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:
7 files changed
+165
-8
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+46
-5
@@ -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;
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -57,6 +57,9 @@ Client *client_add(WM *wm, Window win) {
|
||||
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
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user