From 3d44f75d0959313b1fc6a4e556d89927308a8cbe Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 07:05:23 +0000 Subject: [PATCH 01/35] Do not leak buffer name on failure, from Jeong, Heon. --- cmd-set-buffer.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd-set-buffer.c b/cmd-set-buffer.c index 6e640fec6..3e540f706 100644 --- a/cmd-set-buffer.c +++ b/cmd-set-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-set-buffer.c,v 1.37 2026/02/15 17:43:26 nicm Exp $ */ +/* $OpenBSD: cmd-set-buffer.c,v 1.38 2026/08/24 07:05:23 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -100,6 +100,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) cmdq_error(item, "%s", cause); goto fail; } + free(bufname); return (CMD_RETURN_NORMAL); } @@ -107,8 +108,10 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) cmdq_error(item, "no data specified"); goto fail; } - if ((newsize = strlen(args_string(args, 0))) == 0) + if ((newsize = strlen(args_string(args, 0))) == 0) { + free(bufname); return (CMD_RETURN_NORMAL); + } if (args_has(args, 'a') && pb != NULL) { olddata = paste_buffer_data(pb, &bufsize); @@ -127,6 +130,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'w') && tc != NULL) tty_set_selection(&tc->tty, "", bufdata, bufsize); + free(bufname); return (CMD_RETURN_NORMAL); fail: From 995b39579506049aadbe321fbc4fb9818c50e2a3 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 07:06:58 +0000 Subject: [PATCH 02/35] Do not leak format when drawing preview, from Jeong, Heon. --- window-tree.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/window-tree.c b/window-tree.c index 9c00e9e8a..3fdf968fb 100644 --- a/window-tree.c +++ b/window-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-tree.c,v 1.94 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window-tree.c,v 1.95 2026/08/24 07:06:58 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -642,6 +642,7 @@ window_tree_draw_session(struct window_tree_modedata *data, struct session *s, } free(label); } + format_free(ft); if (loop != end - 1) { screen_write_cursormove(ctx, cx + offset + width, cy, @@ -790,6 +791,7 @@ window_tree_draw_window(struct window_tree_modedata *data, struct session *s, } free(label); } + format_free(ft); if (loop != end - 1) { screen_write_cursormove(ctx, cx + offset + width, cy, From 4cabc2ae85eea7d80a4d7f7fc75c8191f4c72c86 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 07:08:19 +0000 Subject: [PATCH 03/35] Do not leak path when destroying client, from Jeong, Heon. --- server-client.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server-client.c b/server-client.c index f1cdd8736..e5b15fc3a 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.505 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.506 2026/08/24 07:08:19 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -529,6 +529,7 @@ server_client_lost(struct client *c) input_cancel_requests(c); free(c->title); + free(c->path); free((void *)c->cwd); evtimer_del(&c->repeat_timer); From 7792a74c76c03ad0074880d9e6bbdc27cf3afb7f Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 07:10:38 +0000 Subject: [PATCH 04/35] Free pane fallback range when freeing pane, from Jeong, Heon. --- window.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/window.c b/window.c index 852c8e666..a61e2652d 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.370 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.371 2026/08/24 07:10:38 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1534,10 +1534,9 @@ window_pane_free(struct window_pane *wp) log_debug("pane %%%u freed (%d references)", wp->id, wp->references); free(wp->searchstr); - screen_free(&wp->status_screen); screen_free(&wp->base); - + free(wp->r.ranges); options_free(wp->options); free((void *)wp->cwd); free(wp->shell); From c9f5ecb4b55dcdb80af914eb2320f71f0f1234ec Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 07:14:54 +0000 Subject: [PATCH 05/35] Do not leak cached status line, from Jeong, Heon. --- window.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/window.c b/window.c index a61e2652d..d1e74980b 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.371 2026/08/24 07:10:38 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.372 2026/08/24 07:14:54 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1543,6 +1543,7 @@ window_pane_free(struct window_pane *wp) cmd_free_argv(wp->argc, wp->argv); colour_palette_free(&wp->palette); style_ranges_free(&wp->border_status_line.ranges); + free(wp->border_status_line.expanded); free(wp); } From 41e701ad8408a3411bb59198a067c4ef471abba8 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 24 Aug 2026 15:27:25 +0100 Subject: [PATCH 06/35] Return early if image is zero height when generating fallback, otherwise we can allocated too little space. Reported by Vivek Parikh. --- image.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/image.c b/image.c index f0543dc4f..5613cf920 100644 --- a/image.c +++ b/image.c @@ -84,6 +84,11 @@ image_fallback(char **ret, u_int sx, u_int sy) char *buf, *label; u_int py, size, lsize; + if (sy == 0) { + *ret = xstrdup(""); + return; + } + /* Allocate first line. */ lsize = xasprintf(&label, "SIXEL IMAGE (%ux%u)\r\n", sx, sy) + 1; if (sx < lsize - 3) From 402d366bba94cf13eaddd793a889f56f91cfcb61 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 07:26:43 +0000 Subject: [PATCH 07/35] Add {} to shell escape characters (special in bash/ksh/csh), GitHub issue 5514. --- format.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/format.c b/format.c index 0eb5b394a..928eb078e 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.412 2026/08/05 07:31:08 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.413 2026/08/24 07:26:43 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -4369,7 +4369,7 @@ format_quote_shell(const char *s) at = out = xmalloc(strlen(s) * 2 + 1); for (cp = s; *cp != '\0'; cp++) { - if (strchr("|&;<>()$`\\\"'*?[# =%\n\t", *cp) != NULL) + if (strchr("|&;<>(){}$`\\\"'*?[# =%\n\t", *cp) != NULL) *at++ = '\\'; *at++ = *cp; } From 7d358208b9e2dce3b041b17d611bb285f1363673 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 14:07:56 +0000 Subject: [PATCH 08/35] Free any remaining items when destroying screen, from Jeong, Heon. --- screen-write.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/screen-write.c b/screen-write.c index 22b555bd5..c94ece65a 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.288 2026/08/21 09:53:04 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.289 2026/08/24 14:07:56 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -338,10 +338,18 @@ screen_write_make_list(struct screen *s) void screen_write_free_list(struct screen *s) { - u_int y; + struct screen_write_cline *cl; + struct screen_write_citem *ci, *ci1; + u_int y; - for (y = 0; y < screen_size_y(s); y++) - free(s->write_list[y].data); + for (y = 0; y < screen_size_y(s); y++) { + cl = &s->write_list[y]; + TAILQ_FOREACH_SAFE(ci, &cl->items, entry, ci1) { + TAILQ_REMOVE(&cl->items, ci, entry); + screen_write_free_citem(ci); + } + free(cl->data); + } free(s->write_list); } From 5b54c7d41af85cd7f15f0c272d8980db945e2033 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 15:05:26 +0000 Subject: [PATCH 09/35] Always use synchronized updates in copy mode (like when not in the active pane), since copy mode will almost always moved the cursor. From Artem Goldenberg in GitHub issue 5525. --- screen-write.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/screen-write.c b/screen-write.c index c94ece65a..cf7a662c8 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.289 2026/08/24 14:07:56 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.290 2026/08/24 15:05:26 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -305,12 +305,14 @@ screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, if (~ctx->flags & SCREEN_WRITE_SYNC) { /* - * For the active pane or for an overlay (no pane), we want to - * only use synchronized updates if requested (commands that - * move the cursor); for other panes, always use it, since the - * cursor will have to move. + * For the active pane showing its base screen or for an + * overlay (no pane), only use synchronized updates if + * requested (commands that move the cursor); for other panes + * or a pane in a mode, always use it, since the cursor will + * have to move. */ - if (ctx->wp != NULL && ctx->wp != ctx->wp->window->active) + if (ctx->wp != NULL && (ctx->wp != ctx->wp->window->active || + ctx->wp->screen != &ctx->wp->base)) ttyctx->flags |= TTY_CTX_SYNC; else { if (ctx->wp == NULL) From dc78d8f9ea78c30d31d622da37246d464d725bcc Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 20:34:26 +0000 Subject: [PATCH 10/35] Do not add messages to cfg_causes after config has finished, GitHub issue 5528. --- cmd-queue.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cmd-queue.c b/cmd-queue.c index dc1b8cc63..4ecec8f7d 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-queue.c,v 1.122 2026/08/03 13:38:42 nicm Exp $ */ +/* $OpenBSD: cmd-queue.c,v 1.123 2026/08/24 20:34:26 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -851,7 +851,18 @@ cmdq_error(struct cmdq_item *item, const char *fmt, ...) if (c == NULL) { cmd_get_source(cmd, &file, &line); - cfg_add_cause("%s:%u: %s", file, line, msg); + if (!cfg_finished) { + if (file != NULL) + cfg_add_cause("%s:%u: %s", file, line, msg); + else + cfg_add_cause("%s", msg); + } else { + if (file != NULL) { + server_add_message("message: %s:%u: %s", file, + line, msg); + } else + server_add_message("message: %s", msg); + } } else if (c->session == NULL || (c->flags & CLIENT_CONTROL)) { server_add_message("%s message: %s", c->name, msg); if (~c->flags & CLIENT_UTF8) { From 2d5328a8604b026d9b143c6c46d85283c0676ed5 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 21:17:19 +0000 Subject: [PATCH 11/35] If a pane has no scrollbar, don't bother to redraw it, and if redraw is deferred change scrollbar redraws into redraw-all-scrollbars not redraw entire window. GitHub issue 5529. --- server-client.c | 20 ++++++++++++++------ tmux.h | 4 ++-- window.c | 4 +++- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/server-client.c b/server-client.c index e5b15fc3a..819a57dad 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.506 2026/08/24 07:08:19 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.507 2026/08/24 21:17:19 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -2489,7 +2489,7 @@ server_client_check_redraw(struct client *c) /* Work out if a redraw is actually needed. */ needed = 0; - if (c->flags & CLIENT_ALLREDRAWFLAGS) + if (c->flags & (CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS)) needed = 1; else if (server_client_any_pane_redraw(c)) needed = 1; @@ -2515,8 +2515,14 @@ server_client_check_redraw(struct client *c) log_debug("redraw timer started"); evtimer_add(&ev, &tv); } - if (server_client_any_pane_redraw(c)) - c->flags |= CLIENT_REDRAWWINDOW; + TAILQ_FOREACH(wp, &w->panes, entry) { + if (wp->flags & PANE_REDRAW) { + c->flags |= CLIENT_REDRAWWINDOW; + break; + } + if (wp->flags & PANE_REDRAWSCROLLBAR) + c->flags |= CLIENT_REDRAWSCROLLBARS; + } return; } @@ -2535,7 +2541,8 @@ server_client_check_redraw(struct client *c) log_debug("%s: redraw pane %%%u", __func__, wp->id); redraw_pane(c, wp); - } else if (wp->flags & PANE_REDRAWSCROLLBAR) { + } else if ((wp->flags & PANE_REDRAWSCROLLBAR) || + (c->flags & CLIENT_REDRAWSCROLLBARS)) { log_debug("%s: redraw scrollbar %%%u", __func__, wp->id); redraw_pane_scrollbar(c, wp); @@ -2565,7 +2572,8 @@ server_client_check_redraw(struct client *c) * All the redraw flags can now be cleared. Also record how many bytes * were written. */ - c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_STATUSFORCE); + c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS| + CLIENT_STATUSFORCE); c->redraw = EVBUFFER_LENGTH(tty->out); log_debug("%s: redraw added %zu bytes", c->name, c->redraw); } diff --git a/tmux.h b/tmux.h index c7fb2539e..1366b57ea 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1429 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1430 2026/08/24 21:17:19 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2249,7 +2249,7 @@ struct client { #define CLIENT_STARTSERVER 0x10000000 #define CLIENT_REDRAWMENU 0x20000000 #define CLIENT_NOFORK 0x40000000 -/* 0x80000000ULL unused */ +#define CLIENT_REDRAWSCROLLBARS 0x80000000ULL #define CLIENT_CONTROL_PAUSEAFTER 0x100000000ULL #define CLIENT_CONTROL_WAITEXIT 0x200000000ULL #define CLIENT_WINDOWSIZECHANGED 0x400000000ULL diff --git a/window.c b/window.c index d1e74980b..9feb1e71c 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.372 2026/08/24 07:14:54 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.373 2026/08/24 21:17:19 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1471,6 +1471,8 @@ window_pane_scrollbar_overlay_visible(struct window_pane *wp) void window_pane_scrollbar_redraw(struct window_pane *wp) { + if (!window_pane_scrollbar_visible(wp)) + return; if (window_pane_scrollbar_overlay_visible(wp)) { wp->flags |= PANE_REDRAW; return; From 50ea32162f8ebf2f6843c2f89e15abf9e85d828f Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 21:19:40 +0000 Subject: [PATCH 12/35] Correct a comparison ('\0' should be 0). --- window-buffer.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/window-buffer.c b/window-buffer.c index e19f6205d..f68e76f85 100644 --- a/window-buffer.c +++ b/window-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-buffer.c,v 1.51 2026/07/15 12:45:39 nicm Exp $ */ +/* $OpenBSD: window-buffer.c,v 1.52 2026/08/24 21:19:40 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -567,9 +567,7 @@ window_buffer_edit_close_cb(char *buf, size_t len, void *arg) } oldbuf = paste_buffer_data(pb, &oldlen); - if (oldlen != '\0' && - oldbuf[oldlen - 1] != '\n' && - buf[len - 1] == '\n') + if (oldlen != 0 && oldbuf[oldlen - 1] != '\n' && buf[len - 1] == '\n') len--; if (len != 0) paste_replace(pb, buf, len); From 1459c90a7fa6a70afd1e8438fa9985141e4002be Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 25 Aug 2026 06:04:33 +0000 Subject: [PATCH 13/35] Tidy up args_make_commands reference counting to fix a memory leak, from Jeong, Heon. --- arguments.c | 8 ++++---- cmd-command-prompt.c | 4 +++- cmd-if-shell.c | 5 ++++- cmd-run-shell.c | 4 +++- window-panes.c | 3 ++- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/arguments.c b/arguments.c index baafd6d52..f2e50003b 100644 --- a/arguments.c +++ b/arguments.c @@ -1,4 +1,4 @@ -/* $OpenBSD: arguments.c,v 1.66 2026/06/26 09:54:56 nicm Exp $ */ +/* $OpenBSD: arguments.c,v 1.67 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott @@ -762,8 +762,6 @@ args_make_commands_now(struct cmd *self, struct cmdq_item *item, u_int idx, cmdq_error(item, "%s", error); free(error); } - else - cmdlist->references++; args_make_commands_free(state); return (cmdlist); } @@ -827,8 +825,10 @@ args_make_commands(struct args_command_state *state, int argc, char **argv, int i; if (state->cmdlist != NULL) { - if (argc == 0) + if (argc == 0) { + state->cmdlist->references++; return (state->cmdlist); + } return (cmd_list_copy(state->cmdlist, argc, argv)); } diff --git a/cmd-command-prompt.c b/cmd-command-prompt.c index ac1117347..1d135b834 100644 --- a/cmd-command-prompt.c +++ b/cmd-command-prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-command-prompt.c,v 1.75 2026/06/25 11:39:11 nicm Exp $ */ +/* $OpenBSD: cmd-command-prompt.c,v 1.76 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -242,9 +242,11 @@ cmd_command_prompt_callback(struct client *c, void *data, const char *s, } else if (item == NULL) { new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); + cmd_list_free(cmdlist); } else { new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); cmdq_insert_after(item, new_item); + cmd_list_free(cmdlist); } cmd_free_argv(argc, argv); diff --git a/cmd-if-shell.c b/cmd-if-shell.c index 49b92a996..59d34f9d6 100644 --- a/cmd-if-shell.c +++ b/cmd-if-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-if-shell.c,v 1.86 2025/08/01 09:05:51 nicm Exp $ */ +/* $OpenBSD: cmd-if-shell.c,v 1.87 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -98,6 +98,7 @@ cmd_if_shell_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_ERROR); new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); cmdq_insert_after(item, new_item); + cmd_list_free(cmdlist); return (CMD_RETURN_NORMAL); } @@ -164,9 +165,11 @@ cmd_if_shell_callback(struct job *job) } else if (item == NULL) { new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); + cmd_list_free(cmdlist); } else { new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); cmdq_insert_after(item, new_item); + cmd_list_free(cmdlist); } out: diff --git a/cmd-run-shell.c b/cmd-run-shell.c index 59d5aeea6..7e0c62e56 100644 --- a/cmd-run-shell.c +++ b/cmd-run-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-run-shell.c,v 1.93 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: cmd-run-shell.c,v 1.94 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -233,9 +233,11 @@ cmd_run_shell_timer(__unused int fd, __unused short events, void* arg) } else if (item == NULL) { new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); + cmd_list_free(cmdlist); } else { new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); cmdq_insert_after(item, new_item); + cmd_list_free(cmdlist); } if (cdata->item != NULL) diff --git a/window-panes.c b/window-panes.c index f1104b4f3..e65e59606 100644 --- a/window-panes.c +++ b/window-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-panes.c,v 1.4 2026/07/29 14:06:32 nicm Exp $ */ +/* $OpenBSD: window-panes.c,v 1.5 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -1009,6 +1009,7 @@ window_panes_run_command(struct window_panes_modedata *data, struct client *c, } else { new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); + cmd_list_free(cmdlist); } free(expanded); } From 0851650c6be43fe23477fee380fc6672d0348e57 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 25 Aug 2026 06:14:16 +0000 Subject: [PATCH 14/35] Do not leak exit message on client free, from Jeong, Heon. --- server-client.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server-client.c b/server-client.c index 819a57dad..80efc614d 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.507 2026/08/24 21:17:19 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.508 2026/08/25 06:14:16 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -531,6 +531,8 @@ server_client_lost(struct client *c) free(c->title); free(c->path); free((void *)c->cwd); + free(c->exit_session); + free(c->exit_message); evtimer_del(&c->repeat_timer); evtimer_del(&c->click_timer); @@ -2412,8 +2414,6 @@ server_client_check_exit(struct client *c, int force) proc_send(c->peer, c->exit_msgtype, -1, name, strlen(name) + 1); break; } - free(c->exit_session); - free(c->exit_message); } /* Redraw timer callback. */ From 6f4c9b32af0354c4afc5e901600c6b48aca25352 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 25 Aug 2026 07:23:30 +0000 Subject: [PATCH 15/35] Do not pass NULL to qsort, and add checks for invalid session or window after swap. --- sort.c | 4 ++-- window-switch.c | 5 +++-- window-tree.c | 6 +++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/sort.c b/sort.c index 31f6e82f1..1b71db494 100644 --- a/sort.c +++ b/sort.c @@ -1,4 +1,4 @@ -/* $OpenBSD: sort.c,v 1.9 2026/06/29 07:45:09 nicm Exp $ */ +/* $OpenBSD: sort.c,v 1.10 2026/08/25 07:23:30 nicm Exp $ */ /* * Copyright (c) 2026 Dane Jensen @@ -32,7 +32,7 @@ sort_qsort(void *l, u_int len, u_int size, int (*cmp)(const void *, u_int i; void *tmp, **ll; - if (sort_crit->order == SORT_END) + if (len < 2 || sort_crit->order == SORT_END) return; if (sort_crit->order == SORT_ORDER) { diff --git a/window-switch.c b/window-switch.c index 6712d8a89..f9cb16ec5 100644 --- a/window-switch.c +++ b/window-switch.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-switch.c,v 1.2 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window-switch.c,v 1.3 2026/08/25 07:23:30 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -227,7 +227,8 @@ window_switch_build(struct window_switch_modedata *data) m = xreallocarray(m, n + 1, sizeof *m); m[n++] = item; } - qsort(m, n, sizeof *m, window_switch_compare); + if (n > 1) + qsort(m, n, sizeof *m, window_switch_compare); free(data->matches); data->matches = m; diff --git a/window-tree.c b/window-tree.c index 3fdf968fb..49f9bbecc 100644 --- a/window-tree.c +++ b/window-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-tree.c,v 1.95 2026/08/24 07:06:58 nicm Exp $ */ +/* $OpenBSD: window-tree.c,v 1.96 2026/08/25 07:23:30 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -1022,6 +1022,10 @@ window_tree_swap(void *cur_itemdata, void *other_itemdata, window_tree_pull_item(other, &other_session, &other_winlink, &other_pane); + if (cur_session == NULL || cur_winlink == NULL) + return (0); + if (other_session == NULL || other_winlink == NULL) + return (0); if (cur_session != other_session) return (0); From 4157b64754a15ea5eda87fcb545134bdf4f5885b Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 25 Aug 2026 08:37:08 +0000 Subject: [PATCH 16/35] =?UTF-8?q?Add=20a=20clear-on-attach=20option=20whic?= =?UTF-8?q?h=20when=20turned=20off=20scrolls=20away=20the=20existing=20ter?= =?UTF-8?q?minal=20state=20instead=20of=20entering=20the=20alternate=20scr?= =?UTF-8?q?een.=20This=20is=20useful=20in=20environments=20where=20tmux=20?= =?UTF-8?q?is=20started=20as=20a=20login=20shell=20but=20it=20is=20importa?= =?UTF-8?q?nt=20that=20any=20pre-tmux=20messages=20are=20still=20visible.?= =?UTF-8?q?=20GitHub=20issue=205508=20from=20Josef=20=C5=98=C3=ADdk=C3=BD.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- options-table.c | 12 +++++++++++- tmux.1 | 13 +++++++++++-- tmux.h | 3 ++- tty-term.c | 3 ++- tty.c | 28 ++++++++++++++++++++++------ 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/options-table.c b/options-table.c index 54352319b..ecca20153 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.242 2026/07/27 08:03:01 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.243 2026/08/25 08:37:08 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -306,6 +306,16 @@ const struct options_table_entry options_table[] = { "When this is reached, the oldest buffer is deleted." }, + { .name = "clear-on-attach", + .type = OPTIONS_TABLE_FLAG, + .scope = OPTIONS_TABLE_SERVER, + .default_num = 1, + .text = "Whether to use the alternate screen and clear it when " + "a client is attached. When disabled, tmux does not " + "enter the alternate screen on attach so terminal " + "content before tmux remains in scrollback." + }, + { .name = "command-alias", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_SERVER, diff --git a/tmux.1 b/tmux.1 index 244daf9fc..cd861be2b 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1158 2026/08/20 09:19:24 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1159 2026/08/25 08:37:08 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 20 2026 $ +.Dd $Mdocdate: August 25 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -4752,6 +4752,15 @@ for backspace. Set the number of buffers; as new buffers are added to the top of the stack, old ones are removed from the bottom if necessary to maintain this maximum length. +.It Xo Ic clear\-on\-attach +.Op Ic on | off +.Xc +When enabled (the default), the client terminal switches to the alternate +screen and clears it when a client is attached. +When disabled, +.Nm +does not enter the alternate screen; instead the existing terminal content is +scrolled into the scrollback buffer so it remains accessible. .It Xo Ic command\-alias[] .Ar name=value .Xc diff --git a/tmux.h b/tmux.h index 1366b57ea..2fa3f3987 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1430 2026/08/24 21:17:19 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1431 2026/08/25 08:37:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -474,6 +474,7 @@ enum tty_code_code { TTYC_ICH1, TTYC_IL, TTYC_IL1, + TTYC_IND, TTYC_INDN, TTYC_INVIS, TTYC_KCBT, diff --git a/tty-term.c b/tty-term.c index 99341c0ce..3f3bc9c8d 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-term.c,v 1.108 2026/08/17 14:47:41 nicm Exp $ */ +/* $OpenBSD: tty-term.c,v 1.109 2026/08/25 08:37:08 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -108,6 +108,7 @@ static const struct tty_term_code_entry tty_term_codes[] = { [TTYC_ICH] = { TTYCODE_STRING, "ich" }, [TTYC_IL1] = { TTYCODE_STRING, "il1" }, [TTYC_IL] = { TTYCODE_STRING, "il" }, + [TTYC_IND] = { TTYCODE_STRING, "ind" }, [TTYC_INDN] = { TTYCODE_STRING, "indn" }, [TTYC_INVIS] = { TTYCODE_STRING, "invis" }, [TTYC_KCBT] = { TTYCODE_STRING, "kcbt" }, diff --git a/tty.c b/tty.c index bf2671b33..588659fc8 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.479 2026/08/18 09:01:20 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.480 2026/08/25 08:37:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -333,6 +333,7 @@ tty_start_tty(struct tty *tty) { struct client *c = tty->client; struct termios tio; + u_int i; setblocking(c->fd, 0); event_add(&tty->event_in, NULL); @@ -348,10 +349,21 @@ tty_start_tty(struct tty *tty) if (tcsetattr(c->fd, TCSANOW, &tio) == 0) tcflush(c->fd, TCOFLUSH); - tty_putcode(tty, TTYC_SMCUP); - + if (options_get_number(global_options, "clear-on-attach")) { + tty_putcode(tty, TTYC_SMCUP); + tty_putcode(tty, TTYC_CLEAR); + } else { + tty_putcode_ii(tty, TTYC_CSR, 0, tty->sy - 1); + tty_putcode_ii(tty, TTYC_CUP, 0, tty->sy - 1); + if (tty_term_has(tty->term, TTYC_INDN)) + tty_putcode_i(tty, TTYC_INDN, tty->sy + 1); + else if (tty_term_has(tty->term, TTYC_IND)) { + for (i = 0; i < tty->sy + 1; i++) + tty_putcode(tty, TTYC_IND); + } else + tty_putcode(tty, TTYC_CLEAR); + } tty_putcode(tty, TTYC_SMKX); - tty_putcode(tty, TTYC_CLEAR); if (tty_acs_needed(tty)) { log_debug("%s: using capabilities for ACS", c->name); @@ -467,7 +479,8 @@ tty_stop_tty(struct tty *tty) tty_raw(tty, tty_term_string(tty->term, TTYC_RMACS)); tty_raw(tty, tty_term_string(tty->term, TTYC_SGR0)); tty_raw(tty, tty_term_string(tty->term, TTYC_RMKX)); - tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR)); + if (options_get_number(global_options, "clear-on-attach")) + tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR)); if (tty->cstyle != SCREEN_CURSOR_DEFAULT) { if (tty_term_has(tty->term, TTYC_SE)) tty_raw(tty, tty_term_string(tty->term, TTYC_SE)); @@ -492,7 +505,10 @@ tty_stop_tty(struct tty *tty) if (tty_use_margin(tty)) tty_raw(tty, tty_term_string(tty->term, TTYC_DSMG)); - tty_raw(tty, tty_term_string(tty->term, TTYC_RMCUP)); + if (options_get_number(global_options, "clear-on-attach")) + tty_raw(tty, tty_term_string(tty->term, TTYC_RMCUP)); + else + tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR)); if (tty->term->flags & TERM_VT100LIKE) tty_raw(tty, "\033[?2031l"); From 8e48c889dd229f4bb3046c4efbe01459aacf7b04 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 25 Aug 2026 19:36:06 +0100 Subject: [PATCH 17/35] Add a resize test. --- regress/floating-pane-geometry.sh | 69 +++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/regress/floating-pane-geometry.sh b/regress/floating-pane-geometry.sh index 7e80e8942..b04344ff2 100644 --- a/regress/floating-pane-geometry.sh +++ b/regress/floating-pane-geometry.sh @@ -199,5 +199,74 @@ must_fail $TMUX resize-pane -t "$id" -x 0 must_fail $TMUX resize-pane -t "$id" -y 0 $TMUX kill-pane -t "$id" || exit 1 +# --- Tiled pane resize with floating cells in the layout --- + +$TMUX set-option -w -u pane-border-lines || exit 1 +base=$($TMUX display-message -p '#{pane_id}') + +# A floating cell after the last tiled cell must not become the recipient of a +# relative or absolute resize. This is the sequence from GitHub issue 5135. +floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \ + -x 50% -y 50% -X 50% -Y 50% 'sleep 100') || + fail "new-pane for tiled resize test failed" +lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') || + fail "split-window for tiled resize test failed" + +$TMUX resize-pane -t "$lower" -U 5 || fail "relative tiled resize failed" +must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 7 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 8 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 16 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 13 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 10 + +$TMUX resize-pane -t "$lower" -y 11 || fail "absolute tiled reset failed" +$TMUX resize-pane -t "$lower" -y 16 || fail "absolute tiled resize failed" +must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 7 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 8 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 16 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 10 + +$TMUX kill-pane -t "$floating" || exit 1 +$TMUX kill-pane -t "$lower" || exit 1 + +# A floating cell between tiled siblings must be skipped when finding the cell +# which donates space to a resize. +lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') || + fail "split-window for middle floating cell test failed" +floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \ + -x 20 -y 8 -X 30 -Y 8 'sleep 100') || + fail "new-pane for middle floating cell test failed" + +$TMUX resize-pane -t "$base" -D 3 || fail "resize past floating cell failed" +must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 15 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 16 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 8 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 6 + +$TMUX kill-pane -t "$floating" || exit 1 +$TMUX kill-pane -t "$lower" || exit 1 + +# A small floating child must not limit the available tiled space in a nested +# layout with a different split direction. +lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') || + fail "split-window for nested floating cell test failed" +right=$($TMUX split-window -dhPF '#{pane_id}' -t "$lower" 'sleep 100') || + fail "horizontal split for nested floating cell test failed" +floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$lower" \ + -x 20 -y 3 -X 30 -Y 10 'sleep 100') || + fail "new-pane for nested floating cell test failed" + +$TMUX resize-pane -t "$base" -D 3 || fail "nested tiled resize failed" +must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 15 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 16 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 8 +must_equal "$($TMUX display-message -p -t "$right" '#{pane_top}')" 16 +must_equal "$($TMUX display-message -p -t "$right" '#{pane_height}')" 8 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 1 + +$TMUX kill-pane -t "$floating" || exit 1 +$TMUX kill-pane -t "$right" || exit 1 +$TMUX kill-pane -t "$lower" || exit 1 + $TMUX kill-server 2>/dev/null exit 0 From b26b240613da0ace7abe80f2c89f74ce11db8eb9 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 25 Aug 2026 18:38:05 +0000 Subject: [PATCH 18/35] =?UTF-8?q?Fix=20tiled=20pane=20resizing=20with=20fl?= =?UTF-8?q?oating=20layout=20cells,=20reported=20by=20Kevin=20Hovs=C3=A4te?= =?UTF-8?q?r.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- layout.c | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/layout.c b/layout.c index 02a4b681a..ac87f30e5 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout.c,v 1.97 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: layout.c,v 1.98 2026/08/25 18:38:05 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -539,6 +539,10 @@ layout_resize_check(struct window *w, struct layout_cell *lc, status = window_get_pane_status(w); + /* Floating cells do not take space from the tiled layout. */ + if (!layout_cell_is_tiled(lc) && !layout_cell_has_tiled_child(lc)) + return (0); + if (lc->type == LAYOUT_WINDOWPANE) { /* Space available in this cell only. */ if (type == LAYOUT_LEFTRIGHT) { @@ -568,6 +572,9 @@ layout_resize_check(struct window *w, struct layout_cell *lc, /* Different type: minimum of available space in child cells. */ minimum = UINT_MAX; TAILQ_FOREACH(lcchild, &lc->cells, entry) { + if (!layout_cell_is_tiled(lcchild) && + !layout_cell_has_tiled_child(lcchild)) + continue; available = layout_resize_check(w, lcchild, type); if (available < minimum) minimum = available; @@ -661,7 +668,7 @@ layout_resize_set_size(struct window *w, struct layout_cell *lc, /* Find and return the nearest neighbour to a cell in a specific direction. */ static struct layout_cell * -layout_cell_get_neighbour_direction(struct layout_cell *lc, int direction) +layout_cell_get_neighbour_dir(struct layout_cell *lc, int direction) { struct layout_cell *lcn = lc; @@ -695,9 +702,9 @@ layout_cell_get_neighbour(struct layout_cell *lc) if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) direction = !direction; - lcother = layout_cell_get_neighbour_direction(lc, direction); + lcother = layout_cell_get_neighbour_dir(lc, direction); if (lcother == NULL) - lcother = layout_cell_get_neighbour_direction(lc, !direction); + lcother = layout_cell_get_neighbour_dir(lc, !direction); return (lcother); } @@ -860,7 +867,7 @@ layout_resize_pane_to(struct window_pane *wp, enum layout_type type, size = lc->g.sx; else size = lc->g.sy; - if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) + if (layout_cell_is_last_tiled(lc)) change = size - new_size; else change = new_size - size; @@ -985,11 +992,11 @@ layout_resize_pane(struct window_pane *wp, enum layout_type type, int change, if (lcparent == NULL) return; - /* If this is the last cell, move back one. */ - if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) { - do - lc = TAILQ_PREV(lc, layout_cells, entry); - while (lc->flags & LAYOUT_CELL_FLOATING); + /* If this is the last tiled cell, move back one. */ + if (layout_cell_is_last_tiled(lc)) { + lc = layout_cell_get_neighbour_dir(lc, 0); + if (lc == NULL) + return; } layout_resize_layout(wp->window, lc, type, change, opposite); @@ -1007,22 +1014,22 @@ layout_resize_pane_grow(struct window *w, struct layout_cell *lc, lcadd = lc; /* Look towards the tail for a suitable cell for reduction. */ - lcremove = TAILQ_NEXT(lc, entry); + lcremove = layout_cell_get_neighbour_dir(lc, 1); while (lcremove != NULL) { size = layout_resize_check(w, lcremove, type); if (size > 0) break; - lcremove = TAILQ_NEXT(lcremove, entry); + lcremove = layout_cell_get_neighbour_dir(lcremove, 1); } /* If none found, look towards the head. */ if (opposite && lcremove == NULL) { - lcremove = TAILQ_PREV(lc, layout_cells, entry); + lcremove = layout_cell_get_neighbour_dir(lc, 0); while (lcremove != NULL) { size = layout_resize_check(w, lcremove, type); if (size > 0) break; - lcremove = TAILQ_PREV(lcremove, layout_cells, entry); + lcremove = layout_cell_get_neighbour_dir(lcremove, 0); } } if (lcremove == NULL) @@ -1050,13 +1057,13 @@ layout_resize_pane_shrink(struct window *w, struct layout_cell *lc, size = layout_resize_check(w, lcremove, type); if (size != 0) break; - lcremove = TAILQ_PREV(lcremove, layout_cells, entry); + lcremove = layout_cell_get_neighbour_dir(lcremove, 0); } while (lcremove != NULL); if (lcremove == NULL) return (0); /* And add onto the next cell (from the original cell). */ - lcadd = TAILQ_NEXT(lc, entry); + lcadd = layout_cell_get_neighbour_dir(lc, 1); if (lcadd == NULL) return (0); From 52b452948fe8fb11062fa88bb41f3c56d7e0f025 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 28 Aug 2026 08:56:12 +0100 Subject: [PATCH 19/35] Check for buffer write error. --- regress/buffers.sh | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/regress/buffers.sh b/regress/buffers.sh index fdacacb5b..18ae4439c 100644 --- a/regress/buffers.sh +++ b/regress/buffers.sh @@ -28,7 +28,7 @@ $TMUX kill-server 2>/dev/null TMP=$(mktemp) TMP2=$(mktemp) -trap 'rm -f "$TMP" "$TMP2"; $TMUX kill-server 2>/dev/null' 0 1 15 +trap 'rm -f "$TMP" "$TMP2" "$TMP".*; $TMUX kill-server 2>/dev/null' 0 1 15 # check_ok $cmd... # @@ -270,6 +270,27 @@ if [ $? -eq 0 ]; then exit 1 fi +# A write error after a successful open must be reported to the server. +dd if=/dev/zero of="$TMP.big" bs=100000 count=1 2>/dev/null +check_ok load-buffer -b write-error "$TMP.big" +mkfifo "$TMP.fifo" || exit 1 +dd if="$TMP.fifo" of="$TMP.got" bs=1 count=8 2>/dev/null & +reader=$! +exec 3>"$TMP.fifo" +out=$($TMUX save-buffer -b write-error "$TMP.fifo" 3>&- 2>&1) +status=$? +exec 3>&- +wait "$reader" +if [ $status -eq 0 ]; then + echo "save-buffer after write error succeeded." + exit 1 +fi +if [ "$(wc -c <"$TMP.got")" -ne 8 ]; then + echo "save-buffer write error test wrote wrong amount." + exit 1 +fi +check_ok delete-buffer -b write-error + # save-buffer - writes to stdout and load-buffer - reads from stdin. out=$($TMUX save-buffer -b sb -) if [ "$out" != "data" ]; then From 78dad45f3bd13727653633fa43ec62cc336fe6e6 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 28 Aug 2026 09:06:45 +0100 Subject: [PATCH 20/35] Add timeout for lock and don't keep credentials for regress, from Andreas Fehlner. --- .github/workflows/lock.yml | 1 + .github/workflows/regress.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index e6765da99..ab9529b65 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -17,6 +17,7 @@ jobs: action: runs-on: ubuntu-latest if: github.repository == 'tmux/tmux' + timeout-minutes: 10 steps: - uses: dessant/lock-threads@v6 with: diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index a84478357..376fc7bde 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -39,6 +39,8 @@ jobs: steps: - name: checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: dependencies if: runner.os == 'Linux' From 013ada58c864c68402aa28c7ad8eef542f346c9d Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 28 Aug 2026 07:36:01 +0000 Subject: [PATCH 21/35] Ignore focus events earlier to avoid them interfering with prefix, GitHub issue 5539. --- server-client.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server-client.c b/server-client.c index 80efc614d..97c83514f 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.508 2026/08/25 06:14:16 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.509 2026/08/28 07:36:01 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -1504,6 +1504,10 @@ server_client_key_callback(struct cmdq_item *item, void *data) TAILQ_EMPTY(&wp->modes)) goto forward_key; + /* Focus events are not keys and cannot be bound. */ + if (key == KEYC_FOCUS_IN || key == KEYC_FOCUS_OUT) + goto forward_key; + /* * Work out the current key table. If the pane is in a mode, use * the mode table instead of the default key table. From 65e811570be17411e6c1996e2e3a8d5c1707a4ae Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 28 Aug 2026 08:02:16 +0000 Subject: [PATCH 22/35] Do no free input context if it is NULL when popup create fails. --- popup.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/popup.c b/popup.c index 5e037922f..8dd058d57 100644 --- a/popup.c +++ b/popup.c @@ -1,4 +1,4 @@ -/* $OpenBSD: popup.c,v 1.76 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: popup.c,v 1.77 2026/08/28 08:02:16 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -80,7 +80,8 @@ popup_free(struct popup_data *pd) if (pd->job != NULL) job_free(pd->job); - input_free(pd->ictx); + if (pd->ictx != NULL) + input_free(pd->ictx); free(pd->r.ranges); screen_free(&pd->s); From 7bad4f772208e1c78de0ad422c7638d7fcf74c4c Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 28 Aug 2026 08:11:08 +0000 Subject: [PATCH 23/35] Do not walk off end of grid lines if last line is wrapped, reported by Moe Khalilov. --- grid.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/grid.c b/grid.c index ba1bb8730..6ab58eb62 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.156 2026/08/03 12:58:53 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.157 2026/08/28 08:11:08 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -1584,7 +1584,7 @@ grid_wrap_position(struct grid *gd, u_int px, u_int py, u_int *wx, u_int *wy) void grid_unwrap_position(struct grid *gd, u_int *px, u_int *py, u_int wx, u_int wy) { - u_int yy, ay = 0; + u_int yy, ay = 0, ey = gd->hsize + gd->sy - 1; for (yy = 0; yy < gd->hsize + gd->sy - 1; yy++) { if (ay == wy) @@ -1598,7 +1598,7 @@ grid_unwrap_position(struct grid *gd, u_int *px, u_int *py, u_int wx, u_int wy) * until we find the end or the line now containing wx. */ if (wx == UINT_MAX) { - while (gd->linedata[yy].flags & GRID_LINE_WRAPPED) + while (yy < ey && gd->linedata[yy].flags & GRID_LINE_WRAPPED) yy++; wx = gd->linedata[yy].cellused; } else { From 39357c829c62c1e0c0af6e7a8715b0073c65e8e9 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 31 Aug 2026 07:41:48 +0000 Subject: [PATCH 24/35] Clear stale marks when a search regex fails to compile, GitHub issue 5547 from Uzair Aftab. --- window-copy.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/window-copy.c b/window-copy.c index 8c492c42e..2437c1784 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.426 2026/08/19 19:55:04 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.427 2026/08/31 07:41:48 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -4851,6 +4851,8 @@ window_copy_search_marks(struct window_mode_entry *wme, struct screen *ssp, cflags |= REG_ICASE; if (regcomp(®, sbuf, cflags) != 0) { free(sbuf); + free(data->searchmark); + data->searchmark = NULL; return (0); } free(sbuf); From 6a1ecdcce34cd2682b3bf06b14a10ce4f24c8165 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 31 Aug 2026 07:42:56 +0000 Subject: [PATCH 25/35] Do not try to append zero size cells, from Uzair Aftab. --- window-copy.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/window-copy.c b/window-copy.c index 2437c1784..572a199fb 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.427 2026/08/31 07:41:48 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.428 2026/08/31 07:42:56 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -4301,7 +4301,7 @@ window_copy_stringify(struct grid *gd, u_int py, u_int first, u_int last, } if (dlen == 1) buf[bx++] = *d; - else { + else if (dlen != 0) { memcpy(buf + bx, d, dlen); bx += dlen; } From c552d1bf167a240d28f82b4d78a604a12da3326a Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 31 Aug 2026 07:44:39 +0000 Subject: [PATCH 26/35] Layout cell can be replaced by unzoom, fixes use after free. From Uzair Aftab. --- cmd-resize-pane.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index 01ddf424a..fc5062ab4 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-resize-pane.c,v 1.67 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-resize-pane.c,v 1.68 2026/08/31 07:44:39 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -92,6 +92,7 @@ cmd_resize_pane_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); } server_unzoom_window(w); + lc = wp->layout_cell; /* may have been replaced by unzoom */ if (args_has(args, 'x')) { x = args_percentage(args, 'x', 0, PANE_MAXIMUM, w->sx, &cause); From af3e4d2e5b63fa2db14834b17b20b40771744b91 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 31 Aug 2026 07:46:55 +0000 Subject: [PATCH 27/35] Do not allow popups for control clients, from Noam Stolero. --- cmd-display-menu.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd-display-menu.c b/cmd-display-menu.c index 19ad3a9b4..83faaf6ae 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-display-menu.c,v 1.52 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: cmd-display-menu.c,v 1.53 2026/08/31 07:46:55 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -592,6 +592,8 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item) server_client_clear_overlay(tc); return (CMD_RETURN_NORMAL); } + if (tc->flags & CLIENT_CONTROL) + return (CMD_RETURN_NORMAL); if (!modify && tc->overlay_draw != NULL) return (CMD_RETURN_NORMAL); From 8b3834c34fd54df0be8798ad6412aab7d58d118d Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 31 Aug 2026 07:51:56 +0000 Subject: [PATCH 28/35] Do not leak temporary commands when expanding aliases, from Jeong, Heon. --- cmd-parse.y | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd-parse.y b/cmd-parse.y index 29b7780e0..4a6ec66b1 100644 --- a/cmd-parse.y +++ b/cmd-parse.y @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-parse.y,v 1.58 2026/04/27 12:31:11 nicm Exp $ */ +/* $OpenBSD: cmd-parse.y,v 1.59 2026/08/31 07:51:56 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -802,6 +802,7 @@ cmd_parse_expand_alias(struct cmd_parse_command *cmd, if (last == NULL) { pr->status = CMD_PARSE_SUCCESS; pr->cmdlist = cmd_list_new(); + cmd_parse_free_commands(cmds); return (1); } @@ -814,6 +815,7 @@ cmd_parse_expand_alias(struct cmd_parse_command *cmd, pi->flags |= CMD_PARSE_NOALIAS; cmd_parse_build_commands(cmds, pi, pr); pi->flags &= ~CMD_PARSE_NOALIAS; + cmd_parse_free_commands(cmds); return (1); } From 9c1d5a080be8c1c1943fbfd0eee3677ed79cfe47 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 31 Aug 2026 20:52:54 +0100 Subject: [PATCH 29/35] Use hash for actions, from Andreas Fehlner. --- .github/workflows/lock.yml | 2 +- .github/workflows/regress.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index ab9529b65..902783e08 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -19,7 +19,7 @@ jobs: if: github.repository == 'tmux/tmux' timeout-minutes: 10 steps: - - uses: dessant/lock-threads@v6 + - uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6 with: github-token: ${{ github.token }} issue-inactive-days: '30' diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 376fc7bde..b1e19e9e0 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -38,7 +38,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: persist-credentials: false @@ -83,7 +83,7 @@ jobs: - name: logs if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: regress-logs-${{ matrix.name }} path: regress/logs/*.log From f2bfd611e25dbc3ab39164b2ac93c0aeae9f5f75 Mon Sep 17 00:00:00 2001 From: kirill Date: Mon, 31 Aug 2026 12:41:03 +0000 Subject: [PATCH 30/35] tmux: add utf8 terminal-features OK: nicm@ --- tmux.1 | 6 ++++-- tty-features.c | 20 +++++++++++++++++--- window-client.c | 5 +++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/tmux.1 b/tmux.1 index cd861be2b..d2c0a251a 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1159 2026/08/25 08:37:08 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1160 2026/08/31 12:41:03 kirill 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 25 2026 $ +.Dd $Mdocdate: August 31 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -5111,6 +5111,8 @@ Supports title setting. .It usstyle Allows underscore style and colour to be set. +.It utf8 +Supports UTF\-8 output. .El .It Ic terminal\-overrides[] Ar string Allow terminal descriptions read using diff --git a/tty-features.c b/tty-features.c index cebdeb033..56c692083 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-features.c,v 1.42 2026/08/17 14:47:41 nicm Exp $ */ +/* $OpenBSD: tty-features.c,v 1.43 2026/08/31 12:41:03 kirill Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -358,6 +358,13 @@ static const struct tty_feature tty_feature_progressbar = { 0 }; +/* Terminal supports UTF-8. */ +static const struct tty_feature tty_feature_utf8 = { + "utf8", + NULL, + 0 +}; + /* Available terminal features. */ static const struct tty_feature *const tty_features[] = { &tty_feature_256, @@ -380,7 +387,8 @@ static const struct tty_feature *const tty_features[] = { &tty_feature_strikethrough, &tty_feature_sync, &tty_feature_title, - &tty_feature_usstyle + &tty_feature_usstyle, + &tty_feature_utf8 }; /* Parse features for client. */ @@ -463,6 +471,9 @@ tty_feature_present(struct tty_term *term, const char *name) u_int i; char *copy; + if (strcmp(name, "utf8") == 0) + return ((term->tty->client->flags & CLIENT_UTF8) != 0); + for (i = 0; i < nitems(tty_features); i++) { tf = tty_features[i]; if (strcmp(tf->name, name) == 0) { @@ -476,7 +487,8 @@ tty_feature_present(struct tty_term *term, const char *name) * We don't just have the feature flag set. Check if the capabilities * supported by the client are actual set instead. */ - if (tf == NULL || strcmp(name, "ignorefkeys") == 0) + if (tf == NULL || tf->capabilities == NULL || + strcmp(name, "ignorefkeys") == 0) return (0); if (tf->flags != 0 && (term->flags & tf->flags) != tf->flags) return (0); @@ -524,6 +536,8 @@ tty_apply_features(struct tty_term *term) } } term->flags |= tf->flags; + if (tf == &tty_feature_utf8) + c->flags |= CLIENT_UTF8; } if ((term->applied_features|feat) == term->applied_features) return (0); diff --git a/window-client.c b/window-client.c index f42aa5b7a..3c2f81097 100644 --- a/window-client.c +++ b/window-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-client.c,v 1.48 2026/08/05 08:54:56 nicm Exp $ */ +/* $OpenBSD: window-client.c,v 1.49 2026/08/31 12:41:03 kirill Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -102,7 +102,8 @@ static const char *window_client_info_lines[] = { WINDOW_CLIENT_FEATURE(sync) " " WINDOW_CLIENT_FEATURE(title), " #[#{E:tree-mode-border-style},acs]x#[default] " - WINDOW_CLIENT_FEATURE(usstyle), + WINDOW_CLIENT_FEATURE(usstyle) " " + WINDOW_CLIENT_FEATURE(utf8), "#[#{E:tree-mode-border-style},acs]qqqqqqqqqqqqqqn#{R:q,#{window_width}}#[default]", "#[fg=themelightgrey]prefix #[#{E:tree-mode-border-style},acs]x#[default] " From 0530a7d89a2c4eb375c2b0ca1f87bd3e8978e109 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 31 Aug 2026 19:27:46 +0000 Subject: [PATCH 31/35] Add format variables for history change counters, from Michael K Darling. --- format.c | 42 +++++++++++++++++++++++++++++++++++++++++- tmux.1 | 5 ++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/format.c b/format.c index 928eb078e..477675a57 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.413 2026/08/24 07:26:43 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.414 2026/08/31 19:27:46 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1873,6 +1873,37 @@ format_cb_cursor_blinking(struct format_tree *ft) return (NULL); } +/* Callback for history_added. */ +static void * +format_cb_history_added(struct format_tree *ft) +{ + if (ft->wp != NULL) + return (format_printf("%u", ft->wp->base.grid->scroll_added)); + return (NULL); +} + +/* Callback for history_collected. */ +static void * +format_cb_history_collected(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%u", wp->base.grid->scroll_collected)); + return (NULL); +} + +/* Callback for history_generation. */ +static void * +format_cb_history_generation(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%u", wp->base.grid->scroll_generation)); + return (NULL); +} + /* Callback for history_limit. */ static void * format_cb_history_limit(struct format_tree *ft) @@ -3645,12 +3676,21 @@ static const struct format_table_entry format_table[] = { { "cursor_y", FORMAT_TABLE_STRING, format_cb_cursor_y }, + { "history_added", FORMAT_TABLE_STRING, + format_cb_history_added + }, { "history_all_bytes", FORMAT_TABLE_STRING, format_cb_history_all_bytes }, { "history_bytes", FORMAT_TABLE_STRING, format_cb_history_bytes }, + { "history_collected", FORMAT_TABLE_STRING, + format_cb_history_collected + }, + { "history_generation", FORMAT_TABLE_STRING, + format_cb_history_generation + }, { "history_limit", FORMAT_TABLE_STRING, format_cb_history_limit }, diff --git a/tmux.1 b/tmux.1 index d2c0a251a..41e0c03e2 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1160 2026/08/31 12:41:03 kirill Exp $ +.\" $OpenBSD: tmux.1,v 1.1161 2026/08/31 19:27:46 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -7316,7 +7316,10 @@ The following variables are available, where appropriate: .It Li "cursor_very_visible" Ta "" Ta "1 if the cursor is in very visible mode" .It Li "cursor_x" Ta "" Ta "Cursor X position in pane" .It Li "cursor_y" Ta "" Ta "Cursor Y position in pane" +.It Li "history_added" Ta "" Ta "Number of lines scrolled into pane history" .It Li "history_bytes" Ta "" Ta "Number of bytes in window history" +.It Li "history_collected" Ta "" Ta "Number of oldest pane history lines discarded at the history limit" +.It Li "history_generation" Ta "" Ta "Generation incremented when pane history is cleared or reflowed" .It Li "history_limit" Ta "" Ta "Maximum window history lines" .It Li "history_size" Ta "" Ta "Size of history in lines" .It Li "hook" Ta "" Ta "Name of running hook, if any" From b402687fcd9f99d82b5c47b41e082f8032df1530 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 31 Aug 2026 19:34:09 +0000 Subject: [PATCH 32/35] Add a generation counter to pane output, from Michael K Darling. --- format.c | 18 +++++++++++++++++- input.c | 3 ++- tmux.1 | 3 ++- tmux.h | 5 +++-- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/format.c b/format.c index 477675a57..daca3b30e 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.414 2026/08/31 19:27:46 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.415 2026/08/31 19:34:09 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -2293,6 +2293,19 @@ format_cb_pane_last_output_time(struct format_tree *ft) return (NULL); } +/* Callback for pane_output_generation. */ +static void * +format_cb_pane_output_generation(struct format_tree *ft) +{ + unsigned long long value; + + if (ft->wp != NULL) { + value = ft->wp->output_generation; + return (format_printf("%llu", value)); + } + return (NULL); +} + /* Callback for pane_last_prompt_time. */ static void * format_cb_pane_last_prompt_time(struct format_tree *ft) @@ -3871,6 +3884,9 @@ static const struct format_table_entry format_table[] = { { "pane_mode", FORMAT_TABLE_STRING, format_cb_pane_mode }, + { "pane_output_generation", FORMAT_TABLE_STRING, + format_cb_pane_output_generation + }, { "pane_path", FORMAT_TABLE_STRING, format_cb_pane_path }, diff --git a/input.c b/input.c index 5f9c5f4ab..9a443b49d 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.270 2026/08/17 20:04:00 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.271 2026/08/31 19:34:09 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1049,6 +1049,7 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len) if (len == 0) return; + wp->output_generation++; window_update_activity(wp->window); if (~wp->flags & PANE_ACTIVITY) { wp->flags |= PANE_ACTIVITY; diff --git a/tmux.1 b/tmux.1 index 41e0c03e2..2b2b28f17 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1161 2026/08/31 19:27:46 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1162 2026/08/31 19:34:09 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -7418,6 +7418,7 @@ The following variables are available, where appropriate: .It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set" .It Li "pane_modal_flag" Ta "" Ta "1 if pane is modal" .It Li "pane_mode" Ta "" Ta "Name of pane mode, if any" +.It Li "pane_output_generation" Ta "" Ta "Generation incremented when pane output is processed" .It Li "pane_path" Ta "" Ta "Path of pane (can be set by application)" .It Li "pane_pid" Ta "" Ta "PID of first process in pane" .It Li "pane_pipe" Ta "" Ta "1 if pane is being piped" diff --git a/tmux.h b/tmux.h index 2fa3f3987..02059ccc7 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1431 2026/08/25 08:37:08 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1432 2026/08/31 19:34:09 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1323,9 +1323,10 @@ struct window_pane { char tty[TTY_NAME_MAX]; int status; struct timeval dead_time; - struct cmdq_item *wait_item; /* new-pane -W: waiting for pane exit */ + struct cmdq_item *wait_item; struct spawn_editor_state *editor; + uint64_t output_generation; time_t last_output_time; time_t last_prompt_time; time_t cmd_start_time; From 77309c07b081223841dd27ed89159dec93438d20 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 1 Sep 2026 13:50:56 +0100 Subject: [PATCH 33/35] Whitespace regress test from Hongyi Zhao. --- regress/copy-mode-unicode-whitespace.sh | 163 ++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100755 regress/copy-mode-unicode-whitespace.sh diff --git a/regress/copy-mode-unicode-whitespace.sh b/regress/copy-mode-unicode-whitespace.sh new file mode 100755 index 000000000..a532606a8 --- /dev/null +++ b/regress/copy-mode-unicode-whitespace.sh @@ -0,0 +1,163 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +fail() +{ + echo "$1" + [ -z "$TMUX" ] || $TMUX kill-server 2>/dev/null + exit 1 +} + +goto_cell() +{ + row=$1 + column=$2 + + $TMUX send-keys -X history-top || fail "$mode: history-top failed" + $TMUX send-keys -X start-of-line || fail "$mode: start-of-line failed" + [ "$row" -eq 0 ] || + $TMUX send-keys -X -N "$row" cursor-down || + fail "$mode: cursor-down failed" + [ "$column" -eq 0 ] || + $TMUX send-keys -X -N "$column" cursor-right || + fail "$mode: cursor-right failed" +} + +check_cursor() +{ + expected=$1 + actual=$($TMUX display-message -p '#{copy_cursor_x},#{copy_cursor_y}') + [ "$actual" = "$expected" ] || + fail "$mode: expected cursor $expected, got $actual" +} + +check_word() +{ + row=$1 + column=$2 + expected=$3 + + goto_cell "$row" "$column" + $TMUX set-buffer sentinel || fail "$mode: set-buffer failed" + $TMUX send-keys -X select-word || fail "$mode: select-word failed" + $TMUX send-keys -X copy-selection || fail "$mode: copy-selection failed" + actual=$($TMUX show-buffer 2>/dev/null) + [ "$actual" = "$expected" ] || + fail "$mode: expected word '$expected', got '$actual'" +} + +check_next_word() +{ + row=$1 + expected=$2 + + goto_cell "$row" 0 + $TMUX send-keys -X next-word || fail "$mode: next-word failed" + $TMUX set-buffer sentinel || fail "$mode: set-buffer failed" + $TMUX send-keys -X select-word || fail "$mode: select-word failed" + $TMUX send-keys -X copy-selection || fail "$mode: copy-selection failed" + actual=$($TMUX show-buffer 2>/dev/null) + [ "$actual" = "$expected" ] || + fail "$mode: expected next word '$expected', got '$actual'" +} + +for mode in emacs vi; do + TMUX="$TEST_TMUX -Lunicode-whitespace-$mode-$$ -f/dev/null" + $TMUX kill-server 2>/dev/null + $TMUX new-session -d -x16 -y20 \ + "printf 'aa bb\naa\tbb\naa\302\240bb\naa\343\200\200bb\naa:bb\naaaaaaaaaaaaaaa\302\240b\nabcdefghijklmnopq\nhardone\nhardtwo\naa\341\232\200bb\naa\342\200\200bb\naa\342\200\250bb\naa\342\200\251bb\naa\342\200\257bb\naa\342\201\237bb\nxa\302\205b c\n'; exec cat" || + fail "$mode: new-session failed" + $TMUX set-option -g window-size manual || fail "$mode: set size failed" + $TMUX set-window-option -g mode-keys "$mode" || + fail "$mode: set mode-keys failed" + $TMUX set-window-option -g word-separators "" || + fail "$mode: clear word-separators failed" + $TMUX copy-mode || fail "$mode: copy-mode failed" + + # ASCII space and TAB retain their existing behaviour. + goto_cell 0 0 + $TMUX send-keys -X next-space || fail "$mode: ASCII next-space failed" + check_cursor 3,0 + goto_cell 1 0 + $TMUX send-keys -X next-space || fail "$mode: TAB next-space failed" + check_cursor 8,1 + $TMUX send-keys -X previous-space || + fail "$mode: TAB previous-space failed" + check_cursor 0,1 + + # NBSP is whitespace for selection and word movement. + goto_cell 2 0 + $TMUX send-keys -X next-word || fail "$mode: NBSP next-word failed" + check_cursor 3,2 + $TMUX send-keys -X previous-word || + fail "$mode: NBSP previous-word failed" + check_cursor 0,2 + check_word 2 0 aa + goto_cell 2 0 + $TMUX send-keys -X next-word-end || + fail "$mode: NBSP next-word-end failed" + if [ "$mode" = emacs ]; then + check_cursor 2,2 + else + check_cursor 1,2 + fi + + # U+3000 is two columns wide; both its leading and padding cells must be + # skipped as one whitespace character in either direction. + goto_cell 3 0 + $TMUX send-keys -X next-word || fail "$mode: U+3000 next-word failed" + check_cursor 4,3 + $TMUX send-keys -X previous-word || + fail "$mode: U+3000 previous-word failed" + check_cursor 0,3 + goto_cell 3 0 + $TMUX send-keys -X next-space-end || + fail "$mode: U+3000 next-space-end failed" + if [ "$mode" = emacs ]; then + check_cursor 2,3 + else + check_cursor 1,3 + fi + + # Other characters in word-separators remain literal when space enables + # Unicode whitespace matching. + $TMUX set-window-option -g word-separators ': ' || + fail "$mode: set custom word-separators failed" + goto_cell 4 0 + $TMUX send-keys -X next-word || fail "$mode: literal next-word failed" + check_cursor 2,4 + $TMUX set-window-option -g word-separators "" || + fail "$mode: restore word-separators failed" + + # Whitespace in the last column remains a boundary across a soft wrap. + goto_cell 5 0 + $TMUX send-keys -X next-space || + fail "$mode: wrapped NBSP next-space failed" + check_cursor 0,6 + + # A word split only by a soft wrap remains one word, while a hard line + # boundary still terminates selection. + check_word 7 0 abcdefghijklmnopq + check_word 9 0 hardone + + # Check the remaining representative White_Space ranges and U+0085 + # combined into a cell with another character. + check_next_word 11 bb + check_next_word 12 bb + check_next_word 13 bb + check_next_word 14 bb + check_next_word 15 bb + check_next_word 16 bb + goto_cell 17 0 + $TMUX send-keys -X next-word || + fail "$mode: combined U+0085 next-word failed" + check_cursor 2,17 + + $TMUX kill-server 2>/dev/null +done + +exit 0 From 8f3101d8208707b90e2cc93354d37a81cc2ce1ea Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 1 Sep 2026 12:49:49 +0000 Subject: [PATCH 34/35] Extend word commands to use any Unicode whitespace character, GitHub issue 5562 from Hongyi Zhao. --- grid.c | 34 +++++++++++++++---------- options-table.c | 5 ++-- tmux.1 | 19 +++++++++----- tmux.h | 3 ++- utf8.c | 68 ++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 105 insertions(+), 24 deletions(-) diff --git a/grid.c b/grid.c index 6ab58eb62..7539d72ef 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.157 2026/08/28 08:11:08 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.158 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -1660,21 +1660,29 @@ grid_in_set(struct grid *gd, u_int px, u_int py, const char *set) { struct grid_cell gc, tmp_gc; u_int pxx; + int has_tab, has_space; + + has_tab = (strchr(set, '\t') != NULL); + has_space = (strchr(set, ' ') != NULL); grid_get_cell(gd, px, py, &gc); - if (strchr(set, '\t')) { - if (gc.flags & GRID_FLAG_PADDING) { - pxx = px; - do - grid_get_cell(gd, --pxx, py, &tmp_gc); - while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING); - if (tmp_gc.flags & GRID_FLAG_TAB) - return (tmp_gc.data.width - (px - pxx)); - } else if (gc.flags & GRID_FLAG_TAB) - return (gc.data.width); - } - if (gc.flags & GRID_FLAG_PADDING) + if (gc.flags & GRID_FLAG_PADDING) { + if (!has_tab && !has_space) + return (0); + pxx = px; + do + grid_get_cell(gd, --pxx, py, &tmp_gc); + while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING); + if (((has_tab || has_space) && + (tmp_gc.flags & GRID_FLAG_TAB)) || + (has_space && utf8_has_whitespace(&tmp_gc.data))) + return (tmp_gc.data.width - (px - pxx)); return (0); + } + if ((has_tab || has_space) && (gc.flags & GRID_FLAG_TAB)) + return (gc.data.width); + if (has_space && utf8_has_whitespace(&gc.data)) + return (gc.data.width == 0 ? 1 : gc.data.width); return (utf8_cstrhas(set, &gc.data)); } diff --git a/options-table.c b/options-table.c index ecca20153..81f043864 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.243 2026/08/25 08:37:08 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.244 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1251,7 +1251,8 @@ const struct options_table_entry options_table[] = { * underscore. */ .default_str = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", - .text = "Characters considered to separate words." + .text = "Characters considered to separate words; a space matches " + "any character with the Unicode White_Space property." }, /* Window options. */ diff --git a/tmux.1 b/tmux.1 index 2b2b28f17..8ecca4fbd 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1162 2026/08/31 19:34:09 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1163 2026/09/01 12:49:49 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 31 2026 $ +.Dd $Mdocdate: September 1 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -2206,14 +2206,14 @@ Move to the end of the next word. .Xc Same as .Ic next\-word -but use a space alone as the word separator. +but treat all non-whitespace characters as part of a word. .It Xo .Ic next\-space\-end (vi: E) .Xc Same as .Ic next\-word\-end -but use a space alone as the word separator. +but treat all non-whitespace characters as part of a word. .It Xo .Ic other\-end (vi: o) @@ -2287,7 +2287,7 @@ Move to the previous word. .Xc Same as .Ic previous\-word -but use a space alone as the word separator. +but treat all non-whitespace characters as part of a word. .It Xo .Ic rectangle\-on .Xc @@ -2572,8 +2572,8 @@ Word separators can be customized with the session option. Next word moves to the start of the next word, next word end to the end of the next word and previous word to the start of the previous word. -The three next and previous space keys work similarly but use a space alone as -the word separator. +The three next and previous space keys work similarly but treat all +non-whitespace characters as part of a word. Setting .Em word\-separators to the empty string makes next/previous word equivalent to next/previous space. @@ -5647,6 +5647,11 @@ If set to both, a bell and a message are produced. Sets the session's conception of what characters are considered word separators, for the purposes of the next and previous word commands in copy mode. +A space in +.Ar string +matches any character with the Unicode +.Em White_Space +property. .El .Pp Available window options are: diff --git a/tmux.h b/tmux.h index 02059ccc7..c920f8408 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1432 2026/08/31 19:34:09 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1433 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -4057,6 +4057,7 @@ void session_update_history(struct session *); /* utf8.c */ enum utf8_state utf8_towc (const struct utf8_data *, wchar_t *); enum utf8_state utf8_fromwc(wchar_t wc, struct utf8_data *); +int utf8_has_whitespace(const struct utf8_data *); void utf8_update_width_cache(void); utf8_char utf8_build_one(u_char); enum utf8_state utf8_from_data(const struct utf8_data *, utf8_char *); diff --git a/utf8.c b/utf8.c index e634b43f1..076b1ff56 100644 --- a/utf8.c +++ b/utf8.c @@ -1,4 +1,4 @@ -/* $OpenBSD: utf8.c,v 1.71 2026/05/12 09:37:25 nicm Exp $ */ +/* $OpenBSD: utf8.c,v 1.72 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -594,6 +594,72 @@ utf8_towc(const struct utf8_data *ud, wchar_t *wc) return (UTF8_DONE); } +/* Check for a Unicode whitespace character. */ +int +utf8_has_whitespace(const struct utf8_data *ud) +{ + struct utf8_data tmp; + wchar_t wc; + u_int offset = 0, size; + u_char ch; + + while (offset < ud->size) { + ch = ud->data[offset]; + if (ch < 0x80) { + wc = ch; + size = 1; + } else { + if (ch >= 0xc2 && ch <= 0xdf) + size = 2; + else if (ch >= 0xe0 && ch <= 0xef) + size = 3; + else if (ch >= 0xf0 && ch <= 0xf4) + size = 4; + else + return (0); + if (size > ud->size - offset) + return (0); + + memset(&tmp, 0, sizeof tmp); + memcpy(tmp.data, ud->data + offset, size); + tmp.size = tmp.have = size; + if (utf8_towc(&tmp, &wc) != UTF8_DONE) + return (0); + } + offset += size; + + switch (wc) { + case 0x0009: + case 0x000A: + case 0x000B: + case 0x000C: + case 0x000D: + case 0x0020: + case 0x0085: + case 0x00A0: + case 0x1680: + case 0x2000: + case 0x2001: + case 0x2002: + case 0x2003: + case 0x2004: + case 0x2005: + case 0x2006: + case 0x2007: + case 0x2008: + case 0x2009: + case 0x200A: + case 0x2028: + case 0x2029: + case 0x202F: + case 0x205F: + case 0x3000: + return (1); + } + } + return (0); +} + /* Convert wide character to UTF-8 character. */ enum utf8_state utf8_fromwc(wchar_t wc, struct utf8_data *ud) From bd8218e85d9305cda3abbe9a54a4288e538632e9 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 1 Sep 2026 13:04:29 +0000 Subject: [PATCH 35/35] When changing selection-mode to line, set up the selection start and end correctly, GitHub issue 5545. --- window-copy.c | 65 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/window-copy.c b/window-copy.c index 572a199fb..c5529a3ff 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.428 2026/08/31 07:42:56 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.429 2026/09/01 13:04:29 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2207,15 +2207,76 @@ window_copy_cmd_selection_mode(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; struct options *so = cs->s->options; struct window_copy_mode_data *data = wme->data; + struct grid_reader gr; const char *s = args_string(cs->wargs, 0); + u_int sx, sy, ex, ey, fx, fy, x, y; if (s == NULL || strcasecmp(s, "char") == 0 || strcasecmp(s, "c") == 0) data->selflag = SEL_CHAR; else if (strcasecmp(s, "word") == 0 || strcasecmp(s, "w") == 0) { data->separators = options_get_string(so, "word-separators"); data->selflag = SEL_WORD; - } else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0) + } else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0) { data->selflag = SEL_LINE; + if (data->screen.sel == NULL) + return (WINDOW_COPY_CMD_MOVE); + + /* + * Line selection normally starts with select-line, which sets + * up the reset positions used when the cursor changes + * direction. Do the same when changing an existing selection + * to line mode. + */ + if (data->cursordrag == CURSORDRAG_SEL) { + fx = data->endselx; + fy = data->endsely; + } else { + fx = data->selx; + fy = data->sely; + } + + sx = data->selx; + sy = data->sely; + ex = data->endselx; + ey = data->endsely; + if (ey < sy || (ey == sy && ex < sx)) { + x = sx; sx = ex; ex = x; + y = sy; sy = ey; ey = y; + } + grid_reader_start(&gr, data->backing->grid, sx, sy); + grid_reader_cursor_start_of_line(&gr, 1); + grid_reader_get_cursor(&gr, &sx, &sy); + grid_reader_start(&gr, data->backing->grid, ex, ey); + grid_reader_cursor_end_of_line(&gr, 1, 0); + grid_reader_get_cursor(&gr, &ex, &ey); + + data->rectflag = 0; + data->selrx = data->selx = sx; + data->selry = data->sely = sy; + data->endselrx = data->endselx = ex; + data->endselry = data->endsely = ey; + + x = data->cx; + y = screen_hsize(data->backing) + data->cy - data->oy; + data->dx = fx; + data->dy = fy; + if (data->cursordrag != CURSORDRAG_NONE && + (y < fy || (y == fy && x < fx))) { + data->lineflag = LINE_SEL_RIGHT_LEFT; + data->cursordrag = CURSORDRAG_SEL; + window_copy_scroll_to(wme, sx, sy, 1); + } else { + data->lineflag = LINE_SEL_LEFT_RIGHT; + if (data->cursordrag != CURSORDRAG_NONE) { + data->cursordrag = CURSORDRAG_ENDSEL; + x = window_copy_cursor_limit(wme, ey, 0); + window_copy_scroll_to(wme, x, ey, 1); + } + } + if (data->cursordrag == CURSORDRAG_NONE) + window_copy_set_selection(wme, 0, 0); + return (WINDOW_COPY_CMD_REDRAW); + } return (WINDOW_COPY_CMD_MOVE); }