Skip to main content

ralph/cli/
completions.rs

1//! Shell completion script generation for Ralph CLI.
2//!
3//! Responsibilities:
4//! - Generate shell completion scripts for supported shells (bash, zsh, fish, PowerShell, Elvish).
5//! - Provide a CLI command to output completion scripts to stdout.
6//!
7//! Not handled here:
8//! - Installation of completion scripts to system directories (user responsibility).
9//! - Runtime shell detection or automatic configuration.
10//!
11//! Invariants/assumptions:
12//! - Completion scripts are generated using clap_complete and written to stdout.
13//! - Users redirect output to appropriate shell-specific completion directories.
14
15use anyhow::Result;
16use clap::{CommandFactory, ValueEnum};
17use clap_complete::{Shell as ClapShell, generate};
18
19/// Arguments for the completions command.
20#[derive(clap::Args, Debug)]
21pub struct CompletionsArgs {
22    /// The shell to generate completions for
23    #[arg(value_enum)]
24    pub shell: Shell,
25}
26
27/// Supported shells for completion generation.
28#[derive(Clone, Copy, Debug, ValueEnum)]
29pub enum Shell {
30    /// Bash shell completions
31    Bash,
32    /// Zsh shell completions
33    Zsh,
34    /// Fish shell completions
35    Fish,
36    /// PowerShell completions
37    #[value(name = "powershell")]
38    PowerShell,
39    /// Elvish shell completions
40    Elvish,
41}
42
43impl From<Shell> for ClapShell {
44    fn from(shell: Shell) -> Self {
45        match shell {
46            Shell::Bash => ClapShell::Bash,
47            Shell::Zsh => ClapShell::Zsh,
48            Shell::Fish => ClapShell::Fish,
49            Shell::PowerShell => ClapShell::PowerShell,
50            Shell::Elvish => ClapShell::Elvish,
51        }
52    }
53}
54
55/// Generate and print shell completion script for the specified shell.
56///
57/// The completion script is written to stdout. Users should redirect
58/// the output to the appropriate location for their shell.
59pub fn handle_completions(args: CompletionsArgs) -> Result<()> {
60    let mut cmd = crate::cli::Cli::command();
61    let shell: ClapShell = args.shell.into();
62    let bin_name = cmd.get_name().to_string();
63    generate(shell, &mut cmd, bin_name, &mut std::io::stdout());
64    Ok(())
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use clap::ValueEnum;
71
72    #[test]
73    fn shell_enum_parses_bash() {
74        let shell = Shell::from_str("bash", true).expect("parse bash");
75        assert!(matches!(shell, Shell::Bash));
76    }
77
78    #[test]
79    fn shell_enum_parses_zsh() {
80        let shell = Shell::from_str("zsh", true).expect("parse zsh");
81        assert!(matches!(shell, Shell::Zsh));
82    }
83
84    #[test]
85    fn shell_enum_parses_fish() {
86        let shell = Shell::from_str("fish", true).expect("parse fish");
87        assert!(matches!(shell, Shell::Fish));
88    }
89
90    #[test]
91    fn shell_enum_parses_powershell() {
92        let shell = Shell::from_str("powershell", true).expect("parse powershell");
93        assert!(matches!(shell, Shell::PowerShell));
94    }
95
96    #[test]
97    fn shell_enum_parses_elvish() {
98        let shell = Shell::from_str("elvish", true).expect("parse elvish");
99        assert!(matches!(shell, Shell::Elvish));
100    }
101
102    #[test]
103    fn shell_enum_rejects_invalid() {
104        let result = Shell::from_str("invalid", true);
105        assert!(result.is_err());
106    }
107
108    #[test]
109    fn shell_conversion_to_clap_shell() {
110        assert!(matches!(ClapShell::from(Shell::Bash), ClapShell::Bash));
111        assert!(matches!(ClapShell::from(Shell::Zsh), ClapShell::Zsh));
112        assert!(matches!(ClapShell::from(Shell::Fish), ClapShell::Fish));
113        assert!(matches!(
114            ClapShell::from(Shell::PowerShell),
115            ClapShell::PowerShell
116        ));
117        assert!(matches!(ClapShell::from(Shell::Elvish), ClapShell::Elvish));
118    }
119
120    #[test]
121    fn handle_completions_generates_non_empty_output() {
122        let mut output = Vec::new();
123        let mut cmd = crate::cli::Cli::command();
124        let bin_name = cmd.get_name().to_string();
125        generate(ClapShell::Bash, &mut cmd, bin_name, &mut output);
126
127        assert!(!output.is_empty(), "completion script should not be empty");
128        let output_str = String::from_utf8(output).expect("valid UTF-8");
129        assert!(
130            output_str.contains("ralph"),
131            "completion script should reference 'ralph'"
132        );
133    }
134}