Skip to main content

max / alloy

Swap the terminal from rio to shop Build shop from source at a pinned revision in the rust-build stage and copy it into the image; drop rio from the dnf set. Update STACK.md with the authored-vs-adopted reasoning, the accepted costs, and the yazi graphics detection question shop's lack of TERM_PROGRAM opens.
Author: Max Johnson <me@maxj.phd> · 2026-07-31 20:44 UTC
Signed with PGP, not checked
Commit: 0771ea5caa5a43c9bd7b4ba3ad65667c3d9c8079
Parent: 9a574a7
13 files changed, +111 insertions, -49 deletions
M Containerfile +55 -4
@@ -31,7 +31,53 @@
31 31 # greeter's console palette; skelgen emits that table now, along with the rest
32 32 # of the desktop skeleton, so there is one implementation of the ANSI mapping
33 33 # instead of three.
34 - RUN dnf install -y cargo rust && dnf clean all
34 + # git, and the two libraries shop links, on top of the toolchain. The console
35 + # needs none of them; shop is Wayland and GPU facing, so it links
36 + # libwayland-client and libxkbcommon at build time and will not compile
37 + # without their headers. `ldd` on the built binary is the check if this list
38 + # ever looks wrong.
39 + RUN dnf install -y cargo rust git wayland-devel libxkbcommon-devel pkgconf \
40 + && dnf clean all
41 +
42 + # =====================================================================
43 + # shop, Alloy's terminal.
44 + # =====================================================================
45 + # Built from source rather than installed from a repo, because there is no
46 + # repo: shop is first-party and packaged nowhere. Owning an RPM spec, a COPR
47 + # and a release cadence for a binary whose only consumer is this image buys
48 + # nothing, and Alloy publishes no other artifact (distribution is builders,
49 + # not images).
50 + #
51 + # Pinned by revision, not by branch. An unpinned build is a different image
52 + # every day for the same Containerfile, and the terminal is the one component
53 + # where "it worked yesterday" has no recovery path from inside the session.
54 + # Bump this deliberately.
55 + #
56 + # `cargo install` rather than a second COPY-and-build stage: shop is a separate
57 + # repo with its own workspace, so there is no local tree to copy and no
58 + # existing pin to imitate. `--locked` builds the dependency graph shop
59 + # committed rather than whatever resolves that day, matching the console's
60 + # `--locked` below.
61 + #
62 + # Above the console's source on purpose. This layer is keyed on the revision
63 + # alone, so console edits — the thing that changes most often — reuse it
64 + # instead of rebuilding a terminal that did not change. Moving it below the
65 + # `COPY crates/` would defeat the dependency-cache split that follows.
66 + ARG SHOP_REV=1a32ff7809fa8fff429f2dacc8c8be63caa20a4c
67 + RUN cargo install \
68 + --git https://makenot.work/git/max/shop.git \
69 + --rev "${SHOP_REV}" \
70 + --locked \
71 + --root /shop \
72 + shop
73 +
74 + # The binary exists and runs. Every other way this fails is silent at build
75 + # time and total at first login: sway binds $mod+Return to `shop` and the
76 + # launcher spawns `${TERMINAL:-shop}`, so a missing binary is a session with
77 + # no way to reach a shell and no way to edit the config that would fix it,
78 + # short of a VT. Same reasoning as the console's stub assertions below.
79 + RUN test -x /shop/bin/shop \
80 + || { echo "shop did not install; the session would have no terminal" >&2; exit 1; }
35 81
36 82 WORKDIR /src
37 83
@@ -350,8 +396,8 @@
350 396 libnotify \
351 397 satty \
352 398 swww \
353 - # Terminal, editor, shell, prompt
354 - rio \
399 + # Editor, shell, prompt. The terminal is not here: shop is built from
400 + # source in the rust-build stage and copied in below.
355 401 helix \
356 402 nushell \
357 403 starship \
@@ -788,7 +834,7 @@
788 834 gnome-session \
789 835 # foot, a second terminal nothing in Alloy invokes. It arrives as a
790 836 # weak dependency of sway-config-upstream, whose config binds it as
791 - # sway's default terminal; Alloy ships its own config and binds rio
837 + # sway's default terminal; Alloy ships its own config and binds shop
792 838 # (docs/STACK.md#terminal), so nothing here ever launches it. Left in,
793 839 # it puts three of the eleven entries in the new launcher (Foot, Foot
794 840 # Client, Foot Server), which is how an unused package stops being
@@ -1276,6 +1322,11 @@
1276 1322 # something the operator dropped in.
1277 1323 COPY --from=rust-build /src/target/release/alloy /usr/bin/alloy
1278 1324
1325 + # The terminal, from the same stage. /usr/bin for the same reason as the
1326 + # console: shipped software, and /usr/local is not part of a bootc image's
1327 + # immutable tree.
1328 + COPY --from=rust-build /shop/bin/shop /usr/bin/shop
1329 +
1279 1330 # The themes that binary refuses to run without. Kept next to it so the
1280 1331 # two move together: shipping the console without these is not a degraded
1281 1332 # console, it is one that exits on launch.
@@ -17,7 +17,7 @@
17 17 Large, declarative iconography lived on GUI surfaces (egui lockscreen ornament, launcher app icons, settings headers, marquee-app heroes). Those surfaces were dropped in the pivot, so the hero tier lost most of its homes. It survives only where a graphical surface remains:
18 18
19 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.
20 + - **First-boot splash**: renderable via shop's kitty-graphics protocol, or shown before the session starts.
21 21 - **Brand assets** outside the running system (repo social card, README).
22 22
23 23 Everything else that used to carry a hero illustration is now terminal chrome and uses the functional glyph tier instead. The hero tier is therefore **deferred**: the style below is preserved so the identity is not lost and so the surviving surfaces have a spec, but no hero illustrations are on the critical path for the TUI work.
M docs/SHELL.md +1 -1
@@ -12,7 +12,7 @@
12 12
13 13 The pick that keeps the ecosystem tax bounded:
14 14
15 - - **Login shell:** `nu`. What the user sees when they open rio, when they SSH in, when they open a scratch pane.
15 + - **Login shell:** `nu`. What the user sees when they open shop, when they SSH in, when they open a scratch pane.
16 16 - **`/bin/sh`:** bash, unchanged. Every `curl | sh` invokes this. Untouched.
17 17 - **`/bin/bash`:** bash, unchanged. Every `#!/bin/bash` script runs here. Untouched.
18 18 - **`/etc/shells`:** contains both nu and bash. `chsh -s /bin/bash` reverts a user to bash cleanly and reversibly.
M docs/STACK.md +22 -15
@@ -50,7 +50,7 @@
50 50
51 51 ## Launcher
52 52
53 - **`alloy-menu`: fzf over desktop entries, run in rio.** Bound `Mod+D`. A shell script in `usr/bin/`, about forty lines, that reads `.desktop` files from the system, user and Flatpak paths, pipes names through fzf, and hands the chosen `Exec` line to `swaymsg exec` so the app outlives the terminal that launched it.
53 + **`alloy-menu`: fzf over desktop entries, run in shop.** Bound `Mod+D`. A shell script in `usr/bin/`, about forty lines, that reads `.desktop` files from the system, user and Flatpak paths, pipes names through fzf, and hands the chosen `Exec` line to `swaymsg exec` so the app outlives the terminal that launched it.
54 54
55 55 The split with `Mod+Return` is the design. Typing a command is what a shell is for, and a shell is one keystroke away, so the launcher does not list every binary on `PATH`: doing that buries the browser under coreutils. It lists the graphical apps that have a name, an icon and a binary nobody remembers. Two keys, two jobs.
56 56
@@ -113,32 +113,39 @@
113 113
114 114 ## Terminal
115 115
116 - **rio.** Rust, WGPU-backed, TOML config, kitty and sixel and iTerm2 graphics protocols. Ships in Fedora repos.
116 + **shop.** Rust, WGPU-backed, Wayland-only, TOML config, kitty graphics protocol. Alloy's own, built from source into the image. Adopted 2026-07-31, replacing rio.
117 117
118 - The pick is driven by the yazi commitment above. Yazi is the TUI primary, "where keyboard-driven Alloy users will live by default." A terminal without graphics protocols neuters the primary tool, so the question isn't "are previews a nice-to-have" but "does the primary tool run at full capability." That reframes the axis and makes rio the honest answer.
118 + The pick is still driven by the yazi commitment above. Yazi is the TUI primary, "where keyboard-driven Alloy users will live by default." A terminal without graphics protocols neuters the primary tool, so the question isn't "are previews a nice-to-have" but "does the primary tool run at full capability." shop implements the kitty graphics protocol, which is the one yazi wants.
119 119
120 - Secondary alignments:
120 + rio satisfied that much too, and was the pick until shop met its daily-drive bar. What an adopted terminal could not satisfy is the rest:
121 +
122 + - **The terminal is a surface Alloy designs.** Every other surface in the image renders from one makeover theme through one intent table. An adopted terminal renders from sixteen hex values transcribed into its own config, and the two drift the moment either side moves. shop takes a theme by id and resolves the slots itself, from the same `makeover::ansi_intent` table the kernel cmdline and setvtrgb render from. There is nothing left to transcribe, which is why `templates/etc/skel/.config/shop/config.toml.in` carries no colour at all.
123 + - **First-party.** The console is authored. The terminal is the other surface a user is inside all day, and adopting it put the most-used window in the image on someone else's roadmap.
124 + - **Wayland-only.** The rest of the stack already assumes it. A terminal carrying an X11 path carries it for nobody here.
125 +
126 + Secondary alignments, unchanged by the swap:
121 127 - **TOML config.** No scripting language dependency, consistent with Alloy's rejection of Lua/yuck/similar config surfaces elsewhere in the stack.
122 - - **Design-forward posture.** Rio treats the terminal as a displayed surface, not just a fast text renderer. That matches Alloy's design-system stance.
123 128 - **No multiplexing.** Tabs, splits, panes are sway's job. A terminal that also tiles is duplicated budget. Alloy therefore ships no multiplexer either: tmux and zellij are not in the image, and a terminal's own docs should not send an Alloy user looking for them. A window is a window, and `Mod+H/J/K/L` moves between them.
124 129
125 - **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:
130 + **yazi should run with kitty graphics under shop, not sixel.** shop implements kitty graphics and no sixel, so there is no compat path to fall back to inside the terminal: it is inline previews or chafa half-blocks.
126 131
127 - - Rio sets `TERM=xterm-256color` and identifies itself via `TERM_PROGRAM=rio`. Yazi's runtime detection picks kitty graphics from that.
128 - - `yazi.toml` default: leave image previews on with `image_filter = "triangle"` and `image_quality = 75`. No explicit backend key needed.
129 - - Verify at v0 packaging time by opening an image directory in yazi under rio and confirming inline preview renders (not falling back to chafa/half-block).
132 + - `yazi.toml` default: leave image previews on with `image_filter = "triangle"` and `image_quality = 75`.
133 + - **Open, and the one thing to check first.** Yazi picks its graphics protocol by runtime detection. rio identified itself with `TERM_PROGRAM=rio`; shop sets `TERM=xterm-256color` and no `TERM_PROGRAM` at all, so yazi has one fewer signal than it had, and `xterm-256color` on its own advertises no graphics capability. Verify by opening an image directory in yazi under shop and confirming inline previews render. If they do not, the fix is on shop's side or in `yazi.toml`, not in yazi.
130 134
131 - (Reduce to a real `yazi.toml` file when the v0 stack packaging lands. Documented here so the pick's implication isn't lost.)
135 + Cost of authoring rather than adopting, stated plainly:
132 136
133 - Risk accepted: rio is ~2 years mature vs. alacritty's ~7. If rio proves flaky in real use, the fallback is alacritty, and the graphics gap becomes a knowing loss. Naming this trade-off here rather than pretending rio is drama-free.
137 + - **No repo to ship from.** shop is packaged nowhere, so the image builds it from source at a pinned revision (`SHOP_REV` in the Containerfile). An image build needs network to fetch it, and bumping the pin is a deliberate act rather than something `dnf update` does.
138 + - **It is new.** Written in 2026-07. The daily-drive bar it had to clear was selection and clipboard plus real key handling, both landed 2026-07-31. Still absent: scrollback, and with it the scroll wheel and scrollback search; sixel; OSC 8 hyperlinks; an I-beam pointer over the grid.
139 + - **The complexity was the objection, and it was a fair one.** Terminals are among the most complex userland software: ANSI, terminfo, graphics protocols, IME, OSC, performance under load. This section previously rejected writing one on exactly that ground. The scope was taken on deliberately, with a stated bar and a fallback, rather than by deciding the objection was wrong.
140 +
141 + Risk accepted, and the fallback is unchanged: **alacritty**, if shop proves unworkable. The graphics gap becomes a knowing loss then, exactly as it would have if rio had failed.
134 142
135 143 Rejected:
136 - - **alacritty.** Upstream has firmly refused sixel/kitty graphics for years: a definitional stance, not a pending PR. Kills yazi previews permanently.
144 + - **rio.** The prior pick, and a good one for an adopted terminal: Rust, WGPU, TOML, all three graphics protocols, in Fedora repos. Replaced for the reasons above, none of which are faults of rio's.
145 + - **alacritty.** Upstream has firmly refused sixel/kitty graphics for years: a definitional stance, not a pending PR. Kills yazi previews permanently. Kept as the fallback anyway, with that loss named.
137 146 - **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.
138 147 - **cosmic-term.** Uses `alacritty_terminal` as the backend (the same graphics gap as alacritty) and additionally couples Alloy's default terminal to libcosmic's visual identity, which the design-system stance rejects.
139 148
140 - No custom egui terminal: terminals are among the most complex userland software (ANSI, terminfo, sixel/kitty graphics, IME, OSC, performance under load). Wrong scope.
141 -
142 149 ## Browser
143 150
144 151 **Helium baked into the image as the default, unconfigured.** Helium is ungoogled-chromium with the behavior Alloy used to hand-build on top of Firefox already set upstream: ads, trackers, cookie banners and third-party cookies blocked by default, fingerprinting tampered with, no analytics, and no network request at all on first launch. Alloy ships no policy file, no preference seed, no chrome CSS and no extension pin for it. Shipping nothing is the reason it is the default, not an omission next to it. The packaging rules this follows are in the wiki note `alloy-packaging-policy`; the short form is that an app earns a recommendation by not needing configuration, which is the same test that picked helix over a configured vim.
@@ -445,7 +452,7 @@
445 452 Pango consumers are not a cell grid and keep the plain family, where the fuller icons are free.
446 453
447 454 Applied at:
448 - - Rio, all five `fonts.*.family` entries: `IosevkaTerm Nerd Font Mono`.
455 + - shop: nothing to apply. It bundles `IosevkaTerm Nerd Font Mono` into the binary and has no font setting, so the terminal cannot be pointed at a font the image does not carry. This is also why it renders nerd-font glyphs with no system font install.
449 456 - fontconfig `monospace` alias: `IosevkaTerm Nerd Font Mono`, for everything else that asks for mono.
450 457 - `templates/etc/skel/.config/mpv/mpv.conf.in`, `templates/etc/skel/.config/imv/config.in`, the sway bar and title font, mako, swaylock, satty: the plain `IosevkaTerm Nerd Font`, all pango.
451 458
@@ -7,7 +7,7 @@
7 7 the previous one stays on disk to roll back to.
8 8 - **sway as the compositor.** i3-style manual tiling on Wayland. Workspaces,
9 9 splits, tabs, and stacks. No scrolling columns, no animation.
10 - - **A terminal-first stack.** rio for the terminal, nushell for the shell,
10 + - **A terminal-first stack.** shop for the terminal, nushell for the shell,
11 11 helix for the editor, yazi for files, bottom for processes, zathura for PDFs,
12 12 imv for images, mpv for video.
13 13 - **One authored program, the `alloy` console.** Network, audio, displays,
@@ -23,7 +23,7 @@
23 23
24 24 Mod+Return
25 25
26 - That is rio, running nushell. It is where you will spend most of your time.
26 + That is shop, running nushell. It is where you will spend most of your time.
27 27
28 28 If you would rather find a graphical app by name than type a binary:
29 29
@@ -43,7 +43,7 @@
43 43 the timezone did.
44 44
45 45 **Applications** holds config files, listed by the app they configure rather
46 - than by path. You pick `rio`, not `~/.config/rio/config.toml`. Editing here is
46 + than by path. You pick `yazi`, not `~/.config/yazi/yazi.toml`. Editing here is
47 47 schema-driven: fields carry types, ranges, allowed values, and their own help
48 48 text, and the file keeps its comments and key order when it is written back.
49 49
@@ -7,9 +7,9 @@
7 7
8 8 yazi
9 9
10 - A two-pane file manager in the terminal, with previews. Under rio it renders
10 + A two-pane file manager in the terminal, with previews. Under shop it renders
11 11 images inline using the kitty graphics protocol rather than falling back to
12 - coloured blocks, which is most of the reason rio is the terminal.
12 + coloured blocks, which is most of what the terminal is asked for here.
13 13
14 14 `?` lists its keys. `q` leaves.
15 15
@@ -111,6 +111,6 @@
111 111 ## Terminal, editor, and everything else
112 112
113 113 Each adopted tool has its own themed config under `~/.config/`, rendered from
114 - the same palette: rio, helix, yazi, mako, swaylock, satty, bottom, zathura,
114 + the same palette: shop, helix, yazi, mako, swaylock, satty, bottom, zathura,
115 115 imv, mpv, swayosd, and the sway config itself. Editing one is fine; the theme
116 116 system will notice and stop overwriting it.
@@ -24,9 +24,13 @@
24 24
25 25 ### Nothing opens when I press Mod+Return
26 26
27 - Check the config's terminal name. Fedora packages the terminal as `rio` but
28 - installs its binary as `rioterm`, and a config naming `rio` leaves you with no
29 - way to open a terminal at all.
27 + Check the config's terminal name. `~/.config/sway/config` sets `$term shop`,
28 + and if that line has been edited to something the image does not carry, the
29 + binding runs nothing and says nothing.
30 +
31 + `shop` is built into the image rather than installed from a repo, so the
32 + package manager cannot tell you whether it is there. `command -v shop` from a
33 + virtual console can.
30 34
31 35 Two ways out that do not need a terminal: `Mod+D` opens the launcher, and
32 36 `Ctrl+Alt+F2` gets you a virtual console where you can edit
@@ -65,9 +65,9 @@
65 65 [ -n "$cmd" ] || exit 0
66 66
67 67 # TUI entries get a terminal to live in. $TERMINAL so this stays true if the
68 - # terminal pick ever changes; rioterm is the STACK.md default.
68 + # terminal pick ever changes; shop is the STACK.md default.
69 69 if [ "$needs_term" = "1" ]; then
70 - cmd="${TERMINAL:-rioterm} -e $cmd"
70 + cmd="${TERMINAL:-shop} -e $cmd"
71 71 fi
72 72
73 73 # swaymsg runs it through sh, so a full Exec line keeps its arguments. Outside