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>
717 lines
26 KiB
C
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;
|
|
}
|