Skip to content

Instantly share code, notes, and snippets.

@lukKowalski
Last active March 11, 2026 11:12
Show Gist options
  • Select an option

  • Save lukKowalski/1464f0e7b6eccdbb5a0f56fd1be0fe5b to your computer and use it in GitHub Desktop.

Select an option

Save lukKowalski/1464f0e7b6eccdbb5a0f56fd1be0fe5b to your computer and use it in GitHub Desktop.
Switch through git worktrees of a current repo
#!/usr/bin/env bash
# wt-pick: interactive git worktree picker, outputs selected path
#
# How to install and use:
# 1. Copy this to your ~/.local under wt-pick file name
# 2. Add to ~/.zshrc to cd on pick:
# wt() { local p; p=$(wt-pick "$@") && cd "$p"; }
# use with wt-pick - to output picked dir path or wt to also cd into in
set -euo pipefail
# ── colors ────────────────────────────────────────────────────────────────────
BOLD=$'\033[1m'
DIM=$'\033[2m'
RESET=$'\033[0m'
GREEN=$'\033[32m'
CYAN=$'\033[36m'
YELLOW=$'\033[33m'
RED=$'\033[31m'
# ── check git repo ─────────────────────────────────────────────────────────────
if ! git rev-parse --git-dir > /dev/null 2>&1; then
printf '%b✗ Not in a git repository%b\n' "$RED$BOLD" "$RESET" >&2
exit 1
fi
# ── parse worktrees into "branch\tpath" lines ──────────────────────────────────
worktrees=()
while IFS= read -r line; do
worktrees+=("$line")
done < <(
git worktree list --porcelain | awk '
/^worktree / { path = substr($0, 10) }
/^branch / { branch = substr($0, 8); sub("refs/heads/", "", branch) }
/^HEAD / { if (!branch) branch = "(detached " substr($0, 6, 7) ")" }
/^$/ { if (path) printf "%s\t%s\n", branch, path; path=""; branch="" }
'
)
if [[ ${#worktrees[@]} -eq 0 ]]; then
printf '%b⚠ No worktrees found%b\n' "$YELLOW" "$RESET" >&2
exit 1
fi
# ── fzf picker ─────────────────────────────────────────────────────────────────
if command -v fzf &>/dev/null; then
selected=$(
printf '%s\n' "${worktrees[@]}" | awk -F'\t' '{
n = split($2, parts, "/")
printf "%-25s\t%-40s %s\n", parts[n], $1, $2
}' | fzf \
--prompt=" worktree › " \
--height=50% \
--reverse \
--ansi \
--no-sort \
--header=$' DIR BRANCH' \
--color='header:italic:dim,prompt:cyan:bold,pointer:green,hl:green:bold'
) || exit 0
awk '{ print $NF }' <<< "$selected"
exit 0
fi
# ── arrow-key menu (fallback when fzf is not available) ───────────────────────
branches=()
dirs=()
paths=()
for item in "${worktrees[@]}"; do
IFS=$'\t' read -r br pt <<< "$item"
branches+=("$br")
paths+=("$pt")
dirs+=("$(basename "$pt")")
done
n=${#branches[@]}
idx=0
# Save terminal state and switch to raw mode
old_stty=""
old_stty=$(stty -g 2>/dev/null) || true
cleanup() {
[[ -n "$old_stty" ]] && stty "$old_stty" 2>/dev/null || true
printf '\033[?25h' >&2 # restore cursor
}
trap cleanup EXIT INT TERM
stty -echo -icanon min 1 time 0
printf '\033[?25l' >&2 # hide cursor
draw() {
printf '\033[2J\033[H' >&2 # clear screen, move to top-left
printf '\n%s Git Worktrees%s %s↑↓ / j k navigate · enter select · q quit%s\n\n' \
"$BOLD$CYAN" "$RESET" "$DIM" "$RESET" >&2
printf ' %s%-25s %s%s\n' "$DIM" "DIR" "BRANCH" "$RESET" >&2
local i
for (( i=0; i<n; i++ )); do
if (( i == idx )); then
printf ' %s▶ %-25s %s%s%s\n' \
"$GREEN$BOLD" "${dirs[$i]}" "$RESET$CYAN" "${branches[$i]}" "$RESET" >&2
else
printf ' %s%-25s%s %s%s%s\n' \
"$BOLD" "${dirs[$i]}" "$RESET" "$DIM" "${branches[$i]}" "$RESET" >&2
fi
done
}
draw
while true; do
read -rsn1 key
if [[ $key == $'\x1b' ]]; then
read -rsn2 -t 1 key
key=$'\x1b'"$key"
fi
case "$key" in
$'\x1b[A'|k) idx=$(( idx > 0 ? idx - 1 : idx )) ; draw ;;
$'\x1b[B'|j) idx=$(( idx < n-1 ? idx + 1 : idx )) ; draw ;;
''|$'\n') break ;;
q|Q) printf '\n' >&2 ; exit 0 ;;
esac
done
printf '\n' >&2
echo "${paths[$idx]}"
@lukKowalski
Copy link
Author

Demo:

ezgif-3557f764209ca630

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment