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

+8
View File
@@ -111,8 +111,14 @@ void db_update_apps(Deskbar *db) {
Atom net_wm_state = XInternAtom(db->dpy, "_NET_WM_STATE", False);
Atom net_wm_state_hidden = XInternAtom(db->dpy, "_NET_WM_STATE_HIDDEN", False);
Atom net_wm_desktop = XInternAtom(db->dpy, "_NET_WM_DESKTOP", False);
for (unsigned long i = 0; i < n && db->napps < DB_MAX_APPS; i++) {
/* only show windows on the current desktop (or sticky: desktop == 0xFFFFFFFF) */
long win_desk = ewmh_get_long(db, wins[i], net_wm_desktop);
if (win_desk >= 0 && win_desk != (long)db->cur_ws && win_desk != (long)0xFFFFFFFF)
continue;
db->apps[db->napps].win = wins[i];
db->apps[db->napps].focused = (wins[i] == (Window)active_win);
db->apps[db->napps].minimized = 0;
@@ -1285,6 +1291,8 @@ void db_run(Deskbar *db) {
db_draw(db);
} else if (e.xproperty.atom == db->net_current_desktop) {
db_update_workspace(db);
db_update_apps(db); /* re-filter by new desktop */
db_resize(db);
db_draw(db);
}
break;
+8
View File
@@ -7,6 +7,14 @@ ifeq ($(PKG_XFT_C),)
PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig
endif
# Optional: imlib2 for freedesktop icon loading
PKG_IL_C := $(shell pkg-config --cflags imlib2 2>/dev/null)
PKG_IL_L := $(shell pkg-config --libs imlib2 2>/dev/null)
ifneq ($(PKG_IL_C),)
PKG_XFT_C += $(PKG_IL_C) -DHAVE_IMLIB2
PKG_XFT_L += $(PKG_IL_L)
endif
CFLAGS = -Wall -Wextra -O2 $(PKG_XFT_C)
LDFLAGS = $(PKG_XFT_L)
+51 -10
View File
@@ -58,13 +58,13 @@ static const unsigned long ICON_BG[] = {
#define FONT_ICON "Sans:size=13:weight=bold"
#define MAX_FLT 64
typedef struct { char name[64]; char cmd[256]; char cat[32]; } App;
typedef struct { char name[64]; char cmd[256]; char cat[32]; char icon[64]; } App;
static App APPS[MAX_APPS];
static int NAPP = 0;
static const char *CATS[] = {
"All", "Internet", "Media", "Office", "System", "Settings"
"All", "Internet", "Media", "Office", "System", "Settings", "Other"
};
#define NCAT ((int)(sizeof(CATS)/sizeof(CATS[0])))
@@ -89,7 +89,7 @@ static const char *map_category(const char *cats) {
strstr(cats, "FileManager") || strstr(cats, "Utility") ||
strstr(cats, "Monitor") || strstr(cats, "Security"))
return "System";
return "";
return "Other";
}
static void clean_exec(char *dst, const char *src, size_t dstsz) {
@@ -108,7 +108,7 @@ static void load_desktop_file(const char *path) {
FILE *f = fopen(path, "r");
if (!f) return;
char name[64] = {0}, exec[256] = {0}, cats[128] = {0};
char name[64] = {0}, exec[256] = {0}, cats[128] = {0}, icon[64] = {0};
int in_entry = 0, nodisplay = 0, is_app = 0;
char line[512];
@@ -126,6 +126,8 @@ static void load_desktop_file(const char *path) {
snprintf(exec, sizeof(exec), "%s", line + 5);
else if (!cats[0] && strncmp(line, "Categories=", 11) == 0)
snprintf(cats, sizeof(cats), "%s", line + 11);
else if (!icon[0] && strncmp(line, "Icon=", 5) == 0)
snprintf(icon, sizeof(icon), "%s", line + 5);
else if (strncmp(line, "NoDisplay=true", 14) == 0)
nodisplay = 1;
else if (strncmp(line, "Type=Application", 16) == 0)
@@ -137,6 +139,7 @@ static void load_desktop_file(const char *path) {
snprintf(APPS[NAPP].name, 64, "%s", name);
clean_exec(APPS[NAPP].cmd, exec, 256);
snprintf(APPS[NAPP].cat, 32, "%s", map_category(cats));
snprintf(APPS[NAPP].icon, 64, "%s", icon);
NAPP++;
}
@@ -292,6 +295,39 @@ static int card_at_vis(int x, int y) {
return row * COLS + col;
}
/* ── icon loading (imlib2, optional) ─────────────────────────────────── */
#ifdef HAVE_IMLIB2
#include <Imlib2.h>
static const char *ICON_DIRS[] = {
"/usr/share/icons/Papirus-Dark/48x48/apps",
"/usr/share/icons/Papirus/48x48/apps",
"/usr/share/icons/hicolor/48x48/apps",
"/usr/share/pixmaps",
NULL
};
static int render_icon(L *l, const char *icon_name, int ix, int iy, int sz) {
if (!icon_name || !icon_name[0]) return 0;
char path[512];
for (int i = 0; ICON_DIRS[i]; i++) {
snprintf(path, sizeof(path), "%s/%s.png", ICON_DIRS[i], icon_name);
if (access(path, R_OK) == 0) goto found;
}
return 0;
found:;
imlib_context_set_display(l->dpy);
imlib_context_set_visual(l->visual);
imlib_context_set_colormap(l->cmap);
imlib_context_set_drawable(l->win);
imlib_context_set_blend(1);
Imlib_Image img = imlib_load_image_immediately(path);
if (!img) return 0;
imlib_context_set_image(img);
imlib_render_image_on_drawable_at_size(ix, iy, sz, sz);
imlib_free_image();
return 1;
}
#endif
/* ── draw ────────────────────────────────────────────────────────────── */
static void draw(L *l) {
XftDraw *xd = XftDrawCreate(l->dpy, l->win, l->visual, l->cmap);
@@ -382,12 +418,17 @@ static void draw(L *l) {
for (const char *p = APPS[app_i].name; *p; p++) h = h * 31 + (unsigned char)*p;
int ix = cx + (CARD_W - ICON_SZ) / 2;
int iy = cy + 10;
XSetForeground(l->dpy, l->gc, mkc(l, ICON_BG[h % NICON]));
XFillRectangle(l->dpy, l->win, l->gc, ix, iy, ICON_SZ, ICON_SZ);
char letter[2] = { APPS[app_i].name[0], '\0' };
if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32;
ds_ctr(l, xd, l->font_icon, 0xEAF0FF, ix, iy, ICON_SZ, ICON_SZ, letter);
int icon_rendered = 0;
#ifdef HAVE_IMLIB2
icon_rendered = render_icon(l, APPS[app_i].icon, ix, iy, ICON_SZ);
#endif
if (!icon_rendered) {
XSetForeground(l->dpy, l->gc, mkc(l, ICON_BG[h % NICON]));
XFillRectangle(l->dpy, l->win, l->gc, ix, iy, ICON_SZ, ICON_SZ);
char letter[2] = { APPS[app_i].name[0], '\0' };
if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32;
ds_ctr(l, xd, l->font_icon, 0xEAF0FF, ix, iy, ICON_SZ, ICON_SZ, letter);
}
char lbl[14];
snprintf(lbl, sizeof(lbl), "%.12s", APPS[app_i].name);
+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,