feat(cli): tab completion for bash, zsh and fish - #228
Merged
Conversation
…command table `loopky completion bash|zsh|fish` prints a completion script on stdout, for `eval` or for a file in the directory the shell already searches. Generated from one table (`CommandSurface.kt`) rather than checked in as three scripts. A completion cannot fail loudly — it offers a flag the parser refuses, or stays silent about one that works, and the user shrugs and types it out — so three files that were right on the day they were written would go wrong at the first flag added afterwards with nothing reporting it. `CompletionTest` holds the table against `Args.SWITCHES` and against the usage block, in both directions, so a flag that nothing describes fails the build instead. Four things not to undo: - **Nothing it offers touches the network.** Completing a deck id after `deck show` would put a homeserver round trip on a keypress — a tab that hangs for a second on a good network and forever on an expired session, in the one place the user cannot interrupt without losing the line. Deck ids are `Operand.Opaque`: zsh and fish name the word and offer nothing. A test asserts no script contains an address. - **`completion` runs before Koin, beside `update`.** Generating a static string must not depend on `libpubkycore` loading, so a host outside the shipped matrix can still install completions. - **The update check is off for this verb.** The script's documented home is a line in `.bashrc`, which puts this command in the path of every new shell; the check is awaited before exit, and a terminal that opens a second slower once a day is a bug nobody would trace back to a completion script. - **Each script walks the line rather than counting words.** `loopky --env staging deck create` is legal, and taking "the first one or two words" reads `staging` as the command. fish's own `__fish_seen_subcommand_from` has the same flaw one layer up — it matches a word anywhere on the line, an option's *value* included — so the fish script computes the command itself. `--yes` and `--force` stay parseable and are deliberately not offered: nothing here prompts, so advertising them would describe a confirmation step that does not exist. `UNOFFERED_SWITCHES` records that as a decision the test can tell from an omission. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0116NZ92Pe7aGQpqtiRkP4Ye
…pts parse The `.deb` and the Homebrew formula now ship all three completions, **generated by running the binary they are packaging** rather than copied from the tree — the scripts describe one specific build's surface, and a checked-in copy would drift the first time a flag is added. The `curl` installer only *says* the command exists. Enabling completions means writing to a shell rc file or a system directory, and an installer that edits `~/.bashrc` behind you is one you cannot cleanly undo — on a box where the whole install is "copy one file", that is the wrong trade. CI syntax-checks all three, which is the one thing a unit test cannot do: `CompletionTest` asserts what is *in* each script, and nothing in Kotlin notices an unbalanced quote or a `case` missing its `esac`. That failure lands in the user's `.bashrc` as a syntax complaint about a file nobody wrote. `-n` is parse-only, so the step needs no session and no network; the bash script is additionally sourced and `complete -p` asserted, since a script that parses can still define nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0116NZ92Pe7aGQpqtiRkP4Ye
Architecture §13.13 for the reasoning — why it is generated rather than shipped, why nothing it offers reaches the network, why it runs before Koin with the update check off, and why each script walks the line instead of counting words. §13.14/§13.15 renumbered. `journeys/RESULTS.md` for the run: bash locally, zsh and fish in containers, driven rather than built. That is what caught the zsh bug — the trailing `_loopky "$@"` is right for an autoloaded `$fpath/_loopky` and wrong for the `eval` install the same file documents, where it runs the function outside any completion context and prints "command not found" for `_describe` on every new shell. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0116NZ92Pe7aGQpqtiRkP4Ye
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.
loopkyhad no tab completion. It does now, for bash, zsh and fish.Commands, subcommands, every flag, and the values of the flags with a closed set —
--env,--separator, the shell name.importcompletes filenames; so do--from-fileand--qr-out.The
.deband the Homebrew formula install all three for you.Generated from one table, not checked in as three scripts
A completion cannot fail loudly. It offers a flag the parser refuses, or stays silent about one
that works, and the user shrugs and types it out — so three files that were right the day they
were written go wrong at the first flag added afterwards, with nothing reporting it.
CommandSurface.ktdescribes the commands, their options and what each option accepts; the threegenerators read that and nothing else.
CompletionTestholds it againstArgs.SWITCHESandagainst the usage block in both directions, so a flag that nothing describes fails the build. The
.deband the formula generate their copies by running the binary they are packaging, for thesame reason.
Four things not to undo
deck showwould put ahomeserver round trip on a keypress — a tab that hangs for a second on a good network and
forever on the hourly-expired session (Every homeserver write dies on /session with a raw transport error, and publish reports it as "check your connection" #165), in the one place you cannot interrupt without
losing the line. Deck ids are
Operand.Opaque: zsh and fish name the word and offer nothing. Atest asserts no script contains an address.
completionruns before Koin, besideupdate. Generating a static string must not dependon
libpubkycoreloading, so a host outside the shipped matrix can still install completions..bashrc,which puts this command in the path of every new shell; the check is awaited before exit, and a
terminal that opens a second slower once a day is a bug nobody would trace back here.
loopky --env staging deck createislegal, and "the first one or two words" reads
stagingas the command. fish's own__fish_seen_subcommand_fromhas the same flaw one layer up — it matches a word anywhere on theline, an option's value included — so the fish script computes the command itself.
--yesand--forcestay parseable and are deliberately not offered: nothing here prompts, soadvertising them would describe a confirmation step that does not exist.
UNOFFERED_SWITCHESrecords that as a decision the test can tell apart from an omission.
The installer only mentions the command. Enabling completions means writing to a shell rc file or
a system directory, and an installer that edits
~/.bashrcbehind you is one you cannot cleanlyundo — on a box where the whole install is "copy one file", that is the wrong trade.
Verified by driving the shells, not by building
Full table in
journeys/RESULTS.md. bash locally, zsh and fish inalpine:3.20containers, sinceneither is installed on this machine. Every case passed, including the two that motivated the walk:
loopky --env staging deck <TAB>andloopky card add abc --front x --ba<TAB>.Driving zsh found a bug a green build could not. The script ends with
_loopky "$@", which iscorrect when zsh autoloads it out of
$fpath— and wrong for theeval "$(loopky completion zsh)"install the same file documents, where it runs_loopkyoutside any completion context andprints "command not found" for
_describeon every new shell. It now branches onfuncstack[1]and registers with
compdefon the sourced path; both documented install lines work as written.CI gains a step that runs
bash -n/zsh -n/fish -nover the three scripts and sources thebash one — the one class of failure a Kotlin test cannot see, and the one that lands in a user's
.bashrcas a syntax complaint about a file nobody wrote../gradlew :cli:test :detektAllgreen. Reasoning indocs/Architecture.md§13.13; usage incli/README.md. No app UI is touched, so nojourneys/*.xmlapplies.🤖 Generated with Claude Code
https://claude.ai/code/session_0116NZ92Pe7aGQpqtiRkP4Ye