Skip to main content

max / alloy

docs: strip AI tells across docs/ Punctuation pass over all thirteen files, plus the vocabulary and decorative-glyph categories from the exorcise checklist. 187 em dashes, none left. Handled in three passes rather than one substitution, because the right replacement depends on the clause. Headings and bold-label bullets took colons. Paired parentheticals became parentheses or comma pairs. Prose singles took whichever of comma, colon, semicolon, or full stop the sentence wanted, which is why this could not be a regex. Code fences and inline code were excluded throughout, so no example, path, or command changed. Arrows went to words: bool to toggle rather than bool -> toggle, hex to linear sRGB rather than the chained-arrow form. One table cell used an em dash as a not-applicable placeholder and now says (none), matching the row above it. Vocabulary: bespoke to custom in six places, paradigm to model in five. Both read plainer and neither changes a claim. Deliberately kept, since they carry meaning rather than padding: literally in "never construct a Color literally", which is about literal values; actually in "the path Firefox actually reads" and "how curl | sh actually resolves", where the contrast with the assumed answer is the point of the sentence; and extremely in "extremely long unicode name", which is a sample-data label naming a test case. Curated is left alone throughout, being Alloy's own positioning word rather than an import. Nothing found in the remaining categories: no emoji, no curly quotes, no false contrasts, no hedge openers, no closing platitudes, no trailing exclamations. The docs were already clean on everything semantic; this was punctuation and word choice. 179 insertions against 179 deletions, no file gaining or losing a line. Every internal link still resolves. No code touched.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-19 18:17 UTC
Signed with PGP, not checked
Commit: 9ccfa54048040fd3b5b60342ac0925f88d08b348
Parent: 0d0725b
13 files changed, +179 insertions, -179 deletions
@@ -7,9 +7,9 @@
7 7 `alloy_tui` exposes:
8 8
9 9 - **Design tokens** as a runtime `Theme` value resolving theme-common intents into ratatui `Color` / `Style`. Single source of truth for everything in [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md).
10 - - **Themed primitives** — blocks, list rows, tags, form fields, status bars, log panes — configured against the tokens, each carrying its focus/selected/disabled variants.
11 - - **Layout helpers** — Alloy-flavored wrappers over ratatui's `Layout` constraint solver (section spacing measured in cells).
12 - - **A focus + keymap model** — the piece ratatui does not give you. Rendering is immediate-mode, but input is event-driven, so the crate owns the navigation model the app drives from its event loop: `FocusRing` across panes, `Cursor` down a list.
10 + - **Themed primitives** configured against the tokens: blocks, list rows, tags, form fields, status bars, log panes. Each carries its focus/selected/disabled variants.
11 + - **Layout helpers**: Alloy-flavored wrappers over ratatui's `Layout` constraint solver (section spacing measured in cells).
12 + - **A focus + keymap model**: the piece ratatui does not give you. Rendering is immediate-mode, but input is event-driven, so the crate owns the navigation model the app drives from its event loop: `FocusRing` across panes, `Cursor` down a list.
13 13
14 14 It does *not* expose:
15 15
M docs/CONSOLE.md +22 -22
@@ -6,11 +6,11 @@
6 6
7 7 ## Thesis
8 8
9 - Distros grow graphical settings apps because Unix's underlying CLIs are excellent primitives but a terrible surface for casual use — nobody wants to look up `wpctl set-sink-volume @DEFAULT_AUDIO_SINK@ 0.5+` at 3am. GNOME's answer is a large graphical settings app. Alloy's answer is a small ratatui console that fronts those same CLIs with a shared visual identity and keymap.
9 + Distros grow graphical settings apps because Unix's underlying CLIs are excellent primitives but a terrible surface for casual use. Nobody wants to look up `wpctl set-sink-volume @DEFAULT_AUDIO_SINK@ 0.5+` at 3am. GNOME's answer is a large graphical settings app. Alloy's answer is a small ratatui console that fronts those same CLIs with a shared visual identity and keymap.
10 10
11 11 The elegant part is that "controlling the system" and "editing a config file" are the same design problem with two different data sources. Once the console has schema-driven form widgets for editing live state (interface toggles, sink volumes, output modes), pointing those same widgets at a TOML file gives a config editor for free.
12 12
13 - This kills the "graphical settings app" question permanently — Alloy has one settings surface, it's the console, and it's ratatui.
13 + This kills the "graphical settings app" question permanently: Alloy has one settings surface, it's the console, and it's ratatui.
14 14
15 15 ## Subcommand shape
16 16
@@ -44,7 +44,7 @@
44 44 Each subcommand is a small ratatui adapter over an existing CLI. The subcommands share:
45 45
46 46 - The `alloy_tui` design-system crate (colors, typography, layout, form widgets).
47 - - A common navigation model (Tab across sections, Enter to activate, Esc to cancel — Alloy's keymap conventions live in one place).
47 + - A common navigation model (Tab across sections, Enter to activate, Esc to cancel; Alloy's keymap conventions live in one place).
48 48 - A common status area (busy/error/dirty indicators).
49 49 - A common command-log pane (every action shows the underlying CLI invocation, so users learn the primitive, not the wrapper).
50 50
@@ -54,32 +54,32 @@
54 54
55 55 Every command is executed as argv rather than through a shell, and displayed the same way it is run, quoting arguments that contain whitespace so the logged line can be pasted into a shell and mean the same thing there.
56 56
57 - ## `alloy config` — schema-driven editor
57 + ## `alloy config`: schema-driven editor
58 58
59 59 The config editor is the *same UI pattern* applied to file contents instead of live system state. Point it at a TOML or KDL file, get a navigable form. Save writes the file back.
60 60
61 61 Behavior:
62 62
63 - - **Roundtrip-safe.** The editor preserves comments, formatting, and key order. It edits values in place, not "read → serialize → write." Uses `toml_edit` for TOML and a KDL equivalent for KDL.
64 - - **Schema-required.** No schema, no form — the editor refuses to render a config it doesn't have a schema for, rather than falling back to value-inferred forms that guess wrong. Falls back to a plain text edit pane (with syntax highlighting) as an explicit escape.
65 - - **Live validation.** Type errors, out-of-range values, missing required keys, and unknown keys all surface in the form as inline diagnostics — not on save.
63 + - **Roundtrip-safe.** The editor preserves comments, formatting, and key order. It edits values in place, not "read, serialize, write." Uses `toml_edit` for TOML and a KDL equivalent for KDL.
64 + - **Schema-required.** No schema, no form: the editor refuses to render a config it doesn't have a schema for, rather than falling back to value-inferred forms that guess wrong. Falls back to a plain text edit pane (with syntax highlighting) as an explicit escape.
65 + - **Live validation.** Type errors, out-of-range values, missing required keys, and unknown keys all surface in the form as inline diagnostics rather than on save.
66 66 - **Documentation inline.** Schema entries carry a description string that shows in the form as help text next to the field.
67 67
68 68 ## Schema strategy
69 69
70 70 Three options considered. Committing to option 2 with 3 as opportunistic input.
71 71
72 - 1. **Value inference.** Read the file, infer form from value types (bool → toggle, string → input, table → subsection). Cheap. Loses everything about validation, enums, docs, defaults, sections. **Rejected** — produces "editable but not useful" forms, which is worse than no form.
73 - 2. **Explicit schema files, one per adopted config.** Alloy ships `schemas/rio.toml.schema`, `schemas/yazi.toml.schema`, `schemas/mako.schema`, etc. **Picked.** Alloy's whole thesis is "curated stack with authored defaults" — schemas are the natural extension of that. Bounded catalog: ~10 schemas for the adopted v0 stack. (The sway config's i3-style syntax is neither TOML nor KDL, so it takes the text-edit fallback, not a schema form.)
74 - 3. **Upstream schemas (JSON Schema, KDL schema) where they exist.** Some tools ship JSON Schema; most don't. **Adopted opportunistically** — where an upstream schema exists and is maintained, Alloy imports/converts it rather than authoring its own. Falls back to option 2 for the rest.
72 + 1. **Value inference.** Read the file, infer form from value types (bool to toggle, string to input, table to subsection). Cheap. Loses everything about validation, enums, docs, defaults, sections. **Rejected**, because it produces "editable but not useful" forms, which is worse than no form.
73 + 2. **Explicit schema files, one per adopted config.** Alloy ships `schemas/rio.toml.schema`, `schemas/yazi.toml.schema`, `schemas/mako.schema`, etc. **Picked.** Alloy's whole thesis is "curated stack with authored defaults", and schemas are the natural extension of that. Bounded catalog: ~10 schemas for the adopted v0 stack. (The sway config's i3-style syntax is neither TOML nor KDL, so it takes the text-edit fallback, not a schema form.)
74 + 3. **Upstream schemas (JSON Schema, KDL schema) where they exist.** Some tools ship JSON Schema; most don't. **Adopted opportunistically**: where an upstream schema exists and is maintained, Alloy imports/converts it rather than authoring its own. Falls back to option 2 for the rest.
75 75
76 76 ### The schema format
77 77
78 - Alloy authors a small TOML dialect for describing TOML (and a parallel one for KDL). Not JSON Schema — JSON Schema is powerful but verbose and awkward for humans. Not TOML's own type system — insufficient (no enums, no docs, no cross-field constraints).
78 + Alloy authors a small TOML dialect for describing TOML (and a parallel one for KDL). Not JSON Schema, which is powerful but verbose and awkward for humans. Not TOML's own type system, which is insufficient (no enums, no docs, no cross-field constraints).
79 79
80 80 Schema-format v1, worked in full at [`schemas/rio.toml.schema`](../schemas/rio.toml.schema). The design pass against rio's real config surface shaked out five affordances beyond a naive field list: sections, groups, presets, format hints, and an unknown-key policy.
81 81
82 - **Header.** Every schema declares its target file, the tool it targets, a target-version semver range, the schema-DSL version, and an unknown-key policy. Unknown keys default to `preserve` so that a rio release adding a new field never bricks the editor — the field survives the edit round-trip and surfaces as an info diagnostic.
82 + **Header.** Every schema declares its target file, the tool it targets, a target-version semver range, the schema-DSL version, and an unknown-key policy. Unknown keys default to `preserve` so that a rio release adding a new field never bricks the editor: the field survives the edit round-trip and surfaces as an info diagnostic.
83 83
84 84 ```toml
85 85 [schema]
@@ -92,13 +92,13 @@
92 92
93 93 **Sections** (`[[section]]`) declare UI-level grouping: one collapsible pane per section, ordered as declared. Fields fall into their section by path prefix. Without sections the form would be a flat list ordered by field-declaration; sections make headers and per-section descriptions explicit.
94 94
95 - **Fields** (`[[field]]`) carry the atomic edit unit. Every field has `path` (dotted TOML path — hyphens are literal key names), `type`, `description`, and `default`. Types: `bool`, `int`, `float`, `string`, `color`, `path`, `enum`, `list`, `table`. Constraints: `range` (int/float), `pattern` (string regex), `values` (enum), `required` (default false). Format hints: `color format = "hex" | "hex-alpha" | "css"`, `path format = "file" | "dir" | "any"` + optional `must_exist`. `enum` values may be flat (`["a", "b"]`) or structured (`[{value, label, description}, ...]`) when the raw value is unfriendly (rio's `"Disabled"` / `"Transparent"` gets a friendlier label).
95 + **Fields** (`[[field]]`) carry the atomic edit unit. Every field has `path` (dotted TOML path; hyphens are literal key names), `type`, `description`, and `default`. Types: `bool`, `int`, `float`, `string`, `color`, `path`, `enum`, `list`, `table`. Constraints: `range` (int/float), `pattern` (string regex), `values` (enum), `required` (default false). Format hints: `color format = "hex" | "hex-alpha" | "css"`, `path format = "file" | "dir" | "any"` + optional `must_exist`. `enum` values may be flat (`["a", "b"]`) or structured (`[{value, label, description}, ...]`) when the raw value is unfriendly (rio's `"Disabled"` / `"Transparent"` gets a friendlier label).
96 96
97 - **Groups** (`[[group]]`) collapse repetition. Rio's `[colors]` has ~25 palette slots, all colors, all hex — a `[[group]]` with `path = "colors"`, `type = "color"`, `format = "hex"`, and 25 `entries` expands to 25 fields at `colors.<key>` without 25 near-identical `[[field]]` blocks. Groups are DSL sugar over fields, not a runtime concept — the editor materializes them into the same form widgets.
97 + **Groups** (`[[group]]`) collapse repetition. Rio's `[colors]` has ~25 palette slots, all colors, all hex. A `[[group]]` with `path = "colors"`, `type = "color"`, `format = "hex"`, and 25 `entries` expands to 25 fields at `colors.<key>` without 25 near-identical `[[field]]` blocks. Groups are DSL sugar over fields, not a runtime concept: the editor materializes them into the same form widgets.
98 98
99 - **Presets** (`[[preset]]`) give palette-heavy configs the UX they need: one action swaps a bundle. Each preset declares `name`, `description`, and a `values` map of `path → value`. Applied as one atomic edit — single dirty state, one undo entry. Presets are the answer to "how does a user pick a theme without editing 25 hex codes." Optional per schema.
99 + **Presets** (`[[preset]]`) give palette-heavy configs the UX they need: one action swaps a bundle. Each preset declares `name`, `description`, and a `values` map of `path` to value. Applied as one atomic edit: single dirty state, one undo entry. Presets are the answer to "how does a user pick a theme without editing 25 hex codes." Optional per schema.
100 100
101 - **List-of-tables** (`type = "list"`, `element = {type = "table", fields = [...]}`) covers repeating records like `[[bindings.keys]]`. The element sub-schema is inlined for authoring simplicity — no cross-file references to chase.
101 + **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.
102 102
103 103 **Cross-field constraints deliberately excluded.** JSON Schema showed how much complexity `if`/`then`/`allOf`/`oneOf` buys. The editor renders every field; the target tool ignores the irrelevant ones. If a specific "hide field B when field A is off" case shows up in daily use, `enabled_when` can land in schema-DSL v2.
104 104
@@ -109,7 +109,7 @@
109 109
110 110 The KDL parallel dialect follows the same shape; only the `path` grammar and the roundtrip-safe editing library differ (see the KDL open question below).
111 111
112 - ## `alloy_tui` — the ratatui design system
112 + ## `alloy_tui`: the ratatui design system
113 113
114 114 `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 theme-common `.toml` theme files at runtime (see [TOKENS.md](TOKENS.md)); palette and semantic colors render as terminal chrome.
115 115
@@ -126,14 +126,14 @@
126 126
127 127 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.
128 128
129 - The immediate-mode paradigm's stability property (see [STACK.md](STACK.md)) is now shipped in one place: everything Alloy authors renders through `alloy_tui`.
129 + 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`.
130 130
131 131 ## Relationship to the rest of the stack
132 132
133 133 - **Replaces the graphical settings app** Alloy would otherwise eventually be pressured into shipping.
134 134 - **Complements yazi.** Yazi is for files; the console is for state and config. Different data, similar TUI aesthetic.
135 135 - **Fronts, does not replace, the underlying CLIs.** `nmcli`, `wpctl`, `rpm-ostree`, `swaymsg` all remain the ground truth. The console is a courteous surface.
136 - - **Design-system parity.** `alloy_tui` is the single design system; the console, bottom, and tuigreet already read as one terminal-native family — same palette, same information hierarchy.
136 + - **Design-system parity.** `alloy_tui` is the single design system; the console, bottom, and tuigreet already read as one terminal-native family: same palette, same information hierarchy.
137 137
138 138 ## Roadmap
139 139
@@ -151,13 +151,13 @@
151 151 ## Non-goals
152 152
153 153 - **Not a shell replacement.** Users still live in rio + zellij/tmux + helix. The console is invoked for specific tasks, then closed.
154 - - **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.
154 + - **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.
155 155 - **Not a launcher.** Launching is the shell (terminal-driven); there is no graphical launcher. The console is invoked by name, not discovered via search.
156 156 - **Not a package manager.** `alloy update` fronts `rpm-ostree` for atomic upgrades and rollbacks; installing individual packages is a rare enough action on Silverblue that keeping it out of the console is honest.
157 157
158 158 ## Open questions
159 159
160 160 - [x] **Schema format finalized: schema-format v1.** Worked example at [`schemas/rio.toml.schema`](../schemas/rio.toml.schema); DSL reference in the section above. The rio design pass surfaced five affordances the original sketch missed (sections, groups, presets, format hints, unknown-key policy).
161 - - [ ] KDL editing story — `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.
161 + - [ ] 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.
162 162 - [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.
163 - - [x] **Naming: one entry point — `alloy config <path>`.** No `alloy edit` alias. Fewer names to remember; the "config = editor" mental model holds for KDL files too — they're structured, machine-editable formats, which is what `alloy config` addresses.
163 + - [x] **Naming: one entry point, `alloy config <path>`.** No `alloy edit` alias. Fewer names to remember; the "config = editor" mental model holds for KDL files too, since they're structured, machine-editable formats, which is what `alloy config` addresses.
M docs/CONTINUITY.md +11 -11
@@ -6,7 +6,7 @@
6 6
7 7 ## Thesis
8 8
9 - The machine is disposable. The state is not. What survives a reinstall or a hardware upgrade should not depend on the user remembering to copy files, re-establish keys, or reconfigure a mesh. Alloy ships the two adopted tools that make that true — network identity and file synchronization — as base-image citizens, gated behind an explicit enrollment gesture and controlled through the Alloy Console.
9 + The machine is disposable. The state is not. What survives a reinstall or a hardware upgrade should not depend on the user remembering to copy files, re-establish keys, or reconfigure a mesh. Alloy ships the two adopted tools that make that true (network identity and file synchronization) as base-image citizens, gated behind an explicit enrollment gesture and controlled through the Alloy Console.
10 10
11 11 Everything in this document is a curated adoption of existing tools. Alloy authors nothing in this space. The Alloy contribution is: the tools are present at first boot, they enroll in one screen, and they are controlled through the same design-system surface as everything else.
12 12
@@ -29,7 +29,7 @@
29 29 - Runs as a systemd user service (`syncthing.service`). Not root, not system-wide.
30 30 - The daemon starts after `alloy sync` enrollment or an explicit `systemctl --user enable --now syncthing`.
31 31 - Not enabled on-by-default with no folders configured; a running daemon with nothing to do is unjustified surface.
32 - - Web UI on `127.0.0.1:8384` remains reachable — Alloy does not disable it — but is not the recommended interface. `alloy sync` is.
32 + - Web UI on `127.0.0.1:8384` remains reachable (Alloy does not disable it) but is not the recommended interface. `alloy sync` is.
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
@@ -49,22 +49,22 @@
49 49 +---------------------------------------------------------+
50 50 ```
51 51
52 - Skip is fine and reversible — later, the same enrollments live under `alloy mesh` and `alloy sync`. This is not a wizard, and Alloy does not grow one. Two rows, two actions, done.
52 + Skip is fine and reversible: later, the same enrollments live under `alloy mesh` and `alloy sync`. This is not a wizard, and Alloy does not grow one. Two rows, two actions, done.
53 53
54 - **Deferred to v1.** The first-boot flow itself is v1 scope. v0.5 users start the daemons manually via the console subcommands. Shipping the subcommands first, and the wizard after, is the correct ordering — the wizard is a thin shim over the enrollment actions the subcommands already expose.
54 + **Deferred to v1.** The first-boot flow itself is v1 scope. v0.5 users start the daemons manually via the console subcommands. Shipping the subcommands first, and the wizard after, is the correct ordering: the wizard is a thin shim over the enrollment actions the subcommands already expose.
55 55
56 56 ## Console subcommands
57 57
58 58 `alloy mesh` and `alloy sync` live under Alloy Console. Full spec in [CONSOLE.md](CONSOLE.md); scope summary here so this document stands alone:
59 59
60 60 - **`alloy mesh`** (was `alloy tail`; the old verb remains an alias). A ratatui front over Tailscale, named for what it is rather than who makes it, since Headscale users drive the same client. Replaces `tailscale status` as the daily-use surface. **Shipped:** peer list with online status and last-seen, this machine first, exit-node selection and clearing, and the control plane named in the title when it is self-hosted. **Still to come:** MagicDNS lookup, share/unshare, and the enrollment flow.
61 - - **`alloy sync`** — ratatui front over Syncthing's REST API. Folder list with per-folder sync state, device list with connection state, add/remove folder, add/remove device, pause/resume. Does not try to replicate the Syncthing web UI's full feature surface — just the operations users actually perform. The web UI remains available for edge cases.
61 + - **`alloy sync`**: ratatui front over Syncthing's REST API. Folder list with per-folder sync state, device list with connection state, add/remove folder, add/remove device, pause/resume. Does not try to replicate the Syncthing web UI's full feature surface, only the operations users perform. The web UI remains available for edge cases.
62 62
63 63 ## Non-goals
64 64
65 65 - **Secrets sync.** Bitwarden, pass, KeePassXC, etc. Too personal, and the security model varies too much to have a defensible default. Users bring their own.
66 66 - **Dotfiles sync as an opinionated setup.** Syncthing *can* carry `~/.config`, but Alloy does not ship a preconfigured dotfile-sync arrangement. Provide the daemon; let the user decide what to sync.
67 - - **Backup (partial):** See [Backup](#backup) below — Alloy ships restic in the image but declines to prescribe a policy. Sync is mesh + latest-wins; backup is encrypted-at-rest + versioned. Different trust models; the two must not conflate.
67 + - **Backup (partial):** See [Backup](#backup) below. Alloy ships restic in the image but declines to prescribe a policy. Sync is mesh + latest-wins; backup is encrypted-at-rest + versioned. Different trust models; the two must not conflate.
68 68 - **Cross-user or multi-account state.** Alloy is a personal-machine story. Multi-user file sharing is a Syncthing feature the user can configure themselves; Alloy does not prescribe.
69 69
70 70 ## Honest costs
@@ -77,17 +77,17 @@
77 77
78 78 **restic.** Go, CLI-first, encrypted, deduplicating, snapshot-based. Broad backend support (local, SFTP, S3, B2, rclone-anything). Stable repo format. Widest tutorial coverage of any modern backup tool. In Fedora repos.
79 79
80 - **Shipped in the base image. No default policy.** Alloy provides the tool; the user provides the folders, schedule, retention, and destination. Sync (Tailscale, Syncthing) has defensible defaults because enroll-and-it-works is the whole model; backup does not — "where do you back up to" is answerable only per-user.
80 + **Shipped in the base image. No default policy.** Alloy provides the tool; the user provides the folders, schedule, retention, and destination. Sync (Tailscale, Syncthing) has defensible defaults because enroll-and-it-works is the whole model; backup does not: "where do you back up to" is answerable only per-user.
81 81
82 - The forcing observation: when a user's laptop dies, they have their sync mesh (Syncthing pulled files back) and their network identity (Tailscale re-enrolls). What they don't have without configuring restic is *point-in-time recovery* — the "I deleted this file three weeks ago and just noticed" case that only versioned backup solves.
82 + The forcing observation: when a user's laptop dies, they have their sync mesh (Syncthing pulled files back) and their network identity (Tailscale re-enrolls). What they don't have without configuring restic is *point-in-time recovery*, the "I deleted this file three weeks ago and just noticed" case that only versioned backup solves.
83 83
84 - Rejected: borg (Python; historical repo-format transitions), kopia (GUI-first; adds complexity Alloy doesn't need), duplicity (older, less-featured), rclone (sync, not backup — no snapshot history or dedup).
84 + Rejected: borg (Python; historical repo-format transitions), kopia (GUI-first; adds complexity Alloy doesn't need), duplicity (older, less-featured), rclone (sync, not backup; no snapshot history or dedup).
85 85
86 - **Deferred: `alloy backup` subcommand.** A ratatui front for `restic snapshots` / `restic backup` / `restic restore` fits the Alloy Console pattern (see [CONSOLE.md](CONSOLE.md)). v2+ candidate — worth building once someone is actually using restic daily and wants a nicer surface.
86 + **Deferred: `alloy backup` subcommand.** A ratatui front for `restic snapshots` / `restic backup` / `restic restore` fits the Alloy Console pattern (see [CONSOLE.md](CONSOLE.md)). v2+ candidate, worth building once someone is using restic daily and wants a nicer surface.
87 87
88 88 ## Open questions
89 89
90 - - [ ] MagicDNS interaction with Fedora's `systemd-resolved` — verify no config conflict on a fresh Silverblue install with Tailscale enrolled.
90 + - [ ] MagicDNS interaction with Fedora's `systemd-resolved`: verify no config conflict on a fresh Silverblue install with Tailscale enrolled.
91 91 - [x] **Syncthing's `discosrv` and `relaysrv`: defaults for v1.** Public infrastructure is fine for the enroll-and-it-works model. Expose configuration in `alloy sync` only if requests appear from users self-hosting the discovery / relay side.
92 92 - [ ] Headscale login-server field in `alloy mesh` enrollment: preserve across `tailscale down` / `tailscale up` cycles automatically, or require re-entry? Automatic is the correct default; verify Tailscale client behavior supports it cleanly.
93 93 - [x] **First-boot screen appears once.** If the user skips both Tailscale and Syncthing, `alloy mesh` and `alloy sync` are the enrollment paths after that; the first-boot flow does not re-appear.
@@ -2,7 +2,7 @@
2 2
3 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).
4 4
5 - Reference imagery: [inspo/](inspo/) — terminal rices that demonstrate most of these principles.
5 + Reference imagery: [inspo/](inspo/): terminal rices that demonstrate most of these principles.
6 6
7 7 ## Core principle: tinted greyscale chrome, color as information
8 8
@@ -20,11 +20,11 @@
20 20
21 21 A small, fixed set of accent hues, used only when their meaning is the point:
22 22
23 - - **Red** — error, over-threshold, destructive action confirmed
24 - - **Yellow / amber** — warning, near-threshold, attention without alarm
25 - - **Green** — healthy, success, in-bounds utilization
26 - - **Blue** — informational, link, in-progress
27 - - **Magenta / violet** — categorical accent for syntax or data series where the other four are taken
23 + - **Red**: error, over-threshold, destructive action confirmed
24 + - **Yellow / amber**: warning, near-threshold, attention without alarm
25 + - **Green**: healthy, success, in-bounds utilization
26 + - **Blue**: informational, link, in-progress
27 + - **Magenta / violet**: categorical accent for syntax or data series where the other four are taken
28 28
29 29 Saturation stays moderate. Accents sit as foreground on the tinted-greyscale surface; they do not become the surface. A dense data readout is fully colored where it encodes severity or series identity, with every cell of chrome remaining tinted grey.
30 30
@@ -47,13 +47,13 @@
47 47
48 48 Monochrome chrome, no shadows, and color-reserved-for-information remove the usual vocabularies for signaling interactivity. In the Apple HIG tradition of *discoverable depth*, the replacement vocabulary:
49 49
50 - **Surface elevation is the depth language.** Use at least three ramp positions that read as elevation: `surface` (base, the terminal background), `surface-raised` (bordered controls), `surface-overlay` (popovers, menus, dialogs). The step between levels is tonal. A control announces itself by sitting on a raised, bordered surface; a data panel sits flush and borderless on `surface`. Elevation is a static affordance for control chrome, not a focus cue — the focused pane does *not* swap its surface.
50 + **Surface elevation is the depth language.** Use at least three ramp positions that read as elevation: `surface` (base, the terminal background), `surface-raised` (bordered controls), `surface-overlay` (popovers, menus, dialogs). The step between levels is tonal. A control announces itself by sitting on a raised, bordered surface; a data panel sits flush and borderless on `surface`. Elevation is a static affordance for control chrome, not a focus cue: the focused pane does *not* swap its surface.
51 51
52 - **State variants are mandatory, not optional.** A terminal has no pointer, so the states reduce to what keyboard focus expresses: `default`, `focused`, `selected`, `disabled`. Focused switches the pane's border to `border-strong` — and nothing else changes: no surface swap, no title reweighting, no glyph. Selected does the same at row scope: a `border-strong` leading-edge stripe (a border-like cue) and no surface change. Disabled drops text to `text-muted`. No Alloy widget ships with only a default state. (Hover/pressed remain documented for any adopted GUI surface but do not apply to authored TUIs.)
52 + **State variants are mandatory, not optional.** A terminal has no pointer, so the states reduce to what keyboard focus expresses: `default`, `focused`, `selected`, `disabled`. Focused switches the pane's border to `border-strong`, and nothing else changes: no surface swap, no title reweighting, no glyph. Selected does the same at row scope: a `border-strong` leading-edge stripe (a border-like cue) and no surface change. Disabled drops text to `text-muted`. No Alloy widget ships with only a default state. (Hover/pressed remain documented for any adopted GUI surface but do not apply to authored TUIs.)
53 53
54 54 **Borders are the boundary cue.** A single box-drawing border at `border` announces a control's edge; switching that border to `border-strong` announces focus. This is functional chrome, not decoration.
55 55
56 - **Focus is obligatory, unambiguous, and border-only.** Keyboard focus is shown by the focused pane's border going to `border-strong`, with enough contrast to be unmistakable at arm's length — and that is the *entire* cue. No surface swap, no accent, no glyph, no title styling change. Focus uses the ramp, not the information palette; it is chrome, not data. Making focus subtle as a "clean" choice is a regression; stacking extra signals on top of the border is also a regression, in the other direction.
56 + **Focus is obligatory, unambiguous, and border-only.** Keyboard focus is shown by the focused pane's border going to `border-strong`, with enough contrast to be unmistakable at arm's length, and that is the *entire* cue. No surface swap, no accent, no glyph, no title styling change. Focus uses the ramp, not the information palette; it is chrome, not data. Making focus subtle as a "clean" choice is a regression; stacking extra signals on top of the border is also a regression, in the other direction.
57 57
58 58 **Square-and-flush is for data; bordered is for controls.** Data panels and tabular regions are borderless and flush on `surface`. Interactive controls (fields, list rows that respond to Enter, buttons) carry a box border. The presence or absence of a border is itself the affordance cue.
59 59
@@ -63,7 +63,7 @@
63 63
64 64 **Action hierarchy: polarity-driven.** A primary action inverts polarity (its cell run is `text-primary` background with `surface-raised` text), visually heavy without recruiting an accent. Secondary actions sit on `surface` with normal text. Disabled actions hold `text-muted`. A pane never shows more than one primary action.
65 65
66 - **List and pane selection: leading-edge stripe.** The current selection in a list is marked with a `border-strong` stripe (a single reverse or block cell) along the leading edge of the row. The row's surface does not change, and no glyph or reweighting is added — the stripe is the only cue, matching the pane-border rule at row scope. It rhymes with the focus border (also `border-strong`) but reads as an edge, so selection and focus stay distinguishable when they coincide. Selection is chrome; never an accent.
66 + **List and pane selection: leading-edge stripe.** The current selection in a list is marked with a `border-strong` stripe (a single reverse or block cell) along the leading edge of the row. The row's surface does not change, and no glyph or reweighting is added; the stripe is the only cue, matching the pane-border rule at row scope. It rhymes with the focus border (also `border-strong`) but reads as an edge, so selection and focus stay distinguishable when they coincide. Selection is chrome; never an accent.
67 67
68 68 **Inline messages: accent only at the edge and glyph.** A color-as-information message (error, warn, healthy, info) carries a single-cell accent bar at the leading edge and an accent glyph; the message body stays `text-primary`. The accent never fills the message's surface (that would turn chrome into data).
69 69
@@ -20,7 +20,7 @@
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 21 - **Folding the screen back does nothing special** now that the notes flow is shelved; the session stays as-is.
22 22
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 bespoke tablet shell is out of scope.
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
@@ -54,11 +54,11 @@
54 54
55 55 **Notes app pick: Rnote.** Rust, GTK4, purpose-built for stylus free-hand notes. Ships in Fedora repos. Infinite canvas, pressure and tilt, eraser, autosave, export to PDF/SVG. Fits Alloy's Rust-forward posture and the workflow exactly.
56 56
57 - Rejected: Xournal++ (mature but heavier and more PDF-annotation-shaped than free-writing-shaped), bespoke egui note app (real design-system win but a marquee-app-sized project for a feature Rnote already solves well; revisit only if Rnote hits a wall).
57 + Rejected: Xournal++ (mature but heavier and more PDF-annotation-shaped than free-writing-shaped), a custom egui note app (real design-system win but a marquee-app-sized project for a feature Rnote already solves well; revisit only if Rnote hits a wall).
58 58
59 59 **Daemon.** Small user-session Rust binary, single-purpose. Reads `SW_TABLET_MODE` transitions (via evdev or `iio-sensor-proxy`) and issues Niri IPC commands. Approximately 100 LOC. Ships as a systemd user service. Named `alloy-hinged` (working name).
60 60
61 - Not a general "device-adaptation layer" — deliberately single-purpose. If rotation, OSK, ambient light, or lid handling ever become real needs, they get their own daemons or get absorbed here with intent; not preemptively scaffolded.
61 + Not a general "device-adaptation layer"; deliberately single-purpose. If rotation, OSK, ambient light, or lid handling ever become real needs, they get their own daemons or get absorbed here with intent; not preemptively scaffolded.
62 62
63 63 **Physical keyboard and trackpad when folded.** The kernel usually suppresses these on `SW_TABLET_MODE` transitions, but behavior varies by device. Verify on the FW12 specifically. If it doesn't, one extra libinput toggle in `alloy-hinged` on fold/unfold covers it.
64 64
@@ -87,7 +87,7 @@
87 87
88 88 All expected to work with in-tree Fedora kernel drivers. No Alloy-specific handling planned. If a specific chipset in the FW12 turns out to need out-of-tree firmware, add a note here and to the image-composition manifest.
89 89
90 - Physical camera and microphone switches are on the chassis (Framework standard). Alloy does not need to plumb software mute — the hardware switch is authoritative.
90 + Physical camera and microphone switches are on the chassis (Framework standard). Alloy does not need to plumb software mute: the hardware switch is authoritative.
91 91
92 92 ## Expansion cards
93 93
@@ -99,6 +99,6 @@
99 99 ## Open questions
100 100
101 101 - [ ] fprintd unlock through swaylock's PAM stack on the FW12: enroll, then verify unlock at swaylock and at sudo.
102 - - [ ] Fractional-scale validation at 1.25x on this panel under sway — do terminal glyph metrics stay crisp?
102 + - [ ] Fractional-scale validation at 1.25x on this panel under sway: do terminal glyph metrics stay crisp?
103 103 - [ ] s2idle drain measurement on FW12 running the Alloy image, baseline vs. tuned. If drain is bad enough to matter, revisit whether Alloy ships any power tweaks or leaves it to Fedora defaults.
104 104 - [ ] (shelved) The `alloy-hinged` fold-to-notes questions (keyboard/trackpad suppression on `SW_TABLET_MODE`, Rnote fullscreen behavior) revive only if the tablet flow comes back.
@@ -2,7 +2,7 @@
2 2
3 3 Two icon tiers with non-overlapping roles. Functional icons identify; hero illustrations anchor. Since the 2026-07-17 pivot to TUIs, the functional tier is the primary and near-universal one; the hero tier survives only on the handful of surfaces that are still drawn as images rather than terminal cells.
4 4
5 - ## Functional tier — Nerd Font glyphs (primary)
5 + ## Functional tier: Nerd Font glyphs (primary)
6 6
7 7 For small, identifying icons in chrome: list-item markers, pane/status indicators, file-type glyphs, severity glyphs paired with accent text, footer keymap hints.
8 8
@@ -12,12 +12,12 @@
12 12
13 13 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. `alloy_tui` widgets reach for these glyphs by semantic name (a `severity_glyph(Urgency)`, a `pane_marker`) so the choices stay centralized.
14 14
15 - ## Hero tier — isometric line illustrations (reserved, mostly deferred)
15 + ## Hero tier: isometric line illustrations (reserved, mostly deferred)
16 16
17 17 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:
18 18
19 - - **swaylock background** — the adopted lockscreen can display a background image; a hero plate belongs here.
20 - - **First-boot splash** — renderable via rio's kitty-graphics protocol, or shown before the session starts.
19 + - **swaylock background**: the adopted lockscreen can display a background image; a hero plate belongs here.
20 + - **First-boot splash**: renderable via rio's kitty-graphics protocol, or shown before the session starts.
21 21 - **Brand assets** outside the running system (repo social card, README).
22 22
23 23 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.
@@ -36,7 +36,7 @@
36 36 - **Hatching is mandatory** (the detail that separates engineering-manual iso from generic flat art): cross-hatch for metal (45° grid), parallel hatch for concrete/stone, stippling for ground/fill.
37 37 - **Doubled edges on solid objects** for mass.
38 38 - **Annotation callouts** on splash/hero plates: small letter/number labels with leader lines.
39 - - **Cropping is intentional** — a plate can run off its container edge, reinforcing "one page from a manual."
39 + - **Cropping is intentional**: a plate can run off its container edge, reinforcing "one page from a manual."
40 40 - **No color.** Even a warning lamp or copper busbar stays on the ramp. Color is reserved for information per [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#core-principle-tinted-greyscale-chrome-color-as-information).
41 41
42 42 ### Surviving subject map
M docs/IMAGE.md +13 -13
@@ -4,7 +4,7 @@
4 4
5 5 ## Landscape
6 6
7 - Silverblue and its atomic siblings don't have a mutable root filesystem. The OS is an immutable commit — a tree of files that swaps atomically at boot. Users can't `dnf install` into a running system; they either layer packages locally (rebuilding their image) or pull a pre-built image.
7 + Silverblue and its atomic siblings don't have a mutable root filesystem. The OS is an immutable commit, a tree of files that swaps atomically at boot. Users can't `dnf install` into a running system; they either layer packages locally (rebuilding their image) or pull a pre-built image.
8 8
9 9 For a distro builder, "image composition" is the question of how you produce that immutable commit and how users receive it.
10 10
@@ -14,11 +14,11 @@
14 14
15 15 Classical Silverblue build. Treefile in YAML declares packages, kernel arguments, systemd presets. `rpm-ostree compose tree` produces an ostree commit, pushed to an ostree repository. Users pull via `rpm-ostree upgrade`.
16 16
17 - **Cost:** you host an ostree repository (real infrastructure — nginx serving `/var/ostree` or equivalent). Legacy-shaped; Fedora itself is migrating away.
17 + **Cost:** you host an ostree repository (real infrastructure: nginx serving `/var/ostree` or equivalent). Legacy-shaped; Fedora itself is migrating away.
18 18
19 19 ### bootc + Containerfile (modern, where Fedora is going)
20 20
21 - The OS *is* an OCI container image. Distributed via any OCI registry. Users pull with `bootc switch <registry-url>` and reboot. Build is `podman build` from a Containerfile — same as building any Docker image.
21 + The OS *is* an OCI container image. Distributed via any OCI registry. Users pull with `bootc switch <registry-url>` and reboot. Build is `podman build` from a Containerfile, the same as building any Docker image.
22 22
23 23 **Cost:** pre-1.0, occasional breaking changes as bootc matures. Alloy accepts drift maintenance the same way it does for nushell config and userChrome.css.
24 24
@@ -30,7 +30,7 @@
30 30
31 31 ### "Install Silverblue and run our script" (non-option)
32 32
33 - Zero build infrastructure. But users get a mutated Silverblue with an Alloy veneer — not shipped as "Alloy." Manifesto commitment breaks. Rejected.
33 + Zero build infrastructure. But users get a mutated Silverblue with an Alloy veneer, not shipped as "Alloy." Manifesto commitment breaks. Rejected.
34 34
35 35 ## Pick: bootc + Containerfile
36 36
@@ -46,15 +46,15 @@
46 46
47 47 sr.ht ships Fedora build VMs with `dnf`, `podman`, `buildah`, `skopeo` preinstalled. When ready, the build file at `.builds/alloy-image.yml` triggers on every push.
48 48
49 - **Current state:** the build file lives at `builds.disabled/alloy-image.yml` — parked outside the `.builds/` path sr.ht auto-runs, so no CI resources are consumed while the Containerfile is being iterated locally. Rename the directory back to `.builds/` and push to re-enable.
49 + **Current state:** the build file lives at `builds.disabled/alloy-image.yml`, parked outside the `.builds/` path sr.ht auto-runs, so no CI resources are consumed while the Containerfile is being iterated locally. Rename the directory back to `.builds/` and push to re-enable.
50 50
51 51 ### Registry: `quay.io` (deferred until distribution starts)
52 52
53 53 Red Hat's container registry is the intended destination once Alloy has an image worth distributing. Free for public images, ecosystem-coherent with the `quay.io/fedora/fedora-bootc:43` base.
54 54
55 - **Current state:** no registry provisioned. `.builds/alloy-image.yml` runs a **build + lint smoke test** only — validates the Containerfile on every push, without publishing anywhere. When the image is worth distributing, the CI file grows back `sign` and `push` tasks and `quay.io/alloy/alloy` gets provisioned with a robot account.
55 + **Current state:** no registry provisioned. `.builds/alloy-image.yml` runs a **build + lint smoke test** only, validating the Containerfile on every push, without publishing anywhere. When the image is worth distributing, the CI file grows back `sign` and `push` tasks and `quay.io/alloy/alloy` gets provisioned with a robot account.
56 56
57 - Rejected alternatives (for when distribution starts): ghcr.io (source-registry ecosystem mismatch), Docker Hub (pull rate limits, no advantage), self-hosted (real infra commitment, not worth v0 operational load — revisit v2+).
57 + Rejected alternatives (for when distribution starts): ghcr.io (source-registry ecosystem mismatch), Docker Hub (pull rate limits, no advantage), self-hosted (real infra commitment, not worth v0 operational load; revisit v2+).
58 58
59 59 ### Signing: cosign (deferred alongside distribution)
60 60
@@ -66,23 +66,23 @@
66 66
67 67 1. **Base:** `FROM quay.io/fedora/fedora-bootc:43`.
68 68 2. **Third-party repos:** Tailscale, any COPRs Alloy depends on for packages not in Fedora main.
69 - 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.
70 - 4. **Package removals:** stock Silverblue desktop pieces Alloy replaces (gnome-shell, gdm — the latter gated on the greeter pick).
69 + 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.
70 + 4. **Package removals:** stock Silverblue desktop pieces Alloy replaces (gnome-shell, gdm; the latter gated on the greeter pick).
71 71 5. **Config tree:** the tree at `etc/skel/.config/*` and `etc/skel/.mozilla/*` (new-user defaults, including the Firefox first-launch profile seed), `etc/*` (system-wide, including `etc/firefox/policies/policies.json`), and `usr/lib64/firefox/*` (Firefox autoconfig + `mozilla.cfg` default prefs) in the repo maps 1:1 into the image.
72 72 6. **Systemd presets:** which services are enabled by default (syncthing off by default, gammastep off until enrolled, alloy-hinged conditionally on FW12, etc.).
73 73 7. **Branding:** os-release, plymouth splash.
74 74 8. **Validation:** `bootc container lint` runs at build.
75 75
76 - The base browser (Firefox) ships as an RPM baked into the image — one code path, no first-boot delay, and enterprise policies (`/etc/firefox/policies/policies.json`) take effect immediately. The `flatpak` client is included so users can pull ungoogled-chromium and other Flathub-only apps on demand post-install; no Flatpaks are provisioned at build or first-boot time.
76 + The base browser (Firefox) ships as an RPM baked into the image: one code path, no first-boot delay, and enterprise policies (`/etc/firefox/policies/policies.json`) take effect immediately. The `flatpak` client is included so users can pull ungoogled-chromium and other Flathub-only apps on demand post-install; no Flatpaks are provisioned at build or first-boot time.
77 77
78 78 ## Update cadence
79 79
80 80 The sr.ht build triggers on:
81 81
82 - - **Push to `main`** — Alloy commits.
83 - - **Weekly cron** — picks up upstream Fedora base image updates. sr.ht doesn't have webhook receivers for external triggers, so cron is the pragmatic answer.
82 + - **Push to `main`**: Alloy commits.
83 + - **Weekly cron**: picks up upstream Fedora base image updates. sr.ht doesn't have webhook receivers for external triggers, so cron is the pragmatic answer.
84 84
85 - Users receive updates via `bootc upgrade` — no push notifications, no forced restart. Alloy's update UX is the standard bootc UX.
85 + Users receive updates via `bootc upgrade`: no push notifications, no forced restart. Alloy's update UX is the standard bootc UX.
86 86
87 87 ## Fedora version tracking
88 88
@@ -27,7 +27,7 @@
27 27 | **GNOME** | Silverblue | Bluefin |
28 28 | **KDE** | Kinoite | (none) |
29 29 | **Tiling (Sway)** | Sericea | **Alloy** |
30 - | **Gaming** | — | Bazzite |
30 + | **Gaming** | (none) | Bazzite |
31 31
32 32 Sericea and Kinoite are bare windowing on an immutable base with no opinionated user layer. Bluefin is GNOME-flavored developer experience. Bazzite is gaming. Alloy is opinionated tiling.
33 33
@@ -1,4 +1,4 @@
1 - # Marquee apps — retired
1 + # Marquee apps: retired
2 2
3 3 *This document is retired. The 2026-07-17 pivot dropped the egui marquee-app pipeline. Alloy authors one surface now: the `alloy` console on the `alloy_tui` ratatui design system. See [CONSOLE.md](CONSOLE.md) and [COMPONENT-LIBRARY.md](COMPONENT-LIBRARY.md).*
4 4
@@ -6,9 +6,9 @@
6 6
7 7 ## The question
8 8
9 - How do designers collaborate with engineers on immediate-mode UIs without the designers learning Rust? Figma exists because retained-mode reactive frameworks (React, SwiftUI, Flutter, Slint) accept pixel-positioned mockups as a meaningful artifact. Immediate-mode toolkits like egui don't, because immediate-mode UI is fundamentally not a tree of positioned visual elements — it is a *function* that takes state and produces frames.
9 + How do designers collaborate with engineers on immediate-mode UIs without the designers learning Rust? Figma exists because retained-mode reactive frameworks (React, SwiftUI, Flutter, Slint) accept pixel-positioned mockups as a meaningful artifact. Immediate-mode toolkits like egui don't, because immediate-mode UI is not a tree of positioned visual elements. It is a *function* that takes state and produces frames.
10 10
11 - If immediate-mode is the right architectural choice (Alloy's principle 3 says it is), then the design-tool gap is real. Nobody has filled it because immediate-mode UI hasn't been mainstream enough to support a serious tool, and the existing design-tool paradigm doesn't fit. That's both the opportunity and the warning.
11 + If immediate-mode is the right architectural choice (Alloy's principle 3 says it is), then the design-tool gap is real. Nobody has filled it because immediate-mode UI hasn't been mainstream enough to support a serious tool, and the existing design-tool model doesn't fit. That's both the opportunity and the warning.
12 12
13 13 ## Why Figma's model doesn't translate
14 14
@@ -22,7 +22,7 @@
22 22 The translation is lossy. Designs drawn this way routinely miss:
23 23
24 24 - State transitions (Figma draws snapshots; the actual UX is the motion between them).
25 - - Edge cases (long text, empty data, error variants — not drawn means not designed).
25 + - Edge cases (long text, empty data, error variants; not drawn means not designed).
26 26 - Real data shapes (mockup data is always neat).
27 27 - Viewport variance (pixel-perfect is meaningless when the window resizes).
28 28
@@ -30,30 +30,30 @@
30 30
31 31 ## What immediate-mode rewards instead
32 32
33 - Immediate-mode reframes the unit of design work. There is no screen — there is a *function rendered every frame from current state*. So the meaningful artifacts a designer produces are:
33 + Immediate-mode reframes the unit of design work. There is no screen, only a *function rendered every frame from current state*. So the meaningful artifacts a designer produces are:
34 34
35 - 1. **Tokens** — palette, type scale, spacing, radii, motion (or its absence). Flat data, already covered by the theme-common theme files described in [COMPONENT-LIBRARY.md](COMPONENT-LIBRARY.md).
36 - 2. **Themed primitives** — buttons, text fields, tags, focus rings. Visual specification of state variants.
37 - 3. **Composition vocabulary** — "this is a card with these slots," "this is a list row with this arrangement," "this section is dense; this one is breathy." A *grammar*, not a pixel layout.
35 + 1. **Tokens**: palette, type scale, spacing, radii, motion (or its absence). Flat data, already covered by the theme-common theme files described in [COMPONENT-LIBRARY.md](COMPONENT-LIBRARY.md).
36 + 2. **Themed primitives**: buttons, text fields, tags, focus rings. Visual specification of state variants.
37 + 3. **Composition vocabulary**: "this is a card with these slots," "this is a list row with this arrangement," "this section is dense; this one is breathy." A *grammar*, not a pixel layout.
38 38 4. **Data shapes the composition accepts.** A `PackageRow` accepts `(name, version, urgency, icon)`. The designer specifies what fields render in what arrangement; the engineer provides the data.
39 39
40 40 The screen disappears as a unit of work. A "page" becomes a composition of components against a data source. The designer's contribution is upstream of any specific screen.
41 41
42 42 ## Sketch: what the tool might be
43 43
44 - The hypothetical Alloy design tool (working name **`crucible`** — alloys are formed in crucibles; preserves the metallurgy frame) would:
44 + The hypothetical Alloy design tool (working name **`crucible`**; alloys are formed in crucibles, which preserves the metallurgy frame) would:
45 45
46 46 ### Component as unit, not screen as unit
47 47
48 - The primary view is a component you're editing, not a canvas of screens. You edit `PackageRow` once; the tool renders it in isolation against multiple sample data sets (`short name`, `extremely long unicode name`, `missing version`, `red urgency tag`, `disabled state`) — all visible at once. You do not draw four artboards; you specify variations of one component and the tool shows them live.
48 + The primary view is a component you're editing, not a canvas of screens. You edit `PackageRow` once; the tool renders it in isolation against multiple sample data sets (`short name`, `extremely long unicode name`, `missing version`, `red urgency tag`, `disabled state`), all visible at once. You do not draw four artboards; you specify variations of one component and the tool shows them live.
49 49
50 50 ### Token-first authoring
51 51
52 - Most design work is the token graph. The tool's primary editing surface is the palette, the typography scale, the spacing scale, the radii, the elevation steps. Changes propagate instantly to every component preview. This is already most of Figma's value — and it round-trips cleanly to a theme-common theme file (the `cast` tool idea generalized).
52 + Most design work is the token graph. The tool's primary editing surface is the palette, the typography scale, the spacing scale, the radii, the elevation steps. Changes propagate instantly to every component preview. This is already most of Figma's value, and it round-trips cleanly to a theme-common theme file (the `cast` tool idea generalized).
53 53
54 54 ### Composition grammar, not pixel layout
55 55
56 - When the designer composes a component, they're not positioning pixels — they're choosing layout primitives that map 1:1 to the authored call vocabulary (ratatui's `Layout` constraints plus `alloy_tui`'s `section` / `AlloyForm` / `AlloyList` helpers, or egui's `horizontal` / `vertical` / `card` / `field_row` if authored GUI ever returns). The tool's output is a `.alloy.ron` (or `.alloy.toml`) describing the composition structurally:
56 + When the designer composes a component, they're not positioning pixels, they're choosing layout primitives that map 1:1 to the authored call vocabulary (ratatui's `Layout` constraints plus `alloy_tui`'s `section` / `AlloyForm` / `AlloyList` helpers, or egui's `horizontal` / `vertical` / `card` / `field_row` if authored GUI ever returns). The tool's output is a `.alloy.ron` (or `.alloy.toml`) describing the composition structurally:
57 57
58 58 ```
59 59 PackageRow = card(elevation = raised) {
@@ -70,7 +70,7 @@
70 70 }
71 71 ```
72 72
73 - The engineer's `impl Widget for PackageRow` reads this composition file (or has it compiled in via `build.rs`) and renders it against the actual data. The interaction logic — what "update" does — is engineer territory. The visual structure is designer territory. The seam between them is the composition file.
73 + The engineer's `impl Widget for PackageRow` reads this composition file (or has it compiled in via `build.rs`) and renders it against the actual data. The interaction logic, what "update" does, is engineer territory. The visual structure is designer territory. The seam between them is the composition file.
74 74
75 75 ### Live render against sample data
76 76
@@ -87,18 +87,18 @@
87 87 1. **How rich does the composition vocabulary need to be?** Too thin and designers can't express enough; too rich and the tool reinvents Rust visually. The egui call vocabulary is a good first approximation but may not be sufficient.
88 88 2. **How are conditional renders specified?** "If this field is present, show the button; otherwise hide it" is a code-side concern in egui. Does the composition language need basic conditionals? If yes, where does it stop being design and start being programming?
89 89 3. **How are state variants specified?** Hover/pressed/focused/disabled are mandatory per the design language. Does the designer author state-specific variants of compositions, or only state-specific tokens (and the composition is state-neutral)?
90 - 4. **How is custom drawing handled?** A custom sparkline or gauge widget cannot be expressed in any composition vocabulary. The tool should explicitly cede those — they're engineer-authored ratatui code that consumes tokens but isn't designed in the tool.
90 + 4. **How is custom drawing handled?** A custom sparkline or gauge widget cannot be expressed in any composition vocabulary. The tool should explicitly cede those: they're engineer-authored ratatui code that consumes tokens but isn't designed in the tool.
91 91 5. **What's the minimum viable scope?** Probably: token editing + a fixed set of primitives + a fixed-shape `card` and `list` composition. Everything else escalates fast.
92 92 6. **Is the right output format RON, TOML, or a custom DSL?** RON is most expressive; TOML is most readable; a custom DSL is most controllable but most expensive to maintain.
93 93
94 94 ## Prior art and adjacent things
95 95
96 - - **Storybook** (web) — component-first isolation and state preview. Closest existing parallel, but its primitives are React components, not a domain-independent grammar.
97 - - **Penpot** — open-source Figma alternative. Hackable; a better foundation than Figma for this kind of experiment if the tool went the "plugin to an existing design app" route instead of "standalone tool."
98 - - **Tokens Studio** (Figma plugin) — proves token-sync alone is valuable.
99 - - **Lottie** — a grammar for one specific concern (animation). Demonstrates that a constrained grammar can be a real design-tool output.
100 - - **rerun.io** — Rust + egui-based data visualization tool. Not a design tool, but a demonstration that immediate-mode UI can support sophisticated tooling.
101 - - **Hex / Retool** — visual programming over data. Wrong shape for design (they conflate design with logic) but their data-first ergonomic is correct.
96 + - **Storybook** (web): component-first isolation and state preview. Closest existing parallel, but its primitives are React components, not a domain-independent grammar.
97 + - **Penpot**: open-source Figma alternative. Hackable; a better foundation than Figma for this kind of experiment if the tool went the "plugin to an existing design app" route instead of "standalone tool."
98 + - **Tokens Studio** (Figma plugin): proves token-sync alone is valuable.
99 + - **Lottie**: a grammar for one specific concern (animation). Demonstrates that a constrained grammar can be a real design-tool output.
100 + - **rerun.io**: Rust + egui-based data visualization tool. Not a design tool, but a demonstration that immediate-mode UI can support sophisticated tooling.
101 + - **Hex / Retool**: visual programming over data. Wrong shape for design (they conflate design with logic) but their data-first ergonomic is correct.
102 102
103 103 ## What this commits Alloy to right now
104 104
@@ -110,7 +110,7 @@
110 110 The further commitments Alloy *could* make to keep this even more open (small, low-cost, worth considering):
111 111
112 112 - Define composition files (`<Component>.alloy.ron`) as a possible authoring surface for components, even if the v1 design system crate only consumes them via `include_str!`. This makes the surface real before any tool exists.
113 - - Keep all themed primitives' visual configuration token-driven and avoid hardcoded design decisions in widget code — so a future tool editing tokens has full reach.
113 + - Keep all themed primitives' visual configuration token-driven and avoid hardcoded design decisions in widget code, so a future tool editing tokens has full reach.
114 114
115 115 ## Status
116 116
M docs/SHELL.md +8 -8
@@ -4,7 +4,7 @@
4 4
5 5 ## Thesis
6 6
7 - Alloy's audience writes their own scripts and reaches for the shell as a tool-glue surface, not just an interactive prompt. That reframes the shell pick away from "safe universal default" and toward "which shell makes daily tool-glue actually good." Nushell wins that on structural grounds — structured pipelines with typed values instead of stringly-typed byte streams — and every daily interaction compounds the advantage.
7 + Alloy's audience writes their own scripts and reaches for the shell as a tool-glue surface, not just an interactive prompt. That reframes the shell pick away from "safe universal default" and toward "which shell makes daily tool-glue actually good." Nushell wins that on structural grounds (structured pipelines with typed values instead of stringly-typed byte streams), and every daily interaction compounds the advantage.
8 8
9 9 The pick is coherent with the rest of Alloy: `alloy console` treats configs as typed data, `alloy_tui` treats widgets as functions over state, and now the shell treats commands as functions over typed pipelines. Same principle at three layers.
10 10
@@ -19,23 +19,23 @@
19 19
20 20 **Consequence: `curl -fsSL https://example.com/install.sh | sh` works identically to any other distro.** The login shell is not in the invocation path. This was the biggest concern about picking a non-POSIX shell, and it turns out to be a misread of how `curl | sh` actually resolves.
21 21
22 - **Consequence: every `#!/bin/bash` or `#!/usr/bin/env bash` script the user writes or downloads runs in bash.** Their own tool-glue scripts can be nu (`#!/usr/bin/env nu`) — that's where the structured-pipeline advantage lives — but nothing forces them to.
22 + **Consequence: every `#!/bin/bash` or `#!/usr/bin/env bash` script the user writes or downloads runs in bash.** Their own tool-glue scripts can be nu (`#!/usr/bin/env nu`), which is where the structured-pipeline advantage lives, but nothing forces them to.
23 23
24 24 ## What Alloy ships
25 25
26 26 `etc/skel/.config/nushell/` in the Alloy repo:
27 27
28 - - `config.nu` — main nu configuration. Table style, history (sqlite backend), fuzzy completions, Alloy color palette applied to nu's own output, integration hooks for starship / zoxide / direnv (conditional on presence).
29 - - `env.nu` — environment. `EDITOR=hx`, `VISUAL=hx`, PATH additions for `~/.local/bin` and `~/.cargo/bin`, cache directory prep for integration inits.
30 - - `aliases.nu` — a small curated set (`ll`, `la`, `..`, `...`, git shortcuts). No plugin framework.
28 + - `config.nu`: main nu configuration. Table style, history (sqlite backend), fuzzy completions, Alloy color palette applied to nu's own output, integration hooks for starship / zoxide / direnv (conditional on presence).
29 + - `env.nu`: environment. `EDITOR=hx`, `VISUAL=hx`, PATH additions for `~/.local/bin` and `~/.cargo/bin`, cache directory prep for integration inits.
30 + - `aliases.nu`: a small curated set (`ll`, `la`, `..`, `...`, git shortcuts). No plugin framework.
31 31
32 - The Alloy image installs these into `~/.config/nushell/` on first boot (or symlinks — packaging decision at v0).
32 + The Alloy image installs these into `~/.config/nushell/` on first boot (or symlinks; packaging decision at v0).
33 33
34 34 ## Interoperability
35 35
36 36 Where daily-use friction shows up, and how Alloy handles it:
37 37
38 - - **Tool init lines.** `eval "$(starship init bash)"` and equivalents. Starship, zoxide, direnv, atuin, and carapace all ship `--init nu`. For the long tail of tools that don't, Alloy ships small wrapper snippets in `etc/skel/.config/nushell/integrations/` as they're needed — added on demand, not preemptively.
38 + - **Tool init lines.** `eval "$(starship init bash)"` and equivalents. Starship, zoxide, direnv, atuin, and carapace all ship `--init nu`. For the long tail of tools that don't, Alloy ships small wrapper snippets in `etc/skel/.config/nushell/integrations/` as they're needed, added on demand rather than preemptively.
39 39 - **Interactive one-liners.** `FOO=bar cmd && cmd2` becomes `FOO=bar; cmd; cmd2` (or `try / catch` for the conditional flavor). Muscle-memory tax; not a real barrier.
40 40 - **`export`.** `$env.FOO = "bar"` in nu, `export FOO=bar` in bash. Both work in their respective contexts.
41 41 - **Piped stdin.** Third-party CLIs that expect line-oriented text stdin: nu's default output is a table, but piping to an external command auto-flattens to text. Explicit control via `| to text` when needed.
@@ -52,7 +52,7 @@
52 52
53 53 **starship** (Rust, cross-shell). Boring, works, ships `starship init nu` first-class. Alloy configures starship's own palette to match the tokens.
54 54
55 - Bespoke nu prompt as a small design-system exercise is a legitimate future project — nushell's prompt hook is rich enough to author an Alloy prompt directly — but not v0 scope. Deferred.
55 + A custom nu prompt as a small design-system exercise is a legitimate future project (nushell's prompt hook is rich enough to author an Alloy prompt directly), but not v0 scope. Deferred.
56 56
57 57 ## Rejected
58 58
M docs/STACK.md +72 -72
@@ -4,15 +4,15 @@
4 4
5 5 ## Base
6 6
7 - **Fedora Silverblue / rpm-ostree.** Atomic image-based base. The pivot point of the whole project — riding an existing immutable base instead of building a parallel ecosystem (the sap/mountaineer trap) is what makes Alloy a months-shaped project instead of a years-shaped one.
7 + **Fedora Silverblue / rpm-ostree.** Atomic image-based base. The pivot point of the whole project: riding an existing immutable base instead of building a parallel ecosystem (the sap/mountaineer trap) is what makes Alloy a months-shaped project instead of a years-shaped one.
8 8
9 - **ublue relationship: alongside, not downstream.** Preserves naming and architectural independence. The "Alloy" name was picked specifically to avoid the ublue "Blue\*" prefix. Tradeoff: no shared image-building infrastructure — whatever Alloy ships for image composition is on Alloy to build/maintain. Upside: total architectural freedom and a clean brand.
9 + **ublue relationship: alongside, not downstream.** Preserves naming and architectural independence. The "Alloy" name was picked specifically to avoid the ublue "Blue\*" prefix. Tradeoff: no shared image-building infrastructure, so whatever Alloy ships for image composition is on Alloy to build/maintain. Upside: total architectural freedom and a clean brand.
10 10
11 - **Composition strategy:** bootc + Containerfile, `FROM quay.io/fedora/fedora-bootc:42`. Source on `git.sr.ht/~maxmj/alloy`, CI on `builds.sr.ht`, published container image on `quay.io/alloy/alloy`. This is the ublue *convention* (Containerfile, OCI registry, CI-driven build) without ublue's base image — which is what "alongside ublue" resolves to in practice. Full delivery strategy and open questions in [IMAGE.md](IMAGE.md). Containerfile at the repo root; sr.ht CI at `.builds/alloy-image.yml`.
11 + **Composition strategy:** bootc + Containerfile, `FROM quay.io/fedora/fedora-bootc:42`. Source on `git.sr.ht/~maxmj/alloy`, CI on `builds.sr.ht`, published container image on `quay.io/alloy/alloy`. This is the ublue *convention* (Containerfile, OCI registry, CI-driven build) without ublue's base image, which is what "alongside ublue" resolves to in practice. Full delivery strategy and open questions in [IMAGE.md](IMAGE.md). Containerfile at the repo root; sr.ht CI at `.builds/alloy-image.yml`.
12 12
13 13 ## Compositor
14 14
15 - **Sway.** Mature, i3-style manual tiling (workspaces plus split/tabbed/stacked containers), Wayland, well-packaged on Fedora. The i3 model is the tiling paradigm Alloy wants: predictable, workspace-based, no infinite scroll. C rather than Rust, accepted as a knowing trade: no mature Rust i3-style tiler exists, and the paradigm outweighs toolkit purity here. The whole sway* ecosystem (swaylock, swayidle, swayosd, swaybar) fits behind it with zero glue.
15 + **Sway.** Mature, i3-style manual tiling (workspaces plus split/tabbed/stacked containers), Wayland, well-packaged on Fedora. The i3 model is the tiling model Alloy wants: predictable, workspace-based, no infinite scroll. C rather than Rust, accepted as a knowing trade: no mature Rust i3-style tiler exists, and the model outweighs toolkit purity here. The whole sway* ecosystem (swaylock, swayidle, swayosd, swaybar) fits behind it with zero glue.
16 16
17 17 Rejected: Niri (Alloy's original pick; its scrolling-column model was the specific thing rejected in the 2026-07-17 pivot, see [MANIFESTO.md](MANIFESTO.md#status)), Hyprland (governance contested, animation-forward against Alloy's understatement), river (tag-based/dwm-shaped, Zig, further from i3 than sway), dwl (tiny but too bare for a curated default).
18 18
@@ -32,7 +32,7 @@
32 32
33 33 ## Lock
34 34
35 - **swaylock** (C, canonical, minimal, security-audited). Bound Mod+Ctrl+L. A Wayland session-lock surface is inherently graphical and cannot be a TUI, so this is one of the pieces Alloy adopts rather than authors. The prior plan for a bespoke egui lockscreen (the first marquee app) was dropped in the pivot; lock crash-recovery is now swaylock/PAM/greetd's concern. fprintd unlock, if wanted, wires through swaylock's PAM stack.
35 + **swaylock** (C, canonical, minimal, security-audited). Bound Mod+Ctrl+L. A Wayland session-lock surface is inherently graphical and cannot be a TUI, so this is one of the pieces Alloy adopts rather than authors. The prior plan for a custom egui lockscreen (the first marquee app) was dropped in the pivot; lock crash-recovery is now swaylock/PAM/greetd's concern. fprintd unlock, if wanted, wires through swaylock's PAM stack.
36 36
37 37 ## Launcher
38 38
@@ -42,7 +42,7 @@
42 42
43 43 ## Notification daemon
44 44
45 - **mako** (C, by emersion, de facto Wayland notification daemon, lightweight). Notifications are a layer-shell surface, another graphical piece Alloy adopts rather than authors. The prior plan for a bespoke egui notification daemon (the third marquee app) was dropped in the pivot.
45 + **mako** (C, by emersion, de facto Wayland notification daemon, lightweight). Notifications are a layer-shell surface, another graphical piece Alloy adopts rather than authors. The prior plan for a custom egui notification daemon (the third marquee app) was dropped in the pivot.
46 46
47 47 Rejected: swaync (C++), fnott (C), cosmic-notifications (drags libcosmic in; mako is lighter and already fits).
48 48
@@ -56,17 +56,17 @@
56 56
57 57 ## File manager
58 58
59 - **yazi** (Rust, async, plugin system, sixel/kitty/iTerm image preview). TUI-only, no GUI fallback shipped — the pivot's TUI-first line applies here the same way it applied to Rnote. Firefox and other GUI apps that need file dialogs go through xdg-desktop-portal, not a bundled file manager, so the daily case is covered.
59 + **yazi** (Rust, async, plugin system, sixel/kitty/iTerm image preview). TUI-only, no GUI fallback shipped; the pivot's TUI-first line applies here the same way it applied to Rnote. Firefox and other GUI apps that need file dialogs go through xdg-desktop-portal, not a bundled file manager, so the daily case is covered.
60 60
61 61 No custom egui file manager planned. Scope is too large (file ops, permissions, drag-drop, thumbnails, archives, mounts, trash, search, batch ops, associations) for a marquee-app slot, and the pivot moved off graphical authored surfaces anyway.
62 62
63 - Rejected: broot (Rust TUI, useful as a complement but different paradigm), nautilus / dolphin (not Rust), cosmic-files (was the pre-pivot GUI fallback; dropped along with the rest of the graphical stack). Users who want a graphical file manager install one themselves — `flatpak install flathub com.system76.CosmicFiles` (or thunar, nautilus) — same posture as ungoogled-chromium.
63 + Rejected: broot (Rust TUI, useful as a complement but a different model), nautilus / dolphin (not Rust), cosmic-files (was the pre-pivot GUI fallback; dropped along with the rest of the graphical stack). Users who want a graphical file manager install one themselves with `flatpak install flathub com.system76.CosmicFiles` (or thunar, nautilus), the same posture as ungoogled-chromium.
64 64
65 65 ## Text editor
66 66
67 67 **Helix.** Rust, modal (selection-first, Kakoune-shaped), LSP + tree-sitter + formatters batteries-included, TOML config, immediate-mode-stable, useful with zero config. On a terminal-first system the editor is a TUI; there is no graphical-editor default. Config and the Akari Dawn theme are covered under "system introspection" below.
68 68
69 - The prior two-audience answer (Helix for terminal users, Gram for graphical) collapsed in the pivot: Gram (a Zed/gpui fork) was dropped along with the rest of the GUI stack. **deox** — Alloy's own Helix fork — becomes the intended default once it has a public home to build from; until then, stock Helix ships.
69 + The prior two-audience answer (Helix for terminal users, Gram for graphical) collapsed in the pivot: Gram (a Zed/gpui fork) was dropped along with the rest of the GUI stack. **deox**, Alloy's own Helix fork, becomes the intended default once it has a public home to build from; until then, stock Helix ships.
70 70
71 71 Rejected: neovim (modernity comes from importing a config framework, the pattern Alloy rejects), vim/kakoune (older or ancestral), Gram/Zed/cosmic-edit (graphical editors, off-thesis now). Users who want a different editor install it themselves.
72 72
@@ -74,10 +74,10 @@
74 74
75 75 **Curated collection of public-domain impressionist paintings.** Reliably muted, atmospheric, never garish. Aligns with the design language on three axes: tonally restrained (no chrome-fighting saturation), atmospheric depth (mirrors the surface-elevation affordance vocabulary), and the impressionist obsession with *light as information* rhymes with the "color reserved for information" rule.
76 76
77 - All major impressionist works are clean PD (Monet, the latest, died 1926). Sourcing priority — institutions that explicitly waive reproduction-photo rights:
77 + All major impressionist works are clean PD (Monet, the latest, died 1926). Sourcing priority goes to institutions that explicitly waive reproduction-photo rights:
78 78
79 79 - **Met Open Access** (CC0)
80 - - **Art Institute of Chicago** (CC0 — strong impressionist holdings)
80 + - **Art Institute of Chicago** (CC0; strong impressionist holdings)
81 81 - **Rijksmuseum Rijksstudio** (free high-res)
82 82 - **National Gallery of Art (Washington)** (open access)
83 83
@@ -85,7 +85,7 @@
85 85
86 86 **Daemon: swww** (Rust, daemon + CLI, IPC-driven, soft-fade transitions, per-output). CLI-driven cycling is enough for a terminal-first system; a picker, if it ever lands, is an `alloy` console verb (metadata/curation in the console, surface management in the daemon over IPC), not an authored GUI. Soft fade on cycle is the tonally correct transition for impressionist art.
87 87
88 - Rejected: wpaperd (Rust, more structured cycling-as-daemon-concern — second pick if picker descopes), cosmic-bg (Rust, System76, sway compatibility unverified, not worth the detour while swww exists), swaybg (C, no cycling/transitions — boring fallback only).
88 + Rejected: wpaperd (Rust, more structured cycling-as-daemon-concern; second pick if picker descopes), cosmic-bg (Rust, System76, sway compatibility unverified, not worth the detour while swww exists), swaybg (C, no cycling/transitions; boring fallback only).
89 89
90 90 Honest gap: none of these handle ICC color management; that's a Wayland compositor-level concern still being finalized.
91 91
@@ -93,7 +93,7 @@
93 93
94 94 **rio.** Rust, WGPU-backed, TOML config, kitty and sixel and iTerm2 graphics protocols. Ships in Fedora repos.
95 95
96 - The pick is driven by the yazi commitment above. Yazi is the TUI primary — "where keyboard-driven Alloy users will live by default." A terminal without graphics protocols neuters the primary tool, so the question isn't "are previews a nice-to-have" but "does the primary tool run at full capability." That reframes the axis and makes rio the honest answer.
96 + The pick is driven by the yazi commitment above. Yazi is the TUI primary, "where keyboard-driven Alloy users will live by default." A terminal without graphics protocols neuters the primary tool, so the question isn't "are previews a nice-to-have" but "does the primary tool run at full capability." That reframes the axis and makes rio the honest answer.
97 97
98 98 Secondary alignments:
99 99 - **TOML config.** No scripting language dependency, consistent with Alloy's rejection of Lua/yuck/similar config surfaces elsewhere in the stack.
@@ -111,20 +111,20 @@
111 111 Risk accepted: rio is ~2 years mature vs. alacritty's ~7. If rio proves flaky in real use, the fallback is alacritty, and the graphics gap becomes a knowing loss. Naming this trade-off here rather than pretending rio is drama-free.
112 112
113 113 Rejected:
114 - - **alacritty.** Upstream has firmly refused sixel/kitty graphics for years — definitional stance, not a pending PR. Kills yazi previews permanently.
114 + - **alacritty.** Upstream has firmly refused sixel/kitty graphics for years: a definitional stance, not a pending PR. Kills yazi previews permanently.
115 115 - **wezterm.** Lua config violates the "no scripting-language configs" line held elsewhere in the stack. Its central pitch (built-in multiplexing, tabs, SSH client) duplicates sway's tiling and is spent budget in this stack.
116 - - **cosmic-term.** Uses `alacritty_terminal` as the backend — same graphics gap as alacritty — and additionally couples Alloy's default terminal to libcosmic's visual identity, which the design-system stance rejects.
116 + - **cosmic-term.** Uses `alacritty_terminal` as the backend (the same graphics gap as alacritty) and additionally couples Alloy's default terminal to libcosmic's visual identity, which the design-system stance rejects.
117 117
118 - No custom egui terminal — terminals are among the most complex userland software (ANSI, terminfo, sixel/kitty graphics, IME, OSC, performance under load). Wrong scope.
118 + No custom egui terminal: terminals are among the most complex userland software (ANSI, terminfo, sixel/kitty graphics, IME, OSC, performance under load). Wrong scope.
119 119
120 120 ## Browser
121 121
122 - **Firefox (upstream) baked in as the default; ungoogled-chromium available as an opt-in Flatpak.** Alloy's identity work happens at the engine level, not the fork level, so upstream Firefox is the right base — no ESR lag, no fork-specific patch drift, the most-tested Gecko build shipping. Alloy owns the visual and behavioral layer via four files, each landing at the path Firefox actually reads:
122 + **Firefox (upstream) baked in as the default; ungoogled-chromium available as an opt-in Flatpak.** Alloy's identity work happens at the engine level, not the fork level, so upstream Firefox is the right base: no ESR lag, no fork-specific patch drift, the most-tested Gecko build shipping. Alloy owns the visual and behavioral layer via four files, each landing at the path Firefox actually reads:
123 123
124 - - [`etc/firefox/policies/policies.json`](../etc/firefox/policies/policies.json) — enterprise policy. Pins **uBlock Origin** as a force-installed, update-locked extension. Disables telemetry, Pocket, studies, sponsored tiles, new-tab feed, formfill, and password saving at the policy layer (higher-precedence than user prefs).
125 - - [`usr/lib64/firefox/mozilla.cfg`](../usr/lib64/firefox/mozilla.cfg) — system-wide default prefs, loaded via Firefox autoconfig. Configures compact UI density, blank new tab, DuckDuckGo suggestions off, quiet scroll, and enables `toolkit.legacyUserProfileCustomizations.stylesheets` so `userChrome.css` gets read. Every entry uses `defaultPref()` so users can still override in `about:config`.
126 - - [`usr/lib64/firefox/defaults/pref/autoconfig.js`](../usr/lib64/firefox/defaults/pref/autoconfig.js) — one-line pointer telling Firefox to load `mozilla.cfg` at startup.
127 - - [`etc/skel/.mozilla/firefox/profiles.ini`](../etc/skel/.mozilla/firefox/profiles.ini) + [`etc/skel/.mozilla/firefox/alloy.default/chrome/userChrome.css`](../etc/skel/.mozilla/firefox/alloy.default/chrome/userChrome.css) — first-launch profile seed. `profiles.ini` names a fixed-path profile (`alloy.default/`) so the pre-seeded `chrome/userChrome.css` (Helium-style ultra-compact horizontal, Alloy light-mode tokens from [TOKENS.md](TOKENS.md)) lands under it. Firefox opens the seeded profile on first launch instead of generating a random-suffix one.
124 + - [`etc/firefox/policies/policies.json`](../etc/firefox/policies/policies.json): enterprise policy. Pins **uBlock Origin** as a force-installed, update-locked extension. Disables telemetry, Pocket, studies, sponsored tiles, new-tab feed, formfill, and password saving at the policy layer (higher-precedence than user prefs).
125 + - [`usr/lib64/firefox/mozilla.cfg`](../usr/lib64/firefox/mozilla.cfg): system-wide default prefs, loaded via Firefox autoconfig. Configures compact UI density, blank new tab, DuckDuckGo suggestions off, quiet scroll, and enables `toolkit.legacyUserProfileCustomizations.stylesheets` so `userChrome.css` gets read. Every entry uses `defaultPref()` so users can still override in `about:config`.
126 + - [`usr/lib64/firefox/defaults/pref/autoconfig.js`](../usr/lib64/firefox/defaults/pref/autoconfig.js): one-line pointer telling Firefox to load `mozilla.cfg` at startup.
127 + - [`etc/skel/.mozilla/firefox/profiles.ini`](../etc/skel/.mozilla/firefox/profiles.ini) + [`etc/skel/.mozilla/firefox/alloy.default/chrome/userChrome.css`](../etc/skel/.mozilla/firefox/alloy.default/chrome/userChrome.css): first-launch profile seed. `profiles.ini` names a fixed-path profile (`alloy.default/`) so the pre-seeded `chrome/userChrome.css` (Helium-style ultra-compact horizontal, Alloy light-mode tokens from [TOKENS.md](TOKENS.md)) lands under it. Firefox opens the seeded profile on first launch instead of generating a random-suffix one.
128 128
129 129 Runs all modern sites (uBlock Origin blocks ads, never JS). Horizontal tabs only, per firm preference.
130 130
@@ -142,7 +142,7 @@
142 142
143 143 Rejected:
144 144 - **Floorp.** Was the previous pick. Rough edges in daily use, ESR-based cadence lags Firefox security patches, fork-specific patches add drift Alloy doesn't own. Upstream Firefox with policies + userChrome.css captures the actual value (visual and behavioral configurability) at less cost.
145 - - **LibreWolf.** Ships uBlock Origin preinstalled — real win — but its hardening (`resistFingerprinting`, letterboxing, cookie clears on close, WebGL off) breaks modern sites. Un-hardening it to pass Alloy's "runs all modern sites" bar erases the reason to choose it over Firefox.
145 + - **LibreWolf.** Ships uBlock Origin preinstalled (a real win), but its hardening (`resistFingerprinting`, letterboxing, cookie clears on close, WebGL off) breaks modern sites. Un-hardening it to pass Alloy's "runs all modern sites" bar erases the reason to choose it over Firefox.
146 146 - **Zen Browser.** Gecko-based and design-forward, but its identity centers vertical tabs and sidebar-forward layout. Revisit only if Zen ships a first-class horizontal mode as a supported configuration.
147 147 - **Chromium as default, Brave, Vivaldi, Arc, Helium.** Alloy doesn't reinforce browser-engine monoculture. Ungoogled-chromium covers the "I need Blink" case without making it the default.
148 148
@@ -150,77 +150,77 @@
150 150
151 151 **Nushell as the login shell; bash unchanged as `/bin/sh` and `/bin/bash`.** Full rationale, architecture, and interoperability rules in [SHELL.md](SHELL.md).
152 152
153 - The short version: Alloy's audience writes their own scripts, so nu's structured pipelines earn their ecosystem cost. Every daily tool-glue interaction compounds the advantage. The `curl | sh` install pattern is unaffected because it invokes `/bin/sh`, not the login shell — Alloy leaves bash in place at both `/bin/sh` and `/bin/bash`, and nu is added as an option in `/etc/shells`. Reversible with `chsh -s /bin/bash`.
153 + The short version: Alloy's audience writes their own scripts, so nu's structured pipelines earn their ecosystem cost. Every daily tool-glue interaction compounds the advantage. The `curl | sh` install pattern is unaffected because it invokes `/bin/sh`, not the login shell; Alloy leaves bash in place at both `/bin/sh` and `/bin/bash`, and nu is added as an option in `/etc/shells`. Reversible with `chsh -s /bin/bash`.
154 154
155 - Config lives at [`etc/skel/.config/nushell/`](../etc/skel/.config/nushell/) — `env.nu`, `config.nu`, `aliases.nu`, plus a README covering install and verification.
155 + Config lives at [`etc/skel/.config/nushell/`](../etc/skel/.config/nushell/): `env.nu`, `config.nu`, `aliases.nu`, plus a README covering install and verification.
156 156
157 - **Prompt:** starship (Rust, cross-shell). Cross-shell prompt engines are exactly the kind of tool where "boring, works" earns its keep. Bespoke Alloy nu prompt deferred as a design-system exercise for later.
157 + **Prompt:** starship (Rust, cross-shell). Cross-shell prompt engines are exactly the kind of tool where "boring, works" earns its keep. A custom Alloy nu prompt is deferred as a design-system exercise for later.
158 158
159 159 Rejected: fish (safer pick but Alloy's audience is script-writers), zsh (raw zsh unpleasant; adopting oh-my-zsh/zinit imports someone else's opinions), bash-as-login (un-opinionated at a layer where Alloy is opinionated everywhere else).
160 160
161 - ## Utility defaults — content viewers
161 + ## Utility defaults: content viewers
162 162
163 163 Alloy ships defaults for the three content types users open constantly. Not shipping them cedes the choice to whatever a user's first web search returns (VLC for video, nomacs for images), which conflicts with the curated-defaults principle everywhere else.
164 164
165 165 ### Video: **mpv**
166 166
167 - C, mature, keyboard-driven, no chrome by default, extensible via Lua. Handles local files, streams, and YouTube via yt-dlp. Zero visual identity out of the box, which is exactly what a video player should be. Config at [`etc/skel/.config/mpv/`](../etc/skel/.config/mpv/) — hardware decode, no OSC/OSD chatter, screenshots to `~/Pictures/Screenshots`, yt-dlp capped at 1080p.
167 + C, mature, keyboard-driven, no chrome by default, extensible via Lua. Handles local files, streams, and YouTube via yt-dlp. Zero visual identity out of the box, which is exactly what a video player should be. Config at [`etc/skel/.config/mpv/`](../etc/skel/.config/mpv/): hardware decode, no OSC/OSD chatter, screenshots to `~/Pictures/Screenshots`, yt-dlp capped at 1080p.
168 168
169 - Rejected: VLC (retained-mode GTK UI, oversized surface), celluloid (adds retained UI on top of mpv — defeats the point), Haruna (Qt/KDE-shaped). No production-quality Rust video player exists.
169 + Rejected: VLC (retained-mode GTK UI, oversized surface), celluloid (adds retained UI on top of mpv, defeating the point), Haruna (Qt/KDE-shaped). No production-quality Rust video player exists.
170 170
171 171 ### Images: **imv**
172 172
173 - C, Wayland-native, tiny, dedicated to viewing. Config at [`etc/skel/.config/imv/`](../etc/skel/.config/imv/) — Alloy warm-cream background, overlay hidden by default, vi-like binds inherited from imv defaults.
173 + C, Wayland-native, tiny, dedicated to viewing. Config at [`etc/skel/.config/imv/`](../etc/skel/.config/imv/): Alloy warm-cream background, overlay hidden by default, vi-like binds inherited from imv defaults.
174 174
175 - Rejected: oculante (Rust and Alloy-toolkit-aligned via egui, but its RAW/EXIF/crop feature set drifts toward "photo tool" territory that overlaps GIMP/darktable's job — wrong scope for a base-image image viewer), swayimg (newer, less mature), nomacs (Qt, heavy), feh (X11).
175 + Rejected: oculante (Rust and Alloy-toolkit-aligned via egui, but its RAW/EXIF/crop feature set drifts toward "photo tool" territory that overlaps GIMP/darktable's job; wrong scope for a base-image image viewer), swayimg (newer, less mature), nomacs (Qt, heavy), feh (X11).
176 176
177 - Revisit oculante if daily-use workflow reveals gaps imv can't cover — the Rust-alignment case is real, just outweighed here by focus-of-scope.
177 + Revisit oculante if daily-use workflow reveals gaps imv can't cover; the Rust-alignment case is real, outweighed here by focus-of-scope.
178 178
179 179 ### PDF: **zathura**
180 180
181 - C, vim-like keybinds, MuPDF backend, tiny, extensible to djvu/ps/epub via plugins. Config at [`etc/skel/.config/zathura/`](../etc/skel/.config/zathura/) — Alloy palette on chrome, `i` toggles recolor for reading dark PDFs on the cream background, statusbar-only chrome.
181 + C, vim-like keybinds, MuPDF backend, tiny, extensible to djvu/ps/epub via plugins. Config at [`etc/skel/.config/zathura/`](../etc/skel/.config/zathura/): Alloy palette on chrome, `i` toggles recolor for reading dark PDFs on the cream background, statusbar-only chrome.
182 182
183 - Rejected: sioyek (research-oriented, C++/Qt, great for papers but too specialized for base image — users who read lots of academic PDFs install it themselves), evince/okular (retained-mode, mouse-driven), mupdf viewer (even smaller than zathura but no config surface worth naming). No production-quality Rust PDF viewer exists.
183 + Rejected: sioyek (research-oriented, C++/Qt, great for papers but too specialized for base image; users who read lots of academic PDFs install it themselves), evince/okular (retained-mode, mouse-driven), mupdf viewer (even smaller than zathura but no config surface worth naming). No production-quality Rust PDF viewer exists.
184 184
185 185 **PDF annotation is not a shipped default.** zathura reads; the Rnote fold-to-notes workflow was shelved in the pivot (TUI-first clashes with a stylus GUI, see [MANIFESTO.md](MANIFESTO.md#status) and [HARDWARE-FW12.md](HARDWARE-FW12.md)). Do not add annotation to the zathura config.
186 186
187 - ## Utility defaults — system introspection
187 + ## Utility defaults: system introspection
188 188
189 189 The daily-use tools that replace legacy GNU-utils habits with keyboard-driven Rust equivalents. All three ship in the base image.
190 190
191 191 ### Editor: **helix**
192 192
193 - Rust, modal (selection-first grammar, Kakoune-shaped), LSP + tree-sitter + formatters batteries-included, TOML config, zero-config to be useful. Config at [`etc/skel/.config/helix/`](../etc/skel/.config/helix/) — editor UX in `config.toml`, Akari Dawn theme shipped verbatim at `themes/akari-dawn.toml` (with `akari-night.toml` alongside for dark mode) from Shu Kutsuzawa's [Akari](https://github.com/cappyzawa/akari-theme) (MIT).
193 + Rust, modal (selection-first grammar, Kakoune-shaped), LSP + tree-sitter + formatters batteries-included, TOML config, zero-config to be useful. Config at [`etc/skel/.config/helix/`](../etc/skel/.config/helix/): editor UX in `config.toml`, Akari Dawn theme shipped verbatim at `themes/akari-dawn.toml` (with `akari-night.toml` alongside for dark mode) from Shu Kutsuzawa's [Akari](https://github.com/cappyzawa/akari-theme) (MIT).
194 194
195 195 `EDITOR=hx` is set by `etc/skel/.config/nushell/env.nu`; git commit messages, `crontab -e`, and every other `$EDITOR`-respecting tool routes to helix.
196 196
197 197 Rejected:
198 - - **neovim.** Ecosystem depth is real, but raw neovim isn't modern — modernity comes from importing a config framework (LazyVim, kickstart, AstroNvim), which is the same pattern Alloy rejected for zsh + oh-my-zsh. Alloy's curated-defaults ethos wants a tool whose defaults *are* the value; helix delivers that, neovim doesn't.
198 + - **neovim.** Ecosystem depth is real, but raw neovim isn't modern; modernity comes from importing a config framework (LazyVim, kickstart, AstroNvim), which is the same pattern Alloy rejected for zsh + oh-my-zsh. Alloy's curated-defaults ethos wants a tool whose defaults *are* the value; helix delivers that, neovim doesn't.
199 199 - **vim.** Older, less LSP-integrated, less tree-sitter-integrated. Zero Alloy alignment.
200 200 - **Kakoune.** Helix's direct ancestor in grammar; helix is the modern re-implementation with LSP built-in.
201 - - **cosmic-edit.** Retained-mode iced UI — different tool class (graphical editor). Not the shipped default; users install if wanted.
201 + - **cosmic-edit.** Retained-mode iced UI, a different tool class (graphical editor). Not the shipped default; users install if wanted.
202 202
203 203 **Real cost:** vim muscle memory transfers imperfectly. Helix's selection-first grammar (`3wd` instead of `d3w`) is a genuine improvement but takes 1-2 weeks to internalize. Users who prefer vim install neovim via `dnf`.
204 204
205 205 ### Monitor: **bottom** (`btm`)
206 206
207 - Rust, ratatui-based, TOML config. Config at [`etc/skel/.config/bottom/`](../etc/skel/.config/bottom/) — Alloy palette on graphs and borders, click disabled (keyboard-only), CPU widget as the default focus.
207 + Rust, ratatui-based, TOML config. Config at [`etc/skel/.config/bottom/`](../etc/skel/.config/bottom/): Alloy palette on graphs and borders, click disabled (keyboard-only), CPU widget as the default focus.
208 208
209 - Same ratatui toolkit family as the forthcoming `alloy_tui` crate — bottom and `alloy console` read as design siblings.
209 + Same ratatui toolkit family as the forthcoming `alloy_tui` crate, so bottom and `alloy console` read as design siblings.
210 210
211 211 Nu alias: `top = btm` (`etc/skel/.config/nushell/aliases.nu`).
212 212
213 - Rejected: htop (C, canonical but less capable, no disk I/O), btop (C++, decorative — Alloy's aesthetic is understated), gtop (Node.js, not applicable).
213 + Rejected: htop (C, canonical but less capable, no disk I/O), btop (C++, decorative; Alloy's aesthetic is understated), gtop (Node.js, not applicable).
214 214
215 215 ### Disk usage: **dua**
216 216
217 - Rust, interactive terminal UI, vim-like navigation, marks-for-deletion. Minimal config surface (CLI flags at runtime). Nu alias: `du = dua interactive` — the interactive TUI is the daily-use mode.
217 + Rust, interactive terminal UI, vim-like navigation, marks-for-deletion. Minimal config surface (CLI flags at runtime). Nu alias: `du = dua interactive`; the interactive TUI is the daily-use mode.
218 218
219 - Rejected: ncdu (C, works but not Rust), gdu (Go, no advantage over dua), dust (Rust, one-shot rather than interactive — a different tool for a different job; users install via `dnf` if they want scriptable disk summaries).
219 + Rejected: ncdu (C, works but not Rust), gdu (Go, no advantage over dua), dust (Rust, one-shot rather than interactive; a different tool for a different job, users install via `dnf` if they want scriptable disk summaries).
220 220
221 - **dust deferred** rather than shipped — one tool per job is cleaner for the base image. Revisit if daily use reveals a real need for a scriptable companion.
221 + **dust deferred** rather than shipped: one tool per job is cleaner for the base image. Revisit if daily use reveals a real need for a scriptable companion.
222 222
223 - ## Utility defaults — Wayland session glue
223 + ## Utility defaults: Wayland session glue
224 224
225 225 The small tools that translate keybinds and system state into user-visible behavior. Least aesthetic axis of the three utility clusters; picks are dictated by "plays well with sway" more than visual identity.
226 226
@@ -235,7 +235,7 @@
235 235 exec wl-paste --type image --watch cliphist store
236 236 ```
237 237
238 - No config file — cliphist's storage lives at `~/.local/share/cliphist/db` and needs no tuning.
238 + No config file; cliphist's storage lives at `~/.local/share/cliphist/db` and needs no tuning.
239 239
240 240 Rejected: clipse (Go, a full TUI where a `cliphist list` pipe suffices), copyq (Qt, retained-mode GUI overkill).
241 241
@@ -243,29 +243,29 @@
243 243
244 244 Rust, hardware-accelerated (VAAPI), small. CLI-driven, no config file.
245 245
246 - Rejected: wf-recorder (C++, older, no hardware accel by default), OBS (way overscope for base image — power users install).
246 + Rejected: wf-recorder (C++, older, no hardware accel by default), OBS (way overscope for base image; power users install).
247 247
248 248 **Not in the v0 image (deferred 2026-07-19).** wl-screenrec is packaged in neither Fedora main nor Terra, so shipping it meant a dedicated `cargo install` build stage that pulled a Rust toolchain, clang, and the ffmpeg headers (912 packages) into every cold build for one binary. The pick stands; only the packaging cost is being refused. It returns when a repo carries it, or when there is a second cargo-built binary to share the stage's cost. Until then the image has no screen recorder and the `screenrec` Nu function is removed from `etc/skel/.config/nushell/aliases.nu`. Users who want it now: `cargo install wl-screenrec`.
249 249
250 250 ### Volume/brightness OSD: **swayosd**
251 251
252 - Rust, systemd user daemon, GTK-rendered overlays for volume/brightness/caps-lock/num-lock. Config at [`etc/skel/.config/swayosd/`](../etc/skel/.config/swayosd/) — Alloy palette CSS with amber `accent-warn` progress bar.
252 + Rust, systemd user daemon, GTK-rendered overlays for volume/brightness/caps-lock/num-lock. Config at [`etc/skel/.config/swayosd/`](../etc/skel/.config/swayosd/): Alloy palette CSS with amber `accent-warn` progress bar.
253 253
254 254 **Sway integration:** Fn keys bound to `swayosd-client --output-volume raise` and similar in the sway config (see the config for the block).
255 255
256 - Rejected: avizo (Python, less maintained), custom mako notifications for the OSD (mako is for notifications, not indicator overlays — different job).
256 + Rejected: avizo (Python, less maintained), custom mako notifications for the OSD (mako is for notifications, not indicator overlays; different job).
257 257
258 258 ### Media keys: **playerctl**
259 259
260 - C, MPRIS client. CLI. No config — sway binds media keys directly to `exec playerctl play-pause` and similar. Handles Spotify, mpv, Firefox, and any MPRIS-compliant source.
260 + C, MPRIS client. CLI. No config; sway binds media keys directly to `exec playerctl play-pause` and similar. Handles Spotify, mpv, Firefox, and any MPRIS-compliant source.
261 261
262 262 Rejected: playerctld (still a playerctl variant), no serious alternative.
263 263
264 264 ### Night mode: **gammastep**
265 265
266 - C, small daemon, Wayland-native color-temperature adjustment. Config at [`etc/skel/.config/gammastep/`](../etc/skel/.config/gammastep/) — 3700K night / 6500K day with fade, manual location placeholder the user edits.
266 + C, small daemon, Wayland-native color-temperature adjustment. Config at [`etc/skel/.config/gammastep/`](../etc/skel/.config/gammastep/): 3700K night / 6500K day with fade, manual location placeholder the user edits.
267 267
268 - Rejected: redshift (X11), wlsunset (simpler but time-only, no location provider — gammastep can do both).
268 + Rejected: redshift (X11), wlsunset (simpler but time-only, no location provider; gammastep can do both).
269 269
270 270 ## Cursor theme
271 271
@@ -273,33 +273,33 @@
273 273
274 274 Applied three ways because different apps read cursor state from different places:
275 275
276 - - `~/.icons/default/index.theme` inherits from `Bibata-Modern-Classic` — most apps and sway itself resolve this.
277 - - `gtk-cursor-theme-name` in `~/.config/gtk-{3.0,4.0}/settings.ini` — GTK apps.
278 - - `XCURSOR_THEME` and `XCURSOR_SIZE` in `etc/skel/.config/nushell/env.nu` — everything that reads env vars.
276 + - `~/.icons/default/index.theme` inherits from `Bibata-Modern-Classic`; most apps and sway itself resolve this.
277 + - `gtk-cursor-theme-name` in `~/.config/gtk-{3.0,4.0}/settings.ini`: GTK apps.
278 + - `XCURSOR_THEME` and `XCURSOR_SIZE` in `etc/skel/.config/nushell/env.nu`: everything that reads env vars.
279 279
280 280 Full config at [`etc/skel/.icons/default/`](../etc/skel/.icons/default/).
281 281
282 - Rejected: DMZ-White/Black (the most historically classic Linux cursor — X.Org/Ubuntu/Debian/Fedora default for 15+ years — but its high-contrast angularity fights Alloy's warm chrome; ship this only if the target aesthetic is "utilitarian Linux default"), Adwaita (GNOME's cool grey; fights warm palette), Bibata Modern Ice (too cool), Bibata Modern Amber (doubles with the accent-warn amber — reads too warm overall).
282 + Rejected: DMZ-White/Black (the most historically classic Linux cursor, the X.Org/Ubuntu/Debian/Fedora default for 15+ years, but its high-contrast angularity fights Alloy's warm chrome; ship this only if the target aesthetic is "utilitarian Linux default"), Adwaita (GNOME's cool grey; fights warm palette), Bibata Modern Ice (too cool), Bibata Modern Amber (doubles with the accent-warn amber; reads too warm overall).
283 283
284 - ## GTK theme — palette patch over adw-gtk3
284 + ## GTK theme: palette patch over adw-gtk3
285 285
286 286 **adw-gtk3 (base) + Alloy palette patch (layered).** Install `adw-gtk3-theme` from Fedora as the base, then override libadwaita's ~20 named color tokens (`window_bg_color`, `accent_color`, `card_bg_color`, etc.) with the Alloy light-mode ramp via `~/.config/gtk-{3.0,4.0}/gtk.css`.
287 287
288 288 Config at [`etc/skel/.config/gtk-3.0/`](../etc/skel/.config/gtk-3.0/) and [`etc/skel/.config/gtk-4.0/`](../etc/skel/.config/gtk-4.0/) with matching `gtk.css` and `settings.ini` per version.
289 289
290 - **What this covers:** any GTK 3 or GTK 4 app that consumes libadwaita's named tokens — swayosd (GTK-rendered overlays), Firefox's system dialogs, xdg-desktop-portal-gtk dialogs, etc.
290 + **What this covers:** any GTK 3 or GTK 4 app that consumes libadwaita's named tokens: swayosd (GTK-rendered overlays), Firefox's system dialogs, xdg-desktop-portal-gtk dialogs, etc.
291 291
292 292 **What this doesn't cover:**
293 - - Legacy GTK 3 apps with their own token sets (rare — most have migrated).
293 + - Legacy GTK 3 apps with their own token sets (rare; most have migrated).
294 294 - Qt apps. Different theming pipeline entirely; Alloy's Qt surface is near-zero, so left unaddressed.
295 295 - Dark-mode variant. Light-first. Add `@media (prefers-color-scheme: dark)` blocks when needed.
296 296
297 297 **Ongoing maintenance:** libadwaita renames or adds tokens across GNOME releases (~2x/year). Small deltas, not rewrites. Read the libadwaita release notes when Fedora ships a new GNOME version.
298 298
299 299 Rejected:
300 - - **Bespoke Alloy GTK theme (fork adw-gtk3 with Alloy tokens baked in).** Real project — hundreds of selectors, ongoing GTK-version drift. Deferred to v2+ if Alloy has resources for it. The palette-patch approach captures 90% of the visual win for 10% of the work.
300 + - **Custom Alloy GTK theme (fork adw-gtk3 with Alloy tokens baked in).** A real project: hundreds of selectors, ongoing GTK-version drift. Deferred to v2+ if Alloy has resources for it. The palette-patch approach captures 90% of the visual win for 10% of the work.
301 301 - **libadwaita's own accent-color mechanism (GNOME 47+).** Only touches accent, leaves chrome greys untouched. Insufficient for a full palette swap to a curated theme like Akari.
302 - - **Kvantum, Materia, Arc, Yaru, WhiteSur.** All ship their own visual identity that would have to be re-styled to match Alloy — same total work as building the bespoke theme, without the base of libadwaita compatibility.
302 + - **Kvantum, Materia, Arc, Yaru, WhiteSur.** All ship their own visual identity that would have to be re-styled to match Alloy: the same total work as building the custom theme, without the base of libadwaita compatibility.
303 303
304 304 ## Fonts
305 305
@@ -309,27 +309,27 @@
309 309
310 310 ### Monospace: **IosevkaTerm Nerd Font**
311 311
312 - The Term variant of Iosevka drops ligatures — safe for TUIs (yazi, bottom, helix status columns) that count characters. The Nerd Font bundle carries the glyphs starship, yazi, and bottom need for prompt icons and file-type indicators. Iosevka's narrow proportions give more columns per line than JetBrains Mono or Fira Code — real screen-real-estate value in a tiling stack.
312 + The Term variant of Iosevka drops ligatures, which is safe for TUIs (yazi, bottom, helix status columns) that count characters. The Nerd Font bundle carries the glyphs starship, yazi, and bottom need for prompt icons and file-type indicators. Iosevka's narrow proportions give more columns per line than JetBrains Mono or Fira Code, real screen-real-estate value in a tiling stack.
313 313
314 314 Applied at:
315 - - `etc/skel/.config/mpv/mpv.conf` — `osd-font='IosevkaTerm Nerd Font'`.
316 - - `etc/skel/.config/imv/config` — `overlay_font = IosevkaTerm Nerd Font:11`.
317 - - Rio config (when it lands) — `fonts.regular.family = "IosevkaTerm Nerd Font"`.
318 - - fontconfig `monospace` alias — everything else that asks for mono.
315 + - `etc/skel/.config/mpv/mpv.conf`: `osd-font='IosevkaTerm Nerd Font'`.
316 + - `etc/skel/.config/imv/config`: `overlay_font = IosevkaTerm Nerd Font:11`.
317 + - Rio config (when it lands): `fonts.regular.family = "IosevkaTerm Nerd Font"`.
318 + - fontconfig `monospace` alias: everything else that asks for mono.
319 319
320 320 Rejected: JetBrains Mono (wider characters cost columns per line), Fira Code (older, ligatures on by default), Cascadia Code (Microsoft-shaped, fine but less Alloy-neutral), Berkeley Mono (paid, can't ship).
321 321
322 322 ### Sans: **Atkinson Hyperlegible**
323 323
324 - Designed by the Braille Institute specifically to maximize letter distinguishability (Il1, O0, cCG all made unambiguous). Optimized-for-readability by literal institutional charter — the honest answer to "make UI text as readable as possible." Fedora repos ship it.
324 + Designed by the Braille Institute specifically to maximize letter distinguishability (Il1, O0, cCG all made unambiguous). Optimized-for-readability by institutional charter, the honest answer to "make UI text as readable as possible." Fedora repos ship it.
325 325
326 326 Applied at:
327 - - `etc/skel/.config/gtk-{3.0,4.0}/settings.ini` — `gtk-font-name = Atkinson Hyperlegible 11`.
328 - - fontconfig `sans-serif` alias — everything else that asks for sans.
327 + - `etc/skel/.config/gtk-{3.0,4.0}/settings.ini`: `gtk-font-name = Atkinson Hyperlegible 11`.
328 + - fontconfig `sans-serif` alias: everything else that asks for sans.
329 329
330 330 **One-line swap to Inter** if a more conventional interface font is preferred: Inter is tighter at UI sizes, more common in mainstream design, still highly legible. Replace the first `<family>` in the sans-serif alias and the `gtk-font-name` value. Alloy's shipped default is Atkinson because it matches the "optimize readability" thesis more literally.
331 331
332 - Rejected: Inter (excellent but less legibility-maximized — kept as documented alternate), IBM Plex Sans (corporate-shaped), Roboto/Cantarell (default-neutral, no advantage over Atkinson), Iosevka Aile (family coherence appealing but Aile isn't as readable at small sizes as dedicated UI fonts).
332 + Rejected: Inter (excellent but less legibility-maximized; kept as documented alternate), IBM Plex Sans (corporate-shaped), Roboto/Cantarell (default-neutral, no advantage over Atkinson), Iosevka Aile (family coherence appealing but Aile isn't as readable at small sizes as dedicated UI fonts).
333 333
334 334 ### Serif: **not shipped**
335 335
@@ -337,7 +337,7 @@
337 337
338 338 ## Greeter
339 339
340 - **greetd + tuigreet.** Rust, minimal, ratatui-rendered. greetd is the daemon that owns VT1; tuigreet is the ratatui client that prompts for user/password and execs `sway` on successful auth. Config at [`etc/greetd/`](../etc/greetd/) — `/etc/greetd/config.toml` sets up VT1 with tuigreet + Alloy palette applied via `--theme`.
340 + **greetd + tuigreet.** Rust, minimal, ratatui-rendered. greetd is the daemon that owns VT1; tuigreet is the ratatui client that prompts for user/password and execs `sway` on successful auth. Config at [`etc/greetd/`](../etc/greetd/): `/etc/greetd/config.toml` sets up VT1 with tuigreet + Alloy palette applied via `--theme`.
341 341
342 342 Same ratatui toolkit family as `alloy console` and `bottom`; the login screen reads as a design sibling to the rest of the Alloy chrome.
343 343
@@ -346,7 +346,7 @@
346 346 - **sddm.** Qt-based, KDE default. Qt surface is near-zero elsewhere in Alloy; adopting it just for the login screen is unjustified.
347 347 - **lightdm.** X11-first, Wayland support via plugins; older shape.
348 348 - **ly.** TUI display manager (ncurses, C). Established but not Rust, no design-system alignment.
349 - - **regreet.** Rust GTK greetd greeter — nicer visuals than tuigreet but drags GTK into the login layer. tuigreet's austerity is a feature.
349 + - **regreet.** Rust GTK greetd greeter; nicer visuals than tuigreet but drags GTK into the login layer. tuigreet's austerity is a feature.
350 350 - **agreety.** greetd's default plain-text prompt. Works but visually inconsistent with the rest of the stack.
351 351
352 - **Later: bespoke Alloy greetd greeter using `alloy_tui`.** A v2+ candidate alongside the console. Would put the login screen inside the same design system as everything else Alloy authors. Deferred until `alloy_tui` v1 lands.
352 + **Later: a custom Alloy greetd greeter using `alloy_tui`.** A v2+ candidate alongside the console. Would put the login screen inside the same design system as everything else Alloy authors. Deferred until `alloy_tui` v1 lands.
M docs/TOKENS.md +8 -8
@@ -70,11 +70,11 @@
70 70 - **Tinted-not-neutral chrome.** Chrome tones should carry a hue tint (see [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#core-principle-tinted-greyscale-chrome-color-as-information)); Akari's warm-clay palette is the canonical worked example.
71 71 - **Accent-on-glyph, not on body text.** Confirmed by contrast math in the audit: accents that clear WCAG AA-text (4.5:1) on the theme's surface can be used freely; accents that only clear AA-UI (3.0:1) go on glyphs and edge markers, with body text staying at `content.primary`.
72 72
73 - Because storage is hex, OKLCH doesn't need to be preserved in the file — it's the mental model of the person authoring the theme.
73 + Because storage is hex, OKLCH doesn't need to be preserved in the file; it's the mental model of the person authoring the theme.
74 74
75 75 ## Gate: the WCAG audit
76 76
77 - Any theme, downloaded or authored, is audited by [`tools/wcag_audit.py`](../tools/wcag_audit.py). It reads a theme-common `.toml`, converts hex → linear sRGB → WCAG 2.1 relative luminance, and reports pass/fail for every affordance-carrying pair (text-on-surface, borders-on-surface, accents-on-surface, elevation deltas). Also computes `border-subtle` / `border-strong` via the same derivation Alloy uses at runtime.
77 + Any theme, downloaded or authored, is audited by [`tools/wcag_audit.py`](../tools/wcag_audit.py). It reads a theme-common `.toml`, converts hex to linear sRGB to WCAG 2.1 relative luminance, and reports pass/fail for every affordance-carrying pair (text-on-surface, borders-on-surface, accents-on-surface, elevation deltas). Also computes `border-subtle` / `border-strong` via the same derivation Alloy uses at runtime.
78 78
79 79 Run:
80 80
@@ -82,9 +82,9 @@
82 82 python3 tools/wcag_audit.py MNW/shared/themes/akari-dawn.toml
83 83 ```
84 84
85 - The audit is not a hard-fail gate — a theme with accents that only reach AA-UI on some surface tiers is still shippable if the accent-on-glyph rule is honored. The audit's job is to make trade-offs visible.
85 + The audit is not a hard-fail gate: a theme with accents that only reach AA-UI on some surface tiers is still shippable if the accent-on-glyph rule is honored. The audit's job is to make trade-offs visible.
86 86
87 - ## Verified contrast — Akari Dawn
87 + ## Verified contrast: Akari Dawn
88 88
89 89 Full audit output at time of adoption. Method: OKLCH-agnostic; reads hex, computes WCAG 2.1 relative luminance directly.
90 90
@@ -106,13 +106,13 @@
106 106 | `line.border` | 1.37 | 1.49 | 1.55 |
107 107 | `border-subtle` (derived) | 1.12 | 1.22 | 1.27 |
108 108
109 - `border-strong` clears AA-UI on all three surfaces — the derivation formula works for Akari's soft border. `line.border` and `border-subtle` are decorative dividers, not required to meet 3.0.
109 + `border-strong` clears AA-UI on all three surfaces; the derivation formula works for Akari's soft border. `line.border` and `border-subtle` are decorative dividers, not required to meet 3.0.
110 110
111 111 **Accents on surfaces:**
112 112
113 113 | Accent | vs page | vs raised | vs sunken | vs overlay | Notes |
114 114 |---|---:|---:|---:|---:|---|
115 - | `action.primary` (lantern `#8a4530`) | 5.28 | 5.74 | **4.11** | 5.99 | AA-text on three of four; AA-UI on `sunken` — glyph-safe on the darkest tier |
115 + | `action.primary` (lantern `#8a4530`) | 5.28 | 5.74 | **4.11** | 5.99 | AA-text on three of four; AA-UI on `sunken`, glyph-safe on the darkest tier |
116 116 | `status.danger` (`#6a2828`) | 8.07 | 8.78 | 6.28 | 9.15 | AAA everywhere |
117 117 | `status.success` (`#3a5830`) | 6.00 | 6.53 | 4.67 | 6.81 | AA-text everywhere |
118 118 | `status.warning` (`#b07840`) | **2.80** | **3.05** | **2.18** | **3.18** | Fails AA-text on all tiers; AA-UI only on `raised`/`overlay`. **Amber-on-cream is inherent to Akari's aesthetic**; per the accent-on-glyph rule, warning is expressed as glyph + edge marker only. |
@@ -120,9 +120,9 @@
120 120
121 121 The two failure modes are both Akari-inherent, not derivation bugs, and both survive Alloy's discipline: `action.primary` on `surface.sunken` still clears AA-UI (glyph-safe on the darkest recessed panel), and `status.warning`'s amber is a glyph-only accent per the same rule that governs `accent-syntax` in the previous Alloy palette.
122 122
123 - ## Verified contrast — Akari Night
123 + ## Verified contrast: Akari Night
124 124
125 - Text on surfaces all AAA or AA-text. Borders derived at 7+ contrast (dark themes have more headroom because the border tone is against a light content.primary). Notable accent behavior: `status.danger` at `#d25046` lands at 3.3–4.1:1 across surfaces — AA-UI territory, glyph-safe under the same rule. Full audit reproducible via the script.
125 + Text on surfaces all AAA or AA-text. Borders derived at 7+ contrast (dark themes have more headroom because the border tone is against a light content.primary). Notable accent behavior: `status.danger` at `#d25046` lands at 3.3–4.1:1 across surfaces: AA-UI territory, glyph-safe under the same rule. Full audit reproducible via the script.
126 126
127 127 ## What's deferred
128 128