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); diff --git a/configure.ac b/configure.ac index f0766ee27..3e0ade958 100644 --- a/configure.ac +++ b/configure.ac @@ -544,18 +544,17 @@ fi # Enable image support. AC_ARG_ENABLE( 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( 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 PKG_CHECK_MODULES( IMAGE, diff --git a/format.c b/format.c index 0df305046..2e4b4057d 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.427 2026/10/12 12:22:42 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -3074,14 +3074,11 @@ format_cb_version(__unused struct format_tree *ft) static void * format_cb_image_support(__unused struct format_tree *ft) { - char *value; - #ifdef ENABLE_IMAGES - value = xstrdup("1"); + return (xstrdup("1")); #else - value = xstrdup("0"); + return (xstrdup("0")); #endif - return (value); } /* Callback for active_window_index. */ @@ -5809,6 +5806,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) @@ -5894,9 +5902,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); @@ -5936,10 +5946,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); diff --git a/grid.c b/grid.c index 88eafaa46..785109e73 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 @@ -45,7 +45,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 @@ -651,7 +651,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; } diff --git a/image-sixel.c b/image-sixel.c index 5ed384a5a..68d266f80 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -19,6 +19,7 @@ #include +#include #include #include #include @@ -236,19 +237,21 @@ sixel_parse_write(struct sixel_image *si, u_int ch) return (0); } -/* Parse a number, clamping values too large for u_int so limit checks fail. */ -static u_int -sixel_parse_number(const char *cp, char **endptr) +static int +sixel_parse_number(const char *cp, char **endptr, u_int *value) { - unsigned long value; + unsigned long long number; - value = strtoul(cp, endptr, 10); - if (value > UINT_MAX) - return (UINT_MAX); - return (value); + 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); } -/* Parse a SIXEL raster attribute sequence. */ static const char * 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); } - 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 != ';') { log_debug("%s: missing ;", __func__); 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__); 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) { log_debug("%s: extra ;", __func__); return (NULL); @@ -319,7 +328,10 @@ sixel_parse_colour(struct sixel_image *si, const char *cp, const char *end) 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) { log_debug("%s: too many colours", __func__); return (NULL); @@ -330,22 +342,34 @@ sixel_parse_colour(struct sixel_image *si, const char *cp, const char *end) if (endptr == last || *endptr != ';') 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 != ';') { log_debug("%s: missing ;", __func__); 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 != ';') { log_debug("%s: missing ;", __func__); 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 != ';') { log_debug("%s: missing ;", __func__); 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) { log_debug("%s: missing ;", __func__); return (NULL); diff --git a/image.c b/image.c index 946da49ea..5759d08f9 100644 --- a/image.c +++ b/image.c @@ -138,7 +138,7 @@ static const struct image_backend image_backend_sixel = { static const struct image_backend * 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); if (tty->term != NULL && tty->term->flags & TERM_SIXEL && tty->xpixel != 0 && tty->ypixel != 0) { diff --git a/input-keys.c b/input-keys.c index 4cade4e53..aa2b8abbb 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 @@ -620,7 +620,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); } } 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); diff --git a/options-table.c b/options-table.c index 76784e825..15179d87a 100644 --- a/options-table.c +++ b/options-table.c @@ -557,7 +557,6 @@ const struct options_table_entry options_table[] = { .scope = OPTIONS_TABLE_SERVER, .flags = OPTIONS_TABLE_IS_ARRAY, .default_str = "xterm*:clipboard:ccolour:cstyle:focus:title," - "xterm-kitty*:kitty," "screen*:title," "rxvt*:ignorefkeys", .separator = ",", 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/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/image-kitty-clear.sh b/regress/image-kitty-clear.sh index 4fd6d6abc..0b784acb5 100644 --- a/regress/image-kitty-clear.sh +++ b/regress/image-kitty-clear.sh @@ -23,7 +23,7 @@ $TMUX new-session -d -x 10 -y 4 " $TMUX set -g status off || exit 1 $TMUX set -g scroll-on-clear off || exit 1 $TMUX set -as terminal-features ',*:sixel@' || exit 1 -$TMUX set -as terminal-features ',*:kitty@' || exit 1 +$TMUX set -as terminal-features ',*:kittygraphics@' || exit 1 $TMUX set -as terminal-features ',*:RGB' || exit 1 $TMUX2 new-session -d -x 10 -y 4 "$TMUX attach-session" || exit 1 $TMUX2 set -g status off || exit 1 diff --git a/regress/image-kitty-layers.sh b/regress/image-kitty-layers.sh index 5272ac573..e09b94e2c 100644 --- a/regress/image-kitty-layers.sh +++ b/regress/image-kitty-layers.sh @@ -109,7 +109,7 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-layers-') as tmp: if run(inner, 'display-message', '-p', '#{image_support}') == '0': raise SystemExit(0) run(inner, 'set', '-g', 'status', 'off') - run(inner, 'set', '-as', 'terminal-features', ',*:kitty') + run(inner, 'set', '-as', 'terminal-features', ',*:kittygraphics') pane = run(outer, 'new-session', '-dP', '-F', '#{pane_id}', '-x', '40', '-y', '12', 'exec sh') run(outer, 'set', '-g', 'status', 'off') diff --git a/regress/image-kitty-protocol.sh b/regress/image-kitty-protocol.sh index 7fdfffd16..40df2455f 100644 --- a/regress/image-kitty-protocol.sh +++ b/regress/image-kitty-protocol.sh @@ -112,7 +112,7 @@ with tempfile.TemporaryDirectory(prefix='tmux-kitty-protocol-') as tmp: raise SystemExit(0) run('set', '-g', 'status', 'off') run('set', '-as', 'terminal-features', ',*:sixel@') - run('set', '-as', 'terminal-features', ',*:kitty@') + run('set', '-as', 'terminal-features', ',*:kittygraphics@') run('set', '-as', 'terminal-features', ',*:RGB') subprocess.run(outer + ['new-session', '-d', '-x', '40', '-y', '12'], check=True) subprocess.run(outer + ['set', '-g', 'status', 'off'], check=True) diff --git a/regress/image-kitty-region-scroll.sh b/regress/image-kitty-region-scroll.sh index c18fb11d5..7d60564bc 100755 --- a/regress/image-kitty-region-scroll.sh +++ b/regress/image-kitty-region-scroll.sh @@ -55,12 +55,12 @@ $TMUX new-session -d -s inner -x 40 -y 20 "printf '$HEADER'; exec sh" || sleep 0.3 [ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 -$TMUX set -as terminal-features ',*:kitty' || exit 1 +$TMUX set -as terminal-features ',*:kittygraphics' || exit 1 $TMUX2 new-session -d -x 40 -y 20 || exit 1 OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) [ -n "$OUTER" ] || fail "No outer pane." -$TMUX2 set -as terminal-features ',*:kitty@' || fail "disable outer kitty failed" +$TMUX2 set -as terminal-features ',*:kittygraphics@' || fail "disable outer kitty failed" $TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed" $TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed" $TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed" diff --git a/regress/image-scroll-no-text-redraw.sh b/regress/image-scroll-no-text-redraw.sh index f466cbce2..6398259c1 100644 --- a/regress/image-scroll-no-text-redraw.sh +++ b/regress/image-scroll-no-text-redraw.sh @@ -57,7 +57,7 @@ scroll_markers() $TMUX2 new-session -d -x 40 -y 20 || exit 1 OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) [ -n "$OUTER" ] || fail "No outer pane." - $TMUX2 set -as terminal-features ',*:kitty@' || + $TMUX2 set -as terminal-features ',*:kittygraphics@' || fail "disable outer kitty failed" : >"$TMP" $TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed" @@ -76,11 +76,11 @@ scroll_markers() N=$(grep -ac MARKER "$TMP") } -scroll_markers ',*:kitty,*:imagescroll' +scroll_markers ',*:kittygraphics,*:imagescroll' [ "$N" -eq 0 ] || fail "existing text was redrawn ($N lines) scrolling with imagescroll" -scroll_markers ',*:kitty' +scroll_markers ',*:kittygraphics' [ "$N" -gt 0 ] || fail "text was not redrawn scrolling without imagescroll" 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/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 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 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 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 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 diff --git a/regress/tty-keys-replies.sh b/regress/tty-keys-replies.sh index 57ff54db9..b47e6a8a9 100755 --- a/regress/tty-keys-replies.sh +++ b/regress/tty-keys-replies.sh @@ -43,8 +43,22 @@ def matches(name, value): return "ignorefkeys" in features if name == "XDA": return kind == "ReplyTest" - if name == "XDA-kitty": - return kind == "kitty(0.43.0)" and "kitty" in features + if name in ("XDA-kitty", "XDA-kitty-nosync"): + required = { + "256", "RGB", "bpaste", "clipboard", "mouse", + "strikethrough", "title", "ccolour", "cstyle", "extkeys", + "focus", "hyperlinks", "osc7", "usstyle", "kittygraphics"} + if name == "XDA-kitty": + required.add("sync") + elif "sync" in features: + return False + return kind == "kitty(0.43.0)" and required <= features + if name == "XDA-ghostty": + return kind == "ghostty 1.2.0" and { + "kittygraphics", "margins", "sync", "usstyle", "progressbar"} <= features + if name == "XDA-WezTerm": + return kind == "WezTerm 20240203-110809-5046fc22" and { + "sixel", "margins", "usstyle"} <= features if name == "sync": return "sync" in features if name == "characters": @@ -57,6 +71,9 @@ reports = [ ("DA2", b"\033[>85;1;0c"), ("XDA", b"\033P>|ReplyTest\033\\"), ("XDA-kitty", b"\033P>|kitty(0.43.0)\033\\"), + ("XDA-kitty-nosync", b"\033P>|kitty(0.43.0)\033\\"), + ("XDA-ghostty", b"\033P>|ghostty 1.2.0\033\\"), + ("XDA-WezTerm", b"\033P>|WezTerm 20240203-110809-5046fc22\033\\"), ("sync", b"\033[?2026;2$y"), ("characters", b"\033[8;17;61t"), ("pixels", b"\033[4;480;800t"), @@ -83,6 +100,8 @@ for name, report in reports: else: run("set-option", "-s", "escape-time", "60000") run("set-option", "-g", "@seen", "") + if name == "XDA-kitty-nosync": + run("set-option", "-as", "terminal-features", ",*:sync@") run("bind-key", "-n", "Escape", "set-option", "-gF", "@seen", "#{@seen}E") pid, fd = os.forkpty() @@ -143,6 +162,12 @@ for name, report in reports: assert actual == baseline, (context, "unexpected effect", actual) else: assert matches(name, actual), (context, "missing effect", actual) + if name in ("XDA-kitty", "XDA-kitty-nosync", "XDA-ghostty"): + assert run("display-message", "-p", "#{I/f:kittygraphics}") == "1", ( + context, "kittygraphics feature lookup failed") + if name == "XDA-kitty-nosync": + assert run("display-message", "-p", "#{I/f:sync}") == "0", ( + context, "kitty enabled synchronized updates") assert run("show-option", "-gv", "@seen") == ( "E" if prefixed else ""), (context, "Escape count") cases += 1 diff --git a/resize.c b/resize.c index e5d95b3d9..91d05c4f5 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 @@ -159,8 +159,10 @@ clients_calculate_size(int type, int current, struct client *c, */ *xpixel = w->xpixel; *ypixel = w->ypixel; - } else - *xpixel = *ypixel = 0; + } else { + *xpixel = 0; + *ypixel = 0; + } /* * 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. */ 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 && - w->new_xpixel == xpixel && w->new_ypixel == ypixel) { + 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 && - w->xpixel == xpixel && w->ypixel == ypixel) { + if (!now && + changed && + w->sx == sx && + w->sy == sy && + w->xpixel == xpixel && + w->ypixel == ypixel) changed = 0; - } } /* diff --git a/screen.c b/screen.c index a06f1e26c..01cde52ef 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 @@ -716,6 +716,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); diff --git a/server-client.c b/server-client.c index 46e4af0d1..6f824662d 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 @@ -2599,7 +2599,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; diff --git a/tmux.1 b/tmux.1 index 266453f76..29c8a601e 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.1184 2026/10/12 12:35:03 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 @@ -5206,8 +5206,8 @@ Ignore function keys from and use the .Nm internal set only. -.It kitty -Supports the Kitty graphics protocol. +.It kittygraphics +Supports the Kitty graphics protocol with Unicode placeholders. .It margins Supports DECSLRM margins. .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_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" diff --git a/tmux.h b/tmux.h index 897d3534f..3710cdd5b 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.1457 2026/10/12 12:35:03 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -643,6 +643,7 @@ enum tty_code_code { TTYC_KUP6, TTYC_KUP7, TTYC_MS, + TTYC_NCV, TTYC_NOBR, TTYC_OL, TTYC_OP, @@ -1871,8 +1872,8 @@ struct tty_term { #define TERM_VT100LIKE 0x20 #define TERM_SIXEL 0x40 #define TERM_INVALIDMS 0x80 -#define TERM_KITTY 0x100 -#define TERM_NOREPLACE 0x200 +#define TERM_NOREPLACE 0x100 +#define TERM_KITTYGRAPHICS 0x200 #ifdef ENABLE_IMAGES #define TERM_IMAGE_QUADRANTS 0x400 #define TERM_IMAGE_SEXTANTS 0x800 diff --git a/tty-features.c b/tty-features.c index 0eef43511..65858888c 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.48 2026/10/12 12:35:03 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -236,11 +236,9 @@ static const struct tty_feature tty_feature_strikethrough = { 0 }; -#define TTY_FEATURE_SYNC "Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;" - /* Terminal supports synchronized updates. */ static const char *const tty_feature_sync_capabilities[] = { - TTY_FEATURE_SYNC, + "Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;", NULL }; static const struct tty_feature tty_feature_sync = { @@ -371,16 +369,11 @@ static const struct tty_feature tty_feature_sixel = { TERM_SIXEL }; -/* Terminal has Kitty graphics protocol Unicode placeholder capability. */ -static const char *const tty_feature_kitty_capabilities[] = { - "Kty", - TTY_FEATURE_SYNC, - NULL -}; -static const struct tty_feature tty_feature_kitty = { - "kitty", - tty_feature_kitty_capabilities, - TERM_KITTY +/* Terminal supports Kitty graphics Unicode placeholders. */ +static const struct tty_feature tty_feature_kittygraphics = { + "kittygraphics", + NULL, + TERM_KITTYGRAPHICS }; #ifdef ENABLE_IMAGES @@ -439,7 +432,7 @@ static const struct tty_feature *const tty_features[] = { &tty_feature_extkeys, &tty_feature_focus, &tty_feature_ignorefkeys, - &tty_feature_kitty, + &tty_feature_kittygraphics, #ifdef ENABLE_IMAGES &tty_feature_image_quadrants, &tty_feature_image_sextants, @@ -702,7 +695,7 @@ tty_default_features(struct client *c, const char *name, u_int version) "sync," "usstyle," "progressbar," - "kitty" + "kittygraphics" }, { .name = "kitty", .features = TTY_FEATURES_BASE_MODERN_XTERM "," @@ -715,7 +708,7 @@ tty_default_features(struct client *c, const char *name, u_int version) "osc7," "sync," "usstyle," - "kitty" + "kittygraphics" }, { .name = "Rio", .features = TTY_FEATURES_BASE_MODERN_XTERM "," diff --git a/tty-keys.c b/tty-keys.c index f91e1b84d..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 diff --git a/tty-term.c b/tty-term.c index 1ac078cd4..3311701ba 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 @@ -254,6 +254,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 06ba2203f..edec0f69a 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 *); @@ -2425,6 +2426,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) @@ -2495,6 +2523,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, 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]",