Merge branch 'master' into 4902-image-support

# Conflicts:
#	Makefile.am
#	configure.ac
#	format.c
#	image-sixel.c
#	regress/tty-keys-replies.sh
#	resize.c
#	screen-redraw.c
#	screen-write.c
#	screen.c
#	tmux.1
#	tmux.h
#	tty-features.c
#	tty.c
This commit is contained in:
Nicholas Marriott
2026-10-12 17:01:36 +01:00
37 changed files with 781 additions and 122 deletions

View File

@@ -0,0 +1,87 @@
#!/bin/sh
# 1049h saves the cursor when it enters the alternate screen, and neither 47l
# nor 1049l clears it. If the screen was resized after it was saved, 1049l can
# restore a row below the saved grid it reflows.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest$$ -f/dev/null"
DIR=$(mktemp -d)
trap '$TMUX kill-server 2>/dev/null; rm -rf $DIR' 0 1 15
exit_status=0
# Wait until the pane's program has set its path (OSC 7) to $1.
wait_path() {
_i=0
while [ "$($TMUX display -p '#{pane_path}' 2>/dev/null)" != "$1" ]; do
if ! $TMUX has 2>/dev/null; then
echo "FAIL: $2: the server died"
exit 1
fi
_i=$((_i + 1))
if [ $_i -ge 400 ]; then
echo "FAIL: $2: timed out waiting for $1"
exit_status=1
return 1
fi
sleep 0.05
done
}
# Respawn the pane, $2 by $3, running $1. Where the program waits for a file
# in $DIR, the test resizes the window and then creates it.
run() {
rm -f $DIR/go*
$TMUX resize-window -x $2 -y $3 || exit 1
$TMUX respawn-pane -k "$1; exec sleep 1000" || exit 1
}
check() {
if [ "$2" != "$3" ]; then
echo "FAIL: $1: '$2', not '$3'"
exit_status=1
fi
}
$TMUX new -d -x 10 -y 5 'sleep 1000' || exit 1
$TMUX set -g window-size manual || exit 1
# Outside the alternate screen (47l): the cursor after a full line goes to
# the last column, as with no 47l. Unchanged.
run "printf 'abcdefghij\\033[?1049h\\033[?47l\\033[?1049lX\\033]7;done\\007'" 10 5
if wait_path done "1049l outside the alternate screen"; then
check "1049l outside the alternate screen" \
"$($TMUX capture-pane -p | head -1)" abcdefghiX
fi
# A full line, 1049h, a wider screen, 1049l: the cursor is after the line's
# last character and X follows it. Unchanged.
run "printf 'abcdefghij\\033[?1049h\\033]7;in\\007'; while [ ! -e $DIR/go ]; do sleep 0.05; done; printf '\\033[?1049lX\\033]7;done\\007'" 10 5
if wait_path in "full line, then wider"; then
$TMUX resize-window -x 20 || exit 1
touch $DIR/go
wait_path done "full line, then wider" &&
check "full line, then wider" \
"$($TMUX capture-pane -p | head -1)" abcdefghijX
fi
# cy 8 saved 10 high; 47l, 20x4, 47h, 40x4, 1049l. The cursor goes to the
# last row of the saved grid, which is 4 high.
run "printf '\\033[9;1Hrow\\033[?1049h\\033[?47l\\033]7;one\\007'; while [ ! -e $DIR/go1 ]; do sleep 0.05; done; printf '\\033[?47h\\033]7;two\\007'; while [ ! -e $DIR/go2 ]; do sleep 0.05; done; printf '\\033[?1049lX\\033]7;done\\007'" 40 10
if wait_path one "saved taller"; then
$TMUX resize-window -x 20 -y 4 || exit 1
touch $DIR/go1
if wait_path two "saved taller"; then
$TMUX resize-window -x 40 -y 4 || exit 1
touch $DIR/go2
wait_path done "saved taller" &&
check "saved taller" \
"$($TMUX capture-pane -p | grep row)" rowX
fi
fi
exit $exit_status

72
regress/client-exit-resize.sh Executable file
View File

@@ -0,0 +1,72 @@
#!/bin/sh
# A client that has sent MSG_EXITING can still send MSG_RESIZE. The server
# must not use the tty it closed for MSG_EXITING. Here the client is stopped
# and sent SIGTERM and then SIGWINCH for a size with no pixel size, so the
# server would ask the terminal for one.
PATH=/bin:/usr/bin:/usr/local/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest$$ -f/dev/null"
trap '$TMUX kill-server 2>/dev/null' 0 1 15
$TMUX new -d 'sleep 1000' || exit 1
$TMUX set-hook -g client-resized 'set -g @resized 1' || exit 1
python3 - $TMUX <<'EOF2' || exit 1
import fcntl, os, pty, signal, struct, subprocess, sys, termios, time
tmux = sys.argv[1:]
def size(fd, rows, cols, xpixel, ypixel):
fcntl.ioctl(fd, termios.TIOCSWINSZ,
struct.pack("HHHH", rows, cols, cols * xpixel, rows * ypixel))
master, slave = pty.openpty()
size(slave, 20, 60, 8, 16)
client = os.fork()
if client == 0:
os.setsid()
fcntl.ioctl(slave, termios.TIOCSCTTY, 0)
for fd in (0, 1, 2):
os.dup2(slave, fd)
os.environ["TERM"] = "xterm"
os.execvp(tmux[0], tmux + ["attach"])
os.set_blocking(master, False)
# Wait until the server has handled the resize the client sends when it
# attaches.
for i in range(200):
try:
os.read(master, 65536)
except OSError:
pass
out = subprocess.run(tmux + ["show", "-gv", "@resized"],
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout
if out.strip() == b"1":
break
time.sleep(0.05)
else:
print("the client did not attach")
sys.exit(1)
# SIGTERM is sent before SIGWINCH, so the client handles it first whether
# it handles pending signals in signal number order or in the order sent.
os.kill(client, signal.SIGSTOP)
os.waitpid(client, os.WUNTRACED)
os.kill(client, signal.SIGTERM)
size(master, 23, 61, 0, 0)
os.kill(client, signal.SIGWINCH)
os.kill(client, signal.SIGCONT)
for i in range(200):
if os.waitpid(client, os.WNOHANG)[0] != 0:
break
time.sleep(0.05)
else:
print("the client did not exit")
sys.exit(1)
EOF2
sleep 1
$TMUX has || { echo "the server died"; exit 1; }
exit 0

View File

@@ -226,10 +226,24 @@ test_format "#{e|/|f|2:10,3}" "3.33"
# Default number of decimal places for float is two.
test_format "#{e|*|f:2.5,2}" "5.00"
# Division by zero must not crash the server (result is unspecified).
$TMUX display-message -p "#{e|/|:5,0}" >/dev/null 2>&1
$TMUX display-message -p "#{e|/|f:5,0}" >/dev/null 2>&1
assert_alive "division by zero"
# An integer expression with a side or result that is NaN or out of the range
# of a long long expands to empty: division or modulus by zero, nan or inf, or
# 1e30. LLONG_MAX written out is 2^63 as a double, which is out of range; the
# largest double below 2^63 and -2^63 still convert. Comparisons are integer
# expressions too. Floating point gives inf.
test_format "#{e|/|:5,0}" ""
test_format "#{e|m|:0,0}" ""
test_format "#{e|m|:7,0}" ""
test_format "#{e|+|:nan,0}" ""
test_format "#{e|+|:-inf,1}" ""
test_format "#{e|-|:1e30,1e30}" ""
test_format "#{e|*|:1e30,1}" ""
test_format "#{e|+|:9223372036854775807,0}" ""
test_format "#{e|<|:nan,1}" ""
test_format "#{e|+|:9223372036854774784,0}" "9223372036854774784"
test_format "#{e|-|:-9223372036854775808,0}" "-9223372036854775808"
test_format "#{e|/|f:5,0}" "inf"
assert_alive "expressions with no integer value"
# --- ASCII and repeat ----------------------------------------------------

View File

@@ -79,6 +79,7 @@ history_limit
history_size
host
host_short
image_support
insert_flag
keypad_cursor_flag
keypad_flag

View File

@@ -23,7 +23,7 @@ $TMUX new-session -d -x 10 -y 4 "
$TMUX set -g status off || exit 1
$TMUX set -g scroll-on-clear off || exit 1
$TMUX set -as terminal-features ',*:sixel@' || exit 1
$TMUX set -as terminal-features ',*:kitty@' || exit 1
$TMUX set -as terminal-features ',*:kittygraphics@' || exit 1
$TMUX set -as terminal-features ',*:RGB' || exit 1
$TMUX2 new-session -d -x 10 -y 4 "$TMUX attach-session" || exit 1
$TMUX2 set -g status off || exit 1

View File

@@ -109,7 +109,7 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-layers-') as tmp:
if run(inner, 'display-message', '-p', '#{image_support}') == '0':
raise SystemExit(0)
run(inner, 'set', '-g', 'status', 'off')
run(inner, 'set', '-as', 'terminal-features', ',*:kitty')
run(inner, 'set', '-as', 'terminal-features', ',*:kittygraphics')
pane = run(outer, 'new-session', '-dP', '-F', '#{pane_id}',
'-x', '40', '-y', '12', 'exec sh')
run(outer, 'set', '-g', 'status', 'off')

View File

@@ -112,7 +112,7 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp:
raise SystemExit(0)
run('set', '-g', 'status', 'off')
run('set', '-as', 'terminal-features', ',*:sixel@')
run('set', '-as', 'terminal-features', ',*:kitty@')
run('set', '-as', 'terminal-features', ',*:kittygraphics@')
run('set', '-as', 'terminal-features', ',*:RGB')
subprocess.run(outer + ['new-session', '-d', '-x', '40', '-y', '12'], check=True)
subprocess.run(outer + ['set', '-g', 'status', 'off'], check=True)

View File

@@ -55,12 +55,12 @@ $TMUX new-session -d -s inner -x 40 -y 20 "printf '$HEADER'; exec sh" ||
sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -as terminal-features ',*:kitty' || exit 1
$TMUX set -as terminal-features ',*:kittygraphics' || exit 1
$TMUX2 new-session -d -x 40 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 set -as terminal-features ',*:kitty@' || fail "disable outer kitty failed"
$TMUX2 set -as terminal-features ',*:kittygraphics@' || fail "disable outer kitty failed"
$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"

View File

@@ -57,7 +57,7 @@ scroll_markers()
$TMUX2 new-session -d -x 40 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 set -as terminal-features ',*:kitty@' ||
$TMUX2 set -as terminal-features ',*:kittygraphics@' ||
fail "disable outer kitty failed"
: >"$TMP"
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
@@ -76,11 +76,11 @@ scroll_markers()
N=$(grep -ac MARKER "$TMP")
}
scroll_markers ',*:kitty,*:imagescroll'
scroll_markers ',*:kittygraphics,*:imagescroll'
[ "$N" -eq 0 ] ||
fail "existing text was redrawn ($N lines) scrolling with imagescroll"
scroll_markers ',*:kitty'
scroll_markers ',*:kittygraphics'
[ "$N" -gt 0 ] ||
fail "text was not redrawn scrolling without imagescroll"

View File

@@ -0,0 +1,88 @@
#!/bin/sh
. ./input-common.inc
# Exercise expansion by VS16, a skin tone modifier and a regional indicator.
for kind in variation modifier flag; do
case "$kind" in
variation)
combine_seq='\342\234\224\357\270\217'
text='✔️'
;;
modifier)
combine_seq='\342\230\235\360\237\217\273'
text='☝🏻'
;;
flag)
combine_seq='\360\237\207\254\360\237\207\247'
text='🇬🇧'
;;
esac
start_pane "$kind-overwrite" 10 3 "A\347\225\214BC\r${combine_seq}X"
check_capture "$kind-overwrite" "${text}XBC"
check_cursor "$kind-overwrite" '3,0'
check_raw_matches "$kind-overwrite" \
"C 0,0 data=\(2,[0-9]+,$text\) flags=NONE\[0\]" \
'C 0,1 .*flags=PADDING\[[0-9a-f]+\]' \
'C 0,2 data=\(1,1,X\) flags=NONE\[0\]'
start_pane "$kind-edge" 5 3 "abcd${combine_seq}X"
check_capture "$kind-edge" "abcd
${text}X"
check_cursor "$kind-edge" '3,1'
check_raw_matches "$kind-edge" \
'L 0 \(0\) flags=.*WRAPPED' \
'C 0,4 data=\(1,1, \)' \
"C 1,0 data=\(2,[0-9]+,$text\) flags=NONE\[0\]" \
'C 1,1 .*flags=PADDING\[[0-9a-f]+\]' \
'C 1,2 data=\(1,1,X\) flags=NONE\[0\]'
start_pane "$kind-fit" 5 3 "abc${combine_seq}X"
check_capture "$kind-fit" "abc${text}
X"
check_cursor "$kind-fit" '1,1'
start_pane "$kind-scroll" 5 2 "11111\r\nabcd${combine_seq}X"
check_capture "$kind-scroll" "abcd
${text}X"
check_cursor "$kind-scroll" '3,1'
start_pane "$kind-region" 5 4 \
"top\033[2;1Hold\033[4;1Hend\033[2;3r\033[3;1Habcd${combine_seq}X"
check_capture "$kind-region" "top
abcd
${text}X
end"
check_cursor "$kind-region" '3,2'
start_pane "$kind-insert" 10 3 "ABCDE\r\033[4h${combine_seq}X"
check_capture "$kind-insert" "${text}XABCDE"
check_cursor "$kind-insert" '3,0'
start_pane "$kind-insert-edge" 5 3 \
"\033[2;1HABCDE\033[1;1Habcd\033[4h${combine_seq}X"
check_capture "$kind-insert-edge" "abcd
${text}XAB"
check_cursor "$kind-insert-edge" '3,1'
start_pane "$kind-nowrap" 5 3 "\033[?7labc${combine_seq}"
check_capture "$kind-nowrap" "abc${text}"
check_cursor "$kind-nowrap" '4,0'
check_raw_matches "$kind-nowrap" \
"C 0,3 data=\(2,[0-9]+,$text\) flags=NONE\[0\]" \
'C 0,4 .*flags=PADDING\[[0-9a-f]+\]'
done
# A two-column cluster cannot fit in a one-column pane. Keep its narrow base.
start_pane one-column 1 3 '\342\234\224\357\270\217'
check_capture one-column '✔'
check_cursor one-column '1,0'
check_raw_no_matches one-column 'data=\(2,'
# Without wrapping, even a wide cell filling the pane leaves the cursor inside.
start_pane two-columns-nowrap 2 3 '\033[?7l\342\234\224\357\270\217'
check_capture two-columns-nowrap '✔️'
check_cursor two-columns-nowrap '1,0'
exit $exit_status

View File

@@ -40,7 +40,7 @@ start_pane tabs 12 3 'A\tB\033[2g\r\033[IC'
check_raw_matches tabs \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,1 data=\(7,7, \) flags=TAB\[[0-9a-f]+\]' \
'C 0,2 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\]' \
'C 0,2 data=\(0,0,\) flags=PADDING\[[0-9a-f]+\]' \
'C 0,8 data=\(1,1,B\) flags=NONE\[0\]' \
'C 0,0 data=\(1,1,C\) flags=NONE\[0\]'

View File

@@ -7,7 +7,7 @@ check_capture wide 'AあB'
check_raw_matches wide \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,1 data=\(2,3,あ\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,2 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,2 data=\(0,0,\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,3 data=\(1,1,B\) flags=NONE\[0\]'
start_pane combining 8 3 'e\314\201x'
@@ -20,21 +20,21 @@ start_pane emoji 10 3 '\360\237\230\200Z'
check_raw_matches emoji \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,0 data=\(2,4,😀\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,1 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,1 data=\(0,0,\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,2 data=\(1,1,Z\) flags=NONE\[0\]'
start_pane flag 10 3 '\360\237\207\254\360\237\207\247!'
check_raw_matches flag \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,0 data=\(2,8,🇬🇧\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,1 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,1 data=\(0,0,\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,2 data=\(1,1,!\) flags=NONE\[0\]'
start_pane variation 10 3 '*\357\270\217!'
check_raw_matches variation \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,0 data=\(2,[0-9]+,.*\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,1 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,1 data=\(0,0,\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,2 data=\(1,1,!\) flags=NONE\[0\]'
start_pane invalid 10 3 '\377A'
@@ -69,14 +69,14 @@ check_raw_matches overwrite-wide-pad \
start_pane overwrite-wide-with-wide 10 3 'A\343\201\202B\r\033[1C\347\225\214'
check_raw_matches overwrite-wide-with-wide \
'C 0,1 data=\(2,3,界\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,2 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,2 data=\(0,0,\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,3 data=\(1,1,B\) flags=NONE\[0\]'
start_pane wide-right-edge 4 3 'ABC\343\201\202Z'
check_raw_matches wide-right-edge \
'L 0 \(0\) flags=WRAPPED\[[0-9a-f]+\]' \
'C 1,0 data=\(2,3,あ\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 1,1 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 1,1 data=\(0,0,\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 1,2 data=\(1,1,Z\) flags=NONE\[0\]'
exit $exit_status

View File

@@ -0,0 +1,60 @@
#!/bin/sh
# Wide-character padding must not count towards the reflowed line width.
. ./input-common.inc
check_reflow()
{
# Resizing can move the first physical line into history.
$TMUX capture-pane -pN"$2" -t "$1:" -S - -E - |
normalize_capture >"$TMP"
printf '%s\n' "${3:-$text}" >"$EXP"
cmp "$TMP" "$EXP" || fail "$1 reflow (width $width, flags -pN$2)"
}
text='中文文档甲 中文文档乙 中文文档丙 中文文档丁 中文文档戊 中文文档己'
# Default and RGB backgrounds exercise compact and extended padding cells.
for padding in compact extended; do
style=''
if [ "$padding" = extended ]; then
style='\033[48;2;1;2;3m'
fi
width=96
start_pane_history "wide-$padding" 96 18 "${style}${text}\\n"
check_reflow "wide-$padding" ''
for width in 47 96 47 96; do
$TMUX resize-window -t "wide-$padding:" -x "$width" -y 18 ||
exit 1
check_reflow "wide-$padding" J
if [ "$width" = 96 ]; then
check_reflow "wide-$padding" ''
fi
done
done
# Include both a split between wide characters and an odd-width boundary.
text='中文文档'
start_pane_history boundary 8 18 "${text}\\n"
for width in 4 8 3 8; do
$TMUX resize-window -t boundary: -x "$width" -y 18 || exit 1
check_reflow boundary J
case "$width" in
4)
check_reflow boundary '' '中文
文档'
;;
3)
check_reflow boundary '' '中
文
文
档'
;;
8)
check_reflow boundary ''
;;
esac
done
exit $exit_status

29
regress/menu-pane-killed.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/sh
# A menu opened for a pane is drawn again after the pane is killed. Drawing it
# must not use the pane. The terminal is a pane of a second server.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest$$ -f/dev/null"
TMUX2="$TEST_TMUX -Ltest2$$ -f/dev/null"
trap '$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null' 0 1 15
$TMUX new -d 'sleep 1000' || exit 1
$TMUX2 new -d "unset TMUX; exec $TMUX attach" || exit 1
n=0
while [ -z "$($TMUX lsc 2>/dev/null)" ]; do
n=$((n + 1))
[ $n -gt 100 ] && exit 1
sleep 0.1
done
P=$($TMUX splitw -d -P -F '#{pane_id}' 'sleep 1000') || exit 1
$TMUX display-menu -c "$($TMUX lsc -F '#{client_name}')" -t $P one 1 '' ||
exit 1
$TMUX kill-pane -t $P
sleep 1
$TMUX has || { echo "the server died"; exit 1; }
exit 0

48
regress/ncv.sh Normal file
View File

@@ -0,0 +1,48 @@
#!/bin/sh
# Check that attributes listed in ncv are not used with colours.
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"
fail() {
echo "$*" >&2
exit 1
}
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)
trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" 0 1 15
$TMUX2 new -d -x20 -y5 \
"printf '\033[2;4mPLAIN\033[0m'; exec sleep 100" || exit 1
$TMUX2 set -g status-left '' || exit 1
$TMUX2 set -g status-right '' || exit 1
$TMUX2 set -g status-format[0] \
'#[fg=black,bg=green,underscore,dim]MARK' || exit 1
$TMUX2 set -as terminal-overrides ',screen:ncv=18' || exit 1
$TMUX new -d -x20 -y5 || exit 1
$TMUX set -g status off || exit 1
$TMUX send -l "$TMUX2 attach" || exit 1
$TMUX send Enter || exit 1
sleep 1
$TMUX capturep -pe >$TMP || exit 1
ESC=$(printf '\033')
grep -q "${ESC}\\[30m${ESC}\\[42mMARK" $TMP ||
fail "coloured status text missing"
grep -q "${ESC}\\[2;4mPLAIN" $TMP ||
fail "attributes without colours were filtered"
tail -1 $TMP | grep -q "${ESC}\\[4m" && fail "underline not filtered"
tail -1 $TMP | grep -q "${ESC}\\[2m" && fail "dim not filtered"
exit 0

View File

@@ -0,0 +1,43 @@
#!/bin/sh
# Reusing a session name must not reuse an unrelated session group.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
trap '$TMUX kill-server 2>/dev/null' 0 1 15
$TMUX new -d -s a || exit 1
$TMUX new -d -t a -s b || exit 1
$TMUX kill-session -t a || exit 1
# Occupy the first suffix too, without creating a session of that name.
$TMUX new -d -t a-1 -s other || exit 1
$TMUX new -d -s a || exit 1
AW=$($TMUX display -p -t a: '#{window_id}')
BW=$($TMUX display -p -t b: '#{window_id}')
$TMUX new -d -t a -s c || exit 1
[ "$($TMUX display -p -t a: '#{session_group}')" = a-2 ] || exit 1
[ "$($TMUX display -p -t c: '#{session_group}')" = a-2 ] || exit 1
[ "$($TMUX display -p -t b: '#{session_group_list}')" = b ] || exit 1
[ "$($TMUX display -p -t a: '#{window_id}')" = "$AW" ] || exit 1
[ "$($TMUX display -p -t c: '#{window_id}')" = "$AW" ] || exit 1
[ "$AW" != "$BW" ] || exit 1
# Window changes propagate only within the new group.
$TMUX new-window -d -t a || exit 1
[ "$($TMUX display -p -t c: '#{session_windows}')" = 2 ] || exit 1
[ "$($TMUX display -p -t b: '#{session_windows}')" = 1 ] || exit 1
# An existing session still joins its group; a group-only target still works.
$TMUX new -d -t c -s d || exit 1
[ "$($TMUX display -p -t d: '#{session_group}')" = a-2 ] || exit 1
$TMUX new -d -t a-1 -s e || exit 1
[ "$($TMUX display -p -t e: '#{session_group}')" = a-1 ] || exit 1
[ "$($TMUX display -p -t e: '#{window_id}')" = \
"$($TMUX display -p -t other: '#{window_id}')" ] || exit 1
exit 0

101
regress/tty-keys-backtab.sh Normal file
View File

@@ -0,0 +1,101 @@
#!/bin/sh
# Extended Shift+Tab must remain BackTab when the pane does not request
# modifyOtherKeys=2, including when the input carries implied Meta.
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 -y24 \
'stty raw -echo; exec cat -tv' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g assume-paste-time 0 || exit 1
$INNER set-option -s extended-keys 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 -y24 "$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
expected_output=
assert_key()
{
name=$1
expected_output=$expected_output$2
bytes=$(printf '%s' "$3" | od -An -v -tx1)
$OUTER send-keys -H $bytes || exit 1
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'"
}
pane_state='#{pane_key_mode}:#{pane_current_command}'
for mode in 0 1 2; do
for format in xterm csi-u; do
$INNER new-window \
"printf '\033[>4;${mode}m'; stty raw -echo; exec cat -tv" ||
exit 1
key_mode="Ext $mode"
[ "$mode" -eq 0 ] && key_mode=VT10x
i=0
while [ "$($INNER display-message -p "$pane_state")" != \
"$key_mode:cat" ]; do
[ "$i" -lt 100 ] || fail "pane did not enter mode $mode"
sleep 0.05
i=$((i + 1))
done
$INNER set-option -s extended-keys-format "$format" || exit 1
expected_output=
# Every modifier combination containing Shift.
for modifier in 2 4 6 8; do
if [ "$mode" -eq 2 ]; then
if [ "$format" = xterm ]; then
expected="^[[27;${modifier};9~"
else
expected="^[[9;${modifier}u"
fi
else
expected='^[[Z'
fi
case_name="mode $mode, $format, modifier $modifier"
assert_key "$case_name, CSI u input" "$expected" \
"$(printf '\033[9;%su' "$modifier")"
assert_key "$case_name, xterm input" "$expected" \
"$(printf '\033[27;%s;9~' "$modifier")"
done
done
done
exit 0

View File

@@ -43,8 +43,22 @@ def matches(name, value):
return "ignorefkeys" in features
if name == "XDA":
return kind == "ReplyTest"
if name == "XDA-kitty":
return kind == "kitty(0.43.0)" and "kitty" in features
if name in ("XDA-kitty", "XDA-kitty-nosync"):
required = {
"256", "RGB", "bpaste", "clipboard", "mouse",
"strikethrough", "title", "ccolour", "cstyle", "extkeys",
"focus", "hyperlinks", "osc7", "usstyle", "kittygraphics"}
if name == "XDA-kitty":
required.add("sync")
elif "sync" in features:
return False
return kind == "kitty(0.43.0)" and required <= features
if name == "XDA-ghostty":
return kind == "ghostty 1.2.0" and {
"kittygraphics", "margins", "sync", "usstyle", "progressbar"} <= features
if name == "XDA-WezTerm":
return kind == "WezTerm 20240203-110809-5046fc22" and {
"sixel", "margins", "usstyle"} <= features
if name == "sync":
return "sync" in features
if name == "characters":
@@ -57,6 +71,9 @@ reports = [
("DA2", b"\033[>85;1;0c"),
("XDA", b"\033P>|ReplyTest\033\\"),
("XDA-kitty", b"\033P>|kitty(0.43.0)\033\\"),
("XDA-kitty-nosync", b"\033P>|kitty(0.43.0)\033\\"),
("XDA-ghostty", b"\033P>|ghostty 1.2.0\033\\"),
("XDA-WezTerm", b"\033P>|WezTerm 20240203-110809-5046fc22\033\\"),
("sync", b"\033[?2026;2$y"),
("characters", b"\033[8;17;61t"),
("pixels", b"\033[4;480;800t"),
@@ -83,6 +100,8 @@ for name, report in reports:
else:
run("set-option", "-s", "escape-time", "60000")
run("set-option", "-g", "@seen", "")
if name == "XDA-kitty-nosync":
run("set-option", "-as", "terminal-features", ",*:sync@")
run("bind-key", "-n", "Escape", "set-option", "-gF",
"@seen", "#{@seen}E")
pid, fd = os.forkpty()
@@ -143,6 +162,12 @@ for name, report in reports:
assert actual == baseline, (context, "unexpected effect", actual)
else:
assert matches(name, actual), (context, "missing effect", actual)
if name in ("XDA-kitty", "XDA-kitty-nosync", "XDA-ghostty"):
assert run("display-message", "-p", "#{I/f:kittygraphics}") == "1", (
context, "kittygraphics feature lookup failed")
if name == "XDA-kitty-nosync":
assert run("display-message", "-p", "#{I/f:sync}") == "0", (
context, "kitty enabled synchronized updates")
assert run("show-option", "-gv", "@seen") == (
"E" if prefixed else ""), (context, "Escape count")
cases += 1