Files
spaceshell/DOCS/superpowers/specs/2026-06-14-settings-modal-design.md
T
vasyansk 4aacebcc60 docs: settings modal design spec
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>
2026-06-14 09:01:53 +07:00

7.1 KiB
Raw Blame History

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
  • 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::GetConfigres 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 1020.
  • 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.