Files
OrionOS/wm/src/client.c
T
Claude 2a42b826fb Add 6 desktop features: workspace shortcuts, Exposé, deskbar icons, notifications, screen lock, move-to-desktop
- Super+1-4: switch virtual desktop; Super+Shift+1-4: move focused window to desktop N
- Super+Space: Exposé window overview (tiled grid), click to select, Escape to cancel
- wm/src/expose.c: expose_show/expose_hide using ceil(sqrt) grid layout with saved/restored geometry
- client_move_to_desktop(): moves window between desktops, fires _NET_WM_DESKTOP update
- on_client_message: handles _NET_WM_DESKTOP to move windows from external requests
- deskbar: optional imlib2 app icon rendering (Papirus-Dark → Papirus → hicolor → pixmaps fallback)
- deskbar: XGetClassHint populates cls field for better icon lookup
- deskbar: context menu extended with separator + "→ Desktop 1-4" items via _NET_WM_DESKTOP
- orionnotify/: freedesktop.org notification daemon (libdbus-1), stacks up to 5 popups top-right
- orionlock/: PAM-authenticated fullscreen screen lock with clock, date, password dots
- Lock Screen menu item wired to orionlock
- Root Makefile includes orionnotify and orionlock targets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHiBQKPfUoaJgCKuA9op1H
2026-08-30 20:14:45 +00:00

377 lines
13 KiB
C

#include "wm.h"
/* ── WM_STATE ICCCM helper ───────────────────────────────────────────── */
void client_set_wm_state(WM *wm, Client *c, long state) {
long data[2] = { state, None };
XChangeProperty(wm->dpy, c->win, wm->wm_state, wm->wm_state,
32, PropModeReplace, (unsigned char *)data, 2);
if (state == WM_STATE_ICONIC) {
XChangeProperty(wm->dpy, c->win, wm->net_wm_state, XA_ATOM,
32, PropModeReplace,
(unsigned char *)&wm->net_wm_state_hidden, 1);
} else {
XDeleteProperty(wm->dpy, c->win, wm->net_wm_state);
}
}
static void update_net_client_list(WM *wm) {
Atom net_client_list = XInternAtom(wm->dpy, "_NET_CLIENT_LIST", False);
int count = 0;
for (Client *c = wm->clients; c; c = c->next) count++;
Window *wins = calloc(count, sizeof(Window));
int i = 0;
for (Client *c = wm->clients; c; c = c->next) wins[i++] = c->win;
XChangeProperty(wm->dpy, wm->root, net_client_list, XA_WINDOW, 32,
PropModeReplace, (unsigned char *)wins, count);
free(wins);
}
/* ── add / remove ────────────────────────────────────────────────────── */
/* smart placement: cascade from top-left, offset each window */
static void smart_place(WM *wm, Client *c) {
static int cascade_x = 40, cascade_y = 40;
int sw = DisplayWidth(wm->dpy, wm->screen);
int sh = DisplayHeight(wm->dpy, wm->screen);
/* reset cascade if it would go off-screen */
if (cascade_x + (int)c->w > sw - 40 || cascade_y + (int)c->h > sh - 40) {
cascade_x = 40;
cascade_y = 40;
}
c->x = cascade_x;
c->y = cascade_y;
cascade_x += 30;
cascade_y += 30;
}
Client *client_add(WM *wm, Window win) {
XWindowAttributes wa;
if (!XGetWindowAttributes(wm->dpy, win, &wa)) return NULL;
if (wa.override_redirect) return NULL;
if (client_find_by_win(wm, win)) return NULL;
Client *c = calloc(1, sizeof(Client));
c->win = win;
c->w = wa.width;
c->h = wa.height;
c->desktop = wm->cur_desktop;
c->min_w = MIN_WIN_W;
c->min_h = MIN_WIN_H;
/* use application-requested position if non-zero, otherwise smart place */
if (wa.x == 0 && wa.y == 0) {
smart_place(wm, c);
} else {
c->x = wa.x;
c->y = wa.y;
}
/* never land behind deskbar */
int sw = DisplayWidth(wm->dpy, wm->screen);
if (c->x + (int)c->w > sw - DESKBAR_W && c->y < 200)
c->y = 40;
/* fetch title */
char *name = NULL;
Atom utf8 = XInternAtom(wm->dpy, "UTF8_STRING", False);
Atom net_wm = XInternAtom(wm->dpy, "_NET_WM_NAME", False);
{
Atom type; int fmt; unsigned long n, after;
unsigned char *data = NULL;
if (XGetWindowProperty(wm->dpy, win, net_wm, 0, 256, False,
utf8, &type, &fmt, &n, &after, &data)
== Success && data && n > 0) {
snprintf(c->title, 255, "%.*s", (int)n, data);
XFree(data);
} else {
if (data) XFree(data);
if (XFetchName(wm->dpy, win, &name) && name) {
strncpy(c->title, name, 255);
XFree(name);
} else {
strncpy(c->title, "Untitled", 255);
}
}
}
/* ICCCM WM_NORMAL_HINTS — size constraints and resize increments */
{
XSizeHints hints;
long supplied = 0;
if (XGetWMNormalHints(wm->dpy, win, &hints, &supplied)) {
if (hints.flags & PMinSize) {
if (hints.min_width > c->min_w) c->min_w = hints.min_width;
if (hints.min_height > c->min_h) c->min_h = hints.min_height;
}
if (hints.flags & PResizeInc) {
c->inc_w = hints.width_inc;
c->inc_h = hints.height_inc;
}
if (hints.flags & PBaseSize) {
c->base_w = hints.base_width;
c->base_h = hints.base_height;
} else if (hints.flags & PMinSize) {
c->base_w = hints.min_width;
c->base_h = hints.min_height;
}
}
}
/* advertise which desktop this window lives on */
{
long desk = wm->cur_desktop;
XChangeProperty(wm->dpy, win, wm->net_wm_desktop, XA_CARDINAL, 32,
PropModeReplace, (unsigned char *)&desk, 1);
}
int fw = (int)c->w + BORDER_W * 2;
int fh = (int)c->h + TITLE_HEIGHT + BORDER_W;
XSetWindowAttributes swa;
swa.background_pixel = color_alloc(wm, COLOR_TITLE_INACTIVE);
swa.event_mask = SubstructureRedirectMask | SubstructureNotifyMask
| ButtonPressMask | ButtonReleaseMask
| PointerMotionMask | ExposureMask
| EnterWindowMask;
swa.override_redirect = False;
swa.cursor = wm->cur_normal;
c->frame = XCreateWindow(
wm->dpy, wm->root,
c->x, c->y, fw, fh, 0,
CopyFromParent, InputOutput, CopyFromParent,
CWBackPixel | CWEventMask | CWOverrideRedirect | CWCursor,
&swa
);
XReparentWindow(wm->dpy, win, c->frame, BORDER_W, TITLE_HEIGHT);
XSelectInput(wm->dpy, win, PropertyChangeMask | FocusChangeMask);
c->next = wm->clients;
wm->clients = c;
XMapWindow(wm->dpy, c->frame);
XMapWindow(wm->dpy, win);
client_set_wm_state(wm, c, WM_STATE_NORMAL);
update_net_client_list(wm);
client_focus(wm, c);
return c;
}
void client_remove(WM *wm, Window win) {
Client *prev = NULL, *c = wm->clients;
while (c) {
if (c->win == win || c->frame == win) {
if (prev) prev->next = c->next;
else wm->clients = c->next;
if (wm->focused == c) {
wm->focused = wm->clients;
if (wm->focused) client_focus(wm, wm->focused);
else wm->focused = NULL;
}
XDestroyWindow(wm->dpy, c->frame);
free(c);
update_net_client_list(wm);
return;
}
prev = c; c = c->next;
}
}
/* ── lookup ──────────────────────────────────────────────────────────── */
Client *client_find_by_frame(WM *wm, Window frame) {
for (Client *c = wm->clients; c; c = c->next)
if (c->frame == frame) return c;
return NULL;
}
Client *client_find_by_win(WM *wm, Window win) {
for (Client *c = wm->clients; c; c = c->next)
if (c->win == win) return c;
return NULL;
}
/* ── focus / raise ───────────────────────────────────────────────────── */
void client_focus(WM *wm, Client *c) {
if (!c || c->minimized) return;
if (wm->focused && wm->focused != c) {
wm->focused->focused = 0;
frame_draw(wm, wm->focused);
}
wm->focused = c;
c->focused = 1;
XSetInputFocus(wm->dpy, c->win, RevertToPointerRoot, CurrentTime);
XRaiseWindow(wm->dpy, c->frame);
frame_draw(wm, c);
/* advertise active window */
Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False);
XChangeProperty(wm->dpy, wm->root, net_active, XA_WINDOW, 32,
PropModeReplace, (unsigned char *)&c->win, 1);
}
void client_raise(WM *wm, Client *c) {
XRaiseWindow(wm->dpy, c->frame);
}
/* ── minimize / restore ──────────────────────────────────────────────── */
void client_minimize(WM *wm, Client *c) {
if (c->minimized) return;
if (c->maximized) client_unmaximize(wm, c);
c->minimized = 1;
c->focused = 0;
XUnmapWindow(wm->dpy, c->frame);
client_set_wm_state(wm, c, WM_STATE_ICONIC);
if (wm->focused == c) {
wm->focused = NULL;
for (Client *n = wm->clients; n; n = n->next) {
if (!n->minimized) { client_focus(wm, n); break; }
}
}
}
void client_restore(WM *wm, Client *c) {
if (!c->minimized) { client_focus(wm, c); return; }
c->minimized = 0;
XMapWindow(wm->dpy, c->frame);
client_set_wm_state(wm, c, WM_STATE_NORMAL);
client_focus(wm, c);
}
/* ── maximize / unmaximize ───────────────────────────────────────────── */
void client_maximize(WM *wm, Client *c) {
if (c->maximized || c->minimized) return;
/* save current geometry */
c->saved_x = c->x;
c->saved_y = c->y;
c->saved_w = c->w;
c->saved_h = c->h;
c->maximized = 1;
int sw = DisplayWidth(wm->dpy, wm->screen);
int sh = DisplayHeight(wm->dpy, wm->screen);
/* leave room for deskbar on the right */
int deskbar_w = DESKBAR_W;
int new_w = sw - deskbar_w - BORDER_W * 2;
int new_h = sh - TITLE_HEIGHT - BORDER_W;
c->x = 0; c->y = 0;
c->w = (unsigned)new_w;
c->h = (unsigned)new_h;
int fw = (int)c->w + BORDER_W * 2;
int fh = (int)c->h + TITLE_HEIGHT + BORDER_W;
XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh);
XResizeWindow(wm->dpy, c->win, c->w, c->h);
/* advertise maximized state via EWMH */
Atom states[2] = { wm->net_wm_state_maximized_vert,
wm->net_wm_state_maximized_horz };
XChangeProperty(wm->dpy, c->win, wm->net_wm_state, XA_ATOM, 32,
PropModeReplace, (unsigned char *)states, 2);
client_focus(wm, c);
frame_draw(wm, c);
}
void client_unmaximize(WM *wm, Client *c) {
if (!c->maximized) return;
c->maximized = 0;
c->x = c->saved_x;
c->y = c->saved_y;
c->w = c->saved_w;
c->h = c->saved_h;
int fw = (int)c->w + BORDER_W * 2;
int fh = (int)c->h + TITLE_HEIGHT + BORDER_W;
XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh);
XResizeWindow(wm->dpy, c->win, c->w, c->h);
XDeleteProperty(wm->dpy, c->win, wm->net_wm_state);
client_focus(wm, c);
frame_draw(wm, c);
}
void client_toggle_maximize(WM *wm, Client *c) {
if (c->maximized) client_unmaximize(wm, c);
else client_maximize(wm, c);
}
/* ── edge snap ───────────────────────────────────────────────────────── */
void client_snap_left(WM *wm, Client *c) {
if (c->maximized) client_unmaximize(wm, c);
int sw = DisplayWidth(wm->dpy, wm->screen);
int sh = DisplayHeight(wm->dpy, wm->screen);
int deskbar_w = DESKBAR_W;
int half_w = (sw - deskbar_w) / 2;
c->x = 0;
c->y = 0;
c->w = (unsigned)(half_w - BORDER_W * 2);
c->h = (unsigned)(sh - TITLE_HEIGHT - BORDER_W);
int fw = (int)c->w + BORDER_W * 2;
int fh = (int)c->h + TITLE_HEIGHT + BORDER_W;
XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh);
XResizeWindow(wm->dpy, c->win, c->w, c->h);
frame_draw(wm, c);
}
void client_move_to_desktop(WM *wm, Client *c, int new_desk) {
if (new_desk < 0 || new_desk >= 4 || c->desktop == new_desk) return;
/* hide frame if it's on the current (now-active) desktop */
if (c->desktop == wm->cur_desktop && !c->minimized)
XUnmapWindow(wm->dpy, c->frame);
c->desktop = new_desk;
/* update _NET_WM_DESKTOP */
long desk = new_desk;
XChangeProperty(wm->dpy, c->win, wm->net_wm_desktop, XA_CARDINAL, 32,
PropModeReplace, (unsigned char *)&desk, 1);
/* if we moved the focused window away, re-focus another */
if (wm->focused == c) {
wm->focused->focused = 0;
frame_draw(wm, wm->focused);
wm->focused = NULL;
for (Client *n = wm->clients; n; n = n->next) {
if (!n->minimized && n->desktop == wm->cur_desktop) {
client_focus(wm, n);
break;
}
}
}
}
void client_snap_right(WM *wm, Client *c) {
if (c->maximized) client_unmaximize(wm, c);
int sw = DisplayWidth(wm->dpy, wm->screen);
int sh = DisplayHeight(wm->dpy, wm->screen);
int deskbar_w = DESKBAR_W;
int half_w = (sw - deskbar_w) / 2;
c->x = half_w;
c->y = 0;
c->w = (unsigned)(sw - deskbar_w - half_w - BORDER_W * 2);
c->h = (unsigned)(sh - TITLE_HEIGHT - BORDER_W);
int fw = (int)c->w + BORDER_W * 2;
int fh = (int)c->h + TITLE_HEIGHT + BORDER_W;
XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh);
XResizeWindow(wm->dpy, c->win, c->w, c->h);
frame_draw(wm, c);
}