From 415d265728d044a3b7e83cb1fcdae772e08a0fd3 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 29 Jul 2026 14:06:32 +0000 Subject: [PATCH 01/29] Add a flag to turn off scrollbars for display-panes; GitHub issue 5440 from David Marte. --- tmux.h | 4 +++- window-panes.c | 5 +++-- window.c | 18 ++++++++++++++---- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/tmux.h b/tmux.h index 0f3969a1c..4ac613711 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1416 2026/07/27 19:15:58 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1417 2026/07/29 14:06:32 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1160,6 +1160,8 @@ struct window_mode { int flags; #define WINDOW_MODE_HIDE_PANE_STATUS 0x1 #define WINDOW_MODE_NO_STACK 0x2 +#define WINDOW_MODE_FILL_WINDOW 0x4 +#define WINDOW_MODE_HIDE_SCROLLBARS 0x8 struct screen *(*init)(struct window_mode_entry *, struct cmdq_item *, struct cmd_find_state *, diff --git a/window-panes.c b/window-panes.c index 981121460..f1104b4f3 100644 --- a/window-panes.c +++ b/window-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-panes.c,v 1.3 2026/07/19 19:53:11 nicm Exp $ */ +/* $OpenBSD: window-panes.c,v 1.4 2026/07/29 14:06:32 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -35,7 +35,8 @@ static void window_panes_key(struct window_mode_entry *, const struct window_mode window_panes_mode = { .name = "panes-mode", - .flags = WINDOW_MODE_HIDE_PANE_STATUS|WINDOW_MODE_NO_STACK, + .flags = WINDOW_MODE_HIDE_PANE_STATUS|WINDOW_MODE_NO_STACK| + WINDOW_MODE_FILL_WINDOW|WINDOW_MODE_HIDE_SCROLLBARS, .init = window_panes_init, .free = window_panes_free, diff --git a/window.c b/window.c index ea68d23e6..5067ab596 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.368 2026/07/23 09:38:27 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.369 2026/07/29 14:06:32 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2449,11 +2449,21 @@ window_pane_mode(struct window_pane *wp) int window_pane_show_scrollbar(struct window_pane *wp) { + struct window *w = wp->window; + struct window_mode_entry *wme; + if (SCREEN_IS_ALTERNATE(&wp->base)) return (0); - if (wp->window->sb == PANE_SCROLLBARS_ALWAYS || - wp->window->sb == PANE_SCROLLBARS_AUTOHIDE || - (wp->window->sb == PANE_SCROLLBARS_MODAL && + if ((w->flags & WINDOW_ZOOMED) && w->active != NULL) { + wme = TAILQ_FIRST(&w->active->modes); + if (wme != NULL && + (wme->mode->flags & WINDOW_MODE_HIDE_SCROLLBARS)) { + return (0); + } + } + if (w->sb == PANE_SCROLLBARS_ALWAYS || + w->sb == PANE_SCROLLBARS_AUTOHIDE || + (w->sb == PANE_SCROLLBARS_MODAL && window_pane_mode(wp) != WINDOW_PANE_NO_MODE)) return (1); return (0); From 44b8a40b8c8dea2fb938b38263844986b1deb6d1 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 29 Jul 2026 17:42:56 +0000 Subject: [PATCH 02/29] Correctly skip padding at end of line, GitHub issue 5411. --- grid-reader.c | 9 +++------ grid.c | 22 +++++++++++++++++++++- tmux.h | 3 ++- window-copy.c | 11 ++++------- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/grid-reader.c b/grid-reader.c index 9b40def6c..4cf47b8cb 100644 --- a/grid-reader.c +++ b/grid-reader.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid-reader.c,v 1.10 2026/05/17 13:12:21 nicm Exp $ */ +/* $OpenBSD: grid-reader.c,v 1.11 2026/07/29 17:42:56 nicm Exp $ */ /* * Copyright (c) 2020 Anindya Mukherjee @@ -54,11 +54,8 @@ grid_reader_cursor_right(struct grid_reader *gr, int wrap, int all, int onemore) px = gr->gd->sx; else if (onemore) px = grid_reader_line_length(gr); - else { - px = grid_reader_line_length(gr); - if (px != 0) - px--; - } + else + px = grid_line_limit(gr->gd, gr->cy); if (wrap && gr->cx >= px && gr->cy < gr->gd->hsize + gr->gd->sy - 1) { grid_reader_cursor_start_of_line(gr, 0); diff --git a/grid.c b/grid.c index fa0c94ffe..4e837f108 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.154 2026/07/20 11:16:33 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.155 2026/07/29 17:42:56 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -1630,6 +1630,26 @@ grid_line_length(struct grid *gd, u_int py) return (px); } +/* Get last position on line, not including padding. */ +u_int +grid_line_limit(struct grid *gd, u_int py) +{ + struct grid_cell gc; + u_int px; + + px = grid_line_length(gd, py); + if (px == 0) + return (0); + px--; + while (px > 0) { + grid_get_cell(gd, px, py, &gc); + if (~gc.flags & GRID_FLAG_PADDING) + break; + px--; + } + return (px); +} + /* Check if character is in set. */ int grid_in_set(struct grid *gd, u_int px, u_int py, const char *set) diff --git a/tmux.h b/tmux.h index 4ac613711..ca5df3826 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1417 2026/07/29 14:06:32 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1418 2026/07/29 17:42:56 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3464,6 +3464,7 @@ void grid_reflow(struct grid *, u_int); void grid_wrap_position(struct grid *, u_int, u_int, u_int *, u_int *); void grid_unwrap_position(struct grid *, u_int *, u_int *, u_int, u_int); u_int grid_line_length(struct grid *, u_int); +u_int grid_line_limit(struct grid *, u_int); int grid_in_set(struct grid *, u_int, u_int, const char *); /* grid-reader.c */ diff --git a/window-copy.c b/window-copy.c index ff58dfbad..dabb2a99c 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.423 2026/07/21 11:52:13 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.424 2026/07/29 17:42:56 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -6215,16 +6215,13 @@ static u_int window_copy_cursor_limit(struct window_mode_entry *wme, u_int py, int allow_onemore) { + struct window_copy_mode_data *data = wme->data; struct options *oo = wme->wp->window->options; - u_int len; - len = window_copy_find_length(wme, py); if (allow_onemore || options_get_number(oo, "mode-keys") != MODEKEY_VI) - return (len); - if (len == 0) - return (0); - return (len - 1); + return (window_copy_find_length(wme, py)); + return (grid_line_limit(data->backing->grid, py)); } static void From 265fb7b4ea386be4b2b3cd226113c74a370e8246 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 29 Jul 2026 21:18:38 +0100 Subject: [PATCH 03/29] Test for vi keys and padding. --- regress/copy-mode-test-vi.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/regress/copy-mode-test-vi.sh b/regress/copy-mode-test-vi.sh index 71810b006..11b5ef894 100644 --- a/regress/copy-mode-test-vi.sh +++ b/regress/copy-mode-test-vi.sh @@ -114,5 +114,29 @@ $TMUX send-keys -X next-space-end $TMUX send-keys -X copy-selection [ "$($TMUX show-buffer)" = "500xyz" ] || exit 1 +# Test that vi cursor movement does not stop on the padding cell of a wide +# character at the end of a line. +$TMUX kill-server 2>/dev/null +$TMUX new -d -x20 -y5 \ + "printf 'abc中\nxyz\n'; exec cat" || exit 1 +$TMUX set-window-option -g mode-keys vi +$TMUX copy-mode +$TMUX send-keys -X history-top +$TMUX send-keys -X start-of-line +$TMUX send-keys -X cursor-right +$TMUX send-keys -X cursor-right +$TMUX send-keys -X cursor-right +[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "3,0" ] || + exit 1 +$TMUX send-keys -X cursor-right +[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "0,1" ] || + exit 1 +$TMUX send-keys -X cursor-left +[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "3,0" ] || + exit 1 +$TMUX send-keys -X cursor-left +[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "2,0" ] || + exit 1 + $TMUX kill-server 2>/dev/null exit 0 From 8dfb7d029488ec6f6bee7ba47e317b2de78712ab Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 29 Jul 2026 20:43:20 +0000 Subject: [PATCH 04/29] Fix unzoomed width when scrollbars are hidden. --- format.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/format.c b/format.c index 4b35027a5..40c495ff7 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.409 2026/07/26 15:21:53 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.410 2026/07/29 20:43:20 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -2614,20 +2614,23 @@ format_cb_pane_unzoomed_width(struct format_tree *ft) { struct window_pane *wp = ft->wp; struct layout_cell *lc; - int sb_w, sb_pad; + int saved, sb_w, sb_pad; u_int sx; if (wp == NULL) return (NULL); lc = wp->saved_layout_cell; + saved = (lc != NULL); if (lc == NULL) lc = wp->layout_cell; if (lc == NULL) return (NULL); sx = lc->g.sx; - if (window_pane_scrollbar_reserve(wp)) { + if ((saved && !SCREEN_IS_ALTERNATE(&wp->base) && + wp->window->sb == PANE_SCROLLBARS_ALWAYS) || + (!saved && window_pane_scrollbar_reserve(wp))) { sb_w = wp->scrollbar_style.width; sb_pad = wp->scrollbar_style.pad; if (sb_w < 1) From cce56fa19b29ae2368390bdf2d464e97d61b59f3 Mon Sep 17 00:00:00 2001 From: miod Date: Sat, 1 Aug 2026 17:04:12 +0000 Subject: [PATCH 05/29] Pessimize compiler flags for screen-redraw.c to prevent tmux from dumping core upon startup on landisk. --- Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d7673eab2..a6d16a618 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# $OpenBSD: Makefile,v 1.121 2026/07/14 17:17:17 nicm Exp $ +# $OpenBSD: Makefile,v 1.122 2026/08/01 17:04:12 miod Exp $ PROG= tmux SRCS= alerts.c \ @@ -153,4 +153,9 @@ CFLAGS += -I${.CURDIR} LDADD= -lutil -lcurses -levent -lm DPADD= ${LIBUTIL} ${LIBCURSES} ${LIBEVENT} ${LIBM} +.if "${MACHINE_ARCH}" == "sh" +screen-redraw.o: + ${CC} ${CFLAGS} -fno-stack-protector ${CPPFLAGS} -c ${.IMPSRC} +.endif + .include From fa8c5789a6efb552ef6dd1690ed9534b43e8ccbb Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 3 Aug 2026 09:53:43 +0100 Subject: [PATCH 06/29] Sleep after kill. --- regress/copy-mode-test-vi.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/regress/copy-mode-test-vi.sh b/regress/copy-mode-test-vi.sh index 11b5ef894..817a5d4af 100644 --- a/regress/copy-mode-test-vi.sh +++ b/regress/copy-mode-test-vi.sh @@ -117,6 +117,7 @@ $TMUX send-keys -X copy-selection # Test that vi cursor movement does not stop on the padding cell of a wide # character at the end of a line. $TMUX kill-server 2>/dev/null +sleep 1 $TMUX new -d -x20 -y5 \ "printf 'abc中\nxyz\n'; exec cat" || exit 1 $TMUX set-window-option -g mode-keys vi From fc9270c9430c18c4e114580c1005e1ae878cc9ec Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 3 Aug 2026 11:09:04 +0100 Subject: [PATCH 07/29] Test for socket error from Rayan Salhab. --- regress/server-socket-error.sh | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100755 regress/server-socket-error.sh diff --git a/regress/server-socket-error.sh b/regress/server-socket-error.sh new file mode 100755 index 000000000..60ff4c07d --- /dev/null +++ b/regress/server-socket-error.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +# Server socket creation failures must produce a nonzero client exit status. + +PATH=/bin:/usr/bin +TERM=screen +export TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +# Root bypasses directory mode bits, so it cannot trigger this failure. +[ "$(id -u)" -eq 0 ] && exit 0 + +TMP=$(mktemp -d) || exit 1 +SOCKET="$TMP/ro/socket" + +cleanup() +{ + chmod 700 "$TMP/ro" 2>/dev/null + rm -rf "$TMP" +} + +fail() +{ + echo "$1" >&2 + cleanup + exit 1 +} + +mkdir "$TMP/ro" || exit 1 +chmod 500 "$TMP/ro" || exit 1 +trap cleanup 0 1 15 + +check_failure() +{ + output=$($TEST_TMUX -S "$SOCKET" -f/dev/null "$@" 2>&1) + retval=$? + + [ "$retval" -ne 0 ] || fail "tmux $* exited zero: $output" + case "$output" in + "error creating $SOCKET ("*) ;; + *) fail "tmux $* produced unexpected error: $output" ;; + esac +} + +check_failure start-server +check_failure new-session -d + +exit 0 From 433a5cf580f44c0d88dbc1752a8bfb459162e14d Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 3 Aug 2026 13:59:27 +0100 Subject: [PATCH 08/29] Add regress test from Ayman Bagabas. --- regress/tab-cell-background.sh | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100755 regress/tab-cell-background.sh diff --git a/regress/tab-cell-background.sh b/regress/tab-cell-background.sh new file mode 100755 index 000000000..350b77854 --- /dev/null +++ b/regress/tab-cell-background.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +# When a write breaks up a wide character or a tab, the columns it covered must +# keep the background colour, both in the grid and on the terminal. The second +# server is attached to the first so that what is actually sent is checked, not +# just what tmux has stored. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" +$TMUX2 kill-server 2>/dev/null + +TMP=$(mktemp) +trap "rm -f $TMP" 0 1 15 + +# 1. Write inside a tab. 2. Overwrite the right half of a wide character. +# 3. Two tabs of different colours, the first partly deleted so that padding +# from both ends up next to each other, then written over. +# The sleep is so that the client is attached before anything is written and +# the changes are sent as they happen rather than as one redraw. +$TMUX2 -f/dev/null new -d -x24 -y3 " + sleep 2 + printf '\033[44m\033[K\t\033[4GX' + printf '\033[2;1H\033[44m\033[K\344\270\226\033[2GX' + printf '\033[3;1H\033[42m\033[K\t\033[43m\033[K\t\033[0m\033[4G\033[8P\rx' + cat" || exit 1 +$TMUX2 set -g status off || exit 1 + +$TMUX -f/dev/null new -x24 -y4 -d "$TMUX2 attach" || exit 1 +sleep 4 + +$TMUX capturep -pe -S0 -E2 >$TMP || exit 1 +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +printf '\033[44m X\n X\n\033[49mx\033[42m \033[43m \033[49m\n' | + cmp - $TMP || exit 1 + +exit 0 From 4918360ea12cdc15f2a43ce021f7e0631eb46856 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 3 Aug 2026 10:07:57 +0000 Subject: [PATCH 09/29] Exit failure on socket failure, from Rayan Salhab. --- server.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server.c b/server.c index 6fe7f2c7b..ffd5b4ddf 100644 --- a/server.c +++ b/server.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server.c,v 1.214 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: server.c,v 1.215 2026/08/03 10:07:57 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -234,6 +234,7 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base, if (cause != NULL) { if (c != NULL) { c->exit_message = cause; + c->retval = 1; c->flags |= CLIENT_EXIT; } else { fprintf(stderr, "%s\n", cause); @@ -392,6 +393,7 @@ server_accept(int fd, short events, __unused void *data) c = server_client_create(newfd); if (!server_acl_join(c)) { c->exit_message = xstrdup("access not allowed"); + c->retval = 1; c->flags |= CLIENT_EXIT; } } From 01a775a46c250761cf650058010fc0c6fe128732 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 3 Aug 2026 12:58:53 +0000 Subject: [PATCH 10/29] Store background colour in padding cells and correctly clear adjacent cells when tabs are overwritten, GitHu issue 5441 from Ayman Bagabas. --- grid-view.c | 6 +-- grid.c | 10 +++-- screen-write.c | 112 +++++++++++++++++++++++++++++-------------------- tmux.h | 6 +-- 4 files changed, 79 insertions(+), 55 deletions(-) diff --git a/grid-view.c b/grid-view.c index 86317526f..5659d5763 100644 --- a/grid-view.c +++ b/grid-view.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid-view.c,v 1.38 2026/01/22 08:55:01 nicm Exp $ */ +/* $OpenBSD: grid-view.c,v 1.39 2026/08/03 12:58:53 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -47,9 +47,9 @@ grid_view_set_cell(struct grid *gd, u_int px, u_int py, /* Set padding. */ void -grid_view_set_padding(struct grid *gd, u_int px, u_int py) +grid_view_set_padding(struct grid *gd, u_int px, u_int py, int bg) { - grid_set_padding(gd, grid_view_x(gd, px), grid_view_y(gd, py)); + grid_set_padding(gd, grid_view_x(gd, px), grid_view_y(gd, py), bg); } /* Set cells. */ diff --git a/grid.c b/grid.c index 4e837f108..ba1bb8730 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.155 2026/07/29 17:42:56 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.156 2026/08/03 12:58:53 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -631,9 +631,13 @@ grid_set_cell(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc) /* Set padding at position. */ void -grid_set_padding(struct grid *gd, u_int px, u_int py) +grid_set_padding(struct grid *gd, u_int px, u_int py, int bg) { - grid_set_cell(gd, px, py, &grid_padding_cell); + struct grid_cell gc; + + memcpy(&gc, &grid_padding_cell, sizeof gc); + gc.bg = bg; + grid_set_cell(gd, px, py, &gc); } /* Set cells at position. */ diff --git a/screen-write.c b/screen-write.c index 6cfc80ad0..71dc99200 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.285 2026/07/26 09:02:08 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.286 2026/08/03 12:58:53 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2393,15 +2393,62 @@ screen_write_collect_insert_clear(struct screen_write_ctx *ctx, u_int px, } } +/* + * Clear a cell that is being broken up by a write over part of it, keeping the + * background so the columns it covered do not lose their colour. + */ +static void +screen_write_clear_cell(struct grid *gd, u_int px, u_int py) +{ + struct grid_cell gc; + int bg; + + grid_view_get_cell(gd, px, py, &gc); + bg = gc.bg; + + memcpy(&gc, &grid_default_cell, sizeof gc); + gc.bg = bg; + grid_view_set_cell(gd, px, py, &gc); +} + +/* + * Insert clears for a range of cells already cleared in the grid. Adjacent + * cells may have come from different characters and so have different + * backgrounds, so this is done in runs of the same colour. + */ +static void +screen_write_insert_clears(struct screen_write_ctx *ctx, u_int px, u_int nx) +{ + struct screen *s = ctx->s; + struct grid_cell gc; + u_int xx, start = px, n; + int bg = 8; + + for (xx = px; xx < px + nx; xx++) { + grid_view_get_cell(s->grid, xx, s->cy, &gc); + if (xx == start) + bg = gc.bg; + else if (gc.bg != bg) { + n = xx - start; + log_debug("%s: from %u, size %u", __func__, start, n); + screen_write_collect_insert_clear(ctx, start, n, bg); + start = xx; + bg = gc.bg; + } + } + log_debug("%s: from %u, size %u", __func__, start, xx - start); + screen_write_collect_insert_clear(ctx, start, xx - start, bg); +} + /* Finish and store collected cells. */ void screen_write_collect_end(struct screen_write_ctx *ctx) { struct screen *s = ctx->s; - struct screen_write_citem *ci = ctx->item, *bci = NULL, *aci; + struct screen_write_citem *ci = ctx->item; struct screen_write_cline *cl = &s->write_list[s->cy]; struct grid_cell gc; - u_int xx; + u_int xx, bx = 0, bnx = 0; if (ci->used == 0) return; @@ -2417,8 +2464,7 @@ screen_write_collect_end(struct screen_write_ctx *ctx) grid_view_get_cell(s->grid, xx, s->cy, &gc); if (~gc.flags & GRID_FLAG_PADDING) break; - grid_view_set_cell(s->grid, xx, s->cy, - &grid_default_cell); + screen_write_clear_cell(s->grid, xx, s->cy); log_debug("%s: padding erased (before) at %u (cx %u)", __func__, xx, s->cx); } @@ -2427,47 +2473,31 @@ screen_write_collect_end(struct screen_write_ctx *ctx) grid_view_get_cell(s->grid, 0, s->cy, &gc); if (gc.data.width > 1 || (gc.flags & GRID_FLAG_PADDING)) { - grid_view_set_cell(s->grid, xx, s->cy, - &grid_default_cell); + screen_write_clear_cell(s->grid, xx, s->cy); log_debug("%s: padding erased (before) at %u " "(cx %u)", __func__, xx, s->cx); } - } - if (xx != s->cx) { - bci = ctx->item; - bci->type = CLEAR; - bci->x = xx; - bci->bg = 8; - bci->used = s->cx - xx; - log_debug("%s: padding erased (before): from %u, " - "size %u", __func__, bci->x, bci->used); + bx = xx; + bnx = s->cx - xx; } } grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x, ci->used); - if (bci != NULL) - screen_write_collect_insert(ctx, bci); + if (bnx != 0) + screen_write_insert_clears(ctx, bx, bnx); screen_write_set_cursor(ctx, s->cx + ci->used, -1); for (xx = s->cx; xx < screen_size_x(s); xx++) { grid_view_get_cell(s->grid, xx, s->cy, &gc); if (~gc.flags & GRID_FLAG_PADDING) break; - grid_view_set_cell(s->grid, xx, s->cy, &grid_default_cell); - log_debug("%s: padding erased (after) at %u (cx %u)", - __func__, xx, s->cx); - } - if (xx != s->cx) { - aci = ctx->item; - aci->type = CLEAR; - aci->x = s->cx; - aci->bg = 8; - aci->used = xx - s->cx; - log_debug("%s: padding erased (after): from %u, size %u", - __func__, aci->x, aci->used); - screen_write_collect_insert(ctx, aci); + screen_write_clear_cell(s->grid, xx, s->cy); + log_debug("%s: padding erased (after) at %u (cx %u)", __func__, + xx, s->cx); } + if (xx != s->cx) + screen_write_insert_clears(ctx, s->cx, xx - s->cx); } /* Write cell data, collecting if necessary. */ @@ -2595,7 +2625,7 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) */ for (xx = s->cx + 1; xx < s->cx + width; xx++) { log_debug("%s: new padding at %u,%u", __func__, xx, s->cy); - grid_view_set_padding(gd, xx, s->cy); + grid_view_set_padding(gd, xx, s->cy, gc->bg); skip = 0; } @@ -2807,7 +2837,7 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) /* Set the new cell. */ grid_view_set_cell(gd, cx - n, cy, &last); if (force_wide) - grid_view_set_padding(gd, cx - 1, cy); + grid_view_set_padding(gd, cx - 1, cy, last.bg); /* * Check if all of this character is visible. No character will be @@ -2876,12 +2906,12 @@ screen_write_overwrite(struct screen_write_ctx *ctx, struct grid_cell *gc, if (~tmp_gc.flags & GRID_FLAG_PADDING) break; log_debug("%s: padding at %u,%u", __func__, xx, s->cy); - grid_view_set_cell(gd, xx, s->cy, &grid_default_cell); + screen_write_clear_cell(gd, xx, s->cy); } /* Overwrite the character at the start of this padding. */ log_debug("%s: character at %u,%u", __func__, xx, s->cy); - grid_view_set_cell(gd, xx, s->cy, &grid_default_cell); + screen_write_clear_cell(gd, xx, s->cy); done = 1; } @@ -2899,17 +2929,7 @@ screen_write_overwrite(struct screen_write_ctx *ctx, struct grid_cell *gc, break; log_debug("%s: overwrite at %u,%u", __func__, xx, s->cy); - if (gc->flags & GRID_FLAG_TAB) { - memcpy(&tmp_gc, gc, sizeof tmp_gc); - memset(tmp_gc.data.data, 0, - sizeof tmp_gc.data.data); - *tmp_gc.data.data = ' '; - tmp_gc.data.width = tmp_gc.data.size = - tmp_gc.data.have = 1; - grid_view_set_cell(gd, xx, s->cy, &tmp_gc); - } else - grid_view_set_cell(gd, xx, s->cy, - &grid_default_cell); + screen_write_clear_cell(gd, xx, s->cy); done = 1; } } diff --git a/tmux.h b/tmux.h index ca5df3826..f880f4ae2 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1418 2026/07/29 17:42:56 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1419 2026/08/03 12:58:53 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3447,7 +3447,7 @@ void grid_clear_history(struct grid *); const struct grid_line *grid_peek_line(struct grid *, u_int); void grid_get_cell(struct grid *, u_int, u_int, struct grid_cell *); void grid_set_cell(struct grid *, u_int, u_int, const struct grid_cell *); -void grid_set_padding(struct grid *, u_int, u_int); +void grid_set_padding(struct grid *, u_int, u_int, int); void grid_set_cells(struct grid *, u_int, u_int, const struct grid_cell *, const char *, size_t); struct grid_line *grid_get_line(struct grid *, u_int); @@ -3492,7 +3492,7 @@ void grid_reader_cursor_back_to_indentation(struct grid_reader *); void grid_view_get_cell(struct grid *, u_int, u_int, struct grid_cell *); void grid_view_set_cell(struct grid *, u_int, u_int, const struct grid_cell *); -void grid_view_set_padding(struct grid *, u_int, u_int); +void grid_view_set_padding(struct grid *, u_int, u_int, int); void grid_view_set_cells(struct grid *, u_int, u_int, const struct grid_cell *, const char *, size_t); void grid_view_clear_history(struct grid *, u_int); From 6db5175e4edaaf3d57822282a980813e388667a8 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 3 Aug 2026 13:38:42 +0000 Subject: [PATCH 11/29] Queue control mode notifications rather than emitting them inside %begin/%end, GitHub issue 5458 from George Nachman. --- cfg.c | 16 +++-- cmd-queue.c | 4 +- control-notify.c | 73 +++++++++++---------- control.c | 163 +++++++++++++++++++++++++++++++++++++---------- tmux.h | 4 +- 5 files changed, 183 insertions(+), 77 deletions(-) diff --git a/cfg.c b/cfg.c index 8e67105a0..a4ba07061 100644 --- a/cfg.c +++ b/cfg.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cfg.c,v 1.90 2026/07/14 17:17:17 nicm Exp $ */ +/* $OpenBSD: cfg.c,v 1.91 2026/08/03 13:38:42 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -224,13 +224,15 @@ cfg_print_causes(struct cmdq_item *item) { struct client *c = cmdq_get_client(item); u_int i; + char *cause; for (i = 0; i < cfg_ncauses; i++) { + cause = cfg_causes[i]; if (c != NULL && (c->flags & CLIENT_CONTROL)) - control_write(c, "%%config-error %s", cfg_causes[i]); + control_notify_write(c, "%%config-error %s", cause); else - cmdq_print(item, "%s", cfg_causes[i]); - free(cfg_causes[i]); + cmdq_print(item, "%s", cause); + free(cause); } free(cfg_causes); @@ -245,14 +247,16 @@ cfg_show_causes(struct session *s) struct window_pane *wp; struct window_mode_entry *wme; u_int i; + char *cause; if (cfg_ncauses == 0) return; if (c != NULL && (c->flags & CLIENT_CONTROL)) { for (i = 0; i < cfg_ncauses; i++) { - control_write(c, "%%config-error %s", cfg_causes[i]); - free(cfg_causes[i]); + cause = cfg_causes[i]; + control_notify_write(c, "%%config-error %s", cause); + free(cause); } goto out; } diff --git a/cmd-queue.c b/cmd-queue.c index 973b28e37..dc1b8cc63 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-queue.c,v 1.121 2026/07/14 17:17:17 nicm Exp $ */ +/* $OpenBSD: cmd-queue.c,v 1.122 2026/08/03 13:38:42 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -803,7 +803,7 @@ cmdq_guard(struct cmdq_item *item, const char *guard, int flags) u_int number = item->number; if (c != NULL && (c->flags & CLIENT_CONTROL)) - control_write(c, "%%%s %ld %u %d", guard, t, number, flags); + control_write_guard(c, guard, t, number, flags); } /* Show message from command. */ diff --git a/control-notify.c b/control-notify.c index 6fec1683f..adfc9dcb0 100644 --- a/control-notify.c +++ b/control-notify.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control-notify.c,v 1.37 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: control-notify.c,v 1.38 2026/08/03 13:38:42 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -44,7 +44,8 @@ control_pane_mode_changed_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - control_write(c, "%%pane-mode-changed %%%u", wp->id); + control_notify_write(c, "%%pane-mode-changed %%%u", + wp->id); } return; } @@ -53,8 +54,9 @@ control_pane_mode_changed_cb(__unused const char *name, if (value == NULL) return; TAILQ_FOREACH(c, &clients, entry) { - if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) - control_write(c, "%%pane-mode-changed %s", value); + if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) + continue; + control_notify_write(c, "%%pane-mode-changed %s", value); } free(value); } @@ -92,7 +94,7 @@ control_window_layout_changed_cb(__unused const char *name, continue; s = c->session; if (winlink_find_by_window_id(&s->windows, w->id) != NULL) - control_write(c, "%s", cp); + control_notify_write(c, "%s", cp); } free(cp); } @@ -111,7 +113,7 @@ control_window_pane_changed_cb(__unused const char *name, if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - control_write(c, "%%window-pane-changed @%u %%%u", w->id, + control_notify_write(c, "%%window-pane-changed @%u %%%u", w->id, w->active->id); } } @@ -133,9 +135,11 @@ control_window_unlinked_cb(__unused const char *name, struct event_payload *ep, cs = c->session; if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) - control_write(c, "%%window-close @%u", w->id); - else - control_write(c, "%%unlinked-window-close @%u", w->id); + control_notify_write(c, "%%window-close @%u", w->id); + else { + control_notify_write(c, "%%unlinked-window-close @%u", + w->id); + } } } @@ -156,9 +160,11 @@ control_window_linked_cb(__unused const char *name, struct event_payload *ep, cs = c->session; if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) - control_write(c, "%%window-add @%u", w->id); - else - control_write(c, "%%unlinked-window-add @%u", w->id); + control_notify_write(c, "%%window-add @%u", w->id); + else { + control_notify_write(c, "%%unlinked-window-add @%u", + w->id); + } } } @@ -179,11 +185,11 @@ control_window_renamed_cb(__unused const char *name, struct event_payload *ep, cs = c->session; if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) { - control_write(c, "%%window-renamed @%u %s", w->id, - w->name); - } else { - control_write(c, "%%unlinked-window-renamed @%u %s", + control_notify_write(c, "%%window-renamed @%u %s", w->id, w->name); + } else { + control_notify_write(c, + "%%unlinked-window-renamed @%u %s", w->id, w->name); } } } @@ -206,11 +212,12 @@ control_client_session_changed_cb(__unused const char *name, continue; if (cc == c) { - control_write(c, "%%session-changed $%u %s", s->id, - s->name); + control_notify_write(c, "%%session-changed $%u %s", + s->id, s->name); } else { - control_write(c, "%%client-session-changed %s $%u %s", - cc->name, s->id, s->name); + control_notify_write(c, + "%%client-session-changed %s $%u %s", cc->name, + s->id, s->name); } } } @@ -226,8 +233,9 @@ control_client_detached_cb(__unused const char *name, struct event_payload *ep, if (cc == NULL) return; TAILQ_FOREACH(c, &clients, entry) { - if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) - control_write(c, "%%client-detached %s", cc->name); + if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) + continue; + control_notify_write(c, "%%client-detached %s", cc->name); } } @@ -244,8 +252,8 @@ control_session_renamed_cb(__unused const char *name, struct event_payload *ep, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%session-renamed $%u %s", s->id, s->name); + control_notify_write(c, "%%session-renamed $%u %s", s->id, + s->name); } } @@ -259,8 +267,7 @@ control_session_created_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%sessions-changed"); + control_notify_write(c, "%%sessions-changed"); } } @@ -274,8 +281,7 @@ control_session_closed_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%sessions-changed"); + control_notify_write(c, "%%sessions-changed"); } } @@ -298,9 +304,8 @@ control_session_window_changed_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%session-window-changed $%u @%u", s->id, - s->curw->window->id); + control_notify_write(c, "%%session-window-changed $%u @%u", + s->id, s->curw->window->id); } } @@ -317,8 +322,7 @@ control_paste_buffer_changed_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%paste-buffer-changed %s", pbname); + control_notify_write(c, "%%paste-buffer-changed %s", pbname); } } @@ -335,8 +339,7 @@ control_paste_buffer_deleted_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%paste-buffer-deleted %s", pbname); + control_notify_write(c, "%%paste-buffer-deleted %s", pbname); } } diff --git a/control.c b/control.c index 92645a8bc..d3d078dd0 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.62 2026/07/17 08:37:29 nicm Exp $ */ +/* $OpenBSD: control.c,v 1.63 2026/08/03 13:38:42 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -53,6 +53,17 @@ struct control_block { TAILQ_ENTRY(control_block) all_entry; }; +/* + * A notification line deferred because it was generated while a command's + * %begin/%end guard block was open. Notifications must never appear inside a + * guard block, so they are held here and flushed once the block closes. + */ +struct control_line { + char *line; + + TAILQ_ENTRY(control_line) entry; +}; + /* Control client pane. */ struct control_pane { u_int pane; @@ -102,6 +113,13 @@ struct control_state { struct bufferevent *write_event; struct monitor_set *subs; + + /* + * Depth of open %begin/%end guard blocks and notifications deferred + * until the outermost block closes. + */ + int guard_depth; + TAILQ_HEAD(, control_line) deferred; }; /* Low and high watermarks. */ @@ -352,7 +370,7 @@ control_continue_pane(struct client *c, struct window_pane *wp) cp->flags &= ~CONTROL_PANE_PAUSED; memcpy(&cp->offset, &wp->offset, sizeof cp->offset); memcpy(&cp->queued, &wp->offset, sizeof cp->queued); - control_write(c, "%%continue %%%u", wp->id); + control_notify_write(c, "%%continue %%%u", wp->id); } } @@ -366,52 +384,120 @@ control_pause_pane(struct client *c, struct window_pane *wp) if (~cp->flags & CONTROL_PANE_PAUSED) { cp->flags |= CONTROL_PANE_PAUSED; control_discard_pane(c, cp); - control_write(c, "%%pause %%%u", wp->id); + control_notify_write(c, "%%pause %%%u", wp->id); } } -/* Write a line. */ -static void printflike(2, 0) -control_vwrite(struct client *c, const char *fmt, va_list ap) -{ - struct control_state *cs = c->control_state; - char *s; - - xvasprintf(&s, fmt, ap); - log_debug("%s: %s: writing line: %s", __func__, c->name, s); - - bufferevent_write(cs->write_event, s, strlen(s)); - bufferevent_write(cs->write_event, "\n", 1); - - bufferevent_enable(cs->write_event, EV_WRITE); - free(s); -} - -/* Write a line. */ -void -control_write(struct client *c, const char *fmt, ...) +/* Write an already-formatted line, queueing it behind %output if needed. */ +static void +control_write_line(struct client *c, char *line) { struct control_state *cs = c->control_state; struct control_block *cb; - va_list ap; - - va_start(ap, fmt); if (TAILQ_EMPTY(&cs->all_blocks)) { - control_vwrite(c, fmt, ap); - va_end(ap); + log_debug("%s: %s: writing line: %s", __func__, c->name, line); + bufferevent_write(cs->write_event, line, strlen(line)); + bufferevent_write(cs->write_event, "\n", 1); + bufferevent_enable(cs->write_event, EV_WRITE); + free(line); return; } cb = xcalloc(1, sizeof *cb); - xvasprintf(&cb->line, fmt, ap); + cb->line = line; TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry); cb->t = get_timer(); log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line); bufferevent_enable(cs->write_event, EV_WRITE); +} +/* Flush notifications that were deferred while a guard block was open. */ +static void +control_flush_deferred(struct client *c) +{ + struct control_state *cs = c->control_state; + struct control_line *cl, *cl1; + + TAILQ_FOREACH_SAFE(cl, &cs->deferred, entry, cl1) { + TAILQ_REMOVE(&cs->deferred, cl, entry); + control_write_line(c, cl->line); + free(cl); + } +} + +/* + * Write a line of command output or error text. This is a sink for arbitrary + * user-controlled text (command output, capture-pane, error messages), so it + * must never try to interpret the content: guard tracking is done only in + * control_write_guard. + */ +void +control_write(struct client *c, const char *fmt, ...) +{ + va_list ap; + char *line; + + va_start(ap, fmt); + xvasprintf(&line, fmt, ap); va_end(ap); + + control_write_line(c, line); +} + +/* + * Write a %begin, %end or %error guard around a command's output. This is the + * only place guard lines are produced, so the block depth is maintained here; + * when the outermost block closes any deferred notifications are flushed after + * it. "guard" is always one of the fixed strings from cmdq_guard, never user + * text. + */ +void +control_write_guard(struct client *c, const char *guard, long t, u_int number, + int flags) +{ + struct control_state *cs = c->control_state; + char *line; + + if (strcmp(guard, "begin") == 0) + cs->guard_depth++; + + xasprintf(&line, "%%%s %ld %u %d", guard, t, number, flags); + control_write_line(c, line); + + if (strcmp(guard, "begin") != 0 && cs->guard_depth > 0 && + --cs->guard_depth == 0) + control_flush_deferred(c); +} + +/* + * Write a notification line. Notifications must never appear inside a command's + * %begin/%end guard block, so if one is open the line is deferred until it + * closes. + */ +void +control_notify_write(struct client *c, const char *fmt, ...) +{ + struct control_state *cs = c->control_state; + struct control_line *cl; + va_list ap; + char *line; + + va_start(ap, fmt); + xvasprintf(&line, fmt, ap); + va_end(ap); + + if (cs->guard_depth == 0) { + control_write_line(c, line); + return; + } + + log_debug("%s: %s: deferring notification: %s", __func__, c->name, + line); + cl = xcalloc(1, sizeof *cl); + cl->line = line; + TAILQ_INSERT_TAIL(&cs->deferred, cl, entry); } /* Check age for this pane. */ @@ -438,7 +524,7 @@ control_check_age(struct client *c, struct window_pane *wp, return (0); cp->flags |= CONTROL_PANE_PAUSED; control_discard_pane(c, cp); - control_write(c, "%%pause %%%u", wp->id); + control_notify_write(c, "%%pause %%%u", wp->id); } else { if (age < CONTROL_MAXIMUM_AGE) return (0); @@ -805,14 +891,17 @@ control_sub_change(struct monitor_change *change, __unused void *data) if (wp != NULL) { w = wp->window; - control_write(c, "%%subscription-changed %s $%u @%u %u %%%u : %s", + control_notify_write(c, + "%%subscription-changed %s $%u @%u %u %%%u : %s", change->name, s->id, w->id, wl->idx, wp->id, change->value); } else if (wl != NULL) { w = wl->window; - control_write(c, "%%subscription-changed %s $%u @%u %u - : %s", + control_notify_write(c, + "%%subscription-changed %s $%u @%u %u - : %s", change->name, s->id, w->id, wl->idx, change->value); } else { - control_write(c, "%%subscription-changed %s $%u - - - : %s", + control_notify_write(c, + "%%subscription-changed %s $%u - - - : %s", change->name, s->id, change->value); } } @@ -835,6 +924,7 @@ control_start(struct client *c) RB_INIT(&cs->windows); TAILQ_INIT(&cs->pending_list); TAILQ_INIT(&cs->all_blocks); + TAILQ_INIT(&cs->deferred); cs->subs = monitor_create_client(c, control_sub_change, NULL); cs->read_event = bufferevent_new(c->fd, control_read_callback, @@ -885,12 +975,19 @@ control_stop(struct client *c) struct control_state *cs = c->control_state; struct control_block *cb, *cb1; struct control_window *cw, *cw1; + struct control_line *cl, *cl1; if (cs == NULL) return; monitor_destroy(cs->subs); + TAILQ_FOREACH_SAFE(cl, &cs->deferred, entry, cl1) { + TAILQ_REMOVE(&cs->deferred, cl, entry); + free(cl->line); + free(cl); + } + if (~c->flags & CLIENT_CONTROLCONTROL) bufferevent_free(cs->write_event); bufferevent_free(cs->read_event); diff --git a/tmux.h b/tmux.h index f880f4ae2..d732b55e8 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1419 2026/08/03 12:58:53 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1420 2026/08/03 13:38:42 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3985,6 +3985,8 @@ struct window_pane_offset *control_pane_offset(struct client *, struct window_pane *, int *); void control_reset_offsets(struct client *); void printflike(2, 3) control_write(struct client *, const char *, ...); +void printflike(2, 3) control_notify_write(struct client *, const char *, ...); +void control_write_guard(struct client *, const char *, long, u_int, int); void control_write_output(struct client *, struct window_pane *); int control_all_done(struct client *); void control_add_sub(struct client *, const char *, enum monitor_type, int, From cd3f577c8520035ab4b51dd62d3b033a253a3911 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 3 Aug 2026 20:27:10 +0100 Subject: [PATCH 12/29] Add test for control mode events and guards. --- regress/control-notify-guard.sh | 158 ++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100755 regress/control-notify-guard.sh diff --git a/regress/control-notify-guard.sh b/regress/control-notify-guard.sh new file mode 100755 index 000000000..459ec9f35 --- /dev/null +++ b/regress/control-notify-guard.sh @@ -0,0 +1,158 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +TMPDIR=$(mktemp -d) +IN="$TMPDIR/in" +OUT="$TMPDIR/out" +BADCFG="$TMPDIR/bad.conf" +PID= + +cleanup() +{ + exec 3>&- 2>/dev/null + [ -n "$PID" ] && kill "$PID" 2>/dev/null + $TMUX kill-server 2>/dev/null + rm -rf "$TMPDIR" +} +trap cleanup EXIT + +fail() +{ + echo "$1" >&2 + [ -s "$OUT" ] && cat "$OUT" >&2 + exit 1 +} + +wait_for() +{ + pattern=$1 + timeout=${2:-60} + i=0 + + while [ "$i" -lt "$timeout" ]; do + grep -F -- "$pattern" "$OUT" >/dev/null 2>&1 && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "missing: $pattern" +} + +wait_for_count() +{ + pattern=$1 + expected=$2 + timeout=${3:-60} + i=0 + + while [ "$i" -lt "$timeout" ]; do + count=$(grep -F -c -- "$pattern" "$OUT" 2>/dev/null) + [ "$count" -ge "$expected" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "missing count $expected for: $pattern" +} + +send() +{ + printf '%s\n' "$1" >&3 || fail "failed to send: $1" +} + +# A notification is a standalone protocol message. It must never be part of +# the response between the real %begin and matching %end or %error for a +# command. Ignore guard-looking command output while a real guard is open. +check_guards() +{ + awk ' + function notification(line) { + return line ~ /^%(sessions-changed|session-changed |unlinked-window-add )/ || + line ~ /^%(pause |continue |config-error )/ + } + !open && $0 ~ /^%begin [0-9]+ [0-9]+ [0-9]+$/ { + open = 1 + time = $2 + number = $3 + flags = $4 + next + } + open && ($0 == "%end " time " " number " " flags || + $0 == "%error " time " " number " " flags) { + open = 0 + next + } + notification($0) { + seen[$1]++ + if (open) { + print "notification inside guard: " $0 > "/dev/stderr" + bad = 1 + } + } + END { + if (open) { + print "unterminated command guard" > "/dev/stderr" + bad = 1 + } + if (seen["%sessions-changed"] < 2 || + seen["%session-changed"] < 1 || + seen["%unlinked-window-add"] < 2 || + seen["%pause"] < 1 || seen["%continue"] < 1 || + seen["%config-error"] < 1) { + print "missing expected notification" > "/dev/stderr" + bad = 1 + } + exit bad + }' "$OUT" || fail "bad control protocol notification ordering" +} + +$TMUX kill-server 2>/dev/null +$TMUX new-session -d -s guard -x 80 -y 24 || exit 1 +pane=$($TMUX display-message -p -t guard:0.0 '#{pane_id}') || exit 1 + +mkfifo "$IN" || exit 1 +: >"$OUT" +$TMUX -C attach-session -t guard <"$IN" >"$OUT" 2>&1 & +PID=$! +exec 3>"$IN" + +# Attaching generates a session notification synchronously from inside the +# attach command. It must follow the closing guard. +wait_for '%session-changed ' + +# Guard-looking output is arbitrary command output, not protocol state. Make +# new-session both generate notifications and print an unmatched fake %begin; +# it must not leave those notifications permanently deferred. +send "set-option -g @fake-begin '%begin 1 2 3'" +send "new-session -d -P -F '#{@fake-begin}' -s fake-begin" +wait_for_count '%sessions-changed' 1 + +# Likewise, fake %end output from a command that generates multiple +# notifications must not flush them before the command actually ends. +send "set-option -g @fake-end '%end 1 2 3'" +send "new-session -d -P -F '#{@fake-end}' -s fake-end" +wait_for_count '%sessions-changed' 2 + +# Exercise notification paths outside control-notify.c. +send "refresh-client -A '$pane:pause'" +wait_for "%pause $pane" +send "refresh-client -A '$pane:continue'" +wait_for "%continue $pane" + +printf '%s\n' 'not-a-command' >"$BADCFG" +send "source-file '$BADCFG'" +wait_for '%config-error ' + +# A final command guarantees all earlier command guards have completed before +# the transcript is checked. +send "display-message -p 'guard-check-done'" +wait_for 'guard-check-done' +check_guards + +exit 0 From ca0d77855fe6aa2429054c693f25349d6face95b Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 3 Aug 2026 21:18:40 +0100 Subject: [PATCH 13/29] Client exit test from Ben Maurer. --- regress/control-client-exit-stalled.sh | 77 ++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100755 regress/control-client-exit-stalled.sh diff --git a/regress/control-client-exit-stalled.sh b/regress/control-client-exit-stalled.sh new file mode 100755 index 000000000..431fd66d8 --- /dev/null +++ b/regress/control-client-exit-stalled.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +# A control client whose terminal has stopped accepting output (for example +# its ssh connection or terminal emulator died) never drains the output that +# server_client_check_exit waits on before sending MSG_EXIT, and may then +# never close its socket either. This must not prevent the server from +# exiting: after kill-server the server previously stayed alive forever with +# server_exit set, closing every new connection immediately, so every new +# client failed with "server exited unexpectedly" until the server was killed +# by hand. The server now bounds the exit handshake: it discards output it +# can never deliver and drops the client if it still does not close. +# +# The stalled terminal is simulated by a fifo whose read end is held open but +# never read. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest" +$TMUX kill-server 2>/dev/null + +DIR=$(mktemp -d) +FIFO=$DIR/fifo +SERVER= + +mkfifo "$FIFO" || exit 1 + +cleanup() { + [ -n "$SERVER" ] && kill -9 "$SERVER" 2>/dev/null + $TMUX kill-server 2>/dev/null + exec 8<&- 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +# A detached session whose pane floods printable output forever. +$TMUX -f/dev/null new -d -x 80 -y 24 -s rt 'cat /dev/zero | tr "\000" x' || exit 1 +SERVER=$($TMUX display -pt rt '#{pid}') + +# Attach a control client with output down the fifo; stdin held open by sleep +# so it stays attached. The fifo fills and is never read, so a backlog forms +# in the server that can never be delivered. +( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 & +exec 8<"$FIFO" + +n=0 +while [ $n -lt 50 ]; do + $TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . && break + sleep 0.1 + n=$((n + 1)) +done +$TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . || + { echo "control client did not attach"; exit 1; } + +# Let the pane flood fill the fifo so the client's output is stuck. +sleep 2 + +# Ask the server to exit. This sets CLIENT_EXIT on the stalled client, whose +# output can never drain. +$TMUX kill-server 2>/dev/null + +# The server must exit within the exit timeout (10 seconds) plus slack, +# rather than staying in limbo forever rejecting new clients. +n=0 +while [ $n -lt 100 ]; do + if ! kill -0 "$SERVER" 2>/dev/null; then + SERVER= + exit 0 + fi + sleep 0.2 + n=$((n + 1)) +done + +echo "server did not exit after kill-server; new clients see:" +$TMUX ls 2>&1 +exit 1 From f07a76d959c3bd49410ea9d69c29e45338554941 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 3 Aug 2026 21:30:44 +0100 Subject: [PATCH 14/29] Use move-pane for a test. --- regress/pane-ops.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/regress/pane-ops.sh b/regress/pane-ops.sh index 29c79e2e2..41a6a2a45 100644 --- a/regress/pane-ops.sh +++ b/regress/pane-ops.sh @@ -192,13 +192,13 @@ check_fail "invalid window name: $(printf 'a\377b')" \ break-pane -d -n "$(printf 'a\377b')" -s "$p1" -t P: # join-pane can move a pane from one window to another without destroying -# the source window if other panes remain. (On this branch move-pane is -# reserved for floating panes, covered by floating-pane-geometry.sh.) +# the source window if other panes remain. move-pane does the same when no +# floating-pane movement flags are given. check_ok new-window -d -t P:5 -n other check_ok join-pane -d -s "$p1" -t P:5.0 check_fmt 'P:5' '#{window_panes}' '2' check_fmt 'P:0' '#{window_panes}' '3' -check_ok join-pane -d -v -s "$p1" -t "$p2" +check_ok move-pane -d -v -s "$p1" -t "$p2" check_fmt 'P:0' '#{window_panes}' '4' check_fmt 'P:5' '#{window_panes}' '1' From 114aa6808218c68e69dcd0730d67567fe44681ef Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 3 Aug 2026 20:18:20 +0000 Subject: [PATCH 15/29] Do not let a stuck client prevent the server from exiting - give up after 10 seconds. GitHub issue 5444 from Ben Maurer. --- control.c | 19 ++++++++++++++++++- server-client.c | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- tmux.h | 5 ++++- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/control.c b/control.c index d3d078dd0..036a25882 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.63 2026/08/03 13:38:42 nicm Exp $ */ +/* $OpenBSD: control.c,v 1.64 2026/08/03 20:18:20 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -968,6 +968,23 @@ control_discard(struct client *c) bufferevent_disable(cs->read_event, EV_READ); } +/* + * Discard all output for a client, including output which has already been + * queued to be written. + */ +void +control_discard_all(struct client *c) +{ + struct control_state *cs = c->control_state; + struct control_block *cb, *cb1; + struct evbuffer *evb = cs->write_event->output; + + control_discard(c); + TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1) + control_free_block(cs, cb); + evbuffer_drain(evb, EVBUFFER_LENGTH(evb)); +} + /* Stop control mode. */ void control_stop(struct client *c) diff --git a/server-client.c b/server-client.c index d389dacef..a7270e88f 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.500 2026/07/28 13:17:45 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.501 2026/08/03 20:18:20 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -42,6 +42,7 @@ static key_code server_client_check_mouse(struct client *, struct key_event *); static void server_client_repeat_timer(int, short, void *); static void server_client_click_timer(int, short, void *); static void server_client_check_exit(struct client *); +static void server_client_exit_timer(int, short, void *); static void server_client_check_redraw(struct client *); static void server_client_check_modes(struct client *); static void server_client_set_title(struct client *); @@ -314,6 +315,7 @@ server_client_create(int fd) evtimer_set(&c->repeat_timer, server_client_repeat_timer, c); evtimer_set(&c->click_timer, server_client_click_timer, c); + evtimer_set(&c->exit_timer, server_client_exit_timer, c); c->click_wp = -1; @@ -530,6 +532,7 @@ server_client_lost(struct client *c) evtimer_del(&c->repeat_timer); evtimer_del(&c->click_timer); + evtimer_del(&c->exit_timer); if (event_initialized(&c->cycle_timer)) evtimer_del(&c->cycle_timer); @@ -2290,6 +2293,39 @@ server_client_click_timer(__unused int fd, __unused short events, void *data) c->flags &= ~(CLIENT_DOUBLECLICK|CLIENT_TRIPLECLICK); } +/* Start client exit timer. */ +static void +server_client_start_exit_timer(struct client *c) +{ + struct timeval tv = { .tv_sec = 10 }; + + if (!evtimer_pending(&c->exit_timer, NULL)) + evtimer_add(&c->exit_timer, &tv); +} + +/* Exit timer has expired: stop waiting for the client. */ +static void +server_client_exit_timer(__unused int fd, __unused short events, void *data) +{ + struct client *c = data; + struct client_file *cf; + + if (c->flags & (CLIENT_DEAD|CLIENT_SUSPENDED)) + return; + + if (c->flags & CLIENT_EXITED) { + log_debug("%s: %s took too long to exit", __func__, c->name); + server_client_lost(c); + } else if (c->flags & CLIENT_EXIT) { + log_debug("%s: %s took too long to flush", __func__, c->name); + if (c->flags & CLIENT_CONTROL) + control_discard_all(c); + RB_FOREACH(cf, client_files, &c->files) + evbuffer_drain(cf->buffer, EVBUFFER_LENGTH(cf->buffer)); + server_client_check_exit(c); + } +} + /* Check if client should be exited. */ static void server_client_check_exit(struct client *c) @@ -2306,15 +2342,22 @@ server_client_check_exit(struct client *c) if (c->flags & CLIENT_CONTROL) { control_discard(c); - if (!control_all_done(c)) + if (!control_all_done(c)) { + server_client_start_exit_timer(c); return; + } } RB_FOREACH(cf, client_files, &c->files) { - if (EVBUFFER_LENGTH(cf->buffer) != 0) + if (EVBUFFER_LENGTH(cf->buffer) != 0) { + server_client_start_exit_timer(c); return; + } } c->flags |= CLIENT_EXITED; + evtimer_del(&c->exit_timer); + server_client_start_exit_timer(c); + switch (c->exit_type) { case CLIENT_EXIT_RETURN: if (c->exit_message != NULL) diff --git a/tmux.h b/tmux.h index d732b55e8..83fa06381 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1420 2026/08/03 13:38:42 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1421 2026/08/03 20:18:20 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2202,6 +2202,8 @@ struct client { struct event click_timer; int click_loc; int click_wp; + + struct event exit_timer; u_int click_button; struct mouse_event click_event; @@ -3970,6 +3972,7 @@ time_t monitor_get_fire_time(struct monitor_set *, const char *); /* control.c */ void control_discard(struct client *); +void control_discard_all(struct client *); void control_start(struct client *); void control_ready(struct client *); void control_stop(struct client *); From d2322d0114c1d3313aa84151804a399e1c0b6ddf Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 3 Aug 2026 20:29:52 +0000 Subject: [PATCH 16/29] Check for floating only for flags that require a floating pane, reported by Ilya Grigoriev. --- cmd-join-pane.c | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/cmd-join-pane.c b/cmd-join-pane.c index fd4355daa..8790db297 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-join-pane.c,v 1.73 2026/07/17 15:24:30 nicm Exp $ */ +/* $OpenBSD: cmd-join-pane.c,v 1.74 2026/08/03 20:29:52 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -432,25 +432,23 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) if (cmd_get_entry(self) == &cmd_move_pane_entry) { if (args_has(args, 'M')) return (cmd_join_pane_mouse_update(item)); - if (!window_pane_is_floating(dst_wp)) { - cmdq_error(item, "pane is not floating"); - return (CMD_RETURN_ERROR); - } - if ((s = args_get(args, 'P')) != NULL) { - server_unzoom_window(dst_w); - return (cmd_join_pane_place(item, dst_wl, dst_wp, s)); - } - if ((s = args_get(args, 'z')) != NULL) { - server_unzoom_window(dst_w); - return (cmd_join_pane_zindex(item, dst_wl, dst_wp, s)); - } - if (args_has(args, 'X') || + if (args_has(args, 'P') || + args_has(args, 'z') || + args_has(args, 'X') || args_has(args, 'Y') || args_has(args, 'U') || args_has(args, 'D') || args_has(args, 'L') || args_has(args, 'R')) { + if (!window_pane_is_floating(dst_wp)) { + cmdq_error(item, "pane is not floating"); + return (CMD_RETURN_ERROR); + } server_unzoom_window(dst_w); + if ((s = args_get(args, 'P')) != NULL) + return (cmd_join_pane_place(item, dst_wl, dst_wp, s)); + if ((s = args_get(args, 'z')) != NULL) + return (cmd_join_pane_zindex(item, dst_wl, dst_wp, s)); return (cmd_join_pane_move(item, args, dst_wl, dst_wp)); } } From 5cd1976a4a5bbefc9391d2e3febc4ca4d74ebede Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 4 Aug 2026 11:18:22 +0000 Subject: [PATCH 17/29] Do not attempt to drain write buffer on stuck clients, since it requires a weird dance to make libevent do it. Instead, just ignore the client and destroy the buffer normally if not drained in 10 seconds. --- control.c | 10 +++------- server-client.c | 41 +++++++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/control.c b/control.c index 036a25882..7f76ff9e3 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.64 2026/08/03 20:18:20 nicm Exp $ */ +/* $OpenBSD: control.c,v 1.65 2026/08/04 11:18:22 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -968,21 +968,17 @@ control_discard(struct client *c) bufferevent_disable(cs->read_event, EV_READ); } -/* - * Discard all output for a client, including output which has already been - * queued to be written. - */ +/* Discard all tmux-owned queued control blocks and stop writing. */ void control_discard_all(struct client *c) { struct control_state *cs = c->control_state; struct control_block *cb, *cb1; - struct evbuffer *evb = cs->write_event->output; control_discard(c); TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1) control_free_block(cs, cb); - evbuffer_drain(evb, EVBUFFER_LENGTH(evb)); + bufferevent_disable(cs->write_event, EV_WRITE); } /* Stop control mode. */ diff --git a/server-client.c b/server-client.c index a7270e88f..27b7688fe 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.501 2026/08/03 20:18:20 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.502 2026/08/04 11:18:22 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -41,7 +41,7 @@ static void server_client_check_window_resize(struct window *); static key_code server_client_check_mouse(struct client *, struct key_event *); static void server_client_repeat_timer(int, short, void *); static void server_client_click_timer(int, short, void *); -static void server_client_check_exit(struct client *); +static void server_client_check_exit(struct client *, int); static void server_client_exit_timer(int, short, void *); static void server_client_check_redraw(struct client *); static void server_client_check_modes(struct client *); @@ -1857,7 +1857,7 @@ server_client_loop(void) /* Check clients. */ TAILQ_FOREACH(c, &clients, entry) { - server_client_check_exit(c); + server_client_check_exit(c, 0); if (c->session != NULL && c->session->curw != NULL) { server_client_check_modes(c); server_client_check_redraw(c); @@ -2307,8 +2307,7 @@ server_client_start_exit_timer(struct client *c) static void server_client_exit_timer(__unused int fd, __unused short events, void *data) { - struct client *c = data; - struct client_file *cf; + struct client *c = data; if (c->flags & (CLIENT_DEAD|CLIENT_SUSPENDED)) return; @@ -2318,17 +2317,13 @@ server_client_exit_timer(__unused int fd, __unused short events, void *data) server_client_lost(c); } else if (c->flags & CLIENT_EXIT) { log_debug("%s: %s took too long to flush", __func__, c->name); - if (c->flags & CLIENT_CONTROL) - control_discard_all(c); - RB_FOREACH(cf, client_files, &c->files) - evbuffer_drain(cf->buffer, EVBUFFER_LENGTH(cf->buffer)); - server_client_check_exit(c); + server_client_check_exit(c, 1); } } -/* Check if client should be exited. */ +/* Check if client should be exited, abandoning buffered output if forced. */ static void -server_client_check_exit(struct client *c) +server_client_check_exit(struct client *c, int force) { struct client_file *cf; const char *name = c->exit_session; @@ -2341,16 +2336,22 @@ server_client_check_exit(struct client *c) return; if (c->flags & CLIENT_CONTROL) { - control_discard(c); - if (!control_all_done(c)) { - server_client_start_exit_timer(c); - return; + if (force) + control_discard_all(c); + else { + control_discard(c); + if (!control_all_done(c)) { + server_client_start_exit_timer(c); + return; + } } } - RB_FOREACH(cf, client_files, &c->files) { - if (EVBUFFER_LENGTH(cf->buffer) != 0) { - server_client_start_exit_timer(c); - return; + if (!force) { + RB_FOREACH(cf, client_files, &c->files) { + if (EVBUFFER_LENGTH(cf->buffer) != 0) { + server_client_start_exit_timer(c); + return; + } } } c->flags |= CLIENT_EXITED; From 3894397b07fd8473a7bb315b1aaa7605bd6d4447 Mon Sep 17 00:00:00 2001 From: claudio Date: Tue, 4 Aug 2026 13:16:03 +0000 Subject: [PATCH 18/29] Switch from imsg_get to imsgbuf_get and switch type of n to int. OK tb@ --- proc.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proc.c b/proc.c index 194371236..34dfd25bc 100644 --- a/proc.c +++ b/proc.c @@ -1,4 +1,4 @@ -/* $OpenBSD: proc.c,v 1.31 2026/06/08 21:38:19 nicm Exp $ */ +/* $OpenBSD: proc.c,v 1.32 2026/08/04 13:16:03 claudio Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott @@ -74,7 +74,7 @@ static void proc_event_cb(__unused int fd, short events, void *arg) { struct tmuxpeer *peer = arg; - ssize_t n; + int n; struct imsg imsg; if (!(peer->flags & PEER_BAD) && (events & EV_READ)) { @@ -83,7 +83,7 @@ proc_event_cb(__unused int fd, short events, void *arg) return; } for (;;) { - if ((n = imsg_get(&peer->ibuf, &imsg)) == -1) { + if ((n = imsgbuf_get(&peer->ibuf, &imsg)) == -1) { peer->dispatchcb(NULL, peer->arg); return; } From 72e705cbd6c3d92b48c56fa2f687a12b838a6b66 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 5 Aug 2026 08:26:10 +0100 Subject: [PATCH 19/29] Tweak times for macOS. --- regress/pipe-pane.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/regress/pipe-pane.sh b/regress/pipe-pane.sh index ee302ffbb..310861a5e 100644 --- a/regress/pipe-pane.sh +++ b/regress/pipe-pane.sh @@ -35,9 +35,9 @@ check_alive() # A pipe-pane -I child may write after the pane process has exited. With # remain-on-exit, the pane stays around but its bufferevent has been freed. -check_ok new-session -d -s pipe -x 80 -y 24 'sleep 0.2' +check_ok new-session -d -s pipe -x 80 -y 24 'sleep 1' check_ok set-option -t pipe:0 remain-on-exit on -check_ok pipe-pane -t pipe:0.0 -I 'sleep 0.6; printf x' +check_ok pipe-pane -t pipe:0.0 -I 'sleep 2; printf x' i=0 while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do @@ -46,7 +46,7 @@ while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do sleep 0.1 done -sleep 0.7 +sleep 2 check_alive $TMUX kill-server 2>/dev/null From 0198ee9e51bd47c7947fde3d9f5ebdc13ae82030 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 5 Aug 2026 07:27:45 +0000 Subject: [PATCH 20/29] Do not log when logging is off, from Michael Grant. --- format.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/format.c b/format.c index 40c495ff7..c6b734d56 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.410 2026/07/29 20:43:20 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.411 2026/08/05 07:27:45 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -4182,6 +4182,8 @@ format_log_debug_cb(const char *key, const char *value, void *arg) void format_log_debug(struct format_tree *ft, const char *prefix) { + if (log_get_level() == 0) + return; format_each(ft, format_log_debug_cb, (void *)prefix); } From fe384e06bc9fa83c589c0294ab3f9489fe848c99 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 5 Aug 2026 07:31:08 +0000 Subject: [PATCH 21/29] Add pane_private_modes format with list of DEC private modes, from George Nachman. --- format.c | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- tmux.1 | 5 +++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/format.c b/format.c index c6b734d56..0eb5b394a 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.411 2026/08/05 07:27:45 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.412 2026/08/05 07:31:08 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -2087,6 +2087,60 @@ format_cb_synchronized_output_flag(struct format_tree *ft) return (NULL); } +/* Callback for pane_private_modes. */ +static void * +format_cb_pane_private_modes(struct format_tree *ft) +{ + static const struct { + int mode; + int number; + } table[] = { + { MODE_KCURSOR, 1 }, /* DECCKM */ + { MODE_ORIGIN, 6 }, /* DECOM */ + { MODE_WRAP, 7 }, /* DECAWM */ + { MODE_CURSOR_BLINKING, 12 }, /* cursor blinking */ + { MODE_CURSOR, 25 }, /* DECTCEM */ + { MODE_MOUSE_STANDARD, 1000 }, /* mouse normal tracking */ + { MODE_MOUSE_BUTTON, 1002 }, /* mouse button tracking */ + { MODE_MOUSE_ALL, 1003 }, /* mouse any tracking */ + { MODE_FOCUSON, 1004 }, /* focus reporting */ + { MODE_MOUSE_UTF8, 1005 }, /* mouse: UTF-8 */ + { MODE_MOUSE_SGR, 1006 }, /* mouse: SGR */ + { MODE_BRACKETPASTE, 2004 }, /* bracketed paste */ + { MODE_SYNC, 2026 }, /* synchronized output */ + { MODE_THEME_UPDATES, 2031 }, /* theme update notifications */ + }; + int mode; + char *value = NULL, *tmp; + u_int i; + + if (ft->wp == NULL) + return (NULL); + mode = ft->wp->base.mode; + + for (i = 0; i < nitems(table); i++) { + if (~mode & table[i].mode) + continue; + /* + * Only report cursor blinking when set by the application, not + * when it comes from the cursor-style option. + */ + if (table[i].mode == MODE_CURSOR_BLINKING && + (~mode & MODE_CURSOR_BLINKING_SET)) + continue; + if (value == NULL) + xasprintf(&value, "%d", table[i].number); + else { + xasprintf(&tmp, "%s,%d", value, table[i].number); + free(value); + value = tmp; + } + } + if (value == NULL) + return (xstrdup("")); + return (value); +} + /* Callback for pane_active. */ static void * format_cb_pane_active(struct format_tree *ft) @@ -3795,6 +3849,9 @@ static const struct format_table_entry format_table[] = { { "pane_pipe_pid", FORMAT_TABLE_STRING, format_cb_pane_pipe_pid }, + { "pane_private_modes", FORMAT_TABLE_STRING, + format_cb_pane_private_modes + }, { "pane_right", FORMAT_TABLE_STRING, format_cb_pane_right }, diff --git a/tmux.1 b/tmux.1 index cb5e1aee7..c689ea553 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1152 2026/07/29 08:58:33 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1153 2026/08/05 07:31: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: July 29 2026 $ +.Dd $Mdocdate: August 5 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -7395,6 +7395,7 @@ The following variables are available, where appropriate: .It Li "pane_pid" Ta "" Ta "PID of first process in pane" .It Li "pane_pipe" Ta "" Ta "1 if pane is being piped" .It Li "pane_pipe_pid" Ta "" Ta "PID of pipe process, if any" +.It Li "pane_private_modes" Ta "" Ta "List of enabled DECSET private modes" .It Li "pane_pb_state" Ta "" Ta "Pane progress bar state, one of hidden, normal, error, indeterminate, paused (can be set by application)" .It Li "pane_pb_progress" Ta "" Ta "Pane progress bar progress percentage (can be set by application)" .It Li "pane_right" Ta "" Ta "Right of pane" From 1995b5559d749bd804ac938500814e2c0e049bfd Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 5 Aug 2026 07:35:35 +0000 Subject: [PATCH 22/29] Do not crash when synchronizing groups and there is no current window. GitHub issue 5467 from Ju-an Zhang. --- session.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/session.c b/session.c index f51df3bbd..c0ea1306a 100644 --- a/session.c +++ b/session.c @@ -1,4 +1,4 @@ -/* $OpenBSD: session.c,v 1.106 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: session.c,v 1.107 2026/08/05 07:35:35 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -711,8 +711,10 @@ session_group_synchronize1(struct session *target, struct session *s) /* Fix up the current window. */ if (s->curw != NULL) s->curw = winlink_find_by_index(&s->windows, s->curw->idx); - else + else if (target->curw != NULL) s->curw = winlink_find_by_index(&s->windows, target->curw->idx); + if (s->curw == NULL) + s->curw = RB_MIN(winlinks, &s->windows); /* Fix up the last window stack. */ memcpy(&old_lastw, &s->lastw, sizeof old_lastw); From e56a04215313b42326e91971cb83759d410d61a2 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 5 Aug 2026 07:50:21 +0000 Subject: [PATCH 23/29] Use ACS arrows for tree mode. --- mode-tree.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mode-tree.c b/mode-tree.c index defd587bb..5132eb94c 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: mode-tree.c,v 1.100 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: mode-tree.c,v 1.101 2026/08/05 07:50:21 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -47,7 +47,7 @@ enum mode_tree_preview { "#[acs]x" MODE_TREE_PREFIX_STYLE " }," \ "#{mode_tree_repeat}}" \ "#{?mode_tree_branch," \ - "#[acs]#{?mode_tree_last,mq,tq}" MODE_TREE_PREFIX_STYLE "> ,}" \ + "#[acs]#{?mode_tree_last,mq,tq}+" MODE_TREE_PREFIX_STYLE " ,}" \ "#{?mode_tree_has_children," \ "#{?mode_tree_expanded,#[fg=themered]-" MODE_TREE_PREFIX_STYLE " ," \ "#[fg=themegreen]+" MODE_TREE_PREFIX_STYLE " }," \ From bd483a63cfa6714f765ef396bdcb35a0729149b0 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 5 Aug 2026 08:54:56 +0000 Subject: [PATCH 24/29] Ignore Ms if it is invalid (for example no %p1) and report in client mode, GitHub issue 5460. --- tmux.h | 3 ++- tty-term.c | 23 ++++++++++++++++++++++- window-client.c | 16 +++++++++++----- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/tmux.h b/tmux.h index 83fa06381..15b997b76 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1421 2026/08/03 20:18:20 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1422 2026/08/05 08:54:56 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1697,6 +1697,7 @@ struct tty_term { #define TERM_RGBCOLOURS 0x10 #define TERM_VT100LIKE 0x20 #define TERM_SIXEL 0x40 +#define TERM_INVALIDMS 0x80 int flags; LIST_ENTRY(tty_term) entry; diff --git a/tty-term.c b/tty-term.c index c8ceb2e56..7a4b89ad7 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-term.c,v 1.106 2026/06/13 09:17:29 nicm Exp $ */ +/* $OpenBSD: tty-term.c,v 1.107 2026/08/05 08:54:56 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -28,6 +28,7 @@ #include "tmux.h" static char *tty_term_strip(const char *); +static void tty_term_validate(struct tty_term *); struct tty_terms tty_terms = LIST_HEAD_INITIALIZER(tty_terms); @@ -516,6 +517,26 @@ tty_term_apply_overrides(struct tty_term *term) acs = "a#j+k+l+m+n+o-p-q-r-s-t+u+v+w+x|y~."; for (; acs[0] != '\0' && acs[1] != '\0'; acs += 2) term->acs[(u_char) acs[0]][0] = acs[1]; + + tty_term_validate(term); +} + +static void +tty_term_validate(struct tty_term *term) +{ + struct tty_code *code = &term->codes[TTYC_MS]; + + if (code->type != TTYCODE_STRING) + return; + if (*tty_term_string_ss(term, TTYC_MS, "c", "?") != '\0') { + term->flags &= ~TERM_INVALIDMS; + return; + } + + log_debug("removing invalid Ms capability"); + term->flags |= TERM_INVALIDMS; + free(code->value.string); + code->type = TTYCODE_NONE; } struct tty_term * diff --git a/window-client.c b/window-client.c index b94c22119..f42aa5b7a 100644 --- a/window-client.c +++ b/window-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-client.c,v 1.47 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window-client.c,v 1.48 2026/08/05 08:54:56 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -113,14 +113,16 @@ static const char *window_client_info_lines[] = { "#{?#{I/c:kmous},,#[align=right]unavailable: [kmous] missing}", "#[fg=themelightgrey]set-clipboard #[#{E:tree-mode-border-style},acs]x#[default] " - "#{?#{!=:#{set-clipboard},off},#{?#{I/f:clipboard},," + "#{?#{!=:#{set-clipboard},off},#{?#{I/c:Ms},," "#[fg=themered]}#{set-clipboard},#[fg=themelightgrey]off} " - "#{?#{I/f:clipboard},,#[align=right]unavailable: [Ms] missing}", + "#{?#{I/c:Ms},,#[align=right]unavailable: [Ms] " + "#{?clipboard_invalid,invalid,missing}}", "#[fg=themelightgrey]get-clipboard #[#{E:tree-mode-border-style},acs]x#[default] " - "#{?#{!=:#{get-clipboard},off},#{?#{I/f:clipboard},," + "#{?#{!=:#{get-clipboard},off},#{?#{I/c:Ms},," "#[fg=themered]}#{get-clipboard},#[fg=themelightgrey]off} " - "#{?#{I/f:clipboard},,#[align=right]unavailable: [Ms] missing}", + "#{?#{I/c:Ms},,#[align=right]unavailable: [Ms] " + "#{?clipboard_invalid,invalid,missing}}", "#[fg=themelightgrey]focus-events #[#{E:tree-mode-border-style},acs]x#[default] " "#{?focus-events,#{?#{I/f:focus},,#[fg=themered]}on,#[fg=themelightgrey]off} " @@ -273,6 +275,10 @@ window_client_draw_info(__unused void *modedata, void *itemdata, char *expanded; ft = format_create_defaults(NULL, c, NULL, NULL, NULL); + if (c->tty.term->flags & TERM_INVALIDMS) + format_add(ft, "clipboard_invalid", "1"); + else + format_add(ft, "clipboard_invalid", "0"); screen_write_cursormove(ctx, cx, cy, 0); for (i = 0; i < nitems(window_client_info_lines); i++) { From 4a9dbac43573fe4ba7e9798b159eb718ff8df2d7 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 5 Aug 2026 12:23:25 +0000 Subject: [PATCH 25/29] Add a refresh-now command and change the r binding back to it in copy mode. --- key-bindings.c | 6 +++--- tmux.1 | 8 ++++++-- window-copy.c | 48 ++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/key-bindings.c b/key-bindings.c index 55f12bc83..2f44cb218 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.190 2026/07/22 19:23:59 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.191 2026/08/05 12:23:25 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -595,7 +595,7 @@ key_bindings_init(void) "bind -Tcopy-mode g { command-prompt -P -p'(goto line)' { send -X goto-line -- '%%' } }", "bind -Tcopy-mode n { send -X search-again }", "bind -Tcopy-mode q { send -X cancel }", - "bind -Tcopy-mode r { send -X refresh-toggle }", + "bind -Tcopy-mode r { send -X refresh-now }", "bind -Tcopy-mode t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }", "bind -Tcopy-mode Home { send -X start-of-line }", "bind -Tcopy-mode End { send -X end-of-line }", @@ -705,7 +705,7 @@ key_bindings_init(void) "bind -Tcopy-mode-vi n { send -X search-again }", "bind -Tcopy-mode-vi o { send -X other-end }", "bind -Tcopy-mode-vi q { send -X cancel }", - "bind -Tcopy-mode-vi r { send -X refresh-toggle }", + "bind -Tcopy-mode-vi r { send -X refresh-now }", "bind -Tcopy-mode-vi t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }", "bind -Tcopy-mode-vi v { send -X rectangle-toggle }", "bind -Tcopy-mode-vi w { send -X next-word }", diff --git a/tmux.1 b/tmux.1 index c689ea553..c72bf35ef 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1153 2026/08/05 07:31:08 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1154 2026/08/05 12:23:25 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -2317,10 +2317,14 @@ the bottom and is paused while a selection is in progress. .Xc Turn off automatic refresh of the content from the pane. .It Xo -.Ic refresh\-toggle +.Ic refresh\-now (vi: r) (emacs: r) .Xc +Refresh the content from the pane once. +.It Xo +.Ic refresh\-toggle +.Xc Toggle automatic refresh of the content from the pane. .It Xo .Ic scroll\-bottom diff --git a/window-copy.c b/window-copy.c index dabb2a99c..716a0f037 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.424 2026/07/29 17:42:56 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.425 2026/08/05 12:23:25 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -54,6 +54,7 @@ static void window_copy_redraw_screen(struct window_mode_entry *); static void window_copy_do_refresh(struct window_mode_entry *, int); static void window_copy_refresh_timer(int, short, void *); static void window_copy_refresh_arm(struct window_mode_entry *); +static int window_copy_refresh_allowed(struct window_mode_entry *); static void window_copy_refresh_start(struct window_mode_entry *); static void window_copy_refresh_stop(struct window_mode_entry *); static void window_copy_style_changed(struct window_mode_entry *); @@ -3036,6 +3037,20 @@ window_copy_refresh_arm(struct window_mode_entry *wme) evtimer_add(&data->refresh_timer, &tv); } +static int +window_copy_refresh_allowed(struct window_mode_entry *wme) +{ + struct window_copy_mode_data *data = wme->data; + + /* + * Do not refresh a view of another pane (copy-mode -s): the source may + * disappear and changes are not tracked on this pane. + */ + if (data->viewmode || wme->swp != wme->wp) + return (0); + return (1); +} + static void window_copy_refresh_timer(__unused int fd, __unused short events, void *arg) { @@ -3070,11 +3085,7 @@ window_copy_refresh_start(struct window_mode_entry *wme) { struct window_copy_mode_data *data = wme->data; - /* - * Do not refresh a view of another pane (copy-mode -s): the source may - * disappear and changes are not tracked on this pane. - */ - if (data->viewmode || wme->swp != wme->wp || data->refresh_active) + if (!window_copy_refresh_allowed(wme) || data->refresh_active) return; data->refresh_active = 1; window_copy_refresh_arm(wme); @@ -3089,6 +3100,25 @@ window_copy_refresh_stop(struct window_mode_entry *wme) evtimer_del(&data->refresh_timer); } +static enum window_copy_cmd_action +window_copy_cmd_refresh_now(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct window_copy_mode_data *data = wme->data; + struct window_pane *wp = wme->wp; + int follow; + + if (!window_copy_refresh_allowed(wme)) + return (WINDOW_COPY_CMD_NOTHING); + + follow = (data->oy == 0 && + data->cy == screen_size_y(&data->screen) - 1); + window_copy_do_refresh(wme, follow); + wp->flags &= ~PANE_UNSEENCHANGES; + + return (WINDOW_COPY_CMD_REDRAW); +} + static enum window_copy_cmd_action window_copy_cmd_refresh_on(struct window_copy_cmd_state *cs) { @@ -3631,6 +3661,12 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_NEVER, .f = window_copy_cmd_refresh_off }, + { .command = "refresh-now", + .args = { "", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_refresh_now + }, { .command = "refresh-toggle", .args = { "", 0, 0, NULL }, .flags = WINDOW_COPY_CMD_FLAG_READONLY, From 641e75e6e251de581fa3154fd2d1523d17d76395 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 6 Aug 2026 09:05:04 +0000 Subject: [PATCH 26/29] Add T key to change pane title, from Fouad Wahabi in GitHub issue 5461. --- key-bindings.c | 3 ++- tmux.1 | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/key-bindings.c b/key-bindings.c index 2f44cb218..2555b9fcb 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.191 2026/08/05 12:23:25 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.192 2026/08/06 09:05:04 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -421,6 +421,7 @@ key_bindings_init(void) "bind -N 'Spread panes out evenly' E { select-layout -E }", "bind -N 'Switch to the last client' L { switch-client -l }", "bind -N 'Clear the marked pane' M { select-pane -M }", + "bind -N 'Change the pane title' T { command-prompt -I'#T' { select-pane -T '%%' } }", "bind -N 'Enter copy mode' [ { copy-mode }", "bind -N 'Paste the most recent paste buffer' ] { paste-buffer -p }", "bind -N 'Create a new window' c { new-window }", diff --git a/tmux.1 b/tmux.1 index c72bf35ef..d0f54c91e 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1154 2026/08/05 12:23:25 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1155 2026/08/06 09:05:04 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 5 2026 $ +.Dd $Mdocdate: August 6 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -354,6 +354,8 @@ Force redraw of the attached client. Select a new session for the attached client interactively. .It t Show the time. +.It T +Change the current pane title. .It w Choose the current window interactively. .It x From 35ab6600429881b7a594548ca23d7c0d5006c2a1 Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Thu, 6 Aug 2026 10:23:46 -0400 Subject: [PATCH 27/29] ci: only run `lock` and `regress` workflows on the main repo Forks are just wasting CI cycles spinning actions which are probably not looked at. Also avoid locking issues and PRs on any forks with upstream's policies. --- .github/workflows/lock.yml | 1 + .github/workflows/regress.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 9bce72248..e6765da99 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -16,6 +16,7 @@ concurrency: jobs: action: runs-on: ubuntu-latest + if: github.repository == 'tmux/tmux' steps: - uses: dessant/lock-threads@v6 with: diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 95935f2f8..3cc036425 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -16,6 +16,7 @@ jobs: regress: name: ${{ matrix.name }} runs-on: ${{ matrix.runner }} + if: github.repository == 'tmux/tmux' timeout-minutes: 45 strategy: From 2ec1038ffd49a2fb8e5741d689bc8889820a896e Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 6 Aug 2026 21:47:20 +0000 Subject: [PATCH 28/29] =?UTF-8?q?Always=20unzoom=20before=20splitting=20wi?= =?UTF-8?q?ndow,=20for=20floating=20panes=20also.=20We=20already=20unzoome?= =?UTF-8?q?d=20for=20tiled=20panes=20and=20this=20does=20the=20same=20for?= =?UTF-8?q?=20floating=20panes=20(until=20we=20support=20having=20them=20f?= =?UTF-8?q?loat=20over=20a=20zoomed=20pane).=20From=20=C3=89ric=20NICOLAS.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd-split-window.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd-split-window.c b/cmd-split-window.c index 8cefba937..a089ba6a5 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.146 2026/07/21 12:28:43 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.147 2026/08/06 21:47:20 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -97,6 +97,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) enum pane_lines lines; u_int count = args_count(args); + window_unzoom(w, 1); + if (cmd_get_entry(self) == &cmd_new_pane_entry) is_floating = !args_has(args, 'L'); else { From 10048f6579209fd1ca18ff6c5436628dec012371 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 7 Aug 2026 08:10:53 +0000 Subject: [PATCH 29/29] Only unzoom in the floating-split case, modal panes take care of themselves. --- cmd-split-window.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd-split-window.c b/cmd-split-window.c index a089ba6a5..e919b6f03 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.147 2026/08/06 21:47:20 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.148 2026/08/07 08:10:53 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -97,11 +97,10 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) enum pane_lines lines; u_int count = args_count(args); - window_unzoom(w, 1); - if (cmd_get_entry(self) == &cmd_new_pane_entry) is_floating = !args_has(args, 'L'); else { + window_unzoom(w, 1); is_floating = window_pane_is_floating(wp); flags |= SPAWN_SPLIT; }