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>
849 lines
31 KiB
C
849 lines
31 KiB
C
/* 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;
|
||
}
|