Phase 1/2: .desktop launcher, volume control in deskbar
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
9089c14a19
commit
2b8ea90a57
2 files changed
+266
-95
No files matched your search
+41
-4
@@ -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)
|
||||
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)
|
||||
|
||||
+225
-91
@@ -3,6 +3,8 @@
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/keysym.h>
|
||||
#include <X11/Xft/Xft.h>
|
||||
#include <dirent.h>
|
||||
#include <strings.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user