Files
OrionOS/orionpkg/orionpkg.c
T
nmemmertandClaude Sonnet 4.6 0ed61cea4c Add orionpkg (package manager GUI) + installer locale step
orionpkg/orionpkg.c (~500 lines, native Xlib/Xft):
- Launches with checkupdates (pacman-contrib) to list pending upgrades
- ST_CHECKING: animated spinner while querying
- ST_UPDATES: scrollable package list with name / old ver → new ver columns
- ST_UPDATING: pkexec pacman -Syu in background, live log output,
  progress bar estimated from pacman's (N/M) counters
- ST_DONE / ST_ERROR: success or failure screen
- Refresh (r key), scroll (mouse wheel / arrow keys), Close (Esc/q)
- pacman-contrib added to ISO packages

Installer: add STEP_LOCALE (timezone + keyboard layout):
- Inserted between STEP_DISK and STEP_USER
- Timezone pane: searchable filter input + scrollable list of 37 zones
- Keyboard pane: 20 common layouts with code + description
- Timezone and keyboard passed as args 6/7 to orioninstall
- orioninstall: sets /etc/vconsole.conf and X11 xorg.conf.d keyboard
  config from args; uses selected timezone for localtime symlink

Also:
- Right-click menu: "Software Updates" → orionpkg
- Root Makefile: orionpkg target + install rule
- ISO: orionpkg binary installed, permissions map, gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 14:39:00 -04:00

717 lines
26 KiB
C

/* orionpkg.c — 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.
*/
#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 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
/* ── 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_BTN 0x182848
#define C_BTN_HOV 0x1E3860
#define C_BTN_UPD 0x1A4888
#define C_BTN_UPD_H 0x2060AA
#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_BAR_BG 0x141E30
#define C_BAR_FG 0x1E58A8
#define C_MONO 0x7AAAC8
#define FONT_UI "Sans:size=9"
#define FONT_HDR "Sans:size=10:weight=bold"
#define FONT_MONO "DejaVu Sans Mono:size=8"
/* ── states ─────────────────────────────────────────────────────────── */
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;
/* ── package entry ───────────────────────────────────────────────────── */
#define MAX_PKGS 256
typedef struct { char name[48]; char old_ver[24]; char new_ver[24]; } Pkg;
/* ── log buffer ──────────────────────────────────────────────────────── */
#define MAX_LOG 300
#define LOG_COLS 90
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++;
}
/* ── app struct ──────────────────────────────────────────────────────── */
typedef struct {
Display *dpy;
int screen;
Window root, win;
GC gc;
Colormap cmap;
Visual *visual;
int depth;
XftFont *font_ui, *font_hdr, *font_mono;
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 */
LogBuf log;
int update_fd; /* pipe fd for pacman output */
int progress; /* 0-100 estimated */
int spin_frame; /* animation tick */
char status_msg[256];
} Pkg_App;
/* ── X helpers ───────────────────────────────────────────────────────── */
static unsigned long xcol(Pkg_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(Pkg_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(Pkg_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) {
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) {
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 textw(Pkg_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 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;
}
/* ── button ──────────────────────────────────────────────────────────── */
static void draw_btn(Pkg_App *a, int btn_id, int x, int y, int w, int h,
const char *label, unsigned long bg, unsigned long bghov) {
unsigned long b = (a->hover_btn == 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);
}
/* ── header ──────────────────────────────────────────────────────────── */
static void draw_header(Pkg_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);
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);
}
/* ── footer ──────────────────────────────────────────────────────────── */
static void draw_footer(Pkg_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;
/* 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->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);
}
}
/* ── spinning indicator ──────────────────────────────────────────────── */
static const char *SPIN[] = { "⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏" };
#define NSPIN 10
/* ── 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_updates(Pkg_App *a) {
fill(a, C_BODY, 0, BODY_Y, WIN_W, BODY_H);
draw_list_header(a);
int list_y = BODY_Y + ROW_H;
int vis = LIST_ROWS_VIS - 1;
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);
if (is_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 = 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);
/* arrow */
text(a, a->font_ui, C_TEXT_DIM, ARROW_X - 16, ty, "→");
text(a, a->font_ui, C_TEXT_OK, NEWVER_X, ty, a->pkgs[i].new_ver);
}
/* scrollbar */
if (a->npkgs > vis) {
int sb_x = WIN_W - 6;
int sb_h = BODY_H - ROW_H;
int th_h = sb_h * vis / 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);
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) {
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);
/* 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);
int start = (a->log.count > LOG_ROWS_VIS)
? (a->log.head - LOG_ROWS_VIS + 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;
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);
}
}
/* ── simple status screens ───────────────────────────────────────────── */
static void draw_centered(Pkg_App *a, unsigned long col,
const char *line1, const char *line2) {
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);
}
}
/* ── full redraw ─────────────────────────────────────────────────────── */
static void draw(Pkg_App *a) {
fill(a, C_BG, 0, 0, WIN_W, WIN_H);
draw_header(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;
}
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) {
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;
}
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]);
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));
_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;
draw(a);
}
static void read_update_output(Pkg_App *a) {
char buf[4096];
ssize_t n = read(a->update_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;
a->progress = ok ? 100 : a->progress;
if (!ok) snprintf(a->status_msg, sizeof(a->status_msg),
"pacman exited with code %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) {
int cur, 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;
}
clen = 0;
} 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) {
int fy = WIN_H - FOOT_H;
int by = fy + (FOOT_H - BTN_H) / 2;
/* close */
int cx = WIN_W - BTN_W - PAD;
if (in_rect(x, y, cx, by, BTN_W, BTN_H)) return 3;
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;
}
return 0;
}
static void on_click(Pkg_App *a, int x, int y) {
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 */ }
}
}
}
static void on_motion(Pkg_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);
}
}
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;
a->scroll += dir;
if (a->scroll < 0) a->scroll = 0;
if (a->scroll > max_scroll) a->scroll = max_scroll > 0 ? max_scroll : 0;
draw(a);
}
/* ── init ────────────────────────────────────────────────────────────── */
static void init(Pkg_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->state = ST_CHECKING;
a->update_fd = -1;
a->hover_btn = 0;
a->hover_pkg = -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->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 = WIN_W; sh->min_height = 300;
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);
XMapWindow(a->dpy, a->win);
XFlush(a->dpy);
}
/* ── event loop ──────────────────────────────────────────────────────── */
static void run(Pkg_App *a) {
Atom wm_delete = XInternAtom(a->dpy, "WM_DELETE_WINDOW", False);
/* kick off initial check */
run_check(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;
/* 200ms tick for spinner animation */
int r = poll(pfds, nfds, 200);
if (a->update_fd >= 0 && pfds[1].revents & (POLLIN|POLLHUP))
read_update_output(a);
/* spinner animation when checking */
if (a->state == ST_CHECKING || a->state == ST_UPDATING) {
a->spin_frame++;
if (a->state == ST_CHECKING) 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 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);
return 0;
}