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
+267
-96
No files matched your search
+42
-5
@@ -161,6 +161,23 @@ static int read_cpu_percent(void) {
|
|||||||
return (int)(100 * (d_total - d_idle) / d_total);
|
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) {
|
static int read_battery_percent(void) {
|
||||||
FILE *f;
|
FILE *f;
|
||||||
char path[80];
|
char path[80];
|
||||||
@@ -422,16 +439,20 @@ void db_draw(Deskbar *db) {
|
|||||||
int mem = read_mem_percent();
|
int mem = read_mem_percent();
|
||||||
int bat = read_battery_percent();
|
int bat = read_battery_percent();
|
||||||
char bst = read_battery_status();
|
char bst = read_battery_status();
|
||||||
char status[64];
|
int vol = read_volume_percent();
|
||||||
|
char status[80];
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
|
if (vol >= 0)
|
||||||
|
pos += snprintf(status + pos, sizeof(status) - pos,
|
||||||
|
"VOL %d%%", vol);
|
||||||
if (cpu >= 0)
|
if (cpu >= 0)
|
||||||
pos += snprintf(status + pos, sizeof(status) - pos,
|
pos += snprintf(status + pos, sizeof(status) - pos,
|
||||||
"CPU %d%%", cpu);
|
"%sCPU %d%%", pos ? " " : "", cpu);
|
||||||
if (mem >= 0)
|
if (mem >= 0)
|
||||||
pos += snprintf(status + pos, sizeof(status) - pos,
|
pos += snprintf(status + pos, sizeof(status) - pos,
|
||||||
" MEM %d%%", mem);
|
" MEM %d%%", mem);
|
||||||
if (bat >= 0) {
|
if (bat >= 0) {
|
||||||
const char *arrow = (bst == 'C') ? "↑" : (bst == 'F') ? "=" : "↓";
|
const char *arrow = (bst == 'C') ? "+" : (bst == 'F') ? "=" : "-";
|
||||||
pos += snprintf(status + pos, sizeof(status) - pos,
|
pos += snprintf(status + pos, sizeof(status) - pos,
|
||||||
" BAT %d%%%s", bat, arrow);
|
" BAT %d%%%s", bat, arrow);
|
||||||
}
|
}
|
||||||
@@ -888,8 +909,24 @@ void db_run(Deskbar *db) {
|
|||||||
if (e.xexpose.count == 0) db_draw(db);
|
if (e.xexpose.count == 0) db_draw(db);
|
||||||
break;
|
break;
|
||||||
case ButtonPress:
|
case ButtonPress:
|
||||||
if (e.xany.window == db->win)
|
if (e.xany.window == db->win) {
|
||||||
db_click(db, e.xbutton.x, e.xbutton.y);
|
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;
|
break;
|
||||||
case MotionNotify:
|
case MotionNotify:
|
||||||
if (e.xany.window == db->win)
|
if (e.xany.window == db->win)
|
||||||
|
|||||||
+225
-91
@@ -3,6 +3,8 @@
|
|||||||
#include <X11/Xatom.h>
|
#include <X11/Xatom.h>
|
||||||
#include <X11/keysym.h>
|
#include <X11/keysym.h>
|
||||||
#include <X11/Xft/Xft.h>
|
#include <X11/Xft/Xft.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <strings.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -11,19 +13,20 @@
|
|||||||
/* ── dimensions ─────────────────────────────────────────────────────── */
|
/* ── dimensions ─────────────────────────────────────────────────────── */
|
||||||
#define WIN_W 580
|
#define WIN_W 580
|
||||||
#define WIN_H 420
|
#define WIN_H 420
|
||||||
#define HDR_H 50 /* search header */
|
#define HDR_H 50
|
||||||
#define TAB_H 34 /* category tab strip */
|
#define TAB_H 34
|
||||||
#define CONTENT_Y (HDR_H + TAB_H)
|
#define CONTENT_Y (HDR_H + TAB_H)
|
||||||
#define PAD 12 /* grid outer margin */
|
#define PAD 12
|
||||||
#define CARD_W 96
|
#define CARD_W 96
|
||||||
#define CARD_H 84
|
#define CARD_H 84
|
||||||
#define CARD_GAP 8
|
#define CARD_GAP 8
|
||||||
#define COLS 5
|
#define COLS 5
|
||||||
#define ICON_SZ 38
|
#define ICON_SZ 38
|
||||||
|
|
||||||
/* grid left edge */
|
|
||||||
#define GX0 ((WIN_W - (COLS * CARD_W + (COLS-1) * CARD_GAP)) / 2)
|
#define GX0 ((WIN_W - (COLS * CARD_W + (COLS-1) * CARD_GAP)) / 2)
|
||||||
#define GY0 (CONTENT_Y + PAD)
|
#define GY0 (CONTENT_Y + PAD)
|
||||||
|
#define ROWS_VIS 3 /* rows visible before scrolling */
|
||||||
|
#define MAX_APPS 256
|
||||||
|
|
||||||
/* ── palette ─────────────────────────────────────────────────────────── */
|
/* ── palette ─────────────────────────────────────────────────────────── */
|
||||||
#define C_BG 0x0E1220
|
#define C_BG 0x0E1220
|
||||||
@@ -40,6 +43,8 @@
|
|||||||
#define C_TEXT_DIM 0x344A62
|
#define C_TEXT_DIM 0x344A62
|
||||||
#define C_TAB_TXT 0x486070
|
#define C_TAB_TXT 0x486070
|
||||||
#define C_TAB_TSEL 0xA8C4E8
|
#define C_TAB_TSEL 0xA8C4E8
|
||||||
|
#define C_SCROLL_BG 0x0C1020
|
||||||
|
#define C_SCROLL_TH 0x1E3258
|
||||||
|
|
||||||
static const unsigned long ICON_BG[] = {
|
static const unsigned long ICON_BG[] = {
|
||||||
0x1A4D88, 0x166C3C, 0x6C3018,
|
0x1A4D88, 0x166C3C, 0x6C3018,
|
||||||
@@ -53,38 +58,147 @@ static const unsigned long ICON_BG[] = {
|
|||||||
#define FONT_ICON "Sans:size=13:weight=bold"
|
#define FONT_ICON "Sans:size=13:weight=bold"
|
||||||
#define MAX_FLT 64
|
#define MAX_FLT 64
|
||||||
|
|
||||||
typedef struct { const char *name, *cmd, *cat; } App;
|
typedef struct { char name[64]; char cmd[256]; char cat[32]; } App;
|
||||||
static const App APPS[] = {
|
|
||||||
{ "Firefox", "firefox &", "Internet" },
|
static App APPS[MAX_APPS];
|
||||||
{ "Email", "firefox https://mail.google.com &", "Internet" },
|
static int NAPP = 0;
|
||||||
{ "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])))
|
|
||||||
|
|
||||||
static const char *CATS[] = {
|
static const char *CATS[] = {
|
||||||
"All", "Internet", "Media", "Office", "System", "Settings"
|
"All", "Internet", "Media", "Office", "System", "Settings"
|
||||||
};
|
};
|
||||||
#define NCAT ((int)(sizeof(CATS)/sizeof(CATS[0])))
|
#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 {
|
typedef struct {
|
||||||
Display *dpy; int screen;
|
Display *dpy; int screen;
|
||||||
Window root, win; GC gc;
|
Window root, win; GC gc;
|
||||||
Colormap cmap; Visual *visual;
|
Colormap cmap; Visual *visual;
|
||||||
XftFont *font_ui, *font_logo, *font_icon;
|
XftFont *font_ui, *font_logo, *font_icon;
|
||||||
int sel_cat, hov;
|
int sel_cat, hov, scroll;
|
||||||
char filter[MAX_FLT];
|
char filter[MAX_FLT];
|
||||||
int filter_len;
|
int filter_len;
|
||||||
} L;
|
} L;
|
||||||
@@ -124,17 +238,19 @@ static void ds_ctr(L *l, XftDraw *xd, XftFont *f, unsigned long rgb,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── query ───────────────────────────────────────────────────────────── */
|
/* ── query ───────────────────────────────────────────────────────────── */
|
||||||
static int nvis(L *l, int out[NAPP]) {
|
static int nvis(L *l, int out[MAX_APPS]) {
|
||||||
int n = 0;
|
int n = 0;
|
||||||
const char *cat = CATS[l->sel_cat];
|
const char *cat = CATS[l->sel_cat];
|
||||||
for (int i = 0; i < NAPP; i++) {
|
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) {
|
if (l->filter_len > 0) {
|
||||||
char nm[64], fl[MAX_FLT];
|
char nm[64], fl[MAX_FLT];
|
||||||
snprintf(nm, sizeof(nm), "%s", APPS[i].name);
|
snprintf(nm, sizeof(nm), "%s", APPS[i].name);
|
||||||
snprintf(fl, sizeof(fl), "%s", l->filter);
|
snprintf(fl, sizeof(fl), "%s", l->filter);
|
||||||
for (char *p=nm; *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;
|
for (char *p = fl; *p; p++) if (*p >= 'A' && *p <= 'Z') *p += 32;
|
||||||
if (!strstr(nm, fl)) continue;
|
if (!strstr(nm, fl)) continue;
|
||||||
}
|
}
|
||||||
out[n++] = i;
|
out[n++] = i;
|
||||||
@@ -142,7 +258,6 @@ static int nvis(L *l, int out[NAPP]) {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tab widths — measured once per draw via font metrics */
|
|
||||||
static void tab_layout(L *l, int widths[NCAT], int *total) {
|
static void tab_layout(L *l, int widths[NCAT], int *total) {
|
||||||
*total = 0;
|
*total = 0;
|
||||||
for (int i = 0; i < NCAT; i++) {
|
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) {
|
static int tab_at(L *l, int x, int y) {
|
||||||
if (y < HDR_H || y >= CONTENT_Y) return -1;
|
if (y < HDR_H || y >= CONTENT_Y) return -1;
|
||||||
int w[NCAT], total; tab_layout(l, w, &total);
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* hit: which card index (into vis[]) is at point? -1 = none */
|
/* card index within visible slice (adjusted for scroll) */
|
||||||
static int card_at(int x, int y) {
|
static int card_at_vis(int x, int y) {
|
||||||
if (y < CONTENT_Y) return -1;
|
if (y < CONTENT_Y) return -1;
|
||||||
int lx = x - GX0;
|
int lx = x - GX0, ly = y - GY0;
|
||||||
int ly = y - GY0;
|
|
||||||
if (lx < 0 || ly < 0) return -1;
|
if (lx < 0 || ly < 0) return -1;
|
||||||
int col = lx / (CARD_W + CARD_GAP);
|
int col = lx / (CARD_W + CARD_GAP);
|
||||||
int row = ly / (CARD_H + CARD_GAP);
|
int row = ly / (CARD_H + CARD_GAP);
|
||||||
if (col < 0 || col >= COLS || row < 0) return -1;
|
if (col < 0 || col >= COLS || row < 0 || row >= ROWS_VIS) return -1;
|
||||||
/* reject click in gap between cards */
|
if (lx - col*(CARD_W+CARD_GAP) >= CARD_W) return -1;
|
||||||
int cx = col * (CARD_W + CARD_GAP);
|
if (ly - row*(CARD_H+CARD_GAP) >= CARD_H) return -1;
|
||||||
int cy = row * (CARD_H + CARD_GAP);
|
|
||||||
if (lx - cx >= CARD_W || ly - cy >= CARD_H) return -1;
|
|
||||||
return row * COLS + col;
|
return row * COLS + col;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── draw ─────────────────────────────────────────────────────────────── */
|
/* ── draw ────────────────────────────────────────────────────────────── */
|
||||||
static void draw(L *l) {
|
static void draw(L *l) {
|
||||||
XftDraw *xd = XftDrawCreate(l->dpy, l->win, l->visual, l->cmap);
|
XftDraw *xd = XftDrawCreate(l->dpy, l->win, l->visual, l->cmap);
|
||||||
|
|
||||||
/* background */
|
|
||||||
XSetForeground(l->dpy, l->gc, mkc(l, C_BG));
|
XSetForeground(l->dpy, l->gc, mkc(l, C_BG));
|
||||||
XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, WIN_H);
|
XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, WIN_H);
|
||||||
|
|
||||||
/* header */
|
/* header */
|
||||||
XSetForeground(l->dpy, l->gc, mkc(l, C_HDR));
|
XSetForeground(l->dpy, l->gc, mkc(l, C_HDR));
|
||||||
XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, HDR_H);
|
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));
|
XSetForeground(l->dpy, l->gc, mkc(l, C_ACCENT));
|
||||||
XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, 2);
|
XFillRectangle(l->dpy, l->win, l->gc, 0, 0, WIN_W, 2);
|
||||||
|
|
||||||
/* logo */
|
|
||||||
ds(l, xd, l->font_logo, C_ACCENT, 14, 13, "SkyOS");
|
ds(l, xd, l->font_logo, C_ACCENT, 14, 13, "SkyOS");
|
||||||
|
|
||||||
/* search box */
|
/* search box */
|
||||||
@@ -207,17 +314,15 @@ static void draw(L *l) {
|
|||||||
XFillRectangle(l->dpy, l->win, l->gc, sx, sy, sw, sh);
|
XFillRectangle(l->dpy, l->win, l->gc, sx, sy, sw, sh);
|
||||||
XSetForeground(l->dpy, l->gc, mkc(l, C_SEARCH_BD));
|
XSetForeground(l->dpy, l->gc, mkc(l, C_SEARCH_BD));
|
||||||
XDrawRectangle(l->dpy, l->win, l->gc, sx, sy, sw, sh);
|
XDrawRectangle(l->dpy, l->win, l->gc, sx, sy, sw, sh);
|
||||||
|
|
||||||
if (l->filter_len == 0) {
|
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 {
|
} else {
|
||||||
char buf[MAX_FLT+2];
|
char buf[MAX_FLT + 2];
|
||||||
snprintf(buf, sizeof(buf), "%s|", l->filter);
|
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));
|
XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER));
|
||||||
XDrawLine(l->dpy, l->win, l->gc, 0, HDR_H, WIN_W, HDR_H);
|
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++) {
|
for (int i = 0; i < NCAT; i++) {
|
||||||
if (i == l->sel_cat) {
|
if (i == l->sel_cat) {
|
||||||
XSetForeground(l->dpy, l->gc, mkc(l, C_TAB_SEL));
|
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);
|
XFillRectangle(l->dpy, l->win, l->gc, tx, HDR_H + 1, tw[i], TAB_H - 1);
|
||||||
/* accent underline */
|
|
||||||
XSetForeground(l->dpy, l->gc, mkc(l, C_ACCENT));
|
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,
|
||||||
ds_ctr(l, xd, l->font_ui, tc, tx, HDR_H, tw[i], TAB_H, CATS[i]);
|
(i == l->sel_cat) ? C_TAB_TSEL : C_TAB_TXT,
|
||||||
|
tx, HDR_H, tw[i], TAB_H, CATS[i]);
|
||||||
tx += tw[i];
|
tx += tw[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* separator below tabs */
|
|
||||||
XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER));
|
XSetForeground(l->dpy, l->gc, mkc(l, C_BORDER));
|
||||||
XDrawLine(l->dpy, l->win, l->gc, 0, CONTENT_Y, WIN_W, CONTENT_Y);
|
XDrawLine(l->dpy, l->win, l->gc, 0, CONTENT_Y, WIN_W, CONTENT_Y);
|
||||||
|
|
||||||
/* app grid */
|
/* app grid with scroll */
|
||||||
int vis[NAPP];
|
int vis[MAX_APPS];
|
||||||
int nv = nvis(l, vis);
|
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) {
|
if (nv == 0) {
|
||||||
ds_ctr(l, xd, l->font_ui, C_TEXT_DIM,
|
ds_ctr(l, xd, l->font_ui, C_TEXT_DIM,
|
||||||
0, CONTENT_Y, WIN_W, WIN_H - CONTENT_Y, "No apps found");
|
0, CONTENT_Y, WIN_W, WIN_H - CONTENT_Y, "No apps found");
|
||||||
} else {
|
} 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 col = i % COLS;
|
||||||
int row = i / COLS;
|
int row = i / COLS;
|
||||||
int cx = GX0 + col * (CARD_W + CARD_GAP);
|
int cx = GX0 + col * (CARD_W + CARD_GAP);
|
||||||
int cy = GY0 + row * (CARD_H + 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);
|
int hover = (i == l->hov);
|
||||||
|
|
||||||
/* card hover highlight */
|
|
||||||
if (hover) {
|
if (hover) {
|
||||||
XSetForeground(l->dpy, l->gc, mkc(l, C_CARD_HOV));
|
XSetForeground(l->dpy, l->gc, mkc(l, C_CARD_HOV));
|
||||||
XFillRectangle(l->dpy, l->win, l->gc, cx, cy, CARD_W, CARD_H);
|
XFillRectangle(l->dpy, l->win, l->gc, cx, cy, CARD_W, CARD_H);
|
||||||
XSetForeground(l->dpy, l->gc, mkc(l, C_CARD_BD));
|
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[vi];
|
||||||
int app_i = vis[i];
|
|
||||||
unsigned int h = 0;
|
unsigned int h = 0;
|
||||||
for (const char *p = APPS[app_i].name; *p; p++) h = h*31 + (unsigned char)*p;
|
for (const char *p = APPS[app_i].name; *p; p++) h = h * 31 + (unsigned char)*p;
|
||||||
unsigned long ibg = ICON_BG[h % NICON];
|
|
||||||
int ix = cx + (CARD_W - ICON_SZ) / 2;
|
int ix = cx + (CARD_W - ICON_SZ) / 2;
|
||||||
int iy = cy + 10;
|
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);
|
XFillRectangle(l->dpy, l->win, l->gc, ix, iy, ICON_SZ, ICON_SZ);
|
||||||
|
|
||||||
/* icon letter */
|
|
||||||
char letter[2] = { APPS[app_i].name[0], '\0' };
|
char letter[2] = { APPS[app_i].name[0], '\0' };
|
||||||
if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32;
|
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);
|
ds_ctr(l, xd, l->font_icon, 0xEAF0FF, ix, iy, ICON_SZ, ICON_SZ, letter);
|
||||||
|
|
||||||
/* label */
|
|
||||||
char lbl[14];
|
char lbl[14];
|
||||||
snprintf(lbl, sizeof(lbl), "%.12s", APPS[app_i].name);
|
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,
|
||||||
ds_ctr(l, xd, l->font_ui, tc,
|
hover ? C_TEXT_HOV : C_TEXT,
|
||||||
cx, iy + ICON_SZ + 4, CARD_W, CARD_H - ICON_SZ - 18, lbl);
|
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 */
|
/* outer border */
|
||||||
XSetForeground(l->dpy, l->gc, mkc(l, C_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);
|
XftDrawDestroy(xd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── event handlers ──────────────────────────────────────────────────── */
|
/* ── event handlers ──────────────────────────────────────────────────── */
|
||||||
static void launch(L *l, int hov) {
|
static void launch(L *l, int vis_idx) {
|
||||||
int vis[NAPP];
|
int vis[MAX_APPS];
|
||||||
int nv = nvis(l, vis);
|
int nv = nvis(l, vis);
|
||||||
if (hov >= 0 && hov < nv) {
|
int abs_idx = l->scroll * COLS + vis_idx;
|
||||||
system(APPS[vis[hov]].cmd);
|
if (abs_idx >= 0 && abs_idx < nv) {
|
||||||
|
system(APPS[vis[abs_idx]].cmd);
|
||||||
XDestroyWindow(l->dpy, l->win);
|
XDestroyWindow(l->dpy, l->win);
|
||||||
XCloseDisplay(l->dpy);
|
XCloseDisplay(l->dpy);
|
||||||
exit(0);
|
exit(0);
|
||||||
@@ -311,28 +434,32 @@ static void launch(L *l, int hov) {
|
|||||||
static void on_key(L *l, XKeyEvent *e) {
|
static void on_key(L *l, XKeyEvent *e) {
|
||||||
KeySym sym = XLookupKeysym(e, 0);
|
KeySym sym = XLookupKeysym(e, 0);
|
||||||
if (sym == XK_Escape) {
|
if (sym == XK_Escape) {
|
||||||
XDestroyWindow(l->dpy, l->win);
|
XDestroyWindow(l->dpy, l->win); XCloseDisplay(l->dpy); exit(0);
|
||||||
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) {
|
if (sym == XK_BackSpace && l->filter_len > 0) {
|
||||||
l->filter[--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};
|
char buf[8] = {0};
|
||||||
int len = XLookupString(e, buf, sizeof(buf)-1, &sym, NULL);
|
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) {
|
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++] = buf[0];
|
||||||
l->filter[l->filter_len] = '\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);
|
draw(l);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── main ─────────────────────────────────────────────────────────────── */
|
/* ── main ─────────────────────────────────────────────────────────────── */
|
||||||
int main(void) {
|
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);
|
l.dpy = XOpenDisplay(NULL);
|
||||||
if (!l.dpy) { fprintf(stderr, "skylauncher: cannot open display\n"); return 1; }
|
if (!l.dpy) { fprintf(stderr, "skylauncher: cannot open display\n"); return 1; }
|
||||||
@@ -377,21 +504,28 @@ int main(void) {
|
|||||||
break;
|
break;
|
||||||
case ButtonPress:
|
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);
|
int t = tab_at(&l, e.xbutton.x, e.xbutton.y);
|
||||||
if (t >= 0) {
|
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 {
|
} else {
|
||||||
int idx = card_at(e.xbutton.x, e.xbutton.y);
|
int idx = card_at_vis(e.xbutton.x, e.xbutton.y);
|
||||||
launch(&l, idx);
|
if (idx >= 0) launch(&l, idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MotionNotify:
|
case MotionNotify:
|
||||||
{
|
{
|
||||||
int idx = card_at(e.xmotion.x, e.xmotion.y);
|
int vis[MAX_APPS]; int nv = nvis(&l, vis);
|
||||||
int vis[NAPP]; int nv = nvis(&l, vis);
|
int idx = card_at_vis(e.xmotion.x, e.xmotion.y);
|
||||||
int prev = l.hov;
|
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);
|
if (l.hov != prev) draw(&l);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in new issue
Block a user