diff --git a/orionpkg/Makefile b/orionpkg/Makefile index 042cefe..9de096e 100644 --- a/orionpkg/Makefile +++ b/orionpkg/Makefile @@ -1,15 +1,22 @@ +CC = gcc + +PKG_C := $(shell pkg-config --cflags x11 xft 2>/dev/null) +PKG_L := $(shell pkg-config --libs x11 xft 2>/dev/null) + +ifeq ($(PKG_C),) + $(error "Required: libx11-dev libxft-dev") +endif + +CFLAGS = -Wall -Wextra -O2 $(PKG_C) +LDFLAGS = $(PKG_L) + BIN = orionpkg -CC = gcc -CFLAGS = -Wall -Wextra -O2 \ - $(shell pkg-config --cflags x11 xft fontconfig 2>/dev/null) -LIBS = $(shell pkg-config --libs x11 xft fontconfig 2>/dev/null) \ - -lX11 -lXft -lfontconfig - +.PHONY: all clean install +all: $(BIN) $(BIN): orionpkg.c - $(CC) $(CFLAGS) -o $@ $< $(LIBS) - + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +install: $(BIN) + install -Dm755 $(BIN) /usr/local/bin/$(BIN) clean: rm -f $(BIN) - -.PHONY: clean diff --git a/orionpkg/orionpkg.c b/orionpkg/orionpkg.c index 9305fe0..eabb039 100644 --- a/orionpkg/orionpkg.c +++ b/orionpkg/orionpkg.c @@ -1,9 +1,12 @@ -/* orionpkg.c — OrionOS Package Manager +/* + * orionpkg — OrionOS Package Manager * - * States: CHECKING → UPTODATE | UPDATES → UPDATING → DONE/ERROR - * Uses checkupdates (pacman-contrib) to list pending upgrades. - * Runs pkexec pacman -Syu for the actual update. - * Native C/Xlib + Xft, same dark-navy palette as OrionOS. + * Three tabs: + * Search — pacman -Ss, install selected with pkexec pacman -S + * Installed — pacman -Q, remove selected with pkexec pacman -R + * Updates — checkupdates, upgrade all with pkexec pacman -Syu + * + * Native C/Xlib + Xft. Same dark-navy palette as the rest of OrionOS. */ #include #include @@ -19,17 +22,20 @@ #include #include -/* ── layout ─────────────────────────────────────────────────────────── */ -#define WIN_W 620 -#define WIN_H 500 -#define HDR_H 60 -#define FOOT_H 52 -#define BODY_Y HDR_H -#define BODY_H (WIN_H - HDR_H - FOOT_H) -#define PAD 20 -#define ROW_H 28 -#define BTN_W 120 -#define BTN_H 30 +/* ── layout ──────────────────────────────────────────────────────────── */ +#define WIN_W 700 +#define WIN_H 540 +#define TAB_H 36 +#define HDR_H 52 +#define FOOT_H 52 +#define BODY_Y (HDR_H + TAB_H) +#define BODY_H (WIN_H - HDR_H - TAB_H - FOOT_H) +#define PAD 16 +#define ROW_H 28 +#define BTN_W 130 +#define BTN_H 30 +#define SEARCH_H 28 +#define NTABS 3 /* ── palette ─────────────────────────────────────────────────────────── */ #define C_BG 0x0E1220 @@ -41,10 +47,12 @@ #define C_SEP 0x162030 #define C_ACCENT 0x1E58A8 #define C_ACCENT_H 0x2A70CC +#define C_TAB_ACT 0x182848 +#define C_TAB_IDLE 0x090C18 #define C_BTN 0x182848 #define C_BTN_HOV 0x1E3860 -#define C_BTN_UPD 0x1A4888 -#define C_BTN_UPD_H 0x2060AA +#define C_BTN_ACT 0x1E58A8 +#define C_BTN_ACT_H 0x2A70CC #define C_BTN_DNG 0x5A1414 #define C_BTN_DNG_H 0x882020 #define C_TEXT 0xA8C4DC @@ -58,31 +66,46 @@ #define C_TEXT_WARN 0xFFCC44 #define C_ROW_ALT 0x0C1520 #define C_ROW_HOV 0x132030 +#define C_ROW_SEL 0x1A3058 #define C_BAR_BG 0x141E30 #define C_BAR_FG 0x1E58A8 #define C_MONO 0x7AAAC8 +#define C_INPUT_BG 0x080E1A +#define C_INPUT_BOR 0x2A4870 +#define C_INPUT_ACT 0x1E58A8 +#define C_CURSOR 0xA0C8E8 #define FONT_UI "Sans:size=9" #define FONT_HDR "Sans:size=10:weight=bold" #define FONT_MONO "DejaVu Sans Mono:size=8" -/* ── states ─────────────────────────────────────────────────────────── */ +/* ── tabs ─────────────────────────────────────────────────────────────── */ +typedef enum { TAB_SEARCH = 0, TAB_INSTALLED = 1, TAB_UPDATES = 2 } Tab; +static const char *TAB_LABELS[] = { "Search", "Installed", "Updates" }; + +/* ── operation state ─────────────────────────────────────────────────── */ typedef enum { - ST_CHECKING, /* running checkupdates */ - ST_UPTODATE, /* no updates found */ - ST_UPDATES, /* update list ready */ - ST_UPDATING, /* pacman -Syu running */ - ST_DONE, /* success */ - ST_ERROR, /* failed */ -} State; + OP_IDLE, + OP_SEARCHING, + OP_LOADING, /* loading installed list */ + OP_CHECKING, /* checkupdates */ + OP_RUNNING, /* install / remove / upgrade in progress */ + OP_DONE, + OP_ERROR, +} OpState; /* ── package entry ───────────────────────────────────────────────────── */ -#define MAX_PKGS 256 -typedef struct { char name[48]; char old_ver[24]; char new_ver[24]; } Pkg; +#define MAX_PKGS 512 +typedef struct { + char name[64]; + char ver[32]; + char desc[128]; + int installed; /* 1 if installed (search results only) */ +} Pkg; /* ── log buffer ──────────────────────────────────────────────────────── */ -#define MAX_LOG 300 -#define LOG_COLS 90 +#define MAX_LOG 300 +#define LOG_COLS 96 typedef struct { char lines[MAX_LOG][LOG_COLS+1]; int head, count; @@ -94,35 +117,58 @@ static void log_push(LogBuf *lb, const char *line) { if (lb->count < MAX_LOG) lb->count++; } -/* ── app struct ──────────────────────────────────────────────────────── */ +/* ── text input ──────────────────────────────────────────────────────── */ typedef struct { - Display *dpy; - int screen; - Window root, win; - GC gc; - Colormap cmap; - Visual *visual; - int depth; + char buf[256]; + int len; + int focused; +} TextInput; - XftFont *font_ui, *font_hdr, *font_mono; +static void input_insert(TextInput *t, char c) { + if (t->len >= 254) return; + t->buf[t->len++] = c; + t->buf[t->len] = '\0'; +} +static void input_backspace(TextInput *t) { + if (t->len > 0) t->buf[--t->len] = '\0'; +} - State state; - Pkg pkgs[MAX_PKGS]; - int npkgs; - int scroll; /* list scroll offset */ - int hover_pkg; /* hovered list row */ - int hover_btn; /* 0=none,1=update,2=refresh,3=close */ +/* ── app ─────────────────────────────────────────────────────────────── */ +typedef struct { + Display *dpy; + int screen; + Window root, win; + GC gc; + Colormap cmap; + Visual *visual; + int depth; - LogBuf log; - int update_fd; /* pipe fd for pacman output */ - int progress; /* 0-100 estimated */ - int spin_frame; /* animation tick */ + XftFont *font_ui, *font_hdr, *font_mono; - char status_msg[256]; -} Pkg_App; + Tab tab; + OpState op; + + Pkg pkgs[MAX_PKGS]; + int npkgs; + int selected; /* -1 = none */ + int scroll; + int hover_row; + int hover_btn; /* 1=action, 2=refresh/cancel, 3=close */ + + TextInput search_input; + int search_focused; + + LogBuf log; + int op_fd; + int progress; + int spin_frame; + + char status[256]; + char op_pkg[64]; /* package being installed/removed */ +} App; /* ── X helpers ───────────────────────────────────────────────────────── */ -static unsigned long xcol(Pkg_App *a, unsigned long rgb) { +static unsigned long xcol(App *a, unsigned long rgb) { XColor c = { .red = ((rgb>>16)&0xFF)*257, .green = ((rgb>> 8)&0xFF)*257, @@ -132,8 +178,7 @@ static unsigned long xcol(Pkg_App *a, unsigned long rgb) { XAllocColor(a->dpy, a->cmap, &c); return c.pixel; } - -static void xftcol(Pkg_App *a, unsigned long rgb, XftColor *out) { +static void xftcol(App *a, unsigned long rgb, XftColor *out) { XRenderColor rc = { .red = ((rgb>>16)&0xFF)*257, .green = ((rgb>> 8)&0xFF)*257, @@ -142,19 +187,15 @@ static void xftcol(Pkg_App *a, unsigned long rgb, XftColor *out) { }; XftColorAllocValue(a->dpy, a->visual, a->cmap, &rc, out); } - -static void fill(Pkg_App *a, unsigned long rgb, int x, int y, int w, int h) { +static void fill(App *a, unsigned long rgb, int x, int y, int w, int h) { XSetForeground(a->dpy, a->gc, xcol(a, rgb)); XFillRectangle(a->dpy, a->win, a->gc, x, y, w, h); } - -static void outline(Pkg_App *a, unsigned long rgb, int x, int y, int w, int h) { +static void outline(App *a, unsigned long rgb, int x, int y, int w, int h) { XSetForeground(a->dpy, a->gc, xcol(a, rgb)); XDrawRectangle(a->dpy, a->win, a->gc, x, y, w-1, h-1); } - -static void text(Pkg_App *a, XftFont *f, unsigned long rgb, - int x, int y, const char *s) { +static void txt(App *a, XftFont *f, unsigned long rgb, int x, int y, const char *s) { if (!s || !s[0]) return; XftDraw *xd = XftDrawCreate(a->dpy, a->win, a->visual, a->cmap); XftColor c; xftcol(a, rgb, &c); @@ -162,190 +203,238 @@ static void text(Pkg_App *a, XftFont *f, unsigned long rgb, XftColorFree(a->dpy, a->visual, a->cmap, &c); XftDrawDestroy(xd); } - -static int textw(Pkg_App *a, XftFont *f, const char *s) { +static int txtw(App *a, XftFont *f, const char *s) { XGlyphInfo ext; XftTextExtentsUtf8(a->dpy, f, (const FcChar8 *)s, strlen(s), &ext); return ext.xOff; } - -static int baseline(XftFont *f, int y, int h) { - return y + (h + f->ascent - f->descent) / 2; -} - +static int bl(XftFont *f, int y, int h) { return y + (h + f->ascent - f->descent)/2; } static int in_rect(int mx, int my, int x, int y, int w, int h) { return mx>=x && mx=y && myhover_btn == btn_id) ? bghov : bg; + unsigned long b = (a->hover_btn == id) ? bghov : bg; fill(a, b, x, y, w, h); outline(a, C_BORDER, x, y, w, h); - int tx = x + (w - textw(a, a->font_ui, label)) / 2; - text(a, a->font_ui, C_TEXT_HDR, tx, baseline(a->font_ui, y, h), label); + int tx = x + (w - txtw(a, a->font_ui, label))/2; + txt(a, a->font_ui, C_TEXT_HDR, tx, bl(a->font_ui, y, h), label); } -/* ── header ──────────────────────────────────────────────────────────── */ -static void draw_header(Pkg_App *a) { +static void draw_header(App *a) { fill(a, C_HDR, 0, 0, WIN_W, HDR_H); fill(a, C_BORDER, 0, HDR_H-1, WIN_W, 1); - - /* accent stripe */ fill(a, C_ACCENT, 0, 0, 3, HDR_H); + txt(a, a->font_hdr, C_TEXT_HDR, PAD+8, bl(a->font_hdr, 0, HDR_H)-6, + "OrionOS Package Manager"); - text(a, a->font_hdr, C_TEXT_HDR, PAD + 8, baseline(a->font_hdr, 0, HDR_H) - 8, - "OrionOS Package Manager"); - - /* subtitle */ - const char *sub = (a->state == ST_CHECKING) ? "Checking for updates…" : - (a->state == ST_UPTODATE) ? "System is up to date" : - (a->state == ST_UPDATING) ? "Updating system…" : - (a->state == ST_DONE) ? "Update complete" : - (a->state == ST_ERROR) ? "Update failed" : - /* ST_UPDATES */ ""; - char sub_buf[64]; - if (a->state == ST_UPDATES) { - snprintf(sub_buf, sizeof(sub_buf), "%d package%s available", - a->npkgs, a->npkgs == 1 ? "" : "s"); - sub = sub_buf; - } - unsigned long sc = (a->state == ST_DONE) ? C_TEXT_OK : - (a->state == ST_ERROR) ? C_TEXT_ERR : C_TEXT_DIM; - text(a, a->font_ui, sc, PAD + 8, baseline(a->font_ui, 0, HDR_H) + 10, sub); + const char *sub = (a->op == OP_SEARCHING) ? "Searching…" : + (a->op == OP_LOADING) ? "Loading package list…" : + (a->op == OP_CHECKING) ? "Checking for updates…" : + (a->op == OP_RUNNING) ? a->status : + (a->op == OP_DONE) ? a->status : + (a->op == OP_ERROR) ? a->status : + (a->tab == TAB_SEARCH && a->npkgs > 0) + ? "Press Enter or click Search" : ""; + unsigned long sc = (a->op == OP_DONE) ? C_TEXT_OK : + (a->op == OP_ERROR) ? C_TEXT_ERR : C_TEXT_DIM; + txt(a, a->font_ui, sc, PAD+8, bl(a->font_hdr, 0, HDR_H)+10, sub); } -/* ── footer ──────────────────────────────────────────────────────────── */ -static void draw_footer(Pkg_App *a) { +static void draw_tabs(App *a) { + int ty = HDR_H; + int tw = WIN_W / NTABS; + fill(a, C_HDR, 0, ty, WIN_W, TAB_H); + fill(a, C_BORDER, 0, ty+TAB_H-1, WIN_W, 1); + for (int i = 0; i < NTABS; i++) { + int tx = i * tw; + int is_act = (a->tab == (Tab)i); + fill(a, is_act ? C_TAB_ACT : C_TAB_IDLE, tx, ty, tw, TAB_H); + if (is_act) fill(a, C_ACCENT, tx, ty+TAB_H-2, tw, 2); + else outline(a, C_BORDER, tx, ty, tw, TAB_H); + unsigned long tc = is_act ? C_TEXT_HDR : C_TEXT_DIM; + int lx = tx + (tw - txtw(a, a->font_ui, TAB_LABELS[i]))/2; + txt(a, a->font_ui, tc, lx, bl(a->font_ui, ty, TAB_H), TAB_LABELS[i]); + } +} + +static void draw_footer(App *a) { int fy = WIN_H - FOOT_H; fill(a, C_FOOT, 0, fy, WIN_W, FOOT_H); fill(a, C_BORDER, 0, fy, WIN_W, 1); + int by = fy + (FOOT_H - BTN_H)/2; - int by = fy + (FOOT_H - BTN_H) / 2; + draw_btn(a, 3, WIN_W - BTN_W - PAD, by, BTN_W, BTN_H, "Close", C_BTN, C_BTN_HOV); - /* Close always available */ - draw_btn(a, 3, WIN_W - BTN_W - PAD, by, BTN_W, BTN_H, - "Close", C_BTN, C_BTN_HOV); + if (a->op == OP_RUNNING) { + draw_btn(a, 2, PAD, by, BTN_W, BTN_H, "Cancel", C_BTN_DNG, C_BTN_DNG_H); + return; + } - if (a->state == ST_UPDATES) { - draw_btn(a, 1, WIN_W - BTN_W*2 - PAD - 8, by, BTN_W, BTN_H, - "Update All", C_BTN_UPD, C_BTN_UPD_H); - draw_btn(a, 2, PAD, by, BTN_W - 20, BTN_H, - "Refresh", C_BTN, C_BTN_HOV); - } else if (a->state == ST_UPTODATE || a->state == ST_ERROR) { - draw_btn(a, 2, PAD, by, BTN_W - 20, BTN_H, - "Refresh", C_BTN, C_BTN_HOV); + if (a->tab == TAB_SEARCH) { + draw_btn(a, 2, PAD, by, BTN_W, BTN_H, "Search", C_BTN, C_BTN_HOV); + if (a->selected >= 0 && a->selected < a->npkgs) { + const char *lbl = a->pkgs[a->selected].installed ? "Reinstall" : "Install"; + draw_btn(a, 1, WIN_W - BTN_W*2 - PAD - 8, by, BTN_W, BTN_H, + lbl, C_BTN_ACT, C_BTN_ACT_H); + } + } else if (a->tab == TAB_INSTALLED) { + draw_btn(a, 2, PAD, by, BTN_W, BTN_H, "Refresh", C_BTN, C_BTN_HOV); + if (a->selected >= 0 && a->selected < a->npkgs) + draw_btn(a, 1, WIN_W - BTN_W*2 - PAD - 8, by, BTN_W, BTN_H, + "Remove", C_BTN_DNG, C_BTN_DNG_H); + } else { /* updates */ + draw_btn(a, 2, PAD, by, BTN_W, BTN_H, "Refresh", C_BTN, C_BTN_HOV); + if (a->npkgs > 0) + draw_btn(a, 1, WIN_W - BTN_W*2 - PAD - 8, by, BTN_W, BTN_H, + "Update All", C_BTN_ACT, C_BTN_ACT_H); } } -/* ── spinning indicator ──────────────────────────────────────────────── */ -static const char *SPIN[] = { "⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏" }; -#define NSPIN 10 +/* ── search input field ──────────────────────────────────────────────── */ +#define INPUT_X PAD +#define INPUT_Y (BODY_Y + PAD) +#define INPUT_W (WIN_W - PAD*2 - BTN_W - 8) +#define INPUT_H SEARCH_H -/* ── package list ────────────────────────────────────────────────────── */ -#define LIST_ROWS_VIS ((BODY_H - 4) / ROW_H) -#define VER_X (WIN_W / 2 - 40) -#define ARROW_X (WIN_W / 2 + 80) -#define NEWVER_X (WIN_W / 2 + 100) - -static void draw_list_header(Pkg_App *a) { - int hy = BODY_Y + 2; - fill(a, 0x0A1220, 0, hy, WIN_W, ROW_H - 2); - fill(a, C_SEP, 0, hy + ROW_H - 3, WIN_W, 1); - text(a, a->font_ui, C_TEXT_DIM, PAD, baseline(a->font_ui, hy, ROW_H-2), "Package"); - text(a, a->font_ui, C_TEXT_DIM, VER_X, baseline(a->font_ui, hy, ROW_H-2), "Current"); - text(a, a->font_ui, C_TEXT_DIM, NEWVER_X, baseline(a->font_ui, hy, ROW_H-2), "New"); +static void draw_search_input(App *a) { + fill(a, C_INPUT_BG, INPUT_X, INPUT_Y, INPUT_W, INPUT_H); + unsigned long bor = a->search_input.focused ? C_INPUT_ACT : C_INPUT_BOR; + outline(a, bor, INPUT_X, INPUT_Y, INPUT_W, INPUT_H); + txt(a, a->font_ui, C_TEXT, INPUT_X+6, bl(a->font_ui, INPUT_Y, INPUT_H), + a->search_input.buf); + /* cursor */ + if (a->search_input.focused) { + int cx = INPUT_X + 6 + txtw(a, a->font_ui, a->search_input.buf); + XSetForeground(a->dpy, a->gc, xcol(a, C_CURSOR)); + XDrawLine(a->dpy, a->win, a->gc, cx, INPUT_Y+4, cx, INPUT_Y+INPUT_H-5); + } } -static void draw_updates(Pkg_App *a) { - fill(a, C_BODY, 0, BODY_Y, WIN_W, BODY_H); +/* ── package list ────────────────────────────────────────────────────── */ +#define LIST_Y_SEARCH (INPUT_Y + INPUT_H + 6) +#define LIST_Y_PLAIN (BODY_Y + 2) - draw_list_header(a); +static int list_top(App *a) { + return (a->tab == TAB_SEARCH) ? LIST_Y_SEARCH : LIST_Y_PLAIN; +} +static int list_rows_vis(App *a) { + int lh = WIN_H - FOOT_H - list_top(a); + return lh / ROW_H; +} - int list_y = BODY_Y + ROW_H; - int vis = LIST_ROWS_VIS - 1; +#define DESC_X 200 - for (int i = a->scroll; i < a->npkgs && (i - a->scroll) < vis; i++) { - int ry = list_y + (i - a->scroll) * ROW_H; - int is_hov = (a->hover_pkg == i); +static void draw_list(App *a) { + int ly = list_top(a); + int vis = list_rows_vis(a); - if (is_hov) + /* column headers */ + fill(a, 0x0A1220, 0, ly, WIN_W, ROW_H-2); + fill(a, C_SEP, 0, ly+ROW_H-3, WIN_W, 1); + txt(a, a->font_ui, C_TEXT_DIM, PAD, bl(a->font_ui, ly, ROW_H-2), "Name"); + txt(a, a->font_ui, C_TEXT_DIM, DESC_X, bl(a->font_ui, ly, ROW_H-2), "Version"); + txt(a, a->font_ui, C_TEXT_DIM, DESC_X+100, bl(a->font_ui, ly, ROW_H-2), "Description"); + ly += ROW_H; + + for (int i = a->scroll; i < a->npkgs && (i-a->scroll) < vis-1; i++) { + int ry = ly + (i-a->scroll)*ROW_H; + int sel = (a->selected == i); + int hov = (a->hover_row == i); + + if (sel) + fill(a, C_ROW_SEL, 0, ry, WIN_W, ROW_H); + else if (hov) fill(a, C_ROW_HOV, 0, ry, WIN_W, ROW_H); - else if ((i - a->scroll) % 2 == 1) + else if ((i-a->scroll)%2 == 1) fill(a, C_ROW_ALT, 0, ry, WIN_W, ROW_H); - fill(a, C_SEP, 0, ry + ROW_H - 1, WIN_W, 1); + fill(a, C_SEP, 0, ry+ROW_H-1, WIN_W, 1); - int ty = baseline(a->font_ui, ry, ROW_H); - text(a, a->font_ui, is_hov ? C_TEXT_HOV : C_TEXT_PKG, PAD, ty, a->pkgs[i].name); - text(a, a->font_ui, C_TEXT_VER, VER_X, ty, a->pkgs[i].old_ver); + int ty = bl(a->font_ui, ry, ROW_H); + unsigned long nc = sel ? C_TEXT_HOV : C_TEXT_PKG; + txt(a, a->font_ui, nc, PAD, ty, a->pkgs[i].name); + txt(a, a->font_ui, C_TEXT_VER, DESC_X, ty, a->pkgs[i].ver); - /* arrow */ - text(a, a->font_ui, C_TEXT_DIM, ARROW_X - 16, ty, "→"); + /* clip description */ + char desc[80]; + strncpy(desc, a->pkgs[i].desc, 79); desc[79] = '\0'; + XRectangle clip = { (short)(DESC_X+100), (short)ry, + (unsigned short)(WIN_W - DESC_X - 108), + (unsigned short)ROW_H }; + XftDraw *xd = XftDrawCreate(a->dpy, a->win, a->visual, a->cmap); + XftDrawSetClipRectangles(xd, 0, 0, &clip, 1); + XftColor col; xftcol(a, sel ? C_TEXT_HOV : C_TEXT_DIM, &col); + XftDrawStringUtf8(xd, &col, a->font_ui, DESC_X+100, ty, + (const FcChar8 *)desc, strlen(desc)); + XftColorFree(a->dpy, a->visual, a->cmap, &col); + XftDrawSetClip(xd, None); + XftDrawDestroy(xd); - text(a, a->font_ui, C_TEXT_OK, NEWVER_X, ty, a->pkgs[i].new_ver); + /* installed badge on search tab */ + if (a->tab == TAB_SEARCH && a->pkgs[i].installed) { + int bx = WIN_W - 74; + fill(a, 0x0F2818, bx, ry+5, 66, ROW_H-10); + outline(a, 0x2A6840, bx, ry+5, 66, ROW_H-10); + txt(a, a->font_ui, C_TEXT_OK, bx+4, bl(a->font_ui, ry+5, ROW_H-10), "installed"); + } } /* scrollbar */ - if (a->npkgs > vis) { + int vis2 = vis - 1; + if (a->npkgs > vis2) { int sb_x = WIN_W - 6; - int sb_h = BODY_H - ROW_H; - int th_h = sb_h * vis / a->npkgs; + int sb_h = (vis2) * ROW_H; + int th_h = sb_h * vis2 / a->npkgs; if (th_h < 20) th_h = 20; - int th_y = list_y + (sb_h - th_h) * a->scroll / (a->npkgs - vis); - fill(a, C_SEP, sb_x, list_y, 4, sb_h); + int th_y = ly + (sb_h - th_h) * a->scroll / (a->npkgs - vis2); + fill(a, C_SEP, sb_x, ly, 4, sb_h); fill(a, C_ACCENT, sb_x, th_y, 4, th_h); } } -/* ── log view (used during update) ──────────────────────────────────── */ -#define LOG_ROWS_VIS ((BODY_H - 40) / 16) -#define LOG_ROW_H 16 - -static void draw_log(Pkg_App *a) { +/* ── log view ────────────────────────────────────────────────────────── */ +#define LOG_ROW_H 16 +static void draw_log(App *a) { fill(a, C_BODY, 0, BODY_Y, WIN_W, BODY_H); - /* progress bar */ int bx = PAD, bw = WIN_W - PAD*2, bh = 8; int by = BODY_Y + 10; fill(a, C_BAR_BG, bx, by, bw, bh); int fw = bw * a->progress / 100; if (fw > 0) fill(a, C_BAR_FG, bx, by, fw, bh); outline(a, C_BORDER, bx, by, bw, bh); - char pct[16]; snprintf(pct, sizeof(pct), "%d%%", a->progress); - text(a, a->font_ui, C_TEXT, bx + bw + 6, - baseline(a->font_ui, by, bh), pct); + txt(a, a->font_ui, C_TEXT, bx+bw+6, bl(a->font_ui, by, bh), pct); - /* log panel */ int ly = by + bh + 10; - fill(a, C_PANEL, PAD, ly, WIN_W - PAD*2, BODY_H - (ly - BODY_Y) - 4); - outline(a, C_BORDER, PAD, ly, WIN_W - PAD*2, BODY_H - (ly - BODY_Y) - 4); + fill(a, C_PANEL, PAD, ly, WIN_W-PAD*2, BODY_H-(ly-BODY_Y)-4); + outline(a, C_BORDER, PAD, ly, WIN_W-PAD*2, BODY_H-(ly-BODY_Y)-4); - int start = (a->log.count > LOG_ROWS_VIS) - ? (a->log.head - LOG_ROWS_VIS + MAX_LOG) % MAX_LOG + int log_rows = (BODY_H-(ly-BODY_Y)-8) / LOG_ROW_H; + int start = (a->log.count > log_rows) + ? (a->log.head - log_rows + MAX_LOG) % MAX_LOG : (a->log.head - a->log.count + MAX_LOG) % MAX_LOG; - int show = a->log.count < LOG_ROWS_VIS ? a->log.count : LOG_ROWS_VIS; + int show = a->log.count < log_rows ? a->log.count : log_rows; for (int i = 0; i < show; i++) { int idx = (start + i) % MAX_LOG; const char *line = a->log.lines[idx]; - int logy = ly + 4 + i * LOG_ROW_H; - + int logy = ly + 4 + i*LOG_ROW_H; unsigned long c = C_MONO; - if (strstr(line, "error:") || strstr(line, "Error")) c = C_TEXT_ERR; - else if (strstr(line, "warning:")) c = C_TEXT_WARN; - else if (line[0] == '(') c = C_TEXT_OK; + if (strstr(line,"error:") || strstr(line,"Error")) c = C_TEXT_ERR; + else if (strstr(line,"warning:")) c = C_TEXT_WARN; + else if (line[0]=='(') c = C_TEXT_OK; XRectangle clip = { (short)(PAD+2), (short)logy, - (unsigned short)(WIN_W - PAD*2 - 4), + (unsigned short)(WIN_W-PAD*2-4), (unsigned short)LOG_ROW_H }; XftDraw *xd = XftDrawCreate(a->dpy, a->win, a->visual, a->cmap); XftDrawSetClipRectangles(xd, 0, 0, &clip, 1); XftColor col; xftcol(a, c, &col); - XftDrawStringUtf8(xd, &col, a->font_mono, - PAD + 4, logy + LOG_ROW_H - 3, + XftDrawStringUtf8(xd, &col, a->font_mono, PAD+4, logy+LOG_ROW_H-3, (const FcChar8 *)line, strlen(line)); XftColorFree(a->dpy, a->visual, a->cmap, &col); XftDrawSetClip(xd, None); @@ -353,245 +442,358 @@ static void draw_log(Pkg_App *a) { } } -/* ── simple status screens ───────────────────────────────────────────── */ -static void draw_centered(Pkg_App *a, unsigned long col, - const char *line1, const char *line2) { +/* ── spinner ─────────────────────────────────────────────────────────── */ +static const char *SPIN[] = {"⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"}; +#define NSPIN 10 + +static void draw_spinner(App *a, const char *msg) { fill(a, C_BODY, 0, BODY_Y, WIN_W, BODY_H); - int cy = BODY_Y + BODY_H / 2 - 20; - text(a, a->font_hdr, col, - (WIN_W - textw(a, a->font_hdr, line1)) / 2, cy, line1); - if (line2 && line2[0]) { - cy += 28; - text(a, a->font_ui, C_TEXT_DIM, - (WIN_W - textw(a, a->font_ui, line2)) / 2, cy, line2); - } + char buf[128]; + snprintf(buf, sizeof(buf), "%s %s", SPIN[a->spin_frame%NSPIN], msg); + txt(a, a->font_hdr, C_TEXT_DIM, + (WIN_W - txtw(a, a->font_hdr, buf))/2, + BODY_Y + BODY_H/2 - 8, buf); } /* ── full redraw ─────────────────────────────────────────────────────── */ -static void draw(Pkg_App *a) { +static void draw(App *a) { fill(a, C_BG, 0, 0, WIN_W, WIN_H); draw_header(a); + draw_tabs(a); - switch (a->state) { - case ST_CHECKING: { - fill(a, C_BODY, 0, BODY_Y, WIN_W, BODY_H); - char buf[32]; - snprintf(buf, sizeof(buf), "%s Checking for updates…", - SPIN[a->spin_frame % NSPIN]); - text(a, a->font_hdr, C_TEXT_DIM, - (WIN_W - textw(a, a->font_hdr, buf)) / 2, - BODY_Y + BODY_H/2 - 8, buf); - break; + if (a->op == OP_RUNNING) { + draw_log(a); + } else if (a->op == OP_SEARCHING || a->op == OP_LOADING || a->op == OP_CHECKING) { + const char *msg = (a->op == OP_SEARCHING) ? "Searching packages…" : + (a->op == OP_LOADING) ? "Loading installed packages…" : + "Checking for updates…"; + draw_spinner(a, msg); + } else { + fill(a, C_BODY, 0, BODY_Y, WIN_W, BODY_H); + if (a->tab == TAB_SEARCH) draw_search_input(a); + if (a->npkgs > 0) draw_list(a); + else if (a->tab == TAB_SEARCH) { + txt(a, a->font_ui, C_TEXT_DIM, + (WIN_W - txtw(a, a->font_ui, "Type a package name and press Search"))/2, + BODY_Y + BODY_H/2, "Type a package name and press Search"); + } else if (a->op == OP_DONE || a->op == OP_ERROR) { + unsigned long c = (a->op == OP_DONE) ? C_TEXT_OK : C_TEXT_ERR; + txt(a, a->font_hdr, c, + (WIN_W - txtw(a, a->font_hdr, a->status))/2, + BODY_Y + BODY_H/2 - 10, a->status); + } else { + txt(a, a->font_ui, C_TEXT_DIM, + (WIN_W - txtw(a, a->font_ui, "No packages found"))/2, + BODY_Y + BODY_H/2, "No packages found"); } - case ST_UPTODATE: - draw_centered(a, C_TEXT_OK, - "✓ System is up to date", - "No packages require updating."); - break; - case ST_UPDATES: - draw_updates(a); - break; - case ST_UPDATING: - draw_log(a); - break; - case ST_DONE: - draw_centered(a, C_TEXT_OK, - "✓ Update complete", - "All packages have been updated successfully."); - break; - case ST_ERROR: - draw_centered(a, C_TEXT_ERR, - "✗ Update failed", - a->status_msg[0] ? a->status_msg - : "Check the log for details."); - break; } draw_footer(a); XFlush(a->dpy); } -/* ── checkupdates ────────────────────────────────────────────────────── */ -static void run_check(Pkg_App *a) { - a->state = ST_CHECKING; - a->npkgs = 0; - a->scroll = 0; - draw(a); - - FILE *f = popen("checkupdates 2>/dev/null || true", "r"); - if (!f) { - a->state = ST_ERROR; - snprintf(a->status_msg, sizeof(a->status_msg), - "checkupdates not found — install pacman-contrib"); - draw(a); return; - } - - char line[256]; - while (fgets(line, sizeof(line), f) && a->npkgs < MAX_PKGS) { - /* format: name old_ver -> new_ver */ - char name[48], old[24], arrow[4], newv[24]; - if (sscanf(line, "%47s %23s %3s %23s", name, old, arrow, newv) == 4) { - strncpy(a->pkgs[a->npkgs].name, name, 47); - strncpy(a->pkgs[a->npkgs].old_ver, old, 23); - strncpy(a->pkgs[a->npkgs].new_ver, newv, 23); - a->npkgs++; - } - } - pclose(f); - - a->state = (a->npkgs > 0) ? ST_UPDATES : ST_UPTODATE; - draw(a); -} - -/* ── update ──────────────────────────────────────────────────────────── */ -static void start_update(Pkg_App *a) { +/* ── async subprocess ────────────────────────────────────────────────── */ +static int spawn_pipe(const char **argv) { int pipefd[2]; - if (pipe(pipefd) < 0) { - a->state = ST_ERROR; - snprintf(a->status_msg, sizeof(a->status_msg), - "pipe() failed: %s", strerror(errno)); - draw(a); return; - } - + if (pipe(pipefd) < 0) return -1; pid_t pid = fork(); - if (pid < 0) { - a->state = ST_ERROR; - snprintf(a->status_msg, sizeof(a->status_msg), - "fork() failed: %s", strerror(errno)); - close(pipefd[0]); close(pipefd[1]); - draw(a); return; - } - + if (pid < 0) { close(pipefd[0]); close(pipefd[1]); return -1; } if (pid == 0) { close(pipefd[0]); dup2(pipefd[1], STDOUT_FILENO); dup2(pipefd[1], STDERR_FILENO); close(pipefd[1]); - execlp("pkexec", "pkexec", "pacman", "-Syu", "--noconfirm", NULL); - fprintf(stderr, "exec failed: %s\n", strerror(errno)); + execvp(argv[0], (char *const *)argv); _exit(1); } - close(pipefd[1]); - a->update_fd = pipefd[0]; - fcntl(a->update_fd, F_SETFL, O_NONBLOCK); - a->state = ST_UPDATING; - a->progress = 0; - a->log.head = a->log.count = 0; + fcntl(pipefd[0], F_SETFL, O_NONBLOCK); + return pipefd[0]; +} + +/* ── search (pacman -Ss) ─────────────────────────────────────────────── */ +static void do_search(App *a) { + if (!a->search_input.len) return; + a->op = OP_SEARCHING; + a->npkgs = 0; + a->scroll = 0; + a->selected = -1; + draw(a); + + /* run synchronously (fast enough) */ + char cmd[320]; + snprintf(cmd, sizeof(cmd), "pacman -Ss '%s' 2>/dev/null", a->search_input.buf); + FILE *f = popen(cmd, "r"); + if (!f) { a->op = OP_ERROR; snprintf(a->status,sizeof(a->status),"popen failed"); draw(a); return; } + + char line[512], desc[128], name[64], ver[32]; + int pending = 0; + while (fgets(line, sizeof(line), f) && a->npkgs < MAX_PKGS) { + /* pacman -Ss format: + * repo/name ver [flags] + * description + */ + if (line[0] == ' ' || line[0] == '\t') { + /* description line */ + if (pending) { + char *d = line; while (*d==' '||*d=='\t') d++; + d[strcspn(d,"\n")] = '\0'; + strncpy(a->pkgs[a->npkgs].desc, d, 127); + a->npkgs++; + pending = 0; + } + } else { + /* name line */ + char repo[32]; int inst = 0; + if (sscanf(line, "%31[^/]/%63s %31s", repo, name, ver) >= 2) { + /* check if [installed] appears anywhere on the line */ + inst = (strstr(line, "[installed]") != NULL); + strncpy(a->pkgs[a->npkgs].name, name, 63); + strncpy(a->pkgs[a->npkgs].ver, ver, 31); + a->pkgs[a->npkgs].desc[0] = '\0'; + a->pkgs[a->npkgs].installed = inst; + pending = 1; + } + } + } + /* flush last pending if no trailing desc line */ + (void)desc; + pclose(f); + a->op = OP_IDLE; draw(a); } -static void read_update_output(Pkg_App *a) { +/* ── installed list (pacman -Q) ──────────────────────────────────────── */ +static void load_installed(App *a) { + a->op = OP_LOADING; + a->npkgs = 0; + a->scroll = 0; + a->selected = -1; + draw(a); + + FILE *f = popen("pacman -Q 2>/dev/null", "r"); + if (!f) { a->op = OP_ERROR; snprintf(a->status,sizeof(a->status),"pacman -Q failed"); draw(a); return; } + + char line[256]; + while (fgets(line, sizeof(line), f) && a->npkgs < MAX_PKGS) { + char n[64], v[32]; + if (sscanf(line, "%63s %31s", n, v) == 2) { + strncpy(a->pkgs[a->npkgs].name, n, 63); + strncpy(a->pkgs[a->npkgs].ver, v, 31); + a->pkgs[a->npkgs].desc[0] = '\0'; + a->pkgs[a->npkgs].installed = 1; + a->npkgs++; + } + } + pclose(f); + a->op = OP_IDLE; + draw(a); +} + +/* ── update check (checkupdates) ─────────────────────────────────────── */ +static void check_updates(App *a) { + a->op = OP_CHECKING; + a->npkgs = 0; + a->scroll = 0; + a->selected = -1; + draw(a); + + FILE *f = popen("checkupdates 2>/dev/null || true", "r"); + if (!f) { a->op = OP_ERROR; snprintf(a->status,sizeof(a->status),"checkupdates not found — install pacman-contrib"); draw(a); return; } + + char line[256]; + while (fgets(line, sizeof(line), f) && a->npkgs < MAX_PKGS) { + char n[64], old[32], arrow[4], newv[32]; + if (sscanf(line, "%63s %31s %3s %31s", n, old, arrow, newv) == 4) { + strncpy(a->pkgs[a->npkgs].name, n, 63); + strncpy(a->pkgs[a->npkgs].ver, old, 31); + strncpy(a->pkgs[a->npkgs].desc, newv, 127); + a->npkgs++; + } + } + pclose(f); + a->op = OP_IDLE; + if (a->npkgs == 0) snprintf(a->status, sizeof(a->status), "System is up to date"); + draw(a); +} + +/* ── start async operation ────────────────────────────────────────────── */ +static void run_op(App *a, const char **argv, const char *desc) { + a->op_fd = spawn_pipe(argv); + a->op = OP_RUNNING; + a->progress = 0; + a->log.head = a->log.count = 0; + snprintf(a->status, sizeof(a->status), "%s", desc); + draw(a); +} + +static void do_install(App *a) { + if (a->selected < 0 || a->selected >= a->npkgs) return; + snprintf(a->op_pkg, sizeof(a->op_pkg), "%s", a->pkgs[a->selected].name); + const char *argv[] = { "pkexec", "pacman", "-S", "--noconfirm", a->op_pkg, NULL }; + char msg[128]; snprintf(msg, sizeof(msg), "Installing %s…", a->op_pkg); + run_op(a, argv, msg); +} + +static void do_remove(App *a) { + if (a->selected < 0 || a->selected >= a->npkgs) return; + snprintf(a->op_pkg, sizeof(a->op_pkg), "%s", a->pkgs[a->selected].name); + const char *argv[] = { "pkexec", "pacman", "-R", "--noconfirm", a->op_pkg, NULL }; + char msg[128]; snprintf(msg, sizeof(msg), "Removing %s…", a->op_pkg); + run_op(a, argv, msg); +} + +static void do_upgrade(App *a) { + const char *argv[] = { "pkexec", "pacman", "-Syu", "--noconfirm", NULL }; + run_op(a, argv, "Upgrading system…"); +} + +/* ── read async output ───────────────────────────────────────────────── */ +static void read_op_output(App *a) { char buf[4096]; - ssize_t n = read(a->update_fd, buf, sizeof(buf)-1); + ssize_t n = read(a->op_fd, buf, sizeof(buf)-1); if (n <= 0) { - close(a->update_fd); - a->update_fd = -1; - /* check exit */ - int status = 0; - waitpid(-1, &status, WNOHANG); - int ok = (WIFEXITED(status) && WEXITSTATUS(status) == 0); - a->state = ok ? ST_DONE : ST_ERROR; + close(a->op_fd); a->op_fd = -1; + int status = 0; waitpid(-1, &status, WNOHANG); + int ok = WIFEXITED(status) && WEXITSTATUS(status) == 0; + a->op = ok ? OP_DONE : OP_ERROR; a->progress = ok ? 100 : a->progress; - if (!ok) snprintf(a->status_msg, sizeof(a->status_msg), - "pacman exited with code %d", WEXITSTATUS(status)); + if (ok) { + char done[128]; + snprintf(done, sizeof(done), "%s done", a->op_pkg[0] ? a->op_pkg : "Operation"); + snprintf(a->status, sizeof(a->status), "✓ %s", done); + } else { + snprintf(a->status, sizeof(a->status), "✗ Failed (exit %d)", WEXITSTATUS(status)); + } draw(a); return; } - buf[n] = '\0'; - /* split on newlines, push to log */ static char carry[256]; static int clen = 0; for (int i = 0; i < (int)n; i++) { if (buf[i] == '\n' || buf[i] == '\r') { carry[clen] = '\0'; if (clen > 0) { log_push(&a->log, carry); - /* estimate progress from ":: Downloading" lines */ - if (strncmp(carry, "(", 1) == 0) { + if (carry[0]=='(') { int cur, tot; - if (sscanf(carry, "(%d/%d)", &cur, &tot) == 2 && tot > 0) - a->progress = 10 + cur * 80 / tot; + if (sscanf(carry,"(%d/%d)",&cur,&tot)==2 && tot>0) + a->progress = 10 + cur*80/tot; } - if (strstr(carry, "Running post-transaction")) a->progress = 90; - if (strstr(carry, "Upgrade complete")) a->progress = 100; + if (strstr(carry,"Running post")) a->progress = 90; } clen = 0; - } else if (clen < (int)sizeof(carry)-2) { - carry[clen++] = buf[i]; - } + } else if (clen < (int)sizeof(carry)-2) carry[clen++] = buf[i]; } draw(a); } -/* ── hover / click ───────────────────────────────────────────────────── */ -static int btn_at(Pkg_App *a, int x, int y) { +/* ── hit testing ─────────────────────────────────────────────────────── */ +static int tab_at(int x, int y) { + if (y < HDR_H || y >= HDR_H+TAB_H) return -1; + int tw = WIN_W / NTABS; + int t = x / tw; + return (t >= 0 && t < NTABS) ? t : -1; +} + +static int row_at(App *a, int x, int y) { + (void)x; + int ly = list_top(a) + ROW_H; /* skip header row */ + if (y < ly) return -1; + int row = (y - ly) / ROW_H + a->scroll; + return (row >= 0 && row < a->npkgs) ? row : -1; +} + +static int btn_at(App *a, int x, int y) { int fy = WIN_H - FOOT_H; - int by = fy + (FOOT_H - BTN_H) / 2; + int by = fy + (FOOT_H - BTN_H)/2; + if (in_rect(x, y, WIN_W-BTN_W-PAD, by, BTN_W, BTN_H)) return 3; - /* close */ - int cx = WIN_W - BTN_W - PAD; - if (in_rect(x, y, cx, by, BTN_W, BTN_H)) return 3; + if (a->op == OP_RUNNING) + return in_rect(x, y, PAD, by, BTN_W, BTN_H) ? 2 : 0; - if (a->state == ST_UPDATES) { - /* update all */ - int ux = WIN_W - BTN_W*2 - PAD - 8; - if (in_rect(x, y, ux, by, BTN_W, BTN_H)) return 1; - /* refresh */ - if (in_rect(x, y, PAD, by, BTN_W-20, BTN_H)) return 2; - } else if (a->state == ST_UPTODATE || a->state == ST_ERROR) { - if (in_rect(x, y, PAD, by, BTN_W-20, BTN_H)) return 2; + if (a->tab == TAB_SEARCH) { + if (in_rect(x, y, PAD, by, BTN_W, BTN_H)) return 2; + if (a->selected>=0 && in_rect(x, y, WIN_W-BTN_W*2-PAD-8, by, BTN_W, BTN_H)) return 1; + } else if (a->tab == TAB_INSTALLED) { + if (in_rect(x, y, PAD, by, BTN_W, BTN_H)) return 2; + if (a->selected>=0 && in_rect(x, y, WIN_W-BTN_W*2-PAD-8, by, BTN_W, BTN_H)) return 1; + } else { + if (in_rect(x, y, PAD, by, BTN_W, BTN_H)) return 2; + if (a->npkgs>0 && in_rect(x, y, WIN_W-BTN_W*2-PAD-8, by, BTN_W, BTN_H)) return 1; } return 0; } -static void on_click(Pkg_App *a, int x, int y) { +static void switch_tab(App *a, Tab t) { + if (a->op == OP_RUNNING) return; + a->tab = t; + a->npkgs = 0; a->scroll = 0; a->selected = -1; + a->op = OP_IDLE; a->status[0] = '\0'; + if (t == TAB_INSTALLED) load_installed(a); + else if (t == TAB_UPDATES) check_updates(a); + else draw(a); +} + +static void on_click(App *a, int x, int y) { + /* tab click */ + int t = tab_at(x, y); + if (t >= 0) { switch_tab(a, (Tab)t); return; } + + /* search input click */ + if (a->tab == TAB_SEARCH && + in_rect(x, y, INPUT_X, INPUT_Y, INPUT_W, INPUT_H)) { + a->search_input.focused = 1; + draw(a); return; + } else { + a->search_input.focused = 0; + } + + /* button click */ int b = btn_at(a, x, y); if (b == 3) { XDestroyWindow(a->dpy, a->win); return; } - if (b == 2) { run_check(a); return; } - if (b == 1 && a->state == ST_UPDATES) { start_update(a); return; } - - /* package list click */ - if (a->state == ST_UPDATES) { - int list_y = BODY_Y + ROW_H; - int vis = LIST_ROWS_VIS - 1; - if (in_rect(x, y, 0, list_y, WIN_W, vis * ROW_H)) { - int row = (y - list_y) / ROW_H + a->scroll; - if (row >= 0 && row < a->npkgs) { /* selection, no action yet */ } + if (b == 2) { + if (a->op == OP_RUNNING && a->op_fd >= 0) { + close(a->op_fd); a->op_fd = -1; + a->op = OP_IDLE; draw(a); return; } + if (a->tab == TAB_SEARCH) { do_search(a); return; } + if (a->tab == TAB_INSTALLED) { load_installed(a); return; } + check_updates(a); return; + } + if (b == 1) { + if (a->tab == TAB_SEARCH) { do_install(a); return; } + if (a->tab == TAB_INSTALLED) { do_remove(a); return; } + if (a->tab == TAB_UPDATES) { do_upgrade(a); return; } + } + + /* row click */ + if (a->op == OP_IDLE && a->npkgs > 0) { + int r = row_at(a, x, y); + if (r >= 0) { a->selected = (a->selected == r) ? -1 : r; draw(a); return; } } } -static void on_motion(Pkg_App *a, int x, int y) { +static void on_motion(App *a, int x, int y) { int nb = btn_at(a, x, y); - int np = -1; - - if (a->state == ST_UPDATES) { - int list_y = BODY_Y + ROW_H; - int vis = LIST_ROWS_VIS - 1; - if (in_rect(x, y, 0, list_y, WIN_W, vis * ROW_H)) - np = (y - list_y) / ROW_H + a->scroll; - } - - if (nb != a->hover_btn || np != a->hover_pkg) { - a->hover_btn = nb; - a->hover_pkg = np; - draw(a); + int nr = (a->op == OP_IDLE && a->npkgs > 0) ? row_at(a, x, y) : -1; + if (nb != a->hover_btn || nr != a->hover_row) { + a->hover_btn = nb; a->hover_row = nr; draw(a); } } -static void on_scroll(Pkg_App *a, int dir) { - if (a->state != ST_UPDATES) return; - int vis = LIST_ROWS_VIS - 1; - int max_scroll = a->npkgs - vis; +static void on_scroll(App *a, int dir) { + if (a->op != OP_IDLE || a->npkgs == 0) return; + int vis = list_rows_vis(a) - 1; a->scroll += dir; - if (a->scroll < 0) a->scroll = 0; - if (a->scroll > max_scroll) a->scroll = max_scroll > 0 ? max_scroll : 0; + if (a->scroll < 0) a->scroll = 0; + int mx = a->npkgs - vis; + if (a->scroll > mx) a->scroll = mx > 0 ? mx : 0; draw(a); } -/* ── init ────────────────────────────────────────────────────────────── */ -static void init(Pkg_App *a) { - a->dpy = XOpenDisplay(NULL); +/* ── init ─────────────────────────────────────────────────────────────── */ +static void app_init(App *a) { + a->dpy = XOpenDisplay(NULL); if (!a->dpy) { fputs("orionpkg: cannot open display\n", stderr); exit(1); } a->screen = DefaultScreen(a->dpy); a->root = RootWindow(a->dpy, a->screen); @@ -606,111 +808,107 @@ static void init(Pkg_App *a) { fputs("orionpkg: cannot load fonts\n", stderr); exit(1); } - a->state = ST_CHECKING; - a->update_fd = -1; + a->tab = TAB_SEARCH; + a->op = OP_IDLE; + a->op_fd = -1; + a->selected = -1; a->hover_btn = 0; - a->hover_pkg = -1; + a->hover_row = -1; + a->search_input.focused = 1; XSetWindowAttributes swa = { .background_pixel = xcol(a, C_BG), .event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | KeyPressMask | StructureNotifyMask, }; - a->win = XCreateWindow(a->dpy, a->root, - 120, 120, WIN_W, WIN_H, 0, + a->win = XCreateWindow(a->dpy, a->root, 100, 100, WIN_W, WIN_H, 0, a->depth, InputOutput, a->visual, CWBackPixel | CWEventMask, &swa); a->gc = XCreateGC(a->dpy, a->win, 0, NULL); XStoreName(a->dpy, a->win, "OrionOS Package Manager"); XClassHint *ch = XAllocClassHint(); - if (ch) { - ch->res_name = (char *)"orionpkg"; ch->res_class = (char *)"OrionPkg"; - XSetClassHint(a->dpy, a->win, ch); XFree(ch); - } + if (ch) { ch->res_name=(char*)"orionpkg"; ch->res_class=(char*)"OrionPkg"; XSetClassHint(a->dpy,a->win,ch); XFree(ch); } XSizeHints *sh = XAllocSizeHints(); - if (sh) { - sh->flags = PMinSize; - sh->min_width = WIN_W; sh->min_height = 300; - XSetWMNormalHints(a->dpy, a->win, sh); XFree(sh); - } + if (sh) { sh->flags=PMinSize; sh->min_width=560; sh->min_height=360; XSetWMNormalHints(a->dpy,a->win,sh); XFree(sh); } - Atom wm_delete = XInternAtom(a->dpy, "WM_DELETE_WINDOW", False); - Atom wm_protos = XInternAtom(a->dpy, "WM_PROTOCOLS", False); - XChangeProperty(a->dpy, a->win, wm_protos, XA_ATOM, 32, - PropModeReplace, (unsigned char *)&wm_delete, 1); + Atom wd = XInternAtom(a->dpy,"WM_DELETE_WINDOW",False); + Atom wp = XInternAtom(a->dpy,"WM_PROTOCOLS",False); + XChangeProperty(a->dpy, a->win, wp, XA_ATOM, 32, PropModeReplace, (unsigned char*)&wd, 1); XMapWindow(a->dpy, a->win); XFlush(a->dpy); } -/* ── event loop ──────────────────────────────────────────────────────── */ -static void run(Pkg_App *a) { +/* ── event loop ───────────────────────────────────────────────────────── */ +static void app_run(App *a) { Atom wm_delete = XInternAtom(a->dpy, "WM_DELETE_WINDOW", False); - - /* kick off initial check */ - run_check(a); + draw(a); struct pollfd pfds[2]; pfds[0].fd = ConnectionNumber(a->dpy); pfds[0].events = POLLIN; for (;;) { - pfds[1].fd = a->update_fd; - pfds[1].events = (a->update_fd >= 0) ? POLLIN : 0; - int nfds = (a->update_fd >= 0) ? 2 : 1; + pfds[1].fd = a->op_fd; + pfds[1].events = (a->op_fd >= 0) ? POLLIN : 0; + int nfds = (a->op_fd >= 0) ? 2 : 1; - /* 200ms tick for spinner animation */ - int r = poll(pfds, nfds, 200); + poll(pfds, nfds, 200); - if (a->update_fd >= 0 && pfds[1].revents & (POLLIN|POLLHUP)) - read_update_output(a); + if (a->op_fd >= 0 && pfds[1].revents & (POLLIN|POLLHUP)) + read_op_output(a); - /* spinner animation when checking */ - if (a->state == ST_CHECKING || a->state == ST_UPDATING) { + /* spinner animation */ + if (a->op == OP_SEARCHING || a->op == OP_LOADING || + a->op == OP_CHECKING || a->op == OP_RUNNING) { a->spin_frame++; - if (a->state == ST_CHECKING) draw(a); + if (a->op != OP_RUNNING) draw(a); } - if (r <= 0 && a->update_fd < 0) { /* idle */ } - while (XPending(a->dpy)) { XEvent ev; XNextEvent(a->dpy, &ev); switch (ev.type) { - case Expose: - if (ev.xexpose.count == 0) draw(a); - break; - case ButtonPress: - if (ev.xbutton.button == Button1) - on_click(a, ev.xbutton.x, ev.xbutton.y); - else if (ev.xbutton.button == Button4) on_scroll(a, -3); - else if (ev.xbutton.button == Button5) on_scroll(a, 3); - break; - case MotionNotify: - on_motion(a, ev.xmotion.x, ev.xmotion.y); - break; - case KeyPress: { - KeySym sym = XLookupKeysym(&ev.xkey, 0); - if (sym == XK_Escape || sym == XK_q) return; - if (sym == XK_r) run_check(a); - if (sym == XK_Up) on_scroll(a, -3); - if (sym == XK_Down) on_scroll(a, 3); - break; + case Expose: + if (ev.xexpose.count == 0) draw(a); + break; + case ButtonPress: + if (ev.xbutton.button == Button1) + on_click(a, ev.xbutton.x, ev.xbutton.y); + else if (ev.xbutton.button == Button4) on_scroll(a, -3); + else if (ev.xbutton.button == Button5) on_scroll(a, 3); + break; + case MotionNotify: + on_motion(a, ev.xmotion.x, ev.xmotion.y); + break; + case KeyPress: { + char ch[8] = {0}; + KeySym sym; + XLookupString(&ev.xkey, ch, sizeof(ch)-1, &sym, NULL); + if (sym == XK_Escape || sym == XK_q) return; + if (sym == XK_Return && a->tab == TAB_SEARCH) { do_search(a); break; } + if (a->search_input.focused && a->tab == TAB_SEARCH) { + if (sym == XK_BackSpace) { input_backspace(&a->search_input); draw(a); } + else if (ch[0] >= 0x20 && ch[0] < 0x7F) { input_insert(&a->search_input, ch[0]); draw(a); } } - case ClientMessage: - if ((Atom)ev.xclient.data.l[0] == wm_delete) return; - break; - case DestroyNotify: return; + if (sym == XK_Up) on_scroll(a, -1); + if (sym == XK_Down) on_scroll(a, 1); + break; + } + case ClientMessage: + if ((Atom)ev.xclient.data.l[0] == wm_delete) return; + break; + case DestroyNotify: return; } } } } int main(void) { - Pkg_App a = {0}; - init(&a); - run(&a); + App a = {0}; + app_init(&a); + app_run(&a); return 0; }