commit 7a4f7a82ad32108455de774e2acc33bca68687bb Author: retoor Date: Sun Dec 28 03:14:31 2025 +0100 chore: update c, css, d files diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0b23588 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 DWN Project + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8dc3dae --- /dev/null +++ b/Makefile @@ -0,0 +1,263 @@ +# DWN - Desktop Window Manager +# Simple Makefile - just run 'make' to build! + +# Compiler settings +CC = gcc +CFLAGS = -Wall -Wextra -O2 -I./include +LDFLAGS = -lX11 -lXext -lXinerama -lXrandr -lXft -lfontconfig -ldbus-1 -lcurl -lm -lpthread + +# Directories +SRC_DIR = src +INC_DIR = include +BUILD_DIR = build +BIN_DIR = bin + +# Find all source files automatically +SRCS = $(wildcard $(SRC_DIR)/*.c) +OBJS = $(SRCS:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o) +DEPS = $(OBJS:.o=.d) + +# Output binary +TARGET = $(BIN_DIR)/dwn + +# Installation paths +PREFIX ?= /usr/local +BINDIR = $(PREFIX)/bin +DATADIR = $(PREFIX)/share +SYSCONFDIR = /etc + +# Get pkg-config flags (with error handling) +PKG_CFLAGS := $(shell pkg-config --cflags x11 xext xinerama xrandr xft fontconfig dbus-1 2>/dev/null) +PKG_LIBS := $(shell pkg-config --libs x11 xext xinerama xrandr xft fontconfig dbus-1 2>/dev/null) + +# Use pkg-config if available +ifneq ($(PKG_LIBS),) + LDFLAGS = $(PKG_LIBS) -lcurl -lm -lpthread +endif +ifneq ($(PKG_CFLAGS),) + CFLAGS += $(PKG_CFLAGS) +endif + +# ============================================================================= +# MAIN TARGETS +# ============================================================================= + +.PHONY: all help clean install uninstall debug run test deps check-deps + +# Default target - show help if first time, otherwise build +all: check-deps $(TARGET) + @echo "" + @echo "Build successful! Binary created at: $(TARGET)" + @echo "" + @echo "Next steps:" + @echo " make run - Test DWN in a window (safe, doesn't affect your desktop)" + @echo " make install - Install DWN system-wide" + @echo "" + +# Show help +help: + @echo "==============================================" + @echo " DWN - Desktop Window Manager" + @echo "==============================================" + @echo "" + @echo "QUICK START:" + @echo " make deps - Install required dependencies (Ubuntu/Debian)" + @echo " make - Build DWN" + @echo " make run - Test DWN in a safe window" + @echo " make install - Install to your system" + @echo "" + @echo "ALL COMMANDS:" + @echo " make - Build DWN (release version)" + @echo " make debug - Build with debug symbols" + @echo " make run - Test in Xephyr window (safe!)" + @echo " make install - Install to $(BINDIR)" + @echo " make uninstall- Remove from system" + @echo " make clean - Remove build files" + @echo " make deps - Install dependencies (needs sudo)" + @echo " make help - Show this help" + @echo "" + @echo "AFTER INSTALL:" + @echo " 1. Log out of your current session" + @echo " 2. At login screen, select 'DWN' as your session" + @echo " 3. Log in and enjoy!" + @echo "" + +# ============================================================================= +# BUILD TARGETS +# ============================================================================= + +# Build with debug symbols +debug: CFLAGS += -g -DDEBUG +debug: clean $(TARGET) + @echo "Debug build complete!" + +# Link all object files into final binary +$(TARGET): $(OBJS) | $(BIN_DIR) + @echo "Linking..." + @$(CC) $(OBJS) -o $@ $(LDFLAGS) + +# Compile each source file +$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR) + @echo "Compiling $<..." + @$(CC) $(CFLAGS) -MMD -MP -c $< -o $@ + +# Create build directory +$(BUILD_DIR): + @mkdir -p $(BUILD_DIR) + +# Create bin directory +$(BIN_DIR): + @mkdir -p $(BIN_DIR) + +# Include dependency files +-include $(DEPS) + +# ============================================================================= +# INSTALL / UNINSTALL +# ============================================================================= + +install: $(TARGET) + @echo "Installing DWN..." + @install -Dm755 $(TARGET) $(DESTDIR)$(BINDIR)/dwn + @install -Dm644 scripts/dwn.desktop $(DESTDIR)$(DATADIR)/xsessions/dwn.desktop + @mkdir -p $(DESTDIR)$(SYSCONFDIR)/dwn + @install -Dm644 config/config.example $(DESTDIR)$(SYSCONFDIR)/dwn/config.example + @echo "" + @echo "==============================================" + @echo " Installation complete!" + @echo "==============================================" + @echo "" + @echo "To configure DWN, run:" + @echo " mkdir -p ~/.config/dwn" + @echo " cp $(SYSCONFDIR)/dwn/config.example ~/.config/dwn/config" + @echo "" + @echo "Then log out and select 'DWN' at the login screen!" + @echo "" + +uninstall: + @echo "Removing DWN..." + @rm -f $(DESTDIR)$(BINDIR)/dwn + @rm -f $(DESTDIR)$(DATADIR)/xsessions/dwn.desktop + @rm -rf $(DESTDIR)$(SYSCONFDIR)/dwn + @echo "DWN has been uninstalled." + +# ============================================================================= +# DEVELOPMENT / TESTING +# ============================================================================= + +# Test DWN in a nested X server (safe - doesn't touch your desktop) +run: $(TARGET) + @echo "Starting DWN in test mode..." + @echo "(Press Alt+Shift+Q inside the window to quit)" + Xephyr :1 -screen 1280x720 & + sleep 1 + DISPLAY=:1 $(TARGET) + +# Clean build files +clean: + @echo "Cleaning build files..." + @rm -rf $(BUILD_DIR) $(BIN_DIR) + @echo "Clean complete." + +# ============================================================================= +# DEPENDENCY MANAGEMENT +# ============================================================================= + +# Check if dependencies are installed +check-deps: + @command -v pkg-config >/dev/null 2>&1 || { \ + echo "ERROR: pkg-config is not installed!"; \ + echo "Run: make deps"; \ + exit 1; \ + } + @pkg-config --exists x11 2>/dev/null || { \ + echo "ERROR: X11 development libraries not found!"; \ + echo "Run: make deps"; \ + exit 1; \ + } + @pkg-config --exists dbus-1 2>/dev/null || { \ + echo "ERROR: D-Bus development libraries not found!"; \ + echo "Run: make deps"; \ + exit 1; \ + } + +# Install dependencies (Ubuntu/Debian) +deps: + @echo "Installing dependencies..." + @echo "This requires sudo (administrator) access." + @echo "" + @if command -v apt >/dev/null 2>&1; then \ + sudo apt update && sudo apt install -y \ + build-essential \ + pkg-config \ + libx11-dev \ + libxext-dev \ + libxinerama-dev \ + libxrandr-dev \ + libxft-dev \ + libfontconfig1-dev \ + libdbus-1-dev \ + libcurl4-openssl-dev \ + xserver-xephyr \ + dmenu; \ + echo ""; \ + echo "Dependencies installed successfully!"; \ + echo "Now run: make"; \ + elif command -v dnf >/dev/null 2>&1; then \ + sudo dnf install -y \ + gcc make \ + pkg-config \ + libX11-devel \ + libXext-devel \ + libXinerama-devel \ + libXrandr-devel \ + dbus-devel \ + libcurl-devel \ + xorg-x11-server-Xephyr \ + dmenu; \ + echo ""; \ + echo "Dependencies installed successfully!"; \ + echo "Now run: make"; \ + elif command -v pacman >/dev/null 2>&1; then \ + sudo pacman -S --needed \ + base-devel \ + pkg-config \ + libx11 \ + libxext \ + libxinerama \ + libxrandr \ + dbus \ + curl \ + xorg-server-xephyr \ + dmenu; \ + echo ""; \ + echo "Dependencies installed successfully!"; \ + echo "Now run: make"; \ + else \ + echo "ERROR: Could not detect package manager!"; \ + echo "Please install these packages manually:"; \ + echo " - GCC and Make"; \ + echo " - pkg-config"; \ + echo " - X11, Xext, Xinerama, Xrandr development libraries"; \ + echo " - D-Bus development library"; \ + echo " - libcurl development library"; \ + echo " - Xephyr (for testing)"; \ + echo " - dmenu (for app launcher)"; \ + exit 1; \ + fi + +# ============================================================================= +# CODE QUALITY (for developers) +# ============================================================================= + +.PHONY: format check + +format: + @echo "Formatting code..." + @clang-format -i $(SRC_DIR)/*.c $(INC_DIR)/*.h 2>/dev/null || \ + echo "Note: clang-format not installed (optional)" + +check: + @echo "Running static analysis..." + @cppcheck --enable=all --inconclusive -I$(INC_DIR) $(SRC_DIR) 2>/dev/null || \ + echo "Note: cppcheck not installed (optional)" diff --git a/bin/dwn b/bin/dwn new file mode 100755 index 0000000..02852c2 Binary files /dev/null and b/bin/dwn differ diff --git a/build/ai.d b/build/ai.d new file mode 100644 index 0000000..bf56531 --- /dev/null +++ b/build/ai.d @@ -0,0 +1,46 @@ +build/ai.o: src/ai.c include/ai.h include/dwn.h include/config.h \ + include/client.h include/workspace.h include/notifications.h \ + /usr/include/dbus-1.0/dbus/dbus.h \ + /usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h \ + /usr/include/dbus-1.0/dbus/dbus-macros.h \ + /usr/include/dbus-1.0/dbus/dbus-address.h \ + /usr/include/dbus-1.0/dbus/dbus-types.h \ + /usr/include/dbus-1.0/dbus/dbus-errors.h \ + /usr/include/dbus-1.0/dbus/dbus-protocol.h \ + /usr/include/dbus-1.0/dbus/dbus-bus.h \ + /usr/include/dbus-1.0/dbus/dbus-connection.h \ + /usr/include/dbus-1.0/dbus/dbus-memory.h \ + /usr/include/dbus-1.0/dbus/dbus-message.h \ + /usr/include/dbus-1.0/dbus/dbus-shared.h \ + /usr/include/dbus-1.0/dbus/dbus-misc.h \ + /usr/include/dbus-1.0/dbus/dbus-pending-call.h \ + /usr/include/dbus-1.0/dbus/dbus-server.h \ + /usr/include/dbus-1.0/dbus/dbus-signature.h \ + /usr/include/dbus-1.0/dbus/dbus-syntax.h \ + /usr/include/dbus-1.0/dbus/dbus-threads.h include/util.h include/cJSON.h +include/ai.h: +include/dwn.h: +include/config.h: +include/client.h: +include/workspace.h: +include/notifications.h: +/usr/include/dbus-1.0/dbus/dbus.h: +/usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h: +/usr/include/dbus-1.0/dbus/dbus-macros.h: +/usr/include/dbus-1.0/dbus/dbus-address.h: +/usr/include/dbus-1.0/dbus/dbus-types.h: +/usr/include/dbus-1.0/dbus/dbus-errors.h: +/usr/include/dbus-1.0/dbus/dbus-protocol.h: +/usr/include/dbus-1.0/dbus/dbus-bus.h: +/usr/include/dbus-1.0/dbus/dbus-connection.h: +/usr/include/dbus-1.0/dbus/dbus-memory.h: +/usr/include/dbus-1.0/dbus/dbus-message.h: +/usr/include/dbus-1.0/dbus/dbus-shared.h: +/usr/include/dbus-1.0/dbus/dbus-misc.h: +/usr/include/dbus-1.0/dbus/dbus-pending-call.h: +/usr/include/dbus-1.0/dbus/dbus-server.h: +/usr/include/dbus-1.0/dbus/dbus-signature.h: +/usr/include/dbus-1.0/dbus/dbus-syntax.h: +/usr/include/dbus-1.0/dbus/dbus-threads.h: +include/util.h: +include/cJSON.h: diff --git a/build/ai.o b/build/ai.o new file mode 100644 index 0000000..99bdaca Binary files /dev/null and b/build/ai.o differ diff --git a/build/applauncher.d b/build/applauncher.d new file mode 100644 index 0000000..44031aa --- /dev/null +++ b/build/applauncher.d @@ -0,0 +1,6 @@ +build/applauncher.o: src/applauncher.c include/applauncher.h \ + include/config.h include/dwn.h include/util.h +include/applauncher.h: +include/config.h: +include/dwn.h: +include/util.h: diff --git a/build/applauncher.o b/build/applauncher.o new file mode 100644 index 0000000..c910e13 Binary files /dev/null and b/build/applauncher.o differ diff --git a/build/atoms.d b/build/atoms.d new file mode 100644 index 0000000..3c1b1a1 --- /dev/null +++ b/build/atoms.d @@ -0,0 +1,4 @@ +build/atoms.o: src/atoms.c include/atoms.h include/dwn.h include/util.h +include/atoms.h: +include/dwn.h: +include/util.h: diff --git a/build/atoms.o b/build/atoms.o new file mode 100644 index 0000000..bfcc609 Binary files /dev/null and b/build/atoms.o differ diff --git a/build/cJSON.d b/build/cJSON.d new file mode 100644 index 0000000..68522b3 --- /dev/null +++ b/build/cJSON.d @@ -0,0 +1,2 @@ +build/cJSON.o: src/cJSON.c include/cJSON.h +include/cJSON.h: diff --git a/build/cJSON.o b/build/cJSON.o new file mode 100644 index 0000000..f835fcc Binary files /dev/null and b/build/cJSON.o differ diff --git a/build/client.d b/build/client.d new file mode 100644 index 0000000..45f07aa --- /dev/null +++ b/build/client.d @@ -0,0 +1,47 @@ +build/client.o: src/client.c include/client.h include/dwn.h \ + include/atoms.h include/config.h include/util.h include/workspace.h \ + include/decorations.h include/notifications.h \ + /usr/include/dbus-1.0/dbus/dbus.h \ + /usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h \ + /usr/include/dbus-1.0/dbus/dbus-macros.h \ + /usr/include/dbus-1.0/dbus/dbus-address.h \ + /usr/include/dbus-1.0/dbus/dbus-types.h \ + /usr/include/dbus-1.0/dbus/dbus-errors.h \ + /usr/include/dbus-1.0/dbus/dbus-protocol.h \ + /usr/include/dbus-1.0/dbus/dbus-bus.h \ + /usr/include/dbus-1.0/dbus/dbus-connection.h \ + /usr/include/dbus-1.0/dbus/dbus-memory.h \ + /usr/include/dbus-1.0/dbus/dbus-message.h \ + /usr/include/dbus-1.0/dbus/dbus-shared.h \ + /usr/include/dbus-1.0/dbus/dbus-misc.h \ + /usr/include/dbus-1.0/dbus/dbus-pending-call.h \ + /usr/include/dbus-1.0/dbus/dbus-server.h \ + /usr/include/dbus-1.0/dbus/dbus-signature.h \ + /usr/include/dbus-1.0/dbus/dbus-syntax.h \ + /usr/include/dbus-1.0/dbus/dbus-threads.h +include/client.h: +include/dwn.h: +include/atoms.h: +include/config.h: +include/util.h: +include/workspace.h: +include/decorations.h: +include/notifications.h: +/usr/include/dbus-1.0/dbus/dbus.h: +/usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h: +/usr/include/dbus-1.0/dbus/dbus-macros.h: +/usr/include/dbus-1.0/dbus/dbus-address.h: +/usr/include/dbus-1.0/dbus/dbus-types.h: +/usr/include/dbus-1.0/dbus/dbus-errors.h: +/usr/include/dbus-1.0/dbus/dbus-protocol.h: +/usr/include/dbus-1.0/dbus/dbus-bus.h: +/usr/include/dbus-1.0/dbus/dbus-connection.h: +/usr/include/dbus-1.0/dbus/dbus-memory.h: +/usr/include/dbus-1.0/dbus/dbus-message.h: +/usr/include/dbus-1.0/dbus/dbus-shared.h: +/usr/include/dbus-1.0/dbus/dbus-misc.h: +/usr/include/dbus-1.0/dbus/dbus-pending-call.h: +/usr/include/dbus-1.0/dbus/dbus-server.h: +/usr/include/dbus-1.0/dbus/dbus-signature.h: +/usr/include/dbus-1.0/dbus/dbus-syntax.h: +/usr/include/dbus-1.0/dbus/dbus-threads.h: diff --git a/build/client.o b/build/client.o new file mode 100644 index 0000000..abf18c7 Binary files /dev/null and b/build/client.o differ diff --git a/build/config.d b/build/config.d new file mode 100644 index 0000000..b243385 --- /dev/null +++ b/build/config.d @@ -0,0 +1,5 @@ +build/config.o: src/config.c include/config.h include/dwn.h \ + include/util.h +include/config.h: +include/dwn.h: +include/util.h: diff --git a/build/config.o b/build/config.o new file mode 100644 index 0000000..4d7415e Binary files /dev/null and b/build/config.o differ diff --git a/build/decorations.d b/build/decorations.d new file mode 100644 index 0000000..cd162ed --- /dev/null +++ b/build/decorations.d @@ -0,0 +1,7 @@ +build/decorations.o: src/decorations.c include/decorations.h \ + include/dwn.h include/client.h include/config.h include/util.h +include/decorations.h: +include/dwn.h: +include/client.h: +include/config.h: +include/util.h: diff --git a/build/decorations.o b/build/decorations.o new file mode 100644 index 0000000..9e3a5b6 Binary files /dev/null and b/build/decorations.o differ diff --git a/build/keys.d b/build/keys.d new file mode 100644 index 0000000..885058c --- /dev/null +++ b/build/keys.d @@ -0,0 +1,49 @@ +build/keys.o: src/keys.c include/keys.h include/dwn.h include/client.h \ + include/workspace.h include/config.h include/util.h include/ai.h \ + include/notifications.h /usr/include/dbus-1.0/dbus/dbus.h \ + /usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h \ + /usr/include/dbus-1.0/dbus/dbus-macros.h \ + /usr/include/dbus-1.0/dbus/dbus-address.h \ + /usr/include/dbus-1.0/dbus/dbus-types.h \ + /usr/include/dbus-1.0/dbus/dbus-errors.h \ + /usr/include/dbus-1.0/dbus/dbus-protocol.h \ + /usr/include/dbus-1.0/dbus/dbus-bus.h \ + /usr/include/dbus-1.0/dbus/dbus-connection.h \ + /usr/include/dbus-1.0/dbus/dbus-memory.h \ + /usr/include/dbus-1.0/dbus/dbus-message.h \ + /usr/include/dbus-1.0/dbus/dbus-shared.h \ + /usr/include/dbus-1.0/dbus/dbus-misc.h \ + /usr/include/dbus-1.0/dbus/dbus-pending-call.h \ + /usr/include/dbus-1.0/dbus/dbus-server.h \ + /usr/include/dbus-1.0/dbus/dbus-signature.h \ + /usr/include/dbus-1.0/dbus/dbus-syntax.h \ + /usr/include/dbus-1.0/dbus/dbus-threads.h include/news.h \ + include/applauncher.h +include/keys.h: +include/dwn.h: +include/client.h: +include/workspace.h: +include/config.h: +include/util.h: +include/ai.h: +include/notifications.h: +/usr/include/dbus-1.0/dbus/dbus.h: +/usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h: +/usr/include/dbus-1.0/dbus/dbus-macros.h: +/usr/include/dbus-1.0/dbus/dbus-address.h: +/usr/include/dbus-1.0/dbus/dbus-types.h: +/usr/include/dbus-1.0/dbus/dbus-errors.h: +/usr/include/dbus-1.0/dbus/dbus-protocol.h: +/usr/include/dbus-1.0/dbus/dbus-bus.h: +/usr/include/dbus-1.0/dbus/dbus-connection.h: +/usr/include/dbus-1.0/dbus/dbus-memory.h: +/usr/include/dbus-1.0/dbus/dbus-message.h: +/usr/include/dbus-1.0/dbus/dbus-shared.h: +/usr/include/dbus-1.0/dbus/dbus-misc.h: +/usr/include/dbus-1.0/dbus/dbus-pending-call.h: +/usr/include/dbus-1.0/dbus/dbus-server.h: +/usr/include/dbus-1.0/dbus/dbus-signature.h: +/usr/include/dbus-1.0/dbus/dbus-syntax.h: +/usr/include/dbus-1.0/dbus/dbus-threads.h: +include/news.h: +include/applauncher.h: diff --git a/build/keys.o b/build/keys.o new file mode 100644 index 0000000..d1c4f08 Binary files /dev/null and b/build/keys.o differ diff --git a/build/layout.d b/build/layout.d new file mode 100644 index 0000000..6bd55f2 --- /dev/null +++ b/build/layout.d @@ -0,0 +1,8 @@ +build/layout.o: src/layout.c include/layout.h include/dwn.h \ + include/client.h include/workspace.h include/config.h include/util.h +include/layout.h: +include/dwn.h: +include/client.h: +include/workspace.h: +include/config.h: +include/util.h: diff --git a/build/layout.o b/build/layout.o new file mode 100644 index 0000000..f1dda5a Binary files /dev/null and b/build/layout.o differ diff --git a/build/main.d b/build/main.d new file mode 100644 index 0000000..1fdc703 --- /dev/null +++ b/build/main.d @@ -0,0 +1,56 @@ +build/main.o: src/main.c include/dwn.h include/config.h include/dwn.h \ + include/atoms.h include/client.h include/workspace.h include/layout.h \ + include/decorations.h include/panel.h include/keys.h \ + include/notifications.h /usr/include/dbus-1.0/dbus/dbus.h \ + /usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h \ + /usr/include/dbus-1.0/dbus/dbus-macros.h \ + /usr/include/dbus-1.0/dbus/dbus-address.h \ + /usr/include/dbus-1.0/dbus/dbus-types.h \ + /usr/include/dbus-1.0/dbus/dbus-errors.h \ + /usr/include/dbus-1.0/dbus/dbus-protocol.h \ + /usr/include/dbus-1.0/dbus/dbus-bus.h \ + /usr/include/dbus-1.0/dbus/dbus-connection.h \ + /usr/include/dbus-1.0/dbus/dbus-memory.h \ + /usr/include/dbus-1.0/dbus/dbus-message.h \ + /usr/include/dbus-1.0/dbus/dbus-shared.h \ + /usr/include/dbus-1.0/dbus/dbus-misc.h \ + /usr/include/dbus-1.0/dbus/dbus-pending-call.h \ + /usr/include/dbus-1.0/dbus/dbus-server.h \ + /usr/include/dbus-1.0/dbus/dbus-signature.h \ + /usr/include/dbus-1.0/dbus/dbus-syntax.h \ + /usr/include/dbus-1.0/dbus/dbus-threads.h include/systray.h \ + include/news.h include/applauncher.h include/ai.h include/util.h +include/dwn.h: +include/config.h: +include/dwn.h: +include/atoms.h: +include/client.h: +include/workspace.h: +include/layout.h: +include/decorations.h: +include/panel.h: +include/keys.h: +include/notifications.h: +/usr/include/dbus-1.0/dbus/dbus.h: +/usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h: +/usr/include/dbus-1.0/dbus/dbus-macros.h: +/usr/include/dbus-1.0/dbus/dbus-address.h: +/usr/include/dbus-1.0/dbus/dbus-types.h: +/usr/include/dbus-1.0/dbus/dbus-errors.h: +/usr/include/dbus-1.0/dbus/dbus-protocol.h: +/usr/include/dbus-1.0/dbus/dbus-bus.h: +/usr/include/dbus-1.0/dbus/dbus-connection.h: +/usr/include/dbus-1.0/dbus/dbus-memory.h: +/usr/include/dbus-1.0/dbus/dbus-message.h: +/usr/include/dbus-1.0/dbus/dbus-shared.h: +/usr/include/dbus-1.0/dbus/dbus-misc.h: +/usr/include/dbus-1.0/dbus/dbus-pending-call.h: +/usr/include/dbus-1.0/dbus/dbus-server.h: +/usr/include/dbus-1.0/dbus/dbus-signature.h: +/usr/include/dbus-1.0/dbus/dbus-syntax.h: +/usr/include/dbus-1.0/dbus/dbus-threads.h: +include/systray.h: +include/news.h: +include/applauncher.h: +include/ai.h: +include/util.h: diff --git a/build/main.o b/build/main.o new file mode 100644 index 0000000..939e7bc Binary files /dev/null and b/build/main.o differ diff --git a/build/news.d b/build/news.d new file mode 100644 index 0000000..aae321d --- /dev/null +++ b/build/news.d @@ -0,0 +1,8 @@ +build/news.o: src/news.c include/news.h include/dwn.h include/panel.h \ + include/config.h include/util.h include/cJSON.h +include/news.h: +include/dwn.h: +include/panel.h: +include/config.h: +include/util.h: +include/cJSON.h: diff --git a/build/news.o b/build/news.o new file mode 100644 index 0000000..55a3472 Binary files /dev/null and b/build/news.o differ diff --git a/build/notifications.d b/build/notifications.d new file mode 100644 index 0000000..5195c48 --- /dev/null +++ b/build/notifications.d @@ -0,0 +1,42 @@ +build/notifications.o: src/notifications.c include/notifications.h \ + include/dwn.h /usr/include/dbus-1.0/dbus/dbus.h \ + /usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h \ + /usr/include/dbus-1.0/dbus/dbus-macros.h \ + /usr/include/dbus-1.0/dbus/dbus-address.h \ + /usr/include/dbus-1.0/dbus/dbus-types.h \ + /usr/include/dbus-1.0/dbus/dbus-errors.h \ + /usr/include/dbus-1.0/dbus/dbus-protocol.h \ + /usr/include/dbus-1.0/dbus/dbus-bus.h \ + /usr/include/dbus-1.0/dbus/dbus-connection.h \ + /usr/include/dbus-1.0/dbus/dbus-memory.h \ + /usr/include/dbus-1.0/dbus/dbus-message.h \ + /usr/include/dbus-1.0/dbus/dbus-shared.h \ + /usr/include/dbus-1.0/dbus/dbus-misc.h \ + /usr/include/dbus-1.0/dbus/dbus-pending-call.h \ + /usr/include/dbus-1.0/dbus/dbus-server.h \ + /usr/include/dbus-1.0/dbus/dbus-signature.h \ + /usr/include/dbus-1.0/dbus/dbus-syntax.h \ + /usr/include/dbus-1.0/dbus/dbus-threads.h include/config.h \ + include/util.h +include/notifications.h: +include/dwn.h: +/usr/include/dbus-1.0/dbus/dbus.h: +/usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h: +/usr/include/dbus-1.0/dbus/dbus-macros.h: +/usr/include/dbus-1.0/dbus/dbus-address.h: +/usr/include/dbus-1.0/dbus/dbus-types.h: +/usr/include/dbus-1.0/dbus/dbus-errors.h: +/usr/include/dbus-1.0/dbus/dbus-protocol.h: +/usr/include/dbus-1.0/dbus/dbus-bus.h: +/usr/include/dbus-1.0/dbus/dbus-connection.h: +/usr/include/dbus-1.0/dbus/dbus-memory.h: +/usr/include/dbus-1.0/dbus/dbus-message.h: +/usr/include/dbus-1.0/dbus/dbus-shared.h: +/usr/include/dbus-1.0/dbus/dbus-misc.h: +/usr/include/dbus-1.0/dbus/dbus-pending-call.h: +/usr/include/dbus-1.0/dbus/dbus-server.h: +/usr/include/dbus-1.0/dbus/dbus-signature.h: +/usr/include/dbus-1.0/dbus/dbus-syntax.h: +/usr/include/dbus-1.0/dbus/dbus-threads.h: +include/config.h: +include/util.h: diff --git a/build/notifications.o b/build/notifications.o new file mode 100644 index 0000000..83bacb5 Binary files /dev/null and b/build/notifications.o differ diff --git a/build/panel.d b/build/panel.d new file mode 100644 index 0000000..e028e4b --- /dev/null +++ b/build/panel.d @@ -0,0 +1,13 @@ +build/panel.o: src/panel.c include/panel.h include/dwn.h \ + include/workspace.h include/layout.h include/client.h include/config.h \ + include/util.h include/atoms.h include/systray.h include/news.h +include/panel.h: +include/dwn.h: +include/workspace.h: +include/layout.h: +include/client.h: +include/config.h: +include/util.h: +include/atoms.h: +include/systray.h: +include/news.h: diff --git a/build/panel.o b/build/panel.o new file mode 100644 index 0000000..3e45e4a Binary files /dev/null and b/build/panel.o differ diff --git a/build/systray.d b/build/systray.d new file mode 100644 index 0000000..99b72c6 --- /dev/null +++ b/build/systray.d @@ -0,0 +1,44 @@ +build/systray.o: src/systray.c include/systray.h include/dwn.h \ + include/panel.h include/config.h include/util.h include/notifications.h \ + /usr/include/dbus-1.0/dbus/dbus.h \ + /usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h \ + /usr/include/dbus-1.0/dbus/dbus-macros.h \ + /usr/include/dbus-1.0/dbus/dbus-address.h \ + /usr/include/dbus-1.0/dbus/dbus-types.h \ + /usr/include/dbus-1.0/dbus/dbus-errors.h \ + /usr/include/dbus-1.0/dbus/dbus-protocol.h \ + /usr/include/dbus-1.0/dbus/dbus-bus.h \ + /usr/include/dbus-1.0/dbus/dbus-connection.h \ + /usr/include/dbus-1.0/dbus/dbus-memory.h \ + /usr/include/dbus-1.0/dbus/dbus-message.h \ + /usr/include/dbus-1.0/dbus/dbus-shared.h \ + /usr/include/dbus-1.0/dbus/dbus-misc.h \ + /usr/include/dbus-1.0/dbus/dbus-pending-call.h \ + /usr/include/dbus-1.0/dbus/dbus-server.h \ + /usr/include/dbus-1.0/dbus/dbus-signature.h \ + /usr/include/dbus-1.0/dbus/dbus-syntax.h \ + /usr/include/dbus-1.0/dbus/dbus-threads.h +include/systray.h: +include/dwn.h: +include/panel.h: +include/config.h: +include/util.h: +include/notifications.h: +/usr/include/dbus-1.0/dbus/dbus.h: +/usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h: +/usr/include/dbus-1.0/dbus/dbus-macros.h: +/usr/include/dbus-1.0/dbus/dbus-address.h: +/usr/include/dbus-1.0/dbus/dbus-types.h: +/usr/include/dbus-1.0/dbus/dbus-errors.h: +/usr/include/dbus-1.0/dbus/dbus-protocol.h: +/usr/include/dbus-1.0/dbus/dbus-bus.h: +/usr/include/dbus-1.0/dbus/dbus-connection.h: +/usr/include/dbus-1.0/dbus/dbus-memory.h: +/usr/include/dbus-1.0/dbus/dbus-message.h: +/usr/include/dbus-1.0/dbus/dbus-shared.h: +/usr/include/dbus-1.0/dbus/dbus-misc.h: +/usr/include/dbus-1.0/dbus/dbus-pending-call.h: +/usr/include/dbus-1.0/dbus/dbus-server.h: +/usr/include/dbus-1.0/dbus/dbus-signature.h: +/usr/include/dbus-1.0/dbus/dbus-syntax.h: +/usr/include/dbus-1.0/dbus/dbus-threads.h: diff --git a/build/systray.o b/build/systray.o new file mode 100644 index 0000000..12fc721 Binary files /dev/null and b/build/systray.o differ diff --git a/build/util.d b/build/util.d new file mode 100644 index 0000000..2702406 --- /dev/null +++ b/build/util.d @@ -0,0 +1,3 @@ +build/util.o: src/util.c include/util.h include/dwn.h +include/util.h: +include/dwn.h: diff --git a/build/util.o b/build/util.o new file mode 100644 index 0000000..5f6514e Binary files /dev/null and b/build/util.o differ diff --git a/build/workspace.d b/build/workspace.d new file mode 100644 index 0000000..bf10441 --- /dev/null +++ b/build/workspace.d @@ -0,0 +1,10 @@ +build/workspace.o: src/workspace.c include/workspace.h include/dwn.h \ + include/client.h include/layout.h include/atoms.h include/util.h \ + include/config.h +include/workspace.h: +include/dwn.h: +include/client.h: +include/layout.h: +include/atoms.h: +include/util.h: +include/config.h: diff --git a/build/workspace.o b/build/workspace.o new file mode 100644 index 0000000..cf10d4b Binary files /dev/null and b/build/workspace.o differ diff --git a/config/config.example b/config/config.example new file mode 100644 index 0000000..c53dcbe --- /dev/null +++ b/config/config.example @@ -0,0 +1,93 @@ +# DWN - Desktop Window Manager Configuration +# Copy this file to ~/.config/dwn/config + +[general] +# Terminal emulator (default: xfce4-terminal) +terminal = xfce4-terminal + +# Application launcher (default: dmenu_run) +launcher = dmenu_run + +# File manager (default: thunar) +file_manager = thunar + +# Focus mode: click or follow (default: click) +focus_mode = click + +# Show window decorations (default: true) +decorations = true + +[appearance] +# Border width in pixels (default: 2) +border_width = 2 + +# Title bar height in pixels (default: 24) +title_height = 24 + +# Panel height in pixels (default: 28) +panel_height = 28 + +# Gap between windows in pixels (default: 4) +gap = 4 + +# Font for titles and panels (default: fixed) +# Use xlsfonts to list available fonts +font = -misc-fixed-medium-r-normal--13-*-*-*-*-*-iso8859-1 + +[layout] +# Default layout: tiling, floating, or monocle (default: tiling) +default = tiling + +# Master area ratio (0.1 to 0.9, default: 0.55) +master_ratio = 0.55 + +# Number of windows in master area (default: 1) +master_count = 1 + +[panels] +# Enable top panel (default: true) +top = true + +# Enable bottom panel (default: true) +bottom = true + +[colors] +# All colors in hex format (#RRGGBB) + +# Panel colors +panel_bg = #1a1a2e +panel_fg = #e0e0e0 + +# Workspace indicator colors +workspace_active = #4a90d9 +workspace_inactive = #3a3a4e +workspace_urgent = #d94a4a + +# Window title bar colors +title_focused_bg = #2d3a4a +title_focused_fg = #ffffff +title_unfocused_bg = #1a1a1a +title_unfocused_fg = #808080 + +# Window border colors +border_focused = #4a90d9 +border_unfocused = #333333 + +# Notification colors +notification_bg = #2a2a3e +notification_fg = #ffffff + +[ai] +# AI model to use (default: google/gemini-2.0-flash-exp:free) +# See https://openrouter.ai/models for available models +model = google/gemini-2.0-flash-exp:free + +# OpenRouter API key for AI features (Super+Shift+A) +# Can also be set via OPENROUTER_API_KEY environment variable +# Sign up and get your key at: https://openrouter.ai/keys +# openrouter_api_key = sk-or-v1-your-key-here + +# Exa API key for semantic web search (Super+Shift+E) +# Can also be set via EXA_API_KEY environment variable +# Sign up and get your key at: https://dashboard.exa.ai/api-keys +# exa_api_key = your-exa-key-here diff --git a/include/ai.h b/include/ai.h new file mode 100644 index 0000000..a41ac02 --- /dev/null +++ b/include/ai.h @@ -0,0 +1,94 @@ +/* + * DWN - Desktop Window Manager + * AI Integration (OpenRouter API) + */ + +#ifndef DWN_AI_H +#define DWN_AI_H + +#include "dwn.h" +#include + +/* AI request states */ +typedef enum { + AI_STATE_IDLE, + AI_STATE_PENDING, + AI_STATE_COMPLETED, + AI_STATE_ERROR +} AIState; + +/* AI request structure */ +typedef struct AIRequest { + char *prompt; + char *response; + AIState state; + void (*callback)(struct AIRequest *req); + void *user_data; + struct AIRequest *next; +} AIRequest; + +/* AI context for window analysis */ +typedef struct { + char focused_window[256]; + char focused_class[64]; + char workspace_windows[1024]; + int window_count; +} AIContext; + +/* Initialization */ +bool ai_init(void); +void ai_cleanup(void); +bool ai_is_available(void); + +/* API calls */ +AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *)); +void ai_cancel_request(AIRequest *req); +void ai_process_pending(void); + +/* Context analysis */ +void ai_update_context(void); +const char *ai_analyze_task(void); +const char *ai_suggest_window(void); +const char *ai_suggest_app(void); + +/* Command palette */ +void ai_show_command_palette(void); +void ai_execute_command(const char *command); + +/* Smart features */ +void ai_auto_organize_workspace(void); +void ai_suggest_layout(void); +void ai_analyze_workflow(void); + +/* Notification intelligence */ +bool ai_should_show_notification(const char *app, const char *summary); +int ai_notification_priority(const char *app, const char *summary); + +/* Performance monitoring */ +void ai_monitor_performance(void); +const char *ai_performance_suggestion(void); + +/* Exa semantic search */ +typedef struct { + char title[256]; + char url[512]; + char snippet[1024]; + float score; +} ExaSearchResult; + +typedef struct ExaRequest { + char *query; + ExaSearchResult results[10]; + int result_count; + int state; /* Uses AIState enum */ + void (*callback)(struct ExaRequest *req); + void *user_data; + struct ExaRequest *next; +} ExaRequest; + +bool exa_is_available(void); +ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *)); +void exa_process_pending(void); +void exa_show_app_launcher(void); + +#endif /* DWN_AI_H */ diff --git a/include/applauncher.h b/include/applauncher.h new file mode 100644 index 0000000..1ef5f2a --- /dev/null +++ b/include/applauncher.h @@ -0,0 +1,48 @@ +/* + * DWN - Desktop Window Manager + * Application launcher with .desktop file support + */ + +#ifndef DWN_APPLAUNCHER_H +#define DWN_APPLAUNCHER_H + +#include + +/* Maximum applications to track */ +#define MAX_APPS 512 +#define MAX_RECENT_APPS 10 + +/* Application entry from .desktop file */ +typedef struct { + char name[128]; /* Display name */ + char exec[512]; /* Command to execute */ + char icon[128]; /* Icon name (unused for now) */ + char desktop_id[256]; /* Desktop file basename for tracking */ + bool terminal; /* Run in terminal */ + bool hidden; /* Should be hidden */ +} AppEntry; + +/* Application launcher state */ +typedef struct { + AppEntry apps[MAX_APPS]; + int app_count; + char recent[MAX_RECENT_APPS][256]; /* Desktop IDs of recent apps */ + int recent_count; +} AppLauncherState; + +/* Initialize the app launcher (scans .desktop files) */ +void applauncher_init(void); + +/* Cleanup resources */ +void applauncher_cleanup(void); + +/* Rescan .desktop files */ +void applauncher_refresh(void); + +/* Show the application launcher (dmenu-based) */ +void applauncher_show(void); + +/* Launch an application by desktop_id */ +void applauncher_launch(const char *desktop_id); + +#endif /* DWN_APPLAUNCHER_H */ diff --git a/include/atoms.h b/include/atoms.h new file mode 100644 index 0000000..4bd88fd --- /dev/null +++ b/include/atoms.h @@ -0,0 +1,148 @@ +/* + * DWN - Desktop Window Manager + * X11 Atoms management (EWMH/ICCCM compliance) + */ + +#ifndef DWN_ATOMS_H +#define DWN_ATOMS_H + +#include +#include + +/* EWMH (Extended Window Manager Hints) atoms */ +typedef struct { + /* Root window properties */ + Atom NET_SUPPORTED; + Atom NET_SUPPORTING_WM_CHECK; + Atom NET_CLIENT_LIST; + Atom NET_CLIENT_LIST_STACKING; + Atom NET_NUMBER_OF_DESKTOPS; + Atom NET_DESKTOP_GEOMETRY; + Atom NET_DESKTOP_VIEWPORT; + Atom NET_CURRENT_DESKTOP; + Atom NET_DESKTOP_NAMES; + Atom NET_ACTIVE_WINDOW; + Atom NET_WORKAREA; + + /* Client window properties */ + Atom NET_WM_NAME; + Atom NET_WM_VISIBLE_NAME; + Atom NET_WM_DESKTOP; + Atom NET_WM_WINDOW_TYPE; + Atom NET_WM_STATE; + Atom NET_WM_ALLOWED_ACTIONS; + Atom NET_WM_STRUT; + Atom NET_WM_STRUT_PARTIAL; + Atom NET_WM_PID; + + /* Window types */ + Atom NET_WM_WINDOW_TYPE_DESKTOP; + Atom NET_WM_WINDOW_TYPE_DOCK; + Atom NET_WM_WINDOW_TYPE_TOOLBAR; + Atom NET_WM_WINDOW_TYPE_MENU; + Atom NET_WM_WINDOW_TYPE_UTILITY; + Atom NET_WM_WINDOW_TYPE_SPLASH; + Atom NET_WM_WINDOW_TYPE_DIALOG; + Atom NET_WM_WINDOW_TYPE_NORMAL; + Atom NET_WM_WINDOW_TYPE_NOTIFICATION; + + /* Window states */ + Atom NET_WM_STATE_MODAL; + Atom NET_WM_STATE_STICKY; + Atom NET_WM_STATE_MAXIMIZED_VERT; + Atom NET_WM_STATE_MAXIMIZED_HORZ; + Atom NET_WM_STATE_SHADED; + Atom NET_WM_STATE_SKIP_TASKBAR; + Atom NET_WM_STATE_SKIP_PAGER; + Atom NET_WM_STATE_HIDDEN; + Atom NET_WM_STATE_FULLSCREEN; + Atom NET_WM_STATE_ABOVE; + Atom NET_WM_STATE_BELOW; + Atom NET_WM_STATE_DEMANDS_ATTENTION; + Atom NET_WM_STATE_FOCUSED; + + /* Actions */ + Atom NET_WM_ACTION_MOVE; + Atom NET_WM_ACTION_RESIZE; + Atom NET_WM_ACTION_MINIMIZE; + Atom NET_WM_ACTION_SHADE; + Atom NET_WM_ACTION_STICK; + Atom NET_WM_ACTION_MAXIMIZE_HORZ; + Atom NET_WM_ACTION_MAXIMIZE_VERT; + Atom NET_WM_ACTION_FULLSCREEN; + Atom NET_WM_ACTION_CHANGE_DESKTOP; + Atom NET_WM_ACTION_CLOSE; + + /* Client messages */ + Atom NET_CLOSE_WINDOW; + Atom NET_MOVERESIZE_WINDOW; + Atom NET_WM_MOVERESIZE; + Atom NET_REQUEST_FRAME_EXTENTS; + Atom NET_FRAME_EXTENTS; + + /* System tray */ + Atom NET_SYSTEM_TRAY_OPCODE; + Atom NET_SYSTEM_TRAY_S0; + Atom MANAGER; + Atom XEMBED; + Atom XEMBED_INFO; +} EWMHAtoms; + +/* ICCCM (Inter-Client Communication Conventions Manual) atoms */ +typedef struct { + Atom WM_PROTOCOLS; + Atom WM_DELETE_WINDOW; + Atom WM_TAKE_FOCUS; + Atom WM_STATE; + Atom WM_CHANGE_STATE; + Atom WM_CLASS; + Atom WM_NAME; + Atom WM_TRANSIENT_FOR; + Atom WM_CLIENT_LEADER; + Atom WM_WINDOW_ROLE; +} ICCCMAtoms; + +/* Other useful atoms */ +typedef struct { + Atom UTF8_STRING; + Atom COMPOUND_TEXT; + Atom MOTIF_WM_HINTS; + Atom CLIPBOARD; + Atom PRIMARY; + Atom DWN_RESTART; +} MiscAtoms; + +/* Global atom containers */ +extern EWMHAtoms ewmh; +extern ICCCMAtoms icccm; +extern MiscAtoms misc_atoms; + +/* Initialization */ +void atoms_init(Display *display); + +/* EWMH root window setup */ +void atoms_setup_ewmh(void); +void atoms_update_client_list(void); +void atoms_update_desktop_names(void); +void atoms_set_current_desktop(int desktop); +void atoms_set_active_window(Window window); +void atoms_set_number_of_desktops(int count); + +/* Window property helpers */ +bool atoms_get_window_type(Window window, Atom *type); +bool atoms_get_window_state(Window window, Atom **states, int *count); +bool atoms_set_window_state(Window window, Atom *states, int count); +bool atoms_get_window_desktop(Window window, int *desktop); +bool atoms_set_window_desktop(Window window, int desktop); +char *atoms_get_window_name(Window window); +bool atoms_get_wm_class(Window window, char *class_name, char *instance_name, size_t len); + +/* Protocol helpers */ +bool atoms_window_supports_protocol(Window window, Atom protocol); +void atoms_send_protocol(Window window, Atom protocol, Time timestamp); + +/* Client message sending */ +void atoms_send_client_message(Window window, Atom message_type, + long data0, long data1, long data2, long data3, long data4); + +#endif /* DWN_ATOMS_H */ diff --git a/include/cJSON.h b/include/cJSON.h new file mode 100644 index 0000000..cab5feb --- /dev/null +++ b/include/cJSON.h @@ -0,0 +1,306 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef cJSON__h +#define cJSON__h + +#ifdef __cplusplus +extern "C" +{ +#endif + +#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32)) +#define __WINDOWS__ +#endif + +#ifdef __WINDOWS__ + +/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options: + +CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols +CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default) +CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol + +For *nix builds that support visibility attribute, you can define similar behavior by + +setting default visibility to hidden by adding +-fvisibility=hidden (for gcc) +or +-xldscope=hidden (for sun cc) +to CFLAGS + +then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does + +*/ + +#define CJSON_CDECL __cdecl +#define CJSON_STDCALL __stdcall + +/* export symbols by default, this is necessary for copy pasting the C and header file */ +#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_EXPORT_SYMBOLS +#endif + +#if defined(CJSON_HIDE_SYMBOLS) +#define CJSON_PUBLIC(type) type CJSON_STDCALL +#elif defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL +#elif defined(CJSON_IMPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL +#endif +#else /* !__WINDOWS__ */ +#define CJSON_CDECL +#define CJSON_STDCALL + +#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY) +#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type +#else +#define CJSON_PUBLIC(type) type +#endif +#endif + +/* project version */ +#define CJSON_VERSION_MAJOR 1 +#define CJSON_VERSION_MINOR 7 +#define CJSON_VERSION_PATCH 19 + +#include + +/* cJSON Types: */ +#define cJSON_Invalid (0) +#define cJSON_False (1 << 0) +#define cJSON_True (1 << 1) +#define cJSON_NULL (1 << 2) +#define cJSON_Number (1 << 3) +#define cJSON_String (1 << 4) +#define cJSON_Array (1 << 5) +#define cJSON_Object (1 << 6) +#define cJSON_Raw (1 << 7) /* raw json */ + +#define cJSON_IsReference 256 +#define cJSON_StringIsConst 512 + +/* The cJSON structure: */ +typedef struct cJSON +{ + /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */ + struct cJSON *next; + struct cJSON *prev; + /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */ + struct cJSON *child; + + /* The type of the item, as above. */ + int type; + + /* The item's string, if type==cJSON_String and type == cJSON_Raw */ + char *valuestring; + /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */ + int valueint; + /* The item's number, if type==cJSON_Number */ + double valuedouble; + + /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ + char *string; +} cJSON; + +typedef struct cJSON_Hooks +{ + /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */ + void *(CJSON_CDECL *malloc_fn)(size_t sz); + void (CJSON_CDECL *free_fn)(void *ptr); +} cJSON_Hooks; + +typedef int cJSON_bool; + +/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them. + * This is to prevent stack overflows. */ +#ifndef CJSON_NESTING_LIMIT +#define CJSON_NESTING_LIMIT 1000 +#endif + +/* Limits the length of circular references can be before cJSON rejects to parse them. + * This is to prevent stack overflows. */ +#ifndef CJSON_CIRCULAR_LIMIT +#define CJSON_CIRCULAR_LIMIT 10000 +#endif + +/* returns the version of cJSON as a string */ +CJSON_PUBLIC(const char*) cJSON_Version(void); + +/* Supply malloc, realloc and free functions to cJSON */ +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks); + +/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */ +/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length); +/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ +/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated); + +/* Render a cJSON entity to text for transfer/storage. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item); +/* Render a cJSON entity to text for transfer/storage without any formatting. */ +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item); +/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */ +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt); +/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */ +/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */ +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format); +/* Delete a cJSON entity and all subentities. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item); + +/* Returns the number of items in an array (or object). */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array); +/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */ +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index); +/* Get item "string" from object. Case insensitive. */ +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string); +/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */ +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void); + +/* Check item type and return its value */ +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item); +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item); + +/* These functions check the type of an item */ +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item); + +/* These calls create a cJSON item of the appropriate type. */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean); +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num); +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string); +/* raw json */ +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw); +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void); + +/* Create a string where valuestring references a string so + * it will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string); +/* Create an object/array that only references it's elements so + * they will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child); +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child); + +/* These utilities create an Array of count items. + * The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count); + +/* Append item to the specified array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item); +/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object. + * WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before + * writing to `item->string` */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item); +/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item); + +/* Remove/Detach items from Arrays/Objects. */ +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string); + +/* Update array items. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */ +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem); + +/* Duplicate a cJSON item */ +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse); +/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will + * need to be released. With recurse!=0, it will duplicate any children connected to the item. + * The item->next and ->prev pointers are always zero on return from Duplicate. */ +/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal. + * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */ +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive); + +/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings. + * The input pointer json cannot point to a read-only address area, such as a string constant, + * but should point to a readable and writable address area. */ +CJSON_PUBLIC(void) cJSON_Minify(char *json); + +/* Helper functions for creating and adding items to an object at the same time. + * They return the added item or NULL on failure. */ +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean); +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number); +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string); +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw); +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name); + +/* When assigning an integer value, it needs to be propagated to valuedouble too. */ +#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number)) +/* helper for the cJSON_SetNumberValue macro */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number); +#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number)) +/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring); + +/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/ +#define cJSON_SetBoolValue(object, boolValue) ( \ + (object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \ + (object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \ + cJSON_Invalid\ +) + +/* Macro for iterating over an array or object */ +#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next) + +/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */ +CJSON_PUBLIC(void *) cJSON_malloc(size_t size); +CJSON_PUBLIC(void) cJSON_free(void *object); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/client.h b/include/client.h new file mode 100644 index 0000000..bb095f2 --- /dev/null +++ b/include/client.h @@ -0,0 +1,80 @@ +/* + * DWN - Desktop Window Manager + * Client (window) management + */ + +#ifndef DWN_CLIENT_H +#define DWN_CLIENT_H + +#include "dwn.h" +#include + +/* Client creation and destruction */ +Client *client_create(Window window); +void client_destroy(Client *client); + +/* Client management */ +Client *client_manage(Window window); +void client_unmanage(Client *client); +Client *client_find_by_window(Window window); +Client *client_find_by_frame(Window frame); + +/* Client state */ +void client_focus(Client *client); +void client_unfocus(Client *client); +void client_raise(Client *client); +void client_lower(Client *client); +void client_minimize(Client *client); +void client_restore(Client *client); + +/* Client geometry */ +void client_move(Client *client, int x, int y); +void client_resize(Client *client, int width, int height); +void client_move_resize(Client *client, int x, int y, int width, int height); +void client_configure(Client *client); +void client_apply_size_hints(Client *client, int *width, int *height); + +/* Client properties */ +void client_update_title(Client *client); +void client_update_class(Client *client); +void client_set_fullscreen(Client *client, bool fullscreen); +void client_toggle_fullscreen(Client *client); +void client_set_floating(Client *client, bool floating); +void client_toggle_floating(Client *client); + +/* Window type checking */ +bool client_is_floating(Client *client); +bool client_is_fullscreen(Client *client); +bool client_is_minimized(Client *client); +bool client_is_dialog(Window window); +bool client_is_dock(Window window); +bool client_is_desktop(Window window); + +/* Frame management */ +void client_create_frame(Client *client); +void client_destroy_frame(Client *client); +void client_reparent_to_frame(Client *client); +void client_reparent_from_frame(Client *client); + +/* Visibility */ +void client_show(Client *client); +void client_hide(Client *client); +bool client_is_visible(Client *client); + +/* Close handling */ +void client_close(Client *client); +void client_kill(Client *client); + +/* List operations */ +void client_add_to_list(Client *client); +void client_remove_from_list(Client *client); +int client_count(void); +int client_count_on_workspace(int workspace); + +/* Iteration */ +Client *client_get_next(Client *client); +Client *client_get_prev(Client *client); +Client *client_get_first(void); +Client *client_get_last(void); + +#endif /* DWN_CLIENT_H */ diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..507644f --- /dev/null +++ b/include/config.h @@ -0,0 +1,87 @@ +/* + * DWN - Desktop Window Manager + * Configuration system + */ + +#ifndef DWN_CONFIG_H +#define DWN_CONFIG_H + +#include "dwn.h" +#include + +/* Color configuration */ +typedef struct { + unsigned long panel_bg; + unsigned long panel_fg; + unsigned long workspace_active; + unsigned long workspace_inactive; + unsigned long workspace_urgent; + unsigned long title_focused_bg; + unsigned long title_focused_fg; + unsigned long title_unfocused_bg; + unsigned long title_unfocused_fg; + unsigned long border_focused; + unsigned long border_unfocused; + unsigned long notification_bg; + unsigned long notification_fg; +} ColorScheme; + +/* Configuration structure */ +struct Config { + /* General */ + char terminal[128]; + char launcher[128]; + char file_manager[128]; + FocusMode focus_mode; + bool show_decorations; + + /* Appearance */ + int border_width; + int title_height; + int panel_height; + int gap; + char font_name[128]; + ColorScheme colors; + + /* Layout */ + float default_master_ratio; + int default_master_count; + LayoutType default_layout; + + /* Panels */ + bool top_panel_enabled; + bool bottom_panel_enabled; + + /* AI */ + char openrouter_api_key[256]; + char exa_api_key[256]; + char ai_model[64]; + bool ai_enabled; + + /* Paths */ + char config_path[512]; + char log_path[512]; +}; + +/* Configuration functions */ +Config *config_create(void); +void config_destroy(Config *cfg); +bool config_load(Config *cfg, const char *path); +bool config_reload(Config *cfg); +void config_set_defaults(Config *cfg); + +/* Getters for commonly used values */ +const char *config_get_terminal(void); +const char *config_get_launcher(void); +int config_get_border_width(void); +int config_get_title_height(void); +int config_get_panel_height(void); +int config_get_gap(void); +const ColorScheme *config_get_colors(void); + +/* INI parsing helpers */ +typedef void (*ConfigCallback)(const char *section, const char *key, + const char *value, void *user_data); +bool config_parse_ini(const char *path, ConfigCallback callback, void *user_data); + +#endif /* DWN_CONFIG_H */ diff --git a/include/decorations.h b/include/decorations.h new file mode 100644 index 0000000..916ebe4 --- /dev/null +++ b/include/decorations.h @@ -0,0 +1,48 @@ +/* + * DWN - Desktop Window Manager + * Window decorations (title bars, borders, buttons) + */ + +#ifndef DWN_DECORATIONS_H +#define DWN_DECORATIONS_H + +#include "dwn.h" +#include + +/* Button types */ +typedef enum { + BUTTON_CLOSE, + BUTTON_MAXIMIZE, + BUTTON_MINIMIZE, + BUTTON_COUNT +} ButtonType; + +/* Button areas for hit testing */ +typedef struct { + int x, y; + int width, height; +} ButtonArea; + +/* Decoration initialization */ +void decorations_init(void); +void decorations_cleanup(void); + +/* Rendering */ +void decorations_render(Client *client, bool focused); +void decorations_render_title_bar(Client *client, bool focused); +void decorations_render_buttons(Client *client, bool focused); +void decorations_render_border(Client *client, bool focused); + +/* Hit testing */ +ButtonType decorations_hit_test_button(Client *client, int x, int y); +bool decorations_hit_test_title_bar(Client *client, int x, int y); +bool decorations_hit_test_resize_area(Client *client, int x, int y, int *direction); + +/* Button actions */ +void decorations_button_press(Client *client, ButtonType button); + +/* Text rendering */ +void decorations_draw_text(Window window, GC gc, int x, int y, + const char *text, unsigned long color); + +#endif /* DWN_DECORATIONS_H */ diff --git a/include/dwn.h b/include/dwn.h new file mode 100644 index 0000000..ba4e466 --- /dev/null +++ b/include/dwn.h @@ -0,0 +1,168 @@ +/* + * DWN - Desktop Window Manager + * Main header with shared types and global state + */ + +#ifndef DWN_H +#define DWN_H + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Version */ +#define DWN_VERSION "1.0.0" +#define DWN_NAME "DWN" + +/* Limits */ +#define MAX_CLIENTS 256 +#define MAX_WORKSPACES 9 +#define MAX_MONITORS 8 +#define MAX_NOTIFICATIONS 32 +#define MAX_KEYBINDINGS 64 + +/* Default dimensions */ +#define DEFAULT_BORDER_WIDTH 2 +#define DEFAULT_TITLE_HEIGHT 24 +#define DEFAULT_PANEL_HEIGHT 28 +#define DEFAULT_GAP 4 + +/* Layout types */ +typedef enum { + LAYOUT_TILING, + LAYOUT_FLOATING, + LAYOUT_MONOCLE, + LAYOUT_COUNT +} LayoutType; + +/* Focus modes */ +typedef enum { + FOCUS_CLICK, + FOCUS_FOLLOW +} FocusMode; + +/* Client state flags */ +typedef enum { + CLIENT_NORMAL = 0, + CLIENT_FLOATING = (1 << 0), + CLIENT_FULLSCREEN = (1 << 1), + CLIENT_URGENT = (1 << 2), + CLIENT_MINIMIZED = (1 << 3), + CLIENT_STICKY = (1 << 4) +} ClientFlags; + +/* Forward declarations */ +typedef struct Client Client; +typedef struct Workspace Workspace; +typedef struct Monitor Monitor; +typedef struct Panel Panel; +typedef struct Config Config; + +/* Client structure - represents a managed window */ +struct Client { + Window window; /* Application window */ + Window frame; /* Frame window (decoration) */ + int x, y; /* Position */ + int width, height; /* Size */ + int old_x, old_y; /* Previous position (for floating restore) */ + int old_width, old_height; + int border_width; + uint32_t flags; /* ClientFlags bitmask */ + unsigned int workspace; /* Current workspace (0-8) */ + char title[256]; /* Window title */ + char class[64]; /* Window class */ + Client *next; /* Linked list */ + Client *prev; +}; + +/* Monitor structure - represents a physical display */ +struct Monitor { + int x, y; + int width, height; + int index; + bool primary; +}; + +/* Workspace structure */ +struct Workspace { + Client *clients; /* Head of client list */ + Client *focused; /* Currently focused client */ + LayoutType layout; /* Current layout */ + float master_ratio; /* Ratio for master area in tiling */ + int master_count; /* Number of windows in master area */ + char name[32]; /* Workspace name */ +}; + +/* Panel widget types */ +typedef enum { + WIDGET_WORKSPACES, + WIDGET_TASKBAR, + WIDGET_CLOCK, + WIDGET_SYSTRAY, + WIDGET_AI_STATUS, + WIDGET_SEPARATOR +} WidgetType; + +/* Global state - singleton pattern */ +typedef struct { + Display *display; + int screen; + Window root; + int screen_width; + int screen_height; + + /* Monitors */ + Monitor monitors[MAX_MONITORS]; + int monitor_count; + + /* Workspaces */ + Workspace workspaces[MAX_WORKSPACES]; + int current_workspace; + + /* Clients */ + Client *client_list; /* All clients */ + int client_count; + + /* Panels */ + Panel *top_panel; + Panel *bottom_panel; + + /* Configuration */ + Config *config; + + /* State */ + bool running; + bool ai_enabled; + + /* Graphics contexts */ + GC gc; + XFontStruct *font; + XftFont *xft_font; /* Xft font for UTF-8 rendering */ + Colormap colormap; + + /* Drag state */ + Client *drag_client; + int drag_start_x, drag_start_y; + int drag_orig_x, drag_orig_y; + int drag_orig_w, drag_orig_h; + bool resizing; +} DWNState; + +/* Global state accessor */ +extern DWNState *dwn; + +/* Core functions */ +int dwn_init(void); +void dwn_cleanup(void); +void dwn_run(void); +void dwn_quit(void); + +/* Event handlers */ +void dwn_handle_event(XEvent *ev); + +#endif /* DWN_H */ diff --git a/include/keys.h b/include/keys.h new file mode 100644 index 0000000..c09a23e --- /dev/null +++ b/include/keys.h @@ -0,0 +1,98 @@ +/* + * DWN - Desktop Window Manager + * Keyboard shortcut handling + */ + +#ifndef DWN_KEYS_H +#define DWN_KEYS_H + +#include "dwn.h" +#include +#include + +/* Modifier masks */ +#define MOD_ALT Mod1Mask +#define MOD_CTRL ControlMask +#define MOD_SHIFT ShiftMask +#define MOD_SUPER Mod4Mask + +/* Key binding callback type */ +typedef void (*KeyCallback)(void); + +/* Key binding structure */ +typedef struct { + unsigned int modifiers; + KeySym keysym; + KeyCallback callback; + const char *description; +} KeyBinding; + +/* Initialization */ +void keys_init(void); +void keys_cleanup(void); +void keys_grab_all(void); +void keys_ungrab_all(void); + +/* Key binding registration */ +void keys_bind(unsigned int modifiers, KeySym keysym, + KeyCallback callback, const char *description); +void keys_unbind(unsigned int modifiers, KeySym keysym); +void keys_clear_all(void); + +/* Key event handling */ +void keys_handle_press(XKeyEvent *ev); +void keys_handle_release(XKeyEvent *ev); + +/* Default key bindings */ +void keys_setup_defaults(void); + +/* Key binding callbacks */ +void key_spawn_terminal(void); +void key_spawn_launcher(void); +void key_spawn_file_manager(void); +void key_spawn_browser(void); +void key_close_window(void); +void key_quit_dwn(void); +void key_cycle_layout(void); +void key_toggle_floating(void); +void key_toggle_fullscreen(void); +void key_toggle_maximize(void); +void key_focus_next(void); +void key_focus_prev(void); +void key_workspace_next(void); +void key_workspace_prev(void); +void key_workspace_1(void); +void key_workspace_2(void); +void key_workspace_3(void); +void key_workspace_4(void); +void key_workspace_5(void); +void key_workspace_6(void); +void key_workspace_7(void); +void key_workspace_8(void); +void key_workspace_9(void); +void key_move_to_workspace_1(void); +void key_move_to_workspace_2(void); +void key_move_to_workspace_3(void); +void key_move_to_workspace_4(void); +void key_move_to_workspace_5(void); +void key_move_to_workspace_6(void); +void key_move_to_workspace_7(void); +void key_move_to_workspace_8(void); +void key_move_to_workspace_9(void); +void key_increase_master(void); +void key_decrease_master(void); +void key_increase_master_count(void); +void key_decrease_master_count(void); +void key_toggle_ai(void); +void key_ai_command(void); +void key_show_shortcuts(void); +void key_start_tutorial(void); + +/* Tutorial system */ +void tutorial_start(void); +void tutorial_stop(void); +void tutorial_next_step(void); +void tutorial_check_key(unsigned int modifiers, KeySym keysym); +bool tutorial_is_active(void); + +#endif /* DWN_KEYS_H */ diff --git a/include/layout.h b/include/layout.h new file mode 100644 index 0000000..883746b --- /dev/null +++ b/include/layout.h @@ -0,0 +1,25 @@ +/* + * DWN - Desktop Window Manager + * Layout algorithms (tiling, floating, monocle) + */ + +#ifndef DWN_LAYOUT_H +#define DWN_LAYOUT_H + +#include "dwn.h" + +/* Layout arrangement */ +void layout_arrange(int workspace); +void layout_arrange_tiling(int workspace); +void layout_arrange_floating(int workspace); +void layout_arrange_monocle(int workspace); + +/* Layout helpers */ +int layout_get_usable_area(int *x, int *y, int *width, int *height); +int layout_count_tiled_clients(int workspace); + +/* Layout names */ +const char *layout_get_name(LayoutType layout); +const char *layout_get_symbol(LayoutType layout); + +#endif /* DWN_LAYOUT_H */ diff --git a/include/news.h b/include/news.h new file mode 100644 index 0000000..6eefc6e --- /dev/null +++ b/include/news.h @@ -0,0 +1,66 @@ +/* + * DWN - Desktop Window Manager + * News ticker for bottom panel + */ + +#ifndef DWN_NEWS_H +#define DWN_NEWS_H + +#include "dwn.h" +#include + +/* Maximum articles to cache */ +#define MAX_NEWS_ARTICLES 50 +#define NEWS_API_URL "https://news.app.molodetz.nl/api" + +/* News article */ +typedef struct { + char title[256]; + char content[1024]; + char link[512]; + char author[128]; +} NewsArticle; + +/* News ticker state */ +typedef struct { + NewsArticle articles[MAX_NEWS_ARTICLES]; + int article_count; + int current_article; /* Currently displayed article index */ + double scroll_offset; /* Sub-pixel offset for smooth scrolling */ + bool fetching; /* Currently fetching from API */ + bool has_error; /* Last fetch failed */ + long last_fetch; /* Timestamp of last fetch */ + long last_scroll_update; /* Timestamp of last scroll update */ + bool interactive_mode; /* User is navigating with up/down */ + int display_widths[MAX_NEWS_ARTICLES]; /* Cached text widths */ + int total_width; /* Total scrollable width */ + int render_x; /* X position where news starts rendering */ + int render_width; /* Width of news render area */ + bool widths_dirty; /* Need to recalculate widths */ +} NewsState; + +/* Global state */ +extern NewsState news_state; + +/* Initialization */ +void news_init(void); +void news_cleanup(void); + +/* Fetching */ +void news_fetch_async(void); +void news_update(void); /* Called from main loop */ + +/* Navigation */ +void news_next_article(void); +void news_prev_article(void); +void news_open_current(void); + +/* Rendering */ +void news_render(Panel *panel, int x, int max_width, int *used_width); +void news_handle_click(int x, int y); + +/* Thread-safe access */ +void news_lock(void); +void news_unlock(void); + +#endif /* DWN_NEWS_H */ diff --git a/include/notifications.h b/include/notifications.h new file mode 100644 index 0000000..d44f8b0 --- /dev/null +++ b/include/notifications.h @@ -0,0 +1,73 @@ +/* + * DWN - Desktop Window Manager + * D-Bus Notification daemon + */ + +#ifndef DWN_NOTIFICATIONS_H +#define DWN_NOTIFICATIONS_H + +#include "dwn.h" +#include +#include + +/* Notification urgency levels */ +typedef enum { + NOTIFY_URGENCY_LOW, + NOTIFY_URGENCY_NORMAL, + NOTIFY_URGENCY_CRITICAL +} NotifyUrgency; + +/* Notification structure */ +typedef struct Notification { + uint32_t id; + char app_name[64]; + char summary[512]; /* Larger summary for AI responses */ + char *body; /* Dynamically allocated - unlimited size */ + size_t body_len; /* Length of body text */ + char icon[256]; + int timeout; /* -1 = default, 0 = never, >0 = milliseconds */ + NotifyUrgency urgency; + long expire_time; /* Timestamp when notification should disappear */ + Window window; /* X11 window for rendering */ + int width; /* Dynamic width based on content */ + int height; /* Dynamic height based on content */ + struct Notification *next; +} Notification; + +/* D-Bus connection */ +extern DBusConnection *dbus_conn; + +/* Initialization */ +bool notifications_init(void); +void notifications_cleanup(void); + +/* D-Bus handling */ +void notifications_process_messages(void); +bool notifications_register_service(void); + +/* Notification management */ +uint32_t notification_show(const char *app_name, const char *summary, + const char *body, const char *icon, int timeout); +void notification_close(uint32_t id); +void notification_close_all(void); +Notification *notification_find(uint32_t id); +Notification *notification_find_by_window(Window window); + +/* Rendering */ +void notification_render(Notification *notif); +void notifications_render_all(void); +void notifications_update(void); +void notifications_position(void); +void notifications_raise_all(void); + +/* D-Bus method handlers */ +DBusHandlerResult notifications_handle_message(DBusConnection *conn, + DBusMessage *msg, + void *user_data); + +/* Server info */ +void notifications_get_server_info(const char **name, const char **vendor, + const char **version, const char **spec_version); +void notifications_get_capabilities(const char ***caps, int *count); + +#endif /* DWN_NOTIFICATIONS_H */ diff --git a/include/panel.h b/include/panel.h new file mode 100644 index 0000000..0392cd3 --- /dev/null +++ b/include/panel.h @@ -0,0 +1,65 @@ +/* + * DWN - Desktop Window Manager + * Panel system (top and bottom panels with widgets) + */ + +#ifndef DWN_PANEL_H +#define DWN_PANEL_H + +#include "dwn.h" +#include + +/* Panel position */ +typedef enum { + PANEL_TOP, + PANEL_BOTTOM +} PanelPosition; + +/* Panel structure */ +struct Panel { + Window window; + PanelPosition position; + int x, y; + int width, height; + bool visible; + Pixmap buffer; /* Double buffering */ +}; + +/* Panel initialization */ +Panel *panel_create(PanelPosition position); +void panel_destroy(Panel *panel); +void panels_init(void); +void panels_cleanup(void); + +/* Panel rendering */ +void panel_render(Panel *panel); +void panel_render_all(void); +void panel_render_workspaces(Panel *panel, int x, int *width); +void panel_render_taskbar(Panel *panel, int x, int *width); +void panel_render_clock(Panel *panel, int x, int *width); +void panel_render_systray(Panel *panel, int x, int *width); +void panel_render_layout_indicator(Panel *panel, int x, int *width); +void panel_render_ai_status(Panel *panel, int x, int *width); + +/* Panel interaction */ +void panel_handle_click(Panel *panel, int x, int y, int button); +int panel_hit_test_workspace(Panel *panel, int x, int y); +Client *panel_hit_test_taskbar(Panel *panel, int x, int y); + +/* Panel visibility */ +void panel_show(Panel *panel); +void panel_hide(Panel *panel); +void panel_toggle(Panel *panel); + +/* Clock updates */ +void panel_update_clock(void); + +/* System stats updates */ +void panel_update_system_stats(void); + +/* System tray */ +void panel_init_systray(void); +void panel_add_systray_icon(Window icon); +void panel_remove_systray_icon(Window icon); + +#endif /* DWN_PANEL_H */ diff --git a/include/systray.h b/include/systray.h new file mode 100644 index 0000000..546508a --- /dev/null +++ b/include/systray.h @@ -0,0 +1,134 @@ +/* + * DWN - Desktop Window Manager + * System tray widgets (WiFi, Audio, etc.) + */ + +#ifndef DWN_SYSTRAY_H +#define DWN_SYSTRAY_H + +#include "dwn.h" +#include + +/* Maximum number of WiFi networks to show */ +#define MAX_WIFI_NETWORKS 20 + +/* WiFi network info */ +typedef struct { + char ssid[64]; + int signal; /* Signal strength 0-100 */ + char security[32]; /* Security type (WPA2, etc.) */ + bool connected; +} WifiNetwork; + +/* WiFi state */ +typedef struct { + bool enabled; + bool connected; + char current_ssid[64]; + int signal_strength; + WifiNetwork networks[MAX_WIFI_NETWORKS]; + int network_count; + long last_scan; /* Timestamp of last scan */ +} WifiState; + +/* Audio state */ +typedef struct { + int volume; /* 0-100 */ + bool muted; +} AudioState; + +/* Battery state */ +typedef struct { + bool present; /* Battery exists */ + bool charging; /* Currently charging */ + int percentage; /* 0-100 */ + int time_remaining; /* Minutes remaining */ +} BatteryState; + +/* Volume slider popup */ +typedef struct { + Window window; + int x, y; + int width, height; + bool visible; + bool dragging; +} VolumeSlider; + +/* Dropdown menu */ +typedef struct { + Window window; + int x, y; + int width, height; + int item_count; + int hovered_item; + bool visible; + void (*on_select)(int index); +} DropdownMenu; + +/* System tray state */ +extern WifiState wifi_state; +extern AudioState audio_state; +extern BatteryState battery_state; +extern DropdownMenu *wifi_menu; +extern VolumeSlider *volume_slider; + +/* Initialization */ +void systray_init(void); +void systray_cleanup(void); + +/* WiFi functions */ +void wifi_update_state(void); +void wifi_scan_networks(void); +void wifi_connect(const char *ssid); +void wifi_disconnect(void); +const char *wifi_get_icon(void); + +/* Audio functions */ +void audio_update_state(void); +void audio_set_volume(int volume); +void audio_toggle_mute(void); +const char *audio_get_icon(void); + +/* Battery functions */ +void battery_update_state(void); +const char *battery_get_icon(void); + +/* Volume slider functions */ +VolumeSlider *volume_slider_create(int x, int y); +void volume_slider_destroy(VolumeSlider *slider); +void volume_slider_show(VolumeSlider *slider); +void volume_slider_hide(VolumeSlider *slider); +void volume_slider_render(VolumeSlider *slider); +void volume_slider_handle_click(VolumeSlider *slider, int x, int y); +void volume_slider_handle_motion(VolumeSlider *slider, int x, int y); +void volume_slider_handle_release(VolumeSlider *slider); + +/* Dropdown menu functions */ +DropdownMenu *dropdown_create(int x, int y, int width); +void dropdown_destroy(DropdownMenu *menu); +void dropdown_show(DropdownMenu *menu); +void dropdown_hide(DropdownMenu *menu); +void dropdown_add_item(DropdownMenu *menu, const char *label); +void dropdown_render(DropdownMenu *menu); +int dropdown_hit_test(DropdownMenu *menu, int x, int y); +void dropdown_handle_click(DropdownMenu *menu, int x, int y); +void dropdown_handle_motion(DropdownMenu *menu, int x, int y); + +/* Panel rendering for systray */ +void systray_render(Panel *panel, int x, int *width); +int systray_get_width(void); +void systray_handle_click(int x, int y, int button); +int systray_hit_test(int x); /* Returns: 0=wifi, 1=audio, -1=none */ + +/* Periodic update */ +void systray_update(void); + +/* Thread-safe state access */ +void systray_lock(void); +void systray_unlock(void); + +/* Thread-safe state snapshots (copies state under lock) */ +BatteryState systray_get_battery_snapshot(void); +AudioState systray_get_audio_snapshot(void); + +#endif /* DWN_SYSTRAY_H */ diff --git a/include/util.h b/include/util.h new file mode 100644 index 0000000..8e833c7 --- /dev/null +++ b/include/util.h @@ -0,0 +1,68 @@ +/* + * DWN - Desktop Window Manager + * Utility functions + */ + +#ifndef DWN_UTIL_H +#define DWN_UTIL_H + +#include +#include +#include + +/* Logging levels */ +typedef enum { + LOG_DEBUG, + LOG_INFO, + LOG_WARN, + LOG_ERROR +} LogLevel; + +/* Async Logging - non-blocking with max file size (5MB) and rotation */ +void log_init(const char *log_file); +void log_close(void); +void log_set_level(LogLevel level); +void log_flush(void); /* Force flush pending logs (call before exit/crash) */ +void log_msg(LogLevel level, const char *fmt, ...); + +/* Convenience macros */ +#define LOG_DEBUG(...) log_msg(LOG_DEBUG, __VA_ARGS__) +#define LOG_INFO(...) log_msg(LOG_INFO, __VA_ARGS__) +#define LOG_WARN(...) log_msg(LOG_WARN, __VA_ARGS__) +#define LOG_ERROR(...) log_msg(LOG_ERROR, __VA_ARGS__) + +/* Memory allocation with error checking */ +void *dwn_malloc(size_t size); +void *dwn_calloc(size_t nmemb, size_t size); +void *dwn_realloc(void *ptr, size_t size); +char *dwn_strdup(const char *s); +void dwn_free(void *ptr); +void secure_wipe(void *ptr, size_t size); /* Securely wipe sensitive data */ + +/* String utilities */ +char *str_trim(char *str); +bool str_starts_with(const char *str, const char *prefix); +bool str_ends_with(const char *str, const char *suffix); +int str_split(char *str, char delim, char **parts, int max_parts); +char *shell_escape(const char *str); /* Escape string for safe shell use */ + +/* File utilities */ +bool file_exists(const char *path); +char *file_read_all(const char *path); +bool file_write_all(const char *path, const char *content); +char *expand_path(const char *path); + +/* Color utilities */ +unsigned long parse_color(const char *color_str); +void color_to_rgb(unsigned long color, int *r, int *g, int *b); + +/* Time utilities */ +long get_time_ms(void); +void sleep_ms(int ms); + +/* Process utilities */ +int spawn(const char *cmd); +int spawn_async(const char *cmd); +char *spawn_capture(const char *cmd); + +#endif /* DWN_UTIL_H */ diff --git a/include/workspace.h b/include/workspace.h new file mode 100644 index 0000000..ace5354 --- /dev/null +++ b/include/workspace.h @@ -0,0 +1,61 @@ +/* + * DWN - Desktop Window Manager + * Workspace (tag/virtual desktop) management + */ + +#ifndef DWN_WORKSPACE_H +#define DWN_WORKSPACE_H + +#include "dwn.h" +#include + +/* Workspace initialization */ +void workspace_init(void); +void workspace_cleanup(void); + +/* Workspace access */ +Workspace *workspace_get(int index); +Workspace *workspace_get_current(void); +int workspace_get_current_index(void); + +/* Workspace switching */ +void workspace_switch(int index); +void workspace_switch_next(void); +void workspace_switch_prev(void); + +/* Client management within workspaces */ +void workspace_add_client(int workspace, Client *client); +void workspace_remove_client(int workspace, Client *client); +void workspace_move_client(Client *client, int new_workspace); +Client *workspace_get_first_client(int workspace); +Client *workspace_get_focused_client(int workspace); + +/* Layout */ +void workspace_set_layout(int workspace, LayoutType layout); +LayoutType workspace_get_layout(int workspace); +void workspace_cycle_layout(int workspace); +void workspace_set_master_ratio(int workspace, float ratio); +void workspace_adjust_master_ratio(int workspace, float delta); +void workspace_set_master_count(int workspace, int count); +void workspace_adjust_master_count(int workspace, int delta); + +/* Arrangement */ +void workspace_arrange(int workspace); +void workspace_arrange_current(void); + +/* Visibility */ +void workspace_show(int workspace); +void workspace_hide(int workspace); + +/* Properties */ +void workspace_set_name(int workspace, const char *name); +const char *workspace_get_name(int workspace); +int workspace_client_count(int workspace); +bool workspace_is_empty(int workspace); + +/* Focus cycling within workspace */ +void workspace_focus_next(void); +void workspace_focus_prev(void); +void workspace_focus_master(void); + +#endif /* DWN_WORKSPACE_H */ diff --git a/scripts/dwn.desktop b/scripts/dwn.desktop new file mode 100644 index 0000000..efc7a5c --- /dev/null +++ b/scripts/dwn.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=DWN +Comment=AI-enhanced desktop window manager +Exec=dwn +TryExec=dwn +Type=Application +DesktopNames=DWN +Keywords=wm;tiling;window;manager; diff --git a/scripts/xinitrc.example b/scripts/xinitrc.example new file mode 100644 index 0000000..587584f --- /dev/null +++ b/scripts/xinitrc.example @@ -0,0 +1,42 @@ +#!/bin/sh +# Example .xinitrc for starting DWN with startx +# Copy this to ~/.xinitrc and modify as needed + +# Set environment variables +export XDG_SESSION_TYPE=x11 +export XDG_CURRENT_DESKTOP=DWN + +# Optional: Set AI API keys +# export OPENROUTER_API_KEY="your-api-key-here" +# export EXA_API_KEY="your-exa-key-here" + +# Optional: Start background services +# Start D-Bus session bus if not already running +if [ -z "$DBUS_SESSION_BUS_ADDRESS" ]; then + eval $(dbus-launch --sh-syntax) +fi + +# Optional: Start compositor for transparency/shadows +# picom & + +# Optional: Set wallpaper +# feh --bg-scale ~/.wallpaper.png & + +# Optional: Start notification daemon (DWN has built-in) +# Note: DWN includes its own notification daemon + +# Optional: Start XFCE components for additional functionality +# xfce4-power-manager & +# xfsettingsd & + +# Optional: Start polkit authentication agent +# /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 & + +# Optional: Start network manager applet +# nm-applet & + +# Optional: Start clipboard manager +# xfce4-clipman & + +# Start DWN +exec dwn diff --git a/site/ai-features.html b/site/ai-features.html new file mode 100644 index 0000000..e227235 --- /dev/null +++ b/site/ai-features.html @@ -0,0 +1,458 @@ + + + + + + + AI Features - DWN Window Manager + + + +
+ +
+ +
+
+
+

AI Integration

+

+ Control your desktop with natural language and get intelligent assistance. +

+
+
+ +
+
+
+ Optional Features +

AI features are completely optional and require external API keys. + DWN works perfectly without them.

+
+ +

Overview

+

+ DWN integrates with two AI services to provide intelligent desktop assistance: +

+
    +
  • OpenRouter API - Powers the AI command palette and context analysis
  • +
  • Exa API - Provides semantic web search capabilities
  • +
+ +
+
+
🤖
+

AI Command Palette

+

Type natural language commands to control your desktop. + Launch apps, query system info, and more.

+

Super + Shift + A

+
+
+
👁
+

Context Analysis

+

AI analyzes your current workspace to understand your task + and provide relevant suggestions.

+

Super + A

+
+
+
🔍
+

Semantic Search

+

Search the web using meaning, not just keywords. + Find relevant content instantly.

+

Super + Shift + E

+
+
+ + +

Setting Up OpenRouter

+

+ OpenRouter provides access to multiple AI models through a single API. + You can use free models or paid ones depending on your needs. +

+ +
+
+
1
+
+

Get an API Key

+

Visit https://openrouter.ai/keys + and create a free account to get your API key.

+
+
+ +
+
2
+
+

Set the Environment Variable

+

Add to your shell profile (~/.bashrc, ~/.zshrc, etc.):

+
+ ~/.bashrc + +
+
export OPENROUTER_API_KEY="sk-or-v1-your-key-here"
+
+
+ +
+
3
+
+

Choose a Model (Optional)

+

Configure the AI model in your DWN config file:

+
+ ~/.config/dwn/config + +
+
[ai]
+model = google/gemini-2.0-flash-exp:free
+

+ Browse available models at openrouter.ai/models +

+
+
+
+ +
+

Recommended Free Models

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Model IDProviderBest For
google/gemini-2.0-flash-exp:freeGoogleFast responses, good general use
meta-llama/llama-3.2-3b-instruct:freeMetaQuick commands, lightweight
mistralai/mistral-7b-instruct:freeMistralBalanced performance
+
+
+ + +

AI Command Palette

+

+ Press Super + Shift + A to open the command palette. + Type natural language commands and press Enter. +

+ +

Supported Commands

+
+ + + + + + + + + + + + + + + + + + + + + +
CategoryExamples
Launch Applications + "open firefox"
+ "run terminal"
+ "launch file manager"
+ "start chrome" +
System Queries + "what time is it"
+ "how much memory is free"
+ "what's my IP address"
+ "show disk usage" +
General Questions + "how do I resize the master area"
+ "what's the shortcut for fullscreen"
+ "explain tiling mode" +
+
+ +
+ Pro Tip +

The AI understands context. You can say "open browser" instead of + remembering the exact application name - it will figure out what you mean.

+
+ + +

Context Analysis

+

+ Press Super + A to see AI-powered analysis of your current workspace. +

+ +
+

What It Shows

+
    +
  • Task Type - Coding, browsing, communication, etc.
  • +
  • Focused Window - Currently active application
  • +
  • Suggestions - Relevant shortcuts or actions based on context
  • +
  • Workspace Summary - Overview of open applications
  • +
+
+ + +

Setting Up Exa Search

+

+ Exa provides semantic search - finding content based on meaning rather than exact keywords. +

+ +
+
+
1
+
+

Get an Exa API Key

+

Visit https://dashboard.exa.ai/api-keys + and create an account to get your API key.

+
+
+ +
+
2
+
+

Set the Environment Variable

+
+ ~/.bashrc + +
+
export EXA_API_KEY="your-exa-key-here"
+
+
+ +
+
3
+
+

Start Searching

+

Press Super + Shift + E and type your query.

+
+
+
+ + + +

+ Unlike traditional search, Exa understands the meaning of your query. +

+ +
+
+

Traditional Search

+

Keyword matching

+
    +
  • Exact keyword matches
  • +
  • Boolean operators needed
  • +
  • Miss relevant results
  • +
+

+ "nginx reverse proxy setup tutorial" +

+
+ +
+ +

Search Tips

+
    +
  • Use natural, conversational queries
  • +
  • Be specific about what you're looking for
  • +
  • Results appear in a dmenu/rofi list - select to open in browser
  • +
  • Search includes articles, documentation, tutorials, and more
  • +
+ + +

Privacy Considerations

+
+ Data Sent to External Services +

When using AI features, the following data is sent to external APIs:

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureData SentService
Command PaletteYour typed commandOpenRouter (then to model provider)
Context AnalysisWindow titles, app namesOpenRouter (then to model provider)
Semantic SearchYour search queryExa
+
+ +

+ If you're concerned about privacy, you can: +

+
    +
  • Not configure API keys (AI features simply won't work)
  • +
  • Use OpenRouter with privacy-focused models
  • +
  • Only use AI features when needed
  • +
+ + +

Troubleshooting

+ +
+ +
+
+

Make sure your API key is properly set:

+
    +
  1. Check the environment variable: echo $OPENROUTER_API_KEY
  2. +
  3. Ensure the variable is exported in your shell profile
  4. +
  5. Log out and back in, or source your profile: source ~/.bashrc
  6. +
  7. Restart DWN
  8. +
+
+
+
+ +
+ +
+
+

Try using a faster model. Free models can sometimes be slow due to rate limiting. + Gemini Flash is usually the fastest free option.

+
+
+
+ +
+ +
+
+

Check your Exa API key and ensure you have remaining credits. + Visit the Exa dashboard to check your usage.

+
+
+
+ +
+
+
+ + + + + + diff --git a/site/architecture.html b/site/architecture.html new file mode 100644 index 0000000..e4ce219 --- /dev/null +++ b/site/architecture.html @@ -0,0 +1,565 @@ + + + + + + + Architecture - DWN Window Manager + + + +
+ +
+ +
+
+
+

Architecture

+

+ Technical documentation for developers and contributors. +

+
+
+ +
+
+

Overview

+

+ DWN is written in ANSI C (C99) and follows a modular single-responsibility architecture. + A global DWNState singleton manages all state, and the main event loop + dispatches X11 events to specialized modules. +

+ +
+

Project Statistics

+
+
+
~10K
+
Lines of Code
+
+
+
12
+
Core Modules
+
+
+
C99
+
Standard
+
+
+
MIT
+
License
+
+
+
+ + +

Directory Structure

+
+ Project Layout +
+
dwn/
+├── src/                  # Source files (.c)
+│   ├── main.c           # Entry point, event loop
+│   ├── client.c         # Window management
+│   ├── workspace.c      # Virtual desktops
+│   ├── layout.c         # Tiling algorithms
+│   ├── decorations.c    # Title bars, borders
+│   ├── panel.c          # Top/bottom panels
+│   ├── systray.c        # System tray widgets
+│   ├── notifications.c  # D-Bus notifications
+│   ├── atoms.c          # X11 atoms (EWMH/ICCCM)
+│   ├── keys.c           # Keyboard handling
+│   ├── config.c         # INI parser
+│   ├── ai.c             # AI integration
+│   └── util.c           # Utilities
+├── include/             # Header files (.h)
+├── site/                # Documentation website
+├── Makefile             # Build system
+├── CLAUDE.md            # AI assistant context
+└── README.md            # Project readme
+ + +

Core Modules

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ModuleFileResponsibility
Mainmain.cX11 initialization, event loop, signal handling, module orchestration
Clientclient.cWindow lifecycle, focus management, frame creation, client list
Workspaceworkspace.c9 virtual desktops, per-workspace state, window assignment
Layoutlayout.cTiling (master+stack), floating, monocle layout algorithms
Decorationsdecorations.cWindow title bars, borders, decoration rendering
Panelpanel.cTop panel (taskbar, workspace indicators), bottom panel (clock)
Systraysystray.cSystem tray with WiFi/audio/battery indicators, dropdowns
Notificationsnotifications.cD-Bus notification daemon (org.freedesktop.Notifications)
Atomsatoms.cX11 EWMH/ICCCM atom management and property handling
Keyskeys.cKeyboard shortcut capture, keybinding registry, callbacks
Configconfig.cINI-style config loading and parsing
AIai.cAsync OpenRouter API integration, Exa semantic search
Utilutil.cLogging, memory allocation, string utilities, file helpers
+
+ + +

Module Dependencies

+
+
main.c (orchestrator)
+    ├── client.c
+    │   ├── decorations.c
+    │   ├── config.c
+    │   └── atoms.c
+    ├── workspace.c
+    │   ├── client.c
+    │   ├── layout.c
+    │   └── atoms.c
+    ├── panel.c
+    │   ├── client.c
+    │   └── config.c
+    ├── systray.c
+    │   └── config.c
+    ├── notifications.c (independent)
+    ├── ai.c (independent)
+    └── keys.c
+        └── config.c
+
+ + +

Global State (DWNState)

+

+ All window manager state is centralized in a single DWNState structure. + This simplifies state management and makes the codebase easier to understand. +

+ +
+ include/dwn.h (simplified) +
+
typedef struct {
+    Display *display;           // X11 connection
+    Window root;                // Root window
+    int screen;                 // Default screen
+
+    Client *clients[MAX_CLIENTS];   // All managed windows
+    int client_count;
+
+    Workspace workspaces[MAX_WORKSPACES];  // Virtual desktops
+    int current_workspace;
+
+    Panel top_panel;
+    Panel bottom_panel;
+
+    Config config;              // User configuration
+    KeyBinding keys[MAX_KEYBINDINGS];
+
+    // EWMH atoms
+    Atom atoms[ATOM_COUNT];
+} DWNState;
+
+extern DWNState *dwn;  // Global singleton
+ + +

Event Loop

+

+ DWN uses a traditional X11 event loop with XNextEvent. Events are dispatched + to appropriate handlers based on type. +

+ +
+ main.c (simplified) +
+
int main(int argc, char *argv[]) {
+    dwn_init();           // Initialize X11, atoms, config
+    setup_keybindings();  // Register keyboard shortcuts
+    setup_panels();       // Create panel windows
+
+    XEvent event;
+    while (running) {
+        XNextEvent(dwn->display, &event);
+
+        switch (event.type) {
+            case MapRequest:
+                handle_map_request(&event.xmaprequest);
+                break;
+            case UnmapNotify:
+                handle_unmap_notify(&event.xunmap);
+                break;
+            case KeyPress:
+                handle_key_press(&event.xkey);
+                break;
+            case ButtonPress:
+                handle_button_press(&event.xbutton);
+                break;
+            case ConfigureRequest:
+                handle_configure_request(&event.xconfigurerequest);
+                break;
+            // ... more event types
+        }
+    }
+
+    dwn_cleanup();
+    return 0;
+}
+ + +

Key Constants

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConstantValuePurpose
MAX_CLIENTS256Maximum managed windows
MAX_WORKSPACES9Number of virtual desktops
MAX_MONITORS8Multi-monitor support limit
MAX_NOTIFICATIONS32Concurrent notifications
MAX_KEYBINDINGS64Registered keyboard shortcuts
+
+ + +

Coding Conventions

+
+
+

Naming

+
    +
  • snake_case for functions and variables
  • +
  • CamelCase for types and structs
  • +
  • Module prefix for functions (e.g., client_focus())
  • +
  • Constants in UPPER_SNAKE_CASE
  • +
+
+
+

Style

+
    +
  • 4-space indentation
  • +
  • K&R brace style
  • +
  • Max 100 characters per line
  • +
  • clang-format for consistency
  • +
+
+
+ +
+ Example Function +
+
void client_focus(Client *c) {
+    if (!c) return;
+
+    // Unfocus previous
+    if (dwn->focused && dwn->focused != c) {
+        client_unfocus(dwn->focused);
+    }
+
+    dwn->focused = c;
+    XSetInputFocus(dwn->display, c->window, RevertToPointerRoot, CurrentTime);
+    XRaiseWindow(dwn->display, c->frame);
+
+    decorations_update(c);
+    atoms_set_active_window(c->window);
+}
+ + +

EWMH/ICCCM Support

+

+ DWN implements key Extended Window Manager Hints and ICCCM protocols + for compatibility with modern applications. +

+ +
+
+

EWMH Atoms

+
    +
  • _NET_SUPPORTED
  • +
  • _NET_CLIENT_LIST
  • +
  • _NET_CLIENT_LIST_STACKING
  • +
  • _NET_ACTIVE_WINDOW
  • +
  • _NET_CURRENT_DESKTOP
  • +
  • _NET_NUMBER_OF_DESKTOPS
  • +
  • _NET_WM_STATE
  • +
  • _NET_WM_STATE_FULLSCREEN
  • +
  • _NET_WM_STATE_MAXIMIZED_*
  • +
  • _NET_WM_WINDOW_TYPE
  • +
  • _NET_WM_NAME
  • +
+
+
+

ICCCM Support

+
    +
  • WM_STATE
  • +
  • WM_PROTOCOLS
  • +
  • WM_DELETE_WINDOW
  • +
  • WM_TAKE_FOCUS
  • +
  • WM_NORMAL_HINTS
  • +
  • WM_SIZE_HINTS
  • +
  • WM_CLASS
  • +
  • WM_NAME
  • +
  • WM_TRANSIENT_FOR
  • +
+
+
+ + +

Build System

+

+ DWN uses a simple Makefile-based build system with pkg-config for dependency detection. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TargetCommandDescription
Build (release)makeOptimized build with -O2
Build (debug)make debugDebug symbols, -DDEBUG flag
Installsudo make installInstall to PREFIX (/usr/local)
Cleanmake cleanRemove build artifacts
Formatmake formatRun clang-format on sources
Checkmake checkRun cppcheck static analysis
Testmake runRun in Xephyr nested server
Dependenciesmake depsAuto-install for your distro
+
+ + +

Contributing

+

+ Contributions are welcome! Here's how to get started: +

+ +
+
+
1
+
+

Fork & Clone

+

Fork the repository and clone your fork locally.

+
+
+
+
2
+
+

Create a Branch

+

Create a feature branch: git checkout -b feature/my-feature

+
+
+
+
3
+
+

Make Changes

+

Follow coding conventions. Run make format and make check.

+
+
+
+
4
+
+

Test

+

Test your changes with make run in a nested X server.

+
+
+
+
5
+
+

Submit PR

+

Push your branch and open a pull request with a clear description.

+
+
+
+ +
+
+
+ + + + + + diff --git a/site/configuration.html b/site/configuration.html new file mode 100644 index 0000000..5e26383 --- /dev/null +++ b/site/configuration.html @@ -0,0 +1,520 @@ + + + + + + + Configuration - DWN Window Manager + + + +
+ +
+ +
+
+
+

Configuration Guide

+

+ Customize every aspect of DWN to match your workflow and style. +

+
+
+ +
+
+

Configuration File

+

+ DWN reads its configuration from ~/.config/dwn/config using an INI-style format. + Changes take effect on restart (or you can reload in a future version). +

+ +
+ First Run +

DWN creates a default configuration file on first run if one doesn't exist. + You can also copy the example config from the source repository.

+
+ + +

[general] - Core Settings

+

+ Basic behavior settings for applications and focus handling. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionDefaultDescription
terminalxfce4-terminalTerminal emulator launched with Ctrl+Alt+T
launcherdmenu_runApplication launcher for Alt+F2
file_managerthunarFile manager for Super+E
focus_modeclickclick or follow (sloppy focus)
decorationstrueShow window title bars and borders
+
+ +
+ Example + +
+
[general]
+terminal = alacritty
+launcher = rofi -show run
+file_manager = nautilus
+focus_mode = click
+decorations = true
+ + +

[appearance] - Visual Settings

+

+ Control the visual appearance of windows, panels, and gaps. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionDefaultRangeDescription
border_width20-50Window border width in pixels
title_height240-100Title bar height in pixels
panel_height280-100Top/bottom panel height
gap40-100Gap between tiled windows
fontfixed-X11 font name for text
+
+ +
+ Example + +
+
[appearance]
+border_width = 2
+title_height = 28
+panel_height = 32
+gap = 8
+font = DejaVu Sans-10
+ + +

[layout] - Layout Behavior

+

+ Configure the default layout mode and tiling parameters. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionDefaultRangeDescription
defaulttiling-tiling, floating, or monocle
master_ratio0.550.1-0.9Portion of screen for master area
master_count11-10Number of windows in master area
+
+ +
+ Example + +
+
[layout]
+default = tiling
+master_ratio = 0.60
+master_count = 1
+ + +

[panels] - Panel Visibility

+

+ Control which panels are displayed. +

+ +
+ + + + + + + + + + + + + + + + + + + + +
OptionDefaultDescription
toptrueShow top panel (workspaces, taskbar, systray)
bottomtrueShow bottom panel (clock)
+
+ +
+ Example - Minimal Setup + +
+
[panels]
+top = true
+bottom = false
+ + +

[colors] - Color Scheme

+

+ Customize all colors using hex format (#RRGGBB). +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionDefaultDescription
panel_bg#1a1a2ePanel background color
panel_fg#e0e0e0Panel text color
workspace_active#4a90d9Active workspace indicator
workspace_inactive#3a3a4eInactive workspace indicator
workspace_urgent#d94a4aUrgent workspace indicator
title_focused_bg#2d3a4aFocused window title background
title_focused_fg#ffffffFocused window title text
title_unfocused_bg#1a1a1aUnfocused window title background
title_unfocused_fg#808080Unfocused window title text
border_focused#4a90d9Focused window border
border_unfocused#333333Unfocused window border
notification_bg#2a2a3eNotification background
notification_fg#ffffffNotification text
+
+ +
+ Example - Nord Theme + +
+
[colors]
+panel_bg = #2e3440
+panel_fg = #eceff4
+workspace_active = #88c0d0
+workspace_inactive = #4c566a
+workspace_urgent = #bf616a
+title_focused_bg = #3b4252
+title_focused_fg = #eceff4
+title_unfocused_bg = #2e3440
+title_unfocused_fg = #4c566a
+border_focused = #88c0d0
+border_unfocused = #3b4252
+notification_bg = #3b4252
+notification_fg = #eceff4
+ + +

[ai] - AI Integration

+

+ Configure AI features. See AI Features for full setup instructions. +

+ +
+ + + + + + + + + + + + + + + + + + + + + +
OptionDescription
modelOpenRouter model ID (e.g., google/gemini-2.0-flash-exp:free)
openrouter_api_keyYour OpenRouter API key (or use environment variable)
exa_api_keyYour Exa API key (or use environment variable)
+
+ +
+ Example + +
+
[ai]
+model = google/gemini-2.0-flash-exp:free
+openrouter_api_key = sk-or-v1-your-key-here
+exa_api_key = your-exa-key-here
+ +
+ Security Note +

For better security, use environment variables instead of storing API keys in the config file: + export OPENROUTER_API_KEY=sk-or-v1-...

+
+ + +

Complete Configuration Example

+
+ ~/.config/dwn/config + +
+
# DWN Window Manager Configuration
+# https://dwn.github.io
+
+[general]
+terminal = alacritty
+launcher = rofi -show drun
+file_manager = thunar
+focus_mode = click
+decorations = true
+
+[appearance]
+border_width = 2
+title_height = 24
+panel_height = 28
+gap = 6
+font = DejaVu Sans-10
+
+[layout]
+default = tiling
+master_ratio = 0.55
+master_count = 1
+
+[panels]
+top = true
+bottom = true
+
+[colors]
+panel_bg = #1a1a2e
+panel_fg = #e0e0e0
+workspace_active = #4a90d9
+workspace_inactive = #3a3a4e
+workspace_urgent = #d94a4a
+title_focused_bg = #2d3a4a
+title_focused_fg = #ffffff
+title_unfocused_bg = #1a1a1a
+title_unfocused_fg = #808080
+border_focused = #4a90d9
+border_unfocused = #333333
+notification_bg = #2a2a3e
+notification_fg = #ffffff
+
+[ai]
+model = google/gemini-2.0-flash-exp:free
+# API keys via environment variables recommended
+ +
+
+
+ + + + + + diff --git a/site/css/style.css b/site/css/style.css new file mode 100644 index 0000000..0e492dd --- /dev/null +++ b/site/css/style.css @@ -0,0 +1,1085 @@ +/* DWN Window Manager - Website Styles */ + +/* CSS Variables */ +:root { + --primary: #4a90d9; + --primary-dark: #3a7bc8; + --primary-light: #6ba3e0; + --secondary: #2d3a4a; + --accent: #d94a4a; + --bg-dark: #1a1a2e; + --bg-darker: #12121f; + --bg-card: #2a2a3e; + --bg-light: #f5f5f7; + --text-light: #e0e0e0; + --text-muted: #808080; + --text-dark: #1a1a1a; + --border-color: #3a3a4e; + --success: #4ad94a; + --warning: #d9d94a; + --code-bg: #1e1e2e; + --gradient-start: #1a1a2e; + --gradient-end: #2d3a4a; + --font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.2); + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 16px; + --transition: 0.2s ease; +} + +/* Reset & Base */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + scroll-behavior: smooth; + font-size: 16px; +} + +body { + font-family: var(--font-sans); + background: var(--bg-dark); + color: var(--text-light); + line-height: 1.6; + min-height: 100vh; +} + +a { + color: var(--primary); + text-decoration: none; + transition: color var(--transition); +} + +a:hover { + color: var(--primary-light); +} + +img { + max-width: 100%; + height: auto; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 600; + line-height: 1.3; + margin-bottom: 1rem; +} + +h1 { font-size: 3rem; } +h2 { font-size: 2.25rem; } +h3 { font-size: 1.75rem; } +h4 { font-size: 1.25rem; } + +p { + margin-bottom: 1rem; +} + +/* Layout */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1.5rem; +} + +.section { + padding: 5rem 0; +} + +.section-alt { + background: var(--bg-darker); +} + +/* Header & Navigation */ +header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: rgba(26, 26, 46, 0.95); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border-color); +} + +nav { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + max-width: 1200px; + margin: 0 auto; +} + +.logo { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 1.5rem; + font-weight: 700; + color: var(--text-light); +} + +.logo-icon { + width: 40px; + height: 40px; + background: linear-gradient(135deg, var(--primary), var(--primary-dark)); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 1.25rem; +} + +.nav-links { + display: flex; + list-style: none; + gap: 2rem; +} + +.nav-links a { + color: var(--text-light); + font-weight: 500; + padding: 0.5rem 0; + position: relative; +} + +.nav-links a::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 2px; + background: var(--primary); + transition: width var(--transition); +} + +.nav-links a:hover::after, +.nav-links a.active::after { + width: 100%; +} + +.nav-toggle { + display: none; + flex-direction: column; + gap: 5px; + cursor: pointer; + padding: 0.5rem; +} + +.nav-toggle span { + width: 25px; + height: 3px; + background: var(--text-light); + border-radius: 2px; + transition: var(--transition); +} + +/* Dropdown Menu */ +.dropdown { + position: relative; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 0.5rem 0; + min-width: 200px; + opacity: 0; + visibility: hidden; + transform: translateY(10px); + transition: all var(--transition); + box-shadow: var(--shadow-lg); +} + +.dropdown:hover .dropdown-menu { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.dropdown-menu a { + display: block; + padding: 0.75rem 1rem; + color: var(--text-light); +} + +.dropdown-menu a:hover { + background: var(--bg-dark); +} + +/* Hero Section */ +.hero { + padding: 10rem 0 6rem; + background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%); + position: relative; + overflow: hidden; +} + +.hero::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%234a90d9' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); + opacity: 0.5; +} + +.hero-content { + position: relative; + text-align: center; + max-width: 800px; + margin: 0 auto; +} + +.hero h1 { + font-size: 3.5rem; + margin-bottom: 1.5rem; + background: linear-gradient(135deg, var(--text-light) 0%, var(--primary-light) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero .subtitle { + font-size: 1.25rem; + color: var(--text-muted); + margin-bottom: 2rem; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.hero-buttons { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.875rem 1.75rem; + border-radius: var(--radius-md); + font-weight: 600; + font-size: 1rem; + cursor: pointer; + border: none; + transition: all var(--transition); +} + +.btn-primary { + background: var(--primary); + color: white; +} + +.btn-primary:hover { + background: var(--primary-dark); + color: white; + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.btn-secondary { + background: transparent; + color: var(--text-light); + border: 2px solid var(--border-color); +} + +.btn-secondary:hover { + border-color: var(--primary); + color: var(--primary); +} + +.btn-sm { + padding: 0.5rem 1rem; + font-size: 0.875rem; +} + +.btn-lg { + padding: 1rem 2rem; + font-size: 1.125rem; +} + +/* Feature Cards */ +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin-top: 3rem; +} + +.feature-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: 2rem; + transition: all var(--transition); +} + +.feature-card:hover { + transform: translateY(-4px); + border-color: var(--primary); + box-shadow: var(--shadow-lg); +} + +.feature-icon { + width: 60px; + height: 60px; + background: linear-gradient(135deg, var(--primary), var(--primary-dark)); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.75rem; + margin-bottom: 1.5rem; +} + +.feature-card h3 { + font-size: 1.25rem; + margin-bottom: 0.75rem; +} + +.feature-card p { + color: var(--text-muted); + margin-bottom: 0; +} + +/* Stats Section */ +.stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 2rem; + text-align: center; + padding: 3rem 0; +} + +.stat-item h3 { + font-size: 3rem; + color: var(--primary); + margin-bottom: 0.5rem; +} + +.stat-item p { + color: var(--text-muted); + font-size: 1.125rem; +} + +/* Code Blocks */ +pre, code { + font-family: var(--font-mono); +} + +code { + background: var(--code-bg); + padding: 0.2em 0.4em; + border-radius: var(--radius-sm); + font-size: 0.9em; +} + +pre { + background: var(--code-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 1.5rem; + overflow-x: auto; + margin: 1.5rem 0; +} + +pre code { + background: none; + padding: 0; + font-size: 0.875rem; + line-height: 1.7; +} + +.code-header { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--bg-darker); + padding: 0.75rem 1rem; + border-radius: var(--radius-md) var(--radius-md) 0 0; + border: 1px solid var(--border-color); + border-bottom: none; + margin-top: 1.5rem; +} + +.code-header + pre { + margin-top: 0; + border-radius: 0 0 var(--radius-md) var(--radius-md); +} + +.code-header span { + color: var(--text-muted); + font-size: 0.875rem; +} + +.copy-btn { + background: var(--bg-card); + border: 1px solid var(--border-color); + color: var(--text-light); + padding: 0.25rem 0.75rem; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.75rem; + transition: all var(--transition); +} + +.copy-btn:hover { + background: var(--primary); + border-color: var(--primary); +} + +/* Tables */ +.table-wrapper { + overflow-x: auto; + margin: 1.5rem 0; +} + +table { + width: 100%; + border-collapse: collapse; + background: var(--bg-card); + border-radius: var(--radius-md); + overflow: hidden; +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +th { + background: var(--bg-darker); + font-weight: 600; + color: var(--text-light); +} + +tr:last-child td { + border-bottom: none; +} + +tr:hover td { + background: rgba(74, 144, 217, 0.05); +} + +kbd { + display: inline-block; + background: var(--bg-darker); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + padding: 0.2em 0.5em; + font-family: var(--font-mono); + font-size: 0.85em; + box-shadow: 0 2px 0 var(--border-color); +} + +/* Sidebar Layout (for docs) */ +.docs-layout { + display: grid; + grid-template-columns: 280px 1fr; + gap: 3rem; + padding-top: 6rem; + min-height: 100vh; +} + +.docs-sidebar { + position: sticky; + top: 6rem; + height: calc(100vh - 7rem); + overflow-y: auto; + padding: 2rem 0; + border-right: 1px solid var(--border-color); +} + +.docs-sidebar ul { + list-style: none; +} + +.docs-sidebar > ul > li { + margin-bottom: 1.5rem; +} + +.docs-sidebar .section-title { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-muted); + margin-bottom: 0.75rem; + padding-left: 1rem; +} + +.docs-sidebar a { + display: block; + padding: 0.5rem 1rem; + color: var(--text-light); + border-radius: var(--radius-sm); + transition: all var(--transition); +} + +.docs-sidebar a:hover { + background: var(--bg-card); +} + +.docs-sidebar a.active { + background: var(--primary); + color: white; +} + +.docs-content { + padding: 2rem 0 4rem; + max-width: 800px; +} + +.docs-content h1 { + margin-bottom: 0.5rem; +} + +.docs-content .lead { + font-size: 1.25rem; + color: var(--text-muted); + margin-bottom: 2rem; +} + +/* Cards */ +.card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: 2rem; + margin-bottom: 1.5rem; +} + +.card h3 { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.card-icon { + width: 40px; + height: 40px; + background: var(--primary); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; +} + +/* Alert Boxes */ +.alert { + padding: 1rem 1.5rem; + border-radius: var(--radius-md); + margin: 1.5rem 0; + border-left: 4px solid; +} + +.alert-info { + background: rgba(74, 144, 217, 0.1); + border-color: var(--primary); +} + +.alert-warning { + background: rgba(217, 217, 74, 0.1); + border-color: var(--warning); +} + +.alert-success { + background: rgba(74, 217, 74, 0.1); + border-color: var(--success); +} + +.alert-danger { + background: rgba(217, 74, 74, 0.1); + border-color: var(--accent); +} + +.alert-title { + font-weight: 600; + margin-bottom: 0.25rem; +} + +/* Tabs */ +.tabs { + display: flex; + gap: 0; + border-bottom: 1px solid var(--border-color); + margin-bottom: 1.5rem; +} + +.tab { + padding: 1rem 1.5rem; + cursor: pointer; + color: var(--text-muted); + border-bottom: 2px solid transparent; + transition: all var(--transition); + background: none; + border-top: none; + border-left: none; + border-right: none; + font-size: 1rem; +} + +.tab:hover { + color: var(--text-light); +} + +.tab.active { + color: var(--primary); + border-bottom-color: var(--primary); +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* Comparison Table */ +.comparison { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 2rem; + margin: 3rem 0; +} + +.comparison-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: 2rem; + text-align: center; +} + +.comparison-card.featured { + border-color: var(--primary); + position: relative; +} + +.comparison-card.featured::before { + content: 'Recommended'; + position: absolute; + top: -12px; + left: 50%; + transform: translateX(-50%); + background: var(--primary); + color: white; + padding: 0.25rem 1rem; + border-radius: var(--radius-sm); + font-size: 0.75rem; + font-weight: 600; +} + +.comparison-card h3 { + font-size: 1.5rem; + margin-bottom: 1rem; +} + +.comparison-card ul { + list-style: none; + text-align: left; + margin: 1.5rem 0; +} + +.comparison-card li { + padding: 0.5rem 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.comparison-card li::before { + content: '✓'; + color: var(--success); + font-weight: 600; +} + +/* Footer */ +footer { + background: var(--bg-darker); + border-top: 1px solid var(--border-color); + padding: 4rem 0 2rem; +} + +.footer-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 3rem; + margin-bottom: 3rem; +} + +.footer-section h4 { + font-size: 1rem; + margin-bottom: 1rem; + color: var(--text-light); +} + +.footer-section ul { + list-style: none; +} + +.footer-section li { + margin-bottom: 0.5rem; +} + +.footer-section a { + color: var(--text-muted); +} + +.footer-section a:hover { + color: var(--primary); +} + +.footer-bottom { + text-align: center; + padding-top: 2rem; + border-top: 1px solid var(--border-color); + color: var(--text-muted); +} + +/* Screenshots/Gallery */ +.screenshot-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 1.5rem; + margin: 2rem 0; +} + +.screenshot { + background: var(--bg-darker); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + overflow: hidden; + transition: all var(--transition); +} + +.screenshot:hover { + transform: scale(1.02); + box-shadow: var(--shadow-lg); +} + +.screenshot-placeholder { + aspect-ratio: 16 / 10; + background: linear-gradient(135deg, var(--bg-dark) 0%, var(--bg-card) 100%); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + font-size: 0.875rem; +} + +.screenshot-caption { + padding: 1rem; + font-size: 0.875rem; + color: var(--text-muted); +} + +/* Step-by-step Guide */ +.steps { + margin: 2rem 0; +} + +.step { + display: flex; + gap: 1.5rem; + margin-bottom: 2rem; +} + +.step-number { + flex-shrink: 0; + width: 40px; + height: 40px; + background: var(--primary); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + color: white; +} + +.step-content h4 { + margin-bottom: 0.5rem; +} + +.step-content p { + color: var(--text-muted); +} + +/* Testimonials */ +.testimonials { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin: 3rem 0; +} + +.testimonial { + background: var(--bg-card); + border-radius: var(--radius-lg); + padding: 2rem; + border: 1px solid var(--border-color); +} + +.testimonial-text { + font-style: italic; + margin-bottom: 1.5rem; + color: var(--text-light); +} + +.testimonial-author { + display: flex; + align-items: center; + gap: 1rem; +} + +.testimonial-avatar { + width: 48px; + height: 48px; + background: var(--primary); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + color: white; +} + +.testimonial-info strong { + display: block; + color: var(--text-light); +} + +.testimonial-info span { + font-size: 0.875rem; + color: var(--text-muted); +} + +/* FAQ Accordion */ +.faq-item { + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + margin-bottom: 1rem; + overflow: hidden; +} + +.faq-question { + width: 100%; + padding: 1.25rem; + background: var(--bg-card); + border: none; + text-align: left; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 1rem; + font-weight: 500; + color: var(--text-light); + transition: all var(--transition); +} + +.faq-question:hover { + background: var(--bg-darker); +} + +.faq-question::after { + content: '+'; + font-size: 1.5rem; + color: var(--primary); + transition: transform var(--transition); +} + +.faq-item.active .faq-question::after { + transform: rotate(45deg); +} + +.faq-answer { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; +} + +.faq-item.active .faq-answer { + max-height: 500px; +} + +.faq-answer-content { + padding: 1.25rem; + color: var(--text-muted); + border-top: 1px solid var(--border-color); +} + +/* Badges */ +.badge { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.badge-primary { + background: rgba(74, 144, 217, 0.2); + color: var(--primary); +} + +.badge-success { + background: rgba(74, 217, 74, 0.2); + color: var(--success); +} + +.badge-warning { + background: rgba(217, 217, 74, 0.2); + color: var(--warning); +} + +/* Search Box */ +.search-box { + position: relative; + margin-bottom: 2rem; +} + +.search-box input { + width: 100%; + padding: 1rem 1rem 1rem 3rem; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + color: var(--text-light); + font-size: 1rem; + transition: all var(--transition); +} + +.search-box input:focus { + outline: none; + border-color: var(--primary); +} + +.search-box::before { + content: '🔍'; + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); +} + +/* Responsive */ +@media (max-width: 968px) { + .docs-layout { + grid-template-columns: 1fr; + } + + .docs-sidebar { + position: relative; + top: 0; + height: auto; + border-right: none; + border-bottom: 1px solid var(--border-color); + padding: 1rem 0; + } +} + +@media (max-width: 768px) { + .nav-links { + position: fixed; + top: 70px; + left: 0; + right: 0; + background: var(--bg-dark); + flex-direction: column; + padding: 2rem; + gap: 1rem; + border-bottom: 1px solid var(--border-color); + transform: translateY(-100%); + opacity: 0; + visibility: hidden; + transition: all var(--transition); + } + + .nav-links.active { + transform: translateY(0); + opacity: 1; + visibility: visible; + } + + .nav-toggle { + display: flex; + } + + .hero h1 { + font-size: 2.5rem; + } + + h1 { font-size: 2.25rem; } + h2 { font-size: 1.75rem; } + + .hero { + padding: 8rem 0 4rem; + } + + .section { + padding: 3rem 0; + } + + .step { + flex-direction: column; + gap: 1rem; + } +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeIn 0.5s ease forwards; +} + +/* Print Styles */ +@media print { + header, footer, .nav-toggle { + display: none; + } + + body { + background: white; + color: black; + } + + .hero { + padding: 2rem 0; + } +} diff --git a/site/documentation.html b/site/documentation.html new file mode 100644 index 0000000..15d24f8 --- /dev/null +++ b/site/documentation.html @@ -0,0 +1,423 @@ + + + + + + + Documentation - DWN Window Manager + + + +
+ +
+ +
+ + +
+

Getting Started with DWN

+

+ Learn the fundamentals of DWN and become productive in minutes. +

+ +

First Steps

+

+ After installing DWN and starting your session, + you'll see a clean desktop with two panels: a top panel with workspace indicators, + taskbar, and system tray, and a bottom panel showing the clock. +

+ +

Opening Your First Application

+

Start by launching a terminal and application launcher:

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ShortcutAction
Ctrl + Alt + TOpen terminal
Alt + F2Open application launcher (dmenu/rofi)
Super + EOpen file manager
Super + BOpen web browser
+
+ +
+ Tip: Run the Tutorial +

Press Super + T to start an interactive + tutorial that will guide you through all essential shortcuts.

+
+ +

Basic Concepts

+ +

The Super Key

+

+ Most DWN shortcuts use the Super key (often the Windows key or Command key). + This keeps shortcuts separate from application shortcuts that typically use + Ctrl or Alt. +

+ +

Focus Model

+

+ By default, DWN uses "click to focus" - you click on a window to focus it. + You can change this to "focus follows mouse" (sloppy focus) in the configuration. +

+ +

Window Decorations

+

+ Each window has a title bar showing its name. The title bar color indicates focus: +

+
    +
  • Blue title bar - Focused window
  • +
  • Gray title bar - Unfocused window
  • +
+ +

Interactive Tutorial

+

+ DWN includes a built-in interactive tutorial that teaches you essential shortcuts + step by step. The tutorial: +

+
    +
  • Shows instructions for each shortcut
  • +
  • Waits for you to press the correct keys
  • +
  • Automatically advances when you complete each step
  • +
  • Covers all essential shortcuts from basic to advanced
  • +
+ +
+

Start the Tutorial

+

Press Super + T at any time to start or resume the tutorial.

+
+ +

Managing Windows

+ +

Window Operations

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ShortcutAction
Alt + F4Close focused window
Alt + TabCycle to next window
Alt + Shift + TabCycle to previous window
Alt + F10Toggle maximize
Alt + F11Toggle fullscreen
Super + F9Toggle floating for current window
+
+ +

Moving and Resizing

+

In floating mode, you can move and resize windows with the mouse:

+
    +
  • Move - Click and drag the title bar
  • +
  • Resize - Drag any window edge or corner
  • +
+ +

Using Workspaces

+

+ DWN provides 9 virtual workspaces to organize your windows. You can see which + workspaces are active in the top panel. +

+ +

Workspace Navigation

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ShortcutAction
F1 - F9Switch to workspace 1-9
Shift + F1 - F9Move window to workspace 1-9
Ctrl + Alt + RightNext workspace
Ctrl + Alt + LeftPrevious workspace
+
+ +

Workspace Organization Tips

+
    +
  • Workspace 1 - Main work (editor, terminal)
  • +
  • Workspace 2 - Web browser, documentation
  • +
  • Workspace 3 - Communication (email, chat)
  • +
  • Workspace 4-9 - Project-specific contexts
  • +
+ +

Layout Modes

+

+ DWN supports three layout modes. Press Super + Space to cycle + through them. +

+ +
+
+

Tiling

+

+ Windows automatically arranged in master-stack layout. + Perfect for development workflows. +

+
+
+

Floating

+

+ Traditional overlapping windows. + Move and resize freely. +

+
+
+

Monocle

+

+ One fullscreen window at a time. + Great for focused work. +

+
+
+ +

Tiling Layout Controls

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ShortcutAction
Super + HShrink master area
Super + LExpand master area
Super + IIncrease master window count
Super + DDecrease master window count
+
+ +

Panels & System Tray

+ +

Top Panel

+

The top panel contains:

+
    +
  • Workspace indicators - Click to switch, highlighted when occupied
  • +
  • Taskbar - Shows windows on current workspace
  • +
  • System tray - Battery, volume, WiFi (see below)
  • +
+ +

System Tray

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IndicatorClickRight-clickScroll
VolumeShow sliderToggle muteAdjust volume
WiFiShow networksDisconnect-
Battery---
+
+ +

Bottom Panel

+

+ The bottom panel shows the current time and date. Both panels can be hidden + in the configuration if you prefer a minimal setup. +

+ +

Next Steps

+

Now that you know the basics, explore these topics:

+ +
+
+ + + + + + diff --git a/site/features.html b/site/features.html new file mode 100644 index 0000000..b72b795 --- /dev/null +++ b/site/features.html @@ -0,0 +1,411 @@ + + + + + + + Features - DWN Window Manager + + + +
+ +
+ +
+
+
+

Powerful Features

+

+ Everything you need for a productive desktop experience, without the bloat. +

+
+
+ + +
+
+

Window Management

+

+ DWN provides flexible window management that adapts to your workflow, whether you prefer + the precision of tiling or the freedom of floating windows. +

+ +
+
+

Tiling Layout

+

Master-stack tiling with configurable master area ratio. Windows automatically + organize into a primary area and a stack, maximizing screen real estate.

+
    +
  • Adjustable master area ratio (0.1 - 0.9)
  • +
  • Multiple windows in master area
  • +
  • Smart stack arrangement
  • +
  • Configurable gaps between windows
  • +
+
+
+

Floating Layout

+

Traditional floating window management with drag-and-drop positioning. + Perfect for workflows that need overlapping windows or free-form arrangement.

+
    +
  • Click and drag to move windows
  • +
  • Resize from any edge or corner
  • +
  • Window snapping support
  • +
  • Respect minimum size hints
  • +
+
+
+

Monocle Layout

+

Full-screen single window mode for focused work. Each window takes up + the entire workspace, perfect for presentations or deep concentration.

+
    +
  • Maximize focused window
  • +
  • Quick window cycling
  • +
  • Ideal for single-task focus
  • +
  • Works great on small screens
  • +
+
+
+ +
+ Pro Tip +

Switch layouts instantly with Super + Space. + Your window arrangement is preserved when switching back.

+
+
+
+ + +
+
+

Virtual Workspaces

+

+ Nine virtual desktops give you unlimited room to organize your work. + Each workspace maintains its own window state and layout preferences. +

+ +
+
+
1-9
+

9 Workspaces

+

Quick access via F1-F9 keys. Organize projects, contexts, or tasks across + dedicated spaces.

+
+
+
+

Window Transfer

+

Move windows between workspaces with Shift+F1-F9. Quick and keyboard-driven.

+
+
+
📈
+

Per-Workspace State

+

Each workspace remembers its layout mode, window positions, and focused window.

+
+
+
👁
+

Visual Indicators

+

Panel shows active and occupied workspaces at a glance with color coding.

+
+
+
+
+ + +
+
+

Panels & System Tray

+

+ Built-in panels provide essential information and quick access to common functions + without needing external tools or status bars. +

+ +
+
+

Top Panel

+

The top panel contains your main controls and information:

+
    +
  • Workspace Indicators - Click or use shortcuts to switch
  • +
  • Taskbar - Shows windows on current workspace
  • +
  • System Tray - Battery, volume, WiFi indicators
  • +
+
+
+

Bottom Panel

+

Optional bottom panel for additional information:

+
    +
  • Clock Display - Time and date
  • +
  • Status Messages - System notifications
  • +
  • Customizable - Can be hidden in config
  • +
+
+
+ +

System Tray Features

+
+
+

🔋 Battery Monitor

+

Shows current battery percentage with color-coded status:

+
    +
  • Red when below 20%
  • +
  • Blue when charging
  • +
  • Auto-hides on desktops
  • +
+
+
+

🔊 Volume Control

+

Full audio control at your fingertips:

+
    +
  • Click for volume slider
  • +
  • Scroll to adjust
  • +
  • Right-click to mute
  • +
+
+
+

📶 WiFi Manager

+

Network management made simple:

+
    +
  • Click for network list
  • +
  • Signal strength indicators
  • +
  • Current SSID display
  • +
+
+
+
+
+ + +
+
+

Notification System

+

+ Built-in D-Bus notification daemon following freedesktop.org standards. + No need for external notification tools like dunst or notify-osd. +

+ +
+
+

Standards Compliant

+

Implements the org.freedesktop.Notifications D-Bus interface. + Works seamlessly with any application that sends desktop notifications.

+
+
+

Customizable Appearance

+

Configure notification colors and positioning through the config file. + Notifications match your overall color scheme automatically.

+
+
+ +
+ Capacity +

DWN can display up to 32 notifications simultaneously, + with automatic queuing and timeout management.

+
+
+
+ + +
+
+

AI Integration

+

+ Optional AI features powered by OpenRouter API and Exa semantic search. + Control your desktop with natural language and get intelligent assistance. +

+ +
+
+

🤖 AI Command Palette

+

Press Super + Shift + A and type natural + language commands like "open firefox" or "launch terminal".

+ Learn More +
+
+

🔍 Semantic Web Search

+

Search the web semantically with Exa integration. Find relevant content based + on meaning, not just keywords.

+ Learn More +
+
+

🎓 Context Analysis

+

AI analyzes your current workspace to understand what you're working on + and provides relevant suggestions.

+ Learn More +
+
+
+
+ + +
+
+

Standards Compliance

+

+ DWN implements EWMH and ICCCM protocols for maximum compatibility with X11 applications. +

+ +
+
+

EWMH Support

+

Extended Window Manager Hints for modern application features:

+
    +
  • _NET_WM_STATE (fullscreen, maximized, etc.)
  • +
  • _NET_ACTIVE_WINDOW
  • +
  • _NET_CLIENT_LIST and _NET_CLIENT_LIST_STACKING
  • +
  • _NET_CURRENT_DESKTOP and _NET_NUMBER_OF_DESKTOPS
  • +
  • _NET_WM_WINDOW_TYPE
  • +
+
+
+

ICCCM Compliance

+

Inter-Client Communication Conventions Manual support:

+
    +
  • WM_STATE management
  • +
  • WM_PROTOCOLS (WM_DELETE_WINDOW, WM_TAKE_FOCUS)
  • +
  • WM_NORMAL_HINTS (size hints)
  • +
  • WM_CLASS for window matching
  • +
  • WM_NAME and _NET_WM_NAME
  • +
+
+
+
+
+ + +
+
+

Technical Specifications

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SpecificationValue
LanguageANSI C (C99)
Maximum Clients256 windows
Workspaces9 virtual desktops
Monitor SupportUp to 8 monitors (Xinerama/Xrandr)
Notifications32 concurrent
Keybindings64 configurable shortcuts
Memory Usage< 5MB typical
ConfigurationINI-style (~/.config/dwn/config)
+
+
+
+ + +
+
+

Ready to Try DWN?

+

+ Get started in minutes with our simple installation process. +

+ +
+
+
+ + + + + + diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..b555703 --- /dev/null +++ b/site/index.html @@ -0,0 +1,352 @@ + + + + + + + + DWN Window Manager - Modern X11 Window Management + + + +
+ +
+ +
+ +
+
+

Modern Window Management for X11

+

+ DWN is a production-ready window manager written in ANSI C with XFCE-like functionality, + powerful tiling layouts, and optional AI integration. Fast, flexible, and fully featured. +

+ +
+
+ + +
+
+

Why Choose DWN?

+

+ DWN combines the simplicity of traditional floating window managers with the productivity + of tiling layouts, all wrapped in a modern, customizable package. +

+
+
+
+

Multiple Layouts

+

Switch seamlessly between tiling, floating, and monocle layouts. + Resize master areas and organize windows exactly how you work.

+
+
+
+

9 Workspaces

+

Organize your workflow across 9 virtual desktops with per-workspace + state. Move windows between workspaces with a single keystroke.

+
+
+
🎨
+

Fully Customizable

+

INI-style configuration with extensive theming options. Customize colors, + fonts, borders, gaps, and behavior to match your style.

+
+
+
💡
+

AI Integration

+

Optional AI command palette and semantic web search. Control your desktop + with natural language and get intelligent suggestions.

+
+
+
🔔
+

Notification Daemon

+

Built-in D-Bus notification support following freedesktop.org standards. + No need for external notification daemons.

+
+
+
💻
+

System Tray

+

Integrated system tray with battery, volume, and WiFi indicators. + Volume slider and network selection dropdowns included.

+
+
+
+
+ + +
+
+
+
+

~10K

+

Lines of Pure C

+
+
+

0

+

Runtime Dependencies

+
+
+

<5MB

+

Memory Footprint

+
+
+

64+

+

Keyboard Shortcuts

+
+
+
+
+ + +
+
+

Get Up and Running in Minutes

+

+ DWN is designed for easy installation and immediate productivity. + Build from source or use your distribution's package manager. +

+ +
+
+ Terminal + +
+
# Clone the repository
+git clone https://github.com/dwn/dwn.git
+cd dwn
+
+# Install dependencies (auto-detects your distro)
+make deps
+
+# Build and install
+make
+sudo make install
+
+# Add to your .xinitrc
+echo "exec dwn" >> ~/.xinitrc
+
+ +

+ Full Installation Guide +

+
+
+ + +
+
+

See DWN in Action

+

+ A clean, modern interface that stays out of your way while providing everything you need. +

+
+
+
+ [Tiling Layout with Terminal and Editor] +
+
Master-stack tiling layout perfect for development
+
+
+
+ [System Tray and Notifications] +
+
Integrated system tray with volume and WiFi controls
+
+
+
+ [AI Command Palette] +
+
AI-powered command palette for natural language control
+
+
+
+
+ + +
+
+

How DWN Compares

+

+ DWN bridges the gap between minimal tiling managers and full desktop environments. +

+
+
+

Minimal Tiling WMs

+

dwm, i3, bspwm

+
    +
  • Lightweight and fast
  • +
  • Keyboard-driven workflow
  • +
  • Highly customizable
  • +
  • Steep learning curve
  • +
  • Requires additional tools
  • +
+
+ +
+

Desktop Environments

+

XFCE, GNOME, KDE

+
    +
  • Feature complete
  • +
  • User-friendly
  • +
  • Heavy resource usage
  • +
  • Less customizable
  • +
  • Slower performance
  • +
+
+
+
+
+ + +
+
+

What Users Say

+
+
+

+ "Finally, a window manager that doesn't make me choose between + productivity and usability. DWN just works." +

+
+
JD
+
+ John Developer + Senior Software Engineer +
+
+
+
+

+ "The AI command palette changed how I interact with my desktop. + It's like having an assistant for window management." +

+
+
SA
+
+ Sarah Admin + System Administrator +
+
+
+
+

+ "Coming from i3, the learning curve was minimal. The built-in + tutorial had me productive in under 10 minutes." +

+
+
ML
+
+ Mike Linux + DevOps Engineer +
+
+
+
+
+
+ + +
+
+

Ready to Transform Your Desktop?

+

+ Join thousands of developers and power users who have made DWN their window manager of choice. +

+ +
+
+
+ + + + + + diff --git a/site/installation.html b/site/installation.html new file mode 100644 index 0000000..e85c15a --- /dev/null +++ b/site/installation.html @@ -0,0 +1,610 @@ + + + + + + + Installation - DWN Window Manager + + + +
+ +
+ +
+
+
+

Installation Guide

+

+ Get DWN running on your system in just a few minutes. +

+
+
+ + +
+
+

Requirements

+

+ DWN requires X11 and a few common libraries. Most Linux distributions include these by default. +

+ +
+

Required Dependencies

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LibraryPurposePackage (Debian/Ubuntu)
libX11X Window System client librarylibx11-dev
libXextX extensions librarylibxext-dev
libXineramaMulti-monitor supportlibxinerama-dev
libXrandrDisplay configurationlibxrandr-dev
libXftFont renderinglibxft-dev
fontconfigFont configurationlibfontconfig1-dev
libdbus-1D-Bus for notificationslibdbus-1-dev
libcurlAI features (optional)libcurl4-openssl-dev
+
+
+
+
+ + +
+
+

Quick Installation

+

+ The fastest way to get started. Our build system auto-detects your distribution. +

+ +
+
+
1
+
+

Clone the Repository

+

Download the latest source code from GitHub.

+
+ Terminal + +
+
git clone https://github.com/dwn/dwn.git
+cd dwn
+
+
+ +
+
2
+
+

Install Dependencies

+

Automatically install required packages for your distribution.

+
+ Terminal + +
+
make deps
+

+ Supports Debian, Ubuntu, Fedora, Arch, openSUSE, and Void Linux. +

+
+
+ +
+
3
+
+

Build DWN

+

Compile the window manager with optimizations.

+
+ Terminal + +
+
make
+
+
+ +
+
4
+
+

Install System-wide

+

Install the binary to your system PATH.

+
+ Terminal + +
+
sudo make install
+

+ Default location: /usr/local/bin/dwn. Override with PREFIX=/custom/path make install +

+
+
+ +
+
5
+
+

Configure Your Session

+

Add DWN to your X session startup.

+
+ ~/.xinitrc + +
+
exec dwn
+
+
+
+
+
+ + +
+
+

Distribution-Specific Instructions

+ +
+ + + + +
+ +
+

Debian / Ubuntu / Linux Mint

+
+ Terminal + +
+
# Install dependencies
+sudo apt update
+sudo apt install -y \
+    build-essential \
+    libx11-dev \
+    libxext-dev \
+    libxinerama-dev \
+    libxrandr-dev \
+    libxft-dev \
+    libfontconfig1-dev \
+    libdbus-1-dev \
+    libcurl4-openssl-dev \
+    pkg-config
+
+# Build and install
+git clone https://github.com/dwn/dwn.git
+cd dwn
+make
+sudo make install
+
+ +
+

Fedora / RHEL / CentOS

+
+ Terminal + +
+
# Install dependencies
+sudo dnf install -y \
+    gcc \
+    make \
+    libX11-devel \
+    libXext-devel \
+    libXinerama-devel \
+    libXrandr-devel \
+    libXft-devel \
+    fontconfig-devel \
+    dbus-devel \
+    libcurl-devel \
+    pkg-config
+
+# Build and install
+git clone https://github.com/dwn/dwn.git
+cd dwn
+make
+sudo make install
+
+ +
+

Arch Linux / Manjaro

+
+ Terminal + +
+
# Install dependencies
+sudo pacman -S --needed \
+    base-devel \
+    libx11 \
+    libxext \
+    libxinerama \
+    libxrandr \
+    libxft \
+    fontconfig \
+    dbus \
+    curl \
+    pkg-config
+
+# Build and install
+git clone https://github.com/dwn/dwn.git
+cd dwn
+make
+sudo make install
+
+ AUR Package +

An AUR package dwn-git may also be available: + yay -S dwn-git

+
+
+ +
+

Void Linux

+
+ Terminal + +
+
# Install dependencies
+sudo xbps-install -S \
+    base-devel \
+    libX11-devel \
+    libXext-devel \
+    libXinerama-devel \
+    libXrandr-devel \
+    libXft-devel \
+    fontconfig-devel \
+    dbus-devel \
+    libcurl-devel \
+    pkg-config
+
+# Build and install
+git clone https://github.com/dwn/dwn.git
+cd dwn
+make
+sudo make install
+
+
+
+ + +
+
+

Session Setup

+

+ Configure your display manager or xinit to start DWN. +

+ +
+
+

Using xinit / startx

+

For minimal setups using startx:

+
+ ~/.xinitrc + +
+
# Optional: set display settings
+xrandr --output DP-1 --mode 2560x1440
+
+# Optional: set wallpaper
+feh --bg-fill ~/wallpaper.jpg
+
+# Start DWN
+exec dwn
+

+ Then run startx from a TTY. +

+
+
+

Using a Display Manager

+

Create a desktop entry for GDM, LightDM, etc:

+
+ /usr/share/xsessions/dwn.desktop + +
+
[Desktop Entry]
+Name=DWN
+Comment=DWN Window Manager
+Exec=dwn
+Type=Application
+DesktopNames=DWN
+

+ DWN will appear in your display manager's session menu. +

+
+
+
+
+ + +
+
+

Testing in a Nested X Server

+

+ Test DWN without leaving your current session using Xephyr. +

+ +
+

Using make run

+

The easiest way to test DWN safely:

+
+ Terminal + +
+
# Make sure Xephyr is installed
+# Debian/Ubuntu: sudo apt install xserver-xephyr
+# Fedora: sudo dnf install xorg-x11-server-Xephyr
+# Arch: sudo pacman -S xorg-server-xephyr
+
+# Run DWN in a nested window
+make run
+

+ This opens a 1280x720 window running DWN. Perfect for experimenting with configuration changes. +

+
+ +
+

Manual Xephyr Setup

+

For more control over the test environment:

+
+ Terminal + +
+
# Start Xephyr on display :1
+Xephyr :1 -screen 1920x1080 &
+
+# Run DWN on that display
+DISPLAY=:1 ./dwn
+
+# Open a terminal in the test environment
+DISPLAY=:1 xterm &
+
+
+
+ + +
+
+

Post-Installation

+ +
+
+
1
+
+

Create Configuration Directory

+

DWN will create this automatically on first run, but you can set it up in advance:

+
+ Terminal + +
+
mkdir -p ~/.config/dwn
+
+
+ +
+
2
+
+

Run the Interactive Tutorial

+

Once DWN is running, press Super + T to start the built-in tutorial + that will teach you all the essential shortcuts.

+
+
+ +
+
3
+
+

View All Shortcuts

+

Press Super + S to see a complete list of keyboard shortcuts.

+
+
+ +
+
4
+
+

Customize Your Setup

+

See the Configuration Guide to personalize DWN to your liking.

+
+
+
+
+
+ + +
+
+

Troubleshooting

+ +
+ +
+
+

This error means DWN can't connect to an X server. Make sure:

+
    +
  • You're running from a TTY with startx, not from within another X session
  • +
  • The DISPLAY environment variable is set correctly
  • +
  • X server is installed and working (Xorg -configure to test)
  • +
+
+
+
+ +
+ +
+
+

Install pkg-config for your distribution:

+
sudo apt install pkg-config    # Debian/Ubuntu
+sudo dnf install pkg-config    # Fedora
+sudo pacman -S pkg-config      # Arch
+
+
+
+ +
+ +
+
+

Make sure you have the development packages installed, not just the runtime libraries. + On Debian/Ubuntu, install packages ending with -dev. + Run make deps to auto-install everything.

+
+
+
+ +
+ +
+
+

Check for conflicts with other programs grabbing keys:

+
    +
  • Make sure no other window manager is running
  • +
  • Check if compositor (like picom) is grabbing keys
  • +
  • Verify keyboard layout is correct with setxkbmap
  • +
+
+
+
+ +
+ +
+
+

DWN uses Xft for font rendering. Install some good fonts:

+
sudo apt install fonts-dejavu fonts-liberation  # Debian/Ubuntu
+sudo dnf install dejavu-fonts-all liberation-fonts  # Fedora
+

You can configure the font in ~/.config/dwn/config.

+
+
+
+
+
+ + +
+
+

Installation Complete?

+

+ Learn how to use DWN effectively with our documentation. +

+ +
+
+
+ + + + + + diff --git a/site/js/main.js b/site/js/main.js new file mode 100644 index 0000000..09b1aea --- /dev/null +++ b/site/js/main.js @@ -0,0 +1,304 @@ +/** + * DWN Window Manager - Website JavaScript + * Vanilla JS for interactivity + */ + +// Mobile Navigation Toggle +function toggleNav() { + const navLinks = document.querySelector('.nav-links'); + const toggle = document.querySelector('.nav-toggle'); + + navLinks.classList.toggle('active'); + toggle.classList.toggle('active'); +} + +// Close mobile nav when clicking outside +document.addEventListener('click', function(e) { + const nav = document.querySelector('nav'); + const navLinks = document.querySelector('.nav-links'); + + if (navLinks && navLinks.classList.contains('active')) { + if (!nav.contains(e.target)) { + navLinks.classList.remove('active'); + } + } +}); + +// Close mobile nav when clicking a link +document.querySelectorAll('.nav-links a').forEach(link => { + link.addEventListener('click', () => { + const navLinks = document.querySelector('.nav-links'); + if (navLinks) { + navLinks.classList.remove('active'); + } + }); +}); + +// Tab functionality +function showTab(tabId) { + // Hide all tab contents + document.querySelectorAll('.tab-content').forEach(content => { + content.classList.remove('active'); + }); + + // Deactivate all tabs + document.querySelectorAll('.tab').forEach(tab => { + tab.classList.remove('active'); + }); + + // Show selected tab content + const selectedContent = document.getElementById(tabId); + if (selectedContent) { + selectedContent.classList.add('active'); + } + + // Activate clicked tab + event.target.classList.add('active'); +} + +// FAQ Accordion +function toggleFaq(button) { + const faqItem = button.closest('.faq-item'); + const isActive = faqItem.classList.contains('active'); + + // Close all FAQ items + document.querySelectorAll('.faq-item').forEach(item => { + item.classList.remove('active'); + }); + + // Open clicked item if it wasn't already open + if (!isActive) { + faqItem.classList.add('active'); + } +} + +// Copy to clipboard functionality +function copyCode(button) { + const codeBlock = button.closest('.code-header').nextElementSibling; + const code = codeBlock.querySelector('code'); + + if (code) { + const text = code.textContent; + + navigator.clipboard.writeText(text).then(() => { + // Show feedback + const originalText = button.textContent; + button.textContent = 'Copied!'; + button.style.background = 'var(--success)'; + + setTimeout(() => { + button.textContent = originalText; + button.style.background = ''; + }, 2000); + }).catch(err => { + console.error('Failed to copy:', err); + button.textContent = 'Error'; + }); + } +} + +// Shortcut search/filter functionality +function filterShortcuts() { + const searchInput = document.getElementById('shortcut-search'); + if (!searchInput) return; + + const filter = searchInput.value.toLowerCase(); + const tables = document.querySelectorAll('.shortcuts-table'); + + tables.forEach(table => { + const rows = table.querySelectorAll('tbody tr'); + let visibleCount = 0; + + rows.forEach(row => { + const text = row.textContent.toLowerCase(); + if (text.includes(filter)) { + row.style.display = ''; + visibleCount++; + } else { + row.style.display = 'none'; + } + }); + + // Show/hide the entire table section if no matches + const section = table.closest('.table-wrapper'); + const header = section ? section.previousElementSibling : null; + + if (section && header && header.tagName === 'H2') { + if (visibleCount === 0 && filter !== '') { + section.style.display = 'none'; + header.style.display = 'none'; + } else { + section.style.display = ''; + header.style.display = ''; + } + } + }); +} + +// Smooth scroll for anchor links +document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function(e) { + const href = this.getAttribute('href'); + if (href === '#') return; + + e.preventDefault(); + const target = document.querySelector(href); + + if (target) { + const headerOffset = 80; // Account for fixed header + const elementPosition = target.getBoundingClientRect().top; + const offsetPosition = elementPosition + window.pageYOffset - headerOffset; + + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth' + }); + + // Update URL without scrolling + history.pushState(null, null, href); + } + }); +}); + +// Highlight active section in docs sidebar +function updateActiveSection() { + const sidebar = document.querySelector('.docs-sidebar'); + if (!sidebar) return; + + const sections = document.querySelectorAll('h2[id], h3[id]'); + const links = sidebar.querySelectorAll('a[href^="#"]'); + + let currentSection = ''; + const scrollPos = window.scrollY + 100; + + sections.forEach(section => { + if (section.offsetTop <= scrollPos) { + currentSection = section.id; + } + }); + + links.forEach(link => { + link.classList.remove('active'); + if (link.getAttribute('href') === '#' + currentSection) { + link.classList.add('active'); + } + }); +} + +// Throttle scroll events +let scrollTimeout; +window.addEventListener('scroll', () => { + if (scrollTimeout) return; + + scrollTimeout = setTimeout(() => { + updateActiveSection(); + scrollTimeout = null; + }, 100); +}); + +// Header background on scroll +function updateHeaderBackground() { + const header = document.querySelector('header'); + if (!header) return; + + if (window.scrollY > 50) { + header.style.background = 'rgba(26, 26, 46, 0.98)'; + } else { + header.style.background = 'rgba(26, 26, 46, 0.95)'; + } +} + +window.addEventListener('scroll', updateHeaderBackground); + +// Animate elements on scroll (intersection observer) +function initScrollAnimations() { + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('fade-in'); + observer.unobserve(entry.target); + } + }); + }, { + threshold: 0.1, + rootMargin: '0px 0px -50px 0px' + }); + + // Observe feature cards and other elements + document.querySelectorAll('.feature-card, .card, .comparison-card, .testimonial').forEach(el => { + el.style.opacity = '0'; + observer.observe(el); + }); +} + +// Keyboard shortcut for search (/) +document.addEventListener('keydown', (e) => { + if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) { + const searchInput = document.getElementById('shortcut-search'); + if (searchInput) { + e.preventDefault(); + searchInput.focus(); + } + } + + // Escape to close search/nav + if (e.key === 'Escape') { + const searchInput = document.getElementById('shortcut-search'); + if (searchInput && document.activeElement === searchInput) { + searchInput.blur(); + searchInput.value = ''; + filterShortcuts(); + } + + const navLinks = document.querySelector('.nav-links'); + if (navLinks) { + navLinks.classList.remove('active'); + } + } +}); + +// External link handling - open in new tab +document.querySelectorAll('a[href^="http"]').forEach(link => { + if (!link.hostname.includes(window.location.hostname)) { + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer'); + } +}); + +// Initialize on DOM ready +document.addEventListener('DOMContentLoaded', () => { + initScrollAnimations(); + updateActiveSection(); + updateHeaderBackground(); + + // Set current year in footer if needed + const yearSpan = document.querySelector('.current-year'); + if (yearSpan) { + yearSpan.textContent = new Date().getFullYear(); + } +}); + +// Print-friendly handling +window.addEventListener('beforeprint', () => { + // Expand all FAQs for printing + document.querySelectorAll('.faq-item').forEach(item => { + item.classList.add('active'); + }); + + // Show all tab contents + document.querySelectorAll('.tab-content').forEach(content => { + content.style.display = 'block'; + }); +}); + +window.addEventListener('afterprint', () => { + // Restore FAQ state + document.querySelectorAll('.faq-item').forEach(item => { + item.classList.remove('active'); + }); + + // Restore tab state + document.querySelectorAll('.tab-content').forEach(content => { + content.style.display = ''; + }); +}); diff --git a/site/shortcuts.html b/site/shortcuts.html new file mode 100644 index 0000000..53a6777 --- /dev/null +++ b/site/shortcuts.html @@ -0,0 +1,413 @@ + + + + + + + Keyboard Shortcuts - DWN Window Manager + + + +
+ +
+ +
+
+
+

Keyboard Shortcuts

+

+ Complete reference for all DWN keyboard shortcuts. + Press Super + S in DWN to view this list. +

+
+
+ +
+
+ + + +

Application Launchers

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ShortcutAction
Ctrl + Alt + TOpen terminal (configurable)
Alt + F2Open application launcher (dmenu/rofi)
Super + EOpen file manager (configurable)
Super + BOpen web browser
+
+ + +

Window Management

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ShortcutAction
Alt + F4Close focused window
Alt + TabCycle to next window
Alt + Shift + TabCycle to previous window
Alt + F10Toggle maximize
Alt + F11Toggle fullscreen
Super + F9Toggle floating mode for focused window
+
+ + +

Workspace Navigation

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ShortcutAction
F1Switch to workspace 1
F2Switch to workspace 2
F3Switch to workspace 3
F4Switch to workspace 4
F5Switch to workspace 5
F6Switch to workspace 6
F7Switch to workspace 7
F8Switch to workspace 8
F9Switch to workspace 9
Shift + F1Move focused window to workspace 1
Shift + F2Move focused window to workspace 2
Shift + F3Move focused window to workspace 3
Shift + F4Move focused window to workspace 4
Shift + F5Move focused window to workspace 5
Shift + F6Move focused window to workspace 6
Shift + F7Move focused window to workspace 7
Shift + F8Move focused window to workspace 8
Shift + F9Move focused window to workspace 9
Ctrl + Alt + RightSwitch to next workspace
Ctrl + Alt + LeftSwitch to previous workspace
+
+ + +

Layout Control

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ShortcutAction
Super + SpaceCycle layout mode (tiling → floating → monocle)
Super + HShrink master area
Super + LExpand master area
Super + IIncrease master window count
Super + DDecrease master window count
+
+ + +

AI Features

+

+ These shortcuts require API keys to be configured. See AI Features for setup. +

+
+ + + + + + + + + + + + + + + + + + + + + +
ShortcutAction
Super + AShow AI context analysis
Super + Shift + AOpen AI command palette
Super + Shift + EOpen Exa semantic web search
+
+ + +

Help & System

+
+ + + + + + + + + + + + + + + + + + + + + +
ShortcutAction
Super + SShow all keyboard shortcuts
Super + TStart/continue interactive tutorial
Super + BackspaceQuit DWN
+
+ + +
+

Printable Quick Reference

+

Essential shortcuts to memorize when starting with DWN:

+ +
+
+

Must Know

+
    +
  • + Ctrl+Alt+T Terminal +
  • +
  • + Alt+F2 Launcher +
  • +
  • + Alt+F4 Close window +
  • +
  • + Alt+Tab Switch windows +
  • +
  • + F1-F9 Workspaces +
  • +
+
+
+

Power User

+
    +
  • + Super+Space Change layout +
  • +
  • + Super+H/L Resize master +
  • +
  • + Shift+F1-9 Move to workspace +
  • +
  • + Super+Shift+A AI command +
  • +
  • + Super+S Show shortcuts +
  • +
+
+
+
+
+
+
+ + + + + + diff --git a/src/ai.c b/src/ai.c new file mode 100644 index 0000000..81ab432 --- /dev/null +++ b/src/ai.c @@ -0,0 +1,872 @@ +/* + * DWN - Desktop Window Manager + * AI Integration implementation + */ + +#include "ai.h" +#include "config.h" +#include "client.h" +#include "workspace.h" +#include "notifications.h" +#include "util.h" +#include "cJSON.h" + +#include +#include +#include +#include + +/* API endpoints */ +#define OPENROUTER_URL "https://openrouter.ai/api/v1/chat/completions" + +/* Request queue */ +static AIRequest *request_queue = NULL; +static CURLM *curl_multi = NULL; +static AIContext current_context; + +/* Response buffer for curl */ +typedef struct { + char *data; + size_t size; +} ResponseBuffer; + +/* ========== CURL callbacks ========== */ + +static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) +{ + size_t realsize = size * nmemb; + ResponseBuffer *buf = (ResponseBuffer *)userp; + + char *ptr = realloc(buf->data, buf->size + realsize + 1); + if (ptr == NULL) { + return 0; + } + + buf->data = ptr; + memcpy(&(buf->data[buf->size]), contents, realsize); + buf->size += realsize; + buf->data[buf->size] = '\0'; + + return realsize; +} + +/* ========== Initialization ========== */ + +bool ai_init(void) +{ + if (dwn == NULL || dwn->config == NULL) { + return false; + } + + if (dwn->config->openrouter_api_key[0] == '\0') { + LOG_INFO("AI features disabled (no OPENROUTER_API_KEY)"); + dwn->ai_enabled = false; + return true; /* Not an error, just disabled */ + } + + /* Initialize curl */ + curl_global_init(CURL_GLOBAL_DEFAULT); + curl_multi = curl_multi_init(); + + if (curl_multi == NULL) { + LOG_ERROR("Failed to initialize curl multi handle"); + return false; + } + + dwn->ai_enabled = true; + LOG_INFO("AI features enabled"); + + return true; +} + +void ai_cleanup(void) +{ + /* Cancel all pending requests */ + while (request_queue != NULL) { + AIRequest *next = request_queue->next; + if (request_queue->prompt) free(request_queue->prompt); + if (request_queue->response) free(request_queue->response); + free(request_queue); + request_queue = next; + } + + if (curl_multi != NULL) { + curl_multi_cleanup(curl_multi); + curl_multi = NULL; + } + + curl_global_cleanup(); +} + +bool ai_is_available(void) +{ + return dwn != NULL && dwn->ai_enabled; +} + +/* ========== API calls ========== */ + +AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *)) +{ + if (!ai_is_available() || prompt == NULL) { + return NULL; + } + + AIRequest *req = dwn_calloc(1, sizeof(AIRequest)); + req->prompt = dwn_strdup(prompt); + req->state = AI_STATE_PENDING; + req->callback = callback; + + /* Build JSON request body */ + char *json_prompt = dwn_malloc(strlen(prompt) * 2 + 256); + char *escaped_prompt = dwn_malloc(strlen(prompt) * 2 + 1); + + /* Escape special characters in prompt */ + const char *src = prompt; + char *dst = escaped_prompt; + while (*src) { + if (*src == '"' || *src == '\\' || *src == '\n' || *src == '\r' || *src == '\t') { + *dst++ = '\\'; + if (*src == '\n') *dst++ = 'n'; + else if (*src == '\r') *dst++ = 'r'; + else if (*src == '\t') *dst++ = 't'; + else *dst++ = *src; + } else { + *dst++ = *src; + } + src++; + } + *dst = '\0'; + + snprintf(json_prompt, strlen(prompt) * 2 + 256, + "{\"model\":\"%s\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}]}", + dwn->config->ai_model, escaped_prompt); + + dwn_free(escaped_prompt); + + /* Create curl easy handle */ + CURL *easy = curl_easy_init(); + if (easy == NULL) { + dwn_free(json_prompt); + dwn_free(req->prompt); + dwn_free(req); + return NULL; + } + + /* Response buffer */ + ResponseBuffer *response = dwn_calloc(1, sizeof(ResponseBuffer)); + + /* Set curl options */ + struct curl_slist *headers = NULL; + char auth_header[300]; + snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", + dwn->config->openrouter_api_key); + + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, auth_header); + + curl_easy_setopt(easy, CURLOPT_URL, OPENROUTER_URL); + curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(easy, CURLOPT_POSTFIELDS, json_prompt); + curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(easy, CURLOPT_WRITEDATA, response); + curl_easy_setopt(easy, CURLOPT_TIMEOUT, 30L); + curl_easy_setopt(easy, CURLOPT_PRIVATE, req); + curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L); + + /* Add to multi handle */ + curl_multi_add_handle(curl_multi, easy); + + /* Add to queue */ + req->next = request_queue; + request_queue = req; + + /* Store response buffer pointer for cleanup */ + req->user_data = response; + + LOG_DEBUG("AI request sent: %.50s...", prompt); + + /* Note: json_prompt and headers will be freed after request completes */ + + return req; +} + +void ai_cancel_request(AIRequest *req) +{ + if (req == NULL) { + return; + } + + /* Remove from queue */ + AIRequest **pp = &request_queue; + while (*pp != NULL) { + if (*pp == req) { + *pp = req->next; + break; + } + pp = &(*pp)->next; + } + + req->state = AI_STATE_ERROR; + + if (req->prompt) dwn_free(req->prompt); + if (req->response) dwn_free(req->response); + if (req->user_data) { + ResponseBuffer *buf = (ResponseBuffer *)req->user_data; + if (buf->data) free(buf->data); + dwn_free(buf); + } + dwn_free(req); +} + +void ai_process_pending(void) +{ + if (curl_multi == NULL) { + return; + } + + int running_handles; + curl_multi_perform(curl_multi, &running_handles); + + /* Check for completed requests */ + CURLMsg *msg; + int msgs_left; + + while ((msg = curl_multi_info_read(curl_multi, &msgs_left)) != NULL) { + if (msg->msg == CURLMSG_DONE) { + CURL *easy = msg->easy_handle; + AIRequest *req = NULL; + + curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req); + + if (req != NULL) { + ResponseBuffer *buf = (ResponseBuffer *)req->user_data; + + if (msg->data.result == CURLE_OK && buf != NULL && buf->data != NULL) { + /* Parse response using cJSON */ + /* OpenRouter format: {"choices":[{"message":{"content":"..."}}]} */ + cJSON *root = cJSON_Parse(buf->data); + if (root != NULL) { + cJSON *choices = cJSON_GetObjectItemCaseSensitive(root, "choices"); + if (cJSON_IsArray(choices) && cJSON_GetArraySize(choices) > 0) { + cJSON *first_choice = cJSON_GetArrayItem(choices, 0); + cJSON *message = cJSON_GetObjectItemCaseSensitive(first_choice, "message"); + if (message != NULL) { + cJSON *content = cJSON_GetObjectItemCaseSensitive(message, "content"); + if (cJSON_IsString(content) && content->valuestring != NULL) { + req->response = dwn_strdup(content->valuestring); + req->state = AI_STATE_COMPLETED; + } + } + } + /* Check for error in response */ + if (req->state != AI_STATE_COMPLETED) { + cJSON *error = cJSON_GetObjectItemCaseSensitive(root, "error"); + if (error != NULL) { + cJSON *err_msg = cJSON_GetObjectItemCaseSensitive(error, "message"); + if (cJSON_IsString(err_msg)) { + req->response = dwn_strdup(err_msg->valuestring); + req->state = AI_STATE_ERROR; + LOG_ERROR("AI API error: %s", err_msg->valuestring); + } + } + } + cJSON_Delete(root); + } + + if (req->state != AI_STATE_COMPLETED && req->state != AI_STATE_ERROR) { + /* Fallback: return raw response for debugging */ + req->response = dwn_strdup(buf->data); + req->state = AI_STATE_COMPLETED; + LOG_WARN("Could not parse AI response, returning raw"); + } + } else { + req->state = AI_STATE_ERROR; + LOG_ERROR("AI request failed: %s", curl_easy_strerror(msg->data.result)); + } + + /* Call callback */ + if (req->callback != NULL) { + req->callback(req); + } + + /* Cleanup */ + if (buf != NULL) { + if (buf->data) free(buf->data); + dwn_free(buf); + } + } + + curl_multi_remove_handle(curl_multi, easy); + curl_easy_cleanup(easy); + } + } +} + +/* ========== Context analysis ========== */ + +void ai_update_context(void) +{ + memset(¤t_context, 0, sizeof(current_context)); + + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + snprintf(current_context.focused_window, sizeof(current_context.focused_window), + "%s", ws->focused->title); + snprintf(current_context.focused_class, sizeof(current_context.focused_class), + "%s", ws->focused->class); + } + + /* Build list of windows on current workspace */ + int offset = 0; + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)dwn->current_workspace) { + int written = snprintf(current_context.workspace_windows + offset, + sizeof(current_context.workspace_windows) - offset, + "%s%s", offset > 0 ? ", " : "", c->title); + if (written > 0) { + offset += written; + } + current_context.window_count++; + } + } +} + +const char *ai_analyze_task(void) +{ + /* Analyze based on focused window class */ + const char *class = current_context.focused_class; + + if (strstr(class, "code") || strstr(class, "Code") || + strstr(class, "vim") || strstr(class, "emacs")) { + return "coding"; + } + if (strstr(class, "firefox") || strstr(class, "chrome") || + strstr(class, "Firefox") || strstr(class, "Chrome")) { + return "browsing"; + } + if (strstr(class, "slack") || strstr(class, "discord") || + strstr(class, "Slack") || strstr(class, "Discord")) { + return "communication"; + } + if (strstr(class, "terminal") || strstr(class, "Terminal")) { + return "terminal"; + } + + return "general"; +} + +const char *ai_suggest_window(void) +{ + /* Simple heuristic suggestion */ + const char *task = ai_analyze_task(); + + if (strcmp(task, "coding") == 0) { + return "Consider opening a terminal for testing"; + } + if (strcmp(task, "browsing") == 0) { + return "Documentation or reference material available?"; + } + + return NULL; +} + +const char *ai_suggest_app(void) +{ + return NULL; /* Would require more context */ +} + +/* ========== Command palette ========== */ + +/* Callback for AI command response */ +static void ai_command_response_callback(AIRequest *req) +{ + if (req == NULL) { + return; + } + + if (req->state == AI_STATE_COMPLETED && req->response != NULL) { + /* Check if response contains a command to execute */ + /* Format: [RUN: command] or [EXEC: command] */ + char *run_cmd = strstr(req->response, "[RUN:"); + if (run_cmd == NULL) { + run_cmd = strstr(req->response, "[EXEC:"); + } + + if (run_cmd != NULL) { + /* Extract command */ + char *cmd_start = strchr(run_cmd, ':'); + if (cmd_start != NULL) { + cmd_start++; + while (*cmd_start == ' ') cmd_start++; + + char *cmd_end = strchr(cmd_start, ']'); + if (cmd_end != NULL) { + size_t cmd_len = cmd_end - cmd_start; + char *cmd = dwn_malloc(cmd_len + 1); + strncpy(cmd, cmd_start, cmd_len); + cmd[cmd_len] = '\0'; + + /* Trim trailing spaces */ + while (cmd_len > 0 && cmd[cmd_len - 1] == ' ') { + cmd[--cmd_len] = '\0'; + } + + LOG_INFO("AI executing command: %s", cmd); + notification_show("DWN AI", "Running", cmd, NULL, 2000); + spawn_async(cmd); + dwn_free(cmd); + } + } + } else { + /* No command, just show response */ + notification_show("DWN AI", "Response", req->response, NULL, 8000); + } + } else { + notification_show("DWN AI", "Error", "Failed to get AI response", NULL, 3000); + } + + /* Cleanup - don't free req itself, it's managed by the queue */ + /* The queue will be cleaned up separately */ +} + +void ai_show_command_palette(void) +{ + if (!ai_is_available()) { + notification_show("DWN", "AI Unavailable", + "Set OPENROUTER_API_KEY to enable AI features", + NULL, 3000); + return; + } + + /* Check if dmenu or rofi is available */ + char *input = NULL; + + /* Try dmenu first, then rofi */ + if (spawn("command -v dmenu >/dev/null 2>&1") == 0) { + input = spawn_capture("echo '' | dmenu -p 'Ask AI:'"); + } else if (spawn("command -v rofi >/dev/null 2>&1") == 0) { + input = spawn_capture("rofi -dmenu -p 'Ask AI:'"); + } else { + notification_show("DWN AI", "Missing Dependency", + "Install dmenu or rofi for AI command palette:\n" + "sudo apt install dmenu", + NULL, 5000); + return; + } + + if (input == NULL || input[0] == '\0') { + if (input != NULL) { + dwn_free(input); + } + LOG_DEBUG("AI command palette cancelled"); + return; + } + + LOG_DEBUG("AI command palette input: %s", input); + + /* Show "thinking" notification */ + notification_show("DWN AI", "Processing...", input, NULL, 2000); + + /* Build context-aware prompt */ + ai_update_context(); + const char *task = ai_analyze_task(); + + char prompt[2048]; + snprintf(prompt, sizeof(prompt), + "You are an AI assistant integrated into a Linux window manager called DWN. " + "You can execute shell commands for the user.\n\n" + "IMPORTANT: When the user asks you to run, open, launch, or start an application, " + "respond with the command in this exact format: [RUN: command]\n" + "Examples:\n" + "- User: 'open chrome' -> [RUN: google-chrome]\n" + "- User: 'run firefox' -> [RUN: firefox]\n" + "- User: 'open file manager' -> [RUN: thunar]\n" + "- User: 'launch terminal' -> [RUN: xfce4-terminal]\n" + "- User: 'open vs code' -> [RUN: code]\n\n" + "For questions or non-command requests, respond briefly (1-2 sentences) without the [RUN:] format.\n\n" + "User's current task: %s\n" + "User's request: %s", + task, input); + + dwn_free(input); + + /* Send request */ + ai_send_request(prompt, ai_command_response_callback); +} + +void ai_execute_command(const char *command) +{ + if (!ai_is_available() || command == NULL) { + return; + } + + LOG_DEBUG("AI executing command: %s", command); + + /* Send to AI for interpretation */ + char prompt[512]; + snprintf(prompt, sizeof(prompt), + "User command: %s\nCurrent task: %s\nRespond with a single action to take.", + command, ai_analyze_task()); + + ai_send_request(prompt, NULL); +} + +/* ========== Smart features ========== */ + +void ai_auto_organize_workspace(void) +{ + LOG_DEBUG("AI auto-organize (placeholder)"); +} + +void ai_suggest_layout(void) +{ + LOG_DEBUG("AI layout suggestion (placeholder)"); +} + +void ai_analyze_workflow(void) +{ + LOG_DEBUG("AI workflow analysis (placeholder)"); +} + +/* ========== Notification intelligence ========== */ + +bool ai_should_show_notification(const char *app, const char *summary) +{ + /* Simple filtering - could be enhanced with AI */ + (void)app; + (void)summary; + return true; /* Show all by default */ +} + +int ai_notification_priority(const char *app, const char *summary) +{ + /* Simple priority assignment */ + if (strstr(summary, "urgent") || strstr(summary, "Urgent") || + strstr(summary, "error") || strstr(summary, "Error")) { + return 3; + } + if (strstr(app, "slack") || strstr(app, "Slack") || + strstr(app, "discord") || strstr(app, "Discord")) { + return 2; + } + return 1; +} + +/* ========== Performance monitoring ========== */ + +void ai_monitor_performance(void) +{ + /* Read from /proc for basic metrics */ + LOG_DEBUG("AI performance monitoring (placeholder)"); +} + +const char *ai_performance_suggestion(void) +{ + return NULL; +} + +/* ========== Exa Semantic Search ========== */ + +#define EXA_API_URL "https://api.exa.ai/search" + +static ExaRequest *exa_queue = NULL; + +bool exa_is_available(void) +{ + return dwn != NULL && dwn->config != NULL && + dwn->config->exa_api_key[0] != '\0'; +} + +/* Parse Exa JSON response using cJSON */ +static void exa_parse_response(ExaRequest *req, const char *json) +{ + if (req == NULL || json == NULL) { + return; + } + + req->result_count = 0; + + cJSON *root = cJSON_Parse(json); + if (root == NULL) { + LOG_WARN("Failed to parse Exa response JSON"); + return; + } + + cJSON *results = cJSON_GetObjectItemCaseSensitive(root, "results"); + if (!cJSON_IsArray(results)) { + cJSON_Delete(root); + return; + } + + int array_size = cJSON_GetArraySize(results); + for (int i = 0; i < array_size && req->result_count < 10; i++) { + cJSON *item = cJSON_GetArrayItem(results, i); + if (!cJSON_IsObject(item)) continue; + + ExaSearchResult *res = &req->results[req->result_count]; + + /* Extract title */ + cJSON *title = cJSON_GetObjectItemCaseSensitive(item, "title"); + if (cJSON_IsString(title) && title->valuestring != NULL) { + strncpy(res->title, title->valuestring, sizeof(res->title) - 1); + res->title[sizeof(res->title) - 1] = '\0'; + } + + /* Extract URL */ + cJSON *url = cJSON_GetObjectItemCaseSensitive(item, "url"); + if (cJSON_IsString(url) && url->valuestring != NULL) { + strncpy(res->url, url->valuestring, sizeof(res->url) - 1); + res->url[sizeof(res->url) - 1] = '\0'; + } + + /* Extract text/snippet if available */ + cJSON *text = cJSON_GetObjectItemCaseSensitive(item, "text"); + if (cJSON_IsString(text) && text->valuestring != NULL) { + strncpy(res->snippet, text->valuestring, sizeof(res->snippet) - 1); + res->snippet[sizeof(res->snippet) - 1] = '\0'; + } + + req->result_count++; + } + + cJSON_Delete(root); + LOG_DEBUG("Parsed %d Exa results", req->result_count); +} + +ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *)) +{ + if (!exa_is_available() || query == NULL) { + return NULL; + } + + ExaRequest *req = dwn_calloc(1, sizeof(ExaRequest)); + req->query = dwn_strdup(query); + req->state = AI_STATE_PENDING; + req->callback = callback; + req->result_count = 0; + + /* Build JSON request */ + char *json_query = dwn_malloc(strlen(query) * 2 + 256); + char *escaped = dwn_malloc(strlen(query) * 2 + 1); + + /* Escape query string */ + const char *src = query; + char *dst = escaped; + while (*src) { + if (*src == '"' || *src == '\\') { + *dst++ = '\\'; + } + *dst++ = *src++; + } + *dst = '\0'; + + snprintf(json_query, strlen(query) * 2 + 256, + "{\"query\":\"%s\",\"type\":\"auto\",\"numResults\":10,\"contents\":{\"text\":true}}", + escaped); + dwn_free(escaped); + + /* Create curl handle */ + CURL *easy = curl_easy_init(); + if (easy == NULL) { + dwn_free(json_query); + dwn_free(req->query); + dwn_free(req); + return NULL; + } + + /* Response buffer */ + ResponseBuffer *response = dwn_calloc(1, sizeof(ResponseBuffer)); + + /* Set headers */ + struct curl_slist *headers = NULL; + char api_header[300]; + snprintf(api_header, sizeof(api_header), "x-api-key: %s", + dwn->config->exa_api_key); + + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, api_header); + + curl_easy_setopt(easy, CURLOPT_URL, EXA_API_URL); + curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(easy, CURLOPT_POSTFIELDS, json_query); + curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(easy, CURLOPT_WRITEDATA, response); + curl_easy_setopt(easy, CURLOPT_TIMEOUT, 15L); + curl_easy_setopt(easy, CURLOPT_PRIVATE, req); + curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L); + + /* Add to multi handle */ + if (curl_multi == NULL) { + curl_multi = curl_multi_init(); + } + curl_multi_add_handle(curl_multi, easy); + + /* Add to queue */ + req->next = exa_queue; + exa_queue = req; + req->user_data = response; + + LOG_DEBUG("Exa search sent: %s", query); + + return req; +} + +void exa_process_pending(void) +{ + if (curl_multi == NULL || exa_queue == NULL) { + return; + } + + int running_handles; + curl_multi_perform(curl_multi, &running_handles); + + CURLMsg *msg; + int msgs_left; + + while ((msg = curl_multi_info_read(curl_multi, &msgs_left)) != NULL) { + if (msg->msg == CURLMSG_DONE) { + CURL *easy = msg->easy_handle; + ExaRequest *req = NULL; + + curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req); + + /* Check if this is an Exa request (in exa_queue) */ + bool is_exa = false; + for (ExaRequest *r = exa_queue; r != NULL; r = r->next) { + if (r == req) { + is_exa = true; + break; + } + } + + if (is_exa && req != NULL) { + ResponseBuffer *buf = (ResponseBuffer *)req->user_data; + + if (msg->data.result == CURLE_OK && buf != NULL && buf->data != NULL) { + exa_parse_response(req, buf->data); + req->state = AI_STATE_COMPLETED; + } else { + req->state = AI_STATE_ERROR; + LOG_ERROR("Exa request failed: %s", curl_easy_strerror(msg->data.result)); + } + + if (req->callback != NULL) { + req->callback(req); + } + + /* Cleanup buffer */ + if (buf != NULL) { + if (buf->data) free(buf->data); + dwn_free(buf); + } + + /* Remove from queue */ + ExaRequest **pp = &exa_queue; + while (*pp != NULL) { + if (*pp == req) { + *pp = req->next; + break; + } + pp = &(*pp)->next; + } + } + + curl_multi_remove_handle(curl_multi, easy); + curl_easy_cleanup(easy); + } + } +} + +/* Callback for app launcher search */ +static void exa_launcher_callback(ExaRequest *req) +{ + if (req == NULL || req->state != AI_STATE_COMPLETED) { + notification_show("Exa Search", "Error", "Search failed", NULL, 3000); + return; + } + + if (req->result_count == 0) { + notification_show("Exa Search", "No Results", req->query, NULL, 3000); + return; + } + + /* Show results via dmenu/rofi - use bounded string operations */ + size_t choices_size = req->result_count * 300; + char *choices = dwn_malloc(choices_size); + size_t offset = 0; + choices[0] = '\0'; + + for (int i = 0; i < req->result_count; i++) { + int written = snprintf(choices + offset, choices_size - offset, + "%s%s", offset > 0 ? "\n" : "", req->results[i].title); + if (written > 0 && (size_t)written < choices_size - offset) { + offset += written; + } + } + + /* Show in dmenu - escape choices to prevent command injection */ + char *escaped_choices = shell_escape(choices); + char *cmd = dwn_malloc(strlen(escaped_choices) + 64); + snprintf(cmd, strlen(escaped_choices) + 64, "echo %s | dmenu -l 10 -p 'Results:'", escaped_choices); + + char *selected = spawn_capture(cmd); + dwn_free(cmd); + dwn_free(escaped_choices); + + if (selected != NULL && selected[0] != '\0') { + /* Find which result was selected and open URL */ + for (int i = 0; i < req->result_count; i++) { + if (strncmp(selected, req->results[i].title, strlen(req->results[i].title)) == 0) { + /* Escape URL to prevent command injection */ + char *escaped_url = shell_escape(req->results[i].url); + char *open_cmd = dwn_malloc(strlen(escaped_url) + 32); + snprintf(open_cmd, strlen(escaped_url) + 32, "xdg-open %s &", escaped_url); + spawn_async(open_cmd); + dwn_free(open_cmd); + dwn_free(escaped_url); + break; + } + } + dwn_free(selected); + } + + dwn_free(choices); + if (req->query) dwn_free(req->query); + dwn_free(req); +} + +void exa_show_app_launcher(void) +{ + if (!exa_is_available()) { + notification_show("Exa", "Unavailable", + "Set EXA_API_KEY in config to enable semantic search", + NULL, 3000); + return; + } + + /* Get search query from user */ + char *query = NULL; + + if (spawn("command -v dmenu >/dev/null 2>&1") == 0) { + query = spawn_capture("echo '' | dmenu -p 'Exa Search:'"); + } else if (spawn("command -v rofi >/dev/null 2>&1") == 0) { + query = spawn_capture("rofi -dmenu -p 'Exa Search:'"); + } else { + notification_show("Exa", "Missing Dependency", + "Install dmenu or rofi", NULL, 3000); + return; + } + + if (query == NULL || query[0] == '\0') { + if (query != NULL) dwn_free(query); + return; + } + + /* Remove trailing newline */ + query[strcspn(query, "\n")] = '\0'; + + notification_show("Exa", "Searching...", query, NULL, 2000); + exa_search(query, exa_launcher_callback); +} diff --git a/src/applauncher.c b/src/applauncher.c new file mode 100644 index 0000000..6ad02f3 --- /dev/null +++ b/src/applauncher.c @@ -0,0 +1,507 @@ +/* + * DWN - Desktop Window Manager + * Application launcher with .desktop file support + */ + +#include "applauncher.h" +#include "config.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Global state */ +static AppLauncherState launcher_state = {0}; + +/* Path to recent apps file */ +static char recent_file_path[512] = {0}; + +/* Forward declarations */ +static void scan_desktop_dir(const char *dir); +static int parse_desktop_file(const char *path, AppEntry *entry); +static void load_recent_apps(void); +static void save_recent_apps(void); +static void add_to_recent(const char *desktop_id); +static char *strip_field_codes(const char *exec); +static int compare_apps(const void *a, const void *b); + +/* ========== Initialization ========== */ + +void applauncher_init(void) +{ + memset(&launcher_state, 0, sizeof(launcher_state)); + + /* Set up recent file path */ + const char *home = getenv("HOME"); + if (home == NULL) { + struct passwd *pw = getpwuid(getuid()); + home = pw ? pw->pw_dir : "/tmp"; + } + snprintf(recent_file_path, sizeof(recent_file_path), + "%s/.local/share/dwn/recent_apps", home); + + /* Ensure directory exists */ + char dir_path[512]; + snprintf(dir_path, sizeof(dir_path), "%s/.local/share/dwn", home); + mkdir(dir_path, 0755); + + /* Load recent apps first */ + load_recent_apps(); + + /* Scan application directories */ + applauncher_refresh(); + + LOG_INFO("App launcher initialized with %d applications", launcher_state.app_count); +} + +void applauncher_cleanup(void) +{ + save_recent_apps(); +} + +void applauncher_refresh(void) +{ + launcher_state.app_count = 0; + + /* Scan system applications */ + scan_desktop_dir("/usr/share/applications"); + + /* Scan user applications (takes precedence) */ + const char *home = getenv("HOME"); + if (home) { + char user_apps[512]; + snprintf(user_apps, sizeof(user_apps), "%s/.local/share/applications", home); + scan_desktop_dir(user_apps); + } + + /* Sort apps alphabetically by name */ + qsort(launcher_state.apps, launcher_state.app_count, + sizeof(AppEntry), compare_apps); + + LOG_DEBUG("Refreshed app list: %d applications found", launcher_state.app_count); +} + +/* ========== Directory Scanning ========== */ + +static void scan_desktop_dir(const char *dir) +{ + DIR *d = opendir(dir); + if (d == NULL) { + return; + } + + struct dirent *entry; + while ((entry = readdir(d)) != NULL) { + if (launcher_state.app_count >= MAX_APPS) { + break; + } + + /* Only process .desktop files */ + size_t len = strlen(entry->d_name); + if (len < 9 || strcmp(entry->d_name + len - 8, ".desktop") != 0) { + continue; + } + + /* Build full path */ + char path[1024]; + snprintf(path, sizeof(path), "%s/%s", dir, entry->d_name); + + /* Check if already exists (user apps override system) */ + bool exists = false; + for (int i = 0; i < launcher_state.app_count; i++) { + if (strcmp(launcher_state.apps[i].desktop_id, entry->d_name) == 0) { + exists = true; + break; + } + } + if (exists) { + continue; + } + + /* Parse the desktop file */ + AppEntry app = {0}; + if (parse_desktop_file(path, &app) == 0) { + snprintf(app.desktop_id, sizeof(app.desktop_id), "%s", entry->d_name); + launcher_state.apps[launcher_state.app_count++] = app; + } + } + + closedir(d); +} + +/* ========== Desktop File Parsing ========== */ + +static int parse_desktop_file(const char *path, AppEntry *entry) +{ + FILE *f = fopen(path, "r"); + if (f == NULL) { + return -1; + } + + char line[1024]; + bool in_desktop_entry = false; + bool has_name = false; + bool has_exec = false; + + entry->hidden = false; + entry->terminal = false; + + while (fgets(line, sizeof(line), f) != NULL) { + /* Remove trailing whitespace/newline */ + size_t len = strlen(line); + while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r' || + line[len-1] == ' ' || line[len-1] == '\t')) { + line[--len] = '\0'; + } + + /* Check for section header */ + if (line[0] == '[') { + in_desktop_entry = (strcmp(line, "[Desktop Entry]") == 0); + continue; + } + + if (!in_desktop_entry) { + continue; + } + + /* Parse key=value */ + char *eq = strchr(line, '='); + if (eq == NULL) { + continue; + } + + *eq = '\0'; + char *key = line; + char *value = eq + 1; + + /* Skip localized entries (Name[en]=...) */ + if (strchr(key, '[') != NULL) { + continue; + } + + if (strcmp(key, "Name") == 0 && !has_name) { + strncpy(entry->name, value, sizeof(entry->name) - 1); + has_name = true; + } else if (strcmp(key, "Exec") == 0 && !has_exec) { + strncpy(entry->exec, value, sizeof(entry->exec) - 1); + has_exec = true; + } else if (strcmp(key, "Icon") == 0) { + strncpy(entry->icon, value, sizeof(entry->icon) - 1); + } else if (strcmp(key, "Terminal") == 0) { + entry->terminal = (strcasecmp(value, "true") == 0); + } else if (strcmp(key, "Type") == 0) { + /* Skip non-Application types */ + if (strcmp(value, "Application") != 0) { + fclose(f); + return -1; + } + } else if (strcmp(key, "Hidden") == 0 || strcmp(key, "NoDisplay") == 0) { + if (strcasecmp(value, "true") == 0) { + entry->hidden = true; + } + } + } + + fclose(f); + + /* Must have name and exec, and not be hidden */ + if (!has_name || !has_exec || entry->hidden) { + return -1; + } + + return 0; +} + +/* ========== Recent Apps Management ========== */ + +static void load_recent_apps(void) +{ + launcher_state.recent_count = 0; + + FILE *f = fopen(recent_file_path, "r"); + if (f == NULL) { + return; + } + + char line[256]; + while (fgets(line, sizeof(line), f) != NULL && + launcher_state.recent_count < MAX_RECENT_APPS) { + /* Remove newline */ + size_t len = strlen(line); + if (len > 0 && line[len-1] == '\n') { + line[len-1] = '\0'; + } + if (line[0] != '\0') { + snprintf(launcher_state.recent[launcher_state.recent_count], + sizeof(launcher_state.recent[0]), "%s", line); + launcher_state.recent_count++; + } + } + + fclose(f); + LOG_DEBUG("Loaded %d recent apps", launcher_state.recent_count); +} + +static void save_recent_apps(void) +{ + FILE *f = fopen(recent_file_path, "w"); + if (f == NULL) { + LOG_WARN("Could not save recent apps to %s", recent_file_path); + return; + } + + for (int i = 0; i < launcher_state.recent_count; i++) { + fprintf(f, "%s\n", launcher_state.recent[i]); + } + + fclose(f); +} + +static void add_to_recent(const char *desktop_id) +{ + /* Remove if already in list */ + for (int i = 0; i < launcher_state.recent_count; i++) { + if (strcmp(launcher_state.recent[i], desktop_id) == 0) { + /* Shift everything down */ + for (int j = i; j > 0; j--) { + strcpy(launcher_state.recent[j], launcher_state.recent[j-1]); + } + strcpy(launcher_state.recent[0], desktop_id); + save_recent_apps(); + return; + } + } + + /* Add to front, shift others */ + if (launcher_state.recent_count < MAX_RECENT_APPS) { + launcher_state.recent_count++; + } + + for (int i = launcher_state.recent_count - 1; i > 0; i--) { + strcpy(launcher_state.recent[i], launcher_state.recent[i-1]); + } + strncpy(launcher_state.recent[0], desktop_id, sizeof(launcher_state.recent[0]) - 1); + + save_recent_apps(); +} + +/* ========== Helper Functions ========== */ + +static char *strip_field_codes(const char *exec) +{ + static char result[512]; + size_t j = 0; + + for (size_t i = 0; exec[i] && j < sizeof(result) - 1; i++) { + if (exec[i] == '%' && exec[i+1]) { + /* Skip field codes: %f, %F, %u, %U, %d, %D, %n, %N, %i, %c, %k */ + char code = exec[i+1]; + if (code == 'f' || code == 'F' || code == 'u' || code == 'U' || + code == 'd' || code == 'D' || code == 'n' || code == 'N' || + code == 'i' || code == 'c' || code == 'k') { + i++; /* Skip the code character */ + /* Also skip trailing space if any */ + if (exec[i+1] == ' ') { + i++; + } + continue; + } + } + result[j++] = exec[i]; + } + result[j] = '\0'; + + /* Trim trailing whitespace */ + while (j > 0 && (result[j-1] == ' ' || result[j-1] == '\t')) { + result[--j] = '\0'; + } + + return result; +} + +static int compare_apps(const void *a, const void *b) +{ + const AppEntry *app_a = (const AppEntry *)a; + const AppEntry *app_b = (const AppEntry *)b; + return strcasecmp(app_a->name, app_b->name); +} + +/* Find app by desktop_id */ +static AppEntry *find_app_by_id(const char *desktop_id) +{ + for (int i = 0; i < launcher_state.app_count; i++) { + if (strcmp(launcher_state.apps[i].desktop_id, desktop_id) == 0) { + return &launcher_state.apps[i]; + } + } + return NULL; +} + +/* Find app by name */ +static AppEntry *find_app_by_name(const char *name) +{ + for (int i = 0; i < launcher_state.app_count; i++) { + if (strcmp(launcher_state.apps[i].name, name) == 0) { + return &launcher_state.apps[i]; + } + } + return NULL; +} + +/* ========== Launcher Display ========== */ + +void applauncher_show(void) +{ + if (launcher_state.app_count == 0) { + LOG_WARN("No applications found"); + return; + } + + /* Build the list for dmenu: + * - Recent apps first (if any) + * - Then all apps alphabetically + */ + size_t buf_size = launcher_state.app_count * 140; + char *choices = malloc(buf_size); + if (choices == NULL) { + LOG_ERROR("Failed to allocate memory for app list"); + return; + } + choices[0] = '\0'; + + /* Track which apps we've added (to avoid duplicates) */ + bool *added = calloc(launcher_state.app_count, sizeof(bool)); + if (added == NULL) { + free(choices); + return; + } + + size_t pos = 0; + + /* Add recent apps first */ + for (int i = 0; i < launcher_state.recent_count; i++) { + AppEntry *app = find_app_by_id(launcher_state.recent[i]); + if (app != NULL) { + /* Mark as added */ + for (int j = 0; j < launcher_state.app_count; j++) { + if (&launcher_state.apps[j] == app) { + added[j] = true; + break; + } + } + size_t name_len = strlen(app->name); + if (pos + name_len + 2 < buf_size) { + memcpy(choices + pos, app->name, name_len); + pos += name_len; + choices[pos++] = '\n'; + } + } + } + + /* Add remaining apps */ + for (int i = 0; i < launcher_state.app_count; i++) { + if (added[i]) { + continue; + } + size_t name_len = strlen(launcher_state.apps[i].name); + if (pos + name_len + 2 < buf_size) { + memcpy(choices + pos, launcher_state.apps[i].name, name_len); + pos += name_len; + choices[pos++] = '\n'; + } + } + + if (pos > 0) { + choices[pos-1] = '\0'; /* Remove trailing newline */ + } + + free(added); + + /* Write choices to a temp file to avoid shell escaping issues */ + char tmp_path[] = "/tmp/dwn_apps_XXXXXX"; + int tmp_fd = mkstemp(tmp_path); + if (tmp_fd < 0) { + free(choices); + LOG_ERROR("Failed to create temp file for app list"); + return; + } + + ssize_t written = write(tmp_fd, choices, strlen(choices)); + close(tmp_fd); + free(choices); + + if (written < 0) { + unlink(tmp_path); + LOG_ERROR("Failed to write app list to temp file"); + return; + } + + /* Run dmenu */ + char cmd[1024]; + snprintf(cmd, sizeof(cmd), + "cat '%s' | dmenu -i -l 10 -p 'Run:'; rm -f '%s'", + tmp_path, tmp_path); + + FILE *p = popen(cmd, "r"); + + if (p == NULL) { + LOG_ERROR("Failed to run dmenu"); + return; + } + + char selected[256] = {0}; + if (fgets(selected, sizeof(selected), p) != NULL) { + /* Remove trailing newline */ + size_t len = strlen(selected); + if (len > 0 && selected[len-1] == '\n') { + selected[len-1] = '\0'; + } + } + pclose(p); + + if (selected[0] == '\0') { + return; /* User cancelled */ + } + + /* Find and launch the selected app */ + AppEntry *app = find_app_by_name(selected); + if (app != NULL) { + applauncher_launch(app->desktop_id); + } else { + /* User typed a custom command */ + spawn_async(selected); + } +} + +void applauncher_launch(const char *desktop_id) +{ + AppEntry *app = find_app_by_id(desktop_id); + if (app == NULL) { + LOG_WARN("App not found: %s", desktop_id); + return; + } + + /* Strip field codes from exec */ + char *exec_cmd = strip_field_codes(app->exec); + + /* Build command */ + char cmd[1024]; + if (app->terminal) { + const char *terminal = config_get_terminal(); + snprintf(cmd, sizeof(cmd), "%s -e %s", terminal, exec_cmd); + } else { + snprintf(cmd, sizeof(cmd), "%s", exec_cmd); + } + + LOG_INFO("Launching: %s (%s)", app->name, cmd); + spawn_async(cmd); + + /* Add to recent apps */ + add_to_recent(desktop_id); +} diff --git a/src/atoms.c b/src/atoms.c new file mode 100644 index 0000000..e024882 --- /dev/null +++ b/src/atoms.c @@ -0,0 +1,463 @@ +/* + * DWN - Desktop Window Manager + * X11 Atoms management implementation + */ + +#include "atoms.h" +#include "dwn.h" +#include "util.h" + +#include +#include +#include + +/* Global atom containers */ +EWMHAtoms ewmh; +ICCCMAtoms icccm; +MiscAtoms misc_atoms; + +/* Helper macro for atom initialization */ +#define ATOM(name) XInternAtom(display, name, False) + +void atoms_init(Display *display) +{ + LOG_DEBUG("Initializing X11 atoms"); + + /* EWMH root window properties */ + ewmh.NET_SUPPORTED = ATOM("_NET_SUPPORTED"); + ewmh.NET_SUPPORTING_WM_CHECK = ATOM("_NET_SUPPORTING_WM_CHECK"); + ewmh.NET_CLIENT_LIST = ATOM("_NET_CLIENT_LIST"); + ewmh.NET_CLIENT_LIST_STACKING = ATOM("_NET_CLIENT_LIST_STACKING"); + ewmh.NET_NUMBER_OF_DESKTOPS = ATOM("_NET_NUMBER_OF_DESKTOPS"); + ewmh.NET_DESKTOP_GEOMETRY = ATOM("_NET_DESKTOP_GEOMETRY"); + ewmh.NET_DESKTOP_VIEWPORT = ATOM("_NET_DESKTOP_VIEWPORT"); + ewmh.NET_CURRENT_DESKTOP = ATOM("_NET_CURRENT_DESKTOP"); + ewmh.NET_DESKTOP_NAMES = ATOM("_NET_DESKTOP_NAMES"); + ewmh.NET_ACTIVE_WINDOW = ATOM("_NET_ACTIVE_WINDOW"); + ewmh.NET_WORKAREA = ATOM("_NET_WORKAREA"); + + /* EWMH client window properties */ + ewmh.NET_WM_NAME = ATOM("_NET_WM_NAME"); + ewmh.NET_WM_VISIBLE_NAME = ATOM("_NET_WM_VISIBLE_NAME"); + ewmh.NET_WM_DESKTOP = ATOM("_NET_WM_DESKTOP"); + ewmh.NET_WM_WINDOW_TYPE = ATOM("_NET_WM_WINDOW_TYPE"); + ewmh.NET_WM_STATE = ATOM("_NET_WM_STATE"); + ewmh.NET_WM_ALLOWED_ACTIONS = ATOM("_NET_WM_ALLOWED_ACTIONS"); + ewmh.NET_WM_STRUT = ATOM("_NET_WM_STRUT"); + ewmh.NET_WM_STRUT_PARTIAL = ATOM("_NET_WM_STRUT_PARTIAL"); + ewmh.NET_WM_PID = ATOM("_NET_WM_PID"); + + /* Window types */ + ewmh.NET_WM_WINDOW_TYPE_DESKTOP = ATOM("_NET_WM_WINDOW_TYPE_DESKTOP"); + ewmh.NET_WM_WINDOW_TYPE_DOCK = ATOM("_NET_WM_WINDOW_TYPE_DOCK"); + ewmh.NET_WM_WINDOW_TYPE_TOOLBAR = ATOM("_NET_WM_WINDOW_TYPE_TOOLBAR"); + ewmh.NET_WM_WINDOW_TYPE_MENU = ATOM("_NET_WM_WINDOW_TYPE_MENU"); + ewmh.NET_WM_WINDOW_TYPE_UTILITY = ATOM("_NET_WM_WINDOW_TYPE_UTILITY"); + ewmh.NET_WM_WINDOW_TYPE_SPLASH = ATOM("_NET_WM_WINDOW_TYPE_SPLASH"); + ewmh.NET_WM_WINDOW_TYPE_DIALOG = ATOM("_NET_WM_WINDOW_TYPE_DIALOG"); + ewmh.NET_WM_WINDOW_TYPE_NORMAL = ATOM("_NET_WM_WINDOW_TYPE_NORMAL"); + ewmh.NET_WM_WINDOW_TYPE_NOTIFICATION = ATOM("_NET_WM_WINDOW_TYPE_NOTIFICATION"); + + /* Window states */ + ewmh.NET_WM_STATE_MODAL = ATOM("_NET_WM_STATE_MODAL"); + ewmh.NET_WM_STATE_STICKY = ATOM("_NET_WM_STATE_STICKY"); + ewmh.NET_WM_STATE_MAXIMIZED_VERT = ATOM("_NET_WM_STATE_MAXIMIZED_VERT"); + ewmh.NET_WM_STATE_MAXIMIZED_HORZ = ATOM("_NET_WM_STATE_MAXIMIZED_HORZ"); + ewmh.NET_WM_STATE_SHADED = ATOM("_NET_WM_STATE_SHADED"); + ewmh.NET_WM_STATE_SKIP_TASKBAR = ATOM("_NET_WM_STATE_SKIP_TASKBAR"); + ewmh.NET_WM_STATE_SKIP_PAGER = ATOM("_NET_WM_STATE_SKIP_PAGER"); + ewmh.NET_WM_STATE_HIDDEN = ATOM("_NET_WM_STATE_HIDDEN"); + ewmh.NET_WM_STATE_FULLSCREEN = ATOM("_NET_WM_STATE_FULLSCREEN"); + ewmh.NET_WM_STATE_ABOVE = ATOM("_NET_WM_STATE_ABOVE"); + ewmh.NET_WM_STATE_BELOW = ATOM("_NET_WM_STATE_BELOW"); + ewmh.NET_WM_STATE_DEMANDS_ATTENTION = ATOM("_NET_WM_STATE_DEMANDS_ATTENTION"); + ewmh.NET_WM_STATE_FOCUSED = ATOM("_NET_WM_STATE_FOCUSED"); + + /* Actions */ + ewmh.NET_WM_ACTION_MOVE = ATOM("_NET_WM_ACTION_MOVE"); + ewmh.NET_WM_ACTION_RESIZE = ATOM("_NET_WM_ACTION_RESIZE"); + ewmh.NET_WM_ACTION_MINIMIZE = ATOM("_NET_WM_ACTION_MINIMIZE"); + ewmh.NET_WM_ACTION_SHADE = ATOM("_NET_WM_ACTION_SHADE"); + ewmh.NET_WM_ACTION_STICK = ATOM("_NET_WM_ACTION_STICK"); + ewmh.NET_WM_ACTION_MAXIMIZE_HORZ = ATOM("_NET_WM_ACTION_MAXIMIZE_HORZ"); + ewmh.NET_WM_ACTION_MAXIMIZE_VERT = ATOM("_NET_WM_ACTION_MAXIMIZE_VERT"); + ewmh.NET_WM_ACTION_FULLSCREEN = ATOM("_NET_WM_ACTION_FULLSCREEN"); + ewmh.NET_WM_ACTION_CHANGE_DESKTOP = ATOM("_NET_WM_ACTION_CHANGE_DESKTOP"); + ewmh.NET_WM_ACTION_CLOSE = ATOM("_NET_WM_ACTION_CLOSE"); + + /* Client messages */ + ewmh.NET_CLOSE_WINDOW = ATOM("_NET_CLOSE_WINDOW"); + ewmh.NET_MOVERESIZE_WINDOW = ATOM("_NET_MOVERESIZE_WINDOW"); + ewmh.NET_WM_MOVERESIZE = ATOM("_NET_WM_MOVERESIZE"); + ewmh.NET_REQUEST_FRAME_EXTENTS = ATOM("_NET_REQUEST_FRAME_EXTENTS"); + ewmh.NET_FRAME_EXTENTS = ATOM("_NET_FRAME_EXTENTS"); + + /* System tray */ + ewmh.NET_SYSTEM_TRAY_OPCODE = ATOM("_NET_SYSTEM_TRAY_OPCODE"); + ewmh.NET_SYSTEM_TRAY_S0 = ATOM("_NET_SYSTEM_TRAY_S0"); + ewmh.MANAGER = ATOM("MANAGER"); + ewmh.XEMBED = ATOM("_XEMBED"); + ewmh.XEMBED_INFO = ATOM("_XEMBED_INFO"); + + /* ICCCM atoms */ + icccm.WM_PROTOCOLS = ATOM("WM_PROTOCOLS"); + icccm.WM_DELETE_WINDOW = ATOM("WM_DELETE_WINDOW"); + icccm.WM_TAKE_FOCUS = ATOM("WM_TAKE_FOCUS"); + icccm.WM_STATE = ATOM("WM_STATE"); + icccm.WM_CHANGE_STATE = ATOM("WM_CHANGE_STATE"); + icccm.WM_CLASS = ATOM("WM_CLASS"); + icccm.WM_NAME = ATOM("WM_NAME"); + icccm.WM_TRANSIENT_FOR = ATOM("WM_TRANSIENT_FOR"); + icccm.WM_CLIENT_LEADER = ATOM("WM_CLIENT_LEADER"); + icccm.WM_WINDOW_ROLE = ATOM("WM_WINDOW_ROLE"); + + /* Misc atoms */ + misc_atoms.UTF8_STRING = ATOM("UTF8_STRING"); + misc_atoms.COMPOUND_TEXT = ATOM("COMPOUND_TEXT"); + misc_atoms.MOTIF_WM_HINTS = ATOM("_MOTIF_WM_HINTS"); + misc_atoms.CLIPBOARD = ATOM("CLIPBOARD"); + misc_atoms.PRIMARY = ATOM("PRIMARY"); + misc_atoms.DWN_RESTART = ATOM("_DWN_RESTART"); + + LOG_DEBUG("X11 atoms initialized"); +} + +/* ========== EWMH Setup ========== */ + +void atoms_setup_ewmh(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + Display *dpy = dwn->display; + Window root = dwn->root; + + /* Create a check window for _NET_SUPPORTING_WM_CHECK */ + Window check = XCreateSimpleWindow(dpy, root, 0, 0, 1, 1, 0, 0, 0); + + /* Set _NET_SUPPORTING_WM_CHECK on root and check window */ + XChangeProperty(dpy, root, ewmh.NET_SUPPORTING_WM_CHECK, + XA_WINDOW, 32, PropModeReplace, + (unsigned char *)&check, 1); + XChangeProperty(dpy, check, ewmh.NET_SUPPORTING_WM_CHECK, + XA_WINDOW, 32, PropModeReplace, + (unsigned char *)&check, 1); + + /* Set _NET_WM_NAME on check window */ + XChangeProperty(dpy, check, ewmh.NET_WM_NAME, + misc_atoms.UTF8_STRING, 8, PropModeReplace, + (unsigned char *)DWN_NAME, strlen(DWN_NAME)); + + /* Set supported atoms */ + Atom supported[] = { + ewmh.NET_SUPPORTED, + ewmh.NET_SUPPORTING_WM_CHECK, + ewmh.NET_CLIENT_LIST, + ewmh.NET_CLIENT_LIST_STACKING, + ewmh.NET_NUMBER_OF_DESKTOPS, + ewmh.NET_CURRENT_DESKTOP, + ewmh.NET_DESKTOP_NAMES, + ewmh.NET_ACTIVE_WINDOW, + ewmh.NET_WM_NAME, + ewmh.NET_WM_DESKTOP, + ewmh.NET_WM_WINDOW_TYPE, + ewmh.NET_WM_STATE, + ewmh.NET_WM_STATE_FULLSCREEN, + ewmh.NET_WM_STATE_HIDDEN, + ewmh.NET_WM_STATE_DEMANDS_ATTENTION, + ewmh.NET_CLOSE_WINDOW, + ewmh.NET_WM_STRUT_PARTIAL, + ewmh.NET_FRAME_EXTENTS + }; + + XChangeProperty(dpy, root, ewmh.NET_SUPPORTED, + XA_ATOM, 32, PropModeReplace, + (unsigned char *)supported, sizeof(supported) / sizeof(Atom)); + + /* Set number of desktops */ + atoms_set_number_of_desktops(MAX_WORKSPACES); + + /* Set current desktop */ + atoms_set_current_desktop(0); + + /* Update desktop names */ + atoms_update_desktop_names(); + + LOG_INFO("EWMH compliance initialized"); +} + +void atoms_update_client_list(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + Window windows[MAX_CLIENTS]; + int count = 0; + + for (Client *c = dwn->client_list; c != NULL && count < MAX_CLIENTS; c = c->next) { + windows[count++] = c->window; + } + + XChangeProperty(dwn->display, dwn->root, ewmh.NET_CLIENT_LIST, + XA_WINDOW, 32, PropModeReplace, + (unsigned char *)windows, count); + + XChangeProperty(dwn->display, dwn->root, ewmh.NET_CLIENT_LIST_STACKING, + XA_WINDOW, 32, PropModeReplace, + (unsigned char *)windows, count); +} + +void atoms_update_desktop_names(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + /* Build null-separated list of workspace names */ + char names[MAX_WORKSPACES * 32]; + int offset = 0; + + for (int i = 0; i < MAX_WORKSPACES; i++) { + const char *name = dwn->workspaces[i].name; + if (name[0] == '\0') { + offset += snprintf(names + offset, sizeof(names) - offset, "%d", i + 1); + } else { + offset += snprintf(names + offset, sizeof(names) - offset, "%s", name); + } + names[offset++] = '\0'; + } + + XChangeProperty(dwn->display, dwn->root, ewmh.NET_DESKTOP_NAMES, + misc_atoms.UTF8_STRING, 8, PropModeReplace, + (unsigned char *)names, offset); +} + +void atoms_set_current_desktop(int desktop) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + long data = desktop; + XChangeProperty(dwn->display, dwn->root, ewmh.NET_CURRENT_DESKTOP, + XA_CARDINAL, 32, PropModeReplace, + (unsigned char *)&data, 1); +} + +void atoms_set_active_window(Window window) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + XChangeProperty(dwn->display, dwn->root, ewmh.NET_ACTIVE_WINDOW, + XA_WINDOW, 32, PropModeReplace, + (unsigned char *)&window, 1); +} + +void atoms_set_number_of_desktops(int count) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + long data = count; + XChangeProperty(dwn->display, dwn->root, ewmh.NET_NUMBER_OF_DESKTOPS, + XA_CARDINAL, 32, PropModeReplace, + (unsigned char *)&data, 1); +} + +/* ========== Window property helpers ========== */ + +bool atoms_get_window_type(Window window, Atom *type) +{ + if (dwn == NULL || dwn->display == NULL || type == NULL) { + return false; + } + + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + + if (XGetWindowProperty(dwn->display, window, ewmh.NET_WM_WINDOW_TYPE, + 0, 1, False, XA_ATOM, + &actual_type, &actual_format, &nitems, &bytes_after, + &data) == Success && data != NULL) { + *type = *(Atom *)data; + XFree(data); + return true; + } + + return false; +} + +bool atoms_get_window_desktop(Window window, int *desktop) +{ + if (dwn == NULL || dwn->display == NULL || desktop == NULL) { + return false; + } + + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + + if (XGetWindowProperty(dwn->display, window, ewmh.NET_WM_DESKTOP, + 0, 1, False, XA_CARDINAL, + &actual_type, &actual_format, &nitems, &bytes_after, + &data) == Success && data != NULL) { + *desktop = *(int *)data; + XFree(data); + return true; + } + + return false; +} + +bool atoms_set_window_desktop(Window window, int desktop) +{ + if (dwn == NULL || dwn->display == NULL) { + return false; + } + + long data = desktop; + XChangeProperty(dwn->display, window, ewmh.NET_WM_DESKTOP, + XA_CARDINAL, 32, PropModeReplace, + (unsigned char *)&data, 1); + return true; +} + +char *atoms_get_window_name(Window window) +{ + if (dwn == NULL || dwn->display == NULL) { + return NULL; + } + + Display *dpy = dwn->display; + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + + /* Try _NET_WM_NAME first (UTF-8) */ + if (XGetWindowProperty(dpy, window, ewmh.NET_WM_NAME, + 0, 256, False, misc_atoms.UTF8_STRING, + &actual_type, &actual_format, &nitems, &bytes_after, + &data) == Success && data != NULL) { + char *name = dwn_strdup((char *)data); + XFree(data); + return name; + } + + /* Fall back to WM_NAME */ + if (XGetWindowProperty(dpy, window, icccm.WM_NAME, + 0, 256, False, XA_STRING, + &actual_type, &actual_format, &nitems, &bytes_after, + &data) == Success && data != NULL) { + char *name = dwn_strdup((char *)data); + XFree(data); + return name; + } + + /* Last resort: XFetchName */ + char *name = NULL; + if (XFetchName(dpy, window, &name) && name != NULL) { + char *result = dwn_strdup(name); + XFree(name); + return result; + } + + return dwn_strdup("Untitled"); +} + +bool atoms_get_wm_class(Window window, char *class_name, char *instance_name, size_t len) +{ + if (dwn == NULL || dwn->display == NULL) { + return false; + } + + XClassHint hint; + if (XGetClassHint(dwn->display, window, &hint)) { + if (class_name != NULL && hint.res_class != NULL) { + strncpy(class_name, hint.res_class, len - 1); + class_name[len - 1] = '\0'; + } + if (instance_name != NULL && hint.res_name != NULL) { + strncpy(instance_name, hint.res_name, len - 1); + instance_name[len - 1] = '\0'; + } + if (hint.res_class) XFree(hint.res_class); + if (hint.res_name) XFree(hint.res_name); + return true; + } + + return false; +} + +/* ========== Protocol helpers ========== */ + +bool atoms_window_supports_protocol(Window window, Atom protocol) +{ + if (dwn == NULL || dwn->display == NULL) { + return false; + } + + Atom *protocols = NULL; + int count = 0; + + if (XGetWMProtocols(dwn->display, window, &protocols, &count)) { + for (int i = 0; i < count; i++) { + if (protocols[i] == protocol) { + XFree(protocols); + return true; + } + } + XFree(protocols); + } + + return false; +} + +void atoms_send_protocol(Window window, Atom protocol, Time timestamp) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + XEvent ev; + memset(&ev, 0, sizeof(ev)); + ev.type = ClientMessage; + ev.xclient.window = window; + ev.xclient.message_type = icccm.WM_PROTOCOLS; + ev.xclient.format = 32; + ev.xclient.data.l[0] = protocol; + ev.xclient.data.l[1] = timestamp; + + XSendEvent(dwn->display, window, False, NoEventMask, &ev); +} + +void atoms_send_client_message(Window window, Atom message_type, + long data0, long data1, long data2, long data3, long data4) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + XEvent ev; + memset(&ev, 0, sizeof(ev)); + ev.type = ClientMessage; + ev.xclient.window = window; + ev.xclient.message_type = message_type; + ev.xclient.format = 32; + ev.xclient.data.l[0] = data0; + ev.xclient.data.l[1] = data1; + ev.xclient.data.l[2] = data2; + ev.xclient.data.l[3] = data3; + ev.xclient.data.l[4] = data4; + + XSendEvent(dwn->display, dwn->root, False, + SubstructureNotifyMask | SubstructureRedirectMask, &ev); +} diff --git a/src/cJSON.c b/src/cJSON.c new file mode 100644 index 0000000..6e4fb0d --- /dev/null +++ b/src/cJSON.c @@ -0,0 +1,3191 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* cJSON */ +/* JSON parser in C. */ + +/* disable warnings about old C89 functions in MSVC */ +#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER) +#define _CRT_SECURE_NO_DEPRECATE +#endif + +#ifdef __GNUC__ +#pragma GCC visibility push(default) +#endif +#if defined(_MSC_VER) +#pragma warning (push) +/* disable warning about single line comments in system headers */ +#pragma warning (disable : 4001) +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_LOCALES +#include +#endif + +#if defined(_MSC_VER) +#pragma warning (pop) +#endif +#ifdef __GNUC__ +#pragma GCC visibility pop +#endif + +#include "cJSON.h" + +/* define our own boolean type */ +#ifdef true +#undef true +#endif +#define true ((cJSON_bool)1) + +#ifdef false +#undef false +#endif +#define false ((cJSON_bool)0) + +/* define isnan and isinf for ANSI C, if in C99 or above, isnan and isinf has been defined in math.h */ +#ifndef isinf +#define isinf(d) (isnan((d - d)) && !isnan(d)) +#endif +#ifndef isnan +#define isnan(d) (d != d) +#endif + +#ifndef NAN +#ifdef _WIN32 +#define NAN sqrt(-1.0) +#else +#define NAN 0.0/0.0 +#endif +#endif + +typedef struct { + const unsigned char *json; + size_t position; +} error; +static error global_error = { NULL, 0 }; + +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void) +{ + return (const char*) (global_error.json + global_error.position); +} + +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item) +{ + if (!cJSON_IsString(item)) + { + return NULL; + } + + return item->valuestring; +} + +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item) +{ + if (!cJSON_IsNumber(item)) + { + return (double) NAN; + } + + return item->valuedouble; +} + +/* This is a safeguard to prevent copy-pasters from using incompatible C and header files */ +#if (CJSON_VERSION_MAJOR != 1) || (CJSON_VERSION_MINOR != 7) || (CJSON_VERSION_PATCH != 19) + #error cJSON.h and cJSON.c have different versions. Make sure that both have the same. +#endif + +CJSON_PUBLIC(const char*) cJSON_Version(void) +{ + static char version[15]; + sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); + + return version; +} + +/* Case insensitive string comparison, doesn't consider two NULL pointers equal though */ +static int case_insensitive_strcmp(const unsigned char *string1, const unsigned char *string2) +{ + if ((string1 == NULL) || (string2 == NULL)) + { + return 1; + } + + if (string1 == string2) + { + return 0; + } + + for(; tolower(*string1) == tolower(*string2); (void)string1++, string2++) + { + if (*string1 == '\0') + { + return 0; + } + } + + return tolower(*string1) - tolower(*string2); +} + +typedef struct internal_hooks +{ + void *(CJSON_CDECL *allocate)(size_t size); + void (CJSON_CDECL *deallocate)(void *pointer); + void *(CJSON_CDECL *reallocate)(void *pointer, size_t size); +} internal_hooks; + +#if defined(_MSC_VER) +/* work around MSVC error C2322: '...' address of dllimport '...' is not static */ +static void * CJSON_CDECL internal_malloc(size_t size) +{ + return malloc(size); +} +static void CJSON_CDECL internal_free(void *pointer) +{ + free(pointer); +} +static void * CJSON_CDECL internal_realloc(void *pointer, size_t size) +{ + return realloc(pointer, size); +} +#else +#define internal_malloc malloc +#define internal_free free +#define internal_realloc realloc +#endif + +/* strlen of character literals resolved at compile time */ +#define static_strlen(string_literal) (sizeof(string_literal) - sizeof("")) + +static internal_hooks global_hooks = { internal_malloc, internal_free, internal_realloc }; + +static unsigned char* cJSON_strdup(const unsigned char* string, const internal_hooks * const hooks) +{ + size_t length = 0; + unsigned char *copy = NULL; + + if (string == NULL) + { + return NULL; + } + + length = strlen((const char*)string) + sizeof(""); + copy = (unsigned char*)hooks->allocate(length); + if (copy == NULL) + { + return NULL; + } + memcpy(copy, string, length); + + return copy; +} + +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks) +{ + if (hooks == NULL) + { + /* Reset hooks */ + global_hooks.allocate = malloc; + global_hooks.deallocate = free; + global_hooks.reallocate = realloc; + return; + } + + global_hooks.allocate = malloc; + if (hooks->malloc_fn != NULL) + { + global_hooks.allocate = hooks->malloc_fn; + } + + global_hooks.deallocate = free; + if (hooks->free_fn != NULL) + { + global_hooks.deallocate = hooks->free_fn; + } + + /* use realloc only if both free and malloc are used */ + global_hooks.reallocate = NULL; + if ((global_hooks.allocate == malloc) && (global_hooks.deallocate == free)) + { + global_hooks.reallocate = realloc; + } +} + +/* Internal constructor. */ +static cJSON *cJSON_New_Item(const internal_hooks * const hooks) +{ + cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON)); + if (node) + { + memset(node, '\0', sizeof(cJSON)); + } + + return node; +} + +/* Delete a cJSON structure. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item) +{ + cJSON *next = NULL; + while (item != NULL) + { + next = item->next; + if (!(item->type & cJSON_IsReference) && (item->child != NULL)) + { + cJSON_Delete(item->child); + } + if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL)) + { + global_hooks.deallocate(item->valuestring); + item->valuestring = NULL; + } + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + global_hooks.deallocate(item->string); + item->string = NULL; + } + global_hooks.deallocate(item); + item = next; + } +} + +/* get the decimal point character of the current locale */ +static unsigned char get_decimal_point(void) +{ +#ifdef ENABLE_LOCALES + struct lconv *lconv = localeconv(); + return (unsigned char) lconv->decimal_point[0]; +#else + return '.'; +#endif +} + +typedef struct +{ + const unsigned char *content; + size_t length; + size_t offset; + size_t depth; /* How deeply nested (in arrays/objects) is the input at the current offset. */ + internal_hooks hooks; +} parse_buffer; + +/* check if the given size is left to read in a given parse buffer (starting with 1) */ +#define can_read(buffer, size) ((buffer != NULL) && (((buffer)->offset + size) <= (buffer)->length)) +/* check if the buffer can be accessed at the given index (starting with 0) */ +#define can_access_at_index(buffer, index) ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length)) +#define cannot_access_at_index(buffer, index) (!can_access_at_index(buffer, index)) +/* get a pointer to the buffer at the position */ +#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset) + +/* Parse the input text to generate a number, and populate the result into item. */ +static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer) +{ + double number = 0; + unsigned char *after_end = NULL; + unsigned char *number_c_string; + unsigned char decimal_point = get_decimal_point(); + size_t i = 0; + size_t number_string_length = 0; + cJSON_bool has_decimal_point = false; + + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; + } + + /* copy the number into a temporary buffer and replace '.' with the decimal point + * of the current locale (for strtod) + * This also takes care of '\0' not necessarily being available for marking the end of the input */ + for (i = 0; can_access_at_index(input_buffer, i); i++) + { + switch (buffer_at_offset(input_buffer)[i]) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '+': + case '-': + case 'e': + case 'E': + number_string_length++; + break; + + case '.': + number_string_length++; + has_decimal_point = true; + break; + + default: + goto loop_end; + } + } +loop_end: + /* malloc for temporary buffer, add 1 for '\0' */ + number_c_string = (unsigned char *) input_buffer->hooks.allocate(number_string_length + 1); + if (number_c_string == NULL) + { + return false; /* allocation failure */ + } + + memcpy(number_c_string, buffer_at_offset(input_buffer), number_string_length); + number_c_string[number_string_length] = '\0'; + + if (has_decimal_point) + { + for (i = 0; i < number_string_length; i++) + { + if (number_c_string[i] == '.') + { + /* replace '.' with the decimal point of the current locale (for strtod) */ + number_c_string[i] = decimal_point; + } + } + } + + number = strtod((const char*)number_c_string, (char**)&after_end); + if (number_c_string == after_end) + { + /* free the temporary buffer */ + input_buffer->hooks.deallocate(number_c_string); + return false; /* parse_error */ + } + + item->valuedouble = number; + + /* use saturation in case of overflow */ + if (number >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)number; + } + + item->type = cJSON_Number; + + input_buffer->offset += (size_t)(after_end - number_c_string); + /* free the temporary buffer */ + input_buffer->hooks.deallocate(number_c_string); + return true; +} + +/* don't ask me, but the original cJSON_SetNumberValue returns an integer or double */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number) +{ + if (number >= INT_MAX) + { + object->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + object->valueint = INT_MIN; + } + else + { + object->valueint = (int)number; + } + + return object->valuedouble = number; +} + +/* Note: when passing a NULL valuestring, cJSON_SetValuestring treats this as an error and return NULL */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring) +{ + char *copy = NULL; + size_t v1_len; + size_t v2_len; + /* if object's type is not cJSON_String or is cJSON_IsReference, it should not set valuestring */ + if ((object == NULL) || !(object->type & cJSON_String) || (object->type & cJSON_IsReference)) + { + return NULL; + } + /* return NULL if the object is corrupted or valuestring is NULL */ + if (object->valuestring == NULL || valuestring == NULL) + { + return NULL; + } + + v1_len = strlen(valuestring); + v2_len = strlen(object->valuestring); + + if (v1_len <= v2_len) + { + /* strcpy does not handle overlapping string: [X1, X2] [Y1, Y2] => X2 < Y1 or Y2 < X1 */ + if (!( valuestring + v1_len < object->valuestring || object->valuestring + v2_len < valuestring )) + { + return NULL; + } + strcpy(object->valuestring, valuestring); + return object->valuestring; + } + copy = (char*) cJSON_strdup((const unsigned char*)valuestring, &global_hooks); + if (copy == NULL) + { + return NULL; + } + if (object->valuestring != NULL) + { + cJSON_free(object->valuestring); + } + object->valuestring = copy; + + return copy; +} + +typedef struct +{ + unsigned char *buffer; + size_t length; + size_t offset; + size_t depth; /* current nesting depth (for formatted printing) */ + cJSON_bool noalloc; + cJSON_bool format; /* is this print a formatted print */ + internal_hooks hooks; +} printbuffer; + +/* realloc printbuffer if necessary to have at least "needed" bytes more */ +static unsigned char* ensure(printbuffer * const p, size_t needed) +{ + unsigned char *newbuffer = NULL; + size_t newsize = 0; + + if ((p == NULL) || (p->buffer == NULL)) + { + return NULL; + } + + if ((p->length > 0) && (p->offset >= p->length)) + { + /* make sure that offset is valid */ + return NULL; + } + + if (needed > INT_MAX) + { + /* sizes bigger than INT_MAX are currently not supported */ + return NULL; + } + + needed += p->offset + 1; + if (needed <= p->length) + { + return p->buffer + p->offset; + } + + if (p->noalloc) { + return NULL; + } + + /* calculate new buffer size */ + if (needed > (INT_MAX / 2)) + { + /* overflow of int, use INT_MAX if possible */ + if (needed <= INT_MAX) + { + newsize = INT_MAX; + } + else + { + return NULL; + } + } + else + { + newsize = needed * 2; + } + + if (p->hooks.reallocate != NULL) + { + /* reallocate with realloc if available */ + newbuffer = (unsigned char*)p->hooks.reallocate(p->buffer, newsize); + if (newbuffer == NULL) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + } + else + { + /* otherwise reallocate manually */ + newbuffer = (unsigned char*)p->hooks.allocate(newsize); + if (!newbuffer) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + + memcpy(newbuffer, p->buffer, p->offset + 1); + p->hooks.deallocate(p->buffer); + } + p->length = newsize; + p->buffer = newbuffer; + + return newbuffer + p->offset; +} + +/* calculate the new length of the string in a printbuffer and update the offset */ +static void update_offset(printbuffer * const buffer) +{ + const unsigned char *buffer_pointer = NULL; + if ((buffer == NULL) || (buffer->buffer == NULL)) + { + return; + } + buffer_pointer = buffer->buffer + buffer->offset; + + buffer->offset += strlen((const char*)buffer_pointer); +} + +/* securely comparison of floating-point variables */ +static cJSON_bool compare_double(double a, double b) +{ + double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b); + return (fabs(a - b) <= maxVal * DBL_EPSILON); +} + +/* Render the number nicely from the given item into a string. */ +static cJSON_bool print_number(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + double d = item->valuedouble; + int length = 0; + size_t i = 0; + unsigned char number_buffer[26] = {0}; /* temporary buffer to print the number into */ + unsigned char decimal_point = get_decimal_point(); + double test = 0.0; + + if (output_buffer == NULL) + { + return false; + } + + /* This checks for NaN and Infinity */ + if (isnan(d) || isinf(d)) + { + length = sprintf((char*)number_buffer, "null"); + } + else if(d == (double)item->valueint) + { + length = sprintf((char*)number_buffer, "%d", item->valueint); + } + else + { + /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ + length = sprintf((char*)number_buffer, "%1.15g", d); + + /* Check whether the original double can be recovered */ + if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) + { + /* If not, print with 17 decimal places of precision */ + length = sprintf((char*)number_buffer, "%1.17g", d); + } + } + + /* sprintf failed or buffer overrun occurred */ + if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1))) + { + return false; + } + + /* reserve appropriate space in the output */ + output_pointer = ensure(output_buffer, (size_t)length + sizeof("")); + if (output_pointer == NULL) + { + return false; + } + + /* copy the printed number to the output and replace locale + * dependent decimal point with '.' */ + for (i = 0; i < ((size_t)length); i++) + { + if (number_buffer[i] == decimal_point) + { + output_pointer[i] = '.'; + continue; + } + + output_pointer[i] = number_buffer[i]; + } + output_pointer[i] = '\0'; + + output_buffer->offset += (size_t)length; + + return true; +} + +/* parse 4 digit hexadecimal number */ +static unsigned parse_hex4(const unsigned char * const input) +{ + unsigned int h = 0; + size_t i = 0; + + for (i = 0; i < 4; i++) + { + /* parse digit */ + if ((input[i] >= '0') && (input[i] <= '9')) + { + h += (unsigned int) input[i] - '0'; + } + else if ((input[i] >= 'A') && (input[i] <= 'F')) + { + h += (unsigned int) 10 + input[i] - 'A'; + } + else if ((input[i] >= 'a') && (input[i] <= 'f')) + { + h += (unsigned int) 10 + input[i] - 'a'; + } + else /* invalid */ + { + return 0; + } + + if (i < 3) + { + /* shift left to make place for the next nibble */ + h = h << 4; + } + } + + return h; +} + +/* converts a UTF-16 literal to UTF-8 + * A literal can be one or two sequences of the form \uXXXX */ +static unsigned char utf16_literal_to_utf8(const unsigned char * const input_pointer, const unsigned char * const input_end, unsigned char **output_pointer) +{ + long unsigned int codepoint = 0; + unsigned int first_code = 0; + const unsigned char *first_sequence = input_pointer; + unsigned char utf8_length = 0; + unsigned char utf8_position = 0; + unsigned char sequence_length = 0; + unsigned char first_byte_mark = 0; + + if ((input_end - first_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + /* get the first utf16 sequence */ + first_code = parse_hex4(first_sequence + 2); + + /* check that the code is valid */ + if (((first_code >= 0xDC00) && (first_code <= 0xDFFF))) + { + goto fail; + } + + /* UTF16 surrogate pair */ + if ((first_code >= 0xD800) && (first_code <= 0xDBFF)) + { + const unsigned char *second_sequence = first_sequence + 6; + unsigned int second_code = 0; + sequence_length = 12; /* \uXXXX\uXXXX */ + + if ((input_end - second_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + if ((second_sequence[0] != '\\') || (second_sequence[1] != 'u')) + { + /* missing second half of the surrogate pair */ + goto fail; + } + + /* get the second utf16 sequence */ + second_code = parse_hex4(second_sequence + 2); + /* check that the code is valid */ + if ((second_code < 0xDC00) || (second_code > 0xDFFF)) + { + /* invalid second half of the surrogate pair */ + goto fail; + } + + + /* calculate the unicode codepoint from the surrogate pair */ + codepoint = 0x10000 + (((first_code & 0x3FF) << 10) | (second_code & 0x3FF)); + } + else + { + sequence_length = 6; /* \uXXXX */ + codepoint = first_code; + } + + /* encode as UTF-8 + * takes at maximum 4 bytes to encode: + * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ + if (codepoint < 0x80) + { + /* normal ascii, encoding 0xxxxxxx */ + utf8_length = 1; + } + else if (codepoint < 0x800) + { + /* two bytes, encoding 110xxxxx 10xxxxxx */ + utf8_length = 2; + first_byte_mark = 0xC0; /* 11000000 */ + } + else if (codepoint < 0x10000) + { + /* three bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx */ + utf8_length = 3; + first_byte_mark = 0xE0; /* 11100000 */ + } + else if (codepoint <= 0x10FFFF) + { + /* four bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx */ + utf8_length = 4; + first_byte_mark = 0xF0; /* 11110000 */ + } + else + { + /* invalid unicode codepoint */ + goto fail; + } + + /* encode as utf8 */ + for (utf8_position = (unsigned char)(utf8_length - 1); utf8_position > 0; utf8_position--) + { + /* 10xxxxxx */ + (*output_pointer)[utf8_position] = (unsigned char)((codepoint | 0x80) & 0xBF); + codepoint >>= 6; + } + /* encode first byte */ + if (utf8_length > 1) + { + (*output_pointer)[0] = (unsigned char)((codepoint | first_byte_mark) & 0xFF); + } + else + { + (*output_pointer)[0] = (unsigned char)(codepoint & 0x7F); + } + + *output_pointer += utf8_length; + + return sequence_length; + +fail: + return 0; +} + +/* Parse the input text into an unescaped cinput, and populate item. */ +static cJSON_bool parse_string(cJSON * const item, parse_buffer * const input_buffer) +{ + const unsigned char *input_pointer = buffer_at_offset(input_buffer) + 1; + const unsigned char *input_end = buffer_at_offset(input_buffer) + 1; + unsigned char *output_pointer = NULL; + unsigned char *output = NULL; + + /* not a string */ + if (buffer_at_offset(input_buffer)[0] != '\"') + { + goto fail; + } + + { + /* calculate approximate size of the output (overestimate) */ + size_t allocation_length = 0; + size_t skipped_bytes = 0; + while (((size_t)(input_end - input_buffer->content) < input_buffer->length) && (*input_end != '\"')) + { + /* is escape sequence */ + if (input_end[0] == '\\') + { + if ((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length) + { + /* prevent buffer overflow when last input character is a backslash */ + goto fail; + } + skipped_bytes++; + input_end++; + } + input_end++; + } + if (((size_t)(input_end - input_buffer->content) >= input_buffer->length) || (*input_end != '\"')) + { + goto fail; /* string ended unexpectedly */ + } + + /* This is at most how much we need for the output */ + allocation_length = (size_t) (input_end - buffer_at_offset(input_buffer)) - skipped_bytes; + output = (unsigned char*)input_buffer->hooks.allocate(allocation_length + sizeof("")); + if (output == NULL) + { + goto fail; /* allocation failure */ + } + } + + output_pointer = output; + /* loop through the string literal */ + while (input_pointer < input_end) + { + if (*input_pointer != '\\') + { + *output_pointer++ = *input_pointer++; + } + /* escape sequence */ + else + { + unsigned char sequence_length = 2; + if ((input_end - input_pointer) < 1) + { + goto fail; + } + + switch (input_pointer[1]) + { + case 'b': + *output_pointer++ = '\b'; + break; + case 'f': + *output_pointer++ = '\f'; + break; + case 'n': + *output_pointer++ = '\n'; + break; + case 'r': + *output_pointer++ = '\r'; + break; + case 't': + *output_pointer++ = '\t'; + break; + case '\"': + case '\\': + case '/': + *output_pointer++ = input_pointer[1]; + break; + + /* UTF-16 literal */ + case 'u': + sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer); + if (sequence_length == 0) + { + /* failed to convert UTF16-literal to UTF-8 */ + goto fail; + } + break; + + default: + goto fail; + } + input_pointer += sequence_length; + } + } + + /* zero terminate the output */ + *output_pointer = '\0'; + + item->type = cJSON_String; + item->valuestring = (char*)output; + + input_buffer->offset = (size_t) (input_end - input_buffer->content); + input_buffer->offset++; + + return true; + +fail: + if (output != NULL) + { + input_buffer->hooks.deallocate(output); + output = NULL; + } + + if (input_pointer != NULL) + { + input_buffer->offset = (size_t)(input_pointer - input_buffer->content); + } + + return false; +} + +/* Render the cstring provided to an escaped version that can be printed. */ +static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer) +{ + const unsigned char *input_pointer = NULL; + unsigned char *output = NULL; + unsigned char *output_pointer = NULL; + size_t output_length = 0; + /* numbers of additional characters needed for escaping */ + size_t escape_characters = 0; + + if (output_buffer == NULL) + { + return false; + } + + /* empty string */ + if (input == NULL) + { + output = ensure(output_buffer, sizeof("\"\"")); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "\"\""); + + return true; + } + + /* set "flag" to 1 if something needs to be escaped */ + for (input_pointer = input; *input_pointer; input_pointer++) + { + switch (*input_pointer) + { + case '\"': + case '\\': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + /* one character escape sequence */ + escape_characters++; + break; + default: + if (*input_pointer < 32) + { + /* UTF-16 escape sequence uXXXX */ + escape_characters += 5; + } + break; + } + } + output_length = (size_t)(input_pointer - input) + escape_characters; + + output = ensure(output_buffer, output_length + sizeof("\"\"")); + if (output == NULL) + { + return false; + } + + /* no characters have to be escaped */ + if (escape_characters == 0) + { + output[0] = '\"'; + memcpy(output + 1, input, output_length); + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; + } + + output[0] = '\"'; + output_pointer = output + 1; + /* copy the string */ + for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) + { + if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) + { + /* normal character, copy */ + *output_pointer = *input_pointer; + } + else + { + /* character needs to be escaped */ + *output_pointer++ = '\\'; + switch (*input_pointer) + { + case '\\': + *output_pointer = '\\'; + break; + case '\"': + *output_pointer = '\"'; + break; + case '\b': + *output_pointer = 'b'; + break; + case '\f': + *output_pointer = 'f'; + break; + case '\n': + *output_pointer = 'n'; + break; + case '\r': + *output_pointer = 'r'; + break; + case '\t': + *output_pointer = 't'; + break; + default: + /* escape and print as unicode codepoint */ + sprintf((char*)output_pointer, "u%04x", *input_pointer); + output_pointer += 4; + break; + } + } + } + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; +} + +/* Invoke print_string_ptr (which is useful) on an item. */ +static cJSON_bool print_string(const cJSON * const item, printbuffer * const p) +{ + return print_string_ptr((unsigned char*)item->valuestring, p); +} + +/* Predeclare these prototypes. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer); + +/* Utility to jump whitespace and cr/lf */ +static parse_buffer *buffer_skip_whitespace(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL)) + { + return NULL; + } + + if (cannot_access_at_index(buffer, 0)) + { + return buffer; + } + + while (can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32)) + { + buffer->offset++; + } + + if (buffer->offset == buffer->length) + { + buffer->offset--; + } + + return buffer; +} + +/* skip the UTF-8 BOM (byte order mark) if it is at the beginning of a buffer */ +static parse_buffer *skip_utf8_bom(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0)) + { + return NULL; + } + + if (can_access_at_index(buffer, 4) && (strncmp((const char*)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0)) + { + buffer->offset += 3; + } + + return buffer; +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + size_t buffer_length; + + if (NULL == value) + { + return NULL; + } + + /* Adding null character size due to require_null_terminated. */ + buffer_length = strlen(value) + sizeof(""); + + return cJSON_ParseWithLengthOpts(value, buffer_length, return_parse_end, require_null_terminated); +} + +/* Parse an object - create a new root, and populate. */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } }; + cJSON *item = NULL; + + /* reset error position */ + global_error.json = NULL; + global_error.position = 0; + + if (value == NULL || 0 == buffer_length) + { + goto fail; + } + + buffer.content = (const unsigned char*)value; + buffer.length = buffer_length; + buffer.offset = 0; + buffer.hooks = global_hooks; + + item = cJSON_New_Item(&global_hooks); + if (item == NULL) /* memory fail */ + { + goto fail; + } + + if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer)))) + { + /* parse failure. ep is set. */ + goto fail; + } + + /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */ + if (require_null_terminated) + { + buffer_skip_whitespace(&buffer); + if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0') + { + goto fail; + } + } + if (return_parse_end) + { + *return_parse_end = (const char*)buffer_at_offset(&buffer); + } + + return item; + +fail: + if (item != NULL) + { + cJSON_Delete(item); + } + + if (value != NULL) + { + error local_error; + local_error.json = (const unsigned char*)value; + local_error.position = 0; + + if (buffer.offset < buffer.length) + { + local_error.position = buffer.offset; + } + else if (buffer.length > 0) + { + local_error.position = buffer.length - 1; + } + + if (return_parse_end != NULL) + { + *return_parse_end = (const char*)local_error.json + local_error.position; + } + + global_error = local_error; + } + + return NULL; +} + +/* Default options for cJSON_Parse */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value) +{ + return cJSON_ParseWithOpts(value, 0, 0); +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length) +{ + return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0); +} + +#define cjson_min(a, b) (((a) < (b)) ? (a) : (b)) + +static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks) +{ + static const size_t default_buffer_size = 256; + printbuffer buffer[1]; + unsigned char *printed = NULL; + + memset(buffer, 0, sizeof(buffer)); + + /* create buffer */ + buffer->buffer = (unsigned char*) hooks->allocate(default_buffer_size); + buffer->length = default_buffer_size; + buffer->format = format; + buffer->hooks = *hooks; + if (buffer->buffer == NULL) + { + goto fail; + } + + /* print the value */ + if (!print_value(item, buffer)) + { + goto fail; + } + update_offset(buffer); + + /* check if reallocate is available */ + if (hooks->reallocate != NULL) + { + printed = (unsigned char*) hooks->reallocate(buffer->buffer, buffer->offset + 1); + if (printed == NULL) { + goto fail; + } + buffer->buffer = NULL; + } + else /* otherwise copy the JSON over to a new buffer */ + { + printed = (unsigned char*) hooks->allocate(buffer->offset + 1); + if (printed == NULL) + { + goto fail; + } + memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1)); + printed[buffer->offset] = '\0'; /* just to be sure */ + + /* free the buffer */ + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + return printed; + +fail: + if (buffer->buffer != NULL) + { + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + if (printed != NULL) + { + hooks->deallocate(printed); + printed = NULL; + } + + return NULL; +} + +/* Render a cJSON item/entity/structure to text. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item) +{ + return (char*)print(item, true, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item) +{ + return (char*)print(item, false, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if (prebuffer < 0) + { + return NULL; + } + + p.buffer = (unsigned char*)global_hooks.allocate((size_t)prebuffer); + if (!p.buffer) + { + return NULL; + } + + p.length = (size_t)prebuffer; + p.offset = 0; + p.noalloc = false; + p.format = fmt; + p.hooks = global_hooks; + + if (!print_value(item, &p)) + { + global_hooks.deallocate(p.buffer); + p.buffer = NULL; + return NULL; + } + + return (char*)p.buffer; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if ((length < 0) || (buffer == NULL)) + { + return false; + } + + p.buffer = (unsigned char*)buffer; + p.length = (size_t)length; + p.offset = 0; + p.noalloc = true; + p.format = format; + p.hooks = global_hooks; + + return print_value(item, &p); +} + +/* Parser core - when encountering text, process appropriately. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer) +{ + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; /* no input */ + } + + /* parse the different types of values */ + /* null */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "null", 4) == 0)) + { + item->type = cJSON_NULL; + input_buffer->offset += 4; + return true; + } + /* false */ + if (can_read(input_buffer, 5) && (strncmp((const char*)buffer_at_offset(input_buffer), "false", 5) == 0)) + { + item->type = cJSON_False; + input_buffer->offset += 5; + return true; + } + /* true */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "true", 4) == 0)) + { + item->type = cJSON_True; + item->valueint = 1; + input_buffer->offset += 4; + return true; + } + /* string */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"')) + { + return parse_string(item, input_buffer); + } + /* number */ + if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9')))) + { + return parse_number(item, input_buffer); + } + /* array */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '[')) + { + return parse_array(item, input_buffer); + } + /* object */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{')) + { + return parse_object(item, input_buffer); + } + + return false; +} + +/* Render a value to text. */ +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output = NULL; + + if ((item == NULL) || (output_buffer == NULL)) + { + return false; + } + + switch ((item->type) & 0xFF) + { + case cJSON_NULL: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "null"); + return true; + + case cJSON_False: + output = ensure(output_buffer, 6); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "false"); + return true; + + case cJSON_True: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "true"); + return true; + + case cJSON_Number: + return print_number(item, output_buffer); + + case cJSON_Raw: + { + size_t raw_length = 0; + if (item->valuestring == NULL) + { + return false; + } + + raw_length = strlen(item->valuestring) + sizeof(""); + output = ensure(output_buffer, raw_length); + if (output == NULL) + { + return false; + } + memcpy(output, item->valuestring, raw_length); + return true; + } + + case cJSON_String: + return print_string(item, output_buffer); + + case cJSON_Array: + return print_array(item, output_buffer); + + case cJSON_Object: + return print_object(item, output_buffer); + + default: + return false; + } +} + +/* Build an array from input text. */ +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* head of the linked list */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (buffer_at_offset(input_buffer)[0] != '[') + { + /* not an array */ + goto fail; + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']')) + { + /* empty array */ + goto success; + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + /* parse next value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']') + { + goto fail; /* expected end of array */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Array; + item->child = head; + + input_buffer->offset++; + + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an array to text */ +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_element = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output array. */ + /* opening square bracket */ + output_pointer = ensure(output_buffer, 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer = '['; + output_buffer->offset++; + output_buffer->depth++; + + while (current_element != NULL) + { + if (!print_value(current_element, output_buffer)) + { + return false; + } + update_offset(output_buffer); + if (current_element->next) + { + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ','; + if(output_buffer->format) + { + *output_pointer++ = ' '; + } + *output_pointer = '\0'; + output_buffer->offset += length; + } + current_element = current_element->next; + } + + output_pointer = ensure(output_buffer, 2); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ']'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Build an object from the text. */ +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* linked list head */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{')) + { + goto fail; /* not an object */ + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}')) + { + goto success; /* empty object */ + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + if (cannot_access_at_index(input_buffer, 1)) + { + goto fail; /* nothing comes after the comma */ + } + + /* parse the name of the child */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_string(current_item, input_buffer)) + { + goto fail; /* failed to parse name */ + } + buffer_skip_whitespace(input_buffer); + + /* swap valuestring and string, because we parsed the name */ + current_item->string = current_item->valuestring; + current_item->valuestring = NULL; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':')) + { + goto fail; /* invalid object */ + } + + /* parse the value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}')) + { + goto fail; /* expected end of object */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Object; + item->child = head; + + input_buffer->offset++; + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an object to text. */ +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_item = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output: */ + length = (size_t) (output_buffer->format ? 2 : 1); /* fmt: {\n */ + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer++ = '{'; + output_buffer->depth++; + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + output_buffer->offset += length; + + while (current_item) + { + if (output_buffer->format) + { + size_t i; + output_pointer = ensure(output_buffer, output_buffer->depth); + if (output_pointer == NULL) + { + return false; + } + for (i = 0; i < output_buffer->depth; i++) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += output_buffer->depth; + } + + /* print key */ + if (!print_string_ptr((unsigned char*)current_item->string, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ':'; + if (output_buffer->format) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += length; + + /* print value */ + if (!print_value(current_item, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + /* print comma if not last */ + length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0)); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + if (current_item->next) + { + *output_pointer++ = ','; + } + + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + *output_pointer = '\0'; + output_buffer->offset += length; + + current_item = current_item->next; + } + + output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2); + if (output_pointer == NULL) + { + return false; + } + if (output_buffer->format) + { + size_t i; + for (i = 0; i < (output_buffer->depth - 1); i++) + { + *output_pointer++ = '\t'; + } + } + *output_pointer++ = '}'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Get Array size/item / object item. */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array) +{ + cJSON *child = NULL; + size_t size = 0; + + if (array == NULL) + { + return 0; + } + + child = array->child; + + while(child != NULL) + { + size++; + child = child->next; + } + + /* FIXME: Can overflow here. Cannot be fixed without breaking the API */ + + return (int)size; +} + +static cJSON* get_array_item(const cJSON *array, size_t index) +{ + cJSON *current_child = NULL; + + if (array == NULL) + { + return NULL; + } + + current_child = array->child; + while ((current_child != NULL) && (index > 0)) + { + index--; + current_child = current_child->next; + } + + return current_child; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index) +{ + if (index < 0) + { + return NULL; + } + + return get_array_item(array, (size_t)index); +} + +static cJSON *get_object_item(const cJSON * const object, const char * const name, const cJSON_bool case_sensitive) +{ + cJSON *current_element = NULL; + + if ((object == NULL) || (name == NULL)) + { + return NULL; + } + + current_element = object->child; + if (case_sensitive) + { + while ((current_element != NULL) && (current_element->string != NULL) && (strcmp(name, current_element->string) != 0)) + { + current_element = current_element->next; + } + } + else + { + while ((current_element != NULL) && (case_insensitive_strcmp((const unsigned char*)name, (const unsigned char*)(current_element->string)) != 0)) + { + current_element = current_element->next; + } + } + + if ((current_element == NULL) || (current_element->string == NULL)) { + return NULL; + } + + return current_element; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, false); +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string) +{ + return cJSON_GetObjectItem(object, string) ? 1 : 0; +} + +/* Utility for array list handling. */ +static void suffix_object(cJSON *prev, cJSON *item) +{ + prev->next = item; + item->prev = prev; +} + +/* Utility for handling references. */ +static cJSON *create_reference(const cJSON *item, const internal_hooks * const hooks) +{ + cJSON *reference = NULL; + if (item == NULL) + { + return NULL; + } + + reference = cJSON_New_Item(hooks); + if (reference == NULL) + { + return NULL; + } + + memcpy(reference, item, sizeof(cJSON)); + reference->string = NULL; + reference->type |= cJSON_IsReference; + reference->next = reference->prev = NULL; + return reference; +} + +static cJSON_bool add_item_to_array(cJSON *array, cJSON *item) +{ + cJSON *child = NULL; + + if ((item == NULL) || (array == NULL) || (array == item)) + { + return false; + } + + child = array->child; + /* + * To find the last item in array quickly, we use prev in array + */ + if (child == NULL) + { + /* list is empty, start new one */ + array->child = item; + item->prev = item; + item->next = NULL; + } + else + { + /* append to the end */ + if (child->prev) + { + suffix_object(child->prev, item); + array->child->prev = item; + } + } + + return true; +} + +/* Add item to array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item) +{ + return add_item_to_array(array, item); +} + +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic push +#endif +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif +/* helper function to cast away const */ +static void* cast_away_const(const void* string) +{ + return (void*)string; +} +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic pop +#endif + + +static cJSON_bool add_item_to_object(cJSON * const object, const char * const string, cJSON * const item, const internal_hooks * const hooks, const cJSON_bool constant_key) +{ + char *new_key = NULL; + int new_type = cJSON_Invalid; + + if ((object == NULL) || (string == NULL) || (item == NULL) || (object == item)) + { + return false; + } + + if (constant_key) + { + new_key = (char*)cast_away_const(string); + new_type = item->type | cJSON_StringIsConst; + } + else + { + new_key = (char*)cJSON_strdup((const unsigned char*)string, hooks); + if (new_key == NULL) + { + return false; + } + + new_type = item->type & ~cJSON_StringIsConst; + } + + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + hooks->deallocate(item->string); + } + + item->string = new_key; + item->type = new_type; + + return add_item_to_array(object, item); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, false); +} + +/* Add an item to an object with constant string as key */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item) +{ + if (array == NULL) + { + return false; + } + + return add_item_to_array(array, create_reference(item, &global_hooks)); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item) +{ + if ((object == NULL) || (string == NULL)) + { + return false; + } + + return add_item_to_object(object, string, create_reference(item, &global_hooks), &global_hooks, false); +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name) +{ + cJSON *null = cJSON_CreateNull(); + if (add_item_to_object(object, name, null, &global_hooks, false)) + { + return null; + } + + cJSON_Delete(null); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name) +{ + cJSON *true_item = cJSON_CreateTrue(); + if (add_item_to_object(object, name, true_item, &global_hooks, false)) + { + return true_item; + } + + cJSON_Delete(true_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name) +{ + cJSON *false_item = cJSON_CreateFalse(); + if (add_item_to_object(object, name, false_item, &global_hooks, false)) + { + return false_item; + } + + cJSON_Delete(false_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean) +{ + cJSON *bool_item = cJSON_CreateBool(boolean); + if (add_item_to_object(object, name, bool_item, &global_hooks, false)) + { + return bool_item; + } + + cJSON_Delete(bool_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number) +{ + cJSON *number_item = cJSON_CreateNumber(number); + if (add_item_to_object(object, name, number_item, &global_hooks, false)) + { + return number_item; + } + + cJSON_Delete(number_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string) +{ + cJSON *string_item = cJSON_CreateString(string); + if (add_item_to_object(object, name, string_item, &global_hooks, false)) + { + return string_item; + } + + cJSON_Delete(string_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw) +{ + cJSON *raw_item = cJSON_CreateRaw(raw); + if (add_item_to_object(object, name, raw_item, &global_hooks, false)) + { + return raw_item; + } + + cJSON_Delete(raw_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name) +{ + cJSON *object_item = cJSON_CreateObject(); + if (add_item_to_object(object, name, object_item, &global_hooks, false)) + { + return object_item; + } + + cJSON_Delete(object_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name) +{ + cJSON *array = cJSON_CreateArray(); + if (add_item_to_object(object, name, array, &global_hooks, false)) + { + return array; + } + + cJSON_Delete(array); + return NULL; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item) +{ + if ((parent == NULL) || (item == NULL) || (item != parent->child && item->prev == NULL)) + { + return NULL; + } + + if (item != parent->child) + { + /* not the first element */ + item->prev->next = item->next; + } + if (item->next != NULL) + { + /* not the last element */ + item->next->prev = item->prev; + } + + if (item == parent->child) + { + /* first element */ + parent->child = item->next; + } + else if (item->next == NULL) + { + /* last element */ + parent->child->prev = item->prev; + } + + /* make sure the detached item doesn't point anywhere anymore */ + item->prev = NULL; + item->next = NULL; + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which) +{ + if (which < 0) + { + return NULL; + } + + return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which) +{ + cJSON_Delete(cJSON_DetachItemFromArray(array, which)); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItem(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItemCaseSensitive(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObject(object, string)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObjectCaseSensitive(object, string)); +} + +/* Replace array/object items with new ones. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem) +{ + cJSON *after_inserted = NULL; + + if (which < 0 || newitem == NULL) + { + return false; + } + + after_inserted = get_array_item(array, (size_t)which); + if (after_inserted == NULL) + { + return add_item_to_array(array, newitem); + } + + if (after_inserted != array->child && after_inserted->prev == NULL) { + /* return false if after_inserted is a corrupted array item */ + return false; + } + + newitem->next = after_inserted; + newitem->prev = after_inserted->prev; + after_inserted->prev = newitem; + if (after_inserted == array->child) + { + array->child = newitem; + } + else + { + newitem->prev->next = newitem; + } + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement) +{ + if ((parent == NULL) || (parent->child == NULL) || (replacement == NULL) || (item == NULL)) + { + return false; + } + + if (replacement == item) + { + return true; + } + + replacement->next = item->next; + replacement->prev = item->prev; + + if (replacement->next != NULL) + { + replacement->next->prev = replacement; + } + if (parent->child == item) + { + if (parent->child->prev == parent->child) + { + replacement->prev = replacement; + } + parent->child = replacement; + } + else + { /* + * To find the last item in array quickly, we use prev in array. + * We can't modify the last item's next pointer where this item was the parent's child + */ + if (replacement->prev != NULL) + { + replacement->prev->next = replacement; + } + if (replacement->next == NULL) + { + parent->child->prev = replacement; + } + } + + item->next = NULL; + item->prev = NULL; + cJSON_Delete(item); + + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem) +{ + if (which < 0) + { + return false; + } + + return cJSON_ReplaceItemViaPointer(array, get_array_item(array, (size_t)which), newitem); +} + +static cJSON_bool replace_item_in_object(cJSON *object, const char *string, cJSON *replacement, cJSON_bool case_sensitive) +{ + if ((replacement == NULL) || (string == NULL)) + { + return false; + } + + /* replace the name in the replacement */ + if (!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL)) + { + cJSON_free(replacement->string); + } + replacement->string = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if (replacement->string == NULL) + { + return false; + } + + replacement->type &= ~cJSON_StringIsConst; + + return cJSON_ReplaceItemViaPointer(object, get_object_item(object, string, case_sensitive), replacement); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, false); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, true); +} + +/* Create basic types: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_NULL; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_True; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = boolean ? cJSON_True : cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Number; + item->valuedouble = num; + + /* use saturation in case of overflow */ + if (num >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (num <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)num; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_String; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) + { + item->type = cJSON_String | cJSON_IsReference; + item->valuestring = (char*)cast_away_const(string); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Object | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child) { + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Array | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Raw; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)raw, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type=cJSON_Array; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_Object; + } + + return item; +} + +/* Create Arrays: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if (!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber((double)numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (strings == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for (i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateString(strings[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p,n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +/* Duplication */ +cJSON * cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recurse); + +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse) +{ + return cJSON_Duplicate_rec(item, 0, recurse ); +} + +cJSON * cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recurse) +{ + cJSON *newitem = NULL; + cJSON *child = NULL; + cJSON *next = NULL; + cJSON *newchild = NULL; + + /* Bail on bad ptr */ + if (!item) + { + goto fail; + } + /* Create new item */ + newitem = cJSON_New_Item(&global_hooks); + if (!newitem) + { + goto fail; + } + /* Copy over all vars */ + newitem->type = item->type & (~cJSON_IsReference); + newitem->valueint = item->valueint; + newitem->valuedouble = item->valuedouble; + if (item->valuestring) + { + newitem->valuestring = (char*)cJSON_strdup((unsigned char*)item->valuestring, &global_hooks); + if (!newitem->valuestring) + { + goto fail; + } + } + if (item->string) + { + newitem->string = (item->type&cJSON_StringIsConst) ? item->string : (char*)cJSON_strdup((unsigned char*)item->string, &global_hooks); + if (!newitem->string) + { + goto fail; + } + } + /* If non-recursive, then we're done! */ + if (!recurse) + { + return newitem; + } + /* Walk the ->next chain for the child. */ + child = item->child; + while (child != NULL) + { + if(depth >= CJSON_CIRCULAR_LIMIT) { + goto fail; + } + newchild = cJSON_Duplicate_rec(child, depth + 1, true); /* Duplicate (with recurse) each item in the ->next chain */ + if (!newchild) + { + goto fail; + } + if (next != NULL) + { + /* If newitem->child already set, then crosswire ->prev and ->next and move on */ + next->next = newchild; + newchild->prev = next; + next = newchild; + } + else + { + /* Set newitem->child and move to it */ + newitem->child = newchild; + next = newchild; + } + child = child->next; + } + if (newitem && newitem->child) + { + newitem->child->prev = newchild; + } + + return newitem; + +fail: + if (newitem != NULL) + { + cJSON_Delete(newitem); + } + + return NULL; +} + +static void skip_oneline_comment(char **input) +{ + *input += static_strlen("//"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if ((*input)[0] == '\n') { + *input += static_strlen("\n"); + return; + } + } +} + +static void skip_multiline_comment(char **input) +{ + *input += static_strlen("/*"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if (((*input)[0] == '*') && ((*input)[1] == '/')) + { + *input += static_strlen("*/"); + return; + } + } +} + +static void minify_string(char **input, char **output) { + (*output)[0] = (*input)[0]; + *input += static_strlen("\""); + *output += static_strlen("\""); + + + for (; (*input)[0] != '\0'; (void)++(*input), ++(*output)) { + (*output)[0] = (*input)[0]; + + if ((*input)[0] == '\"') { + (*output)[0] = '\"'; + *input += static_strlen("\""); + *output += static_strlen("\""); + return; + } else if (((*input)[0] == '\\') && ((*input)[1] == '\"')) { + (*output)[1] = (*input)[1]; + *input += static_strlen("\""); + *output += static_strlen("\""); + } + } +} + +CJSON_PUBLIC(void) cJSON_Minify(char *json) +{ + char *into = json; + + if (json == NULL) + { + return; + } + + while (json[0] != '\0') + { + switch (json[0]) + { + case ' ': + case '\t': + case '\r': + case '\n': + json++; + break; + + case '/': + if (json[1] == '/') + { + skip_oneline_comment(&json); + } + else if (json[1] == '*') + { + skip_multiline_comment(&json); + } else { + json++; + } + break; + + case '\"': + minify_string(&json, (char**)&into); + break; + + default: + into[0] = json[0]; + json++; + into++; + } + } + + /* and null-terminate. */ + *into = '\0'; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Invalid; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_False; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xff) == cJSON_True; +} + + +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & (cJSON_True | cJSON_False)) != 0; +} +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_NULL; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Number; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_String; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Array; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Object; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Raw; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive) +{ + if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) + { + return false; + } + + /* check if type is valid */ + switch (a->type & 0xFF) + { + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + case cJSON_Number: + case cJSON_String: + case cJSON_Raw: + case cJSON_Array: + case cJSON_Object: + break; + + default: + return false; + } + + /* identical objects are equal */ + if (a == b) + { + return true; + } + + switch (a->type & 0xFF) + { + /* in these cases and equal type is enough */ + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + return true; + + case cJSON_Number: + if (compare_double(a->valuedouble, b->valuedouble)) + { + return true; + } + return false; + + case cJSON_String: + case cJSON_Raw: + if ((a->valuestring == NULL) || (b->valuestring == NULL)) + { + return false; + } + if (strcmp(a->valuestring, b->valuestring) == 0) + { + return true; + } + + return false; + + case cJSON_Array: + { + cJSON *a_element = a->child; + cJSON *b_element = b->child; + + for (; (a_element != NULL) && (b_element != NULL);) + { + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + + a_element = a_element->next; + b_element = b_element->next; + } + + /* one of the arrays is longer than the other */ + if (a_element != b_element) { + return false; + } + + return true; + } + + case cJSON_Object: + { + cJSON *a_element = NULL; + cJSON *b_element = NULL; + cJSON_ArrayForEach(a_element, a) + { + /* TODO This has O(n^2) runtime, which is horrible! */ + b_element = get_object_item(b, a_element->string, case_sensitive); + if (b_element == NULL) + { + return false; + } + + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + } + + /* doing this twice, once on a and b to prevent true comparison if a subset of b + * TODO: Do this the proper way, this is just a fix for now */ + cJSON_ArrayForEach(b_element, b) + { + a_element = get_object_item(a, b_element->string, case_sensitive); + if (a_element == NULL) + { + return false; + } + + if (!cJSON_Compare(b_element, a_element, case_sensitive)) + { + return false; + } + } + + return true; + } + + default: + return false; + } +} + +CJSON_PUBLIC(void *) cJSON_malloc(size_t size) +{ + return global_hooks.allocate(size); +} + +CJSON_PUBLIC(void) cJSON_free(void *object) +{ + global_hooks.deallocate(object); + object = NULL; +} diff --git a/src/client.c b/src/client.c new file mode 100644 index 0000000..14c582b --- /dev/null +++ b/src/client.c @@ -0,0 +1,1110 @@ +/* + * DWN - Desktop Window Manager + * Client (window) management implementation + */ + +#include "client.h" +#include "atoms.h" +#include "config.h" +#include "util.h" +#include "workspace.h" +#include "decorations.h" +#include "notifications.h" + +#include +#include +#include +#include + +/* ========== Client creation/destruction ========== */ + +Client *client_create(Window window) +{ + /* Defensive: validate global state before proceeding */ + if (dwn == NULL || dwn->display == NULL) { + LOG_ERROR("client_create: dwn or display is NULL"); + return NULL; + } + + if (window == None) { + LOG_ERROR("client_create: invalid window (None)"); + return NULL; + } + + Client *client = dwn_calloc(1, sizeof(Client)); + if (client == NULL) { + LOG_ERROR("client_create: failed to allocate client"); + return NULL; + } + + client->window = window; + client->frame = None; + client->border_width = config_get_border_width(); + client->flags = CLIENT_NORMAL; + client->workspace = dwn->current_workspace; + client->next = NULL; + client->prev = NULL; + + /* Get initial geometry from window */ + XWindowAttributes wa; + int orig_width = 640, orig_height = 480; + if (XGetWindowAttributes(dwn->display, window, &wa)) { + orig_width = wa.width; + orig_height = wa.height; + } + + /* Calculate work area (account for panels) */ + int work_x = 0; + int work_y = 0; + int work_width = dwn->screen_width; + int work_height = dwn->screen_height; + + if (dwn->config != NULL) { + if (dwn->config->top_panel_enabled) { + work_y += config_get_panel_height(); + work_height -= config_get_panel_height(); + } + if (dwn->config->bottom_panel_enabled) { + work_height -= config_get_panel_height(); + } + } + + /* Set size to 75% of work area or original size, whichever is larger */ + int target_width = (work_width * 75) / 100; + int target_height = (work_height * 75) / 100; + + client->width = (orig_width > target_width) ? orig_width : target_width; + client->height = (orig_height > target_height) ? orig_height : target_height; + + /* Clamp to work area */ + if (client->width > work_width - 20) client->width = work_width - 20; + if (client->height > work_height - 20) client->height = work_height - 20; + + /* Center on screen */ + client->x = work_x + (work_width - client->width) / 2; + client->y = work_y + (work_height - client->height) / 2; + + /* Save original geometry for floating restore */ + client->old_x = client->x; + client->old_y = client->y; + client->old_width = client->width; + client->old_height = client->height; + + /* Update properties */ + client_update_title(client); + client_update_class(client); + + return client; +} + +void client_destroy(Client *client) +{ + if (client == NULL) { + return; + } + + if (client->frame != None) { + client_destroy_frame(client); + } + + dwn_free(client); +} + +/* Sync log for debugging - disabled in production */ +static inline void client_sync_log(const char *msg) +{ + (void)msg; /* No-op - enable for debugging */ +} + +/* ========== Client management ========== */ + +Client *client_manage(Window window) +{ + client_sync_log("client_manage: START"); + + if (dwn == NULL || dwn->display == NULL) { + client_sync_log("client_manage: dwn NULL"); + return NULL; + } + + /* Check if already managed */ + Client *existing = client_find_by_window(window); + if (existing != NULL) { + client_sync_log("client_manage: already managed"); + return existing; + } + + /* Check window type - don't manage docks, desktops */ + if (client_is_dock(window) || client_is_desktop(window)) { + LOG_DEBUG("Skipping dock/desktop window: %lu", window); + client_sync_log("client_manage: skip dock/desktop"); + return NULL; + } + + LOG_DEBUG("Managing window: %lu", window); + client_sync_log("client_manage: creating client"); + + /* Create client */ + Client *client = client_create(window); + if (client == NULL) { + LOG_ERROR("client_manage: failed to create client for window %lu", window); + client_sync_log("client_manage: create FAILED"); + return NULL; + } + + client_sync_log("client_manage: checking dialog"); + + /* Check if it should be floating (dialogs, etc.) */ + if (client_is_dialog(window)) { + client->flags |= CLIENT_FLOATING; + } + + client_sync_log("client_manage: creating frame"); + + /* Create frame with decorations */ + client_create_frame(client); + + client_sync_log("client_manage: verifying window"); + + /* Verify window still exists before reparenting */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, window, &wa)) { + LOG_WARN("client_manage: window %lu disappeared before reparenting", window); + client_sync_log("client_manage: window gone before reparent"); + client_destroy(client); + return NULL; + } + + client_sync_log("client_manage: reparenting"); + client_reparent_to_frame(client); + + /* Verify both window AND frame still exist after reparent */ + client_sync_log("client_manage: verifying after reparent"); + XSync(dwn->display, False); /* Flush any pending errors */ + + if (client->frame == None) { + client_sync_log("client_manage: frame is None after reparent"); + LOG_WARN("client_manage: frame creation failed for window %lu", window); + client_destroy(client); + return NULL; + } + + XWindowAttributes wa_frame; + if (!XGetWindowAttributes(dwn->display, client->frame, &wa_frame)) { + client_sync_log("client_manage: frame gone after reparent"); + LOG_WARN("client_manage: frame %lu no longer exists", client->frame); + client_destroy(client); + return NULL; + } + + if (!XGetWindowAttributes(dwn->display, window, &wa)) { + client_sync_log("client_manage: window gone after reparent"); + LOG_WARN("client_manage: window %lu disappeared after reparent", window); + client_destroy_frame(client); + client_destroy(client); + return NULL; + } + + client_sync_log("client_manage: configuring"); + /* Resize client window to match frame dimensions */ + client_configure(client); + + client_sync_log("client_manage: adding to list"); + /* Add to client list */ + client_add_to_list(client); + + client_sync_log("client_manage: adding to workspace"); + /* Add to current workspace */ + workspace_add_client(dwn->current_workspace, client); + + client_sync_log("client_manage: setting EWMH"); + /* Set EWMH properties */ + atoms_set_window_desktop(window, client->workspace); + atoms_update_client_list(); + + client_sync_log("client_manage: verifying window again"); + /* Verify window still exists before subscribing to events */ + if (!XGetWindowAttributes(dwn->display, window, &wa)) { + LOG_WARN("client_manage: window %lu disappeared before event setup", window); + client_sync_log("client_manage: window gone before events"); + workspace_remove_client(client->workspace, client); + client_remove_from_list(client); + client_destroy(client); + atoms_update_client_list(); + return NULL; + } + + client_sync_log("client_manage: selecting input"); + /* Subscribe to window events */ + XSelectInput(dwn->display, window, + EnterWindowMask | FocusChangeMask | PropertyChangeMask | + StructureNotifyMask); + + client_sync_log("client_manage: grabbing button"); + /* Grab button for click-to-focus (will replay event to app after focusing) */ + XGrabButton(dwn->display, Button1, AnyModifier, window, False, + ButtonPressMask, GrabModeSync, GrabModeAsync, None, None); + + client_sync_log("client_manage: syncing X"); + /* Sync to ensure all operations completed */ + XSync(dwn->display, False); + + client_sync_log("client_manage: showing"); + /* Map and focus */ + client_show(client); + + client_sync_log("client_manage: focusing"); + client_focus(client); + + client_sync_log("client_manage: DONE"); + return client; +} + +void client_unmanage(Client *client) +{ + client_sync_log("client_unmanage: START"); + + if (client == NULL) { + client_sync_log("client_unmanage: NULL client"); + return; + } + + LOG_DEBUG("Unmanaging window: %lu", client->window); + + client_sync_log("client_unmanage: remove from workspace"); + /* Remove from workspace */ + workspace_remove_client(client->workspace, client); + + client_sync_log("client_unmanage: remove from list"); + /* Remove from client list */ + client_remove_from_list(client); + + client_sync_log("client_unmanage: reparent from frame"); + /* Reparent back to root */ + client_reparent_from_frame(client); + + client_sync_log("client_unmanage: update EWMH"); + /* Update EWMH */ + atoms_update_client_list(); + + client_sync_log("client_unmanage: destroy client"); + /* Destroy client */ + client_destroy(client); + + client_sync_log("client_unmanage: focus next"); + /* Focus next client if needed */ + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused == NULL) { + Client *next = workspace_get_first_client(dwn->current_workspace); + if (next != NULL) { + client_focus(next); + } + } + + client_sync_log("client_unmanage: DONE"); +} + +Client *client_find_by_window(Window window) +{ + if (dwn == NULL || window == None) { + return NULL; + } + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->window == window) { + return c; + } + } + return NULL; +} + +Client *client_find_by_frame(Window frame) +{ + if (dwn == NULL || frame == None) { + return NULL; + } + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->frame == frame) { + return c; + } + } + return NULL; +} + +/* ========== Client state ========== */ + +void client_focus(Client *client) +{ + client_sync_log("client_focus: START"); + + if (client == NULL || dwn == NULL || dwn->display == NULL) { + client_sync_log("client_focus: NULL check failed"); + return; + } + + /* Defensive: verify window is still valid */ + if (client->window == None) { + LOG_WARN("client_focus: client has invalid window (None)"); + client_sync_log("client_focus: window is None"); + return; + } + + /* Verify window still exists before focusing */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_WARN("client_focus: window %lu no longer exists", client->window); + client_sync_log("client_focus: window gone"); + return; + } + + client_sync_log("client_focus: unfocusing previous"); + + /* Unfocus previous */ + Workspace *ws = workspace_get(client->workspace); + if (ws != NULL && ws->focused != NULL && ws->focused != client) { + client_unfocus(ws->focused); + } + + client_sync_log("client_focus: XSetInputFocus"); + + /* Set focus with error handling */ + XSetInputFocus(dwn->display, client->window, RevertToPointerRoot, CurrentTime); + XSync(dwn->display, False); /* Catch any X errors immediately */ + + client_sync_log("client_focus: updating workspace"); + + /* Update workspace */ + if (ws != NULL) { + ws->focused = client; + } + + client_sync_log("client_focus: raising"); + + /* Raise window */ + client_raise(client); + + client_sync_log("client_focus: decorations"); + + /* Update decorations */ + decorations_render(client, true); + + client_sync_log("client_focus: EWMH"); + + /* Update EWMH */ + atoms_set_active_window(client->window); + + client_sync_log("client_focus: DONE"); + LOG_DEBUG("Focused window: %s", client->title); +} + +void client_unfocus(Client *client) +{ + if (client == NULL) { + return; + } + + /* Update decorations */ + decorations_render(client, false); +} + +void client_raise(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + Window win = (client->frame != None) ? client->frame : client->window; + if (win == None) { + return; + } + + XRaiseWindow(dwn->display, win); + + /* Keep notifications on top */ + notifications_raise_all(); +} + +void client_lower(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + Window win = (client->frame != None) ? client->frame : client->window; + if (win == None) { + return; + } + XLowerWindow(dwn->display, win); +} + +void client_minimize(Client *client) +{ + if (client == NULL) { + return; + } + + client->flags |= CLIENT_MINIMIZED; + client_hide(client); +} + +void client_restore(Client *client) +{ + if (client == NULL) { + return; + } + + client->flags &= ~CLIENT_MINIMIZED; + client_show(client); + client_focus(client); +} + +/* ========== Client geometry ========== */ + +void client_move(Client *client, int x, int y) +{ + if (client == NULL) { + return; + } + + client->x = x; + client->y = y; + client_configure(client); +} + +void client_resize(Client *client, int width, int height) +{ + if (client == NULL) { + return; + } + + client_apply_size_hints(client, &width, &height); + client->width = width; + client->height = height; + client_configure(client); +} + +void client_move_resize(Client *client, int x, int y, int width, int height) +{ + if (client == NULL) { + return; + } + + client_apply_size_hints(client, &width, &height); + client->x = x; + client->y = y; + client->width = width; + client->height = height; + client_configure(client); +} + +void client_configure(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + /* Verify window still exists before configuring */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_DEBUG("client_configure: window no longer exists"); + return; + } + + int title_height = (dwn->config && dwn->config->show_decorations) ? + config_get_title_height() : 0; + int border = client->border_width; + + if (client->frame != None) { + /* Move/resize frame */ + XMoveResizeWindow(dwn->display, client->frame, + client->x - border, + client->y - title_height - border, + client->width + 2 * border, + client->height + title_height + 2 * border); + + /* Move/resize client window within frame */ + XMoveResizeWindow(dwn->display, client->window, + border, title_height + border, + client->width, client->height); + } else { + XMoveResizeWindow(dwn->display, client->window, + client->x, client->y, + client->width, client->height); + } + + /* Send configure notify to client */ + XConfigureEvent ce; + memset(&ce, 0, sizeof(ce)); /* Zero-initialize */ + ce.type = ConfigureNotify; + ce.event = client->window; + ce.window = client->window; + ce.x = client->x; + ce.y = client->y; + ce.width = client->width; + ce.height = client->height; + ce.border_width = 0; + ce.above = None; + ce.override_redirect = False; + + XSendEvent(dwn->display, client->window, False, StructureNotifyMask, (XEvent *)&ce); +} + +void client_apply_size_hints(Client *client, int *width, int *height) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + XSizeHints hints; + long supplied; + + if (!XGetWMNormalHints(dwn->display, client->window, &hints, &supplied)) { + return; + } + + /* Minimum size */ + if (hints.flags & PMinSize) { + if (*width < hints.min_width) *width = hints.min_width; + if (*height < hints.min_height) *height = hints.min_height; + } + + /* Maximum size */ + if (hints.flags & PMaxSize) { + if (*width > hints.max_width) *width = hints.max_width; + if (*height > hints.max_height) *height = hints.max_height; + } + + /* Size increments */ + if (hints.flags & PResizeInc) { + int base_w = (hints.flags & PBaseSize) ? hints.base_width : 0; + int base_h = (hints.flags & PBaseSize) ? hints.base_height : 0; + + *width = base_w + ((*width - base_w) / hints.width_inc) * hints.width_inc; + *height = base_h + ((*height - base_h) / hints.height_inc) * hints.height_inc; + } +} + +/* ========== Client properties ========== */ + +void client_update_title(Client *client) +{ + if (client == NULL) { + return; + } + + char *name = atoms_get_window_name(client->window); + if (name != NULL) { + strncpy(client->title, name, sizeof(client->title) - 1); + client->title[sizeof(client->title) - 1] = '\0'; + dwn_free(name); + } else { + strncpy(client->title, "Untitled", sizeof(client->title) - 1); + } + + /* Redraw decorations */ + if (client->frame != None) { + Workspace *ws = workspace_get(client->workspace); + bool focused = (ws != NULL && ws->focused == client); + decorations_render(client, focused); + } +} + +void client_update_class(Client *client) +{ + if (client == NULL) { + return; + } + + atoms_get_wm_class(client->window, client->class, NULL, sizeof(client->class)); +} + +void client_set_fullscreen(Client *client, bool fullscreen) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + /* Verify window still exists */ + if (client->window == None) { + return; + } + + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_WARN("client_set_fullscreen: window %lu no longer exists", client->window); + return; + } + + if (fullscreen) { + /* Save current geometry */ + if (!(client->flags & CLIENT_FULLSCREEN)) { + client->old_x = client->x; + client->old_y = client->y; + client->old_width = client->width; + client->old_height = client->height; + } + + client->flags |= CLIENT_FULLSCREEN; + + /* Get monitor dimensions */ + client->x = 0; + client->y = 0; + client->width = dwn->screen_width; + client->height = dwn->screen_height; + + /* Reparent window to root and hide frame */ + if (client->frame != None) { + XUnmapWindow(dwn->display, client->frame); + XSync(dwn->display, False); + XReparentWindow(dwn->display, client->window, dwn->root, 0, 0); + XSync(dwn->display, False); + } + XMoveResizeWindow(dwn->display, client->window, + 0, 0, client->width, client->height); + XMapWindow(dwn->display, client->window); + XRaiseWindow(dwn->display, client->window); + } else { + client->flags &= ~CLIENT_FULLSCREEN; + + /* Restore geometry */ + client->x = client->old_x; + client->y = client->old_y; + client->width = client->old_width; + client->height = client->old_height; + + /* Reparent back to frame and show it */ + if (client->frame != None) { + int title_height = config_get_title_height(); + int border = client->border_width; + XSync(dwn->display, False); + XReparentWindow(dwn->display, client->window, client->frame, + border, title_height + border); + XSync(dwn->display, False); + XMapWindow(dwn->display, client->frame); + } + client_configure(client); + } +} + +void client_toggle_fullscreen(Client *client) +{ + if (client == NULL) { + return; + } + client_set_fullscreen(client, !(client->flags & CLIENT_FULLSCREEN)); +} + +void client_set_floating(Client *client, bool floating) +{ + if (client == NULL) { + return; + } + + if (floating) { + client->flags |= CLIENT_FLOATING; + } else { + client->flags &= ~CLIENT_FLOATING; + } +} + +void client_toggle_floating(Client *client) +{ + if (client == NULL) { + return; + } + client_set_floating(client, !(client->flags & CLIENT_FLOATING)); +} + +/* ========== Window type checking ========== */ + +bool client_is_floating(Client *client) +{ + return client != NULL && (client->flags & CLIENT_FLOATING); +} + +bool client_is_fullscreen(Client *client) +{ + return client != NULL && (client->flags & CLIENT_FULLSCREEN); +} + +bool client_is_minimized(Client *client) +{ + return client != NULL && (client->flags & CLIENT_MINIMIZED); +} + +bool client_is_dialog(Window window) +{ + Atom type; + if (atoms_get_window_type(window, &type)) { + return type == ewmh.NET_WM_WINDOW_TYPE_DIALOG; + } + + /* Check transient hint */ + Window transient_for = None; + if (XGetTransientForHint(dwn->display, window, &transient_for)) { + return transient_for != None; + } + + return false; +} + +bool client_is_dock(Window window) +{ + Atom type; + if (atoms_get_window_type(window, &type)) { + return type == ewmh.NET_WM_WINDOW_TYPE_DOCK; + } + return false; +} + +bool client_is_desktop(Window window) +{ + Atom type; + if (atoms_get_window_type(window, &type)) { + return type == ewmh.NET_WM_WINDOW_TYPE_DESKTOP; + } + return false; +} + +/* ========== Frame management ========== */ + +void client_create_frame(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + LOG_ERROR("client_create_frame: invalid parameters"); + return; + } + + if (!dwn->config || !dwn->config->show_decorations) { + return; + } + + /* Defensive: verify client window is valid */ + if (client->window == None) { + LOG_ERROR("client_create_frame: client has no valid window"); + return; + } + + int title_height = config_get_title_height(); + int border = client->border_width; + + /* Ensure reasonable dimensions */ + int frame_width = client->width + 2 * border; + int frame_height = client->height + title_height + 2 * border; + if (frame_width <= 0 || frame_height <= 0) { + LOG_ERROR("client_create_frame: invalid dimensions (%dx%d)", frame_width, frame_height); + return; + } + + /* Create frame window */ + XSetWindowAttributes swa; + memset(&swa, 0, sizeof(swa)); /* Defensive: zero-initialize */ + swa.override_redirect = True; + swa.background_pixel = dwn->config->colors.title_unfocused_bg; + swa.border_pixel = dwn->config->colors.border_unfocused; + swa.event_mask = SubstructureRedirectMask | SubstructureNotifyMask | + ButtonPressMask | ButtonReleaseMask | PointerMotionMask | + ExposureMask | EnterWindowMask; + + client->frame = XCreateWindow(dwn->display, dwn->root, + client->x - border, + client->y - title_height - border, + frame_width, + frame_height, + 0, + CopyFromParent, InputOutput, CopyFromParent, + CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, + &swa); + + /* Verify frame was created successfully */ + if (client->frame == None) { + LOG_ERROR("client_create_frame: XCreateWindow failed for window %lu", client->window); + return; + } + + XSetWindowBorderWidth(dwn->display, client->frame, border); + XSync(dwn->display, False); /* Catch any X errors */ + + LOG_DEBUG("Created frame %lu for window %lu", client->frame, client->window); +} + +void client_destroy_frame(Client *client) +{ + if (client == NULL || client->frame == None) { + return; + } + + XDestroyWindow(dwn->display, client->frame); + client->frame = None; +} + +void client_reparent_to_frame(Client *client) +{ + if (client == NULL || client->frame == None) { + return; + } + + if (dwn == NULL || dwn->display == NULL) { + return; + } + + /* Verify window still exists before reparenting */ + if (client->window == None) { + LOG_WARN("client_reparent_to_frame: client has no valid window"); + return; + } + + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_WARN("client_reparent_to_frame: window %lu no longer exists", client->window); + return; + } + + int title_height = config_get_title_height(); + int border = client->border_width; + + /* Sync before reparent to catch any pending errors */ + XSync(dwn->display, False); + + XReparentWindow(dwn->display, client->window, client->frame, + border, title_height + border); + + /* Sync after reparent to catch errors immediately */ + XSync(dwn->display, False); + + /* Save the frame window association */ + XSaveContext(dwn->display, client->window, XUniqueContext(), (XPointer)client); +} + +void client_reparent_from_frame(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->frame == None || client->window == None) { + return; + } + + /* Verify window still exists before reparenting */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_WARN("client_reparent_from_frame: window %lu no longer exists", client->window); + return; + } + + /* Sync before reparent to catch any pending errors */ + XSync(dwn->display, False); + + XReparentWindow(dwn->display, client->window, dwn->root, + client->x, client->y); + + /* Sync after reparent to catch errors immediately */ + XSync(dwn->display, False); +} + +/* ========== Visibility ========== */ + +void client_show(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + /* Verify window still exists before mapping */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_DEBUG("client_show: window no longer exists"); + return; + } + + if (client->frame != None) { + XMapWindow(dwn->display, client->frame); + } + XMapWindow(dwn->display, client->window); +} + +void client_hide(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + /* Verify window still exists before unmapping */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_DEBUG("client_hide: window no longer exists"); + return; + } + + if (client->frame != None) { + XUnmapWindow(dwn->display, client->frame); + } + XUnmapWindow(dwn->display, client->window); +} + +bool client_is_visible(Client *client) +{ + if (client == NULL) { + return false; + } + + /* Hidden windows are not visible */ + if (client->flags & CLIENT_MINIMIZED) { + return false; + } + + /* Only visible if on current workspace (or sticky) */ + if (!(client->flags & CLIENT_STICKY) && + client->workspace != (unsigned int)dwn->current_workspace) { + return false; + } + + return true; +} + +/* ========== Close handling ========== */ + +void client_close(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + /* Verify window still exists */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_DEBUG("client_close: window no longer exists"); + return; + } + + /* Try WM_DELETE_WINDOW first */ + if (atoms_window_supports_protocol(client->window, icccm.WM_DELETE_WINDOW)) { + atoms_send_protocol(client->window, icccm.WM_DELETE_WINDOW, CurrentTime); + } else { + /* Force kill */ + client_kill(client); + } +} + +void client_kill(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + XKillClient(dwn->display, client->window); +} + +/* ========== List operations ========== */ + +void client_add_to_list(Client *client) +{ + if (client == NULL || dwn == NULL) { + return; + } + + client->next = dwn->client_list; + client->prev = NULL; + + if (dwn->client_list != NULL) { + dwn->client_list->prev = client; + } + + dwn->client_list = client; + dwn->client_count++; +} + +void client_remove_from_list(Client *client) +{ + if (client == NULL || dwn == NULL) { + return; + } + + if (client->prev != NULL) { + client->prev->next = client->next; + } else { + dwn->client_list = client->next; + } + + if (client->next != NULL) { + client->next->prev = client->prev; + } + + dwn->client_count--; +} + +int client_count(void) +{ + return dwn != NULL ? dwn->client_count : 0; +} + +int client_count_on_workspace(int workspace) +{ + int count = 0; + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)workspace) { + count++; + } + } + return count; +} + +/* ========== Iteration ========== */ + +Client *client_get_next(Client *client) +{ + return client != NULL ? client->next : NULL; +} + +Client *client_get_prev(Client *client) +{ + return client != NULL ? client->prev : NULL; +} + +Client *client_get_first(void) +{ + return dwn != NULL ? dwn->client_list : NULL; +} + +Client *client_get_last(void) +{ + if (dwn == NULL || dwn->client_list == NULL) { + return NULL; + } + + Client *c = dwn->client_list; + while (c->next != NULL) { + c = c->next; + } + return c; +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..3efb77e --- /dev/null +++ b/src/config.c @@ -0,0 +1,361 @@ +/* + * DWN - Desktop Window Manager + * Configuration system implementation + */ + +#include "config.h" +#include "util.h" + +#include +#include +#include +#include + +/* Default color values (dark modern theme) */ +#define DEFAULT_PANEL_BG "#1a1a2e" +#define DEFAULT_PANEL_FG "#e0e0e0" +#define DEFAULT_WS_ACTIVE "#4a90d9" +#define DEFAULT_WS_INACTIVE "#3a3a4e" +#define DEFAULT_WS_URGENT "#d94a4a" +#define DEFAULT_TITLE_FOCUSED_BG "#2d3a4a" +#define DEFAULT_TITLE_FOCUSED_FG "#ffffff" +#define DEFAULT_TITLE_UNFOCUSED_BG "#1a1a1a" +#define DEFAULT_TITLE_UNFOCUSED_FG "#808080" +#define DEFAULT_BORDER_FOCUSED "#4a90d9" +#define DEFAULT_BORDER_UNFOCUSED "#333333" +#define DEFAULT_NOTIFICATION_BG "#2a2a3e" +#define DEFAULT_NOTIFICATION_FG "#ffffff" + +/* ========== Configuration creation/destruction ========== */ + +Config *config_create(void) +{ + Config *cfg = dwn_calloc(1, sizeof(Config)); + config_set_defaults(cfg); + return cfg; +} + +void config_destroy(Config *cfg) +{ + if (cfg != NULL) { + /* Securely wipe API keys before freeing */ + secure_wipe(cfg->openrouter_api_key, sizeof(cfg->openrouter_api_key)); + secure_wipe(cfg->exa_api_key, sizeof(cfg->exa_api_key)); + dwn_free(cfg); + } +} + +void config_set_defaults(Config *cfg) +{ + if (cfg == NULL) { + return; + } + + /* General */ + strncpy(cfg->terminal, "xfce4-terminal", sizeof(cfg->terminal) - 1); + strncpy(cfg->launcher, "dmenu_run", sizeof(cfg->launcher) - 1); + strncpy(cfg->file_manager, "thunar", sizeof(cfg->file_manager) - 1); + cfg->focus_mode = FOCUS_CLICK; + cfg->show_decorations = true; + + /* Appearance */ + cfg->border_width = DEFAULT_BORDER_WIDTH; + cfg->title_height = DEFAULT_TITLE_HEIGHT; + cfg->panel_height = DEFAULT_PANEL_HEIGHT; + cfg->gap = DEFAULT_GAP; + strncpy(cfg->font_name, "fixed", sizeof(cfg->font_name) - 1); + + /* Layout */ + cfg->default_master_ratio = 0.55f; + cfg->default_master_count = 1; + cfg->default_layout = LAYOUT_TILING; + + /* Panels */ + cfg->top_panel_enabled = true; + cfg->bottom_panel_enabled = true; + + /* AI (disabled by default, enabled if API key found) */ + cfg->openrouter_api_key[0] = '\0'; + cfg->exa_api_key[0] = '\0'; + strncpy(cfg->ai_model, "google/gemini-2.0-flash-exp:free", sizeof(cfg->ai_model) - 1); + cfg->ai_enabled = false; + + /* Paths */ + strncpy(cfg->config_path, "~/.config/dwn/config", sizeof(cfg->config_path) - 1); + strncpy(cfg->log_path, "~/.local/share/dwn/dwn.log", sizeof(cfg->log_path) - 1); +} + +/* ========== INI Parser ========== */ + +typedef struct { + Config *cfg; + char current_section[64]; +} ParseContext; + +static void handle_config_entry(const char *section, const char *key, + const char *value, void *user_data) +{ + ParseContext *ctx = (ParseContext *)user_data; + Config *cfg = ctx->cfg; + + if (strcmp(section, "general") == 0) { + if (strcmp(key, "terminal") == 0) { + strncpy(cfg->terminal, value, sizeof(cfg->terminal) - 1); + } else if (strcmp(key, "launcher") == 0) { + strncpy(cfg->launcher, value, sizeof(cfg->launcher) - 1); + } else if (strcmp(key, "file_manager") == 0) { + strncpy(cfg->file_manager, value, sizeof(cfg->file_manager) - 1); + } else if (strcmp(key, "focus_mode") == 0) { + if (strcmp(value, "follow") == 0) { + cfg->focus_mode = FOCUS_FOLLOW; + } else { + cfg->focus_mode = FOCUS_CLICK; + } + } else if (strcmp(key, "decorations") == 0) { + cfg->show_decorations = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); + } + } else if (strcmp(section, "appearance") == 0) { + if (strcmp(key, "border_width") == 0) { + int val = atoi(value); + cfg->border_width = (val >= 0 && val <= 50) ? val : DEFAULT_BORDER_WIDTH; + } else if (strcmp(key, "title_height") == 0) { + int val = atoi(value); + cfg->title_height = (val >= 0 && val <= 100) ? val : DEFAULT_TITLE_HEIGHT; + } else if (strcmp(key, "panel_height") == 0) { + int val = atoi(value); + cfg->panel_height = (val >= 0 && val <= 100) ? val : DEFAULT_PANEL_HEIGHT; + } else if (strcmp(key, "gap") == 0) { + int val = atoi(value); + cfg->gap = (val >= 0 && val <= 100) ? val : DEFAULT_GAP; + } else if (strcmp(key, "font") == 0) { + strncpy(cfg->font_name, value, sizeof(cfg->font_name) - 1); + } + } else if (strcmp(section, "layout") == 0) { + if (strcmp(key, "master_ratio") == 0) { + float val = atof(value); + cfg->default_master_ratio = (val >= 0.1f && val <= 0.9f) ? val : 0.55f; + } else if (strcmp(key, "master_count") == 0) { + int val = atoi(value); + cfg->default_master_count = (val >= 1 && val <= 10) ? val : 1; + } else if (strcmp(key, "default") == 0) { + if (strcmp(value, "floating") == 0) { + cfg->default_layout = LAYOUT_FLOATING; + } else if (strcmp(value, "monocle") == 0) { + cfg->default_layout = LAYOUT_MONOCLE; + } else { + cfg->default_layout = LAYOUT_TILING; + } + } + } else if (strcmp(section, "panels") == 0) { + if (strcmp(key, "top") == 0) { + cfg->top_panel_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); + } else if (strcmp(key, "bottom") == 0) { + cfg->bottom_panel_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); + } + } else if (strcmp(section, "ai") == 0) { + if (strcmp(key, "model") == 0) { + strncpy(cfg->ai_model, value, sizeof(cfg->ai_model) - 1); + } else if (strcmp(key, "openrouter_api_key") == 0) { + strncpy(cfg->openrouter_api_key, value, sizeof(cfg->openrouter_api_key) - 1); + cfg->ai_enabled = true; + } else if (strcmp(key, "exa_api_key") == 0) { + strncpy(cfg->exa_api_key, value, sizeof(cfg->exa_api_key) - 1); + } + } +} + +bool config_parse_ini(const char *path, ConfigCallback callback, void *user_data) +{ + char *expanded = expand_path(path); + FILE *f = fopen(expanded, "r"); + dwn_free(expanded); + + if (f == NULL) { + return false; + } + + char line[1024]; + char section[64] = ""; + + while (fgets(line, sizeof(line), f) != NULL) { + char *trimmed = str_trim(line); + + /* Skip empty lines and comments */ + if (trimmed[0] == '\0' || trimmed[0] == '#' || trimmed[0] == ';') { + continue; + } + + /* Section header */ + if (trimmed[0] == '[') { + char *end = strchr(trimmed, ']'); + if (end != NULL) { + *end = '\0'; + strncpy(section, trimmed + 1, sizeof(section) - 1); + } + continue; + } + + /* Key = value */ + char *equals = strchr(trimmed, '='); + if (equals != NULL) { + *equals = '\0'; + char *key = str_trim(trimmed); + char *value = str_trim(equals + 1); + + /* Remove quotes from value */ + size_t vlen = strlen(value); + if (vlen >= 2 && ((value[0] == '"' && value[vlen-1] == '"') || + (value[0] == '\'' && value[vlen-1] == '\''))) { + value[vlen-1] = '\0'; + value++; + } + + callback(section, key, value, user_data); + } + } + + fclose(f); + return true; +} + +/* ========== Load configuration ========== */ + +bool config_load(Config *cfg, const char *path) +{ + if (cfg == NULL) { + return false; + } + + /* Set defaults first */ + config_set_defaults(cfg); + + /* Determine config path */ + const char *config_path = path; + if (config_path == NULL) { + config_path = cfg->config_path; + } + + /* Parse config file first (environment variables override below) */ + ParseContext ctx = { .cfg = cfg }; + char *expanded = expand_path(config_path); + + if (file_exists(expanded)) { + LOG_INFO("Loading configuration from %s", expanded); + config_parse_ini(expanded, handle_config_entry, &ctx); + } else { + LOG_INFO("No config file found at %s, using defaults", expanded); + } + + dwn_free(expanded); + + /* Check for API keys in environment (override config file) */ + const char *openrouter_key = getenv("OPENROUTER_API_KEY"); + if (openrouter_key != NULL && openrouter_key[0] != '\0') { + strncpy(cfg->openrouter_api_key, openrouter_key, sizeof(cfg->openrouter_api_key) - 1); + cfg->ai_enabled = true; + LOG_INFO("OpenRouter API key found in environment"); + } + + const char *exa_key = getenv("EXA_API_KEY"); + if (exa_key != NULL && exa_key[0] != '\0') { + strncpy(cfg->exa_api_key, exa_key, sizeof(cfg->exa_api_key) - 1); + } + + /* Log AI status */ + if (cfg->ai_enabled) { + LOG_INFO("AI features enabled"); + } + + return true; +} + +bool config_reload(Config *cfg) +{ + if (cfg == NULL) { + return false; + } + /* Securely wipe existing API keys before reloading */ + secure_wipe(cfg->openrouter_api_key, sizeof(cfg->openrouter_api_key)); + secure_wipe(cfg->exa_api_key, sizeof(cfg->exa_api_key)); + return config_load(cfg, cfg->config_path); +} + +/* ========== Getters ========== */ + +const char *config_get_terminal(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->terminal; + } + return "xterm"; +} + +const char *config_get_launcher(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->launcher; + } + return "dmenu_run"; +} + +int config_get_border_width(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->border_width; + } + return DEFAULT_BORDER_WIDTH; +} + +int config_get_title_height(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->title_height; + } + return DEFAULT_TITLE_HEIGHT; +} + +int config_get_panel_height(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->panel_height; + } + return DEFAULT_PANEL_HEIGHT; +} + +int config_get_gap(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->gap; + } + return DEFAULT_GAP; +} + +const ColorScheme *config_get_colors(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return &dwn->config->colors; + } + return NULL; +} + +/* Initialize colors (must be called after display is open) */ +void config_init_colors(Config *cfg) +{ + if (cfg == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + cfg->colors.panel_bg = parse_color(DEFAULT_PANEL_BG); + cfg->colors.panel_fg = parse_color(DEFAULT_PANEL_FG); + cfg->colors.workspace_active = parse_color(DEFAULT_WS_ACTIVE); + cfg->colors.workspace_inactive = parse_color(DEFAULT_WS_INACTIVE); + cfg->colors.workspace_urgent = parse_color(DEFAULT_WS_URGENT); + cfg->colors.title_focused_bg = parse_color(DEFAULT_TITLE_FOCUSED_BG); + cfg->colors.title_focused_fg = parse_color(DEFAULT_TITLE_FOCUSED_FG); + cfg->colors.title_unfocused_bg = parse_color(DEFAULT_TITLE_UNFOCUSED_BG); + cfg->colors.title_unfocused_fg = parse_color(DEFAULT_TITLE_UNFOCUSED_FG); + cfg->colors.border_focused = parse_color(DEFAULT_BORDER_FOCUSED); + cfg->colors.border_unfocused = parse_color(DEFAULT_BORDER_UNFOCUSED); + cfg->colors.notification_bg = parse_color(DEFAULT_NOTIFICATION_BG); + cfg->colors.notification_fg = parse_color(DEFAULT_NOTIFICATION_FG); +} diff --git a/src/decorations.c b/src/decorations.c new file mode 100644 index 0000000..c769912 --- /dev/null +++ b/src/decorations.c @@ -0,0 +1,368 @@ +/* + * DWN - Desktop Window Manager + * Window decorations implementation + */ + +#include "decorations.h" +#include "client.h" +#include "config.h" +#include "util.h" + +#include +#include +#include + +/* Button dimensions */ +#define BUTTON_SIZE 16 +#define BUTTON_PADDING 4 + +/* Resize edge size */ +#define RESIZE_EDGE 8 + +/* ========== Initialization ========== */ + +void decorations_init(void) +{ + LOG_DEBUG("Decorations system initialized"); +} + +void decorations_cleanup(void) +{ + /* Nothing to clean up */ +} + +/* ========== Rendering ========== */ + +void decorations_render(Client *client, bool focused) +{ + if (client == NULL || client->frame == None) { + return; + } + + if (dwn == NULL || dwn->display == NULL || dwn->config == NULL) { + return; + } + + decorations_render_title_bar(client, focused); + decorations_render_buttons(client, focused); + decorations_render_border(client, focused); +} + +void decorations_render_title_bar(Client *client, bool focused) +{ + if (client == NULL || client->frame == None) { + return; + } + + if (dwn == NULL || dwn->display == NULL || dwn->gc == None || dwn->config == NULL) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + int title_height = config_get_title_height(); + int border = client->border_width; + + /* Set colors based on focus */ + unsigned long bg_color = focused ? colors->title_focused_bg : colors->title_unfocused_bg; + unsigned long fg_color = focused ? colors->title_focused_fg : colors->title_unfocused_fg; + + /* Draw title bar background */ + XSetForeground(dpy, dwn->gc, bg_color); + XFillRectangle(dpy, client->frame, dwn->gc, + border, border, + client->width, title_height); + + /* Draw title text */ + if (client->title[0] != '\0' && dwn->xft_font != NULL) { + int text_y = border + (title_height + dwn->xft_font->ascent) / 2; + int text_x = border + BUTTON_PADDING; + + /* Truncate title if too long */ + int max_width = client->width - 3 * (BUTTON_SIZE + BUTTON_PADDING) - 2 * BUTTON_PADDING; + char display_title[256]; + strncpy(display_title, client->title, sizeof(display_title) - 4); /* Leave room for "..." */ + display_title[sizeof(display_title) - 4] = '\0'; + + XGlyphInfo extents; + XftTextExtentsUtf8(dpy, dwn->xft_font, + (const FcChar8 *)display_title, strlen(display_title), &extents); + int text_width = extents.xOff; + + /* Truncate UTF-8 aware: find valid UTF-8 boundary */ + bool title_truncated = false; + while (text_width > max_width && strlen(display_title) > 3) { + size_t len = strlen(display_title); + /* Move back to find UTF-8 character boundary */ + size_t cut = len - 1; + while (cut > 0 && (display_title[cut] & 0xC0) == 0x80) { + cut--; /* Skip continuation bytes */ + } + if (cut > 0) cut--; /* Remove one more character for ellipsis space */ + while (cut > 0 && (display_title[cut] & 0xC0) == 0x80) { + cut--; + } + display_title[cut] = '\0'; + title_truncated = true; + + XftTextExtentsUtf8(dpy, dwn->xft_font, + (const FcChar8 *)display_title, strlen(display_title), &extents); + text_width = extents.xOff; + } + if (title_truncated) { + strncat(display_title, "...", sizeof(display_title) - strlen(display_title) - 1); + } + + /* Draw with Xft for UTF-8 support */ + XftDraw *xft_draw = XftDrawCreate(dpy, client->frame, + DefaultVisual(dpy, dwn->screen), + dwn->colormap); + if (xft_draw != NULL) { + XftColor xft_color; + XRenderColor render_color; + render_color.red = ((fg_color >> 16) & 0xFF) * 257; + render_color.green = ((fg_color >> 8) & 0xFF) * 257; + render_color.blue = (fg_color & 0xFF) * 257; + render_color.alpha = 0xFFFF; + + XftColorAllocValue(dpy, DefaultVisual(dpy, dwn->screen), + dwn->colormap, &render_color, &xft_color); + + XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, + text_x, text_y, + (const FcChar8 *)display_title, strlen(display_title)); + + XftColorFree(dpy, DefaultVisual(dpy, dwn->screen), + dwn->colormap, &xft_color); + XftDrawDestroy(xft_draw); + } + } +} + +void decorations_render_buttons(Client *client, bool focused) +{ + if (client == NULL || client->frame == None) { + return; + } + + if (dwn == NULL || dwn->display == NULL || dwn->gc == None || dwn->config == NULL) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + int title_height = config_get_title_height(); + int border = client->border_width; + + /* Button positions (right-aligned) */ + int button_y = border + (title_height - BUTTON_SIZE) / 2; + int close_x = border + client->width - BUTTON_SIZE - BUTTON_PADDING; + int max_x = close_x - BUTTON_SIZE - BUTTON_PADDING; + int min_x = max_x - BUTTON_SIZE - BUTTON_PADDING; + + unsigned long bg_color = focused ? colors->title_focused_bg : colors->title_unfocused_bg; + + /* Close button (red X) */ + XSetForeground(dpy, dwn->gc, bg_color); + XFillRectangle(dpy, client->frame, dwn->gc, close_x, button_y, BUTTON_SIZE, BUTTON_SIZE); + XSetForeground(dpy, dwn->gc, 0xcc4444); /* Red */ + XDrawLine(dpy, client->frame, dwn->gc, + close_x + 3, button_y + 3, + close_x + BUTTON_SIZE - 4, button_y + BUTTON_SIZE - 4); + XDrawLine(dpy, client->frame, dwn->gc, + close_x + BUTTON_SIZE - 4, button_y + 3, + close_x + 3, button_y + BUTTON_SIZE - 4); + + /* Maximize button (square) */ + XSetForeground(dpy, dwn->gc, bg_color); + XFillRectangle(dpy, client->frame, dwn->gc, max_x, button_y, BUTTON_SIZE, BUTTON_SIZE); + XSetForeground(dpy, dwn->gc, 0x44cc44); /* Green */ + XDrawRectangle(dpy, client->frame, dwn->gc, + max_x + 3, button_y + 3, + BUTTON_SIZE - 7, BUTTON_SIZE - 7); + + /* Minimize button (line) */ + XSetForeground(dpy, dwn->gc, bg_color); + XFillRectangle(dpy, client->frame, dwn->gc, min_x, button_y, BUTTON_SIZE, BUTTON_SIZE); + XSetForeground(dpy, dwn->gc, 0xcccc44); /* Yellow */ + XDrawLine(dpy, client->frame, dwn->gc, + min_x + 3, button_y + BUTTON_SIZE - 5, + min_x + BUTTON_SIZE - 4, button_y + BUTTON_SIZE - 5); +} + +void decorations_render_border(Client *client, bool focused) +{ + if (client == NULL || client->frame == None) { + return; + } + + if (dwn == NULL || dwn->display == NULL || dwn->config == NULL) { + return; + } + + const ColorScheme *colors = config_get_colors(); + unsigned long border_color = focused ? colors->border_focused : colors->border_unfocused; + + XSetWindowBorder(dwn->display, client->frame, border_color); +} + +/* ========== Hit testing ========== */ + +ButtonType decorations_hit_test_button(Client *client, int x, int y) +{ + if (client == NULL) { + return BUTTON_COUNT; /* No button hit */ + } + + int title_height = config_get_title_height(); + int border = client->border_width; + + /* Check if in title bar area */ + if (y < border || y > border + title_height) { + return BUTTON_COUNT; + } + + int button_y = border + (title_height - BUTTON_SIZE) / 2; + int close_x = border + client->width - BUTTON_SIZE - BUTTON_PADDING; + int max_x = close_x - BUTTON_SIZE - BUTTON_PADDING; + int min_x = max_x - BUTTON_SIZE - BUTTON_PADDING; + + /* Check close button */ + if (x >= close_x && x < close_x + BUTTON_SIZE && + y >= button_y && y < button_y + BUTTON_SIZE) { + return BUTTON_CLOSE; + } + + /* Check maximize button */ + if (x >= max_x && x < max_x + BUTTON_SIZE && + y >= button_y && y < button_y + BUTTON_SIZE) { + return BUTTON_MAXIMIZE; + } + + /* Check minimize button */ + if (x >= min_x && x < min_x + BUTTON_SIZE && + y >= button_y && y < button_y + BUTTON_SIZE) { + return BUTTON_MINIMIZE; + } + + return BUTTON_COUNT; +} + +bool decorations_hit_test_title_bar(Client *client, int x, int y) +{ + if (client == NULL) { + return false; + } + + int title_height = config_get_title_height(); + int border = client->border_width; + + /* In title bar but not on a button */ + if (y >= border && y < border + title_height) { + ButtonType btn = decorations_hit_test_button(client, x, y); + return btn == BUTTON_COUNT; + } + + return false; +} + +bool decorations_hit_test_resize_area(Client *client, int x, int y, int *direction) +{ + if (client == NULL || direction == NULL) { + return false; + } + + int title_height = config_get_title_height(); + int border = client->border_width; + int frame_width = client->width + 2 * border; + int frame_height = client->height + title_height + 2 * border; + + *direction = 0; + + /* Check edges */ + bool left = (x < RESIZE_EDGE); + bool right = (x > frame_width - RESIZE_EDGE); + bool top = (y < RESIZE_EDGE); + bool bottom = (y > frame_height - RESIZE_EDGE); + + if (!left && !right && !top && !bottom) { + return false; + } + + /* Encode direction as bitmask */ + if (left) *direction |= 1; + if (right) *direction |= 2; + if (top) *direction |= 4; + if (bottom) *direction |= 8; + + return true; +} + +/* ========== Button actions ========== */ + +void decorations_button_press(Client *client, ButtonType button) +{ + if (client == NULL) { + return; + } + + switch (button) { + case BUTTON_CLOSE: + LOG_DEBUG("Close button pressed for %s", client->title); + client_close(client); + break; + + case BUTTON_MAXIMIZE: + LOG_DEBUG("Maximize button pressed for %s", client->title); + client_toggle_fullscreen(client); + break; + + case BUTTON_MINIMIZE: + LOG_DEBUG("Minimize button pressed for %s", client->title); + client_minimize(client); + break; + + default: + break; + } +} + +/* ========== Text rendering ========== */ + +void decorations_draw_text(Window window, GC gc, int x, int y, + const char *text, unsigned long color) +{ + if (text == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + /* Use Xft for UTF-8 support */ + if (dwn->xft_font != NULL) { + XftDraw *xft_draw = XftDrawCreate(dwn->display, window, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap); + if (xft_draw != NULL) { + XftColor xft_color; + XRenderColor render_color; + render_color.red = ((color >> 16) & 0xFF) * 257; + render_color.green = ((color >> 8) & 0xFF) * 257; + render_color.blue = (color & 0xFF) * 257; + render_color.alpha = 0xFFFF; + + XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &render_color, &xft_color); + + XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, + x, y, (const FcChar8 *)text, strlen(text)); + + XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &xft_color); + XftDrawDestroy(xft_draw); + return; + } + } + + /* Fallback to legacy X11 text */ + XSetForeground(dwn->display, gc, color); + XDrawString(dwn->display, window, gc, x, y, text, strlen(text)); +} diff --git a/src/keys.c b/src/keys.c new file mode 100644 index 0000000..3167aee --- /dev/null +++ b/src/keys.c @@ -0,0 +1,842 @@ +/* + * DWN - Desktop Window Manager + * Keyboard shortcut handling implementation + */ + +#include "keys.h" +#include "client.h" +#include "workspace.h" +#include "config.h" +#include "util.h" +#include "ai.h" +#include "notifications.h" +#include "news.h" +#include "applauncher.h" + +#include +#include + +/* Forward declarations for key callbacks */ +void key_spawn_terminal(void); +void key_spawn_launcher(void); +void key_spawn_file_manager(void); +void key_spawn_browser(void); +void key_close_window(void); +void key_quit_dwn(void); +void key_cycle_layout(void); +void key_toggle_floating(void); +void key_toggle_fullscreen(void); +void key_toggle_maximize(void); +void key_focus_next(void); +void key_focus_prev(void); +void key_workspace_next(void); +void key_workspace_prev(void); +void key_workspace_1(void); +void key_workspace_2(void); +void key_workspace_3(void); +void key_workspace_4(void); +void key_workspace_5(void); +void key_workspace_6(void); +void key_workspace_7(void); +void key_workspace_8(void); +void key_workspace_9(void); +void key_move_to_workspace_1(void); +void key_move_to_workspace_2(void); +void key_move_to_workspace_3(void); +void key_move_to_workspace_4(void); +void key_move_to_workspace_5(void); +void key_move_to_workspace_6(void); +void key_move_to_workspace_7(void); +void key_move_to_workspace_8(void); +void key_move_to_workspace_9(void); +void key_increase_master(void); +void key_decrease_master(void); +void key_increase_master_count(void); +void key_decrease_master_count(void); +void key_toggle_ai(void); +void key_ai_command(void); +void key_exa_search(void); +void key_news_next(void); +void key_news_prev(void); +void key_news_open(void); +void key_show_shortcuts(void); +void key_start_tutorial(void); +void key_screenshot(void); + +/* Key bindings storage */ +static KeyBinding bindings[MAX_KEYBINDINGS]; +static int binding_count = 0; + +/* ========== Tutorial System ========== */ + +typedef struct { + const char *title; + const char *instruction; + const char *hint; + unsigned int modifiers; + KeySym keysym; +} TutorialStep; + +static const TutorialStep tutorial_steps[] = { + { + "Welcome to DWN!", + "This tutorial teaches all keyboard shortcuts.\n\n" + "Press Super+T to continue...", + "(Super = Windows/Meta key)", + MOD_SUPER, XK_t + }, + /* === Applications === */ + { + "1/20: Open Terminal", + "The terminal is your command center.\n\n" + "Press: Ctrl + Alt + T", + "Hold Ctrl and Alt, then press T", + MOD_CTRL | MOD_ALT, XK_t + }, + { + "2/20: App Launcher", + "Launch any application by name.\n\n" + "Press: Alt + F2", + "Type app name and press Enter", + MOD_ALT, XK_F2 + }, + { + "3/20: File Manager", + "Open the file manager.\n\n" + "Press: Super + E", + "", + MOD_SUPER, XK_e + }, + { + "4/20: Web Browser", + "Open your default web browser.\n\n" + "Press: Super + B", + "", + MOD_SUPER, XK_b + }, + /* === Window Management === */ + { + "5/20: Switch Windows", + "Cycle through open windows.\n\n" + "Press: Alt + Tab", + "Hold Alt, press Tab repeatedly", + MOD_ALT, XK_Tab + }, + { + "6/20: Close Window", + "Close the focused window.\n\n" + "Press: Alt + F4", + "", + MOD_ALT, XK_F4 + }, + { + "7/20: Toggle Maximize", + "Maximize or restore a window.\n\n" + "Press: Alt + F10", + "", + MOD_ALT, XK_F10 + }, + { + "8/20: Toggle Fullscreen", + "Make a window fullscreen.\n\n" + "Press: Alt + F11", + "Press again to exit", + MOD_ALT, XK_F11 + }, + { + "9/20: Toggle Floating", + "Make window float above tiled windows.\n\n" + "Press: Super + F9", + "", + MOD_SUPER, XK_F9 + }, + /* === Workspaces === */ + { + "10/20: Next Workspace", + "Switch to the next virtual desktop.\n\n" + "Press: Ctrl + Alt + Right", + "", + MOD_CTRL | MOD_ALT, XK_Right + }, + { + "11/20: Previous Workspace", + "Switch to the previous workspace.\n\n" + "Press: Ctrl + Alt + Left", + "", + MOD_CTRL | MOD_ALT, XK_Left + }, + { + "12/20: Go to Workspace", + "Jump directly to workspace 1.\n\n" + "Press: F1", + "Use F1-F9 for workspaces 1-9", + 0, XK_F1 + }, + /* === Layout === */ + { + "13/20: Cycle Layout", + "Switch between tiling, floating, monocle.\n\n" + "Press: Super + Space", + "Try it multiple times!", + MOD_SUPER, XK_space + }, + { + "14/20: Expand Master", + "Make the master area larger.\n\n" + "Press: Super + L", + "", + MOD_SUPER, XK_l + }, + { + "15/20: Shrink Master", + "Make the master area smaller.\n\n" + "Press: Super + H", + "", + MOD_SUPER, XK_h + }, + { + "16/20: Add to Master", + "Increase windows in master area.\n\n" + "Press: Super + I", + "", + MOD_SUPER, XK_i + }, + /* === AI Features === */ + { + "17/20: AI Context", + "Show AI analysis of your current task.\n\n" + "Press: Super + A", + "Requires OPENROUTER_API_KEY", + MOD_SUPER, XK_a + }, + { + "18/20: AI Command", + "Ask AI to run commands for you.\n\n" + "Press: Super + Shift + A", + "Requires OPENROUTER_API_KEY", + MOD_SUPER | MOD_SHIFT, XK_a + }, + { + "19/20: Exa Search", + "Semantic web search.\n\n" + "Press: Super + Shift + E", + "Requires EXA_API_KEY", + MOD_SUPER | MOD_SHIFT, XK_e + }, + /* === Help & System === */ + { + "20/20: Show Shortcuts", + "Display all keyboard shortcuts.\n\n" + "Press: Super + S", + "Shows complete reference", + MOD_SUPER, XK_s + }, + { + "Tutorial Complete!", + "You've learned all DWN shortcuts!\n\n" + "Super+S: Show all shortcuts\n" + "Super+T: Restart this tutorial\n\n" + "Press: Super + Backspace to quit DWN", + "Congratulations!", + MOD_SUPER, XK_BackSpace + } +}; + +#define TUTORIAL_STEPS (sizeof(tutorial_steps) / sizeof(tutorial_steps[0])) + +static bool tutorial_active = false; +static int tutorial_current_step = 0; + +bool tutorial_is_active(void) +{ + return tutorial_active; +} + +void tutorial_start(void) +{ + tutorial_active = true; + tutorial_current_step = 0; + + const TutorialStep *step = &tutorial_steps[0]; + char msg[512]; + snprintf(msg, sizeof(msg), "%s\n\n%s", step->instruction, step->hint); + notification_show("DWN Tutorial", step->title, msg, NULL, 0); /* No timeout */ +} + +void tutorial_stop(void) +{ + tutorial_active = false; + tutorial_current_step = 0; + notification_show("DWN Tutorial", "Tutorial Stopped", + "Press Super+T to restart anytime.", NULL, 3000); +} + +void tutorial_next_step(void) +{ + tutorial_current_step++; + + if (tutorial_current_step >= (int)TUTORIAL_STEPS) { + tutorial_active = false; + tutorial_current_step = 0; + return; + } + + const TutorialStep *step = &tutorial_steps[tutorial_current_step]; + char msg[512]; + snprintf(msg, sizeof(msg), "%s\n\n%s", step->instruction, step->hint); + notification_show("DWN Tutorial", step->title, msg, NULL, 0); +} + +void tutorial_check_key(unsigned int modifiers, KeySym keysym) +{ + if (!tutorial_active) { + return; + } + + const TutorialStep *step = &tutorial_steps[tutorial_current_step]; + + /* Check if the pressed key matches the expected key */ + if (modifiers == step->modifiers && keysym == step->keysym) { + /* Correct key! Move to next step */ + tutorial_next_step(); + } +} + +/* ========== Initialization ========== */ + +void keys_init(void) +{ + binding_count = 0; + memset(bindings, 0, sizeof(bindings)); + + keys_setup_defaults(); + keys_grab_all(); + + LOG_INFO("Keyboard shortcuts initialized (%d bindings)", binding_count); +} + +void keys_cleanup(void) +{ + keys_ungrab_all(); + keys_clear_all(); +} + +void keys_grab_all(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + Display *dpy = dwn->display; + Window root = dwn->root; + + /* Ungrab first to avoid errors */ + XUngrabKey(dpy, AnyKey, AnyModifier, root); + + /* Grab all registered bindings */ + for (int i = 0; i < binding_count; i++) { + KeyCode code = XKeysymToKeycode(dpy, bindings[i].keysym); + if (code == 0) { + LOG_WARN("Could not get keycode for keysym %lu", bindings[i].keysym); + continue; + } + + /* Grab with and without NumLock/CapsLock */ + unsigned int modifiers[] = { + bindings[i].modifiers, + bindings[i].modifiers | Mod2Mask, /* NumLock */ + bindings[i].modifiers | LockMask, /* CapsLock */ + bindings[i].modifiers | Mod2Mask | LockMask + }; + + for (size_t j = 0; j < sizeof(modifiers) / sizeof(modifiers[0]); j++) { + XGrabKey(dpy, code, modifiers[j], root, True, + GrabModeAsync, GrabModeAsync); + } + } +} + +void keys_ungrab_all(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + XUngrabKey(dwn->display, AnyKey, AnyModifier, dwn->root); +} + +/* ========== Key binding registration ========== */ + +void keys_bind(unsigned int modifiers, KeySym keysym, + KeyCallback callback, const char *description) +{ + if (binding_count >= MAX_KEYBINDINGS) { + LOG_WARN("Maximum key bindings reached"); + return; + } + + bindings[binding_count].modifiers = modifiers; + bindings[binding_count].keysym = keysym; + bindings[binding_count].callback = callback; + bindings[binding_count].description = description; + binding_count++; +} + +void keys_unbind(unsigned int modifiers, KeySym keysym) +{ + for (int i = 0; i < binding_count; i++) { + if (bindings[i].modifiers == modifiers && + bindings[i].keysym == keysym) { + /* Shift remaining bindings */ + memmove(&bindings[i], &bindings[i + 1], + (binding_count - i - 1) * sizeof(KeyBinding)); + binding_count--; + return; + } + } +} + +void keys_clear_all(void) +{ + binding_count = 0; + memset(bindings, 0, sizeof(bindings)); +} + +/* ========== Key event handling ========== */ + +void keys_handle_press(XKeyEvent *ev) +{ + if (ev == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + KeySym keysym = XLookupKeysym(ev, 0); + + /* Clean modifiers (remove NumLock, CapsLock) */ + unsigned int clean_mask = ev->state & ~(Mod2Mask | LockMask); + + /* Check tutorial progress */ + if (tutorial_is_active()) { + tutorial_check_key(clean_mask, keysym); + } + + for (int i = 0; i < binding_count; i++) { + if (bindings[i].keysym == keysym && + bindings[i].modifiers == clean_mask) { + if (bindings[i].callback != NULL) { + LOG_DEBUG("Key binding triggered: %s", bindings[i].description); + bindings[i].callback(); + } + return; + } + } +} + +void keys_handle_release(XKeyEvent *ev) +{ + /* Currently no release handling needed */ + (void)ev; +} + +/* ========== Default key bindings (XFCE-style) ========== */ + +void keys_setup_defaults(void) +{ + /* ===== XFCE-style shortcuts ===== */ + + /* Terminal: Ctrl+Alt+T (XFCE default) */ + keys_bind(MOD_CTRL | MOD_ALT, XK_t, key_spawn_terminal, "Spawn terminal"); + + /* Application finder: Alt+F2 (XFCE default) */ + keys_bind(MOD_ALT, XK_F2, key_spawn_launcher, "Application finder"); + + /* File manager: Super+E (XFCE default) */ + keys_bind(MOD_SUPER, XK_e, key_spawn_file_manager, "File manager"); + + /* Browser: Super+B */ + keys_bind(MOD_SUPER, XK_b, key_spawn_browser, "Web browser"); + + /* Close window: Alt+F4 (XFCE default) */ + keys_bind(MOD_ALT, XK_F4, key_close_window, "Close window"); + + /* Maximize toggle: Alt+F10 (XFCE default) */ + keys_bind(MOD_ALT, XK_F10, key_toggle_maximize, "Toggle maximize"); + + /* Fullscreen: Alt+F11 (XFCE default) */ + keys_bind(MOD_ALT, XK_F11, key_toggle_fullscreen, "Toggle fullscreen"); + + /* Cycle windows: Alt+Tab (XFCE default) */ + keys_bind(MOD_ALT, XK_Tab, key_focus_next, "Cycle windows"); + keys_bind(MOD_ALT | MOD_SHIFT, XK_Tab, key_focus_prev, "Cycle windows reverse"); + + /* Toggle floating: Super+F9 */ + keys_bind(MOD_SUPER, XK_F9, key_toggle_floating, "Toggle floating"); + + /* Next/Previous workspace: Ctrl+Alt+Right/Left (XFCE default) */ + keys_bind(MOD_CTRL | MOD_ALT, XK_Right, key_workspace_next, "Next workspace"); + keys_bind(MOD_CTRL | MOD_ALT, XK_Left, key_workspace_prev, "Previous workspace"); + + /* Switch to workspace: F1-F9 */ + keys_bind(0, XK_F1, key_workspace_1, "Switch to workspace 1"); + keys_bind(0, XK_F2, key_workspace_2, "Switch to workspace 2"); + keys_bind(0, XK_F3, key_workspace_3, "Switch to workspace 3"); + keys_bind(0, XK_F4, key_workspace_4, "Switch to workspace 4"); + keys_bind(0, XK_F5, key_workspace_5, "Switch to workspace 5"); + keys_bind(0, XK_F6, key_workspace_6, "Switch to workspace 6"); + keys_bind(0, XK_F7, key_workspace_7, "Switch to workspace 7"); + keys_bind(0, XK_F8, key_workspace_8, "Switch to workspace 8"); + keys_bind(0, XK_F9, key_workspace_9, "Switch to workspace 9"); + + /* Move to workspace: Shift+F1-F9 */ + keys_bind(MOD_SHIFT, XK_F1, key_move_to_workspace_1, "Move to workspace 1"); + keys_bind(MOD_SHIFT, XK_F2, key_move_to_workspace_2, "Move to workspace 2"); + keys_bind(MOD_SHIFT, XK_F3, key_move_to_workspace_3, "Move to workspace 3"); + keys_bind(MOD_SHIFT, XK_F4, key_move_to_workspace_4, "Move to workspace 4"); + keys_bind(MOD_SHIFT, XK_F5, key_move_to_workspace_5, "Move to workspace 5"); + keys_bind(MOD_SHIFT, XK_F6, key_move_to_workspace_6, "Move to workspace 6"); + keys_bind(MOD_SHIFT, XK_F7, key_move_to_workspace_7, "Move to workspace 7"); + keys_bind(MOD_SHIFT, XK_F8, key_move_to_workspace_8, "Move to workspace 8"); + keys_bind(MOD_SHIFT, XK_F9, key_move_to_workspace_9, "Move to workspace 9"); + + /* ===== DWN-specific shortcuts (all use Super key) ===== */ + + /* Quit DWN: Super+BackSpace */ + keys_bind(MOD_SUPER, XK_BackSpace, key_quit_dwn, "Quit DWN"); + + /* Cycle layout mode: Super+Space */ + keys_bind(MOD_SUPER, XK_space, key_cycle_layout, "Cycle layout"); + + /* Master area adjustments: Super+H/L/I/D */ + keys_bind(MOD_SUPER, XK_l, key_increase_master, "Increase master ratio"); + keys_bind(MOD_SUPER, XK_h, key_decrease_master, "Decrease master ratio"); + keys_bind(MOD_SUPER, XK_i, key_increase_master_count, "Increase master count"); + keys_bind(MOD_SUPER, XK_d, key_decrease_master_count, "Decrease master count"); + + /* AI: Super+A */ + keys_bind(MOD_SUPER, XK_a, key_toggle_ai, "Toggle AI context"); + keys_bind(MOD_SUPER | MOD_SHIFT, XK_a, key_ai_command, "AI command"); + + /* Exa semantic search: Super+Shift+E */ + keys_bind(MOD_SUPER | MOD_SHIFT, XK_e, key_exa_search, "Exa web search"); + + /* Show shortcuts: Super+S */ + keys_bind(MOD_SUPER, XK_s, key_show_shortcuts, "Show shortcuts"); + + /* Tutorial: Super+T */ + keys_bind(MOD_SUPER, XK_t, key_start_tutorial, "Start tutorial"); + + /* ===== News ticker navigation ===== */ + /* Super+Down: Next news article */ + keys_bind(MOD_SUPER, XK_Down, key_news_next, "Next news article"); + + /* Super+Up: Previous news article */ + keys_bind(MOD_SUPER, XK_Up, key_news_prev, "Previous news article"); + + /* Super+Return: Open current news article */ + keys_bind(MOD_SUPER, XK_Return, key_news_open, "Open news article"); + + /* Screenshot: Print Screen (XFCE default) */ + keys_bind(0, XK_Print, key_screenshot, "Take screenshot"); +} + +/* ========== Key binding callbacks ========== */ + +void key_spawn_terminal(void) +{ + const char *terminal = config_get_terminal(); + spawn_async(terminal); +} + +void key_spawn_launcher(void) +{ + applauncher_show(); +} + +void key_spawn_file_manager(void) +{ + if (dwn != NULL && dwn->config != NULL) { + spawn_async(dwn->config->file_manager); + } else { + spawn_async("thunar"); + } +} + +void key_spawn_browser(void) +{ + spawn_async("xdg-open http://"); +} + +void key_close_window(void) +{ + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + client_close(ws->focused); + } +} + +void key_quit_dwn(void) +{ + LOG_INFO("Quit requested via keyboard shortcut"); + dwn_quit(); +} + +void key_cycle_layout(void) +{ + if (dwn == NULL) { + return; + } + workspace_cycle_layout(dwn->current_workspace); +} + +void key_toggle_floating(void) +{ + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + client_toggle_floating(ws->focused); + workspace_arrange_current(); + } +} + +void key_toggle_fullscreen(void) +{ + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + client_toggle_fullscreen(ws->focused); + } +} + +void key_toggle_maximize(void) +{ + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + /* Toggle between maximized and normal state */ + /* For now, use fullscreen as maximize equivalent */ + client_toggle_fullscreen(ws->focused); + } +} + +void key_focus_next(void) +{ + workspace_focus_next(); +} + +void key_focus_prev(void) +{ + workspace_focus_prev(); +} + +void key_workspace_next(void) +{ + if (dwn == NULL) { + return; + } + int next = dwn->current_workspace + 1; + if (next >= MAX_WORKSPACES) { + next = 0; /* Wrap around */ + } + workspace_switch(next); +} + +void key_workspace_prev(void) +{ + if (dwn == NULL) { + return; + } + int prev = dwn->current_workspace - 1; + if (prev < 0) { + prev = MAX_WORKSPACES - 1; /* Wrap around */ + } + workspace_switch(prev); +} + +/* Workspace switching */ +void key_workspace_1(void) { workspace_switch(0); } +void key_workspace_2(void) { workspace_switch(1); } +void key_workspace_3(void) { workspace_switch(2); } +void key_workspace_4(void) { workspace_switch(3); } +void key_workspace_5(void) { workspace_switch(4); } +void key_workspace_6(void) { workspace_switch(5); } +void key_workspace_7(void) { workspace_switch(6); } +void key_workspace_8(void) { workspace_switch(7); } +void key_workspace_9(void) { workspace_switch(8); } + +/* Move to workspace */ +static void move_focused_to_workspace(int ws) +{ + Workspace *current = workspace_get_current(); + if (current != NULL && current->focused != NULL) { + workspace_move_client(current->focused, ws); + } +} + +void key_move_to_workspace_1(void) { move_focused_to_workspace(0); } +void key_move_to_workspace_2(void) { move_focused_to_workspace(1); } +void key_move_to_workspace_3(void) { move_focused_to_workspace(2); } +void key_move_to_workspace_4(void) { move_focused_to_workspace(3); } +void key_move_to_workspace_5(void) { move_focused_to_workspace(4); } +void key_move_to_workspace_6(void) { move_focused_to_workspace(5); } +void key_move_to_workspace_7(void) { move_focused_to_workspace(6); } +void key_move_to_workspace_8(void) { move_focused_to_workspace(7); } +void key_move_to_workspace_9(void) { move_focused_to_workspace(8); } + +/* Master area adjustments */ +void key_increase_master(void) +{ + if (dwn == NULL) { + return; + } + workspace_adjust_master_ratio(dwn->current_workspace, 0.05f); +} + +void key_decrease_master(void) +{ + if (dwn == NULL) { + return; + } + workspace_adjust_master_ratio(dwn->current_workspace, -0.05f); +} + +void key_increase_master_count(void) +{ + if (dwn == NULL) { + return; + } + workspace_adjust_master_count(dwn->current_workspace, 1); +} + +void key_decrease_master_count(void) +{ + if (dwn == NULL) { + return; + } + workspace_adjust_master_count(dwn->current_workspace, -1); +} + +/* AI functions */ +void key_toggle_ai(void) +{ + if (dwn == NULL) { + return; + } + + if (!dwn->ai_enabled) { + notification_show("DWN AI", "AI Disabled", + "Set OPENROUTER_API_KEY to enable AI features", + NULL, 3000); + return; + } + + /* Update and show AI context */ + ai_update_context(); + const char *task = ai_analyze_task(); + const char *suggestion = ai_suggest_window(); + + char body[512]; + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + snprintf(body, sizeof(body), + "Current task: %s\nFocused: %s\n%s", + task ? task : "Unknown", + ws->focused->title[0] ? ws->focused->title : "(unnamed)", + suggestion ? suggestion : "Press Alt+A to ask AI for help"); + } else { + snprintf(body, sizeof(body), + "No window focused.\nPress Alt+A to ask AI for help"); + } + + notification_show("DWN AI", "Context Analysis", body, NULL, 4000); +} + +void key_ai_command(void) +{ + if (dwn == NULL) { + return; + } + + if (dwn->ai_enabled) { + ai_show_command_palette(); + } else { + LOG_INFO("AI not enabled (no API key)"); + } +} + +void key_exa_search(void) +{ + exa_show_app_launcher(); +} + +void key_show_shortcuts(void) +{ + const char *shortcuts = + "=== Applications ===\n" + "Ctrl+Alt+T Terminal\n" + "Alt+F2 App launcher\n" + "Super+E File manager\n" + "Super+B Web browser\n" + "Print Screenshot\n" + "\n" + "=== Window Management ===\n" + "Alt+F4 Close window\n" + "Alt+Tab Next window\n" + "Alt+Shift+Tab Previous window\n" + "Alt+F10 Toggle maximize\n" + "Alt+F11 Toggle fullscreen\n" + "Super+F9 Toggle floating\n" + "\n" + "=== Workspaces ===\n" + "F1-F9 Switch to workspace\n" + "Shift+F1-F9 Move window to workspace\n" + "Ctrl+Alt+Right Next workspace\n" + "Ctrl+Alt+Left Previous workspace\n" + "\n" + "=== Layout Control ===\n" + "Super+Space Cycle layout (tile/float/mono)\n" + "Super+H Shrink master area\n" + "Super+L Expand master area\n" + "Super+I Add to master\n" + "Super+D Remove from master\n" + "\n" + "=== AI Features ===\n" + "Super+A Show AI context\n" + "Super+Shift+A AI command palette\n" + "Super+Shift+E Exa semantic search\n" + "\n" + "=== Help & System ===\n" + "Super+S Show shortcuts (this)\n" + "Super+T Interactive tutorial\n" + "Super+Backspace Quit DWN"; + + notification_show("DWN Shortcuts", "Complete Reference", shortcuts, NULL, 0); +} + +void key_start_tutorial(void) +{ + if (tutorial_is_active()) { + /* If already in tutorial, this acts as "next" */ + tutorial_next_step(); + } else { + tutorial_start(); + } +} + +/* ========== News ticker callbacks ========== */ + +void key_news_next(void) +{ + news_next_article(); +} + +void key_news_prev(void) +{ + news_prev_article(); +} + +void key_news_open(void) +{ + news_open_current(); +} + +void key_screenshot(void) +{ + spawn_async("xfce4-screenshooter"); +} diff --git a/src/layout.c b/src/layout.c new file mode 100644 index 0000000..22631a9 --- /dev/null +++ b/src/layout.c @@ -0,0 +1,263 @@ +/* + * DWN - Desktop Window Manager + * Layout algorithms implementation + */ + +#include "layout.h" +#include "client.h" +#include "workspace.h" +#include "config.h" +#include "util.h" + +/* Layout names and symbols */ +static const char *layout_names[] = { + "Tiling", + "Floating", + "Monocle" +}; + +static const char *layout_symbols[] = { + "[]=", + "><>", + "[M]" +}; + +/* ========== Main arrangement function ========== */ + +void layout_arrange(int workspace) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + switch (ws->layout) { + case LAYOUT_TILING: + layout_arrange_tiling(workspace); + break; + case LAYOUT_FLOATING: + layout_arrange_floating(workspace); + break; + case LAYOUT_MONOCLE: + layout_arrange_monocle(workspace); + break; + default: + layout_arrange_tiling(workspace); + break; + } +} + +/* ========== Tiling layout ========== */ + +void layout_arrange_tiling(int workspace) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + /* Get usable screen area (excluding panels) */ + int area_x, area_y, area_width, area_height; + layout_get_usable_area(&area_x, &area_y, &area_width, &area_height); + + int gap = config_get_gap(); + int title_height = config_get_title_height(); + int border = config_get_border_width(); + + /* Count tiled (non-floating) clients */ + int n = layout_count_tiled_clients(workspace); + if (n == 0) { + return; + } + + int master_count = ws->master_count; + if (master_count > n) { + master_count = n; + } + + /* Calculate master area width */ + int master_width; + if (n <= master_count) { + master_width = area_width - 2 * gap; + } else { + master_width = (int)((area_width - 3 * gap) * ws->master_ratio); + } + + int stack_width = area_width - master_width - 3 * gap; + + /* Arrange windows */ + int i = 0; + int master_y = area_y + gap; + int stack_y = area_y + gap; + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace != (unsigned int)workspace) { + continue; + } + if (client_is_floating(c) || client_is_fullscreen(c) || client_is_minimized(c)) { + continue; + } + + int x, y, w, h; + + if (i < master_count) { + /* Master area */ + int master_h = (area_height - 2 * gap - (master_count - 1) * gap) / master_count; + + x = area_x + gap; + y = master_y; + w = master_width; + h = master_h; + + master_y += h + gap; + } else { + /* Stack area */ + int stack_count = n - master_count; + int stack_h = (area_height - 2 * gap - (stack_count - 1) * gap) / stack_count; + + x = area_x + gap + master_width + gap; + y = stack_y; + w = stack_width; + h = stack_h; + + stack_y += h + gap; + } + + /* Account for decorations */ + int actual_h = h - title_height - 2 * border; + int actual_w = w - 2 * border; + + if (actual_h < 50) actual_h = 50; + if (actual_w < 50) actual_w = 50; + + client_move_resize(c, x + border, y + title_height + border, + actual_w, actual_h); + + i++; + } +} + +/* ========== Floating layout ========== */ + +void layout_arrange_floating(int workspace) +{ + /* In floating mode, we don't rearrange windows automatically. + Just make sure all clients are configured properly. */ + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace != (unsigned int)workspace) { + continue; + } + if (client_is_minimized(c)) { + continue; + } + + /* Ensure window is within screen bounds */ + int area_x, area_y, area_width, area_height; + layout_get_usable_area(&area_x, &area_y, &area_width, &area_height); + + if (c->x < area_x) c->x = area_x; + if (c->y < area_y) c->y = area_y; + if (c->x + c->width > area_x + area_width) { + c->x = area_x + area_width - c->width; + } + if (c->y + c->height > area_y + area_height) { + c->y = area_y + area_height - c->height; + } + + client_configure(c); + } +} + +/* ========== Monocle layout ========== */ + +void layout_arrange_monocle(int workspace) +{ + /* Get usable screen area */ + int area_x, area_y, area_width, area_height; + layout_get_usable_area(&area_x, &area_y, &area_width, &area_height); + + int gap = config_get_gap(); + int title_height = config_get_title_height(); + int border = config_get_border_width(); + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace != (unsigned int)workspace) { + continue; + } + if (client_is_floating(c) || client_is_fullscreen(c) || client_is_minimized(c)) { + continue; + } + + /* All tiled windows take the full usable area */ + int x = area_x + gap; + int y = area_y + gap; + int w = area_width - 2 * gap - 2 * border; + int h = area_height - 2 * gap - title_height - 2 * border; + + if (h < 50) h = 50; + if (w < 50) w = 50; + + client_move_resize(c, x + border, y + title_height + border, w, h); + } +} + +/* ========== Helpers ========== */ + +int layout_get_usable_area(int *x, int *y, int *width, int *height) +{ + if (dwn == NULL) { + return -1; + } + + int panel_height = config_get_panel_height(); + bool top_panel = (dwn->config != NULL) ? dwn->config->top_panel_enabled : true; + bool bottom_panel = (dwn->config != NULL) ? dwn->config->bottom_panel_enabled : true; + + *x = 0; + *y = top_panel ? panel_height : 0; + *width = dwn->screen_width; + *height = dwn->screen_height; + + if (top_panel) { + *height -= panel_height; + } + if (bottom_panel) { + *height -= panel_height; + } + + return 0; +} + +int layout_count_tiled_clients(int workspace) +{ + int count = 0; + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace != (unsigned int)workspace) { + continue; + } + if (client_is_floating(c) || client_is_fullscreen(c) || client_is_minimized(c)) { + continue; + } + count++; + } + + return count; +} + +const char *layout_get_name(LayoutType layout) +{ + if (layout >= 0 && layout < LAYOUT_COUNT) { + return layout_names[layout]; + } + return "Unknown"; +} + +const char *layout_get_symbol(LayoutType layout) +{ + if (layout >= 0 && layout < LAYOUT_COUNT) { + return layout_symbols[layout]; + } + return "???"; +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..97a96fb --- /dev/null +++ b/src/main.c @@ -0,0 +1,1016 @@ +/* + * DWN - Desktop Window Manager + * Main entry point and event loop + */ + +#include "dwn.h" +#include "config.h" +#include "atoms.h" +#include "client.h" +#include "workspace.h" +#include "layout.h" +#include "decorations.h" +#include "panel.h" +#include "keys.h" +#include "notifications.h" +#include "systray.h" +#include "news.h" +#include "applauncher.h" +#include "ai.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Global state instance */ +DWNState *dwn = NULL; +static DWNState dwn_state; + +/* Signal handling */ +static volatile sig_atomic_t received_signal = 0; + +static void signal_handler(int sig) +{ + received_signal = sig; +} + +/* Crash signal handler - flush logs before dying */ +static void crash_signal_handler(int sig) +{ + /* Flush logs synchronously before crashing */ + log_flush(); + + /* Re-raise the signal with default handler to get proper crash behavior */ + signal(sig, SIG_DFL); + raise(sig); +} + +/* X11 error handlers */ +static int last_x_error = 0; /* Track last error for checking */ + +static int x_error_handler(Display *dpy, XErrorEvent *ev) +{ + char error_text[256]; + XGetErrorText(dpy, ev->error_code, error_text, sizeof(error_text)); + + /* Store last error code for functions that want to check */ + last_x_error = ev->error_code; + + /* Write all X errors to crash log for debugging */ + FILE *crash = fopen("/tmp/dwn_crash.log", "a"); + if (crash) { + fprintf(crash, "[X_ERROR] code=%d request=%d resource=%lu: %s\n", + ev->error_code, ev->request_code, ev->resourceid, error_text); + fflush(crash); + fsync(fileno(crash)); + fclose(crash); + } + + /* BadWindow errors are common and recoverable - just log and continue */ + if (ev->error_code == BadWindow) { + LOG_DEBUG("X11 BadWindow error (request %d, resource %lu) - window was destroyed", + ev->request_code, ev->resourceid); + return 0; /* Continue - this is not fatal */ + } + + /* BadMatch, BadValue, BadDrawable are also often recoverable */ + if (ev->error_code == BadMatch || ev->error_code == BadValue || + ev->error_code == BadDrawable || ev->error_code == BadPixmap) { + LOG_WARN("X11 error: %s (request %d, resource %lu) - continuing", + error_text, ev->request_code, ev->resourceid); + return 0; /* Continue - these are recoverable */ + } + + /* Log other errors but don't crash */ + LOG_WARN("X11 error: %s (request %d, resource %lu)", + error_text, ev->request_code, ev->resourceid); + return 0; +} + +static int x_io_error_handler(Display *dpy) +{ + (void)dpy; + + /* Write directly to crash log - do not rely on async logging */ + FILE *crash = fopen("/tmp/dwn_crash.log", "a"); + if (crash) { + fprintf(crash, "[IO_ERROR] Fatal X11 I/O error - X server connection lost\n"); + fflush(crash); + fsync(fileno(crash)); + fclose(crash); + } + fprintf(stderr, "[IO_ERROR] Fatal X11 I/O error - X server connection lost\n"); + fflush(stderr); + + /* I/O errors mean the X server connection is broken - we must exit */ + /* But first, try to flush any pending logs */ + log_flush(); + LOG_ERROR("Fatal X11 I/O error - X server connection lost"); + log_flush(); + _exit(EXIT_FAILURE); /* Use _exit to avoid cleanup that might touch X */ + return 0; /* Never reached */ +} + +/* Check if another WM is running */ +static int wm_detected = 0; + +static int wm_detect_error_handler(Display *dpy, XErrorEvent *ev) +{ + (void)dpy; + (void)ev; + wm_detected = 1; + return 0; +} + +static bool check_other_wm(void) +{ + XSetErrorHandler(wm_detect_error_handler); + + XSelectInput(dwn->display, dwn->root, + SubstructureRedirectMask | SubstructureNotifyMask); + XSync(dwn->display, False); + + XSetErrorHandler(x_error_handler); + + return wm_detected != 0; +} + +/* Initialize multi-monitor support */ +static void init_monitors(void) +{ + if (!XineramaIsActive(dwn->display)) { + /* Single monitor */ + dwn->monitors[0].x = 0; + dwn->monitors[0].y = 0; + dwn->monitors[0].width = dwn->screen_width; + dwn->monitors[0].height = dwn->screen_height; + dwn->monitors[0].index = 0; + dwn->monitors[0].primary = true; + dwn->monitor_count = 1; + return; + } + + int num_screens; + XineramaScreenInfo *info = XineramaQueryScreens(dwn->display, &num_screens); + + if (info == NULL || num_screens == 0) { + dwn->monitors[0].x = 0; + dwn->monitors[0].y = 0; + dwn->monitors[0].width = dwn->screen_width; + dwn->monitors[0].height = dwn->screen_height; + dwn->monitors[0].index = 0; + dwn->monitors[0].primary = true; + dwn->monitor_count = 1; + return; + } + + dwn->monitor_count = (num_screens > MAX_MONITORS) ? MAX_MONITORS : num_screens; + + for (int i = 0; i < dwn->monitor_count; i++) { + dwn->monitors[i].x = info[i].x_org; + dwn->monitors[i].y = info[i].y_org; + dwn->monitors[i].width = info[i].width; + dwn->monitors[i].height = info[i].height; + dwn->monitors[i].index = i; + dwn->monitors[i].primary = (i == 0); + } + + XFree(info); + + LOG_INFO("Detected %d monitor(s)", dwn->monitor_count); +} + +/* Scan for existing windows */ +static void scan_existing_windows(void) +{ + Window root_return, parent_return; + Window *children; + unsigned int num_children; + + if (XQueryTree(dwn->display, dwn->root, &root_return, &parent_return, + &children, &num_children)) { + for (unsigned int i = 0; i < num_children; i++) { + XWindowAttributes wa; + if (XGetWindowAttributes(dwn->display, children[i], &wa)) { + if (wa.map_state == IsViewable && wa.override_redirect == False) { + client_manage(children[i]); + } + } + } + if (children != NULL) { + XFree(children); + } + } + + LOG_INFO("Scanned %d existing window(s)", client_count()); +} + +/* ========== Event handlers ========== */ + +static void handle_map_request(XMapRequestEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (ev->window == None) { + LOG_WARN("handle_map_request: received invalid window (None)"); + return; + } + + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, ev->window, &wa)) { + LOG_WARN("handle_map_request: XGetWindowAttributes failed for window %lu", ev->window); + return; + } + + if (wa.override_redirect) { + return; + } + + client_manage(ev->window); +} + +static void handle_unmap_notify(XUnmapEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL) { + return; + } + + Client *c = client_find_by_window(ev->window); + if (c == NULL) { + return; + } + + /* Ignore synthetic events */ + if (ev->send_event) { + return; + } + + /* Don't unmanage windows that are intentionally hidden: + * - Windows on a different workspace (hidden during workspace switch) + * - Minimized windows + * These windows are still managed, just not visible */ + if (c->workspace != (unsigned int)dwn->current_workspace) { + return; + } + if (c->flags & CLIENT_MINIMIZED) { + return; + } + + /* Window was actually closed/withdrawn - unmanage it */ + client_unmanage(c); +} + +static void handle_destroy_notify(XDestroyWindowEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL) { + return; + } + + Client *c = client_find_by_window(ev->window); + if (c != NULL) { + client_unmanage(c); + } +} + +static void handle_configure_request(XConfigureRequestEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + Client *c = client_find_by_window(ev->window); + + if (c != NULL) { + /* Managed window - respect some requests for floating windows */ + if (client_is_floating(c) || client_is_fullscreen(c)) { + if (ev->value_mask & CWX) c->x = ev->x; + if (ev->value_mask & CWY) c->y = ev->y; + if (ev->value_mask & CWWidth) c->width = ev->width; + if (ev->value_mask & CWHeight) c->height = ev->height; + client_configure(c); + } else { + /* Just send configure notify with current geometry */ + client_configure(c); + } + } else { + /* Unmanaged window - pass through */ + XWindowChanges wc; + wc.x = ev->x; + wc.y = ev->y; + wc.width = ev->width; + wc.height = ev->height; + wc.border_width = ev->border_width; + wc.sibling = ev->above; + wc.stack_mode = ev->detail; + + XConfigureWindow(dwn->display, ev->window, ev->value_mask, &wc); + } +} + +static void handle_property_notify(XPropertyEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL) { + return; + } + + Client *c = client_find_by_window(ev->window); + if (c == NULL) { + return; + } + + if (ev->atom == icccm.WM_NAME || ev->atom == ewmh.NET_WM_NAME) { + client_update_title(c); + panel_render_all(); + } +} + +static void handle_expose(XExposeEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL) { + return; + } + + /* Only handle final expose in a sequence */ + if (ev->count != 0) { + return; + } + + /* Check if it's a panel */ + if (dwn->top_panel != NULL && ev->window == dwn->top_panel->window) { + panel_render(dwn->top_panel); + return; + } + if (dwn->bottom_panel != NULL && ev->window == dwn->bottom_panel->window) { + panel_render(dwn->bottom_panel); + return; + } + + /* Check if it's a notification */ + Notification *notif = notification_find_by_window(ev->window); + if (notif != NULL) { + notification_render(notif); + return; + } + + /* Check if it's a frame */ + Client *c = client_find_by_frame(ev->window); + if (c != NULL) { + Workspace *ws = workspace_get(c->workspace); + bool focused = (ws != NULL && ws->focused == c); + decorations_render(c, focused); + } +} + +static void handle_enter_notify(XCrossingEvent *ev) +{ + /* Defensive: validate all pointers */ + if (ev == NULL || dwn == NULL || dwn->config == NULL) { + return; + } + + if (dwn->config->focus_mode != FOCUS_FOLLOW) { + return; + } + + /* Focus on enter for follow-mouse mode */ + Client *c = client_find_by_frame(ev->window); + if (c == NULL) { + c = client_find_by_window(ev->window); + } + + if (c != NULL && c->workspace == (unsigned int)dwn->current_workspace) { + client_focus(c); + } +} + +static void handle_button_press(XButtonEvent *ev) +{ + /* Defensive: validate all pointers */ + if (ev == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + /* Check volume slider first */ + if (volume_slider != NULL && volume_slider->visible) { + if (ev->window == volume_slider->window) { + volume_slider_handle_click(volume_slider, ev->x, ev->y); + return; + } else { + /* Clicked outside slider - close it */ + volume_slider_hide(volume_slider); + } + } + + /* Check WiFi dropdown menu */ + if (wifi_menu != NULL && wifi_menu->visible) { + if (ev->window == wifi_menu->window) { + dropdown_handle_click(wifi_menu, ev->x, ev->y); + return; + } else { + /* Clicked outside dropdown - close it */ + dropdown_hide(wifi_menu); + } + } + + /* Check notifications first - clicking dismisses them */ + Notification *notif = notification_find_by_window(ev->window); + if (notif != NULL) { + notification_close(notif->id); + return; + } + + /* Check panels first */ + if (dwn->top_panel != NULL && ev->window == dwn->top_panel->window) { + panel_handle_click(dwn->top_panel, ev->x, ev->y, ev->button); + return; + } + if (dwn->bottom_panel != NULL && ev->window == dwn->bottom_panel->window) { + panel_handle_click(dwn->bottom_panel, ev->x, ev->y, ev->button); + return; + } + + /* Find client */ + Client *c = client_find_by_frame(ev->window); + bool is_client_window = false; + if (c == NULL) { + c = client_find_by_window(ev->window); + is_client_window = (c != NULL); + } + + if (c == NULL) { + return; + } + + /* Focus on click */ + client_focus(c); + + /* If click was on client window content, replay the event to the application */ + if (is_client_window) { + XAllowEvents(dwn->display, ReplayPointer, ev->time); + return; + } + + /* Check for button clicks in decorations */ + if (c->frame != None && ev->window == c->frame) { + ButtonType btn = decorations_hit_test_button(c, ev->x, ev->y); + if (btn != BUTTON_COUNT) { + decorations_button_press(c, btn); + return; + } + + /* Check for title bar drag */ + if (decorations_hit_test_title_bar(c, ev->x, ev->y)) { + if (ev->button == 1) { + /* Start move */ + dwn->drag_client = c; + dwn->drag_start_x = ev->x_root; + dwn->drag_start_y = ev->y_root; + dwn->drag_orig_x = c->x; + dwn->drag_orig_y = c->y; + dwn->resizing = false; + + XGrabPointer(dwn->display, c->frame, True, + PointerMotionMask | ButtonReleaseMask, + GrabModeAsync, GrabModeAsync, None, None, CurrentTime); + } + return; + } + + /* Check for resize */ + int direction; + if (decorations_hit_test_resize_area(c, ev->x, ev->y, &direction)) { + if (ev->button == 1) { + dwn->drag_client = c; + dwn->drag_start_x = ev->x_root; + dwn->drag_start_y = ev->y_root; + dwn->drag_orig_x = c->x; + dwn->drag_orig_y = c->y; + dwn->drag_orig_w = c->width; + dwn->drag_orig_h = c->height; + dwn->resizing = true; + + XGrabPointer(dwn->display, c->frame, True, + PointerMotionMask | ButtonReleaseMask, + GrabModeAsync, GrabModeAsync, None, None, CurrentTime); + } + } + } +} + +static void handle_button_release(XButtonEvent *ev) +{ + /* Defensive: validate pointers */ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + /* Handle volume slider release */ + if (volume_slider != NULL && volume_slider->visible && volume_slider->dragging) { + volume_slider_handle_release(volume_slider); + } + + (void)ev; + + if (dwn->drag_client != NULL) { + XUngrabPointer(dwn->display, CurrentTime); + dwn->drag_client = NULL; + } +} + +static void handle_motion_notify(XMotionEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + /* Check volume slider drag */ + if (volume_slider != NULL && volume_slider->visible && ev->window == volume_slider->window) { + volume_slider_handle_motion(volume_slider, ev->x, ev->y); + return; + } + + /* Check WiFi dropdown hover */ + if (wifi_menu != NULL && wifi_menu->visible && ev->window == wifi_menu->window) { + dropdown_handle_motion(wifi_menu, ev->x, ev->y); + return; + } + + if (dwn->drag_client == NULL) { + return; + } + + Client *c = dwn->drag_client; + if (c == NULL) { + dwn->drag_client = NULL; /* Reset invalid drag state */ + return; + } + + int dx = ev->x_root - dwn->drag_start_x; + int dy = ev->y_root - dwn->drag_start_y; + + if (dwn->resizing) { + int new_w = dwn->drag_orig_w + dx; + int new_h = dwn->drag_orig_h + dy; + if (new_w < 50) new_w = 50; + if (new_h < 50) new_h = 50; + client_resize(c, new_w, new_h); + } else { + client_move(c, dwn->drag_orig_x + dx, dwn->drag_orig_y + dy); + } +} + +static void handle_client_message(XClientMessageEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + Client *c = client_find_by_window(ev->window); + + if (ev->message_type == ewmh.NET_ACTIVE_WINDOW) { + if (c != NULL) { + if (c->workspace != (unsigned int)dwn->current_workspace) { + workspace_switch(c->workspace); + } + client_focus(c); + } + } else if (ev->message_type == ewmh.NET_CLOSE_WINDOW) { + if (c != NULL) { + client_close(c); + } + } else if (ev->message_type == ewmh.NET_WM_STATE) { + if (c != NULL) { + Atom action = ev->data.l[0]; + Atom prop1 = ev->data.l[1]; + Atom prop2 = ev->data.l[2]; + + bool set = (action == 1); + bool toggle = (action == 2); + + if (prop1 == ewmh.NET_WM_STATE_FULLSCREEN || + prop2 == ewmh.NET_WM_STATE_FULLSCREEN) { + if (toggle) { + client_toggle_fullscreen(c); + } else { + client_set_fullscreen(c, set); + } + } + } + } else if (ev->message_type == ewmh.NET_CURRENT_DESKTOP) { + int desktop = ev->data.l[0]; + if (desktop >= 0 && desktop < MAX_WORKSPACES) { + workspace_switch(desktop); + } + } +} + +/* Sync log - disabled in production (enable for debugging crashes) */ +#if 0 +static FILE *crash_log_file = NULL; + +static void sync_log(const char *msg) +{ + if (crash_log_file == NULL) { + crash_log_file = fopen("/tmp/dwn_crash.log", "a"); + } + fprintf(stderr, "[SYNC] %s\n", msg); + fflush(stderr); + if (crash_log_file != NULL) { + fprintf(crash_log_file, "[SYNC] %s\n", msg); + fflush(crash_log_file); + fsync(fileno(crash_log_file)); + } +} +#endif + +/* Main event dispatcher */ +void dwn_handle_event(XEvent *ev) +{ + /* Defensive: validate event pointer */ + if (ev == NULL) { + LOG_WARN("dwn_handle_event: received NULL event"); + return; + } + + /* Defensive: validate global state */ + if (dwn == NULL || dwn->display == NULL) { + LOG_ERROR("dwn_handle_event: dwn or display is NULL"); + return; + } + + switch (ev->type) { + case MapRequest: + handle_map_request(&ev->xmaprequest); + break; + case UnmapNotify: + handle_unmap_notify(&ev->xunmap); + break; + case DestroyNotify: + handle_destroy_notify(&ev->xdestroywindow); + break; + case ConfigureRequest: + handle_configure_request(&ev->xconfigurerequest); + break; + case PropertyNotify: + handle_property_notify(&ev->xproperty); + break; + case Expose: + handle_expose(&ev->xexpose); + break; + case EnterNotify: + handle_enter_notify(&ev->xcrossing); + break; + case ButtonPress: + handle_button_press(&ev->xbutton); + break; + case ButtonRelease: + handle_button_release(&ev->xbutton); + break; + case MotionNotify: + handle_motion_notify(&ev->xmotion); + break; + case KeyPress: + keys_handle_press(&ev->xkey); + break; + case KeyRelease: + keys_handle_release(&ev->xkey); + break; + case ClientMessage: + handle_client_message(&ev->xclient); + break; + default: + break; + } +} + +/* ========== Core functions ========== */ + +int dwn_init(void) +{ + /* Initialize global state */ + dwn = &dwn_state; + memset(dwn, 0, sizeof(DWNState)); + + /* Open display */ + dwn->display = XOpenDisplay(NULL); + if (dwn->display == NULL) { + fprintf(stderr, "Cannot open X display\n"); + return -1; + } + + dwn->screen = DefaultScreen(dwn->display); + dwn->root = RootWindow(dwn->display, dwn->screen); + dwn->screen_width = DisplayWidth(dwn->display, dwn->screen); + dwn->screen_height = DisplayHeight(dwn->display, dwn->screen); + dwn->colormap = DefaultColormap(dwn->display, dwn->screen); + + /* Set error handlers */ + XSetErrorHandler(x_error_handler); + XSetIOErrorHandler(x_io_error_handler); + + /* Check for other WM */ + if (check_other_wm()) { + fprintf(stderr, "Another window manager is already running\n"); + XCloseDisplay(dwn->display); + return -1; + } + + /* Initialize logging */ + log_init("~/.local/share/dwn/dwn.log"); + LOG_INFO("DWN %s starting", DWN_VERSION); + + /* Load configuration */ + dwn->config = config_create(); + config_load(dwn->config, NULL); + + /* Initialize colors (after display is open) */ + extern void config_init_colors(Config *cfg); + config_init_colors(dwn->config); + + /* Load font */ + dwn->font = XLoadQueryFont(dwn->display, dwn->config->font_name); + if (dwn->font == NULL) { + dwn->font = XLoadQueryFont(dwn->display, "fixed"); + } + + /* Load Xft font for UTF-8 support */ + dwn->xft_font = XftFontOpenName(dwn->display, dwn->screen, + "monospace:size=10:antialias=true"); + if (dwn->xft_font == NULL) { + dwn->xft_font = XftFontOpenName(dwn->display, dwn->screen, + "fixed:size=10"); + } + if (dwn->xft_font != NULL) { + LOG_INFO("Loaded Xft font for UTF-8 support"); + } + + /* Create GC */ + XGCValues gcv; + gcv.foreground = dwn->config->colors.panel_fg; + gcv.background = dwn->config->colors.panel_bg; + gcv.font = dwn->font ? dwn->font->fid : None; + dwn->gc = XCreateGC(dwn->display, dwn->root, + GCForeground | GCBackground | (dwn->font ? GCFont : 0), &gcv); + + /* Initialize atoms */ + atoms_init(dwn->display); + + /* Initialize monitors */ + init_monitors(); + + /* Initialize workspaces */ + workspace_init(); + + /* Initialize decorations */ + decorations_init(); + + /* Initialize panels */ + panels_init(); + + /* Initialize system tray (WiFi, Audio indicators) */ + systray_init(); + + /* Initialize news ticker */ + news_init(); + + /* Initialize app launcher */ + applauncher_init(); + + /* Initialize keyboard shortcuts */ + keys_init(); + + /* Initialize D-Bus notifications */ + notifications_init(); + + /* Initialize AI */ + ai_init(); + + /* Setup EWMH */ + atoms_setup_ewmh(); + + /* Select events on root window */ + XSelectInput(dwn->display, dwn->root, + SubstructureRedirectMask | SubstructureNotifyMask | + StructureNotifyMask | PropertyChangeMask | + ButtonPressMask | PointerMotionMask); + + /* Set cursor */ + Cursor cursor = XCreateFontCursor(dwn->display, XC_left_ptr); + XDefineCursor(dwn->display, dwn->root, cursor); + + /* Scan existing windows */ + scan_existing_windows(); + + /* Arrange initial workspace */ + workspace_arrange_current(); + + /* Render panels */ + panel_render_all(); + + dwn->running = true; + + LOG_INFO("DWN initialized successfully"); + + return 0; +} + +void dwn_cleanup(void) +{ + LOG_INFO("DWN shutting down"); + + /* Cleanup subsystems */ + ai_cleanup(); + notifications_cleanup(); + news_cleanup(); + applauncher_cleanup(); + keys_cleanup(); + systray_cleanup(); + panels_cleanup(); + decorations_cleanup(); + workspace_cleanup(); + + /* Unmanage all clients */ + while (dwn->client_list != NULL) { + client_unmanage(dwn->client_list); + } + + /* Free resources */ + if (dwn->gc != None) { + XFreeGC(dwn->display, dwn->gc); + } + if (dwn->font != NULL) { + XFreeFont(dwn->display, dwn->font); + } + if (dwn->xft_font != NULL) { + XftFontClose(dwn->display, dwn->xft_font); + } + if (dwn->config != NULL) { + config_destroy(dwn->config); + } + + /* Close display */ + if (dwn->display != NULL) { + XCloseDisplay(dwn->display); + } + + log_close(); + + dwn = NULL; +} + +void dwn_run(void) +{ + int x11_fd = ConnectionNumber(dwn->display); + int dbus_fd = -1; + + if (dbus_conn != NULL) { + dbus_connection_get_unix_fd(dbus_conn, &dbus_fd); + } + + long last_clock_update = 0; + long last_news_update = 0; + + while (dwn->running && received_signal == 0) { + /* Handle pending X events */ + while (XPending(dwn->display)) { + XEvent ev; + XNextEvent(dwn->display, &ev); + dwn_handle_event(&ev); + } + + /* Process D-Bus messages */ + notifications_process_messages(); + + /* Process AI requests */ + ai_process_pending(); + + /* Process Exa requests */ + exa_process_pending(); + + /* Update notifications (check for expired) */ + notifications_update(); + + long now = get_time_ms(); + + /* Update news ticker frequently for smooth scrolling (~60fps) */ + if (now - last_news_update >= 16) { + news_update(); + panel_render_all(); + last_news_update = now; + } + + /* Update clock and system stats every second */ + if (now - last_clock_update >= 1000) { + panel_update_clock(); + panel_update_system_stats(); + systray_update(); + last_clock_update = now; + } + + /* Wait for events with timeout - short for smooth animation */ + fd_set fds; + FD_ZERO(&fds); + FD_SET(x11_fd, &fds); + if (dbus_fd >= 0) { + FD_SET(dbus_fd, &fds); + } + + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 16000; /* ~16ms for 60fps smooth scrolling */ + + int max_fd = x11_fd; + if (dbus_fd > max_fd) max_fd = dbus_fd; + + select(max_fd + 1, &fds, NULL, NULL, &tv); + } + + /* Handle signal */ + if (received_signal != 0) { + LOG_INFO("Received signal %d", received_signal); + } +} + +void dwn_quit(void) +{ + dwn->running = false; +} + +/* ========== Main ========== */ + +static void print_usage(const char *program) +{ + printf("Usage: %s [OPTIONS]\n", program); + printf("\n"); + printf("Options:\n"); + printf(" -h, --help Show this help message\n"); + printf(" -v, --version Show version information\n"); + printf(" -c CONFIG Use specified config file\n"); + printf("\n"); + printf("Environment variables:\n"); + printf(" OPENROUTER_API_KEY Enable AI features\n"); + printf(" EXA_API_KEY Enable semantic search\n"); +} + +static void print_version(void) +{ + printf("DWN - Desktop Window Manager %s\n", DWN_VERSION); + printf("AI-enhanced X11 window manager\n"); +} + +int main(int argc, char *argv[]) +{ + /* Parse arguments */ + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return 0; + } + if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) { + print_version(); + return 0; + } + } + + /* Setup signal handlers */ + signal(SIGTERM, signal_handler); + signal(SIGINT, signal_handler); + signal(SIGHUP, signal_handler); + + /* Setup crash signal handlers to flush logs before dying */ + signal(SIGSEGV, crash_signal_handler); + signal(SIGABRT, crash_signal_handler); + signal(SIGFPE, crash_signal_handler); + signal(SIGBUS, crash_signal_handler); + signal(SIGILL, crash_signal_handler); + + /* Initialize */ + if (dwn_init() != 0) { + return EXIT_FAILURE; + } + + /* Run event loop */ + dwn_run(); + + /* Cleanup */ + dwn_cleanup(); + + return EXIT_SUCCESS; +} diff --git a/src/news.c b/src/news.c new file mode 100644 index 0000000..cd1d35e --- /dev/null +++ b/src/news.c @@ -0,0 +1,697 @@ +/* + * DWN - Desktop Window Manager + * News ticker implementation + */ + +#include "news.h" +#include "panel.h" +#include "config.h" +#include "util.h" +#include "cJSON.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Scroll speed in pixels per second */ +#define SCROLL_SPEED_PPS 80 + +/* Fetch interval in milliseconds (5 minutes) */ +#define FETCH_INTERVAL 300000 + +/* Global state */ +NewsState news_state = {0}; + +/* Thread safety */ +static pthread_mutex_t news_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_t fetch_thread; +static volatile int fetch_running = 0; + +/* CURL response buffer */ +typedef struct { + char *data; + size_t size; +} CurlBuffer; + +static size_t news_curl_write_cb(void *contents, size_t size, size_t nmemb, void *userp) +{ + size_t realsize = size * nmemb; + CurlBuffer *buf = (CurlBuffer *)userp; + + char *ptr = realloc(buf->data, buf->size + realsize + 1); + if (ptr == NULL) { + return 0; + } + + buf->data = ptr; + memcpy(&(buf->data[buf->size]), contents, realsize); + buf->size += realsize; + buf->data[buf->size] = '\0'; + + return realsize; +} + +/* Decode numeric HTML entity ({ or ) */ +static int decode_numeric_entity(const char *src, char *out, size_t max_out) +{ + if (src[0] != '&' || src[1] != '#') return 0; + + const char *p = src + 2; + int base = 10; + if (*p == 'x' || *p == 'X') { + base = 16; + p++; + } + + char *end; + long code = strtol(p, &end, base); + if (*end != ';' || code <= 0) return 0; + + int consumed = (end - src) + 1; + + if (code < 128 && max_out >= 1) { + out[0] = (char)code; + return consumed; + } else if (code < 0x800 && max_out >= 2) { + out[0] = 0xC0 | (code >> 6); + out[1] = 0x80 | (code & 0x3F); + return consumed; + } else if (code < 0x10000 && max_out >= 3) { + out[0] = 0xE0 | (code >> 12); + out[1] = 0x80 | ((code >> 6) & 0x3F); + out[2] = 0x80 | (code & 0x3F); + return consumed; + } + return 0; +} + +/* Strip HTML tags and decode entities */ +static void strip_html(char *dst, const char *src, size_t max_len) +{ + size_t j = 0; + bool in_tag = false; + + for (size_t i = 0; src[i] && j < max_len - 1; i++) { + if (src[i] == '<') { + in_tag = true; + } else if (src[i] == '>') { + in_tag = false; + } else if (!in_tag) { + if (src[i] == '&') { + /* Named entities */ + if (strncmp(&src[i], "&", 5) == 0) { + dst[j++] = '&'; i += 4; + } else if (strncmp(&src[i], "<", 4) == 0) { + dst[j++] = '<'; i += 3; + } else if (strncmp(&src[i], ">", 4) == 0) { + dst[j++] = '>'; i += 3; + } else if (strncmp(&src[i], """, 6) == 0) { + dst[j++] = '"'; i += 5; + } else if (strncmp(&src[i], "'", 6) == 0) { + dst[j++] = '\''; i += 5; + } else if (strncmp(&src[i], " ", 6) == 0) { + dst[j++] = ' '; i += 5; + } else if (strncmp(&src[i], "–", 7) == 0) { + dst[j++] = '-'; i += 6; + } else if (strncmp(&src[i], "—", 7) == 0) { + dst[j++] = '-'; dst[j++] = '-'; i += 6; + } else if (strncmp(&src[i], "…", 8) == 0) { + dst[j++] = '.'; dst[j++] = '.'; dst[j++] = '.'; i += 7; + } else if (strncmp(&src[i], "‘", 7) == 0 || strncmp(&src[i], "’", 7) == 0) { + dst[j++] = '\''; i += 6; + } else if (strncmp(&src[i], "“", 7) == 0 || strncmp(&src[i], "”", 7) == 0) { + dst[j++] = '"'; i += 6; + } else if (src[i+1] == '#') { + /* Numeric entity */ + char decoded[4] = {0}; + int consumed = decode_numeric_entity(&src[i], decoded, sizeof(decoded)); + if (consumed > 0) { + for (int k = 0; decoded[k] && j < max_len - 1; k++) { + dst[j++] = decoded[k]; + } + i += consumed - 1; + } else { + dst[j++] = src[i]; + } + } else { + dst[j++] = src[i]; + } + } else if (src[i] == '\n' || src[i] == '\r' || src[i] == '\t') { + dst[j++] = ' '; + } else { + dst[j++] = src[i]; + } + } + } + dst[j] = '\0'; + + /* Collapse multiple spaces */ + char *read = dst, *write = dst; + bool last_space = false; + while (*read) { + if (*read == ' ') { + if (!last_space) { + *write++ = *read; + } + last_space = true; + } else { + *write++ = *read; + last_space = false; + } + read++; + } + *write = '\0'; +} + +/* Parse JSON response and populate articles */ +static int parse_news_json(const char *json_str) +{ + cJSON *root = cJSON_Parse(json_str); + if (root == NULL) { + LOG_WARN("Failed to parse news JSON"); + return -1; + } + + cJSON *articles = cJSON_GetObjectItemCaseSensitive(root, "articles"); + if (!cJSON_IsArray(articles)) { + LOG_WARN("No articles array in news JSON"); + cJSON_Delete(root); + return -1; + } + + int count = 0; + cJSON *article; + cJSON_ArrayForEach(article, articles) { + if (count >= MAX_NEWS_ARTICLES) break; + + cJSON *title = cJSON_GetObjectItemCaseSensitive(article, "title"); + cJSON *content = cJSON_GetObjectItemCaseSensitive(article, "content"); + cJSON *description = cJSON_GetObjectItemCaseSensitive(article, "description"); + cJSON *link = cJSON_GetObjectItemCaseSensitive(article, "link"); + cJSON *author = cJSON_GetObjectItemCaseSensitive(article, "author"); + + NewsArticle *art = &news_state.articles[count]; + memset(art, 0, sizeof(NewsArticle)); + + if (cJSON_IsString(title) && title->valuestring) { + strip_html(art->title, title->valuestring, sizeof(art->title)); + } + + /* Prefer content, fallback to description */ + const char *text = NULL; + if (cJSON_IsString(content) && content->valuestring && strlen(content->valuestring) > 0) { + text = content->valuestring; + } else if (cJSON_IsString(description) && description->valuestring) { + text = description->valuestring; + } + + if (text) { + strip_html(art->content, text, sizeof(art->content)); + } else { + /* Use title as fallback */ + strncpy(art->content, art->title, sizeof(art->content) - 1); + } + + if (cJSON_IsString(link) && link->valuestring) { + strncpy(art->link, link->valuestring, sizeof(art->link) - 1); + } + + if (cJSON_IsString(author) && author->valuestring) { + strncpy(art->author, author->valuestring, sizeof(art->author) - 1); + } + + count++; + } + + cJSON_Delete(root); + return count; +} + +/* Separator between articles */ +#define NEWS_SEPARATOR " • " +#define NEWS_SEPARATOR_LEN 5 + +/* Forward declarations */ +static int news_find_article_at_x(int click_x); + +/* Get UTF-8 text width using Xft */ +static int news_text_width(const char *text) +{ + if (dwn == NULL || text == NULL) return 0; + + if (dwn->xft_font != NULL) { + XGlyphInfo extents; + XftTextExtentsUtf8(dwn->display, dwn->xft_font, + (const FcChar8 *)text, strlen(text), &extents); + return extents.xOff; + } + + if (dwn->font != NULL) { + return XTextWidth(dwn->font, text, strlen(text)); + } + + return strlen(text) * 8; +} + +/* Draw UTF-8 text using Xft with clipping */ +static void news_draw_text_clipped(Drawable d, int x, int y, const char *text, + unsigned long color, XRectangle *clip) +{ + if (dwn == NULL || dwn->display == NULL || text == NULL) return; + + if (dwn->xft_font != NULL) { + XftDraw *xft_draw = XftDrawCreate(dwn->display, d, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap); + if (xft_draw != NULL) { + if (clip != NULL) { + Region region = XCreateRegion(); + XUnionRectWithRegion(clip, region, region); + XftDrawSetClip(xft_draw, region); + XDestroyRegion(region); + } + + XftColor xft_color; + XRenderColor render_color; + + render_color.red = ((color >> 16) & 0xFF) * 257; + render_color.green = ((color >> 8) & 0xFF) * 257; + render_color.blue = (color & 0xFF) * 257; + render_color.alpha = 0xFFFF; + + XftColorAllocValue(dwn->display, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &render_color, &xft_color); + + XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, + x, y, (const FcChar8 *)text, strlen(text)); + + XftColorFree(dwn->display, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &xft_color); + XftDrawDestroy(xft_draw); + return; + } + } + + if (clip != NULL) { + XSetClipRectangles(dwn->display, dwn->gc, 0, 0, clip, 1, Unsorted); + } + XSetForeground(dwn->display, dwn->gc, color); + XDrawString(dwn->display, d, dwn->gc, x, y, text, strlen(text)); + if (clip != NULL) { + XSetClipMask(dwn->display, dwn->gc, None); + } +} + +/* Get display text for an article */ +static void get_article_display_text(int index, char *buf, size_t buf_size) +{ + if (index < 0 || index >= news_state.article_count) { + buf[0] = '\0'; + return; + } + NewsArticle *art = &news_state.articles[index]; + if (art->content[0] && strcmp(art->content, art->title) != 0) { + snprintf(buf, buf_size, "%s: %s", art->title, art->content); + } else { + snprintf(buf, buf_size, "%s", art->title); + } +} + +/* Recalculate cached widths after fetching */ +static void news_recalc_widths(void) +{ + if (dwn == NULL) { + return; + } + + int separator_width = news_text_width(NEWS_SEPARATOR); + news_state.total_width = 0; + + for (int i = 0; i < news_state.article_count; i++) { + char display_text[2048]; + get_article_display_text(i, display_text, sizeof(display_text)); + news_state.display_widths[i] = news_text_width(display_text); + news_state.total_width += news_state.display_widths[i] + separator_width; + } +} + +/* Background fetch thread */ +static void *news_fetch_thread(void *arg) +{ + (void)arg; + + CURL *curl = curl_easy_init(); + if (curl == NULL) { + pthread_mutex_lock(&news_mutex); + news_state.fetching = false; + news_state.has_error = true; + pthread_mutex_unlock(&news_mutex); + return NULL; + } + + CurlBuffer buf = {0}; + buf.data = malloc(1); + buf.data[0] = '\0'; + + curl_easy_setopt(curl, CURLOPT_URL, NEWS_API_URL); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, news_curl_write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "DWN/1.0"); + + CURLcode res = curl_easy_perform(curl); + + pthread_mutex_lock(&news_mutex); + if (res == CURLE_OK && buf.data && buf.size > 0) { + int count = parse_news_json(buf.data); + if (count > 0) { + news_state.article_count = count; + news_state.has_error = false; + news_state.current_article = 0; + news_state.scroll_offset = 0.0; + news_state.last_scroll_update = 0; + news_state.widths_dirty = true; + LOG_INFO("Fetched %d news articles", count); + } else { + news_state.has_error = true; + } + } else { + news_state.has_error = true; + LOG_DEBUG("News fetch failed: %s", curl_easy_strerror(res)); + } + news_state.fetching = false; + news_state.last_fetch = get_time_ms(); + pthread_mutex_unlock(&news_mutex); + + free(buf.data); + curl_easy_cleanup(curl); + + return NULL; +} + +/* ========== Public API ========== */ + +void news_init(void) +{ + memset(&news_state, 0, sizeof(news_state)); + news_state.last_fetch = 0; + + /* Start initial fetch */ + news_fetch_async(); + + LOG_INFO("News ticker initialized"); +} + +void news_cleanup(void) +{ + /* Wait for any pending fetch */ + if (fetch_running) { + pthread_join(fetch_thread, NULL); + fetch_running = 0; + } +} + +void news_fetch_async(void) +{ + pthread_mutex_lock(&news_mutex); + if (news_state.fetching) { + pthread_mutex_unlock(&news_mutex); + return; + } + news_state.fetching = true; + pthread_mutex_unlock(&news_mutex); + + /* Wait for previous thread if still running */ + if (fetch_running) { + pthread_join(fetch_thread, NULL); + } + + fetch_running = 1; + if (pthread_create(&fetch_thread, NULL, news_fetch_thread, NULL) != 0) { + pthread_mutex_lock(&news_mutex); + news_state.fetching = false; + news_state.has_error = true; + pthread_mutex_unlock(&news_mutex); + fetch_running = 0; + } +} + +void news_update(void) +{ + long now = get_time_ms(); + + pthread_mutex_lock(&news_mutex); + + /* Auto-refresh if interval passed */ + if (!news_state.fetching && (now - news_state.last_fetch) >= FETCH_INTERVAL) { + pthread_mutex_unlock(&news_mutex); + news_fetch_async(); + pthread_mutex_lock(&news_mutex); + } + + /* Time-based smooth scrolling */ + if (!news_state.interactive_mode && news_state.article_count > 0) { + if (news_state.last_scroll_update > 0) { + long delta_ms = now - news_state.last_scroll_update; + /* Calculate smooth sub-pixel scroll amount based on elapsed time */ + double scroll_amount = (SCROLL_SPEED_PPS * delta_ms) / 1000.0; + news_state.scroll_offset += scroll_amount; + } + news_state.last_scroll_update = now; + } + + pthread_mutex_unlock(&news_mutex); +} + +void news_next_article(void) +{ + pthread_mutex_lock(&news_mutex); + if (news_state.article_count > 0) { + news_state.current_article = (news_state.current_article + 1) % news_state.article_count; + news_state.scroll_offset = 0.0; + news_state.last_scroll_update = 0; + news_state.interactive_mode = true; + } + pthread_mutex_unlock(&news_mutex); +} + +void news_prev_article(void) +{ + pthread_mutex_lock(&news_mutex); + if (news_state.article_count > 0) { + news_state.current_article--; + if (news_state.current_article < 0) { + news_state.current_article = news_state.article_count - 1; + } + news_state.scroll_offset = 0.0; + news_state.last_scroll_update = 0; + news_state.interactive_mode = true; + } + pthread_mutex_unlock(&news_mutex); +} + +void news_open_current(void) +{ + pthread_mutex_lock(&news_mutex); + if (news_state.article_count > 0 && news_state.total_width > 0) { + int article_index = news_find_article_at_x(news_state.render_x); + if (article_index < 0) article_index = 0; + + char url[512]; + strncpy(url, news_state.articles[article_index].link, sizeof(url) - 1); + url[sizeof(url) - 1] = '\0'; + pthread_mutex_unlock(&news_mutex); + + pid_t pid = fork(); + if (pid == 0) { + setsid(); + pid_t pid2 = fork(); + if (pid2 == 0) { + execlp("xdg-open", "xdg-open", url, NULL); + _exit(1); + } + _exit(0); + } else if (pid > 0) { + int status; + waitpid(pid, &status, 0); + } + } else { + pthread_mutex_unlock(&news_mutex); + } +} + +/* Find article at given x position within news area */ +static int news_find_article_at_x(int click_x) +{ + if (news_state.article_count == 0 || news_state.total_width == 0) { + return -1; + } + + int separator_width = news_text_width(NEWS_SEPARATOR); + int scroll_offset = (int)news_state.scroll_offset % news_state.total_width; + int rel_x = click_x - news_state.render_x; + + int content_x = scroll_offset + rel_x; + if (content_x < 0) { + content_x += news_state.total_width; + } + content_x = content_x % news_state.total_width; + + int pos = 0; + for (int i = 0; i < news_state.article_count; i++) { + int article_total = news_state.display_widths[i] + separator_width; + if (content_x < pos + article_total) { + return i; + } + pos += article_total; + } + return 0; +} + +void news_render(Panel *panel, int x, int max_width, int *used_width) +{ + if (panel == NULL || used_width == NULL || dwn == NULL) { + *used_width = 0; + return; + } + + if (dwn->xft_font == NULL && dwn->font == NULL) { + *used_width = 0; + return; + } + + pthread_mutex_lock(&news_mutex); + + if (news_state.article_count == 0) { + pthread_mutex_unlock(&news_mutex); + *used_width = 0; + return; + } + + if (news_state.widths_dirty) { + news_recalc_widths(); + news_state.widths_dirty = false; + } + + if (news_state.total_width == 0) { + pthread_mutex_unlock(&news_mutex); + *used_width = 0; + return; + } + + news_state.render_x = x; + news_state.render_width = max_width; + + int separator_width = news_text_width(NEWS_SEPARATOR); + int scroll_offset = (int)news_state.scroll_offset % news_state.total_width; + + pthread_mutex_unlock(&news_mutex); + + const ColorScheme *colors = config_get_colors(); + + int text_y; + if (dwn->xft_font != NULL) { + text_y = (panel->height + dwn->xft_font->ascent - dwn->xft_font->descent) / 2; + } else { + text_y = (panel->height + dwn->font->ascent - dwn->font->descent) / 2; + } + + XRectangle clip_rect = { x, 0, max_width, panel->height }; + + pthread_mutex_lock(&news_mutex); + + int draw_x = x - scroll_offset; + int articles_drawn = 0; + int start_article = 0; + + int pos = 0; + for (int i = 0; i < news_state.article_count; i++) { + int article_total = news_state.display_widths[i] + separator_width; + if (pos + article_total > scroll_offset) { + start_article = i; + draw_x = x + (pos - scroll_offset); + break; + } + pos += article_total; + } + + int current_article = start_article; + while (draw_x < x + max_width && articles_drawn < news_state.article_count * 2) { + char display_text[2048]; + get_article_display_text(current_article, display_text, sizeof(display_text)); + + if (draw_x + news_state.display_widths[current_article] > x) { + news_draw_text_clipped(panel->buffer, draw_x, text_y, display_text, + colors->panel_fg, &clip_rect); + } + draw_x += news_state.display_widths[current_article]; + + if (draw_x < x + max_width) { + news_draw_text_clipped(panel->buffer, draw_x, text_y, NEWS_SEPARATOR, + colors->panel_fg, &clip_rect); + } + draw_x += separator_width; + + current_article = (current_article + 1) % news_state.article_count; + articles_drawn++; + } + + pthread_mutex_unlock(&news_mutex); + + *used_width = max_width; +} + +void news_handle_click(int x, int y) +{ + (void)y; + + pthread_mutex_lock(&news_mutex); + if (news_state.article_count == 0) { + pthread_mutex_unlock(&news_mutex); + return; + } + + int article_index = news_find_article_at_x(x); + if (article_index >= 0 && article_index < news_state.article_count) { + char url[512]; + strncpy(url, news_state.articles[article_index].link, sizeof(url) - 1); + url[sizeof(url) - 1] = '\0'; + pthread_mutex_unlock(&news_mutex); + + pid_t pid = fork(); + if (pid == 0) { + setsid(); + pid_t pid2 = fork(); + if (pid2 == 0) { + execlp("xdg-open", "xdg-open", url, NULL); + _exit(1); + } + _exit(0); + } else if (pid > 0) { + int status; + waitpid(pid, &status, 0); + } + } else { + pthread_mutex_unlock(&news_mutex); + } +} + +void news_lock(void) +{ + pthread_mutex_lock(&news_mutex); +} + +void news_unlock(void) +{ + pthread_mutex_unlock(&news_mutex); +} diff --git a/src/notifications.c b/src/notifications.c new file mode 100644 index 0000000..221a37a --- /dev/null +++ b/src/notifications.c @@ -0,0 +1,856 @@ +/* + * DWN - Desktop Window Manager + * D-Bus Notification daemon implementation + */ + +#include "notifications.h" +#include "config.h" +#include "util.h" + +#include +#include +#include + +/* D-Bus connection */ +DBusConnection *dbus_conn = NULL; + +/* Notification list */ +static Notification *notification_list = NULL; +static uint32_t next_notification_id = 1; + +/* Notification dimensions - dynamic based on screen size */ +#define NOTIFICATION_MIN_WIDTH 280 +#define NOTIFICATION_PADDING 12 +#define NOTIFICATION_MARGIN 10 + +/* Get max width: 33% of screen width */ +static int notification_max_width(void) +{ + if (dwn == NULL) return 500; + return dwn->screen_width / 3; +} + +/* Get max height: 50% of screen height */ +static int notification_max_height(void) +{ + if (dwn == NULL) return 600; + return dwn->screen_height / 2; +} + +/* Default timeout */ +#define DEFAULT_TIMEOUT 5000 + +/* ========== UTF-8 text helpers ========== */ + +/* Get text width using Xft (UTF-8 aware) */ +static int notif_text_width(const char *text, int len) +{ + if (text == NULL || dwn == NULL) return len * 7; /* Fallback estimate */ + + if (dwn->xft_font != NULL) { + XGlyphInfo extents; + XftTextExtentsUtf8(dwn->display, dwn->xft_font, + (const FcChar8 *)text, len, &extents); + return extents.xOff; + } + + if (dwn->font != NULL) { + return XTextWidth(dwn->font, text, len); + } + + return len * 7; /* Fallback estimate */ +} + +/* Get line height */ +static int notif_line_height(void) +{ + if (dwn == NULL) return 14; + + if (dwn->xft_font != NULL) { + return dwn->xft_font->ascent + dwn->xft_font->descent + 2; + } + + if (dwn->font != NULL) { + return dwn->font->ascent + dwn->font->descent + 2; + } + + return 14; +} + +/* Get font ascent */ +static int notif_font_ascent(void) +{ + if (dwn == NULL) return 12; + + if (dwn->xft_font != NULL) { + return dwn->xft_font->ascent; + } + + if (dwn->font != NULL) { + return dwn->font->ascent; + } + + return 12; +} + +/* Draw text using Xft (UTF-8 aware) */ +static void notif_draw_text(Window win, int x, int y, const char *text, + int len, unsigned long color) +{ + if (text == NULL || dwn == NULL || dwn->display == NULL) return; + + /* Use Xft for UTF-8 support */ + if (dwn->xft_font != NULL) { + XftDraw *xft_draw = XftDrawCreate(dwn->display, win, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap); + if (xft_draw != NULL) { + XftColor xft_color; + XRenderColor render_color; + render_color.red = ((color >> 16) & 0xFF) * 257; + render_color.green = ((color >> 8) & 0xFF) * 257; + render_color.blue = (color & 0xFF) * 257; + render_color.alpha = 0xFFFF; + + XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &render_color, &xft_color); + + XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, + x, y, (const FcChar8 *)text, len); + + XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &xft_color); + XftDrawDestroy(xft_draw); + return; + } + } + + /* Fallback to legacy X11 text */ + XSetForeground(dwn->display, dwn->gc, color); + XDrawString(dwn->display, win, dwn->gc, x, y, text, len); +} + +/* ========== Initialization ========== */ + +bool notifications_init(void) +{ + DBusError err; + dbus_error_init(&err); + + /* Connect to session bus */ + dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &err); + if (dbus_error_is_set(&err)) { + LOG_ERROR("D-Bus connection error: %s", err.message); + dbus_error_free(&err); + return false; + } + + if (dbus_conn == NULL) { + LOG_ERROR("Failed to connect to D-Bus session bus"); + return false; + } + + /* Register notification service */ + if (!notifications_register_service()) { + LOG_WARN("Could not register notification service (another daemon running?)"); + /* Don't fail - we can still function as a WM */ + } + + LOG_INFO("Notification daemon initialized"); + return true; +} + +void notifications_cleanup(void) +{ + /* Close all notifications */ + notification_close_all(); + + /* D-Bus connection is managed by libdbus */ + dbus_conn = NULL; +} + +/* ========== D-Bus handling ========== */ + +bool notifications_register_service(void) +{ + if (dbus_conn == NULL) { + return false; + } + + DBusError err; + dbus_error_init(&err); + + /* Request the notification service name */ + int result = dbus_bus_request_name(dbus_conn, "org.freedesktop.Notifications", + DBUS_NAME_FLAG_REPLACE_EXISTING, &err); + + if (dbus_error_is_set(&err)) { + LOG_ERROR("D-Bus name request error: %s", err.message); + dbus_error_free(&err); + return false; + } + + if (result != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { + LOG_WARN("Could not become primary owner of notification service"); + return false; + } + + /* Add message filter */ + dbus_connection_add_filter(dbus_conn, notifications_handle_message, NULL, NULL); + + LOG_INFO("Registered as org.freedesktop.Notifications"); + return true; +} + +void notifications_process_messages(void) +{ + if (dbus_conn == NULL) { + return; + } + + /* Process pending D-Bus messages (non-blocking) */ + dbus_connection_read_write(dbus_conn, 0); + + while (dbus_connection_dispatch(dbus_conn) == DBUS_DISPATCH_DATA_REMAINS) { + /* Keep dispatching */ + } +} + +DBusHandlerResult notifications_handle_message(DBusConnection *conn, + DBusMessage *msg, + void *user_data) +{ + (void)user_data; + + const char *interface = dbus_message_get_interface(msg); + const char *member = dbus_message_get_member(msg); + + if (interface == NULL || member == NULL) { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + if (strcmp(interface, "org.freedesktop.Notifications") != 0) { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + /* Handle Notify method */ + if (strcmp(member, "Notify") == 0) { + DBusMessageIter args; + if (!dbus_message_iter_init(msg, &args)) { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + char *app_name = ""; + uint32_t replaces_id = 0; + char *icon = ""; + char *summary = ""; + char *body = ""; + int32_t timeout = -1; + + /* Parse arguments */ + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) { + dbus_message_iter_get_basic(&args, &app_name); + dbus_message_iter_next(&args); + } + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_UINT32) { + dbus_message_iter_get_basic(&args, &replaces_id); + dbus_message_iter_next(&args); + } + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) { + dbus_message_iter_get_basic(&args, &icon); + dbus_message_iter_next(&args); + } + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) { + dbus_message_iter_get_basic(&args, &summary); + dbus_message_iter_next(&args); + } + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) { + dbus_message_iter_get_basic(&args, &body); + dbus_message_iter_next(&args); + } + + /* Skip actions array */ + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_ARRAY) { + dbus_message_iter_next(&args); + } + + /* Skip hints dict */ + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_ARRAY) { + dbus_message_iter_next(&args); + } + + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_INT32) { + dbus_message_iter_get_basic(&args, &timeout); + } + + /* Show notification */ + uint32_t id = notification_show(app_name, summary, body, icon, timeout); + + /* Send reply */ + DBusMessage *reply = dbus_message_new_method_return(msg); + dbus_message_append_args(reply, DBUS_TYPE_UINT32, &id, DBUS_TYPE_INVALID); + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; + } + + /* Handle CloseNotification method */ + if (strcmp(member, "CloseNotification") == 0) { + DBusMessageIter args; + if (dbus_message_iter_init(msg, &args) && + dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_UINT32) { + uint32_t id; + dbus_message_iter_get_basic(&args, &id); + notification_close(id); + } + + DBusMessage *reply = dbus_message_new_method_return(msg); + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; + } + + /* Handle GetCapabilities method */ + if (strcmp(member, "GetCapabilities") == 0) { + DBusMessage *reply = dbus_message_new_method_return(msg); + DBusMessageIter args_iter, array_iter; + + dbus_message_iter_init_append(reply, &args_iter); + dbus_message_iter_open_container(&args_iter, DBUS_TYPE_ARRAY, "s", &array_iter); + + const char *caps[] = { "body", "body-markup" }; + for (size_t i = 0; i < sizeof(caps) / sizeof(caps[0]); i++) { + dbus_message_iter_append_basic(&array_iter, DBUS_TYPE_STRING, &caps[i]); + } + + dbus_message_iter_close_container(&args_iter, &array_iter); + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; + } + + /* Handle GetServerInformation method */ + if (strcmp(member, "GetServerInformation") == 0) { + DBusMessage *reply = dbus_message_new_method_return(msg); + + const char *name = "DWN"; + const char *vendor = "DWN Project"; + const char *version = DWN_VERSION; + const char *spec_version = "1.2"; + + dbus_message_append_args(reply, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &vendor, + DBUS_TYPE_STRING, &version, + DBUS_TYPE_STRING, &spec_version, + DBUS_TYPE_INVALID); + + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +/* ========== Notification management ========== */ + +#define MAX_VISIBLE_NOTIFICATIONS 3 + +/* Calculate notification size based on content - accounts for word wrapping */ +static void notification_calculate_size(const char *summary, const char *body, + int *out_width, int *out_height) +{ + int max_width = notification_max_width(); + int max_height = notification_max_height(); + int min_height = 80; + int content_max_width = max_width - 2 * NOTIFICATION_PADDING; + + int width = NOTIFICATION_MIN_WIDTH; + int height = NOTIFICATION_PADDING * 2; + int line_height = notif_line_height(); + + /* Add space for summary */ + if (summary != NULL && summary[0] != '\0') { + int summary_width = NOTIFICATION_PADDING * 2; + summary_width += notif_text_width(summary, strlen(summary)); + if (summary_width > width) width = summary_width; + height += line_height + 4; + } + + /* Count wrapped lines in body */ + if (body != NULL && body[0] != '\0') { + const char *p = body; + int wrapped_line_count = 0; + + while (*p != '\0') { + /* Find end of this logical line (newline) */ + const char *line_start = p; + while (*p != '\0' && *p != '\n') p++; + size_t logical_line_len = p - line_start; + + if (logical_line_len == 0) { + /* Empty line */ + wrapped_line_count++; + } else { + /* Count how many wrapped lines this logical line needs */ + const char *lp = line_start; + while (lp < line_start + logical_line_len) { + size_t remaining = (line_start + logical_line_len) - lp; + size_t fit_len = 0; + + /* Find how much fits on one line */ + for (size_t i = 0; i < remaining; i++) { + if ((lp[i] & 0xC0) == 0x80) continue; /* Skip continuation bytes */ + int w = notif_text_width(lp, i + 1); + if (w > content_max_width) break; + fit_len = i + 1; + } + + /* Include trailing UTF-8 continuation bytes */ + while (fit_len < remaining && (lp[fit_len] & 0xC0) == 0x80) { + fit_len++; + } + + /* Force at least one character */ + if (fit_len == 0 && remaining > 0) { + fit_len = 1; + while (fit_len < remaining && (lp[fit_len] & 0xC0) == 0x80) { + fit_len++; + } + } + + if (fit_len == 0) break; + + lp += fit_len; + /* Skip leading spaces on continuation */ + while (lp < line_start + logical_line_len && *lp == ' ') lp++; + + wrapped_line_count++; + + /* Stop if we'd exceed max height */ + if (height + (wrapped_line_count * line_height) > max_height - line_height - NOTIFICATION_PADDING) { + goto done_counting; + } + } + } + + if (*p == '\n') p++; + } + +done_counting: + height += wrapped_line_count * line_height; + } + + /* Add space for app name at bottom */ + height += line_height + NOTIFICATION_PADDING; + + /* Use full max_width since we're wrapping, not truncating */ + width = max_width; + + /* Clamp to min/max */ + if (width < NOTIFICATION_MIN_WIDTH) width = NOTIFICATION_MIN_WIDTH; + if (width > max_width) width = max_width; + if (height < min_height) height = min_height; + if (height > max_height) height = max_height; + + *out_width = width; + *out_height = height; +} + +uint32_t notification_show(const char *app_name, const char *summary, + const char *body, const char *icon, int timeout) +{ + /* Count existing notifications and close oldest if we have too many */ + int count = 0; + Notification *oldest = NULL; + for (Notification *n = notification_list; n != NULL; n = n->next) { + count++; + oldest = n; /* Last in list is oldest (we prepend new ones) */ + } + + /* Close oldest notification if we're at the limit */ + if (count >= MAX_VISIBLE_NOTIFICATIONS && oldest != NULL) { + notification_close(oldest->id); + } + + Notification *notif = dwn_calloc(1, sizeof(Notification)); + + notif->id = next_notification_id++; + + if (app_name) strncpy(notif->app_name, app_name, sizeof(notif->app_name) - 1); + if (summary) strncpy(notif->summary, summary, sizeof(notif->summary) - 1); + + /* Dynamically allocate body - unlimited size */ + if (body != NULL && body[0] != '\0') { + notif->body_len = strlen(body); + notif->body = dwn_malloc(notif->body_len + 1); + if (notif->body != NULL) { + memcpy(notif->body, body, notif->body_len + 1); + } + } else { + notif->body = NULL; + notif->body_len = 0; + } + + if (icon) strncpy(notif->icon, icon, sizeof(notif->icon) - 1); + + notif->timeout = (timeout < 0) ? DEFAULT_TIMEOUT : timeout; + notif->urgency = NOTIFY_URGENCY_NORMAL; + notif->expire_time = (notif->timeout > 0) ? + get_time_ms() + notif->timeout : 0; + + /* Calculate size based on content */ + int notif_width, notif_height; + notification_calculate_size(summary, body, ¬if_width, ¬if_height); + notif->width = notif_width; + notif->height = notif_height; + + /* Create notification window */ + if (dwn != NULL && dwn->display != NULL) { + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = dwn->config->colors.notification_bg; + swa.event_mask = ExposureMask | ButtonPressMask; + + notif->window = XCreateWindow(dwn->display, dwn->root, + 0, 0, + notif->width, notif->height, + 1, + CopyFromParent, InputOutput, CopyFromParent, + CWOverrideRedirect | CWBackPixel | CWEventMask, + &swa); + + XSetWindowBorder(dwn->display, notif->window, + dwn->config->colors.border_focused); + } + + /* Add to list */ + notif->next = notification_list; + notification_list = notif; + + /* Position and show */ + notifications_position(); + notification_render(notif); + + if (notif->window != None) { + XMapRaised(dwn->display, notif->window); + } + + LOG_DEBUG("Notification %u (%dx%d): %s - %s", notif->id, + notif->width, notif->height, notif->summary, notif->body); + + return notif->id; +} + +void notification_close(uint32_t id) +{ + Notification *prev = NULL; + Notification *notif = notification_list; + + while (notif != NULL) { + if (notif->id == id) { + /* Remove from list */ + if (prev != NULL) { + prev->next = notif->next; + } else { + notification_list = notif->next; + } + + /* Destroy window */ + if (notif->window != None && dwn != NULL && dwn->display != NULL) { + XDestroyWindow(dwn->display, notif->window); + } + + /* Free dynamically allocated body */ + if (notif->body != NULL) { + dwn_free(notif->body); + notif->body = NULL; + } + + dwn_free(notif); + + /* Reposition remaining notifications */ + notifications_position(); + return; + } + + prev = notif; + notif = notif->next; + } +} + +void notification_close_all(void) +{ + while (notification_list != NULL) { + notification_close(notification_list->id); + } +} + +Notification *notification_find(uint32_t id) +{ + for (Notification *n = notification_list; n != NULL; n = n->next) { + if (n->id == id) { + return n; + } + } + return NULL; +} + +Notification *notification_find_by_window(Window window) +{ + for (Notification *n = notification_list; n != NULL; n = n->next) { + if (n->window == window) { + return n; + } + } + return NULL; +} + +/* ========== Rendering ========== */ + +void notification_render(Notification *notif) +{ + if (notif == NULL || notif->window == None) { + LOG_DEBUG("notification_render: notif or window is NULL"); + return; + } + + if (dwn == NULL || dwn->display == NULL) { + LOG_DEBUG("notification_render: dwn or display is NULL"); + return; + } + + if (dwn->xft_font == NULL && dwn->font == NULL) { + LOG_ERROR("notification_render: no font available - cannot render text"); + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + + if (colors == NULL) { + LOG_ERROR("notification_render: colors is NULL"); + return; + } + + LOG_DEBUG("Rendering notification: summary='%s', body_len=%zu", + notif->summary, notif->body_len); + + /* Set legacy font in GC if available (for fallback) */ + if (dwn->font != NULL) { + XSetFont(dpy, dwn->gc, dwn->font->fid); + } + + /* Clear background using dynamic size */ + XSetForeground(dpy, dwn->gc, colors->notification_bg); + XFillRectangle(dpy, notif->window, dwn->gc, 0, 0, + notif->width, notif->height); + + /* Draw summary (title) */ + int line_height = notif_line_height(); + int y = NOTIFICATION_PADDING + notif_font_ascent(); + notif_draw_text(notif->window, NOTIFICATION_PADDING, y, + notif->summary, strlen(notif->summary), + colors->notification_fg); + + /* Draw body - handle multiple lines */ + y += line_height + 4; + + int max_width = notif->width - 2 * NOTIFICATION_PADDING; + int max_y = notif->height - line_height - NOTIFICATION_PADDING; + + /* Only process body if it exists */ + if (notif->body != NULL && notif->body_len > 0) { + /* Dynamically allocate body copy for tokenization */ + char *body_copy = dwn_malloc(notif->body_len + 1); + if (body_copy != NULL) { + memcpy(body_copy, notif->body, notif->body_len + 1); + + /* Split by newlines and draw each line */ + char *line = body_copy; + char *next; + + while (line != NULL && *line != '\0' && y < max_y) { + /* Find next newline */ + next = strchr(line, '\n'); + if (next != NULL) { + *next = '\0'; + next++; + } + + /* Skip empty lines but count them for spacing */ + if (*line == '\0') { + y += line_height / 2; /* Half-height for empty lines */ + line = next; + continue; + } + + /* Word wrap long lines instead of truncating */ + size_t line_len = strlen(line); + const char *p = line; + + while (*p != '\0' && y < max_y) { + /* Find how much text fits on this line */ + size_t fit_len = 0; + size_t last_space = 0; + + for (size_t i = 0; p[i] != '\0'; i++) { + /* Skip UTF-8 continuation bytes for character counting */ + if ((p[i] & 0xC0) == 0x80) continue; + + int width = notif_text_width(p, i + 1); + if (width > max_width) { + break; + } + fit_len = i + 1; + + /* Track last space for word wrapping */ + if (p[i] == ' ') { + last_space = i; + } + } + + /* Adjust to include full UTF-8 characters */ + while (fit_len < line_len && (p[fit_len] & 0xC0) == 0x80) { + fit_len++; + } + + /* If we couldn't fit anything, force at least one character */ + if (fit_len == 0 && *p != '\0') { + fit_len = 1; + while (fit_len < line_len && (p[fit_len] & 0xC0) == 0x80) { + fit_len++; + } + } + + /* Prefer breaking at word boundary if possible */ + if (fit_len < strlen(p) && last_space > 0 && last_space > fit_len / 2) { + fit_len = last_space + 1; /* Include the space */ + } + + /* Draw this segment */ + if (fit_len > 0) { + char *segment = dwn_malloc(fit_len + 1); + if (segment != NULL) { + memcpy(segment, p, fit_len); + segment[fit_len] = '\0'; + + notif_draw_text(notif->window, NOTIFICATION_PADDING, y, + segment, strlen(segment), colors->panel_fg); + + dwn_free(segment); + } + + p += fit_len; + /* Skip leading spaces on new line */ + while (*p == ' ') p++; + + y += line_height; + } else { + break; + } + } + + line = next; + } + + dwn_free(body_copy); + } + } + + /* Draw app name at bottom */ + if (notif->app_name[0] != '\0') { + y = notif->height - NOTIFICATION_PADDING; + notif_draw_text(notif->window, NOTIFICATION_PADDING, y, + notif->app_name, strlen(notif->app_name), + colors->workspace_inactive); + } + + /* Force the drawing to be sent to X server */ + XFlush(dpy); +} + +void notifications_render_all(void) +{ + for (Notification *n = notification_list; n != NULL; n = n->next) { + notification_render(n); + } +} + +void notifications_update(void) +{ + long now = get_time_ms(); + + /* Check for expired notifications */ + Notification *notif = notification_list; + while (notif != NULL) { + Notification *next = notif->next; + + if (notif->expire_time > 0 && now >= notif->expire_time) { + notification_close(notif->id); + } + + notif = next; + } +} + +void notifications_position(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + int y = NOTIFICATION_MARGIN; + + /* Account for top panel */ + if (dwn->config && dwn->config->top_panel_enabled) { + y += config_get_panel_height(); + } + + for (Notification *n = notification_list; n != NULL; n = n->next) { + if (n->window != None) { + /* Position from right edge using notification's own width */ + int x = dwn->screen_width - n->width - NOTIFICATION_MARGIN; + XMoveWindow(dwn->display, n->window, x, y); + } + /* Stack using notification's own height */ + y += n->height + NOTIFICATION_MARGIN; + } +} + +void notifications_raise_all(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + /* Raise all notification windows to the top */ + for (Notification *n = notification_list; n != NULL; n = n->next) { + if (n->window != None) { + XRaiseWindow(dwn->display, n->window); + } + } +} + +/* ========== Server info ========== */ + +void notifications_get_server_info(const char **name, const char **vendor, + const char **version, const char **spec_version) +{ + if (name) *name = "DWN"; + if (vendor) *vendor = "DWN Project"; + if (version) *version = DWN_VERSION; + if (spec_version) *spec_version = "1.2"; +} + +void notifications_get_capabilities(const char ***caps, int *count) +{ + static const char *capabilities[] = { "body", "body-markup" }; + if (caps) *caps = capabilities; + if (count) *count = 2; +} diff --git a/src/panel.c b/src/panel.c new file mode 100644 index 0000000..b7ac118 --- /dev/null +++ b/src/panel.c @@ -0,0 +1,892 @@ +/* + * DWN - Desktop Window Manager + * Panel system implementation + */ + +#include "panel.h" +#include "workspace.h" +#include "layout.h" +#include "client.h" +#include "config.h" +#include "util.h" +#include "atoms.h" +#include "systray.h" +#include "news.h" + +#include +#include +#include +#include +#include +#include + +/* Panel padding and spacing */ +#define PANEL_PADDING 8 +#define WIDGET_SPACING 12 +#define WORKSPACE_WIDTH 28 +#define TASKBAR_ITEM_WIDTH 150 + +/* Clock format */ +#define CLOCK_FORMAT "%H:%M:%S" +#define DATE_FORMAT "%Y-%m-%d" + +/* Static clock buffer */ +static char clock_buffer[32] = ""; +static char date_buffer[32] = ""; + +/* System stats */ +typedef struct { + int cpu_percent; /* CPU usage percentage */ + int mem_percent; /* Memory usage percentage */ + int mem_used_mb; /* Memory used in MB */ + int mem_total_mb; /* Total memory in MB */ + float load_1min; /* 1-minute load average */ + float load_5min; /* 5-minute load average */ + float load_15min; /* 15-minute load average */ + /* CPU calculation state */ + unsigned long long prev_idle; + unsigned long long prev_total; +} SystemStats; + +static SystemStats sys_stats = {0}; + +/* Forward declarations */ +static void panel_render_system_stats(Panel *panel, int x, int *width); +static int panel_calculate_stats_width(void); + +/* ========== UTF-8 text helpers ========== */ + +/* Get text width using Xft (UTF-8 aware) */ +static int panel_text_width(const char *text, int len) +{ + if (text == NULL || dwn == NULL) return 0; + + if (dwn->xft_font != NULL) { + XGlyphInfo extents; + XftTextExtentsUtf8(dwn->display, dwn->xft_font, + (const FcChar8 *)text, len, &extents); + return extents.xOff; + } + + /* Fallback to legacy font */ + if (dwn->font != NULL) { + return XTextWidth(dwn->font, text, len); + } + + return 0; +} + +/* Draw text using Xft (UTF-8 aware) */ +static void panel_draw_text(Drawable d, int x, int y, const char *text, + int len, unsigned long color) +{ + if (text == NULL || dwn == NULL || dwn->display == NULL) return; + + /* Use Xft for UTF-8 support */ + if (dwn->xft_font != NULL) { + XftDraw *xft_draw = XftDrawCreate(dwn->display, d, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap); + if (xft_draw != NULL) { + XftColor xft_color; + XRenderColor render_color; + render_color.red = ((color >> 16) & 0xFF) * 257; + render_color.green = ((color >> 8) & 0xFF) * 257; + render_color.blue = (color & 0xFF) * 257; + render_color.alpha = 0xFFFF; + + XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &render_color, &xft_color); + + XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, + x, y, (const FcChar8 *)text, len); + + XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &xft_color); + XftDrawDestroy(xft_draw); + return; + } + } + + /* Fallback to legacy X11 text */ + XSetForeground(dwn->display, dwn->gc, color); + XDrawString(dwn->display, d, dwn->gc, x, y, text, len); +} + +/* Get text Y position for vertical centering */ +static int panel_text_y(int panel_height) +{ + if (dwn->xft_font != NULL) { + return (panel_height + dwn->xft_font->ascent) / 2; + } + if (dwn->font != NULL) { + return (panel_height + dwn->font->ascent - dwn->font->descent) / 2; + } + return panel_height / 2; +} + +/* ========== Panel creation/destruction ========== */ + +Panel *panel_create(PanelPosition position) +{ + if (dwn == NULL || dwn->display == NULL) { + return NULL; + } + + Panel *panel = dwn_calloc(1, sizeof(Panel)); + panel->position = position; + panel->width = dwn->screen_width; + panel->height = config_get_panel_height(); + panel->x = 0; + panel->visible = true; + + if (position == PANEL_TOP) { + panel->y = 0; + } else { + panel->y = dwn->screen_height - panel->height; + } + + /* Create panel window */ + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = dwn->config->colors.panel_bg; + swa.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask; + + panel->window = XCreateWindow(dwn->display, dwn->root, + panel->x, panel->y, + panel->width, panel->height, + 0, + CopyFromParent, InputOutput, CopyFromParent, + CWOverrideRedirect | CWBackPixel | CWEventMask, + &swa); + + /* Create double buffer */ + panel->buffer = XCreatePixmap(dwn->display, panel->window, + panel->width, panel->height, + DefaultDepth(dwn->display, dwn->screen)); + + /* Set EWMH strut to reserve space */ + long strut[4] = { 0, 0, 0, 0 }; + if (position == PANEL_TOP) { + strut[2] = panel->height; + } else { + strut[3] = panel->height; + } + + XChangeProperty(dwn->display, panel->window, ewmh.NET_WM_STRUT, + XA_CARDINAL, 32, PropModeReplace, + (unsigned char *)strut, 4); + + LOG_DEBUG("Created %s panel at y=%d", + position == PANEL_TOP ? "top" : "bottom", panel->y); + + return panel; +} + +void panel_destroy(Panel *panel) +{ + if (panel == NULL) { + return; + } + + if (panel->buffer != None) { + XFreePixmap(dwn->display, panel->buffer); + } + + if (panel->window != None) { + XDestroyWindow(dwn->display, panel->window); + } + + dwn_free(panel); +} + +void panels_init(void) +{ + if (dwn == NULL || dwn->config == NULL) { + return; + } + + if (dwn->config->top_panel_enabled) { + dwn->top_panel = panel_create(PANEL_TOP); + if (dwn->top_panel != NULL) { + XMapRaised(dwn->display, dwn->top_panel->window); + } + } + + if (dwn->config->bottom_panel_enabled) { + dwn->bottom_panel = panel_create(PANEL_BOTTOM); + if (dwn->bottom_panel != NULL) { + XMapRaised(dwn->display, dwn->bottom_panel->window); + } + } + + /* Initial clock and stats update */ + panel_update_clock(); + panel_update_system_stats(); + + LOG_INFO("Panels initialized"); +} + +void panels_cleanup(void) +{ + if (dwn->top_panel != NULL) { + panel_destroy(dwn->top_panel); + dwn->top_panel = NULL; + } + + if (dwn->bottom_panel != NULL) { + panel_destroy(dwn->bottom_panel); + dwn->bottom_panel = NULL; + } +} + +/* ========== Panel rendering ========== */ + +void panel_render(Panel *panel) +{ + if (panel == NULL || !panel->visible) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + + /* Clear buffer with background color */ + XSetForeground(dpy, dwn->gc, colors->panel_bg); + XFillRectangle(dpy, panel->buffer, dwn->gc, 0, 0, panel->width, panel->height); + + int x = PANEL_PADDING; + int width; + + if (panel->position == PANEL_TOP) { + /* Top panel: workspaces, layout indicator, taskbar, systray */ + + /* Workspace indicators */ + panel_render_workspaces(panel, x, &width); + x += width + WIDGET_SPACING; + + /* Layout indicator */ + panel_render_layout_indicator(panel, x, &width); + x += width + WIDGET_SPACING; + + /* Taskbar (takes remaining space, but leave room for systray) */ + panel_render_taskbar(panel, x, &width); + + /* System tray (right side) - WiFi, Audio, etc. */ + int systray_actual_width = systray_get_width(); + int systray_x = panel->width - systray_actual_width - PANEL_PADDING; + systray_render(panel, systray_x, &width); + + /* AI status (left of systray) */ + if (dwn->ai_enabled) { + int ai_x = systray_x - 60; + panel_render_ai_status(panel, ai_x, &width); + } + } else { + /* Bottom panel: date (left), news ticker (center), system stats + clock (right) */ + + /* Date (left side) */ + int date_width = 0; + if (dwn->xft_font != NULL || dwn->font != NULL) { + int text_y = panel_text_y(panel->height); + panel_draw_text(panel->buffer, x, text_y, date_buffer, + strlen(date_buffer), colors->panel_fg); + date_width = panel_text_width(date_buffer, strlen(date_buffer)); + } + + /* Calculate positions from right edge */ + int clock_width = panel_text_width(clock_buffer, strlen(clock_buffer)); + int stats_width = panel_calculate_stats_width(); + + /* Clock at rightmost position */ + int clock_x = panel->width - clock_width - PANEL_PADDING; + panel_render_clock(panel, clock_x, &width); + + /* Stats immediately left of clock */ + int stats_x = clock_x - stats_width - WIDGET_SPACING; + panel_render_system_stats(panel, stats_x, &width); + + /* News ticker between date and stats */ + int news_start = PANEL_PADDING + date_width + WIDGET_SPACING * 2; + int news_max_width = stats_x - news_start - WIDGET_SPACING; + if (news_max_width > 100) { /* Only show if there's reasonable space */ + int news_width = 0; + news_render(panel, news_start, news_max_width, &news_width); + } + } + + /* Copy buffer to window */ + XCopyArea(dpy, panel->buffer, panel->window, dwn->gc, + 0, 0, panel->width, panel->height, 0, 0); + + XFlush(dpy); +} + +void panel_render_all(void) +{ + if (dwn->top_panel != NULL) { + panel_render(dwn->top_panel); + } + if (dwn->bottom_panel != NULL) { + panel_render(dwn->bottom_panel); + } +} + +void panel_render_workspaces(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + int text_y = panel_text_y(panel->height); + + *width = 0; + + for (int i = 0; i < MAX_WORKSPACES; i++) { + bool active = (i == dwn->current_workspace); + bool has_clients = !workspace_is_empty(i); + + /* Background */ + unsigned long bg = active ? colors->workspace_active : colors->panel_bg; + XSetForeground(dpy, dwn->gc, bg); + XFillRectangle(dpy, panel->buffer, dwn->gc, + x + i * WORKSPACE_WIDTH, 2, + WORKSPACE_WIDTH - 2, panel->height - 4); + + /* Workspace number */ + char num[4]; + snprintf(num, sizeof(num), "%d", i + 1); + + unsigned long fg = active ? colors->panel_bg : + (has_clients ? colors->panel_fg : colors->workspace_inactive); + + int text_x = x + i * WORKSPACE_WIDTH + (WORKSPACE_WIDTH - panel_text_width(num, strlen(num))) / 2; + panel_draw_text(panel->buffer, text_x, text_y, num, strlen(num), fg); + } + + *width = MAX_WORKSPACES * WORKSPACE_WIDTH; +} + +void panel_render_taskbar(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + int text_y = panel_text_y(panel->height); + + int current_x = x; + int available_width = panel->width - x - 100 - PANEL_PADDING; + int item_count = 0; + + /* Count visible clients */ + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) { + item_count++; + } + } + + if (item_count == 0) { + *width = 0; + return; + } + + int item_width = available_width / item_count; + if (item_width > TASKBAR_ITEM_WIDTH) { + item_width = TASKBAR_ITEM_WIDTH; + } + + /* Render each client */ + Workspace *ws = workspace_get_current(); + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace != (unsigned int)dwn->current_workspace || client_is_minimized(c)) { + continue; + } + + bool focused = (ws != NULL && ws->focused == c); + + /* Background */ + unsigned long bg = focused ? colors->workspace_active : colors->panel_bg; + XSetForeground(dpy, dwn->gc, bg); + XFillRectangle(dpy, panel->buffer, dwn->gc, + current_x, 2, item_width - 2, panel->height - 4); + + /* Title - leave room for "..." (4 bytes including null) */ + char title[64]; + strncpy(title, c->title, sizeof(title) - 4); /* Leave room for "..." */ + title[sizeof(title) - 4] = '\0'; + + /* Truncate UTF-8 aware if necessary */ + int max_text_width = item_width - 8; + bool truncated = false; + while (panel_text_width(title, strlen(title)) > max_text_width && strlen(title) > 3) { + size_t len = strlen(title); + /* Move back to find UTF-8 character boundary */ + size_t cut = len - 1; + while (cut > 0 && (title[cut] & 0xC0) == 0x80) { + cut--; /* Skip continuation bytes */ + } + if (cut > 0) cut--; + while (cut > 0 && (title[cut] & 0xC0) == 0x80) { + cut--; + } + /* Ensure we have room for "..." */ + if (cut > sizeof(title) - 4) { + cut = sizeof(title) - 4; + } + title[cut] = '\0'; + truncated = true; + } + if (truncated) { + strncat(title, "...", sizeof(title) - strlen(title) - 1); + } + + unsigned long fg = focused ? colors->panel_bg : colors->panel_fg; + panel_draw_text(panel->buffer, current_x + 4, text_y, title, strlen(title), fg); + + current_x += item_width; + } + + *width = current_x - x; +} + +void panel_render_clock(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + return; + } + if (dwn->xft_font == NULL && dwn->font == NULL) { + return; + } + + const ColorScheme *colors = config_get_colors(); + int text_y = panel_text_y(panel->height); + + panel_draw_text(panel->buffer, x, text_y, clock_buffer, + strlen(clock_buffer), colors->panel_fg); + + *width = panel_text_width(clock_buffer, strlen(clock_buffer)); +} + +void panel_render_systray(Panel *panel, int x, int *width) +{ + /* System tray placeholder - actual implementation requires + handling _NET_SYSTEM_TRAY protocol */ + (void)panel; + (void)x; + *width = 0; +} + +void panel_render_layout_indicator(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + return; + } + if (dwn->xft_font == NULL && dwn->font == NULL) { + return; + } + + const ColorScheme *colors = config_get_colors(); + int text_y = panel_text_y(panel->height); + + Workspace *ws = workspace_get_current(); + const char *symbol = layout_get_symbol(ws != NULL ? ws->layout : LAYOUT_TILING); + + panel_draw_text(panel->buffer, x, text_y, symbol, strlen(symbol), + colors->workspace_active); + + *width = panel_text_width(symbol, strlen(symbol)); +} + +void panel_render_ai_status(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + return; + } + if (dwn->xft_font == NULL && dwn->font == NULL) { + return; + } + + const ColorScheme *colors = config_get_colors(); + int text_y = panel_text_y(panel->height); + + const char *status = dwn->ai_enabled ? "[AI]" : ""; + + panel_draw_text(panel->buffer, x, text_y, status, strlen(status), + colors->workspace_active); + + *width = panel_text_width(status, strlen(status)); +} + +/* ========== Panel interaction ========== */ + +void panel_handle_click(Panel *panel, int x, int y, int button) +{ + if (panel == NULL) { + return; + } + + if (panel->position == PANEL_TOP) { + /* Check systray click first (right side) */ + int systray_actual_width = systray_get_width(); + int systray_start = panel->width - systray_actual_width - PANEL_PADDING; + if (x >= systray_start) { + systray_handle_click(x, y, button); + return; + } + + /* Check workspace click */ + int ws = panel_hit_test_workspace(panel, x, y); + if (ws >= 0 && ws < MAX_WORKSPACES) { + if (button == 1) { /* Left click */ + workspace_switch(ws); + } + return; + } + + /* Check taskbar click */ + Client *c = panel_hit_test_taskbar(panel, x, y); + if (c != NULL) { + if (button == 1) { + client_focus(c); + } else if (button == 3) { /* Right click */ + client_close(c); + } + return; + } + } else if (panel->position == PANEL_BOTTOM) { + /* Bottom panel - click on news opens article in browser */ + if (button == 1) { + news_handle_click(x, y); + } + } +} + +int panel_hit_test_workspace(Panel *panel, int x, int y) +{ + (void)y; + + if (panel == NULL || panel->position != PANEL_TOP) { + return -1; + } + + if (x < PANEL_PADDING || x >= PANEL_PADDING + MAX_WORKSPACES * WORKSPACE_WIDTH) { + return -1; + } + + return (x - PANEL_PADDING) / WORKSPACE_WIDTH; +} + +Client *panel_hit_test_taskbar(Panel *panel, int x, int y) +{ + (void)y; + + if (panel == NULL || panel->position != PANEL_TOP) { + return NULL; + } + + int taskbar_start = PANEL_PADDING + MAX_WORKSPACES * WORKSPACE_WIDTH + WIDGET_SPACING + 50; + if (x < taskbar_start) { + return NULL; + } + + int available_width = panel->width - taskbar_start - 100 - PANEL_PADDING; + int item_count = 0; + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) { + item_count++; + } + } + + if (item_count == 0) { + return NULL; + } + + int item_width = available_width / item_count; + if (item_width > TASKBAR_ITEM_WIDTH) { + item_width = TASKBAR_ITEM_WIDTH; + } + + int index = (x - taskbar_start) / item_width; + int i = 0; + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) { + if (i == index) { + return c; + } + i++; + } + } + + return NULL; +} + +/* ========== Panel visibility ========== */ + +void panel_show(Panel *panel) +{ + if (panel == NULL) { + return; + } + + panel->visible = true; + XMapRaised(dwn->display, panel->window); + panel_render(panel); +} + +void panel_hide(Panel *panel) +{ + if (panel == NULL) { + return; + } + + panel->visible = false; + XUnmapWindow(dwn->display, panel->window); +} + +void panel_toggle(Panel *panel) +{ + if (panel == NULL) { + return; + } + + if (panel->visible) { + panel_hide(panel); + } else { + panel_show(panel); + } +} + +/* ========== Clock updates ========== */ + +void panel_update_clock(void) +{ + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + + strftime(clock_buffer, sizeof(clock_buffer), CLOCK_FORMAT, tm_info); + strftime(date_buffer, sizeof(date_buffer), DATE_FORMAT, tm_info); +} + +/* ========== System stats ========== */ + +void panel_update_system_stats(void) +{ + FILE *fp; + char line[256]; + + /* Read CPU stats from /proc/stat */ + fp = fopen("/proc/stat", "r"); + if (fp != NULL) { + if (fgets(line, sizeof(line), fp) != NULL) { + unsigned long long user, nice, system, idle, iowait, irq, softirq; + if (sscanf(line, "cpu %llu %llu %llu %llu %llu %llu %llu", + &user, &nice, &system, &idle, &iowait, &irq, &softirq) >= 4) { + unsigned long long total = user + nice + system + idle + iowait + irq + softirq; + unsigned long long idle_time = idle + iowait; + + if (sys_stats.prev_total > 0) { + unsigned long long total_diff = total - sys_stats.prev_total; + unsigned long long idle_diff = idle_time - sys_stats.prev_idle; + if (total_diff > 0) { + sys_stats.cpu_percent = (int)(100 * (total_diff - idle_diff) / total_diff); + } + } + + sys_stats.prev_total = total; + sys_stats.prev_idle = idle_time; + } + } + fclose(fp); + } + + /* Read memory stats from /proc/meminfo */ + fp = fopen("/proc/meminfo", "r"); + if (fp != NULL) { + unsigned long mem_total = 0, mem_free = 0, buffers = 0, cached = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + if (strncmp(line, "MemTotal:", 9) == 0) { + sscanf(line + 9, " %lu", &mem_total); + } else if (strncmp(line, "MemFree:", 8) == 0) { + sscanf(line + 8, " %lu", &mem_free); + } else if (strncmp(line, "Buffers:", 8) == 0) { + sscanf(line + 8, " %lu", &buffers); + } else if (strncmp(line, "Cached:", 7) == 0) { + sscanf(line + 7, " %lu", &cached); + break; /* Got all we need */ + } + } + fclose(fp); + + if (mem_total > 0) { + unsigned long used = mem_total - mem_free - buffers - cached; + sys_stats.mem_total_mb = (int)(mem_total / 1024); + sys_stats.mem_used_mb = (int)(used / 1024); + sys_stats.mem_percent = (int)(100 * used / mem_total); + } + } + + /* Read load average from /proc/loadavg */ + fp = fopen("/proc/loadavg", "r"); + if (fp != NULL) { + if (fscanf(fp, "%f %f %f", + &sys_stats.load_1min, + &sys_stats.load_5min, + &sys_stats.load_15min) != 3) { + sys_stats.load_1min = 0; + sys_stats.load_5min = 0; + sys_stats.load_15min = 0; + } + fclose(fp); + } +} + +/* Calculate stats width without rendering */ +static int panel_calculate_stats_width(void) +{ + if (dwn->xft_font == NULL && dwn->font == NULL) return 0; + + char buf[256]; + int total = 0; + + /* CPU */ + int len = snprintf(buf, sizeof(buf), "CPU:%2d%%", sys_stats.cpu_percent); + total += panel_text_width(buf, len) + WIDGET_SPACING; + + /* Memory */ + if (sys_stats.mem_total_mb >= 1024) { + len = snprintf(buf, sizeof(buf), "MEM:%.1fG/%dG", + sys_stats.mem_used_mb / 1024.0f, sys_stats.mem_total_mb / 1024); + } else { + len = snprintf(buf, sizeof(buf), "MEM:%dM/%dM", + sys_stats.mem_used_mb, sys_stats.mem_total_mb); + } + total += panel_text_width(buf, len) + WIDGET_SPACING; + + /* Load */ + len = snprintf(buf, sizeof(buf), "Load:%.2f %.2f %.2f", + sys_stats.load_1min, sys_stats.load_5min, sys_stats.load_15min); + total += panel_text_width(buf, len) + WIDGET_SPACING; + + /* Battery - use thread-safe snapshot */ + BatteryState bat_snap = systray_get_battery_snapshot(); + if (bat_snap.present) { + const char *bat_icon = bat_snap.charging ? "[+]" : "[=]"; + len = snprintf(buf, sizeof(buf), "%s%d%%", bat_icon, bat_snap.percentage); + total += panel_text_width(buf, len) + WIDGET_SPACING; + } + + return total; +} + +static void panel_render_system_stats(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + *width = 0; + return; + } + if (dwn->xft_font == NULL && dwn->font == NULL) { + *width = 0; + return; + } + + const ColorScheme *colors = config_get_colors(); + int text_y = panel_text_y(panel->height); + int current_x = x; + + /* Format: "CPU: 25% | MEM: 4.2G/16G | Load: 1.23 0.98 0.76 | BAT: 85%" */ + + char stats_buf[256]; + int len; + + /* CPU with color coding */ + unsigned long cpu_color = colors->panel_fg; + if (sys_stats.cpu_percent >= 90) { + cpu_color = colors->workspace_urgent; /* Red for high CPU */ + } else if (sys_stats.cpu_percent >= 70) { + cpu_color = 0xFFA500; /* Orange for medium-high */ + } + + len = snprintf(stats_buf, sizeof(stats_buf), "CPU:%2d%%", sys_stats.cpu_percent); + panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, cpu_color); + current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING; + + /* Memory */ + unsigned long mem_color = colors->panel_fg; + if (sys_stats.mem_percent >= 90) { + mem_color = colors->workspace_urgent; + } else if (sys_stats.mem_percent >= 75) { + mem_color = 0xFFA500; + } + + if (sys_stats.mem_total_mb >= 1024) { + len = snprintf(stats_buf, sizeof(stats_buf), "MEM:%.1fG/%dG", + sys_stats.mem_used_mb / 1024.0f, sys_stats.mem_total_mb / 1024); + } else { + len = snprintf(stats_buf, sizeof(stats_buf), "MEM:%dM/%dM", + sys_stats.mem_used_mb, sys_stats.mem_total_mb); + } + panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, mem_color); + current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING; + + /* Load average */ + unsigned long load_color = colors->panel_fg; + if (sys_stats.load_1min >= 4.0f) { + load_color = colors->workspace_urgent; + } else if (sys_stats.load_1min >= 2.0f) { + load_color = 0xFFA500; + } + + len = snprintf(stats_buf, sizeof(stats_buf), "Load:%.2f %.2f %.2f", + sys_stats.load_1min, sys_stats.load_5min, sys_stats.load_15min); + panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, load_color); + current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING; + + /* Battery (if present) - use thread-safe snapshot */ + BatteryState bat_snap = systray_get_battery_snapshot(); + if (bat_snap.present) { + unsigned long bat_color = colors->panel_fg; + if (bat_snap.percentage <= 20 && !bat_snap.charging) { + bat_color = colors->workspace_urgent; /* Red for low */ + } else if (bat_snap.percentage <= 40 && !bat_snap.charging) { + bat_color = 0xFFA500; /* Orange for medium-low */ + } else if (bat_snap.charging) { + bat_color = colors->workspace_active; /* Blue for charging */ + } + + const char *bat_icon = bat_snap.charging ? "[+]" : "[=]"; + len = snprintf(stats_buf, sizeof(stats_buf), "%s%d%%", bat_icon, bat_snap.percentage); + panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, bat_color); + current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING; + } + + *width = current_x - x; +} + +/* ========== System tray ========== */ + +void panel_init_systray(void) +{ + /* System tray initialization - requires _NET_SYSTEM_TRAY protocol */ + LOG_DEBUG("System tray initialization (placeholder)"); +} + +void panel_add_systray_icon(Window icon) +{ + (void)icon; + LOG_DEBUG("Add systray icon (placeholder)"); +} + +void panel_remove_systray_icon(Window icon) +{ + (void)icon; + LOG_DEBUG("Remove systray icon (placeholder)"); +} diff --git a/src/systray.c b/src/systray.c new file mode 100644 index 0000000..9f39b61 --- /dev/null +++ b/src/systray.c @@ -0,0 +1,1105 @@ +/* + * DWN - Desktop Window Manager + * System tray widgets implementation with UTF-8 support + */ + +#include "systray.h" +#include "panel.h" +#include "config.h" +#include "util.h" +#include "notifications.h" + +#include +#include +#include +#include +#include +#include + +/* UTF-8 Icons for system tray */ +#define ICON_WIFI_FULL "\xE2\x96\x82\xE2\x96\x84\xE2\x96\x86\xE2\x96\x88" /* Full signal bars */ +#define ICON_WIFI_OFF "\xE2\x9C\x97" /* X mark */ +#define ICON_VOLUME_HIGH "\xF0\x9F\x94\x8A" /* Speaker high */ +#define ICON_VOLUME_MED "\xF0\x9F\x94\x89" /* Speaker medium */ +#define ICON_VOLUME_LOW "\xF0\x9F\x94\x88" /* Speaker low */ +#define ICON_VOLUME_MUTE "\xF0\x9F\x94\x87" /* Speaker muted */ +#define ICON_BATTERY_FULL "\xF0\x9F\x94\x8B" /* Battery full */ +#define ICON_BATTERY_CHARGE "\xE2\x9A\xA1" /* Lightning bolt */ + +/* Simple ASCII fallback icons */ +#define ASCII_WIFI_ON "W" +#define ASCII_WIFI_OFF "-" +#define ASCII_VOL_HIGH "V" +#define ASCII_VOL_MUTE "M" +#define ASCII_BATTERY "B" +#define ASCII_CHARGING "+" + +/* Widget dimensions */ +#define SYSTRAY_SPACING 12 +#define DROPDOWN_ITEM_HEIGHT 28 +#define DROPDOWN_WIDTH 400 +#define DROPDOWN_PADDING 8 +#define SLIDER_WIDTH 30 +#define SLIDER_HEIGHT 120 +#define SLIDER_PADDING 8 +#define SLIDER_KNOB_HEIGHT 8 + +/* Scan interval in milliseconds */ +#define WIFI_SCAN_INTERVAL 10000 + +/* Global state */ +WifiState wifi_state = {0}; +AudioState audio_state = {0}; +BatteryState battery_state = {0}; +DropdownMenu *wifi_menu = NULL; +VolumeSlider *volume_slider = NULL; + +/* Async update thread */ +static pthread_t update_thread; +static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER; +static volatile int thread_running = 0; + +/* Systray position tracking */ +static int systray_x = 0; +static int systray_width = 0; +static int wifi_icon_x = 0; +static int audio_icon_x = 0; + +/* Forward declarations for internal update functions */ +static void wifi_update_state_internal(void); +static void audio_update_state_internal(void); + +/* ========== UTF-8 Text Drawing ========== */ + +static void draw_utf8_text(Drawable d, int x, int y, const char *text, unsigned long color) +{ + if (dwn == NULL || dwn->display == NULL || text == NULL) { + return; + } + + if (dwn->xft_font != NULL) { + /* Use Xft for proper UTF-8 rendering */ + XftDraw *xft_draw = XftDrawCreate(dwn->display, d, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap); + if (xft_draw != NULL) { + XftColor xft_color; + XRenderColor render_color; + + /* Convert X11 color to XRender color */ + render_color.red = ((color >> 16) & 0xFF) * 257; + render_color.green = ((color >> 8) & 0xFF) * 257; + render_color.blue = (color & 0xFF) * 257; + render_color.alpha = 0xFFFF; + + XftColorAllocValue(dwn->display, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &render_color, &xft_color); + + XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, + x, y, (const FcChar8 *)text, strlen(text)); + + XftColorFree(dwn->display, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &xft_color); + XftDrawDestroy(xft_draw); + return; + } + } + + /* Fallback to basic X11 drawing */ + XSetForeground(dwn->display, dwn->gc, color); + XDrawString(dwn->display, d, dwn->gc, x, y, text, strlen(text)); +} + +static int get_text_width(const char *text) +{ + if (dwn == NULL || text == NULL) { + return 0; + } + + if (dwn->xft_font != NULL) { + XGlyphInfo extents; + XftTextExtentsUtf8(dwn->display, dwn->xft_font, + (const FcChar8 *)text, strlen(text), &extents); + return extents.xOff; + } + + if (dwn->font != NULL) { + return XTextWidth(dwn->font, text, strlen(text)); + } + + return strlen(text) * 8; /* Rough estimate */ +} + +/* ========== Initialization ========== */ + +/* Background thread for async state updates */ +static void *systray_update_thread(void *arg) +{ + (void)arg; + + while (thread_running) { + /* Update WiFi state (slow - uses popen) */ + wifi_update_state_internal(); + + /* Update audio state (slow - uses popen) */ + audio_update_state_internal(); + + /* Update battery state (fast - reads /sys files) */ + pthread_mutex_lock(&state_mutex); + battery_update_state(); + pthread_mutex_unlock(&state_mutex); + + /* Sleep for 2 seconds between updates */ + for (int i = 0; i < 20 && thread_running; i++) { + usleep(100000); /* 100ms chunks for responsive shutdown */ + } + } + + return NULL; +} + +void systray_init(void) +{ + memset(&wifi_state, 0, sizeof(wifi_state)); + memset(&audio_state, 0, sizeof(audio_state)); + memset(&battery_state, 0, sizeof(battery_state)); + + /* Set default states for instant display */ + audio_state.volume = 50; + wifi_state.enabled = true; + + /* Start background update thread - all updates happen async */ + thread_running = 1; + if (pthread_create(&update_thread, NULL, systray_update_thread, NULL) != 0) { + LOG_WARN("Failed to create systray update thread"); + thread_running = 0; + } + + LOG_INFO("System tray initialized"); +} + +void systray_cleanup(void) +{ + /* Stop the update thread */ + if (thread_running) { + thread_running = 0; + pthread_join(update_thread, NULL); + } + + if (wifi_menu != NULL) { + dropdown_destroy(wifi_menu); + wifi_menu = NULL; + } + if (volume_slider != NULL) { + volume_slider_destroy(volume_slider); + volume_slider = NULL; + } +} + +/* ========== Battery Functions ========== */ + +void battery_update_state(void) +{ + DIR *dir; + struct dirent *entry; + char path[512]; + char value[64]; + FILE *fp; + + battery_state.present = false; + battery_state.charging = false; + battery_state.percentage = 0; + + /* Look for battery in /sys/class/power_supply */ + dir = opendir("/sys/class/power_supply"); + if (dir == NULL) { + return; + } + + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + /* Check if this is a battery (not AC adapter) */ + snprintf(path, sizeof(path), "/sys/class/power_supply/%s/type", entry->d_name); + fp = fopen(path, "r"); + if (fp != NULL) { + if (fgets(value, sizeof(value), fp) != NULL) { + value[strcspn(value, "\n")] = '\0'; + if (strcmp(value, "Battery") == 0) { + battery_state.present = true; + + /* Get capacity */ + snprintf(path, sizeof(path), "/sys/class/power_supply/%s/capacity", entry->d_name); + FILE *cap_fp = fopen(path, "r"); + if (cap_fp != NULL) { + if (fgets(value, sizeof(value), cap_fp) != NULL) { + battery_state.percentage = atoi(value); + } + fclose(cap_fp); + } + + /* Get status (charging/discharging) */ + snprintf(path, sizeof(path), "/sys/class/power_supply/%s/status", entry->d_name); + FILE *status_fp = fopen(path, "r"); + if (status_fp != NULL) { + if (fgets(value, sizeof(value), status_fp) != NULL) { + value[strcspn(value, "\n")] = '\0'; + battery_state.charging = (strcmp(value, "Charging") == 0 || + strcmp(value, "Full") == 0); + } + fclose(status_fp); + } + + fclose(fp); + break; /* Found a battery, stop searching */ + } + } + fclose(fp); + } + } + closedir(dir); +} + +const char *battery_get_icon(void) +{ + if (!battery_state.present) { + return ""; /* No battery = no icon */ + } + + if (battery_state.charging) { + return ASCII_CHARGING; + } + + return ASCII_BATTERY; +} + +/* ========== WiFi Functions ========== */ + +/* Internal update function - called from background thread */ +static void wifi_update_state_internal(void) +{ + FILE *fp; + char line[256]; + WifiState temp_state = {0}; + + /* Check if WiFi is connected and get current SSID */ + fp = popen("nmcli -t -f GENERAL.STATE,GENERAL.CONNECTION dev show 2>/dev/null | head -2", "r"); + if (fp != NULL) { + while (fgets(line, sizeof(line), fp) != NULL) { + line[strcspn(line, "\n")] = '\0'; + + if (strstr(line, "GENERAL.STATE:") != NULL) { + if (strstr(line, "100 (connected)") != NULL) { + temp_state.connected = true; + temp_state.enabled = true; + } + } + if (strstr(line, "GENERAL.CONNECTION:") != NULL) { + char *ssid = strchr(line, ':'); + if (ssid != NULL && strlen(ssid) > 1) { + ssid++; + strncpy(temp_state.current_ssid, ssid, sizeof(temp_state.current_ssid) - 1); + } + } + } + pclose(fp); + } + + /* Get signal strength if connected */ + if (temp_state.connected) { + fp = popen("nmcli -t -f IN-USE,SIGNAL dev wifi list 2>/dev/null | grep '^\\*' | cut -d: -f2", "r"); + if (fp != NULL) { + if (fgets(line, sizeof(line), fp) != NULL) { + temp_state.signal_strength = atoi(line); + } + pclose(fp); + } + } + + /* Copy to global state with mutex protection */ + pthread_mutex_lock(&state_mutex); + wifi_state.connected = temp_state.connected; + wifi_state.enabled = temp_state.enabled; + wifi_state.signal_strength = temp_state.signal_strength; + memcpy(wifi_state.current_ssid, temp_state.current_ssid, sizeof(wifi_state.current_ssid)); + pthread_mutex_unlock(&state_mutex); +} + +/* Public function - now just reads cached state (non-blocking) */ +void wifi_update_state(void) +{ + /* State is updated by background thread - this is now a no-op */ + /* Kept for API compatibility */ +} + +void wifi_scan_networks(void) +{ + FILE *fp; + char line[512]; + + /* Build network list in temporary storage first */ + WifiNetwork temp_networks[MAX_WIFI_NETWORKS]; + int temp_count = 0; + long scan_time = get_time_ms(); + + fp = popen("nmcli -t -f SSID,SIGNAL,SECURITY,IN-USE dev wifi list 2>/dev/null", "r"); + if (fp == NULL) { + LOG_WARN("Failed to scan WiFi networks"); + return; + } + + while (fgets(line, sizeof(line), fp) != NULL && temp_count < MAX_WIFI_NETWORKS) { + line[strcspn(line, "\n")] = '\0'; + + char *ssid = strtok(line, ":"); + char *signal = strtok(NULL, ":"); + char *security = strtok(NULL, ":"); + char *in_use = strtok(NULL, ":"); + + if (ssid != NULL && strlen(ssid) > 0) { + WifiNetwork *net = &temp_networks[temp_count]; + strncpy(net->ssid, ssid, sizeof(net->ssid) - 1); + net->ssid[sizeof(net->ssid) - 1] = '\0'; + net->signal = signal ? atoi(signal) : 0; + strncpy(net->security, security ? security : "", sizeof(net->security) - 1); + net->security[sizeof(net->security) - 1] = '\0'; + net->connected = (in_use != NULL && in_use[0] == '*'); + temp_count++; + } + } + pclose(fp); + + /* Copy to global state under lock */ + pthread_mutex_lock(&state_mutex); + memcpy(wifi_state.networks, temp_networks, sizeof(temp_networks)); + wifi_state.network_count = temp_count; + wifi_state.last_scan = scan_time; + pthread_mutex_unlock(&state_mutex); + + LOG_DEBUG("Scanned %d WiFi networks", temp_count); +} + +void wifi_connect(const char *ssid) +{ + if (ssid == NULL || strlen(ssid) == 0) { + return; + } + + char cmd[256]; + snprintf(cmd, sizeof(cmd), "nmcli dev wifi connect \"%s\" &", ssid); + + char msg[128]; + snprintf(msg, sizeof(msg), "Connecting to %s...", ssid); + notification_show("WiFi", "Connecting", msg, NULL, 3000); + + spawn_async(cmd); + + pthread_mutex_lock(&state_mutex); + wifi_state.connected = false; + pthread_mutex_unlock(&state_mutex); +} + +void wifi_disconnect(void) +{ + spawn_async("nmcli dev disconnect wlan0 &"); + + pthread_mutex_lock(&state_mutex); + wifi_state.connected = false; + pthread_mutex_unlock(&state_mutex); + + notification_show("WiFi", "Disconnected", "WiFi connection closed", NULL, 2000); +} + +const char *wifi_get_icon(void) +{ + if (!wifi_state.enabled || !wifi_state.connected) { + return ASCII_WIFI_OFF; + } + return ASCII_WIFI_ON; +} + +/* ========== Audio Functions ========== */ + +/* Internal update function - called from background thread */ +static void audio_update_state_internal(void) +{ + FILE *fp; + char line[256]; + int volume = 50; + bool muted = false; + + fp = popen("amixer get Master 2>/dev/null | grep -o '[0-9]*%\\|\\[on\\]\\|\\[off\\]' | head -2", "r"); + if (fp != NULL) { + while (fgets(line, sizeof(line), fp) != NULL) { + line[strcspn(line, "\n")] = '\0'; + + if (strchr(line, '%') != NULL) { + volume = atoi(line); + } else if (strcmp(line, "[off]") == 0) { + muted = true; + } else if (strcmp(line, "[on]") == 0) { + muted = false; + } + } + pclose(fp); + } + + /* Copy to global state with mutex protection */ + pthread_mutex_lock(&state_mutex); + audio_state.volume = volume; + audio_state.muted = muted; + pthread_mutex_unlock(&state_mutex); +} + +/* Public function - now just reads cached state (non-blocking) */ +void audio_update_state(void) +{ + /* State is updated by background thread - this is now a no-op */ + /* Kept for API compatibility */ +} + +void audio_set_volume(int volume) +{ + if (volume < 0) volume = 0; + if (volume > 100) volume = 100; + + char cmd[64]; + snprintf(cmd, sizeof(cmd), "amixer set Master %d%% >/dev/null 2>&1 &", volume); + spawn_async(cmd); + + pthread_mutex_lock(&state_mutex); + audio_state.volume = volume; + audio_state.muted = false; + pthread_mutex_unlock(&state_mutex); +} + +void audio_toggle_mute(void) +{ + spawn_async("amixer set Master toggle >/dev/null 2>&1 &"); + + pthread_mutex_lock(&state_mutex); + audio_state.muted = !audio_state.muted; + pthread_mutex_unlock(&state_mutex); +} + +const char *audio_get_icon(void) +{ + if (audio_state.muted || audio_state.volume == 0) { + return ASCII_VOL_MUTE; + } + return ASCII_VOL_HIGH; +} + +/* ========== Volume Slider ========== */ + +VolumeSlider *volume_slider_create(int x, int y) +{ + if (dwn == NULL || dwn->display == NULL) { + return NULL; + } + + VolumeSlider *slider = dwn_calloc(1, sizeof(VolumeSlider)); + slider->x = x; + slider->y = y; + slider->width = SLIDER_WIDTH; + slider->height = SLIDER_HEIGHT; + slider->visible = false; + slider->dragging = false; + + return slider; +} + +void volume_slider_destroy(VolumeSlider *slider) +{ + if (slider == NULL) { + return; + } + + if (slider->window != None && dwn != NULL && dwn->display != NULL) { + XDestroyWindow(dwn->display, slider->window); + } + + dwn_free(slider); +} + +void volume_slider_show(VolumeSlider *slider) +{ + if (slider == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (slider->window == None) { + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = dwn->config->colors.panel_bg; + swa.border_pixel = dwn->config->colors.border_focused; + swa.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask | + PointerMotionMask | LeaveWindowMask; + + slider->window = XCreateWindow(dwn->display, dwn->root, + slider->x, slider->y, + slider->width, slider->height, + 1, + CopyFromParent, InputOutput, CopyFromParent, + CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, + &swa); + } + + slider->visible = true; + XMapRaised(dwn->display, slider->window); + volume_slider_render(slider); +} + +void volume_slider_hide(VolumeSlider *slider) +{ + if (slider == NULL || slider->window == None) { + return; + } + + slider->visible = false; + slider->dragging = false; + XUnmapWindow(dwn->display, slider->window); +} + +void volume_slider_render(VolumeSlider *slider) +{ + if (slider == NULL || slider->window == None || !slider->visible) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + + /* Clear background */ + XSetForeground(dpy, dwn->gc, colors->panel_bg); + XFillRectangle(dpy, slider->window, dwn->gc, 0, 0, slider->width, slider->height); + + /* Draw track */ + int track_x = slider->width / 2 - 2; + int track_y = SLIDER_PADDING; + int track_height = slider->height - 2 * SLIDER_PADDING; + + XSetForeground(dpy, dwn->gc, colors->workspace_inactive); + XFillRectangle(dpy, slider->window, dwn->gc, track_x, track_y, 4, track_height); + + /* Draw filled portion */ + int fill_height = (audio_state.volume * track_height) / 100; + int fill_y = track_y + track_height - fill_height; + + XSetForeground(dpy, dwn->gc, colors->workspace_active); + XFillRectangle(dpy, slider->window, dwn->gc, track_x, fill_y, 4, fill_height); + + /* Draw knob */ + int knob_y = fill_y - SLIDER_KNOB_HEIGHT / 2; + if (knob_y < track_y) knob_y = track_y; + if (knob_y > track_y + track_height - SLIDER_KNOB_HEIGHT) { + knob_y = track_y + track_height - SLIDER_KNOB_HEIGHT; + } + + XSetForeground(dpy, dwn->gc, colors->panel_fg); + XFillRectangle(dpy, slider->window, dwn->gc, + track_x - 4, knob_y, 12, SLIDER_KNOB_HEIGHT); + + /* Draw percentage text at bottom */ + char vol_text[8]; + snprintf(vol_text, sizeof(vol_text), "%d%%", audio_state.volume); + int text_width = get_text_width(vol_text); + int text_x = (slider->width - text_width) / 2; + + draw_utf8_text(slider->window, text_x, slider->height - 4, vol_text, colors->panel_fg); + + XFlush(dpy); +} + +void volume_slider_handle_click(VolumeSlider *slider, int x, int y) +{ + (void)x; + + if (slider == NULL || !slider->visible) { + return; + } + + slider->dragging = true; + + /* Calculate volume from y position */ + int track_y = SLIDER_PADDING; + int track_height = slider->height - 2 * SLIDER_PADDING; + + int relative_y = y - track_y; + if (relative_y < 0) relative_y = 0; + if (relative_y > track_height) relative_y = track_height; + + int new_volume = 100 - (relative_y * 100 / track_height); + audio_set_volume(new_volume); + volume_slider_render(slider); + panel_render_all(); +} + +void volume_slider_handle_motion(VolumeSlider *slider, int x, int y) +{ + if (slider == NULL || !slider->visible || !slider->dragging) { + return; + } + + volume_slider_handle_click(slider, x, y); +} + +void volume_slider_handle_release(VolumeSlider *slider) +{ + if (slider == NULL) { + return; + } + slider->dragging = false; +} + +/* ========== Dropdown Menu ========== */ + +DropdownMenu *dropdown_create(int x, int y, int width) +{ + if (dwn == NULL || dwn->display == NULL) { + return NULL; + } + + DropdownMenu *menu = dwn_calloc(1, sizeof(DropdownMenu)); + menu->x = x; + menu->y = y; + menu->width = width; + menu->height = 0; + menu->item_count = 0; + menu->hovered_item = -1; + menu->visible = false; + menu->on_select = NULL; + + return menu; +} + +void dropdown_destroy(DropdownMenu *menu) +{ + if (menu == NULL) { + return; + } + + if (menu->window != None && dwn != NULL && dwn->display != NULL) { + XDestroyWindow(dwn->display, menu->window); + } + + dwn_free(menu); +} + +void dropdown_show(DropdownMenu *menu) +{ + if (menu == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + wifi_scan_networks(); + + menu->item_count = wifi_state.network_count; + menu->height = menu->item_count * DROPDOWN_ITEM_HEIGHT + 2 * DROPDOWN_PADDING; + + if (menu->height < DROPDOWN_ITEM_HEIGHT + 2 * DROPDOWN_PADDING) { + menu->height = DROPDOWN_ITEM_HEIGHT + 2 * DROPDOWN_PADDING; + menu->item_count = 1; + } + + /* Calculate auto-width based on longest network name + signal */ + int max_text_width = 0; + for (int i = 0; i < wifi_state.network_count; i++) { + WifiNetwork *net = &wifi_state.networks[i]; + char label[128]; + snprintf(label, sizeof(label), "%s %d%%", net->ssid, net->signal); + int text_width = get_text_width(label); + if (text_width > max_text_width) { + max_text_width = text_width; + } + } + int min_width = 250; + int calculated_width = max_text_width + 2 * DROPDOWN_PADDING + 30; + menu->width = (calculated_width > min_width) ? calculated_width : min_width; + + if (menu->window == None) { + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = dwn->config->colors.panel_bg; + swa.border_pixel = dwn->config->colors.border_focused; + swa.event_mask = ExposureMask | ButtonPressMask | PointerMotionMask | + LeaveWindowMask | EnterWindowMask; + + menu->window = XCreateWindow(dwn->display, dwn->root, + menu->x, menu->y, + menu->width, menu->height, + 1, + CopyFromParent, InputOutput, CopyFromParent, + CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, + &swa); + } else { + XMoveResizeWindow(dwn->display, menu->window, + menu->x, menu->y, menu->width, menu->height); + } + + menu->visible = true; + XMapRaised(dwn->display, menu->window); + dropdown_render(menu); +} + +void dropdown_hide(DropdownMenu *menu) +{ + if (menu == NULL || menu->window == None) { + return; + } + + menu->visible = false; + XUnmapWindow(dwn->display, menu->window); +} + +void dropdown_render(DropdownMenu *menu) +{ + if (menu == NULL || menu->window == None || !menu->visible) { + return; + } + + if (dwn == NULL || dwn->display == NULL) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + + int font_height = 12; + if (dwn->xft_font != NULL) { + font_height = dwn->xft_font->ascent; + } else if (dwn->font != NULL) { + font_height = dwn->font->ascent; + } + int text_y_offset = (DROPDOWN_ITEM_HEIGHT + font_height) / 2; + + /* Clear background */ + XSetForeground(dpy, dwn->gc, colors->panel_bg); + XFillRectangle(dpy, menu->window, dwn->gc, 0, 0, menu->width, menu->height); + + if (wifi_state.network_count == 0) { + draw_utf8_text(menu->window, DROPDOWN_PADDING, DROPDOWN_PADDING + text_y_offset, + "No networks found", colors->workspace_inactive); + } else { + for (int i = 0; i < wifi_state.network_count && i < MAX_WIFI_NETWORKS; i++) { + WifiNetwork *net = &wifi_state.networks[i]; + int y = DROPDOWN_PADDING + i * DROPDOWN_ITEM_HEIGHT; + + /* Highlight hovered item */ + if (i == menu->hovered_item) { + XSetForeground(dpy, dwn->gc, colors->workspace_active); + XFillRectangle(dpy, menu->window, dwn->gc, + 2, y, menu->width - 4, DROPDOWN_ITEM_HEIGHT); + } + + unsigned long text_color = (i == menu->hovered_item) ? colors->panel_bg : colors->panel_fg; + + /* Network name */ + char label[80]; + if (net->connected) { + snprintf(label, sizeof(label), "> %s", net->ssid); + } else { + snprintf(label, sizeof(label), " %s", net->ssid); + } + + /* Truncate if needed */ + if (strlen(label) > 25) { + label[22] = '.'; + label[23] = '.'; + label[24] = '.'; + label[25] = '\0'; + } + + draw_utf8_text(menu->window, DROPDOWN_PADDING, y + text_y_offset, label, text_color); + + /* Signal strength */ + char signal[8]; + snprintf(signal, sizeof(signal), "%d%%", net->signal); + int signal_width = get_text_width(signal); + int signal_x = menu->width - DROPDOWN_PADDING - signal_width; + + unsigned long signal_color = (i == menu->hovered_item) ? colors->panel_bg : colors->workspace_inactive; + draw_utf8_text(menu->window, signal_x, y + text_y_offset, signal, signal_color); + } + } + + XFlush(dpy); +} + +int dropdown_hit_test(DropdownMenu *menu, int x, int y) +{ + (void)x; + + if (menu == NULL || !menu->visible) { + return -1; + } + + if (y < DROPDOWN_PADDING || y >= menu->height - DROPDOWN_PADDING) { + return -1; + } + + int item = (y - DROPDOWN_PADDING) / DROPDOWN_ITEM_HEIGHT; + if (item >= 0 && item < menu->item_count) { + return item; + } + + return -1; +} + +void dropdown_handle_click(DropdownMenu *menu, int x, int y) +{ + if (menu == NULL || !menu->visible) { + return; + } + + int item = dropdown_hit_test(menu, x, y); + if (item >= 0 && item < wifi_state.network_count) { + WifiNetwork *net = &wifi_state.networks[item]; + + if (net->connected) { + wifi_disconnect(); + } else { + wifi_connect(net->ssid); + } + + dropdown_hide(menu); + } +} + +void dropdown_handle_motion(DropdownMenu *menu, int x, int y) +{ + if (menu == NULL || !menu->visible) { + return; + } + + int old_hovered = menu->hovered_item; + menu->hovered_item = dropdown_hit_test(menu, x, y); + + if (menu->hovered_item != old_hovered) { + dropdown_render(menu); + } +} + +/* ========== System Tray Rendering ========== */ + +int systray_get_width(void) +{ + pthread_mutex_lock(&state_mutex); + AudioState audio_snap = audio_state; + WifiState wifi_snap; + wifi_snap.connected = wifi_state.connected; + wifi_snap.enabled = wifi_state.enabled; + wifi_snap.signal_strength = wifi_state.signal_strength; + memcpy(wifi_snap.current_ssid, wifi_state.current_ssid, sizeof(wifi_snap.current_ssid)); + pthread_mutex_unlock(&state_mutex); + + char audio_label[32]; + const char *audio_icon = (audio_snap.muted || audio_snap.volume == 0) ? ASCII_VOL_MUTE : ASCII_VOL_HIGH; + snprintf(audio_label, sizeof(audio_label), "%s%d%%", audio_icon, audio_snap.volume); + + char wifi_label[128]; + const char *wifi_icon_str = (!wifi_snap.enabled || !wifi_snap.connected) ? ASCII_WIFI_OFF : ASCII_WIFI_ON; + if (wifi_snap.connected && wifi_snap.current_ssid[0] != '\0') { + snprintf(wifi_label, sizeof(wifi_label), "%s %s %d%%", + wifi_icon_str, wifi_snap.current_ssid, wifi_snap.signal_strength); + } else { + snprintf(wifi_label, sizeof(wifi_label), "%s", wifi_icon_str); + } + + return get_text_width(audio_label) + SYSTRAY_SPACING + get_text_width(wifi_label); +} + +void systray_render(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + return; + } + + const ColorScheme *colors = config_get_colors(); + + /* Take thread-safe snapshots of state */ + pthread_mutex_lock(&state_mutex); + AudioState audio_snap = audio_state; + WifiState wifi_snap; + wifi_snap.connected = wifi_state.connected; + wifi_snap.enabled = wifi_state.enabled; + wifi_snap.signal_strength = wifi_state.signal_strength; + memcpy(wifi_snap.current_ssid, wifi_state.current_ssid, sizeof(wifi_snap.current_ssid)); + pthread_mutex_unlock(&state_mutex); + + int font_height = 12; + if (dwn->xft_font != NULL) { + font_height = dwn->xft_font->ascent; + } else if (dwn->font != NULL) { + font_height = dwn->font->ascent; + } + int text_y = (panel->height + font_height) / 2; + + systray_x = x; + int current_x = x; + + /* Audio indicator */ + audio_icon_x = current_x; + char audio_label[32]; + const char *audio_icon = (audio_snap.muted || audio_snap.volume == 0) ? ASCII_VOL_MUTE : ASCII_VOL_HIGH; + snprintf(audio_label, sizeof(audio_label), "%s%d%%", audio_icon, audio_snap.volume); + + unsigned long audio_color = audio_snap.muted ? colors->workspace_inactive : colors->panel_fg; + draw_utf8_text(panel->buffer, current_x, text_y, audio_label, audio_color); + current_x += get_text_width(audio_label) + SYSTRAY_SPACING; + + /* WiFi indicator - show SSID and signal strength */ + wifi_icon_x = current_x; + char wifi_label[128]; + const char *wifi_icon_str = (!wifi_snap.enabled || !wifi_snap.connected) ? ASCII_WIFI_OFF : ASCII_WIFI_ON; + + if (wifi_snap.connected && wifi_snap.current_ssid[0] != '\0') { + snprintf(wifi_label, sizeof(wifi_label), "%s %s %d%%", + wifi_icon_str, wifi_snap.current_ssid, wifi_snap.signal_strength); + } else { + snprintf(wifi_label, sizeof(wifi_label), "%s", wifi_icon_str); + } + + unsigned long wifi_color = wifi_snap.connected ? colors->panel_fg : colors->workspace_inactive; + draw_utf8_text(panel->buffer, current_x, text_y, wifi_label, wifi_color); + current_x += get_text_width(wifi_label); + + systray_width = current_x - x; + *width = systray_width; +} + +int systray_hit_test(int x) +{ + if (x >= wifi_icon_x) { + return 0; /* WiFi - extends to end of systray */ + } + if (x >= audio_icon_x && x < wifi_icon_x) { + return 1; /* Audio */ + } + return -1; +} + +void systray_handle_click(int x, int y, int button) +{ + int widget = systray_hit_test(x); + + if (widget == 0) { /* WiFi clicked */ + /* Hide volume slider if open */ + if (volume_slider != NULL && volume_slider->visible) { + volume_slider_hide(volume_slider); + } + + if (button == 1) { + if (wifi_menu == NULL) { + int panel_height = config_get_panel_height(); + wifi_menu = dropdown_create(wifi_icon_x, panel_height, DROPDOWN_WIDTH); + } + + if (wifi_menu->visible) { + dropdown_hide(wifi_menu); + } else { + wifi_menu->x = wifi_icon_x; + wifi_menu->y = config_get_panel_height(); + dropdown_show(wifi_menu); + } + } else if (button == 3) { + if (wifi_state.connected) { + wifi_disconnect(); + } + } + } else if (widget == 1) { /* Audio clicked */ + /* Hide wifi menu if open */ + if (wifi_menu != NULL && wifi_menu->visible) { + dropdown_hide(wifi_menu); + } + + if (button == 1) { + /* Show volume slider */ + if (volume_slider == NULL) { + int panel_height = config_get_panel_height(); + volume_slider = volume_slider_create(audio_icon_x, panel_height); + } + + if (volume_slider->visible) { + volume_slider_hide(volume_slider); + } else { + volume_slider->x = audio_icon_x; + volume_slider->y = config_get_panel_height(); + volume_slider_show(volume_slider); + } + } else if (button == 3) { + /* Right-click: toggle mute */ + audio_toggle_mute(); + panel_render_all(); + } else if (button == 4) { + /* Scroll up: increase volume */ + audio_set_volume(audio_state.volume + 5); + if (volume_slider != NULL && volume_slider->visible) { + volume_slider_render(volume_slider); + } + panel_render_all(); + } else if (button == 5) { + /* Scroll down: decrease volume */ + audio_set_volume(audio_state.volume - 5); + if (volume_slider != NULL && volume_slider->visible) { + volume_slider_render(volume_slider); + } + panel_render_all(); + } + } + + (void)y; +} + +void systray_update(void) +{ + /* State is updated asynchronously by the background thread. + * This function now only handles the WiFi menu re-scan. */ + + /* Re-scan WiFi networks periodically if menu is open */ + if (wifi_menu != NULL && wifi_menu->visible) { + long now = get_time_ms(); + pthread_mutex_lock(&state_mutex); + long last_scan = wifi_state.last_scan; + pthread_mutex_unlock(&state_mutex); + + if (now - last_scan >= WIFI_SCAN_INTERVAL) { + wifi_scan_networks(); + dropdown_render(wifi_menu); + } + } +} + +/* ========== Thread-safe access functions ========== */ + +void systray_lock(void) +{ + pthread_mutex_lock(&state_mutex); +} + +void systray_unlock(void) +{ + pthread_mutex_unlock(&state_mutex); +} + +BatteryState systray_get_battery_snapshot(void) +{ + BatteryState snapshot; + pthread_mutex_lock(&state_mutex); + snapshot = battery_state; + pthread_mutex_unlock(&state_mutex); + return snapshot; +} + +AudioState systray_get_audio_snapshot(void) +{ + AudioState snapshot; + pthread_mutex_lock(&state_mutex); + snapshot = audio_state; + pthread_mutex_unlock(&state_mutex); + return snapshot; +} diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..06cb92a --- /dev/null +++ b/src/util.c @@ -0,0 +1,709 @@ +/* + * DWN - Desktop Window Manager + * Utility functions implementation + */ + +#include "util.h" +#include "dwn.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ========== Async Logging Configuration ========== */ + +#define LOG_RING_SIZE 256 /* Number of log entries in ring buffer */ +#define LOG_MSG_MAX_LEN 512 /* Max length of a single log message */ +#define LOG_MAX_FILE_SIZE (5 * 1024 * 1024) /* 5 MB max log file size */ +#define LOG_FLUSH_INTERVAL_MS 100 /* Flush to disk every 100ms */ + +/* Log entry in ring buffer */ +typedef struct { + char message[LOG_MSG_MAX_LEN]; + LogLevel level; + _Atomic int ready; /* 0 = empty, 1 = ready to write */ +} LogEntry; + +/* Static state for async logging */ +static LogEntry log_ring[LOG_RING_SIZE]; +static _Atomic size_t log_write_idx = 0; /* Next slot to write to */ +static _Atomic size_t log_read_idx = 0; /* Next slot to read from */ +static _Atomic int log_running = 0; /* Log thread running flag */ +static pthread_t log_thread; +static int log_fd = -1; /* File descriptor for log file */ +static char *log_path = NULL; /* Path to log file */ +static LogLevel min_level = LOG_INFO; +static _Atomic size_t log_file_size = 0; /* Current log file size */ + +static const char *level_names[] = { + "DEBUG", + "INFO", + "WARN", + "ERROR" +}; + +static const char *level_colors[] = { + "\033[36m", /* Cyan for DEBUG */ + "\033[32m", /* Green for INFO */ + "\033[33m", /* Yellow for WARN */ + "\033[31m" /* Red for ERROR */ +}; + +/* Forward declarations for internal log functions */ +static void log_rotate_if_needed(void); +static void *log_writer_thread(void *arg); + +/* ========== Async Logging Implementation ========== */ + +/* Rotate log file if it exceeds max size */ +static void log_rotate_if_needed(void) +{ + if (log_fd < 0 || log_path == NULL) { + return; + } + + size_t current_size = atomic_load(&log_file_size); + if (current_size < LOG_MAX_FILE_SIZE) { + return; + } + + /* Close current file */ + close(log_fd); + log_fd = -1; + + /* Create backup filename */ + size_t path_len = strlen(log_path); + char *backup_path = malloc(path_len + 8); + if (backup_path != NULL) { + snprintf(backup_path, path_len + 8, "%s.old", log_path); + /* Remove old backup if exists, rename current to backup */ + unlink(backup_path); + rename(log_path, backup_path); + free(backup_path); + } + + /* Reopen fresh log file */ + log_fd = open(log_path, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644); + atomic_store(&log_file_size, 0); +} + +/* Background thread that writes log entries to file */ +static void *log_writer_thread(void *arg) +{ + (void)arg; + + while (atomic_load(&log_running)) { + size_t read_idx = atomic_load(&log_read_idx); + size_t write_idx = atomic_load(&log_write_idx); + + /* Process all available entries */ + while (read_idx != write_idx) { + size_t idx = read_idx % LOG_RING_SIZE; + LogEntry *entry = &log_ring[idx]; + + /* Wait for entry to be ready (spin briefly) */ + int attempts = 0; + while (!atomic_load(&entry->ready) && attempts < 100) { + attempts++; + /* Brief yield */ + struct timespec ts = {0, 1000}; /* 1 microsecond */ + nanosleep(&ts, NULL); + } + + if (atomic_load(&entry->ready)) { + /* Write to file if open */ + if (log_fd >= 0) { + log_rotate_if_needed(); + if (log_fd >= 0) { + ssize_t written = write(log_fd, entry->message, strlen(entry->message)); + if (written > 0) { + atomic_fetch_add(&log_file_size, (size_t)written); + } + } + } + + /* Mark entry as consumed */ + atomic_store(&entry->ready, 0); + } + + read_idx++; + atomic_store(&log_read_idx, read_idx); + } + + /* Sleep before next check */ + struct timespec ts = {0, LOG_FLUSH_INTERVAL_MS * 1000000L}; + nanosleep(&ts, NULL); + } + + return NULL; +} + +void log_init(const char *path) +{ + /* Initialize ring buffer */ + memset(log_ring, 0, sizeof(log_ring)); + atomic_store(&log_write_idx, 0); + atomic_store(&log_read_idx, 0); + + if (path != NULL) { + char *expanded = expand_path(path); + if (expanded != NULL) { + /* Ensure directory exists */ + char *dir = strdup(expanded); + if (dir != NULL) { + char *last_slash = strrchr(dir, '/'); + if (last_slash != NULL) { + *last_slash = '\0'; + /* Create directory recursively using mkdir */ + struct stat st; + if (stat(dir, &st) != 0) { + /* Directory doesn't exist, try to create it */ + char cmd[512]; + snprintf(cmd, sizeof(cmd), "mkdir -p '%s' 2>/dev/null", dir); + int ret = system(cmd); + (void)ret; /* Ignore result - file open will fail if dir creation fails */ + } + } + free(dir); + } + + /* Open log file with O_APPEND for atomic writes */ + log_fd = open(expanded, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644); + if (log_fd < 0) { + fprintf(stderr, "Warning: Could not open log file: %s\n", expanded); + } else { + /* Get current file size */ + struct stat st; + if (fstat(log_fd, &st) == 0) { + atomic_store(&log_file_size, (size_t)st.st_size); + } + } + + log_path = expanded; /* Keep for rotation */ + } + } + + /* Start background writer thread */ + atomic_store(&log_running, 1); + if (pthread_create(&log_thread, NULL, log_writer_thread, NULL) != 0) { + fprintf(stderr, "Warning: Could not create log writer thread\n"); + atomic_store(&log_running, 0); + } +} + +void log_close(void) +{ + /* Signal thread to stop */ + if (!atomic_load(&log_running)) { + goto cleanup; + } + atomic_store(&log_running, 0); + + /* Wait for thread to finish - give it time to flush */ + pthread_join(log_thread, NULL); + +cleanup: + /* Close file */ + if (log_fd >= 0) { + close(log_fd); + log_fd = -1; + } + + /* Free path */ + if (log_path != NULL) { + free(log_path); + log_path = NULL; + } +} + +void log_set_level(LogLevel level) +{ + min_level = level; +} + +void log_flush(void) +{ + /* Force flush all pending log entries synchronously */ + if (!atomic_load(&log_running) || log_fd < 0) { + return; + } + + size_t read_idx = atomic_load(&log_read_idx); + size_t write_idx = atomic_load(&log_write_idx); + + while (read_idx != write_idx) { + size_t idx = read_idx % LOG_RING_SIZE; + LogEntry *entry = &log_ring[idx]; + + if (atomic_load(&entry->ready)) { + ssize_t written = write(log_fd, entry->message, strlen(entry->message)); + if (written > 0) { + atomic_fetch_add(&log_file_size, (size_t)written); + } + atomic_store(&entry->ready, 0); + } + + read_idx++; + atomic_store(&log_read_idx, read_idx); + } + + /* Sync to disk */ + fsync(log_fd); +} + +void log_msg(LogLevel level, const char *fmt, ...) +{ + if (level < min_level) { + return; + } + + /* Get timestamp */ + time_t now = time(NULL); + struct tm tm_info; + localtime_r(&now, &tm_info); /* Thread-safe version */ + char time_buf[32]; + strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &tm_info); + + /* Format the log message */ + char msg_buf[LOG_MSG_MAX_LEN - 64]; /* Leave room for prefix */ + va_list args; + va_start(args, fmt); + vsnprintf(msg_buf, sizeof(msg_buf), fmt, args); + va_end(args); + + /* Print to stderr (always, for immediate visibility) */ + fprintf(stderr, "%s[%s] %s: %s%s\n", + level_colors[level], time_buf, level_names[level], "\033[0m", msg_buf); + + /* Add to ring buffer for async file write (non-blocking) */ + if (atomic_load(&log_running)) { + size_t write_idx = atomic_fetch_add(&log_write_idx, 1); + size_t idx = write_idx % LOG_RING_SIZE; + LogEntry *entry = &log_ring[idx]; + + /* Check if slot is available (not overwriting unread entry) */ + /* If buffer is full, we drop the message rather than block */ + if (!atomic_load(&entry->ready)) { + entry->level = level; + snprintf(entry->message, sizeof(entry->message), + "[%s] %s: %s\n", time_buf, level_names[level], msg_buf); + atomic_store(&entry->ready, 1); + } + /* If entry->ready is true, buffer is full - message is dropped (non-blocking) */ + } +} + +/* ========== Memory allocation ========== */ + +void *dwn_malloc(size_t size) +{ + void *ptr = malloc(size); + if (ptr == NULL && size > 0) { + LOG_ERROR("Memory allocation failed for %zu bytes", size); + exit(EXIT_FAILURE); + } + return ptr; +} + +void *dwn_calloc(size_t nmemb, size_t size) +{ + void *ptr = calloc(nmemb, size); + if (ptr == NULL && nmemb > 0 && size > 0) { + LOG_ERROR("Memory allocation failed for %zu elements", nmemb); + exit(EXIT_FAILURE); + } + return ptr; +} + +void *dwn_realloc(void *ptr, size_t size) +{ + void *new_ptr = realloc(ptr, size); + if (new_ptr == NULL && size > 0) { + LOG_ERROR("Memory reallocation failed for %zu bytes", size); + exit(EXIT_FAILURE); + } + return new_ptr; +} + +char *dwn_strdup(const char *s) +{ + if (s == NULL) { + return NULL; + } + char *dup = strdup(s); + if (dup == NULL) { + LOG_ERROR("String duplication failed"); + exit(EXIT_FAILURE); + } + return dup; +} + +void dwn_free(void *ptr) +{ + free(ptr); +} + +void secure_wipe(void *ptr, size_t size) +{ + if (ptr == NULL || size == 0) { + return; + } +#if defined(__GLIBC__) && (__GLIBC__ >= 2) && (__GLIBC_MINOR__ >= 25) + /* Use explicit_bzero if available (glibc 2.25+) */ + explicit_bzero(ptr, size); +#else + /* Fallback: Use volatile to prevent compiler optimization */ + volatile unsigned char *p = (volatile unsigned char *)ptr; + while (size--) { + *p++ = 0; + } +#endif +} + +/* ========== String utilities ========== */ + +char *str_trim(char *str) +{ + if (str == NULL) { + return NULL; + } + + /* Trim leading whitespace */ + while (isspace((unsigned char)*str)) { + str++; + } + + if (*str == '\0') { + return str; + } + + /* Trim trailing whitespace */ + char *end = str + strlen(str) - 1; + while (end > str && isspace((unsigned char)*end)) { + end--; + } + end[1] = '\0'; + + return str; +} + +bool str_starts_with(const char *str, const char *prefix) +{ + if (str == NULL || prefix == NULL) { + return false; + } + return strncmp(str, prefix, strlen(prefix)) == 0; +} + +bool str_ends_with(const char *str, const char *suffix) +{ + if (str == NULL || suffix == NULL) { + return false; + } + size_t str_len = strlen(str); + size_t suffix_len = strlen(suffix); + if (suffix_len > str_len) { + return false; + } + return strcmp(str + str_len - suffix_len, suffix) == 0; +} + +int str_split(char *str, char delim, char **parts, int max_parts) +{ + if (str == NULL || parts == NULL || max_parts <= 0) { + return 0; + } + + int count = 0; + char *start = str; + + while (*str && count < max_parts) { + if (*str == delim) { + *str = '\0'; + parts[count++] = start; + start = str + 1; + } + str++; + } + + if (count < max_parts && *start) { + parts[count++] = start; + } + + return count; +} + +char *shell_escape(const char *str) +{ + if (str == NULL) { + return dwn_strdup(""); + } + + /* Count single quotes to determine buffer size */ + size_t quotes = 0; + for (const char *p = str; *p; p++) { + if (*p == '\'') quotes++; + } + + /* Each single quote becomes '\'' (4 chars), plus 2 for surrounding quotes */ + size_t len = strlen(str) + quotes * 3 + 3; + char *escaped = dwn_malloc(len); + char *dst = escaped; + + *dst++ = '\''; + for (const char *src = str; *src; src++) { + if (*src == '\'') { + /* Close quote, add escaped quote, reopen quote: '\'' */ + *dst++ = '\''; + *dst++ = '\\'; + *dst++ = '\''; + *dst++ = '\''; + } else { + *dst++ = *src; + } + } + *dst++ = '\''; + *dst = '\0'; + + return escaped; +} + +/* ========== File utilities ========== */ + +bool file_exists(const char *path) +{ + if (path == NULL) { + return false; + } + struct stat st; + return stat(path, &st) == 0; +} + +char *file_read_all(const char *path) +{ + FILE *f = fopen(path, "r"); + if (f == NULL) { + return NULL; + } + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + char *content = dwn_malloc(size + 1); + size_t read = fread(content, 1, size, f); + content[read] = '\0'; + + fclose(f); + return content; +} + +bool file_write_all(const char *path, const char *content) +{ + FILE *f = fopen(path, "w"); + if (f == NULL) { + return false; + } + + size_t len = strlen(content); + size_t written = fwrite(content, 1, len, f); + fclose(f); + + return written == len; +} + +char *expand_path(const char *path) +{ + if (path == NULL) { + return NULL; + } + + if (path[0] == '~') { + const char *home = getenv("HOME"); + if (home == NULL) { + struct passwd *pw = getpwuid(getuid()); + if (pw != NULL) { + home = pw->pw_dir; + } + } + if (home != NULL) { + size_t len = strlen(home) + strlen(path); + char *expanded = dwn_malloc(len); + snprintf(expanded, len, "%s%s", home, path + 1); + return expanded; + } + } + + return dwn_strdup(path); +} + +/* ========== Color utilities ========== */ + +unsigned long parse_color(const char *color_str) +{ + if (color_str == NULL || dwn == NULL || dwn->display == NULL) { + return 0; + } + + XColor color, exact; + if (XAllocNamedColor(dwn->display, dwn->colormap, color_str, &color, &exact)) { + return color.pixel; + } + + /* Try parsing as hex */ + if (color_str[0] == '#') { + unsigned int r, g, b; + if (sscanf(color_str + 1, "%02x%02x%02x", &r, &g, &b) == 3) { + color.red = r << 8; + color.green = g << 8; + color.blue = b << 8; + color.flags = DoRed | DoGreen | DoBlue; + if (XAllocColor(dwn->display, dwn->colormap, &color)) { + return color.pixel; + } + } + } + + LOG_WARN("Could not parse color: %s", color_str); + return 0; +} + +void color_to_rgb(unsigned long color, int *r, int *g, int *b) +{ + XColor xc; + xc.pixel = color; + XQueryColor(dwn->display, dwn->colormap, &xc); + *r = xc.red >> 8; + *g = xc.green >> 8; + *b = xc.blue >> 8; +} + +/* ========== Time utilities ========== */ + +long get_time_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000); +} + +void sleep_ms(int ms) +{ + struct timespec ts; + ts.tv_sec = ms / 1000; + ts.tv_nsec = (ms % 1000) * 1000000L; + nanosleep(&ts, NULL); +} + +/* ========== Process utilities ========== */ + +int spawn(const char *cmd) +{ + if (cmd == NULL) { + return -1; + } + + LOG_DEBUG("Spawning: %s", cmd); + + pid_t pid = fork(); + if (pid == 0) { + /* Child process */ + setsid(); + execl("/bin/sh", "sh", "-c", cmd, NULL); + _exit(EXIT_FAILURE); + } else if (pid < 0) { + LOG_ERROR("Fork failed: %s", strerror(errno)); + return -1; + } + + /* Wait for child */ + int status; + waitpid(pid, &status, 0); + return WEXITSTATUS(status); +} + +int spawn_async(const char *cmd) +{ + if (cmd == NULL) { + return -1; + } + + LOG_DEBUG("Spawning async: %s", cmd); + + pid_t pid = fork(); + if (pid == 0) { + /* Child process */ + setsid(); + + /* Double fork to avoid zombies */ + pid_t pid2 = fork(); + if (pid2 == 0) { + execl("/bin/sh", "sh", "-c", cmd, NULL); + _exit(EXIT_FAILURE); + } + _exit(EXIT_SUCCESS); + } else if (pid < 0) { + LOG_ERROR("Fork failed: %s", strerror(errno)); + return -1; + } + + /* Wait for first child (which exits immediately) */ + int status; + waitpid(pid, &status, 0); + return 0; +} + +char *spawn_capture(const char *cmd) +{ + if (cmd == NULL) { + return NULL; + } + + LOG_DEBUG("Spawning capture: %s", cmd); + + FILE *fp = popen(cmd, "r"); + if (fp == NULL) { + LOG_ERROR("popen failed: %s", strerror(errno)); + return NULL; + } + + /* Read output */ + size_t buf_size = 1024; + size_t len = 0; + char *output = dwn_malloc(buf_size); + output[0] = '\0'; + + char line[256]; + while (fgets(line, sizeof(line), fp) != NULL) { + size_t line_len = strlen(line); + if (len + line_len + 1 > buf_size) { + buf_size *= 2; + output = dwn_realloc(output, buf_size); + } + /* Use memcpy with explicit bounds instead of strcpy */ + memcpy(output + len, line, line_len + 1); + len += line_len; + } + + int status = pclose(fp); + if (status != 0) { + LOG_DEBUG("Command exited with status %d", status); + } + + /* Trim trailing newline */ + if (len > 0 && output[len - 1] == '\n') { + output[len - 1] = '\0'; + } + + return output; +} diff --git a/src/workspace.c b/src/workspace.c new file mode 100644 index 0000000..1db163f --- /dev/null +++ b/src/workspace.c @@ -0,0 +1,483 @@ +/* + * DWN - Desktop Window Manager + * Workspace management implementation + */ + +#include "workspace.h" +#include "client.h" +#include "layout.h" +#include "atoms.h" +#include "util.h" +#include "config.h" + +#include +#include + +/* ========== Initialization ========== */ + +void workspace_init(void) +{ + if (dwn == NULL) { + return; + } + + LOG_DEBUG("Initializing %d workspaces", MAX_WORKSPACES); + + for (int i = 0; i < MAX_WORKSPACES; i++) { + Workspace *ws = &dwn->workspaces[i]; + + ws->clients = NULL; + ws->focused = NULL; + ws->layout = (dwn->config != NULL) ? + dwn->config->default_layout : LAYOUT_TILING; + ws->master_ratio = (dwn->config != NULL) ? + dwn->config->default_master_ratio : 0.55f; + ws->master_count = (dwn->config != NULL) ? + dwn->config->default_master_count : 1; + + /* Default names: "1", "2", ..., "9" */ + snprintf(ws->name, sizeof(ws->name), "%d", i + 1); + } + + dwn->current_workspace = 0; +} + +void workspace_cleanup(void) +{ + /* Clients are cleaned up separately */ +} + +/* ========== Workspace access ========== */ + +Workspace *workspace_get(int index) +{ + if (dwn == NULL || index < 0 || index >= MAX_WORKSPACES) { + return NULL; + } + return &dwn->workspaces[index]; +} + +Workspace *workspace_get_current(void) +{ + return workspace_get(dwn->current_workspace); +} + +int workspace_get_current_index(void) +{ + return dwn != NULL ? dwn->current_workspace : 0; +} + +/* ========== Workspace switching ========== */ + +void workspace_switch(int index) +{ + if (dwn == NULL || index < 0 || index >= MAX_WORKSPACES) { + return; + } + + if (index == dwn->current_workspace) { + return; + } + + LOG_DEBUG("Switching from workspace %d to %d", + dwn->current_workspace + 1, index + 1); + + /* Grab server to batch all X operations - prevents flickering and improves speed */ + XGrabServer(dwn->display); + + /* Hide current workspace windows */ + workspace_hide(dwn->current_workspace); + + /* Update current workspace */ + int old_workspace = dwn->current_workspace; + dwn->current_workspace = index; + + /* Show new workspace windows */ + workspace_show(index); + + /* Arrange windows on new workspace */ + workspace_arrange(index); + + /* Focus appropriate window */ + Workspace *ws = workspace_get(index); + if (ws != NULL) { + if (ws->focused != NULL) { + client_focus(ws->focused); + } else { + Client *first = workspace_get_first_client(index); + if (first != NULL) { + client_focus(first); + } else { + /* No windows, focus root */ + XSetInputFocus(dwn->display, dwn->root, RevertToPointerRoot, CurrentTime); + atoms_set_active_window(None); + } + } + } + + /* Update EWMH */ + atoms_set_current_desktop(index); + + /* Release server and sync - all changes appear atomically */ + XUngrabServer(dwn->display); + XSync(dwn->display, False); + + (void)old_workspace; +} + +void workspace_switch_next(void) +{ + int next = (dwn->current_workspace + 1) % MAX_WORKSPACES; + workspace_switch(next); +} + +void workspace_switch_prev(void) +{ + int prev = (dwn->current_workspace - 1 + MAX_WORKSPACES) % MAX_WORKSPACES; + workspace_switch(prev); +} + +/* ========== Client management ========== */ + +void workspace_add_client(int workspace, Client *client) +{ + if (client == NULL || workspace < 0 || workspace >= MAX_WORKSPACES) { + return; + } + + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + /* Add to workspace client list (at head) */ + client->workspace = workspace; + + /* We don't maintain a separate linked list per workspace, + just use the client's workspace field */ +} + +void workspace_remove_client(int workspace, Client *client) +{ + if (client == NULL || workspace < 0 || workspace >= MAX_WORKSPACES) { + return; + } + + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + /* If this was the focused client, clear it */ + if (ws->focused == client) { + ws->focused = NULL; + } +} + +void workspace_move_client(Client *client, int new_workspace) +{ + if (client == NULL || new_workspace < 0 || new_workspace >= MAX_WORKSPACES) { + return; + } + + if (client->workspace == (unsigned int)new_workspace) { + return; + } + + int old_workspace = client->workspace; + + LOG_DEBUG("Moving window '%s' from workspace %d to %d", + client->title, old_workspace + 1, new_workspace + 1); + + /* Grab server to batch all X operations */ + XGrabServer(dwn->display); + + /* Remove from old workspace */ + workspace_remove_client(old_workspace, client); + + /* Add to new workspace */ + workspace_add_client(new_workspace, client); + + /* Update EWMH */ + atoms_set_window_desktop(client->window, new_workspace); + + /* Hide if moving to non-current workspace */ + if (new_workspace != dwn->current_workspace) { + client_hide(client); + } else { + client_show(client); + } + + /* Rearrange both workspaces */ + workspace_arrange(old_workspace); + workspace_arrange(new_workspace); + + /* Release server and sync */ + XUngrabServer(dwn->display); + XSync(dwn->display, False); +} + +Client *workspace_get_first_client(int workspace) +{ + if (workspace < 0 || workspace >= MAX_WORKSPACES) { + return NULL; + } + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)workspace && !client_is_minimized(c)) { + return c; + } + } + + return NULL; +} + +Client *workspace_get_focused_client(int workspace) +{ + Workspace *ws = workspace_get(workspace); + return ws != NULL ? ws->focused : NULL; +} + +/* ========== Layout ========== */ + +void workspace_set_layout(int workspace, LayoutType layout) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + ws->layout = layout; + + /* Batch the arrangement */ + XGrabServer(dwn->display); + workspace_arrange(workspace); + XUngrabServer(dwn->display); + XSync(dwn->display, False); + + LOG_DEBUG("Workspace %d layout set to %d", workspace + 1, layout); +} + +LayoutType workspace_get_layout(int workspace) +{ + Workspace *ws = workspace_get(workspace); + return ws != NULL ? ws->layout : LAYOUT_TILING; +} + +void workspace_cycle_layout(int workspace) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + ws->layout = (ws->layout + 1) % LAYOUT_COUNT; + + /* Batch the arrangement for smoother transition */ + XGrabServer(dwn->display); + workspace_arrange(workspace); + XUngrabServer(dwn->display); + XSync(dwn->display, False); + + const char *layout_names[] = { "Tiling", "Floating", "Monocle" }; + LOG_INFO("Workspace %d: %s layout", workspace + 1, layout_names[ws->layout]); +} + +void workspace_set_master_ratio(int workspace, float ratio) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + /* Clamp ratio */ + if (ratio < 0.1f) ratio = 0.1f; + if (ratio > 0.9f) ratio = 0.9f; + + ws->master_ratio = ratio; + + /* Batch the arrangement */ + XGrabServer(dwn->display); + workspace_arrange(workspace); + XUngrabServer(dwn->display); + XSync(dwn->display, False); +} + +void workspace_adjust_master_ratio(int workspace, float delta) +{ + Workspace *ws = workspace_get(workspace); + if (ws != NULL) { + workspace_set_master_ratio(workspace, ws->master_ratio + delta); + } +} + +void workspace_set_master_count(int workspace, int count) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + if (count < 1) count = 1; + ws->master_count = count; + + /* Batch the arrangement */ + XGrabServer(dwn->display); + workspace_arrange(workspace); + XUngrabServer(dwn->display); + XSync(dwn->display, False); +} + +void workspace_adjust_master_count(int workspace, int delta) +{ + Workspace *ws = workspace_get(workspace); + if (ws != NULL) { + workspace_set_master_count(workspace, ws->master_count + delta); + } +} + +/* ========== Arrangement ========== */ + +void workspace_arrange(int workspace) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + /* Only arrange visible workspace */ + if (workspace != dwn->current_workspace) { + return; + } + + layout_arrange(workspace); +} + +void workspace_arrange_current(void) +{ + /* Batch the arrangement */ + XGrabServer(dwn->display); + workspace_arrange(dwn->current_workspace); + XUngrabServer(dwn->display); + XSync(dwn->display, False); +} + +/* ========== Visibility ========== */ + +void workspace_show(int workspace) +{ + if (workspace < 0 || workspace >= MAX_WORKSPACES) { + return; + } + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)workspace && !client_is_minimized(c)) { + client_show(c); + } + } +} + +void workspace_hide(int workspace) +{ + if (workspace < 0 || workspace >= MAX_WORKSPACES) { + return; + } + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)workspace) { + client_hide(c); + } + } +} + +/* ========== Properties ========== */ + +void workspace_set_name(int workspace, const char *name) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL || name == NULL) { + return; + } + + strncpy(ws->name, name, sizeof(ws->name) - 1); + ws->name[sizeof(ws->name) - 1] = '\0'; + + atoms_update_desktop_names(); +} + +const char *workspace_get_name(int workspace) +{ + Workspace *ws = workspace_get(workspace); + return ws != NULL ? ws->name : ""; +} + +int workspace_client_count(int workspace) +{ + return client_count_on_workspace(workspace); +} + +bool workspace_is_empty(int workspace) +{ + return workspace_client_count(workspace) == 0; +} + +/* ========== Focus cycling ========== */ + +void workspace_focus_next(void) +{ + Workspace *ws = workspace_get_current(); + if (ws == NULL || ws->focused == NULL) { + return; + } + + /* Find next client on same workspace */ + Client *next = ws->focused->next; + while (next != NULL && next->workspace != (unsigned int)dwn->current_workspace) { + next = next->next; + } + + /* Wrap around */ + if (next == NULL) { + next = workspace_get_first_client(dwn->current_workspace); + } + + if (next != NULL && next != ws->focused) { + client_focus(next); + } +} + +void workspace_focus_prev(void) +{ + Workspace *ws = workspace_get_current(); + if (ws == NULL || ws->focused == NULL) { + return; + } + + /* Find previous client on same workspace */ + Client *prev = ws->focused->prev; + while (prev != NULL && prev->workspace != (unsigned int)dwn->current_workspace) { + prev = prev->prev; + } + + /* Wrap around */ + if (prev == NULL) { + for (Client *c = client_get_last(); c != NULL; c = c->prev) { + if (c->workspace == (unsigned int)dwn->current_workspace) { + prev = c; + break; + } + } + } + + if (prev != NULL && prev != ws->focused) { + client_focus(prev); + } +} + +void workspace_focus_master(void) +{ + Client *master = workspace_get_first_client(dwn->current_workspace); + if (master != NULL) { + client_focus(master); + } +}