Skip to main content

max / alloy

docs: close the stack coverage gaps STACK.md claims to be "every pick with reasoning and what was rejected" and principle 5 says every default is a position defended in writing. The 2026-07-21 preinstalled-software audit found three places that was not true, and checking them turned up a fourth. zoxide and direnv had no entry. They appeared only in SHELL.md, as integration mechanics rather than as picks. Both now sit under Shell with their rejected alternatives. direnv's entry carries the principle-1 comparison that was missing: mise (Rust, but the env feature rides along with a version manager Alloy has no position on) and shadowenv (Rust, right scope, Lisp config, which is the line already held against wezterm and eww). restic vs rustic was unmade, and it is the only pick in the image where a competitive Rust option exists. Decided for restic on three grounds, none of them technical: rustic is in neither Fedora nor Terra (repoquery against both), so it needs a cargo install stage, the cost that already deferred wl-screenrec; tutorial coverage is worth most in the tool you reach for in a panic; and the shared repo format means waiting costs nothing, since a user can point rustic at the same repository later. Recorded as a distribution decision rather than a claim restic is the better program. STACK.md said the base was fedora-bootc:42. It has been 43. The fourth: SHELL.md listed direnv among the tools that ship a nu init. It does not. direnv 2.35.0 rejects both `nu` and `nushell`, supporting gzenv, murex, vim, zsh, bash, elvish, fish, json, tcsh, pwsh, systemd and gha. That claim is why the missing integration read as an oversight rather than something Alloy has to author, so SHELL.md now names `direnv export json` as the seam a nu hook gets built on, and STACK.md's direnv entry says outright that it ships inert. Same file also described config.nu as holding integration hooks, which it deliberately does not: `source` is parse-time in nu, so a runtime-gated source is a parse error that costs the whole file. Screenshot paragraph updated for the bind that landed in e074d79.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-22 00:20 UTC
Signed with PGP, not checked
Commit: c407451adcd7a52e5ea4d858c1956373fbf89d8f
Parent: e074d79
3 files changed, +31 insertions, -5 deletions
@@ -81,7 +81,15 @@
81 81
82 82 The forcing observation: when a user's laptop dies, they have their sync mesh (Syncthing pulled files back) and their network identity (Tailscale re-enrolls). What they don't have without configuring restic is *point-in-time recovery*, the "I deleted this file three weeks ago and just noticed" case that only versioned backup solves.
83 83
84 - Rejected: borg (Python; historical repo-format transitions), kopia (GUI-first; adds complexity Alloy doesn't need), duplicity (older, less-featured), rclone (sync, not backup; no snapshot history or dedup).
84 + **rustic, and why not yet.** rustic is a Rust reimplementation of restic that reads and writes the same repository format, which makes it the one place in the image where a competitive Rust option exists against an adopted Go tool. Principle 1 says that comparison gets made rather than skipped. Three things decide it for restic today, and none of them are about the code:
85 +
86 + - **Packaging.** restic is in Fedora main (0.19.0 as of this writing). rustic is in neither Fedora nor Terra, verified by repoquery against both, so shipping it means a `cargo install` stage in the Containerfile. That is the same cost that deferred wl-screenrec (see [STACK.md](STACK.md#screen-recorder-wl-screenrec)), and it buys less here: the console's `rust-build` stage already pays for the toolchain, but a backup tool nobody has configured yet is a thin reason to grow the build.
87 + - **Tutorial coverage.** The reason restic was picked stands. Backup is the one tool where a user searching in a panic needs the answer to already exist, and restic has a decade of write-ups behind it.
88 + - **Nothing is lost by waiting.** The repo format is shared. A user who starts on restic and later prefers rustic points it at the same repository; no migration, no re-seed, no lock-in. That is unusual and it makes this decision cheap to revisit.
89 +
90 + Revisit when rustic lands in Fedora or Terra, or when there is a second cargo-built binary to share a build stage with. The honest summary: restic is shipped for distribution reasons, not for technical superiority over rustic.
91 +
92 + Also rejected: borg (Python; historical repo-format transitions), kopia (GUI-first; adds complexity Alloy doesn't need), duplicity (older, less-featured), rclone (sync, not backup; no snapshot history or dedup).
85 93
86 94 **Deferred: `alloy backup` subcommand.** A ratatui front for `restic snapshots` / `restic backup` / `restic restore` fits the Alloy Console pattern (see [CONSOLE.md](CONSOLE.md)). v2+ candidate, worth building once someone is using restic daily and wants a nicer surface.
87 95
M docs/SHELL.md +2 -2
@@ -25,7 +25,7 @@
25 25
26 26 `etc/skel/.config/nushell/` in the Alloy repo:
27 27
28 - - `config.nu`: main nu configuration. Table style, history (sqlite backend), fuzzy completions, Alloy color palette applied to nu's own output, integration hooks for starship / zoxide / direnv (conditional on presence).
28 + - `config.nu`: main nu configuration. Table style, history (sqlite backend), fuzzy completions, Alloy color palette applied to nu's own output. Tool integrations are deliberately *not* here: `source` is a parse-time keyword in nu, so it cannot be gated behind a runtime check without failing the parse and costing the whole file. The image writes them to `/usr/share/nushell/vendor/autoload/` instead, which nushell reads before `config.nu` runs.
29 29 - `env.nu`: environment. `EDITOR=hx`, `VISUAL=hx`, PATH additions for `~/.local/bin` and `~/.cargo/bin`, cache directory prep for integration inits.
30 30 - `aliases.nu`: a small curated set (`ll`, `la`, `..`, `...`, git shortcuts). No plugin framework.
31 31
@@ -35,7 +35,7 @@
35 35
36 36 Where daily-use friction shows up, and how Alloy handles it:
37 37
38 - - **Tool init lines.** `eval "$(starship init bash)"` and equivalents. Starship, zoxide, direnv, atuin, and carapace all ship `--init nu`. For the long tail of tools that don't, Alloy ships small wrapper snippets in `etc/skel/.config/nushell/integrations/` as they're needed, added on demand rather than preemptively.
38 + - **Tool init lines.** `eval "$(starship init bash)"` and equivalents. Starship, zoxide, atuin, and carapace all emit a nu init directly, and the image generates those at build time into `/usr/share/nushell/vendor/autoload/`. **direnv does not.** As of 2.35.0 its `hook` and `export` targets are bash, zsh, fish, elvish, tcsh, murex, pwsh, vim, systemd, gha, json, and gzenv, with no nu among them; `direnv export json` is the seam a nu hook has to be built on. For that and the rest of the long tail, Alloy ships small wrapper snippets in `etc/skel/.config/nushell/integrations/`, added on demand rather than preemptively.
39 39 - **Interactive one-liners.** `FOO=bar cmd && cmd2` becomes `FOO=bar; cmd; cmd2` (or `try / catch` for the conditional flavor). Muscle-memory tax; not a real barrier.
40 40 - **`export`.** `$env.FOO = "bar"` in nu, `export FOO=bar` in bash. Both work in their respective contexts.
41 41 - **Piped stdin.** Third-party CLIs that expect line-oriented text stdin: nu's default output is a table, but piping to an external command auto-flattens to text. Explicit control via `| to text` when needed.
M docs/STACK.md +20 -2
@@ -8,7 +8,7 @@
8 8
9 9 **ublue relationship: alongside, not downstream.** Preserves naming and architectural independence. The "Alloy" name was picked specifically to avoid the ublue "Blue\*" prefix. Tradeoff: no shared image-building infrastructure, so whatever Alloy ships for image composition is on Alloy to build/maintain. Upside: total architectural freedom and a clean brand.
10 10
11 - **Composition strategy:** bootc + Containerfile, `FROM quay.io/fedora/fedora-bootc:42`. Source on `git.sr.ht/~maxmj/alloy`, CI on `builds.sr.ht`, published container image on `quay.io/alloy/alloy`. This is the ublue *convention* (Containerfile, OCI registry, CI-driven build) without ublue's base image, which is what "alongside ublue" resolves to in practice. Full delivery strategy and open questions in [IMAGE.md](IMAGE.md). Containerfile at the repo root; sr.ht CI at `.builds/alloy-image.yml`.
11 + **Composition strategy:** bootc + Containerfile, `FROM quay.io/fedora/fedora-bootc:43`. Source on `git.sr.ht/~maxmj/alloy`, CI on `builds.sr.ht`, published container image on `quay.io/alloy/alloy`. This is the ublue *convention* (Containerfile, OCI registry, CI-driven build) without ublue's base image, which is what "alongside ublue" resolves to in practice. Full delivery strategy and open questions in [IMAGE.md](IMAGE.md). Containerfile at the repo root; sr.ht CI at `.builds/alloy-image.yml`.
12 12
13 13 ## Compositor
14 14
@@ -48,7 +48,7 @@
48 48
49 49 ## Screenshot stack
50 50
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.
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+Print (region via slurp), Ctrl+Print (active window). The active-window grab reads the focused rect out of `swaymsg -t get_tree` with jq, which the fedora-bootc base already carries, so it costs nothing to ship.
52 52 - **satty** for annotation when needed (Rust, modern, replaces swappy). Mod+Print annotates the most recent capture.
53 53 - **wl-clipboard** for clipboard plumbing.
54 54
@@ -174,6 +174,24 @@
174 174
175 175 Rejected: fish (safer pick but Alloy's audience is script-writers), zsh (raw zsh unpleasant; adopting oh-my-zsh/zinit imports someone else's opinions), bash-as-login (un-opinionated at a layer where Alloy is opinionated everywhere else).
176 176
177 + ### Directory jumping: **zoxide**
178 +
179 + Rust, tiny, no daemon. Tracks the directories you actually visit and ranks them by frequency and recency, so `z alloy` lands in the project from anywhere. On a tiling system where every workspace holds a terminal, the alternative is retyping paths all day; this is one of the few adjuncts that pays for itself within a session of use.
180 +
181 + The Containerfile generates the nu integration at build time (`zoxide init nushell` into `/usr/share/nushell/vendor/autoload/`), so it is live on first login with nothing for the user to wire up.
182 +
183 + Rejected: autojump (Python, effectively unmaintained), z / z.lua (shell-script and Lua ancestors of the idea; zoxide is the modern rewrite of exactly this), fasd (unmaintained), fzf-driven `cd` (needs a config recipe rather than a default, which is the pattern Alloy rejects), and shipping nothing (nu's `cd` plus tab completion is the baseline zoxide beats).
184 +
185 + ### Per-directory environment: **direnv**
186 +
187 + Go, mature, the de facto answer for binding a project's environment to its directory: `cd` into a repo and its `.envrc` loads, leave and it unloads. Alloy's audience keeps toolchains, API endpoints, and per-project paths out of the global environment, and direnv is how that stays automatic instead of remembered.
188 +
189 + Go rather than Rust, named per principle 1. Two Rust options exist and were weighed. **mise** does per-directory env, but its center of gravity is tool-version management (an asdf replacement), so adopting it for the env half means shipping a version manager Alloy has no position on. **shadowenv** (Rust, Shopify) is closer to the right scope, but configures in a Lisp dialect, which is the same scripting-language-config line the stack holds against wezterm and eww. direnv's `.envrc` is shell, which every user already reads.
190 +
191 + Also rejected: autoenv (bare `source`, no security model, no unload), and per-project `nu` scripts sourced by hand (not automatic, which is the whole feature).
192 +
193 + **Known gap: direnv ships inert.** It has no nushell hook, so the build-time autoload trick used for starship and zoxide has nothing to call. `direnv export json` is the supported seam, and the integration is an Alloy-authored snippet in `etc/skel/.config/nushell/integrations/` per [SHELL.md](SHELL.md#interoperability). Until that lands the binary is present and does nothing under the login shell.
194 +
177 195 ## Utility defaults: content viewers
178 196
179 197 Alloy ships defaults for the three content types users open constantly. Not shipping them cedes the choice to whatever a user's first web search returns (VLC for video, nomacs for images), which conflicts with the curated-defaults principle everywhere else.