Fixes #7–#11: launcher Other tab, ICCCM size hints, icon loading, multi-desktop

Fix #7: Add "Other" catch-all category to the launcher. map_category() now
returns "Other" instead of "" for unrecognized .desktop Categories, and an
"Other" tab is added to the CATS list so those apps are discoverable instead
of silently hidden.

Fix #8: Respect ICCCM WM_NORMAL_HINTS. client_add() reads PMinSize,
PResizeInc, and PBaseSize from the window's size hints. The resize drag
handler in events.c clamps to min_w/min_h and snaps to the
width_inc/height_inc grid, fixing jank in character-cell terminal emulators
(xterm, Alacritty).

Fix #10: Optional freedesktop icon loading via imlib2. The launcher now
parses the Icon= field from .desktop files and attempts to render a PNG icon
from Papirus-Dark (or hicolor fallback). When imlib2 is not present the
existing letter-square is shown unchanged. launcher/Makefile auto-detects
imlib2 via pkg-config and sets -DHAVE_IMLIB2.

Fix #11: True 4-desktop support in the WM. Each Client carries a desktop
field (assigned at map time from wm->cur_desktop). Clicking a workspace
button in the deskbar sends _NET_CURRENT_DESKTOP; the WM now handles it via
wm_switch_desktop() which unmaps frames on the leaving desktop, maps frames
on the arriving desktop, and re-focuses. _NET_WM_DESKTOP is set per window
so the deskbar can filter db_update_apps() to show only current-desktop
windows (sticky windows with desktop 0xFFFFFFFF are always shown).
This commit is contained in:
Claude committed 2026-08-30 20:00:10 +00:00
1 parent 36641e77e2
commit b0f64d9b2e
7 files changed
+173 -16

No files matched your search

+9
View File
@@ -73,10 +73,15 @@ typedef struct Client {
int focused;
int minimized;
int maximized;
int desktop; /* virtual desktop this window lives on */
int saved_x, saved_y; /* pre-maximize position */
unsigned saved_w, saved_h; /* pre-maximize size */
Time last_click_time; /* for double-click detection */
char title[256];
/* ICCCM WM_NORMAL_HINTS */
int min_w, min_h; /* PMinSize */
int inc_w, inc_h; /* PResizeInc — 0 means no constraint */
int base_w, base_h; /* PBaseSize — baseline for increment calc */
struct Client *next;
} Client;
@@ -104,6 +109,8 @@ typedef struct WM {
Client *clients;
Client *focused;
int cur_desktop; /* active virtual desktop (0-based) */
Atom wm_delete_window;
Atom wm_protocols;
Atom wm_state;
@@ -112,6 +119,8 @@ typedef struct WM {
Atom net_wm_state_hidden;
Atom net_wm_state_maximized_vert;
Atom net_wm_state_maximized_horz;
Atom net_wm_desktop;
Atom net_current_desktop;
DragMode drag_mode;
int drag_start_x, drag_start_y;
+36 -3
View File
@@ -54,9 +54,12 @@ Client *client_add(WM *wm, Window win) {
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->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) {
@@ -93,6 +96,36 @@ Client *client_add(WM *wm, Window win) {
}
}
/* 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;
+56 -2
View File
@@ -94,11 +94,55 @@ void on_enter_notify(WM *wm, XCrossingEvent *e) {
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);
/* _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;
}
if (e->message_type == net_active) {
Client *c = client_find_by_win(wm, e->window);
if (c) client_restore(wm, c);
@@ -271,8 +315,18 @@ void on_motion_notify(WM *wm, XMotionEvent *e) {
} else {
int nw = (int)wm->drag_win_w + dx;
int nh = (int)wm->drag_win_h + dy;
if (nw < MIN_WIN_W) nw = MIN_WIN_W;
if (nh < MIN_WIN_H) nh = MIN_WIN_H;
/* 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);
+5 -1
View File
@@ -48,6 +48,8 @@ void wm_init(WM *wm) {
wm->net_wm_state_hidden = XInternAtom(wm->dpy, "_NET_WM_STATE_HIDDEN", False);
wm->net_wm_state_maximized_vert = XInternAtom(wm->dpy, "_NET_WM_STATE_MAXIMIZED_VERT", False);
wm->net_wm_state_maximized_horz = XInternAtom(wm->dpy, "_NET_WM_STATE_MAXIMIZED_HORZ", False);
wm->net_wm_desktop = XInternAtom(wm->dpy, "_NET_WM_DESKTOP", False);
wm->net_current_desktop = XInternAtom(wm->dpy, "_NET_CURRENT_DESKTOP", False);
/* advertise basic EWMH support */
Atom net_supported = XInternAtom(wm->dpy, "_NET_SUPPORTED", False);
@@ -55,10 +57,12 @@ void wm_init(WM *wm) {
wm->net_wm_state, wm->net_wm_state_hidden,
wm->net_wm_state_maximized_vert,
wm->net_wm_state_maximized_horz,
wm->net_wm_desktop,
wm->net_current_desktop,
XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False),
XInternAtom(wm->dpy, "_NET_CLIENT_LIST", False),
XInternAtom(wm->dpy, "_NET_CLOSE_WINDOW", False),
XInternAtom(wm->dpy, "_NET_NUMBER_OF_DESKTOPS", False),
XInternAtom(wm->dpy, "_NET_CURRENT_DESKTOP", False),
XInternAtom(wm->dpy, "_NET_WM_NAME", False),
};
XChangeProperty(wm->dpy, wm->root, net_supported, XA_ATOM, 32,