#include "wm.h" /* ── helpers ─────────────────────────────────────────────────────────── */ static void send_delete(WM *wm, Window win) { XEvent ev = {0}; ev.xclient.type = ClientMessage; ev.xclient.window = win; ev.xclient.message_type = wm->wm_protocols; ev.xclient.format = 32; ev.xclient.data.l[0] = wm->wm_delete_window; ev.xclient.data.l[1] = CurrentTime; XSendEvent(wm->dpy, win, False, NoEventMask, &ev); } static void set_cursor(WM *wm, Client *c, int x, int y) { Cursor cur; if (!c->maximized && frame_hit_resize(c, x, y)) cur = wm->cur_resize; else if (y < TITLE_HEIGHT) cur = wm->cur_move; else cur = wm->cur_normal; XDefineCursor(wm->dpy, c->frame, cur); } /* ── snap on drag release ────────────────────────────────────────────── */ static void check_snap(WM *wm, Client *c) { int sw = DisplayWidth(wm->dpy, wm->screen); int sh = DisplayHeight(wm->dpy, wm->screen); if (c->x <= SNAP_DIST) { if (c->y <= SNAP_DIST) { client_maximize(wm, c); } else { client_snap_left(wm, c); } } else if (c->x + (int)(c->w + BORDER_W * 2) >= sw - DESKBAR_W - SNAP_DIST) { client_snap_right(wm, c); } else if (c->y <= SNAP_DIST) { client_maximize(wm, c); } else { (void)sh; } } /* ── event handlers ──────────────────────────────────────────────────── */ void on_map_request(WM *wm, XMapRequestEvent *e) { client_add(wm, e->window); } void on_unmap_notify(WM *wm, XUnmapEvent *e) { if (e->send_event) return; Client *c = client_find_by_win(wm, e->window); if (c && !c->minimized) client_remove(wm, e->window); } void on_destroy_notify(WM *wm, XDestroyWindowEvent *e) { Client *c = client_find_by_win(wm, e->window); if (!c) c = client_find_by_frame(wm, e->window); if (c) client_remove(wm, c->win); } void on_expose(WM *wm, XExposeEvent *e) { if (e->count != 0) return; if (wm->menu_visible && e->window == wm->menu_win) { menu_draw(wm); return; } if (wm->swl_visible && e->window == wm->swl_win) { switcher_draw(wm); return; } Client *c = client_find_by_frame(wm, e->window); if (c && !c->minimized) frame_draw(wm, c); } void on_configure_request(WM *wm, XConfigureRequestEvent *e) { Client *c = client_find_by_win(wm, e->window); XWindowChanges wc; wc.x = e->x; wc.y = e->y; wc.width = e->width; wc.height = e->height; wc.border_width = e->border_width; wc.sibling = e->above; wc.stack_mode = e->detail; if (c && !c->maximized) { c->w = e->width; c->h = e->height; XResizeWindow(wm->dpy, c->frame, c->w + BORDER_W*2, c->h + TITLE_HEIGHT + BORDER_W); } XConfigureWindow(wm->dpy, e->window, e->value_mask, &wc); } void on_enter_notify(WM *wm, XCrossingEvent *e) { Client *c = client_find_by_frame(wm, e->window); if (c && !c->minimized) set_cursor(wm, c, e->x, e->y); } static void wm_switch_desktop(WM *wm, int new_desk) { if (new_desk < 0 || new_desk >= 4 || new_desk == wm->cur_desktop) return; /* hide windows on old desktop */ for (Client *c = wm->clients; c; c = c->next) { if (!c->minimized && c->desktop == wm->cur_desktop) XUnmapWindow(wm->dpy, c->frame); } wm->cur_desktop = new_desk; /* show windows on new desktop */ for (Client *c = wm->clients; c; c = c->next) { if (!c->minimized && c->desktop == new_desk) XMapWindow(wm->dpy, c->frame); } /* update focus */ if (wm->focused && wm->focused->desktop != new_desk) { wm->focused->focused = 0; frame_draw(wm, wm->focused); wm->focused = NULL; } if (!wm->focused) { for (Client *c = wm->clients; c; c = c->next) { if (!c->minimized && c->desktop == new_desk) { client_focus(wm, c); break; } } } /* advertise new desktop */ long nd = new_desk; XChangeProperty(wm->dpy, wm->root, wm->net_current_desktop, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&nd, 1); } void on_client_message(WM *wm, XClientMessageEvent *e) { Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False); Atom net_close = XInternAtom(wm->dpy, "_NET_CLOSE_WINDOW", False); Atom wm_change_state = XInternAtom(wm->dpy, "WM_CHANGE_STATE", False); Atom net_wm_desktop = XInternAtom(wm->dpy, "_NET_WM_DESKTOP", False); /* _NET_CURRENT_DESKTOP → switch virtual desktop */ if (e->message_type == wm->net_current_desktop && e->window == wm->root) { wm_switch_desktop(wm, (int)e->data.l[0]); return; } /* _NET_WM_DESKTOP → move window to desktop */ if (e->message_type == net_wm_desktop) { Client *c = client_find_by_win(wm, e->window); if (c) client_move_to_desktop(wm, c, (int)e->data.l[0]); return; } if (e->message_type == net_active) { Client *c = client_find_by_win(wm, e->window); if (c) client_restore(wm, c); return; } if (e->message_type == net_close) { Client *c = client_find_by_win(wm, e->window); if (c) { XEvent ev = {0}; ev.xclient.type = ClientMessage; ev.xclient.window = c->win; ev.xclient.message_type = wm->wm_protocols; ev.xclient.format = 32; ev.xclient.data.l[0] = wm->wm_delete_window; ev.xclient.data.l[1] = CurrentTime; XSendEvent(wm->dpy, c->win, False, NoEventMask, &ev); } return; } /* WM_CHANGE_STATE IconicState → minimize */ if (e->message_type == wm_change_state && e->data.l[0] == 3) { Client *c = client_find_by_win(wm, e->window); if (c && !c->minimized) client_minimize(wm, c); return; } } /* Called when a managed window changes a property (title, etc.) */ void on_property_notify(WM *wm, XPropertyEvent *e) { Client *c = client_find_by_win(wm, e->window); if (!c || c->minimized) return; Atom wm_name = XInternAtom(wm->dpy, "WM_NAME", False); Atom net_wm_name = XInternAtom(wm->dpy, "_NET_WM_NAME", False); Atom utf8 = XInternAtom(wm->dpy, "UTF8_STRING", False); if (e->atom != wm_name && e->atom != net_wm_name) return; /* re-fetch title */ char new_title[256] = {0}; { Atom type; int fmt; unsigned long n, after; unsigned char *data = NULL; if (XGetWindowProperty(wm->dpy, c->win, net_wm_name, 0, 256, False, utf8, &type, &fmt, &n, &after, &data) == Success && data && n > 0) { snprintf(new_title, 255, "%.*s", (int)n, data); XFree(data); } else { if (data) XFree(data); char *name = NULL; if (XFetchName(wm->dpy, c->win, &name) && name) { strncpy(new_title, name, 255); XFree(name); } } } if (new_title[0] && strcmp(new_title, c->title) != 0) { strncpy(c->title, new_title, 255); frame_draw(wm, c); } } void on_button_press(WM *wm, XButtonEvent *e) { /* Exposé — click on any frame to select and exit */ if (wm->expose_active) { Client *c = client_find_by_frame(wm, e->window); if (c) expose_hide(wm, c->frame); else expose_hide(wm, None); return; } /* menu window */ if (wm->menu_visible && e->window == wm->menu_win) { if (e->button == Button1) menu_click(wm, e->x, e->y); else menu_hide(wm); return; } /* right-click root → menu */ if (e->window == wm->root && e->button == Button3) { menu_show(wm, e->x_root, e->y_root); return; } if (wm->menu_visible) { menu_hide(wm); return; } Client *c = client_find_by_frame(wm, e->window); if (!c || c->minimized) return; client_focus(wm, c); if (e->button != Button1) return; if (frame_hit_close(c, e->x, e->y)) { send_delete(wm, c->win); return; } if (frame_hit_maximize(c, e->x, e->y)) { client_toggle_maximize(wm, c); return; } if (frame_hit_minimize(c, e->x, e->y)) { client_minimize(wm, c); return; } if (!c->maximized && frame_hit_resize(c, e->x, e->y)) { wm->drag_mode = DRAG_RESIZE; wm->drag_frame = c->frame; wm->drag_start_x = e->x_root; wm->drag_start_y = e->y_root; wm->drag_win_w = c->w; wm->drag_win_h = c->h; XGrabPointer(wm->dpy, c->frame, False, ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, wm->cur_resize, CurrentTime); return; } if (e->y < TITLE_HEIGHT) { /* double-click title bar → maximize toggle */ if (e->time - c->last_click_time < DBL_CLICK_MS) { client_toggle_maximize(wm, c); c->last_click_time = 0; return; } c->last_click_time = e->time; if (c->maximized) return; /* don't drag a maximized window */ wm->drag_mode = DRAG_MOVE; wm->drag_frame = c->frame; wm->drag_start_x = e->x_root; wm->drag_start_y = e->y_root; wm->drag_win_x = c->x; wm->drag_win_y = c->y; XGrabPointer(wm->dpy, c->frame, False, ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, wm->cur_move, CurrentTime); } } void on_button_release(WM *wm, XButtonEvent *e) { (void)e; if (wm->drag_mode == DRAG_MOVE) { /* snap check after move */ Client *c = client_find_by_frame(wm, wm->drag_frame); if (c) check_snap(wm, c); } if (wm->drag_mode != DRAG_NONE) { wm->drag_mode = DRAG_NONE; XUngrabPointer(wm->dpy, CurrentTime); } } void on_motion_notify(WM *wm, XMotionEvent *e) { if (wm->menu_visible && e->window == wm->menu_win) { int idx = menu_item_at(e->y); if (idx != wm->menu_hovered) { wm->menu_hovered = idx; menu_draw(wm); } return; } if (wm->drag_mode == DRAG_NONE) { Client *c = client_find_by_frame(wm, e->window); if (c) set_cursor(wm, c, e->x, e->y); return; } Client *c = client_find_by_frame(wm, wm->drag_frame); if (!c) return; int dx = e->x_root - wm->drag_start_x; int dy = e->y_root - wm->drag_start_y; if (wm->drag_mode == DRAG_MOVE) { c->x = wm->drag_win_x + dx; c->y = wm->drag_win_y + dy; if (c->y < 0) c->y = 0; /* don't drag above screen top */ XMoveWindow(wm->dpy, c->frame, c->x, c->y); } else { int nw = (int)wm->drag_win_w + dx; int nh = (int)wm->drag_win_h + dy; /* clamp to minimum */ if (nw < c->min_w) nw = c->min_w; if (nh < c->min_h) nh = c->min_h; /* snap to resize-increment grid (e.g. terminal character cells) */ if (c->inc_w > 1) { nw = c->base_w + ((nw - c->base_w) / c->inc_w) * c->inc_w; if (nw < c->min_w) nw += c->inc_w; } if (c->inc_h > 1) { nh = c->base_h + ((nh - c->base_h) / c->inc_h) * c->inc_h; if (nh < c->min_h) nh += c->inc_h; } c->w = (unsigned)nw; c->h = (unsigned)nh; XResizeWindow(wm->dpy, c->frame, c->w+BORDER_W*2, c->h+TITLE_HEIGHT+BORDER_W); XResizeWindow(wm->dpy, c->win, c->w, c->h); frame_draw(wm, c); } } void on_key_press(WM *wm, XKeyEvent *e) { KeySym sym = XLookupKeysym(e, 0); /* Alt+F4 — close focused window */ if ((e->state & Mod1Mask) && sym == XK_F4 && wm->focused) send_delete(wm, wm->focused->win); /* Alt+Tab / Shift+Alt+Tab — visual switcher */ if ((e->state & Mod1Mask) && sym == XK_Tab) { int backward = (e->state & ShiftMask) != 0; if (wm->swl_visible) { if (backward) switcher_prev(wm); else switcher_next(wm); } else if (wm->clients) { switcher_show(wm); if (backward && wm->swl_visible) { wm->swl_idx = wm->swl_count - 1; switcher_draw(wm); } } return; } /* While switcher is visible, eat all other keys until Alt released */ if (wm->swl_visible) { if (sym == XK_Escape) switcher_hide(wm, 0); return; } /* Alt+F9 — minimize */ if ((e->state & Mod1Mask) && sym == XK_F9 && wm->focused) client_minimize(wm, wm->focused); /* Alt+F10 — maximize toggle */ if ((e->state & Mod1Mask) && sym == XK_F10 && wm->focused) client_toggle_maximize(wm, wm->focused); /* Alt+F1 — launcher */ if ((e->state & Mod1Mask) && sym == XK_F1) system("orionlauncher &"); /* PrintScreen — screenshot */ if (sym == XK_Print) { if (e->state & ShiftMask) system("orionshot region &"); else system("orionshot full &"); } /* XF86 media keys */ if (sym == XF86XK_AudioRaiseVolume) system("pactl set-sink-volume @DEFAULT_SINK@ +5% 2>/dev/null; " "pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | " "grep -oP '[0-9]+%' | head -1 | " "xargs -I{} notify-send -t 1500 'Volume' '{}' 2>/dev/null &"); if (sym == XF86XK_AudioLowerVolume) system("pactl set-sink-volume @DEFAULT_SINK@ -5% 2>/dev/null; " "pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | " "grep -oP '[0-9]+%' | head -1 | " "xargs -I{} notify-send -t 1500 'Volume' '{}' 2>/dev/null &"); if (sym == XF86XK_AudioMute) system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null; " "notify-send -t 1500 'Volume' 'Muted toggled' 2>/dev/null &"); if (sym == XF86XK_AudioPlay) system("playerctl play-pause 2>/dev/null &"); if (sym == XF86XK_AudioNext) system("playerctl next 2>/dev/null &"); if (sym == XF86XK_AudioPrev) system("playerctl previous 2>/dev/null &"); if (sym == XF86XK_MonBrightnessUp) system("brightnessctl set +10% 2>/dev/null; " "notify-send -t 1200 'Brightness' " "\"$(brightnessctl g 2>/dev/null)\" 2>/dev/null &"); if (sym == XF86XK_MonBrightnessDown) system("brightnessctl set 10%- 2>/dev/null; " "notify-send -t 1200 'Brightness' " "\"$(brightnessctl g 2>/dev/null)\" 2>/dev/null &"); /* Super+V — clipboard manager */ if ((e->state & Mod4Mask) && sym == XK_v) system("orionclip --show &"); /* Super+Left — snap left */ if ((e->state & Mod4Mask) && sym == XK_Left && wm->focused) client_snap_left(wm, wm->focused); /* Super+Right — snap right */ if ((e->state & Mod4Mask) && sym == XK_Right && wm->focused) client_snap_right(wm, wm->focused); /* Super+Up — maximize */ if ((e->state & Mod4Mask) && sym == XK_Up && wm->focused) client_maximize(wm, wm->focused); /* Super+Down — unmaximize / restore */ if ((e->state & Mod4Mask) && sym == XK_Down && wm->focused) client_unmaximize(wm, wm->focused); /* Super+Space — Exposé window overview */ if ((e->state & Mod4Mask) && sym == XK_space) { if (wm->expose_active) expose_hide(wm, None); else expose_show(wm); return; } /* Super+1-4 — switch to desktop N */ if ((e->state & Mod4Mask) && !(e->state & ShiftMask)) { int desk = -1; if (sym == XK_1) desk = 0; else if (sym == XK_2) desk = 1; else if (sym == XK_3) desk = 2; else if (sym == XK_4) desk = 3; if (desk >= 0) { if (wm->expose_active) expose_hide(wm, None); wm_switch_desktop(wm, desk); return; } } /* Super+Shift+1-4 — move focused window to desktop N */ if ((e->state & Mod4Mask) && (e->state & ShiftMask) && wm->focused) { int desk = -1; if (sym == XK_1) desk = 0; else if (sym == XK_2) desk = 1; else if (sym == XK_3) desk = 2; else if (sym == XK_4) desk = 3; if (desk >= 0) { client_move_to_desktop(wm, wm->focused, desk); return; } } /* Escape — hide menu or exit Exposé */ if (sym == XK_Escape) { if (wm->expose_active) expose_hide(wm, None); else if (wm->menu_visible) menu_hide(wm); } } void on_key_release(WM *wm, XKeyEvent *e) { if (!wm->swl_visible) return; KeySym sym = XLookupKeysym(e, 0); if (sym == XK_Alt_L || sym == XK_Alt_R || sym == XK_Meta_L || sym == XK_Meta_R) switcher_hide(wm, 1); }