gn_cli/commands/
completions.rs1use anyhow::{bail, Context, Result};
2use clap::{Args, CommandFactory};
3use clap_complete::{generate, Shell};
4use std::fs;
5use std::io;
6use std::path::{Path, PathBuf};
7
8#[derive(Args, Debug, Clone)]
9pub struct CompletionsArgs {
10 #[arg(value_enum)]
12 pub shell: Option<Shell>,
13
14 #[arg(long)]
16 pub install: bool,
17}
18
19pub fn run(args: &CompletionsArgs) -> Result<()> {
20 if args.install {
21 install_completions(args.shell)?;
22 } else if let Some(shell) = args.shell {
23 let mut cmd = crate::Cli::command();
24 generate(shell, &mut cmd, "git-notes", &mut io::stdout());
26 let mut cmd_gn = crate::Cli::command();
27 generate(shell, &mut cmd_gn, "gn", &mut io::stdout());
28 } else {
29 bail!("Please specify a shell (bash, zsh, fish, powershell, elvish) or use --install");
30 }
31 Ok(())
32}
33
34pub fn detect_shell() -> Option<Shell> {
36 if let Ok(shell_env) = std::env::var("SHELL") {
37 let lower = shell_env.to_lowercase();
38 if lower.contains("zsh") {
39 return Some(Shell::Zsh);
40 } else if lower.contains("fish") {
41 return Some(Shell::Fish);
42 } else if lower.contains("bash") {
43 return Some(Shell::Bash);
44 } else if lower.contains("elvish") {
45 return Some(Shell::Elvish);
46 }
47 }
48
49 #[cfg(windows)]
50 {
51 if std::env::var_os("PSModulePath").is_some() {
52 return Some(Shell::PowerShell);
53 }
54 }
55
56 None
57}
58
59fn home_dir() -> Option<PathBuf> {
61 std::env::var_os("USERPROFILE")
62 .or_else(|| std::env::var_os("HOME"))
63 .map(PathBuf::from)
64}
65
66pub fn generate_completion_script(shell: Shell) -> Vec<u8> {
68 let mut buf = Vec::new();
69 let mut cmd_git_notes = crate::Cli::command();
70 generate(shell, &mut cmd_git_notes, "git-notes", &mut buf);
71 buf.push(b'\n');
72 let mut cmd_gn = crate::Cli::command();
73 generate(shell, &mut cmd_gn, "gn", &mut buf);
74 buf
75}
76
77pub fn install_completions(explicit_shell: Option<Shell>) -> Result<()> {
79 let shell = match explicit_shell.or_else(detect_shell) {
80 Some(s) => s,
81 None => {
82 #[cfg(windows)]
83 {
84 Shell::PowerShell
85 }
86 #[cfg(not(windows))]
87 {
88 bail!("Could not automatically detect shell. Please specify one explicitly, e.g.: gn completions zsh --install");
89 }
90 }
91 };
92
93 let home = home_dir().context("Failed to determine user home directory")?;
94 let script_bytes = generate_completion_script(shell);
95
96 match shell {
97 Shell::Zsh => {
98 let zsh_comp_dir = home.join(".zsh").join("completions");
100 fs::create_dir_all(&zsh_comp_dir)
101 .with_context(|| format!("Failed to create directory {}", zsh_comp_dir.display()))?;
102
103 let mut gn_buf = Vec::new();
104 let mut cmd_gn = crate::Cli::command();
105 generate(Shell::Zsh, &mut cmd_gn, "gn", &mut gn_buf);
106 let gn_path = zsh_comp_dir.join("_gn");
107 fs::write(&gn_path, gn_buf)?;
108
109 let mut git_notes_buf = Vec::new();
110 let mut cmd_git_notes = crate::Cli::command();
111 generate(Shell::Zsh, &mut cmd_git_notes, "git-notes", &mut git_notes_buf);
112 let git_notes_path = zsh_comp_dir.join("_git-notes");
113 fs::write(&git_notes_path, git_notes_buf)?;
114
115 println!("\x1b[32m✓ Installed Zsh completions:\x1b[0m");
116 println!(" • {}", gn_path.display());
117 println!(" • {}", git_notes_path.display());
118 println!("\n\x1b[90mEnsure your ~/.zshrc contains:\x1b[0m");
119 println!(" fpath=(~/.zsh/completions $fpath)");
120 println!(" autoload -Uz compinit && compinit");
121 }
122 Shell::Bash => {
123 let bash_comp_dir = home.join(".bash_completion.d");
125 fs::create_dir_all(&bash_comp_dir)
126 .with_context(|| format!("Failed to create directory {}", bash_comp_dir.display()))?;
127 let target_path = bash_comp_dir.join("gn");
128 fs::write(&target_path, script_bytes)?;
129
130 println!("\x1b[32m✓ Installed Bash completions:\x1b[0m");
131 println!(" • {}", target_path.display());
132 println!("\n\x1b[90mEnsure ~/.bashrc sources ~/.bash_completion.d:\x1b[0m");
133 println!(" for f in ~/.bash_completion.d/*; do [[ -f \"$f\" ]] && source \"$f\"; done");
134 }
135 Shell::Fish => {
136 let fish_comp_dir = home.join(".config").join("fish").join("completions");
138 fs::create_dir_all(&fish_comp_dir)
139 .with_context(|| format!("Failed to create directory {}", fish_comp_dir.display()))?;
140
141 let mut gn_buf = Vec::new();
142 let mut cmd_gn = crate::Cli::command();
143 generate(Shell::Fish, &mut cmd_gn, "gn", &mut gn_buf);
144 let gn_path = fish_comp_dir.join("gn.fish");
145 fs::write(&gn_path, gn_buf)?;
146
147 let mut git_notes_buf = Vec::new();
148 let mut cmd_git_notes = crate::Cli::command();
149 generate(Shell::Fish, &mut cmd_git_notes, "git-notes", &mut git_notes_buf);
150 let git_notes_path = fish_comp_dir.join("git-notes.fish");
151 fs::write(&git_notes_path, git_notes_buf)?;
152
153 println!("\x1b[32m✓ Installed Fish completions:\x1b[0m");
154 println!(" • {}", gn_path.display());
155 println!(" • {}", git_notes_path.display());
156 }
157 Shell::PowerShell => {
158 let profile_path = get_powershell_profile_path(&home);
160 if let Some(parent) = profile_path.parent() {
161 let _ = fs::create_dir_all(parent);
162 }
163
164 let comp_dir = home.join(".git-notes").join("completions");
165 fs::create_dir_all(&comp_dir)?;
166 let script_path = comp_dir.join("gn-completions.ps1");
167 fs::write(&script_path, &script_bytes)?;
168
169 let source_cmd = format!("\n. \"{}\"\n", script_path.display());
170 let current_content = fs::read_to_string(&profile_path).unwrap_or_default();
171 if !current_content.contains("gn-completions.ps1") {
172 let mut new_content = current_content;
173 new_content.push_str(&source_cmd);
174 fs::write(&profile_path, new_content)?;
175 }
176
177 println!("\x1b[32m✓ Installed PowerShell completions:\x1b[0m");
178 println!(" • Script: {}", script_path.display());
179 println!(" • Profile: {}", profile_path.display());
180 }
181 Shell::Elvish => {
182 let elvish_comp_dir = home.join(".elvish").join("lib");
184 fs::create_dir_all(&elvish_comp_dir)
185 .with_context(|| format!("Failed to create directory {}", elvish_comp_dir.display()))?;
186 let target_path = elvish_comp_dir.join("gn.elv");
187 fs::write(&target_path, script_bytes)?;
188
189 println!("\x1b[32m✓ Installed Elvish completions:\x1b[0m");
190 println!(" • {}", target_path.display());
191 }
192 _ => {
193 bail!("Automated installation not yet supported for {:?}. Please generate output directly with: gn completions {:?}", shell, shell);
194 }
195 }
196
197 Ok(())
198}
199
200fn get_powershell_profile_path(home: &Path) -> PathBuf {
201 #[cfg(windows)]
202 {
203 let docs = home.join("Documents");
205 let ps7 = docs.join("PowerShell").join("Microsoft.PowerShell_profile.ps1");
206 if ps7.exists() || !docs.join("WindowsPowerShell").exists() {
207 ps7
208 } else {
209 docs.join("WindowsPowerShell").join("Microsoft.PowerShell_profile.ps1")
210 }
211 }
212 #[cfg(not(windows))]
213 {
214 home.join(".config").join("powershell").join("Microsoft.PowerShell_profile.ps1")
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_generate_completion_script() {
224 let bash_script = generate_completion_script(Shell::Bash);
225 assert!(!bash_script.is_empty());
226 let bash_str = String::from_utf8_lossy(&bash_script);
227 assert!(bash_str.contains("git-notes"));
228 assert!(bash_str.contains("gn"));
229
230 let zsh_script = generate_completion_script(Shell::Zsh);
231 assert!(!zsh_script.is_empty());
232
233 let fish_script = generate_completion_script(Shell::Fish);
234 assert!(!fish_script.is_empty());
235
236 let ps_script = generate_completion_script(Shell::PowerShell);
237 assert!(!ps_script.is_empty());
238
239 let elvish_script = generate_completion_script(Shell::Elvish);
240 assert!(!elvish_script.is_empty());
241 }
242}