0101 — The settings page edits the file
The TUI gets a settings page, and it is a file editor with a schema.
Full source summary
The TUI gets a settings page, and it is a file editor with a schema. Every row is one key of the user's config.toml; every edit sets or removes that one key in that one file, preserving every other byte, and is validated before it is written. Running state never writes back. The catalogue of settings is pinned to the config schema by tests, and theme slots are contributed by the renderer that owns them.
Status: Accepted Date: 2026-09-08
Context
ADR-0023 chose pure-config: one TOML file is the whole source of truth, defaults are a live base layer compiled into the binary, and phux never writes settings back from running state. Its tradeoffs section named the cost and the door it left open: “No in-app settings editing … a GUI/TUI settings surface, if ever wanted, must round-trip through the file, not a side channel.”
The surface is now wanted. phux has many knobs, spread over eight tables,
and the only ways to learn them were the annotated default.toml, the
generated config reference, and phux config show --layers. None of those
is reachable from inside an attach without leaving the pane, and none
answers “what does this do, what is it set to, who set it, and when will a
change land” in one place.
Decision
-
A file editor with a schema. The settings page (
settingsaction,prefix-Sby default) lists every scalar setting with its effective value, its shipped default, the layer that set it, its description, and when a change applies. Committing an edit sets or removes exactly one dotted key in the user’s ownconfig.tomlthroughphux_config::settings::write_edit: atoml_editdocument mutation that keeps comments, order, and formatting, validated byphux config checkand a full parse before the file is replaced atomically. Removing a key is how “reset to default” is spelled; the base layer shows through. -
Only explicit edits write. Runtime toggles (
toggle-sidebar,toggle-zoom, mouse opt-out) still never touch the file. The page writes when the user acts on a row on the page, and nowhere else. -
Apply is the existing reload. A successful write asks the driver for the same atomic
reload-configswap; keys a reload does not cover are labelled with when they land (next attach, next server start), so the page never implies an effect it cannot produce. -
The catalogue is pinned to the schema.
phux_config::settings::CATALOGcarries one row per scalar key with its kind, bounds, summary, detail, and applies-when; a test walks the serialized schema and fails when a field lacks a row or a row names no field. Composite keys (widget lists, binding tables, hook and registry arrays) are composition, not knobs, and stay out. -
Theme slots come from the renderer.
[theme]is a free-form map in the schema; the slot vocabulary lives inphux_tui::render::Theme. The TUI contributes its slots to the page as rows of kindColor, and their test pins the list to the struct’s fields. -
Layers are read-only from the page. A key set by an
extendslayer shows that layer as its origin; resetting it is refused with the layer’s path, because removing the key from the user’s file could not change what the layer says. Overriding it in the user’s file is allowed, as it always was. -
No
set-optionverb. ADR-0023’s rejection of an imperative CLI knob stands. The library half the page uses would make one trivial to add; this ADR does not add it.
Why
The file stays the source of truth because the page can only produce
files a hand would: a sparse overlay, comments intact, one key per edit,
with the base layer still live underneath. phux config show --layers,
git diff on a dotfiles checkout, and a colleague reading the file all see
exactly what the page did. That is the property ADR-0023 protected, and a
settings page that re-serialized the typed config would have destroyed it
on the first save.
Validating with phux config check before writing turns the page into a
guided editor rather than a way to break the file from inside the TUI: a
chord that does not parse, a byte cap exceeded, an unknown enum variant is
refused with the same message the CLI gives, and the file on disk is the
one that was there before.
Tradeoffs
- A second consumer of the schema’s vocabulary. The catalogue’s
detail texts paraphrase the schema and
default.tomlcomments; the pin test catches a missing row, not stale prose. Prose drift is a review concern, as it already is for the generated reference. - Symlinked configs are followed.
write_editcanonicalizes the path so a dotfiles symlink keeps pointing at its target and the target’s mode survives. The binary’s registry writer refuses symlinks; the two differ on purpose and the page documents its choice. - Composite settings stay in the file. Widget lists, binding tables,
hooks, and plugin registries are not editable from the page. The page
shows where the file is and points at
phux config check.
Alternatives
Runtime-only knobs (no write). A page that changed the running client and forgot on detach. Rejected: it is the imperative model ADR-0023 declined, minus even the persistence, and it teaches users that the page and the file disagree.
Re-serialize the typed config on save. Simplest writer. Rejected: it flattens a hand-commented file into a generated one and materializes every default into the user’s file, which is the scaffold anti-pattern ADR-0023 exists to prevent.
Watch the file and skip the reload step. Rejected for the reason docs/consumers/tui.md section 4.3 already records: a saved-mid-edit file would take effect before the user asked. The page reuses the explicit reload instead.