From 2b8ea90a57064a35fd7059c49118eca660713d92 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:16:18 -0400 Subject: [PATCH] Phase 1/2: .desktop launcher, volume control in deskbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Launcher now loads apps dynamically from /usr/share/applications and ~/.local/share/applications instead of a hardcoded list; maps desktop Categories to tab system, falls back to built-in list on systems without .desktop files (dev/macOS builds) - Launcher gains scroll support: mouse wheel and PgUp/PgDn scroll the app grid, scrollbar drawn on right edge when needed - Deskbar status bar shows VOL % from pactl (cached, polls every 5s) - Deskbar status bar: scroll wheel changes volume ±5%, left-click toggles mute Co-Authored-By: Claude Sonnet 4.6 --- deskbar/src/deskbar.c | 47 +++++- launcher/src/launcher.c | 316 ++++++++++++++++++++++++++++------------ 2 files changed, 267 insertions(+), 96 deletions(-) diff --git a/deskbar/src/deskbar.c b/deskbar/src/deskbar.c index 350f464..bb0f721 100644 --- a/deskbar/src/deskbar.c +++ b/deskbar/src/deskbar.c @@ -161,6 +161,23 @@ static int read_cpu_percent(void) { return (int)(100 * (d_total - d_idle) / d_total); } +static int read_volume_percent(void) { + static int cached = -1; + static int ticks = 0; + if (ticks-- > 0) return cached; + ticks = 4; /* re-poll every 5 seconds */ + FILE *f = popen("pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null", "r"); + if (!f) return cached; + char line[256]; + cached = -1; + if (fgets(line, sizeof(line), f)) { + char *p = strstr(line, "/ "); + if (p) cached = atoi(p + 2); + } + pclose(f); + return cached; +} + static int read_battery_percent(void) { FILE *f; char path[80]; @@ -422,16 +439,20 @@ void db_draw(Deskbar *db) { int mem = read_mem_percent(); int bat = read_battery_percent(); char bst = read_battery_status(); - char status[64]; + int vol = read_volume_percent(); + char status[80]; int pos = 0; + if (vol >= 0) + pos += snprintf(status + pos, sizeof(status) - pos, + "VOL %d%%", vol); if (cpu >= 0) pos += snprintf(status + pos, sizeof(status) - pos, - "CPU %d%%", cpu); + "%sCPU %d%%", pos ? " " : "", cpu); if (mem >= 0) pos += snprintf(status + pos, sizeof(status) - pos, " MEM %d%%", mem); if (bat >= 0) { - const char *arrow = (bst == 'C') ? "↑" : (bst == 'F') ? "=" : "↓"; + const char *arrow = (bst == 'C') ? "+" : (bst == 'F') ? "=" : "-"; pos += snprintf(status + pos, sizeof(status) - pos, " BAT %d%%%s", bat, arrow); } @@ -888,8 +909,24 @@ void db_run(Deskbar *db) { if (e.xexpose.count == 0) db_draw(db); break; case ButtonPress: - if (e.xany.window == db->win) - db_click(db, e.xbutton.x, e.xbutton.y); + if (e.xany.window == db->win) { + int sy = db_status_y(db); + if (e.xbutton.button == Button4) { + /* scroll up on status bar = volume up */ + if (e.xbutton.y >= sy) + system("pactl set-sink-volume @DEFAULT_SINK@ +5% 2>/dev/null &"); + break; + } + if (e.xbutton.button == Button5) { + if (e.xbutton.y >= sy) + system("pactl set-sink-volume @DEFAULT_SINK@ -5% 2>/dev/null &"); + break; + } + if (e.xbutton.button == Button1 && e.xbutton.y >= sy) + system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null &"); + else + db_click(db, e.xbutton.x, e.xbutton.y); + } break; case MotionNotify: if (e.xany.window == db->win) diff --git a/launcher/src/launcher.c b/launcher/src/launcher.c index ecf60b5..5cbd4cd 100644 --- a/launcher/src/launcher.c +++ b/launcher/src/launcher.c @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -11,19 +13,20 @@ /* ── dimensions ─────────────────────────────────────────────────────── */ #define WIN_W 580 #define WIN_H 420 -#define HDR_H 50 /* search header */ -#define TAB_H 34 /* category tab strip */ +#define HDR_H 50 +#define TAB_H 34 #define CONTENT_Y (HDR_H + TAB_H) -#define PAD 12 /* grid outer margin */ +#define PAD 12 #define CARD_W 96 #define CARD_H 84 #define CARD_GAP 8 #define COLS 5 #define ICON_SZ 38 -/* grid left edge */ #define GX0 ((WIN_W - (COLS * CARD_W + (COLS-1) * CARD_GAP)) / 2) #define GY0 (CONTENT_Y + PAD) +#define ROWS_VIS 3 /* rows visible before scrolling */ +#define MAX_APPS 256 /* ── palette ─────────────────────────────────────────────────────────── */ #define C_BG 0x0E1220 @@ -40,6 +43,8 @@ #define C_TEXT_DIM 0x344A62 #define C_TAB_TXT 0x486070 #define C_TAB_TSEL 0xA8C4E8 +#define C_SCROLL_BG 0x0C1020 +#define C_SCROLL_TH 0x1E3258 static const unsigned long ICON_BG[] = { 0x1A4D88, 0x166C3C, 0x6C3018, @@ -53,38 +58,147 @@ static const unsigned long ICON_BG[] = { #define FONT_ICON "Sans:size=13:weight=bold" #define MAX_FLT 64 -typedef struct { const char *name, *cmd, *cat; } App; -static const App APPS[] = { - { "Firefox", "firefox &", "Internet" }, - { "Email", "firefox https://mail.google.com &", "Internet" }, - { "Media Player", "mpv --player-operation-mode=pseudo-gui &", "Media" }, - { "Images", "viewnior &", "Media" }, - { "OnlyOffice", "onlyoffice-desktopeditors &", "Office" }, - { "PDF Viewer", "evince &", "Office" }, - { "Terminal", "xterm &", "System" }, - { "Files", "thunar &", "System" }, - { "Text Editor", "mousepad &", "System" }, - { "System Info", "xterm -e 'fastfetch; read' &", "System" }, - { "Screen Lock", "xscreensaver-command -lock &", "System" }, - { "Install", "xterm -e 'sudo skyinstall' &", "System" }, - { "Display", "arandr &", "Settings" }, - { "Audio", "pavucontrol &", "Settings" }, - { "Network", "nm-connection-editor &", "Settings" }, - { "Bluetooth", "blueman-manager &", "Settings" }, -}; -#define NAPP ((int)(sizeof(APPS)/sizeof(APPS[0]))) +typedef struct { char name[64]; char cmd[256]; char cat[32]; } App; + +static App APPS[MAX_APPS]; +static int NAPP = 0; static const char *CATS[] = { "All", "Internet", "Media", "Office", "System", "Settings" }; #define NCAT ((int)(sizeof(CATS)/sizeof(CATS[0]))) +/* ── .desktop loader ─────────────────────────────────────────────────── */ + +static const char *map_category(const char *cats) { + if (strstr(cats, "WebBrowser") || strstr(cats, "Network") || + strstr(cats, "Email") || strstr(cats, "Chat")) + return "Internet"; + if (strstr(cats, "AudioVideo") || strstr(cats, "Audio") || + strstr(cats, "Video") || strstr(cats, "Player") || + strstr(cats, "Music") || strstr(cats, "Graphics")) + return "Media"; + if (strstr(cats, "Office") || strstr(cats, "WordProcessor") || + strstr(cats, "Spreadsheet") || strstr(cats, "Presentation") || + strstr(cats, "Viewer")) + return "Office"; + if (strstr(cats, "Settings") || strstr(cats, "HardwareSettings") || + strstr(cats, "PackageManager") || strstr(cats, "DesktopSettings")) + return "Settings"; + if (strstr(cats, "System") || strstr(cats, "TerminalEmulator") || + strstr(cats, "FileManager") || strstr(cats, "Utility") || + strstr(cats, "Monitor") || strstr(cats, "Security")) + return "System"; + return ""; +} + +static void clean_exec(char *dst, const char *src, size_t dstsz) { + int len = 0; + for (const char *p = src; *p && len < (int)dstsz - 3; p++) { + if (*p == '%' && *(p + 1)) { p++; continue; } + dst[len++] = *p; + } + while (len > 0 && dst[len - 1] == ' ') len--; + if (len > 0) { dst[len++] = ' '; dst[len++] = '&'; } + dst[len] = '\0'; +} + +static void load_desktop_file(const char *path) { + if (NAPP >= MAX_APPS) return; + FILE *f = fopen(path, "r"); + if (!f) return; + + char name[64] = {0}, exec[256] = {0}, cats[128] = {0}; + int in_entry = 0, nodisplay = 0, is_app = 0; + char line[512]; + + while (fgets(line, sizeof(line), f)) { + size_t l = strlen(line); + while (l > 0 && (line[l-1] == '\n' || line[l-1] == '\r')) line[--l] = '\0'; + + if (strcmp(line, "[Desktop Entry]") == 0) { in_entry = 1; continue; } + if (line[0] == '[') { if (in_entry) break; continue; } + if (!in_entry) continue; + + if (!name[0] && strncmp(line, "Name=", 5) == 0) + snprintf(name, sizeof(name), "%s", line + 5); + else if (!exec[0] && strncmp(line, "Exec=", 5) == 0) + 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 (strncmp(line, "NoDisplay=true", 14) == 0) + nodisplay = 1; + else if (strncmp(line, "Type=Application", 16) == 0) + is_app = 1; + } + fclose(f); + if (!is_app || !name[0] || !exec[0] || nodisplay) return; + + snprintf(APPS[NAPP].name, 64, "%s", name); + clean_exec(APPS[NAPP].cmd, exec, 256); + snprintf(APPS[NAPP].cat, 32, "%s", map_category(cats)); + NAPP++; +} + +static void load_desktop_dir(const char *dir) { + DIR *d = opendir(dir); + if (!d) return; + struct dirent *ent; + char path[512]; + while ((ent = readdir(d)) && NAPP < MAX_APPS) { + const char *nm = ent->d_name; + size_t l = strlen(nm); + if (l < 8 || strcmp(nm + l - 8, ".desktop") != 0) continue; + snprintf(path, sizeof(path), "%s/%s", dir, nm); + load_desktop_file(path); + } + closedir(d); +} + +static int app_cmp(const void *a, const void *b) { + return strcasecmp(((const App *)a)->name, ((const App *)b)->name); +} + +static void load_apps(void) { + load_desktop_dir("/usr/share/applications"); + const char *home = getenv("HOME"); + if (home) { + char p[512]; + snprintf(p, sizeof(p), "%s/.local/share/applications", home); + load_desktop_dir(p); + } + + /* fallback if no .desktop files (dev environment, macOS, etc.) */ + if (NAPP == 0) { + static const struct { const char *n, *c, *cat; } FB[] = { + { "Firefox", "firefox &", "Internet" }, + { "Terminal", "alacritty &", "System" }, + { "Files", "thunar &", "System" }, + { "Text Editor", "mousepad &", "System" }, + { "Media", "mpv --player-operation-mode=pseudo-gui &", "Media" }, + { "Images", "viewnior &", "Media" }, + { "PDF Viewer", "evince &", "Office" }, + { "Display", "arandr &", "Settings" }, + { "Audio", "pavucontrol &", "Settings" }, + }; + for (int i = 0; i < (int)(sizeof(FB)/sizeof(FB[0])); i++) { + snprintf(APPS[i].name, 64, "%s", FB[i].n); + snprintf(APPS[i].cmd, 256, "%s", FB[i].c); + snprintf(APPS[i].cat, 32, "%s", FB[i].cat); + } + NAPP = sizeof(FB)/sizeof(FB[0]); + } + + qsort(APPS, NAPP, sizeof(App), app_cmp); +} + +/* ── app state ───────────────────────────────────────────────────────── */ typedef struct { Display *dpy; int screen; Window root, win; GC gc; Colormap cmap; Visual *visual; XftFont *font_ui, *font_logo, *font_icon; - int sel_cat, hov; + int sel_cat, hov, scroll; char filter[MAX_FLT]; int filter_len; } L; @@ -124,17 +238,19 @@ static void ds_ctr(L *l, XftDraw *xd, XftFont *f, unsigned long rgb, } /* ── query ───────────────────────────────────────────────────────────── */ -static int nvis(L *l, int out[NAPP]) { +static int nvis(L *l, int out[MAX_APPS]) { int n = 0; const char *cat = CATS[l->sel_cat]; for (int i = 0; i < NAPP; i++) { - if (strcmp(cat,"All")!=0 && strcmp(APPS[i].cat,cat)!=0) continue; + if (strcmp(cat, "All") != 0) { + if (APPS[i].cat[0] == '\0' || strcmp(APPS[i].cat, cat) != 0) continue; + } if (l->filter_len > 0) { char nm[64], fl[MAX_FLT]; snprintf(nm, sizeof(nm), "%s", APPS[i].name); snprintf(fl, sizeof(fl), "%s", l->filter); - for (char *p=nm; *p; p++) if (*p>='A'&&*p<='Z') *p+=32; - for (char *p=fl; *p; p++) if (*p>='A'&&*p<='Z') *p+=32; + for (char *p = nm; *p; p++) if (*p >= 'A' && *p <= 'Z') *p += 32; + for (char *p = fl; *p; p++) if (*p >= 'A' && *p <= 'Z') *p += 32; if (!strstr(nm, fl)) continue; } out[n++] = i; @@ -142,7 +258,6 @@ static int nvis(L *l, int out[NAPP]) { return n; } -/* tab widths — measured once per draw via font metrics */ static void tab_layout(L *l, int widths[NCAT], int *total) { *total = 0; for (int i = 0; i < NCAT; i++) { @@ -153,7 +268,6 @@ static void tab_layout(L *l, int widths[NCAT], int *total) { } } -/* hit: which category tab contains point (x,y)? -1 = none */ static int tab_at(L *l, int x, int y) { if (y < HDR_H || y >= CONTENT_Y) return -1; int w[NCAT], total; tab_layout(l, w, &total); @@ -165,39 +279,32 @@ static int tab_at(L *l, int x, int y) { return -1; } -/* hit: which card index (into vis[]) is at point? -1 = none */ -static int card_at(int x, int y) { +/* card index within visible slice (adjusted for scroll) */ +static int card_at_vis(int x, int y) { if (y < CONTENT_Y) return -1; - int lx = x - GX0; - int ly = y - GY0; + int lx = x - GX0, ly = y - GY0; if (lx < 0 || ly < 0) return -1; int col = lx / (CARD_W + CARD_GAP); int row = ly / (CARD_H + CARD_GAP); - if (col < 0 || col >= COLS || row < 0) return -1; - /* reject click in gap between cards */ - int cx = col * (CARD_W + CARD_GAP); - int cy = row * (CARD_H + CARD_GAP); - if (lx - cx >= CARD_W || ly - cy >= CARD_H) return -1; + if (col < 0 || col >= COLS || row < 0 || row >= ROWS_VIS) return -1; + if (lx - col*(CARD_W+CARD_GAP) >= CARD_W) return -1; + if (ly - row*(CARD_H+CARD_GAP) >= CARD_H) return -1; return row * COLS + col; } -/* ── draw ─────────────────────────────────────────────────────────────── */ +/* ── draw ────────────────────────────────────────────────────────────── */ static void draw(L *l) { XftDraw *xd = XftDrawCreate(l->dpy, l->win, l->visual, l->cmap); - /* background */ XSetForeground(l->dpy, l->gc, mkc(l, C_BG)); XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, WIN_H); /* header */ XSetForeground(l->dpy, l->gc, mkc(l, C_HDR)); XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, HDR_H); - - /* accent top stripe */ XSetForeground(l->dpy, l->gc, mkc(l, C_ACCENT)); XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, 2); - /* logo */ ds(l, xd, l->font_logo, C_ACCENT, 14, 13, "SkyOS"); /* search box */ @@ -207,17 +314,15 @@ static void draw(L *l) { XFillRectangle(l->dpy, l->win, l->gc, sx, sy, sw, sh); XSetForeground(l->dpy, l->gc, mkc(l, C_SEARCH_BD)); XDrawRectangle(l->dpy, l->win, l->gc, sx, sy, sw, sh); - if (l->filter_len == 0) { - ds(l, xd, l->font_ui, C_TEXT_DIM, sx+8, sy+7, "Search..."); + ds(l, xd, l->font_ui, C_TEXT_DIM, sx + 8, sy + 7, "Search apps..."); } else { - char buf[MAX_FLT+2]; + char buf[MAX_FLT + 2]; snprintf(buf, sizeof(buf), "%s|", l->filter); - ds(l, xd, l->font_ui, C_TEXT_HOV, sx+8, sy+7, buf); + ds(l, xd, l->font_ui, C_TEXT_HOV, sx + 8, sy + 7, buf); } } - /* separator below header */ XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER)); XDrawLine(l->dpy, l->win, l->gc, 0, HDR_H, WIN_W, HDR_H); @@ -227,81 +332,99 @@ static void draw(L *l) { for (int i = 0; i < NCAT; i++) { if (i == l->sel_cat) { XSetForeground(l->dpy, l->gc, mkc(l, C_TAB_SEL)); - XFillRectangle(l->dpy, l->win, l->gc, tx, HDR_H+1, tw[i], TAB_H-1); - /* accent underline */ + XFillRectangle(l->dpy, l->win, l->gc, tx, HDR_H + 1, tw[i], TAB_H - 1); XSetForeground(l->dpy, l->gc, mkc(l, C_ACCENT)); - XFillRectangle(l->dpy, l->win, l->gc, tx+5, HDR_H+TAB_H-2, tw[i]-10, 2); + XFillRectangle(l->dpy, l->win, l->gc, tx + 5, HDR_H + TAB_H - 2, tw[i] - 10, 2); } - unsigned long tc = (i == l->sel_cat) ? C_TAB_TSEL : C_TAB_TXT; - ds_ctr(l, xd, l->font_ui, tc, tx, HDR_H, tw[i], TAB_H, CATS[i]); + ds_ctr(l, xd, l->font_ui, + (i == l->sel_cat) ? C_TAB_TSEL : C_TAB_TXT, + tx, HDR_H, tw[i], TAB_H, CATS[i]); tx += tw[i]; } - /* separator below tabs */ XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER)); XDrawLine(l->dpy, l->win, l->gc, 0, CONTENT_Y, WIN_W, CONTENT_Y); - /* app grid */ - int vis[NAPP]; + /* app grid with scroll */ + int vis[MAX_APPS]; int nv = nvis(l, vis); + /* clamp scroll */ + int max_scroll = (nv - ROWS_VIS * COLS + COLS - 1) / COLS; + if (max_scroll < 0) max_scroll = 0; + if (l->scroll > max_scroll) l->scroll = max_scroll; + if (l->scroll < 0) l->scroll = 0; + + int vis_start = l->scroll * COLS; + if (nv == 0) { ds_ctr(l, xd, l->font_ui, C_TEXT_DIM, 0, CONTENT_Y, WIN_W, WIN_H - CONTENT_Y, "No apps found"); } else { - for (int i = 0; i < nv; i++) { + for (int i = 0; i < ROWS_VIS * COLS; i++) { + int vi = vis_start + i; + if (vi >= nv) break; int col = i % COLS; int row = i / COLS; int cx = GX0 + col * (CARD_W + CARD_GAP); int cy = GY0 + row * (CARD_H + CARD_GAP); - if (cy + CARD_H > WIN_H) break; /* clip to window */ int hover = (i == l->hov); - /* card hover highlight */ if (hover) { XSetForeground(l->dpy, l->gc, mkc(l, C_CARD_HOV)); XFillRectangle(l->dpy, l->win, l->gc, cx, cy, CARD_W, CARD_H); XSetForeground(l->dpy, l->gc, mkc(l, C_CARD_BD)); - XDrawRectangle(l->dpy, l->win, l->gc, cx, cy, CARD_W-1, CARD_H-1); + XDrawRectangle(l->dpy, l->win, l->gc, cx, cy, CARD_W - 1, CARD_H - 1); } - /* icon square */ - int app_i = vis[i]; + int app_i = vis[vi]; unsigned int h = 0; - for (const char *p = APPS[app_i].name; *p; p++) h = h*31 + (unsigned char)*p; - unsigned long ibg = ICON_BG[h % NICON]; + 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, ibg)); + XSetForeground(l->dpy, l->gc, mkc(l, ICON_BG[h % NICON])); XFillRectangle(l->dpy, l->win, l->gc, ix, iy, ICON_SZ, ICON_SZ); - /* icon letter */ 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); - /* label */ char lbl[14]; snprintf(lbl, sizeof(lbl), "%.12s", APPS[app_i].name); - unsigned long tc = hover ? C_TEXT_HOV : C_TEXT; - ds_ctr(l, xd, l->font_ui, tc, + ds_ctr(l, xd, l->font_ui, + hover ? C_TEXT_HOV : C_TEXT, cx, iy + ICON_SZ + 4, CARD_W, CARD_H - ICON_SZ - 18, lbl); } } + /* scrollbar */ + if (max_scroll > 0) { + int sb_x = WIN_W - 8, sb_y = CONTENT_Y + 4; + int sb_h = WIN_H - CONTENT_Y - 8; + XSetForeground(l->dpy, l->gc, mkc(l, C_SCROLL_BG)); + XFillRectangle(l->dpy, l->win, l->gc, sb_x, sb_y, 6, sb_h); + int total_rows = (nv + COLS - 1) / COLS; + int thumb_h = sb_h * ROWS_VIS / total_rows; + if (thumb_h < 12) thumb_h = 12; + int thumb_y = sb_y + (sb_h - thumb_h) * l->scroll / max_scroll; + XSetForeground(l->dpy, l->gc, mkc(l, C_SCROLL_TH)); + XFillRectangle(l->dpy, l->win, l->gc, sb_x + 1, thumb_y, 4, thumb_h); + } + /* outer border */ XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER)); - XDrawRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W-1, WIN_H-1); + XDrawRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W - 1, WIN_H - 1); XftDrawDestroy(xd); } /* ── event handlers ──────────────────────────────────────────────────── */ -static void launch(L *l, int hov) { - int vis[NAPP]; +static void launch(L *l, int vis_idx) { + int vis[MAX_APPS]; int nv = nvis(l, vis); - if (hov >= 0 && hov < nv) { - system(APPS[vis[hov]].cmd); + int abs_idx = l->scroll * COLS + vis_idx; + if (abs_idx >= 0 && abs_idx < nv) { + system(APPS[vis[abs_idx]].cmd); XDestroyWindow(l->dpy, l->win); XCloseDisplay(l->dpy); exit(0); @@ -311,28 +434,32 @@ static void launch(L *l, int hov) { static void on_key(L *l, XKeyEvent *e) { KeySym sym = XLookupKeysym(e, 0); if (sym == XK_Escape) { - XDestroyWindow(l->dpy, l->win); - XCloseDisplay(l->dpy); - exit(0); + XDestroyWindow(l->dpy, l->win); XCloseDisplay(l->dpy); exit(0); } - if (sym == XK_Return) { launch(l, l->hov); return; } + if (sym == XK_Return) { if (l->hov >= 0) launch(l, l->hov); return; } if (sym == XK_BackSpace && l->filter_len > 0) { l->filter[--l->filter_len] = '\0'; - l->hov = -1; draw(l); return; + l->scroll = 0; l->hov = -1; draw(l); return; } + /* page up/down for keyboard scroll */ + if (sym == XK_Prior) { l->scroll -= ROWS_VIS; if (l->scroll < 0) l->scroll = 0; draw(l); return; } + if (sym == XK_Next) { l->scroll += ROWS_VIS; draw(l); return; } + char buf[8] = {0}; - int len = XLookupString(e, buf, sizeof(buf)-1, &sym, NULL); - if (len > 0 && buf[0] >= 0x20 && buf[0] < 0x7F && l->filter_len < MAX_FLT-1) { + int len = XLookupString(e, buf, sizeof(buf) - 1, &sym, NULL); + if (len > 0 && buf[0] >= 0x20 && buf[0] < 0x7F && l->filter_len < MAX_FLT - 1) { l->filter[l->filter_len++] = buf[0]; l->filter[l->filter_len] = '\0'; - l->sel_cat = 0; l->hov = -1; + l->sel_cat = 0; l->scroll = 0; l->hov = -1; draw(l); } } /* ── main ─────────────────────────────────────────────────────────────── */ int main(void) { - L l = {0}; l.sel_cat = 0; l.hov = -1; + load_apps(); + + L l = {0}; l.sel_cat = 0; l.hov = -1; l.scroll = 0; l.dpy = XOpenDisplay(NULL); if (!l.dpy) { fprintf(stderr, "skylauncher: cannot open display\n"); return 1; } @@ -377,21 +504,28 @@ int main(void) { break; case ButtonPress: { + /* scroll wheel */ + if (e.xbutton.button == Button4) { + l.scroll -= 1; if (l.scroll < 0) l.scroll = 0; draw(&l); break; + } + if (e.xbutton.button == Button5) { + l.scroll += 1; draw(&l); break; + } int t = tab_at(&l, e.xbutton.x, e.xbutton.y); if (t >= 0) { - l.sel_cat = t; l.hov = -1; draw(&l); + l.sel_cat = t; l.scroll = 0; l.hov = -1; draw(&l); } else { - int idx = card_at(e.xbutton.x, e.xbutton.y); - launch(&l, idx); + int idx = card_at_vis(e.xbutton.x, e.xbutton.y); + if (idx >= 0) launch(&l, idx); } } break; case MotionNotify: { - int idx = card_at(e.xmotion.x, e.xmotion.y); - int vis[NAPP]; int nv = nvis(&l, vis); + int vis[MAX_APPS]; int nv = nvis(&l, vis); + int idx = card_at_vis(e.xmotion.x, e.xmotion.y); int prev = l.hov; - l.hov = (idx >= 0 && idx < nv) ? idx : -1; + l.hov = (idx >= 0 && (l.scroll * COLS + idx) < nv) ? idx : -1; if (l.hov != prev) draw(&l); } break;