roosta.tmux-fuzzback/scripts/fuzzback.sh
Daniel Berg 84cf690881
Fix issue with unsupported bind-key -N option
As pointed out by @k-kuroguro, the `-K` option isn't supported before
tmux 3.1, so I modified the supported.sh script to handle conditionally
setting this option on init

Refs:
1905afb3d8/CHANGES (L806C1-L811C28)
https://github.com/roosta/tmux-fuzzback/issues/24
2025-05-01 22:22:03 +02:00

350 lines
9.3 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# shellcheck disable=SC2001
# Strict mode
# http://redsymbol.net/articles/unofficial-bash-strict-mode/
set -uo pipefail
IFS=$'\n\t'
CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
SUPPORTED_VERSION="2.4"
# Pull in helpers
# shellcheck source=helpers.sh
. "$CURRENT_DIR/helpers.sh"
CAPTURE_FILENAME="$(get_capture_filename)"
finder_split_cmd() {
local fuzzback_finder="$3"
local hide_preview="$5"
local finder
local preview_window=""
if [ "$hide_preview" -eq 1 ];then
preview_window=":hidden"
fi
finder='fzf-tmux'
if [ "$fuzzback_finder" = "sk" ]; then
finder='sk-tmux'
fi
"$finder" \
--delimiter=":" \
--ansi \
--bind="$1" \
--delimiter=":" \
--layout="$finder_layout" \
--no-multi \
--print-query \
--with-nth="3.." \
--color="$4" \
--preview="$CURRENT_DIR/preview.sh $CAPTURE_FILENAME {}" \
--preview-window="$preview_window"
}
fzf_popup_cmd() {
local hide_preview="$5"
local preview_window=""
if [ "$hide_preview" -eq 1 ];then
preview_window=":hidden"
fi
fzf-tmux -p "$1" \
--ansi \
--bind="$2" \
--delimiter=":" \
--layout="$3" \
--no-multi \
--print-query \
--with-nth="3.." \
--color="$4" \
--preview="$CURRENT_DIR/preview.sh $CAPTURE_FILENAME {}" \
--preview-window="$preview_window"
}
rev_cmd() {
if cmd_exists tac; then
tac
else
tail -r
fi
}
cursor_up() {
local line_number
line_number="$1"
tmux send-keys -X -N "$line_number" cursor-up
}
cursor_down() {
local line_number
line_number="$1"
tmux send-keys -X -N "$line_number" cursor-down
}
# https://github.com/tmux-plugins/tmux-copycat/blob/d7f7e6c1de0bc0d6915f4beea5be6a8a42045c09/scripts/copycat_jump.sh#L68
escape_backslash() {
local string="$1"
echo "$string" | sed 's/\\/\\\\/g'
}
# Get columns position of search query
# https://github.com/tmux-plugins/tmux-copycat/blob/d7f7e6c1de0bc0d6915f4beea5be6a8a42045c09/scripts/copycat_jump.sh#L73
query_column() {
local query="$1"
local result_line="$2"
local column zero_index platform
# OS X awk cannot have `=` as the first char in the variable (bug in awk).
# If exists, changing the `=` character with `.` to avoid error.
platform="$(uname)"
if [ "$platform" == "Darwin" ]; then
result_line="$(echo "$result_line" | sed 's/^=/./')"
query="$(echo "$query" | sed 's/^=/./')"
fi
# awk treats \r, \n, \t etc as single characters and that messes up match
# highlighting. For that reason, we're escaping backslashes so above chars
# are treated literally.
result_line="$(escape_backslash "$result_line")"
query="$(escape_backslash "$query")"
column=$($AWK_CMD -v a="$result_line" -v b="$query" 'BEGIN{print index(a,b)}')
zero_index=$((column - 1))
echo "$zero_index"
}
# maximum line number that can be reached via tmux goto-line
# https://github.com/tmux-plugins/tmux-copycat/blob/e95528ebaeb6300d8620c8748a686b786056f374/scripts/copycat_jump.sh#L159
get_max_jump() {
local max_jump max_lines pane_height
local max_lines="$1"
local pane_height="$2"
max_jump=$((max_lines - pane_height))
# max jump can't be lower than zero
if [ "$max_jump" -lt "0" ]; then
max_jump="0"
fi
echo "$max_jump"
}
# Goto line in scrollback buffer
goto_line() {
local correction
local line_number="$1"
local direction="$2"
local max_jump="$3"
local max_lines="$4"
correction="0"
if [ "$direction" -eq "1" ];then
if [ "$line_number" -gt "$max_jump" ]; then
# We need to reach a line number that is not accessible via goto-line.
# So we need to correct position to reach the desired line number
correct_line_number="$max_jump"
correction=$((line_number - correct_line_number))
else
# we can reach the desired line number via goto-line. Correction not
# needed.
correct_line_number="$line_number"
fi
tmux send-keys -X goto-line "$correct_line_number"
# Correct if needed
if [ "$correction" -gt "0" ]; then
local scroll_top="$(( max_lines - line_number - 1 ))"
cursor_up "$max_lines"
cursor_down "$scroll_top"
fi
# Centering
# -------------
# If no corrections (meaning result is not at the top of scrollback)
# we can then 'center' the result within a pane.
if [ "$correction" -eq "0" ]; then
local half_pane_height="$((pane_height / 2))"
# creating as much padding as possible, up to half pane height
center "$line_number" "$half_pane_height"
fi
else
tmux send-keys -X goto-line "0"
cursor_down "$(( line_number + 1))"
fi
}
goto_column() {
local column="$1"
tmux send-keys -X start-of-line
if [ "$column" -gt "0" ]; then
tmux send-keys -X -N "$column" cursor-right
fi
}
# Center result on screen
# https://github.com/tmux-plugins/tmux-copycat/blob/e95528ebaeb6300d8620c8748a686b786056f374/scripts/copycat_jump.sh#L127
center() {
local number_of_lines="$1"
local maximum_padding="$2"
local padding
# Padding should not be greater than half pane height
# (it wouldn't be centered then).
if [ "$number_of_lines" -gt "$maximum_padding" ]; then
padding="$maximum_padding"
else
padding="$number_of_lines"
fi
# cannot create padding, exit function
if [ "$padding" -eq "0" ]; then
return
fi
tmux send-keys -X -N "$padding" cursor-down
tmux send-keys -X -N "$padding" cursor-up
}
get_line_number() {
local position line_number
position=$(echo "$1" | cut -d':' -f2 | xargs)
line_number=$((position - 1))
echo "$line_number"
}
# Direction to move is stored in results at pos 1
get_direction() {
local direction
direction=$(echo "$1" | cut -d':' -f1 | xargs)
echo "$direction"
}
# Get the cursor y position when starting fuzzback
get_pos() {
local cursor_y
cursor_y=$(tmux display -p '#{cursor_y}')
echo "$cursor_y"
}
# Store captured scrollback in temp file
create_capture_file() {
mkdir -p "$(get_tmp_dir)"
chmod 0700 "$(get_tmp_dir)"
tmux capture-pane -e -p -S - > "$CAPTURE_FILENAME"
}
# Create a file that is the head of the scrollback. Ends where the cursor y
# position was when fuzzback was started
create_head_file() {
local head_n="$1"
local head_filename
head_filename="$(get_head_filename)"
head -n "$head_n" < "$CAPTURE_FILENAME" \
| rev_cmd \
| nl -b 'a' -s ':' \
| sed 's/^/1:/' \
| sed '1s/$//' \
> "$head_filename"
}
# Create a file that is the content below cursor then starting fuzzback
create_tail_file() {
local tail_n="$1"
local tail_filename trimmed
tail_filename="$(get_tail_filename)"
tmp_filename="$(get_tmp_filename)"
tail -n "$tail_n" < "$CAPTURE_FILENAME" > "$tmp_filename"
trimmed=$(cat "$tmp_filename")
if [ -z "$trimmed" ]; then
echo "$trimmed" > "$tail_filename"
else
nl -b 'a' -s ':' < "$tmp_filename" \
| rev_cmd \
| sed 's/^/-1:/' \
| sed 's/$//' \
> "$tail_filename"
fi
rm -f "$tmp_filename"
}
fuzzback() {
local match line_number pane_height query max_lines max_jump
local correct_line_number trimmed_line column pos pos_rev
local capture_height head_n tail_n
local enable_popup popup_size finder_bind finder_layout
create_capture_file
# Options
enable_popup="$(tmux_get '@fuzzback-popup' 0)"
popup_size="$(tmux_get '@fuzzback-popup-size' "50%")"
finder_bind="$(tmux_get '@fuzzback-finder-bind' 'ctrl-y:accept')"
finder_layout="$(tmux_get '@fuzzback-finder-layout' 'default')"
fuzzback_finder="$(tmux_get '@fuzzback-finder' 'fzf')"
fzf_colors="$(tmux_get '@fuzzback-fzf-colors' 'dark')"
fzf_hide_preview="$(tmux_get '@fuzzback-hide-preview' 0)"
pos=$(get_pos)
pane_height="$(tmux display-message -p '#{pane_height}')"
pos_rev=$(( pane_height - pos ))
head_file=$(get_head_filename)
tail_file=$(get_tail_filename)
capture_height=$(wc -l < "$CAPTURE_FILENAME")
head_n=$(( capture_height - pos_rev + 1 ))
tail_n=$(( pos_rev - 1 ))
# We need to create two separate files to account for upward and downward
# movement when starting fuzzback, scrollback movement is relative to start
# cursor_y position
create_head_file "$head_n"
create_tail_file "$tail_n"
# Combine head and tail when searching with fzf
if [ "$enable_popup" -eq 1 ];then
match=$(cat "$tail_file" "$head_file" | fzf_popup_cmd "$popup_size" "$finder_bind" "$finder_layout" "$fzf_colors" "$fzf_hide_preview")
else
match=$(cat "$tail_file" "$head_file" | finder_split_cmd "$finder_bind" "$finder_layout" "$fuzzback_finder" "$fzf_colors" "$fzf_hide_preview")
fi
if [ "$(echo "$match" | wc -l)" -gt "1" ]; then
query="$(head -n 1 <<< "$match")"
rest="$(tail -n 1 <<< "$match")"
trimmed_line=$(echo "$rest" | sed -E 's/-?[0-9]+: +[0-9]+://')
line_number=$(get_line_number "$rest")
direction=$(get_direction "$rest")
column=$(query_column "$query" "$trimmed_line")
max_lines=$(wc -l < "$head_file")
max_jump=$(get_max_jump "$max_lines" "$pane_height")
# Quit copy-mode before starting a new fuzzback session. This solves issues
# with starting fuzzback when already in copy-mode
tmux copy-mode -q
tmux copy-mode
# Move to position
goto_line "$line_number" "$direction" "$max_jump" "$max_lines"
goto_column "$column"
fi
delete_old_files
}
version_ok() {
"$CURRENT_DIR/supported.sh" "$SUPPORTED_VERSION"
return $?
}
main() {
if version_ok; then
fuzzback
else
# Exit when version check fails and after message is displayed
exit 1
fi
}
main