chore: update c, css, d files

This commit is contained in:
retoor 2025-12-28 03:14:31 +01:00
commit 7a4f7a82ad
80 changed files with 21222 additions and 0 deletions

21
LICENSE Normal file
View File

@ -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.

263
Makefile Normal file
View File

@ -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)"

BIN
bin/dwn Executable file

Binary file not shown.

46
build/ai.d Normal file
View File

@ -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:

BIN
build/ai.o Normal file

Binary file not shown.

6
build/applauncher.d Normal file
View File

@ -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:

BIN
build/applauncher.o Normal file

Binary file not shown.

4
build/atoms.d Normal file
View File

@ -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:

BIN
build/atoms.o Normal file

Binary file not shown.

2
build/cJSON.d Normal file
View File

@ -0,0 +1,2 @@
build/cJSON.o: src/cJSON.c include/cJSON.h
include/cJSON.h:

BIN
build/cJSON.o Normal file

Binary file not shown.

47
build/client.d Normal file
View File

@ -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:

BIN
build/client.o Normal file

Binary file not shown.

5
build/config.d Normal file
View File

@ -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:

BIN
build/config.o Normal file

Binary file not shown.

7
build/decorations.d Normal file
View File

@ -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:

BIN
build/decorations.o Normal file

Binary file not shown.

49
build/keys.d Normal file
View File

@ -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:

BIN
build/keys.o Normal file

Binary file not shown.

8
build/layout.d Normal file
View File

@ -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:

BIN
build/layout.o Normal file

Binary file not shown.

56
build/main.d Normal file
View File

@ -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:

BIN
build/main.o Normal file

Binary file not shown.

8
build/news.d Normal file
View File

@ -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:

BIN
build/news.o Normal file

Binary file not shown.

42
build/notifications.d Normal file
View File

@ -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:

BIN
build/notifications.o Normal file

Binary file not shown.

13
build/panel.d Normal file
View File

@ -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:

BIN
build/panel.o Normal file

Binary file not shown.

44
build/systray.d Normal file
View File

@ -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:

BIN
build/systray.o Normal file

Binary file not shown.

3
build/util.d Normal file
View File

@ -0,0 +1,3 @@
build/util.o: src/util.c include/util.h include/dwn.h
include/util.h:
include/dwn.h:

BIN
build/util.o Normal file

Binary file not shown.

10
build/workspace.d Normal file
View File

@ -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:

BIN
build/workspace.o Normal file

Binary file not shown.

93
config/config.example Normal file
View File

@ -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

94
include/ai.h Normal file
View File

@ -0,0 +1,94 @@
/*
* DWN - Desktop Window Manager
* AI Integration (OpenRouter API)
*/
#ifndef DWN_AI_H
#define DWN_AI_H
#include "dwn.h"
#include <stdbool.h>
/* 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 */

48
include/applauncher.h Normal file
View File

@ -0,0 +1,48 @@
/*
* DWN - Desktop Window Manager
* Application launcher with .desktop file support
*/
#ifndef DWN_APPLAUNCHER_H
#define DWN_APPLAUNCHER_H
#include <stdbool.h>
/* 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 */

148
include/atoms.h Normal file
View File

@ -0,0 +1,148 @@
/*
* DWN - Desktop Window Manager
* X11 Atoms management (EWMH/ICCCM compliance)
*/
#ifndef DWN_ATOMS_H
#define DWN_ATOMS_H
#include <X11/Xlib.h>
#include <stdbool.h>
/* 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 */

306
include/cJSON.h Normal file
View File

@ -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 <stddef.h>
/* 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

80
include/client.h Normal file
View File

@ -0,0 +1,80 @@
/*
* DWN - Desktop Window Manager
* Client (window) management
*/
#ifndef DWN_CLIENT_H
#define DWN_CLIENT_H
#include "dwn.h"
#include <stdbool.h>
/* 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 */

87
include/config.h Normal file
View File

@ -0,0 +1,87 @@
/*
* DWN - Desktop Window Manager
* Configuration system
*/
#ifndef DWN_CONFIG_H
#define DWN_CONFIG_H
#include "dwn.h"
#include <stdbool.h>
/* 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 */

48
include/decorations.h Normal file
View File

@ -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 <stdbool.h>
/* 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 */

168
include/dwn.h Normal file
View File

@ -0,0 +1,168 @@
/*
* DWN - Desktop Window Manager
* Main header with shared types and global state
*/
#ifndef DWN_H
#define DWN_H
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/extensions/Xinerama.h>
#include <X11/extensions/Xrandr.h>
#include <X11/Xft/Xft.h>
#include <stdbool.h>
#include <stdint.h>
/* 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 */

98
include/keys.h Normal file
View File

@ -0,0 +1,98 @@
/*
* DWN - Desktop Window Manager
* Keyboard shortcut handling
*/
#ifndef DWN_KEYS_H
#define DWN_KEYS_H
#include "dwn.h"
#include <stdbool.h>
#include <X11/keysym.h>
/* 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 */

25
include/layout.h Normal file
View File

@ -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 */

66
include/news.h Normal file
View File

@ -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 <stdbool.h>
/* 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 */

73
include/notifications.h Normal file
View File

@ -0,0 +1,73 @@
/*
* DWN - Desktop Window Manager
* D-Bus Notification daemon
*/
#ifndef DWN_NOTIFICATIONS_H
#define DWN_NOTIFICATIONS_H
#include "dwn.h"
#include <stdbool.h>
#include <dbus/dbus.h>
/* 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 */

65
include/panel.h Normal file
View File

@ -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 <stdbool.h>
/* 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 */

134
include/systray.h Normal file
View File

@ -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 <stdbool.h>
/* 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 */

68
include/util.h Normal file
View File

@ -0,0 +1,68 @@
/*
* DWN - Desktop Window Manager
* Utility functions
*/
#ifndef DWN_UTIL_H
#define DWN_UTIL_H
#include <stdbool.h>
#include <stddef.h>
#include <stdarg.h>
/* 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 */

61
include/workspace.h Normal file
View File

@ -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 <stdbool.h>
/* 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 */

8
scripts/dwn.desktop Normal file
View File

@ -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;

42
scripts/xinitrc.example Normal file
View File

@ -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

458
site/ai-features.html Normal file
View File

@ -0,0 +1,458 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="AI integration in DWN window manager - command palette, semantic search, and context analysis.">
<title>AI Features - DWN Window Manager</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<a href="index.html" class="logo">
<span class="logo-icon">D</span>
<span>DWN</span>
</a>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Install</a></li>
<li class="dropdown">
<a href="documentation.html" class="active">Docs</a>
<div class="dropdown-menu">
<a href="documentation.html">Getting Started</a>
<a href="shortcuts.html">Keyboard Shortcuts</a>
<a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a>
</div>
</li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
<div class="nav-toggle" onclick="toggleNav()">
<span></span>
<span></span>
<span></span>
</div>
</nav>
</header>
<main>
<section class="hero" style="padding: 8rem 0 4rem;">
<div class="container hero-content">
<h1>AI Integration</h1>
<p class="subtitle">
Control your desktop with natural language and get intelligent assistance.
</p>
</div>
</section>
<section class="section">
<div class="container">
<div class="alert alert-info" style="margin-bottom: 2rem;">
<strong class="alert-title">Optional Features</strong>
<p style="margin: 0;">AI features are completely optional and require external API keys.
DWN works perfectly without them.</p>
</div>
<h2>Overview</h2>
<p>
DWN integrates with two AI services to provide intelligent desktop assistance:
</p>
<ul style="margin-bottom: 2rem;">
<li><strong>OpenRouter API</strong> - Powers the AI command palette and context analysis</li>
<li><strong>Exa API</strong> - Provides semantic web search capabilities</li>
</ul>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">&#129302;</div>
<h3>AI Command Palette</h3>
<p>Type natural language commands to control your desktop.
Launch apps, query system info, and more.</p>
<p style="margin-top: 1rem;"><kbd>Super</kbd> + <kbd>Shift</kbd> + <kbd>A</kbd></p>
</div>
<div class="feature-card">
<div class="feature-icon">&#128065;</div>
<h3>Context Analysis</h3>
<p>AI analyzes your current workspace to understand your task
and provide relevant suggestions.</p>
<p style="margin-top: 1rem;"><kbd>Super</kbd> + <kbd>A</kbd></p>
</div>
<div class="feature-card">
<div class="feature-icon">&#128269;</div>
<h3>Semantic Search</h3>
<p>Search the web using meaning, not just keywords.
Find relevant content instantly.</p>
<p style="margin-top: 1rem;"><kbd>Super</kbd> + <kbd>Shift</kbd> + <kbd>E</kbd></p>
</div>
</div>
<!-- Setup OpenRouter -->
<h2 id="openrouter" style="margin-top: 4rem;">Setting Up OpenRouter</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
OpenRouter provides access to multiple AI models through a single API.
You can use free models or paid ones depending on your needs.
</p>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h4>Get an API Key</h4>
<p>Visit <a href="https://openrouter.ai/keys" target="_blank">https://openrouter.ai/keys</a>
and create a free account to get your API key.</p>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h4>Set the Environment Variable</h4>
<p>Add to your shell profile (~/.bashrc, ~/.zshrc, etc.):</p>
<div class="code-header">
<span>~/.bashrc</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>export OPENROUTER_API_KEY="sk-or-v1-your-key-here"</code></pre>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<h4>Choose a Model (Optional)</h4>
<p>Configure the AI model in your DWN config file:</p>
<div class="code-header">
<span>~/.config/dwn/config</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>[ai]
model = google/gemini-2.0-flash-exp:free</code></pre>
<p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--text-muted);">
Browse available models at <a href="https://openrouter.ai/models" target="_blank">openrouter.ai/models</a>
</p>
</div>
</div>
</div>
<div class="card" style="margin-top: 2rem;">
<h3>Recommended Free Models</h3>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Model ID</th>
<th>Provider</th>
<th>Best For</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>google/gemini-2.0-flash-exp:free</code></td>
<td>Google</td>
<td>Fast responses, good general use</td>
</tr>
<tr>
<td><code>meta-llama/llama-3.2-3b-instruct:free</code></td>
<td>Meta</td>
<td>Quick commands, lightweight</td>
</tr>
<tr>
<td><code>mistralai/mistral-7b-instruct:free</code></td>
<td>Mistral</td>
<td>Balanced performance</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- AI Command Palette -->
<h2 id="command-palette" style="margin-top: 4rem;">AI Command Palette</h2>
<p>
Press <kbd>Super</kbd> + <kbd>Shift</kbd> + <kbd>A</kbd> to open the command palette.
Type natural language commands and press Enter.
</p>
<h3>Supported Commands</h3>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Category</th>
<th>Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Launch Applications</strong></td>
<td>
"open firefox"<br>
"run terminal"<br>
"launch file manager"<br>
"start chrome"
</td>
</tr>
<tr>
<td><strong>System Queries</strong></td>
<td>
"what time is it"<br>
"how much memory is free"<br>
"what's my IP address"<br>
"show disk usage"
</td>
</tr>
<tr>
<td><strong>General Questions</strong></td>
<td>
"how do I resize the master area"<br>
"what's the shortcut for fullscreen"<br>
"explain tiling mode"
</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-success" style="margin-top: 1.5rem;">
<strong class="alert-title">Pro Tip</strong>
<p style="margin: 0;">The AI understands context. You can say "open browser" instead of
remembering the exact application name - it will figure out what you mean.</p>
</div>
<!-- Context Analysis -->
<h2 id="context" style="margin-top: 4rem;">Context Analysis</h2>
<p>
Press <kbd>Super</kbd> + <kbd>A</kbd> to see AI-powered analysis of your current workspace.
</p>
<div class="card">
<h3>What It Shows</h3>
<ul style="padding-left: 1.25rem;">
<li><strong>Task Type</strong> - Coding, browsing, communication, etc.</li>
<li><strong>Focused Window</strong> - Currently active application</li>
<li><strong>Suggestions</strong> - Relevant shortcuts or actions based on context</li>
<li><strong>Workspace Summary</strong> - Overview of open applications</li>
</ul>
</div>
<!-- Setup Exa -->
<h2 id="exa" style="margin-top: 4rem;">Setting Up Exa Search</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Exa provides semantic search - finding content based on meaning rather than exact keywords.
</p>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h4>Get an Exa API Key</h4>
<p>Visit <a href="https://dashboard.exa.ai/api-keys" target="_blank">https://dashboard.exa.ai/api-keys</a>
and create an account to get your API key.</p>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h4>Set the Environment Variable</h4>
<div class="code-header">
<span>~/.bashrc</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>export EXA_API_KEY="your-exa-key-here"</code></pre>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<h4>Start Searching</h4>
<p>Press <kbd>Super</kbd> + <kbd>Shift</kbd> + <kbd>E</kbd> and type your query.</p>
</div>
</div>
</div>
<!-- Semantic Search -->
<h2 id="search" style="margin-top: 4rem;">Using Semantic Search</h2>
<p>
Unlike traditional search, Exa understands the meaning of your query.
</p>
<div class="comparison" style="grid-template-columns: repeat(2, 1fr);">
<div class="comparison-card">
<h3>Traditional Search</h3>
<p style="color: var(--text-muted);">Keyword matching</p>
<ul>
<li>Exact keyword matches</li>
<li>Boolean operators needed</li>
<li>Miss relevant results</li>
</ul>
<p style="margin-top: 1rem; font-style: italic; color: var(--text-muted);">
"nginx reverse proxy setup tutorial"
</p>
</div>
<div class="comparison-card featured">
<h3>Semantic Search</h3>
<p style="color: var(--text-muted);">Meaning-based</p>
<ul>
<li>Understands intent</li>
<li>Natural language</li>
<li>Finds related content</li>
</ul>
<p style="margin-top: 1rem; font-style: italic; color: var(--text-muted);">
"how to configure nginx as a reverse proxy"
</p>
</div>
</div>
<h3 style="margin-top: 2rem;">Search Tips</h3>
<ul>
<li>Use natural, conversational queries</li>
<li>Be specific about what you're looking for</li>
<li>Results appear in a dmenu/rofi list - select to open in browser</li>
<li>Search includes articles, documentation, tutorials, and more</li>
</ul>
<!-- Privacy -->
<h2 id="privacy" style="margin-top: 4rem;">Privacy Considerations</h2>
<div class="alert alert-warning">
<strong class="alert-title">Data Sent to External Services</strong>
<p style="margin: 0;">When using AI features, the following data is sent to external APIs:</p>
</div>
<div class="table-wrapper" style="margin-top: 1rem;">
<table>
<thead>
<tr>
<th>Feature</th>
<th>Data Sent</th>
<th>Service</th>
</tr>
</thead>
<tbody>
<tr>
<td>Command Palette</td>
<td>Your typed command</td>
<td>OpenRouter (then to model provider)</td>
</tr>
<tr>
<td>Context Analysis</td>
<td>Window titles, app names</td>
<td>OpenRouter (then to model provider)</td>
</tr>
<tr>
<td>Semantic Search</td>
<td>Your search query</td>
<td>Exa</td>
</tr>
</tbody>
</table>
</div>
<p style="margin-top: 1rem; color: var(--text-muted);">
If you're concerned about privacy, you can:
</p>
<ul style="color: var(--text-muted);">
<li>Not configure API keys (AI features simply won't work)</li>
<li>Use OpenRouter with privacy-focused models</li>
<li>Only use AI features when needed</li>
</ul>
<!-- Troubleshooting -->
<h2 id="troubleshooting" style="margin-top: 4rem;">Troubleshooting</h2>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
AI commands don't work - "API key not configured"
</button>
<div class="faq-answer">
<div class="faq-answer-content">
<p>Make sure your API key is properly set:</p>
<ol style="padding-left: 1.25rem; margin-top: 0.5rem;">
<li>Check the environment variable: <code>echo $OPENROUTER_API_KEY</code></li>
<li>Ensure the variable is exported in your shell profile</li>
<li>Log out and back in, or source your profile: <code>source ~/.bashrc</code></li>
<li>Restart DWN</li>
</ol>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
Slow responses from AI
</button>
<div class="faq-answer">
<div class="faq-answer-content">
<p>Try using a faster model. Free models can sometimes be slow due to rate limiting.
Gemini Flash is usually the fastest free option.</p>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
Exa search returns no results
</button>
<div class="faq-answer">
<div class="faq-answer-content">
<p>Check your Exa API key and ensure you have remaining credits.
Visit the <a href="https://dashboard.exa.ai">Exa dashboard</a> to check your usage.</p>
</div>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
<div class="footer-section">
<h4>DWN Window Manager</h4>
<p style="color: var(--text-muted);">
A modern, production-ready X11 window manager with XFCE-like
functionality and optional AI integration.
</p>
</div>
<div class="footer-section">
<h4>Documentation</h4>
<ul>
<li><a href="documentation.html">Getting Started</a></li>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Resources</h4>
<ul>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Installation</a></li>
<li><a href="ai-features.html">AI Integration</a></li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Community</h4>
<ul>
<li><a href="https://github.com/dwn/dwn/issues">Issue Tracker</a></li>
<li><a href="https://github.com/dwn/dwn/discussions">Discussions</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/CONTRIBUTING.md">Contributing</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/LICENSE">License (MIT)</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>DWN Window Manager - Open Source under the MIT License</p>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

565
site/architecture.html Normal file
View File

@ -0,0 +1,565 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Technical architecture documentation for DWN window manager - codebase structure, modules, and internals.">
<title>Architecture - DWN Window Manager</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<a href="index.html" class="logo">
<span class="logo-icon">D</span>
<span>DWN</span>
</a>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Install</a></li>
<li class="dropdown">
<a href="documentation.html" class="active">Docs</a>
<div class="dropdown-menu">
<a href="documentation.html">Getting Started</a>
<a href="shortcuts.html">Keyboard Shortcuts</a>
<a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a>
</div>
</li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
<div class="nav-toggle" onclick="toggleNav()">
<span></span>
<span></span>
<span></span>
</div>
</nav>
</header>
<main>
<section class="hero" style="padding: 8rem 0 4rem;">
<div class="container hero-content">
<h1>Architecture</h1>
<p class="subtitle">
Technical documentation for developers and contributors.
</p>
</div>
</section>
<section class="section">
<div class="container">
<h2>Overview</h2>
<p>
DWN is written in ANSI C (C99) and follows a modular single-responsibility architecture.
A global <code>DWNState</code> singleton manages all state, and the main event loop
dispatches X11 events to specialized modules.
</p>
<div class="card" style="margin: 2rem 0;">
<h3>Project Statistics</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; text-align: center;">
<div>
<div style="font-size: 2rem; font-weight: 700; color: var(--primary);">~10K</div>
<div style="color: var(--text-muted);">Lines of Code</div>
</div>
<div>
<div style="font-size: 2rem; font-weight: 700; color: var(--primary);">12</div>
<div style="color: var(--text-muted);">Core Modules</div>
</div>
<div>
<div style="font-size: 2rem; font-weight: 700; color: var(--primary);">C99</div>
<div style="color: var(--text-muted);">Standard</div>
</div>
<div>
<div style="font-size: 2rem; font-weight: 700; color: var(--primary);">MIT</div>
<div style="color: var(--text-muted);">License</div>
</div>
</div>
</div>
<!-- Directory Structure -->
<h2 id="structure" style="margin-top: 3rem;">Directory Structure</h2>
<div class="code-header">
<span>Project Layout</span>
</div>
<pre><code>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</code></pre>
<!-- Core Modules -->
<h2 id="modules" style="margin-top: 3rem;">Core Modules</h2>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Module</th>
<th>File</th>
<th>Responsibility</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Main</strong></td>
<td><code>main.c</code></td>
<td>X11 initialization, event loop, signal handling, module orchestration</td>
</tr>
<tr>
<td><strong>Client</strong></td>
<td><code>client.c</code></td>
<td>Window lifecycle, focus management, frame creation, client list</td>
</tr>
<tr>
<td><strong>Workspace</strong></td>
<td><code>workspace.c</code></td>
<td>9 virtual desktops, per-workspace state, window assignment</td>
</tr>
<tr>
<td><strong>Layout</strong></td>
<td><code>layout.c</code></td>
<td>Tiling (master+stack), floating, monocle layout algorithms</td>
</tr>
<tr>
<td><strong>Decorations</strong></td>
<td><code>decorations.c</code></td>
<td>Window title bars, borders, decoration rendering</td>
</tr>
<tr>
<td><strong>Panel</strong></td>
<td><code>panel.c</code></td>
<td>Top panel (taskbar, workspace indicators), bottom panel (clock)</td>
</tr>
<tr>
<td><strong>Systray</strong></td>
<td><code>systray.c</code></td>
<td>System tray with WiFi/audio/battery indicators, dropdowns</td>
</tr>
<tr>
<td><strong>Notifications</strong></td>
<td><code>notifications.c</code></td>
<td>D-Bus notification daemon (org.freedesktop.Notifications)</td>
</tr>
<tr>
<td><strong>Atoms</strong></td>
<td><code>atoms.c</code></td>
<td>X11 EWMH/ICCCM atom management and property handling</td>
</tr>
<tr>
<td><strong>Keys</strong></td>
<td><code>keys.c</code></td>
<td>Keyboard shortcut capture, keybinding registry, callbacks</td>
</tr>
<tr>
<td><strong>Config</strong></td>
<td><code>config.c</code></td>
<td>INI-style config loading and parsing</td>
</tr>
<tr>
<td><strong>AI</strong></td>
<td><code>ai.c</code></td>
<td>Async OpenRouter API integration, Exa semantic search</td>
</tr>
<tr>
<td><strong>Util</strong></td>
<td><code>util.c</code></td>
<td>Logging, memory allocation, string utilities, file helpers</td>
</tr>
</tbody>
</table>
</div>
<!-- Module Dependencies -->
<h2 id="dependencies" style="margin-top: 3rem;">Module Dependencies</h2>
<div class="card">
<pre style="margin: 0; background: transparent; border: none; padding: 0;"><code>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</code></pre>
</div>
<!-- Global State -->
<h2 id="state" style="margin-top: 3rem;">Global State (DWNState)</h2>
<p>
All window manager state is centralized in a single <code>DWNState</code> structure.
This simplifies state management and makes the codebase easier to understand.
</p>
<div class="code-header">
<span>include/dwn.h (simplified)</span>
</div>
<pre><code>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</code></pre>
<!-- Event Loop -->
<h2 id="events" style="margin-top: 3rem;">Event Loop</h2>
<p>
DWN uses a traditional X11 event loop with XNextEvent. Events are dispatched
to appropriate handlers based on type.
</p>
<div class="code-header">
<span>main.c (simplified)</span>
</div>
<pre><code>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;
}</code></pre>
<!-- Key Constants -->
<h2 id="constants" style="margin-top: 3rem;">Key Constants</h2>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Constant</th>
<th>Value</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MAX_CLIENTS</code></td>
<td>256</td>
<td>Maximum managed windows</td>
</tr>
<tr>
<td><code>MAX_WORKSPACES</code></td>
<td>9</td>
<td>Number of virtual desktops</td>
</tr>
<tr>
<td><code>MAX_MONITORS</code></td>
<td>8</td>
<td>Multi-monitor support limit</td>
</tr>
<tr>
<td><code>MAX_NOTIFICATIONS</code></td>
<td>32</td>
<td>Concurrent notifications</td>
</tr>
<tr>
<td><code>MAX_KEYBINDINGS</code></td>
<td>64</td>
<td>Registered keyboard shortcuts</td>
</tr>
</tbody>
</table>
</div>
<!-- Coding Conventions -->
<h2 id="conventions" style="margin-top: 3rem;">Coding Conventions</h2>
<div class="features-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="card">
<h3>Naming</h3>
<ul style="padding-left: 1.25rem;">
<li><code>snake_case</code> for functions and variables</li>
<li><code>CamelCase</code> for types and structs</li>
<li>Module prefix for functions (e.g., <code>client_focus()</code>)</li>
<li>Constants in <code>UPPER_SNAKE_CASE</code></li>
</ul>
</div>
<div class="card">
<h3>Style</h3>
<ul style="padding-left: 1.25rem;">
<li>4-space indentation</li>
<li>K&R brace style</li>
<li>Max 100 characters per line</li>
<li>clang-format for consistency</li>
</ul>
</div>
</div>
<div class="code-header" style="margin-top: 1.5rem;">
<span>Example Function</span>
</div>
<pre><code>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);
}</code></pre>
<!-- EWMH/ICCCM -->
<h2 id="protocols" style="margin-top: 3rem;">EWMH/ICCCM Support</h2>
<p>
DWN implements key Extended Window Manager Hints and ICCCM protocols
for compatibility with modern applications.
</p>
<div class="features-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="card">
<h3>EWMH Atoms</h3>
<ul style="padding-left: 1.25rem; font-family: var(--font-mono); font-size: 0.875rem;">
<li>_NET_SUPPORTED</li>
<li>_NET_CLIENT_LIST</li>
<li>_NET_CLIENT_LIST_STACKING</li>
<li>_NET_ACTIVE_WINDOW</li>
<li>_NET_CURRENT_DESKTOP</li>
<li>_NET_NUMBER_OF_DESKTOPS</li>
<li>_NET_WM_STATE</li>
<li>_NET_WM_STATE_FULLSCREEN</li>
<li>_NET_WM_STATE_MAXIMIZED_*</li>
<li>_NET_WM_WINDOW_TYPE</li>
<li>_NET_WM_NAME</li>
</ul>
</div>
<div class="card">
<h3>ICCCM Support</h3>
<ul style="padding-left: 1.25rem; font-family: var(--font-mono); font-size: 0.875rem;">
<li>WM_STATE</li>
<li>WM_PROTOCOLS</li>
<li>WM_DELETE_WINDOW</li>
<li>WM_TAKE_FOCUS</li>
<li>WM_NORMAL_HINTS</li>
<li>WM_SIZE_HINTS</li>
<li>WM_CLASS</li>
<li>WM_NAME</li>
<li>WM_TRANSIENT_FOR</li>
</ul>
</div>
</div>
<!-- Build System -->
<h2 id="build" style="margin-top: 3rem;">Build System</h2>
<p>
DWN uses a simple Makefile-based build system with pkg-config for dependency detection.
</p>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Target</th>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Build (release)</td>
<td><code>make</code></td>
<td>Optimized build with -O2</td>
</tr>
<tr>
<td>Build (debug)</td>
<td><code>make debug</code></td>
<td>Debug symbols, -DDEBUG flag</td>
</tr>
<tr>
<td>Install</td>
<td><code>sudo make install</code></td>
<td>Install to PREFIX (/usr/local)</td>
</tr>
<tr>
<td>Clean</td>
<td><code>make clean</code></td>
<td>Remove build artifacts</td>
</tr>
<tr>
<td>Format</td>
<td><code>make format</code></td>
<td>Run clang-format on sources</td>
</tr>
<tr>
<td>Check</td>
<td><code>make check</code></td>
<td>Run cppcheck static analysis</td>
</tr>
<tr>
<td>Test</td>
<td><code>make run</code></td>
<td>Run in Xephyr nested server</td>
</tr>
<tr>
<td>Dependencies</td>
<td><code>make deps</code></td>
<td>Auto-install for your distro</td>
</tr>
</tbody>
</table>
</div>
<!-- Contributing -->
<h2 id="contributing" style="margin-top: 3rem;">Contributing</h2>
<p>
Contributions are welcome! Here's how to get started:
</p>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h4>Fork & Clone</h4>
<p>Fork the repository and clone your fork locally.</p>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h4>Create a Branch</h4>
<p>Create a feature branch: <code>git checkout -b feature/my-feature</code></p>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<h4>Make Changes</h4>
<p>Follow coding conventions. Run <code>make format</code> and <code>make check</code>.</p>
</div>
</div>
<div class="step">
<div class="step-number">4</div>
<div class="step-content">
<h4>Test</h4>
<p>Test your changes with <code>make run</code> in a nested X server.</p>
</div>
</div>
<div class="step">
<div class="step-number">5</div>
<div class="step-content">
<h4>Submit PR</h4>
<p>Push your branch and open a pull request with a clear description.</p>
</div>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
<div class="footer-section">
<h4>DWN Window Manager</h4>
<p style="color: var(--text-muted);">
A modern, production-ready X11 window manager with XFCE-like
functionality and optional AI integration.
</p>
</div>
<div class="footer-section">
<h4>Documentation</h4>
<ul>
<li><a href="documentation.html">Getting Started</a></li>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Resources</h4>
<ul>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Installation</a></li>
<li><a href="ai-features.html">AI Integration</a></li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Community</h4>
<ul>
<li><a href="https://github.com/dwn/dwn/issues">Issue Tracker</a></li>
<li><a href="https://github.com/dwn/dwn/discussions">Discussions</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/CONTRIBUTING.md">Contributing</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/LICENSE">License (MIT)</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>DWN Window Manager - Open Source under the MIT License</p>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

520
site/configuration.html Normal file
View File

@ -0,0 +1,520 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Complete configuration guide for DWN window manager - customize colors, behavior, and more.">
<title>Configuration - DWN Window Manager</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<a href="index.html" class="logo">
<span class="logo-icon">D</span>
<span>DWN</span>
</a>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Install</a></li>
<li class="dropdown">
<a href="documentation.html" class="active">Docs</a>
<div class="dropdown-menu">
<a href="documentation.html">Getting Started</a>
<a href="shortcuts.html">Keyboard Shortcuts</a>
<a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a>
</div>
</li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
<div class="nav-toggle" onclick="toggleNav()">
<span></span>
<span></span>
<span></span>
</div>
</nav>
</header>
<main>
<section class="hero" style="padding: 8rem 0 4rem;">
<div class="container hero-content">
<h1>Configuration Guide</h1>
<p class="subtitle">
Customize every aspect of DWN to match your workflow and style.
</p>
</div>
</section>
<section class="section">
<div class="container">
<h2>Configuration File</h2>
<p>
DWN reads its configuration from <code>~/.config/dwn/config</code> using an INI-style format.
Changes take effect on restart (or you can reload in a future version).
</p>
<div class="alert alert-info">
<strong class="alert-title">First Run</strong>
<p style="margin: 0;">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.</p>
</div>
<!-- General Section -->
<h2 id="general" style="margin-top: 3rem;">[general] - Core Settings</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Basic behavior settings for applications and focus handling.
</p>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Option</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>terminal</code></td>
<td><code>xfce4-terminal</code></td>
<td>Terminal emulator launched with Ctrl+Alt+T</td>
</tr>
<tr>
<td><code>launcher</code></td>
<td><code>dmenu_run</code></td>
<td>Application launcher for Alt+F2</td>
</tr>
<tr>
<td><code>file_manager</code></td>
<td><code>thunar</code></td>
<td>File manager for Super+E</td>
</tr>
<tr>
<td><code>focus_mode</code></td>
<td><code>click</code></td>
<td><code>click</code> or <code>follow</code> (sloppy focus)</td>
</tr>
<tr>
<td><code>decorations</code></td>
<td><code>true</code></td>
<td>Show window title bars and borders</td>
</tr>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>[general]
terminal = alacritty
launcher = rofi -show run
file_manager = nautilus
focus_mode = click
decorations = true</code></pre>
<!-- Appearance Section -->
<h2 id="appearance" style="margin-top: 3rem;">[appearance] - Visual Settings</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Control the visual appearance of windows, panels, and gaps.
</p>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Option</th>
<th>Default</th>
<th>Range</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>border_width</code></td>
<td><code>2</code></td>
<td>0-50</td>
<td>Window border width in pixels</td>
</tr>
<tr>
<td><code>title_height</code></td>
<td><code>24</code></td>
<td>0-100</td>
<td>Title bar height in pixels</td>
</tr>
<tr>
<td><code>panel_height</code></td>
<td><code>28</code></td>
<td>0-100</td>
<td>Top/bottom panel height</td>
</tr>
<tr>
<td><code>gap</code></td>
<td><code>4</code></td>
<td>0-100</td>
<td>Gap between tiled windows</td>
</tr>
<tr>
<td><code>font</code></td>
<td><code>fixed</code></td>
<td>-</td>
<td>X11 font name for text</td>
</tr>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>[appearance]
border_width = 2
title_height = 28
panel_height = 32
gap = 8
font = DejaVu Sans-10</code></pre>
<!-- Layout Section -->
<h2 id="layout" style="margin-top: 3rem;">[layout] - Layout Behavior</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Configure the default layout mode and tiling parameters.
</p>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Option</th>
<th>Default</th>
<th>Range</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>default</code></td>
<td><code>tiling</code></td>
<td>-</td>
<td><code>tiling</code>, <code>floating</code>, or <code>monocle</code></td>
</tr>
<tr>
<td><code>master_ratio</code></td>
<td><code>0.55</code></td>
<td>0.1-0.9</td>
<td>Portion of screen for master area</td>
</tr>
<tr>
<td><code>master_count</code></td>
<td><code>1</code></td>
<td>1-10</td>
<td>Number of windows in master area</td>
</tr>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>[layout]
default = tiling
master_ratio = 0.60
master_count = 1</code></pre>
<!-- Panels Section -->
<h2 id="panels" style="margin-top: 3rem;">[panels] - Panel Visibility</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Control which panels are displayed.
</p>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Option</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>top</code></td>
<td><code>true</code></td>
<td>Show top panel (workspaces, taskbar, systray)</td>
</tr>
<tr>
<td><code>bottom</code></td>
<td><code>true</code></td>
<td>Show bottom panel (clock)</td>
</tr>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example - Minimal Setup</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>[panels]
top = true
bottom = false</code></pre>
<!-- Colors Section -->
<h2 id="colors" style="margin-top: 3rem;">[colors] - Color Scheme</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Customize all colors using hex format (#RRGGBB).
</p>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Option</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>panel_bg</code></td>
<td><code>#1a1a2e</code></td>
<td>Panel background color</td>
</tr>
<tr>
<td><code>panel_fg</code></td>
<td><code>#e0e0e0</code></td>
<td>Panel text color</td>
</tr>
<tr>
<td><code>workspace_active</code></td>
<td><code>#4a90d9</code></td>
<td>Active workspace indicator</td>
</tr>
<tr>
<td><code>workspace_inactive</code></td>
<td><code>#3a3a4e</code></td>
<td>Inactive workspace indicator</td>
</tr>
<tr>
<td><code>workspace_urgent</code></td>
<td><code>#d94a4a</code></td>
<td>Urgent workspace indicator</td>
</tr>
<tr>
<td><code>title_focused_bg</code></td>
<td><code>#2d3a4a</code></td>
<td>Focused window title background</td>
</tr>
<tr>
<td><code>title_focused_fg</code></td>
<td><code>#ffffff</code></td>
<td>Focused window title text</td>
</tr>
<tr>
<td><code>title_unfocused_bg</code></td>
<td><code>#1a1a1a</code></td>
<td>Unfocused window title background</td>
</tr>
<tr>
<td><code>title_unfocused_fg</code></td>
<td><code>#808080</code></td>
<td>Unfocused window title text</td>
</tr>
<tr>
<td><code>border_focused</code></td>
<td><code>#4a90d9</code></td>
<td>Focused window border</td>
</tr>
<tr>
<td><code>border_unfocused</code></td>
<td><code>#333333</code></td>
<td>Unfocused window border</td>
</tr>
<tr>
<td><code>notification_bg</code></td>
<td><code>#2a2a3e</code></td>
<td>Notification background</td>
</tr>
<tr>
<td><code>notification_fg</code></td>
<td><code>#ffffff</code></td>
<td>Notification text</td>
</tr>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example - Nord Theme</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>[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</code></pre>
<!-- AI Section -->
<h2 id="ai" style="margin-top: 3rem;">[ai] - AI Integration</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Configure AI features. See <a href="ai-features.html">AI Features</a> for full setup instructions.
</p>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>model</code></td>
<td>OpenRouter model ID (e.g., <code>google/gemini-2.0-flash-exp:free</code>)</td>
</tr>
<tr>
<td><code>openrouter_api_key</code></td>
<td>Your OpenRouter API key (or use environment variable)</td>
</tr>
<tr>
<td><code>exa_api_key</code></td>
<td>Your Exa API key (or use environment variable)</td>
</tr>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>[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</code></pre>
<div class="alert alert-warning" style="margin-top: 1rem;">
<strong class="alert-title">Security Note</strong>
<p style="margin: 0;">For better security, use environment variables instead of storing API keys in the config file:
<code>export OPENROUTER_API_KEY=sk-or-v1-...</code></p>
</div>
<!-- Complete Example -->
<h2 style="margin-top: 3rem;">Complete Configuration Example</h2>
<div class="code-header">
<span>~/.config/dwn/config</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code># 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</code></pre>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
<div class="footer-section">
<h4>DWN Window Manager</h4>
<p style="color: var(--text-muted);">
A modern, production-ready X11 window manager with XFCE-like
functionality and optional AI integration.
</p>
</div>
<div class="footer-section">
<h4>Documentation</h4>
<ul>
<li><a href="documentation.html">Getting Started</a></li>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Resources</h4>
<ul>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Installation</a></li>
<li><a href="ai-features.html">AI Integration</a></li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Community</h4>
<ul>
<li><a href="https://github.com/dwn/dwn/issues">Issue Tracker</a></li>
<li><a href="https://github.com/dwn/dwn/discussions">Discussions</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/CONTRIBUTING.md">Contributing</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/LICENSE">License (MIT)</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>DWN Window Manager - Open Source under the MIT License</p>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

1085
site/css/style.css Normal file

File diff suppressed because it is too large Load Diff

423
site/documentation.html Normal file
View File

@ -0,0 +1,423 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Getting started with DWN window manager - learn the basics and become productive quickly.">
<title>Documentation - DWN Window Manager</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<a href="index.html" class="logo">
<span class="logo-icon">D</span>
<span>DWN</span>
</a>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Install</a></li>
<li class="dropdown">
<a href="documentation.html" class="active">Docs</a>
<div class="dropdown-menu">
<a href="documentation.html">Getting Started</a>
<a href="shortcuts.html">Keyboard Shortcuts</a>
<a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a>
</div>
</li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
<div class="nav-toggle" onclick="toggleNav()">
<span></span>
<span></span>
<span></span>
</div>
</nav>
</header>
<div class="docs-layout container">
<aside class="docs-sidebar">
<ul>
<li>
<span class="section-title">Getting Started</span>
<ul>
<li><a href="#introduction" class="active">Introduction</a></li>
<li><a href="#first-steps">First Steps</a></li>
<li><a href="#basic-concepts">Basic Concepts</a></li>
<li><a href="#tutorial">Interactive Tutorial</a></li>
</ul>
</li>
<li>
<span class="section-title">Core Usage</span>
<ul>
<li><a href="#windows">Managing Windows</a></li>
<li><a href="#workspaces">Using Workspaces</a></li>
<li><a href="#layouts">Layout Modes</a></li>
<li><a href="#panels">Panels & Systray</a></li>
</ul>
</li>
<li>
<span class="section-title">Reference</span>
<ul>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li>
<li><a href="ai-features.html">AI Features</a></li>
<li><a href="architecture.html">Architecture</a></li>
</ul>
</li>
</ul>
</aside>
<main class="docs-content">
<h1 id="introduction">Getting Started with DWN</h1>
<p class="lead">
Learn the fundamentals of DWN and become productive in minutes.
</p>
<h2 id="first-steps">First Steps</h2>
<p>
After <a href="installation.html">installing DWN</a> 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.
</p>
<h3>Opening Your First Application</h3>
<p>Start by launching a terminal and application launcher:</p>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>T</kbd></td>
<td>Open terminal</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>F2</kbd></td>
<td>Open application launcher (dmenu/rofi)</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>E</kbd></td>
<td>Open file manager</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>B</kbd></td>
<td>Open web browser</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-info">
<strong class="alert-title">Tip: Run the Tutorial</strong>
<p style="margin: 0;">Press <kbd>Super</kbd> + <kbd>T</kbd> to start an interactive
tutorial that will guide you through all essential shortcuts.</p>
</div>
<h2 id="basic-concepts">Basic Concepts</h2>
<h3>The Super Key</h3>
<p>
Most DWN shortcuts use the <kbd>Super</kbd> key (often the Windows key or Command key).
This keeps shortcuts separate from application shortcuts that typically use
<kbd>Ctrl</kbd> or <kbd>Alt</kbd>.
</p>
<h3>Focus Model</h3>
<p>
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.
</p>
<h3>Window Decorations</h3>
<p>
Each window has a title bar showing its name. The title bar color indicates focus:
</p>
<ul>
<li><strong>Blue title bar</strong> - Focused window</li>
<li><strong>Gray title bar</strong> - Unfocused window</li>
</ul>
<h2 id="tutorial">Interactive Tutorial</h2>
<p>
DWN includes a built-in interactive tutorial that teaches you essential shortcuts
step by step. The tutorial:
</p>
<ul>
<li>Shows instructions for each shortcut</li>
<li>Waits for you to press the correct keys</li>
<li>Automatically advances when you complete each step</li>
<li>Covers all essential shortcuts from basic to advanced</li>
</ul>
<div class="card">
<h3>Start the Tutorial</h3>
<p>Press <kbd>Super</kbd> + <kbd>T</kbd> at any time to start or resume the tutorial.</p>
</div>
<h2 id="windows">Managing Windows</h2>
<h3>Window Operations</h3>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Alt</kbd> + <kbd>F4</kbd></td>
<td>Close focused window</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>Tab</kbd></td>
<td>Cycle to next window</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
<td>Cycle to previous window</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>F10</kbd></td>
<td>Toggle maximize</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>F11</kbd></td>
<td>Toggle fullscreen</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>F9</kbd></td>
<td>Toggle floating for current window</td>
</tr>
</tbody>
</table>
</div>
<h3>Moving and Resizing</h3>
<p>In floating mode, you can move and resize windows with the mouse:</p>
<ul>
<li><strong>Move</strong> - Click and drag the title bar</li>
<li><strong>Resize</strong> - Drag any window edge or corner</li>
</ul>
<h2 id="workspaces">Using Workspaces</h2>
<p>
DWN provides 9 virtual workspaces to organize your windows. You can see which
workspaces are active in the top panel.
</p>
<h3>Workspace Navigation</h3>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>F1</kbd> - <kbd>F9</kbd></td>
<td>Switch to workspace 1-9</td>
</tr>
<tr>
<td><kbd>Shift</kbd> + <kbd>F1</kbd> - <kbd>F9</kbd></td>
<td>Move window to workspace 1-9</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>Right</kbd></td>
<td>Next workspace</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>Left</kbd></td>
<td>Previous workspace</td>
</tr>
</tbody>
</table>
</div>
<h3>Workspace Organization Tips</h3>
<ul>
<li><strong>Workspace 1</strong> - Main work (editor, terminal)</li>
<li><strong>Workspace 2</strong> - Web browser, documentation</li>
<li><strong>Workspace 3</strong> - Communication (email, chat)</li>
<li><strong>Workspace 4-9</strong> - Project-specific contexts</li>
</ul>
<h2 id="layouts">Layout Modes</h2>
<p>
DWN supports three layout modes. Press <kbd>Super</kbd> + <kbd>Space</kbd> to cycle
through them.
</p>
<div class="features-grid" style="grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 1.5rem 0;">
<div class="card" style="padding: 1.5rem;">
<h4>Tiling</h4>
<p style="color: var(--text-muted); font-size: 0.9rem;">
Windows automatically arranged in master-stack layout.
Perfect for development workflows.
</p>
</div>
<div class="card" style="padding: 1.5rem;">
<h4>Floating</h4>
<p style="color: var(--text-muted); font-size: 0.9rem;">
Traditional overlapping windows.
Move and resize freely.
</p>
</div>
<div class="card" style="padding: 1.5rem;">
<h4>Monocle</h4>
<p style="color: var(--text-muted); font-size: 0.9rem;">
One fullscreen window at a time.
Great for focused work.
</p>
</div>
</div>
<h3>Tiling Layout Controls</h3>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Super</kbd> + <kbd>H</kbd></td>
<td>Shrink master area</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>L</kbd></td>
<td>Expand master area</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>I</kbd></td>
<td>Increase master window count</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>D</kbd></td>
<td>Decrease master window count</td>
</tr>
</tbody>
</table>
</div>
<h2 id="panels">Panels & System Tray</h2>
<h3>Top Panel</h3>
<p>The top panel contains:</p>
<ul>
<li><strong>Workspace indicators</strong> - Click to switch, highlighted when occupied</li>
<li><strong>Taskbar</strong> - Shows windows on current workspace</li>
<li><strong>System tray</strong> - Battery, volume, WiFi (see below)</li>
</ul>
<h3>System Tray</h3>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Indicator</th>
<th>Click</th>
<th>Right-click</th>
<th>Scroll</th>
</tr>
</thead>
<tbody>
<tr>
<td>Volume</td>
<td>Show slider</td>
<td>Toggle mute</td>
<td>Adjust volume</td>
</tr>
<tr>
<td>WiFi</td>
<td>Show networks</td>
<td>Disconnect</td>
<td>-</td>
</tr>
<tr>
<td>Battery</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
</tbody>
</table>
</div>
<h3>Bottom Panel</h3>
<p>
The bottom panel shows the current time and date. Both panels can be hidden
in the configuration if you prefer a minimal setup.
</p>
<h2>Next Steps</h2>
<p>Now that you know the basics, explore these topics:</p>
<ul>
<li><a href="shortcuts.html">Complete Keyboard Shortcuts Reference</a></li>
<li><a href="configuration.html">Customizing DWN</a></li>
<li><a href="ai-features.html">Using AI Features</a></li>
</ul>
</main>
</div>
<footer>
<div class="container">
<div class="footer-grid">
<div class="footer-section">
<h4>DWN Window Manager</h4>
<p style="color: var(--text-muted);">
A modern, production-ready X11 window manager with XFCE-like
functionality and optional AI integration.
</p>
</div>
<div class="footer-section">
<h4>Documentation</h4>
<ul>
<li><a href="documentation.html">Getting Started</a></li>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Resources</h4>
<ul>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Installation</a></li>
<li><a href="ai-features.html">AI Integration</a></li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Community</h4>
<ul>
<li><a href="https://github.com/dwn/dwn/issues">Issue Tracker</a></li>
<li><a href="https://github.com/dwn/dwn/discussions">Discussions</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/CONTRIBUTING.md">Contributing</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/LICENSE">License (MIT)</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>DWN Window Manager - Open Source under the MIT License</p>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

411
site/features.html Normal file
View File

@ -0,0 +1,411 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Explore the powerful features of DWN window manager - tiling layouts, workspaces, AI integration, and more.">
<title>Features - DWN Window Manager</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<a href="index.html" class="logo">
<span class="logo-icon">D</span>
<span>DWN</span>
</a>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="features.html" class="active">Features</a></li>
<li><a href="installation.html">Install</a></li>
<li class="dropdown">
<a href="documentation.html">Docs</a>
<div class="dropdown-menu">
<a href="documentation.html">Getting Started</a>
<a href="shortcuts.html">Keyboard Shortcuts</a>
<a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a>
</div>
</li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
<div class="nav-toggle" onclick="toggleNav()">
<span></span>
<span></span>
<span></span>
</div>
</nav>
</header>
<main>
<section class="hero" style="padding: 8rem 0 4rem;">
<div class="container hero-content">
<h1>Powerful Features</h1>
<p class="subtitle">
Everything you need for a productive desktop experience, without the bloat.
</p>
</div>
</section>
<!-- Window Management -->
<section class="section">
<div class="container">
<h2>Window Management</h2>
<p style="color: var(--text-muted); max-width: 700px; margin-bottom: 2rem;">
DWN provides flexible window management that adapts to your workflow, whether you prefer
the precision of tiling or the freedom of floating windows.
</p>
<div class="features-grid">
<div class="card">
<h3><span class="card-icon">&#9783;</span> Tiling Layout</h3>
<p>Master-stack tiling with configurable master area ratio. Windows automatically
organize into a primary area and a stack, maximizing screen real estate.</p>
<ul style="margin-top: 1rem; padding-left: 1.25rem; color: var(--text-muted);">
<li>Adjustable master area ratio (0.1 - 0.9)</li>
<li>Multiple windows in master area</li>
<li>Smart stack arrangement</li>
<li>Configurable gaps between windows</li>
</ul>
</div>
<div class="card">
<h3><span class="card-icon">&#10063;</span> Floating Layout</h3>
<p>Traditional floating window management with drag-and-drop positioning.
Perfect for workflows that need overlapping windows or free-form arrangement.</p>
<ul style="margin-top: 1rem; padding-left: 1.25rem; color: var(--text-muted);">
<li>Click and drag to move windows</li>
<li>Resize from any edge or corner</li>
<li>Window snapping support</li>
<li>Respect minimum size hints</li>
</ul>
</div>
<div class="card">
<h3><span class="card-icon">&#9744;</span> Monocle Layout</h3>
<p>Full-screen single window mode for focused work. Each window takes up
the entire workspace, perfect for presentations or deep concentration.</p>
<ul style="margin-top: 1rem; padding-left: 1.25rem; color: var(--text-muted);">
<li>Maximize focused window</li>
<li>Quick window cycling</li>
<li>Ideal for single-task focus</li>
<li>Works great on small screens</li>
</ul>
</div>
</div>
<div class="alert alert-info" style="margin-top: 2rem;">
<strong class="alert-title">Pro Tip</strong>
<p style="margin: 0;">Switch layouts instantly with <kbd>Super</kbd> + <kbd>Space</kbd>.
Your window arrangement is preserved when switching back.</p>
</div>
</div>
</section>
<!-- Workspaces -->
<section class="section section-alt">
<div class="container">
<h2>Virtual Workspaces</h2>
<p style="color: var(--text-muted); max-width: 700px; margin-bottom: 2rem;">
Nine virtual desktops give you unlimited room to organize your work.
Each workspace maintains its own window state and layout preferences.
</p>
<div class="features-grid" style="grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));">
<div class="feature-card">
<div class="feature-icon">1-9</div>
<h3>9 Workspaces</h3>
<p>Quick access via F1-F9 keys. Organize projects, contexts, or tasks across
dedicated spaces.</p>
</div>
<div class="feature-card">
<div class="feature-icon">&#8596;</div>
<h3>Window Transfer</h3>
<p>Move windows between workspaces with Shift+F1-F9. Quick and keyboard-driven.</p>
</div>
<div class="feature-card">
<div class="feature-icon">&#128200;</div>
<h3>Per-Workspace State</h3>
<p>Each workspace remembers its layout mode, window positions, and focused window.</p>
</div>
<div class="feature-card">
<div class="feature-icon">&#128065;</div>
<h3>Visual Indicators</h3>
<p>Panel shows active and occupied workspaces at a glance with color coding.</p>
</div>
</div>
</div>
</section>
<!-- Panel & System Tray -->
<section class="section">
<div class="container">
<h2>Panels & System Tray</h2>
<p style="color: var(--text-muted); max-width: 700px; margin-bottom: 2rem;">
Built-in panels provide essential information and quick access to common functions
without needing external tools or status bars.
</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
<div class="card">
<h3>Top Panel</h3>
<p>The top panel contains your main controls and information:</p>
<ul style="margin-top: 1rem; padding-left: 1.25rem; color: var(--text-muted);">
<li><strong>Workspace Indicators</strong> - Click or use shortcuts to switch</li>
<li><strong>Taskbar</strong> - Shows windows on current workspace</li>
<li><strong>System Tray</strong> - Battery, volume, WiFi indicators</li>
</ul>
</div>
<div class="card">
<h3>Bottom Panel</h3>
<p>Optional bottom panel for additional information:</p>
<ul style="margin-top: 1rem; padding-left: 1.25rem; color: var(--text-muted);">
<li><strong>Clock Display</strong> - Time and date</li>
<li><strong>Status Messages</strong> - System notifications</li>
<li><strong>Customizable</strong> - Can be hidden in config</li>
</ul>
</div>
</div>
<h3 style="margin-top: 3rem; margin-bottom: 1.5rem;">System Tray Features</h3>
<div class="features-grid">
<div class="card">
<h3>&#128267; Battery Monitor</h3>
<p>Shows current battery percentage with color-coded status:</p>
<ul style="margin-top: 1rem; padding-left: 1.25rem; color: var(--text-muted);">
<li>Red when below 20%</li>
<li>Blue when charging</li>
<li>Auto-hides on desktops</li>
</ul>
</div>
<div class="card">
<h3>&#128266; Volume Control</h3>
<p>Full audio control at your fingertips:</p>
<ul style="margin-top: 1rem; padding-left: 1.25rem; color: var(--text-muted);">
<li>Click for volume slider</li>
<li>Scroll to adjust</li>
<li>Right-click to mute</li>
</ul>
</div>
<div class="card">
<h3>&#128246; WiFi Manager</h3>
<p>Network management made simple:</p>
<ul style="margin-top: 1rem; padding-left: 1.25rem; color: var(--text-muted);">
<li>Click for network list</li>
<li>Signal strength indicators</li>
<li>Current SSID display</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Notifications -->
<section class="section section-alt">
<div class="container">
<h2>Notification System</h2>
<p style="color: var(--text-muted); max-width: 700px; margin-bottom: 2rem;">
Built-in D-Bus notification daemon following freedesktop.org standards.
No need for external notification tools like dunst or notify-osd.
</p>
<div class="features-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="card">
<h3>Standards Compliant</h3>
<p>Implements the org.freedesktop.Notifications D-Bus interface.
Works seamlessly with any application that sends desktop notifications.</p>
</div>
<div class="card">
<h3>Customizable Appearance</h3>
<p>Configure notification colors and positioning through the config file.
Notifications match your overall color scheme automatically.</p>
</div>
</div>
<div class="alert alert-success" style="margin-top: 2rem;">
<strong class="alert-title">Capacity</strong>
<p style="margin: 0;">DWN can display up to 32 notifications simultaneously,
with automatic queuing and timeout management.</p>
</div>
</div>
</section>
<!-- AI Features Preview -->
<section class="section">
<div class="container">
<h2>AI Integration</h2>
<p style="color: var(--text-muted); max-width: 700px; margin-bottom: 2rem;">
Optional AI features powered by OpenRouter API and Exa semantic search.
Control your desktop with natural language and get intelligent assistance.
</p>
<div class="features-grid">
<div class="card">
<h3>&#129302; AI Command Palette</h3>
<p>Press <kbd>Super</kbd> + <kbd>Shift</kbd> + <kbd>A</kbd> and type natural
language commands like "open firefox" or "launch terminal".</p>
<a href="ai-features.html" class="btn btn-sm btn-secondary" style="margin-top: 1rem;">Learn More</a>
</div>
<div class="card">
<h3>&#128269; Semantic Web Search</h3>
<p>Search the web semantically with Exa integration. Find relevant content based
on meaning, not just keywords.</p>
<a href="ai-features.html" class="btn btn-sm btn-secondary" style="margin-top: 1rem;">Learn More</a>
</div>
<div class="card">
<h3>&#127891; Context Analysis</h3>
<p>AI analyzes your current workspace to understand what you're working on
and provides relevant suggestions.</p>
<a href="ai-features.html" class="btn btn-sm btn-secondary" style="margin-top: 1rem;">Learn More</a>
</div>
</div>
</div>
</section>
<!-- EWMH/ICCCM -->
<section class="section section-alt">
<div class="container">
<h2>Standards Compliance</h2>
<p style="color: var(--text-muted); max-width: 700px; margin-bottom: 2rem;">
DWN implements EWMH and ICCCM protocols for maximum compatibility with X11 applications.
</p>
<div class="features-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="card">
<h3>EWMH Support</h3>
<p>Extended Window Manager Hints for modern application features:</p>
<ul style="margin-top: 1rem; padding-left: 1.25rem; color: var(--text-muted);">
<li>_NET_WM_STATE (fullscreen, maximized, etc.)</li>
<li>_NET_ACTIVE_WINDOW</li>
<li>_NET_CLIENT_LIST and _NET_CLIENT_LIST_STACKING</li>
<li>_NET_CURRENT_DESKTOP and _NET_NUMBER_OF_DESKTOPS</li>
<li>_NET_WM_WINDOW_TYPE</li>
</ul>
</div>
<div class="card">
<h3>ICCCM Compliance</h3>
<p>Inter-Client Communication Conventions Manual support:</p>
<ul style="margin-top: 1rem; padding-left: 1.25rem; color: var(--text-muted);">
<li>WM_STATE management</li>
<li>WM_PROTOCOLS (WM_DELETE_WINDOW, WM_TAKE_FOCUS)</li>
<li>WM_NORMAL_HINTS (size hints)</li>
<li>WM_CLASS for window matching</li>
<li>WM_NAME and _NET_WM_NAME</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Technical Specs -->
<section class="section">
<div class="container">
<h2>Technical Specifications</h2>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Specification</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Language</td>
<td>ANSI C (C99)</td>
</tr>
<tr>
<td>Maximum Clients</td>
<td>256 windows</td>
</tr>
<tr>
<td>Workspaces</td>
<td>9 virtual desktops</td>
</tr>
<tr>
<td>Monitor Support</td>
<td>Up to 8 monitors (Xinerama/Xrandr)</td>
</tr>
<tr>
<td>Notifications</td>
<td>32 concurrent</td>
</tr>
<tr>
<td>Keybindings</td>
<td>64 configurable shortcuts</td>
</tr>
<tr>
<td>Memory Usage</td>
<td>&lt; 5MB typical</td>
</tr>
<tr>
<td>Configuration</td>
<td>INI-style (~/.config/dwn/config)</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- CTA -->
<section class="section section-alt">
<div class="container" style="text-align: center;">
<h2>Ready to Try DWN?</h2>
<p style="color: var(--text-muted); max-width: 500px; margin: 0 auto 2rem;">
Get started in minutes with our simple installation process.
</p>
<div class="hero-buttons" style="justify-content: center;">
<a href="installation.html" class="btn btn-primary btn-lg">Install Now</a>
<a href="shortcuts.html" class="btn btn-secondary btn-lg">View Shortcuts</a>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
<div class="footer-section">
<h4>DWN Window Manager</h4>
<p style="color: var(--text-muted);">
A modern, production-ready X11 window manager with XFCE-like
functionality and optional AI integration.
</p>
</div>
<div class="footer-section">
<h4>Documentation</h4>
<ul>
<li><a href="documentation.html">Getting Started</a></li>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Resources</h4>
<ul>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Installation</a></li>
<li><a href="ai-features.html">AI Integration</a></li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Community</h4>
<ul>
<li><a href="https://github.com/dwn/dwn/issues">Issue Tracker</a></li>
<li><a href="https://github.com/dwn/dwn/discussions">Discussions</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/CONTRIBUTING.md">Contributing</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/LICENSE">License (MIT)</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>DWN Window Manager - Open Source under the MIT License</p>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

352
site/index.html Normal file
View File

@ -0,0 +1,352 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="DWN - A modern, production-ready X11 window manager with XFCE-like functionality and optional AI integration.">
<meta name="keywords" content="window manager, X11, Linux, tiling, floating, EWMH, AI, productivity">
<title>DWN Window Manager - Modern X11 Window Management</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<a href="index.html" class="logo">
<span class="logo-icon">D</span>
<span>DWN</span>
</a>
<ul class="nav-links">
<li><a href="index.html" class="active">Home</a></li>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Install</a></li>
<li class="dropdown">
<a href="documentation.html">Docs</a>
<div class="dropdown-menu">
<a href="documentation.html">Getting Started</a>
<a href="shortcuts.html">Keyboard Shortcuts</a>
<a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a>
</div>
</li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
<div class="nav-toggle" onclick="toggleNav()">
<span></span>
<span></span>
<span></span>
</div>
</nav>
</header>
<main>
<!-- Hero Section -->
<section class="hero">
<div class="container hero-content">
<h1>Modern Window Management for X11</h1>
<p class="subtitle">
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.
</p>
<div class="hero-buttons">
<a href="installation.html" class="btn btn-primary btn-lg">Get Started</a>
<a href="features.html" class="btn btn-secondary btn-lg">Explore Features</a>
</div>
</div>
</section>
<!-- Key Features Overview -->
<section class="section">
<div class="container">
<h2 style="text-align: center; margin-bottom: 1rem;">Why Choose DWN?</h2>
<p style="text-align: center; color: var(--text-muted); max-width: 600px; margin: 0 auto 3rem;">
DWN combines the simplicity of traditional floating window managers with the productivity
of tiling layouts, all wrapped in a modern, customizable package.
</p>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">&#9776;</div>
<h3>Multiple Layouts</h3>
<p>Switch seamlessly between tiling, floating, and monocle layouts.
Resize master areas and organize windows exactly how you work.</p>
</div>
<div class="feature-card">
<div class="feature-icon">&#9881;</div>
<h3>9 Workspaces</h3>
<p>Organize your workflow across 9 virtual desktops with per-workspace
state. Move windows between workspaces with a single keystroke.</p>
</div>
<div class="feature-card">
<div class="feature-icon">&#127912;</div>
<h3>Fully Customizable</h3>
<p>INI-style configuration with extensive theming options. Customize colors,
fonts, borders, gaps, and behavior to match your style.</p>
</div>
<div class="feature-card">
<div class="feature-icon">&#128161;</div>
<h3>AI Integration</h3>
<p>Optional AI command palette and semantic web search. Control your desktop
with natural language and get intelligent suggestions.</p>
</div>
<div class="feature-card">
<div class="feature-icon">&#128276;</div>
<h3>Notification Daemon</h3>
<p>Built-in D-Bus notification support following freedesktop.org standards.
No need for external notification daemons.</p>
</div>
<div class="feature-card">
<div class="feature-icon">&#128187;</div>
<h3>System Tray</h3>
<p>Integrated system tray with battery, volume, and WiFi indicators.
Volume slider and network selection dropdowns included.</p>
</div>
</div>
</div>
</section>
<!-- Stats -->
<section class="section section-alt">
<div class="container">
<div class="stats">
<div class="stat-item">
<h3>~10K</h3>
<p>Lines of Pure C</p>
</div>
<div class="stat-item">
<h3>0</h3>
<p>Runtime Dependencies</p>
</div>
<div class="stat-item">
<h3>&lt;5MB</h3>
<p>Memory Footprint</p>
</div>
<div class="stat-item">
<h3>64+</h3>
<p>Keyboard Shortcuts</p>
</div>
</div>
</div>
</section>
<!-- Quick Start -->
<section class="section">
<div class="container">
<h2 style="text-align: center;">Get Up and Running in Minutes</h2>
<p style="text-align: center; color: var(--text-muted); max-width: 600px; margin: 0 auto 3rem;">
DWN is designed for easy installation and immediate productivity.
Build from source or use your distribution's package manager.
</p>
<div class="card" style="max-width: 700px; margin: 0 auto;">
<div class="code-header">
<span>Terminal</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code># 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</code></pre>
</div>
<p style="text-align: center; margin-top: 2rem;">
<a href="installation.html" class="btn btn-primary">Full Installation Guide</a>
</p>
</div>
</section>
<!-- Screenshots Preview -->
<section class="section section-alt">
<div class="container">
<h2 style="text-align: center;">See DWN in Action</h2>
<p style="text-align: center; color: var(--text-muted); max-width: 600px; margin: 0 auto 3rem;">
A clean, modern interface that stays out of your way while providing everything you need.
</p>
<div class="screenshot-grid">
<div class="screenshot">
<div class="screenshot-placeholder">
[Tiling Layout with Terminal and Editor]
</div>
<div class="screenshot-caption">Master-stack tiling layout perfect for development</div>
</div>
<div class="screenshot">
<div class="screenshot-placeholder">
[System Tray and Notifications]
</div>
<div class="screenshot-caption">Integrated system tray with volume and WiFi controls</div>
</div>
<div class="screenshot">
<div class="screenshot-placeholder">
[AI Command Palette]
</div>
<div class="screenshot-caption">AI-powered command palette for natural language control</div>
</div>
</div>
</div>
</section>
<!-- Comparison -->
<section class="section">
<div class="container">
<h2 style="text-align: center;">How DWN Compares</h2>
<p style="text-align: center; color: var(--text-muted); max-width: 600px; margin: 0 auto 3rem;">
DWN bridges the gap between minimal tiling managers and full desktop environments.
</p>
<div class="comparison">
<div class="comparison-card">
<h3>Minimal Tiling WMs</h3>
<p style="color: var(--text-muted);">dwm, i3, bspwm</p>
<ul>
<li>Lightweight and fast</li>
<li>Keyboard-driven workflow</li>
<li>Highly customizable</li>
<li>Steep learning curve</li>
<li>Requires additional tools</li>
</ul>
</div>
<div class="comparison-card featured">
<h3>DWN</h3>
<p style="color: var(--text-muted);">Best of both worlds</p>
<ul>
<li>Lightweight and fast</li>
<li>Keyboard-driven workflow</li>
<li>Highly customizable</li>
<li>Interactive tutorial</li>
<li>Built-in panels and systray</li>
<li>AI integration optional</li>
<li>Notification daemon included</li>
</ul>
</div>
<div class="comparison-card">
<h3>Desktop Environments</h3>
<p style="color: var(--text-muted);">XFCE, GNOME, KDE</p>
<ul>
<li>Feature complete</li>
<li>User-friendly</li>
<li>Heavy resource usage</li>
<li>Less customizable</li>
<li>Slower performance</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Testimonials -->
<section class="section section-alt">
<div class="container">
<h2 style="text-align: center;">What Users Say</h2>
<div class="testimonials">
<div class="testimonial">
<p class="testimonial-text">
"Finally, a window manager that doesn't make me choose between
productivity and usability. DWN just works."
</p>
<div class="testimonial-author">
<div class="testimonial-avatar">JD</div>
<div class="testimonial-info">
<strong>John Developer</strong>
<span>Senior Software Engineer</span>
</div>
</div>
</div>
<div class="testimonial">
<p class="testimonial-text">
"The AI command palette changed how I interact with my desktop.
It's like having an assistant for window management."
</p>
<div class="testimonial-author">
<div class="testimonial-avatar">SA</div>
<div class="testimonial-info">
<strong>Sarah Admin</strong>
<span>System Administrator</span>
</div>
</div>
</div>
<div class="testimonial">
<p class="testimonial-text">
"Coming from i3, the learning curve was minimal. The built-in
tutorial had me productive in under 10 minutes."
</p>
<div class="testimonial-author">
<div class="testimonial-avatar">ML</div>
<div class="testimonial-info">
<strong>Mike Linux</strong>
<span>DevOps Engineer</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="section">
<div class="container" style="text-align: center;">
<h2>Ready to Transform Your Desktop?</h2>
<p style="color: var(--text-muted); max-width: 500px; margin: 0 auto 2rem;">
Join thousands of developers and power users who have made DWN their window manager of choice.
</p>
<div class="hero-buttons" style="justify-content: center;">
<a href="installation.html" class="btn btn-primary btn-lg">Install DWN</a>
<a href="documentation.html" class="btn btn-secondary btn-lg">Read the Docs</a>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
<div class="footer-section">
<h4>DWN Window Manager</h4>
<p style="color: var(--text-muted);">
A modern, production-ready X11 window manager with XFCE-like
functionality and optional AI integration.
</p>
</div>
<div class="footer-section">
<h4>Documentation</h4>
<ul>
<li><a href="documentation.html">Getting Started</a></li>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Resources</h4>
<ul>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Installation</a></li>
<li><a href="ai-features.html">AI Integration</a></li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Community</h4>
<ul>
<li><a href="https://github.com/dwn/dwn/issues">Issue Tracker</a></li>
<li><a href="https://github.com/dwn/dwn/discussions">Discussions</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/CONTRIBUTING.md">Contributing</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/LICENSE">License (MIT)</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>DWN Window Manager - Open Source under the MIT License</p>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

610
site/installation.html Normal file
View File

@ -0,0 +1,610 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Install DWN window manager - step by step guide for Debian, Ubuntu, Fedora, Arch Linux and more.">
<title>Installation - DWN Window Manager</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<a href="index.html" class="logo">
<span class="logo-icon">D</span>
<span>DWN</span>
</a>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html" class="active">Install</a></li>
<li class="dropdown">
<a href="documentation.html">Docs</a>
<div class="dropdown-menu">
<a href="documentation.html">Getting Started</a>
<a href="shortcuts.html">Keyboard Shortcuts</a>
<a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a>
</div>
</li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
<div class="nav-toggle" onclick="toggleNav()">
<span></span>
<span></span>
<span></span>
</div>
</nav>
</header>
<main>
<section class="hero" style="padding: 8rem 0 4rem;">
<div class="container hero-content">
<h1>Installation Guide</h1>
<p class="subtitle">
Get DWN running on your system in just a few minutes.
</p>
</div>
</section>
<!-- Requirements -->
<section class="section">
<div class="container">
<h2>Requirements</h2>
<p style="color: var(--text-muted); margin-bottom: 2rem;">
DWN requires X11 and a few common libraries. Most Linux distributions include these by default.
</p>
<div class="card">
<h3>Required Dependencies</h3>
<div class="table-wrapper" style="margin-top: 1rem;">
<table>
<thead>
<tr>
<th>Library</th>
<th>Purpose</th>
<th>Package (Debian/Ubuntu)</th>
</tr>
</thead>
<tbody>
<tr>
<td>libX11</td>
<td>X Window System client library</td>
<td><code>libx11-dev</code></td>
</tr>
<tr>
<td>libXext</td>
<td>X extensions library</td>
<td><code>libxext-dev</code></td>
</tr>
<tr>
<td>libXinerama</td>
<td>Multi-monitor support</td>
<td><code>libxinerama-dev</code></td>
</tr>
<tr>
<td>libXrandr</td>
<td>Display configuration</td>
<td><code>libxrandr-dev</code></td>
</tr>
<tr>
<td>libXft</td>
<td>Font rendering</td>
<td><code>libxft-dev</code></td>
</tr>
<tr>
<td>fontconfig</td>
<td>Font configuration</td>
<td><code>libfontconfig1-dev</code></td>
</tr>
<tr>
<td>libdbus-1</td>
<td>D-Bus for notifications</td>
<td><code>libdbus-1-dev</code></td>
</tr>
<tr>
<td>libcurl</td>
<td>AI features (optional)</td>
<td><code>libcurl4-openssl-dev</code></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- Quick Install -->
<section class="section section-alt">
<div class="container">
<h2>Quick Installation</h2>
<p style="color: var(--text-muted); margin-bottom: 2rem;">
The fastest way to get started. Our build system auto-detects your distribution.
</p>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h4>Clone the Repository</h4>
<p>Download the latest source code from GitHub.</p>
<div class="code-header">
<span>Terminal</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>git clone https://github.com/dwn/dwn.git
cd dwn</code></pre>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h4>Install Dependencies</h4>
<p>Automatically install required packages for your distribution.</p>
<div class="code-header">
<span>Terminal</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>make deps</code></pre>
<p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--text-muted);">
Supports Debian, Ubuntu, Fedora, Arch, openSUSE, and Void Linux.
</p>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<h4>Build DWN</h4>
<p>Compile the window manager with optimizations.</p>
<div class="code-header">
<span>Terminal</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>make</code></pre>
</div>
</div>
<div class="step">
<div class="step-number">4</div>
<div class="step-content">
<h4>Install System-wide</h4>
<p>Install the binary to your system PATH.</p>
<div class="code-header">
<span>Terminal</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>sudo make install</code></pre>
<p style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--text-muted);">
Default location: <code>/usr/local/bin/dwn</code>. Override with <code>PREFIX=/custom/path make install</code>
</p>
</div>
</div>
<div class="step">
<div class="step-number">5</div>
<div class="step-content">
<h4>Configure Your Session</h4>
<p>Add DWN to your X session startup.</p>
<div class="code-header">
<span>~/.xinitrc</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>exec dwn</code></pre>
</div>
</div>
</div>
</div>
</section>
<!-- Distribution-specific -->
<section class="section">
<div class="container">
<h2>Distribution-Specific Instructions</h2>
<div class="tabs">
<button class="tab active" onclick="showTab('debian')">Debian/Ubuntu</button>
<button class="tab" onclick="showTab('fedora')">Fedora</button>
<button class="tab" onclick="showTab('arch')">Arch Linux</button>
<button class="tab" onclick="showTab('void')">Void Linux</button>
</div>
<div id="debian" class="tab-content active">
<h3>Debian / Ubuntu / Linux Mint</h3>
<div class="code-header">
<span>Terminal</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code># 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</code></pre>
</div>
<div id="fedora" class="tab-content">
<h3>Fedora / RHEL / CentOS</h3>
<div class="code-header">
<span>Terminal</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code># 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</code></pre>
</div>
<div id="arch" class="tab-content">
<h3>Arch Linux / Manjaro</h3>
<div class="code-header">
<span>Terminal</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code># 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</code></pre>
<div class="alert alert-info" style="margin-top: 1rem;">
<strong>AUR Package</strong>
<p style="margin: 0;">An AUR package <code>dwn-git</code> may also be available:
<code>yay -S dwn-git</code></p>
</div>
</div>
<div id="void" class="tab-content">
<h3>Void Linux</h3>
<div class="code-header">
<span>Terminal</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code># 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</code></pre>
</div>
</div>
</section>
<!-- Session Setup -->
<section class="section section-alt">
<div class="container">
<h2>Session Setup</h2>
<p style="color: var(--text-muted); margin-bottom: 2rem;">
Configure your display manager or xinit to start DWN.
</p>
<div class="features-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="card">
<h3>Using xinit / startx</h3>
<p>For minimal setups using startx:</p>
<div class="code-header">
<span>~/.xinitrc</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code># Optional: set display settings
xrandr --output DP-1 --mode 2560x1440
# Optional: set wallpaper
feh --bg-fill ~/wallpaper.jpg
# Start DWN
exec dwn</code></pre>
<p style="margin-top: 1rem; font-size: 0.875rem; color: var(--text-muted);">
Then run <code>startx</code> from a TTY.
</p>
</div>
<div class="card">
<h3>Using a Display Manager</h3>
<p>Create a desktop entry for GDM, LightDM, etc:</p>
<div class="code-header">
<span>/usr/share/xsessions/dwn.desktop</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>[Desktop Entry]
Name=DWN
Comment=DWN Window Manager
Exec=dwn
Type=Application
DesktopNames=DWN</code></pre>
<p style="margin-top: 1rem; font-size: 0.875rem; color: var(--text-muted);">
DWN will appear in your display manager's session menu.
</p>
</div>
</div>
</div>
</section>
<!-- Testing -->
<section class="section">
<div class="container">
<h2>Testing in a Nested X Server</h2>
<p style="color: var(--text-muted); margin-bottom: 2rem;">
Test DWN without leaving your current session using Xephyr.
</p>
<div class="card">
<h3>Using make run</h3>
<p>The easiest way to test DWN safely:</p>
<div class="code-header">
<span>Terminal</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code># 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</code></pre>
<p style="margin-top: 1rem; color: var(--text-muted);">
This opens a 1280x720 window running DWN. Perfect for experimenting with configuration changes.
</p>
</div>
<div class="card" style="margin-top: 1.5rem;">
<h3>Manual Xephyr Setup</h3>
<p>For more control over the test environment:</p>
<div class="code-header">
<span>Terminal</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code># 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 &</code></pre>
</div>
</div>
</section>
<!-- Post-install -->
<section class="section section-alt">
<div class="container">
<h2>Post-Installation</h2>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h4>Create Configuration Directory</h4>
<p>DWN will create this automatically on first run, but you can set it up in advance:</p>
<div class="code-header">
<span>Terminal</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>mkdir -p ~/.config/dwn</code></pre>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h4>Run the Interactive Tutorial</h4>
<p>Once DWN is running, press <kbd>Super</kbd> + <kbd>T</kbd> to start the built-in tutorial
that will teach you all the essential shortcuts.</p>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<h4>View All Shortcuts</h4>
<p>Press <kbd>Super</kbd> + <kbd>S</kbd> to see a complete list of keyboard shortcuts.</p>
</div>
</div>
<div class="step">
<div class="step-number">4</div>
<div class="step-content">
<h4>Customize Your Setup</h4>
<p>See the <a href="configuration.html">Configuration Guide</a> to personalize DWN to your liking.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Troubleshooting -->
<section class="section">
<div class="container">
<h2>Troubleshooting</h2>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
DWN doesn't start - "cannot open display"
</button>
<div class="faq-answer">
<div class="faq-answer-content">
<p>This error means DWN can't connect to an X server. Make sure:</p>
<ul style="padding-left: 1.25rem; margin-top: 0.5rem;">
<li>You're running from a TTY with <code>startx</code>, not from within another X session</li>
<li>The DISPLAY environment variable is set correctly</li>
<li>X server is installed and working (<code>Xorg -configure</code> to test)</li>
</ul>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
Build fails - "pkg-config: command not found"
</button>
<div class="faq-answer">
<div class="faq-answer-content">
<p>Install pkg-config for your distribution:</p>
<pre style="margin-top: 0.5rem;"><code>sudo apt install pkg-config # Debian/Ubuntu
sudo dnf install pkg-config # Fedora
sudo pacman -S pkg-config # Arch</code></pre>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
Missing header files during build
</button>
<div class="faq-answer">
<div class="faq-answer-content">
<p>Make sure you have the development packages installed, not just the runtime libraries.
On Debian/Ubuntu, install packages ending with <code>-dev</code>.
Run <code>make deps</code> to auto-install everything.</p>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
Keyboard shortcuts don't work
</button>
<div class="faq-answer">
<div class="faq-answer-content">
<p>Check for conflicts with other programs grabbing keys:</p>
<ul style="padding-left: 1.25rem; margin-top: 0.5rem;">
<li>Make sure no other window manager is running</li>
<li>Check if compositor (like picom) is grabbing keys</li>
<li>Verify keyboard layout is correct with <code>setxkbmap</code></li>
</ul>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
Fonts look bad or missing
</button>
<div class="faq-answer">
<div class="faq-answer-content">
<p>DWN uses Xft for font rendering. Install some good fonts:</p>
<pre style="margin-top: 0.5rem;"><code>sudo apt install fonts-dejavu fonts-liberation # Debian/Ubuntu
sudo dnf install dejavu-fonts-all liberation-fonts # Fedora</code></pre>
<p style="margin-top: 0.5rem;">You can configure the font in <code>~/.config/dwn/config</code>.</p>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="section section-alt">
<div class="container" style="text-align: center;">
<h2>Installation Complete?</h2>
<p style="color: var(--text-muted); max-width: 500px; margin: 0 auto 2rem;">
Learn how to use DWN effectively with our documentation.
</p>
<div class="hero-buttons" style="justify-content: center;">
<a href="documentation.html" class="btn btn-primary btn-lg">Getting Started Guide</a>
<a href="shortcuts.html" class="btn btn-secondary btn-lg">Keyboard Shortcuts</a>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
<div class="footer-section">
<h4>DWN Window Manager</h4>
<p style="color: var(--text-muted);">
A modern, production-ready X11 window manager with XFCE-like
functionality and optional AI integration.
</p>
</div>
<div class="footer-section">
<h4>Documentation</h4>
<ul>
<li><a href="documentation.html">Getting Started</a></li>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Resources</h4>
<ul>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Installation</a></li>
<li><a href="ai-features.html">AI Integration</a></li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Community</h4>
<ul>
<li><a href="https://github.com/dwn/dwn/issues">Issue Tracker</a></li>
<li><a href="https://github.com/dwn/dwn/discussions">Discussions</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/CONTRIBUTING.md">Contributing</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/LICENSE">License (MIT)</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>DWN Window Manager - Open Source under the MIT License</p>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

304
site/js/main.js Normal file
View File

@ -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 = '';
});
});

413
site/shortcuts.html Normal file
View File

@ -0,0 +1,413 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Complete keyboard shortcuts reference for DWN window manager.">
<title>Keyboard Shortcuts - DWN Window Manager</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<a href="index.html" class="logo">
<span class="logo-icon">D</span>
<span>DWN</span>
</a>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Install</a></li>
<li class="dropdown">
<a href="documentation.html" class="active">Docs</a>
<div class="dropdown-menu">
<a href="documentation.html">Getting Started</a>
<a href="shortcuts.html">Keyboard Shortcuts</a>
<a href="configuration.html">Configuration</a>
<a href="ai-features.html">AI Features</a>
<a href="architecture.html">Architecture</a>
</div>
</li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
<div class="nav-toggle" onclick="toggleNav()">
<span></span>
<span></span>
<span></span>
</div>
</nav>
</header>
<main>
<section class="hero" style="padding: 8rem 0 4rem;">
<div class="container hero-content">
<h1>Keyboard Shortcuts</h1>
<p class="subtitle">
Complete reference for all DWN keyboard shortcuts.
Press <kbd>Super</kbd> + <kbd>S</kbd> in DWN to view this list.
</p>
</div>
</section>
<section class="section">
<div class="container">
<div class="search-box">
<input type="text" id="shortcut-search" placeholder="Search shortcuts..." onkeyup="filterShortcuts()">
</div>
<!-- Application Launchers -->
<h2 id="launchers">Application Launchers</h2>
<div class="table-wrapper">
<table class="shortcuts-table">
<thead>
<tr>
<th style="width: 40%;">Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>T</kbd></td>
<td>Open terminal (configurable)</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>F2</kbd></td>
<td>Open application launcher (dmenu/rofi)</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>E</kbd></td>
<td>Open file manager (configurable)</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>B</kbd></td>
<td>Open web browser</td>
</tr>
</tbody>
</table>
</div>
<!-- Window Management -->
<h2 id="windows">Window Management</h2>
<div class="table-wrapper">
<table class="shortcuts-table">
<thead>
<tr>
<th style="width: 40%;">Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Alt</kbd> + <kbd>F4</kbd></td>
<td>Close focused window</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>Tab</kbd></td>
<td>Cycle to next window</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
<td>Cycle to previous window</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>F10</kbd></td>
<td>Toggle maximize</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>F11</kbd></td>
<td>Toggle fullscreen</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>F9</kbd></td>
<td>Toggle floating mode for focused window</td>
</tr>
</tbody>
</table>
</div>
<!-- Workspace Navigation -->
<h2 id="workspaces">Workspace Navigation</h2>
<div class="table-wrapper">
<table class="shortcuts-table">
<thead>
<tr>
<th style="width: 40%;">Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>F1</kbd></td>
<td>Switch to workspace 1</td>
</tr>
<tr>
<td><kbd>F2</kbd></td>
<td>Switch to workspace 2</td>
</tr>
<tr>
<td><kbd>F3</kbd></td>
<td>Switch to workspace 3</td>
</tr>
<tr>
<td><kbd>F4</kbd></td>
<td>Switch to workspace 4</td>
</tr>
<tr>
<td><kbd>F5</kbd></td>
<td>Switch to workspace 5</td>
</tr>
<tr>
<td><kbd>F6</kbd></td>
<td>Switch to workspace 6</td>
</tr>
<tr>
<td><kbd>F7</kbd></td>
<td>Switch to workspace 7</td>
</tr>
<tr>
<td><kbd>F8</kbd></td>
<td>Switch to workspace 8</td>
</tr>
<tr>
<td><kbd>F9</kbd></td>
<td>Switch to workspace 9</td>
</tr>
<tr>
<td><kbd>Shift</kbd> + <kbd>F1</kbd></td>
<td>Move focused window to workspace 1</td>
</tr>
<tr>
<td><kbd>Shift</kbd> + <kbd>F2</kbd></td>
<td>Move focused window to workspace 2</td>
</tr>
<tr>
<td><kbd>Shift</kbd> + <kbd>F3</kbd></td>
<td>Move focused window to workspace 3</td>
</tr>
<tr>
<td><kbd>Shift</kbd> + <kbd>F4</kbd></td>
<td>Move focused window to workspace 4</td>
</tr>
<tr>
<td><kbd>Shift</kbd> + <kbd>F5</kbd></td>
<td>Move focused window to workspace 5</td>
</tr>
<tr>
<td><kbd>Shift</kbd> + <kbd>F6</kbd></td>
<td>Move focused window to workspace 6</td>
</tr>
<tr>
<td><kbd>Shift</kbd> + <kbd>F7</kbd></td>
<td>Move focused window to workspace 7</td>
</tr>
<tr>
<td><kbd>Shift</kbd> + <kbd>F8</kbd></td>
<td>Move focused window to workspace 8</td>
</tr>
<tr>
<td><kbd>Shift</kbd> + <kbd>F9</kbd></td>
<td>Move focused window to workspace 9</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>Right</kbd></td>
<td>Switch to next workspace</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>Left</kbd></td>
<td>Switch to previous workspace</td>
</tr>
</tbody>
</table>
</div>
<!-- Layout Control -->
<h2 id="layouts">Layout Control</h2>
<div class="table-wrapper">
<table class="shortcuts-table">
<thead>
<tr>
<th style="width: 40%;">Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Super</kbd> + <kbd>Space</kbd></td>
<td>Cycle layout mode (tiling → floating → monocle)</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>H</kbd></td>
<td>Shrink master area</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>L</kbd></td>
<td>Expand master area</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>I</kbd></td>
<td>Increase master window count</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>D</kbd></td>
<td>Decrease master window count</td>
</tr>
</tbody>
</table>
</div>
<!-- AI Features -->
<h2 id="ai">AI Features</h2>
<p style="color: var(--text-muted); margin-bottom: 1rem;">
These shortcuts require API keys to be configured. See <a href="ai-features.html">AI Features</a> for setup.
</p>
<div class="table-wrapper">
<table class="shortcuts-table">
<thead>
<tr>
<th style="width: 40%;">Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Super</kbd> + <kbd>A</kbd></td>
<td>Show AI context analysis</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>Shift</kbd> + <kbd>A</kbd></td>
<td>Open AI command palette</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>Shift</kbd> + <kbd>E</kbd></td>
<td>Open Exa semantic web search</td>
</tr>
</tbody>
</table>
</div>
<!-- Help & System -->
<h2 id="system">Help & System</h2>
<div class="table-wrapper">
<table class="shortcuts-table">
<thead>
<tr>
<th style="width: 40%;">Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Super</kbd> + <kbd>S</kbd></td>
<td>Show all keyboard shortcuts</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>T</kbd></td>
<td>Start/continue interactive tutorial</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>Backspace</kbd></td>
<td>Quit DWN</td>
</tr>
</tbody>
</table>
</div>
<!-- Printable Reference -->
<div class="card" style="margin-top: 3rem;">
<h3>Printable Quick Reference</h3>
<p>Essential shortcuts to memorize when starting with DWN:</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;">
<div>
<h4 style="color: var(--primary); margin-bottom: 0.75rem;">Must Know</h4>
<ul style="list-style: none; padding: 0;">
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>T</kbd> Terminal
</li>
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<kbd>Alt</kbd>+<kbd>F2</kbd> Launcher
</li>
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<kbd>Alt</kbd>+<kbd>F4</kbd> Close window
</li>
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<kbd>Alt</kbd>+<kbd>Tab</kbd> Switch windows
</li>
<li style="padding: 0.5rem 0;">
<kbd>F1</kbd>-<kbd>F9</kbd> Workspaces
</li>
</ul>
</div>
<div>
<h4 style="color: var(--primary); margin-bottom: 0.75rem;">Power User</h4>
<ul style="list-style: none; padding: 0;">
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<kbd>Super</kbd>+<kbd>Space</kbd> Change layout
</li>
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<kbd>Super</kbd>+<kbd>H</kbd>/<kbd>L</kbd> Resize master
</li>
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<kbd>Shift</kbd>+<kbd>F1-9</kbd> Move to workspace
</li>
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<kbd>Super</kbd>+<kbd>Shift</kbd>+<kbd>A</kbd> AI command
</li>
<li style="padding: 0.5rem 0;">
<kbd>Super</kbd>+<kbd>S</kbd> Show shortcuts
</li>
</ul>
</div>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
<div class="footer-section">
<h4>DWN Window Manager</h4>
<p style="color: var(--text-muted);">
A modern, production-ready X11 window manager with XFCE-like
functionality and optional AI integration.
</p>
</div>
<div class="footer-section">
<h4>Documentation</h4>
<ul>
<li><a href="documentation.html">Getting Started</a></li>
<li><a href="shortcuts.html">Keyboard Shortcuts</a></li>
<li><a href="configuration.html">Configuration</a></li>
<li><a href="architecture.html">Architecture</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Resources</h4>
<ul>
<li><a href="features.html">Features</a></li>
<li><a href="installation.html">Installation</a></li>
<li><a href="ai-features.html">AI Integration</a></li>
<li><a href="https://github.com/dwn/dwn">GitHub</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Community</h4>
<ul>
<li><a href="https://github.com/dwn/dwn/issues">Issue Tracker</a></li>
<li><a href="https://github.com/dwn/dwn/discussions">Discussions</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/CONTRIBUTING.md">Contributing</a></li>
<li><a href="https://github.com/dwn/dwn/blob/main/LICENSE">License (MIT)</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>DWN Window Manager - Open Source under the MIT License</p>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

872
src/ai.c Normal file
View File

@ -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 <curl/curl.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
/* 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(&current_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);
}

507
src/applauncher.c Normal file
View File

@ -0,0 +1,507 @@
/*
* DWN - Desktop Window Manager
* Application launcher with .desktop file support
*/
#include "applauncher.h"
#include "config.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <ctype.h>
#include <pwd.h>
/* 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);
}

463
src/atoms.c Normal file
View File

@ -0,0 +1,463 @@
/*
* DWN - Desktop Window Manager
* X11 Atoms management implementation
*/
#include "atoms.h"
#include "dwn.h"
#include "util.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
/* 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);
}

3191
src/cJSON.c Normal file

File diff suppressed because it is too large Load Diff

1110
src/client.c Normal file

File diff suppressed because it is too large Load Diff

361
src/config.c Normal file
View File

@ -0,0 +1,361 @@
/*
* DWN - Desktop Window Manager
* Configuration system implementation
*/
#include "config.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
/* 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);
}

368
src/decorations.c Normal file
View File

@ -0,0 +1,368 @@
/*
* DWN - Desktop Window Manager
* Window decorations implementation
*/
#include "decorations.h"
#include "client.h"
#include "config.h"
#include "util.h"
#include <string.h>
#include <stdbool.h>
#include <X11/Xft/Xft.h>
/* 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));
}

842
src/keys.c Normal file
View File

@ -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 <stdio.h>
#include <string.h>
/* 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");
}

263
src/layout.c Normal file
View File

@ -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 "???";
}

1016
src/main.c Normal file

File diff suppressed because it is too large Load Diff

697
src/news.c Normal file
View File

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include <curl/curl.h>
#include <X11/Xft/Xft.h>
/* 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 (&#123; or &#x1F;) */
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], "&amp;", 5) == 0) {
dst[j++] = '&'; i += 4;
} else if (strncmp(&src[i], "&lt;", 4) == 0) {
dst[j++] = '<'; i += 3;
} else if (strncmp(&src[i], "&gt;", 4) == 0) {
dst[j++] = '>'; i += 3;
} else if (strncmp(&src[i], "&quot;", 6) == 0) {
dst[j++] = '"'; i += 5;
} else if (strncmp(&src[i], "&apos;", 6) == 0) {
dst[j++] = '\''; i += 5;
} else if (strncmp(&src[i], "&nbsp;", 6) == 0) {
dst[j++] = ' '; i += 5;
} else if (strncmp(&src[i], "&ndash;", 7) == 0) {
dst[j++] = '-'; i += 6;
} else if (strncmp(&src[i], "&mdash;", 7) == 0) {
dst[j++] = '-'; dst[j++] = '-'; i += 6;
} else if (strncmp(&src[i], "&hellip;", 8) == 0) {
dst[j++] = '.'; dst[j++] = '.'; dst[j++] = '.'; i += 7;
} else if (strncmp(&src[i], "&lsquo;", 7) == 0 || strncmp(&src[i], "&rsquo;", 7) == 0) {
dst[j++] = '\''; i += 6;
} else if (strncmp(&src[i], "&ldquo;", 7) == 0 || strncmp(&src[i], "&rdquo;", 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);
}

856
src/notifications.c Normal file
View File

@ -0,0 +1,856 @@
/*
* DWN - Desktop Window Manager
* D-Bus Notification daemon implementation
*/
#include "notifications.h"
#include "config.h"
#include "util.h"
#include <string.h>
#include <stdlib.h>
#include <X11/Xft/Xft.h>
/* 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, &notif_width, &notif_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;
}

892
src/panel.c Normal file
View File

@ -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 <string.h>
#include <stdbool.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <X11/Xft/Xft.h>
/* 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)");
}

1105
src/systray.c Normal file

File diff suppressed because it is too large Load Diff

709
src/util.c Normal file
View File

@ -0,0 +1,709 @@
/*
* DWN - Desktop Window Manager
* Utility functions implementation
*/
#include "util.h"
#include "dwn.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <errno.h>
#include <pwd.h>
#include <pthread.h>
#include <stdatomic.h>
#include <fcntl.h>
/* ========== 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;
}

483
src/workspace.c Normal file
View File

@ -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 <string.h>
#include <stdio.h>
/* ========== 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);
}
}