1#define _POSIX_C_SOURCE 200809L
2#include <stdlib.h>
3#include <time.h>
4#include <string.h>
5
6#include <X11/Xlib.h>
7#include <X11/Xatom.h>
8#include <X11/keysym.h>
9#include <X11/XF86keysym.h>
10#include <X11/cursorfont.h>
11#include <X11/Xproto.h>
12
13#include "glitch.h"
14#include "config.h"
15
16extern WindowManager wm;
17
18Atom _NET_WM_DESKTOP;
19static Atom _NET_CURRENT_DESKTOP;
20static Atom _NET_NUMBER_OF_DESKTOPS;
21static Atom _NET_CLIENT_LIST;
22static Atom _NET_WM_STATE;
23static Atom _NET_WM_NAME;
24static Atom _NET_SUPPORTING_WM_CHECK;
25static Atom _NET_WM_STATE_FULLSCREEN;
26static Atom _NET_ACTIVE_WINDOW;
27static Atom _NET_WM_STATE_STICKY;
28static Atom _NET_WM_STATE_MAXIMIZED_HORZ;
29static Atom _NET_WM_STATE_MAXIMIZED_VERT;
30static Atom _NET_WM_STATE_ABOVE;
31static Atom _GLITCH_PRE_HMAX_GEOM;
32static Atom _GLITCH_PRE_VMAX_GEOM;
33static Atom _GLITCH_PRE_FULLSCREEN_GEOM;
34static Atom _MOTIF_WM_HINTS;
35static Atom WM_PROTOCOLS;
36static Atom WM_DELETE_WINDOW;
37static Atom WM_TAKE_FOCUS;
38static Atom _NET_SUPPORTED;
39static Atom _NET_FRAME_EXTENTS;
40static Atom WM_STATE_ATOM;
41
42static void update_window_border(Window window, int active) {
43 if (window == None) return;
44 if (!window_exists(window)) return;
45
46 Atom actual_type;
47 int actual_format;
48 unsigned long nitems, bytes_after;
49 unsigned char *prop = NULL;
50 int has_decorations = 1;
51
52 // Check _MOTIF_WM_HINTS to see if the window requested no decorations
53 XErrorHandler old = XSetErrorHandler(ignore_x_error);
54 int status = XGetWindowProperty(wm.dpy, window, _MOTIF_WM_HINTS, 0, 5, False, AnyPropertyType,
55 &actual_type, &actual_format, &nitems, &bytes_after, &prop);
56 XSync(wm.dpy, False);
57 XSetErrorHandler(old);
58
59 if (status == Success) {
60 if (prop && nitems >= 3) {
61 unsigned long flags = ((unsigned long *)prop)[0];
62 unsigned long decorations = ((unsigned long *)prop)[2];
63 // If flags bit 1 is set (MWM_HINTS_DECORATIONS) and decorations bit 0 is cleared (MWM_DECOR_ALL/BORDER), then no border.
64 // Simplification: if decorations is 0, assume no border.
65 if ((flags & 2) && (decorations & 1) == 0) {
66 has_decorations = 0;
67 }
68 }
69 if (prop) XFree(prop);
70 }
71
72 unsigned int bw = has_decorations ? border_size : 0;
73 XSetWindowBorderWidth(wm.dpy, window, bw);
74
75 if (active) {
76 if (is_always_on_top(window)) {
77 XSetWindowBorder(wm.dpy, window, wm.borders.on_top_active);
78 } else if (is_sticky(window)) {
79 XSetWindowBorder(wm.dpy, window, wm.borders.sticky_active);
80 } else {
81 XSetWindowBorder(wm.dpy, window, wm.borders.normal_active);
82 }
83 } else {
84 if (is_always_on_top(window)) {
85 XSetWindowBorder(wm.dpy, window, wm.borders.on_top_inactive);
86 } else if (is_sticky(window)) {
87 XSetWindowBorder(wm.dpy, window, wm.borders.sticky_inactive);
88 } else {
89 XSetWindowBorder(wm.dpy, window, wm.borders.normal_inactive);
90 }
91 }
92}
93
94static void update_wm_state(Window w, Atom state_atom, int add);
95static int has_wm_state(Window w, Atom state_atom);
96static void check_and_clear_maximized_state(Window w, int horizontal, int vertical);
97static void add_client(Window w);
98static void remove_client(Window w);
99static Window get_toplevel_window(Window w);
100static void set_fullscreen(Window w, int full);
101static void send_configure(Window w);
102static Client *wintoclient(Window w);
103
104int x_error_handler(Display *dpy, XErrorEvent *ee) {
105 (void) dpy;
106
107 if (ee->error_code == BadWindow ||
108 (ee->request_code == X_SetInputFocus && ee->error_code == BadMatch) ||
109 (ee->request_code == X_PolyText8 && ee->error_code == BadDrawable) ||
110 (ee->request_code == X_PolyFillRectangle && ee->error_code == BadDrawable) ||
111 (ee->request_code == X_PolySegment && ee->error_code == BadDrawable) ||
112 (ee->request_code == X_ConfigureWindow && ee->error_code == BadMatch) ||
113 (ee->request_code == X_GrabButton && ee->error_code == BadAccess) ||
114 (ee->request_code == X_GrabKey && ee->error_code == BadAccess) ||
115 (ee->request_code == X_CopyArea && ee->error_code == BadDrawable)) {
116 return 0;
117 }
118 log_message(stderr, LOG_ERROR, "Fatal X Error: request_code=%d, error_code=%d, resource_id=0x%lx", ee->request_code, ee->error_code, ee->resourceid);
119 return 0;
120}
121
122static void set_client_state(Window w, long state) {
123 long data[] = { state, None };
124 XChangeProperty(wm.dpy, w, WM_STATE_ATOM, WM_STATE_ATOM, 32, PropModeReplace, (unsigned char *)data, 2);
125}
126
127static void add_client(Window w) {
128 // Check if already in list or is root.
129 if (w == wm.root) return;
130 Client *c = wm.clients;
131 while (c) {
132 if (c->window == w) return;
133 c = c->next;
134 }
135
136 Client *new_c = malloc(sizeof(Client));
137 if (!new_c) return;
138
139 new_c->window = w;
140 new_c->next = wm.clients;
141 new_c->prev = NULL;
142 new_c->saved_x = 0;
143 new_c->saved_y = 0;
144 new_c->saved_w = 0;
145 new_c->saved_h = 0;
146 new_c->has_saved_state = 0;
147
148 if (wm.clients) {
149 wm.clients->prev = new_c;
150 }
151 wm.clients = new_c;
152 log_message(stdout, LOG_DEBUG, "Added client 0x%lx", w);
153
154 unsigned long extents[4] = {0, 0, 0, 0};
155 XChangeProperty(wm.dpy, w, _NET_FRAME_EXTENTS, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)extents, 4);
156 set_client_state(w, NormalState);
157}
158
159static void remove_client(Window w) {
160 Client *c = wm.clients;
161 while (c) {
162 if (c->window == w) {
163 if (c->prev) {
164 c->prev->next = c->next;
165 } else {
166 wm.clients = c->next;
167 }
168
169 if (c->next) {
170 c->next->prev = c->prev;
171 }
172
173 set_client_state(w, WithdrawnState);
174 free(c);
175 log_message(stdout, LOG_DEBUG, "Removed client 0x%lx", w);
176 return;
177 }
178 c = c->next;
179 }
180}
181
182static Window get_toplevel_window(Window w) {
183 if (w == None || w == wm.root) return None;
184
185 Client *c = wm.clients;
186 while (c) {
187 if (c->window == w) return w;
188 c = c->next;
189 }
190
191 Window root, parent, *children;
192 unsigned int nchildren;
193 if (XQueryTree(wm.dpy, w, &root, &parent, &children, &nchildren)) {
194 if (children) XFree(children);
195 if (parent == root || parent == None) return None;
196 return get_toplevel_window(parent);
197 }
198
199 return None;
200}
201
202static void scan_windows(void) {
203 unsigned int nwins;
204 Window d1, d2, *wins;
205 XWindowAttributes wa;
206
207 if (XQueryTree(wm.dpy, wm.root, &d1, &d2, &wins, &nwins)) {
208 for (unsigned int i = 0; i < nwins; i++) {
209 if (XGetWindowAttributes(wm.dpy, wins[i], &wa)
210 && !wa.override_redirect && (wa.map_state == IsViewable || wa.map_state == IsUnmapped)) {
211 add_client(wins[i]);
212 XSelectInput(wm.dpy, wins[i], EnterWindowMask | LeaveWindowMask);
213 grab_buttons(wins[i]);
214 update_window_border(wins[i], 0);
215 }
216 }
217 if (wins) XFree(wins);
218 }
219
220 // Restore focus.
221 Window focus_win;
222 int revert_to;
223 XGetInputFocus(wm.dpy, &focus_win, &revert_to);
224 if (focus_win != None && focus_win != wm.root) {
225 Window toplevel = get_toplevel_window(focus_win);
226 if (toplevel != None && window_exists(toplevel)) {
227 set_active_window(toplevel, CurrentTime);
228 set_active_border(toplevel);
229 }
230 }
231}
232
233void init_window_manager(void) {
234 wm.dpy = XOpenDisplay(NULL);
235 if (!wm.dpy) {
236 log_message(stdout, LOG_ERROR, "Cannot open display");
237 abort();
238 }
239
240 XSetErrorHandler(x_error_handler);
241
242 wm.screen = DefaultScreen(wm.dpy);
243 wm.root = RootWindow(wm.dpy, wm.screen);
244 XSetWindowBackground(wm.dpy, wm.root, BlackPixel(wm.dpy, wm.screen));
245 XClearWindow(wm.dpy, wm.root);
246
247 // Create and sets up cursors.
248 wm.cursor_default = XCreateFontCursor(wm.dpy, XC_left_ptr);
249 wm.cursor_move = XCreateFontCursor(wm.dpy, XC_fleur);
250 wm.cursor_resize = XCreateFontCursor(wm.dpy, XC_sizing);
251 XDefineCursor(wm.dpy, wm.root, wm.cursor_default);
252 log_message(stdout, LOG_DEBUG, "Setting up default cursors");
253
254 // Root window input selection masks.
255 XSelectInput(wm.dpy, wm.root,
256 SubstructureRedirectMask | SubstructureNotifyMask |
257 FocusChangeMask | EnterWindowMask | LeaveWindowMask |
258 ButtonPressMask | ExposureMask | PropertyChangeMask);
259
260 // Initialize EWMH atoms.
261 _NET_WM_DESKTOP = XInternAtom(wm.dpy, "_NET_WM_DESKTOP", False);
262 _NET_CURRENT_DESKTOP = XInternAtom(wm.dpy, "_NET_CURRENT_DESKTOP", False);
263 _NET_NUMBER_OF_DESKTOPS = XInternAtom(wm.dpy, "_NET_NUMBER_OF_DESKTOPS", False);
264 _NET_CLIENT_LIST = XInternAtom(wm.dpy, "_NET_CLIENT_LIST", False);
265 _NET_WM_STATE = XInternAtom(wm.dpy, "_NET_WM_STATE", False);
266 _NET_WM_STATE_FULLSCREEN = XInternAtom(wm.dpy, "_NET_WM_STATE_FULLSCREEN", False);
267 _NET_ACTIVE_WINDOW = XInternAtom(wm.dpy, "_NET_ACTIVE_WINDOW", False);
268 _NET_WM_STATE_STICKY = XInternAtom(wm.dpy, "_NET_WM_STATE_STICKY", False);
269 _NET_WM_STATE_MAXIMIZED_HORZ = XInternAtom(wm.dpy, "_NET_WM_STATE_MAXIMIZED_HORZ", False);
270 _NET_WM_STATE_MAXIMIZED_VERT = XInternAtom(wm.dpy, "_NET_WM_STATE_MAXIMIZED_VERT", False);
271 _NET_WM_STATE_ABOVE = XInternAtom(wm.dpy, "_NET_WM_STATE_ABOVE", False);
272 _NET_SUPPORTED = XInternAtom(wm.dpy, "_NET_SUPPORTED", False);
273 _NET_SUPPORTING_WM_CHECK = XInternAtom(wm.dpy, "_NET_SUPPORTING_WM_CHECK", False);
274 _NET_WM_NAME = XInternAtom(wm.dpy, "_NET_WM_NAME", False);
275 _NET_FRAME_EXTENTS = XInternAtom(wm.dpy, "_NET_FRAME_EXTENTS", False);
276 WM_STATE_ATOM = XInternAtom(wm.dpy, "WM_STATE", False);
277
278 // Create supporting window for EWMH compliance.
279 XSetWindowAttributes wa;
280 wa.override_redirect = True;
281 Window check_win = XCreateWindow(wm.dpy, wm.root, -1, -1, 1, 1, 0,
282 CopyFromParent, InputOutput, CopyFromParent,
283 CWOverrideRedirect, &wa);
284 XMapWindow(wm.dpy, check_win);
285 XChangeProperty(wm.dpy, check_win, _NET_SUPPORTING_WM_CHECK, XA_WINDOW, 32, PropModeReplace, (unsigned char *)&check_win, 1);
286 XChangeProperty(wm.dpy, check_win, _NET_WM_NAME, XA_STRING, 8, PropModeReplace, (unsigned char *)"LG3D", 4);
287 XChangeProperty(wm.dpy, wm.root, _NET_SUPPORTING_WM_CHECK, XA_WINDOW, 32, PropModeReplace, (unsigned char *)&check_win, 1);
288 XChangeProperty(wm.dpy, wm.root, _NET_WM_NAME, XA_STRING, 8, PropModeReplace, (unsigned char *)"LG3D", 4);
289
290 // Set supported atoms.
291 Atom net_atoms[] = {
292 _NET_SUPPORTED,
293 _NET_SUPPORTING_WM_CHECK,
294 _NET_WM_NAME,
295 _NET_FRAME_EXTENTS,
296 _NET_WM_DESKTOP,
297 _NET_CURRENT_DESKTOP,
298 _NET_NUMBER_OF_DESKTOPS,
299 _NET_CLIENT_LIST,
300 _NET_WM_STATE,
301 _NET_WM_STATE_FULLSCREEN,
302 _NET_ACTIVE_WINDOW,
303 _NET_WM_STATE_STICKY,
304 _NET_WM_STATE_MAXIMIZED_HORZ,
305 _NET_WM_STATE_MAXIMIZED_VERT,
306 _NET_WM_STATE_ABOVE,
307 WM_STATE_ATOM,
308 WM_DELETE_WINDOW,
309 WM_TAKE_FOCUS
310 };
311 XChangeProperty(wm.dpy, wm.root, _NET_SUPPORTED, XA_ATOM, 32, PropModeReplace, (unsigned char *)net_atoms, LENGTH(net_atoms));
312
313 // Set number of desktops and current desktop.
314 static unsigned long num_desktops = NUM_DESKTOPS;
315 static unsigned long current_desktop = 1;
316 wm.current_desktop = 1;
317 XChangeProperty(wm.dpy, wm.root, _NET_NUMBER_OF_DESKTOPS, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&num_desktops, 1);
318 XChangeProperty(wm.dpy, wm.root, _NET_CURRENT_DESKTOP, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)¤t_desktop, 1);
319 log_message(stdout, LOG_DEBUG, "Registering %d desktops", NUM_DESKTOPS);
320
321 // Initialize layout modes.
322 for (int i = 0; i <= NUM_DESKTOPS; i++) {
323 wm.layout_modes[i] = LAYOUT_FLOATING;
324 }
325
326 // Initialize colormap early as it's needed for Xft.
327 wm.cmap = DefaultColormap(wm.dpy, wm.screen);
328
329 // Setup Xft font and drawing.
330 wm.font = XftFontOpenName(wm.dpy, wm.screen, widget_font);
331 if (!wm.font) {
332 log_message(stdout, LOG_WARNING, "Failed to load font %s, falling back to fixed", widget_font);
333 wm.font = XftFontOpenName(wm.dpy, wm.screen, "fixed");
334 }
335
336 wm.launcher_font = XftFontOpenName(wm.dpy, wm.screen, launcher_font_name);
337 if (!wm.launcher_font) {
338 log_message(stdout, LOG_WARNING, "Failed to load launcher font %s, falling back to fixed", launcher_font_name);
339 wm.launcher_font = XftFontOpenName(wm.dpy, wm.screen, "fixed");
340 }
341
342 Visual *visual = DefaultVisual(wm.dpy, wm.screen);
343
344 // Create XftDraw for the root window.
345 wm.xft_draw = XftDrawCreate(wm.dpy, wm.root, visual, wm.cmap);
346
347 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, indicator_fg_color, &wm.xft_color)) {
348 log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to white", indicator_fg_color);
349 XRenderColor render_color = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
350 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_color);
351 }
352
353 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, indicator_bg_color, &wm.xft_bg_color)) {
354 log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to black", indicator_bg_color);
355 XRenderColor render_color = {0x0000, 0x0000, 0x0000, 0xFFFF};
356 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_bg_color);
357 }
358
359 // Root background color (black) for widgets to blend in.
360 XRenderColor black_render = {0x0000, 0x0000, 0x0000, 0xFFFF};
361 XftColorAllocValue(wm.dpy, visual, wm.cmap, &black_render, &wm.xft_root_bg_color);
362
363 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, mic_active_bg_color, &wm.xft_mic_active_bg)) {
364 log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to orange", mic_active_bg_color);
365 XRenderColor render_color = {0xFFFF, 0x8000, 0x0000, 0xFFFF};
366 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_mic_active_bg);
367 }
368
369 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, mic_muted_bg_color, &wm.xft_mic_muted_bg)) {
370 log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to dark gray", mic_muted_bg_color);
371 XRenderColor render_color = {0x4444, 0x4444, 0x4444, 0xFFFF};
372 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_mic_muted_bg);
373 }
374
375 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, mic_active_fg_color, &wm.xft_mic_active_fg)) {
376 log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to white", mic_active_fg_color);
377 XRenderColor render_color = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
378 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_mic_active_fg);
379 }
380
381 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, mic_muted_fg_color, &wm.xft_mic_muted_fg)) {
382 log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to white", mic_muted_fg_color);
383 XRenderColor render_color = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
384 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_mic_muted_fg);
385 }
386
387 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, layout_tile_bg_color, &wm.xft_layout_tile_bg)) {
388 log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to dark green", layout_tile_bg_color);
389 XRenderColor render_color = {0x0000, 0x6400, 0x0000, 0xFFFF};
390 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_layout_tile_bg);
391 }
392
393 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, layout_float_bg_color, &wm.xft_layout_float_bg)) {
394 log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to gray", layout_float_bg_color);
395 XRenderColor render_color = {0x3333, 0x3333, 0x3333, 0xFFFF};
396 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_layout_float_bg);
397 }
398
399 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, layout_tile_fg_color, &wm.xft_layout_tile_fg)) {
400 log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to white", layout_tile_fg_color);
401 XRenderColor render_color = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
402 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_layout_tile_fg);
403 }
404
405 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, layout_float_fg_color, &wm.xft_layout_float_fg)) {
406 log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to white", layout_float_fg_color);
407 XRenderColor render_color = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
408 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_layout_float_fg);
409 }
410
411 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, launcher_bg_color, &wm.xft_launcher_bg)) {
412 XRenderColor render_color = {0x0000, 0x0000, 0x0000, 0xFFFF};
413 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_launcher_bg);
414 }
415 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, launcher_border_color, &wm.xft_launcher_border)) {
416 XRenderColor render_color = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
417 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_launcher_border);
418 }
419 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, launcher_fg_color, &wm.xft_launcher_fg)) {
420 XRenderColor render_color = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
421 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_launcher_fg);
422 }
423 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, launcher_hl_bg_color, &wm.xft_launcher_hl_bg)) {
424 XRenderColor render_color = {0x8000, 0x8000, 0x0000, 0xFFFF};
425 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_launcher_hl_bg);
426 }
427 if (!XftColorAllocName(wm.dpy, visual, wm.cmap, launcher_hl_fg_color, &wm.xft_launcher_hl_fg)) {
428 XRenderColor render_color = {0x0000, 0x0000, 0x0000, 0xFFFF};
429 XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_launcher_hl_fg);
430 }
431
432 wm.running = 1;
433
434 // Grab keys for keybinds.
435 for (unsigned int i = 0; i < LENGTH(keybinds); i++) {
436 KeyCode keycode = XKeysymToKeycode(wm.dpy, keybinds[i].keysym);
437 if (keycode) {
438 XGrabKey(wm.dpy, keycode, keybinds[i].mod, wm.root, True, GrabModeAsync, GrabModeAsync);
439 log_message(stdout, LOG_DEBUG, "Grabbed key: mod=0x%x, keysym=0x%lx", keybinds[i].mod, keybinds[i].keysym);
440 }
441 }
442
443 // Grab keys for shortcuts.
444 for (unsigned int i = 0; i < LENGTH(shortcuts); i++) {
445 KeyCode keycode = XKeysymToKeycode(wm.dpy, shortcuts[i].keysym);
446 if (keycode) {
447 XGrabKey(wm.dpy, keycode, shortcuts[i].mod, wm.root, True, GrabModeAsync, GrabModeAsync);
448 log_message(stdout, LOG_DEBUG, "Grabbed shortcut: mod=0x%x, keysym=0x%lx, command=%s", shortcuts[i].mod, shortcuts[i].keysym, shortcuts[i].cmd);
449 }
450 }
451
452 // Grab keys for window dragging (with MODKEY).
453 XGrabButton(wm.dpy, 1, MODKEY, wm.root, True, ButtonPressMask|ButtonReleaseMask|PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None);
454 XGrabButton(wm.dpy, 3, MODKEY, wm.root, True, ButtonPressMask|ButtonReleaseMask|PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None);
455 log_message(stdout, LOG_DEBUG, "Registering grab keys for window dragging");
456
457 // Prepare border colors.
458 XColor active_color, inactive_color, sticky_active_color, sticky_inactive_color, dummy;
459
460 wm.borders.normal_active = BlackPixel(wm.dpy, wm.screen);
461 wm.borders.normal_inactive = BlackPixel(wm.dpy, wm.screen);
462 wm.borders.sticky_active = BlackPixel(wm.dpy, wm.screen);
463 wm.borders.sticky_inactive = BlackPixel(wm.dpy, wm.screen);
464
465 if (XAllocNamedColor(wm.dpy, wm.cmap, active_border_color, &active_color, &dummy)) {
466 wm.borders.normal_active = active_color.pixel;
467 }
468
469 if (XAllocNamedColor(wm.dpy, wm.cmap, inactive_border_color, &inactive_color, &dummy)) {
470 wm.borders.normal_inactive = inactive_color.pixel;
471 }
472
473 if (XAllocNamedColor(wm.dpy, wm.cmap, sticky_active_border_color, &sticky_active_color, &dummy)) {
474 wm.borders.sticky_active = sticky_active_color.pixel;
475 }
476
477 if (XAllocNamedColor(wm.dpy, wm.cmap, sticky_inactive_border_color, &sticky_inactive_color, &dummy)) {
478 wm.borders.sticky_inactive = sticky_inactive_color.pixel;
479 }
480
481 XColor on_top_active, on_top_inactive;
482 wm.borders.on_top_active = BlackPixel(wm.dpy, wm.screen);
483 wm.borders.on_top_inactive = BlackPixel(wm.dpy, wm.screen);
484
485 if (XAllocNamedColor(wm.dpy, wm.cmap, on_top_active_border_color, &on_top_active, &dummy)) {
486 wm.borders.on_top_active = on_top_active.pixel;
487 }
488 if (XAllocNamedColor(wm.dpy, wm.cmap, on_top_inactive_border_color, &on_top_inactive, &dummy)) {
489 wm.borders.on_top_inactive = on_top_inactive.pixel;
490 }
491
492 // Scan for existing windows and apply initial layout.
493 scan_windows();
494 apply_tiling_layout();
495
496 redraw_widgets();
497 update_client_list();
498 init_audio();
499 XSync(wm.dpy, False);
500}
501
502void execute_shortcut(const char *command) {
503 if (!command || strlen(command) == 0) {
504 log_message(stdout, LOG_WARNING, "Empty command provided to execute_shortcut");
505 return;
506 }
507
508 pid_t pid = fork();
509 if (pid == -1) {
510 log_message(stdout, LOG_ERROR, "Failed to fork process for command: %s", command);
511 return;
512 }
513
514 if (pid == 0) {
515 if (wm.dpy) close(ConnectionNumber(wm.dpy));
516 setsid();
517 execl("/bin/sh", "sh", "-c", command, (char *)NULL);
518 log_message(stderr, LOG_ERROR, "Failed to execute command: %s", command);
519 exit(1);
520 } else {
521 log_message(stdout, LOG_DEBUG, "Executed command in background: %s", command);
522 }
523}
524
525void deinit_window_manager(void) {
526 deinit_audio();
527 XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_color);
528 XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_bg_color);
529 XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_root_bg_color);
530 XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_mic_active_bg);
531 XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_mic_muted_bg);
532 XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_mic_active_fg);
533 XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_mic_muted_fg);
534
535 XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_launcher_bg);
536 XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_launcher_border);
537 XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_launcher_fg);
538 XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_launcher_hl_bg);
539 XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_launcher_hl_fg);
540
541 XftDrawDestroy(wm.xft_draw);
542
543 if (wm.launcher_win) XDestroyWindow(wm.dpy, wm.launcher_win);
544 if (wm.launcher_items) {
545 for (int i = 0; i < wm.launcher_items_count; i++) {
546 free(wm.launcher_items[i].name);
547 free(wm.launcher_items[i].exec);
548 }
549 free(wm.launcher_items);
550 }
551 if (wm.launcher_filtered) free(wm.launcher_filtered);
552
553 XftFontClose(wm.dpy, wm.font);
554 XftFontClose(wm.dpy, wm.launcher_font);
555 XFreeCursor(wm.dpy, wm.cursor_default);
556 XFreeCursor(wm.dpy, wm.cursor_move);
557 XFreeCursor(wm.dpy, wm.cursor_resize);
558}
559
560int is_always_on_top(Window window) {
561 if (window == None) return 0;
562 return has_wm_state(window, _NET_WM_STATE_ABOVE);
563}
564
565void raise_window(Window window) {
566 if (window == None) return;
567 if (!window_exists(window)) return;
568
569 // If the window is already always-on-top, just raise it to the absolute top.
570 if (is_always_on_top(window)) {
571 XRaiseWindow(wm.dpy, window);
572 return;
573 }
574
575 // Otherwise, find the lowest "always-on-top" window and stack this window just below it.
576 // If no "always-on-top" window exists, raise to top.
577 Window root_return, parent_return, *children;
578 unsigned int nchildren;
579 if (XQueryTree(wm.dpy, wm.root, &root_return, &parent_return, &children, &nchildren)) {
580 // Traverse from bottom to top.
581 for (unsigned int i = 0; i < nchildren; i++) {
582 if (children[i] == window) continue;
583
584 if (is_always_on_top(children[i])) {
585 // Found the first (lowest) always-on-top window.
586 // Stack 'window' below 'children[i]'.
587 XWindowChanges changes;
588 changes.sibling = children[i];
589 changes.stack_mode = Below;
590 XConfigureWindow(wm.dpy, window, CWSibling | CWStackMode, &changes);
591 XFree(children);
592 return;
593 }
594 }
595 if (children) XFree(children);
596 }
597
598 // No always-on-top windows found, or XQueryTree failed.
599 XRaiseWindow(wm.dpy, window);
600}
601
602int ignore_x_error(Display *dpy, XErrorEvent *err) {
603 (void)dpy;
604 (void)err;
605 return 0;
606}
607
608int window_exists(Window window) {
609 if (window == None) return 0;
610 XErrorHandler old = XSetErrorHandler(ignore_x_error);
611 XWindowAttributes attr;
612 Status status = XGetWindowAttributes(wm.dpy, window, &attr);
613 XSync(wm.dpy, False);
614 XSetErrorHandler(old);
615 return status != 0;
616}
617
618void set_active_window(Window window, Time time) {
619 (void)time; // Use CurrentTime for more reliable focus stealing/pulling.
620
621 if (window != None) {
622 if (!window_exists(window)) return;
623
624 wm.active = window;
625 XChangeProperty(wm.dpy, wm.root, _NET_ACTIVE_WINDOW, XA_WINDOW, 32, PropModeReplace, (unsigned char *)&window, 1);
626
627 // Check for WM_TAKE_FOCUS support.
628 int take_focus = 0;
629 Atom *protocols = NULL;
630 int count = 0;
631 if (XGetWMProtocols(wm.dpy, window, &protocols, &count)) {
632 for (int i = 0; i < count; i++) {
633 if (protocols[i] == WM_TAKE_FOCUS) {
634 take_focus = 1;
635 break;
636 }
637 }
638 XFree(protocols);
639 }
640
641 // Check WM_HINTS if needed (logging only for now as we force focus anyway)
642 XWMHints *hints = XGetWMHints(wm.dpy, window);
643 if (hints) {
644 log_message(stdout, LOG_DEBUG, "Window hints: input=%d", !!(hints->flags & InputHint && hints->input));
645 XFree(hints);
646 }
647
648 // Always set X focus for managed windows.
649 XSetInputFocus(wm.dpy, window, RevertToParent, CurrentTime);
650 log_message(stdout, LOG_DEBUG, "Set input focus to 0x%lx", window);
651
652 if (take_focus) {
653 XEvent ev = {0};
654 ev.type = ClientMessage;
655 ev.xclient.window = window;
656 ev.xclient.message_type = WM_PROTOCOLS;
657 ev.xclient.format = 32;
658 ev.xclient.data.l[0] = WM_TAKE_FOCUS;
659 ev.xclient.data.l[1] = CurrentTime;
660 XSendEvent(wm.dpy, window, False, NoEventMask, &ev);
661 log_message(stdout, LOG_DEBUG, "Sent WM_TAKE_FOCUS to 0x%lx", window);
662 }
663 } else {
664 XDeleteProperty(wm.dpy, wm.root, _NET_ACTIVE_WINDOW);
665 wm.active = None;
666 XSetInputFocus(wm.dpy, wm.root, RevertToParent, CurrentTime);
667 log_message(stdout, LOG_DEBUG, "Reset focus to root");
668 }
669 XFlush(wm.dpy);
670}
671
672Window get_active_window(void) {
673 Atom _NET_ACTIVE_WINDOW = XInternAtom(wm.dpy, "_NET_ACTIVE_WINDOW", False);
674 Atom actual_type;
675 int actual_format;
676 unsigned long nitems, bytes_after;
677 unsigned char *prop = NULL;
678 Window active = None;
679
680 if (XGetWindowProperty(wm.dpy, wm.root, _NET_ACTIVE_WINDOW, 0, (~0L), False, AnyPropertyType, &actual_type, &actual_format, &nitems, &bytes_after, &prop) == Success) {
681 if (prop && nitems >= 1) {
682 active = *(Window *)prop;
683 }
684 }
685
686 if (prop) XFree(prop);
687 return active;
688}
689
690void get_cursor_offset(Window window, int *dx, int *dy) {
691 Window root, child;
692 int root_x, root_y;
693 unsigned int mask;
694 XQueryPointer(wm.dpy, window, &root, &child, &root_x, &root_y, dx, dy, &mask);
695}
696
697// https://tronche.com/gui/x/xlib/events/structure-control/configure.html
698void handle_configure_request(void) {
699 XConfigureRequestEvent *ev = &wm.ev.xconfigurerequest;
700 XWindowChanges changes;
701
702 changes.x = ev->x;
703 changes.y = ev->y;
704 changes.width = ev->width;
705 changes.height = ev->height;
706 changes.border_width = ev->border_width;
707 changes.sibling = ev->above;
708 changes.stack_mode = ev->detail;
709
710 XErrorHandler old = XSetErrorHandler(ignore_x_error);
711 XConfigureWindow(wm.dpy, ev->window, ev->value_mask, &changes);
712 XSync(wm.dpy, False);
713 XSetErrorHandler(old);
714
715 log_message(stdout, LOG_DEBUG, "ConfigureRequest for 0x%lx (x=%d, y=%d, w=%d, h=%d)", ev->window, ev->x, ev->y, ev->width, ev->height);
716}
717
718void handle_configure_notify(void) {
719 XConfigureEvent *ev = &wm.ev.xconfigure;
720
721 if (ev->window == wm.root || ev->send_event) return;
722
723 // Only send synthetic events for windows we manage as top-level clients.
724 Client *c;
725 for (c = wm.clients; c; c = c->next) {
726 if (c->window == ev->window) {
727 log_message(stdout, LOG_DEBUG, "Sending synthetic ConfigureNotify to 0x%lx (x=%d, y=%d, w=%d, h=%d)", ev->window, ev->x, ev->y, ev->width, ev->height);
728 send_configure(c->window);
729 break;
730 }
731 }
732}
733
734void handle_map_notify(void) {
735 Window window = wm.ev.xmap.window;
736 if (window == wm.root) return;
737
738 log_message(stdout, LOG_DEBUG, "MapNotify for 0x%lx", window);
739
740 // Check if this is a managed window.
741 Client *c;
742 for (c = wm.clients; c; c = c->next) {
743 if (c->window == window) {
744 // Shows, raises and focuses the window.
745 set_active_border(window);
746 set_active_window(window, CurrentTime);
747
748 // Ensure it has synthetic configure after mapping.
749 send_configure(window);
750
751 log_message(stdout, LOG_DEBUG, "Focused and configured 0x%lx after MapNotify", window);
752 break;
753 }
754 }
755 redraw_widgets();
756}
757
758// https://tronche.com/gui/x/xlib/events/structure-control/map.html
759void handle_map_request(void) {
760 Window window = wm.ev.xmaprequest.window;
761 if (window == wm.root) return;
762
763 XErrorHandler old = XSetErrorHandler(ignore_x_error);
764
765 // Move window under cursor position and clamps inside the screen bounds.
766 XWindowAttributes check_attr;
767 if (XGetWindowAttributes(wm.dpy, window, &check_attr)) {
768 XSelectInput(wm.dpy, window, EnterWindowMask | LeaveWindowMask | StructureNotifyMask | FocusChangeMask);
769
770 Window root_return, child_return;
771 int root_x, root_y, win_x, win_y;
772 unsigned int mask;
773
774 if (XQueryPointer(wm.dpy, wm.root, &root_return, &child_return, &root_x, &root_y, &win_x, &win_y, &mask)) {
775 int new_x = root_x - (check_attr.width / 2);
776 int new_y = root_y - (check_attr.height / 2);
777 int screen_width = DisplayWidth(wm.dpy, wm.screen);
778 int screen_height = DisplayHeight(wm.dpy, wm.screen);
779
780 if (new_x < 0) new_x = 0;
781 if (new_y < 0) new_y = 0;
782 if (new_x + check_attr.width > screen_width) new_x = screen_width - check_attr.width;
783 if (new_y + check_attr.height > screen_height) new_y = screen_height - check_attr.height;
784
785 XMoveWindow(wm.dpy, window, new_x, new_y);
786 log_message(stdout, LOG_DEBUG, "Positioned new window 0x%lx at cursor (%d, %d)", window, root_x, root_y);
787 }
788 }
789
790 // Tag window with current desktop.
791 unsigned long desktop = wm.current_desktop;
792 XChangeProperty(wm.dpy, window, _NET_WM_DESKTOP, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&desktop, 1);
793
794 // Grab buttons for click-to-focus.
795 grab_buttons(window);
796
797 add_client(window);
798 apply_tiling_layout();
799
800 XMapWindow(wm.dpy, window);
801 raise_window(window);
802
803 XSync(wm.dpy, False);
804 XSetErrorHandler(old);
805
806 log_message(stdout, LOG_DEBUG, "Window 0x%lx added and requested map on desktop %d", window, wm.current_desktop);
807 update_client_list();
808}
809
810// https://tronche.com/gui/x/xlib/events/window-state-change/unmap.html
811void handle_unmap_notify(void) {
812 Window window = wm.ev.xunmap.window;
813 if (window == wm.active) {
814 set_active_window(None, CurrentTime);
815 }
816
817 // So if we get an unmap for a sticky window, it means the application closed it.
818 if (is_sticky(window)) {
819 remove_client(window);
820 }
821
822 log_message(stdout, LOG_DEBUG, "Window 0x%lx unmapped", window);
823 apply_tiling_layout();
824 update_client_list();
825}
826
827// https://tronche.com/gui/x/xlib/events/window-state-change/destroy.html
828void handle_destroy_notify(void) {
829 Window window = wm.ev.xdestroywindow.window;
830 if (window == wm.active) {
831 set_active_window(None, CurrentTime);
832 }
833 remove_client(window);
834 apply_tiling_layout();
835 log_message(stdout, LOG_DEBUG, "Window 0x%lx destroyed", window);
836 update_client_list();
837}
838
839// https://tronche.com/gui/x/xlib/events/client-communication/property.html
840void handle_property_notify(void) {
841 Window window = wm.ev.xproperty.window;
842 Atom prop = wm.ev.xproperty.atom;
843 char *name = XGetAtomName(wm.dpy, prop);
844 log_message(stdout, LOG_DEBUG, "Window 0x%lx got property notification %s", window, name);
845}
846
847// https://tronche.com/gui/x/xlib/events/keyboard-pointer/keyboard-pointer.html
848void handle_motion_notify(void) {
849 if (wm.start.subwindow != None && (wm.start.state & MODKEY)) {
850 int xdiff = wm.ev.xmotion.x_root - wm.start.x_root;
851 int ydiff = wm.ev.xmotion.y_root - wm.start.y_root;
852
853 XMoveResizeWindow(wm.dpy, wm.start.subwindow,
854 wm.attr.x + (wm.start.button == 1 ? xdiff : 0),
855 wm.attr.y + (wm.start.button == 1 ? ydiff : 0),
856 MAX(100, wm.attr.width + (wm.start.button == 3 ? xdiff : 0)),
857 MAX(100, wm.attr.height + (wm.start.button == 3 ? ydiff : 0)));
858 send_configure(wm.start.subwindow);
859
860 // Reset maximization state on manual move/resize.
861 if (wm.start.button == 1) {
862 check_and_clear_maximized_state(wm.start.subwindow, 1, 1);
863 } else if (wm.start.button == 3) {
864 check_and_clear_maximized_state(wm.start.subwindow, 1, 1);
865 }
866 }
867}
868
869// https://tronche.com/gui/x/xlib/events/client-communication/client-message.html
870void handle_client_message(void) {
871 Window window = wm.ev.xclient.window;
872 Atom message_type = wm.ev.xclient.message_type;
873
874 if (message_type == _NET_WM_STATE) {
875 Atom atom1 = (Atom)wm.ev.xclient.data.l[1];
876 Atom atom2 = (Atom)wm.ev.xclient.data.l[2];
877 int action = wm.ev.xclient.data.l[0]; // 0: Remove, 1: Add, 2: Toggle
878
879 if (atom1 == _NET_WM_STATE_FULLSCREEN || atom2 == _NET_WM_STATE_FULLSCREEN) {
880 int currently_fullscreen = has_wm_state(window, _NET_WM_STATE_FULLSCREEN);
881 int should_fullscreen = 0;
882
883 if (action == 1) { // ADD
884 should_fullscreen = 1;
885 } else if (action == 0) { // REMOVE
886 should_fullscreen = 0;
887 } else if (action == 2) { // TOGGLE
888 should_fullscreen = !currently_fullscreen;
889 }
890
891 set_fullscreen(window, should_fullscreen);
892 }
893 }
894
895 log_message(stdout, LOG_DEBUG, "Window 0x%lx got message type of %lu", window, message_type);
896 redraw_widgets();
897 XFlush(wm.dpy);
898}
899
900static Client *wintoclient(Window w) {
901 if (w == None || w == wm.root) return NULL;
902 Client *c;
903 for (c = wm.clients; c; c = c->next) {
904 if (c->window == w) return c;
905 }
906 // Search parent hierarchy
907 Window root, parent, *children;
908 unsigned int nchildren;
909 XErrorHandler old = XSetErrorHandler(ignore_x_error);
910 while (w != wm.root && w != None) {
911 if (XQueryTree(wm.dpy, w, &root, &parent, &children, &nchildren)) {
912 if (children) XFree(children);
913 for (c = wm.clients; c; c = c->next) {
914 if (c->window == parent) {
915 XSetErrorHandler(old);
916 return c;
917 }
918 }
919 w = parent;
920 } else {
921 break;
922 }
923 }
924 XSetErrorHandler(old);
925 return NULL;
926}
927
928static void send_configure(Window w) {
929 XWindowAttributes wa;
930 if (!XGetWindowAttributes(wm.dpy, w, &wa)) return;
931
932 XConfigureEvent ce;
933 ce.type = ConfigureNotify;
934 ce.display = wm.dpy;
935 ce.event = w;
936 ce.window = w;
937 ce.x = wa.x;
938 ce.y = wa.y;
939 ce.width = wa.width;
940 ce.height = wa.height;
941 ce.border_width = wa.border_width;
942 ce.above = None;
943 ce.override_redirect = False;
944 XSendEvent(wm.dpy, w, False, StructureNotifyMask, (XEvent *)&ce);
945}
946
947static void set_fullscreen(Window window, int full) {
948 int currently_fullscreen = has_wm_state(window, _NET_WM_STATE_FULLSCREEN);
949
950 if (full && !currently_fullscreen) {
951 // Enable Fullscreen
952 XWindowAttributes attr;
953 if (XGetWindowAttributes(wm.dpy, window, &attr)) {
954 // Save geometry
955 long geom[4] = { attr.x, attr.y, attr.width, attr.height };
956 XChangeProperty(wm.dpy, window, _GLITCH_PRE_FULLSCREEN_GEOM, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)geom, 4);
957
958 int screen_width = DisplayWidth(wm.dpy, wm.screen);
959 int screen_height = DisplayHeight(wm.dpy, wm.screen);
960
961 XMoveResizeWindow(wm.dpy, window, 0, 0, screen_width, screen_height);
962 send_configure(window);
963 XSetWindowBorderWidth(wm.dpy, window, 0);
964 XRaiseWindow(wm.dpy, window);
965
966 update_wm_state(window, _NET_WM_STATE_FULLSCREEN, 1);
967 log_message(stdout, LOG_DEBUG, "Fullscreen enabled for 0x%lx", window);
968 }
969 } else if (!full && currently_fullscreen) {
970 // Disable Fullscreen
971 Atom actual_type;
972 int actual_format;
973 unsigned long nitems, bytes_after;
974 unsigned char *prop = NULL;
975
976 XErrorHandler old = XSetErrorHandler(ignore_x_error);
977 int status = XGetWindowProperty(wm.dpy, window, _GLITCH_PRE_FULLSCREEN_GEOM, 0, 4, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
978 XSync(wm.dpy, False);
979 XSetErrorHandler(old);
980
981 if (status == Success && prop && nitems == 4) {
982 long *geom = (long *)prop;
983 XMoveResizeWindow(wm.dpy, window, (int)geom[0], (int)geom[1], (unsigned int)geom[2], (unsigned int)geom[3]);
984 send_configure(window);
985 }
986 if (prop) XFree(prop);
987
988 XDeleteProperty(wm.dpy, window, _GLITCH_PRE_FULLSCREEN_GEOM);
989
990 // Restore border
991 int border_w = border_size;
992 XSetWindowBorderWidth(wm.dpy, window, border_w);
993
994 update_wm_state(window, _NET_WM_STATE_FULLSCREEN, 0);
995 set_active_border(window); // Updates border color and width correctly
996
997 log_message(stdout, LOG_DEBUG, "Fullscreen disabled for 0x%lx", window);
998 }
999}
1000
1001void toggle_fullscreen(const Arg *arg) {
1002 (void)arg;
1003 if (wm.active == None) return;
1004 int currently_fullscreen = has_wm_state(wm.active, _NET_WM_STATE_FULLSCREEN);
1005 set_fullscreen(wm.active, !currently_fullscreen);
1006}
1007
1008void center_window(const Arg *arg) {
1009 (void)arg;
1010 if (wm.active == None) return;
1011
1012 XWindowAttributes attr;
1013 if (XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
1014 int screen_width = DisplayWidth(wm.dpy, wm.screen);
1015 int screen_height = DisplayHeight(wm.dpy, wm.screen);
1016 int new_x = (screen_width - attr.width) / 2;
1017 int new_y = (screen_height - attr.height) / 2;
1018
1019 if (new_x < 0) new_x = 0;
1020 if (new_y < 0) new_y = 0;
1021
1022 XMoveWindow(wm.dpy, wm.active, new_x, new_y);
1023 send_configure(wm.active);
1024 log_message(stdout, LOG_DEBUG, "Centered window 0x%lx at (%d, %d)", wm.active, new_x, new_y);
1025 }
1026}
1027
1028// https://tronche.com/gui/x/xlib/events/keyboard-pointer/keyboard-pointer.html
1029void handle_button_press(void) {
1030 Window window = None;
1031 Client *c = wintoclient(wm.ev.xbutton.window);
1032 if (!c) c = wintoclient(wm.ev.xbutton.subwindow);
1033
1034 if (c) {
1035 window = c->window;
1036 } else if (wm.ev.xbutton.window != wm.root) {
1037 window = wm.ev.xbutton.window;
1038 } else {
1039 window = wm.ev.xbutton.subwindow;
1040 }
1041
1042 log_message(stdout, LOG_DEBUG, "ButtonPress: window=0x%lx, subwindow=0x%lx, button=%d, state=0x%x, targeting=0x%lx",
1043 wm.ev.xbutton.window, wm.ev.xbutton.subwindow, wm.ev.xbutton.button, wm.ev.xbutton.state, window);
1044
1045 if (window == None || window == wm.root) {
1046 log_message(stdout, LOG_DEBUG, "ButtonPress on root or None, ignoring");
1047 XAllowEvents(wm.dpy, ReplayPointer, wm.ev.xbutton.time);
1048 return;
1049 }
1050
1051 // Focus and raise on any click.
1052 set_active_border(window);
1053 set_active_window(window, wm.ev.xbutton.time);
1054
1055 XErrorHandler old = XSetErrorHandler(ignore_x_error);
1056 raise_window(window);
1057 XSync(wm.dpy, False);
1058 XSetErrorHandler(old);
1059
1060 log_message(stdout, LOG_DEBUG, "Focused and raised window 0x%lx", window);
1061
1062 if (wm.ev.xbutton.state & MODKEY) {
1063 old = XSetErrorHandler(ignore_x_error);
1064 Status s = XGetWindowAttributes(wm.dpy, window, &wm.attr);
1065 XSync(wm.dpy, False);
1066 XSetErrorHandler(old);
1067
1068 if (s == 0) {
1069 log_message(stdout, LOG_WARNING, "Failed to get window attributes for 0x%lx, ignoring drag", window);
1070 XAllowEvents(wm.dpy, AsyncPointer, CurrentTime);
1071 return;
1072 }
1073
1074 wm.start = wm.ev.xbutton;
1075 // Ensure we use the top-level window for dragging, not a sub-window.
1076 wm.start.subwindow = window;
1077
1078 // Set global error handler to ignore errors during drag (e.g. if window closes).
1079 XSetErrorHandler(ignore_x_error);
1080
1081 switch (wm.ev.xbutton.button) {
1082 case 1: {
1083 XDefineCursor(wm.dpy, window, wm.cursor_move);
1084 log_message(stdout, LOG_DEBUG, "Setting cursor to move");
1085 } break;
1086 case 3: {
1087 XDefineCursor(wm.dpy, window, wm.cursor_resize);
1088 log_message(stdout, LOG_DEBUG, "Setting cursor to resize");
1089 } break;
1090 }
1091
1092 // Use AsyncPointer to consume the event and unfreeze the pointer.
1093 XAllowEvents(wm.dpy, AsyncPointer, wm.ev.xbutton.time);
1094 log_message(stdout, LOG_DEBUG, "Window 0x%lx got button press with MODKEY, unfreezing", window);
1095 } else {
1096 // Replay the click to the application if no modifier was used.
1097 XAllowEvents(wm.dpy, ReplayPointer, wm.ev.xbutton.time);
1098 log_message(stdout, LOG_DEBUG, "Window 0x%lx got button press, replaying pointer", window);
1099 }
1100
1101 redraw_widgets();
1102 XFlush(wm.dpy);
1103}
1104
1105
1106
1107void goto_desktop(const Arg *arg) {
1108 if (arg->i < 1 || arg->i > NUM_DESKTOPS || (unsigned int)arg->i == wm.current_desktop) return;
1109
1110 unsigned int old_desktop = wm.current_desktop;
1111 wm.current_desktop = arg->i;
1112
1113 unsigned int nchildren;
1114 Window root_return, parent_return, *children;
1115 XWindowAttributes wa;
1116 Window new_active = None;
1117
1118 if (XQueryTree(wm.dpy, wm.root, &root_return, &parent_return, &children, &nchildren)) {
1119 for (unsigned int i = 0; i < nchildren; i++) {
1120 if (!XGetWindowAttributes(wm.dpy, children[i], &wa) || wa.override_redirect)
1121 continue;
1122
1123 unsigned long desktop;
1124 Atom actual_type;
1125 int actual_format;
1126 unsigned long nitems, bytes_after;
1127 unsigned char *prop = NULL;
1128
1129 int status = XGetWindowProperty(wm.dpy, children[i], _NET_WM_DESKTOP, 0, 1, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
1130 if (status == Success && prop && nitems > 0) {
1131 desktop = *(unsigned long *)prop;
1132 if (desktop == (unsigned long)arg->i) {
1133 XMapWindow(wm.dpy, children[i]);
1134 new_active = children[i];
1135 } else if (desktop == (unsigned long)old_desktop) {
1136 // Don't unmap sticky windows
1137 if (!is_sticky(children[i])) {
1138 XUnmapWindow(wm.dpy, children[i]);
1139 }
1140 }
1141 }
1142 // Sticky windows should always be shown and raised
1143 if (is_sticky(children[i])) {
1144 XRaiseWindow(wm.dpy, children[i]);
1145 }
1146 if (prop) XFree(prop);
1147 }
1148 if (children) XFree(children);
1149 }
1150
1151 unsigned long desktop_val = wm.current_desktop;
1152 XChangeProperty(wm.dpy, wm.root, _NET_CURRENT_DESKTOP, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&desktop_val, 1);
1153
1154 set_active_window(new_active, CurrentTime);
1155 set_active_border(new_active);
1156 apply_tiling_layout();
1157
1158 widget_desktop_indicator();
1159 widget_datetime();
1160 log_message(stdout, LOG_DEBUG, "Switched to desktop %d", wm.current_desktop);
1161 XFlush(wm.dpy);
1162}
1163
1164void send_window_to_desktop(const Arg *arg) {
1165 if (wm.active == None || arg->i < 1 || arg->i > NUM_DESKTOPS || (unsigned int)arg->i == wm.current_desktop) return;
1166
1167 unsigned long desktop = arg->i;
1168 XChangeProperty(wm.dpy, wm.active, _NET_WM_DESKTOP, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&desktop, 1);
1169
1170 // Reset border before unmapping to avoid "stuck" active borders on other desktops.
1171 XSetWindowBorder(wm.dpy, wm.active, wm.borders.normal_inactive);
1172 XUnmapWindow(wm.dpy, wm.active);
1173
1174 wm.active = None;
1175 apply_tiling_layout();
1176 widget_desktop_indicator();
1177 widget_datetime();
1178 log_message(stdout, LOG_DEBUG, "Moved window to desktop %d", arg->i);
1179 XFlush(wm.dpy);
1180}
1181
1182
1183void update_client_list(void) {
1184 XDeleteProperty(wm.dpy, wm.root, _NET_CLIENT_LIST);
1185
1186 Client *c = wm.clients;
1187 while (c) {
1188 XChangeProperty(wm.dpy, wm.root, _NET_CLIENT_LIST, XA_WINDOW, 32, PropModeAppend, (unsigned char *)&c->window, 1);
1189 c = c->next;
1190 }
1191}
1192
1193void grab_buttons(Window window) {
1194 XGrabButton(wm.dpy, AnyButton, AnyModifier, window, True, ButtonPressMask, GrabModeSync, GrabModeAsync, None, None);
1195}
1196
1197// https://tronche.com/gui/x/xlib/events/keyboard-pointer/keyboard-pointer.html
1198void handle_button_release(void) {
1199 if (wm.start.subwindow != None && (wm.start.state & MODKEY)) {
1200 XDefineCursor(wm.dpy, wm.start.subwindow, None);
1201
1202 // Restore window manager error handler
1203 XSetErrorHandler(x_error_handler);
1204
1205 log_message(stdout, LOG_DEBUG, "Restored window manager cursor on window 0x%lx", wm.start.subwindow);
1206 wm.start.subwindow = None;
1207 }
1208
1209 log_message(stdout, LOG_DEBUG, "ButtonRelease: event window=0x%lx, subwindow=0x%lx", wm.ev.xbutton.window, wm.ev.xbutton.subwindow);
1210 XFlush(wm.dpy);
1211}
1212
1213// https://tronche.com/gui/x/xlib/events/keyboard-pointer/keyboard-pointer.html
1214void handle_key_press(void) {
1215 if (wm.launcher_active) {
1216 launcher_handle_key();
1217 return;
1218 }
1219
1220 log_message(stdout, LOG_DEBUG, ">> Key pressed > active window 0x%lx", wm.ev.xkey.subwindow);
1221 if (wm.ev.type != KeyPress) return;
1222
1223 KeySym keysym = XLookupKeysym(&wm.ev.xkey, 0);
1224
1225 // Check keybinds first.
1226 for (unsigned int i = 0; i < LENGTH(keybinds); i++) {
1227 if (keysym == keybinds[i].keysym && (wm.ev.xkey.state & (Mod1Mask|Mod2Mask|Mod3Mask|Mod4Mask|ControlMask|ShiftMask)) == keybinds[i].mod) {
1228 keybinds[i].func(&keybinds[i].arg);
1229 XFlush(wm.dpy);
1230 return;
1231 }
1232 }
1233
1234 // Check shortcuts next.
1235 for (unsigned int i = 0; i < LENGTH(shortcuts); i++) {
1236 if (keysym == shortcuts[i].keysym && (wm.ev.xkey.state & (Mod1Mask|Mod2Mask|Mod3Mask|Mod4Mask|ControlMask|ShiftMask)) == shortcuts[i].mod) {
1237 execute_shortcut(shortcuts[i].cmd);
1238 XFlush(wm.dpy);
1239 return;
1240 }
1241 }
1242
1243 XFlush(wm.dpy);
1244}
1245
1246// https://tronche.com/gui/x/xlib/events/keyboard-pointer/keyboard-pointer.html
1247void handle_key_release(void) {
1248 if (wm.ev.type != KeyRelease) return;
1249
1250 KeySym keysym = XLookupKeysym(&wm.ev.xkey, 0);
1251
1252 if (wm.is_cycling) {
1253 if (keysym == XK_Alt_L || keysym == XK_Alt_R) {
1254 // Activate selected window
1255 if (wm.cycle_clients && wm.cycle_count > 0 && wm.active_cycle_index >= 0) {
1256 Window target = wm.cycle_clients[wm.active_cycle_index];
1257 set_active_border(target);
1258 set_active_window(target, CurrentTime);
1259 raise_window(target);
1260 XSync(wm.dpy, False);
1261 }
1262 end_cycling();
1263 }
1264 }
1265}
1266
1267void handle_focus_in(void) {
1268 Window window = wm.ev.xfocus.window;
1269 if (window == wm.root || window == None) return;
1270
1271 Client *c = wintoclient(window);
1272 if (wm.active != None && (!c || c->window != wm.active)) {
1273 log_message(stdout, LOG_DEBUG, "Window 0x%lx focus in (foreign), forcing back to active 0x%lx", window, wm.active);
1274 set_active_window(wm.active, CurrentTime);
1275 }
1276}
1277
1278void handle_focus_out(void) {
1279 Window window = wm.ev.xfocus.window;
1280 if (window != wm.root) {
1281 log_message(stdout, LOG_DEBUG, "Window 0x%lx focus out", window);
1282 }
1283}
1284
1285void handle_enter_notify(void) {
1286 Window window = wm.ev.xcrossing.window;
1287 if (window != wm.root) {
1288 log_message(stdout, LOG_DEBUG, "Window 0x%lx enter notify", window);
1289 }
1290}
1291
1292void handle_expose(void) {
1293 if (wm.ev.xexpose.count == 0 && wm.ev.xexpose.window == wm.root) {
1294 // Rate limit redraws from Expose events to 200ms (5fps) to prevent flashing/high CPU
1295 struct timespec ts;
1296 clock_gettime(CLOCK_MONOTONIC, &ts);
1297 unsigned long now_ms = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
1298
1299 if (now_ms - wm.last_widget_update > 50) {
1300 redraw_widgets();
1301 wm.last_widget_update = now_ms;
1302 }
1303 }
1304}
1305
1306void set_active_border(Window window) {
1307 if (window == None) return;
1308 if (!window_exists(window)) return;
1309
1310 // Setting current active window to inactive.
1311 if (wm.active != None && wm.active != window) {
1312 update_window_border(wm.active, 0);
1313 log_message(stdout, LOG_DEBUG, "Active window 0x%lx border set to inactive", wm.active);
1314 }
1315
1316 // Setting desired window to active.
1317 update_window_border(window, 1);
1318 XFlush(wm.dpy);
1319
1320 log_message(stdout, LOG_DEBUG, "Desired window 0x%lx border set to active", window);
1321}
1322
1323void move_window_x(const Arg *arg) {
1324 if (wm.active == None) return;
1325
1326 XWindowAttributes attr;
1327 XGetWindowAttributes(wm.dpy, wm.active, &attr);
1328 XMoveWindow(wm.dpy, wm.active, attr.x + arg->i, attr.y);
1329 check_and_clear_maximized_state(wm.active, 1, 0);
1330 log_message(stdout, LOG_DEBUG, "Move window 0x%lx on X by %d", wm.active, arg->i);
1331
1332 XSync(wm.dpy, False);
1333 XFlush(wm.dpy);
1334}
1335
1336void move_window_y(const Arg *arg) {
1337 if (wm.active == None) return;
1338
1339 XWindowAttributes attr;
1340 XGetWindowAttributes(wm.dpy, wm.active, &attr);
1341 XMoveWindow(wm.dpy, wm.active, attr.x, attr.y + arg->i);
1342 check_and_clear_maximized_state(wm.active, 0, 1);
1343 log_message(stdout, LOG_DEBUG, "Move window 0x%lx on Y by %d", wm.active, arg->i);
1344
1345 XSync(wm.dpy, False);
1346 XFlush(wm.dpy);
1347}
1348
1349void close_window(const Arg *arg) {
1350 (void)arg;
1351 if (wm.active == None) return;
1352
1353 int supported = 0;
1354 Atom *protocols = NULL;
1355 int count = 0;
1356 if (XGetWMProtocols(wm.dpy, wm.active, &protocols, &count)) {
1357 for (int i = 0; i < count; i++) {
1358 if (protocols[i] == WM_DELETE_WINDOW) {
1359 supported = 1;
1360 break;
1361 }
1362 }
1363 XFree(protocols);
1364 }
1365
1366 if (supported) {
1367 XEvent ev = {0};
1368 ev.type = ClientMessage;
1369 ev.xclient.window = wm.active;
1370 ev.xclient.message_type = WM_PROTOCOLS;
1371 ev.xclient.format = 32;
1372 ev.xclient.data.l[0] = WM_DELETE_WINDOW;
1373 ev.xclient.data.l[1] = CurrentTime;
1374 XSendEvent(wm.dpy, wm.active, False, NoEventMask, &ev);
1375 log_message(stdout, LOG_DEBUG, "Sent WM_DELETE_WINDOW to 0x%lx", wm.active);
1376 } else {
1377 XKillClient(wm.dpy, wm.active);
1378 log_message(stdout, LOG_DEBUG, "Killed client 0x%lx", wm.active);
1379 }
1380}
1381
1382void resize_window_x(const Arg *arg) {
1383 if (wm.active == None) return;
1384
1385 XWindowAttributes attr;
1386 XGetWindowAttributes(wm.dpy, wm.active, &attr);
1387 XResizeWindow(wm.dpy, wm.active, MAX(1, attr.width + arg->i), attr.height);
1388 check_and_clear_maximized_state(wm.active, 1, 0);
1389 XFlush(wm.dpy);
1390
1391 log_message(stdout, LOG_DEBUG, "Resize window 0x%lx on X by %d", wm.active, arg->i);
1392}
1393
1394void resize_window_y(const Arg *arg) {
1395 if (wm.active == None) return;
1396
1397 XWindowAttributes attr;
1398 XGetWindowAttributes(wm.dpy, wm.active, &attr);
1399 XResizeWindow(wm.dpy, wm.active, attr.width, MAX(1, attr.height + arg->i));
1400 check_and_clear_maximized_state(wm.active, 0, 1);
1401 XFlush(wm.dpy);
1402
1403 log_message(stdout, LOG_DEBUG, "Resize window 0x%lx on Y by %d", wm.active, arg->i);
1404}
1405
1406void window_snap_up(const Arg *arg) {
1407 (void)arg;
1408 if (wm.active == None) return;
1409
1410 XWindowAttributes attr;
1411 if (!XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
1412 log_message(stdout, LOG_DEBUG, "Failed to get window attributes for 0x%lx", wm.active);
1413 return;
1414 }
1415
1416 int rel_x, rel_y;
1417 get_cursor_offset(wm.active, &rel_x, &rel_y);
1418
1419 XMoveWindow(wm.dpy, wm.active, attr.x, 0);
1420 check_and_clear_maximized_state(wm.active, 0, 1);
1421 XFlush(wm.dpy);
1422
1423 log_message(stdout, LOG_DEBUG, "Snapped window 0x%lx to top edge", wm.active);
1424}
1425
1426void window_snap_down(const Arg *arg) {
1427 (void)arg;
1428 if (wm.active == None) return;
1429
1430 XWindowAttributes attr;
1431 if (!XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
1432 log_message(stdout, LOG_DEBUG, "Failed to get window attributes for 0x%lx", wm.active);
1433 return;
1434 }
1435
1436 int rel_x, rel_y;
1437 int y = DisplayHeight(wm.dpy, DefaultScreen(wm.dpy)) - attr.height - (2 * attr.border_width);
1438 get_cursor_offset(wm.active, &rel_x, &rel_y);
1439
1440 XMoveWindow(wm.dpy, wm.active, attr.x, y);
1441 check_and_clear_maximized_state(wm.active, 0, 1);
1442 XFlush(wm.dpy);
1443
1444 log_message(stdout, LOG_DEBUG, "Snapped window 0x%lx to bottom edge", wm.active);
1445}
1446
1447void window_snap_left(const Arg *arg) {
1448 (void)arg;
1449 if (wm.active == None) return;
1450
1451 XWindowAttributes attr;
1452 if (!XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
1453 log_message(stdout, LOG_DEBUG, "Failed to get window attributes for 0x%lx", wm.active);
1454 return;
1455 }
1456
1457 int rel_x, rel_y;
1458 get_cursor_offset(wm.active, &rel_x, &rel_y);
1459
1460 XMoveWindow(wm.dpy, wm.active, 0, attr.y);
1461 check_and_clear_maximized_state(wm.active, 1, 0);
1462 XFlush(wm.dpy);
1463
1464 log_message(stdout, LOG_DEBUG, "Snapped window 0x%lx to left edge", wm.active);
1465}
1466
1467void window_snap_right(const Arg *arg) {
1468 (void)arg;
1469 if (wm.active == None) return;
1470
1471 XWindowAttributes attr;
1472 if (!XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
1473 log_message(stdout, LOG_DEBUG, "Failed to get window attributes for 0x%lx", wm.active);
1474 return;
1475 }
1476
1477 int rel_x, rel_y;
1478 int x = DisplayWidth(wm.dpy, DefaultScreen(wm.dpy)) - attr.width - (2 * attr.border_width);
1479 get_cursor_offset(wm.active, &rel_x, &rel_y);
1480
1481 XMoveWindow(wm.dpy, wm.active, x, attr.y);
1482 check_and_clear_maximized_state(wm.active, 1, 0);
1483 XFlush(wm.dpy);
1484
1485 log_message(stdout, LOG_DEBUG, "Snapped window 0x%lx to right edge", wm.active);
1486}
1487
1488static void update_wm_state(Window w, Atom state_atom, int add) {
1489 Atom actual_type;
1490 int actual_format;
1491 unsigned long nitems, bytes_after;
1492 unsigned char *prop = NULL;
1493
1494 XErrorHandler old = XSetErrorHandler(ignore_x_error);
1495 int status = XGetWindowProperty(wm.dpy, w, _NET_WM_STATE, 0, (~0L), False, XA_ATOM, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
1496 XSync(wm.dpy, False);
1497 XSetErrorHandler(old);
1498
1499 if (status == Success) {
1500 Atom *new_atoms = malloc(sizeof(Atom) * (nitems + 1));
1501 int count = 0;
1502 if (prop && nitems > 0) {
1503 Atom *atoms = (Atom *)prop;
1504 for (unsigned long i = 0; i < nitems; i++) {
1505 if (atoms[i] != state_atom) {
1506 new_atoms[count++] = atoms[i];
1507 }
1508 }
1509 }
1510 if (add) {
1511 new_atoms[count++] = state_atom;
1512 }
1513 XChangeProperty(wm.dpy, w, _NET_WM_STATE, XA_ATOM, 32, PropModeReplace, (unsigned char *)new_atoms, count);
1514 free(new_atoms);
1515 }
1516 if (prop) XFree(prop);
1517}
1518
1519static int has_wm_state(Window w, Atom state_atom) {
1520 Atom actual_type;
1521 int actual_format;
1522 unsigned long nitems, bytes_after;
1523 unsigned char *prop = NULL;
1524 int found = 0;
1525
1526 XErrorHandler old = XSetErrorHandler(ignore_x_error);
1527 int status = XGetWindowProperty(wm.dpy, w, _NET_WM_STATE, 0, (~0L), False, XA_ATOM, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
1528 XSync(wm.dpy, False);
1529 XSetErrorHandler(old);
1530
1531 if (status == Success) {
1532 if (prop && nitems > 0) {
1533 Atom *atoms = (Atom *)prop;
1534 for (unsigned long i = 0; i < nitems; i++) {
1535 if (atoms[i] == state_atom) {
1536 found = 1;
1537 break;
1538 }
1539 }
1540 }
1541 }
1542 if (prop) XFree(prop);
1543 return found;
1544}
1545
1546static void check_and_clear_maximized_state(Window w, int horizontal, int vertical) {
1547 if (horizontal && has_wm_state(w, _NET_WM_STATE_MAXIMIZED_HORZ)) {
1548 update_wm_state(w, _NET_WM_STATE_MAXIMIZED_HORZ, 0);
1549 XDeleteProperty(wm.dpy, w, _GLITCH_PRE_HMAX_GEOM);
1550 log_message(stdout, LOG_DEBUG, "Cleared horizontal maximization state for window 0x%lx due to interaction", w);
1551 }
1552 if (vertical && has_wm_state(w, _NET_WM_STATE_MAXIMIZED_VERT)) {
1553 update_wm_state(w, _NET_WM_STATE_MAXIMIZED_VERT, 0);
1554 XDeleteProperty(wm.dpy, w, _GLITCH_PRE_VMAX_GEOM);
1555 log_message(stdout, LOG_DEBUG, "Cleared vertical maximization state for window 0x%lx due to interaction", w);
1556 }
1557}
1558
1559void window_hmaximize(const Arg *arg) {
1560 (void)arg;
1561 if (wm.active == None) return;
1562
1563 if (has_wm_state(wm.active, _NET_WM_STATE_MAXIMIZED_HORZ)) {
1564 // Restore geometry
1565 Atom actual_type;
1566 int actual_format;
1567 unsigned long nitems, bytes_after;
1568 unsigned char *prop = NULL;
1569
1570 XErrorHandler old = XSetErrorHandler(ignore_x_error);
1571 int status = XGetWindowProperty(wm.dpy, wm.active, _GLITCH_PRE_HMAX_GEOM, 0, 4, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
1572 XSync(wm.dpy, False);
1573 XSetErrorHandler(old);
1574
1575 if (status == Success) {
1576 if (prop && nitems == 4) {
1577 long *geom = (long *)prop;
1578 XMoveResizeWindow(wm.dpy, wm.active, (int)geom[0], (int)geom[1], (unsigned int)geom[2], (unsigned int)geom[3]);
1579 update_wm_state(wm.active, _NET_WM_STATE_MAXIMIZED_HORZ, 0);
1580 XDeleteProperty(wm.dpy, wm.active, _GLITCH_PRE_HMAX_GEOM);
1581 log_message(stdout, LOG_DEBUG, "Restored horizontal geometry for window 0x%lx", wm.active);
1582 }
1583 }
1584 if (prop) XFree(prop);
1585 } else {
1586 // Save geometry and maximize
1587 XWindowAttributes attr;
1588 if (XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
1589 long geom[4] = { attr.x, attr.y, attr.width, attr.height };
1590 XChangeProperty(wm.dpy, wm.active, _GLITCH_PRE_HMAX_GEOM, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)geom, 4);
1591
1592 int screen_width = DisplayWidth(wm.dpy, wm.screen);
1593 XMoveResizeWindow(wm.dpy, wm.active, 0, attr.y, screen_width - (2 * attr.border_width), attr.height);
1594 update_wm_state(wm.active, _NET_WM_STATE_MAXIMIZED_HORZ, 1);
1595 log_message(stdout, LOG_DEBUG, "Horizontally maximized window 0x%lx", wm.active);
1596 }
1597 }
1598 XFlush(wm.dpy);
1599}
1600
1601void window_vmaximize(const Arg *arg) {
1602 (void)arg;
1603 if (wm.active == None) return;
1604
1605 if (has_wm_state(wm.active, _NET_WM_STATE_MAXIMIZED_VERT)) {
1606 // Restore geometry
1607 Atom actual_type;
1608 int actual_format;
1609 unsigned long nitems, bytes_after;
1610 unsigned char *prop = NULL;
1611
1612 XErrorHandler old = XSetErrorHandler(ignore_x_error);
1613 int status = XGetWindowProperty(wm.dpy, wm.active, _GLITCH_PRE_VMAX_GEOM, 0, 4, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
1614 XSync(wm.dpy, False);
1615 XSetErrorHandler(old);
1616
1617 if (status == Success) {
1618 if (prop && nitems == 4) {
1619 long *geom = (long *)prop;
1620 XMoveResizeWindow(wm.dpy, wm.active, (int)geom[0], (int)geom[1], (unsigned int)geom[2], (unsigned int)geom[3]);
1621 update_wm_state(wm.active, _NET_WM_STATE_MAXIMIZED_VERT, 0);
1622 XDeleteProperty(wm.dpy, wm.active, _GLITCH_PRE_VMAX_GEOM);
1623 log_message(stdout, LOG_DEBUG, "Restored vertical geometry for window 0x%lx", wm.active);
1624 }
1625 }
1626 if (prop) XFree(prop);
1627 } else {
1628 // Save geometry and maximize
1629 XWindowAttributes attr;
1630 if (XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
1631 long geom[4] = { attr.x, attr.y, attr.width, attr.height };
1632 XChangeProperty(wm.dpy, wm.active, _GLITCH_PRE_VMAX_GEOM, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)geom, 4);
1633
1634 int screen_height = DisplayHeight(wm.dpy, wm.screen);
1635 XMoveResizeWindow(wm.dpy, wm.active, attr.x, 0, attr.width, screen_height - (2 * attr.border_width));
1636 update_wm_state(wm.active, _NET_WM_STATE_MAXIMIZED_VERT, 1);
1637 log_message(stdout, LOG_DEBUG, "Vertically maximized window 0x%lx", wm.active);
1638 }
1639 }
1640 XFlush(wm.dpy);
1641}
1642
1643void quit(const Arg *arg) {
1644 (void)arg;
1645 log_message(stdout, LOG_DEBUG, "Quit window manager");
1646 wm.running = 0;
1647}
1648
1649void toggle_pip(const Arg *arg) {
1650 (void)arg;
1651 if (wm.active == None) return;
1652
1653 int sticky = is_sticky(wm.active);
1654 unsigned long desktop = sticky ? wm.current_desktop : 0xFFFFFFFF; // 0xFFFFFFFF is often used for "all desktops"
1655
1656 // Toggle _NET_WM_DESKTOP
1657 XChangeProperty(wm.dpy, wm.active, _NET_WM_DESKTOP, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&desktop, 1);
1658
1659 // Toggle _NET_WM_STATE_STICKY
1660 if (!sticky) {
1661 XChangeProperty(wm.dpy, wm.active, _NET_WM_STATE, XA_ATOM, 32, PropModeAppend, (unsigned char *)&_NET_WM_STATE_STICKY, 1);
1662
1663 // If enabling PiP: resize to a small corner window
1664 int screen_width = DisplayWidth(wm.dpy, wm.screen);
1665 int screen_height = DisplayHeight(wm.dpy, wm.screen);
1666 int pip_w = screen_width / 4;
1667 int pip_h = screen_height / 4;
1668 int x = screen_width - pip_w - 20;
1669 int y = screen_height - pip_h - 20;
1670
1671 XMoveResizeWindow(wm.dpy, wm.active, x, y, pip_w, pip_h);
1672 XRaiseWindow(wm.dpy, wm.active);
1673 } else {
1674 // If disabling: just remove sticky state (could restore size if we saved it)
1675 // For now, let's just remove the atom. This is a bit complex with XChangeProperty,
1676 // but since we only have one state usually it might be okay to just clear it if we were sure.
1677 // Better way: get property, remove atom from list, set property.
1678
1679 Atom actual_type;
1680 int actual_format;
1681 unsigned long nitems, bytes_after;
1682 unsigned char *prop = NULL;
1683
1684 XErrorHandler old = XSetErrorHandler(ignore_x_error);
1685 int status = XGetWindowProperty(wm.dpy, wm.active, _NET_WM_STATE, 0, (~0L), False, XA_ATOM, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
1686 XSync(wm.dpy, False);
1687 XSetErrorHandler(old);
1688
1689 if (status == Success) {
1690 if (prop && nitems > 0) {
1691 Atom *atoms = (Atom *)prop;
1692 Atom *new_atoms = malloc(sizeof(Atom) * nitems);
1693 int count = 0;
1694 for (unsigned long i = 0; i < nitems; i++) {
1695 if (atoms[i] != _NET_WM_STATE_STICKY) {
1696 new_atoms[count++] = atoms[i];
1697 }
1698 }
1699 XChangeProperty(wm.dpy, wm.active, _NET_WM_STATE, XA_ATOM, 32, PropModeReplace, (unsigned char *)new_atoms, count);
1700 free(new_atoms);
1701 }
1702 }
1703 if (prop) XFree(prop);
1704 }
1705
1706 set_active_border(wm.active);
1707 widget_desktop_indicator();
1708 widget_datetime();
1709 log_message(stdout, LOG_DEBUG, "Toggled PiP for window 0x%lx (sticky=%d)", wm.active, !sticky);
1710 XFlush(wm.dpy);
1711}
1712
1713int is_sticky(Window window) {
1714 if (window == None) return 0;
1715
1716 // Check _NET_WM_DESKTOP first
1717 Atom actual_type;
1718 int actual_format;
1719 unsigned long nitems, bytes_after;
1720 unsigned char *prop = NULL;
1721
1722 XErrorHandler old = XSetErrorHandler(ignore_x_error);
1723 int status = XGetWindowProperty(wm.dpy, window, _NET_WM_DESKTOP, 0, 1, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
1724 XSync(wm.dpy, False);
1725 XSetErrorHandler(old);
1726
1727 if (status == Success) {
1728 if (prop && nitems > 0) {
1729 unsigned long desktop = *(unsigned long *)prop;
1730 if (desktop == 0xFFFFFFFF) {
1731 XFree(prop);
1732 return 1;
1733 }
1734 }
1735 }
1736 if (prop) XFree(prop);
1737
1738 // Also check _NET_WM_STATE for _NET_WM_STATE_STICKY
1739 prop = NULL;
1740 old = XSetErrorHandler(ignore_x_error);
1741 status = XGetWindowProperty(wm.dpy, window, _NET_WM_STATE, 0, (~0L), False, XA_ATOM, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
1742 XSync(wm.dpy, False);
1743 XSetErrorHandler(old);
1744
1745 if (status == Success) {
1746 if (prop && nitems > 0) {
1747 Atom *atoms = (Atom *)prop;
1748 for (unsigned long i = 0; i < nitems; i++) {
1749 if (atoms[i] == _NET_WM_STATE_STICKY) {
1750 XFree(prop);
1751 return 1;
1752 }
1753 }
1754 }
1755 }
1756 if (prop) XFree(prop);
1757
1758 return 0;
1759}
1760
1761void toggle_always_on_top(const Arg *arg) {
1762 (void)arg;
1763 if (wm.active == None) return;
1764
1765 int above = is_always_on_top(wm.active);
1766 update_wm_state(wm.active, _NET_WM_STATE_ABOVE, !above);
1767
1768 raise_window(wm.active);
1769 set_active_border(wm.active);
1770
1771 log_message(stdout, LOG_DEBUG, "Toggled always-on-top for window 0x%lx (now %s)", wm.active, !above ? "ON" : "OFF");
1772}
1773
1774void reload(const Arg *arg) {
1775 (void)arg;
1776 wm.running = 0;
1777 wm.restart = 1;
1778 log_message(stdout, LOG_DEBUG, "Reload window manager");
1779}
1780
1781void apply_tiling_layout(void) {
1782 if (wm.layout_modes[wm.current_desktop] != LAYOUT_TILING) return;
1783
1784 int n = 0;
1785 for (Client *c = wm.clients; c; c = c->next) {
1786 if (window_exists(c->window)) {
1787 unsigned long desktop;
1788 Atom actual_type;
1789 int actual_format;
1790 unsigned long nitems, bytes_after;
1791 unsigned char *prop = NULL;
1792
1793 int status = XGetWindowProperty(wm.dpy, c->window, _NET_WM_DESKTOP, 0, 1, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
1794 if (status == Success && prop && nitems > 0) {
1795 desktop = *(unsigned long *)prop;
1796 XFree(prop);
1797 if (desktop == wm.current_desktop && !is_sticky(c->window) && !is_always_on_top(c->window) && !has_wm_state(c->window, _NET_WM_STATE_FULLSCREEN)) {
1798 n++;
1799 }
1800 } else if (prop) {
1801 XFree(prop);
1802 }
1803 }
1804 }
1805
1806 if (n == 0) return;
1807
1808 int screen_width = DisplayWidth(wm.dpy, wm.screen);
1809 int screen_height = DisplayHeight(wm.dpy, wm.screen);
1810 int mw = (n > 1) ? screen_width / 3 : screen_width;
1811 int i = 0;
1812
1813 for (Client *c = wm.clients; c; c = c->next) {
1814 if (window_exists(c->window)) {
1815 unsigned long desktop;
1816 Atom actual_type;
1817 int actual_format;
1818 unsigned long nitems, bytes_after;
1819 unsigned char *prop = NULL;
1820
1821 int status = XGetWindowProperty(wm.dpy, c->window, _NET_WM_DESKTOP, 0, 1, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
1822 if (status == Success && prop && nitems > 0) {
1823 desktop = *(unsigned long *)prop;
1824 XFree(prop);
1825 if (desktop == wm.current_desktop && !is_sticky(c->window) && !is_always_on_top(c->window) && !has_wm_state(c->window, _NET_WM_STATE_FULLSCREEN)) {
1826 if (n == 1) {
1827 XMoveResizeWindow(wm.dpy, c->window, 0, 0, screen_width - 2 * border_size, screen_height - 2 * border_size);
1828 send_configure(c->window);
1829 } else if (i == 0) { // Master
1830 XMoveResizeWindow(wm.dpy, c->window, 0, 0, mw - 2 * border_size, screen_height - 2 * border_size);
1831 send_configure(c->window);
1832 } else { // Stack
1833 int h = screen_height / (n - 1);
1834 XMoveResizeWindow(wm.dpy, c->window, mw, (i - 1) * h, screen_width - mw - 2 * border_size, h - 2 * border_size);
1835 send_configure(c->window);
1836 }
1837 i++;
1838 }
1839 } else if (prop) {
1840 XFree(prop);
1841 }
1842 }
1843 }
1844}
1845
1846void toggle_layout(const Arg *arg) {
1847 (void)arg;
1848 LayoutMode current_mode = wm.layout_modes[wm.current_desktop];
1849
1850 if (current_mode == LAYOUT_FLOATING) {
1851 // Moving to TILING, save floating positions
1852 for (Client *c = wm.clients; c; c = c->next) {
1853 if (!window_exists(c->window)) continue;
1854
1855 unsigned long desktop;
1856 Atom actual_type;
1857 int actual_format;
1858 unsigned long nitems, bytes_after;
1859 unsigned char *prop = NULL;
1860
1861 int status = XGetWindowProperty(wm.dpy, c->window, _NET_WM_DESKTOP, 0, 1, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
1862 if (status == Success && prop && nitems > 0) {
1863 desktop = *(unsigned long *)prop;
1864 XFree(prop);
1865 if (desktop == wm.current_desktop && !is_sticky(c->window) && !is_always_on_top(c->window) && !has_wm_state(c->window, _NET_WM_STATE_FULLSCREEN)) {
1866 XWindowAttributes attr;
1867 if (XGetWindowAttributes(wm.dpy, c->window, &attr)) {
1868 c->saved_x = attr.x;
1869 c->saved_y = attr.y;
1870 c->saved_w = attr.width;
1871 c->saved_h = attr.height;
1872 c->has_saved_state = 1;
1873 }
1874 }
1875 } else if (prop) {
1876 XFree(prop);
1877 }
1878 }
1879 wm.layout_modes[wm.current_desktop] = LAYOUT_TILING;
1880 apply_tiling_layout();
1881 } else {
1882 // Moving to FLOATING, restore positions
1883 wm.layout_modes[wm.current_desktop] = LAYOUT_FLOATING;
1884 for (Client *c = wm.clients; c; c = c->next) {
1885 if (!window_exists(c->window)) continue;
1886 if (c->has_saved_state) {
1887 unsigned long desktop;
1888 Atom actual_type;
1889 int actual_format;
1890 unsigned long nitems, bytes_after;
1891 unsigned char *prop = NULL;
1892
1893 int status = XGetWindowProperty(wm.dpy, c->window, _NET_WM_DESKTOP, 0, 1, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
1894 if (status == Success && prop && nitems > 0) {
1895 desktop = *(unsigned long *)prop;
1896 XFree(prop);
1897 if (desktop == wm.current_desktop) {
1898 XMoveResizeWindow(wm.dpy, c->window, c->saved_x, c->saved_y, c->saved_w, c->saved_h);
1899 c->has_saved_state = 0;
1900 }
1901 } else if (prop) {
1902 XFree(prop);
1903 }
1904 }
1905 }
1906 }
1907 redraw_widgets();
1908 log_message(stdout, LOG_DEBUG, "Toggled layout for desktop %d to %s", wm.current_desktop, wm.layout_modes[wm.current_desktop] == LAYOUT_TILING ? "TILING" : "FLOATING");
1909}