Files
OrionOS/wm/src/events.c
T
Claude a2092fd989 Add hot corners, media keys, orionshot, orionclip, notification history, autostart, GTK accent update, wallpaper slideshow, ISO build
- wm: hot corner polling via select()-based wm_run loop; TL=Exposé, TR=show desktop, BL=launcher, BR=lock
- wm: XF86 media key grabs (volume, brightness, playback) + Super+V for clipboard
- wm: autostart reads ~/.config/orionos/autostart on startup
- events: media key dispatch (amixer/brightnessctl/playerctl); Super+V → orionclip --show; Print → orionshot
- orionshot: new X11 screenshot tool, full-screen and rubber-band region modes, saves PNG to ~/Pictures/Screenshots/
- orionclip: new clipboard manager daemon; polls clipboard, stores 20 entries, popup via Super+V IPC
- orionnotify: append-only notification history log at ~/.local/share/orionos/notif-history.log
- themes: GTK accent updated from #3A6898 to #1E58A8 to match WM/deskbar
- themes: orionwallpaper gains --slideshow DIR [SECS] mode using feh + sleep loop
- iso: Docker-based archiso builder, live-session xinitrc, profiledef.sh
- Makefile: add orionshot and orionclip to all/clean/install; Docker-based iso target
2026-08-30 20:47:47 +00:00

500 lines
17 KiB
C

#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);
}