From f466d6289ebeed6fb0d234527761279dd54fd655 Mon Sep 17 00:00:00 2001 From: r1w1s1 Date: Mon, 10 Aug 2026 15:38:55 -0300 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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