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

@@ -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 <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -82,7 +82,8 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item)
char *cause, *cwd = NULL, *cp, *ename; char *cause, *cwd = NULL, *cp, *ename;
char *wname = NULL, *sname = NULL, *prefix = NULL; char *wname = NULL, *sname = NULL, *prefix = NULL;
int detached, already_attached, is_control = 0; 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 }; struct spawn_context sc = { 0 };
enum cmd_retval retval; enum cmd_retval retval;
struct cmd_find_state fs; 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. * so add it to the group and synchronize.
*/ */
if (group != NULL) { if (group != NULL) {
if (sg == NULL) { if (sg == NULL && groupwith == NULL)
if (groupwith != NULL) { sg = session_group_new(group);
sg = session_group_new(groupwith->name); else if (sg == NULL) {
session_group_add(sg, groupwith); cp = xstrdup(groupwith->name);
} else for (idx = 1; session_group_find(cp) != NULL; idx++) {
sg = session_group_new(group); 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_add(sg, s);
session_group_synchronize_to(s); session_group_synchronize_to(s);

View File

@@ -544,18 +544,17 @@ fi
# Enable image support. # Enable image support.
AC_ARG_ENABLE( AC_ARG_ENABLE(
images, images,
AS_HELP_STRING(--enable-images, enable Kitty/SIXEL/ASCII images) AS_HELP_STRING(--enable-images, enable image support)
) )
# Keep the old option as an alias for one compatibility cycle.
AC_ARG_ENABLE( AC_ARG_ENABLE(
sixel, sixel,
AS_HELP_STRING(--enable-sixel, deprecated alias for --enable-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_MSG_WARN([--enable-sixel is deprecated, use --enable-images instead to enable.])
enable_images=yes
fi
if test "x$enable_images" = xyes; then if test "x$enable_images" = xyes; then
PKG_CHECK_MODULES( PKG_CHECK_MODULES(
IMAGE, IMAGE,

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: format.c,v 1.425 2026/10/05 10:59:37 nicm Exp $ */ /* $OpenBSD: format.c,v 1.427 2026/10/12 12:22:42 nicm Exp $ */
/* /*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -3074,14 +3074,11 @@ format_cb_version(__unused struct format_tree *ft)
static void * static void *
format_cb_image_support(__unused struct format_tree *ft) format_cb_image_support(__unused struct format_tree *ft)
{ {
char *value;
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
value = xstrdup("1"); return (xstrdup("1"));
#else #else
value = xstrdup("0"); return (xstrdup("0"));
#endif #endif
return (value);
} }
/* Callback for active_window_index. */ /* Callback for active_window_index. */
@@ -5809,6 +5806,17 @@ format_loop_clients(struct format_expand_state *es, const char *fmt)
return (value); 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 * static char *
format_replace_expression(struct format_modifier *mexp, format_replace_expression(struct format_modifier *mexp,
struct format_expand_state *es, const char *copy) struct format_expand_state *es, const char *copy)
@@ -5894,9 +5902,11 @@ format_replace_expression(struct format_modifier *mexp,
goto fail; goto fail;
} }
if (!use_fp) { if (!use_fp &&
mleft = (long long)mleft; (format_truncate_integer(&mleft) != 0 ||
mright = (long long)mright; 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 left side is: %.*f", prec, mleft);
format_log(es, "expression right side is: %.*f", prec, mright); format_log(es, "expression right side is: %.*f", prec, mright);
@@ -5936,10 +5946,11 @@ format_replace_expression(struct format_modifier *mexp,
result = (mleft <= mright); result = (mleft <= mright);
break; break;
} }
if (use_fp) if (!use_fp && format_truncate_integer(&result) != 0) {
xasprintf(&value, "%.*f", prec, result); format_log(es, "expression result is out of range");
else goto fail;
xasprintf(&value, "%.*f", prec, (double)(long long)result); }
xasprintf(&value, "%.*f", prec, result);
format_log(es, "expression result is %s", value); format_log(es, "expression result is %s", value);
free(right); free(right);

9
grid.c
View File

@@ -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 <nicholas.marriott@gmail.com> * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -45,7 +45,7 @@ const struct grid_cell grid_default_cell = {
/* /*
* Padding grid cell data. Padding cells are the only zero width cell that * 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 = { static const struct grid_cell grid_padding_cell = {
{ { '!' }, 0, 0, 0 }, 0, GRID_FLAG_PADDING, 8, 8, 8, 0 { { '!' }, 0, 0, 0 }, 0, GRID_FLAG_PADDING, 8, 8, 8, 0
@@ -651,7 +651,10 @@ grid_get_cell1(struct grid_line *gl, u_int px, struct grid_cell *gc)
if (gce->flags & GRID_FLAG_BG256) if (gce->flags & GRID_FLAG_BG256)
gc->bg |= COLOUR_FLAG_256; gc->bg |= COLOUR_FLAG_256;
gc->us = 8; 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; gc->link = 0;
} }

View File

@@ -19,6 +19,7 @@
#include <sys/types.h> #include <sys/types.h>
#include <errno.h>
#include <limits.h> #include <limits.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -236,19 +237,21 @@ sixel_parse_write(struct sixel_image *si, u_int ch)
return (0); return (0);
} }
/* Parse a number, clamping values too large for u_int so limit checks fail. */ static int
static u_int sixel_parse_number(const char *cp, char **endptr, u_int *value)
sixel_parse_number(const char *cp, char **endptr)
{ {
unsigned long value; unsigned long long number;
value = strtoul(cp, endptr, 10); if (*cp < '0' || *cp > '9')
if (value > UINT_MAX) return (1);
return (UINT_MAX); errno = 0;
return (value); number = strtoull(cp, endptr, 10);
if (errno == ERANGE || number > UINT_MAX)
return (1);
*value = number;
return (0);
} }
/* Parse a SIXEL raster attribute sequence. */
static const char * static const char *
sixel_parse_attributes(struct sixel_image *si, const char *cp, const char *end) sixel_parse_attributes(struct sixel_image *si, const char *cp, const char *end)
{ {
@@ -274,7 +277,10 @@ sixel_parse_attributes(struct sixel_image *si, const char *cp, const char *end)
return (NULL); return (NULL);
} }
x = sixel_parse_number(endptr + 1, &endptr); if (sixel_parse_number(endptr + 1, &endptr, &x) != 0) {
log_debug("%s: invalid number", __func__);
return (NULL);
}
if (endptr == last || *endptr != ';') { if (endptr == last || *endptr != ';') {
log_debug("%s: missing ;", __func__); log_debug("%s: missing ;", __func__);
return (NULL); return (NULL);
@@ -283,7 +289,10 @@ sixel_parse_attributes(struct sixel_image *si, const char *cp, const char *end)
log_debug("%s: image is too wide", __func__); log_debug("%s: image is too wide", __func__);
return (NULL); return (NULL);
} }
y = sixel_parse_number(endptr + 1, &endptr); if (sixel_parse_number(endptr + 1, &endptr, &y) != 0) {
log_debug("%s: invalid number", __func__);
return (NULL);
}
if (endptr != last) { if (endptr != last) {
log_debug("%s: extra ;", __func__); log_debug("%s: extra ;", __func__);
return (NULL); return (NULL);
@@ -319,7 +328,10 @@ sixel_parse_colour(struct sixel_image *si, const char *cp, const char *end)
last++; last++;
} }
c = sixel_parse_number(cp, &endptr); if (sixel_parse_number(cp, &endptr, &c) != 0) {
log_debug("%s: invalid number", __func__);
return (NULL);
}
if (c > SIXEL_COLOUR_REGISTERS) { if (c > SIXEL_COLOUR_REGISTERS) {
log_debug("%s: too many colours", __func__); log_debug("%s: too many colours", __func__);
return (NULL); return (NULL);
@@ -330,22 +342,34 @@ sixel_parse_colour(struct sixel_image *si, const char *cp, const char *end)
if (endptr == last || *endptr != ';') if (endptr == last || *endptr != ';')
return (last); return (last);
type = sixel_parse_number(endptr + 1, &endptr); if (sixel_parse_number(endptr + 1, &endptr, &type) != 0) {
log_debug("%s: invalid number", __func__);
return (NULL);
}
if (endptr == last || *endptr != ';') { if (endptr == last || *endptr != ';') {
log_debug("%s: missing ;", __func__); log_debug("%s: missing ;", __func__);
return (NULL); return (NULL);
} }
c1 = sixel_parse_number(endptr + 1, &endptr); if (sixel_parse_number(endptr + 1, &endptr, &c1) != 0) {
log_debug("%s: invalid number", __func__);
return (NULL);
}
if (endptr == last || *endptr != ';') { if (endptr == last || *endptr != ';') {
log_debug("%s: missing ;", __func__); log_debug("%s: missing ;", __func__);
return (NULL); return (NULL);
} }
c2 = sixel_parse_number(endptr + 1, &endptr); if (sixel_parse_number(endptr + 1, &endptr, &c2) != 0) {
log_debug("%s: invalid number", __func__);
return (NULL);
}
if (endptr == last || *endptr != ';') { if (endptr == last || *endptr != ';') {
log_debug("%s: missing ;", __func__); log_debug("%s: missing ;", __func__);
return (NULL); return (NULL);
} }
c3 = sixel_parse_number(endptr + 1, &endptr); if (sixel_parse_number(endptr + 1, &endptr, &c3) != 0) {
log_debug("%s: invalid number", __func__);
return (NULL);
}
if (endptr != last) { if (endptr != last) {
log_debug("%s: missing ;", __func__); log_debug("%s: missing ;", __func__);
return (NULL); return (NULL);

View File

@@ -138,7 +138,7 @@ static const struct image_backend image_backend_sixel = {
static const struct image_backend * static const struct image_backend *
image_tty_find_backend(struct tty *tty) image_tty_find_backend(struct tty *tty)
{ {
if (tty->term != NULL && tty->term->flags & TERM_KITTY) if (tty->term != NULL && tty->term->flags & TERM_KITTYGRAPHICS)
return (&image_backend_kitty); return (&image_backend_kitty);
if (tty->term != NULL && tty->term->flags & TERM_SIXEL && if (tty->term != NULL && tty->term->flags & TERM_SIXEL &&
tty->xpixel != 0 && tty->ypixel != 0) { tty->xpixel != 0 && tty->ypixel != 0) {

View File

@@ -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 <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -620,7 +620,7 @@ input_key(struct screen *s, struct bufferevent *bev, key_code key)
key = '\011' | (key & ~KEYC_MASK_KEY) | KEYC_SHIFT; key = '\011' | (key & ~KEYC_MASK_KEY) | KEYC_SHIFT;
} else { } else {
/* Otherwise clear modifiers. */ /* Otherwise clear modifiers. */
key &= ~KEYC_MASK_MODIFIERS; key &= ~(KEYC_MASK_MODIFIERS|KEYC_IMPLIED_META);
} }
} }

13
menu.c
View File

@@ -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 <nicholas.marriott@gmail.com> * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -194,10 +194,19 @@ static void
menu_reapply_styles(struct menu_data *md) menu_reapply_styles(struct menu_data *md)
{ {
struct options *o = md->w->options; struct options *o = md->w->options;
struct session *s = NULL;
struct winlink *wl = NULL;
struct window_pane *wp = NULL;
struct format_tree *ft; struct format_tree *ft;
struct style sytmp; 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. */ /* Reapply menu style from options. */
memcpy(&md->style_gc, &grid_default_cell, sizeof md->style_gc); memcpy(&md->style_gc, &grid_default_cell, sizeof md->style_gc);

View File

@@ -557,7 +557,6 @@ const struct options_table_entry options_table[] = {
.scope = OPTIONS_TABLE_SERVER, .scope = OPTIONS_TABLE_SERVER,
.flags = OPTIONS_TABLE_IS_ARRAY, .flags = OPTIONS_TABLE_IS_ARRAY,
.default_str = "xterm*:clipboard:ccolour:cstyle:focus:title," .default_str = "xterm*:clipboard:ccolour:cstyle:focus:title,"
"xterm-kitty*:kitty,"
"screen*:title," "screen*:title,"
"rxvt*:ignorefkeys", "rxvt*:ignorefkeys",
.separator = ",", .separator = ",",

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. # Default number of decimal places for float is two.
test_format "#{e|*|f:2.5,2}" "5.00" test_format "#{e|*|f:2.5,2}" "5.00"
# Division by zero must not crash the server (result is unspecified). # An integer expression with a side or result that is NaN or out of the range
$TMUX display-message -p "#{e|/|:5,0}" >/dev/null 2>&1 # of a long long expands to empty: division or modulus by zero, nan or inf, or
$TMUX display-message -p "#{e|/|f:5,0}" >/dev/null 2>&1 # 1e30. LLONG_MAX written out is 2^63 as a double, which is out of range; the
assert_alive "division by zero" # 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 ---------------------------------------------------- # --- ASCII and repeat ----------------------------------------------------

View File

@@ -79,6 +79,7 @@ history_limit
history_size history_size
host host
host_short host_short
image_support
insert_flag insert_flag
keypad_cursor_flag keypad_cursor_flag
keypad_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 status off || exit 1
$TMUX set -g scroll-on-clear 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 ',*: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 $TMUX set -as terminal-features ',*:RGB' || exit 1
$TMUX2 new-session -d -x 10 -y 4 "$TMUX attach-session" || exit 1 $TMUX2 new-session -d -x 10 -y 4 "$TMUX attach-session" || exit 1
$TMUX2 set -g status off || 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': if run(inner, 'display-message', '-p', '#{image_support}') == '0':
raise SystemExit(0) raise SystemExit(0)
run(inner, 'set', '-g', 'status', 'off') 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}', pane = run(outer, 'new-session', '-dP', '-F', '#{pane_id}',
'-x', '40', '-y', '12', 'exec sh') '-x', '40', '-y', '12', 'exec sh')
run(outer, 'set', '-g', 'status', 'off') run(outer, 'set', '-g', 'status', 'off')

View File

@@ -112,7 +112,7 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp:
raise SystemExit(0) raise SystemExit(0)
run('set', '-g', 'status', 'off') run('set', '-g', 'status', 'off')
run('set', '-as', 'terminal-features', ',*:sixel@') run('set', '-as', 'terminal-features', ',*:sixel@')
run('set', '-as', 'terminal-features', ',*:kitty@') run('set', '-as', 'terminal-features', ',*:kittygraphics@')
run('set', '-as', 'terminal-features', ',*:RGB') run('set', '-as', 'terminal-features', ',*:RGB')
subprocess.run(outer + ['new-session', '-d', '-x', '40', '-y', '12'], check=True) subprocess.run(outer + ['new-session', '-d', '-x', '40', '-y', '12'], check=True)
subprocess.run(outer + ['set', '-g', 'status', 'off'], 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 sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 [ "$($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 $TMUX2 new-session -d -x 40 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane." [ -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 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" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter 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 $TMUX2 new-session -d -x 40 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane." [ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 set -as terminal-features ',*:kitty@' || $TMUX2 set -as terminal-features ',*:kittygraphics@' ||
fail "disable outer kitty failed" fail "disable outer kitty failed"
: >"$TMP" : >"$TMP"
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed" $TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
@@ -76,11 +76,11 @@ scroll_markers()
N=$(grep -ac MARKER "$TMP") N=$(grep -ac MARKER "$TMP")
} }
scroll_markers ',*:kitty,*:imagescroll' scroll_markers ',*:kittygraphics,*:imagescroll'
[ "$N" -eq 0 ] || [ "$N" -eq 0 ] ||
fail "existing text was redrawn ($N lines) scrolling with imagescroll" fail "existing text was redrawn ($N lines) scrolling with imagescroll"
scroll_markers ',*:kitty' scroll_markers ',*:kittygraphics'
[ "$N" -gt 0 ] || [ "$N" -gt 0 ] ||
fail "text was not redrawn scrolling without imagescroll" 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 \ check_raw_matches tabs \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \ 'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,1 data=\(7,7, \) flags=TAB\[[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,8 data=\(1,1,B\) flags=NONE\[0\]' \
'C 0,0 data=\(1,1,C\) 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 \ check_raw_matches wide \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \ 'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,1 data=\(2,3,あ\) flags=NONE\[0\] attr=NONE\[0\]' \ '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\]' 'C 0,3 data=\(1,1,B\) flags=NONE\[0\]'
start_pane combining 8 3 'e\314\201x' 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 \ check_raw_matches emoji \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \ 'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,0 data=\(2,4,😀\) flags=NONE\[0\] attr=NONE\[0\]' \ '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\]' 'C 0,2 data=\(1,1,Z\) flags=NONE\[0\]'
start_pane flag 10 3 '\360\237\207\254\360\237\207\247!' start_pane flag 10 3 '\360\237\207\254\360\237\207\247!'
check_raw_matches flag \ check_raw_matches flag \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \ 'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,0 data=\(2,8,🇬🇧\) flags=NONE\[0\] attr=NONE\[0\]' \ '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\]' 'C 0,2 data=\(1,1,!\) flags=NONE\[0\]'
start_pane variation 10 3 '*\357\270\217!' start_pane variation 10 3 '*\357\270\217!'
check_raw_matches variation \ check_raw_matches variation \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \ 'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,0 data=\(2,[0-9]+,.*\) flags=NONE\[0\] attr=NONE\[0\]' \ '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\]' 'C 0,2 data=\(1,1,!\) flags=NONE\[0\]'
start_pane invalid 10 3 '\377A' 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' 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 \ check_raw_matches overwrite-wide-with-wide \
'C 0,1 data=\(2,3,界\) flags=NONE\[0\] attr=NONE\[0\]' \ '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\]' 'C 0,3 data=\(1,1,B\) flags=NONE\[0\]'
start_pane wide-right-edge 4 3 'ABC\343\201\202Z' start_pane wide-right-edge 4 3 'ABC\343\201\202Z'
check_raw_matches wide-right-edge \ check_raw_matches wide-right-edge \
'L 0 \(0\) flags=WRAPPED\[[0-9a-f]+\]' \ 'L 0 \(0\) flags=WRAPPED\[[0-9a-f]+\]' \
'C 1,0 data=\(2,3,あ\) flags=NONE\[0\] attr=NONE\[0\]' \ '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\]' 'C 1,2 data=\(1,1,Z\) flags=NONE\[0\]'
exit $exit_status 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 return "ignorefkeys" in features
if name == "XDA": if name == "XDA":
return kind == "ReplyTest" return kind == "ReplyTest"
if name == "XDA-kitty": if name in ("XDA-kitty", "XDA-kitty-nosync"):
return kind == "kitty(0.43.0)" and "kitty" in features 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": if name == "sync":
return "sync" in features return "sync" in features
if name == "characters": if name == "characters":
@@ -57,6 +71,9 @@ reports = [
("DA2", b"\033[>85;1;0c"), ("DA2", b"\033[>85;1;0c"),
("XDA", b"\033P>|ReplyTest\033\\"), ("XDA", b"\033P>|ReplyTest\033\\"),
("XDA-kitty", b"\033P>|kitty(0.43.0)\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"), ("sync", b"\033[?2026;2$y"),
("characters", b"\033[8;17;61t"), ("characters", b"\033[8;17;61t"),
("pixels", b"\033[4;480;800t"), ("pixels", b"\033[4;480;800t"),
@@ -83,6 +100,8 @@ for name, report in reports:
else: else:
run("set-option", "-s", "escape-time", "60000") run("set-option", "-s", "escape-time", "60000")
run("set-option", "-g", "@seen", "") 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", run("bind-key", "-n", "Escape", "set-option", "-gF",
"@seen", "#{@seen}E") "@seen", "#{@seen}E")
pid, fd = os.forkpty() pid, fd = os.forkpty()
@@ -143,6 +162,12 @@ for name, report in reports:
assert actual == baseline, (context, "unexpected effect", actual) assert actual == baseline, (context, "unexpected effect", actual)
else: else:
assert matches(name, actual), (context, "missing effect", actual) 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") == ( assert run("show-option", "-gv", "@seen") == (
"E" if prefixed else ""), (context, "Escape count") "E" if prefixed else ""), (context, "Escape count")
cases += 1 cases += 1

View File

@@ -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 <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -159,8 +159,10 @@ clients_calculate_size(int type, int current, struct client *c,
*/ */
*xpixel = w->xpixel; *xpixel = w->xpixel;
*ypixel = w->ypixel; *ypixel = w->ypixel;
} else } else {
*xpixel = *ypixel = 0; *xpixel = 0;
*ypixel = 0;
}
/* /*
* For latest, count the number of clients with this window. We only * For latest, count the number of clients with this window. We only
@@ -388,21 +390,31 @@ recalculate_size(struct window *w, int now)
/* Look for a suitable client and get the new size. */ /* Look for a suitable client and get the new size. */
changed = clients_calculate_size(type, current, NULL, NULL, w, changed = clients_calculate_size(type, current, NULL, NULL, w,
recalculate_size_skip_client, &sx, &sy, &xpixel, &ypixel); 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 * Make sure the size has actually changed. If the window has already
* got a resize scheduled, then use the new size; otherwise the old. * got a resize scheduled, then use the new size; otherwise the old.
*/ */
if (w->flags & WINDOW_RESIZE) { if (w->flags & WINDOW_RESIZE) {
if (!now && changed && w->new_sx == sx && w->new_sy == sy && if (!now &&
w->new_xpixel == xpixel && w->new_ypixel == ypixel) { changed &&
w->new_sx == sx &&
w->new_sy == sy &&
w->new_xpixel == xpixel &&
w->new_ypixel == ypixel)
changed = 0; changed = 0;
}
} else { } else {
if (!now && changed && w->sx == sx && w->sy == sy && if (!now &&
w->xpixel == xpixel && w->ypixel == ypixel) { changed &&
w->sx == sx &&
w->sy == sy &&
w->xpixel == xpixel &&
w->ypixel == ypixel)
changed = 0; changed = 0;
}
} }
/* /*

View File

@@ -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 <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -716,6 +716,10 @@ screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor)
return 0; 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. */ /* Restore the saved grid. */
grid_duplicate_lines(s->grid, screen_hsize(s), s->saved_grid, 0, grid_duplicate_lines(s->grid, screen_hsize(s), s->saved_grid, 0,
s->saved_grid->sy); s->saved_grid->sy);

View File

@@ -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 <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -2599,7 +2599,7 @@ server_client_dispatch(struct imsg *imsg, void *arg)
if (datalen != 0) if (datalen != 0)
goto bad; goto bad;
if (c->flags & CLIENT_CONTROL) if ((c->flags & CLIENT_CONTROL) || (~c->tty.flags & TTY_OPENED))
break; break;
server_client_update_latest(c); server_client_update_latest(c);
old_sx = c->tty.sx; old_sx = c->tty.sx;

9
tmux.1
View File

@@ -1,4 +1,4 @@
.\" $OpenBSD: tmux.1,v 1.1182 2026/10/09 13:12:14 nicm Exp $ .\" $OpenBSD: tmux.1,v 1.1184 2026/10/12 12:35:03 nicm Exp $
.\" .\"
.\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> .\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
.\" .\"
@@ -14,7 +14,7 @@
.\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
.\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" 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 .Dt TMUX 1
.Os .Os
.Sh NAME .Sh NAME
@@ -5206,8 +5206,8 @@ Ignore function keys from
and use the and use the
.Nm .Nm
internal set only. internal set only.
.It kitty .It kittygraphics
Supports the Kitty graphics protocol. Supports the Kitty graphics protocol with Unicode placeholders.
.It margins .It margins
Supports DECSLRM margins. Supports DECSLRM margins.
.It mouse .It mouse
@@ -7632,7 +7632,6 @@ The following variables are available, where appropriate:
.It Li "session_stack" Ta "" Ta "Window indexes in most recent order" .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 "session_windows" Ta "" Ta "Number of windows in session"
.It Li "socket_path" Ta "" Ta "Server socket path" .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 "start_time" Ta "" Ta "Server start time"
.It Li "synchronized_output_flag" Ta "" Ta "1 if pane has synchronized output enabled" .It Li "synchronized_output_flag" Ta "" Ta "1 if pane has synchronized output enabled"
.It Li "uid" Ta "" Ta "Server UID" .It Li "uid" Ta "" Ta "Server UID"

7
tmux.h
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tmux.h,v 1.1455 2026/10/09 13:12:14 nicm Exp $ */ /* $OpenBSD: tmux.h,v 1.1457 2026/10/12 12:35:03 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -643,6 +643,7 @@ enum tty_code_code {
TTYC_KUP6, TTYC_KUP6,
TTYC_KUP7, TTYC_KUP7,
TTYC_MS, TTYC_MS,
TTYC_NCV,
TTYC_NOBR, TTYC_NOBR,
TTYC_OL, TTYC_OL,
TTYC_OP, TTYC_OP,
@@ -1871,8 +1872,8 @@ struct tty_term {
#define TERM_VT100LIKE 0x20 #define TERM_VT100LIKE 0x20
#define TERM_SIXEL 0x40 #define TERM_SIXEL 0x40
#define TERM_INVALIDMS 0x80 #define TERM_INVALIDMS 0x80
#define TERM_KITTY 0x100 #define TERM_NOREPLACE 0x100
#define TERM_NOREPLACE 0x200 #define TERM_KITTYGRAPHICS 0x200
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
#define TERM_IMAGE_QUADRANTS 0x400 #define TERM_IMAGE_QUADRANTS 0x400
#define TERM_IMAGE_SEXTANTS 0x800 #define TERM_IMAGE_SEXTANTS 0x800

View File

@@ -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.48 2026/10/12 12:35:03 nicm Exp $ */
/* /*
* Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -236,11 +236,9 @@ static const struct tty_feature tty_feature_strikethrough = {
0 0
}; };
#define TTY_FEATURE_SYNC "Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;"
/* Terminal supports synchronized updates. */ /* Terminal supports synchronized updates. */
static const char *const tty_feature_sync_capabilities[] = { static const char *const tty_feature_sync_capabilities[] = {
TTY_FEATURE_SYNC, "Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;",
NULL NULL
}; };
static const struct tty_feature tty_feature_sync = { static const struct tty_feature tty_feature_sync = {
@@ -371,16 +369,11 @@ static const struct tty_feature tty_feature_sixel = {
TERM_SIXEL TERM_SIXEL
}; };
/* Terminal has Kitty graphics protocol Unicode placeholder capability. */ /* Terminal supports Kitty graphics Unicode placeholders. */
static const char *const tty_feature_kitty_capabilities[] = { static const struct tty_feature tty_feature_kittygraphics = {
"Kty", "kittygraphics",
TTY_FEATURE_SYNC, NULL,
NULL TERM_KITTYGRAPHICS
};
static const struct tty_feature tty_feature_kitty = {
"kitty",
tty_feature_kitty_capabilities,
TERM_KITTY
}; };
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
@@ -439,7 +432,7 @@ static const struct tty_feature *const tty_features[] = {
&tty_feature_extkeys, &tty_feature_extkeys,
&tty_feature_focus, &tty_feature_focus,
&tty_feature_ignorefkeys, &tty_feature_ignorefkeys,
&tty_feature_kitty, &tty_feature_kittygraphics,
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
&tty_feature_image_quadrants, &tty_feature_image_quadrants,
&tty_feature_image_sextants, &tty_feature_image_sextants,
@@ -702,7 +695,7 @@ tty_default_features(struct client *c, const char *name, u_int version)
"sync," "sync,"
"usstyle," "usstyle,"
"progressbar," "progressbar,"
"kitty" "kittygraphics"
}, },
{ .name = "kitty", { .name = "kitty",
.features = TTY_FEATURES_BASE_MODERN_XTERM "," .features = TTY_FEATURES_BASE_MODERN_XTERM ","
@@ -715,7 +708,7 @@ tty_default_features(struct client *c, const char *name, u_int version)
"osc7," "osc7,"
"sync," "sync,"
"usstyle," "usstyle,"
"kitty" "kittygraphics"
}, },
{ .name = "Rio", { .name = "Rio",
.features = TTY_FEATURES_BASE_MODERN_XTERM "," .features = TTY_FEATURES_BASE_MODERN_XTERM ","

View File

@@ -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 <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>

View File

@@ -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 <nicholas.marriott@gmail.com> * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -254,6 +254,7 @@ static const struct tty_term_code_entry tty_term_codes[] = {
[TTYC_KUP6] = { TTYCODE_STRING, "kUP6" }, [TTYC_KUP6] = { TTYCODE_STRING, "kUP6" },
[TTYC_KUP7] = { TTYCODE_STRING, "kUP7" }, [TTYC_KUP7] = { TTYCODE_STRING, "kUP7" },
[TTYC_MS] = { TTYCODE_STRING, "Ms" }, [TTYC_MS] = { TTYCODE_STRING, "Ms" },
[TTYC_NCV] = { TTYCODE_NUMBER, "ncv" },
[TTYC_NOBR] = { TTYCODE_STRING, "Nobr" }, [TTYC_NOBR] = { TTYCODE_STRING, "Nobr" },
[TTYC_OL] = { TTYCODE_STRING, "ol" }, [TTYC_OL] = { TTYCODE_STRING, "ol" },
[TTYC_OP] = { TTYCODE_STRING, "op" }, [TTYC_OP] = { TTYCODE_STRING, "op" },

31
tty.c
View File

@@ -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 <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -51,6 +51,7 @@ static void tty_check_bg(struct tty *, struct colour_palette *,
struct grid_cell *); struct grid_cell *);
static void tty_check_us(struct tty *, struct colour_palette *, static void tty_check_us(struct tty *, struct colour_palette *,
struct grid_cell *); struct grid_cell *);
static void tty_check_ncv(struct tty *, struct grid_cell *);
static int tty_map_theme_colour(struct tty *, int); static int tty_map_theme_colour(struct tty *, int);
static void tty_colours_fg(struct tty *, const struct grid_cell *); static void tty_colours_fg(struct tty *, const struct grid_cell *);
static void tty_colours_bg(struct tty *, const struct grid_cell *); static void tty_colours_bg(struct tty *, const struct grid_cell *);
@@ -2425,6 +2426,33 @@ tty_dim_default_colour(struct tty *tty, int c, int foreground)
return (c); 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 void
tty_attributes(struct tty *tty, const struct grid_cell *gc, tty_attributes(struct tty *tty, const struct grid_cell *gc,
const struct tty_style_ctx *style_ctx) const struct tty_style_ctx *style_ctx)
@@ -2495,6 +2523,7 @@ tty_attributes(struct tty *tty, const struct grid_cell *gc,
tty_check_fg(tty, palette, &gc2); tty_check_fg(tty, palette, &gc2);
tty_check_bg(tty, palette, &gc2); tty_check_bg(tty, palette, &gc2);
tty_check_us(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, * If any bits are being cleared or the underline colour is now default,

View File

@@ -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 <nicholas.marriott@gmail.com> * Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -89,19 +89,20 @@ static const char *window_client_info_lines[] = {
" #[#{E:tree-mode-border-style},acs]x#[default] " " #[#{E:tree-mode-border-style},acs]x#[default] "
WINDOW_CLIENT_FEATURE(hyperlinks) " " WINDOW_CLIENT_FEATURE(hyperlinks) " "
WINDOW_CLIENT_FEATURE(ignorefkeys) " " WINDOW_CLIENT_FEATURE(ignorefkeys) " "
WINDOW_CLIENT_FEATURE(margins) " " WINDOW_CLIENT_FEATURE(kittygraphics) " "
WINDOW_CLIENT_FEATURE(mouse), WINDOW_CLIENT_FEATURE(margins),
" #[#{E:tree-mode-border-style},acs]x#[default] " " #[#{E:tree-mode-border-style},acs]x#[default] "
WINDOW_CLIENT_FEATURE(mouse) " "
WINDOW_CLIENT_FEATURE(osc7) " " WINDOW_CLIENT_FEATURE(osc7) " "
WINDOW_CLIENT_FEATURE(overline) " " WINDOW_CLIENT_FEATURE(overline) " "
WINDOW_CLIENT_FEATURE(progressbar) " " WINDOW_CLIENT_FEATURE(progressbar),
WINDOW_CLIENT_FEATURE(rectfill),
" #[#{E:tree-mode-border-style},acs]x#[default] " " #[#{E:tree-mode-border-style},acs]x#[default] "
WINDOW_CLIENT_FEATURE(rectfill) " "
WINDOW_CLIENT_FEATURE(sixel) " " WINDOW_CLIENT_FEATURE(sixel) " "
WINDOW_CLIENT_FEATURE(strikethrough) " " WINDOW_CLIENT_FEATURE(strikethrough) " "
WINDOW_CLIENT_FEATURE(sync) " " WINDOW_CLIENT_FEATURE(sync),
WINDOW_CLIENT_FEATURE(title),
" #[#{E:tree-mode-border-style},acs]x#[default] " " #[#{E:tree-mode-border-style},acs]x#[default] "
WINDOW_CLIENT_FEATURE(title) " "
WINDOW_CLIENT_FEATURE(usstyle) " " WINDOW_CLIENT_FEATURE(usstyle) " "
WINDOW_CLIENT_FEATURE(utf8), WINDOW_CLIENT_FEATURE(utf8),
"#[#{E:tree-mode-border-style},acs]qqqqqqqqqqqqqqn#{R:q,#{window_width}}#[default]", "#[#{E:tree-mode-border-style},acs]qqqqqqqqqqqqqqn#{R:q,#{window_width}}#[default]",