Skip to content

Commit b64edc6

Browse files
karan925helizaga
andauthored
[Bugfix] open with AI with fuzzy search (#140)
* fix(init): use fzf --expect for editor/ai actions so TUI apps get full terminal access * fix(init): resolve broken merge in bash/zsh fzf blocks The merge of main into fix-open-ai inserted new --expect code above the existing porcelain/empty-state logic instead of replacing it, leaving a dangling _gtr_selection subshell and duplicate variable declarations that produced syntax errors in the generated shell code. Move the _gtr_key/_gtr_line declarations to after the empty-state guard and remove the orphaned half-finished assignment lines so bash -n passes cleanly on the generated init output. * fix(init): handle Fish empty-line collapse in fzf --expect output Fish command substitution collapses empty lines, so when Enter is pressed with --expect, the empty key line disappears and the array has only 1 element instead of 2. Detect this by checking count and adjust indices accordingly. Also strengthen the fish enter test to validate string split + set dir parsing rather than just checking for 'cd '. --------- Co-authored-by: Tom Elizaga <tom.elizaga@gmail.com>
1 parent 584c17a commit b64edc6

2 files changed

Lines changed: 264 additions & 23 deletions

File tree

lib/commands/init.sh

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ __FUNC__() {
8989
echo "No worktrees to pick from. Create one with: git gtr new <branch>" >&2
9090
return 0
9191
fi
92-
local _gtr_selection
92+
local _gtr_selection _gtr_key _gtr_line
9393
_gtr_selection="$(printf '%s\n' "$_gtr_porcelain" | fzf \
9494
--delimiter=$'\t' \
9595
--with-nth=2 \
@@ -100,13 +100,23 @@ __FUNC__() {
100100
--header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \
101101
--preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \
102102
--preview-window=right:50% \
103-
--bind='ctrl-e:execute(git gtr editor {2})' \
104-
--bind='ctrl-a:execute(git gtr ai {2})' \
105-
--bind='ctrl-d:execute(git gtr rm {2})+reload(git gtr list --porcelain)' \
106-
--bind='ctrl-y:execute(git gtr copy {2})' \
103+
--expect=ctrl-a,ctrl-e \
104+
--bind='ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)' \
105+
--bind='ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)' \
107106
--bind='ctrl-r:reload(git gtr list --porcelain)')" || return 0
108107
[ -z "$_gtr_selection" ] && return 0
109-
dir="$(printf '%s' "$_gtr_selection" | cut -f1)"
108+
_gtr_key="$(head -1 <<< "$_gtr_selection")"
109+
_gtr_line="$(sed -n '2p' <<< "$_gtr_selection")"
110+
[ -z "$_gtr_line" ] && return 0
111+
# ctrl-a/ctrl-e: run after fzf exits (needs full terminal for TUI apps)
112+
if [ "$_gtr_key" = "ctrl-a" ]; then
113+
command git gtr ai "$(printf '%s' "$_gtr_line" | cut -f2)"
114+
return $?
115+
elif [ "$_gtr_key" = "ctrl-e" ]; then
116+
command git gtr editor "$(printf '%s' "$_gtr_line" | cut -f2)"
117+
return $?
118+
fi
119+
dir="$(printf '%s' "$_gtr_line" | cut -f1)"
110120
elif [ "$#" -eq 0 ]; then
111121
echo "Usage: __FUNC__ cd <branch>" >&2
112122
echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2
@@ -195,7 +205,7 @@ __FUNC__() {
195205
echo "No worktrees to pick from. Create one with: git gtr new <branch>" >&2
196206
return 0
197207
fi
198-
local _gtr_selection
208+
local _gtr_selection _gtr_key _gtr_line
199209
_gtr_selection="$(printf '%s\n' "$_gtr_porcelain" | fzf \
200210
--delimiter=$'\t' \
201211
--with-nth=2 \
@@ -206,13 +216,23 @@ __FUNC__() {
206216
--header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \
207217
--preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \
208218
--preview-window=right:50% \
209-
--bind='ctrl-e:execute(git gtr editor {2})' \
210-
--bind='ctrl-a:execute(git gtr ai {2})' \
211-
--bind='ctrl-d:execute(git gtr rm {2})+reload(git gtr list --porcelain)' \
212-
--bind='ctrl-y:execute(git gtr copy {2})' \
219+
--expect=ctrl-a,ctrl-e \
220+
--bind='ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)' \
221+
--bind='ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)' \
213222
--bind='ctrl-r:reload(git gtr list --porcelain)')" || return 0
214223
[ -z "$_gtr_selection" ] && return 0
215-
dir="$(printf '%s' "$_gtr_selection" | cut -f1)"
224+
_gtr_key="$(head -1 <<< "$_gtr_selection")"
225+
_gtr_line="$(sed -n '2p' <<< "$_gtr_selection")"
226+
[ -z "$_gtr_line" ] && return 0
227+
# ctrl-a/ctrl-e: run after fzf exits (needs full terminal for TUI apps)
228+
if [ "$_gtr_key" = "ctrl-a" ]; then
229+
command git gtr ai "$(printf '%s' "$_gtr_line" | cut -f2)"
230+
return $?
231+
elif [ "$_gtr_key" = "ctrl-e" ]; then
232+
command git gtr editor "$(printf '%s' "$_gtr_line" | cut -f2)"
233+
return $?
234+
fi
235+
dir="$(printf '%s' "$_gtr_line" | cut -f1)"
216236
elif [ "$#" -eq 0 ]; then
217237
echo "Usage: __FUNC__ cd <branch>" >&2
218238
echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2
@@ -314,14 +334,32 @@ function __FUNC__
314334
--header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \
315335
--preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \
316336
--preview-window=right:50% \
317-
--bind='ctrl-e:execute(git gtr editor {2})' \
318-
--bind='ctrl-a:execute(git gtr ai {2})' \
319-
--bind='ctrl-d:execute(git gtr rm {2})+reload(git gtr list --porcelain)' \
320-
--bind='ctrl-y:execute(git gtr copy {2})' \
337+
--expect=ctrl-a,ctrl-e \
338+
--bind='ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)' \
339+
--bind='ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)' \
321340
--bind='ctrl-r:reload(git gtr list --porcelain)')
322341
or return 0
323342
test -z "$_gtr_selection"; and return 0
324-
set dir (string split \t -- "$_gtr_selection")[1]
343+
# --expect gives two lines: key (index 1) and selection (index 2)
344+
# Fish collapses empty lines in command substitution, so when Enter
345+
# is pressed the empty key line disappears and count drops to 1.
346+
if test (count $_gtr_selection) -eq 1
347+
set -l _gtr_key ""
348+
set -l _gtr_line "$_gtr_selection[1]"
349+
else
350+
set -l _gtr_key "$_gtr_selection[1]"
351+
set -l _gtr_line "$_gtr_selection[2]"
352+
end
353+
test -z "$_gtr_line"; and return 0
354+
# ctrl-a/ctrl-e: run after fzf exits (needs full terminal for TUI apps)
355+
if test "$_gtr_key" = "ctrl-a"
356+
command git gtr ai (string split \t -- "$_gtr_line")[2]
357+
return $status
358+
else if test "$_gtr_key" = "ctrl-e"
359+
command git gtr editor (string split \t -- "$_gtr_line")[2]
360+
return $status
361+
end
362+
set dir (string split \t -- "$_gtr_line")[1]
325363
else if test (count $argv) -eq 1
326364
echo "Usage: __FUNC__ cd <branch>" >&2
327365
echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2

tests/init.bats

Lines changed: 209 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,39 +160,242 @@ setup() {
160160
161161
# ── fzf interactive picker ───────────────────────────────────────────────────
162162
163-
@test "bash output includes fzf picker for cd with no args" {
163+
# ── fzf: general setup ──────────────────────────────────────────────────────
164+
165+
@test "bash output includes fzf detection for cd with no args" {
164166
run cmd_init bash
165167
[ "$status" -eq 0 ]
166168
[[ "$output" == *"command -v fzf"* ]]
167169
[[ "$output" == *"--prompt='Worktree> '"* ]]
168170
[[ "$output" == *"--with-nth=2"* ]]
169-
[[ "$output" == *"ctrl-e:execute"* ]]
170171
}
171172
172-
@test "zsh output includes fzf picker for cd with no args" {
173+
@test "zsh output includes fzf detection for cd with no args" {
173174
run cmd_init zsh
174175
[ "$status" -eq 0 ]
175176
[[ "$output" == *"command -v fzf"* ]]
176177
[[ "$output" == *"--prompt='Worktree> '"* ]]
177178
[[ "$output" == *"--with-nth=2"* ]]
178-
[[ "$output" == *"ctrl-e:execute"* ]]
179179
}
180180
181-
@test "fish output includes fzf picker for cd with no args" {
181+
@test "fish output includes fzf detection for cd with no args" {
182182
run cmd_init fish
183183
[ "$status" -eq 0 ]
184184
[[ "$output" == *"type -q fzf"* ]]
185185
[[ "$output" == *"--prompt='Worktree> '"* ]]
186186
[[ "$output" == *"--with-nth=2"* ]]
187-
[[ "$output" == *"ctrl-e:execute"* ]]
188187
}
189188
189+
# ── fzf: header shows all keybindings ───────────────────────────────────────
190+
191+
@test "bash fzf header lists all keybindings" {
192+
run cmd_init bash
193+
[ "$status" -eq 0 ]
194+
[[ "$output" == *"enter:cd"* ]]
195+
[[ "$output" == *"ctrl-e:editor"* ]]
196+
[[ "$output" == *"ctrl-a:ai"* ]]
197+
[[ "$output" == *"ctrl-d:delete"* ]]
198+
[[ "$output" == *"ctrl-y:copy"* ]]
199+
[[ "$output" == *"ctrl-r:refresh"* ]]
200+
}
201+
202+
@test "zsh fzf header lists all keybindings" {
203+
run cmd_init zsh
204+
[ "$status" -eq 0 ]
205+
[[ "$output" == *"enter:cd"* ]]
206+
[[ "$output" == *"ctrl-e:editor"* ]]
207+
[[ "$output" == *"ctrl-a:ai"* ]]
208+
[[ "$output" == *"ctrl-d:delete"* ]]
209+
[[ "$output" == *"ctrl-y:copy"* ]]
210+
[[ "$output" == *"ctrl-r:refresh"* ]]
211+
}
212+
213+
@test "fish fzf header lists all keybindings" {
214+
run cmd_init fish
215+
[ "$status" -eq 0 ]
216+
[[ "$output" == *"enter:cd"* ]]
217+
[[ "$output" == *"ctrl-e:editor"* ]]
218+
[[ "$output" == *"ctrl-a:ai"* ]]
219+
[[ "$output" == *"ctrl-d:delete"* ]]
220+
[[ "$output" == *"ctrl-y:copy"* ]]
221+
[[ "$output" == *"ctrl-r:refresh"* ]]
222+
}
223+
224+
# ── fzf: enter (cd) ─────────────────────────────────────────────────────────
225+
226+
@test "bash fzf enter extracts path from selection field 1 and cd" {
227+
run cmd_init bash
228+
[ "$status" -eq 0 ]
229+
# Selection is parsed with cut -f1 to get path, then cd
230+
[[ "$output" == *'cut -f1'* ]]
231+
[[ "$output" == *'cd "$dir"'* ]]
232+
}
233+
234+
@test "zsh fzf enter extracts path from selection field 1 and cd" {
235+
run cmd_init zsh
236+
[ "$status" -eq 0 ]
237+
[[ "$output" == *'cut -f1'* ]]
238+
[[ "$output" == *'cd "$dir"'* ]]
239+
}
240+
241+
@test "fish fzf enter extracts path from selection and cd" {
242+
run cmd_init fish
243+
[ "$status" -eq 0 ]
244+
# Fish uses string split to extract path, then cd
245+
[[ "$output" == *'string split'* ]]
246+
[[ "$output" == *'set dir'* ]]
247+
[[ "$output" == *'cd $dir'* ]]
248+
}
249+
250+
# ── fzf: ctrl-e (editor) — via --expect ──────────────────────────────────────
251+
252+
@test "bash fzf ctrl-e handled via --expect for full terminal access" {
253+
run cmd_init bash
254+
[ "$status" -eq 0 ]
255+
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
256+
[[ "$output" == *'git gtr editor'* ]]
257+
}
258+
259+
@test "zsh fzf ctrl-e handled via --expect for full terminal access" {
260+
run cmd_init zsh
261+
[ "$status" -eq 0 ]
262+
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
263+
[[ "$output" == *'git gtr editor'* ]]
264+
}
265+
266+
@test "fish fzf ctrl-e handled via --expect for full terminal access" {
267+
run cmd_init fish
268+
[ "$status" -eq 0 ]
269+
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
270+
[[ "$output" == *'git gtr editor'* ]]
271+
}
272+
273+
# ── fzf: ctrl-a (ai) — via --expect ─────────────────────────────────────────
274+
275+
@test "bash fzf ctrl-a runs git gtr ai after fzf exits" {
276+
run cmd_init bash
277+
[ "$status" -eq 0 ]
278+
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
279+
[[ "$output" == *'git gtr ai'* ]]
280+
}
281+
282+
@test "zsh fzf ctrl-a runs git gtr ai after fzf exits" {
283+
run cmd_init zsh
284+
[ "$status" -eq 0 ]
285+
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
286+
[[ "$output" == *'git gtr ai'* ]]
287+
}
288+
289+
@test "fish fzf ctrl-a runs git gtr ai after fzf exits" {
290+
run cmd_init fish
291+
[ "$status" -eq 0 ]
292+
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
293+
[[ "$output" == *'git gtr ai'* ]]
294+
}
295+
296+
# ── fzf: ctrl-d (delete + reload) ───────────────────────────────────────────
297+
298+
@test "bash fzf ctrl-d runs git gtr rm and reloads list" {
299+
run cmd_init bash
300+
[ "$status" -eq 0 ]
301+
[[ "$output" == *"ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)"* ]]
302+
}
303+
304+
@test "zsh fzf ctrl-d runs git gtr rm and reloads list" {
305+
run cmd_init zsh
306+
[ "$status" -eq 0 ]
307+
[[ "$output" == *"ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)"* ]]
308+
}
309+
310+
@test "fish fzf ctrl-d runs git gtr rm and reloads list" {
311+
run cmd_init fish
312+
[ "$status" -eq 0 ]
313+
[[ "$output" == *"ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)"* ]]
314+
}
315+
316+
# ── fzf: ctrl-y (copy) ──────────────────────────────────────────────────────
317+
318+
@test "bash fzf ctrl-y runs git gtr copy on selected branch" {
319+
run cmd_init bash
320+
[ "$status" -eq 0 ]
321+
[[ "$output" == *"ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)"* ]]
322+
}
323+
324+
@test "zsh fzf ctrl-y runs git gtr copy on selected branch" {
325+
run cmd_init zsh
326+
[ "$status" -eq 0 ]
327+
[[ "$output" == *"ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)"* ]]
328+
}
329+
330+
@test "fish fzf ctrl-y runs git gtr copy on selected branch" {
331+
run cmd_init fish
332+
[ "$status" -eq 0 ]
333+
[[ "$output" == *"ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)"* ]]
334+
}
335+
336+
# ── fzf: ctrl-r (refresh) ───────────────────────────────────────────────────
337+
338+
@test "bash fzf ctrl-r reloads worktree list" {
339+
run cmd_init bash
340+
[ "$status" -eq 0 ]
341+
[[ "$output" == *"ctrl-r:reload(git gtr list --porcelain)"* ]]
342+
}
343+
344+
@test "zsh fzf ctrl-r reloads worktree list" {
345+
run cmd_init zsh
346+
[ "$status" -eq 0 ]
347+
[[ "$output" == *"ctrl-r:reload(git gtr list --porcelain)"* ]]
348+
}
349+
350+
@test "fish fzf ctrl-r reloads worktree list" {
351+
run cmd_init fish
352+
[ "$status" -eq 0 ]
353+
[[ "$output" == *"ctrl-r:reload(git gtr list --porcelain)"* ]]
354+
}
355+
356+
# ── fzf: preview window ─────────────────────────────────────────────────────
357+
358+
@test "bash fzf preview shows git log and status" {
359+
run cmd_init bash
360+
[ "$status" -eq 0 ]
361+
[[ "$output" == *"--preview="* ]]
362+
[[ "$output" == *"git -C {1} log --oneline --graph --color=always"* ]]
363+
[[ "$output" == *"git -C {1} status --short"* ]]
364+
[[ "$output" == *"--preview-window=right:50%"* ]]
365+
}
366+
367+
@test "zsh fzf preview shows git log and status" {
368+
run cmd_init zsh
369+
[ "$status" -eq 0 ]
370+
[[ "$output" == *"--preview="* ]]
371+
[[ "$output" == *"git -C {1} log --oneline --graph --color=always"* ]]
372+
[[ "$output" == *"git -C {1} status --short"* ]]
373+
[[ "$output" == *"--preview-window=right:50%"* ]]
374+
}
375+
376+
@test "fish fzf preview shows git log and status" {
377+
run cmd_init fish
378+
[ "$status" -eq 0 ]
379+
[[ "$output" == *"--preview="* ]]
380+
[[ "$output" == *"git -C {1} log --oneline --graph --color=always"* ]]
381+
[[ "$output" == *"git -C {1} status --short"* ]]
382+
[[ "$output" == *"--preview-window=right:50%"* ]]
383+
}
384+
385+
# ── fzf: fallback messages ──────────────────────────────────────────────────
386+
190387
@test "bash output shows fzf install hint when no args and no fzf" {
191388
run cmd_init bash
192389
[ "$status" -eq 0 ]
193390
[[ "$output" == *'Install fzf for an interactive picker'* ]]
194391
}
195392
393+
@test "zsh output shows fzf install hint when no args and no fzf" {
394+
run cmd_init zsh
395+
[ "$status" -eq 0 ]
396+
[[ "$output" == *'Install fzf for an interactive picker'* ]]
397+
}
398+
196399
@test "fish output shows fzf install hint when no args and no fzf" {
197400
run cmd_init fish
198401
[ "$status" -eq 0 ]

0 commit comments

Comments
 (0)