- 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
377 lines
13 KiB
C
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);
|
|
}
|