Skip to main content

max / alloy

skel: make the nushell config load at all Every login printed a two-screen parse error and then dropped the entire config, so accounts got stock nushell: no theme, no vi mode, no history settings, no completions, no aliases, and the banner config.nu turns off. The error was the visible part; the discarded config was the cost. `source` is a parse-time keyword. It takes a path known while the file is parsed, so `let cache = ...; source $cache` cannot work, and neither can gating it behind a runtime `if`: the whole block is parsed either way. Both integration blocks were written that way. The aliases line below them is the same keyword with `$nu`, a parse-time constant, which is why that one was fine and shows the difference. starship and zoxide now come from /usr/share/nushell/vendor/autoload/, generated in the image. nushell reads its autoload directories itself, so nothing needs sourcing, no per-user cache is written, and the prompt is there on first login. Generating at shell start instead would miss that first login: the autoload directories are read before env.nu's writes land, as checked against 0.112.2. With the config loading, nushell then rejected two options it had never reached: filesize.metric and filesize.format, whose shape changed. The intent was metric units, which `unit` carries now. Verified on a running install: a fresh shell starts silent, show_banner is false, edit_mode is vi, `ll` resolves, history is sqlite. Also drops the em dashes from user-visible copy: the install view's title, which the console could not draw until the colour fix and which reads "install (lsblk), step 1 of 4" now, three pkg.rs messages, and the shipped dotfile comments.
Author: Max Johnson <me@maxj.phd> · 2026-07-21 22:48 UTC
Signed with PGP, not checked
Commit: a7e070eb239284945b350c1d603c7859c3ead0bb
Parent: 52a9f01
8 files changed, +62 insertions, -41 deletions
@@ -274,6 +274,35 @@
274 274 # /etc/shells". bash stays at /bin/sh and /bin/bash for scripts.
275 275 RUN echo /usr/bin/nu >> /etc/shells
276 276
277 + # =====================================================================
278 + # Shell integrations, generated once into nushell's system autoload dir.
279 + # =====================================================================
280 + # nushell sources every .nu in its autoload directories at startup, and
281 + # /usr/share/nushell/vendor/autoload is the first one it looks in. Files
282 + # put here are picked up by every account with no per-user setup, which
283 + # is what an image with a read-only /usr wants.
284 + #
285 + # Generated at build time rather than on first shell start, for two
286 + # reasons that only showed up when this was tried the other way round.
287 + # `source` is a nushell *parse-time* keyword: it takes a path known while
288 + # the file is being parsed, so `let cache = ...; source $cache` fails with
289 + # "Value is not a parse-time constant" and, because the failure is a parse
290 + # error, nushell discards the whole config file. The account then gets
291 + # stock nushell: no theme, no vi mode, no history settings, no aliases,
292 + # and the banner that config.nu turns off. The second reason is ordering:
293 + # the autoload directories are read before env.nu's writes land, so a file
294 + # generated at shell start is not seen until the *next* shell.
295 + #
296 + # Guarded on the binaries so this step does not fail the build if either
297 + # package leaves the image; nushell simply autoloads whatever is here.
298 + RUN mkdir -p /usr/share/nushell/vendor/autoload \
299 + && if command -v starship >/dev/null; then \
300 + starship init nu > /usr/share/nushell/vendor/autoload/starship.nu; \
301 + fi \
302 + && if command -v zoxide >/dev/null; then \
303 + zoxide init nushell > /usr/share/nushell/vendor/autoload/zoxide.nu; \
304 + fi
305 +
277 306 # =====================================================================
278 307 # Config tree — the etc/ and usr/ trees in the repo map 1:1 into
279 308 # the image. Per-user defaults live under etc/skel/.config/ and
@@ -2,9 +2,10 @@
2 2 //!
3 3 //! Every command the console runs passes through here, which is what makes the
4 4 //! log pane's promise honest: the pane cannot show a command the console did
5 - //! not run, and it cannot run one it does not show. docs/CONSOLE.md — "the
6 - //! console is not trying to hide the CLI, it's trying to make the CLI
7 - //! approachable" — is enforced structurally rather than by remembering to log.
5 + //! not run, and it cannot run one it does not show. The promise docs/CONSOLE.md
6 + //! makes, "the console is not trying to hide the CLI, it's trying to make the
7 + //! CLI approachable", is enforced structurally rather than by remembering to
8 + //! log.
8 9 //!
9 10 //! [`Invocation`] is the common case and [`Effect`] is the general one. Almost
10 11 //! everything the console does is running someone else's CLI, but not quite
@@ -1590,7 +1590,7 @@
1590 1590 impl View for InstallView {
1591 1591 fn title(&self) -> String {
1592 1592 format!(
1593 - "install ({}) — step {} of {}: {}",
1593 + "install ({}), step {} of {}: {}",
1594 1594 self.backend.name(),
1595 1595 self.steps.current() + 1,
1596 1596 self.steps.len(),
@@ -140,8 +140,8 @@
140 140 ///
141 141 /// `Other` keeps the backend's own word rather than collapsing it. Podman's
142 142 /// state vocabulary gains members (`paused`, `stopping`, `exited`), and showing
143 - /// an unfamiliar one beats mapping it to "unknown" — same reasoning as `mesh`
144 - /// reporting `BackendState` verbatim.
143 + /// an unfamiliar one beats mapping it to "unknown", the same reasoning as
144 + /// `mesh` reporting `BackendState` verbatim.
145 145 #[derive(Debug, Clone, PartialEq, Eq)]
146 146 pub enum BoxState {
147 147 Running,
@@ -1245,7 +1245,7 @@
1245 1245 // check and same wording as `toggle`, for the same reason: the state
1246 1246 // is the console's fact, not the backend's.
1247 1247 if boxed.state == BoxState::Absent {
1248 - anyhow::bail!("{} does not exist yet — press c to create it", boxed.name);
1248 + anyhow::bail!("{} does not exist yet; press c to create it", boxed.name);
1249 1249 }
1250 1250 let (name, spec) = self.declared_spec(boxed, "exported")?;
1251 1251 for effect in self.backends[index].export(name, spec)? {
@@ -1272,7 +1272,7 @@
1272 1272 // command that can only fail. The state is the console's fact to
1273 1273 // check, not the backend's.
1274 1274 if boxed.state == BoxState::Absent {
1275 - anyhow::bail!("{} does not exist yet — press c to create it", boxed.name);
1275 + anyhow::bail!("{} does not exist yet; press c to create it", boxed.name);
1276 1276 }
1277 1277 let backend = &self.backends[index];
1278 1278 let Some(invocation) = backend.stop(boxed).or_else(|| backend.start(boxed)) else {
@@ -1322,7 +1322,7 @@
1322 1322 };
1323 1323 if row.boxed.state == BoxState::Absent {
1324 1324 self.error = Some(format!(
1325 - "{} does not exist yet — press c to create it",
1325 + "{} does not exist yet; press c to create it",
1326 1326 row.boxed.name
1327 1327 ));
1328 1328 return Flow::Continue;
@@ -68,9 +68,9 @@
68 68 /// The furthest step the user has reached, current or behind them.
69 69 ///
70 70 /// For the step indicator: steps at or below this are answered, the rest
71 - /// are not yet visited. Nothing draws that indicator yet — the title says
72 - /// "step 2 of 4" instead — so this is the last of the type still waiting on
73 - /// its consumer, and the allow comes off with it.
71 + /// are not yet visited. Nothing draws that indicator yet, since the title
72 + /// says "step 2 of 4" instead, so this is the last of the type still
73 + /// waiting on its consumer, and the allow comes off with it.
74 74 #[allow(dead_code)]
75 75 pub const fn furthest(&self) -> usize {
76 76 self.furthest
@@ -4,7 +4,7 @@
4 4 # reconsider whether the alias earned its place.
5 5
6 6 # -------------------------------------------------------------------
7 - # ls variants — nu's `ls` is already structured; these add flags
7 + # ls variants. nu's `ls` is already structured; these add flags
8 8 # -------------------------------------------------------------------
9 9 alias ll = ls -la
10 10 alias la = ls -a
@@ -51,4 +51,4 @@
51 51 # -------------------------------------------------------------------
52 52 # alias net = alloy net
53 53 # alias audio = alloy audio
54 - # — deferred; enable once alloy console v0.5 ships.
54 + # Deferred; enable once alloy console v0.5 ships.
@@ -1,13 +1,13 @@
1 1 # Alloy Nushell config
2 2 #
3 - # Loaded on every nu shell start. Configures nu's own behavior — table style,
3 + # Loaded on every nu shell start. Configures nu's own behavior: table style,
4 4 # history, completions, keybindings, color palette for nu's own output.
5 5 #
6 6 # Aliases live in aliases.nu (sourced at the bottom).
7 7 # Environment lives in env.nu (loaded before this file by nu itself).
8 8
9 9 # -------------------------------------------------------------------
10 - # Alloy color palette — Akari Dawn (default light).
10 + # Alloy color palette, Akari Dawn (default light).
11 11 # Source of truth: makeover's themes/akari-dawn.toml.
12 12 # -------------------------------------------------------------------
13 13 let alloy = {
@@ -90,9 +90,13 @@
90 90 use_ls_colors: true
91 91 }
92 92
93 + # `metric: true` and `format: "auto"` were this record's shape in an
94 + # older nushell and are rejected by 0.112 as unknown options. The intent
95 + # was metric units, which `unit` now carries.
93 96 filesize: {
94 - metric: true
95 - format: "auto"
97 + unit: "metric"
98 + show_unit: true
99 + precision: 1
96 100 }
97 101
98 102 cursor_shape: {
@@ -172,30 +176,17 @@
172 176 }
173 177
174 178 # -------------------------------------------------------------------
175 - # Integrations — conditionally loaded if the tool is present
179 + # Integrations
176 180 # -------------------------------------------------------------------
177 -
178 - # starship prompt
179 - if (which starship | is-not-empty) {
180 - let starship_cache = ($nu.cache-dir | path join "starship-init.nu")
181 - if not ($starship_cache | path exists) {
182 - mkdir ($starship_cache | path dirname)
183 - starship init nu | save -f $starship_cache
184 - }
185 - source $starship_cache
186 - }
187 -
188 - # zoxide directory jumper
189 - if (which zoxide | is-not-empty) {
190 - let zoxide_cache = ($nu.cache-dir | path join "zoxide-init.nu")
191 - if not ($zoxide_cache | path exists) {
192 - mkdir ($zoxide_cache | path dirname)
193 - zoxide init nushell | save -f $zoxide_cache
194 - }
195 - source $zoxide_cache
196 - }
181 + # starship and zoxide are not set up here. The image generates their init
182 + # files into /usr/share/nushell/vendor/autoload/, which nushell sources by
183 + # itself before this file runs. See the Containerfile for why: `source` is
184 + # a parse-time keyword, so it cannot take a path a `let` computed and it
185 + # cannot be gated behind a runtime `if`. Written that way it was a parse
186 + # error, and a parse error costs the whole file, not just its own line.
197 187
198 188 # -------------------------------------------------------------------
199 - # Aliases (small, curated — see aliases.nu)
189 + # Aliases (small, curated; see aliases.nu)
200 190 # -------------------------------------------------------------------
191 + # `$nu` is a parse-time constant, so this path is one `source` can take.
201 192 source ($nu.default-config-dir | path join "aliases.nu")
@@ -50,7 +50,7 @@
50 50 # a comment for future-Alloy debugging.
51 51
52 52 # -------------------------------------------------------------------
53 - # Cursor — Bibata Modern Classic, size 24. Also declared in
53 + # Cursor: Bibata Modern Classic, size 24. Also declared in
54 54 # ~/.icons/default/index.theme and the GTK settings.ini; env vars here
55 55 # cover apps and toolkits that read XCURSOR_* directly.
56 56 # -------------------------------------------------------------------