Merge and fix conflicts.

This commit is contained in:
Michael Grant
2026-10-10 10:16:38 +02:00
150 changed files with 8083 additions and 1084 deletions

View File

@@ -0,0 +1,34 @@
#!/bin/sh
# A pending pane resize must still reach the pty when the application enters
# the alternate screen before the resize timer fires.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest$$ -f/dev/null"
TMP=$(mktemp)
TMP2=$(mktemp)
trap 'rm -f "$TMP" "$TMP2"; $TMUX kill-server 2>/dev/null' 0 1 15
$TMUX new -d -x 80 -y 24 \
"read x; printf '\033[?1049h'; read x; stty size >$TMP; sleep 100" \
</dev/null || exit 1
sleep 1
# The first split resizes and starts the resize timer, the second queues
# another resize while the timer is pending.
$TMUX splitw -h -d 'sleep 100' || exit 1
$TMUX splitw -v -d -t:0.0 'sleep 100' || exit 1
$TMUX send -t:0.0 Enter || exit 1
sleep 1
$TMUX send -t:0.0 Enter || exit 1
sleep 1
$TMUX display -p -t:0.0 '#{pane_height} #{pane_width}' >$TMP2
cmp -s "$TMP" "$TMP2" || exit 1
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -0,0 +1,79 @@
#!/bin/sh
# Selecting a pane with attach-session from an already attached client must
# redraw pane contents when window-style and window-active-style differ.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lattach-redraw-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lattach-redraw-outer-$$ -f/dev/null"
BEFORE=$DIR/before
AFTER=$DIR/after
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
LEFT=$($INNER new-session -dPF '#{pane_id}' -s inner -x 40 -y 8 \
"printf 'LEFT'; exec sleep 100") || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g default-terminal screen || exit 1
$INNER split-window -h -t "$LEFT" "printf 'RIGHT'; exec sleep 100" || exit 1
$INNER set-option -w -t "$LEFT" window-style bg=red || exit 1
$INNER set-option -w -t "$LEFT" window-active-style bg=blue || exit 1
$INNER bind-key -n x attach-session -t "$LEFT" || exit 1
$OUTER new-session -d -s outer -x 40 -y 8 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lattach-redraw-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
sleep 1
$OUTER send-keys -t outer:0.0 x || exit 1
sleep 1
[ "$($INNER display-message -p -t inner '#{pane_id}')" = "$LEFT" ] ||
fail "attach-session did not select the target pane"
$OUTER capture-pane -pe -t outer:0.0 >"$BEFORE" || exit 1
# A forced redraw produces the correct scene. It must be identical to the
# scene drawn immediately by attach-session.
$INNER refresh-client -t "$CLIENT" || exit 1
sleep 1
$OUTER capture-pane -pe -t outer:0.0 >"$AFTER" || exit 1
cmp -s "$BEFORE" "$AFTER" ||
fail "attach-session left stale active/inactive pane styles"
exit 0

View File

@@ -0,0 +1,51 @@
#!/bin/sh
# a client lost while the configuration is loading is freed
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMPDIR=$(mktemp -d) || exit 1
TMUX="$TEST_TMUX -S$TMPDIR/tmux.sock"
cleanup()
{
touch $TMPDIR/go
$TMUX kill-server 2>/dev/null
rm -rf "$TMPDIR"
}
trap cleanup 0 1 15
# Poll until a command succeeds.
wait_for()
{
i=0
while ! "$@"; do
[ $i -eq 100 ] && exit 1
i=$((i + 1))
sleep 0.1
done
}
# The run-shell holds the configuration until the go file exists.
cat <<EOF >$TMPDIR/conf
new-session -d -s keep
run-shell 'touch $TMPDIR/started; i=0; while [ ! -f $TMPDIR/go ] && [ \$i -lt 100 ]; do sleep 0.1; i=\$((i + 1)); done'
EOF
cd $TMPDIR || exit 1
$TMUX -v -f$TMPDIR/conf new-session -d -s first </dev/null &
pid=$!
wait_for test -f $TMPDIR/started
kill $pid
wait $pid
log=$(echo $TMPDIR/tmux-server-*.log)
wait_for grep -q 'lost client' $log
c=$(sed -n 's/.* lost client //p' $log)
# The client is freed straight away, well before the configuration ends.
wait_for grep -q "free client $c " $log
exit 0

View File

@@ -0,0 +1,90 @@
#!/bin/sh
# A client lost before its configuration callback fires must not start waiting.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 - "$TEST_TMUX" "$(dirname "$0")/../tmux-protocol.h" <<'PY'
import os
from pathlib import Path
import re
import socket
import struct
import subprocess
import sys
import tempfile
import time
tmux = sys.argv[1]
header = Path(sys.argv[2]).read_text()
version = int(re.search(r"#define PROTOCOL_VERSION\s+(\d+)", header)[1])
enum = re.search(r"enum msgtype\s*\{(.*?)\};", header, re.S)[1]
enum = re.sub(r"/\*.*?\*/", "", enum, flags=re.S)
types = {}
value = 0
for entry in enum.split(","):
entry = entry.strip()
if not entry:
continue
if "=" in entry:
entry, number = entry.split("=")
value = int(number.strip())
types[entry.strip()] = value
value += 1
def message(name, body=b""):
return struct.pack("=IIII", types[name], 16 + len(body), version,
os.getpid()) + body
def wait_for(predicate):
end = time.monotonic() + 5
while time.monotonic() < end:
result = predicate()
if result:
return result
time.sleep(0.05)
raise AssertionError("timed out waiting for client cleanup")
with tempfile.TemporaryDirectory(prefix="tmux-cfg-early-") as tmp:
root = Path(tmp)
sockpath = root / "socket"
go = root / "go"
conf = root / "conf"
conf.write_text("run-shell 'i=0; while [ ! -f %s ] && [ $i -lt 100 ]; "
"do sleep 0.1; i=$((i + 1)); done'\n" % go)
server = subprocess.Popen([tmux, "-D", "-v", "-S", str(sockpath),
"-f", str(conf)], cwd=tmp, stdout=subprocess.DEVNULL)
logpath = root / ("tmux-server-%d.log" % server.pid)
def log():
return logpath.read_text() if logpath.exists() else ""
try:
wait_for(sockpath.exists)
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
client.connect(str(sockpath))
# Both messages are read together. The short MSG_COMMAND makes
# proc_event_cb lose the client before server_loop runs its queue.
client.sendall(message("MSG_IDENTIFY_CWD", tmp.encode() + b"\0") +
message("MSG_IDENTIFY_CLIENTPID", struct.pack("=i", os.getpid())) +
message("MSG_IDENTIFY_DONE") + message("MSG_COMMAND"))
lost = wait_for(lambda: re.search(r"lost client (\S+)", log()))[1]
wait_for(lambda: "free client %s (0 references)" % lost in log())
contents = log()
callback = "cmdq_next <client-%d>: [cfg_client_done/" % os.getpid()
assert contents.index("lost client " + lost) < \
contents.index(callback), contents
# Cleanup must happen while configuration is still blocked.
assert "cmdq_next <global>: [cfg_done/" not in contents, contents
finally:
go.touch()
server.terminate()
try:
server.wait(timeout=3)
except subprocess.TimeoutExpired:
server.kill()
server.wait()
PY

60
regress/cfg-load-once.sh Normal file
View File

@@ -0,0 +1,60 @@
#!/bin/sh
# the configuration is loaded once if the client that started it is lost, and
# the next client still waits for it to finish
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMPDIR=$(mktemp -d) || exit 1
TMUX="$TEST_TMUX -S$TMPDIR/tmux.sock"
cleanup()
{
touch $TMPDIR/go
$TMUX kill-server 2>/dev/null
rm -rf "$TMPDIR"
}
trap cleanup 0 1 15
# Poll until a command succeeds.
wait_for()
{
i=0
while ! "$@"; do
[ $i -eq 100 ] && exit 1
i=$((i + 1))
sleep 0.1
done
}
# The run-shell holds the configuration until the go file exists.
cat <<EOF >$TMPDIR/conf
set -ga @runs x
new-session -d -s keep
run-shell 'touch $TMPDIR/started; i=0; while [ ! -f $TMPDIR/go ] && [ \$i -lt 100 ]; do sleep 0.1; i=\$((i + 1)); done'
set -g @late 1
EOF
cd $TMPDIR || exit 1
$TMUX -v -f$TMPDIR/conf new-session -d -s first </dev/null &
pid=$!
wait_for test -f $TMPDIR/started
kill $pid
wait $pid
log=$(echo $TMPDIR/tmux-server-*.log)
wait_for grep -q 'lost client' $log
# The second client must identify before the configuration is released.
$TMUX new-session -d -s second \; display -p '#{@runs} #{@late}' \
</dev/null >$TMPDIR/out &
pid=$!
wait_for grep -q "name is client-$pid\$" $log
touch $TMPDIR/go
wait $pid || exit 1
[ "$(cat $TMPDIR/out)" = "x 1" ] || exit 1
exit 0

View File

@@ -5,6 +5,15 @@
PATH=/bin:/usr/bin
TERM=screen
# The pane's shell must not be the user's own interactive shell: a custom
# PS1/PROMPT_COMMAND that sets the terminal title (as many do, for tmux/xterm
# TERM types) would redraw over the titles this test sets and checks on
# every prompt, regardless of how long it waits first.
shell=
if command -v bash >/dev/null 2>&1; then
shell='bash --noprofile --norc +o history'
fi
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
@@ -30,7 +39,7 @@ must_equal()
[ "$got" = "$want" ] || fail "got '$got', expected '$want'"
}
$TMUX new-session -d -x 80 -y 24 || exit 1
$TMUX new-session -d -x 80 -y 24 -- $shell || exit 1
$TMUX set-option -qg allow-set-title on || exit 1
$TMUX set-option -qg allow-rename on || exit 1
$TMUX set-option -qg automatic-rename off || exit 1

View File

@@ -0,0 +1,69 @@
#!/bin/sh
# A bare assignment in a command sequence must not discard earlier commands.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
CONF=$(mktemp)
trap '$TMUX kill-server 2>/dev/null; rm -f "$CONF"' 0
trap 'exit 1' 1 2 15
$TMUX new-session -d || exit 1
check()
{
name=$1
expected=$2
$TMUX set -g @trace '' || exit 1
$TMUX set-environment -gu PARSE_ASSIGN || exit 1
printf '%s\n' "$3" >"$CONF"
$TMUX source-file "$CONF" || exit 1
trace=$($TMUX show-options -gqv @trace) || exit 1
assignment=$($TMUX show-environment -g PARSE_ASSIGN 2>/dev/null)
out="$trace:$assignment"
if [ "$out" != "$expected" ]; then
echo "$name: expected '$expected', got '$out'"
cat "$CONF"
exit 1
fi
}
check trailing 'ab:PARSE_ASSIGN=1' \
'set -ag @trace a; set -ag @trace b; PARSE_ASSIGN=1'
check middle 'ab:PARSE_ASSIGN=1' \
'set -ag @trace a; PARSE_ASSIGN=1; set -ag @trace b'
check leading 'ab:PARSE_ASSIGN=1' \
'PARSE_ASSIGN=1; set -ag @trace a; set -ag @trace b'
check repeated 'ab:PARSE_ASSIGN=2' \
'set -ag @trace a; PARSE_ASSIGN=1; set -ag @trace b; PARSE_ASSIGN=2'
check semicolon 'a:PARSE_ASSIGN=1' \
'set -ag @trace a; PARSE_ASSIGN=1;'
check standalone ':PARSE_ASSIGN=1' 'PARSE_ASSIGN=1'
check braces 'ab:PARSE_ASSIGN=1' \
'if-shell -F 1 { set -ag @trace a; set -ag @trace b; PARSE_ASSIGN=1 }'
check active 'ab:PARSE_ASSIGN=1' \
'%if 1 set -ag @trace a; set -ag @trace b; PARSE_ASSIGN=1 %endif'
check inactive ':' \
'%if 0 set -ag @trace a; set -ag @trace b; PARSE_ASSIGN=1 %endif'
# An assignment must not hide an invalid command earlier in the sequence.
$TMUX set -g @trace '' || exit 1
printf '%s\n' \
'set -ag @trace a; nonexistent-command; PARSE_ASSIGN=1' >"$CONF"
if out=$($TMUX source-file "$CONF" 2>&1); then
echo 'Invalid command before assignment was ignored'
exit 1
fi
if [ "$out" != "$CONF:1: unknown command: nonexistent-command" ]; then
echo "Unexpected parse error: '$out'"
exit 1
fi
trace=$($TMUX show-options -gqv @trace) || exit 1
if [ -n "$trace" ]; then
echo 'Commands executed despite a parse error'
exit 1
fi
exit 0

View File

@@ -0,0 +1,75 @@
#!/bin/sh
# Assignments and commands must use the first matching conditional branch.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
CONF=$(mktemp)
trap '$TMUX kill-server 2>/dev/null; rm -f "$CONF"' 0
trap 'exit 1' 1 2 15
$TMUX new-session -d || exit 1
branch()
{
printf '%%%s %s%s' "$1" "$2" "$sep"
printf '%sPARSE_BRANCH=%s%sset -g @branch %s%s' \
"$prefix" "$3" "$sep" "$3" "$sep"
}
# Cover no, one and two elif branches, with and without else, all condition
# values, and nesting inside both an active and an inactive outer branch.
for mode in normal hidden inline; do
prefix=
sep='
'
[ "$mode" = hidden ] && prefix='%hidden '
[ "$mode" = inline ] && sep=' '
for elifs in 0 1 2; do
bits=0
while [ "$bits" -lt "$((1 << (elifs + 1)))" ]; do
for otherwise in 0 1; do
for outer in 0 1; do
expected=unchanged
{
printf '%s\n' 'PARSE_BRANCH=unchanged' \
'set -g @branch unchanged'
printf '%%if %s\n' "$outer"
n=0
while [ "$n" -le "$elifs" ]; do
condition=$(((bits >> n) & 1))
directive=elif
[ "$n" -eq 0 ] && directive=if
branch "$directive" "$condition" "$n"
if [ "$condition" -eq 1 ] &&
[ "$expected" = unchanged ]; then
expected=$n
fi
n=$((n + 1))
done
if [ "$otherwise" -eq 1 ]; then
branch else '' else
[ "$expected" = unchanged ] && expected=else
fi
printf '%s\n' '%endif' '%endif' \
'set -g @value "$PARSE_BRANCH"'
} >"$CONF"
[ "$outer" -eq 0 ] && expected=unchanged
$TMUX source-file "$CONF" || exit 1
out=$($TMUX display-message -p '#{@value}:#{@branch}')
if [ "$out" != "$expected:$expected" ]; then
echo "$mode, $elifs elifs, bits=$bits, else=$otherwise, outer=$outer"
echo "Expected $expected:$expected, got $out"
cat "$CONF"
exit 1
fi
done
done
bits=$((bits + 1))
done
done
done
exit 0

View File

@@ -0,0 +1,48 @@
#!/bin/sh
# copy-mode -S can be called from a hook without an attached client.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUT=$(mktemp -d) || exit 1
TMUX_TMPDIR=$OUT
export TMUX_TMPDIR
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
}
trap cleanup EXIT
trap 'exit 1' 1 2 3 15
$TMUX new-session -d -s test 'sleep 300' || fail "new-session failed"
$TMUX set -g remain-on-exit on || fail "setting remain-on-exit failed"
$TMUX set-hook -g pane-died 'copy-mode -S' ||
fail "setting pane-died hook failed"
$TMUX respawn-pane -k -t test:0 'exit 0' || fail "respawn-pane failed"
i=0
while [ "$i" -lt 50 ]; do
$TMUX has-session -t test 2>/dev/null || \
fail "server died in copy-mode -S"
[ "$($TMUX display-message -p -t test:0 '#{pane_dead}' 2>/dev/null)" = 1 ] && \
break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "pane-died hook was not triggered"
sleep 0.5
$TMUX has-session -t test || fail "server died after copy-mode -S"
exit 0

161
regress/copy-mode-search-count.sh Executable file
View File

@@ -0,0 +1,161 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
cleanup()
{
$TMUX kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 3 15
$TMUX new -d -x40 -y10 \
"printf 'zero needle\none needle\ntwo needle\n'; cat" || exit 1
$TMUX set -g window-size manual || exit 1
check_count()
{
[ "$($TMUX display-message -p \
'#{search_count_current}/#{search_count}')" = "$1" ] || exit 1
}
check_unsearched()
{
check_count /
[ -z "$($TMUX display-message -p '#{search_count_present}')" ] ||
exit 1
case "$($TMUX display-message -p \
'#{E:copy-mode-position-format}')" in
*match*|*result*) exit 1 ;;
esac
}
for keys in vi emacs; do
$TMUX set -g mode-keys "$keys" || exit 1
$TMUX copy-mode || exit 1
check_unsearched
$TMUX send-keys -X scroll-up || exit 1
check_unsearched
$TMUX resize-window -x40 -y10 || exit 1
check_unsearched
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -X start-of-line || exit 1
$TMUX send-keys -X search-forward-text needle || exit 1
check_count 1/3
$TMUX send-keys -X search-again || exit 1
check_count 2/3
$TMUX resize-window -x30 -y8 || exit 1
check_count 2/3
$TMUX send-keys -X search-again || exit 1
check_count 3/3
$TMUX send-keys -X search-again || exit 1
check_count 1/3
$TMUX send-keys -X search-reverse || exit 1
if [ "$keys" = vi ]; then
reverse_index=3
else
reverse_index=1
fi
check_count "$reverse_index/3"
case "$($TMUX display-message -p \
'#{E:copy-mode-position-format}')" in
*"($reverse_index of 3 matches)") ;;
*) exit 1 ;;
esac
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode || exit 1
check_unsearched
$TMUX send-keys -X history-bottom || exit 1
$TMUX send-keys -X search-backward-text needle || exit 1
check_count 3/3
$TMUX send-keys -X cancel || exit 1
# Moving onto the second match before searching again skips to the third.
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -X start-of-line || exit 1
$TMUX send-keys -X search-forward-text needle || exit 1
$TMUX send-keys -X cursor-down || exit 1
$TMUX send-keys -X search-again || exit 1
check_count 3/3
$TMUX send-keys -X cancel || exit 1
done
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -X search-forward-text absent || exit 1
check_count /0
case "$($TMUX display-message -p '#{E:copy-mode-position-format}')" in
*'(0 matches)') ;;
*) exit 1 ;;
esac
$TMUX send-keys -X search-forward-text one || exit 1
check_count 1/1
case "$($TMUX display-message -p '#{E:copy-mode-position-format}')" in
*'(1 of 1 match)') ;;
*) exit 1 ;;
esac
$TMUX send-keys -X cancel || exit 1
# Backward searches can find an overlapping start inside a marked match.
# The index must refer to the match after the cursor moves to its beginning.
$TMUX new-window -n adjacent "printf 'aaaa'; cat" || exit 1
for keys in vi emacs; do
$TMUX set -g mode-keys "$keys" || exit 1
$TMUX copy-mode || exit 1
check_unsearched
$TMUX send-keys -X search-backward-incremental -- '=aa' || exit 1
check_count 2/2
$TMUX send-keys -X search-backward-incremental -- '-aa' || exit 1
check_count 1/2
case "$($TMUX display-message -p \
'#{E:copy-mode-position-format}')" in
*'(1 of 2 matches)') ;;
*) exit 1 ;;
esac
$TMUX send-keys -X search-backward-incremental -- '-aa' || exit 1
check_count 2/2
$TMUX send-keys -X search-backward-incremental -- '-aa' || exit 1
check_count 1/2
$TMUX send-keys -X search-backward-incremental -- '=' || exit 1
check_unsearched
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode || exit 1
check_unsearched
$TMUX send-keys -X search-backward-text aa || exit 1
check_count 2/2
$TMUX send-keys -X search-again || exit 1
check_count 1/2
$TMUX send-keys -X search-again || exit 1
check_count 2/2
$TMUX send-keys -X cancel || exit 1
done
# Scrolling into real history must not create a search count.
$TMUX new-window -n scroll \
'i=0; while [ $i -lt 80 ]; do echo "line $i"; i=$((i + 1)); done; cat' ||
exit 1
$TMUX copy-mode -u || exit 1
[ "$($TMUX display-message -p '#{scroll_position}')" -gt 0 ] || exit 1
check_unsearched
$TMUX send-keys -X history-top || exit 1
check_unsearched
$TMUX resize-window -x40 -y10 || exit 1
check_unsearched
$TMUX send-keys -X cancel || exit 1
format=$($TMUX show -gv copy-mode-position-format) || exit 1
case "$format" in
*'#{search_count_current} of '*'#{search_count} matches'*) ;;
*) exit 1 ;;
esac
exit 0

View File

@@ -78,4 +78,59 @@ expected=$(printf 'alpha\nbeta\ngamma')
[ "$($TMUX show-buffer)" = "$expected" ] ||
fail "reversed line-mode selection did not keep complete lines"
# Word mode must initialize its reset boundaries from the existing selection,
# rather than extending it back to the origin on the next cursor movement.
for mode in emacs vi; do
$TMUX send-keys -X cancel || exit 1
$TMUX set-window-option -g mode-keys "$mode" || exit 1
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -X cursor-down || exit 1
$TMUX send-keys -N2 -X cursor-right || exit 1
$TMUX send-keys -X begin-selection || exit 1
$TMUX send-keys -X cursor-right || exit 1
$TMUX send-keys -X selection-mode word || exit 1
$TMUX send-keys -X copy-selection-no-clear -C || exit 1
[ "$($TMUX show-buffer)" = beta ] ||
fail "$mode: word-mode selection did not expand to the current word"
$TMUX send-keys -X cursor-right || exit 1
$TMUX send-keys -X copy-selection-no-clear -C || exit 1
expected=$(printf 'beta\ngamma')
[ "$($TMUX show-buffer)" = "$expected" ] ||
fail "$mode: word-mode movement reset the selection to the origin"
# Reversing the character selection before changing mode must keep the
# fixed end and its containing word when the cursor moves further back.
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -N2 -X cursor-down || exit 1
$TMUX send-keys -N2 -X cursor-right || exit 1
$TMUX send-keys -X begin-selection || exit 1
$TMUX send-keys -X cursor-up || exit 1
$TMUX send-keys -X selection-mode word || exit 1
$TMUX send-keys -X cursor-left || exit 1
$TMUX send-keys -X copy-selection-no-clear -C || exit 1
expected=$(printf 'alpha\nbeta\ngamma')
[ "$($TMUX show-buffer)" = "$expected" ] ||
fail "$mode: reversed word-mode selection lost its fixed end"
# A stopped selection should expand to words without moving its cursor.
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -X cursor-down || exit 1
$TMUX send-keys -N2 -X cursor-right || exit 1
$TMUX send-keys -X begin-selection || exit 1
$TMUX send-keys -X cursor-right || exit 1
$TMUX send-keys -X stop-selection || exit 1
before=$($TMUX display-message -p '#{copy_cursor_x},#{copy_cursor_y}')
$TMUX send-keys -X selection-mode word || exit 1
[ "$($TMUX display-message -p '#{copy_cursor_x},#{copy_cursor_y}')" = \
"$before" ] || fail "$mode: word-mode switch moved a stopped cursor"
$TMUX send-keys -X copy-selection-no-clear -C || exit 1
[ "$($TMUX show-buffer)" = beta ] ||
fail "$mode: stopped word-mode selection did not contain the whole word"
done
exit 0

View File

@@ -110,4 +110,92 @@ wheel 64 5 5
$TMUX send-keys -X copy-selection-no-clear || exit 1
expect_buffer "$extended_start"
# Perform a mouse drag across a sequence of col,row coordinates (1-based).
# SGR mouse format: \033[<btn;col;rowM (btn 0=press, 32=drag) and
# \033[<0;col;rowm (release).
drag()
{
# Mouse button 1 down (press) at the starting coordinate.
col=${1%,*}
row=${1#*,}
seq=$(printf '\033[<0;%s;%sM' "$col" "$row")
shift
# Mouse drag (button 1 held) across each subsequent coordinate.
for pos; do
col=${pos%,*}
row=${pos#*,}
seq="$seq$(printf '\033[<32;%s;%sM' "$col" "$row")"
done
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
# Keep the button held briefly so auto-scrolling can run, and
# capture copy-mode state before mouse release exits copy mode.
sleep 0.5
scroll_pos=$($TMUX display -p '#{scroll_position}')
cursor_y=$($TMUX display -p '#{copy_cursor_y}')
# Mouse button 1 up (release) at the final coordinate.
seq=$(printf '\033[<0;%s;%sm' "$col" "$row")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
}
# Turn off status line so all 10 pane rows are visible in the outer client,
# and write a 24-char line on the bottom row (row 10, longer than the 18-char
# line 79 above it).
$TMUX set -g status off || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX send-keys -l "line 80 xxxxxxxxxxxxxxxx" || exit 1
sleep 0.2
for mode in emacs vi; do
$TMUX setw -g mode-keys "$mode" || exit 1
# In emacs mode the selection end column is exclusive, so selecting all
# characters of an 18- or 24-char line requires placing the cursor one
# column past the last character; in vi mode the end column is inclusive.
if [ "$mode" = emacs ]; then
ecol18=19
ecol24=25
else
ecol18=18
ecol24=24
fi
# Drag along the bottom row (row 10) past the end of the line and back
# when already at the bottom of history; the selection must stay on
# line 80 instead of jumping up to line 79.
$TMUX copy-mode || exit 1
drag 1,10 "$ecol24,10" "$((ecol24 + 1)),10" "$ecol24,10"
expect_buffer "line 80 xxxxxxxxxxxxxxxx"
# Scroll up 1 line so line 80 is off-screen below, then drag from row 9
# to the bottom row (row 10) to auto-scroll down and select lines 78..80.
$TMUX copy-mode || exit 1
$TMUX send-keys -X scroll-up || exit 1
drag 1,9 23,10 "$ecol24,10"
expect_buffer "$(printf 'line 78 xxxxxxxxxx\nline 79 xxxxxxxxxx\nline 80 xxxxxxxxxxxxxxxx')"
# Scroll 1 line below the top of history so line 00 is off-screen above,
# then drag from row 2 to the top row (row 1) to auto-scroll up and
# select lines 00..02.
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -X scroll-down || exit 1
drag "$ecol18,2" 1,1
expect_buffer "$(printf 'line 00 xxxxxxxxxx\nline 01 xxxxxxxxxx\nline 02 xxxxxxxxxx')"
# Scroll up 5 lines, then drag onto the bottom row and keep the button
# held to auto-scroll all the way to the bottom.
$TMUX copy-mode || exit 1
$TMUX send-keys -N5 -X scroll-up || exit 1
drag 1,9 5,10
[ "$scroll_pos" = 0 ] || fail "$mode: drag auto-scroll stopped at $scroll_pos"
# At the top of history, drag onto the top row and keep the button
# held: the cursor must stay on the top row.
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
drag 5,2 5,1
[ "$cursor_y" = 0 ] || fail "$mode: cursor left the top row (row $cursor_y)"
done
exit 0

View File

@@ -0,0 +1,149 @@
#!/bin/sh
# Word selection can extend beyond the cursor's row. Compare what an attached
# client drew with a full copy-mode repaint after extending and shrinking it.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -Lword-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lword-outer-$$ -f/dev/null"
DIR=$(mktemp -d) || exit 1
exit_status=0
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0
trap 'exit 1' 1 2 3 15
fail()
{
echo "$*" >&2
exit 1
}
check_buffer()
{
$INNER send-keys -X copy-selection-no-clear || exit 1
$INNER save-buffer "$DIR/copied" || exit 1
cmp -s "$DIR/expected" "$DIR/copied" || fail 'unexpected copied text'
}
check_redraw()
{
label=$1
sleep 0.2
$OUTER capture-pane -ep -t outer:0.0 >"$DIR/before" || exit 1
$INNER send-keys -X toggle-position || exit 1
$INNER send-keys -X toggle-position || exit 1
sleep 0.2
$OUTER capture-pane -ep -t outer:0.0 >"$DIR/after" || exit 1
if ! cmp -s "$DIR/before" "$DIR/after"; then
diff -u "$DIR/before" "$DIR/after" >&2
echo "$label: stale word-selection highlight" >&2
exit_status=1
fi
}
$INNER new-session -d -s inner -x20 -y8 'sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mode-keys emacs || exit 1
$INNER set-option -g copy-mode-position-format '' || exit 1
$INNER set-option -g copy-mode-selection-style 'fg=white,bg=red' || exit 1
$OUTER new-session -d -s outer -x20 -y8 "$INNER attach -t inner" || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
sleep 0.5
# A wrapped word ending on the penultimate row. In emacs mode end-of-line
# moves onto the exclusive end and extends the selection into the blank row
# below; start-of-line then removes that row from the selection.
awk 'BEGIN { print "PREFIX"; for (i = 0; i < 115; i++) printf "a"; print "" }' \
>"$DIR/text" || exit 1
$INNER new-window -t inner:1 "cat '$DIR/text'; exec sleep 100" || exit 1
$INNER resize-window -x20 -y8 || exit 1
sleep 0.2
$INNER copy-mode || exit 1
$INNER send-keys -X history-top || exit 1
$INNER send-keys -X cursor-down || exit 1
$INNER send-keys -X select-word || exit 1
$INNER send-keys -X end-of-line || exit 1
[ "$($INNER display-message -p '#{selection_end_y}')" = 7 ] ||
fail 'selection did not extend into the blank row'
check_redraw 'extend onto blank row'
awk 'BEGIN { for (i = 0; i < 115; i++) printf "a"; print "" }' \
>"$DIR/expected" || exit 1
check_buffer
$INNER send-keys -X start-of-line || exit 1
[ "$($INNER display-message -p '#{selection_end_y}')" = 6 ] ||
fail 'selection did not shrink away from the blank row'
check_redraw 'shrink away from blank row'
awk 'BEGIN { for (i = 0; i < 115; i++) printf "a" }' \
>"$DIR/expected" || exit 1
check_buffer
# The selected next word can itself wrap across several rows even though the
# cursor only moved within one row. All of those rows must be repainted.
awk 'BEGIN { print "PREFIX"; print "one";
for (i = 0; i < 65; i++) printf "b"; print ""; print "END" }' \
>"$DIR/text" || exit 1
$INNER new-window -t inner:2 "cat '$DIR/text'; exec sleep 100" || exit 1
$INNER resize-window -x20 -y8 || exit 1
sleep 0.2
$INNER copy-mode || exit 1
$INNER send-keys -X history-top || exit 1
$INNER send-keys -X cursor-down || exit 1
$INNER send-keys -X select-word || exit 1
$INNER send-keys -X end-of-line || exit 1
[ "$($INNER display-message -p '#{selection_end_y}')" = 5 ] ||
fail 'selection did not extend over the wrapped next word'
check_redraw 'extend over a wrapped next word'
awk 'BEGIN { print "one"; for (i = 0; i < 65; i++) printf "b" }' \
>"$DIR/expected" || exit 1
check_buffer
$INNER send-keys -X start-of-line || exit 1
check_redraw 'shrink away from a wrapped next word'
printf 'one' >"$DIR/expected" || exit 1
check_buffer
# Drag a word selection onto the bottom row and back while there is still
# history below the viewport. A redraw must not read the next backing row
# and paint it onto the last visible row.
awk 'BEGIN { for (i = 0; i < 30; i++) printf "line%02d abc def\n", i }' \
>"$DIR/text" || exit 1
$INNER new-window -t inner:3 "cat '$DIR/text'; exec sleep 100" || exit 1
$INNER resize-window -x20 -y8 || exit 1
$INNER set-option -g mouse on || exit 1
$INNER unbind-key -Tcopy-mode MouseDragEnd1Pane || exit 1
sleep 0.2
$INNER copy-mode || exit 1
$INNER send-keys -X history-top || exit 1
$INNER send-keys -X cursor-down || exit 1
$INNER send-keys -X select-word || exit 1
$OUTER send-keys -l "$(printf '\033[<0;3;2M')" || exit 1
sleep 0.1
$OUTER send-keys -l "$(printf '\033[<32;8;7M')" || exit 1
sleep 0.1
$INNER send-keys -X selection-mode word || exit 1
$OUTER send-keys -l "$(printf '\033[<32;8;8M')" || exit 1
sleep 0.1
# Leave the edge to cancel the auto-scroll timer before comparing.
$OUTER send-keys -l "$(printf '\033[<32;8;7M')" || exit 1
sleep 0.1
$OUTER send-keys -l "$(printf '\033[<0;8;7m')" || exit 1
sleep 0.1
[ "$($INNER display-message -p '#{selection_mode}')" = word ] ||
fail 'mouse drag did not retain word selection mode'
[ "$($INNER display-message -p '#{scroll_position}')" -gt 0 ] ||
fail 'mouse drag reached the bottom of history'
check_redraw 'drag word selection at bottom of viewport'
exit "$exit_status"

View File

@@ -0,0 +1,66 @@
#!/bin/sh
# display-message -c must take the client formats from the given client, even
# when it is attached to a different session from the target pane.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0 1 15
cleanup
$INNER new-session -d -s one -x40 -y10 'exec sleep 100' || exit 1
$INNER new-session -d -s two -x40 -y10 'exec sleep 100' || exit 1
$INNER set-option -g status off || exit 1
# Each client has a distinct TERM so that client_termname identifies it.
attach()
{
echo "env -i PATH=/bin:/usr/bin TERM=$1 LC_ALL=C.UTF-8 $TEST_TMUX -LtestI$$ -f/dev/null attach-session -t $2"
}
$OUTER new-session -d -s outer -x40 -y10 "$(attach screen one)" || exit 1
$OUTER new-window -d "$(attach xterm two)" || exit 1
$OUTER set-option -g status off || exit 1
i=0
while [ "$i" -lt 50 ]; do
n=$($INNER list-clients -F '#{client_name}' 2>/dev/null|wc -l)
[ "$n" -eq 2 ] && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "clients did not attach"
c1=$($INNER list-clients -F '#{client_name}' -f '#{==:#{client_termname},screen}')
c2=$($INNER list-clients -F '#{client_name}' -f '#{==:#{client_termname},xterm}')
[ -n "$c1" ] && [ -n "$c2" ] || fail "could not find both clients"
# The target is in session two, but the client is the one in session one.
term=$($INNER display-message -c "$c1" -t two: -p '#{client_termname}')
[ "$term" = screen ] || fail "client_termname was '$term', not screen"
# The session formats still come from the target.
session=$($INNER display-message -c "$c1" -t two: -p '#{session_name}')
[ "$session" = two ] || fail "session_name was '$session', not two"
term=$($INNER display-message -c "$c2" -t one: -p '#{client_termname}')
[ "$term" = xterm ] || fail "client_termname was '$term', not xterm"
exit 0

View File

@@ -0,0 +1,135 @@
#!/bin/sh
# Damage-only redraws must evaluate both active and inactive border styles
# for each client, even though the cached border cells belong to the pane.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lborder-style-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lborder-style-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_clients()
{
i=0
while [ "$i" -lt 50 ]; do
count=$($INNER list-clients 2>/dev/null | wc -l)
[ "$count" -eq 2 ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "two inner clients did not attach"
}
wait_for_marker()
{
target=$1
marker=$2
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "client in $target did not receive $marker"
}
cat >"$DIR/emitter.pl" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $phase (1 .. 2) {
while (!-e "$ENV{TRIGGER}-$phase") {
select undef, undef, undef, 0.01;
}
# Change an unused palette entry and acknowledge it in the pane body.
print "\e]4;200;rgb:11/22/0$phase\a\e[1;1HDAMAGE$phase";
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 60 -y 20 \
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -g pane-border-lines simple || exit 1
$INNER set -g pane-border-status top || exit 1
$INNER set -g pane-border-format 'CLIENT=#{client_name}' || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 35 -y 6 -X 5 -Y 5 \
'sleep 100') || exit 1
$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1
$OUTER set -g status off || exit 1
$OUTER set -g window-size manual || exit 1
$OUTER set -g default-terminal screen || exit 1
LEFT=$($OUTER display-message -p -t outer:0.0 '#{pane_id}') || exit 1
RIGHT=$($OUTER split-window -h -PF '#{pane_id}' 'sleep 100') || exit 1
for target in "$LEFT" "$RIGHT"; do
$OUTER respawn-pane -k -t "$target" "$INNER attach-session -t inner" ||
exit 1
done
wait_for_clients
NAME1=$($OUTER display-message -p -t "$LEFT" '#{pane_tty}') || exit 1
NAME2=$($OUTER display-message -p -t "$RIGHT" '#{pane_tty}') || exit 1
STYLE="fg=#{?#{==:#{client_name},$NAME1},red,blue}"
$INNER set -g pane-border-style "$STYLE" || exit 1
$INNER set -g pane-active-border-style "$STYLE" || exit 1
# Capture just the floating pane's rows, excluding the acknowledgement in
# the tiled pane. Exercise each cache by changing the floating pane's focus.
phase=1
while [ "$phase" -le 2 ]; do
if [ "$phase" -eq 2 ]; then
$INNER select-pane -t "$FLOAT" || exit 1
fi
$INNER refresh-client -t "$NAME1" || exit 1
$INNER refresh-client -t "$NAME2" || exit 1
sleep 0.5
for target in "$LEFT" "$RIGHT"; do
$OUTER capture-pane -pe -S 5 -E 10 -t "$target" \
>"$DIR/before-$target" || exit 1
done
RED=$(printf '\033[31m')
BLUE=$(printf '\033[34m')
grep -Fq "$RED" "$DIR/before-$LEFT" || fail "missing red border"
grep -Fq "$BLUE" "$DIR/before-$RIGHT" || fail "missing blue border"
: >"$DIR/trigger-$phase"
wait_for_marker "$LEFT" "DAMAGE$phase"
wait_for_marker "$RIGHT" "DAMAGE$phase"
sleep 0.2
for target in "$LEFT" "$RIGHT"; do
$OUTER capture-pane -pe -S 5 -E 10 -t "$target" \
>"$CAPTURE" || exit 1
diff -u "$DIR/before-$target" "$CAPTURE" ||
fail "phase $phase changed $target's border style"
done
phase=$((phase + 1))
done
exit 0

View File

@@ -0,0 +1,88 @@
#!/bin/sh
# Each drag motion must draw the floating pane content once.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Ldoublecomp-outer-$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.15
}
$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; exec sleep 100" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g status-interval 0 || exit 1
$INNER set-option -g automatic-rename off || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \
"printf 'DRAGMARK'; exec sleep 100") || exit 1
$OUTER new-session -d -s outer -x 40 -y 15 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
GRABCOL=$((XOFF + 3))
BORDERROW=$YOFF
# Begin capture after focus changes from the initial mouse press have settled.
mouse 0 "$GRABCOL" "$BORDERROW" M
$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output'" || exit 1
$INNER refresh-client || exit 1
sleep 0.5
grep -aq DRAGMARK "$DIR/output" || fail "capture missed floating pane content"
grep -aq OUTSIDE "$DIR/output" || fail "capture missed untouched row"
offset=$(wc -c <"$DIR/output")
i=0
steps=6
while [ "$i" -lt "$steps" ]; do
GRABCOL=$((GRABCOL + 1))
mouse 32 "$GRABCOL" "$BORDERROW" M
i=$((i + 1))
done
mouse 0 "$GRABCOL" "$BORDERROW" m
sleep 0.3
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" -eq "$((XOFF + steps))" ] || fail "floating pane did not move six columns"
tail -c +"$((offset + 1))" "$DIR/output" >"$DIR/drag-output"
n=$(perl -0777 -ne '$n = () = /DRAGMARK/g; print "$n\n"' "$DIR/drag-output")
[ "$n" -eq "$steps" ] ||
fail "$steps drag motions wrote the floating pane content $n times"
exit 0

View File

@@ -0,0 +1,108 @@
#!/bin/sh
# Regression test for a floating-pane drag bug: cmd_resize_pane_redraw_floating()
# (cmd-resize-pane.c) reported damage for just a dragged floating pane's
# content rectangle, not the one-cell border frame drawn around it (see the
# "floating" case in screen-redraw.c, which draws that frame at
# xoff-1/yoff-1 through xoff+sx/yoff+sy - one cell outside the pane's own
# content area). Damage scoped to only the content area left the frame's
# previous position undrawn as the pane moved, so dragging it left a trail
# of un-erased border frames behind - visible as several "corners" stacked
# up rather than just the pane's current one.
#
# This bug has nothing to do with images - it reproduces with a plain
# floating pane and no image support required.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
# drag STARTCOL STARTROW ENDCOL ENDROW
#
# Write a plain (unmodified) SGR button-1 press, drag update and release at
# 1-based positions to the outer pane holding the inner client - this
# matches the default MouseDown1Border/MouseDrag1Border bindings used to
# move or resize a floating pane by its border.
drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
$TMUX set -g mouse on
$TMUX set -g default-command 'sh -c "sleep 100"'
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) ||
fail "new-pane -X -Y failed"
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
$TMUX2 new-session -d -x 60 -y 20 "$TMUX attach -t inner" || exit 1
sleep 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# Sanity check: exactly one floating pane, so exactly one top-left corner,
# before dragging anything.
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
n=$(grep -o '┌' $TMP | wc -l)
[ "$n" -eq 1 ] || fail "sanity: expected 1 corner before drag, found $n"
# Drag the floating pane by its top border (row FTOP-1, some column within
# its width) down several rows in a few separate steps, then release. A
# single drag() call already does press/motion/release, so call it several
# times in a row to simulate a multi-step real drag.
GRABCOL=$((FLEFT + FWIDTH / 2))
STARTROW=$FTOP
i=0
while [ $i -lt 6 ]; do
newrow=$((STARTROW + i + 1))
drag $((GRABCOL + 1)) $((STARTROW + i)) $((GRABCOL + 1)) $newrow
i=$((i + 1))
done
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
# Exactly one top-left corner should remain - the pane's current position.
# This is expected to fail before the fix: multiple corners (a trail of
# un-erased frames) would remain from the intermediate drag positions.
n=$(grep -o '┌' $TMP | wc -l)
[ "$n" -eq 1 ] || fail "expected exactly 1 corner after drag, found $n (ghost frames left behind)"
exit 0

View File

@@ -0,0 +1,160 @@
#!/bin/sh
# Regression test: dragging a floating pane across another pane's ordinary
# content must not redraw that other pane's scrollbar, unless the drag
# actually crosses the scrollbar's own strip.
#
# cmd_resize_pane_redraw_floating() (cmd-resize-pane.c) used to flag
# PANE_REDRAWSCROLLBAR on any pane whose whole *body* intersected the
# floating pane's old or new rectangle, rather than just its narrow
# scrollbar strip - so dragging a floating pane back and forth over an
# ordinary tiled pane's content (never touching its scrollbar) still
# needlessly redrew that pane's scrollbar on every motion step. See
# tmux-image-redraw-known-bugs.md for the full write-up.
#
# This is checked by giving the non-dragged pane a distinctive scrollbar
# colour and counting how many times its SGR code appears in the client's
# raw output while the floating pane is dragged vertically over that pane's
# body, well clear of its scrollbar column: with the fix, it should never
# reappear after the initial draw.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
# drag STARTCOL STARTROW ENDCOL ENDROW
drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.5
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
$TMUX set -g status-right '' || fail "set status-right failed"
$TMUX set -g mouse on || fail "set mouse failed"
$TMUX set -g default-command 'sh -c "sleep 100"' || fail "set default-command failed"
$TMUX set -g pane-scrollbars on || fail "set pane-scrollbars failed"
$TMUX split-window -h -t inner 'sh -c "sleep 100"' || fail "split-window failed"
PANES=$($TMUX list-panes -t inner -F '#{pane_id} #{pane_left}')
LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1)
[ -n "$LEFT" ] || fail "could not identify left pane"
# A distinctive scrollbar colour for the non-dragged (left) pane only.
$TMUX set -p -t "$LEFT" pane-scrollbars-style 'fg=colour201,bg=colour17' ||
fail "set pane-scrollbars-style failed"
ALEFT=$($TMUX display-message -p -t "$LEFT" '#{pane_left}')
ATOP=$($TMUX display-message -p -t "$LEFT" '#{pane_top}')
AWIDTH=$($TMUX display-message -p -t "$LEFT" '#{pane_width}')
AHEIGHT=$($TMUX display-message -p -t "$LEFT" '#{pane_height}')
[ "$AWIDTH" -gt 15 ] || fail "left pane too narrow for this test ($AWIDTH)"
# A small floating pane placed well inside the left pane's content area,
# clear of its (right-hand) scrollbar column by several columns.
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 8 -y 5 \
-X $((ALEFT + 2)) -Y $((ATOP + 2))) || fail "new-pane -X -Y failed"
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
[ $((FLEFT + FWIDTH + 3)) -lt $((ALEFT + AWIDTH)) ] ||
fail "sanity: floating pane too close to the scrollbar column"
# Start the outer session with a plain shell, then start capturing before
# triggering the attach - starting the attach as the outer pane's initial
# command would mean pipe-pane only starts after the attach-driven initial
# redraw (which draws the scrollbars) has already happened, missing it.
$TMUX2 new-session -d -x 60 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
# Sanity check: the distinctive scrollbar colour reaches the client at all.
grep -qa '48;5;201' $TMP || fail "sanity: scrollbar colour never reached the client"
: >$TMP
# Drag the floating pane straight up and down by its top border, staying at
# a fixed column the whole time - this never crosses the left pane's
# scrollbar strip, only its ordinary content.
GRABCOL=$((FLEFT + FWIDTH / 2))
row=$FTOP
i=0
while [ $i -lt 6 ]; do
newrow=$((row + 1))
drag $GRABCOL $row $GRABCOL $newrow
row=$newrow
i=$((i + 1))
done
i=0
while [ $i -lt 6 ]; do
newrow=$((row - 1))
drag $GRABCOL $row $GRABCOL $newrow
row=$newrow
i=$((i + 1))
done
# The scrollbar colour should never reappear - its geometry never changed,
# and the drag never crossed its column. This is expected to fail before
# the fix - see the header comment.
n=$(grep -ac '48;5;201' $TMP)
[ "$n" -eq 0 ] ||
fail "left pane's scrollbar was redrawn $n times while dragging over its body only"
# Cross the scrollbar and return, comparing each damaged redraw with a full
# refresh to check that covering and uncovering its cells leaves no artifacts.
offset=$((ALEFT + AWIDTH - FLEFT - FWIDTH / 2))
for delta in "$offset" "$((-offset))"; do
oldleft=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
newcol=$((GRABCOL + delta))
drag "$GRABCOL" "$row" "$newcol" "$row"
GRABCOL=$newcol
newleft=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
[ "$newleft" -eq "$((oldleft + delta))" ] ||
fail "floating pane did not move across the scrollbar as expected"
before=$($TMUX2 capture-pane -pe -t "$OUTER" -S 0 -E "$((AHEIGHT - 1))")
$TMUX refresh-client || fail "refresh-client failed"
sleep 0.5
after=$($TMUX2 capture-pane -pe -t "$OUTER" -S 0 -E "$((AHEIGHT - 1))")
[ "$before" = "$after" ] ||
fail "scrollbar crossing differs from a full redraw"
done
exit 0

View File

@@ -0,0 +1,93 @@
#!/bin/sh
# Synchronized drag output must not cause a full redraw of untouched rows.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lsyncdefer-outer-$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.15
}
$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; exec sleep 100" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g status-interval 0 || exit 1
$INNER set-option -g automatic-rename off || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \
"printf 'DRAGMARK'; exec sleep 100") || exit 1
$OUTER new-session -d -s outer -x 40 -y 15 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$INNER set-option -as terminal-features ',screen-256color:sync' || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
GRABCOL=$((XOFF + 3))
BORDERROW=$YOFF
# Begin capture after focus changes from the initial mouse press have settled.
mouse 0 "$GRABCOL" "$BORDERROW" M
$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output'" || exit 1
$INNER refresh-client || exit 1
sleep 0.5
grep -aq DRAGMARK "$DIR/output" || fail "capture missed floating pane content"
grep -aq OUTSIDE "$DIR/output" || fail "capture missed untouched row"
offset=$(wc -c <"$DIR/output")
i=0
steps=6
while [ "$i" -lt "$steps" ]; do
GRABCOL=$((GRABCOL + 1))
mouse 32 "$GRABCOL" "$BORDERROW" M
i=$((i + 1))
done
mouse 0 "$GRABCOL" "$BORDERROW" m
sleep 0.3
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" -eq "$((XOFF + steps))" ] || fail "floating pane did not move six columns"
tail -c +"$((offset + 1))" "$DIR/output" >"$DIR/drag-output"
n=$(perl -0777 -ne '$n = () = /DRAGMARK/g; print "$n\n"' "$DIR/drag-output")
[ "$n" -ge "$steps" ] || fail "drag output did not reach the client"
perl -0777 -ne 'exit(/\e\[\?2026h/ ? 0 : 1)' "$DIR/drag-output" ||
fail "drag did not use synchronized output"
if grep -aq OUTSIDE "$DIR/drag-output"; then
fail "synchronized drag redrew an untouched row"
fi
exit 0

View File

@@ -0,0 +1,200 @@
#!/bin/sh
# Damage at a floating pane's vacated edge must always redraw a complete
# grid character. redraw_damage_grow_span_clip() (screen-redraw.c) widens a
# damage rectangle's left edge by one cell whenever it isn't already at the
# span's own edge, to pull in a wide character's base half when the edge
# lands on its padding half - but it did this unconditionally, with no
# check of which half it was actually touching. When the edge instead
# already lands cleanly on a fresh character's base cell, growing left
# walks into the *previous*, unrelated character's padding cell and blanks
# it (tty_draw_line() treats any leading padding cell as proof its own
# range starts mid-character).
#
# This is a general damage-composition bug, not specific to any one kind of
# pane, but the exact column parity needs to be deterministic to actually
# catch it (a lucky parity draws fine). This constructs it by creating the
# floating pane, checking its real position (the border-framing offset
# added to -X is not something to hand-compute), and recreating it one
# column over if necessary until the vacated rectangle's left edge lands on
# a base cell.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lwidechar-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lwidechar-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
FLOAT=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
slice_columns()
{
# Extract terminal columns [COL1, COL2) from lines [ROW1, ROW2] of
# $1, decoding UTF-8 - the pane's own new position (well clear of
# this range) must not affect the comparison, so this only looks at
# the narrow strip actually vacated, not the whole line. capture-pane
# text has one decoded character per double-width cell pair (every
# character here is width 2), so terminal columns are converted to
# character indices by halving before slicing.
perl -CSD -e '
my ($row1, $row2, $col1, $col2, $file) = @ARGV;
open my $fh, "<:encoding(UTF-8)", $file or die $!;
my @lines = <$fh>;
my $c1 = int($col1 / 2);
my $c2 = int(($col2 + 1) / 2);
for my $n ($row1 .. $row2) {
my $line = $lines[$n - 1];
$line =~ s/\R\z//;
print substr($line, $c1, $c2 - $c1), "\n";
}
' "$ROW1" "$ROW2" "$COL1" "$COL2" "$1"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
slice_columns "$BASE" >"$DIR/want"
slice_columns "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the floating pane's vacated edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars off || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lwidechar-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
# Create the floating pane, then check its actual resulting position. Try
# adjacent starting columns until the vacated rectangle's left edge
# (xoff - 1) lands on an even (base-cell) column - the odd case is the one
# every earlier manual test happened to land on by chance.
startx=5
tries=0
while [ "$tries" -lt 2 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 12 -y 3 -X "$startx" \
-Y 5 'sh -c "printf FLOATMARK; exec sleep 100"') || exit 1
sleep 0.2
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
oldleft=$((XOFF - 1))
if [ $((oldleft % 2)) -eq 0 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
[ $(((XOFF - 1) % 2)) -eq 0 ] || fail "could not find bad-parity starting column"
wait_outer_has FLOATMARK
ROW1=$((YOFF + 1))
ROW2=$((YOFF + 3))
COL1=$((XOFF - 4))
COL2=$((XOFF + 4))
# Grab the pane's top border a couple of columns in (avoiding the corner
# cells) and drag it well clear of its old rectangle.
GRABCOL=$((XOFF + 3))
BORDERROW=$((YOFF))
seq=$(printf '\033[<0;%s;%sM' "$GRABCOL" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<32;%s;%sM' "$((GRABCOL + 15))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<0;%s;%sm' "$((GRABCOL + 15))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
wait_old_rows_restored
exit 0

View File

@@ -0,0 +1,206 @@
#!/bin/sh
# Mirror of floating-pane-drag-wide-character.sh for the *right* edge of a
# damage rectangle. redraw_damage_grow_span_clip() (screen-redraw.c) widens
# a damage rectangle's right edge by one cell whenever it isn't already at
# the span's own edge, to pull in a wide character's base half when the
# edge lands on its padding half - but it did this unconditionally, with no
# check of which half it was actually touching. When the edge instead
# already lands cleanly on a fresh character's base cell (a character fully
# outside the range), growing right pulls in just that base cell -
# tty_draw_line() sees it can't fit that character's full width in the
# remaining range (tty_draw_line_get_empty()'s gc->data.width > nx check)
# and clears it, exactly as the left-edge bug cleared a neighbouring
# character's padding half.
#
# As with the left-edge test, the exact column parity needs to be
# deterministic to actually catch it. This constructs it by creating the
# floating pane, checking its real position, and recreating it one column
# over if necessary until the vacated rectangle's right edge lands on a
# base cell.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lwidecharr-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lwidecharr-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
FLOAT=
PANEWIDTH=12
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
slice_columns()
{
# Extract terminal columns [COL1, COL2) from lines [ROW1, ROW2] of
# $1, decoding UTF-8 - the pane's own new position (well clear of
# this range) must not affect the comparison, so this only looks at
# the narrow strip actually vacated, not the whole line. capture-pane
# text has one decoded character per double-width cell pair (every
# character here is width 2), so terminal columns are converted to
# character indices by halving before slicing.
perl -CSD -e '
my ($row1, $row2, $col1, $col2, $file) = @ARGV;
open my $fh, "<:encoding(UTF-8)", $file or die $!;
my @lines = <$fh>;
my $c1 = int($col1 / 2);
my $c2 = int(($col2 + 1) / 2);
for my $n ($row1 .. $row2) {
my $line = $lines[$n - 1];
$line =~ s/\R\z//;
print substr($line, $c1, $c2 - $c1), "\n";
}
' "$ROW1" "$ROW2" "$COL1" "$COL2" "$1"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
slice_columns "$BASE" >"$DIR/want"
slice_columns "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the floating pane's vacated right edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars off || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lwidecharr-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
# Create the floating pane, then check its actual resulting position. Try
# adjacent starting columns until the vacated rectangle's right edge
# (xoff + PANEWIDTH + 1, the border-grown exclusive end - see
# window_pane_damage_floating(), window.c) lands on an even (base-cell)
# column.
startx=5
tries=0
while [ "$tries" -lt 2 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x "$PANEWIDTH" -y 3 \
-X "$startx" -Y 5 \
'sh -c "printf FLOATMARK; exec sleep 100"') || exit 1
sleep 0.2
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
oldright=$((XOFF + PANEWIDTH + 1))
if [ $((oldright % 2)) -eq 0 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
[ $(((XOFF + PANEWIDTH + 1) % 2)) -eq 0 ] ||
fail "could not find bad-parity starting column"
wait_outer_has FLOATMARK
ROW1=$((YOFF + 1))
ROW2=$((YOFF + 3))
COL1=$((XOFF + PANEWIDTH - 2))
COL2=$((XOFF + PANEWIDTH + 2))
# Grab the pane's top border a couple of columns in (avoiding the corner
# cells) and drag it well clear of its old rectangle - far enough right
# that its new position starts past the checked columns above (which sit
# just past the pane's *original* right edge).
GRABCOL=$((XOFF + 3))
BORDERROW=$((YOFF))
seq=$(printf '\033[<0;%s;%sM' "$GRABCOL" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<32;%s;%sM' "$((GRABCOL + 20))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<0;%s;%sm' "$((GRABCOL + 20))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
wait_old_rows_restored
exit 0

View File

@@ -0,0 +1,102 @@
#!/bin/sh
# Returning from the alternate screen must redraw the visible part of a
# floating pane clipped at the left edge, the top edge, or both.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Loffscreen-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $row (1 .. 5) {
print "\e[$row;1H", 'A' x 15;
}
while (!-e "$ENV{TRIGGER}-alternate") {
select undef, undef, undef, 0.01;
}
print "\e[?1049h";
for my $row (1 .. 5) {
print "\e[$row;1H", 'B' x 15;
}
while (!-e "$ENV{TRIGGER}-restore") {
select undef, undef, undef, 0.01;
}
print "\e[?1049l";
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
for position in left top both; do
case "$position" in
left) x=-5; y=2 ;;
top) x=5; y=-2 ;;
both) x=-5; y=-2 ;;
esac
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X "$x" -Y "$y" \
"TRIGGER='$DIR/$position' perl '$EMITTER'") || exit 1
[ "$($INNER display-message -p -t "$FLOAT" '#{pane_left},#{pane_top}')" = "$((x + 1)),$((y + 1))" ] ||
fail "$position: floating pane has unexpected position"
wait_outer_has AAAAA
cp "$CAPTURE" "$DIR/primary"
: >"$DIR/$position-alternate"
wait_outer_has BBBBB
: >"$DIR/$position-restore"
wait_outer_has AAAAA
cmp -s "$DIR/primary" "$CAPTURE" ||
fail "$position: primary screen was not completely restored"
$INNER kill-pane -t "$FLOAT" || exit 1
done
exit 0

View File

@@ -0,0 +1,179 @@
#!/bin/sh
# A damage rectangle's clip range is grown to avoid splitting a wide
# character, but redraw_damage_grow_span_clip() (screen-redraw.c) only ever
# checks the span's own pane *content* grid (wp->screen) for that. For a
# REDRAW_SPAN_PANE span, that same range is then also handed to
# redraw_damage_draw_pane_prompt() to recompose the pane's separately
# rendered prompt (wp->prompt, e.g. from "command-prompt -P") over the
# damaged sub-range - but the prompt is drawn into its own, freshly
# allocated one-line screen, unrelated to the pane's content grid, so a
# range grown (or left ungrown) against the content is not necessarily
# grown correctly for the prompt's own wide characters.
#
# This is invisible when the pane's own content is plain ASCII (as here):
# redraw_damage_grow_span_clip() never finds anything to grow against, so
# the raw, ungrown geometric range is passed straight through to the
# prompt - and if that range's edge lands mid-character in the *prompt's*
# grid, tty_draw_line() clears the character it cuts through
# (tty_draw_line_get_empty()'s gc->data.width > nx check, for a trailing
# base cell with no room left for its padding half).
#
# The trigger is a palette change (OSC 4) in a tiled pane that is one half
# of a vertical split running the full height of the window - occluded
# under the floating pane, but still geometrically triggering a redraw of
# its own rectangle. Positioned so the split boundary falls inside the
# floating pane's own CJK prompt, this reproduces exactly Codex's report:
# "a floating pane containing a CJK prompt across a tiled-pane boundary -
# a palette update in the tiled pane blanks a prompt character."
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -Lpromptwide-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lpromptwide-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
CAPTURE=$DIR/capture
FLOAT=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_prompt_row_intact()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
line=$(sed -n "${PROMPTROW}p" "$CAPTURE")
case $line in
*"$PROMPTTEXT"*) return 0 ;;
esac
sleep 0.1
i=$((i + 1))
done
fail "the CJK prompt was not intact after the palette-triggered damage - got: $line"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
my $line = <STDIN>;
print "\e]4;1;rgb:11/22/33\e\\";
sleep 100;
PERL
$INNER new-session -d -s inner -x 60 -y 12 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
LEFT=$($INNER list-panes -t inner -F '#{pane_id}') || exit 1
# Split so the boundary between the two tiled panes falls at column 17 -
# used below to pick a floating-pane column that lands the boundary
# mid-character inside the prompt.
RIGHT=$($INNER split-window -t inner -h -l 43 -PF '#{pane_id}' \
'sleep 100') || exit 1
RX=$($INNER display-message -p -t "$RIGHT" '#{pane_left}') || exit 1
# Create the floating pane, then check its actual resulting position (the
# border-framing offset added to -X is not something to hand-compute). Try
# adjacent starting columns until the split boundary lands on an odd
# (padding-half) column of the prompt's own numbering, and within the
# prompt's 12-column width.
startx=10
tries=0
while [ "$tries" -lt 4 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 24 -y 5 -X "$startx" \
-Y 2 "sh -c 'i=0; while [ \$i -lt 10 ]; do \
printf AAAAAAAAAAAAAAAAAAAAAA\\\\n; i=\$((i+1)); done; sleep 100'") ||
exit 1
sleep 0.2
X1=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
Y1=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
H1=$($INNER display-message -p -t "$FLOAT" '#{pane_height}')
local=$((RX - X1 - 1))
if [ "$local" -ge 1 ] && [ "$local" -le 11 ] &&
[ $((local % 2)) -eq 1 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
local=$((RX - X1 - 1))
[ "$local" -ge 1 ] && [ "$local" -le 11 ] && [ $((local % 2)) -eq 1 ] ||
fail "could not find bad-parity starting column"
$OUTER new-session -d -s outer -x 60 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lpromptwide-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has AAAAAAAAAAAAAAAAAAAAAA
CLIENT=$($INNER list-clients -F '#{client_name}') || exit 1
$INNER select-pane -t "$FLOAT" || exit 1
PROMPTTEXT=$(printf '\344\270\255' | perl -CSD -ne 'print $_ x 6')
$INNER command-prompt -b -P -t "$CLIENT" -p "$PROMPTTEXT" \
'display-message -- %1' || exit 1
wait_outer_has "$PROMPTTEXT"
PROMPTROW=$((Y1 + H1))
# Trigger the damage: unblock the emitter so it fires the palette change in
# the left tiled pane, which is occluded under (but geometrically overlaps)
# the floating pane's prompt row.
$INNER send-keys -t "$LEFT" Enter || exit 1
wait_prompt_row_intact
exit 0

View File

@@ -0,0 +1,151 @@
#!/bin/sh
# Damage redraws must show each client's own pane status, even though the
# cached status screen is shared between clients.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lstatuscc-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_clients()
{
i=0
while [ "$i" -lt 50 ]; do
count=$($INNER list-clients 2>/dev/null | wc -l)
[ "$count" -eq 2 ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "two inner clients did not attach"
}
wait_for_marker()
{
target=$1
marker=$2
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "client in $target did not receive $marker"
}
cat >"$DIR/emitter.pl" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $phase (1 .. 4) {
while (!-e "$ENV{TRIGGER}-$phase") {
select undef, undef, undef, 0.01;
}
# Change an unused palette entry and acknowledge it in the pane body.
print "\e]4;200;rgb:11/22/0$phase\a\e[1;1HDAMAGE$phase";
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 60 -y 20 \
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -g pane-border-lines simple || exit 1
$INNER set -g pane-border-status top || exit 1
$INNER set -g pane-border-format 'CLIENT=<#{client_name}>' || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 35 -y 6 -X 5 -Y 5 \
'sleep 100') || exit 1
$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1
$OUTER set -g status off || exit 1
$OUTER set -g window-size manual || exit 1
$OUTER set -g default-terminal screen || exit 1
LEFT=$($OUTER display-message -p -t outer:0.0 '#{pane_id}') || exit 1
RIGHT=$($OUTER split-window -h -PF '#{pane_id}' 'sleep 100') || exit 1
for target in "$LEFT" "$RIGHT"; do
$OUTER respawn-pane -k -t "$target" "$INNER attach-session -t inner" ||
exit 1
done
wait_for_clients
NAME1=$($OUTER display-message -p -t "$LEFT" '#{pane_tty}') || exit 1
NAME2=$($OUTER display-message -p -t "$RIGHT" '#{pane_tty}') || exit 1
# Disable periodic status updates above and trigger damage without a command
# that also requests a status redraw. Each client must keep its own title.
$INNER refresh-client -t "$NAME1" || exit 1
$INNER refresh-client -t "$NAME2" || exit 1
sleep 0.5
for phase in 0 1 2; do
if [ "$phase" -ne 0 ]; then
: >"$DIR/trigger-$phase"
wait_for_marker "$LEFT" "DAMAGE$phase"
wait_for_marker "$RIGHT" "DAMAGE$phase"
sleep 0.2
fi
for target in "$LEFT" "$RIGHT"; do
if [ "$target" = "$LEFT" ]; then
name=$NAME1
other=$NAME2
else
name=$NAME2
other=$NAME1
fi
$OUTER capture-pane -p -S 5 -E 10 -t "$target" \
>"$CAPTURE" || exit 1
grep -Fq "CLIENT=<$name>" "$CAPTURE" ||
fail "phase $phase: missing $name's pane status"
if grep -Fq "CLIENT=<$other>" "$CAPTURE"; then
fail "phase $phase: $name received $other's pane status"
fi
done
done
# Leave just one client so a cache keyed only by client would remain stale.
$OUTER respawn-pane -k -t "$RIGHT" 'sleep 100' || exit 1
i=0
while [ "$($INNER list-clients | wc -l)" -ne 1 ]; do
[ "$i" -lt 50 ] || fail "second client did not detach"
sleep 0.1
i=$((i + 1))
done
$INNER set-environment -g TEST_STATUS_VALUE initial || exit 1
$INNER set -g pane-border-format 'VALUE=#{TEST_STATUS_VALUE}' || exit 1
$INNER refresh-client -t "$NAME1" || exit 1
wait_for_marker "$LEFT" VALUE=initial
for phase in 3 4; do
# Changing the environment does not itself request a status redraw.
$INNER set-environment -g TEST_STATUS_VALUE "phase$phase" || exit 1
: >"$DIR/trigger-$phase"
wait_for_marker "$LEFT" "DAMAGE$phase"
wait_for_marker "$LEFT" "VALUE=phase$phase"
done
exit 0

View File

@@ -0,0 +1,102 @@
#!/bin/sh
# Resizing a floating pane must refresh session status formats which depend on
# its geometry, not only the pane scene and borders.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lfloating-status-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lfloating-status-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has_status()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
tail -1 "$CAPTURE" | grep -q "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client status did not show $marker"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
$INNER new-session -d -s inner -x 50 -y 12 'sleep 100' || exit 1
FLOAT=$($INNER new-pane -PF '#{pane_id}' -x 10 -y 5 -X 5 -Y 3 \
'sleep 100') || fail "could not create floating pane"
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g status on || exit 1
$INNER set-option -g status-position bottom || exit 1
$INNER set-option -g status-left 'WIDTH=#{pane_width}' || exit 1
$INNER set-option -g status-right '' || exit 1
$INNER set-option -g status-interval 0 || exit 1
$OUTER new-session -d -s outer -x 50 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lfloating-status-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
OLD_WIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}')
wait_outer_has_status "WIDTH=$OLD_WIDTH"
RIGHT=$($INNER display-message -p -t "$FLOAT" '#{pane_right}')
TOP=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
X=$((RIGHT + 2))
Y=$((TOP + 2))
# Grab the right frame and enlarge the floating pane.
mouse 0 "$X" "$Y" M
mouse 32 "$((X + 1))" "$Y" M
mouse 32 "$((X + 8))" "$Y" M
mouse 0 "$((X + 8))" "$Y" m
NEW_WIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}')
[ "$NEW_WIDTH" -ne "$OLD_WIDTH" ] || fail "floating pane was not resized"
wait_outer_has_status "WIDTH=$NEW_WIDTH"
exit 0

View File

@@ -245,6 +245,19 @@ 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"
# Limit the result size as well as the repeat count, including nested repeats.
test_format "#{n:#{R:abcdefgh,8192}}" "65536"
test_format "#{R:abcdefgh,8193}" ""
test_format "#{R:,10000}" ""
test_format "#{n:#{R:#{R:x,256},256}}" "65536"
test_format "#{R:#{R:x,256},257}" ""
test_format "#{R:#{R:abcdefgh,8192}x,1}" ""
test_format "#{n:#{R:中文中,7281}}" "65529"
test_format "#{R:中文中,7282}" ""
test_format "#{n:#{R:x,10000}}" "10000"
test_format "#{R:x,10001}" ""
# Fuzzy matching must not receive an enormous string from nested repeats.
test_format "#{m/p:a,#{R:#{R:#{R:a,353},38},90}}" ""
# --- Width, padding and truncation ---------------------------------------

View File

@@ -26,7 +26,8 @@ FMT="$FMT #{window_active_clients} #{window_active_clients_list}"
$TMUX kill-server 2>/dev/null
# No session: the callbacks have nothing to read and must return empty.
out=$($TMUX start\; display-message -p "$FMT") || fail "display with no session"
out=$($TMUX start\; set -s exit-empty off \; display-message -p "$FMT") ||
fail "display with no session"
[ "$out" = " " ] || fail "no session expanded to '$out'"
$TMUX new-session -d -s src -n one -- sleep 30 || fail "new-session src"

2
regress/format-strings.sh Normal file → Executable file
View File

@@ -176,7 +176,7 @@ test_conditional_with_pane_in_mode "#{?pane_in_mode,abc,#{?#{==:#{session_name},
test_conditional_with_session_name "#{?pane_in_mode,abc,#{?#{==:#{session_name},Summer},ABC,XYZ}}" "ABC" "XYZ"
# Some fancy stackings
test_conditional_with_pane_in_mode "#{?#{==:#{?pane_in_mode,#{session_name},#(echo Spring)},Summer},abc,xyz}" "abc" "xyz"
test_conditional_with_pane_in_mode "#{?#{==:#{?pane_in_mode,#{session_name},#{@cold}},Summer},abc,xyz}" "abc" "xyz"

View File

@@ -0,0 +1,45 @@
#!/bin/sh
# time modifiers with a value outside the range the system can convert
PATH=/bin:/usr/bin
TERM=screen
TZ=UTC
export TZ
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)
trap "rm -f $TMP; $TMUX kill-server 2>/dev/null" 0 1 15
$TMUX new -d
$TMUX set -g @ok 1700000000
$TMUX set -g @huge 3093527980800
$TMUX set -g @bad 9223372036854775807
# A convertible time still formats, so an empty result below means the value
# was rejected and not that the modifier stopped working.
$TMUX display -p '[#{t:@ok}]' >$TMP
(echo "[Tue Nov 14 22:13:20 2023]"|cmp -s - $TMP) || exit 1
$TMUX display -p '[#{t/f/zz:@ok}]' >$TMP
(echo "[zz]"|cmp -s - $TMP) || exit 1
$TMUX display -p '[#{t/p:@ok}]' >$TMP
(echo "[Nov23]"|cmp -s - $TMP) || exit 1
# A year with more digits than ctime has room for must not be printed cut
# short, but other modifiers can still format it.
$TMUX display -p '[#{t:@huge}]' >$TMP
grep -Eqx '\[\]|\[Sat Jan 1 00:00:00 100000\]' $TMP || exit 1
$TMUX display -p '[#{t/f/zz:@huge}]' >$TMP
(echo "[zz]"|cmp -s - $TMP) || exit 1
$TMUX display -p '[#{t:@bad}]' >$TMP
(echo "[]"|cmp -s - $TMP) || exit 1
$TMUX display -p '[#{t/p:@bad}]' >$TMP
(echo "[]"|cmp -s - $TMP) || exit 1
$TMUX display -p '[#{t/f/zz:@bad}]' >$TMP
(echo "[]"|cmp -s - $TMP) || exit 1
exit 0

View File

@@ -0,0 +1,47 @@
#!/bin/sh
. ./input-common.inc
# The shortest collected prefix must leave the final cell at the right edge.
start_pane narrow 2 3 '\033[?7lABC'
check_capture narrow 'AC'
check_cursor narrow '1,0'
check_flags narrow '- AC'
# REP may fill the final cell, but subsequent text still overwrites it.
start_pane repeat 5 3 '\033[?7lA\033[9bYZ'
check_capture repeat 'AAAAZ'
check_cursor repeat '4,0'
check_flags repeat '- AAAAZ'
# Style changes must flush collected cells without moving the final cursor.
start_pane styled 5 3 '\033[?7l\033[31mAB\033[32mCDE\033[34mZ'
check_capture styled 'ABCDZ'
check_cursor styled '4,0'
check_raw_matches styled \
'C 0,0 data=\(1,1,A\).*fg=red\[1\] ' \
'C 0,2 data=\(1,1,C\).*fg=green\[2\] ' \
'C 0,4 data=\(1,1,Z\).*fg=blue\[4\] '
# Overwriting a wide character's padding must clear the old leading cell.
start_pane padding 8 3 '\033[?7lA\343\201\202B\r\033[2CX'
check_capture padding 'A XB'
check_cursor padding '3,0'
check_raw_matches padding \
'C 0,1 data=\(1,1, \) flags=NONE\[0\]' \
'C 0,2 data=\(1,1,X\) flags=NONE\[0\]'
# Insert mode must shift existing cells even when autowrap is disabled.
start_pane insert 5 3 '\033[?7lABCD\r\033[C\033[4hXY\033[4l'
check_capture insert 'AXYBC'
check_cursor insert '3,0'
# Re-enabling wrap after a collected prefix must retain the wrap boundary.
start_pane toggle 5 3 '\033[?7lABCD\033[?7hEF'
check_capture toggle 'ABCDE
F'
check_cursor toggle '1,1'
check_flags toggle 'W ABCDE
- F'
exit $exit_status

View File

@@ -1,5 +1,10 @@
#!/bin/sh
# Verify palette and clipboard payload delivery with both OSC terminators,
# fragmented replies and a separate Escape before the reply. Invalid payloads
# must leave the request pending for a subsequent valid reply. Check that
# foreground and background reports update the colours queried by a pane.
PATH=/bin:/usr/bin
TERM=screen
@@ -8,6 +13,7 @@ TERM=screen
python3 - "$TEST_TMUX" <<'PY'
import os
import select
import shlex
import signal
import subprocess
import sys
@@ -49,22 +55,30 @@ def read_until(fd, needle, timeout=5):
raise RuntimeError("did not see terminal request %r in %r" %
(needle, data))
def wait_file(path, timeout=5):
def wait_file(path, length, timeout=5):
end = time.time() + timeout
while time.time() < end:
try:
with open(path, "rb") as f:
data = f.read()
if data:
if len(data) >= length:
return data
except FileNotFoundError:
pass
time.sleep(0.05)
return b""
def respawn(command):
def respawn(command, output):
with open(output, "wb"):
pass
run("respawn-window", "-k", "-t", "requests:0", command)
time.sleep(0.2)
def wait_pane(expected, context):
end = time.monotonic() + 5
while run("capture-pane", "-p").stdout.strip() != expected:
assert time.monotonic() < end, (context, "unexpected pane bytes",
run("capture-pane", "-p").stdout.strip())
time.sleep(.01)
def cleanup(pid=None):
if pid is not None:
@@ -73,6 +87,8 @@ def cleanup(pid=None):
except ProcessLookupError:
pass
run("kill-server", check=False)
if pid is not None:
os.waitpid(pid, 0)
run("kill-server", check=False)
run("new-session", "-d", "-x", "80", "-y", "24", "-s", "requests",
@@ -82,37 +98,97 @@ pid, fd = attach()
try:
time.sleep(0.5)
with tempfile.NamedTemporaryFile(delete=False) as f:
palette_out = f.name
respawn("stty raw -echo min 1 time 50; "
"printf '\\033]4;99;?\\033\\\\'; "
"dd bs=1 count=27 2>/dev/null | cat -v >%s; sleep 1" %
palette_out)
read_until(fd, b"\033]4;99;?\033\\")
os.write(fd, b"\033]4;99;rgb:0101/0202/0303\033\\")
got = wait_file(palette_out)
expected = b"^[]4;99;rgb:0101/0202/0303^[\\"
if got != expected:
raise AssertionError("palette reply: expected %r got %r" %
(expected, got))
run("set-option", "-g", "assume-paste-time", "0")
run("set-option", "-s", "escape-time", "1000")
run("set-option", "-g", "@seen", "")
run("bind-key", "-n", "Escape", "set-option", "-gF", "@seen",
"#{@seen}E")
run("set-option", "-s", "set-clipboard", "on")
run("set-option", "-s", "get-clipboard", "request")
with tempfile.NamedTemporaryFile(delete=False) as f:
clip_out = f.name
respawn("stty raw -echo min 1 time 50; "
"printf '\\033]52;c;?\\033\\\\'; "
"dd bs=1 count=21 2>/dev/null | cat -v >%s; sleep 1" %
clip_out)
data = read_until(fd, b"]52;")
if b"?" not in data:
raise RuntimeError("clipboard request missing query in %r" % data)
os.write(fd, b"\033]52;c;UmVxdWVzdA==\033\\")
got = wait_file(clip_out)
expected = b"^[]52;c;UmVxdWVzdA==^[\\"
if got != expected:
raise AssertionError("clipboard reply: expected %r got %r" %
(expected, got))
expected_escapes = b""
replies = [
("palette", "\\033]4;99;?\\033\\\\", b"\033]4;99;?\033\\",
b"\033]4;99;rgb:0101/0202/0303", b"\033]4;99;invalid"),
("clipboard", "\\033]52;c;?\\033\\\\", b"]52;",
b"\033]52;c;UmVxdWVzdA==", b"\033]52;c;!!!!"),
]
with tempfile.TemporaryDirectory() as directory:
output = os.path.join(directory, "reply")
for name, query, needle, payload, invalid in replies:
for prefixed in (False, True):
for split in (False, True):
for terminator in (b"\007", b"\033\\"):
context = (name, prefixed, split, terminator)
# Replies to the pane use its request's ST terminator,
# regardless of the terminal reply's terminator.
expected = payload + b"\033\\"
respawn("stty raw -echo min 1 time 50; "
"printf '%s'; dd bs=1 count=%d 2>/dev/null >%s; "
"exec cat -v" %
(query, len(expected), shlex.quote(output)), output)
data = read_until(fd, needle)
assert b"?" in data, (context, "query missing", data)
os.write(fd, invalid + terminator)
time.sleep(.05)
with open(output, "rb") as f:
assert f.read() == b"", (context, "invalid payload delivered")
wire = (b"\033" if prefixed else b"") + payload + terminator
if split:
os.write(fd, wire[:-1])
time.sleep(.05)
with open(output, "rb") as f:
assert f.read() == b"", (context, "partial payload delivered")
os.write(fd, wire[-1:])
else:
os.write(fd, wire)
got = wait_file(output, len(expected))
assert got == expected, (context, expected, got)
if prefixed:
expected_escapes += b"E"
seen = run("show-option", "-gv", "@seen").stdout.strip()
assert seen == expected_escapes, (context, "Escape count", seen)
# Check that decoding did not deliver the reply twice
# or leak raw bytes after delivering the payload.
os.write(fd, b"Z")
wait_pane(b"Z", context)
colour_value = 1
for colour in (10, 11):
for prefixed in (False, True):
for terminator in (b"\007", b"\033\\"):
context = ("colour", colour, prefixed, terminator)
# A different colour each time prevents an earlier
# successful report from masking a later ignored one.
payload = b"\033]%d;rgb:%04x/0202/0303" % (
colour, colour_value * 257)
colour_value += 1
wire = (b"\033" if prefixed else b"") + payload + terminator
if prefixed:
os.write(fd, wire[:-1])
time.sleep(.05)
assert run("capture-pane", "-p").stdout.strip() == b"Z", (
context, "partial reply leaked")
os.write(fd, wire[-1:] + b"X")
expected_escapes += b"E"
else:
os.write(fd, wire + b"X")
wait_pane(b"ZX", context)
seen = run("show-option", "-gv", "@seen").stdout.strip()
assert seen == expected_escapes, (context, "Escape count", seen)
expected = payload + b"\033\\"
respawn("stty raw -echo min 1 time 50; "
"printf '\\033]%d;?\\033\\\\'; "
"dd bs=1 count=%d 2>/dev/null >%s; exec cat -v" %
(colour, len(expected), shlex.quote(output)), output)
got = wait_file(output, len(expected))
assert got == expected, (context, "colour not applied", got)
os.write(fd, b"Z")
wait_pane(b"Z", context)
finally:
cleanup(pid)
os.close(fd)
PY

View File

@@ -0,0 +1,80 @@
#!/bin/sh
# Moving a pane within a window must preserve a modal pane's return target.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
}
trap cleanup EXIT
fail()
{
echo "$*" >&2
exit 1
}
check_ok()
{
$TMUX "$@" || fail "command failed: $*"
}
check_active()
{
active=$($TMUX display-message -p -t "$1" '#{pane_id}')
[ "$active" = "$2" ] ||
fail "active pane is $active, expected $2"
}
check_ok new-session -d -s modal -x 120 -y 40 'cat'
p0=$($TMUX display-message -p -t modal:0 '#{pane_id}')
p1=$($TMUX split-window -PF '#{pane_id}' -t "$p0" 'cat') ||
fail "first split failed"
p2=$($TMUX split-window -PF '#{pane_id}' -t "$p1" 'cat') ||
fail "second split failed"
# Both commands, with and without -d, must restore the moved pane after
# the modal closes. The modal must keep focus during the move.
for command in join-pane move-pane; do
for detached in '' -d; do
check_ok select-pane -t "$p2"
modal=$($TMUX new-pane -OdPF '#{pane_id}' -t "$p0" \
-x 80 -y 10 'cat') || fail "new modal failed"
check_active modal:0 "$modal"
check_ok "$command" $detached -h -s "$p2" -t "$p0"
check_active modal:0 "$modal"
check_ok kill-pane -t "$modal"
check_active modal:0 "$p2"
done
done
# Moving a different pane must also preserve the return target.
modal=$($TMUX new-pane -OPF '#{pane_id}' -t "$p0" \
-x 80 -y 10 'cat') || fail "new modal failed"
check_ok join-pane -h -s "$p1" -t "$p0"
check_active modal:0 "$modal"
check_ok kill-pane -t "$modal"
check_active modal:0 "$p2"
# Moving the return target to another window must still forget it, so
# closing the modal selects a pane remaining in its own window.
check_ok new-window -d -t modal:1 'cat'
modal=$($TMUX new-pane -OPF '#{pane_id}' -t "$p0" \
-x 80 -y 10 'cat') || fail "new modal failed"
check_ok join-pane -d -h -s "$p2" -t modal:1
check_active modal:0 "$modal"
check_ok kill-pane -t "$modal"
active=$($TMUX display-message -p -t modal:0 '#{pane_id}')
case "$active" in
"$p0"|"$p1") ;;
*) fail "modal returned to a pane outside its window: $active" ;;
esac
window=$($TMUX display-message -p -t "$p2" '#{window_index}')
[ "$window" = 1 ] || fail "pane did not move to the other window"

View File

@@ -189,15 +189,15 @@ while [ "$i" -le 20 ]; do
idx=$((50 + i))
run_tmux new-session -d -s "$s" -n base 'sleep 1000' >/dev/null
run_tmux split-window -t "$s:base" 'sleep 1000' >/dev/null
run_tmux respawn-pane -k -t "$s:base.1" 'sleep 1000' >/dev/null
run_tmux split-window -E -t "$s:base" >/dev/null
run_tmux respawn-pane -k -E -t "$s:base.1" >/dev/null
run_tmux kill-pane -t "$s:base.1" >/dev/null
run_tmux new-window -t "$s" -n second 'sleep 1000' >/dev/null
run_tmux new-window -E -t "$s" -n second >/dev/null
run_tmux link-window -s "$s:second" -t "life:$idx" >/dev/null
run_tmux unlink-window -t "life:$idx" >/dev/null
run_tmux new-window -t "$s" -n single 'sleep 1000' >/dev/null
run_tmux new-window -E -t "$s" -n single >/dev/null
run_tmux kill-pane -t "$s:single.0" >/dev/null
run_tmux kill-window -t "$s:base" >/dev/null

164
regress/menu-live-output.sh Normal file
View File

@@ -0,0 +1,164 @@
#!/bin/sh
# Inspect an attached client's terminal while panes write behind a menu.
# Incremental output must agree with the scene renderer, including floating
# panes and a client viewport smaller than the window.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
TMP=$(mktemp -d)
cleanup() {
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
rm -f "$TMP/output" "$TMP/float" "$TMP/actual" "$TMP/expected"
rmdir "$TMP"
}
trap cleanup 0 1 15
fail() {
echo "$*" >&2
exit 1
}
# A full scene redraw is the reference for each incremental update. Check an
# uncovered marker before refreshing, so a frozen pane cannot pass the test.
check_update() {
printf '%b' "$2" >&3
printf '\033[1;1H\033[2K%s' "$1" >&3
sleep 1
$TMUX capturep -p >"$TMP/actual" || exit 1
grep -q "^$1" "$TMP/actual" || fail "$1: pane stopped updating"
$TMUX capturep -pe >"$TMP/actual" || exit 1
$TMUX2 refresh-client || exit 1
sleep 1
$TMUX capturep -pe >"$TMP/expected" || exit 1
if ! cmp -s "$TMP/actual" "$TMP/expected"; then
diff -u "$TMP/expected" "$TMP/actual" >&2
fail "$1: incremental output differs from scene redraw"
fi
}
menu() {
$TMUX2 display-menu -b simple -T live -C 0 "$@" \
alpha a "" beta b "" || exit 1
sleep 1
$TMUX capturep -p >"$TMP/actual" || exit 1
grep -q alpha "$TMP/actual" || fail "menu did not appear"
}
mkfifo "$TMP/output" "$TMP/float" || exit 1
$TMUX2 new -d -x40 -y14 "exec cat '$TMP/output'" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -g window-size manual || exit 1
$TMUX2 resizew -x40 -y14 || exit 1
$TMUX2 set -as terminal-features ',screen:clipboard' || exit 1
$TMUX2 set -g set-clipboard on || exit 1
exec 3>"$TMP/output"
$TMUX new -d -x40 -y14 "$TMUX2 attach" || exit 1
$TMUX set -g status off || exit 1
$TMUX set -g window-size manual || exit 1
$TMUX resizew -x40 -y14 || exit 1
$TMUX set -g set-clipboard on || exit 1
sleep 1
menu -x6 -y8
check_update text '\033[6;1Habcdefghijklmnopqrstuvwxyz0123456789'
check_update clear-line '\033[6;1H\033[2K'
check_update clear-end-line '\033[6;1H\033[K'
check_update clear-start-line '\033[6;40H\033[1K'
check_update erase-character '\033[6;1H\033[40X'
check_update clear-screen '\033[2J'
check_update clear-end-screen '\033[3;1H\033[J'
check_update clear-start-screen '\033[10;40H\033[1J'
check_update scroll '\033[14;1Hone\r\ntwo\r\nthree\r\nfour\r\n'
check_update reverse-scroll '\033[1;1H\033M'
check_update insert-character '\033[6;1Habcdefghijklmnopqrstuvwxyz\033[6;1H\033[3@'
check_update delete-character '\033[6;1Habcdefghijklmnopqrstuvwxyz\033[6;1H\033[3P'
check_update insert-line '\033[5;1H\033[2L'
check_update delete-line '\033[5;1H\033[2M'
check_update wide-text '\033[6;6H\347\225\214\347\225\214\347\225\214'
check_update overwrite-wide '\033[6;5H\347\225\214\033[6;6Hx'
check_update insert-mode '\033[6;1H\033[4habc\033[4l'
check_update sync '\033[?2026h\033[6;1Habcdefghijklmnopqrstuvwxyz\033[?2026l'
check_update sync-scroll '\033[?2026h\033[14;1Hone\r\ntwo\r\nthree\r\n\033[?2026l'
# Text on the right of the menu crosses the client viewport edge. A terminal
# fallback which redraws the entire pane line would overwrite the menu.
$TMUX2 resizew -x60 -y14 || exit 1
sleep 1
check_update viewport '\033[6;1Habcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwx'
$TMUX2 resizew -x40 -y14 || exit 1
# Window coordinates must still be used when the status line is above it.
$TMUX2 set -g status on || exit 1
$TMUX2 set -g status-position top || exit 1
$TMUX2 set -g status-format[0] '' || exit 1
$TMUX2 resizew -y13 || exit 1
sleep 1
check_update top-status '\033[6;1Habcdefghijklmnopqrstuvwxyz0123456789'
$TMUX2 set -g status off || exit 1
$TMUX2 resizew -y14 || exit 1
# Both the menu and floating panes obscure the base pane. The menu also
# obscures incremental output from the floating pane itself.
FLOAT=$($TMUX2 new-pane -dPF '#{pane_id}' -x24 -y8 -X4 -Y2 \
"exec cat '$TMP/float'") || exit 1
exec 4>"$TMP/float"
sleep 1
check_update behind-float '\033[6;1Habcdefghijklmnopqrstuvwxyz0123456789'
printf '\033[4;1HABCDEFGHIJKLMNOPQRSTUVWX' >&4
check_update float-text ''
printf '\033[4;1H\033[4hxyz\033[4l' >&4
check_update float-insert ''
printf '\033[8;1Hone\r\ntwo\r\nthree\r\n' >&4
check_update float-scroll ''
$TMUX2 kill-pane -t "$FLOAT" || exit 1
# Without a menu, the same whole-line and insert-mode paths must continue to
# respect floating panes. This also exercises split visible ranges.
$TMUX send Escape || exit 1
FLOAT=$($TMUX2 new-pane -dPF '#{pane_id}' -x12 -y6 -X6 -Y3 \
'exec sleep 100') || exit 1
$TMUX2 resizew -x60 -y14 || exit 1
sleep 1
check_update float-viewport '\033[6;1Habcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwx'
check_update float-insert-mode '\033[6;1H\033[4habc\033[4l'
$TMUX2 kill-pane -t "$FLOAT" || exit 1
$TMUX2 resizew -x40 -y14 || exit 1
# A menu covering an entire line leaves an empty visible range. Borderless
# menus and menus larger than the window must use their actual screen size.
menu -b none -x0 -y8
check_update borderless '\033[7;1Habcdefghijklmnopqrstuvwxyz0123456789'
$TMUX send Escape || exit 1
$TMUX2 display-menu -b simple -T live -C 0 -x0 -y8 \
'This menu is wider than the whole window' a '' beta b '' || exit 1
sleep 1
check_update full-width '\033[6;1Habcdefghijklmnopqrstuvwxyz0123456789'
# Nonvisual output is still delivered while a menu is open.
printf '\033]52;c;bWVudS1jbGlwYm9hcmQ=\007' >&3
sleep 1
[ "$($TMUX show-buffer 2>/dev/null)" = menu-clipboard ] ||
fail "menu blocked the clipboard update"
# The pane grid must keep updating under the menu, both for capture-pane and
# for the redraw when the menu closes.
printf '\033[2J\033[6;7Hlatest content' >&3
sleep 1
$TMUX2 capturep -p -t%0 >"$TMP/actual" || exit 1
grep -q 'latest content' "$TMP/actual" || fail "underlying pane is stale"
$TMUX send Escape || exit 1
sleep 1
$TMUX capturep -p >"$TMP/actual" || exit 1
grep -q 'latest content' "$TMP/actual" || fail "menu close left stale content"
exit 0

143
regress/new-pane-mouse-status.sh Executable file
View File

@@ -0,0 +1,143 @@
#!/bin/sh
# A Ctrl-drag starting on the status line must not create a floating pane.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Lmouse-status-inner-$$ -f/dev/null"
TMUX2="$TEST_TMUX -Lmouse-status-outer-$$ -f/dev/null"
cleanup()
{
$TMUX2 kill-server >/dev/null 2>&1
$TMUX kill-server >/dev/null 2>&1
}
trap cleanup 0 1 15
fail()
{
echo "$*" >&2
exit 1
}
wait_status()
{
i=0
while [ "$i" -lt 100 ]; do
screen=$($TMUX2 capture-pane -p -t outer:0.0) || exit 1
if printf '%s\n' "$screen" | grep -q 'WINDOW WINDOW WINDOW' &&
printf '%s\n' "$screen" | grep -q 'PANE PANE PANE PANE' &&
printf '%s\n' "$screen" | grep -q 'CONTROL CONTROL CONTROL'; then
return
fi
sleep 0.05
i=$((i + 1))
done
fail "status lines were not redrawn"
}
wait_option()
{
option=$1
i=0
while [ "$i" -lt 100 ]; do
got=$($TMUX show -gv "$option" 2>/dev/null)
[ "$got" = "$2" ] && return
sleep 0.05
i=$((i + 1))
done
fail "got $option '$got', expected '$2'"
}
mouse()
{
seq=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$TMUX2 send-keys -t outer:0.0 -l "$seq" || fail "send mouse failed"
sleep 0.2
}
check_drag()
{
start=$1
end=$2
sync=$3
if [ -n "$sync" ]; then
$TMUX set -g @drag-end '' || exit 1
fi
# First move along the status line, then into the pane. The first
# movement establishes the drag origin without dispatching new-pane.
mouse 16 10 "$start" M
mouse 48 11 "$start" M
mouse 48 16 "$end" M
mouse 48 18 "$end" M
mouse 16 18 "$end" m
if [ -n "$sync" ]; then
wait_option @drag-end done
fi
panes=$($TMUX list-panes -F '#{pane_id}') || fail "server exited"
[ "$panes" = "$BASE" ] || fail "status-line drag created a pane: $panes"
}
$TMUX new-session -d -s inner -x 60 -y 20 'sleep 100' || exit 1
$TMUX set -g mouse on || exit 1
$TMUX set -g default-command 'sleep 100' || exit 1
BASE=$($TMUX list-panes -F '#{pane_id}') || exit 1
$TMUX2 new-session -d -s outer -x 60 -y 20 'sleep 100' || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1
sleep 1
check_drag 20 15
$TMUX set -g status 2 || exit 1
sleep 0.5
check_drag 20 14
$TMUX set -g status-position top || exit 1
sleep 0.5
check_drag 2 8
$TMUX set -g status on || exit 1
sleep 0.5
check_drag 1 7
# Check drags starting on window, pane and control ranges.
$TMUX set -g status 3 || exit 1
$TMUX set -g 'status-format[0]' '#[range=window|0]WINDOW WINDOW WINDOW#[norange]' || exit 1
$TMUX set -g 'status-format[1]' "#[range=pane|$BASE]PANE PANE PANE PANE#[norange]" || exit 1
$TMUX set -g 'status-format[2]' '#[range=control|0]CONTROL CONTROL CONTROL#[norange]' || exit 1
$TMUX bind -n C-MouseDragEnd1Pane set -g @drag-end done || exit 1
wait_status
row=1
for range in window pane control; do
check_drag "$row" 9 sync
row=$((row + 1))
done
# A new drag inside the pane must still use the default Pane binding.
$TMUX set -g status on || exit 1
sleep 0.5
mouse 16 3 4 M
mouse 48 10 8 M
mouse 16 10 8 m
panes=$($TMUX list-panes -F '#{pane_floating_flag}') || fail "server exited"
[ "$panes" = "$(printf '0\n1')" ] || fail "pane drag did not create a pane"
# Starting on the status line must not dispatch Empty bindings either.
id=$($TMUX list-panes -F '#{pane_id}' | tail -n 1)
$TMUX kill-pane -t "$id" || exit 1
$TMUX break-pane -W -s "$BASE" || exit 1
$TMUX resize-pane -t "$BASE" -x 10 -y 4 || exit 1
$TMUX move-pane -t "$BASE" -P top-left || exit 1
sleep 0.5
check_drag 1 10
exit 0

View File

@@ -131,6 +131,14 @@ printf '%s\n' "$captured" | grep -Fq 'show-buffer' ||
printf '%s\n' "$captured" | grep -Fq 'show-environment' ||
fail "ambiguous completion list was incomplete"
$OUTER send-keys Escape || exit 1
# Wait until Escape has closed the prompt before sending another Meta key.
# Otherwise the two can be parsed together as a single escape sequence.
i=0
while capture | grep -Fq '(word)'; do
[ "$i" -lt 50 ] || fail "completion prompt did not close"
sleep 0.1
i=$((i + 1))
done
# Add entries to both history rings through real prompts.
bind_prompt 'history-command'

View File

@@ -0,0 +1,113 @@
#!/bin/sh
# Many wrapped rows crossing a panned viewport produce disjoint damage.
# Every row must survive the rectangle-count limit and subsequent merging.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Laccumulate-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Laccumulate-outer-$$ -f/dev/null"
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
fail()
{
echo "$*" >&2
exit 1
}
wait_marker()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$DIR/capture" || exit 1
grep -q "$1" "$DIR/capture" && return 0
sleep 0.1
i=$((i + 1))
done
fail "client did not receive $1"
}
cat >"$DIR/emitter.pl" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $row (1 .. 80) {
printf "\e[%d;1HROW%02d-%s", $row, $row, 'abcdefghij' x 6;
}
print "\e[1;1H";
while (!-e $ENV{TRIGGER}) {
select undef, undef, undef, 0.01;
}
my $output = '';
# Each wrapped second row needs a separate damage rectangle. Keep the
# batch below the PTY read size so all 18 arrive in the same input pass.
for my $region (0 .. 17) {
my $top = 1 + $region * 4;
$output .= "\e[$top;1H" . ('A' x 80) . ('B' x 24) .
sprintf('DAMAGE%02d', $region) . ('B' x 48);
}
$output .= "\e[80;21HDONE";
syswrite STDOUT, $output;
while (!-e "$ENV{TRIGGER}-merge") {
select undef, undef, undef, 0.01;
}
# Returning from the alternate screen requests a full-pane rectangle. A
# later wrapped-row rectangle overlaps it and must not shrink that damage.
$output = "\e[?1049h\e[40;21HALT-VISIBLE\e[?1049l";
$output .= "\e[1;1H" . ('C' x 80) . ('D' x 24) . 'MERGE00' . ('D' x 49);
$output .= "\e[80;21HMERGED";
syswrite STDOUT, $output;
sleep 100;
PERL
$INNER new-session -d -s inner -x 80 -y 80 \
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -as terminal-features ',screen:sync' || exit 1
$INNER set -g pane-border-lines simple || exit 1
$OUTER new-session -d -s outer -x 40 -y 80 'sleep 100' || exit 1
$OUTER set -g status off || exit 1
$OUTER set -g window-size manual || exit 1
$OUTER set -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 "$INNER attach -t inner" || exit 1
wait_marker ROW80
CLIENT=$($OUTER display -p -t outer:0.0 '#{pane_tty}') || exit 1
$INNER refresh-client -t "$CLIENT" -R 20 || exit 1
# Keep the emitter inactive so its writes use a synchronized frame. Otherwise
# its own queued output can force a full redraw and hide lost rectangles.
$INNER new-pane -x 6 -y 3 -X 65 -Y 74 'sleep 100' || exit 1
sleep 0.2
: >"$DIR/trigger"
wait_marker DONE
sleep 0.2
$OUTER capture-pane -p -t outer:0.0 >"$DIR/before" || exit 1
region=0
while [ "$region" -lt 18 ]; do
marker=$(printf 'DAMAGE%02d' "$region")
sed -n "$((2 + region * 4))p" "$DIR/before" | grep -q "$marker" ||
fail "missing damage for region $region"
region=$((region + 1))
done
$INNER refresh-client -t "$CLIENT" || exit 1
sleep 0.2
$OUTER capture-pane -p -t outer:0.0 >"$DIR/after" || exit 1
diff -u "$DIR/before" "$DIR/after" || fail "accumulated redraw differs from full redraw"
: >"$DIR/trigger-merge"
wait_marker MERGED
sleep 0.2
$OUTER capture-pane -p -t outer:0.0 >"$DIR/before" || exit 1
sed -n '2p' "$DIR/before" | grep -q MERGE00 || fail "merged damage lost the wrapped row"
$INNER refresh-client -t "$CLIENT" || exit 1
sleep 0.2
$OUTER capture-pane -p -t outer:0.0 >"$DIR/after" || exit 1
diff -u "$DIR/before" "$DIR/after" || fail "merged redraw differs from full redraw"
exit 0

View File

@@ -0,0 +1,140 @@
#!/bin/sh
# One terminal stops reading while another consumes floating-pane damage.
# The slow client must catch up after the window's shared damage is cleared.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lblocked-inner-$$ -f/dev/null"
FAST="$TEST_TMUX -Lblocked-fast-$$ -f/dev/null"
SLOW="$TEST_TMUX -Lblocked-slow-$$ -f/dev/null"
STOPPED=
cleanup()
{
[ -n "$STOPPED" ] && kill -CONT "$STOPPED" 2>/dev/null
$FAST kill-server 2>/dev/null
$SLOW kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
fail()
{
echo "$*" >&2
exit 1
}
wait_marker()
{
terminal=$1
marker=$2
i=0
while [ "$i" -lt 100 ]; do
$terminal capture-pane -p >"$DIR/capture" || exit 1
grep -q "$marker" "$DIR/capture" && return 0
sleep 0.1
i=$((i + 1))
done
fail "client did not receive $marker"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$FAST send-keys -l "$sequence" || exit 1
sleep 0.2
}
cat >"$DIR/emitter.pl" <<'PERL'
use strict;
use warnings;
$| = 1;
print 'READY';
while (!-e "$ENV{TRIGGER}-flood") {
select undef, undef, undef, 0.01;
}
my $frame = 0;
while (!-e "$ENV{TRIGGER}-stop") {
my $output = '';
for my $row (1 .. 20) {
$output .= "\e[$row;1H" . join('', map { chr(33 + ($_ + $frame) % 80) } 0 .. 77);
}
print $output;
$frame++;
select undef, undef, undef, 0.01;
}
for my $row (1 .. 20) {
printf "\e[%d;1HFINAL%02d-%s", $row, $row, '0123456789' x 7;
}
print "\e[20;1HDONE";
sleep 100;
PERL
$INNER new-session -d -s inner -x 240 -y 120 \
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -g mouse on || exit 1
$INNER set -g pane-border-lines simple || exit 1
FLOAT=$($INNER new-pane -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5 \
'printf FLOAT; exec sleep 100') || exit 1
for terminal in "$FAST" "$SLOW"; do
$terminal new-session -d -x 240 -y 120 'sleep 100' || exit 1
$terminal set -g status off || exit 1
$terminal set -g window-size manual || exit 1
$terminal set -g default-terminal screen || exit 1
$terminal respawn-pane -k "$INNER attach -t inner" || exit 1
wait_marker "$terminal" READY
done
FASTCLIENT=$($FAST display -p '#{pane_tty}') || exit 1
SLOWCLIENT=$($SLOW display -p '#{pane_tty}') || exit 1
SLOWPID=$($SLOW display -p '#{pid}') || exit 1
before=$($INNER display -p -c "$FASTCLIENT" '#{client_written}') || exit 1
slowbefore=$($INNER display -p -c "$SLOWCLIENT" '#{client_written}') || exit 1
# The large terminal keeps this backlog below the discard threshold, so
# automatic recovery from discarded output cannot mask lost damage.
# Stop only our outer server, leaving its inner client attached to a PTY
# whose master is no longer read. This creates real terminal backpressure.
STOPPED=$SLOWPID
kill -STOP "$SLOWPID" || exit 1
: >"$DIR/trigger-flood"
i=0
while :; do
written=$($INNER display -p -c "$SLOWCLIENT" '#{client_written}') || exit 1
fastwritten=$($INNER display -p -c "$FASTCLIENT" '#{client_written}') || exit 1
# More than a PTY can buffer has been queued for the stopped terminal,
# while the other terminal is still receiving the same output.
[ "$written" -gt "$((slowbefore + 65536))" ] &&
[ "$fastwritten" -gt "$((before + 65536))" ] && break
[ "$i" -lt 100 ] || fail "slow client did not become blocked"
sleep 0.1
i=$((i + 1))
done
: >"$DIR/trigger-stop"
wait_marker "$FAST" DONE
[ "$($INNER display -p -c "$FASTCLIENT" '#{client_discarded}')" -eq 0 ] ||
fail "fast client also became blocked"
mouse 0 12 6 M
mouse 32 42 6 M
mouse 0 42 6 m
[ "$($INNER display -p -t "$FLOAT" '#{pane_left}')" -eq 36 ] || fail "pane did not move"
$FAST capture-pane -p >"$DIR/fast-before" || exit 1
# Ensure the fast client already restored the vacated frame.
sed -n '6p' "$DIR/fast-before" | grep -q '^FINAL06-0123456789' ||
fail "fast client did not restore old footprint"
kill -CONT "$SLOWPID" || exit 1
STOPPED=
wait_marker "$SLOW" DONE
# Allow queued terminal output and deferred redraws to drain.
sleep 0.5
[ "$($INNER display -p -c "$SLOWCLIENT" '#{client_discarded}')" -eq 0 ] ||
fail "discard recovery could mask lost deferred damage"
$SLOW capture-pane -p >"$DIR/slow-before" || exit 1
diff -u "$DIR/fast-before" "$DIR/slow-before" || fail "slow client did not catch up"
$INNER refresh-client -t "$FASTCLIENT" || exit 1
sleep 0.2
$FAST capture-pane -p >"$DIR/after" || exit 1
diff -u "$DIR/fast-before" "$DIR/after" || fail "damage redraw differed from full redraw"
exit 0

112
regress/redraw-damage-only.sh Executable file
View File

@@ -0,0 +1,112 @@
#!/bin/sh
# Check a redraw callback which has no accompanying client redraw flags. A
# wrapped row crossing a panned viewport cannot use the direct tty path.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Ldamage-only-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Ldamage-only-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
TRIGGER=$DIR/trigger
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_inner_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t inner:0.0 2>/dev/null |
grep -q "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner pane did not contain $marker"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $row (1 .. 12) {
print "\e[$row;1H", 'o' x 79;
}
print "\e[1;1H";
while (!-e $ENV{TRIGGER}) {
select undef, undef, undef, 0.01;
}
my $second = ('B' x 24) . 'DAMAGE-ONLY' . ('B' x 45);
print "\e[5;1H", ('A' x 80), $second;
sleep 100;
PERL
$INNER new-session -d -s inner -x 80 -y 12 \
"TRIGGER='$TRIGGER' perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x 40 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Ldamage-only-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
$INNER refresh-client -t "$CLIENT" -R 20 || exit 1
wait_outer_has oooooooooo
: >"$TRIGGER"
wait_inner_has DAMAGE-ONLY
wait_outer_has DAMAGE-ONLY
exit 0

View File

@@ -0,0 +1,112 @@
#!/bin/sh
# Moving and interactively resizing floating panes must restore both clients,
# including a smaller client panned horizontally and vertically.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lviewports-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lviewports-outer-$$ -f/dev/null"
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
fail()
{
echo "$*" >&2
exit 1
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t wide:0.0 -l "$sequence" || exit 1
sleep 0.2
}
assert_scene()
{
# Compare what each terminal actually received with a fresh full redraw.
# Capture both first: refreshing one client must not repair the other.
for target in wide small; do
$OUTER capture-pane -p -t "$target:0.0" >"$DIR/$target-before" || exit 1
done
$INNER refresh-client -t "$WIDE" || exit 1
$INNER refresh-client -t "$SMALL" || exit 1
sleep 0.2
for target in wide small; do
$OUTER capture-pane -p -t "$target:0.0" >"$DIR/$target-after" || exit 1
diff -u "$DIR/$target-before" "$DIR/$target-after" ||
fail "$1: $target client differed from a full redraw"
done
}
cat >"$DIR/background.pl" <<'PERL'
$| = 1;
for my $row (1 .. 24) {
printf "\e[%d;1HROW%02d-", $row, $row;
print '0123456789' x 7;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 80 -y 24 "perl '$DIR/background.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -g mouse on || exit 1
$INNER set -g default-command 'sleep 100' || exit 1
$INNER set -g pane-border-lines simple || exit 1
$OUTER new-session -d -s wide -x 80 -y 24 'sleep 100' || exit 1
$OUTER set -g status off || exit 1
$OUTER set -g window-size manual || exit 1
$OUTER set -g default-terminal screen || exit 1
$OUTER new-session -d -s small -x 40 -y 12 'sleep 100' || exit 1
for target in wide small; do
$OUTER respawn-pane -k -t "$target:0.0" "$INNER attach -t inner" || exit 1
done
i=0
while [ "$($INNER list-clients | wc -l)" -ne 2 ]; do
[ "$i" -lt 50 ] || fail "two clients did not attach"
sleep 0.1
i=$((i + 1))
done
WIDE=$($OUTER display -p -t wide:0.0 '#{pane_tty}') || exit 1
SMALL=$($OUTER display -p -t small:0.0 '#{pane_tty}') || exit 1
$INNER refresh-client -t "$SMALL" -R 20 || exit 1
$INNER refresh-client -t "$SMALL" -D 6 || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 16 -y 6 -X 25 -Y 9) || exit 1
sleep 0.3
assert_scene initial
# Confirm that the two clients really have different viewports.
[ "$(head -1 "$DIR/wide-before" | cut -c1-6)" = ROW01- ] || fail "wrong wide viewport"
[ "$(head -1 "$DIR/small-before" | cut -c1-6)" != ROW01- ] || fail "small client was not panned"
# Meta-drag the body using move-pane -M, including a partially clipped position.
mouse 8 30 12 M
mouse 40 48 15 M
[ "$($INNER display -p -t "$FLOAT" '#{pane_left}')" -eq 44 ] || fail "Meta-drag did not move pane"
assert_scene move-right
mouse 40 28 10 M
assert_scene move-back
mouse 8 28 10 m
# Ctrl-drag creates a new floating pane, then changes its size while held.
mouse 16 52 14 M
mouse 48 75 23 M
NEW=$($INNER display -p '#{pane_id}') || exit 1
[ "$NEW" != "$FLOAT" ] || fail "Ctrl-drag did not create a pane"
[ "$($INNER display -p -t "$NEW" '#{pane_floating_flag}')" -eq 1 ] || fail "new pane is not floating"
assert_scene create
mouse 48 64 19 M
[ "$($INNER display -p -t "$NEW" '#{pane_width}')" -eq 11 ] || fail "Ctrl-drag did not shrink pane"
assert_scene shrink
mouse 16 64 19 m
assert_scene release
exit 0

186
regress/redraw-multiclient.sh Executable file
View File

@@ -0,0 +1,186 @@
#!/bin/sh
# Redraw a moved floating pane on both attached clients viewing the same
# window. Window redraw work must not be consumed by only one client.
#
# Uses ASCII pane borders (rather than the default UTF-8 box-drawing) because
# this test nests a real tmux client inside another tmux's pane to get a
# genuine terminal to capture from; that nested-tmux relay has been observed
# to mis-render a cell that previously held a multi-byte UTF-8 border
# character being overwritten later by plain content, on the outer instance's
# own interpretation, independent of anything the inner tmux sends. That is a
# nested-test-harness artifact, not a real tmux bug - confirmed by replaying
# the exact same drag sequence against a real terminal (xterm), where it
# never reproduces. ASCII borders avoid the artifact entirely.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lredraw-multi-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_clients()
{
i=0
while [ "$i" -lt 50 ]; do
count=$($INNER list-clients 2>/dev/null | wc -l)
[ "$count" -eq 2 ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "two inner clients did not attach"
}
wait_outer_has()
{
target=$1
marker=$2
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t "$target" >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer pane $target did not show $marker"
}
wait_float_left()
{
comparison=$1
limit=$2
i=0
while [ "$i" -lt 50 ]; do
left=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
if [ "$comparison" = gt ] && [ "$left" -gt "$limit" ]; then
return 0
fi
if [ "$comparison" = lt ] && [ "$left" -lt "$limit" ]; then
return 0
fi
sleep 0.1
i=$((i + 1))
done
fail "floating pane did not move"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$2" "$3" "$4" "$5")
$OUTER send-keys -t "$1" -l "$sequence" || exit 1
sleep 0.1
}
drag_float()
{
target=$1
startcol=$2
startrow=$3
endcol=$4
mouse "$target" 0 "$startcol" "$startrow" M
mouse "$target" 32 "$endcol" "$startrow" M
mouse "$target" 0 "$endcol" "$startrow" m
}
assert_scene()
{
target=$1
base=$2
firstcol=$3
lastcol=$4
$OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1
# With ASCII (simple) borders every corner and junction is the same
# '+', so one rectangular floating pane always draws exactly 4 of
# them; more means a stale frame was left behind somewhere.
corners=$(grep -o '+' "$CAPTURE" | wc -l)
[ "$corners" -eq 4 ] ||
fail "outer pane $target had $corners floating frames"
sed -n '6,11p' "$base" | cut -c"$firstcol-$lastcol" >"$DIR/want"
sed -n '6,11p' "$CAPTURE" | cut -c"$firstcol-$lastcol" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" ||
fail "outer pane $target did not restore the old floating area"
}
C="sh -c 'i=0; while [ \$i -lt 20 ]; do printf \"\\033[%d;1HBG-ROW-%02d-abcdefghijklmnopqrstuvwxyz0123456789\" \$((i + 1)) \$i; i=\$((i + 1)); done; exec sleep 100'"
$INNER new-session -d -s inner -x 60 -y 20 "$C" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g default-command 'sleep 100' || exit 1
$INNER set-option -g pane-border-lines simple || exit 1
$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER split-window -h -t outer:0.0 'sleep 100' || exit 1
PANES=$($OUTER list-panes -t outer:0 -F '#{pane_id} #{pane_left}')
LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1)
RIGHT=$(echo "$PANES" | sort -k2 -n | tail -1 | cut -d' ' -f1)
[ -n "$LEFT" ] && [ -n "$RIGHT" ] || fail "could not find outer panes"
$OUTER respawn-pane -k -t "$LEFT" \
"$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
$OUTER respawn-pane -k -t "$RIGHT" \
"$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_clients
wait_outer_has "$LEFT" BG-ROW-19
wait_outer_has "$RIGHT" BG-ROW-19
$OUTER capture-pane -p -t "$LEFT" >"$DIR/base-left" || exit 1
$OUTER capture-pane -p -t "$RIGHT" >"$DIR/base-right" || exit 1
FLOAT=$($INNER new-pane -dPF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) ||
fail "could not create floating pane"
wait_outer_has "$LEFT" '+'
wait_outer_has "$RIGHT" '+'
FTOP=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}')
GRABCOL=$((FLEFT + FWIDTH / 2 + 1))
# Move right through one client and require both clients to restore the old
# left-hand footprint.
drag_float "$LEFT" "$GRABCOL" "$FTOP" $((GRABCOL + 30))
wait_float_left gt 30
assert_scene "$LEFT" "$DIR/base-left" 1 20
assert_scene "$RIGHT" "$DIR/base-right" 1 20
# Move back through the other client and check the old right-hand footprint.
FLEFT=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
GRABCOL=$((FLEFT + FWIDTH / 2 + 1))
drag_float "$RIGHT" "$GRABCOL" "$FTOP" $((GRABCOL - 30))
wait_float_left lt 10
assert_scene "$LEFT" "$DIR/base-left" 35 60
assert_scene "$RIGHT" "$DIR/base-right" 35 60
exit 0

208
regress/redraw-screen-write.sh Executable file
View File

@@ -0,0 +1,208 @@
#!/bin/sh
# Check that full and region screen-write fallbacks update an attached client,
# not only tmux's internal pane grid.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
EMITTER=$DIR/emitter.pl
CAPTURE=$DIR/capture
INNER=
OUTER=
N=0
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
[ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null
[ -n "$INNER" ] && $INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_outer_lacks()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" || return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client still showed $marker"
}
wait_inner_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t inner:0.0 2>/dev/null |
grep -q "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner pane did not contain $marker"
}
wait_inner_lacks()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t inner:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" || return 0
sleep 0.1
i=$((i + 1))
done
fail "inner pane still contained $marker"
}
setup()
{
mode=$1
[ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null
[ -n "$INNER" ] && $INNER kill-server 2>/dev/null
N=$((N + 1))
INNER="$TEST_TMUX -Lredraw-write-inner-$$-$N -f/dev/null"
OUTER="$TEST_TMUX -Lredraw-write-outer-$$-$N -f/dev/null"
$INNER new-session -d -s inner -x 40 -y 12 \
"MODE=$mode READY='$DIR/ready-$N' TRIGGER='$DIR/trigger-$N' perl '$EMITTER'" ||
exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x 40 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lredraw-write-inner-$$-$N -f/dev/null attach-session -t inner" ||
exit 1
}
trigger()
{
: >"$DIR/trigger-$N-${1:-1}"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
my $mode = $ENV{MODE};
my $ready = $ENV{READY};
my $trigger = $ENV{TRIGGER};
sub fill_screen {
my ($prefix) = @_;
print "\e[2J\e[H";
for my $row (0 .. 11) {
printf "\e[%d;1H%s-ROW-%02d", $row + 1, $prefix, $row;
}
}
if ($mode eq 'ris') {
fill_screen('RIS');
} elsif ($mode eq 'alternate') {
fill_screen('BASE');
} elsif ($mode eq 'scroll') {
fill_screen('SCROLL');
} else {
die "unknown mode $mode\n";
}
open my $fh, '>', $ready or die "$ready: $!\n";
close $fh;
while (!-e "$trigger-1") {
select undef, undef, undef, 0.01;
}
if ($mode eq 'ris') {
print "\ec";
} elsif ($mode eq 'alternate') {
print "\e[?1049h";
fill_screen('ALT');
while (!-e "$trigger-2") {
select undef, undef, undef, 0.01;
}
print "\e[?1049l";
} else {
print "\e[12;1H\r\nSCROLL-NEW";
}
sleep 100;
PERL
# RIS clears the complete screen. The source pane and attached client must both
# lose every old row.
setup ris
wait_outer_has RIS-ROW-11
trigger
wait_inner_lacks RIS-ROW
wait_outer_lacks RIS-ROW
# Leaving the alternate screen restores every row of the base screen.
setup alternate
wait_outer_has BASE-ROW-11
trigger
wait_inner_has ALT-ROW-11
wait_outer_has ALT-ROW-11
wait_outer_lacks BASE-ROW
trigger 2
wait_inner_has BASE-ROW-11
wait_outer_has BASE-ROW-11
wait_outer_lacks ALT-ROW
# Scrolling a pane which is narrower than the terminal redraws its complete
# region. Check the physical client row by row after the source grid shifts.
setup scroll
$INNER split-window -h -t inner:0 'sleep 100' || exit 1
wait_outer_has SCROLL-ROW-11
trigger
wait_inner_has SCROLL-NEW
wait_outer_has SCROLL-NEW
row=1
while [ "$row" -le 11 ]; do
expected=$(printf 'SCROLL-ROW-%02d' "$row")
actual=$(sed -n "${row}p" "$CAPTURE")
case "$actual" in
"$expected"*) ;;
*) fail "outer row $row was not redrawn as $expected" ;;
esac
row=$((row + 1))
done
actual=$(sed -n '12p' "$CAPTURE")
case "$actual" in
SCROLL-NEW*) ;;
*) fail "outer bottom row was not redrawn as SCROLL-NEW" ;;
esac
exit 0

View File

@@ -0,0 +1,29 @@
#!/bin/sh
# respawn-pane -k -E leaves an empty pane, which must not be treated as exited
# when the process it replaced is reaped
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
sleep 1
# The only pane of the only session: the server must not exit.
$TMUX new -d 'sleep 1000' || exit 1
$TMUX respawn-pane -k -E || exit 1
sleep 1
$TMUX has-session -t0 2>/dev/null || exit 1
$TMUX kill-server 2>/dev/null
sleep 1
# With remain-on-exit the pane must not be dead or keep the old tty.
$TMUX new -d 'sleep 1000' \; set -g remain-on-exit on || exit 1
$TMUX respawn-pane -k -E || exit 1
sleep 1
[ "$($TMUX display -p '#{pane_dead}:#{pane_tty}')" = "0:" ] || exit 1
$TMUX kill-server 2>/dev/null
exit 0

41
regress/screen-redraw-floating.sh Normal file → Executable file
View File

@@ -30,21 +30,47 @@ fail() {
}
compare() {
sleep 1
$TMUX capturep -p $2 >$TMP || exit 1
if [ -n "$GENERATE" ]; then
sleep 1
$TMUX capturep -p $2 >$TMP || exit 1
cp $TMP "$RESULTS/$1.result" || exit 1
echo "generated $1"
else
cmp -s $TMP "$RESULTS/$1.result" || \
fail "scene $1 differs from $RESULTS/$1.result"
return
fi
tries=0
while [ "$tries" -lt 100 ]; do
$TMUX capturep -p $2 >$TMP || exit 1
cmp -s $TMP "$RESULTS/$1.result" && return
sleep 0.1
tries=$((tries + 1))
done
fail "scene $1 differs from $RESULTS/$1.result"
}
wait_for_pids() {
for pid in $1; do
tries=0
while kill -0 "$pid" 2>/dev/null; do
tries=$((tries + 1))
[ "$tries" -lt 100 ] || \
fail "timed out waiting for pane $pid to exit"
sleep 0.05
done
done
}
# new_scene <width> <height>: fresh inner window of the given window size.
new_scene() {
$TMUX2 neww -d "sh -c 'printf base; exec sleep 100'" || exit 1
$TMUX2 selectw -t:\$ || exit 1
if [ -n "$window" ]; then
pids=$($TMUX2 list-panes -t"$window" -F '#{pane_pid}') || exit 1
$TMUX2 killw -t"$window" || exit 1
wait_for_pids "$pids"
fi
window=$($TMUX2 neww -dP -F '#{window_id}' \
"sh -c 'printf base; exec sleep 100'") || exit 1
$TMUX2 selectw -t"$window" || exit 1
$TMUX2 resizew -x$1 -y$2 || exit 1
}
@@ -74,6 +100,7 @@ $TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new -d -x40 -y12 "sh -c 'printf base; exec sleep 100'" || exit 1
window=
$TMUX2 set -g status off || exit 1
$TMUX2 set -g window-size manual || exit 1
$TMUX2 set -g pane-border-format " #{pane_title} " || exit 1

View File

@@ -0,0 +1,40 @@
#!/bin/sh
# swap-pane refusing a floating pane must leave the zoom alone
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)
trap "rm -f $TMP; $TMUX kill-server 2>/dev/null" 0 1 15
$TMUX new -d
$TMUX splitw -d
$TMUX new-pane -d
echo $($TMUX lsp -F'#{pane_id}:#{pane_floating_flag}') >$TMP
(echo "%0:0 %1:0 %2:1"|cmp -s - $TMP) || exit 1
$TMUX resize-pane -Z -t %0
[ "$($TMUX display -p '#{window_zoomed_flag}')" = 1 ] || exit 1
$TMUX swap-pane -Z -D -t %2
[ "$?" = 0 ] && exit 1
[ "$($TMUX display -p '#{window_zoomed_flag}')" = 1 ] || exit 1
$TMUX swap-pane -Z -U -t %2
[ "$?" = 0 ] && exit 1
[ "$($TMUX display -p '#{window_zoomed_flag}')" = 1 ] || exit 1
# A swap that is allowed still happens.
$TMUX swap-pane -D -t %0 || exit 1
echo $($TMUX lsp -F'#{pane_id}') >$TMP
(echo "%1 %0 %2"|cmp -s - $TMP) || exit 1
$TMUX kill-server 2>/dev/null
exit 0

81
regress/switch-client-redraw.sh Executable file
View File

@@ -0,0 +1,81 @@
#!/bin/sh
# Switching windows in the same session must redraw the attached client. The
# session object is shared, so its current winlink cannot be compared after it
# has already been changed.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lswitch-redraw-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lswitch-redraw-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
$INNER new-session -d -s inner -x 40 -y 8 \
"printf '\033[2J\033[HA-WINDOW'; exec sleep 100" || exit 1
$INNER new-window -d -t inner:1 \
"printf '\033[2J\033[HB-WINDOW'; exec sleep 100" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x 40 -y 8 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lswitch-redraw-inner-$$ -f/dev/null attach-session -t inner:0" ||
exit 1
wait_for_client
wait_outer_has A-WINDOW
$INNER switch-client -c "$CLIENT" -t inner:1.0 || exit 1
[ "$($INNER display-message -p -t inner '#{window_index}')" -eq 1 ] ||
fail "server did not select window 1"
wait_outer_has B-WINDOW
exit 0

View File

@@ -63,6 +63,22 @@ wait_count()
fail "found $count '$marker' rows, expected $want"
}
wait_first()
{
marker=$1
want=$2
i=0
while [ "$i" -lt 50 ]; do
got=$(capture | grep -F "$marker" | head -1)
case "$got" in
"$want"*) return 0 ;;
esac
sleep 0.1
i=$((i + 1))
done
fail "first '$marker' row is '$got', expected prefix '$want'"
}
wait_format()
{
target=$1
@@ -150,6 +166,22 @@ case "$picked" in
*) fail "window selection produced '$picked', expected '=bravo:0.'" ;;
esac
# -O selects the sort order and -r reverses it. By name the aaa window is
# first, by index main is, and by name reversed pick-two is.
$INNER new-window -d -t alpha: -n aaa 'exec sleep 100' || exit 1
$INNER switch-mode -w -t alpha:main.0 -F 'ORDER #{window_name}' -O name || exit 1
wait_first 'ORDER ' 'ORDER aaa'
$INNER send-keys -t alpha:main.0 Escape || exit 1
wait_format alpha:main.0 '#{pane_in_mode}' 0
$INNER switch-mode -w -t alpha:main.0 -F 'ORDER #{window_name}' -O index || exit 1
wait_first 'ORDER ' 'ORDER last'
$INNER send-keys -t alpha:main.0 Escape || exit 1
wait_format alpha:main.0 '#{pane_in_mode}' 0
$INNER switch-mode -w -t alpha:main.0 -F 'ORDER #{window_name}' -O name -r || exit 1
wait_first 'ORDER ' 'ORDER pick-two'
$INNER send-keys -t alpha:main.0 Escape || exit 1
wait_format alpha:main.0 '#{pane_in_mode}' 0
# Navigation with an empty result set must be harmless, and Escape cancels
# without running the custom command.
$INNER set-option -g @picked unchanged || exit 1

View File

@@ -0,0 +1,127 @@
#!/bin/sh
# Clearing an area of a pane that is only partly visible in a client smaller
# than the window must account for status lines at the top. The area is
# clamped in window coordinates but drawn in terminal coordinates, which are
# shifted down by the status lines.
#
# Phase 1: the viewport is at the top, so the bottom of the cleared area is
# hidden; the last rows on the terminal must still be cleared.
# Phase 2: the viewport is at the bottom, so the top of the cleared area is
# hidden; this used to fail with "tty_clamp_area: y too big".
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
count=$($INNER list-clients 2>/dev/null | wc -l)
[ "$count" -eq 1 ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_for_marker()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER has-session 2>/dev/null || fail "inner server exited"
$OUTER capture-pane -p -t outer >"$CAPTURE" || exit 1
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "client did not receive $marker"
}
# Fill all 20 rows, then clear from the top and print a marker on a row that
# is visible in the current viewport.
cat >"$DIR/emitter.pl" <<'PERL'
use strict;
use warnings;
sub wait_for {
my ($file) = @_;
select undef, undef, undef, 0.01 while !-e $file;
}
$| = 1;
for my $phase (1 .. 2) {
wait_for("$ENV{TRIGGER}-fill-$phase");
print "\e[H", join("\r\n", ("FILL") x 20);
wait_for("$ENV{TRIGGER}-clear-$phase");
my $row = $phase == 1 ? 1 : 20;
print "\e[H\e[J\e[$row;1HCLEARED$phase";
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 20 \
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g status 2 || exit 1
$INNER set -g status-position top || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -g automatic-rename off || exit 1
# The client is as large as the window, so with two status lines only 18
# window rows are visible.
$OUTER new-session -d -s outer -x 40 -y 20 'sleep 100' || exit 1
$OUTER set -g status off || exit 1
$OUTER set -g window-size manual || exit 1
$OUTER set -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer "$INNER attach-session -t inner" || exit 1
wait_for_client
NAME=$($OUTER display-message -p -t outer '#{pane_tty}') || exit 1
for phase in 1 2; do
if [ "$phase" -eq 1 ]; then
PAN=-U
else
PAN=-D
fi
: >"$DIR/trigger-fill-$phase"
sleep 0.2
$INNER refresh-client -t "$NAME" $PAN 100 || exit 1
sleep 0.5
: >"$DIR/trigger-clear-$phase"
wait_for_marker "CLEARED$phase"
sleep 0.2
$INNER has-session 2>/dev/null || fail "inner server exited"
$OUTER capture-pane -p -t outer >"$CAPTURE" || exit 1
if grep -q FILL "$CAPTURE"; then
cat "$CAPTURE" >&2
fail "phase $phase left rows uncleared"
fi
done
exit 0

View File

@@ -0,0 +1,97 @@
#!/bin/sh
# An Escape followed by an extended key must remain a separate key. The
# sequence's leading Escape must not be consumed as the second half of
# M-Escape.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 15
$INNER new-session -d -s inner -x80 -y10 "stty raw -echo; exec cat -v" ||
exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g assume-paste-time 0 || exit 1
$INNER set-option -s escape-time 1000 || exit 1
$INNER set-option -s extended-keys on || exit 1
$INNER unbind-key -a -T root || exit 1
$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1
$OUTER set-option -g assume-paste-time 0 || exit 1
i=0
while [ -z "$($INNER list-clients -F '#{client_name}')" ]; do
[ "$i" -lt 100 ] || fail 'inner client did not attach'
sleep 0.05
i=$((i + 1))
done
bind_key()
{
$INNER bind-key -n "$1" send-keys -l "$2" || exit 1
}
bind_key Escape E
bind_key M-Escape A
bind_key C-a C
bind_key M-C-a M
bind_key M-Up U
bind_key M-DC D
expected_output=
send_raw()
{
# Hex arguments avoid command separators in a partial sequence.
bytes=$(printf '%s' "$1" | od -An -v -tx1)
$OUTER send-keys -H $bytes || exit 1
}
assert_events()
{
name=$1
expected_output=$expected_output$2
send_raw "$3"
if [ "$#" -eq 4 ]; then
sleep 0.05
send_raw "$4"
fi
i=0
while [ "$i" -lt 100 ]; do
actual=$($INNER capture-pane -p | tr -d '\n')
[ "$actual" = "$expected_output" ] && return 0
sleep 0.05
i=$((i + 1))
done
fail "$name: got '$actual', expected '$expected_output'"
}
assert_events 'CSI u' C "$(printf '\033[97;5u')"
assert_events 'Escape and CSI u' EC "$(printf '\033\033[97;5u')"
assert_events 'Escape and xterm' EC "$(printf '\033\033[27;5;97~')"
assert_events 'Escape and CSI u with Meta' EM "$(printf '\033\033[97;7u')"
assert_events 'split CSI u' EC "$(printf '\033\033[97;')" '5u'
assert_events 'split xterm' EC "$(printf '\033\033[27;5')" ';97~'
assert_events 'CSI u split after two Escapes' EC "$(printf '\033\033')" '[97;5u'
assert_events 'xterm split after two Escapes' EC "$(printf '\033\033')" '[27;5;97~'
assert_events 'M-Escape' A "$(printf '\033\033')"
assert_events 'M-Up' U "$(printf '\033\033[A')"
assert_events 'M-DC' D "$(printf '\033\033[3~')"
exit 0

View File

@@ -0,0 +1,143 @@
#!/bin/sh
# An Escape followed by a focus report must remain a separate key. The
# report's leading Escape must not be consumed as the second half of M-Escape,
# and the focus change must still reach the client and the pane.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 15
$INNER new-session -d -s inner -x80 -y10 \
"printf '\033[?1004h'; stty raw -echo; exec cat -v" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g assume-paste-time 0 || exit 1
$INNER set-option -g focus-events on || exit 1
$INNER set-option -s escape-time 1000 || exit 1
$INNER unbind-key -a -T root || exit 1
$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1
$OUTER set-option -g assume-paste-time 0 || exit 1
i=0
while [ -z "$($INNER list-clients -F '#{client_name}')" ]; do
[ "$i" -lt 100 ] || fail 'inner client did not attach'
sleep 0.05
i=$((i + 1))
done
bind_key()
{
$INNER bind-key -n "$1" send-keys -l "$2" || exit 1
}
bind_key Escape E
bind_key M-Escape A
bind_key M-Up U
send_raw()
{
# Hex arguments avoid command separators in a partial sequence.
bytes=$(printf '%s' "$1" | od -An -v -tx1)
$OUTER send-keys -H $bytes || exit 1
}
capture()
{
$INNER capture-pane -p | tr -d '\n'
}
focused()
{
case $($INNER list-clients -F '#{client_flags}') in
*focused*)
echo yes
;;
*)
echo no
;;
esac
}
# Start unfocused, whatever the state after attaching, once the pane has
# enabled focus reporting.
i=0
while :; do
send_raw "$(printf '\033[I\033[O')"
sleep 0.05
case $(capture) in
*'^[[O')
[ "$(focused)" = no ] && break
;;
esac
[ "$i" -lt 100 ] || fail 'pane did not receive focus reports'
i=$((i + 1))
done
expected_output=$(capture)
# A focus report is forwarded to the pane directly, while the Escape binding
# runs from the command queue, so the pane may see them in either order. The
# second expected output, if not empty, is that other order.
assert_events()
{
name=$1
expected=$expected_output$2
if [ -n "$3" ]; then
alternative=$expected_output$3
else
alternative=$expected
fi
expected_focus=$4
send_raw "$5"
if [ "$#" -eq 6 ]; then
sleep 0.05
send_raw "$6"
fi
i=0
while [ "$i" -lt 100 ]; do
actual=$(capture)
actual_focus=$(focused)
if [ "$actual" = "$expected" ] ||
[ "$actual" = "$alternative" ]; then
if [ "$actual_focus" = "$expected_focus" ]; then
expected_output=$actual
return 0
fi
fi
sleep 0.05
i=$((i + 1))
done
fail "$name: got '$actual' focused=$actual_focus," \
"expected '$expected' focused=$expected_focus"
}
assert_events 'FocusIn' '^[[I' '' yes "$(printf '\033[I')"
assert_events 'FocusOut' '^[[O' '' no "$(printf '\033[O')"
assert_events 'Escape and FocusIn' 'E^[[I' '^[[IE' yes \
"$(printf '\033\033[I')"
assert_events 'Escape and FocusOut' 'E^[[O' '^[[OE' no \
"$(printf '\033\033[O')"
assert_events 'split FocusIn' 'E^[[I' '^[[IE' yes \
"$(printf '\033\033[')" 'I'
assert_events 'FocusOut split after two Escapes' 'E^[[O' '^[[OE' no \
"$(printf '\033\033')" '[O'
assert_events 'M-Escape' A '' no "$(printf '\033\033')"
assert_events 'M-Up' U '' no "$(printf '\033\033[A')"
exit 0

View File

@@ -0,0 +1,98 @@
#!/bin/sh
# An Escape followed by a mouse report must remain a separate key. The
# report's leading Escape must not be consumed as the second half of M-Escape.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 15
$INNER new-session -d -s inner -x80 -y10 \
"stty raw -echo; printf '\033[?1003h\033[?1006h'; exec cat -v" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g assume-paste-time 0 || exit 1
$INNER set-option -s escape-time 1000 || exit 1
$INNER unbind-key -a -T root || exit 1
$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1
$OUTER set-option -g assume-paste-time 0 || exit 1
i=0
while [ -z "$($INNER list-clients -F '#{client_name}')" ]; do
[ "$i" -lt 100 ] || fail 'inner client did not attach'
sleep 0.05
i=$((i + 1))
done
bind_key()
{
$INNER bind-key -n "$1" send-keys -l "$2" || exit 1
}
bind_key Escape E
bind_key M-Escape A
bind_key M-a B
bind_key M-Up U
expected_output=
send_raw()
{
# Hex arguments avoid command separators in a partial mouse report.
bytes=$(printf '%s' "$1" | od -An -v -tx1)
$OUTER send-keys -H $bytes || exit 1
}
assert_events()
{
name=$1
expected_output=$expected_output$2
send_raw "$3"
if [ "$#" -eq 4 ]; then
sleep 0.05
send_raw "$4"
fi
i=0
while [ "$i" -lt 100 ]; do
actual=$($INNER capture-pane -p | tr -d '\n')
[ "$actual" = "$expected_output" ] && return 0
sleep 0.05
i=$((i + 1))
done
fail "$name: got '$actual', expected '$expected_output'"
}
assert_events 'SGR motion' 'E^[[<35;3;3M' "$(printf '\033\033[<35;3;3M')"
assert_events 'SGR press' 'E^[[<0;3;3M' "$(printf '\033\033[<0;3;3M')"
assert_events 'SGR release' 'E^[[<0;3;3m' "$(printf '\033\033[<0;3;3m')"
assert_events 'SGR wheel' 'E^[[<64;3;3M' "$(printf '\033\033[<64;3;3M')"
assert_events 'X10 motion' 'E^[[MC##' "$(printf '\033\033[MC##')"
assert_events 'X10 press' 'E^[[M ##' "$(printf '\033\033[M ##')"
assert_events 'split SGR report' 'E^[[<35;3;3M' "$(printf '\033\033[<35;')" '3;3M'
assert_events 'split mouse prefix' 'E^[[<35;3;3M' "$(printf '\033\033[')" '<35;3;3M'
assert_events 'split after two Escapes' 'E^[[<35;3;3M' "$(printf '\033\033')" '[<35;3;3M'
assert_events 'invalid SGR coordinates' E "$(printf '\033\033[<35;0;3M')"
assert_events 'discarded wheel release' E "$(printf '\033\033[<64;3;3m')"
assert_events 'M-Escape' A "$(printf '\033\033')"
assert_events 'M-Escape and bracket' 'A[' "$(printf '\033\033[')"
assert_events 'M-a' B "$(printf '\033a')"
assert_events 'M-Up' U "$(printf '\033\033[A')"
exit 0

87
regress/tty-keys-paste.sh Normal file
View File

@@ -0,0 +1,87 @@
#!/bin/sh
# Paste state must change only when the start or end key completes. A separate
# Escape before paste start uses its binding, pasted keys bypass bindings, and
# the first key after paste end uses its binding again.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 15
$INNER new-session -d -s inner -x80 -y10 \
"stty raw -echo; printf '\033[?2004h'; exec cat -v" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g assume-paste-time 0 || exit 1
$INNER set-option -s escape-time 20 || exit 1
$INNER unbind-key -a -T root || exit 1
$INNER bind-key -n Escape send-keys -l E || exit 1
$INNER bind-key -n a send-keys -l A || exit 1
$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1
$OUTER set-option -g assume-paste-time 0 || exit 1
i=0
while [ -z "$($INNER list-clients -F '#{client_name}')" ] ||
[ "$($INNER display-message -p '#{bracket_paste_flag}')" != 1 ]; do
[ "$i" -lt 100 ] || fail 'client did not attach or enable bracketed paste'
sleep 0.05
i=$((i + 1))
done
send_raw()
{
bytes=$(printf '%s' "$1" | od -An -v -tx1)
$OUTER send-keys -H $bytes || exit 1
}
assert_output()
{
i=0
while [ "$i" -lt 100 ]; do
actual=$($INNER capture-pane -p | tr -d '\n')
[ "$actual" = "$1" ] && return 0
sleep 0.05
i=$((i + 1))
done
fail "got '$actual', expected '$1'"
}
send_raw "$(printf '\033\033[200~a\033[201~a')"
expected='E^[[200~a^[[201~A'
assert_output "$expected"
# A partial paste end gets more time than escape-time. The final key must
# still be outside the paste when the rest arrives after that time.
send_raw "$(printf '\033[200~a\033[20')"
sleep 0.05
send_raw '1~a'
expected=$expected'^[[200~a^[[201~A'
assert_output "$expected"
# Split the start immediately before its final byte, including an extra
# Escape. Neither the Escape binding nor paste mode may be lost.
$INNER set-option -s escape-time 1000 || exit 1
send_raw "$(printf '\033\033[200')"
sleep 0.05
send_raw "$(printf '~a\033[201~a')"
expected=$expected'E^[[200~a^[[201~A'
assert_output "$expected"
exit 0

152
regress/tty-keys-replies.sh Executable file
View File

@@ -0,0 +1,152 @@
#!/bin/sh
# Test terminal reply state changes, including replies fragmented at every
# byte and replies preceded by a separate Escape. Use a PTY directly so that
# an outer tmux cannot answer startup queries before the test does.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 - "$TEST_TMUX" <<'PY'
import fcntl
import os
import select
import struct
import subprocess
import sys
import termios
import time
tmux = sys.argv[1]
server = [tmux, "", "-f/dev/null"]
def run(*args, check=True):
p = subprocess.run(server + list(args), capture_output=True)
if check and p.returncode != 0:
raise RuntimeError("%r: %s" % (args, p.stderr.decode()))
return p.stdout.decode().strip()
def state():
return run("list-clients", "-F",
"#{client_termfeatures}|#{client_termtype}|"
"#{client_width}x#{client_height}|"
"#{client_cell_width}x#{client_cell_height}")
def matches(name, value):
features, kind, size, cells = value.split("|")
features = set(features.split(","))
if name == "DA":
return {"sixel", "margins", "rectfill", "clipboard"} <= features
if name == "DA2":
return "ignorefkeys" in features
if name == "XDA":
return kind == "ReplyTest"
if name == "sync":
return "sync" in features
if name == "characters":
return size == "61x17"
if name == "pixels":
return cells == "10x20"
reports = [
("DA", b"\033[?65;4;21;28;52c"),
("DA2", b"\033[>85;1;0c"),
("XDA", b"\033P>|ReplyTest\033\\"),
("sync", b"\033[?2026;2$y"),
("characters", b"\033[8;17;61t"),
("pixels", b"\033[4;480;800t"),
]
cases = 0
for name, report in reports:
for prefixed in (False, True):
deliveries = ["whole", "split", "bytewise"]
if name == "sync" and not prefixed:
deliveries += ["invalid", "expired"]
for delivery in deliveries:
context = (name, prefixed, delivery)
pid = fd = None
# Each case needs fresh one-shot startup reply flags. A new
# socket also avoids racing shutdown of the previous server.
server[1] = "-LtestReplies%d-%d" % (os.getpid(), cases)
try:
run("new-session", "-d", "-s", "replies", "-x", "80",
"-y", "24", "stty raw -echo; exec cat -v")
run("set-option", "-g", "status", "off")
run("set-option", "-g", "assume-paste-time", "0")
if delivery == "expired":
run("set-option", "-s", "escape-time", "100")
else:
run("set-option", "-s", "escape-time", "60000")
run("set-option", "-g", "@seen", "")
run("bind-key", "-n", "Escape", "set-option", "-gF",
"@seen", "#{@seen}E")
pid, fd = os.forkpty()
if pid == 0:
os.environ["TERM"] = "xterm-256color"
fcntl.ioctl(0, termios.TIOCSWINSZ,
struct.pack("HHHH", 24, 80, 0, 0))
os.execv(tmux, server + ["attach-session", "-t", "replies"])
output = b""
deadline = time.monotonic() + 5
while b"\033[?2026$p" not in output:
assert time.monotonic() < deadline, (context, "missing query")
if select.select([fd], [], [], .01)[0]:
output += os.read(fd, 65536)
baseline = state()
assert not matches(name, baseline), (context, baseline)
wire = (b"\033" if prefixed else b"") + report
if delivery == "invalid":
# A near match must fall back to ordinary key parsing.
wire = b"\033[?2026;5$y"
if delivery in ("whole", "invalid"):
chunks = [wire]
elif delivery in ("split", "expired"):
chunks = [wire[:-1], wire[-1:]]
else:
chunks = [wire[i:i+1] for i in range(len(wire))]
for part in chunks[:-1]:
os.write(fd, part)
time.sleep(.01)
if delivery != "bytewise":
actual = state()
assert actual == baseline, (context, "premature effect", actual)
assert run("capture-pane", "-p") == "", (context, "partial leak")
expected = "Z"
if delivery == "expired":
# Allow the partial reply's timer to expire. The pending
# query extends it to at least 500ms; wait for fallback
# rather than assuming a fixed expiration time.
expected = wire[:-1].replace(b"\033", b"^[").decode()
deadline = time.monotonic() + 5
while run("capture-pane", "-p") != expected:
assert time.monotonic() < deadline, (context, "timer stalled")
if select.select([fd], [], [], .01)[0]:
os.read(fd, 65536)
if delivery in ("invalid", "expired"):
expected = wire.replace(b"\033", b"^[").decode() + "Z"
# A trailing key confirms that the entire reply has been
# processed, including cases with no extra Escape.
os.write(fd, chunks[-1] + b"Z")
deadline = time.monotonic() + 5
while run("capture-pane", "-p") != expected:
assert time.monotonic() < deadline, (context,
"reply leaked or input stalled", run("capture-pane", "-p"))
if select.select([fd], [], [], .01)[0]:
os.read(fd, 65536)
actual = state()
if delivery in ("invalid", "expired"):
assert actual == baseline, (context, "unexpected effect", actual)
else:
assert matches(name, actual), (context, "missing effect", actual)
assert run("show-option", "-gv", "@seen") == (
"E" if prefixed else ""), (context, "Escape count")
cases += 1
finally:
run("kill-server", check=False)
if fd is not None:
os.close(fd)
if pid is not None:
os.waitpid(pid, 0)
PY

View File

@@ -0,0 +1,127 @@
#!/bin/sh
# An Escape followed by a terminal reply must remain a separate key. The
# reply's leading Escape must not be consumed as the second half of a Meta
# key, which would send the rest of the reply to the pane as text. A reply
# split before the end of a known key must wait for the rest.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 15
$INNER new-session -d -s inner -x80 -y10 "stty raw -echo; exec cat -v" ||
exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g assume-paste-time 0 || exit 1
$INNER set-option -s escape-time 1000 || exit 1
$INNER unbind-key -a -T root || exit 1
$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1
$OUTER set-option -g assume-paste-time 0 || exit 1
i=0
while [ -z "$($INNER list-clients -F '#{client_name}')" ]; do
[ "$i" -lt 100 ] || fail 'inner client did not attach'
sleep 0.05
i=$((i + 1))
done
bind_key()
{
$INNER bind-key -n "$1" send-keys -l "$2" || exit 1
}
bind_key Escape E
bind_key M-Escape A
bind_key M-] B
bind_key M-O P
bind_key M-Up U
send_raw()
{
# Hex arguments avoid command separators in a partial sequence.
bytes=$(printf '%s' "$1" | od -An -v -tx1)
$OUTER send-keys -H $bytes || exit 1
}
capture()
{
$INNER capture-pane -p | tr -d '\n'
}
theme()
{
$INNER list-clients -F '#{client_theme}'
}
expected_output=
assert_keys()
{
name=$1
expected=$expected_output$2
expected_theme=$3
send_raw "$4"
if [ "$#" -eq 5 ]; then
sleep 0.05
send_raw "$5"
fi
i=0
while [ "$i" -lt 100 ]; do
actual=$(capture)
actual_theme=$(theme)
if [ "$actual" = "$expected" ] &&
[ "$actual_theme" = "$expected_theme" ]; then
expected_output=$actual
return 0
fi
sleep 0.05
i=$((i + 1))
done
fail "$name: got '$actual' theme=$actual_theme," \
"expected '$expected' theme=$expected_theme"
}
assert_keys 'Escape and dark theme report' E dark \
"$(printf '\033\033[?997;1n')"
assert_keys 'Escape and light theme report' E light \
"$(printf '\033\033[?997;2n')"
assert_keys 'split theme report' '' dark \
"$(printf '\033[?997')" ';1n'
assert_keys 'Escape and light theme report, split' E light \
"$(printf '\033\033[?99')" '7;2n'
assert_keys 'theme report split after two Escapes' E dark \
"$(printf '\033\033')" '[?997;1n'
assert_keys 'Escape and light theme report again' E light \
"$(printf '\033\033[?997;2n')"
assert_keys 'Escape and background colour' E light \
"$(printf '\033\033]11;rgb:ffff/ffff/ffff\033\\')"
assert_keys 'Escape and foreground colour (BEL)' E light \
"$(printf '\033\033]10;rgb:0000/0000/0000\007')"
assert_keys 'split background colour' E light \
"$(printf '\033\033]11;rgb:ff')" "$(printf 'ff/ffff/ffff\033\\')"
assert_keys 'Escape and palette colour' E light \
"$(printf '\033\033]4;1;rgb:ffff/0000/0000\033\\')"
assert_keys 'Escape and clipboard' E light \
"$(printf '\033\033]52;c;dGVzdA==\007')"
assert_keys 'M-Escape' A light "$(printf '\033\033')"
assert_keys 'M-]' B light "$(printf '\033]')"
assert_keys 'M-O' P light "$(printf '\033O')"
assert_keys 'M-Up' U light "$(printf '\033\033[A')"
exit 0

96
regress/tty-margins-sb.sh Executable file
View File

@@ -0,0 +1,96 @@
#!/bin/sh
# With left/right margins, a pane narrowed by a scrollbar can scroll without
# retransmitting its existing rows. Both paths must produce the same screen.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
cat >"$DIR/emitter.pl" <<'PERL'
use strict;
use warnings;
$| = 1;
print join("\r\n", map { "KEEP0$_" } 1 .. 6);
while (!-e $ENV{TRIGGER}) {
select undef, undef, undef, 0.01;
}
print "\r\nNEWROW";
sleep 100;
PERL
j=0
for margins in on off; do
INNER="$TEST_TMUX -LA$j$$ -f/dev/null"
OUTER="$TEST_TMUX -LB$j$$ -f/dev/null"
$INNER new-session -d -s inner -x 40 -y 6 \
"TRIGGER='$DIR/trigger-$margins' perl '$DIR/emitter.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g pane-scrollbars on || exit 1
if [ "$margins" = on ]; then
$INNER set -as terminal-features ',screen-256color:margins' || exit 1
fi
$OUTER new-session -d -s outer -x 40 -y 6 'sleep 100' || exit 1
$OUTER set -g status off || exit 1
$OUTER set -g window-size manual || exit 1
$OUTER set -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 "$INNER attach -t inner" || exit 1
sleep 0.5
$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output-$margins'" || exit 1
$INNER refresh-client || exit 1
sleep 0.5
grep -aq KEEP02 "$DIR/output-$margins" || fail "initial rows not captured"
offset=$(wc -c <"$DIR/output-$margins")
: >"$DIR/trigger-$margins"
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$DIR/screen-$margins" || exit 1
grep -q NEWROW "$DIR/screen-$margins" && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "$margins: scroll did not reach the terminal"
sleep 0.2
tail -c +"$((offset + 1))" "$DIR/output-$margins" >"$DIR/scroll-$margins"
grep -aq NEWROW "$DIR/scroll-$margins" || fail "scroll output not captured"
if [ "$margins" = on ]; then
if grep -aq KEEP02 "$DIR/scroll-$margins"; then
fail "scroll with margins retransmitted an existing row"
fi
else
grep -aq KEEP02 "$DIR/scroll-$margins" ||
fail "scroll without margins did not exercise the redraw fallback"
fi
printf 'KEEP02\nKEEP03\nKEEP04\nKEEP05\nKEEP06\nNEWROW\n' >"$DIR/expected"
cmp -s "$DIR/expected" "$DIR/screen-$margins" ||
fail "$margins: terminal did not contain the expected scrolled rows"
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
j=$((j + 1))
done
exit 0

View File

@@ -0,0 +1,123 @@
#!/bin/sh
# Dragging an inactive pane's scrollbar slider changes which pane is
# active (window_copy_scroll(), window-copy.c). Every other place in the
# codebase that changes the active pane pairs it with
# window_redraw_active_switch() (or falls back to a full window redraw) so
# the old/new active panes' window-active-style/window-style colours get
# repainted - window_copy_scroll() was the one caller that did neither, so
# a scrollbar-slider drag changed focus but left both panes' body colours
# stale. This used to be masked by window_set_active_pane() itself doing
# an unconditional full redraw on every active-pane change, until that was
# narrowed to borders/status-only for the common case.
#
# This creates two tiled panes with clearly distinguishable
# window-active-style/window-style backgrounds, puts the *inactive* one in
# copy mode, drags its scrollbar slider, and checks - via an attached
# client's own received bytes, since window-style is applied during
# redraw composition rather than stored in the grid, so capture-pane
# alone would not reflect it - that the newly active pane's marker text
# immediately shows the active-style colour, not the stale one.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lsbfocus-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lsbfocus-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
ESC=$(printf '\033')
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat -A "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
get_mark_color()
{
$OUTER capture-pane -e -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
LC_ALL=C grep -a "MARK1" "$CAPTURE" |
LC_ALL=C grep -aoE "${ESC}\[4[12]m" | tail -1
}
wait_mark_color()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
[ "$(get_mark_color)" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "MARK1's pane never showed the expected background (wanted '$want', got '$(get_mark_color)')"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars on || exit 1
$INNER setw pane-scrollbars-position right || exit 1
$INNER setw pane-scrollbars-style 'width=1,pad=0' || exit 1
$INNER set-option -g window-active-style 'bg=red' || exit 1
$INNER set-option -g window-style 'bg=green' || exit 1
$INNER split-window -h -t inner -d 'printf MARK1; sleep 100' || exit 1
PANE1=$($INNER list-panes -t inner -F '#{pane_id}' | sed -n 2p)
[ "$($INNER display-message -p -t "$PANE1" '#{pane_active}')" = 0 ] ||
fail "sanity: MARK1's pane is already active before the drag"
# Put the inactive pane in copy mode without switching focus (copy-mode -t
# targets a pane without making it active).
$INNER copy-mode -t "$PANE1" || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lsbfocus-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_mark_color "${ESC}[42m"
XOFF=$($INNER display-message -p -t "$PANE1" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$PANE1" '#{pane_top}')
SX=$($INNER display-message -p -t "$PANE1" '#{pane_width}')
SBCOL=$((XOFF + SX + 1))
SBROW=$((YOFF + 1))
# With no scrollback beyond the pane's own content, the slider fills the
# whole scrollbar track, so any point on it (here, its very first row) is
# on the slider.
mouse 0 "$SBCOL" "$SBROW" M
mouse 32 "$SBCOL" "$((SBROW + 1))" M
mouse 0 "$SBCOL" "$((SBROW + 1))" m
[ "$($INNER display-message -p -t "$PANE1" '#{pane_active}')" = 1 ] ||
fail "sanity: dragging the scrollbar slider did not change focus"
wait_mark_color "${ESC}[41m"
exit 0