Skip to main content

max / alloy

docs: realign the design docs to the TUI-first, Sway pivot Rewrite the design docs for the 2026-07-17 pivot (Sway compositor, ratatui-authored, egui/marquee dropped, FW12 fold-to-notes shelved): - MANIFESTO: TUI-first thesis, author/adopt flip, egui->ratatui carries the immediate-mode principle. - STACK: Sway, swaybar, swaylock, mako, grim/slurp, Helix-only editor, terminal-driven launcher, sway session-glue integrations. - CONSOLE: alloy_tui is the design system; drop the egui sibling, the hinged subcommand, and niri/ironbar schema refs. - COMPONENT-LIBRARY + TOKENS: retargeted to ratatui as the build spec for alloy_tui (four idioms, focus/keymap model, palette->Color::Rgb, cell-grid geometry, terminal font notes). - DESIGN-LANGUAGE, ICONOGRAPHY, RESEARCH, COSMIC, MARQUEE-APPS, HARDWARE-FW12, IMAGE, SHELL: adapted or retired to match. Leaves the agent's in-flight crates/alloy_tui and Cargo.lock untouched.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-18 02:33 UTC
Signed with PGP, not checked
Commit: 8a86b10eb2923ff4bbe8b48ae9631c99ab473868
Parent: bdce9c4
13 files changed, +371 insertions, -484 deletions
@@ -1,137 +1,144 @@
1 1 # Alloy component library
2 2
3 - The intended idioms for the `alloy_ui` crate — the egui design-system library that all marquee Alloy apps depend on.
3 + The intended idioms for the `alloy_tui` crate: the ratatui design-system library the `alloy` console and every authored Alloy TUI depend on. It is Alloy's one authored visual identity (the egui `alloy_ui` sibling was dropped in the 2026-07-17 pivot).
4 4
5 5 ## Scope
6 6
7 - `alloy_ui` exposes:
7 + `alloy_tui` exposes:
8 8
9 - - **Design tokens** — palette, typography scale, spacing scale, geometry constants. Single source of truth for everything in [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md).
10 - - **Themed primitives** — buttons, text fields, tags, list rows, focus rings — all configured against the tokens, all carrying mandatory state variants.
11 - - **Layout combinators** — Alloy-flavored wrappers over egui's existing `horizontal` / `vertical` / `scope` HOFs.
12 - - **Composed patterns** — cards, dialog skeletons, form rows — built from the primitives.
9 + - **Design tokens** as ratatui `Color` / `Style` / `Modifier` constants. 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 a small focus-ring/navigation model the app drives from its event loop.
13 + - **Backend detection** — the mock-or-real pattern ported from mountaineer-sysop's `sysop-tui`, so a TUI runs against mocks on any host and binds to real system state when present.
13 14
14 15 It does *not* expose:
15 16
16 - - Application-level state or logic
17 - - Reactive primitives, signals, observables (would violate principle 3 in [MANIFESTO.md](MANIFESTO.md))
18 - - Hot-reloadable DSL files (we don't have one to reload)
17 + - Application-level state or logic.
18 + - Reactive primitives, signals, observables (would violate principle 3 in [MANIFESTO.md](MANIFESTO.md)).
19 +
20 + ## The ratatui architecture, stated plainly
21 +
22 + ratatui is immediate-mode *rendering* with event-driven *input*. Each tick the app draws its whole UI from current state into a `Frame` (no retained widget tree to diverge), then separately handles input events that mutate that state. `alloy_tui` follows that split: the primitives below are render helpers, and the focus/keymap model is what turns key events into state changes. There is no inline `button.clicked()` as in a retained or egui-style toolkit; a "button" renders in its current state, and the app's event handler decides what the activation key does when that control holds focus.
19 23
20 24 ## The four idioms
21 25
22 - The "DSL feel" recovers through pure Rust patterns. The crate uses four idioms consistently so authoring across the marquee apps feels uniform.
26 + The crate uses four idioms consistently so authoring across TUIs feels uniform.
23 27
24 - ### Functions as components
28 + ### Functions for one-shot render helpers
25 29
26 - The lightest pattern. A component is a function taking `&mut Ui`:
30 + The lightest pattern. A helper takes the `Frame` and an area and draws from state:
27 31
28 32 ```rust
29 - pub fn primary_button(ui: &mut Ui, label: &str) -> Response {
30 - ui.add(
31 - Button::new(label)
32 - .fill(tokens::SURFACE_RAISED)
33 - .stroke(Stroke::new(1.0, tokens::BORDER))
34 - .corner_radius(tokens::RADIUS_CONTROL),
35 - )
33 + pub fn status_bar(f: &mut Frame, area: Rect, state: &StatusState) {
34 + let style = match state.severity {
35 + Severity::Error => tokens::style_on_surface(tokens::ACCENT_ERROR),
36 + Severity::Ok => tokens::TEXT_MUTED,
37 + };
38 + f.render_widget(Paragraph::new(state.message.as_str()).style(style), area);
36 39 }
37 -
38 - // Call site:
39 - if primary_button(ui, "Install").clicked() { install(); }
40 40 ```
41 41
42 - Use functions for one-shot widgets that take few arguments and don't need configuration.
42 + Use functions for one-shot chrome that takes few arguments and needs no configuration.
43 43
44 44 ### Builders for configurable widgets
45 45
46 - For widgets with optional configuration:
46 + ratatui widgets are already builders; `alloy_tui` wraps them so the theming is not restatable per call site. A wrapper either returns a configured ratatui widget or renders itself:
47 47
48 48 ```rust
49 - pub struct Card<'a> {
49 + pub struct AlloyBlock<'a> {
50 50 title: &'a str,
51 - body: Option<&'a str>,
52 - primary: Option<(&'a str, Box<dyn FnOnce() + 'a>)>,
53 - secondary: Option<(&'a str, Box<dyn FnOnce() + 'a>)>,
51 + focused: bool,
52 + kind: SurfaceKind, // Data (square) vs Control (bordered)
54 53 }
55 54
56 - impl<'a> Card<'a> {
55 + impl<'a> AlloyBlock<'a> {
57 56 pub fn new(title: &'a str) -> Self { /* ... */ }
58 - pub fn body(mut self, body: &'a str) -> Self { /* ... */ }
59 - pub fn primary(mut self, label: &'a str, on_click: impl FnOnce() + 'a) -> Self { /* ... */ }
60 - pub fn secondary(mut self, label: &'a str, on_click: impl FnOnce() + 'a) -> Self { /* ... */ }
61 - pub fn show(self, ui: &mut Ui) -> Response { /* ... */ }
62 - }
63 -
64 - // Call site reads DSL-like:
65 - Card::new("Update available")
66 - .body("A layered package is ready.")
67 - .primary("Install", || install())
68 - .secondary("Skip", || skip())
69 - .show(ui);
70 - ```
71 -
72 - ### HOFs for layout and scope
73 -
74 - egui's layout primitives are already HOFs taking closures. Alloy extends with themed wrappers:
75 -
76 - ```rust
77 - pub fn section<R>(ui: &mut Ui, title: &str, content: impl FnOnce(&mut Ui) -> R) -> R {
78 - ui.vertical(|ui| {
79 - ui.add_space(tokens::SPACING_LG);
80 - ui.label(RichText::new(title).font(tokens::FONT_SECTION_HEADER));
81 - ui.add_space(tokens::SPACING_SM);
82 - ui.scope(content).inner
83 - }).inner
84 - }
85 -
86 - // Composes naturally:
87 - alloy_ui::section(ui, "Network", |ui| {
88 - alloy_ui::field_row(ui, "SSID", |ui| {
89 - ui.text_edit_singleline(&mut self.ssid);
90 - });
91 - alloy_ui::field_row(ui, "Password", |ui| {
92 - ui.add(TextEdit::singleline(&mut self.pw).password(true));
93 - });
94 - });
95 - ```
96 -
97 - ### `impl Widget` for shared reusable types
98 -
99 - egui's canonical extension trait. Use for types that have state or that need to participate in `ui.add(...)` chains:
100 -
101 - ```rust
102 - pub struct UrgencyTag { kind: Urgency }
103 -
104 - impl UrgencyTag {
105 - pub fn new(kind: Urgency) -> Self { Self { kind } }
106 - }
107 -
108 - impl Widget for UrgencyTag {
109 - fn ui(self, ui: &mut Ui) -> Response {
110 - let color = match self.kind {
111 - Urgency::Low => tokens::INFO,
112 - Urgency::Medium => tokens::WARN,
113 - Urgency::High => tokens::ERROR,
114 - };
115 - let text = RichText::new(self.kind.label()).color(tokens::TEXT_PRIMARY);
116 - let bg = ui.painter().add(/* rounded rect at `color` */);
117 - let response = ui.add(/* text label */);
118 - ui.painter().set(bg, /* finalized rect */);
119 - response
57 + pub fn focused(mut self, yes: bool) -> Self { /* ... */ }
58 + pub fn kind(mut self, kind: SurfaceKind) -> Self { /* ... */ }
59 + // Returns a themed ratatui Block; the caller renders it.
60 + pub fn block(self) -> Block<'a> {
61 + let border_style = if self.focused { tokens::BORDER_STRONG } else { tokens::BORDER };
62 + Block::default()
63 + .title(self.title)
64 + .borders(Borders::ALL)
65 + .border_type(BorderType::Plain)
66 + .border_style(border_style)
67 + .style(tokens::SURFACE_RAISED)
120 68 }
121 69 }
122 70
123 71 // Call site:
124 - ui.add(UrgencyTag::new(Urgency::High));
72 + let inner = AlloyBlock::new("Network").focused(self.focus == Pane::Net).block();
73 + f.render_widget(inner, area);
125 74 ```
126 75
76 + ### Layout helpers over the constraint solver
77 +
78 + ratatui's `Layout` is the flow primitive; `alloy_tui` adds themed splits that bake in Alloy's cell spacing:
79 +
80 + ```rust
81 + // A titled section: a header row, one cell of breathing space, then the body area.
82 + pub fn section(area: Rect, title: &str) -> (Rect /* header */, Rect /* body */) {
83 + let rows = Layout::vertical([
84 + Constraint::Length(1), // header
85 + Constraint::Length(tokens::SPACE_SM),// gap, in cells
86 + Constraint::Min(0), // body
87 + ]).split(area);
88 + (rows[0], rows[2])
89 + }
90 + ```
91 +
92 + Composition is by nesting `Layout` splits, exactly as ratatui intends; the helpers only fix the spacing constants and header styling.
93 +
94 + ### `Widget` / `StatefulWidget` for reusable types
95 +
96 + ratatui's canonical extension traits. Use for types that render themselves and, when they carry selection/scroll state, `StatefulWidget`:
97 +
98 + ```rust
99 + pub struct UrgencyTag { kind: Urgency }
100 +
101 + impl Widget for UrgencyTag {
102 + fn render(self, area: Rect, buf: &mut Buffer) {
103 + let accent = match self.kind {
104 + Urgency::Low => tokens::ACCENT_INFO,
105 + Urgency::Medium => tokens::ACCENT_WARN,
106 + Urgency::High => tokens::ACCENT_ERROR,
107 + };
108 + // Accent on the glyph + label only, never as a filled background:
109 + // color is information, not chrome (see DESIGN-LANGUAGE.md).
110 + Line::from(vec![
111 + Span::styled(self.kind.glyph(), Style::default().fg(accent)),
112 + Span::raw(" "),
113 + Span::styled(self.kind.label(), tokens::TEXT_PRIMARY),
114 + ]).render(area, buf);
115 + }
116 + }
117 +
118 + // Call site:
119 + f.render_widget(UrgencyTag { kind: Urgency::High }, area);
120 + ```
121 +
122 + The starter widget set (per [CONSOLE.md](CONSOLE.md)): `AlloyBlock`, `AlloyList`, `AlloyForm`, `AlloyTable`, `AlloyStatusBar`, `AlloyLog`, plus the schema-driven form fields (`ColorField`, `EnumField`, `RangedNumberField`, `ToggleField`, `TextField`).
123 +
124 + ## Focus and keymap model
125 +
126 + The part ratatui does not provide. `alloy_tui` ships:
127 +
128 + - A `Focus` type: an ordered ring of focusable pane/field ids, with `next()` / `prev()` / `current()`. The app holds one `Focus` in its state; widgets render their focused variant when `focus.current() == self.id`.
129 + - Reserved keymap constants so every Alloy TUI navigates identically: `Tab` / `Shift-Tab` move focus, `Enter` activates, `Esc` cancels, `Ctrl-S` saves, `q` quits. These live in one place; apps match against them rather than hardcoding keycodes.
130 + - A footer-chrome helper that renders the active keymap hints along the bottom row, so the reserved keys are always discoverable.
131 +
132 + This is the direct descendant of `sysop-tui`; port its focus/footer/keymap code as the seed.
133 +
127 134 ## Design tokens
128 135
129 - Tokens are the single source of truth. They live in a TOML file at the crate root and are compiled into Rust constants at build time via a `build.rs` step. This is deliberately a one-way pipeline — designers (or design tooling) author the TOML; Rust code consumes the constants.
136 + Tokens are the single source of truth. They live in a TOML file at the crate root and are compiled into Rust constants at build time via a `build.rs` step. One-way pipeline: design tooling (or a hand-edited TOML) authors the tokens; Rust consumes the generated constants.
130 137
131 - Canonical token values (light + dark ramp, accent palette, typography, geometry) are locked in [TOKENS.md](TOKENS.md). The TOML below is illustrative shape; the actual `tokens.toml` is generated from TOKENS.md when the crate is scaffolded.
138 + Canonical values (light + dark ramp, accent palette, typography role split, geometry) are locked in [TOKENS.md](TOKENS.md), which also explains which tokens survive the move to a terminal cell grid (the palette and semantic roles) and which do not (pixel radii, sub-cell borders). The TOML below is illustrative shape:
132 139
133 140 ```toml
134 - # alloy_ui/tokens.toml (shape; values per TOKENS.md)
141 + # alloy_tui/tokens.toml (shape; values per TOKENS.md)
135 142 [palette.light]
136 143 surface = "oklch(96% 0.012 80)"
137 144 surface-raised = "oklch(98% 0.012 80)"
@@ -139,6 +146,7 @@
139 146 text-primary = "oklch(18% 0.012 80)"
140 147 text-muted = "oklch(55% 0.012 80)"
141 148 border = "oklch(80% 0.012 80)"
149 + border-strong = "oklch(65% 0.012 80)"
142 150
143 151 [palette.accents]
144 152 error = "oklch(55% 0.18 25)"
@@ -147,68 +155,42 @@
147 155 info = "oklch(60% 0.16 240)"
148 156 syntax = "oklch(55% 0.18 310)"
149 157
150 - [typography]
151 - mono = "JetBrains Mono Nerd Font"
152 - size-body = 14
153 - size-section-header = 16
154 - weight-body = 400
155 - weight-heading = 600
156 -
157 - [geometry]
158 - spacing-sm = 4
159 - spacing-md = 8
160 - spacing-lg = 16
161 - radius-control = 4
162 - border-width = 1
158 + [spacing] # in terminal cells, not pixels
159 + sm = 1
160 + md = 2
161 + lg = 3
163 162 ```
164 163
165 - The build step generates `tokens.rs`:
164 + The build step converts OKLCH to sRGB and emits `tokens.rs` as ratatui values:
166 165
167 166 ```rust
168 167 // generated
169 - pub const SURFACE: Color32 = /* parsed OKLCH → sRGB */;
170 - pub const RADIUS_CONTROL: f32 = 4.0;
171 - // ...
168 + pub const SURFACE: Style = Style::new().bg(Color::Rgb(0xF5, 0xF3, 0xEC));
169 + pub const SURFACE_RAISED: Style = Style::new().bg(Color::Rgb(0xFA, 0xF8, 0xF2));
170 + pub const TEXT_PRIMARY: Style = Style::new().fg(Color::Rgb(0x2A, 0x28, 0x22));
171 + pub const BORDER: Style = Style::new().fg(Color::Rgb(0xC9, 0xC3, 0xB4));
172 + pub const BORDER_STRONG: Style = Style::new().fg(Color::Rgb(0xA0, 0x99, 0x82));
173 + pub const ACCENT_ERROR: Color = Color::Rgb(0xC0, 0x3A, 0x2F);
174 + pub const SPACE_SM: u16 = 1;
172 175 ```
173 176
174 - Dark mode is the polarity-flipped derivative of the light ramp — same hue, same chroma, L-stops repositioned to keep elevation ordering intact (raised reads lighter than base in both modes). See [TOKENS.md](TOKENS.md#the-ramp-dark-mode-derived) for the resolved derivation and the locked dark L values.
177 + Truecolor is the target (rio and every modern terminal render 24-bit `Color::Rgb`). Provide a 256-color downgrade table for terminals that report no truecolor, computed at build time from the same OKLCH values.
178 +
179 + Dark mode is the polarity-flipped derivative of the light ramp (same hue, same chroma, L-stops repositioned so `surface-raised` still reads lighter than `surface`). The crate exposes both palettes and a `Mode` the app selects at startup or from `$COLORFGBG` / terminal query. See [TOKENS.md](TOKENS.md#the-ramp-dark-mode-derived).
175 180
176 181 ## Per-frame discipline
177 182
178 - The crate exposes a single visual-application function called at the top of every frame:
179 -
180 - ```rust
181 - pub fn apply_alloy_visuals(ctx: &Context, mode: Mode) {
182 - let mut style = (*ctx.style()).clone();
183 - style.visuals = match mode {
184 - Mode::Light => visuals_light(),
185 - Mode::Dark => visuals_dark(),
186 - };
187 - style.spacing = spacing();
188 - style.text_styles = text_styles();
189 - ctx.set_style(style);
190 - }
191 - ```
192 -
193 - Marquee apps call this once per frame at the top of their `update` loop. There is no other path by which Alloy visuals enter an app. The discipline keeps the theming centralized and prevents per-app drift.
183 + ratatui already redraws the whole frame each tick, so there is no `set_style` seam to police as egui had. The discipline is narrower and just as strict: **widgets never construct a `Color` or `Style` literally; they only reference `tokens::` constants.** A single `Theme { mode }` value threads through the render call so light/dark selection happens in one place. That keeps theming centralized and prevents per-app drift.
194 184
195 185 ## What this enforces
196 186
197 - - Every Alloy app looks coherent because they all consume the same tokens and the same primitives.
198 - - The non-reactive principle is enforced by not exposing any reactive primitives in the crate.
199 - - Mandatory state variants from [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#affordance-depth-via-ramp-not-via-color) are baked into the themed primitives — `primary_button` cannot be called without producing all five states because egui's `Button` handles them and Alloy's wrapper themes all five.
200 - - The token TOML is the seam where design tools could plug in.
187 + - Every Alloy TUI looks coherent because they all consume the same tokens and the same primitives.
188 + - The non-reactive principle is enforced by exposing no reactive primitives.
189 + - Mandatory state variants from [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#affordance-depth-via-ramp-not-via-color) are baked into the primitives: `AlloyBlock` cannot render without resolving its focused/unfocused border from the ramp, and form fields resolve their disabled style from `text-muted`.
190 + - The token TOML is the seam where design tooling could plug in.
201 191
202 - ## Late-term: Figma → tokens (and maybe more)
192 + ## Late-term: a design tool, and the token seam
203 193
204 - The token TOML is the deliberate seam where a Figma integration could live. Realistic shape, in order of feasibility:
194 + The token TOML is the deliberate seam where external design tooling could live, and it is toolkit-agnostic: anything that emits a valid `tokens.toml` is a viable producer (a hand-edited file, Tokens Studio, a future Alloy-built tool). A speculative token-sync tool keeps the working name **`cast`** (alloys are cast; the tool casts a design source into Alloy tokens). The broader research question of a design tool native to immediate-mode UI is captured in [RESEARCH-IMMEDIATE-MODE-DESIGN.md](RESEARCH-IMMEDIATE-MODE-DESIGN.md); with the pivot to ratatui, that question now targets terminal compositions rather than GPU-drawn ones, but the architecture is the same.
205 195
206 - **Phase 1 — Token sync (realistic, high value).** A Figma plugin (TypeScript) or a Rust CLI hitting the Figma REST API that reads color styles, text styles, spacing variables, and corner-radius variables out of an Alloy design file in Figma and emits `tokens.toml`. Round-trips cleanly because tokens are flat key-value data. ~2-week scope if the Figma file is structured deliberately for export. Realistic v3+ candidate.
207 -
208 - **Phase 2 — Static skeleton generation (interesting, limited).** Read Figma frames that represent component layouts (cards, dialog templates, list rows) and emit `impl Widget` skeletons that lay out children using `alloy_ui::section` / `field_row` / etc. Useful for the "designer sketches the shape, dev fills in the behavior" workflow. Wrong shape for full UIs because Figma's absolute positioning and constraint model don't map cleanly to egui's flow-based layout — but for constrained component patterns it works. Maybe v4+, probably narrower scope than imagined.
209 -
210 - **Phase 3 — Full WYSIWYG transpilation (not the right shape).** Trying to make Figma the source of truth for entire app layouts fights both tools. egui is intentionally about authored layout in code; making it the consumer of a visual designer's pixel-positioned output loses egui's strengths and inherits Figma's weaknesses. Skip.
211 -
212 - The architectural commitment that keeps this door open: **tokens are a file, not Rust source.** As long as `tokens.toml` is the authority and Rust consumes generated constants, *anything* that emits a valid `tokens.toml` works as a design tool — Figma, Tokens Studio, Penpot, a future Alloy-built design app, or just a TOML in someone's editor. The Figma plugin becomes one possible producer, not a coupling.
213 -
214 - A speculative name for the Figma tool, if it ever exists: **`cast`** (alloys are cast; the tool casts Figma into Alloy tokens).
196 + The commitment that keeps the door open: **tokens are a file, not Rust source.** As long as `tokens.toml` is the authority and Rust consumes generated constants, the producer is swappable and the design system stays portable.
M docs/CONSOLE.md +13 -13
@@ -17,9 +17,8 @@
17 17 ```
18 18 alloy net # network: nmcli / iwd front
19 19 alloy audio # audio: wpctl / pactl front
20 - alloy display # outputs: niri msg output / wlr-randr front
20 + alloy display # outputs: swaymsg output / wlr-randr front
21 21 alloy update # system updates: rpm-ostree front
22 - alloy hinged # alloy-hinged daemon status / toggle
23 22 alloy tail # tailscale front (see CONTINUITY.md)
24 23 alloy sync # syncthing front (see CONTINUITY.md)
25 24 alloy config <path> # schema-driven TOML/KDL editor
@@ -50,8 +49,8 @@
50 49 Three options considered. Committing to option 2 with 3 as opportunistic input.
51 50
52 51 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.
53 - 2. **Explicit schema files, one per adopted config.** Alloy ships `schemas/rio.toml.schema`, `schemas/yazi.toml.schema`, `schemas/niri.kdl.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.
54 - 3. **Upstream schemas (JSON Schema, KDL schema) where they exist.** Niri publishes a KDL schema; 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.
52 + 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.)
53 + 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.
55 54
56 55 ### The schema format
57 56
@@ -95,37 +94,38 @@
95 94
96 95 ## `alloy_tui` — the ratatui design system
97 96
98 - Sibling to `alloy_ui` (egui). Both consume the same `tokens.toml` (see [TOKENS.md](TOKENS.md)); the palette, typography, and semantic colors are unified across GUI and TUI.
97 + `alloy_tui` **is** Alloy's design system. The pivot dropped the egui `alloy_ui` sibling, so there is no GUI counterpart — this crate carries the whole authored visual identity. It consumes `tokens.toml` (see [TOKENS.md](TOKENS.md)); palette, typography, and semantic colors render as terminal chrome.
99 98
100 99 Contents (v1):
101 100 - Themed `ratatui` widget wrappers: `AlloyBlock`, `AlloyList`, `AlloyForm`, `AlloyTable`, `AlloyStatusBar`, `AlloyLog`.
102 - - Extension traits on `ratatui::Frame` matching the pattern from `alloy_ui`.
101 + - Extension traits on `ratatui::Frame`.
103 102 - Form-field widgets driven by the schema types above (`ColorField`, `EnumField`, `RangedNumberField`, etc.).
104 103 - Consistent keymap constants (Tab, Shift-Tab, Enter, Esc, Ctrl-S, q).
104 + - Footer chrome, reserved keys, and mock-or-real backend detection ported from mountaineer-sysop's `sysop-tui`.
105 105
106 - Coherence across GUI and TUI was already a claimed property of the immediate-mode paradigm in [STACK.md](STACK.md). `alloy_tui` alongside `alloy_ui` makes it a *shipped* property.
106 + 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`.
107 107
108 108 ## Relationship to the rest of the stack
109 109
110 110 - **Replaces the graphical settings app** Alloy would otherwise eventually be pressured into shipping.
111 111 - **Complements yazi.** Yazi is for files; the console is for state and config. Different data, similar TUI aesthetic.
112 - - **Fronts, does not replace, the underlying CLIs.** `nmcli`, `wpctl`, `rpm-ostree`, `niri msg` all remain the ground truth. The console is a courteous surface.
113 - - **Design-system parity.** `alloy console` and `alloy` (egui apps) should feel like siblings — same palette, same typography weights, same information hierarchy — even though one draws with block characters and the other with pixels.
112 + - **Fronts, does not replace, the underlying CLIs.** `nmcli`, `wpctl`, `rpm-ostree`, `swaymsg` all remain the ground truth. The console is a courteous surface.
113 + - **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.
114 114
115 115 ## Roadmap
116 116
117 - - **v0.5 (post-v0 stack, pre-v1 lockscreen).** `alloy config` alone, with schemas for the v0-adopted configs (rio, yazi, niri, mako, ironbar). No live-state subcommands yet. Ships as a way for users to configure the stack without hand-editing TOML.
117 + - **v0.5 (post-v0 stack).** `alloy config` alone, with schemas for the v0-adopted TOML configs (rio, yazi, mako, and others; the sway config takes the text-edit fallback). No live-state subcommands yet. Ships as a way for users to configure the stack without hand-editing TOML.
118 118 - **v1.** Add `alloy net`, `alloy audio`, `alloy display`, `alloy update`, `alloy tail`, `alloy sync`. Fills out the console as a system control surface. First-boot flow (see [CONTINUITY.md](CONTINUITY.md)) is a thin shim over `alloy tail` and `alloy sync` enrollments.
119 - - **v1.x.** `alloy hinged` for FW12 users. Additional adopted-tool schemas as the v0 stack grows.
119 + - **v1.x.** Additional adopted-tool schemas as the v0 stack grows. (`alloy hinged` was shelved with the FW12 tablet flow in the pivot.)
120 120 - **v2+.** Third-party subcommand registration (a well-known directory of ratatui adapters the console discovers at runtime), if a real ecosystem case emerges. Not planned.
121 121
122 - Positioned between v0 stack packaging and v1 marquee apps. Roughly 3-6 months of real work at the v1 scope. Smaller than v1 egui lockscreen individually but broader across subcommands.
122 + Positioned right after v0 stack packaging as the primary authored work. Roughly 3-6 months at the v1 scope, broad across subcommands. With the egui marquee apps dropped, the console is the wedge.
123 123
124 124 ## Non-goals
125 125
126 126 - **Not a shell replacement.** Users still live in rio + zellij/tmux + helix. The console is invoked for specific tasks, then closed.
127 127 - **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.
128 - - **Not a launcher.** anyrun is the launcher. The console is not discovered via search — it's invoked by name.
128 + - **Not a launcher.** Launching is the shell (terminal-driven); there is no graphical launcher. The console is invoked by name, not discovered via search.
129 129 - **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.
130 130
131 131 ## Open questions
M docs/COSMIC.md +16 -16
@@ -1,31 +1,31 @@
1 1 # COSMIC adoption
2 2
3 - System76's COSMIC desktop is the deepest Rust + Linux investment outside Mozilla. GPLv3 — no longer a license-forcing function now that Alloy authors in egui (MIT/Apache), but still permissively-compatible. Project Alloy pulls on COSMIC's libraries and one app, with deliberate restraint to avoid ceding visual identity.
3 + System76's COSMIC desktop is the deepest Rust + Linux investment outside Mozilla. GPLv3, permissively-compatible with Alloy (which authors in ratatui, MIT). Project Alloy pulls on COSMIC's libraries and one app, with deliberate restraint to avoid ceding visual identity.
4 4
5 - Architectural note: libcosmic and iced are reactive, which conflicts with Alloy's non-reactive principle ([MANIFESTO.md](MANIFESTO.md) principle 3) for *authored* apps. Adopted COSMIC components are exempt — they're not our code, they're upstream's. The principle binds what Alloy writes, not what Alloy ships.
5 + Architectural note: libcosmic and iced are reactive, which conflicts with Alloy's non-reactive principle ([MANIFESTO.md](MANIFESTO.md) principle 3) for *authored* apps. Adopted COSMIC components are exempt: they are not our code, they are upstream's. The principle binds what Alloy writes, not what Alloy ships.
6 6
7 7 ## Adopted
8 8
9 - - **cosmic-text** — text rendering library (font shaping, layout, BiDi). Pure library, no visual coupling. Dependency-grade adoption — just add the crate.
10 - - **cosmic-files** — GUI file manager (named in [STACK.md](STACK.md) as the graphical fallback). Accepts libcosmic visual flavor in those windows as a knowing trade.
9 + - **cosmic-text** — text rendering library (font shaping, layout, BiDi). Pure library, no visual coupling. Dependency-grade adoption. (Reaches Alloy transitively today; a direct dependency only if an authored surface needs shaping beyond what ratatui provides.)
10 + - **cosmic-files** — GUI file manager, named in [STACK.md](STACK.md) as the graphical fallback to yazi. Since the pivot dropped Alloy's authored GUI, this is effectively the one non-session GUI app Alloy ships, and it wears libcosmic's visual flavor as a knowing trade.
11 11
12 12 ## Considered, deferred
13 13
14 - - **cosmic-term** — third Rust terminal option to revisit when the terminal decision is reopened. Uses libcosmic widgets so visually COSMIC-flavored, but standalone-runnable.
15 - - **cosmic-edit** — only if Alloy ships a default GUI text editor. Open question whether the default should be helix/zed/vim in the terminal vs a graphical editor.
16 - - **cosmic-notifications** — potential more-Rust-aligned v0/v1 alternative to mako if it runs cleanly under Niri. Worth a few minutes of investigation when the time comes.
17 - - **cosmic-screenshot** — probably skip; Niri built-in is cleaner.
18 - - **cosmic-icons** — worth taking if Alloy doesn't build its own icon theme. Visually COSMIC-flavored.
19 - - **cosmic-bg** — wallpaper daemon. Revisit if Niri lacks a clean wallpaper story.
14 + - **cosmic-term** — a Rust terminal option to revisit only if the rio decision is reopened. libcosmic-flavored but standalone-runnable.
15 + - **cosmic-edit** — not relevant now: Alloy's editor is a TUI (helix, then deox). No graphical-editor default.
16 + - **cosmic-notifications** — mako is settled as the notification daemon; revisit only if mako proves inadequate.
17 + - **cosmic-screenshot** — skip; grim + slurp is the picked screenshot stack under sway.
18 + - **cosmic-icons** — worth taking only if a graphical surface ever needs a themed icon set; the TUI uses Nerd Font glyphs (see [ICONOGRAPHY.md](ICONOGRAPHY.md)).
19 + - **cosmic-bg** — wallpaper daemon; swww is the pick and works on sway.
20 20
21 21 ## Skipped — conflicts with picks or DE-coupled
22 22
23 - - **libcosmic** as an app framework — committed to egui, and libcosmic is reactive anyway.
24 - - **cosmic-panel** — picked Ironbar.
25 - - **cosmic-launcher** — picked anyrun.
26 - - **cosmic-comp** — picked Niri.
27 - - **cosmic-session, cosmic-settings, cosmic-randr, cosmic-workspaces, cosmic-store** — DE-coupled or overlap with Alloy-authored components (the package GUI replaces the store).
23 + - **libcosmic** as an app framework — Alloy authors in ratatui, and libcosmic is reactive anyway.
24 + - **cosmic-panel** — picked swaybar.
25 + - **cosmic-launcher** — no graphical launcher (terminal-driven).
26 + - **cosmic-comp** — picked sway.
27 + - **cosmic-session, cosmic-settings, cosmic-randr, cosmic-workspaces, cosmic-store** — DE-coupled or overlap with the `alloy` console (`alloy update` fronts rpm-ostree, replacing the store; `alloy net`/`display`/`audio` replace the settings surfaces).
28 28
29 29 ## Known tension
30 30
31 - Adopting cosmic-files means a libcosmic-flavored window sits alongside Alloy's egui marquee apps. Accepted as a knowing trade — building a Rust GUI file manager from scratch is wrong-scope vs the value of having one at all. Heterogeneity is the price.
31 + Adopting cosmic-files means a libcosmic-flavored window sits alongside an otherwise terminal-native, ramp-themed system. Accepted as a knowing trade: building a Rust GUI file manager is wrong-scope against the value of having a graphical fallback at all, and yazi is the primary anyway. Heterogeneity at the authored/adopted seam is the price, per [MANIFESTO.md](MANIFESTO.md#what-we-author-vs-what-we-adopt).
@@ -1,22 +1,20 @@
1 1 # Alloy Design Language
2 2
3 - The visual rules Alloy's authored apps follow, and that adopted apps are themed toward where possible.
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/) — a niri rice on Arch that demonstrates most of these principles in a non-Alloy stack.
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
9 - The UI is monochrome in a single tinted greyscale ramp. "Tinted" is the load-bearing word: the ramp is biased toward a hue (warm beige in light mode, cool slate in dark), never neutral grey, never pure white or black at the extremes. Surfaces, text, borders, icons, scrollbars, focus rings, decorative elements — all live on this ramp.
9 + The UI is monochrome in a single tinted greyscale ramp. "Tinted" is the load-bearing word: the ramp is biased toward a hue (warm beige in light mode, warm charcoal in dark), never neutral grey, never pure white or black at the extremes. Surfaces, text, borders, box-drawing, scrollbars, focus borders, all live on this ramp.
10 10
11 - Color is reserved for information. A red value means something is wrong. A green bar means utilization. A syntax-highlighted token means it carries semantic weight worth the eye's attention. If a color does not encode information, it does not appear.
11 + Color is reserved for information. A red value means something is wrong. A green bar means utilization. A syntax-highlighted token carries semantic weight worth the eye's attention. If a color does not encode information, it does not appear.
12 12
13 - This is the rule that organizes everything else.
13 + This is the rule that organizes everything else, and it is a natural fit for a terminal, where color is scarce and legible precisely because it is rare.
14 14
15 15 ## Light and dark are polarity flips of the same tint
16 16
17 - Dark mode is not a different palette. It is the light-mode ramp with its lightness inverted along the same hue axis. A warm-beige light mode flips to a warm-charcoal dark mode; the underlying tint hue is preserved. An app themed for one is themed for both, and switching is a single lightness inversion, not a recolor.
18 -
19 - Practical consequence: define the ramp once as a sequence of (hue, chroma, lightness) stops. The mode toggle inverts lightness only. Components reference ramp positions (`surface`, `surface-raised`, `text-primary`, `text-muted`, `border`) and never raw hex values.
17 + Dark mode is not a different palette. It is the light-mode ramp with its lightness inverted along the same hue axis. A warm-beige light mode flips to a warm-charcoal dark mode; the tint hue is preserved. Components reference ramp positions (`surface`, `surface-raised`, `text-primary`, `text-muted`, `border`) and never raw values. The mode toggle inverts lightness only.
20 18
21 19 ## The information palette
22 20
@@ -28,65 +26,55 @@
28 26 - **Blue** — informational, link, in-progress
29 27 - **Magenta / violet** — categorical accent for syntax or data series where the other four are taken
30 28
31 - Saturation stays moderate. These accents sit on the tinted-greyscale surface; they do not become the surface. The reference btop-style screenshot ([inspo/...zwjh98...](inspo/light-mode-rice-for-a-light-laptop-niri-v0-zwjh98apqxig1.webp)) is the canonical example: dense data, fully colored where it encodes severity or series identity, every pixel of chrome remaining tinted grey.
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.
32 30
33 31 ## What this rules out
34 32
35 - - Branded accent colors on non-data chrome (no "Alloy purple" buttons, header bars, or focus rings).
36 - - Decorative gradients, glassmorphism, neon highlights, ambient color washes.
33 + - Branded accent colors on non-data chrome (no "Alloy" accent on pane borders, headers, or focus).
34 + - Ambient color washes, gratuitous reverse-video blocks, rainbow chrome.
37 35 - Per-app theming that introduces a new hue. An app picks a position on the existing ramp; it does not extend the palette.
38 - - Status communicated by emoji or icon-only. Color-as-information is paired with a label or shape, never color alone (accessibility floor).
36 + - Status communicated by color alone. Color-as-information is paired with a label or glyph, never color alone (accessibility floor, and doubly important in a terminal where a user may have remapped the 16-color base).
39 37
40 38 ## Typography
41 39
42 - Mono-only. Two monospace families with disjoint roles: a **working font** (Iosevka) for anything authored or read — terminal, code, body, labels, controls, settings descriptions — and a **display font** (Departure Mono) for readouts: clocks, dashboard numerics, status panel values, lockscreen, the *value on the instrument* as opposed to the chrome around it. No paired humanist sans; prose stays in the working mono.
43 -
44 - Sizes step on a small fixed scale. In the working font, **weight** (Regular ↔ SemiBold) is the primary hierarchy lever. In the display font, weight is unavailable by design (single-weight); hierarchy comes from **size + case + tracking** — `UPPERCASE` letter-spaced labels above large numerics, in the mil-spec placard tradition.
45 -
46 - The exact tokens (font names, sizes, weights, tracking, line-heights) live in [TOKENS.md](TOKENS.md#typography).
40 + Mono, by nature: a terminal renders one fixed-width font (IosevkaTerm Nerd Font). The former two-font working/display split does not apply; hierarchy comes from the levers a terminal has: **weight** (`Modifier::BOLD` for emphasis and section headers), **case** (`UPPERCASE` letter-labels above readouts, in the mil-spec placard tradition), and **Nerd Font glyphs**. Column alignment is free: the terminal font is already tabular, so numeric columns align without a special display font. The exact tokens live in [TOKENS.md](TOKENS.md#typography).
47 41
48 42 ## Geometry
49 43
50 - Rectilinear. Thin one-pixel borders in a low-contrast ramp position. No shadows. Corners are square or have a single small radius applied uniformly. Padding is generous around tabular regions so the data — which carries the color — has room to breathe against the grey chrome.
44 + Rectilinear, and a terminal is rectilinear by construction. Box-drawing borders (`BorderType::Plain`) in a low-contrast ramp position. No shadows (a terminal has none to give, which suits the no-decorative-shadows rule). No corner radius (cells cannot round). Generous blank-cell padding around tabular regions so the data, which carries the color, has room to breathe against the grey chrome.
51 45
52 46 ## Affordance: depth via ramp, not via color
53 47
54 - The constraints above (monochrome chrome, no decorative shadows, color reserved for information) remove the standard vocabularies designers reach for to signal interactivity. The rice screenshots in [inspo/](inspo/) are honest about this — they're terminal dashboards where almost nothing is clickable, so they never had to solve the affordance problem. Alloy's authored apps do.
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:
55 49
56 - The replacement vocabulary, in the Apple HIG tradition of *discoverable depth*:
50 + **Surface elevation is the depth language.** Use at least three ramp positions that read as elevation: `surface` (base, the terminal background), `surface-raised` (the focused pane, active list, 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`.
57 51
58 - **Surface elevation is the depth language.** Define at minimum three ramp positions that read as elevation: `surface` (base), `surface-raised` (interactive containers, cards, controls), `surface-overlay` (popovers, menus, dialogs). The step between levels is tonal, not shadowed. A control announces itself as interactive by sitting on a raised surface; a data panel sits flush on `surface`. Elevation is the primary affordance cue.
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`. Selected adds a `border-strong` leading-edge stripe on the row. 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.)
59 53
60 - **State variants are mandatory, not optional.** Every interactive element defines `default`, `hover`, `pressed`, `focused`, and `disabled` at minimum. Hover shifts one ramp step toward the user (lighter in light mode, lighter in dark mode — toward the eye, not toward the mode's polarity). Pressed shifts one ramp step away. Disabled drops to a `text-muted`-adjacent position with the same surface. There is no such thing as an egui widget shipped with only a default state.
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.
61 55
62 - **Functional tonal edges are permitted; decorative shadows are not.** A one-step tonal edge (a 1px border drawn at `surface-raised` + 1 ramp step, or an inset/outset highlight of the same magnitude) is a *functional* affordance cue — it announces a control's boundary. This is not a softening of the no-decorative-shadows rule; it is a clarification that the rule targets ambient drop shadows, glows, and glassmorphism, not the one-step edge cues the HIG tradition uses to define interactive boundaries.
56 + **Focus is obligatory and unambiguous.** Keyboard focus is shown by the focused pane's border going to `border-strong`, with enough contrast to be unmistakable at arm's length. Focus uses the ramp, not the information palette; it is chrome, not data. Making focus subtle as a "clean" choice is a regression.
63 57
64 - **Focus rings are obligatory and unambiguous.** Keyboard focus is shown by a ring at a ramp position with enough contrast against the focused element's surface that it is unmistakable at arm's length on a laptop screen. Focus rings use the ramp, not the information palette — they are chrome, not data. Skipping focus rings, or making them visually subtle as a "clean" choice, is a regression.
65 -
66 - **Square is for data; small-radius is for controls.** The "single small radius applied uniformly" from [Geometry](#geometry) is *uniform within controls*, not uniform across surface kinds. Data panels and tabular regions are square. Interactive controls (buttons, fields, switches, list rows that respond to click) carry the small radius. The shape difference is itself an affordance cue — corners communicate whether a region expects to be touched.
67 -
68 - **Cursor changes are part of the contract.** Pointer over interactive elements becomes the standard pointing cursor; over text regions becomes the I-beam; over draggable handles becomes the resize/move cursor. This is enforced at the egui widget level (in the Alloy design-system crate) so it cannot be forgotten per-app.
69 -
70 - These rules apply to every authored chrome element. They do not loosen the color-as-information principle — the entire affordance vocabulary above lives on the tinted greyscale ramp.
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.
71 59
72 60 ## Composition patterns
73 61
74 - Patterns that emerge from applying the affordance rules above to common layouts. These are binding wherever the layout applies.
62 + Binding wherever the layout applies.
75 63
76 - **Action hierarchy: three tiers, polarity-driven.** Primary actions invert polarity (the button's surface is `text-primary`, its text is `surface-raised`, its border is `text-primary`) — visually heavy without recruiting an accent color. Secondary actions sit on `surface` (one step down from `surface-raised`) with normal text. Disabled actions hold `surface-raised` with `text-muted` text and `border-subtle` borders. Three tiers, all chrome. A panel never shows more than one primary action.
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.
77 65
78 - **List and sidebar selection: leading-edge stripe.** The current selection in a sidebar or list is marked with a 2px stripe at `border-strong` along the leading edge of the row, with the row's surface raised one step. This rhymes with the focus ring (also `border-strong`) but applied as an edge instead of a ring, so selection and focus are distinguishable when they coincide. Selection is chrome; do not color it with 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, with the row's surface raised one step. 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.
79 67
80 - **Inline messages: accent only at the edge.** Color-as-information messages (error, warn, healthy, info) carry a 3px left border in the accent color on a `surface-raised` background. The message text and accompanying glyph use the accent; the message body text stays at `text-primary`. The accent never fills the message's surface — that would turn chrome into data.
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).
81 69
82 - **Titlebars: one step above raised.** Window titlebars sit on `surface-overlay` to mark the window perimeter as distinct from its content (`surface-raised`). This is the only non-overlay use of `surface-overlay`; everything else at that level is a true popover.
70 + **Titlebars / header rows: one step above raised.** A window or pane header sits on `surface-overlay` to mark its perimeter as distinct from content on `surface-raised`.
83 71
84 - **Tabular numerics: always in display font with tabular-nums.** Any column of numeric values uses Departure Mono with `font-variant-numeric: tabular-nums`, regardless of whether it sits inside otherwise-Iosevka chrome. Column alignment is non-negotiable; the working font cannot reliably provide it.
72 + **Tabular numerics: the terminal font, right-aligned.** Numeric columns align for free in a fixed-width terminal font; right-align them and pad with blank cells. No special display font is needed or available.
85 73
86 74 ## Iconography
87 75
88 - Two tiers with non-overlapping roles. Functional icons are Nerd Font glyphs from the typography stack, used inline at text size. Hero icons are isometric line illustrations drawn from a single thematic world (a mid-century electromechanical relay station). Full rules, subject map, and style guide live in [ICONOGRAPHY.md](ICONOGRAPHY.md).
76 + Two tiers with non-overlapping roles. Functional icons are Nerd Font glyphs from the terminal font, used inline at cell size, and are the primary tier for TUIs. Hero illustrations survive only on the few graphical surfaces that remain (splash, swaylock background). Full rules in [ICONOGRAPHY.md](ICONOGRAPHY.md).
89 77
90 78 ## Scope
91 79
92 - These rules are binding for egui apps Alloy authors and for the ratatui helpers. Adopted apps (cosmic-files, ghostty, nvim, etc.) are themed toward the ramp where their theming system permits, and accepted as-is where it does not. Heterogeneity at the seam between authored and adopted is a known trade, called out in [MANIFESTO.md](MANIFESTO.md#what-we-author-vs-what-we-adopt).
80 + Binding for the ratatui surfaces Alloy authors (`alloy_tui`, the console). Adopted apps (sway chrome, swaylock, mako, cosmic-files, helix, yazi, bottom) are themed toward the ramp where their theming permits, and accepted as-is where it does not. Heterogeneity at the seam between authored and adopted is a known trade, called out in [MANIFESTO.md](MANIFESTO.md#what-we-author-vs-what-we-adopt).
@@ -12,37 +12,39 @@
12 12
13 13 ## Alloy's stance on the 2-in-1 form factor
14 14
15 - **Alloy stays keyboard-first. Folding the screen back is a single-purpose gesture: it opens a stylus notes canvas. That's the entire tablet-mode feature.**
15 + **Alloy stays keyboard-first, and the FW12 is supported as a laptop.** The fold-to-notes tablet gesture (a stylus notes canvas on hinge fold) was **shelved in the 2026-07-17 pivot**: it needs a stylus GUI (Rnote), which clashes with the TUI-first direction. The "Fold-to-notes" section below is kept as a record and revives only if a tablet UX returns.
16 16
17 - The manifesto commits Alloy to a keyboard-driven tiling environment. A 2-in-1 does not change that thesis. What it changes is the meaning of the fold-back hinge gesture:
17 + The manifesto commits Alloy to a keyboard-driven tiling environment. A 2-in-1 does not change that thesis. In clamshell use:
18 18
19 - - **Clamshell mode is the entire Alloy experience.** Niri, Ironbar, anyrun, the full stack, unchanged.
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. Niri and libinput already do this without Alloy code.
21 - - **Folding the screen back switches to a dedicated notes workspace** with a stylus notes app in fullscreen. Unfolding switches back to the previous workspace. No other tablet-mode behavior — no launcher shell, no touch-first UI, no OSK, no rotation (the notes app has infinite canvas; portrait is not needed).
19 + - **Clamshell mode is the entire Alloy experience.** Sway and the full terminal-first stack, unchanged.
20 + - **Touch and stylus in clamshell mode are pointer input, nothing more.** Tap to focus a window, drag to scroll, stylus behaves as a pointer. Sway and libinput already do this without Alloy code.
21 + - **Folding the screen back does nothing special** now that the notes flow is shelved; the session stays as-is.
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. The fold-to-notes gesture gives the 2-in-1 hardware a single, clear, high-value purpose without introducing a second UI shell.
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.
24 24
25 25 ## Display
26 26
27 27 - **Native panel:** 12.2" 1920x1200 (16:10), matte, IPS-class, ~185 PPI. Sits in the awkward middle where 1.0x is too small and 2.0x is too big.
28 - - **Alloy default scale:** 1.25x fractional under Niri (`output "eDP-1" { scale 1.25 }` in `niri.kdl`). Niri's fractional-scale support handles the compositor side; egui apps need to honor `wp-fractional-scale-v1` (already handled by upstream egui/winit — verify at v0 sanity check).
28 + - **Alloy default scale:** 1.25x fractional under sway (`output eDP-1 scale 1.25` in the sway config's per-machine drop-in). sway handles the compositor side; terminal apps scale with the compositor, so there is no per-app fractional-scale work as there would have been for egui. Verify glyph crispness at v0 sanity check.
29 29 - **Contrast/color:** validate the DESIGN-LANGUAGE.md dark-mode L stops on this panel specifically (currently deferred in `todo.md`). Matte 400-nit IPS is roughly the panel class most Alloy users are likely to have; if the tokens miss here, they miss for the base audience.
30 30
31 31 Rejected: 1.0x default (glyphs too small at arm's length on 12.2"), 1.5x default (wastes usable columns in a scrollable-tiling model where horizontal density matters).
32 32
33 33 ## Touch and stylus as pointer
34 34
35 - Touch and stylus both work out of the box on Wayland via libinput. Niri routes them to clients as pointer events. No Alloy code required for the base case.
35 + Touch and stylus both work out of the box on Wayland via libinput. Sway routes them to clients as pointer events. No Alloy code required for the base case.
36 36
37 - - **Touch:** tap to focus, drag to scroll. Niri's built-in touch gestures cover column scrolling and workspace switching.
37 + - **Touch:** tap to focus, drag to scroll. Sway's built-in touch handling covers workspace-switch gestures.
38 38 - **Stylus:** USI 2.0 supported by the FW12 panel. Routes through libinput's tablet-tool path as pointer input. Pressure and tilt are available to any client that asks; Alloy's own widgets do not consume them.
39 39 - **Palm rejection:** handled by libinput. No Alloy work.
40 40
41 - `alloy_ui` does not ship a Touch/Pointer input-mode split. Widgets render the same way regardless of input source; hit targets follow the design system's existing sizing tokens. If touch ergonomics become a real problem in practice, revisit — but not preemptively.
41 + `alloy_tui` does not ship a Touch/Pointer input-mode split; a TUI is keyboard-first by nature and terminal apps take pointer input only incidentally. If touch ergonomics ever matter, revisit, but not preemptively.
42 42
43 - ## Fold-to-notes
43 + ## Fold-to-notes (SHELVED)
44 44
45 - The kernel exposes hinge state via `SW_TABLET_MODE` on Framework's ACPI/EC path. Alloy consumes it for one purpose: switching to a dedicated notes workspace on fold, and back on unfold.
45 + **Shelved in the 2026-07-17 pivot** (TUI-first clashes with a stylus GUI). Kept as a record; the `alloy-hinged` daemon, the notes workspace, and the Rnote pick below are not on the roadmap and revive only if a tablet UX returns. The niri IPC calls below are historical.
46 +
47 + The kernel exposes hinge state via `SW_TABLET_MODE` on Framework's ACPI/EC path. The shelved design consumed it for one purpose: switching to a dedicated notes workspace on fold, and back on unfold.
46 48
47 49 **Behavior:**
48 50
@@ -67,7 +69,7 @@
67 69 Framework 12 ships a fingerprint reader on the power button. Upstream Linux support is via `libfprint`; enrollment through `fprintd`. Fedora Silverblue includes the stack.
68 70
69 71 - Alloy configures PAM to allow fingerprint at sudo and at the lockscreen.
70 - - The v1 egui lockscreen must handle the fprintd path (not just password). Tracked here rather than in the lockscreen crate README so the FW12 target is the forcing function.
72 + - Fingerprint unlock wires through swaylock's PAM stack (the authored egui lockscreen was dropped in the pivot). No Alloy code beyond PAM config.
71 73
72 74 ## Firmware (fwupd / LVFS)
73 75
@@ -78,7 +80,7 @@
78 80 ## Power management
79 81
80 82 - **Suspend model:** s2idle (Modern Standby / S0ix) only. S3 is not available on the platform. Idle drain during s2idle has historically been a Linux pain point on Intel; Fedora's `power-profiles-daemon` + tuned defaults are the current best baseline. Track s2idle drain as an open concern; do not layer TLP on top of ppd.
81 - - **Profiles:** default to `balanced`. Expose profile switching in the (eventual) Alloy status bar; do not build a dedicated power tool.
83 + - **Profiles:** default to `balanced`. Expose profile switching via the `alloy` console (an `alloy power` verb, or within `alloy display`), not a dedicated power tool.
82 84 - **Lid close:** suspend, standard.
83 85
84 86 ## Wi-Fi, Bluetooth, webcam, audio
@@ -96,8 +98,7 @@
96 98
97 99 ## Open questions
98 100
99 - - [ ] Does the FW12 kernel auto-suppress the physical keyboard and trackpad on `SW_TABLET_MODE`, or does `alloy-hinged` need to do it via libinput toggle?
100 - - [ ] Rnote fullscreen behavior on a Niri workspace — does it cooperate cleanly with `focus-workspace previous`, or does it need any config for kiosk-like behavior?
101 - - [ ] fprintd + Alloy egui lockscreen: what does the async challenge/response loop look like in an immediate-mode UI? Prototype required before v1 lockscreen is called done.
102 - - [ ] Fractional-scale validation on real egui apps (audiofiles is the closest existing test) at 1.25x on this panel — do glyph metrics survive?
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?
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 + - [ ] (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.
@@ -1,84 +1,58 @@
1 1 # Alloy Iconography
2 2
3 - Two icon tiers with non-overlapping roles. Functional icons identify; hero illustrations anchor.
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
5 + ## Functional tier — Nerd Font glyphs (primary)
6 6
7 - For small, identifying icons in chrome: toolbar buttons, list-item markers, status indicators in the bar, file-type glyphs in a list, error/warn/info glyphs paired with accent text.
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
9 - - **Source:** the nerd-patched glyph range already present in `IosevkaNerdFont` (working font) and `DepartureMonoNerdFontMono` (display font). No separate icon font.
10 - - **Rendering:** treated as text. Inherits the working-font weight and the surrounding text's ramp position. Never accent-colored unless paired with information (e.g. an error glyph beside an error message uses `accent-error`).
11 - - **Size:** the surrounding text size. No standalone sizing scale.
9 + - **Source:** the nerd-patched glyph range in the terminal font (`IosevkaTermNerdFont`). No separate icon font.
10 + - **Rendering:** treated as text, a cell wide. Inherits the surrounding text's ramp position and weight. Never accent-colored unless paired with information (an error glyph beside an error message uses `accent-error`).
11 + - **Size:** the cell. There is no standalone sizing scale in a terminal.
12 12
13 - This tier exists so the brand doesn't have to draw 400 icons. It is uncomplicated and not where the brand lives.
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
15 + ## Hero tier — isometric line illustrations (reserved, mostly deferred)
16 16
17 - For large, declarative iconography: lockscreen ornament, launcher app icons, settings category headers, marquee-app heros, empty states, splash. This is where Alloy's visual identity lives.
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 +
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 + - **Brand assets** outside the running system (repo social card, README).
22 +
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.
18 24
19 25 ### Theme: a mid-century electromechanical relay station
20 26
21 - The illustrations are all subjects from a single fictional facility — a small Bell System / GE-era relay station — rendered as if they're plates from its service manual. Anyone looking at two illustrations should recognize they belong to the same body of work without having to think about it.
27 + The illustrations are subjects from a single fictional facility, a small Bell System / GE-era relay station, rendered as if they are plates from its service manual. The metaphor is unforced: a computer is in spirit an electromechanical relay system, and a tiling shell is in spirit a control room. Two illustrations should read as the same body of work without thought.
22 28
23 - The reason this is the right metaphor: a computer is in spirit an electromechanical relay system, and a tiling user shell is in spirit a control room. The mapping is unforced.
29 + The theme also survives at cell scale as a motif: ASCII/box-drawing framing, placard-style `UPPERCASE` labels, and annotation-callout phrasing in the console can carry the service-manual register without a single rendered illustration.
24 30
25 - ### Subject map
31 + ### Style rules (for the surviving image surfaces)
26 32
27 - Every Alloy surface that gets a hero illustration draws its subject from this facility.
28 -
29 - | Alloy surface | Facility subject |
30 - |---|---|
31 - | Lockscreen ornament | Master switch / sealed bulkhead / front gate |
32 - | Package GUI hero | Parts bay — crated equipment on a pallet |
33 - | Notification daemon | Annunciator panel (the labeled-lamp board) |
34 - | Settings → Network | Microwave relay tower |
35 - | Settings → Display | Oscilloscope / CRT bay |
36 - | Settings → Sound | Audio bay / signal generator rack |
37 - | Settings → Power | Generator + transformer + breaker panel |
38 - | Settings → Updates | Maintenance bench / parts cabinet |
39 - | Settings → Storage | Filing cabinet / archive room |
40 - | Settings → Privacy | Locked equipment cage |
41 - | Empty state | "Bay empty" labeled console with no equipment in it |
42 - | Splash / first boot | Establishing axonometric of the whole facility |
43 - | Marquee app icons | One distinct piece of facility equipment per app |
44 -
45 - When a new surface needs a hero illustration, pick a subject from the facility. Do not extend the world to a different setting; coherence is the point.
46 -
47 - ### Style rules
48 -
49 - - **Projection:** true isometric, 30° / 30° / 30°. No perspective. No exceptions.
50 - - **Stroke:** 1.5px line, drawn at `border-strong` on `surface-raised`. Slightly heavier than the 1px chrome borders so the illustration reads as *drawn* rather than as UI.
33 + - **Projection:** true isometric, 30° / 30° / 30°. No perspective.
34 + - **Stroke:** 1.5px line at `border-strong` on `surface-raised`. Heavier than chrome borders so it reads as *drawn*.
51 35 - **Fills:** none. Pure line.
52 - - **Hatching is mandatory.** This is the single detail that separates engineering-manual iso from generic flat illustration.
53 - - Cross-hatch for metal (regular grid, 45°).
54 - - Parallel hatch for concrete or stone (single direction, evenly spaced).
55 - - Stippling for fill/ground/sand (dots, irregularly spaced).
56 - - **Doubled edges on solid objects.** An engineering-drawing convention for indicating mass — the visible silhouette edges are drawn twice with a small offset.
57 - - **Annotation callouts** on hero illustrations only (splash, marquee app heroes). Small letter or number labels with leader lines pointing to components. Settings category icons at 64px do not get callouts (would be too busy).
58 - - **Cropping is intentional.** Illustrations can run off the edge of their container as if they're a fragment of a larger plate. Reinforces the "this is one page from a manual" feeling.
59 - - **No color.** Even when illustrating something that would be colored in reality (a red warning light on the annunciator panel, copper busbars), the illustration stays on the ramp. Color is reserved for information per [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#core-principle-tinted-greyscale-chrome-color-as-information).
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 + - **Doubled edges on solid objects** for mass.
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."
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).
60 41
61 - ### Sizes
42 + ### Surviving subject map
62 43
63 - | Use | Size | Callouts |
64 - |---|---|---|
65 - | Settings category icon | 64px | no |
66 - | Marquee app icon (launcher / dock) | 96px | no |
67 - | Lockscreen ornament | 128–192px | optional |
68 - | Empty state | 200–280px | optional |
69 - | Marquee app hero (in header) | 240–320px | yes |
70 - | Splash / first boot | full surface | yes |
44 + | Surviving surface | Facility subject |
45 + |---|---|
46 + | swaylock background | Master switch / sealed bulkhead / front gate |
47 + | First-boot splash | Establishing axonometric of the whole facility |
48 + | Repo / brand assets | One distinct piece of facility equipment |
49 +
50 + (The former package-GUI, notification, and per-settings-category heroes are retired with those GUI surfaces.)
71 51
72 52 ### Production
73 53
74 - For v0 mockup: rough placeholders drawn inline as SVG. These exist to demonstrate the style rules; they are not the real illustrations.
75 -
76 - For v1: 8–12 real illustrations covering the subjects above, drawn either by Max or commissioned from an illustrator who can match the register. Stock axonometric libraries (Streamline, Iconscout) will dilute the brand and are not acceptable — the whole point of the theme is that the illustrations feel *authored* for one specific system.
77 -
78 - Vendored under `alloy_ui/assets/icons/hero/` once the crate is scaffolded. SVG format, ramp colors as `currentColor` so light/dark mode polarity flips work without re-export.
54 + For any surviving surface: SVG, ramp colors as `currentColor` so light/dark polarity flips work without re-export. Vendored under `alloy_tui/assets/` (splash) or the repo root (brand). Stock axonometric libraries dilute the brand and are not acceptable; the point of the theme is that the plates feel authored for one specific system. Drawn by Max or commissioned to match the register.
79 55
80 56 ## The boundary between tiers
81 57
82 - If you're tempted to make a functional icon into a small isometric illustration, don't. A 16px iso line drawing reads as noise, not as iconography. Functional icons stay as Nerd Font glyphs at text size. Isometric is reserved for surfaces large enough that the hatching and line work can be read at viewing distance — 64px floor.
83 -
84 - Conversely, if you're tempted to use a Nerd Font glyph for a hero spot to save illustration work, don't. The brand identity is the hero tier; cutting it makes Alloy look like every other minimalist Linux shell.
58 + If tempted to make a functional icon into a tiny isometric drawing, don't: a cell-scale iso line reads as noise. Functional icons stay Nerd Font glyphs at cell size. Isometric is reserved for image surfaces large enough that the hatching reads at viewing distance. In a TUI-first Alloy, that means the functional tier does almost all the work, and that is correct.
M docs/IMAGE.md +1 -1
@@ -90,7 +90,7 @@
90 90
91 91 ## Open questions
92 92
93 - - [ ] Verify which Alloy packages are in Fedora main vs. need COPRs. Candidates that may need COPRs: `anyrun`, `satty`, `wl-screenrec` (depending on Fedora version). Audit at v0 packaging time.
93 + - [ ] Verify which Alloy packages are in Fedora main vs. need COPRs. Candidates that may need COPRs: `satty`, `wl-screenrec` (depending on Fedora version). Audit at v0 packaging time.
94 94 - [ ] `bootc-image-builder` for ISO generation. First-time-user path is `bootc install` from a live environment; the ISO is what makes that a smooth experience. Verify the ISO builder handles Alloy's specific package set.
95 95 - [ ] Single-stage vs. multi-stage Containerfile. Single-stage until rebuild time hurts.
96 96 - [x] sr.ht account: **`~maxmj`** for v0. LLC-owned `~makecreative` remains the long-term target; migration if/when the project grows.
M docs/MANIFESTO.md +17 -14
@@ -1,22 +1,24 @@
1 1 # Project Alloy
2 2
3 - *An opinionated tiling user layer for Fedora Silverblue.*
3 + *An opinionated, TUI-first tiling user layer for Fedora Silverblue.*
4 4
5 5 ## What Alloy is
6 6
7 - Project Alloy is to Silverblue what Omarchy is to Arch: a curated, beautifully-defaulted, keyboard-driven, tiling-first environment that fuses an immutable base, a Wayland compositor, an opinionated app/tool stack, and a project-authored design system into one coherent system. The name is the thesis — an alloy is stronger than its component metals.
7 + Project Alloy is to Silverblue what Omarchy is to Arch: a curated, beautifully-defaulted, keyboard-driven, tiling-first environment that fuses an immutable base, a Wayland compositor, an opinionated terminal-first stack, and a project-authored design system into one coherent system. The name is the thesis: an alloy is stronger than its component metals.
8 +
9 + Alloy is terminal-native. The one thing it authors is a ratatui design system and the `alloy` console built on it. Everything graphical it needs, it adopts.
8 10
9 11 ## What Alloy is not
10 12
11 13 - Not a from-scratch package manager. The previous `sap` design (typed configuration language, federated curator registry, content-addressed outputs) is shelved; we live on rpm-ostree.
12 14 - Not a from-scratch distro. The previous `mountaineer` design (curated Alpine, apk-on-`/etc/apk/world`, s6 init) is shelved; we live on Fedora Silverblue.
13 - - Not a ublue spin. Alloy is **alongside** Universal Blue (Bluefin, Bazzite, Aurora), not downstream of it — the name was picked specifically to avoid the ublue "Blue\*" prefix convention.
15 + - Not a ublue spin. Alloy is **alongside** Universal Blue (Bluefin, Bazzite, Aurora), not downstream of it; the name was picked specifically to avoid the ublue "Blue\*" prefix convention.
14 16 - Not a gaming distro. The audience overlaps with Omarchy and Omakub, not with Bazzite.
15 - - Not a GNOME spin. Alloy's design system is custom-drawn (egui) and does not pick up libadwaita theming.
17 + - Not a GNOME spin. Alloy's authored design system is custom-drawn (ratatui) and does not pick up libadwaita theming.
16 18
17 19 ## Audience
18 20
19 - Developers, keyboard-driven power users, Linux-native sensibility. People who would rather configure once than configure often, who prefer modern Rust tools over decades-old legacy ones, and who want the tiling-first immutable experience that no existing Fedora atomic variant currently delivers as a curated whole.
21 + Developers, keyboard-driven power users, Linux-native sensibility. People who would rather configure once than configure often, who prefer modern Rust tools over decades-old legacy ones, who live in the terminal, and who want the tiling-first immutable experience that no existing Fedora atomic variant currently delivers as a curated whole.
20 22
21 23 The empty quadrant Alloy fills:
22 24
@@ -33,29 +35,30 @@
33 35
34 36 1. **Modern, well-featured, Rust-written tools.** When competitive Rust-native options exist, pick them. Where they don't, name the gap honestly and consider filling it.
35 37
36 - 2. **Native to Alloy's design system, not native to GNOME.** egui is custom-drawn. We commit to maintaining an egui design-system crate — themed widgets and extension traits on `egui::Ui` — that Alloy's authored apps pull from. Heterogeneity with adopted apps (e.g. cosmic-files in libcosmic, Gram in gpui) is a knowing trade, not an accident.
38 + 2. **Native to Alloy's design system, not native to GNOME.** Alloy authors in ratatui and maintains a design-system crate, `alloy_tui`: a palette, themed widgets, footer chrome, and reserved keys that the authored console and any future authored TUI pull from. Heterogeneity with adopted apps (sway's own chrome, swaylock, mako) is a knowing trade, not an accident.
37 39
38 - 3. **Non-reactive by principle.** Alloy avoids the reactive UI pattern in everything it authors. State → view binding, declarative property graphs, async data trickling into UI components — the cluster of patterns that produces staged appearance, animation creep, hover-everywhere, and state-divergence bugs — is the "web-shaped feel" Alloy refuses on principle. egui (immediate-mode) and ratatui (immediate-mode TUI) are the two toolkits Alloy authors in. Both render the whole frame from current state each cycle, making state divergence architecturally impossible.
40 + 3. **Non-reactive by principle.** Alloy avoids the reactive UI pattern in everything it authors. State-to-view binding, declarative property graphs, async data trickling into UI components, the cluster of patterns that produces staged appearance, animation creep, hover-everywhere, and state-divergence bugs, is the "web-shaped feel" Alloy refuses on principle. ratatui is immediate-mode: it renders the whole frame from current state each cycle, making state divergence architecturally impossible. (egui, immediate-mode GUI, shares this property; Alloy no longer authors GUI apps, but the principle is the same one and would apply if it ever did.)
39 41
40 42 4. **Backwards compatible with hardware, not with software.** Same principle as `_meta`'s cross-cutting rule. Operators are expected to be on the current Alloy release; hardware support is broad.
41 43
42 44 5. **Opinionated defaults; the configuration is the documentation.** Every default is a position we will defend in writing.
43 45
44 - 6. **The package GUI teaches the mental model.** The wedge product is a user-friendly graphical package manager that helps new Silverblue users understand the three-surface install model (rpm-ostree layered / Flatpak / distrobox). This is the hardest thing about Silverblue and the thing GNOME Software does not address well.
46 + 6. **The console teaches the mental model.** The wedge is the `alloy` console: a terminal-native front door to the system, including a package view that makes Silverblue's three-surface install model (rpm-ostree layered / Flatpak / distrobox) legible. That model is the hardest thing about Silverblue and the thing GNOME Software does not address well.
45 47
46 48 ## What we author vs what we adopt
47 49
48 50 **Alloy authors** (in Rust):
49 - - The package-management GUI (egui).
50 - - Sysop-style helper TUIs (ratatui) — wifi, services, image swaps, generation rollback. The mountaineer-sysop pattern carries over.
51 - - Marquee egui apps that anchor the design system — see [MARQUEE-APPS.md](MARQUEE-APPS.md).
51 + - `alloy_tui`, the ratatui design-system crate (palette, themed widgets, footer chrome, mock-or-real backend detection). It is the design system.
52 + - The `alloy` console (ratatui): info, services, storage, wifi/net, image swaps, generation rollback, a package view, and `alloy config` schema-driven config editing. The mountaineer-sysop pattern carries over directly; see [CONSOLE.md](CONSOLE.md).
52 53
53 54 **Alloy curates** (from upstream):
54 55 - Fedora Silverblue as the base.
55 - - Niri as the compositor.
56 - - The userland stack — see [STACK.md](STACK.md).
57 - - Selected COSMIC components — see [COSMIC.md](COSMIC.md).
56 + - Sway as the compositor.
57 + - swaylock (lockscreen), mako (notifications), swaybar (bar): the graphical pieces a TUI cannot serve, adopted rather than authored.
58 + - The userland stack; see [STACK.md](STACK.md).
58 59
59 60 ## Status
60 61
61 62 Pre-v0. This document is the project. Code follows.
63 +
64 + Pivoted 2026-07-17 from an egui-authored, Niri scrolling-tiler design to this TUI-first, Sway shape. The scrolling-tiler model and the egui marquee-app wedge (lockscreen, package GUI, notification daemon, egui bar) were dropped; the immediate-mode thesis carried over unchanged because ratatui is immediate-mode too.
@@ -1,24 +1,21 @@
1 - # Marquee egui app pipeline
1 + # Marquee apps — retired
2 2
3 - The user-facing apps Alloy authors in egui to anchor the design system. Each is small, high-visibility, and fills a gap where adopting an existing Rust app isn't the right answer.
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
5 - | Tier | App | Why it qualifies |
6 - |---|---|---|
7 - | v1 (#1, alloy_ui shakedown) | **Lockscreen** | Small, high-visibility (every wake), security-sensitive (simplicity helps), ext-session-lock-v1 is a clean protocol, no production-quality Rust lockscreen exists today. First marquee app: smallest contained Wayland integration, exercises both fonts (Iosevka for the password field, Departure Mono for clock and date) in their canonical roles. |
8 - | v1 (#2, design-system reusability proof) | **Package GUI** | The pedagogical wedge product. Teaches the rpm-ostree layered / Flatpak / distrobox decision. This is the hardest thing on Silverblue and the original first-tool framing. Starts as soon as `alloy_ui` is shaken down by the lockscreen; ships into v0 stack alongside it. |
9 - | v1 candidate | **Wallpaper picker** | Browse the curated impressionist collection ([STACK.md](STACK.md#wallpapers)) with painter / title / year / source metadata, preview, set-as-current. Small scope, distinct surface from the lockscreen, gives the collection a first-class home rather than treating it as filesystem chrome. Bumps to v1 if scoped tight. |
10 - | v1 candidate | **TextEdit-equivalent** | Small native-feeling graphical text editor for ordinary users (audience: the same people the package GUI serves — opened a config file from the file manager). Intentionally scoped down: open/edit/save, multi-tab, find/replace, tree-sitter highlighting, real OS file dialogs. No project tree, no LSP, no debugger, no plugin system. Power users install Helix. ~2–3 month scope. Pairs with the non-reactive principle as a concrete proof point. |
11 - | v2 | **Notification daemon** | High-visibility (every alert), bounded scope, real Rust ecosystem gap. More scope than the lockscreen — action invocation, persistent history, urgency, replace-id semantics, DND — but well-defined protocol (`org.freedesktop.Notifications` D-Bus + wlr-layer-shell). |
12 - | v3+ | **Bar** | Larger, less protocol-clean, but a major visual anchor. Ironbar gets us there in the interim — don't let its config become load-bearing. |
13 - | Later | **File manager** | Large surface area; cosmic-files already exists in Rust + iced. Not in the marquee tier unless cosmic-files becomes unacceptable. |
14 - | Never | **Terminal, launcher, screenshot** | Existing Rust options (or compositor built-ins) are the right shape. Re-authoring would be wrong-scope. |
5 + The marquee pipeline was a set of user-facing apps Alloy would author in egui to anchor the design system (a lockscreen, a package GUI, a wallpaper picker, a small text editor, a notification daemon, a bar). With egui dropped, each is either adopted from upstream or folded into the console.
15 6
16 - ## The design-system commitment
7 + ## What each intended app became
17 8
18 - Alloy authors an egui design-system crate — themed widgets, extension traits on `egui::Ui`, palette and typography constants — that all marquee apps pull from. The lockscreen is the first concrete consumer. Adding the package GUI as the second consumer is what proves the crate is reusable (rather than a one-app abstraction).
9 + | Intended egui marquee app | Now |
10 + |---|---|
11 + | Lockscreen | **Adopted: swaylock** (a Wayland lock surface is graphical; adopt, don't author). See [STACK.md](STACK.md#lock). |
12 + | Package GUI | **A TUI view in the `alloy` console** (`alloy update` fronts rpm-ostree; the three-surface install model is taught there). See [CONSOLE.md](CONSOLE.md). |
13 + | Wallpaper picker | **A future `alloy` console verb** over swww; no authored GUI. See [STACK.md](STACK.md#wallpapers). |
14 + | TextEdit-equivalent | **Dropped:** the editor is a TUI (helix, then deox). |
15 + | Notification daemon | **Adopted: mako.** See [STACK.md](STACK.md#notification-daemon). |
16 + | Bar | **Adopted: swaybar** (sway built-in). See [STACK.md](STACK.md#bar). |
17 + | File manager | **Adopted: cosmic-files** (GUI fallback to yazi). See [COSMIC.md](COSMIC.md). |
19 18
20 - Decisions like color palette, typography, motion language (or its absence — see the non-reactive principle), iconography style, and density should be made before the second app starts, even if they're refined later.
19 + ## What carried over
21 20
22 - ## The immediate-mode discipline
23 -
24 - Every marquee app renders its full UI each frame from current state — no retained widget state, no async updates trickling into components. This is what makes the "stable feel" property hold across the suite. See [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md) for the affordance vocabulary and [MANIFESTO.md](MANIFESTO.md) principle 3 for the architectural rationale.
21 + The design-system commitment survived the pivot intact: Alloy authors one design-system crate that everything authored pulls from. It is now `alloy_tui` (ratatui) instead of `alloy_ui` (egui), and its first and central consumer is the `alloy` console rather than a lockscreen. The immediate-mode discipline is unchanged, because ratatui is immediate-mode too. See [MANIFESTO.md](MANIFESTO.md) principle 3 and [COMPONENT-LIBRARY.md](COMPONENT-LIBRARY.md).
@@ -2,6 +2,8 @@
2 2
3 3 *A late-term, research-shaped project. No commitment, no timeline. Captured here so the architectural decisions Alloy makes now don't accidentally close the door on it.*
4 4
5 + *Pivot note (2026-07-17): Alloy now authors ratatui, not egui. The question and its architecture are unchanged (both are immediate-mode, function-over-state), but the tool would target terminal compositions rather than GPU-drawn ones. egui appears below as the original immediate-mode example; read it as "immediate-mode UI, egui or ratatui."*
6 +
5 7 ## The question
6 8
7 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.
@@ -51,7 +53,7 @@
51 53
52 54 ### Composition grammar, not pixel layout
53 55
54 - When the designer composes a component, they're not positioning pixels — they're choosing layout primitives that map 1:1 to egui's call vocabulary: `horizontal(...)`, `vertical(...)`, `card(...)`, `field_row(...)`. 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:
55 57
56 58 ```
57 59 PackageRow = card(elevation = raised) {
@@ -72,11 +74,11 @@
72 74
73 75 ### Live render against sample data
74 76
75 - The tool embeds an actual egui render context. The designer's preview is not a static mockup; it is the same code path the production app uses, fed sample data the designer authors alongside the component. "What does this look like with the longest possible name?" becomes a typed-in test case, not an unsketched edge case.
77 + The tool embeds an actual render context (a ratatui terminal backend for the TUI target). The designer's preview is not a static mockup; it is the same code path the production app uses, fed sample data the designer authors alongside the component. "What does this look like with the longest possible name?" becomes a typed-in test case, not an unsketched edge case.
76 78
77 79 ### Engineer-facing output
78 80
79 - The artifacts produced are: `tokens.toml` (tokens), `<Component>.alloy.ron` files (compositions), and a small registry of which components exist. The engineer wires these into the marquee apps. There is no pixel-perfect mockup to translate; the composition file *is* the spec.
81 + The artifacts produced are: `tokens.toml` (tokens), `<Component>.alloy.ron` files (compositions), and a small registry of which components exist. The engineer wires these into the authored TUIs (the console). There is no pixel-perfect mockup to translate; the composition file *is* the spec.
80 82
81 83 ## Open research questions
82 84
@@ -85,7 +87,7 @@
85 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.
86 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?
87 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)?
88 - 4. **How is custom drawing handled?** A custom waveform widget for audio cannot be expressed in any composition vocabulary. The tool should explicitly cede those — they're engineer-authored egui 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.
89 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.
90 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.
91 93
M docs/SHELL.md +1 -1
@@ -6,7 +6,7 @@
6 6
7 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 - The pick is coherent with the rest of Alloy: `alloy console` treats configs as typed data, `alloy_ui` treats widgets as functions over state, and now the shell treats commands as functions over typed pipelines. Same principle at three layers.
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
11 11 ## Architecture: nu is login-only
12 12
M docs/STACK.md +38 -65
@@ -12,70 +12,47 @@
12 12
13 13 ## Compositor
14 14
15 - **Niri.** Modern, well-featured, Rust, KDL config, in Fedora repos. Scrollable-column tiling carries part of the "opinionated defaults" weight upstream — Niri's own opinion about windowing means Alloy doesn't have to invent a tiling model.
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.
16 16
17 - Rejected: Hyprland (governance is contested), Sway (Sericea already ships it — lowest-risk but a smaller idea than Niri-plus-taste).
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
19 - ## GUI toolkit
19 + ## Authored toolkit
20 20
21 - **egui.** Immediate-mode Rust GUI: every frame the UI code runs top to bottom from current state, no bindings, no observables, no retained UI state to diverge. Picked because **Alloy's principle is to avoid the reactive pattern** — staged appearance, async UI trickling, state-divergence bugs, the "web-shaped feel" — and immediate-mode rules those out by construction. egui is essentially TUIs-but-graphical, which is the stability property the project is reaching for.
21 + **ratatui.** Alloy authors one thing, the `alloy` console and its `alloy_tui` design-system crate, and it authors in ratatui. Immediate-mode: every frame the UI code runs top to bottom from current state, no bindings, no observables, no retained UI state to diverge. Picked because **Alloy's principle is to avoid the reactive pattern** (staged appearance, async UI trickling, state-divergence bugs, the "web-shaped feel"), and immediate-mode rules those out by construction. The mountaineer-sysop `sysop-tui` pattern carries over directly as the seed of `alloy_tui`: palette, themed widgets, footer chrome, reserved keys, mock-or-real backend detection. See [CONSOLE.md](CONSOLE.md).
22 22
23 - Practical model: a Rust function takes `&mut egui::Ui`, draws everything from current state each frame. The UI literally cannot show stale data because there is nothing retained to be stale. State divergence is architecturally impossible.
23 + Conceptual coherence is a bonus: bottom, tuigreet, and the console all read as one ratatui family.
24 24
25 - Tradeoffs accepted:
26 - - **Designer-authorable DSL is gone.** The original Slint pitch was that designers could author UI without being Rust devs; that capability is forfeit. Alloy's design-system primitives live in a Rust crate as themed widgets and extension traits on `egui::Ui`.
27 - - **Aesthetic identity has to be hand-built.** egui defaults read as "debug overlay"; the Alloy look requires deliberate theming work in Rust against the patterns in [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md).
28 - - **Constant redraw cost.** egui throttles when nothing changes but the model is still per-frame; CPU/GPU usage is higher than retained toolkits on idle.
29 -
30 - Tradeoffs gained:
31 - - **License flexibility.** egui is MIT/Apache. Alloy is no longer forced into GPLv3 by its GUI toolkit — license choice is re-opened (see [todo.md](todo.md)).
32 - - **Conceptual coherence with ratatui.** Both are functions-over-state immediate-mode paradigms. Authoring across GUI and TUI becomes more uniform.
33 - - **Prior experience.** Audiofiles already runs on egui, so the project enters with working knowledge.
34 -
35 - Rejected: Slint (reactive at its core — `Text { text: my_property; }` is exactly the reactive pattern; the toolkit cannot satisfy the non-reactive principle even when used carefully), iced (reactive), gpui (reactive — what Zed/Gram use), Floem (reactive), Dioxus (React-shaped reactive), gtk4-rs (imperative retained-mode is non-reactive but couples to GNOME visual identity Alloy rejected), Dear ImGui via bindings (immediate-mode but C++ underneath), Tauri (JS frontend conflicts with Rust-first).
36 -
37 - ## TUI toolkit
38 -
39 - **ratatui.** For sysop-style helpers (wifi, services, image swaps, generation rollback). The mountaineer-sysop pattern carries over.
25 + **No authored GUI.** The 2026-07-17 pivot dropped egui and the marquee-app plan (lockscreen, package GUI, notification daemon, egui bar). The graphical pieces a TUI cannot serve are adopted, not authored: swaylock, mako, swaybar. egui was the prior pick (also immediate-mode, MIT/Apache, and already used in audiofiles); the immediate-mode thesis is unchanged, only the render target moved from GPU surface to terminal cells. Rejected GUI toolkits, recorded in case authored GUI ever returns: Slint / iced / gpui / Floem / Dioxus (all reactive), gtk4-rs (couples to GNOME identity), Tauri (JS frontend conflicts with Rust-first).
40 26
41 27 ## Bar
42 28
43 - - **v0: Ironbar** (Rust, GTK-based, Polybar-inspired, TOML/JSON/Corn config). Rust-native, actively developed, good Niri compatibility.
44 - - **v1+: bespoke egui bar** as a possible later marquee design-system app. Don't let Ironbar config accrete to the point of becoming load-bearing — document migration intent.
29 + **swaybar** (sway's built-in bar). Configured in the sway config's `bar {}` block with a `status_command`; no extra package or daemon. The v0 scaffold ships a minimal clock placeholder; a real status line (or the future `alloy` console status view) is a shaping task, not a toolkit decision.
45 30
46 - Rejected: eww (Rust but Lisp/yuck config clashes with the Alloy design system), waybar (C++, not Rust), yambar (YAML, ruled out).
31 + Rejected: Ironbar (the prior pick, dropped with the GTK stack in the pivot), waybar (C++, and swaybar already covers the need), eww (Lisp/yuck config clashes with the stack), yambar (YAML, ruled out). Re-adopt a standalone bar only if swaybar's status protocol proves too limiting.
47 32
48 33 ## Lock
49 34
50 - - **v0: swaylock** (C, canonical, minimal, security-audited — ships fast).
51 - - **v1: bespoke egui lockscreen** as the first marquee design-system app. Small focused UI, security-sensitive (simplicity helps), maximum visibility (every wake), ext-session-lock-v1 is a clean protocol. Better first-egui-app than the package GUI because it's smaller and more visible.
52 -
53 - Rust gap noted honestly: no production-quality Rust lockscreen exists today; Alloy fills it with the v1 egui build rather than adopting one.
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.
54 36
55 37 ## Launcher
56 38
57 - **anyrun** (Rust, plugin-based with Lua + Rust plugins, GTK4-rendered).
39 + **None by default (terminal-driven).** anyrun (the prior GTK launcher) was dropped in the pivot. On a terminal-first system, launching is the shell: open apps from rio or from yazi. `Mod+D` is left unbound in the scaffold for a TUI launcher when one is chosen (a fuzzy picker run inside a terminal, e.g. via `rio -e`).
58 40
59 - No custom egui launcher planned — anyrun's plugin model already covers what would be built, and launcher complexity (fuzzy search, app indexing, modes, history) is too large to re-author without strong reason.
60 -
61 - Rejected: fuzzel (C, canonical but not Rust), tofi (C, minimalist).
41 + Rejected as graphical launchers: anyrun (GTK, dropped), fuzzel/tofi (C, and a graphical launcher is off-thesis now). Candidate TUI pickers to evaluate: a small nu/fzf app picker, or a dedicated `alloy` console verb.
62 42
63 43 ## Notification daemon
64 44
65 - - **v0/v1: mako** (C, by emersion, de facto Wayland notification daemon, lightweight).
66 - - **v2: bespoke egui notification daemon** as the third marquee design-system app (after lockscreen v1 and package GUI). Fills a real Rust ecosystem gap. High visibility (every alert), protocol is well-defined (`org.freedesktop.Notifications` D-Bus + wlr-layer-shell). More scope than the lockscreen — action invocation, persistent history, urgency, replace-id semantics, DND — but bounded.
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.
67 46
68 - Rejected: swaync (C++), fnott (C). cosmic-notifications is worth evaluating as a more-Rust-aligned v0/v1 alternative to mako if it runs cleanly under Niri.
47 + Rejected: swaync (C++), fnott (C), cosmic-notifications (drags libcosmic in; mako is lighter and already fits).
69 48
70 49 ## Screenshot stack
71 50
72 - - **Niri built-in** for capture + region select (bound in `niri.kdl`). Compositor-native selection is smoother than slurp-as-overlay (knows about windows/workspaces).
73 - - **satty** for annotation when needed (Rust, modern, replaces swappy).
51 + - **grim** for capture, **slurp** for region select. sway has no built-in screenshot (Niri did, which is why this stack changed in the pivot); grim+slurp is the canonical wlroots pairing. Bound in the sway config: Print (full), Shift/Ctrl+Print (region via slurp). Active-window grab wants jq to parse the tree, left as a documented optional.
52 + - **satty** for annotation when needed (Rust, modern, replaces swappy). Mod+Print annotates the most recent capture.
74 53 - **wl-clipboard** for clipboard plumbing.
75 54
76 - No custom egui work — screenshot is composable by design.
77 -
78 - Rejected: grim + slurp + swappy (classic C stack, dated annotator); wayshot + slurp + satty (Rust replacement stack but adds two tools Niri's built-in already covers). Re-adopt wayshot+slurp only if compositor-independence or non-IPC scripted capture becomes a real constraint.
55 + Rejected: swappy (dated annotator; satty replaces it), wayshot (grim is the more standard wlroots grabber).
79 56
80 57 ## File manager
81 58
@@ -88,15 +65,11 @@
88 65
89 66 ## Text editor
90 67
91 - **Gram** ([codeberg.org/GramEditor/gram](https://codeberg.org/GramEditor/gram)). A Zed fork that strips AI integration, telemetry, proprietary collaboration, and subscription nags. Rust, gpui rendering, GPLv3. Inherits Zed's batteries-included posture: DAP debugger, git integration, multi-language LSP, extension compatibility with Zed's ecosystem.
68 + **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 Alloy Flatwhite theme are covered under "system introspection" below.
92 69
93 - **Power-user editor: Helix.** Pre-installed and pre-configured for terminal users. Helix is a TUI editor with the immediate-mode stability properties Alloy values; serving terminal-first users with their preferred editor while offering Gram (or eventually a custom egui editor — see [MARQUEE-APPS.md](MARQUEE-APPS.md)) for graphical users is a deliberate two-audience answer.
70 + 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.
94 71
95 - Why Gram over Zed itself: the "no AI / no telemetry / no proprietary" stance matches Alloy's broader posture exactly. Why Gram over a terminal editor (helix / nvim / kakoune): Alloy ships a graphical editor by default; users who want terminal editors install them, but the default is GUI.
96 -
97 - Rejected: cosmic-edit (less mature than Gram, would still introduce libcosmic visuals — Gram's gpui at least carries Zed's design polish), Zed (telemetry / AI defaults conflict with Alloy posture), helix-only (terminal-only as default is the wrong choice for a graphical desktop).
98 -
99 - **Heterogeneity note:** Alloy now ships three distinct GUI visual systems — egui (authored marquee apps), iced/libcosmic (cosmic-files), gpui (Gram). Editors are conventionally their own visual world, so the additional trade is absorbable; called out in [MANIFESTO.md](MANIFESTO.md#what-we-author-vs-what-we-adopt).
72 + 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.
100 73
101 74 ## Wallpapers
102 75
@@ -111,9 +84,9 @@
111 84
112 85 Avoid Google Arts & Culture as a *source* (mixed rights); use it for discovery only. The wallpaper picker shows painter + title + year + source institution.
113 86
114 - **Daemon: swww** (Rust, daemon + CLI, IPC-driven, soft-fade transitions, per-output). Daemon + CLI split is the right architecture for the v1 egui wallpaper picker — curation/metadata/UI in the picker, surface management in the daemon, picker drives the daemon via IPC. Soft fade on cycle is the tonally correct transition for impressionist art.
87 + **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.
115 88
116 - Rejected: wpaperd (Rust, more structured cycling-as-daemon-concern — second pick if picker descopes), cosmic-bg (Rust, System76, Niri compatibility unverified — not worth the detour while swww exists), swaybg (C, no cycling/transitions — boring fallback only).
89 + 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).
117 90
118 91 Honest gap: none of these handle ICC color management; that's a Wayland compositor-level concern still being finalized.
119 92
@@ -126,7 +99,7 @@
126 99 Secondary alignments:
127 100 - **TOML config.** No scripting language dependency, consistent with Alloy's rejection of Lua/yuck/similar config surfaces elsewhere in the stack.
128 101 - **Design-forward posture.** Rio treats the terminal as a displayed surface, not just a fast text renderer. That matches Alloy's design-system stance.
129 - - **No multiplexing.** Tabs, splits, panes are Niri's job. A terminal that also tiles is duplicated budget.
102 + - **No multiplexing.** Tabs, splits, panes are sway's job. A terminal that also tiles is duplicated budget.
130 103
131 104 **yazi runs with kitty graphics under rio, not sixel.** Both work in rio; kitty is the modern protocol, sixel is the compat path. Yazi picks the graphics protocol at runtime by detecting terminal capability from `TERM` / `TERM_PROGRAM`, so the config work is small:
132 105
@@ -140,7 +113,7 @@
140 113
141 114 Rejected:
142 115 - **alacritty.** Upstream has firmly refused sixel/kitty graphics for years — definitional stance, not a pending PR. Kills yazi previews permanently.
143 - - **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 Niri's tiling and is spent budget in this stack.
116 + - **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.
144 117 - **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.
145 118
146 119 No custom egui terminal — terminals are among the most complex userland software (ANSI, terminfo, sixel/kitty graphics, IME, OSC, performance under load). Wrong scope.
@@ -204,7 +177,7 @@
204 177
205 178 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.
206 179
207 - **Clean split with Rnote:** zathura reads, Rnote annotates (fold-to-notes workflow, see [HARDWARE-FW12.md](HARDWARE-FW12.md)). Do not add annotation to the zathura config.
180 + **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.
208 181
209 182 ## Utility defaults — system introspection
210 183
@@ -244,22 +217,22 @@
244 217
245 218 ## Utility defaults — Wayland session glue
246 219
247 - 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 Niri" more than visual identity.
220 + 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.
248 221
249 222 ### Clipboard history: **cliphist**
250 223
251 - Go, wlroots-friendly, tiny. Text and image histories stored in a local DB, queryable via `cliphist list` and pasteable via `cliphist decode`. Paired with anyrun for interactive selection (anyrun-plugin-cliphist exists).
224 + Go, wlroots-friendly, tiny. Text and image histories stored in a local DB, queryable via `cliphist list` and pasteable via `cliphist decode`. Interactive selection via `cliphist list` piped to a TUI picker (fzf/nu), or a future `alloy` console verb.
252 225
253 - **Niri integration:** `spawn-sh-at-startup` two watchers in `niri.kdl`:
226 + **Sway integration:** two watchers `exec`'d from the sway config:
254 227
255 - ```kdl
256 - spawn-sh-at-startup "wl-paste --type text --watch cliphist store"
257 - spawn-sh-at-startup "wl-paste --type image --watch cliphist store"
228 + ```
229 + exec wl-paste --type text --watch cliphist store
230 + exec wl-paste --type image --watch cliphist store
258 231 ```
259 232
260 233 No config file — cliphist's storage lives at `~/.local/share/cliphist/db` and needs no tuning.
261 234
262 - Rejected: clipse (Go, TUI-only, no anyrun integration), copyq (Qt, retained-mode GUI overkill).
235 + Rejected: clipse (Go, a full TUI where a `cliphist list` pipe suffices), copyq (Qt, retained-mode GUI overkill).
263 236
264 237 ### Screen recorder: **wl-screenrec**
265 238
@@ -271,13 +244,13 @@
271 244
272 245 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.
273 246
274 - **Niri integration:** Fn keys bound to `swayosd-client --output-volume=raise` and similar in `niri.kdl` (see the config's README for the block).
247 + **Sway integration:** Fn keys bound to `swayosd-client --output-volume raise` and similar in the sway config (see the config for the block).
275 248
276 249 Rejected: avizo (Python, less maintained), custom mako notifications for the OSD (mako is for notifications, not indicator overlays — different job).
277 250
278 251 ### Media keys: **playerctl**
279 252
280 - C, MPRIS client. CLI. No config — Niri binds media keys directly to `spawn "playerctl" "play-pause"` and similar. Handles Spotify, mpv, Firefox/Floorp, and any MPRIS-compliant source.
253 + C, MPRIS client. CLI. No config — sway binds media keys directly to `exec playerctl play-pause` and similar. Handles Spotify, mpv, Firefox/Floorp, and any MPRIS-compliant source.
281 254
282 255 Rejected: playerctld (still a playerctl variant), no serious alternative.
283 256
@@ -293,7 +266,7 @@
293 266
294 267 Applied three ways because different apps read cursor state from different places:
295 268
296 - - `~/.icons/default/index.theme` inherits from `Bibata-Modern-Classic` — most apps and Niri itself resolve this.
269 + - `~/.icons/default/index.theme` inherits from `Bibata-Modern-Classic` — most apps and sway itself resolve this.
297 270 - `gtk-cursor-theme-name` in `~/.config/gtk-{3.0,4.0}/settings.ini` — GTK apps.
298 271 - `XCURSOR_THEME` and `XCURSOR_SIZE` in `etc/skel/.config/nushell/env.nu` — everything that reads env vars.
299 272
@@ -307,7 +280,7 @@
307 280
308 281 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.
309 282
310 - **What this covers:** any GTK 3 or GTK 4 app that consumes libadwaita's named tokens — Rnote, Ironbar, Floorp's system dialogs, cosmic-files if GTK-based, etc.
283 + **What this covers:** any GTK 3 or GTK 4 app that consumes libadwaita's named tokens — swayosd (GTK-rendered overlays), Floorp's system dialogs, cosmic-files if GTK-based, etc.
311 284
312 285 **What this doesn't cover:**
313 286 - Legacy GTK 3 apps with their own token sets (rare — most have migrated).
@@ -325,7 +298,7 @@
325 298
326 299 Optimized for readability. A single fontconfig at `~/.config/fontconfig/fonts.conf` routes every app that asks for a generic family to Alloy's picks. Config at [`etc/skel/.config/fontconfig/`](../etc/skel/.config/fontconfig/).
327 300
328 - **Departure Mono, Alloy's brand mark, is not in the fontconfig chain.** It stays reserved for authored surfaces (marquee UI, headers, brand elements) and is invoked by name where wanted. Nobody reads code or long text in Departure.
301 + **Departure Mono, Alloy's brand mark, is not in the fontconfig chain.** It stays reserved for authored surfaces (the console and `alloy_tui`, headers, brand elements) and is invoked by name where wanted. Nobody reads code or long text in Departure.
329 302
330 303 ### Monospace: **IosevkaTerm Nerd Font**
331 304
@@ -357,7 +330,7 @@
357 330
358 331 ## Greeter
359 332
360 - **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 `niri-session` on successful auth. Config at [`etc/greetd/`](../etc/greetd/) — `/etc/greetd/config.toml` sets up VT1 with tuigreet + Alloy palette applied via `--theme`.
333 + **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`.
361 334
362 335 Same ratatui toolkit family as `alloy console` and `bottom`; the login screen reads as a design sibling to the rest of the Alloy chrome.
363 336
@@ -369,4 +342,4 @@
369 342 - **regreet.** Rust GTK greetd greeter — nicer visuals than tuigreet but drags GTK into the login layer. tuigreet's austerity is a feature.
370 343 - **agreety.** greetd's default plain-text prompt. Works but visually inconsistent with the rest of the stack.
371 344
372 - **Later: bespoke Alloy greetd greeter using `alloy_tui`.** Marquee-app candidate for v2+ alongside the lockscreen and console. Would put the login screen inside the same design system as everything else Alloy authors. Deferred until `alloy_tui` v1 lands.
345 + **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.
M docs/TOKENS.md +58 -91
@@ -1,9 +1,11 @@
1 1 # Alloy Tokens
2 2
3 - The canonical, locked design tokens for Alloy. This file is the source of truth; `alloy_ui/tokens.toml` (when scaffolded) is generated from these decisions, and `tokens.rs` is generated from that. Changing a value here changes every authored Alloy surface.
3 + The canonical, locked design tokens for Alloy. This file is the source of truth; `alloy_tui/tokens.toml` (when scaffolded) is generated from these decisions, and `tokens.rs` is generated from that. Changing a value here changes every authored Alloy surface.
4 4
5 5 The rules these tokens implement live in [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md). This file is where rules become numbers.
6 6
7 + **Terminal target.** Since the 2026-07-17 pivot, Alloy authors TUIs (ratatui), not GUIs. The **palette and the semantic-color rules survive unchanged** and are the load-bearing part of this file. What does not survive a terminal cell grid: pixel sizes, corner radii, sub-cell border widths, and the two-font working/display split (a terminal renders one font). Each section below states what it becomes in a terminal.
8 +
7 9 ## Tint hue
8 10
9 11 **`H = 80`** — a warm yellow-orange. Reads as beige in light mode, warm charcoal in dark mode. Picked over alternatives (H=60 too yellow, H=40 too orange, H=90 drifts greenish at high lightness) because it stays unambiguously warm across the full L range.
@@ -12,7 +14,7 @@
12 14
13 15 ## Tint chroma
14 16
15 - **`C = 0.012`** for chrome. Just above the perceptual threshold against a neutral grey reference at typical viewing distance, well below where the tint starts to read as "colored" rather than "warm." A neutral grey of equivalent L is visibly cooler when placed adjacent.
17 + **`C = 0.012`** for chrome. Just above the perceptual threshold against a neutral grey reference at typical viewing distance, well below where the tint starts to read as "colored" rather than "warm."
16 18
17 19 ## The ramp (light mode, canonical)
18 20
@@ -20,30 +22,30 @@
20 22
21 23 | Token | L | Use |
22 24 |---|---|---|
23 - | `surface-sunken` | 92 | Recessed wells (search inputs, code blocks inside surface-raised) |
24 - | `surface` | 96 | Window background, base canvas |
25 - | `surface-raised` | 98 | Cards, panels, default control surface |
26 - | `surface-overlay` | 99.5 | Popovers, menus, dialogs, tooltips |
25 + | `surface-sunken` | 92 | Recessed wells (search inputs, log panes inside a raised surface) |
26 + | `surface` | 96 | Base canvas, the terminal background Alloy targets |
27 + | `surface-raised` | 98 | Focused pane, active list, default control surface |
28 + | `surface-overlay` | 99.5 | Popovers, menus, modal dialogs |
27 29 | `border-subtle` | 88 | Internal dividers within a single surface tier |
28 - | `border` | 80 | Default control borders, panel edges |
29 - | `border-strong` | 65 | Focus rings, emphasized boundaries |
30 + | `border` | 80 | Default block borders, pane edges |
31 + | `border-strong` | 65 | Focus borders, emphasized boundaries, selection stripe |
30 32 | `text-muted` | 55 | Disabled text, secondary metadata, placeholder |
31 33 | `text-secondary` | 38 | Labels, section headers, non-primary body |
32 34 | `text-primary` | 18 | Default body, primary content |
33 35
34 - Eight L-stops covering chrome (top four), borders (middle three), and text (bottom three), with `text-muted` straddling the boundary because it must read as text against `surface` and as a border-adjacent disabled cue against `surface-raised`.
36 + Eight L-stops covering chrome (top four), borders (middle three), and text (bottom three), with `text-muted` straddling the boundary.
37 +
38 + **In a terminal:** each stop becomes a `ratatui::style::Color::Rgb` after OKLCH to sRGB conversion. Surfaces are applied as `.bg(...)`, text and borders as `.fg(...)`. Not every terminal cell can carry an independent background cheaply, so surface tiers are expressed by drawing bordered `Block`s with the tier's `bg`, not by tinting whole regions; adjacent tiers stay one or two L-stops apart so they read as distinct even at 24-bit.
35 39
36 40 ## The ramp (dark mode, derived)
37 41
38 - Dark mode preserves H and C and inverts L *polarity*, but does **not** strictly compute `L_dark = 100 − L_light`. Strict inversion would break the elevation rule from [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#affordance-depth-via-ramp-not-via-color), which requires `surface-raised` to read as closer to the eye than `surface` in *both* modes — meaning lighter in both modes, not lighter-then-darker.
39 -
40 - The correct derivation: the chrome tier flips to low L with elevation increasing the L slightly per step (raised is brighter than base, as before). The text tier flips to high L. Borders span the middle. Same ordering semantics, opposite anchor.
42 + Dark mode preserves H and C and inverts L *polarity*, but does **not** strictly compute `L_dark = 100 − L_light`. Strict inversion would break the elevation rule from [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#affordance-depth-via-ramp-not-via-color), which requires `surface-raised` to read as closer to the eye than `surface` in *both* modes (lighter in both, not lighter-then-darker).
41 43
42 44 | Token | L | Use |
43 45 |---|---|---|
44 46 | `surface-sunken` | 8 | (recessed wells) |
45 - | `surface` | 12 | (window background) |
46 - | `surface-raised` | 15 | (cards, controls) |
47 + | `surface` | 12 | (base canvas) |
48 + | `surface-raised` | 15 | (focused pane, controls) |
47 49 | `surface-overlay` | 19 | (popovers, dialogs) |
48 50 | `border-subtle` | 22 | |
49 51 | `border` | 30 | |
@@ -52,30 +54,26 @@
52 54 | `text-secondary` | 78 | |
53 55 | `text-primary` | 94 | |
54 56
55 - The exact L values are calibrated so contrast ratios between text-primary/surface, text-secondary/surface, and border/surface match within ±0.3 across modes when measured in WCAG 2.1 terms. Verify with a contrast checker on first implementation; adjust here if a stop drifts.
57 + The exact L values are calibrated so contrast ratios between text-primary/surface, text-secondary/surface, and border/surface match within ±0.3 across modes in WCAG 2.1 terms. Verify with a contrast checker on first implementation. The app selects `Mode` at startup, from a `--theme` flag, or by querying the terminal (`$COLORFGBG` / OSC background query).
56 58
57 - ## Hover, pressed, focused, disabled
59 + ## Focus, selected, disabled
58 60
59 - Mandatory per [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#affordance-depth-via-ramp-not-via-color). Computed from the ramp, not separate values.
61 + Mandatory per [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#affordance-depth-via-ramp-not-via-color). Computed from the ramp, not separate values. A terminal has no hover or pressed state (no pointer contract), so the interactive states reduce to three that keyboard focus can express.
60 62
61 - For a control at `surface-raised`:
63 + For a control/pane at `surface-raised`:
62 64
63 65 | State | Surface | Border | Text |
64 66 |---|---|---|---|
65 67 | default | `surface-raised` | `border` | `text-primary` |
66 - | hover | `surface-overlay` | `border` | `text-primary` |
67 - | pressed | `surface` | `border` | `text-primary` |
68 - | focused | `surface-raised` | `border-strong` (2px ring offset 1px outside the control's geometry) | `text-primary` |
68 + | focused | `surface-raised` | `border-strong` (the block's border switches to `border-strong`) | `text-primary` |
69 + | selected | `surface-raised`, plus a `border-strong` leading-edge stripe on the row | `border` | `text-primary` |
69 70 | disabled | `surface-raised` | `border-subtle` | `text-muted` |
70 71
71 - "One step toward the eye" = next-lighter ramp stop, both modes.
72 - "One step away" = next-darker ramp stop, both modes.
73 -
74 - Focus ring is `border-strong`, never an accent color. Focus is chrome, not data.
72 + Focus border and selection stripe are `border-strong`, never an accent color. Focus is chrome, not data. (The former hover/pressed one-step-toward/away-from-the-eye rules do not apply in a terminal; keep them documented in DESIGN-LANGUAGE for any adopted GUI surface.)
75 73
76 74 ## The information palette
77 75
78 - Locked. Same OKLCH in both modes — accents are recognized by hue, and inverting them per mode would defeat that recognition. Their job is to be unmistakable on either surface, which is why they sit at moderate chroma in the mid-L range where they have adequate contrast against both `surface` (L=96) and `surface` (L=12).
76 + Locked. Same OKLCH in both modes; accents are recognized by hue, and inverting them per mode would defeat that recognition. Their job is to be unmistakable on either surface.
79 77
80 78 | Token | OKLCH | Hue family | Meaning |
81 79 |---|---|---|---|
@@ -85,100 +83,69 @@
85 83 | `accent-info` | `oklch(60% 0.16 240)` | blue | Informational, link, in-progress |
86 84 | `accent-syntax` | `oklch(55% 0.18 310)` | magenta | Categorical accent, syntax token, fifth-series data |
87 85
88 - Chroma sits between 0.15 and 0.18 — moderate, per design language. All five are in-gamut for sRGB at the stated L.
86 + Chroma sits between 0.15 and 0.18. All five are in-gamut for sRGB at the stated L.
89 87
90 - Accents never appear on chrome — no accent-tinted backgrounds, no accent borders on inactive controls, no accent focus rings. They appear in data, on glyphs that encode severity, and in syntax highlighting.
88 + Accents never appear on chrome: no accent-tinted pane backgrounds, no accent borders on inactive controls, no accent focus. They appear in data, on glyphs that encode severity, and in syntax highlighting. In a terminal, an accent is a `.fg(...)` on the glyph and label, never a filled `.bg(...)` behind chrome.
91 89
92 90 ### Accent-on-surface text rule
93 91
94 - When accent color is rendered as text directly on a surface (e.g. an error message), use the accent value as-is on `surface-raised` or higher. On `surface-sunken` in dark mode, accents at L≈55 can lose contrast; promote to L+5 for that specific pairing or move the message to a higher surface tier.
92 + When accent color is rendered as text directly on a surface, use the accent value as-is on `surface-raised` or higher. On `surface-sunken` in dark mode, accents at L≈55 can lose contrast; promote to L+5 for that pairing or move the message to a higher surface tier.
95 93
96 94 ## Typography
97 95
98 - Two monospace fonts with distinct, non-overlapping roles. There is no prose-paired humanist sans; mono everywhere, by design.
96 + **A terminal renders one font at one size.** The two-font working/display system from the GUI design does not survive: you cannot swap fonts or point sizes mid-terminal. What survives is the *role distinction* and the levers a terminal actually has (weight via `Modifier::BOLD`, case, and Nerd Font glyphs).
99 97
100 - ### Working font: Iosevka (`font-mono-working`)
98 + ### The terminal font: IosevkaTerm Nerd Font
101 99
102 - The font for anything *authored or read* during work: terminal, code editor, body text, labels, controls, menus, settings descriptions, package-manager output, navigation chrome, section headers.
103 -
104 - - **License:** OFL.
105 - - **Build:** `IosevkaNerdFont` (nerd-patched upstream build for icon coverage).
106 - - **Weights available:** Thin / ExtraLight / Light / Regular / Medium / SemiBold / Bold / ExtraBold / Heavy. Alloy uses Regular (400) and SemiBold (600) only; the rest are unused but available for opt-in.
107 - - **Variant note:** standard Iosevka throughout. Inside terminal emulators where ligatures interfere with column alignment, fall back to `IosevkaTermNerdFont` — same metrics, ligatures off. This is the only sanctioned variant split.
108 - - **Hierarchy lever:** weight (400 → 600), per [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#typography).
109 -
110 - ### Display font: Departure Mono (`font-mono-display`)
111 -
112 - The font for anything *displayed as a readout*: clocks, calendar dates, status panel numerics (CPU%, network throughput, battery), dashboard values, lockscreen primary surface, splash, big-number widgets. Used for the *value* on an instrument, not for the chrome around the instrument.
100 + Everything authored or read is this font, because it is the terminal's font. The `Term` variant drops ligatures (safe for cell alignment), and the Nerd Font bundle carries the glyphs the console and status chrome need. Iosevka's narrow proportions give more columns per line, real value in a tiling stack. This is set in the rio config, not by `alloy_tui`; the crate assumes it.
113 101
114 102 - **License:** OFL.
115 - - **Build:** `DepartureMonoNerdFontMono` from Nerd Fonts v3.4.0 (Departure upstream v1.422). The `Mono` (NFM) variant fits icons inside a single character cell so leading status glyphs align vertically against numeric columns; the non-Mono NF variant lets icons run ~1.5× wide and breaks tabular readouts.
116 - - **Weights available:** Regular only. By design; single-weight is part of the aesthetic.
117 - - **Hierarchy lever:** size + case + tracking, not weight. `READOUT LABELS` are `UPPERCASE` with `tracking-display-caps`; large numerics rely on size alone.
118 - - **Coverage caveat:** Departure's unicode range is limited. Verify before any surface that may render arbitrary user-supplied strings; fall back to Iosevka where coverage is uncertain.
103 + - **Hierarchy lever:** weight (`Modifier::BOLD`) for emphasis and section headers; case (`UPPERCASE` labels) and Nerd Font glyphs for readout chrome.
119 104
120 - ### The use-which test
105 + ### Departure Mono: reserved, not a live TUI font
121 106
122 - If you have to ask, ask one question: *is this text being authored/read, or is it being displayed as a value off an instrument?* Authored/read → Iosevka. Displayed-as-readout → Departure. Section headers in working chrome stay Iosevka (they're navigational, not displayed). Big-number values in a dashboard go Departure even when they sit inside a working surface.
107 + Departure Mono was the GUI display font for big-number readouts. A terminal cannot render it as a second live font, so it does **not** appear in running TUIs. It stays reserved for surfaces that are drawn as images rather than terminal cells: a first-boot splash (rendered via rio's kitty-graphics protocol), the swaylock background, and brand assets. Large numeric readouts inside a TUI (a clock, a dashboard value) are built from the terminal font using figlet-style cell art or Nerd Font block glyphs, not a font swap.
123 108
124 109 ### Tokens
125 110
126 111 | Token | Value |
127 112 |---|---|
128 - | `font-mono-working` | `IosevkaNerdFont` |
129 - | `font-mono-working-noligatures` | `IosevkaTermNerdFont` |
130 - | `font-mono-display` | `DepartureMonoNerdFontMono` |
131 - | `size-caption` | 12 |
132 - | `size-body` | 14 |
133 - | `size-section-header` | 16 |
134 - | `size-title` | 20 |
135 - | `size-readout-sm` | 16 |
136 - | `size-readout-md` | 24 |
137 - | `size-readout-lg` | 48 |
138 - | `size-readout-xl` | 72 |
139 - | `weight-body` | 400 |
140 - | `weight-emphasis` | 600 |
141 - | `tracking-body` | 0 |
142 - | `tracking-display` | 0.04em |
143 - | `tracking-display-caps` | 0.08em |
144 - | `line-height-body` | 1.4 |
145 - | `line-height-tabular` | 1.3 |
146 - | `line-height-display` | 1.0 |
113 + | `font-terminal` | `IosevkaTermNerdFont` (set in rio; assumed by `alloy_tui`) |
114 + | `font-display-reserved` | `DepartureMonoNerdFontMono` (image/asset surfaces only) |
115 + | `weight-body` | Regular (no modifier) |
116 + | `weight-emphasis` | `Modifier::BOLD` |
117 + | `tracking-display-caps` | `UPPERCASE` label convention (no sub-cell tracking in a terminal) |
147 118
148 - Sizes are integer points; egui consumes them as f32. `line-height-tabular` bumped from 1.2 to 1.3 to give pixel-grid display fonts and nerd-patched glyphs room without clipping.
119 + Point sizes, line-heights, and sub-cell tracking are omitted: in a terminal they are fixed by the emulator and the cell grid, not by Alloy.
149 120
150 121 ## Geometry
151 122
152 - | Token | Value |
153 - |---|---|
154 - | `space-xs` | 2 |
155 - | `space-sm` | 4 |
156 - | `space-md` | 8 |
157 - | `space-lg` | 16 |
158 - | `space-xl` | 24 |
159 - | `radius-control` | 4 |
160 - | `radius-overlay` | 6 |
161 - | `radius-data` | 0 |
162 - | `border-width-default` | 1 |
163 - | `border-width-focus` | 2 |
164 - | `focus-ring-offset` | 1 |
123 + A terminal grid has cells, not pixels, and no sub-cell rounding. The geometry tokens reduce to cell counts and box-drawing choices.
165 124
166 - `radius-data = 0` makes the square-vs-rounded shape difference explicit per [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#affordance-depth-via-ramp-not-via-color) — data panels are square, controls have the small radius. Overlays use a slightly larger radius (6) so popovers read as distinct surfaces rather than scaled controls.
125 + | Token | Value | Terminal meaning |
126 + |---|---|---|
127 + | `space-xs` | 0 | no gap (adjacent cells) |
128 + | `space-sm` | 1 | one blank cell/row |
129 + | `space-md` | 2 | two cells/rows |
130 + | `space-lg` | 3 | three cells/rows |
131 + | `border-data` | none | data panels are borderless (`Borders::NONE`), flush on `surface` |
132 + | `border-control` | `BorderType::Plain` | controls/panes get a single-line box border |
133 + | `border-focus` | `BorderType::Plain` at `border-strong` | focus is a color change on the same border, not a thicker one |
167 134
168 - All numeric values are unitless points; egui consumes them as f32.
135 + `radius-*`, `border-width-*`, and `focus-ring-offset` from the GUI design are dropped: terminals cannot round corners, vary border width, or offset a ring outside a cell. The square-vs-bordered distinction (data flush and borderless, controls bordered) carries the same affordance the radius did in the GUI.
169 136
170 137 ## What's deferred
171 138
172 - - Validating the dark-mode L stops against measured WCAG contrast on a calibrated display.
173 - - Verifying Departure's unicode coverage and documenting an explicit fallback chain (likely Iosevka) for code points outside its range.
174 - - Icon stroke weight, line-art rules — these belong with the icon-theme decision, also open per todo.
139 + - Validating the dark-mode L stops against measured WCAG contrast on a calibrated display and in real terminals (truecolor and 256-color).
140 + - The 256-color downgrade table generated from the OKLCH values, for terminals that report no truecolor.
141 + - Figlet/Nerd-Font recipes for the large readout numerics that Departure used to provide.
175 142
176 143 ## How this file is consumed
177 144
178 - Until the egui design-system crate is scaffolded, this is markdown. When scaffolded:
145 + Until `alloy_tui` is scaffolded, this is markdown. When scaffolded:
179 146
180 - 1. These values are transcribed into `alloy_ui/tokens.toml` verbatim. The TOML is the build-time input.
181 - 2. `build.rs` parses the TOML, converts OKLCH→sRGB, emits `tokens.rs` with typed constants (`Color32`, `f32`, `&'static str`).
182 - 3. Rust code only ever imports from `tokens.rs`. No app or widget defines a color, size, or radius locally.
147 + 1. The palette and cell-spacing values are transcribed into `alloy_tui/tokens.toml`. The TOML is the build-time input.
148 + 2. `build.rs` parses the TOML, converts OKLCH to sRGB, and emits `tokens.rs` with typed ratatui constants (`Style`, `Color`, `Modifier`, `u16` cell counts) plus a 256-color fallback table.
149 + 3. Rust code only ever imports from `tokens.rs`. No app or widget constructs a color, style, or spacing value locally.
183 150
184 - Changes to tokens flow: edit this file → edit `tokens.toml` → rebuild → all surfaces update. This file stays canonical because the TOML can be regenerated from it but not vice versa (rationale lines, alternative considerations, and the dark-mode derivation argument don't survive a round-trip through TOML).
151 + Changes flow: edit this file, edit `tokens.toml`, rebuild, every surface updates. This file stays canonical because the TOML can be regenerated from it but not vice versa (the rationale, alternatives, and the dark-mode derivation argument do not survive a round-trip through TOML).