From 76aec4726039ea54de4fb75ab164463bf2a909d5 Mon Sep 17 00:00:00 2001 From: "Kenneth Benzie (Benie)" Date: Wed, 10 Jun 2026 15:01:21 +0100 Subject: [PATCH] Make project picker have dynamic width --- project.sh | 109 +++++++++++++++++++++++++++++++++++++++-------------- tmux.conf | 4 +- 2 files changed, 83 insertions(+), 30 deletions(-) diff --git a/project.sh b/project.sh index 0c30bce..0e026cd 100755 --- a/project.sh +++ b/project.sh @@ -4,35 +4,88 @@ set -e projects_dir=$HOME/Projects -# All depth-2 directories without @ are included unconditionally -projects=() -for dir in "$projects_dir"/*/*/; do - [ -d "$dir" ] || continue - relative="${dir#$projects_dir/}" - relative="${relative%/}" - [[ "$relative" != *@* ]] && projects+=("$relative") -done +# Print the sorted, de-duplicated list of selectable projects. +list_projects() { + local projects=() dir relative line -# For @ directories, only include those with .git -if command -v fd &>/dev/null; then - while IFS= read -r line; do - projects+=("$line") - done < <(fd -H '^\.git$' "$projects_dir" | - grep '@' | - sed -E -e "s|^$projects_dir/||" -e 's|/\.git/?$||') -else - while IFS= read -r line; do - projects+=("$line") - done < <(find "$projects_dir" \( -name 'node_modules' -o -name '.git' \) \ - -prune -name '.git' -print | - grep '@' | - sed -E -e "s|^$projects_dir/||" -e 's|/\.git/?$||') + # All depth-2 directories without @ are included unconditionally + for dir in "$projects_dir"/*/*/; do + [ -d "$dir" ] || continue + relative="${dir#$projects_dir/}" + relative="${relative%/}" + [[ "$relative" != *@* ]] && projects+=("$relative") + done + + # For @ directories, only include those with .git + if command -v fd &>/dev/null; then + while IFS= read -r line; do + projects+=("$line") + done < <(fd -H '^\.git$' "$projects_dir" | + grep '@' | + sed -E -e "s|^$projects_dir/||" -e 's|/\.git/?$||') + else + while IFS= read -r line; do + projects+=("$line") + done < <(find "$projects_dir" \( -name 'node_modules' -o -name '.git' \) \ + -prune -name '.git' -print | + grep '@' | + sed -E -e "s|^$projects_dir/||" -e 's|/\.git/?$||') + fi + + printf '%s\n' "${projects[@]}" | sort -u +} + +# Second pass: runs inside the popup. Read the list the first pass already +# built (so the directory scan only happens once) and open the selection. +if [[ "${1:-}" == --pick ]]; then + listfile=$2 + trap 'rm -f "$listfile"' EXIT + + # Cancelling fzf (Esc/^C) exits non-zero; treat it as "no selection" and + # leave quietly, rather than letting the status propagate out of the popup + # and print '...project.sh returned 130' in the parent pane. + project=$( + fzf --layout=reverse --info=hidden --border=rounded --cycle < "$listfile" + ) || exit 0 + [ -n "$project" ] || exit 0 + + tmux new-window -n "$project" -c "$projects_dir/$project" + ~/.local/share/tmux/layouts/window-auto + exit fi -project=$( - printf '%s\n' "${projects[@]}" | sort -u | - fzf --layout=reverse --info=hidden --border=rounded --cycle -) +# First pass: scan once, size a popup to fit the longest project name, then +# re-launch ourselves inside it. fzf has no width of its own — it fills the +# popup — so the popup width is what we scale. +listfile=$(mktemp) +list_projects > "$listfile" -tmux new-window -n "$project" -c "$projects_dir/$project" -~/.local/share/tmux/layouts/window-auto +longest=0 +while IFS= read -r line; do + if (( ${#line} > longest )); then + longest=${#line} + fi +done < "$listfile" + +# We're launched from run-shell, which owns no client, so resolve the client +# that triggered us. It both anchors the popup (-c, without which the popup +# gets no tty and fzf can't draw) and gives the terminal width for the cap. +client=$(tmux display -p '#{client_name}') +cols=$(tmux display -p -c "$client" '#{client_width}') + +# Clamp between the original fixed width and 90% of the terminal. In between, +# pad for fzf's chrome: rounded border (2) + pointer gutter (2) + scrollbar (1) +# + a column of breathing room. +min=60 +max=$(( cols * 90 / 100 )) +width=$(( longest + 6 )) +(( width < min )) && width=$min +(( width > max )) && width=$max + +flags=(-c "$client" -w "$width" -h 10) +# Match the borderless popup the keybinding used on tmux >= 3.3. +if ~/.config/tmux/check-version.sh ">= 3.3"; then + flags+=(-B) +fi + +tmux display-popup "${flags[@]}" -E "'$0' --pick '$listfile'" diff --git a/tmux.conf b/tmux.conf index c7e7530..35274b8 100644 --- a/tmux.conf +++ b/tmux.conf @@ -68,7 +68,6 @@ if -b '~/.config/tmux/check-version.sh ">= 3.2" and "< 3.3"' { bind H display-popup -w 75% -h 75% -E htop bind I display-popup -d '#{pane_current_path}' -E ~/.config/tmux/interpreter.sh bind N display-popup -w 75% -h 90% -E ~/.config/tmux/notes.sh - bind P display-popup -w 60 -h 10 -E ~/.config/tmux/project.sh bind S display-popup -w 60 -h 10 -E ~/.config/tmux/session.sh bind T display-popup -d '#{pane_current_path}' -E $SHELL } @@ -79,11 +78,12 @@ if -b '~/.config/tmux/check-version.sh ">= 3.3"' { bind H display-popup -S fg=#54546D -b rounded -w 50% -h 75% -E htop bind I display-popup -S fg=#54546D -b rounded -d '#{pane_current_path}' -E ~/.config/tmux/interpreter.sh bind N display-popup -S fg=#54546D -b rounded -w 75% -h 90% -E ~/.config/tmux/notes.sh - bind P display-popup -B -w 60 -h 10 -E ~/.config/tmux/project.sh bind S display-popup -B -w 60 -h 10 -E ~/.config/tmux/session.sh bind T display-popup -S fg=#54546D -d '#{pane_current_path}' -E $SHELL } +bind P run-shell ~/.config/tmux/project.sh + # Restore old next/previous window bindings bind C-n next-window bind C-p previous-window