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
915 lines
35 KiB
C
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;
|
|
}
|