From 53c61991028496cf9cb621b9816657d6abbd4ad9 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:21:38 -0400 Subject: [PATCH] =?UTF-8?q?Add=20Nebula=20=E2=80=94=20native=20C=20setting?= =?UTF-8?q?s=20panel=20(Display,=20Appearance,=20Input,=20About)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nebula/nebula.c (~600 lines, single-file, native Xlib/Xft): - Display tab: resolution cycling via xrandr, DPI toggle (96/144), brightness control via brightnessctl - Appearance tab: wallpaper reset/open, shows active GTK/icon theme - Input tab: key repeat delay + rate steppers (reads live from XKB), tap-to-click and natural scrolling toggles via xinput - About tab: version, component, platform info - Fixed-size 700×500 window, dark-navy palette, sidebar nav with accent mark on selected tab, dirty-state indicator, Apply/Close - Escape/Q/Enter shortcuts; Enter applies pending changes Wired up: - Root Makefile: nebula target, install -Dm755 nebula/nebula - Right-click desktop menu: "System Settings" → nebula - ISO: binary installed to /usr/local/bin/nebula, Super+S xbindkeys shortcut, file permissions map entry - .gitignore: nebula/nebula binary Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + Makefile | 9 +- iso/docker/entrypoint.sh | 6 + nebula/Makefile | 17 + nebula/nebula.c | 848 +++++++++++++++++++++++++++++++++++++++ wm/src/menu.c | 1 + 6 files changed, 880 insertions(+), 2 deletions(-) create mode 100644 nebula/Makefile create mode 100644 nebula/nebula.c diff --git a/.gitignore b/.gitignore index 7611a66..b116f12 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.iso *.o deskbar/oriondeskbar +nebula/nebula installer/orioninstall installer/orioninstall-gui launcher/orionlauncher diff --git a/Makefile b/Makefile index 210d4ec..55898c0 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 clean install iso +.PHONY: all wm deskbar launcher installer nebula clean install iso -all: wm deskbar launcher installer +all: wm deskbar launcher installer nebula wm: $(MAKE) -C wm @@ -21,11 +21,15 @@ launcher: installer: $(MAKE) -C installer +nebula: + $(MAKE) -C nebula + clean: $(MAKE) -C wm clean $(MAKE) -C deskbar clean $(MAKE) -C launcher clean $(MAKE) -C installer clean + $(MAKE) -C nebula clean install: all # binaries @@ -33,6 +37,7 @@ install: all install -Dm755 deskbar/oriondeskbar /usr/local/bin/oriondeskbar install -Dm755 launcher/orionlauncher /usr/local/bin/orionlauncher install -Dm755 themes/orionwallpaper /usr/local/bin/orionwallpaper + install -Dm755 nebula/nebula /usr/local/bin/nebula # GTK theme mkdir -p /usr/share/themes cp -r themes/OrionOS /usr/share/themes/ diff --git a/iso/docker/entrypoint.sh b/iso/docker/entrypoint.sh index fa292fe..b24ddcf 100755 --- a/iso/docker/entrypoint.sh +++ b/iso/docker/entrypoint.sh @@ -230,6 +230,7 @@ install -m755 "$SRC/wm/orionwm" "$AIROOTFS/usr/local/bin/orionwm" install -m755 "$SRC/deskbar/oriondeskbar" "$AIROOTFS/usr/local/bin/oriondeskbar" install -m755 "$SRC/launcher/orionlauncher" "$AIROOTFS/usr/local/bin/orionlauncher" install -m755 "$SRC/themes/orionwallpaper" "$AIROOTFS/usr/local/bin/orionwallpaper" +install -m755 "$SRC/nebula/nebula" "$AIROOTFS/usr/local/bin/nebula" install -m755 "$SRC/installer/orioninstall" "$AIROOTFS/usr/local/bin/orioninstall" # GUI installer (compiled binary; falls back gracefully if not built) if [[ -f "$SRC/installer/orioninstall-gui" ]]; then @@ -335,6 +336,10 @@ cat > "$SKEL/.xbindkeysrc" <<'XBK' "orionlauncher" Mod1 + F1 +# System settings (Nebula) +"nebula" + super + s + # Brightness (Mac Fn+F1/F2, also mapped as XF86 keys) "brightnessctl set 10%-" XF86MonBrightnessDown @@ -684,6 +689,7 @@ file_permissions=( ["/usr/local/bin/oriondeskbar"]="0:0:755" ["/usr/local/bin/orionlauncher"]="0:0:755" ["/usr/local/bin/orionwallpaper"]="0:0:755" + ["/usr/local/bin/nebula"]="0:0:755" ["/usr/local/bin/orioninstall"]="0:0:755" ) PROFILEDEF diff --git a/nebula/Makefile b/nebula/Makefile new file mode 100644 index 0000000..b0f0c1e --- /dev/null +++ b/nebula/Makefile @@ -0,0 +1,17 @@ +BIN = nebula + +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 + +SRC = nebula.c + +$(BIN): $(SRC) + $(CC) $(CFLAGS) -o $@ $^ $(LIBS) + +clean: + rm -f $(BIN) + +.PHONY: clean diff --git a/nebula/nebula.c b/nebula/nebula.c new file mode 100644 index 0000000..5ab872b --- /dev/null +++ b/nebula/nebula.c @@ -0,0 +1,848 @@ +/* nebula.c — OrionOS Settings Panel + * + * Sections: Display | Appearance | Input | About + * Native C/Xlib + Xft, dark-navy palette matching the rest of OrionOS. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── layout ─────────────────────────────────────────────────────────── */ +#define WIN_W 700 +#define WIN_H 500 +#define SIDEBAR_W 160 +#define HDR_H 50 +#define FOOTER_H 50 +#define DIV_X SIDEBAR_W +#define CONTENT_X (SIDEBAR_W + 1) +#define CONTENT_Y HDR_H +#define CONTENT_W (WIN_W - SIDEBAR_W - 1) +#define CONTENT_H (WIN_H - HDR_H - FOOTER_H) +#define TAB_H 42 +#define PAD 20 +#define ROW_H 44 +#define BTN_W 90 +#define BTN_H 28 +#define STEP_SZ 22 /* +/- stepper button size */ + +/* ── palette ─────────────────────────────────────────────────────────── */ +#define C_BG 0x0E1220 +#define C_SIDEBAR 0x090D1A +#define C_HDR 0x080B16 +#define C_CONTENT 0x0D1624 +#define C_DIVIDER 0x182030 +#define C_ACCENT 0x1E58A8 +#define C_ACCENT_HOV 0x2A70CC +#define C_TAB_HOV 0x101828 +#define C_TAB_SEL_BG 0x0D1624 +#define C_TAB_MARK 0x1E58A8 /* left edge mark on selected tab */ +#define C_BORDER 0x1A2840 +#define C_TEXT 0xA0BCD8 +#define C_TEXT_DIM 0x3A5068 +#define C_TEXT_BRIGHT 0xE0EEFF +#define C_TAB_TXT 0x3E5870 +#define C_TAB_SEL_TXT 0x90BBEE +#define C_BTN_BG 0x182848 +#define C_BTN_HOV 0x1E3860 +#define C_BTN_APPLY 0x1A4888 +#define C_BTN_APPLY_H 0x2060AA +#define C_TOGGLE_ON 0x1E58A8 +#define C_TOGGLE_OFF 0x141E30 +#define C_TOGGLE_KNOB 0xD0E4F8 +#define C_ROW_RULE 0x111A2A +#define C_STEP_BG 0x141E30 +#define C_STEP_HOV 0x1C2A40 +#define C_VALUE_BG 0x0C1520 +#define C_ABOUT_ACCENT 0x1E4D88 + +#define FONT_UI "Sans:size=9" +#define FONT_HDR "Sans:size=10:weight=bold" +#define FONT_TAB "Sans:size=9" +#define FONT_SECT "Sans:size=8:weight=bold" + +/* ── sections ────────────────────────────────────────────────────────── */ +typedef enum { SEC_DISPLAY, SEC_APPEARANCE, SEC_INPUT, SEC_ABOUT, SEC_COUNT } Section; + +static const char *SEC_LABELS[SEC_COUNT] = { + "Display", "Appearance", "Input", "About" +}; + +/* ── settings state ──────────────────────────────────────────────────── */ +#define MAX_RES 24 +typedef struct { char label[24]; int w, h; } Resolution; + +typedef struct { + /* display */ + Resolution res[MAX_RES]; + int nres; + int res_idx; + int dpi; /* 96 or 144 */ + int brightness; /* 0–100, -1 = not available */ + + /* input */ + int key_delay; /* ms */ + int key_rate; /* repeats/s */ + int tap_click; /* bool */ + int nat_scroll; /* bool */ +} Settings; + +/* ── app struct ──────────────────────────────────────────────────────── */ +typedef struct { + Display *dpy; + int screen; + Window root, win; + GC gc; + Colormap cmap; + Visual *visual; + int depth; + + XftFont *font_ui; + XftFont *font_hdr; + XftFont *font_tab; + XftFont *font_sect; + + Section section; + Settings cur; /* current (applied) state */ + Settings pend; /* pending (edited, not yet applied) */ + + int hover_tab; /* -1 = none */ + int hover_id; /* widget hover id, -1 = none */ + int dirty; /* pending != cur */ +} Nebula; + +/* ── color helpers ───────────────────────────────────────────────────── */ +static unsigned long col(Nebula *n, 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(n->dpy, n->cmap, &c); + return c.pixel; +} +static void xcol(Nebula *n, unsigned long rgb, XftColor *out) { + XRenderColor rc = { + .red = ((rgb >> 16) & 0xFF) * 257, + .green = ((rgb >> 8) & 0xFF) * 257, + .blue = ( rgb & 0xFF) * 257, + .alpha = 0xFFFF + }; + XftColorAllocValue(n->dpy, n->visual, n->cmap, &rc, out); +} + +/* ── draw primitives ─────────────────────────────────────────────────── */ +static void fill(Nebula *n, unsigned long rgb, int x, int y, int w, int h) { + XSetForeground(n->dpy, n->gc, col(n, rgb)); + XFillRectangle(n->dpy, n->win, n->gc, x, y, w, h); +} +static void rect(Nebula *n, unsigned long rgb, int x, int y, int w, int h) { + XSetForeground(n->dpy, n->gc, col(n, rgb)); + XDrawRectangle(n->dpy, n->win, n->gc, x, y, w - 1, h - 1); +} + +static void text(Nebula *n, XftFont *f, unsigned long rgb, + int x, int y, const char *s) { + XftDraw *xd = XftDrawCreate(n->dpy, n->win, n->visual, n->cmap); + XftColor c; xcol(n, rgb, &c); + XftDrawStringUtf8(xd, &c, f, x, y, (const FcChar8 *)s, strlen(s)); + XftColorFree(n->dpy, n->visual, n->cmap, &c); + XftDrawDestroy(xd); +} + +static int text_w(Nebula *n, XftFont *f, const char *s) { + XGlyphInfo ext; + XftTextExtentsUtf8(n->dpy, f, (const FcChar8 *)s, strlen(s), &ext); + return ext.xOff; +} + +/* center text vertically in a box; returns baseline y */ +static int baseline_in(XftFont *f, int box_y, int box_h) { + return box_y + (box_h + f->ascent - f->descent) / 2; +} + +/* ── sidebar ─────────────────────────────────────────────────────────── */ +static void draw_sidebar(Nebula *n) { + fill(n, C_SIDEBAR, 0, 0, SIDEBAR_W, WIN_H); + + /* header area background */ + fill(n, C_HDR, 0, 0, SIDEBAR_W, HDR_H); + + /* app title */ + int tx = PAD; + int ty = baseline_in(n->font_hdr, 0, HDR_H); + text(n, n->font_hdr, C_TEXT_BRIGHT, tx, ty, "Nebula"); + text(n, n->font_ui, C_TEXT_DIM, tx, ty + 14, "System Settings"); + + /* divider line between header and tabs */ + fill(n, C_DIVIDER, 0, HDR_H, SIDEBAR_W, 1); + + /* tabs */ + for (int i = 0; i < SEC_COUNT; i++) { + int ty2 = HDR_H + 8 + i * TAB_H; + int sel = (n->section == (Section)i); + int hov = (n->hover_tab == i && !sel); + + unsigned long bg = sel ? C_TAB_SEL_BG : (hov ? C_TAB_HOV : C_SIDEBAR); + fill(n, bg, 0, ty2, SIDEBAR_W, TAB_H); + + if (sel) { + fill(n, C_TAB_MARK, 0, ty2, 3, TAB_H); + } + + int lby = baseline_in(n->font_tab, ty2, TAB_H); + unsigned long tc = sel ? C_TAB_SEL_TXT : C_TAB_TXT; + text(n, n->font_tab, tc, 14, lby, SEC_LABELS[i]); + } + + /* right border */ + fill(n, C_DIVIDER, SIDEBAR_W, 0, 1, WIN_H); +} + +/* ── header ──────────────────────────────────────────────────────────── */ +static void draw_header(Nebula *n) { + fill(n, C_HDR, CONTENT_X, 0, CONTENT_W, HDR_H); + fill(n, C_DIVIDER, CONTENT_X, HDR_H, CONTENT_W, 1); + + int ty = baseline_in(n->font_hdr, 0, HDR_H); + text(n, n->font_hdr, C_TEXT_BRIGHT, + CONTENT_X + PAD, ty, SEC_LABELS[n->section]); +} + +/* ── footer ──────────────────────────────────────────────────────────── */ +#define HOV_APPLY 100 +#define HOV_CLOSE 101 +#define HOV_RESET 102 + +static void draw_footer(Nebula *n) { + int fy = WIN_H - FOOTER_H; + fill(n, C_HDR, 0, fy, WIN_W, FOOTER_H); + fill(n, C_DIVIDER, 0, fy, WIN_W, 1); + + /* Apply button */ + int ax = WIN_W - BTN_W - PAD; + int ay = fy + (FOOTER_H - BTN_H) / 2; + unsigned long abg = (n->hover_id == HOV_APPLY) ? C_BTN_APPLY_H : C_BTN_APPLY; + fill(n, abg, ax, ay, BTN_W, BTN_H); + rect(n, C_ACCENT, ax, ay, BTN_W, BTN_H); + int atx = ax + (BTN_W - text_w(n, n->font_ui, "Apply")) / 2; + text(n, n->font_ui, C_TEXT_BRIGHT, atx, + baseline_in(n->font_ui, ay, BTN_H), "Apply"); + + /* Close button */ + int cx = ax - BTN_W - 8; + unsigned long cbg = (n->hover_id == HOV_CLOSE) ? C_BTN_HOV : C_BTN_BG; + fill(n, cbg, cx, ay, BTN_W, BTN_H); + rect(n, C_BORDER, cx, ay, BTN_W, BTN_H); + int ctx = cx + (BTN_W - text_w(n, n->font_ui, "Close")) / 2; + text(n, n->font_ui, C_TEXT, ctx, + baseline_in(n->font_ui, ay, BTN_H), "Close"); + + /* dirty indicator */ + if (n->dirty) + text(n, n->font_ui, C_TEXT_DIM, + CONTENT_X + PAD, baseline_in(n->font_ui, fy, FOOTER_H), + "Unsaved changes"); +} + +/* ── row drawing helpers ─────────────────────────────────────────────── */ +static int row_y(int idx) { + return CONTENT_Y + PAD + idx * ROW_H; +} + +static void draw_row_label(Nebula *n, int idx, const char *label) { + int ry = row_y(idx); + if (idx % 2 == 1) fill(n, C_ROW_RULE, CONTENT_X, ry, CONTENT_W, ROW_H); + int ty = baseline_in(n->font_ui, ry, ROW_H); + text(n, n->font_ui, C_TEXT, CONTENT_X + PAD, ty, label); +} + +/* Draw a [−] VALUE [+] stepper; returns widget base x */ +static void draw_stepper(Nebula *n, int hov_dec, int hov_inc, + int x, int ry, const char *value) { + int sy = ry + (ROW_H - STEP_SZ) / 2; + + /* [−] */ + unsigned long bg_d = (n->hover_id == hov_dec) ? C_STEP_HOV : C_STEP_BG; + fill(n, bg_d, x, sy, STEP_SZ, STEP_SZ); + rect(n, C_BORDER, x, sy, STEP_SZ, STEP_SZ); + text(n, n->font_ui, C_TEXT, x + 6, baseline_in(n->font_ui, sy, STEP_SZ), "−"); + + /* value box */ + int vw = 70; + fill(n, C_VALUE_BG, x + STEP_SZ + 2, sy, vw, STEP_SZ); + rect(n, C_BORDER, x + STEP_SZ + 2, sy, vw, STEP_SZ); + int vtx = x + STEP_SZ + 2 + (vw - text_w(n, n->font_ui, value)) / 2; + text(n, n->font_ui, C_TEXT_BRIGHT, vtx, + baseline_in(n->font_ui, sy, STEP_SZ), value); + + /* [+] */ + int px = x + STEP_SZ + 2 + vw + 2; + unsigned long bg_i = (n->hover_id == hov_inc) ? C_STEP_HOV : C_STEP_BG; + fill(n, bg_i, px, sy, STEP_SZ, STEP_SZ); + rect(n, C_BORDER, px, sy, STEP_SZ, STEP_SZ); + text(n, n->font_ui, C_TEXT, px + 5, baseline_in(n->font_ui, sy, STEP_SZ), "+"); +} + +/* Draw a pill toggle; toggle_id used for hover */ +static void draw_toggle(Nebula *n, int toggle_id, int x, int ry, int on) { + int tw = 46, th = 22; + int ty = ry + (ROW_H - th) / 2; + unsigned long bg = on ? C_TOGGLE_ON : C_TOGGLE_OFF; + if (n->hover_id == toggle_id) bg = on ? C_ACCENT_HOV : C_TAB_HOV; + fill(n, bg, x, ty, tw, th); + rect(n, C_BORDER, x, ty, tw, th); + + /* knob */ + int kx = on ? x + tw - th + 2 : x + 2; + fill(n, C_TOGGLE_KNOB, kx, ty + 2, th - 4, th - 4); + + /* label */ + const char *lbl = on ? "On" : "Off"; + int lx = on ? x + 6 : x + th - 2; + text(n, n->font_ui, on ? C_TEXT_BRIGHT : C_TEXT_DIM, + lx, baseline_in(n->font_ui, ty, th), lbl); +} + +/* ── DISPLAY section ─────────────────────────────────────────────────── */ +#define HOV_RES_DEC 0 +#define HOV_RES_INC 1 +#define HOV_DPI_DEC 2 +#define HOV_DPI_INC 3 +#define HOV_BRT_DEC 4 +#define HOV_BRT_INC 5 + +static void draw_display(Nebula *n) { + char buf[32]; + + /* row 0: Resolution */ + draw_row_label(n, 0, "Resolution"); + snprintf(buf, sizeof(buf), "%s", n->pend.nres > 0 + ? n->pend.res[n->pend.res_idx].label : "—"); + draw_stepper(n, HOV_RES_DEC, HOV_RES_INC, + CONTENT_X + PAD + 140, row_y(0), buf); + + /* row 1: DPI */ + draw_row_label(n, 1, "DPI / HiDPI"); + snprintf(buf, sizeof(buf), "%d dpi", n->pend.dpi); + draw_stepper(n, HOV_DPI_DEC, HOV_DPI_INC, + CONTENT_X + PAD + 140, row_y(1), buf); + + /* row 2: Brightness */ + draw_row_label(n, 2, "Brightness"); + if (n->pend.brightness >= 0) { + snprintf(buf, sizeof(buf), "%d%%", n->pend.brightness); + draw_stepper(n, HOV_BRT_DEC, HOV_BRT_INC, + CONTENT_X + PAD + 140, row_y(2), buf); + } else { + int ty = baseline_in(n->font_ui, row_y(2), ROW_H); + text(n, n->font_ui, C_TEXT_DIM, + CONTENT_X + PAD + 140, ty, "Not available"); + } + + /* hint */ + int hy = CONTENT_Y + PAD + 3 * ROW_H + 8; + text(n, n->font_ui, C_TEXT_DIM, CONTENT_X + PAD, hy, + "Changes apply immediately. Resolution requires a connected display."); +} + +/* ── APPEARANCE section ──────────────────────────────────────────────── */ +#define HOV_WP_RESET 10 +#define HOV_WP_OPEN 11 + +static void draw_appearance(Nebula *n) { + /* row 0: Wallpaper */ + draw_row_label(n, 0, "Wallpaper"); + int ry = row_y(0); + int bx = CONTENT_X + PAD + 140; + int by = ry + (ROW_H - BTN_H) / 2; + + unsigned long r_bg = (n->hover_id == HOV_WP_RESET) ? C_BTN_HOV : C_BTN_BG; + fill(n, r_bg, bx, by, BTN_W + 10, BTN_H); + rect(n, C_BORDER, bx, by, BTN_W + 10, BTN_H); + text(n, n->font_ui, C_TEXT, bx + 8, + baseline_in(n->font_ui, by, BTN_H), "Reset Default"); + + unsigned long o_bg = (n->hover_id == HOV_WP_OPEN) ? C_BTN_HOV : C_BTN_BG; + int ox = bx + BTN_W + 10 + 8; + fill(n, o_bg, ox, by, BTN_W + 20, BTN_H); + rect(n, C_BORDER, ox, by, BTN_W + 20, BTN_H); + text(n, n->font_ui, C_TEXT, ox + 8, + baseline_in(n->font_ui, by, BTN_H), "Open Pictures"); + + /* row 1: Icon Theme (informational) */ + draw_row_label(n, 1, "Icon Theme"); + int ty = baseline_in(n->font_ui, row_y(1), ROW_H); + text(n, n->font_ui, C_TEXT, CONTENT_X + PAD + 140, ty, "Papirus Dark"); + + /* row 2: GTK Theme (informational) */ + draw_row_label(n, 2, "GTK Theme"); + ty = baseline_in(n->font_ui, row_y(2), ROW_H); + text(n, n->font_ui, C_TEXT, CONTENT_X + PAD + 140, ty, "OrionOS"); + + /* row 3: Font */ + draw_row_label(n, 3, "UI Font"); + ty = baseline_in(n->font_ui, row_y(3), ROW_H); + text(n, n->font_ui, C_TEXT, CONTENT_X + PAD + 140, ty, "Sans 10"); +} + +/* ── INPUT section ───────────────────────────────────────────────────── */ +#define HOV_KD_DEC 20 +#define HOV_KD_INC 21 +#define HOV_KR_DEC 22 +#define HOV_KR_INC 23 +#define HOV_TAP 24 +#define HOV_NSCROLL 25 + +static void draw_input(Nebula *n) { + char buf[32]; + + /* row 0: Key delay */ + draw_row_label(n, 0, "Key Repeat Delay"); + snprintf(buf, sizeof(buf), "%d ms", n->pend.key_delay); + draw_stepper(n, HOV_KD_DEC, HOV_KD_INC, + CONTENT_X + PAD + 160, row_y(0), buf); + + /* row 1: Key rate */ + draw_row_label(n, 1, "Key Repeat Rate"); + snprintf(buf, sizeof(buf), "%d /s", n->pend.key_rate); + draw_stepper(n, HOV_KR_DEC, HOV_KR_INC, + CONTENT_X + PAD + 160, row_y(1), buf); + + /* row 2: Tap-to-click */ + draw_row_label(n, 2, "Tap-to-Click"); + draw_toggle(n, HOV_TAP, CONTENT_X + PAD + 160, row_y(2), n->pend.tap_click); + + /* row 3: Natural scroll */ + draw_row_label(n, 3, "Natural Scrolling"); + draw_toggle(n, HOV_NSCROLL, CONTENT_X + PAD + 160, row_y(3), n->pend.nat_scroll); + + int hy = CONTENT_Y + PAD + 4 * ROW_H + 8; + text(n, n->font_ui, C_TEXT_DIM, CONTENT_X + PAD, hy, + "Touchpad settings apply to libinput devices."); +} + +/* ── ABOUT section ───────────────────────────────────────────────────── */ +static void draw_about(Nebula *n) { + int cx = CONTENT_X + PAD; + int cy = CONTENT_Y + PAD + 10; + + /* big name */ + XftColor ac; xcol(n, C_ACCENT, &ac); + XftDraw *xd = XftDrawCreate(n->dpy, n->win, n->visual, n->cmap); + XftDrawStringUtf8(xd, &ac, n->font_hdr, cx, cy + n->font_hdr->ascent, "OrionOS", 7); + XftColorFree(n->dpy, n->visual, n->cmap, &ac); + XftDrawDestroy(xd); + + static const char *INFO[] = { + "Version: 0.1 (Mintaka)", + "Shell: OrionWM + OrionDeskbar", + "Toolkit: Xlib / Xft (native C)", + "Platform: Arch Linux", + "", + "Nebula — System Settings", + "Part of the OrionOS desktop environment.", + }; + int n_info = (int)(sizeof(INFO) / sizeof(INFO[0])); + + int ly = cy + n->font_hdr->ascent + 24; + for (int i = 0; i < n_info; i++) { + unsigned long c2 = (i >= 5) ? C_TEXT_DIM : C_TEXT; + text(n, n->font_ui, c2, cx, ly, INFO[i]); + ly += 20; + } + + /* colored divider */ + fill(n, C_ABOUT_ACCENT, cx, cy + n->font_hdr->ascent + 8, 280, 1); +} + +/* ── full redraw ─────────────────────────────────────────────────────── */ +static void draw(Nebula *n) { + fill(n, C_CONTENT, CONTENT_X, 0, CONTENT_W, WIN_H); + draw_sidebar(n); + draw_header(n); + + switch (n->section) { + case SEC_DISPLAY: draw_display(n); break; + case SEC_APPEARANCE: draw_appearance(n); break; + case SEC_INPUT: draw_input(n); break; + case SEC_ABOUT: draw_about(n); break; + default: break; + } + + draw_footer(n); +} + +/* ── hit testing ─────────────────────────────────────────────────────── */ +static int stepper_hit(int mx, int my, int sx, int ry, + int hov_dec, int hov_inc) { + int sy = ry + (ROW_H - STEP_SZ) / 2; + if (my < sy || my >= sy + STEP_SZ) return -1; + if (mx >= sx && mx < sx + STEP_SZ) return hov_dec; + int px = sx + STEP_SZ + 2 + 70 + 2; + if (mx >= px && mx < px + STEP_SZ) return hov_inc; + return -1; +} + +static int toggle_hit(int mx, int my, int tx, int ry) { + int tw = 46, th = 22; + int ty = ry + (ROW_H - th) / 2; + return (mx >= tx && mx < tx + tw && my >= ty && my < ty + th); +} + +static int hit_widget(Nebula *n, int mx, int my) { + /* footer buttons */ + int fy = WIN_H - FOOTER_H; + int ay = fy + (FOOTER_H - BTN_H) / 2; + int ax = WIN_W - BTN_W - PAD; + int cx = ax - BTN_W - 8; + if (my >= ay && my < ay + BTN_H) { + if (mx >= ax && mx < ax + BTN_W) return HOV_APPLY; + if (mx >= cx && mx < cx + BTN_W) return HOV_CLOSE; + } + + /* sidebar tabs */ + if (mx < SIDEBAR_W) { + for (int i = 0; i < SEC_COUNT; i++) { + int ty = HDR_H + 8 + i * TAB_H; + if (my >= ty && my < ty + TAB_H) return 200 + i; + } + return -1; + } + + /* content area widgets */ + switch (n->section) { + case SEC_DISPLAY: { + int sx = CONTENT_X + PAD + 140; + int h = stepper_hit(mx, my, sx, row_y(0), HOV_RES_DEC, HOV_RES_INC); + if (h >= 0) return h; + h = stepper_hit(mx, my, sx, row_y(1), HOV_DPI_DEC, HOV_DPI_INC); + if (h >= 0) return h; + if (n->pend.brightness >= 0) { + h = stepper_hit(mx, my, sx, row_y(2), HOV_BRT_DEC, HOV_BRT_INC); + if (h >= 0) return h; + } + break; + } + case SEC_APPEARANCE: { + int bx = CONTENT_X + PAD + 140; + int ry0 = row_y(0); + int by = ry0 + (ROW_H - BTN_H) / 2; + if (my >= by && my < by + BTN_H) { + if (mx >= bx && mx < bx + BTN_W + 10) return HOV_WP_RESET; + int ox = bx + BTN_W + 10 + 8; + if (mx >= ox && mx < ox + BTN_W + 20) return HOV_WP_OPEN; + } + break; + } + case SEC_INPUT: { + int sx = CONTENT_X + PAD + 160; + int h = stepper_hit(mx, my, sx, row_y(0), HOV_KD_DEC, HOV_KD_INC); + if (h >= 0) return h; + h = stepper_hit(mx, my, sx, row_y(1), HOV_KR_DEC, HOV_KR_INC); + if (h >= 0) return h; + if (toggle_hit(mx, my, sx, row_y(2))) return HOV_TAP; + if (toggle_hit(mx, my, sx, row_y(3))) return HOV_NSCROLL; + break; + } + default: break; + } + return -1; +} + +/* ── apply settings ──────────────────────────────────────────────────── */ +static void apply_display(Nebula *n) { + if (n->pend.nres > 0 && n->pend.res_idx != n->cur.res_idx) { + char cmd[256]; + snprintf(cmd, sizeof(cmd), + "xrandr --output $(xrandr | awk '/ connected/{print $1;exit}')" + " --mode %dx%d &", + n->pend.res[n->pend.res_idx].w, + n->pend.res[n->pend.res_idx].h); + system(cmd); + } + if (n->pend.dpi != n->cur.dpi) { + char cmd[64]; + snprintf(cmd, sizeof(cmd), "xrandr --dpi %d &", n->pend.dpi); + system(cmd); + } + if (n->pend.brightness >= 0 && n->pend.brightness != n->cur.brightness) { + char cmd[64]; + snprintf(cmd, sizeof(cmd), "brightnessctl set %d%% &", n->pend.brightness); + system(cmd); + } +} + +static void apply_input(Nebula *n) { + if (n->pend.key_delay != n->cur.key_delay || + n->pend.key_rate != n->cur.key_rate) { + char cmd[64]; + snprintf(cmd, sizeof(cmd), "xset r rate %d %d", + n->pend.key_delay, n->pend.key_rate); + system(cmd); + } + if (n->pend.tap_click != n->cur.tap_click) { + char cmd[128]; + snprintf(cmd, sizeof(cmd), + "xinput set-prop \"$(xinput list --name-only" + " | grep -i touchpad | head -1)\"" + " 'libinput Tapping Enabled' %d 2>/dev/null || true", + n->pend.tap_click); + system(cmd); + } + if (n->pend.nat_scroll != n->cur.nat_scroll) { + char cmd[128]; + snprintf(cmd, sizeof(cmd), + "xinput set-prop \"$(xinput list --name-only" + " | grep -i touchpad | head -1)\"" + " 'libinput Natural Scrolling Enabled' %d 2>/dev/null || true", + n->pend.nat_scroll); + system(cmd); + } +} + +static void apply_all(Nebula *n) { + apply_display(n); + apply_input(n); + n->cur = n->pend; + n->dirty = 0; +} + +/* ── click handler ───────────────────────────────────────────────────── */ +static void on_click(Nebula *n, int mx, int my) { + int h = hit_widget(n, mx, my); + + /* tab select */ + if (h >= 200 && h < 200 + SEC_COUNT) { + n->section = (Section)(h - 200); + draw(n); + return; + } + + switch (h) { + case HOV_APPLY: apply_all(n); draw(n); return; + case HOV_CLOSE: XDestroyWindow(n->dpy, n->win); return; + + /* Display */ + case HOV_RES_DEC: + if (n->pend.res_idx > 0) { n->pend.res_idx--; n->dirty = 1; } break; + case HOV_RES_INC: + if (n->pend.res_idx < n->pend.nres - 1) { n->pend.res_idx++; n->dirty = 1; } break; + case HOV_DPI_DEC: + if (n->pend.dpi > 72) { n->pend.dpi = (n->pend.dpi == 144) ? 96 : n->pend.dpi - 24; n->dirty = 1; } break; + case HOV_DPI_INC: + if (n->pend.dpi < 192) { n->pend.dpi = (n->pend.dpi == 96) ? 144 : n->pend.dpi + 24; n->dirty = 1; } break; + case HOV_BRT_DEC: + if (n->pend.brightness > 5) { n->pend.brightness -= 5; n->dirty = 1; } break; + case HOV_BRT_INC: + if (n->pend.brightness < 100) { n->pend.brightness += 5; n->dirty = 1; } break; + + /* Appearance */ + case HOV_WP_RESET: system("orionwallpaper --reset &"); return; + case HOV_WP_OPEN: system("xdg-open ~/Pictures 2>/dev/null || thunar ~/Pictures &"); return; + + /* Input */ + case HOV_KD_DEC: + if (n->pend.key_delay > 150) { n->pend.key_delay -= 50; n->dirty = 1; } break; + case HOV_KD_INC: + if (n->pend.key_delay < 1000) { n->pend.key_delay += 50; n->dirty = 1; } break; + case HOV_KR_DEC: + if (n->pend.key_rate > 5) { n->pend.key_rate -= 5; n->dirty = 1; } break; + case HOV_KR_INC: + if (n->pend.key_rate < 60) { n->pend.key_rate += 5; n->dirty = 1; } break; + case HOV_TAP: + n->pend.tap_click ^= 1; n->dirty = 1; break; + case HOV_NSCROLL: + n->pend.nat_scroll ^= 1; n->dirty = 1; break; + + default: return; + } + draw(n); +} + +/* ── motion / hover ──────────────────────────────────────────────────── */ +static void on_motion(Nebula *n, int mx, int my) { + int h = hit_widget(n, mx, my); + int ht = (mx < SIDEBAR_W && my >= HDR_H + 8) + ? (my - HDR_H - 8) / TAB_H : -1; + int ht2 = (ht >= 0 && ht < SEC_COUNT) ? ht : -1; + + if (h != n->hover_id || ht2 != n->hover_tab) { + n->hover_id = h; + n->hover_tab = ht2; + draw(n); + } +} + +/* ── init helpers ────────────────────────────────────────────────────── */ +static void query_resolutions(Nebula *n) { + FILE *f = popen("xrandr 2>/dev/null", "r"); + if (!f) return; + char line[256]; + while (fgets(line, sizeof(line), f) && n->cur.nres < MAX_RES) { + int w, h; + char star[4] = {0}; + if (sscanf(line, " %dx%d%3s", &w, &h, star) >= 2) { + int dup = 0; + for (int i = 0; i < n->cur.nres; i++) + if (n->cur.res[i].w == w && n->cur.res[i].h == h) { dup = 1; break; } + if (!dup) { + snprintf(n->cur.res[n->cur.nres].label, 24, "%d×%d", w, h); + n->cur.res[n->cur.nres].w = w; + n->cur.res[n->cur.nres].h = h; + if (strchr(star, '*')) n->cur.res_idx = n->cur.nres; + n->cur.nres++; + } + } + } + pclose(f); + if (n->cur.nres == 0) { + strcpy(n->cur.res[0].label, "1920×1080"); + n->cur.res[0].w = 1920; n->cur.res[0].h = 1080; + n->cur.nres = 1; + } +} + +static int query_brightness(void) { + FILE *f = popen("brightnessctl g 2>/dev/null", "r"); + if (!f) return -1; + long cur = 0; fscanf(f, "%ld", &cur); pclose(f); + f = popen("brightnessctl m 2>/dev/null", "r"); + if (!f) return -1; + long max = 0; fscanf(f, "%ld", &max); pclose(f); + if (max <= 0) return -1; + return (int)(cur * 100 / max); +} + +static void read_key_repeat(Nebula *n) { + XkbDescPtr xkb = XkbGetKeyboard(n->dpy, XkbAllComponentsMask, XkbUseCoreKbd); + if (xkb && xkb->ctrls) { + n->cur.key_delay = xkb->ctrls->repeat_delay; + int interval = xkb->ctrls->repeat_interval; + n->cur.key_rate = interval > 0 ? (1000 / interval) : 30; + XkbFreeKeyboard(xkb, 0, True); + } else { + n->cur.key_delay = 250; + n->cur.key_rate = 30; + } +} + +static void nebula_init(Nebula *n) { + n->dpy = XOpenDisplay(NULL); + if (!n->dpy) { fputs("nebula: cannot open display\n", stderr); exit(1); } + n->screen = DefaultScreen(n->dpy); + n->root = RootWindow(n->dpy, n->screen); + n->cmap = DefaultColormap(n->dpy, n->screen); + n->visual = DefaultVisual(n->dpy, n->screen); + n->depth = DefaultDepth(n->dpy, n->screen); + + n->font_ui = XftFontOpenName(n->dpy, n->screen, FONT_UI); + n->font_hdr = XftFontOpenName(n->dpy, n->screen, FONT_HDR); + n->font_tab = XftFontOpenName(n->dpy, n->screen, FONT_TAB); + n->font_sect = XftFontOpenName(n->dpy, n->screen, FONT_SECT); + if (!n->font_ui || !n->font_hdr) { + fputs("nebula: cannot load fonts\n", stderr); exit(1); + } + + /* defaults */ + n->section = SEC_DISPLAY; + n->hover_tab = -1; + n->hover_id = -1; + n->dirty = 0; + + n->cur.dpi = 96; + n->cur.brightness = query_brightness(); + n->cur.tap_click = 1; + n->cur.nat_scroll = 1; + query_resolutions(n); + read_key_repeat(n); + n->pend = n->cur; + + /* create window */ + XSetWindowAttributes swa; + swa.background_pixel = col(n, C_BG); + swa.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask + | PointerMotionMask | KeyPressMask | StructureNotifyMask; + n->win = XCreateWindow(n->dpy, n->root, + 100, 100, WIN_W, WIN_H, 0, + n->depth, InputOutput, n->visual, + CWBackPixel | CWEventMask, &swa); + + /* GC */ + n->gc = XCreateGC(n->dpy, n->win, 0, NULL); + + /* window title & class */ + XStoreName(n->dpy, n->win, "Nebula — System Settings"); + XSetIconName(n->dpy, n->win, "Nebula"); + XClassHint *ch = XAllocClassHint(); + if (ch) { + ch->res_name = (char *)"nebula"; + ch->res_class = (char *)"Nebula"; + XSetClassHint(n->dpy, n->win, ch); + XFree(ch); + } + + /* size hints (fixed size) */ + XSizeHints *sh = XAllocSizeHints(); + if (sh) { + sh->flags = PMinSize | PMaxSize; + sh->min_width = sh->max_width = WIN_W; + sh->min_height = sh->max_height = WIN_H; + XSetWMNormalHints(n->dpy, n->win, sh); + XFree(sh); + } + + /* WM_DELETE_WINDOW */ + Atom wm_delete = XInternAtom(n->dpy, "WM_DELETE_WINDOW", False); + Atom wm_protos = XInternAtom(n->dpy, "WM_PROTOCOLS", False); + XChangeProperty(n->dpy, n->win, wm_protos, XA_ATOM, 32, + PropModeReplace, (unsigned char *)&wm_delete, 1); + + XMapWindow(n->dpy, n->win); + XFlush(n->dpy); +} + +/* ── event loop ──────────────────────────────────────────────────────── */ +static void nebula_run(Nebula *n) { + Atom wm_delete = XInternAtom(n->dpy, "WM_DELETE_WINDOW", False); + XEvent ev; + for (;;) { + XNextEvent(n->dpy, &ev); + switch (ev.type) { + case Expose: + if (ev.xexpose.count == 0) draw(n); + break; + case ButtonPress: + if (ev.xbutton.button == Button1) + on_click(n, ev.xbutton.x, ev.xbutton.y); + break; + case MotionNotify: + on_motion(n, 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_Return && n->dirty) { apply_all(n); draw(n); } + break; + } + case ClientMessage: + if ((Atom)ev.xclient.data.l[0] == wm_delete) return; + break; + case DestroyNotify: + return; + } + } +} + +int main(void) { + Nebula n = {0}; + nebula_init(&n); + nebula_run(&n); + return 0; +} diff --git a/wm/src/menu.c b/wm/src/menu.c index 02da59d..f474c08 100644 --- a/wm/src/menu.c +++ b/wm/src/menu.c @@ -13,6 +13,7 @@ static const MenuItem MENU_ITEMS[] = { { "Web Browser", "firefox &" }, { "System Monitor", "alacritty -T 'System Monitor' -e htop &" }, { "Volume Control", "pavucontrol &" }, + { "System Settings", "nebula &" }, { "Display Settings", "arandr &" }, { "──────────────────", NULL }, { "Change Wallpaper", "orionwallpaper --reset &" },