diff --git a/lib/commands/init.sh b/lib/commands/init.sh index daf30c0..2c6fedc 100644 --- a/lib/commands/init.sh +++ b/lib/commands/init.sh @@ -89,7 +89,7 @@ __FUNC__() { echo "No worktrees to pick from. Create one with: git gtr new " >&2 return 0 fi - local _gtr_selection + local _gtr_selection _gtr_key _gtr_line _gtr_selection="$(printf '%s\n' "$_gtr_porcelain" | fzf \ --delimiter=$'\t' \ --with-nth=2 \ @@ -100,13 +100,23 @@ __FUNC__() { --header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \ --preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \ --preview-window=right:50% \ - --bind='ctrl-e:execute(git gtr editor {2})' \ - --bind='ctrl-a:execute(git gtr ai {2})' \ - --bind='ctrl-d:execute(git gtr rm {2})+reload(git gtr list --porcelain)' \ - --bind='ctrl-y:execute(git gtr copy {2})' \ + --expect=ctrl-a,ctrl-e \ + --bind='ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)' \ + --bind='ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)' \ --bind='ctrl-r:reload(git gtr list --porcelain)')" || return 0 [ -z "$_gtr_selection" ] && return 0 - dir="$(printf '%s' "$_gtr_selection" | cut -f1)" + _gtr_key="$(head -1 <<< "$_gtr_selection")" + _gtr_line="$(sed -n '2p' <<< "$_gtr_selection")" + [ -z "$_gtr_line" ] && return 0 + # ctrl-a/ctrl-e: run after fzf exits (needs full terminal for TUI apps) + if [ "$_gtr_key" = "ctrl-a" ]; then + command git gtr ai "$(printf '%s' "$_gtr_line" | cut -f2)" + return $? + elif [ "$_gtr_key" = "ctrl-e" ]; then + command git gtr editor "$(printf '%s' "$_gtr_line" | cut -f2)" + return $? + fi + dir="$(printf '%s' "$_gtr_line" | cut -f1)" elif [ "$#" -eq 0 ]; then echo "Usage: __FUNC__ cd " >&2 echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2 @@ -195,7 +205,7 @@ __FUNC__() { echo "No worktrees to pick from. Create one with: git gtr new " >&2 return 0 fi - local _gtr_selection + local _gtr_selection _gtr_key _gtr_line _gtr_selection="$(printf '%s\n' "$_gtr_porcelain" | fzf \ --delimiter=$'\t' \ --with-nth=2 \ @@ -206,13 +216,23 @@ __FUNC__() { --header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \ --preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \ --preview-window=right:50% \ - --bind='ctrl-e:execute(git gtr editor {2})' \ - --bind='ctrl-a:execute(git gtr ai {2})' \ - --bind='ctrl-d:execute(git gtr rm {2})+reload(git gtr list --porcelain)' \ - --bind='ctrl-y:execute(git gtr copy {2})' \ + --expect=ctrl-a,ctrl-e \ + --bind='ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)' \ + --bind='ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)' \ --bind='ctrl-r:reload(git gtr list --porcelain)')" || return 0 [ -z "$_gtr_selection" ] && return 0 - dir="$(printf '%s' "$_gtr_selection" | cut -f1)" + _gtr_key="$(head -1 <<< "$_gtr_selection")" + _gtr_line="$(sed -n '2p' <<< "$_gtr_selection")" + [ -z "$_gtr_line" ] && return 0 + # ctrl-a/ctrl-e: run after fzf exits (needs full terminal for TUI apps) + if [ "$_gtr_key" = "ctrl-a" ]; then + command git gtr ai "$(printf '%s' "$_gtr_line" | cut -f2)" + return $? + elif [ "$_gtr_key" = "ctrl-e" ]; then + command git gtr editor "$(printf '%s' "$_gtr_line" | cut -f2)" + return $? + fi + dir="$(printf '%s' "$_gtr_line" | cut -f1)" elif [ "$#" -eq 0 ]; then echo "Usage: __FUNC__ cd " >&2 echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2 @@ -314,14 +334,32 @@ function __FUNC__ --header='enter:cd │ ctrl-e:editor │ ctrl-a:ai │ ctrl-d:delete │ ctrl-y:copy │ ctrl-r:refresh' \ --preview='git -C {1} log --oneline --graph --color=always -15 2>/dev/null; echo "---"; git -C {1} status --short 2>/dev/null' \ --preview-window=right:50% \ - --bind='ctrl-e:execute(git gtr editor {2})' \ - --bind='ctrl-a:execute(git gtr ai {2})' \ - --bind='ctrl-d:execute(git gtr rm {2})+reload(git gtr list --porcelain)' \ - --bind='ctrl-y:execute(git gtr copy {2})' \ + --expect=ctrl-a,ctrl-e \ + --bind='ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)' \ + --bind='ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)' \ --bind='ctrl-r:reload(git gtr list --porcelain)') or return 0 test -z "$_gtr_selection"; and return 0 - set dir (string split \t -- "$_gtr_selection")[1] + # --expect gives two lines: key (index 1) and selection (index 2) + # Fish collapses empty lines in command substitution, so when Enter + # is pressed the empty key line disappears and count drops to 1. + if test (count $_gtr_selection) -eq 1 + set -l _gtr_key "" + set -l _gtr_line "$_gtr_selection[1]" + else + set -l _gtr_key "$_gtr_selection[1]" + set -l _gtr_line "$_gtr_selection[2]" + end + test -z "$_gtr_line"; and return 0 + # ctrl-a/ctrl-e: run after fzf exits (needs full terminal for TUI apps) + if test "$_gtr_key" = "ctrl-a" + command git gtr ai (string split \t -- "$_gtr_line")[2] + return $status + else if test "$_gtr_key" = "ctrl-e" + command git gtr editor (string split \t -- "$_gtr_line")[2] + return $status + end + set dir (string split \t -- "$_gtr_line")[1] else if test (count $argv) -eq 1 echo "Usage: __FUNC__ cd " >&2 echo "Tip: Install fzf for an interactive picker (https://github.com/junegunn/fzf)" >&2 diff --git a/tests/init.bats b/tests/init.bats index 2c5f844..8741fac 100644 --- a/tests/init.bats +++ b/tests/init.bats @@ -160,39 +160,242 @@ setup() { # ── fzf interactive picker ─────────────────────────────────────────────────── -@test "bash output includes fzf picker for cd with no args" { +# ── fzf: general setup ────────────────────────────────────────────────────── + +@test "bash output includes fzf detection for cd with no args" { run cmd_init bash [ "$status" -eq 0 ] [[ "$output" == *"command -v fzf"* ]] [[ "$output" == *"--prompt='Worktree> '"* ]] [[ "$output" == *"--with-nth=2"* ]] - [[ "$output" == *"ctrl-e:execute"* ]] } -@test "zsh output includes fzf picker for cd with no args" { +@test "zsh output includes fzf detection for cd with no args" { run cmd_init zsh [ "$status" -eq 0 ] [[ "$output" == *"command -v fzf"* ]] [[ "$output" == *"--prompt='Worktree> '"* ]] [[ "$output" == *"--with-nth=2"* ]] - [[ "$output" == *"ctrl-e:execute"* ]] } -@test "fish output includes fzf picker for cd with no args" { +@test "fish output includes fzf detection for cd with no args" { run cmd_init fish [ "$status" -eq 0 ] [[ "$output" == *"type -q fzf"* ]] [[ "$output" == *"--prompt='Worktree> '"* ]] [[ "$output" == *"--with-nth=2"* ]] - [[ "$output" == *"ctrl-e:execute"* ]] } +# ── fzf: header shows all keybindings ─────────────────────────────────────── + +@test "bash fzf header lists all keybindings" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *"enter:cd"* ]] + [[ "$output" == *"ctrl-e:editor"* ]] + [[ "$output" == *"ctrl-a:ai"* ]] + [[ "$output" == *"ctrl-d:delete"* ]] + [[ "$output" == *"ctrl-y:copy"* ]] + [[ "$output" == *"ctrl-r:refresh"* ]] +} + +@test "zsh fzf header lists all keybindings" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *"enter:cd"* ]] + [[ "$output" == *"ctrl-e:editor"* ]] + [[ "$output" == *"ctrl-a:ai"* ]] + [[ "$output" == *"ctrl-d:delete"* ]] + [[ "$output" == *"ctrl-y:copy"* ]] + [[ "$output" == *"ctrl-r:refresh"* ]] +} + +@test "fish fzf header lists all keybindings" { + run cmd_init fish + [ "$status" -eq 0 ] + [[ "$output" == *"enter:cd"* ]] + [[ "$output" == *"ctrl-e:editor"* ]] + [[ "$output" == *"ctrl-a:ai"* ]] + [[ "$output" == *"ctrl-d:delete"* ]] + [[ "$output" == *"ctrl-y:copy"* ]] + [[ "$output" == *"ctrl-r:refresh"* ]] +} + +# ── fzf: enter (cd) ───────────────────────────────────────────────────────── + +@test "bash fzf enter extracts path from selection field 1 and cd" { + run cmd_init bash + [ "$status" -eq 0 ] + # Selection is parsed with cut -f1 to get path, then cd + [[ "$output" == *'cut -f1'* ]] + [[ "$output" == *'cd "$dir"'* ]] +} + +@test "zsh fzf enter extracts path from selection field 1 and cd" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *'cut -f1'* ]] + [[ "$output" == *'cd "$dir"'* ]] +} + +@test "fish fzf enter extracts path from selection and cd" { + run cmd_init fish + [ "$status" -eq 0 ] + # Fish uses string split to extract path, then cd + [[ "$output" == *'string split'* ]] + [[ "$output" == *'set dir'* ]] + [[ "$output" == *'cd $dir'* ]] +} + +# ── fzf: ctrl-e (editor) — via --expect ────────────────────────────────────── + +@test "bash fzf ctrl-e handled via --expect for full terminal access" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]] + [[ "$output" == *'git gtr editor'* ]] +} + +@test "zsh fzf ctrl-e handled via --expect for full terminal access" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]] + [[ "$output" == *'git gtr editor'* ]] +} + +@test "fish fzf ctrl-e handled via --expect for full terminal access" { + run cmd_init fish + [ "$status" -eq 0 ] + [[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]] + [[ "$output" == *'git gtr editor'* ]] +} + +# ── fzf: ctrl-a (ai) — via --expect ───────────────────────────────────────── + +@test "bash fzf ctrl-a runs git gtr ai after fzf exits" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]] + [[ "$output" == *'git gtr ai'* ]] +} + +@test "zsh fzf ctrl-a runs git gtr ai after fzf exits" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]] + [[ "$output" == *'git gtr ai'* ]] +} + +@test "fish fzf ctrl-a runs git gtr ai after fzf exits" { + run cmd_init fish + [ "$status" -eq 0 ] + [[ "$output" == *"--expect=ctrl-a,ctrl-e"* ]] + [[ "$output" == *'git gtr ai'* ]] +} + +# ── fzf: ctrl-d (delete + reload) ─────────────────────────────────────────── + +@test "bash fzf ctrl-d runs git gtr rm and reloads list" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *"ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)"* ]] +} + +@test "zsh fzf ctrl-d runs git gtr rm and reloads list" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *"ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)"* ]] +} + +@test "fish fzf ctrl-d runs git gtr rm and reloads list" { + run cmd_init fish + [ "$status" -eq 0 ] + [[ "$output" == *"ctrl-d:execute(git gtr rm {2} > /dev/tty 2>&1 < /dev/tty)+reload(git gtr list --porcelain)"* ]] +} + +# ── fzf: ctrl-y (copy) ────────────────────────────────────────────────────── + +@test "bash fzf ctrl-y runs git gtr copy on selected branch" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *"ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)"* ]] +} + +@test "zsh fzf ctrl-y runs git gtr copy on selected branch" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *"ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)"* ]] +} + +@test "fish fzf ctrl-y runs git gtr copy on selected branch" { + run cmd_init fish + [ "$status" -eq 0 ] + [[ "$output" == *"ctrl-y:execute(git gtr copy {2} > /dev/tty 2>&1 < /dev/tty)"* ]] +} + +# ── fzf: ctrl-r (refresh) ─────────────────────────────────────────────────── + +@test "bash fzf ctrl-r reloads worktree list" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *"ctrl-r:reload(git gtr list --porcelain)"* ]] +} + +@test "zsh fzf ctrl-r reloads worktree list" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *"ctrl-r:reload(git gtr list --porcelain)"* ]] +} + +@test "fish fzf ctrl-r reloads worktree list" { + run cmd_init fish + [ "$status" -eq 0 ] + [[ "$output" == *"ctrl-r:reload(git gtr list --porcelain)"* ]] +} + +# ── fzf: preview window ───────────────────────────────────────────────────── + +@test "bash fzf preview shows git log and status" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *"--preview="* ]] + [[ "$output" == *"git -C {1} log --oneline --graph --color=always"* ]] + [[ "$output" == *"git -C {1} status --short"* ]] + [[ "$output" == *"--preview-window=right:50%"* ]] +} + +@test "zsh fzf preview shows git log and status" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *"--preview="* ]] + [[ "$output" == *"git -C {1} log --oneline --graph --color=always"* ]] + [[ "$output" == *"git -C {1} status --short"* ]] + [[ "$output" == *"--preview-window=right:50%"* ]] +} + +@test "fish fzf preview shows git log and status" { + run cmd_init fish + [ "$status" -eq 0 ] + [[ "$output" == *"--preview="* ]] + [[ "$output" == *"git -C {1} log --oneline --graph --color=always"* ]] + [[ "$output" == *"git -C {1} status --short"* ]] + [[ "$output" == *"--preview-window=right:50%"* ]] +} + +# ── fzf: fallback messages ────────────────────────────────────────────────── + @test "bash output shows fzf install hint when no args and no fzf" { run cmd_init bash [ "$status" -eq 0 ] [[ "$output" == *'Install fzf for an interactive picker'* ]] } +@test "zsh output shows fzf install hint when no args and no fzf" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *'Install fzf for an interactive picker'* ]] +} + @test "fish output shows fzf install hint when no args and no fzf" { run cmd_init fish [ "$status" -eq 0 ]