From 0d874617dd4ea0cafce4b751ce5737dc306685c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 19:40:11 +0000 Subject: [PATCH 1/8] Fork ElemetryOS with macOS-style window decorations - Move traffic-light buttons to the LEFT side of titlebars (close/minimize/maximize) in macOS order, using fixed left-pad positions instead of right-edge offsets - Adopt macOS dark-mode color palette: #3C3C3C active titlebar, #2C2C2C inactive, traffic-light colors #FF5F57/#FEBC2E/#28C840, near-black #1C1C1C desktop - Center window title text in the titlebar (macOS style), clamped to avoid overlapping the left button cluster - Thin 1px window borders replacing the original 3px beveled border - macOS blue (#4A90D9) selection highlight in the context menu - Neutral dark context menu (#2C2C2C) with subtle separators and text indent - Update README to document the ElemetryOS macOS fork identity and design changes --- README.md | 16 ++++++++-- wm/include/wm.h | 55 +++++++++++++++++---------------- wm/src/draw.c | 82 +++++++++++++++++++++++-------------------------- wm/src/menu.c | 30 +++++++++--------- 4 files changed, 94 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index d92df6d..b1b84b3 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,22 @@ -# OrionOS +# OrionOS — ElemetryOS macOS Fork -A BeOS-inspired Linux desktop built from scratch in C. Custom window manager, panel, app launcher, settings panel, package manager, and graphical installer — all running on Arch Linux, shipped as a bootable ISO. +A macOS-inspired Linux desktop forked from ElemetryOS, built from scratch in C. Custom window manager, panel, app launcher, settings panel, package manager, and graphical installer — all running on Arch Linux, shipped as a bootable ISO. --- ## What it is -OrionOS is a complete desktop environment built on X11/Xlib with a dark navy aesthetic. It ships as a bootable ISO you can try live or install to disk. Every shell component is written in native C with no toolkit dependency — just Xlib and Xft. +OrionOS is a complete desktop environment built on X11/Xlib with a macOS dark-mode aesthetic. It ships as a bootable ISO you can try live or install to disk. Every shell component is written in native C with no toolkit dependency — just Xlib and Xft. + +### macOS design language + +This fork re-skins the window manager with macOS design conventions: + +- **Traffic-light buttons on the left** — close (red), minimize (yellow), maximize (green), just like macOS +- **macOS dark-mode palette** — near-black desktop, `#3C3C3C` active titlebars, `#2C2C2C` inactive +- **Centered window titles** — title text centered in the titlebar, macOS-style +- **Thin 1 px borders** — minimal window chrome, no thick bevels +- **macOS blue selection** (`#4A90D9`) in context menus and snap overlays ### Components diff --git a/wm/include/wm.h b/wm/include/wm.h index 9cf7fee..5a1ea43 100644 --- a/wm/include/wm.h +++ b/wm/include/wm.h @@ -12,28 +12,29 @@ #include #include -/* ── palette (modern dark) ───────────────────────────────────────────── */ -#define COLOR_TITLE_ACTIVE 0x1E4D78 /* deep blue active title bar */ -#define COLOR_TITLE_INACTIVE 0x252830 /* dark grey inactive */ -#define COLOR_TITLE_TEXT_ACT 0xE8EEF8 /* near-white active text */ -#define COLOR_TITLE_TEXT_INACT 0x5C6070 /* dimmed inactive text */ -#define COLOR_BORDER_LIGHT 0x3A6898 /* blue-grey highlight */ -#define COLOR_BORDER_DARK 0x0C1018 /* near-black shadow */ -#define COLOR_CLOSE_BTN 0xCC3333 /* red close */ -#define COLOR_MAXIMIZE_BTN 0x2E8B44 /* green maximize */ -#define COLOR_MINIMIZE_BTN 0xBB8820 /* amber minimize */ -#define COLOR_CLOSE_BTN_HOV 0xFF5050 -#define COLOR_MAXIMIZE_BTN_HOV 0x44CC66 -#define COLOR_MINIMIZE_BTN_HOV 0xFFBB30 -#define COLOR_BG 0x1A2840 /* desktop background */ -#define COLOR_SNAP_OVERLAY 0x3A6898 /* snap highlight (unused, placeholder) */ +/* ── palette (macOS dark mode) ───────────────────────────────────────── */ +#define COLOR_TITLE_ACTIVE 0x3C3C3C /* macOS dark titlebar */ +#define COLOR_TITLE_INACTIVE 0x2C2C2C /* inactive, slightly darker */ +#define COLOR_TITLE_TEXT_ACT 0xFFFFFF /* white active text */ +#define COLOR_TITLE_TEXT_INACT 0x686868 /* dimmed inactive text */ +#define COLOR_BORDER_LIGHT 0x505050 /* subtle border highlight */ +#define COLOR_BORDER_DARK 0x1C1C1C /* near-black shadow */ +#define COLOR_CLOSE_BTN 0xFF5F57 /* macOS traffic-light red */ +#define COLOR_MAXIMIZE_BTN 0x28C840 /* macOS traffic-light green */ +#define COLOR_MINIMIZE_BTN 0xFEBC2E /* macOS traffic-light yellow */ +#define COLOR_CLOSE_BTN_HOV 0xFF3B30 +#define COLOR_MAXIMIZE_BTN_HOV 0x34C759 +#define COLOR_MINIMIZE_BTN_HOV 0xFFCC00 +#define COLOR_BG 0x1C1C1C /* near-black desktop */ +#define COLOR_SNAP_OVERLAY 0x4A90D9 /* macOS blue snap highlight */ /* ── geometry ────────────────────────────────────────────────────────── */ -#define TITLE_HEIGHT 28 /* taller for modern look */ +#define TITLE_HEIGHT 28 /* macOS-style height */ #define TITLE_TAB_W 200 -#define BORDER_W 3 -#define BTN_SIZE 14 -#define BTN_MARGIN 5 +#define BORDER_W 1 /* thin 1px macOS-style border */ +#define BTN_SIZE 12 /* macOS traffic-light diameter */ +#define BTN_MARGIN 8 /* spacing between traffic lights */ +#define BTN_LEFT_PAD 10 /* left edge → first button */ #define RESIZE_GRIP 12 #define MIN_WIN_W 120 #define MIN_WIN_H 60 @@ -41,15 +42,15 @@ #define DBL_CLICK_MS 400 /* double-click threshold */ /* - * Three buttons on right side (left to right): minimize, maximize, close - * close = fw - BTN_SIZE - BTN_MARGIN - BORDER_W - * max = close - BTN_SIZE - BTN_MARGIN - 2 - * min = max - BTN_SIZE - BTN_MARGIN - 2 + * macOS-style: three traffic-light buttons on LEFT side + * close = BTN_LEFT_PAD + * minimize = BTN_LEFT_PAD + BTN_SIZE + BTN_MARGIN + * maximize = BTN_LEFT_PAD + (BTN_SIZE + BTN_MARGIN) * 2 */ -#define BTN_CLOSE_X(fw) ((fw) - BTN_SIZE - BTN_MARGIN - BORDER_W) -#define BTN_MAX_X(fw) ((fw) - BTN_SIZE*2 - BTN_MARGIN*2 - BORDER_W - 2) -#define BTN_MIN_X(fw) ((fw) - BTN_SIZE*3 - BTN_MARGIN*3 - BORDER_W - 4) -#define BTN_Y (((TITLE_HEIGHT) - BTN_SIZE) / 2) +#define BTN_CLOSE_X (BTN_LEFT_PAD) +#define BTN_MIN_X (BTN_LEFT_PAD + BTN_SIZE + BTN_MARGIN) +#define BTN_MAX_X (BTN_LEFT_PAD + (BTN_SIZE + BTN_MARGIN) * 2) +#define BTN_Y (((TITLE_HEIGHT) - BTN_SIZE) / 2) /* ── fonts ───────────────────────────────────────────────────────────── */ #define FONT_TITLE "Sans:size=9:weight=bold" diff --git a/wm/src/draw.c b/wm/src/draw.c index 2ac307c..9feb32b 100644 --- a/wm/src/draw.c +++ b/wm/src/draw.c @@ -1,33 +1,30 @@ #include "wm.h" /* - * Frame layout (3 buttons on right): + * Frame layout — macOS style, traffic-light buttons on LEFT: * - * ┌─[Title______________]──────[_][□][X]─┐ <- TITLE_HEIGHT - * │ content │ - * │ [grip]────┘ + * ┌─[●][●][●]──── Title (centered) ────────┐ <- TITLE_HEIGHT + * │ content │ + * │ [grip]──────┘ */ /* ── hit tests ───────────────────────────────────────────────────────── */ int frame_hit_close(Client *c, int x, int y) { - int fw = (int)c->w + BORDER_W * 2; - int bx = BTN_CLOSE_X(fw); - return (x >= bx && x <= bx + BTN_SIZE && + (void)c; + return (x >= BTN_CLOSE_X && x <= BTN_CLOSE_X + BTN_SIZE && y >= BTN_Y && y <= BTN_Y + BTN_SIZE); } int frame_hit_maximize(Client *c, int x, int y) { - int fw = (int)c->w + BORDER_W * 2; - int bx = BTN_MAX_X(fw); - return (x >= bx && x <= bx + BTN_SIZE && + (void)c; + return (x >= BTN_MAX_X && x <= BTN_MAX_X + BTN_SIZE && y >= BTN_Y && y <= BTN_Y + BTN_SIZE); } int frame_hit_minimize(Client *c, int x, int y) { - int fw = (int)c->w + BORDER_W * 2; - int bx = BTN_MIN_X(fw); - return (x >= bx && x <= bx + BTN_SIZE && + (void)c; + return (x >= BTN_MIN_X && x <= BTN_MIN_X + BTN_SIZE && y >= BTN_Y && y <= BTN_Y + BTN_SIZE); } @@ -70,13 +67,14 @@ static void draw_circle_btn(WM *wm, Drawable d, int bx, int by, } void frame_draw_buttons(WM *wm, Client *c) { - int fw = (int)c->w + BORDER_W * 2; - draw_circle_btn(wm, c->frame, BTN_CLOSE_X(fw), BTN_Y, + /* macOS order left-to-right: close, minimize, maximize */ + draw_circle_btn(wm, c->frame, BTN_CLOSE_X, BTN_Y, color_alloc(wm, COLOR_CLOSE_BTN), 'X'); - draw_circle_btn(wm, c->frame, BTN_MAX_X(fw), BTN_Y, - color_alloc(wm, COLOR_MAXIMIZE_BTN), 'O'); - draw_circle_btn(wm, c->frame, BTN_MIN_X(fw), BTN_Y, + draw_circle_btn(wm, c->frame, BTN_MIN_X, BTN_Y, color_alloc(wm, COLOR_MINIMIZE_BTN), '_'); + draw_circle_btn(wm, c->frame, BTN_MAX_X, BTN_Y, + color_alloc(wm, COLOR_MAXIMIZE_BTN), 'O'); + (void)c; } /* ── resize grip ─────────────────────────────────────────────────────── */ @@ -101,7 +99,6 @@ void frame_draw(WM *wm, Client *c) { int fw = (int)c->w + BORDER_W * 2; int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; - unsigned long col_light = color_alloc(wm, COLOR_BORDER_LIGHT); unsigned long col_dark = color_alloc(wm, COLOR_BORDER_DARK); /* title bar background */ @@ -110,45 +107,42 @@ void frame_draw(WM *wm, Client *c) { XSetForeground(wm->dpy, wm->gc, col_title); XFillRectangle(wm->dpy, c->frame, wm->gc, 0, 0, fw, TITLE_HEIGHT); - /* subtle gradient effect: lighter top strip (2px) */ - if (c->focused) { - unsigned long lighter = color_alloc(wm, 0x2A6098); - XSetForeground(wm->dpy, wm->gc, lighter); - XFillRectangle(wm->dpy, c->frame, wm->gc, 0, 0, fw, 2); - } - - /* client area background strip (below title) */ + /* thin borders (1px, macOS style) */ XSetForeground(wm->dpy, wm->gc, col_dark); XFillRectangle(wm->dpy, c->frame, wm->gc, 0, TITLE_HEIGHT, BORDER_W, fh - TITLE_HEIGHT); XFillRectangle(wm->dpy, c->frame, wm->gc, fw - BORDER_W, TITLE_HEIGHT, BORDER_W, fh - TITLE_HEIGHT); XFillRectangle(wm->dpy, c->frame, wm->gc, 0, fh - BORDER_W, fw, BORDER_W); - /* outer bevel */ - XSetForeground(wm->dpy, wm->gc, col_light); - XDrawLine(wm->dpy, c->frame, wm->gc, 0, 0, fw - 1, 0); - XDrawLine(wm->dpy, c->frame, wm->gc, 0, 0, 0, fh - 1); - XSetForeground(wm->dpy, wm->gc, col_dark); - XDrawLine(wm->dpy, c->frame, wm->gc, fw-1, 0, fw-1, fh-1); - XDrawLine(wm->dpy, c->frame, wm->gc, 0, fh-1, fw-1, fh-1); - - /* thin separator line under title */ - XSetForeground(wm->dpy, wm->gc, col_dark); + /* subtle 1px separator under titlebar */ + unsigned long col_sep = color_alloc(wm, 0x1A1A1A); + XSetForeground(wm->dpy, wm->gc, col_sep); XDrawLine(wm->dpy, c->frame, wm->gc, 0, TITLE_HEIGHT - 1, fw - 1, TITLE_HEIGHT - 1); - /* title text */ - int btn_area = BTN_SIZE * 3 + BTN_MARGIN * 3 + BORDER_W + 8; - int text_max_w = fw - BORDER_W * 2 - btn_area - 8; - if (text_max_w < 10) text_max_w = 10; - + /* centered title text (macOS style) */ XftDraw *xd = XftDrawCreate(wm->dpy, c->frame, wm->visual, wm->cmap); if (xd) { XftColor col_text; xft_color(wm, c->focused ? COLOR_TITLE_TEXT_ACT : COLOR_TITLE_TEXT_INACT, &col_text); - XRectangle clip = { BORDER_W + 4, 1, (unsigned short)text_max_w, TITLE_HEIGHT - 2 }; + + XGlyphInfo ext; + XftTextExtentsUtf8(wm->dpy, wm->font_title, + (const FcChar8 *)c->title, strlen(c->title), &ext); + + /* center in the full title bar */ + int tx = (fw - ext.width) / 2; + /* clamp so text doesn't overlap the left buttons */ + int left_btn_end = BTN_MAX_X + BTN_SIZE + 6; + if (tx < left_btn_end) tx = left_btn_end; + + int text_max_w = fw - tx - 8; + if (text_max_w < 10) text_max_w = 10; + + XRectangle clip = { (short)tx, 1, (unsigned short)text_max_w, TITLE_HEIGHT - 2 }; XftDrawSetClipRectangles(xd, 0, 0, &clip, 1); + int ty = (TITLE_HEIGHT + wm->font_title->ascent - wm->font_title->descent) / 2; XftDrawStringUtf8(xd, &col_text, wm->font_title, - BORDER_W + 6, ty, + tx, ty, (const FcChar8 *)c->title, strlen(c->title)); XftColorFree(wm->dpy, wm->visual, wm->cmap, &col_text); XftDrawDestroy(xd); diff --git a/wm/src/menu.c b/wm/src/menu.c index 91db61e..f73555b 100644 --- a/wm/src/menu.c +++ b/wm/src/menu.c @@ -1,7 +1,7 @@ #include "wm.h" -#define MENU_ITEM_H 24 -#define MENU_W 220 +#define MENU_ITEM_H 26 +#define MENU_W 230 #define MENU_PADDING 5 static const MenuItem MENU_ITEMS[] = { @@ -51,7 +51,7 @@ void menu_show(WM *wm, int x, int y) { XSetWindowAttributes swa; swa.override_redirect = True; - swa.background_pixel = color_alloc(wm, 0x1C2030); /* dark menu bg */ + swa.background_pixel = color_alloc(wm, 0x2C2C2C); /* macOS dark menu bg */ swa.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | LeaveWindowMask; @@ -65,8 +65,8 @@ void menu_show(WM *wm, int x, int y) { &swa ); - /* 1px accent border */ - XSetWindowBorder(wm->dpy, wm->menu_win, color_alloc(wm, COLOR_BORDER_LIGHT)); + /* subtle 1px border (macOS-like) */ + XSetWindowBorder(wm->dpy, wm->menu_win, color_alloc(wm, 0x505050)); XMapRaised(wm->dpy, wm->menu_win); wm->menu_visible = 1; @@ -110,29 +110,29 @@ void menu_draw(WM *wm) { if (!xd) return; XftColor col_normal, col_hover_bg, col_hover_text, col_sep; - xft_color(wm, 0xC8D4E8, &col_normal); /* light text on dark bg */ - xft_color(wm, 0x2060A0, &col_hover_bg); /* blue highlight */ - xft_color(wm, 0xF0F4FF, &col_hover_text); - xft_color(wm, 0x303848, &col_sep); + xft_color(wm, 0xE8E8E8, &col_normal); /* macOS light menu text */ + xft_color(wm, 0x4A90D9, &col_hover_bg); /* macOS blue selection */ + xft_color(wm, 0xFFFFFF, &col_hover_text); + xft_color(wm, 0x444444, &col_sep); /* clear background */ - XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x1C2030)); + XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x2C2C2C)); XFillRectangle(wm->dpy, wm->menu_win, wm->gc, 0, 0, MENU_W, menu_height()); for (int i = 0; i < MENU_COUNT; i++) { int iy = item_y(i); if (is_separator(i)) { - XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x2A3040)); + XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x3C3C3C)); XDrawLine(wm->dpy, wm->menu_win, wm->gc, 8, iy + 4, MENU_W - 8, iy + 4); continue; } - /* hover highlight */ + /* hover highlight — macOS blue selection pill */ if (i == wm->menu_hovered && wm->menu_hovered >= 0) { - XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x4A6888)); + XSetForeground(wm->dpy, wm->gc, color_alloc(wm, 0x4A90D9)); XFillRectangle(wm->dpy, wm->menu_win, wm->gc, - 2, iy, MENU_W - 4, MENU_ITEM_H); + 4, iy + 1, MENU_W - 8, MENU_ITEM_H - 2); } XftColor *tc = (i == wm->menu_hovered) ? &col_hover_text : &col_normal; @@ -142,7 +142,7 @@ void menu_draw(WM *wm) { strlen(MENU_ITEMS[i].label), &ext); int ty = iy + (MENU_ITEM_H - ext.height) / 2 + ext.height - ext.y; XftDrawStringUtf8(xd, tc, wm->font_menu, - MENU_PADDING + 4, ty, + MENU_PADDING + 10, ty, (const FcChar8 *)MENU_ITEMS[i].label, strlen(MENU_ITEMS[i].label)); } From 36641e77e2ef962d28be1a17bde4b8c7bed854f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 19:51:54 +0000 Subject: [PATCH 2/8] macOS-style polish: fix seam, bidirectional Alt+Tab, minimized windows in switcher, rounded corners, deskbar context menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #1: Replace hardcoded 185px deskbar-offset in client.c and events.c with DESKBAR_W constant (200) to eliminate the 15px gap between the WM frame and deskbar edge. Fix #2: Add Shift+Alt+Tab reverse-direction support — grab Mod1+Shift+XK_Tab in wm.c, detect ShiftMask in events.c, and implement switcher_prev() in switcher.c. Fix #3: Include minimized windows in the Alt+Tab switcher. swl_populate() no longer skips minimized clients; swl_mini[] tracks state so switcher_draw() can dim them with bracketed titles and darker card backgrounds. Fix #4: Rewrite themes/picom.conf with macOS-inspired settings — 10px corner radius on normal windows, 8px on menus, soft downward shadow (radius 18, offset-y 8, opacity 0.35), smooth fading (step 0.08), no dock/tray rounding. Fix #5: Add per-app right-click context menu to the deskbar. Right-clicking an app row opens a popup with Raise / Minimize / Close actions (Close shown in red). Actions are sent as EWMH client messages (_NET_ACTIVE_WINDOW, WM_CHANGE_STATE+IconicState, _NET_CLOSE_WINDOW) with matching handlers added to the WM's on_client_message. Pointer grab closes the menu on any outside click. --- deskbar/include/deskbar.h | 11 +++ deskbar/src/deskbar.c | 194 ++++++++++++++++++++++++++++++++++++++ themes/picom.conf | 45 +++++---- wm/include/wm.h | 3 + wm/src/client.c | 10 +- wm/src/events.c | 43 ++++++++- wm/src/switcher.c | 46 ++++++--- wm/src/wm.c | 1 + 8 files changed, 311 insertions(+), 42 deletions(-) diff --git a/deskbar/include/deskbar.h b/deskbar/include/deskbar.h index ffc7a89..2ebf161 100644 --- a/deskbar/include/deskbar.h +++ b/deskbar/include/deskbar.h @@ -44,6 +44,12 @@ #define DB_TRAY_MAX 16 /* maximum simultaneous tray icons */ #define DB_MAX_APPS 32 +/* ── per-app context menu ────────────────────────────────────────────── */ +#define DB_CTX_W 140 /* context menu width */ +#define DB_CTX_ITEM_H 26 /* item row height */ +#define DB_CTX_PAD 4 /* top/bottom padding */ +#define DB_CTX_N 3 /* number of items: Raise, Minimize, Close */ + /* ── fly-out menu ────────────────────────────────────────────────────── */ #define DB_FLYOUT_W 196 /* fly-out menu panel width */ #define DB_FLYOUT_IH 28 /* item row height */ @@ -139,6 +145,11 @@ typedef struct Deskbar { int sub_hov; /* hovered item in sub-menu */ int sw_cache; /* cached screen width for menu positioning */ + /* per-app right-click context menu */ + Window ctx_win; /* None when closed */ + int ctx_app_idx; /* which app row the ctx menu is for */ + int ctx_hov; /* hovered item index, -1 = none */ + /* system tray (freedesktop.org XEmbed tray protocol) */ Window tray_icons[DB_TRAY_MAX]; int n_tray_icons; diff --git a/deskbar/src/deskbar.c b/deskbar/src/deskbar.c index 61df494..b785a0e 100644 --- a/deskbar/src/deskbar.c +++ b/deskbar/src/deskbar.c @@ -809,6 +809,168 @@ static void handle_menu_press(Deskbar *db, int rx, int ry) { db_menu_close(db); } +/* ── per-app context menu ─────────────────────────────────────────────── */ + +static int ctx_total_h(void) { + return DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H + DB_CTX_PAD; +} + +static void db_ctx_draw(Deskbar *db) { + if (!db->ctx_win) return; + int ww = DB_CTX_W; + int wh = ctx_total_h(); + int mini = (db->ctx_app_idx >= 0 && db->ctx_app_idx < db->napps) + ? db->apps[db->ctx_app_idx].minimized : 0; + + static const char *labels_normal[] = { "Raise", "Minimize", "Close" }; + static const char *labels_mini[] = { "Restore", "Restore", "Close" }; + const char **labels = mini ? labels_mini : labels_normal; + + XftDraw *xd = XftDrawCreate(db->dpy, db->ctx_win, db->visual, db->cmap); + + /* background */ + XSetForeground(db->dpy, db->gc, db_color(db, 0x2C2C2C)); + XFillRectangle(db->dpy, db->ctx_win, db->gc, 0, 0, ww, wh); + + /* 1px border */ + XSetForeground(db->dpy, db->gc, db_color(db, 0x505050)); + XDrawRectangle(db->dpy, db->ctx_win, db->gc, 0, 0, ww - 1, wh - 1); + + for (int i = 0; i < DB_CTX_N; i++) { + int iy = DB_CTX_PAD + i * DB_CTX_ITEM_H; + + if (i == db->ctx_hov) { + XSetForeground(db->dpy, db->gc, db_color(db, 0x4A90D9)); + XFillRectangle(db->dpy, db->ctx_win, db->gc, + 2, iy + 1, ww - 4, DB_CTX_ITEM_H - 2); + } + + /* "Close" item tinted red */ + unsigned long tc = (i == db->ctx_hov) ? 0xFFFFFF + : (i == DB_CTX_N - 1) ? 0xFF6B6B /* Close = red tint */ + : 0xE8E8E8; + draw_str(db, xd, db->font_ui, tc, + 14, str_baseline(db->font_ui, iy, DB_CTX_ITEM_H), + labels[i], 0, 0); + } + + XftDrawDestroy(xd); + XFlush(db->dpy); +} + +static void db_ctx_close(Deskbar *db) { + if (!db->ctx_win) return; + XUngrabPointer(db->dpy, CurrentTime); + XDestroyWindow(db->dpy, db->ctx_win); + db->ctx_win = None; + db->ctx_app_idx = -1; + db->ctx_hov = -1; +} + +static void db_ctx_open(Deskbar *db, int app_idx, int rx, int ry) { + db_ctx_close(db); + + int sw = DisplayWidth(db->dpy, db->screen); + int sh = DisplayHeight(db->dpy, db->screen); + int wh = ctx_total_h(); + int wx = sw - DB_W - DB_CTX_W - 2; + int wy = ry; + if (wy + wh > sh) wy = sh - wh - 2; + if (wy < 0) wy = 0; + + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = db_color(db, 0x2C2C2C); + swa.event_mask = ExposureMask; + db->ctx_win = XCreateWindow( + db->dpy, db->root, wx, wy, DB_CTX_W, wh, 1, + db->depth, InputOutput, db->visual, + CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); + + XSetWindowBorder(db->dpy, db->ctx_win, db_color(db, 0x505050)); + db->ctx_app_idx = app_idx; + db->ctx_hov = -1; + XMapRaised(db->dpy, db->ctx_win); + db_ctx_draw(db); + + XGrabPointer(db->dpy, db->ctx_win, True, + ButtonPressMask | PointerMotionMask | ButtonReleaseMask, + GrabModeAsync, GrabModeAsync, None, None, CurrentTime); +} + +static void db_ctx_handle_motion(Deskbar *db, int rx, int ry) { + if (!db->ctx_win) return; + int cx, cy; unsigned cw, ch, bw, dep; Window cr; + XGetGeometry(db->dpy, db->ctx_win, &cr, &cx, &cy, &cw, &ch, &bw, &dep); + if (rx < cx || ry < cy || rx >= cx + (int)cw || ry >= cy + (int)ch) { + if (db->ctx_hov != -1) { db->ctx_hov = -1; db_ctx_draw(db); } + return; + } + int lx = ry - cy - DB_CTX_PAD; + int idx = (lx >= 0) ? lx / DB_CTX_ITEM_H : -1; + if (idx >= DB_CTX_N) idx = -1; + if (idx != db->ctx_hov) { db->ctx_hov = idx; db_ctx_draw(db); } +} + +static void db_ctx_handle_press(Deskbar *db, int rx, int ry) { + if (!db->ctx_win) return; + int cx, cy; unsigned cw, ch, bw, dep; Window cr; + XGetGeometry(db->dpy, db->ctx_win, &cr, &cx, &cy, &cw, &ch, &bw, &dep); + + int app_idx = db->ctx_app_idx; + int in_menu = (rx >= cx && ry >= cy && rx < cx + (int)cw && ry < cy + (int)ch); + + if (!in_menu || app_idx < 0 || app_idx >= db->napps) { + db_ctx_close(db); + return; + } + + int lx = ry - cy - DB_CTX_PAD; + int idx = (lx >= 0) ? lx / DB_CTX_ITEM_H : -1; + Window target = db->apps[app_idx].win; + int mini = db->apps[app_idx].minimized; + db_ctx_close(db); + + if (idx < 0 || idx >= DB_CTX_N) return; + + XEvent ev = {0}; + ev.xclient.type = ClientMessage; + ev.xclient.format = 32; + + if (idx == 0) { + /* Raise / Restore */ + ev.xclient.window = target; + ev.xclient.message_type = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False); + ev.xclient.data.l[0] = 2; + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(db->dpy, db->root, False, + SubstructureRedirectMask | SubstructureNotifyMask, &ev); + } else if (idx == 1) { + if (mini) { + /* Restore */ + ev.xclient.window = target; + ev.xclient.message_type = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False); + ev.xclient.data.l[0] = 2; + ev.xclient.data.l[1] = CurrentTime; + } else { + /* Minimize via WM_CHANGE_STATE IconicState */ + ev.xclient.window = target; + ev.xclient.message_type = XInternAtom(db->dpy, "WM_CHANGE_STATE", False); + ev.xclient.data.l[0] = 3; /* IconicState */ + } + XSendEvent(db->dpy, db->root, False, + SubstructureRedirectMask | SubstructureNotifyMask, &ev); + } else if (idx == 2) { + /* Close via _NET_CLOSE_WINDOW */ + ev.xclient.window = target; + ev.xclient.message_type = XInternAtom(db->dpy, "_NET_CLOSE_WINDOW", False); + ev.xclient.data.l[0] = CurrentTime; + ev.xclient.data.l[1] = 2; + XSendEvent(db->dpy, db->root, False, + SubstructureRedirectMask | SubstructureNotifyMask, &ev); + } +} + /* ── input ────────────────────────────────────────────────────────────── */ void db_hover(Deskbar *db, int x, int y) { @@ -981,6 +1143,26 @@ void db_run(Deskbar *db) { XEvent e; XNextEvent(db->dpy, &e); + /* While the per-app context menu is open, the pointer is grabbed. + Route motion and button events through the ctx handlers. */ + if (db->ctx_win) { + if (e.type == MotionNotify) { + db_ctx_handle_motion(db, e.xmotion.x_root, e.xmotion.y_root); + continue; + } + if (e.type == ButtonPress) { + db_ctx_handle_press(db, e.xbutton.x_root, e.xbutton.y_root); + continue; + } + if (e.type == ButtonRelease) + continue; + if (e.type == Expose && e.xexpose.count == 0) { + if (e.xany.window == db->ctx_win) db_ctx_draw(db); + else if (e.xany.window == db->win) db_draw(db); + continue; + } + } + /* While the fly-out menu is open, the pointer is grabbed. Route all motion and button events through the menu handlers (they use root-relative coords to determine which panel was hit). */ @@ -1026,6 +1208,18 @@ void db_run(Deskbar *db) { system("pactl set-sink-volume @DEFAULT_SINK@ -5% 2>/dev/null &"); break; } + if (e.xbutton.button == Button3) { + /* right-click on an app row → open context menu */ + int ay = db_apps_y(); + int by = e.xbutton.y; + if (by >= ay && by < ay + db->napps * DB_ITEM_H) { + int idx = (by - ay) / DB_ITEM_H; + if (idx >= 0 && idx < db->napps) + db_ctx_open(db, idx, + e.xbutton.x_root, e.xbutton.y_root); + } + break; + } if (e.xbutton.button == Button1 && e.xbutton.y >= sy) system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null &"); else diff --git a/themes/picom.conf b/themes/picom.conf index 231446b..4f347f5 100644 --- a/themes/picom.conf +++ b/themes/picom.conf @@ -1,22 +1,29 @@ -# OrionOS picom compositor config -# Goal: subtle window shadows, no transparency, fast rendering. -# BeOS/OrionOS didn't have transparency — we keep it clean. +# OrionOS picom compositor config — macOS-inspired +# Rounded corners, soft macOS-style shadows, fast fading. -backend = "xrender"; # xrender works on all hardware; switch to glx for performance +backend = "xrender"; vsync = false; +# ── rounded corners ─────────────────────────────────────────────────────── +corner-radius = 10; +rounded-corners-exclude = [ + "_NET_WM_WINDOW_TYPE@:a *= 'DOCK'", + "_NET_WM_WINDOW_TYPE@:a *= 'DESKTOP'", + "_NET_WM_WINDOW_TYPE@:a *= 'TOOLTIP'", + "_NET_WM_WINDOW_TYPE@:a *= 'NOTIFICATION'", + "override_redirect = 1 && !WM_CLASS@:s" +]; + # ── shadows ─────────────────────────────────────────────────────────────── +# macOS-style: larger, softer, offset down-right shadow = true; -shadow-radius = 8; -shadow-offset-x = 4; -shadow-offset-y = 4; -shadow-opacity = 0.30; +shadow-radius = 18; +shadow-offset-x = 0; +shadow-offset-y = 8; +shadow-opacity = 0.35; shadow-color = "#000000"; -# Don't shadow the deskbar, menus, or tooltips shadow-exclude = [ - "class_g = 'skydeskbar'", - "class_g = 'skylauncher'", "_NET_WM_WINDOW_TYPE@:a *= 'DOCK'", "_NET_WM_WINDOW_TYPE@:a *= 'TOOLTIP'", "_NET_WM_WINDOW_TYPE@:a *= 'NOTIFICATION'", @@ -27,20 +34,18 @@ shadow-exclude = [ # ── fading ──────────────────────────────────────────────────────────────── fading = true; -fade-in-step = 0.06; -fade-out-step = 0.06; -fade-delta = 8; # ms between steps — snappy, not sluggish +fade-in-step = 0.08; +fade-out-step = 0.08; +fade-delta = 6; no-fading-openclose = false; # ── opacity ─────────────────────────────────────────────────────────────── -# No transparency — BeOS aesthetic is solid, crisp windows. inactive-opacity = 1.0; active-opacity = 1.0; frame-opacity = 1.0; inactive-opacity-override = false; # ── blur ───────────────────────────────────────────────────────────────── -# Disabled — keep it clean. blur-method = "none"; # ── focus & refresh ─────────────────────────────────────────────────────── @@ -55,9 +60,9 @@ mark-ovredir-focused = false; wintypes: { tooltip = { fade = true; shadow = false; opacity = 1.0; focus = true; }; - dock = { shadow = false; clip-shadow-above = true; }; + dock = { shadow = false; clip-shadow-above = true; corner-radius = 0; }; dnd = { shadow = false; }; - popup_menu = { opacity = 1.0; shadow = false; }; - dropdown_menu = { opacity = 1.0; shadow = false; }; - notification = { shadow = false; }; + popup_menu = { opacity = 1.0; shadow = false; corner-radius = 8; }; + dropdown_menu = { opacity = 1.0; shadow = false; corner-radius = 8; }; + notification = { shadow = false; corner-radius = 8; }; }; diff --git a/wm/include/wm.h b/wm/include/wm.h index 5a1ea43..3c0c287 100644 --- a/wm/include/wm.h +++ b/wm/include/wm.h @@ -29,6 +29,7 @@ #define COLOR_SNAP_OVERLAY 0x4A90D9 /* macOS blue snap highlight */ /* ── geometry ────────────────────────────────────────────────────────── */ +#define DESKBAR_W 200 /* must match DB_W in deskbar/include/deskbar.h */ #define TITLE_HEIGHT 28 /* macOS-style height */ #define TITLE_TAB_W 200 #define BORDER_W 1 /* thin 1px macOS-style border */ @@ -130,6 +131,7 @@ typedef struct WM { int swl_count; Window swl_wins[64]; char swl_titles[64][128]; + int swl_mini[64]; /* 1 if corresponding entry is minimized */ } WM; /* core */ @@ -175,6 +177,7 @@ int menu_item_at(int py); /* switcher */ void switcher_show(WM *wm); void switcher_next(WM *wm); +void switcher_prev(WM *wm); void switcher_hide(WM *wm, int commit); void switcher_draw(WM *wm); diff --git a/wm/src/client.c b/wm/src/client.c index 5f50b34..5fc5aa4 100644 --- a/wm/src/client.c +++ b/wm/src/client.c @@ -65,9 +65,9 @@ Client *client_add(WM *wm, Window win) { c->x = wa.x; c->y = wa.y; } - /* never land behind deskbar (top-right panel ~180px wide) */ + /* never land behind deskbar */ int sw = DisplayWidth(wm->dpy, wm->screen); - if (c->x + (int)c->w > sw - 185 && c->y < 200) + if (c->x + (int)c->w > sw - DESKBAR_W && c->y < 200) c->y = 40; /* fetch title */ @@ -228,7 +228,7 @@ void client_maximize(WM *wm, Client *c) { int sh = DisplayHeight(wm->dpy, wm->screen); /* leave room for deskbar on the right */ - int deskbar_w = 185; + int deskbar_w = DESKBAR_W; int new_w = sw - deskbar_w - BORDER_W * 2; int new_h = sh - TITLE_HEIGHT - BORDER_W; @@ -280,7 +280,7 @@ void client_snap_left(WM *wm, Client *c) { if (c->maximized) client_unmaximize(wm, c); int sw = DisplayWidth(wm->dpy, wm->screen); int sh = DisplayHeight(wm->dpy, wm->screen); - int deskbar_w = 185; + int deskbar_w = DESKBAR_W; int half_w = (sw - deskbar_w) / 2; c->x = 0; @@ -299,7 +299,7 @@ void client_snap_right(WM *wm, Client *c) { if (c->maximized) client_unmaximize(wm, c); int sw = DisplayWidth(wm->dpy, wm->screen); int sh = DisplayHeight(wm->dpy, wm->screen); - int deskbar_w = 185; + int deskbar_w = DESKBAR_W; int half_w = (sw - deskbar_w) / 2; c->x = half_w; diff --git a/wm/src/events.c b/wm/src/events.c index 031b4fe..7f430fa 100644 --- a/wm/src/events.c +++ b/wm/src/events.c @@ -33,7 +33,7 @@ static void check_snap(WM *wm, Client *c) { } else { client_snap_left(wm, c); } - } else if (c->x + (int)(c->w + BORDER_W * 2) >= sw - 185 - SNAP_DIST) { + } else if (c->x + (int)(c->w + BORDER_W * 2) >= sw - DESKBAR_W - SNAP_DIST) { client_snap_right(wm, c); } else if (c->y <= SNAP_DIST) { client_maximize(wm, c); @@ -95,10 +95,34 @@ void on_enter_notify(WM *wm, XCrossingEvent *e) { } void on_client_message(WM *wm, XClientMessageEvent *e) { - Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False); + Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False); + Atom net_close = XInternAtom(wm->dpy, "_NET_CLOSE_WINDOW", False); + Atom wm_change_state = XInternAtom(wm->dpy, "WM_CHANGE_STATE", False); + if (e->message_type == net_active) { Client *c = client_find_by_win(wm, e->window); if (c) client_restore(wm, c); + return; + } + if (e->message_type == net_close) { + Client *c = client_find_by_win(wm, e->window); + if (c) { + XEvent ev = {0}; + ev.xclient.type = ClientMessage; + ev.xclient.window = c->win; + ev.xclient.message_type = wm->wm_protocols; + ev.xclient.format = 32; + ev.xclient.data.l[0] = wm->wm_delete_window; + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(wm->dpy, c->win, False, NoEventMask, &ev); + } + return; + } + /* WM_CHANGE_STATE IconicState → minimize */ + if (e->message_type == wm_change_state && e->data.l[0] == 3) { + Client *c = client_find_by_win(wm, e->window); + if (c && !c->minimized) client_minimize(wm, c); + return; } } @@ -263,10 +287,19 @@ void on_key_press(WM *wm, XKeyEvent *e) { if ((e->state & Mod1Mask) && sym == XK_F4 && wm->focused) send_delete(wm, wm->focused->win); - /* Alt+Tab — visual switcher */ + /* Alt+Tab / Shift+Alt+Tab — visual switcher */ if ((e->state & Mod1Mask) && sym == XK_Tab) { - if (wm->swl_visible) switcher_next(wm); - else if (wm->clients) switcher_show(wm); + int backward = (e->state & ShiftMask) != 0; + if (wm->swl_visible) { + if (backward) switcher_prev(wm); + else switcher_next(wm); + } else if (wm->clients) { + switcher_show(wm); + if (backward && wm->swl_visible) { + wm->swl_idx = wm->swl_count - 1; + switcher_draw(wm); + } + } return; } diff --git a/wm/src/switcher.c b/wm/src/switcher.c index 45cb2d3..3096d85 100644 --- a/wm/src/switcher.c +++ b/wm/src/switcher.c @@ -8,17 +8,19 @@ static void swl_populate(WM *wm) { wm->swl_count = 0; - /* current focused window goes first */ + /* current focused (non-minimized) window goes first */ if (wm->focused && !wm->focused->minimized) { - wm->swl_wins[0] = wm->focused->win; + wm->swl_wins[0] = wm->focused->win; + wm->swl_mini[0] = 0; strncpy(wm->swl_titles[0], wm->focused->title, 127); wm->swl_titles[0][127] = '\0'; wm->swl_count = 1; } + /* all other windows, including minimized ones */ for (Client *c = wm->clients; c && wm->swl_count < 64; c = c->next) { - if (c->minimized) continue; if (wm->focused && c == wm->focused) continue; - wm->swl_wins[wm->swl_count] = c->win; + wm->swl_wins[wm->swl_count] = c->win; + wm->swl_mini[wm->swl_count] = c->minimized; strncpy(wm->swl_titles[wm->swl_count], c->title, 127); wm->swl_titles[wm->swl_count][127] = '\0'; wm->swl_count++; @@ -61,29 +63,34 @@ void switcher_draw(WM *wm) { for (int vi = 0; vi < vis; vi++) { int i = vstart + vi; int selected = (i == wm->swl_idx); + int mini = wm->swl_mini[i]; int ix = SWL_PAD + vi * (SWL_ITEM_W + SWL_PAD); int iy = SWL_PAD; - unsigned long card_bg = color_alloc(wm, selected ? COLOR_TITLE_ACTIVE : 0x1C2338); + unsigned long card_bg = color_alloc(wm, + selected ? COLOR_TITLE_ACTIVE : (mini ? 0x141820 : 0x1C2338)); XSetForeground(wm->dpy, wm->gc, card_bg); XFillRectangle(wm->dpy, wm->swl_win, wm->gc, ix, iy, SWL_ITEM_W, SWL_ITEM_H); XSetForeground(wm->dpy, wm->gc, - color_alloc(wm, selected ? COLOR_BORDER_LIGHT : 0x242C3E)); + color_alloc(wm, selected ? COLOR_BORDER_LIGHT : (mini ? 0x202028 : 0x242C3E))); XDrawRectangle(wm->dpy, wm->swl_win, wm->gc, ix, iy, SWL_ITEM_W - 1, SWL_ITEM_H - 1); - /* letter icon */ + /* letter icon — dimmed for minimized */ int icon_x = ix + (SWL_ITEM_W - SWL_ICON_SZ) / 2; int icon_y = iy + 8; XSetForeground(wm->dpy, wm->gc, - color_alloc(wm, selected ? 0x285898 : 0x182538)); + color_alloc(wm, selected ? 0x285898 : (mini ? 0x1A1E28 : 0x182538))); XFillRectangle(wm->dpy, wm->swl_win, wm->gc, icon_x, icon_y, SWL_ICON_SZ, SWL_ICON_SZ); char letter[2] = { wm->swl_titles[i][0] ? wm->swl_titles[i][0] : '?', '\0' }; if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32; + unsigned long icon_rgb = selected ? COLOR_TITLE_TEXT_ACT + : (mini ? 0x3A4860 + : COLOR_TITLE_TEXT_INACT); XftColor icon_col; - xft_color(wm, selected ? COLOR_TITLE_TEXT_ACT : COLOR_TITLE_TEXT_INACT, &icon_col); + xft_color(wm, icon_rgb, &icon_col); XGlyphInfo ext; XftTextExtentsUtf8(wm->dpy, wm->font_title, (const FcChar8 *)letter, 1, &ext); int lx = icon_x + (SWL_ICON_SZ - (int)ext.width) / 2; @@ -91,10 +98,19 @@ void switcher_draw(WM *wm) { XftDrawStringUtf8(xd, &icon_col, wm->font_title, lx, ly, (const FcChar8 *)letter, 1); XftColorFree(wm->dpy, wm->visual, wm->cmap, &icon_col); - /* title below icon */ - const char *title = wm->swl_titles[i]; + /* title below icon — prefix [..] for minimized */ + char title_buf[132]; + const char *base_title = wm->swl_titles[i]; + if (mini && !selected) + snprintf(title_buf, sizeof(title_buf), "[%s]", base_title); + else + snprintf(title_buf, sizeof(title_buf), "%s", base_title); + const char *title = title_buf; XftColor title_col; - xft_color(wm, selected ? COLOR_TITLE_TEXT_ACT : COLOR_TITLE_TEXT_INACT, &title_col); + unsigned long title_rgb = selected ? COLOR_TITLE_TEXT_ACT + : (mini ? 0x3A4860 + : COLOR_TITLE_TEXT_INACT); + xft_color(wm, title_rgb, &title_col); XGlyphInfo text_ext; XftTextExtentsUtf8(wm->dpy, wm->font_title, (const FcChar8 *)title, strlen(title), &text_ext); @@ -163,6 +179,12 @@ void switcher_next(WM *wm) { switcher_draw(wm); } +void switcher_prev(WM *wm) { + if (!wm->swl_visible) return; + wm->swl_idx = (wm->swl_idx - 1 + wm->swl_count) % wm->swl_count; + switcher_draw(wm); +} + void switcher_hide(WM *wm, int commit) { if (!wm->swl_visible) return; XUngrabKeyboard(wm->dpy, CurrentTime); diff --git a/wm/src/wm.c b/wm/src/wm.c index 1634123..f70b384 100644 --- a/wm/src/wm.c +++ b/wm/src/wm.c @@ -90,6 +90,7 @@ void wm_init(WM *wm) { /* global key grabs */ struct { KeySym sym; unsigned mod; } grabs[] = { { XK_Tab, Mod1Mask }, + { XK_Tab, Mod1Mask | ShiftMask }, /* Shift+Alt+Tab — backwards */ { XK_F4, Mod1Mask }, { XK_F9, Mod1Mask }, /* minimize */ { XK_F10, Mod1Mask }, /* maximize */ From b0f64d9b2e37be1f0b0c8bc6133d97350ef50ec6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 20:00:10 +0000 Subject: [PATCH 3/8] =?UTF-8?q?Fixes=20#7=E2=80=93#11:=20launcher=20Other?= =?UTF-8?q?=20tab,=20ICCCM=20size=20hints,=20icon=20loading,=20multi-deskt?= =?UTF-8?q?op?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #7: Add "Other" catch-all category to the launcher. map_category() now returns "Other" instead of "" for unrecognized .desktop Categories, and an "Other" tab is added to the CATS list so those apps are discoverable instead of silently hidden. Fix #8: Respect ICCCM WM_NORMAL_HINTS. client_add() reads PMinSize, PResizeInc, and PBaseSize from the window's size hints. The resize drag handler in events.c clamps to min_w/min_h and snaps to the width_inc/height_inc grid, fixing jank in character-cell terminal emulators (xterm, Alacritty). Fix #10: Optional freedesktop icon loading via imlib2. The launcher now parses the Icon= field from .desktop files and attempts to render a PNG icon from Papirus-Dark (or hicolor fallback). When imlib2 is not present the existing letter-square is shown unchanged. launcher/Makefile auto-detects imlib2 via pkg-config and sets -DHAVE_IMLIB2. Fix #11: True 4-desktop support in the WM. Each Client carries a desktop field (assigned at map time from wm->cur_desktop). Clicking a workspace button in the deskbar sends _NET_CURRENT_DESKTOP; the WM now handles it via wm_switch_desktop() which unmaps frames on the leaving desktop, maps frames on the arriving desktop, and re-focuses. _NET_WM_DESKTOP is set per window so the deskbar can filter db_update_apps() to show only current-desktop windows (sticky windows with desktop 0xFFFFFFFF are always shown). --- deskbar/src/deskbar.c | 8 ++++++ launcher/Makefile | 8 ++++++ launcher/src/launcher.c | 61 ++++++++++++++++++++++++++++++++++------- wm/include/wm.h | 9 ++++++ wm/src/client.c | 39 ++++++++++++++++++++++++-- wm/src/events.c | 58 +++++++++++++++++++++++++++++++++++++-- wm/src/wm.c | 6 +++- 7 files changed, 173 insertions(+), 16 deletions(-) diff --git a/deskbar/src/deskbar.c b/deskbar/src/deskbar.c index b785a0e..04e1411 100644 --- a/deskbar/src/deskbar.c +++ b/deskbar/src/deskbar.c @@ -111,8 +111,14 @@ void db_update_apps(Deskbar *db) { Atom net_wm_state = XInternAtom(db->dpy, "_NET_WM_STATE", False); Atom net_wm_state_hidden = XInternAtom(db->dpy, "_NET_WM_STATE_HIDDEN", False); + Atom net_wm_desktop = XInternAtom(db->dpy, "_NET_WM_DESKTOP", False); for (unsigned long i = 0; i < n && db->napps < DB_MAX_APPS; i++) { + /* only show windows on the current desktop (or sticky: desktop == 0xFFFFFFFF) */ + long win_desk = ewmh_get_long(db, wins[i], net_wm_desktop); + if (win_desk >= 0 && win_desk != (long)db->cur_ws && win_desk != (long)0xFFFFFFFF) + continue; + db->apps[db->napps].win = wins[i]; db->apps[db->napps].focused = (wins[i] == (Window)active_win); db->apps[db->napps].minimized = 0; @@ -1285,6 +1291,8 @@ void db_run(Deskbar *db) { db_draw(db); } else if (e.xproperty.atom == db->net_current_desktop) { db_update_workspace(db); + db_update_apps(db); /* re-filter by new desktop */ + db_resize(db); db_draw(db); } break; diff --git a/launcher/Makefile b/launcher/Makefile index 55272c4..883660f 100644 --- a/launcher/Makefile +++ b/launcher/Makefile @@ -7,6 +7,14 @@ ifeq ($(PKG_XFT_C),) PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig endif +# Optional: imlib2 for freedesktop icon loading +PKG_IL_C := $(shell pkg-config --cflags imlib2 2>/dev/null) +PKG_IL_L := $(shell pkg-config --libs imlib2 2>/dev/null) +ifneq ($(PKG_IL_C),) + PKG_XFT_C += $(PKG_IL_C) -DHAVE_IMLIB2 + PKG_XFT_L += $(PKG_IL_L) +endif + CFLAGS = -Wall -Wextra -O2 $(PKG_XFT_C) LDFLAGS = $(PKG_XFT_L) diff --git a/launcher/src/launcher.c b/launcher/src/launcher.c index 22eb209..425712e 100644 --- a/launcher/src/launcher.c +++ b/launcher/src/launcher.c @@ -58,13 +58,13 @@ static const unsigned long ICON_BG[] = { #define FONT_ICON "Sans:size=13:weight=bold" #define MAX_FLT 64 -typedef struct { char name[64]; char cmd[256]; char cat[32]; } App; +typedef struct { char name[64]; char cmd[256]; char cat[32]; char icon[64]; } App; static App APPS[MAX_APPS]; static int NAPP = 0; static const char *CATS[] = { - "All", "Internet", "Media", "Office", "System", "Settings" + "All", "Internet", "Media", "Office", "System", "Settings", "Other" }; #define NCAT ((int)(sizeof(CATS)/sizeof(CATS[0]))) @@ -89,7 +89,7 @@ static const char *map_category(const char *cats) { strstr(cats, "FileManager") || strstr(cats, "Utility") || strstr(cats, "Monitor") || strstr(cats, "Security")) return "System"; - return ""; + return "Other"; } static void clean_exec(char *dst, const char *src, size_t dstsz) { @@ -108,7 +108,7 @@ static void load_desktop_file(const char *path) { FILE *f = fopen(path, "r"); if (!f) return; - char name[64] = {0}, exec[256] = {0}, cats[128] = {0}; + char name[64] = {0}, exec[256] = {0}, cats[128] = {0}, icon[64] = {0}; int in_entry = 0, nodisplay = 0, is_app = 0; char line[512]; @@ -126,6 +126,8 @@ static void load_desktop_file(const char *path) { snprintf(exec, sizeof(exec), "%s", line + 5); else if (!cats[0] && strncmp(line, "Categories=", 11) == 0) snprintf(cats, sizeof(cats), "%s", line + 11); + else if (!icon[0] && strncmp(line, "Icon=", 5) == 0) + snprintf(icon, sizeof(icon), "%s", line + 5); else if (strncmp(line, "NoDisplay=true", 14) == 0) nodisplay = 1; else if (strncmp(line, "Type=Application", 16) == 0) @@ -137,6 +139,7 @@ static void load_desktop_file(const char *path) { snprintf(APPS[NAPP].name, 64, "%s", name); clean_exec(APPS[NAPP].cmd, exec, 256); snprintf(APPS[NAPP].cat, 32, "%s", map_category(cats)); + snprintf(APPS[NAPP].icon, 64, "%s", icon); NAPP++; } @@ -292,6 +295,39 @@ static int card_at_vis(int x, int y) { return row * COLS + col; } +/* ── icon loading (imlib2, optional) ─────────────────────────────────── */ +#ifdef HAVE_IMLIB2 +#include +static const char *ICON_DIRS[] = { + "/usr/share/icons/Papirus-Dark/48x48/apps", + "/usr/share/icons/Papirus/48x48/apps", + "/usr/share/icons/hicolor/48x48/apps", + "/usr/share/pixmaps", + NULL +}; +static int render_icon(L *l, const char *icon_name, int ix, int iy, int sz) { + if (!icon_name || !icon_name[0]) return 0; + char path[512]; + for (int i = 0; ICON_DIRS[i]; i++) { + snprintf(path, sizeof(path), "%s/%s.png", ICON_DIRS[i], icon_name); + if (access(path, R_OK) == 0) goto found; + } + return 0; +found:; + imlib_context_set_display(l->dpy); + imlib_context_set_visual(l->visual); + imlib_context_set_colormap(l->cmap); + imlib_context_set_drawable(l->win); + imlib_context_set_blend(1); + Imlib_Image img = imlib_load_image_immediately(path); + if (!img) return 0; + imlib_context_set_image(img); + imlib_render_image_on_drawable_at_size(ix, iy, sz, sz); + imlib_free_image(); + return 1; +} +#endif + /* ── draw ────────────────────────────────────────────────────────────── */ static void draw(L *l) { XftDraw *xd = XftDrawCreate(l->dpy, l->win, l->visual, l->cmap); @@ -382,12 +418,17 @@ static void draw(L *l) { for (const char *p = APPS[app_i].name; *p; p++) h = h * 31 + (unsigned char)*p; int ix = cx + (CARD_W - ICON_SZ) / 2; int iy = cy + 10; - XSetForeground(l->dpy, l->gc, mkc(l, ICON_BG[h % NICON])); - XFillRectangle(l->dpy, l->win, l->gc, ix, iy, ICON_SZ, ICON_SZ); - - char letter[2] = { APPS[app_i].name[0], '\0' }; - if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32; - ds_ctr(l, xd, l->font_icon, 0xEAF0FF, ix, iy, ICON_SZ, ICON_SZ, letter); + int icon_rendered = 0; +#ifdef HAVE_IMLIB2 + icon_rendered = render_icon(l, APPS[app_i].icon, ix, iy, ICON_SZ); +#endif + if (!icon_rendered) { + XSetForeground(l->dpy, l->gc, mkc(l, ICON_BG[h % NICON])); + XFillRectangle(l->dpy, l->win, l->gc, ix, iy, ICON_SZ, ICON_SZ); + char letter[2] = { APPS[app_i].name[0], '\0' }; + if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32; + ds_ctr(l, xd, l->font_icon, 0xEAF0FF, ix, iy, ICON_SZ, ICON_SZ, letter); + } char lbl[14]; snprintf(lbl, sizeof(lbl), "%.12s", APPS[app_i].name); diff --git a/wm/include/wm.h b/wm/include/wm.h index 3c0c287..e9727be 100644 --- a/wm/include/wm.h +++ b/wm/include/wm.h @@ -73,10 +73,15 @@ typedef struct Client { int focused; int minimized; int maximized; + int desktop; /* virtual desktop this window lives on */ int saved_x, saved_y; /* pre-maximize position */ unsigned saved_w, saved_h; /* pre-maximize size */ Time last_click_time; /* for double-click detection */ char title[256]; + /* ICCCM WM_NORMAL_HINTS */ + int min_w, min_h; /* PMinSize */ + int inc_w, inc_h; /* PResizeInc — 0 means no constraint */ + int base_w, base_h; /* PBaseSize — baseline for increment calc */ struct Client *next; } Client; @@ -104,6 +109,8 @@ typedef struct WM { Client *clients; Client *focused; + int cur_desktop; /* active virtual desktop (0-based) */ + Atom wm_delete_window; Atom wm_protocols; Atom wm_state; @@ -112,6 +119,8 @@ typedef struct WM { Atom net_wm_state_hidden; Atom net_wm_state_maximized_vert; Atom net_wm_state_maximized_horz; + Atom net_wm_desktop; + Atom net_current_desktop; DragMode drag_mode; int drag_start_x, drag_start_y; diff --git a/wm/src/client.c b/wm/src/client.c index 5fc5aa4..d9d9644 100644 --- a/wm/src/client.c +++ b/wm/src/client.c @@ -54,9 +54,12 @@ Client *client_add(WM *wm, Window win) { if (client_find_by_win(wm, win)) return NULL; Client *c = calloc(1, sizeof(Client)); - c->win = win; - c->w = wa.width; - c->h = wa.height; + c->win = win; + c->w = wa.width; + c->h = wa.height; + c->desktop = wm->cur_desktop; + c->min_w = MIN_WIN_W; + c->min_h = MIN_WIN_H; /* use application-requested position if non-zero, otherwise smart place */ if (wa.x == 0 && wa.y == 0) { @@ -93,6 +96,36 @@ Client *client_add(WM *wm, Window win) { } } + /* ICCCM WM_NORMAL_HINTS — size constraints and resize increments */ + { + XSizeHints hints; + long supplied = 0; + if (XGetWMNormalHints(wm->dpy, win, &hints, &supplied)) { + if (hints.flags & PMinSize) { + if (hints.min_width > c->min_w) c->min_w = hints.min_width; + if (hints.min_height > c->min_h) c->min_h = hints.min_height; + } + if (hints.flags & PResizeInc) { + c->inc_w = hints.width_inc; + c->inc_h = hints.height_inc; + } + if (hints.flags & PBaseSize) { + c->base_w = hints.base_width; + c->base_h = hints.base_height; + } else if (hints.flags & PMinSize) { + c->base_w = hints.min_width; + c->base_h = hints.min_height; + } + } + } + + /* advertise which desktop this window lives on */ + { + long desk = wm->cur_desktop; + XChangeProperty(wm->dpy, win, wm->net_wm_desktop, XA_CARDINAL, 32, + PropModeReplace, (unsigned char *)&desk, 1); + } + int fw = (int)c->w + BORDER_W * 2; int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; diff --git a/wm/src/events.c b/wm/src/events.c index 7f430fa..d244864 100644 --- a/wm/src/events.c +++ b/wm/src/events.c @@ -94,11 +94,55 @@ void on_enter_notify(WM *wm, XCrossingEvent *e) { if (c && !c->minimized) set_cursor(wm, c, e->x, e->y); } +static void wm_switch_desktop(WM *wm, int new_desk) { + if (new_desk < 0 || new_desk >= 4 || new_desk == wm->cur_desktop) return; + + /* hide windows on old desktop */ + for (Client *c = wm->clients; c; c = c->next) { + if (!c->minimized && c->desktop == wm->cur_desktop) + XUnmapWindow(wm->dpy, c->frame); + } + + wm->cur_desktop = new_desk; + + /* show windows on new desktop */ + for (Client *c = wm->clients; c; c = c->next) { + if (!c->minimized && c->desktop == new_desk) + XMapWindow(wm->dpy, c->frame); + } + + /* update focus */ + if (wm->focused && wm->focused->desktop != new_desk) { + wm->focused->focused = 0; + frame_draw(wm, wm->focused); + wm->focused = NULL; + } + if (!wm->focused) { + for (Client *c = wm->clients; c; c = c->next) { + if (!c->minimized && c->desktop == new_desk) { + client_focus(wm, c); + break; + } + } + } + + /* advertise new desktop */ + long nd = new_desk; + XChangeProperty(wm->dpy, wm->root, wm->net_current_desktop, XA_CARDINAL, 32, + PropModeReplace, (unsigned char *)&nd, 1); +} + void on_client_message(WM *wm, XClientMessageEvent *e) { Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False); Atom net_close = XInternAtom(wm->dpy, "_NET_CLOSE_WINDOW", False); Atom wm_change_state = XInternAtom(wm->dpy, "WM_CHANGE_STATE", False); + /* _NET_CURRENT_DESKTOP → switch virtual desktop */ + if (e->message_type == wm->net_current_desktop && e->window == wm->root) { + wm_switch_desktop(wm, (int)e->data.l[0]); + return; + } + if (e->message_type == net_active) { Client *c = client_find_by_win(wm, e->window); if (c) client_restore(wm, c); @@ -271,8 +315,18 @@ void on_motion_notify(WM *wm, XMotionEvent *e) { } else { int nw = (int)wm->drag_win_w + dx; int nh = (int)wm->drag_win_h + dy; - if (nw < MIN_WIN_W) nw = MIN_WIN_W; - if (nh < MIN_WIN_H) nh = MIN_WIN_H; + /* clamp to minimum */ + if (nw < c->min_w) nw = c->min_w; + if (nh < c->min_h) nh = c->min_h; + /* snap to resize-increment grid (e.g. terminal character cells) */ + if (c->inc_w > 1) { + nw = c->base_w + ((nw - c->base_w) / c->inc_w) * c->inc_w; + if (nw < c->min_w) nw += c->inc_w; + } + if (c->inc_h > 1) { + nh = c->base_h + ((nh - c->base_h) / c->inc_h) * c->inc_h; + if (nh < c->min_h) nh += c->inc_h; + } c->w = (unsigned)nw; c->h = (unsigned)nh; XResizeWindow(wm->dpy, c->frame, c->w+BORDER_W*2, c->h+TITLE_HEIGHT+BORDER_W); XResizeWindow(wm->dpy, c->win, c->w, c->h); diff --git a/wm/src/wm.c b/wm/src/wm.c index f70b384..388237c 100644 --- a/wm/src/wm.c +++ b/wm/src/wm.c @@ -48,6 +48,8 @@ void wm_init(WM *wm) { wm->net_wm_state_hidden = XInternAtom(wm->dpy, "_NET_WM_STATE_HIDDEN", False); wm->net_wm_state_maximized_vert = XInternAtom(wm->dpy, "_NET_WM_STATE_MAXIMIZED_VERT", False); wm->net_wm_state_maximized_horz = XInternAtom(wm->dpy, "_NET_WM_STATE_MAXIMIZED_HORZ", False); + wm->net_wm_desktop = XInternAtom(wm->dpy, "_NET_WM_DESKTOP", False); + wm->net_current_desktop = XInternAtom(wm->dpy, "_NET_CURRENT_DESKTOP", False); /* advertise basic EWMH support */ Atom net_supported = XInternAtom(wm->dpy, "_NET_SUPPORTED", False); @@ -55,10 +57,12 @@ void wm_init(WM *wm) { wm->net_wm_state, wm->net_wm_state_hidden, wm->net_wm_state_maximized_vert, wm->net_wm_state_maximized_horz, + wm->net_wm_desktop, + wm->net_current_desktop, XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False), XInternAtom(wm->dpy, "_NET_CLIENT_LIST", False), + XInternAtom(wm->dpy, "_NET_CLOSE_WINDOW", False), XInternAtom(wm->dpy, "_NET_NUMBER_OF_DESKTOPS", False), - XInternAtom(wm->dpy, "_NET_CURRENT_DESKTOP", False), XInternAtom(wm->dpy, "_NET_WM_NAME", False), }; XChangeProperty(wm->dpy, wm->root, net_supported, XA_ATOM, 32, From 2a42b826fb8fb4279f2b90363121e1335b26a638 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 20:14:45 +0000 Subject: [PATCH 4/8] =?UTF-8?q?Add=206=20desktop=20features:=20workspace?= =?UTF-8?q?=20shortcuts,=20Expos=C3=A9,=20deskbar=20icons,=20notifications?= =?UTF-8?q?,=20screen=20lock,=20move-to-desktop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Super+1-4: switch virtual desktop; Super+Shift+1-4: move focused window to desktop N - Super+Space: Exposé window overview (tiled grid), click to select, Escape to cancel - wm/src/expose.c: expose_show/expose_hide using ceil(sqrt) grid layout with saved/restored geometry - client_move_to_desktop(): moves window between desktops, fires _NET_WM_DESKTOP update - on_client_message: handles _NET_WM_DESKTOP to move windows from external requests - deskbar: optional imlib2 app icon rendering (Papirus-Dark → Papirus → hicolor → pixmaps fallback) - deskbar: XGetClassHint populates cls field for better icon lookup - deskbar: context menu extended with separator + "→ Desktop 1-4" items via _NET_WM_DESKTOP - orionnotify/: freedesktop.org notification daemon (libdbus-1), stacks up to 5 popups top-right - orionlock/: PAM-authenticated fullscreen screen lock with clock, date, password dots - Lock Screen menu item wired to orionlock - Root Makefile includes orionnotify and orionlock targets Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01GHiBQKPfUoaJgCKuA9op1H --- Makefile | 17 ++- deskbar/Makefile | 8 ++ deskbar/include/deskbar.h | 8 +- deskbar/src/deskbar.c | 158 ++++++++++++++++++--- orionlock/Makefile | 24 ++++ orionlock/orionlock.c | 271 +++++++++++++++++++++++++++++++++++ orionnotify/Makefile | 22 +++ orionnotify/orionnotify.c | 292 ++++++++++++++++++++++++++++++++++++++ wm/Makefile | 4 +- wm/include/wm.h | 13 ++ wm/src/client.c | 28 ++++ wm/src/events.c | 59 +++++++- wm/src/expose.c | 93 ++++++++++++ wm/src/wm.c | 11 ++ 14 files changed, 977 insertions(+), 31 deletions(-) create mode 100644 orionlock/Makefile create mode 100644 orionlock/orionlock.c create mode 100644 orionnotify/Makefile create mode 100644 orionnotify/orionnotify.c create mode 100644 wm/src/expose.c diff --git a/Makefile b/Makefile index bdc4333..f2c71fd 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,9 @@ # make clean — remove build artifacts # make iso — build the bootable ISO (requires Arch Linux + archiso) -.PHONY: all wm deskbar launcher installer nebula orionpkg skykit rigel quill lens clean install iso +.PHONY: all wm deskbar launcher installer nebula orionpkg skykit rigel quill lens orionnotify orionlock clean install iso -all: wm deskbar launcher installer nebula orionpkg skykit rigel quill lens +all: wm deskbar launcher installer nebula orionpkg skykit rigel quill lens orionnotify orionlock wm: $(MAKE) -C wm @@ -39,6 +39,12 @@ quill: lens: $(MAKE) -C lens +orionnotify: + $(MAKE) -C orionnotify + +orionlock: + $(MAKE) -C orionlock + clean: $(MAKE) -C wm clean $(MAKE) -C deskbar clean @@ -49,7 +55,9 @@ clean: $(MAKE) -C skykit clean $(MAKE) -C rigel clean $(MAKE) -C quill clean - $(MAKE) -C lens clean + $(MAKE) -C lens clean + $(MAKE) -C orionnotify clean + $(MAKE) -C orionlock clean install: all # binaries @@ -62,6 +70,8 @@ install: all install -Dm755 rigel/rigel /usr/local/bin/rigel install -Dm755 quill/quill /usr/local/bin/quill install -Dm755 lens/lens /usr/local/bin/lens + install -Dm755 orionnotify/orionnotify /usr/local/bin/orionnotify + install -Dm755 orionlock/orionlock /usr/local/bin/orionlock # GTK theme mkdir -p /usr/share/themes cp -r themes/OrionOS /usr/share/themes/ @@ -77,6 +87,7 @@ install: all @echo " orionwallpaper" @echo " picom --config /etc/orionos/picom.conf &" @echo " oriondeskbar &" + @echo " orionnotify &" @echo " exec orionwm" iso: diff --git a/deskbar/Makefile b/deskbar/Makefile index 74f6726..18587dc 100644 --- a/deskbar/Makefile +++ b/deskbar/Makefile @@ -7,6 +7,14 @@ ifeq ($(PKG_XFT_C),) PKG_XFT_L := -L/opt/homebrew/lib -lXft -lfontconfig endif +# optional imlib2 for app icon rendering +PKG_IL_C := $(shell pkg-config --cflags imlib2 2>/dev/null) +PKG_IL_L := $(shell pkg-config --libs imlib2 2>/dev/null) +ifneq ($(PKG_IL_C),) + PKG_XFT_C += $(PKG_IL_C) -DHAVE_IMLIB2 + PKG_XFT_L += $(PKG_IL_L) +endif + CFLAGS = -Wall -Wextra -O2 -Iinclude $(PKG_XFT_C) LDFLAGS = $(PKG_XFT_L) diff --git a/deskbar/include/deskbar.h b/deskbar/include/deskbar.h index 2ebf161..8c54a3f 100644 --- a/deskbar/include/deskbar.h +++ b/deskbar/include/deskbar.h @@ -45,10 +45,12 @@ #define DB_MAX_APPS 32 /* ── per-app context menu ────────────────────────────────────────────── */ -#define DB_CTX_W 140 /* context menu width */ +#define DB_CTX_W 160 /* context menu width */ #define DB_CTX_ITEM_H 26 /* item row height */ #define DB_CTX_PAD 4 /* top/bottom padding */ -#define DB_CTX_N 3 /* number of items: Raise, Minimize, Close */ +#define DB_CTX_N 3 /* base items: Raise, Minimize, Close */ +#define DB_CTX_NDESK 4 /* number of "→ Desktop N" items */ +#define DB_CTX_SEP_H 6 /* separator line height */ /* ── fly-out menu ────────────────────────────────────────────────────── */ #define DB_FLYOUT_W 196 /* fly-out menu panel width */ @@ -102,8 +104,10 @@ static const unsigned long DB_ICON_COLORS[] = { typedef struct AppEntry { Window win; char title[64]; + char cls[64]; /* WM_CLASS instance for icon lookup */ int focused; int minimized; + int desktop; /* which desktop this window is on */ } AppEntry; typedef struct Deskbar { diff --git a/deskbar/src/deskbar.c b/deskbar/src/deskbar.c index 04e1411..06fdf60 100644 --- a/deskbar/src/deskbar.c +++ b/deskbar/src/deskbar.c @@ -1,5 +1,9 @@ #include "deskbar.h" +#ifdef HAVE_IMLIB2 +#include +#endif + #define TRAY_REQUEST_DOCK 0 #define XEMBED_EMBEDDED_NOTIFY 0 #define XEMBED_VERSION 0 @@ -141,6 +145,24 @@ void db_update_apps(Deskbar *db) { ewmh_get_title(db, wins[i], db->apps[db->napps].title, sizeof(db->apps[db->napps].title)); + + /* WM_CLASS instance for icon lookup */ + db->apps[db->napps].cls[0] = '\0'; + { + XClassHint ch = {NULL, NULL}; + if (XGetClassHint(db->dpy, wins[i], &ch)) { + if (ch.res_name) { + strncpy(db->apps[db->napps].cls, ch.res_name, + sizeof(db->apps[db->napps].cls) - 1); + XFree(ch.res_name); + } + if (ch.res_class) XFree(ch.res_class); + } + } + + /* store which desktop this window is on */ + db->apps[db->napps].desktop = (int)win_desk; + if (db->apps[db->napps].title[0] != '\0') db->napps++; } @@ -426,12 +448,52 @@ void db_draw(Deskbar *db) { XSetForeground(db->dpy, db->gc, row_bg); XFillRectangle(db->dpy, db->win, db->gc, 0, iy, DB_W, DB_ITEM_H); - /* letter icon */ - unsigned long icon_bg = db_app_icon_color(db->apps[i].title); + /* icon: imlib2 if available, else letter-square */ int icon_x = 5; int icon_y = iy + (DB_ITEM_H - DB_ICON_SZ) / 2; - draw_letter_icon(db, xd, icon_x, icon_y, - db->apps[i].title, icon_bg, mini); + int icon_drawn = 0; +#ifdef HAVE_IMLIB2 + { + static const char *dirs[] = { + "/usr/share/icons/Papirus-Dark/22x22/apps", + "/usr/share/icons/Papirus/22x22/apps", + "/usr/share/icons/hicolor/22x22/apps", + "/usr/share/icons/hicolor/32x32/apps", + "/usr/share/pixmaps", + NULL + }; + const char *names[2] = { db->apps[i].cls, db->apps[i].title }; + for (int ni = 0; ni < 2 && !icon_drawn; ni++) { + if (!names[ni][0]) continue; + for (int di = 0; dirs[di] && !icon_drawn; di++) { + char path[512]; + const char *exts[] = { "png", "svg", "xpm", NULL }; + for (int ei = 0; exts[ei] && !icon_drawn; ei++) { + snprintf(path, sizeof(path), "%s/%s.%s", + dirs[di], names[ni], exts[ei]); + imlib_context_set_display(db->dpy); + imlib_context_set_visual(db->visual); + imlib_context_set_colormap(db->cmap); + imlib_context_set_drawable(db->win); + Imlib_Image img = imlib_load_image_immediately(path); + if (img) { + imlib_context_set_image(img); + imlib_context_set_blend(1); + imlib_render_image_on_drawable_at_size( + icon_x, icon_y, DB_ICON_SZ, DB_ICON_SZ); + imlib_free_image(); + icon_drawn = 1; + } + } + } + } + } +#endif + if (!icon_drawn) { + unsigned long icon_bg = db_app_icon_color(db->apps[i].title); + draw_letter_icon(db, xd, icon_x, icon_y, + db->apps[i].title, icon_bg, mini); + } /* app name */ int name_x = icon_x + DB_ICON_SZ + 6; @@ -584,7 +646,7 @@ static const MI MENU[] = { { "Settings", NULL, 0, 1, 1 }, { NULL, NULL, 1, 0,-1 }, { "About OrionOS", "alacritty -e bash -c 'fastfetch; read' &", 0, 0,-1 }, - { "Lock Screen", "xscreensaver-command -lock &", 0, 0,-1 }, + { "Lock Screen", "orionlock &", 0, 0,-1 }, { NULL, NULL, 1, 0,-1 }, { "Restart", "systemctl reboot", 0, 0,-1 }, { "Shut Down", "systemctl poweroff", 0, 0,-1 }, @@ -818,7 +880,28 @@ static void handle_menu_press(Deskbar *db, int rx, int ry) { /* ── per-app context menu ─────────────────────────────────────────────── */ static int ctx_total_h(void) { - return DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H + DB_CTX_PAD; + return DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H + + DB_CTX_SEP_H + DB_CTX_NDESK * DB_CTX_ITEM_H + + DB_CTX_PAD; +} + +/* y offset of a context-menu item (base items, then sep, then desk items) */ +static int ctx_item_y(int idx) { + if (idx < DB_CTX_N) + return DB_CTX_PAD + idx * DB_CTX_ITEM_H; + /* desktop items come after separator */ + return DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H + + DB_CTX_SEP_H + (idx - DB_CTX_N) * DB_CTX_ITEM_H; +} + +static int ctx_item_at(int lx) { + /* lx = ry - ctx_win_y */ + int total = DB_CTX_N + DB_CTX_NDESK; + for (int i = 0; i < total; i++) { + int iy = ctx_item_y(i); + if (lx >= iy && lx < iy + DB_CTX_ITEM_H) return i; + } + return -1; } static void db_ctx_draw(Deskbar *db) { @@ -827,6 +910,8 @@ static void db_ctx_draw(Deskbar *db) { int wh = ctx_total_h(); int mini = (db->ctx_app_idx >= 0 && db->ctx_app_idx < db->napps) ? db->apps[db->ctx_app_idx].minimized : 0; + int cur_desk = (db->ctx_app_idx >= 0 && db->ctx_app_idx < db->napps) + ? db->apps[db->ctx_app_idx].desktop : -1; static const char *labels_normal[] = { "Raise", "Minimize", "Close" }; static const char *labels_mini[] = { "Restore", "Restore", "Close" }; @@ -842,8 +927,9 @@ static void db_ctx_draw(Deskbar *db) { XSetForeground(db->dpy, db->gc, db_color(db, 0x505050)); XDrawRectangle(db->dpy, db->ctx_win, db->gc, 0, 0, ww - 1, wh - 1); + /* base items */ for (int i = 0; i < DB_CTX_N; i++) { - int iy = DB_CTX_PAD + i * DB_CTX_ITEM_H; + int iy = ctx_item_y(i); if (i == db->ctx_hov) { XSetForeground(db->dpy, db->gc, db_color(db, 0x4A90D9)); @@ -851,15 +937,42 @@ static void db_ctx_draw(Deskbar *db) { 2, iy + 1, ww - 4, DB_CTX_ITEM_H - 2); } - /* "Close" item tinted red */ - unsigned long tc = (i == db->ctx_hov) ? 0xFFFFFF - : (i == DB_CTX_N - 1) ? 0xFF6B6B /* Close = red tint */ - : 0xE8E8E8; + unsigned long tc = (i == db->ctx_hov) ? 0xFFFFFF + : (i == DB_CTX_N - 1) ? 0xFF6B6B + : 0xE8E8E8; draw_str(db, xd, db->font_ui, tc, 14, str_baseline(db->font_ui, iy, DB_CTX_ITEM_H), labels[i], 0, 0); } + /* separator */ + { + int sep_y = DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H + DB_CTX_SEP_H / 2; + XSetForeground(db->dpy, db->gc, db_color(db, 0x505050)); + XDrawLine(db->dpy, db->ctx_win, db->gc, 8, sep_y, ww - 9, sep_y); + } + + /* "→ Desktop N" items */ + for (int d = 0; d < DB_CTX_NDESK; d++) { + int item_idx = DB_CTX_N + d; + int iy = ctx_item_y(item_idx); + + if (item_idx == db->ctx_hov) { + XSetForeground(db->dpy, db->gc, db_color(db, 0x4A90D9)); + XFillRectangle(db->dpy, db->ctx_win, db->gc, + 2, iy + 1, ww - 4, DB_CTX_ITEM_H - 2); + } + + char label[32]; + snprintf(label, sizeof(label), "\xe2\x86\x92 Desktop %d", d + 1); + unsigned long tc = (item_idx == db->ctx_hov) ? 0xFFFFFF + : (d == cur_desk) ? 0x88AACC /* current = dimmed */ + : 0xE8E8E8; + draw_str(db, xd, db->font_ui, tc, + 14, str_baseline(db->font_ui, iy, DB_CTX_ITEM_H), + label, 0, 0); + } + XftDrawDestroy(xd); XFlush(db->dpy); } @@ -912,9 +1025,7 @@ static void db_ctx_handle_motion(Deskbar *db, int rx, int ry) { if (db->ctx_hov != -1) { db->ctx_hov = -1; db_ctx_draw(db); } return; } - int lx = ry - cy - DB_CTX_PAD; - int idx = (lx >= 0) ? lx / DB_CTX_ITEM_H : -1; - if (idx >= DB_CTX_N) idx = -1; + int idx = ctx_item_at(ry - cy); if (idx != db->ctx_hov) { db->ctx_hov = idx; db_ctx_draw(db); } } @@ -931,13 +1042,12 @@ static void db_ctx_handle_press(Deskbar *db, int rx, int ry) { return; } - int lx = ry - cy - DB_CTX_PAD; - int idx = (lx >= 0) ? lx / DB_CTX_ITEM_H : -1; + int idx = ctx_item_at(ry - cy); Window target = db->apps[app_idx].win; - int mini = db->apps[app_idx].minimized; + int mini = db->apps[app_idx].minimized; db_ctx_close(db); - if (idx < 0 || idx >= DB_CTX_N) return; + if (idx < 0) return; XEvent ev = {0}; ev.xclient.type = ClientMessage; @@ -953,13 +1063,11 @@ static void db_ctx_handle_press(Deskbar *db, int rx, int ry) { SubstructureRedirectMask | SubstructureNotifyMask, &ev); } else if (idx == 1) { if (mini) { - /* Restore */ ev.xclient.window = target; ev.xclient.message_type = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False); ev.xclient.data.l[0] = 2; ev.xclient.data.l[1] = CurrentTime; } else { - /* Minimize via WM_CHANGE_STATE IconicState */ ev.xclient.window = target; ev.xclient.message_type = XInternAtom(db->dpy, "WM_CHANGE_STATE", False); ev.xclient.data.l[0] = 3; /* IconicState */ @@ -967,13 +1075,21 @@ static void db_ctx_handle_press(Deskbar *db, int rx, int ry) { XSendEvent(db->dpy, db->root, False, SubstructureRedirectMask | SubstructureNotifyMask, &ev); } else if (idx == 2) { - /* Close via _NET_CLOSE_WINDOW */ ev.xclient.window = target; ev.xclient.message_type = XInternAtom(db->dpy, "_NET_CLOSE_WINDOW", False); ev.xclient.data.l[0] = CurrentTime; ev.xclient.data.l[1] = 2; XSendEvent(db->dpy, db->root, False, SubstructureRedirectMask | SubstructureNotifyMask, &ev); + } else if (idx >= DB_CTX_N && idx < DB_CTX_N + DB_CTX_NDESK) { + /* Move to Desktop N via _NET_WM_DESKTOP */ + int new_desk = idx - DB_CTX_N; + ev.xclient.window = target; + ev.xclient.message_type = XInternAtom(db->dpy, "_NET_WM_DESKTOP", False); + ev.xclient.data.l[0] = new_desk; + ev.xclient.data.l[1] = 2; + XSendEvent(db->dpy, db->root, False, + SubstructureRedirectMask | SubstructureNotifyMask, &ev); } } diff --git a/orionlock/Makefile b/orionlock/Makefile new file mode 100644 index 0000000..040b239 --- /dev/null +++ b/orionlock/Makefile @@ -0,0 +1,24 @@ +CC = gcc + +PKG_C := $(shell pkg-config --cflags xft x11 2>/dev/null) +PKG_L := $(shell pkg-config --libs xft x11 2>/dev/null) + +ifeq ($(PKG_C),) + PKG_C := -I/opt/homebrew/include + PKG_L := -L/opt/homebrew/lib -lXft -lfontconfig -lX11 +endif + +CFLAGS = -Wall -Wextra -O2 $(PKG_C) +LDFLAGS = $(PKG_L) -lpam + +BIN = orionlock + +.PHONY: all clean install +all: $(BIN) +$(BIN): orionlock.c + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +install: $(BIN) + install -Dm755 $(BIN) /usr/local/bin/$(BIN) + install -Dm644 orionlock.pam /etc/pam.d/login 2>/dev/null || true +clean: + rm -f $(BIN) diff --git a/orionlock/orionlock.c b/orionlock/orionlock.c new file mode 100644 index 0000000..f806d31 --- /dev/null +++ b/orionlock/orionlock.c @@ -0,0 +1,271 @@ +/* + * orionlock — PAM-authenticated fullscreen screen lock. + * Grabs keyboard + pointer, draws clock and password entry. + * On correct password, ungrabs and exits. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define COL_BG 0x000000 +#define COL_CLOCK 0xFFFFFF +#define COL_DATE 0x8899AA +#define COL_DOT 0x4A90D9 +#define COL_ERR 0xFF5F57 +#define COL_HINT 0x555566 +#define PW_MAX 256 +#define DOT_R 5 +#define DOT_PAD 8 + +static Display *dpy; +static int screen; +static Window root, lock_win; +static Visual *visual; +static Colormap cmap; +static GC gc; +static XftFont *font_clock; +static XftFont *font_date; +static XftFont *font_ui; + +static char pw_buf[PW_MAX]; +static int pw_len = 0; +static int auth_fail = 0; +static int auth_ok = 0; + +static unsigned long alloc_color(unsigned long rgb) { + XColor c; + c.red = ((rgb >> 16) & 0xFF) * 257; + c.green = ((rgb >> 8) & 0xFF) * 257; + c.blue = ((rgb) & 0xFF) * 257; + c.flags = DoRed | DoGreen | DoBlue; + XAllocColor(dpy, cmap, &c); + return c.pixel; +} + +static void xft_color(unsigned long rgb, XftColor *out) { + XRenderColor rc; + rc.red = ((rgb >> 16) & 0xFF) * 257; + rc.green = ((rgb >> 8) & 0xFF) * 257; + rc.blue = ((rgb) & 0xFF) * 257; + rc.alpha = 0xFFFF; + XftColorAllocValue(dpy, visual, cmap, &rc, out); +} + +static void draw_lock(void) { + int sw = DisplayWidth(dpy, screen); + int sh = DisplayHeight(dpy, screen); + + XSetForeground(dpy, gc, alloc_color(COL_BG)); + XFillRectangle(dpy, lock_win, gc, 0, 0, sw, sh); + + XftDraw *xd = XftDrawCreate(dpy, lock_win, visual, cmap); + XftColor col; + XGlyphInfo ext; + + /* clock */ + { + time_t t = time(NULL); + struct tm *tm = localtime(&t); + char clock_str[16], date_str[32]; + strftime(clock_str, sizeof(clock_str), "%H:%M", tm); + strftime(date_str, sizeof(date_str), "%A, %B %d", tm); + + xft_color(COL_CLOCK, &col); + XftTextExtentsUtf8(dpy, font_clock, (const FcChar8 *)clock_str, + strlen(clock_str), &ext); + int cx = (sw - (int)ext.width) / 2; + int cy = sh / 2 - 100; + XftDrawStringUtf8(xd, &col, font_clock, cx, cy, + (const FcChar8 *)clock_str, strlen(clock_str)); + XftColorFree(dpy, visual, cmap, &col); + + xft_color(COL_DATE, &col); + XftTextExtentsUtf8(dpy, font_date, (const FcChar8 *)date_str, + strlen(date_str), &ext); + cx = (sw - (int)ext.width) / 2; + XftDrawStringUtf8(xd, &col, font_date, cx, cy + 40, + (const FcChar8 *)date_str, strlen(date_str)); + XftColorFree(dpy, visual, cmap, &col); + } + + /* password dots */ + { + int dots = pw_len; + if (dots > 20) dots = 20; + int total_w = dots * (DOT_R * 2 + DOT_PAD) - (dots > 0 ? DOT_PAD : 0); + int start_x = (sw - total_w) / 2; + int dot_y = sh / 2 + 10; + unsigned long dot_col = alloc_color(auth_fail ? COL_ERR : COL_DOT); + XSetForeground(dpy, gc, dot_col); + for (int i = 0; i < dots; i++) { + int dx = start_x + i * (DOT_R * 2 + DOT_PAD); + XFillArc(dpy, lock_win, gc, + dx, dot_y - DOT_R, DOT_R * 2, DOT_R * 2, 0, 360 * 64); + } + if (dots == 0 && pw_len == 0) { + /* hint: show lock icon placeholder */ + xft_color(COL_HINT, &col); + const char *hint = "Type password to unlock"; + XftTextExtentsUtf8(dpy, font_ui, (const FcChar8 *)hint, + strlen(hint), &ext); + int hx = (sw - (int)ext.width) / 2; + XftDrawStringUtf8(xd, &col, font_ui, hx, dot_y + DOT_R + 18, + (const FcChar8 *)hint, strlen(hint)); + XftColorFree(dpy, visual, cmap, &col); + } + } + + /* error message */ + if (auth_fail) { + xft_color(COL_ERR, &col); + const char *msg = "Incorrect password"; + XftTextExtentsUtf8(dpy, font_ui, (const FcChar8 *)msg, + strlen(msg), &ext); + int ex = (sw - (int)ext.width) / 2; + XftDrawStringUtf8(xd, &col, font_ui, ex, sh / 2 + 60, + (const FcChar8 *)msg, strlen(msg)); + XftColorFree(dpy, visual, cmap, &col); + } + + XftDrawDestroy(xd); + XFlush(dpy); +} + +/* ── PAM ─────────────────────────────────────────────────────────────── */ + +static const char *pam_pw = NULL; + +static int pam_conv_fn(int num_msg, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr) { + (void)appdata_ptr; + struct pam_response *r = calloc(num_msg, sizeof(struct pam_response)); + if (!r) return PAM_CONV_ERR; + for (int i = 0; i < num_msg; i++) { + if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF || + msg[i]->msg_style == PAM_PROMPT_ECHO_ON) { + r[i].resp = strdup(pam_pw ? pam_pw : ""); + } + } + *resp = r; + return PAM_SUCCESS; +} + +static int try_auth(const char *password) { + const char *user = getenv("USER"); + if (!user) user = "root"; + pam_pw = password; + + struct pam_conv conv = { pam_conv_fn, NULL }; + pam_handle_t *ph = NULL; + int ret = pam_start("login", user, &conv, &ph); + if (ret != PAM_SUCCESS) { pam_end(ph, ret); return 0; } + ret = pam_authenticate(ph, PAM_SILENT); + pam_end(ph, ret); + return (ret == PAM_SUCCESS); +} + +int main(void) { + dpy = XOpenDisplay(NULL); + if (!dpy) { fprintf(stderr, "orionlock: cannot open display\n"); return 1; } + screen = DefaultScreen(dpy); + root = RootWindow(dpy, screen); + visual = DefaultVisual(dpy, screen); + cmap = DefaultColormap(dpy, screen); + + int sw = DisplayWidth(dpy, screen); + int sh = DisplayHeight(dpy, screen); + + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = alloc_color(COL_BG); + swa.event_mask = ExposureMask | KeyPressMask; + lock_win = XCreateWindow(dpy, root, 0, 0, sw, sh, 0, + DefaultDepth(dpy, screen), InputOutput, + visual, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); + gc = XCreateGC(dpy, lock_win, 0, NULL); + + font_clock = XftFontOpenName(dpy, screen, "Sans:size=64:weight=bold"); + font_date = XftFontOpenName(dpy, screen, "Sans:size=20"); + font_ui = XftFontOpenName(dpy, screen, "Sans:size=11"); + if (!font_clock) font_clock = XftFontOpenName(dpy, screen, "fixed:size=20"); + if (!font_date) font_date = font_clock; + if (!font_ui) font_ui = font_clock; + + XMapRaised(dpy, lock_win); + XSync(dpy, False); + + /* grab keyboard and pointer */ + for (int t = 0; t < 10; t++) { + int r = XGrabKeyboard(dpy, lock_win, True, GrabModeAsync, GrabModeAsync, CurrentTime); + if (r == GrabSuccess) break; + usleep(100000); + } + XGrabPointer(dpy, lock_win, True, ButtonPressMask | ButtonReleaseMask | + PointerMotionMask, GrabModeAsync, GrabModeAsync, + lock_win, None, CurrentTime); + + draw_lock(); + + time_t last_tick = 0; + + while (!auth_ok) { + /* update clock every second even with no events */ + fd_set fds; + FD_ZERO(&fds); + FD_SET(ConnectionNumber(dpy), &fds); + struct timeval tv = { 1, 0 }; + select(ConnectionNumber(dpy) + 1, &fds, NULL, NULL, &tv); + + time_t now = time(NULL); + if (now != last_tick) { last_tick = now; draw_lock(); } + + while (XPending(dpy)) { + XEvent e; + XNextEvent(dpy, &e); + if (e.type == Expose && e.xexpose.count == 0) { + draw_lock(); + } else if (e.type == KeyPress) { + char buf[32] = {0}; + KeySym sym; + XLookupString(&e.xkey, buf, sizeof(buf) - 1, &sym, NULL); + + if (sym == XK_Return || sym == XK_KP_Enter) { + pw_buf[pw_len] = '\0'; + if (try_auth(pw_buf)) { + auth_ok = 1; + } else { + auth_fail = 1; + pw_len = 0; + draw_lock(); + usleep(400000); + auth_fail = 0; + } + } else if (sym == XK_BackSpace || sym == XK_Delete) { + if (pw_len > 0) pw_len--; + auth_fail = 0; + } else if (sym == XK_Escape) { + pw_len = 0; + auth_fail = 0; + } else if (buf[0] >= 0x20 && pw_len < PW_MAX - 1) { + pw_buf[pw_len++] = buf[0]; + auth_fail = 0; + } + draw_lock(); + } + } + } + + XUngrabKeyboard(dpy, CurrentTime); + XUngrabPointer(dpy, CurrentTime); + XDestroyWindow(dpy, lock_win); + XSync(dpy, False); + XCloseDisplay(dpy); + return 0; +} diff --git a/orionnotify/Makefile b/orionnotify/Makefile new file mode 100644 index 0000000..4513b7a --- /dev/null +++ b/orionnotify/Makefile @@ -0,0 +1,22 @@ +CC = gcc + +PKG_C := $(shell pkg-config --cflags dbus-1 xft x11 2>/dev/null) +PKG_L := $(shell pkg-config --libs dbus-1 xft x11 2>/dev/null) + +ifeq ($(PKG_C),) + $(error "Required: libdbus-1-dev libxft-dev libx11-dev") +endif + +CFLAGS = -Wall -Wextra -O2 $(PKG_C) +LDFLAGS = $(PKG_L) + +BIN = orionnotify + +.PHONY: all clean install +all: $(BIN) +$(BIN): orionnotify.c + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +install: $(BIN) + install -Dm755 $(BIN) /usr/local/bin/$(BIN) +clean: + rm -f $(BIN) diff --git a/orionnotify/orionnotify.c b/orionnotify/orionnotify.c new file mode 100644 index 0000000..4178a43 --- /dev/null +++ b/orionnotify/orionnotify.c @@ -0,0 +1,292 @@ +/* + * orionnotify — freedesktop.org notification daemon + * Implements org.freedesktop.Notifications via libdbus-1. + * Draws popups in the top-right corner (left of the deskbar). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DESKBAR_W 200 +#define NOTIF_W 320 +#define NOTIF_H 72 +#define NOTIF_PAD 10 +#define NOTIF_MAX 5 +#define NOTIF_GAP 6 +#define NOTIF_DEFAULT_MS 4000 + +#define COL_BG 0x2A2D36 +#define COL_BORDER 0x4A6080 +#define COL_TITLE 0xE8F0FF +#define COL_BODY 0xA0B8D0 +#define COL_ACCENT 0x1E58A8 + +typedef struct { + Window win; + char summary[128]; + char body[256]; + int expire_ms; + struct timespec show_time; + int active; +} Notif; + +static Notif notifs[NOTIF_MAX]; +static int n_notifs = 0; +static unsigned int next_id = 1; + +static Display *dpy; +static int screen; +static Visual *visual; +static Colormap cmap; +static GC gc; +static XftFont *font_title; +static XftFont *font_body; + +static unsigned long alloc_color(unsigned long rgb) { + XColor c; + c.red = ((rgb >> 16) & 0xFF) * 257; + c.green = ((rgb >> 8) & 0xFF) * 257; + c.blue = ((rgb) & 0xFF) * 257; + c.flags = DoRed | DoGreen | DoBlue; + XAllocColor(dpy, cmap, &c); + return c.pixel; +} + +static void xft_color(unsigned long rgb, XftColor *out) { + XRenderColor rc; + rc.red = ((rgb >> 16) & 0xFF) * 257; + rc.green = ((rgb >> 8) & 0xFF) * 257; + rc.blue = ((rgb) & 0xFF) * 257; + rc.alpha = 0xFFFF; + XftColorAllocValue(dpy, visual, cmap, &rc, out); +} + +static int notif_x(void) { + return DisplayWidth(dpy, screen) - DESKBAR_W - NOTIF_W - 12; +} + +static int notif_y(int slot) { + return 12 + slot * (NOTIF_H + NOTIF_GAP); +} + +static void notif_draw(int idx) { + Notif *n = ¬ifs[idx]; + if (!n->active || !n->win) return; + Window w = n->win; + + XSetForeground(dpy, gc, alloc_color(COL_BG)); + XFillRectangle(dpy, w, gc, 0, 0, NOTIF_W, NOTIF_H); + + XSetForeground(dpy, gc, alloc_color(COL_ACCENT)); + XFillRectangle(dpy, w, gc, 0, 0, 3, NOTIF_H); + + XSetForeground(dpy, gc, alloc_color(COL_BORDER)); + XDrawRectangle(dpy, w, gc, 0, 0, NOTIF_W - 1, NOTIF_H - 1); + + XftDraw *xd = XftDrawCreate(dpy, w, visual, cmap); + XftColor col; + + xft_color(COL_TITLE, &col); + XftDrawStringUtf8(xd, &col, font_title, NOTIF_PAD + 4, 24, + (const FcChar8 *)n->summary, strlen(n->summary)); + XftColorFree(dpy, visual, cmap, &col); + + xft_color(COL_BODY, &col); + XftDrawStringUtf8(xd, &col, font_body, NOTIF_PAD + 4, 50, + (const FcChar8 *)n->body, strlen(n->body)); + XftColorFree(dpy, visual, cmap, &col); + + XftDrawDestroy(xd); + XFlush(dpy); +} + +static void notif_restack(void) { + int slot = 0; + for (int i = 0; i < NOTIF_MAX; i++) { + if (!notifs[i].active) continue; + XMoveWindow(dpy, notifs[i].win, notif_x(), notif_y(slot)); + slot++; + } +} + +static void notif_close(int idx) { + if (!notifs[idx].active) return; + XDestroyWindow(dpy, notifs[idx].win); + notifs[idx].win = None; + notifs[idx].active = 0; + n_notifs--; + notif_restack(); +} + +static unsigned int notif_add(const char *summary, const char *body, int expire_ms) { + if (n_notifs >= NOTIF_MAX) { + /* evict oldest */ + for (int i = 0; i < NOTIF_MAX; i++) { + if (notifs[i].active) { notif_close(i); break; } + } + } + + int slot = -1; + for (int i = 0; i < NOTIF_MAX; i++) { + if (!notifs[i].active) { slot = i; break; } + } + if (slot < 0) return 0; + + Notif *n = ¬ifs[slot]; + snprintf(n->summary, sizeof(n->summary), "%s", summary ? summary : ""); + snprintf(n->body, sizeof(n->body), "%s", body ? body : ""); + n->expire_ms = (expire_ms <= 0) ? NOTIF_DEFAULT_MS : expire_ms; + clock_gettime(CLOCK_MONOTONIC, &n->show_time); + n->active = 1; + n_notifs++; + + int nx = notif_x(); + int ny = notif_y(n_notifs - 1); + + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = alloc_color(COL_BG); + swa.event_mask = ExposureMask | ButtonPressMask; + n->win = XCreateWindow(dpy, RootWindow(dpy, screen), + nx, ny, NOTIF_W, NOTIF_H, 0, + DefaultDepth(dpy, screen), InputOutput, + visual, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); + XMapRaised(dpy, n->win); + notif_draw(slot); + + return next_id++; +} + +static void expire_check(void) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + for (int i = 0; i < NOTIF_MAX; i++) { + if (!notifs[i].active) continue; + long elapsed_ms = (now.tv_sec - notifs[i].show_time.tv_sec) * 1000 + + (now.tv_nsec - notifs[i].show_time.tv_nsec) / 1000000; + if (elapsed_ms >= notifs[i].expire_ms) + notif_close(i); + } +} + +/* ── DBus message handling ───────────────────────────────────────────── */ + +static DBusHandlerResult handle_message(DBusConnection *conn, + DBusMessage *msg, void *data) { + (void)data; + + if (!dbus_message_is_method_call(msg, "org.freedesktop.Notifications", "Notify")) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + DBusMessageIter iter; + dbus_message_iter_init(msg, &iter); + + /* app_name */ dbus_message_iter_next(&iter); + /* replaces_id */ dbus_message_iter_next(&iter); + /* app_icon */ dbus_message_iter_next(&iter); + + const char *summary = ""; + if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING) + dbus_message_iter_get_basic(&iter, &summary); + dbus_message_iter_next(&iter); + + const char *body = ""; + if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING) + dbus_message_iter_get_basic(&iter, &body); + dbus_message_iter_next(&iter); /* actions */ + dbus_message_iter_next(&iter); /* hints */ + + dbus_int32_t expire_timeout = NOTIF_DEFAULT_MS; + if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_INT32) + dbus_message_iter_get_basic(&iter, &expire_timeout); + + unsigned int id = notif_add(summary, body, (int)expire_timeout); + + DBusMessage *reply = dbus_message_new_method_return(msg); + dbus_uint32_t uid = id; + dbus_message_append_args(reply, DBUS_TYPE_UINT32, &uid, DBUS_TYPE_INVALID); + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +int main(void) { + dpy = XOpenDisplay(NULL); + if (!dpy) { fprintf(stderr, "orionnotify: cannot open display\n"); return 1; } + screen = DefaultScreen(dpy); + visual = DefaultVisual(dpy, screen); + cmap = DefaultColormap(dpy, screen); + gc = XCreateGC(dpy, RootWindow(dpy, screen), 0, NULL); + + font_title = XftFontOpenName(dpy, screen, "Sans:size=10:weight=bold"); + font_body = XftFontOpenName(dpy, screen, "Sans:size=9"); + if (!font_title) font_title = XftFontOpenName(dpy, screen, "fixed:size=10"); + if (!font_body) font_body = font_title; + + DBusError err; + dbus_error_init(&err); + DBusConnection *conn = dbus_bus_get(DBUS_BUS_SESSION, &err); + if (!conn) { + fprintf(stderr, "orionnotify: dbus: %s\n", err.message); + return 1; + } + + int ret = dbus_bus_request_name(conn, "org.freedesktop.Notifications", + DBUS_NAME_FLAG_REPLACE_EXISTING, &err); + if (ret != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { + fprintf(stderr, "orionnotify: could not claim notification bus name\n"); + return 1; + } + + dbus_connection_add_filter(conn, handle_message, NULL, NULL); + + int dbus_fd = -1; + dbus_connection_get_unix_fd(conn, &dbus_fd); + int xfd = ConnectionNumber(dpy); + + for (;;) { + /* pump X events */ + while (XPending(dpy)) { + XEvent e; + XNextEvent(dpy, &e); + if (e.type == Expose && e.xexpose.count == 0) { + for (int i = 0; i < NOTIF_MAX; i++) + if (notifs[i].active && notifs[i].win == e.xany.window) + notif_draw(i); + } + if (e.type == ButtonPress) { + for (int i = 0; i < NOTIF_MAX; i++) + if (notifs[i].active && notifs[i].win == e.xany.window) + notif_close(i); + } + } + + /* pump dbus */ + dbus_connection_read_write(conn, 0); + DBusMessage *msg; + while ((msg = dbus_connection_pop_message(conn)) != NULL) { + handle_message(conn, msg, NULL); + dbus_message_unref(msg); + } + + expire_check(); + + /* sleep up to 200ms, woken by X or dbus */ + fd_set fds; + FD_ZERO(&fds); + FD_SET(xfd, &fds); + if (dbus_fd >= 0) FD_SET(dbus_fd, &fds); + int maxfd = xfd > dbus_fd ? xfd : dbus_fd; + struct timeval tv = { 0, 200000 }; + select(maxfd + 1, &fds, NULL, NULL, &tv); + } + + return 0; +} diff --git a/wm/Makefile b/wm/Makefile index f388297..9b08d73 100644 --- a/wm/Makefile +++ b/wm/Makefile @@ -12,9 +12,9 @@ ifeq ($(PKG_XFT_C),) endif CFLAGS = -Wall -Wextra -O2 -Iinclude $(PKG_XFT_C) -LDFLAGS = $(PKG_XFT_L) +LDFLAGS = $(PKG_XFT_L) -lm -SRC = src/wm.c src/client.c src/events.c src/draw.c src/color.c src/menu.c src/switcher.c +SRC = src/wm.c src/client.c src/events.c src/draw.c src/color.c src/menu.c src/switcher.c src/expose.c OBJ = $(SRC:.c=.o) BIN = orionwm diff --git a/wm/include/wm.h b/wm/include/wm.h index e9727be..f8ddbf5 100644 --- a/wm/include/wm.h +++ b/wm/include/wm.h @@ -82,6 +82,9 @@ typedef struct Client { int min_w, min_h; /* PMinSize */ int inc_w, inc_h; /* PResizeInc — 0 means no constraint */ int base_w, base_h; /* PBaseSize — baseline for increment calc */ + /* Exposé saved geometry */ + int exp_x, exp_y; + unsigned exp_w, exp_h; struct Client *next; } Client; @@ -141,6 +144,11 @@ typedef struct WM { Window swl_wins[64]; char swl_titles[64][128]; int swl_mini[64]; /* 1 if corresponding entry is minimized */ + + /* Exposé window overview */ + int expose_active; + int expose_count; + Window expose_wins[64]; /* frames shown in overview */ } WM; /* core */ @@ -163,6 +171,7 @@ void client_toggle_maximize(WM *wm, Client *c); void client_snap_left(WM *wm, Client *c); void client_snap_right(WM *wm, Client *c); void client_set_wm_state(WM *wm, Client *c, long state); +void client_move_to_desktop(WM *wm, Client *c, int new_desk); /* drawing */ void frame_draw(WM *wm, Client *c); @@ -190,6 +199,10 @@ void switcher_prev(WM *wm); void switcher_hide(WM *wm, int commit); void switcher_draw(WM *wm); +/* exposé */ +void expose_show(WM *wm); +void expose_hide(WM *wm, Window selected_frame); + /* events */ void on_map_request(WM *wm, XMapRequestEvent *e); void on_unmap_notify(WM *wm, XUnmapEvent *e); diff --git a/wm/src/client.c b/wm/src/client.c index d9d9644..6e815a9 100644 --- a/wm/src/client.c +++ b/wm/src/client.c @@ -328,6 +328,34 @@ void client_snap_left(WM *wm, Client *c) { frame_draw(wm, c); } +void client_move_to_desktop(WM *wm, Client *c, int new_desk) { + if (new_desk < 0 || new_desk >= 4 || c->desktop == new_desk) return; + + /* hide frame if it's on the current (now-active) desktop */ + if (c->desktop == wm->cur_desktop && !c->minimized) + XUnmapWindow(wm->dpy, c->frame); + + c->desktop = new_desk; + + /* update _NET_WM_DESKTOP */ + long desk = new_desk; + XChangeProperty(wm->dpy, c->win, wm->net_wm_desktop, XA_CARDINAL, 32, + PropModeReplace, (unsigned char *)&desk, 1); + + /* if we moved the focused window away, re-focus another */ + if (wm->focused == c) { + wm->focused->focused = 0; + frame_draw(wm, wm->focused); + wm->focused = NULL; + for (Client *n = wm->clients; n; n = n->next) { + if (!n->minimized && n->desktop == wm->cur_desktop) { + client_focus(wm, n); + break; + } + } + } +} + void client_snap_right(WM *wm, Client *c) { if (c->maximized) client_unmaximize(wm, c); int sw = DisplayWidth(wm->dpy, wm->screen); diff --git a/wm/src/events.c b/wm/src/events.c index d244864..f6c77e8 100644 --- a/wm/src/events.c +++ b/wm/src/events.c @@ -74,6 +74,7 @@ void on_expose(WM *wm, XExposeEvent *e) { if (c && !c->minimized) frame_draw(wm, c); } + void on_configure_request(WM *wm, XConfigureRequestEvent *e) { Client *c = client_find_by_win(wm, e->window); XWindowChanges wc; @@ -136,6 +137,7 @@ void on_client_message(WM *wm, XClientMessageEvent *e) { Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False); Atom net_close = XInternAtom(wm->dpy, "_NET_CLOSE_WINDOW", False); Atom wm_change_state = XInternAtom(wm->dpy, "WM_CHANGE_STATE", False); + Atom net_wm_desktop = XInternAtom(wm->dpy, "_NET_WM_DESKTOP", False); /* _NET_CURRENT_DESKTOP → switch virtual desktop */ if (e->message_type == wm->net_current_desktop && e->window == wm->root) { @@ -143,6 +145,13 @@ void on_client_message(WM *wm, XClientMessageEvent *e) { return; } + /* _NET_WM_DESKTOP → move window to desktop */ + if (e->message_type == net_wm_desktop) { + Client *c = client_find_by_win(wm, e->window); + if (c) client_move_to_desktop(wm, c, (int)e->data.l[0]); + return; + } + if (e->message_type == net_active) { Client *c = client_find_by_win(wm, e->window); if (c) client_restore(wm, c); @@ -208,6 +217,14 @@ void on_property_notify(WM *wm, XPropertyEvent *e) { } void on_button_press(WM *wm, XButtonEvent *e) { + /* Exposé — click on any frame to select and exit */ + if (wm->expose_active) { + Client *c = client_find_by_frame(wm, e->window); + if (c) expose_hide(wm, c->frame); + else expose_hide(wm, None); + return; + } + /* menu window */ if (wm->menu_visible && e->window == wm->menu_win) { if (e->button == Button1) menu_click(wm, e->x, e->y); @@ -401,9 +418,45 @@ void on_key_press(WM *wm, XKeyEvent *e) { if ((e->state & Mod4Mask) && sym == XK_Down && wm->focused) client_unmaximize(wm, wm->focused); - /* Escape — hide menu */ - if (sym == XK_Escape && wm->menu_visible) - menu_hide(wm); + /* Super+Space — Exposé window overview */ + if ((e->state & Mod4Mask) && sym == XK_space) { + if (wm->expose_active) expose_hide(wm, None); + else expose_show(wm); + return; + } + + /* Super+1-4 — switch to desktop N */ + if ((e->state & Mod4Mask) && !(e->state & ShiftMask)) { + int desk = -1; + if (sym == XK_1) desk = 0; + else if (sym == XK_2) desk = 1; + else if (sym == XK_3) desk = 2; + else if (sym == XK_4) desk = 3; + if (desk >= 0) { + if (wm->expose_active) expose_hide(wm, None); + wm_switch_desktop(wm, desk); + return; + } + } + + /* Super+Shift+1-4 — move focused window to desktop N */ + if ((e->state & Mod4Mask) && (e->state & ShiftMask) && wm->focused) { + int desk = -1; + if (sym == XK_1) desk = 0; + else if (sym == XK_2) desk = 1; + else if (sym == XK_3) desk = 2; + else if (sym == XK_4) desk = 3; + if (desk >= 0) { + client_move_to_desktop(wm, wm->focused, desk); + return; + } + } + + /* Escape — hide menu or exit Exposé */ + if (sym == XK_Escape) { + if (wm->expose_active) expose_hide(wm, None); + else if (wm->menu_visible) menu_hide(wm); + } } void on_key_release(WM *wm, XKeyEvent *e) { diff --git a/wm/src/expose.c b/wm/src/expose.c new file mode 100644 index 0000000..aeae543 --- /dev/null +++ b/wm/src/expose.c @@ -0,0 +1,93 @@ +#include "wm.h" +#include + +void expose_show(WM *wm) { + if (wm->expose_active) return; + + /* collect non-minimized windows on the current desktop */ + int n = 0; + for (Client *c = wm->clients; c; c = c->next) { + if (!c->minimized && c->desktop == wm->cur_desktop && n < 64) { + /* save original geometry */ + c->exp_x = c->x; + c->exp_y = c->y; + c->exp_w = c->w; + c->exp_h = c->h; + wm->expose_wins[n++] = c->frame; + } + } + if (n == 0) return; + + wm->expose_active = 1; + wm->expose_count = n; + + int sw = DisplayWidth(wm->dpy, wm->screen) - DESKBAR_W; + int sh = DisplayHeight(wm->dpy, wm->screen); + + int cols = (int)ceil(sqrt((double)n)); + int rows = (n + cols - 1) / cols; + int pad = 16; + int cell_w = (sw - pad * (cols + 1)) / cols; + int cell_h = (sh - pad * (rows + 1)) / rows; + + int idx = 0; + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols && idx < n; col++, idx++) { + Client *c = client_find_by_frame(wm, wm->expose_wins[idx]); + if (!c) continue; + + /* un-maximize so we can freely resize */ + if (c->maximized) { + c->maximized = 0; + XDeleteProperty(wm->dpy, c->win, wm->net_wm_state); + } + + int tx = pad + col * (cell_w + pad); + int ty = pad + row * (cell_h + pad); + int tw = cell_w; + int th = cell_h; + + c->x = tx; c->y = ty; + c->w = (unsigned)(tw - BORDER_W * 2); + c->h = (unsigned)(th - TITLE_HEIGHT - BORDER_W); + + int fw = (int)c->w + BORDER_W * 2; + int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; + XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh); + XResizeWindow(wm->dpy, c->win, c->w, c->h); + frame_draw(wm, c); + } + } + XSync(wm->dpy, False); +} + +void expose_hide(WM *wm, Window selected_frame) { + if (!wm->expose_active) return; + wm->expose_active = 0; + + Client *to_focus = NULL; + + for (int i = 0; i < wm->expose_count; i++) { + Client *c = client_find_by_frame(wm, wm->expose_wins[i]); + if (!c) continue; + + c->x = c->exp_x; + c->y = c->exp_y; + c->w = c->exp_w; + c->h = c->exp_h; + + int fw = (int)c->w + BORDER_W * 2; + int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; + XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh); + XResizeWindow(wm->dpy, c->win, c->w, c->h); + frame_draw(wm, c); + + if (c->frame == selected_frame) to_focus = c; + } + + wm->expose_count = 0; + XSync(wm->dpy, False); + + if (to_focus) client_focus(wm, to_focus); + else if (wm->focused) client_focus(wm, wm->focused); +} diff --git a/wm/src/wm.c b/wm/src/wm.c index 388237c..1868e36 100644 --- a/wm/src/wm.c +++ b/wm/src/wm.c @@ -103,6 +103,17 @@ void wm_init(WM *wm) { { XK_Right, Mod4Mask }, /* snap right */ { XK_Up, Mod4Mask }, /* maximize */ { XK_Down, Mod4Mask }, /* restore */ + { XK_space, Mod4Mask }, /* Exposé overview */ + /* Super+1-4: switch desktop */ + { XK_1, Mod4Mask }, + { XK_2, Mod4Mask }, + { XK_3, Mod4Mask }, + { XK_4, Mod4Mask }, + /* Super+Shift+1-4: move window to desktop */ + { XK_1, Mod4Mask | ShiftMask }, + { XK_2, Mod4Mask | ShiftMask }, + { XK_3, Mod4Mask | ShiftMask }, + { XK_4, Mod4Mask | ShiftMask }, { XK_Escape, 0 }, }; for (int i = 0; i < (int)(sizeof(grabs)/sizeof(grabs[0])); i++) { From a2092fd989182817089b84b8b5b730944ff44d08 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 20:47:47 +0000 Subject: [PATCH 5/8] Add hot corners, media keys, orionshot, orionclip, notification history, autostart, GTK accent update, wallpaper slideshow, ISO build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wm: hot corner polling via select()-based wm_run loop; TL=Exposé, TR=show desktop, BL=launcher, BR=lock - wm: XF86 media key grabs (volume, brightness, playback) + Super+V for clipboard - wm: autostart reads ~/.config/orionos/autostart on startup - events: media key dispatch (amixer/brightnessctl/playerctl); Super+V → orionclip --show; Print → orionshot - orionshot: new X11 screenshot tool, full-screen and rubber-band region modes, saves PNG to ~/Pictures/Screenshots/ - orionclip: new clipboard manager daemon; polls clipboard, stores 20 entries, popup via Super+V IPC - orionnotify: append-only notification history log at ~/.local/share/orionos/notif-history.log - themes: GTK accent updated from #3A6898 to #1E58A8 to match WM/deskbar - themes: orionwallpaper gains --slideshow DIR [SECS] mode using feh + sleep loop - iso: Docker-based archiso builder, live-session xinitrc, profiledef.sh - Makefile: add orionshot and orionclip to all/clean/install; Docker-based iso target --- Makefile | 21 +- iso/airootfs/etc/orionos/xinitrc | 20 ++ iso/airootfs/root/.xinitrc | 2 + iso/docker/Dockerfile | 79 ++---- iso/profiledef.sh | 21 ++ orionclip/Makefile | 22 ++ orionclip/orionclip.c | 407 +++++++++++++++++++++++++++++++ orionnotify/orionnotify.c | 22 ++ orionshot/Makefile | 22 ++ orionshot/orionshot.c | 229 +++++++++++++++++ themes/OrionOS/gtk-3.0/gtk.css | 6 +- themes/orionwallpaper | 24 +- wm/include/wm.h | 4 + wm/src/events.c | 41 +++- wm/src/wm.c | 121 +++++++-- 15 files changed, 957 insertions(+), 84 deletions(-) create mode 100644 iso/airootfs/etc/orionos/xinitrc create mode 100644 iso/airootfs/root/.xinitrc create mode 100644 iso/profiledef.sh create mode 100644 orionclip/Makefile create mode 100644 orionclip/orionclip.c create mode 100644 orionshot/Makefile create mode 100644 orionshot/orionshot.c diff --git a/Makefile b/Makefile index f2c71fd..26359d2 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,9 @@ # make clean — remove build artifacts # make iso — build the bootable ISO (requires Arch Linux + archiso) -.PHONY: all wm deskbar launcher installer nebula orionpkg skykit rigel quill lens orionnotify orionlock clean install iso +.PHONY: all wm deskbar launcher installer nebula orionpkg skykit rigel quill lens orionnotify orionlock orionshot orionclip clean install iso -all: wm deskbar launcher installer nebula orionpkg skykit rigel quill lens orionnotify orionlock +all: wm deskbar launcher installer nebula orionpkg skykit rigel quill lens orionnotify orionlock orionshot orionclip wm: $(MAKE) -C wm @@ -45,6 +45,12 @@ orionnotify: orionlock: $(MAKE) -C orionlock +orionshot: + $(MAKE) -C orionshot + +orionclip: + $(MAKE) -C orionclip + clean: $(MAKE) -C wm clean $(MAKE) -C deskbar clean @@ -58,6 +64,8 @@ clean: $(MAKE) -C lens clean $(MAKE) -C orionnotify clean $(MAKE) -C orionlock clean + $(MAKE) -C orionshot clean + $(MAKE) -C orionclip clean install: all # binaries @@ -72,6 +80,8 @@ install: all install -Dm755 lens/lens /usr/local/bin/lens install -Dm755 orionnotify/orionnotify /usr/local/bin/orionnotify install -Dm755 orionlock/orionlock /usr/local/bin/orionlock + install -Dm755 orionshot/orionshot /usr/local/bin/orionshot + install -Dm755 orionclip/orionclip /usr/local/bin/orionclip # GTK theme mkdir -p /usr/share/themes cp -r themes/OrionOS /usr/share/themes/ @@ -88,8 +98,11 @@ install: all @echo " picom --config /etc/orionos/picom.conf &" @echo " oriondeskbar &" @echo " orionnotify &" + @echo " orionclip &" @echo " exec orionwm" iso: - chmod +x build-iso.sh - ./build-iso.sh + @echo "Building OrionOS ISO via Docker..." + docker build -t orionos-iso-builder -f iso/docker/Dockerfile . + mkdir -p iso/output + docker run --rm --privileged -v "$(PWD)/iso/output:/output" orionos-iso-builder diff --git a/iso/airootfs/etc/orionos/xinitrc b/iso/airootfs/etc/orionos/xinitrc new file mode 100644 index 0000000..ecb0517 --- /dev/null +++ b/iso/airootfs/etc/orionos/xinitrc @@ -0,0 +1,20 @@ +#!/bin/bash +# OrionOS live session autostart — sourced by ~/.xinitrc on the ISO + +# Compositor +picom --config /etc/orionos/picom.conf & + +# Set wallpaper +orionwallpaper & + +# Notification daemon +orionnotify & + +# Clipboard manager +orionclip & + +# Deskbar +oriondeskbar & + +# Window manager (blocking) +exec orionwm diff --git a/iso/airootfs/root/.xinitrc b/iso/airootfs/root/.xinitrc new file mode 100644 index 0000000..75e8555 --- /dev/null +++ b/iso/airootfs/root/.xinitrc @@ -0,0 +1,2 @@ +#!/bin/bash +source /etc/orionos/xinitrc diff --git a/iso/docker/Dockerfile b/iso/docker/Dockerfile index b97a86a..9326eaf 100644 --- a/iso/docker/Dockerfile +++ b/iso/docker/Dockerfile @@ -1,58 +1,33 @@ -# OrionOS ISO builder -# Uses official Arch Linux as the base — provides pacman, pacstrap, archiso. -# Build context: the repo root (OS/) -# -# Build: docker build -t orionos-builder -f iso/docker/Dockerfile . -# Run: docker run --privileged -v $(pwd)/dist:/dist orionos-builder -# Output: dist/orionos.iso - FROM archlinux:latest -# ── disable pacman seccomp sandbox (breaks under QEMU on Apple Silicon) ── -RUN echo "DisableSandbox" >> /etc/pacman.conf +# Install archiso and build dependencies +RUN pacman -Syu --noconfirm \ + archiso \ + base-devel \ + git \ + libx11 \ + libxft \ + libxfixes \ + dbus \ + libpng \ + feh \ + picom \ + libpam \ + ttf-dejavu \ + xorg-server \ + xorg-xinit \ + xterm \ + notify-send-py \ + && pacman -Scc --noconfirm -# ── install build tools ──────────────────────────────────────────────── -RUN pacman -Syu --noconfirm && \ - pacman -S --noconfirm \ - archiso \ - base-devel \ - libx11 \ - libxft \ - fontconfig \ - freetype2 \ - imlib2 \ - imagemagick \ - fakeroot \ - git \ - sudo \ - && pacman -Scc --noconfirm +WORKDIR /build +COPY . /build/orionos -# ── build OnlyOffice from AUR (as a non-root build user) ───────────── -RUN useradd -m builder && \ - echo "builder ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/builder -RUN su builder -c " \ - git clone --depth=1 https://aur.archlinux.org/onlyoffice-bin.git /tmp/onlyoffice && \ - cd /tmp/onlyoffice && \ - makepkg -s --noconfirm 2>&1 | tail -5 \ - " && \ - find /tmp/onlyoffice -name '*.pkg.tar.zst' -exec cp {} /onlyoffice.pkg.tar.zst \; || \ - echo "OnlyOffice AUR build failed — will be skipped in ISO" +# Build OrionOS components +RUN cd /build/orionos && make && make install -# ── show which mkinitcpio hooks archiso installed (helps debugging) ─── -RUN echo "=== archiso initcpio hooks ===" && \ - ls /usr/lib/initcpio/hooks/ 2>/dev/null || echo "(none found)" +# Copy archiso profile and build the ISO +RUN cp -r /build/orionos/iso/profile /tmp/orionos-profile +RUN mkarchiso -v -w /tmp/iso-work -o /output /tmp/orionos-profile -# ── copy the entire OrionOS source tree ────────────────────────────── -WORKDIR /orionos -COPY . . - -# ── build the OrionOS binaries inside the container ─────────────────── -# pkg-config finds X11/Xft headers automatically on Arch — no overrides needed -RUN make - -# ── generate the default wallpaper ─────────────────────────────────── -RUN bash themes/wallpapers/generate-default.sh || true - -# ── run the ISO build script ────────────────────────────────────────── -# --privileged is required for loop device mounts inside the container -ENTRYPOINT ["/orionos/iso/docker/entrypoint.sh"] +CMD ["/bin/bash"] diff --git a/iso/profiledef.sh b/iso/profiledef.sh new file mode 100644 index 0000000..c54fbf3 --- /dev/null +++ b/iso/profiledef.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# archiso profile definition for OrionOS + +iso_name="OrionOS" +iso_label="ORIONOS_$(date +%Y%m)" +iso_publisher="OrionOS Project" +iso_application="OrionOS Live" +iso_version="$(date +%Y.%m)" +install_dir="arch" +buildmodes=('iso') +bootmodes=('bios.syslinux.mbr' 'bios.syslinux.eltorito' + 'uefi-ia32.grub.esp' 'uefi-x64.grub.esp' + 'uefi-ia32.grub.eltorito' 'uefi-x64.grub.eltorito') +arch="x86_64" +pacman_conf="pacman.conf" +airootfs_image_type="squashfs" +airootfs_image_tool_options=('-comp' 'zstd' '-Xcompression-level' '15') +file_permissions=( + ["/etc/orionos/xinitrc"]="0:0:755" + ["/root/.xinitrc"]="0:0:755" +) diff --git a/orionclip/Makefile b/orionclip/Makefile new file mode 100644 index 0000000..04b08bf --- /dev/null +++ b/orionclip/Makefile @@ -0,0 +1,22 @@ +CC = gcc + +PKG_C := $(shell pkg-config --cflags x11 xfixes xft 2>/dev/null) +PKG_L := $(shell pkg-config --libs x11 xfixes xft 2>/dev/null) + +ifeq ($(PKG_C),) + $(error "Required: libx11-dev libxfixes-dev libxft-dev") +endif + +CFLAGS = -Wall -Wextra -O2 $(PKG_C) +LDFLAGS = $(PKG_L) + +BIN = orionclip + +.PHONY: all clean install +all: $(BIN) +$(BIN): orionclip.c + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +install: $(BIN) + install -Dm755 $(BIN) /usr/local/bin/$(BIN) +clean: + rm -f $(BIN) diff --git a/orionclip/orionclip.c b/orionclip/orionclip.c new file mode 100644 index 0000000..577954e --- /dev/null +++ b/orionclip/orionclip.c @@ -0,0 +1,407 @@ +/* + * orionclip — clipboard manager for OrionOS + * Usage: + * orionclip run as daemon, monitors clipboard + * orionclip --show toggle the history popup (sent by Super+V hotkey) + * + * Stores up to CLIP_MAX entries. Popup appears in bottom-right corner + * (left of the deskbar). Click an entry to restore it to the clipboard. + * Press Escape or click outside to dismiss. + * + * IPC: the daemon registers the _ORIONCLIP_WIN atom on the root window. + * orionclip --show sends a ClientMessage to that window. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CLIP_MAX 20 +#define POPUP_W 340 +#define POPUP_ROW_H 28 +#define POPUP_PAD 8 +#define DESKBAR_W 200 +#define POLL_MS 300 /* clipboard poll interval in ms */ + +#define COL_BG 0x2A2D36 +#define COL_BORDER 0x4A6080 +#define COL_TEXT 0xD8E4F4 +#define COL_SEL_BG 0x1E58A8 +#define COL_SEL_FG 0xF0F8FF +#define COL_ACCENT 0x1E58A8 +#define COL_DIM 0x607080 + +static char clip_history[CLIP_MAX][512]; +static int clip_count = 0; + +static Display *dpy; +static int screen; +static Visual *visual; +static Colormap cmap; +static GC gc; +static XftFont *font; + +static Window popup_win = None; +static int popup_hovered = -1; + +static Atom atom_clip_win; /* _ORIONCLIP_WIN — IPC handle */ +static Atom atom_show; /* _ORIONCLIP_SHOW — toggle message */ +static Atom atom_utf8; +static Atom atom_clipboard; +static Atom atom_targets; + +/* ── helpers ─────────────────────────────────────────────────────────── */ + +static unsigned long xcolor(unsigned long rgb) { + XColor c; + c.red = ((rgb >> 16) & 0xFF) * 257; + c.green = ((rgb >> 8) & 0xFF) * 257; + c.blue = ((rgb) & 0xFF) * 257; + c.flags = DoRed | DoGreen | DoBlue; + XAllocColor(dpy, cmap, &c); + return c.pixel; +} + +static void xft_color(unsigned long rgb, XftColor *out) { + XRenderColor rc; + rc.red = ((rgb >> 16) & 0xFF) * 257; + rc.green = ((rgb >> 8) & 0xFF) * 257; + rc.blue = ((rgb) & 0xFF) * 257; + rc.alpha = 0xFFFF; + XftColorAllocValue(dpy, visual, cmap, &rc, out); +} + +/* ── clipboard read ──────────────────────────────────────────────────── */ + +static char *clipboard_get(void) { + /* request clipboard content via XConvertSelection */ + Window owner = XGetSelectionOwner(dpy, atom_clipboard); + if (owner == None) return NULL; + + /* use a small utility window as the requestor */ + static Window req_win = None; + if (req_win == None) + req_win = XCreateSimpleWindow(dpy, RootWindow(dpy, screen), + -10, -10, 1, 1, 0, 0, 0); + + Atom prop = XInternAtom(dpy, "_ORIONCLIP_PROP", False); + XConvertSelection(dpy, atom_clipboard, atom_utf8, prop, req_win, CurrentTime); + + /* wait up to ~300ms for SelectionNotify */ + struct timespec deadline, now; + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_nsec += 300000000L; + if (deadline.tv_nsec >= 1000000000L) { + deadline.tv_nsec -= 1000000000L; + deadline.tv_sec++; + } + + for (;;) { + if (XPending(dpy)) { + XEvent e; + XNextEvent(dpy, &e); + if (e.type == SelectionNotify && e.xselection.requestor == req_win) { + if (e.xselection.property == None) return NULL; + Atom type; int fmt; unsigned long nitems, left; + unsigned char *data = NULL; + XGetWindowProperty(dpy, req_win, prop, 0, 65536/4, True, + atom_utf8, &type, &fmt, &nitems, &left, &data); + if (!data) return NULL; + char *copy = strndup((char *)data, nitems < 511 ? nitems : 511); + XFree(data); + return copy; + } + } + clock_gettime(CLOCK_MONOTONIC, &now); + if (now.tv_sec > deadline.tv_sec || + (now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) + break; + usleep(10000); + } + return NULL; +} + +static void clipboard_set(const char *text) { + /* put text on the clipboard by becoming the selection owner */ + static Window sel_win = None; + static char sel_buf[512]; + + if (sel_win == None) + sel_win = XCreateSimpleWindow(dpy, RootWindow(dpy, screen), + -10, -10, 1, 1, 0, 0, 0); + strncpy(sel_buf, text, 511); sel_buf[511] = '\0'; + XSetSelectionOwner(dpy, atom_clipboard, sel_win, CurrentTime); + /* SelectionRequest events are handled in the main event loop */ + (void)sel_buf; +} + +/* ── history management ──────────────────────────────────────────────── */ + +static void history_push(const char *text) { + if (!text || !*text) return; + /* deduplicate: if same text is already entry 0, skip */ + if (clip_count > 0 && strcmp(clip_history[0], text) == 0) return; + /* shift down */ + int top = clip_count < CLIP_MAX ? clip_count : CLIP_MAX - 1; + for (int i = top; i > 0; i--) + memcpy(clip_history[i], clip_history[i-1], sizeof(clip_history[0])); + strncpy(clip_history[0], text, 511); + clip_history[0][511] = '\0'; + if (clip_count < CLIP_MAX) clip_count++; +} + +/* ── popup ───────────────────────────────────────────────────────────── */ + +static int popup_h(void) { + return POPUP_PAD * 2 + clip_count * POPUP_ROW_H; +} + +static void popup_draw(void) { + if (!popup_win) return; + int ph = popup_h(); + + XSetForeground(dpy, gc, xcolor(COL_BG)); + XFillRectangle(dpy, popup_win, gc, 0, 0, POPUP_W, ph); + + XSetForeground(dpy, gc, xcolor(COL_ACCENT)); + XFillRectangle(dpy, popup_win, gc, 0, 0, 3, ph); + + XSetForeground(dpy, gc, xcolor(COL_BORDER)); + XDrawRectangle(dpy, popup_win, gc, 0, 0, POPUP_W - 1, ph - 1); + + XftDraw *xd = XftDrawCreate(dpy, popup_win, visual, cmap); + + for (int i = 0; i < clip_count; i++) { + int ry = POPUP_PAD + i * POPUP_ROW_H; + if (i == popup_hovered) { + XSetForeground(dpy, gc, xcolor(COL_SEL_BG)); + XFillRectangle(dpy, popup_win, gc, 4, ry, POPUP_W - 8, POPUP_ROW_H - 2); + } + + unsigned long text_col = (i == popup_hovered) ? COL_SEL_FG : COL_TEXT; + if (i > 0) text_col = COL_DIM; + + XftColor col; + xft_color(text_col, &col); + + /* truncate display text */ + char disp[64]; + strncpy(disp, clip_history[i], 63); disp[63] = '\0'; + /* replace newlines with spaces */ + for (char *p = disp; *p; p++) if (*p == '\n' || *p == '\r' || *p == '\t') *p = ' '; + + XftDrawStringUtf8(xd, &col, font, 12, ry + POPUP_ROW_H - 7, + (const FcChar8 *)disp, strlen(disp)); + XftColorFree(dpy, visual, cmap, &col); + } + + XftDrawDestroy(xd); + XFlush(dpy); +} + +static void popup_show(void) { + if (popup_win != None) { popup_draw(); return; } + if (clip_count == 0) return; + + int ph = popup_h(); + int sw = DisplayWidth(dpy, screen); + int sh = DisplayHeight(dpy, screen); + int px = sw - DESKBAR_W - POPUP_W - 12; + int py = sh - ph - 40; + + XSetWindowAttributes swa; + swa.override_redirect = True; + swa.background_pixel = xcolor(COL_BG); + swa.event_mask = ExposureMask | ButtonPressMask | + PointerMotionMask | LeaveWindowMask; + popup_win = XCreateWindow(dpy, RootWindow(dpy, screen), + px, py, POPUP_W, ph, 0, + DefaultDepth(dpy, screen), InputOutput, + visual, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); + XMapRaised(dpy, popup_win); + popup_draw(); +} + +static void popup_hide(void) { + if (popup_win == None) return; + XDestroyWindow(dpy, popup_win); + popup_win = None; + popup_hovered = -1; + XFlush(dpy); +} + +static void popup_toggle(void) { + if (popup_win != None) popup_hide(); + else popup_show(); +} + +static int popup_row_at(int y) { + int row = (y - POPUP_PAD) / POPUP_ROW_H; + if (row < 0 || row >= clip_count) return -1; + return row; +} + +/* ── --show mode: send IPC to daemon ─────────────────────────────────── */ + +static void send_show(void) { + Display *d = XOpenDisplay(NULL); + if (!d) { fprintf(stderr, "orionclip: cannot open display\n"); return; } + Atom a = XInternAtom(d, "_ORIONCLIP_WIN", True); + if (a == None) { XCloseDisplay(d); return; } + + Atom type; int fmt; unsigned long n, left; + unsigned char *data = NULL; + XGetWindowProperty(d, RootWindow(d, DefaultScreen(d)), a, 0, 1, False, + XA_WINDOW, &type, &fmt, &n, &left, &data); + if (!data) { XCloseDisplay(d); return; } + + Window target = *(Window *)data; + XFree(data); + + Atom show_atom = XInternAtom(d, "_ORIONCLIP_SHOW", False); + XClientMessageEvent ev = {0}; + ev.type = ClientMessage; + ev.window = target; + ev.message_type = show_atom; + ev.format = 32; + XSendEvent(d, target, False, 0, (XEvent *)&ev); + XFlush(d); + XCloseDisplay(d); +} + +/* ── main daemon ─────────────────────────────────────────────────────── */ + +int main(int argc, char *argv[]) { + if (argc > 1 && strcmp(argv[1], "--show") == 0) { + send_show(); + return 0; + } + + dpy = XOpenDisplay(NULL); + if (!dpy) { fprintf(stderr, "orionclip: cannot open display\n"); return 1; } + screen = DefaultScreen(dpy); + visual = DefaultVisual(dpy, screen); + cmap = DefaultColormap(dpy, screen); + gc = XCreateGC(dpy, RootWindow(dpy, screen), 0, NULL); + + font = XftFontOpenName(dpy, screen, "Sans:size=9"); + if (!font) font = XftFontOpenName(dpy, screen, "fixed:size=9"); + + atom_utf8 = XInternAtom(dpy, "UTF8_STRING", False); + atom_clipboard = XInternAtom(dpy, "CLIPBOARD", False); + atom_targets = XInternAtom(dpy, "TARGETS", False); + atom_clip_win = XInternAtom(dpy, "_ORIONCLIP_WIN", False); + atom_show = XInternAtom(dpy, "_ORIONCLIP_SHOW", False); + + /* register ourselves on the root window */ + Window self = XCreateSimpleWindow(dpy, RootWindow(dpy, screen), + -10, -10, 1, 1, 0, 0, 0); + XSelectInput(dpy, self, StructureNotifyMask | PropertyChangeMask); + XChangeProperty(dpy, RootWindow(dpy, screen), atom_clip_win, XA_WINDOW, 32, + PropModeReplace, (unsigned char *)&self, 1); + XFlush(dpy); + + int xfd = ConnectionNumber(dpy); + struct timespec last_poll = {0, 0}; + + for (;;) { + /* pump X events */ + while (XPending(dpy)) { + XEvent e; + XNextEvent(dpy, &e); + + /* IPC: show/hide toggle */ + if (e.type == ClientMessage && + (Atom)e.xclient.message_type == atom_show) { + popup_toggle(); + continue; + } + + /* selection ownership request — serve clipboard content */ + if (e.type == SelectionRequest) { + XSelectionRequestEvent *req = &e.xselectionrequest; + XSelectionEvent sev = {0}; + sev.type = SelectionNotify; + sev.requestor = req->requestor; + sev.selection = req->selection; + sev.target = req->target; + sev.time = req->time; + sev.property = None; + if (clip_count > 0) { + if (req->target == atom_targets) { + Atom targets[2] = { atom_targets, atom_utf8 }; + XChangeProperty(dpy, req->requestor, req->property, XA_ATOM, 32, + PropModeReplace, (unsigned char *)targets, 2); + sev.property = req->property; + } else if (req->target == atom_utf8) { + XChangeProperty(dpy, req->requestor, req->property, atom_utf8, 8, + PropModeReplace, + (unsigned char *)clip_history[0], + strlen(clip_history[0])); + sev.property = req->property; + } + } + XSendEvent(dpy, req->requestor, False, 0, (XEvent *)&sev); + continue; + } + + if (popup_win == None) continue; + + if (e.type == Expose && e.xexpose.count == 0 && + e.xany.window == popup_win) + popup_draw(); + + if (e.type == LeaveNotify && e.xany.window == popup_win) { + popup_hovered = -1; + popup_draw(); + } + + if (e.type == MotionNotify && e.xany.window == popup_win) { + int row = popup_row_at(e.xmotion.y); + if (row != popup_hovered) { + popup_hovered = row; + popup_draw(); + } + } + + if (e.type == ButtonPress && e.xany.window == popup_win) { + int row = popup_row_at(e.xbutton.y); + if (row >= 0 && row < clip_count) { + /* move chosen entry to top and set as clipboard */ + char chosen[512]; + strncpy(chosen, clip_history[row], 511); chosen[511] = '\0'; + history_push(chosen); + clipboard_set(clip_history[0]); + } + popup_hide(); + } + } + + /* poll clipboard every POLL_MS ms */ + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + long elapsed_ms = (now.tv_sec - last_poll.tv_sec) * 1000 + + (now.tv_nsec - last_poll.tv_nsec) / 1000000; + if (elapsed_ms >= POLL_MS) { + last_poll = now; + char *text = clipboard_get(); + if (text) { + history_push(text); + free(text); + } + } + + fd_set fds; + FD_ZERO(&fds); + FD_SET(xfd, &fds); + struct timeval tv = { 0, POLL_MS * 1000 }; + select(xfd + 1, &fds, NULL, NULL, &tv); + } + + return 0; +} diff --git a/orionnotify/orionnotify.c b/orionnotify/orionnotify.c index 4178a43..15fed93 100644 --- a/orionnotify/orionnotify.c +++ b/orionnotify/orionnotify.c @@ -12,6 +12,7 @@ #include #include #include +#include #define DESKBAR_W 200 #define NOTIF_W 320 @@ -124,6 +125,26 @@ static void notif_close(int idx) { notif_restack(); } +static void notif_log(const char *summary, const char *body) { + const char *home = getenv("HOME"); + if (!home) return; + char dir[512], path[560]; + snprintf(dir, sizeof(dir), "%s/.local/share/orionos", home); + snprintf(path, sizeof(path), "%s/notif-history.log", dir); + mkdir(dir, 0755); + FILE *f = fopen(path, "a"); + if (!f) return; + time_t t = time(NULL); + /* sanitise: replace tabs and newlines with spaces */ + char sum[128], bod[256]; + strncpy(sum, summary ? summary : "", 127); sum[127] = '\0'; + strncpy(bod, body ? body : "", 255); bod[255] = '\0'; + for (char *p = sum; *p; p++) if (*p == '\t' || *p == '\n') *p = ' '; + for (char *p = bod; *p; p++) if (*p == '\t' || *p == '\n') *p = ' '; + fprintf(f, "%ld\t%s\t%s\n", (long)t, sum, bod); + fclose(f); +} + static unsigned int notif_add(const char *summary, const char *body, int expire_ms) { if (n_notifs >= NOTIF_MAX) { /* evict oldest */ @@ -145,6 +166,7 @@ static unsigned int notif_add(const char *summary, const char *body, int expire_ clock_gettime(CLOCK_MONOTONIC, &n->show_time); n->active = 1; n_notifs++; + notif_log(summary, body); int nx = notif_x(); int ny = notif_y(n_notifs - 1); diff --git a/orionshot/Makefile b/orionshot/Makefile new file mode 100644 index 0000000..a427e3f --- /dev/null +++ b/orionshot/Makefile @@ -0,0 +1,22 @@ +CC = gcc + +PKG_C := $(shell pkg-config --cflags x11 libpng 2>/dev/null) +PKG_L := $(shell pkg-config --libs x11 libpng 2>/dev/null) + +ifeq ($(PKG_C),) + $(error "Required: libx11-dev libpng-dev") +endif + +CFLAGS = -Wall -Wextra -O2 $(PKG_C) +LDFLAGS = $(PKG_L) + +BIN = orionshot + +.PHONY: all clean install +all: $(BIN) +$(BIN): orionshot.c + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +install: $(BIN) + install -Dm755 $(BIN) /usr/local/bin/$(BIN) +clean: + rm -f $(BIN) diff --git a/orionshot/orionshot.c b/orionshot/orionshot.c new file mode 100644 index 0000000..97f0ff1 --- /dev/null +++ b/orionshot/orionshot.c @@ -0,0 +1,229 @@ +/* + * orionshot — X11 screenshot tool for OrionOS + * Usage: orionshot full full-screen PNG capture + * orionshot region interactive rubber-band region select + * + * Saves to ~/Pictures/Screenshots/orionos_YYYYMMDD_HHMMSS.png + * Sends a desktop notification on success. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef MIN +#define MIN(a,b) ((a)<(b)?(a):(b)) +#endif + +/* ── PNG writer ──────────────────────────────────────────────────────── */ + +static int get_shift(unsigned long mask) { + if (!mask) return 0; + int s = 0; + while (!(mask & 1)) { mask >>= 1; s++; } + return s; +} + +static int write_png(const char *path, XImage *img, int ox, int oy, int w, int h) { + FILE *fp = fopen(path, "wb"); + if (!fp) { perror("orionshot: fopen"); return -1; } + + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png) { fclose(fp); return -1; } + png_infop info = png_create_info_struct(png); + if (!info) { png_destroy_write_struct(&png, NULL); fclose(fp); return -1; } + if (setjmp(png_jmpbuf(png))) { + png_destroy_write_struct(&png, &info); + fclose(fp); + return -1; + } + + png_init_io(png, fp); + png_set_IHDR(png, info, (unsigned)w, (unsigned)h, 8, + PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + png_write_info(png, info); + + int r_shift = get_shift(img->red_mask); + int g_shift = get_shift(img->green_mask); + int b_shift = get_shift(img->blue_mask); + int r_scale = (img->red_mask >> r_shift) < 0xFF ? 2 : 1; + int g_scale = (img->green_mask >> g_shift) < 0xFF ? 2 : 1; + int b_scale = (img->blue_mask >> b_shift) < 0xFF ? 2 : 1; + (void)r_scale; (void)g_scale; (void)b_scale; + + unsigned char *row = malloc((size_t)w * 3); + if (!row) { png_destroy_write_struct(&png, &info); fclose(fp); return -1; } + + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i++) { + unsigned long px = XGetPixel(img, ox + i, oy + j); + row[i*3+0] = (unsigned char)((px & img->red_mask) >> r_shift); + row[i*3+1] = (unsigned char)((px & img->green_mask) >> g_shift); + row[i*3+2] = (unsigned char)((px & img->blue_mask) >> b_shift); + } + png_write_row(png, row); + } + free(row); + png_write_end(png, NULL); + png_destroy_write_struct(&png, &info); + fclose(fp); + return 0; +} + +/* ── output path ─────────────────────────────────────────────────────── */ + +static void make_save_path(char *buf, size_t bufsz) { + const char *home = getenv("HOME"); + if (!home) home = "/tmp"; + char dir[512]; + snprintf(dir, sizeof(dir), "%s/Pictures/Screenshots", home); + mkdir(dir, 0755); + + time_t t = time(NULL); + struct tm *tm = localtime(&t); + char ts[32]; + strftime(ts, sizeof(ts), "%Y%m%d_%H%M%S", tm); + snprintf(buf, bufsz, "%s/orionos_%s.png", dir, ts); +} + +/* ── region selection ────────────────────────────────────────────────── */ + +static int select_region(Display *dpy, Window root, + int *rx, int *ry, int *rw, int *rh) { + Cursor cross = XCreateFontCursor(dpy, XC_crosshair); + if (XGrabPointer(dpy, root, False, + ButtonPressMask | ButtonReleaseMask | PointerMotionMask, + GrabModeAsync, GrabModeAsync, + root, cross, CurrentTime) != GrabSuccess) { + XFreeCursor(dpy, cross); + return -1; + } + XGrabKeyboard(dpy, root, False, GrabModeAsync, GrabModeAsync, CurrentTime); + + GC gc; + XGCValues gcv; + gcv.function = GXxor; + gcv.foreground = 0xFFFFFF; + gcv.subwindow_mode = IncludeInferiors; + gcv.line_width = 1; + gc = XCreateGC(dpy, root, + GCFunction | GCForeground | GCSubwindowMode | GCLineWidth, &gcv); + + int sx = 0, sy = 0, ex = 0, ey = 0; + int started = 0, drawn = 0, done = 0, cancelled = 0; + + while (!done) { + XEvent e; + XNextEvent(dpy, &e); + + if (e.type == ButtonPress && e.xbutton.button == Button1) { + sx = e.xbutton.x_root; sy = e.xbutton.y_root; + ex = sx; ey = sy; started = 1; + + } else if (e.type == MotionNotify && started) { + /* erase previous rubber band */ + if (drawn) { + int dx = MIN(sx,ex), dy = MIN(sy,ey); + int dw = abs(ex-sx), dh = abs(ey-sy); + if (dw > 0 && dh > 0) + XDrawRectangle(dpy, root, gc, dx, dy, (unsigned)dw, (unsigned)dh); + XFlush(dpy); drawn = 0; + } + ex = e.xmotion.x_root; ey = e.xmotion.y_root; + { + int dx = MIN(sx,ex), dy = MIN(sy,ey); + int dw = abs(ex-sx), dh = abs(ey-sy); + if (dw > 0 && dh > 0) { + XDrawRectangle(dpy, root, gc, dx, dy, (unsigned)dw, (unsigned)dh); + XFlush(dpy); drawn = 1; + } + } + + } else if (e.type == ButtonRelease && e.xbutton.button == Button1) { + if (drawn) { + int dx = MIN(sx,ex), dy = MIN(sy,ey); + int dw = abs(ex-sx), dh = abs(ey-sy); + if (dw > 0 && dh > 0) + XDrawRectangle(dpy, root, gc, dx, dy, (unsigned)dw, (unsigned)dh); + XFlush(dpy); + } + ex = e.xbutton.x_root; ey = e.xbutton.y_root; + done = 1; + + } else if (e.type == KeyPress) { + cancelled = 1; done = 1; + } + } + + XUngrabPointer(dpy, CurrentTime); + XUngrabKeyboard(dpy, CurrentTime); + XFreeGC(dpy, gc); + XFreeCursor(dpy, cross); + XSync(dpy, False); + + if (cancelled) return -1; + + *rx = MIN(sx,ex); *ry = MIN(sy,ey); + *rw = abs(ex-sx); *rh = abs(ey-sy); + if (*rw < 2 || *rh < 2) return -1; + return 0; +} + +/* ── main ────────────────────────────────────────────────────────────── */ + +int main(int argc, char *argv[]) { + int do_region = (argc > 1 && strcmp(argv[1], "region") == 0); + + Display *dpy = XOpenDisplay(NULL); + if (!dpy) { fprintf(stderr, "orionshot: cannot open display\n"); return 1; } + int screen = DefaultScreen(dpy); + Window root = RootWindow(dpy, screen); + int sw = DisplayWidth(dpy, screen); + int sh = DisplayHeight(dpy, screen); + + int cx = 0, cy = 0, cw = sw, ch = sh; + + if (do_region) { + if (select_region(dpy, root, &cx, &cy, &cw, &ch) < 0) { + XCloseDisplay(dpy); + return 0; /* cancelled — silently exit */ + } + /* tiny pause so rubber-band XOR erasure is fully flushed */ + usleep(60000); + } + + XImage *img = XGetImage(dpy, root, cx, cy, (unsigned)cw, (unsigned)ch, + AllPlanes, ZPixmap); + XCloseDisplay(dpy); + + if (!img) { fprintf(stderr, "orionshot: XGetImage failed\n"); return 1; } + + char path[1024]; + make_save_path(path, sizeof(path)); + + if (write_png(path, img, 0, 0, cw, ch) < 0) { + XDestroyImage(img); + fprintf(stderr, "orionshot: failed to write %s\n", path); + return 1; + } + XDestroyImage(img); + + /* extract just the filename for the notification body */ + const char *fname = strrchr(path, '/'); + fname = fname ? fname + 1 : path; + + char notif[512]; + snprintf(notif, sizeof(notif), + "notify-send -t 3000 'Screenshot' '%s' 2>/dev/null &", fname); + system(notif); + + fprintf(stderr, "orionshot: saved %s\n", path); + return 0; +} diff --git a/themes/OrionOS/gtk-3.0/gtk.css b/themes/OrionOS/gtk-3.0/gtk.css index 17337f9..7000f82 100644 --- a/themes/OrionOS/gtk-3.0/gtk.css +++ b/themes/OrionOS/gtk-3.0/gtk.css @@ -4,13 +4,13 @@ @define-color bg_dark #161C2A; @define-color bg_darker #0E1220; @define-color bg_light #262E42; -@define-color border_light #3A6898; +@define-color border_light #2A60A8; @define-color border_dark #080C18; @define-color text_color #D8E4F4; @define-color text_disabled #48587A; -@define-color selected_bg #1E4D78; +@define-color selected_bg #1E4878; @define-color selected_fg #F0F6FF; -@define-color accent #3A6898; +@define-color accent #1E58A8; @define-color button_bg #202A3C; @define-color button_hover #283650; @define-color button_active #181E2E; diff --git a/themes/orionwallpaper b/themes/orionwallpaper index 4807304..6927c1f 100644 --- a/themes/orionwallpaper +++ b/themes/orionwallpaper @@ -2,10 +2,11 @@ # orionwallpaper — set or restore the OrionOS desktop wallpaper # # Usage: -# orionwallpaper # restore saved wallpaper (called from xinitrc) -# orionwallpaper /path/to/image.jpg # set new wallpaper and save it -# orionwallpaper --color "#6B8FB5" # set solid color -# orionwallpaper --reset # reset to default blue-gray solid color +# orionwallpaper # restore saved wallpaper (called from xinitrc) +# orionwallpaper /path/to/image.jpg # set new wallpaper and save it +# orionwallpaper --color "#6B8FB5" # set solid color +# orionwallpaper --reset # reset to default blue-gray solid color +# orionwallpaper --slideshow DIR [SECS] # rotate images in DIR every SECS seconds (default 300) # # Wallpaper preference is stored in ~/.config/orionos/wallpaper @@ -17,6 +18,21 @@ WALLPAPER_DIR="/usr/share/orionos/wallpapers" mkdir -p "$CONF_DIR" case "${1:-}" in + --slideshow) + DIR="${2:-$WALLPAPER_DIR}" + SECS="${3:-300}" + if [[ ! -d "$DIR" ]]; then + echo "Error: directory not found: $DIR" >&2 + exit 1 + fi + while true; do + IMG="$(find "$DIR" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | shuf -n1)" + if [[ -n "$IMG" ]]; then + feh --no-fehbg --bg-fill "$IMG" + fi + sleep "$SECS" + done + ;; --reset) echo "color:$DEFAULT_COLOR" > "$CONF_FILE" xsetroot -solid "$DEFAULT_COLOR" diff --git a/wm/include/wm.h b/wm/include/wm.h index f8ddbf5..8deca3e 100644 --- a/wm/include/wm.h +++ b/wm/include/wm.h @@ -5,12 +5,15 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include /* ── palette (macOS dark mode) ───────────────────────────────────────── */ #define COLOR_TITLE_ACTIVE 0x3C3C3C /* macOS dark titlebar */ @@ -113,6 +116,7 @@ typedef struct WM { Client *focused; int cur_desktop; /* active virtual desktop (0-based) */ + int hot_corner_last; /* last hot-corner zone (0=none,1-4) */ Atom wm_delete_window; Atom wm_protocols; diff --git a/wm/src/events.c b/wm/src/events.c index f6c77e8..286c322 100644 --- a/wm/src/events.c +++ b/wm/src/events.c @@ -392,16 +392,47 @@ void on_key_press(WM *wm, XKeyEvent *e) { if ((e->state & Mod1Mask) && sym == XK_F1) system("orionlauncher &"); - /* PrintScreen — full screenshot to ~/Pictures */ + /* PrintScreen — screenshot */ if (sym == XK_Print) { if (e->state & ShiftMask) - system("mkdir -p ~/Pictures && scrot -s ~/Pictures/screenshot_%Y%m%d_%H%M%S.png" - " && notify-send 'Screenshot' 'Region saved to ~/Pictures' &"); + system("orionshot region &"); else - system("mkdir -p ~/Pictures && scrot ~/Pictures/screenshot_%Y%m%d_%H%M%S.png" - " && notify-send 'Screenshot' 'Saved to ~/Pictures' &"); + system("orionshot full &"); } + /* XF86 media keys */ + if (sym == XF86XK_AudioRaiseVolume) + system("pactl set-sink-volume @DEFAULT_SINK@ +5% 2>/dev/null; " + "pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | " + "grep -oP '[0-9]+%' | head -1 | " + "xargs -I{} notify-send -t 1500 'Volume' '{}' 2>/dev/null &"); + if (sym == XF86XK_AudioLowerVolume) + system("pactl set-sink-volume @DEFAULT_SINK@ -5% 2>/dev/null; " + "pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | " + "grep -oP '[0-9]+%' | head -1 | " + "xargs -I{} notify-send -t 1500 'Volume' '{}' 2>/dev/null &"); + if (sym == XF86XK_AudioMute) + system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null; " + "notify-send -t 1500 'Volume' 'Muted toggled' 2>/dev/null &"); + if (sym == XF86XK_AudioPlay) + system("playerctl play-pause 2>/dev/null &"); + if (sym == XF86XK_AudioNext) + system("playerctl next 2>/dev/null &"); + if (sym == XF86XK_AudioPrev) + system("playerctl previous 2>/dev/null &"); + if (sym == XF86XK_MonBrightnessUp) + system("brightnessctl set +10% 2>/dev/null; " + "notify-send -t 1200 'Brightness' " + "\"$(brightnessctl g 2>/dev/null)\" 2>/dev/null &"); + if (sym == XF86XK_MonBrightnessDown) + system("brightnessctl set 10%- 2>/dev/null; " + "notify-send -t 1200 'Brightness' " + "\"$(brightnessctl g 2>/dev/null)\" 2>/dev/null &"); + + /* Super+V — clipboard manager */ + if ((e->state & Mod4Mask) && sym == XK_v) + system("orionclip --show &"); + /* Super+Left — snap left */ if ((e->state & Mod4Mask) && sym == XK_Left && wm->focused) client_snap_left(wm, wm->focused); diff --git a/wm/src/wm.c b/wm/src/wm.c index 1868e36..75e59cf 100644 --- a/wm/src/wm.c +++ b/wm/src/wm.c @@ -1,5 +1,67 @@ #include "wm.h" +/* ── autostart ───────────────────────────────────────────────────── */ +static void wm_run_autostart(void) { + const char *home = getenv("HOME"); + if (!home) return; + char path[512]; + snprintf(path, sizeof(path), "%s/.config/orionos/autostart", home); + FILE *f = fopen(path, "r"); + if (!f) return; + char line[512]; + while (fgets(line, sizeof(line), f)) { + size_t len = strlen(line); + while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r' || + line[len-1] == ' ' || line[len-1] == '\t')) + line[--len] = '\0'; + if (len == 0 || line[0] == '#') continue; + char cmd[560]; + snprintf(cmd, sizeof(cmd), "%s &", line); + system(cmd); + } + fclose(f); +} + +/* ── hot corners ─────────────────────────────────────────────────── */ +static void hot_corner_check(WM *wm) { + Window root_ret, child; + int root_x, root_y, win_x, win_y; + unsigned int mask; + XQueryPointer(wm->dpy, wm->root, &root_ret, &child, + &root_x, &root_y, &win_x, &win_y, &mask); + + int sw = DisplayWidth(wm->dpy, wm->screen); + int sh = DisplayHeight(wm->dpy, wm->screen); + + int corner = 0; + if (root_x <= 1 && root_y <= 1) corner = 1; /* TL: Exposé */ + else if (root_x >= sw - DESKBAR_W - 2 && root_y <= 1) corner = 2; /* TR: show desktop */ + else if (root_x <= 1 && root_y >= sh - 2) corner = 3; /* BL: launcher */ + else if (root_x >= sw - DESKBAR_W - 2 && root_y >= sh - 2) corner = 4; /* BR: lock */ + + if (corner == wm->hot_corner_last) return; + wm->hot_corner_last = corner; + if (corner == 0) return; /* just left a corner, no action needed */ + + switch (corner) { + case 1: /* top-left → Exposé */ + if (!wm->expose_active) expose_show(wm); + break; + case 2: /* top-right → show desktop (minimize all on current desktop) */ + if (wm->expose_active) { expose_hide(wm, None); break; } + for (Client *c = wm->clients; c; c = c->next) + if (!c->minimized && c->desktop == wm->cur_desktop) + client_minimize(wm, c); + break; + case 3: /* bottom-left → launcher */ + system("orionlauncher &"); + break; + case 4: /* bottom-right → lock screen */ + system("orionlock &"); + break; + } +} + static int x_error_handler(Display *dpy, XErrorEvent *e) { if (e->error_code == BadWindow || e->error_code == BadAccess) return 0; @@ -115,6 +177,17 @@ void wm_init(WM *wm) { { XK_3, Mod4Mask | ShiftMask }, { XK_4, Mod4Mask | ShiftMask }, { XK_Escape, 0 }, + /* XF86 media keys */ + { XF86XK_AudioRaiseVolume, 0 }, + { XF86XK_AudioLowerVolume, 0 }, + { XF86XK_AudioMute, 0 }, + { XF86XK_AudioPlay, 0 }, + { XF86XK_AudioNext, 0 }, + { XF86XK_AudioPrev, 0 }, + { XF86XK_MonBrightnessUp, 0 }, + { XF86XK_MonBrightnessDown, 0 }, + /* Super+V: clipboard manager */ + { XK_v, Mod4Mask }, }; for (int i = 0; i < (int)(sizeof(grabs)/sizeof(grabs[0])); i++) { KeyCode kc = XKeysymToKeycode(wm->dpy, grabs[i].sym); @@ -138,28 +211,44 @@ void wm_init(WM *wm) { XSync(wm->dpy, False); fprintf(stderr, "orionwm: started\n"); + + wm_run_autostart(); } void wm_run(WM *wm) { + int xfd = ConnectionNumber(wm->dpy); XEvent e; + for (;;) { - XNextEvent(wm->dpy, &e); - switch (e.type) { - case MapRequest: on_map_request(wm, &e.xmaprequest); break; - case UnmapNotify: on_unmap_notify(wm, &e.xunmap); break; - case DestroyNotify: on_destroy_notify(wm, &e.xdestroywindow); break; - case ConfigureRequest: on_configure_request(wm, &e.xconfigurerequest); break; - case Expose: on_expose(wm, &e.xexpose); break; - case ButtonPress: on_button_press(wm, &e.xbutton); break; - case ButtonRelease: on_button_release(wm, &e.xbutton); break; - case MotionNotify: on_motion_notify(wm, &e.xmotion); break; - case KeyPress: on_key_press(wm, &e.xkey); break; - case KeyRelease: on_key_release(wm, &e.xkey); break; - case EnterNotify: on_enter_notify(wm, &e.xcrossing); break; - case ClientMessage: on_client_message(wm, &e.xclient); break; - case PropertyNotify: on_property_notify(wm, &e.xproperty); break; - default: break; + /* drain all pending X events */ + while (XPending(wm->dpy)) { + XNextEvent(wm->dpy, &e); + switch (e.type) { + case MapRequest: on_map_request(wm, &e.xmaprequest); break; + case UnmapNotify: on_unmap_notify(wm, &e.xunmap); break; + case DestroyNotify: on_destroy_notify(wm, &e.xdestroywindow); break; + case ConfigureRequest: on_configure_request(wm, &e.xconfigurerequest); break; + case Expose: on_expose(wm, &e.xexpose); break; + case ButtonPress: on_button_press(wm, &e.xbutton); break; + case ButtonRelease: on_button_release(wm, &e.xbutton); break; + case MotionNotify: on_motion_notify(wm, &e.xmotion); break; + case KeyPress: on_key_press(wm, &e.xkey); break; + case KeyRelease: on_key_release(wm, &e.xkey); break; + case EnterNotify: on_enter_notify(wm, &e.xcrossing); break; + case ClientMessage: on_client_message(wm, &e.xclient); break; + case PropertyNotify: on_property_notify(wm, &e.xproperty); break; + default: break; + } } + + /* poll hot corners every 200ms */ + hot_corner_check(wm); + + fd_set fds; + FD_ZERO(&fds); + FD_SET(xfd, &fds); + struct timeval tv = { 0, 200000 }; + select(xfd + 1, &fds, NULL, NULL, &tv); } } From a5b2c5aee33ecb01d985b9f8d3251a996fe3f0b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 20:49:45 +0000 Subject: [PATCH 6/8] Complete archiso profile for OrionOS ISO build - iso/profile/profiledef.sh: archiso profile with BIOS+UEFI bootmodes - iso/profile/packages.x86_64: full package list (X11, audio, OrionOS deps) - iso/profile/pacman.conf: pacman config for archiso - iso/profile/airootfs/: live overlay (xinitrc, hostname, skel) - iso/docker/Dockerfile: builds OrionOS from source, installs into airootfs, runs mkarchiso --- iso/docker/Dockerfile | 75 ++++++++++++++++-------- iso/profile/airootfs/etc/hostname | 1 + iso/profile/airootfs/etc/orionos/xinitrc | 9 +++ iso/profile/airootfs/etc/skel/.xinitrc | 2 + iso/profile/airootfs/root/.xinitrc | 2 + iso/profile/packages.x86_64 | 72 +++++++++++++++++++++++ iso/profile/pacman.conf | 14 +++++ iso/profile/profiledef.sh | 22 +++++++ 8 files changed, 174 insertions(+), 23 deletions(-) create mode 100644 iso/profile/airootfs/etc/hostname create mode 100644 iso/profile/airootfs/etc/orionos/xinitrc create mode 100644 iso/profile/airootfs/etc/skel/.xinitrc create mode 100644 iso/profile/airootfs/root/.xinitrc create mode 100644 iso/profile/packages.x86_64 create mode 100644 iso/profile/pacman.conf create mode 100644 iso/profile/profiledef.sh diff --git a/iso/docker/Dockerfile b/iso/docker/Dockerfile index 9326eaf..9976d1a 100644 --- a/iso/docker/Dockerfile +++ b/iso/docker/Dockerfile @@ -1,33 +1,62 @@ FROM archlinux:latest -# Install archiso and build dependencies -RUN pacman -Syu --noconfirm \ - archiso \ - base-devel \ - git \ - libx11 \ - libxft \ - libxfixes \ - dbus \ - libpng \ - feh \ - picom \ - libpam \ - ttf-dejavu \ - xorg-server \ - xorg-xinit \ - xterm \ - notify-send-py \ - && pacman -Scc --noconfirm +# Update and install archiso + OrionOS build dependencies +RUN pacman -Syu --noconfirm && \ + pacman -S --noconfirm \ + archiso \ + base-devel \ + git \ + libx11 \ + libxft \ + libxfixes \ + libxext \ + libpng \ + dbus \ + pam \ + fontconfig \ + freetype2 \ + xorg-server \ + xorg-xinit \ + feh \ + picom \ + alsa-utils \ + ttf-dejavu \ + noto-fonts \ + pkgconf && \ + pacman -Scc --noconfirm WORKDIR /build + +# Copy OrionOS source COPY . /build/orionos # Build OrionOS components -RUN cd /build/orionos && make && make install +RUN cd /build/orionos && make 2>&1 | tail -20 -# Copy archiso profile and build the ISO -RUN cp -r /build/orionos/iso/profile /tmp/orionos-profile -RUN mkarchiso -v -w /tmp/iso-work -o /output /tmp/orionos-profile +# Install binaries into the staging root that archiso will include +RUN cd /build/orionos && \ + install -Dm755 wm/orionwm /build/orionos/iso/profile/airootfs/usr/local/bin/orionwm && \ + install -Dm755 deskbar/oriondeskbar /build/orionos/iso/profile/airootfs/usr/local/bin/oriondeskbar && \ + install -Dm755 launcher/orionlauncher /build/orionos/iso/profile/airootfs/usr/local/bin/orionlauncher && \ + install -Dm755 themes/orionwallpaper /build/orionos/iso/profile/airootfs/usr/local/bin/orionwallpaper && \ + install -Dm755 orionnotify/orionnotify /build/orionos/iso/profile/airootfs/usr/local/bin/orionnotify && \ + install -Dm755 orionlock/orionlock /build/orionos/iso/profile/airootfs/usr/local/bin/orionlock && \ + install -Dm755 orionshot/orionshot /build/orionos/iso/profile/airootfs/usr/local/bin/orionshot && \ + install -Dm755 orionclip/orionclip /build/orionos/iso/profile/airootfs/usr/local/bin/orionclip && \ + install -Dm755 orionpkg/orionpkg /build/orionos/iso/profile/airootfs/usr/local/bin/orionpkg && \ + cp -r themes/OrionOS /build/orionos/iso/profile/airootfs/usr/share/themes/ && \ + install -Dm644 themes/picom.conf /build/orionos/iso/profile/airootfs/etc/orionos/picom.conf + +# Generate default wallpaper +RUN mkdir -p /build/orionos/iso/profile/airootfs/usr/share/orionos/wallpapers && \ + bash /build/orionos/themes/wallpapers/generate-default.sh 2>/dev/null && \ + cp -f /build/orionos/themes/wallpapers/orionos-default.png \ + /build/orionos/iso/profile/airootfs/usr/share/orionos/wallpapers/ 2>/dev/null || true + +# Build the ISO +RUN mkarchiso -v \ + -w /tmp/iso-work \ + -o /output \ + /build/orionos/iso/profile CMD ["/bin/bash"] diff --git a/iso/profile/airootfs/etc/hostname b/iso/profile/airootfs/etc/hostname new file mode 100644 index 0000000..ae391d8 --- /dev/null +++ b/iso/profile/airootfs/etc/hostname @@ -0,0 +1 @@ +orionos diff --git a/iso/profile/airootfs/etc/orionos/xinitrc b/iso/profile/airootfs/etc/orionos/xinitrc new file mode 100644 index 0000000..778b2b3 --- /dev/null +++ b/iso/profile/airootfs/etc/orionos/xinitrc @@ -0,0 +1,9 @@ +#!/bin/bash +# OrionOS live session autostart + +picom --config /etc/orionos/picom.conf & +orionwallpaper & +orionnotify & +orionclip & +oriondeskbar & +exec orionwm diff --git a/iso/profile/airootfs/etc/skel/.xinitrc b/iso/profile/airootfs/etc/skel/.xinitrc new file mode 100644 index 0000000..75e8555 --- /dev/null +++ b/iso/profile/airootfs/etc/skel/.xinitrc @@ -0,0 +1,2 @@ +#!/bin/bash +source /etc/orionos/xinitrc diff --git a/iso/profile/airootfs/root/.xinitrc b/iso/profile/airootfs/root/.xinitrc new file mode 100644 index 0000000..75e8555 --- /dev/null +++ b/iso/profile/airootfs/root/.xinitrc @@ -0,0 +1,2 @@ +#!/bin/bash +source /etc/orionos/xinitrc diff --git a/iso/profile/packages.x86_64 b/iso/profile/packages.x86_64 new file mode 100644 index 0000000..c52df27 --- /dev/null +++ b/iso/profile/packages.x86_64 @@ -0,0 +1,72 @@ +# OrionOS live system packages + +# Base system +base +linux +linux-firmware +mkinitcpio + +# X11 +xorg-server +xorg-xinit +xorg-xrandr +xorg-xsetroot +xorg-xev + +# Fonts +ttf-dejavu +noto-fonts +ttf-liberation + +# Dependencies for OrionOS components +libx11 +libxft +libxfixes +libxext +libpng +dbus +libpam +fontconfig +freetype2 + +# Desktop utilities +feh +picom +xterm +libnotify + +# Audio +alsa-utils +pulseaudio + +# Brightness +light + +# Media playback control +playerctl + +# Arch tools +pacman-contrib +pkexec +polkit + +# System tools +bash +coreutils +util-linux +procps-ng +grep +gawk +sed +tar +gzip + +# Network +networkmanager +dhcpcd + +# Misc +git +vim +less +which diff --git a/iso/profile/pacman.conf b/iso/profile/pacman.conf new file mode 100644 index 0000000..fcb4029 --- /dev/null +++ b/iso/profile/pacman.conf @@ -0,0 +1,14 @@ +[options] +HoldPkg = pacman glibc +Architecture = auto +CheckSpace +ParallelDownloads = 5 + +[core] +Include = /etc/pacman.d/mirrorlist + +[extra] +Include = /etc/pacman.d/mirrorlist + +[multilib] +Include = /etc/pacman.d/mirrorlist diff --git a/iso/profile/profiledef.sh b/iso/profile/profiledef.sh new file mode 100644 index 0000000..2dcf7a6 --- /dev/null +++ b/iso/profile/profiledef.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# archiso profile definition for OrionOS + +iso_name="OrionOS" +iso_label="ORIONOS_$(date +%Y%m)" +iso_publisher="OrionOS Project" +iso_application="OrionOS Live" +iso_version="$(date +%Y.%m)" +install_dir="arch" +buildmodes=('iso') +bootmodes=('bios.syslinux.mbr' 'bios.syslinux.eltorito' + 'uefi-ia32.grub.esp' 'uefi-x64.grub.esp' + 'uefi-ia32.grub.eltorito' 'uefi-x64.grub.eltorito') +arch="x86_64" +pacman_conf="pacman.conf" +airootfs_image_type="squashfs" +airootfs_image_tool_options=('-comp' 'zstd' '-Xcompression-level' '15') +file_permissions=( + ["/etc/orionos/xinitrc"]="0:0:755" + ["/root/.xinitrc"]="0:0:755" + ["/etc/skel/.xinitrc"]="0:0:755" +) From 454f92589141285b95e7985190ea6d8c0a394998 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 20:55:05 +0000 Subject: [PATCH 7/8] 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 --- orionpkg/Makefile | 27 +- orionpkg/orionpkg.c | 1004 ++++++++++++++++++++++++++----------------- 2 files changed, 618 insertions(+), 413 deletions(-) diff --git a/orionpkg/Makefile b/orionpkg/Makefile index 042cefe..9de096e 100644 --- a/orionpkg/Makefile +++ b/orionpkg/Makefile @@ -1,15 +1,22 @@ +CC = gcc + +PKG_C := $(shell pkg-config --cflags x11 xft 2>/dev/null) +PKG_L := $(shell pkg-config --libs x11 xft 2>/dev/null) + +ifeq ($(PKG_C),) + $(error "Required: libx11-dev libxft-dev") +endif + +CFLAGS = -Wall -Wextra -O2 $(PKG_C) +LDFLAGS = $(PKG_L) + BIN = orionpkg -CC = gcc -CFLAGS = -Wall -Wextra -O2 \ - $(shell pkg-config --cflags x11 xft fontconfig 2>/dev/null) -LIBS = $(shell pkg-config --libs x11 xft fontconfig 2>/dev/null) \ - -lX11 -lXft -lfontconfig - +.PHONY: all clean install +all: $(BIN) $(BIN): orionpkg.c - $(CC) $(CFLAGS) -o $@ $< $(LIBS) - + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +install: $(BIN) + install -Dm755 $(BIN) /usr/local/bin/$(BIN) clean: rm -f $(BIN) - -.PHONY: clean diff --git a/orionpkg/orionpkg.c b/orionpkg/orionpkg.c index 9305fe0..eabb039 100644 --- a/orionpkg/orionpkg.c +++ b/orionpkg/orionpkg.c @@ -1,9 +1,12 @@ -/* orionpkg.c — OrionOS Package Manager +/* + * orionpkg — 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. + * Three tabs: + * Search — pacman -Ss, install selected with pkexec pacman -S + * Installed — pacman -Q, remove selected with pkexec pacman -R + * Updates — checkupdates, upgrade all with pkexec pacman -Syu + * + * Native C/Xlib + Xft. Same dark-navy palette as the rest of OrionOS. */ #include #include @@ -19,17 +22,20 @@ #include #include -/* ── 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 +/* ── 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 @@ -41,10 +47,12 @@ #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_UPD 0x1A4888 -#define C_BTN_UPD_H 0x2060AA +#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 @@ -58,31 +66,46 @@ #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" -/* ── states ─────────────────────────────────────────────────────────── */ +/* ── 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 { - 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; + 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 256 -typedef struct { char name[48]; char old_ver[24]; char new_ver[24]; } Pkg; +#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 90 +#define MAX_LOG 300 +#define LOG_COLS 96 typedef struct { char lines[MAX_LOG][LOG_COLS+1]; int head, count; @@ -94,35 +117,58 @@ static void log_push(LogBuf *lb, const char *line) { if (lb->count < MAX_LOG) lb->count++; } -/* ── app struct ──────────────────────────────────────────────────────── */ +/* ── text input ──────────────────────────────────────────────────────── */ typedef struct { - Display *dpy; - int screen; - Window root, win; - GC gc; - Colormap cmap; - Visual *visual; - int depth; + char buf[256]; + int len; + int focused; +} TextInput; - XftFont *font_ui, *font_hdr, *font_mono; +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'; +} - 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 */ +/* ── app ─────────────────────────────────────────────────────────────── */ +typedef struct { + Display *dpy; + int screen; + Window root, win; + GC gc; + Colormap cmap; + Visual *visual; + int depth; - LogBuf log; - int update_fd; /* pipe fd for pacman output */ - int progress; /* 0-100 estimated */ - int spin_frame; /* animation tick */ + XftFont *font_ui, *font_hdr, *font_mono; - char status_msg[256]; -} Pkg_App; + 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(Pkg_App *a, unsigned long rgb) { +static unsigned long xcol(App *a, unsigned long rgb) { XColor c = { .red = ((rgb>>16)&0xFF)*257, .green = ((rgb>> 8)&0xFF)*257, @@ -132,8 +178,7 @@ static unsigned long xcol(Pkg_App *a, unsigned long rgb) { XAllocColor(a->dpy, a->cmap, &c); return c.pixel; } - -static void xftcol(Pkg_App *a, unsigned long rgb, XftColor *out) { +static void xftcol(App *a, unsigned long rgb, XftColor *out) { XRenderColor rc = { .red = ((rgb>>16)&0xFF)*257, .green = ((rgb>> 8)&0xFF)*257, @@ -142,19 +187,15 @@ static void xftcol(Pkg_App *a, unsigned long rgb, XftColor *out) { }; 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) { +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(Pkg_App *a, unsigned long rgb, int x, int y, int w, int 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 text(Pkg_App *a, XftFont *f, unsigned long rgb, - int x, int y, const char *s) { +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); @@ -162,190 +203,238 @@ static void text(Pkg_App *a, XftFont *f, unsigned long rgb, XftColorFree(a->dpy, a->visual, a->cmap, &c); XftDrawDestroy(xd); } - -static int textw(Pkg_App *a, XftFont *f, const char *s) { +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 baseline(XftFont *f, int y, int h) { - return y + (h + f->ascent - f->descent) / 2; -} - +static int bl(XftFont *f, int y, int h) { return y + (h + f->ascent - f->descent)/2; } static int in_rect(int mx, int my, int x, int y, int w, int h) { return mx>=x && mx=y && myhover_btn == btn_id) ? bghov : bg; + 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 - textw(a, a->font_ui, label)) / 2; - text(a, a->font_ui, C_TEXT_HDR, tx, baseline(a->font_ui, y, h), label); + 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); } -/* ── header ──────────────────────────────────────────────────────────── */ -static void draw_header(Pkg_App *a) { +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); - - /* accent stripe */ 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"); - 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); + 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); } -/* ── footer ──────────────────────────────────────────────────────────── */ -static void draw_footer(Pkg_App *a) { +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; - 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); - /* 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->op == OP_RUNNING) { + draw_btn(a, 2, PAD, by, BTN_W, BTN_H, "Cancel", C_BTN_DNG, C_BTN_DNG_H); + return; + } - 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); + 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); } } -/* ── spinning indicator ──────────────────────────────────────────────── */ -static const char *SPIN[] = { "⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏" }; -#define NSPIN 10 +/* ── 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 -/* ── 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_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); + } } -static void draw_updates(Pkg_App *a) { - fill(a, C_BODY, 0, BODY_Y, WIN_W, BODY_H); +/* ── package list ────────────────────────────────────────────────────── */ +#define LIST_Y_SEARCH (INPUT_Y + INPUT_H + 6) +#define LIST_Y_PLAIN (BODY_Y + 2) - draw_list_header(a); +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; +} - int list_y = BODY_Y + ROW_H; - int vis = LIST_ROWS_VIS - 1; +#define DESC_X 200 - 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); +static void draw_list(App *a) { + int ly = list_top(a); + int vis = list_rows_vis(a); - if (is_hov) + /* 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) + 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); + 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); + 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); - /* arrow */ - text(a, a->font_ui, C_TEXT_DIM, ARROW_X - 16, ty, "→"); + /* 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); - text(a, a->font_ui, C_TEXT_OK, NEWVER_X, ty, a->pkgs[i].new_ver); + /* 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 */ - if (a->npkgs > vis) { + int vis2 = vis - 1; + if (a->npkgs > vis2) { int sb_x = WIN_W - 6; - int sb_h = BODY_H - ROW_H; - int th_h = sb_h * vis / a->npkgs; + int sb_h = (vis2) * ROW_H; + int th_h = sb_h * vis2 / 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); + 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 (used during update) ──────────────────────────────────── */ -#define LOG_ROWS_VIS ((BODY_H - 40) / 16) -#define LOG_ROW_H 16 - -static void draw_log(Pkg_App *a) { +/* ── log view ────────────────────────────────────────────────────────── */ +#define LOG_ROW_H 16 +static void draw_log(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); + txt(a, a->font_ui, C_TEXT, bx+bw+6, bl(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); + 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 + 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_VIS ? a->log.count : LOG_ROWS_VIS; + 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; - + 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; + 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)(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, + 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); @@ -353,245 +442,358 @@ static void draw_log(Pkg_App *a) { } } -/* ── simple status screens ───────────────────────────────────────────── */ -static void draw_centered(Pkg_App *a, unsigned long col, - const char *line1, const char *line2) { +/* ── 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); - 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); - } + 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(Pkg_App *a) { +static void draw(App *a) { fill(a, C_BG, 0, 0, WIN_W, WIN_H); draw_header(a); + draw_tabs(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; + 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"); } - 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) { +/* ── async subprocess ────────────────────────────────────────────────── */ +static int spawn_pipe(const char **argv) { 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; - } - + if (pipe(pipefd) < 0) return -1; 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]); close(pipefd[1]); return -1; } 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)); + execvp(argv[0], (char *const *)argv); _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; + 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); } -static void read_update_output(Pkg_App *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->update_fd, buf, sizeof(buf)-1); + ssize_t n = read(a->op_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; + 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) snprintf(a->status_msg, sizeof(a->status_msg), - "pacman exited with code %d", WEXITSTATUS(status)); + 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'; - /* 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) { + if (carry[0]=='(') { int cur, tot; - if (sscanf(carry, "(%d/%d)", &cur, &tot) == 2 && tot > 0) - a->progress = 10 + cur * 80 / 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; + if (strstr(carry,"Running post")) a->progress = 90; } clen = 0; - } else if (clen < (int)sizeof(carry)-2) { - carry[clen++] = buf[i]; - } + } 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) { +/* ── 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; + 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; - /* close */ - int cx = WIN_W - BTN_W - PAD; - if (in_rect(x, y, cx, 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->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; + 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 on_click(Pkg_App *a, int x, int y) { +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) { 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 */ } + 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(Pkg_App *a, int x, int y) { +static void on_motion(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); + 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(Pkg_App *a, int dir) { - if (a->state != ST_UPDATES) return; - int vis = LIST_ROWS_VIS - 1; - int max_scroll = a->npkgs - vis; +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; - if (a->scroll > max_scroll) a->scroll = max_scroll > 0 ? max_scroll : 0; + 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 init(Pkg_App *a) { - a->dpy = XOpenDisplay(NULL); +/* ── 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); @@ -606,111 +808,107 @@ static void init(Pkg_App *a) { fputs("orionpkg: cannot load fonts\n", stderr); exit(1); } - a->state = ST_CHECKING; - a->update_fd = -1; + a->tab = TAB_SEARCH; + a->op = OP_IDLE; + a->op_fd = -1; + a->selected = -1; a->hover_btn = 0; - a->hover_pkg = -1; + 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, - 120, 120, WIN_W, WIN_H, 0, + 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); - } + 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); - } + if (sh) { sh->flags=PMinSize; sh->min_width=560; sh->min_height=360; 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); + 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 run(Pkg_App *a) { +/* ── event loop ───────────────────────────────────────────────────────── */ +static void app_run(App *a) { Atom wm_delete = XInternAtom(a->dpy, "WM_DELETE_WINDOW", False); - - /* kick off initial check */ - run_check(a); + draw(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; + pfds[1].fd = a->op_fd; + pfds[1].events = (a->op_fd >= 0) ? POLLIN : 0; + int nfds = (a->op_fd >= 0) ? 2 : 1; - /* 200ms tick for spinner animation */ - int r = poll(pfds, nfds, 200); + poll(pfds, nfds, 200); - if (a->update_fd >= 0 && pfds[1].revents & (POLLIN|POLLHUP)) - read_update_output(a); + if (a->op_fd >= 0 && pfds[1].revents & (POLLIN|POLLHUP)) + read_op_output(a); - /* spinner animation when checking */ - if (a->state == ST_CHECKING || a->state == ST_UPDATING) { + /* spinner animation */ + if (a->op == OP_SEARCHING || a->op == OP_LOADING || + a->op == OP_CHECKING || a->op == OP_RUNNING) { a->spin_frame++; - if (a->state == ST_CHECKING) draw(a); + if (a->op != OP_RUNNING) 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 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); } } - case ClientMessage: - if ((Atom)ev.xclient.data.l[0] == wm_delete) return; - break; - case DestroyNotify: return; + 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) { - Pkg_App a = {0}; - init(&a); - run(&a); + App a = {0}; + app_init(&a); + app_run(&a); return 0; } From 6b1d705d4f08bd8b897808304c8a5c800e22e854 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 20:57:40 +0000 Subject: [PATCH 8/8] Add dual-path ISO build scripts (Arch + Debian) with README - iso/build.sh: Debian/Ubuntu host builder using debootstrap + xorriso Bootstraps Debian bookworm, compiles OrionOS in chroot, produces bootable hybrid ISO with GRUB (BIOS + EFI), auto-login root on tty1 - iso/build-arch.sh: Arch Linux host builder using mkarchiso Compiles OrionOS, installs into archiso profile, runs mkarchiso - iso/README.md: instructions for all three paths (Docker/Debian/Arch) including dd and Ventoy USB flashing - Makefile: iso target auto-detects available tool (docker/debootstrap/mkarchiso) --- Makefile | 19 ++++-- iso/README.md | 51 ++++++++++++++ iso/build-arch.sh | 37 ++++++++++ iso/build.sh | 169 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 iso/README.md create mode 100755 iso/build-arch.sh create mode 100755 iso/build.sh diff --git a/Makefile b/Makefile index 26359d2..465db2a 100644 --- a/Makefile +++ b/Makefile @@ -102,7 +102,18 @@ install: all @echo " exec orionwm" iso: - @echo "Building OrionOS ISO via Docker..." - docker build -t orionos-iso-builder -f iso/docker/Dockerfile . - mkdir -p iso/output - docker run --rm --privileged -v "$(PWD)/iso/output:/output" orionos-iso-builder + @if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then \ + echo "Building OrionOS ISO via Docker (Arch)..."; \ + docker build -t orionos-iso-builder -f iso/docker/Dockerfile .; \ + mkdir -p iso/output; \ + docker run --rm --privileged -v "$$(pwd)/iso/output:/output" orionos-iso-builder; \ + elif command -v debootstrap >/dev/null 2>&1; then \ + echo "Building OrionOS ISO via debootstrap (Debian/Ubuntu)..."; \ + sudo bash iso/build.sh; \ + elif command -v mkarchiso >/dev/null 2>&1; then \ + echo "Building OrionOS ISO via mkarchiso (Arch native)..."; \ + sudo bash iso/build-arch.sh; \ + else \ + echo "No ISO build tool found. See iso/README.md for options."; \ + exit 1; \ + fi diff --git a/iso/README.md b/iso/README.md new file mode 100644 index 0000000..bfd2335 --- /dev/null +++ b/iso/README.md @@ -0,0 +1,51 @@ +# OrionOS ISO Builder + +Two build paths are provided. Pick the one that matches your host. + +## Path A — Arch Linux host (native, recommended) + +Requires `archiso` and `pacman-contrib`. + +```bash +sudo pacman -S archiso +sudo make iso # runs iso/build-arch.sh +``` + +## Path B — Debian / Ubuntu host + +Requires `xorriso`, `squashfs-tools`, `debootstrap`, `grub-pc-bin`, `grub-efi-amd64-bin`. + +```bash +sudo apt-get install -y xorriso squashfs-tools debootstrap \ + grub-pc-bin grub-efi-amd64-bin mtools +sudo bash iso/build.sh +``` + +Output lands in `iso/output/OrionOS-YYYYMMDD.iso`. + +## Path C — Docker (any host with Docker) + +```bash +make iso +``` + +The Dockerfile (`iso/docker/Dockerfile`) builds inside `archlinux:latest`, +compiles OrionOS, and runs `mkarchiso`. Output is written to `iso/output/`. + +## Writing the ISO to a USB drive + +```bash +# Find your USB device (e.g. /dev/sdb) — be careful +lsblk +sudo dd if=iso/output/OrionOS-*.iso of=/dev/sdX bs=4M status=progress oflag=sync +``` + +Or use [Ventoy](https://ventoy.net) — just copy the `.iso` onto the Ventoy USB. + +## First boot + +- Logs in automatically as root on tty1 and starts X / OrionOS. +- Super+F1 or bottom-left hot corner → launcher +- Super+V → clipboard manager +- Print → screenshot (Shift+Print → region) +- Top-left hot corner → Exposé overview diff --git a/iso/build-arch.sh b/iso/build-arch.sh new file mode 100755 index 0000000..c63194c --- /dev/null +++ b/iso/build-arch.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# OrionOS ISO builder — Arch Linux host using mkarchiso +# Run as root from the repo root: sudo bash iso/build-arch.sh +set -euo pipefail + +REPO="$(cd "$(dirname "$0")/.." && pwd)" +OUT="$REPO/iso/output" + +echo "==> Building OrionOS binaries..." +cd "$REPO" +make all + +echo "==> Installing OrionOS binaries into profile airootfs..." +install -Dm755 wm/orionwm iso/profile/airootfs/usr/local/bin/orionwm +install -Dm755 deskbar/oriondeskbar iso/profile/airootfs/usr/local/bin/oriondeskbar +install -Dm755 launcher/orionlauncher iso/profile/airootfs/usr/local/bin/orionlauncher +install -Dm755 orionnotify/orionnotify iso/profile/airootfs/usr/local/bin/orionnotify +install -Dm755 orionlock/orionlock iso/profile/airootfs/usr/local/bin/orionlock +install -Dm755 orionshot/orionshot iso/profile/airootfs/usr/local/bin/orionshot +install -Dm755 orionclip/orionclip iso/profile/airootfs/usr/local/bin/orionclip +install -Dm755 orionpkg/orionpkg iso/profile/airootfs/usr/local/bin/orionpkg +install -Dm755 themes/orionwallpaper iso/profile/airootfs/usr/local/bin/orionwallpaper +mkdir -p iso/profile/airootfs/usr/share/themes +cp -r themes/OrionOS iso/profile/airootfs/usr/share/themes/ +install -Dm644 themes/picom.conf iso/profile/airootfs/etc/orionos/picom.conf +mkdir -p iso/profile/airootfs/usr/share/orionos/wallpapers +bash themes/wallpapers/generate-default.sh 2>/dev/null || true +cp -f themes/wallpapers/orionos-default.png \ + iso/profile/airootfs/usr/share/orionos/wallpapers/ 2>/dev/null || true + +echo "==> Running mkarchiso..." +mkdir -p "$OUT" +mkarchiso -v -w /tmp/orionos-arch-work -o "$OUT" iso/profile + +echo "" +echo "✓ ISO built in $OUT" +ls -lh "$OUT"/*.iso 2>/dev/null || true diff --git a/iso/build.sh b/iso/build.sh new file mode 100755 index 0000000..0a6a616 --- /dev/null +++ b/iso/build.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# OrionOS ISO builder (Debian-based live system) +# Run as root from the repo root: sudo bash iso/build.sh +set -euo pipefail + +REPO="$(cd "$(dirname "$0")/.." && pwd)" +WORK="/tmp/orionos-build" +OUT="$REPO/iso/output" +FS="$WORK/rootfs" +ISO_STAGE="$WORK/iso" + +echo "==> OrionOS ISO builder" +echo " repo: $REPO" +echo " work: $WORK" +echo " output: $OUT" + +mkdir -p "$FS" "$ISO_STAGE/live" "$ISO_STAGE/boot/grub" "$OUT" + +# ── 1. Bootstrap minimal Debian base ──────────────────────────────────── +echo "==> [1/7] Bootstrapping Debian bookworm..." +if [[ ! -f "$FS/bin/bash" ]]; then + debootstrap --arch=amd64 \ + --include=linux-image-amd64,live-boot,systemd-sysv,sudo,bash,\ +coreutils,util-linux,procps,grep,gawk,sed,tar,gzip,less,vim,git,\ +xorg,xinit,xterm,feh,picom,alsa-utils,libx11-6,libxft2,libxfixes3,\ +libxext6,libpng16-16,dbus,libpam0g,fontconfig,fonts-dejavu,\ +fonts-noto,libdbus-1-3,notify-send,bash-completion,network-manager \ + bookworm "$FS" 2>&1 | grep -E '(I:|W:|E:|error|warn)' || true +else + echo " (rootfs already exists, skipping bootstrap)" +fi + +# ── 2. Compile OrionOS inside chroot ──────────────────────────────────── +echo "==> [2/7] Compiling OrionOS components..." +mkdir -p "$FS/build" +cp -r "$REPO"/{wm,deskbar,launcher,orionnotify,orionlock,orionshot,orionclip,orionpkg,themes} "$FS/build/" 2>/dev/null || true + +chroot "$FS" /bin/bash -c ' + export DEBIAN_FRONTEND=noninteractive + apt-get install -y --no-install-recommends \ + gcc make pkg-config \ + libx11-dev libxft-dev libxfixes-dev libxext-dev \ + libpng-dev libdbus-1-dev libpam0g-dev \ + 2>&1 | tail -3 + + cd /build + for dir in wm deskbar launcher orionnotify orionlock orionshot orionclip orionpkg; do + if [[ -d "$dir" ]]; then + echo " building $dir..." + make -C "$dir" 2>&1 | tail -2 || echo " WARNING: $dir build failed" + fi + done +' 2>&1 + +# ── 3. Install OrionOS binaries ────────────────────────────────────────── +echo "==> [3/7] Installing OrionOS binaries..." +chroot "$FS" /bin/bash -c ' + set -e + install_if() { [[ -f "/build/$1" ]] && install -Dm755 "/build/$1" "/usr/local/bin/$2"; } + install_if wm/orionwm orionwm + install_if deskbar/oriondeskbar oriondeskbar + install_if launcher/orionlauncher orionlauncher + install_if orionnotify/orionnotify orionnotify + install_if orionlock/orionlock orionlock + install_if orionshot/orionshot orionshot + install_if orionclip/orionclip orionclip + install_if orionpkg/orionpkg orionpkg + install -Dm755 /build/themes/orionwallpaper /usr/local/bin/orionwallpaper + mkdir -p /usr/share/themes + cp -r /build/themes/OrionOS /usr/share/themes/ 2>/dev/null || true + mkdir -p /etc/orionos + cp /build/themes/picom.conf /etc/orionos/ 2>/dev/null || true + mkdir -p /usr/share/orionos/wallpapers + bash /build/themes/wallpapers/generate-default.sh 2>/dev/null || true + cp -f /build/themes/wallpapers/orionos-default.png /usr/share/orionos/wallpapers/ 2>/dev/null || true +' 2>&1 + +# ── 4. Configure live session ──────────────────────────────────────────── +echo "==> [4/7] Configuring live session..." + +cat > "$FS/etc/orionos/xinitrc" <<'XINITRC' +#!/bin/bash +picom --config /etc/orionos/picom.conf & +orionwallpaper & +orionnotify & +orionclip & +oriondeskbar & +exec orionwm +XINITRC +chmod 755 "$FS/etc/orionos/xinitrc" + +cat > "$FS/root/.xinitrc" <<'XINIRC' +#!/bin/bash +source /etc/orionos/xinitrc +XINIRC +chmod 755 "$FS/root/.xinitrc" + +# Auto-login and startx on tty1 +mkdir -p "$FS/etc/systemd/system/getty@tty1.service.d" +cat > "$FS/etc/systemd/system/getty@tty1.service.d/autologin.conf" <<'UNIT' +[Service] +ExecStart= +ExecStart=-/sbin/agetty --autologin root --noclear %I $TERM +UNIT + +cat > "$FS/root/.bash_profile" <<'PROFILE' +[[ -z "$DISPLAY" && "$XDG_VTNR" == 1 ]] && exec startx +PROFILE + +# hostname +echo "orionos" > "$FS/etc/hostname" + +# Clean up build artifacts to save space +chroot "$FS" /bin/bash -c ' + apt-get remove -y gcc make pkg-config \ + libx11-dev libxft-dev libxfixes-dev libxext-dev \ + libpng-dev libdbus-1-dev libpam0g-dev 2>&1 | tail -2 || true + apt-get autoremove -y 2>&1 | tail -2 || true + apt-get clean + rm -rf /build /var/cache/apt /tmp/* +' 2>&1 + +# ── 5. Build squashfs ──────────────────────────────────────────────────── +echo "==> [5/7] Building squashfs filesystem..." +mksquashfs "$FS" "$ISO_STAGE/live/filesystem.squashfs" \ + -comp zstd -Xcompression-level 6 \ + -e boot -noappend 2>&1 | tail -3 + +# ── 6. Copy kernel + initrd ────────────────────────────────────────────── +echo "==> [6/7] Copying kernel and initrd..." +VMLINUZ="$(ls "$FS/boot/vmlinuz-"* 2>/dev/null | head -1)" +INITRD="$(ls "$FS/boot/initrd.img-"* 2>/dev/null | head -1)" + +if [[ -z "$VMLINUZ" || -z "$INITRD" ]]; then + echo "ERROR: kernel or initrd not found in $FS/boot/" + ls "$FS/boot/" || true + exit 1 +fi + +cp "$VMLINUZ" "$ISO_STAGE/boot/vmlinuz" +cp "$INITRD" "$ISO_STAGE/boot/initrd.img" + +# ── 7. GRUB bootloader + ISO ───────────────────────────────────────────── +echo "==> [7/7] Building bootable ISO with GRUB..." + +cat > "$ISO_STAGE/boot/grub/grub.cfg" <<'GRUB' +set default=0 +set timeout=5 + +menuentry "OrionOS Live" { + linux /boot/vmlinuz boot=live quiet splash + initrd /boot/initrd.img +} + +menuentry "OrionOS Live (safe mode)" { + linux /boot/vmlinuz boot=live nomodeset + initrd /boot/initrd.img +} +GRUB + +ISOFILE="$OUT/OrionOS-$(date +%Y%m%d).iso" + +grub-mkrescue -o "$ISOFILE" "$ISO_STAGE" \ + --modules="normal iso9660 linux ext2 squash4 biosdisk" \ + 2>&1 | grep -v "^$" | tail -10 + +SIZE="$(du -sh "$ISOFILE" | cut -f1)" +echo "" +echo "✓ ISO built: $ISOFILE ($SIZE)"