/* * orionpkg — OrionOS Package Manager * * 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 #include #include #include #include #include #include #include #include #include #include #include /* ── 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 #define C_HDR 0x090C18 #define C_BODY 0x0D1624 #define C_FOOT 0x090C18 #define C_PANEL 0x111828 #define C_BORDER 0x1A2840 #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_ACT 0x1E58A8 #define C_BTN_ACT_H 0x2A70CC #define C_BTN_DNG 0x5A1414 #define C_BTN_DNG_H 0x882020 #define C_TEXT 0xA8C4DC #define C_TEXT_DIM 0x3E5870 #define C_TEXT_HOV 0xE0EEFF #define C_TEXT_HDR 0xE8F2FF #define C_TEXT_PKG 0xC0D8F0 #define C_TEXT_VER 0x4A7090 #define C_TEXT_OK 0x44BB77 #define C_TEXT_ERR 0xFF5555 #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" /* ── 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 { 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 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 96 typedef struct { char lines[MAX_LOG][LOG_COLS+1]; int head, count; } LogBuf; static void log_push(LogBuf *lb, const char *line) { snprintf(lb->lines[lb->head], LOG_COLS, "%s", line); lb->head = (lb->head + 1) % MAX_LOG; if (lb->count < MAX_LOG) lb->count++; } /* ── text input ──────────────────────────────────────────────────────── */ typedef struct { char buf[256]; int len; int focused; } TextInput; 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'; } /* ── app ─────────────────────────────────────────────────────────────── */ typedef struct { Display *dpy; int screen; Window root, win; GC gc; Colormap cmap; Visual *visual; int depth; XftFont *font_ui, *font_hdr, *font_mono; 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(App *a, unsigned long rgb) { XColor c = { .red = ((rgb>>16)&0xFF)*257, .green = ((rgb>> 8)&0xFF)*257, .blue = ( rgb &0xFF)*257, .flags = DoRed|DoGreen|DoBlue }; XAllocColor(a->dpy, a->cmap, &c); return c.pixel; } static void xftcol(App *a, unsigned long rgb, XftColor *out) { XRenderColor rc = { .red = ((rgb>>16)&0xFF)*257, .green = ((rgb>> 8)&0xFF)*257, .blue = ( rgb &0xFF)*257, .alpha = 0xFFFF }; XftColorAllocValue(a->dpy, a->visual, a->cmap, &rc, out); } 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(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 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); XftDrawStringUtf8(xd, &c, f, x, y, (const FcChar8 *)s, strlen(s)); XftColorFree(a->dpy, a->visual, a->cmap, &c); XftDrawDestroy(xd); } 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 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 == id) ? bghov : bg; fill(a, b, x, y, w, h); outline(a, C_BORDER, x, y, w, h); 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); } 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); 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"); 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); } 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; 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->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); } } /* ── 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 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); } } /* ── package list ────────────────────────────────────────────────────── */ #define LIST_Y_SEARCH (INPUT_Y + INPUT_H + 6) #define LIST_Y_PLAIN (BODY_Y + 2) 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; } #define DESC_X 200 static void draw_list(App *a) { int ly = list_top(a); int vis = list_rows_vis(a); /* 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) fill(a, C_ROW_ALT, 0, ry, WIN_W, ROW_H); fill(a, C_SEP, 0, ry+ROW_H-1, WIN_W, 1); 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); /* 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); /* 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 */ int vis2 = vis - 1; if (a->npkgs > vis2) { int sb_x = WIN_W - 6; int sb_h = (vis2) * ROW_H; int th_h = sb_h * vis2 / a->npkgs; if (th_h < 20) th_h = 20; 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 ────────────────────────────────────────────────────────── */ #define LOG_ROW_H 16 static void draw_log(App *a) { fill(a, C_BODY, 0, BODY_Y, WIN_W, BODY_H); 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); txt(a, a->font_ui, C_TEXT, bx+bw+6, bl(a->font_ui, by, bh), pct); 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); 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 ? 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; 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; XRectangle clip = { (short)(PAD+2), (short)logy, (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, (const FcChar8 *)line, strlen(line)); XftColorFree(a->dpy, a->visual, a->cmap, &col); XftDrawSetClip(xd, None); XftDrawDestroy(xd); } } /* ── 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); 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(App *a) { fill(a, C_BG, 0, 0, WIN_W, WIN_H); draw_header(a); draw_tabs(a); 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"); } } draw_footer(a); XFlush(a->dpy); } /* ── async subprocess ────────────────────────────────────────────────── */ static int spawn_pipe(const char **argv) { int pipefd[2]; if (pipe(pipefd) < 0) return -1; pid_t pid = fork(); 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]); execvp(argv[0], (char *const *)argv); _exit(1); } close(pipefd[1]); 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); } /* ── 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->op_fd, buf, sizeof(buf)-1); if (n <= 0) { 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) { 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'; 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); if (carry[0]=='(') { int cur, tot; if (sscanf(carry,"(%d/%d)",&cur,&tot)==2 && tot>0) a->progress = 10 + cur*80/tot; } if (strstr(carry,"Running post")) a->progress = 90; } clen = 0; } else if (clen < (int)sizeof(carry)-2) carry[clen++] = buf[i]; } draw(a); } /* ── 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; if (in_rect(x, y, WIN_W-BTN_W-PAD, 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->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 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) { 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(App *a, int x, int y) { int nb = btn_at(a, x, y); 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(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; int mx = a->npkgs - vis; if (a->scroll > mx) a->scroll = mx > 0 ? mx : 0; draw(a); } /* ── 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); a->cmap = DefaultColormap(a->dpy, a->screen); a->visual = DefaultVisual(a->dpy, a->screen); a->depth = DefaultDepth(a->dpy, a->screen); a->font_ui = XftFontOpenName(a->dpy, a->screen, FONT_UI); a->font_hdr = XftFontOpenName(a->dpy, a->screen, FONT_HDR); a->font_mono = XftFontOpenName(a->dpy, a->screen, FONT_MONO); if (!a->font_ui || !a->font_hdr) { fputs("orionpkg: cannot load fonts\n", stderr); exit(1); } a->tab = TAB_SEARCH; a->op = OP_IDLE; a->op_fd = -1; a->selected = -1; a->hover_btn = 0; 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, 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); } XSizeHints *sh = XAllocSizeHints(); if (sh) { sh->flags=PMinSize; sh->min_width=560; sh->min_height=360; XSetWMNormalHints(a->dpy,a->win,sh); XFree(sh); } 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 app_run(App *a) { Atom wm_delete = XInternAtom(a->dpy, "WM_DELETE_WINDOW", False); draw(a); struct pollfd pfds[2]; pfds[0].fd = ConnectionNumber(a->dpy); pfds[0].events = POLLIN; for (;;) { pfds[1].fd = a->op_fd; pfds[1].events = (a->op_fd >= 0) ? POLLIN : 0; int nfds = (a->op_fd >= 0) ? 2 : 1; poll(pfds, nfds, 200); if (a->op_fd >= 0 && pfds[1].revents & (POLLIN|POLLHUP)) read_op_output(a); /* spinner animation */ if (a->op == OP_SEARCHING || a->op == OP_LOADING || a->op == OP_CHECKING || a->op == OP_RUNNING) { a->spin_frame++; if (a->op != OP_RUNNING) draw(a); } 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: { 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); } } 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) { App a = {0}; app_init(&a); app_run(&a); return 0; }