From d6db719b19a4a95664135c02a186d179aef95977 Mon Sep 17 00:00:00 2001 From: Pete Dietl Date: Tue, 22 Sep 2026 00:21:23 -0500 Subject: [PATCH 01/10] Wait for the pane to echo the key before capturing in input-keys.sh --- regress/input-keys.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/regress/input-keys.sh b/regress/input-keys.sh index cb771d19a..76dd5ea73 100644 --- a/regress/input-keys.sh +++ b/regress/input-keys.sh @@ -22,7 +22,19 @@ assert_key () { clear-history -t$W \; \ send-keys -t$W "$key" 'EOL' || exit 1 - actual_code=$($TMUX capturep -pt$W | \ + # cat echoes the keys back asynchronously, so wait for the EOL marker + # to reach the pane instead of capturing straight away. + i=0 + while [ $i -lt 50 ]; do + screen=$($TMUX capturep -pt$W) + case "$screen" in + *EOL*) break ;; + esac + i=$((i + 1)) + sleep 0.1 + done + + actual_code=$(printf '%s\n' "$screen" | \ head -1 | \ sed -e 's/EOL.*$//') From cb9f9b588fd4c6aebcef318464b1d1119c9277d2 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 22 Sep 2026 07:52:36 +0100 Subject: [PATCH 02/10] Fix test. --- regress/input-malformed.sh | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/regress/input-malformed.sh b/regress/input-malformed.sh index ac9364a6b..855af9f6c 100644 --- a/regress/input-malformed.sh +++ b/regress/input-malformed.sh @@ -2,13 +2,13 @@ . ./input-common.inc -# Large strings may take longer than start_cmd's delay to reach the parser. +# Wait for the terminator and following text to reach the parser. check_discard() { name=$1 printf 'OK\n' >"$EXP" i=0 - while [ "$i" -lt 100 ]; do + while [ "$i" -lt 300 ]; do capture_grid "$name" >"$TMP" cmp -s "$TMP" "$EXP" && return 0 sleep 0.05 @@ -17,6 +17,23 @@ check_discard() fail "$name (timed out waiting for discard)" } +# The missing-terminator timer may expire before the string limit is reached +# on a slow build. Clear any payload printed after the timeout, then check +# that the string did not change the title and normal output has resumed. +test_discard() +{ + name=$1 + prefix=$2 + start_cmd "$name" 8 3 "$INPUT_HOLD" + $TMUX select-pane -T discard-test || exit 1 + $TMUX respawn-pane -k \ + "perl -e 'print qq{$prefix}, q{x} x 1100000, qq{\e\\\\\e[H\e[2JOK}'; $INPUT_HOLD" || exit 1 + check_discard "$name" + $TMUX display-message -p -t "$name:" '#{pane_title}' >"$TMP" || exit 1 + printf 'discard-test\n' >"$EXP" + cmp "$TMP" "$EXP" || fail "$name title" +} + start_cmd csi-param-discard 8 3 \ "perl -e 'print qq{\e[}, q{1} x 80, qq{\030OK}'; sleep 2" check_capture csi-param-discard 'OK' @@ -25,13 +42,8 @@ start_cmd csi-interm-discard 8 3 \ "perl -e 'print qq{\e[ \030OK}'; sleep 2" check_capture csi-interm-discard 'OK' -start_cmd osc-discard 8 3 \ - "perl -e 'print qq{\e]2;}, q{x} x 1100000, qq{\e\\\\OK}'; exec cat" -check_discard osc-discard - -start_cmd apc-discard 8 3 \ - "perl -e 'print qq{\e_}, q{x} x 1100000, qq{\e\\\\OK}'; exec cat" -check_discard apc-discard +test_discard osc-discard '\e]2;' +test_discard apc-discard '\e_' start_pane unknown-csi 8 3 '\033[?9999zOK' check_capture unknown-csi 'OK' From 0e1f6ed80d7c5447b159304aa0323a0b59acdd7b Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 22 Sep 2026 06:46:50 +0000 Subject: [PATCH 03/10] Do not leak waiting clients (in wait-for) if they are killed, GitHub issue 5614. --- cmd-queue.c | 5 ++++- cmd-wait-for.c | 36 +++++++++++++++++++++++++++++++++++- server-client.c | 5 ++++- tmux.h | 3 ++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/cmd-queue.c b/cmd-queue.c index 4ecec8f7d..f96edd2dc 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-queue.c,v 1.123 2026/08/24 20:34:26 nicm Exp $ */ +/* $OpenBSD: cmd-queue.c,v 1.124 2026/09/22 06:46:50 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -581,6 +581,9 @@ cmdq_fire_command(struct cmdq_item *item) int flags, quiet = 0; char *tmp; + if (item->client != NULL && (item->client->flags & CLIENT_DEAD)) + return (CMD_RETURN_ERROR); + if (cfg_finished) cmdq_add_message(item); if (log_get_level() > 1) { diff --git a/cmd-wait-for.c b/cmd-wait-for.c index 0e4130fcd..7326c7519 100644 --- a/cmd-wait-for.c +++ b/cmd-wait-for.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-wait-for.c,v 1.23 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-wait-for.c,v 1.24 2026/09/22 06:46:50 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -480,6 +480,40 @@ cmd_wait_for_unlock(struct cmdq_item *item, const char *name, return (CMD_RETURN_NORMAL); } +void +cmd_wait_for_client_lost(struct client *c) +{ + struct wait_channel *wc, *wc1; + struct wait_item *wi, *wi1; + struct wait_event_item *wei, *wei1; + + TAILQ_FOREACH_SAFE(wei, &wait_event_items, entry, wei1) { + if (cmdq_get_client(wei->item) == c) { + TAILQ_REMOVE(&wait_event_items, wei, entry); + cmdq_continue(wei->item); + cmd_wait_for_event_free(wei); + } + } + + RB_FOREACH_SAFE(wc, wait_channels, &wait_channels, wc1) { + TAILQ_FOREACH_SAFE(wi, &wc->waiters, entry, wi1) { + if (cmdq_get_client(wi->item) == c) { + cmdq_continue(wi->item); + TAILQ_REMOVE(&wc->waiters, wi, entry); + free(wi); + } + } + TAILQ_FOREACH_SAFE(wi, &wc->lockers, entry, wi1) { + if (cmdq_get_client(wi->item) == c) { + cmdq_continue(wi->item); + TAILQ_REMOVE(&wc->lockers, wi, entry); + free(wi); + } + } + cmd_wait_for_remove_empty(wc); + } +} + void cmd_wait_for_flush(void) { diff --git a/server-client.c b/server-client.c index e5041e24c..7eac47087 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.513 2026/09/21 10:22:31 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.514 2026/09/22 06:46:50 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -389,6 +389,9 @@ server_client_lost(struct client *c) TAILQ_REMOVE(&clients, c, entry); log_debug("lost client %p", c); + cmd_wait_for_client_lost(c); + cmdq_next(c); + if (c->flags & CLIENT_ATTACHED) { server_client_attached_lost(c); events_fire_client("client-detached", c); diff --git a/tmux.h b/tmux.h index c292ade9d..086b49559 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1444 2026/09/21 12:14:32 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1445 2026/09/22 06:46:50 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3160,6 +3160,7 @@ void cmdq_print_data(struct cmdq_item *, struct evbuffer *); void printflike(2, 3) cmdq_error(struct cmdq_item *, const char *, ...); /* cmd-wait-for.c */ +void cmd_wait_for_client_lost(struct client *); void cmd_wait_for_flush(void); /* client.c */ From 4624bc2129c5e723a5386d36003599eb3f2e6bb7 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 22 Sep 2026 06:48:01 +0000 Subject: [PATCH 04/10] Correctly write Z indexes to v2 layouts if zoomed, GitHub issue 5624. --- layout-custom.c | 42 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/layout-custom.c b/layout-custom.c index 59bfeb075..514db1369 100644 --- a/layout-custom.c +++ b/layout-custom.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout-custom.c,v 1.42 2026/09/20 08:37:47 nicm Exp $ */ +/* $OpenBSD: layout-custom.c,v 1.43 2026/09/22 06:48:01 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott @@ -307,6 +307,37 @@ bad: return (xstrdup("0000,")); } +/* Get a floating pane cell's z-index in the layout being dumped. */ +static u_int +layout_cell_zindex(struct layout_cell *lc) +{ + struct window_pane *wp = lc->wp, *wq; + struct window *w = wp->window; + struct layout_cell *other; + int saved = (lc == wp->saved_layout_cell); + u_int i = 0; + + if (saved && + w->active != NULL && + (w->active->flags & PANE_ZOOMED) && + (w->active->saved_layout_cell->flags & LAYOUT_CELL_FLOATING)) { + if (wp == w->active) + return (0); + i++; + } + TAILQ_FOREACH(wq, &w->z_index, zentry) { + if (wq == wp) + break; + if (saved) + other = wq->saved_layout_cell; + else + other = wq->layout_cell; + if (other != NULL && (other->flags & LAYOUT_CELL_FLOATING)) + i++; + } + return (i); +} + /* Append information for a single cell in a JSON (v2) format. */ static int layout_append_v2(struct layout_cell *lc, struct layout_string *ls) @@ -315,7 +346,7 @@ layout_append_v2(struct layout_cell *lc, struct layout_string *ls) struct window_pane *wp; enum layout_type type; char c; - u_int i, n; + u_int i, n, z; if (lc == NULL) return (-1); @@ -356,9 +387,10 @@ layout_append_v2(struct layout_cell *lc, struct layout_string *ls) if (window_pane_index(wp, &i) != 0) return (-1); layout_string_write(ls, ",\"i\":%u", i); - if ((lc->flags & LAYOUT_CELL_FLOATING) && - window_pane_zindex(wp, &i) == 0) - layout_string_write(ls, ",\"z\":%u", i); + if (lc->flags & LAYOUT_CELL_FLOATING) { + z = layout_cell_zindex(lc); + layout_string_write(ls, ",\"z\":%u", z); + } layout_string_write(ls, ",\"I\":\"%%%u\"", wp->id); } From 19b766077827885d91a9d3a6af253f179499f47e Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 22 Sep 2026 06:49:47 +0000 Subject: [PATCH 05/10] Only rely on the previous line wrapping when the cursor is actually on it, GitHub issue 5625 from Ben Maurer. --- tty-draw.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tty-draw.c b/tty-draw.c index cc190f821..d707d1bf3 100644 --- a/tty-draw.c +++ b/tty-draw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-draw.c,v 1.16 2026/09/21 10:22:31 nicm Exp $ */ +/* $OpenBSD: tty-draw.c,v 1.17 2026/09/22 06:49:47 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -218,7 +218,12 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, } /* Did the previous line wrap on to this one? */ - if (py != 0 && atx == 0 && tty->cx >= tty->sx && nx == tty->sx) { + if (py != 0 && + atx == 0 && + tty->cx >= tty->sx && + tty->cy != UINT_MAX && + tty->cy + 1 == aty && + nx == tty->sx) { gl = grid_get_line(gd, gd->hsize + py - 1); if (gl->flags & GRID_LINE_WRAPPED) wrapped = 1; From 83f62d0acc548e6d58fec8a7332b48d913ce9bd6 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 22 Sep 2026 06:58:05 +0000 Subject: [PATCH 06/10] Add a feature for mintty's application escape to avoid dodgy terminals whinging about not supporting it, GitHub issue 5626 from Pete Dietl. --- tmux.1 | 9 +++++++-- tmux.h | 4 +++- tty-features.c | 16 +++++++++++++++- tty-term.c | 4 +++- tty.c | 8 +++----- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/tmux.1 b/tmux.1 index 1d5ef7344..3487b8831 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1171 2026/09/21 10:22:31 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1172 2026/09/22 06:58:05 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: September 21 2026 $ +.Dd $Mdocdate: September 22 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -5083,6 +5083,9 @@ The available features are: .Bl -tag -width Ds .It 256 Supports 256 colours with the SGR escape sequences. +.It appesc +Supports application escape key mode, where the Escape key sends a sequence +that cannot be mistaken for the start of another key. .It clipboard Allows setting the system clipboard. .It ccolour @@ -8831,6 +8834,8 @@ These are set automatically if the capability is present. .It Em \&Dseks , \&Eneks Disable and enable extended keys. +.It Em \&Dsesc , \&Enesc +Disable and enable application escape key mode. .It Em \&Dsfcs , \&Enfcs Disable and enable focus reporting. These are set automatically if the diff --git a/tmux.h b/tmux.h index 086b49559..f739014a2 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1445 2026/09/22 06:46:50 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1446 2026/09/22 06:58:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -455,6 +455,7 @@ enum tty_code_code { TTYC_DL1, TTYC_DSBP, TTYC_DSEKS, + TTYC_DSESC, TTYC_DSFCS, TTYC_DSMG, TTYC_E3, @@ -465,6 +466,7 @@ enum tty_code_code { TTYC_ENACS, TTYC_ENBP, TTYC_ENEKS, + TTYC_ENESC, TTYC_ENFCS, TTYC_ENMG, TTYC_FSL, diff --git a/tty-features.c b/tty-features.c index 56c692083..04ec4269b 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-features.c,v 1.43 2026/08/31 12:41:03 kirill Exp $ */ +/* $OpenBSD: tty-features.c,v 1.44 2026/09/22 06:58:06 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -179,6 +179,18 @@ static const struct tty_feature tty_feature_focus = { 0 }; +/* Terminal supports application escape key mode. */ +static const char *const tty_feature_appesc_capabilities[] = { + "Enesc=\\E[?7727h", + "Dsesc=\\E[?7727l", + NULL +}; +static const struct tty_feature tty_feature_appesc = { + "appesc", + tty_feature_appesc_capabilities, + 0 +}; + /* Terminal supports cursor styles. */ static const char *const tty_feature_cstyle_capabilities[] = { "Ss=\\E[%p1%d q", @@ -368,6 +380,7 @@ static const struct tty_feature tty_feature_utf8 = { /* Available terminal features. */ static const struct tty_feature *const tty_features[] = { &tty_feature_256, + &tty_feature_appesc, &tty_feature_bpaste, &tty_feature_ccolour, &tty_feature_clipboard, @@ -558,6 +571,7 @@ tty_default_features(struct client *c, const char *name, u_int version) "256,RGB,bpaste,clipboard,mouse,strikethrough,title" { .name = "mintty", .features = TTY_FEATURES_BASE_MODERN_XTERM "," + "appesc," "ccolour," "cstyle," "extkeys," diff --git a/tty-term.c b/tty-term.c index 3f3bc9c8d..95bc76189 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-term.c,v 1.109 2026/08/25 08:37:08 nicm Exp $ */ +/* $OpenBSD: tty-term.c,v 1.110 2026/09/22 06:58:06 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -87,6 +87,7 @@ static const struct tty_term_code_entry tty_term_codes[] = { [TTYC_DL1] = { TTYCODE_STRING, "dl1" }, [TTYC_DL] = { TTYCODE_STRING, "dl" }, [TTYC_DSEKS] = { TTYCODE_STRING, "Dseks" }, + [TTYC_DSESC] = { TTYCODE_STRING, "Dsesc" }, [TTYC_DSFCS] = { TTYCODE_STRING, "Dsfcs" }, [TTYC_DSBP] = { TTYCODE_STRING, "Dsbp" }, [TTYC_DSMG] = { TTYCODE_STRING, "Dsmg" }, @@ -98,6 +99,7 @@ static const struct tty_term_code_entry tty_term_codes[] = { [TTYC_ENACS] = { TTYCODE_STRING, "enacs" }, [TTYC_ENBP] = { TTYCODE_STRING, "Enbp" }, [TTYC_ENEKS] = { TTYCODE_STRING, "Eneks" }, + [TTYC_ENESC] = { TTYCODE_STRING, "Enesc" }, [TTYC_ENFCS] = { TTYCODE_STRING, "Enfcs" }, [TTYC_ENMG] = { TTYCODE_STRING, "Enmg" }, [TTYC_FSL] = { TTYCODE_STRING, "fsl" }, diff --git a/tty.c b/tty.c index bf9eb884f..3190bd8b0 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.481 2026/09/21 10:22:31 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.482 2026/09/22 06:58:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -497,8 +497,7 @@ tty_stop_tty(struct tty *tty) if (tty_term_has(tty->term, TTYC_DSBP)) tty_raw(tty, tty_term_string(tty->term, TTYC_DSBP)); - if (tty->term->flags & TERM_VT100LIKE) - tty_raw(tty, "\033[?7727l"); + tty_raw(tty, tty_term_string(tty->term, TTYC_DSESC)); tty_raw(tty, tty_term_string(tty->term, TTYC_DSFCS)); tty_raw(tty, tty_term_string(tty->term, TTYC_DSEKS)); @@ -555,8 +554,7 @@ tty_update_features(struct tty *tty) tty_puts(tty, tty_term_string(tty->term, TTYC_ENEKS)); if (options_get_number(global_options, "focus-events")) tty_puts(tty, tty_term_string(tty->term, TTYC_ENFCS)); - if (tty->term->flags & TERM_VT100LIKE) - tty_puts(tty, "\033[?7727h"); + tty_puts(tty, tty_term_string(tty->term, TTYC_ENESC)); /* * Features might have changed since the first draw during attach. For From 83c8962cf9f4daae0c7a442d636c7a3c835b5187 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 22 Sep 2026 14:25:51 +0100 Subject: [PATCH 07/10] Some tests. --- regress/layout-custom.sh | 41 +++++++- regress/sync-output-wrapped.sh | 116 ++++++++++++++++++++++ regress/wait-for-client-lost.sh | 164 ++++++++++++++++++++++++++++++++ 3 files changed, 317 insertions(+), 4 deletions(-) create mode 100755 regress/sync-output-wrapped.sh create mode 100644 regress/wait-for-client-lost.sh diff --git a/regress/layout-custom.sh b/regress/layout-custom.sh index 3b7a2de13..6fc7f76d4 100644 --- a/regress/layout-custom.sh +++ b/regress/layout-custom.sh @@ -377,10 +377,7 @@ must_equal 'Layout after select-pane back' "$(layout L:two)" "$SPLIT" # With nothing zoomed the two layout formats agree. # -# The zoomed case is deliberately not covered here. While a pane is zoomed -# #{window_layout} dumps the saved (unzoomed) layout and -# #{window_visible_layout} the zoomed one, but that depends on how zooming -# stashes the layout root rather than on anything in layout-custom.c. +# Zoomed layouts are checked below with floating panes. must_equal 'Visible layout' "$(visible_layout L:two)" "$SPLIT" # --------------------------------------------------------------------------- @@ -704,6 +701,42 @@ must_contain 'Floating layout back z-index' "$floating" '"z":1' check_ok select-layout -t L:float "$floating" must_equal 'Floating layout after round trip' "$(raw_layout L:float)" "$floating" +# A dump taken while zoomed must retain the unzoomed floating z-indexes. +# Check hidden floats, floats above zoom, and a mixture of the two. +for flags in '' A mixed; do + check_ok new-window -d -t L: -n zoom + zt=$($TMUX display-message -p -t L:zoom '#{pane_id}') + check_ok split-window -d -h -t "$zt" + case "$flags" in + A|mixed) first=-Ad ;; + *) first=-d ;; + esac + case "$flags" in + A) second=-Ad ;; + *) second=-d ;; + esac + check_ok new-pane "$first" -t "$zt" -x 20 -y 6 '' + check_ok new-pane "$second" -t "$zt" -x 30 -y 8 '' + for pane in 0 2 3; do + check_ok select-pane -t "L:zoom.$pane" + before=$(raw_layout L:zoom) + legacy=$(v1_layout L:zoom) + check_ok resize-pane -Z -t "L:zoom.$pane" + during=$(raw_layout L:zoom) + must_equal 'Layout while zoomed' "$during" "$before" + must_equal 'Legacy layout while zoomed' \ + "$(v1_layout L:zoom)" "$legacy" + must_equal 'Zoom after dumping layout' \ + "$($TMUX display-message -p -t L:zoom '#{window_zoomed_flag}')" 1 + must_differ 'Visible layout while zoomed' \ + "$(visible_layout L:zoom)" "$(layout L:zoom)" + check_ok select-layout -t L:zoom "$during" + must_equal 'Round trip from zoomed layout' \ + "$(raw_layout L:zoom)" "$before" + done + check_ok kill-window -t L:zoom +done + # --------------------------------------------------------------------------- # Floating panes and the legacy (v1) format. # diff --git a/regress/sync-output-wrapped.sh b/regress/sync-output-wrapped.sh new file mode 100755 index 000000000..0038eb913 --- /dev/null +++ b/regress/sync-output-wrapped.sh @@ -0,0 +1,116 @@ +#!/bin/sh + +# When only some lines are redrawn after a synchronized update, a line +# following a wrapped line must be drawn at its own position, not where the +# cursor happens to be. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +TMUX_TMPDIR=$DIR +export TMUX_TMPDIR + +INNER="$TEST_TMUX -Li$$ -f/dev/null" +OUTER="$TEST_TMUX -Lo$$ -f/dev/null" +CONTROL=$DIR/control +EMITTER=$DIR/emitter.pl + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_file() +{ + i=0 + while [ "$i" -lt 50 ] && [ ! -e "$1" ]; do + sleep 0.1 + i=$((i + 1)) + done + [ -e "$1" ] || fail "$2" +} + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + $INNER list-clients -F '#{client_termfeatures}' 2>/dev/null | + grep -q 'sync' && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "sync-capable client did not attach" +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +my $control = $ENV{CONTROL}; +(my $dir = $control) =~ s{/[^/]+$}{}; + +# Fill the screen with 80 column lines and no newlines so every line wraps. +my $out = "\e[H\e[2J"; +for my $row (1 .. 24) { + $out .= substr(sprintf("ROW%02d_", $row) . ('x' x 80), 0, 80); +} +syswrite STDOUT, $out . "\e[1;1H"; + +open my $painted, '>', "$dir/painted" or die "$dir/painted: $!\n"; +close $painted; +while (!-e $control) { + select undef, undef, undef, 0.01; +} + +# Change rows 2 and 6 in one synchronized update. +my $frame = "\e[?2026h" . + "\e[2;1H" . substr("NEW02_" . ('#' x 80), 0, 80) . + "\e[6;1H" . substr("NEW06_" . ('%' x 80), 0, 80) . + "\e[24;1H" . + "\e[?2026l"; +my $written = syswrite STDOUT, $frame; +die "short synchronized frame write\n" + unless defined $written && $written == length $frame; +open my $done, '>', "$dir/done" or die "$dir/done: $!\n"; +close $done; +select undef, undef, undef, 3; +PERL + +$INNER new-session -d -s inner -x 80 -y 24 \ + "CONTROL='$CONTROL' perl '$EMITTER'" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +$INNER set-option -as terminal-features '*:sync' || exit 1 + +$OUTER new-session -d -s outer -x 80 -y 24 \ + "$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" || + exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +wait_for_client + +wait_for_file "$DIR/painted" "application did not paint" +sleep 1 +: >"$CONTROL" +wait_for_file "$DIR/done" "application did not finish" +sleep 1 + +$OUTER capture-pane -p -t outer:0.0 >"$DIR/screen" || exit 1 +sed -n 2p "$DIR/screen" | grep -q '^NEW02_#' || fail "row 2 not redrawn" +sed -n 3p "$DIR/screen" | grep -q '^ROW03_x' || fail "row 3 overwritten" +sed -n 6p "$DIR/screen" | grep -q '^NEW06_%' || fail "row 6 not redrawn" +exit 0 diff --git a/regress/wait-for-client-lost.sh b/regress/wait-for-client-lost.sh new file mode 100644 index 000000000..cb36b22a4 --- /dev/null +++ b/regress/wait-for-client-lost.sh @@ -0,0 +1,164 @@ +#!/bin/sh + +# A disconnected waiter must release both its wait and its command queue. +PATH=/bin:/usr/bin +TERM=screen +export TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) +TMUX="$TEST_TMUX -S$OUT/socket -f/dev/null" +pids= + +cleanup() +{ + for pid in $pids; do + kill "$pid" 2>/dev/null || true + done + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +fail() +{ + echo "$*" >&2 + exit 1 +} + +wait_list() +{ + expected=$1 + shift + i=0 + while [ $i -lt 100 ]; do + value=$($TMUX wait-for -l "$@") || fail "list waiters failed" + [ "$value" = "$expected" ] && return + i=$((i + 1)) + sleep 0.05 + done + fail "expected waiters '$expected', got '$value'" +} + +wait_free() +{ + client_pid=$1 + i=0 + while [ $i -lt 100 ]; do + awk -v pid="$client_pid" ' + $4 == "IDENTIFY_CLIENTPID" && $5 == pid { address = $3 } + address != "" && $2 == "free" && $3 == "client" && + $4 == address && $5 == "(0" { freed = 1 } + END { exit !freed } + ' "$OUT"/tmux-server-*.log && return + i=$((i + 1)) + sleep 0.05 + done + fail "client $client_pid was not freed" +} + +cd "$OUT" || exit 1 +$TMUX -vv new-session -d -s test || fail "new-session failed" +$TMUX set -g @continued 0 || fail "set option failed" + +for signal in TERM KILL; do + for kind in channel lock event; do + name="$kind-$signal" + case $kind in + channel) args="$name"; list_args="$name" ;; + lock) + $TMUX wait-for -L "$name" || fail "lock failed" + args="-L $name"; list_args="$name" + ;; + event) args="-E @$name"; list_args="-E @$name" ;; + esac + + $TMUX wait-for $args \; set -g @continued 1 & + dead=$! + pids="$dead" + wait_list "client-$dead" $list_args + + $TMUX wait-for $args & + live=$! + pids="$dead $live" + wait_list "$(printf 'client-%s\nclient-%s' "$dead" "$live")" \ + $list_args + + kill -"$signal" "$dead" || fail "kill failed" + wait "$dead" 2>/dev/null || true + pids="$live" + wait_list "client-$live" $list_args + wait_free "$dead" + [ "$($TMUX show -gqv @continued)" = 0 ] || + fail "disconnected client's next command ran" + + case $kind in + channel) $TMUX wait-for -S "$name" || fail "signal failed" ;; + lock) $TMUX wait-for -U "$name" || fail "unlock failed" ;; + event) + $TMUX wait-for -E -w "client-$live" "@$name" || + fail "wake event waiter failed" + ;; + esac + wait_list "" $list_args + wait_free "$live" + wait "$live" || fail "surviving waiter failed" + pids= + if [ "$kind" = lock ]; then + $TMUX wait-for -U "$name" || fail "final unlock failed" + fi + done +done + +# Removing the last waiter must remove its otherwise unused channel, and must +# not create a pending signal. Unrelated pending signals must be preserved. +$TMUX wait-for -S saved || fail "save signal failed" +$TMUX wait-for last & +dead=$! +pids="$dead" +wait_list "client-$dead" last +kill -KILL "$dead" || fail "kill last waiter failed" +wait "$dead" 2>/dev/null || true +pids= +wait_list "" last +wait_free "$dead" +grep -F 'remove empty wait channel last' "$OUT"/tmux-server-*.log \ + >/dev/null || fail "empty channel was not removed" + +$TMUX wait-for saved \; wait-for last & +live=$! +pids="$live" +wait_list "client-$live" last +$TMUX wait-for -S last || fail "signal last failed" +wait_free "$live" +wait "$live" || fail "pending signal was not preserved" +pids= + +# Source-file leaves a cleanup callback behind the blocked command. +printf 'wait-for sourced\nset -g @continued 1\n' >"$OUT/source.conf" +$TMUX source-file "$OUT/source.conf" & +dead=$! +pids="$dead" +wait_list "client-$dead" sourced +kill -KILL "$dead" || fail "kill source-file waiter failed" +wait "$dead" 2>/dev/null || true +pids= +wait_list "" sourced +wait_free "$dead" +[ "$($TMUX show -gqv @continued)" = 0 ] || + fail "disconnected source-file client's next command ran" + +# Firing an event after cancellation must not call the removed event sink. +$TMUX wait-for -E window-renamed & +dead=$! +pids="$dead" +wait_list "client-$dead" -E window-renamed +kill -KILL "$dead" || fail "kill event waiter failed" +wait "$dead" 2>/dev/null || true +pids= +wait_list "" -E window-renamed +wait_free "$dead" +$TMUX rename-window -t test:0 renamed || fail "rename-window failed" +$TMUX display-message -p '#{pid}' >/dev/null || fail "server exited" + +exit 0 From e7618f41c23ba4f8434e792c5d82f267d64cad22 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 22 Sep 2026 14:10:26 +0000 Subject: [PATCH 08/10] Some terminals have extended DECSCUSR to allow it to reset to the default style and put that as the Se capability in their terminfo entry. Change so that if Ss and Se exist, tmux does not overwrite them with its own. GitHub issue 5476. --- tmux.h | 5 +++-- tty-features.c | 8 ++++---- tty-term.c | 10 +++++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/tmux.h b/tmux.h index f739014a2..ef161807d 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1446 2026/09/22 06:58:06 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1447 2026/09/22 14:10:26 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1708,6 +1708,7 @@ struct tty_term { #define TERM_VT100LIKE 0x20 #define TERM_SIXEL 0x40 #define TERM_INVALIDMS 0x80 +#define TERM_NOREPLACE 0x100 int flags; LIST_ENTRY(tty_term) entry; @@ -2942,7 +2943,7 @@ void tty_default_colours(struct grid_cell *, struct window_pane *, u_int *); /* tty-term.c */ extern struct tty_terms tty_terms; u_int tty_term_ncodes(void); -void tty_term_apply(struct tty_term *, const char *, int); +void tty_term_apply(struct tty_term *, const char *, int, int); void tty_term_apply_overrides(struct tty_term *); struct tty_term *tty_term_create(struct tty *, char *, char **, u_int, char **); void tty_term_free(struct tty_term *); diff --git a/tty-features.c b/tty-features.c index 04ec4269b..74559dcf1 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-features.c,v 1.44 2026/09/22 06:58:06 nicm Exp $ */ +/* $OpenBSD: tty-features.c,v 1.45 2026/09/22 14:10:26 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -200,7 +200,7 @@ static const char *const tty_feature_cstyle_capabilities[] = { static const struct tty_feature tty_feature_cstyle = { "cstyle", tty_feature_cstyle_capabilities, - 0 + TERM_NOREPLACE }; /* Terminal supports cursor colours. */ @@ -544,11 +544,11 @@ tty_apply_features(struct tty_term *term) capability = tf->capabilities; while (*capability != NULL) { log_debug("adding capability: %s", *capability); - tty_term_apply(term, *capability, 1); + tty_term_apply(term, *capability, 1, tf->flags); capability++; } } - term->flags |= tf->flags; + term->flags |= (tf->flags & ~TERM_NOREPLACE); if (tf == &tty_feature_utf8) c->flags |= CLIENT_UTF8; } diff --git a/tty-term.c b/tty-term.c index 95bc76189..f2d8b67b6 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-term.c,v 1.110 2026/09/22 06:58:06 nicm Exp $ */ +/* $OpenBSD: tty-term.c,v 1.111 2026/09/22 14:10:26 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -361,7 +361,8 @@ tty_term_override_next(const char *s, size_t *offset) } void -tty_term_apply(struct tty_term *term, const char *capabilities, int quiet) +tty_term_apply(struct tty_term *term, const char *capabilities, int quiet, + int flags) { const struct tty_term_code_entry *ent; struct tty_code *code; @@ -404,6 +405,9 @@ tty_term_apply(struct tty_term *term, const char *capabilities, int quiet) if (strcmp(s, ent->name) != 0) continue; code = &term->codes[i]; + if ((flags & TERM_NOREPLACE) && + code->type != TTYCODE_NONE) + continue; if (remove) { code->type = TTYCODE_NONE; @@ -456,7 +460,7 @@ tty_term_apply_overrides(struct tty_term *term) offset = 0; first = tty_term_override_next(s, &offset); if (first != NULL && fnmatch(first, term->name, 0) == 0) - tty_term_apply(term, s + offset, 0); + tty_term_apply(term, s + offset, 0, 0); a = options_array_next(a); } From 183c3432b05ff8c6de6ec2511d1253032df972d1 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 22 Sep 2026 16:56:07 +0000 Subject: [PATCH 09/10] Get x and y the right way round, fixes crash when opening a mode prompt when the client is offset into the window; may be the crash reported by sthen@. --- screen-redraw.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/screen-redraw.c b/screen-redraw.c index f25a8e70e..a52aec45f 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-redraw.c,v 1.160 2026/09/21 10:22:31 nicm Exp $ */ +/* $OpenBSD: screen-redraw.c,v 1.161 2026/09/22 16:56:07 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -1655,7 +1655,7 @@ redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp) prompt_draw(wp->prompt, &pdd); screen_write_stop(&ctx); - tty_draw_line(tty, &screen, 0, offset, width, px, cy, NULL); + tty_draw_line(tty, &screen, offset, 0, width, px, cy, NULL); screen_free(&screen); } From 20f76fb069a05889d0d5fe9de232492d2e281269 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Wed, 23 Sep 2026 07:37:06 +0100 Subject: [PATCH 10/10] tty: broaden DECSLRM (margins) terminal-feature coverage Any pane that doesn't span the terminal's full width - because pane-scrollbars is on (the scrollbar occupies a column) or the pane is one of a side-by-side split - needs DECSLRM (left/right margin) support to use the fast native-scroll path (tty_cmd_linefeed()/scrollup()/scrolldown()/ reverseindex(), tty.c: "(!tty_full_width(tty, ctx) && !tty_use_margin(tty))"). Without it, every single scroll falls back to tty_redraw_region()'s full manual repaint of the whole region - confirmed via -vv log ("tty_redraw_region: ... large region redraw") - which is a real source of flicker, and separately implicated in reports of image content not surviving a scroll in this area of the tree. The DECSLRM machinery itself (tty_margin_pane()/tty_margin()) is already built and already called from all four native-scroll dispatch functions. What was missing was terminal-capability coverage: - WezTerm and ghostty are already correctly identified via the existing XTVERSION mechanism (tty_keys_extended_device_attributes(), tty-keys.c), and both genuinely implement DECSLRM - confirmed directly in their own source (WezTerm: decslrm() in csi.rs, documented in their escape-sequences reference, a changelog entry fixing a DECSLRM bug confirming active use; ghostty: handled in dcs.zig/stream.zig, explicitly emitted alongside DECSTBM in their own formatter.zig, with its own terminfo entry for it). Neither's entry in tty_default_features()'s table (tty-features.c) granted "margins". This was a gap, not a detection problem, backed by source-level evidence rather than guesswork - kitty and Rio show zero DECSLRM references in their own source, so their omission is left as-is. - Windows Terminal's DECSLRM support was independently confirmed by direct, non-tmux escape-sequence testing, but it can never be identified via XTVERSION - the tracking issue for that (github.com/microsoft/terminal#18382) was explicitly closed not_planned by the maintainer, calling XTVERSION "not extensible or helpful for feature detection". Detected instead via the WT_SESSION environment variable Windows Terminal sets for every child process - a long-standing, stable signal already used for this exact purpose by many other tools. Checked in tty_term_create() (tty-term.c) alongside the existing COLORTERM-based RGB/256 detection, which already reads a named variable out of the attaching client's own environment (c->environ, populated via the MSG_IDENTIFY_ENVIRON handshake) the same way. Deliberately grants only "margins" for now, not the full modern-xterm feature bundle other entries get, since no other capability has been verified for it. New regress/tty-margins-wt-session.sh reproduces the scrollbar-pane scenario with WT_SESSION injected into the attaching client's environment and checks, via the -vv log, that no tty_redraw_region() fallback occurs - plus a sanity phase confirming the same scenario does fall back without WT_SESSION set, proving the test isn't accidentally trivial. Verified failing 3/3 against the pre-fix code and passing 5/5 standalone; full regress suite passes clean twice, with only pre-existing, unrelated failures (check-names.sh - confirmed identical on unmodified master; prompt-words-history.sh - already-known flake; four untracked image-*-noflash scratch tests left over from unrelated work on another branch, not part of this tree's tracked suite). Co-Authored-By: Claude Sonnet 5 --- regress/tty-margins-wt-session.sh | 124 ++++++++++++++++++++++++++++++ tty-features.c | 18 +++++ tty-term.c | 9 +++ 3 files changed, 151 insertions(+) create mode 100755 regress/tty-margins-wt-session.sh diff --git a/regress/tty-margins-wt-session.sh b/regress/tty-margins-wt-session.sh new file mode 100755 index 000000000..858bcc3d5 --- /dev/null +++ b/regress/tty-margins-wt-session.sh @@ -0,0 +1,124 @@ +#!/bin/sh + +# Windows Terminal cannot be identified by the XTVERSION mechanism +# tty_default_features() (tty-features.c) otherwise uses for every other +# terminal in its table - its maintainers have explicitly declined to +# implement it (github.com/microsoft/terminal issue 18382, closed +# not_planned). tty_term_create() (tty-term.c) instead detects it via the +# WT_SESSION environment variable Windows Terminal sets for every child +# process, granting it the "margins" (DECSLRM) terminal feature. +# +# Without DECSLRM, a pane that doesn't span the terminal's full width - +# because pane-scrollbars is on (the scrollbar occupies a column) or the +# pane is one of a side-by-side split - can't use the fast native-scroll +# path (tty_cmd_linefeed()/scrollup()/scrolldown()/reverseindex(), tty.c: +# "(!tty_full_width(tty, ctx) && !tty_use_margin(tty))") and falls back to +# tty_redraw_region()'s full manual repaint on every single scroll - a +# real, confirmed source of flicker (and, separately, of image content not +# surviving a scroll). +# +# This checks the actual server-side decision via the -vv log: a pane with +# pane-scrollbars on, scrolled several times, must never fall back to +# tty_redraw_region() when the attaching client's environment carries +# WT_SESSION - and must (as a sanity check that this scenario would +# otherwise hit the fallback at all) when it does not. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +cd "$DIR" || exit 1 +INNER="$TEST_TMUX -vv -Lwtsession-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lwtsession-outer-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + cd / + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null) + [ -n "$CLIENT" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +run_scroll_phase() +{ + label=$1 + attachcmd=$2 + + rm -f tmux-server*.log + + $INNER new-session -d -s inner -x 40 -y 6 'exec sh' || exit 1 + $INNER set -g status off || exit 1 + $INNER set -g window-size manual || exit 1 + $INNER set -g pane-scrollbars on || exit 1 + + $OUTER new-session -d -x 40 -y 6 || exit 1 + OUTERPANE=$($OUTER list-panes -F '#{pane_id}') || exit 1 + $OUTER set -g status off || exit 1 + $OUTER set -g window-size manual || exit 1 + $OUTER set -g default-terminal screen-256color || exit 1 + $OUTER send-keys -t "$OUTERPANE" -l "$attachcmd" || exit 1 + $OUTER send-keys -t "$OUTERPANE" Enter || exit 1 + sleep 1 + + wait_for_client + + i=0 + while [ "$i" -lt 8 ]; do + $INNER send-keys -t inner Enter || exit 1 + sleep 0.2 + i=$((i + 1)) + done + sleep 0.3 + + LOG=$(ls tmux-server*.log 2>/dev/null | head -1) + [ -n "$LOG" ] || fail "$label: sanity: no server -vv log was produced" + + n=$(grep -c "tty_redraw_region.*large region redraw" "$LOG") + + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + + echo "$n" +} + +# Phase 1: WT_SESSION present in the attaching client's environment - must +# never fall back to a full region redraw. +n_with=$(run_scroll_phase "with WT_SESSION" \ + "WT_SESSION=deadbeef-0000-0000-0000-000000000000 $INNER attach -t inner") +[ "$n_with" -eq 0 ] || + fail "with WT_SESSION set, scrolling a scrollbar-enabled pane still fell back to a full region redraw ($n_with times) - margins was not granted" + +# Phase 2: sanity check - without WT_SESSION, the same scenario must +# actually hit the fallback, proving phase 1 wasn't accidentally trivial. +# Explicitly strip WT_SESSION/WSLENV rather than relying on them being +# unset in the ambient environment - this test may itself be run from +# inside a real Windows Terminal/WSL session. +n_without=$(run_scroll_phase "without WT_SESSION" \ + "env -u WT_SESSION -u WSLENV $INNER attach -t inner") +[ "$n_without" -gt 0 ] || + fail "sanity: without WT_SESSION, scrolling a scrollbar-enabled pane never fell back to a full region redraw - this scenario no longer exercises the bug this test checks for" + +exit 0 diff --git a/tty-features.c b/tty-features.c index 0f3b64fa5..14f1b83b9 100644 --- a/tty-features.c +++ b/tty-features.c @@ -637,6 +637,7 @@ tty_default_features(struct client *c, const char *name, u_int version) "extkeys," "focus," "hyperlinks," + "margins," "usstyle" }, { .name = "ghostty", @@ -645,6 +646,7 @@ tty_default_features(struct client *c, const char *name, u_int version) "cstyle," "extkeys," "focus," + "margins," "overline," "hyperlinks," "osc7," @@ -675,6 +677,22 @@ tty_default_features(struct client *c, const char *name, u_int version) "cstyle," "extkeys," "focus" + }, + /* + * Windows Terminal cannot be identified by the XTVERSION + * mechanism used for the other entries above - its + * maintainers have declined to implement it (see + * github.com/microsoft/terminal issue 18382). It is instead + * detected via the WT_SESSION environment variable it sets + * for every child process (tty_term_create(), tty-term.c). + * DECSLRM support was independently confirmed by direct + * (non-tmux) testing; other capabilities have not been + * verified, so only margins is granted here - deliberately + * not the full modern-xterm feature bundle other entries + * get. + */ + { .name = "WindowsTerminal", + .features = "margins" } }; u_int i; diff --git a/tty-term.c b/tty-term.c index 1ac078cd4..944ce628e 100644 --- a/tty-term.c +++ b/tty-term.c @@ -645,6 +645,15 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, tty_parse_client_features(c, "256", ","); } + /* + * Windows Terminal cannot be identified by XTVERSION (its + * maintainers have declined to implement it), but it sets + * WT_SESSION for every child process - see the WindowsTerminal + * entry in tty_default_features()'s table (tty-features.c). + */ + if (environ_find(c->environ, "WT_SESSION") != NULL) + tty_default_features(c, "WindowsTerminal", 0); + /* Apply overrides so any capabilities used for features are changed. */ tty_term_apply_overrides(term);