summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--.vimrc63
-rw-r--r--LICENSE24
-rw-r--r--Makefile31
-rw-r--r--README.md230
-rw-r--r--config.def.h92
-rw-r--r--glitch.h164
-rw-r--r--logging.c71
-rw-r--r--main.c124
-rw-r--r--manager.c1584
-rw-r--r--switcher.c187
-rw-r--r--widgets.c66
12 files changed, 2640 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d37392c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+glitch
+tags
+config.h
+
diff --git a/.vimrc b/.vimrc
new file mode 100644
index 0000000..f333a42
--- /dev/null
+++ b/.vimrc
@@ -0,0 +1,63 @@
+let g:_executable = 'glitch'
+let g:_arguments = ''
+let g:_envs = { 'DISPLAY': ':69', 'LOG_LEVEL': '3', 'DEBUG': '1' }
+let g:_make = 'make -B'
+
+set makeprg=make
+set errorformat=%f:%l:%c:\ %m
+packadd termdebug
+
+let g:termdebug_config = {}
+let g:termdebug_config['variables_window'] = v:true
+
+nnoremap <leader>x :call LocalRun()<CR>
+nnoremap <leader>c :call LocalMake()<CR>
+nnoremap <leader>m :call LocalDebugMain()<CR>
+nnoremap <leader>l :call LocalDebugLine()<CR>
+
+function! LocalRun() abort
+ let envs = join( map(items(g:_envs), { _, kv -> kv[0] . '=' . kv[1] }), ' ')
+ execute printf("term env %s ./%s %s", envs, g:_executable, g:_arguments)
+endfunction
+
+function! LocalDebugMain() abort
+ execute printf('Termdebug %s %s', g:_executable, g:_arguments)
+
+ for [k, v] in items(g:_envs)
+ call TermDebugSendCommand(printf('set env %s %s', k, v))
+ endfor
+
+ call TermDebugSendCommand('directory ' . getcwd())
+ call TermDebugSendCommand('break main')
+ call TermDebugSendCommand('run')
+endfunction
+
+function! LocalDebugLine() abort
+ execute printf('Termdebug %s %s', g:_executable, g:_arguments)
+
+ for [k, v] in items(g:_envs)
+ call TermDebugSendCommand(printf('set env %s %s', k, v))
+ endfor
+
+ call TermDebugSendCommand('directory ' . getcwd())
+ call TermDebugSendCommand(printf('break %s:%d', expand('%:p'), line('.')))
+ call TermDebugSendCommand('run')
+endfunction
+
+function! LocalMake() abort
+ let envs = join( map(items(g:_envs), { _, kv -> kv[0] . '=' . kv[1] }), ' ')
+ execute printf('silent !env %s %s', g:_make, envs)
+
+ " Filter non valid errors out of quicklist.
+ let qfl = getqflist()
+ let filtered = filter(copy(qfl), {_, entry -> entry.valid == 1})
+ call setqflist(filtered, 'r')
+
+ redraw!
+
+ if len(filtered) > 0
+ execute exists(':CtrlPQuickfix') ? 'CtrlPQuickfix' : 'copen'
+ else
+ cclose
+ endif
+endfunction
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..31d811a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+BSD 2-Clause License
+
+Copyright (c) 2026, Mitja Felicijan <mitja.felicijan@gmail.com>
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6bd69ad
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,31 @@
+CC ?= clang
+CFLAGS := -std=c99 -pedantic -Wall -Wextra -Wunused -Wswitch-enum
+INCLUDES := $(shell pkg-config --cflags xft)
+LDFLAGS := $(shell pkg-config --libs x11 xft) -lpthread
+DESTDIR ?= /usr/local
+DISPLAY_NUM := 69
+
+ifdef DEBUG
+ CFLAGS += -ggdb -DDEBUG
+endif
+
+ifdef OPTIMIZE
+ CFLAGS += -O$(OPTIMIZE)
+endif
+
+all: glitch
+
+glitch: main.c logging.c manager.c widgets.c switcher.c
+ $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS)
+
+config.h:
+ [ -f config.h ] || cp config.def.h config.h
+
+install: all
+ install -Dm755 glitch $(DESTDIR)/usr/local/bin/glitch
+
+clean:
+ rm -f glitch
+
+virt:
+ Xephyr -br -ac -noreset -no-host-grab -sw-cursor -screen 1000x1000 :$(DISPLAY_NUM)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3764dd1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,230 @@
+Glitch is a minimal X11 window manager controlled by keyboard shortcuts.
+
+> [!IMPORTANT]
+> I built this window manager for personal use. There are no guarantees of
+> stability. Main purpose is to learn X11 and also to have a window manager that
+> is simple and that I can easily modify to my liking.
+
+## Key Features
+
+- **Window Movement**: Move windows by pixel values
+- **Window Resizing**: Resize windows
+- **Desktop Management**: Multiple desktops and moving windows between them
+- **Window Control**: Kill, fullscreen, PiP, and always-on-top
+- **Maximization**: Vertical and horizontal maximization
+- **Edge Snapping**: Snap windows to screen edges
+- **Window Centering**: Center windows on screen
+- **Live Reload**: Reload configuration without restart
+
+## Technical Details
+
+- Built on X11/Xlib for low-level window management
+- Uses EWMH (Extended Window Manager Hints) for fullscreen and state functionality
+- Maintains state for maximized windows to enable toggle behavior
+- Implements proper X11 event handling and window attribute management
+
+## Requirements
+
+- C compiler (GCC or Clang)
+- GNU Make
+- pkg-config
+- X11 and Freetype development libraries
+
+### Installing Dependencies
+
+**Void Linux:**
+
+```sh
+sudo xbps-install libX11-devel freetype-devel pkg-config
+```
+
+## Compilation
+
+```sh
+# Build normally
+make
+
+# Use a specific compiler
+CC=clang make
+CC=gcc make
+
+# Build with debug symbols
+DEBUG=1 make
+
+# Compile with optimization levels
+OPTIMIZE=2 make
+
+# Clean build
+make clean && make
+```
+
+### Testing in Virtual Display
+
+For safe testing without affecting your main session:
+
+```sh
+# Start Xephyr virtual display (requires Xephyr installed)
+make virt
+
+# In another terminal, run the window manager in the virtual display
+DISPLAY=:69 ./glitch
+```
+
+## Installation
+
+```sh
+# Install to /usr/local/bin by default
+sudo make install
+```
+
+## Running Glitch
+
+### Starting the Window Manager
+
+**From a display manager (login screen):**
+- Add Glitch to your display manager's session list
+- Select it from the session menu
+
+**From a terminal (if already in X11):**
+```sh
+# Exit current window manager first, then:
+./glitch
+```
+
+**From a TTY (text console):**
+```sh
+# Start X server and window manager
+startx ./glitch
+```
+
+## Configuration
+
+Glitch uses a simple configuration system based on C header files. The
+configuration is compiled into the binary, so you need to recompile after making
+changes.
+
+### Setting Up Configuration
+
+1. **Copy the default configuration**:
+ ```sh
+ cp config.def.h config.h
+ ```
+
+2. **Edit your configuration**:
+ ```sh
+ vim config.h # or your preferred editor
+ ```
+
+3. **Recompile the window manager**:
+ ```sh
+ make clean && make
+ ```
+
+4. **Restart or Reload**:
+ - Quit and restart, or
+ - Use `Mod+Shift+r` to reload in-place
+
+### Configuration Structure
+
+The configuration uses two main arrays:
+
+1. **`shortcuts[]`** - Maps keys to shell commands
+2. **`keybinds[]`** - Maps keys to window manager functions
+
+### Default Key Bindings
+
+Modifier key: `Mod4` (Super/Windows key)
+
+#### Window Movement
+
+```c
+{ MODKEY, XK_Left, move_window_x, { .i = -75 } },
+{ MODKEY, XK_Right, move_window_x, { .i = +75 } },
+{ MODKEY, XK_Up, move_window_y, { .i = -75 } },
+{ MODKEY, XK_Down, move_window_y, { .i = +75 } },
+{ MODKEY, XK_c, center_window, { 0 } },
+```
+
+#### Window Resizing
+
+```c
+{ MODKEY | ShiftMask, XK_Left, resize_window_x, { .i = -75 } },
+{ MODKEY | ShiftMask, XK_Right, resize_window_x, { .i = +75 } },
+{ MODKEY | ShiftMask, XK_Up, resize_window_y, { .i = -75 } },
+{ MODKEY | ShiftMask, XK_Down, resize_window_y, { .i = +75 } },
+```
+
+#### Desktop Management
+
+```c
+// Switch to desktop
+{ MODKEY, XK_1, goto_desktop, { .i = 1 } },
+// ... up to XK_9
+
+// Move window to desktop
+{ MODKEY | ShiftMask, XK_1, send_window_to_desktop, { .i = 1 } },
+// ... up to XK_9
+```
+
+#### Window Control
+
+```c
+{ MODKEY, XK_f, toggle_fullscreen, { 0 } },
+{ MODKEY, XK_q, close_window, { 0 } },
+{ MODKEY | ShiftMask, XK_q, quit, { 0 } },
+{ MODKEY | ShiftMask, XK_r, reload, { 0 } },
+{ MODKEY | ShiftMask, XK_s, toggle_pip, { 0 } },
+{ MODKEY | ShiftMask, XK_t, toggle_always_on_top,{ 0 } },
+{ Mod1Mask, XK_Tab, cycle_active_window, { .i = 0 } },
+```
+
+#### Window Maximization
+
+```c
+{ MODKEY, XK_x, window_hmaximize, { 0 } },
+{ MODKEY, XK_z, window_vmaximize, { 0 } },
+```
+
+#### Window Snapping
+
+```c
+{ MODKEY | ControlMask, XK_Up, window_snap_up, { 0 } },
+{ MODKEY | ControlMask, XK_Down, window_snap_down, { 0 } },
+{ MODKEY | ControlMask, XK_Right, window_snap_right, { 0 } },
+{ MODKEY | ControlMask, XK_Left, window_snap_left, { 0 } },
+```
+
+### Shell Commands
+
+Defined in `shortcuts[]` array:
+- `Mod+Return`: Terminal (st)
+- `Mod+p`: Application launcher (rofi)
+- `Mod+w`: Browser (brave)
+- `Mod+e`: File Manager (thunar)
+- `Mod+s`: Screen magnifier (xmagnify)
+- `Control+Escape`: Screenshot (maim)
+
+## Function Reference
+
+| Function | Category | Parameters | Description |
+| --- | --- | --- | --- |
+| `move_window_x` | Movement | `arg->i` (pixels) | Move window horizontally (positive = right) |
+| `move_window_y` | Movement | `arg->i` (pixels) | Move window vertically (positive = down) |
+| `resize_window_x` | Resize | `arg->i` (pixels) | Resize window width (positive = wider) |
+| `resize_window_y` | Resize | `arg->i` (pixels) | Resize window height (positive = taller) |
+| `center_window` | Movement | None | Center window on screen |
+| `window_snap_up` | Snap | None | Snap window to top edge |
+| `window_snap_down` | Snap | None | Snap window to bottom edge |
+| `window_snap_left` | Snap | None | Snap window to left edge |
+| `window_snap_right` | Snap | None | Snap window to right edge |
+| `goto_desktop` | Desktop | `arg->i` (desktop #) | Switch to specified desktop |
+| `send_window_to_desktop` | Desktop | `arg->i` (desktop #) | Move window to specified desktop |
+| `cycle_active_window` | Focus | `arg->i` (0=fwd, 1=back) | Cycle focus through windows |
+| `close_window` | Control | None | Gracefully close active window |
+| `quit` | Control | None | Exit the window manager |
+| `toggle_fullscreen` | Control | None | Toggle fullscreen mode |
+| `toggle_pip` | Control | None | Toggle Picture-in-Picture mode |
+| `toggle_always_on_top` | Control | None | Toggle Always-on-Top status |
+| `window_hmaximize` | Maximize | None | Toggle horizontal maximize |
+| `window_vmaximize` | Maximize | None | Toggle vertical maximize |
+| `reload` | System | None | Reload configuration/restart WM |
diff --git a/config.def.h b/config.def.h
new file mode 100644
index 0000000..dc7965d
--- /dev/null
+++ b/config.def.h
@@ -0,0 +1,92 @@
+// List of X11 keyboard symbol names.
+// https://cgit.freedesktop.org/xorg/proto/x11proto/tree/keysymdef.h
+// https://cgit.freedesktop.org/xorg/proto/x11proto/tree/XF86keysym.h
+
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-variable"
+
+#include "glitch.h"
+
+#define MODKEY Mod4Mask // Mod1Mask is Alt, Mod4Mask is Windows key.
+
+static int border_size = 3;
+static const char *active_border_color = "khaki";
+static const char *inactive_border_color = "darkgray";
+static const char *sticky_active_border_color = "violet";
+static const char *sticky_inactive_border_color = "cyan";
+static const char *on_top_active_border_color = "orange";
+static const char *on_top_inactive_border_color = "darkorange";
+
+static const char *widget_font = "Berkeley Mono:size=7:bold";
+static const char *indicator_fg_color = "white";
+static const char *indicator_bg_color = "blue";
+static const char *widget_fg_color = "#999999";
+static const char *time_format = "%A %d.%m.%Y %H:%M:%S";
+
+static Shortcut shortcuts[] = {
+ /* Mask KeySym Shell command */
+ { MODKEY, XK_Return, "st -f \"Berkeley Mono:style=Bold:size=10\" -g 80x40" },
+ { MODKEY, XK_p, "rofi -show drun -theme ~/.black.rasi" },
+ { ControlMask, XK_Escape, "sh -c 'maim -s | xclip -selection clipboard -t image/png'" },
+ { MODKEY, XK_w, "/home/m/Applications/brave --new-window" },
+ { MODKEY, XK_e, "thunar" },
+ { MODKEY, XK_s, "xmagnify -s 1000 -z 3" },
+ { MODKEY, XK_r, "simplescreenrecorder" },
+ { MODKEY, XK_l, "xlock" },
+ { 0, XF86XK_AudioLowerVolume, "pactl set-sink-volume @DEFAULT_SINK@ -5%" },
+ { 0, XF86XK_AudioRaiseVolume, "pactl set-sink-volume @DEFAULT_SINK@ +5%" },
+ { 0, XF86XK_AudioMute, "pactl set-sink-mute @DEFAULT_SINK@ toggle" },
+ { MODKEY, XK_bracketright, "pats -t" },
+};
+
+static Keybinds keybinds[] = {
+ /* Mask KeySym Function Argument */
+ { Mod1Mask, XK_Tab, cycle_active_window, { .i = 0 } },
+ { Mod1Mask | ShiftMask, XK_Tab, cycle_active_window, { .i = 1 } },
+ { MODKEY, XK_Left, move_window_x, { .i = -75 } },
+ { MODKEY, XK_Right, move_window_x, { .i = +75 } },
+ { MODKEY, XK_Up, move_window_y, { .i = -75 } },
+ { MODKEY, XK_Down, move_window_y, { .i = +75 } },
+ { MODKEY | ShiftMask, XK_Left, resize_window_x, { .i = -75 } },
+ { MODKEY | ShiftMask, XK_Right, resize_window_x, { .i = +75 } },
+ { MODKEY | ShiftMask, XK_Up, resize_window_y, { .i = -75 } },
+ { MODKEY | ShiftMask, XK_Down, resize_window_y, { .i = +75 } },
+ { MODKEY | ControlMask, XK_Up, window_snap_up, { 0 } },
+ { MODKEY | ControlMask, XK_Down, window_snap_down, { 0 } },
+ { MODKEY | ControlMask, XK_Right, window_snap_right, { 0 } },
+ { MODKEY | ControlMask, XK_Left, window_snap_left, { 0 } },
+ { MODKEY, XK_1, goto_desktop, { .i = 1 } },
+ { MODKEY, XK_2, goto_desktop, { .i = 2 } },
+ { MODKEY, XK_3, goto_desktop, { .i = 3 } },
+ { MODKEY, XK_4, goto_desktop, { .i = 4 } },
+ { MODKEY, XK_5, goto_desktop, { .i = 5 } },
+ { MODKEY, XK_6, goto_desktop, { .i = 6 } },
+ { MODKEY, XK_7, goto_desktop, { .i = 7 } },
+ { MODKEY, XK_8, goto_desktop, { .i = 8 } },
+ { MODKEY, XK_9, goto_desktop, { .i = 9 } },
+ { MODKEY | ShiftMask, XK_1, send_window_to_desktop, { .i = 1 } },
+ { MODKEY | ShiftMask, XK_2, send_window_to_desktop, { .i = 2 } },
+ { MODKEY | ShiftMask, XK_3, send_window_to_desktop, { .i = 3 } },
+ { MODKEY | ShiftMask, XK_4, send_window_to_desktop, { .i = 4 } },
+ { MODKEY | ShiftMask, XK_5, send_window_to_desktop, { .i = 5 } },
+ { MODKEY | ShiftMask, XK_6, send_window_to_desktop, { .i = 6 } },
+ { MODKEY | ShiftMask, XK_7, send_window_to_desktop, { .i = 7 } },
+ { MODKEY | ShiftMask, XK_8, send_window_to_desktop, { .i = 8 } },
+ { MODKEY | ShiftMask, XK_9, send_window_to_desktop, { .i = 9 } },
+ { MODKEY | ShiftMask, XK_s, toggle_pip, { 0 } },
+ { MODKEY | ShiftMask, XK_t, toggle_always_on_top,{ 0 } },
+ { MODKEY, XK_x, window_hmaximize, { 0 } },
+ { MODKEY, XK_z, window_vmaximize, { 0 } },
+ { MODKEY, XK_f, toggle_fullscreen, { 0 } },
+ { MODKEY | ShiftMask, XK_r, reload, { 0 } },
+ { MODKEY, XK_c, center_window, { 0 } },
+ { MODKEY | ShiftMask, XK_q, quit, { 0 } },
+ { MODKEY, XK_q, close_window, { 0 } },
+};
+
+#pragma GCC diagnostic pop
+
+#endif // CONFIG_H
diff --git a/glitch.h b/glitch.h
new file mode 100644
index 0000000..d8b9166
--- /dev/null
+++ b/glitch.h
@@ -0,0 +1,164 @@
+#ifndef GLITCH_H
+#define GLITCH_H
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include <X11/Xlib.h>
+#include <X11/keysym.h>
+#include <X11/XF86keysym.h>
+#include <X11/Xft/Xft.h>
+
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#define LENGTH(x) (sizeof(x) / sizeof((x)[0]))
+
+#define NUM_DESKTOPS 9
+
+extern Atom _NET_WM_DESKTOP;
+
+#define COLOR_INFO "\x1B[0m" // White
+#define COLOR_DEBUG "\x1B[36m" // Cyan
+#define COLOR_WARNING "\x1B[33m" // Yellow
+#define COLOR_ERROR "\x1B[31m" // Red
+#define COLOR_RESET "\x1B[0m"
+
+typedef enum {
+ LOG_INFO,
+ LOG_DEBUG,
+ LOG_WARNING,
+ LOG_ERROR,
+} LogLevel;
+
+typedef struct {
+ unsigned long normal_active;
+ unsigned long normal_inactive;
+ unsigned long sticky_active;
+ unsigned long sticky_inactive;
+ unsigned long on_top_active;
+ unsigned long on_top_inactive;
+} Borders;
+
+typedef struct Client {
+ Window window;
+ struct Client *next;
+ struct Client *prev;
+} Client;
+
+typedef struct {
+ Display *dpy;
+ Window root;
+ Window active;
+ int screen;
+ XEvent ev;
+ XButtonEvent start;
+ XWindowAttributes attr;
+
+ Cursor cursor_default;
+ Cursor cursor_move;
+ Cursor cursor_resize;
+
+ Colormap cmap;
+ Borders borders;
+
+ int running;
+ int restart;
+
+ unsigned int current_desktop;
+ XftFont *font;
+ XftDraw *xft_draw;
+ XftColor xft_color;
+ XftColor xft_bg_color;
+ XftColor xft_root_bg_color;
+ XftColor xft_widget_color;
+
+ unsigned long last_widget_update;
+ Client *clients;
+
+ int is_cycling;
+ Window cycle_win;
+ Window *cycle_clients;
+ int cycle_count;
+ int active_cycle_index;
+} WindowManager;
+
+typedef struct {
+ int i;
+ const char *s;
+} Arg;
+
+typedef struct {
+ unsigned int mod;
+ KeySym keysym;
+ void (*func)(const Arg *);
+ Arg arg;
+} Keybinds;
+
+typedef struct {
+ unsigned int mod;
+ KeySym keysym;
+ const char *cmd;
+} Shortcut;
+
+void set_log_level(LogLevel level);
+LogLevel get_log_level_from_env(void);
+void log_message(FILE *stream, LogLevel level, const char* format, ...);
+
+void init_window_manager(void);
+void deinit_window_manager(void);
+void handle_map_request(void);
+void handle_unmap_notify(void);
+void handle_destroy_notify(void);
+void handle_property_notify(void);
+void handle_motion_notify(void);
+void handle_client_message(void);
+void handle_button_press(void);
+void handle_button_release(void);
+void handle_key_press(void);
+void handle_key_release(void);
+void handle_focus_in(void);
+void handle_focus_out(void);
+void handle_enter_notify(void);
+void handle_expose(void);
+void handle_configure_request(void);
+
+Window get_active_window(void);
+void set_active_window(Window window, Time time);
+void set_active_border(Window window);
+void grab_buttons(Window window);
+void get_cursor_offset(Window window, int *dx, int *dy);
+int window_exists(Window window);
+int ignore_x_error(Display *dpy, XErrorEvent *err);
+
+void move_window_x(const Arg *arg);
+void move_window_y(const Arg *arg);
+void resize_window_x(const Arg *arg);
+void resize_window_y(const Arg *arg);
+void window_snap_up(const Arg *arg);
+void window_snap_down(const Arg *arg);
+void window_snap_left(const Arg *arg);
+void window_snap_right(const Arg *arg);
+void window_hmaximize(const Arg *arg);
+void window_vmaximize(const Arg *arg);
+void close_window(const Arg *arg);
+
+void quit(const Arg *arg);
+void reload(const Arg *arg);
+
+void goto_desktop(const Arg *arg);
+void send_window_to_desktop(const Arg *arg);
+void toggle_pip(const Arg *arg);
+int is_sticky(Window window);
+int is_always_on_top(Window window);
+void update_client_list(void);
+void toggle_always_on_top(const Arg *arg);
+void execute_shortcut(const char *command);
+void cycle_active_window(const Arg *arg);
+void end_cycling(void);
+void toggle_fullscreen(const Arg *arg);
+void center_window(const Arg *arg);
+
+void widget_desktop_indicator(void);
+void widget_datetime(void);
+void redraw_widgets(void);
+
+#endif // GLITCH_H
diff --git a/logging.c b/logging.c
new file mode 100644
index 0000000..4f383c1
--- /dev/null
+++ b/logging.c
@@ -0,0 +1,71 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <sys/time.h>
+
+#include "glitch.h"
+
+static LogLevel max_level = LOG_INFO;
+
+static const char* level_strings[] = {
+ "INFO",
+ "DEBUG",
+ "WARN",
+ "ERROR",
+};
+
+static const char* level_colors[] = {
+ COLOR_INFO,
+ COLOR_DEBUG,
+ COLOR_WARNING,
+ COLOR_ERROR,
+};
+
+void set_log_level(LogLevel level) {
+ max_level = level;
+}
+
+LogLevel get_log_level_from_env(void) {
+ const char *env = getenv("LOG_LEVEL");
+ if (env) {
+ int level = atoi(env);
+ if (level >= 0 && level <= 3) {
+ return (LogLevel)level;
+ }
+ }
+
+ return max_level;
+}
+
+void log_message(FILE *stream, LogLevel level, const char* format, ...) {
+ if (max_level < level) return;
+
+ struct timeval tv;
+ gettimeofday(&tv, NULL);
+ struct tm* tm_info = localtime(&tv.tv_sec);
+
+ char time_str[24];
+ strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info);
+
+ const char *color = isatty(fileno(stream)) ? level_colors[level] : "";
+ const char *reset = isatty(fileno(stream)) ? COLOR_RESET : "";
+
+ const char* log_format = "%s[%s.%03d] [%-5s] ";
+ fprintf(stream, log_format,
+ color,
+ time_str,
+ (int)(tv.tv_usec / 1000),
+ level_strings[level]);
+
+ va_list args;
+ va_start(args, format);
+ vfprintf(stream, format, args);
+ va_end(args);
+
+ fprintf(stream, "%s\n", reset);
+ fflush(stream);
+}
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..9453557
--- /dev/null
+++ b/main.c
@@ -0,0 +1,124 @@
+#include <pthread.h>
+#include <unistd.h>
+
+#include "glitch.h"
+
+WindowManager wm = {0};
+
+static void* expose_timer_thread(void* arg) {
+ (void)arg;
+
+ for(;;) {
+ sleep(1);
+
+ if (wm.dpy != NULL) {
+ XEvent event = {0};
+ event.type = Expose;
+ event.xexpose.window = wm.root;
+ event.xexpose.x = 0;
+ event.xexpose.y = 0;
+ event.xexpose.width = 1;
+ event.xexpose.height = 1;
+ event.xexpose.count = 0;
+
+ // This is thread-safe - XSendEvent is designed for this.
+ XSendEvent(wm.dpy, wm.root, False, ExposureMask, &event);
+ XFlush(wm.dpy);
+ }
+ }
+ return NULL;
+}
+
+int main(int argc, char *argv[]) {
+ (void)argc;
+ set_log_level(get_log_level_from_env());
+
+ // Initialize X11 threading support.
+ if (!XInitThreads()) {
+ log_message(stderr, LOG_ERROR, "XInitThreads failed");
+ return 1;
+ }
+
+ init_window_manager();
+
+ // Starts Expose ticker for updating widgets.
+ pthread_t timer_tid;
+ if (pthread_create(&timer_tid, NULL, expose_timer_thread, NULL) != 0) {
+ log_message(stderr, LOG_ERROR, "failed to create timer thread");
+ } else {
+ pthread_detach(timer_tid);
+ }
+
+ wm.running = 1;
+ while(wm.running) {
+ XNextEvent(wm.dpy, &wm.ev);
+
+ switch (wm.ev.type) {
+ case MapRequest:
+ handle_map_request();
+ break;
+ case UnmapNotify:
+ handle_unmap_notify();
+ break;
+ case DestroyNotify:
+ handle_destroy_notify();
+ break;
+ case PropertyNotify:
+ handle_property_notify();
+ break;
+ case MotionNotify:
+ // Compress MotionNotify events.
+ while (XCheckTypedEvent(wm.dpy, MotionNotify, &wm.ev));
+ handle_motion_notify();
+ break;
+ case ClientMessage:
+ {
+ static Atom redraw_atom = None;
+ if (redraw_atom == None) redraw_atom = XInternAtom(wm.dpy, "GLITCH_WIDGET_REDRAW", False);
+
+ if (wm.ev.xclient.message_type == redraw_atom) {
+ redraw_widgets();
+ } else {
+ handle_client_message();
+ }
+ }
+ break;
+ case ButtonPress:
+ handle_button_press();
+ break;
+ case ButtonRelease:
+ handle_button_release();
+ break;
+ case KeyPress:
+ handle_key_press();
+ break;
+ case KeyRelease:
+ handle_key_release();
+ break;
+ case FocusIn:
+ handle_focus_in();
+ break;
+ case FocusOut:
+ handle_focus_out();
+ break;
+ case EnterNotify:
+ handle_enter_notify();
+ break;
+ case Expose:
+ handle_expose();
+ break;
+ case ConfigureRequest:
+ handle_configure_request();
+ break;
+ }
+ }
+
+ deinit_window_manager();
+
+ if (wm.restart) {
+ execvp(argv[0], argv);
+ perror("execvp");
+ }
+
+ return 0;
+}
diff --git a/manager.c b/manager.c
new file mode 100644
index 0000000..1d628ba
--- /dev/null
+++ b/manager.c
@@ -0,0 +1,1584 @@
+#define _POSIX_C_SOURCE 200809L
+#include <stdlib.h>
+#include <time.h>
+#include <string.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xatom.h>
+#include <X11/keysym.h>
+#include <X11/XF86keysym.h>
+#include <X11/cursorfont.h>
+#include <X11/Xproto.h>
+
+#include "glitch.h"
+#include "config.h"
+
+extern WindowManager wm;
+
+Atom _NET_WM_DESKTOP;
+static Atom _NET_CURRENT_DESKTOP;
+static Atom _NET_NUMBER_OF_DESKTOPS;
+static Atom _NET_CLIENT_LIST;
+static Atom _NET_WM_STATE;
+static Atom _NET_WM_NAME;
+static Atom _NET_SUPPORTING_WM_CHECK;
+static Atom _NET_WM_STATE_FULLSCREEN;
+static Atom _NET_ACTIVE_WINDOW;
+static Atom _NET_WM_STATE_STICKY;
+static Atom _NET_WM_STATE_MAXIMIZED_HORZ;
+static Atom _NET_WM_STATE_MAXIMIZED_VERT;
+static Atom _NET_WM_STATE_ABOVE;
+static Atom _GLITCH_PRE_HMAX_GEOM;
+static Atom _GLITCH_PRE_VMAX_GEOM;
+static Atom _GLITCH_PRE_FULLSCREEN_GEOM;
+static Atom _MOTIF_WM_HINTS;
+static Atom WM_PROTOCOLS;
+static Atom WM_DELETE_WINDOW;
+static Atom WM_TAKE_FOCUS;
+static Atom _NET_SUPPORTED;
+
+static void update_wm_state(Window w, Atom state_atom, int add);
+static int has_wm_state(Window w, Atom state_atom);
+static void check_and_clear_maximized_state(Window w, int horizontal, int vertical);
+static void add_client(Window w);
+static void remove_client(Window w);
+static Window get_toplevel_window(Window w);
+static void set_fullscreen(Window w, int full);
+
+int x_error_handler(Display *dpy, XErrorEvent *ee) {
+ (void) dpy;
+
+ if (ee->error_code == BadWindow ||
+ (ee->request_code == X_SetInputFocus && ee->error_code == BadMatch) ||
+ (ee->request_code == X_PolyText8 && ee->error_code == BadDrawable) ||
+ (ee->request_code == X_PolyFillRectangle && ee->error_code == BadDrawable) ||
+ (ee->request_code == X_PolySegment && ee->error_code == BadDrawable) ||
+ (ee->request_code == X_ConfigureWindow && ee->error_code == BadMatch) ||
+ (ee->request_code == X_GrabButton && ee->error_code == BadAccess) ||
+ (ee->request_code == X_GrabKey && ee->error_code == BadAccess) ||
+ (ee->request_code == X_CopyArea && ee->error_code == BadDrawable)) {
+ return 0;
+ }
+ 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);
+ return 0;
+}
+
+static void add_client(Window w) {
+ // Check if already in list or is root.
+ if (w == wm.root) return;
+ Client *c = wm.clients;
+ while (c) {
+ if (c->window == w) return;
+ c = c->next;
+ }
+
+ Client *new_c = malloc(sizeof(Client));
+ if (!new_c) return;
+
+ new_c->window = w;
+ new_c->next = wm.clients;
+ new_c->prev = NULL;
+
+ if (wm.clients) {
+ wm.clients->prev = new_c;
+ }
+ wm.clients = new_c;
+ log_message(stdout, LOG_DEBUG, "Added client 0x%lx", w);
+}
+
+static void remove_client(Window w) {
+ Client *c = wm.clients;
+ while (c) {
+ if (c->window == w) {
+ if (c->prev) {
+ c->prev->next = c->next;
+ } else {
+ wm.clients = c->next;
+ }
+
+ if (c->next) {
+ c->next->prev = c->prev;
+ }
+
+ free(c);
+ log_message(stdout, LOG_DEBUG, "Removed client 0x%lx", w);
+ return;
+ }
+ c = c->next;
+ }
+}
+
+static Window get_toplevel_window(Window w) {
+ if (w == None || w == wm.root) return None;
+
+ Client *c = wm.clients;
+ while (c) {
+ if (c->window == w) return w;
+ c = c->next;
+ }
+
+ Window root, parent, *children;
+ unsigned int nchildren;
+ if (XQueryTree(wm.dpy, w, &root, &parent, &children, &nchildren)) {
+ if (children) XFree(children);
+ if (parent == root || parent == None) return None;
+ return get_toplevel_window(parent);
+ }
+
+ return None;
+}
+
+static void scan_windows(void) {
+ unsigned int nwins;
+ Window d1, d2, *wins;
+ XWindowAttributes wa;
+
+ if (XQueryTree(wm.dpy, wm.root, &d1, &d2, &wins, &nwins)) {
+ for (unsigned int i = 0; i < nwins; i++) {
+ if (XGetWindowAttributes(wm.dpy, wins[i], &wa)
+ && !wa.override_redirect && (wa.map_state == IsViewable || wa.map_state == IsUnmapped)) {
+ add_client(wins[i]);
+ XSelectInput(wm.dpy, wins[i], EnterWindowMask | LeaveWindowMask);
+ grab_buttons(wins[i]);
+
+ XSetWindowBorderWidth(wm.dpy, wins[i], border_size);
+ if (is_sticky(wins[i])) {
+ XSetWindowBorder(wm.dpy, wins[i], wm.borders.sticky_inactive);
+ } else if (is_always_on_top(wins[i])) {
+ XSetWindowBorder(wm.dpy, wins[i], wm.borders.on_top_inactive);
+ } else {
+ XSetWindowBorder(wm.dpy, wins[i], wm.borders.normal_inactive);
+ }
+ }
+ }
+ if (wins) XFree(wins);
+ }
+
+ // Restore focus.
+ Window focus_win;
+ int revert_to;
+ XGetInputFocus(wm.dpy, &focus_win, &revert_to);
+ if (focus_win != None && focus_win != wm.root) {
+ Window toplevel = get_toplevel_window(focus_win);
+ if (toplevel != None && window_exists(toplevel)) {
+ set_active_window(toplevel, CurrentTime);
+ set_active_border(toplevel);
+ }
+ }
+}
+
+void init_window_manager(void) {
+ wm.dpy = XOpenDisplay(NULL);
+ if (!wm.dpy) {
+ log_message(stdout, LOG_ERROR, "Cannot open display");
+ abort();
+ }
+
+ XSetErrorHandler(x_error_handler);
+
+ wm.screen = DefaultScreen(wm.dpy);
+ wm.root = RootWindow(wm.dpy, wm.screen);
+ XSetWindowBackground(wm.dpy, wm.root, BlackPixel(wm.dpy, wm.screen));
+ XClearWindow(wm.dpy, wm.root);
+
+ // Create and sets up cursors.
+ wm.cursor_default = XCreateFontCursor(wm.dpy, XC_left_ptr);
+ wm.cursor_move = XCreateFontCursor(wm.dpy, XC_fleur);
+ wm.cursor_resize = XCreateFontCursor(wm.dpy, XC_sizing);
+ XDefineCursor(wm.dpy, wm.root, wm.cursor_default);
+ log_message(stdout, LOG_DEBUG, "Setting up default cursors");
+
+ // Root window input selection masks.
+ XSelectInput(wm.dpy, wm.root,
+ SubstructureRedirectMask | SubstructureNotifyMask |
+ FocusChangeMask | EnterWindowMask | LeaveWindowMask |
+ ButtonPressMask | ExposureMask | PropertyChangeMask);
+
+ // Initialize EWMH atoms.
+ _NET_WM_DESKTOP = XInternAtom(wm.dpy, "_NET_WM_DESKTOP", False);
+ _NET_CURRENT_DESKTOP = XInternAtom(wm.dpy, "_NET_CURRENT_DESKTOP", False);
+ _NET_NUMBER_OF_DESKTOPS = XInternAtom(wm.dpy, "_NET_NUMBER_OF_DESKTOPS", False);
+ _NET_CLIENT_LIST = XInternAtom(wm.dpy, "_NET_CLIENT_LIST", False);
+ _NET_WM_STATE = XInternAtom(wm.dpy, "_NET_WM_STATE", False);
+ _NET_WM_STATE_FULLSCREEN = XInternAtom(wm.dpy, "_NET_WM_STATE_FULLSCREEN", False);
+ _NET_ACTIVE_WINDOW = XInternAtom(wm.dpy, "_NET_ACTIVE_WINDOW", False);
+ _NET_WM_STATE_STICKY = XInternAtom(wm.dpy, "_NET_WM_STATE_STICKY", False);
+ _NET_WM_STATE_MAXIMIZED_HORZ = XInternAtom(wm.dpy, "_NET_WM_STATE_MAXIMIZED_HORZ", False);
+ _NET_WM_STATE_MAXIMIZED_VERT = XInternAtom(wm.dpy, "_NET_WM_STATE_MAXIMIZED_VERT", False);
+ _NET_WM_STATE_ABOVE = XInternAtom(wm.dpy, "_NET_WM_STATE_ABOVE", False);
+ _NET_SUPPORTED = XInternAtom(wm.dpy, "_NET_SUPPORTED", False);
+ _NET_SUPPORTING_WM_CHECK = XInternAtom(wm.dpy, "_NET_SUPPORTING_WM_CHECK", False);
+ _NET_WM_NAME = XInternAtom(wm.dpy, "_NET_WM_NAME", False);
+ _GLITCH_PRE_HMAX_GEOM = XInternAtom(wm.dpy, "_GLITCH_PRE_HMAX_GEOM", False);
+ _GLITCH_PRE_VMAX_GEOM = XInternAtom(wm.dpy, "_GLITCH_PRE_VMAX_GEOM", False);
+ _GLITCH_PRE_FULLSCREEN_GEOM = XInternAtom(wm.dpy, "_GLITCH_PRE_FULLSCREEN_GEOM", False);
+ _MOTIF_WM_HINTS = XInternAtom(wm.dpy, "_MOTIF_WM_HINTS", False);
+ WM_PROTOCOLS = XInternAtom(wm.dpy, "WM_PROTOCOLS", False);
+ WM_DELETE_WINDOW = XInternAtom(wm.dpy, "WM_DELETE_WINDOW", False);
+ WM_TAKE_FOCUS = XInternAtom(wm.dpy, "WM_TAKE_FOCUS", False);
+
+ // Create supporting window for EWMH compliance.
+ Window check_win = XCreateSimpleWindow(wm.dpy, wm.root, 0, 0, 1, 1, 0, 0, 0);
+ XChangeProperty(wm.dpy, check_win, _NET_SUPPORTING_WM_CHECK, XA_WINDOW, 32, PropModeReplace, (unsigned char *)&check_win, 1);
+ XChangeProperty(wm.dpy, check_win, _NET_WM_NAME, XA_STRING, 8, PropModeReplace, (unsigned char *)"glitch", 6);
+ XChangeProperty(wm.dpy, wm.root, _NET_SUPPORTING_WM_CHECK, XA_WINDOW, 32, PropModeReplace, (unsigned char *)&check_win, 1);
+ XChangeProperty(wm.dpy, wm.root, _NET_WM_NAME, XA_STRING, 8, PropModeReplace, (unsigned char *)"glitch", 6);
+
+ // Set supported atoms.
+ Atom net_atoms[] = {
+ _NET_SUPPORTED,
+ _NET_SUPPORTING_WM_CHECK,
+ _NET_WM_NAME,
+ _NET_WM_DESKTOP,
+ _NET_CURRENT_DESKTOP,
+ _NET_NUMBER_OF_DESKTOPS,
+ _NET_CLIENT_LIST,
+ _NET_WM_STATE,
+ _NET_WM_STATE_FULLSCREEN,
+ _NET_ACTIVE_WINDOW,
+ _NET_WM_STATE_STICKY,
+ _NET_WM_STATE_MAXIMIZED_HORZ,
+ _NET_WM_STATE_MAXIMIZED_VERT,
+ _NET_WM_STATE_ABOVE,
+ WM_DELETE_WINDOW,
+ WM_TAKE_FOCUS
+ };
+ XChangeProperty(wm.dpy, wm.root, _NET_SUPPORTED, XA_ATOM, 32, PropModeReplace, (unsigned char *)net_atoms, LENGTH(net_atoms));
+
+ // Set number of desktops and current desktop.
+ static unsigned long num_desktops = NUM_DESKTOPS;
+ static unsigned long current_desktop = 1;
+ wm.current_desktop = 1;
+ XChangeProperty(wm.dpy, wm.root, _NET_NUMBER_OF_DESKTOPS, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&num_desktops, 1);
+ XChangeProperty(wm.dpy, wm.root, _NET_CURRENT_DESKTOP, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&current_desktop, 1);
+ log_message(stdout, LOG_DEBUG, "Registering %d desktops", NUM_DESKTOPS);
+
+ // Initialize colormap early as it's needed for Xft.
+ wm.cmap = DefaultColormap(wm.dpy, wm.screen);
+
+ // Setup Xft font and drawing.
+ wm.font = XftFontOpenName(wm.dpy, wm.screen, widget_font);
+ if (!wm.font) {
+ log_message(stdout, LOG_WARNING, "Failed to load font %s, falling back to fixed", widget_font);
+ wm.font = XftFontOpenName(wm.dpy, wm.screen, "fixed");
+ }
+
+ Visual *visual = DefaultVisual(wm.dpy, wm.screen);
+
+ // Create XftDraw for the root window.
+ wm.xft_draw = XftDrawCreate(wm.dpy, wm.root, visual, wm.cmap);
+
+ if (!XftColorAllocName(wm.dpy, visual, wm.cmap, indicator_fg_color, &wm.xft_color)) {
+ log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to white", indicator_fg_color);
+ XRenderColor render_color = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
+ XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_color);
+ }
+
+ if (!XftColorAllocName(wm.dpy, visual, wm.cmap, indicator_bg_color, &wm.xft_bg_color)) {
+ log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to black", indicator_bg_color);
+ XRenderColor render_color = {0x0000, 0x0000, 0x0000, 0xFFFF};
+ XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_bg_color);
+ }
+
+ // Root background color (black) for widgets to blend in.
+ XRenderColor black_render = {0x0000, 0x0000, 0x0000, 0xFFFF};
+ XftColorAllocValue(wm.dpy, visual, wm.cmap, &black_render, &wm.xft_root_bg_color);
+
+ if (!XftColorAllocName(wm.dpy, visual, wm.cmap, widget_fg_color, &wm.xft_widget_color)) {
+ log_message(stdout, LOG_WARNING, "Failed to allocate color %s, falling back to gray", widget_fg_color);
+ XRenderColor render_color = {0x8888, 0x8888, 0x8888, 0xFFFF};
+ XftColorAllocValue(wm.dpy, visual, wm.cmap, &render_color, &wm.xft_widget_color);
+ }
+
+ wm.running = 1;
+
+ scan_windows();
+
+ // Grab keys for keybinds.
+ for (unsigned int i = 0; i < LENGTH(keybinds); i++) {
+ KeyCode keycode = XKeysymToKeycode(wm.dpy, keybinds[i].keysym);
+ if (keycode) {
+ XGrabKey(wm.dpy, keycode, keybinds[i].mod, wm.root, True, GrabModeAsync, GrabModeAsync);
+ log_message(stdout, LOG_DEBUG, "Grabbed key: mod=0x%x, keysym=0x%lx", keybinds[i].mod, keybinds[i].keysym);
+ }
+ }
+
+ // Grab keys for shortcuts.
+ for (unsigned int i = 0; i < LENGTH(shortcuts); i++) {
+ KeyCode keycode = XKeysymToKeycode(wm.dpy, shortcuts[i].keysym);
+ if (keycode) {
+ XGrabKey(wm.dpy, keycode, shortcuts[i].mod, wm.root, True, GrabModeAsync, GrabModeAsync);
+ log_message(stdout, LOG_DEBUG, "Grabbed shortcut: mod=0x%x, keysym=0x%lx, command=%s", shortcuts[i].mod, shortcuts[i].keysym, shortcuts[i].cmd);
+ }
+ }
+
+ // Grab keys for window dragging (with MODKEY).
+ XGrabButton(wm.dpy, 1, MODKEY, wm.root, True, ButtonPressMask|ButtonReleaseMask|PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None);
+ XGrabButton(wm.dpy, 3, MODKEY, wm.root, True, ButtonPressMask|ButtonReleaseMask|PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None);
+ log_message(stdout, LOG_DEBUG, "Registering grab keys for window dragging");
+
+ // Prepare border colors.
+ XColor active_color, inactive_color, sticky_active_color, sticky_inactive_color, dummy;
+
+ wm.borders.normal_active = BlackPixel(wm.dpy, wm.screen);
+ wm.borders.normal_inactive = BlackPixel(wm.dpy, wm.screen);
+ wm.borders.sticky_active = BlackPixel(wm.dpy, wm.screen);
+ wm.borders.sticky_inactive = BlackPixel(wm.dpy, wm.screen);
+
+ if (XAllocNamedColor(wm.dpy, wm.cmap, active_border_color, &active_color, &dummy)) {
+ wm.borders.normal_active = active_color.pixel;
+ }
+
+ if (XAllocNamedColor(wm.dpy, wm.cmap, inactive_border_color, &inactive_color, &dummy)) {
+ wm.borders.normal_inactive = inactive_color.pixel;
+ }
+
+ if (XAllocNamedColor(wm.dpy, wm.cmap, sticky_active_border_color, &sticky_active_color, &dummy)) {
+ wm.borders.sticky_active = sticky_active_color.pixel;
+ }
+
+ if (XAllocNamedColor(wm.dpy, wm.cmap, sticky_inactive_border_color, &sticky_inactive_color, &dummy)) {
+ wm.borders.sticky_inactive = sticky_inactive_color.pixel;
+ }
+
+ XColor on_top_active, on_top_inactive;
+ wm.borders.on_top_active = BlackPixel(wm.dpy, wm.screen);
+ wm.borders.on_top_inactive = BlackPixel(wm.dpy, wm.screen);
+
+ if (XAllocNamedColor(wm.dpy, wm.cmap, on_top_active_border_color, &on_top_active, &dummy)) {
+ wm.borders.on_top_active = on_top_active.pixel;
+ }
+ if (XAllocNamedColor(wm.dpy, wm.cmap, on_top_inactive_border_color, &on_top_inactive, &dummy)) {
+ wm.borders.on_top_inactive = on_top_inactive.pixel;
+ }
+
+ // Scan for existing windows.
+ unsigned int nchildren;
+ Window root_return, parent_return, *children;
+ XWindowAttributes wa;
+
+ if (XQueryTree(wm.dpy, wm.root, &root_return, &parent_return, &children, &nchildren)) {
+ for (unsigned int i = 0; i < nchildren; i++) {
+ if (!XGetWindowAttributes(wm.dpy, children[i], &wa) || wa.override_redirect)
+ continue;
+ if (wa.map_state == IsViewable) {
+ grab_buttons(children[i]);
+ XSelectInput(wm.dpy, children[i], EnterWindowMask | LeaveWindowMask);
+ add_client(children[i]);
+ log_message(stdout, LOG_DEBUG, "Grabbed existing window 0x%lx", children[i]);
+ }
+ }
+ if (children) XFree(children);
+ }
+ redraw_widgets();
+ update_client_list();
+ XSync(wm.dpy, False);
+}
+
+void execute_shortcut(const char *command) {
+ if (!command || strlen(command) == 0) {
+ log_message(stdout, LOG_WARNING, "Empty command provided to execute_shortcut");
+ return;
+ }
+
+ pid_t pid = fork();
+ if (pid == -1) {
+ log_message(stdout, LOG_ERROR, "Failed to fork process for command: %s", command);
+ return;
+ }
+
+ if (pid == 0) {
+ if (wm.dpy) close(ConnectionNumber(wm.dpy));
+ setsid();
+ execl("/bin/sh", "sh", "-c", command, (char *)NULL);
+ log_message(stderr, LOG_ERROR, "Failed to execute command: %s", command);
+ exit(1);
+ } else {
+ log_message(stdout, LOG_DEBUG, "Executed command in background: %s", command);
+ }
+}
+
+void deinit_window_manager(void) {
+ XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_color);
+ XftColorFree(wm.dpy, DefaultVisual(wm.dpy, wm.screen), wm.cmap, &wm.xft_bg_color);
+ XftDrawDestroy(wm.xft_draw);
+
+ XftFontClose(wm.dpy, wm.font);
+ XFreeCursor(wm.dpy, wm.cursor_default);
+ XFreeCursor(wm.dpy, wm.cursor_move);
+ XFreeCursor(wm.dpy, wm.cursor_resize);
+}
+
+int is_always_on_top(Window window) {
+ if (window == None) return 0;
+ return has_wm_state(window, _NET_WM_STATE_ABOVE);
+}
+
+void raise_window(Window window) {
+ if (window == None) return;
+ if (!window_exists(window)) return;
+
+ // If the window is already always-on-top, just raise it to the absolute top.
+ if (is_always_on_top(window)) {
+ XRaiseWindow(wm.dpy, window);
+ return;
+ }
+
+ // Otherwise, find the lowest "always-on-top" window and stack this window just below it.
+ // If no "always-on-top" window exists, raise to top.
+ Window root_return, parent_return, *children;
+ unsigned int nchildren;
+ if (XQueryTree(wm.dpy, wm.root, &root_return, &parent_return, &children, &nchildren)) {
+ // Traverse from bottom to top.
+ for (unsigned int i = 0; i < nchildren; i++) {
+ if (children[i] == window) continue;
+
+ if (is_always_on_top(children[i])) {
+ // Found the first (lowest) always-on-top window.
+ // Stack 'window' below 'children[i]'.
+ XWindowChanges changes;
+ changes.sibling = children[i];
+ changes.stack_mode = Below;
+ XConfigureWindow(wm.dpy, window, CWSibling | CWStackMode, &changes);
+ XFree(children);
+ return;
+ }
+ }
+ if (children) XFree(children);
+ }
+
+ // No always-on-top windows found, or XQueryTree failed.
+ XRaiseWindow(wm.dpy, window);
+}
+
+int ignore_x_error(Display *dpy, XErrorEvent *err) {
+ (void)dpy;
+ (void)err;
+ return 0;
+}
+
+int window_exists(Window window) {
+ if (window == None) return 0;
+ XErrorHandler old = XSetErrorHandler(ignore_x_error);
+ XWindowAttributes attr;
+ Status status = XGetWindowAttributes(wm.dpy, window, &attr);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+ return status != 0;
+}
+
+void set_active_window(Window window, Time time) {
+ if (window != None) {
+ if (!window_exists(window)) return;
+
+ XChangeProperty(wm.dpy, wm.root, _NET_ACTIVE_WINDOW, XA_WINDOW, 32, PropModeReplace, (unsigned char *)&window, 1);
+ wm.active = window;
+
+ // Check for WM_TAKE_FOCUS support.
+ int take_focus = 0;
+ Atom *protocols = NULL;
+ int count = 0;
+ if (XGetWMProtocols(wm.dpy, window, &protocols, &count)) {
+ for (int i = 0; i < count; i++) {
+ if (protocols[i] == WM_TAKE_FOCUS) {
+ take_focus = 1;
+ break;
+ }
+ }
+ XFree(protocols);
+ }
+
+ if (take_focus) {
+ XEvent ev;
+ ev.type = ClientMessage;
+ ev.xclient.window = window;
+ ev.xclient.message_type = WM_PROTOCOLS;
+ ev.xclient.format = 32;
+ ev.xclient.data.l[0] = WM_TAKE_FOCUS;
+ ev.xclient.data.l[1] = time;
+ XSendEvent(wm.dpy, window, False, NoEventMask, &ev);
+ log_message(stdout, LOG_DEBUG, "Sent WM_TAKE_FOCUS to 0x%lx with time %lu", window, time);
+ }
+
+ int expects_input = 1;
+ XWMHints *hints = XGetWMHints(wm.dpy, window);
+ if (hints) {
+ if ((hints->flags & InputHint) && !hints->input) {
+ expects_input = 0;
+ }
+ XFree(hints);
+ }
+
+ if (expects_input) {
+ XSetInputFocus(wm.dpy, window, RevertToPointerRoot, time);
+ log_message(stdout, LOG_DEBUG, "Set input focus to 0x%lx with time %lu", window, time);
+ } else {
+ log_message(stdout, LOG_DEBUG, "Window 0x%lx does not expect input, skipping XSetInputFocus", window);
+ }
+ } else {
+ XDeleteProperty(wm.dpy, wm.root, _NET_ACTIVE_WINDOW);
+ wm.active = None;
+ XSetInputFocus(wm.dpy, wm.root, RevertToPointerRoot, time);
+ }
+ XFlush(wm.dpy);
+}
+
+Window get_active_window(void) {
+ Atom _NET_ACTIVE_WINDOW = XInternAtom(wm.dpy, "_NET_ACTIVE_WINDOW", False);
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+ Window active = None;
+
+ if (XGetWindowProperty(wm.dpy, wm.root, _NET_ACTIVE_WINDOW, 0, (~0L), False, AnyPropertyType, &actual_type, &actual_format, &nitems, &bytes_after, &prop) == Success) {
+ if (prop && nitems >= 1) {
+ active = *(Window *)prop;
+ }
+ }
+
+ if (prop) XFree(prop);
+ return active;
+}
+
+void get_cursor_offset(Window window, int *dx, int *dy) {
+ Window root, child;
+ int root_x, root_y;
+ unsigned int mask;
+ XQueryPointer(wm.dpy, window, &root, &child, &root_x, &root_y, dx, dy, &mask);
+}
+
+// https://tronche.com/gui/x/xlib/events/structure-control/configure.html
+void handle_configure_request(void) {
+ XConfigureRequestEvent *ev = &wm.ev.xconfigurerequest;
+ XWindowChanges changes;
+
+ changes.x = ev->x;
+ changes.y = ev->y;
+ changes.width = ev->width;
+ changes.height = ev->height;
+ changes.border_width = ev->border_width;
+ changes.sibling = ev->above;
+ changes.stack_mode = ev->detail;
+
+ XConfigureWindow(wm.dpy, ev->window, ev->value_mask, &changes);
+ 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);
+}
+
+// https://tronche.com/gui/x/xlib/events/structure-control/map.html
+void handle_map_request(void) {
+ Window window = wm.ev.xmaprequest.window;
+ if (window == wm.root) return;
+
+ // Move window under cursor position and clamps inside the screen bounds.
+ XWindowAttributes check_attr;
+ if (XGetWindowAttributes(wm.dpy, window, &check_attr)) {
+ XSelectInput(wm.dpy, window, EnterWindowMask | LeaveWindowMask);
+
+ Window root_return, child_return;
+ int root_x, root_y, win_x, win_y;
+ unsigned int mask;
+
+ if (XQueryPointer(wm.dpy, wm.root, &root_return, &child_return, &root_x, &root_y, &win_x, &win_y, &mask)) {
+ int new_x = root_x - (check_attr.width / 2);
+ int new_y = root_y - (check_attr.height / 2);
+ int screen_width = DisplayWidth(wm.dpy, wm.screen);
+ int screen_height = DisplayHeight(wm.dpy, wm.screen);
+
+ if (new_x < 0) new_x = 0;
+ if (new_y < 0) new_y = 0;
+ if (new_x + check_attr.width > screen_width) new_x = screen_width - check_attr.width;
+ if (new_y + check_attr.height > screen_height) new_y = screen_height - check_attr.height;
+
+ XMoveWindow(wm.dpy, window, new_x, new_y);
+ log_message(stdout, LOG_DEBUG, "Positioned new window 0x%lx at cursor (%d, %d)", window, root_x, root_y);
+ }
+ }
+
+ // Tag window with current desktop.
+ unsigned long desktop = wm.current_desktop;
+ XChangeProperty(wm.dpy, window, _NET_WM_DESKTOP, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&desktop, 1);
+
+ XMapWindow(wm.dpy, window);
+ raise_window(window);
+
+ // Shows, raises and focuses the window.
+ set_active_border(window);
+ set_active_window(window, CurrentTime);
+
+ // Grab buttons for click-to-focus.
+ grab_buttons(window);
+
+ add_client(window);
+ log_message(stdout, LOG_DEBUG, "Window 0x%lx mapped and grabbed on desktop %d", window, wm.current_desktop);
+ redraw_widgets();
+ update_client_list();
+}
+
+// https://tronche.com/gui/x/xlib/events/window-state-change/unmap.html
+void handle_unmap_notify(void) {
+ Window window = wm.ev.xunmap.window;
+ if (window == wm.active) {
+ wm.active = None;
+ }
+
+ // So if we get an unmap for a sticky window, it means the application closed it.
+ if (is_sticky(window)) {
+ remove_client(window);
+ }
+
+ log_message(stdout, LOG_DEBUG, "Window 0x%lx unmapped", window);
+ update_client_list();
+}
+
+// https://tronche.com/gui/x/xlib/events/window-state-change/destroy.html
+void handle_destroy_notify(void) {
+ Window window = wm.ev.xdestroywindow.window;
+ if (window == wm.active) {
+ wm.active = None;
+ }
+ remove_client(window);
+ log_message(stdout, LOG_DEBUG, "Window 0x%lx destroyed", window);
+ update_client_list();
+}
+
+// https://tronche.com/gui/x/xlib/events/client-communication/property.html
+void handle_property_notify(void) {
+ Window window = wm.ev.xproperty.window;
+ Atom prop = wm.ev.xproperty.atom;
+ char *name = XGetAtomName(wm.dpy, prop);
+ log_message(stdout, LOG_DEBUG, "Window 0x%lx got property notification %s", window, name);
+}
+
+// https://tronche.com/gui/x/xlib/events/keyboard-pointer/keyboard-pointer.html
+void handle_motion_notify(void) {
+ if (wm.start.subwindow != None && (wm.start.state & MODKEY)) {
+ int xdiff = wm.ev.xmotion.x_root - wm.start.x_root;
+ int ydiff = wm.ev.xmotion.y_root - wm.start.y_root;
+
+ XMoveResizeWindow(wm.dpy, wm.start.subwindow,
+ wm.attr.x + (wm.start.button == 1 ? xdiff : 0),
+ wm.attr.y + (wm.start.button == 1 ? ydiff : 0),
+ MAX(100, wm.attr.width + (wm.start.button == 3 ? xdiff : 0)),
+ MAX(100, wm.attr.height + (wm.start.button == 3 ? ydiff : 0)));
+
+ // Reset maximization state on manual move/resize.
+ if (wm.start.button == 1) {
+ check_and_clear_maximized_state(wm.start.subwindow, 1, 1);
+ } else if (wm.start.button == 3) {
+ check_and_clear_maximized_state(wm.start.subwindow, 1, 1);
+ }
+ }
+}
+
+// https://tronche.com/gui/x/xlib/events/client-communication/client-message.html
+void handle_client_message(void) {
+ Window window = wm.ev.xclient.window;
+ Atom message_type = wm.ev.xclient.message_type;
+
+ if (message_type == _NET_WM_STATE) {
+ Atom atom1 = (Atom)wm.ev.xclient.data.l[1];
+ Atom atom2 = (Atom)wm.ev.xclient.data.l[2];
+ int action = wm.ev.xclient.data.l[0]; // 0: Remove, 1: Add, 2: Toggle
+
+ if (atom1 == _NET_WM_STATE_FULLSCREEN || atom2 == _NET_WM_STATE_FULLSCREEN) {
+ int currently_fullscreen = has_wm_state(window, _NET_WM_STATE_FULLSCREEN);
+ int should_fullscreen = 0;
+
+ if (action == 1) { // ADD
+ should_fullscreen = 1;
+ } else if (action == 0) { // REMOVE
+ should_fullscreen = 0;
+ } else if (action == 2) { // TOGGLE
+ should_fullscreen = !currently_fullscreen;
+ }
+
+ set_fullscreen(window, should_fullscreen);
+ }
+ }
+
+ log_message(stdout, LOG_DEBUG, "Window 0x%lx got message type of %lu", window, message_type);
+ redraw_widgets();
+ XFlush(wm.dpy);
+}
+
+static void set_fullscreen(Window window, int full) {
+ int currently_fullscreen = has_wm_state(window, _NET_WM_STATE_FULLSCREEN);
+
+ if (full && !currently_fullscreen) {
+ // Enable Fullscreen
+ XWindowAttributes attr;
+ if (XGetWindowAttributes(wm.dpy, window, &attr)) {
+ // Save geometry
+ long geom[4] = { attr.x, attr.y, attr.width, attr.height };
+ XChangeProperty(wm.dpy, window, _GLITCH_PRE_FULLSCREEN_GEOM, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)geom, 4);
+
+ int screen_width = DisplayWidth(wm.dpy, wm.screen);
+ int screen_height = DisplayHeight(wm.dpy, wm.screen);
+
+ XMoveResizeWindow(wm.dpy, window, 0, 0, screen_width, screen_height);
+ XSetWindowBorderWidth(wm.dpy, window, 0);
+ XRaiseWindow(wm.dpy, window);
+
+ update_wm_state(window, _NET_WM_STATE_FULLSCREEN, 1);
+ log_message(stdout, LOG_DEBUG, "Fullscreen enabled for 0x%lx", window);
+ }
+ } else if (!full && currently_fullscreen) {
+ // Disable Fullscreen
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+
+ XErrorHandler old = XSetErrorHandler(ignore_x_error);
+ int status = XGetWindowProperty(wm.dpy, window, _GLITCH_PRE_FULLSCREEN_GEOM, 0, 4, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ if (status == Success && prop && nitems == 4) {
+ long *geom = (long *)prop;
+ XMoveResizeWindow(wm.dpy, window, (int)geom[0], (int)geom[1], (unsigned int)geom[2], (unsigned int)geom[3]);
+ }
+ if (prop) XFree(prop);
+
+ XDeleteProperty(wm.dpy, window, _GLITCH_PRE_FULLSCREEN_GEOM);
+
+ // Restore border
+ int border_w = border_size;
+ XSetWindowBorderWidth(wm.dpy, window, border_w);
+
+ update_wm_state(window, _NET_WM_STATE_FULLSCREEN, 0);
+ set_active_border(window); // Updates border color and width correctly
+
+ log_message(stdout, LOG_DEBUG, "Fullscreen disabled for 0x%lx", window);
+ }
+}
+
+void toggle_fullscreen(const Arg *arg) {
+ (void)arg;
+ if (wm.active == None) return;
+ int currently_fullscreen = has_wm_state(wm.active, _NET_WM_STATE_FULLSCREEN);
+ set_fullscreen(wm.active, !currently_fullscreen);
+}
+
+void center_window(const Arg *arg) {
+ (void)arg;
+ if (wm.active == None) return;
+
+ XWindowAttributes attr;
+ if (XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
+ int screen_width = DisplayWidth(wm.dpy, wm.screen);
+ int screen_height = DisplayHeight(wm.dpy, wm.screen);
+ int new_x = (screen_width - attr.width) / 2;
+ int new_y = (screen_height - attr.height) / 2;
+
+ if (new_x < 0) new_x = 0;
+ if (new_y < 0) new_y = 0;
+
+ XMoveWindow(wm.dpy, wm.active, new_x, new_y);
+ log_message(stdout, LOG_DEBUG, "Centered window 0x%lx at (%d, %d)", wm.active, new_x, new_y);
+ }
+}
+
+// https://tronche.com/gui/x/xlib/events/keyboard-pointer/keyboard-pointer.html
+void handle_button_press(void) {
+ Window window;
+ if (wm.ev.xbutton.window == wm.root) {
+ window = wm.ev.xbutton.subwindow;
+ } else {
+ window = wm.ev.xbutton.window;
+ }
+
+ log_message(stdout, LOG_DEBUG, "ButtonPress: window=0x%lx, subwindow=0x%lx, button=%d, state=0x%x, targeting=0x%lx",
+ wm.ev.xbutton.window, wm.ev.xbutton.subwindow, wm.ev.xbutton.button, wm.ev.xbutton.state, window);
+
+ if (window == None || window == wm.root) {
+ log_message(stdout, LOG_DEBUG, "ButtonPress on root or None, ignoring");
+ XAllowEvents(wm.dpy, ReplayPointer, wm.ev.xbutton.time);
+ return;
+ }
+
+ // Focus and raise on any click.
+ set_active_border(window);
+ set_active_window(window, wm.ev.xbutton.time);
+
+ XErrorHandler old = XSetErrorHandler(ignore_x_error);
+ raise_window(window);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ log_message(stdout, LOG_DEBUG, "Focused and raised window 0x%lx", window);
+
+ if (wm.ev.xbutton.state & MODKEY) {
+ old = XSetErrorHandler(ignore_x_error);
+ Status s = XGetWindowAttributes(wm.dpy, window, &wm.attr);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ if (s == 0) {
+ log_message(stdout, LOG_WARNING, "Failed to get window attributes for 0x%lx, ignoring drag", window);
+ XAllowEvents(wm.dpy, AsyncPointer, CurrentTime);
+ return;
+ }
+
+ wm.start = wm.ev.xbutton;
+ // Ensure we use the top-level window for dragging, not a sub-window.
+ wm.start.subwindow = window;
+
+ // Set global error handler to ignore errors during drag (e.g. if window closes).
+ XSetErrorHandler(ignore_x_error);
+
+ switch (wm.ev.xbutton.button) {
+ case 1: {
+ XDefineCursor(wm.dpy, window, wm.cursor_move);
+ log_message(stdout, LOG_DEBUG, "Setting cursor to move");
+ } break;
+ case 3: {
+ XDefineCursor(wm.dpy, window, wm.cursor_resize);
+ log_message(stdout, LOG_DEBUG, "Setting cursor to resize");
+ } break;
+ }
+
+ // Use AsyncPointer to consume the event and unfreeze the pointer.
+ XAllowEvents(wm.dpy, AsyncPointer, wm.ev.xbutton.time);
+ log_message(stdout, LOG_DEBUG, "Window 0x%lx got button press with MODKEY, unfreezing", window);
+ } else {
+ // Replay the click to the application if no modifier was used.
+ XAllowEvents(wm.dpy, ReplayPointer, wm.ev.xbutton.time);
+ log_message(stdout, LOG_DEBUG, "Window 0x%lx got button press, replaying pointer", window);
+ }
+
+ redraw_widgets();
+ XFlush(wm.dpy);
+}
+
+
+
+void goto_desktop(const Arg *arg) {
+ if (arg->i < 1 || arg->i > NUM_DESKTOPS || (unsigned int)arg->i == wm.current_desktop) return;
+
+ unsigned int old_desktop = wm.current_desktop;
+ wm.current_desktop = arg->i;
+
+ unsigned int nchildren;
+ Window root_return, parent_return, *children;
+ XWindowAttributes wa;
+ Window new_active = None;
+
+ if (XQueryTree(wm.dpy, wm.root, &root_return, &parent_return, &children, &nchildren)) {
+ for (unsigned int i = 0; i < nchildren; i++) {
+ if (!XGetWindowAttributes(wm.dpy, children[i], &wa) || wa.override_redirect)
+ continue;
+
+ unsigned long desktop;
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+
+ int status = XGetWindowProperty(wm.dpy, children[i], _NET_WM_DESKTOP, 0, 1, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
+ if (status == Success && prop && nitems > 0) {
+ desktop = *(unsigned long *)prop;
+ if (desktop == (unsigned long)arg->i) {
+ XMapWindow(wm.dpy, children[i]);
+ new_active = children[i];
+ } else if (desktop == (unsigned long)old_desktop) {
+ // Don't unmap sticky windows
+ if (!is_sticky(children[i])) {
+ XUnmapWindow(wm.dpy, children[i]);
+ }
+ }
+ }
+ // Sticky windows should always be shown and raised
+ if (is_sticky(children[i])) {
+ XRaiseWindow(wm.dpy, children[i]);
+ }
+ if (prop) XFree(prop);
+ }
+ if (children) XFree(children);
+ }
+
+ unsigned long desktop_val = wm.current_desktop;
+ XChangeProperty(wm.dpy, wm.root, _NET_CURRENT_DESKTOP, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&desktop_val, 1);
+
+ set_active_window(new_active, CurrentTime);
+ set_active_border(new_active);
+
+ widget_desktop_indicator();
+ widget_datetime();
+ log_message(stdout, LOG_DEBUG, "Switched to desktop %d", wm.current_desktop);
+ XFlush(wm.dpy);
+}
+
+void send_window_to_desktop(const Arg *arg) {
+ if (wm.active == None || arg->i < 1 || arg->i > NUM_DESKTOPS || (unsigned int)arg->i == wm.current_desktop) return;
+
+ unsigned long desktop = arg->i;
+ XChangeProperty(wm.dpy, wm.active, _NET_WM_DESKTOP, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&desktop, 1);
+
+ // Reset border before unmapping to avoid "stuck" active borders on other desktops.
+ XSetWindowBorder(wm.dpy, wm.active, wm.borders.normal_inactive);
+ XUnmapWindow(wm.dpy, wm.active);
+
+ wm.active = None;
+ widget_desktop_indicator();
+ widget_datetime();
+ log_message(stdout, LOG_DEBUG, "Moved window to desktop %d", arg->i);
+ XFlush(wm.dpy);
+}
+
+
+void update_client_list(void) {
+ XDeleteProperty(wm.dpy, wm.root, _NET_CLIENT_LIST);
+
+ Client *c = wm.clients;
+ while (c) {
+ XChangeProperty(wm.dpy, wm.root, _NET_CLIENT_LIST, XA_WINDOW, 32, PropModeAppend, (unsigned char *)&c->window, 1);
+ c = c->next;
+ }
+}
+
+void grab_buttons(Window window) {
+ XGrabButton(wm.dpy, AnyButton, AnyModifier, window, True, ButtonPressMask, GrabModeSync, GrabModeAsync, None, None);
+}
+
+// https://tronche.com/gui/x/xlib/events/keyboard-pointer/keyboard-pointer.html
+void handle_button_release(void) {
+ if (wm.start.subwindow != None && (wm.start.state & MODKEY)) {
+ XDefineCursor(wm.dpy, wm.start.subwindow, None);
+
+ // Restore default error handler
+ XSetErrorHandler(NULL);
+
+ log_message(stdout, LOG_DEBUG, "Restored default cursor on window 0x%lx", wm.start.subwindow);
+ wm.start.subwindow = None;
+ }
+
+ log_message(stdout, LOG_DEBUG, "ButtonRelease: event window=0x%lx, subwindow=0x%lx", wm.ev.xbutton.window, wm.ev.xbutton.subwindow);
+ XFlush(wm.dpy);
+}
+
+// https://tronche.com/gui/x/xlib/events/keyboard-pointer/keyboard-pointer.html
+void handle_key_press(void) {
+ log_message(stdout, LOG_DEBUG, ">> Key pressed > active window 0x%lx", wm.ev.xkey.subwindow);
+ if (wm.ev.type != KeyPress) return;
+
+ KeySym keysym = XLookupKeysym(&wm.ev.xkey, 0);
+
+ // Check keybinds first.
+ for (unsigned int i = 0; i < LENGTH(keybinds); i++) {
+ if (keysym == keybinds[i].keysym && (wm.ev.xkey.state & (Mod1Mask|Mod2Mask|Mod3Mask|Mod4Mask|ControlMask|ShiftMask)) == keybinds[i].mod) {
+ keybinds[i].func(&keybinds[i].arg);
+ XFlush(wm.dpy);
+ return;
+ }
+ }
+
+ // Check shortcuts next.
+ for (unsigned int i = 0; i < LENGTH(shortcuts); i++) {
+ if (keysym == shortcuts[i].keysym && (wm.ev.xkey.state & (Mod1Mask|Mod2Mask|Mod3Mask|Mod4Mask|ControlMask|ShiftMask)) == shortcuts[i].mod) {
+ execute_shortcut(shortcuts[i].cmd);
+ XFlush(wm.dpy);
+ return;
+ }
+ }
+
+ XFlush(wm.dpy);
+}
+
+// https://tronche.com/gui/x/xlib/events/keyboard-pointer/keyboard-pointer.html
+void handle_key_release(void) {
+ if (wm.ev.type != KeyRelease) return;
+
+ KeySym keysym = XLookupKeysym(&wm.ev.xkey, 0);
+
+ if (wm.is_cycling) {
+ if (keysym == XK_Alt_L || keysym == XK_Alt_R) {
+ // Activate selected window
+ if (wm.cycle_clients && wm.cycle_count > 0 && wm.active_cycle_index >= 0) {
+ Window target = wm.cycle_clients[wm.active_cycle_index];
+ set_active_border(target);
+ set_active_window(target, CurrentTime);
+ raise_window(target);
+ XSync(wm.dpy, False);
+ }
+ end_cycling();
+ }
+ }
+}
+
+void handle_focus_in(void) {
+ Window window = wm.ev.xfocus.window;
+ if (window != wm.root) {
+ log_message(stdout, LOG_DEBUG, "Window 0x%lx focus in", window);
+ }
+}
+
+void handle_focus_out(void) {
+ Window window = wm.ev.xfocus.window;
+ if (window != wm.root) {
+ log_message(stdout, LOG_DEBUG, "Window 0x%lx focus out", window);
+ }
+}
+
+void handle_enter_notify(void) {
+ Window window = wm.ev.xcrossing.window;
+ if (window != wm.root) {
+ log_message(stdout, LOG_DEBUG, "Window 0x%lx enter notify", window);
+ }
+}
+
+void handle_expose(void) {
+ if (wm.ev.xexpose.count == 0 && wm.ev.xexpose.window == wm.root) {
+ // Rate limit redraws from Expose events to 200ms (5fps) to prevent flashing/high CPU
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ unsigned long now_ms = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
+
+ if (now_ms - wm.last_widget_update > 50) {
+ redraw_widgets();
+ wm.last_widget_update = now_ms;
+ }
+ }
+}
+
+void redraw_widgets(void) {
+ widget_desktop_indicator();
+ widget_datetime();
+}
+
+void set_active_border(Window window) {
+ if (window == None) return;
+ if (!window_exists(window)) return;
+
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+ int has_decorations = 1;
+
+ // Check _MOTIF_WM_HINTS to see if the window requested no decorations
+ XErrorHandler old = XSetErrorHandler(ignore_x_error);
+ int status = XGetWindowProperty(wm.dpy, window, _MOTIF_WM_HINTS, 0, 5, False, AnyPropertyType,
+ &actual_type, &actual_format, &nitems, &bytes_after, &prop);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ if (status == Success) {
+ if (prop && nitems >= 3) {
+ unsigned long flags = ((unsigned long *)prop)[0];
+ unsigned long decorations = ((unsigned long *)prop)[2];
+ // If flags bit 1 is set (MWM_HINTS_DECORATIONS) and decorations bit 0 is cleared (MWM_DECOR_ALL/BORDER), then no border.
+ // Simplification: if decorations is 0, assume no border.
+ if ((flags & 2) && (decorations & 1) == 0) {
+ has_decorations = 0;
+ }
+ }
+ if (prop) XFree(prop);
+ }
+
+ unsigned int bw = has_decorations ? border_size : 0;
+
+ // Setting current active window to inactive.
+ if (wm.active != None) {
+ // For safety/speed, let's re-check hints for the old active window too.
+ int old_has_decorations = 1;
+
+ old = XSetErrorHandler(ignore_x_error);
+ status = XGetWindowProperty(wm.dpy, wm.active, _MOTIF_WM_HINTS, 0, 5, False, AnyPropertyType, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ if (status == Success) {
+ if (prop && nitems >= 3) {
+ unsigned long flags = ((unsigned long *)prop)[0];
+ unsigned long decorations = ((unsigned long *)prop)[2];
+ if ((flags & 2) && (decorations & 1) == 0) {
+ old_has_decorations = 0;
+ }
+ }
+ if (prop) XFree(prop);
+ }
+
+ XSetWindowBorderWidth(wm.dpy, wm.active, old_has_decorations ? border_size : 0);
+ if (is_always_on_top(wm.active)) {
+ XSetWindowBorder(wm.dpy, wm.active, wm.borders.on_top_inactive);
+ } else if (is_sticky(wm.active)) {
+ XSetWindowBorder(wm.dpy, wm.active, wm.borders.sticky_inactive);
+ } else {
+ XSetWindowBorder(wm.dpy, wm.active, wm.borders.normal_inactive);
+ }
+ log_message(stdout, LOG_DEBUG, "Active window 0x%lx border set to inactive", wm.active);
+ }
+
+ // Setting desired window to active.
+ XSetWindowBorderWidth(wm.dpy, window, bw);
+ if (is_always_on_top(window)) {
+ XSetWindowBorder(wm.dpy, window, wm.borders.on_top_active);
+ } else if (is_sticky(window)) {
+ XSetWindowBorder(wm.dpy, window, wm.borders.sticky_active);
+ } else {
+ XSetWindowBorder(wm.dpy, window, wm.borders.normal_active);
+ }
+ XFlush(wm.dpy);
+
+ log_message(stdout, LOG_DEBUG, "Desired window 0x%lx border set to active", window);
+}
+
+void move_window_x(const Arg *arg) {
+ if (wm.active == None) return;
+
+ XWindowAttributes attr;
+ XGetWindowAttributes(wm.dpy, wm.active, &attr);
+ XMoveWindow(wm.dpy, wm.active, attr.x + arg->i, attr.y);
+ check_and_clear_maximized_state(wm.active, 1, 0);
+ log_message(stdout, LOG_DEBUG, "Move window 0x%lx on X by %d", wm.active, arg->i);
+
+ XSync(wm.dpy, True);
+ XFlush(wm.dpy);
+}
+
+void move_window_y(const Arg *arg) {
+ if (wm.active == None) return;
+
+ XWindowAttributes attr;
+ XGetWindowAttributes(wm.dpy, wm.active, &attr);
+ XMoveWindow(wm.dpy, wm.active, attr.x, attr.y + arg->i);
+ check_and_clear_maximized_state(wm.active, 0, 1);
+ log_message(stdout, LOG_DEBUG, "Move window 0x%lx on Y by %d", wm.active, arg->i);
+
+ XSync(wm.dpy, True);
+ XFlush(wm.dpy);
+}
+
+void close_window(const Arg *arg) {
+ (void)arg;
+ if (wm.active == None) return;
+
+ int supported = 0;
+ Atom *protocols = NULL;
+ int count = 0;
+ if (XGetWMProtocols(wm.dpy, wm.active, &protocols, &count)) {
+ for (int i = 0; i < count; i++) {
+ if (protocols[i] == WM_DELETE_WINDOW) {
+ supported = 1;
+ break;
+ }
+ }
+ XFree(protocols);
+ }
+
+ if (supported) {
+ XEvent ev;
+ ev.type = ClientMessage;
+ ev.xclient.window = wm.active;
+ ev.xclient.message_type = WM_PROTOCOLS;
+ ev.xclient.format = 32;
+ ev.xclient.data.l[0] = WM_DELETE_WINDOW;
+ ev.xclient.data.l[1] = CurrentTime;
+ XSendEvent(wm.dpy, wm.active, False, NoEventMask, &ev);
+ log_message(stdout, LOG_DEBUG, "Sent WM_DELETE_WINDOW to 0x%lx", wm.active);
+ } else {
+ XKillClient(wm.dpy, wm.active);
+ log_message(stdout, LOG_DEBUG, "Killed client 0x%lx", wm.active);
+ }
+}
+
+void resize_window_x(const Arg *arg) {
+ if (wm.active == None) return;
+
+ XWindowAttributes attr;
+ XGetWindowAttributes(wm.dpy, wm.active, &attr);
+ XResizeWindow(wm.dpy, wm.active, MAX(1, attr.width + arg->i), attr.height);
+ check_and_clear_maximized_state(wm.active, 1, 0);
+ XFlush(wm.dpy);
+
+ log_message(stdout, LOG_DEBUG, "Resize window 0x%lx on X by %d", wm.active, arg->i);
+}
+
+void resize_window_y(const Arg *arg) {
+ if (wm.active == None) return;
+
+ XWindowAttributes attr;
+ XGetWindowAttributes(wm.dpy, wm.active, &attr);
+ XResizeWindow(wm.dpy, wm.active, attr.width, MAX(1, attr.height + arg->i));
+ check_and_clear_maximized_state(wm.active, 0, 1);
+ XFlush(wm.dpy);
+
+ log_message(stdout, LOG_DEBUG, "Resize window 0x%lx on Y by %d", wm.active, arg->i);
+}
+
+void window_snap_up(const Arg *arg) {
+ (void)arg;
+ if (wm.active == None) return;
+
+ XWindowAttributes attr;
+ if (!XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
+ log_message(stdout, LOG_DEBUG, "Failed to get window attributes for 0x%lx", wm.active);
+ return;
+ }
+
+ int rel_x, rel_y;
+ get_cursor_offset(wm.active, &rel_x, &rel_y);
+
+ XMoveWindow(wm.dpy, wm.active, attr.x, 0);
+ check_and_clear_maximized_state(wm.active, 0, 1);
+ XFlush(wm.dpy);
+
+ log_message(stdout, LOG_DEBUG, "Snapped window 0x%lx to top edge", wm.active);
+}
+
+void window_snap_down(const Arg *arg) {
+ (void)arg;
+ if (wm.active == None) return;
+
+ XWindowAttributes attr;
+ if (!XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
+ log_message(stdout, LOG_DEBUG, "Failed to get window attributes for 0x%lx", wm.active);
+ return;
+ }
+
+ int rel_x, rel_y;
+ int y = DisplayHeight(wm.dpy, DefaultScreen(wm.dpy)) - attr.height - (2 * attr.border_width);
+ get_cursor_offset(wm.active, &rel_x, &rel_y);
+
+ XMoveWindow(wm.dpy, wm.active, attr.x, y);
+ check_and_clear_maximized_state(wm.active, 0, 1);
+ XFlush(wm.dpy);
+
+ log_message(stdout, LOG_DEBUG, "Snapped window 0x%lx to bottom edge", wm.active);
+}
+
+void window_snap_left(const Arg *arg) {
+ (void)arg;
+ if (wm.active == None) return;
+
+ XWindowAttributes attr;
+ if (!XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
+ log_message(stdout, LOG_DEBUG, "Failed to get window attributes for 0x%lx", wm.active);
+ return;
+ }
+
+ int rel_x, rel_y;
+ get_cursor_offset(wm.active, &rel_x, &rel_y);
+
+ XMoveWindow(wm.dpy, wm.active, 0, attr.y);
+ check_and_clear_maximized_state(wm.active, 1, 0);
+ XFlush(wm.dpy);
+
+ log_message(stdout, LOG_DEBUG, "Snapped window 0x%lx to left edge", wm.active);
+}
+
+void window_snap_right(const Arg *arg) {
+ (void)arg;
+ if (wm.active == None) return;
+
+ XWindowAttributes attr;
+ if (!XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
+ log_message(stdout, LOG_DEBUG, "Failed to get window attributes for 0x%lx", wm.active);
+ return;
+ }
+
+ int rel_x, rel_y;
+ int x = DisplayWidth(wm.dpy, DefaultScreen(wm.dpy)) - attr.width - (2 * attr.border_width);
+ get_cursor_offset(wm.active, &rel_x, &rel_y);
+
+ XMoveWindow(wm.dpy, wm.active, x, attr.y);
+ check_and_clear_maximized_state(wm.active, 1, 0);
+ XFlush(wm.dpy);
+
+ log_message(stdout, LOG_DEBUG, "Snapped window 0x%lx to right edge", wm.active);
+}
+
+static void update_wm_state(Window w, Atom state_atom, int add) {
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+
+ XErrorHandler old = XSetErrorHandler(ignore_x_error);
+ int status = XGetWindowProperty(wm.dpy, w, _NET_WM_STATE, 0, (~0L), False, XA_ATOM, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ if (status == Success) {
+ Atom *new_atoms = malloc(sizeof(Atom) * (nitems + 1));
+ int count = 0;
+ if (prop && nitems > 0) {
+ Atom *atoms = (Atom *)prop;
+ for (unsigned long i = 0; i < nitems; i++) {
+ if (atoms[i] != state_atom) {
+ new_atoms[count++] = atoms[i];
+ }
+ }
+ }
+ if (add) {
+ new_atoms[count++] = state_atom;
+ }
+ XChangeProperty(wm.dpy, w, _NET_WM_STATE, XA_ATOM, 32, PropModeReplace, (unsigned char *)new_atoms, count);
+ free(new_atoms);
+ }
+ if (prop) XFree(prop);
+}
+
+static int has_wm_state(Window w, Atom state_atom) {
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+ int found = 0;
+
+ XErrorHandler old = XSetErrorHandler(ignore_x_error);
+ int status = XGetWindowProperty(wm.dpy, w, _NET_WM_STATE, 0, (~0L), False, XA_ATOM, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ if (status == Success) {
+ if (prop && nitems > 0) {
+ Atom *atoms = (Atom *)prop;
+ for (unsigned long i = 0; i < nitems; i++) {
+ if (atoms[i] == state_atom) {
+ found = 1;
+ break;
+ }
+ }
+ }
+ }
+ if (prop) XFree(prop);
+ return found;
+}
+
+static void check_and_clear_maximized_state(Window w, int horizontal, int vertical) {
+ if (horizontal && has_wm_state(w, _NET_WM_STATE_MAXIMIZED_HORZ)) {
+ update_wm_state(w, _NET_WM_STATE_MAXIMIZED_HORZ, 0);
+ XDeleteProperty(wm.dpy, w, _GLITCH_PRE_HMAX_GEOM);
+ log_message(stdout, LOG_DEBUG, "Cleared horizontal maximization state for window 0x%lx due to interaction", w);
+ }
+ if (vertical && has_wm_state(w, _NET_WM_STATE_MAXIMIZED_VERT)) {
+ update_wm_state(w, _NET_WM_STATE_MAXIMIZED_VERT, 0);
+ XDeleteProperty(wm.dpy, w, _GLITCH_PRE_VMAX_GEOM);
+ log_message(stdout, LOG_DEBUG, "Cleared vertical maximization state for window 0x%lx due to interaction", w);
+ }
+}
+
+void window_hmaximize(const Arg *arg) {
+ (void)arg;
+ if (wm.active == None) return;
+
+ if (has_wm_state(wm.active, _NET_WM_STATE_MAXIMIZED_HORZ)) {
+ // Restore geometry
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+
+ XErrorHandler old = XSetErrorHandler(ignore_x_error);
+ int status = XGetWindowProperty(wm.dpy, wm.active, _GLITCH_PRE_HMAX_GEOM, 0, 4, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ if (status == Success) {
+ if (prop && nitems == 4) {
+ long *geom = (long *)prop;
+ XMoveResizeWindow(wm.dpy, wm.active, (int)geom[0], (int)geom[1], (unsigned int)geom[2], (unsigned int)geom[3]);
+ update_wm_state(wm.active, _NET_WM_STATE_MAXIMIZED_HORZ, 0);
+ XDeleteProperty(wm.dpy, wm.active, _GLITCH_PRE_HMAX_GEOM);
+ log_message(stdout, LOG_DEBUG, "Restored horizontal geometry for window 0x%lx", wm.active);
+ }
+ }
+ if (prop) XFree(prop);
+ } else {
+ // Save geometry and maximize
+ XWindowAttributes attr;
+ if (XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
+ long geom[4] = { attr.x, attr.y, attr.width, attr.height };
+ XChangeProperty(wm.dpy, wm.active, _GLITCH_PRE_HMAX_GEOM, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)geom, 4);
+
+ int screen_width = DisplayWidth(wm.dpy, wm.screen);
+ XMoveResizeWindow(wm.dpy, wm.active, 0, attr.y, screen_width - (2 * attr.border_width), attr.height);
+ update_wm_state(wm.active, _NET_WM_STATE_MAXIMIZED_HORZ, 1);
+ log_message(stdout, LOG_DEBUG, "Horizontally maximized window 0x%lx", wm.active);
+ }
+ }
+ XFlush(wm.dpy);
+}
+
+void window_vmaximize(const Arg *arg) {
+ (void)arg;
+ if (wm.active == None) return;
+
+ if (has_wm_state(wm.active, _NET_WM_STATE_MAXIMIZED_VERT)) {
+ // Restore geometry
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+
+ XErrorHandler old = XSetErrorHandler(ignore_x_error);
+ int status = XGetWindowProperty(wm.dpy, wm.active, _GLITCH_PRE_VMAX_GEOM, 0, 4, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ if (status == Success) {
+ if (prop && nitems == 4) {
+ long *geom = (long *)prop;
+ XMoveResizeWindow(wm.dpy, wm.active, (int)geom[0], (int)geom[1], (unsigned int)geom[2], (unsigned int)geom[3]);
+ update_wm_state(wm.active, _NET_WM_STATE_MAXIMIZED_VERT, 0);
+ XDeleteProperty(wm.dpy, wm.active, _GLITCH_PRE_VMAX_GEOM);
+ log_message(stdout, LOG_DEBUG, "Restored vertical geometry for window 0x%lx", wm.active);
+ }
+ }
+ if (prop) XFree(prop);
+ } else {
+ // Save geometry and maximize
+ XWindowAttributes attr;
+ if (XGetWindowAttributes(wm.dpy, wm.active, &attr)) {
+ long geom[4] = { attr.x, attr.y, attr.width, attr.height };
+ XChangeProperty(wm.dpy, wm.active, _GLITCH_PRE_VMAX_GEOM, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)geom, 4);
+
+ int screen_height = DisplayHeight(wm.dpy, wm.screen);
+ XMoveResizeWindow(wm.dpy, wm.active, attr.x, 0, attr.width, screen_height - (2 * attr.border_width));
+ update_wm_state(wm.active, _NET_WM_STATE_MAXIMIZED_VERT, 1);
+ log_message(stdout, LOG_DEBUG, "Vertically maximized window 0x%lx", wm.active);
+ }
+ }
+ XFlush(wm.dpy);
+}
+
+void quit(const Arg *arg) {
+ (void)arg;
+ log_message(stdout, LOG_DEBUG, "Quit window manager");
+ wm.running = 0;
+}
+
+void toggle_pip(const Arg *arg) {
+ (void)arg;
+ if (wm.active == None) return;
+
+ int sticky = is_sticky(wm.active);
+ unsigned long desktop = sticky ? wm.current_desktop : 0xFFFFFFFF; // 0xFFFFFFFF is often used for "all desktops"
+
+ // Toggle _NET_WM_DESKTOP
+ XChangeProperty(wm.dpy, wm.active, _NET_WM_DESKTOP, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&desktop, 1);
+
+ // Toggle _NET_WM_STATE_STICKY
+ if (!sticky) {
+ XChangeProperty(wm.dpy, wm.active, _NET_WM_STATE, XA_ATOM, 32, PropModeAppend, (unsigned char *)&_NET_WM_STATE_STICKY, 1);
+
+ // If enabling PiP: resize to a small corner window
+ int screen_width = DisplayWidth(wm.dpy, wm.screen);
+ int screen_height = DisplayHeight(wm.dpy, wm.screen);
+ int pip_w = screen_width / 4;
+ int pip_h = screen_height / 4;
+ int x = screen_width - pip_w - 20;
+ int y = screen_height - pip_h - 20;
+
+ XMoveResizeWindow(wm.dpy, wm.active, x, y, pip_w, pip_h);
+ XRaiseWindow(wm.dpy, wm.active);
+ } else {
+ // If disabling: just remove sticky state (could restore size if we saved it)
+ // For now, let's just remove the atom. This is a bit complex with XChangeProperty,
+ // but since we only have one state usually it might be okay to just clear it if we were sure.
+ // Better way: get property, remove atom from list, set property.
+
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+
+ XErrorHandler old = XSetErrorHandler(ignore_x_error);
+ int status = XGetWindowProperty(wm.dpy, wm.active, _NET_WM_STATE, 0, (~0L), False, XA_ATOM, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ if (status == Success) {
+ if (prop && nitems > 0) {
+ Atom *atoms = (Atom *)prop;
+ Atom *new_atoms = malloc(sizeof(Atom) * nitems);
+ int count = 0;
+ for (unsigned long i = 0; i < nitems; i++) {
+ if (atoms[i] != _NET_WM_STATE_STICKY) {
+ new_atoms[count++] = atoms[i];
+ }
+ }
+ XChangeProperty(wm.dpy, wm.active, _NET_WM_STATE, XA_ATOM, 32, PropModeReplace, (unsigned char *)new_atoms, count);
+ free(new_atoms);
+ }
+ }
+ if (prop) XFree(prop);
+ }
+
+ set_active_border(wm.active);
+ widget_desktop_indicator();
+ widget_datetime();
+ log_message(stdout, LOG_DEBUG, "Toggled PiP for window 0x%lx (sticky=%d)", wm.active, !sticky);
+ XFlush(wm.dpy);
+}
+
+int is_sticky(Window window) {
+ if (window == None) return 0;
+
+ // Check _NET_WM_DESKTOP first
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+
+ XErrorHandler old = XSetErrorHandler(ignore_x_error);
+ int status = XGetWindowProperty(wm.dpy, window, _NET_WM_DESKTOP, 0, 1, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ if (status == Success) {
+ if (prop && nitems > 0) {
+ unsigned long desktop = *(unsigned long *)prop;
+ if (desktop == 0xFFFFFFFF) {
+ XFree(prop);
+ return 1;
+ }
+ }
+ }
+ if (prop) XFree(prop);
+
+ // Also check _NET_WM_STATE for _NET_WM_STATE_STICKY
+ prop = NULL;
+ old = XSetErrorHandler(ignore_x_error);
+ status = XGetWindowProperty(wm.dpy, window, _NET_WM_STATE, 0, (~0L), False, XA_ATOM, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ if (status == Success) {
+ if (prop && nitems > 0) {
+ Atom *atoms = (Atom *)prop;
+ for (unsigned long i = 0; i < nitems; i++) {
+ if (atoms[i] == _NET_WM_STATE_STICKY) {
+ XFree(prop);
+ return 1;
+ }
+ }
+ }
+ }
+ if (prop) XFree(prop);
+
+ return 0;
+}
+
+void toggle_always_on_top(const Arg *arg) {
+ (void)arg;
+ if (wm.active == None) return;
+
+ int above = is_always_on_top(wm.active);
+ update_wm_state(wm.active, _NET_WM_STATE_ABOVE, !above);
+
+ raise_window(wm.active);
+ set_active_border(wm.active);
+
+ log_message(stdout, LOG_DEBUG, "Toggled always-on-top for window 0x%lx (now %s)", wm.active, !above ? "ON" : "OFF");
+}
+
+void reload(const Arg *arg) {
+ (void)arg;
+ wm.running = 0;
+ wm.restart = 1;
+ log_message(stdout, LOG_DEBUG, "Reload window manager");
+}
diff --git a/switcher.c b/switcher.c
new file mode 100644
index 0000000..e14554a
--- /dev/null
+++ b/switcher.c
@@ -0,0 +1,187 @@
+#include <string.h>
+#include <stdlib.h>
+
+#include <X11/Xatom.h>
+#include <X11/Xproto.h>
+
+#include "glitch.h"
+#include "config.h"
+
+extern WindowManager wm;
+
+static void draw_switcher(void) {
+ if (!wm.cycle_win || wm.cycle_count == 0) return;
+
+ XSetWindowAttributes wa;
+ wa.background_pixel = WhitePixel(wm.dpy, wm.screen);
+ XChangeWindowAttributes(wm.dpy, wm.cycle_win, CWBackPixel, &wa);
+ XClearWindow(wm.dpy, wm.cycle_win);
+
+ int box_size = 100;
+ int x_offset = 0;
+ int y_offset = 0;
+
+ for (int i = 0; i < wm.cycle_count; i++) {
+ Window w = wm.cycle_clients[i];
+ int is_selected = (i == wm.active_cycle_index);
+
+ // Draw box background
+ if (is_selected) {
+ // Use blue color for active background (wm.xft_bg_color is usually blue from config)
+ XSetForeground(wm.dpy, DefaultGC(wm.dpy, wm.screen), wm.xft_bg_color.pixel);
+ XFillRectangle(wm.dpy, wm.cycle_win, DefaultGC(wm.dpy, wm.screen), x_offset, y_offset, box_size, box_size);
+ } else {
+ XSetForeground(wm.dpy, DefaultGC(wm.dpy, wm.screen), WhitePixel(wm.dpy, wm.screen));
+ XFillRectangle(wm.dpy, wm.cycle_win, DefaultGC(wm.dpy, wm.screen), x_offset, y_offset, box_size, box_size);
+ }
+
+ // Draw Window Name
+ char *name = NULL;
+ Atom utf8_string = XInternAtom(wm.dpy, "UTF8_STRING", False);
+ if (XFetchName(wm.dpy, w, &name) || XGetWindowProperty(wm.dpy, w, XInternAtom(wm.dpy, "_NET_WM_NAME", False), 0, (~0L), False, utf8_string, &(Atom){0}, &(int){0}, &(unsigned long){0}, &(unsigned long){0}, (unsigned char **)&name) == Success) {
+ if (name) {
+ // Selected: White text. Unselected: Black text.
+ XftColor *color = is_selected ? &wm.xft_color : &wm.xft_root_bg_color;
+ // NOTE: wm.xft_color is "white" (indicator_fg_color), wm.xft_root_bg_color is "black".
+
+ XftDraw *draw = XftDrawCreate(wm.dpy, wm.cycle_win, DefaultVisual(wm.dpy, wm.screen), wm.cmap);
+ if (draw) {
+ if (strlen(name) > 8) {
+ char truncated[9];
+ strncpy(truncated, name, 8);
+ truncated[8] = '\0';
+ XftDrawStringUtf8(draw, color, wm.font, x_offset + 10, y_offset + 90, (const FcChar8 *)truncated, strlen(truncated));
+ } else {
+ XftDrawStringUtf8(draw, color, wm.font, x_offset + 10, y_offset + 90, (const FcChar8 *)name, strlen(name));
+ }
+ XftDrawDestroy(draw);
+ }
+ XFree(name);
+ }
+ }
+
+ x_offset += box_size;
+ }
+ XFlush(wm.dpy);
+}
+
+void end_cycling(void) {
+ if (!wm.is_cycling) return;
+
+ wm.is_cycling = 0;
+ if (wm.cycle_win) {
+ XDestroyWindow(wm.dpy, wm.cycle_win);
+ wm.cycle_win = None;
+ }
+ if (wm.cycle_clients) {
+ free(wm.cycle_clients);
+ wm.cycle_clients = NULL;
+ }
+ wm.cycle_count = 0;
+ wm.active_cycle_index = -1;
+
+ XUngrabKeyboard(wm.dpy, CurrentTime);
+ log_message(stdout, LOG_DEBUG, "Ended window cycling");
+}
+
+void cycle_active_window(const Arg *arg) {
+ // If not already cycling, initialize
+ if (!wm.is_cycling) {
+ wm.is_cycling = 1;
+
+ // Grab keyboard to catch Alt release (key release)
+ // We grab it on the root window.
+ XGrabKeyboard(wm.dpy, wm.root, True, GrabModeAsync, GrabModeAsync, CurrentTime);
+
+ // Count clients
+ int count = 0;
+ Client *c = wm.clients;
+ while (c) {
+ count++;
+ c = c->next;
+ }
+
+ if (count == 0) {
+ end_cycling();
+ return;
+ }
+
+ wm.cycle_clients = malloc(sizeof(Window) * count);
+ wm.cycle_count = 0;
+ wm.active_cycle_index = 0;
+
+ // Filter for current desktop and mapped windows
+ c = wm.clients;
+ int current_window_index = -1;
+
+ while (c) {
+ Window w = c->window;
+
+ unsigned long desktop = 0;
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+
+ XErrorHandler old = XSetErrorHandler(ignore_x_error);
+ int status = XGetWindowProperty(wm.dpy, w, _NET_WM_DESKTOP, 0, 1, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, &bytes_after, &prop);
+ XSync(wm.dpy, False);
+ XSetErrorHandler(old);
+
+ int on_current_desktop = 0;
+ if (status == Success && prop && nitems > 0) {
+ desktop = *(unsigned long *)prop;
+ if (desktop == wm.current_desktop) {
+ on_current_desktop = 1;
+ }
+ }
+ if (prop) XFree(prop);
+ if (is_sticky(w)) on_current_desktop = 1;
+
+ if (on_current_desktop) {
+ XWindowAttributes wa;
+ XGetWindowAttributes(wm.dpy, w, &wa);
+ if (wa.map_state == IsViewable) {
+ wm.cycle_clients[wm.cycle_count] = w;
+ if (w == wm.active) {
+ current_window_index = wm.cycle_count;
+ }
+ wm.cycle_count++;
+ }
+ }
+ c = c->next;
+ }
+
+ if (wm.cycle_count == 0) {
+ end_cycling();
+ return;
+ }
+
+ wm.active_cycle_index = (current_window_index + 1) % wm.cycle_count;
+
+ // Create switcher window
+ int box_size = 100;
+
+ int width = (box_size * wm.cycle_count);
+ int height = box_size;
+ int screen_width = DisplayWidth(wm.dpy, wm.screen);
+ int screen_height = DisplayHeight(wm.dpy, wm.screen);
+ int x = (screen_width - width) / 2;
+ int y = (screen_height * 2) / 3;
+
+ XSetWindowAttributes wa;
+ wa.override_redirect = True;
+ wa.background_pixel = BlackPixel(wm.dpy, wm.screen);
+ wa.border_pixel = BlackPixel(wm.dpy, wm.screen);
+
+ wm.cycle_win = XCreateWindow(wm.dpy, wm.root, x, y, width, height, 0, CopyFromParent, InputOutput, CopyFromParent, CWOverrideRedirect | CWBackPixel | CWBorderPixel, &wa);
+
+ XMapRaised(wm.dpy, wm.cycle_win);
+ } else {
+ // Already cycling, just move selection
+ int delta = (arg->i == 0) ? 1 : -1;
+ wm.active_cycle_index = (wm.active_cycle_index + delta + wm.cycle_count) % wm.cycle_count;
+ }
+
+ draw_switcher();
+}
diff --git a/widgets.c b/widgets.c
new file mode 100644
index 0000000..3c043c3
--- /dev/null
+++ b/widgets.c
@@ -0,0 +1,66 @@
+#include <time.h>
+#include <string.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xft/Xft.h>
+
+#include "glitch.h"
+#include "config.h"
+
+extern WindowManager wm;
+
+void widget_desktop_indicator(void) {
+ int screen_width = DisplayWidth(wm.dpy, wm.screen);
+ int padding = 3;
+
+ char buf[8];
+ snprintf(buf, sizeof(buf), "%u", wm.current_desktop);
+
+ XGlyphInfo extents;
+ XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)buf, strlen(buf), &extents);
+
+ int size = (wm.font->height > extents.width ? wm.font->height : extents.width) + padding * 2;
+ int x = screen_width - size - 10;
+ int y = 10;
+
+ // Draw the background square.
+ XftDrawRect(wm.xft_draw, &wm.xft_bg_color, x, y, size, size);
+
+ // Center the text in the square.
+ int text_x = x + (size - extents.width) / 2 + extents.x;
+ int text_y = y + (size - wm.font->ascent - wm.font->descent) / 2 + wm.font->ascent;
+
+ XftDrawStringUtf8(wm.xft_draw, &wm.xft_color, wm.font, text_x, text_y, (FcChar8 *)buf, strlen(buf));
+}
+
+void widget_datetime(void) {
+ int screen_width = DisplayWidth(wm.dpy, wm.screen);
+ int padding = 3;
+
+ // We need to know the desktop indicator size to position the time correctly.
+ char desktop_buf[8];
+ snprintf(desktop_buf, sizeof(desktop_buf), "%u", wm.current_desktop);
+ XGlyphInfo desktop_extents;
+ XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)desktop_buf, strlen(desktop_buf), &desktop_extents);
+ int desktop_size = (wm.font->height > desktop_extents.width ? wm.font->height : desktop_extents.width) + padding * 2;
+ int desktop_x = screen_width - desktop_size - 10;
+
+ char time_buf[64];
+ time_t now = time(NULL);
+ struct tm *tm_info = localtime(&now);
+ strftime(time_buf, sizeof(time_buf), time_format, tm_info);
+
+ XGlyphInfo time_extents;
+ XftTextExtentsUtf8(wm.dpy, wm.font, (FcChar8 *)time_buf, strlen(time_buf), &time_extents);
+
+ int time_x = desktop_x - time_extents.xOff - 20;
+ int y = 10;
+ int win_height = desktop_size;
+
+ // Draw the background.
+ XftDrawRect(wm.xft_draw, &wm.xft_root_bg_color, time_x - 50, y, time_extents.xOff + 50, win_height);
+
+ // Draw the time.
+ int time_text_y = y + (win_height - wm.font->ascent - wm.font->descent) / 2 + wm.font->ascent;
+ XftDrawStringUtf8(wm.xft_draw, &wm.xft_color, wm.font, time_x, time_text_y, (FcChar8 *)time_buf, strlen(time_buf));
+}