| 286 |
286 |
|
|
| 287 |
287 |
|
### View architecture
|
| 288 |
288 |
|
|
| 289 |
|
- |
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:
|
|
289 |
+ |
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, and save. The pieces:
|
| 290 |
290 |
|
|
| 291 |
291 |
|
- **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.
|
| 292 |
292 |
|
- **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.
|
| 294 |
294 |
|
- **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.
|
| 295 |
295 |
|
- **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.
|
| 296 |
296 |
|
- **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.
|
| 297 |
|
- |
- **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 "..."`.
|
| 298 |
297 |
|
- **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.
|
| 299 |
298 |
|
- **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.
|
| 300 |
299 |
|
- **Fallback.** A file the form engine cannot render opens in the syntax-highlighted text-edit pane instead, and a diagnostic heads the pane when there is one to show. Two routes into it are built: a schema declaring a syntax with no bind behind it (`sway`, `kdl`, `text`), and a target the `toml_edit` bind refused to parse. The second is the one that changed a behavior rather than adding one — a config that failed to parse used to render as a red paragraph, so the one thing a user could not do with a broken config was open it and fix it.
|
| 313 |
312 |
|
|
| 314 |
313 |
|
Alloy authors a small TOML dialect for describing TOML (and a parallel one for KDL). Not JSON Schema, which is powerful but verbose and awkward for humans. Not TOML's own type system, which is insufficient (no enums, no docs, no cross-field constraints).
|
| 315 |
314 |
|
|
| 316 |
|
- |
Schema-format v1, worked in full at [`crates/alloy/testdata/rio.toml.schema`](../crates/alloy/testdata/rio.toml.schema). The design pass against rio's real config surface shook out five affordances beyond a naive field list: sections, groups, presets, format hints, and an unknown-key policy.
|
|
315 |
+ |
Schema-format v1, worked in full at [`crates/alloy/testdata/rio.toml.schema`](../crates/alloy/testdata/rio.toml.schema). The design pass against rio's real config surface shook out five affordances beyond a naive field list: sections, groups, presets, format hints, and an unknown-key policy. Presets did not survive the build; see below.
|
| 317 |
316 |
|
|
| 318 |
317 |
|
That worked example is a test fixture now rather than a shipped schema, since shop replaced rio in the image (see [STACK.md](STACK.md#terminal)). It stays the format's reference because nothing shipped exercises the DSL as widely: `schemas/shop.toml.schema`, the only catalog entry today, is three plain fields, which is what a terminal needs when it resolves its palette from a theme id instead of from ninety transcribed keys.
|
| 319 |
318 |
|
|
| 335 |
334 |
|
|
| 336 |
335 |
|
**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.
|
| 337 |
336 |
|
|
| 338 |
|
- |
**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.
|
|
337 |
+ |
**Presets are gone, ruled 2026-08-27** (GoingsOn alloy `60a088c9`). `[[preset]]` was specced here as the answer to "how does a user pick a theme without editing 29 hex codes", and the parser and the atomic-apply path were both built and tested. Nothing ever reached them: presets are not on the `Bind` trait, so exposing one needs a mode of its own rather than a keybinding, and no schema the image ships declares any. Rather than keep a tested capability with no caller, the whole construct came out of the DSL. A schema declaring `[[preset]]` is now an unknown key and takes the fallback. The code is in git and the reasoning is in `60a088c9`, so if a schema starts wanting bundles the mode earns itself then.
|
| 339 |
338 |
|
|
| 340 |
339 |
|
**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.
|
| 341 |
340 |
|
|
| 343 |
342 |
|
|
| 344 |
343 |
|
Properties the format has to hold:
|
| 345 |
344 |
|
|
| 346 |
|
- |
- **Readable and writable by hand.** Alloy will author dozens of these. Rio's schema at ~250 lines is the size baseline; if a schema takes a day to write, the catalog is months of work. If it takes an hour, it's a weekend project. Groups and presets are the sugar that keep the hour number honest.
|
|
345 |
+ |
- **Readable and writable by hand.** Alloy will author dozens of these. Rio's schema at ~250 lines is the size baseline; if a schema takes a day to write, the catalog is months of work. If it takes an hour, it's a weekend project. Groups are the sugar that keeps the hour number honest.
|
| 347 |
346 |
|
- **Extensible without breaking editors.** New field types and constraints will get added over time. The editor treats unknown types as text-edit fallback rather than refusing to open the file; `schema_version` on the header lets the editor detect an incompatible schema DSL and route to fallback cleanly.
|
| 348 |
347 |
|
|
| 349 |
348 |
|
The KDL parallel dialect follows the same shape; only the `path` grammar and the roundtrip-safe editing library differ (see the KDL open question below).
|
| 382 |
381 |
|
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.
|
| 383 |
382 |
|
|
| 384 |
383 |
|
- **Shipped.** `alloy net`, `alloy audio`, `alloy mesh`, `alloy setup`, 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.
|
| 385 |
|
- |
- **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, and `schemas/sway.schema` is the header that declares it). 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, **shipped**; (4) tab chrome and the Applications tab, **shipped**; (5) per-field edit plus save and the quit-confirm, **shipped**, then the filterable pick overlay, **shipped**, which was the last field type that could not be changed; (6) the System tab, time rows first, **shipped** — one front, one enum, one bool, one display-only row, which is the smallest complete slice of the command side; (7) the remaining System rows, **shipped** except theme, which needs somewhere to persist a choice and a shell that can re-theme mid-run; (8) presets and live diagnostics, the collapsible sections having landed with the tab; (9) the text-edit fallback, **shipped**, which is also what puts a schema-less app in the list at all: the catalog is built from `.schema` files, so sway reaches it through a header-only schema declaring `syntax = "sway"` rather than through a second catalog; (10) `AlloyTable` read-only.
|
|
384 |
+ |
- **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, and `schemas/sway.schema` is the header that declares it). 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, **shipped**; (4) tab chrome and the Applications tab, **shipped**; (5) per-field edit plus save and the quit-confirm, **shipped**, then the filterable pick overlay, **shipped**, which was the last field type that could not be changed; (6) the System tab, time rows first, **shipped** — one front, one enum, one bool, one display-only row, which is the smallest complete slice of the command side; (7) the remaining System rows, **shipped** except theme, which needs somewhere to persist a choice and a shell that can re-theme mid-run; (8) live diagnostics, the collapsible sections having landed with the tab, presets having been removed from the DSL; (9) the text-edit fallback, **shipped**, which is also what puts a schema-less app in the list at all: the catalog is built from `.schema` files, so sway reaches it through a header-only schema declaring `syntax = "sway"` rather than through a second catalog; (10) `AlloyTable` read-only.
|
| 386 |
385 |
|
- **Written against a real capture, still short one case.** `alloy display` fronts `swaymsg` alone. `wlr-randr` is gone from this line and kanshi never arrived: neither is in the image, and sway re-applies stored `output` config on hotplug by connector name or by the `make model serial` identifier, which is the feature kanshi would have been carried for. The verb's one string serves both consumers, since `output eDP-1 scale 1.25` in a config file and `swaymsg output eDP-1 scale 1.25` at runtime are the same words after the command name; the console runs it and writes the identical text to `~/.config/sway/config.d/50-display.conf`, which the shipped sway config includes after `/etc/sway/config.d/*` so the user's file wins. The parser is written against a verbatim `swaymsg -t get_outputs` capture from the FW12 install (2026-07-29, sway 1.11) and re-checkable inside a session with `reads_this_machines_real_outputs`. The installer seeds that same file from the same generator, so a fresh machine boots at the scale its panel wants rather than at 1.0: there is no compositor to ask during an install, so the panel is read from sysfs and its physical size from EDID, and the scale is that density snapped to the ladder the `s` key walks. A panel that cannot be read seeds nothing, which is 1.0 and one keypress from correct. What is still missing is a multi-output capture: nobody has attached a second display to an Alloy machine, and that is the case parsers break on, so the mode picker is deliberately unbuilt (the one testable panel advertises exactly one mode).
|
| 387 |
386 |
|
- **Shipped, and the only screen that is not a question.** `alloy install`'s credits page, added after the summary as a sixth step. It names each project Alloy ships, its SPDX identifier and its URL, off `crates/alloy/credits.toml` embedded in the binary. The install now starts from this screen rather than from the summary, which is the ordering the page argues for: the last screen before a disk is repartitioned should be the one that says whose work is about to be written to it. Curated rather than generated, so it drifts by design and keeping it current is a release step (see [IMAGE.md](IMAGE.md)). Not a source offer and not a license-text viewer: the full texts stay in `/usr/share/licenses` on the installed system.
|
| 388 |
387 |
|
- **Shipped as a view, with its enforcement half still ahead of it.** `alloy usb`, two tabs over sysfs: the bus, and the Type-C connectors. No usbguard dependency, no CLI fronted, and it works on an image that carries neither. usbguard itself is in both profiles as of 2026-08-22 and its daemon is not enabled, which is the deliberate state rather than an oversight: the package's stock policy is an empty rule file plus `ImplicitPolicyTarget=block`, so arming it before the policy exists deauthorizes the keyboard at boot. The keyboard gate landed the same day and is the part that makes deny-unknown safe: `usr/bin/alloy-usb-gate` drops enforcement whenever the machine has zero usable keyboards, continuously rather than at boot, on the client profile only. It counts `ID_INPUT_KEYBOARD` and not `ID_INPUT_KEY`, which is what keeps a power button from reading as a keyboard, and it reads the input subsystem rather than the USB bus, which is what makes the Framework's i8042 keyboard count without a special case. What is left is the policy itself: deny-unknown, with an activation the user performs from this screen.
|
| 401 |
400 |
|
|
| 402 |
401 |
|
## Open questions
|
| 403 |
402 |
|
|
| 404 |
|
- |
- [x] **Schema format finalized: schema-format v1.** Worked example at [`crates/alloy/testdata/rio.toml.schema`](../crates/alloy/testdata/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).
|
|
403 |
+ |
- [x] **Schema format finalized: schema-format v1.** Worked example at [`crates/alloy/testdata/rio.toml.schema`](../crates/alloy/testdata/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); presets were removed again in 2026-08.
|
| 405 |
404 |
|
- [ ] 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.
|
| 406 |
405 |
|
- [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.
|
| 407 |
406 |
|
- [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`.
|