Config in config.toml via daemon (Get/Set/ConfigChanged), low-churn CSS-var theme switching (Dark/Light + accent), terminal font, default shell, and daemon status with Stop/Restart. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.1 KiB
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:
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
Configgainsterminal: TerminalConfigandappearance: AppearanceConfigstructs, 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_shellgoes through the existing env→toml→passwd→$SHELL→/bin/sh resolver; font/theme fall back to defaults). This is whatGetConfigreturns.
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→reswith the effective config (all fields resolved).Cmd::SetConfig { default_shell?, font_family?, font_size?, theme?, accent? }— each fieldOption,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-memoryConfig,Config::save(), broadcastConfigChanged, reply ok. On validation error replyerr("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_COLORlikewise maps tovar(--c-st-…).PALETTES = { dark: {...real}, light: {...real} }andACCENTS = { blue, teal, purple, green, orange }with real hex values.applyTheme(theme, accent)writes the resolved palette todocument.documentElement.styleas--c-*variables (accent overrides the palette's accent-derived tokens).resolvePalette(theme, accent)returns a real-color object for places that can't usevar()(xterm).
App.tsxcallsapplyThemeon first config load and on everyconfig_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) fromresolvePalette(theme, accent). - On
config_changed: applyterm.options.fontFamily/fontSize/themeto 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
Shutdowncommand exists; the launchd plist setsKeepAlive=true, and the Tauri bridge hasensure_daemon(lazy spawn + poll socket) on connect. - Stop: send
Shutdown; GUI shows disconnected (existingspacesh:disconnectedpath). - Restart: send
Shutdown, then trigger a reconnect; the bridge'sensure_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
- Daemon config model:
TerminalConfig/AppearanceConfig,Config::save(), in-memory config in server state, resolver consults memory. Tests: round-trip, partial-file defaults, save+reload. - Protocol:
GetConfig,SetConfig,ConfigChanged+ bridge commands + socketBridge fns +config_changedwiring in App. - theme.ts CSS-var refactor +
applyTheme/resolvePalette+ light palette + accents; App applies on load and on config_changed. - TerminalView: font + xterm theme from config, live re-apply on change.
- Settings.tsx modal (Terminal/Appearance/Shell sections) wired to the gear.
- Daemon section + Stop/Restart with confirm and reconnect verification.
Risks / notes
- Light-mode xterm legibility: tune the light ANSI palette on real output.
config_changedmust 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.