vkteams_bot_cli/
completion.rs

1//! Shell completion generation for VK Teams Bot CLI
2//!
3//! This module provides functionality to generate shell completion scripts
4//! for various shells including bash, zsh, fish, and PowerShell.
5
6#[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/// Available shell types for completion generation
22#[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/// Generate shell completion script for the specified shell
44///
45/// # Arguments
46/// * `shell` - The shell type to generate completions for
47/// * `output_path` - Optional path to write the completion script to
48///
49/// # Returns
50/// * `Ok(())` if completion generation succeeds
51/// * `Err(CliError)` if generation fails
52#[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/// Print installation instructions for the generated completion script
78#[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/// Generate completion for all supported shells
124///
125/// # Arguments
126/// * `output_dir` - Directory to write completion scripts to
127///
128/// # Returns
129/// * `Ok(())` if all completions are generated successfully
130/// * `Err(CliError)` if any generation fails
131#[cfg(feature = "completion")]
132pub fn generate_all_completions(output_dir: &Path) -> CliResult<()> {
133    // Ensure output directory exists
134    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/// Get the default completion directory for the current system
158#[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/// Install completion script to the appropriate system location
169///
170/// # Arguments
171/// * `shell` - The shell to install completion for
172///
173/// # Returns
174/// * `Ok(())` if installation succeeds
175/// * `Err(CliError)` if installation fails
176#[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 to temporary file
182    generate_completion(shell, Some(&temp_file))?;
183
184    // Determine target location
185    let target_path = get_system_completion_path(shell)?;
186
187    // Ensure target directory exists
188    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    // Copy to target location
195    fs::copy(&temp_file, &target_path)
196        .map_err(|e| CliError::FileError(format!("Failed to install completion: {e}")))?;
197
198    // Clean up temporary file
199    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/// Get the system completion path for a given shell
208#[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            // On Windows, use Documents/PowerShell/Scripts
221            #[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/// Print post-installation instructions
236#[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        // Test that generation doesn't panic
271        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        // Check that files were created
290        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        // Try to generate completions in a directory with no write permission
324        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        // Simulate error by making target path unwritable
336        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        // Patch get_system_completion_path to return unwritable path
343        // (Requires refactor to inject path, so just check that function exists)
344        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        // There is no unknown shell in enum, but test coverage for all variants
351        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}