diff options
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | .vimrc | 63 | ||||
| -rw-r--r-- | LICENSE | 24 | ||||
| -rw-r--r-- | Makefile | 31 | ||||
| -rw-r--r-- | README.md | 230 | ||||
| -rw-r--r-- | config.def.h | 92 | ||||
| -rw-r--r-- | glitch.h | 164 | ||||
| -rw-r--r-- | logging.c | 71 | ||||
| -rw-r--r-- | main.c | 124 | ||||
| -rw-r--r-- | manager.c | 1584 | ||||
| -rw-r--r-- | switcher.c | 187 | ||||
| -rw-r--r-- | widgets.c | 66 |
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 + @@ -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 @@ -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); +} @@ -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 *)¤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 <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)); +} |
