git_worktree_cli/
completions.rs1use clap_complete::Shell;
2use colored::Colorize;
3use std::env;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use crate::error::{Error, Result};
8
9const 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 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); writable
24 }
25
26 #[cfg(not(unix))]
27 {
28 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 if cfg!(windows) {
62 return Ok(Shell::PowerShell);
63 }
64
65 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 let user_path = PathBuf::from(&home).join(".local/share/bash-completion/completions");
80
81 let system_paths = vec![
83 PathBuf::from("/usr/local/share/bash-completion/completions"),
84 PathBuf::from("/etc/bash_completion.d"),
85 ];
86
87 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 Ok(user_path.join("gwt"))
98 }
99 Shell::Zsh => {
100 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 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 if let Some(parent) = install_path.parent() {
125 fs::create_dir_all(parent)?;
126 }
127
128 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 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 if !content.is_empty() && !content.ends_with('\n') {
204 content.push('\n');
205 }
206
207 content.push_str("\n# Git worktree CLI completions\n");
209 content.push_str(&format!("fpath=({} $fpath)\n", fpath_dir));
210
211 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 if !install_path.exists() {
230 return Ok(false);
231 }
232
233 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}