mirror of
https://github.com/tmux/tmux.git
synced 2026-10-11 04:44:25 +00:00
Merge and fix conflicts.
This commit is contained in:
34
regress/alternate-screen-resize.sh
Normal file
34
regress/alternate-screen-resize.sh
Normal 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
|
||||
79
regress/attach-session-redraw.sh
Executable file
79
regress/attach-session-redraw.sh
Executable 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
|
||||
51
regress/cfg-client-free.sh
Normal file
51
regress/cfg-client-free.sh
Normal 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
|
||||
90
regress/cfg-client-lost-before-wait.sh
Normal file
90
regress/cfg-client-lost-before-wait.sh
Normal 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
60
regress/cfg-load-once.sh
Normal 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
|
||||
@@ -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
|
||||
|
||||
69
regress/cmd-parse-assignment.sh
Normal file
69
regress/cmd-parse-assignment.sh
Normal 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
|
||||
75
regress/cmd-parse-condition.sh
Normal file
75
regress/cmd-parse-condition.sh
Normal 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
|
||||
48
regress/copy-mode-no-client-scroll.sh
Normal file
48
regress/copy-mode-no-client-scroll.sh
Normal 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
161
regress/copy-mode-search-count.sh
Executable 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
149
regress/copy-mode-word-redraw.sh
Normal file
149
regress/copy-mode-word-redraw.sh
Normal 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"
|
||||
66
regress/display-message-client.sh
Executable file
66
regress/display-message-client.sh
Executable 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
|
||||
135
regress/floating-pane-border-clients.sh
Executable file
135
regress/floating-pane-border-clients.sh
Executable 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
|
||||
88
regress/floating-pane-drag-composite.sh
Executable file
88
regress/floating-pane-drag-composite.sh
Executable 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
|
||||
108
regress/floating-pane-drag-ghost.sh
Executable file
108
regress/floating-pane-drag-ghost.sh
Executable 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
|
||||
160
regress/floating-pane-drag-sb-strip.sh
Executable file
160
regress/floating-pane-drag-sb-strip.sh
Executable 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
|
||||
93
regress/floating-pane-drag-sync.sh
Executable file
93
regress/floating-pane-drag-sync.sh
Executable 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
|
||||
200
regress/floating-pane-drag-wide-character.sh
Executable file
200
regress/floating-pane-drag-wide-character.sh
Executable 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
|
||||
206
regress/floating-pane-drag-wide-right.sh
Executable file
206
regress/floating-pane-drag-wide-right.sh
Executable 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
|
||||
102
regress/floating-pane-offscreen-alt.sh
Executable file
102
regress/floating-pane-offscreen-alt.sh
Executable 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
|
||||
179
regress/floating-pane-prompt-wide-character.sh
Executable file
179
regress/floating-pane-prompt-wide-character.sh
Executable 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
|
||||
151
regress/floating-pane-status-cross-client.sh
Executable file
151
regress/floating-pane-status-cross-client.sh
Executable 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
|
||||
102
regress/floating-pane-status-format.sh
Executable file
102
regress/floating-pane-status-format.sh
Executable 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
|
||||
@@ -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 ---------------------------------------
|
||||
|
||||
@@ -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
2
regress/format-strings.sh
Normal file → Executable 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"
|
||||
|
||||
|
||||
|
||||
|
||||
45
regress/format-time-range.sh
Normal file
45
regress/format-time-range.sh
Normal 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
|
||||
47
regress/input-nowrap-collect.sh
Normal file
47
regress/input-nowrap-collect.sh
Normal 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
|
||||
@@ -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
|
||||
|
||||
80
regress/join-pane-modal.sh
Normal file
80
regress/join-pane-modal.sh
Normal 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"
|
||||
@@ -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
164
regress/menu-live-output.sh
Normal 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
143
regress/new-pane-mouse-status.sh
Executable 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
|
||||
@@ -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'
|
||||
|
||||
113
regress/redraw-damage-accumulate.sh
Normal file
113
regress/redraw-damage-accumulate.sh
Normal 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
|
||||
140
regress/redraw-damage-blocked-client.sh
Normal file
140
regress/redraw-damage-blocked-client.sh
Normal 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
112
regress/redraw-damage-only.sh
Executable 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
|
||||
112
regress/redraw-damage-viewports.sh
Normal file
112
regress/redraw-damage-viewports.sh
Normal 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
186
regress/redraw-multiclient.sh
Executable 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
208
regress/redraw-screen-write.sh
Executable 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
|
||||
29
regress/respawn-pane-empty.sh
Normal file
29
regress/respawn-pane-empty.sh
Normal 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
41
regress/screen-redraw-floating.sh
Normal file → Executable 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
|
||||
|
||||
40
regress/swap-pane-zoom-error.sh
Normal file
40
regress/swap-pane-zoom-error.sh
Normal 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
81
regress/switch-client-redraw.sh
Executable 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
|
||||
@@ -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
|
||||
|
||||
127
regress/tty-clamp-area-status-top.sh
Executable file
127
regress/tty-clamp-area-status-top.sh
Executable 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
|
||||
97
regress/tty-keys-extended-escape.sh
Normal file
97
regress/tty-keys-extended-escape.sh
Normal 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
|
||||
143
regress/tty-keys-focus-escape.sh
Normal file
143
regress/tty-keys-focus-escape.sh
Normal 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
|
||||
98
regress/tty-keys-mouse-escape.sh
Normal file
98
regress/tty-keys-mouse-escape.sh
Normal 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
87
regress/tty-keys-paste.sh
Normal 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
152
regress/tty-keys-replies.sh
Executable 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
|
||||
127
regress/tty-keys-reply-escape.sh
Normal file
127
regress/tty-keys-reply-escape.sh
Normal 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
96
regress/tty-margins-sb.sh
Executable 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
|
||||
123
regress/window-copy-sb-drag-focus-style.sh
Executable file
123
regress/window-copy-sb-drag-focus-style.sh
Executable 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
|
||||
Reference in New Issue
Block a user