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>
This commit is contained in:
2026-06-14 09:01:53 +07:00
parent 5b08b204b6
commit 4aacebcc60
@@ -0,0 +1,160 @@
# 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.