mirror of
https://github.com/tmux/tmux.git
synced 2026-10-11 04:44:25 +00:00
Tests for 5675.
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
#!/bin/sh
|
#!/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
|
PATH=/bin:/usr/bin
|
||||||
TERM=screen
|
TERM=screen
|
||||||
|
|
||||||
@@ -8,6 +13,7 @@ TERM=screen
|
|||||||
python3 - "$TEST_TMUX" <<'PY'
|
python3 - "$TEST_TMUX" <<'PY'
|
||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
|
import shlex
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -49,13 +55,13 @@ def read_until(fd, needle, timeout=5):
|
|||||||
raise RuntimeError("did not see terminal request %r in %r" %
|
raise RuntimeError("did not see terminal request %r in %r" %
|
||||||
(needle, data))
|
(needle, data))
|
||||||
|
|
||||||
def wait_file(path, timeout=5):
|
def wait_file(path, length, timeout=5):
|
||||||
end = time.time() + timeout
|
end = time.time() + timeout
|
||||||
while time.time() < end:
|
while time.time() < end:
|
||||||
try:
|
try:
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
if data:
|
if len(data) >= length:
|
||||||
return data
|
return data
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
@@ -66,6 +72,13 @@ def respawn(command):
|
|||||||
run("respawn-window", "-k", "-t", "requests:0", command)
|
run("respawn-window", "-k", "-t", "requests:0", command)
|
||||||
time.sleep(0.2)
|
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):
|
def cleanup(pid=None):
|
||||||
if pid is not None:
|
if pid is not None:
|
||||||
try:
|
try:
|
||||||
@@ -73,6 +86,8 @@ def cleanup(pid=None):
|
|||||||
except ProcessLookupError:
|
except ProcessLookupError:
|
||||||
pass
|
pass
|
||||||
run("kill-server", check=False)
|
run("kill-server", check=False)
|
||||||
|
if pid is not None:
|
||||||
|
os.waitpid(pid, 0)
|
||||||
|
|
||||||
run("kill-server", check=False)
|
run("kill-server", check=False)
|
||||||
run("new-session", "-d", "-x", "80", "-y", "24", "-s", "requests",
|
run("new-session", "-d", "-x", "80", "-y", "24", "-s", "requests",
|
||||||
@@ -82,37 +97,97 @@ pid, fd = attach()
|
|||||||
try:
|
try:
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
run("set-option", "-g", "assume-paste-time", "0")
|
||||||
palette_out = f.name
|
run("set-option", "-s", "escape-time", "1000")
|
||||||
respawn("stty raw -echo min 1 time 50; "
|
run("set-option", "-g", "@seen", "")
|
||||||
"printf '\\033]4;99;?\\033\\\\'; "
|
run("bind-key", "-n", "Escape", "set-option", "-gF", "@seen",
|
||||||
"dd bs=1 count=27 2>/dev/null | cat -v >%s; sleep 1" %
|
"#{@seen}E")
|
||||||
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", "-s", "set-clipboard", "on")
|
run("set-option", "-s", "set-clipboard", "on")
|
||||||
run("set-option", "-s", "get-clipboard", "request")
|
run("set-option", "-s", "get-clipboard", "request")
|
||||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
expected_escapes = b""
|
||||||
clip_out = f.name
|
replies = [
|
||||||
respawn("stty raw -echo min 1 time 50; "
|
("palette", "\\033]4;99;?\\033\\\\", b"\033]4;99;?\033\\",
|
||||||
"printf '\\033]52;c;?\\033\\\\'; "
|
b"\033]4;99;rgb:0101/0202/0303", b"\033]4;99;invalid"),
|
||||||
"dd bs=1 count=21 2>/dev/null | cat -v >%s; sleep 1" %
|
("clipboard", "\\033]52;c;?\\033\\\\", b"]52;",
|
||||||
clip_out)
|
b"\033]52;c;UmVxdWVzdA==", b"\033]52;c;!!!!"),
|
||||||
data = read_until(fd, b"]52;")
|
]
|
||||||
if b"?" not in data:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
raise RuntimeError("clipboard request missing query in %r" % data)
|
output = os.path.join(directory, "reply")
|
||||||
os.write(fd, b"\033]52;c;UmVxdWVzdA==\033\\")
|
for name, query, needle, payload, invalid in replies:
|
||||||
got = wait_file(clip_out)
|
for prefixed in (False, True):
|
||||||
expected = b"^[]52;c;UmVxdWVzdA==^[\\"
|
for split in (False, True):
|
||||||
if got != expected:
|
for terminator in (b"\007", b"\033\\"):
|
||||||
raise AssertionError("clipboard reply: expected %r got %r" %
|
context = (name, prefixed, split, terminator)
|
||||||
(expected, got))
|
# 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:
|
finally:
|
||||||
cleanup(pid)
|
cleanup(pid)
|
||||||
|
os.close(fd)
|
||||||
PY
|
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