Skip to content

Commit 5f86077

Browse files
nsheapsclaude
andcommitted
feat: replace gum with fzf, add delete and PR preview
- Replace all gum usage with fzf for TUI interactions - Add ANSI color helper functions for styling - Add 'd' key to delete worktrees with confirmation modal - Add PR preview pane showing `gh pr view` output - Add file-based caching for PR previews - Use fzf --style=full for enhanced styling - Change required dependency from gum to fzf Co-Authored-By: Claude Code (User Settings, in: ${CLAUDE_PROJECT_DIR}) <noreply@anthropic.com>
1 parent 721abc6 commit 5f86077

1 file changed

Lines changed: 135 additions & 19 deletions

File tree

bin/git-wt

Lines changed: 135 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ get_version() {
4949
SCAN_DIR="${HOME}/src"
5050
TARGET_BRANCH=""
5151
UPDATE_CHECK_FILE="/tmp/git-wt-update-check-$$"
52+
PR_CACHE_DIR="/tmp/git-wt-pr-cache-$$"
53+
export PR_CACHE_DIR # Export for fzf preview subshell
54+
55+
# ANSI color helpers (defined early for use in exit trap)
56+
style_bold() { printf '\033[1m%s\033[0m\n' "$1"; }
57+
style_warn() { printf '\033[38;5;214m%s\033[0m\n' "$1"; }
58+
style_error() { printf '\033[38;5;196m%s\033[0m\n' "$1"; }
59+
style_success() { printf '\033[38;5;82m%s\033[0m\n' "$1"; }
60+
style_bold_error() { printf '\033[1;38;5;196m%s\033[0m\n' "$1"; }
5261

5362
# Background update check - runs asynchronously
5463
check_for_updates() {
@@ -84,7 +93,10 @@ check_for_updates "$CURRENT_VERSION" &
8493
UPDATE_CHECK_PID=$!
8594

8695
# Cleanup and show update notification on exit
87-
show_update_notification() {
96+
cleanup_and_notify() {
97+
# Cleanup PR cache directory
98+
rm -rf "$PR_CACHE_DIR" 2>/dev/null || true
99+
88100
# Wait briefly for background check (non-blocking if already done)
89101
wait "$UPDATE_CHECK_PID" 2>/dev/null || true
90102

@@ -97,12 +109,12 @@ show_update_notification() {
97109
if [[ "$update_info" == UPDATE_AVAILABLE=* ]]; then
98110
local new_version="${update_info#UPDATE_AVAILABLE=}"
99111
echo ""
100-
gum style --foreground 214 "A new release of git-wt is available: ${CURRENT_VERSION#v}$new_version"
112+
style_warn "A new release of git-wt is available: ${CURRENT_VERSION#v}$new_version"
101113
echo "To upgrade, run: brew upgrade git-wt"
102114
fi
103115
fi
104116
}
105-
trap show_update_notification EXIT
117+
trap cleanup_and_notify EXIT
106118

107119
show_help() {
108120
sed -n '2,17p' "$0" | sed 's/^# //' | sed 's/^#//'
@@ -124,15 +136,31 @@ check_tool() {
124136
if ! command -v "$1" &>/dev/null; then
125137
echo "Error: $1 is required but not installed" >&2
126138
case "$1" in
127-
gum) echo "Install with: brew install gum" >&2 ;;
139+
fzf) echo "Install with: brew install fzf" >&2 ;;
128140
esac
129141
exit 1
130142
fi
131143
}
132144

133-
check_tool gum
145+
check_tool fzf
134146
check_tool git
135147

148+
# Confirm prompt (returns 0 for yes, 1 for no)
149+
confirm() {
150+
local prompt="${1:-Confirm?}"
151+
local result
152+
result=$(printf 'Yes\nNo' | fzf --header="$prompt" --height=5 --no-info)
153+
[[ "$result" == "Yes" ]]
154+
}
155+
156+
# Text input prompt
157+
input_prompt() {
158+
local placeholder="${1:-Enter value}"
159+
local result
160+
result=$(fzf --print-query --header="$placeholder" --height=3 --no-info < /dev/null 2>/dev/null | head -1)
161+
echo "$result"
162+
}
163+
136164
# Function to find repos in a directory
137165
find_repos_in_dir() {
138166
local dir="$1"
@@ -148,11 +176,10 @@ select_repo() {
148176
local repos=()
149177

150178
echo ""
151-
gum style --bold "Not in a git repository"
179+
style_bold "Not in a git repository"
152180
echo ""
153181

154-
echo ""
155-
gum spin --title "Scanning $SCAN_DIR for git repos..." -- sleep 0.5
182+
echo "Scanning $SCAN_DIR for git repos..."
156183

157184
# Find repos
158185
while IFS= read -r repo_path; do
@@ -167,7 +194,7 @@ select_repo() {
167194
fi
168195

169196
# Let user select
170-
SELECTED_REPO=$(printf '%s\n' "${repos[@]}" | gum filter --placeholder "Select repository...")
197+
SELECTED_REPO=$(printf '%s\n' "${repos[@]}" | fzf --prompt="Select repository> ")
171198
if [[ -z "$SELECTED_REPO" ]]; then
172199
echo "Cancelled"
173200
exit 0
@@ -239,11 +266,11 @@ switch_to_branch() {
239266

240267
# Ask what to base it on
241268
local base_options=("$default_branch (default)" "other...")
242-
local base_selected=$(printf '%s\n' "${base_options[@]}" | gum choose --header "Create new branch based on:")
269+
local base_selected=$(printf '%s\n' "${base_options[@]}" | fzf --header="Create new branch based on:" --height=5 --no-info)
243270

244271
local base_branch="$default_branch"
245272
if [[ "$base_selected" == "other..."* ]]; then
246-
base_branch=$(git -C "$git_root" branch -a --format='%(refname:short)' | gum filter --placeholder "Select base branch...")
273+
base_branch=$(git -C "$git_root" branch -a --format='%(refname:short)' | fzf --prompt="Select base branch> ")
247274
fi
248275

249276
git -C "$git_root" worktree add -b "$branch" "$worktree_path" "$base_branch"
@@ -289,12 +316,17 @@ fi
289316

290317
# Fetch from remote first
291318
echo ""
292-
gum spin --title "Fetching from remote..." -- git -C "$GIT_ROOT" fetch origin --prune 2>/dev/null || true
319+
echo "Fetching from remote..."
320+
git -C "$GIT_ROOT" fetch origin --prune 2>/dev/null || true
321+
322+
# Setup for PR preview caching
323+
mkdir -p "$PR_CACHE_DIR"
324+
export GIT_ROOT # Export for fzf preview subshell
293325

294326
# If branch specified, directly create/switch to that worktree
295327
if [[ -n "$TARGET_BRANCH" ]]; then
296328
echo ""
297-
gum style --bold "Switching to worktree for: $TARGET_BRANCH"
329+
style_bold "Switching to worktree for: $TARGET_BRANCH"
298330
echo ""
299331

300332
switch_to_branch "$TARGET_BRANCH" "$GIT_ROOT"
@@ -303,7 +335,7 @@ fi
303335

304336
# Interactive mode - offer worktree selection
305337
echo ""
306-
gum style --bold "Git Worktree Selector"
338+
style_bold "Git Worktree Selector"
307339
echo ""
308340

309341
# Get existing worktrees (including root checkout) and track their branches
@@ -358,19 +390,103 @@ done
358390
# Add option to switch repo (step "up")
359391
MENU_OPTIONS+=("🔄 (switch repository)")
360392

361-
# Show selection menu (--no-sort keeps existing worktrees at top while filtering)
362-
SELECTED=$(printf '%s\n' "${MENU_OPTIONS[@]}" | gum filter --no-sort --placeholder "Select or create worktree...")
393+
# Show selection menu with fzf
394+
# Preview shows PR info from cache, falls back to single fetch if not cached yet
395+
KEY_PRESSED=""
396+
FZF_RESULT=$(printf '%s\n' "${MENU_OPTIONS[@]}" | fzf \
397+
--style=full \
398+
--expect=d \
399+
--no-sort \
400+
--header="enter=select, d=delete worktree" \
401+
--prompt="Select or create worktree> " \
402+
--preview='
403+
item={}
404+
# Extract branch name from different formats
405+
if [[ "$item" == "📂 [worktree]"* ]] || [[ "$item" == "🏠 [root]"* ]]; then
406+
branch=$(echo "$item" | sed "s/^[^ ]* \[[^]]*\] //" | sed "s/ →.*//")
407+
elif [[ "$item" == "📁"* ]] || [[ "$item" == "🔄"* ]]; then
408+
echo "No preview available"
409+
exit 0
410+
else
411+
branch="$item"
412+
fi
413+
414+
# Read from cache if available
415+
safe_branch=$(echo "$branch" | tr "/" "_")
416+
cache_file="$PR_CACHE_DIR/$safe_branch"
417+
418+
if [[ -f "$cache_file" ]]; then
419+
cat "$cache_file"
420+
elif command -v gh &>/dev/null; then
421+
# Cache miss - fetch just this branch from correct repo context
422+
cd "$GIT_ROOT" 2>/dev/null || true
423+
result=$(gh pr view "$branch" 2>&1)
424+
if [[ $? -eq 0 ]]; then
425+
echo "$result" | tee "$cache_file"
426+
else
427+
echo "No PR found for branch: $branch" | tee "$cache_file"
428+
fi
429+
else
430+
echo "No PR found for branch: $branch"
431+
fi
432+
' \
433+
--preview-window=right:50%:wrap)
434+
KEY_PRESSED=$(echo "$FZF_RESULT" | head -1)
435+
SELECTED=$(echo "$FZF_RESULT" | tail -1)
363436

364437
if [[ -z "$SELECTED" ]]; then
365438
echo "Cancelled"
366439
exit 0
367440
fi
368441

442+
# Handle 'd' key press for delete
443+
if [[ "$KEY_PRESSED" == "d" ]]; then
444+
# Only allow deleting worktrees (not root, not create option, not branches)
445+
if [[ "$SELECTED" != "📂 [worktree]"* ]]; then
446+
echo ""
447+
style_warn "Cannot delete: only worktrees can be deleted with 'd'"
448+
echo "Selected item: $SELECTED"
449+
exit 1
450+
fi
451+
452+
# Extract worktree path
453+
WT_PATH=$(echo "$SELECTED" | sed 's/.*→ //')
454+
WT_BRANCH=$(echo "$SELECTED" | sed 's/📂 \[worktree\] //' | sed 's/ →.*//')
455+
456+
echo ""
457+
style_bold_error "Delete Worktree"
458+
echo ""
459+
echo "Branch: $WT_BRANCH"
460+
echo "Path: $WT_PATH"
461+
echo ""
462+
463+
if confirm "Are you sure you want to delete this worktree?"; then
464+
# Remove the worktree
465+
if git -C "$GIT_ROOT" worktree remove "$WT_PATH" 2>/dev/null; then
466+
echo ""
467+
style_success "✓ Worktree deleted successfully"
468+
else
469+
# Try force remove if normal remove fails
470+
echo ""
471+
style_warn "Worktree has uncommitted changes or other issues."
472+
if confirm "Force delete? (will discard any uncommitted changes)"; then
473+
git -C "$GIT_ROOT" worktree remove --force "$WT_PATH"
474+
style_success "✓ Worktree force deleted"
475+
else
476+
echo "Cancelled"
477+
fi
478+
fi
479+
else
480+
echo "Cancelled"
481+
fi
482+
exit 0
483+
fi
484+
369485
# Handle selection
370486
case "$SELECTED" in
371487
"📁 (create new worktree)")
372488
# Get branch name
373-
BRANCH_NAME=$(gum input --placeholder "Enter new branch name...")
489+
BRANCH_NAME=$(input_prompt "Enter new branch name")
374490
if [[ -z "$BRANCH_NAME" ]]; then
375491
echo "Cancelled"
376492
exit 0
@@ -381,12 +497,12 @@ case "$SELECTED" in
381497
DEFAULT_BRANCH=$(git -C "$GIT_ROOT" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
382498

383499
BASE_OPTIONS=("$DEFAULT_BRANCH (default)" "$CURRENT_BRANCH (current)" "other...")
384-
BASE_SELECTED=$(printf '%s\n' "${BASE_OPTIONS[@]}" | gum choose --header "Base branch:")
500+
BASE_SELECTED=$(printf '%s\n' "${BASE_OPTIONS[@]}" | fzf --header="Base branch:" --height=6 --no-info)
385501

386502
case "$BASE_SELECTED" in
387503
*"(default)"*) BASE_BRANCH="$DEFAULT_BRANCH" ;;
388504
*"(current)"*) BASE_BRANCH="$CURRENT_BRANCH" ;;
389-
*) BASE_BRANCH=$(git -C "$GIT_ROOT" branch -a --format='%(refname:short)' | gum filter --placeholder "Select base branch...") ;;
505+
*) BASE_BRANCH=$(git -C "$GIT_ROOT" branch -a --format='%(refname:short)' | fzf --prompt="Select base branch> ") ;;
390506
esac
391507

392508
# Create worktree path

0 commit comments

Comments
 (0)