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