bin: stop tmux-grab from guessing at line wraps
The wrap-join heuristic in bin/,tmux-grab reconnected hard-wrapped
lines by checking for whitespace at the line boundary, but that
signature also matches almost any two adjacent non-blank terminal
lines (a job-control message right above the next shell prompt, for
instance). It fused unrelated lines into a spurious candidate while
still emitting one of them alone, and once fzf truncated both to the
popup width they read as duplicates.
Capture with tmux capture-pane -J instead: it joins exactly the rows
tmux itself tracked as wrapped, so the heuristic (RUNS_AWK,
combined_candidates, find_run_range) is no longer needed and is
removed. A hard-wrapped URL or path now arrives as one line and the
existing whitespace-tokenizing pass picks it up whole.
Assisted-by: Claude Sonnet 5 via Claude Code
diff --git a/bin/,tmux-grab b/bin/,tmux-grab
index f91d6427..cba92444 100755
--- a/bin/,tmux-grab
+++ b/bin/,tmux-grab
@@ -5,79 +5,15 @@ if [[ "${TRACE-0}" == "1" ]]; then
set -o xtrace
fi
-# Shared awk source, concatenated ahead of a caller-supplied emit()/END.
-# joinable() encodes the wrap heuristic: two physical lines belong to the
-# same logical string only when neither side shows a deliberate break --
-# the left line must not end in whitespace and the right must not begin
-# with it (a padded or indented boundary means the newline was intentional,
-# not a hard terminal wrap). Empty lines fail both tests, so a blank line --
-# two newlines, not "only a newline in between" -- always breaks a run.
-# walk_runs() reports each maximal chain of 2+ such lines via emit(start,end).
-# shellcheck disable=SC2016 # $0 et al. are awk fields, not shell expansions.
-RUNS_AWK='
-function joinable(a, b) {
- return (a != "" && b != "" && a !~ /[[:space:]]$/ && b !~ /^[[:space:]]/)
-}
-function walk_runs( i, j) {
- i = 1
- while (i <= NR) {
- j = i
- while (j < NR && joinable(lines[j], lines[j+1])) j++
- if (j > i) emit(i, j)
- i = (j > i) ? j + 1 : i + 1
- }
-}
-{ lines[NR] = $0 }
-'
-
-# Emits one candidate per wrap-joined run, newest (bottom-most) run first to
-# match the reversed token passes below. Each run yields its no-separator
-# concatenation plus every member line individually, so both the
-# reconstructed string and its pieces stay grabbable.
-combined_candidates() {
- awk "$RUNS_AWK"'
- function emit(s, e) { runs_start[++runs_n] = s; runs_end[runs_n] = e }
- END {
- walk_runs()
- for (r = runs_n; r >= 1; r--) {
- joined = ""
- for (k = runs_start[r]; k <= runs_end[r]; k++) joined = joined lines[k]
- print joined
- for (k = runs_start[r]; k <= runs_end[r]; k++) print lines[k]
- }
- }
- ' <<<"$1"
-}
-
-# Locates the run whose concatenation equals a combined candidate, so the
-# preview can bold its span. Prints "start end" (1-based line numbers) of the
-# last such run -- the one closest to now -- or nothing. TOKEN rides in via
-# the environment to dodge awk -v backslash processing on URLs and paths.
-find_run_range() {
- local token=$1 file=$2
- TOKEN="$token" awk "$RUNS_AWK"'
- function emit(s, e, k, joined) {
- joined = ""
- for (k = s; k <= e; k++) joined = joined lines[k]
- if (joined == ENVIRON["TOKEN"]) { best_start = s; best_end = e }
- }
- END {
- walk_runs()
- if (best_start > 0) print best_start, best_end
- }
- ' "$file"
-}
-
-# Prints a file window spanning 2 lines on either side of [hlstart, hlend],
-# with that inclusive range bolded (a single line for plain tokens, a span
-# for combined candidates).
+# Prints a file window spanning 2 lines on either side of hlline, with that
+# line bolded.
render_preview() {
- local file=$1 hlstart=$2 hlend=$3
- local start=$(( hlstart > 2 ? hlstart - 2 : 1 ))
- local end=$(( hlend + 2 ))
- awk -v start="$start" -v end="$end" -v hlstart="$hlstart" -v hlend="$hlend" '
+ 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 >= hlstart && NR <= hlend) printf "\033[1m%s\033[0m\n", $0
+ if (NR == hlline) printf "\033[1m%s\033[0m\n", $0
else print
}
' "$file"
@@ -90,10 +26,15 @@ main() {
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 -S - -t "$pane_id")
+ 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
@@ -111,20 +52,19 @@ main() {
local pane_text_reversed
pane_text_reversed=$(tail -r <<<"$pane_text")
- # Four passes, feeding a first-seen dedup so exact repeats collapse.
- # Wrap-joined runs come first (natural order, so it reads the unreversed
- # capture) to surface reconstructed strings at the top; then
- # whitespace-delimited tokens (so "foo/bar:baz" is a candidate); 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
- # filter drops any candidate carrying a raw control byte -- stray escape-
- # sequence remnants, bells, and the like -- so only real text reaches fzf.
+ # Three passes, feeding a first-seen dedup so exact repeats collapse.
+ # 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 filter drops any candidate carrying a raw control byte --
+ # stray escape-sequence remnants, bells, and the like -- so only real
+ # text reaches fzf.
local tokens
tokens=$(
{
- combined_candidates "$pane_text"
printf '%s\n' "$pane_text_reversed" | grep --only-matching --extended-regexp '\S+' || true
printf '%s\n' "$pane_text_reversed" | awk '{
line = $0
@@ -196,23 +136,13 @@ preview_mode() {
return
fi
- # A plain token, a substring of a line, or an individual member line of a
- # run all appear verbatim on some line; grep keeps natural top-to-bottom
- # order, so the last match is the occurrence closest to "now".
+ # 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" "$line"
- return
- fi
-
- # A combined candidate exists only as the concatenation of consecutive
- # lines, never verbatim, so fall back to locating its run and bolding the
- # whole span.
- local range
- range=$(find_run_range "$token" "$file") || true
- if [[ -n "$range" ]]; then
- render_preview "$file" "${range% *}" "${range#* }"
+ render_preview "$file" "$line"
return
fi