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:
@@ -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 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.
|
||||
Reference in New Issue
Block a user