From baf16f041905c411fdebdf48a8254d96f3418b82 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 2 Jul 2026 07:36:14 +0100 Subject: [PATCH] Format tests. --- regress/format-modifiers.sh | 358 +++++++++++++++++++++++++++++---- regress/format-mouse.sh | 143 +++++++++++++ regress/format-variables.sh | 388 ++++++++++++++++++++++++++++++++++++ 3 files changed, 854 insertions(+), 35 deletions(-) create mode 100644 regress/format-mouse.sh create mode 100644 regress/format-variables.sh diff --git a/regress/format-modifiers.sh b/regress/format-modifiers.sh index b7cef5ecf..518261345 100644 --- a/regress/format-modifiers.sh +++ b/regress/format-modifiers.sh @@ -43,6 +43,29 @@ test_format() fi } +# test_expand $format $expected +# +# Expand $format in a plain format_expand context (list-windows -F on the +# single-window "tf" session) rather than the format_expand_time context of +# display-message. This matters for t/f: display-message runs the whole format +# through strftime(3), so a strftime specifier there must be doubled (%%H); in a +# format_expand context a single specifier (%H) is applied directly to the +# variable's time. +test_expand() +{ + fmt="$1" + exp="$2" + + out=$($TMUX list-windows -t tf -F "$fmt") + + if [ "$out" != "$exp" ]; then + echo "Format test failed for '$fmt'." + echo "Expected: '$exp'" + echo "But got '$out'" + exit 1 + fi +} + # assert_alive # # Check that the server is still responding (used after operations that could @@ -58,15 +81,22 @@ assert_alive() $TMUX kill-server 2>/dev/null $TMUX new-session -d -s main -x 80 -y 24 || exit 1 +# Single-window session used by test_expand for format_expand-context tests. +$TMUX new-session -d -s tf || exit 1 + # User options used as inputs. Modifiers operate on variable names, so plain -# literals must be provided via options (or a nested #{l:...}). -$TMUX set @s 'abcdefghij' || exit 1 -$TMUX set @path '/usr/local/bin/foo' || exit 1 -$TMUX set @name 'window-name' || exit 1 -$TMUX set @greek 'αβγ' || exit 1 # 6 bytes, 3 columns wide -$TMUX set @cjk '中文' || exit 1 # 6 bytes, 4 columns wide -$TMUX set @host 'myhost' || exit 1 -$TMUX set @ts '1000000000' || exit 1 # 2001-09-09 01:46:40 UTC +# literals must be provided via options (or a nested #{l:...}). They are set +# globally (-g) so they are visible from every session, including the "tf" +# session used by test_expand. +$TMUX set -g @s 'abcdefghij' || exit 1 +$TMUX set -g @path '/usr/local/bin/foo' || exit 1 +$TMUX set -g @name 'window-name' || exit 1 +$TMUX set -g @greek 'αβγ' || exit 1 # 6 bytes, 3 columns wide +$TMUX set -g @cjk '中文' || exit 1 # 6 bytes, 4 columns wide +$TMUX set -g @host 'myhost' || exit 1 +$TMUX set -g @ts '1000000000' || exit 1 # 2001-09-09 01:46:40 UTC +$TMUX set -g @sp 'a b$c' || exit 1 # shell-special characters for q: +$TMUX set -g @hash 'a#b' || exit 1 # a "#" for q/e: # --- Comparisons and matching -------------------------------------------- @@ -84,6 +114,15 @@ test_format "#{m/r:^[0-9]+\$,12a45}" "0" # m/ri: regular expression, ignore case. test_format "#{m/ri:^ab+\$,ABBB}" "1" test_format "#{m/ri:^ab+\$,ACCC}" "0" +# m/z: fuzzy match, returns a boolean. +test_format "#{m/z:foo,foobar}" "1" +test_format "#{m/z:xyz,foobar}" "0" +# m/p: fuzzy match, returns the matched (0-based) column positions. +test_format "#{m/p:ac,abc}" "0,2" +test_format "#{m/p:xyz,abc}" "" +# Fuzzy match against empty text. +test_format "#{m/p:x,}" "" +test_format "#{m/z:x,}" "0" # String comparisons. test_format "#{==:#{@host},myhost}" "1" @@ -107,6 +146,29 @@ test_format "#{!!:0}" "0" test_format "#{!!:non-empty}" "1" +# --- Quoting (q) --------------------------------------------------------- + +# q: escapes shell special characters with a backslash. +test_format "#{q:@sp}" 'a\ b\$c' +# q/e and q/h escape "#" for the format/style parser by doubling it. +test_format "#{q/e:@hash}" 'a##b' +test_format "#{q/h:@hash}" 'a##b' +# q/a quotes the value as a single shell argument. +test_format "#{q/a:@sp}" '"a b\$c"' + + +# --- Name existence (N) -------------------------------------------------- + +# N/w is true if a window with the (expanded) name exists in the session, N/s +# if a session with that name exists. The default (no argument) is /w. +$TMUX rename-window -t main:0 knownwin +test_format "#{N/s:main}" "1" +test_format "#{N/s:nosuchsession}" "0" +test_format "#{N/w:knownwin}" "1" "main:" +test_format "#{N/w:nosuchwindow}" "0" "main:" +test_format "#{N:nosuchwindow}" "0" "main:" + + # --- Numeric operations (e) ---------------------------------------------- # Integer operators. @@ -145,9 +207,14 @@ assert_alive "division by zero" # a: numeric value to its ASCII character. test_format "#{a:98}" "b" test_format "#{a:65}" "A" +# a: out-of-range or non-numeric input yields an empty string. +test_format "#{a:200}" "" +test_format "#{a:notanumber}" "" # R: repeat first argument second-argument times. test_format "#{R:a,3}" "aaa" test_format "#{R:ab,2}" "abab" +# A long repeat exercises output-buffer growth during expansion. +test_format "#{n:#{R:x,300}}" "300" # --- Width, padding and truncation --------------------------------------- @@ -155,15 +222,23 @@ test_format "#{R:ab,2}" "abab" # =N truncates from the start, =-N from the end. test_format "#{=5:@s}" "abcde" test_format "#{=-5:@s}" "fghij" +# = with no width, or a non-numeric width, does not truncate. +test_format "#{=:@s}" "abcdefghij" +test_format "#{=/x:@s}" "abcdefghij" # A marker is appended/prepended only when trimming actually occurs. test_format "#{=/5/...:@s}" "abcde..." test_format "#{=/5/...:@name}" "windo..." test_format "#{=/20/...:@s}" "abcdefghij" -# Truncation is display-width (UTF-8) aware. +# Truncation is display-width (UTF-8) aware: a wide (2-column) character is only +# included if it fits entirely within the limit. test_format "#{=3:@greek}" "αβγ" test_format "#{=2:@greek}" "αβ" test_format "#{=2:@cjk}" "中" test_format "#{=1:@cjk}" "" +# Markers with wide characters: the marker is added when trimming occurs, and a +# limit that splits a wide character drops it entirely. +test_format "#{=/2/x:@cjk}" "中x" +test_format "#{=/1/x:@cjk}" "x" # p pads to a width: a positive width left-aligns (pads on the right), a # negative width right-aligns (pads on the left). @@ -171,6 +246,12 @@ test_format "#{p12:@name}" "window-name " test_format "#{p-12:@name}" " window-name" # No padding once the value already meets the width. test_format "#{p3:@name}" "window-name" +# p with no width does nothing. +test_format "#{p:@name}" "window-name" +# Padding is display-width aware: @cjk is 4 columns wide, so p6/p-6 add two +# spaces (not four). +test_format "#{p6:@cjk}" "中文 " +test_format "#{p-6:@cjk}" " 中文" # n is byte length, w is display width. test_format "#{n:@s}" "10" @@ -199,34 +280,40 @@ if [ -z "$($TMUX display-message -p '#{t/r:@ts}')" ]; then exit 1 fi -# t/f: custom strftime format applied to the variable's time. The % specifiers -# are doubled because display-message also expands the format through strftime; -# the colon inside the format is escaped as '#:'. -test_format "#{t/f/%%Y:@ts}" "2001" -test_format "#{t/f/%%Y-%%m-%%d:@ts}" "2001-09-09" -test_format "#{t/f/%%H#:%%M#:%%S:@ts}" "01:46:40" +# t/f: custom strftime format applied to the variable's time. Tested in a +# format_expand context (list-windows -F), where a single strftime specifier is +# applied directly. (In display-message, which additionally expands the format +# through strftime, these would need to be doubled - %%Y etc.) The colon in the +# format is escaped as '#:' because it is otherwise the modifier separator. +test_expand "#{t/f/%Y:@ts}" "2001" +test_expand "#{t/f/%Y-%m-%d:@ts}" "2001-09-09" +test_expand "#{t/f/%H#:%M#:%S:@ts}" "01:46:40" +# An escaped comma in the custom format is unescaped before strftime. +test_expand "#{t/f/%Y#,end:@ts}" "2001,end" +# T: expands its argument and then runs the result through strftime with the +# current time. A value with no strftime specifier is returned unchanged. +test_format "#{T:@ts}" "1000000000" -# --- Loops (S, W, P) ----------------------------------------------------- - -# Windows in the session, iterated in index order. -$TMUX set -g automatic-rename off -$TMUX rename-window -t main:0 w0 -$TMUX new-window -t main: -n w1 -$TMUX new-window -t main: -n w2 -test_format "#{W:#{window_index}}" "012" "main:" -test_format "#{W:[#{window_name}]}" "[w0][w1][w2]" "main:" - -# Panes: iteration order depends on layout, so assert a per-item constant to -# check the count/iteration only. -$TMUX split-window -t main:0 -d -$TMUX split-window -t main:0 -d -test_format "#{P:x}" "xxx" "main:0" - -# Sessions: assert a per-session constant (order independent). -$TMUX new-session -d -s alpha -$TMUX new-session -d -s beta -test_format "#{S:s}" "sss" +# t/p (pretty) and t/r (relative) format times by age relative to now, with a +# different branch per age band. Build options a known number of seconds in the +# past and check each yields a non-empty result (the exact text depends on the +# wall clock, so only non-emptiness is asserted). +now=$(date +%s) +for age in 30 300 4000 90000 200000 3000000 40000000; do + $TMUX set -g @age "$((now - age))" + if [ -z "$($TMUX display-message -p '#{t/r:@age}')" ]; then + echo "Empty #{t/r:@age} for age ${age}s" + exit 1 + fi + if [ -z "$($TMUX display-message -p '#{t/p:@age}')" ]; then + echo "Empty #{t/p:@age} for age ${age}s" + exit 1 + fi +done +# A time in the future has no relative form. +$TMUX set -g @future "$((now + 100000))" +test_format "#{t/r:@future}" "" # --- Content search (C) -------------------------------------------------- @@ -254,6 +341,10 @@ test_format "#{c:#7f7f7f}" "7f7f7f" test_format "#{c/f:red}" "${ESC}[31m" test_format "#{c/b:red}" "${ESC}[41m" test_format "#{c/b:colour4}" "${ESC}[48;5;4m" +# "none" gives a reset; an unknown colour gives an empty string. +test_format "#{c/f:none}" "${ESC}[0m" +test_format "#{c:notacolour}" "" +test_format "#{c/f:notacolour}" "" # --- Nesting and limits -------------------------------------------------- @@ -267,9 +358,206 @@ test_format "#{n:#{b:@path}}" "3" # Nested l: literal expanded then truncated. test_format "#{=5:#{l:abcdefghij}}" "abcde" +# Deeper nesting: basename -> pad to 10 -> truncate to 5. +test_format "#{=5:#{p10:#{b:@path}}}" "foo " +# A substitution applied to a nested basename. +test_format "#{s/o/O/:#{b:@path}}" "fOO" + # Unbounded self-recursion must hit the loop limit rather than crash. $TMUX set @rec '#{E:@rec}' $TMUX display-message -p '#{E:@rec}' >/dev/null 2>&1 assert_alive "recursive expansion" + +# --- Missing, malformed and limit inputs --------------------------------- + +# An undefined variable expands to empty; modifiers on it behave sensibly. +test_format "#{@undefined}" "" +test_format "#{=5:@undefined}" "" +test_format "#{b:@undefined}" "" +test_format "#{n:@undefined}" "0" + +# Malformed numeric expressions expand to empty rather than erroring out. +test_format "#{e|+|:notanumber,2}" "" # invalid left operand +test_format "#{e|+|:2,notanumber}" "" # invalid right operand +test_format "#{e|badop|:1,2}" "" # unknown operator +test_format "#{e|+|f|x:1,2}" "" # invalid precision +test_format "#{e|+|:1}" "" # too few operands +test_format "#{e|+|f|2|extra:1,2}" "" # too many arguments (limit is 3) + +# Repeat with a non-numeric or zero count yields an empty string. +test_format "#{R:a,notanumber}" "" +test_format "#{R:a,0}" "" + +# Comparisons with too few arguments expand to empty. +test_format "#{==:a}" "" +test_format "#{<:a}" "" + +# A substitution with fewer than two arguments is a no-op. +test_format "#{s/onlyone:@s}" "abcdefghij" + +# A non-numeric width for = or p is treated as no width (no change). +test_format "#{=/x:@s}" "abcdefghij" +test_format "#{p/x:@s}" "abcdefghij" + +# The I (client terminal) modifier with no attached client is empty; this also +# exercises its argument parsing (/c termcap, /f feature, default). The +# non-empty terminal cases are covered with a real client in format-variables.sh. +test_format "#{I/c:RGB}" "" +test_format "#{I/f:overline}" "" +test_format "#{I:x}" "" + + +# --- Escaping inside modifiers ------------------------------------------- + +# A "," or "#" inside a modifier argument is escaped with "#". +test_format "#{s/#,/-/:#{l:a,b,c}}" "a-b-c" # escaped comma in the pattern +test_format "#{=/3/#,:@s}" "abc," # escaped comma in the marker +# The truncation marker is itself expanded as a format. +test_format "#{=/3/#{l:>}:@s}" "abc>" + +# Substitution flags: a third argument of "i" is case-insensitive; an invalid +# regular expression leaves the text unchanged. +test_format "#{s/A/X/i:@s}" "Xbcdefghij" +test_format "#{s/[/X/:@s}" "abcdefghij" + + +# --- Unicode in modifier arguments --------------------------------------- + +# Wide (CJK) and emoji text: matching, substitution, repeat and markers all +# operate on characters, and n/w report bytes/columns. +$TMUX set -g @emoji '😀😀' || exit 1 # 8 bytes, 4 columns +test_format "#{m:*中*,#{@cjk}}" "1" +test_format "#{s/文/X/:@cjk}" "中X" +test_format "#{R:中,3}" "中中中" +test_format "#{=/1/中:@s}" "a中" +test_format "#{w:@emoji}" "4" +test_format "#{n:@emoji}" "8" +test_format "#{=2:@emoji}" "😀" + + +# --- Server messages (show-messages) ------------------------------------- + +# show-messages formats each logged message (this exercises the message-time +# formatting path); just check the server survives producing it. +$TMUX show-messages >/dev/null 2>&1 +assert_alive "show-messages" + + +# --- Verbose expansion (logging) ----------------------------------------- + +# display-message -v turns on format logging, so re-expanding a representative +# set of formats with -v exercises the logging code paths. Only survival is +# checked; the log text itself is not asserted. +for f in \ + '#{=3:@s}' \ + '#{e|+|:2,3}' \ + '#{e|*|f|2:2.5,2}' \ + '#{m:*a*,abc}' \ + '#{<:3,5}' \ + '#{s/a/X/:@s}' \ + '#{b:@path}' \ + '#{t:@ts}' \ + '#{p6:@name}' \ + '#{=3:#{b:@path}}'; do + $TMUX display-message -v -p "$f" >/dev/null 2>&1 +done +assert_alive "verbose expansion" + + +# --- Loops and sorting (S, W, P, L) -------------------------------------- +# +# These need a fully controlled server so the set of sessions, windows and +# panes (and their order) is known, so start from a clean server. This must be +# the last section as it discards the setup above. +$TMUX kill-server 2>/dev/null + +# Sessions, created in this order, so session ids (and hence creation order) +# are zeta=$0, alpha=$1, mike=$2. +$TMUX new-session -d -s zeta -x 80 -y 24 || exit 1 +$TMUX new-session -d -s alpha || exit 1 +$TMUX new-session -d -s mike || exit 1 +$TMUX set -g automatic-rename off + +# S loops over every session. The default order is by session id (SORT_INDEX), +# /i is the same, /n is by name, and the r suffix reverses. +test_format "#{S:#{session_name} }" "zeta alpha mike " +test_format "#{S/i:#{session_name} }" "zeta alpha mike " +test_format "#{S/n:#{session_name} }" "alpha mike zeta " +test_format "#{S/nr:#{session_name} }" "zeta mike alpha " +test_format "#{S/ir:#{session_name} }" "mike alpha zeta " +# /t sorts by activity time; the exact order is timing-dependent, so just check +# every session is still iterated (this exercises the activity-sort branch). +test_format "#{S/t:x}" "xxx" +# An unrecognised sort letter falls back to the default order; /r on its own +# reverses that default (this covers the fall-through branch). +test_format "#{S/r:#{session_name} }" "mike alpha zeta " + +# Windows in session zeta: window 0 renamed charlie, then alpha at 1, bravo at +# 2. The default order is by index (SORT_ORDER), /n is by name, r reverses. +$TMUX rename-window -t zeta:0 charlie +$TMUX new-window -d -t zeta:1 -n alpha +$TMUX new-window -d -t zeta:2 -n bravo +test_format "#{W:#{window_name} }" "charlie alpha bravo " "zeta:" +test_format "#{W/n:#{window_name} }" "alpha bravo charlie " "zeta:" +test_format "#{W/nr:#{window_name} }" "charlie bravo alpha " "zeta:" +test_format "#{W/ir:#{window_index}}" "210" "zeta:" +# /i (by index) and /t (by activity); /i matches the default order here. +test_format "#{W/i:#{window_name} }" "charlie alpha bravo " "zeta:" +test_format "#{W/t:x}" "xxx" "zeta:" +# An unrecognised sort letter falls back to the default order; /r reverses it. +test_format "#{W/r:#{window_name} }" "bravo alpha charlie " "zeta:" + +# Panes in window zeta:charlie. Splitting the active (newest) pane each time +# makes pane index match creation order (0,1,2 left to right). The default +# order is by creation (SORT_CREATION), r reverses. +$TMUX split-window -h -t zeta:charlie +$TMUX split-window -h -t zeta:charlie +test_format "#{P:#{pane_index}}" "012" "zeta:charlie" +test_format "#{P/r:#{pane_index}}" "210" "zeta:charlie" +# A pane-sort argument is accepted; for panes only the r (reverse) suffix has an +# effect, so these all keep the count and exercise the argument branch. +test_format "#{P/i:x}" "xxx" "zeta:charlie" +test_format "#{P/n:x}" "xxx" "zeta:charlie" +test_format "#{P/t:x}" "xxx" "zeta:charlie" + +# Verbose expansion of the loops, to exercise their logging paths. +$TMUX display-message -v -p "#{S:#{session_name}}" >/dev/null 2>&1 +$TMUX display-message -v -t zeta: -p "#{W:#{window_name}}" >/dev/null 2>&1 +$TMUX display-message -v -t zeta:charlie -p "#{P:#{pane_index}}" >/dev/null 2>&1 +assert_alive "verbose loop expansion" + +# L loops over attached clients. Attach two control-mode clients, each held +# open by a background process keeping a FIFO's write end open. +FIFO1="${TMPDIR:-/tmp}/fmt-l-$$-1" +FIFO2="${TMPDIR:-/tmp}/fmt-l-$$-2" +rm -f "$FIFO1" "$FIFO2" +mkfifo "$FIFO1" "$FIFO2" || exit 1 +# Hold the write ends open so the control clients stay attached. +sleep 30 >"$FIFO1" & +HOLD1=$! +sleep 30 >"$FIFO2" & +HOLD2=$! +$TMUX -C attach -t zeta <"$FIFO1" >/dev/null 2>&1 & +CC1=$! +$TMUX -C attach -t alpha <"$FIFO2" >/dev/null 2>&1 & +CC2=$! +sleep 1 +# Two clients attached: L emits one item per client. +test_format "#{L:x}" "xx" +# The client sort orders (default, index, name, activity, reversed) are all +# accepted; assert only the count so the test does not depend on client names or +# timing. +test_format "#{L/i:x}" "xx" +test_format "#{L/n:x}" "xx" +test_format "#{L/t:x}" "xx" +test_format "#{L/nr:x}" "xx" +test_format "#{L/r:x}" "xx" +# Now detach one and confirm the count drops to one. +kill $HOLD2 2>/dev/null +sleep 1 +test_format "#{L:x}" "x" +kill $HOLD1 $CC1 $CC2 2>/dev/null +rm -f "$FIFO1" "$FIFO2" + exit 0 diff --git a/regress/format-mouse.sh b/regress/format-mouse.sh new file mode 100644 index 000000000..8032f8a57 --- /dev/null +++ b/regress/format-mouse.sh @@ -0,0 +1,143 @@ +#!/bin/sh + +# Tests of the mouse format variables (mouse_x, mouse_y, mouse_word, +# mouse_line, ...). These are only populated while a mouse key binding is being +# dispatched, so the test drives a real mouse event: +# +# - an inner client is attached inside a pane of a second ("outer") tmux +# server, giving the inner server a genuine terminal; +# - mouse mode is on and a MouseDown1Pane binding records the mouse format +# variables into an option; +# - an SGR mouse sequence is written to the outer pane, so the inner client +# receives it as a real mouse click. +# +# This exercises the mouse callbacks and the grid word/line lookup code that +# display-message cannot otherwise reach. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" + +cleanup() +{ + $TMUX kill-server >/dev/null 2>&1 + $TMUX2 kill-server >/dev/null 2>&1 +} +fail() +{ + echo "$1" + cleanup + exit 1 +} + +# click COL ROW +# +# Write an SGR mouse press then release (button 0) at 1-based COL/ROW to the +# outer pane holding the inner client. +click() +{ + col="$1" + row="$2" + seq=$(printf '\033[<0;%s;%sM\033[<0;%s;%sm' "$col" "$row" "$col" "$row") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} + +cleanup + +# Inner session with a single pane running cat, so its content is exactly what +# we send it. +$TMUX new-session -d -s cov -x 80 -y 24 'cat' || exit 1 +$TMUX set -g mouse on +sleep 1 +$TMUX send-keys -t cov:0.0 'alpha beta gamma' Enter +sleep 1 + +# Record every pane mouse variable when the pane is clicked. +$TMUX bind -n MouseDown1Pane run-shell \ + "$TMUX set -g @m 'x=#{mouse_x} y=#{mouse_y} word=#{mouse_word} line=#{mouse_line} pane=#{mouse_pane} hl=[#{mouse_hyperlink}]'" + +# Attach a real client inside an outer tmux pane. Clicks all target the first +# row, which lines up with the inner client regardless of the outer status line. +$TMUX2 new-session -d -x 80 -y 24 "$TMUX attach -t cov" || exit 1 +sleep 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." + +# Click column 3, row 1: over the first word ("alpha") of the first line. +click 3 1 + +M=$($TMUX show -gv @m 2>/dev/null) +[ -n "$M" ] || fail "Mouse binding did not fire (no @m)." + +# mouse_x is 0-based column (SGR column 3 -> x 2); mouse_y is 0-based row 0. +case "$M" in +*"x=2 "*) ;; +*) fail "Unexpected mouse_x in: $M" ;; +esac +case "$M" in +*"y=0 "*) ;; +*) fail "Unexpected mouse_y in: $M" ;; +esac +# mouse_word is the word under the cursor, mouse_line the whole line. +case "$M" in +*"word=alpha "*) ;; +*) fail "Unexpected mouse_word in: $M" ;; +esac +case "$M" in +*"line=alpha beta gamma "*) ;; +*) fail "Unexpected mouse_line in: $M" ;; +esac + +# A click in a different column selects a different word. +click 8 1 +M=$($TMUX show -gv @m 2>/dev/null) +case "$M" in +*"word=beta "*) ;; +*) fail "Unexpected mouse_word for second click in: $M" ;; +esac + +# The same variables have a separate path when the pane is in a mode (the word +# and line come from the mode, not the live grid). A binding in the copy-mode +# key table fires while copy mode is active. +$TMUX bind -T copy-mode MouseDown1Pane run-shell \ + "$TMUX set -g @cm 'x=#{mouse_x} word=#{mouse_word} line=#{mouse_line}'" +$TMUX copy-mode -t cov:0.0 +sleep 1 +click 8 1 +CM=$($TMUX show -gv @cm 2>/dev/null) +case "$CM" in +*"word=beta"*) ;; +*) fail "Unexpected copy-mode mouse_word in: $CM" ;; +esac +$TMUX send-keys -t cov:0.0 -X cancel +sleep 1 + +# Hyperlinks: a new window whose pane emits an OSC 8 hyperlink over the text +# "LINKED". Clicking it reports the target URL via mouse_hyperlink (this drives +# the grid hyperlink lookup). The emitter is written to a small script to keep +# the escape sequence readable. +LINKSH="${TMPDIR:-/tmp}/fmt-mouse-link-$$.sh" +cat >"$LINKSH" <<'EOF' +#!/bin/sh +printf '\033]8;;http://example.com\033\\LINKED\033]8;;\033\\\n' +exec cat +EOF +chmod +x "$LINKSH" +$TMUX neww -t cov: -n link "$LINKSH" +sleep 1 +$TMUX select-window -t cov:link +sleep 1 +click 3 1 +M=$($TMUX show -gv @m 2>/dev/null) +rm -f "$LINKSH" +case "$M" in +*"hl=[http://example.com]"*) ;; +*) fail "Unexpected mouse_hyperlink in: $M" ;; +esac + +cleanup +exit 0 diff --git a/regress/format-variables.sh b/regress/format-variables.sh new file mode 100644 index 000000000..dccc0e390 --- /dev/null +++ b/regress/format-variables.sh @@ -0,0 +1,388 @@ +#!/bin/sh + +# Tests that every format variable listed in tmux(1) (the format_table in +# format.c) can be expanded without crashing the server, and checks the value +# of a stable subset. +# +# The main point is coverage and crash-safety: each variable is expanded in a +# rich context - a real attached client (from a nested tmux), a control-mode +# client, a grouped session, two windows with a bell alert, a window with two +# panes running cat, a paste buffer and options - so the per-variable callbacks +# actually run. format-modifiers.sh covers the modifier machinery; this covers +# the variable callbacks. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest -f/dev/null" +# A second server on its own socket provides a real terminal (an inner client +# attached inside one of its panes) so client terminal variables are populated. +TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" + +# Every variable name in format_table[]. Kept as a plain word list so it can be +# iterated with normal shell word splitting. +NAMES=" +active_window_index +alternate_on +alternate_saved_x +alternate_saved_y +bracket_paste_flag +buffer_created +buffer_full +buffer_mode_format +buffer_name +buffer_sample +buffer_size +client_activity +client_cell_height +client_cell_width +client_colours +client_control_mode +client_created +client_discarded +client_flags +client_height +client_key_table +client_last_session +client_mode_format +client_name +client_pid +client_prefix +client_readonly +client_session +client_termfeatures +client_termname +client_termtype +client_theme +client_tty +client_uid +client_user +client_utf8 +client_width +client_written +config_files +cursor_blinking +cursor_character +cursor_colour +cursor_flag +cursor_shape +cursor_very_visible +cursor_x +cursor_y +history_all_bytes +history_bytes +history_limit +history_size +host +host_short +insert_flag +keypad_cursor_flag +keypad_flag +last_window_index +mouse_all_flag +mouse_any_flag +mouse_button_flag +mouse_hyperlink +mouse_line +mouse_pane +mouse_sgr_flag +mouse_standard_flag +mouse_status_line +mouse_status_range +mouse_utf8_flag +mouse_word +mouse_x +mouse_y +next_session_id +origin_flag +pane_active +pane_at_bottom +pane_at_left +pane_at_right +pane_at_top +pane_bg +pane_bottom +pane_current_command +pane_current_path +pane_dead +pane_dead_signal +pane_dead_status +pane_dead_time +pane_fg +pane_flags +pane_floating_flag +pane_format +pane_height +pane_id +pane_in_mode +pane_index +pane_input_off +pane_key_mode +pane_last +pane_left +pane_marked +pane_marked_set +pane_mode +pane_path +pane_pb_progress +pane_pb_state +pane_pid +pane_pipe +pane_pipe_pid +pane_right +pane_search_string +pane_start_command +pane_start_path +pane_synchronized +pane_tabs +pane_title +pane_top +pane_tty +pane_unseen_changes +pane_width +pane_x +pane_y +pane_z +pane_zoomed_flag +pid +scroll_region_lower +scroll_region_upper +server_sessions +session_active +session_activity +session_activity_flag +session_alert +session_alerts +session_attached +session_attached_list +session_bell_flag +session_created +session_format +session_group +session_group_attached +session_group_attached_list +session_group_list +session_group_many_attached +session_group_size +session_grouped +session_id +session_last_attached +session_many_attached +session_marked +session_name +session_path +session_silence_flag +session_stack +session_windows +sixel_support +socket_path +start_time +synchronized_output_flag +tree_mode_format +uid +user +version +window_active +window_active_clients +window_active_clients_list +window_active_sessions +window_active_sessions_list +window_activity +window_activity_flag +window_bell_flag +window_bigger +window_cell_height +window_cell_width +window_end_flag +window_flags +window_format +window_height +window_id +window_index +window_last_flag +window_layout +window_linked +window_linked_sessions +window_linked_sessions_list +window_marked_flag +window_name +window_offset_x +window_offset_y +window_panes +window_raw_flags +window_silence_flag +window_stack_index +window_start_flag +window_visible_layout +window_width +window_zoomed_flag +wrap_flag +" + +# test_var $name $expected [$extra_args...] +# +# Expand a single #{name} and compare against $expected. Any extra arguments +# are passed straight to display-message (e.g. -c or -t). +test_var() +{ + name="$1" + exp="$2" + shift 2 + + out=$($TMUX display-message "$@" -p "#{$name}") + if [ "$out" != "$exp" ]; then + echo "Variable test failed for '#{$name}'." + echo "Expected: '$exp'" + echo "But got '$out'" + exit 1 + fi +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive)" != "alive" ]; then + echo "Server did not survive: $1" + exit 1 + fi +} + +FIFO="${TMPDIR:-/tmp}/fmt-vars-$$" +HOLD="" +CC="" + +cleanup() +{ + [ -n "$HOLD" ] && kill $HOLD 2>/dev/null + [ -n "$CC" ] && kill $CC 2>/dev/null + rm -f "$FIFO" + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} +fail() +{ + echo "$1" + cleanup + exit 1 +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +# A session "cov" with a window "win0" holding two panes running cat, plus a +# second window, an option and a paste buffer. +$TMUX new-session -d -s cov -x 80 -y 24 -n win0 'cat' || exit 1 +$TMUX set -g automatic-rename off +$TMUX set -g monitor-bell on +$TMUX set -g monitor-activity on +$TMUX split-window -t cov:win0 -d 'cat' || exit 1 +$TMUX new-window -d -t cov:1 -n win1 'cat' || exit 1 +$TMUX set -g @opt 'optionvalue' || exit 1 +$TMUX set-buffer -b buf0 'somebuffer' || exit 1 + +# A second session grouped with cov, so the session_group_* variables have real +# data to report. +$TMUX new-session -d -s cov2 -t cov || exit 1 + +sleep 1 +$TMUX send-keys -t cov:win0.0 'some pane content' Enter +# Ring the bell in the non-current window so a bell alert is raised on the +# session (this populates session_alert/session_alerts and window_bell_flag). +$TMUX send-keys -t cov:win1.0 C-g +sleep 1 + +# Attach a control-mode client, held open by a background process keeping the +# write end of a FIFO open, so client_* variables have a client to read. +rm -f "$FIFO" +mkfifo "$FIFO" || exit 1 +sleep 30 >"$FIFO" & +HOLD=$! +$TMUX -C attach -t cov <"$FIFO" >/dev/null 2>&1 & +CC=$! + +# Attach a real client too: an inner tmux running inside a pane of the second +# server gets a genuine terminal, which populates the terminal-dependent client +# variables (client_termname, cursor_shape, the I modifier, ...). +$TMUX2 new-session -d -x 90 -y 30 "$TMUX attach -t cov" || exit 1 +sleep 1 + +# The real (terminal) client, identified by not being in control mode. +RC=$($TMUX list-clients -F '#{client_control_mode} #{client_name}' | + awk '$1==0 { print $2; exit }') +# The control client. +CLIENT=$($TMUX list-clients -F '#{client_control_mode} #{client_name}' | + awk '$1==1 { print $2; exit }') +[ -n "$RC" ] || fail "No real client attached." +[ -n "$CLIENT" ] || fail "No control client attached." + +# Expand every variable at once, with the real terminal client and a target +# pane in context, and confirm the server survives. This runs every callback. +FMT="" +for n in $NAMES; do + FMT="$FMT#{$n}" +done +$TMUX display-message -c "$RC" -t cov:win0.0 -p "$FMT" >/dev/null 2>&1 +assert_alive "expanding all variables together" + +# Expand each variable on its own too, so a crash can be pinned to one name. +for n in $NAMES; do + $TMUX display-message -c "$RC" -t cov:win0.0 -p "#{$n}" >/dev/null 2>&1 + assert_alive "expanding #{$n}" +done + +# Deterministic checks on stable variables (targeting pane 0 of window 0). +TGT="cov:win0.0" +test_var session_name "cov" -t "$TGT" +test_var window_name "win0" -t "$TGT" +test_var window_index "0" -t "$TGT" +test_var window_panes "2" -t "$TGT" +test_var session_windows "2" -t "$TGT" +test_var pane_index "0" -t "$TGT" +test_var pane_in_mode "0" -t "$TGT" +test_var pane_at_top "1" -t "$TGT" +test_var pane_at_left "1" -t "$TGT" +test_var last_window_index "1" -t "$TGT" +test_var pid "$($TMUX display-message -p '#{pid}')" -t "$TGT" + +# The grouped session is reported as such. +test_var session_grouped "1" -t "cov:" +test_var session_group_size "2" -t "cov:" + +# list-buffers -F formats each paste buffer (this fills in the paste-buffer +# format defaults). +if [ "$($TMUX list-buffers -F '#{buffer_name}=#{buffer_sample}')" != \ + "buf0=somebuffer" ]; then + fail "Unexpected list-buffers format output." +fi + +# Version reported by the variable matches tmux -V. +VER=$($TMUX -V | sed 's/^tmux //') +test_var version "$VER" -t "$TGT" + +# Client variables from each kind of client. +test_var client_name "$CLIENT" -c "$CLIENT" +test_var client_control_mode "1" -c "$CLIENT" +test_var client_control_mode "0" -c "$RC" +test_var socket_path "$($TMUX display-message -p '#{socket_path}')" -c "$CLIENT" +# The real client has a terminal, so termcap/feature/environ queries work. +test_var "I/e:TERM" "$($TMUX display-message -c "$RC" -p '#{client_termname}')" \ + -c "$RC" +# Termcap and feature queries against a real terminal return a boolean. +case "$($TMUX display-message -c "$RC" -p '#{I/c:colors}')" in +0|1) ;; +*) fail "Unexpected #{I/c:colors} for real client." ;; +esac +case "$($TMUX display-message -c "$RC" -p '#{I/f:256}')" in +0|1) ;; +*) fail "Unexpected #{I/f:256} for real client." ;; +esac + +# Time variables through the pretty and relative modifiers: start_time is the +# recent server start, exercising the "last 24 hours" and "just now" paths. +[ -n "$($TMUX display-message -p '#{t/p:start_time}')" ] || + fail "Empty #{t/p:start_time}." +[ -n "$($TMUX display-message -p '#{t/r:start_time}')" ] || + fail "Empty #{t/r:start_time}." + +cleanup +exit 0