Files
OrionOS/nebula/nebula.c
T
nmemmertandClaude Sonnet 4.6 53c6199102 Add Nebula — native C settings panel (Display, Appearance, Input, About)
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 <noreply@anthropic.com>
2026-07-23 14:21:38 -04:00

849 lines
31 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* nebula.c — OrionOS Settings Panel
*
* Sections: Display | Appearance | Input | About
* Native C/Xlib + Xft, dark-navy palette matching the rest of OrionOS.
*/
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/keysym.h>
#include <X11/Xft/Xft.h>
#include <X11/XKBlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* ── 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;
}