chore: update c, css, d files
This commit is contained in:
commit
7a4f7a82ad
21
LICENSE
Normal file
21
LICENSE
Normal 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
263
Makefile
Normal 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)"
|
||||||
46
build/ai.d
Normal file
46
build/ai.d
Normal 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
BIN
build/ai.o
Normal file
Binary file not shown.
6
build/applauncher.d
Normal file
6
build/applauncher.d
Normal 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
BIN
build/applauncher.o
Normal file
Binary file not shown.
4
build/atoms.d
Normal file
4
build/atoms.d
Normal 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
BIN
build/atoms.o
Normal file
Binary file not shown.
2
build/cJSON.d
Normal file
2
build/cJSON.d
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
build/cJSON.o: src/cJSON.c include/cJSON.h
|
||||||
|
include/cJSON.h:
|
||||||
BIN
build/cJSON.o
Normal file
BIN
build/cJSON.o
Normal file
Binary file not shown.
47
build/client.d
Normal file
47
build/client.d
Normal 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
BIN
build/client.o
Normal file
Binary file not shown.
5
build/config.d
Normal file
5
build/config.d
Normal 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
BIN
build/config.o
Normal file
Binary file not shown.
7
build/decorations.d
Normal file
7
build/decorations.d
Normal 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
BIN
build/decorations.o
Normal file
Binary file not shown.
49
build/keys.d
Normal file
49
build/keys.d
Normal 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
BIN
build/keys.o
Normal file
Binary file not shown.
8
build/layout.d
Normal file
8
build/layout.d
Normal 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
BIN
build/layout.o
Normal file
Binary file not shown.
56
build/main.d
Normal file
56
build/main.d
Normal 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
BIN
build/main.o
Normal file
Binary file not shown.
8
build/news.d
Normal file
8
build/news.d
Normal 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
BIN
build/news.o
Normal file
Binary file not shown.
42
build/notifications.d
Normal file
42
build/notifications.d
Normal 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
BIN
build/notifications.o
Normal file
Binary file not shown.
13
build/panel.d
Normal file
13
build/panel.d
Normal 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
BIN
build/panel.o
Normal file
Binary file not shown.
44
build/systray.d
Normal file
44
build/systray.d
Normal 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
BIN
build/systray.o
Normal file
Binary file not shown.
3
build/util.d
Normal file
3
build/util.d
Normal 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
BIN
build/util.o
Normal file
Binary file not shown.
10
build/workspace.d
Normal file
10
build/workspace.d
Normal 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
BIN
build/workspace.o
Normal file
Binary file not shown.
93
config/config.example
Normal file
93
config/config.example
Normal 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
94
include/ai.h
Normal 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
48
include/applauncher.h
Normal 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
148
include/atoms.h
Normal 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
306
include/cJSON.h
Normal 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
80
include/client.h
Normal 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
87
include/config.h
Normal 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
48
include/decorations.h
Normal 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
168
include/dwn.h
Normal 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
98
include/keys.h
Normal 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
25
include/layout.h
Normal 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
66
include/news.h
Normal 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
73
include/notifications.h
Normal 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
65
include/panel.h
Normal 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
134
include/systray.h
Normal 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
68
include/util.h
Normal 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
61
include/workspace.h
Normal 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
8
scripts/dwn.desktop
Normal 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
42
scripts/xinitrc.example
Normal 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
458
site/ai-features.html
Normal 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">🤖</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">👁</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">🔍</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
565
site/architecture.html
Normal 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
520
site/configuration.html
Normal 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
1085
site/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
423
site/documentation.html
Normal file
423
site/documentation.html
Normal 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
411
site/features.html
Normal 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">☷</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">❏</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">☐</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">↔</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">📈</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">👁</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>🔋 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>🔊 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>📶 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>🤖 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>🔍 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>🎓 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>< 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
352
site/index.html
Normal 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">☰</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">⚙</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">🎨</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">💡</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">🔔</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">💻</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><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
610
site/installation.html
Normal 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
304
site/js/main.js
Normal 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
413
site/shortcuts.html
Normal 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
872
src/ai.c
Normal 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(¤t_context, 0, sizeof(current_context));
|
||||||
|
|
||||||
|
Workspace *ws = workspace_get_current();
|
||||||
|
if (ws != NULL && ws->focused != NULL) {
|
||||||
|
snprintf(current_context.focused_window, sizeof(current_context.focused_window),
|
||||||
|
"%s", ws->focused->title);
|
||||||
|
snprintf(current_context.focused_class, sizeof(current_context.focused_class),
|
||||||
|
"%s", ws->focused->class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build list of windows on current workspace */
|
||||||
|
int offset = 0;
|
||||||
|
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
|
||||||
|
if (c->workspace == (unsigned int)dwn->current_workspace) {
|
||||||
|
int written = snprintf(current_context.workspace_windows + offset,
|
||||||
|
sizeof(current_context.workspace_windows) - offset,
|
||||||
|
"%s%s", offset > 0 ? ", " : "", c->title);
|
||||||
|
if (written > 0) {
|
||||||
|
offset += written;
|
||||||
|
}
|
||||||
|
current_context.window_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *ai_analyze_task(void)
|
||||||
|
{
|
||||||
|
/* Analyze based on focused window class */
|
||||||
|
const char *class = current_context.focused_class;
|
||||||
|
|
||||||
|
if (strstr(class, "code") || strstr(class, "Code") ||
|
||||||
|
strstr(class, "vim") || strstr(class, "emacs")) {
|
||||||
|
return "coding";
|
||||||
|
}
|
||||||
|
if (strstr(class, "firefox") || strstr(class, "chrome") ||
|
||||||
|
strstr(class, "Firefox") || strstr(class, "Chrome")) {
|
||||||
|
return "browsing";
|
||||||
|
}
|
||||||
|
if (strstr(class, "slack") || strstr(class, "discord") ||
|
||||||
|
strstr(class, "Slack") || strstr(class, "Discord")) {
|
||||||
|
return "communication";
|
||||||
|
}
|
||||||
|
if (strstr(class, "terminal") || strstr(class, "Terminal")) {
|
||||||
|
return "terminal";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "general";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *ai_suggest_window(void)
|
||||||
|
{
|
||||||
|
/* Simple heuristic suggestion */
|
||||||
|
const char *task = ai_analyze_task();
|
||||||
|
|
||||||
|
if (strcmp(task, "coding") == 0) {
|
||||||
|
return "Consider opening a terminal for testing";
|
||||||
|
}
|
||||||
|
if (strcmp(task, "browsing") == 0) {
|
||||||
|
return "Documentation or reference material available?";
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *ai_suggest_app(void)
|
||||||
|
{
|
||||||
|
return NULL; /* Would require more context */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Command palette ========== */
|
||||||
|
|
||||||
|
/* Callback for AI command response */
|
||||||
|
static void ai_command_response_callback(AIRequest *req)
|
||||||
|
{
|
||||||
|
if (req == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req->state == AI_STATE_COMPLETED && req->response != NULL) {
|
||||||
|
/* Check if response contains a command to execute */
|
||||||
|
/* Format: [RUN: command] or [EXEC: command] */
|
||||||
|
char *run_cmd = strstr(req->response, "[RUN:");
|
||||||
|
if (run_cmd == NULL) {
|
||||||
|
run_cmd = strstr(req->response, "[EXEC:");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (run_cmd != NULL) {
|
||||||
|
/* Extract command */
|
||||||
|
char *cmd_start = strchr(run_cmd, ':');
|
||||||
|
if (cmd_start != NULL) {
|
||||||
|
cmd_start++;
|
||||||
|
while (*cmd_start == ' ') cmd_start++;
|
||||||
|
|
||||||
|
char *cmd_end = strchr(cmd_start, ']');
|
||||||
|
if (cmd_end != NULL) {
|
||||||
|
size_t cmd_len = cmd_end - cmd_start;
|
||||||
|
char *cmd = dwn_malloc(cmd_len + 1);
|
||||||
|
strncpy(cmd, cmd_start, cmd_len);
|
||||||
|
cmd[cmd_len] = '\0';
|
||||||
|
|
||||||
|
/* Trim trailing spaces */
|
||||||
|
while (cmd_len > 0 && cmd[cmd_len - 1] == ' ') {
|
||||||
|
cmd[--cmd_len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("AI executing command: %s", cmd);
|
||||||
|
notification_show("DWN AI", "Running", cmd, NULL, 2000);
|
||||||
|
spawn_async(cmd);
|
||||||
|
dwn_free(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* No command, just show response */
|
||||||
|
notification_show("DWN AI", "Response", req->response, NULL, 8000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notification_show("DWN AI", "Error", "Failed to get AI response", NULL, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cleanup - don't free req itself, it's managed by the queue */
|
||||||
|
/* The queue will be cleaned up separately */
|
||||||
|
}
|
||||||
|
|
||||||
|
void ai_show_command_palette(void)
|
||||||
|
{
|
||||||
|
if (!ai_is_available()) {
|
||||||
|
notification_show("DWN", "AI Unavailable",
|
||||||
|
"Set OPENROUTER_API_KEY to enable AI features",
|
||||||
|
NULL, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check if dmenu or rofi is available */
|
||||||
|
char *input = NULL;
|
||||||
|
|
||||||
|
/* Try dmenu first, then rofi */
|
||||||
|
if (spawn("command -v dmenu >/dev/null 2>&1") == 0) {
|
||||||
|
input = spawn_capture("echo '' | dmenu -p 'Ask AI:'");
|
||||||
|
} else if (spawn("command -v rofi >/dev/null 2>&1") == 0) {
|
||||||
|
input = spawn_capture("rofi -dmenu -p 'Ask AI:'");
|
||||||
|
} else {
|
||||||
|
notification_show("DWN AI", "Missing Dependency",
|
||||||
|
"Install dmenu or rofi for AI command palette:\n"
|
||||||
|
"sudo apt install dmenu",
|
||||||
|
NULL, 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input == NULL || input[0] == '\0') {
|
||||||
|
if (input != NULL) {
|
||||||
|
dwn_free(input);
|
||||||
|
}
|
||||||
|
LOG_DEBUG("AI command palette cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("AI command palette input: %s", input);
|
||||||
|
|
||||||
|
/* Show "thinking" notification */
|
||||||
|
notification_show("DWN AI", "Processing...", input, NULL, 2000);
|
||||||
|
|
||||||
|
/* Build context-aware prompt */
|
||||||
|
ai_update_context();
|
||||||
|
const char *task = ai_analyze_task();
|
||||||
|
|
||||||
|
char prompt[2048];
|
||||||
|
snprintf(prompt, sizeof(prompt),
|
||||||
|
"You are an AI assistant integrated into a Linux window manager called DWN. "
|
||||||
|
"You can execute shell commands for the user.\n\n"
|
||||||
|
"IMPORTANT: When the user asks you to run, open, launch, or start an application, "
|
||||||
|
"respond with the command in this exact format: [RUN: command]\n"
|
||||||
|
"Examples:\n"
|
||||||
|
"- User: 'open chrome' -> [RUN: google-chrome]\n"
|
||||||
|
"- User: 'run firefox' -> [RUN: firefox]\n"
|
||||||
|
"- User: 'open file manager' -> [RUN: thunar]\n"
|
||||||
|
"- User: 'launch terminal' -> [RUN: xfce4-terminal]\n"
|
||||||
|
"- User: 'open vs code' -> [RUN: code]\n\n"
|
||||||
|
"For questions or non-command requests, respond briefly (1-2 sentences) without the [RUN:] format.\n\n"
|
||||||
|
"User's current task: %s\n"
|
||||||
|
"User's request: %s",
|
||||||
|
task, input);
|
||||||
|
|
||||||
|
dwn_free(input);
|
||||||
|
|
||||||
|
/* Send request */
|
||||||
|
ai_send_request(prompt, ai_command_response_callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ai_execute_command(const char *command)
|
||||||
|
{
|
||||||
|
if (!ai_is_available() || command == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("AI executing command: %s", command);
|
||||||
|
|
||||||
|
/* Send to AI for interpretation */
|
||||||
|
char prompt[512];
|
||||||
|
snprintf(prompt, sizeof(prompt),
|
||||||
|
"User command: %s\nCurrent task: %s\nRespond with a single action to take.",
|
||||||
|
command, ai_analyze_task());
|
||||||
|
|
||||||
|
ai_send_request(prompt, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Smart features ========== */
|
||||||
|
|
||||||
|
void ai_auto_organize_workspace(void)
|
||||||
|
{
|
||||||
|
LOG_DEBUG("AI auto-organize (placeholder)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ai_suggest_layout(void)
|
||||||
|
{
|
||||||
|
LOG_DEBUG("AI layout suggestion (placeholder)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ai_analyze_workflow(void)
|
||||||
|
{
|
||||||
|
LOG_DEBUG("AI workflow analysis (placeholder)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Notification intelligence ========== */
|
||||||
|
|
||||||
|
bool ai_should_show_notification(const char *app, const char *summary)
|
||||||
|
{
|
||||||
|
/* Simple filtering - could be enhanced with AI */
|
||||||
|
(void)app;
|
||||||
|
(void)summary;
|
||||||
|
return true; /* Show all by default */
|
||||||
|
}
|
||||||
|
|
||||||
|
int ai_notification_priority(const char *app, const char *summary)
|
||||||
|
{
|
||||||
|
/* Simple priority assignment */
|
||||||
|
if (strstr(summary, "urgent") || strstr(summary, "Urgent") ||
|
||||||
|
strstr(summary, "error") || strstr(summary, "Error")) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
if (strstr(app, "slack") || strstr(app, "Slack") ||
|
||||||
|
strstr(app, "discord") || strstr(app, "Discord")) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Performance monitoring ========== */
|
||||||
|
|
||||||
|
void ai_monitor_performance(void)
|
||||||
|
{
|
||||||
|
/* Read from /proc for basic metrics */
|
||||||
|
LOG_DEBUG("AI performance monitoring (placeholder)");
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *ai_performance_suggestion(void)
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Exa Semantic Search ========== */
|
||||||
|
|
||||||
|
#define EXA_API_URL "https://api.exa.ai/search"
|
||||||
|
|
||||||
|
static ExaRequest *exa_queue = NULL;
|
||||||
|
|
||||||
|
bool exa_is_available(void)
|
||||||
|
{
|
||||||
|
return dwn != NULL && dwn->config != NULL &&
|
||||||
|
dwn->config->exa_api_key[0] != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse Exa JSON response using cJSON */
|
||||||
|
static void exa_parse_response(ExaRequest *req, const char *json)
|
||||||
|
{
|
||||||
|
if (req == NULL || json == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
req->result_count = 0;
|
||||||
|
|
||||||
|
cJSON *root = cJSON_Parse(json);
|
||||||
|
if (root == NULL) {
|
||||||
|
LOG_WARN("Failed to parse Exa response JSON");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *results = cJSON_GetObjectItemCaseSensitive(root, "results");
|
||||||
|
if (!cJSON_IsArray(results)) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int array_size = cJSON_GetArraySize(results);
|
||||||
|
for (int i = 0; i < array_size && req->result_count < 10; i++) {
|
||||||
|
cJSON *item = cJSON_GetArrayItem(results, i);
|
||||||
|
if (!cJSON_IsObject(item)) continue;
|
||||||
|
|
||||||
|
ExaSearchResult *res = &req->results[req->result_count];
|
||||||
|
|
||||||
|
/* Extract title */
|
||||||
|
cJSON *title = cJSON_GetObjectItemCaseSensitive(item, "title");
|
||||||
|
if (cJSON_IsString(title) && title->valuestring != NULL) {
|
||||||
|
strncpy(res->title, title->valuestring, sizeof(res->title) - 1);
|
||||||
|
res->title[sizeof(res->title) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extract URL */
|
||||||
|
cJSON *url = cJSON_GetObjectItemCaseSensitive(item, "url");
|
||||||
|
if (cJSON_IsString(url) && url->valuestring != NULL) {
|
||||||
|
strncpy(res->url, url->valuestring, sizeof(res->url) - 1);
|
||||||
|
res->url[sizeof(res->url) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extract text/snippet if available */
|
||||||
|
cJSON *text = cJSON_GetObjectItemCaseSensitive(item, "text");
|
||||||
|
if (cJSON_IsString(text) && text->valuestring != NULL) {
|
||||||
|
strncpy(res->snippet, text->valuestring, sizeof(res->snippet) - 1);
|
||||||
|
res->snippet[sizeof(res->snippet) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
req->result_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(root);
|
||||||
|
LOG_DEBUG("Parsed %d Exa results", req->result_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
|
||||||
|
{
|
||||||
|
if (!exa_is_available() || query == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExaRequest *req = dwn_calloc(1, sizeof(ExaRequest));
|
||||||
|
req->query = dwn_strdup(query);
|
||||||
|
req->state = AI_STATE_PENDING;
|
||||||
|
req->callback = callback;
|
||||||
|
req->result_count = 0;
|
||||||
|
|
||||||
|
/* Build JSON request */
|
||||||
|
char *json_query = dwn_malloc(strlen(query) * 2 + 256);
|
||||||
|
char *escaped = dwn_malloc(strlen(query) * 2 + 1);
|
||||||
|
|
||||||
|
/* Escape query string */
|
||||||
|
const char *src = query;
|
||||||
|
char *dst = escaped;
|
||||||
|
while (*src) {
|
||||||
|
if (*src == '"' || *src == '\\') {
|
||||||
|
*dst++ = '\\';
|
||||||
|
}
|
||||||
|
*dst++ = *src++;
|
||||||
|
}
|
||||||
|
*dst = '\0';
|
||||||
|
|
||||||
|
snprintf(json_query, strlen(query) * 2 + 256,
|
||||||
|
"{\"query\":\"%s\",\"type\":\"auto\",\"numResults\":10,\"contents\":{\"text\":true}}",
|
||||||
|
escaped);
|
||||||
|
dwn_free(escaped);
|
||||||
|
|
||||||
|
/* Create curl handle */
|
||||||
|
CURL *easy = curl_easy_init();
|
||||||
|
if (easy == NULL) {
|
||||||
|
dwn_free(json_query);
|
||||||
|
dwn_free(req->query);
|
||||||
|
dwn_free(req);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Response buffer */
|
||||||
|
ResponseBuffer *response = dwn_calloc(1, sizeof(ResponseBuffer));
|
||||||
|
|
||||||
|
/* Set headers */
|
||||||
|
struct curl_slist *headers = NULL;
|
||||||
|
char api_header[300];
|
||||||
|
snprintf(api_header, sizeof(api_header), "x-api-key: %s",
|
||||||
|
dwn->config->exa_api_key);
|
||||||
|
|
||||||
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||||
|
headers = curl_slist_append(headers, api_header);
|
||||||
|
|
||||||
|
curl_easy_setopt(easy, CURLOPT_URL, EXA_API_URL);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_POSTFIELDS, json_query);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_WRITEDATA, response);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 15L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_PRIVATE, req);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||||
|
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||||
|
|
||||||
|
/* Add to multi handle */
|
||||||
|
if (curl_multi == NULL) {
|
||||||
|
curl_multi = curl_multi_init();
|
||||||
|
}
|
||||||
|
curl_multi_add_handle(curl_multi, easy);
|
||||||
|
|
||||||
|
/* Add to queue */
|
||||||
|
req->next = exa_queue;
|
||||||
|
exa_queue = req;
|
||||||
|
req->user_data = response;
|
||||||
|
|
||||||
|
LOG_DEBUG("Exa search sent: %s", query);
|
||||||
|
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
void exa_process_pending(void)
|
||||||
|
{
|
||||||
|
if (curl_multi == NULL || exa_queue == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int running_handles;
|
||||||
|
curl_multi_perform(curl_multi, &running_handles);
|
||||||
|
|
||||||
|
CURLMsg *msg;
|
||||||
|
int msgs_left;
|
||||||
|
|
||||||
|
while ((msg = curl_multi_info_read(curl_multi, &msgs_left)) != NULL) {
|
||||||
|
if (msg->msg == CURLMSG_DONE) {
|
||||||
|
CURL *easy = msg->easy_handle;
|
||||||
|
ExaRequest *req = NULL;
|
||||||
|
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req);
|
||||||
|
|
||||||
|
/* Check if this is an Exa request (in exa_queue) */
|
||||||
|
bool is_exa = false;
|
||||||
|
for (ExaRequest *r = exa_queue; r != NULL; r = r->next) {
|
||||||
|
if (r == req) {
|
||||||
|
is_exa = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_exa && req != NULL) {
|
||||||
|
ResponseBuffer *buf = (ResponseBuffer *)req->user_data;
|
||||||
|
|
||||||
|
if (msg->data.result == CURLE_OK && buf != NULL && buf->data != NULL) {
|
||||||
|
exa_parse_response(req, buf->data);
|
||||||
|
req->state = AI_STATE_COMPLETED;
|
||||||
|
} else {
|
||||||
|
req->state = AI_STATE_ERROR;
|
||||||
|
LOG_ERROR("Exa request failed: %s", curl_easy_strerror(msg->data.result));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req->callback != NULL) {
|
||||||
|
req->callback(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cleanup buffer */
|
||||||
|
if (buf != NULL) {
|
||||||
|
if (buf->data) free(buf->data);
|
||||||
|
dwn_free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove from queue */
|
||||||
|
ExaRequest **pp = &exa_queue;
|
||||||
|
while (*pp != NULL) {
|
||||||
|
if (*pp == req) {
|
||||||
|
*pp = req->next;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pp = &(*pp)->next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_multi_remove_handle(curl_multi, easy);
|
||||||
|
curl_easy_cleanup(easy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Callback for app launcher search */
|
||||||
|
static void exa_launcher_callback(ExaRequest *req)
|
||||||
|
{
|
||||||
|
if (req == NULL || req->state != AI_STATE_COMPLETED) {
|
||||||
|
notification_show("Exa Search", "Error", "Search failed", NULL, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req->result_count == 0) {
|
||||||
|
notification_show("Exa Search", "No Results", req->query, NULL, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show results via dmenu/rofi - use bounded string operations */
|
||||||
|
size_t choices_size = req->result_count * 300;
|
||||||
|
char *choices = dwn_malloc(choices_size);
|
||||||
|
size_t offset = 0;
|
||||||
|
choices[0] = '\0';
|
||||||
|
|
||||||
|
for (int i = 0; i < req->result_count; i++) {
|
||||||
|
int written = snprintf(choices + offset, choices_size - offset,
|
||||||
|
"%s%s", offset > 0 ? "\n" : "", req->results[i].title);
|
||||||
|
if (written > 0 && (size_t)written < choices_size - offset) {
|
||||||
|
offset += written;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show in dmenu - escape choices to prevent command injection */
|
||||||
|
char *escaped_choices = shell_escape(choices);
|
||||||
|
char *cmd = dwn_malloc(strlen(escaped_choices) + 64);
|
||||||
|
snprintf(cmd, strlen(escaped_choices) + 64, "echo %s | dmenu -l 10 -p 'Results:'", escaped_choices);
|
||||||
|
|
||||||
|
char *selected = spawn_capture(cmd);
|
||||||
|
dwn_free(cmd);
|
||||||
|
dwn_free(escaped_choices);
|
||||||
|
|
||||||
|
if (selected != NULL && selected[0] != '\0') {
|
||||||
|
/* Find which result was selected and open URL */
|
||||||
|
for (int i = 0; i < req->result_count; i++) {
|
||||||
|
if (strncmp(selected, req->results[i].title, strlen(req->results[i].title)) == 0) {
|
||||||
|
/* Escape URL to prevent command injection */
|
||||||
|
char *escaped_url = shell_escape(req->results[i].url);
|
||||||
|
char *open_cmd = dwn_malloc(strlen(escaped_url) + 32);
|
||||||
|
snprintf(open_cmd, strlen(escaped_url) + 32, "xdg-open %s &", escaped_url);
|
||||||
|
spawn_async(open_cmd);
|
||||||
|
dwn_free(open_cmd);
|
||||||
|
dwn_free(escaped_url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dwn_free(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
dwn_free(choices);
|
||||||
|
if (req->query) dwn_free(req->query);
|
||||||
|
dwn_free(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
void exa_show_app_launcher(void)
|
||||||
|
{
|
||||||
|
if (!exa_is_available()) {
|
||||||
|
notification_show("Exa", "Unavailable",
|
||||||
|
"Set EXA_API_KEY in config to enable semantic search",
|
||||||
|
NULL, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get search query from user */
|
||||||
|
char *query = NULL;
|
||||||
|
|
||||||
|
if (spawn("command -v dmenu >/dev/null 2>&1") == 0) {
|
||||||
|
query = spawn_capture("echo '' | dmenu -p 'Exa Search:'");
|
||||||
|
} else if (spawn("command -v rofi >/dev/null 2>&1") == 0) {
|
||||||
|
query = spawn_capture("rofi -dmenu -p 'Exa Search:'");
|
||||||
|
} else {
|
||||||
|
notification_show("Exa", "Missing Dependency",
|
||||||
|
"Install dmenu or rofi", NULL, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query == NULL || query[0] == '\0') {
|
||||||
|
if (query != NULL) dwn_free(query);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove trailing newline */
|
||||||
|
query[strcspn(query, "\n")] = '\0';
|
||||||
|
|
||||||
|
notification_show("Exa", "Searching...", query, NULL, 2000);
|
||||||
|
exa_search(query, exa_launcher_callback);
|
||||||
|
}
|
||||||
507
src/applauncher.c
Normal file
507
src/applauncher.c
Normal 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
463
src/atoms.c
Normal 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
3191
src/cJSON.c
Normal file
File diff suppressed because it is too large
Load Diff
1110
src/client.c
Normal file
1110
src/client.c
Normal file
File diff suppressed because it is too large
Load Diff
361
src/config.c
Normal file
361
src/config.c
Normal 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
368
src/decorations.c
Normal 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
842
src/keys.c
Normal 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
263
src/layout.c
Normal 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
1016
src/main.c
Normal file
File diff suppressed because it is too large
Load Diff
697
src/news.c
Normal file
697
src/news.c
Normal 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 ({ or ) */
|
||||||
|
static int decode_numeric_entity(const char *src, char *out, size_t max_out)
|
||||||
|
{
|
||||||
|
if (src[0] != '&' || src[1] != '#') return 0;
|
||||||
|
|
||||||
|
const char *p = src + 2;
|
||||||
|
int base = 10;
|
||||||
|
if (*p == 'x' || *p == 'X') {
|
||||||
|
base = 16;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *end;
|
||||||
|
long code = strtol(p, &end, base);
|
||||||
|
if (*end != ';' || code <= 0) return 0;
|
||||||
|
|
||||||
|
int consumed = (end - src) + 1;
|
||||||
|
|
||||||
|
if (code < 128 && max_out >= 1) {
|
||||||
|
out[0] = (char)code;
|
||||||
|
return consumed;
|
||||||
|
} else if (code < 0x800 && max_out >= 2) {
|
||||||
|
out[0] = 0xC0 | (code >> 6);
|
||||||
|
out[1] = 0x80 | (code & 0x3F);
|
||||||
|
return consumed;
|
||||||
|
} else if (code < 0x10000 && max_out >= 3) {
|
||||||
|
out[0] = 0xE0 | (code >> 12);
|
||||||
|
out[1] = 0x80 | ((code >> 6) & 0x3F);
|
||||||
|
out[2] = 0x80 | (code & 0x3F);
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strip HTML tags and decode entities */
|
||||||
|
static void strip_html(char *dst, const char *src, size_t max_len)
|
||||||
|
{
|
||||||
|
size_t j = 0;
|
||||||
|
bool in_tag = false;
|
||||||
|
|
||||||
|
for (size_t i = 0; src[i] && j < max_len - 1; i++) {
|
||||||
|
if (src[i] == '<') {
|
||||||
|
in_tag = true;
|
||||||
|
} else if (src[i] == '>') {
|
||||||
|
in_tag = false;
|
||||||
|
} else if (!in_tag) {
|
||||||
|
if (src[i] == '&') {
|
||||||
|
/* Named entities */
|
||||||
|
if (strncmp(&src[i], "&", 5) == 0) {
|
||||||
|
dst[j++] = '&'; i += 4;
|
||||||
|
} else if (strncmp(&src[i], "<", 4) == 0) {
|
||||||
|
dst[j++] = '<'; i += 3;
|
||||||
|
} else if (strncmp(&src[i], ">", 4) == 0) {
|
||||||
|
dst[j++] = '>'; i += 3;
|
||||||
|
} else if (strncmp(&src[i], """, 6) == 0) {
|
||||||
|
dst[j++] = '"'; i += 5;
|
||||||
|
} else if (strncmp(&src[i], "'", 6) == 0) {
|
||||||
|
dst[j++] = '\''; i += 5;
|
||||||
|
} else if (strncmp(&src[i], " ", 6) == 0) {
|
||||||
|
dst[j++] = ' '; i += 5;
|
||||||
|
} else if (strncmp(&src[i], "–", 7) == 0) {
|
||||||
|
dst[j++] = '-'; i += 6;
|
||||||
|
} else if (strncmp(&src[i], "—", 7) == 0) {
|
||||||
|
dst[j++] = '-'; dst[j++] = '-'; i += 6;
|
||||||
|
} else if (strncmp(&src[i], "…", 8) == 0) {
|
||||||
|
dst[j++] = '.'; dst[j++] = '.'; dst[j++] = '.'; i += 7;
|
||||||
|
} else if (strncmp(&src[i], "‘", 7) == 0 || strncmp(&src[i], "’", 7) == 0) {
|
||||||
|
dst[j++] = '\''; i += 6;
|
||||||
|
} else if (strncmp(&src[i], "“", 7) == 0 || strncmp(&src[i], "”", 7) == 0) {
|
||||||
|
dst[j++] = '"'; i += 6;
|
||||||
|
} else if (src[i+1] == '#') {
|
||||||
|
/* Numeric entity */
|
||||||
|
char decoded[4] = {0};
|
||||||
|
int consumed = decode_numeric_entity(&src[i], decoded, sizeof(decoded));
|
||||||
|
if (consumed > 0) {
|
||||||
|
for (int k = 0; decoded[k] && j < max_len - 1; k++) {
|
||||||
|
dst[j++] = decoded[k];
|
||||||
|
}
|
||||||
|
i += consumed - 1;
|
||||||
|
} else {
|
||||||
|
dst[j++] = src[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dst[j++] = src[i];
|
||||||
|
}
|
||||||
|
} else if (src[i] == '\n' || src[i] == '\r' || src[i] == '\t') {
|
||||||
|
dst[j++] = ' ';
|
||||||
|
} else {
|
||||||
|
dst[j++] = src[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dst[j] = '\0';
|
||||||
|
|
||||||
|
/* Collapse multiple spaces */
|
||||||
|
char *read = dst, *write = dst;
|
||||||
|
bool last_space = false;
|
||||||
|
while (*read) {
|
||||||
|
if (*read == ' ') {
|
||||||
|
if (!last_space) {
|
||||||
|
*write++ = *read;
|
||||||
|
}
|
||||||
|
last_space = true;
|
||||||
|
} else {
|
||||||
|
*write++ = *read;
|
||||||
|
last_space = false;
|
||||||
|
}
|
||||||
|
read++;
|
||||||
|
}
|
||||||
|
*write = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse JSON response and populate articles */
|
||||||
|
static int parse_news_json(const char *json_str)
|
||||||
|
{
|
||||||
|
cJSON *root = cJSON_Parse(json_str);
|
||||||
|
if (root == NULL) {
|
||||||
|
LOG_WARN("Failed to parse news JSON");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *articles = cJSON_GetObjectItemCaseSensitive(root, "articles");
|
||||||
|
if (!cJSON_IsArray(articles)) {
|
||||||
|
LOG_WARN("No articles array in news JSON");
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
cJSON *article;
|
||||||
|
cJSON_ArrayForEach(article, articles) {
|
||||||
|
if (count >= MAX_NEWS_ARTICLES) break;
|
||||||
|
|
||||||
|
cJSON *title = cJSON_GetObjectItemCaseSensitive(article, "title");
|
||||||
|
cJSON *content = cJSON_GetObjectItemCaseSensitive(article, "content");
|
||||||
|
cJSON *description = cJSON_GetObjectItemCaseSensitive(article, "description");
|
||||||
|
cJSON *link = cJSON_GetObjectItemCaseSensitive(article, "link");
|
||||||
|
cJSON *author = cJSON_GetObjectItemCaseSensitive(article, "author");
|
||||||
|
|
||||||
|
NewsArticle *art = &news_state.articles[count];
|
||||||
|
memset(art, 0, sizeof(NewsArticle));
|
||||||
|
|
||||||
|
if (cJSON_IsString(title) && title->valuestring) {
|
||||||
|
strip_html(art->title, title->valuestring, sizeof(art->title));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prefer content, fallback to description */
|
||||||
|
const char *text = NULL;
|
||||||
|
if (cJSON_IsString(content) && content->valuestring && strlen(content->valuestring) > 0) {
|
||||||
|
text = content->valuestring;
|
||||||
|
} else if (cJSON_IsString(description) && description->valuestring) {
|
||||||
|
text = description->valuestring;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
strip_html(art->content, text, sizeof(art->content));
|
||||||
|
} else {
|
||||||
|
/* Use title as fallback */
|
||||||
|
strncpy(art->content, art->title, sizeof(art->content) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cJSON_IsString(link) && link->valuestring) {
|
||||||
|
strncpy(art->link, link->valuestring, sizeof(art->link) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cJSON_IsString(author) && author->valuestring) {
|
||||||
|
strncpy(art->author, author->valuestring, sizeof(art->author) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Separator between articles */
|
||||||
|
#define NEWS_SEPARATOR " • "
|
||||||
|
#define NEWS_SEPARATOR_LEN 5
|
||||||
|
|
||||||
|
/* Forward declarations */
|
||||||
|
static int news_find_article_at_x(int click_x);
|
||||||
|
|
||||||
|
/* Get UTF-8 text width using Xft */
|
||||||
|
static int news_text_width(const char *text)
|
||||||
|
{
|
||||||
|
if (dwn == NULL || text == NULL) return 0;
|
||||||
|
|
||||||
|
if (dwn->xft_font != NULL) {
|
||||||
|
XGlyphInfo extents;
|
||||||
|
XftTextExtentsUtf8(dwn->display, dwn->xft_font,
|
||||||
|
(const FcChar8 *)text, strlen(text), &extents);
|
||||||
|
return extents.xOff;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dwn->font != NULL) {
|
||||||
|
return XTextWidth(dwn->font, text, strlen(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
return strlen(text) * 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Draw UTF-8 text using Xft with clipping */
|
||||||
|
static void news_draw_text_clipped(Drawable d, int x, int y, const char *text,
|
||||||
|
unsigned long color, XRectangle *clip)
|
||||||
|
{
|
||||||
|
if (dwn == NULL || dwn->display == NULL || text == NULL) return;
|
||||||
|
|
||||||
|
if (dwn->xft_font != NULL) {
|
||||||
|
XftDraw *xft_draw = XftDrawCreate(dwn->display, d,
|
||||||
|
DefaultVisual(dwn->display, dwn->screen),
|
||||||
|
dwn->colormap);
|
||||||
|
if (xft_draw != NULL) {
|
||||||
|
if (clip != NULL) {
|
||||||
|
Region region = XCreateRegion();
|
||||||
|
XUnionRectWithRegion(clip, region, region);
|
||||||
|
XftDrawSetClip(xft_draw, region);
|
||||||
|
XDestroyRegion(region);
|
||||||
|
}
|
||||||
|
|
||||||
|
XftColor xft_color;
|
||||||
|
XRenderColor render_color;
|
||||||
|
|
||||||
|
render_color.red = ((color >> 16) & 0xFF) * 257;
|
||||||
|
render_color.green = ((color >> 8) & 0xFF) * 257;
|
||||||
|
render_color.blue = (color & 0xFF) * 257;
|
||||||
|
render_color.alpha = 0xFFFF;
|
||||||
|
|
||||||
|
XftColorAllocValue(dwn->display,
|
||||||
|
DefaultVisual(dwn->display, dwn->screen),
|
||||||
|
dwn->colormap, &render_color, &xft_color);
|
||||||
|
|
||||||
|
XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font,
|
||||||
|
x, y, (const FcChar8 *)text, strlen(text));
|
||||||
|
|
||||||
|
XftColorFree(dwn->display,
|
||||||
|
DefaultVisual(dwn->display, dwn->screen),
|
||||||
|
dwn->colormap, &xft_color);
|
||||||
|
XftDrawDestroy(xft_draw);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clip != NULL) {
|
||||||
|
XSetClipRectangles(dwn->display, dwn->gc, 0, 0, clip, 1, Unsorted);
|
||||||
|
}
|
||||||
|
XSetForeground(dwn->display, dwn->gc, color);
|
||||||
|
XDrawString(dwn->display, d, dwn->gc, x, y, text, strlen(text));
|
||||||
|
if (clip != NULL) {
|
||||||
|
XSetClipMask(dwn->display, dwn->gc, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get display text for an article */
|
||||||
|
static void get_article_display_text(int index, char *buf, size_t buf_size)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= news_state.article_count) {
|
||||||
|
buf[0] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NewsArticle *art = &news_state.articles[index];
|
||||||
|
if (art->content[0] && strcmp(art->content, art->title) != 0) {
|
||||||
|
snprintf(buf, buf_size, "%s: %s", art->title, art->content);
|
||||||
|
} else {
|
||||||
|
snprintf(buf, buf_size, "%s", art->title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Recalculate cached widths after fetching */
|
||||||
|
static void news_recalc_widths(void)
|
||||||
|
{
|
||||||
|
if (dwn == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int separator_width = news_text_width(NEWS_SEPARATOR);
|
||||||
|
news_state.total_width = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < news_state.article_count; i++) {
|
||||||
|
char display_text[2048];
|
||||||
|
get_article_display_text(i, display_text, sizeof(display_text));
|
||||||
|
news_state.display_widths[i] = news_text_width(display_text);
|
||||||
|
news_state.total_width += news_state.display_widths[i] + separator_width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background fetch thread */
|
||||||
|
static void *news_fetch_thread(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
|
||||||
|
CURL *curl = curl_easy_init();
|
||||||
|
if (curl == NULL) {
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
news_state.fetching = false;
|
||||||
|
news_state.has_error = true;
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurlBuffer buf = {0};
|
||||||
|
buf.data = malloc(1);
|
||||||
|
buf.data[0] = '\0';
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, NEWS_API_URL);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, news_curl_write_cb);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "DWN/1.0");
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
if (res == CURLE_OK && buf.data && buf.size > 0) {
|
||||||
|
int count = parse_news_json(buf.data);
|
||||||
|
if (count > 0) {
|
||||||
|
news_state.article_count = count;
|
||||||
|
news_state.has_error = false;
|
||||||
|
news_state.current_article = 0;
|
||||||
|
news_state.scroll_offset = 0.0;
|
||||||
|
news_state.last_scroll_update = 0;
|
||||||
|
news_state.widths_dirty = true;
|
||||||
|
LOG_INFO("Fetched %d news articles", count);
|
||||||
|
} else {
|
||||||
|
news_state.has_error = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
news_state.has_error = true;
|
||||||
|
LOG_DEBUG("News fetch failed: %s", curl_easy_strerror(res));
|
||||||
|
}
|
||||||
|
news_state.fetching = false;
|
||||||
|
news_state.last_fetch = get_time_ms();
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
|
||||||
|
free(buf.data);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Public API ========== */
|
||||||
|
|
||||||
|
void news_init(void)
|
||||||
|
{
|
||||||
|
memset(&news_state, 0, sizeof(news_state));
|
||||||
|
news_state.last_fetch = 0;
|
||||||
|
|
||||||
|
/* Start initial fetch */
|
||||||
|
news_fetch_async();
|
||||||
|
|
||||||
|
LOG_INFO("News ticker initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
void news_cleanup(void)
|
||||||
|
{
|
||||||
|
/* Wait for any pending fetch */
|
||||||
|
if (fetch_running) {
|
||||||
|
pthread_join(fetch_thread, NULL);
|
||||||
|
fetch_running = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void news_fetch_async(void)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
if (news_state.fetching) {
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
news_state.fetching = true;
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
|
||||||
|
/* Wait for previous thread if still running */
|
||||||
|
if (fetch_running) {
|
||||||
|
pthread_join(fetch_thread, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_running = 1;
|
||||||
|
if (pthread_create(&fetch_thread, NULL, news_fetch_thread, NULL) != 0) {
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
news_state.fetching = false;
|
||||||
|
news_state.has_error = true;
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
fetch_running = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void news_update(void)
|
||||||
|
{
|
||||||
|
long now = get_time_ms();
|
||||||
|
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
|
||||||
|
/* Auto-refresh if interval passed */
|
||||||
|
if (!news_state.fetching && (now - news_state.last_fetch) >= FETCH_INTERVAL) {
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
news_fetch_async();
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Time-based smooth scrolling */
|
||||||
|
if (!news_state.interactive_mode && news_state.article_count > 0) {
|
||||||
|
if (news_state.last_scroll_update > 0) {
|
||||||
|
long delta_ms = now - news_state.last_scroll_update;
|
||||||
|
/* Calculate smooth sub-pixel scroll amount based on elapsed time */
|
||||||
|
double scroll_amount = (SCROLL_SPEED_PPS * delta_ms) / 1000.0;
|
||||||
|
news_state.scroll_offset += scroll_amount;
|
||||||
|
}
|
||||||
|
news_state.last_scroll_update = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void news_next_article(void)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
if (news_state.article_count > 0) {
|
||||||
|
news_state.current_article = (news_state.current_article + 1) % news_state.article_count;
|
||||||
|
news_state.scroll_offset = 0.0;
|
||||||
|
news_state.last_scroll_update = 0;
|
||||||
|
news_state.interactive_mode = true;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void news_prev_article(void)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
if (news_state.article_count > 0) {
|
||||||
|
news_state.current_article--;
|
||||||
|
if (news_state.current_article < 0) {
|
||||||
|
news_state.current_article = news_state.article_count - 1;
|
||||||
|
}
|
||||||
|
news_state.scroll_offset = 0.0;
|
||||||
|
news_state.last_scroll_update = 0;
|
||||||
|
news_state.interactive_mode = true;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void news_open_current(void)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
if (news_state.article_count > 0 && news_state.total_width > 0) {
|
||||||
|
int article_index = news_find_article_at_x(news_state.render_x);
|
||||||
|
if (article_index < 0) article_index = 0;
|
||||||
|
|
||||||
|
char url[512];
|
||||||
|
strncpy(url, news_state.articles[article_index].link, sizeof(url) - 1);
|
||||||
|
url[sizeof(url) - 1] = '\0';
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
setsid();
|
||||||
|
pid_t pid2 = fork();
|
||||||
|
if (pid2 == 0) {
|
||||||
|
execlp("xdg-open", "xdg-open", url, NULL);
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
_exit(0);
|
||||||
|
} else if (pid > 0) {
|
||||||
|
int status;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find article at given x position within news area */
|
||||||
|
static int news_find_article_at_x(int click_x)
|
||||||
|
{
|
||||||
|
if (news_state.article_count == 0 || news_state.total_width == 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int separator_width = news_text_width(NEWS_SEPARATOR);
|
||||||
|
int scroll_offset = (int)news_state.scroll_offset % news_state.total_width;
|
||||||
|
int rel_x = click_x - news_state.render_x;
|
||||||
|
|
||||||
|
int content_x = scroll_offset + rel_x;
|
||||||
|
if (content_x < 0) {
|
||||||
|
content_x += news_state.total_width;
|
||||||
|
}
|
||||||
|
content_x = content_x % news_state.total_width;
|
||||||
|
|
||||||
|
int pos = 0;
|
||||||
|
for (int i = 0; i < news_state.article_count; i++) {
|
||||||
|
int article_total = news_state.display_widths[i] + separator_width;
|
||||||
|
if (content_x < pos + article_total) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
pos += article_total;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void news_render(Panel *panel, int x, int max_width, int *used_width)
|
||||||
|
{
|
||||||
|
if (panel == NULL || used_width == NULL || dwn == NULL) {
|
||||||
|
*used_width = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dwn->xft_font == NULL && dwn->font == NULL) {
|
||||||
|
*used_width = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
|
||||||
|
if (news_state.article_count == 0) {
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
*used_width = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (news_state.widths_dirty) {
|
||||||
|
news_recalc_widths();
|
||||||
|
news_state.widths_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (news_state.total_width == 0) {
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
*used_width = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
news_state.render_x = x;
|
||||||
|
news_state.render_width = max_width;
|
||||||
|
|
||||||
|
int separator_width = news_text_width(NEWS_SEPARATOR);
|
||||||
|
int scroll_offset = (int)news_state.scroll_offset % news_state.total_width;
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
|
||||||
|
const ColorScheme *colors = config_get_colors();
|
||||||
|
|
||||||
|
int text_y;
|
||||||
|
if (dwn->xft_font != NULL) {
|
||||||
|
text_y = (panel->height + dwn->xft_font->ascent - dwn->xft_font->descent) / 2;
|
||||||
|
} else {
|
||||||
|
text_y = (panel->height + dwn->font->ascent - dwn->font->descent) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
XRectangle clip_rect = { x, 0, max_width, panel->height };
|
||||||
|
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
|
||||||
|
int draw_x = x - scroll_offset;
|
||||||
|
int articles_drawn = 0;
|
||||||
|
int start_article = 0;
|
||||||
|
|
||||||
|
int pos = 0;
|
||||||
|
for (int i = 0; i < news_state.article_count; i++) {
|
||||||
|
int article_total = news_state.display_widths[i] + separator_width;
|
||||||
|
if (pos + article_total > scroll_offset) {
|
||||||
|
start_article = i;
|
||||||
|
draw_x = x + (pos - scroll_offset);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pos += article_total;
|
||||||
|
}
|
||||||
|
|
||||||
|
int current_article = start_article;
|
||||||
|
while (draw_x < x + max_width && articles_drawn < news_state.article_count * 2) {
|
||||||
|
char display_text[2048];
|
||||||
|
get_article_display_text(current_article, display_text, sizeof(display_text));
|
||||||
|
|
||||||
|
if (draw_x + news_state.display_widths[current_article] > x) {
|
||||||
|
news_draw_text_clipped(panel->buffer, draw_x, text_y, display_text,
|
||||||
|
colors->panel_fg, &clip_rect);
|
||||||
|
}
|
||||||
|
draw_x += news_state.display_widths[current_article];
|
||||||
|
|
||||||
|
if (draw_x < x + max_width) {
|
||||||
|
news_draw_text_clipped(panel->buffer, draw_x, text_y, NEWS_SEPARATOR,
|
||||||
|
colors->panel_fg, &clip_rect);
|
||||||
|
}
|
||||||
|
draw_x += separator_width;
|
||||||
|
|
||||||
|
current_article = (current_article + 1) % news_state.article_count;
|
||||||
|
articles_drawn++;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
|
||||||
|
*used_width = max_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
void news_handle_click(int x, int y)
|
||||||
|
{
|
||||||
|
(void)y;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
if (news_state.article_count == 0) {
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int article_index = news_find_article_at_x(x);
|
||||||
|
if (article_index >= 0 && article_index < news_state.article_count) {
|
||||||
|
char url[512];
|
||||||
|
strncpy(url, news_state.articles[article_index].link, sizeof(url) - 1);
|
||||||
|
url[sizeof(url) - 1] = '\0';
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
setsid();
|
||||||
|
pid_t pid2 = fork();
|
||||||
|
if (pid2 == 0) {
|
||||||
|
execlp("xdg-open", "xdg-open", url, NULL);
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
_exit(0);
|
||||||
|
} else if (pid > 0) {
|
||||||
|
int status;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void news_lock(void)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&news_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void news_unlock(void)
|
||||||
|
{
|
||||||
|
pthread_mutex_unlock(&news_mutex);
|
||||||
|
}
|
||||||
856
src/notifications.c
Normal file
856
src/notifications.c
Normal 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, ¬if_width, ¬if_height);
|
||||||
|
notif->width = notif_width;
|
||||||
|
notif->height = notif_height;
|
||||||
|
|
||||||
|
/* Create notification window */
|
||||||
|
if (dwn != NULL && dwn->display != NULL) {
|
||||||
|
XSetWindowAttributes swa;
|
||||||
|
swa.override_redirect = True;
|
||||||
|
swa.background_pixel = dwn->config->colors.notification_bg;
|
||||||
|
swa.event_mask = ExposureMask | ButtonPressMask;
|
||||||
|
|
||||||
|
notif->window = XCreateWindow(dwn->display, dwn->root,
|
||||||
|
0, 0,
|
||||||
|
notif->width, notif->height,
|
||||||
|
1,
|
||||||
|
CopyFromParent, InputOutput, CopyFromParent,
|
||||||
|
CWOverrideRedirect | CWBackPixel | CWEventMask,
|
||||||
|
&swa);
|
||||||
|
|
||||||
|
XSetWindowBorder(dwn->display, notif->window,
|
||||||
|
dwn->config->colors.border_focused);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add to list */
|
||||||
|
notif->next = notification_list;
|
||||||
|
notification_list = notif;
|
||||||
|
|
||||||
|
/* Position and show */
|
||||||
|
notifications_position();
|
||||||
|
notification_render(notif);
|
||||||
|
|
||||||
|
if (notif->window != None) {
|
||||||
|
XMapRaised(dwn->display, notif->window);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("Notification %u (%dx%d): %s - %s", notif->id,
|
||||||
|
notif->width, notif->height, notif->summary, notif->body);
|
||||||
|
|
||||||
|
return notif->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void notification_close(uint32_t id)
|
||||||
|
{
|
||||||
|
Notification *prev = NULL;
|
||||||
|
Notification *notif = notification_list;
|
||||||
|
|
||||||
|
while (notif != NULL) {
|
||||||
|
if (notif->id == id) {
|
||||||
|
/* Remove from list */
|
||||||
|
if (prev != NULL) {
|
||||||
|
prev->next = notif->next;
|
||||||
|
} else {
|
||||||
|
notification_list = notif->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Destroy window */
|
||||||
|
if (notif->window != None && dwn != NULL && dwn->display != NULL) {
|
||||||
|
XDestroyWindow(dwn->display, notif->window);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Free dynamically allocated body */
|
||||||
|
if (notif->body != NULL) {
|
||||||
|
dwn_free(notif->body);
|
||||||
|
notif->body = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
dwn_free(notif);
|
||||||
|
|
||||||
|
/* Reposition remaining notifications */
|
||||||
|
notifications_position();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = notif;
|
||||||
|
notif = notif->next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void notification_close_all(void)
|
||||||
|
{
|
||||||
|
while (notification_list != NULL) {
|
||||||
|
notification_close(notification_list->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification *notification_find(uint32_t id)
|
||||||
|
{
|
||||||
|
for (Notification *n = notification_list; n != NULL; n = n->next) {
|
||||||
|
if (n->id == id) {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification *notification_find_by_window(Window window)
|
||||||
|
{
|
||||||
|
for (Notification *n = notification_list; n != NULL; n = n->next) {
|
||||||
|
if (n->window == window) {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Rendering ========== */
|
||||||
|
|
||||||
|
void notification_render(Notification *notif)
|
||||||
|
{
|
||||||
|
if (notif == NULL || notif->window == None) {
|
||||||
|
LOG_DEBUG("notification_render: notif or window is NULL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dwn == NULL || dwn->display == NULL) {
|
||||||
|
LOG_DEBUG("notification_render: dwn or display is NULL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dwn->xft_font == NULL && dwn->font == NULL) {
|
||||||
|
LOG_ERROR("notification_render: no font available - cannot render text");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Display *dpy = dwn->display;
|
||||||
|
const ColorScheme *colors = config_get_colors();
|
||||||
|
|
||||||
|
if (colors == NULL) {
|
||||||
|
LOG_ERROR("notification_render: colors is NULL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("Rendering notification: summary='%s', body_len=%zu",
|
||||||
|
notif->summary, notif->body_len);
|
||||||
|
|
||||||
|
/* Set legacy font in GC if available (for fallback) */
|
||||||
|
if (dwn->font != NULL) {
|
||||||
|
XSetFont(dpy, dwn->gc, dwn->font->fid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clear background using dynamic size */
|
||||||
|
XSetForeground(dpy, dwn->gc, colors->notification_bg);
|
||||||
|
XFillRectangle(dpy, notif->window, dwn->gc, 0, 0,
|
||||||
|
notif->width, notif->height);
|
||||||
|
|
||||||
|
/* Draw summary (title) */
|
||||||
|
int line_height = notif_line_height();
|
||||||
|
int y = NOTIFICATION_PADDING + notif_font_ascent();
|
||||||
|
notif_draw_text(notif->window, NOTIFICATION_PADDING, y,
|
||||||
|
notif->summary, strlen(notif->summary),
|
||||||
|
colors->notification_fg);
|
||||||
|
|
||||||
|
/* Draw body - handle multiple lines */
|
||||||
|
y += line_height + 4;
|
||||||
|
|
||||||
|
int max_width = notif->width - 2 * NOTIFICATION_PADDING;
|
||||||
|
int max_y = notif->height - line_height - NOTIFICATION_PADDING;
|
||||||
|
|
||||||
|
/* Only process body if it exists */
|
||||||
|
if (notif->body != NULL && notif->body_len > 0) {
|
||||||
|
/* Dynamically allocate body copy for tokenization */
|
||||||
|
char *body_copy = dwn_malloc(notif->body_len + 1);
|
||||||
|
if (body_copy != NULL) {
|
||||||
|
memcpy(body_copy, notif->body, notif->body_len + 1);
|
||||||
|
|
||||||
|
/* Split by newlines and draw each line */
|
||||||
|
char *line = body_copy;
|
||||||
|
char *next;
|
||||||
|
|
||||||
|
while (line != NULL && *line != '\0' && y < max_y) {
|
||||||
|
/* Find next newline */
|
||||||
|
next = strchr(line, '\n');
|
||||||
|
if (next != NULL) {
|
||||||
|
*next = '\0';
|
||||||
|
next++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skip empty lines but count them for spacing */
|
||||||
|
if (*line == '\0') {
|
||||||
|
y += line_height / 2; /* Half-height for empty lines */
|
||||||
|
line = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Word wrap long lines instead of truncating */
|
||||||
|
size_t line_len = strlen(line);
|
||||||
|
const char *p = line;
|
||||||
|
|
||||||
|
while (*p != '\0' && y < max_y) {
|
||||||
|
/* Find how much text fits on this line */
|
||||||
|
size_t fit_len = 0;
|
||||||
|
size_t last_space = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; p[i] != '\0'; i++) {
|
||||||
|
/* Skip UTF-8 continuation bytes for character counting */
|
||||||
|
if ((p[i] & 0xC0) == 0x80) continue;
|
||||||
|
|
||||||
|
int width = notif_text_width(p, i + 1);
|
||||||
|
if (width > max_width) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fit_len = i + 1;
|
||||||
|
|
||||||
|
/* Track last space for word wrapping */
|
||||||
|
if (p[i] == ' ') {
|
||||||
|
last_space = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust to include full UTF-8 characters */
|
||||||
|
while (fit_len < line_len && (p[fit_len] & 0xC0) == 0x80) {
|
||||||
|
fit_len++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we couldn't fit anything, force at least one character */
|
||||||
|
if (fit_len == 0 && *p != '\0') {
|
||||||
|
fit_len = 1;
|
||||||
|
while (fit_len < line_len && (p[fit_len] & 0xC0) == 0x80) {
|
||||||
|
fit_len++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prefer breaking at word boundary if possible */
|
||||||
|
if (fit_len < strlen(p) && last_space > 0 && last_space > fit_len / 2) {
|
||||||
|
fit_len = last_space + 1; /* Include the space */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Draw this segment */
|
||||||
|
if (fit_len > 0) {
|
||||||
|
char *segment = dwn_malloc(fit_len + 1);
|
||||||
|
if (segment != NULL) {
|
||||||
|
memcpy(segment, p, fit_len);
|
||||||
|
segment[fit_len] = '\0';
|
||||||
|
|
||||||
|
notif_draw_text(notif->window, NOTIFICATION_PADDING, y,
|
||||||
|
segment, strlen(segment), colors->panel_fg);
|
||||||
|
|
||||||
|
dwn_free(segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
p += fit_len;
|
||||||
|
/* Skip leading spaces on new line */
|
||||||
|
while (*p == ' ') p++;
|
||||||
|
|
||||||
|
y += line_height;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
dwn_free(body_copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Draw app name at bottom */
|
||||||
|
if (notif->app_name[0] != '\0') {
|
||||||
|
y = notif->height - NOTIFICATION_PADDING;
|
||||||
|
notif_draw_text(notif->window, NOTIFICATION_PADDING, y,
|
||||||
|
notif->app_name, strlen(notif->app_name),
|
||||||
|
colors->workspace_inactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force the drawing to be sent to X server */
|
||||||
|
XFlush(dpy);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifications_render_all(void)
|
||||||
|
{
|
||||||
|
for (Notification *n = notification_list; n != NULL; n = n->next) {
|
||||||
|
notification_render(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifications_update(void)
|
||||||
|
{
|
||||||
|
long now = get_time_ms();
|
||||||
|
|
||||||
|
/* Check for expired notifications */
|
||||||
|
Notification *notif = notification_list;
|
||||||
|
while (notif != NULL) {
|
||||||
|
Notification *next = notif->next;
|
||||||
|
|
||||||
|
if (notif->expire_time > 0 && now >= notif->expire_time) {
|
||||||
|
notification_close(notif->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
notif = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifications_position(void)
|
||||||
|
{
|
||||||
|
if (dwn == NULL || dwn->display == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int y = NOTIFICATION_MARGIN;
|
||||||
|
|
||||||
|
/* Account for top panel */
|
||||||
|
if (dwn->config && dwn->config->top_panel_enabled) {
|
||||||
|
y += config_get_panel_height();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Notification *n = notification_list; n != NULL; n = n->next) {
|
||||||
|
if (n->window != None) {
|
||||||
|
/* Position from right edge using notification's own width */
|
||||||
|
int x = dwn->screen_width - n->width - NOTIFICATION_MARGIN;
|
||||||
|
XMoveWindow(dwn->display, n->window, x, y);
|
||||||
|
}
|
||||||
|
/* Stack using notification's own height */
|
||||||
|
y += n->height + NOTIFICATION_MARGIN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifications_raise_all(void)
|
||||||
|
{
|
||||||
|
if (dwn == NULL || dwn->display == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Raise all notification windows to the top */
|
||||||
|
for (Notification *n = notification_list; n != NULL; n = n->next) {
|
||||||
|
if (n->window != None) {
|
||||||
|
XRaiseWindow(dwn->display, n->window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Server info ========== */
|
||||||
|
|
||||||
|
void notifications_get_server_info(const char **name, const char **vendor,
|
||||||
|
const char **version, const char **spec_version)
|
||||||
|
{
|
||||||
|
if (name) *name = "DWN";
|
||||||
|
if (vendor) *vendor = "DWN Project";
|
||||||
|
if (version) *version = DWN_VERSION;
|
||||||
|
if (spec_version) *spec_version = "1.2";
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifications_get_capabilities(const char ***caps, int *count)
|
||||||
|
{
|
||||||
|
static const char *capabilities[] = { "body", "body-markup" };
|
||||||
|
if (caps) *caps = capabilities;
|
||||||
|
if (count) *count = 2;
|
||||||
|
}
|
||||||
892
src/panel.c
Normal file
892
src/panel.c
Normal 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
1105
src/systray.c
Normal file
File diff suppressed because it is too large
Load Diff
709
src/util.c
Normal file
709
src/util.c
Normal 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
483
src/workspace.c
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user