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 = "cc";
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: 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
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(':')
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
106fn 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 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 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 if !completion_dir.exists() {
155 fs::create_dir_all(&completion_dir)?;
156 }
157
158 let completion_file = completion_dir.join(filename);
159
160 let mut cmd = Cli::command();
162 let mut content = Vec::new();
163 generate(shell, &mut cmd, "cc", &mut content);
164
165 fs::write(&completion_file, content)?;
167
168 Ok(completion_file)
169}
170
171pub 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
204fn 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 assert!(!shells.is_empty());
238 }
239
240 #[test]
241 fn test_generate_bash_completion() {
242 let result = generate_completions(Shell::Bash);
244 assert!(result.is_ok());
245 }
246}