linkmarks_cli/cmd/completions.rs
1//! `linkmarks completions <shell>` — emit a shell completion script.
2//!
3//! Operator-side convenience: instead of pinning a static file in
4//! the repo (which goes stale the moment we add or rename a flag)
5//! we regenerate from the live `Cli` parser at install time. The
6//! script is printed to stdout so the caller can pipe it anywhere.
7//!
8//! Typical install flows:
9//!
10//! ```bash
11//! # bash
12//! linkmarks completions bash > ~/.local/share/bash-completion/completions/linkmarks
13//!
14//! # zsh (add to fpath, then `compinit`)
15//! linkmarks completions zsh > "${fpath[1]}/_linkmarks"
16//!
17//! # fish
18//! linkmarks completions fish > ~/.config/fish/completions/linkmarks.fish
19//!
20//! # powershell
21//! linkmarks completions powershell > "$HOME\Documents\PowerShell\Completion\linkmarks.ps1"
22//! ```
23
24use anyhow::Result;
25use clap::Args;
26use clap_complete::Shell;
27use std::io;
28
29/// Arguments for `linkmarks completions`.
30#[derive(Args, Debug)]
31pub struct CompletionsArgs {
32 /// Which shell to emit the completion script for.
33 #[arg(value_enum)]
34 pub shell: Shell,
35}
36
37/// Print a completion script for `args.shell` to stdout.
38///
39/// Returns `exit_codes::OK` on success. We do not need the resolved
40/// `Paths` for completions — the script is regenerated from the live
41/// `Cli` parser at install time, so there is no store or config IO.
42pub fn run(args: CompletionsArgs, _paths: crate::Paths) -> Result<i32> {
43 let mut cmd = crate::build_cli();
44 let bin_name = cmd.get_name().to_string();
45 // `clap_complete::generate` returns `()`. A write failure to a
46 // piped target surfaces as a broken pipe on the next write; for
47 // operator-side completion install that's acceptable (the shell
48 // completion will simply be truncated and the user will see
49 // an obvious install failure on the `source` step).
50 clap_complete::generate(args.shell, &mut cmd, bin_name, &mut io::stdout().lock());
51 Ok(crate::exit_codes::OK)
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use clap::{CommandFactory, Parser};
58
59 /// Parse the canonical CLI and run `clap_complete::generate` for
60 /// the given shell into a `Vec<u8>`. We intentionally bypass the
61 /// `run` entry point so the test does not touch stdout (which
62 /// would race with `cargo test`'s own capture).
63 fn emit(shell: Shell) -> Vec<u8> {
64 let mut cmd = <crate::Cli as CommandFactory>::command();
65 let bin = cmd.get_name().to_string();
66 let mut buf: Vec<u8> = Vec::new();
67 clap_complete::generate(shell, &mut cmd, bin, &mut buf);
68 buf
69 }
70
71 #[test]
72 fn bash_output_contains_subcommands_and_bin_name() {
73 let script = String::from_utf8(emit(Shell::Bash)).expect("utf8");
74 // bash completion uses `_comp_linkmarks` as the function
75 // prefix derived from the bin name; the case statement
76 // should mention our subcommands (we don't pin names — the
77 // surface is owned by clap, this test is structural).
78 assert!(script.contains("linkmarks"), "bin name missing");
79 assert!(
80 script.contains("init") || script.contains("list") || script.contains("tui"),
81 "expected at least one subcommand literal"
82 );
83 }
84
85 #[test]
86 fn zsh_output_is_a_compdef_script() {
87 let script = String::from_utf8(emit(Shell::Zsh)).expect("utf8");
88 // zsh completions end with a `#compdef <bin>` footer so they
89 // can be sourced via `compinit` from $fpath.
90 assert!(
91 script.contains("#compdef"),
92 "zsh script missing #compdef footer"
93 );
94 }
95
96 #[test]
97 fn fish_output_uses_complete_command() {
98 let script = String::from_utf8(emit(Shell::Fish)).expect("utf8");
99 // Fish completions register via `complete -c <bin> ...`.
100 assert!(
101 script.contains("complete -c linkmarks"),
102 "fish script missing `complete -c linkmarks` registration"
103 );
104 }
105
106 #[test]
107 fn powershell_output_uses_register_argumentcompleter() {
108 let script = String::from_utf8(emit(Shell::PowerShell)).expect("utf8");
109 // PowerShell completion is wired via Register-ArgumentCompleter.
110 assert!(
111 script.contains("Register-ArgumentCompleter"),
112 "powershell script missing Register-ArgumentCompleter"
113 );
114 }
115
116 #[test]
117 fn elvish_output_uses_edit_completion_arg_completer() {
118 let script = String::from_utf8(emit(Shell::Elvish)).expect("utf8");
119 // Elvish completion uses `edit:completion:arg-completer`.
120 assert!(
121 script.contains("edit:completion:arg-completer"),
122 "elvish script missing completion binding"
123 );
124 }
125
126 #[test]
127 fn all_supported_shells_appear_in_subcommand_help() {
128 // Smoke: `--help` against the `completions` subcommand must
129 // list every shell we expose. If clap renames or drops one
130 // this test will surface it.
131 let cli = crate::Cli::try_parse_from(["linkmarks", "completions", "--help"]);
132 let help = format!("{:?}", cli).to_lowercase();
133 assert!(help.contains("bash"));
134 assert!(help.contains("zsh"));
135 assert!(help.contains("fish"));
136 assert!(help.contains("powershell"));
137 assert!(help.contains("elvish"));
138 }
139}