cascade_cli/cli/commands/
completions.rs

1use crate::cli::Cli;
2use crate::errors::{CascadeError, Result};
3use clap::CommandFactory;
4use clap_complete::{generate, Shell};
5use std::fs;
6use std::io;
7use std::path::PathBuf;
8
9/// Generate shell completions for the specified shell
10pub fn generate_completions(shell: Shell) -> Result<()> {
11    let mut cmd = Cli::command();
12    let bin_name = "cc";
13
14    generate(shell, &mut cmd, bin_name, &mut io::stdout());
15    Ok(())
16}
17
18/// Install shell completions to the system
19pub fn install_completions(shell: Option<Shell>) -> Result<()> {
20    let shells_to_install = if let Some(shell) = shell {
21        vec![shell]
22    } else {
23        // Auto-detect available shells
24        detect_available_shells()
25    };
26
27    let mut installed = Vec::new();
28    let mut errors = Vec::new();
29
30    for shell in shells_to_install {
31        match install_completion_for_shell(shell) {
32            Ok(path) => {
33                installed.push((shell, path));
34            }
35            Err(e) => {
36                errors.push((shell, e));
37            }
38        }
39    }
40
41    // Report results
42    if !installed.is_empty() {
43        println!("āœ… Shell completions installed:");
44        for (shell, path) in installed {
45            println!("   {:?}: {}", shell, path.display());
46        }
47
48        println!("\nšŸ’” Next steps:");
49        println!("   1. Restart your shell or run: source ~/.bashrc (or equivalent)");
50        println!("   2. Try: cc <TAB><TAB>");
51    }
52
53    if !errors.is_empty() {
54        println!("\nāš ļø  Some installations failed:");
55        for (shell, error) in errors {
56            println!("   {shell:?}: {error}");
57        }
58    }
59
60    Ok(())
61}
62
63/// Detect which shells are available on the system
64fn detect_available_shells() -> Vec<Shell> {
65    let mut shells = Vec::new();
66
67    // Check for bash
68    if which_shell("bash").is_some() {
69        shells.push(Shell::Bash);
70    }
71
72    // Check for zsh
73    if which_shell("zsh").is_some() {
74        shells.push(Shell::Zsh);
75    }
76
77    // Check for fish
78    if which_shell("fish").is_some() {
79        shells.push(Shell::Fish);
80    }
81
82    // Default to bash if nothing found
83    if shells.is_empty() {
84        shells.push(Shell::Bash);
85    }
86
87    shells
88}
89
90/// Check if a shell exists in PATH
91fn which_shell(shell: &str) -> Option<PathBuf> {
92    std::env::var("PATH")
93        .ok()?
94        .split(':')
95        .map(PathBuf::from)
96        .find_map(|path| {
97            let shell_path = path.join(shell);
98            if shell_path.exists() {
99                Some(shell_path)
100            } else {
101                None
102            }
103        })
104}
105
106/// Install completion for a specific shell
107fn install_completion_for_shell(shell: Shell) -> Result<PathBuf> {
108    let home_dir =
109        dirs::home_dir().ok_or_else(|| CascadeError::config("Could not find home directory"))?;
110
111    let (completion_dir, filename) = match shell {
112        Shell::Bash => {
113            // Try multiple common bash completion directories
114            let dirs = vec![
115                home_dir.join(".bash_completion.d"),
116                PathBuf::from("/usr/local/etc/bash_completion.d"),
117                PathBuf::from("/etc/bash_completion.d"),
118            ];
119
120            let dir = dirs
121                .into_iter()
122                .find(|d| d.exists() || d.parent().is_some_and(|p| p.exists()))
123                .unwrap_or_else(|| home_dir.join(".bash_completion.d"));
124
125            (dir, "cc")
126        }
127        Shell::Zsh => {
128            // Check for oh-my-zsh first, then standard locations
129            let dirs = vec![
130                home_dir.join(".oh-my-zsh/completions"),
131                home_dir.join(".zsh/completions"),
132                PathBuf::from("/usr/local/share/zsh/site-functions"),
133            ];
134
135            let dir = dirs
136                .into_iter()
137                .find(|d| d.exists() || d.parent().is_some_and(|p| p.exists()))
138                .unwrap_or_else(|| home_dir.join(".zsh/completions"));
139
140            (dir, "_cc")
141        }
142        Shell::Fish => {
143            let dir = home_dir.join(".config/fish/completions");
144            (dir, "cc.fish")
145        }
146        _ => {
147            return Err(CascadeError::config(format!(
148                "Unsupported shell: {shell:?}"
149            )));
150        }
151    };
152
153    // Create directory if it doesn't exist
154    if !completion_dir.exists() {
155        fs::create_dir_all(&completion_dir)?;
156    }
157
158    let completion_file = completion_dir.join(filename);
159
160    // Generate completion content
161    let mut cmd = Cli::command();
162    let mut content = Vec::new();
163    generate(shell, &mut cmd, "cc", &mut content);
164
165    // Write to file
166    fs::write(&completion_file, content)?;
167
168    Ok(completion_file)
169}
170
171/// Show installation status and guidance
172pub fn show_completions_status() -> Result<()> {
173    println!("šŸš€ Shell Completions Status");
174    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━");
175
176    let available_shells = detect_available_shells();
177
178    println!("\nšŸ“Š Available shells:");
179    for shell in &available_shells {
180        let status = check_completion_installed(*shell);
181        let status_icon = if status { "āœ…" } else { "āŒ" };
182        println!("   {status_icon} {shell:?}");
183    }
184
185    if available_shells
186        .iter()
187        .any(|s| !check_completion_installed(*s))
188    {
189        println!("\nšŸ’” To install completions:");
190        println!("   cc completions install");
191        println!("   cc completions install --shell bash  # for specific shell");
192    } else {
193        println!("\nšŸŽ‰ All available shells have completions installed!");
194    }
195
196    println!("\nšŸ”§ Manual installation:");
197    println!("   cc completions generate bash > ~/.bash_completion.d/cc");
198    println!("   cc completions generate zsh > ~/.zsh/completions/_cc");
199    println!("   cc completions generate fish > ~/.config/fish/completions/cc.fish");
200
201    Ok(())
202}
203
204/// Check if completion is installed for a shell
205fn check_completion_installed(shell: Shell) -> bool {
206    let home_dir = match dirs::home_dir() {
207        Some(dir) => dir,
208        None => return false,
209    };
210
211    let possible_paths = match shell {
212        Shell::Bash => vec![
213            home_dir.join(".bash_completion.d/cc"),
214            PathBuf::from("/usr/local/etc/bash_completion.d/cc"),
215            PathBuf::from("/etc/bash_completion.d/cc"),
216        ],
217        Shell::Zsh => vec![
218            home_dir.join(".oh-my-zsh/completions/_cc"),
219            home_dir.join(".zsh/completions/_cc"),
220            PathBuf::from("/usr/local/share/zsh/site-functions/_cc"),
221        ],
222        Shell::Fish => vec![home_dir.join(".config/fish/completions/cc.fish")],
223        _ => return false,
224    };
225
226    possible_paths.iter().any(|path| path.exists())
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_detect_shells() {
235        let shells = detect_available_shells();
236        // Should always have at least one shell (bash fallback)
237        assert!(!shells.is_empty());
238    }
239
240    #[test]
241    fn test_generate_bash_completion() {
242        // Just test that it doesn't panic
243        let result = generate_completions(Shell::Bash);
244        assert!(result.is_ok());
245    }
246}