Skip to content

Commit daee0c1

Browse files
authored
feat(init): interactive fzf worktree picker + fix zsh locale error (#134) (#136)
1 parent c74f843 commit daee0c1

File tree

5 files changed

+155
-5
lines changed

5 files changed

+155
-5
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ git gtr ai my-feature # Start claude
100100
git gtr run my-feature npm test # Run tests
101101

102102
# Navigate to worktree
103+
gtr cd # Interactive picker (requires fzf + shell integration)
103104
gtr cd my-feature # Requires: eval "$(git gtr init bash)"
104105
cd "$(git gtr go my-feature)" # Alternative without shell integration
105106

@@ -217,10 +218,13 @@ cd "$(git gtr go 1)" # Navigate to main repo
217218
eval "$(git gtr init bash)"
218219

219220
# Then navigate with:
221+
gtr cd # Interactive worktree picker (requires fzf)
220222
gtr cd my-feature
221223
gtr cd 1
222224
```
223225

226+
With [fzf](https://github.com/junegunn/fzf) installed, `gtr cd` (no arguments) opens a command palette with git log preview and keybindings: `ctrl-e` editor, `ctrl-a` AI, `ctrl-d` delete, `ctrl-y` copy, `ctrl-r` refresh.
227+
224228
> **Note:** If `gtr` conflicts with another command (e.g., GNU `tr` from coreutils), use `--as` to pick a different name:
225229
>
226230
> ```bash

lib/commands/doctor.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ cmd_doctor() {
8888
# Check hosting provider
8989
if [ -n "$repo_root" ]; then
9090
local provider
91-
provider=$(detect_provider 2>/dev/null)
91+
provider=$(detect_provider 2>/dev/null) || true
9292
if [ -n "$provider" ]; then
9393
echo "[OK] Provider: $provider"
9494
case "$provider" in
@@ -112,6 +112,13 @@ cmd_doctor() {
112112
fi
113113
fi
114114

115+
# Check fzf (optional, for interactive picker)
116+
if command -v fzf >/dev/null 2>&1; then
117+
echo "[OK] fzf: $(fzf --version 2>/dev/null | awk '{print $1}') (interactive picker available)"
118+
else
119+
echo "[i] fzf: not found (install for interactive picker: gtr cd)"
120+
fi
121+
115122
echo ""
116123
if [ "$issues" -eq 0 ]; then
117124
echo "Everything looks good!"

lib/commands/help.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,17 @@ Setup:
367367
After setup:
368368
gtr cd my-feature # cd to worktree
369369
gtr cd 1 # cd to main repo
370+
gtr cd # interactive picker (requires fzf)
370371
gtr <command> # same as git gtr <command>
372+
373+
Command palette (gtr cd with no arguments, requires fzf):
374+
enter cd into selected worktree
375+
ctrl-e open in editor
376+
ctrl-a start AI tool
377+
ctrl-d delete worktree (with confirmation)
378+
ctrl-y copy files to worktree
379+
ctrl-r refresh list
380+
esc cancel
371381
EOF
372382
}
373383

@@ -549,6 +559,7 @@ SETUP & MAINTENANCE:
549559
Generate shell integration for cd support (bash, zsh, fish)
550560
--as <name>: custom function name (default: gtr)
551561
Usage: eval "$(git gtr init bash)"
562+
With fzf: 'gtr cd' opens a command palette (preview, editor, AI, delete)
552563
553564
version
554565
Show version
@@ -572,6 +583,7 @@ WORKFLOW EXAMPLES:
572583
git gtr run feature/user-auth npm run dev # Start dev server
573584
574585
# Navigate to worktree directory
586+
gtr cd # Interactive picker (requires fzf)
575587
gtr cd feature/user-auth # With shell integration (git gtr init)
576588
cd "$(git gtr go feature/user-auth)" # Without shell integration
577589

lib/commands/init.sh

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,33 @@ __FUNC__() {
8282
if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then
8383
shift
8484
local dir
85-
dir="$(command git gtr go "$@")" && cd "$dir" && {
85+
if [ "$#" -eq 0 ] && command -v fzf >/dev/null 2>&1; then
86+
local _gtr_selection
87+
_gtr_selection="$(command git gtr list --porcelain | fzf \
88+
--delimiter=$'\t' \
89+
--with-nth=2 \
90+
--ansi \
91+
--layout=reverse \
92+
--border \
93+
--prompt='Worktree> ' \
94+
--header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \
95+
--preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \
96+
--preview-window=right:50% \
97+
--bind='ctrl-e:execute(git gtr editor {2})' \
98+
--bind='ctrl-a:execute(git gtr ai {2})' \
99+
--bind='ctrl-d:execute(git gtr rm {2})+reload(git gtr list --porcelain)' \
100+
--bind='ctrl-y:execute(git gtr copy {2})' \
101+
--bind='ctrl-r:reload(git gtr list --porcelain)')" || return 0
102+
[ -z "$_gtr_selection" ] && return 0
103+
dir="$(printf '%s' "$_gtr_selection" | cut -f1)"
104+
elif [ "$#" -eq 0 ]; then
105+
echo "Usage: __FUNC__ cd <branch>" >&2
106+
echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2
107+
return 1
108+
else
109+
dir="$(command git gtr go "$@")" || return $?
110+
fi
111+
cd "$dir" && {
86112
local _gtr_hooks _gtr_hook _gtr_seen _gtr_config_file
87113
_gtr_hooks=""
88114
_gtr_seen=""
@@ -152,10 +178,37 @@ _init_zsh() {
152178
# eval "$(git gtr init zsh)"
153179
154180
__FUNC__() {
181+
emulate -L zsh
155182
if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then
156183
shift
157184
local dir
158-
dir="$(command git gtr go "$@")" && cd "$dir" && {
185+
if [ "$#" -eq 0 ] && command -v fzf >/dev/null 2>&1; then
186+
local _gtr_selection
187+
_gtr_selection="$(command git gtr list --porcelain | fzf \
188+
--delimiter=$'\t' \
189+
--with-nth=2 \
190+
--ansi \
191+
--layout=reverse \
192+
--border \
193+
--prompt='Worktree> ' \
194+
--header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \
195+
--preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \
196+
--preview-window=right:50% \
197+
--bind='ctrl-e:execute(git gtr editor {2})' \
198+
--bind='ctrl-a:execute(git gtr ai {2})' \
199+
--bind='ctrl-d:execute(git gtr rm {2})+reload(git gtr list --porcelain)' \
200+
--bind='ctrl-y:execute(git gtr copy {2})' \
201+
--bind='ctrl-r:reload(git gtr list --porcelain)')" || return 0
202+
[ -z "$_gtr_selection" ] && return 0
203+
dir="$(printf '%s' "$_gtr_selection" | cut -f1)"
204+
elif [ "$#" -eq 0 ]; then
205+
echo "Usage: __FUNC__ cd <branch>" >&2
206+
echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2
207+
return 1
208+
else
209+
dir="$(command git gtr go "$@")" || return $?
210+
fi
211+
cd "$dir" && {
159212
local _gtr_hooks _gtr_hook _gtr_seen _gtr_config_file
160213
_gtr_hooks=""
161214
_gtr_seen=""
@@ -232,8 +285,35 @@ _init_fish() {
232285
233286
function __FUNC__
234287
if test (count $argv) -gt 0; and test "$argv[1]" = "cd"
235-
set -l dir (command git gtr go $argv[2..])
236-
and cd $dir
288+
set -l dir
289+
if test (count $argv) -eq 1; and type -q fzf
290+
set -l _gtr_selection (command git gtr list --porcelain | fzf \
291+
--delimiter=\t \
292+
--with-nth=2 \
293+
--ansi \
294+
--layout=reverse \
295+
--border \
296+
--prompt='Worktree> ' \
297+
--header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \
298+
--preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \
299+
--preview-window=right:50% \
300+
--bind='ctrl-e:execute(git gtr editor {2})' \
301+
--bind='ctrl-a:execute(git gtr ai {2})' \
302+
--bind='ctrl-d:execute(git gtr rm {2})+reload(git gtr list --porcelain)' \
303+
--bind='ctrl-y:execute(git gtr copy {2})' \
304+
--bind='ctrl-r:reload(git gtr list --porcelain)')
305+
or return 0
306+
test -z "$_gtr_selection"; and return 0
307+
set dir (string split \t -- "$_gtr_selection")[1]
308+
else if test (count $argv) -eq 1
309+
echo "Usage: __FUNC__ cd <branch>" >&2
310+
echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2
311+
return 1
312+
else
313+
set dir (command git gtr go $argv[2..])
314+
or return $status
315+
end
316+
cd $dir
237317
and begin
238318
set -l _gtr_hooks
239319
set -l _gtr_seen

tests/init.bats

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,53 @@ setup() {
158158
[ "$status" -eq 1 ]
159159
}
160160
161+
# ── fzf interactive picker ───────────────────────────────────────────────────
162+
163+
@test "bash output includes fzf picker for cd with no args" {
164+
run cmd_init bash
165+
[ "$status" -eq 0 ]
166+
[[ "$output" == *"command -v fzf"* ]]
167+
[[ "$output" == *"--prompt='Worktree> '"* ]]
168+
[[ "$output" == *"--with-nth=2"* ]]
169+
[[ "$output" == *"ctrl-e:execute"* ]]
170+
}
171+
172+
@test "zsh output includes fzf picker for cd with no args" {
173+
run cmd_init zsh
174+
[ "$status" -eq 0 ]
175+
[[ "$output" == *"command -v fzf"* ]]
176+
[[ "$output" == *"--prompt='Worktree> '"* ]]
177+
[[ "$output" == *"--with-nth=2"* ]]
178+
[[ "$output" == *"ctrl-e:execute"* ]]
179+
}
180+
181+
@test "fish output includes fzf picker for cd with no args" {
182+
run cmd_init fish
183+
[ "$status" -eq 0 ]
184+
[[ "$output" == *"type -q fzf"* ]]
185+
[[ "$output" == *"--prompt='Worktree> '"* ]]
186+
[[ "$output" == *"--with-nth=2"* ]]
187+
[[ "$output" == *"ctrl-e:execute"* ]]
188+
}
189+
190+
@test "bash output shows fzf install hint when no args and no fzf" {
191+
run cmd_init bash
192+
[ "$status" -eq 0 ]
193+
[[ "$output" == *'Install fzf for an interactive picker'* ]]
194+
}
195+
196+
@test "fish output shows fzf install hint when no args and no fzf" {
197+
run cmd_init fish
198+
[ "$status" -eq 0 ]
199+
[[ "$output" == *'Install fzf for an interactive picker'* ]]
200+
}
201+
202+
@test "--as replaces function name in fzf fallback message" {
203+
run cmd_init bash --as gwtr
204+
[ "$status" -eq 0 ]
205+
[[ "$output" == *'Usage: gwtr cd <branch>'* ]]
206+
}
207+
161208
# ── git gtr passthrough preserved ────────────────────────────────────────────
162209
163210
@test "bash output passes non-cd commands to git gtr" {

0 commit comments

Comments
 (0)