From ea6891fb8aebe0d8ab0337e5fbc5cd037b0106d8 Mon Sep 17 00:00:00 2001 From: Dhanraj E Date: Fri, 9 Oct 2026 20:38:17 +0530 Subject: [PATCH 01/22] Fix off-by-one buffer growth in sixel_print_add --- image-sixel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image-sixel.c b/image-sixel.c index a004d78f8..75dc5ee41 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -481,7 +481,7 @@ static void sixel_print_add(char **buf, size_t *len, size_t *used, const char *s, size_t slen) { - while (*used + slen >= *len + 1) { + while (*used + slen + 1 > *len) { *buf = xreallocarray(*buf, 2, *len); (*len) *= 2; } From e94426e02b1b3db2918c45b1e42e8ec0dc391d82 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 12 Oct 2026 08:55:11 +0100 Subject: [PATCH 02/22] Test for session group name. --- regress/session-group-name.sh | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 regress/session-group-name.sh diff --git a/regress/session-group-name.sh b/regress/session-group-name.sh new file mode 100644 index 000000000..979c04dd5 --- /dev/null +++ b/regress/session-group-name.sh @@ -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 From 634e96f8cc319e861b58459d44d4b9037d0f081f Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 12 Oct 2026 09:09:13 +0100 Subject: [PATCH 03/22] Tests for 5718. --- regress/input-raw-edit.sh | 2 +- regress/input-raw-unicode.sh | 12 ++++---- regress/input-reflow-wide.sh | 60 ++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 regress/input-reflow-wide.sh diff --git a/regress/input-raw-edit.sh b/regress/input-raw-edit.sh index 6d78cc3c4..e6e8938c8 100644 --- a/regress/input-raw-edit.sh +++ b/regress/input-raw-edit.sh @@ -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\]' diff --git a/regress/input-raw-unicode.sh b/regress/input-raw-unicode.sh index 76e2f5512..99530f151 100644 --- a/regress/input-raw-unicode.sh +++ b/regress/input-raw-unicode.sh @@ -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 diff --git a/regress/input-reflow-wide.sh b/regress/input-reflow-wide.sh new file mode 100644 index 000000000..a5646d25e --- /dev/null +++ b/regress/input-reflow-wide.sh @@ -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 From 12f8dddc7ca6f988a3c2ae7813154f280f0e8ca8 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 12 Oct 2026 10:37:25 +0100 Subject: [PATCH 04/22] =?UTF-8?q?Tests=20for=205730=20from=20Joan=20Fabr?= =?UTF-8?q?=C3=A9gat.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- regress/input-combine-width.sh | 88 ++++++++++++++++++++++++++++ regress/tty-keys-backtab.sh | 101 +++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 regress/input-combine-width.sh create mode 100644 regress/tty-keys-backtab.sh diff --git a/regress/input-combine-width.sh b/regress/input-combine-width.sh new file mode 100644 index 000000000..4584d6b71 --- /dev/null +++ b/regress/input-combine-width.sh @@ -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 diff --git a/regress/tty-keys-backtab.sh b/regress/tty-keys-backtab.sh new file mode 100644 index 000000000..b0d447533 --- /dev/null +++ b/regress/tty-keys-backtab.sh @@ -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 From b868c9aa1408feb504badcba0de51c43eea71cf0 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 12 Oct 2026 11:54:19 +0100 Subject: [PATCH 05/22] Tests for 5722-5725. --- regress/alt-screen-saved-cursor.sh | 87 ++++++++++++++++++++++++++++++ regress/client-exit-resize.sh | 72 +++++++++++++++++++++++++ regress/format-modifiers.sh | 22 ++++++-- regress/menu-pane-killed.sh | 29 ++++++++++ 4 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 regress/alt-screen-saved-cursor.sh create mode 100755 regress/client-exit-resize.sh create mode 100755 regress/menu-pane-killed.sh diff --git a/regress/alt-screen-saved-cursor.sh b/regress/alt-screen-saved-cursor.sh new file mode 100644 index 000000000..26ef20fef --- /dev/null +++ b/regress/alt-screen-saved-cursor.sh @@ -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 diff --git a/regress/client-exit-resize.sh b/regress/client-exit-resize.sh new file mode 100755 index 000000000..a1dfab023 --- /dev/null +++ b/regress/client-exit-resize.sh @@ -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 diff --git a/regress/format-modifiers.sh b/regress/format-modifiers.sh index 3fdfb59bd..648c19232 100644 --- a/regress/format-modifiers.sh +++ b/regress/format-modifiers.sh @@ -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 ---------------------------------------------------- diff --git a/regress/menu-pane-killed.sh b/regress/menu-pane-killed.sh new file mode 100755 index 000000000..4290ab6a6 --- /dev/null +++ b/regress/menu-pane-killed.sh @@ -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 From 8f7cf1fdef1c890fa60809d2377890a0e7d32858 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 12 Oct 2026 11:40:17 +0100 Subject: [PATCH 06/22] Add ncv test. --- regress/ncv.sh | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 regress/ncv.sh diff --git a/regress/ncv.sh b/regress/ncv.sh new file mode 100644 index 000000000..c1aafeee7 --- /dev/null +++ b/regress/ncv.sh @@ -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 From 12c93f50f06db6c6c7e1bd92d5c509cbc6e96751 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 07:52:05 +0000 Subject: [PATCH 07/22] Do not reuse old session group names when a new session is the same as its original session, GitHub issue 5704. --- cmd-new-session.c | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/cmd-new-session.c b/cmd-new-session.c index 7f7643bfe..83a027de9 100644 --- a/cmd-new-session.c +++ b/cmd-new-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-new-session.c,v 1.153 2026/10/09 13:12:14 nicm Exp $ */ +/* $OpenBSD: cmd-new-session.c,v 1.154 2026/10/12 07:52:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -82,7 +82,8 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) char *cause, *cwd = NULL, *cp, *ename; char *wname = NULL, *sname = NULL, *prefix = NULL; int detached, already_attached, is_control = 0; - u_int sx, sy, dsx, dsy, count = args_count(args); + u_int sx, sy, dsx, dsy, idx; + u_int count = args_count(args); struct spawn_context sc = { 0 }; enum cmd_retval retval; struct cmd_find_state fs; @@ -316,12 +317,17 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) * so add it to the group and synchronize. */ if (group != NULL) { - if (sg == NULL) { - if (groupwith != NULL) { - sg = session_group_new(groupwith->name); - session_group_add(sg, groupwith); - } else - sg = session_group_new(group); + if (sg == NULL && groupwith == NULL) + sg = session_group_new(group); + else if (sg == NULL) { + cp = xstrdup(groupwith->name); + for (idx = 1; session_group_find(cp) != NULL; idx++) { + free(cp); + xasprintf(&cp, "%s-%u", groupwith->name, idx); + } + sg = session_group_new(cp); + free(cp); + session_group_add(sg, groupwith); } session_group_add(sg, s); session_group_synchronize_to(s); From ef52b9e1cbdba16b673db75f8c73cfed7b307a28 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 08:08:37 +0000 Subject: [PATCH 08/22] Do not treat compact padding cells as width 1, make them width 0. GitHub issue 5718 from dustgle at gmail dot com. --- grid.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/grid.c b/grid.c index 19d1054ef..8af37d83d 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.159 2026/10/08 07:50:05 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.160 2026/10/12 08:08:37 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -42,7 +42,7 @@ const struct grid_cell grid_default_cell = { /* * Padding grid cell data. Padding cells are the only zero width cell that - * appears in the grid - because of this, they are always extended cells. + * appears in the grid. */ static const struct grid_cell grid_padding_cell = { { { '!' }, 0, 0, 0 }, 0, GRID_FLAG_PADDING, 8, 8, 8, 0 @@ -591,7 +591,10 @@ grid_get_cell1(struct grid_line *gl, u_int px, struct grid_cell *gc) if (gce->flags & GRID_FLAG_BG256) gc->bg |= COLOUR_FLAG_256; gc->us = 8; - utf8_set(&gc->data, gce->data.data); + if (gc->flags & GRID_FLAG_PADDING) + memcpy(&gc->data, &grid_padding_cell.data, sizeof gc->data); + else + utf8_set(&gc->data, gce->data.data); gc->link = 0; } From 8ba04e721c3141a2eb686ed24f6c8813808fb9e8 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 09:37:07 +0000 Subject: [PATCH 09/22] =?UTF-8?q?Clear=20implied=20Meta=20when=20discardin?= =?UTF-8?q?g=20BackTab=20modifiers,=20GitHub=20issue=205730=20from=20Joan?= =?UTF-8?q?=20Fabr=C3=A9gat.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- input-keys.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/input-keys.c b/input-keys.c index 3208a33c3..ee1e3aa87 100644 --- a/input-keys.c +++ b/input-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input-keys.c,v 1.114 2026/06/15 21:47:01 nicm Exp $ */ +/* $OpenBSD: input-keys.c,v 1.115 2026/10/12 09:37:07 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -621,7 +621,7 @@ input_key(struct screen *s, struct bufferevent *bev, key_code key) key = '\011' | (key & ~KEYC_MASK_KEY) | KEYC_SHIFT; } else { /* Otherwise clear modifiers. */ - key &= ~KEYC_MASK_MODIFIERS; + key &= ~(KEYC_MASK_MODIFIERS|KEYC_IMPLIED_META); } } From 801703a3ac85bc7738c6d48b791be82359707090 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 10:39:05 +0000 Subject: [PATCH 10/22] =?UTF-8?q?Respect=20the=20ncv=20capability=20and=20?= =?UTF-8?q?do=20not=20combine=20attributes=20and=20colours=20that=20will?= =?UTF-8?q?=20not=20work,=20reported=20by=20Joan=20Bruguera=20Mic=C3=B3=20?= =?UTF-8?q?in=20GitHub=20issue=205727.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tmux.h | 3 ++- tty-term.c | 3 ++- tty.c | 31 ++++++++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/tmux.h b/tmux.h index fdea4145a..65bd00761 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1455 2026/10/09 13:12:14 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1456 2026/10/12 10:39:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -629,6 +629,7 @@ enum tty_code_code { TTYC_KUP6, TTYC_KUP7, TTYC_MS, + TTYC_NCV, TTYC_NOBR, TTYC_OL, TTYC_OP, diff --git a/tty-term.c b/tty-term.c index f2d8b67b6..e34a872b4 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-term.c,v 1.111 2026/09/22 14:10:26 nicm Exp $ */ +/* $OpenBSD: tty-term.c,v 1.112 2026/10/12 10:39:05 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -251,6 +251,7 @@ static const struct tty_term_code_entry tty_term_codes[] = { [TTYC_KUP6] = { TTYCODE_STRING, "kUP6" }, [TTYC_KUP7] = { TTYCODE_STRING, "kUP7" }, [TTYC_MS] = { TTYCODE_STRING, "Ms" }, + [TTYC_NCV] = { TTYCODE_NUMBER, "ncv" }, [TTYC_NOBR] = { TTYCODE_STRING, "Nobr" }, [TTYC_OL] = { TTYCODE_STRING, "ol" }, [TTYC_OP] = { TTYCODE_STRING, "op" }, diff --git a/tty.c b/tty.c index 161747655..ad2853d42 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.485 2026/10/07 07:34:59 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.486 2026/10/12 10:39:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -51,6 +51,7 @@ static void tty_check_bg(struct tty *, struct colour_palette *, struct grid_cell *); static void tty_check_us(struct tty *, struct colour_palette *, struct grid_cell *); +static void tty_check_ncv(struct tty *, struct grid_cell *); static int tty_map_theme_colour(struct tty *, int); static void tty_colours_fg(struct tty *, const struct grid_cell *); static void tty_colours_bg(struct tty *, const struct grid_cell *); @@ -2350,6 +2351,33 @@ tty_dim_default_colour(struct tty *tty, int c, int foreground) return (c); } +static void +tty_check_ncv(struct tty *tty, struct grid_cell *gc) +{ + int n; + + if (COLOUR_DEFAULT(gc->fg) && COLOUR_DEFAULT(gc->bg)) + return; + + n = tty_term_number(tty->term, TTYC_NCV); + if (n & 2) + gc->attr &= ~GRID_ATTR_ALL_UNDERSCORE; + if (n & 4) + gc->attr &= ~GRID_ATTR_REVERSE; + if (n & 8) + gc->attr &= ~GRID_ATTR_BLINK; + if (n & 16) + gc->attr &= ~GRID_ATTR_DIM; + if (n & 32) + gc->attr &= ~GRID_ATTR_BRIGHT; + if (n & 64) + gc->attr &= ~GRID_ATTR_HIDDEN; + if (n & 256) + gc->attr &= ~GRID_ATTR_CHARSET; + if (n & 32768) + gc->attr &= ~GRID_ATTR_ITALICS; +} + void tty_attributes(struct tty *tty, const struct grid_cell *gc, const struct tty_style_ctx *style_ctx) @@ -2420,6 +2448,7 @@ tty_attributes(struct tty *tty, const struct grid_cell *gc, tty_check_fg(tty, palette, &gc2); tty_check_bg(tty, palette, &gc2); tty_check_us(tty, palette, &gc2); + tty_check_ncv(tty, &gc2); /* * If any bits are being cleared or the underline colour is now default, From 40a1901bada25279a438a83ab2add0db52247689 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 12 Oct 2026 13:19:29 +0100 Subject: [PATCH 11/22] Change --enable-sixel to --enable-images (the former stays as an alias). --- Makefile.am | 4 ++-- configure.ac | 25 +++++++++++++++++-------- format.c | 2 +- input.c | 8 ++++---- screen-redraw.c | 2 +- screen-write.c | 38 +++++++++++++++++++------------------- screen.c | 18 +++++++++--------- tmux.h | 14 +++++++------- tty.c | 8 ++++---- 9 files changed, 64 insertions(+), 55 deletions(-) diff --git a/Makefile.am b/Makefile.am index 349d6e224..5a561819e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -249,8 +249,8 @@ if HAVE_UTF8PROC nodist_tmux_SOURCES += compat/utf8proc.c endif -# Enable sixel support. -if ENABLE_SIXEL +# Enable image support. +if ENABLE_IMAGES dist_tmux_SOURCES += image.c image-sixel.c endif diff --git a/configure.ac b/configure.ac index 5da5f30af..703327bcf 100644 --- a/configure.ac +++ b/configure.ac @@ -541,15 +541,24 @@ if test "x$enable_cgroups" = xyes; then fi fi -# Enable sixel support. +# Enable image support. +AC_ARG_ENABLE( + images, + AS_HELP_STRING(--enable-images, enable image support) +) AC_ARG_ENABLE( sixel, - AS_HELP_STRING(--enable-sixel, enable sixel images) + AS_HELP_STRING(--enable-sixel, alias for --enable-images), + [ + if test "x$enable_images" = x; then + enable_images=$enable_sixel + fi + ] ) -if test "x$enable_sixel" = xyes; then - AC_DEFINE(ENABLE_SIXEL) +if test "x$enable_images" = xyes; then + AC_DEFINE(ENABLE_IMAGES) fi -AM_CONDITIONAL(ENABLE_SIXEL, [test "x$enable_sixel" = xyes]) +AM_CONDITIONAL(ENABLE_IMAGES, [test "x$enable_images" = xyes]) # Check for b64_ntop. If we have b64_ntop, we assume b64_pton as well. AC_MSG_CHECKING(for b64_ntop) @@ -1148,10 +1157,10 @@ if test "x$enable_asan" = xyes; then else AC_MSG_NOTICE([ASAN: off]) fi -if test "x$enable_sixel" = xyes; then - AC_MSG_NOTICE([SIXEL: on]) +if test "x$enable_images" = xyes; then + AC_MSG_NOTICE([images: on]) else - AC_MSG_NOTICE([SIXEL: off]) + AC_MSG_NOTICE([images: off]) fi if test "x$enable_debug" = xyes; then AC_MSG_NOTICE([debug: on]) diff --git a/format.c b/format.c index 26a64f964..cb9e34061 100644 --- a/format.c +++ b/format.c @@ -3074,7 +3074,7 @@ format_cb_version(__unused struct format_tree *ft) static void * format_cb_sixel_support(__unused struct format_tree *ft) { -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES return (xstrdup("1")); #else return (xstrdup("0")); diff --git a/input.c b/input.c index c38d12560..9bd03cf4e 100644 --- a/input.c +++ b/input.c @@ -1582,7 +1582,7 @@ input_csi_dispatch(struct input_ctx *ictx) case -1: break; case 0: -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES input_reply(ictx, 1, "\033[?1;2;4c"); #else input_reply(ictx, 1, "\033[?1;2c"); @@ -2100,7 +2100,7 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx) static void input_csi_dispatch_sm_graphics(__unused struct input_ctx *ictx) { -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES int n, m, o; if (ictx->param_list_len > 3) @@ -2625,7 +2625,7 @@ input_dcs_dispatch(struct input_ctx *ictx) const char prefix[] = "tmux;"; const u_int prefixlen = (sizeof prefix) - 1; long long allow_passthrough = 0; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES struct window *w; struct sixel_image *si; int p2; @@ -2641,7 +2641,7 @@ input_dcs_dispatch(struct input_ctx *ictx) return (0); } -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (wp != NULL && buf[0] == 'q' && ictx->interm_len == 0) { w = wp->window; if (input_split(ictx) != 0) diff --git a/screen-redraw.c b/screen-redraw.c index e4fb93281..9d9704f98 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1921,7 +1921,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) tty_reset(tty); -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (wp != NULL) tty_draw_images(c, wp); else { diff --git a/screen-write.c b/screen-write.c index 754fba547..bc2cedd2c 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1471,7 +1471,7 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx) memcpy(&gc, &grid_default_cell, sizeof gc); utf8_set(&gc.data, 'E'); -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_free_all(s) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -1518,7 +1518,7 @@ screen_write_insertcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) if (s->cx > screen_size_x(s) - 1) return; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_check_line(s, s->cy, 1) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -1559,7 +1559,7 @@ screen_write_deletecharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) if (s->cx > screen_size_x(s) - 1) return; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_check_line(s, s->cy, 1) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -1600,7 +1600,7 @@ screen_write_clearcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) if (s->cx > screen_size_x(s) - 1) return; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_check_line(s, s->cy, 1) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -1635,7 +1635,7 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg) if (ny == 0) ny = 1; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -1703,7 +1703,7 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg) if (ny == 0) ny = 1; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -1776,7 +1776,7 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg) if (gl->cellsize == 0 && COLOUR_DEFAULT(bg)) return; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_check_line(s, s->cy, 1) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -1815,7 +1815,7 @@ screen_write_clearendofline(struct screen_write_ctx *ctx, u_int bg) if (s->cx > sx - 1 || (s->cx >= gl->cellsize && COLOUR_DEFAULT(bg))) return; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_check_line(s, s->cy, 1) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -1842,7 +1842,7 @@ screen_write_clearstartofline(struct screen_write_ctx *ctx, u_int bg) return; } -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_check_line(s, s->cy, 1) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -1896,7 +1896,7 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg) return; } -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_free_all(s) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -1948,7 +1948,7 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg) struct screen *s = ctx->s; struct grid *gd = s->grid; struct grid_line *gl; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES int redraw = 0; #endif u_int rupper = s->rupper, rlower = s->rlower; @@ -1971,7 +1971,7 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg) return; } -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (rlower == screen_size_y(s) - 1) redraw = image_scroll_up(s, 1); else @@ -2003,7 +2003,7 @@ screen_write_scrollup(struct screen_write_ctx *ctx, u_int lines, u_int bg) ctx->bg = bg; } -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_scroll_up(s, lines) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -2032,7 +2032,7 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg) else if (lines > s->rlower - s->rupper + 1) lines = s->rlower - s->rupper + 1; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_free_all(s) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -2073,7 +2073,7 @@ screen_write_clearendofscreen(struct screen_write_ctx *ctx, u_int bg) struct visible_ranges *r; struct visible_range *ri; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -2154,7 +2154,7 @@ screen_write_clearstartofscreen(struct screen_write_ctx *ctx, u_int bg) struct visible_ranges *r; struct visible_range *ri; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_check_line(s, 0, s->cy - 1) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -2227,7 +2227,7 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg) struct visible_ranges *r; struct visible_range *ri; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_free_all(s) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -2735,7 +2735,7 @@ screen_write_collect_end(struct screen_write_ctx *ctx) } } -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES if (image_check_area(s, s->cx, s->cy, ci->used, 1) && ctx->wp != NULL) ctx->wp->flags |= PANE_REDRAW; #endif @@ -3239,7 +3239,7 @@ screen_write_rawstring(struct screen_write_ctx *ctx, u_char *str, u_int len, tty_write(tty_cmd_rawstring, &ttyctx); } -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES /* Write a SIXEL image. */ void screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si, diff --git a/screen.c b/screen.c index f9ca89af5..7120e1058 100644 --- a/screen.c +++ b/screen.c @@ -93,7 +93,7 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit) s->tabs = NULL; s->sel = NULL; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES TAILQ_INIT(&s->images); TAILQ_INIT(&s->saved_images); #endif @@ -132,7 +132,7 @@ screen_reinit(struct screen *s, int check) screen_clear_selection(s); screen_free_titles(s); -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES image_free_all(s); #endif @@ -154,7 +154,7 @@ screen_reset_hyperlinks(struct screen *s) void screen_free(struct screen *s) { -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES struct image *im; #endif @@ -174,7 +174,7 @@ screen_free(struct screen *s) hyperlinks_free(s->hyperlinks); screen_free_titles(s); -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES /* * Images saved when entering the alternate screen stay linked in the * global list; move them back so they are freed and unlinked here, or @@ -377,7 +377,7 @@ screen_resize_cursor(struct screen *s, u_int sx, u_int sy, int reflow, if (sy != screen_size_y(s)) screen_resize_y(s, sy, eat_empty, &cy); -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES image_free_all(s); #endif @@ -692,7 +692,7 @@ int screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor) { u_int sx, sy; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES struct image *im; #endif @@ -709,7 +709,7 @@ screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor) } memcpy(&s->saved_cell, gc, sizeof s->saved_cell); -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES TAILQ_CONCAT(&s->saved_images, &s->images, entry); TAILQ_FOREACH(im, &s->saved_images, entry) im->list = &s->saved_images; @@ -728,7 +728,7 @@ int screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor) { u_int sx = screen_size_x(s), sy = screen_size_y(s); -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES struct image *im; #endif @@ -774,7 +774,7 @@ screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor) grid_destroy(s->saved_grid); s->saved_grid = NULL; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES image_free_all(s); TAILQ_CONCAT(&s->images, &s->saved_images, entry); TAILQ_FOREACH(im, &s->images, entry) diff --git a/tmux.h b/tmux.h index 5b6ef46cd..e7aa55eef 100644 --- a/tmux.h +++ b/tmux.h @@ -77,7 +77,7 @@ struct screen_write_cline; struct screen_write_ctx; struct session; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES struct sixel_image; #endif @@ -1037,7 +1037,7 @@ struct style { u_int link; }; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES /* Image. */ struct image { struct screen *s; @@ -1113,7 +1113,7 @@ struct screen { bitstr_t *tabs; struct screen_sel *sel; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES struct images images; struct images saved_images; #endif @@ -1904,7 +1904,7 @@ struct tty_ctx { size_t size; } sel; -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES struct image *image; #endif }; @@ -3005,7 +3005,7 @@ void tty_cmd_scrolldown(struct tty *, const struct tty_ctx *); void tty_cmd_reverseindex(struct tty *, const struct tty_ctx *); void tty_cmd_setselection(struct tty *, const struct tty_ctx *); void tty_cmd_rawstring(struct tty *, const struct tty_ctx *); -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES void tty_cmd_sixelimage(struct tty *, const struct tty_ctx *); void tty_draw_images(struct client *, struct window_pane *); #endif @@ -3649,7 +3649,7 @@ void screen_write_setselection(struct screen_write_ctx *, const char *, u_char *, u_int); void screen_write_rawstring(struct screen_write_ctx *, u_char *, u_int, int); -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES void screen_write_sixelimage(struct screen_write_ctx *, struct sixel_image *, u_int); #endif @@ -4237,7 +4237,7 @@ void spawn_editor_finish(struct window_pane *); /* regsub.c */ char *regsub(const char *, const char *, const char *, int); -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES /* image.c */ int image_free_all(struct screen *); struct image *image_store(struct screen *, struct sixel_image *); diff --git a/tty.c b/tty.c index 817d46ff8..494e5634f 100644 --- a/tty.c +++ b/tty.c @@ -68,7 +68,7 @@ static void tty_emulate_repeat(struct tty *, enum tty_code_code, enum tty_code_code, u_int); static void tty_draw_pane(struct tty *, const struct tty_ctx *, u_int); -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES static void tty_write_one(void (*)(struct tty *, const struct tty_ctx *), struct client *, struct tty_ctx *); #endif @@ -1460,7 +1460,7 @@ tty_check_codeset(struct tty *tty, const struct grid_cell *gc) return (&new); } -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES /* Update context for client. */ static int tty_set_client_cb(struct tty_ctx *ttyctx, struct client *c) @@ -1588,7 +1588,7 @@ tty_write(void (*cmdfn)(struct tty *, const struct tty_ctx *), } } -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES /* Only write to the incoming tty instead of every client. */ static void tty_write_one(void (*cmdfn)(struct tty *, const struct tty_ctx *), @@ -2034,7 +2034,7 @@ tty_cmd_rawstring(struct tty *tty, const struct tty_ctx *ctx) tty_invalidate(tty); } -#ifdef ENABLE_SIXEL +#ifdef ENABLE_IMAGES void tty_cmd_sixelimage(struct tty *tty, const struct tty_ctx *ctx) { From fcb9ac2b2bc812d4c7d139f21ce6cef8b7298d3b Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 12 Oct 2026 13:49:23 +0100 Subject: [PATCH 12/22] Reject invalid and out of range numbers. --- image-sixel.c | 51 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/image-sixel.c b/image-sixel.c index 75dc5ee41..0b7fa7512 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -18,6 +18,7 @@ #include +#include #include #include @@ -133,6 +134,21 @@ sixel_parse_write(struct sixel_image *si, u_int ch) return (0); } +static int +sixel_parse_number(const char *cp, char **endptr, u_int *value) +{ + unsigned long long number; + + if (*cp < '0' || *cp > '9') + return (1); + errno = 0; + number = strtoull(cp, endptr, 10); + if (errno == ERANGE || number > UINT_MAX) + return (1); + *value = number; + return (0); +} + static const char * sixel_parse_attributes(struct sixel_image *si, const char *cp, const char *end) { @@ -157,7 +173,10 @@ sixel_parse_attributes(struct sixel_image *si, const char *cp, const char *end) return (NULL); } - x = strtoul(endptr + 1, &endptr, 10); + if (sixel_parse_number(endptr + 1, &endptr, &x) != 0) { + log_debug("%s: invalid number", __func__); + return (NULL); + } if (endptr == last || *endptr != ';') { log_debug("%s: missing ;", __func__); return (NULL); @@ -166,7 +185,10 @@ sixel_parse_attributes(struct sixel_image *si, const char *cp, const char *end) log_debug("%s: image is too wide", __func__); return (NULL); } - y = strtoul(endptr + 1, &endptr, 10); + if (sixel_parse_number(endptr + 1, &endptr, &y) != 0) { + log_debug("%s: invalid number", __func__); + return (NULL); + } if (endptr != last) { log_debug("%s: extra ;", __func__); return (NULL); @@ -200,7 +222,10 @@ sixel_parse_colour(struct sixel_image *si, const char *cp, const char *end) last++; } - c = strtoul(cp, &endptr, 10); + if (sixel_parse_number(cp, &endptr, &c) != 0) { + log_debug("%s: invalid number", __func__); + return (NULL); + } if (c > SIXEL_COLOUR_REGISTERS) { log_debug("%s: too many colours", __func__); return (NULL); @@ -211,22 +236,34 @@ sixel_parse_colour(struct sixel_image *si, const char *cp, const char *end) if (endptr == last || *endptr != ';') return (last); - type = strtoul(endptr + 1, &endptr, 10); + if (sixel_parse_number(endptr + 1, &endptr, &type) != 0) { + log_debug("%s: invalid number", __func__); + return (NULL); + } if (endptr == last || *endptr != ';') { log_debug("%s: missing ;", __func__); return (NULL); } - c1 = strtoul(endptr + 1, &endptr, 10); + if (sixel_parse_number(endptr + 1, &endptr, &c1) != 0) { + log_debug("%s: invalid number", __func__); + return (NULL); + } if (endptr == last || *endptr != ';') { log_debug("%s: missing ;", __func__); return (NULL); } - c2 = strtoul(endptr + 1, &endptr, 10); + if (sixel_parse_number(endptr + 1, &endptr, &c2) != 0) { + log_debug("%s: invalid number", __func__); + return (NULL); + } if (endptr == last || *endptr != ';') { log_debug("%s: missing ;", __func__); return (NULL); } - c3 = strtoul(endptr + 1, &endptr, 10); + if (sixel_parse_number(endptr + 1, &endptr, &c3) != 0) { + log_debug("%s: invalid number", __func__); + return (NULL); + } if (endptr != last) { log_debug("%s: missing ;", __func__); return (NULL); From 5bfa83d1fa7ec8a1407b7c40a3fdfad8002fa4e9 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 10:46:12 +0000 Subject: [PATCH 13/22] Do not use a dead pane when redrawing a menu, from someone in GitHub issue 5722. --- menu.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/menu.c b/menu.c index cb087296e..b7917fa72 100644 --- a/menu.c +++ b/menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD: menu.c,v 1.71 2026/09/21 12:14:32 nicm Exp $ */ +/* $OpenBSD: menu.c,v 1.72 2026/10/12 10:46:12 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -194,10 +194,19 @@ static void menu_reapply_styles(struct menu_data *md) { struct options *o = md->w->options; + struct session *s = NULL; + struct winlink *wl = NULL; + struct window_pane *wp = NULL; struct format_tree *ft; struct style sytmp; - ft = format_create_defaults(NULL, NULL, md->fs.s, md->fs.wl, md->fs.wp); + /* The pane (or session) the menu was opened for may be gone. */ + if (cmd_find_valid_state(&md->fs)) { + s = md->fs.s; + wl = md->fs.wl; + wp = md->fs.wp; + } + ft = format_create_defaults(NULL, NULL, s, wl, wp); /* Reapply menu style from options. */ memcpy(&md->style_gc, &grid_default_cell, sizeof md->style_gc); From ddddd25d704840826bac6f43f17ee0a896b6a8d9 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 10:48:05 +0000 Subject: [PATCH 14/22] Ignore a resize from a client whose tty is closed, from someone in GitHub issue 5723. --- server-client.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server-client.c b/server-client.c index 214ebe561..a71574f4b 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.519 2026/10/09 10:16:02 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.520 2026/10/12 10:48:05 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -2604,7 +2604,7 @@ server_client_dispatch(struct imsg *imsg, void *arg) if (datalen != 0) goto bad; - if (c->flags & CLIENT_CONTROL) + if ((c->flags & CLIENT_CONTROL) || (~c->tty.flags & TTY_OPENED)) break; server_client_update_latest(c); old_sx = c->tty.sx; From 7a2ea39a3e22ea154efc3f6ff11f2c7b2d6593a8 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 10:50:06 +0000 Subject: [PATCH 15/22] Keep the row RM 1049 restores inside the saved grid, from someone in GitHub issue 5724. --- screen.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/screen.c b/screen.c index 2533beb5c..402c4fba2 100644 --- a/screen.c +++ b/screen.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen.c,v 1.107 2026/07/26 09:20:54 nicm Exp $ */ +/* $OpenBSD: screen.c,v 1.108 2026/10/12 10:50:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -717,6 +717,10 @@ screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor) return 0; } + /* Clamp cursor to the current screen size. */ + if (s->cy > screen_size_y(s) - 1) + s->cy = screen_size_y(s) - 1; + /* Restore the saved grid. */ grid_duplicate_lines(s->grid, screen_hsize(s), s->saved_grid, 0, s->saved_grid->sy); From 4caff240c8936dc963510da7b221beeeb222dc34 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 10:53:59 +0000 Subject: [PATCH 16/22] Fail an integer format expression with a NaN or out-of-range value, from someone in GitHub issue 5725. --- format.c | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/format.c b/format.c index 4c4ede394..596367edc 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.425 2026/10/05 10:59:37 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.426 2026/10/12 10:53:59 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -5785,6 +5785,17 @@ format_loop_clients(struct format_expand_state *es, const char *fmt) return (value); } +/* Truncate a value to a long long, failing if it is NaN or out of range. */ +static int +format_truncate_integer(double *v) +{ + /* (double)LLONG_MAX is 2^63, which does not fit; -2^63 does. */ + if (isnan(*v) || *v >= (double)LLONG_MAX || *v < (double)LLONG_MIN) + return (-1); + *v = (long long)*v; + return (0); +} + static char * format_replace_expression(struct format_modifier *mexp, struct format_expand_state *es, const char *copy) @@ -5870,9 +5881,11 @@ format_replace_expression(struct format_modifier *mexp, goto fail; } - if (!use_fp) { - mleft = (long long)mleft; - mright = (long long)mright; + if (!use_fp && + (format_truncate_integer(&mleft) != 0 || + format_truncate_integer(&mright) != 0)) { + format_log(es, "expression side is out of range"); + goto fail; } format_log(es, "expression left side is: %.*f", prec, mleft); format_log(es, "expression right side is: %.*f", prec, mright); @@ -5912,10 +5925,11 @@ format_replace_expression(struct format_modifier *mexp, result = (mleft <= mright); break; } - if (use_fp) - xasprintf(&value, "%.*f", prec, result); - else - xasprintf(&value, "%.*f", prec, (double)(long long)result); + if (!use_fp && format_truncate_integer(&result) != 0) { + format_log(es, "expression result is out of range"); + goto fail; + } + xasprintf(&value, "%.*f", prec, result); format_log(es, "expression result is %s", value); free(right); From 7fe3e928dd6cae776568aa9818755bc9ce256058 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 12:22:42 +0000 Subject: [PATCH 17/22] Add image_support to replace sixel_support variable. --- format.c | 11 +++++++---- tmux.1 | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/format.c b/format.c index 596367edc..361ae20bf 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.426 2026/10/12 10:53:59 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.427 2026/10/12 12:22:42 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -3056,9 +3056,9 @@ format_cb_version(__unused struct format_tree *ft) return (xstrdup(getversion())); } -/* Callback for sixel_support. */ +/* Callback for image_support. */ static void * -format_cb_sixel_support(__unused struct format_tree *ft) +format_cb_image_support(__unused struct format_tree *ft) { return (xstrdup("0")); } @@ -3735,6 +3735,9 @@ static const struct format_table_entry format_table[] = { { "host_short", FORMAT_TABLE_STRING, format_cb_host_short }, + { "image_support", FORMAT_TABLE_STRING, + format_cb_image_support + }, { "insert_flag", FORMAT_TABLE_STRING, format_cb_insert_flag }, @@ -4072,7 +4075,7 @@ static const struct format_table_entry format_table[] = { format_cb_session_windows }, { "sixel_support", FORMAT_TABLE_STRING, - format_cb_sixel_support + format_cb_image_support }, { "socket_path", FORMAT_TABLE_STRING, format_cb_socket_path diff --git a/tmux.1 b/tmux.1 index ac47986a5..a250197af 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1182 2026/10/09 13:12:14 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1183 2026/10/12 12:22:42 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: October 9 2026 $ +.Dd $Mdocdate: October 12 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -7457,6 +7457,7 @@ The following variables are available, where appropriate: .It Li "hook_window_name" Ta "" Ta "Name of window where hook was run, if any" .It Li "host" Ta "#H" Ta "Hostname of local host" .It Li "host_short" Ta "#h" Ta "Hostname of local host (no domain name)" +.It Li "image_support" Ta "" Ta "1 if server has support for images" .It Li "insert_flag" Ta "" Ta "Pane insert flag" .It Li "key_string" Ta "" Ta "String representation of the key binding" .It Li "key_repeat" Ta "" Ta "1 if key binding is repeatable" @@ -7611,7 +7612,6 @@ The following variables are available, where appropriate: .It Li "session_stack" Ta "" Ta "Window indexes in most recent order" .It Li "session_windows" Ta "" Ta "Number of windows in session" .It Li "socket_path" Ta "" Ta "Server socket path" -.It Li "sixel_support" Ta "" Ta "1 if server has support for SIXEL" .It Li "start_time" Ta "" Ta "Server start time" .It Li "synchronized_output_flag" Ta "" Ta "1 if pane has synchronized output enabled" .It Li "uid" Ta "" Ta "Server UID" From 4d2e4e52e8ea108cc65413cf317f7871a3813292 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 12:26:46 +0000 Subject: [PATCH 18/22] Add Kitty feature detection and sixel to WezTerm. --- tty-features.c | 14 +++++++++++++- tty-keys.c | 4 +++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tty-features.c b/tty-features.c index e3c9f19b2..ef0fdc53c 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-features.c,v 1.46 2026/09/24 08:16:13 nicm Exp $ */ +/* $OpenBSD: tty-features.c,v 1.47 2026/10/12 12:26:46 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -628,6 +628,7 @@ tty_default_features(struct client *c, const char *name, u_int version) "focus," "hyperlinks," "margins," + "sixel," "usstyle" }, { .name = "ghostty", @@ -644,6 +645,17 @@ tty_default_features(struct client *c, const char *name, u_int version) "usstyle," "progressbar" }, + { .name = "kitty", + .features = TTY_FEATURES_BASE_MODERN_XTERM "," + "ccolour," + "cstyle," + "extkeys," + "focus," + "hyperlinks," + "osc7," + "sync," + "usstyle" + }, { .name = "Rio", .features = TTY_FEATURES_BASE_MODERN_XTERM "," "ccolour," diff --git a/tty-keys.c b/tty-keys.c index 96c73f1b6..b9ca2f019 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-keys.c,v 1.216 2026/10/06 17:49:45 nicm Exp $ */ +/* $OpenBSD: tty-keys.c,v 1.217 2026/10/12 12:26:46 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1675,6 +1675,8 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, tty_default_features(c, "mintty", 0); else if (strncmp(tmp, "foot(", 5) == 0) tty_default_features(c, "foot", 0); + else if (strncmp(tmp, "kitty(", 6) == 0) + tty_default_features(c, "kitty", 0); else if (strncmp(tmp, "WezTerm ", 7) == 0) tty_default_features(c, "WezTerm", 0); else if (strncmp(tmp, "ghostty ", 8) == 0) From b865fe844d20a73174202095915c1e52aacf3121 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 12:35:03 +0000 Subject: [PATCH 19/22] Add kittygraphics terminal feature. --- tmux.1 | 4 +++- tmux.h | 3 ++- tty-features.c | 16 +++++++++++++--- window-client.c | 15 ++++++++------- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/tmux.1 b/tmux.1 index a250197af..b19c2bb16 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1183 2026/10/12 12:22:42 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1184 2026/10/12 12:35:03 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -5198,6 +5198,8 @@ Ignore function keys from and use the .Nm internal set only. +.It kittygraphics +Supports the Kitty graphics protocol with Unicode placeholders. .It margins Supports DECSLRM margins. .It mouse diff --git a/tmux.h b/tmux.h index 65bd00761..b74ac1889 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1456 2026/10/12 10:39:05 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1457 2026/10/12 12:35:03 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1730,6 +1730,7 @@ struct tty_term { #define TERM_SIXEL 0x40 #define TERM_INVALIDMS 0x80 #define TERM_NOREPLACE 0x100 +#define TERM_KITTYGRAPHICS 0x200 int flags; LIST_ENTRY(tty_term) entry; diff --git a/tty-features.c b/tty-features.c index ef0fdc53c..80af558d0 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-features.c,v 1.47 2026/10/12 12:26:46 nicm Exp $ */ +/* $OpenBSD: tty-features.c,v 1.48 2026/10/12 12:35:03 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -359,6 +359,13 @@ static const struct tty_feature tty_feature_sixel = { TERM_SIXEL }; +/* Terminal supports Kitty graphics Unicode placeholders. */ +static const struct tty_feature tty_feature_kittygraphics = { + "kittygraphics", + NULL, + TERM_KITTYGRAPHICS +}; + /* Terminal supports the OSC 9;4 progress bar. */ static const char *const tty_feature_progressbar_capabilities[] = { "Spb=\\E]9;4;%p1%d;%p2%d\\E\\\\", @@ -389,6 +396,7 @@ static const struct tty_feature *const tty_features[] = { &tty_feature_extkeys, &tty_feature_focus, &tty_feature_ignorefkeys, + &tty_feature_kittygraphics, &tty_feature_margins, &tty_feature_mouse, &tty_feature_osc7, @@ -643,7 +651,8 @@ tty_default_features(struct client *c, const char *name, u_int version) "osc7," "sync," "usstyle," - "progressbar" + "progressbar," + "kittygraphics" }, { .name = "kitty", .features = TTY_FEATURES_BASE_MODERN_XTERM "," @@ -654,7 +663,8 @@ tty_default_features(struct client *c, const char *name, u_int version) "hyperlinks," "osc7," "sync," - "usstyle" + "usstyle," + "kittygraphics" }, { .name = "Rio", .features = TTY_FEATURES_BASE_MODERN_XTERM "," diff --git a/window-client.c b/window-client.c index 3c2f81097..05bb2f801 100644 --- a/window-client.c +++ b/window-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-client.c,v 1.49 2026/08/31 12:41:03 kirill Exp $ */ +/* $OpenBSD: window-client.c,v 1.50 2026/10/12 12:35:03 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -89,19 +89,20 @@ static const char *window_client_info_lines[] = { " #[#{E:tree-mode-border-style},acs]x#[default] " WINDOW_CLIENT_FEATURE(hyperlinks) " " WINDOW_CLIENT_FEATURE(ignorefkeys) " " - WINDOW_CLIENT_FEATURE(margins) " " - WINDOW_CLIENT_FEATURE(mouse), + WINDOW_CLIENT_FEATURE(kittygraphics) " " + WINDOW_CLIENT_FEATURE(margins), " #[#{E:tree-mode-border-style},acs]x#[default] " + WINDOW_CLIENT_FEATURE(mouse) " " WINDOW_CLIENT_FEATURE(osc7) " " WINDOW_CLIENT_FEATURE(overline) " " - WINDOW_CLIENT_FEATURE(progressbar) " " - WINDOW_CLIENT_FEATURE(rectfill), + WINDOW_CLIENT_FEATURE(progressbar), " #[#{E:tree-mode-border-style},acs]x#[default] " + WINDOW_CLIENT_FEATURE(rectfill) " " WINDOW_CLIENT_FEATURE(sixel) " " WINDOW_CLIENT_FEATURE(strikethrough) " " - WINDOW_CLIENT_FEATURE(sync) " " - WINDOW_CLIENT_FEATURE(title), + WINDOW_CLIENT_FEATURE(sync), " #[#{E:tree-mode-border-style},acs]x#[default] " + WINDOW_CLIENT_FEATURE(title) " " WINDOW_CLIENT_FEATURE(usstyle) " " WINDOW_CLIENT_FEATURE(utf8), "#[#{E:tree-mode-border-style},acs]qqqqqqqqqqqqqqn#{R:q,#{window_width}}#[default]", From 89e2015fb2756b9ae027c7154562f127ed807cd1 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 12 Oct 2026 13:08:19 +0000 Subject: [PATCH 20/22] Preserve pixel sizes with manual sizing. --- resize.c | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/resize.c b/resize.c index 67562522d..108f7c318 100644 --- a/resize.c +++ b/resize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: resize.c,v 1.59 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: resize.c,v 1.60 2026/10/12 13:08:19 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -152,7 +152,13 @@ clients_calculate_size(int type, int current, struct client *c, *sx = UINT_MAX; *sy = UINT_MAX; } - *xpixel = *ypixel = 0; + if (w != NULL && type == WINDOW_SIZE_MANUAL) { + *xpixel = w->xpixel; + *ypixel = w->ypixel; + } else { + *xpixel = 0; + *ypixel = 0; + } /* * For latest, count the number of clients with this window. We only @@ -380,16 +386,30 @@ recalculate_size(struct window *w, int now) /* Look for a suitable client and get the new size. */ changed = clients_calculate_size(type, current, NULL, NULL, w, recalculate_size_skip_client, &sx, &sy, &xpixel, &ypixel); + if (xpixel == 0) + xpixel = DEFAULT_XPIXEL; + if (ypixel == 0) + ypixel = DEFAULT_YPIXEL; /* * Make sure the size has actually changed. If the window has already * got a resize scheduled, then use the new size; otherwise the old. */ if (w->flags & WINDOW_RESIZE) { - if (!now && changed && w->new_sx == sx && w->new_sy == sy) + if (!now && + changed && + w->new_sx == sx && + w->new_sy == sy && + w->new_xpixel == xpixel && + w->new_ypixel == ypixel) changed = 0; } else { - if (!now && changed && w->sx == sx && w->sy == sy) + if (!now && + changed && + w->sx == sx && + w->sy == sy && + w->xpixel == xpixel && + w->ypixel == ypixel) changed = 0; } From 30791b73764ee9f512107abf9d1979e5776ba274 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 12 Oct 2026 16:36:04 +0100 Subject: [PATCH 21/22] Tests for image bits. --- regress/format-variables.sh | 1 + regress/tty-keys-replies.sh | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/regress/format-variables.sh b/regress/format-variables.sh index 6908ad956..dced1a711 100644 --- a/regress/format-variables.sh +++ b/regress/format-variables.sh @@ -79,6 +79,7 @@ history_limit history_size host host_short +image_support insert_flag keypad_cursor_flag keypad_flag diff --git a/regress/tty-keys-replies.sh b/regress/tty-keys-replies.sh index e67717c7a..b47e6a8a9 100755 --- a/regress/tty-keys-replies.sh +++ b/regress/tty-keys-replies.sh @@ -43,6 +43,22 @@ def matches(name, value): return "ignorefkeys" in features if name == "XDA": return kind == "ReplyTest" + 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": @@ -54,6 +70,10 @@ reports = [ ("DA", b"\033[?65;4;21;28;52c"), ("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"), @@ -80,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() @@ -140,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 From 58eb73a6d1bac0919f3f264c057e4c7b4348f9e4 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 12 Oct 2026 16:37:21 +0100 Subject: [PATCH 22/22] Fix DA test with images on. --- regress/input-replies.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/regress/input-replies.sh b/regress/input-replies.sh index 5fc77f843..1f260ca00 100644 --- a/regress/input-replies.sh +++ b/regress/input-replies.sh @@ -60,7 +60,11 @@ query_timeout() query "dsr-ok" '^[[0n' '\033[5n' 4 '' query "dsr-cursor" '^[[1;1R' '\033[6n' 6 '' -query "da-primary" '^[[?1;2c' '\033[c' 7 '' +if [ "$($TMUX display-message -p '#{image_support}')" = 1 ]; then + query "da-primary" '^[[?1;2;4c' '\033[c' 9 '' +else + query "da-primary" '^[[?1;2c' '\033[c' 7 '' +fi query "da-secondary" '^[[>84;0;0c' '\033[>c' 10 '' query "decrqm-irm-reset" '^[[4;2$y' '\033[4$p' 7 '' query "decrqm-irm-set" '^[[4;1$y' '\033[4$p' 7 '\033[4h'