# Settings Modal — Design Spec Date: 2026-06-14 Status: Approved (design); ready for implementation plan. ## Goal A Settings modal (opened from the existing TopBar gear) that lets the user configure the terminal font, appearance (Dark/Light + accent), the default shell, and view/control the daemon. All settings persist in `~/.spacesh/config.toml` via the daemon — the GUI holds no settings state — and changes apply live to every connected client through a `config_changed` event. This honors the core invariant: the daemon is the single source of truth; the GUI and CLI are thin clients. Settings are a daemon concern with a GUI view. ## Out of scope (v1) - Per-workspace or per-surface overrides (global config only). - Keybinding customization. - Notification settings (deferred to the M5 Telegram/MAX work). - Arbitrary user themes (only Dark + Light, each with an accent choice). ## 1. Config model (daemon) Extend `crates/spaceshd/src/config.rs`'s `Config` (currently `default_shell` only) with nested sections, all optional with sane defaults: ```toml default_shell = "/bin/zsh" # existing [terminal] font_family = "JetBrains Mono" font_size = 13 # clamped 10..=20 [appearance] theme = "dark" # "dark" | "light" accent = "blue" # blue | teal | purple | green | orange ``` - `Config` gains `terminal: TerminalConfig` and `appearance: AppearanceConfig` structs, each `#[serde(default)]` so a partial or missing file still loads. - Add a **write path** (config.rs currently only reads): `Config::save()` serializes to `~/.spacesh/config.toml` (toml crate already a dependency). - An **effective config** accessor returns resolved values (e.g. `default_shell` goes through the existing env→toml→passwd→$SHELL→/bin/sh resolver; font/theme fall back to defaults). This is what `GetConfig` returns. The daemon holds the loaded `Config` in its server state so reads don't hit disk each time; `SetConfig` mutates it, persists, and broadcasts. ## 2. Protocol `crates/spacesh-proto/src/message.rs`: - `Cmd::GetConfig` → `res` with the effective config (all fields resolved). - `Cmd::SetConfig { default_shell?, font_family?, font_size?, theme?, accent? }` — each field `Option`, `None` = no change. Validates (font_size clamp, theme and accent enums), updates state, writes config.toml, then broadcasts. - `Evt::ConfigChanged { config }` — full effective config, pushed to all subscribers so every GUI applies the change live. Serialization stays isolated in `spacesh-proto` per the one-protocol invariant. ## 3. Daemon handlers `server.rs`: - `GetConfig` → reply with effective config. - `SetConfig` → apply to in-memory `Config`, `Config::save()`, broadcast `ConfigChanged`, reply ok. On validation error reply `err("BAD_CONFIG", …)`. - The default-shell resolver already reads config.toml; once the daemon holds config in memory, `default_shell()` should consult the in-memory value first (so a SetConfig change affects *new* surfaces without a disk re-read). ## 4. Theme: runtime switching (low-churn) Components already use `COLORS.x` in inline styles. Keep the keys; change the values to CSS custom-property references so no component is touched: - `theme.ts`: - `COLORS.textPrimary = "var(--c-text-primary)"` (etc.) — same keys, var() values. `STATE_COLOR` likewise maps to `var(--c-st-…)`. - `PALETTES = { dark: {...real}, light: {...real} }` and `ACCENTS = { blue, teal, purple, green, orange }` with real hex values. - `applyTheme(theme, accent)` writes the resolved palette to `document.documentElement.style` as `--c-*` variables (accent overrides the palette's accent-derived tokens). - `resolvePalette(theme, accent)` returns a **real-color** object for places that can't use `var()` (xterm). - `App.tsx` calls `applyTheme` on first config load and on every `config_changed`. Light palette: define a readable light token set (bg/app/panel/elevated, borders, text tiers, status colors tuned for light bg). Accent feeds `--c-accent` and accent-derived tokens (e.g. focus border, search active match). ## 5. TerminalView `TerminalView` must use real colors, not var(), for xterm: - Read font_family/font_size and theme/accent from the current config (passed as props from App, or via a small config context). - Build the xterm `theme` (background, foreground, cursor, selection, ANSI) from `resolvePalette(theme, accent)`. - On `config_changed`: apply `term.options.fontFamily/fontSize/theme` to the live terminal and re-fit (so size changes reflow correctly). ## 6. Settings modal (GUI) New `Settings.tsx`, opened from the TopBar gear (handler already present as a no-op/mock). Modal pattern mirrors `Wizard`/`ConfirmDelete` (overlay + focus trap + Esc). Loads via `getConfig()` on open; each control calls `setConfig` (text inputs debounced) and relies on the `config_changed` broadcast to apply. Sections: - **Terminal** — font family dropdown (curated: JetBrains Mono + a few common monospace families), font size 10–20. - **Appearance** — Dark/Light segmented toggle; accent swatch row. - **Shell** — default shell (text input/picker over the resolver; empty = auto-detect). - **Daemon** — read-only: version, pid, uptime, sessions/workspaces count, socket path (from `getHealth`). Buttons **Stop** and **Restart**, each behind an inline confirm (reuse ConfirmDelete-style confirmation or a small inline "are you sure"). ## 7. Daemon Stop / Restart - The `Shutdown` command exists; the launchd plist sets `KeepAlive=true`, and the Tauri bridge has `ensure_daemon` (lazy spawn + poll socket) on connect. - **Stop**: send `Shutdown`; GUI shows disconnected (existing `spacesh:disconnected` path). - **Restart**: send `Shutdown`, then trigger a reconnect; the bridge's `ensure_daemon` (or launchd KeepAlive) respawns the daemon and the GUI reattaches. Verify the bridge reconnects automatically after a server-side disconnect; if not, add an explicit reconnect trigger invoked after Restart. ## Implementation order 1. Daemon config model: `TerminalConfig`/`AppearanceConfig`, `Config::save()`, in-memory config in server state, resolver consults memory. Tests: round-trip, partial-file defaults, save+reload. 2. Protocol: `GetConfig`, `SetConfig`, `ConfigChanged` + bridge commands + socketBridge fns + `config_changed` wiring in App. 3. theme.ts CSS-var refactor + `applyTheme`/`resolvePalette` + light palette + accents; App applies on load and on config_changed. 4. TerminalView: font + xterm theme from config, live re-apply on change. 5. Settings.tsx modal (Terminal/Appearance/Shell sections) wired to the gear. 6. Daemon section + Stop/Restart with confirm and reconnect verification. ## Risks / notes - Light-mode xterm legibility: tune the light ANSI palette on real output. - `config_changed` must not feedback-loop: applying a broadcast must not re-emit SetConfig. - Font dropdown lists families that may not be installed; curated list keeps this low-risk, but a missing family silently falls back (acceptable). - Restart depends on respawn (launchd KeepAlive or bridge ensure_daemon); confirm one path works before shipping the button.