cascade_cli/cli/commands/
completions.rs1use 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
9pub fn generate_completions(shell: Shell) -> Result<()> {
11 let mut cmd = Cli::command();
12 let bin_name = "ca";
13
14 generate(shell, &mut cmd, bin_name, &mut io::stdout());
15 Ok(())
16}
17
18pub fn install_completions(shell: Option<Shell>) -> Result<()> {
20 let shells_to_install = if let Some(shell) = shell {
21 vec![shell]
22 } else {
23 detect_current_and_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 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: ca <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
63fn detect_current_and_available_shells() -> Vec<Shell> {
65 let mut shells = Vec::new();
66
67 if let Some(current_shell) = detect_current_shell() {
69 shells.push(current_shell);
70 println!("š Detected current shell: {current_shell:?}");
71 return shells; }
73
74 println!("š Could not detect current shell, checking available shells...");
76 detect_available_shells()
77}
78
79fn detect_current_shell() -> Option<Shell> {
81 let shell_path = std::env::var("SHELL").ok()?;
82 let shell_name = std::path::Path::new(&shell_path).file_name()?.to_str()?;
83
84 match shell_name {
85 "bash" => Some(Shell::Bash),
86 "zsh" => Some(Shell::Zsh),
87 "fish" => Some(Shell::Fish),
88 _ => None,
89 }
90}
91
92fn detect_available_shells() -> Vec<Shell> {
94 let mut shells = Vec::new();
95
96 if which_shell("bash").is_some() {
98 shells.push(Shell::Bash);
99 }
100
101 if which_shell("zsh").is_some() {
103 shells.push(Shell::Zsh);
104 }
105
106 if which_shell("fish").is_some() {
108 shells.push(Shell::Fish);
109 }
110
111 if shells.is_empty() {
113 shells.push(Shell::Bash);
114 }
115
116 shells
117}
118
119fn which_shell(shell: &str) -> Option<PathBuf> {
121 std::env::var("PATH")
122 .ok()?
123 .split(crate::utils::platform::path_separator())
124 .map(PathBuf::from)
125 .find_map(|path| {
126 let shell_path = path.join(crate::utils::platform::executable_name(shell));
127 if crate::utils::platform::is_executable(&shell_path) {
128 Some(shell_path)
129 } else {
130 None
131 }
132 })
133}
134
135fn install_completion_for_shell(shell: Shell) -> Result<PathBuf> {
137 let completion_dirs = crate::utils::platform::shell_completion_dirs();
139
140 let (completion_dir, filename) = match shell {
141 Shell::Bash => {
142 let bash_dirs: Vec<_> = completion_dirs
144 .iter()
145 .filter(|(name, _)| name.contains("bash"))
146 .map(|(_, path)| path.clone())
147 .collect();
148
149 let dir = bash_dirs
150 .into_iter()
151 .find(|d| d.exists() || d.parent().is_some_and(|p| p.exists()))
152 .or_else(|| {
153 dirs::home_dir().map(|h| h.join(".bash_completion.d"))
155 })
156 .ok_or_else(|| {
157 CascadeError::config("Could not find suitable bash completion directory")
158 })?;
159
160 (dir, "ca")
161 }
162 Shell::Zsh => {
163 let zsh_dirs: Vec<_> = completion_dirs
165 .iter()
166 .filter(|(name, _)| name.contains("zsh"))
167 .map(|(_, path)| path.clone())
168 .collect();
169
170 let dir = zsh_dirs
171 .into_iter()
172 .find(|d| d.exists() || d.parent().is_some_and(|p| p.exists()))
173 .or_else(|| {
174 dirs::home_dir().map(|h| h.join(".zsh/completions"))
176 })
177 .ok_or_else(|| {
178 CascadeError::config("Could not find suitable zsh completion directory")
179 })?;
180
181 (dir, "_ca")
182 }
183 Shell::Fish => {
184 let fish_dirs: Vec<_> = completion_dirs
186 .iter()
187 .filter(|(name, _)| name.contains("fish"))
188 .map(|(_, path)| path.clone())
189 .collect();
190
191 let dir = fish_dirs
192 .into_iter()
193 .find(|d| d.exists() || d.parent().is_some_and(|p| p.exists()))
194 .or_else(|| {
195 dirs::home_dir().map(|h| h.join(".config/fish/completions"))
197 })
198 .ok_or_else(|| {
199 CascadeError::config("Could not find suitable fish completion directory")
200 })?;
201
202 (dir, "ca.fish")
203 }
204 _ => {
205 return Err(CascadeError::config(format!(
206 "Unsupported shell: {shell:?}"
207 )));
208 }
209 };
210
211 if !completion_dir.exists() {
213 fs::create_dir_all(&completion_dir)?;
214 }
215
216 let completion_file =
217 completion_dir.join(crate::utils::path_validation::sanitize_filename(filename));
218
219 crate::utils::path_validation::validate_config_path(&completion_file, &completion_dir)?;
221
222 let mut cmd = Cli::command();
224 let mut content = Vec::new();
225 generate(shell, &mut cmd, "ca", &mut content);
226
227 crate::utils::atomic_file::write_bytes(&completion_file, &content)?;
229
230 Ok(completion_file)
231}
232
233pub fn show_completions_status() -> Result<()> {
235 println!("š Shell Completions Status");
236 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāā");
237
238 let available_shells = detect_available_shells();
239
240 println!("\nš Available shells:");
241 for shell in &available_shells {
242 let status = check_completion_installed(*shell);
243 let status_icon = if status { "ā
" } else { "ā" };
244 println!(" {status_icon} {shell:?}");
245 }
246
247 if available_shells
248 .iter()
249 .any(|s| !check_completion_installed(*s))
250 {
251 println!("\nš” To install completions:");
252 println!(" ca completions install");
253 println!(" ca completions install --shell bash # for specific shell");
254 } else {
255 println!("\nš All available shells have completions installed!");
256 }
257
258 println!("\nš§ Manual installation:");
259 println!(" ca completions generate bash > ~/.bash_completion.d/ca");
260 println!(" ca completions generate zsh > ~/.zsh/completions/_ca");
261 println!(" ca completions generate fish > ~/.config/fish/completions/ca.fish");
262
263 Ok(())
264}
265
266fn check_completion_installed(shell: Shell) -> bool {
268 let home_dir = match dirs::home_dir() {
269 Some(dir) => dir,
270 None => return false,
271 };
272
273 let possible_paths = match shell {
274 Shell::Bash => vec![
275 home_dir.join(".bash_completion.d/ca"),
276 PathBuf::from("/usr/local/etc/bash_completion.d/ca"),
277 PathBuf::from("/etc/bash_completion.d/ca"),
278 ],
279 Shell::Zsh => vec![
280 home_dir.join(".oh-my-zsh/completions/_ca"),
281 home_dir.join(".zsh/completions/_ca"),
282 PathBuf::from("/usr/local/share/zsh/site-functions/_ca"),
283 ],
284 Shell::Fish => vec![home_dir.join(".config/fish/completions/ca.fish")],
285 _ => return false,
286 };
287
288 possible_paths.iter().any(|path| path.exists())
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_detect_shells() {
297 let shells = detect_available_shells();
298 assert!(!shells.is_empty());
299 }
300
301 #[test]
302 fn test_generate_bash_completion() {
303 let result = generate_completions(Shell::Bash);
304 assert!(result.is_ok());
305 }
306
307 #[test]
308 fn test_detect_current_shell() {
309 std::env::set_var("SHELL", "/bin/zsh");
311 let shell = detect_current_shell();
312 assert_eq!(shell, Some(Shell::Zsh));
313
314 std::env::set_var("SHELL", "/usr/bin/bash");
315 let shell = detect_current_shell();
316 assert_eq!(shell, Some(Shell::Bash));
317
318 std::env::set_var("SHELL", "/usr/local/bin/fish");
319 let shell = detect_current_shell();
320 assert_eq!(shell, Some(Shell::Fish));
321
322 std::env::set_var("SHELL", "/bin/unknown");
323 let shell = detect_current_shell();
324 assert_eq!(shell, None);
325
326 std::env::remove_var("SHELL");
328 }
329}