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 = "csc";
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_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: 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
63fn detect_available_shells() -> Vec<Shell> {
65 let mut shells = Vec::new();
66
67 if which_shell("bash").is_some() {
69 shells.push(Shell::Bash);
70 }
71
72 if which_shell("zsh").is_some() {
74 shells.push(Shell::Zsh);
75 }
76
77 if which_shell("fish").is_some() {
79 shells.push(Shell::Fish);
80 }
81
82 if shells.is_empty() {
84 shells.push(Shell::Bash);
85 }
86
87 shells
88}
89
90fn 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
106fn install_completion_for_shell(shell: Shell) -> Result<PathBuf> {
108 let completion_dirs = crate::utils::platform::shell_completion_dirs();
110
111 let (completion_dir, filename) = match shell {
112 Shell::Bash => {
113 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 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 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 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 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 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 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 crate::utils::path_validation::validate_config_path(&completion_file, &completion_dir)?;
192
193 let mut cmd = Cli::command();
195 let mut content = Vec::new();
196 generate(shell, &mut cmd, "csc", &mut content);
197
198 crate::utils::atomic_file::write_bytes(&completion_file, &content)?;
200
201 Ok(completion_file)
202}
203
204pub 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
237fn 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 assert!(!shells.is_empty());
271 }
272
273 #[test]
274 fn test_generate_bash_completion() {
275 let result = generate_completions(Shell::Bash);
277 assert!(result.is_ok());
278 }
279}