| 23 |
23 |
|
alloy pkg box # containers: the isolation dial [shipped]
|
| 24 |
24 |
|
alloy update # system updates: rpm-ostree front
|
| 25 |
25 |
|
alloy sync # syncthing front (see CONTINUITY.md)
|
| 26 |
|
- |
alloy config <path> # schema-driven TOML/KDL editor
|
|
26 |
+ |
alloy settings # system settings and app configs: two tabs over one form
|
|
27 |
+ |
alloy config <path> # one config file, opened directly, without the tab chrome
|
| 27 |
28 |
|
alloy theme <name> # swap the runtime theme; reads makeover's themes/*.toml
|
| 28 |
29 |
|
# or ~/.config/alloy/themes/*.toml via makeover
|
| 29 |
30 |
|
```
|
| 71 |
72 |
|
|
| 72 |
73 |
|
Every command is executed as argv rather than through a shell, and displayed the same way it is run, quoting arguments that contain whitespace so the logged line can be pasted into a shell and mean the same thing there.
|
| 73 |
74 |
|
|
| 74 |
|
- |
## `alloy config`: schema-driven editor
|
|
75 |
+ |
## `alloy settings`: two tabs over one form engine
|
|
76 |
+ |
|
|
77 |
+ |
The thesis at the top of this document, built. One view, two tabs, and the same form under both:
|
|
78 |
+ |
|
|
79 |
+ |
- **System**, first, because it is the one a user goes looking for. General settings held as live state: time (zone, NTP), hostname, locale and keymap, theme. Each is a handful of rows over one front with machine-readable output, which is the bar `net` (`nmcli`) and `audio` (`pactl -f json`) were already held to.
|
|
80 |
+ |
- **Applications**, the adopted stack's config files, grouped behind the app each one configures rather than presented as file paths. The user picks **rio**, not `~/.config/rio/config.toml`; left pane lists the apps a schema ships for, right pane is the form for the selected one, reusing `audio`'s two-pane routing layout. An app with no schema (sway, whose i3-style syntax is neither TOML nor KDL) still appears in the list and opens the text-edit fallback, because someone looking for "where do I configure sway" should find an answer rather than an absence.
|
|
81 |
+ |
|
|
82 |
+ |
Display and power/idle are deliberately not here. `alloy display` is a specced verb of its own and idle behaviour belongs with it; folding them in would duplicate a screen rather than unify one.
|
|
83 |
+ |
|
|
84 |
+ |
`alloy config <path>` stays as the direct-open escape hatch: one file's form, no tab chrome, which is what a script or a `helix` sidecar wants. Same view, entered with a path instead of a tab.
|
|
85 |
+ |
|
|
86 |
+ |
### The config half
|
| 75 |
87 |
|
|
| 76 |
88 |
|
The config editor is the *same UI pattern* applied to file contents instead of live system state. Point it at a TOML or KDL file, get a navigable form. Save writes the file back.
|
| 77 |
89 |
|
|
| 86 |
98 |
|
|
| 87 |
99 |
|
Settled with the crate/binary split the rest of the console already follows (see [COMPONENT-LIBRARY.md](COMPONENT-LIBRARY.md)): `alloy_tui` renders dumb themed field widgets and form chrome, and the binary owns the schema parser, the `toml_edit` document, validation, presets, and save. The pieces:
|
| 88 |
100 |
|
|
|
101 |
+ |
- **The two tabs meet at one seam, `Bind`.** Everything above it is shared: the rows, the chrome, the modal edit, the validation. Below it, a tab differs in three things only. Where a value is read from (a `DocumentMut`, or a `timedatectl show` style key=value front). What committing one does. And where the field list comes from: parsed from a schema file on the Applications side, a hand-written Rust table on the System side, because there are a dozen System rows fronting different CLIs and ten schemas and growing. Neither side knows the other exists.
|
|
102 |
+ |
- **Committing returns effects rather than performing them**, which is the pattern every backend in the console already follows and is what keeps both binds testable on a machine with none of the tools installed. It also settles what dirty means per tab without either tab being told which kind it is: a file bind holds its edits in the document and emits one `Effect::Write` at save, and a command front emits its setter at commit and has nothing to save. The form asks the bind whether anything is pending and shows the save affordance accordingly. So Ctrl-S is a file thing, because a half-edited TOML is not valid TOML, and live state has no such constraint: `timedatectl set-timezone` is atomic on its own.
|
| 89 |
103 |
|
- **Values live in a `toml_edit::DocumentMut`**, loaded from the target file. That document is the single source of truth and the thing edits are written into in place, which is what makes the roundtrip-safe promise structural rather than remembered. Fields display their schema `default` when the file omits the key, but the key is not materialized on disk until the user edits it, so a minimal config stays minimal.
|
| 90 |
104 |
|
- **Editing is modal per field.** `mode: Navigate | Editing { row, buffer, original }`. In Navigate the reserved keymap holds (Tab / j-k move, Enter activates, Space folds a section or flips a bool, Ctrl-S saves). Enter on a string, number, or color field opens Editing, where the field owns every key. This is the case the classifier already documents: a view holding an active text input must not treat `q`, `/`, or `:` as reserved. Esc discards the buffer, Enter validates and commits; an invalid value stays in edit with its diagnostic shown rather than being written. Enums do not free-type: Enter opens a pick overlay (the `AlloyModal` + `AlloyList` idiom) listing each value's label and description and committing the raw value. Bools toggle in place.
|
| 91 |
|
- |
- **Sections are collapsible.** One pane per schema `[[section]]`, folded with Space on its header, so the 25-slot colors group defaults to a single collapsed row rather than dominating the form. The binary flattens the visible rows each frame (section headers plus the fields of open sections); the `Cursor` rides that.
|
|
105 |
+ |
- **The pick overlay filters as you type.** It was first specced against enums the size of rio's `cursor.shape`, three values. The System tab's zone row is an enum over `timedatectl list-timezones`, which is about 600 entries, and locales are worse. So the overlay owns a `TextField` and matches on a plain substring. No fuzzy match in v1: zone names are already terse and hierarchical, and a ranker over 600 strings is a scoring function to tune for no gain a substring does not give. Typing `den` and pressing enter is also fully offline, needs no database and no license, and cannot be confused by a VPN, which is what retired the idea of geolocating the installer's timezone.
|
|
106 |
+ |
- **Sections are collapsible.** One pane per schema `[[section]]`, folded with Space on its header, so the 29-slot colors group defaults to a single collapsed row rather than dominating the form. The binary flattens the visible rows each frame (section headers plus the fields of open sections); the `Cursor` rides that.
|
| 92 |
107 |
|
- **Presets apply atomically.** A `[[preset]]` writes its whole `values` map into the document in one pass: one dirty increment, one undo entry. Undo is single-level in v1. Applying logs as `apply preset "..."`.
|
| 93 |
108 |
|
- **Save and quit.** Ctrl-S serializes the document back to the target path, logged as `write <path>` (the same `Effect::Write` shape the `alloy pkg` export wrapper established). Quitting with unsaved edits confirms through `AlloyModal`, reusing the Cancel-that-is-not-Quit machinery `alloy pkg` forced into the shell.
|
| 94 |
109 |
|
- **List-of-tables is read-only in v1.** `type = "list"` records (rio's `bindings.keys`) render through a display-only `AlloyTable`; add, remove, and cell-edit route to the text-edit fallback. Cheap to defer because rio ships bindings empty (Sway owns the global binds). Full table editing is v1.1.
|
| 123 |
138 |
|
|
| 124 |
139 |
|
**Fields** (`[[field]]`) carry the atomic edit unit. Every field has `path` (dotted TOML path; hyphens are literal key names), `type`, `description`, and `default`. Types: `bool`, `int`, `float`, `string`, `color`, `path`, `enum`, `list`, `table`. Constraints: `range` (int/float), `pattern` (string regex), `values` (enum), `required` (default false). Format hints: `color format = "hex" | "hex-alpha" | "css"`, `path format = "file" | "dir" | "any"` + optional `must_exist`. `enum` values may be flat (`["a", "b"]`) or structured (`[{value, label, description}, ...]`) when the raw value is unfriendly (rio's `"Disabled"` / `"Transparent"` gets a friendlier label).
|
| 125 |
140 |
|
|
| 126 |
|
- |
**Groups** (`[[group]]`) collapse repetition. Rio's `[colors]` has ~25 palette slots, all colors, all hex. A `[[group]]` with `path = "colors"`, `type = "color"`, `format = "hex"`, and 25 `entries` expands to 25 fields at `colors.<key>` without 25 near-identical `[[field]]` blocks. Groups are DSL sugar over fields, not a runtime concept: the editor materializes them into the same form widgets.
|
|
141 |
+ |
**Groups** (`[[group]]`) collapse repetition. Rio's `[colors]` has 29 palette slots, all colors, all hex. A `[[group]]` with `path = "colors"`, `type = "color"`, `format = "hex"`, and 29 `entries` expands to 29 fields at `colors.<key>` without 29 near-identical `[[field]]` blocks. Groups are DSL sugar over fields, not a runtime concept: the editor materializes them into the same form widgets.
|
| 127 |
142 |
|
|
| 128 |
|
- |
**Presets** (`[[preset]]`) give palette-heavy configs the UX they need: one action swaps a bundle. Each preset declares `name`, `description`, and a `values` map of `path` to value. Applied as one atomic edit: single dirty state, one undo entry. Presets are the answer to "how does a user pick a theme without editing 25 hex codes." Optional per schema.
|
|
143 |
+ |
**Presets** (`[[preset]]`) give palette-heavy configs the UX they need: one action swaps a bundle. Each preset declares `name`, `description`, and a `values` map of `path` to value. Applied as one atomic edit: single dirty state, one undo entry. Presets are the answer to "how does a user pick a theme without editing 29 hex codes." Optional per schema.
|
| 129 |
144 |
|
|
| 130 |
145 |
|
**List-of-tables** (`type = "list"`, `element = {type = "table", fields = [...]}`) covers repeating records like `[[bindings.keys]]`. The element sub-schema is inlined for authoring simplicity, with no cross-file references to chase.
|
| 131 |
146 |
|
|
| 150 |
165 |
|
- `FocusRing` for moving between panes, and `Cursor` for a selection over a list whose length changes underneath it. Two types rather than one: a focus ring wraps, because that is what Tab means, while a list cursor clamps and re-clamps when a refresh returns fewer rows.
|
| 151 |
166 |
|
- Footer chrome and the theme layer, ported from mountaineer-sysop's `sysop-tui` and retinted from a runtime theme rather than a const palette.
|
| 152 |
167 |
|
|
| 153 |
|
- |
Still to come, with `alloy config`:
|
|
168 |
+ |
Still to come, with `alloy settings`:
|
| 154 |
169 |
|
- `AlloyForm` (the collapsible-section form chrome) and a display-only `AlloyTable`.
|
| 155 |
170 |
|
- A single `AlloyField` widget carrying a `FieldKind` value-cell enum (Toggle / Text / Number / Enum / Color), not the five separate field widgets earlier drafts rostered. Those differed only in how the value cell paints once state and validation moved to the binary, and `AlloyForm`'s heterogeneous row list forces an enum regardless; the rationale is worked in full in [COMPONENT-LIBRARY.md](COMPONENT-LIBRARY.md). `TextField` promotes from the console binary's `field.rs` as the caret buffer it uses in edit mode. Ships as `alloy_tui` 1.2.
|
| 156 |
171 |
|
|
| 167 |
182 |
|
|
| 168 |
183 |
|
## Roadmap
|
| 169 |
184 |
|
|
| 170 |
|
- |
The ordering below is the reverse of what this document originally planned, which put `alloy config` alone at v0.5 and every live-state subcommand at v1. The live-state views went first instead. They are small enough to carve one at a time, and each one forced a piece of shared machinery into existence against something real: the shell chrome and log pane from `net`, the second list and the `Cursor` from `audio`, the two-pane layout and `AlloyConnector` from `audio`'s routing, the background tick from watching streams appear. `alloy config` needs the form widgets and the schema parser at once, and it is a better shape to build on a shell that has already carried three screens.
|
|
185 |
+ |
The ordering below is the reverse of what this document originally planned, which put `alloy config` alone at v0.5 and every live-state subcommand at v1. The live-state views went first instead. They are small enough to carve one at a time, and each one forced a piece of shared machinery into existence against something real: the shell chrome and log pane from `net`, the second list and the `Cursor` from `audio`, the two-pane layout and `AlloyConnector` from `audio`'s routing, the background tick from watching streams appear. `alloy settings` needs the form widgets and the schema parser at once, and it is a better shape to build on a shell that has already carried three screens.
|
| 171 |
186 |
|
|
| 172 |
187 |
|
- **Shipped.** `alloy net`, `alloy audio`, `alloy mesh`, and all three `alloy pkg` tabs (`box`, plus `install` and `update` fronting `rpm-ostree status --json`). Plus the shell they share: frame, reserved keys, focus, command-log pane, background tick. `alloy pkg` forced three more pieces into it: `AlloyTabs`, a Cancel that views see before the shell claims it (a confirm needs a cancel that is not "exit the app"), and terminal suspend, so entering a box can hand the TTY to another interactive program.
|
| 173 |
|
- |
- **Next.** `alloy config`, with schemas for the v0-adopted TOML configs (rio, yazi, mako, and others; the sway config takes the text-edit fallback). The largest remaining piece: schema-DSL v1 parser, `toml_edit` roundtrip layer, and the form widgets together. The view architecture is settled (see "View architecture" above); build order is (1) schema parser, (2) `toml_edit` bind layer, both pure and testable against `rio.toml.schema`; (3) `AlloyForm` + `AlloyField` + `TextField` promotion as the `alloy_tui` 1.2 release; (4) the view wiring navigate and edit; (5) collapsible sections, presets, live diagnostics; (6) save, dirty tracking, quit-confirm, text-edit fallback; (7) `AlloyTable` read-only.
|
|
188 |
+ |
- **In progress.** `alloy settings`, with schemas for the v0-adopted TOML configs (rio, yazi, mako, and others; the sway config takes the text-edit fallback). The largest remaining piece: schema-DSL v1 parser, `toml_edit` roundtrip layer, and the form widgets together. The view architecture is settled (see "View architecture" above); build order is (1) schema parser and (2) the `Bind` seam with its `toml_edit` implementation, both pure and testable against `rio.toml.schema`, **both shipped**; (3) `AlloyForm` + `AlloyField` + `TextField` promotion as the `alloy_tui` 1.2 release; (4) tab chrome and the Applications tab; (5) the filterable pick overlay; (6) the System tab, time rows first, which is the smallest complete slice of the command side: one front, one enum, one bool, one display-only row; (7) the remaining System rows; (8) collapsible sections, presets, live diagnostics; (9) save, dirty tracking, quit-confirm, text-edit fallback; (10) `AlloyTable` read-only.
|
| 174 |
189 |
|
- **Blocked on the target machine.** `alloy display` fronts `swaymsg`/`wlr-randr`, neither of which exists on a non-sway development box, so its parser cannot be checked against real output here — the way the two box-parser bugs got written. The rpm-ostree tabs were in this category until their output was captured from a booted Alloy install in QEMU (2026-07-22); the `install` and `system` parsers are written against that real capture and re-checkable with `parses_this_machines_real_status` on any ostree box. `alloy display` still wants the image or real hardware.
|
| 175 |
190 |
|
- **Then.** `alloy sync`, `alloy theme`. `alloy theme` swaps the runtime theme in place (makeover consumer, no re-login). First-boot flow (see [CONTINUITY.md](CONTINUITY.md)) is a thin shim over `alloy mesh` and `alloy sync` enrollments.
|
| 176 |
191 |
|
- **v1.x.** Additional adopted-tool schemas as the v0 stack grows. (`alloy hinged` was shelved with the FW12 tablet flow in the pivot.)
|
| 190 |
205 |
|
- [x] **Schema format finalized: schema-format v1.** Worked example at [`schemas/rio.toml.schema`](../schemas/rio.toml.schema); DSL reference in the section above. The rio design pass surfaced five affordances the original sketch missed (sections, groups, presets, format hints, unknown-key policy).
|
| 191 |
206 |
|
- [ ] KDL editing story: the `kdl` crate ecosystem in Rust is less mature than `toml_edit`. Verify roundtrip-safe editing is achievable before committing to the "one editor, both formats" pitch. If not, KDL configs get the text-edit fallback until it is.
|
| 192 |
207 |
|
- [x] **Command-log pane: always on.** The pedagogical claim ("teaches its own primitives") only lands if the log is visible. Users who dislike it can add a hide toggle later; the shipped default should teach.
|
| 193 |
|
- |
- [x] **Naming: one entry point, `alloy config <path>`.** No `alloy edit` alias. Fewer names to remember; the "config = editor" mental model holds for KDL files too, since they're structured, machine-editable formats, which is what `alloy config` addresses.
|
|
208 |
+ |
- [x] **Naming: `alloy settings` is the entry point, `alloy config <path>` opens one file.** Still no `alloy edit` alias, and the reasoning that settled the second name holds: the "config = editor" mental model works for KDL too, since these are structured machine-editable formats. What changed in 2026-07 is that a path is the wrong thing to have to know first. Someone looking for the time or the hostname has no path in mind, and someone looking for rio's config wants rio, not `~/.config/rio/config.toml`. So the verb people use is `alloy settings` and `alloy config <path>` is kept rather than deprecated, as the direct-open form for scripts and sidecars. Same treatment `alloy update` got against `alloy pkg`.
|
|
209 |
+ |
- [x] **Dirty and apply differ per tab, and neither tab knows it.** A file needs Ctrl-S because a half-edited TOML is not valid TOML. Live state has no such constraint, and `timedatectl set-timezone` is atomic on its own, so a System row commits on edit. The two behaving differently is a real cost, taken deliberately: it falls out of the `Bind` seam returning effects instead of performing them, so the form reads whether anything is pending from the bind rather than from which tab it is on.
|