From 7a4f7a82ad32108455de774e2acc33bca68687bb Mon Sep 17 00:00:00 2001 From: retoor Date: Sun, 28 Dec 2025 03:14:31 +0100 Subject: [PATCH] chore: update c, css, d files --- LICENSE | 21 + Makefile | 263 ++++ bin/dwn | Bin 0 -> 216616 bytes build/ai.d | 46 + build/ai.o | Bin 0 -> 29392 bytes build/applauncher.d | 6 + build/applauncher.o | Bin 0 -> 14608 bytes build/atoms.d | 4 + build/atoms.o | Bin 0 -> 22088 bytes build/cJSON.d | 2 + build/cJSON.o | Bin 0 -> 36808 bytes build/client.d | 47 + build/client.o | Bin 0 -> 24688 bytes build/config.d | 5 + build/config.o | Bin 0 -> 13568 bytes build/decorations.d | 7 + build/decorations.o | Bin 0 -> 9576 bytes build/keys.d | 49 + build/keys.o | Bin 0 -> 38520 bytes build/layout.d | 8 + build/layout.o | Bin 0 -> 6008 bytes build/main.d | 56 + build/main.o | Bin 0 -> 26312 bytes build/news.d | 8 + build/news.o | Bin 0 -> 20416 bytes build/notifications.d | 42 + build/notifications.o | Bin 0 -> 20040 bytes build/panel.d | 13 + build/panel.o | Bin 0 -> 23344 bytes build/systray.d | 44 + build/systray.o | Bin 0 -> 32904 bytes build/util.d | 3 + build/util.o | Bin 0 -> 19952 bytes build/workspace.d | 10 + build/workspace.o | Bin 0 -> 13288 bytes config/config.example | 93 ++ include/ai.h | 94 ++ include/applauncher.h | 48 + include/atoms.h | 148 ++ include/cJSON.h | 306 ++++ include/client.h | 80 + include/config.h | 87 ++ include/decorations.h | 48 + include/dwn.h | 168 +++ include/keys.h | 98 ++ include/layout.h | 25 + include/news.h | 66 + include/notifications.h | 73 + include/panel.h | 65 + include/systray.h | 134 ++ include/util.h | 68 + include/workspace.h | 61 + scripts/dwn.desktop | 8 + scripts/xinitrc.example | 42 + site/ai-features.html | 458 ++++++ site/architecture.html | 565 +++++++ site/configuration.html | 520 +++++++ site/css/style.css | 1085 +++++++++++++ site/documentation.html | 423 ++++++ site/features.html | 411 +++++ site/index.html | 352 +++++ site/installation.html | 610 ++++++++ site/js/main.js | 304 ++++ site/shortcuts.html | 413 +++++ src/ai.c | 872 +++++++++++ src/applauncher.c | 507 +++++++ src/atoms.c | 463 ++++++ src/cJSON.c | 3191 +++++++++++++++++++++++++++++++++++++++ src/client.c | 1110 ++++++++++++++ src/config.c | 361 +++++ src/decorations.c | 368 +++++ src/keys.c | 842 +++++++++++ src/layout.c | 263 ++++ src/main.c | 1016 +++++++++++++ src/news.c | 697 +++++++++ src/notifications.c | 856 +++++++++++ src/panel.c | 892 +++++++++++ src/systray.c | 1105 ++++++++++++++ src/util.c | 709 +++++++++ src/workspace.c | 483 ++++++ 80 files changed, 21222 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100755 bin/dwn create mode 100644 build/ai.d create mode 100644 build/ai.o create mode 100644 build/applauncher.d create mode 100644 build/applauncher.o create mode 100644 build/atoms.d create mode 100644 build/atoms.o create mode 100644 build/cJSON.d create mode 100644 build/cJSON.o create mode 100644 build/client.d create mode 100644 build/client.o create mode 100644 build/config.d create mode 100644 build/config.o create mode 100644 build/decorations.d create mode 100644 build/decorations.o create mode 100644 build/keys.d create mode 100644 build/keys.o create mode 100644 build/layout.d create mode 100644 build/layout.o create mode 100644 build/main.d create mode 100644 build/main.o create mode 100644 build/news.d create mode 100644 build/news.o create mode 100644 build/notifications.d create mode 100644 build/notifications.o create mode 100644 build/panel.d create mode 100644 build/panel.o create mode 100644 build/systray.d create mode 100644 build/systray.o create mode 100644 build/util.d create mode 100644 build/util.o create mode 100644 build/workspace.d create mode 100644 build/workspace.o create mode 100644 config/config.example create mode 100644 include/ai.h create mode 100644 include/applauncher.h create mode 100644 include/atoms.h create mode 100644 include/cJSON.h create mode 100644 include/client.h create mode 100644 include/config.h create mode 100644 include/decorations.h create mode 100644 include/dwn.h create mode 100644 include/keys.h create mode 100644 include/layout.h create mode 100644 include/news.h create mode 100644 include/notifications.h create mode 100644 include/panel.h create mode 100644 include/systray.h create mode 100644 include/util.h create mode 100644 include/workspace.h create mode 100644 scripts/dwn.desktop create mode 100644 scripts/xinitrc.example create mode 100644 site/ai-features.html create mode 100644 site/architecture.html create mode 100644 site/configuration.html create mode 100644 site/css/style.css create mode 100644 site/documentation.html create mode 100644 site/features.html create mode 100644 site/index.html create mode 100644 site/installation.html create mode 100644 site/js/main.js create mode 100644 site/shortcuts.html create mode 100644 src/ai.c create mode 100644 src/applauncher.c create mode 100644 src/atoms.c create mode 100644 src/cJSON.c create mode 100644 src/client.c create mode 100644 src/config.c create mode 100644 src/decorations.c create mode 100644 src/keys.c create mode 100644 src/layout.c create mode 100644 src/main.c create mode 100644 src/news.c create mode 100644 src/notifications.c create mode 100644 src/panel.c create mode 100644 src/systray.c create mode 100644 src/util.c create mode 100644 src/workspace.c diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0b23588 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 DWN Project + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8dc3dae --- /dev/null +++ b/Makefile @@ -0,0 +1,263 @@ +# DWN - Desktop Window Manager +# Simple Makefile - just run 'make' to build! + +# Compiler settings +CC = gcc +CFLAGS = -Wall -Wextra -O2 -I./include +LDFLAGS = -lX11 -lXext -lXinerama -lXrandr -lXft -lfontconfig -ldbus-1 -lcurl -lm -lpthread + +# Directories +SRC_DIR = src +INC_DIR = include +BUILD_DIR = build +BIN_DIR = bin + +# Find all source files automatically +SRCS = $(wildcard $(SRC_DIR)/*.c) +OBJS = $(SRCS:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o) +DEPS = $(OBJS:.o=.d) + +# Output binary +TARGET = $(BIN_DIR)/dwn + +# Installation paths +PREFIX ?= /usr/local +BINDIR = $(PREFIX)/bin +DATADIR = $(PREFIX)/share +SYSCONFDIR = /etc + +# Get pkg-config flags (with error handling) +PKG_CFLAGS := $(shell pkg-config --cflags x11 xext xinerama xrandr xft fontconfig dbus-1 2>/dev/null) +PKG_LIBS := $(shell pkg-config --libs x11 xext xinerama xrandr xft fontconfig dbus-1 2>/dev/null) + +# Use pkg-config if available +ifneq ($(PKG_LIBS),) + LDFLAGS = $(PKG_LIBS) -lcurl -lm -lpthread +endif +ifneq ($(PKG_CFLAGS),) + CFLAGS += $(PKG_CFLAGS) +endif + +# ============================================================================= +# MAIN TARGETS +# ============================================================================= + +.PHONY: all help clean install uninstall debug run test deps check-deps + +# Default target - show help if first time, otherwise build +all: check-deps $(TARGET) + @echo "" + @echo "Build successful! Binary created at: $(TARGET)" + @echo "" + @echo "Next steps:" + @echo " make run - Test DWN in a window (safe, doesn't affect your desktop)" + @echo " make install - Install DWN system-wide" + @echo "" + +# Show help +help: + @echo "==============================================" + @echo " DWN - Desktop Window Manager" + @echo "==============================================" + @echo "" + @echo "QUICK START:" + @echo " make deps - Install required dependencies (Ubuntu/Debian)" + @echo " make - Build DWN" + @echo " make run - Test DWN in a safe window" + @echo " make install - Install to your system" + @echo "" + @echo "ALL COMMANDS:" + @echo " make - Build DWN (release version)" + @echo " make debug - Build with debug symbols" + @echo " make run - Test in Xephyr window (safe!)" + @echo " make install - Install to $(BINDIR)" + @echo " make uninstall- Remove from system" + @echo " make clean - Remove build files" + @echo " make deps - Install dependencies (needs sudo)" + @echo " make help - Show this help" + @echo "" + @echo "AFTER INSTALL:" + @echo " 1. Log out of your current session" + @echo " 2. At login screen, select 'DWN' as your session" + @echo " 3. Log in and enjoy!" + @echo "" + +# ============================================================================= +# BUILD TARGETS +# ============================================================================= + +# Build with debug symbols +debug: CFLAGS += -g -DDEBUG +debug: clean $(TARGET) + @echo "Debug build complete!" + +# Link all object files into final binary +$(TARGET): $(OBJS) | $(BIN_DIR) + @echo "Linking..." + @$(CC) $(OBJS) -o $@ $(LDFLAGS) + +# Compile each source file +$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR) + @echo "Compiling $<..." + @$(CC) $(CFLAGS) -MMD -MP -c $< -o $@ + +# Create build directory +$(BUILD_DIR): + @mkdir -p $(BUILD_DIR) + +# Create bin directory +$(BIN_DIR): + @mkdir -p $(BIN_DIR) + +# Include dependency files +-include $(DEPS) + +# ============================================================================= +# INSTALL / UNINSTALL +# ============================================================================= + +install: $(TARGET) + @echo "Installing DWN..." + @install -Dm755 $(TARGET) $(DESTDIR)$(BINDIR)/dwn + @install -Dm644 scripts/dwn.desktop $(DESTDIR)$(DATADIR)/xsessions/dwn.desktop + @mkdir -p $(DESTDIR)$(SYSCONFDIR)/dwn + @install -Dm644 config/config.example $(DESTDIR)$(SYSCONFDIR)/dwn/config.example + @echo "" + @echo "==============================================" + @echo " Installation complete!" + @echo "==============================================" + @echo "" + @echo "To configure DWN, run:" + @echo " mkdir -p ~/.config/dwn" + @echo " cp $(SYSCONFDIR)/dwn/config.example ~/.config/dwn/config" + @echo "" + @echo "Then log out and select 'DWN' at the login screen!" + @echo "" + +uninstall: + @echo "Removing DWN..." + @rm -f $(DESTDIR)$(BINDIR)/dwn + @rm -f $(DESTDIR)$(DATADIR)/xsessions/dwn.desktop + @rm -rf $(DESTDIR)$(SYSCONFDIR)/dwn + @echo "DWN has been uninstalled." + +# ============================================================================= +# DEVELOPMENT / TESTING +# ============================================================================= + +# Test DWN in a nested X server (safe - doesn't touch your desktop) +run: $(TARGET) + @echo "Starting DWN in test mode..." + @echo "(Press Alt+Shift+Q inside the window to quit)" + Xephyr :1 -screen 1280x720 & + sleep 1 + DISPLAY=:1 $(TARGET) + +# Clean build files +clean: + @echo "Cleaning build files..." + @rm -rf $(BUILD_DIR) $(BIN_DIR) + @echo "Clean complete." + +# ============================================================================= +# DEPENDENCY MANAGEMENT +# ============================================================================= + +# Check if dependencies are installed +check-deps: + @command -v pkg-config >/dev/null 2>&1 || { \ + echo "ERROR: pkg-config is not installed!"; \ + echo "Run: make deps"; \ + exit 1; \ + } + @pkg-config --exists x11 2>/dev/null || { \ + echo "ERROR: X11 development libraries not found!"; \ + echo "Run: make deps"; \ + exit 1; \ + } + @pkg-config --exists dbus-1 2>/dev/null || { \ + echo "ERROR: D-Bus development libraries not found!"; \ + echo "Run: make deps"; \ + exit 1; \ + } + +# Install dependencies (Ubuntu/Debian) +deps: + @echo "Installing dependencies..." + @echo "This requires sudo (administrator) access." + @echo "" + @if command -v apt >/dev/null 2>&1; then \ + sudo apt update && sudo apt install -y \ + build-essential \ + pkg-config \ + libx11-dev \ + libxext-dev \ + libxinerama-dev \ + libxrandr-dev \ + libxft-dev \ + libfontconfig1-dev \ + libdbus-1-dev \ + libcurl4-openssl-dev \ + xserver-xephyr \ + dmenu; \ + echo ""; \ + echo "Dependencies installed successfully!"; \ + echo "Now run: make"; \ + elif command -v dnf >/dev/null 2>&1; then \ + sudo dnf install -y \ + gcc make \ + pkg-config \ + libX11-devel \ + libXext-devel \ + libXinerama-devel \ + libXrandr-devel \ + dbus-devel \ + libcurl-devel \ + xorg-x11-server-Xephyr \ + dmenu; \ + echo ""; \ + echo "Dependencies installed successfully!"; \ + echo "Now run: make"; \ + elif command -v pacman >/dev/null 2>&1; then \ + sudo pacman -S --needed \ + base-devel \ + pkg-config \ + libx11 \ + libxext \ + libxinerama \ + libxrandr \ + dbus \ + curl \ + xorg-server-xephyr \ + dmenu; \ + echo ""; \ + echo "Dependencies installed successfully!"; \ + echo "Now run: make"; \ + else \ + echo "ERROR: Could not detect package manager!"; \ + echo "Please install these packages manually:"; \ + echo " - GCC and Make"; \ + echo " - pkg-config"; \ + echo " - X11, Xext, Xinerama, Xrandr development libraries"; \ + echo " - D-Bus development library"; \ + echo " - libcurl development library"; \ + echo " - Xephyr (for testing)"; \ + echo " - dmenu (for app launcher)"; \ + exit 1; \ + fi + +# ============================================================================= +# CODE QUALITY (for developers) +# ============================================================================= + +.PHONY: format check + +format: + @echo "Formatting code..." + @clang-format -i $(SRC_DIR)/*.c $(INC_DIR)/*.h 2>/dev/null || \ + echo "Note: clang-format not installed (optional)" + +check: + @echo "Running static analysis..." + @cppcheck --enable=all --inconclusive -I$(INC_DIR) $(SRC_DIR) 2>/dev/null || \ + echo "Note: cppcheck not installed (optional)" diff --git a/bin/dwn b/bin/dwn new file mode 100755 index 0000000000000000000000000000000000000000..02852c2c39d7b4b3f7dd9a8f659a236c20672b1e GIT binary patch literal 216616 zcmeFad3+Q_8b90<5(sC9BNz`P;Gjf76C|2QA{i2(M+S(TQP2=_Kr|$AG6O*n0+S%U z;~>kj9=ofuc;SycIi@e{bs(ZT9(C_d4yr1{Kcf*Iw ze7{xoRMk^YRXufdXX$9qm?(!sGJZNpw@QTS%Q%yz2WX=^=-(tMOG=Y^;P(J2QR)ab z8vnEOQ0r%$p3L}+)#DLO>7LQUt)Exu@r+NSg!D=2Y=1`UA;xDM$CNIHzUFdl{rv7- zo`dlz3F%mVaZx;>|gc6r0lR+{n{eKeO~= z7@vkdsf_eLSJ$WYGZ|s@V|-fE)#&N0pH7}kGCnB}`k_zy@A(HWko9vlMZ%BqX{4L{ zov(<#Tgzu)qkP7vQO;aF-A5C7e%8-unPn$MJ~@^8y|I?p1J>n!iKWn=gI9G2! zxt9VD@E0CEXMDnhf{oi*DK#0a?bbH+7*ZYmVH%b*G~WH zmUBZ{>Fk?^-%wUGw5+t;Up=%s?WUnO4Zpss;`(7s?w3eU@~ED*j%AWGEACq2zQTX{ zY3y+@^V8zp^H!XCWO3T*;1wA!r+0sO+_&{C(B{__C~&WfgteeSF2*;bq8HRSyN7n2`nqv!I@ z7mvRgjeIdV2{w8=VNe~*p*Y_+NHqc-*Z+=lOD!*{i5muGG4 z{Es&JDX@|MwM~6rw~_yjjr^-^^dGQ^|DcWhA8pF>oQ)oa+3=Ux^xvCp+OZ$XlZgM; zpF3^zb{Y{c){o}fNUV7-&bwq{J};K0h@ZY*~EX|rhNz7=rh4aPL@si|7OGg z*+%|XHue48#-3%HetphH&mC;)yV0gRVH>`eO}*SU<^RMc{$!hajkW17FWKl{v$69h zZ0bALCf`5Ul=HleU2U<6Kgp(?#Www=(5Ah9w2A+bO?hV6wAUgVJGsWD{6}o^&9#ve zZ=>f18-AuudyTY-Uu{#)%{K9CZS=X!rd^KM*uzscc35F!=a<=(=UJO}dBi5)S8U|a zy7Xf6+BzG3UTLF;W*hl#oAynz(ZenqIUm`S^KUlg>1Cr&pN;%^Hsy@B(L=5cztu+n zJ#F%(wdBS0f2)mMy>8Qwp0Me^tv2~4*~C9&qqiY8`L4F%&)DeaDjWG*Z0ftfMoztr zJ^aN+&J8yFR2w~fVPl^jn{i{Njr|Nk`zGSQ_2+k+d}rI}XMs(9SK8?36Pxm{w~_yu zO*_u8sqa3Ue9zjHGuwv0(Z)VwZQ85Qrd|_m>UGQ}{$d;bkFaUSH*MCq6&!#+MY~&BO(L)cL`cAWn-)N(^lQwd$wTXX~P5eu2 z^3Al7Gu1|Koo)1=Zj-NM!*8+4caBZIM{VS+Li~Z&K?AtAO~06j_&ud7q*?VS0DfR* z>}RY^zAE(A6aOTA-yLSdfAZZ^#^)CmdyD6kR{4s(Q^sePRg@P`DVSYWEam6VnO9Ms zU*#+C`ttK77WJYL^ClO}FV3zg_Z3(Bq`a}kzUigqMHLHNKA*RAw%=D=#mVux-U?qu zVMQ76^M#pRT-w_tW|MJbBmmGY*R&nu|p<&g4j_ZNE?W>=I| zAb^BSFD>%TmGZK^#Ra}%BbPB{{wk1q-bT{N3(E_oyz!prbL~;$ki; z$6K(#Q(9hZq>B`;*vAy36qFa0k=XGS^HHCw()){zC{(}&OhJ(7saR0#6+;Rq6wE6w z(#uc!gIamUPy{laR9xsQD4zpkpccz6E3J$$yb;|K+ZVu1+AnwcD$q39bCI+djb2&o z^+D{^@;OEhy!ED3;Ipuzh)FLm(wlKoL1|T__%Mr#a>{dhsc){)Qn|(DMWy9)ctOWz zBd3bWg)THEm||byT&ffmxD4ss<&}PJl-XqkRaK-~DQ{e9Ss5zBtcI7$XcwyVWSA{U zCuJJSLhYHPF%^Y=y=e>+U`;*+Z57ec_<|}JF&7*$$cdH3$Ts^UnF zImIO~Aj8;T&rHqedMFCdhns1;t}(fEUS(M%*GR`Un-n6w1@j8rRjxu`>3nEFCzwtq z7eXoJ&{hem21BRv5MVmy)?`4v(-EZpIl9`1>Q_zmm82P|NqxLsxd5&LBRy|2BR;cd z)-$ISl=)enjG_)D70vcn<<`q{} z70fBlFQ`Ouit-D*bE?`0l*0V;5uJkaeG4mf$woRBjQ&ubkGy;Zq^TltS^3Mo#U<^;Lkh=2O^eV?3eb<+%O%yY!M&uQ@WKW| z?=Q+<;6>)`Q$@;pK`tcEUtU_BU!u1(uW(7kNZKj59A+sM`n_fO#RXLhQ4o}y!8v6W zvkS`d3(JZN%Keo>6pVT$7|uB}&tK*%&2L9BIhf~Iwqh((Hj9FCmX?=P@EBEPn5PZ3 zv;4VaZ}B{gR{0olF%gKu$ZABEiELL`E}$L7i&pHbsMIUT<4`eRA_YY}1HI;~xFslM zl~i6(UQtz6TwEztl~-aA^_55%eXAtYvb?ZzAv_9v!Dk)*JJwkmIMHI>gcQ<{kUu*g z(Ir3ER6h0hh)#T7B$VdeOGS@VjTMhnG2c)tE14l1!z|nwB%&f!MzWj-&#Rhpx>rI3 z@>3w8O4Md?HH?_$&y0sPiqBiZ8UlG17E~1%LLjs*rpiYTL0{8j7gkl4OYm7pd?nL5 z;^U;fQ1=_>VUUwpTR|mogS?WL=66Kmy70j(gHysZ0LgG8#N5#nCQtU{50}OwEIWVL^}|vlUvG|lOEJk5 zlfBVQWq!LcEt2X+% zcuF%gmgQ&4rCJze;N|bcBs(BMEE&Zi>Gau2C^xSgmBq;xzj$=iyxCEfQ)#Q&#% z_!%qKPSR0u^h4pzLipiPw_)koLHaQg-c9;i52yHbe|!WxILN&lmw8EPDQ*b$kh<&m z&n&#Vbcv21Ww@I(P{(gD+*KN?<322X7b#80n^<^fX_StAmzH8h9xIL4ac>sCqjZOk zUuW@Sq(U99WjIn(hretbQ@)55PC z!TDwj|ML}`*DU;-iJU)W;VUyZf6l^h(D}I6dAL!|UAmkE3*W5APqgs=(c`CC_+NBB z%fiRf#tnWH3qQok`B@ge`vA_*weV@WoJtFC$XRLO`|EO6Tlh*{&SMsSoz8Et@WXXE zn=E|29>3ng8|B$+;m7K74qEtAx*W~I8*-#K+PBwTx|~=GpLi>e7iZ!9di=o_Ue)IGt0soa;h!-o4TAD3*W5ES!v-7IqNL^C%T*$EPUKZ zuAdDS{)irbr-kn@kjHPf@L9T?y%zowJ^m>Rf4$D1v+y;#9Emof@niHyqh1LXK3kWQ zXyG^Lat2#?Lr#{3&(q~77XF|vC)dIoa^_n2dAgiR3!jk1>r-vvm+J9XTll}~{9_hA zPnWaK!W(inS@@TAIrSF)FhJIq} zxjdsC4V-M@4Lzh;_!eD18{X$~;Ot0r$JqEu)Z^nAt{uNgh_AaEEJ?k<8~2ncAVH8L z@;in2dzl3Ot6AVP1b(l;?-TeqLB7Zz6ym=q#McD=Q-L?mFK`?q{afG@-{$&(GYJpU z{246pJO&R-7I++eMSju*K62)P&@6$cyp114;HhnlpIm{bG8sR40&my^h0PLpIEay- zxdM-)n#fP3!0WCt3#%4*{i;0UYXrVWM2aM>6nOnAKMP$g@W$PI3V2N5aXJ+FStsz~ zdE^TM-#Zc;ey_mOJ$&P5lfcItBtGi}zOTUV6!=R7zFFWC1b(l;Un=ki1^zOD*987@ zfj=ei#EpupcE@S4Dn5cpF9pDFO?1pZcmm)2PZs#m0-q-EV+1}+;KvHQBJhg9=L)=A;PV9jHi4fd@Z$u2uE2W) zzEa@F3w*V}PZ0PTfuAVwD+NAR;8zR$?E?Rpz)uqRbplU!Y>l561b&J^;&X$*PZju0 z0zXaQ>ji$g!0#0JJb`Z(_&WrCufX3a@COBchQMnAKU3gO3H)6Ge@@`<7I^7h)Bf`X zK33po34ENu7YKZUz|R)=M1e09_`w2SB=E@sUo7xx0$(EVSpq*t;1z+NEAY7jUn=l< z0)LOd&l3211%9r;mkE5O!0TIMEVNqS%Z2zg0$(BUD+Rt%;8zR$eFFcOzji#+!0#0JYJqPS_=N($SK#j#_=5t!NZ>VrUo7ya1pWbm zKPT`@1YUa2wEv|7A1m-R0v{*v%LG0_;Fk-0qQD0Pez3sT3VgD_*9m-@z^@SaEP;Pe z;1z*iDe$=huL^vg!2dzuX9;{z;O7ebLjqqZ@F9V(7Wh>HUnB4j3;arfUoG&f1^$l$ z|Cqq95%_fi|0jWeLEzU4{04#lv%qf>_(uf3Uf>@U_?-g(n7}s+{Nn<@SK$95@COC{ zuL7?L{NDurl)yhB@aF{nNr9KPnD+maz{d*wI)RT9_`eH$g21mA_(Xw!THpr@{4)Zd zEbz|?e44;NC-7MU|GdB}0{?=*=L-Bk1U^sT|0(dZ1pXy~pDXY$3w))(zasF}0{^PO z*9iOufnO=`{}TAs0{@!8KPK?63;a5Pe?#D35coF*euKbo6!=X7|CYel3;ZU5-zo5K z3w*P{Zx;By0{@P{9~Ag^1zr>Q_XPfwz;6-wa{~Xqz)SjmPA72n0v{{zTLnH&;2Q)! zLEyIue4@Z_7x=*fzeC`Y1-?<>(*(Xr;IjmNr@$)$zf0hA1^xqp&lC6$1%8&me0qH1MN*3f%4uoGbaWvueCXOMz z#>5>7FE=qQ*1}#B#}b}x;?9I8nYat#Q6}z6c$kU15gusb?u2`pxCh}V6U&58pO4fx zj_@H9_axk6;$DOsOx&CBYbNePc)f|^39m76U&6~xd?FM2#DfU0 zG4a)emz($+!d?>(COq53NrWev_*%lFOne>TVJ03zc%X@g67FT<>j_7h_y)qKe~Z*V zneZVKrx0#2@i4*-CccsIYbG8}c)f{J39m8nO@x=5_-4Xh6Q>cLZQ^volT3UI;ZY_Y zL3o&nGYAhfaVFtjCcc$$l!-?YKK*N?{#k?%nb<|R#l)isH<&n^@M|W{A-vwiqY1Au z@fgC(O+1#c*Tf3p*(P=qo@C>)hR#N!F~GVuh$Q6`>9`1CK4`sWfp zWa8Tix0rYm;RX{=Cj6R-rx0Fm;;Dq!n0OlDa*zgnOCzZo*L}&L@2O=Scl$5k6$%0>UjOo=v#H#D#=kGjS2&^(HPRyvD>O zgqNFm4q>l}X#xt*HgPH8NhZFB@F)}COL&-x%LorN@jSx4Ok7Sl%ET3fPoIm_zmo7F z6W>R;#l&914JNK4{F;eP5e0FNhbaa;ZY|3E8$@# z{u|+eCVql&FB3mWILgFN5kCFjNd4ClK4jv*6K*l_dcqARewy%WCVqzSdJ{iOc#Vml zBfQ+i&lC2V_yxkVP5ckSlT7?i!lO+5BH>{seu?lv6TeKjmx*5?9A)BH37`HcQvVHv z51II1gj-Df8sP>LzfSly6Td-ty@}r>yvD>E2`@MCTZFwP-b8q|iQgtX$;6upk23K) zgol~{c|L?Ub+xZu!LLjiW%+GFR|eH7F=$@B^G?Q1y8f!2^KuY zf^W6pn=JTx3%=Tdue9L47Tm*vJ6iDhJKO8yj0K;t;3F2i--7p8@NNrkwBRikywQSR zvEb({_(=fc!>qix8QOMF0tUdEqIy*Pq5%I7JRD(-(&Aa3JYFh!SgM+ z+=5Fi_-+fHX2BCIc#H+#YQZ;I@bwmawFO^k!F?^bhXr@E;PX>0?Qg*+Ecl28@3-JR z7QEYn8!dQ?1#h(AS1kBB3x3jqAF<$77QDiOmss$83of_d5(~cDf~Q&V1PdNx!M9rQ zO%{B;1z&B!S6Xmi3+`dT9WD6$6r=sI(rWiZ=TEL_uBommQzr-79P+9g9g-9pd)}>{ zQ9|Xho={n1T*fBm>4f(+giBUOU)u2 zbJfy%6A_2%5jE_D*44|kwCq;`J7PWQ$K_gDQ!2r`q#SwURZPOOkTBQ(6iM(u?hf{a zfVVa|BzEsCrC$3H+v)FjK_V@Tg9Rm&`6>dnC&1$SlY8;~kNo>)`d-Js@2BqzBPn0y zDVI>nLLTE7#hA{&$9;wGQZqAr(OCx!lHk}65B~fa*?Kttglm! zyVXzcR@6gE;P9#3DWg;BQ+7eA-#}Y!@f{{dNk5s1z~2r=3B}w-AL-|O@i_W?WfDL7 z?A&%x?q7qfrEU5r;tyc)Q!k^Br1Oe;O4)j1q_Xu~2gT8-e0tv32Lk@A3y5ty$k+Ww zda}=&%-<0#`LCO*1TrVlS4I8K*G&m#t^m-&CsD~Fl(jMD3t-1xNE^wY(uWtctG8Jh z#AEh9Rgqsk?N)!MPT*F5Qq&Lmk**8Jy6H-&tfNCwPiO_mht7AimEh!9MGY%z2akF} zN&i9#WyC4+s1KB+Pd(}(Z6UQwFdJ!mdmIN9H9NL38db-Uutz<}4Qt!zl*6sZjq_lo zu1&7J7cCZ88W$zkrQn+qoF1#xwaB$j0)b-*iuxrQfAim|>-BMJb=X`{)Fu>1u1!aj zEw@1DT7Sx!sJ@gcq&RA-9f|(<)g@+?mB3a|;a0b{4x*X}nFwE%!1oSUO-9c| ze-~F{bWa?RxAIbOIFY6pqO1%g9^FHBtfM=1i%eAQ)7LF*V5CsM~HI(a~#|f@$Q&xW_7wHhMoC}>rLR}Eb&hJ_Kje}VR-XA^&F9nk8ZUt~nH^xEA%Cp9a62`rn&XVs1}$+&*u@LJM~U3( zj=Hubi&=kF>h^hpMb21vXmLO4h0nk4kUXL8f5(T?v=w&Vwi(hTk34!O*|0n5KPmOD zX&&{6ClvGJQ>bJR$1-=^trRw+xu4n!{ih$2*VMZLO%C_6#%NOefStmID&RKlrc=;Vs5(n~fZ9ww;8q*8f#BTgS3G<5tP-lsLY{A6B+?6p zLA(bt_o!#J`S^1g_;jn=wQ;B^%_iCS98cPz%|Td_zPg+#zZ5Y z61;@OP@ka=Far@CJYru&)MqhBUW*iw@g^8Ey0Q!1Rgr6HD_;rL(4P-vn2t8caB4QK zB0KU;d>sh?6Tmiy6a8Yx=eL_4R{Zz~bU zGaA1qE5Uw6tWf)WlPyD$qV83KGX^WcG_LoT!qmqzQz?3IEXLS+p9}-g)^ffdcx@$p zSNrwkBK_|o{_nR_o!fkI+KVR;*c79Ipr0Axf78BoWKyN_D8cye%DbSb80>tVDLal@M_Ynsgzd9eYiL_Z_v%TEE?NoW z7fTN9C6MTr^2X6f{!MMYuUt{TcWt5*SEZ>5(SC0oK`K_{r_XwCZFJ0(H%2L;`=T5E0yJ6VeRAVChkR zp|EQZ2BtS-z9*(XXB><$uVTQSqP#xvkr@;mn z#d(5r6XCGd(IM^lI;`aWfiG@8vB|Yx0B8^5LJcp3FP&!BP-@Gg=o->YqMOKBh{u%; z$gG1ppi<-zs3zC<8>A+SSuv65na;#uc3eP<3Y>Gu4<2GsvlB?)F^JUGtY?b0*z+xQ zij&%T*dUZK(Zh}R0PlX<^M~8o!e6rccuiTXS+&oPVmZkgIFwq1!ofR0CtTvC^ovp9 z;8>Os?sB4zz872nq$f;i(;gR+CPk8FTawDPcab+{6jH964iu(eP{+PUD8~cZi91nG z(rqWszs~q3Z4dF}mcaA=0JmSrE2U@$Q3oaHq4M3I$Q*R-Lmw5HTy^r!L#Znuf>%tX zYKw#Ak#16e^4td8R4LYZKYQn)Lkp>uQpfZ9|$TZKAM z7)la&l~hHmDO5#If|mNcvVBX*wI^t~+8lle(zHf2I3F+A`fhqk{o8csLGNfvXy|u2 zQ5Y>SH)4+YQF*bc6GA1eZ8?O(yv8X(1$`I2XqRs!=BEsdBc0jO>m-IrnuoNUBh(V( zV%b1Sqw{-gdJ4sVvko$rZOx!a{aK`#y(O5+gO4S{+~B^;wd;`=^Z8SyKhxwh=+V&_ zqq?#FjM3{Kh#st=KO~r*o#vnGE^HcI*Y-Bvj4N%XU-0%{f1Z+MXKW!nUY@vHN#A)} zC?ipkvv(>>Rxwo}n7?!2R}-WBT8DMz08OQ0?* z29v82{A&$$F_f@wKAB@+d#o{1xbnsEYBHLoVlAo}%6tNPR6_kG0ck%#>7>1GG>#kb z5!KD3dJ$Fon8)$|2mDQ(zmxbkb$&beft(*j{1ZC=68JdIUrT%)@u7a7qCUvdA42fY z^gWc>m4ZunGP&+P-mebxu#?#cyUhrj%EOv@*k=@Wy%Cnq!`|UxZ&6q;o@FjkF!#E? zJj#^;4`WSrqnH3Jdk? zO%!GO6pxa#o<-?LQL2sP0T$Np!!_vQ3-BG|JM)jVF+9z@=V$g=Gu5=G6SI)pMa(9Oc{_omsjhRO`; zn-@`uGWfkT82!Y#K}f6Rajq_z%sZ5MJyrInMdXS#jHf*o4RKvHw@7mB9(?ntSRSTM zLZlRI1bS$vIJkbe%Mme|8cdX1OkxQB2QISOZfh|E6I}@~-_)=7PUv*at@U_Vxh+_VIE7w~u@D6_3Aw ze4m+$x|p1)Ur^NTt9VhhmV1!HJZN1Bk){lV=Xx+1CcG^{9vkwyzD(LC>5!42Jp`wb zRGW};MytfSK-VfOj^U5SQkB4L)0XUKoyc1LQ0ninD^gh4gHl-6qbOx0e8|PaV^AuV zzS~8@H^ENZtrw@c)$>!|BX0$B){hW>04gT`VF?t>MuoqT9fVSM>wJm9*K+<|D2?Jz z;Jm*QaT`ONyG7?_g8LBh;03UEUuXmLfNTWR^8l^8&fNg+8J+tXNZO4Y{>+v&~pdOH{Ua@zZ7 zJbk^LgC65ipH2hKm)ywWg(eCP(Ds!U{WWx9th~qabhPq5_8;PD z1FXPKu#669NASt}a|&C3kq(3bx0zJ3Y&+X|Xuk$q-Pn=pqm4!}$E)Ayi^Y@1avY;s z@Sn(vufyS%4aLfbRt|n-sI5E1*4<3YC2ehO!m7It&5p!_V1sPkt*A$_Fow=vx~q%C za?#h_Y-ftr+GuCUXZ}Md^~z_d`)Cu%z|Na+5;$MFmwG#zQD_*<@N*n_;MSrKVNQNaSJ(EsZP|3mfL|W zW?=ndKx@s#7-)x{XG4_sBT`~>ZVE|Ln_6Gf8oq)k*58Ew5IB~H&S0)E6%=5AUVxi% zy7E#w#y#IqT5G0h2lwD)t1GvgK$?RNJ~Ds8);5J^XkIEly8Ug{C zhEHU%$0G(`4r1Q<11$R1@f3{4$-#VmdkJ6nB(5$;v~oq2Vn?d zIIa;tg{Ji!^@In(vDzxcX7ZwOj?$?0heWD$EY&6liAD!EQSmS&!v=0c334!rU7@|j zlTnhFiX%yUx447zQSdXq6nEh#o7+$)rF0kVisP^#7Fzc>2W#EXrYs9uGY)n&*2Z=s zQ-w{?;_`qd9y%Tmh!g&hn}%^c&W;tli^BQr^){<^+XOcdc|tuHb4ma^EwX#?r-Pi4W57 z-t-_lH8ln{>yvgeHE*N52K=m>A1u-9$Z`Ehk~@N~rrp5Uq*r_OF%DN@l+i zGxqgU(i{Am5?s!B)q%mkoxNw?H-ftO3C%%M3VO^gmGp|R?a>?z)ZrxjvPgPbko3WWhNSTqN|I|^p<1lO4;vcR-pWKL z*hDat`t&pU+Ft94wCpKP?GiMg?#DC#AQXcAl=&3$(0Wx;A+d(XikmK&*NlLJPfv>QB~STdh%*6WBnEGO-k7qhF}ec{ zw&_&8Q0A{I5Mw5~2TwrjzZv1bVGow+**tIj`Du!t`br&3wF9ZbS0k0~KZi2^P9^+& z9+l8P7;>K{xju67)oo$gBqDjpV@7ynVPrictbf)bKAr14%rhvW40d$nYcaOI3wJ^_ zbp5%eBIZkcr92CULPy%<8*6aFXt?&x45oU<(GL1hS7sDB8!7wTp_ikhW2L$?ONOqd zbL~+4`oB{Dq&;Dbmr`JxLs_J!UCN>sp3gkywcaCi!7HiBLj$y~+1oj&1^PGb8J9*%;5@s4A9`>XrOA^Q$Rq`HhpbGw)AU{TV)^M6BYmqhGN)j-YgZ=nl1 zW%j^}#;#O}dI{^_nAyoGr1ag9KBc}f$JqlrNXJ=mv5+jmu~8y&1>YSLjW}N5LISG@ z2|9=tVp6aLNR`nl(3)fu>2HI8qSi*=0BKu#OEtKu5oYUtoXiBY&KZ|1h{uUfW0l-U zV-Jp&zjr8(X8$)e?>du_WQJV(86=FZJ?>kz1)ZHq|05}!3SEstH$v_V9zfH-=Fb*Vppj?BjB;s&sEE_v;a@N;Zv zt98;}7ZN<5Pe>yCXt1zL)Bgz{fuhk+*$)!F*wy0pQA<^SFrKs3AsK$=z zTIXd5RRTxPDeCH9NM6!8I-5!et)yhw9NZf2@T2l~-s%ZMJG(UwO#f3I;zQTSN z8)z~MkXO60ECGBRygje1H>3<^i>Gz?grQq^k<^afI)5o%7@UeFhCi9Tat~GZw_=Sj+<2BBVp) zrPw>x4!}71xsP0n?M;l@U$H8&3bEaoI%Lgj7RuZ@9A*6toyv$tR{oNc<{481sQfsH zCae4uQMVkYE(WmnBg!^)ARJcYJ|Ci*Vk&cVB|pl9(unD+##OP_EVKQT!hYisE%0mS zr6WmB85JM#E~!}@-PXk)*O(LCCXSzU5ygK$8%6b;|MCe-;eV5Q!8{}n#{Zg)-3i)0 z3#Goc6neNh55Eq<{DSfSNoN-ULa8tQ4So+qNZO?crRE_WMw&;idyrJSY&)r%+27r4 zt5wV9WrWGZ98)m|~)F|kYN+f4*bBEr&k|uEIs*w43xYna2eWk5KmvseJ z&}8ihr^DjWzS^%JlH&UPu*4x@*&OcekGte8e^h|AreWBk!pTP^gB)a zLMj}lkVW;OsF66nBQ?Kt_I;xiV$zOjx)HNz>2cCR&AWpod|=$a4TbVB@9Rl;c-Ro3 zqb3BwfVDlW(PAE^bZVHH(QTnV$*{Mo2WVqJ+cA~*k<22j5rdicxVqq~S)05Hhau3; z&^vGLg2p-C+GjOjV*=`P6ULjZ2V>n^kK)Yu3QRJ~fBOyLj+5@B9r}2Pd2cf_*JURu6L%;l6SX?8 zq&LG-o0X&n?KTXkp7dY+U$A&+cdicph@7V}KO~X2aj*tpo$0 z_JU`;0`^OG{&X!<9r?cK7umSe<(kTdScuVw7koqB(B1+XFx6w>$5~o3Io7xmZnjiM zQ3H2jR@4Eo1H2!YP~8*g-C?*nCKk8rl%dfbsMo@Rp+Mf>u+-P}cS6wL89{$tFywgBe}w*Mg>UGu5&DyULYdy- z^}j%m{z6@c7|WPf-Q*5 zuAp0M8e`56fq9`bp2cT)05qbFZ=y{n%Y50+=bwx2(Qb;&0}+gQ1FFHsLp1q|d1#oN z(#6i`hfqNrf9g9fv6{S{+R50RIZW+`-I7F4VVN^}3#^2$Op+--N|OWP#WDBuJBn8r zGws$cbTYK+A%dZPV_91qjKzc+tvL2l^ug3)Ud0$QzVtiln;61S=gZLP%>IpY+g2I& zl94H!>C!#lABH}j+gr3hb@^gVLz0Ti9> z?k;N}q3-k@{-X-^d{nq<+6QP_=Cn-+MJKyMxlzgp94X1m8z|ATZEO-4G1EC%uA7C1 z!g3|LZkN33FolMO-i2bM>}$pKG;vZB^XH2@$`)5Aqvcg}kA}1wSCf$-`43X(R9FeH zw=yB*LkxgZ_C@2~L z+g)9m6^v@kNp3?v?AWZ#nXGNab=VTQB4*(RHa04o^7Gh_Bq!xKlc~WowfA=NIY(Z3 zC7CjO<3zdzF>MDqxo2=Tsy1k)uqL|Vx?OvC8_{RAIgm`7B`Xt=hz%x~o!J7Ce1N|> zb7{oFz{AGxsaSks1e%PaeysVUX{s%Z`EN4sT-g-L7bUR10~i;S(99UAH6b#8 z^2M)`-pX&O>*LoBv;%q$*2C}7-j!B|Z5A5BXmAjwf_M_YY&(r1!c@SvGL=w(YK5B+ za_w32{LwX_P$m5<{{eL5@V9XLnH^prv>r;qNSHC-xOBoZFdqWJ_1WSHxpwR_)( z>`>}(__u6h4bz8$!0-eZC49(F5bP6cr8VF5H1NESeloV06$gzieXh(zoIU%uZKLy5tKXHxIld)=!b!A9!x9t6BN6 zO`DDkkk2r9w4>>SANv`=-AAPnU5WdIX|Ac0wcZ#K_+c?sC}NZT9?&k;ncL$wZ5;ZA zFus!iJQ78@&k)}qJk*G)g;JY2zn1t;Li~?7UrYQ+ z3}S{J8aVGG{>uybBI4f{_)R?i6yl#3_>G*;A^u^3|0m~%6Td*Q92z76x0QQvoX{LhI$D)3u4zn%C`1pYJZLBbB- zApR|Z{|Ar%4DnBaH}wB3=hqVdpb&pO=WB_t5cuag?<0P?z`w-#BH~91yq@nA;;#{S zJ$?@HvcSL0yIa75IH2rk|hS5o?bMe2~jI zO#D)TS2_PV@pA;ej`Q1zpD6GF&c8wY%@^{|5Z@oXq5m2le=YHyg!m6|zLxltm?R8) zSk8GL@m~u3gPboS{(XVh%Rhzq=LP;@9zTcphXwv2&JQPkfxxfeyp#BRf!FoWhxoAq zuh%P@_!|UX*UwLIjfIh-Fp419&aXL8<2{B(gIzz&9D`Vc=-;4kO#qlv#p;Cpla zrxftA!2hY7+2>*6&%$Rh^l;3}_|J(yAn@ODemn6^0>7X0ZxH{gz<))>g=f_QEjzuD*OogZM z7)tr}003>vyQo+&=B-

w%y;1i3a*L>58!KPFGr)>6#C_C&U>XH_w7tM#FEJ(wBn zxmVop!G>p0+x`~0yZn8_$YCmSpiJ-}t60xK$x!GrPN-hj$=1jdcSd$4DK)VbQ8cewKeV0un5 zs87N+?J|_q^falxD1YSmcQ|BpBE}xlO4gsS7)*KI<3O&ug5sAWL%jsZEG}0o<(ZLi z{l3}7>XS{~AMjA}F=iNiU&8E#HA-MGLFildu*g zh^G#)6#SOHV*!cVGI1_;IG3{P9@G*v_Z;tmW=`BAY5m@4Ym4Xt&M_9XX=ElwlN>-3 z1mpXo4)lyuqJH008?(8s?H-4R&Wn&5F@7IvD3PmGJ>Z%|n+|E(<%miWDIR&OF%57m z@96*sc>qfT%ad{;p)2yB6oa+!+q_KJQlL^I?9qsHi1<-nlKBV-e5O8$Pq}aA!|WsV z1^P(BO$#~6hgGmgE^k1jp^t;k!IMXKuvkOaFo>0KG6Dy!1;S^)XDBeK3~S`RgI2SM z{U{xR;$0}2oK%FBbKm%(_ztf7X#N=!V#)sItU+1jzTH<*)V?v#vlMIf<@H0(brg!_ zGA*y~=|wi3ma9dfnxvTbJA>8MV%3HlY42eLhekaDZ-I=fE9-7}KIJs%P^yG6nQh); z8PYoo<1iL%WCmuR=z`8nmV(Z#IncKJzGUDS?pH+~5u_###su^%mar_U>Do^fU8+h( zL+OUHiyr1lhBRa(O=5w7Uu%Ryw#JjChtdU$Yin= zdhp3_*-FCF1|eyvK_51<`V*lO#k>!$1UtFaD6JUEV%FQ*tR=mU4r1&F=>7e|`aO*a zpH%q&Mg5juhx&CC>sQAc@Bc;p`nRuNu#*ebTd~Qg-ly7auUY&5MaR=^a1^M1ce`<| zIhY1L;tJ7?O5qu_$0yh7OVLDXJ9W5IA5iy12GlwXGI>m)xD}3;G4d=ogR8 z?$_S>7mfBc5C-FKcR!Zj2fRX`6lXdWJW89yN`_JBB;_8woR3KOPD4l<%!@{X^C)Hl ziiyjyu1z%2(cl1cOQTFM%-~)_8r^M=b4|wvw=)gTpohww!(CH7p(1CNYf4+{p~0BJ zf5+gD0(0|%vGpKhOQSis?k`LbvPnu}+JIS4H?qP&cYr_4b5DQmZCwVEcQKOt^VtPM z8h&`WvZ!3z`Il*%$;YB)ku~Sy@Nfm)RjnOKZqYW{NrWvLbL(i7EwBA!iMk%O$&`Q< z7Ejx*Ky$RR^Ez#W&SycIR!;nj+RYo<>wg+u$+^(vH^8?elNjPwaZFEI@D0FyZ?}p& zy?FkTp7EkVT$_sy#P&^5d}+kGUu z_05QVQ+hnErDt)wo}NmD2QEotY!AJ!E+m%9haDa4t8+=Dkh)1INjM+B;My_5MMMsM zQTY%dh42{s(&oKFq-B2xjfnwJ7@y&!#C!`+4vmRJOHwzyF5^BoYN22g>16_gWu*4>9JvuFnVS-XP1(9KCiFGYY&{3#KU>~7AU*c}=Y8)HH;&8}!i6>q1 zpVoedY4CA2fuAR7tu)%2{uMWu!RB~DURc^TZ7nnu@fVjJ+lxZD@un6Wp`xFoDnk8g zoiszyfYstFRsy$3zI?a(^V^qDxzX)osX{dO<61bM-sn57Gf|_Yxz&TzBv~GHHgsXkQq}U5pf^shFRvZ7*M+s~jM`D@XKd4cdlA)(yj{Tj78&1h z94>F{BZs`rIL_|8;L1==T(c7Rxr6T-da!=$!Du0L{-KcfeME~{5Qk{HD4PE({@g^! z`xFb15%7=JEs_3Zecyr-9D`2Y(XF1Q_sfhAT|uW@v96jC199MuzSzlq-7vT6jYB%V zKa7L;(-^Ah8lw6u7HffHiQ|K};T=%ByrXG9zFivFVj{o%z@9k7`h?yQps3BBB)q6( zXVs?)okKF}Q%}-X2&vkF8)_K?eVsC9`j51Jx|*mLk%0AkH!j~L?PY@-Za(p+_i+)x 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 c%f)t-|h$k6{D++lKi&YZ+#F?6K( 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 literal 0 HcmV?d00001 diff --git a/build/atoms.d b/build/atoms.d new file mode 100644 index 0000000..3c1b1a1 --- /dev/null +++ b/build/atoms.d @@ -0,0 +1,4 @@ +build/atoms.o: src/atoms.c include/atoms.h include/dwn.h include/util.h +include/atoms.h: +include/dwn.h: +include/util.h: diff --git a/build/atoms.o b/build/atoms.o new file mode 100644 index 0000000000000000000000000000000000000000..bfcc6095b34a85758d0ff834546b4ed0b8a745ab GIT binary patch literal 22088 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 AH2?qr literal 0 HcmV?d00001 diff --git a/build/cJSON.d b/build/cJSON.d new file mode 100644 index 0000000..68522b3 --- /dev/null +++ b/build/cJSON.d @@ -0,0 +1,2 @@ +build/cJSON.o: src/cJSON.c include/cJSON.h +include/cJSON.h: diff --git a/build/cJSON.o b/build/cJSON.o new file mode 100644 index 0000000000000000000000000000000000000000..f835fcc87d37ca4aa1c967f34b89e80c659ea2f3 GIT binary patch literal 36808 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 Y_0wqoO+iMn@}af5#OF*wzIy%tpZ+O+zyJUM literal 0 HcmV?d00001 diff --git a/build/config.d b/build/config.d new file mode 100644 index 0000000..b243385 --- /dev/null +++ b/build/config.d @@ -0,0 +1,5 @@ +build/config.o: src/config.c include/config.h include/dwn.h \ + include/util.h +include/config.h: +include/dwn.h: +include/util.h: diff --git a/build/config.o b/build/config.o new file mode 100644 index 0000000000000000000000000000000000000000..4d7415e358c19b17273cdc033ff2d1a939e3672b GIT binary patch literal 13568 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 literal 0 HcmV?d00001 diff --git a/build/decorations.d b/build/decorations.d new file mode 100644 index 0000000..cd162ed --- /dev/null +++ b/build/decorations.d @@ -0,0 +1,7 @@ +build/decorations.o: src/decorations.c include/decorations.h \ + include/dwn.h include/client.h include/config.h include/util.h +include/decorations.h: +include/dwn.h: +include/client.h: +include/config.h: +include/util.h: diff --git a/build/decorations.o b/build/decorations.o new file mode 100644 index 0000000000000000000000000000000000000000..9e3a5b6337d0c1ac98f3fa4672af3450eb21dc44 GIT binary patch literal 9576 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 XeVJB%sGBMLkxe?lZz&uf%<}&~k*dD1 literal 0 HcmV?d00001 diff --git a/build/layout.d b/build/layout.d new file mode 100644 index 0000000..6bd55f2 --- /dev/null +++ b/build/layout.d @@ -0,0 +1,8 @@ +build/layout.o: src/layout.c include/layout.h include/dwn.h \ + include/client.h include/workspace.h include/config.h include/util.h +include/layout.h: +include/dwn.h: +include/client.h: +include/workspace.h: +include/config.h: +include/util.h: diff --git a/build/layout.o b/build/layout.o new file mode 100644 index 0000000000000000000000000000000000000000..f1dda5a054afdf4054e42ed7a8d59e934927c0ad GIT binary patch literal 6008 zcmb_feQX>@6`%Fl$<=mtk8&wTk|rl~RCkSYoGYXFqBQZ^>tv(aRZ8pvN}bdByta>= z@66p^^U(k)XGdDCuQ4D&`S>G3Ao@>Ls8ETJP|tFS0|nujl1M?2(4x>ZDIpCiPKsmR zo1L-88(XR>p5&W1zj^cKH*em|TYs&+V^@_V5iSzhK^A))CFJnO7xfmZwh$jt$Sq~# z9li7@MU;`3pjEr~SaWLE9_!!Ty7i}hy7jtVy6$}&EvyOMu0ch&$M<@m8J#hADX+Xd zRz-;N$~Vi zqY0mHAnfn_CTl7AJzIbYtOLuwR^%?Y`FRh2+X(;G(i=Z zuMoe*SpJc37veev`|`Jy%`L?4fcRRM+RFZrdioJqDe|w#P1_84HBin)rhkc8{G8J9 zTL^c(CjC>Bu4vLbn)IF~y{W}3n$lUJ@c5}c>H#N)$|f;2=zVc6 z0S&3mv&l59x%S4tUt71-esNKAE9TA(VUKSK`US4D3EL&L^aiF~@i&@vnVu-+3rp

-Z9;p#I$X|v<*`ue#ESuqOElU zE)T!{=;ce`#cm6e&z8At0T0bz485a>2I)9eYtw8X1$I#R)YcGwJq$05RytzJM9Pij6`_~yBOVe&;4#*yx*OWpStsMst44>=gR7me3|)I^zB={bdH{A* zPlxrt4j_o_h{>~oKmCmHI17uew zmo!o7%w=<_jDbWys_brS+ZGIW_ZG8eG1&Oo=p)hCmZl=@Hboc!zN_KS2Y_X56wjZIRSuLrt#WXzT9v~mYE(H^TBXT*Jt3)9jsXPjz{DY> z)-}O=nnTdq=&N(-x>bX&h zW;$c^B^;}TDDs`dAQqiNx%|Pxa580JsQ7ed!@C$nJPEPy90r^*%4(;oD_}2ZG;rQ8s!tFW!t^?nL$01)((9at90=^cBfS`ZT z&pHAB7>x0CKPBMz3;4qVj{nkf`zD5CgYn_`5y2j7F2_d&96wVz{tW>aE=i+tV^@J|YSyhsGZ9p^OyF2;GSfQxZ{P{74F zhXovK5s%vehP(HBQLq>Hdqu#-{k|vQ;(ou*N*E4{@xLqJV*DRu?;hHV@j1kBVX{ZmJ!|PdyjdMS|o|!J3*Db6|DBPac zt)IJaUbjdToB{G@gXWFVM(CmuML>hW zXeO6RW=J%Z8ybRgM55W8X++@@eG4r-tUr(~Mu+pcVIyxIVLpt(M1LNi=>LlZ!3%Dg zrO|@V7N(tX4Fne~9A!N;nA;p_8!<~U!Ux9u>1x_kX*#=n_QWG%mE9Bu(+x%iFa z;|MbDG_)4tQMlvx1;B89H_u_VKE_uR&YQPM805NW_w5fE|1ZNNG{t))%JXb~Py|8e z{5foag6q5IpJwwbE^_YqBQSTl_yw_ufK=$O^DTkjC44^5zvb4(DTndn0OT|yhTj$l jx#uJ1K6`*jj6Jw7yZF1h1dn;S`Ij00Z=o|m-1Gkj0UOz9 literal 0 HcmV?d00001 diff --git a/build/main.d b/build/main.d new file mode 100644 index 0000000..1fdc703 --- /dev/null +++ b/build/main.d @@ -0,0 +1,56 @@ +build/main.o: src/main.c include/dwn.h include/config.h include/dwn.h \ + include/atoms.h include/client.h include/workspace.h include/layout.h \ + include/decorations.h include/panel.h include/keys.h \ + include/notifications.h /usr/include/dbus-1.0/dbus/dbus.h \ + /usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h \ + /usr/include/dbus-1.0/dbus/dbus-macros.h \ + /usr/include/dbus-1.0/dbus/dbus-address.h \ + /usr/include/dbus-1.0/dbus/dbus-types.h \ + /usr/include/dbus-1.0/dbus/dbus-errors.h \ + /usr/include/dbus-1.0/dbus/dbus-protocol.h \ + /usr/include/dbus-1.0/dbus/dbus-bus.h \ + /usr/include/dbus-1.0/dbus/dbus-connection.h \ + /usr/include/dbus-1.0/dbus/dbus-memory.h \ + /usr/include/dbus-1.0/dbus/dbus-message.h \ + /usr/include/dbus-1.0/dbus/dbus-shared.h \ + /usr/include/dbus-1.0/dbus/dbus-misc.h \ + /usr/include/dbus-1.0/dbus/dbus-pending-call.h \ + /usr/include/dbus-1.0/dbus/dbus-server.h \ + /usr/include/dbus-1.0/dbus/dbus-signature.h \ + /usr/include/dbus-1.0/dbus/dbus-syntax.h \ + /usr/include/dbus-1.0/dbus/dbus-threads.h include/systray.h \ + include/news.h include/applauncher.h include/ai.h include/util.h +include/dwn.h: +include/config.h: +include/dwn.h: +include/atoms.h: +include/client.h: +include/workspace.h: +include/layout.h: +include/decorations.h: +include/panel.h: +include/keys.h: +include/notifications.h: +/usr/include/dbus-1.0/dbus/dbus.h: +/usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h: +/usr/include/dbus-1.0/dbus/dbus-macros.h: +/usr/include/dbus-1.0/dbus/dbus-address.h: +/usr/include/dbus-1.0/dbus/dbus-types.h: +/usr/include/dbus-1.0/dbus/dbus-errors.h: +/usr/include/dbus-1.0/dbus/dbus-protocol.h: +/usr/include/dbus-1.0/dbus/dbus-bus.h: +/usr/include/dbus-1.0/dbus/dbus-connection.h: +/usr/include/dbus-1.0/dbus/dbus-memory.h: +/usr/include/dbus-1.0/dbus/dbus-message.h: +/usr/include/dbus-1.0/dbus/dbus-shared.h: +/usr/include/dbus-1.0/dbus/dbus-misc.h: +/usr/include/dbus-1.0/dbus/dbus-pending-call.h: +/usr/include/dbus-1.0/dbus/dbus-server.h: +/usr/include/dbus-1.0/dbus/dbus-signature.h: +/usr/include/dbus-1.0/dbus/dbus-syntax.h: +/usr/include/dbus-1.0/dbus/dbus-threads.h: +include/systray.h: +include/news.h: +include/applauncher.h: +include/ai.h: +include/util.h: diff --git a/build/main.o b/build/main.o new file mode 100644 index 0000000000000000000000000000000000000000..939e7bc185008a15ccd2c88cbfbc308f68201043 GIT binary patch literal 26312 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 Z`>uYS_9MCF${#W85m(6Z2t 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 literal 0 HcmV?d00001 diff --git a/build/systray.d b/build/systray.d new file mode 100644 index 0000000..99b72c6 --- /dev/null +++ b/build/systray.d @@ -0,0 +1,44 @@ +build/systray.o: src/systray.c include/systray.h include/dwn.h \ + include/panel.h include/config.h include/util.h include/notifications.h \ + /usr/include/dbus-1.0/dbus/dbus.h \ + /usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h \ + /usr/include/dbus-1.0/dbus/dbus-macros.h \ + /usr/include/dbus-1.0/dbus/dbus-address.h \ + /usr/include/dbus-1.0/dbus/dbus-types.h \ + /usr/include/dbus-1.0/dbus/dbus-errors.h \ + /usr/include/dbus-1.0/dbus/dbus-protocol.h \ + /usr/include/dbus-1.0/dbus/dbus-bus.h \ + /usr/include/dbus-1.0/dbus/dbus-connection.h \ + /usr/include/dbus-1.0/dbus/dbus-memory.h \ + /usr/include/dbus-1.0/dbus/dbus-message.h \ + /usr/include/dbus-1.0/dbus/dbus-shared.h \ + /usr/include/dbus-1.0/dbus/dbus-misc.h \ + /usr/include/dbus-1.0/dbus/dbus-pending-call.h \ + /usr/include/dbus-1.0/dbus/dbus-server.h \ + /usr/include/dbus-1.0/dbus/dbus-signature.h \ + /usr/include/dbus-1.0/dbus/dbus-syntax.h \ + /usr/include/dbus-1.0/dbus/dbus-threads.h +include/systray.h: +include/dwn.h: +include/panel.h: +include/config.h: +include/util.h: +include/notifications.h: +/usr/include/dbus-1.0/dbus/dbus.h: +/usr/lib/x86_64-linux-gnu/dbus-1.0/include/dbus/dbus-arch-deps.h: +/usr/include/dbus-1.0/dbus/dbus-macros.h: +/usr/include/dbus-1.0/dbus/dbus-address.h: +/usr/include/dbus-1.0/dbus/dbus-types.h: +/usr/include/dbus-1.0/dbus/dbus-errors.h: +/usr/include/dbus-1.0/dbus/dbus-protocol.h: +/usr/include/dbus-1.0/dbus/dbus-bus.h: +/usr/include/dbus-1.0/dbus/dbus-connection.h: +/usr/include/dbus-1.0/dbus/dbus-memory.h: +/usr/include/dbus-1.0/dbus/dbus-message.h: +/usr/include/dbus-1.0/dbus/dbus-shared.h: +/usr/include/dbus-1.0/dbus/dbus-misc.h: +/usr/include/dbus-1.0/dbus/dbus-pending-call.h: +/usr/include/dbus-1.0/dbus/dbus-server.h: +/usr/include/dbus-1.0/dbus/dbus-signature.h: +/usr/include/dbus-1.0/dbus/dbus-syntax.h: +/usr/include/dbus-1.0/dbus/dbus-threads.h: diff --git a/build/systray.o b/build/systray.o new file mode 100644 index 0000000000000000000000000000000000000000..12fc721e9970548bb2813d5076cb7c1b7c70edee GIT binary patch literal 32904 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 literal 0 HcmV?d00001 diff --git a/build/util.d b/build/util.d new file mode 100644 index 0000000..2702406 --- /dev/null +++ b/build/util.d @@ -0,0 +1,3 @@ +build/util.o: src/util.c include/util.h include/dwn.h +include/util.h: +include/dwn.h: diff --git a/build/util.o b/build/util.o new file mode 100644 index 0000000000000000000000000000000000000000..5f6514e27609d104a65eac2950c6958956c77adc GIT binary patch literal 19952 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 literal 0 HcmV?d00001 diff --git a/build/workspace.d b/build/workspace.d new file mode 100644 index 0000000..bf10441 --- /dev/null +++ b/build/workspace.d @@ -0,0 +1,10 @@ +build/workspace.o: src/workspace.c include/workspace.h include/dwn.h \ + include/client.h include/layout.h include/atoms.h include/util.h \ + include/config.h +include/workspace.h: +include/dwn.h: +include/client.h: +include/layout.h: +include/atoms.h: +include/util.h: +include/config.h: diff --git a/build/workspace.o b/build/workspace.o new file mode 100644 index 0000000000000000000000000000000000000000..cf10d4b891b9857763d5475512d4d15f3fcc8035 GIT binary patch literal 13288 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 + +/* AI request states */ +typedef enum { + AI_STATE_IDLE, + AI_STATE_PENDING, + AI_STATE_COMPLETED, + AI_STATE_ERROR +} AIState; + +/* AI request structure */ +typedef struct AIRequest { + char *prompt; + char *response; + AIState state; + void (*callback)(struct AIRequest *req); + void *user_data; + struct AIRequest *next; +} AIRequest; + +/* AI context for window analysis */ +typedef struct { + char focused_window[256]; + char focused_class[64]; + char workspace_windows[1024]; + int window_count; +} AIContext; + +/* Initialization */ +bool ai_init(void); +void ai_cleanup(void); +bool ai_is_available(void); + +/* API calls */ +AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *)); +void ai_cancel_request(AIRequest *req); +void ai_process_pending(void); + +/* Context analysis */ +void ai_update_context(void); +const char *ai_analyze_task(void); +const char *ai_suggest_window(void); +const char *ai_suggest_app(void); + +/* Command palette */ +void ai_show_command_palette(void); +void ai_execute_command(const char *command); + +/* Smart features */ +void ai_auto_organize_workspace(void); +void ai_suggest_layout(void); +void ai_analyze_workflow(void); + +/* Notification intelligence */ +bool ai_should_show_notification(const char *app, const char *summary); +int ai_notification_priority(const char *app, const char *summary); + +/* Performance monitoring */ +void ai_monitor_performance(void); +const char *ai_performance_suggestion(void); + +/* Exa semantic search */ +typedef struct { + char title[256]; + char url[512]; + char snippet[1024]; + float score; +} ExaSearchResult; + +typedef struct ExaRequest { + char *query; + ExaSearchResult results[10]; + int result_count; + int state; /* Uses AIState enum */ + void (*callback)(struct ExaRequest *req); + void *user_data; + struct ExaRequest *next; +} ExaRequest; + +bool exa_is_available(void); +ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *)); +void exa_process_pending(void); +void exa_show_app_launcher(void); + +#endif /* DWN_AI_H */ diff --git a/include/applauncher.h b/include/applauncher.h new file mode 100644 index 0000000..1ef5f2a --- /dev/null +++ b/include/applauncher.h @@ -0,0 +1,48 @@ +/* + * DWN - Desktop Window Manager + * Application launcher with .desktop file support + */ + +#ifndef DWN_APPLAUNCHER_H +#define DWN_APPLAUNCHER_H + +#include + +/* Maximum applications to track */ +#define MAX_APPS 512 +#define MAX_RECENT_APPS 10 + +/* Application entry from .desktop file */ +typedef struct { + char name[128]; /* Display name */ + char exec[512]; /* Command to execute */ + char icon[128]; /* Icon name (unused for now) */ + char desktop_id[256]; /* Desktop file basename for tracking */ + bool terminal; /* Run in terminal */ + bool hidden; /* Should be hidden */ +} AppEntry; + +/* Application launcher state */ +typedef struct { + AppEntry apps[MAX_APPS]; + int app_count; + char recent[MAX_RECENT_APPS][256]; /* Desktop IDs of recent apps */ + int recent_count; +} AppLauncherState; + +/* Initialize the app launcher (scans .desktop files) */ +void applauncher_init(void); + +/* Cleanup resources */ +void applauncher_cleanup(void); + +/* Rescan .desktop files */ +void applauncher_refresh(void); + +/* Show the application launcher (dmenu-based) */ +void applauncher_show(void); + +/* Launch an application by desktop_id */ +void applauncher_launch(const char *desktop_id); + +#endif /* DWN_APPLAUNCHER_H */ diff --git a/include/atoms.h b/include/atoms.h new file mode 100644 index 0000000..4bd88fd --- /dev/null +++ b/include/atoms.h @@ -0,0 +1,148 @@ +/* + * DWN - Desktop Window Manager + * X11 Atoms management (EWMH/ICCCM compliance) + */ + +#ifndef DWN_ATOMS_H +#define DWN_ATOMS_H + +#include +#include + +/* EWMH (Extended Window Manager Hints) atoms */ +typedef struct { + /* Root window properties */ + Atom NET_SUPPORTED; + Atom NET_SUPPORTING_WM_CHECK; + Atom NET_CLIENT_LIST; + Atom NET_CLIENT_LIST_STACKING; + Atom NET_NUMBER_OF_DESKTOPS; + Atom NET_DESKTOP_GEOMETRY; + Atom NET_DESKTOP_VIEWPORT; + Atom NET_CURRENT_DESKTOP; + Atom NET_DESKTOP_NAMES; + Atom NET_ACTIVE_WINDOW; + Atom NET_WORKAREA; + + /* Client window properties */ + Atom NET_WM_NAME; + Atom NET_WM_VISIBLE_NAME; + Atom NET_WM_DESKTOP; + Atom NET_WM_WINDOW_TYPE; + Atom NET_WM_STATE; + Atom NET_WM_ALLOWED_ACTIONS; + Atom NET_WM_STRUT; + Atom NET_WM_STRUT_PARTIAL; + Atom NET_WM_PID; + + /* Window types */ + Atom NET_WM_WINDOW_TYPE_DESKTOP; + Atom NET_WM_WINDOW_TYPE_DOCK; + Atom NET_WM_WINDOW_TYPE_TOOLBAR; + Atom NET_WM_WINDOW_TYPE_MENU; + Atom NET_WM_WINDOW_TYPE_UTILITY; + Atom NET_WM_WINDOW_TYPE_SPLASH; + Atom NET_WM_WINDOW_TYPE_DIALOG; + Atom NET_WM_WINDOW_TYPE_NORMAL; + Atom NET_WM_WINDOW_TYPE_NOTIFICATION; + + /* Window states */ + Atom NET_WM_STATE_MODAL; + Atom NET_WM_STATE_STICKY; + Atom NET_WM_STATE_MAXIMIZED_VERT; + Atom NET_WM_STATE_MAXIMIZED_HORZ; + Atom NET_WM_STATE_SHADED; + Atom NET_WM_STATE_SKIP_TASKBAR; + Atom NET_WM_STATE_SKIP_PAGER; + Atom NET_WM_STATE_HIDDEN; + Atom NET_WM_STATE_FULLSCREEN; + Atom NET_WM_STATE_ABOVE; + Atom NET_WM_STATE_BELOW; + Atom NET_WM_STATE_DEMANDS_ATTENTION; + Atom NET_WM_STATE_FOCUSED; + + /* Actions */ + Atom NET_WM_ACTION_MOVE; + Atom NET_WM_ACTION_RESIZE; + Atom NET_WM_ACTION_MINIMIZE; + Atom NET_WM_ACTION_SHADE; + Atom NET_WM_ACTION_STICK; + Atom NET_WM_ACTION_MAXIMIZE_HORZ; + Atom NET_WM_ACTION_MAXIMIZE_VERT; + Atom NET_WM_ACTION_FULLSCREEN; + Atom NET_WM_ACTION_CHANGE_DESKTOP; + Atom NET_WM_ACTION_CLOSE; + + /* Client messages */ + Atom NET_CLOSE_WINDOW; + Atom NET_MOVERESIZE_WINDOW; + Atom NET_WM_MOVERESIZE; + Atom NET_REQUEST_FRAME_EXTENTS; + Atom NET_FRAME_EXTENTS; + + /* System tray */ + Atom NET_SYSTEM_TRAY_OPCODE; + Atom NET_SYSTEM_TRAY_S0; + Atom MANAGER; + Atom XEMBED; + Atom XEMBED_INFO; +} EWMHAtoms; + +/* ICCCM (Inter-Client Communication Conventions Manual) atoms */ +typedef struct { + Atom WM_PROTOCOLS; + Atom WM_DELETE_WINDOW; + Atom WM_TAKE_FOCUS; + Atom WM_STATE; + Atom WM_CHANGE_STATE; + Atom WM_CLASS; + Atom WM_NAME; + Atom WM_TRANSIENT_FOR; + Atom WM_CLIENT_LEADER; + Atom WM_WINDOW_ROLE; +} ICCCMAtoms; + +/* Other useful atoms */ +typedef struct { + Atom UTF8_STRING; + Atom COMPOUND_TEXT; + Atom MOTIF_WM_HINTS; + Atom CLIPBOARD; + Atom PRIMARY; + Atom DWN_RESTART; +} MiscAtoms; + +/* Global atom containers */ +extern EWMHAtoms ewmh; +extern ICCCMAtoms icccm; +extern MiscAtoms misc_atoms; + +/* Initialization */ +void atoms_init(Display *display); + +/* EWMH root window setup */ +void atoms_setup_ewmh(void); +void atoms_update_client_list(void); +void atoms_update_desktop_names(void); +void atoms_set_current_desktop(int desktop); +void atoms_set_active_window(Window window); +void atoms_set_number_of_desktops(int count); + +/* Window property helpers */ +bool atoms_get_window_type(Window window, Atom *type); +bool atoms_get_window_state(Window window, Atom **states, int *count); +bool atoms_set_window_state(Window window, Atom *states, int count); +bool atoms_get_window_desktop(Window window, int *desktop); +bool atoms_set_window_desktop(Window window, int desktop); +char *atoms_get_window_name(Window window); +bool atoms_get_wm_class(Window window, char *class_name, char *instance_name, size_t len); + +/* Protocol helpers */ +bool atoms_window_supports_protocol(Window window, Atom protocol); +void atoms_send_protocol(Window window, Atom protocol, Time timestamp); + +/* Client message sending */ +void atoms_send_client_message(Window window, Atom message_type, + long data0, long data1, long data2, long data3, long data4); + +#endif /* DWN_ATOMS_H */ diff --git a/include/cJSON.h b/include/cJSON.h new file mode 100644 index 0000000..cab5feb --- /dev/null +++ b/include/cJSON.h @@ -0,0 +1,306 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef cJSON__h +#define cJSON__h + +#ifdef __cplusplus +extern "C" +{ +#endif + +#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32)) +#define __WINDOWS__ +#endif + +#ifdef __WINDOWS__ + +/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options: + +CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols +CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default) +CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol + +For *nix builds that support visibility attribute, you can define similar behavior by + +setting default visibility to hidden by adding +-fvisibility=hidden (for gcc) +or +-xldscope=hidden (for sun cc) +to CFLAGS + +then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does + +*/ + +#define CJSON_CDECL __cdecl +#define CJSON_STDCALL __stdcall + +/* export symbols by default, this is necessary for copy pasting the C and header file */ +#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_EXPORT_SYMBOLS +#endif + +#if defined(CJSON_HIDE_SYMBOLS) +#define CJSON_PUBLIC(type) type CJSON_STDCALL +#elif defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL +#elif defined(CJSON_IMPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL +#endif +#else /* !__WINDOWS__ */ +#define CJSON_CDECL +#define CJSON_STDCALL + +#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY) +#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type +#else +#define CJSON_PUBLIC(type) type +#endif +#endif + +/* project version */ +#define CJSON_VERSION_MAJOR 1 +#define CJSON_VERSION_MINOR 7 +#define CJSON_VERSION_PATCH 19 + +#include + +/* cJSON Types: */ +#define cJSON_Invalid (0) +#define cJSON_False (1 << 0) +#define cJSON_True (1 << 1) +#define cJSON_NULL (1 << 2) +#define cJSON_Number (1 << 3) +#define cJSON_String (1 << 4) +#define cJSON_Array (1 << 5) +#define cJSON_Object (1 << 6) +#define cJSON_Raw (1 << 7) /* raw json */ + +#define cJSON_IsReference 256 +#define cJSON_StringIsConst 512 + +/* The cJSON structure: */ +typedef struct cJSON +{ + /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */ + struct cJSON *next; + struct cJSON *prev; + /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */ + struct cJSON *child; + + /* The type of the item, as above. */ + int type; + + /* The item's string, if type==cJSON_String and type == cJSON_Raw */ + char *valuestring; + /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */ + int valueint; + /* The item's number, if type==cJSON_Number */ + double valuedouble; + + /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ + char *string; +} cJSON; + +typedef struct cJSON_Hooks +{ + /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */ + void *(CJSON_CDECL *malloc_fn)(size_t sz); + void (CJSON_CDECL *free_fn)(void *ptr); +} cJSON_Hooks; + +typedef int cJSON_bool; + +/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them. + * This is to prevent stack overflows. */ +#ifndef CJSON_NESTING_LIMIT +#define CJSON_NESTING_LIMIT 1000 +#endif + +/* Limits the length of circular references can be before cJSON rejects to parse them. + * This is to prevent stack overflows. */ +#ifndef CJSON_CIRCULAR_LIMIT +#define CJSON_CIRCULAR_LIMIT 10000 +#endif + +/* returns the version of cJSON as a string */ +CJSON_PUBLIC(const char*) cJSON_Version(void); + +/* Supply malloc, realloc and free functions to cJSON */ +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks); + +/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */ +/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length); +/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ +/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated); + +/* Render a cJSON entity to text for transfer/storage. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item); +/* Render a cJSON entity to text for transfer/storage without any formatting. */ +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item); +/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */ +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt); +/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */ +/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */ +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format); +/* Delete a cJSON entity and all subentities. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item); + +/* Returns the number of items in an array (or object). */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array); +/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */ +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index); +/* Get item "string" from object. Case insensitive. */ +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string); +/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */ +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void); + +/* Check item type and return its value */ +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item); +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item); + +/* These functions check the type of an item */ +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item); + +/* These calls create a cJSON item of the appropriate type. */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean); +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num); +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string); +/* raw json */ +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw); +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void); + +/* Create a string where valuestring references a string so + * it will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string); +/* Create an object/array that only references it's elements so + * they will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child); +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child); + +/* These utilities create an Array of count items. + * The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count); + +/* Append item to the specified array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item); +/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object. + * WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before + * writing to `item->string` */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item); +/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item); + +/* Remove/Detach items from Arrays/Objects. */ +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string); + +/* Update array items. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */ +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem); + +/* Duplicate a cJSON item */ +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse); +/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will + * need to be released. With recurse!=0, it will duplicate any children connected to the item. + * The item->next and ->prev pointers are always zero on return from Duplicate. */ +/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal. + * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */ +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive); + +/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings. + * The input pointer json cannot point to a read-only address area, such as a string constant, + * but should point to a readable and writable address area. */ +CJSON_PUBLIC(void) cJSON_Minify(char *json); + +/* Helper functions for creating and adding items to an object at the same time. + * They return the added item or NULL on failure. */ +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean); +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number); +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string); +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw); +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name); + +/* When assigning an integer value, it needs to be propagated to valuedouble too. */ +#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number)) +/* helper for the cJSON_SetNumberValue macro */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number); +#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number)) +/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring); + +/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/ +#define cJSON_SetBoolValue(object, boolValue) ( \ + (object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \ + (object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \ + cJSON_Invalid\ +) + +/* Macro for iterating over an array or object */ +#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next) + +/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */ +CJSON_PUBLIC(void *) cJSON_malloc(size_t size); +CJSON_PUBLIC(void) cJSON_free(void *object); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/client.h b/include/client.h new file mode 100644 index 0000000..bb095f2 --- /dev/null +++ b/include/client.h @@ -0,0 +1,80 @@ +/* + * DWN - Desktop Window Manager + * Client (window) management + */ + +#ifndef DWN_CLIENT_H +#define DWN_CLIENT_H + +#include "dwn.h" +#include + +/* Client creation and destruction */ +Client *client_create(Window window); +void client_destroy(Client *client); + +/* Client management */ +Client *client_manage(Window window); +void client_unmanage(Client *client); +Client *client_find_by_window(Window window); +Client *client_find_by_frame(Window frame); + +/* Client state */ +void client_focus(Client *client); +void client_unfocus(Client *client); +void client_raise(Client *client); +void client_lower(Client *client); +void client_minimize(Client *client); +void client_restore(Client *client); + +/* Client geometry */ +void client_move(Client *client, int x, int y); +void client_resize(Client *client, int width, int height); +void client_move_resize(Client *client, int x, int y, int width, int height); +void client_configure(Client *client); +void client_apply_size_hints(Client *client, int *width, int *height); + +/* Client properties */ +void client_update_title(Client *client); +void client_update_class(Client *client); +void client_set_fullscreen(Client *client, bool fullscreen); +void client_toggle_fullscreen(Client *client); +void client_set_floating(Client *client, bool floating); +void client_toggle_floating(Client *client); + +/* Window type checking */ +bool client_is_floating(Client *client); +bool client_is_fullscreen(Client *client); +bool client_is_minimized(Client *client); +bool client_is_dialog(Window window); +bool client_is_dock(Window window); +bool client_is_desktop(Window window); + +/* Frame management */ +void client_create_frame(Client *client); +void client_destroy_frame(Client *client); +void client_reparent_to_frame(Client *client); +void client_reparent_from_frame(Client *client); + +/* Visibility */ +void client_show(Client *client); +void client_hide(Client *client); +bool client_is_visible(Client *client); + +/* Close handling */ +void client_close(Client *client); +void client_kill(Client *client); + +/* List operations */ +void client_add_to_list(Client *client); +void client_remove_from_list(Client *client); +int client_count(void); +int client_count_on_workspace(int workspace); + +/* Iteration */ +Client *client_get_next(Client *client); +Client *client_get_prev(Client *client); +Client *client_get_first(void); +Client *client_get_last(void); + +#endif /* DWN_CLIENT_H */ diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..507644f --- /dev/null +++ b/include/config.h @@ -0,0 +1,87 @@ +/* + * DWN - Desktop Window Manager + * Configuration system + */ + +#ifndef DWN_CONFIG_H +#define DWN_CONFIG_H + +#include "dwn.h" +#include + +/* Color configuration */ +typedef struct { + unsigned long panel_bg; + unsigned long panel_fg; + unsigned long workspace_active; + unsigned long workspace_inactive; + unsigned long workspace_urgent; + unsigned long title_focused_bg; + unsigned long title_focused_fg; + unsigned long title_unfocused_bg; + unsigned long title_unfocused_fg; + unsigned long border_focused; + unsigned long border_unfocused; + unsigned long notification_bg; + unsigned long notification_fg; +} ColorScheme; + +/* Configuration structure */ +struct Config { + /* General */ + char terminal[128]; + char launcher[128]; + char file_manager[128]; + FocusMode focus_mode; + bool show_decorations; + + /* Appearance */ + int border_width; + int title_height; + int panel_height; + int gap; + char font_name[128]; + ColorScheme colors; + + /* Layout */ + float default_master_ratio; + int default_master_count; + LayoutType default_layout; + + /* Panels */ + bool top_panel_enabled; + bool bottom_panel_enabled; + + /* AI */ + char openrouter_api_key[256]; + char exa_api_key[256]; + char ai_model[64]; + bool ai_enabled; + + /* Paths */ + char config_path[512]; + char log_path[512]; +}; + +/* Configuration functions */ +Config *config_create(void); +void config_destroy(Config *cfg); +bool config_load(Config *cfg, const char *path); +bool config_reload(Config *cfg); +void config_set_defaults(Config *cfg); + +/* Getters for commonly used values */ +const char *config_get_terminal(void); +const char *config_get_launcher(void); +int config_get_border_width(void); +int config_get_title_height(void); +int config_get_panel_height(void); +int config_get_gap(void); +const ColorScheme *config_get_colors(void); + +/* INI parsing helpers */ +typedef void (*ConfigCallback)(const char *section, const char *key, + const char *value, void *user_data); +bool config_parse_ini(const char *path, ConfigCallback callback, void *user_data); + +#endif /* DWN_CONFIG_H */ diff --git a/include/decorations.h b/include/decorations.h new file mode 100644 index 0000000..916ebe4 --- /dev/null +++ b/include/decorations.h @@ -0,0 +1,48 @@ +/* + * DWN - Desktop Window Manager + * Window decorations (title bars, borders, buttons) + */ + +#ifndef DWN_DECORATIONS_H +#define DWN_DECORATIONS_H + +#include "dwn.h" +#include + +/* Button types */ +typedef enum { + BUTTON_CLOSE, + BUTTON_MAXIMIZE, + BUTTON_MINIMIZE, + BUTTON_COUNT +} ButtonType; + +/* Button areas for hit testing */ +typedef struct { + int x, y; + int width, height; +} ButtonArea; + +/* Decoration initialization */ +void decorations_init(void); +void decorations_cleanup(void); + +/* Rendering */ +void decorations_render(Client *client, bool focused); +void decorations_render_title_bar(Client *client, bool focused); +void decorations_render_buttons(Client *client, bool focused); +void decorations_render_border(Client *client, bool focused); + +/* Hit testing */ +ButtonType decorations_hit_test_button(Client *client, int x, int y); +bool decorations_hit_test_title_bar(Client *client, int x, int y); +bool decorations_hit_test_resize_area(Client *client, int x, int y, int *direction); + +/* Button actions */ +void decorations_button_press(Client *client, ButtonType button); + +/* Text rendering */ +void decorations_draw_text(Window window, GC gc, int x, int y, + const char *text, unsigned long color); + +#endif /* DWN_DECORATIONS_H */ diff --git a/include/dwn.h b/include/dwn.h new file mode 100644 index 0000000..ba4e466 --- /dev/null +++ b/include/dwn.h @@ -0,0 +1,168 @@ +/* + * DWN - Desktop Window Manager + * Main header with shared types and global state + */ + +#ifndef DWN_H +#define DWN_H + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Version */ +#define DWN_VERSION "1.0.0" +#define DWN_NAME "DWN" + +/* Limits */ +#define MAX_CLIENTS 256 +#define MAX_WORKSPACES 9 +#define MAX_MONITORS 8 +#define MAX_NOTIFICATIONS 32 +#define MAX_KEYBINDINGS 64 + +/* Default dimensions */ +#define DEFAULT_BORDER_WIDTH 2 +#define DEFAULT_TITLE_HEIGHT 24 +#define DEFAULT_PANEL_HEIGHT 28 +#define DEFAULT_GAP 4 + +/* Layout types */ +typedef enum { + LAYOUT_TILING, + LAYOUT_FLOATING, + LAYOUT_MONOCLE, + LAYOUT_COUNT +} LayoutType; + +/* Focus modes */ +typedef enum { + FOCUS_CLICK, + FOCUS_FOLLOW +} FocusMode; + +/* Client state flags */ +typedef enum { + CLIENT_NORMAL = 0, + CLIENT_FLOATING = (1 << 0), + CLIENT_FULLSCREEN = (1 << 1), + CLIENT_URGENT = (1 << 2), + CLIENT_MINIMIZED = (1 << 3), + CLIENT_STICKY = (1 << 4) +} ClientFlags; + +/* Forward declarations */ +typedef struct Client Client; +typedef struct Workspace Workspace; +typedef struct Monitor Monitor; +typedef struct Panel Panel; +typedef struct Config Config; + +/* Client structure - represents a managed window */ +struct Client { + Window window; /* Application window */ + Window frame; /* Frame window (decoration) */ + int x, y; /* Position */ + int width, height; /* Size */ + int old_x, old_y; /* Previous position (for floating restore) */ + int old_width, old_height; + int border_width; + uint32_t flags; /* ClientFlags bitmask */ + unsigned int workspace; /* Current workspace (0-8) */ + char title[256]; /* Window title */ + char class[64]; /* Window class */ + Client *next; /* Linked list */ + Client *prev; +}; + +/* Monitor structure - represents a physical display */ +struct Monitor { + int x, y; + int width, height; + int index; + bool primary; +}; + +/* Workspace structure */ +struct Workspace { + Client *clients; /* Head of client list */ + Client *focused; /* Currently focused client */ + LayoutType layout; /* Current layout */ + float master_ratio; /* Ratio for master area in tiling */ + int master_count; /* Number of windows in master area */ + char name[32]; /* Workspace name */ +}; + +/* Panel widget types */ +typedef enum { + WIDGET_WORKSPACES, + WIDGET_TASKBAR, + WIDGET_CLOCK, + WIDGET_SYSTRAY, + WIDGET_AI_STATUS, + WIDGET_SEPARATOR +} WidgetType; + +/* Global state - singleton pattern */ +typedef struct { + Display *display; + int screen; + Window root; + int screen_width; + int screen_height; + + /* Monitors */ + Monitor monitors[MAX_MONITORS]; + int monitor_count; + + /* Workspaces */ + Workspace workspaces[MAX_WORKSPACES]; + int current_workspace; + + /* Clients */ + Client *client_list; /* All clients */ + int client_count; + + /* Panels */ + Panel *top_panel; + Panel *bottom_panel; + + /* Configuration */ + Config *config; + + /* State */ + bool running; + bool ai_enabled; + + /* Graphics contexts */ + GC gc; + XFontStruct *font; + XftFont *xft_font; /* Xft font for UTF-8 rendering */ + Colormap colormap; + + /* Drag state */ + Client *drag_client; + int drag_start_x, drag_start_y; + int drag_orig_x, drag_orig_y; + int drag_orig_w, drag_orig_h; + bool resizing; +} DWNState; + +/* Global state accessor */ +extern DWNState *dwn; + +/* Core functions */ +int dwn_init(void); +void dwn_cleanup(void); +void dwn_run(void); +void dwn_quit(void); + +/* Event handlers */ +void dwn_handle_event(XEvent *ev); + +#endif /* DWN_H */ diff --git a/include/keys.h b/include/keys.h new file mode 100644 index 0000000..c09a23e --- /dev/null +++ b/include/keys.h @@ -0,0 +1,98 @@ +/* + * DWN - Desktop Window Manager + * Keyboard shortcut handling + */ + +#ifndef DWN_KEYS_H +#define DWN_KEYS_H + +#include "dwn.h" +#include +#include + +/* Modifier masks */ +#define MOD_ALT Mod1Mask +#define MOD_CTRL ControlMask +#define MOD_SHIFT ShiftMask +#define MOD_SUPER Mod4Mask + +/* Key binding callback type */ +typedef void (*KeyCallback)(void); + +/* Key binding structure */ +typedef struct { + unsigned int modifiers; + KeySym keysym; + KeyCallback callback; + const char *description; +} KeyBinding; + +/* Initialization */ +void keys_init(void); +void keys_cleanup(void); +void keys_grab_all(void); +void keys_ungrab_all(void); + +/* Key binding registration */ +void keys_bind(unsigned int modifiers, KeySym keysym, + KeyCallback callback, const char *description); +void keys_unbind(unsigned int modifiers, KeySym keysym); +void keys_clear_all(void); + +/* Key event handling */ +void keys_handle_press(XKeyEvent *ev); +void keys_handle_release(XKeyEvent *ev); + +/* Default key bindings */ +void keys_setup_defaults(void); + +/* Key binding callbacks */ +void key_spawn_terminal(void); +void key_spawn_launcher(void); +void key_spawn_file_manager(void); +void key_spawn_browser(void); +void key_close_window(void); +void key_quit_dwn(void); +void key_cycle_layout(void); +void key_toggle_floating(void); +void key_toggle_fullscreen(void); +void key_toggle_maximize(void); +void key_focus_next(void); +void key_focus_prev(void); +void key_workspace_next(void); +void key_workspace_prev(void); +void key_workspace_1(void); +void key_workspace_2(void); +void key_workspace_3(void); +void key_workspace_4(void); +void key_workspace_5(void); +void key_workspace_6(void); +void key_workspace_7(void); +void key_workspace_8(void); +void key_workspace_9(void); +void key_move_to_workspace_1(void); +void key_move_to_workspace_2(void); +void key_move_to_workspace_3(void); +void key_move_to_workspace_4(void); +void key_move_to_workspace_5(void); +void key_move_to_workspace_6(void); +void key_move_to_workspace_7(void); +void key_move_to_workspace_8(void); +void key_move_to_workspace_9(void); +void key_increase_master(void); +void key_decrease_master(void); +void key_increase_master_count(void); +void key_decrease_master_count(void); +void key_toggle_ai(void); +void key_ai_command(void); +void key_show_shortcuts(void); +void key_start_tutorial(void); + +/* Tutorial system */ +void tutorial_start(void); +void tutorial_stop(void); +void tutorial_next_step(void); +void tutorial_check_key(unsigned int modifiers, KeySym keysym); +bool tutorial_is_active(void); + +#endif /* DWN_KEYS_H */ diff --git a/include/layout.h b/include/layout.h new file mode 100644 index 0000000..883746b --- /dev/null +++ b/include/layout.h @@ -0,0 +1,25 @@ +/* + * DWN - Desktop Window Manager + * Layout algorithms (tiling, floating, monocle) + */ + +#ifndef DWN_LAYOUT_H +#define DWN_LAYOUT_H + +#include "dwn.h" + +/* Layout arrangement */ +void layout_arrange(int workspace); +void layout_arrange_tiling(int workspace); +void layout_arrange_floating(int workspace); +void layout_arrange_monocle(int workspace); + +/* Layout helpers */ +int layout_get_usable_area(int *x, int *y, int *width, int *height); +int layout_count_tiled_clients(int workspace); + +/* Layout names */ +const char *layout_get_name(LayoutType layout); +const char *layout_get_symbol(LayoutType layout); + +#endif /* DWN_LAYOUT_H */ diff --git a/include/news.h b/include/news.h new file mode 100644 index 0000000..6eefc6e --- /dev/null +++ b/include/news.h @@ -0,0 +1,66 @@ +/* + * DWN - Desktop Window Manager + * News ticker for bottom panel + */ + +#ifndef DWN_NEWS_H +#define DWN_NEWS_H + +#include "dwn.h" +#include + +/* Maximum articles to cache */ +#define MAX_NEWS_ARTICLES 50 +#define NEWS_API_URL "https://news.app.molodetz.nl/api" + +/* News article */ +typedef struct { + char title[256]; + char content[1024]; + char link[512]; + char author[128]; +} NewsArticle; + +/* News ticker state */ +typedef struct { + NewsArticle articles[MAX_NEWS_ARTICLES]; + int article_count; + int current_article; /* Currently displayed article index */ + double scroll_offset; /* Sub-pixel offset for smooth scrolling */ + bool fetching; /* Currently fetching from API */ + bool has_error; /* Last fetch failed */ + long last_fetch; /* Timestamp of last fetch */ + long last_scroll_update; /* Timestamp of last scroll update */ + bool interactive_mode; /* User is navigating with up/down */ + int display_widths[MAX_NEWS_ARTICLES]; /* Cached text widths */ + int total_width; /* Total scrollable width */ + int render_x; /* X position where news starts rendering */ + int render_width; /* Width of news render area */ + bool widths_dirty; /* Need to recalculate widths */ +} NewsState; + +/* Global state */ +extern NewsState news_state; + +/* Initialization */ +void news_init(void); +void news_cleanup(void); + +/* Fetching */ +void news_fetch_async(void); +void news_update(void); /* Called from main loop */ + +/* Navigation */ +void news_next_article(void); +void news_prev_article(void); +void news_open_current(void); + +/* Rendering */ +void news_render(Panel *panel, int x, int max_width, int *used_width); +void news_handle_click(int x, int y); + +/* Thread-safe access */ +void news_lock(void); +void news_unlock(void); + +#endif /* DWN_NEWS_H */ diff --git a/include/notifications.h b/include/notifications.h new file mode 100644 index 0000000..d44f8b0 --- /dev/null +++ b/include/notifications.h @@ -0,0 +1,73 @@ +/* + * DWN - Desktop Window Manager + * D-Bus Notification daemon + */ + +#ifndef DWN_NOTIFICATIONS_H +#define DWN_NOTIFICATIONS_H + +#include "dwn.h" +#include +#include + +/* Notification urgency levels */ +typedef enum { + NOTIFY_URGENCY_LOW, + NOTIFY_URGENCY_NORMAL, + NOTIFY_URGENCY_CRITICAL +} NotifyUrgency; + +/* Notification structure */ +typedef struct Notification { + uint32_t id; + char app_name[64]; + char summary[512]; /* Larger summary for AI responses */ + char *body; /* Dynamically allocated - unlimited size */ + size_t body_len; /* Length of body text */ + char icon[256]; + int timeout; /* -1 = default, 0 = never, >0 = milliseconds */ + NotifyUrgency urgency; + long expire_time; /* Timestamp when notification should disappear */ + Window window; /* X11 window for rendering */ + int width; /* Dynamic width based on content */ + int height; /* Dynamic height based on content */ + struct Notification *next; +} Notification; + +/* D-Bus connection */ +extern DBusConnection *dbus_conn; + +/* Initialization */ +bool notifications_init(void); +void notifications_cleanup(void); + +/* D-Bus handling */ +void notifications_process_messages(void); +bool notifications_register_service(void); + +/* Notification management */ +uint32_t notification_show(const char *app_name, const char *summary, + const char *body, const char *icon, int timeout); +void notification_close(uint32_t id); +void notification_close_all(void); +Notification *notification_find(uint32_t id); +Notification *notification_find_by_window(Window window); + +/* Rendering */ +void notification_render(Notification *notif); +void notifications_render_all(void); +void notifications_update(void); +void notifications_position(void); +void notifications_raise_all(void); + +/* D-Bus method handlers */ +DBusHandlerResult notifications_handle_message(DBusConnection *conn, + DBusMessage *msg, + void *user_data); + +/* Server info */ +void notifications_get_server_info(const char **name, const char **vendor, + const char **version, const char **spec_version); +void notifications_get_capabilities(const char ***caps, int *count); + +#endif /* DWN_NOTIFICATIONS_H */ diff --git a/include/panel.h b/include/panel.h new file mode 100644 index 0000000..0392cd3 --- /dev/null +++ b/include/panel.h @@ -0,0 +1,65 @@ +/* + * DWN - Desktop Window Manager + * Panel system (top and bottom panels with widgets) + */ + +#ifndef DWN_PANEL_H +#define DWN_PANEL_H + +#include "dwn.h" +#include + +/* Panel position */ +typedef enum { + PANEL_TOP, + PANEL_BOTTOM +} PanelPosition; + +/* Panel structure */ +struct Panel { + Window window; + PanelPosition position; + int x, y; + int width, height; + bool visible; + Pixmap buffer; /* Double buffering */ +}; + +/* Panel initialization */ +Panel *panel_create(PanelPosition position); +void panel_destroy(Panel *panel); +void panels_init(void); +void panels_cleanup(void); + +/* Panel rendering */ +void panel_render(Panel *panel); +void panel_render_all(void); +void panel_render_workspaces(Panel *panel, int x, int *width); +void panel_render_taskbar(Panel *panel, int x, int *width); +void panel_render_clock(Panel *panel, int x, int *width); +void panel_render_systray(Panel *panel, int x, int *width); +void panel_render_layout_indicator(Panel *panel, int x, int *width); +void panel_render_ai_status(Panel *panel, int x, int *width); + +/* Panel interaction */ +void panel_handle_click(Panel *panel, int x, int y, int button); +int panel_hit_test_workspace(Panel *panel, int x, int y); +Client *panel_hit_test_taskbar(Panel *panel, int x, int y); + +/* Panel visibility */ +void panel_show(Panel *panel); +void panel_hide(Panel *panel); +void panel_toggle(Panel *panel); + +/* Clock updates */ +void panel_update_clock(void); + +/* System stats updates */ +void panel_update_system_stats(void); + +/* System tray */ +void panel_init_systray(void); +void panel_add_systray_icon(Window icon); +void panel_remove_systray_icon(Window icon); + +#endif /* DWN_PANEL_H */ diff --git a/include/systray.h b/include/systray.h new file mode 100644 index 0000000..546508a --- /dev/null +++ b/include/systray.h @@ -0,0 +1,134 @@ +/* + * DWN - Desktop Window Manager + * System tray widgets (WiFi, Audio, etc.) + */ + +#ifndef DWN_SYSTRAY_H +#define DWN_SYSTRAY_H + +#include "dwn.h" +#include + +/* Maximum number of WiFi networks to show */ +#define MAX_WIFI_NETWORKS 20 + +/* WiFi network info */ +typedef struct { + char ssid[64]; + int signal; /* Signal strength 0-100 */ + char security[32]; /* Security type (WPA2, etc.) */ + bool connected; +} WifiNetwork; + +/* WiFi state */ +typedef struct { + bool enabled; + bool connected; + char current_ssid[64]; + int signal_strength; + WifiNetwork networks[MAX_WIFI_NETWORKS]; + int network_count; + long last_scan; /* Timestamp of last scan */ +} WifiState; + +/* Audio state */ +typedef struct { + int volume; /* 0-100 */ + bool muted; +} AudioState; + +/* Battery state */ +typedef struct { + bool present; /* Battery exists */ + bool charging; /* Currently charging */ + int percentage; /* 0-100 */ + int time_remaining; /* Minutes remaining */ +} BatteryState; + +/* Volume slider popup */ +typedef struct { + Window window; + int x, y; + int width, height; + bool visible; + bool dragging; +} VolumeSlider; + +/* Dropdown menu */ +typedef struct { + Window window; + int x, y; + int width, height; + int item_count; + int hovered_item; + bool visible; + void (*on_select)(int index); +} DropdownMenu; + +/* System tray state */ +extern WifiState wifi_state; +extern AudioState audio_state; +extern BatteryState battery_state; +extern DropdownMenu *wifi_menu; +extern VolumeSlider *volume_slider; + +/* Initialization */ +void systray_init(void); +void systray_cleanup(void); + +/* WiFi functions */ +void wifi_update_state(void); +void wifi_scan_networks(void); +void wifi_connect(const char *ssid); +void wifi_disconnect(void); +const char *wifi_get_icon(void); + +/* Audio functions */ +void audio_update_state(void); +void audio_set_volume(int volume); +void audio_toggle_mute(void); +const char *audio_get_icon(void); + +/* Battery functions */ +void battery_update_state(void); +const char *battery_get_icon(void); + +/* Volume slider functions */ +VolumeSlider *volume_slider_create(int x, int y); +void volume_slider_destroy(VolumeSlider *slider); +void volume_slider_show(VolumeSlider *slider); +void volume_slider_hide(VolumeSlider *slider); +void volume_slider_render(VolumeSlider *slider); +void volume_slider_handle_click(VolumeSlider *slider, int x, int y); +void volume_slider_handle_motion(VolumeSlider *slider, int x, int y); +void volume_slider_handle_release(VolumeSlider *slider); + +/* Dropdown menu functions */ +DropdownMenu *dropdown_create(int x, int y, int width); +void dropdown_destroy(DropdownMenu *menu); +void dropdown_show(DropdownMenu *menu); +void dropdown_hide(DropdownMenu *menu); +void dropdown_add_item(DropdownMenu *menu, const char *label); +void dropdown_render(DropdownMenu *menu); +int dropdown_hit_test(DropdownMenu *menu, int x, int y); +void dropdown_handle_click(DropdownMenu *menu, int x, int y); +void dropdown_handle_motion(DropdownMenu *menu, int x, int y); + +/* Panel rendering for systray */ +void systray_render(Panel *panel, int x, int *width); +int systray_get_width(void); +void systray_handle_click(int x, int y, int button); +int systray_hit_test(int x); /* Returns: 0=wifi, 1=audio, -1=none */ + +/* Periodic update */ +void systray_update(void); + +/* Thread-safe state access */ +void systray_lock(void); +void systray_unlock(void); + +/* Thread-safe state snapshots (copies state under lock) */ +BatteryState systray_get_battery_snapshot(void); +AudioState systray_get_audio_snapshot(void); + +#endif /* DWN_SYSTRAY_H */ diff --git a/include/util.h b/include/util.h new file mode 100644 index 0000000..8e833c7 --- /dev/null +++ b/include/util.h @@ -0,0 +1,68 @@ +/* + * DWN - Desktop Window Manager + * Utility functions + */ + +#ifndef DWN_UTIL_H +#define DWN_UTIL_H + +#include +#include +#include + +/* Logging levels */ +typedef enum { + LOG_DEBUG, + LOG_INFO, + LOG_WARN, + LOG_ERROR +} LogLevel; + +/* Async Logging - non-blocking with max file size (5MB) and rotation */ +void log_init(const char *log_file); +void log_close(void); +void log_set_level(LogLevel level); +void log_flush(void); /* Force flush pending logs (call before exit/crash) */ +void log_msg(LogLevel level, const char *fmt, ...); + +/* Convenience macros */ +#define LOG_DEBUG(...) log_msg(LOG_DEBUG, __VA_ARGS__) +#define LOG_INFO(...) log_msg(LOG_INFO, __VA_ARGS__) +#define LOG_WARN(...) log_msg(LOG_WARN, __VA_ARGS__) +#define LOG_ERROR(...) log_msg(LOG_ERROR, __VA_ARGS__) + +/* Memory allocation with error checking */ +void *dwn_malloc(size_t size); +void *dwn_calloc(size_t nmemb, size_t size); +void *dwn_realloc(void *ptr, size_t size); +char *dwn_strdup(const char *s); +void dwn_free(void *ptr); +void secure_wipe(void *ptr, size_t size); /* Securely wipe sensitive data */ + +/* String utilities */ +char *str_trim(char *str); +bool str_starts_with(const char *str, const char *prefix); +bool str_ends_with(const char *str, const char *suffix); +int str_split(char *str, char delim, char **parts, int max_parts); +char *shell_escape(const char *str); /* Escape string for safe shell use */ + +/* File utilities */ +bool file_exists(const char *path); +char *file_read_all(const char *path); +bool file_write_all(const char *path, const char *content); +char *expand_path(const char *path); + +/* Color utilities */ +unsigned long parse_color(const char *color_str); +void color_to_rgb(unsigned long color, int *r, int *g, int *b); + +/* Time utilities */ +long get_time_ms(void); +void sleep_ms(int ms); + +/* Process utilities */ +int spawn(const char *cmd); +int spawn_async(const char *cmd); +char *spawn_capture(const char *cmd); + +#endif /* DWN_UTIL_H */ diff --git a/include/workspace.h b/include/workspace.h new file mode 100644 index 0000000..ace5354 --- /dev/null +++ b/include/workspace.h @@ -0,0 +1,61 @@ +/* + * DWN - Desktop Window Manager + * Workspace (tag/virtual desktop) management + */ + +#ifndef DWN_WORKSPACE_H +#define DWN_WORKSPACE_H + +#include "dwn.h" +#include + +/* Workspace initialization */ +void workspace_init(void); +void workspace_cleanup(void); + +/* Workspace access */ +Workspace *workspace_get(int index); +Workspace *workspace_get_current(void); +int workspace_get_current_index(void); + +/* Workspace switching */ +void workspace_switch(int index); +void workspace_switch_next(void); +void workspace_switch_prev(void); + +/* Client management within workspaces */ +void workspace_add_client(int workspace, Client *client); +void workspace_remove_client(int workspace, Client *client); +void workspace_move_client(Client *client, int new_workspace); +Client *workspace_get_first_client(int workspace); +Client *workspace_get_focused_client(int workspace); + +/* Layout */ +void workspace_set_layout(int workspace, LayoutType layout); +LayoutType workspace_get_layout(int workspace); +void workspace_cycle_layout(int workspace); +void workspace_set_master_ratio(int workspace, float ratio); +void workspace_adjust_master_ratio(int workspace, float delta); +void workspace_set_master_count(int workspace, int count); +void workspace_adjust_master_count(int workspace, int delta); + +/* Arrangement */ +void workspace_arrange(int workspace); +void workspace_arrange_current(void); + +/* Visibility */ +void workspace_show(int workspace); +void workspace_hide(int workspace); + +/* Properties */ +void workspace_set_name(int workspace, const char *name); +const char *workspace_get_name(int workspace); +int workspace_client_count(int workspace); +bool workspace_is_empty(int workspace); + +/* Focus cycling within workspace */ +void workspace_focus_next(void); +void workspace_focus_prev(void); +void workspace_focus_master(void); + +#endif /* DWN_WORKSPACE_H */ diff --git a/scripts/dwn.desktop b/scripts/dwn.desktop new file mode 100644 index 0000000..efc7a5c --- /dev/null +++ b/scripts/dwn.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=DWN +Comment=AI-enhanced desktop window manager +Exec=dwn +TryExec=dwn +Type=Application +DesktopNames=DWN +Keywords=wm;tiling;window;manager; diff --git a/scripts/xinitrc.example b/scripts/xinitrc.example new file mode 100644 index 0000000..587584f --- /dev/null +++ b/scripts/xinitrc.example @@ -0,0 +1,42 @@ +#!/bin/sh +# Example .xinitrc for starting DWN with startx +# Copy this to ~/.xinitrc and modify as needed + +# Set environment variables +export XDG_SESSION_TYPE=x11 +export XDG_CURRENT_DESKTOP=DWN + +# Optional: Set AI API keys +# export OPENROUTER_API_KEY="your-api-key-here" +# export EXA_API_KEY="your-exa-key-here" + +# Optional: Start background services +# Start D-Bus session bus if not already running +if [ -z "$DBUS_SESSION_BUS_ADDRESS" ]; then + eval $(dbus-launch --sh-syntax) +fi + +# Optional: Start compositor for transparency/shadows +# picom & + +# Optional: Set wallpaper +# feh --bg-scale ~/.wallpaper.png & + +# Optional: Start notification daemon (DWN has built-in) +# Note: DWN includes its own notification daemon + +# Optional: Start XFCE components for additional functionality +# xfce4-power-manager & +# xfsettingsd & + +# Optional: Start polkit authentication agent +# /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 & + +# Optional: Start network manager applet +# nm-applet & + +# Optional: Start clipboard manager +# xfce4-clipman & + +# Start DWN +exec dwn diff --git a/site/ai-features.html b/site/ai-features.html new file mode 100644 index 0000000..e227235 --- /dev/null +++ b/site/ai-features.html @@ -0,0 +1,458 @@ + + + + + + + AI Features - DWN Window Manager + + + +

+ +
+ +
+
+
+

AI Integration

+

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

+
+
+ +
+
+
+ Optional Features +

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

+
+ +

Overview

+

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

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

AI Command Palette

+

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

+

Super + Shift + A

+
+
+
👁
+

Context Analysis

+

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

+

Super + A

+
+
+
🔍
+

Semantic Search

+

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

+

Super + Shift + E

+
+
+ + +

Setting Up OpenRouter

+

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

+ +
+
+
1
+
+

Get an API Key

+

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

+
+
+ +
+
2
+
+

Set the Environment Variable

+

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

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

Choose a Model (Optional)

+

Configure the AI model in your DWN config file:

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

+ Browse available models at openrouter.ai/models +

+
+
+
+ +
+

Recommended Free Models

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

AI Command Palette

+

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

+ +

Supported Commands

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

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

+
+ + +

Context Analysis

+

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

+ +
+

What It Shows

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

Setting Up Exa Search

+

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

+ +
+
+
1
+
+

Get an Exa API Key

+

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

+
+
+ +
+
2
+
+

Set the Environment Variable

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

Start Searching

+

Press Super + Shift + E and type your query.

+
+
+
+ + + +

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

+ +
+
+

Traditional Search

+

Keyword matching

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

+ "nginx reverse proxy setup tutorial" +

+
+ +
+ +

Search Tips

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

Privacy Considerations

+
+ Data Sent to External Services +

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

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

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

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

Troubleshooting

+ +
+ +
+
+

Make sure your API key is properly set:

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

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

+
+
+
+ +
+ +
+
+

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

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

Architecture

+

+ Technical documentation for developers and contributors. +

+
+
+ +
+
+

Overview

+

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

+ +
+

Project Statistics

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

Directory Structure

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

Core Modules

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

Module Dependencies

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

Global State (DWNState)

+

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

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

Event Loop

+

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

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

Key Constants

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

Coding Conventions

+
+
+

Naming

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

Style

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

EWMH/ICCCM Support

+

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

+ +
+
+

EWMH Atoms

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

ICCCM Support

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

Build System

+

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

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

Contributing

+

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

+ +
+
+
1
+
+

Fork & Clone

+

Fork the repository and clone your fork locally.

+
+
+
+
2
+
+

Create a Branch

+

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

+
+
+
+
3
+
+

Make Changes

+

Follow coding conventions. Run make format and make check.

+
+
+
+
4
+
+

Test

+

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

+
+
+
+
5
+
+

Submit PR

+

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

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

Configuration Guide

+

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

+
+
+ +
+
+

Configuration File

+

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

+ +
+ First Run +

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

+
+ + +

[general] - Core Settings

+

+ Basic behavior settings for applications and focus handling. +

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

[appearance] - Visual Settings

+

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

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

[layout] - Layout Behavior

+

+ Configure the default layout mode and tiling parameters. +

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

[panels] - Panel Visibility

+

+ Control which panels are displayed. +

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

[colors] - Color Scheme

+

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

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

[ai] - AI Integration

+

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

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

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

+
+ + +

Complete Configuration Example

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

Getting Started with DWN

+

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

+ +

First Steps

+

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

+ +

Opening Your First Application

+

Start by launching a terminal and application launcher:

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

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

+
+ +

Basic Concepts

+ +

The Super Key

+

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

+ +

Focus Model

+

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

+ +

Window Decorations

+

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

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

Interactive Tutorial

+

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

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

Start the Tutorial

+

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

+
+ +

Managing Windows

+ +

Window Operations

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

Moving and Resizing

+

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

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

Using Workspaces

+

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

+ +

Workspace Navigation

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

Workspace Organization Tips

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

Layout Modes

+

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

+ +
+
+

Tiling

+

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

+
+
+

Floating

+

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

+
+
+

Monocle

+

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

+
+
+ +

Tiling Layout Controls

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

Panels & System Tray

+ +

Top Panel

+

The top panel contains:

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

System Tray

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

Bottom Panel

+

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

+ +

Next Steps

+

Now that you know the basics, explore these topics:

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

Powerful Features

+

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

+
+
+ + +
+
+

Window Management

+

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

+ +
+
+

Tiling Layout

+

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

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

Floating Layout

+

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

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

Monocle Layout

+

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

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

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

+
+
+
+ + +
+
+

Virtual Workspaces

+

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

+ +
+
+
1-9
+

9 Workspaces

+

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

+
+
+
+

Window Transfer

+

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

+
+
+
📈
+

Per-Workspace State

+

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

+
+
+
👁
+

Visual Indicators

+

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

+
+
+
+
+ + +
+
+

Panels & System Tray

+

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

+ +
+
+

Top Panel

+

The top panel contains your main controls and information:

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

Bottom Panel

+

Optional bottom panel for additional information:

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

System Tray Features

+
+
+

🔋 Battery Monitor

+

Shows current battery percentage with color-coded status:

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

🔊 Volume Control

+

Full audio control at your fingertips:

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

📶 WiFi Manager

+

Network management made simple:

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

Notification System

+

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

+ +
+
+

Standards Compliant

+

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

+
+
+

Customizable Appearance

+

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

+
+
+ +
+ Capacity +

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

+
+
+
+ + +
+
+

AI Integration

+

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

+ +
+
+

🤖 AI Command Palette

+

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

+ Learn More +
+
+

🔍 Semantic Web Search

+

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

+ Learn More +
+
+

🎓 Context Analysis

+

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

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

Standards Compliance

+

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

+ +
+
+

EWMH Support

+

Extended Window Manager Hints for modern application features:

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

ICCCM Compliance

+

Inter-Client Communication Conventions Manual support:

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

Technical Specifications

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

Ready to Try DWN?

+

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

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

Modern Window Management for X11

+

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

+ +
+
+ + +
+
+

Why Choose DWN?

+

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

+
+
+
+

Multiple Layouts

+

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

+
+
+
+

9 Workspaces

+

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

+
+
+
🎨
+

Fully Customizable

+

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

+
+
+
💡
+

AI Integration

+

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

+
+
+
🔔
+

Notification Daemon

+

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

+
+
+
💻
+

System Tray

+

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

+
+
+
+
+ + +
+
+
+
+

~10K

+

Lines of Pure C

+
+
+

0

+

Runtime Dependencies

+
+
+

<5MB

+

Memory Footprint

+
+
+

64+

+

Keyboard Shortcuts

+
+
+
+
+ + +
+
+

Get Up and Running in Minutes

+

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

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

+ Full Installation Guide +

+
+
+ + +
+
+

See DWN in Action

+

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

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

How DWN Compares

+

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

+
+
+

Minimal Tiling WMs

+

dwm, i3, bspwm

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

Desktop Environments

+

XFCE, GNOME, KDE

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

What Users Say

+
+
+

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

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

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

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

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

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

Ready to Transform Your Desktop?

+

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

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

Installation Guide

+

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

+
+
+ + +
+
+

Requirements

+

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

+ +
+

Required Dependencies

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

Quick Installation

+

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

+ +
+
+
1
+
+

Clone the Repository

+

Download the latest source code from GitHub.

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

Install Dependencies

+

Automatically install required packages for your distribution.

+
+ Terminal + +
+
make deps
+

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

+
+
+ +
+
3
+
+

Build DWN

+

Compile the window manager with optimizations.

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

Install System-wide

+

Install the binary to your system PATH.

+
+ Terminal + +
+
sudo make install
+

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

+
+
+ +
+
5
+
+

Configure Your Session

+

Add DWN to your X session startup.

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

Distribution-Specific Instructions

+ +
+ + + + +
+ +
+

Debian / Ubuntu / Linux Mint

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

Fedora / RHEL / CentOS

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

Arch Linux / Manjaro

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

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

+
+
+ +
+

Void Linux

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

Session Setup

+

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

+ +
+
+

Using xinit / startx

+

For minimal setups using startx:

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

+ Then run startx from a TTY. +

+
+
+

Using a Display Manager

+

Create a desktop entry for GDM, LightDM, etc:

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

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

+
+
+
+
+ + +
+
+

Testing in a Nested X Server

+

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

+ +
+

Using make run

+

The easiest way to test DWN safely:

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

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

+
+ +
+

Manual Xephyr Setup

+

For more control over the test environment:

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

Post-Installation

+ +
+
+
1
+
+

Create Configuration Directory

+

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

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

Run the Interactive Tutorial

+

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

+
+
+ +
+
3
+
+

View All Shortcuts

+

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

+
+
+ +
+
4
+
+

Customize Your Setup

+

See the Configuration Guide to personalize DWN to your liking.

+
+
+
+
+
+ + +
+
+

Troubleshooting

+ +
+ +
+
+

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

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

Install pkg-config for your distribution:

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

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

+
+
+
+ +
+ +
+
+

Check for conflicts with other programs grabbing keys:

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

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

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

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

+
+
+
+
+
+ + +
+
+

Installation Complete?

+

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

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

Keyboard Shortcuts

+

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

+
+
+ +
+
+ + + +

Application Launchers

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

Window Management

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

Workspace Navigation

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

Layout Control

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

AI Features

+

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

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

Help & System

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

Printable Quick Reference

+

Essential shortcuts to memorize when starting with DWN:

+ +
+
+

Must Know

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

Power User

+
    +
  • + Super+Space Change layout +
  • +
  • + Super+H/L Resize master +
  • +
  • + Shift+F1-9 Move to workspace +
  • +
  • + Super+Shift+A AI command +
  • +
  • + Super+S Show shortcuts +
  • +
+
+
+
+
+
+
+ + + + + + diff --git a/src/ai.c b/src/ai.c new file mode 100644 index 0000000..81ab432 --- /dev/null +++ b/src/ai.c @@ -0,0 +1,872 @@ +/* + * DWN - Desktop Window Manager + * AI Integration implementation + */ + +#include "ai.h" +#include "config.h" +#include "client.h" +#include "workspace.h" +#include "notifications.h" +#include "util.h" +#include "cJSON.h" + +#include +#include +#include +#include + +/* API endpoints */ +#define OPENROUTER_URL "https://openrouter.ai/api/v1/chat/completions" + +/* Request queue */ +static AIRequest *request_queue = NULL; +static CURLM *curl_multi = NULL; +static AIContext current_context; + +/* Response buffer for curl */ +typedef struct { + char *data; + size_t size; +} ResponseBuffer; + +/* ========== CURL callbacks ========== */ + +static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) +{ + size_t realsize = size * nmemb; + ResponseBuffer *buf = (ResponseBuffer *)userp; + + char *ptr = realloc(buf->data, buf->size + realsize + 1); + if (ptr == NULL) { + return 0; + } + + buf->data = ptr; + memcpy(&(buf->data[buf->size]), contents, realsize); + buf->size += realsize; + buf->data[buf->size] = '\0'; + + return realsize; +} + +/* ========== Initialization ========== */ + +bool ai_init(void) +{ + if (dwn == NULL || dwn->config == NULL) { + return false; + } + + if (dwn->config->openrouter_api_key[0] == '\0') { + LOG_INFO("AI features disabled (no OPENROUTER_API_KEY)"); + dwn->ai_enabled = false; + return true; /* Not an error, just disabled */ + } + + /* Initialize curl */ + curl_global_init(CURL_GLOBAL_DEFAULT); + curl_multi = curl_multi_init(); + + if (curl_multi == NULL) { + LOG_ERROR("Failed to initialize curl multi handle"); + return false; + } + + dwn->ai_enabled = true; + LOG_INFO("AI features enabled"); + + return true; +} + +void ai_cleanup(void) +{ + /* Cancel all pending requests */ + while (request_queue != NULL) { + AIRequest *next = request_queue->next; + if (request_queue->prompt) free(request_queue->prompt); + if (request_queue->response) free(request_queue->response); + free(request_queue); + request_queue = next; + } + + if (curl_multi != NULL) { + curl_multi_cleanup(curl_multi); + curl_multi = NULL; + } + + curl_global_cleanup(); +} + +bool ai_is_available(void) +{ + return dwn != NULL && dwn->ai_enabled; +} + +/* ========== API calls ========== */ + +AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *)) +{ + if (!ai_is_available() || prompt == NULL) { + return NULL; + } + + AIRequest *req = dwn_calloc(1, sizeof(AIRequest)); + req->prompt = dwn_strdup(prompt); + req->state = AI_STATE_PENDING; + req->callback = callback; + + /* Build JSON request body */ + char *json_prompt = dwn_malloc(strlen(prompt) * 2 + 256); + char *escaped_prompt = dwn_malloc(strlen(prompt) * 2 + 1); + + /* Escape special characters in prompt */ + const char *src = prompt; + char *dst = escaped_prompt; + while (*src) { + if (*src == '"' || *src == '\\' || *src == '\n' || *src == '\r' || *src == '\t') { + *dst++ = '\\'; + if (*src == '\n') *dst++ = 'n'; + else if (*src == '\r') *dst++ = 'r'; + else if (*src == '\t') *dst++ = 't'; + else *dst++ = *src; + } else { + *dst++ = *src; + } + src++; + } + *dst = '\0'; + + snprintf(json_prompt, strlen(prompt) * 2 + 256, + "{\"model\":\"%s\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}]}", + dwn->config->ai_model, escaped_prompt); + + dwn_free(escaped_prompt); + + /* Create curl easy handle */ + CURL *easy = curl_easy_init(); + if (easy == NULL) { + dwn_free(json_prompt); + dwn_free(req->prompt); + dwn_free(req); + return NULL; + } + + /* Response buffer */ + ResponseBuffer *response = dwn_calloc(1, sizeof(ResponseBuffer)); + + /* Set curl options */ + struct curl_slist *headers = NULL; + char auth_header[300]; + snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", + dwn->config->openrouter_api_key); + + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, auth_header); + + curl_easy_setopt(easy, CURLOPT_URL, OPENROUTER_URL); + curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(easy, CURLOPT_POSTFIELDS, json_prompt); + curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(easy, CURLOPT_WRITEDATA, response); + curl_easy_setopt(easy, CURLOPT_TIMEOUT, 30L); + curl_easy_setopt(easy, CURLOPT_PRIVATE, req); + curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L); + + /* Add to multi handle */ + curl_multi_add_handle(curl_multi, easy); + + /* Add to queue */ + req->next = request_queue; + request_queue = req; + + /* Store response buffer pointer for cleanup */ + req->user_data = response; + + LOG_DEBUG("AI request sent: %.50s...", prompt); + + /* Note: json_prompt and headers will be freed after request completes */ + + return req; +} + +void ai_cancel_request(AIRequest *req) +{ + if (req == NULL) { + return; + } + + /* Remove from queue */ + AIRequest **pp = &request_queue; + while (*pp != NULL) { + if (*pp == req) { + *pp = req->next; + break; + } + pp = &(*pp)->next; + } + + req->state = AI_STATE_ERROR; + + if (req->prompt) dwn_free(req->prompt); + if (req->response) dwn_free(req->response); + if (req->user_data) { + ResponseBuffer *buf = (ResponseBuffer *)req->user_data; + if (buf->data) free(buf->data); + dwn_free(buf); + } + dwn_free(req); +} + +void ai_process_pending(void) +{ + if (curl_multi == NULL) { + return; + } + + int running_handles; + curl_multi_perform(curl_multi, &running_handles); + + /* Check for completed requests */ + CURLMsg *msg; + int msgs_left; + + while ((msg = curl_multi_info_read(curl_multi, &msgs_left)) != NULL) { + if (msg->msg == CURLMSG_DONE) { + CURL *easy = msg->easy_handle; + AIRequest *req = NULL; + + curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req); + + if (req != NULL) { + ResponseBuffer *buf = (ResponseBuffer *)req->user_data; + + if (msg->data.result == CURLE_OK && buf != NULL && buf->data != NULL) { + /* Parse response using cJSON */ + /* OpenRouter format: {"choices":[{"message":{"content":"..."}}]} */ + cJSON *root = cJSON_Parse(buf->data); + if (root != NULL) { + cJSON *choices = cJSON_GetObjectItemCaseSensitive(root, "choices"); + if (cJSON_IsArray(choices) && cJSON_GetArraySize(choices) > 0) { + cJSON *first_choice = cJSON_GetArrayItem(choices, 0); + cJSON *message = cJSON_GetObjectItemCaseSensitive(first_choice, "message"); + if (message != NULL) { + cJSON *content = cJSON_GetObjectItemCaseSensitive(message, "content"); + if (cJSON_IsString(content) && content->valuestring != NULL) { + req->response = dwn_strdup(content->valuestring); + req->state = AI_STATE_COMPLETED; + } + } + } + /* Check for error in response */ + if (req->state != AI_STATE_COMPLETED) { + cJSON *error = cJSON_GetObjectItemCaseSensitive(root, "error"); + if (error != NULL) { + cJSON *err_msg = cJSON_GetObjectItemCaseSensitive(error, "message"); + if (cJSON_IsString(err_msg)) { + req->response = dwn_strdup(err_msg->valuestring); + req->state = AI_STATE_ERROR; + LOG_ERROR("AI API error: %s", err_msg->valuestring); + } + } + } + cJSON_Delete(root); + } + + if (req->state != AI_STATE_COMPLETED && req->state != AI_STATE_ERROR) { + /* Fallback: return raw response for debugging */ + req->response = dwn_strdup(buf->data); + req->state = AI_STATE_COMPLETED; + LOG_WARN("Could not parse AI response, returning raw"); + } + } else { + req->state = AI_STATE_ERROR; + LOG_ERROR("AI request failed: %s", curl_easy_strerror(msg->data.result)); + } + + /* Call callback */ + if (req->callback != NULL) { + req->callback(req); + } + + /* Cleanup */ + if (buf != NULL) { + if (buf->data) free(buf->data); + dwn_free(buf); + } + } + + curl_multi_remove_handle(curl_multi, easy); + curl_easy_cleanup(easy); + } + } +} + +/* ========== Context analysis ========== */ + +void ai_update_context(void) +{ + memset(¤t_context, 0, sizeof(current_context)); + + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + snprintf(current_context.focused_window, sizeof(current_context.focused_window), + "%s", ws->focused->title); + snprintf(current_context.focused_class, sizeof(current_context.focused_class), + "%s", ws->focused->class); + } + + /* Build list of windows on current workspace */ + int offset = 0; + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)dwn->current_workspace) { + int written = snprintf(current_context.workspace_windows + offset, + sizeof(current_context.workspace_windows) - offset, + "%s%s", offset > 0 ? ", " : "", c->title); + if (written > 0) { + offset += written; + } + current_context.window_count++; + } + } +} + +const char *ai_analyze_task(void) +{ + /* Analyze based on focused window class */ + const char *class = current_context.focused_class; + + if (strstr(class, "code") || strstr(class, "Code") || + strstr(class, "vim") || strstr(class, "emacs")) { + return "coding"; + } + if (strstr(class, "firefox") || strstr(class, "chrome") || + strstr(class, "Firefox") || strstr(class, "Chrome")) { + return "browsing"; + } + if (strstr(class, "slack") || strstr(class, "discord") || + strstr(class, "Slack") || strstr(class, "Discord")) { + return "communication"; + } + if (strstr(class, "terminal") || strstr(class, "Terminal")) { + return "terminal"; + } + + return "general"; +} + +const char *ai_suggest_window(void) +{ + /* Simple heuristic suggestion */ + const char *task = ai_analyze_task(); + + if (strcmp(task, "coding") == 0) { + return "Consider opening a terminal for testing"; + } + if (strcmp(task, "browsing") == 0) { + return "Documentation or reference material available?"; + } + + return NULL; +} + +const char *ai_suggest_app(void) +{ + return NULL; /* Would require more context */ +} + +/* ========== Command palette ========== */ + +/* Callback for AI command response */ +static void ai_command_response_callback(AIRequest *req) +{ + if (req == NULL) { + return; + } + + if (req->state == AI_STATE_COMPLETED && req->response != NULL) { + /* Check if response contains a command to execute */ + /* Format: [RUN: command] or [EXEC: command] */ + char *run_cmd = strstr(req->response, "[RUN:"); + if (run_cmd == NULL) { + run_cmd = strstr(req->response, "[EXEC:"); + } + + if (run_cmd != NULL) { + /* Extract command */ + char *cmd_start = strchr(run_cmd, ':'); + if (cmd_start != NULL) { + cmd_start++; + while (*cmd_start == ' ') cmd_start++; + + char *cmd_end = strchr(cmd_start, ']'); + if (cmd_end != NULL) { + size_t cmd_len = cmd_end - cmd_start; + char *cmd = dwn_malloc(cmd_len + 1); + strncpy(cmd, cmd_start, cmd_len); + cmd[cmd_len] = '\0'; + + /* Trim trailing spaces */ + while (cmd_len > 0 && cmd[cmd_len - 1] == ' ') { + cmd[--cmd_len] = '\0'; + } + + LOG_INFO("AI executing command: %s", cmd); + notification_show("DWN AI", "Running", cmd, NULL, 2000); + spawn_async(cmd); + dwn_free(cmd); + } + } + } else { + /* No command, just show response */ + notification_show("DWN AI", "Response", req->response, NULL, 8000); + } + } else { + notification_show("DWN AI", "Error", "Failed to get AI response", NULL, 3000); + } + + /* Cleanup - don't free req itself, it's managed by the queue */ + /* The queue will be cleaned up separately */ +} + +void ai_show_command_palette(void) +{ + if (!ai_is_available()) { + notification_show("DWN", "AI Unavailable", + "Set OPENROUTER_API_KEY to enable AI features", + NULL, 3000); + return; + } + + /* Check if dmenu or rofi is available */ + char *input = NULL; + + /* Try dmenu first, then rofi */ + if (spawn("command -v dmenu >/dev/null 2>&1") == 0) { + input = spawn_capture("echo '' | dmenu -p 'Ask AI:'"); + } else if (spawn("command -v rofi >/dev/null 2>&1") == 0) { + input = spawn_capture("rofi -dmenu -p 'Ask AI:'"); + } else { + notification_show("DWN AI", "Missing Dependency", + "Install dmenu or rofi for AI command palette:\n" + "sudo apt install dmenu", + NULL, 5000); + return; + } + + if (input == NULL || input[0] == '\0') { + if (input != NULL) { + dwn_free(input); + } + LOG_DEBUG("AI command palette cancelled"); + return; + } + + LOG_DEBUG("AI command palette input: %s", input); + + /* Show "thinking" notification */ + notification_show("DWN AI", "Processing...", input, NULL, 2000); + + /* Build context-aware prompt */ + ai_update_context(); + const char *task = ai_analyze_task(); + + char prompt[2048]; + snprintf(prompt, sizeof(prompt), + "You are an AI assistant integrated into a Linux window manager called DWN. " + "You can execute shell commands for the user.\n\n" + "IMPORTANT: When the user asks you to run, open, launch, or start an application, " + "respond with the command in this exact format: [RUN: command]\n" + "Examples:\n" + "- User: 'open chrome' -> [RUN: google-chrome]\n" + "- User: 'run firefox' -> [RUN: firefox]\n" + "- User: 'open file manager' -> [RUN: thunar]\n" + "- User: 'launch terminal' -> [RUN: xfce4-terminal]\n" + "- User: 'open vs code' -> [RUN: code]\n\n" + "For questions or non-command requests, respond briefly (1-2 sentences) without the [RUN:] format.\n\n" + "User's current task: %s\n" + "User's request: %s", + task, input); + + dwn_free(input); + + /* Send request */ + ai_send_request(prompt, ai_command_response_callback); +} + +void ai_execute_command(const char *command) +{ + if (!ai_is_available() || command == NULL) { + return; + } + + LOG_DEBUG("AI executing command: %s", command); + + /* Send to AI for interpretation */ + char prompt[512]; + snprintf(prompt, sizeof(prompt), + "User command: %s\nCurrent task: %s\nRespond with a single action to take.", + command, ai_analyze_task()); + + ai_send_request(prompt, NULL); +} + +/* ========== Smart features ========== */ + +void ai_auto_organize_workspace(void) +{ + LOG_DEBUG("AI auto-organize (placeholder)"); +} + +void ai_suggest_layout(void) +{ + LOG_DEBUG("AI layout suggestion (placeholder)"); +} + +void ai_analyze_workflow(void) +{ + LOG_DEBUG("AI workflow analysis (placeholder)"); +} + +/* ========== Notification intelligence ========== */ + +bool ai_should_show_notification(const char *app, const char *summary) +{ + /* Simple filtering - could be enhanced with AI */ + (void)app; + (void)summary; + return true; /* Show all by default */ +} + +int ai_notification_priority(const char *app, const char *summary) +{ + /* Simple priority assignment */ + if (strstr(summary, "urgent") || strstr(summary, "Urgent") || + strstr(summary, "error") || strstr(summary, "Error")) { + return 3; + } + if (strstr(app, "slack") || strstr(app, "Slack") || + strstr(app, "discord") || strstr(app, "Discord")) { + return 2; + } + return 1; +} + +/* ========== Performance monitoring ========== */ + +void ai_monitor_performance(void) +{ + /* Read from /proc for basic metrics */ + LOG_DEBUG("AI performance monitoring (placeholder)"); +} + +const char *ai_performance_suggestion(void) +{ + return NULL; +} + +/* ========== Exa Semantic Search ========== */ + +#define EXA_API_URL "https://api.exa.ai/search" + +static ExaRequest *exa_queue = NULL; + +bool exa_is_available(void) +{ + return dwn != NULL && dwn->config != NULL && + dwn->config->exa_api_key[0] != '\0'; +} + +/* Parse Exa JSON response using cJSON */ +static void exa_parse_response(ExaRequest *req, const char *json) +{ + if (req == NULL || json == NULL) { + return; + } + + req->result_count = 0; + + cJSON *root = cJSON_Parse(json); + if (root == NULL) { + LOG_WARN("Failed to parse Exa response JSON"); + return; + } + + cJSON *results = cJSON_GetObjectItemCaseSensitive(root, "results"); + if (!cJSON_IsArray(results)) { + cJSON_Delete(root); + return; + } + + int array_size = cJSON_GetArraySize(results); + for (int i = 0; i < array_size && req->result_count < 10; i++) { + cJSON *item = cJSON_GetArrayItem(results, i); + if (!cJSON_IsObject(item)) continue; + + ExaSearchResult *res = &req->results[req->result_count]; + + /* Extract title */ + cJSON *title = cJSON_GetObjectItemCaseSensitive(item, "title"); + if (cJSON_IsString(title) && title->valuestring != NULL) { + strncpy(res->title, title->valuestring, sizeof(res->title) - 1); + res->title[sizeof(res->title) - 1] = '\0'; + } + + /* Extract URL */ + cJSON *url = cJSON_GetObjectItemCaseSensitive(item, "url"); + if (cJSON_IsString(url) && url->valuestring != NULL) { + strncpy(res->url, url->valuestring, sizeof(res->url) - 1); + res->url[sizeof(res->url) - 1] = '\0'; + } + + /* Extract text/snippet if available */ + cJSON *text = cJSON_GetObjectItemCaseSensitive(item, "text"); + if (cJSON_IsString(text) && text->valuestring != NULL) { + strncpy(res->snippet, text->valuestring, sizeof(res->snippet) - 1); + res->snippet[sizeof(res->snippet) - 1] = '\0'; + } + + req->result_count++; + } + + cJSON_Delete(root); + LOG_DEBUG("Parsed %d Exa results", req->result_count); +} + +ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *)) +{ + if (!exa_is_available() || query == NULL) { + return NULL; + } + + ExaRequest *req = dwn_calloc(1, sizeof(ExaRequest)); + req->query = dwn_strdup(query); + req->state = AI_STATE_PENDING; + req->callback = callback; + req->result_count = 0; + + /* Build JSON request */ + char *json_query = dwn_malloc(strlen(query) * 2 + 256); + char *escaped = dwn_malloc(strlen(query) * 2 + 1); + + /* Escape query string */ + const char *src = query; + char *dst = escaped; + while (*src) { + if (*src == '"' || *src == '\\') { + *dst++ = '\\'; + } + *dst++ = *src++; + } + *dst = '\0'; + + snprintf(json_query, strlen(query) * 2 + 256, + "{\"query\":\"%s\",\"type\":\"auto\",\"numResults\":10,\"contents\":{\"text\":true}}", + escaped); + dwn_free(escaped); + + /* Create curl handle */ + CURL *easy = curl_easy_init(); + if (easy == NULL) { + dwn_free(json_query); + dwn_free(req->query); + dwn_free(req); + return NULL; + } + + /* Response buffer */ + ResponseBuffer *response = dwn_calloc(1, sizeof(ResponseBuffer)); + + /* Set headers */ + struct curl_slist *headers = NULL; + char api_header[300]; + snprintf(api_header, sizeof(api_header), "x-api-key: %s", + dwn->config->exa_api_key); + + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, api_header); + + curl_easy_setopt(easy, CURLOPT_URL, EXA_API_URL); + curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(easy, CURLOPT_POSTFIELDS, json_query); + curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(easy, CURLOPT_WRITEDATA, response); + curl_easy_setopt(easy, CURLOPT_TIMEOUT, 15L); + curl_easy_setopt(easy, CURLOPT_PRIVATE, req); + curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L); + + /* Add to multi handle */ + if (curl_multi == NULL) { + curl_multi = curl_multi_init(); + } + curl_multi_add_handle(curl_multi, easy); + + /* Add to queue */ + req->next = exa_queue; + exa_queue = req; + req->user_data = response; + + LOG_DEBUG("Exa search sent: %s", query); + + return req; +} + +void exa_process_pending(void) +{ + if (curl_multi == NULL || exa_queue == NULL) { + return; + } + + int running_handles; + curl_multi_perform(curl_multi, &running_handles); + + CURLMsg *msg; + int msgs_left; + + while ((msg = curl_multi_info_read(curl_multi, &msgs_left)) != NULL) { + if (msg->msg == CURLMSG_DONE) { + CURL *easy = msg->easy_handle; + ExaRequest *req = NULL; + + curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req); + + /* Check if this is an Exa request (in exa_queue) */ + bool is_exa = false; + for (ExaRequest *r = exa_queue; r != NULL; r = r->next) { + if (r == req) { + is_exa = true; + break; + } + } + + if (is_exa && req != NULL) { + ResponseBuffer *buf = (ResponseBuffer *)req->user_data; + + if (msg->data.result == CURLE_OK && buf != NULL && buf->data != NULL) { + exa_parse_response(req, buf->data); + req->state = AI_STATE_COMPLETED; + } else { + req->state = AI_STATE_ERROR; + LOG_ERROR("Exa request failed: %s", curl_easy_strerror(msg->data.result)); + } + + if (req->callback != NULL) { + req->callback(req); + } + + /* Cleanup buffer */ + if (buf != NULL) { + if (buf->data) free(buf->data); + dwn_free(buf); + } + + /* Remove from queue */ + ExaRequest **pp = &exa_queue; + while (*pp != NULL) { + if (*pp == req) { + *pp = req->next; + break; + } + pp = &(*pp)->next; + } + } + + curl_multi_remove_handle(curl_multi, easy); + curl_easy_cleanup(easy); + } + } +} + +/* Callback for app launcher search */ +static void exa_launcher_callback(ExaRequest *req) +{ + if (req == NULL || req->state != AI_STATE_COMPLETED) { + notification_show("Exa Search", "Error", "Search failed", NULL, 3000); + return; + } + + if (req->result_count == 0) { + notification_show("Exa Search", "No Results", req->query, NULL, 3000); + return; + } + + /* Show results via dmenu/rofi - use bounded string operations */ + size_t choices_size = req->result_count * 300; + char *choices = dwn_malloc(choices_size); + size_t offset = 0; + choices[0] = '\0'; + + for (int i = 0; i < req->result_count; i++) { + int written = snprintf(choices + offset, choices_size - offset, + "%s%s", offset > 0 ? "\n" : "", req->results[i].title); + if (written > 0 && (size_t)written < choices_size - offset) { + offset += written; + } + } + + /* Show in dmenu - escape choices to prevent command injection */ + char *escaped_choices = shell_escape(choices); + char *cmd = dwn_malloc(strlen(escaped_choices) + 64); + snprintf(cmd, strlen(escaped_choices) + 64, "echo %s | dmenu -l 10 -p 'Results:'", escaped_choices); + + char *selected = spawn_capture(cmd); + dwn_free(cmd); + dwn_free(escaped_choices); + + if (selected != NULL && selected[0] != '\0') { + /* Find which result was selected and open URL */ + for (int i = 0; i < req->result_count; i++) { + if (strncmp(selected, req->results[i].title, strlen(req->results[i].title)) == 0) { + /* Escape URL to prevent command injection */ + char *escaped_url = shell_escape(req->results[i].url); + char *open_cmd = dwn_malloc(strlen(escaped_url) + 32); + snprintf(open_cmd, strlen(escaped_url) + 32, "xdg-open %s &", escaped_url); + spawn_async(open_cmd); + dwn_free(open_cmd); + dwn_free(escaped_url); + break; + } + } + dwn_free(selected); + } + + dwn_free(choices); + if (req->query) dwn_free(req->query); + dwn_free(req); +} + +void exa_show_app_launcher(void) +{ + if (!exa_is_available()) { + notification_show("Exa", "Unavailable", + "Set EXA_API_KEY in config to enable semantic search", + NULL, 3000); + return; + } + + /* Get search query from user */ + char *query = NULL; + + if (spawn("command -v dmenu >/dev/null 2>&1") == 0) { + query = spawn_capture("echo '' | dmenu -p 'Exa Search:'"); + } else if (spawn("command -v rofi >/dev/null 2>&1") == 0) { + query = spawn_capture("rofi -dmenu -p 'Exa Search:'"); + } else { + notification_show("Exa", "Missing Dependency", + "Install dmenu or rofi", NULL, 3000); + return; + } + + if (query == NULL || query[0] == '\0') { + if (query != NULL) dwn_free(query); + return; + } + + /* Remove trailing newline */ + query[strcspn(query, "\n")] = '\0'; + + notification_show("Exa", "Searching...", query, NULL, 2000); + exa_search(query, exa_launcher_callback); +} diff --git a/src/applauncher.c b/src/applauncher.c new file mode 100644 index 0000000..6ad02f3 --- /dev/null +++ b/src/applauncher.c @@ -0,0 +1,507 @@ +/* + * DWN - Desktop Window Manager + * Application launcher with .desktop file support + */ + +#include "applauncher.h" +#include "config.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Global state */ +static AppLauncherState launcher_state = {0}; + +/* Path to recent apps file */ +static char recent_file_path[512] = {0}; + +/* Forward declarations */ +static void scan_desktop_dir(const char *dir); +static int parse_desktop_file(const char *path, AppEntry *entry); +static void load_recent_apps(void); +static void save_recent_apps(void); +static void add_to_recent(const char *desktop_id); +static char *strip_field_codes(const char *exec); +static int compare_apps(const void *a, const void *b); + +/* ========== Initialization ========== */ + +void applauncher_init(void) +{ + memset(&launcher_state, 0, sizeof(launcher_state)); + + /* Set up recent file path */ + const char *home = getenv("HOME"); + if (home == NULL) { + struct passwd *pw = getpwuid(getuid()); + home = pw ? pw->pw_dir : "/tmp"; + } + snprintf(recent_file_path, sizeof(recent_file_path), + "%s/.local/share/dwn/recent_apps", home); + + /* Ensure directory exists */ + char dir_path[512]; + snprintf(dir_path, sizeof(dir_path), "%s/.local/share/dwn", home); + mkdir(dir_path, 0755); + + /* Load recent apps first */ + load_recent_apps(); + + /* Scan application directories */ + applauncher_refresh(); + + LOG_INFO("App launcher initialized with %d applications", launcher_state.app_count); +} + +void applauncher_cleanup(void) +{ + save_recent_apps(); +} + +void applauncher_refresh(void) +{ + launcher_state.app_count = 0; + + /* Scan system applications */ + scan_desktop_dir("/usr/share/applications"); + + /* Scan user applications (takes precedence) */ + const char *home = getenv("HOME"); + if (home) { + char user_apps[512]; + snprintf(user_apps, sizeof(user_apps), "%s/.local/share/applications", home); + scan_desktop_dir(user_apps); + } + + /* Sort apps alphabetically by name */ + qsort(launcher_state.apps, launcher_state.app_count, + sizeof(AppEntry), compare_apps); + + LOG_DEBUG("Refreshed app list: %d applications found", launcher_state.app_count); +} + +/* ========== Directory Scanning ========== */ + +static void scan_desktop_dir(const char *dir) +{ + DIR *d = opendir(dir); + if (d == NULL) { + return; + } + + struct dirent *entry; + while ((entry = readdir(d)) != NULL) { + if (launcher_state.app_count >= MAX_APPS) { + break; + } + + /* Only process .desktop files */ + size_t len = strlen(entry->d_name); + if (len < 9 || strcmp(entry->d_name + len - 8, ".desktop") != 0) { + continue; + } + + /* Build full path */ + char path[1024]; + snprintf(path, sizeof(path), "%s/%s", dir, entry->d_name); + + /* Check if already exists (user apps override system) */ + bool exists = false; + for (int i = 0; i < launcher_state.app_count; i++) { + if (strcmp(launcher_state.apps[i].desktop_id, entry->d_name) == 0) { + exists = true; + break; + } + } + if (exists) { + continue; + } + + /* Parse the desktop file */ + AppEntry app = {0}; + if (parse_desktop_file(path, &app) == 0) { + snprintf(app.desktop_id, sizeof(app.desktop_id), "%s", entry->d_name); + launcher_state.apps[launcher_state.app_count++] = app; + } + } + + closedir(d); +} + +/* ========== Desktop File Parsing ========== */ + +static int parse_desktop_file(const char *path, AppEntry *entry) +{ + FILE *f = fopen(path, "r"); + if (f == NULL) { + return -1; + } + + char line[1024]; + bool in_desktop_entry = false; + bool has_name = false; + bool has_exec = false; + + entry->hidden = false; + entry->terminal = false; + + while (fgets(line, sizeof(line), f) != NULL) { + /* Remove trailing whitespace/newline */ + size_t len = strlen(line); + while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r' || + line[len-1] == ' ' || line[len-1] == '\t')) { + line[--len] = '\0'; + } + + /* Check for section header */ + if (line[0] == '[') { + in_desktop_entry = (strcmp(line, "[Desktop Entry]") == 0); + continue; + } + + if (!in_desktop_entry) { + continue; + } + + /* Parse key=value */ + char *eq = strchr(line, '='); + if (eq == NULL) { + continue; + } + + *eq = '\0'; + char *key = line; + char *value = eq + 1; + + /* Skip localized entries (Name[en]=...) */ + if (strchr(key, '[') != NULL) { + continue; + } + + if (strcmp(key, "Name") == 0 && !has_name) { + strncpy(entry->name, value, sizeof(entry->name) - 1); + has_name = true; + } else if (strcmp(key, "Exec") == 0 && !has_exec) { + strncpy(entry->exec, value, sizeof(entry->exec) - 1); + has_exec = true; + } else if (strcmp(key, "Icon") == 0) { + strncpy(entry->icon, value, sizeof(entry->icon) - 1); + } else if (strcmp(key, "Terminal") == 0) { + entry->terminal = (strcasecmp(value, "true") == 0); + } else if (strcmp(key, "Type") == 0) { + /* Skip non-Application types */ + if (strcmp(value, "Application") != 0) { + fclose(f); + return -1; + } + } else if (strcmp(key, "Hidden") == 0 || strcmp(key, "NoDisplay") == 0) { + if (strcasecmp(value, "true") == 0) { + entry->hidden = true; + } + } + } + + fclose(f); + + /* Must have name and exec, and not be hidden */ + if (!has_name || !has_exec || entry->hidden) { + return -1; + } + + return 0; +} + +/* ========== Recent Apps Management ========== */ + +static void load_recent_apps(void) +{ + launcher_state.recent_count = 0; + + FILE *f = fopen(recent_file_path, "r"); + if (f == NULL) { + return; + } + + char line[256]; + while (fgets(line, sizeof(line), f) != NULL && + launcher_state.recent_count < MAX_RECENT_APPS) { + /* Remove newline */ + size_t len = strlen(line); + if (len > 0 && line[len-1] == '\n') { + line[len-1] = '\0'; + } + if (line[0] != '\0') { + snprintf(launcher_state.recent[launcher_state.recent_count], + sizeof(launcher_state.recent[0]), "%s", line); + launcher_state.recent_count++; + } + } + + fclose(f); + LOG_DEBUG("Loaded %d recent apps", launcher_state.recent_count); +} + +static void save_recent_apps(void) +{ + FILE *f = fopen(recent_file_path, "w"); + if (f == NULL) { + LOG_WARN("Could not save recent apps to %s", recent_file_path); + return; + } + + for (int i = 0; i < launcher_state.recent_count; i++) { + fprintf(f, "%s\n", launcher_state.recent[i]); + } + + fclose(f); +} + +static void add_to_recent(const char *desktop_id) +{ + /* Remove if already in list */ + for (int i = 0; i < launcher_state.recent_count; i++) { + if (strcmp(launcher_state.recent[i], desktop_id) == 0) { + /* Shift everything down */ + for (int j = i; j > 0; j--) { + strcpy(launcher_state.recent[j], launcher_state.recent[j-1]); + } + strcpy(launcher_state.recent[0], desktop_id); + save_recent_apps(); + return; + } + } + + /* Add to front, shift others */ + if (launcher_state.recent_count < MAX_RECENT_APPS) { + launcher_state.recent_count++; + } + + for (int i = launcher_state.recent_count - 1; i > 0; i--) { + strcpy(launcher_state.recent[i], launcher_state.recent[i-1]); + } + strncpy(launcher_state.recent[0], desktop_id, sizeof(launcher_state.recent[0]) - 1); + + save_recent_apps(); +} + +/* ========== Helper Functions ========== */ + +static char *strip_field_codes(const char *exec) +{ + static char result[512]; + size_t j = 0; + + for (size_t i = 0; exec[i] && j < sizeof(result) - 1; i++) { + if (exec[i] == '%' && exec[i+1]) { + /* Skip field codes: %f, %F, %u, %U, %d, %D, %n, %N, %i, %c, %k */ + char code = exec[i+1]; + if (code == 'f' || code == 'F' || code == 'u' || code == 'U' || + code == 'd' || code == 'D' || code == 'n' || code == 'N' || + code == 'i' || code == 'c' || code == 'k') { + i++; /* Skip the code character */ + /* Also skip trailing space if any */ + if (exec[i+1] == ' ') { + i++; + } + continue; + } + } + result[j++] = exec[i]; + } + result[j] = '\0'; + + /* Trim trailing whitespace */ + while (j > 0 && (result[j-1] == ' ' || result[j-1] == '\t')) { + result[--j] = '\0'; + } + + return result; +} + +static int compare_apps(const void *a, const void *b) +{ + const AppEntry *app_a = (const AppEntry *)a; + const AppEntry *app_b = (const AppEntry *)b; + return strcasecmp(app_a->name, app_b->name); +} + +/* Find app by desktop_id */ +static AppEntry *find_app_by_id(const char *desktop_id) +{ + for (int i = 0; i < launcher_state.app_count; i++) { + if (strcmp(launcher_state.apps[i].desktop_id, desktop_id) == 0) { + return &launcher_state.apps[i]; + } + } + return NULL; +} + +/* Find app by name */ +static AppEntry *find_app_by_name(const char *name) +{ + for (int i = 0; i < launcher_state.app_count; i++) { + if (strcmp(launcher_state.apps[i].name, name) == 0) { + return &launcher_state.apps[i]; + } + } + return NULL; +} + +/* ========== Launcher Display ========== */ + +void applauncher_show(void) +{ + if (launcher_state.app_count == 0) { + LOG_WARN("No applications found"); + return; + } + + /* Build the list for dmenu: + * - Recent apps first (if any) + * - Then all apps alphabetically + */ + size_t buf_size = launcher_state.app_count * 140; + char *choices = malloc(buf_size); + if (choices == NULL) { + LOG_ERROR("Failed to allocate memory for app list"); + return; + } + choices[0] = '\0'; + + /* Track which apps we've added (to avoid duplicates) */ + bool *added = calloc(launcher_state.app_count, sizeof(bool)); + if (added == NULL) { + free(choices); + return; + } + + size_t pos = 0; + + /* Add recent apps first */ + for (int i = 0; i < launcher_state.recent_count; i++) { + AppEntry *app = find_app_by_id(launcher_state.recent[i]); + if (app != NULL) { + /* Mark as added */ + for (int j = 0; j < launcher_state.app_count; j++) { + if (&launcher_state.apps[j] == app) { + added[j] = true; + break; + } + } + size_t name_len = strlen(app->name); + if (pos + name_len + 2 < buf_size) { + memcpy(choices + pos, app->name, name_len); + pos += name_len; + choices[pos++] = '\n'; + } + } + } + + /* Add remaining apps */ + for (int i = 0; i < launcher_state.app_count; i++) { + if (added[i]) { + continue; + } + size_t name_len = strlen(launcher_state.apps[i].name); + if (pos + name_len + 2 < buf_size) { + memcpy(choices + pos, launcher_state.apps[i].name, name_len); + pos += name_len; + choices[pos++] = '\n'; + } + } + + if (pos > 0) { + choices[pos-1] = '\0'; /* Remove trailing newline */ + } + + free(added); + + /* Write choices to a temp file to avoid shell escaping issues */ + char tmp_path[] = "/tmp/dwn_apps_XXXXXX"; + int tmp_fd = mkstemp(tmp_path); + if (tmp_fd < 0) { + free(choices); + LOG_ERROR("Failed to create temp file for app list"); + return; + } + + ssize_t written = write(tmp_fd, choices, strlen(choices)); + close(tmp_fd); + free(choices); + + if (written < 0) { + unlink(tmp_path); + LOG_ERROR("Failed to write app list to temp file"); + return; + } + + /* Run dmenu */ + char cmd[1024]; + snprintf(cmd, sizeof(cmd), + "cat '%s' | dmenu -i -l 10 -p 'Run:'; rm -f '%s'", + tmp_path, tmp_path); + + FILE *p = popen(cmd, "r"); + + if (p == NULL) { + LOG_ERROR("Failed to run dmenu"); + return; + } + + char selected[256] = {0}; + if (fgets(selected, sizeof(selected), p) != NULL) { + /* Remove trailing newline */ + size_t len = strlen(selected); + if (len > 0 && selected[len-1] == '\n') { + selected[len-1] = '\0'; + } + } + pclose(p); + + if (selected[0] == '\0') { + return; /* User cancelled */ + } + + /* Find and launch the selected app */ + AppEntry *app = find_app_by_name(selected); + if (app != NULL) { + applauncher_launch(app->desktop_id); + } else { + /* User typed a custom command */ + spawn_async(selected); + } +} + +void applauncher_launch(const char *desktop_id) +{ + AppEntry *app = find_app_by_id(desktop_id); + if (app == NULL) { + LOG_WARN("App not found: %s", desktop_id); + return; + } + + /* Strip field codes from exec */ + char *exec_cmd = strip_field_codes(app->exec); + + /* Build command */ + char cmd[1024]; + if (app->terminal) { + const char *terminal = config_get_terminal(); + snprintf(cmd, sizeof(cmd), "%s -e %s", terminal, exec_cmd); + } else { + snprintf(cmd, sizeof(cmd), "%s", exec_cmd); + } + + LOG_INFO("Launching: %s (%s)", app->name, cmd); + spawn_async(cmd); + + /* Add to recent apps */ + add_to_recent(desktop_id); +} diff --git a/src/atoms.c b/src/atoms.c new file mode 100644 index 0000000..e024882 --- /dev/null +++ b/src/atoms.c @@ -0,0 +1,463 @@ +/* + * DWN - Desktop Window Manager + * X11 Atoms management implementation + */ + +#include "atoms.h" +#include "dwn.h" +#include "util.h" + +#include +#include +#include + +/* Global atom containers */ +EWMHAtoms ewmh; +ICCCMAtoms icccm; +MiscAtoms misc_atoms; + +/* Helper macro for atom initialization */ +#define ATOM(name) XInternAtom(display, name, False) + +void atoms_init(Display *display) +{ + LOG_DEBUG("Initializing X11 atoms"); + + /* EWMH root window properties */ + ewmh.NET_SUPPORTED = ATOM("_NET_SUPPORTED"); + ewmh.NET_SUPPORTING_WM_CHECK = ATOM("_NET_SUPPORTING_WM_CHECK"); + ewmh.NET_CLIENT_LIST = ATOM("_NET_CLIENT_LIST"); + ewmh.NET_CLIENT_LIST_STACKING = ATOM("_NET_CLIENT_LIST_STACKING"); + ewmh.NET_NUMBER_OF_DESKTOPS = ATOM("_NET_NUMBER_OF_DESKTOPS"); + ewmh.NET_DESKTOP_GEOMETRY = ATOM("_NET_DESKTOP_GEOMETRY"); + ewmh.NET_DESKTOP_VIEWPORT = ATOM("_NET_DESKTOP_VIEWPORT"); + ewmh.NET_CURRENT_DESKTOP = ATOM("_NET_CURRENT_DESKTOP"); + ewmh.NET_DESKTOP_NAMES = ATOM("_NET_DESKTOP_NAMES"); + ewmh.NET_ACTIVE_WINDOW = ATOM("_NET_ACTIVE_WINDOW"); + ewmh.NET_WORKAREA = ATOM("_NET_WORKAREA"); + + /* EWMH client window properties */ + ewmh.NET_WM_NAME = ATOM("_NET_WM_NAME"); + ewmh.NET_WM_VISIBLE_NAME = ATOM("_NET_WM_VISIBLE_NAME"); + ewmh.NET_WM_DESKTOP = ATOM("_NET_WM_DESKTOP"); + ewmh.NET_WM_WINDOW_TYPE = ATOM("_NET_WM_WINDOW_TYPE"); + ewmh.NET_WM_STATE = ATOM("_NET_WM_STATE"); + ewmh.NET_WM_ALLOWED_ACTIONS = ATOM("_NET_WM_ALLOWED_ACTIONS"); + ewmh.NET_WM_STRUT = ATOM("_NET_WM_STRUT"); + ewmh.NET_WM_STRUT_PARTIAL = ATOM("_NET_WM_STRUT_PARTIAL"); + ewmh.NET_WM_PID = ATOM("_NET_WM_PID"); + + /* Window types */ + ewmh.NET_WM_WINDOW_TYPE_DESKTOP = ATOM("_NET_WM_WINDOW_TYPE_DESKTOP"); + ewmh.NET_WM_WINDOW_TYPE_DOCK = ATOM("_NET_WM_WINDOW_TYPE_DOCK"); + ewmh.NET_WM_WINDOW_TYPE_TOOLBAR = ATOM("_NET_WM_WINDOW_TYPE_TOOLBAR"); + ewmh.NET_WM_WINDOW_TYPE_MENU = ATOM("_NET_WM_WINDOW_TYPE_MENU"); + ewmh.NET_WM_WINDOW_TYPE_UTILITY = ATOM("_NET_WM_WINDOW_TYPE_UTILITY"); + ewmh.NET_WM_WINDOW_TYPE_SPLASH = ATOM("_NET_WM_WINDOW_TYPE_SPLASH"); + ewmh.NET_WM_WINDOW_TYPE_DIALOG = ATOM("_NET_WM_WINDOW_TYPE_DIALOG"); + ewmh.NET_WM_WINDOW_TYPE_NORMAL = ATOM("_NET_WM_WINDOW_TYPE_NORMAL"); + ewmh.NET_WM_WINDOW_TYPE_NOTIFICATION = ATOM("_NET_WM_WINDOW_TYPE_NOTIFICATION"); + + /* Window states */ + ewmh.NET_WM_STATE_MODAL = ATOM("_NET_WM_STATE_MODAL"); + ewmh.NET_WM_STATE_STICKY = ATOM("_NET_WM_STATE_STICKY"); + ewmh.NET_WM_STATE_MAXIMIZED_VERT = ATOM("_NET_WM_STATE_MAXIMIZED_VERT"); + ewmh.NET_WM_STATE_MAXIMIZED_HORZ = ATOM("_NET_WM_STATE_MAXIMIZED_HORZ"); + ewmh.NET_WM_STATE_SHADED = ATOM("_NET_WM_STATE_SHADED"); + ewmh.NET_WM_STATE_SKIP_TASKBAR = ATOM("_NET_WM_STATE_SKIP_TASKBAR"); + ewmh.NET_WM_STATE_SKIP_PAGER = ATOM("_NET_WM_STATE_SKIP_PAGER"); + ewmh.NET_WM_STATE_HIDDEN = ATOM("_NET_WM_STATE_HIDDEN"); + ewmh.NET_WM_STATE_FULLSCREEN = ATOM("_NET_WM_STATE_FULLSCREEN"); + ewmh.NET_WM_STATE_ABOVE = ATOM("_NET_WM_STATE_ABOVE"); + ewmh.NET_WM_STATE_BELOW = ATOM("_NET_WM_STATE_BELOW"); + ewmh.NET_WM_STATE_DEMANDS_ATTENTION = ATOM("_NET_WM_STATE_DEMANDS_ATTENTION"); + ewmh.NET_WM_STATE_FOCUSED = ATOM("_NET_WM_STATE_FOCUSED"); + + /* Actions */ + ewmh.NET_WM_ACTION_MOVE = ATOM("_NET_WM_ACTION_MOVE"); + ewmh.NET_WM_ACTION_RESIZE = ATOM("_NET_WM_ACTION_RESIZE"); + ewmh.NET_WM_ACTION_MINIMIZE = ATOM("_NET_WM_ACTION_MINIMIZE"); + ewmh.NET_WM_ACTION_SHADE = ATOM("_NET_WM_ACTION_SHADE"); + ewmh.NET_WM_ACTION_STICK = ATOM("_NET_WM_ACTION_STICK"); + ewmh.NET_WM_ACTION_MAXIMIZE_HORZ = ATOM("_NET_WM_ACTION_MAXIMIZE_HORZ"); + ewmh.NET_WM_ACTION_MAXIMIZE_VERT = ATOM("_NET_WM_ACTION_MAXIMIZE_VERT"); + ewmh.NET_WM_ACTION_FULLSCREEN = ATOM("_NET_WM_ACTION_FULLSCREEN"); + ewmh.NET_WM_ACTION_CHANGE_DESKTOP = ATOM("_NET_WM_ACTION_CHANGE_DESKTOP"); + ewmh.NET_WM_ACTION_CLOSE = ATOM("_NET_WM_ACTION_CLOSE"); + + /* Client messages */ + ewmh.NET_CLOSE_WINDOW = ATOM("_NET_CLOSE_WINDOW"); + ewmh.NET_MOVERESIZE_WINDOW = ATOM("_NET_MOVERESIZE_WINDOW"); + ewmh.NET_WM_MOVERESIZE = ATOM("_NET_WM_MOVERESIZE"); + ewmh.NET_REQUEST_FRAME_EXTENTS = ATOM("_NET_REQUEST_FRAME_EXTENTS"); + ewmh.NET_FRAME_EXTENTS = ATOM("_NET_FRAME_EXTENTS"); + + /* System tray */ + ewmh.NET_SYSTEM_TRAY_OPCODE = ATOM("_NET_SYSTEM_TRAY_OPCODE"); + ewmh.NET_SYSTEM_TRAY_S0 = ATOM("_NET_SYSTEM_TRAY_S0"); + ewmh.MANAGER = ATOM("MANAGER"); + ewmh.XEMBED = ATOM("_XEMBED"); + ewmh.XEMBED_INFO = ATOM("_XEMBED_INFO"); + + /* ICCCM atoms */ + icccm.WM_PROTOCOLS = ATOM("WM_PROTOCOLS"); + icccm.WM_DELETE_WINDOW = ATOM("WM_DELETE_WINDOW"); + icccm.WM_TAKE_FOCUS = ATOM("WM_TAKE_FOCUS"); + icccm.WM_STATE = ATOM("WM_STATE"); + icccm.WM_CHANGE_STATE = ATOM("WM_CHANGE_STATE"); + icccm.WM_CLASS = ATOM("WM_CLASS"); + icccm.WM_NAME = ATOM("WM_NAME"); + icccm.WM_TRANSIENT_FOR = ATOM("WM_TRANSIENT_FOR"); + icccm.WM_CLIENT_LEADER = ATOM("WM_CLIENT_LEADER"); + icccm.WM_WINDOW_ROLE = ATOM("WM_WINDOW_ROLE"); + + /* Misc atoms */ + misc_atoms.UTF8_STRING = ATOM("UTF8_STRING"); + misc_atoms.COMPOUND_TEXT = ATOM("COMPOUND_TEXT"); + misc_atoms.MOTIF_WM_HINTS = ATOM("_MOTIF_WM_HINTS"); + misc_atoms.CLIPBOARD = ATOM("CLIPBOARD"); + misc_atoms.PRIMARY = ATOM("PRIMARY"); + misc_atoms.DWN_RESTART = ATOM("_DWN_RESTART"); + + LOG_DEBUG("X11 atoms initialized"); +} + +/* ========== EWMH Setup ========== */ + +void atoms_setup_ewmh(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + Display *dpy = dwn->display; + Window root = dwn->root; + + /* Create a check window for _NET_SUPPORTING_WM_CHECK */ + Window check = XCreateSimpleWindow(dpy, root, 0, 0, 1, 1, 0, 0, 0); + + /* Set _NET_SUPPORTING_WM_CHECK on root and check window */ + XChangeProperty(dpy, root, ewmh.NET_SUPPORTING_WM_CHECK, + XA_WINDOW, 32, PropModeReplace, + (unsigned char *)&check, 1); + XChangeProperty(dpy, check, ewmh.NET_SUPPORTING_WM_CHECK, + XA_WINDOW, 32, PropModeReplace, + (unsigned char *)&check, 1); + + /* Set _NET_WM_NAME on check window */ + XChangeProperty(dpy, check, ewmh.NET_WM_NAME, + misc_atoms.UTF8_STRING, 8, PropModeReplace, + (unsigned char *)DWN_NAME, strlen(DWN_NAME)); + + /* Set supported atoms */ + Atom supported[] = { + ewmh.NET_SUPPORTED, + ewmh.NET_SUPPORTING_WM_CHECK, + ewmh.NET_CLIENT_LIST, + ewmh.NET_CLIENT_LIST_STACKING, + ewmh.NET_NUMBER_OF_DESKTOPS, + ewmh.NET_CURRENT_DESKTOP, + ewmh.NET_DESKTOP_NAMES, + ewmh.NET_ACTIVE_WINDOW, + ewmh.NET_WM_NAME, + ewmh.NET_WM_DESKTOP, + ewmh.NET_WM_WINDOW_TYPE, + ewmh.NET_WM_STATE, + ewmh.NET_WM_STATE_FULLSCREEN, + ewmh.NET_WM_STATE_HIDDEN, + ewmh.NET_WM_STATE_DEMANDS_ATTENTION, + ewmh.NET_CLOSE_WINDOW, + ewmh.NET_WM_STRUT_PARTIAL, + ewmh.NET_FRAME_EXTENTS + }; + + XChangeProperty(dpy, root, ewmh.NET_SUPPORTED, + XA_ATOM, 32, PropModeReplace, + (unsigned char *)supported, sizeof(supported) / sizeof(Atom)); + + /* Set number of desktops */ + atoms_set_number_of_desktops(MAX_WORKSPACES); + + /* Set current desktop */ + atoms_set_current_desktop(0); + + /* Update desktop names */ + atoms_update_desktop_names(); + + LOG_INFO("EWMH compliance initialized"); +} + +void atoms_update_client_list(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + Window windows[MAX_CLIENTS]; + int count = 0; + + for (Client *c = dwn->client_list; c != NULL && count < MAX_CLIENTS; c = c->next) { + windows[count++] = c->window; + } + + XChangeProperty(dwn->display, dwn->root, ewmh.NET_CLIENT_LIST, + XA_WINDOW, 32, PropModeReplace, + (unsigned char *)windows, count); + + XChangeProperty(dwn->display, dwn->root, ewmh.NET_CLIENT_LIST_STACKING, + XA_WINDOW, 32, PropModeReplace, + (unsigned char *)windows, count); +} + +void atoms_update_desktop_names(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + /* Build null-separated list of workspace names */ + char names[MAX_WORKSPACES * 32]; + int offset = 0; + + for (int i = 0; i < MAX_WORKSPACES; i++) { + const char *name = dwn->workspaces[i].name; + if (name[0] == '\0') { + offset += snprintf(names + offset, sizeof(names) - offset, "%d", i + 1); + } else { + offset += snprintf(names + offset, sizeof(names) - offset, "%s", name); + } + names[offset++] = '\0'; + } + + XChangeProperty(dwn->display, dwn->root, ewmh.NET_DESKTOP_NAMES, + misc_atoms.UTF8_STRING, 8, PropModeReplace, + (unsigned char *)names, offset); +} + +void atoms_set_current_desktop(int desktop) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + long data = desktop; + XChangeProperty(dwn->display, dwn->root, ewmh.NET_CURRENT_DESKTOP, + XA_CARDINAL, 32, PropModeReplace, + (unsigned char *)&data, 1); +} + +void atoms_set_active_window(Window window) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + XChangeProperty(dwn->display, dwn->root, ewmh.NET_ACTIVE_WINDOW, + XA_WINDOW, 32, PropModeReplace, + (unsigned char *)&window, 1); +} + +void atoms_set_number_of_desktops(int count) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + long data = count; + XChangeProperty(dwn->display, dwn->root, ewmh.NET_NUMBER_OF_DESKTOPS, + XA_CARDINAL, 32, PropModeReplace, + (unsigned char *)&data, 1); +} + +/* ========== Window property helpers ========== */ + +bool atoms_get_window_type(Window window, Atom *type) +{ + if (dwn == NULL || dwn->display == NULL || type == NULL) { + return false; + } + + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + + if (XGetWindowProperty(dwn->display, window, ewmh.NET_WM_WINDOW_TYPE, + 0, 1, False, XA_ATOM, + &actual_type, &actual_format, &nitems, &bytes_after, + &data) == Success && data != NULL) { + *type = *(Atom *)data; + XFree(data); + return true; + } + + return false; +} + +bool atoms_get_window_desktop(Window window, int *desktop) +{ + if (dwn == NULL || dwn->display == NULL || desktop == NULL) { + return false; + } + + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + + if (XGetWindowProperty(dwn->display, window, ewmh.NET_WM_DESKTOP, + 0, 1, False, XA_CARDINAL, + &actual_type, &actual_format, &nitems, &bytes_after, + &data) == Success && data != NULL) { + *desktop = *(int *)data; + XFree(data); + return true; + } + + return false; +} + +bool atoms_set_window_desktop(Window window, int desktop) +{ + if (dwn == NULL || dwn->display == NULL) { + return false; + } + + long data = desktop; + XChangeProperty(dwn->display, window, ewmh.NET_WM_DESKTOP, + XA_CARDINAL, 32, PropModeReplace, + (unsigned char *)&data, 1); + return true; +} + +char *atoms_get_window_name(Window window) +{ + if (dwn == NULL || dwn->display == NULL) { + return NULL; + } + + Display *dpy = dwn->display; + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + + /* Try _NET_WM_NAME first (UTF-8) */ + if (XGetWindowProperty(dpy, window, ewmh.NET_WM_NAME, + 0, 256, False, misc_atoms.UTF8_STRING, + &actual_type, &actual_format, &nitems, &bytes_after, + &data) == Success && data != NULL) { + char *name = dwn_strdup((char *)data); + XFree(data); + return name; + } + + /* Fall back to WM_NAME */ + if (XGetWindowProperty(dpy, window, icccm.WM_NAME, + 0, 256, False, XA_STRING, + &actual_type, &actual_format, &nitems, &bytes_after, + &data) == Success && data != NULL) { + char *name = dwn_strdup((char *)data); + XFree(data); + return name; + } + + /* Last resort: XFetchName */ + char *name = NULL; + if (XFetchName(dpy, window, &name) && name != NULL) { + char *result = dwn_strdup(name); + XFree(name); + return result; + } + + return dwn_strdup("Untitled"); +} + +bool atoms_get_wm_class(Window window, char *class_name, char *instance_name, size_t len) +{ + if (dwn == NULL || dwn->display == NULL) { + return false; + } + + XClassHint hint; + if (XGetClassHint(dwn->display, window, &hint)) { + if (class_name != NULL && hint.res_class != NULL) { + strncpy(class_name, hint.res_class, len - 1); + class_name[len - 1] = '\0'; + } + if (instance_name != NULL && hint.res_name != NULL) { + strncpy(instance_name, hint.res_name, len - 1); + instance_name[len - 1] = '\0'; + } + if (hint.res_class) XFree(hint.res_class); + if (hint.res_name) XFree(hint.res_name); + return true; + } + + return false; +} + +/* ========== Protocol helpers ========== */ + +bool atoms_window_supports_protocol(Window window, Atom protocol) +{ + if (dwn == NULL || dwn->display == NULL) { + return false; + } + + Atom *protocols = NULL; + int count = 0; + + if (XGetWMProtocols(dwn->display, window, &protocols, &count)) { + for (int i = 0; i < count; i++) { + if (protocols[i] == protocol) { + XFree(protocols); + return true; + } + } + XFree(protocols); + } + + return false; +} + +void atoms_send_protocol(Window window, Atom protocol, Time timestamp) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + XEvent ev; + memset(&ev, 0, sizeof(ev)); + ev.type = ClientMessage; + ev.xclient.window = window; + ev.xclient.message_type = icccm.WM_PROTOCOLS; + ev.xclient.format = 32; + ev.xclient.data.l[0] = protocol; + ev.xclient.data.l[1] = timestamp; + + XSendEvent(dwn->display, window, False, NoEventMask, &ev); +} + +void atoms_send_client_message(Window window, Atom message_type, + long data0, long data1, long data2, long data3, long data4) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + XEvent ev; + memset(&ev, 0, sizeof(ev)); + ev.type = ClientMessage; + ev.xclient.window = window; + ev.xclient.message_type = message_type; + ev.xclient.format = 32; + ev.xclient.data.l[0] = data0; + ev.xclient.data.l[1] = data1; + ev.xclient.data.l[2] = data2; + ev.xclient.data.l[3] = data3; + ev.xclient.data.l[4] = data4; + + XSendEvent(dwn->display, dwn->root, False, + SubstructureNotifyMask | SubstructureRedirectMask, &ev); +} diff --git a/src/cJSON.c b/src/cJSON.c new file mode 100644 index 0000000..6e4fb0d --- /dev/null +++ b/src/cJSON.c @@ -0,0 +1,3191 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* cJSON */ +/* JSON parser in C. */ + +/* disable warnings about old C89 functions in MSVC */ +#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER) +#define _CRT_SECURE_NO_DEPRECATE +#endif + +#ifdef __GNUC__ +#pragma GCC visibility push(default) +#endif +#if defined(_MSC_VER) +#pragma warning (push) +/* disable warning about single line comments in system headers */ +#pragma warning (disable : 4001) +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_LOCALES +#include +#endif + +#if defined(_MSC_VER) +#pragma warning (pop) +#endif +#ifdef __GNUC__ +#pragma GCC visibility pop +#endif + +#include "cJSON.h" + +/* define our own boolean type */ +#ifdef true +#undef true +#endif +#define true ((cJSON_bool)1) + +#ifdef false +#undef false +#endif +#define false ((cJSON_bool)0) + +/* define isnan and isinf for ANSI C, if in C99 or above, isnan and isinf has been defined in math.h */ +#ifndef isinf +#define isinf(d) (isnan((d - d)) && !isnan(d)) +#endif +#ifndef isnan +#define isnan(d) (d != d) +#endif + +#ifndef NAN +#ifdef _WIN32 +#define NAN sqrt(-1.0) +#else +#define NAN 0.0/0.0 +#endif +#endif + +typedef struct { + const unsigned char *json; + size_t position; +} error; +static error global_error = { NULL, 0 }; + +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void) +{ + return (const char*) (global_error.json + global_error.position); +} + +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item) +{ + if (!cJSON_IsString(item)) + { + return NULL; + } + + return item->valuestring; +} + +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item) +{ + if (!cJSON_IsNumber(item)) + { + return (double) NAN; + } + + return item->valuedouble; +} + +/* This is a safeguard to prevent copy-pasters from using incompatible C and header files */ +#if (CJSON_VERSION_MAJOR != 1) || (CJSON_VERSION_MINOR != 7) || (CJSON_VERSION_PATCH != 19) + #error cJSON.h and cJSON.c have different versions. Make sure that both have the same. +#endif + +CJSON_PUBLIC(const char*) cJSON_Version(void) +{ + static char version[15]; + sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); + + return version; +} + +/* Case insensitive string comparison, doesn't consider two NULL pointers equal though */ +static int case_insensitive_strcmp(const unsigned char *string1, const unsigned char *string2) +{ + if ((string1 == NULL) || (string2 == NULL)) + { + return 1; + } + + if (string1 == string2) + { + return 0; + } + + for(; tolower(*string1) == tolower(*string2); (void)string1++, string2++) + { + if (*string1 == '\0') + { + return 0; + } + } + + return tolower(*string1) - tolower(*string2); +} + +typedef struct internal_hooks +{ + void *(CJSON_CDECL *allocate)(size_t size); + void (CJSON_CDECL *deallocate)(void *pointer); + void *(CJSON_CDECL *reallocate)(void *pointer, size_t size); +} internal_hooks; + +#if defined(_MSC_VER) +/* work around MSVC error C2322: '...' address of dllimport '...' is not static */ +static void * CJSON_CDECL internal_malloc(size_t size) +{ + return malloc(size); +} +static void CJSON_CDECL internal_free(void *pointer) +{ + free(pointer); +} +static void * CJSON_CDECL internal_realloc(void *pointer, size_t size) +{ + return realloc(pointer, size); +} +#else +#define internal_malloc malloc +#define internal_free free +#define internal_realloc realloc +#endif + +/* strlen of character literals resolved at compile time */ +#define static_strlen(string_literal) (sizeof(string_literal) - sizeof("")) + +static internal_hooks global_hooks = { internal_malloc, internal_free, internal_realloc }; + +static unsigned char* cJSON_strdup(const unsigned char* string, const internal_hooks * const hooks) +{ + size_t length = 0; + unsigned char *copy = NULL; + + if (string == NULL) + { + return NULL; + } + + length = strlen((const char*)string) + sizeof(""); + copy = (unsigned char*)hooks->allocate(length); + if (copy == NULL) + { + return NULL; + } + memcpy(copy, string, length); + + return copy; +} + +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks) +{ + if (hooks == NULL) + { + /* Reset hooks */ + global_hooks.allocate = malloc; + global_hooks.deallocate = free; + global_hooks.reallocate = realloc; + return; + } + + global_hooks.allocate = malloc; + if (hooks->malloc_fn != NULL) + { + global_hooks.allocate = hooks->malloc_fn; + } + + global_hooks.deallocate = free; + if (hooks->free_fn != NULL) + { + global_hooks.deallocate = hooks->free_fn; + } + + /* use realloc only if both free and malloc are used */ + global_hooks.reallocate = NULL; + if ((global_hooks.allocate == malloc) && (global_hooks.deallocate == free)) + { + global_hooks.reallocate = realloc; + } +} + +/* Internal constructor. */ +static cJSON *cJSON_New_Item(const internal_hooks * const hooks) +{ + cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON)); + if (node) + { + memset(node, '\0', sizeof(cJSON)); + } + + return node; +} + +/* Delete a cJSON structure. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item) +{ + cJSON *next = NULL; + while (item != NULL) + { + next = item->next; + if (!(item->type & cJSON_IsReference) && (item->child != NULL)) + { + cJSON_Delete(item->child); + } + if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL)) + { + global_hooks.deallocate(item->valuestring); + item->valuestring = NULL; + } + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + global_hooks.deallocate(item->string); + item->string = NULL; + } + global_hooks.deallocate(item); + item = next; + } +} + +/* get the decimal point character of the current locale */ +static unsigned char get_decimal_point(void) +{ +#ifdef ENABLE_LOCALES + struct lconv *lconv = localeconv(); + return (unsigned char) lconv->decimal_point[0]; +#else + return '.'; +#endif +} + +typedef struct +{ + const unsigned char *content; + size_t length; + size_t offset; + size_t depth; /* How deeply nested (in arrays/objects) is the input at the current offset. */ + internal_hooks hooks; +} parse_buffer; + +/* check if the given size is left to read in a given parse buffer (starting with 1) */ +#define can_read(buffer, size) ((buffer != NULL) && (((buffer)->offset + size) <= (buffer)->length)) +/* check if the buffer can be accessed at the given index (starting with 0) */ +#define can_access_at_index(buffer, index) ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length)) +#define cannot_access_at_index(buffer, index) (!can_access_at_index(buffer, index)) +/* get a pointer to the buffer at the position */ +#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset) + +/* Parse the input text to generate a number, and populate the result into item. */ +static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer) +{ + double number = 0; + unsigned char *after_end = NULL; + unsigned char *number_c_string; + unsigned char decimal_point = get_decimal_point(); + size_t i = 0; + size_t number_string_length = 0; + cJSON_bool has_decimal_point = false; + + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; + } + + /* copy the number into a temporary buffer and replace '.' with the decimal point + * of the current locale (for strtod) + * This also takes care of '\0' not necessarily being available for marking the end of the input */ + for (i = 0; can_access_at_index(input_buffer, i); i++) + { + switch (buffer_at_offset(input_buffer)[i]) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '+': + case '-': + case 'e': + case 'E': + number_string_length++; + break; + + case '.': + number_string_length++; + has_decimal_point = true; + break; + + default: + goto loop_end; + } + } +loop_end: + /* malloc for temporary buffer, add 1 for '\0' */ + number_c_string = (unsigned char *) input_buffer->hooks.allocate(number_string_length + 1); + if (number_c_string == NULL) + { + return false; /* allocation failure */ + } + + memcpy(number_c_string, buffer_at_offset(input_buffer), number_string_length); + number_c_string[number_string_length] = '\0'; + + if (has_decimal_point) + { + for (i = 0; i < number_string_length; i++) + { + if (number_c_string[i] == '.') + { + /* replace '.' with the decimal point of the current locale (for strtod) */ + number_c_string[i] = decimal_point; + } + } + } + + number = strtod((const char*)number_c_string, (char**)&after_end); + if (number_c_string == after_end) + { + /* free the temporary buffer */ + input_buffer->hooks.deallocate(number_c_string); + return false; /* parse_error */ + } + + item->valuedouble = number; + + /* use saturation in case of overflow */ + if (number >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)number; + } + + item->type = cJSON_Number; + + input_buffer->offset += (size_t)(after_end - number_c_string); + /* free the temporary buffer */ + input_buffer->hooks.deallocate(number_c_string); + return true; +} + +/* don't ask me, but the original cJSON_SetNumberValue returns an integer or double */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number) +{ + if (number >= INT_MAX) + { + object->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + object->valueint = INT_MIN; + } + else + { + object->valueint = (int)number; + } + + return object->valuedouble = number; +} + +/* Note: when passing a NULL valuestring, cJSON_SetValuestring treats this as an error and return NULL */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring) +{ + char *copy = NULL; + size_t v1_len; + size_t v2_len; + /* if object's type is not cJSON_String or is cJSON_IsReference, it should not set valuestring */ + if ((object == NULL) || !(object->type & cJSON_String) || (object->type & cJSON_IsReference)) + { + return NULL; + } + /* return NULL if the object is corrupted or valuestring is NULL */ + if (object->valuestring == NULL || valuestring == NULL) + { + return NULL; + } + + v1_len = strlen(valuestring); + v2_len = strlen(object->valuestring); + + if (v1_len <= v2_len) + { + /* strcpy does not handle overlapping string: [X1, X2] [Y1, Y2] => X2 < Y1 or Y2 < X1 */ + if (!( valuestring + v1_len < object->valuestring || object->valuestring + v2_len < valuestring )) + { + return NULL; + } + strcpy(object->valuestring, valuestring); + return object->valuestring; + } + copy = (char*) cJSON_strdup((const unsigned char*)valuestring, &global_hooks); + if (copy == NULL) + { + return NULL; + } + if (object->valuestring != NULL) + { + cJSON_free(object->valuestring); + } + object->valuestring = copy; + + return copy; +} + +typedef struct +{ + unsigned char *buffer; + size_t length; + size_t offset; + size_t depth; /* current nesting depth (for formatted printing) */ + cJSON_bool noalloc; + cJSON_bool format; /* is this print a formatted print */ + internal_hooks hooks; +} printbuffer; + +/* realloc printbuffer if necessary to have at least "needed" bytes more */ +static unsigned char* ensure(printbuffer * const p, size_t needed) +{ + unsigned char *newbuffer = NULL; + size_t newsize = 0; + + if ((p == NULL) || (p->buffer == NULL)) + { + return NULL; + } + + if ((p->length > 0) && (p->offset >= p->length)) + { + /* make sure that offset is valid */ + return NULL; + } + + if (needed > INT_MAX) + { + /* sizes bigger than INT_MAX are currently not supported */ + return NULL; + } + + needed += p->offset + 1; + if (needed <= p->length) + { + return p->buffer + p->offset; + } + + if (p->noalloc) { + return NULL; + } + + /* calculate new buffer size */ + if (needed > (INT_MAX / 2)) + { + /* overflow of int, use INT_MAX if possible */ + if (needed <= INT_MAX) + { + newsize = INT_MAX; + } + else + { + return NULL; + } + } + else + { + newsize = needed * 2; + } + + if (p->hooks.reallocate != NULL) + { + /* reallocate with realloc if available */ + newbuffer = (unsigned char*)p->hooks.reallocate(p->buffer, newsize); + if (newbuffer == NULL) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + } + else + { + /* otherwise reallocate manually */ + newbuffer = (unsigned char*)p->hooks.allocate(newsize); + if (!newbuffer) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + + memcpy(newbuffer, p->buffer, p->offset + 1); + p->hooks.deallocate(p->buffer); + } + p->length = newsize; + p->buffer = newbuffer; + + return newbuffer + p->offset; +} + +/* calculate the new length of the string in a printbuffer and update the offset */ +static void update_offset(printbuffer * const buffer) +{ + const unsigned char *buffer_pointer = NULL; + if ((buffer == NULL) || (buffer->buffer == NULL)) + { + return; + } + buffer_pointer = buffer->buffer + buffer->offset; + + buffer->offset += strlen((const char*)buffer_pointer); +} + +/* securely comparison of floating-point variables */ +static cJSON_bool compare_double(double a, double b) +{ + double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b); + return (fabs(a - b) <= maxVal * DBL_EPSILON); +} + +/* Render the number nicely from the given item into a string. */ +static cJSON_bool print_number(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + double d = item->valuedouble; + int length = 0; + size_t i = 0; + unsigned char number_buffer[26] = {0}; /* temporary buffer to print the number into */ + unsigned char decimal_point = get_decimal_point(); + double test = 0.0; + + if (output_buffer == NULL) + { + return false; + } + + /* This checks for NaN and Infinity */ + if (isnan(d) || isinf(d)) + { + length = sprintf((char*)number_buffer, "null"); + } + else if(d == (double)item->valueint) + { + length = sprintf((char*)number_buffer, "%d", item->valueint); + } + else + { + /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ + length = sprintf((char*)number_buffer, "%1.15g", d); + + /* Check whether the original double can be recovered */ + if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) + { + /* If not, print with 17 decimal places of precision */ + length = sprintf((char*)number_buffer, "%1.17g", d); + } + } + + /* sprintf failed or buffer overrun occurred */ + if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1))) + { + return false; + } + + /* reserve appropriate space in the output */ + output_pointer = ensure(output_buffer, (size_t)length + sizeof("")); + if (output_pointer == NULL) + { + return false; + } + + /* copy the printed number to the output and replace locale + * dependent decimal point with '.' */ + for (i = 0; i < ((size_t)length); i++) + { + if (number_buffer[i] == decimal_point) + { + output_pointer[i] = '.'; + continue; + } + + output_pointer[i] = number_buffer[i]; + } + output_pointer[i] = '\0'; + + output_buffer->offset += (size_t)length; + + return true; +} + +/* parse 4 digit hexadecimal number */ +static unsigned parse_hex4(const unsigned char * const input) +{ + unsigned int h = 0; + size_t i = 0; + + for (i = 0; i < 4; i++) + { + /* parse digit */ + if ((input[i] >= '0') && (input[i] <= '9')) + { + h += (unsigned int) input[i] - '0'; + } + else if ((input[i] >= 'A') && (input[i] <= 'F')) + { + h += (unsigned int) 10 + input[i] - 'A'; + } + else if ((input[i] >= 'a') && (input[i] <= 'f')) + { + h += (unsigned int) 10 + input[i] - 'a'; + } + else /* invalid */ + { + return 0; + } + + if (i < 3) + { + /* shift left to make place for the next nibble */ + h = h << 4; + } + } + + return h; +} + +/* converts a UTF-16 literal to UTF-8 + * A literal can be one or two sequences of the form \uXXXX */ +static unsigned char utf16_literal_to_utf8(const unsigned char * const input_pointer, const unsigned char * const input_end, unsigned char **output_pointer) +{ + long unsigned int codepoint = 0; + unsigned int first_code = 0; + const unsigned char *first_sequence = input_pointer; + unsigned char utf8_length = 0; + unsigned char utf8_position = 0; + unsigned char sequence_length = 0; + unsigned char first_byte_mark = 0; + + if ((input_end - first_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + /* get the first utf16 sequence */ + first_code = parse_hex4(first_sequence + 2); + + /* check that the code is valid */ + if (((first_code >= 0xDC00) && (first_code <= 0xDFFF))) + { + goto fail; + } + + /* UTF16 surrogate pair */ + if ((first_code >= 0xD800) && (first_code <= 0xDBFF)) + { + const unsigned char *second_sequence = first_sequence + 6; + unsigned int second_code = 0; + sequence_length = 12; /* \uXXXX\uXXXX */ + + if ((input_end - second_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + if ((second_sequence[0] != '\\') || (second_sequence[1] != 'u')) + { + /* missing second half of the surrogate pair */ + goto fail; + } + + /* get the second utf16 sequence */ + second_code = parse_hex4(second_sequence + 2); + /* check that the code is valid */ + if ((second_code < 0xDC00) || (second_code > 0xDFFF)) + { + /* invalid second half of the surrogate pair */ + goto fail; + } + + + /* calculate the unicode codepoint from the surrogate pair */ + codepoint = 0x10000 + (((first_code & 0x3FF) << 10) | (second_code & 0x3FF)); + } + else + { + sequence_length = 6; /* \uXXXX */ + codepoint = first_code; + } + + /* encode as UTF-8 + * takes at maximum 4 bytes to encode: + * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ + if (codepoint < 0x80) + { + /* normal ascii, encoding 0xxxxxxx */ + utf8_length = 1; + } + else if (codepoint < 0x800) + { + /* two bytes, encoding 110xxxxx 10xxxxxx */ + utf8_length = 2; + first_byte_mark = 0xC0; /* 11000000 */ + } + else if (codepoint < 0x10000) + { + /* three bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx */ + utf8_length = 3; + first_byte_mark = 0xE0; /* 11100000 */ + } + else if (codepoint <= 0x10FFFF) + { + /* four bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx */ + utf8_length = 4; + first_byte_mark = 0xF0; /* 11110000 */ + } + else + { + /* invalid unicode codepoint */ + goto fail; + } + + /* encode as utf8 */ + for (utf8_position = (unsigned char)(utf8_length - 1); utf8_position > 0; utf8_position--) + { + /* 10xxxxxx */ + (*output_pointer)[utf8_position] = (unsigned char)((codepoint | 0x80) & 0xBF); + codepoint >>= 6; + } + /* encode first byte */ + if (utf8_length > 1) + { + (*output_pointer)[0] = (unsigned char)((codepoint | first_byte_mark) & 0xFF); + } + else + { + (*output_pointer)[0] = (unsigned char)(codepoint & 0x7F); + } + + *output_pointer += utf8_length; + + return sequence_length; + +fail: + return 0; +} + +/* Parse the input text into an unescaped cinput, and populate item. */ +static cJSON_bool parse_string(cJSON * const item, parse_buffer * const input_buffer) +{ + const unsigned char *input_pointer = buffer_at_offset(input_buffer) + 1; + const unsigned char *input_end = buffer_at_offset(input_buffer) + 1; + unsigned char *output_pointer = NULL; + unsigned char *output = NULL; + + /* not a string */ + if (buffer_at_offset(input_buffer)[0] != '\"') + { + goto fail; + } + + { + /* calculate approximate size of the output (overestimate) */ + size_t allocation_length = 0; + size_t skipped_bytes = 0; + while (((size_t)(input_end - input_buffer->content) < input_buffer->length) && (*input_end != '\"')) + { + /* is escape sequence */ + if (input_end[0] == '\\') + { + if ((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length) + { + /* prevent buffer overflow when last input character is a backslash */ + goto fail; + } + skipped_bytes++; + input_end++; + } + input_end++; + } + if (((size_t)(input_end - input_buffer->content) >= input_buffer->length) || (*input_end != '\"')) + { + goto fail; /* string ended unexpectedly */ + } + + /* This is at most how much we need for the output */ + allocation_length = (size_t) (input_end - buffer_at_offset(input_buffer)) - skipped_bytes; + output = (unsigned char*)input_buffer->hooks.allocate(allocation_length + sizeof("")); + if (output == NULL) + { + goto fail; /* allocation failure */ + } + } + + output_pointer = output; + /* loop through the string literal */ + while (input_pointer < input_end) + { + if (*input_pointer != '\\') + { + *output_pointer++ = *input_pointer++; + } + /* escape sequence */ + else + { + unsigned char sequence_length = 2; + if ((input_end - input_pointer) < 1) + { + goto fail; + } + + switch (input_pointer[1]) + { + case 'b': + *output_pointer++ = '\b'; + break; + case 'f': + *output_pointer++ = '\f'; + break; + case 'n': + *output_pointer++ = '\n'; + break; + case 'r': + *output_pointer++ = '\r'; + break; + case 't': + *output_pointer++ = '\t'; + break; + case '\"': + case '\\': + case '/': + *output_pointer++ = input_pointer[1]; + break; + + /* UTF-16 literal */ + case 'u': + sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer); + if (sequence_length == 0) + { + /* failed to convert UTF16-literal to UTF-8 */ + goto fail; + } + break; + + default: + goto fail; + } + input_pointer += sequence_length; + } + } + + /* zero terminate the output */ + *output_pointer = '\0'; + + item->type = cJSON_String; + item->valuestring = (char*)output; + + input_buffer->offset = (size_t) (input_end - input_buffer->content); + input_buffer->offset++; + + return true; + +fail: + if (output != NULL) + { + input_buffer->hooks.deallocate(output); + output = NULL; + } + + if (input_pointer != NULL) + { + input_buffer->offset = (size_t)(input_pointer - input_buffer->content); + } + + return false; +} + +/* Render the cstring provided to an escaped version that can be printed. */ +static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer) +{ + const unsigned char *input_pointer = NULL; + unsigned char *output = NULL; + unsigned char *output_pointer = NULL; + size_t output_length = 0; + /* numbers of additional characters needed for escaping */ + size_t escape_characters = 0; + + if (output_buffer == NULL) + { + return false; + } + + /* empty string */ + if (input == NULL) + { + output = ensure(output_buffer, sizeof("\"\"")); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "\"\""); + + return true; + } + + /* set "flag" to 1 if something needs to be escaped */ + for (input_pointer = input; *input_pointer; input_pointer++) + { + switch (*input_pointer) + { + case '\"': + case '\\': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + /* one character escape sequence */ + escape_characters++; + break; + default: + if (*input_pointer < 32) + { + /* UTF-16 escape sequence uXXXX */ + escape_characters += 5; + } + break; + } + } + output_length = (size_t)(input_pointer - input) + escape_characters; + + output = ensure(output_buffer, output_length + sizeof("\"\"")); + if (output == NULL) + { + return false; + } + + /* no characters have to be escaped */ + if (escape_characters == 0) + { + output[0] = '\"'; + memcpy(output + 1, input, output_length); + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; + } + + output[0] = '\"'; + output_pointer = output + 1; + /* copy the string */ + for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) + { + if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) + { + /* normal character, copy */ + *output_pointer = *input_pointer; + } + else + { + /* character needs to be escaped */ + *output_pointer++ = '\\'; + switch (*input_pointer) + { + case '\\': + *output_pointer = '\\'; + break; + case '\"': + *output_pointer = '\"'; + break; + case '\b': + *output_pointer = 'b'; + break; + case '\f': + *output_pointer = 'f'; + break; + case '\n': + *output_pointer = 'n'; + break; + case '\r': + *output_pointer = 'r'; + break; + case '\t': + *output_pointer = 't'; + break; + default: + /* escape and print as unicode codepoint */ + sprintf((char*)output_pointer, "u%04x", *input_pointer); + output_pointer += 4; + break; + } + } + } + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; +} + +/* Invoke print_string_ptr (which is useful) on an item. */ +static cJSON_bool print_string(const cJSON * const item, printbuffer * const p) +{ + return print_string_ptr((unsigned char*)item->valuestring, p); +} + +/* Predeclare these prototypes. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer); + +/* Utility to jump whitespace and cr/lf */ +static parse_buffer *buffer_skip_whitespace(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL)) + { + return NULL; + } + + if (cannot_access_at_index(buffer, 0)) + { + return buffer; + } + + while (can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32)) + { + buffer->offset++; + } + + if (buffer->offset == buffer->length) + { + buffer->offset--; + } + + return buffer; +} + +/* skip the UTF-8 BOM (byte order mark) if it is at the beginning of a buffer */ +static parse_buffer *skip_utf8_bom(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0)) + { + return NULL; + } + + if (can_access_at_index(buffer, 4) && (strncmp((const char*)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0)) + { + buffer->offset += 3; + } + + return buffer; +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + size_t buffer_length; + + if (NULL == value) + { + return NULL; + } + + /* Adding null character size due to require_null_terminated. */ + buffer_length = strlen(value) + sizeof(""); + + return cJSON_ParseWithLengthOpts(value, buffer_length, return_parse_end, require_null_terminated); +} + +/* Parse an object - create a new root, and populate. */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } }; + cJSON *item = NULL; + + /* reset error position */ + global_error.json = NULL; + global_error.position = 0; + + if (value == NULL || 0 == buffer_length) + { + goto fail; + } + + buffer.content = (const unsigned char*)value; + buffer.length = buffer_length; + buffer.offset = 0; + buffer.hooks = global_hooks; + + item = cJSON_New_Item(&global_hooks); + if (item == NULL) /* memory fail */ + { + goto fail; + } + + if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer)))) + { + /* parse failure. ep is set. */ + goto fail; + } + + /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */ + if (require_null_terminated) + { + buffer_skip_whitespace(&buffer); + if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0') + { + goto fail; + } + } + if (return_parse_end) + { + *return_parse_end = (const char*)buffer_at_offset(&buffer); + } + + return item; + +fail: + if (item != NULL) + { + cJSON_Delete(item); + } + + if (value != NULL) + { + error local_error; + local_error.json = (const unsigned char*)value; + local_error.position = 0; + + if (buffer.offset < buffer.length) + { + local_error.position = buffer.offset; + } + else if (buffer.length > 0) + { + local_error.position = buffer.length - 1; + } + + if (return_parse_end != NULL) + { + *return_parse_end = (const char*)local_error.json + local_error.position; + } + + global_error = local_error; + } + + return NULL; +} + +/* Default options for cJSON_Parse */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value) +{ + return cJSON_ParseWithOpts(value, 0, 0); +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length) +{ + return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0); +} + +#define cjson_min(a, b) (((a) < (b)) ? (a) : (b)) + +static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks) +{ + static const size_t default_buffer_size = 256; + printbuffer buffer[1]; + unsigned char *printed = NULL; + + memset(buffer, 0, sizeof(buffer)); + + /* create buffer */ + buffer->buffer = (unsigned char*) hooks->allocate(default_buffer_size); + buffer->length = default_buffer_size; + buffer->format = format; + buffer->hooks = *hooks; + if (buffer->buffer == NULL) + { + goto fail; + } + + /* print the value */ + if (!print_value(item, buffer)) + { + goto fail; + } + update_offset(buffer); + + /* check if reallocate is available */ + if (hooks->reallocate != NULL) + { + printed = (unsigned char*) hooks->reallocate(buffer->buffer, buffer->offset + 1); + if (printed == NULL) { + goto fail; + } + buffer->buffer = NULL; + } + else /* otherwise copy the JSON over to a new buffer */ + { + printed = (unsigned char*) hooks->allocate(buffer->offset + 1); + if (printed == NULL) + { + goto fail; + } + memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1)); + printed[buffer->offset] = '\0'; /* just to be sure */ + + /* free the buffer */ + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + return printed; + +fail: + if (buffer->buffer != NULL) + { + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + if (printed != NULL) + { + hooks->deallocate(printed); + printed = NULL; + } + + return NULL; +} + +/* Render a cJSON item/entity/structure to text. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item) +{ + return (char*)print(item, true, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item) +{ + return (char*)print(item, false, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if (prebuffer < 0) + { + return NULL; + } + + p.buffer = (unsigned char*)global_hooks.allocate((size_t)prebuffer); + if (!p.buffer) + { + return NULL; + } + + p.length = (size_t)prebuffer; + p.offset = 0; + p.noalloc = false; + p.format = fmt; + p.hooks = global_hooks; + + if (!print_value(item, &p)) + { + global_hooks.deallocate(p.buffer); + p.buffer = NULL; + return NULL; + } + + return (char*)p.buffer; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if ((length < 0) || (buffer == NULL)) + { + return false; + } + + p.buffer = (unsigned char*)buffer; + p.length = (size_t)length; + p.offset = 0; + p.noalloc = true; + p.format = format; + p.hooks = global_hooks; + + return print_value(item, &p); +} + +/* Parser core - when encountering text, process appropriately. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer) +{ + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; /* no input */ + } + + /* parse the different types of values */ + /* null */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "null", 4) == 0)) + { + item->type = cJSON_NULL; + input_buffer->offset += 4; + return true; + } + /* false */ + if (can_read(input_buffer, 5) && (strncmp((const char*)buffer_at_offset(input_buffer), "false", 5) == 0)) + { + item->type = cJSON_False; + input_buffer->offset += 5; + return true; + } + /* true */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "true", 4) == 0)) + { + item->type = cJSON_True; + item->valueint = 1; + input_buffer->offset += 4; + return true; + } + /* string */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"')) + { + return parse_string(item, input_buffer); + } + /* number */ + if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9')))) + { + return parse_number(item, input_buffer); + } + /* array */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '[')) + { + return parse_array(item, input_buffer); + } + /* object */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{')) + { + return parse_object(item, input_buffer); + } + + return false; +} + +/* Render a value to text. */ +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output = NULL; + + if ((item == NULL) || (output_buffer == NULL)) + { + return false; + } + + switch ((item->type) & 0xFF) + { + case cJSON_NULL: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "null"); + return true; + + case cJSON_False: + output = ensure(output_buffer, 6); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "false"); + return true; + + case cJSON_True: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "true"); + return true; + + case cJSON_Number: + return print_number(item, output_buffer); + + case cJSON_Raw: + { + size_t raw_length = 0; + if (item->valuestring == NULL) + { + return false; + } + + raw_length = strlen(item->valuestring) + sizeof(""); + output = ensure(output_buffer, raw_length); + if (output == NULL) + { + return false; + } + memcpy(output, item->valuestring, raw_length); + return true; + } + + case cJSON_String: + return print_string(item, output_buffer); + + case cJSON_Array: + return print_array(item, output_buffer); + + case cJSON_Object: + return print_object(item, output_buffer); + + default: + return false; + } +} + +/* Build an array from input text. */ +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* head of the linked list */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (buffer_at_offset(input_buffer)[0] != '[') + { + /* not an array */ + goto fail; + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']')) + { + /* empty array */ + goto success; + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + /* parse next value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']') + { + goto fail; /* expected end of array */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Array; + item->child = head; + + input_buffer->offset++; + + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an array to text */ +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_element = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output array. */ + /* opening square bracket */ + output_pointer = ensure(output_buffer, 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer = '['; + output_buffer->offset++; + output_buffer->depth++; + + while (current_element != NULL) + { + if (!print_value(current_element, output_buffer)) + { + return false; + } + update_offset(output_buffer); + if (current_element->next) + { + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ','; + if(output_buffer->format) + { + *output_pointer++ = ' '; + } + *output_pointer = '\0'; + output_buffer->offset += length; + } + current_element = current_element->next; + } + + output_pointer = ensure(output_buffer, 2); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ']'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Build an object from the text. */ +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* linked list head */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{')) + { + goto fail; /* not an object */ + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}')) + { + goto success; /* empty object */ + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + if (cannot_access_at_index(input_buffer, 1)) + { + goto fail; /* nothing comes after the comma */ + } + + /* parse the name of the child */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_string(current_item, input_buffer)) + { + goto fail; /* failed to parse name */ + } + buffer_skip_whitespace(input_buffer); + + /* swap valuestring and string, because we parsed the name */ + current_item->string = current_item->valuestring; + current_item->valuestring = NULL; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':')) + { + goto fail; /* invalid object */ + } + + /* parse the value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}')) + { + goto fail; /* expected end of object */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Object; + item->child = head; + + input_buffer->offset++; + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an object to text. */ +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_item = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output: */ + length = (size_t) (output_buffer->format ? 2 : 1); /* fmt: {\n */ + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer++ = '{'; + output_buffer->depth++; + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + output_buffer->offset += length; + + while (current_item) + { + if (output_buffer->format) + { + size_t i; + output_pointer = ensure(output_buffer, output_buffer->depth); + if (output_pointer == NULL) + { + return false; + } + for (i = 0; i < output_buffer->depth; i++) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += output_buffer->depth; + } + + /* print key */ + if (!print_string_ptr((unsigned char*)current_item->string, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ':'; + if (output_buffer->format) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += length; + + /* print value */ + if (!print_value(current_item, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + /* print comma if not last */ + length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0)); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + if (current_item->next) + { + *output_pointer++ = ','; + } + + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + *output_pointer = '\0'; + output_buffer->offset += length; + + current_item = current_item->next; + } + + output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2); + if (output_pointer == NULL) + { + return false; + } + if (output_buffer->format) + { + size_t i; + for (i = 0; i < (output_buffer->depth - 1); i++) + { + *output_pointer++ = '\t'; + } + } + *output_pointer++ = '}'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Get Array size/item / object item. */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array) +{ + cJSON *child = NULL; + size_t size = 0; + + if (array == NULL) + { + return 0; + } + + child = array->child; + + while(child != NULL) + { + size++; + child = child->next; + } + + /* FIXME: Can overflow here. Cannot be fixed without breaking the API */ + + return (int)size; +} + +static cJSON* get_array_item(const cJSON *array, size_t index) +{ + cJSON *current_child = NULL; + + if (array == NULL) + { + return NULL; + } + + current_child = array->child; + while ((current_child != NULL) && (index > 0)) + { + index--; + current_child = current_child->next; + } + + return current_child; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index) +{ + if (index < 0) + { + return NULL; + } + + return get_array_item(array, (size_t)index); +} + +static cJSON *get_object_item(const cJSON * const object, const char * const name, const cJSON_bool case_sensitive) +{ + cJSON *current_element = NULL; + + if ((object == NULL) || (name == NULL)) + { + return NULL; + } + + current_element = object->child; + if (case_sensitive) + { + while ((current_element != NULL) && (current_element->string != NULL) && (strcmp(name, current_element->string) != 0)) + { + current_element = current_element->next; + } + } + else + { + while ((current_element != NULL) && (case_insensitive_strcmp((const unsigned char*)name, (const unsigned char*)(current_element->string)) != 0)) + { + current_element = current_element->next; + } + } + + if ((current_element == NULL) || (current_element->string == NULL)) { + return NULL; + } + + return current_element; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, false); +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string) +{ + return cJSON_GetObjectItem(object, string) ? 1 : 0; +} + +/* Utility for array list handling. */ +static void suffix_object(cJSON *prev, cJSON *item) +{ + prev->next = item; + item->prev = prev; +} + +/* Utility for handling references. */ +static cJSON *create_reference(const cJSON *item, const internal_hooks * const hooks) +{ + cJSON *reference = NULL; + if (item == NULL) + { + return NULL; + } + + reference = cJSON_New_Item(hooks); + if (reference == NULL) + { + return NULL; + } + + memcpy(reference, item, sizeof(cJSON)); + reference->string = NULL; + reference->type |= cJSON_IsReference; + reference->next = reference->prev = NULL; + return reference; +} + +static cJSON_bool add_item_to_array(cJSON *array, cJSON *item) +{ + cJSON *child = NULL; + + if ((item == NULL) || (array == NULL) || (array == item)) + { + return false; + } + + child = array->child; + /* + * To find the last item in array quickly, we use prev in array + */ + if (child == NULL) + { + /* list is empty, start new one */ + array->child = item; + item->prev = item; + item->next = NULL; + } + else + { + /* append to the end */ + if (child->prev) + { + suffix_object(child->prev, item); + array->child->prev = item; + } + } + + return true; +} + +/* Add item to array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item) +{ + return add_item_to_array(array, item); +} + +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic push +#endif +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif +/* helper function to cast away const */ +static void* cast_away_const(const void* string) +{ + return (void*)string; +} +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic pop +#endif + + +static cJSON_bool add_item_to_object(cJSON * const object, const char * const string, cJSON * const item, const internal_hooks * const hooks, const cJSON_bool constant_key) +{ + char *new_key = NULL; + int new_type = cJSON_Invalid; + + if ((object == NULL) || (string == NULL) || (item == NULL) || (object == item)) + { + return false; + } + + if (constant_key) + { + new_key = (char*)cast_away_const(string); + new_type = item->type | cJSON_StringIsConst; + } + else + { + new_key = (char*)cJSON_strdup((const unsigned char*)string, hooks); + if (new_key == NULL) + { + return false; + } + + new_type = item->type & ~cJSON_StringIsConst; + } + + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + hooks->deallocate(item->string); + } + + item->string = new_key; + item->type = new_type; + + return add_item_to_array(object, item); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, false); +} + +/* Add an item to an object with constant string as key */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item) +{ + if (array == NULL) + { + return false; + } + + return add_item_to_array(array, create_reference(item, &global_hooks)); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item) +{ + if ((object == NULL) || (string == NULL)) + { + return false; + } + + return add_item_to_object(object, string, create_reference(item, &global_hooks), &global_hooks, false); +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name) +{ + cJSON *null = cJSON_CreateNull(); + if (add_item_to_object(object, name, null, &global_hooks, false)) + { + return null; + } + + cJSON_Delete(null); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name) +{ + cJSON *true_item = cJSON_CreateTrue(); + if (add_item_to_object(object, name, true_item, &global_hooks, false)) + { + return true_item; + } + + cJSON_Delete(true_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name) +{ + cJSON *false_item = cJSON_CreateFalse(); + if (add_item_to_object(object, name, false_item, &global_hooks, false)) + { + return false_item; + } + + cJSON_Delete(false_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean) +{ + cJSON *bool_item = cJSON_CreateBool(boolean); + if (add_item_to_object(object, name, bool_item, &global_hooks, false)) + { + return bool_item; + } + + cJSON_Delete(bool_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number) +{ + cJSON *number_item = cJSON_CreateNumber(number); + if (add_item_to_object(object, name, number_item, &global_hooks, false)) + { + return number_item; + } + + cJSON_Delete(number_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string) +{ + cJSON *string_item = cJSON_CreateString(string); + if (add_item_to_object(object, name, string_item, &global_hooks, false)) + { + return string_item; + } + + cJSON_Delete(string_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw) +{ + cJSON *raw_item = cJSON_CreateRaw(raw); + if (add_item_to_object(object, name, raw_item, &global_hooks, false)) + { + return raw_item; + } + + cJSON_Delete(raw_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name) +{ + cJSON *object_item = cJSON_CreateObject(); + if (add_item_to_object(object, name, object_item, &global_hooks, false)) + { + return object_item; + } + + cJSON_Delete(object_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name) +{ + cJSON *array = cJSON_CreateArray(); + if (add_item_to_object(object, name, array, &global_hooks, false)) + { + return array; + } + + cJSON_Delete(array); + return NULL; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item) +{ + if ((parent == NULL) || (item == NULL) || (item != parent->child && item->prev == NULL)) + { + return NULL; + } + + if (item != parent->child) + { + /* not the first element */ + item->prev->next = item->next; + } + if (item->next != NULL) + { + /* not the last element */ + item->next->prev = item->prev; + } + + if (item == parent->child) + { + /* first element */ + parent->child = item->next; + } + else if (item->next == NULL) + { + /* last element */ + parent->child->prev = item->prev; + } + + /* make sure the detached item doesn't point anywhere anymore */ + item->prev = NULL; + item->next = NULL; + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which) +{ + if (which < 0) + { + return NULL; + } + + return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which) +{ + cJSON_Delete(cJSON_DetachItemFromArray(array, which)); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItem(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItemCaseSensitive(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObject(object, string)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObjectCaseSensitive(object, string)); +} + +/* Replace array/object items with new ones. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem) +{ + cJSON *after_inserted = NULL; + + if (which < 0 || newitem == NULL) + { + return false; + } + + after_inserted = get_array_item(array, (size_t)which); + if (after_inserted == NULL) + { + return add_item_to_array(array, newitem); + } + + if (after_inserted != array->child && after_inserted->prev == NULL) { + /* return false if after_inserted is a corrupted array item */ + return false; + } + + newitem->next = after_inserted; + newitem->prev = after_inserted->prev; + after_inserted->prev = newitem; + if (after_inserted == array->child) + { + array->child = newitem; + } + else + { + newitem->prev->next = newitem; + } + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement) +{ + if ((parent == NULL) || (parent->child == NULL) || (replacement == NULL) || (item == NULL)) + { + return false; + } + + if (replacement == item) + { + return true; + } + + replacement->next = item->next; + replacement->prev = item->prev; + + if (replacement->next != NULL) + { + replacement->next->prev = replacement; + } + if (parent->child == item) + { + if (parent->child->prev == parent->child) + { + replacement->prev = replacement; + } + parent->child = replacement; + } + else + { /* + * To find the last item in array quickly, we use prev in array. + * We can't modify the last item's next pointer where this item was the parent's child + */ + if (replacement->prev != NULL) + { + replacement->prev->next = replacement; + } + if (replacement->next == NULL) + { + parent->child->prev = replacement; + } + } + + item->next = NULL; + item->prev = NULL; + cJSON_Delete(item); + + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem) +{ + if (which < 0) + { + return false; + } + + return cJSON_ReplaceItemViaPointer(array, get_array_item(array, (size_t)which), newitem); +} + +static cJSON_bool replace_item_in_object(cJSON *object, const char *string, cJSON *replacement, cJSON_bool case_sensitive) +{ + if ((replacement == NULL) || (string == NULL)) + { + return false; + } + + /* replace the name in the replacement */ + if (!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL)) + { + cJSON_free(replacement->string); + } + replacement->string = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if (replacement->string == NULL) + { + return false; + } + + replacement->type &= ~cJSON_StringIsConst; + + return cJSON_ReplaceItemViaPointer(object, get_object_item(object, string, case_sensitive), replacement); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, false); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, true); +} + +/* Create basic types: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_NULL; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_True; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = boolean ? cJSON_True : cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Number; + item->valuedouble = num; + + /* use saturation in case of overflow */ + if (num >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (num <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)num; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_String; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) + { + item->type = cJSON_String | cJSON_IsReference; + item->valuestring = (char*)cast_away_const(string); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Object | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child) { + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Array | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Raw; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)raw, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type=cJSON_Array; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_Object; + } + + return item; +} + +/* Create Arrays: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if (!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber((double)numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (strings == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for (i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateString(strings[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p,n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +/* Duplication */ +cJSON * cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recurse); + +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse) +{ + return cJSON_Duplicate_rec(item, 0, recurse ); +} + +cJSON * cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recurse) +{ + cJSON *newitem = NULL; + cJSON *child = NULL; + cJSON *next = NULL; + cJSON *newchild = NULL; + + /* Bail on bad ptr */ + if (!item) + { + goto fail; + } + /* Create new item */ + newitem = cJSON_New_Item(&global_hooks); + if (!newitem) + { + goto fail; + } + /* Copy over all vars */ + newitem->type = item->type & (~cJSON_IsReference); + newitem->valueint = item->valueint; + newitem->valuedouble = item->valuedouble; + if (item->valuestring) + { + newitem->valuestring = (char*)cJSON_strdup((unsigned char*)item->valuestring, &global_hooks); + if (!newitem->valuestring) + { + goto fail; + } + } + if (item->string) + { + newitem->string = (item->type&cJSON_StringIsConst) ? item->string : (char*)cJSON_strdup((unsigned char*)item->string, &global_hooks); + if (!newitem->string) + { + goto fail; + } + } + /* If non-recursive, then we're done! */ + if (!recurse) + { + return newitem; + } + /* Walk the ->next chain for the child. */ + child = item->child; + while (child != NULL) + { + if(depth >= CJSON_CIRCULAR_LIMIT) { + goto fail; + } + newchild = cJSON_Duplicate_rec(child, depth + 1, true); /* Duplicate (with recurse) each item in the ->next chain */ + if (!newchild) + { + goto fail; + } + if (next != NULL) + { + /* If newitem->child already set, then crosswire ->prev and ->next and move on */ + next->next = newchild; + newchild->prev = next; + next = newchild; + } + else + { + /* Set newitem->child and move to it */ + newitem->child = newchild; + next = newchild; + } + child = child->next; + } + if (newitem && newitem->child) + { + newitem->child->prev = newchild; + } + + return newitem; + +fail: + if (newitem != NULL) + { + cJSON_Delete(newitem); + } + + return NULL; +} + +static void skip_oneline_comment(char **input) +{ + *input += static_strlen("//"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if ((*input)[0] == '\n') { + *input += static_strlen("\n"); + return; + } + } +} + +static void skip_multiline_comment(char **input) +{ + *input += static_strlen("/*"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if (((*input)[0] == '*') && ((*input)[1] == '/')) + { + *input += static_strlen("*/"); + return; + } + } +} + +static void minify_string(char **input, char **output) { + (*output)[0] = (*input)[0]; + *input += static_strlen("\""); + *output += static_strlen("\""); + + + for (; (*input)[0] != '\0'; (void)++(*input), ++(*output)) { + (*output)[0] = (*input)[0]; + + if ((*input)[0] == '\"') { + (*output)[0] = '\"'; + *input += static_strlen("\""); + *output += static_strlen("\""); + return; + } else if (((*input)[0] == '\\') && ((*input)[1] == '\"')) { + (*output)[1] = (*input)[1]; + *input += static_strlen("\""); + *output += static_strlen("\""); + } + } +} + +CJSON_PUBLIC(void) cJSON_Minify(char *json) +{ + char *into = json; + + if (json == NULL) + { + return; + } + + while (json[0] != '\0') + { + switch (json[0]) + { + case ' ': + case '\t': + case '\r': + case '\n': + json++; + break; + + case '/': + if (json[1] == '/') + { + skip_oneline_comment(&json); + } + else if (json[1] == '*') + { + skip_multiline_comment(&json); + } else { + json++; + } + break; + + case '\"': + minify_string(&json, (char**)&into); + break; + + default: + into[0] = json[0]; + json++; + into++; + } + } + + /* and null-terminate. */ + *into = '\0'; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Invalid; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_False; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xff) == cJSON_True; +} + + +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & (cJSON_True | cJSON_False)) != 0; +} +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_NULL; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Number; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_String; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Array; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Object; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Raw; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive) +{ + if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) + { + return false; + } + + /* check if type is valid */ + switch (a->type & 0xFF) + { + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + case cJSON_Number: + case cJSON_String: + case cJSON_Raw: + case cJSON_Array: + case cJSON_Object: + break; + + default: + return false; + } + + /* identical objects are equal */ + if (a == b) + { + return true; + } + + switch (a->type & 0xFF) + { + /* in these cases and equal type is enough */ + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + return true; + + case cJSON_Number: + if (compare_double(a->valuedouble, b->valuedouble)) + { + return true; + } + return false; + + case cJSON_String: + case cJSON_Raw: + if ((a->valuestring == NULL) || (b->valuestring == NULL)) + { + return false; + } + if (strcmp(a->valuestring, b->valuestring) == 0) + { + return true; + } + + return false; + + case cJSON_Array: + { + cJSON *a_element = a->child; + cJSON *b_element = b->child; + + for (; (a_element != NULL) && (b_element != NULL);) + { + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + + a_element = a_element->next; + b_element = b_element->next; + } + + /* one of the arrays is longer than the other */ + if (a_element != b_element) { + return false; + } + + return true; + } + + case cJSON_Object: + { + cJSON *a_element = NULL; + cJSON *b_element = NULL; + cJSON_ArrayForEach(a_element, a) + { + /* TODO This has O(n^2) runtime, which is horrible! */ + b_element = get_object_item(b, a_element->string, case_sensitive); + if (b_element == NULL) + { + return false; + } + + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + } + + /* doing this twice, once on a and b to prevent true comparison if a subset of b + * TODO: Do this the proper way, this is just a fix for now */ + cJSON_ArrayForEach(b_element, b) + { + a_element = get_object_item(a, b_element->string, case_sensitive); + if (a_element == NULL) + { + return false; + } + + if (!cJSON_Compare(b_element, a_element, case_sensitive)) + { + return false; + } + } + + return true; + } + + default: + return false; + } +} + +CJSON_PUBLIC(void *) cJSON_malloc(size_t size) +{ + return global_hooks.allocate(size); +} + +CJSON_PUBLIC(void) cJSON_free(void *object) +{ + global_hooks.deallocate(object); + object = NULL; +} diff --git a/src/client.c b/src/client.c new file mode 100644 index 0000000..14c582b --- /dev/null +++ b/src/client.c @@ -0,0 +1,1110 @@ +/* + * DWN - Desktop Window Manager + * Client (window) management implementation + */ + +#include "client.h" +#include "atoms.h" +#include "config.h" +#include "util.h" +#include "workspace.h" +#include "decorations.h" +#include "notifications.h" + +#include +#include +#include +#include + +/* ========== Client creation/destruction ========== */ + +Client *client_create(Window window) +{ + /* Defensive: validate global state before proceeding */ + if (dwn == NULL || dwn->display == NULL) { + LOG_ERROR("client_create: dwn or display is NULL"); + return NULL; + } + + if (window == None) { + LOG_ERROR("client_create: invalid window (None)"); + return NULL; + } + + Client *client = dwn_calloc(1, sizeof(Client)); + if (client == NULL) { + LOG_ERROR("client_create: failed to allocate client"); + return NULL; + } + + client->window = window; + client->frame = None; + client->border_width = config_get_border_width(); + client->flags = CLIENT_NORMAL; + client->workspace = dwn->current_workspace; + client->next = NULL; + client->prev = NULL; + + /* Get initial geometry from window */ + XWindowAttributes wa; + int orig_width = 640, orig_height = 480; + if (XGetWindowAttributes(dwn->display, window, &wa)) { + orig_width = wa.width; + orig_height = wa.height; + } + + /* Calculate work area (account for panels) */ + int work_x = 0; + int work_y = 0; + int work_width = dwn->screen_width; + int work_height = dwn->screen_height; + + if (dwn->config != NULL) { + if (dwn->config->top_panel_enabled) { + work_y += config_get_panel_height(); + work_height -= config_get_panel_height(); + } + if (dwn->config->bottom_panel_enabled) { + work_height -= config_get_panel_height(); + } + } + + /* Set size to 75% of work area or original size, whichever is larger */ + int target_width = (work_width * 75) / 100; + int target_height = (work_height * 75) / 100; + + client->width = (orig_width > target_width) ? orig_width : target_width; + client->height = (orig_height > target_height) ? orig_height : target_height; + + /* Clamp to work area */ + if (client->width > work_width - 20) client->width = work_width - 20; + if (client->height > work_height - 20) client->height = work_height - 20; + + /* Center on screen */ + client->x = work_x + (work_width - client->width) / 2; + client->y = work_y + (work_height - client->height) / 2; + + /* Save original geometry for floating restore */ + client->old_x = client->x; + client->old_y = client->y; + client->old_width = client->width; + client->old_height = client->height; + + /* Update properties */ + client_update_title(client); + client_update_class(client); + + return client; +} + +void client_destroy(Client *client) +{ + if (client == NULL) { + return; + } + + if (client->frame != None) { + client_destroy_frame(client); + } + + dwn_free(client); +} + +/* Sync log for debugging - disabled in production */ +static inline void client_sync_log(const char *msg) +{ + (void)msg; /* No-op - enable for debugging */ +} + +/* ========== Client management ========== */ + +Client *client_manage(Window window) +{ + client_sync_log("client_manage: START"); + + if (dwn == NULL || dwn->display == NULL) { + client_sync_log("client_manage: dwn NULL"); + return NULL; + } + + /* Check if already managed */ + Client *existing = client_find_by_window(window); + if (existing != NULL) { + client_sync_log("client_manage: already managed"); + return existing; + } + + /* Check window type - don't manage docks, desktops */ + if (client_is_dock(window) || client_is_desktop(window)) { + LOG_DEBUG("Skipping dock/desktop window: %lu", window); + client_sync_log("client_manage: skip dock/desktop"); + return NULL; + } + + LOG_DEBUG("Managing window: %lu", window); + client_sync_log("client_manage: creating client"); + + /* Create client */ + Client *client = client_create(window); + if (client == NULL) { + LOG_ERROR("client_manage: failed to create client for window %lu", window); + client_sync_log("client_manage: create FAILED"); + return NULL; + } + + client_sync_log("client_manage: checking dialog"); + + /* Check if it should be floating (dialogs, etc.) */ + if (client_is_dialog(window)) { + client->flags |= CLIENT_FLOATING; + } + + client_sync_log("client_manage: creating frame"); + + /* Create frame with decorations */ + client_create_frame(client); + + client_sync_log("client_manage: verifying window"); + + /* Verify window still exists before reparenting */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, window, &wa)) { + LOG_WARN("client_manage: window %lu disappeared before reparenting", window); + client_sync_log("client_manage: window gone before reparent"); + client_destroy(client); + return NULL; + } + + client_sync_log("client_manage: reparenting"); + client_reparent_to_frame(client); + + /* Verify both window AND frame still exist after reparent */ + client_sync_log("client_manage: verifying after reparent"); + XSync(dwn->display, False); /* Flush any pending errors */ + + if (client->frame == None) { + client_sync_log("client_manage: frame is None after reparent"); + LOG_WARN("client_manage: frame creation failed for window %lu", window); + client_destroy(client); + return NULL; + } + + XWindowAttributes wa_frame; + if (!XGetWindowAttributes(dwn->display, client->frame, &wa_frame)) { + client_sync_log("client_manage: frame gone after reparent"); + LOG_WARN("client_manage: frame %lu no longer exists", client->frame); + client_destroy(client); + return NULL; + } + + if (!XGetWindowAttributes(dwn->display, window, &wa)) { + client_sync_log("client_manage: window gone after reparent"); + LOG_WARN("client_manage: window %lu disappeared after reparent", window); + client_destroy_frame(client); + client_destroy(client); + return NULL; + } + + client_sync_log("client_manage: configuring"); + /* Resize client window to match frame dimensions */ + client_configure(client); + + client_sync_log("client_manage: adding to list"); + /* Add to client list */ + client_add_to_list(client); + + client_sync_log("client_manage: adding to workspace"); + /* Add to current workspace */ + workspace_add_client(dwn->current_workspace, client); + + client_sync_log("client_manage: setting EWMH"); + /* Set EWMH properties */ + atoms_set_window_desktop(window, client->workspace); + atoms_update_client_list(); + + client_sync_log("client_manage: verifying window again"); + /* Verify window still exists before subscribing to events */ + if (!XGetWindowAttributes(dwn->display, window, &wa)) { + LOG_WARN("client_manage: window %lu disappeared before event setup", window); + client_sync_log("client_manage: window gone before events"); + workspace_remove_client(client->workspace, client); + client_remove_from_list(client); + client_destroy(client); + atoms_update_client_list(); + return NULL; + } + + client_sync_log("client_manage: selecting input"); + /* Subscribe to window events */ + XSelectInput(dwn->display, window, + EnterWindowMask | FocusChangeMask | PropertyChangeMask | + StructureNotifyMask); + + client_sync_log("client_manage: grabbing button"); + /* Grab button for click-to-focus (will replay event to app after focusing) */ + XGrabButton(dwn->display, Button1, AnyModifier, window, False, + ButtonPressMask, GrabModeSync, GrabModeAsync, None, None); + + client_sync_log("client_manage: syncing X"); + /* Sync to ensure all operations completed */ + XSync(dwn->display, False); + + client_sync_log("client_manage: showing"); + /* Map and focus */ + client_show(client); + + client_sync_log("client_manage: focusing"); + client_focus(client); + + client_sync_log("client_manage: DONE"); + return client; +} + +void client_unmanage(Client *client) +{ + client_sync_log("client_unmanage: START"); + + if (client == NULL) { + client_sync_log("client_unmanage: NULL client"); + return; + } + + LOG_DEBUG("Unmanaging window: %lu", client->window); + + client_sync_log("client_unmanage: remove from workspace"); + /* Remove from workspace */ + workspace_remove_client(client->workspace, client); + + client_sync_log("client_unmanage: remove from list"); + /* Remove from client list */ + client_remove_from_list(client); + + client_sync_log("client_unmanage: reparent from frame"); + /* Reparent back to root */ + client_reparent_from_frame(client); + + client_sync_log("client_unmanage: update EWMH"); + /* Update EWMH */ + atoms_update_client_list(); + + client_sync_log("client_unmanage: destroy client"); + /* Destroy client */ + client_destroy(client); + + client_sync_log("client_unmanage: focus next"); + /* Focus next client if needed */ + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused == NULL) { + Client *next = workspace_get_first_client(dwn->current_workspace); + if (next != NULL) { + client_focus(next); + } + } + + client_sync_log("client_unmanage: DONE"); +} + +Client *client_find_by_window(Window window) +{ + if (dwn == NULL || window == None) { + return NULL; + } + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->window == window) { + return c; + } + } + return NULL; +} + +Client *client_find_by_frame(Window frame) +{ + if (dwn == NULL || frame == None) { + return NULL; + } + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->frame == frame) { + return c; + } + } + return NULL; +} + +/* ========== Client state ========== */ + +void client_focus(Client *client) +{ + client_sync_log("client_focus: START"); + + if (client == NULL || dwn == NULL || dwn->display == NULL) { + client_sync_log("client_focus: NULL check failed"); + return; + } + + /* Defensive: verify window is still valid */ + if (client->window == None) { + LOG_WARN("client_focus: client has invalid window (None)"); + client_sync_log("client_focus: window is None"); + return; + } + + /* Verify window still exists before focusing */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_WARN("client_focus: window %lu no longer exists", client->window); + client_sync_log("client_focus: window gone"); + return; + } + + client_sync_log("client_focus: unfocusing previous"); + + /* Unfocus previous */ + Workspace *ws = workspace_get(client->workspace); + if (ws != NULL && ws->focused != NULL && ws->focused != client) { + client_unfocus(ws->focused); + } + + client_sync_log("client_focus: XSetInputFocus"); + + /* Set focus with error handling */ + XSetInputFocus(dwn->display, client->window, RevertToPointerRoot, CurrentTime); + XSync(dwn->display, False); /* Catch any X errors immediately */ + + client_sync_log("client_focus: updating workspace"); + + /* Update workspace */ + if (ws != NULL) { + ws->focused = client; + } + + client_sync_log("client_focus: raising"); + + /* Raise window */ + client_raise(client); + + client_sync_log("client_focus: decorations"); + + /* Update decorations */ + decorations_render(client, true); + + client_sync_log("client_focus: EWMH"); + + /* Update EWMH */ + atoms_set_active_window(client->window); + + client_sync_log("client_focus: DONE"); + LOG_DEBUG("Focused window: %s", client->title); +} + +void client_unfocus(Client *client) +{ + if (client == NULL) { + return; + } + + /* Update decorations */ + decorations_render(client, false); +} + +void client_raise(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + Window win = (client->frame != None) ? client->frame : client->window; + if (win == None) { + return; + } + + XRaiseWindow(dwn->display, win); + + /* Keep notifications on top */ + notifications_raise_all(); +} + +void client_lower(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + Window win = (client->frame != None) ? client->frame : client->window; + if (win == None) { + return; + } + XLowerWindow(dwn->display, win); +} + +void client_minimize(Client *client) +{ + if (client == NULL) { + return; + } + + client->flags |= CLIENT_MINIMIZED; + client_hide(client); +} + +void client_restore(Client *client) +{ + if (client == NULL) { + return; + } + + client->flags &= ~CLIENT_MINIMIZED; + client_show(client); + client_focus(client); +} + +/* ========== Client geometry ========== */ + +void client_move(Client *client, int x, int y) +{ + if (client == NULL) { + return; + } + + client->x = x; + client->y = y; + client_configure(client); +} + +void client_resize(Client *client, int width, int height) +{ + if (client == NULL) { + return; + } + + client_apply_size_hints(client, &width, &height); + client->width = width; + client->height = height; + client_configure(client); +} + +void client_move_resize(Client *client, int x, int y, int width, int height) +{ + if (client == NULL) { + return; + } + + client_apply_size_hints(client, &width, &height); + client->x = x; + client->y = y; + client->width = width; + client->height = height; + client_configure(client); +} + +void client_configure(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + /* Verify window still exists before configuring */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_DEBUG("client_configure: window no longer exists"); + return; + } + + int title_height = (dwn->config && dwn->config->show_decorations) ? + config_get_title_height() : 0; + int border = client->border_width; + + if (client->frame != None) { + /* Move/resize frame */ + XMoveResizeWindow(dwn->display, client->frame, + client->x - border, + client->y - title_height - border, + client->width + 2 * border, + client->height + title_height + 2 * border); + + /* Move/resize client window within frame */ + XMoveResizeWindow(dwn->display, client->window, + border, title_height + border, + client->width, client->height); + } else { + XMoveResizeWindow(dwn->display, client->window, + client->x, client->y, + client->width, client->height); + } + + /* Send configure notify to client */ + XConfigureEvent ce; + memset(&ce, 0, sizeof(ce)); /* Zero-initialize */ + ce.type = ConfigureNotify; + ce.event = client->window; + ce.window = client->window; + ce.x = client->x; + ce.y = client->y; + ce.width = client->width; + ce.height = client->height; + ce.border_width = 0; + ce.above = None; + ce.override_redirect = False; + + XSendEvent(dwn->display, client->window, False, StructureNotifyMask, (XEvent *)&ce); +} + +void client_apply_size_hints(Client *client, int *width, int *height) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + XSizeHints hints; + long supplied; + + if (!XGetWMNormalHints(dwn->display, client->window, &hints, &supplied)) { + return; + } + + /* Minimum size */ + if (hints.flags & PMinSize) { + if (*width < hints.min_width) *width = hints.min_width; + if (*height < hints.min_height) *height = hints.min_height; + } + + /* Maximum size */ + if (hints.flags & PMaxSize) { + if (*width > hints.max_width) *width = hints.max_width; + if (*height > hints.max_height) *height = hints.max_height; + } + + /* Size increments */ + if (hints.flags & PResizeInc) { + int base_w = (hints.flags & PBaseSize) ? hints.base_width : 0; + int base_h = (hints.flags & PBaseSize) ? hints.base_height : 0; + + *width = base_w + ((*width - base_w) / hints.width_inc) * hints.width_inc; + *height = base_h + ((*height - base_h) / hints.height_inc) * hints.height_inc; + } +} + +/* ========== Client properties ========== */ + +void client_update_title(Client *client) +{ + if (client == NULL) { + return; + } + + char *name = atoms_get_window_name(client->window); + if (name != NULL) { + strncpy(client->title, name, sizeof(client->title) - 1); + client->title[sizeof(client->title) - 1] = '\0'; + dwn_free(name); + } else { + strncpy(client->title, "Untitled", sizeof(client->title) - 1); + } + + /* Redraw decorations */ + if (client->frame != None) { + Workspace *ws = workspace_get(client->workspace); + bool focused = (ws != NULL && ws->focused == client); + decorations_render(client, focused); + } +} + +void client_update_class(Client *client) +{ + if (client == NULL) { + return; + } + + atoms_get_wm_class(client->window, client->class, NULL, sizeof(client->class)); +} + +void client_set_fullscreen(Client *client, bool fullscreen) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + /* Verify window still exists */ + if (client->window == None) { + return; + } + + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_WARN("client_set_fullscreen: window %lu no longer exists", client->window); + return; + } + + if (fullscreen) { + /* Save current geometry */ + if (!(client->flags & CLIENT_FULLSCREEN)) { + client->old_x = client->x; + client->old_y = client->y; + client->old_width = client->width; + client->old_height = client->height; + } + + client->flags |= CLIENT_FULLSCREEN; + + /* Get monitor dimensions */ + client->x = 0; + client->y = 0; + client->width = dwn->screen_width; + client->height = dwn->screen_height; + + /* Reparent window to root and hide frame */ + if (client->frame != None) { + XUnmapWindow(dwn->display, client->frame); + XSync(dwn->display, False); + XReparentWindow(dwn->display, client->window, dwn->root, 0, 0); + XSync(dwn->display, False); + } + XMoveResizeWindow(dwn->display, client->window, + 0, 0, client->width, client->height); + XMapWindow(dwn->display, client->window); + XRaiseWindow(dwn->display, client->window); + } else { + client->flags &= ~CLIENT_FULLSCREEN; + + /* Restore geometry */ + client->x = client->old_x; + client->y = client->old_y; + client->width = client->old_width; + client->height = client->old_height; + + /* Reparent back to frame and show it */ + if (client->frame != None) { + int title_height = config_get_title_height(); + int border = client->border_width; + XSync(dwn->display, False); + XReparentWindow(dwn->display, client->window, client->frame, + border, title_height + border); + XSync(dwn->display, False); + XMapWindow(dwn->display, client->frame); + } + client_configure(client); + } +} + +void client_toggle_fullscreen(Client *client) +{ + if (client == NULL) { + return; + } + client_set_fullscreen(client, !(client->flags & CLIENT_FULLSCREEN)); +} + +void client_set_floating(Client *client, bool floating) +{ + if (client == NULL) { + return; + } + + if (floating) { + client->flags |= CLIENT_FLOATING; + } else { + client->flags &= ~CLIENT_FLOATING; + } +} + +void client_toggle_floating(Client *client) +{ + if (client == NULL) { + return; + } + client_set_floating(client, !(client->flags & CLIENT_FLOATING)); +} + +/* ========== Window type checking ========== */ + +bool client_is_floating(Client *client) +{ + return client != NULL && (client->flags & CLIENT_FLOATING); +} + +bool client_is_fullscreen(Client *client) +{ + return client != NULL && (client->flags & CLIENT_FULLSCREEN); +} + +bool client_is_minimized(Client *client) +{ + return client != NULL && (client->flags & CLIENT_MINIMIZED); +} + +bool client_is_dialog(Window window) +{ + Atom type; + if (atoms_get_window_type(window, &type)) { + return type == ewmh.NET_WM_WINDOW_TYPE_DIALOG; + } + + /* Check transient hint */ + Window transient_for = None; + if (XGetTransientForHint(dwn->display, window, &transient_for)) { + return transient_for != None; + } + + return false; +} + +bool client_is_dock(Window window) +{ + Atom type; + if (atoms_get_window_type(window, &type)) { + return type == ewmh.NET_WM_WINDOW_TYPE_DOCK; + } + return false; +} + +bool client_is_desktop(Window window) +{ + Atom type; + if (atoms_get_window_type(window, &type)) { + return type == ewmh.NET_WM_WINDOW_TYPE_DESKTOP; + } + return false; +} + +/* ========== Frame management ========== */ + +void client_create_frame(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + LOG_ERROR("client_create_frame: invalid parameters"); + return; + } + + if (!dwn->config || !dwn->config->show_decorations) { + return; + } + + /* Defensive: verify client window is valid */ + if (client->window == None) { + LOG_ERROR("client_create_frame: client has no valid window"); + return; + } + + int title_height = config_get_title_height(); + int border = client->border_width; + + /* Ensure reasonable dimensions */ + int frame_width = client->width + 2 * border; + int frame_height = client->height + title_height + 2 * border; + if (frame_width <= 0 || frame_height <= 0) { + LOG_ERROR("client_create_frame: invalid dimensions (%dx%d)", frame_width, frame_height); + return; + } + + /* Create frame window */ + XSetWindowAttributes swa; + memset(&swa, 0, sizeof(swa)); /* Defensive: zero-initialize */ + swa.override_redirect = True; + swa.background_pixel = dwn->config->colors.title_unfocused_bg; + swa.border_pixel = dwn->config->colors.border_unfocused; + swa.event_mask = SubstructureRedirectMask | SubstructureNotifyMask | + ButtonPressMask | ButtonReleaseMask | PointerMotionMask | + ExposureMask | EnterWindowMask; + + client->frame = XCreateWindow(dwn->display, dwn->root, + client->x - border, + client->y - title_height - border, + frame_width, + frame_height, + 0, + CopyFromParent, InputOutput, CopyFromParent, + CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, + &swa); + + /* Verify frame was created successfully */ + if (client->frame == None) { + LOG_ERROR("client_create_frame: XCreateWindow failed for window %lu", client->window); + return; + } + + XSetWindowBorderWidth(dwn->display, client->frame, border); + XSync(dwn->display, False); /* Catch any X errors */ + + LOG_DEBUG("Created frame %lu for window %lu", client->frame, client->window); +} + +void client_destroy_frame(Client *client) +{ + if (client == NULL || client->frame == None) { + return; + } + + XDestroyWindow(dwn->display, client->frame); + client->frame = None; +} + +void client_reparent_to_frame(Client *client) +{ + if (client == NULL || client->frame == None) { + return; + } + + if (dwn == NULL || dwn->display == NULL) { + return; + } + + /* Verify window still exists before reparenting */ + if (client->window == None) { + LOG_WARN("client_reparent_to_frame: client has no valid window"); + return; + } + + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_WARN("client_reparent_to_frame: window %lu no longer exists", client->window); + return; + } + + int title_height = config_get_title_height(); + int border = client->border_width; + + /* Sync before reparent to catch any pending errors */ + XSync(dwn->display, False); + + XReparentWindow(dwn->display, client->window, client->frame, + border, title_height + border); + + /* Sync after reparent to catch errors immediately */ + XSync(dwn->display, False); + + /* Save the frame window association */ + XSaveContext(dwn->display, client->window, XUniqueContext(), (XPointer)client); +} + +void client_reparent_from_frame(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->frame == None || client->window == None) { + return; + } + + /* Verify window still exists before reparenting */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_WARN("client_reparent_from_frame: window %lu no longer exists", client->window); + return; + } + + /* Sync before reparent to catch any pending errors */ + XSync(dwn->display, False); + + XReparentWindow(dwn->display, client->window, dwn->root, + client->x, client->y); + + /* Sync after reparent to catch errors immediately */ + XSync(dwn->display, False); +} + +/* ========== Visibility ========== */ + +void client_show(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + /* Verify window still exists before mapping */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_DEBUG("client_show: window no longer exists"); + return; + } + + if (client->frame != None) { + XMapWindow(dwn->display, client->frame); + } + XMapWindow(dwn->display, client->window); +} + +void client_hide(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + /* Verify window still exists before unmapping */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_DEBUG("client_hide: window no longer exists"); + return; + } + + if (client->frame != None) { + XUnmapWindow(dwn->display, client->frame); + } + XUnmapWindow(dwn->display, client->window); +} + +bool client_is_visible(Client *client) +{ + if (client == NULL) { + return false; + } + + /* Hidden windows are not visible */ + if (client->flags & CLIENT_MINIMIZED) { + return false; + } + + /* Only visible if on current workspace (or sticky) */ + if (!(client->flags & CLIENT_STICKY) && + client->workspace != (unsigned int)dwn->current_workspace) { + return false; + } + + return true; +} + +/* ========== Close handling ========== */ + +void client_close(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + /* Verify window still exists */ + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_DEBUG("client_close: window no longer exists"); + return; + } + + /* Try WM_DELETE_WINDOW first */ + if (atoms_window_supports_protocol(client->window, icccm.WM_DELETE_WINDOW)) { + atoms_send_protocol(client->window, icccm.WM_DELETE_WINDOW, CurrentTime); + } else { + /* Force kill */ + client_kill(client); + } +} + +void client_kill(Client *client) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + XKillClient(dwn->display, client->window); +} + +/* ========== List operations ========== */ + +void client_add_to_list(Client *client) +{ + if (client == NULL || dwn == NULL) { + return; + } + + client->next = dwn->client_list; + client->prev = NULL; + + if (dwn->client_list != NULL) { + dwn->client_list->prev = client; + } + + dwn->client_list = client; + dwn->client_count++; +} + +void client_remove_from_list(Client *client) +{ + if (client == NULL || dwn == NULL) { + return; + } + + if (client->prev != NULL) { + client->prev->next = client->next; + } else { + dwn->client_list = client->next; + } + + if (client->next != NULL) { + client->next->prev = client->prev; + } + + dwn->client_count--; +} + +int client_count(void) +{ + return dwn != NULL ? dwn->client_count : 0; +} + +int client_count_on_workspace(int workspace) +{ + int count = 0; + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)workspace) { + count++; + } + } + return count; +} + +/* ========== Iteration ========== */ + +Client *client_get_next(Client *client) +{ + return client != NULL ? client->next : NULL; +} + +Client *client_get_prev(Client *client) +{ + return client != NULL ? client->prev : NULL; +} + +Client *client_get_first(void) +{ + return dwn != NULL ? dwn->client_list : NULL; +} + +Client *client_get_last(void) +{ + if (dwn == NULL || dwn->client_list == NULL) { + return NULL; + } + + Client *c = dwn->client_list; + while (c->next != NULL) { + c = c->next; + } + return c; +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..3efb77e --- /dev/null +++ b/src/config.c @@ -0,0 +1,361 @@ +/* + * DWN - Desktop Window Manager + * Configuration system implementation + */ + +#include "config.h" +#include "util.h" + +#include +#include +#include +#include + +/* Default color values (dark modern theme) */ +#define DEFAULT_PANEL_BG "#1a1a2e" +#define DEFAULT_PANEL_FG "#e0e0e0" +#define DEFAULT_WS_ACTIVE "#4a90d9" +#define DEFAULT_WS_INACTIVE "#3a3a4e" +#define DEFAULT_WS_URGENT "#d94a4a" +#define DEFAULT_TITLE_FOCUSED_BG "#2d3a4a" +#define DEFAULT_TITLE_FOCUSED_FG "#ffffff" +#define DEFAULT_TITLE_UNFOCUSED_BG "#1a1a1a" +#define DEFAULT_TITLE_UNFOCUSED_FG "#808080" +#define DEFAULT_BORDER_FOCUSED "#4a90d9" +#define DEFAULT_BORDER_UNFOCUSED "#333333" +#define DEFAULT_NOTIFICATION_BG "#2a2a3e" +#define DEFAULT_NOTIFICATION_FG "#ffffff" + +/* ========== Configuration creation/destruction ========== */ + +Config *config_create(void) +{ + Config *cfg = dwn_calloc(1, sizeof(Config)); + config_set_defaults(cfg); + return cfg; +} + +void config_destroy(Config *cfg) +{ + if (cfg != NULL) { + /* Securely wipe API keys before freeing */ + secure_wipe(cfg->openrouter_api_key, sizeof(cfg->openrouter_api_key)); + secure_wipe(cfg->exa_api_key, sizeof(cfg->exa_api_key)); + dwn_free(cfg); + } +} + +void config_set_defaults(Config *cfg) +{ + if (cfg == NULL) { + return; + } + + /* General */ + strncpy(cfg->terminal, "xfce4-terminal", sizeof(cfg->terminal) - 1); + strncpy(cfg->launcher, "dmenu_run", sizeof(cfg->launcher) - 1); + strncpy(cfg->file_manager, "thunar", sizeof(cfg->file_manager) - 1); + cfg->focus_mode = FOCUS_CLICK; + cfg->show_decorations = true; + + /* Appearance */ + cfg->border_width = DEFAULT_BORDER_WIDTH; + cfg->title_height = DEFAULT_TITLE_HEIGHT; + cfg->panel_height = DEFAULT_PANEL_HEIGHT; + cfg->gap = DEFAULT_GAP; + strncpy(cfg->font_name, "fixed", sizeof(cfg->font_name) - 1); + + /* Layout */ + cfg->default_master_ratio = 0.55f; + cfg->default_master_count = 1; + cfg->default_layout = LAYOUT_TILING; + + /* Panels */ + cfg->top_panel_enabled = true; + cfg->bottom_panel_enabled = true; + + /* AI (disabled by default, enabled if API key found) */ + cfg->openrouter_api_key[0] = '\0'; + cfg->exa_api_key[0] = '\0'; + strncpy(cfg->ai_model, "google/gemini-2.0-flash-exp:free", sizeof(cfg->ai_model) - 1); + cfg->ai_enabled = false; + + /* Paths */ + strncpy(cfg->config_path, "~/.config/dwn/config", sizeof(cfg->config_path) - 1); + strncpy(cfg->log_path, "~/.local/share/dwn/dwn.log", sizeof(cfg->log_path) - 1); +} + +/* ========== INI Parser ========== */ + +typedef struct { + Config *cfg; + char current_section[64]; +} ParseContext; + +static void handle_config_entry(const char *section, const char *key, + const char *value, void *user_data) +{ + ParseContext *ctx = (ParseContext *)user_data; + Config *cfg = ctx->cfg; + + if (strcmp(section, "general") == 0) { + if (strcmp(key, "terminal") == 0) { + strncpy(cfg->terminal, value, sizeof(cfg->terminal) - 1); + } else if (strcmp(key, "launcher") == 0) { + strncpy(cfg->launcher, value, sizeof(cfg->launcher) - 1); + } else if (strcmp(key, "file_manager") == 0) { + strncpy(cfg->file_manager, value, sizeof(cfg->file_manager) - 1); + } else if (strcmp(key, "focus_mode") == 0) { + if (strcmp(value, "follow") == 0) { + cfg->focus_mode = FOCUS_FOLLOW; + } else { + cfg->focus_mode = FOCUS_CLICK; + } + } else if (strcmp(key, "decorations") == 0) { + cfg->show_decorations = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); + } + } else if (strcmp(section, "appearance") == 0) { + if (strcmp(key, "border_width") == 0) { + int val = atoi(value); + cfg->border_width = (val >= 0 && val <= 50) ? val : DEFAULT_BORDER_WIDTH; + } else if (strcmp(key, "title_height") == 0) { + int val = atoi(value); + cfg->title_height = (val >= 0 && val <= 100) ? val : DEFAULT_TITLE_HEIGHT; + } else if (strcmp(key, "panel_height") == 0) { + int val = atoi(value); + cfg->panel_height = (val >= 0 && val <= 100) ? val : DEFAULT_PANEL_HEIGHT; + } else if (strcmp(key, "gap") == 0) { + int val = atoi(value); + cfg->gap = (val >= 0 && val <= 100) ? val : DEFAULT_GAP; + } else if (strcmp(key, "font") == 0) { + strncpy(cfg->font_name, value, sizeof(cfg->font_name) - 1); + } + } else if (strcmp(section, "layout") == 0) { + if (strcmp(key, "master_ratio") == 0) { + float val = atof(value); + cfg->default_master_ratio = (val >= 0.1f && val <= 0.9f) ? val : 0.55f; + } else if (strcmp(key, "master_count") == 0) { + int val = atoi(value); + cfg->default_master_count = (val >= 1 && val <= 10) ? val : 1; + } else if (strcmp(key, "default") == 0) { + if (strcmp(value, "floating") == 0) { + cfg->default_layout = LAYOUT_FLOATING; + } else if (strcmp(value, "monocle") == 0) { + cfg->default_layout = LAYOUT_MONOCLE; + } else { + cfg->default_layout = LAYOUT_TILING; + } + } + } else if (strcmp(section, "panels") == 0) { + if (strcmp(key, "top") == 0) { + cfg->top_panel_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); + } else if (strcmp(key, "bottom") == 0) { + cfg->bottom_panel_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); + } + } else if (strcmp(section, "ai") == 0) { + if (strcmp(key, "model") == 0) { + strncpy(cfg->ai_model, value, sizeof(cfg->ai_model) - 1); + } else if (strcmp(key, "openrouter_api_key") == 0) { + strncpy(cfg->openrouter_api_key, value, sizeof(cfg->openrouter_api_key) - 1); + cfg->ai_enabled = true; + } else if (strcmp(key, "exa_api_key") == 0) { + strncpy(cfg->exa_api_key, value, sizeof(cfg->exa_api_key) - 1); + } + } +} + +bool config_parse_ini(const char *path, ConfigCallback callback, void *user_data) +{ + char *expanded = expand_path(path); + FILE *f = fopen(expanded, "r"); + dwn_free(expanded); + + if (f == NULL) { + return false; + } + + char line[1024]; + char section[64] = ""; + + while (fgets(line, sizeof(line), f) != NULL) { + char *trimmed = str_trim(line); + + /* Skip empty lines and comments */ + if (trimmed[0] == '\0' || trimmed[0] == '#' || trimmed[0] == ';') { + continue; + } + + /* Section header */ + if (trimmed[0] == '[') { + char *end = strchr(trimmed, ']'); + if (end != NULL) { + *end = '\0'; + strncpy(section, trimmed + 1, sizeof(section) - 1); + } + continue; + } + + /* Key = value */ + char *equals = strchr(trimmed, '='); + if (equals != NULL) { + *equals = '\0'; + char *key = str_trim(trimmed); + char *value = str_trim(equals + 1); + + /* Remove quotes from value */ + size_t vlen = strlen(value); + if (vlen >= 2 && ((value[0] == '"' && value[vlen-1] == '"') || + (value[0] == '\'' && value[vlen-1] == '\''))) { + value[vlen-1] = '\0'; + value++; + } + + callback(section, key, value, user_data); + } + } + + fclose(f); + return true; +} + +/* ========== Load configuration ========== */ + +bool config_load(Config *cfg, const char *path) +{ + if (cfg == NULL) { + return false; + } + + /* Set defaults first */ + config_set_defaults(cfg); + + /* Determine config path */ + const char *config_path = path; + if (config_path == NULL) { + config_path = cfg->config_path; + } + + /* Parse config file first (environment variables override below) */ + ParseContext ctx = { .cfg = cfg }; + char *expanded = expand_path(config_path); + + if (file_exists(expanded)) { + LOG_INFO("Loading configuration from %s", expanded); + config_parse_ini(expanded, handle_config_entry, &ctx); + } else { + LOG_INFO("No config file found at %s, using defaults", expanded); + } + + dwn_free(expanded); + + /* Check for API keys in environment (override config file) */ + const char *openrouter_key = getenv("OPENROUTER_API_KEY"); + if (openrouter_key != NULL && openrouter_key[0] != '\0') { + strncpy(cfg->openrouter_api_key, openrouter_key, sizeof(cfg->openrouter_api_key) - 1); + cfg->ai_enabled = true; + LOG_INFO("OpenRouter API key found in environment"); + } + + const char *exa_key = getenv("EXA_API_KEY"); + if (exa_key != NULL && exa_key[0] != '\0') { + strncpy(cfg->exa_api_key, exa_key, sizeof(cfg->exa_api_key) - 1); + } + + /* Log AI status */ + if (cfg->ai_enabled) { + LOG_INFO("AI features enabled"); + } + + return true; +} + +bool config_reload(Config *cfg) +{ + if (cfg == NULL) { + return false; + } + /* Securely wipe existing API keys before reloading */ + secure_wipe(cfg->openrouter_api_key, sizeof(cfg->openrouter_api_key)); + secure_wipe(cfg->exa_api_key, sizeof(cfg->exa_api_key)); + return config_load(cfg, cfg->config_path); +} + +/* ========== Getters ========== */ + +const char *config_get_terminal(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->terminal; + } + return "xterm"; +} + +const char *config_get_launcher(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->launcher; + } + return "dmenu_run"; +} + +int config_get_border_width(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->border_width; + } + return DEFAULT_BORDER_WIDTH; +} + +int config_get_title_height(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->title_height; + } + return DEFAULT_TITLE_HEIGHT; +} + +int config_get_panel_height(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->panel_height; + } + return DEFAULT_PANEL_HEIGHT; +} + +int config_get_gap(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->gap; + } + return DEFAULT_GAP; +} + +const ColorScheme *config_get_colors(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return &dwn->config->colors; + } + return NULL; +} + +/* Initialize colors (must be called after display is open) */ +void config_init_colors(Config *cfg) +{ + if (cfg == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + cfg->colors.panel_bg = parse_color(DEFAULT_PANEL_BG); + cfg->colors.panel_fg = parse_color(DEFAULT_PANEL_FG); + cfg->colors.workspace_active = parse_color(DEFAULT_WS_ACTIVE); + cfg->colors.workspace_inactive = parse_color(DEFAULT_WS_INACTIVE); + cfg->colors.workspace_urgent = parse_color(DEFAULT_WS_URGENT); + cfg->colors.title_focused_bg = parse_color(DEFAULT_TITLE_FOCUSED_BG); + cfg->colors.title_focused_fg = parse_color(DEFAULT_TITLE_FOCUSED_FG); + cfg->colors.title_unfocused_bg = parse_color(DEFAULT_TITLE_UNFOCUSED_BG); + cfg->colors.title_unfocused_fg = parse_color(DEFAULT_TITLE_UNFOCUSED_FG); + cfg->colors.border_focused = parse_color(DEFAULT_BORDER_FOCUSED); + cfg->colors.border_unfocused = parse_color(DEFAULT_BORDER_UNFOCUSED); + cfg->colors.notification_bg = parse_color(DEFAULT_NOTIFICATION_BG); + cfg->colors.notification_fg = parse_color(DEFAULT_NOTIFICATION_FG); +} diff --git a/src/decorations.c b/src/decorations.c new file mode 100644 index 0000000..c769912 --- /dev/null +++ b/src/decorations.c @@ -0,0 +1,368 @@ +/* + * DWN - Desktop Window Manager + * Window decorations implementation + */ + +#include "decorations.h" +#include "client.h" +#include "config.h" +#include "util.h" + +#include +#include +#include + +/* Button dimensions */ +#define BUTTON_SIZE 16 +#define BUTTON_PADDING 4 + +/* Resize edge size */ +#define RESIZE_EDGE 8 + +/* ========== Initialization ========== */ + +void decorations_init(void) +{ + LOG_DEBUG("Decorations system initialized"); +} + +void decorations_cleanup(void) +{ + /* Nothing to clean up */ +} + +/* ========== Rendering ========== */ + +void decorations_render(Client *client, bool focused) +{ + if (client == NULL || client->frame == None) { + return; + } + + if (dwn == NULL || dwn->display == NULL || dwn->config == NULL) { + return; + } + + decorations_render_title_bar(client, focused); + decorations_render_buttons(client, focused); + decorations_render_border(client, focused); +} + +void decorations_render_title_bar(Client *client, bool focused) +{ + if (client == NULL || client->frame == None) { + return; + } + + if (dwn == NULL || dwn->display == NULL || dwn->gc == None || dwn->config == NULL) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + int title_height = config_get_title_height(); + int border = client->border_width; + + /* Set colors based on focus */ + unsigned long bg_color = focused ? colors->title_focused_bg : colors->title_unfocused_bg; + unsigned long fg_color = focused ? colors->title_focused_fg : colors->title_unfocused_fg; + + /* Draw title bar background */ + XSetForeground(dpy, dwn->gc, bg_color); + XFillRectangle(dpy, client->frame, dwn->gc, + border, border, + client->width, title_height); + + /* Draw title text */ + if (client->title[0] != '\0' && dwn->xft_font != NULL) { + int text_y = border + (title_height + dwn->xft_font->ascent) / 2; + int text_x = border + BUTTON_PADDING; + + /* Truncate title if too long */ + int max_width = client->width - 3 * (BUTTON_SIZE + BUTTON_PADDING) - 2 * BUTTON_PADDING; + char display_title[256]; + strncpy(display_title, client->title, sizeof(display_title) - 4); /* Leave room for "..." */ + display_title[sizeof(display_title) - 4] = '\0'; + + XGlyphInfo extents; + XftTextExtentsUtf8(dpy, dwn->xft_font, + (const FcChar8 *)display_title, strlen(display_title), &extents); + int text_width = extents.xOff; + + /* Truncate UTF-8 aware: find valid UTF-8 boundary */ + bool title_truncated = false; + while (text_width > max_width && strlen(display_title) > 3) { + size_t len = strlen(display_title); + /* Move back to find UTF-8 character boundary */ + size_t cut = len - 1; + while (cut > 0 && (display_title[cut] & 0xC0) == 0x80) { + cut--; /* Skip continuation bytes */ + } + if (cut > 0) cut--; /* Remove one more character for ellipsis space */ + while (cut > 0 && (display_title[cut] & 0xC0) == 0x80) { + cut--; + } + display_title[cut] = '\0'; + title_truncated = true; + + XftTextExtentsUtf8(dpy, dwn->xft_font, + (const FcChar8 *)display_title, strlen(display_title), &extents); + text_width = extents.xOff; + } + if (title_truncated) { + strncat(display_title, "...", sizeof(display_title) - strlen(display_title) - 1); + } + + /* Draw with Xft for UTF-8 support */ + XftDraw *xft_draw = XftDrawCreate(dpy, client->frame, + DefaultVisual(dpy, dwn->screen), + dwn->colormap); + if (xft_draw != NULL) { + XftColor xft_color; + XRenderColor render_color; + render_color.red = ((fg_color >> 16) & 0xFF) * 257; + render_color.green = ((fg_color >> 8) & 0xFF) * 257; + render_color.blue = (fg_color & 0xFF) * 257; + render_color.alpha = 0xFFFF; + + XftColorAllocValue(dpy, DefaultVisual(dpy, dwn->screen), + dwn->colormap, &render_color, &xft_color); + + XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, + text_x, text_y, + (const FcChar8 *)display_title, strlen(display_title)); + + XftColorFree(dpy, DefaultVisual(dpy, dwn->screen), + dwn->colormap, &xft_color); + XftDrawDestroy(xft_draw); + } + } +} + +void decorations_render_buttons(Client *client, bool focused) +{ + if (client == NULL || client->frame == None) { + return; + } + + if (dwn == NULL || dwn->display == NULL || dwn->gc == None || dwn->config == NULL) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + int title_height = config_get_title_height(); + int border = client->border_width; + + /* Button positions (right-aligned) */ + int button_y = border + (title_height - BUTTON_SIZE) / 2; + int close_x = border + client->width - BUTTON_SIZE - BUTTON_PADDING; + int max_x = close_x - BUTTON_SIZE - BUTTON_PADDING; + int min_x = max_x - BUTTON_SIZE - BUTTON_PADDING; + + unsigned long bg_color = focused ? colors->title_focused_bg : colors->title_unfocused_bg; + + /* Close button (red X) */ + XSetForeground(dpy, dwn->gc, bg_color); + XFillRectangle(dpy, client->frame, dwn->gc, close_x, button_y, BUTTON_SIZE, BUTTON_SIZE); + XSetForeground(dpy, dwn->gc, 0xcc4444); /* Red */ + XDrawLine(dpy, client->frame, dwn->gc, + close_x + 3, button_y + 3, + close_x + BUTTON_SIZE - 4, button_y + BUTTON_SIZE - 4); + XDrawLine(dpy, client->frame, dwn->gc, + close_x + BUTTON_SIZE - 4, button_y + 3, + close_x + 3, button_y + BUTTON_SIZE - 4); + + /* Maximize button (square) */ + XSetForeground(dpy, dwn->gc, bg_color); + XFillRectangle(dpy, client->frame, dwn->gc, max_x, button_y, BUTTON_SIZE, BUTTON_SIZE); + XSetForeground(dpy, dwn->gc, 0x44cc44); /* Green */ + XDrawRectangle(dpy, client->frame, dwn->gc, + max_x + 3, button_y + 3, + BUTTON_SIZE - 7, BUTTON_SIZE - 7); + + /* Minimize button (line) */ + XSetForeground(dpy, dwn->gc, bg_color); + XFillRectangle(dpy, client->frame, dwn->gc, min_x, button_y, BUTTON_SIZE, BUTTON_SIZE); + XSetForeground(dpy, dwn->gc, 0xcccc44); /* Yellow */ + XDrawLine(dpy, client->frame, dwn->gc, + min_x + 3, button_y + BUTTON_SIZE - 5, + min_x + BUTTON_SIZE - 4, button_y + BUTTON_SIZE - 5); +} + +void decorations_render_border(Client *client, bool focused) +{ + if (client == NULL || client->frame == None) { + return; + } + + if (dwn == NULL || dwn->display == NULL || dwn->config == NULL) { + return; + } + + const ColorScheme *colors = config_get_colors(); + unsigned long border_color = focused ? colors->border_focused : colors->border_unfocused; + + XSetWindowBorder(dwn->display, client->frame, border_color); +} + +/* ========== Hit testing ========== */ + +ButtonType decorations_hit_test_button(Client *client, int x, int y) +{ + if (client == NULL) { + return BUTTON_COUNT; /* No button hit */ + } + + int title_height = config_get_title_height(); + int border = client->border_width; + + /* Check if in title bar area */ + if (y < border || y > border + title_height) { + return BUTTON_COUNT; + } + + int button_y = border + (title_height - BUTTON_SIZE) / 2; + int close_x = border + client->width - BUTTON_SIZE - BUTTON_PADDING; + int max_x = close_x - BUTTON_SIZE - BUTTON_PADDING; + int min_x = max_x - BUTTON_SIZE - BUTTON_PADDING; + + /* Check close button */ + if (x >= close_x && x < close_x + BUTTON_SIZE && + y >= button_y && y < button_y + BUTTON_SIZE) { + return BUTTON_CLOSE; + } + + /* Check maximize button */ + if (x >= max_x && x < max_x + BUTTON_SIZE && + y >= button_y && y < button_y + BUTTON_SIZE) { + return BUTTON_MAXIMIZE; + } + + /* Check minimize button */ + if (x >= min_x && x < min_x + BUTTON_SIZE && + y >= button_y && y < button_y + BUTTON_SIZE) { + return BUTTON_MINIMIZE; + } + + return BUTTON_COUNT; +} + +bool decorations_hit_test_title_bar(Client *client, int x, int y) +{ + if (client == NULL) { + return false; + } + + int title_height = config_get_title_height(); + int border = client->border_width; + + /* In title bar but not on a button */ + if (y >= border && y < border + title_height) { + ButtonType btn = decorations_hit_test_button(client, x, y); + return btn == BUTTON_COUNT; + } + + return false; +} + +bool decorations_hit_test_resize_area(Client *client, int x, int y, int *direction) +{ + if (client == NULL || direction == NULL) { + return false; + } + + int title_height = config_get_title_height(); + int border = client->border_width; + int frame_width = client->width + 2 * border; + int frame_height = client->height + title_height + 2 * border; + + *direction = 0; + + /* Check edges */ + bool left = (x < RESIZE_EDGE); + bool right = (x > frame_width - RESIZE_EDGE); + bool top = (y < RESIZE_EDGE); + bool bottom = (y > frame_height - RESIZE_EDGE); + + if (!left && !right && !top && !bottom) { + return false; + } + + /* Encode direction as bitmask */ + if (left) *direction |= 1; + if (right) *direction |= 2; + if (top) *direction |= 4; + if (bottom) *direction |= 8; + + return true; +} + +/* ========== Button actions ========== */ + +void decorations_button_press(Client *client, ButtonType button) +{ + if (client == NULL) { + return; + } + + switch (button) { + case BUTTON_CLOSE: + LOG_DEBUG("Close button pressed for %s", client->title); + client_close(client); + break; + + case BUTTON_MAXIMIZE: + LOG_DEBUG("Maximize button pressed for %s", client->title); + client_toggle_fullscreen(client); + break; + + case BUTTON_MINIMIZE: + LOG_DEBUG("Minimize button pressed for %s", client->title); + client_minimize(client); + break; + + default: + break; + } +} + +/* ========== Text rendering ========== */ + +void decorations_draw_text(Window window, GC gc, int x, int y, + const char *text, unsigned long color) +{ + if (text == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + /* Use Xft for UTF-8 support */ + if (dwn->xft_font != NULL) { + XftDraw *xft_draw = XftDrawCreate(dwn->display, window, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap); + if (xft_draw != NULL) { + XftColor xft_color; + XRenderColor render_color; + render_color.red = ((color >> 16) & 0xFF) * 257; + render_color.green = ((color >> 8) & 0xFF) * 257; + render_color.blue = (color & 0xFF) * 257; + render_color.alpha = 0xFFFF; + + XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &render_color, &xft_color); + + XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, + x, y, (const FcChar8 *)text, strlen(text)); + + XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &xft_color); + XftDrawDestroy(xft_draw); + return; + } + } + + /* Fallback to legacy X11 text */ + XSetForeground(dwn->display, gc, color); + XDrawString(dwn->display, window, gc, x, y, text, strlen(text)); +} diff --git a/src/keys.c b/src/keys.c new file mode 100644 index 0000000..3167aee --- /dev/null +++ b/src/keys.c @@ -0,0 +1,842 @@ +/* + * DWN - Desktop Window Manager + * Keyboard shortcut handling implementation + */ + +#include "keys.h" +#include "client.h" +#include "workspace.h" +#include "config.h" +#include "util.h" +#include "ai.h" +#include "notifications.h" +#include "news.h" +#include "applauncher.h" + +#include +#include + +/* Forward declarations for key callbacks */ +void key_spawn_terminal(void); +void key_spawn_launcher(void); +void key_spawn_file_manager(void); +void key_spawn_browser(void); +void key_close_window(void); +void key_quit_dwn(void); +void key_cycle_layout(void); +void key_toggle_floating(void); +void key_toggle_fullscreen(void); +void key_toggle_maximize(void); +void key_focus_next(void); +void key_focus_prev(void); +void key_workspace_next(void); +void key_workspace_prev(void); +void key_workspace_1(void); +void key_workspace_2(void); +void key_workspace_3(void); +void key_workspace_4(void); +void key_workspace_5(void); +void key_workspace_6(void); +void key_workspace_7(void); +void key_workspace_8(void); +void key_workspace_9(void); +void key_move_to_workspace_1(void); +void key_move_to_workspace_2(void); +void key_move_to_workspace_3(void); +void key_move_to_workspace_4(void); +void key_move_to_workspace_5(void); +void key_move_to_workspace_6(void); +void key_move_to_workspace_7(void); +void key_move_to_workspace_8(void); +void key_move_to_workspace_9(void); +void key_increase_master(void); +void key_decrease_master(void); +void key_increase_master_count(void); +void key_decrease_master_count(void); +void key_toggle_ai(void); +void key_ai_command(void); +void key_exa_search(void); +void key_news_next(void); +void key_news_prev(void); +void key_news_open(void); +void key_show_shortcuts(void); +void key_start_tutorial(void); +void key_screenshot(void); + +/* Key bindings storage */ +static KeyBinding bindings[MAX_KEYBINDINGS]; +static int binding_count = 0; + +/* ========== Tutorial System ========== */ + +typedef struct { + const char *title; + const char *instruction; + const char *hint; + unsigned int modifiers; + KeySym keysym; +} TutorialStep; + +static const TutorialStep tutorial_steps[] = { + { + "Welcome to DWN!", + "This tutorial teaches all keyboard shortcuts.\n\n" + "Press Super+T to continue...", + "(Super = Windows/Meta key)", + MOD_SUPER, XK_t + }, + /* === Applications === */ + { + "1/20: Open Terminal", + "The terminal is your command center.\n\n" + "Press: Ctrl + Alt + T", + "Hold Ctrl and Alt, then press T", + MOD_CTRL | MOD_ALT, XK_t + }, + { + "2/20: App Launcher", + "Launch any application by name.\n\n" + "Press: Alt + F2", + "Type app name and press Enter", + MOD_ALT, XK_F2 + }, + { + "3/20: File Manager", + "Open the file manager.\n\n" + "Press: Super + E", + "", + MOD_SUPER, XK_e + }, + { + "4/20: Web Browser", + "Open your default web browser.\n\n" + "Press: Super + B", + "", + MOD_SUPER, XK_b + }, + /* === Window Management === */ + { + "5/20: Switch Windows", + "Cycle through open windows.\n\n" + "Press: Alt + Tab", + "Hold Alt, press Tab repeatedly", + MOD_ALT, XK_Tab + }, + { + "6/20: Close Window", + "Close the focused window.\n\n" + "Press: Alt + F4", + "", + MOD_ALT, XK_F4 + }, + { + "7/20: Toggle Maximize", + "Maximize or restore a window.\n\n" + "Press: Alt + F10", + "", + MOD_ALT, XK_F10 + }, + { + "8/20: Toggle Fullscreen", + "Make a window fullscreen.\n\n" + "Press: Alt + F11", + "Press again to exit", + MOD_ALT, XK_F11 + }, + { + "9/20: Toggle Floating", + "Make window float above tiled windows.\n\n" + "Press: Super + F9", + "", + MOD_SUPER, XK_F9 + }, + /* === Workspaces === */ + { + "10/20: Next Workspace", + "Switch to the next virtual desktop.\n\n" + "Press: Ctrl + Alt + Right", + "", + MOD_CTRL | MOD_ALT, XK_Right + }, + { + "11/20: Previous Workspace", + "Switch to the previous workspace.\n\n" + "Press: Ctrl + Alt + Left", + "", + MOD_CTRL | MOD_ALT, XK_Left + }, + { + "12/20: Go to Workspace", + "Jump directly to workspace 1.\n\n" + "Press: F1", + "Use F1-F9 for workspaces 1-9", + 0, XK_F1 + }, + /* === Layout === */ + { + "13/20: Cycle Layout", + "Switch between tiling, floating, monocle.\n\n" + "Press: Super + Space", + "Try it multiple times!", + MOD_SUPER, XK_space + }, + { + "14/20: Expand Master", + "Make the master area larger.\n\n" + "Press: Super + L", + "", + MOD_SUPER, XK_l + }, + { + "15/20: Shrink Master", + "Make the master area smaller.\n\n" + "Press: Super + H", + "", + MOD_SUPER, XK_h + }, + { + "16/20: Add to Master", + "Increase windows in master area.\n\n" + "Press: Super + I", + "", + MOD_SUPER, XK_i + }, + /* === AI Features === */ + { + "17/20: AI Context", + "Show AI analysis of your current task.\n\n" + "Press: Super + A", + "Requires OPENROUTER_API_KEY", + MOD_SUPER, XK_a + }, + { + "18/20: AI Command", + "Ask AI to run commands for you.\n\n" + "Press: Super + Shift + A", + "Requires OPENROUTER_API_KEY", + MOD_SUPER | MOD_SHIFT, XK_a + }, + { + "19/20: Exa Search", + "Semantic web search.\n\n" + "Press: Super + Shift + E", + "Requires EXA_API_KEY", + MOD_SUPER | MOD_SHIFT, XK_e + }, + /* === Help & System === */ + { + "20/20: Show Shortcuts", + "Display all keyboard shortcuts.\n\n" + "Press: Super + S", + "Shows complete reference", + MOD_SUPER, XK_s + }, + { + "Tutorial Complete!", + "You've learned all DWN shortcuts!\n\n" + "Super+S: Show all shortcuts\n" + "Super+T: Restart this tutorial\n\n" + "Press: Super + Backspace to quit DWN", + "Congratulations!", + MOD_SUPER, XK_BackSpace + } +}; + +#define TUTORIAL_STEPS (sizeof(tutorial_steps) / sizeof(tutorial_steps[0])) + +static bool tutorial_active = false; +static int tutorial_current_step = 0; + +bool tutorial_is_active(void) +{ + return tutorial_active; +} + +void tutorial_start(void) +{ + tutorial_active = true; + tutorial_current_step = 0; + + const TutorialStep *step = &tutorial_steps[0]; + char msg[512]; + snprintf(msg, sizeof(msg), "%s\n\n%s", step->instruction, step->hint); + notification_show("DWN Tutorial", step->title, msg, NULL, 0); /* No timeout */ +} + +void tutorial_stop(void) +{ + tutorial_active = false; + tutorial_current_step = 0; + notification_show("DWN Tutorial", "Tutorial Stopped", + "Press Super+T to restart anytime.", NULL, 3000); +} + +void tutorial_next_step(void) +{ + tutorial_current_step++; + + if (tutorial_current_step >= (int)TUTORIAL_STEPS) { + tutorial_active = false; + tutorial_current_step = 0; + return; + } + + const TutorialStep *step = &tutorial_steps[tutorial_current_step]; + char msg[512]; + snprintf(msg, sizeof(msg), "%s\n\n%s", step->instruction, step->hint); + notification_show("DWN Tutorial", step->title, msg, NULL, 0); +} + +void tutorial_check_key(unsigned int modifiers, KeySym keysym) +{ + if (!tutorial_active) { + return; + } + + const TutorialStep *step = &tutorial_steps[tutorial_current_step]; + + /* Check if the pressed key matches the expected key */ + if (modifiers == step->modifiers && keysym == step->keysym) { + /* Correct key! Move to next step */ + tutorial_next_step(); + } +} + +/* ========== Initialization ========== */ + +void keys_init(void) +{ + binding_count = 0; + memset(bindings, 0, sizeof(bindings)); + + keys_setup_defaults(); + keys_grab_all(); + + LOG_INFO("Keyboard shortcuts initialized (%d bindings)", binding_count); +} + +void keys_cleanup(void) +{ + keys_ungrab_all(); + keys_clear_all(); +} + +void keys_grab_all(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + Display *dpy = dwn->display; + Window root = dwn->root; + + /* Ungrab first to avoid errors */ + XUngrabKey(dpy, AnyKey, AnyModifier, root); + + /* Grab all registered bindings */ + for (int i = 0; i < binding_count; i++) { + KeyCode code = XKeysymToKeycode(dpy, bindings[i].keysym); + if (code == 0) { + LOG_WARN("Could not get keycode for keysym %lu", bindings[i].keysym); + continue; + } + + /* Grab with and without NumLock/CapsLock */ + unsigned int modifiers[] = { + bindings[i].modifiers, + bindings[i].modifiers | Mod2Mask, /* NumLock */ + bindings[i].modifiers | LockMask, /* CapsLock */ + bindings[i].modifiers | Mod2Mask | LockMask + }; + + for (size_t j = 0; j < sizeof(modifiers) / sizeof(modifiers[0]); j++) { + XGrabKey(dpy, code, modifiers[j], root, True, + GrabModeAsync, GrabModeAsync); + } + } +} + +void keys_ungrab_all(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + XUngrabKey(dwn->display, AnyKey, AnyModifier, dwn->root); +} + +/* ========== Key binding registration ========== */ + +void keys_bind(unsigned int modifiers, KeySym keysym, + KeyCallback callback, const char *description) +{ + if (binding_count >= MAX_KEYBINDINGS) { + LOG_WARN("Maximum key bindings reached"); + return; + } + + bindings[binding_count].modifiers = modifiers; + bindings[binding_count].keysym = keysym; + bindings[binding_count].callback = callback; + bindings[binding_count].description = description; + binding_count++; +} + +void keys_unbind(unsigned int modifiers, KeySym keysym) +{ + for (int i = 0; i < binding_count; i++) { + if (bindings[i].modifiers == modifiers && + bindings[i].keysym == keysym) { + /* Shift remaining bindings */ + memmove(&bindings[i], &bindings[i + 1], + (binding_count - i - 1) * sizeof(KeyBinding)); + binding_count--; + return; + } + } +} + +void keys_clear_all(void) +{ + binding_count = 0; + memset(bindings, 0, sizeof(bindings)); +} + +/* ========== Key event handling ========== */ + +void keys_handle_press(XKeyEvent *ev) +{ + if (ev == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + KeySym keysym = XLookupKeysym(ev, 0); + + /* Clean modifiers (remove NumLock, CapsLock) */ + unsigned int clean_mask = ev->state & ~(Mod2Mask | LockMask); + + /* Check tutorial progress */ + if (tutorial_is_active()) { + tutorial_check_key(clean_mask, keysym); + } + + for (int i = 0; i < binding_count; i++) { + if (bindings[i].keysym == keysym && + bindings[i].modifiers == clean_mask) { + if (bindings[i].callback != NULL) { + LOG_DEBUG("Key binding triggered: %s", bindings[i].description); + bindings[i].callback(); + } + return; + } + } +} + +void keys_handle_release(XKeyEvent *ev) +{ + /* Currently no release handling needed */ + (void)ev; +} + +/* ========== Default key bindings (XFCE-style) ========== */ + +void keys_setup_defaults(void) +{ + /* ===== XFCE-style shortcuts ===== */ + + /* Terminal: Ctrl+Alt+T (XFCE default) */ + keys_bind(MOD_CTRL | MOD_ALT, XK_t, key_spawn_terminal, "Spawn terminal"); + + /* Application finder: Alt+F2 (XFCE default) */ + keys_bind(MOD_ALT, XK_F2, key_spawn_launcher, "Application finder"); + + /* File manager: Super+E (XFCE default) */ + keys_bind(MOD_SUPER, XK_e, key_spawn_file_manager, "File manager"); + + /* Browser: Super+B */ + keys_bind(MOD_SUPER, XK_b, key_spawn_browser, "Web browser"); + + /* Close window: Alt+F4 (XFCE default) */ + keys_bind(MOD_ALT, XK_F4, key_close_window, "Close window"); + + /* Maximize toggle: Alt+F10 (XFCE default) */ + keys_bind(MOD_ALT, XK_F10, key_toggle_maximize, "Toggle maximize"); + + /* Fullscreen: Alt+F11 (XFCE default) */ + keys_bind(MOD_ALT, XK_F11, key_toggle_fullscreen, "Toggle fullscreen"); + + /* Cycle windows: Alt+Tab (XFCE default) */ + keys_bind(MOD_ALT, XK_Tab, key_focus_next, "Cycle windows"); + keys_bind(MOD_ALT | MOD_SHIFT, XK_Tab, key_focus_prev, "Cycle windows reverse"); + + /* Toggle floating: Super+F9 */ + keys_bind(MOD_SUPER, XK_F9, key_toggle_floating, "Toggle floating"); + + /* Next/Previous workspace: Ctrl+Alt+Right/Left (XFCE default) */ + keys_bind(MOD_CTRL | MOD_ALT, XK_Right, key_workspace_next, "Next workspace"); + keys_bind(MOD_CTRL | MOD_ALT, XK_Left, key_workspace_prev, "Previous workspace"); + + /* Switch to workspace: F1-F9 */ + keys_bind(0, XK_F1, key_workspace_1, "Switch to workspace 1"); + keys_bind(0, XK_F2, key_workspace_2, "Switch to workspace 2"); + keys_bind(0, XK_F3, key_workspace_3, "Switch to workspace 3"); + keys_bind(0, XK_F4, key_workspace_4, "Switch to workspace 4"); + keys_bind(0, XK_F5, key_workspace_5, "Switch to workspace 5"); + keys_bind(0, XK_F6, key_workspace_6, "Switch to workspace 6"); + keys_bind(0, XK_F7, key_workspace_7, "Switch to workspace 7"); + keys_bind(0, XK_F8, key_workspace_8, "Switch to workspace 8"); + keys_bind(0, XK_F9, key_workspace_9, "Switch to workspace 9"); + + /* Move to workspace: Shift+F1-F9 */ + keys_bind(MOD_SHIFT, XK_F1, key_move_to_workspace_1, "Move to workspace 1"); + keys_bind(MOD_SHIFT, XK_F2, key_move_to_workspace_2, "Move to workspace 2"); + keys_bind(MOD_SHIFT, XK_F3, key_move_to_workspace_3, "Move to workspace 3"); + keys_bind(MOD_SHIFT, XK_F4, key_move_to_workspace_4, "Move to workspace 4"); + keys_bind(MOD_SHIFT, XK_F5, key_move_to_workspace_5, "Move to workspace 5"); + keys_bind(MOD_SHIFT, XK_F6, key_move_to_workspace_6, "Move to workspace 6"); + keys_bind(MOD_SHIFT, XK_F7, key_move_to_workspace_7, "Move to workspace 7"); + keys_bind(MOD_SHIFT, XK_F8, key_move_to_workspace_8, "Move to workspace 8"); + keys_bind(MOD_SHIFT, XK_F9, key_move_to_workspace_9, "Move to workspace 9"); + + /* ===== DWN-specific shortcuts (all use Super key) ===== */ + + /* Quit DWN: Super+BackSpace */ + keys_bind(MOD_SUPER, XK_BackSpace, key_quit_dwn, "Quit DWN"); + + /* Cycle layout mode: Super+Space */ + keys_bind(MOD_SUPER, XK_space, key_cycle_layout, "Cycle layout"); + + /* Master area adjustments: Super+H/L/I/D */ + keys_bind(MOD_SUPER, XK_l, key_increase_master, "Increase master ratio"); + keys_bind(MOD_SUPER, XK_h, key_decrease_master, "Decrease master ratio"); + keys_bind(MOD_SUPER, XK_i, key_increase_master_count, "Increase master count"); + keys_bind(MOD_SUPER, XK_d, key_decrease_master_count, "Decrease master count"); + + /* AI: Super+A */ + keys_bind(MOD_SUPER, XK_a, key_toggle_ai, "Toggle AI context"); + keys_bind(MOD_SUPER | MOD_SHIFT, XK_a, key_ai_command, "AI command"); + + /* Exa semantic search: Super+Shift+E */ + keys_bind(MOD_SUPER | MOD_SHIFT, XK_e, key_exa_search, "Exa web search"); + + /* Show shortcuts: Super+S */ + keys_bind(MOD_SUPER, XK_s, key_show_shortcuts, "Show shortcuts"); + + /* Tutorial: Super+T */ + keys_bind(MOD_SUPER, XK_t, key_start_tutorial, "Start tutorial"); + + /* ===== News ticker navigation ===== */ + /* Super+Down: Next news article */ + keys_bind(MOD_SUPER, XK_Down, key_news_next, "Next news article"); + + /* Super+Up: Previous news article */ + keys_bind(MOD_SUPER, XK_Up, key_news_prev, "Previous news article"); + + /* Super+Return: Open current news article */ + keys_bind(MOD_SUPER, XK_Return, key_news_open, "Open news article"); + + /* Screenshot: Print Screen (XFCE default) */ + keys_bind(0, XK_Print, key_screenshot, "Take screenshot"); +} + +/* ========== Key binding callbacks ========== */ + +void key_spawn_terminal(void) +{ + const char *terminal = config_get_terminal(); + spawn_async(terminal); +} + +void key_spawn_launcher(void) +{ + applauncher_show(); +} + +void key_spawn_file_manager(void) +{ + if (dwn != NULL && dwn->config != NULL) { + spawn_async(dwn->config->file_manager); + } else { + spawn_async("thunar"); + } +} + +void key_spawn_browser(void) +{ + spawn_async("xdg-open http://"); +} + +void key_close_window(void) +{ + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + client_close(ws->focused); + } +} + +void key_quit_dwn(void) +{ + LOG_INFO("Quit requested via keyboard shortcut"); + dwn_quit(); +} + +void key_cycle_layout(void) +{ + if (dwn == NULL) { + return; + } + workspace_cycle_layout(dwn->current_workspace); +} + +void key_toggle_floating(void) +{ + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + client_toggle_floating(ws->focused); + workspace_arrange_current(); + } +} + +void key_toggle_fullscreen(void) +{ + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + client_toggle_fullscreen(ws->focused); + } +} + +void key_toggle_maximize(void) +{ + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + /* Toggle between maximized and normal state */ + /* For now, use fullscreen as maximize equivalent */ + client_toggle_fullscreen(ws->focused); + } +} + +void key_focus_next(void) +{ + workspace_focus_next(); +} + +void key_focus_prev(void) +{ + workspace_focus_prev(); +} + +void key_workspace_next(void) +{ + if (dwn == NULL) { + return; + } + int next = dwn->current_workspace + 1; + if (next >= MAX_WORKSPACES) { + next = 0; /* Wrap around */ + } + workspace_switch(next); +} + +void key_workspace_prev(void) +{ + if (dwn == NULL) { + return; + } + int prev = dwn->current_workspace - 1; + if (prev < 0) { + prev = MAX_WORKSPACES - 1; /* Wrap around */ + } + workspace_switch(prev); +} + +/* Workspace switching */ +void key_workspace_1(void) { workspace_switch(0); } +void key_workspace_2(void) { workspace_switch(1); } +void key_workspace_3(void) { workspace_switch(2); } +void key_workspace_4(void) { workspace_switch(3); } +void key_workspace_5(void) { workspace_switch(4); } +void key_workspace_6(void) { workspace_switch(5); } +void key_workspace_7(void) { workspace_switch(6); } +void key_workspace_8(void) { workspace_switch(7); } +void key_workspace_9(void) { workspace_switch(8); } + +/* Move to workspace */ +static void move_focused_to_workspace(int ws) +{ + Workspace *current = workspace_get_current(); + if (current != NULL && current->focused != NULL) { + workspace_move_client(current->focused, ws); + } +} + +void key_move_to_workspace_1(void) { move_focused_to_workspace(0); } +void key_move_to_workspace_2(void) { move_focused_to_workspace(1); } +void key_move_to_workspace_3(void) { move_focused_to_workspace(2); } +void key_move_to_workspace_4(void) { move_focused_to_workspace(3); } +void key_move_to_workspace_5(void) { move_focused_to_workspace(4); } +void key_move_to_workspace_6(void) { move_focused_to_workspace(5); } +void key_move_to_workspace_7(void) { move_focused_to_workspace(6); } +void key_move_to_workspace_8(void) { move_focused_to_workspace(7); } +void key_move_to_workspace_9(void) { move_focused_to_workspace(8); } + +/* Master area adjustments */ +void key_increase_master(void) +{ + if (dwn == NULL) { + return; + } + workspace_adjust_master_ratio(dwn->current_workspace, 0.05f); +} + +void key_decrease_master(void) +{ + if (dwn == NULL) { + return; + } + workspace_adjust_master_ratio(dwn->current_workspace, -0.05f); +} + +void key_increase_master_count(void) +{ + if (dwn == NULL) { + return; + } + workspace_adjust_master_count(dwn->current_workspace, 1); +} + +void key_decrease_master_count(void) +{ + if (dwn == NULL) { + return; + } + workspace_adjust_master_count(dwn->current_workspace, -1); +} + +/* AI functions */ +void key_toggle_ai(void) +{ + if (dwn == NULL) { + return; + } + + if (!dwn->ai_enabled) { + notification_show("DWN AI", "AI Disabled", + "Set OPENROUTER_API_KEY to enable AI features", + NULL, 3000); + return; + } + + /* Update and show AI context */ + ai_update_context(); + const char *task = ai_analyze_task(); + const char *suggestion = ai_suggest_window(); + + char body[512]; + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + snprintf(body, sizeof(body), + "Current task: %s\nFocused: %s\n%s", + task ? task : "Unknown", + ws->focused->title[0] ? ws->focused->title : "(unnamed)", + suggestion ? suggestion : "Press Alt+A to ask AI for help"); + } else { + snprintf(body, sizeof(body), + "No window focused.\nPress Alt+A to ask AI for help"); + } + + notification_show("DWN AI", "Context Analysis", body, NULL, 4000); +} + +void key_ai_command(void) +{ + if (dwn == NULL) { + return; + } + + if (dwn->ai_enabled) { + ai_show_command_palette(); + } else { + LOG_INFO("AI not enabled (no API key)"); + } +} + +void key_exa_search(void) +{ + exa_show_app_launcher(); +} + +void key_show_shortcuts(void) +{ + const char *shortcuts = + "=== Applications ===\n" + "Ctrl+Alt+T Terminal\n" + "Alt+F2 App launcher\n" + "Super+E File manager\n" + "Super+B Web browser\n" + "Print Screenshot\n" + "\n" + "=== Window Management ===\n" + "Alt+F4 Close window\n" + "Alt+Tab Next window\n" + "Alt+Shift+Tab Previous window\n" + "Alt+F10 Toggle maximize\n" + "Alt+F11 Toggle fullscreen\n" + "Super+F9 Toggle floating\n" + "\n" + "=== Workspaces ===\n" + "F1-F9 Switch to workspace\n" + "Shift+F1-F9 Move window to workspace\n" + "Ctrl+Alt+Right Next workspace\n" + "Ctrl+Alt+Left Previous workspace\n" + "\n" + "=== Layout Control ===\n" + "Super+Space Cycle layout (tile/float/mono)\n" + "Super+H Shrink master area\n" + "Super+L Expand master area\n" + "Super+I Add to master\n" + "Super+D Remove from master\n" + "\n" + "=== AI Features ===\n" + "Super+A Show AI context\n" + "Super+Shift+A AI command palette\n" + "Super+Shift+E Exa semantic search\n" + "\n" + "=== Help & System ===\n" + "Super+S Show shortcuts (this)\n" + "Super+T Interactive tutorial\n" + "Super+Backspace Quit DWN"; + + notification_show("DWN Shortcuts", "Complete Reference", shortcuts, NULL, 0); +} + +void key_start_tutorial(void) +{ + if (tutorial_is_active()) { + /* If already in tutorial, this acts as "next" */ + tutorial_next_step(); + } else { + tutorial_start(); + } +} + +/* ========== News ticker callbacks ========== */ + +void key_news_next(void) +{ + news_next_article(); +} + +void key_news_prev(void) +{ + news_prev_article(); +} + +void key_news_open(void) +{ + news_open_current(); +} + +void key_screenshot(void) +{ + spawn_async("xfce4-screenshooter"); +} diff --git a/src/layout.c b/src/layout.c new file mode 100644 index 0000000..22631a9 --- /dev/null +++ b/src/layout.c @@ -0,0 +1,263 @@ +/* + * DWN - Desktop Window Manager + * Layout algorithms implementation + */ + +#include "layout.h" +#include "client.h" +#include "workspace.h" +#include "config.h" +#include "util.h" + +/* Layout names and symbols */ +static const char *layout_names[] = { + "Tiling", + "Floating", + "Monocle" +}; + +static const char *layout_symbols[] = { + "[]=", + "><>", + "[M]" +}; + +/* ========== Main arrangement function ========== */ + +void layout_arrange(int workspace) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + switch (ws->layout) { + case LAYOUT_TILING: + layout_arrange_tiling(workspace); + break; + case LAYOUT_FLOATING: + layout_arrange_floating(workspace); + break; + case LAYOUT_MONOCLE: + layout_arrange_monocle(workspace); + break; + default: + layout_arrange_tiling(workspace); + break; + } +} + +/* ========== Tiling layout ========== */ + +void layout_arrange_tiling(int workspace) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + /* Get usable screen area (excluding panels) */ + int area_x, area_y, area_width, area_height; + layout_get_usable_area(&area_x, &area_y, &area_width, &area_height); + + int gap = config_get_gap(); + int title_height = config_get_title_height(); + int border = config_get_border_width(); + + /* Count tiled (non-floating) clients */ + int n = layout_count_tiled_clients(workspace); + if (n == 0) { + return; + } + + int master_count = ws->master_count; + if (master_count > n) { + master_count = n; + } + + /* Calculate master area width */ + int master_width; + if (n <= master_count) { + master_width = area_width - 2 * gap; + } else { + master_width = (int)((area_width - 3 * gap) * ws->master_ratio); + } + + int stack_width = area_width - master_width - 3 * gap; + + /* Arrange windows */ + int i = 0; + int master_y = area_y + gap; + int stack_y = area_y + gap; + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace != (unsigned int)workspace) { + continue; + } + if (client_is_floating(c) || client_is_fullscreen(c) || client_is_minimized(c)) { + continue; + } + + int x, y, w, h; + + if (i < master_count) { + /* Master area */ + int master_h = (area_height - 2 * gap - (master_count - 1) * gap) / master_count; + + x = area_x + gap; + y = master_y; + w = master_width; + h = master_h; + + master_y += h + gap; + } else { + /* Stack area */ + int stack_count = n - master_count; + int stack_h = (area_height - 2 * gap - (stack_count - 1) * gap) / stack_count; + + x = area_x + gap + master_width + gap; + y = stack_y; + w = stack_width; + h = stack_h; + + stack_y += h + gap; + } + + /* Account for decorations */ + int actual_h = h - title_height - 2 * border; + int actual_w = w - 2 * border; + + if (actual_h < 50) actual_h = 50; + if (actual_w < 50) actual_w = 50; + + client_move_resize(c, x + border, y + title_height + border, + actual_w, actual_h); + + i++; + } +} + +/* ========== Floating layout ========== */ + +void layout_arrange_floating(int workspace) +{ + /* In floating mode, we don't rearrange windows automatically. + Just make sure all clients are configured properly. */ + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace != (unsigned int)workspace) { + continue; + } + if (client_is_minimized(c)) { + continue; + } + + /* Ensure window is within screen bounds */ + int area_x, area_y, area_width, area_height; + layout_get_usable_area(&area_x, &area_y, &area_width, &area_height); + + if (c->x < area_x) c->x = area_x; + if (c->y < area_y) c->y = area_y; + if (c->x + c->width > area_x + area_width) { + c->x = area_x + area_width - c->width; + } + if (c->y + c->height > area_y + area_height) { + c->y = area_y + area_height - c->height; + } + + client_configure(c); + } +} + +/* ========== Monocle layout ========== */ + +void layout_arrange_monocle(int workspace) +{ + /* Get usable screen area */ + int area_x, area_y, area_width, area_height; + layout_get_usable_area(&area_x, &area_y, &area_width, &area_height); + + int gap = config_get_gap(); + int title_height = config_get_title_height(); + int border = config_get_border_width(); + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace != (unsigned int)workspace) { + continue; + } + if (client_is_floating(c) || client_is_fullscreen(c) || client_is_minimized(c)) { + continue; + } + + /* All tiled windows take the full usable area */ + int x = area_x + gap; + int y = area_y + gap; + int w = area_width - 2 * gap - 2 * border; + int h = area_height - 2 * gap - title_height - 2 * border; + + if (h < 50) h = 50; + if (w < 50) w = 50; + + client_move_resize(c, x + border, y + title_height + border, w, h); + } +} + +/* ========== Helpers ========== */ + +int layout_get_usable_area(int *x, int *y, int *width, int *height) +{ + if (dwn == NULL) { + return -1; + } + + int panel_height = config_get_panel_height(); + bool top_panel = (dwn->config != NULL) ? dwn->config->top_panel_enabled : true; + bool bottom_panel = (dwn->config != NULL) ? dwn->config->bottom_panel_enabled : true; + + *x = 0; + *y = top_panel ? panel_height : 0; + *width = dwn->screen_width; + *height = dwn->screen_height; + + if (top_panel) { + *height -= panel_height; + } + if (bottom_panel) { + *height -= panel_height; + } + + return 0; +} + +int layout_count_tiled_clients(int workspace) +{ + int count = 0; + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace != (unsigned int)workspace) { + continue; + } + if (client_is_floating(c) || client_is_fullscreen(c) || client_is_minimized(c)) { + continue; + } + count++; + } + + return count; +} + +const char *layout_get_name(LayoutType layout) +{ + if (layout >= 0 && layout < LAYOUT_COUNT) { + return layout_names[layout]; + } + return "Unknown"; +} + +const char *layout_get_symbol(LayoutType layout) +{ + if (layout >= 0 && layout < LAYOUT_COUNT) { + return layout_symbols[layout]; + } + return "???"; +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..97a96fb --- /dev/null +++ b/src/main.c @@ -0,0 +1,1016 @@ +/* + * DWN - Desktop Window Manager + * Main entry point and event loop + */ + +#include "dwn.h" +#include "config.h" +#include "atoms.h" +#include "client.h" +#include "workspace.h" +#include "layout.h" +#include "decorations.h" +#include "panel.h" +#include "keys.h" +#include "notifications.h" +#include "systray.h" +#include "news.h" +#include "applauncher.h" +#include "ai.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Global state instance */ +DWNState *dwn = NULL; +static DWNState dwn_state; + +/* Signal handling */ +static volatile sig_atomic_t received_signal = 0; + +static void signal_handler(int sig) +{ + received_signal = sig; +} + +/* Crash signal handler - flush logs before dying */ +static void crash_signal_handler(int sig) +{ + /* Flush logs synchronously before crashing */ + log_flush(); + + /* Re-raise the signal with default handler to get proper crash behavior */ + signal(sig, SIG_DFL); + raise(sig); +} + +/* X11 error handlers */ +static int last_x_error = 0; /* Track last error for checking */ + +static int x_error_handler(Display *dpy, XErrorEvent *ev) +{ + char error_text[256]; + XGetErrorText(dpy, ev->error_code, error_text, sizeof(error_text)); + + /* Store last error code for functions that want to check */ + last_x_error = ev->error_code; + + /* Write all X errors to crash log for debugging */ + FILE *crash = fopen("/tmp/dwn_crash.log", "a"); + if (crash) { + fprintf(crash, "[X_ERROR] code=%d request=%d resource=%lu: %s\n", + ev->error_code, ev->request_code, ev->resourceid, error_text); + fflush(crash); + fsync(fileno(crash)); + fclose(crash); + } + + /* BadWindow errors are common and recoverable - just log and continue */ + if (ev->error_code == BadWindow) { + LOG_DEBUG("X11 BadWindow error (request %d, resource %lu) - window was destroyed", + ev->request_code, ev->resourceid); + return 0; /* Continue - this is not fatal */ + } + + /* BadMatch, BadValue, BadDrawable are also often recoverable */ + if (ev->error_code == BadMatch || ev->error_code == BadValue || + ev->error_code == BadDrawable || ev->error_code == BadPixmap) { + LOG_WARN("X11 error: %s (request %d, resource %lu) - continuing", + error_text, ev->request_code, ev->resourceid); + return 0; /* Continue - these are recoverable */ + } + + /* Log other errors but don't crash */ + LOG_WARN("X11 error: %s (request %d, resource %lu)", + error_text, ev->request_code, ev->resourceid); + return 0; +} + +static int x_io_error_handler(Display *dpy) +{ + (void)dpy; + + /* Write directly to crash log - do not rely on async logging */ + FILE *crash = fopen("/tmp/dwn_crash.log", "a"); + if (crash) { + fprintf(crash, "[IO_ERROR] Fatal X11 I/O error - X server connection lost\n"); + fflush(crash); + fsync(fileno(crash)); + fclose(crash); + } + fprintf(stderr, "[IO_ERROR] Fatal X11 I/O error - X server connection lost\n"); + fflush(stderr); + + /* I/O errors mean the X server connection is broken - we must exit */ + /* But first, try to flush any pending logs */ + log_flush(); + LOG_ERROR("Fatal X11 I/O error - X server connection lost"); + log_flush(); + _exit(EXIT_FAILURE); /* Use _exit to avoid cleanup that might touch X */ + return 0; /* Never reached */ +} + +/* Check if another WM is running */ +static int wm_detected = 0; + +static int wm_detect_error_handler(Display *dpy, XErrorEvent *ev) +{ + (void)dpy; + (void)ev; + wm_detected = 1; + return 0; +} + +static bool check_other_wm(void) +{ + XSetErrorHandler(wm_detect_error_handler); + + XSelectInput(dwn->display, dwn->root, + SubstructureRedirectMask | SubstructureNotifyMask); + XSync(dwn->display, False); + + XSetErrorHandler(x_error_handler); + + return wm_detected != 0; +} + +/* Initialize multi-monitor support */ +static void init_monitors(void) +{ + if (!XineramaIsActive(dwn->display)) { + /* Single monitor */ + dwn->monitors[0].x = 0; + dwn->monitors[0].y = 0; + dwn->monitors[0].width = dwn->screen_width; + dwn->monitors[0].height = dwn->screen_height; + dwn->monitors[0].index = 0; + dwn->monitors[0].primary = true; + dwn->monitor_count = 1; + return; + } + + int num_screens; + XineramaScreenInfo *info = XineramaQueryScreens(dwn->display, &num_screens); + + if (info == NULL || num_screens == 0) { + dwn->monitors[0].x = 0; + dwn->monitors[0].y = 0; + dwn->monitors[0].width = dwn->screen_width; + dwn->monitors[0].height = dwn->screen_height; + dwn->monitors[0].index = 0; + dwn->monitors[0].primary = true; + dwn->monitor_count = 1; + return; + } + + dwn->monitor_count = (num_screens > MAX_MONITORS) ? MAX_MONITORS : num_screens; + + for (int i = 0; i < dwn->monitor_count; i++) { + dwn->monitors[i].x = info[i].x_org; + dwn->monitors[i].y = info[i].y_org; + dwn->monitors[i].width = info[i].width; + dwn->monitors[i].height = info[i].height; + dwn->monitors[i].index = i; + dwn->monitors[i].primary = (i == 0); + } + + XFree(info); + + LOG_INFO("Detected %d monitor(s)", dwn->monitor_count); +} + +/* Scan for existing windows */ +static void scan_existing_windows(void) +{ + Window root_return, parent_return; + Window *children; + unsigned int num_children; + + if (XQueryTree(dwn->display, dwn->root, &root_return, &parent_return, + &children, &num_children)) { + for (unsigned int i = 0; i < num_children; i++) { + XWindowAttributes wa; + if (XGetWindowAttributes(dwn->display, children[i], &wa)) { + if (wa.map_state == IsViewable && wa.override_redirect == False) { + client_manage(children[i]); + } + } + } + if (children != NULL) { + XFree(children); + } + } + + LOG_INFO("Scanned %d existing window(s)", client_count()); +} + +/* ========== Event handlers ========== */ + +static void handle_map_request(XMapRequestEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (ev->window == None) { + LOG_WARN("handle_map_request: received invalid window (None)"); + return; + } + + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, ev->window, &wa)) { + LOG_WARN("handle_map_request: XGetWindowAttributes failed for window %lu", ev->window); + return; + } + + if (wa.override_redirect) { + return; + } + + client_manage(ev->window); +} + +static void handle_unmap_notify(XUnmapEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL) { + return; + } + + Client *c = client_find_by_window(ev->window); + if (c == NULL) { + return; + } + + /* Ignore synthetic events */ + if (ev->send_event) { + return; + } + + /* Don't unmanage windows that are intentionally hidden: + * - Windows on a different workspace (hidden during workspace switch) + * - Minimized windows + * These windows are still managed, just not visible */ + if (c->workspace != (unsigned int)dwn->current_workspace) { + return; + } + if (c->flags & CLIENT_MINIMIZED) { + return; + } + + /* Window was actually closed/withdrawn - unmanage it */ + client_unmanage(c); +} + +static void handle_destroy_notify(XDestroyWindowEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL) { + return; + } + + Client *c = client_find_by_window(ev->window); + if (c != NULL) { + client_unmanage(c); + } +} + +static void handle_configure_request(XConfigureRequestEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + Client *c = client_find_by_window(ev->window); + + if (c != NULL) { + /* Managed window - respect some requests for floating windows */ + if (client_is_floating(c) || client_is_fullscreen(c)) { + if (ev->value_mask & CWX) c->x = ev->x; + if (ev->value_mask & CWY) c->y = ev->y; + if (ev->value_mask & CWWidth) c->width = ev->width; + if (ev->value_mask & CWHeight) c->height = ev->height; + client_configure(c); + } else { + /* Just send configure notify with current geometry */ + client_configure(c); + } + } else { + /* Unmanaged window - pass through */ + XWindowChanges wc; + wc.x = ev->x; + wc.y = ev->y; + wc.width = ev->width; + wc.height = ev->height; + wc.border_width = ev->border_width; + wc.sibling = ev->above; + wc.stack_mode = ev->detail; + + XConfigureWindow(dwn->display, ev->window, ev->value_mask, &wc); + } +} + +static void handle_property_notify(XPropertyEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL) { + return; + } + + Client *c = client_find_by_window(ev->window); + if (c == NULL) { + return; + } + + if (ev->atom == icccm.WM_NAME || ev->atom == ewmh.NET_WM_NAME) { + client_update_title(c); + panel_render_all(); + } +} + +static void handle_expose(XExposeEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL) { + return; + } + + /* Only handle final expose in a sequence */ + if (ev->count != 0) { + return; + } + + /* Check if it's a panel */ + if (dwn->top_panel != NULL && ev->window == dwn->top_panel->window) { + panel_render(dwn->top_panel); + return; + } + if (dwn->bottom_panel != NULL && ev->window == dwn->bottom_panel->window) { + panel_render(dwn->bottom_panel); + return; + } + + /* Check if it's a notification */ + Notification *notif = notification_find_by_window(ev->window); + if (notif != NULL) { + notification_render(notif); + return; + } + + /* Check if it's a frame */ + Client *c = client_find_by_frame(ev->window); + if (c != NULL) { + Workspace *ws = workspace_get(c->workspace); + bool focused = (ws != NULL && ws->focused == c); + decorations_render(c, focused); + } +} + +static void handle_enter_notify(XCrossingEvent *ev) +{ + /* Defensive: validate all pointers */ + if (ev == NULL || dwn == NULL || dwn->config == NULL) { + return; + } + + if (dwn->config->focus_mode != FOCUS_FOLLOW) { + return; + } + + /* Focus on enter for follow-mouse mode */ + Client *c = client_find_by_frame(ev->window); + if (c == NULL) { + c = client_find_by_window(ev->window); + } + + if (c != NULL && c->workspace == (unsigned int)dwn->current_workspace) { + client_focus(c); + } +} + +static void handle_button_press(XButtonEvent *ev) +{ + /* Defensive: validate all pointers */ + if (ev == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + /* Check volume slider first */ + if (volume_slider != NULL && volume_slider->visible) { + if (ev->window == volume_slider->window) { + volume_slider_handle_click(volume_slider, ev->x, ev->y); + return; + } else { + /* Clicked outside slider - close it */ + volume_slider_hide(volume_slider); + } + } + + /* Check WiFi dropdown menu */ + if (wifi_menu != NULL && wifi_menu->visible) { + if (ev->window == wifi_menu->window) { + dropdown_handle_click(wifi_menu, ev->x, ev->y); + return; + } else { + /* Clicked outside dropdown - close it */ + dropdown_hide(wifi_menu); + } + } + + /* Check notifications first - clicking dismisses them */ + Notification *notif = notification_find_by_window(ev->window); + if (notif != NULL) { + notification_close(notif->id); + return; + } + + /* Check panels first */ + if (dwn->top_panel != NULL && ev->window == dwn->top_panel->window) { + panel_handle_click(dwn->top_panel, ev->x, ev->y, ev->button); + return; + } + if (dwn->bottom_panel != NULL && ev->window == dwn->bottom_panel->window) { + panel_handle_click(dwn->bottom_panel, ev->x, ev->y, ev->button); + return; + } + + /* Find client */ + Client *c = client_find_by_frame(ev->window); + bool is_client_window = false; + if (c == NULL) { + c = client_find_by_window(ev->window); + is_client_window = (c != NULL); + } + + if (c == NULL) { + return; + } + + /* Focus on click */ + client_focus(c); + + /* If click was on client window content, replay the event to the application */ + if (is_client_window) { + XAllowEvents(dwn->display, ReplayPointer, ev->time); + return; + } + + /* Check for button clicks in decorations */ + if (c->frame != None && ev->window == c->frame) { + ButtonType btn = decorations_hit_test_button(c, ev->x, ev->y); + if (btn != BUTTON_COUNT) { + decorations_button_press(c, btn); + return; + } + + /* Check for title bar drag */ + if (decorations_hit_test_title_bar(c, ev->x, ev->y)) { + if (ev->button == 1) { + /* Start move */ + dwn->drag_client = c; + dwn->drag_start_x = ev->x_root; + dwn->drag_start_y = ev->y_root; + dwn->drag_orig_x = c->x; + dwn->drag_orig_y = c->y; + dwn->resizing = false; + + XGrabPointer(dwn->display, c->frame, True, + PointerMotionMask | ButtonReleaseMask, + GrabModeAsync, GrabModeAsync, None, None, CurrentTime); + } + return; + } + + /* Check for resize */ + int direction; + if (decorations_hit_test_resize_area(c, ev->x, ev->y, &direction)) { + if (ev->button == 1) { + dwn->drag_client = c; + dwn->drag_start_x = ev->x_root; + dwn->drag_start_y = ev->y_root; + dwn->drag_orig_x = c->x; + dwn->drag_orig_y = c->y; + dwn->drag_orig_w = c->width; + dwn->drag_orig_h = c->height; + dwn->resizing = true; + + XGrabPointer(dwn->display, c->frame, True, + PointerMotionMask | ButtonReleaseMask, + GrabModeAsync, GrabModeAsync, None, None, CurrentTime); + } + } + } +} + +static void handle_button_release(XButtonEvent *ev) +{ + /* Defensive: validate pointers */ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + /* Handle volume slider release */ + if (volume_slider != NULL && volume_slider->visible && volume_slider->dragging) { + volume_slider_handle_release(volume_slider); + } + + (void)ev; + + if (dwn->drag_client != NULL) { + XUngrabPointer(dwn->display, CurrentTime); + dwn->drag_client = NULL; + } +} + +static void handle_motion_notify(XMotionEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + /* Check volume slider drag */ + if (volume_slider != NULL && volume_slider->visible && ev->window == volume_slider->window) { + volume_slider_handle_motion(volume_slider, ev->x, ev->y); + return; + } + + /* Check WiFi dropdown hover */ + if (wifi_menu != NULL && wifi_menu->visible && ev->window == wifi_menu->window) { + dropdown_handle_motion(wifi_menu, ev->x, ev->y); + return; + } + + if (dwn->drag_client == NULL) { + return; + } + + Client *c = dwn->drag_client; + if (c == NULL) { + dwn->drag_client = NULL; /* Reset invalid drag state */ + return; + } + + int dx = ev->x_root - dwn->drag_start_x; + int dy = ev->y_root - dwn->drag_start_y; + + if (dwn->resizing) { + int new_w = dwn->drag_orig_w + dx; + int new_h = dwn->drag_orig_h + dy; + if (new_w < 50) new_w = 50; + if (new_h < 50) new_h = 50; + client_resize(c, new_w, new_h); + } else { + client_move(c, dwn->drag_orig_x + dx, dwn->drag_orig_y + dy); + } +} + +static void handle_client_message(XClientMessageEvent *ev) +{ + /* Defensive: validate pointers */ + if (ev == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + Client *c = client_find_by_window(ev->window); + + if (ev->message_type == ewmh.NET_ACTIVE_WINDOW) { + if (c != NULL) { + if (c->workspace != (unsigned int)dwn->current_workspace) { + workspace_switch(c->workspace); + } + client_focus(c); + } + } else if (ev->message_type == ewmh.NET_CLOSE_WINDOW) { + if (c != NULL) { + client_close(c); + } + } else if (ev->message_type == ewmh.NET_WM_STATE) { + if (c != NULL) { + Atom action = ev->data.l[0]; + Atom prop1 = ev->data.l[1]; + Atom prop2 = ev->data.l[2]; + + bool set = (action == 1); + bool toggle = (action == 2); + + if (prop1 == ewmh.NET_WM_STATE_FULLSCREEN || + prop2 == ewmh.NET_WM_STATE_FULLSCREEN) { + if (toggle) { + client_toggle_fullscreen(c); + } else { + client_set_fullscreen(c, set); + } + } + } + } else if (ev->message_type == ewmh.NET_CURRENT_DESKTOP) { + int desktop = ev->data.l[0]; + if (desktop >= 0 && desktop < MAX_WORKSPACES) { + workspace_switch(desktop); + } + } +} + +/* Sync log - disabled in production (enable for debugging crashes) */ +#if 0 +static FILE *crash_log_file = NULL; + +static void sync_log(const char *msg) +{ + if (crash_log_file == NULL) { + crash_log_file = fopen("/tmp/dwn_crash.log", "a"); + } + fprintf(stderr, "[SYNC] %s\n", msg); + fflush(stderr); + if (crash_log_file != NULL) { + fprintf(crash_log_file, "[SYNC] %s\n", msg); + fflush(crash_log_file); + fsync(fileno(crash_log_file)); + } +} +#endif + +/* Main event dispatcher */ +void dwn_handle_event(XEvent *ev) +{ + /* Defensive: validate event pointer */ + if (ev == NULL) { + LOG_WARN("dwn_handle_event: received NULL event"); + return; + } + + /* Defensive: validate global state */ + if (dwn == NULL || dwn->display == NULL) { + LOG_ERROR("dwn_handle_event: dwn or display is NULL"); + return; + } + + switch (ev->type) { + case MapRequest: + handle_map_request(&ev->xmaprequest); + break; + case UnmapNotify: + handle_unmap_notify(&ev->xunmap); + break; + case DestroyNotify: + handle_destroy_notify(&ev->xdestroywindow); + break; + case ConfigureRequest: + handle_configure_request(&ev->xconfigurerequest); + break; + case PropertyNotify: + handle_property_notify(&ev->xproperty); + break; + case Expose: + handle_expose(&ev->xexpose); + break; + case EnterNotify: + handle_enter_notify(&ev->xcrossing); + break; + case ButtonPress: + handle_button_press(&ev->xbutton); + break; + case ButtonRelease: + handle_button_release(&ev->xbutton); + break; + case MotionNotify: + handle_motion_notify(&ev->xmotion); + break; + case KeyPress: + keys_handle_press(&ev->xkey); + break; + case KeyRelease: + keys_handle_release(&ev->xkey); + break; + case ClientMessage: + handle_client_message(&ev->xclient); + break; + default: + break; + } +} + +/* ========== Core functions ========== */ + +int dwn_init(void) +{ + /* Initialize global state */ + dwn = &dwn_state; + memset(dwn, 0, sizeof(DWNState)); + + /* Open display */ + dwn->display = XOpenDisplay(NULL); + if (dwn->display == NULL) { + fprintf(stderr, "Cannot open X display\n"); + return -1; + } + + dwn->screen = DefaultScreen(dwn->display); + dwn->root = RootWindow(dwn->display, dwn->screen); + dwn->screen_width = DisplayWidth(dwn->display, dwn->screen); + dwn->screen_height = DisplayHeight(dwn->display, dwn->screen); + dwn->colormap = DefaultColormap(dwn->display, dwn->screen); + + /* Set error handlers */ + XSetErrorHandler(x_error_handler); + XSetIOErrorHandler(x_io_error_handler); + + /* Check for other WM */ + if (check_other_wm()) { + fprintf(stderr, "Another window manager is already running\n"); + XCloseDisplay(dwn->display); + return -1; + } + + /* Initialize logging */ + log_init("~/.local/share/dwn/dwn.log"); + LOG_INFO("DWN %s starting", DWN_VERSION); + + /* Load configuration */ + dwn->config = config_create(); + config_load(dwn->config, NULL); + + /* Initialize colors (after display is open) */ + extern void config_init_colors(Config *cfg); + config_init_colors(dwn->config); + + /* Load font */ + dwn->font = XLoadQueryFont(dwn->display, dwn->config->font_name); + if (dwn->font == NULL) { + dwn->font = XLoadQueryFont(dwn->display, "fixed"); + } + + /* Load Xft font for UTF-8 support */ + dwn->xft_font = XftFontOpenName(dwn->display, dwn->screen, + "monospace:size=10:antialias=true"); + if (dwn->xft_font == NULL) { + dwn->xft_font = XftFontOpenName(dwn->display, dwn->screen, + "fixed:size=10"); + } + if (dwn->xft_font != NULL) { + LOG_INFO("Loaded Xft font for UTF-8 support"); + } + + /* Create GC */ + XGCValues gcv; + gcv.foreground = dwn->config->colors.panel_fg; + gcv.background = dwn->config->colors.panel_bg; + gcv.font = dwn->font ? dwn->font->fid : None; + dwn->gc = XCreateGC(dwn->display, dwn->root, + GCForeground | GCBackground | (dwn->font ? GCFont : 0), &gcv); + + /* Initialize atoms */ + atoms_init(dwn->display); + + /* Initialize monitors */ + init_monitors(); + + /* Initialize workspaces */ + workspace_init(); + + /* Initialize decorations */ + decorations_init(); + + /* Initialize panels */ + panels_init(); + + /* Initialize system tray (WiFi, Audio indicators) */ + systray_init(); + + /* Initialize news ticker */ + news_init(); + + /* Initialize app launcher */ + applauncher_init(); + + /* Initialize keyboard shortcuts */ + keys_init(); + + /* Initialize D-Bus notifications */ + notifications_init(); + + /* Initialize AI */ + ai_init(); + + /* Setup EWMH */ + atoms_setup_ewmh(); + + /* Select events on root window */ + XSelectInput(dwn->display, dwn->root, + SubstructureRedirectMask | SubstructureNotifyMask | + StructureNotifyMask | PropertyChangeMask | + ButtonPressMask | PointerMotionMask); + + /* Set cursor */ + Cursor cursor = XCreateFontCursor(dwn->display, XC_left_ptr); + XDefineCursor(dwn->display, dwn->root, cursor); + + /* Scan existing windows */ + scan_existing_windows(); + + /* Arrange initial workspace */ + workspace_arrange_current(); + + /* Render panels */ + panel_render_all(); + + dwn->running = true; + + LOG_INFO("DWN initialized successfully"); + + return 0; +} + +void dwn_cleanup(void) +{ + LOG_INFO("DWN shutting down"); + + /* Cleanup subsystems */ + ai_cleanup(); + notifications_cleanup(); + news_cleanup(); + applauncher_cleanup(); + keys_cleanup(); + systray_cleanup(); + panels_cleanup(); + decorations_cleanup(); + workspace_cleanup(); + + /* Unmanage all clients */ + while (dwn->client_list != NULL) { + client_unmanage(dwn->client_list); + } + + /* Free resources */ + if (dwn->gc != None) { + XFreeGC(dwn->display, dwn->gc); + } + if (dwn->font != NULL) { + XFreeFont(dwn->display, dwn->font); + } + if (dwn->xft_font != NULL) { + XftFontClose(dwn->display, dwn->xft_font); + } + if (dwn->config != NULL) { + config_destroy(dwn->config); + } + + /* Close display */ + if (dwn->display != NULL) { + XCloseDisplay(dwn->display); + } + + log_close(); + + dwn = NULL; +} + +void dwn_run(void) +{ + int x11_fd = ConnectionNumber(dwn->display); + int dbus_fd = -1; + + if (dbus_conn != NULL) { + dbus_connection_get_unix_fd(dbus_conn, &dbus_fd); + } + + long last_clock_update = 0; + long last_news_update = 0; + + while (dwn->running && received_signal == 0) { + /* Handle pending X events */ + while (XPending(dwn->display)) { + XEvent ev; + XNextEvent(dwn->display, &ev); + dwn_handle_event(&ev); + } + + /* Process D-Bus messages */ + notifications_process_messages(); + + /* Process AI requests */ + ai_process_pending(); + + /* Process Exa requests */ + exa_process_pending(); + + /* Update notifications (check for expired) */ + notifications_update(); + + long now = get_time_ms(); + + /* Update news ticker frequently for smooth scrolling (~60fps) */ + if (now - last_news_update >= 16) { + news_update(); + panel_render_all(); + last_news_update = now; + } + + /* Update clock and system stats every second */ + if (now - last_clock_update >= 1000) { + panel_update_clock(); + panel_update_system_stats(); + systray_update(); + last_clock_update = now; + } + + /* Wait for events with timeout - short for smooth animation */ + fd_set fds; + FD_ZERO(&fds); + FD_SET(x11_fd, &fds); + if (dbus_fd >= 0) { + FD_SET(dbus_fd, &fds); + } + + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 16000; /* ~16ms for 60fps smooth scrolling */ + + int max_fd = x11_fd; + if (dbus_fd > max_fd) max_fd = dbus_fd; + + select(max_fd + 1, &fds, NULL, NULL, &tv); + } + + /* Handle signal */ + if (received_signal != 0) { + LOG_INFO("Received signal %d", received_signal); + } +} + +void dwn_quit(void) +{ + dwn->running = false; +} + +/* ========== Main ========== */ + +static void print_usage(const char *program) +{ + printf("Usage: %s [OPTIONS]\n", program); + printf("\n"); + printf("Options:\n"); + printf(" -h, --help Show this help message\n"); + printf(" -v, --version Show version information\n"); + printf(" -c CONFIG Use specified config file\n"); + printf("\n"); + printf("Environment variables:\n"); + printf(" OPENROUTER_API_KEY Enable AI features\n"); + printf(" EXA_API_KEY Enable semantic search\n"); +} + +static void print_version(void) +{ + printf("DWN - Desktop Window Manager %s\n", DWN_VERSION); + printf("AI-enhanced X11 window manager\n"); +} + +int main(int argc, char *argv[]) +{ + /* Parse arguments */ + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return 0; + } + if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) { + print_version(); + return 0; + } + } + + /* Setup signal handlers */ + signal(SIGTERM, signal_handler); + signal(SIGINT, signal_handler); + signal(SIGHUP, signal_handler); + + /* Setup crash signal handlers to flush logs before dying */ + signal(SIGSEGV, crash_signal_handler); + signal(SIGABRT, crash_signal_handler); + signal(SIGFPE, crash_signal_handler); + signal(SIGBUS, crash_signal_handler); + signal(SIGILL, crash_signal_handler); + + /* Initialize */ + if (dwn_init() != 0) { + return EXIT_FAILURE; + } + + /* Run event loop */ + dwn_run(); + + /* Cleanup */ + dwn_cleanup(); + + return EXIT_SUCCESS; +} diff --git a/src/news.c b/src/news.c new file mode 100644 index 0000000..cd1d35e --- /dev/null +++ b/src/news.c @@ -0,0 +1,697 @@ +/* + * DWN - Desktop Window Manager + * News ticker implementation + */ + +#include "news.h" +#include "panel.h" +#include "config.h" +#include "util.h" +#include "cJSON.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Scroll speed in pixels per second */ +#define SCROLL_SPEED_PPS 80 + +/* Fetch interval in milliseconds (5 minutes) */ +#define FETCH_INTERVAL 300000 + +/* Global state */ +NewsState news_state = {0}; + +/* Thread safety */ +static pthread_mutex_t news_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_t fetch_thread; +static volatile int fetch_running = 0; + +/* CURL response buffer */ +typedef struct { + char *data; + size_t size; +} CurlBuffer; + +static size_t news_curl_write_cb(void *contents, size_t size, size_t nmemb, void *userp) +{ + size_t realsize = size * nmemb; + CurlBuffer *buf = (CurlBuffer *)userp; + + char *ptr = realloc(buf->data, buf->size + realsize + 1); + if (ptr == NULL) { + return 0; + } + + buf->data = ptr; + memcpy(&(buf->data[buf->size]), contents, realsize); + buf->size += realsize; + buf->data[buf->size] = '\0'; + + return realsize; +} + +/* Decode numeric HTML entity ({ or ) */ +static int decode_numeric_entity(const char *src, char *out, size_t max_out) +{ + if (src[0] != '&' || src[1] != '#') return 0; + + const char *p = src + 2; + int base = 10; + if (*p == 'x' || *p == 'X') { + base = 16; + p++; + } + + char *end; + long code = strtol(p, &end, base); + if (*end != ';' || code <= 0) return 0; + + int consumed = (end - src) + 1; + + if (code < 128 && max_out >= 1) { + out[0] = (char)code; + return consumed; + } else if (code < 0x800 && max_out >= 2) { + out[0] = 0xC0 | (code >> 6); + out[1] = 0x80 | (code & 0x3F); + return consumed; + } else if (code < 0x10000 && max_out >= 3) { + out[0] = 0xE0 | (code >> 12); + out[1] = 0x80 | ((code >> 6) & 0x3F); + out[2] = 0x80 | (code & 0x3F); + return consumed; + } + return 0; +} + +/* Strip HTML tags and decode entities */ +static void strip_html(char *dst, const char *src, size_t max_len) +{ + size_t j = 0; + bool in_tag = false; + + for (size_t i = 0; src[i] && j < max_len - 1; i++) { + if (src[i] == '<') { + in_tag = true; + } else if (src[i] == '>') { + in_tag = false; + } else if (!in_tag) { + if (src[i] == '&') { + /* Named entities */ + if (strncmp(&src[i], "&", 5) == 0) { + dst[j++] = '&'; i += 4; + } else if (strncmp(&src[i], "<", 4) == 0) { + dst[j++] = '<'; i += 3; + } else if (strncmp(&src[i], ">", 4) == 0) { + dst[j++] = '>'; i += 3; + } else if (strncmp(&src[i], """, 6) == 0) { + dst[j++] = '"'; i += 5; + } else if (strncmp(&src[i], "'", 6) == 0) { + dst[j++] = '\''; i += 5; + } else if (strncmp(&src[i], " ", 6) == 0) { + dst[j++] = ' '; i += 5; + } else if (strncmp(&src[i], "–", 7) == 0) { + dst[j++] = '-'; i += 6; + } else if (strncmp(&src[i], "—", 7) == 0) { + dst[j++] = '-'; dst[j++] = '-'; i += 6; + } else if (strncmp(&src[i], "…", 8) == 0) { + dst[j++] = '.'; dst[j++] = '.'; dst[j++] = '.'; i += 7; + } else if (strncmp(&src[i], "‘", 7) == 0 || strncmp(&src[i], "’", 7) == 0) { + dst[j++] = '\''; i += 6; + } else if (strncmp(&src[i], "“", 7) == 0 || strncmp(&src[i], "”", 7) == 0) { + dst[j++] = '"'; i += 6; + } else if (src[i+1] == '#') { + /* Numeric entity */ + char decoded[4] = {0}; + int consumed = decode_numeric_entity(&src[i], decoded, sizeof(decoded)); + if (consumed > 0) { + for (int k = 0; decoded[k] && j < max_len - 1; k++) { + dst[j++] = decoded[k]; + } + i += consumed - 1; + } else { + dst[j++] = src[i]; + } + } else { + dst[j++] = src[i]; + } + } else if (src[i] == '\n' || src[i] == '\r' || src[i] == '\t') { + dst[j++] = ' '; + } else { + dst[j++] = src[i]; + } + } + } + dst[j] = '\0'; + + /* Collapse multiple spaces */ + char *read = dst, *write = dst; + bool last_space = false; + while (*read) { + if (*read == ' ') { + if (!last_space) { + *write++ = *read; + } + last_space = true; + } else { + *write++ = *read; + last_space = false; + } + read++; + } + *write = '\0'; +} + +/* Parse JSON response and populate articles */ +static int parse_news_json(const char *json_str) +{ + cJSON *root = cJSON_Parse(json_str); + if (root == NULL) { + LOG_WARN("Failed to parse news JSON"); + return -1; + } + + cJSON *articles = cJSON_GetObjectItemCaseSensitive(root, "articles"); + if (!cJSON_IsArray(articles)) { + LOG_WARN("No articles array in news JSON"); + cJSON_Delete(root); + return -1; + } + + int count = 0; + cJSON *article; + cJSON_ArrayForEach(article, articles) { + if (count >= MAX_NEWS_ARTICLES) break; + + cJSON *title = cJSON_GetObjectItemCaseSensitive(article, "title"); + cJSON *content = cJSON_GetObjectItemCaseSensitive(article, "content"); + cJSON *description = cJSON_GetObjectItemCaseSensitive(article, "description"); + cJSON *link = cJSON_GetObjectItemCaseSensitive(article, "link"); + cJSON *author = cJSON_GetObjectItemCaseSensitive(article, "author"); + + NewsArticle *art = &news_state.articles[count]; + memset(art, 0, sizeof(NewsArticle)); + + if (cJSON_IsString(title) && title->valuestring) { + strip_html(art->title, title->valuestring, sizeof(art->title)); + } + + /* Prefer content, fallback to description */ + const char *text = NULL; + if (cJSON_IsString(content) && content->valuestring && strlen(content->valuestring) > 0) { + text = content->valuestring; + } else if (cJSON_IsString(description) && description->valuestring) { + text = description->valuestring; + } + + if (text) { + strip_html(art->content, text, sizeof(art->content)); + } else { + /* Use title as fallback */ + strncpy(art->content, art->title, sizeof(art->content) - 1); + } + + if (cJSON_IsString(link) && link->valuestring) { + strncpy(art->link, link->valuestring, sizeof(art->link) - 1); + } + + if (cJSON_IsString(author) && author->valuestring) { + strncpy(art->author, author->valuestring, sizeof(art->author) - 1); + } + + count++; + } + + cJSON_Delete(root); + return count; +} + +/* Separator between articles */ +#define NEWS_SEPARATOR " • " +#define NEWS_SEPARATOR_LEN 5 + +/* Forward declarations */ +static int news_find_article_at_x(int click_x); + +/* Get UTF-8 text width using Xft */ +static int news_text_width(const char *text) +{ + if (dwn == NULL || text == NULL) return 0; + + if (dwn->xft_font != NULL) { + XGlyphInfo extents; + XftTextExtentsUtf8(dwn->display, dwn->xft_font, + (const FcChar8 *)text, strlen(text), &extents); + return extents.xOff; + } + + if (dwn->font != NULL) { + return XTextWidth(dwn->font, text, strlen(text)); + } + + return strlen(text) * 8; +} + +/* Draw UTF-8 text using Xft with clipping */ +static void news_draw_text_clipped(Drawable d, int x, int y, const char *text, + unsigned long color, XRectangle *clip) +{ + if (dwn == NULL || dwn->display == NULL || text == NULL) return; + + if (dwn->xft_font != NULL) { + XftDraw *xft_draw = XftDrawCreate(dwn->display, d, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap); + if (xft_draw != NULL) { + if (clip != NULL) { + Region region = XCreateRegion(); + XUnionRectWithRegion(clip, region, region); + XftDrawSetClip(xft_draw, region); + XDestroyRegion(region); + } + + XftColor xft_color; + XRenderColor render_color; + + render_color.red = ((color >> 16) & 0xFF) * 257; + render_color.green = ((color >> 8) & 0xFF) * 257; + render_color.blue = (color & 0xFF) * 257; + render_color.alpha = 0xFFFF; + + XftColorAllocValue(dwn->display, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &render_color, &xft_color); + + XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, + x, y, (const FcChar8 *)text, strlen(text)); + + XftColorFree(dwn->display, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &xft_color); + XftDrawDestroy(xft_draw); + return; + } + } + + if (clip != NULL) { + XSetClipRectangles(dwn->display, dwn->gc, 0, 0, clip, 1, Unsorted); + } + XSetForeground(dwn->display, dwn->gc, color); + XDrawString(dwn->display, d, dwn->gc, x, y, text, strlen(text)); + if (clip != NULL) { + XSetClipMask(dwn->display, dwn->gc, None); + } +} + +/* Get display text for an article */ +static void get_article_display_text(int index, char *buf, size_t buf_size) +{ + if (index < 0 || index >= news_state.article_count) { + buf[0] = '\0'; + return; + } + NewsArticle *art = &news_state.articles[index]; + if (art->content[0] && strcmp(art->content, art->title) != 0) { + snprintf(buf, buf_size, "%s: %s", art->title, art->content); + } else { + snprintf(buf, buf_size, "%s", art->title); + } +} + +/* Recalculate cached widths after fetching */ +static void news_recalc_widths(void) +{ + if (dwn == NULL) { + return; + } + + int separator_width = news_text_width(NEWS_SEPARATOR); + news_state.total_width = 0; + + for (int i = 0; i < news_state.article_count; i++) { + char display_text[2048]; + get_article_display_text(i, display_text, sizeof(display_text)); + news_state.display_widths[i] = news_text_width(display_text); + news_state.total_width += news_state.display_widths[i] + separator_width; + } +} + +/* Background fetch thread */ +static void *news_fetch_thread(void *arg) +{ + (void)arg; + + CURL *curl = curl_easy_init(); + if (curl == NULL) { + pthread_mutex_lock(&news_mutex); + news_state.fetching = false; + news_state.has_error = true; + pthread_mutex_unlock(&news_mutex); + return NULL; + } + + CurlBuffer buf = {0}; + buf.data = malloc(1); + buf.data[0] = '\0'; + + curl_easy_setopt(curl, CURLOPT_URL, NEWS_API_URL); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, news_curl_write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "DWN/1.0"); + + CURLcode res = curl_easy_perform(curl); + + pthread_mutex_lock(&news_mutex); + if (res == CURLE_OK && buf.data && buf.size > 0) { + int count = parse_news_json(buf.data); + if (count > 0) { + news_state.article_count = count; + news_state.has_error = false; + news_state.current_article = 0; + news_state.scroll_offset = 0.0; + news_state.last_scroll_update = 0; + news_state.widths_dirty = true; + LOG_INFO("Fetched %d news articles", count); + } else { + news_state.has_error = true; + } + } else { + news_state.has_error = true; + LOG_DEBUG("News fetch failed: %s", curl_easy_strerror(res)); + } + news_state.fetching = false; + news_state.last_fetch = get_time_ms(); + pthread_mutex_unlock(&news_mutex); + + free(buf.data); + curl_easy_cleanup(curl); + + return NULL; +} + +/* ========== Public API ========== */ + +void news_init(void) +{ + memset(&news_state, 0, sizeof(news_state)); + news_state.last_fetch = 0; + + /* Start initial fetch */ + news_fetch_async(); + + LOG_INFO("News ticker initialized"); +} + +void news_cleanup(void) +{ + /* Wait for any pending fetch */ + if (fetch_running) { + pthread_join(fetch_thread, NULL); + fetch_running = 0; + } +} + +void news_fetch_async(void) +{ + pthread_mutex_lock(&news_mutex); + if (news_state.fetching) { + pthread_mutex_unlock(&news_mutex); + return; + } + news_state.fetching = true; + pthread_mutex_unlock(&news_mutex); + + /* Wait for previous thread if still running */ + if (fetch_running) { + pthread_join(fetch_thread, NULL); + } + + fetch_running = 1; + if (pthread_create(&fetch_thread, NULL, news_fetch_thread, NULL) != 0) { + pthread_mutex_lock(&news_mutex); + news_state.fetching = false; + news_state.has_error = true; + pthread_mutex_unlock(&news_mutex); + fetch_running = 0; + } +} + +void news_update(void) +{ + long now = get_time_ms(); + + pthread_mutex_lock(&news_mutex); + + /* Auto-refresh if interval passed */ + if (!news_state.fetching && (now - news_state.last_fetch) >= FETCH_INTERVAL) { + pthread_mutex_unlock(&news_mutex); + news_fetch_async(); + pthread_mutex_lock(&news_mutex); + } + + /* Time-based smooth scrolling */ + if (!news_state.interactive_mode && news_state.article_count > 0) { + if (news_state.last_scroll_update > 0) { + long delta_ms = now - news_state.last_scroll_update; + /* Calculate smooth sub-pixel scroll amount based on elapsed time */ + double scroll_amount = (SCROLL_SPEED_PPS * delta_ms) / 1000.0; + news_state.scroll_offset += scroll_amount; + } + news_state.last_scroll_update = now; + } + + pthread_mutex_unlock(&news_mutex); +} + +void news_next_article(void) +{ + pthread_mutex_lock(&news_mutex); + if (news_state.article_count > 0) { + news_state.current_article = (news_state.current_article + 1) % news_state.article_count; + news_state.scroll_offset = 0.0; + news_state.last_scroll_update = 0; + news_state.interactive_mode = true; + } + pthread_mutex_unlock(&news_mutex); +} + +void news_prev_article(void) +{ + pthread_mutex_lock(&news_mutex); + if (news_state.article_count > 0) { + news_state.current_article--; + if (news_state.current_article < 0) { + news_state.current_article = news_state.article_count - 1; + } + news_state.scroll_offset = 0.0; + news_state.last_scroll_update = 0; + news_state.interactive_mode = true; + } + pthread_mutex_unlock(&news_mutex); +} + +void news_open_current(void) +{ + pthread_mutex_lock(&news_mutex); + if (news_state.article_count > 0 && news_state.total_width > 0) { + int article_index = news_find_article_at_x(news_state.render_x); + if (article_index < 0) article_index = 0; + + char url[512]; + strncpy(url, news_state.articles[article_index].link, sizeof(url) - 1); + url[sizeof(url) - 1] = '\0'; + pthread_mutex_unlock(&news_mutex); + + pid_t pid = fork(); + if (pid == 0) { + setsid(); + pid_t pid2 = fork(); + if (pid2 == 0) { + execlp("xdg-open", "xdg-open", url, NULL); + _exit(1); + } + _exit(0); + } else if (pid > 0) { + int status; + waitpid(pid, &status, 0); + } + } else { + pthread_mutex_unlock(&news_mutex); + } +} + +/* Find article at given x position within news area */ +static int news_find_article_at_x(int click_x) +{ + if (news_state.article_count == 0 || news_state.total_width == 0) { + return -1; + } + + int separator_width = news_text_width(NEWS_SEPARATOR); + int scroll_offset = (int)news_state.scroll_offset % news_state.total_width; + int rel_x = click_x - news_state.render_x; + + int content_x = scroll_offset + rel_x; + if (content_x < 0) { + content_x += news_state.total_width; + } + content_x = content_x % news_state.total_width; + + int pos = 0; + for (int i = 0; i < news_state.article_count; i++) { + int article_total = news_state.display_widths[i] + separator_width; + if (content_x < pos + article_total) { + return i; + } + pos += article_total; + } + return 0; +} + +void news_render(Panel *panel, int x, int max_width, int *used_width) +{ + if (panel == NULL || used_width == NULL || dwn == NULL) { + *used_width = 0; + return; + } + + if (dwn->xft_font == NULL && dwn->font == NULL) { + *used_width = 0; + return; + } + + pthread_mutex_lock(&news_mutex); + + if (news_state.article_count == 0) { + pthread_mutex_unlock(&news_mutex); + *used_width = 0; + return; + } + + if (news_state.widths_dirty) { + news_recalc_widths(); + news_state.widths_dirty = false; + } + + if (news_state.total_width == 0) { + pthread_mutex_unlock(&news_mutex); + *used_width = 0; + return; + } + + news_state.render_x = x; + news_state.render_width = max_width; + + int separator_width = news_text_width(NEWS_SEPARATOR); + int scroll_offset = (int)news_state.scroll_offset % news_state.total_width; + + pthread_mutex_unlock(&news_mutex); + + const ColorScheme *colors = config_get_colors(); + + int text_y; + if (dwn->xft_font != NULL) { + text_y = (panel->height + dwn->xft_font->ascent - dwn->xft_font->descent) / 2; + } else { + text_y = (panel->height + dwn->font->ascent - dwn->font->descent) / 2; + } + + XRectangle clip_rect = { x, 0, max_width, panel->height }; + + pthread_mutex_lock(&news_mutex); + + int draw_x = x - scroll_offset; + int articles_drawn = 0; + int start_article = 0; + + int pos = 0; + for (int i = 0; i < news_state.article_count; i++) { + int article_total = news_state.display_widths[i] + separator_width; + if (pos + article_total > scroll_offset) { + start_article = i; + draw_x = x + (pos - scroll_offset); + break; + } + pos += article_total; + } + + int current_article = start_article; + while (draw_x < x + max_width && articles_drawn < news_state.article_count * 2) { + char display_text[2048]; + get_article_display_text(current_article, display_text, sizeof(display_text)); + + if (draw_x + news_state.display_widths[current_article] > x) { + news_draw_text_clipped(panel->buffer, draw_x, text_y, display_text, + colors->panel_fg, &clip_rect); + } + draw_x += news_state.display_widths[current_article]; + + if (draw_x < x + max_width) { + news_draw_text_clipped(panel->buffer, draw_x, text_y, NEWS_SEPARATOR, + colors->panel_fg, &clip_rect); + } + draw_x += separator_width; + + current_article = (current_article + 1) % news_state.article_count; + articles_drawn++; + } + + pthread_mutex_unlock(&news_mutex); + + *used_width = max_width; +} + +void news_handle_click(int x, int y) +{ + (void)y; + + pthread_mutex_lock(&news_mutex); + if (news_state.article_count == 0) { + pthread_mutex_unlock(&news_mutex); + return; + } + + int article_index = news_find_article_at_x(x); + if (article_index >= 0 && article_index < news_state.article_count) { + char url[512]; + strncpy(url, news_state.articles[article_index].link, sizeof(url) - 1); + url[sizeof(url) - 1] = '\0'; + pthread_mutex_unlock(&news_mutex); + + pid_t pid = fork(); + if (pid == 0) { + setsid(); + pid_t pid2 = fork(); + if (pid2 == 0) { + execlp("xdg-open", "xdg-open", url, NULL); + _exit(1); + } + _exit(0); + } else if (pid > 0) { + int status; + waitpid(pid, &status, 0); + } + } else { + pthread_mutex_unlock(&news_mutex); + } +} + +void news_lock(void) +{ + pthread_mutex_lock(&news_mutex); +} + +void news_unlock(void) +{ + pthread_mutex_unlock(&news_mutex); +} diff --git a/src/notifications.c b/src/notifications.c new file mode 100644 index 0000000..221a37a --- /dev/null +++ b/src/notifications.c @@ -0,0 +1,856 @@ +/* + * DWN - Desktop Window Manager + * D-Bus Notification daemon implementation + */ + +#include "notifications.h" +#include "config.h" +#include "util.h" + +#include +#include +#include + +/* D-Bus connection */ +DBusConnection *dbus_conn = NULL; + +/* Notification list */ +static Notification *notification_list = NULL; +static uint32_t next_notification_id = 1; + +/* Notification dimensions - dynamic based on screen size */ +#define NOTIFICATION_MIN_WIDTH 280 +#define NOTIFICATION_PADDING 12 +#define NOTIFICATION_MARGIN 10 + +/* Get max width: 33% of screen width */ +static int notification_max_width(void) +{ + if (dwn == NULL) return 500; + return dwn->screen_width / 3; +} + +/* Get max height: 50% of screen height */ +static int notification_max_height(void) +{ + if (dwn == NULL) return 600; + return dwn->screen_height / 2; +} + +/* Default timeout */ +#define DEFAULT_TIMEOUT 5000 + +/* ========== UTF-8 text helpers ========== */ + +/* Get text width using Xft (UTF-8 aware) */ +static int notif_text_width(const char *text, int len) +{ + if (text == NULL || dwn == NULL) return len * 7; /* Fallback estimate */ + + if (dwn->xft_font != NULL) { + XGlyphInfo extents; + XftTextExtentsUtf8(dwn->display, dwn->xft_font, + (const FcChar8 *)text, len, &extents); + return extents.xOff; + } + + if (dwn->font != NULL) { + return XTextWidth(dwn->font, text, len); + } + + return len * 7; /* Fallback estimate */ +} + +/* Get line height */ +static int notif_line_height(void) +{ + if (dwn == NULL) return 14; + + if (dwn->xft_font != NULL) { + return dwn->xft_font->ascent + dwn->xft_font->descent + 2; + } + + if (dwn->font != NULL) { + return dwn->font->ascent + dwn->font->descent + 2; + } + + return 14; +} + +/* Get font ascent */ +static int notif_font_ascent(void) +{ + if (dwn == NULL) return 12; + + if (dwn->xft_font != NULL) { + return dwn->xft_font->ascent; + } + + if (dwn->font != NULL) { + return dwn->font->ascent; + } + + return 12; +} + +/* Draw text using Xft (UTF-8 aware) */ +static void notif_draw_text(Window win, int x, int y, const char *text, + int len, unsigned long color) +{ + if (text == NULL || dwn == NULL || dwn->display == NULL) return; + + /* Use Xft for UTF-8 support */ + if (dwn->xft_font != NULL) { + XftDraw *xft_draw = XftDrawCreate(dwn->display, win, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap); + if (xft_draw != NULL) { + XftColor xft_color; + XRenderColor render_color; + render_color.red = ((color >> 16) & 0xFF) * 257; + render_color.green = ((color >> 8) & 0xFF) * 257; + render_color.blue = (color & 0xFF) * 257; + render_color.alpha = 0xFFFF; + + XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &render_color, &xft_color); + + XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, + x, y, (const FcChar8 *)text, len); + + XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &xft_color); + XftDrawDestroy(xft_draw); + return; + } + } + + /* Fallback to legacy X11 text */ + XSetForeground(dwn->display, dwn->gc, color); + XDrawString(dwn->display, win, dwn->gc, x, y, text, len); +} + +/* ========== Initialization ========== */ + +bool notifications_init(void) +{ + DBusError err; + dbus_error_init(&err); + + /* Connect to session bus */ + dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &err); + if (dbus_error_is_set(&err)) { + LOG_ERROR("D-Bus connection error: %s", err.message); + dbus_error_free(&err); + return false; + } + + if (dbus_conn == NULL) { + LOG_ERROR("Failed to connect to D-Bus session bus"); + return false; + } + + /* Register notification service */ + if (!notifications_register_service()) { + LOG_WARN("Could not register notification service (another daemon running?)"); + /* Don't fail - we can still function as a WM */ + } + + LOG_INFO("Notification daemon initialized"); + return true; +} + +void notifications_cleanup(void) +{ + /* Close all notifications */ + notification_close_all(); + + /* D-Bus connection is managed by libdbus */ + dbus_conn = NULL; +} + +/* ========== D-Bus handling ========== */ + +bool notifications_register_service(void) +{ + if (dbus_conn == NULL) { + return false; + } + + DBusError err; + dbus_error_init(&err); + + /* Request the notification service name */ + int result = dbus_bus_request_name(dbus_conn, "org.freedesktop.Notifications", + DBUS_NAME_FLAG_REPLACE_EXISTING, &err); + + if (dbus_error_is_set(&err)) { + LOG_ERROR("D-Bus name request error: %s", err.message); + dbus_error_free(&err); + return false; + } + + if (result != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { + LOG_WARN("Could not become primary owner of notification service"); + return false; + } + + /* Add message filter */ + dbus_connection_add_filter(dbus_conn, notifications_handle_message, NULL, NULL); + + LOG_INFO("Registered as org.freedesktop.Notifications"); + return true; +} + +void notifications_process_messages(void) +{ + if (dbus_conn == NULL) { + return; + } + + /* Process pending D-Bus messages (non-blocking) */ + dbus_connection_read_write(dbus_conn, 0); + + while (dbus_connection_dispatch(dbus_conn) == DBUS_DISPATCH_DATA_REMAINS) { + /* Keep dispatching */ + } +} + +DBusHandlerResult notifications_handle_message(DBusConnection *conn, + DBusMessage *msg, + void *user_data) +{ + (void)user_data; + + const char *interface = dbus_message_get_interface(msg); + const char *member = dbus_message_get_member(msg); + + if (interface == NULL || member == NULL) { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + if (strcmp(interface, "org.freedesktop.Notifications") != 0) { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + /* Handle Notify method */ + if (strcmp(member, "Notify") == 0) { + DBusMessageIter args; + if (!dbus_message_iter_init(msg, &args)) { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + char *app_name = ""; + uint32_t replaces_id = 0; + char *icon = ""; + char *summary = ""; + char *body = ""; + int32_t timeout = -1; + + /* Parse arguments */ + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) { + dbus_message_iter_get_basic(&args, &app_name); + dbus_message_iter_next(&args); + } + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_UINT32) { + dbus_message_iter_get_basic(&args, &replaces_id); + dbus_message_iter_next(&args); + } + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) { + dbus_message_iter_get_basic(&args, &icon); + dbus_message_iter_next(&args); + } + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) { + dbus_message_iter_get_basic(&args, &summary); + dbus_message_iter_next(&args); + } + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) { + dbus_message_iter_get_basic(&args, &body); + dbus_message_iter_next(&args); + } + + /* Skip actions array */ + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_ARRAY) { + dbus_message_iter_next(&args); + } + + /* Skip hints dict */ + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_ARRAY) { + dbus_message_iter_next(&args); + } + + if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_INT32) { + dbus_message_iter_get_basic(&args, &timeout); + } + + /* Show notification */ + uint32_t id = notification_show(app_name, summary, body, icon, timeout); + + /* Send reply */ + DBusMessage *reply = dbus_message_new_method_return(msg); + dbus_message_append_args(reply, DBUS_TYPE_UINT32, &id, DBUS_TYPE_INVALID); + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; + } + + /* Handle CloseNotification method */ + if (strcmp(member, "CloseNotification") == 0) { + DBusMessageIter args; + if (dbus_message_iter_init(msg, &args) && + dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_UINT32) { + uint32_t id; + dbus_message_iter_get_basic(&args, &id); + notification_close(id); + } + + DBusMessage *reply = dbus_message_new_method_return(msg); + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; + } + + /* Handle GetCapabilities method */ + if (strcmp(member, "GetCapabilities") == 0) { + DBusMessage *reply = dbus_message_new_method_return(msg); + DBusMessageIter args_iter, array_iter; + + dbus_message_iter_init_append(reply, &args_iter); + dbus_message_iter_open_container(&args_iter, DBUS_TYPE_ARRAY, "s", &array_iter); + + const char *caps[] = { "body", "body-markup" }; + for (size_t i = 0; i < sizeof(caps) / sizeof(caps[0]); i++) { + dbus_message_iter_append_basic(&array_iter, DBUS_TYPE_STRING, &caps[i]); + } + + dbus_message_iter_close_container(&args_iter, &array_iter); + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; + } + + /* Handle GetServerInformation method */ + if (strcmp(member, "GetServerInformation") == 0) { + DBusMessage *reply = dbus_message_new_method_return(msg); + + const char *name = "DWN"; + const char *vendor = "DWN Project"; + const char *version = DWN_VERSION; + const char *spec_version = "1.2"; + + dbus_message_append_args(reply, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &vendor, + DBUS_TYPE_STRING, &version, + DBUS_TYPE_STRING, &spec_version, + DBUS_TYPE_INVALID); + + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +/* ========== Notification management ========== */ + +#define MAX_VISIBLE_NOTIFICATIONS 3 + +/* Calculate notification size based on content - accounts for word wrapping */ +static void notification_calculate_size(const char *summary, const char *body, + int *out_width, int *out_height) +{ + int max_width = notification_max_width(); + int max_height = notification_max_height(); + int min_height = 80; + int content_max_width = max_width - 2 * NOTIFICATION_PADDING; + + int width = NOTIFICATION_MIN_WIDTH; + int height = NOTIFICATION_PADDING * 2; + int line_height = notif_line_height(); + + /* Add space for summary */ + if (summary != NULL && summary[0] != '\0') { + int summary_width = NOTIFICATION_PADDING * 2; + summary_width += notif_text_width(summary, strlen(summary)); + if (summary_width > width) width = summary_width; + height += line_height + 4; + } + + /* Count wrapped lines in body */ + if (body != NULL && body[0] != '\0') { + const char *p = body; + int wrapped_line_count = 0; + + while (*p != '\0') { + /* Find end of this logical line (newline) */ + const char *line_start = p; + while (*p != '\0' && *p != '\n') p++; + size_t logical_line_len = p - line_start; + + if (logical_line_len == 0) { + /* Empty line */ + wrapped_line_count++; + } else { + /* Count how many wrapped lines this logical line needs */ + const char *lp = line_start; + while (lp < line_start + logical_line_len) { + size_t remaining = (line_start + logical_line_len) - lp; + size_t fit_len = 0; + + /* Find how much fits on one line */ + for (size_t i = 0; i < remaining; i++) { + if ((lp[i] & 0xC0) == 0x80) continue; /* Skip continuation bytes */ + int w = notif_text_width(lp, i + 1); + if (w > content_max_width) break; + fit_len = i + 1; + } + + /* Include trailing UTF-8 continuation bytes */ + while (fit_len < remaining && (lp[fit_len] & 0xC0) == 0x80) { + fit_len++; + } + + /* Force at least one character */ + if (fit_len == 0 && remaining > 0) { + fit_len = 1; + while (fit_len < remaining && (lp[fit_len] & 0xC0) == 0x80) { + fit_len++; + } + } + + if (fit_len == 0) break; + + lp += fit_len; + /* Skip leading spaces on continuation */ + while (lp < line_start + logical_line_len && *lp == ' ') lp++; + + wrapped_line_count++; + + /* Stop if we'd exceed max height */ + if (height + (wrapped_line_count * line_height) > max_height - line_height - NOTIFICATION_PADDING) { + goto done_counting; + } + } + } + + if (*p == '\n') p++; + } + +done_counting: + height += wrapped_line_count * line_height; + } + + /* Add space for app name at bottom */ + height += line_height + NOTIFICATION_PADDING; + + /* Use full max_width since we're wrapping, not truncating */ + width = max_width; + + /* Clamp to min/max */ + if (width < NOTIFICATION_MIN_WIDTH) width = NOTIFICATION_MIN_WIDTH; + if (width > max_width) width = max_width; + if (height < min_height) height = min_height; + if (height > max_height) height = max_height; + + *out_width = width; + *out_height = height; +} + +uint32_t notification_show(const char *app_name, const char *summary, + const char *body, const char *icon, int timeout) +{ + /* Count existing notifications and close oldest if we have too many */ + int count = 0; + Notification *oldest = NULL; + for (Notification *n = notification_list; n != NULL; n = n->next) { + count++; + oldest = n; /* Last in list is oldest (we prepend new ones) */ + } + + /* Close oldest notification if we're at the limit */ + if (count >= MAX_VISIBLE_NOTIFICATIONS && oldest != NULL) { + notification_close(oldest->id); + } + + Notification *notif = dwn_calloc(1, sizeof(Notification)); + + notif->id = next_notification_id++; + + if (app_name) strncpy(notif->app_name, app_name, sizeof(notif->app_name) - 1); + if (summary) strncpy(notif->summary, summary, sizeof(notif->summary) - 1); + + /* Dynamically allocate body - unlimited size */ + if (body != NULL && body[0] != '\0') { + notif->body_len = strlen(body); + notif->body = dwn_malloc(notif->body_len + 1); + if (notif->body != NULL) { + memcpy(notif->body, body, notif->body_len + 1); + } + } else { + notif->body = NULL; + notif->body_len = 0; + } + + if (icon) strncpy(notif->icon, icon, sizeof(notif->icon) - 1); + + notif->timeout = (timeout < 0) ? DEFAULT_TIMEOUT : timeout; + notif->urgency = NOTIFY_URGENCY_NORMAL; + notif->expire_time = (notif->timeout > 0) ? + get_time_ms() + notif->timeout : 0; + + /* Calculate size based on content */ + int notif_width, notif_height; + notification_calculate_size(summary, body, ¬if_width, ¬if_height); + notif->width = notif_width; + notif->height = notif_height; + + /* Create notification window */ + if (dwn != NULL && dwn->display != NULL) { + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = dwn->config->colors.notification_bg; + swa.event_mask = ExposureMask | ButtonPressMask; + + notif->window = XCreateWindow(dwn->display, dwn->root, + 0, 0, + notif->width, notif->height, + 1, + CopyFromParent, InputOutput, CopyFromParent, + CWOverrideRedirect | CWBackPixel | CWEventMask, + &swa); + + XSetWindowBorder(dwn->display, notif->window, + dwn->config->colors.border_focused); + } + + /* Add to list */ + notif->next = notification_list; + notification_list = notif; + + /* Position and show */ + notifications_position(); + notification_render(notif); + + if (notif->window != None) { + XMapRaised(dwn->display, notif->window); + } + + LOG_DEBUG("Notification %u (%dx%d): %s - %s", notif->id, + notif->width, notif->height, notif->summary, notif->body); + + return notif->id; +} + +void notification_close(uint32_t id) +{ + Notification *prev = NULL; + Notification *notif = notification_list; + + while (notif != NULL) { + if (notif->id == id) { + /* Remove from list */ + if (prev != NULL) { + prev->next = notif->next; + } else { + notification_list = notif->next; + } + + /* Destroy window */ + if (notif->window != None && dwn != NULL && dwn->display != NULL) { + XDestroyWindow(dwn->display, notif->window); + } + + /* Free dynamically allocated body */ + if (notif->body != NULL) { + dwn_free(notif->body); + notif->body = NULL; + } + + dwn_free(notif); + + /* Reposition remaining notifications */ + notifications_position(); + return; + } + + prev = notif; + notif = notif->next; + } +} + +void notification_close_all(void) +{ + while (notification_list != NULL) { + notification_close(notification_list->id); + } +} + +Notification *notification_find(uint32_t id) +{ + for (Notification *n = notification_list; n != NULL; n = n->next) { + if (n->id == id) { + return n; + } + } + return NULL; +} + +Notification *notification_find_by_window(Window window) +{ + for (Notification *n = notification_list; n != NULL; n = n->next) { + if (n->window == window) { + return n; + } + } + return NULL; +} + +/* ========== Rendering ========== */ + +void notification_render(Notification *notif) +{ + if (notif == NULL || notif->window == None) { + LOG_DEBUG("notification_render: notif or window is NULL"); + return; + } + + if (dwn == NULL || dwn->display == NULL) { + LOG_DEBUG("notification_render: dwn or display is NULL"); + return; + } + + if (dwn->xft_font == NULL && dwn->font == NULL) { + LOG_ERROR("notification_render: no font available - cannot render text"); + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + + if (colors == NULL) { + LOG_ERROR("notification_render: colors is NULL"); + return; + } + + LOG_DEBUG("Rendering notification: summary='%s', body_len=%zu", + notif->summary, notif->body_len); + + /* Set legacy font in GC if available (for fallback) */ + if (dwn->font != NULL) { + XSetFont(dpy, dwn->gc, dwn->font->fid); + } + + /* Clear background using dynamic size */ + XSetForeground(dpy, dwn->gc, colors->notification_bg); + XFillRectangle(dpy, notif->window, dwn->gc, 0, 0, + notif->width, notif->height); + + /* Draw summary (title) */ + int line_height = notif_line_height(); + int y = NOTIFICATION_PADDING + notif_font_ascent(); + notif_draw_text(notif->window, NOTIFICATION_PADDING, y, + notif->summary, strlen(notif->summary), + colors->notification_fg); + + /* Draw body - handle multiple lines */ + y += line_height + 4; + + int max_width = notif->width - 2 * NOTIFICATION_PADDING; + int max_y = notif->height - line_height - NOTIFICATION_PADDING; + + /* Only process body if it exists */ + if (notif->body != NULL && notif->body_len > 0) { + /* Dynamically allocate body copy for tokenization */ + char *body_copy = dwn_malloc(notif->body_len + 1); + if (body_copy != NULL) { + memcpy(body_copy, notif->body, notif->body_len + 1); + + /* Split by newlines and draw each line */ + char *line = body_copy; + char *next; + + while (line != NULL && *line != '\0' && y < max_y) { + /* Find next newline */ + next = strchr(line, '\n'); + if (next != NULL) { + *next = '\0'; + next++; + } + + /* Skip empty lines but count them for spacing */ + if (*line == '\0') { + y += line_height / 2; /* Half-height for empty lines */ + line = next; + continue; + } + + /* Word wrap long lines instead of truncating */ + size_t line_len = strlen(line); + const char *p = line; + + while (*p != '\0' && y < max_y) { + /* Find how much text fits on this line */ + size_t fit_len = 0; + size_t last_space = 0; + + for (size_t i = 0; p[i] != '\0'; i++) { + /* Skip UTF-8 continuation bytes for character counting */ + if ((p[i] & 0xC0) == 0x80) continue; + + int width = notif_text_width(p, i + 1); + if (width > max_width) { + break; + } + fit_len = i + 1; + + /* Track last space for word wrapping */ + if (p[i] == ' ') { + last_space = i; + } + } + + /* Adjust to include full UTF-8 characters */ + while (fit_len < line_len && (p[fit_len] & 0xC0) == 0x80) { + fit_len++; + } + + /* If we couldn't fit anything, force at least one character */ + if (fit_len == 0 && *p != '\0') { + fit_len = 1; + while (fit_len < line_len && (p[fit_len] & 0xC0) == 0x80) { + fit_len++; + } + } + + /* Prefer breaking at word boundary if possible */ + if (fit_len < strlen(p) && last_space > 0 && last_space > fit_len / 2) { + fit_len = last_space + 1; /* Include the space */ + } + + /* Draw this segment */ + if (fit_len > 0) { + char *segment = dwn_malloc(fit_len + 1); + if (segment != NULL) { + memcpy(segment, p, fit_len); + segment[fit_len] = '\0'; + + notif_draw_text(notif->window, NOTIFICATION_PADDING, y, + segment, strlen(segment), colors->panel_fg); + + dwn_free(segment); + } + + p += fit_len; + /* Skip leading spaces on new line */ + while (*p == ' ') p++; + + y += line_height; + } else { + break; + } + } + + line = next; + } + + dwn_free(body_copy); + } + } + + /* Draw app name at bottom */ + if (notif->app_name[0] != '\0') { + y = notif->height - NOTIFICATION_PADDING; + notif_draw_text(notif->window, NOTIFICATION_PADDING, y, + notif->app_name, strlen(notif->app_name), + colors->workspace_inactive); + } + + /* Force the drawing to be sent to X server */ + XFlush(dpy); +} + +void notifications_render_all(void) +{ + for (Notification *n = notification_list; n != NULL; n = n->next) { + notification_render(n); + } +} + +void notifications_update(void) +{ + long now = get_time_ms(); + + /* Check for expired notifications */ + Notification *notif = notification_list; + while (notif != NULL) { + Notification *next = notif->next; + + if (notif->expire_time > 0 && now >= notif->expire_time) { + notification_close(notif->id); + } + + notif = next; + } +} + +void notifications_position(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + int y = NOTIFICATION_MARGIN; + + /* Account for top panel */ + if (dwn->config && dwn->config->top_panel_enabled) { + y += config_get_panel_height(); + } + + for (Notification *n = notification_list; n != NULL; n = n->next) { + if (n->window != None) { + /* Position from right edge using notification's own width */ + int x = dwn->screen_width - n->width - NOTIFICATION_MARGIN; + XMoveWindow(dwn->display, n->window, x, y); + } + /* Stack using notification's own height */ + y += n->height + NOTIFICATION_MARGIN; + } +} + +void notifications_raise_all(void) +{ + if (dwn == NULL || dwn->display == NULL) { + return; + } + + /* Raise all notification windows to the top */ + for (Notification *n = notification_list; n != NULL; n = n->next) { + if (n->window != None) { + XRaiseWindow(dwn->display, n->window); + } + } +} + +/* ========== Server info ========== */ + +void notifications_get_server_info(const char **name, const char **vendor, + const char **version, const char **spec_version) +{ + if (name) *name = "DWN"; + if (vendor) *vendor = "DWN Project"; + if (version) *version = DWN_VERSION; + if (spec_version) *spec_version = "1.2"; +} + +void notifications_get_capabilities(const char ***caps, int *count) +{ + static const char *capabilities[] = { "body", "body-markup" }; + if (caps) *caps = capabilities; + if (count) *count = 2; +} diff --git a/src/panel.c b/src/panel.c new file mode 100644 index 0000000..b7ac118 --- /dev/null +++ b/src/panel.c @@ -0,0 +1,892 @@ +/* + * DWN - Desktop Window Manager + * Panel system implementation + */ + +#include "panel.h" +#include "workspace.h" +#include "layout.h" +#include "client.h" +#include "config.h" +#include "util.h" +#include "atoms.h" +#include "systray.h" +#include "news.h" + +#include +#include +#include +#include +#include +#include + +/* Panel padding and spacing */ +#define PANEL_PADDING 8 +#define WIDGET_SPACING 12 +#define WORKSPACE_WIDTH 28 +#define TASKBAR_ITEM_WIDTH 150 + +/* Clock format */ +#define CLOCK_FORMAT "%H:%M:%S" +#define DATE_FORMAT "%Y-%m-%d" + +/* Static clock buffer */ +static char clock_buffer[32] = ""; +static char date_buffer[32] = ""; + +/* System stats */ +typedef struct { + int cpu_percent; /* CPU usage percentage */ + int mem_percent; /* Memory usage percentage */ + int mem_used_mb; /* Memory used in MB */ + int mem_total_mb; /* Total memory in MB */ + float load_1min; /* 1-minute load average */ + float load_5min; /* 5-minute load average */ + float load_15min; /* 15-minute load average */ + /* CPU calculation state */ + unsigned long long prev_idle; + unsigned long long prev_total; +} SystemStats; + +static SystemStats sys_stats = {0}; + +/* Forward declarations */ +static void panel_render_system_stats(Panel *panel, int x, int *width); +static int panel_calculate_stats_width(void); + +/* ========== UTF-8 text helpers ========== */ + +/* Get text width using Xft (UTF-8 aware) */ +static int panel_text_width(const char *text, int len) +{ + if (text == NULL || dwn == NULL) return 0; + + if (dwn->xft_font != NULL) { + XGlyphInfo extents; + XftTextExtentsUtf8(dwn->display, dwn->xft_font, + (const FcChar8 *)text, len, &extents); + return extents.xOff; + } + + /* Fallback to legacy font */ + if (dwn->font != NULL) { + return XTextWidth(dwn->font, text, len); + } + + return 0; +} + +/* Draw text using Xft (UTF-8 aware) */ +static void panel_draw_text(Drawable d, int x, int y, const char *text, + int len, unsigned long color) +{ + if (text == NULL || dwn == NULL || dwn->display == NULL) return; + + /* Use Xft for UTF-8 support */ + if (dwn->xft_font != NULL) { + XftDraw *xft_draw = XftDrawCreate(dwn->display, d, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap); + if (xft_draw != NULL) { + XftColor xft_color; + XRenderColor render_color; + render_color.red = ((color >> 16) & 0xFF) * 257; + render_color.green = ((color >> 8) & 0xFF) * 257; + render_color.blue = (color & 0xFF) * 257; + render_color.alpha = 0xFFFF; + + XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &render_color, &xft_color); + + XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, + x, y, (const FcChar8 *)text, len); + + XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &xft_color); + XftDrawDestroy(xft_draw); + return; + } + } + + /* Fallback to legacy X11 text */ + XSetForeground(dwn->display, dwn->gc, color); + XDrawString(dwn->display, d, dwn->gc, x, y, text, len); +} + +/* Get text Y position for vertical centering */ +static int panel_text_y(int panel_height) +{ + if (dwn->xft_font != NULL) { + return (panel_height + dwn->xft_font->ascent) / 2; + } + if (dwn->font != NULL) { + return (panel_height + dwn->font->ascent - dwn->font->descent) / 2; + } + return panel_height / 2; +} + +/* ========== Panel creation/destruction ========== */ + +Panel *panel_create(PanelPosition position) +{ + if (dwn == NULL || dwn->display == NULL) { + return NULL; + } + + Panel *panel = dwn_calloc(1, sizeof(Panel)); + panel->position = position; + panel->width = dwn->screen_width; + panel->height = config_get_panel_height(); + panel->x = 0; + panel->visible = true; + + if (position == PANEL_TOP) { + panel->y = 0; + } else { + panel->y = dwn->screen_height - panel->height; + } + + /* Create panel window */ + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = dwn->config->colors.panel_bg; + swa.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask; + + panel->window = XCreateWindow(dwn->display, dwn->root, + panel->x, panel->y, + panel->width, panel->height, + 0, + CopyFromParent, InputOutput, CopyFromParent, + CWOverrideRedirect | CWBackPixel | CWEventMask, + &swa); + + /* Create double buffer */ + panel->buffer = XCreatePixmap(dwn->display, panel->window, + panel->width, panel->height, + DefaultDepth(dwn->display, dwn->screen)); + + /* Set EWMH strut to reserve space */ + long strut[4] = { 0, 0, 0, 0 }; + if (position == PANEL_TOP) { + strut[2] = panel->height; + } else { + strut[3] = panel->height; + } + + XChangeProperty(dwn->display, panel->window, ewmh.NET_WM_STRUT, + XA_CARDINAL, 32, PropModeReplace, + (unsigned char *)strut, 4); + + LOG_DEBUG("Created %s panel at y=%d", + position == PANEL_TOP ? "top" : "bottom", panel->y); + + return panel; +} + +void panel_destroy(Panel *panel) +{ + if (panel == NULL) { + return; + } + + if (panel->buffer != None) { + XFreePixmap(dwn->display, panel->buffer); + } + + if (panel->window != None) { + XDestroyWindow(dwn->display, panel->window); + } + + dwn_free(panel); +} + +void panels_init(void) +{ + if (dwn == NULL || dwn->config == NULL) { + return; + } + + if (dwn->config->top_panel_enabled) { + dwn->top_panel = panel_create(PANEL_TOP); + if (dwn->top_panel != NULL) { + XMapRaised(dwn->display, dwn->top_panel->window); + } + } + + if (dwn->config->bottom_panel_enabled) { + dwn->bottom_panel = panel_create(PANEL_BOTTOM); + if (dwn->bottom_panel != NULL) { + XMapRaised(dwn->display, dwn->bottom_panel->window); + } + } + + /* Initial clock and stats update */ + panel_update_clock(); + panel_update_system_stats(); + + LOG_INFO("Panels initialized"); +} + +void panels_cleanup(void) +{ + if (dwn->top_panel != NULL) { + panel_destroy(dwn->top_panel); + dwn->top_panel = NULL; + } + + if (dwn->bottom_panel != NULL) { + panel_destroy(dwn->bottom_panel); + dwn->bottom_panel = NULL; + } +} + +/* ========== Panel rendering ========== */ + +void panel_render(Panel *panel) +{ + if (panel == NULL || !panel->visible) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + + /* Clear buffer with background color */ + XSetForeground(dpy, dwn->gc, colors->panel_bg); + XFillRectangle(dpy, panel->buffer, dwn->gc, 0, 0, panel->width, panel->height); + + int x = PANEL_PADDING; + int width; + + if (panel->position == PANEL_TOP) { + /* Top panel: workspaces, layout indicator, taskbar, systray */ + + /* Workspace indicators */ + panel_render_workspaces(panel, x, &width); + x += width + WIDGET_SPACING; + + /* Layout indicator */ + panel_render_layout_indicator(panel, x, &width); + x += width + WIDGET_SPACING; + + /* Taskbar (takes remaining space, but leave room for systray) */ + panel_render_taskbar(panel, x, &width); + + /* System tray (right side) - WiFi, Audio, etc. */ + int systray_actual_width = systray_get_width(); + int systray_x = panel->width - systray_actual_width - PANEL_PADDING; + systray_render(panel, systray_x, &width); + + /* AI status (left of systray) */ + if (dwn->ai_enabled) { + int ai_x = systray_x - 60; + panel_render_ai_status(panel, ai_x, &width); + } + } else { + /* Bottom panel: date (left), news ticker (center), system stats + clock (right) */ + + /* Date (left side) */ + int date_width = 0; + if (dwn->xft_font != NULL || dwn->font != NULL) { + int text_y = panel_text_y(panel->height); + panel_draw_text(panel->buffer, x, text_y, date_buffer, + strlen(date_buffer), colors->panel_fg); + date_width = panel_text_width(date_buffer, strlen(date_buffer)); + } + + /* Calculate positions from right edge */ + int clock_width = panel_text_width(clock_buffer, strlen(clock_buffer)); + int stats_width = panel_calculate_stats_width(); + + /* Clock at rightmost position */ + int clock_x = panel->width - clock_width - PANEL_PADDING; + panel_render_clock(panel, clock_x, &width); + + /* Stats immediately left of clock */ + int stats_x = clock_x - stats_width - WIDGET_SPACING; + panel_render_system_stats(panel, stats_x, &width); + + /* News ticker between date and stats */ + int news_start = PANEL_PADDING + date_width + WIDGET_SPACING * 2; + int news_max_width = stats_x - news_start - WIDGET_SPACING; + if (news_max_width > 100) { /* Only show if there's reasonable space */ + int news_width = 0; + news_render(panel, news_start, news_max_width, &news_width); + } + } + + /* Copy buffer to window */ + XCopyArea(dpy, panel->buffer, panel->window, dwn->gc, + 0, 0, panel->width, panel->height, 0, 0); + + XFlush(dpy); +} + +void panel_render_all(void) +{ + if (dwn->top_panel != NULL) { + panel_render(dwn->top_panel); + } + if (dwn->bottom_panel != NULL) { + panel_render(dwn->bottom_panel); + } +} + +void panel_render_workspaces(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + int text_y = panel_text_y(panel->height); + + *width = 0; + + for (int i = 0; i < MAX_WORKSPACES; i++) { + bool active = (i == dwn->current_workspace); + bool has_clients = !workspace_is_empty(i); + + /* Background */ + unsigned long bg = active ? colors->workspace_active : colors->panel_bg; + XSetForeground(dpy, dwn->gc, bg); + XFillRectangle(dpy, panel->buffer, dwn->gc, + x + i * WORKSPACE_WIDTH, 2, + WORKSPACE_WIDTH - 2, panel->height - 4); + + /* Workspace number */ + char num[4]; + snprintf(num, sizeof(num), "%d", i + 1); + + unsigned long fg = active ? colors->panel_bg : + (has_clients ? colors->panel_fg : colors->workspace_inactive); + + int text_x = x + i * WORKSPACE_WIDTH + (WORKSPACE_WIDTH - panel_text_width(num, strlen(num))) / 2; + panel_draw_text(panel->buffer, text_x, text_y, num, strlen(num), fg); + } + + *width = MAX_WORKSPACES * WORKSPACE_WIDTH; +} + +void panel_render_taskbar(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + int text_y = panel_text_y(panel->height); + + int current_x = x; + int available_width = panel->width - x - 100 - PANEL_PADDING; + int item_count = 0; + + /* Count visible clients */ + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) { + item_count++; + } + } + + if (item_count == 0) { + *width = 0; + return; + } + + int item_width = available_width / item_count; + if (item_width > TASKBAR_ITEM_WIDTH) { + item_width = TASKBAR_ITEM_WIDTH; + } + + /* Render each client */ + Workspace *ws = workspace_get_current(); + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace != (unsigned int)dwn->current_workspace || client_is_minimized(c)) { + continue; + } + + bool focused = (ws != NULL && ws->focused == c); + + /* Background */ + unsigned long bg = focused ? colors->workspace_active : colors->panel_bg; + XSetForeground(dpy, dwn->gc, bg); + XFillRectangle(dpy, panel->buffer, dwn->gc, + current_x, 2, item_width - 2, panel->height - 4); + + /* Title - leave room for "..." (4 bytes including null) */ + char title[64]; + strncpy(title, c->title, sizeof(title) - 4); /* Leave room for "..." */ + title[sizeof(title) - 4] = '\0'; + + /* Truncate UTF-8 aware if necessary */ + int max_text_width = item_width - 8; + bool truncated = false; + while (panel_text_width(title, strlen(title)) > max_text_width && strlen(title) > 3) { + size_t len = strlen(title); + /* Move back to find UTF-8 character boundary */ + size_t cut = len - 1; + while (cut > 0 && (title[cut] & 0xC0) == 0x80) { + cut--; /* Skip continuation bytes */ + } + if (cut > 0) cut--; + while (cut > 0 && (title[cut] & 0xC0) == 0x80) { + cut--; + } + /* Ensure we have room for "..." */ + if (cut > sizeof(title) - 4) { + cut = sizeof(title) - 4; + } + title[cut] = '\0'; + truncated = true; + } + if (truncated) { + strncat(title, "...", sizeof(title) - strlen(title) - 1); + } + + unsigned long fg = focused ? colors->panel_bg : colors->panel_fg; + panel_draw_text(panel->buffer, current_x + 4, text_y, title, strlen(title), fg); + + current_x += item_width; + } + + *width = current_x - x; +} + +void panel_render_clock(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + return; + } + if (dwn->xft_font == NULL && dwn->font == NULL) { + return; + } + + const ColorScheme *colors = config_get_colors(); + int text_y = panel_text_y(panel->height); + + panel_draw_text(panel->buffer, x, text_y, clock_buffer, + strlen(clock_buffer), colors->panel_fg); + + *width = panel_text_width(clock_buffer, strlen(clock_buffer)); +} + +void panel_render_systray(Panel *panel, int x, int *width) +{ + /* System tray placeholder - actual implementation requires + handling _NET_SYSTEM_TRAY protocol */ + (void)panel; + (void)x; + *width = 0; +} + +void panel_render_layout_indicator(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + return; + } + if (dwn->xft_font == NULL && dwn->font == NULL) { + return; + } + + const ColorScheme *colors = config_get_colors(); + int text_y = panel_text_y(panel->height); + + Workspace *ws = workspace_get_current(); + const char *symbol = layout_get_symbol(ws != NULL ? ws->layout : LAYOUT_TILING); + + panel_draw_text(panel->buffer, x, text_y, symbol, strlen(symbol), + colors->workspace_active); + + *width = panel_text_width(symbol, strlen(symbol)); +} + +void panel_render_ai_status(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + return; + } + if (dwn->xft_font == NULL && dwn->font == NULL) { + return; + } + + const ColorScheme *colors = config_get_colors(); + int text_y = panel_text_y(panel->height); + + const char *status = dwn->ai_enabled ? "[AI]" : ""; + + panel_draw_text(panel->buffer, x, text_y, status, strlen(status), + colors->workspace_active); + + *width = panel_text_width(status, strlen(status)); +} + +/* ========== Panel interaction ========== */ + +void panel_handle_click(Panel *panel, int x, int y, int button) +{ + if (panel == NULL) { + return; + } + + if (panel->position == PANEL_TOP) { + /* Check systray click first (right side) */ + int systray_actual_width = systray_get_width(); + int systray_start = panel->width - systray_actual_width - PANEL_PADDING; + if (x >= systray_start) { + systray_handle_click(x, y, button); + return; + } + + /* Check workspace click */ + int ws = panel_hit_test_workspace(panel, x, y); + if (ws >= 0 && ws < MAX_WORKSPACES) { + if (button == 1) { /* Left click */ + workspace_switch(ws); + } + return; + } + + /* Check taskbar click */ + Client *c = panel_hit_test_taskbar(panel, x, y); + if (c != NULL) { + if (button == 1) { + client_focus(c); + } else if (button == 3) { /* Right click */ + client_close(c); + } + return; + } + } else if (panel->position == PANEL_BOTTOM) { + /* Bottom panel - click on news opens article in browser */ + if (button == 1) { + news_handle_click(x, y); + } + } +} + +int panel_hit_test_workspace(Panel *panel, int x, int y) +{ + (void)y; + + if (panel == NULL || panel->position != PANEL_TOP) { + return -1; + } + + if (x < PANEL_PADDING || x >= PANEL_PADDING + MAX_WORKSPACES * WORKSPACE_WIDTH) { + return -1; + } + + return (x - PANEL_PADDING) / WORKSPACE_WIDTH; +} + +Client *panel_hit_test_taskbar(Panel *panel, int x, int y) +{ + (void)y; + + if (panel == NULL || panel->position != PANEL_TOP) { + return NULL; + } + + int taskbar_start = PANEL_PADDING + MAX_WORKSPACES * WORKSPACE_WIDTH + WIDGET_SPACING + 50; + if (x < taskbar_start) { + return NULL; + } + + int available_width = panel->width - taskbar_start - 100 - PANEL_PADDING; + int item_count = 0; + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) { + item_count++; + } + } + + if (item_count == 0) { + return NULL; + } + + int item_width = available_width / item_count; + if (item_width > TASKBAR_ITEM_WIDTH) { + item_width = TASKBAR_ITEM_WIDTH; + } + + int index = (x - taskbar_start) / item_width; + int i = 0; + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) { + if (i == index) { + return c; + } + i++; + } + } + + return NULL; +} + +/* ========== Panel visibility ========== */ + +void panel_show(Panel *panel) +{ + if (panel == NULL) { + return; + } + + panel->visible = true; + XMapRaised(dwn->display, panel->window); + panel_render(panel); +} + +void panel_hide(Panel *panel) +{ + if (panel == NULL) { + return; + } + + panel->visible = false; + XUnmapWindow(dwn->display, panel->window); +} + +void panel_toggle(Panel *panel) +{ + if (panel == NULL) { + return; + } + + if (panel->visible) { + panel_hide(panel); + } else { + panel_show(panel); + } +} + +/* ========== Clock updates ========== */ + +void panel_update_clock(void) +{ + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + + strftime(clock_buffer, sizeof(clock_buffer), CLOCK_FORMAT, tm_info); + strftime(date_buffer, sizeof(date_buffer), DATE_FORMAT, tm_info); +} + +/* ========== System stats ========== */ + +void panel_update_system_stats(void) +{ + FILE *fp; + char line[256]; + + /* Read CPU stats from /proc/stat */ + fp = fopen("/proc/stat", "r"); + if (fp != NULL) { + if (fgets(line, sizeof(line), fp) != NULL) { + unsigned long long user, nice, system, idle, iowait, irq, softirq; + if (sscanf(line, "cpu %llu %llu %llu %llu %llu %llu %llu", + &user, &nice, &system, &idle, &iowait, &irq, &softirq) >= 4) { + unsigned long long total = user + nice + system + idle + iowait + irq + softirq; + unsigned long long idle_time = idle + iowait; + + if (sys_stats.prev_total > 0) { + unsigned long long total_diff = total - sys_stats.prev_total; + unsigned long long idle_diff = idle_time - sys_stats.prev_idle; + if (total_diff > 0) { + sys_stats.cpu_percent = (int)(100 * (total_diff - idle_diff) / total_diff); + } + } + + sys_stats.prev_total = total; + sys_stats.prev_idle = idle_time; + } + } + fclose(fp); + } + + /* Read memory stats from /proc/meminfo */ + fp = fopen("/proc/meminfo", "r"); + if (fp != NULL) { + unsigned long mem_total = 0, mem_free = 0, buffers = 0, cached = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + if (strncmp(line, "MemTotal:", 9) == 0) { + sscanf(line + 9, " %lu", &mem_total); + } else if (strncmp(line, "MemFree:", 8) == 0) { + sscanf(line + 8, " %lu", &mem_free); + } else if (strncmp(line, "Buffers:", 8) == 0) { + sscanf(line + 8, " %lu", &buffers); + } else if (strncmp(line, "Cached:", 7) == 0) { + sscanf(line + 7, " %lu", &cached); + break; /* Got all we need */ + } + } + fclose(fp); + + if (mem_total > 0) { + unsigned long used = mem_total - mem_free - buffers - cached; + sys_stats.mem_total_mb = (int)(mem_total / 1024); + sys_stats.mem_used_mb = (int)(used / 1024); + sys_stats.mem_percent = (int)(100 * used / mem_total); + } + } + + /* Read load average from /proc/loadavg */ + fp = fopen("/proc/loadavg", "r"); + if (fp != NULL) { + if (fscanf(fp, "%f %f %f", + &sys_stats.load_1min, + &sys_stats.load_5min, + &sys_stats.load_15min) != 3) { + sys_stats.load_1min = 0; + sys_stats.load_5min = 0; + sys_stats.load_15min = 0; + } + fclose(fp); + } +} + +/* Calculate stats width without rendering */ +static int panel_calculate_stats_width(void) +{ + if (dwn->xft_font == NULL && dwn->font == NULL) return 0; + + char buf[256]; + int total = 0; + + /* CPU */ + int len = snprintf(buf, sizeof(buf), "CPU:%2d%%", sys_stats.cpu_percent); + total += panel_text_width(buf, len) + WIDGET_SPACING; + + /* Memory */ + if (sys_stats.mem_total_mb >= 1024) { + len = snprintf(buf, sizeof(buf), "MEM:%.1fG/%dG", + sys_stats.mem_used_mb / 1024.0f, sys_stats.mem_total_mb / 1024); + } else { + len = snprintf(buf, sizeof(buf), "MEM:%dM/%dM", + sys_stats.mem_used_mb, sys_stats.mem_total_mb); + } + total += panel_text_width(buf, len) + WIDGET_SPACING; + + /* Load */ + len = snprintf(buf, sizeof(buf), "Load:%.2f %.2f %.2f", + sys_stats.load_1min, sys_stats.load_5min, sys_stats.load_15min); + total += panel_text_width(buf, len) + WIDGET_SPACING; + + /* Battery - use thread-safe snapshot */ + BatteryState bat_snap = systray_get_battery_snapshot(); + if (bat_snap.present) { + const char *bat_icon = bat_snap.charging ? "[+]" : "[=]"; + len = snprintf(buf, sizeof(buf), "%s%d%%", bat_icon, bat_snap.percentage); + total += panel_text_width(buf, len) + WIDGET_SPACING; + } + + return total; +} + +static void panel_render_system_stats(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + *width = 0; + return; + } + if (dwn->xft_font == NULL && dwn->font == NULL) { + *width = 0; + return; + } + + const ColorScheme *colors = config_get_colors(); + int text_y = panel_text_y(panel->height); + int current_x = x; + + /* Format: "CPU: 25% | MEM: 4.2G/16G | Load: 1.23 0.98 0.76 | BAT: 85%" */ + + char stats_buf[256]; + int len; + + /* CPU with color coding */ + unsigned long cpu_color = colors->panel_fg; + if (sys_stats.cpu_percent >= 90) { + cpu_color = colors->workspace_urgent; /* Red for high CPU */ + } else if (sys_stats.cpu_percent >= 70) { + cpu_color = 0xFFA500; /* Orange for medium-high */ + } + + len = snprintf(stats_buf, sizeof(stats_buf), "CPU:%2d%%", sys_stats.cpu_percent); + panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, cpu_color); + current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING; + + /* Memory */ + unsigned long mem_color = colors->panel_fg; + if (sys_stats.mem_percent >= 90) { + mem_color = colors->workspace_urgent; + } else if (sys_stats.mem_percent >= 75) { + mem_color = 0xFFA500; + } + + if (sys_stats.mem_total_mb >= 1024) { + len = snprintf(stats_buf, sizeof(stats_buf), "MEM:%.1fG/%dG", + sys_stats.mem_used_mb / 1024.0f, sys_stats.mem_total_mb / 1024); + } else { + len = snprintf(stats_buf, sizeof(stats_buf), "MEM:%dM/%dM", + sys_stats.mem_used_mb, sys_stats.mem_total_mb); + } + panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, mem_color); + current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING; + + /* Load average */ + unsigned long load_color = colors->panel_fg; + if (sys_stats.load_1min >= 4.0f) { + load_color = colors->workspace_urgent; + } else if (sys_stats.load_1min >= 2.0f) { + load_color = 0xFFA500; + } + + len = snprintf(stats_buf, sizeof(stats_buf), "Load:%.2f %.2f %.2f", + sys_stats.load_1min, sys_stats.load_5min, sys_stats.load_15min); + panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, load_color); + current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING; + + /* Battery (if present) - use thread-safe snapshot */ + BatteryState bat_snap = systray_get_battery_snapshot(); + if (bat_snap.present) { + unsigned long bat_color = colors->panel_fg; + if (bat_snap.percentage <= 20 && !bat_snap.charging) { + bat_color = colors->workspace_urgent; /* Red for low */ + } else if (bat_snap.percentage <= 40 && !bat_snap.charging) { + bat_color = 0xFFA500; /* Orange for medium-low */ + } else if (bat_snap.charging) { + bat_color = colors->workspace_active; /* Blue for charging */ + } + + const char *bat_icon = bat_snap.charging ? "[+]" : "[=]"; + len = snprintf(stats_buf, sizeof(stats_buf), "%s%d%%", bat_icon, bat_snap.percentage); + panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, bat_color); + current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING; + } + + *width = current_x - x; +} + +/* ========== System tray ========== */ + +void panel_init_systray(void) +{ + /* System tray initialization - requires _NET_SYSTEM_TRAY protocol */ + LOG_DEBUG("System tray initialization (placeholder)"); +} + +void panel_add_systray_icon(Window icon) +{ + (void)icon; + LOG_DEBUG("Add systray icon (placeholder)"); +} + +void panel_remove_systray_icon(Window icon) +{ + (void)icon; + LOG_DEBUG("Remove systray icon (placeholder)"); +} diff --git a/src/systray.c b/src/systray.c new file mode 100644 index 0000000..9f39b61 --- /dev/null +++ b/src/systray.c @@ -0,0 +1,1105 @@ +/* + * DWN - Desktop Window Manager + * System tray widgets implementation with UTF-8 support + */ + +#include "systray.h" +#include "panel.h" +#include "config.h" +#include "util.h" +#include "notifications.h" + +#include +#include +#include +#include +#include +#include + +/* UTF-8 Icons for system tray */ +#define ICON_WIFI_FULL "\xE2\x96\x82\xE2\x96\x84\xE2\x96\x86\xE2\x96\x88" /* Full signal bars */ +#define ICON_WIFI_OFF "\xE2\x9C\x97" /* X mark */ +#define ICON_VOLUME_HIGH "\xF0\x9F\x94\x8A" /* Speaker high */ +#define ICON_VOLUME_MED "\xF0\x9F\x94\x89" /* Speaker medium */ +#define ICON_VOLUME_LOW "\xF0\x9F\x94\x88" /* Speaker low */ +#define ICON_VOLUME_MUTE "\xF0\x9F\x94\x87" /* Speaker muted */ +#define ICON_BATTERY_FULL "\xF0\x9F\x94\x8B" /* Battery full */ +#define ICON_BATTERY_CHARGE "\xE2\x9A\xA1" /* Lightning bolt */ + +/* Simple ASCII fallback icons */ +#define ASCII_WIFI_ON "W" +#define ASCII_WIFI_OFF "-" +#define ASCII_VOL_HIGH "V" +#define ASCII_VOL_MUTE "M" +#define ASCII_BATTERY "B" +#define ASCII_CHARGING "+" + +/* Widget dimensions */ +#define SYSTRAY_SPACING 12 +#define DROPDOWN_ITEM_HEIGHT 28 +#define DROPDOWN_WIDTH 400 +#define DROPDOWN_PADDING 8 +#define SLIDER_WIDTH 30 +#define SLIDER_HEIGHT 120 +#define SLIDER_PADDING 8 +#define SLIDER_KNOB_HEIGHT 8 + +/* Scan interval in milliseconds */ +#define WIFI_SCAN_INTERVAL 10000 + +/* Global state */ +WifiState wifi_state = {0}; +AudioState audio_state = {0}; +BatteryState battery_state = {0}; +DropdownMenu *wifi_menu = NULL; +VolumeSlider *volume_slider = NULL; + +/* Async update thread */ +static pthread_t update_thread; +static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER; +static volatile int thread_running = 0; + +/* Systray position tracking */ +static int systray_x = 0; +static int systray_width = 0; +static int wifi_icon_x = 0; +static int audio_icon_x = 0; + +/* Forward declarations for internal update functions */ +static void wifi_update_state_internal(void); +static void audio_update_state_internal(void); + +/* ========== UTF-8 Text Drawing ========== */ + +static void draw_utf8_text(Drawable d, int x, int y, const char *text, unsigned long color) +{ + if (dwn == NULL || dwn->display == NULL || text == NULL) { + return; + } + + if (dwn->xft_font != NULL) { + /* Use Xft for proper UTF-8 rendering */ + XftDraw *xft_draw = XftDrawCreate(dwn->display, d, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap); + if (xft_draw != NULL) { + XftColor xft_color; + XRenderColor render_color; + + /* Convert X11 color to XRender color */ + render_color.red = ((color >> 16) & 0xFF) * 257; + render_color.green = ((color >> 8) & 0xFF) * 257; + render_color.blue = (color & 0xFF) * 257; + render_color.alpha = 0xFFFF; + + XftColorAllocValue(dwn->display, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &render_color, &xft_color); + + XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, + x, y, (const FcChar8 *)text, strlen(text)); + + XftColorFree(dwn->display, + DefaultVisual(dwn->display, dwn->screen), + dwn->colormap, &xft_color); + XftDrawDestroy(xft_draw); + return; + } + } + + /* Fallback to basic X11 drawing */ + XSetForeground(dwn->display, dwn->gc, color); + XDrawString(dwn->display, d, dwn->gc, x, y, text, strlen(text)); +} + +static int get_text_width(const char *text) +{ + if (dwn == NULL || text == NULL) { + return 0; + } + + if (dwn->xft_font != NULL) { + XGlyphInfo extents; + XftTextExtentsUtf8(dwn->display, dwn->xft_font, + (const FcChar8 *)text, strlen(text), &extents); + return extents.xOff; + } + + if (dwn->font != NULL) { + return XTextWidth(dwn->font, text, strlen(text)); + } + + return strlen(text) * 8; /* Rough estimate */ +} + +/* ========== Initialization ========== */ + +/* Background thread for async state updates */ +static void *systray_update_thread(void *arg) +{ + (void)arg; + + while (thread_running) { + /* Update WiFi state (slow - uses popen) */ + wifi_update_state_internal(); + + /* Update audio state (slow - uses popen) */ + audio_update_state_internal(); + + /* Update battery state (fast - reads /sys files) */ + pthread_mutex_lock(&state_mutex); + battery_update_state(); + pthread_mutex_unlock(&state_mutex); + + /* Sleep for 2 seconds between updates */ + for (int i = 0; i < 20 && thread_running; i++) { + usleep(100000); /* 100ms chunks for responsive shutdown */ + } + } + + return NULL; +} + +void systray_init(void) +{ + memset(&wifi_state, 0, sizeof(wifi_state)); + memset(&audio_state, 0, sizeof(audio_state)); + memset(&battery_state, 0, sizeof(battery_state)); + + /* Set default states for instant display */ + audio_state.volume = 50; + wifi_state.enabled = true; + + /* Start background update thread - all updates happen async */ + thread_running = 1; + if (pthread_create(&update_thread, NULL, systray_update_thread, NULL) != 0) { + LOG_WARN("Failed to create systray update thread"); + thread_running = 0; + } + + LOG_INFO("System tray initialized"); +} + +void systray_cleanup(void) +{ + /* Stop the update thread */ + if (thread_running) { + thread_running = 0; + pthread_join(update_thread, NULL); + } + + if (wifi_menu != NULL) { + dropdown_destroy(wifi_menu); + wifi_menu = NULL; + } + if (volume_slider != NULL) { + volume_slider_destroy(volume_slider); + volume_slider = NULL; + } +} + +/* ========== Battery Functions ========== */ + +void battery_update_state(void) +{ + DIR *dir; + struct dirent *entry; + char path[512]; + char value[64]; + FILE *fp; + + battery_state.present = false; + battery_state.charging = false; + battery_state.percentage = 0; + + /* Look for battery in /sys/class/power_supply */ + dir = opendir("/sys/class/power_supply"); + if (dir == NULL) { + return; + } + + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + /* Check if this is a battery (not AC adapter) */ + snprintf(path, sizeof(path), "/sys/class/power_supply/%s/type", entry->d_name); + fp = fopen(path, "r"); + if (fp != NULL) { + if (fgets(value, sizeof(value), fp) != NULL) { + value[strcspn(value, "\n")] = '\0'; + if (strcmp(value, "Battery") == 0) { + battery_state.present = true; + + /* Get capacity */ + snprintf(path, sizeof(path), "/sys/class/power_supply/%s/capacity", entry->d_name); + FILE *cap_fp = fopen(path, "r"); + if (cap_fp != NULL) { + if (fgets(value, sizeof(value), cap_fp) != NULL) { + battery_state.percentage = atoi(value); + } + fclose(cap_fp); + } + + /* Get status (charging/discharging) */ + snprintf(path, sizeof(path), "/sys/class/power_supply/%s/status", entry->d_name); + FILE *status_fp = fopen(path, "r"); + if (status_fp != NULL) { + if (fgets(value, sizeof(value), status_fp) != NULL) { + value[strcspn(value, "\n")] = '\0'; + battery_state.charging = (strcmp(value, "Charging") == 0 || + strcmp(value, "Full") == 0); + } + fclose(status_fp); + } + + fclose(fp); + break; /* Found a battery, stop searching */ + } + } + fclose(fp); + } + } + closedir(dir); +} + +const char *battery_get_icon(void) +{ + if (!battery_state.present) { + return ""; /* No battery = no icon */ + } + + if (battery_state.charging) { + return ASCII_CHARGING; + } + + return ASCII_BATTERY; +} + +/* ========== WiFi Functions ========== */ + +/* Internal update function - called from background thread */ +static void wifi_update_state_internal(void) +{ + FILE *fp; + char line[256]; + WifiState temp_state = {0}; + + /* Check if WiFi is connected and get current SSID */ + fp = popen("nmcli -t -f GENERAL.STATE,GENERAL.CONNECTION dev show 2>/dev/null | head -2", "r"); + if (fp != NULL) { + while (fgets(line, sizeof(line), fp) != NULL) { + line[strcspn(line, "\n")] = '\0'; + + if (strstr(line, "GENERAL.STATE:") != NULL) { + if (strstr(line, "100 (connected)") != NULL) { + temp_state.connected = true; + temp_state.enabled = true; + } + } + if (strstr(line, "GENERAL.CONNECTION:") != NULL) { + char *ssid = strchr(line, ':'); + if (ssid != NULL && strlen(ssid) > 1) { + ssid++; + strncpy(temp_state.current_ssid, ssid, sizeof(temp_state.current_ssid) - 1); + } + } + } + pclose(fp); + } + + /* Get signal strength if connected */ + if (temp_state.connected) { + fp = popen("nmcli -t -f IN-USE,SIGNAL dev wifi list 2>/dev/null | grep '^\\*' | cut -d: -f2", "r"); + if (fp != NULL) { + if (fgets(line, sizeof(line), fp) != NULL) { + temp_state.signal_strength = atoi(line); + } + pclose(fp); + } + } + + /* Copy to global state with mutex protection */ + pthread_mutex_lock(&state_mutex); + wifi_state.connected = temp_state.connected; + wifi_state.enabled = temp_state.enabled; + wifi_state.signal_strength = temp_state.signal_strength; + memcpy(wifi_state.current_ssid, temp_state.current_ssid, sizeof(wifi_state.current_ssid)); + pthread_mutex_unlock(&state_mutex); +} + +/* Public function - now just reads cached state (non-blocking) */ +void wifi_update_state(void) +{ + /* State is updated by background thread - this is now a no-op */ + /* Kept for API compatibility */ +} + +void wifi_scan_networks(void) +{ + FILE *fp; + char line[512]; + + /* Build network list in temporary storage first */ + WifiNetwork temp_networks[MAX_WIFI_NETWORKS]; + int temp_count = 0; + long scan_time = get_time_ms(); + + fp = popen("nmcli -t -f SSID,SIGNAL,SECURITY,IN-USE dev wifi list 2>/dev/null", "r"); + if (fp == NULL) { + LOG_WARN("Failed to scan WiFi networks"); + return; + } + + while (fgets(line, sizeof(line), fp) != NULL && temp_count < MAX_WIFI_NETWORKS) { + line[strcspn(line, "\n")] = '\0'; + + char *ssid = strtok(line, ":"); + char *signal = strtok(NULL, ":"); + char *security = strtok(NULL, ":"); + char *in_use = strtok(NULL, ":"); + + if (ssid != NULL && strlen(ssid) > 0) { + WifiNetwork *net = &temp_networks[temp_count]; + strncpy(net->ssid, ssid, sizeof(net->ssid) - 1); + net->ssid[sizeof(net->ssid) - 1] = '\0'; + net->signal = signal ? atoi(signal) : 0; + strncpy(net->security, security ? security : "", sizeof(net->security) - 1); + net->security[sizeof(net->security) - 1] = '\0'; + net->connected = (in_use != NULL && in_use[0] == '*'); + temp_count++; + } + } + pclose(fp); + + /* Copy to global state under lock */ + pthread_mutex_lock(&state_mutex); + memcpy(wifi_state.networks, temp_networks, sizeof(temp_networks)); + wifi_state.network_count = temp_count; + wifi_state.last_scan = scan_time; + pthread_mutex_unlock(&state_mutex); + + LOG_DEBUG("Scanned %d WiFi networks", temp_count); +} + +void wifi_connect(const char *ssid) +{ + if (ssid == NULL || strlen(ssid) == 0) { + return; + } + + char cmd[256]; + snprintf(cmd, sizeof(cmd), "nmcli dev wifi connect \"%s\" &", ssid); + + char msg[128]; + snprintf(msg, sizeof(msg), "Connecting to %s...", ssid); + notification_show("WiFi", "Connecting", msg, NULL, 3000); + + spawn_async(cmd); + + pthread_mutex_lock(&state_mutex); + wifi_state.connected = false; + pthread_mutex_unlock(&state_mutex); +} + +void wifi_disconnect(void) +{ + spawn_async("nmcli dev disconnect wlan0 &"); + + pthread_mutex_lock(&state_mutex); + wifi_state.connected = false; + pthread_mutex_unlock(&state_mutex); + + notification_show("WiFi", "Disconnected", "WiFi connection closed", NULL, 2000); +} + +const char *wifi_get_icon(void) +{ + if (!wifi_state.enabled || !wifi_state.connected) { + return ASCII_WIFI_OFF; + } + return ASCII_WIFI_ON; +} + +/* ========== Audio Functions ========== */ + +/* Internal update function - called from background thread */ +static void audio_update_state_internal(void) +{ + FILE *fp; + char line[256]; + int volume = 50; + bool muted = false; + + fp = popen("amixer get Master 2>/dev/null | grep -o '[0-9]*%\\|\\[on\\]\\|\\[off\\]' | head -2", "r"); + if (fp != NULL) { + while (fgets(line, sizeof(line), fp) != NULL) { + line[strcspn(line, "\n")] = '\0'; + + if (strchr(line, '%') != NULL) { + volume = atoi(line); + } else if (strcmp(line, "[off]") == 0) { + muted = true; + } else if (strcmp(line, "[on]") == 0) { + muted = false; + } + } + pclose(fp); + } + + /* Copy to global state with mutex protection */ + pthread_mutex_lock(&state_mutex); + audio_state.volume = volume; + audio_state.muted = muted; + pthread_mutex_unlock(&state_mutex); +} + +/* Public function - now just reads cached state (non-blocking) */ +void audio_update_state(void) +{ + /* State is updated by background thread - this is now a no-op */ + /* Kept for API compatibility */ +} + +void audio_set_volume(int volume) +{ + if (volume < 0) volume = 0; + if (volume > 100) volume = 100; + + char cmd[64]; + snprintf(cmd, sizeof(cmd), "amixer set Master %d%% >/dev/null 2>&1 &", volume); + spawn_async(cmd); + + pthread_mutex_lock(&state_mutex); + audio_state.volume = volume; + audio_state.muted = false; + pthread_mutex_unlock(&state_mutex); +} + +void audio_toggle_mute(void) +{ + spawn_async("amixer set Master toggle >/dev/null 2>&1 &"); + + pthread_mutex_lock(&state_mutex); + audio_state.muted = !audio_state.muted; + pthread_mutex_unlock(&state_mutex); +} + +const char *audio_get_icon(void) +{ + if (audio_state.muted || audio_state.volume == 0) { + return ASCII_VOL_MUTE; + } + return ASCII_VOL_HIGH; +} + +/* ========== Volume Slider ========== */ + +VolumeSlider *volume_slider_create(int x, int y) +{ + if (dwn == NULL || dwn->display == NULL) { + return NULL; + } + + VolumeSlider *slider = dwn_calloc(1, sizeof(VolumeSlider)); + slider->x = x; + slider->y = y; + slider->width = SLIDER_WIDTH; + slider->height = SLIDER_HEIGHT; + slider->visible = false; + slider->dragging = false; + + return slider; +} + +void volume_slider_destroy(VolumeSlider *slider) +{ + if (slider == NULL) { + return; + } + + if (slider->window != None && dwn != NULL && dwn->display != NULL) { + XDestroyWindow(dwn->display, slider->window); + } + + dwn_free(slider); +} + +void volume_slider_show(VolumeSlider *slider) +{ + if (slider == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (slider->window == None) { + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = dwn->config->colors.panel_bg; + swa.border_pixel = dwn->config->colors.border_focused; + swa.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask | + PointerMotionMask | LeaveWindowMask; + + slider->window = XCreateWindow(dwn->display, dwn->root, + slider->x, slider->y, + slider->width, slider->height, + 1, + CopyFromParent, InputOutput, CopyFromParent, + CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, + &swa); + } + + slider->visible = true; + XMapRaised(dwn->display, slider->window); + volume_slider_render(slider); +} + +void volume_slider_hide(VolumeSlider *slider) +{ + if (slider == NULL || slider->window == None) { + return; + } + + slider->visible = false; + slider->dragging = false; + XUnmapWindow(dwn->display, slider->window); +} + +void volume_slider_render(VolumeSlider *slider) +{ + if (slider == NULL || slider->window == None || !slider->visible) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + + /* Clear background */ + XSetForeground(dpy, dwn->gc, colors->panel_bg); + XFillRectangle(dpy, slider->window, dwn->gc, 0, 0, slider->width, slider->height); + + /* Draw track */ + int track_x = slider->width / 2 - 2; + int track_y = SLIDER_PADDING; + int track_height = slider->height - 2 * SLIDER_PADDING; + + XSetForeground(dpy, dwn->gc, colors->workspace_inactive); + XFillRectangle(dpy, slider->window, dwn->gc, track_x, track_y, 4, track_height); + + /* Draw filled portion */ + int fill_height = (audio_state.volume * track_height) / 100; + int fill_y = track_y + track_height - fill_height; + + XSetForeground(dpy, dwn->gc, colors->workspace_active); + XFillRectangle(dpy, slider->window, dwn->gc, track_x, fill_y, 4, fill_height); + + /* Draw knob */ + int knob_y = fill_y - SLIDER_KNOB_HEIGHT / 2; + if (knob_y < track_y) knob_y = track_y; + if (knob_y > track_y + track_height - SLIDER_KNOB_HEIGHT) { + knob_y = track_y + track_height - SLIDER_KNOB_HEIGHT; + } + + XSetForeground(dpy, dwn->gc, colors->panel_fg); + XFillRectangle(dpy, slider->window, dwn->gc, + track_x - 4, knob_y, 12, SLIDER_KNOB_HEIGHT); + + /* Draw percentage text at bottom */ + char vol_text[8]; + snprintf(vol_text, sizeof(vol_text), "%d%%", audio_state.volume); + int text_width = get_text_width(vol_text); + int text_x = (slider->width - text_width) / 2; + + draw_utf8_text(slider->window, text_x, slider->height - 4, vol_text, colors->panel_fg); + + XFlush(dpy); +} + +void volume_slider_handle_click(VolumeSlider *slider, int x, int y) +{ + (void)x; + + if (slider == NULL || !slider->visible) { + return; + } + + slider->dragging = true; + + /* Calculate volume from y position */ + int track_y = SLIDER_PADDING; + int track_height = slider->height - 2 * SLIDER_PADDING; + + int relative_y = y - track_y; + if (relative_y < 0) relative_y = 0; + if (relative_y > track_height) relative_y = track_height; + + int new_volume = 100 - (relative_y * 100 / track_height); + audio_set_volume(new_volume); + volume_slider_render(slider); + panel_render_all(); +} + +void volume_slider_handle_motion(VolumeSlider *slider, int x, int y) +{ + if (slider == NULL || !slider->visible || !slider->dragging) { + return; + } + + volume_slider_handle_click(slider, x, y); +} + +void volume_slider_handle_release(VolumeSlider *slider) +{ + if (slider == NULL) { + return; + } + slider->dragging = false; +} + +/* ========== Dropdown Menu ========== */ + +DropdownMenu *dropdown_create(int x, int y, int width) +{ + if (dwn == NULL || dwn->display == NULL) { + return NULL; + } + + DropdownMenu *menu = dwn_calloc(1, sizeof(DropdownMenu)); + menu->x = x; + menu->y = y; + menu->width = width; + menu->height = 0; + menu->item_count = 0; + menu->hovered_item = -1; + menu->visible = false; + menu->on_select = NULL; + + return menu; +} + +void dropdown_destroy(DropdownMenu *menu) +{ + if (menu == NULL) { + return; + } + + if (menu->window != None && dwn != NULL && dwn->display != NULL) { + XDestroyWindow(dwn->display, menu->window); + } + + dwn_free(menu); +} + +void dropdown_show(DropdownMenu *menu) +{ + if (menu == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + wifi_scan_networks(); + + menu->item_count = wifi_state.network_count; + menu->height = menu->item_count * DROPDOWN_ITEM_HEIGHT + 2 * DROPDOWN_PADDING; + + if (menu->height < DROPDOWN_ITEM_HEIGHT + 2 * DROPDOWN_PADDING) { + menu->height = DROPDOWN_ITEM_HEIGHT + 2 * DROPDOWN_PADDING; + menu->item_count = 1; + } + + /* Calculate auto-width based on longest network name + signal */ + int max_text_width = 0; + for (int i = 0; i < wifi_state.network_count; i++) { + WifiNetwork *net = &wifi_state.networks[i]; + char label[128]; + snprintf(label, sizeof(label), "%s %d%%", net->ssid, net->signal); + int text_width = get_text_width(label); + if (text_width > max_text_width) { + max_text_width = text_width; + } + } + int min_width = 250; + int calculated_width = max_text_width + 2 * DROPDOWN_PADDING + 30; + menu->width = (calculated_width > min_width) ? calculated_width : min_width; + + if (menu->window == None) { + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = dwn->config->colors.panel_bg; + swa.border_pixel = dwn->config->colors.border_focused; + swa.event_mask = ExposureMask | ButtonPressMask | PointerMotionMask | + LeaveWindowMask | EnterWindowMask; + + menu->window = XCreateWindow(dwn->display, dwn->root, + menu->x, menu->y, + menu->width, menu->height, + 1, + CopyFromParent, InputOutput, CopyFromParent, + CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, + &swa); + } else { + XMoveResizeWindow(dwn->display, menu->window, + menu->x, menu->y, menu->width, menu->height); + } + + menu->visible = true; + XMapRaised(dwn->display, menu->window); + dropdown_render(menu); +} + +void dropdown_hide(DropdownMenu *menu) +{ + if (menu == NULL || menu->window == None) { + return; + } + + menu->visible = false; + XUnmapWindow(dwn->display, menu->window); +} + +void dropdown_render(DropdownMenu *menu) +{ + if (menu == NULL || menu->window == None || !menu->visible) { + return; + } + + if (dwn == NULL || dwn->display == NULL) { + return; + } + + Display *dpy = dwn->display; + const ColorScheme *colors = config_get_colors(); + + int font_height = 12; + if (dwn->xft_font != NULL) { + font_height = dwn->xft_font->ascent; + } else if (dwn->font != NULL) { + font_height = dwn->font->ascent; + } + int text_y_offset = (DROPDOWN_ITEM_HEIGHT + font_height) / 2; + + /* Clear background */ + XSetForeground(dpy, dwn->gc, colors->panel_bg); + XFillRectangle(dpy, menu->window, dwn->gc, 0, 0, menu->width, menu->height); + + if (wifi_state.network_count == 0) { + draw_utf8_text(menu->window, DROPDOWN_PADDING, DROPDOWN_PADDING + text_y_offset, + "No networks found", colors->workspace_inactive); + } else { + for (int i = 0; i < wifi_state.network_count && i < MAX_WIFI_NETWORKS; i++) { + WifiNetwork *net = &wifi_state.networks[i]; + int y = DROPDOWN_PADDING + i * DROPDOWN_ITEM_HEIGHT; + + /* Highlight hovered item */ + if (i == menu->hovered_item) { + XSetForeground(dpy, dwn->gc, colors->workspace_active); + XFillRectangle(dpy, menu->window, dwn->gc, + 2, y, menu->width - 4, DROPDOWN_ITEM_HEIGHT); + } + + unsigned long text_color = (i == menu->hovered_item) ? colors->panel_bg : colors->panel_fg; + + /* Network name */ + char label[80]; + if (net->connected) { + snprintf(label, sizeof(label), "> %s", net->ssid); + } else { + snprintf(label, sizeof(label), " %s", net->ssid); + } + + /* Truncate if needed */ + if (strlen(label) > 25) { + label[22] = '.'; + label[23] = '.'; + label[24] = '.'; + label[25] = '\0'; + } + + draw_utf8_text(menu->window, DROPDOWN_PADDING, y + text_y_offset, label, text_color); + + /* Signal strength */ + char signal[8]; + snprintf(signal, sizeof(signal), "%d%%", net->signal); + int signal_width = get_text_width(signal); + int signal_x = menu->width - DROPDOWN_PADDING - signal_width; + + unsigned long signal_color = (i == menu->hovered_item) ? colors->panel_bg : colors->workspace_inactive; + draw_utf8_text(menu->window, signal_x, y + text_y_offset, signal, signal_color); + } + } + + XFlush(dpy); +} + +int dropdown_hit_test(DropdownMenu *menu, int x, int y) +{ + (void)x; + + if (menu == NULL || !menu->visible) { + return -1; + } + + if (y < DROPDOWN_PADDING || y >= menu->height - DROPDOWN_PADDING) { + return -1; + } + + int item = (y - DROPDOWN_PADDING) / DROPDOWN_ITEM_HEIGHT; + if (item >= 0 && item < menu->item_count) { + return item; + } + + return -1; +} + +void dropdown_handle_click(DropdownMenu *menu, int x, int y) +{ + if (menu == NULL || !menu->visible) { + return; + } + + int item = dropdown_hit_test(menu, x, y); + if (item >= 0 && item < wifi_state.network_count) { + WifiNetwork *net = &wifi_state.networks[item]; + + if (net->connected) { + wifi_disconnect(); + } else { + wifi_connect(net->ssid); + } + + dropdown_hide(menu); + } +} + +void dropdown_handle_motion(DropdownMenu *menu, int x, int y) +{ + if (menu == NULL || !menu->visible) { + return; + } + + int old_hovered = menu->hovered_item; + menu->hovered_item = dropdown_hit_test(menu, x, y); + + if (menu->hovered_item != old_hovered) { + dropdown_render(menu); + } +} + +/* ========== System Tray Rendering ========== */ + +int systray_get_width(void) +{ + pthread_mutex_lock(&state_mutex); + AudioState audio_snap = audio_state; + WifiState wifi_snap; + wifi_snap.connected = wifi_state.connected; + wifi_snap.enabled = wifi_state.enabled; + wifi_snap.signal_strength = wifi_state.signal_strength; + memcpy(wifi_snap.current_ssid, wifi_state.current_ssid, sizeof(wifi_snap.current_ssid)); + pthread_mutex_unlock(&state_mutex); + + char audio_label[32]; + const char *audio_icon = (audio_snap.muted || audio_snap.volume == 0) ? ASCII_VOL_MUTE : ASCII_VOL_HIGH; + snprintf(audio_label, sizeof(audio_label), "%s%d%%", audio_icon, audio_snap.volume); + + char wifi_label[128]; + const char *wifi_icon_str = (!wifi_snap.enabled || !wifi_snap.connected) ? ASCII_WIFI_OFF : ASCII_WIFI_ON; + if (wifi_snap.connected && wifi_snap.current_ssid[0] != '\0') { + snprintf(wifi_label, sizeof(wifi_label), "%s %s %d%%", + wifi_icon_str, wifi_snap.current_ssid, wifi_snap.signal_strength); + } else { + snprintf(wifi_label, sizeof(wifi_label), "%s", wifi_icon_str); + } + + return get_text_width(audio_label) + SYSTRAY_SPACING + get_text_width(wifi_label); +} + +void systray_render(Panel *panel, int x, int *width) +{ + if (panel == NULL || width == NULL) { + return; + } + + const ColorScheme *colors = config_get_colors(); + + /* Take thread-safe snapshots of state */ + pthread_mutex_lock(&state_mutex); + AudioState audio_snap = audio_state; + WifiState wifi_snap; + wifi_snap.connected = wifi_state.connected; + wifi_snap.enabled = wifi_state.enabled; + wifi_snap.signal_strength = wifi_state.signal_strength; + memcpy(wifi_snap.current_ssid, wifi_state.current_ssid, sizeof(wifi_snap.current_ssid)); + pthread_mutex_unlock(&state_mutex); + + int font_height = 12; + if (dwn->xft_font != NULL) { + font_height = dwn->xft_font->ascent; + } else if (dwn->font != NULL) { + font_height = dwn->font->ascent; + } + int text_y = (panel->height + font_height) / 2; + + systray_x = x; + int current_x = x; + + /* Audio indicator */ + audio_icon_x = current_x; + char audio_label[32]; + const char *audio_icon = (audio_snap.muted || audio_snap.volume == 0) ? ASCII_VOL_MUTE : ASCII_VOL_HIGH; + snprintf(audio_label, sizeof(audio_label), "%s%d%%", audio_icon, audio_snap.volume); + + unsigned long audio_color = audio_snap.muted ? colors->workspace_inactive : colors->panel_fg; + draw_utf8_text(panel->buffer, current_x, text_y, audio_label, audio_color); + current_x += get_text_width(audio_label) + SYSTRAY_SPACING; + + /* WiFi indicator - show SSID and signal strength */ + wifi_icon_x = current_x; + char wifi_label[128]; + const char *wifi_icon_str = (!wifi_snap.enabled || !wifi_snap.connected) ? ASCII_WIFI_OFF : ASCII_WIFI_ON; + + if (wifi_snap.connected && wifi_snap.current_ssid[0] != '\0') { + snprintf(wifi_label, sizeof(wifi_label), "%s %s %d%%", + wifi_icon_str, wifi_snap.current_ssid, wifi_snap.signal_strength); + } else { + snprintf(wifi_label, sizeof(wifi_label), "%s", wifi_icon_str); + } + + unsigned long wifi_color = wifi_snap.connected ? colors->panel_fg : colors->workspace_inactive; + draw_utf8_text(panel->buffer, current_x, text_y, wifi_label, wifi_color); + current_x += get_text_width(wifi_label); + + systray_width = current_x - x; + *width = systray_width; +} + +int systray_hit_test(int x) +{ + if (x >= wifi_icon_x) { + return 0; /* WiFi - extends to end of systray */ + } + if (x >= audio_icon_x && x < wifi_icon_x) { + return 1; /* Audio */ + } + return -1; +} + +void systray_handle_click(int x, int y, int button) +{ + int widget = systray_hit_test(x); + + if (widget == 0) { /* WiFi clicked */ + /* Hide volume slider if open */ + if (volume_slider != NULL && volume_slider->visible) { + volume_slider_hide(volume_slider); + } + + if (button == 1) { + if (wifi_menu == NULL) { + int panel_height = config_get_panel_height(); + wifi_menu = dropdown_create(wifi_icon_x, panel_height, DROPDOWN_WIDTH); + } + + if (wifi_menu->visible) { + dropdown_hide(wifi_menu); + } else { + wifi_menu->x = wifi_icon_x; + wifi_menu->y = config_get_panel_height(); + dropdown_show(wifi_menu); + } + } else if (button == 3) { + if (wifi_state.connected) { + wifi_disconnect(); + } + } + } else if (widget == 1) { /* Audio clicked */ + /* Hide wifi menu if open */ + if (wifi_menu != NULL && wifi_menu->visible) { + dropdown_hide(wifi_menu); + } + + if (button == 1) { + /* Show volume slider */ + if (volume_slider == NULL) { + int panel_height = config_get_panel_height(); + volume_slider = volume_slider_create(audio_icon_x, panel_height); + } + + if (volume_slider->visible) { + volume_slider_hide(volume_slider); + } else { + volume_slider->x = audio_icon_x; + volume_slider->y = config_get_panel_height(); + volume_slider_show(volume_slider); + } + } else if (button == 3) { + /* Right-click: toggle mute */ + audio_toggle_mute(); + panel_render_all(); + } else if (button == 4) { + /* Scroll up: increase volume */ + audio_set_volume(audio_state.volume + 5); + if (volume_slider != NULL && volume_slider->visible) { + volume_slider_render(volume_slider); + } + panel_render_all(); + } else if (button == 5) { + /* Scroll down: decrease volume */ + audio_set_volume(audio_state.volume - 5); + if (volume_slider != NULL && volume_slider->visible) { + volume_slider_render(volume_slider); + } + panel_render_all(); + } + } + + (void)y; +} + +void systray_update(void) +{ + /* State is updated asynchronously by the background thread. + * This function now only handles the WiFi menu re-scan. */ + + /* Re-scan WiFi networks periodically if menu is open */ + if (wifi_menu != NULL && wifi_menu->visible) { + long now = get_time_ms(); + pthread_mutex_lock(&state_mutex); + long last_scan = wifi_state.last_scan; + pthread_mutex_unlock(&state_mutex); + + if (now - last_scan >= WIFI_SCAN_INTERVAL) { + wifi_scan_networks(); + dropdown_render(wifi_menu); + } + } +} + +/* ========== Thread-safe access functions ========== */ + +void systray_lock(void) +{ + pthread_mutex_lock(&state_mutex); +} + +void systray_unlock(void) +{ + pthread_mutex_unlock(&state_mutex); +} + +BatteryState systray_get_battery_snapshot(void) +{ + BatteryState snapshot; + pthread_mutex_lock(&state_mutex); + snapshot = battery_state; + pthread_mutex_unlock(&state_mutex); + return snapshot; +} + +AudioState systray_get_audio_snapshot(void) +{ + AudioState snapshot; + pthread_mutex_lock(&state_mutex); + snapshot = audio_state; + pthread_mutex_unlock(&state_mutex); + return snapshot; +} diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..06cb92a --- /dev/null +++ b/src/util.c @@ -0,0 +1,709 @@ +/* + * DWN - Desktop Window Manager + * Utility functions implementation + */ + +#include "util.h" +#include "dwn.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ========== Async Logging Configuration ========== */ + +#define LOG_RING_SIZE 256 /* Number of log entries in ring buffer */ +#define LOG_MSG_MAX_LEN 512 /* Max length of a single log message */ +#define LOG_MAX_FILE_SIZE (5 * 1024 * 1024) /* 5 MB max log file size */ +#define LOG_FLUSH_INTERVAL_MS 100 /* Flush to disk every 100ms */ + +/* Log entry in ring buffer */ +typedef struct { + char message[LOG_MSG_MAX_LEN]; + LogLevel level; + _Atomic int ready; /* 0 = empty, 1 = ready to write */ +} LogEntry; + +/* Static state for async logging */ +static LogEntry log_ring[LOG_RING_SIZE]; +static _Atomic size_t log_write_idx = 0; /* Next slot to write to */ +static _Atomic size_t log_read_idx = 0; /* Next slot to read from */ +static _Atomic int log_running = 0; /* Log thread running flag */ +static pthread_t log_thread; +static int log_fd = -1; /* File descriptor for log file */ +static char *log_path = NULL; /* Path to log file */ +static LogLevel min_level = LOG_INFO; +static _Atomic size_t log_file_size = 0; /* Current log file size */ + +static const char *level_names[] = { + "DEBUG", + "INFO", + "WARN", + "ERROR" +}; + +static const char *level_colors[] = { + "\033[36m", /* Cyan for DEBUG */ + "\033[32m", /* Green for INFO */ + "\033[33m", /* Yellow for WARN */ + "\033[31m" /* Red for ERROR */ +}; + +/* Forward declarations for internal log functions */ +static void log_rotate_if_needed(void); +static void *log_writer_thread(void *arg); + +/* ========== Async Logging Implementation ========== */ + +/* Rotate log file if it exceeds max size */ +static void log_rotate_if_needed(void) +{ + if (log_fd < 0 || log_path == NULL) { + return; + } + + size_t current_size = atomic_load(&log_file_size); + if (current_size < LOG_MAX_FILE_SIZE) { + return; + } + + /* Close current file */ + close(log_fd); + log_fd = -1; + + /* Create backup filename */ + size_t path_len = strlen(log_path); + char *backup_path = malloc(path_len + 8); + if (backup_path != NULL) { + snprintf(backup_path, path_len + 8, "%s.old", log_path); + /* Remove old backup if exists, rename current to backup */ + unlink(backup_path); + rename(log_path, backup_path); + free(backup_path); + } + + /* Reopen fresh log file */ + log_fd = open(log_path, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644); + atomic_store(&log_file_size, 0); +} + +/* Background thread that writes log entries to file */ +static void *log_writer_thread(void *arg) +{ + (void)arg; + + while (atomic_load(&log_running)) { + size_t read_idx = atomic_load(&log_read_idx); + size_t write_idx = atomic_load(&log_write_idx); + + /* Process all available entries */ + while (read_idx != write_idx) { + size_t idx = read_idx % LOG_RING_SIZE; + LogEntry *entry = &log_ring[idx]; + + /* Wait for entry to be ready (spin briefly) */ + int attempts = 0; + while (!atomic_load(&entry->ready) && attempts < 100) { + attempts++; + /* Brief yield */ + struct timespec ts = {0, 1000}; /* 1 microsecond */ + nanosleep(&ts, NULL); + } + + if (atomic_load(&entry->ready)) { + /* Write to file if open */ + if (log_fd >= 0) { + log_rotate_if_needed(); + if (log_fd >= 0) { + ssize_t written = write(log_fd, entry->message, strlen(entry->message)); + if (written > 0) { + atomic_fetch_add(&log_file_size, (size_t)written); + } + } + } + + /* Mark entry as consumed */ + atomic_store(&entry->ready, 0); + } + + read_idx++; + atomic_store(&log_read_idx, read_idx); + } + + /* Sleep before next check */ + struct timespec ts = {0, LOG_FLUSH_INTERVAL_MS * 1000000L}; + nanosleep(&ts, NULL); + } + + return NULL; +} + +void log_init(const char *path) +{ + /* Initialize ring buffer */ + memset(log_ring, 0, sizeof(log_ring)); + atomic_store(&log_write_idx, 0); + atomic_store(&log_read_idx, 0); + + if (path != NULL) { + char *expanded = expand_path(path); + if (expanded != NULL) { + /* Ensure directory exists */ + char *dir = strdup(expanded); + if (dir != NULL) { + char *last_slash = strrchr(dir, '/'); + if (last_slash != NULL) { + *last_slash = '\0'; + /* Create directory recursively using mkdir */ + struct stat st; + if (stat(dir, &st) != 0) { + /* Directory doesn't exist, try to create it */ + char cmd[512]; + snprintf(cmd, sizeof(cmd), "mkdir -p '%s' 2>/dev/null", dir); + int ret = system(cmd); + (void)ret; /* Ignore result - file open will fail if dir creation fails */ + } + } + free(dir); + } + + /* Open log file with O_APPEND for atomic writes */ + log_fd = open(expanded, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644); + if (log_fd < 0) { + fprintf(stderr, "Warning: Could not open log file: %s\n", expanded); + } else { + /* Get current file size */ + struct stat st; + if (fstat(log_fd, &st) == 0) { + atomic_store(&log_file_size, (size_t)st.st_size); + } + } + + log_path = expanded; /* Keep for rotation */ + } + } + + /* Start background writer thread */ + atomic_store(&log_running, 1); + if (pthread_create(&log_thread, NULL, log_writer_thread, NULL) != 0) { + fprintf(stderr, "Warning: Could not create log writer thread\n"); + atomic_store(&log_running, 0); + } +} + +void log_close(void) +{ + /* Signal thread to stop */ + if (!atomic_load(&log_running)) { + goto cleanup; + } + atomic_store(&log_running, 0); + + /* Wait for thread to finish - give it time to flush */ + pthread_join(log_thread, NULL); + +cleanup: + /* Close file */ + if (log_fd >= 0) { + close(log_fd); + log_fd = -1; + } + + /* Free path */ + if (log_path != NULL) { + free(log_path); + log_path = NULL; + } +} + +void log_set_level(LogLevel level) +{ + min_level = level; +} + +void log_flush(void) +{ + /* Force flush all pending log entries synchronously */ + if (!atomic_load(&log_running) || log_fd < 0) { + return; + } + + size_t read_idx = atomic_load(&log_read_idx); + size_t write_idx = atomic_load(&log_write_idx); + + while (read_idx != write_idx) { + size_t idx = read_idx % LOG_RING_SIZE; + LogEntry *entry = &log_ring[idx]; + + if (atomic_load(&entry->ready)) { + ssize_t written = write(log_fd, entry->message, strlen(entry->message)); + if (written > 0) { + atomic_fetch_add(&log_file_size, (size_t)written); + } + atomic_store(&entry->ready, 0); + } + + read_idx++; + atomic_store(&log_read_idx, read_idx); + } + + /* Sync to disk */ + fsync(log_fd); +} + +void log_msg(LogLevel level, const char *fmt, ...) +{ + if (level < min_level) { + return; + } + + /* Get timestamp */ + time_t now = time(NULL); + struct tm tm_info; + localtime_r(&now, &tm_info); /* Thread-safe version */ + char time_buf[32]; + strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &tm_info); + + /* Format the log message */ + char msg_buf[LOG_MSG_MAX_LEN - 64]; /* Leave room for prefix */ + va_list args; + va_start(args, fmt); + vsnprintf(msg_buf, sizeof(msg_buf), fmt, args); + va_end(args); + + /* Print to stderr (always, for immediate visibility) */ + fprintf(stderr, "%s[%s] %s: %s%s\n", + level_colors[level], time_buf, level_names[level], "\033[0m", msg_buf); + + /* Add to ring buffer for async file write (non-blocking) */ + if (atomic_load(&log_running)) { + size_t write_idx = atomic_fetch_add(&log_write_idx, 1); + size_t idx = write_idx % LOG_RING_SIZE; + LogEntry *entry = &log_ring[idx]; + + /* Check if slot is available (not overwriting unread entry) */ + /* If buffer is full, we drop the message rather than block */ + if (!atomic_load(&entry->ready)) { + entry->level = level; + snprintf(entry->message, sizeof(entry->message), + "[%s] %s: %s\n", time_buf, level_names[level], msg_buf); + atomic_store(&entry->ready, 1); + } + /* If entry->ready is true, buffer is full - message is dropped (non-blocking) */ + } +} + +/* ========== Memory allocation ========== */ + +void *dwn_malloc(size_t size) +{ + void *ptr = malloc(size); + if (ptr == NULL && size > 0) { + LOG_ERROR("Memory allocation failed for %zu bytes", size); + exit(EXIT_FAILURE); + } + return ptr; +} + +void *dwn_calloc(size_t nmemb, size_t size) +{ + void *ptr = calloc(nmemb, size); + if (ptr == NULL && nmemb > 0 && size > 0) { + LOG_ERROR("Memory allocation failed for %zu elements", nmemb); + exit(EXIT_FAILURE); + } + return ptr; +} + +void *dwn_realloc(void *ptr, size_t size) +{ + void *new_ptr = realloc(ptr, size); + if (new_ptr == NULL && size > 0) { + LOG_ERROR("Memory reallocation failed for %zu bytes", size); + exit(EXIT_FAILURE); + } + return new_ptr; +} + +char *dwn_strdup(const char *s) +{ + if (s == NULL) { + return NULL; + } + char *dup = strdup(s); + if (dup == NULL) { + LOG_ERROR("String duplication failed"); + exit(EXIT_FAILURE); + } + return dup; +} + +void dwn_free(void *ptr) +{ + free(ptr); +} + +void secure_wipe(void *ptr, size_t size) +{ + if (ptr == NULL || size == 0) { + return; + } +#if defined(__GLIBC__) && (__GLIBC__ >= 2) && (__GLIBC_MINOR__ >= 25) + /* Use explicit_bzero if available (glibc 2.25+) */ + explicit_bzero(ptr, size); +#else + /* Fallback: Use volatile to prevent compiler optimization */ + volatile unsigned char *p = (volatile unsigned char *)ptr; + while (size--) { + *p++ = 0; + } +#endif +} + +/* ========== String utilities ========== */ + +char *str_trim(char *str) +{ + if (str == NULL) { + return NULL; + } + + /* Trim leading whitespace */ + while (isspace((unsigned char)*str)) { + str++; + } + + if (*str == '\0') { + return str; + } + + /* Trim trailing whitespace */ + char *end = str + strlen(str) - 1; + while (end > str && isspace((unsigned char)*end)) { + end--; + } + end[1] = '\0'; + + return str; +} + +bool str_starts_with(const char *str, const char *prefix) +{ + if (str == NULL || prefix == NULL) { + return false; + } + return strncmp(str, prefix, strlen(prefix)) == 0; +} + +bool str_ends_with(const char *str, const char *suffix) +{ + if (str == NULL || suffix == NULL) { + return false; + } + size_t str_len = strlen(str); + size_t suffix_len = strlen(suffix); + if (suffix_len > str_len) { + return false; + } + return strcmp(str + str_len - suffix_len, suffix) == 0; +} + +int str_split(char *str, char delim, char **parts, int max_parts) +{ + if (str == NULL || parts == NULL || max_parts <= 0) { + return 0; + } + + int count = 0; + char *start = str; + + while (*str && count < max_parts) { + if (*str == delim) { + *str = '\0'; + parts[count++] = start; + start = str + 1; + } + str++; + } + + if (count < max_parts && *start) { + parts[count++] = start; + } + + return count; +} + +char *shell_escape(const char *str) +{ + if (str == NULL) { + return dwn_strdup(""); + } + + /* Count single quotes to determine buffer size */ + size_t quotes = 0; + for (const char *p = str; *p; p++) { + if (*p == '\'') quotes++; + } + + /* Each single quote becomes '\'' (4 chars), plus 2 for surrounding quotes */ + size_t len = strlen(str) + quotes * 3 + 3; + char *escaped = dwn_malloc(len); + char *dst = escaped; + + *dst++ = '\''; + for (const char *src = str; *src; src++) { + if (*src == '\'') { + /* Close quote, add escaped quote, reopen quote: '\'' */ + *dst++ = '\''; + *dst++ = '\\'; + *dst++ = '\''; + *dst++ = '\''; + } else { + *dst++ = *src; + } + } + *dst++ = '\''; + *dst = '\0'; + + return escaped; +} + +/* ========== File utilities ========== */ + +bool file_exists(const char *path) +{ + if (path == NULL) { + return false; + } + struct stat st; + return stat(path, &st) == 0; +} + +char *file_read_all(const char *path) +{ + FILE *f = fopen(path, "r"); + if (f == NULL) { + return NULL; + } + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + char *content = dwn_malloc(size + 1); + size_t read = fread(content, 1, size, f); + content[read] = '\0'; + + fclose(f); + return content; +} + +bool file_write_all(const char *path, const char *content) +{ + FILE *f = fopen(path, "w"); + if (f == NULL) { + return false; + } + + size_t len = strlen(content); + size_t written = fwrite(content, 1, len, f); + fclose(f); + + return written == len; +} + +char *expand_path(const char *path) +{ + if (path == NULL) { + return NULL; + } + + if (path[0] == '~') { + const char *home = getenv("HOME"); + if (home == NULL) { + struct passwd *pw = getpwuid(getuid()); + if (pw != NULL) { + home = pw->pw_dir; + } + } + if (home != NULL) { + size_t len = strlen(home) + strlen(path); + char *expanded = dwn_malloc(len); + snprintf(expanded, len, "%s%s", home, path + 1); + return expanded; + } + } + + return dwn_strdup(path); +} + +/* ========== Color utilities ========== */ + +unsigned long parse_color(const char *color_str) +{ + if (color_str == NULL || dwn == NULL || dwn->display == NULL) { + return 0; + } + + XColor color, exact; + if (XAllocNamedColor(dwn->display, dwn->colormap, color_str, &color, &exact)) { + return color.pixel; + } + + /* Try parsing as hex */ + if (color_str[0] == '#') { + unsigned int r, g, b; + if (sscanf(color_str + 1, "%02x%02x%02x", &r, &g, &b) == 3) { + color.red = r << 8; + color.green = g << 8; + color.blue = b << 8; + color.flags = DoRed | DoGreen | DoBlue; + if (XAllocColor(dwn->display, dwn->colormap, &color)) { + return color.pixel; + } + } + } + + LOG_WARN("Could not parse color: %s", color_str); + return 0; +} + +void color_to_rgb(unsigned long color, int *r, int *g, int *b) +{ + XColor xc; + xc.pixel = color; + XQueryColor(dwn->display, dwn->colormap, &xc); + *r = xc.red >> 8; + *g = xc.green >> 8; + *b = xc.blue >> 8; +} + +/* ========== Time utilities ========== */ + +long get_time_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000); +} + +void sleep_ms(int ms) +{ + struct timespec ts; + ts.tv_sec = ms / 1000; + ts.tv_nsec = (ms % 1000) * 1000000L; + nanosleep(&ts, NULL); +} + +/* ========== Process utilities ========== */ + +int spawn(const char *cmd) +{ + if (cmd == NULL) { + return -1; + } + + LOG_DEBUG("Spawning: %s", cmd); + + pid_t pid = fork(); + if (pid == 0) { + /* Child process */ + setsid(); + execl("/bin/sh", "sh", "-c", cmd, NULL); + _exit(EXIT_FAILURE); + } else if (pid < 0) { + LOG_ERROR("Fork failed: %s", strerror(errno)); + return -1; + } + + /* Wait for child */ + int status; + waitpid(pid, &status, 0); + return WEXITSTATUS(status); +} + +int spawn_async(const char *cmd) +{ + if (cmd == NULL) { + return -1; + } + + LOG_DEBUG("Spawning async: %s", cmd); + + pid_t pid = fork(); + if (pid == 0) { + /* Child process */ + setsid(); + + /* Double fork to avoid zombies */ + pid_t pid2 = fork(); + if (pid2 == 0) { + execl("/bin/sh", "sh", "-c", cmd, NULL); + _exit(EXIT_FAILURE); + } + _exit(EXIT_SUCCESS); + } else if (pid < 0) { + LOG_ERROR("Fork failed: %s", strerror(errno)); + return -1; + } + + /* Wait for first child (which exits immediately) */ + int status; + waitpid(pid, &status, 0); + return 0; +} + +char *spawn_capture(const char *cmd) +{ + if (cmd == NULL) { + return NULL; + } + + LOG_DEBUG("Spawning capture: %s", cmd); + + FILE *fp = popen(cmd, "r"); + if (fp == NULL) { + LOG_ERROR("popen failed: %s", strerror(errno)); + return NULL; + } + + /* Read output */ + size_t buf_size = 1024; + size_t len = 0; + char *output = dwn_malloc(buf_size); + output[0] = '\0'; + + char line[256]; + while (fgets(line, sizeof(line), fp) != NULL) { + size_t line_len = strlen(line); + if (len + line_len + 1 > buf_size) { + buf_size *= 2; + output = dwn_realloc(output, buf_size); + } + /* Use memcpy with explicit bounds instead of strcpy */ + memcpy(output + len, line, line_len + 1); + len += line_len; + } + + int status = pclose(fp); + if (status != 0) { + LOG_DEBUG("Command exited with status %d", status); + } + + /* Trim trailing newline */ + if (len > 0 && output[len - 1] == '\n') { + output[len - 1] = '\0'; + } + + return output; +} diff --git a/src/workspace.c b/src/workspace.c new file mode 100644 index 0000000..1db163f --- /dev/null +++ b/src/workspace.c @@ -0,0 +1,483 @@ +/* + * DWN - Desktop Window Manager + * Workspace management implementation + */ + +#include "workspace.h" +#include "client.h" +#include "layout.h" +#include "atoms.h" +#include "util.h" +#include "config.h" + +#include +#include + +/* ========== Initialization ========== */ + +void workspace_init(void) +{ + if (dwn == NULL) { + return; + } + + LOG_DEBUG("Initializing %d workspaces", MAX_WORKSPACES); + + for (int i = 0; i < MAX_WORKSPACES; i++) { + Workspace *ws = &dwn->workspaces[i]; + + ws->clients = NULL; + ws->focused = NULL; + ws->layout = (dwn->config != NULL) ? + dwn->config->default_layout : LAYOUT_TILING; + ws->master_ratio = (dwn->config != NULL) ? + dwn->config->default_master_ratio : 0.55f; + ws->master_count = (dwn->config != NULL) ? + dwn->config->default_master_count : 1; + + /* Default names: "1", "2", ..., "9" */ + snprintf(ws->name, sizeof(ws->name), "%d", i + 1); + } + + dwn->current_workspace = 0; +} + +void workspace_cleanup(void) +{ + /* Clients are cleaned up separately */ +} + +/* ========== Workspace access ========== */ + +Workspace *workspace_get(int index) +{ + if (dwn == NULL || index < 0 || index >= MAX_WORKSPACES) { + return NULL; + } + return &dwn->workspaces[index]; +} + +Workspace *workspace_get_current(void) +{ + return workspace_get(dwn->current_workspace); +} + +int workspace_get_current_index(void) +{ + return dwn != NULL ? dwn->current_workspace : 0; +} + +/* ========== Workspace switching ========== */ + +void workspace_switch(int index) +{ + if (dwn == NULL || index < 0 || index >= MAX_WORKSPACES) { + return; + } + + if (index == dwn->current_workspace) { + return; + } + + LOG_DEBUG("Switching from workspace %d to %d", + dwn->current_workspace + 1, index + 1); + + /* Grab server to batch all X operations - prevents flickering and improves speed */ + XGrabServer(dwn->display); + + /* Hide current workspace windows */ + workspace_hide(dwn->current_workspace); + + /* Update current workspace */ + int old_workspace = dwn->current_workspace; + dwn->current_workspace = index; + + /* Show new workspace windows */ + workspace_show(index); + + /* Arrange windows on new workspace */ + workspace_arrange(index); + + /* Focus appropriate window */ + Workspace *ws = workspace_get(index); + if (ws != NULL) { + if (ws->focused != NULL) { + client_focus(ws->focused); + } else { + Client *first = workspace_get_first_client(index); + if (first != NULL) { + client_focus(first); + } else { + /* No windows, focus root */ + XSetInputFocus(dwn->display, dwn->root, RevertToPointerRoot, CurrentTime); + atoms_set_active_window(None); + } + } + } + + /* Update EWMH */ + atoms_set_current_desktop(index); + + /* Release server and sync - all changes appear atomically */ + XUngrabServer(dwn->display); + XSync(dwn->display, False); + + (void)old_workspace; +} + +void workspace_switch_next(void) +{ + int next = (dwn->current_workspace + 1) % MAX_WORKSPACES; + workspace_switch(next); +} + +void workspace_switch_prev(void) +{ + int prev = (dwn->current_workspace - 1 + MAX_WORKSPACES) % MAX_WORKSPACES; + workspace_switch(prev); +} + +/* ========== Client management ========== */ + +void workspace_add_client(int workspace, Client *client) +{ + if (client == NULL || workspace < 0 || workspace >= MAX_WORKSPACES) { + return; + } + + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + /* Add to workspace client list (at head) */ + client->workspace = workspace; + + /* We don't maintain a separate linked list per workspace, + just use the client's workspace field */ +} + +void workspace_remove_client(int workspace, Client *client) +{ + if (client == NULL || workspace < 0 || workspace >= MAX_WORKSPACES) { + return; + } + + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + /* If this was the focused client, clear it */ + if (ws->focused == client) { + ws->focused = NULL; + } +} + +void workspace_move_client(Client *client, int new_workspace) +{ + if (client == NULL || new_workspace < 0 || new_workspace >= MAX_WORKSPACES) { + return; + } + + if (client->workspace == (unsigned int)new_workspace) { + return; + } + + int old_workspace = client->workspace; + + LOG_DEBUG("Moving window '%s' from workspace %d to %d", + client->title, old_workspace + 1, new_workspace + 1); + + /* Grab server to batch all X operations */ + XGrabServer(dwn->display); + + /* Remove from old workspace */ + workspace_remove_client(old_workspace, client); + + /* Add to new workspace */ + workspace_add_client(new_workspace, client); + + /* Update EWMH */ + atoms_set_window_desktop(client->window, new_workspace); + + /* Hide if moving to non-current workspace */ + if (new_workspace != dwn->current_workspace) { + client_hide(client); + } else { + client_show(client); + } + + /* Rearrange both workspaces */ + workspace_arrange(old_workspace); + workspace_arrange(new_workspace); + + /* Release server and sync */ + XUngrabServer(dwn->display); + XSync(dwn->display, False); +} + +Client *workspace_get_first_client(int workspace) +{ + if (workspace < 0 || workspace >= MAX_WORKSPACES) { + return NULL; + } + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)workspace && !client_is_minimized(c)) { + return c; + } + } + + return NULL; +} + +Client *workspace_get_focused_client(int workspace) +{ + Workspace *ws = workspace_get(workspace); + return ws != NULL ? ws->focused : NULL; +} + +/* ========== Layout ========== */ + +void workspace_set_layout(int workspace, LayoutType layout) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + ws->layout = layout; + + /* Batch the arrangement */ + XGrabServer(dwn->display); + workspace_arrange(workspace); + XUngrabServer(dwn->display); + XSync(dwn->display, False); + + LOG_DEBUG("Workspace %d layout set to %d", workspace + 1, layout); +} + +LayoutType workspace_get_layout(int workspace) +{ + Workspace *ws = workspace_get(workspace); + return ws != NULL ? ws->layout : LAYOUT_TILING; +} + +void workspace_cycle_layout(int workspace) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + ws->layout = (ws->layout + 1) % LAYOUT_COUNT; + + /* Batch the arrangement for smoother transition */ + XGrabServer(dwn->display); + workspace_arrange(workspace); + XUngrabServer(dwn->display); + XSync(dwn->display, False); + + const char *layout_names[] = { "Tiling", "Floating", "Monocle" }; + LOG_INFO("Workspace %d: %s layout", workspace + 1, layout_names[ws->layout]); +} + +void workspace_set_master_ratio(int workspace, float ratio) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + /* Clamp ratio */ + if (ratio < 0.1f) ratio = 0.1f; + if (ratio > 0.9f) ratio = 0.9f; + + ws->master_ratio = ratio; + + /* Batch the arrangement */ + XGrabServer(dwn->display); + workspace_arrange(workspace); + XUngrabServer(dwn->display); + XSync(dwn->display, False); +} + +void workspace_adjust_master_ratio(int workspace, float delta) +{ + Workspace *ws = workspace_get(workspace); + if (ws != NULL) { + workspace_set_master_ratio(workspace, ws->master_ratio + delta); + } +} + +void workspace_set_master_count(int workspace, int count) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + if (count < 1) count = 1; + ws->master_count = count; + + /* Batch the arrangement */ + XGrabServer(dwn->display); + workspace_arrange(workspace); + XUngrabServer(dwn->display); + XSync(dwn->display, False); +} + +void workspace_adjust_master_count(int workspace, int delta) +{ + Workspace *ws = workspace_get(workspace); + if (ws != NULL) { + workspace_set_master_count(workspace, ws->master_count + delta); + } +} + +/* ========== Arrangement ========== */ + +void workspace_arrange(int workspace) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + /* Only arrange visible workspace */ + if (workspace != dwn->current_workspace) { + return; + } + + layout_arrange(workspace); +} + +void workspace_arrange_current(void) +{ + /* Batch the arrangement */ + XGrabServer(dwn->display); + workspace_arrange(dwn->current_workspace); + XUngrabServer(dwn->display); + XSync(dwn->display, False); +} + +/* ========== Visibility ========== */ + +void workspace_show(int workspace) +{ + if (workspace < 0 || workspace >= MAX_WORKSPACES) { + return; + } + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)workspace && !client_is_minimized(c)) { + client_show(c); + } + } +} + +void workspace_hide(int workspace) +{ + if (workspace < 0 || workspace >= MAX_WORKSPACES) { + return; + } + + for (Client *c = dwn->client_list; c != NULL; c = c->next) { + if (c->workspace == (unsigned int)workspace) { + client_hide(c); + } + } +} + +/* ========== Properties ========== */ + +void workspace_set_name(int workspace, const char *name) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL || name == NULL) { + return; + } + + strncpy(ws->name, name, sizeof(ws->name) - 1); + ws->name[sizeof(ws->name) - 1] = '\0'; + + atoms_update_desktop_names(); +} + +const char *workspace_get_name(int workspace) +{ + Workspace *ws = workspace_get(workspace); + return ws != NULL ? ws->name : ""; +} + +int workspace_client_count(int workspace) +{ + return client_count_on_workspace(workspace); +} + +bool workspace_is_empty(int workspace) +{ + return workspace_client_count(workspace) == 0; +} + +/* ========== Focus cycling ========== */ + +void workspace_focus_next(void) +{ + Workspace *ws = workspace_get_current(); + if (ws == NULL || ws->focused == NULL) { + return; + } + + /* Find next client on same workspace */ + Client *next = ws->focused->next; + while (next != NULL && next->workspace != (unsigned int)dwn->current_workspace) { + next = next->next; + } + + /* Wrap around */ + if (next == NULL) { + next = workspace_get_first_client(dwn->current_workspace); + } + + if (next != NULL && next != ws->focused) { + client_focus(next); + } +} + +void workspace_focus_prev(void) +{ + Workspace *ws = workspace_get_current(); + if (ws == NULL || ws->focused == NULL) { + return; + } + + /* Find previous client on same workspace */ + Client *prev = ws->focused->prev; + while (prev != NULL && prev->workspace != (unsigned int)dwn->current_workspace) { + prev = prev->prev; + } + + /* Wrap around */ + if (prev == NULL) { + for (Client *c = client_get_last(); c != NULL; c = c->prev) { + if (c->workspace == (unsigned int)dwn->current_workspace) { + prev = c; + break; + } + } + } + + if (prev != NULL && prev != ws->focused) { + client_focus(prev); + } +} + +void workspace_focus_master(void) +{ + Client *master = workspace_get_first_client(dwn->current_workspace); + if (master != NULL) { + client_focus(master); + } +}