Skip to content

Commit b024b94

Browse files
committed
fix(init): use fzf --expect for editor/ai actions so TUI apps get full terminal access
1 parent daee0c1 commit b024b94

File tree

2 files changed

+255
-23
lines changed

2 files changed

+255
-23
lines changed

lib/commands/init.sh

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ __FUNC__() {
8383
shift
8484
local dir
8585
if [ "$#" -eq 0 ] && command -v fzf >/dev/null 2>&1; then
86-
local _gtr_selection
86+
local _gtr_selection _gtr_key _gtr_line
8787
_gtr_selection="$(command git gtr list --porcelain | fzf \
8888
--delimiter=$'\t' \
8989
--with-nth=2 \
@@ -94,13 +94,23 @@ __FUNC__() {
9494
--header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \
9595
--preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \
9696
--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})' \
97+
--expect=ctrl-a,ctrl-e \
98+
--bind='ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)' \
99+
--bind='ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)' \
101100
--bind='ctrl-r:reload(git gtr list --porcelain)')" || return 0
102101
[ -z "$_gtr_selection" ] && return 0
103-
dir="$(printf '%s' "$_gtr_selection" | cut -f1)"
102+
_gtr_key="$(head -1 <<< "$_gtr_selection")"
103+
_gtr_line="$(sed -n '2p' <<< "$_gtr_selection")"
104+
[ -z "$_gtr_line" ] && return 0
105+
# ctrl-a/ctrl-e: run after fzf exits (needs full terminal for TUI apps)
106+
if [ "$_gtr_key" = "ctrl-a" ]; then
107+
command git gtr ai "$(printf '%s' "$_gtr_line" | cut -f2)"
108+
return $?
109+
elif [ "$_gtr_key" = "ctrl-e" ]; then
110+
command git gtr editor "$(printf '%s' "$_gtr_line" | cut -f2)"
111+
return $?
112+
fi
113+
dir="$(printf '%s' "$_gtr_line" | cut -f1)"
104114
elif [ "$#" -eq 0 ]; then
105115
echo "Usage: __FUNC__ cd <branch>" >&2
106116
echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2
@@ -183,7 +193,7 @@ __FUNC__() {
183193
shift
184194
local dir
185195
if [ "$#" -eq 0 ] && command -v fzf >/dev/null 2>&1; then
186-
local _gtr_selection
196+
local _gtr_selection _gtr_key _gtr_line
187197
_gtr_selection="$(command git gtr list --porcelain | fzf \
188198
--delimiter=$'\t' \
189199
--with-nth=2 \
@@ -194,13 +204,23 @@ __FUNC__() {
194204
--header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \
195205
--preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \
196206
--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})' \
207+
--expect=ctrl-a,ctrl-e \
208+
--bind='ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)' \
209+
--bind='ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)' \
201210
--bind='ctrl-r:reload(git gtr list --porcelain)')" || return 0
202211
[ -z "$_gtr_selection" ] && return 0
203-
dir="$(printf '%s' "$_gtr_selection" | cut -f1)"
212+
_gtr_key="$(head -1 <<< "$_gtr_selection")"
213+
_gtr_line="$(sed -n '2p' <<< "$_gtr_selection")"
214+
[ -z "$_gtr_line" ] && return 0
215+
# ctrl-a/ctrl-e: run after fzf exits (needs full terminal for TUI apps)
216+
if [ "$_gtr_key" = "ctrl-a" ]; then
217+
command git gtr ai "$(printf '%s' "$_gtr_line" | cut -f2)"
218+
return $?
219+
elif [ "$_gtr_key" = "ctrl-e" ]; then
220+
command git gtr editor "$(printf '%s' "$_gtr_line" | cut -f2)"
221+
return $?
222+
fi
223+
dir="$(printf '%s' "$_gtr_line" | cut -f1)"
204224
elif [ "$#" -eq 0 ]; then
205225
echo "Usage: __FUNC__ cd <branch>" >&2
206226
echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2
@@ -297,14 +317,25 @@ function __FUNC__
297317
--header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \
298318
--preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \
299319
--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})' \
320+
--expect=ctrl-a,ctrl-e \
321+
--bind='ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)' \
322+
--bind='ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)' \
304323
--bind='ctrl-r:reload(git gtr list --porcelain)')
305324
or return 0
306325
test -z "$_gtr_selection"; and return 0
307-
set dir (string split \t -- "$_gtr_selection")[1]
326+
# --expect gives two lines: key (index 1) and selection (index 2)
327+
set -l _gtr_key "$_gtr_selection[1]"
328+
set -l _gtr_line "$_gtr_selection[2]"
329+
test -z "$_gtr_line"; and return 0
330+
# ctrl-a/ctrl-e: run after fzf exits (needs full terminal for TUI apps)
331+
if test "$_gtr_key" = "ctrl-a"
332+
command git gtr ai (string split \t -- "$_gtr_line")[2]
333+
return $status
334+
else if test "$_gtr_key" = "ctrl-e"
335+
command git gtr editor (string split \t -- "$_gtr_line")[2]
336+
return $status
337+
end
338+
set dir (string split \t -- "$_gtr_line")[1]
308339
else if test (count $argv) -eq 1
309340
echo "Usage: __FUNC__ cd <branch>" >&2
310341
echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2

tests/init.bats

Lines changed: 207 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,39 +160,240 @@ 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 or cut to extract path
245+
[[ "$output" == *'cd '* ]]
246+
}
247+
248+
# ── fzf: ctrl-e (editor) — via --expect ──────────────────────────────────────
249+
250+
@test "bash fzf ctrl-e handled via --expect for full terminal access" {
251+
run cmd_init bash
252+
[ "$status" -eq 0 ]
253+
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
254+
[[ "$output" == *'git gtr editor'* ]]
255+
}
256+
257+
@test "zsh fzf ctrl-e handled via --expect for full terminal access" {
258+
run cmd_init zsh
259+
[ "$status" -eq 0 ]
260+
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
261+
[[ "$output" == *'git gtr editor'* ]]
262+
}
263+
264+
@test "fish fzf ctrl-e handled via --expect for full terminal access" {
265+
run cmd_init fish
266+
[ "$status" -eq 0 ]
267+
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
268+
[[ "$output" == *'git gtr editor'* ]]
269+
}
270+
271+
# ── fzf: ctrl-a (ai) — via --expect ─────────────────────────────────────────
272+
273+
@test "bash fzf ctrl-a runs git gtr ai after fzf exits" {
274+
run cmd_init bash
275+
[ "$status" -eq 0 ]
276+
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
277+
[[ "$output" == *'git gtr ai'* ]]
278+
}
279+
280+
@test "zsh fzf ctrl-a runs git gtr ai after fzf exits" {
281+
run cmd_init zsh
282+
[ "$status" -eq 0 ]
283+
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
284+
[[ "$output" == *'git gtr ai'* ]]
285+
}
286+
287+
@test "fish fzf ctrl-a runs git gtr ai after fzf exits" {
288+
run cmd_init fish
289+
[ "$status" -eq 0 ]
290+
[[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]]
291+
[[ "$output" == *'git gtr ai'* ]]
292+
}
293+
294+
# ── fzf: ctrl-d (delete + reload) ───────────────────────────────────────────
295+
296+
@test "bash fzf ctrl-d runs git gtr rm and reloads list" {
297+
run cmd_init bash
298+
[ "$status" -eq 0 ]
299+
[[ "$output" == *"ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)"* ]]
300+
}
301+
302+
@test "zsh fzf ctrl-d runs git gtr rm and reloads list" {
303+
run cmd_init zsh
304+
[ "$status" -eq 0 ]
305+
[[ "$output" == *"ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)"* ]]
306+
}
307+
308+
@test "fish fzf ctrl-d runs git gtr rm and reloads list" {
309+
run cmd_init fish
310+
[ "$status" -eq 0 ]
311+
[[ "$output" == *"ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)"* ]]
312+
}
313+
314+
# ── fzf: ctrl-y (copy) ──────────────────────────────────────────────────────
315+
316+
@test "bash fzf ctrl-y runs git gtr copy on selected branch" {
317+
run cmd_init bash
318+
[ "$status" -eq 0 ]
319+
[[ "$output" == *"ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)"* ]]
320+
}
321+
322+
@test "zsh fzf ctrl-y runs git gtr copy on selected branch" {
323+
run cmd_init zsh
324+
[ "$status" -eq 0 ]
325+
[[ "$output" == *"ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)"* ]]
326+
}
327+
328+
@test "fish fzf ctrl-y runs git gtr copy on selected branch" {
329+
run cmd_init fish
330+
[ "$status" -eq 0 ]
331+
[[ "$output" == *"ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)"* ]]
332+
}
333+
334+
# ── fzf: ctrl-r (refresh) ───────────────────────────────────────────────────
335+
336+
@test "bash fzf ctrl-r reloads worktree list" {
337+
run cmd_init bash
338+
[ "$status" -eq 0 ]
339+
[[ "$output" == *"ctrl-r:reload(git gtr list --porcelain)"* ]]
340+
}
341+
342+
@test "zsh fzf ctrl-r reloads worktree list" {
343+
run cmd_init zsh
344+
[ "$status" -eq 0 ]
345+
[[ "$output" == *"ctrl-r:reload(git gtr list --porcelain)"* ]]
346+
}
347+
348+
@test "fish fzf ctrl-r reloads worktree list" {
349+
run cmd_init fish
350+
[ "$status" -eq 0 ]
351+
[[ "$output" == *"ctrl-r:reload(git gtr list --porcelain)"* ]]
352+
}
353+
354+
# ── fzf: preview window ─────────────────────────────────────────────────────
355+
356+
@test "bash fzf preview shows git log and status" {
357+
run cmd_init bash
358+
[ "$status" -eq 0 ]
359+
[[ "$output" == *"--preview="* ]]
360+
[[ "$output" == *"git -C {1} log --oneline --graph --color=always"* ]]
361+
[[ "$output" == *"git -C {1} status --short"* ]]
362+
[[ "$output" == *"--preview-window=right:50%"* ]]
363+
}
364+
365+
@test "zsh fzf preview shows git log and status" {
366+
run cmd_init zsh
367+
[ "$status" -eq 0 ]
368+
[[ "$output" == *"--preview="* ]]
369+
[[ "$output" == *"git -C {1} log --oneline --graph --color=always"* ]]
370+
[[ "$output" == *"git -C {1} status --short"* ]]
371+
[[ "$output" == *"--preview-window=right:50%"* ]]
372+
}
373+
374+
@test "fish fzf preview shows git log and status" {
375+
run cmd_init fish
376+
[ "$status" -eq 0 ]
377+
[[ "$output" == *"--preview="* ]]
378+
[[ "$output" == *"git -C {1} log --oneline --graph --color=always"* ]]
379+
[[ "$output" == *"git -C {1} status --short"* ]]
380+
[[ "$output" == *"--preview-window=right:50%"* ]]
381+
}
382+
383+
# ── fzf: fallback messages ──────────────────────────────────────────────────
384+
190385
@test "bash output shows fzf install hint when no args and no fzf" {
191386
run cmd_init bash
192387
[ "$status" -eq 0 ]
193388
[[ "$output" == *'Install fzf for an interactive picker'* ]]
194389
}
195390
391+
@test "zsh output shows fzf install hint when no args and no fzf" {
392+
run cmd_init zsh
393+
[ "$status" -eq 0 ]
394+
[[ "$output" == *'Install fzf for an interactive picker'* ]]
395+
}
396+
196397
@test "fish output shows fzf install hint when no args and no fzf" {
197398
run cmd_init fish
198399
[ "$status" -eq 0 ]

0 commit comments

Comments
 (0)