Files
OrionOS/orionpkg/orionpkg.c
T
Claude 454f925891 Rewrite orionpkg into full GUI package manager
Previously only an update manager. Now has three tabs:
- Search: pacman -Ss query, shows results with installed badge, Install button
- Installed: pacman -Q list with scrollable rows, Remove button
- Updates: checkupdates list, Update All runs pkexec pacman -Syu

Features:
- Editable search input field with cursor, Enter to search
- Row selection with highlight, double-click toggles
- Scrollable package lists with scrollbar
- Live log view + progress bar during install/remove/upgrade
- Spinner animation during async operations
- Cancel button during running operations
- Error/success status display
- Same dark-navy palette as rest of OrionOS
2026-08-30 20:55:05 +00:00

915 lines
35 KiB
C

/*
* 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 <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/keysym.h>
#include <X11/Xft/Xft.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
/* ── 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<x+w && my>=y && my<y+h;
}
/* ── draw helpers ────────────────────────────────────────────────────── */
static void draw_btn(App *a, int id, int x, int y, int w, int h,
const char *label, unsigned long bg, unsigned long bghov) {
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 - 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;
}