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 *)&current_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}