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>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-07-23 14:21:38 -04:00
1 parent 98aeafc4f0
commit 53c6199102
6 files changed
+880 -2

No files matched your search

+17
View File
@@ -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
+848
View File
@@ -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 <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;
}