Files
spaceshell/DOCS/superpowers/specs/2026-06-14-settings-modal-design.md
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

161 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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.