diff --git a/deskbar/src/deskbar.c b/deskbar/src/deskbar.c index b785a0e..04e1411 100644 --- a/deskbar/src/deskbar.c +++ b/deskbar/src/deskbar.c @@ -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; diff --git a/launcher/Makefile b/launcher/Makefile index 55272c4..883660f 100644 --- a/launcher/Makefile +++ b/launcher/Makefile @@ -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) diff --git a/launcher/src/launcher.c b/launcher/src/launcher.c index 22eb209..425712e 100644 --- a/launcher/src/launcher.c +++ b/launcher/src/launcher.c @@ -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 +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); diff --git a/wm/include/wm.h b/wm/include/wm.h index 3c0c287..e9727be 100644 --- a/wm/include/wm.h +++ b/wm/include/wm.h @@ -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; diff --git a/wm/src/client.c b/wm/src/client.c index 5fc5aa4..d9d9644 100644 --- a/wm/src/client.c +++ b/wm/src/client.c @@ -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; diff --git a/wm/src/events.c b/wm/src/events.c index 7f430fa..d244864 100644 --- a/wm/src/events.c +++ b/wm/src/events.c @@ -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); diff --git a/wm/src/wm.c b/wm/src/wm.c index f70b384..388237c 100644 --- a/wm/src/wm.c +++ b/wm/src/wm.c @@ -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,