feat(hook): print the snippet a shell evaluates to follow an apply - #80
Merged
Merged
Conversation
A target whose output a shell sources cannot be reached by `reload`. The shell that has the wrong colours is vanadis's parent, and no process replaces its parent's image, so `exec zsh` is unreachable however it is spelled. fzf is the entry `docs/config.md` names for this, and `LS_COLORS`, `GREP_COLORS` and a pager named through a variable are the same shape. `vanadis hook <shell>` prints a snippet to `eval`, the way direnv, mise, starship and zoxide do. The snippet sources the outputs of the targets marked `shell = "zsh"` and registers a prompt hook that sources each again when its own mtime has moved. The state file is not the signal: `apply --only nvim` moves it without touching a shell target, and watching it would put `$XDG_STATE_HOME` and a file format into a snippet that otherwise needs only a path. The decisions the document records: a per-target `shell` key rather than a top-level table, because a table naming targets is a second place a name is written; paths baked into the snippet rather than a config parse per prompt, with re-`eval` as the answer to staleness; zsh and fish detecting a change with no fork, and bash spending one `stat`, with the `stat` chosen when the snippet is generated rather than branched at runtime; a stamp file compared with `-nt` rejected for bash because cleaning it up needs the one `EXIT` trap the user also has; one hook after a second `eval`, and the exit status restored so a prompt that shows it is not lied to. The zsh row of the `reload` table now says what does reach it. The same row in the shipped skill, and the CI that runs the snippet under each shell, land with the implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wxMB7uC4uxSJ2dfHi8ucf
The state file as the signal, a top-level table, and a config read at prompt time are each argued where they come up. Restating them under Rejected alternatives said the same thing twice and made the section long enough to read as the point of the document. What is left is the five the body never raises. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wxMB7uC4uxSJ2dfHi8ucf
`docs/hook.md` decides this. A target whose output a shell sources cannot be reached by `reload`, because the shell holding the stale colours is vanadis's parent. `vanadis hook <zsh|fish|bash>` prints a snippet to evaluate: it sources the output of every target marked `shell`, and registers a prompt hook that sources it again when that file's mtime moves. The snippet holds resolved paths as literals and never runs vanadis again, so a prompt costs no process and a shell that has lost vanadis from `$PATH` keeps following. zsh reads the mtime with `zstat` and fish with `path mtime`, neither forking; bash has no such builtin and spends one `stat`, written in at generation time from the system vanadis is on rather than branched at runtime. Three invariants hold in every shell. A second evaluation leaves one hook registered, by `add-zsh-hook`, by redefining the fish handler, and by a `PROMPT_COMMAND` append guarded on the function name. The hook returns the exit status it found, so a prompt showing `$?` is not lied to. Every name it defines is prefixed `_vanadis_`, and the zsh helper is emulated so a user's `setopt` cannot change what it means. `hook` reads `config.toml` and nothing else: no theme is resolved, no state file is read, `themes/` is not scanned. Every failure leaves stdout empty, so a config that will not load cannot leave half a snippet in the user's shell. A config that loads and names no target for that shell reports on stderr and exits zero, because the shell it is starting is otherwise fine. `tests/hook.rs` evaluates what is printed in a real interactive shell and reads what that shell prints back, because asserting the snippet as a string would establish only that the generator did not change. CI installs the three shells, and the tests refuse to skip a missing shell when `CI` is set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wxMB7uC4uxSJ2dfHi8ucf
fish emits `fish_prompt` only while drawing a prompt, and it draws none when its input is a pipe, `-i` or not. The three tests that need a prompt therefore passed nothing to the hook and read the colours the evaluation itself had sourced. zsh and bash run precmd and `PROMPT_COMMAND` either way, which is why they were green here and on this machine. `script` allocates a terminal and connects the shell to it, so fish runs its reader the way it does for a person. Only fish gets one: a terminal makes the other two draw prompts into the output for nothing. What comes back carries carriage returns, so they are dropped before the lines are read. The handler probe falls back to `functions --handlers`, because the type filter is the part of that command this has not run anywhere yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wxMB7uC4uxSJ2dfHi8ucf
A terminal writes the prompt and the echo of what was typed around what the script prints, so under fish the `bg=` marker is no longer at the start of its line and the filter collected nothing. The two tests that used `contains` were blind to this and passed; the one that compared the whole sequence failed. Every test now compares the sequence of colours, found by scanning for `bg=` followed by one. The echo of `bg=$VANADIS_BG` does not match it, which is what makes the marker readable from the middle of a line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wxMB7uC4uxSJ2dfHi8ucf
The three shells are not interchangeable to drive. fish emits `fish_prompt` only while drawing a prompt and draws none for a pipe, so a piped fish answers every question about a prompt with the state the evaluation itself left, and answers it green. This cost two CI rounds to find and would cost them again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wxMB7uC4uxSJ2dfHi8ucf
#79 was squashed into main, so `docs/hook.md` arrives from both sides as an add. The branch holds the same document plus the two paragraphs this implementation added to it: the fish spelling of the eval line, and why the fish CI run needs a terminal. Resolved to the branch's copy, which is main's copy with those two additions and nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011wxMB7uC4uxSJ2dfHi8ucf
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
docs/hook.md, which #79 decides. Stacked on #79 — the base istorabit/docs/hook, so this diff is the implementation only. Merge #79 first.Part of #77.
What lands
vanadis hook <zsh|fish|bash>prints a snippet to evaluate. It sources the output of everytarget marked
shell, and registers a prompt hook that sources it again when that file's mtimemoves.
shellis a new[[targets]]key; a value that names none of the three is a configerror rather than a target that silently never sources.
zstatfromzsh/statadd-zsh-hook precmdpath mtime--on-event fish_promptstat, written in at generation timePROMPT_COMMANDhookreadsconfig.tomland nothing else: no theme resolved, no state file read,themes/not scanned. Every failure leaves stdout empty, so a config that will not load cannot leave half
a snippet in the user's shell.
How it is tested
tests/hook.rsstarts a real interactive shell, feeds it the snippet, and reads what the shellprints back. Asserting the snippet as a string would establish that the generator did not change
and nothing about whether zsh can parse the result. Five behaviours per shell: sources at
evaluation, sources again after a
cycle, one hook after a second evaluation, the exit statussurvives, and an
apply --onlynaming no shell target re-sources nothing.CI installs zsh and fish. The tests skip a shell that is not installed and refuse to skip when
CIis set, so deleting the install step fails the run instead of quietly passing it.zsh and bash were also driven by hand on this machine before the tests were written. fish is not
installed here, so the fish snippet is first executed by CI on this PR.
Also in this PR
docs/examples/config.toml: the zsh palette target, the reference config's one target readfrom the environment, now carries
shell = "zsh".skills/vanadis/references/reload.mdandSKILL.md: the fzf row now points at the hookinstead of ending at
exec zsh, with a section on marking a target and the rc line. Held backfrom docs(hook): decide how a sourced target follows an apply #79 because the skill ships to users and should not name a command a released binary does
not have.
docs/hook.md: the fish spelling,vanadis hook fish | source, which is what fish users writefor every tool in this shape.
🤖 Generated with Claude Code
https://claude.ai/code/session_011wxMB7uC4uxSJ2dfHi8ucf