git_worktree_manager/
shell_functions.rs1pub fn generate(shell: &str) -> Option<String> {
6 match shell {
7 "bash" | "zsh" => Some(BASH_ZSH_FUNCTION.to_string()),
8 "fish" => Some(FISH_FUNCTION.to_string()),
9 "powershell" | "pwsh" => Some(POWERSHELL_FUNCTION.to_string()),
10 _ => None,
11 }
12}
13
14const BASH_ZSH_FUNCTION: &str = r#"# git-worktree-manager shell functions for bash/zsh
15# Source this file to enable shell functions:
16# source <(gw _shell-function bash)
17
18# Navigate to a worktree by branch name or worktree name.
19# If no argument is provided, show interactive worktree selector.
20gw-cd() {
21 local target=""
22
23 # Parse arguments
24 while [ $# -gt 0 ]; do
25 case "$1" in
26 -*)
27 echo "Error: Unknown option '$1'" >&2
28 echo "Usage: gw-cd [branch|worktree-name]" >&2
29 return 1
30 ;;
31 *)
32 target="$1"
33 shift
34 ;;
35 esac
36 done
37
38 local worktree_path
39
40 if [ -z "$target" ]; then
41 # No argument — interactive selector
42 worktree_path=$(gw _path --interactive)
43 if [ $? -ne 0 ]; then return 1; fi
44 else
45 # Resolve via git worktree list (branch name or worktree name)
46 worktree_path=$(git worktree list --porcelain 2>/dev/null | awk -v t="$target" '
47 /^worktree / { path=$2; name=path; sub(".*/", "", name) }
48 /^branch / && $2 == "refs/heads/"t { print path; exit }
49 /^worktree / && name == t { found_path=path }
50 END { if (found_path != "") print found_path }
51 ')
52 fi
53
54 if [ -z "$worktree_path" ]; then
55 echo "Error: No worktree found for '$target'" >&2
56 return 1
57 fi
58
59 if [ -d "$worktree_path" ]; then
60 cd "$worktree_path" || return 1
61 echo "Switched to worktree: $worktree_path"
62 else
63 echo "Error: Worktree directory not found: $worktree_path" >&2
64 return 1
65 fi
66}
67
68# Tab completion for gw-cd (bash)
69_gw_cd_completion() {
70 local cur="${COMP_WORDS[COMP_CWORD]}"
71
72 local targets
73 targets=$(gw _complete-targets 2>/dev/null)
74 COMPREPLY=($(compgen -W "$targets" -- "$cur"))
75}
76
77# Register completion for bash
78if [ -n "$BASH_VERSION" ]; then
79 complete -F _gw_cd_completion gw-cd
80 eval "$(gw --generate-completion bash 2>/dev/null || true)"
81
82 # Wrap _gw to add dynamic completion for positional-target subcommands
83 _gw_with_config() {
84 local cur="${COMP_WORDS[COMP_CWORD]}"
85 local subcmd="${COMP_WORDS[1]}"
86
87 if [[ "$cur" != -* ]]; then
88 local pos_count=0
89 local start_idx=2
90 local max_pos=0
91
92 case "$subcmd" in
93 rm|resume|spawn|exec)
94 max_pos=1
95 ;;
96 esac
97
98 if [[ $max_pos -gt 0 ]]; then
99 local i
100 for ((i=start_idx; i<COMP_CWORD; i++)); do
101 [[ ${COMP_WORDS[i]} != -* ]] && ((pos_count++))
102 done
103 if [[ $pos_count -lt $max_pos ]]; then
104 local targets
105 targets=$(gw _complete-targets 2>/dev/null)
106 COMPREPLY=($(compgen -W "$targets" -- "$cur"))
107 return
108 fi
109 fi
110 fi
111
112 _gw "$@"
113 }
114 complete -F _gw_with_config -o bashdefault -o default gw
115fi
116
117# Tab completion for zsh
118if [ -n "$ZSH_VERSION" ]; then
119 # Register clap completion for the gw CLI inline
120 eval "$(gw --generate-completion zsh 2>/dev/null)"
121
122 # Wrap _gw to add dynamic completion (targets)
123 _gw_with_config() {
124 local subcmd="${words[2]}"
125
126 if [[ "${words[CURRENT]}" != -* ]]; then
127 local -i pos_count=0
128 local -i start_idx=3
129 local -i max_pos=0
130
131 case "$subcmd" in
132 rm|resume|spawn|exec)
133 max_pos=1
134 ;;
135 esac
136
137 if [[ $max_pos -gt 0 ]]; then
138 local -i i
139 for ((i=start_idx; i<CURRENT; i++)); do
140 [[ ${words[i]} != -* ]] && ((pos_count++))
141 done
142 if [[ $pos_count -lt $max_pos ]]; then
143 local -a targets
144 targets=(${(f)"$(gw _complete-targets 2>/dev/null)"})
145 compadd -a targets
146 return
147 fi
148 fi
149 fi
150
151 _gw "$@"
152 }
153 compdef _gw_with_config gw
154
155 _gw_cd_zsh() {
156 local -a targets
157 targets=(${(f)"$(gw _complete-targets 2>/dev/null)"})
158 compadd -a targets
159 }
160 compdef _gw_cd_zsh gw-cd
161fi
162"#;
163
164const FISH_FUNCTION: &str = r#"# git-worktree-manager shell functions for fish
165# Source this file to enable shell functions:
166# gw _shell-function fish | source
167
168# Navigate to a worktree by branch name or worktree name.
169# If no argument is provided, show interactive worktree selector.
170function gw-cd
171 set -l target ""
172
173 # Parse arguments
174 for arg in $argv
175 switch $arg
176 case '-*'
177 echo "Error: Unknown option '$arg'" >&2
178 echo "Usage: gw-cd [branch|worktree-name]" >&2
179 return 1
180 case '*'
181 set target $arg
182 end
183 end
184
185 set -l worktree_path
186
187 if test -z "$target"
188 # No argument — interactive selector
189 set worktree_path (gw _path --interactive)
190 if test $status -ne 0
191 return 1
192 end
193 else
194 # Resolve via git worktree list (branch name or worktree name)
195 set worktree_path (git worktree list --porcelain 2>/dev/null | awk -v t="$target" '
196 /^worktree / { path=$2; name=path; sub(".*/", "", name) }
197 /^branch / && $2 == "refs/heads/"t { print path; exit }
198 /^worktree / && name == t { found_path=path }
199 END { if (found_path != "") print found_path }
200 ')
201 end
202
203 if test -z "$worktree_path"
204 echo "Error: No worktree found for '$target'" >&2
205 return 1
206 end
207
208 if test -d "$worktree_path"
209 cd "$worktree_path"; or return 1
210 echo "Switched to worktree: $worktree_path"
211 else
212 echo "Error: Worktree directory not found: $worktree_path" >&2
213 return 1
214 end
215end
216
217# Tab completion for gw-cd
218complete -c gw-cd -f -a '(gw _complete-targets 2>/dev/null)'
219
220# Tab completion for the gw CLI (clap-generated)
221gw --generate-completion fish 2>/dev/null | source
222
223# Target completion for subcommands with positional target args
224for cmd in rm resume spawn exec
225 complete -c gw -f -n "__fish_seen_subcommand_from $cmd" -a '(gw _complete-targets 2>/dev/null)'
226end
227
228"#;
229
230const POWERSHELL_FUNCTION: &str = r#"# git-worktree-manager shell functions for PowerShell
231# Source this file to enable shell functions:
232# gw _shell-function powershell | Out-String | Invoke-Expression
233
234# Navigate to a worktree by branch name or worktree name.
235# If no argument is provided, show interactive worktree selector.
236function gw-cd {
237 param(
238 [Parameter(Mandatory=$false, Position=0)]
239 [string]$Target
240 )
241
242 $worktreePath = $null
243
244 if (-not $Target) {
245 # No argument — interactive selector
246 $worktreePath = gw _path --interactive
247 if ($LASTEXITCODE -ne 0) {
248 return
249 }
250 } else {
251 # Resolve via git worktree list (branch name or worktree name)
252 $lines = git worktree list --porcelain 2>&1 | Where-Object { $_ -is [string] }
253 $currentPath = $null
254 foreach ($line in $lines) {
255 if ($line -match '^worktree (.+)$') {
256 $currentPath = $Matches[1]
257 } elseif ($line -match "^branch refs/heads/$([regex]::Escape($Target))$") {
258 $worktreePath = $currentPath
259 break
260 }
261 }
262 if (-not $worktreePath) {
263 # Try matching by worktree directory name
264 foreach ($line in $lines) {
265 if ($line -match '^worktree (.+)$') {
266 $p = $Matches[1]
267 if ([System.IO.Path]::GetFileName($p) -eq $Target) {
268 $worktreePath = $p
269 break
270 }
271 }
272 }
273 }
274 }
275
276 if (-not $worktreePath) {
277 Write-Error "Error: No worktree found for '$Target'"
278 return
279 }
280
281 if (Test-Path -Path $worktreePath -PathType Container) {
282 Set-Location -Path $worktreePath
283 Write-Host "Switched to worktree: $worktreePath"
284 } else {
285 Write-Error "Error: Worktree directory not found: $worktreePath"
286 return
287 }
288}
289
290# Tab completion for gw-cd
291Register-ArgumentCompleter -CommandName gw-cd -ParameterName Target -ScriptBlock {
292 param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
293
294 $targets = gw _complete-targets 2>&1 |
295 Where-Object { $_ -is [string] -and $_.Trim() } |
296 Sort-Object -Unique
297
298 $targets | Where-Object { $_ -like "$wordToComplete*" } |
299 ForEach-Object {
300 [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
301 }
302}
303
304# Native target completion for `gw` subcommands: rm, resume, spawn, exec
305Register-ArgumentCompleter -CommandName gw -Native -ScriptBlock {
306 param($wordToComplete, $commandAst, $cursorPosition)
307
308 # Find the subcommand (first non-flag element after the command name)
309 $elements = $commandAst.CommandElements
310 if ($elements.Count -lt 2) { return }
311 $subcmd = $elements[1].Value
312
313 # Only complete positional targets for these subcommands
314 if ($subcmd -notin @('rm', 'resume', 'spawn', 'exec')) { return }
315
316 # Get completion targets from gw
317 $targets = & gw _complete-targets 2>$null
318
319 $targets | Where-Object { $_ -like "$wordToComplete*" } |
320 ForEach-Object {
321 [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
322 }
323}
324"#;
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn test_generate_bash() {
332 let result = generate("bash");
333 assert!(result.is_some());
334 let script = result.unwrap();
335 assert!(script.contains("gw-cd()"));
336 assert!(script.contains("_gw_cd_completion"));
337 assert!(script.contains("BASH_VERSION"));
338 assert!(script.contains("ZSH_VERSION"));
339 assert!(script.contains("_gw_cd_zsh"));
340 assert!(!script.contains("cw-cd"));
341 assert!(!script.contains("complete -F _gw_with_config -o bashdefault -o default cw"));
342 }
343
344 #[test]
345 fn test_generate_zsh() {
346 let result = generate("zsh");
347 assert!(result.is_some());
348 let script = result.unwrap();
349 assert!(script.contains("compdef _gw_cd_zsh gw-cd"));
350 assert!(!script.contains("cw-cd"));
351 assert!(!script.contains("compdef _gw_with_config cw"));
352 }
353
354 #[test]
355 fn test_generate_fish() {
356 let result = generate("fish");
357 assert!(result.is_some());
358 let script = result.unwrap();
359 assert!(script.contains("function gw-cd"));
360 assert!(script.contains("complete -c gw-cd"));
361 assert!(!script.contains("cw-cd"));
362 assert!(!script.contains("complete -c cw "));
363 }
364
365 #[test]
366 fn test_generate_powershell() {
367 let result = generate("powershell");
368 assert!(result.is_some());
369 let script = result.unwrap();
370 assert!(script.contains("function gw-cd"));
371 assert!(script.contains("Register-ArgumentCompleter"));
372 assert!(!script.contains("cw-cd"));
373 assert!(!script.contains("CommandName gw, cw"));
374 }
375
376 #[test]
377 fn test_powershell_uses_complete_targets() {
378 let script = generate("powershell").unwrap();
379 assert!(
380 script.contains("gw _complete-targets"),
381 "PowerShell script should call gw _complete-targets"
382 );
383 assert!(
384 script.contains("Register-ArgumentCompleter -CommandName gw"),
385 "PowerShell script should register a completer for `gw`"
386 );
387 }
388
389 #[test]
390 fn test_generate_pwsh_alias() {
391 let result = generate("pwsh");
392 assert!(result.is_some());
393 assert_eq!(result, generate("powershell"));
395 }
396
397 #[test]
398 fn test_generate_unknown() {
399 assert!(generate("unknown").is_none());
400 assert!(generate("").is_none());
401 }
402
403 #[test]
405 fn test_bash_uses_complete_targets() {
406 let script = generate("bash").unwrap();
407 assert!(
408 script.contains("gw _complete-targets"),
409 "bash script should use gw _complete-targets for completions"
410 );
411 assert!(
412 !script.contains("_path --list-branches"),
413 "bash script should not reference the removed _path --list-branches"
414 );
415 }
416
417 #[test]
419 #[cfg(not(windows))]
420 fn test_bash_script_syntax() {
421 let script = generate("bash").unwrap();
422
423 let output = std::process::Command::new("bash")
425 .arg("-n")
426 .stdin(std::process::Stdio::piped())
427 .stdout(std::process::Stdio::piped())
428 .stderr(std::process::Stdio::piped())
429 .spawn()
430 .and_then(|mut child| {
431 use std::io::Write;
432 child.stdin.take().unwrap().write_all(script.as_bytes())?;
433 child.wait_with_output()
434 });
435
436 match output {
437 Ok(out) => {
438 let stderr = String::from_utf8_lossy(&out.stderr);
439 assert!(
440 out.status.success(),
441 "bash -n failed for generated bash/zsh script:\n{}",
442 stderr
443 );
444 }
445 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
446 eprintln!("bash not found, skipping syntax check");
447 }
448 Err(e) => panic!("failed to run bash -n: {}", e),
449 }
450 }
451
452 #[test]
454 fn test_fish_script_syntax() {
455 let script = generate("fish").unwrap();
456
457 let output = std::process::Command::new("fish")
458 .arg("--no-execute")
459 .stdin(std::process::Stdio::piped())
460 .stdout(std::process::Stdio::piped())
461 .stderr(std::process::Stdio::piped())
462 .spawn()
463 .and_then(|mut child| {
464 use std::io::Write;
465 child.stdin.take().unwrap().write_all(script.as_bytes())?;
466 child.wait_with_output()
467 });
468
469 match output {
470 Ok(out) => {
471 let stderr = String::from_utf8_lossy(&out.stderr);
472 assert!(
473 out.status.success(),
474 "fish --no-execute failed for generated fish script:\n{}",
475 stderr
476 );
477 }
478 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
479 eprintln!("fish not found, skipping syntax check");
480 }
481 Err(e) => panic!("failed to run fish --no-execute: {}", e),
482 }
483 }
484}