Skip to main content

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}