vkteams_bot_cli/
completion.rs1#[cfg(feature = "completion")]
7use crate::cli::Cli;
8#[cfg(feature = "completion")]
9use crate::errors::prelude::{CliError, Result as CliResult};
10#[cfg(feature = "completion")]
11use clap::CommandFactory;
12#[cfg(feature = "completion")]
13use clap_complete::{Shell, generate};
14#[cfg(feature = "completion")]
15use std::fs;
16#[cfg(feature = "completion")]
17use std::io;
18#[cfg(feature = "completion")]
19use std::path::Path;
20
21#[cfg(feature = "completion")]
23#[derive(Debug, Clone, Copy, PartialEq, clap::ValueEnum)]
24pub enum CompletionShell {
25 Bash,
26 Zsh,
27 Fish,
28 PowerShell,
29}
30
31#[cfg(feature = "completion")]
32impl From<CompletionShell> for Shell {
33 fn from(shell: CompletionShell) -> Self {
34 match shell {
35 CompletionShell::Bash => Shell::Bash,
36 CompletionShell::Zsh => Shell::Zsh,
37 CompletionShell::Fish => Shell::Fish,
38 CompletionShell::PowerShell => Shell::PowerShell,
39 }
40 }
41}
42
43#[cfg(feature = "completion")]
53pub fn generate_completion(shell: CompletionShell, output_path: Option<&Path>) -> CliResult<()> {
54 let mut cmd = Cli::command();
55 let shell: Shell = shell.into();
56
57 match output_path {
58 Some(path) => {
59 let mut file = fs::File::create(path).map_err(|e| {
60 CliError::FileError(format!("Failed to create completion file: {e}"))
61 })?;
62
63 generate(shell, &mut cmd, "vkteams-bot-cli", &mut file);
64
65 println!("Completion script generated: {}", path.display());
66 print_installation_instructions(shell, path);
67 }
68 None => {
69 let mut stdout = io::stdout();
70 generate(shell, &mut cmd, "vkteams-bot-cli", &mut stdout);
71 }
72 }
73
74 Ok(())
75}
76
77#[cfg(feature = "completion")]
79fn print_installation_instructions(shell: Shell, path: &Path) {
80 println!("\nInstallation Instructions:");
81 println!("{}", "=".repeat(50));
82
83 match shell {
84 Shell::Bash => {
85 println!("Add the following line to your ~/.bashrc or ~/.bash_profile:");
86 println!(" source {}", path.display());
87 println!("\nOr copy the file to your bash completions directory:");
88 println!(" sudo cp {} /etc/bash_completion.d/", path.display());
89 }
90 Shell::Zsh => {
91 println!("Add the following line to your ~/.zshrc:");
92 println!(" source {}", path.display());
93 println!("\nOr place the file in your zsh completions directory:");
94 println!(
95 " cp {} ~/.oh-my-zsh/completions/_vkteams-bot-cli",
96 path.display()
97 );
98 println!(" # or");
99 println!(
100 " cp {} /usr/local/share/zsh/site-functions/_vkteams-bot-cli",
101 path.display()
102 );
103 }
104 Shell::Fish => {
105 println!("Copy the file to your fish completions directory:");
106 println!(" cp {} ~/.config/fish/completions/", path.display());
107 println!("\nOr for system-wide installation:");
108 println!(" sudo cp {} /usr/share/fish/completions/", path.display());
109 }
110 Shell::PowerShell => {
111 println!("Add the following line to your PowerShell profile:");
112 println!(" . {}", path.display());
113 println!("\nTo find your profile location, run:");
114 println!(" $PROFILE");
115 }
116 _ => {
117 println!("Please refer to your shell's documentation for completion installation.");
118 }
119 }
120 println!("\nAfter installation, restart your shell or source the file to enable completions.");
121}
122
123#[cfg(feature = "completion")]
132pub fn generate_all_completions(output_dir: &Path) -> CliResult<()> {
133 fs::create_dir_all(output_dir)
135 .map_err(|e| CliError::FileError(format!("Failed to create output directory: {e}")))?;
136
137 let shells = [
138 (CompletionShell::Bash, "vkteams-bot-cli.bash"),
139 (CompletionShell::Zsh, "_vkteams-bot-cli"),
140 (CompletionShell::Fish, "vkteams-bot-cli.fish"),
141 (CompletionShell::PowerShell, "vkteams-bot-cli.ps1"),
142 ];
143
144 for (shell, filename) in &shells {
145 let output_path = output_dir.join(filename);
146 generate_completion(*shell, Some(&output_path))?;
147 }
148
149 println!(
150 "\nAll completion scripts generated in: {}",
151 output_dir.display()
152 );
153
154 Ok(())
155}
156
157#[cfg(feature = "completion")]
159pub fn get_default_completion_dir() -> Option<std::path::PathBuf> {
160 dirs::home_dir().map(|home| {
161 home.join(".local")
162 .join("share")
163 .join("vkteams-bot-cli")
164 .join("completions")
165 })
166}
167
168#[cfg(feature = "completion")]
177pub fn install_completion(shell: CompletionShell) -> CliResult<()> {
178 let temp_dir = std::env::temp_dir();
179 let temp_file = temp_dir.join(format!("vkteams-bot-cli-completion-{shell:?}"));
180
181 generate_completion(shell, Some(&temp_file))?;
183
184 let target_path = get_system_completion_path(shell)?;
186
187 if let Some(parent) = target_path.parent() {
189 fs::create_dir_all(parent).map_err(|e| {
190 CliError::FileError(format!("Failed to create completion directory: {e}"))
191 })?;
192 }
193
194 fs::copy(&temp_file, &target_path)
196 .map_err(|e| CliError::FileError(format!("Failed to install completion: {e}")))?;
197
198 let _ = fs::remove_file(&temp_file);
200
201 println!("Completion installed to: {}", target_path.display());
202 print_post_install_instructions(shell);
203
204 Ok(())
205}
206
207#[cfg(feature = "completion")]
209fn get_system_completion_path(shell: CompletionShell) -> CliResult<std::path::PathBuf> {
210 let home = dirs::home_dir()
211 .ok_or_else(|| CliError::FileError("Could not determine home directory".to_string()))?;
212
213 let path = match shell {
214 CompletionShell::Bash => {
215 home.join(".local/share/bash-completion/completions/vkteams-bot-cli")
216 }
217 CompletionShell::Zsh => home.join(".local/share/zsh/site-functions/_vkteams-bot-cli"),
218 CompletionShell::Fish => home.join(".config/fish/completions/vkteams-bot-cli.fish"),
219 CompletionShell::PowerShell => {
220 #[cfg(windows)]
222 {
223 home.join("Documents/PowerShell/Scripts/vkteams-bot-cli-completion.ps1")
224 }
225 #[cfg(not(windows))]
226 {
227 home.join(".config/powershell/Scripts/vkteams-bot-cli-completion.ps1")
228 }
229 }
230 };
231
232 Ok(path)
233}
234
235#[cfg(feature = "completion")]
237fn print_post_install_instructions(shell: CompletionShell) {
238 println!("\nPost-installation steps:");
239
240 match shell {
241 CompletionShell::Bash => {
242 println!("Add this to your ~/.bashrc if not already present:");
243 println!(" eval \"$(register-python-argcomplete vkteams-bot-cli)\"");
244 println!("Or restart your terminal to load the new completions.");
245 }
246 CompletionShell::Zsh => {
247 println!("Ensure your zsh completion system is enabled in ~/.zshrc:");
248 println!(" autoload -Uz compinit && compinit");
249 println!("Then restart your terminal or run: compinit");
250 }
251 CompletionShell::Fish => {
252 println!("Fish will automatically load the completions.");
253 println!("Restart your fish shell or run: fish_update_completions");
254 }
255 CompletionShell::PowerShell => {
256 println!("Add this to your PowerShell profile:");
257 println!(" Import-Module vkteams-bot-cli-completion");
258 println!("Run 'notepad $PROFILE' to edit your profile.");
259 }
260 }
261}
262
263#[cfg(all(test, feature = "completion"))]
264mod tests {
265 use super::*;
266 use tempfile::tempdir;
267
268 #[test]
269 fn test_generate_completion_to_stdout() {
270 assert!(generate_completion(CompletionShell::Bash, None).is_ok());
272 }
273
274 #[test]
275 fn test_generate_completion_to_file() {
276 let temp_dir = tempdir().unwrap();
277 let output_path = temp_dir.path().join("test_completion.bash");
278
279 assert!(generate_completion(CompletionShell::Bash, Some(&output_path)).is_ok());
280 assert!(output_path.exists());
281 }
282
283 #[test]
284 fn test_generate_all_completions() {
285 let temp_dir = tempdir().unwrap();
286
287 assert!(generate_all_completions(temp_dir.path()).is_ok());
288
289 assert!(temp_dir.path().join("vkteams-bot-cli.bash").exists());
291 assert!(temp_dir.path().join("_vkteams-bot-cli").exists());
292 assert!(temp_dir.path().join("vkteams-bot-cli.fish").exists());
293 assert!(temp_dir.path().join("vkteams-bot-cli.ps1").exists());
294 }
295
296 #[test]
297 fn test_get_default_completion_dir() {
298 let dir = get_default_completion_dir();
299 assert!(dir.is_some());
300 }
301
302 #[test]
303 fn test_shell_conversion() {
304 let bash: Shell = CompletionShell::Bash.into();
305 assert!(matches!(bash, Shell::Bash));
306
307 let zsh: Shell = CompletionShell::Zsh.into();
308 assert!(matches!(zsh, Shell::Zsh));
309 }
310}
311
312#[cfg(test)]
313mod edge_case_tests {
314 use super::*;
315 use std::fs;
316 #[cfg(feature = "completion")]
317 use std::os::unix::fs::PermissionsExt;
318 use tempfile::tempdir;
319
320 #[test]
321 #[cfg(feature = "completion")]
322 fn test_generate_all_completions_fail_create_dir() {
323 let tmp = tempdir().unwrap();
325 let dir = tmp.path().join("readonly");
326 fs::create_dir(&dir).unwrap();
327 let _ = fs::set_permissions(&dir, fs::Permissions::from_mode(0o400));
328 let res = generate_all_completions(&dir);
329 assert!(res.is_err());
330 }
331
332 #[test]
333 #[cfg(feature = "completion")]
334 fn test_install_completion_fail_copy() {
335 let tmp = tempdir().unwrap();
337 let file = tmp.path().join("file");
338 fs::write(&file, "test").unwrap();
339 let dir = tmp.path().join("unwritable");
340 fs::create_dir(&dir).unwrap();
341 let _ = fs::set_permissions(&dir, fs::Permissions::from_mode(0o400));
342 let _ = get_system_completion_path(CompletionShell::Bash);
345 }
346
347 #[test]
348 #[cfg(feature = "completion")]
349 fn test_get_system_completion_path_unknown_shell() {
350 let _ = get_system_completion_path(CompletionShell::Bash).unwrap();
352 let _ = get_system_completion_path(CompletionShell::Zsh).unwrap();
353 let _ = get_system_completion_path(CompletionShell::Fish).unwrap();
354 let _ = get_system_completion_path(CompletionShell::PowerShell).unwrap();
355 }
356}