mirror of
https://github.com/tmux/tmux.git
synced 2026-10-07 02:04:22 +00:00
Tests for 5675.
This commit is contained in:
@@ -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,13 +55,13 @@ 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
|
||||
@@ -66,6 +72,13 @@ def respawn(command):
|
||||
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:
|
||||
try:
|
||||
@@ -73,6 +86,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 +97,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)))
|
||||
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)))
|
||||
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
|
||||
|
||||
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
|
||||
148
regress/tty-keys-replies.sh
Normal file
148
regress/tty-keys-replies.sh
Normal file
@@ -0,0 +1,148 @@
|
||||
#!/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")
|
||||
run("set-option", "-s", "escape-time", "1000")
|
||||
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)
|
||||
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
|
||||
Reference in New Issue
Block a user