From f466d6289ebeed6fb0d234527761279dd54fd655 Mon Sep 17 00:00:00 2001 From: r1w1s1 Date: Mon, 10 Aug 2026 15:38:55 -0300 Subject: [PATCH 01/20] compat: use socklen_t for getsockopt length --- compat/getpeereid.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compat/getpeereid.c b/compat/getpeereid.c index b79f420ad..ed2ed8743 100644 --- a/compat/getpeereid.c +++ b/compat/getpeereid.c @@ -31,7 +31,7 @@ getpeereid(int s, uid_t *uid, gid_t *gid) { #ifdef HAVE_SO_PEERCRED struct ucred uc; - int len = sizeof uc; + socklen_t len = sizeof uc; if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &uc, &len) == -1) return (-1); From 0ac8138ce388624641b495386b61dba75a1af5c9 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 17 Aug 2026 08:06:09 +0100 Subject: [PATCH 02/20] Add a break-pane test for customize mode. --- regress/mode-mutation.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/regress/mode-mutation.sh b/regress/mode-mutation.sh index 96927e995..aedeaca19 100644 --- a/regress/mode-mutation.sh +++ b/regress/mode-mutation.sh @@ -269,6 +269,22 @@ test_customize_mode() assert_alive "customize-mode exit" } +test_customize_break_pane() +{ + start_client option-break + side=$($TMUX split-window -d -P -F '#{pane_id}' \ + -t option-break:0 'cat') || fail "customize split failed" + + $TMUX customize-mode -t "$side" || fail "customize-mode failed" + wait_mode "$side" 1 + $TMUX break-pane -d -s "$side" || fail "customize break-pane failed" + + assert_alive "customize-mode break-pane" + wait_mode "$side" 1 + $TMUX send-keys -t "$side" q || fail "customize-mode quit failed" + wait_mode "$side" 0 +} + test_copy_mode() { start_client copy-a 'i=0; while [ $i -lt 200 ]; do echo "copy mutation line $i"; i=$((i + 1)); done; cat' @@ -302,6 +318,7 @@ test_choose_tree test_choose_buffer test_choose_client test_customize_mode +test_customize_break_pane test_copy_mode cleanup exit 0 From ec34ff52cc3aeddac6d68c92ede60d05ea261cf3 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 06:45:16 +0000 Subject: [PATCH 03/20] Do not copy too many positions when deleting, GitHub issue 5478 from Uzair Aftab. --- prompt.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prompt.c b/prompt.c index c12a249a9..cc808a54e 100644 --- a/prompt.c +++ b/prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD: prompt.c,v 1.5 2026/07/13 10:29:17 nicm Exp $ */ +/* $OpenBSD: prompt.c,v 1.6 2026/08/17 06:45:16 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -1334,7 +1334,7 @@ process_key: if (pr->index != size) { memmove(pr->buffer + pr->index, pr->buffer + pr->index + 1, - (size + 1 - pr->index) * + (size - pr->index) * sizeof *pr->buffer); goto changed; } From a0d3585c6c5200ebce6e48b1dee06cc4b5467ff5 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 07:04:45 +0000 Subject: [PATCH 04/20] Attach window to session earlier so resize cannot cause customize mode to blow up. Reported by Marcel Partap. --- cmd-break-pane.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd-break-pane.c b/cmd-break-pane.c index dcd4e8188..f2f1aff25 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-break-pane.c,v 1.75 2026/07/23 09:38:27 nicm Exp $ */ +/* $OpenBSD: cmd-break-pane.c,v 1.76 2026/08/17 07:04:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -174,13 +174,14 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) } window_set_fill_cells(w); + if (idx == -1) + idx = -1 - options_get_number(dst_s->options, "base-index"); + wl = session_attach(dst_s, w, idx, &cause); /* can't fail */ + layout_init(w, wp); wp->flags |= PANE_CHANGED; colour_palette_from_option(&wp->palette, wp->options); - if (idx == -1) - idx = -1 - options_get_number(dst_s->options, "base-index"); - wl = session_attach(dst_s, w, idx, &cause); /* can't fail */ window_remove_ref(w, __func__); events_fire_window("window-created", w); window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx); From d384557b636e51bd8b2cab63b52c2a748ee5a134 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 07:33:55 +0000 Subject: [PATCH 05/20] Remove flags from both keys for menus, GitHub issue 5484. --- menu.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/menu.c b/menu.c index aab25d7c5..2816056a4 100644 --- a/menu.c +++ b/menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD: menu.c,v 1.69 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: menu.c,v 1.70 2026/08/17 07:33:55 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -337,6 +337,7 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event) struct cmdq_state *state; enum cmd_parse_status status; char *error; + key_code key; if (KEYC_IS_MOUSE(event->key)) { /* @@ -387,7 +388,8 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event) name = menu->items[i].name; if (name == NULL || *name == '-') continue; - if ((event->key & ~KEYC_MASK_FLAGS) == menu->items[i].key) { + key = (event->key & ~KEYC_MASK_FLAGS); + if (key == (menu->items[i].key & ~KEYC_MASK_FLAGS)) { md->choice = i; goto chosen; } From 9fdd04ae61db00dbf754289ecceb70a84af4e8c1 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 07:52:16 +0000 Subject: [PATCH 06/20] Add default terminal features for Rio, GitHub issue 5489 from Raphael Amorim. --- tty-features.c | 14 +++++++++++++- tty-keys.c | 4 +++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tty-features.c b/tty-features.c index 614c63f62..62ec327b8 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-features.c,v 1.40 2026/07/01 06:17:58 nicm Exp $ */ +/* $OpenBSD: tty-features.c,v 1.41 2026/08/17 07:52:16 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -586,6 +586,18 @@ tty_default_features(int *feat, const char *name, u_int version) "usstyle," "progressbar" }, + { .name = "Rio", + .features = TTY_FEATURES_BASE_MODERN_XTERM "," + "ccolour," + "cstyle," + "focus," + "overline," + "hyperlinks," + "osc7," + "sync," + "usstyle," + "progressbar" + }, { .name = "XTerm", /* * xterm also supports DECSLRM and DECFRA, but they can be diff --git a/tty-keys.c b/tty-keys.c index 2d4454b6c..7ece78639 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-keys.c,v 1.211 2026/07/21 07:12:49 nicm Exp $ */ +/* $OpenBSD: tty-keys.c,v 1.212 2026/08/17 07:52:16 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1667,6 +1667,8 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, tty_default_features(features, "WezTerm", 0); else if (strncmp(tmp, "ghostty ", 8) == 0) tty_default_features(features, "ghostty", 0); + else if (strncmp(tmp, "Rio ", 4) == 0) + tty_default_features(features, "Rio", 0); log_debug("%s: received extended DA %.*s", c->name, (int)*size, buf); free(c->term_type); From 1729bb8e8af4a1deddd772761e8c058092cd4cfe Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 07:56:56 +0000 Subject: [PATCH 07/20] If writing a file fails, propagate the error to the server via a new message. Use a client flag rather than bumping the protocol version. GitHub issue 5451. --- client.c | 4 +-- file.c | 86 ++++++++++++++++++++++++++++++++++++++----------- server-client.c | 6 +++- tmux-protocol.h | 10 ++++-- tmux.h | 5 +-- 5 files changed, 86 insertions(+), 25 deletions(-) diff --git a/client.c b/client.c index f413dc819..a59216823 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: client.c,v 1.166 2026/07/10 15:45:11 nicm Exp $ */ +/* $OpenBSD: client.c,v 1.167 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -277,7 +277,7 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, proc_set_signals(client_proc, client_signal); /* Save the flags. */ - client_flags = flags; + client_flags = flags|CLIENT_WRITE_ACK; log_debug("flags are %#llx", (unsigned long long)client_flags); /* Initialize the client socket and start the server. */ diff --git a/file.c b/file.c index 4207f8053..fd73f98df 100644 --- a/file.c +++ b/file.c @@ -1,4 +1,4 @@ -/* $OpenBSD: file.c,v 1.21 2026/07/26 15:08:15 nicm Exp $ */ +/* $OpenBSD: file.c,v 1.22 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -505,7 +505,8 @@ file_push(struct client_file *cf) } else if (cf->stream > 2) { close.stream = cf->stream; proc_send(cf->peer, MSG_WRITE_CLOSE, -1, &close, sizeof close); - file_fire_done(cf); + if (cf->c == NULL || (~cf->c->flags & CLIENT_WRITE_ACK)) + file_fire_done(cf); } free(msg); } @@ -530,14 +531,48 @@ file_write_left(struct client_files *files) return (waiting != 0); } +/* Finish writing a client file. */ +static void +file_write_finished(struct client_file *cf) +{ + struct msg_write_done msg; + + if (cf->event != NULL) { + bufferevent_free(cf->event); + cf->event = NULL; + } + if (cf->fd != -1) { + if (close(cf->fd) != 0 && cf->error == 0) + cf->error = errno; + cf->fd = -1; + } + + msg.stream = cf->stream; + msg.error = cf->error; + proc_send(cf->peer, MSG_WRITE_DONE, -1, &msg, sizeof msg); + + if (cf->cb != NULL) + cf->cb(NULL, NULL, 0, -1, NULL, cf->data); + file_free(cf); +} + /* Client file write error callback. */ static void -file_write_error_callback(__unused struct bufferevent *bev, __unused short what, +file_write_error_callback(__unused struct bufferevent *bev, short what, void *arg) { struct client_file *cf = arg; + int error; + + if (what & EVBUFFER_ERROR) + error = errno; + else + error = EIO; + if (error == 0) + error = EIO; log_debug("write error file %d", cf->stream); + cf->error = error; bufferevent_free(cf->event); cf->event = NULL; @@ -545,7 +580,9 @@ file_write_error_callback(__unused struct bufferevent *bev, __unused short what, close(cf->fd); cf->fd = -1; - if (cf->cb != NULL) + if (cf->closed) + file_write_finished(cf); + else if (cf->cb != NULL) cf->cb(NULL, NULL, 0, -1, NULL, cf->data); } @@ -557,15 +594,10 @@ file_write_callback(__unused struct bufferevent *bev, void *arg) log_debug("write check file %d", cf->stream); - if (cf->cb != NULL) + if (cf->closed && EVBUFFER_LENGTH(cf->event->output) == 0) + file_write_finished(cf); + else if (cf->cb != NULL) cf->cb(NULL, NULL, 0, -1, NULL, cf->data); - - if (cf->closed && EVBUFFER_LENGTH(cf->event->output) == 0) { - bufferevent_free(cf->event); - close(cf->fd); - RB_REMOVE(client_files, cf->tree, cf); - file_free(cf); - } } /* Handle a file write open message (client). */ @@ -666,14 +698,10 @@ file_write_close(struct client_files *files, struct imsg *imsg) if ((cf = RB_FIND(client_files, files, &find)) == NULL) fatalx("unknown stream number"); log_debug("close file %d", cf->stream); + cf->closed = 1; if (cf->event == NULL || EVBUFFER_LENGTH(cf->event->output) == 0) { - if (cf->event != NULL) - bufferevent_free(cf->event); - if (cf->fd != -1) - close(cf->fd); - RB_REMOVE(client_files, files, cf); - file_free(cf); + file_write_finished(cf); } } @@ -832,6 +860,28 @@ file_write_ready(struct client_files *files, struct imsg *imsg) return (0); } +/* Handle a write done message (server). */ +int +file_write_done(struct client_files *files, struct imsg *imsg) +{ + struct msg_write_done *msg = imsg->data; + size_t msglen = imsg->hdr.len - IMSG_HEADER_SIZE; + struct client_file find, *cf; + + if (msglen != sizeof *msg) + return (-1); + find.stream = msg->stream; + if ((cf = RB_FIND(client_files, files, &find)) == NULL) + return (0); + if (cf->c == NULL || (~cf->c->flags & CLIENT_WRITE_ACK)) + return (0); + + log_debug("file %d write done", cf->stream); + cf->error = msg->error; + file_fire_done(cf); + return (0); +} + /* Handle read data message (server). */ int file_read_data(struct client_files *files, struct imsg *imsg) diff --git a/server-client.c b/server-client.c index 27b7688fe..4fe4917ad 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.502 2026/08/04 11:18:22 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.503 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -2700,6 +2700,10 @@ server_client_dispatch(struct imsg *imsg, void *arg) if (file_write_ready(&c->files, imsg) != 0) goto bad; break; + case MSG_WRITE_DONE: + if (file_write_done(&c->files, imsg) != 0) + goto bad; + break; case MSG_READ: if (file_read_data(&c->files, imsg) != 0) goto bad; diff --git a/tmux-protocol.h b/tmux-protocol.h index d823ce7e2..396f10bc9 100644 --- a/tmux-protocol.h +++ b/tmux-protocol.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux-protocol.h,v 1.2 2023/01/06 07:09:27 nicm Exp $ */ +/* $OpenBSD: tmux-protocol.h,v 1.3 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2021 Nicholas Marriott @@ -67,7 +67,8 @@ enum msgtype { MSG_WRITE, MSG_WRITE_READY, MSG_WRITE_CLOSE, - MSG_READ_CANCEL + MSG_READ_CANCEL, + MSG_WRITE_DONE }; /* @@ -116,4 +117,9 @@ struct msg_write_close { int stream; }; +struct msg_write_done { + int stream; + int error; +}; + #endif /* TMUX_PROTOCOL_H */ diff --git a/tmux.h b/tmux.h index 15b997b76..666f901e4 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1422 2026/08/05 08:54:56 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1423 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2252,7 +2252,7 @@ struct client { /* 0x800000000ULL unused */ #define CLIENT_BRACKETPASTING 0x1000000000ULL #define CLIENT_ASSUMEPASTING 0x2000000000ULL -/* 0x4000000000ULL unused */ +#define CLIENT_WRITE_ACK 0x4000000000ULL #define CLIENT_NO_DETACH_ON_DESTROY 0x8000000000ULL #define CLIENT_ALLREDRAWFLAGS \ (CLIENT_REDRAWWINDOW| \ @@ -3235,6 +3235,7 @@ void file_write_close(struct client_files *, struct imsg *); void file_read_open(struct client_files *, struct tmuxpeer *, struct imsg *, int, int, client_file_cb, void *); int file_write_ready(struct client_files *, struct imsg *); +int file_write_done(struct client_files *, struct imsg *); int file_read_data(struct client_files *, struct imsg *); int file_read_done(struct client_files *, struct imsg *); void file_read_cancel(struct client_files *, struct imsg *); From 0f241b74472387a4698c9eb50e09f1817b5dd2e7 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 17 Aug 2026 13:59:10 +0100 Subject: [PATCH 08/20] Update CHANGES. --- CHANGES | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGES b/CHANGES index 80016b0e1..093e29fc9 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,4 @@ -CHANGES FROM 3.7b TO 3.8 - -* Build with jemalloc on macOS to avoid what appears to be a bug in the system - calloc(3) (issue 5385). +CHANGES FROM 3.7c TO 3.8 * Many improvements to floating panes: @@ -121,8 +118,6 @@ CHANGES FROM 3.7b TO 3.8 * Copy mode no longer exits at the bottom while a selection is in progress (issue 5349). -* Fix scrollbar initial state so scrollbars appear on new windows (issue 5339). - * Add style attributes for dimming colours (dim=) and for hyperlinks (link= and nolink) (issues 4842 and 4280 from Moritz Angermann). @@ -169,7 +164,18 @@ CHANGES FROM 3.7b TO 3.8 * Do not crash looking for the next or previous session (issue 5344), or when no client is available. -* Check time periodically in loops rather than for every item (issue 5367). +CHANGES FROM 3.7b TO 3.7c + +* Build with jemalloc on macOS to avoid what appears to be a bug in calloc + (issue 5385). + +* Fix scrollbar initial state so they appear on new windows (issue 5339). + +* Check time periodically in loops rather than every one (issue 5367). + +* Use message-style again as default for message-format. + +* Unzoom before creating floating panes to avoid a crash. CHANGES FROM 3.7a TO 3.7b From a18d4e00679e02d464cd284a9cb6a1453d8a3b26 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 14:47:41 +0000 Subject: [PATCH 09/20] Allow features to be disabled using @ suffix in terminal-features, from Michael Grant. --- tmux.1 | 8 ++++++-- tmux.c | 6 +++--- tmux.h | 16 ++++++++------- tty-features.c | 54 ++++++++++++++++++++++++++++++++++++++------------ tty-keys.c | 35 +++++++++++++++----------------- tty-term.c | 21 ++++++++++---------- tty.c | 6 +++--- 7 files changed, 89 insertions(+), 57 deletions(-) diff --git a/tmux.1 b/tmux.1 index d0f54c91e..de987b22d 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1155 2026/08/06 09:05:04 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1156 2026/08/17 14:47:41 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: August 6 2026 $ +.Dd $Mdocdate: August 17 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -5045,6 +5045,10 @@ This is an array option where each entry is a colon-separated string made up of a terminal type pattern (matched using .Xr glob 7 patterns) followed by a list of terminal features. +A feature may be suffixed with +.Ql @ +to disable it; for example, +.Ql xterm*:sync@ . The available features are: .Bl -tag -width Ds .It 256 diff --git a/tmux.c b/tmux.c index 822e9efd4..18f4603bb 100644 --- a/tmux.c +++ b/tmux.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.c,v 1.222 2026/07/19 19:09:30 nicm Exp $ */ +/* $OpenBSD: tmux.c,v 1.223 2026/08/17 14:47:41 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -418,7 +418,7 @@ main(int argc, char **argv) while ((opt = getopt(argc, argv, "2c:CDdf:hlL:NqS:T:uUvV")) != -1) { switch (opt) { case '2': - tty_add_features(&feat, "256", ":,"); + tty_parse_features("256", ":,", &feat, NULL); break; case 'c': shell_command = optarg; @@ -466,7 +466,7 @@ main(int argc, char **argv) path = xstrdup(optarg); break; case 'T': - tty_add_features(&feat, optarg, ":,"); + tty_parse_features(optarg, ":,", &feat, NULL); break; case 'u': flags |= CLIENT_UTF8; diff --git a/tmux.h b/tmux.h index 666f901e4..6cd104b50 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1423 2026/08/17 07:56:56 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1424 2026/08/17 14:47:41 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1684,7 +1684,7 @@ struct key_event { struct tty_term { char *name; struct tty *tty; - int features; + int applied_features; char acs[UCHAR_MAX + 1][2]; @@ -2185,6 +2185,7 @@ struct client { char *term_name; int term_features; + int term_nofeatures; char *term_type; char **term_caps; u_int term_ncaps; @@ -2954,8 +2955,7 @@ 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_overrides(struct tty_term *); -struct tty_term *tty_term_create(struct tty *, char *, char **, u_int, int *, - char **); +struct tty_term *tty_term_create(struct tty *, char *, char **, u_int, char **); void tty_term_free(struct tty_term *); int tty_term_read_list(const char *, int, char ***, u_int *, char **); @@ -2977,11 +2977,13 @@ int tty_term_flag(struct tty_term *, enum tty_code_code); const char *tty_term_describe(struct tty_term *, enum tty_code_code); /* tty-features.c */ -void tty_add_features(int *, const char *, const char *); +void tty_parse_client_features(struct client *, const char *, + const char *); +void tty_parse_features(const char *, const char *, int *, int *); const char *tty_get_features(int); int tty_feature_present(struct tty_term *, const char *); -int tty_apply_features(struct tty_term *, int); -void tty_default_features(int *, const char *, u_int); +int tty_apply_features(struct tty_term *); +void tty_default_features(struct client *, const char *, u_int); /* tty-acs.c */ int tty_acs_needed(struct tty *); diff --git a/tty-features.c b/tty-features.c index 62ec327b8..cebdeb033 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-features.c,v 1.41 2026/08/17 07:52:16 nicm Exp $ */ +/* $OpenBSD: tty-features.c,v 1.42 2026/08/17 14:47:41 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -383,17 +383,29 @@ static const struct tty_feature *const tty_features[] = { &tty_feature_usstyle }; +/* Parse features for client. */ void -tty_add_features(int *feat, const char *s, const char *separators) +tty_parse_client_features(struct client *c, const char *s, const char *sep) +{ + tty_parse_features(s, sep, &c->term_features, &c->term_nofeatures); +} + +/* Parse features list. */ +void +tty_parse_features(const char *s, const char *sep, int *enabled, int *disabled) { const struct tty_feature *tf; char *next, *loop, *copy; u_int i; + int remove; log_debug("adding terminal features %s", s); loop = copy = xstrdup(s); - while ((next = strsep(&loop, separators)) != NULL) { + while ((next = strsep(&loop, sep)) != NULL) { + remove = (*next != '\0' && next[strlen(next) - 1] == '@'); + if (remove) + next[strlen(next) - 1] = '\0'; for (i = 0; i < nitems(tty_features); i++) { tf = tty_features[i]; if (strcasecmp(tf->name, next) == 0) @@ -403,14 +415,24 @@ tty_add_features(int *feat, const char *s, const char *separators) log_debug("unknown terminal feature: %s", next); break; } - if (~(*feat) & (1 << i)) { + if (remove) { + log_debug("removing terminal feature: %s", tf->name); + *enabled &= ~(1 << i); + if (disabled != NULL) + *disabled |= 1 << i; + continue; + } + if (disabled != NULL && *disabled & (1 << i)) + continue; + if (~(*enabled) & (1 << i)) { log_debug("adding terminal feature: %s", tf->name); - (*feat) |= (1 << i); + (*enabled) |= (1 << i); } } free(copy); } +/* Get features as string. */ const char * tty_get_features(int feat) { @@ -432,6 +454,7 @@ tty_get_features(int feat) return (s); } +/* Check if feature is present. */ int tty_feature_present(struct tty_term *term, const char *name) { @@ -443,8 +466,8 @@ tty_feature_present(struct tty_term *term, const char *name) for (i = 0; i < nitems(tty_features); i++) { tf = tty_features[i]; if (strcmp(tf->name, name) == 0) { - if (term->features & (1 << i)) - return (1); + if (term->applied_features & (1 << i)) + return (1); break; } } @@ -471,19 +494,23 @@ tty_feature_present(struct tty_term *term, const char *name) return (1); } +/* Apply featurs to terminal. */ int -tty_apply_features(struct tty_term *term, int feat) +tty_apply_features(struct tty_term *term) { + struct client *c = term->tty->client; const struct tty_feature *tf; const char *const *capability; + int feat; u_int i; + feat = (c->term_features & ~c->term_nofeatures); if (feat == 0) return (0); log_debug("applying terminal features: %s", tty_get_features(feat)); for (i = 0; i < nitems(tty_features); i++) { - if ((term->features & (1 << i)) || (~feat & (1 << i))) + if ((term->applied_features & (1 << i)) || (~feat & (1 << i))) continue; tf = tty_features[i]; @@ -498,14 +525,15 @@ tty_apply_features(struct tty_term *term, int feat) } term->flags |= tf->flags; } - if ((term->features | feat) == term->features) + if ((term->applied_features|feat) == term->applied_features) return (0); - term->features |= feat; + term->applied_features |= feat; return (1); } +/* Add default features for a terminal identified by name and version. */ void -tty_default_features(int *feat, const char *name, u_int version) +tty_default_features(struct client *c, const char *name, u_int version) { static const struct { const char *name; @@ -618,6 +646,6 @@ tty_default_features(int *feat, const char *name, u_int version) continue; if (version != 0 && version < table[i].version) continue; - tty_add_features(feat, table[i].features, ","); + tty_parse_client_features(c, table[i].features, ","); } } diff --git a/tty-keys.c b/tty-keys.c index 7ece78639..6c2289495 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-keys.c,v 1.212 2026/08/17 07:52:16 nicm Exp $ */ +/* $OpenBSD: tty-keys.c,v 1.213 2026/08/17 14:47:41 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1447,7 +1447,6 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, size_t *size) { struct client *c = tty->client; - int *features = &c->term_features; u_int i, n = 0; char tmp[128], *endptr, p[32] = { 0 }, *cp, *next; @@ -1504,13 +1503,13 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, for (i = 1; i < n; i++) { log_debug("%s: DA feature: %d", c->name, p[i]); if (p[i] == 4) - tty_add_features(features, "sixel", ","); + tty_parse_client_features(c, "sixel", ","); if (p[i] == 21) - tty_add_features(features, "margins", ","); + tty_parse_client_features(c, "margins", ","); if (p[i] == 28) - tty_add_features(features, "rectfill", ","); + tty_parse_client_features(c, "rectfill", ","); if (p[i] == 52) - tty_add_features(features, "clipboard", ","); + tty_parse_client_features(c, "clipboard", ","); } break; } @@ -1531,7 +1530,6 @@ tty_keys_device_attributes2(struct tty *tty, const char *buf, size_t len, size_t *size) { struct client *c = tty->client; - int *features = &c->term_features; u_int i, n = 0; char tmp[128], *endptr, p[32] = { 0 }, *cp, *next; @@ -1585,13 +1583,13 @@ tty_keys_device_attributes2(struct tty *tty, const char *buf, size_t len, */ switch (p[0]) { case 'M': /* mintty */ - tty_default_features(features, "mintty", 0); + tty_default_features(c, "mintty", 0); break; case 'T': /* tmux */ - tty_default_features(features, "tmux", 0); + tty_default_features(c, "tmux", 0); break; case 'U': /* rxvt-unicode */ - tty_default_features(features, "rxvt-unicode", 0); + tty_default_features(c, "rxvt-unicode", 0); break; } log_debug("%s: received secondary DA %.*s", c->name, (int)*size, buf); @@ -1611,7 +1609,6 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, size_t len, size_t *size) { struct client *c = tty->client; - int *features = &c->term_features; u_int i; char tmp[128]; @@ -1654,21 +1651,21 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, /* Add terminal features. */ if (strncmp(tmp, "iTerm2 ", 7) == 0) - tty_default_features(features, "iTerm2", 0); + tty_default_features(c, "iTerm2", 0); else if (strncmp(tmp, "tmux ", 5) == 0) - tty_default_features(features, "tmux", 0); + tty_default_features(c, "tmux", 0); else if (strncmp(tmp, "XTerm(", 6) == 0) - tty_default_features(features, "XTerm", 0); + tty_default_features(c, "XTerm", 0); else if (strncmp(tmp, "mintty ", 7) == 0) - tty_default_features(features, "mintty", 0); + tty_default_features(c, "mintty", 0); else if (strncmp(tmp, "foot(", 5) == 0) - tty_default_features(features, "foot", 0); + tty_default_features(c, "foot", 0); else if (strncmp(tmp, "WezTerm ", 7) == 0) - tty_default_features(features, "WezTerm", 0); + tty_default_features(c, "WezTerm", 0); else if (strncmp(tmp, "ghostty ", 8) == 0) - tty_default_features(features, "ghostty", 0); + tty_default_features(c, "ghostty", 0); else if (strncmp(tmp, "Rio ", 4) == 0) - tty_default_features(features, "Rio", 0); + tty_default_features(c, "Rio", 0); log_debug("%s: received extended DA %.*s", c->name, (int)*size, buf); free(c->term_type); diff --git a/tty-term.c b/tty-term.c index 7a4b89ad7..99341c0ce 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-term.c,v 1.107 2026/08/05 08:54:56 nicm Exp $ */ +/* $OpenBSD: tty-term.c,v 1.108 2026/08/17 14:47:41 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -541,8 +541,9 @@ tty_term_validate(struct tty_term *term) struct tty_term * tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, - int *feat, char **cause) + char **cause) { + struct client *c = tty->client; struct tty_term *term; const struct tty_term_code_entry *ent; struct tty_code *code; @@ -614,19 +615,19 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, offset = 0; first = tty_term_override_next(s, &offset); if (first != NULL && fnmatch(first, term->name, 0) == 0) - tty_add_features(feat, s + offset, ":"); + tty_parse_client_features(c, s + offset, ":"); a = options_array_next(a); } /* Check for COLORTERM. */ - envent = environ_find(tty->client->environ, "COLORTERM"); + envent = environ_find(c->environ, "COLORTERM"); if (envent != NULL) { - log_debug("%s COLORTERM=%s", tty->client->name, envent->value); + log_debug("%s COLORTERM=%s", c->name, envent->value); if (strcasecmp(envent->value, "truecolor") == 0 || strcasecmp(envent->value, "24bit") == 0) - tty_add_features(feat, "RGB", ","); + tty_parse_client_features(c, "RGB", ","); else if (strstr(envent->value, "256") != NULL) - tty_add_features(feat, "256", ","); + tty_parse_client_features(c, "256", ","); } /* Apply overrides so any capabilities used for features are changed. */ @@ -657,17 +658,17 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, s = tty_term_string(term, TTYC_CLEAR); if (tty_term_flag(term, TTYC_XT) || strncmp(s, "\033[", 2) == 0) { term->flags |= TERM_VT100LIKE; - tty_add_features(feat, "bpaste,focus,title", ","); + tty_parse_client_features(c, "bpaste,focus,title", ","); } /* Add RGB feature if terminal has RGB colours. */ if ((tty_term_flag(term, TTYC_TC) || tty_term_has(term, TTYC_RGB)) && (!tty_term_has(term, TTYC_SETRGBF) || !tty_term_has(term, TTYC_SETRGBB))) - tty_add_features(feat, "RGB", ","); + tty_parse_client_features(c, "RGB", ","); /* Apply the features and overrides again. */ - if (tty_apply_features(term, *feat)) + if (tty_apply_features(term)) tty_term_apply_overrides(term); /* Log the capabilities. */ diff --git a/tty.c b/tty.c index 4f2c6a33e..8922efa07 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.477 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.478 2026/08/17 14:47:41 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -272,7 +272,7 @@ tty_open(struct tty *tty, char **cause) struct client *c = tty->client; tty->term = tty_term_create(tty, c->term_name, c->term_caps, - c->term_ncaps, &c->term_features, cause); + c->term_ncaps, cause); if (tty->term == NULL) { tty_close(tty); return (-1); @@ -531,7 +531,7 @@ tty_update_features(struct tty *tty) { struct client *c = tty->client; - if (tty_apply_features(tty->term, c->term_features)) + if (tty_apply_features(tty->term)) tty_term_apply_overrides(tty->term); if (tty_use_margin(tty)) From 30abbe36a818632a6099347fcd20be35a3d53785 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 17 Aug 2026 21:04:26 +0100 Subject: [PATCH 10/20] Regress for sync, from xiangzhedev@gmail.com. --- regress/sync-output-atomic.sh | 173 ++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 regress/sync-output-atomic.sh diff --git a/regress/sync-output-atomic.sh b/regress/sync-output-atomic.sh new file mode 100644 index 000000000..d0979ebe1 --- /dev/null +++ b/regress/sync-output-atomic.sh @@ -0,0 +1,173 @@ +#!/bin/sh + +# Application synchronized updates must remain one physical client transaction, +# including when BSU, printable cells and ESU arrive in one pane read. + +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 -Lsync-output-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lsync-output-outer-$$ -f/dev/null" +CLIENT_BYTES=$DIR/client-bytes +CONTROL=$DIR/control +EMITTER=$DIR/emitter.pl +ASSERT=$DIR/assert-sync-output.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_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" +} + +wait_for_marker() +{ + i=0 + while [ "$i" -lt 50 ]; do + grep -q 'FRAME_000001_ROW_23' "$CLIENT_BYTES" 2>/dev/null && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "client did not receive both synchronized frames" +} + +wait_for_stable_bytes() +{ + previous=-1 + stable=0 + i=0 + while [ "$i" -lt 50 ]; do + current=$(wc -c <"$CLIENT_BYTES" 2>/dev/null) || current=0 + if [ "$current" -gt 0 ] && [ "$current" -eq "$previous" ]; then + stable=$((stable + 1)) + [ "$stable" -eq 5 ] && return 0 + else + stable=0 + fi + previous=$current + sleep 0.1 + i=$((i + 1)) + done + fail "client byte stream did not become stable" +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +my $control = $ENV{CONTROL}; +(my $dir = $control) =~ s{/[^/]+$}{}; +open my $ready, '>', "$dir/ready" or die "$dir/ready: $!\n"; +close $ready; +while (!-e $control) { + select undef, undef, undef, 0.01; +} +for my $frame (0 .. 1) { + my @rows; + for my $row (0 .. 23) { + my $marker = sprintf "FRAME_%06d_ROW_%02d_", $frame, $row; + push @rows, substr($marker . ('X' x 79), 0, 79); + } + my $frame = "\e[?2026h\e[H" . join("\r\n", @rows) . + "\e[?2026l"; + my $written = syswrite STDOUT, $frame; + die "short synchronized frame write\n" + unless defined $written && $written == length $frame; + select undef, undef, undef, 0.2; +} +select undef, undef, undef, 2; +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 -Lsync-output-inner-$$ -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 + +i=0 +while [ "$i" -lt 50 ] && [ ! -e "$DIR/ready" ]; do + sleep 0.1 + i=$((i + 1)) +done +[ -e "$DIR/ready" ] || fail "application emitter did not become ready" +$OUTER pipe-pane -O -t outer:0.0 "cat >'$CLIENT_BYTES'" || exit 1 +: >"$CONTROL" +wait_for_marker +wait_for_stable_bytes +$OUTER pipe-pane -t outer:0.0 || exit 1 + +cat >"$ASSERT" <<'PERL' +use strict; +use warnings; + +my $path = shift; +open my $fh, '<:raw', $path or die "$path: $!\n"; +local $/; +my $bytes = <$fh>; +my ($on, $off) = ("\e[?2026h", "\e[?2026l"); +my ($active, %seen); + +for (my $i = 0; $i < length($bytes);) { + if (substr($bytes, $i, length($on)) eq $on) { + die "duplicate synchronized-output start at byte $i\n" if $active; + $active = 1; + $i += length($on); + next; + } + if (substr($bytes, $i, length($off)) eq $off) { + die "unmatched synchronized-output end at byte $i\n" unless $active; + $active = 0; + $i += length($off); + next; + } + if (substr($bytes, $i) =~ /\AFRAME_(\d{6})_ROW_(\d{2})_/) { + my $marker = "$1:$2"; + die "marker $marker outside synchronized output\n" unless $active; + $seen{$marker}++; + } + $i++; +} +die "unterminated synchronized-output transaction\n" if $active; +for my $frame (0 .. 1) { + for my $row (0 .. 23) { + my $marker = sprintf "%06d:%02d", $frame, $row; + die "missing marker $marker\n" unless $seen{$marker}; + } +} +PERL + +perl "$ASSERT" "$CLIENT_BYTES" || exit 1 From b2de803871e579565a6f79296f386115142593ed Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 20:04:00 +0000 Subject: [PATCH 11/20] Flush output before ending sync. GitHub issue 5495 from xiangzhedev at gmail dot com. --- input.c | 4 ++-- screen-write.c | 15 ++++++++++++++- tmux.h | 3 ++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/input.c b/input.c index ecf07dd61..5f9c5f4ab 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.269 2026/07/20 11:16:33 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.270 2026/08/17 20:04:00 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1974,7 +1974,7 @@ input_csi_dispatch_rm_private(struct input_ctx *ictx) screen_write_mode_clear(sctx, MODE_BRACKETPASTE); break; case 2026: - screen_write_stop_sync(ictx->wp); + screen_write_end_sync(sctx); break; case 2031: screen_write_mode_clear(sctx, MODE_THEME_UPDATES); diff --git a/screen-write.c b/screen-write.c index 71dc99200..990ef4e0f 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.286 2026/08/03 12:58:53 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.287 2026/08/17 20:04:00 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1059,6 +1059,19 @@ screen_write_stop_sync(struct window_pane *wp) log_debug("%s: %%%u stopped sync mode", __func__, wp->id); } +/* Flush pending output before clearing sync mode. */ +void +screen_write_end_sync(struct screen_write_ctx *ctx) +{ + struct window_pane *wp = ctx->wp; + + if (wp == NULL) + return; + if (wp->base.mode & MODE_SYNC) + screen_write_collect_flush(ctx, 0, __func__); + screen_write_stop_sync(wp); +} + /* Cursor up by ny. */ void screen_write_cursorup(struct screen_write_ctx *ctx, u_int ny) diff --git a/tmux.h b/tmux.h index 6cd104b50..11d9e3d4d 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1424 2026/08/17 14:47:41 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1425 2026/08/17 20:04:00 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3554,6 +3554,7 @@ void screen_write_mode_set(struct screen_write_ctx *, int); void screen_write_mode_clear(struct screen_write_ctx *, int); void screen_write_start_sync(struct window_pane *); void screen_write_stop_sync(struct window_pane *); +void screen_write_end_sync(struct screen_write_ctx *); void screen_write_clear_dirty(struct window_pane *); void screen_write_cursorup(struct screen_write_ctx *, u_int); void screen_write_cursordown(struct screen_write_ctx *, u_int); From db194ced5b41b35107e58933c77d02a22d2ccb9a Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 20:14:31 +0000 Subject: [PATCH 12/20] Log unknown message types. --- client.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client.c b/client.c index a59216823..bc0d36da5 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: client.c,v 1.167 2026/08/17 07:56:56 nicm Exp $ */ +/* $OpenBSD: client.c,v 1.168 2026/08/17 20:14:31 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -705,6 +705,9 @@ client_dispatch_wait(struct imsg *imsg) fprintf(stderr, "server version is too old for client\n"); proc_exit(client_proc); break; + default: + log_debug("unknown message type %u", imsg->hdr.type); + break; } } @@ -791,5 +794,8 @@ client_dispatch_attached(struct imsg *imsg) system(data); proc_send(client_peer, MSG_UNLOCK, -1, NULL, 0); break; + default: + log_debug("unknown message type %u", imsg->hdr.type); + break; } } From 19a085a6727650ae660196a38e1a412f1b386615 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 18 Aug 2026 08:23:16 +0100 Subject: [PATCH 13/20] Fix socket names for macOS. --- regress/sync-output-atomic.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/regress/sync-output-atomic.sh b/regress/sync-output-atomic.sh index d0979ebe1..842813d16 100644 --- a/regress/sync-output-atomic.sh +++ b/regress/sync-output-atomic.sh @@ -14,8 +14,8 @@ DIR=$(mktemp -d) || exit 1 TMUX_TMPDIR=$DIR export TMUX_TMPDIR -INNER="$TEST_TMUX -Lsync-output-inner-$$ -f/dev/null" -OUTER="$TEST_TMUX -Lsync-output-outer-$$ -f/dev/null" +INNER="$TEST_TMUX -Li$$ -f/dev/null" +OUTER="$TEST_TMUX -Lo$$ -f/dev/null" CLIENT_BYTES=$DIR/client-bytes CONTROL=$DIR/control EMITTER=$DIR/emitter.pl @@ -112,7 +112,7 @@ $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 -Lsync-output-inner-$$ -f/dev/null attach-session -t inner" || + "$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 From da641e4e8d2b0547487e8c6f28aca0e5981d5a92 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 18 Aug 2026 08:45:11 +0100 Subject: [PATCH 14/20] Add test for GitHub issue 5498 from Alex Rattray. --- regress/respawn-pane-control-lag.sh | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 regress/respawn-pane-control-lag.sh diff --git a/regress/respawn-pane-control-lag.sh b/regress/respawn-pane-control-lag.sh new file mode 100644 index 000000000..b71ce1946 --- /dev/null +++ b/regress/respawn-pane-control-lag.sh @@ -0,0 +1,87 @@ +#!/bin/sh + +# respawn-pane frees the pane's buffer and creates an empty one. The pane +# offsets and each control client's offsets used to keep pointing into the old +# buffer, so if an attached control client had not caught up on the pane's +# output, the next read from the new process ran off the end of the new buffer +# and the server crashed in input_parse. +# +# Two ways for a client to be behind are tested: a control client whose output +# goes down a fifo that is never read (a second, healthy control client keeps +# the pane being read), and a healthy control client viewing a session that +# the pane's window has been moved out of. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest" +$TMUX kill-server 2>/dev/null + +DIR=$(mktemp -d) +FIFO=$DIR/fifo +SERVER= + +mkfifo "$FIFO" || exit 1 + +cleanup() { + [ -n "$SERVER" ] && kill -9 "$SERVER" 2>/dev/null + $TMUX kill-server 2>/dev/null + exec 8<&- 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +alive() { + if ! kill -0 "$SERVER" 2>/dev/null; then + SERVER= + echo "server died after $1" + exit 1 + fi + $TMUX has -t rt || exit 1 +} + +wait_clients() { + n=0 + while [ $n -lt 50 ]; do + [ "$($TMUX lsc 2>/dev/null | wc -l)" -ge $1 ] && return + sleep 0.1 + n=$((n + 1)) + done + echo "control clients did not attach"; exit 1 +} + +# A detached session with a pane that writes a line every 10 milliseconds. +$TMUX -f/dev/null new -d -x 80 -y 24 -s rt \ + 'while :; do echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; sleep 0.01; done' || + exit 1 +SERVER=$($TMUX display -pt rt '#{pid}') + +# One control client with output down the unread fifo and one reading +# normally; stdin of both held open by sleep so they stay attached. +( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 & +exec 8<"$FIFO" +( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >/dev/null 2>&1 & +wait_clients 2 + +# Let the first client fall behind, then respawn the pane. The new process +# writes at once and the server must survive reading it. +sleep 3 +$TMUX respawn-pane -k -t rt:0 'echo respawned; sleep 60' || exit 1 +sleep 1 +alive "respawn-pane with a lagging control client" + +# Now a pane whose window is moved out of the session both clients view: their +# offsets for it stop advancing, so they fall behind until it is respawned. +$TMUX neww -d -t rt \ + 'while :; do echo yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy; sleep 0.01; done' || + exit 1 +sleep 1 +$TMUX new -d -s other || exit 1 +$TMUX movew -d -s rt:1 -t other: || exit 1 +sleep 2 +$TMUX respawn-pane -k -t other:1 'echo respawned; sleep 60' || exit 1 +sleep 1 +alive "respawn-pane on a window moved out of the clients' session" + +exit 0 From 6238793ad71f0bc0a6062c6575d866c180b18d08 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 18 Aug 2026 07:24:49 +0000 Subject: [PATCH 15/20] Restore umask if bind fails, from nachalsa at naver dot com. --- server.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server.c b/server.c index ffd5b4ddf..bc2fcdd92 100644 --- a/server.c +++ b/server.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server.c,v 1.215 2026/08/03 10:07:57 nicm Exp $ */ +/* $OpenBSD: server.c,v 1.216 2026/08/18 07:24:49 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -131,6 +131,7 @@ server_create_socket(uint64_t flags, char **cause) mask = umask(S_IXUSR|S_IRWXG|S_IRWXO); if (bind(fd, (struct sockaddr *)&sa, sizeof sa) == -1) { saved_errno = errno; + umask(mask); close(fd); errno = saved_errno; goto fail; From 72c5037f06625f8dff165ea42c660ee696e9015e Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 18 Aug 2026 07:32:09 +0000 Subject: [PATCH 16/20] Correctly clear clients referring to a session with destroy-unattached, GitHub issue 5497 from Jeong, Heon. --- server-fn.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server-fn.c b/server-fn.c index 149d048ad..503516afe 100644 --- a/server-fn.c +++ b/server-fn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-fn.c,v 1.149 2026/07/27 14:25:46 nicm Exp $ */ +/* $OpenBSD: server-fn.c,v 1.150 2026/08/18 07:32:09 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -548,6 +548,7 @@ server_check_unattached(void) continue; break; } + server_destroy_session(s); session_destroy(s, 1, __func__); } } From 4714f478fed87ae2c510ecd035aa89c5a90dedf8 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 18 Aug 2026 07:43:44 +0000 Subject: [PATCH 17/20] When a pane is respawned, reset the control mode offsets. GitHub issue 5498 from Alex Rattray. --- control.c | 21 ++++++++++++++++++++- spawn.c | 18 ++++++++++++++++-- tmux.h | 3 ++- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/control.c b/control.c index 7f76ff9e3..c3bb82b01 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.65 2026/08/04 11:18:22 nicm Exp $ */ +/* $OpenBSD: control.c,v 1.66 2026/08/18 07:43:44 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -388,6 +388,25 @@ control_pause_pane(struct client *c, struct window_pane *wp) } } +/* + * Reset a pane after its buffer has been replaced: drop any output still + * queued from the old buffer and start again from the pane's own offset. + */ +void +control_reset_pane(struct client *c, struct window_pane *wp) +{ + struct control_pane *cp; + + if (c->control_state == NULL) + return; + cp = control_get_pane(c, wp); + if (cp == NULL) + return; + control_discard_pane(c, cp); + memcpy(&cp->offset, &wp->offset, sizeof cp->offset); + memcpy(&cp->queued, &wp->offset, sizeof cp->queued); +} + /* Write an already-formatted line, queueing it behind %output if needed. */ static void control_write_line(struct client *c, char *line) diff --git a/spawn.c b/spawn.c index 6379e01af..9e162217f 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spawn.c,v 1.50 2026/07/23 09:38:27 nicm Exp $ */ +/* $OpenBSD: spawn.c,v 1.51 2026/08/18 07:43:44 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -245,7 +245,7 @@ struct window_pane * spawn_pane(struct spawn_context *sc, char **cause) { struct cmdq_item *item = sc->item; - struct client *c; + struct client *c, *loop; struct session *s = sc->s; struct session *ts; struct window *w = sc->wl->window; @@ -333,6 +333,20 @@ spawn_pane(struct spawn_context *sc, char **cause) input_free(sc->wp0->ictx); sc->wp0->ictx = NULL; } + + /* + * The old buffer is gone and the new one starts empty, so + * offsets into the old buffer no longer mean anything. Reset + * them, and drop output control clients still had queued. + */ + sc->wp0->offset.used = 0; + sc->wp0->base_offset = 0; + sc->wp0->pipe_offset.used = 0; + TAILQ_FOREACH(loop, &clients, entry) { + if (loop->flags & CLIENT_CONTROL) + control_reset_pane(loop, sc->wp0); + } + new_wp = sc->wp0; new_wp->flags &= ~(PANE_STATUSREADY|PANE_STATUSDRAWN); } else { diff --git a/tmux.h b/tmux.h index 11d9e3d4d..ea03cb463 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1425 2026/08/17 20:04:00 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1426 2026/08/18 07:43:44 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3986,6 +3986,7 @@ void control_set_pane_on(struct client *, struct window_pane *); void control_set_pane_off(struct client *, struct window_pane *); void control_continue_pane(struct client *, struct window_pane *); void control_pause_pane(struct client *, struct window_pane *); +void control_reset_pane(struct client *, struct window_pane *); void control_set_window_size(struct client *, u_int, u_int, u_int); int control_get_window_size(struct client *, u_int, u_int *, u_int *); void control_clear_window_size(struct client *, u_int); From b170c192742761a83ec3c8e1490692414d298860 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 18 Aug 2026 08:05:05 +0000 Subject: [PATCH 18/20] Change correct depth counter for source-file when used from hooks, GitHub issue 5437. --- cmd-source-file.c | 60 ++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/cmd-source-file.c b/cmd-source-file.c index e5dfba543..9c48c6f17 100644 --- a/cmd-source-file.c +++ b/cmd-source-file.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-source-file.c,v 1.62 2025/11/18 08:42:09 nicm Exp $ */ +/* $OpenBSD: cmd-source-file.c,v 1.63 2026/08/18 08:05:05 nicm Exp $ */ /* * Copyright (c) 2008 Tiago Cunha @@ -51,6 +51,7 @@ const struct cmd_entry cmd_source_file_entry = { struct cmd_source_file_data { struct cmdq_item *item; + struct client *client; int flags; struct cmdq_item *after; @@ -61,10 +62,24 @@ struct cmd_source_file_data { u_int nfiles; }; -static enum cmd_retval -cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data) +static void +cmd_source_file_free_data(struct cmd_source_file_data *cdata) { - struct client *c = cmdq_get_client(item); + u_int i; + + for (i = 0; i < cdata->nfiles; i++) + free(cdata->files[i]); + free(cdata->files); + if (cdata->client != NULL) + server_client_unref(cdata->client); + free(cdata); +} + +static enum cmd_retval +cmd_source_file_complete_cb(struct cmdq_item *item, void *data) +{ + struct cmd_source_file_data *cdata = data; + struct client *c = cdata->client; if (c == NULL) { cmd_source_file_depth--; @@ -75,36 +90,36 @@ cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data) } cfg_print_causes(item); + cmd_source_file_free_data(cdata); return (CMD_RETURN_NORMAL); } static void -cmd_source_file_complete(struct client *c, struct cmd_source_file_data *cdata) +cmd_source_file_complete(struct cmd_source_file_data *cdata) { + struct client *c = cdata->client; struct cmdq_item *new_item; - u_int i; - if (cfg_finished) { - if (cdata->retval == CMD_RETURN_ERROR && - c != NULL && - c->session == NULL) - c->retval = 1; - new_item = cmdq_get_callback(cmd_source_file_complete_cb, NULL); - cmdq_insert_after(cdata->after, new_item); + if (!cfg_finished) { + cmd_source_file_free_data(cdata); + return; } - for (i = 0; i < cdata->nfiles; i++) - free(cdata->files[i]); - free(cdata->files); - free(cdata); + if (cdata->retval == CMD_RETURN_ERROR && + c != NULL && + c->session == NULL) + c->retval = 1; + new_item = cmdq_get_callback(cmd_source_file_complete_cb, cdata); + cmdq_insert_after(cdata->after, new_item); } static void -cmd_source_file_done(struct client *c, const char *path, int error, - int closed, struct evbuffer *buffer, void *data) +cmd_source_file_done(__unused struct client *oc, const char *path, + int error, int closed, struct evbuffer *buffer, void *data) { struct cmd_source_file_data *cdata = data; struct cmdq_item *item = cdata->item; + struct client *c = cdata->client; void *bdata = EVBUFFER_DATA(buffer); size_t bsize = EVBUFFER_LENGTH(buffer); u_int n; @@ -128,7 +143,7 @@ cmd_source_file_done(struct client *c, const char *path, int error, if (n < cdata->nfiles) file_read(c, cdata->files[n], cmd_source_file_done, cdata); else { - cmd_source_file_complete(c, cdata); + cmd_source_file_complete(cdata); cmdq_continue(item); } } @@ -188,6 +203,9 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item) cdata = xcalloc(1, sizeof *cdata); cdata->item = item; + cdata->client = c; + if (c != NULL) + c->references++; if (args_has(args, 'q')) cdata->flags |= CMD_PARSE_QUIET; @@ -250,7 +268,7 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item) file_read(c, cdata->files[0], cmd_source_file_done, cdata); retval = CMD_RETURN_WAIT; } else - cmd_source_file_complete(c, cdata); + cmd_source_file_complete(cdata); free(cwd); return (retval); From c5017e6da84e1e5b5db9f41f47def3fe7ba56308 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 18 Aug 2026 11:49:25 +0100 Subject: [PATCH 19/20] Bump time limit. --- .github/workflows/regress.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 3cc036425..a84478357 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -17,7 +17,7 @@ jobs: name: ${{ matrix.name }} runs-on: ${{ matrix.runner }} if: github.repository == 'tmux/tmux' - timeout-minutes: 45 + timeout-minutes: 300 strategy: fail-fast: false From e94fcd96f9384808a404995b935d722fe3ed4358 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 18 Aug 2026 09:01:20 +0000 Subject: [PATCH 20/20] Use DECRQM to detect mode 2026, from Michael Grant. --- tmux.h | 5 +++-- tty-keys.c | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++- tty.c | 4 +++- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/tmux.h b/tmux.h index ea03cb463..68d2044b0 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1426 2026/08/18 07:43:44 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1427 2026/08/18 09:01:20 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1774,8 +1774,9 @@ struct tty { #define TTY_WAITFG 0x2000 #define TTY_WAITBG 0x4000 #define TTY_BRACKETPASTE 0x8000 +#define TTY_HAVESYNC 0x10000 #define TTY_ALL_REQUEST_FLAGS \ - (TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA) + (TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA|TTY_HAVESYNC) int flags; struct tty_term *term; diff --git a/tty-keys.c b/tty-keys.c index 6c2289495..8462e0d9b 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-keys.c,v 1.213 2026/08/17 14:47:41 nicm Exp $ */ +/* $OpenBSD: tty-keys.c,v 1.214 2026/08/18 09:01:20 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -59,6 +59,7 @@ static int tty_keys_device_attributes2(struct tty *, const char *, size_t, size_t *); static int tty_keys_extended_device_attributes(struct tty *, const char *, size_t, size_t *); +static int tty_keys_sync(struct tty *, const char *, size_t, size_t *); static int tty_keys_palette(struct tty *, const char *, size_t, size_t *); /* A key tree entry. */ @@ -771,6 +772,17 @@ tty_keys_next(struct tty *tty) goto partial_key; } + /* Is this a synchronized update mode response? */ + switch (tty_keys_sync(tty, buf, len, &size)) { + case 0: /* yes */ + key = KEYC_UNKNOWN; + goto complete_key; + case -1: /* no, or not valid */ + break; + case 1: /* partial */ + goto partial_key; + } + /* Is this a primary device attributes response? */ switch (tty_keys_device_attributes(tty, buf, len, &size)) { case 0: /* yes */ @@ -1521,6 +1533,54 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, return (0); } +/* + * Handle a synchronized update mode response. Returns 0 for success, -1 for + * failure, 1 for partial. + */ +static int +tty_keys_sync(struct tty *tty, const char *buf, size_t len, size_t *size) +{ + struct client *c = tty->client; + static const char prefix[] = "\033[?2026;"; + size_t i; + int status; + + *size = 0; + if (tty->flags & TTY_HAVESYNC) + return (-1); + + /* The response is always \033[?2026;Ps$y. */ + for (i = 0; i < (sizeof prefix) - 1; i++) { + if (i == len) + return (1); + if (buf[i] != prefix[i]) + return (-1); + } + if (i == len) + return (1); + if (buf[i] < '0' || buf[i] > '4') + return (-1); + status = buf[i++] - '0'; + if (i == len) + return (1); + if (buf[i++] != '$') + return (-1); + if (i == len) + return (1); + if (buf[i++] != 'y') + return (-1); + *size = i; + + if (status == 1 || status == 2 || status == 3) { + tty_parse_client_features(c, "sync", ","); + tty_update_features(tty); + } + log_debug("%s: received DECRPM %.*s", c->name, (int)*size, buf); + tty->flags |= TTY_HAVESYNC; + + return (0); +} + /* * Handle secondary device attributes input. Returns 0 for success, -1 for * failure, 1 for partial. diff --git a/tty.c b/tty.c index 8922efa07..bf2671b33 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.478 2026/08/17 14:47:41 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.479 2026/08/18 09:01:20 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -398,6 +398,8 @@ tty_send_requests(struct tty *tty) tty_puts(tty, "\033[>c"); if (~tty->flags & TTY_HAVEXDA) tty_puts(tty, "\033[>q"); + if (~tty->flags & TTY_HAVESYNC) + tty_puts(tty, "\033[?2026$p"); tty_puts(tty, "\033]10;?\033\\\033]11;?\033\\"); tty->flags |= (TTY_WAITBG|TTY_WAITFG); } else