Skip to main content

tftio_lib/
completions.rs

1//! Shell completion generation module.
2//!
3//! This module provides generic shell completion generation for CLI tools using clap.
4//! It works with any clap `CommandFactory` and generates completions for all major shells.
5
6use clap::CommandFactory;
7use clap_complete::Shell;
8use std::io::{self, Write};
9
10/// Completion content rendered in-memory before it is written anywhere.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct CompletionOutput {
13    /// Installation instructions for the selected shell.
14    pub instructions: String,
15    /// Completion script emitted by `clap_complete`.
16    pub script: String,
17}
18
19/// Render completion installation instructions for a clap-based CLI.
20#[must_use]
21pub fn render_completion_instructions(shell: Shell, bin_name: &str) -> String {
22    match shell {
23        Shell::Bash => format!(
24            "# Shell completion for {bin_name}\n#\n# To enable completions, add this to your shell config:\n#\n#   source <({bin_name} completions bash)\n\n"
25        ),
26        Shell::Zsh => format!(
27            "# Shell completion for {bin_name}\n#\n# To enable completions, add this to your shell config:\n#\n#   {bin_name} completions zsh > ~/.zsh/completions/_{bin_name}\n#   # Ensure fpath includes ~/.zsh/completions\n\n"
28        ),
29        Shell::Fish => format!(
30            "# Shell completion for {bin_name}\n#\n# To enable completions, add this to your shell config:\n#\n#   {bin_name} completions fish | source\n\n"
31        ),
32        Shell::PowerShell => format!(
33            "# Shell completion for {bin_name}\n#\n# To enable completions, add this to your shell config:\n#\n#   {bin_name} completions powershell | Out-String | Invoke-Expression\n\n"
34        ),
35        Shell::Elvish => format!(
36            "# Shell completion for {bin_name}\n#\n# To enable completions, add this to your shell config:\n#\n#   {bin_name} completions elvish | eval\n\n"
37        ),
38        other => format!(
39            "# Shell completion for {bin_name}\n#\n# To enable completions, add this to your shell config:\n#\n#   {bin_name} completions {other}\n\n"
40        ),
41    }
42}
43
44/// Render shell completions fully in memory.
45#[must_use]
46pub fn render_completion<T: CommandFactory>(shell: Shell) -> CompletionOutput {
47    render_completion_from_command(shell, T::command())
48}
49
50/// Render shell completions from a pre-built clap command tree.
51#[must_use]
52pub fn render_completion_from_command(
53    shell: Shell,
54    mut command: clap::Command,
55) -> CompletionOutput {
56    let bin_name = command.get_name().to_string();
57    let mut buffer = Vec::new();
58
59    clap_complete::generate(shell, &mut command, bin_name.clone(), &mut buffer);
60
61    CompletionOutput {
62        instructions: render_completion_instructions(shell, &bin_name),
63        script: String::from_utf8_lossy(&buffer).into_owned(),
64    }
65}
66
67/// Write a previously rendered completion output to a writer.
68///
69/// # Errors
70///
71/// Returns an error if writing fails.
72pub fn write_completion(mut writer: impl Write, output: &CompletionOutput) -> io::Result<()> {
73    writer.write_all(output.instructions.as_bytes())?;
74    writer.write_all(output.script.as_bytes())
75}
76
77/// Generate shell completion scripts for a clap-based CLI.
78///
79/// This function generates shell completions and prints both installation instructions
80/// and the completion script to stdout. It supports bash, zsh, fish, elvish, and `PowerShell`.
81///
82/// # Type Parameters
83/// * `T` - A type that implements `CommandFactory` (typically your clap `Cli` struct)
84///
85/// # Arguments
86/// * `shell` - The shell type to generate completions for
87///
88/// # Examples
89/// ```no_run
90/// use clap::Parser;
91/// use tftio_lib::completions::generate_completions;
92///
93/// #[derive(Parser)]
94/// struct Cli {
95///     // your CLI definition
96/// }
97///
98/// let _ = generate_completions::<Cli>(clap_complete::Shell::Bash);
99/// ```
100///
101/// # Errors
102///
103/// Returns an I/O error when writing instructions or the generated completion
104/// script to standard output fails.
105pub fn generate_completions<T: CommandFactory>(shell: Shell) -> io::Result<()> {
106    generate_completions_from_command(shell, T::command())
107}
108
109/// Generate shell completion scripts from a pre-built clap command tree.
110///
111/// # Errors
112///
113/// Returns an I/O error when writing instructions or the generated completion
114/// script to standard output fails.
115pub fn generate_completions_from_command(shell: Shell, command: clap::Command) -> io::Result<()> {
116    let output = render_completion_from_command(shell, command);
117    write_completion(io::stdout(), &output)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::agent::apply_agent_surface;
124    use crate::{
125        AgentCapability, AgentModeContext, AgentSurfaceSpec, CommandSelector, FlagSelector,
126        LicenseType, RepoInfo, ToolSpec,
127    };
128    use clap::{Arg, Parser, Subcommand};
129
130    #[derive(Parser)]
131    #[command(name = "test-cli")]
132    struct TestCli {
133        #[command(subcommand)]
134        command: TestCommands,
135    }
136
137    #[derive(Subcommand)]
138    enum TestCommands {
139        Version,
140        Test { arg: String },
141    }
142
143    const QUERY_COMMAND: CommandSelector = CommandSelector::new(&["query"]);
144    const QUERY_LIMIT_FLAG: FlagSelector = FlagSelector::new(&["query"], "limit");
145    const QUERY_CAPABILITY: AgentCapability = AgentCapability::new(
146        "query-posts",
147        "Read paginated post records",
148        &[QUERY_COMMAND],
149        &[QUERY_LIMIT_FLAG],
150    );
151    const AGENT_SURFACE: AgentSurfaceSpec = AgentSurfaceSpec::new(&[QUERY_CAPABILITY]);
152
153    fn agent_spec() -> ToolSpec {
154        ToolSpec::new(
155            "test-cli",
156            "Test CLI",
157            "1.2.3",
158            LicenseType::MIT,
159            RepoInfo::new("owner", "repo"),
160            true,
161            false,
162        )
163        .with_agent_surface(&AGENT_SURFACE)
164    }
165
166    #[test]
167    fn test_generate_completions_bash() {
168        // Just verify it doesn't panic
169        let _ = generate_completions::<TestCli>(Shell::Bash);
170    }
171
172    #[test]
173    fn test_generate_completions_zsh() {
174        let _ = generate_completions::<TestCli>(Shell::Zsh);
175    }
176
177    #[test]
178    fn test_generate_completions_fish() {
179        let _ = generate_completions::<TestCli>(Shell::Fish);
180    }
181
182    #[test]
183    fn test_generate_completions_elvish() {
184        let _ = generate_completions::<TestCli>(Shell::Elvish);
185    }
186
187    #[test]
188    fn test_generate_completions_powershell() {
189        let _ = generate_completions::<TestCli>(Shell::PowerShell);
190    }
191
192    #[test]
193    fn test_all_shells_generate_without_panic() {
194        let shells = vec![
195            Shell::Bash,
196            Shell::Zsh,
197            Shell::Fish,
198            Shell::Elvish,
199            Shell::PowerShell,
200        ];
201
202        for shell in shells {
203            let _ = generate_completions::<TestCli>(shell);
204        }
205    }
206
207    #[test]
208    fn render_completion_separates_instructions_from_script() {
209        let output = render_completion::<TestCli>(Shell::Bash);
210
211        assert!(
212            output
213                .instructions
214                .contains("source <(test-cli completions bash)")
215        );
216        assert!(output.script.contains("complete"));
217    }
218
219    #[test]
220    fn agent_surface_redaction_completion_helper_omits_hidden_entries() {
221        let mut command = clap::Command::new("test-cli")
222            .subcommand(
223                clap::Command::new("query")
224                    .arg(Arg::new("limit").long("limit"))
225                    .arg(Arg::new("secret").long("secret")),
226            )
227            .subcommand(clap::Command::new("admin"));
228
229        apply_agent_surface(
230            &mut command,
231            &agent_spec(),
232            &AgentModeContext { active: true },
233        );
234
235        let output = render_completion_from_command(Shell::Bash, command);
236
237        assert!(output.script.contains("query"));
238        assert!(!output.script.contains("admin"));
239        assert!(!output.script.contains("--secret"));
240    }
241}