1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#!/usr/bin/env bash

# Replaces two prior tools: the word-grab-and-insert flow from
# https://github.com/laktak/extrakto, and the copy/open-URL flow from
# https://github.com/junegunn/tmux-fzf-url (via this repo's retired
# ,tmux-copy script, which credited it directly).

set -euo pipefail
if [[ "${TRACE-0}" == "1" ]]; then
    set -o xtrace
fi

# Prints a file window spanning 2 lines on either side of hlline, with that
# line bolded.
render_preview() {
    local file=$1 hlline=$2
    local start=$(( hlline > 2 ? hlline - 2 : 1 ))
    local end=$(( hlline + 2 ))
    awk -v start="$start" -v end="$end" -v hlline="$hlline" '
        NR >= start && NR <= end {
            if (NR == hlline) printf "\033[1m%s\033[0m\n", $0
            else print
        }
    ' "$file"
}

main() {
    # tmux doesn't expand #{pane_id} in a new-pane shell-command argument, so
    # the triggering pane is looked up here instead: {last} is the pane that
    # was active in this window right before the new floating pane took over.
    local pane_id
    pane_id=$(tmux display-message -p -t '{last}' '#{pane_id}')

    # -J asks tmux for its own hard-wrap tracking instead of guessing from
    # whitespace: rows the terminal actually wrapped come back pre-joined (a
    # mid-word URL or path split across rows becomes one line), while
    # separate application-printed lines -- even ones butted up against each
    # other with no blank line between, like a two-line prompt -- stay apart.
    # Captured once in natural (oldest-to-newest) order; reused both as the
    # preview's context source and, reversed below, for token extraction.
    local pane_text
    pane_text=$(tmux capture-pane -p -J -S - -t "$pane_id")
    [[ -n "$pane_text" ]] || exit 0

    # Saved to a file so the preview subprocess -- a separate invocation of
    # this script, spawned by fzf once per highlighted item -- can look up
    # context for that item without re-capturing the pane itself.
    local pane_file
    pane_file=$(mktemp)
    trap 'rm -f "$pane_file"' EXIT
    printf '%s\n' "$pane_text" >"$pane_file"
    export PANE_GRAB_FILE="$pane_file"

    # tail -r reverses line order so the most recent scrollback surfaces
    # first in the fzf list, while keeping left-to-right token order within
    # each line.
    local pane_text_reversed
    pane_text_reversed=$(tail -r <<<"$pane_text")

    # Three passes. Whitespace-delimited tokens first (so "foo/bar:baz" is a
    # candidate, and a hard-wrapped URL or path -- one line now, courtesy of
    # -J -- comes through whole); then parenthesized spans with the parens
    # stripped (so "(utils.rb:42)" yields "utils.rb:42", and a parenthetical
    # phrase survives as one candidate instead of shattering at its inner
    # spaces); then punctuation-split sub-tokens (so "foo", "bar", "baz" are
    # too). A final pair of filters drops any candidate carrying a raw
    # control byte -- stray escape-sequence remnants, bells, and the like --
    # and any candidate with no ASCII in it at all, like a bare prompt glyph
    # or box-drawing character, since those are never what you meant to
    # copy. LC_ALL=C makes the ASCII-range check byte-exact regardless of
    # the caller's locale.
    local raw_candidates
    raw_candidates=$(
        {
            printf '%s\n' "$pane_text_reversed" | grep --only-matching --extended-regexp '\S+' || true
            printf '%s\n' "$pane_text_reversed" | awk '{
                line = $0
                while (match(line, /\([^()]*\)/)) {
                    if (RLENGTH > 2) print substr(line, RSTART + 1, RLENGTH - 2)
                    line = substr(line, RSTART + RLENGTH)
                }
            }'
            printf '%s\n' "$pane_text_reversed" | grep --only-matching --extended-regexp '[[:alnum:]_]+' || true
        } | grep -Ev '[[:cntrl:]]' | LC_ALL=C grep -E '[ -~]' || true
    )

    # A plain first-seen dedup would let a prompt word or repeated cwd
    # component -- present on nearly every scrollback line -- crowd out the
    # one-off candidate actually being hunted for. Sinking anything over a
    # frequency threshold to the bottom (recency order still applies within
    # each bucket) fixes that without losing those tokens outright. The
    # threshold is a share of the total candidate count rather than a fixed
    # number, since what counts as "frequent" scales with how much
    # scrollback got captured -- a 50-line pane and a 50000-line one need
    # different absolute cutoffs but the same relative one. It's set high on
    # purpose: measured against real scrollback, a commit/change id
    # referenced a handful of times in a normal session (log, show,
    # describe, squash target, ...) lands nowhere near the share a prompt
    # word or repeated path component racks up just by the prompt printing
    # on every line.
    local freq_threshold_pct="${TMUX_GRAB_FREQ_THRESHOLD_PCT:-0.5}"
    local tokens
    tokens=$(printf '%s\n' "$raw_candidates" | awk -v threshold="$freq_threshold_pct" '
        {
            count[$0]++
            if (!seen[$0]++) order[++n] = $0
            total++
        }
        END {
            for (i = 1; i <= n; i++) {
                t = order[i]
                bucket = ((count[t] / total) * 100 > threshold) ? 1 : 0
                printf "%d\t%d\t%s\n", bucket, i, t
            }
        }
    ' | sort -t $'\t' -k1,1n -k2,2n | cut -f3-)
    [[ -n "$tokens" ]] || exit 0

    # Float via fzf's own tmux popup, unless the caller already gave us a
    # floating container (e.g. a tmux binding running this in a floating
    # pane), where --tmux would open a redundant, conflicting nested popup.
    # FZF_DEFAULT_OPTS (.zshrc) carries its own --tmux default too, so it's
    # cleared here — otherwise it fires regardless of our own --tmux flag.
    local tmux_opts=(--tmux "80%,60%")
    [[ -n "${IN_TMUX_FLOAT:-}" ]] && tmux_opts=()

    # --expect reports which key completed the selection: it's the first
    # line of output, empty unless ctrl-y/ctrl-o was the trigger, with the
    # selected token on the following line.
    local result
    # macOS ships bash 3.2 (last GPLv2 release); under set -u, "${arr[@]}"
    # on an empty array throws "unbound variable" there (fixed in bash 4.4).
    # The +"${arr[@]}" form works around it for both empty and populated arrays.
    result=$(printf '%s\n' "$tokens" | FZF_DEFAULT_OPTS='' fzf --no-multi --select-1 --no-margin --no-padding \
        "${tmux_opts[@]+"${tmux_opts[@]}"}" \
        --layout reverse \
        --height 100% \
        --highlight-line --info inline-right \
        --header 'enter: insert  ctrl-y: copy  ctrl-o: open' --header-first \
        --expect ctrl-y,ctrl-o \
        --preview "\"$0\" __preview__ {}" \
        --border-label ' grab ') || exit 0
    [[ -n "$result" ]] || exit 0

    local key selection
    key=$(head -n1 <<<"$result")
    selection=$(tail -n +2 <<<"$result")
    [[ -n "$selection" ]] || exit 0

    case "$key" in
        ctrl-y)
            printf '%s' "$selection" | pbcopy
            tmux display-message 'Copied to clipboard'
            ;;
        ctrl-o)
            open -- "$selection"
            ;;
        *)
            tmux send-keys -l -t "$pane_id" -- "$selection"
            ;;
    esac
}

# Renders the preview pane for the item currently highlighted in fzf: the
# 2 lines of scrollback on either side of that item's most recent occurrence,
# with the match itself bolded. Invoked as a fresh instance of this script
# (via the --preview flag above), so it reads the captured pane text from the
# file path the main invocation exported, rather than recapturing.
preview_mode() {
    local token=${1:-}
    local file=${PANE_GRAB_FILE:-}
    if [[ -z "$token" || -z "$file" || ! -f "$file" ]]; then
        echo '(no preview available)'
        return
    fi

    # Every candidate, including a hard-wrapped string -J already joined,
    # appears verbatim on some line; grep keeps natural top-to-bottom order,
    # so the last match is the occurrence closest to "now".
    local line
    line=$(grep -Fn -- "$token" "$file" | tail -n1 | cut -d: -f1) || true
    if [[ -n "$line" ]]; then
        render_preview "$file" "$line"
        return
    fi

    echo '(not found in scrollback)'
}

if [[ "${1:-}" == "__preview__" ]]; then
    preview_mode "${2:-}"
    exit 0
fi

main "$@"