Skip to main content

max / alloy

Strip historical narrative from documentation Remove what a doc used to say, when it changed, the incidents that justified a rule, finished migration narration, and counts and versions that rot. State the rules in the present tense instead. Keep every instruction, prohibition and threshold, and keep the measurements that make a rule actionable. Public-facing docs keep their explanatory voice.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-08-31 01:51 UTC
Signed with PGP, not checked
Commit: 5c1d0c933b4beebdda0f4e985df0d079ccc9bf39
Parent: fd7cb4e
51 files changed, +595 insertions, -788 deletions
M CONTRIBUTING.md +1 -1
@@ -6,7 +6,7 @@
6 6
7 7 Alloy is MIT, copyright Make Creative, LLC. See [LICENSE](LICENSE). Contributed code must be MIT-compatible, and a dependency that is not stays out: no GPL, AGPL or LGPL, which would pull the whole binary back into copyleft.
8 8
9 - It was GPLv3-or-later until 2026-07-30. The rule it now follows is that anything meant to earn money for Make Creative is PolyForm Noncommercial and everything else is as permissive as its upstream allows. Alloy is not sold, and every crate it links is MIT or Apache, so MIT is the floor and there was no reason to sit above it. Copyleft had twice blocked code from moving between Alloy and its siblings, which is the cost it was charging for nothing.
9 + The rule it follows is that anything meant to earn money for Make Creative is PolyForm Noncommercial and everything else is as permissive as its upstream allows. Alloy is not sold, and every crate it links is MIT or Apache, so MIT is the floor.
10 10
11 11 The design system, [`alloy_tui`](https://makenot.work/git/max/alloy_tui), is MIT in its own repo and consumed here from crates.io. It is the reusable library rather than the application, and it versions on its own cadence because projects outside Alloy depend on it.
12 12
M README.md -8
@@ -14,13 +14,6 @@
14 14
15 15 MIT. Copyright (c) 2026 Make Creative, LLC. See [`LICENSE`](LICENSE).
16 16
17 - Alloy was GPLv3-or-later until 2026-07-30. Nothing required it: no dependency it
18 - links is copyleft, it is not a fork of anything, and shipping inside an image
19 - next to the kernel is mere aggregation rather than a combined work. The license
20 - was a scaffold-time choice that twice cost more than it bought, blocking code
21 - from moving between Alloy and its siblings. Releases made before the change
22 - remain available under GPLv3 to anyone who has them.
23 -
24 17 The design system it renders through,
25 18 [`alloy_tui`](https://makenot.work/git/max/alloy_tui), is MIT in its own repo and
26 19 consumed from crates.io.
@@ -34,7 +27,6 @@
34 27
35 28 - [`docs/MANIFESTO.md`](docs/MANIFESTO.md) — what Alloy is, who it's for, the thesis.
36 29 - [`docs/STACK.md`](docs/STACK.md) — every pick (base, compositor, bar, lock, launcher, notifications, screenshot, file manager) with reasoning and what was rejected.
37 - - [`docs/MARQUEE-APPS.md`](docs/MARQUEE-APPS.md) — the egui design-system app pipeline.
38 30 - [`docs/COMPONENT-LIBRARY.md`](docs/COMPONENT-LIBRARY.md) — `alloy_tui` crate idioms and how themes reach the widgets.
39 31 - [`docs/RESEARCH-IMMEDIATE-MODE-DESIGN.md`](docs/RESEARCH-IMMEDIATE-MODE-DESIGN.md) — late-term research sketch: what does Figma look like for immediate-mode UI? How do designers and engineers collaborate without designers writing Rust?
40 32 - [`docs/DESIGN-LANGUAGE.md`](docs/DESIGN-LANGUAGE.md) — tinted-greyscale chrome, color reserved for information.
@@ -1,6 +1,6 @@
1 1 # Alloy component library
2 2
3 - The intended idioms for the `alloy_tui` crate: the ratatui design-system library the `alloy` console and every authored Alloy TUI depend on. It is Alloy's one authored visual identity (the egui `alloy_ui` sibling was dropped in the 2026-07-17 pivot).
3 + The intended idioms for the `alloy_tui` crate: the ratatui design-system library the `alloy` console and every authored Alloy TUI depend on. It is Alloy's one authored visual identity.
4 4
5 5 ## Scope
6 6
@@ -15,7 +15,7 @@
15 15
16 16 - Application-level state or logic.
17 17 - Reactive primitives, signals, observables (would violate principle 3 in [MANIFESTO.md](MANIFESTO.md)).
18 - - **Backend detection**, despite earlier drafts of this document listing it above. The mock-or-real pattern from mountaineer-sysop's `sysop-tui` carries over as a pattern, but the code lives per view in the console binary, because what counts as the real backend is `nmcli` for one view and `pactl` for the next. A design-system crate has no business knowing either name.
18 + - **Backend detection.** The mock-or-real pattern lives per view in the console binary, because what counts as the real backend is `nmcli` for one view and `pactl` for the next. A design-system crate has no business knowing either name.
19 19
20 20 ## The ratatui architecture, stated plainly
21 21
@@ -132,7 +132,7 @@
132 132
133 133 ### The config form: one `AlloyField`, not five
134 134
135 - Earlier drafts of this document and CONSOLE.md rostered five field widgets: `ColorField`, `EnumField`, `RangedNumberField`, `ToggleField`, `TextField`. That roster is a fossil of the pre-pivot egui design, where each field was a stateful component owning its own picker and value state. The ratatui architecture stated above moves state and validation out of the widget and into the console binary, which leaves the five differing in exactly one thing: how the *value cell* paints. So the crate ships one widget:
135 + Five separate field widgets (`ColorField`, `EnumField`, `RangedNumberField`, `ToggleField`, `TextField`) would differ in exactly one thing, because the architecture above keeps state and validation in the console binary rather than in the widget: how the *value cell* paints. So the crate ships one widget:
136 136
137 137 ```rust
138 138 pub enum FieldKind<'a> {
@@ -162,7 +162,7 @@
162 162
163 163 `AlloyPicker` is the overlay a closed vocabulary opens. It was specced against three-value enums like rio's `cursor.shape`; the System tab's zone row is an enum over `timedatectl list-timezones`, about 600 entries, so the overlay filters as you type: a `TextField` above an `AlloyList`, matching on a plain substring. No fuzzy match, because zone names are terse and hierarchical and a ranker over 600 strings is a scoring function to tune for no gain a substring does not give.
164 164
165 - An earlier draft of this section said the overlay needed no new widget, on the reasoning that `AlloyModal` could wrap a list. It cannot: `AlloyModal` renders its own message paragraph, so composing in the binary would have meant hand-rolling the `surface.overlay` chrome next to a widget that already owns it. Every floating thing in every Alloy TUI should read the same, which is the argument for the crate and against the view. `AlloyPicker` owns nothing, like the rest: the buffer, the filtering, and the selection are the caller's.
165 + The overlay needs a widget of its own. `AlloyModal` cannot wrap a list: it renders its own message paragraph, so composing in the binary would mean hand-rolling the `surface.overlay` chrome next to a widget that already owns it. Every floating thing in every Alloy TUI should read the same, which is the argument for the crate and against the view. `AlloyPicker` owns nothing, like the rest: the buffer, the filtering, and the selection are the caller's.
166 166
167 167 ## Focus and keymap model
168 168
@@ -177,7 +177,7 @@
177 177
178 178 ## Design tokens
179 179
180 - Tokens are the single source of truth, and they are read at runtime. Earlier drafts of this document described a `tokens.toml` at the crate root compiled to Rust constants by a `build.rs` step. That pipeline was replaced by makeover before `alloy_tui` was written, and no part of it exists. [TOKENS.md](TOKENS.md) is authoritative; this section only describes the crate-side shape.
180 + Tokens are the single source of truth, and they are read at runtime through makeover. There is no build-time token compilation step. [TOKENS.md](TOKENS.md) is authoritative; this section only describes the crate-side shape.
181 181
182 182 Themes are makeover `.toml` files, the same schema every make-family app reads, living in makeover's `themes/` and `~/.config/alloy/themes/`. `alloy_tui` loads one, resolves the intents it needs into ratatui colors, and derives two Alloy-specific tokens locally so theme files stay minimal and cross-app compatible:
183 183
@@ -208,6 +208,6 @@
208 208
209 209 ## Late-term: a design tool, and the token seam
210 210
211 - The theme file is the deliberate seam where external design tooling could live, and it is toolkit-agnostic: anything that emits a valid makeover `.toml` is a viable producer (a hand-edited file, Tokens Studio, a future Alloy-built tool). The move to makeover widened that seam rather than closing it, since the format is now shared with every make-family app instead of being Alloy's own. A speculative token-sync tool keeps the working name **`cast`** (alloys are cast; the tool casts a design source into Alloy tokens). The broader research question of a design tool native to immediate-mode UI is captured in [RESEARCH-IMMEDIATE-MODE-DESIGN.md](RESEARCH-IMMEDIATE-MODE-DESIGN.md); with the pivot to ratatui, that question now targets terminal compositions rather than GPU-drawn ones, but the architecture is the same.
211 + The theme file is the deliberate seam where external design tooling could live, and it is toolkit-agnostic: anything that emits a valid makeover `.toml` is a viable producer (a hand-edited file, Tokens Studio, a future Alloy-built tool), and the format is shared with every make-family app. A speculative token-sync tool keeps the working name **`cast`** (alloys are cast; the tool casts a design source into Alloy tokens). The broader research question of a design tool native to immediate-mode UI is captured in [RESEARCH-IMMEDIATE-MODE-DESIGN.md](RESEARCH-IMMEDIATE-MODE-DESIGN.md); it targets terminal compositions rather than GPU-drawn ones, and the architecture is the same.
212 212
213 213 The commitment that keeps the door open: **tokens are a file, not Rust source.** As long as the theme file is the authority and Rust holds no hex values of its own, the producer is swappable and the design system stays portable. Runtime loading strengthens that commitment: swapping a theme is now a file change and a relaunch rather than a rebuild.
M docs/CONSOLE.md +23 -24
@@ -58,11 +58,10 @@
58 58 named rather than replaced; `--force` is the only way past that, and it leaves a
59 59 `.alloy-bak`.
60 60
61 - `alloy mesh` was named `alloy tail` when this document was written. It is
62 - generic now, for two reasons. Someone who has never heard of Tailscale should
61 + `alloy mesh` is the generic name, for two reasons. Someone who has never heard of Tailscale should
63 62 still find the screen that lists the machines they can reach, and Headscale is
64 63 a self-hosted control server driving the same client, so a vendor-named verb
65 - would misdescribe half its users. `tail` remains as an alias. The backend name
64 + would misdescribe half its users. `tail` is an alias for it. The backend name
66 65 stays visible in the view title (`mesh (tailscale)`), and a self-hosted control
67 66 plane is named there too (`mesh (tailscale via hs.example.org)`), read once at
68 67 startup from `tailscale debug prefs`. That is a `debug` interface and therefore
@@ -73,7 +72,7 @@
73 72 because the first and third share an object rather than a data source.
74 73 `rpm-ostree install` does not touch the running system, it stages a deployment,
75 74 and that staged deployment is what the system tab shows. `alloy update` keeps its
76 - top-level name because this document already specced it. Design in the wiki note
75 + top-level name. Design in the wiki note
77 76 `alloy-package-ux`.
78 77
79 78 The system tab carries one thing the other two do not: `u` checks what is behind.
@@ -228,7 +227,7 @@
228 227 of them, so the reads go straight to `/sys` the way `alloy display` does, and
229 228 every parse is a pure function over a string.
230 229
231 - That stays true now that the image carries usbguard (2026-08-22). The package is
230 + That stays true even though the image carries usbguard. The package is
232 231 installed on both profiles and its daemon is deliberately not enabled, so the
233 232 screen still has nothing to depend on and still reads the same files on a machine
234 233 where the daemon never starts. When the action half lands it fronts
@@ -249,7 +248,7 @@
249 248 **The two lists are deliberately not joined.** The kernel can expose a
250 249 `connector` symlink from a USB port to the Type-C port under it, which would let
251 250 a row say which connector a device is on. No such link exists anywhere under
252 - `/sys/bus/usb/devices` on fw13 (6.17.9, measured 2026-08-22), so the join is
251 + `/sys/bus/usb/devices` on fw13 (measured on 6.17.9), so the join is
253 252 unavailable on the hardware Alloy runs on. Inferring it from port numbering would
254 253 be a guess that reads as a fact, so the lists stay separate.
255 254
@@ -269,7 +268,7 @@
269 268 - **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, and whether gopass has an age identity. Time, hostname and locale are built, fronting `timedatectl`, `hostnamectl` and `localectl`; theme is the one left. Each writing row runs exactly one command, and every one of those is in the five actions the shipped polkit rule grants, so none of them prompts and none can reach an action deliberately left prompting. The grant is for an active *local* session in `wheel`, which an ssh login is not, so those same rows do prompt over ssh — correctly, since "the person sitting at the machine" is the whole of the argument for granting them. The console answers that with polkit's own text agent rather than by widening the grant: a setter that comes back saying interactive authentication is required is run again with `pkttyagent` alive beside it, which means the screen tears down, polkit asks in the terminal's own colors, and the console rebuilds. It is the honest fallback and looks like one. A row whose front did not answer, or whose vocabulary came back empty, is shown and not settable and says why: hiding it would answer "where do I set this" with silence, and offering it would promise a command that is not there. A vocabulary of one closes the row on the same grounds, since the single entry is whatever is already in force and picking it changes nothing. The locale row is the case that reaches users: the image carries no glibc langpacks, so `localectl` has one locale to list, and the row names it and says that adding a langpack adds a choice. The gate is the count rather than a langpack probe, so the row reopens by itself once one is layered in. The secrets row applies the same idiom to something absent rather than to a front that stayed quiet: Alloy ships gopass and provisions no age identity for it, so the row names the directory gopass will look in and says the store will not decrypt until a key is placed there. It is shown and closed in both states, because writing an identity from a settings form is exactly what the provisioning decision ruled out, and it reports whether a file is there without ever reading it. Its rows commit as they are edited rather than at Ctrl-S, and the form reads that from the bind rather than from which tab it is on. 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.
270 269 - **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 *form* (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. It reaches the list through a header-only schema declaring its syntax, so there is one catalog and one search path rather than a second registry for the apps the first one cannot describe.
271 270
272 - Display and power/idle are deliberately not here. `alloy display` is a verb of its own, now shipped, and `alloy power` is another; idle behaviour belongs with power rather than with display, ruled 2026-08-25. Folding any of them into settings would duplicate a screen rather than unify one.
271 + Display and power/idle are deliberately not here. `alloy display` is a verb of its own and `alloy power` is another; idle behaviour belongs with power rather than with display. Folding any of them into settings would duplicate a screen rather than unify one.
273 272
274 273 `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, and same code under the chrome — a second render path for one file is how the two would start to disagree. The schema is found by the path it declares as its target rather than by the file's name, since two tools can both keep a `config.toml`, and a file no schema targets is refused by name instead of opening as an empty form.
275 274
@@ -295,8 +294,8 @@
295 294 - **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 295 - **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 296 - **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.
298 - - **List-of-tables opens the file.** Ruled 2026-08-27: `type = "list"` records (rio's `bindings.keys`) show as a count on their form row, and Enter on that row swaps the pane from the form to the text-edit fallback over the same file. Esc comes back. Reading and editing arrive together and neither needs `AlloyTable`, which still renders nothing and would need a cursor, an offset and a selection before it could. The route is on the field's *declared* kind, not on the value, so an empty list opens too — which is when a user most wants the file, to add the first record. The dirty-state answer is that the file is the handoff in both directions: the form writes its pending edits before the text pane opens, the form is rebuilt from the file on the way back, and Esc refuses to leave a dirty buffer rather than discarding it.
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.
297 + - **List-of-tables opens the file.** `type = "list"` records (rio's `bindings.keys`) show as a count on their form row, and Enter on that row swaps the pane from the form to the text-edit fallback over the same file. Esc comes back. Reading and editing arrive together and neither needs `AlloyTable`, which still renders nothing and would need a cursor, an offset and a selection before it could. The route is on the field's *declared* kind, not on the value, so an empty list opens too — which is when a user most wants the file, to add the first record. The dirty-state answer is that the file is the handoff in both directions: the form writes its pending edits before the text pane opens, the form is rebuilt from the file on the way back, and Esc refuses to leave a dirty buffer rather than discarding it.
298 + - **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 matters because a config that fails to parse is exactly the one a user needs to open and fix.
300 299 - **The syntax is declared, not inferred.** `syntax = ` in the header names the file's language, defaults to `toml`, and answers both questions at once: what to highlight, and whether a form is possible at all. Declaring the language rather than declaring "no form" is what lets a file change tiers without being rewritten — the day the roundtrip-safe-KDL question below resolves, every `syntax = "kdl"` schema starts rendering a form on its own. A schema whose syntax has no form engine must be a header alone, and must name a `target_path`: fields nobody will draw are an authoring error worth reporting, and a schema that opens no file has no reason to be in the catalog.
301 300 - **The pane is not an editor.** No undo, no selection, no search, no clipboard. The image ships `helix`, and a fallback that grew those would be a worse copy of it living inside a settings screen. Its modality matches the form's, for the reason `classify` documents: in Navigate the reserved keymap holds, so `q` and Tab still work; Enter takes the buffer; Esc leaves it *keeping* what was typed, because discarding a file's worth of edits is not the same act as discarding one retyped value. Highlighting is line by line with no state carried between lines, so a TOML multi-line string has its body colored as code — a deliberate trade against re-scanning from the top of the file to draw any line, and sway, the syntax this was built for, has no multi-line construct at all.
302 301
@@ -334,7 +333,7 @@
334 333
335 334 **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.
336 335
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.
336 + **There are no presets in the DSL.** A schema declaring `[[preset]]` is an unknown key and takes the fallback. 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 wants bundles.
338 337
339 338 **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.
340 339
@@ -349,7 +348,7 @@
349 348
350 349 ## `alloy_tui`: the ratatui design system
351 350
352 - `alloy_tui` **is** Alloy's design system. The pivot dropped the egui `alloy_ui` sibling, so there is no GUI counterpart, and this crate carries the whole authored visual identity. It consumes makeover `.toml` theme files at runtime (see [TOKENS.md](TOKENS.md)); palette and semantic colors render as terminal chrome.
351 + `alloy_tui` **is** Alloy's design system. There is no GUI counterpart, and this crate carries the whole authored visual identity. It consumes makeover `.toml` theme files at runtime (see [TOKENS.md](TOKENS.md)); palette and semantic colors render as terminal chrome.
353 352
354 353 Contents (v1). Shipped:
355 354 - Themed `ratatui` widget wrappers: `AlloyBlock`, `AlloyList`, `AlloyStatusBar`, `AlloyLog`.
@@ -361,11 +360,11 @@
361 360
362 361 Shipped with `alloy settings`, as `alloy_tui` 1.2:
363 362 - `AlloyForm`, the form chrome, over rows that are one line each without exception. That is what lets it scroll through the same stateless `list_offset` an `AlloyList` does, and it is why the focused row's help and diagnostic render on a reserved line at the foot of the form rather than under the row. Sections are header rows and an indent, not nested boxes: a box inside a pane spends two columns a side per level to say what the fold marker already says.
364 - - 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). It also carries an `unset` flag the roster did not have, which is what lets a row show the difference between a key set to 12 and a key absent from a file that defaults to 12.
365 - - `AlloyPicker`, the filterable overlay an enum opens, and a display-only `AlloyTable`. Nothing renders through the table, and list-of-tables no longer waits on it: those records open in the text pane instead. If a read-only table view is ever wanted it is a separate decision, against the cost of giving `AlloyTable` a cursor and an offset it does not have.
363 + - A single `AlloyField` widget carrying a `FieldKind` value-cell enum (Toggle / Text / Number / Enum / Color), rather than five separate field widgets. They would differ only in how the value cell paints once state and validation live in 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). It carries an `unset` flag, which is what lets a row show the difference between a key set to 12 and a key absent from a file that defaults to 12.
364 + - `AlloyPicker`, the filterable overlay an enum opens, and a display-only `AlloyTable`. Nothing renders through the table: list-of-tables records open in the text pane instead. If a read-only table view is ever wanted it is a separate decision, against the cost of giving `AlloyTable` a cursor and an offset it does not have.
366 365 - `TextField` promoted from the console binary's `field.rs` as the caret buffer both the field and the picker's filter use.
367 366
368 - Not in `alloy_tui`, contrary to earlier drafts: mock-or-real backend detection. It lives per view in the console binary, because what counts as "the real backend" is `nmcli` for one view and `pactl` for another. The pattern carries over from `sysop-tui`; the code does not.
367 + Not in `alloy_tui`: mock-or-real backend detection. It lives per view in the console binary, because what counts as "the real backend" is `nmcli` for one view and `pactl` for another.
369 368
370 369 The immediate-mode model's stability property (see [STACK.md](STACK.md)) is now shipped in one place: everything Alloy authors renders through `alloy_tui`.
371 370
@@ -378,30 +377,30 @@
378 377
379 378 ## Roadmap
380 379
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.
380 + The live-state views come first. 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.
382 381
383 382 - **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.
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, no longer on the path to anything: list-of-tables routes to the text pane.
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).
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.
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.
383 + - **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; (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, on the path to nothing: list-of-tables routes to the text pane.
384 + - **Written against a real capture, still short one case.** `alloy display` fronts `swaymsg` alone. Neither `wlr-randr` nor kanshi 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 (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).
385 + - **Shipped, and the only screen that is not a question.** `alloy install`'s credits page. It names each project Alloy ships, its SPDX identifier and its URL, off `crates/alloy/credits.toml` embedded in the binary. The install starts from this screen rather than from the summary: 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.
386 + - **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 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 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.
388 387 - **Then.** `alloy theme`, which swaps the runtime theme in place (makeover consumer, no re-login). The first-boot flow (see [CONTINUITY.md](CONTINUITY.md)) shipped as `alloy setup`: two rows over the same `mesh` and `sync` backend seams, so there is no third enrollment path to keep in agreement with them. It survives the teardown mesh enrollment costs by refreshing on the first tick after a suspend rather than on its poll counter. The shell's post-suspend refresh arrives as one `tick` call, and a counter would swallow four out of five of them, leaving the row the user just enrolled reading "not enrolled" for several seconds. The screen appears once: the session autostart runs `alloy setup --if-first-boot`, and the console records that it asked in the same config store the theme lives in.
389 - - **v1.x.** Additional adopted-tool schemas as the v0 stack grows. (`alloy hinged` was shelved with the FW12 tablet flow in the pivot.)
388 + - **v1.x.** Additional adopted-tool schemas as the v0 stack grows.
390 389 - **v2+.** Third-party subcommand registration (a well-known directory of ratatui adapters the console discovers at runtime), if a real ecosystem case emerges. Not planned.
391 390
392 - Positioned right after v0 stack packaging as the primary authored work. Roughly 3-6 months at the v1 scope, broad across subcommands. With the egui marquee apps dropped, the console is the wedge.
391 + Positioned right after v0 stack packaging as the primary authored work. Roughly 3-6 months at the v1 scope, broad across subcommands. The console is the wedge.
393 392
394 393 ## Non-goals
395 394
396 395 - **Not a shell replacement.** Users still live in rio + nu + helix. The console is invoked for specific tasks, then closed.
397 396 - **Not a systemd control panel.** `systemctl` is fine. If a common systemd operation shows up in daily use, add a subcommand; don't build a general systemd UI.
398 397 - **Not a launcher.** Launching is the shell (terminal-driven); there is no graphical launcher. The console is invoked by name, not discovered via search.
399 - - **Not a store.** The console never browses or searches a catalog. `alloy pkg` shows what is installed, where it lives, and what each surface costs; it does not help you find software you have not named. This non-goal read "not a package manager" until 2026-07-19, which scoped out the wrong half. Browsing is what the line was protecting against. Explaining where a package belongs is MANIFESTO principle 6, and the two are separable.
398 + - **Not a store.** The console never browses or searches a catalog. `alloy pkg` shows what is installed, where it lives, and what each surface costs; it does not help you find software you have not named. Browsing is what this line protects against; explaining where a package belongs is MANIFESTO principle 6, and the two are separable.
400 399
401 400 ## Open questions
402 401
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.
402 + - [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.
404 403 - [ ] 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.
405 404 - [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.
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`.
405 + - [x] **Naming: `alloy settings` is the entry point, `alloy config <path>` opens one file.** There is no `alloy edit` alias: the "config = editor" mental model works for KDL too, since these are structured machine-editable formats. 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 the direct-open form for scripts and sidecars. Same treatment `alloy update` gets against `alloy pkg`.
407 406 - [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.
@@ -33,7 +33,7 @@
33 33
34 34 **Not shipping alternatives.** rsync-based sync is more mechanical and less mesh-shaped than Alloy wants; Resilio is proprietary; NextCloud/Seafile are servers, not sync tools. Syncthing is the honest pick for peer-to-peer file continuity.
35 35
36 - The image does carry the `rsync` binary, added 2026-08-20, and that is not a reversal of the paragraph above. What is rejected here is rsync as the continuity model: a scripted push between two paths is not a mesh and does not survive a machine being away. The binary is in the base because fleet tooling shells out to it as a transport, which is a different job on a different schedule. Nothing in `alloy sync` calls it.
36 + The image does carry the `rsync` binary, and that is not a reversal of the paragraph above. What is rejected here is rsync as the continuity model: a scripted push between two paths is not a mesh and does not survive a machine being away. The binary is in the base because fleet tooling shells out to it as a transport, which is a different job on a different schedule. Nothing in `alloy sync` calls it.
37 37
38 38 ## First-boot flow
39 39
@@ -1,6 +1,6 @@
1 1 # Alloy Design Language
2 2
3 - The visual rules Alloy's authored TUIs follow, and that adopted apps are themed toward where possible. Since the 2026-07-17 pivot, "authored" means ratatui terminal surfaces; the principles below are toolkit-agnostic, and the mechanics are stated for a terminal cell grid (with notes for any adopted GUI surface where they differ).
3 + The visual rules Alloy's authored TUIs follow, and that adopted apps are themed toward where possible. "Authored" means ratatui terminal surfaces. The principles below are toolkit-agnostic, and the mechanics are stated for a terminal cell grid, with notes for any adopted GUI surface where they differ.
4 4
5 5 Reference imagery: [inspo/](inspo/): terminal rices that demonstrate most of these principles.
6 6
@@ -37,7 +37,7 @@
37 37
38 38 ## Typography
39 39
40 - Mono, by nature: a terminal renders one fixed-width font (`Quasi Mono`, the house face). The former two-font working/display split does not apply; hierarchy comes from the levers a terminal has: **weight** (`Modifier::BOLD` for emphasis and section headers), **case** (`UPPERCASE` letter-labels above readouts, in the mil-spec placard tradition), and **the house marks** (ICONOGRAPHY.md's functional tier: triangles, arrows, box drawing, block elements). Column alignment is free: the terminal font is already tabular, so numeric columns align without a special display font. The exact tokens live in [TOKENS.md](TOKENS.md#typography).
40 + Mono, by nature: a terminal renders one fixed-width font (`Quasi Mono`, the house face). Hierarchy comes from the levers a terminal has: **weight** (`Modifier::BOLD` for emphasis and section headers), **case** (`UPPERCASE` letter-labels above readouts, in the mil-spec placard tradition), and **the house marks** (ICONOGRAPHY.md's functional tier: triangles, arrows, box drawing, block elements). Column alignment is free: the terminal font is already tabular, so numeric columns align without a special display font. The exact tokens live in [TOKENS.md](TOKENS.md#typography).
41 41
42 42 ## Geometry
43 43
@@ -73,7 +73,7 @@
73 73
74 74 ## Iconography
75 75
76 - Two tiers with non-overlapping roles. Functional icons are the house glyph set drawn into the face itself — triangles, arrows, box drawing, block elements — used inline at cell size, and are the primary tier for TUIs. There is no Private Use Area in the image as of 2026-08-17. Hero illustrations survive only on the few graphical surfaces that remain (splash, swaylock background). Full rules in [ICONOGRAPHY.md](ICONOGRAPHY.md).
76 + Two tiers with non-overlapping roles. Functional icons are the house glyph set drawn into the face itself — triangles, arrows, box drawing, block elements — used inline at cell size, and are the primary tier for TUIs. There is no Private Use Area in the image. Hero illustrations survive only on the few graphical surfaces that remain (splash, swaylock background). Full rules in [ICONOGRAPHY.md](ICONOGRAPHY.md).
77 77
78 78 ## Scope
79 79
M docs/FONTS.md +29 -43
@@ -5,19 +5,16 @@
5 5 measurement: which codepoints Alloy's own surfaces put on a screen, which of them the
6 6 shipped fonts cover, and what the fallback chain does with the rest.
7 7
8 - First measured 2026-08-06 against the image built from `Containerfile` at `eeeb768`, read
9 - out of `podman image mount localhost/alloy:local` rather than out of the package list. A
10 - package list says what was requested; the mounted rootfs says what is there.
8 + Measured against the built image, read out of `podman image mount localhost/alloy:local`
9 + rather than out of the package list. A package list says what was requested; the mounted
10 + rootfs says what is there.
11 11
12 - **Re-measured 2026-08-17, when the image stopped installing fonts and started cutting
13 - them.** IosevkaTerm Nerd Font and upstream Atkinson Hyperlegible are gone; the image
14 - carries `Quasi Mono` and `Quasi Body`, cut in the build by `quasi-type` at
15 - `6d076b7` from Atkinson Hyperlegible Mono and Atkinson Hyperlegible Next 2.001
16 - (STACK.md#fonts, wiki `typography-standard`). Every number below that names a font is
17 - from that run, reading `cmap` off the two built faces. What the widget stack can reach was
18 - not re-derived: it is a scan of `alloy_tui`, `ratatui-*` and `makeover`, and the lock has
19 - since moved to `alloy_tui 7.1.0` and `makeover 2.5.1`, so treat the 723 as the last
20 - measured ceiling rather than today's.
12 + The image carries `Quasi Mono` and `Quasi Body`, cut in the build by `quasi-type` from
13 + Atkinson Hyperlegible Mono and Atkinson Hyperlegible Next (STACK.md#fonts, wiki
14 + `typography-standard`). Every number below that names a font is read from the `cmap` of
15 + those two built faces. The widget ceiling is a scan of `alloy_tui`, `ratatui-*` and
16 + `makeover` and moves with the lock, so treat the 723 as a last-measured ceiling rather
17 + than today's.
21 18
22 19 Reading the built faces rather than a mounted rootfs is the stronger reading for this
23 20 question and the weaker one for "what is installed": a cut face is fully determined by the
@@ -63,7 +60,7 @@
63 60 | U+2718 | `✘` | starship, error state |
64 61
65 62 Latin-1 punctuation, arrows, four block-element characters and one dingbat. No Private
66 - Use Area: **Alloy itself emits no Nerd Font icons**, and as of 2026-08-17 there is no
63 + Use Area: **Alloy itself emits no Nerd Font icons**, and there is no
67 64 Private Use Area in the image to emit. The PUA reasoning in
68 65 `etc/skel/.config/fontconfig/fonts.conf` and in STACK.md is entirely about third-party
69 66 consumers (starship, yazi, bottom, helix), which is worth knowing before anyone treats
@@ -98,7 +95,6 @@
98 95 | Quasi Mono (shipped) | 535 | **0** | 562 |
99 96 | Quasi Body (shipped) | 378 | 4 | 719 |
100 97 | Atkinson Hyperlegible Mono (the base, not shipped) | 359 | 13 | 730 |
101 - | IosevkaTerm Nerd Font Mono (retired 2026-08-17) | 17,827 | 0 | 0 |
102 98 | Departure Mono NF Mono (not in the image) | 11,468 | 5 | 557 |
103 99
104 100 **The monospace answer is still that there is no gap in what Alloy emits**, and it is now
@@ -136,7 +132,7 @@
136 132 those four.
137 133
138 134 **Departure's gaps only matter outside the running system.** It is not in the image
139 - (STACK.md records the 2026-08-05 audit), so it can only appear in a mockup, the README,
135 + (STACK.md), so it can only appear in a mockup, the README,
140 136 the social card or a splash plate. Of the eighteen codepoints Alloy emits it lacks five:
141 137 `⏎` (U+23CE), `␣` (U+2423), `▸` (U+25B8), `◂` (U+25C2), `✘` (U+2718). Four of those five
142 138 are starship and helix prompt furniture, which is exactly what a terminal mockup renders,
@@ -162,33 +158,24 @@
162 158
163 159 **Both house faces are variable, and fontconfig enumerates their named instances**, so
164 160 `fc-match monospace` answers `Quasi Mono Regular` rather than the file's own default
165 - instance — which is ExtraLight, because a cut keeps its base's default. Measured
166 - 2026-08-17 by installing the built faces and querying fontconfig directly, not on a
167 - booted image; the build asserts the same thing so the trap cannot reach one.
161 + instance, which is ExtraLight, because a cut keeps its base's default. Measured by
162 + installing the built faces and querying fontconfig directly rather than on a booted
163 + image; the build asserts the same thing so the trap cannot reach one.
168 164
169 - **Every family named is installed.** That is new as of 2026-08-16: nine of the thirteen
170 - names this list used to carry were not in the image, and they were deleted (GO task
171 - `6aeeb122`, settled by Max as "delete"; the edit is `29390a99`). Gone from `monospace`:
172 - Iosevka Term, Iosevka Nerd Font, Iosevka, DejaVu Sans Mono. From `sans-serif`: Inter,
173 - DejaVu Sans. From `serif`: Source Serif 4, Source Serif Pro, DejaVu Serif.
165 + **Every family the aliases name is installed, and none may name one that is not.** A name
166 + that is absent costs nothing at runtime, because `<prefer>` skips what is missing, and it
167 + is a reading hazard: the list looks like a depth of fallback that is not there.
174 168
175 - They cost nothing at runtime, because `<prefer>` skips what is absent, and they were not
176 - the emoji-alias defect (that alias changed where emoji resolved to; these changed
177 - nothing). They were a reading hazard: the list looked like a depth of fallback that was
178 - not there.
169 + **The `monospace` chain has one link.** If the house face ever failed to install,
170 + resolution does not reach a second Alloy pick. It drops straight into Fedora's generic
171 + rules (`45-latin.conf`, `60-latin.conf`) and lands on whatever those order first, which
172 + is the layer described below.
179 173
180 - **What the shortened `monospace` list makes visible is that the chain has one link.** If
181 - the house face ever failed to install, resolution does not reach a second Alloy pick. It
182 - drops straight into Fedora's generic rules (`45-latin.conf`, `60-latin.conf`) and lands
183 - on whatever those order first, which is the layer described below. The four dead Iosevka
184 - spellings obscured exactly that.
185 -
186 - **Removing Inter changed one behaviour, and it is the one deletion that was not only
187 - tidying.** Inter is not in the image, so it never served a request. What it did do was
188 - sit at position 2 of `sans-serif`, so anyone who installed Inter for an unrelated app
189 - silently repointed Alloy's whole UI font. Preferring Inter is still supported and is
190 - still what STACK.md describes: an edit to the alias and to `gtk-font-name`, which is a
191 - choice rather than a side effect of having the font on disk.
174 + **Inter is deliberately out of `sans-serif`.** It is not in the image, and a face named
175 + at position 2 of the alias means anyone who installs it for an unrelated app silently
176 + repoints Alloy's whole UI font. Preferring Inter is supported and is what STACK.md
177 + describes: an edit to the alias and to `gtk-font-name`, which is a choice rather than a
178 + side effect of having the font on disk.
192 179
193 180 **What actually catches a gap is the layer below this file**, and it is worth naming
194 181 because the chain above suggests otherwise:
@@ -199,9 +186,8 @@
199 186 2. `59-adwaita-mono-fonts.conf`, `59-liberation-*.conf`, the URW base-35 set: the real
200 187 substitutes present on disk for mono, sans and serif.
201 188 3. The Noto set for everything non-Latin: `NotoSans[wght].ttf` plus 24 per-script
202 - variable faces, `NotoSansCJK-VF.ttc` and `NotoSansMonoCJK-VF.ttc`. This is the layer
203 - the 2026-07-29 coverage fix added, and it is what stops an arbitrary web page from
204 - rendering as rows of missing glyphs.
189 + variable faces, `NotoSansCJK-VF.ttc` and `NotoSansMonoCJK-VF.ttc`. This is what stops
190 + an arbitrary web page from rendering as rows of missing glyphs.
205 191 4. Nothing, for emoji. That is the decided position, not a gap.
206 192
207 193 ## Re-running this
@@ -210,7 +196,7 @@
210 196 a checked-in tool that nobody runs drifts into a lie. Re-derive it by mounting the image
211 197 and reading `cmap` tables directly.
212 198
213 - Half of it no longer needs an image. The faces are cut rather than installed, so
199 + Half of it needs no image. The faces are cut rather than installed, so
214 200 `cargo run -- build quasi-mono` in `quasi-type` produces the exact file the image gets,
215 201 and `cargo run -- proof quasi-mono` rasterises it to a PNG — which is the check that
216 202 catches a wrong shape, since a `cmap` count cannot. What still needs the image is
@@ -12,20 +12,20 @@
12 12
13 13 ## Alloy's stance on the 2-in-1 form factor
14 14
15 - **Alloy stays keyboard-first, and the FW12 is supported as a laptop.** The fold-to-notes tablet gesture (a stylus notes canvas on hinge fold) was **shelved in the 2026-07-17 pivot**: it needs a stylus GUI (Rnote), which clashes with the TUI-first direction. The "Fold-to-notes" section below is kept as a record and revives only if a tablet UX returns.
15 + **Alloy stays keyboard-first, and the FW12 is supported as a laptop.** The fold-to-notes tablet gesture (a stylus notes canvas on hinge fold) is shelved: it needs a stylus GUI, which clashes with the TUI-first direction. The "Fold-to-notes" section below is that shelved design, and it revives only if a tablet UX returns.
16 16
17 17 The manifesto commits Alloy to a keyboard-driven tiling environment. A 2-in-1 does not change that thesis. In clamshell use:
18 18
19 19 - **Clamshell mode is the entire Alloy experience.** Sway and the full terminal-first stack, unchanged.
20 20 - **Touch and stylus in clamshell mode are pointer input, nothing more.** Tap to focus a window, drag to scroll, stylus behaves as a pointer. Sway and libinput already do this without Alloy code.
21 - - **Folding the screen back does nothing special** now that the notes flow is shelved; the session stays as-is.
21 + - **Folding the screen back does nothing special**; the session stays as-is.
22 22
23 23 This stance is deliberate. Building a general-purpose touch UI is a years-shaped project (Phosh, GNOME Shell-for-mobile, Plasma Mobile). Alloy is a months-shaped project, and a TUI-first one; a custom tablet shell is out of scope.
24 24
25 25 ## Display
26 26
27 27 - **Native panel:** 12.2" 1920x1200 (16:10), matte, IPS-class, ~185 PPI. Sits in the awkward middle where 1.0x is too small and 2.0x is too big.
28 - - **Alloy default scale:** 1.25x fractional under sway (`output eDP-1 scale 1.25`). No longer shipped as a system file naming this laptop: the installer reads the panel's size out of its EDID and writes the directive to `~/.config/sway/config.d/50-display.conf`, from the same generator `alloy display` uses, so the value is the machine's own and its owner can change it with one keypress. This panel is the measurement the rule is anchored on — ~185 PPI takes 1.25, which is `PPI_PER_SCALE` in `crates/alloy/src/display.rs`. sway handles the compositor side; terminal apps scale with the compositor, so there is no per-app fractional-scale work as there would have been for egui. Verify glyph crispness at v0 sanity check.
28 + - **Alloy default scale:** 1.25x fractional under sway (`output eDP-1 scale 1.25`). Not shipped as a system file naming this laptop: the installer reads the panel's size out of its EDID and writes the directive to `~/.config/sway/config.d/50-display.conf`, from the same generator `alloy display` uses, so the value is the machine's own and its owner can change it with one keypress. This panel is the measurement the rule is anchored on — ~185 PPI takes 1.25, which is `PPI_PER_SCALE` in `crates/alloy/src/display.rs`. sway handles the compositor side; terminal apps scale with the compositor, so there is no per-app fractional-scale work. Verify glyph crispness at v0 sanity check.
29 29 - **Contrast/color:** validate the DESIGN-LANGUAGE.md dark-mode L stops on this panel specifically (currently deferred in `todo.md`). Matte 400-nit IPS is roughly the panel class most Alloy users are likely to have; if the tokens miss here, they miss for the base audience.
30 30
31 31 Rejected: 1.0x default (glyphs too small at arm's length on 12.2"), 1.5x default (wastes usable columns in a scrollable-tiling model where horizontal density matters).
@@ -42,7 +42,7 @@
42 42
43 43 ## Fold-to-notes (SHELVED)
44 44
45 - **Shelved in the 2026-07-17 pivot** (TUI-first clashes with a stylus GUI). Kept as a record; the `alloy-hinged` daemon, the notes workspace, and the Rnote pick below are not on the roadmap and revive only if a tablet UX returns. The niri IPC calls below are historical.
45 + **Shelved**: TUI-first clashes with a stylus GUI. The `alloy-hinged` daemon, the notes workspace and the Rnote pick below are not on the roadmap and revive only if a tablet UX returns. The niri IPC calls below are written against a compositor Alloy does not ship.
46 46
47 47 The kernel exposes hinge state via `SW_TABLET_MODE` on Framework's ACPI/EC path. The shelved design consumed it for one purpose: switching to a dedicated notes workspace on fold, and back on unfold.
48 48
@@ -69,15 +69,13 @@
69 69 Framework 12 ships a fingerprint reader on the power button. Upstream Linux support is via `libfprint`; enrollment through `fprintd`. Fedora Silverblue includes the stack.
70 70
71 71 - Alloy configures PAM to allow fingerprint wherever the system asks who you are: at the lockscreen, and at the polkit prompt, which is the one `run0` and the console's writing views both raise. The file that matters for the second is `/etc/pam.d/polkit-1`, not `/etc/pam.d/sudo` — Alloy documents `run0` rather than `sudo` (wiki note `alloy-privilege`), so a fingerprint stack that only covered sudo would cover nothing a user is told to type.
72 - - Fingerprint unlock wires through swaylock's PAM stack (the authored egui lockscreen was dropped in the pivot). No Alloy code beyond PAM config.
72 + - Fingerprint unlock wires through swaylock's PAM stack. No Alloy code beyond PAM config.
73 73
74 74 ## Firmware (fwupd / LVFS)
75 75
76 76 Framework publishes firmware via LVFS. Fedora Silverblue ships `fwupd` and enables LVFS by default.
77 77
78 - Alloy's contribution: nothing. There is no first-boot firmware check and no prompt, decided 2026-08-05.
79 -
80 - This document previously specified one, on the grounds that a first-boot device with stale firmware is a bad impression on a laptop this dependent on EC updates. Two things retire that argument. Alloy publishes no image, so the ISO is built on the installer's own machine and is normally minted shortly before it is used; firmware that was current at build time is still current at first boot. And a first-boot prompt is a check nobody asked for, which is the thing the update position in [STACK.md](STACK.md) declines to do. Adding one here for firmware would carve an exception into a principle that is stated without exceptions.
78 + Alloy's contribution: nothing. There is no first-boot firmware check and no prompt. Alloy publishes no image, so the ISO is built on the installer's own machine shortly before it is used, and firmware current at build time is still current at first boot. A first-boot prompt is also a check nobody asked for, which is what the update position in [STACK.md](STACK.md) declines to do.
81 79
82 80 Someone who wants to know runs `fwupdmgr get-devices` and `fwupdmgr update`, which work and need nothing from Alloy. The assumption this rests on, and it is an assumption rather than a guarantee: anyone building their own ISO is savvy enough to update firmware when they care to.
83 81
@@ -2,11 +2,11 @@
2 2
3 3 The second device Alloy is being written against, and the first that is not Intel. This document records what the machine is, what it changes about assumptions the FW12 doc could make, and which of those changes are measured rather than assumed. Companion to [STACK.md](STACK.md), [HARDWARE-FW12.md](HARDWARE-FW12.md), [MIGRATION-FW13.md](MIGRATION-FW13.md), and wiki `alloy-fw13-migration`.
4 4
5 - Per-device docs are the pattern the FW12 doc set, and this is the file it predicted.
5 + Per-device docs are the pattern, and this is the FW13 one.
6 6
7 7 ## What has and has not been tested
8 8
9 - **Alloy has never run on this machine.** Every figure below was measured on 2026-08-11 from the machine's own running system, which is Pop!_OS 24.04 with kernel 6.17.9 under COSMIC on Wayland. Sysfs, EDID, `lspci`, and `fwupdmgr` do not care which distribution asks them, so the hardware facts carry; anything that depends on Alloy's own image, on sway, or on how a panel looks to a person does not, and is marked unverified.
9 + **Alloy has never run on this machine.** Every figure below was measured from the machine's own running system, Pop!_OS 24.04 with kernel 6.17.9 under COSMIC on Wayland. Sysfs, EDID, `lspci`, and `fwupdmgr` do not care which distribution asks them, so the hardware facts carry; anything that depends on Alloy's own image, on sway, or on how a panel looks to a person does not, and is marked unverified.
10 10
11 11 The one reboot that settles the unverified half is the boot test in `alloy-fw13-migration`: boot the SanDisk install medium, stop at disk selection, write nothing.
12 12
@@ -29,9 +29,7 @@
29 29
30 30 Two things follow.
31 31
32 - `detect_panel` filtered on `built_in()`, so the installer seeded the laptop panel and said nothing about the BenQ. A fw13 install finishing lid-closed on the desk therefore came up with a stanza for a panel that is off and nothing for the screen being used.
33 -
34 - **Fixed 2026-08-11.** `detect_outputs` seeds the connectors the kernel reports lit, falling back to the connected built-in panel when nothing is lit, which is what a text-console install with no CRTC bound produces and what the old rule always returned. The reasoning the built-in filter was defending survives the change: a monitor merely plugged into a machine being installed is connected and not enabled, so it is still not seeded. Run against this machine's own sysfs as it stands today, the generator now proposes `output DP-3 scale 1` and nothing for the dark panel.
32 + `detect_outputs` seeds the connectors the kernel reports lit, falling back to the connected built-in panel when nothing is lit, which is what a text-console install with no CRTC bound produces. Seeding on the built-in panel alone would leave a lid-closed desk install with a stanza for a panel that is off and nothing for the screen being used. A monitor merely plugged into a machine being installed is connected and not enabled, so it is still not seeded. Run against this machine's own sysfs, the generator proposes `output DP-3 scale 1` and nothing for the dark panel.
35 33
36 34 And 1.0x on a 163 PPI 28 inch panel is the first case where the ladder's rounding is visibly load-bearing rather than incidental. It may well be right at desk distance, where the FW12's arm's-length argument does not apply. It is worth an eye during the boot test, and it is a hint that viewing distance is the term the PPI rule leaves out.
37 35
@@ -40,20 +38,20 @@
40 38 ## Graphics
41 39
42 40 - **AMD Radeon 840M** integrated (PCI `1002:1114` rev c3, subsystem `f111:000b`), Krackan Point silicon, driven by in-tree `amdgpu`.
43 - - **Kernel floor, answered by version rather than by observation.** The worry recorded in the migration note was that amdgpu on this generation wants a recent kernel. Measured: it runs on 6.17.9 here. Fedora 43 ships the 6.17 series, so Alloy's base clears the same bar the working machine does. Nothing about this has been observed under Alloy itself.
41 + - **Kernel floor, answered by version rather than by observation.** amdgpu on this generation wants a recent kernel. Measured: it runs on 6.17.9 here. Fedora 43 ships the 6.17 series, so Alloy's base clears the same bar the working machine does. Nothing about this has been observed under Alloy itself.
44 42 - Every graphics paragraph in the FW12 doc is Intel and none of it carries. There is no equivalent Alloy-side work implied: the compositor scales, terminal apps scale with the compositor, and there is no per-app fractional-scale story to write here any more than there was there.
45 43
46 44 ## CPU and the build-host consequence
47 45
48 46 AMD Ryzen AI 5 340 with Radeon 840M, 6 cores and 12 threads, one socket.
49 47
50 - This is not a neutral spec line. fw13 is the only x86_64 build host in the tree: Bento's AppImages and the makeover publishes run here. Anything that takes the machine out of service, an install included, stops releases until it is rebuilt by hand, which is why the `~/Code` bootstrap work (infra, filed 2026-08-11) is a prerequisite for step 5 of the migration and not a nicety.
48 + This is not a neutral spec line. fw13 is the only x86_64 build host in the tree: Bento's AppImages and the makeover publishes run here. Anything that takes the machine out of service, an install included, stops releases until it is rebuilt by hand, which is why the `~/Code` bootstrap work (infra) is a prerequisite for step 5 of the migration and not a nicety.
51 49
52 - ## Storage, and a correction
50 + ## Storage
53 51
54 52 One NVMe device: `nvme0n1`, WD BLACK SN7100 2TB, in the M.2 2280 slot.
55 53
56 - **There is no free second M.2 slot for storage.** The migration note listed "an NVMe in the second (2230) M.2 slot" as an option pending confirmation that the slot exists and is free. It exists and it is not free: the 2230 socket carries the MediaTek Wi-Fi module (`c0:00.0`, behind root port `02.3`). The only PCIe root ports with nothing behind them are the USB4 ones.
54 + **There is no free second M.2 slot for storage.** The 2230 socket exists and carries the MediaTek Wi-Fi module (`c0:00.0`, behind root port `02.3`). The only PCIe root ports with nothing behind them are the USB4 ones.
57 55
58 56 So the disk options for an install are the offline shrink of `nvme0n1p3`, or external. Nothing here changes that the boot test needs neither.
59 57
@@ -1,6 +1,6 @@
1 1 # Alloy Iconography
2 2
3 - Three icon tiers with non-overlapping roles, and the division is the point: **functional glyphs identify inside the chrome, application icons identify software, hero illustrations anchor a surface.** Since the 2026-07-17 pivot to TUIs, the functional tier does nearly all the work; the hero tier survives only where a surface is still drawn as an image rather than as terminal cells.
3 + Three icon tiers with non-overlapping roles, and the division is the point: **functional glyphs identify inside the chrome, application icons identify software, hero illustrations anchor a surface.** The functional tier does nearly all the work; the hero tier applies only where a surface is drawn as an image rather than as terminal cells.
4 4
5 5 The application tier is the newest and is the one with a visual language of its own: Mac OS 8, chunky and dithered, where the hero tier is a mid-century relay station rendered as service-manual plates. Two languages in one system is deliberate rather than drift. They never appear at the same size, in the same place, or for the same purpose: an app icon says which program this is, at 32 pixels, in a launcher or a dialog; a hero plate says which system you are looking at, full-bleed, on a lock screen or a splash. Write any new icon into whichever tier its job belongs to rather than reconciling the two.
6 6
@@ -8,21 +8,21 @@
8 8
9 9 For small, identifying icons in chrome: list-item markers, pane/status indicators, file-type glyphs, severity glyphs paired with accent text, footer keymap hints.
10 10
11 - - **Source:** the marks drawn into the house face itself — the triangles, the arrows, the box drawing and block elements, the ballot X and the whitespace renders. `quasi-type`'s `glyphs/manifest.toml` is the set, and it is the same drawing in a terminal, a webview and an egui panel. No separate icon font, and as of 2026-08-17 no Private Use Area anywhere in the image.
11 + - **Source:** the marks drawn into the house face itself — the triangles, the arrows, the box drawing and block elements, the ballot X and the whitespace renders. `quasi-type`'s `glyphs/manifest.toml` is the set, and it is the same drawing in a terminal, a webview and an egui panel. No separate icon font, and no Private Use Area anywhere in the image.
12 12 - **Rendering:** treated as text, a cell wide. Inherits the surrounding text's ramp position and weight. Never accent-colored unless paired with information (an error glyph beside an error message uses `accent-error`).
13 13 - **Size:** the cell. There is no standalone sizing scale in a terminal.
14 14
15 15 This tier does the overwhelming majority of Alloy's iconography now. It is uncomplicated, and in a terminal it is the only icon form that renders inline.
16 16
17 - **The two readings this section used to leave open are settled, and the font change settled them.** It said either the semantic-name accessor was work not yet done, or the geometric-shapes set was the real functional tier and the Nerd Font framing should be retired. It is the second: measured 2026-08-06 ([FONTS.md](FONTS.md)), nothing in Alloy's own source or in `alloy_tui` emits a single Private Use Area codepoint, and what the widgets draw is the geometric-shapes block — `▶` and `▸` for selection, `▾` for disclosure, the block elements for gauges.
17 + **The functional tier is the geometric-shapes set, not a Nerd Font set.** Measured ([FONTS.md](FONTS.md)), nothing in Alloy's own source or in `alloy_tui` emits a single Private Use Area codepoint, and what the widgets draw is the geometric-shapes block: `▶` and `▸` for selection, `▾` for disclosure, the block elements for gauges.
18 18
19 - As of 2026-08-17 there is no PUA left to reach for. The image ships `Quasi Mono` and `Quasi Body`, cut faces carrying the house glyph set, and yazi's built-in Nerd Font filetype icons — the one live PUA consumer on the machine — are off (STACK.md#fonts). So the functional tier is the house set by construction: a mark is in it because it carries meaning the UI depends on, and every renderer draws the same outline.
19 + There is no PUA to reach for. The image ships `Quasi Mono` and `Quasi Body`, cut faces carrying the house glyph set, and yazi's built-in Nerd Font filetype icons — the one live PUA consumer on the machine — are off (STACK.md#fonts). So the functional tier is the house set by construction: a mark is in it because it carries meaning the UI depends on, and every renderer draws the same outline.
20 20
21 21 What is still work rather than fact is the semantic-name accessor (`severity_glyph(Urgency)`, `pane_marker`), which does not exist. Widgets reach for the codepoint directly. That is a centralizing mechanism worth having and it belongs with the icon-set work, not here.
22 22
23 23 ## Application tier: a generic set Alloy owns (decided, not yet drawn)
24 24
25 - Decided 2026-08-06 (Max): a layout set we own the copyright to, covering the basic items Alloy ships, generic rather than per-application. One console icon, not one per terminal emulator. The config TUI is where a user links an installed application to one of them.
25 + A layout set we own the copyright to, covering the basic items Alloy ships, generic rather than per-application. One console icon, not one per terminal emulator. The config TUI is where a user links an installed application to one of them.
26 26
27 27 **The scoping question dissolves because the set covers kinds of application, not applications.** Nobody hand-draws a thousand icons; there are about a dozen kinds. And the fallback question dissolves too, because a gap is closed by the user linking an app to an icon rather than by borrowing somebody else's artwork.
28 28
@@ -32,7 +32,7 @@
32 32
33 33 ### The categories, from what the image ships
34 34
35 - Counted 2026-08-06 against the built image rather than invented (the browser row is Firefox since 2026-08-18; it was Helium when this was counted, and the entry count is unchanged either way): `/usr/share/applications` holds 17 desktop entries, of which 7 are not `NoDisplay`. That is the whole visible surface Alloy itself installs.
35 + Counted against the built image rather than invented: `/usr/share/applications` holds 17 desktop entries, of which 7 are not `NoDisplay`. That is the whole visible surface Alloy itself installs.
36 36
37 37 | Category | What ships in it | XDG category a third-party app would declare |
38 38 |---|---|---|
@@ -51,7 +51,7 @@
51 51
52 52 ### Style, and what is not done
53 53
54 - Mac OS 8: chunky, dithered, 32x32 native, larger sizes drawn rather than scaled (Max, 2026-08-05). **Nothing is drawn yet**, and drawing is the remaining work along with the config TUI screen that does the linking. The category list above is the input that work needed.
54 + Mac OS 8: chunky, dithered, 32x32 native, larger sizes drawn rather than scaled. **Nothing is drawn yet**, and drawing is the remaining work along with the config TUI screen that does the linking. The category list above is the input that work needed.
55 55
56 56 Note the pre-launch state is what it is by accident rather than by choice: with no icon theme installed at all, third-party apps currently resolve to nothing.
57 57
@@ -61,13 +61,13 @@
61 61
62 62 ## Hero tier: isometric line illustrations (reserved, mostly deferred)
63 63
64 - Large, declarative iconography lived on GUI surfaces (egui lockscreen ornament, launcher app icons, settings headers, marquee-app heroes). Those surfaces were dropped in the pivot, so the hero tier lost most of its homes. It survives only where a graphical surface remains:
64 + Large, declarative iconography belongs on graphical surfaces, and Alloy has few. It applies only where one remains:
65 65
66 66 - **swaylock background**: the adopted lockscreen can display a background image; a hero plate belongs here.
67 67 - **First-boot splash**: renderable via shop's kitty-graphics protocol, or shown before the session starts.
68 68 - **Brand assets** outside the running system (repo social card, README).
69 69
70 - Everything else that used to carry a hero illustration is now terminal chrome and uses the functional glyph tier instead. The hero tier is therefore **deferred**: the style below is preserved so the identity is not lost and so the surviving surfaces have a spec, but no hero illustrations are on the critical path for the TUI work.
70 + Every other surface is terminal chrome and uses the functional glyph tier instead. The hero tier is therefore **deferred**: the style below is preserved so the identity is not lost and so the surviving surfaces have a spec, but no hero illustrations are on the critical path for the TUI work.
71 71
72 72 ### Theme: a mid-century electromechanical relay station
73 73
M docs/IMAGE.md +29 -37
@@ -1,6 +1,6 @@
1 1 # Image composition
2 2
3 - How Alloy is built and delivered. bootc + Containerfile, source on `makenot.work`, built locally and natively, and **no image published anywhere, ever** (decided 2026-07-30, see "Registry" below). Companion to [STACK.md](STACK.md).
3 + How Alloy is built and delivered. bootc + Containerfile, source on `makenot.work`, built locally and natively, and **no image published anywhere, ever** (see "Registry" below). Companion to [STACK.md](STACK.md).
4 4
5 5 ## Landscape
6 6
@@ -28,7 +28,7 @@
28 28
29 29 **The nuance:** Alloy takes the ublue-style *convention* (a Containerfile over a bootc base) without ublue's base image: `FROM registry.fedoraproject.org/fedora-bootc:43`, not `FROM ghcr.io/ublue-os/main`. That is what "alongside ublue, not downstream" resolves to in practice.
30 30
31 - This line used to say the convention included an OCI registry and a CI-driven build. It does not, and that is where Alloy parts company with ublue rather than a detail of it: ublue's whole model is a published image somebody else builds for you, and Alloy's is source you build yourself. quay.io appears in this repo in exactly two places, both `FROM` lines pulling Fedora's own bases, and nothing is ever pushed there.
31 + The convention does not include an OCI registry or a CI-driven build, and that is where Alloy parts company with ublue: ublue's model is a published image somebody else builds for you, and Alloy's is source you build yourself. quay.io appears in this repo in exactly two places, both `FROM` lines pulling Fedora's own bases, and nothing is ever pushed there.
32 32
33 33 ### "Install Silverblue and run our script" (non-option)
34 34
@@ -44,27 +44,25 @@
44 44
45 45 Alloy publishes its source on Make Creative's own platform, for the reason the project holds elsewhere: independence from big-tech-monopoly infrastructure at the layer where users install software, and hosting our own source rather than renting someone's. `astra` is the private mirror and `srht` is a backup remote, not the public face. See `~/Code/CLAUDE.md`, "What each remote is for".
46 46
47 - **Current state:** live since 2026-07-30. The source is the distributed artifact, so this URL is not a courtesy listing, it is where Alloy comes from.
47 + The source is the distributed artifact, so this URL is not a courtesy listing, it is where Alloy comes from.
48 48
49 49 ### CI: none. Builds are local and native.
50 50
51 - Alloy has no hosted CI, and the sourcehut build that used to stand in for one is gone (`builds.disabled/alloy-image.yml`, deleted; it had been parked outside `.builds/` since `8d0bca6` and pinned `image: fedora/41`, two releases behind the base).
51 + Alloy has no hosted CI.
52 52
53 53 The image is built by `build/build-image.sh` (disk images, via bootc-image-builder) and `build/build-iso.sh` (the installer ISO) on fw13, against rootful podman. That is the whole build story today, and it is a deliberate consequence of two standing rules rather than an accident: builds are native per architecture and nothing is cross-compiled, and app binaries are never built on the production host.
54 54
55 55 **Both architectures build an installer, not just x86_64.** `build/make-iso.sh` takes its architecture from `uname -m` and maps it to a GRUB target and a removable-media binary name: `x86_64-efi`/`BOOTX64.EFI`, `arm64-efi`/`BOOTAA64.EFI`. Anything else is refused by name rather than defaulted. The builder installs the matching `grub2-efi-*-modules` package the same way, and the script fails early naming that package if the modules are not there, rather than several minutes into a squashfs. `--arch` on `build/build-iso.sh` overrides the detection; it exercises the GRUB half from the other architecture and does not produce a bootable medium, because everything around the boot chain is still the native image.
56 56
57 - Arm is an installer target and not a dd-the-image special case (decided 2026-08-06). The installer is the only place LUKS, the recovery phrase, the ssh key and the hostname are collected, and a written disk image collects none of them, so an arm machine installed by that route would be installed by a path no user of Alloy takes. `alloy install` has never been run on aarch64, so a medium existing is necessary and not sufficient.
57 + Arm is an installer target and not a dd-the-image special case. The installer is the only place LUKS, the recovery phrase, the ssh key and the hostname are collected, and a written disk image collects none of them, so an arm machine installed by that route would be installed by a path no user of Alloy takes. `alloy install` has never been run on aarch64, so a medium existing is necessary and not sufficient.
58 58
59 - An always-on x86_64 builder is still wanted for development. It is no longer between Alloy and other people running it: nothing has to be built centrally for a stranger to install Alloy, because the stranger does the building. astra is always-on but aarch64, fw13 is x86_64 and sleeps aggressively, and the Fedora build host meant to replace Pop!_OS on fw13 is where that resolves whenever it does.
59 + An always-on x86_64 builder is wanted for development. It stands between nobody and running Alloy: nothing has to be built centrally for a stranger to install Alloy, because the stranger does the building. astra is always-on but aarch64, fw13 is x86_64 and sleeps aggressively, and the Fedora build host meant to replace Pop!_OS on fw13 is where that resolves whenever it does.
60 60
61 - What deleting the sourcehut job actually cost is worth naming rather than glossing: it ran a build-and-lint smoke test on every push, and now nothing validates the Containerfile except someone remembering to build. That gap is real and is independent of distribution.
61 + Nothing validates the Containerfile except someone remembering to build it. That gap is real and is independent of distribution.
62 62
63 63 ### Registry: none, and this is settled rather than deferred
64 64
65 - **Nothing is published, and nothing will be.** Distribution is the builder, not the artifact (decided 2026-07-30). Users take the source, configure it, build the ISO on their own machine, and write it to a medium.
66 -
67 - This section previously read as "quay.io once there is an image worth distributing", which framed publishing as inevitable and merely early. It is not the plan. `quay.io/alloy/alloy` is not a reserved future address, and the alternatives that were weighed against it (ghcr.io, Docker Hub, self-hosted) are moot rather than rejected.
65 + **Nothing is published, and nothing will be.** Distribution is the builder, not the artifact. Users take the source, configure it, build the ISO on their own machine, and write it to a medium. `quay.io/alloy/alloy` is not a reserved future address, and the hosts that would otherwise be weighed against it (ghcr.io, Docker Hub, self-hosted) are moot rather than rejected.
68 66
69 67 Three things follow, and they are the point rather than side effects:
70 68
@@ -90,13 +88,13 @@
90 88
91 89 The reason it exists is the measured shape of the loop. A config-only change is fifteen seconds of `podman build` and then four minutes of ISO assembly, and the ISO is installer media nobody is going to install from when the question is whether a sway binding works. Building one to see a config change is the single most expensive habit available here.
92 90
93 - So "we publish nothing" costs a build, not a machine. What a user gives up against a published image is the time to build it, and what they gain is that the thing they boot is the thing they configured. An earlier draft of this file said updating meant rebuilding *and reinstalling*; that was wrong, and it made the no-registry decision look far more expensive than it is.
91 + So "we publish nothing" costs a build, not a machine. What a user gives up against a published image is the time to build it, and what they gain is that the thing they boot is the thing they configured. Updating is a rebuild, not a reinstall.
94 92
95 93 ## Layer structure
96 94
97 95 The Containerfile at the repo root (see `Containerfile`) sketches:
98 96
99 - 1. **Base:** `FROM registry.fedoraproject.org/fedora-bootc:43`. Fedora's own registry rather than quay, and it is a retention choice: quay garbage-collects a digest the moment its tag moves off it, three times taking the pinned base with it and leaving the whole install path unbuildable. Every digest quay had dropped was still served at registry.fedoraproject.org when that was measured on 2026-08-25, and the tag resolves to the same index digest at both, so it is the same content with a longer memory. The mirror that makes retention ours is still owed (`build/refresh-base-digests.sh` says why).
97 + 1. **Base:** `FROM registry.fedoraproject.org/fedora-bootc:43`. Fedora's own registry rather than quay, and it is a retention choice: quay garbage-collects a digest the moment its tag moves off it, three times taking the pinned base with it and leaving the whole install path unbuildable. Every digest quay had dropped was still served at registry.fedoraproject.org when that was measured, and the tag resolves to the same index digest at both, so it is the same content with a longer memory. The mirror that makes retention ours is still owed (`build/refresh-base-digests.sh` says why).
100 98 2. **Third-party repos:** Tailscale, any COPRs Alloy depends on for packages not in Fedora main.
101 99 3. **Package additions:** the full Alloy stack from [STACK.md](STACK.md): compositor, bar, launcher, notifications, terminal, editor, shell, viewers, utilities, continuity daemons, fonts, themes.
102 100 4. **Package removals:** stock Silverblue desktop pieces Alloy replaces (gnome-shell, gdm; the latter gated on the greeter pick).
@@ -104,7 +102,7 @@
104 102 6. **Rendered tree:** everything in the image that carries a color is not in the repo as a finished file. `templates/` holds it with the palette left as tokens, and `skelgen` renders it against the two Akari themes into a second tree that mirrors `/` the same way the config tree does. Themed skeleton files render twice: the light one lands at `etc/skel/<rel>`, the dark one at `usr/share/alloy/skel-night/<rel>`, and `alloy theme apply` copies whichever the user's mode file names into `$HOME` at login. The build asserts the two trees are a bijection and that they do not overlap the repo's own `etc/skel`, because a themed file that quietly loses its dark render leaves a light sway border on a dark desktop and nothing else.
105 103 7. **Systemd presets:** which services are enabled by default (syncthing off by default, gammastep off until enrolled, alloy-hinged conditionally on FW12, etc.).
106 104 8. **Branding:** os-release, plymouth splash. The build stamps the image's build number into os-release here; see [Version fields](#version-fields). It stamps the profile in the same way, rewriting the committed `VARIANT="<profile>"` and `VARIANT_ID=<profile>` placeholders to `Client`/`client` or `Server`/`server`, and failing if either placeholder survives. Only the profile: the tag variants (`firewall-server`, `usbgate-server`) are names chosen by whoever runs the build, and the Containerfile is never told which one it is making.
107 - 9. **Validation:** `bootc container lint --fatal-warnings` runs at build. Fatal since 2026-08-26: it was advisory before that, and spent an unknown number of builds reporting three warnings on `server` and four on `client` that nothing read. Two of its checks constrain what the image may carry, and both are about the upgrade path rather than about tidiness. `var-tmpfiles` wants every directory in `/var` declared in a `tmpfiles.d` file, and `sysusers` wants every account in `/etc/passwd` declared in a `sysusers.d` file, because `/var` and `/etc` are per-machine: `bootc install` copies the image's copies into the stateroot, while an upgrade re-syncs `/usr` and leaves both alone. Content that exists only because a package's `%post` or a `useradd` ran during a build therefore reaches a fresh install and never reaches a machine that upgrades into the image. Alloy's declarations are `usr/lib/tmpfiles.d/50-alloy-var.conf` (plus a `-client` half deleted on `server`, whose lines name users only that profile has) and `usr/lib/sysusers.d/50-alloy-greeter.conf`. **A package added here that drops a new `/var` directory or a new account fails the build until it gets a line in one of them.** The build host's own leavings are deleted rather than declared, in the sweep immediately above the lint: dnf's caches and log, the ldconfig aux-cache, the appstream catalogs, authselect's checksum, and everything under `/run`.
105 + 9. **Validation:** `bootc container lint --fatal-warnings` runs at build, and its warnings are fatal rather than advisory. Two of its checks constrain what the image may carry, and both are about the upgrade path rather than about tidiness. `var-tmpfiles` wants every directory in `/var` declared in a `tmpfiles.d` file, and `sysusers` wants every account in `/etc/passwd` declared in a `sysusers.d` file, because `/var` and `/etc` are per-machine: `bootc install` copies the image's copies into the stateroot, while an upgrade re-syncs `/usr` and leaves both alone. Content that exists only because a package's `%post` or a `useradd` ran during a build therefore reaches a fresh install and never reaches a machine that upgrades into the image. Alloy's declarations are `usr/lib/tmpfiles.d/50-alloy-var.conf` (plus a `-client` half deleted on `server`, whose lines name users only that profile has) and `usr/lib/sysusers.d/50-alloy-greeter.conf`. **A package added here that drops a new `/var` directory or a new account fails the build until it gets a line in one of them.** The build host's own leavings are deleted rather than declared, in the sweep immediately above the lint: dnf's caches and log, the ldconfig aux-cache, the appstream catalogs, authselect's checksum, and everything under `/run`.
108 106
109 107 **A package added here gets a line in `crates/alloy/credits.toml`.** The installer's last screen names the projects Alloy ships and their licenses, off a hand-curated manifest rather than a generated closure, so nothing adds itself. The manifest is embedded in the console binary with `include_str!`, which means the page cannot go missing on installer media or a read-only deployment and also means a manifest edit needs a rebuild. Its own header says which license to record: for anything packaged out of Rust or Go, Fedora's `%{LICENSE}` is the whole vendored closure rather than the project's own terms, so read upstream's LICENSE for those and use `rpm -q --qf '%{LICENSE}'` only for the C packages.
110 108
@@ -112,21 +110,16 @@
112 110
113 111 ## Size
114 112
115 - Measured 2026-08-17 against `localhost/alloy:clip-client`, the client profile with
113 + Measured against `localhost/alloy:clip-client`, the client profile with
116 114 `BROWSER=helium LANGS=rust,go TRIM=unused`. It was 6.19 GB as an image and 5.26 GB
117 115 deployed, and the 0.9 GB between those two numbers is the subject of half this section.
118 116
119 - Every figure below is from that build, so they describe an image built with Go in it.
120 - The default became `LANGS=rust` later the same day and then empty on 2026-08-27, which
121 - takes the whole language layer out of a stock image; the numbers are left as measured
122 - rather than adjusted on paper. A default mint today carries no toolchain at all, so the
123 - 778 MB language row below is a layer only a machine that asked for one builds.
124 -
125 - The same applies to the browser, and more sharply. `BROWSER=helium` was removed on
126 - 2026-08-18 and Firefox is the only answer now, so the browser layer below is a
127 - measurement of a build this tree can no longer produce. It is left as measured for the
128 - same reason: an adjusted number is a guess wearing a measurement's clothes. Re-measure
129 - the browser layer on the next full build.
117 + Every figure below is from that build, so they describe an image built with Go in it and
118 + with a browser this tree no longer offers. `LANGS` defaults to empty, so a stock mint
119 + carries no toolchain and does not build the 778 MB language row at all, and Firefox's
120 + layer is unmeasured. The numbers are left as measured rather than adjusted on paper,
121 + because an adjusted number is a guess wearing a measurement's clothes. Re-measure the
122 + browser layer on the next full build.
130 123
131 124 Two sizes, and they answer different questions. **Image size** is the sum of the layers:
132 125 what a registry stores and what `bootc upgrade` moves. **Deployed size** is the final
@@ -140,8 +133,8 @@
140 133 | Size | Layer | Why it is that size |
141 134 |------|-------|---------------------|
142 135 | 1.44 GB | sway, `xdg-desktop-portal{,-gtk,-wlr}`, the session | The product. This is the desktop. |
143 - | 778 MB | `LANGS=rust,go` | As measured. Go is 230 MB of this layer, and 356 MB by exclusive closure once the packages only it pulls in are counted; that gap is the two questions the measurements answer, not a disagreement. Go left the default on 2026-08-17 and rust on 2026-08-27, so a stock image does not build this layer at all. Argued at `ARG LANGS`. |
144 - | 674 MB | `helium-browser-bin` | The product. Superseded: this is Helium, removed 2026-08-18, and Firefox's layer is unmeasured. |
136 + | 778 MB | `LANGS=rust,go` | As measured. Go is 230 MB of this layer, and 356 MB by exclusive closure once the packages only it pulls in are counted; that gap is the two questions the measurements answer, not a disagreement. `LANGS` is empty by default, so a stock image does not build this layer at all. Argued at `ARG LANGS`. |
137 + | 674 MB | the browser | Measured against Helium; Firefox's layer is unmeasured. |
145 138 | 663 MB | Base package list | The stack, per [STACK.md](STACK.md). |
146 139 | 170 MB | initramfs + `rpmostree-unpackaged-content` | Bootable image. |
147 140 | 147 MB | cups + cups-filters | Printing, client profile only; the server profile presets it off. |
@@ -152,8 +145,7 @@
152 145
153 146 ### Packages that look wrong and are not
154 147
155 - - **`helix-parsers`, 185 MB.** Named explicitly at alloy@11291bd rather than losing
156 - syntax highlighting. Settled.
148 + - **`helix-parsers`, 185 MB.** Named explicitly rather than losing syntax highlighting.
157 149 - **`rust-std-static`, 166 MB.** The name suggests static-musl targets nothing here
158 150 builds. It is the standard library: `rust` carries a versioned hard requirement on it,
159 151 and removing it takes `rust` and `cargo` with it. Not separable from `LANGS=rust`, and
@@ -169,7 +161,7 @@
169 161 requires `libflite.so.1` and five voice libraries for its text-to-speech filter. mpv
170 162 links both libraries directly, `codec2` and `flite` are the only packages providing
171 163 those sonames, and `ffmpeg-free` is the only ffmpeg in the configured repos, so there
172 - is no lighter combination to switch to. Checked 2026-08-17. Dropping 36 MB here means
164 + is no lighter combination to switch to. Dropping 36 MB here means
173 165 building ffmpeg ourselves, which is not worth 36 MB.
174 166 - **`mesa-vulkan-drivers`, 153 MB.** Twelve ICDs, no rpm dependents at all, because the
175 167 Vulkan loader opens one by dlopen off a JSON manifest. The x86_64 ones are load-bearing:
@@ -208,7 +200,7 @@
208 200 qemu-user-static set are ~290 MB off the disk) and worth knowing before anyone tries to
209 201 shrink the image by removing more.
210 202
211 - ### What was cut, 2026-08-17
203 + ### What is cut from the image
212 204
213 205 - **Repo metadata, ~200 MB.** `terra-release` is 674 bytes and its layer was 238 MB of
214 206 `/var/cache/libdnf5`, cleaned by a later layer and therefore never reclaimed. `dnf clean
@@ -260,7 +252,7 @@
260 252 faces for a covered script rather than coverage.
261 253
262 254 None of this is a race to a number. Two of the four largest layers are the desktop and the
263 - browser and both are the product; the point is that every large thing in this image is now
255 + browser and both are the product; the point is that every large thing in this image is
264 256 either explained above or gone.
265 257
266 258 ## Version fields
@@ -280,7 +272,7 @@
280 272
281 273 The build stamp is `<YYYYMMDD>.<serial>`, UTC, and is not in the committed os-release. A placeholder there would be a lie on any machine where the stamping step silently stopped working, so the committed file says `(build <n>, ...)` and the build fails if a literal `<n>` survives. `build/build-image.sh` and `build/build-iso.sh` always pass a stamp (the serial is the UTC time of day, so same-day rebuilds differ without a counter kept anywhere); a bare `podman build` past them passes none, and an unstamped image reports `0.1 (Fedora 43)` rather than inventing a number.
282 274
283 - A commit count was considered as the stamp and rejected: it is identical across rebuilds of one commit, which is exactly the pair the field exists to tell apart.
275 + Not a commit count: that is identical across rebuilds of one commit, which is exactly the pair the field exists to tell apart.
284 276
285 277 `/etc/dnf/vars/releasever` states the base version too, and stays independent of `ALLOY_BASE` on purpose. It is a file, and it overrides `$releasever` expansion whatever os-release says; a pin readable out of the thing it overrides is not a pin.
286 278
@@ -305,8 +297,8 @@
305 297
306 298 - [ ] Verify which Alloy packages are in Fedora main vs. need COPRs. Candidates that may need COPRs: `satty`, `wl-screenrec` (depending on Fedora version). Audit at v0 packaging time.
307 299 - [ ] `bootc-image-builder` for ISO generation. First-time-user path is `bootc install` from a live environment; the ISO is what makes that a smooth experience. Verify the ISO builder handles Alloy's specific package set.
308 - - [x] **Containerfile stage policy: multi-stage only where a non-Fedora Rust binary needs to ship.** Currently that is one stage, `rust-build`, and it builds the Alloy console. Everything else comes from RPM in the runtime stage. Do not add stages preemptively. Corrected 2026-07-19: this line named wl-screenrec as the stage's occupant, which was stale — wl-screenrec was deferred rather than shipped, so for a while there was no stage at all. The console is the case the policy was written for and did not anticipate: it is the one binary Alloy authors, so no repo can ever carry it, and a distro whose headline surface is its console cannot ship without one. It is built on `fedora:43` rather than on the dev box so the toolchain and glibc match the runtime stage.
300 + - [x] **Containerfile stage policy: multi-stage only where a non-Fedora Rust binary needs to ship.** Currently that is one stage, `rust-build`, and it builds the Alloy console. Everything else comes from RPM in the runtime stage. Do not add stages preemptively. The console is the case the policy exists for: it is the one binary Alloy authors, so no repo can ever carry it, and a distro whose headline surface is its console cannot ship without one. It is built on `fedora:43` rather than on the dev box so the toolchain and glibc match the runtime stage.
309 301 - [ ] **An always-on x86_64 builder.** The blocker under "CI" above, and the thing the signing key and the weekly base-update rebuild both wait on. astra is always-on and aarch64; fw13 is x86_64 and sleeps.
310 - - [ ] **Language toolchains selected per image rather than for everyone.** The Rust toolchain went into the base on 2026-07-26 because Alloy's audience is developers and because the build-host role needs cargo present (sandod compiles in its own workdir and will not run without it). It costs 610 MiB installed, which is the largest single line in the package block and roughly twelve times the hardware-health group. One number for every install is the wrong shape as soon as a second language is wanted: a Go or Python or C toolchain each carries a comparable bill, and no machine wants all of them. Minting is where this resolves, since a per-machine image is already being produced with a baked key, so the toolchain set is another mint-time input rather than a new mechanism. Open: whether the base keeps Rust as the default when selection exists, or drops to none and makes every toolchain a choice. See [[alloy-image-minting]].
311 - - [x] Source hosting: **`makenot.work/git/max/alloy`**, our own platform. The old answer here was the sr.ht account `~maxmj` for v0 with LLC-owned `~makecreative` as the long-term target; sourcehut is now a backup remote and that migration question retires with it.
312 - - [x] ~~**Publish `:latest`, `:<fedora-version>`, and `:<fedora-version>-YYYYMMDD` tags.**~~ **Struck 2026-08-06.** A tagging scheme is a publishing question and nothing is published, so there is nothing for it to name. The local build tags `localhost/alloy:local` and a machine adopts it with `bootc switch --transport containers-storage`; reproducibility comes from the source and the pinned base digests rather than from a dated tag. Kept as a struck line rather than deleted, because the scheme reads as an obvious thing to want and this is the answer to whoever proposes it next.
302 + - [ ] **Language toolchains selected per image rather than for everyone.** A Rust toolchain in the base serves Alloy's developer audience and the build-host role, which needs cargo present (sandod compiles in its own workdir and will not run without it). It costs 610 MiB installed, which is the largest single line in the package block and roughly twelve times the hardware-health group. One number for every install is the wrong shape as soon as a second language is wanted: a Go or Python or C toolchain each carries a comparable bill, and no machine wants all of them. Minting is where this resolves, since a per-machine image is already being produced with a baked key, so the toolchain set is another mint-time input rather than a new mechanism. Open: whether the base keeps Rust as the default when selection exists, or drops to none and makes every toolchain a choice. See [[alloy-image-minting]].
303 + - [x] Source hosting: **`makenot.work/git/max/alloy`**, our own platform. sourcehut is a backup remote.
304 + - [x] ~~**Publish `:latest`, `:<fedora-version>`, and `:<fedora-version>-YYYYMMDD` tags.**~~ **Struck.** A tagging scheme is a publishing question and nothing is published, so there is nothing for it to name. The local build tags `localhost/alloy:local` and a machine adopts it with `bootc switch --transport containers-storage`; reproducibility comes from the source and the pinned base digests rather than from a dated tag. Kept as a struck line rather than deleted, because the scheme reads as an obvious thing to want and this is the answer to whoever proposes it next.
@@ -10,8 +10,8 @@
10 10
11 11 ## What Alloy is not
12 12
13 - - Not a from-scratch package manager. The previous `sap` design (typed configuration language, federated curator registry, content-addressed outputs) is shelved; we live on rpm-ostree.
14 - - Not a from-scratch distro. The previous `mountaineer` design (curated Alpine, apk-on-`/etc/apk/world`, s6 init) is shelved; we live on Fedora Silverblue.
13 + - Not a from-scratch package manager. Alloy lives on rpm-ostree.
14 + - Not a from-scratch distro. Alloy lives on Fedora Silverblue.
15 15 - Not a ublue spin. Alloy is **alongside** Universal Blue (Bluefin, Bazzite, Aurora), not downstream of it; the name was picked specifically to avoid the ublue "Blue\*" prefix convention.
16 16 - Not a gaming distro. The audience overlaps with Omarchy and Omakub, not with Bazzite.
17 17 - Not a GNOME spin. Alloy's authored design system is custom-drawn (ratatui) and does not pick up libadwaita theming.
@@ -37,7 +37,7 @@
37 37
38 38 2. **Native to Alloy's design system, not native to GNOME.** Alloy authors in ratatui and maintains a design-system crate, `alloy_tui`: a palette, themed widgets, footer chrome, and reserved keys that the authored console and any future authored TUI pull from. Heterogeneity with adopted apps (sway's own chrome, swaylock, mako) is a knowing trade, not an accident.
39 39
40 - 3. **Non-reactive by principle.** Alloy avoids the reactive UI pattern in everything it authors. State-to-view binding, declarative property graphs, async data trickling into UI components, the cluster of patterns that produces staged appearance, animation creep, hover-everywhere, and state-divergence bugs, is the "web-shaped feel" Alloy refuses on principle. ratatui is immediate-mode: it renders the whole frame from current state each cycle, making state divergence architecturally impossible. (egui, immediate-mode GUI, shares this property; Alloy no longer authors GUI apps, but the principle is the same one and would apply if it ever did.)
40 + 3. **Non-reactive by principle.** Alloy avoids the reactive UI pattern in everything it authors. State-to-view binding, declarative property graphs, async data trickling into UI components, the cluster of patterns that produces staged appearance, animation creep, hover-everywhere, and state-divergence bugs, is the "web-shaped feel" Alloy refuses on principle. ratatui is immediate-mode: it renders the whole frame from current state each cycle, making state divergence architecturally impossible. The same principle applies to any immediate-mode GUI toolkit.
41 41
42 42 4. **Backwards compatible with hardware, not with software.** Same principle as `_meta`'s cross-cutting rule. Operators are expected to be on the current Alloy release; hardware support is broad.
43 43
@@ -53,7 +53,7 @@
53 53
54 54 **Alloy authors** (in Rust):
55 55 - `alloy_tui`, the ratatui design-system crate (palette, themed widgets, footer chrome, mock-or-real backend detection). It is the design system.
56 - - The `alloy` console (ratatui): info, services, storage, wifi/net, image swaps, generation rollback, a package view, and `alloy settings` schema-driven settings and config editing. The mountaineer-sysop pattern carries over directly; see [CONSOLE.md](CONSOLE.md).
56 + - The `alloy` console (ratatui): info, services, storage, wifi/net, image swaps, generation rollback, a package view, and `alloy settings` schema-driven settings and config editing. See [CONSOLE.md](CONSOLE.md).
57 57
58 58 **Alloy curates** (from upstream):
59 59 - Fedora Silverblue as the base.
@@ -64,5 +64,3 @@
64 64 ## Status
65 65
66 66 Pre-v0. This document is the project. Code follows.
67 -
68 - Pivoted 2026-07-17 from an egui-authored, Niri scrolling-tiler design to this TUI-first, Sway shape. The scrolling-tiler model and the egui marquee-app wedge (lockscreen, package GUI, notification daemon, egui bar) were dropped; the immediate-mode thesis carried over unchanged because ratatui is immediate-mode too.
M docs/STACK.md +96 -102
M docs/TOKENS.md +2 -2