git_worktree_cli/
completions.rs

1use clap_complete::Shell;
2use colored::Colorize;
3use std::env;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use crate::error::{Error, Result};
8
9// Include the generated completion files at compile time
10const BASH_COMPLETION: &str = include_str!(concat!(env!("OUT_DIR"), "/completions/gwt.bash"));
11const ZSH_COMPLETION: &str = include_str!(concat!(env!("OUT_DIR"), "/completions/_gwt"));
12const FISH_COMPLETION: &str = include_str!(concat!(env!("OUT_DIR"), "/completions/gwt.fish"));
13const POWERSHELL_COMPLETION: &str = include_str!(concat!(env!("OUT_DIR"), "/completions/_gwt.ps1"));
14const ELVISH_COMPLETION: &str = include_str!(concat!(env!("OUT_DIR"), "/completions/gwt.elv"));
15
16fn is_writable(path: &Path) -> bool {
17    #[cfg(unix)]
18    {
19        // On Unix, check if we have write permission by attempting to create a temp file
20        let test_file = path.join(format!(".gwt_write_test_{}", std::process::id()));
21        let writable = fs::write(&test_file, b"").is_ok();
22        let _ = fs::remove_file(&test_file); // Clean up if it was created
23        writable
24    }
25
26    #[cfg(not(unix))]
27    {
28        // On non-Unix systems, use readonly check
29        fs::metadata(path)
30            .ok()
31            .map(|metadata| !metadata.permissions().readonly())
32            .unwrap_or(false)
33    }
34}
35
36pub fn get_completion_content(shell: Shell) -> &'static str {
37    match shell {
38        Shell::Bash => BASH_COMPLETION,
39        Shell::Zsh => ZSH_COMPLETION,
40        Shell::Fish => FISH_COMPLETION,
41        Shell::PowerShell => POWERSHELL_COMPLETION,
42        Shell::Elvish => ELVISH_COMPLETION,
43        _ => panic!("Unsupported shell: {:?}", shell),
44    }
45}
46
47pub fn detect_shell() -> Result<Shell> {
48    if let Ok(shell_path) = env::var("SHELL") {
49        if shell_path.contains("zsh") {
50            return Ok(Shell::Zsh);
51        } else if shell_path.contains("bash") {
52            return Ok(Shell::Bash);
53        } else if shell_path.contains("fish") {
54            return Ok(Shell::Fish);
55        } else if shell_path.contains("elvish") {
56            return Ok(Shell::Elvish);
57        }
58    }
59
60    // Check for PowerShell on Windows
61    if cfg!(windows) {
62        return Ok(Shell::PowerShell);
63    }
64
65    // Default to zsh on macOS, bash on others
66    if cfg!(target_os = "macos") {
67        Ok(Shell::Zsh)
68    } else {
69        Ok(Shell::Bash)
70    }
71}
72
73pub fn get_completion_install_path(shell: Shell) -> Result<PathBuf> {
74    let home = env::var("HOME").map_err(|_| Error::msg("Could not determine home directory"))?;
75
76    match shell {
77        Shell::Bash => {
78            // Always prefer user-writable directory
79            let user_path = PathBuf::from(&home).join(".local/share/bash-completion/completions");
80
81            // Check for system directories only if user directory doesn't exist
82            let system_paths = vec![
83                PathBuf::from("/usr/local/share/bash-completion/completions"),
84                PathBuf::from("/etc/bash_completion.d"),
85            ];
86
87            // Try to find a writable system directory if user directory doesn't exist
88            if !user_path.exists() {
89                for path in system_paths {
90                    if path.exists() && is_writable(&path) {
91                        return Ok(path.join("gwt"));
92                    }
93                }
94            }
95
96            // Default to user-writable directory (will be created if needed)
97            Ok(user_path.join("gwt"))
98        }
99        Shell::Zsh => {
100            // For Zsh, we'll add to the user's fpath
101            Ok(PathBuf::from(&home).join(".local/share/zsh/site-functions/_gwt"))
102        }
103        Shell::Fish => Ok(PathBuf::from(&home).join(".config/fish/completions/gwt.fish")),
104        Shell::PowerShell => {
105            // PowerShell profile location
106            let profile_path = if cfg!(windows) {
107                PathBuf::from(&env::var("USERPROFILE").unwrap_or(home))
108                    .join("Documents/PowerShell/Modules/gwt-completions/gwt-completions.psm1")
109            } else {
110                PathBuf::from(&home).join(".config/powershell/Modules/gwt-completions/gwt-completions.psm1")
111            };
112            Ok(profile_path)
113        }
114        Shell::Elvish => Ok(PathBuf::from(&home).join(".elvish/lib/gwt-completions.elv")),
115        _ => Err(Error::msg(format!("Unsupported shell: {:?}", shell))),
116    }
117}
118
119pub fn install_completions_for_shell(shell: Shell) -> Result<()> {
120    let content = get_completion_content(shell);
121    let install_path = get_completion_install_path(shell)?;
122
123    // Create parent directory if it doesn't exist
124    if let Some(parent) = install_path.parent() {
125        fs::create_dir_all(parent)?;
126    }
127
128    // Write the completion file
129    fs::write(&install_path, content)?;
130
131    println!(
132        "āœ“ Installed {} completions to: {}",
133        shell.to_string().green(),
134        install_path.display().to_string().cyan()
135    );
136
137    // Shell-specific setup
138    match shell {
139        Shell::Bash => {
140            println!("\nTo activate completions in your current shell, run:");
141            println!("  {}", "source ~/.bashrc".cyan());
142            println!("\nOr start a new terminal session.");
143        }
144        Shell::Zsh => {
145            setup_zsh_completions()?;
146        }
147        Shell::Fish => {
148            println!("\nCompletions will be available immediately in new fish sessions.");
149        }
150        Shell::PowerShell => {
151            println!("\nTo activate completions, add the following to your PowerShell profile:");
152            println!("  {}", "Import-Module gwt-completions".cyan());
153            println!("\nYour profile is located at:");
154            println!("  {}", "$PROFILE".cyan());
155        }
156        Shell::Elvish => {
157            println!("\nTo activate completions, add the following to your ~/.elvish/rc.elv:");
158            println!("  {}", "use gwt-completions".cyan());
159        }
160        _ => {}
161    }
162
163    Ok(())
164}
165
166fn setup_zsh_completions() -> Result<()> {
167    let home = env::var("HOME")?;
168    let zshrc_path = PathBuf::from(&home).join(".zshrc");
169
170    ensure_zshrc_exists(&zshrc_path)?;
171
172    let mut content = fs::read_to_string(&zshrc_path)?;
173    let modified = add_zsh_completion_config(&mut content, &home)?;
174
175    if modified {
176        fs::write(&zshrc_path, content)?;
177        println!("āœ“ Updated ~/.zshrc");
178    }
179
180    show_zsh_activation_instructions();
181    Ok(())
182}
183
184fn ensure_zshrc_exists(zshrc_path: &Path) -> Result<()> {
185    if !zshrc_path.exists() {
186        println!("\n{}: ~/.zshrc does not exist. Creating it...", "Note".yellow());
187        fs::write(zshrc_path, "")?;
188    }
189    Ok(())
190}
191
192fn add_zsh_completion_config(content: &mut String, home: &str) -> Result<bool> {
193    let fpath_dir = format!("{}/.local/share/zsh/site-functions", home);
194
195    if content.contains(&fpath_dir) {
196        println!("\nāœ“ Completion path already configured in ~/.zshrc");
197        return Ok(false);
198    }
199
200    println!("\nāœ“ Adding completion path to ~/.zshrc");
201
202    // Ensure proper newline before adding content
203    if !content.is_empty() && !content.ends_with('\n') {
204        content.push('\n');
205    }
206
207    // Add fpath configuration
208    content.push_str("\n# Git worktree CLI completions\n");
209    content.push_str(&format!("fpath=({} $fpath)\n", fpath_dir));
210
211    // Add compinit if not present
212    if !content.contains("compinit") {
213        content.push_str("autoload -Uz compinit && compinit\n");
214    }
215
216    Ok(true)
217}
218
219fn show_zsh_activation_instructions() {
220    println!("\nTo activate completions in your current shell, run:");
221    println!("  {}", "source ~/.zshrc".cyan());
222    println!("\nOr start a new terminal session.");
223}
224
225pub fn check_completions_installed(shell: Shell) -> Result<bool> {
226    let install_path = get_completion_install_path(shell)?;
227
228    // Check if completion file exists
229    if !install_path.exists() {
230        return Ok(false);
231    }
232
233    // For Zsh, also check if fpath is configured
234    if matches!(shell, Shell::Zsh) {
235        let home = env::var("HOME")?;
236        let zshrc_path = PathBuf::from(&home).join(".zshrc");
237
238        if zshrc_path.exists() {
239            let content = fs::read_to_string(&zshrc_path)?;
240            let fpath_configured = content.contains(&format!("{}/.local/share/zsh/site-functions", home));
241            return Ok(fpath_configured);
242        }
243    }
244
245    Ok(true)
246}