From 288f12d36843b6e404adb35857fcd87943e63944 Mon Sep 17 00:00:00 2001 From: Mitja Felicijan Date: Sat, 24 Jan 2026 17:17:21 +0100 Subject: Engage! --- .gitignore | 4 + .vimrc | 63 +++ LICENSE | 24 + Makefile | 31 ++ README.md | 230 +++++++++ config.def.h | 92 ++++ glitch.h | 164 ++++++ logging.c | 71 +++ main.c | 124 +++++ manager.c | 1584 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ switcher.c | 187 +++++++ widgets.c | 66 +++ 12 files changed, 2640 insertions(+) create mode 100644 .gitignore create mode 100644 .vimrc create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 config.def.h create mode 100644 glitch.h create mode 100644 logging.c create mode 100644 main.c create mode 100644 manager.c create mode 100644 switcher.c create mode 100644 widgets.c 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 x :call LocalRun() +nnoremap c :call LocalMake() +nnoremap m :call LocalDebugMain() +nnoremap l :call LocalDebugLine() + +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 + +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 +#include + +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include + +#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 +#include + +#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 +#include +#include + +#include +#include +#include +#include +#include +#include + +#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 *)¤t_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 +#include + +#include +#include + +#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 +#include + +#include +#include + +#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)); +} -- cgit v1.2.3