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 = "csc";
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: csc <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(crate::utils::platform::path_separator())
95        .map(PathBuf::from)
96        .find_map(|path| {
97            let shell_path = path.join(crate::utils::platform::executable_name(shell));
98            if crate::utils::platform::is_executable(&shell_path) {
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    // Get platform-specific completion directories
109    let completion_dirs = crate::utils::platform::shell_completion_dirs();
110
111    let (completion_dir, filename) = match shell {
112        Shell::Bash => {
113            // Find the first suitable bash completion directory
114            let bash_dirs: Vec<_> = completion_dirs
115                .iter()
116                .filter(|(name, _)| name.contains("bash"))
117                .map(|(_, path)| path.clone())
118                .collect();
119
120            let dir = bash_dirs
121                .into_iter()
122                .find(|d| d.exists() || d.parent().is_some_and(|p| p.exists()))
123                .or_else(|| {
124                    // Fallback to user-specific directory
125                    dirs::home_dir().map(|h| h.join(".bash_completion.d"))
126                })
127                .ok_or_else(|| {
128                    CascadeError::config("Could not find suitable bash completion directory")
129                })?;
130
131            (dir, "csc")
132        }
133        Shell::Zsh => {
134            // Find the first suitable zsh completion directory
135            let zsh_dirs: Vec<_> = completion_dirs
136                .iter()
137                .filter(|(name, _)| name.contains("zsh"))
138                .map(|(_, path)| path.clone())
139                .collect();
140
141            let dir = zsh_dirs
142                .into_iter()
143                .find(|d| d.exists() || d.parent().is_some_and(|p| p.exists()))
144                .or_else(|| {
145                    // Fallback to user-specific directory
146                    dirs::home_dir().map(|h| h.join(".zsh/completions"))
147                })
148                .ok_or_else(|| {
149                    CascadeError::config("Could not find suitable zsh completion directory")
150                })?;
151
152            (dir, "_csc")
153        }
154        Shell::Fish => {
155            // Find the first suitable fish completion directory
156            let fish_dirs: Vec<_> = completion_dirs
157                .iter()
158                .filter(|(name, _)| name.contains("fish"))
159                .map(|(_, path)| path.clone())
160                .collect();
161
162            let dir = fish_dirs
163                .into_iter()
164                .find(|d| d.exists() || d.parent().is_some_and(|p| p.exists()))
165                .or_else(|| {
166                    // Fallback to user-specific directory
167                    dirs::home_dir().map(|h| h.join(".config/fish/completions"))
168                })
169                .ok_or_else(|| {
170                    CascadeError::config("Could not find suitable fish completion directory")
171                })?;
172
173            (dir, "csc.fish")
174        }
175        _ => {
176            return Err(CascadeError::config(format!(
177                "Unsupported shell: {shell:?}"
178            )));
179        }
180    };
181
182    // Create directory if it doesn't exist
183    if !completion_dir.exists() {
184        fs::create_dir_all(&completion_dir)?;
185    }
186
187    let completion_file =
188        completion_dir.join(crate::utils::path_validation::sanitize_filename(filename));
189
190    // Validate the completion file path for security
191    crate::utils::path_validation::validate_config_path(&completion_file, &completion_dir)?;
192
193    // Generate completion content
194    let mut cmd = Cli::command();
195    let mut content = Vec::new();
196    generate(shell, &mut cmd, "csc", &mut content);
197
198    // Write to file atomically
199    crate::utils::atomic_file::write_bytes(&completion_file, &content)?;
200
201    Ok(completion_file)
202}
203
204/// Show installation status and guidance
205pub fn show_completions_status() -> Result<()> {
206    println!("šŸš€ Shell Completions Status");
207    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━");
208
209    let available_shells = detect_available_shells();
210
211    println!("\nšŸ“Š Available shells:");
212    for shell in &available_shells {
213        let status = check_completion_installed(*shell);
214        let status_icon = if status { "āœ…" } else { "āŒ" };
215        println!("   {status_icon} {shell:?}");
216    }
217
218    if available_shells
219        .iter()
220        .any(|s| !check_completion_installed(*s))
221    {
222        println!("\nšŸ’” To install completions:");
223        println!("   csc completions install");
224        println!("   csc completions install --shell bash  # for specific shell");
225    } else {
226        println!("\nšŸŽ‰ All available shells have completions installed!");
227    }
228
229    println!("\nšŸ”§ Manual installation:");
230    println!("   csc completions generate bash > ~/.bash_completion.d/csc");
231    println!("   csc completions generate zsh > ~/.zsh/completions/_csc");
232    println!("   csc completions generate fish > ~/.config/fish/completions/csc.fish");
233
234    Ok(())
235}
236
237/// Check if completion is installed for a shell
238fn check_completion_installed(shell: Shell) -> bool {
239    let home_dir = match dirs::home_dir() {
240        Some(dir) => dir,
241        None => return false,
242    };
243
244    let possible_paths = match shell {
245        Shell::Bash => vec![
246            home_dir.join(".bash_completion.d/csc"),
247            PathBuf::from("/usr/local/etc/bash_completion.d/csc"),
248            PathBuf::from("/etc/bash_completion.d/csc"),
249        ],
250        Shell::Zsh => vec![
251            home_dir.join(".oh-my-zsh/completions/_csc"),
252            home_dir.join(".zsh/completions/_csc"),
253            PathBuf::from("/usr/local/share/zsh/site-functions/_csc"),
254        ],
255        Shell::Fish => vec![home_dir.join(".config/fish/completions/csc.fish")],
256        _ => return false,
257    };
258
259    possible_paths.iter().any(|path| path.exists())
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_detect_shells() {
268        let shells = detect_available_shells();
269        // Should always have at least one shell (bash fallback)
270        assert!(!shells.is_empty());
271    }
272
273    #[test]
274    fn test_generate_bash_completion() {
275        // Just test that it doesn't panic
276        let result = generate_completions(Shell::Bash);
277        assert!(result.is_ok());
278    }
279}