feat: install tab completion for bash, zsh, and fish (#16)

`completions install` generates the script from the same clap definition
the binary parses with, so it cannot drift from `--help`, and writes it
where each shell already looks. fish scans its own completions directory
unprompted; bash and zsh get one marked `source` line appended to their rc
file, because nothing puts a per-user directory on their search path by
default. `completions uninstall` takes back exactly those lines and leaves
the rest of the file alone.
This commit is contained in:
Michel de Graaf
2026-07-26 12:18:53 +02:00
committed by GitHub
parent 72f2e31a2c
commit 534b09a20a
13 changed files with 583 additions and 10 deletions
+53 -1
View File
@@ -93,8 +93,15 @@ fn help_lists_every_scriptable_command() {
assert_eq!(code(&output), EXIT_OK);
let help = stdout(&output);
for command in [
"search", "download", "get", "browse", "room", "message", "portmap",
"search",
"download",
"get",
"browse",
"room",
"message",
"portmap",
"skills",
"completions",
] {
assert!(help.contains(command), "--help should mention {command}");
}
@@ -333,6 +340,51 @@ fn listing_skills_reports_without_writing_anything() {
);
}
/// fish is the shell the whole round trip can be driven for on every platform:
/// its completions directory hangs off `XDG_CONFIG_HOME`, so nothing outside
/// the scratch directory is read or written. bash and zsh also append a line
/// to a real rc file under `$HOME`, which is not a thing to point a test at on
/// Windows — the unit tests cover that half.
#[test]
fn installing_completions_is_repeatable_and_reversible() {
let target = Scratch::new("completions");
let installed = target
.path()
.join("fish")
.join("completions")
.join("soulseek-rs.fish");
let completions = |verb: &str| {
command(&["--json", "completions", verb, "--shell", "fish"])
.env("XDG_CONFIG_HOME", target.path())
.output()
.expect("the binary should run")
};
let first = completions("install");
assert_eq!(code(&first), EXIT_OK, "stderr: {}", stderr(&first));
assert_eq!(action(&first), "installed");
let script = std::fs::read_to_string(&installed).expect("a fish script");
assert!(script.contains("complete -c soulseek-rs"));
assert!(
script.contains("-l download-dir"),
"flags are completed too"
);
assert_eq!(
action(&completions("install")),
"unchanged",
"reinstalling is how you update"
);
let removed = completions("uninstall");
assert_eq!(action(&removed), "removed");
assert!(!installed.exists());
let again = completions("uninstall");
assert_eq!(code(&again), EXIT_OK, "removing nothing is not a failure");
assert_eq!(action(&again), "absent");
}
// ---------------------------------------------------------------------------
// Server required (soulfind)
// ---------------------------------------------------------------------------