max / alloy
- Co-Authored-By
- Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 files changed,
+67 insertions,
-8 deletions
| @@ -340,9 +340,16 @@ | |||
| 340 | 340 | # "Value is not a parse-time constant" and, because the failure is a parse | |
| 341 | 341 | # error, nushell discards the whole config file. The account then gets | |
| 342 | 342 | # stock nushell: no theme, no vi mode, no history settings, no aliases, | |
| 343 | - | # and the banner that config.nu turns off. The second reason is ordering: | |
| 344 | - | # the autoload directories are read before env.nu's writes land, so a file | |
| 345 | - | # generated at shell start is not seen until the *next* shell. | |
| 343 | + | # and the banner that config.nu turns off. The second reason is cost: | |
| 344 | + | # each init spawns a process to re-render output that cannot change | |
| 345 | + | # between shells on a read-only /usr. | |
| 346 | + | # | |
| 347 | + | # Load order, measured on nushell 0.112.2 rather than assumed: env.nu, | |
| 348 | + | # then config.nu, then the vendor autoload dirs, then the user's. The | |
| 349 | + | # autoload pass running *after* config.nu is load-bearing for the | |
| 350 | + | # hand-authored direnv.nu that the config tree below drops into this same | |
| 351 | + | # directory. config.nu assigns `$env.config` wholesale, hooks included, so | |
| 352 | + | # a hook registered any earlier would be discarded before it ever ran. | |
| 346 | 353 | # | |
| 347 | 354 | # Guarded on the binaries so this step does not fail the build if either | |
| 348 | 355 | # package leaves the image; nushell simply autoloads whatever is here. |
| @@ -35,7 +35,9 @@ | |||
| 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, 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. | |
| 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. Alloy therefore writes that one by hand, against `direnv export json`, and ships it into the same autoload directory: [`usr/share/nushell/vendor/autoload/direnv.nu`](../usr/share/nushell/vendor/autoload/direnv.nu). Generated or authored, every integration lands in one place. The rest of the long tail is handled the same way, added on demand rather than preemptively. | |
| 39 | + | ||
| 40 | + | Two things that directory's ordering settles. Nushell reads it *after* `config.nu`, which is what lets an autoload file register a hook: `config.nu` assigns `$env.config` wholesale and would discard anything registered earlier. And `$env.PATH` is a list in nu while direnv reports PATH colon-joined, so the hook converts that one variable rather than loading the diff verbatim. | |
| 39 | 41 | - **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 | 42 | - **`export`.** `$env.FOO = "bar"` in nu, `export FOO=bar` in bash. Both work in their respective contexts. | |
| 41 | 43 | - **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. |
| @@ -190,7 +190,7 @@ | |||
| 190 | 190 | ||
| 191 | 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 | 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. | |
| 193 | + | **The nu integration is Alloy-authored**, at [`usr/share/nushell/vendor/autoload/direnv.nu`](../usr/share/nushell/vendor/autoload/direnv.nu). direnv emits no nu init for the build-time trick used on starship and zoxide to call, so the hook is written against `direnv export json`, a supported export target. It registers a `pre_prompt` hook and a `direnv-reload` command for use by hand after editing an `.envrc`. PATH is applied apart from the rest of the diff, since nushell holds it as a list and direnv reports it colon-joined. | |
| 194 | 194 | ||
| 195 | 195 | ## Utility defaults: content viewers | |
| 196 | 196 |
| @@ -161,6 +161,11 @@ | |||
| 161 | 161 | shape_garbage: { fg: $alloy.accent_error attr: b } | |
| 162 | 162 | } | |
| 163 | 163 | ||
| 164 | + | # This record is assigned wholesale, so these lists are a reset, not a | |
| 165 | + | # starting point: a hook registered before this file runs is discarded | |
| 166 | + | # here. Nothing does, and nothing should. The autoload directory is | |
| 167 | + | # read after this file (direnv's hook lives there) and is the place to | |
| 168 | + | # add one, per-user hooks go below any of this in your own config. | |
| 164 | 169 | hooks: { | |
| 165 | 170 | pre_prompt: [] | |
| 166 | 171 | pre_execution: [] | |
| @@ -178,9 +183,11 @@ | |||
| 178 | 183 | # ------------------------------------------------------------------- | |
| 179 | 184 | # Integrations | |
| 180 | 185 | # ------------------------------------------------------------------- | |
| 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 | |
| 186 | + | # starship, zoxide and direnv are not set up here. They live in | |
| 187 | + | # /usr/share/nushell/vendor/autoload/, which nushell sources by itself | |
| 188 | + | # after this file runs (the first two generated at build time from their | |
| 189 | + | # own init output, direnv hand-authored because it has no nu init to | |
| 190 | + | # generate). See the Containerfile for why: `source` is | |
| 184 | 191 | # a parse-time keyword, so it cannot take a path a `let` computed and it | |
| 185 | 192 | # cannot be gated behind a runtime `if`. Written that way it was a parse | |
| 186 | 193 | # error, and a parse error costs the whole file, not just its own line. |
| @@ -1,0 +1,43 @@ | |||
| 1 | + | # direnv, wired into nushell by hand. | |
| 2 | + | # | |
| 3 | + | # The other integrations in this directory are generated at image build time | |
| 4 | + | # from each tool's own init output (see the Containerfile). direnv has no | |
| 5 | + | # such output to generate: as of 2.35.0 its `hook` and `export` targets are | |
| 6 | + | # bash, zsh, fish, elvish, tcsh, murex, pwsh, vim, systemd, gha, json and | |
| 7 | + | # gzenv, with no nu among them. `json` is the seam this file is built on, | |
| 8 | + | # and it is a supported target rather than a workaround: `direnv export | |
| 9 | + | # json` prints the pending environment diff as a JSON object, which is the | |
| 10 | + | # shape nushell wants anyway. | |
| 11 | + | # | |
| 12 | + | # In that object a value is the variable's new value and a null means unset. | |
| 13 | + | # direnv's own status lines ("direnv: loading .envrc") go to stderr, so they | |
| 14 | + | # reach the terminal untouched while stdout stays parseable. | |
| 15 | + | # | |
| 16 | + | # This file lands in the vendor autoload directory, which nushell reads | |
| 17 | + | # after config.nu. That ordering is what makes the hook below stick: | |
| 18 | + | # config.nu assigns $env.config wholesale, hooks included, so anything | |
| 19 | + | # registered before it would be discarded. | |
| 20 | + | ||
| 21 | + | # Apply direnv's pending environment diff to this shell. Called from the | |
| 22 | + | # pre_prompt hook below, and useful by hand after editing an .envrc. | |
| 23 | + | def --env direnv-reload [] { | |
| 24 | + | if (which direnv | is-empty) { return } | |
| 25 | + | ||
| 26 | + | let diff = (direnv export json | str trim) | |
| 27 | + | if ($diff | is-empty) { return } | |
| 28 | + | let diff = ($diff | from json) | |
| 29 | + | ||
| 30 | + | # PATH is handled apart from the rest. nushell keeps it as a list and | |
| 31 | + | # direnv reports it the way the rest of the world writes it, as one | |
| 32 | + | # colon-joined string, so loading it verbatim would leave the shell with | |
| 33 | + | # a PATH it cannot search. | |
| 34 | + | $diff | reject --optional PATH | load-env | |
| 35 | + | if "PATH" in ($diff | columns) { | |
| 36 | + | let raw = ($diff | get PATH) | |
| 37 | + | $env.PATH = (if $raw == null { [] } else { $raw | split row (char esep) }) | |
| 38 | + | } | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | $env.config.hooks.pre_prompt = ( | |
| 42 | + | $env.config.hooks.pre_prompt | default [] | append {|| direnv-reload } | |
| 43 | + | ) |