From 418ddada23d290ab02417b62213b402a08f4d9b5 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 21 Jul 2026 07:12:49 +0000 Subject: [PATCH 01/85] Increase escape-time for tmux's own queries (refresh-client -l and window size) as well, GitHub issue 5388 from Jackson Edmonds. --- tty-keys.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tty-keys.c b/tty-keys.c index 61e541165..2d4454b6c 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-keys.c,v 1.210 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: tty-keys.c,v 1.211 2026/07/21 07:12:49 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -977,8 +977,9 @@ partial_key: if (delay < 500) delay = 500; } - if ((tty->flags & (TTY_WAITFG|TTY_WAITBG) || - (tty->flags & TTY_ALL_REQUEST_FLAGS) != TTY_ALL_REQUEST_FLAGS) || + if (tty->flags & (TTY_WAITFG|TTY_WAITBG) || + tty->flags & (TTY_OSC52QUERY|TTY_WINSIZEQUERY) || + (tty->flags & TTY_ALL_REQUEST_FLAGS) != TTY_ALL_REQUEST_FLAGS || !TAILQ_EMPTY(&c->input_requests)) { log_debug("%s: increasing delay (active query)", c->name); if (delay < 500) From 0e2576a842104886f3e33f0e62928551e54309cb Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 21 Jul 2026 07:20:18 +0000 Subject: [PATCH 02/85] Use correct cursor position for range end, GitHub issue 5414. --- format-draw.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/format-draw.c b/format-draw.c index ea1da421a..3fb6a9ae3 100644 --- a/format-draw.c +++ b/format-draw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format-draw.c,v 1.33 2026/06/29 17:08:52 nicm Exp $ */ +/* $OpenBSD: format-draw.c,v 1.34 2026/07/21 07:20:18 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -958,7 +958,7 @@ format_draw(struct screen_write_ctx *octx, const struct grid_cell *base, if (srs != NULL) { if (fr != NULL && !format_is_type(fr, &sy)) { if (s[current].cx != fr->start) { - fr->end = s[current].cx + 1; + fr->end = s[current].cx; TAILQ_INSERT_TAIL(&frs, fr, entry); } else free(fr); From 3e61e63851f343439f63bfd2bb5ca357dfc60dc0 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 21 Jul 2026 12:56:32 +0100 Subject: [PATCH 03/85] Test for outside panes with no border. --- regress/screen-redraw-floating.sh | 7 +++++++ .../floating-empty-noborder.result | 12 ++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 regress/screen-redraw-results/floating-empty-noborder.result diff --git a/regress/screen-redraw-floating.sh b/regress/screen-redraw-floating.sh index 67d213699..2db4920a5 100644 --- a/regress/screen-redraw-floating.sh +++ b/regress/screen-redraw-floating.sh @@ -218,4 +218,11 @@ tiled=$($TMUX2 list-panes -F '#{pane_floating_flag} #{pane_id}' | \ $TMUX2 kill-pane -t "$tiled" || exit 1 compare floating-empty +# Same after pane-border-lines is set to none: empty window background is not a +# pane border and should still use the dotted window fill. +new_scene 40 12 +$TMUX2 breakp -W || exit 1 +$TMUX2 set pane-border-lines none || exit 1 +compare floating-empty-noborder + exit 0 diff --git a/regress/screen-redraw-results/floating-empty-noborder.result b/regress/screen-redraw-results/floating-empty-noborder.result new file mode 100644 index 000000000..a2b4c89b6 --- /dev/null +++ b/regress/screen-redraw-results/floating-empty-noborder.result @@ -0,0 +1,12 @@ +········································ +········································ +····base ················ +···· ················ +···· ················ +········································ +········································ +········································ +········································ +········································ +········································ + From abd2f3a9a57851b68d93dcfc95651671c49dd1a2 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 21 Jul 2026 11:52:13 +0000 Subject: [PATCH 04/85] Add copy-mode-current-line-style to set style for current line in copy mode, GitHub issue 5391 from Leo Henon. --- options-table.c | 11 ++++++++++- tmux.1 | 12 ++++++++++-- window-copy.c | 50 ++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/options-table.c b/options-table.c index 82ae2e5ff..124bf5145 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.239 2026/07/19 17:36:38 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.240 2026/07/21 11:52:13 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1431,6 +1431,15 @@ const struct options_table_entry options_table[] = { .text = "Style of current line number in copy mode." }, + { .name = "copy-mode-current-line-style", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_WINDOW, + .default_str = "default", + .flags = OPTIONS_TABLE_IS_STYLE, + .separator = ",", + .text = "Style of the line containing the cursor in copy mode." + }, + { .name = "copy-mode-line-number-style", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW, diff --git a/tmux.1 b/tmux.1 index 8d433131b..4565a1d94 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1144 2026/07/19 17:36:38 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1145 2026/07/21 11:52:13 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 19 2026 $ +.Dd $Mdocdate: July 21 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -5771,6 +5771,14 @@ see the .Sx STYLES section. .Pp +.It Ic copy\-mode\-current\-line\-style Ar style +Set the style of the line containing the cursor in copy mode. +For how to specify +.Ar style , +see the +.Sx STYLES +section. +.Pp .It Ic copy\-mode\-line\-number\-style Ar style Set style of line numbers in copy mode. For how to specify diff --git a/window-copy.c b/window-copy.c index 4a8009b14..ff58dfbad 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.422 2026/07/20 11:16:33 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.423 2026/07/21 11:52:13 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -62,6 +62,7 @@ static void window_copy_set_line_numbers1(struct window_mode_entry *, int, static int window_copy_line_number_mode(struct window_mode_entry *); static int window_copy_line_number_is_absolute(struct window_mode_entry *); static int window_copy_line_numbers_active(struct window_mode_entry *); +static int window_copy_cursor_line_active(struct window_mode_entry *); static u_int window_copy_line_number_width(struct window_mode_entry *); static u_int window_copy_cursor_offset(struct window_mode_entry *, u_int, u_int); @@ -5027,7 +5028,8 @@ window_copy_match_at_cursor(struct window_copy_mode_data *data) static void window_copy_update_style(struct window_mode_entry *wme, u_int fx, u_int fy, struct grid_cell *gc, const struct grid_cell *mgc, - const struct grid_cell *cgc, const struct grid_cell *mkgc) + const struct grid_cell *cgc, const struct grid_cell *mkgc, + const struct grid_cell *clgc) { struct window_pane *wp = wme->wp; struct window_copy_mode_data *data = wme->data; @@ -5035,6 +5037,15 @@ window_copy_update_style(struct window_mode_entry *wme, u_int fx, u_int fy, int inv = 0, found = 0; int keys; + cy = screen_hsize(data->backing) - data->oy + data->cy; + if (fy == cy) { + if (clgc->fg != 8) + gc->fg = clgc->fg; + if (clgc->bg != 8) + gc->bg = clgc->bg; + gc->attr |= clgc->attr; + } + if (data->showmark && fy == data->my) { gc->attr = mkgc->attr; if (fx == data->mx) @@ -5058,7 +5069,6 @@ window_copy_update_style(struct window_mode_entry *wme, u_int fx, u_int fy, if (mark == 0) return; - cy = screen_hsize(data->backing) - data->oy + data->cy; if (window_copy_search_mark_at(data, data->cx, cy, &cursor) == 0) { keys = options_get_number(wp->window->options, "mode-keys"); if (cursor != 0 && @@ -5102,7 +5112,7 @@ static void window_copy_write_one(struct window_mode_entry *wme, struct screen_write_ctx *ctx, u_int px, u_int py, u_int fy, u_int nx, const struct grid_cell *mgc, const struct grid_cell *cgc, - const struct grid_cell *mkgc) + const struct grid_cell *mkgc, const struct grid_cell *clgc) { struct window_copy_mode_data *data = wme->data; struct grid *gd = data->backing->grid; @@ -5114,7 +5124,7 @@ window_copy_write_one(struct window_mode_entry *wme, grid_get_cell(gd, fx, fy, &gc); if (fx + gc.data.width <= nx) { window_copy_update_style(wme, fx, fy, &gc, mgc, cgc, - mkgc); + mkgc, clgc); if (gc.flags & GRID_FLAG_PADDING) { if (ctx->s->cy == py && ctx->s->cx <= px + fx) { gc.flags &= ~GRID_FLAG_PADDING; @@ -5172,6 +5182,16 @@ window_copy_line_numbers_active(struct window_mode_entry *wme) WINDOW_COPY_LINE_NUMBERS_OFF); } +static int +window_copy_cursor_line_active(struct window_mode_entry *wme) +{ + struct options *oo = wme->wp->window->options; + const char *s; + + s = options_get_string(oo, "copy-mode-current-line-style"); + return (strcmp(s, "default") != 0); +} + static u_int window_copy_line_number_width(struct window_mode_entry *wme) { @@ -5288,7 +5308,8 @@ window_copy_write_line(struct window_mode_entry *wme, struct window_copy_mode_data *data = wme->data; struct screen *s = &data->screen; struct options *oo = wp->window->options; - struct grid_cell gc, mgc, cgc, mkgc, ln_gc, cur_ln_gc; + struct grid_cell gc, mgc, cgc, mkgc, clgc, ln_gc; + struct grid_cell cur_ln_gc; u_int sx = screen_size_x(s); u_int hsize = screen_hsize(data->backing); u_int width; @@ -5318,6 +5339,8 @@ window_copy_write_line(struct window_mode_entry *wme, cgc.flags |= GRID_FLAG_NOPALETTE; style_apply(&mkgc, oo, "copy-mode-mark-style", ft); mkgc.flags |= GRID_FLAG_NOPALETTE; + style_apply(&clgc, oo, "copy-mode-current-line-style", ft); + clgc.flags |= GRID_FLAG_NOPALETTE; if (width != 0) { style_apply(&ln_gc, oo, "copy-mode-line-number-style", ft); ln_gc.flags |= GRID_FLAG_NOPALETTE; @@ -5346,7 +5369,7 @@ window_copy_write_line(struct window_mode_entry *wme, } window_copy_write_one(wme, ctx, width, py, hsize - data->oy + py, - content_sx, &mgc, &cgc, &mkgc); + content_sx, &mgc, &cgc, &mkgc, &clgc); if (py == 0 && s->rupper < s->rlower && !data->hide_position) { value = options_get_string(oo, "copy-mode-position-format"); @@ -5598,6 +5621,11 @@ window_copy_update_cursor(struct window_mode_entry *wme, u_int cx, u_int cy) screen_write_stop(&ctx); return; } + if (old_cy != data->cy && window_copy_cursor_line_active(wme)) { + window_copy_redraw_lines(wme, old_cy, 1); + window_copy_redraw_lines(wme, data->cy, 1); + return; + } if (old_cx == screen_size_x(s)) window_copy_redraw_lines(wme, old_cy, 1); if (data->cx == screen_size_x(s)) @@ -6799,6 +6827,10 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny) if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection_view(wme, 0, 0); + if (window_copy_cursor_line_active(wme)) { + window_copy_redraw_screen(wme); + return; + } if (window_copy_line_numbers_active(wme)) { if (window_copy_line_number_mode(wme) != WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { @@ -6866,6 +6898,10 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection_view(wme, 0, 0); + if (window_copy_cursor_line_active(wme)) { + window_copy_redraw_screen(wme); + return; + } if (window_copy_line_numbers_active(wme)) { if (window_copy_line_number_mode(wme) != WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { From 9d583cad5af9792c42fea65f968710546facf4bd Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 21 Jul 2026 11:53:41 +0000 Subject: [PATCH 05/85] If drawing border but not actually on a border, use the default line style. --- screen-redraw.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/screen-redraw.c b/screen-redraw.c index 6bf8dd8b8..a82d0566e 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-redraw.c,v 1.153 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: screen-redraw.c,v 1.154 2026/07/21 11:53:41 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -1230,6 +1230,8 @@ redraw_draw_border_span(struct redraw_draw_ctx *dctx, if (wp == NULL) { redraw_get_default_border_style(dctx, &gc, &pane_lines); + if (span->data.type != REDRAW_SPAN_BORDER) + pane_lines = PANE_LINES_SINGLE; window_get_border_cell(w, NULL, pane_lines, cell_type, &gc); } else { window_pane_get_border_style(wp, c, &gc); From 2848a12562c0958d0fef68f9ead7c52fa59f877e Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 21 Jul 2026 14:41:52 +0100 Subject: [PATCH 06/85] Update tests for fix to floating panes at window edges. --- regress/screen-redraw-floating.sh | 7 +++++++ .../floating-clip-window-edge.result | 12 ++++++++++++ .../screen-redraw-results/floating-outside.result | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 regress/screen-redraw-results/floating-clip-window-edge.result diff --git a/regress/screen-redraw-floating.sh b/regress/screen-redraw-floating.sh index 2db4920a5..d0b03fc7f 100644 --- a/regress/screen-redraw-floating.sh +++ b/regress/screen-redraw-floating.sh @@ -125,6 +125,13 @@ new_scene 28 8 $TMUX2 new-pane -x16 -y5 -X18 -Y4 "sh -c 'printf OUT; exec sleep 100'" || exit 1 compare floating-outside +# Floating pane whose right border is exactly on the window right edge. The +# floating pane is clipped so the tiled pane's right border remains unbroken. +new_scene 28 8 +$TMUX2 respawnp -k "sh -c 'i=0; while [ \$i -lt 8 ]; do printf \"%02d:abcdefghijklmnopq\n\" \$i; i=\$((i + 1)); done; exec sleep 100'" || exit 1 +$TMUX2 new-pane -x10 -y5 -X19 -Y2 "sh -c 'printf EDGE; exec sleep 100'" || exit 1 +compare floating-clip-window-edge + # Floating pane clipped at the top-left corner (negative offsets). new_scene 40 12 $TMUX2 new-pane -x18 -y6 -X-4 -Y-2 "sh -c 'printf TL; exec sleep 100'" || exit 1 diff --git a/regress/screen-redraw-results/floating-clip-window-edge.result b/regress/screen-redraw-results/floating-clip-window-edge.result new file mode 100644 index 000000000..751c4ef5d --- /dev/null +++ b/regress/screen-redraw-results/floating-clip-window-edge.result @@ -0,0 +1,12 @@ +01:abcdefghijklmnopq │··········· +02:abcdefghijklmnopq │··········· +03:abcdefghijklmnop┌────────│··········· +04:abcdefghijklmnop│EDGE │··········· +05:abcdefghijklmnop│ │··········· +06:abcdefghijklmnop│ │··········· +07:abcdefghijklmnop└────────│··········· + │··········· +────────────────────────────┘··········· +········································ +········································ +········································ diff --git a/regress/screen-redraw-results/floating-outside.result b/regress/screen-redraw-results/floating-outside.result index 4fae5b138..e7de24533 100644 --- a/regress/screen-redraw-results/floating-outside.result +++ b/regress/screen-redraw-results/floating-outside.result @@ -6,7 +6,7 @@ base │··········· │OUT │··········· │ │··········· │ │··········· -──────────────────└─────────┘··········· +────────────────────────────┘··········· ········································ ········································ ········································ From 8bf2a4e807a092970dabfac89936d6e887389ecd Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 21 Jul 2026 12:28:43 +0000 Subject: [PATCH 07/85] Add a -C flag to new-pane to have a modal pane close when the mouse is clicked outside it, GitHub issue 5400. --- cmd-split-window.c | 8 +++++--- server-client.c | 7 ++++++- tmux.1 | 7 +++++-- tmux.h | 3 ++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/cmd-split-window.c b/cmd-split-window.c index bcda1d97f..8cefba937 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.145 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.146 2026/07/21 12:28:43 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -41,8 +41,8 @@ const struct cmd_entry cmd_new_pane_entry = { .name = "new-pane", .alias = "newp", - .args = { "bB:c:de:EfF:hIkl:LMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, - .usage = "[-bdefhIklMOPvWZ] [-B border-lines] " + .args = { "bB:Cc:de:EfF:hIkl:LMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, + .usage = "[-bCdefhIklMOPvWZ] [-B border-lines] " "[-c start-directory] [-e environment] " "[-F format] [-l size] [-m message] [-p percentage] " "[-s style] [-S active-border-style] " @@ -202,6 +202,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) */ goto fail; } + if (args_has(args, 'C') && args_has(args, 'O')) + new_wp->flags |= PANE_CLOSEONCLICK; style = args_get(args, 's'); if (style != NULL) { diff --git a/server-client.c b/server-client.c index c64c57d2e..a010c9f16 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.497 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.498 2026/07/21 12:28:43 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -1062,6 +1062,11 @@ have_event: c->tty.mouse_scrolling_flag = 0; c->tty.mouse_slider_mpos = -1; c->tty.mouse_last_pane = -1; + if ((w->modal->flags & PANE_CLOSEONCLICK) && + (type == KEYC_TYPE_MOUSEDOWN || + type == KEYC_TYPE_SECONDCLICK || + type == KEYC_TYPE_TRIPLECLICK)) + server_kill_pane(w->modal); return (KEYC_UNKNOWN); } } diff --git a/tmux.1 b/tmux.1 index 4565a1d94..5fb22d413 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1145 2026/07/21 11:52:13 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1146 2026/07/21 12:28:43 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -3636,7 +3636,7 @@ but a different format may be specified with .Fl F . .Tg newp .It Xo Ic new\-pane -.Op Fl bdefhIkLMOPvWZ +.Op Fl bCdefhIkLMOPvWZ .Op Fl B Ar border\-lines .Op Fl c Ar start\-directory .Op Fl e Ar environment @@ -3692,6 +3692,9 @@ creates a modal pane. A modal pane is always the active pane and prevents interaction with any other panes while it is active. A window can only have one modal pane and it must be a floating pane. +With +.Fl C , +the modal pane is closed when the mouse is clicked outside it. .Pp The .Fl L diff --git a/tmux.h b/tmux.h index 597d169b7..217e5e55e 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1411 2026/07/20 11:16:33 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1412 2026/07/21 12:28:43 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1298,6 +1298,7 @@ struct window_pane { #define PANE_DESTROYED 0x10000 #define PANE_CMDRUNNING 0x20000 #define PANE_ACTIVITY 0x40000 +#define PANE_CLOSEONCLICK 0x80000 bitstr_t *sync_dirty; u_int sync_dirty_size; From c59db2d007351f0da953334b81f5394508204df9 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 21 Jul 2026 13:04:01 +0000 Subject: [PATCH 08/85] Invalidate scene when window is resized. --- window.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/window.c b/window.c index 5f06c1a14..f5bf6870b 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.366 2026/07/19 19:53:11 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.367 2026/07/21 13:04:01 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -573,13 +573,13 @@ window_resize(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) w->sy = sy; if (w->menu != NULL) { menu_resize(w->menu, w); - redraw_invalidate_scene(w); server_redraw_window(w); } if (xpixel != -1) w->xpixel = xpixel; if (ypixel != -1) w->ypixel = ypixel; + redraw_invalidate_scene(w); } void From a57b0f1282129231d3e0934062991feb11381976 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 21 Jul 2026 13:27:41 +0000 Subject: [PATCH 09/85] Clip floating panes exactly at window edge, don't let the border creep into the first outside column. --- screen-redraw.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/screen-redraw.c b/screen-redraw.c index a82d0566e..ea4af99bf 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-redraw.c,v 1.154 2026/07/21 11:53:41 nicm Exp $ */ +/* $OpenBSD: screen-redraw.c,v 1.155 2026/07/21 13:27:41 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -696,20 +696,22 @@ redraw_mark_pane_borders(struct redraw_build_ctx *bctx, struct window_pane *wp, bottom = wp->yoff + wp->sy; mark_left = (left >= 0); - mark_right = (right <= (int)bctx->w->sx); mark_top = (top >= 0); - mark_bottom = (bottom <= (int)bctx->w->sy); if (floating) { + mark_right = (right < (int)bctx->w->sx); + mark_bottom = (bottom < (int)bctx->w->sy); if (left < 0) left = 0; - if (right > (int)bctx->w->sx) + if (right >= (int)bctx->w->sx) right = (int)bctx->w->sx - 1; if (top < 0) top = 0; - if (bottom > (int)bctx->w->sy) + if (bottom >= (int)bctx->w->sy) bottom = (int)bctx->w->sy - 1; } else { + mark_right = (right <= (int)bctx->w->sx); + mark_bottom = (bottom <= (int)bctx->w->sy); if (pane_status == PANE_STATUS_TOP) mark_bottom = 0; else if (pane_status == PANE_STATUS_BOTTOM) From 0868435e81986b53c1d58ab93a2b973d2f311f59 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 22 Jul 2026 14:17:22 +0100 Subject: [PATCH 10/85] Add test for A: modifier, from Fernando Daciuk. --- CHANGES | 3 + regress/format-animation.sh | 109 ++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 regress/format-animation.sh diff --git a/CHANGES b/CHANGES index eaf7576da..5c4ff9664 100644 --- a/CHANGES +++ b/CHANGES @@ -87,6 +87,9 @@ CHANGES FROM 3.7b TO 3.8 while scrolling or while hovered and disappears after pane-scrollbars-timeout (Michael Grant). +* Add a #{A/count:frames} modifier to show a series of frames as an animation + in the status line (issue 5412 from Fernando Daciuk). + * Change command templates so %% is escaped for single quotes while %1 remains unquoted, preventing single quotes in session or target names from terminating quoted command sections (reported by Aliz Hammond). diff --git a/regress/format-animation.sh b/regress/format-animation.sh new file mode 100644 index 000000000..b62b04444 --- /dev/null +++ b/regress/format-animation.sh @@ -0,0 +1,109 @@ +#!/bin/sh + +# Exercise the A format modifier (format_cycle in format.c). It is expanded +# only in a status format and arms the client cycle timer to redraw the status +# line as the frames advance, so the only place its behaviour shows is a +# rendered status line. +# +# The status line is rendered by an inner tmux attached inside an outer tmux +# pane; capturing the outer pane shows what the inner tmux drew. The inner +# window runs sleep and every other status line is blanked, so the capture +# contains the frame and nothing else. +# +# Each frame lasts 700 milliseconds and the capture is repeated once a second, +# so consecutive samples always land on a different frame: no exact period is +# assumed and nothing in the test asks for a redraw, so seeing the frame change +# means the status line animated on its own. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +TMP=$(mktemp) +trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" \ + 0 1 15 + +fail() { + echo "$*" >&2 + exit 1 +} + +# Capture the outer pane and return the text the inner tmux drew. +capture() { + $TMUX capturep -p >$TMP || fail "capture failed" + tr -d '[:space:]' <$TMP +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +# Inner: a window which draws nothing and a status line which is only the +# animation, so the outer capture is the current frame on its own. +$TMUX2 new -d -x30 -y8 "sh -c 'exec sleep 100'" || exit 1 +$TMUX2 set -g window-size latest || exit 1 +i=1 +while [ $i -le 4 ]; do + $TMUX2 set -g status-format[$i] "" || exit 1 + i=$((i + 1)) +done +$TMUX2 set -g status-format[0] "#{A/7:AAAA,BBBB,CCCC}" || exit 1 + +$TMUX new -d -x30 -y8 || exit 1 +$TMUX set -g status off || exit 1 +$TMUX set -g window-size manual || exit 1 +$TMUX set -g default-terminal "tmux-256color" || exit 1 +$TMUX send -l "$TMUX2 attach" || exit 1 +$TMUX send Enter || exit 1 +sleep 1 + +# Sample the frames. Every sample must be one of the frames and at least two +# different ones must be seen; status-interval is 15 seconds so only the cycle +# timer can have redrawn the status line. +seen="" +i=0 +while [ $i -lt 4 ]; do + [ $i -eq 0 ] || sleep 1 + frame=$(capture) + case "$frame" in + AAAA|BBBB|CCCC) ;; + *) fail "status line is '$frame', not a frame" ;; + esac + case " $seen " in + *" $frame "*) ;; + *) seen="$seen $frame" ;; + esac + i=$((i + 1)) +done +set -- $seen +[ $# -ge 2 ] || fail "status line did not animate, only saw$seen" + +# Outside a status format there is no animation at all. +out=$($TMUX2 display-message -p "#{A:ZZZZ,YYYY}") || exit 1 +[ -z "$out" ] || fail "display-message gave '$out', want empty" +out=$($TMUX2 list-panes -F "#{A:ZZZZ,YYYY}") || exit 1 +[ -z "$out" ] || fail "list-panes gave '$out', want empty" + +# Nor inside #(), where the frames would change the command on every frame. +$TMUX2 set -g status-format[0] '#(echo "[#{A:ZZZZ,YYYY}]")' || exit 1 +sleep 1 +out=$(capture) +case "$out" in +*ZZZZ*|*YYYY*) fail "job command saw a frame: '$out'" ;; +*"[]"*) ;; +*) fail "job output is '$out', want []" ;; +esac + +# Empty frames and a missing or bad count must not upset the server. +for f in '#{A:}' '#{A/0:a,b}' '#{A/x:a,b}' '#{A:,,,}'; do + $TMUX2 set -g status-format[0] "$f" || fail "setting $f failed" + $TMUX2 has-session >/dev/null 2>&1 || fail "server lost with $f" +done +sleep 1 +$TMUX2 has-session >/dev/null 2>&1 || fail "server lost after empty frames" + +exit 0 From bd4ac0299484b0930c0644e48680f3a63ad6a0c7 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 22 Jul 2026 08:19:14 +0000 Subject: [PATCH 11/85] Add A modifier to cycle through a series of values, GitHub issue 5412 from Fernando Daciuk. --- format.c | 101 +++++++++++++++++++++++++++++++++++++++++++++--- server-client.c | 4 +- tmux.1 | 24 +++++++++++- tmux.h | 3 +- 4 files changed, 123 insertions(+), 9 deletions(-) diff --git a/format.c b/format.c index a3b1acfa7..f703022b5 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.407 2026/07/20 07:42:13 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.408 2026/07/22 08:19:14 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -128,6 +128,7 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2) #define FORMAT_OPTIONS 0x40000000 #define FORMAT_ENVIRON 0x80000000ULL #define FORMAT_DIFFERENCE 0x100000000ULL +#define FORMAT_CYCLE 0x200000000ULL /* Limit on recursion. */ #define FORMAT_LOOP_LIMIT 100 @@ -138,9 +139,13 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2) /* How often to check the time in long loops. */ #define FORMAT_TIME_LOOP_CHECK 10000 +/* Fixed animation period (ms): redraw interval and shortest frame step. */ +#define FORMAT_CYCLE_PERIOD 100 + /* Format expand flags. */ #define FORMAT_EXPAND_TIME 0x1 #define FORMAT_EXPAND_NOJOBS 0x2 +#define FORMAT_EXPAND_NOCYCLE 0x4 /* Entry in format tree. */ struct format_entry { @@ -413,7 +418,8 @@ format_job_get(struct format_expand_state *es, const char *cmd) RB_INSERT(format_job_tree, jobs, fj); } - format_copy_state(&next, es, FORMAT_EXPAND_NOJOBS); + format_copy_state(&next, es, FORMAT_EXPAND_NOJOBS| + FORMAT_EXPAND_NOCYCLE); next.flags &= ~FORMAT_EXPAND_TIME; expanded = format_expand1(&next, cmd); @@ -4779,7 +4785,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, /* * Modifiers are a ; separated list of the forms: - * l,m,C,a,b,c,d,I,n,t,w,q,E,T,S,W,P,O,V,R,<,> + * l,m,C,a,b,c,d,I,n,t,w,q,E,T,S,W,P,O,V,R,A,<,> * =a * =/a * =/a/ @@ -4798,7 +4804,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, break; /* Check single character modifiers with no arguments. */ - if (strchr("labdnwETSWPOVL!<>", cp[0]) != NULL && + if (strchr("labdnwETSWPOVL!<>A", cp[0]) != NULL && format_is_end(cp[1])) { format_add_modifier(&list, count, cp, 1, NULL, 0); cp++; @@ -4820,7 +4826,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, } /* Now try single character with arguments. */ - if (strchr("ImCLNPSOVst=pReqWc", cp[0]) == NULL) + if (strchr("ImCLNPSOVst=pReqWcA", cp[0]) == NULL) break; c = cp[0]; @@ -5779,6 +5785,74 @@ fail: return (NULL); } +/* Callback for the cycle timer; redraw the status line. */ +static void +format_cycle_callback(__unused int fd, __unused short events, void *arg) +{ + struct client *c = arg; + + if (c->message_string == NULL && c->prompt == NULL) + c->flags |= CLIENT_REDRAWSTATUS; +} + +/* Arm the cycle timer to redraw the status line if it is not already. */ +static void +format_cycle_start_timer(struct client *c) +{ + struct timeval tv; + + tv.tv_sec = FORMAT_CYCLE_PERIOD / 1000; + tv.tv_usec = (FORMAT_CYCLE_PERIOD % 1000) * 1000L; + + if (!event_initialized(&c->cycle_timer)) + evtimer_set(&c->cycle_timer, format_cycle_callback, c); + if (!evtimer_pending(&c->cycle_timer, NULL)) + evtimer_add(&c->cycle_timer, &tv); +} + +/* Expand the "A" animation modifier; see the manual for the syntax. */ +static char * +format_cycle(struct format_expand_state *es, const char *frames, u_int count) +{ + struct format_tree *ft = es->ft; + const char *start, *end, *cp; + u_int n, index, i; + + /* + * A cycle is only expanded in a status format, and never in the + * command or output of #() where it would change on every frame and + * make the job run again. + */ + if (!(ft->flags & FORMAT_STATUS) || (es->flags & FORMAT_EXPAND_NOCYCLE)) + return (xstrdup("")); + if (*frames == '\0') + return (xstrdup("")); + + /* Count the comma-separated frames (there is at least one). */ + n = 1; + for (cp = frames; *cp != '\0'; cp++) { + if (*cp == ',') + n++; + } + index = (es->start_time / (count * FORMAT_CYCLE_PERIOD)) % n; + + /* + * Redraw the status line so the frames advance on their own; a + * single frame never changes so there is nothing to redraw for. + */ + if (n > 1 && ft->client != NULL) + format_cycle_start_timer(ft->client); + + /* Walk to the chosen frame and return a copy of it. */ + start = frames; + for (i = 0; i < index; i++) + start = strchr(start, ',') + 1; + end = strchr(start, ','); + if (end == NULL) + end = start + strlen(start); + return (xstrndup(start, end - start)); +} + /* Replace a key. */ static int format_replace(struct format_expand_state *es, const char *key, size_t keylen, @@ -5799,6 +5873,7 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, struct format_modifier *list, *cmp = NULL, *search = NULL; struct format_modifier **sub = NULL, *mexp = NULL, *fm; struct format_modifier *bool_op_n = NULL; + u_int cycle_count = 1; u_int i, count, nsub = 0, nrep, check = 0; const char *loop_flags = ""; struct format_expand_state next; @@ -5859,6 +5934,15 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, if (errstr != NULL) width = 0; break; + case 'A': + modifiers |= FORMAT_CYCLE; + if (fm->argc < 1) + break; + cycle_count = strtonum(fm->argv[0], 1, 100, + &errstr); + if (errstr != NULL) + cycle_count = 1; + break; case 'w': modifiers |= FORMAT_WIDTH; break; @@ -6080,6 +6164,13 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, goto done; } + /* Is this an animation cycle? */ + if (modifiers & FORMAT_CYCLE) { + value = format_cycle(es, copy, cycle_count); + format_log(es, "cycle '%s' is: %s", copy, value); + goto done; + } + /* Is this a literal string? */ if (modifiers & FORMAT_LITERAL) { format_log(es, "literal string is '%s'", copy); diff --git a/server-client.c b/server-client.c index a010c9f16..081862e43 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.498 2026/07/21 12:28:43 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.499 2026/07/22 08:19:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -530,6 +530,8 @@ server_client_lost(struct client *c) evtimer_del(&c->repeat_timer); evtimer_del(&c->click_timer); + if (event_initialized(&c->cycle_timer)) + evtimer_del(&c->cycle_timer); key_bindings_unref_table(c->keytable); diff --git a/tmux.1 b/tmux.1 index 5fb22d413..c3d66e73f 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1146 2026/07/21 12:28:43 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1147 2026/07/22 08:19:14 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 21 2026 $ +.Dd $Mdocdate: July 22 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -7183,6 +7183,26 @@ For example will be replaced by .Ql #{?pane_in_mode,yes,no} . .Pp +The +.Ql A +modifier shows one of a list of frames in turn: +.Ql #{A/count:frame,frame,...} +or +.Ql #{A:frame,frame,...} . +Each frame is shown for +.Ar count +periods of 100 milliseconds before the next, wrapping back to the first after +the last. +If no count is given, one is used. +The +.Ql A +modifier is expanded only in the status line and +.Ic pane\-border\-format ; +if present, the status line or pane borders are redrawn every 100 milliseconds. +For example, +.Ql #{A/2:|,/,\-,\e} +shows the four frames in turn, changing every 200 milliseconds. +.Pp The following variables are available, where appropriate: .Bl -column "XXXXXXXXXXXXXXXXXXX" "XXXXX" .It Sy "Variable name" Ta Sy "Alias" Ta Sy "Replaced with" diff --git a/tmux.h b/tmux.h index 217e5e55e..04ad25b82 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1412 2026/07/21 12:28:43 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1413 2026/07/22 08:19:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2203,6 +2203,7 @@ struct client { struct mouse_event click_event; struct status_line status; + struct event cycle_timer; enum client_theme theme; struct input_requests input_requests; From 18fc547b829317a886693827b8c4aa531e0e8143 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 22 Jul 2026 19:58:55 +0100 Subject: [PATCH 12/85] Update show hooks tests. --- regress/hooks.sh | 13 +++++++++++++ regress/set-hook-B.sh | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/regress/hooks.sh b/regress/hooks.sh index e1d75bf67..e99b2e768 100644 --- a/regress/hooks.sh +++ b/regress/hooks.sh @@ -86,6 +86,19 @@ echo "$shown" | grep -q '^session-created\[0\]' || echo "$shown" | grep -q '^session-created\[1\]' || fail "missing second array item: $shown" +# User hooks are options, but show-hooks should list only registered @ hooks +# and not ordinary user options. +$TMUX set -g @not-a-hook value || fail "set @not-a-hook failed" +$TMUX set-hook -g @user-hook 'lsk' || + fail "set-hook @user-hook failed" +shown=$($TMUX show-hooks -g) || + fail "show-hooks -g all failed" +echo "$shown" | grep -q '^@user-hook lsk$' || + fail "missing user hook: $shown" +if echo "$shown" | grep -q '^@not-a-hook '; then + fail "show-hooks listed user option: $shown" +fi + # Unsetting removes the whole hook. $TMUX set-hook -gu session-created || fail "set-hook -gu failed" shown=$($TMUX show-hooks -g session-created) || diff --git a/regress/set-hook-B.sh b/regress/set-hook-B.sh index 0144884dc..0226cad00 100644 --- a/regress/set-hook-B.sh +++ b/regress/set-hook-B.sh @@ -67,6 +67,10 @@ shown=$($TMUX show-hooks -g -B @session-name) || fail "show-hooks -B failed" [ "$shown" = '@session-name::#{session_name}' ] || fail "unexpected show-hooks -B output: $shown" +shown=$($TMUX show-hooks -g) || + fail "show-hooks -g failed" +echo "$shown" | grep -q '^@session-name ' || + fail "show-hooks -g did not show monitor hook: $shown" assert_unchanged @seen 0 $TMUX rename-session two || fail "rename-session two failed" From 09a2ea190d325523c4eb0dd45b4bc09c09df8f72 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 22 Jul 2026 20:24:21 +0100 Subject: [PATCH 13/85] Update test. --- regress/new-pane-mouse.sh | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/regress/new-pane-mouse.sh b/regress/new-pane-mouse.sh index 81f0e805b..06d594c8d 100644 --- a/regress/new-pane-mouse.sh +++ b/regress/new-pane-mouse.sh @@ -49,11 +49,33 @@ drag() sleep 1 } +# right_click COL ROW KEY +# +# Open the right-click menu at a 1-based position and choose KEY. +right_click() +{ + col="$1" + row="$2" + key="$3" + + seq=$(printf '\033[<2;%s;%sM' "$col" "$row") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + $TMUX2 send-keys -t "$OUTER" "$key" 2>/dev/null + sleep 1 +} + cleanup $TMUX new-session -d -s inner -x 80 -y 24 || exit 1 $TMUX set -g mouse on $TMUX set -g default-command 'sleep 100' +keys=$($TMUX list-keys -T root -F '#{key_command}' MouseDown3Empty) || + fail "list MouseDown3Empty failed" +case "$keys" in +*"New Pane"*"New Window"*) ;; +*) fail "missing empty-area menu binding: $keys" ;; +esac $TMUX2 new-session -d -x 80 -y 24 "$TMUX attach -t inner" || exit 1 sleep 1 @@ -80,6 +102,17 @@ $TMUX break-pane -W -s "$BASE" || fail "break base pane failed" $TMUX resize-pane -t "$BASE" -x10 -y4 || fail "resize floating base failed" $TMUX move-pane -t "$BASE" -P top-left || fail "move floating base failed" +# Right-click in empty window space and choose New Pane. This creates a new +# floating pane and tiles it. +right_click 20 8 p + +id=$($TMUX list-panes -F '#{?pane_active,#{pane_id},}' | tail -n 1) +[ -n "$id" ] || fail "no pane created from empty-area menu" +[ "$id" != "$BASE" ] || fail "empty-area menu did not create a new pane" + +must_equal "$($TMUX display-message -p -t "$id" '#{pane_floating_flag}')" 0 +$TMUX kill-pane -t "$id" || fail "kill empty-area menu pane failed" + # Drag in empty window space with no tiled pane underneath. drag 40 10 50 15 @@ -92,6 +125,12 @@ must_equal "$($TMUX display-message -p -t "$id" '#{pane_top}')" 10 must_equal "$($TMUX display-message -p -t "$id" '#{pane_width}')" 9 must_equal "$($TMUX display-message -p -t "$id" '#{pane_height}')" 4 +TILED=$($TMUX new-pane -PF '#{pane_id}' 'sleep 100') || + fail "new pane from only floating panes failed" +$TMUX join-pane -t "$TILED" || + fail "tile new pane from only floating panes failed" +must_equal "$($TMUX display-message -p -t "$TILED" '#{pane_floating_flag}')" 0 + TILED=$($TMUX new-window -dPF '#{pane_id}' 'sleep 100') || fail "new tiled window failed" $TMUX new-pane -d -M -L -t "$TILED" 'sleep 100' || fail "new-pane -M -L failed" From 9625fe3b5f7d23817f4f0f07b24877cea58a7d7f Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 22 Jul 2026 18:58:48 +0000 Subject: [PATCH 14/85] Show user hooks correctly with show-hooks. --- cmd-show-options.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/cmd-show-options.c b/cmd-show-options.c index 308912e00..edf2b1354 100644 --- a/cmd-show-options.c +++ b/cmd-show-options.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-show-options.c,v 1.73 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-show-options.c,v 1.74 2026/07/22 18:58:48 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -249,13 +249,17 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, const char *name, *array_key; int parent; - if (cmd_get_entry(self) != &cmd_show_hooks_entry) { - o = options_first(oo); - while (o != NULL) { - if (options_table_entry(o) == NULL) + o = options_first(oo); + while (o != NULL) { + if (options_table_entry(o) == NULL) { + name = options_name(o); + if (cmd_get_entry(self) != &cmd_show_hooks_entry) + cmd_show_options_print(self, item, o, NULL, 0); + else if (*name == '@' && (hooks_is_event(name) || + options_get_monitor_data(o) != NULL)) cmd_show_options_print(self, item, o, NULL, 0); - o = options_next(o); } + o = options_next(o); } for (oe = options_table; oe->name != NULL; oe++) { if (~oe->scope & scope) From 2a5389416a2ef4c65e56a1479b1c34cdf234127a Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 22 Jul 2026 19:23:59 +0000 Subject: [PATCH 15/85] Add a menu on empty areas to allow a new pane to be created. --- key-bindings.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/key-bindings.c b/key-bindings.c index ff6111870..55f12bc83 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.189 2026/07/15 10:38:31 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.190 2026/07/22 19:23:59 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -45,6 +45,9 @@ " ''" \ " 'New After' 'w' {new-window -a}" \ " 'New At End' 'W' {new-window}" +#define DEFAULT_EMPTY_MENU \ + " 'New Pane' 'p' {new-pane; join-pane}" \ + " 'New Window' 'w' {new-window}" #define DEFAULT_PANE_MENU \ " '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Top,}' '<' {send -X history-top}" \ " '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}' '>' {send -X history-bottom}" \ @@ -551,6 +554,8 @@ key_bindings_init(void) /* Mouse button 3 down on pane. */ "bind -n MouseDown3Pane { if -Ft= '#{||:#{mouse_any_flag},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}' { select-pane -t=; send -M } { display-menu -t= -xM -yM -T '#[align=centre]#{pane_index} (#{pane_id})' " DEFAULT_PANE_MENU " } }", "bind -n M-MouseDown3Pane { display-menu -t= -xM -yM -T '#[align=centre]#{pane_index} (#{pane_id})' " DEFAULT_PANE_MENU " }", + "bind -n MouseDown3Empty { display-menu -t= -xM -yM -T '#[align=centre]#{window_index}:#{window_name}' " DEFAULT_EMPTY_MENU " }", + "bind -n M-MouseDown3Empty { display-menu -t= -xM -yM -T '#[align=centre]#{window_index}:#{window_name}' " DEFAULT_EMPTY_MENU " }", /* Mouse on scrollbar. */ "bind -n MouseDown1ScrollbarUp { if -Ft= '#{pane_in_mode}' { send -X page-up } {copy-mode -u } }", From 7277712ca8b603d3b11edbb470de50eb1c64bbf1 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 22 Jul 2026 20:12:58 +0000 Subject: [PATCH 16/85] Switch show-options over to using a format and add -F. --- cmd-show-options.c | 251 ++++++++++++++++++++++++++++----------------- hooks.c | 17 ++- tmux.1 | 25 ++++- tmux.h | 4 +- 4 files changed, 200 insertions(+), 97 deletions(-) diff --git a/cmd-show-options.c b/cmd-show-options.c index edf2b1354..0b3329f7c 100644 --- a/cmd-show-options.c +++ b/cmd-show-options.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-show-options.c,v 1.74 2026/07/22 18:58:48 nicm Exp $ */ +/* $OpenBSD: cmd-show-options.c,v 1.75 2026/07/22 20:12:58 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -28,14 +28,23 @@ * Show options. */ +#define SHOW_OPTIONS_TEMPLATE \ + "#{?option_value_only," \ + "#{option_value}," \ + "#{option_name}#{?option_has_array_key," \ + "[#{option_array_key}],}" \ + "#{?option_is_parent,*,}" \ + "#{?option_has_value, " \ + "#{?option_is_string,#{q/a:option_value},#{option_value}},}}" +#define SHOW_HOOKS_MONITOR_TEMPLATE \ + "#{option_name}:#{hook_monitor_target}:#{hook_monitor_format}" + static enum cmd_retval cmd_show_options_exec(struct cmd *, struct cmdq_item *); static void cmd_show_options_print(struct cmd *, struct cmdq_item *, struct options_entry *, const char *, int); -static void cmd_show_hooks_print_monitor(struct cmdq_item *, - struct options_entry *); -static enum cmd_retval cmd_show_hooks_monitor(struct cmd *, struct cmdq_item *, - int, struct options *); +static void cmd_show_hooks_print_monitor(struct cmd *, + struct cmdq_item *, struct options_entry *); static enum cmd_retval cmd_show_options_all(struct cmd *, struct cmdq_item *, int, struct options *); @@ -43,8 +52,8 @@ const struct cmd_entry cmd_show_options_entry = { .name = "show-options", .alias = "show", - .args = { "AgHpqst:vw", 0, 1, NULL }, - .usage = "[-AgHpqsvw] " CMD_TARGET_PANE_USAGE " [option]", + .args = { "AgF:Hpqst:vw", 0, 1, NULL }, + .usage = "[-AgHpqsvw] [-F format] " CMD_TARGET_PANE_USAGE " [option]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -56,8 +65,8 @@ const struct cmd_entry cmd_show_window_options_entry = { .name = "show-window-options", .alias = "showw", - .args = { "gvt:", 0, 1, NULL }, - .usage = "[-gv] " CMD_TARGET_WINDOW_USAGE " [option]", + .args = { "F:gvt:", 0, 1, NULL }, + .usage = "[-gv] [-F format] " CMD_TARGET_WINDOW_USAGE " [option]", .target = { 't', CMD_FIND_WINDOW, CMD_FIND_CANFAIL }, @@ -69,8 +78,8 @@ const struct cmd_entry cmd_show_hooks_entry = { .name = "show-hooks", .alias = NULL, - .args = { "Bgpt:w", 0, 1, NULL }, - .usage = "[-Bgpw] " CMD_TARGET_PANE_USAGE " [hook]", + .args = { "BF:gpt:w", 0, 1, NULL }, + .usage = "[-Bgpw] [-F format] " CMD_TARGET_PANE_USAGE " [hook]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -86,7 +95,7 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) struct options *oo; char *argument, *name = NULL, *cause; char *array_key = NULL; - int window, ambiguous, parent, scope; + int window, ambiguous, parent, print_parent, scope; struct options_entry *o; window = (cmd_get_entry(self) == &cmd_show_window_options_entry); @@ -102,8 +111,14 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_ERROR); } if (cmd_get_entry(self) == &cmd_show_hooks_entry && - args_has(args, 'B')) - return (cmd_show_hooks_monitor(self, item, scope, oo)); + args_has(args, 'B')) { + o = options_first(oo); + while (o != NULL) { + cmd_show_hooks_print_monitor(self, item, o); + o = options_next(o); + } + return (CMD_RETURN_NORMAL); + } return (cmd_show_options_all(self, item, scope, oo)); } argument = format_single_from_target(item, args_string(args, 0)); @@ -136,11 +151,17 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) if (o != NULL) { if (cmd_get_entry(self) == &cmd_show_hooks_entry && args_has(args, 'B')) - cmd_show_hooks_print_monitor(item, o); - else - cmd_show_options_print(self, item, o, array_key, parent); - } - else if (*name == '@') { + cmd_show_hooks_print_monitor(self, item, o); + else { + print_parent = parent; + if (array_key == NULL && options_is_array(o) && + options_array_first(o) == NULL) { + print_parent = 0; + } + cmd_show_options_print(self, item, o, array_key, + print_parent); + } + } else if (*name == '@') { if (args_has(args, 'q')) goto out; cmdq_error(item, "invalid option: %s", argument); @@ -164,22 +185,21 @@ static void cmd_show_options_print(struct cmd *self, struct cmdq_item *item, struct options_entry *o, const char *array_key, int parent) { - struct args *args = cmd_get_args(self); - struct options_array_item *a; - const char *name = options_name(o); - char *value, *tmp = NULL, *escaped; + struct args *args = cmd_get_args(self); + struct options_array_item *a; + struct format_tree *ft; + const char *name = options_name(o); + const char *template = args_get(args, 'F'); + char *value, *line; + int is_hook = 0, is_user = 0; + int has_value = 1; + const struct options_table_entry *oe = options_table_entry(o); - if (array_key != NULL) { - xasprintf(&tmp, "%s[%s]", name, array_key); - name = tmp; - } else { - if (options_is_array(o)) { - a = options_array_first(o); - if (a == NULL) { - if (!args_has(args, 'v')) - cmdq_print(item, "%s", name); - return; - } + if (array_key != NULL) + value = options_to_string(o, array_key, 0); + else if (options_is_array(o)) { + a = options_array_first(o); + if (a != NULL) { while (a != NULL) { array_key = options_array_item_key(a); cmd_show_options_print(self, item, o, array_key, @@ -188,54 +208,106 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item, } return; } - } + if (template == NULL && args_has(args, 'v')) + return; + value = xstrdup(""); + has_value = 0; + } else + value = options_to_string(o, NULL, 0); - value = options_to_string(o, array_key, 0); - if (args_has(args, 'v')) - cmdq_print(item, "%s", value); - else if (options_is_string(o)) { - escaped = args_escape(value); - if (parent) - cmdq_print(item, "%s* %s", name, escaped); - else - cmdq_print(item, "%s %s", name, escaped); - free(escaped); + if (template == NULL) + template = SHOW_OPTIONS_TEMPLATE; + + if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)) + is_hook = 1; + else if (oe == NULL) + is_user = 1; + + ft = format_create_from_target(item); + format_add(ft, "option_name", "%s", name); + format_add(ft, "option_value", "%s", value); + format_add(ft, "option_value_only", "%d", args_has(args, 'v')); + format_add(ft, "option_is_parent", "%d", parent); + format_add(ft, "option_is_array", "%d", options_is_array(o)); + format_add(ft, "option_is_string", "%d", options_is_string(o)); + format_add(ft, "option_is_hook", "%d", is_hook); + format_add(ft, "option_is_user", "%d", is_user); + format_add(ft, "option_has_value", "%d", has_value); + if (array_key != NULL) { + format_add(ft, "option_array_key", "%s", array_key); + format_add(ft, "option_has_array_key", "1"); } else { - if (parent) - cmdq_print(item, "%s* %s", name, value); - else - cmdq_print(item, "%s %s", name, value); + format_add(ft, "option_array_key", "%s", ""); + format_add(ft, "option_has_array_key", "0"); } - free(value); + line = format_expand(ft, template); + format_free(ft); - free(tmp); + cmdq_print(item, "%s", line); + free(line); + free(value); } static void -cmd_show_hooks_print_monitor(struct cmdq_item *item, struct options_entry *o) +cmd_show_hooks_print_monitor(struct cmd *self, struct cmdq_item *item, + struct options_entry *o) { - char *value; + struct args *args = cmd_get_args(self); + struct format_tree *ft; + enum monitor_type type; + const char *template = args_get(args, 'F'), *format; + char *value, *target, *line; + int id; value = hooks_monitor_to_string(o); if (value == NULL) return; - cmdq_print(item, "%s", value); - free(value); -} - -/* Show all hook monitors. */ -static enum cmd_retval -cmd_show_hooks_monitor(__unused struct cmd *self, struct cmdq_item *item, - __unused int scope, struct options *oo) -{ - struct options_entry *o; - - o = options_first(oo); - while (o != NULL) { - cmd_show_hooks_print_monitor(item, o); - o = options_next(o); + if (!hooks_monitor_get(o, &type, &id, &format)) { + free(value); + return; } - return (CMD_RETURN_NORMAL); + if (template == NULL) + template = SHOW_HOOKS_MONITOR_TEMPLATE; + + switch (type) { + case MONITOR_SESSION: + target = xstrdup(""); + break; + case MONITOR_PANE: + xasprintf(&target, "%%%d", id); + break; + case MONITOR_ALL_PANES: + target = xstrdup("%*"); + break; + case MONITOR_WINDOW: + xasprintf(&target, "@%d", id); + break; + case MONITOR_ALL_WINDOWS: + target = xstrdup("@*"); + break; + } + + ft = format_create_from_target(item); + format_add(ft, "option_name", "%s", options_name(o)); + format_add(ft, "option_value", "%s", value); + format_add(ft, "option_value_only", "%d", 0); + format_add(ft, "option_is_parent", "%d", 0); + format_add(ft, "option_is_array", "%d", 0); + format_add(ft, "option_is_string", "%d", 1); + format_add(ft, "option_is_hook", "%d", 1); + format_add(ft, "option_is_user", "%d", 1); + format_add(ft, "option_has_value", "%d", 1); + format_add(ft, "option_array_key", "%s", ""); + format_add(ft, "option_has_array_key", "0"); + format_add(ft, "hook_monitor_target", "%s", target); + format_add(ft, "hook_monitor_format", "%s", format); + line = format_expand(ft, template); + format_free(ft); + + cmdq_print(item, "%s", line); + free(line); + free(target); + free(value); } static enum cmd_retval @@ -245,18 +317,26 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, struct args *args = cmd_get_args(self); const struct options_table_entry *oe; struct options_entry *o; - struct options_array_item *a; - const char *name, *array_key; - int parent; + const char *name; + int parent, is_user_hook; o = options_first(oo); while (o != NULL) { if (options_table_entry(o) == NULL) { name = options_name(o); - if (cmd_get_entry(self) != &cmd_show_hooks_entry) - cmd_show_options_print(self, item, o, NULL, 0); - else if (*name == '@' && (hooks_is_event(name) || - options_get_monitor_data(o) != NULL)) + is_user_hook = 0; + if (*name == '@') { + if (hooks_is_event(name) || + options_get_monitor_data(o) != NULL) { + is_user_hook = 1; + } + } + if (cmd_get_entry(self) != &cmd_show_hooks_entry) { + if (!is_user_hook || args_has(args, 'H')) { + cmd_show_options_print(self, item, o, + NULL, 0); + } + } else if (is_user_hook) cmd_show_options_print(self, item, o, NULL, 0); } o = options_next(o); @@ -283,24 +363,7 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, } else parent = 0; - if (!options_is_array(o)) - cmd_show_options_print(self, item, o, NULL, parent); - else if ((a = options_array_first(o)) == NULL) { - if (!args_has(args, 'v')) { - name = options_name(o); - if (parent) - cmdq_print(item, "%s*", name); - else - cmdq_print(item, "%s", name); - } - } else { - while (a != NULL) { - array_key = options_array_item_key(a); - cmd_show_options_print(self, item, o, array_key, - parent); - a = options_array_next(a); - } - } + cmd_show_options_print(self, item, o, NULL, parent); } return (CMD_RETURN_NORMAL); } diff --git a/hooks.c b/hooks.c index ae7d90de4..bd0d1d732 100644 --- a/hooks.c +++ b/hooks.c @@ -1,4 +1,4 @@ -/* $OpenBSD: hooks.c,v 1.14 2026/07/13 22:03:08 nicm Exp $ */ +/* $OpenBSD: hooks.c,v 1.15 2026/07/22 20:12:58 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -472,3 +472,18 @@ hooks_monitor_to_string(struct options_entry *o) } return (s); } + +/* Get the parts of a hook monitor. */ +int +hooks_monitor_get(struct options_entry *o, enum monitor_type *type, int *id, + const char **format) +{ + struct hook_monitor *hm = options_get_monitor_data(o); + + if (hm == NULL) + return (0); + *type = hm->type; + *id = hm->id; + *format = hm->format; + return (1); +} diff --git a/tmux.1 b/tmux.1 index c3d66e73f..1a1f9fef5 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1147 2026/07/22 08:19:14 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1148 2026/07/22 20:12:58 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -4689,6 +4689,7 @@ the result would be the default background and a blue foreground. .Tg show .It Xo Ic show\-options .Op Fl AgHpqsvw +.Op Fl F Ar format .Op Fl t Ar target\-pane .Op Ar option .Xc @@ -4716,6 +4717,10 @@ Global session or window options are listed if is used. .Fl v shows only the option value, not the name. +With +.Fl F , +.Ar format +is used for each option. If .Fl q is set, no error will be returned if @@ -6593,6 +6598,7 @@ run immediately. .It Xo Ic show\-hooks .Op Fl Bgpw +.Op Fl F Ar format .Op Fl t Ar target\-pane .Op Ar hook .Xc @@ -6605,6 +6611,10 @@ With shows the subscriptions installed with .Em set\-hook .Fl B . +With +.Fl F , +.Ar format +is used for each hook. .El .Sh MOUSE SUPPORT If the @@ -7269,6 +7279,8 @@ The following variables are available, where appropriate: .It Li "hook" Ta "" Ta "Name of running hook, if any" .It Li "hook_client" Ta "" Ta "Name of client where hook was run, if any" .It Li "hook_last" Ta "" Ta "Previous value for a monitor hook" +.It Li "hook_monitor_format" Ta "" Ta "Format for a monitor hook" +.It Li "hook_monitor_target" Ta "" Ta "Target for a monitor hook" .It Li "hook_pane" Ta "" Ta "ID of pane where hook was run, if any" .It Li "hook_session" Ta "" Ta "ID of session where hook was run, if any" .It Li "hook_session_name" Ta "" Ta "Name of session where hook was run, if any" @@ -7310,6 +7322,17 @@ The following variables are available, where appropriate: .It Li "next_session_id" Ta "" Ta "Unique session ID for next new session" .It Li "next_window_active" Ta "" Ta "1 if next window in W: loop is active" .It Li "next_window_index" Ta "" Ta "Index of next window in W: loop" +.It Li "option_array_key" Ta "" Ta "Array key, if option is an array item" +.It Li "option_has_array_key" Ta "" Ta "1 if option array key is present" +.It Li "option_has_value" Ta "" Ta "1 if option value is present" +.It Li "option_is_array" Ta "" Ta "1 if option is an array" +.It Li "option_is_hook" Ta "" Ta "1 if option is a hook" +.It Li "option_is_parent" Ta "" Ta "1 if option is inherited" +.It Li "option_is_string" Ta "" Ta "1 if option is a string" +.It Li "option_is_user" Ta "" Ta "1 if option is a user option" +.It Li "option_name" Ta "" Ta "Name of option" +.It Li "option_value" Ta "" Ta "Value of option" +.It Li "option_value_only" Ta "" Ta "1 if only option values will be shown" .It Li "origin_flag" Ta "" Ta "Pane origin flag" .It Li "pane_active" Ta "" Ta "1 if active pane" .It Li "pane_at_bottom" Ta "" Ta "1 if pane is at the bottom of window" diff --git a/tmux.h b/tmux.h index 04ad25b82..0e40a5ea2 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1413 2026/07/22 08:19:14 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1414 2026/07/22 20:12:58 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2742,6 +2742,8 @@ void hooks_monitor_add(struct cmdq_item *, struct options *, void hooks_monitor_remove(struct options *, const char *); void hooks_monitor_free(void *); char *hooks_monitor_to_string(struct options_entry *); +int hooks_monitor_get(struct options_entry *, enum monitor_type *, int *, + const char **); /* options.c */ struct options *options_create(struct options *); From 38feebb5d2a5b2be475aca56c406fbdf57cee13d Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 23 Jul 2026 08:06:09 +0100 Subject: [PATCH 17/85] Show options tests. --- regress/options-array.sh | 33 ++++++++ regress/options-values.sh | 24 ++++++ regress/set-hook-B.sh | 11 +++ regress/show-options-output.sh | 136 +++++++++++++++++++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 regress/show-options-output.sh diff --git a/regress/options-array.sh b/regress/options-array.sh index fbaff6ae2..d707fb7d9 100644 --- a/regress/options-array.sh +++ b/regress/options-array.sh @@ -140,6 +140,24 @@ update-environment[notify] EEE" # sorts by ascending numeric key and keeps the gap at [1]. check_ok set -g status-format "" check_array "-g status-format" "status-format" +out=$($TMUX show -gF \ + '#{option_name}:#{option_has_value}:#{option_value}:#{option_is_array}:#{option_has_array_key}:#{option_array_key}' \ + status-format 2>&1) +[ "$out" = "status-format:0::1:0:" ] || { + echo "show -F empty status-format failed." + echo "Expected: 'status-format:0::1:0:'" + echo "But got: '$out'" + exit 1 +} +out=$($TMUX show -gvF \ + '#{option_name}:#{option_value_only}:#{option_has_value}:#{option_value}' \ + status-format 2>&1) +[ "$out" = "status-format:1:0:" ] || { + echo "show -vF empty status-format failed." + echo "Expected: 'status-format:1:0:'" + echo "But got: '$out'" + exit 1 +} check_ok set -g status-format[5] "five" check_ok set -g status-format[0] "zero" check_ok set -g status-format[2] "two" @@ -154,6 +172,21 @@ status-format[5] five status-format[foo-bar] foo-bar status-format[xterm-256color] xterm status-format[zoom] zoom" +out=$($TMUX show -gF \ + '#{option_name}:#{option_array_key}:#{option_is_array}:#{option_has_value}:#{option_has_array_key}:#{option_value}' \ + status-format 2>&1) +[ "$out" = "$(printf '%s' 'status-format:0:1:1:1:zero +status-format:1:1:1:1:one +status-format:2:1:1:1:two +status-format:5:1:1:1:five +status-format:foo-bar:1:1:1:foo-bar +status-format:xterm-256color:1:1:1:xterm +status-format:zoom:1:1:1:zoom')" ] || { + echo "show -F status-format failed." + echo "Expected formatted array output" + echo "But got:"; printf '%s\n' "$out" + exit 1 +} check_value "-gv status-format[01]" "one" check_ok set -gu status-format[zoom] check_value "-gv status-format[zoom]" "" diff --git a/regress/options-values.sh b/regress/options-values.sh index 75da52e01..be08cc136 100644 --- a/regress/options-values.sh +++ b/regress/options-values.sh @@ -168,6 +168,30 @@ check_ok set -g @str "foo" check_ok set -ga @str "bar" check_value "-gv @str" "foobar" +# --- show -F custom format ------------------------------------------------ +# +# show-options and show-hooks use a format for their output, with the default +# format preserving the traditional output. +check_ok set -g @str "two words" +check_value "-g @str" '@str "two words"' +out=$($TMUX show -gF \ + '#{option_name}=#{option_value}=#{option_is_user}=#{option_is_string}=#{option_value_only}' \ + @str 2>&1) +[ "$out" = "@str=two words=1=1=0" ] || { + echo "show -F @str failed." + echo "Expected: '@str=two words=1=1=0'" + echo "But got: '$out'" + exit 1 +} +out=$($TMUX show -gvF '#{option_name}=#{option_value}=#{option_value_only}' \ + @str 2>&1) +[ "$out" = "@str=two words=1" ] || { + echo "show -vF @str failed." + echo "Expected: '@str=two words=1'" + echo "But got: '$out'" + exit 1 +} + # --- -F expands at set time ----------------------------------------------- # # With -F the value is expanded as a format once, at set time; without -F it is diff --git a/regress/set-hook-B.sh b/regress/set-hook-B.sh index 0226cad00..a6b93ce04 100644 --- a/regress/set-hook-B.sh +++ b/regress/set-hook-B.sh @@ -67,6 +67,12 @@ shown=$($TMUX show-hooks -g -B @session-name) || fail "show-hooks -B failed" [ "$shown" = '@session-name::#{session_name}' ] || fail "unexpected show-hooks -B output: $shown" +shown=$($TMUX show-hooks -g -BF \ + '#{option_name}:#{hook_monitor_target}:#{hook_monitor_format}:#{option_value}:#{option_is_hook}:#{option_is_user}' \ + @session-name) || + fail "show-hooks -BF failed" +[ "$shown" = '@session-name::#{session_name}:@session-name::#{session_name}:1:1' ] || + fail "unexpected show-hooks -BF output: $shown" shown=$($TMUX show-hooks -g) || fail "show-hooks -g failed" echo "$shown" | grep -q '^@session-name ' || @@ -116,6 +122,11 @@ $TMUX set -g @pane-seen 0 || fail "set @pane-seen failed" $TMUX set-hook -g -B "@pane:%$pane_number:#{pane_width}" \ 'set -g @pane-seen "#{hook_session}:#{hook_window}:#{hook_window_index}:#{hook_pane}:#{hook_value}"' || fail "set-hook -B pane selector failed" +shown=$($TMUX show-hooks -g -BF \ + '#{option_name}:#{hook_monitor_target}:#{hook_monitor_format}' @pane) || + fail "show-hooks -BF pane failed" +[ "$shown" = "@pane:%$pane_number:#{pane_width}" ] || + fail "unexpected show-hooks -BF pane output: $shown" assert_unchanged @pane-seen 0 $TMUX set-hook -g -B "@pane:%$pane_number:#{@pane-value}" \ 'set -g @pane-seen "#{hook_session}:#{hook_window}:#{hook_window_index}:#{hook_pane}:#{hook_value}"' || diff --git a/regress/show-options-output.sh b/regress/show-options-output.sh new file mode 100644 index 000000000..6b36da5d1 --- /dev/null +++ b/regress/show-options-output.sh @@ -0,0 +1,136 @@ +#!/bin/sh + +# Tests of the default output from show-options and show-hooks. This is +# intended to guard the output compatibility when the implementation is changed +# to use formats internally, so it deliberately does not use show -F. + +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 + +check_value() +{ + out=$($TMUX show $1 2>&1) + if [ "$out" != "$(printf '%s' "$2")" ]; then + echo "show $1 failed." + echo "Expected:"; printf '%s\n' "$2" + echo "But got:"; printf '%s\n' "$out" + exit 1 + fi +} + +check_hooks() +{ + out=$($TMUX show-hooks $1 2>&1) + if [ "$out" != "$(printf '%s' "$2")" ]; then + echo "show-hooks $1 failed." + echo "Expected:"; printf '%s\n' "$2" + echo "But got:"; printf '%s\n' "$out" + exit 1 + fi +} + +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive)" != "alive" ]; then + echo "Server died: $1" + exit 1 + fi +} + +$TMUX new-session -d -s main -x 80 -y 24 || exit 1 + +# Scalar options: strings are escaped, non-strings are not, and -v suppresses +# the option name. +check_ok set -g @words "two words" +check_value "-g @words" '@words "two words"' +check_value "-gv @words" 'two words' +check_ok set -g display-time 1234 +check_value "-g display-time" 'display-time 1234' +check_value "-gv display-time" '1234' + +# Inherited options shown with -A are marked with an asterisk. +check_ok set -g status-left "GLOBAL" +check_ok set -u status-left +out=$($TMUX show -A 2>/dev/null | grep '^status-left\*') +if [ "$out" != "status-left* GLOBAL" ]; then + echo "show -A did not mark inherited status-left." + echo "But got:"; printf '%s\n' "$out" + exit 1 +fi + +# Arrays: an empty array prints just the name unless -v is used; populated +# arrays include every key, including key 0. +check_ok set -g status-format "" +check_value "-g status-format" 'status-format' +check_value "-gv status-format" '' +out=$($TMUX show -A 2>/dev/null | grep '^status-format\*') +if [ "$out" != "status-format*" ]; then + echo "show -A did not mark inherited empty status-format." + echo "But got:"; printf '%s\n' "$out" + exit 1 +fi +check_value "-A status-format" 'status-format' +check_ok set -g update-environment "AAA BBB,CCC" +check_value "-g update-environment" 'update-environment[0] AAA +update-environment[1] BBB +update-environment[2] CCC' +check_value "-gv update-environment[0]" 'AAA' + +# Hooks use the same array output style, and monitor hooks have their own +# default output form. +check_ok set-hook -g window-renamed[first] "display-message renamed" +check_hooks "-g window-renamed" 'window-renamed[first] display-message renamed' +check_ok set-hook -g -B '@monitor:%*:#{pane_width}' +check_hooks "-g -B @monitor" '@monitor:%*:#{pane_width}' +check_ok set-hook -g @user-hook "display-message user" +check_hooks "-g @user-hook" '@user-hook "display-message user"' +out=$($TMUX show -g 2>&1) +echo "$out" | grep -q "^@monitor ''$" && { + echo "show -g showed monitor hook without -H." + echo "But got:"; printf '%s\n' "$out" + exit 1 +} +echo "$out" | grep -q '^@user-hook "display-message user"$' && { + echo "show -g showed user hook without -H." + echo "But got:"; printf '%s\n' "$out" + exit 1 +} +out=$($TMUX show -gH 2>&1) +echo "$out" | grep -q "^@monitor ''$" || { + echo "show -gH did not show monitor hook." + echo "But got:"; printf '%s\n' "$out" + exit 1 +} +echo "$out" | grep -q '^@user-hook "display-message user"$' || { + echo "show -gH did not show user hook." + echo "But got:"; printf '%s\n' "$out" + exit 1 +} +out=$($TMUX show-hooks -g 2>&1) +echo "$out" | grep -q "^@monitor ''$" || { + echo "show-hooks -g did not show monitor hook." + echo "But got:"; printf '%s\n' "$out" + exit 1 +} +echo "$out" | grep -q '^@user-hook "display-message user"$' || { + echo "show-hooks -g did not show user hook." + echo "But got:"; printf '%s\n' "$out" + exit 1 +} + +assert_alive "after show-options output tests" + +$TMUX kill-server 2>/dev/null +exit 0 From 26bdd2b511fbfe01a05759745daf3125fdebd110 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 23 Jul 2026 08:59:04 +0100 Subject: [PATCH 18/85] On macOS, look for utf8proc with no arguments and use it if provided. Only require a flag if it is not available. --- configure.ac | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 51700a2ad..919f5d331 100644 --- a/configure.ac +++ b/configure.ac @@ -415,7 +415,22 @@ AC_ARG_ENABLE( utf8proc, AS_HELP_STRING(--enable-utf8proc, use utf8proc if it is installed) ) +try_utf8proc=no +require_utf8proc=no if test "x$enable_utf8proc" = xyes; then + try_utf8proc=yes +elif test "x$enable_utf8proc" = x; then + case "$host_os" in + *darwin*) + try_utf8proc=yes + require_utf8proc=yes + ;; + esac +fi +if test "x$try_utf8proc" = xyes; then + SAVED_AM_CPPFLAGS="$AM_CPPFLAGS" + SAVED_OLD_CPPFLAGS="$CPPFLAGS" + SAVED_LIBS="$LIBS" PKG_CHECK_MODULES( LIBUTF8PROC, libutf8proc, @@ -423,7 +438,8 @@ if test "x$enable_utf8proc" = xyes; then AM_CPPFLAGS="$LIBUTF8PROC_CFLAGS $AM_CPPFLAGS" CPPFLAGS="$LIBUTF8PROC_CFLAGS $SAVED_CPPFLAGS" LIBS="$LIBUTF8PROC_LIBS $LIBS" - ] + ], + [:] ) AC_CHECK_HEADER(utf8proc.h, enable_utf8proc=yes, enable_utf8proc=no) if test "x$enable_utf8proc" = xyes; then @@ -437,7 +453,14 @@ if test "x$enable_utf8proc" = xyes; then if test "x$enable_utf8proc" = xyes; then AC_DEFINE(HAVE_UTF8PROC) else - AC_MSG_ERROR("utf8proc not found") + AM_CPPFLAGS="$SAVED_AM_CPPFLAGS" + CPPFLAGS="$SAVED_OLD_CPPFLAGS" + LIBS="$SAVED_LIBS" + if test "x$require_utf8proc" = xyes; then + enable_utf8proc= + else + AC_MSG_ERROR("utf8proc not found") + fi fi fi AM_CONDITIONAL(HAVE_UTF8PROC, [test "x$enable_utf8proc" = xyes]) From fb88172346b07fe70a385cee593e64091cb749a7 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 23 Jul 2026 08:59:49 +0100 Subject: [PATCH 19/85] Turn ASAN off again on macOS. --- configure.ac | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/configure.ac b/configure.ac index 919f5d331..40b10feb6 100644 --- a/configure.ac +++ b/configure.ac @@ -76,9 +76,7 @@ AM_CONDITIONAL(IS_OPTIMIZED, test "x$enable_optimizations" = xyes) # Is this --enable-asan? AC_ARG_ENABLE( asan, - AS_HELP_STRING(--enable-asan, enable ASAN build flags), - , - [case "x$host_os" in *darwin*) enable_asan=yes;; esac] + AS_HELP_STRING(--enable-asan, enable ASAN build flags) ) AM_CONDITIONAL(IS_ASAN, test "x$enable_asan" = xyes) From 7568c192767a1f34547e0d3ed2db5a9e0ab7f4d1 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 23 Jul 2026 09:12:16 +0100 Subject: [PATCH 20/85] Log jemalloc version if in use. --- proc.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/proc.c b/proc.c index 7da1b430c..d3536b17e 100644 --- a/proc.c +++ b/proc.c @@ -31,6 +31,10 @@ #include #endif +#ifdef HAVE_JEMALLOC +#include +#endif + #include "tmux.h" struct tmuxproc { @@ -181,6 +185,10 @@ proc_start(const char *name) { struct tmuxproc *tp; struct utsname u; +#ifdef HAVE_JEMALLOC + const char *version; + size_t size = sizeof version; +#endif log_open(name); setproctitle("%s (%s)", name, socket_path); @@ -195,6 +203,11 @@ proc_start(const char *name) #ifdef HAVE_UTF8PROC log_debug("using utf8proc %s", utf8proc_version()); #endif +#ifdef HAVE_JEMALLOC + if (mallctl("version", &version, &size, NULL, 0) != 0) + version = "(unknown version)"; + log_debug("using jemalloc %s", version); +#endif #ifdef NCURSES_VERSION log_debug("using ncurses %s %06u", NCURSES_VERSION, NCURSES_VERSION_PATCH); #endif From a10ed3233c82d2b821e34cb87f0278b625dc8ba4 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 23 Jul 2026 09:26:42 +0100 Subject: [PATCH 21/85] Require jemalloc on macOS. --- configure.ac | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 40b10feb6..433cc1c3c 100644 --- a/configure.ac +++ b/configure.ac @@ -605,7 +605,22 @@ AC_ARG_ENABLE( jemalloc, AS_HELP_STRING(--enable-jemalloc, use jemalloc if it is installed) ) +try_jemalloc=no +require_jemalloc=no if test "x$enable_jemalloc" = xyes; then + try_jemalloc=yes +elif test "x$enable_jemalloc" = x; then + case "$host_os" in + *darwin*) + try_jemalloc=yes + require_jemalloc=yes + ;; + esac +fi +if test "x$try_jemalloc" = xyes; then + SAVED_AM_CPPFLAGS="$AM_CPPFLAGS" + SAVED_OLD_CPPFLAGS="$CPPFLAGS" + SAVED_LIBS="$LIBS" PKG_CHECK_MODULES( JEMALLOC, jemalloc, @@ -614,8 +629,20 @@ if test "x$enable_jemalloc" = xyes; then CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS" LIBS="$LIBS $JEMALLOC_LIBS" ], - AC_MSG_ERROR("jemalloc not found") + enable_jemalloc=no ) + if test "x$enable_jemalloc" = xno; then + AM_CPPFLAGS="$SAVED_AM_CPPFLAGS" + CPPFLAGS="$SAVED_OLD_CPPFLAGS" + LIBS="$SAVED_LIBS" + if test "x$require_jemalloc" = xyes; then + enable_jemalloc= + else + AC_MSG_ERROR("jemalloc not found") + fi + else + enable_jemalloc=yes + fi fi # Check for CMSG_DATA. On some platforms like HP-UX this requires UNIX 95 @@ -962,6 +989,20 @@ case "$host_os" in AC_MSG_NOTICE([]) AC_MSG_ERROR([must give --enable-utf8proc or --disable-utf8proc]) fi + # + # macOS calloc(3) does not appear to always zero memory correctly, + # so complain and suggest using jemalloc instead. + # + if test "x$enable_jemalloc" = x; then + AC_MSG_NOTICE([]) + AC_MSG_NOTICE([ macOS calloc(3) appears not to correctly]) + AC_MSG_NOTICE([ zero allocations in some circumstances;]) + AC_MSG_NOTICE([ to avoid this, configuring with]) + AC_MSG_NOTICE([ --enable-jemalloc is recommended. To build]) + AC_MSG_NOTICE([ without anyway, use --disable-jemalloc]) + AC_MSG_NOTICE([]) + AC_MSG_ERROR([must give --enable-jemalloc or --disable-jemalloc]) + fi ;; *dragonfly*) AC_MSG_RESULT(dragonfly) From 6049298140210612128219d37c07e64dd230b9e3 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 23 Jul 2026 09:26:57 +0100 Subject: [PATCH 22/85] Show what we have found during configure. --- configure.ac | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 433cc1c3c..53c513acc 100644 --- a/configure.ac +++ b/configure.ac @@ -238,6 +238,7 @@ AC_LIBOBJ(getopt_long) # Look for libevent. Try libevent_core or libevent with pkg-config first then # look for the library. +libevent_version=off PKG_CHECK_MODULES( LIBEVENT_CORE, [libevent_core >= 2], @@ -246,6 +247,8 @@ PKG_CHECK_MODULES( CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS" LIBS="$LIBEVENT_CORE_LIBS $LIBS" found_libevent=yes + libevent_version=`$PKG_CONFIG --modversion libevent_core 2>/dev/null` + test "x$libevent_version" = x && libevent_version=on ], found_libevent=no ) @@ -258,6 +261,8 @@ if test x$found_libevent = xno; then CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS" LIBS="$LIBEVENT_LIBS $LIBS" found_libevent=yes + libevent_version=`$PKG_CONFIG --modversion libevent 2>/dev/null` + test "x$libevent_version" = x && libevent_version=on ], found_libevent=no ) @@ -266,7 +271,8 @@ if test x$found_libevent = xno; then AC_SEARCH_LIBS( event_init, [event_core event event-1.4], - found_libevent=yes, + [found_libevent=yes + libevent_version=on], found_libevent=no ) fi @@ -293,6 +299,7 @@ fi # Look for ncurses or curses. Try pkg-config first then directly for the # library. +ncurses_version=off PKG_CHECK_MODULES( LIBTINFOW, tinfow, @@ -301,6 +308,8 @@ PKG_CHECK_MODULES( CPPFLAGS="$LIBTINFOW_CFLAGS $SAVED_CPPFLAGS" LIBS="$LIBTINFOW_LIBS $LIBS" found_ncurses=yes + ncurses_version=`$PKG_CONFIG --modversion tinfow 2>/dev/null` + test "x$ncurses_version" = x && ncurses_version=on ], found_ncurses=no ) @@ -313,6 +322,8 @@ if test "x$found_ncurses" = xno; then CPPFLAGS="$LIBTINFO_CFLAGS $SAVED_CPPFLAGS" LIBS="$LIBTINFO_LIBS $LIBS" found_ncurses=yes + ncurses_version=`$PKG_CONFIG --modversion tinfo 2>/dev/null` + test "x$ncurses_version" = x && ncurses_version=on ], found_ncurses=no ) @@ -326,6 +337,8 @@ if test "x$found_ncurses" = xno; then CPPFLAGS="$LIBNCURSESW_CFLAGS $SAVED_CPPFLAGS" LIBS="$LIBNCURSESW_LIBS $LIBS" found_ncurses=yes + ncurses_version=`$PKG_CONFIG --modversion ncursesw 2>/dev/null` + test "x$ncurses_version" = x && ncurses_version=on ], found_ncurses=no ) @@ -339,6 +352,8 @@ if test "x$found_ncurses" = xno; then CPPFLAGS="$LIBNCURSES_CFLAGS $SAVED_CPPFLAGS" LIBS="$LIBNCURSES_LIBS $LIBS" found_ncurses=yes + ncurses_version=`$PKG_CONFIG --modversion ncurses 2>/dev/null` + test "x$ncurses_version" = x && ncurses_version=on ], found_ncurses=no ) @@ -347,7 +362,8 @@ if test "x$found_ncurses" = xno; then AC_SEARCH_LIBS( setupterm, [tinfow tinfo terminfo ncursesw ncurses], - found_ncurses=yes, + [found_ncurses=yes + ncurses_version=on], found_ncurses=no ) if test "x$found_ncurses" = xyes; then @@ -377,6 +393,7 @@ else LIBS="$LIBS -lcurses" CPPFLAGS="$CPPFLAGS -DHAVE_CURSES_H" AC_DEFINE(HAVE_CURSES_H) + ncurses_version=on else AC_MSG_ERROR("curses not found") fi @@ -387,6 +404,7 @@ AC_CHECK_FUNCS([ \ ]) # Look for utempter. +utempter_version=off AC_ARG_ENABLE( utempter, AS_HELP_STRING(--enable-utempter, use utempter if it is installed) @@ -403,12 +421,14 @@ if test "x$enable_utempter" = xyes; then fi if test "x$enable_utempter" = xyes; then AC_DEFINE(HAVE_UTEMPTER) + utempter_version=on else AC_MSG_ERROR("utempter not found") fi fi # Look for utf8proc. +utf8proc_version=off AC_ARG_ENABLE( utf8proc, AS_HELP_STRING(--enable-utf8proc, use utf8proc if it is installed) @@ -436,6 +456,8 @@ if test "x$try_utf8proc" = xyes; then AM_CPPFLAGS="$LIBUTF8PROC_CFLAGS $AM_CPPFLAGS" CPPFLAGS="$LIBUTF8PROC_CFLAGS $SAVED_CPPFLAGS" LIBS="$LIBUTF8PROC_LIBS $LIBS" + utf8proc_version=`$PKG_CONFIG --modversion libutf8proc 2>/dev/null` + test "x$utf8proc_version" = x && utf8proc_version=on ], [:] ) @@ -450,10 +472,12 @@ if test "x$try_utf8proc" = xyes; then fi if test "x$enable_utf8proc" = xyes; then AC_DEFINE(HAVE_UTF8PROC) + test "x$utf8proc_version" = xoff && utf8proc_version=on else AM_CPPFLAGS="$SAVED_AM_CPPFLAGS" CPPFLAGS="$SAVED_OLD_CPPFLAGS" LIBS="$SAVED_LIBS" + utf8proc_version=off if test "x$require_utf8proc" = xyes; then enable_utf8proc= else @@ -464,6 +488,7 @@ fi AM_CONDITIONAL(HAVE_UTF8PROC, [test "x$enable_utf8proc" = xyes]) # Check for systemd support. +systemd_version=off AC_ARG_ENABLE( systemd, AS_HELP_STRING(--enable-systemd, enable systemd integration) @@ -477,6 +502,8 @@ if test x"$enable_systemd" = xyes; then CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS" LIBS="$SYSTEMD_LIBS $LIBS" found_systemd=yes + systemd_version=`$PKG_CONFIG --modversion libsystemd 2>/dev/null` + test "x$systemd_version" = x && systemd_version=on ], found_systemd=no ) @@ -601,6 +628,7 @@ if test "x$found_malloc_trim" = xyes; then fi # Build against jemalloc if requested. +jemalloc_version=off AC_ARG_ENABLE( jemalloc, AS_HELP_STRING(--enable-jemalloc, use jemalloc if it is installed) @@ -628,6 +656,8 @@ if test "x$try_jemalloc" = xyes; then AM_CPPFLAGS="$JEMALLOC_CFLAGS $AM_CPPFLAGS" CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS" LIBS="$LIBS $JEMALLOC_LIBS" + jemalloc_version=`$PKG_CONFIG --modversion jemalloc 2>/dev/null` + test "x$jemalloc_version" = x && jemalloc_version=on ], enable_jemalloc=no ) @@ -635,6 +665,7 @@ if test "x$try_jemalloc" = xyes; then AM_CPPFLAGS="$SAVED_AM_CPPFLAGS" CPPFLAGS="$SAVED_OLD_CPPFLAGS" LIBS="$SAVED_LIBS" + jemalloc_version=off if test "x$require_jemalloc" = xyes; then enable_jemalloc= else @@ -642,6 +673,8 @@ if test "x$try_jemalloc" = xyes; then fi else enable_jemalloc=yes + AC_DEFINE(HAVE_JEMALLOC) + test "x$jemalloc_version" = xoff && jemalloc_version=on fi fi @@ -1090,6 +1123,26 @@ AC_MSG_CHECKING(lock-command) AC_MSG_RESULT($DEFAULT_LOCK_CMD) AC_SUBST(DEFAULT_LOCK_CMD) +# Print a summary. +AC_MSG_NOTICE([]) +if test "x$enable_asan" = xyes; then + AC_MSG_NOTICE([ASAN: on]) +else + AC_MSG_NOTICE([ASAN: off]) +fi +if test "x$enable_debug" = xyes; then + AC_MSG_NOTICE([debug: on]) +else + AC_MSG_NOTICE([debug: off]) +fi +AC_MSG_NOTICE([jemalloc: $jemalloc_version]) +AC_MSG_NOTICE([libevent: $libevent_version]) +AC_MSG_NOTICE([ncurses: $ncurses_version]) +AC_MSG_NOTICE([systemd: $systemd_version]) +AC_MSG_NOTICE([utempter: $utempter_version]) +AC_MSG_NOTICE([utf8proc: $utf8proc_version]) +AC_MSG_NOTICE([]) + # Save our CFLAGS/CPPFLAGS/LDFLAGS for the Makefile and restore the old user # variables. AC_SUBST(AM_CPPFLAGS) From 689e6f8fac4e48c1d25bd61ca3061e9b590f6246 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 23 Jul 2026 09:30:16 +0100 Subject: [PATCH 23/85] Add jemalloc to regress dependencies. --- .github/workflows/regress.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 8f6633624..95935f2f8 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -61,6 +61,7 @@ jobs: automake \ bison \ libevent \ + jemalloc \ make \ ncurses \ utf8proc \ From a2f3bdb601000c0a6ed436029bbd0664179e5d05 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 23 Jul 2026 10:38:27 +0100 Subject: [PATCH 24/85] fill-character test bits. --- regress/screen-redraw-fill-character.sh | 63 +++++++++++++++++++ regress/screen-redraw-floating.sh | 6 +- .../floating-clip-window-edge.result | 18 +++--- .../floating-outside.result | 18 +++--- 4 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 regress/screen-redraw-fill-character.sh diff --git a/regress/screen-redraw-fill-character.sh b/regress/screen-redraw-fill-character.sh new file mode 100644 index 000000000..deb7b0a11 --- /dev/null +++ b/regress/screen-redraw-fill-character.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +# Exercise fill-character as a format. The window is smaller than the attached +# client so OUTSIDE spans exist, then the only tiled pane is removed so EMPTY +# spans exist inside the window around a floating pane. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +TMP=$(mktemp) +trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" \ + 0 1 15 + +fail() { + echo "$*" >&2 + exit 1 +} + +must_equal() { + if [ "$1" != "$2" ]; then + fail "expected '$2', got '$1'" + fi +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +$TMUX2 new -d -x40 -y12 "sh -c 'printf base; exec sleep 100'" || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 set -g window-size manual || exit 1 +$TMUX2 setw fill-character '#{?is_inside,I,#{?is_outside,O,X}}' || exit 1 +$TMUX2 resizew -x28 -y8 || exit 1 +$TMUX2 new-pane -x12 -y4 -X8 -Y2 "sh -c 'printf FLOAT; exec sleep 100'" || exit 1 +tiled=$($TMUX2 list-panes -F '#{pane_floating_flag} #{pane_id}' | \ + awk '$1==0{print $2; exit}') || exit 1 +$TMUX2 kill-pane -t "$tiled" || exit 1 + +$TMUX new -d -x40 -y12 || exit 1 +$TMUX set -g status off || exit 1 +$TMUX set -g window-size manual || exit 1 +$TMUX set -g default-terminal "tmux-256color" || exit 1 +$TMUX send -l "$TMUX2 attach" || exit 1 +$TMUX send Enter || exit 1 +sleep 1 + +$TMUX capturep -p >$TMP || exit 1 + +must_equal "$(sed -n '1p' $TMP | cut -c1-28)" \ + "IIIIIIIIIIIIIIIIIIIIIIIIIIII" +must_equal "$(sed -n '1p' $TMP | cut -c29-40)" "OOOOOOOOOOOO" +must_equal "$(sed -n '9p' $TMP)" "OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO" + +if grep -q X "$TMP"; then + fail "fill-character used neither inside nor outside" +fi + +exit 0 diff --git a/regress/screen-redraw-floating.sh b/regress/screen-redraw-floating.sh index d0b03fc7f..a7b389fef 100644 --- a/regress/screen-redraw-floating.sh +++ b/regress/screen-redraw-floating.sh @@ -215,9 +215,9 @@ $TMUX2 new-pane -x20 -y6 -X8 -Y7 "sh -c 'printf OVERST; exec sleep 100'" || exit compare floating-over-status # A window left with only a floating pane: killing the single tiled pane removes -# it from the layout, so the area it occupied is no longer owned by any pane and -# is drawn as EMPTY cells (middle dots) around the float. This is the only way to -# produce a REDRAW_SPAN_EMPTY span. +# it from the layout, so the area it occupied is no longer owned by any pane. +# Use a visible fill character for the EMPTY cells around the float. +$TMUX2 set -g fill-character '#[acs]~' || exit 1 new_scene 40 12 $TMUX2 new-pane -x20 -y6 -X8 -Y3 "sh -c 'printf FLOAT; exec sleep 100'" || exit 1 tiled=$($TMUX2 list-panes -F '#{pane_floating_flag} #{pane_id}' | \ diff --git a/regress/screen-redraw-results/floating-clip-window-edge.result b/regress/screen-redraw-results/floating-clip-window-edge.result index 751c4ef5d..97bc394d1 100644 --- a/regress/screen-redraw-results/floating-clip-window-edge.result +++ b/regress/screen-redraw-results/floating-clip-window-edge.result @@ -1,12 +1,12 @@ -01:abcdefghijklmnopq │··········· -02:abcdefghijklmnopq │··········· -03:abcdefghijklmnop┌────────│··········· -04:abcdefghijklmnop│EDGE │··········· -05:abcdefghijklmnop│ │··········· -06:abcdefghijklmnop│ │··········· -07:abcdefghijklmnop└────────│··········· - │··········· -────────────────────────────┘··········· +01:abcdefghijklmnopq ············ +02:abcdefghijklmnopq ············ +03:abcdefghijklmnop┌────────············ +04:abcdefghijklmnop│EDGE ············ +05:abcdefghijklmnop│ ············ +06:abcdefghijklmnop│ ············ +07:abcdefghijklmnop└────────············ + ············ +········································ ········································ ········································ ········································ diff --git a/regress/screen-redraw-results/floating-outside.result b/regress/screen-redraw-results/floating-outside.result index e7de24533..e5ab711c4 100644 --- a/regress/screen-redraw-results/floating-outside.result +++ b/regress/screen-redraw-results/floating-outside.result @@ -1,12 +1,12 @@ -base │··········· - │··········· - │··········· - │··········· - ┌─────────│··········· - │OUT │··········· - │ │··········· - │ │··········· -────────────────────────────┘··········· +base ············ + ············ + ············ + ············ + ┌─────────············ + │OUT ············ + │ ············ + │ ············ +········································ ········································ ········································ ········································ From c59230316b4507320001f41d228bdc13104d6f8f Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 23 Jul 2026 12:23:12 +0100 Subject: [PATCH 25/85] Update CHANGES. --- CHANGES | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES b/CHANGES index 5c4ff9664..80016b0e1 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,8 @@ CHANGES FROM 3.7b TO 3.8 +* Build with jemalloc on macOS to avoid what appears to be a bug in the system + calloc(3) (issue 5385). + * Many improvements to floating panes: - new-pane and split-window can now set the title with -T and border lines @@ -87,6 +90,11 @@ CHANGES FROM 3.7b TO 3.8 while scrolling or while hovered and disappears after pane-scrollbars-timeout (Michael Grant). +* Extend the fill-character option so both inside and outside the window can be + changed. + +* Change set-option and set-hooks to use formats and add a -F flag to each. + * Add a #{A/count:frames} modifier to show a series of frames as an animation in the status line (issue 5412 from Fernando Daciuk). From 8862390c3426ed31a76c1476089eefcc87932e32 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 23 Jul 2026 09:38:27 +0000 Subject: [PATCH 26/85] Extend fill-character to allow inside and outside to be separate and use a different default (filled characters rather than dots) for inside. --- cmd-break-pane.c | 3 +- options-table.c | 6 ++-- options.c | 4 +-- screen-redraw.c | 16 ++++++--- spawn.c | 3 +- tmux.1 | 13 +++++--- tmux.h | 12 ++++--- window-border.c | 84 ++++++++++++++++++++++++++++++++++++++++++------ window.c | 24 +------------- 9 files changed, 111 insertions(+), 54 deletions(-) diff --git a/cmd-break-pane.c b/cmd-break-pane.c index 4fe6397c9..dcd4e8188 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-break-pane.c,v 1.74 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: cmd-break-pane.c,v 1.75 2026/07/23 09:38:27 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -172,6 +172,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) w->name = clean_name(name, 0); options_set_number(w->options, "automatic-rename", 0); } + window_set_fill_cells(w); layout_init(w, wp); wp->flags |= PANE_CHANGED; diff --git a/options-table.c b/options-table.c index 124bf5145..cba062ded 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.240 2026/07/21 11:52:13 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.241 2026/07/23 09:38:27 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1460,8 +1460,8 @@ const struct options_table_entry options_table[] = { { .name = "fill-character", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW, - .default_str = "", - .text = "Character used to fill unused parts of window." + .default_str = "#{?is_inside,#[bg=themedarkgrey] ,#[fg=themelightgrey]#[acs]~}", + .text = "Format used to fill unused parts of window." }, { .name = "main-pane-height", diff --git a/options.c b/options.c index b5238bc69..c3d948020 100644 --- a/options.c +++ b/options.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options.c,v 1.90 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: options.c,v 1.91 2026/07/23 09:38:27 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -1372,7 +1372,7 @@ options_push_changes(const char *name) } if (strcmp(name, "fill-character") == 0) { RB_FOREACH(w, windows, &windows) - window_set_fill_character(w); + window_set_fill_cells(w); } if (strcmp(name, "key-table") == 0) { TAILQ_FOREACH(loop, &clients, entry) diff --git a/screen-redraw.c b/screen-redraw.c index ea4af99bf..55c22a0ec 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-redraw.c,v 1.155 2026/07/21 13:27:41 nicm Exp $ */ +/* $OpenBSD: screen-redraw.c,v 1.156 2026/07/23 09:38:27 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -320,7 +320,7 @@ redraw_reset_cell(struct redraw_build_ctx *bctx, u_int x, u_int y) struct window *w = bctx->w; memset(bc, 0, sizeof *bc); - if (bctx->ox + x <= w->sx && bctx->oy + y <= w->sy) + if (bctx->ox + x < w->sx && bctx->oy + y < w->sy) bc->data.type = REDRAW_SPAN_EMPTY; else bc->data.type = REDRAW_SPAN_OUTSIDE; @@ -1232,9 +1232,15 @@ redraw_draw_border_span(struct redraw_draw_ctx *dctx, if (wp == NULL) { redraw_get_default_border_style(dctx, &gc, &pane_lines); - if (span->data.type != REDRAW_SPAN_BORDER) - pane_lines = PANE_LINES_SINGLE; - window_get_border_cell(w, NULL, pane_lines, cell_type, &gc); + if (span->data.type == REDRAW_SPAN_OUTSIDE) + window_get_fill_cell(w, 0, &gc); + else if (span->data.type == REDRAW_SPAN_EMPTY) + window_get_fill_cell(w, 1, &gc); + else { + if (span->data.type != REDRAW_SPAN_BORDER) + pane_lines = PANE_LINES_SINGLE; + window_get_border_cell(NULL, pane_lines, cell_type, &gc); + } } else { window_pane_get_border_style(wp, c, &gc); window_pane_get_border_cell(wp, cell_type, &gc); diff --git a/spawn.c b/spawn.c index d533c0a04..6379e01af 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spawn.c,v 1.49 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: spawn.c,v 1.50 2026/07/23 09:38:27 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -224,6 +224,7 @@ spawn_window(struct spawn_context *sc, char **cause) w->name = xstrdup(sc->name); options_set_number(w->options, "automatic-rename", 0); } + window_set_fill_cells(w); } /* Switch to the new window if required. */ diff --git a/tmux.1 b/tmux.1 index 1a1f9fef5..52b74c9e5 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1148 2026/07/22 20:12:58 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1149 2026/07/23 09:38:27 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 22 2026 $ +.Dd $Mdocdate: July 23 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -5712,8 +5712,13 @@ Set the time in milliseconds for which the indicators shown by the .Ic display\-panes command appear. .Pp -.It Ic fill\-character Ar character -Set the character used to fill areas of the terminal unused by a window. +.It Ic fill\-character Ar format +Set the format used to fill areas of the terminal unused by a pane. +The format is expanded once for areas inside the window with +.Ql is_inside +set, and once for areas outside the window with +.Ql is_outside +set. .Pp .It Ic main\-pane\-height Ar height .It Ic main\-pane\-width Ar width diff --git a/tmux.h b/tmux.h index 0e40a5ea2..368813635 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1414 2026/07/22 20:12:58 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1415 2026/07/23 09:38:27 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1438,7 +1438,8 @@ struct window { int sb; int sb_pos; - struct utf8_data *fill_character; + struct grid_cell inside_cell; + struct grid_cell outside_cell; int flags; #define WINDOW_BELL 0x1 #define WINDOW_ACTIVITY 0x2 @@ -3732,7 +3733,6 @@ void *window_pane_get_new_data(struct window_pane *, struct window_pane_offset *, size_t *); void window_pane_update_used_data(struct window_pane *, struct window_pane_offset *, size_t); -void window_set_fill_character(struct window *); void window_pane_default_cursor(struct window_pane *); int window_pane_mode(struct window_pane *); int window_pane_show_scrollbar(struct window_pane *); @@ -3760,8 +3760,10 @@ struct style_range *window_pane_status_get_range(struct window_pane *, u_int, int window_pane_is_floating(struct window_pane *); /* window-border.c */ -void window_get_border_cell(struct window *, struct window_pane *, - enum pane_lines, int, struct grid_cell *); +void window_set_fill_cells(struct window *); +void window_get_border_cell(struct window_pane *, enum pane_lines, + int, struct grid_cell *); +void window_get_fill_cell(struct window *, int, struct grid_cell *); void window_pane_get_border_cell(struct window_pane *, int, struct grid_cell *); void window_pane_get_border_style(struct window_pane *, diff --git a/window-border.c b/window-border.c index 5cd88c2bb..8f2209d0e 100644 --- a/window-border.c +++ b/window-border.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-border.c,v 1.2 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: window-border.c,v 1.3 2026/07/23 09:38:27 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -23,18 +23,82 @@ #include "tmux.h" +/* Set window fill cell. */ +static void +window_set_fill_cell(struct window *w, int inside, struct grid_cell *gc) +{ + struct format_tree *ft; + struct screen s; + struct screen_write_ctx ctx; + struct grid_cell new_gc; + const char *value; + char *expanded; + + memcpy(gc, &grid_default_cell, sizeof *gc); + gc->attr |= GRID_ATTR_CHARSET; + utf8_set(&gc->data, CELL_BORDERS[CELL_NONE]); + + ft = format_create(NULL, NULL, FORMAT_WINDOW|w->id, FORMAT_NOJOBS); + format_defaults(ft, NULL, NULL, NULL, w->active); + format_add(ft, "is_inside", "%d", inside); + format_add(ft, "is_outside", "%d", !inside); + + value = options_get_string(w->options, "fill-character"); + expanded = format_expand(ft, value); + format_free(ft); + + screen_init(&s, 1, 1, 0); + screen_write_start(&ctx, &s); + format_draw(&ctx, &grid_default_cell, 1, expanded, NULL, 0); + screen_write_stop(&ctx); + free(expanded); + + grid_view_get_cell(s.grid, 0, 0, &new_gc); + if (new_gc.data.width == 1) + memcpy(gc, &new_gc, sizeof *gc); + screen_free(&s); +} + +/* Set window fill cells. */ +void +window_set_fill_cells(struct window *w) +{ + window_set_fill_cell(w, 1, &w->inside_cell); + window_set_fill_cell(w, 0, &w->outside_cell); +} + +/* Merge a window fill cell over an existing style. */ +static void +window_copy_fill_cell(struct grid_cell *gc, const struct grid_cell *fill) +{ + utf8_copy(&gc->data, &fill->data); + gc->attr |= fill->attr; + gc->flags |= fill->flags; + if (fill->fg != 8) + gc->fg = fill->fg; + if (fill->bg != 8) + gc->bg = fill->bg; + if (fill->us != 8) + gc->us = fill->us; +} + +/* Get window fill cell. */ +void +window_get_fill_cell(struct window *w, int inside, struct grid_cell *gc) +{ + if (inside) + window_copy_fill_cell(gc, &w->inside_cell); + else + window_copy_fill_cell(gc, &w->outside_cell); +} + /* Get border cell. */ void -window_get_border_cell(struct window *w, struct window_pane *wp, - enum pane_lines pane_lines, int cell_type, struct grid_cell *gc) +window_get_border_cell(struct window_pane *wp, enum pane_lines pane_lines, + int cell_type, struct grid_cell *gc) { u_int idx; - if (cell_type == CELL_NONE && w->fill_character != NULL) { - utf8_copy(&gc->data, &w->fill_character[0]); - return; - } - switch (pane_lines) { case PANE_LINES_NUMBER: if (cell_type == CELL_NONE) { @@ -79,7 +143,7 @@ window_pane_get_border_cell(struct window_pane *wp, int cell_type, { enum pane_lines pane_lines = window_pane_get_pane_lines(wp); - window_get_border_cell(wp->window, wp, pane_lines, cell_type, gc); + window_get_border_cell(wp, pane_lines, cell_type, gc); } /* Get pane border style. */ @@ -147,7 +211,7 @@ window_make_pane_status(struct window_pane *wp, struct client *c, u_int width, pane_lines = window_pane_get_pane_lines(wp); for (i = 0; i < width; i++) { cell_type = redraw_get_status_border_cell_type(&span, i); - window_get_border_cell(wp->window, wp, pane_lines, cell_type, &gc); + window_get_border_cell(wp, pane_lines, cell_type, &gc); screen_write_cell(&ctx, &gc); } gc.attr &= ~GRID_ATTR_CHARSET; diff --git a/window.c b/window.c index f5bf6870b..ea68d23e6 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.367 2026/07/21 13:04:01 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.368 2026/07/23 09:38:27 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -439,8 +439,6 @@ window_create(u_int sx, u_int sy, u_int xpixel, u_int ypixel) w->id = next_window_id++; RB_INSERT(windows, &windows, w); - window_set_fill_character(w); - if (gettimeofday(&w->creation_time, NULL) != 0) fatal("gettimeofday failed"); window_update_activity(w); @@ -474,7 +472,6 @@ window_destroy(struct window *w) event_del(&w->offset_timer); options_free(w->options); - free(w->fill_character); free(w->name); free(w); @@ -2431,25 +2428,6 @@ window_pane_update_used_data(struct window_pane *wp, wpo->used += size; } -void -window_set_fill_character(struct window *w) -{ - const char *value; - struct utf8_data *ud; - - free(w->fill_character); - w->fill_character = NULL; - - value = options_get_string(w->options, "fill-character"); - if (*value != '\0' && utf8_isvalid(value)) { - ud = utf8_fromcstr(value); - if (ud != NULL && ud[0].width == 1) - w->fill_character = ud; - else - free(ud); - } -} - void window_pane_default_cursor(struct window_pane *wp) { From 0ec289545a054ee21bac7ded9d474288e43554b7 Mon Sep 17 00:00:00 2001 From: Ilya Grigoriev Date: Thu, 23 Jul 2026 09:58:25 -0700 Subject: [PATCH 27/85] configure.ac: don't enable jemalloc if ASAN enabled on macOS, error if both enabled ASAN and jemalloc both replace `malloc` with their version. Previously, configuring with `--enable-asan` on macOS would enable both jemalloc and ASAN. I'm not sure which one won, but seemed undesired. --- configure.ac | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 53c513acc..549f4a3b3 100644 --- a/configure.ac +++ b/configure.ac @@ -635,13 +635,20 @@ AC_ARG_ENABLE( ) try_jemalloc=no require_jemalloc=no +if test "x$enable_asan" = xyes && test "x$enable_jemalloc" = xyes; then + AC_MSG_ERROR([--enable-asan and --enable-jemalloc cannot be used together]) +fi if test "x$enable_jemalloc" = xyes; then try_jemalloc=yes elif test "x$enable_jemalloc" = x; then case "$host_os" in *darwin*) - try_jemalloc=yes - require_jemalloc=yes + if test "x$enable_asan" = xyes; then + enable_jemalloc=no + else + try_jemalloc=yes + require_jemalloc=yes + fi ;; esac fi From e361982acf62f16c4da34ca0afc91724281e4171 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 24 Jul 2026 09:49:32 +0100 Subject: [PATCH 28/85] Fix some tests. --- .../floating-clip-window-edge.result | 18 +++++++++--------- .../floating-outside.result | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/regress/screen-redraw-results/floating-clip-window-edge.result b/regress/screen-redraw-results/floating-clip-window-edge.result index 97bc394d1..751c4ef5d 100644 --- a/regress/screen-redraw-results/floating-clip-window-edge.result +++ b/regress/screen-redraw-results/floating-clip-window-edge.result @@ -1,12 +1,12 @@ -01:abcdefghijklmnopq ············ -02:abcdefghijklmnopq ············ -03:abcdefghijklmnop┌────────············ -04:abcdefghijklmnop│EDGE ············ -05:abcdefghijklmnop│ ············ -06:abcdefghijklmnop│ ············ -07:abcdefghijklmnop└────────············ - ············ -········································ +01:abcdefghijklmnopq │··········· +02:abcdefghijklmnopq │··········· +03:abcdefghijklmnop┌────────│··········· +04:abcdefghijklmnop│EDGE │··········· +05:abcdefghijklmnop│ │··········· +06:abcdefghijklmnop│ │··········· +07:abcdefghijklmnop└────────│··········· + │··········· +────────────────────────────┘··········· ········································ ········································ ········································ diff --git a/regress/screen-redraw-results/floating-outside.result b/regress/screen-redraw-results/floating-outside.result index e5ab711c4..e7de24533 100644 --- a/regress/screen-redraw-results/floating-outside.result +++ b/regress/screen-redraw-results/floating-outside.result @@ -1,12 +1,12 @@ -base ············ - ············ - ············ - ············ - ┌─────────············ - │OUT ············ - │ ············ - │ ············ -········································ +base │··········· + │··········· + │··········· + │··········· + ┌─────────│··········· + │OUT │··········· + │ │··········· + │ │··········· +────────────────────────────┘··········· ········································ ········································ ········································ From 47f210a9d257b84d624ab6c755c9355a72cf9ac0 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 24 Jul 2026 08:49:23 +0000 Subject: [PATCH 29/85] Replace outside as well as empty cells with the border. --- screen-redraw.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/screen-redraw.c b/screen-redraw.c index 55c22a0ec..42cc5abed 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-redraw.c,v 1.156 2026/07/23 09:38:27 nicm Exp $ */ +/* $OpenBSD: screen-redraw.c,v 1.157 2026/07/24 08:49:23 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -547,7 +547,8 @@ redraw_mark_border_cell(struct redraw_build_ctx *bctx, int wx, int wy, * merged. */ if (!floating) { - if (bc->data.type == REDRAW_SPAN_EMPTY) + if (bc->data.type == REDRAW_SPAN_EMPTY || + bc->data.type == REDRAW_SPAN_OUTSIDE) reset = 1; else if (bc->data.type != REDRAW_SPAN_BORDER) return; From 0768795ce31c064e06cd4dcd65730eaa71bfef25 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 26 Jul 2026 10:02:31 +0100 Subject: [PATCH 30/85] Test for overlong runs of combining characters, GitHub issue 5434. --- regress/input-common.inc | 14 ++++++++++++++ regress/input-unicode.sh | 4 ++++ regress/tty-draw-line.sh | 22 ++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/regress/input-common.inc b/regress/input-common.inc index 100a7be0f..ac04dbc24 100644 --- a/regress/input-common.inc +++ b/regress/input-common.inc @@ -174,3 +174,17 @@ check_raw_matches() fi done } + +check_raw_no_matches() +{ + name=$1 + shift + + capture_raw "$name" >"$TMP" + for unexpected in "$@"; do + if grep -Eq "$unexpected" "$TMP"; then + printf "%s\n" "$unexpected" >"$EXP" + fail "$name raw unexpected" + fi + done +} diff --git a/regress/input-unicode.sh b/regress/input-unicode.sh index ca74a82e8..98c4506e2 100644 --- a/regress/input-unicode.sh +++ b/regress/input-unicode.sh @@ -41,5 +41,9 @@ start_pane combining-left 10 3 '\314\201A\n' check_capture combining-left 'A' check_cursor combining-left '0,1' +start_pane combining-overflow 10 3 'u\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\n' +check_raw_matches combining-overflow '^ C 0,0 data=\(1,31,u' +check_raw_no_matches combining-overflow 'data=\(0,' + $TMUX kill-server 2>/dev/null exit $exit_status diff --git a/regress/tty-draw-line.sh b/regress/tty-draw-line.sh index 59331aa0d..09a956312 100644 --- a/regress/tty-draw-line.sh +++ b/regress/tty-draw-line.sh @@ -34,6 +34,18 @@ captureen() { $TMUX capturep -peNS0 -E- >$TMP || exit 1 } +timed() { + if command -v timeout >/dev/null 2>&1; then + timeout 5 "$@" + else + "$@" + fi +} + +capture_timed() { + timed $TMUX capturep -pS0 -E- >$TMP || exit 1 +} + check_line() { line=$1 want=$2 @@ -81,6 +93,8 @@ $TMUX2 neww -d \ "printf '\033(0x\033(B'; exec sleep 100" || exit 1 $TMUX2 neww -d \ "awk 'BEGIN { for (i = 0; i < 1100; i++) printf \"a\" }'; exec sleep 100" || exit 1 +$TMUX2 neww -d \ + "printf 'u\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245'; exec sleep 100" || exit 1 $TMUX2 selectw -t:0 || exit 1 $TMUX -f/dev/null new -d -x20 -y6 || exit 1 @@ -245,4 +259,12 @@ capture len=$(sed -n 1p $TMP | wc -c) [ "$len" -ge 1100 ] || fail "long same-style line was truncated" +# Too many combining marks on one base character must not leave a standalone +# width-zero cell that can make tty_draw_line loop forever on redraw. +$TMUX resizew -x20 -y6 || exit 1 +timed $TMUX2 selectw -t:13 || fail "zero-width overflow select hung" +sleep 1 +capture_timed +check_grep '^u' + exit 0 From 10726b1b279da05adf5e5a8e38cc3eaade477850 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 26 Jul 2026 10:21:01 +0100 Subject: [PATCH 31/85] Title stack test. --- regress/input-osc.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/regress/input-osc.sh b/regress/input-osc.sh index 19b9d400f..b2c4d25d9 100644 --- a/regress/input-osc.sh +++ b/regress/input-osc.sh @@ -22,6 +22,16 @@ check_capture rename 'X' start_pane apc-title 20 3 '\033_test-title\033\\X\n' check_capture apc-title 'X' +cmd='i=0; while [ "$i" -lt 12 ]; do ' +cmd="${cmd}printf '\\033[22;0t'; i=\$((i + 1)); done; " +cmd="${cmd}printf X; sleep 2" +start_cmd title-stack 20 3 "$cmd" +check_capture title-stack 'X' +$TMUX respawn-pane -k -t title-stack: \ + "printf '\\033[22;0tY'; sleep 2" || exit 1 +sleep 0.3 +check_capture title-stack 'Y' + $TMUX kill-server 2>/dev/null sleep 0.1 $TMUX new-session -d -x 20 -y 3 -s osc52 "sleep 2" || exit 1 From afb95f3134567888c03bc45136ac2d6d5dcfd9cc Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 26 Jul 2026 09:02:08 +0000 Subject: [PATCH 32/85] Do not loop forever if the combining character is too long, GitHub issue 5434 from me at qdrs dot dev. --- screen-write.c | 4 ++-- tty-draw.c | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/screen-write.c b/screen-write.c index 1ba5bed70..6cfc80ad0 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.284 2026/07/20 11:16:33 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.285 2026/07/26 09:02:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2783,7 +2783,7 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) /* Check if this combined character would be too long. */ if (last.data.size + ud->size > sizeof last.data.data) - return (0); + return (zero_width); /* Combining; flush any pending output. */ screen_write_collect_flush(ctx, 0, __func__); diff --git a/tty-draw.c b/tty-draw.c index d47be66bc..06d520e11 100644 --- a/tty-draw.c +++ b/tty-draw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-draw.c,v 1.14 2026/06/19 10:38:29 nicm Exp $ */ +/* $OpenBSD: tty-draw.c,v 1.15 2026/07/26 09:02:08 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -100,6 +100,8 @@ tty_draw_line_get_empty(const struct grid_cell *gc, empty = nx; else if (gc->flags & GRID_FLAG_PADDING) empty = 1; + else if (gc->data.width == 0) + empty = 1; else if (gc->flags & GRID_FLAG_SELECTED) empty = 0; else if (gc->bg == last->bg && gc->attr == 0 && gc->link == 0) { From 55169f12756175d526d7254c53d1701db98e47df Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 26 Jul 2026 09:17:30 +0000 Subject: [PATCH 33/85] Reset title count when freeing titles, GitHub issue 5429 from Brett Smith. --- screen.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/screen.c b/screen.c index 4c754360c..35f26bfa6 100644 --- a/screen.c +++ b/screen.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen.c,v 1.105 2026/06/29 18:17:28 nicm Exp $ */ +/* $OpenBSD: screen.c,v 1.106 2026/07/26 09:17:30 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -70,6 +70,7 @@ screen_free_titles(struct screen *s) free(s->titles); s->titles = NULL; + s->ntitles = 0; } /* Create a new screen. */ From 3068ae06a88269072affc21e9c6e2cb62246905c Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 26 Jul 2026 09:20:54 +0000 Subject: [PATCH 34/85] Also set ntitles to 0 when creating screen in the first place. --- screen.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/screen.c b/screen.c index 35f26bfa6..2533beb5c 100644 --- a/screen.c +++ b/screen.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen.c,v 1.106 2026/07/26 09:17:30 nicm Exp $ */ +/* $OpenBSD: screen.c,v 1.107 2026/07/26 09:20:54 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -82,6 +82,7 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit) s->title = xstrdup(""); s->titles = NULL; + s->ntitles = 0; s->path = NULL; s->cstyle = SCREEN_CURSOR_DEFAULT; From 0255981a546ebb9de8dba2140c4fb0a3773fd811 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 26 Jul 2026 16:08:30 +0100 Subject: [PATCH 35/85] Test run-shell if killed. --- regress/run-shell-output.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/regress/run-shell-output.sh b/regress/run-shell-output.sh index 926a520c9..68c98e5c3 100644 --- a/regress/run-shell-output.sh +++ b/regress/run-shell-output.sh @@ -11,7 +11,7 @@ TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) -trap "rm -f $TMP" 0 1 15 +trap "$TMUX kill-server 2>/dev/null; rm -f $TMP" 0 1 15 $TMUX -f/dev/null new -d "$TMUX run 'echo foo' >$TMP; sleep 10" || exit 1 sleep 1 && [ "$(cat $TMP)" = "foo" ] || exit 1 @@ -20,6 +20,15 @@ $TMUX -f/dev/null new -d "$TMUX run -t: 'echo foo' >$TMP; sleep 10" || exit 1 sleep 1 && [ "$(cat $TMP)" = "" ] || exit 1 [ "$($TMUX display -p '#{pane_mode}')" = "view-mode" ] || exit 1 +$TMUX -f/dev/null new -d -s t1 'sleep 10' || exit 1 +$TMUX -f/dev/null run -d 1 'echo delayed' >$TMP 2>&1 & +pid=$! +sleep 0.2 +kill -9 "$pid" 2>/dev/null +wait "$pid" 2>/dev/null +sleep 2 +$TMUX has-session -t t1 || exit 1 + $TMUX kill-server 2>/dev/null exit 0 From 4cc45aa719a527457ec0d88dcbc7216ca3be2b7e Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 26 Jul 2026 15:08:15 +0000 Subject: [PATCH 36/85] =?UTF-8?q?Do=20not=20try=20to=20write=20to=20client?= =?UTF-8?q?s=20which=20have=20died,=20GitHub=20issue=205431=20from=20Stefa?= =?UTF-8?q?n=20Ladst=C3=A4tter.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- file.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/file.c b/file.c index a9c393c04..4207f8053 100644 --- a/file.c +++ b/file.c @@ -1,4 +1,4 @@ -/* $OpenBSD: file.c,v 1.20 2026/05/17 10:54:01 nicm Exp $ */ +/* $OpenBSD: file.c,v 1.21 2026/07/26 15:08:15 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -188,6 +188,7 @@ file_can_print(struct client *c) { if (c == NULL || (c->flags & CLIENT_ATTACHED) || + (c->flags & CLIENT_DEAD) || (c->flags & CLIENT_CONTROL)) return (0); return (1); From d57d75deee894f2adb9b13fecadb3b2ccadf39bc Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 26 Jul 2026 15:21:53 +0000 Subject: [PATCH 37/85] Include \n and \t in shell special characters, from Nikolas Skarlatos. --- format.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/format.c b/format.c index f703022b5..4b35027a5 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.408 2026/07/22 08:19:14 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.409 2026/07/26 15:21:53 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -4307,7 +4307,7 @@ format_quote_shell(const char *s) at = out = xmalloc(strlen(s) * 2 + 1); for (cp = s; *cp != '\0'; cp++) { - if (strchr("|&;<>()$`\\\"'*?[# =%", *cp) != NULL) + if (strchr("|&;<>()$`\\\"'*?[# =%\n\t", *cp) != NULL) *at++ = '\\'; *at++ = *cp; } From 5269e1411363fc91a5efe628e88b41eb11e24872 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 27 Jul 2026 08:03:01 +0000 Subject: [PATCH 38/85] Add after-swap-window hook from basicalllymiloud at gmail dot com. --- cmd-swap-window.c | 4 ++-- options-table.c | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd-swap-window.c b/cmd-swap-window.c index 980da0395..0141e6a57 100644 --- a/cmd-swap-window.c +++ b/cmd-swap-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-swap-window.c,v 1.29 2025/10/30 13:52:08 nicm Exp $ */ +/* $OpenBSD: cmd-swap-window.c,v 1.30 2026/07/27 08:03:01 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -38,7 +38,7 @@ const struct cmd_entry cmd_swap_window_entry = { .source = { 's', CMD_FIND_WINDOW, CMD_FIND_DEFAULT_MARKED }, .target = { 't', CMD_FIND_WINDOW, 0 }, - .flags = 0, + .flags = CMD_AFTERHOOK, .exec = cmd_swap_window_exec }; diff --git a/options-table.c b/options-table.c index cba062ded..54352319b 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.241 2026/07/23 09:38:27 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.242 2026/07/27 08:03:01 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1945,6 +1945,7 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_AFTER_HOOK("show-messages"), OPTIONS_TABLE_AFTER_HOOK("show-options"), OPTIONS_TABLE_AFTER_HOOK("split-window"), + OPTIONS_TABLE_AFTER_HOOK("swap-window"), OPTIONS_TABLE_AFTER_HOOK("unbind-key"), OPTIONS_TABLE_HOOK("alert-activity", "", "Run when a window has activity."), From 9b97dadc0160313bfc961f17c63a0a19f1d89509 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 27 Jul 2026 15:26:26 +0100 Subject: [PATCH 39/85] Test for pipe-pane. --- regress/pipe-pane.sh | 52 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 regress/pipe-pane.sh diff --git a/regress/pipe-pane.sh b/regress/pipe-pane.sh new file mode 100644 index 000000000..ee302ffbb --- /dev/null +++ b/regress/pipe-pane.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +# Tests of pipe-pane behaviour. + +PATH=/bin:/usr/bin +TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export TERM LANG LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +fail() +{ + echo "$1" + $TMUX kill-server 2>/dev/null + exit 1 +} + +check_ok() +{ + if ! $TMUX "$@"; then + fail "Command failed: $*" + fi +} + +check_alive() +{ + if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then + fail "Server died" + fi +} + +# 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 set-option -t pipe:0 remain-on-exit on +check_ok pipe-pane -t pipe:0.0 -I 'sleep 0.6; printf x' + +i=0 +while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do + i=$((i + 1)) + [ "$i" -gt 50 ] && fail "Pane did not die" + sleep 0.1 +done + +sleep 0.7 +check_alive + +$TMUX kill-server 2>/dev/null From f751d3f2c16493cef5e9a6b27fdce7ea0e713f34 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 27 Jul 2026 14:25:46 +0000 Subject: [PATCH 40/85] When destroying pane, also close pipe and destroy its event. GitHub issue 5424. --- cmd-pipe-pane.c | 8 ++++++-- server-fn.c | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/cmd-pipe-pane.c b/cmd-pipe-pane.c index f18added6..9e04b478b 100644 --- a/cmd-pipe-pane.c +++ b/cmd-pipe-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-pipe-pane.c,v 1.63 2026/04/28 08:47:55 nicm Exp $ */ +/* $OpenBSD: cmd-pipe-pane.c,v 1.64 2026/07/27 14:25:46 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -195,9 +195,13 @@ static void cmd_pipe_pane_read_callback(__unused struct bufferevent *bufev, void *data) { struct window_pane *wp = data; - struct evbuffer *evb = wp->pipe_event->input; + struct evbuffer *evb; size_t available; + if (wp->pipe_event == NULL) + return; + evb = wp->pipe_event->input; + available = EVBUFFER_LENGTH(evb); log_debug("%%%u pipe read %zu", wp->id, available); diff --git a/server-fn.c b/server-fn.c index 70e01860c..149d048ad 100644 --- a/server-fn.c +++ b/server-fn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-fn.c,v 1.148 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: server-fn.c,v 1.149 2026/07/27 14:25:46 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -368,6 +368,12 @@ server_destroy_pane(struct window_pane *wp, int notify) close(wp->fd); wp->fd = -1; } + if (wp->pipe_fd != -1) { + bufferevent_free(wp->pipe_event); + wp->pipe_event = NULL; + close(wp->pipe_fd); + wp->pipe_fd = -1; + } remain_on_exit = options_get_number(wp->options, "remain-on-exit"); if (remain_on_exit != 0 && (~wp->flags & PANE_STATUSREADY)) From 27ab96b284e71ed82c1a8930dd900260fd1583f0 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 27 Jul 2026 19:15:58 +0000 Subject: [PATCH 41/85] Store count of how many times a hook is fired and the last time. --- cmd-show-options.c | 29 ++++++++++++++++++++++++++- hooks.c | 50 +++++++++++++++++++++++++++++++++------------- monitor.c | 30 +++++++++++++++++++++++++++- options.c | 24 +++++++++++++++++++++- tmux.1 | 6 ++++-- tmux.h | 9 ++++++++- 6 files changed, 128 insertions(+), 20 deletions(-) diff --git a/cmd-show-options.c b/cmd-show-options.c index 0b3329f7c..910aaf348 100644 --- a/cmd-show-options.c +++ b/cmd-show-options.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-show-options.c,v 1.75 2026/07/22 20:12:58 nicm Exp $ */ +/* $OpenBSD: cmd-show-options.c,v 1.76 2026/07/27 19:15:58 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -191,6 +191,9 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item, const char *name = options_name(o); const char *template = args_get(args, 'F'); char *value, *line; + struct timeval tv = { 0 }; + u_int fire_count; + time_t fire_time; int is_hook = 0, is_user = 0; int has_value = 1; const struct options_table_entry *oe = options_table_entry(o); @@ -233,6 +236,16 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item, format_add(ft, "option_is_hook", "%d", is_hook); format_add(ft, "option_is_user", "%d", is_user); format_add(ft, "option_has_value", "%d", has_value); + if (cmd_get_entry(self) == &cmd_show_hooks_entry) { + fire_count = options_get_fire_count(o); + format_add(ft, "hook_fire_count", "%u", fire_count); + + fire_time = options_get_fire_time(o); + if (fire_time != 0) { + tv.tv_sec = fire_time; + format_add_tv(ft, "hook_fire_time", &tv); + } + } if (array_key != NULL) { format_add(ft, "option_array_key", "%s", array_key); format_add(ft, "option_has_array_key", "1"); @@ -257,6 +270,9 @@ cmd_show_hooks_print_monitor(struct cmd *self, struct cmdq_item *item, enum monitor_type type; const char *template = args_get(args, 'F'), *format; char *value, *target, *line; + struct timeval tv = { 0 }; + u_int fire_count; + time_t fire_time; int id; value = hooks_monitor_to_string(o); @@ -299,8 +315,19 @@ cmd_show_hooks_print_monitor(struct cmd *self, struct cmdq_item *item, format_add(ft, "option_has_value", "%d", 1); format_add(ft, "option_array_key", "%s", ""); format_add(ft, "option_has_array_key", "0"); + format_add(ft, "hook_monitor_target", "%s", target); format_add(ft, "hook_monitor_format", "%s", format); + + fire_count = hooks_monitor_get_fire_count(o); + format_add(ft, "hook_fire_count", "%u", fire_count); + + fire_time = hooks_monitor_get_fire_time(o); + if (fire_time != 0) { + tv.tv_sec = fire_time; + format_add_tv(ft, "hook_fire_time", &tv); + } + line = format_expand(ft, template); format_free(ft); diff --git a/hooks.c b/hooks.c index bd0d1d732..acc11704d 100644 --- a/hooks.c +++ b/hooks.c @@ -1,4 +1,4 @@ -/* $OpenBSD: hooks.c,v 1.15 2026/07/22 20:12:58 nicm Exp $ */ +/* $OpenBSD: hooks.c,v 1.16 2026/07/27 19:15:58 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -25,7 +25,7 @@ #include "tmux.h" /* Hook monitor state owned by an option entry. */ -struct hook_monitor { +struct hooks_monitor { struct options *oo; struct monitor_set *set; @@ -143,6 +143,7 @@ hooks_insert(struct cmdq_item *item, struct hooks_data *hd) log_debug("%s: hook %s not found", __func__, hd->name); return; } + options_hook_fired(o); if (item == NULL) state = cmdq_new_state(&fs, NULL, CMDQ_STATE_NOHOOKS); @@ -233,7 +234,7 @@ hooks_event_cb(const char *name, struct event_payload *ep, { struct cmdq_item *item; - if (event_payload_get_pointer(ep, "_hook_monitor") != NULL) + if (event_payload_get_pointer(ep, "_hooks_monitor") != NULL) return; item = event_payload_get_pointer(ep, "_cmdq_item"); @@ -324,7 +325,7 @@ hooks_run(struct cmdq_item *item, const char *name) void hooks_monitor_free(void *data) { - struct hook_monitor *hm = data; + struct hooks_monitor *hm = data; events_remove_sink(hm->sink); monitor_destroy(hm->set); @@ -337,7 +338,7 @@ void hooks_monitor_remove(struct options *oo, const char *name) { struct options_entry *o; - struct hook_monitor *hm; + struct hooks_monitor *hm; o = options_get_only(oo, name); if (o == NULL) @@ -355,9 +356,9 @@ static void hooks_monitor_hook_cb(const char *name, struct event_payload *ep, void *sink_data) { - struct hook_monitor *hm = sink_data; + struct hooks_monitor *hm = sink_data; - if (event_payload_get_pointer(ep, "_hook_monitor") == hm) + if (event_payload_get_pointer(ep, "_hooks_monitor") == hm) hooks_insert_event(cmdq_running(NULL), name, ep, hm->oo, 1); } @@ -365,14 +366,14 @@ hooks_monitor_hook_cb(const char *name, struct event_payload *ep, static void hooks_monitor_cb(struct monitor_change *change, void *data) { - struct hook_monitor *hm = data; + struct hooks_monitor *hm = data; struct event_payload *ep; struct winlink *wl = change->wl; struct window_pane *wp = change->wp; struct cmd_find_state fs; ep = event_payload_create(); - event_payload_set_pointer(ep, "_hook_monitor", data, NULL, NULL); + event_payload_set_pointer(ep, "_hooks_monitor", data, NULL, NULL); cmd_find_clear_state(&fs, 0); if (wl != NULL && wp != NULL && wp->window == wl->window) @@ -402,8 +403,7 @@ hooks_monitor_cb(struct monitor_change *change, void *data) event_payload_set_session(ep, "session", change->s); if (wl != NULL) { if (change->s == NULL) - event_payload_set_session(ep, "session", - wl->session); + event_payload_set_session(ep, "session", wl->session); event_payload_set_window(ep, "window", wl->window); event_payload_set_int(ep, "window_index", wl->idx); } @@ -423,7 +423,7 @@ hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, int flags, struct cmd_find_state *fs, struct session *s) { struct options_entry *o; - struct hook_monitor *hm; + struct hooks_monitor *hm; hooks_monitor_remove(oo, name); o = options_get_only(oo, name); @@ -446,7 +446,7 @@ hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, char * hooks_monitor_to_string(struct options_entry *o) { - struct hook_monitor *hm = options_get_monitor_data(o); + struct hooks_monitor *hm = options_get_monitor_data(o); const char *name = options_name(o); char *s; @@ -478,7 +478,7 @@ int hooks_monitor_get(struct options_entry *o, enum monitor_type *type, int *id, const char **format) { - struct hook_monitor *hm = options_get_monitor_data(o); + struct hooks_monitor *hm = options_get_monitor_data(o); if (hm == NULL) return (0); @@ -487,3 +487,25 @@ hooks_monitor_get(struct options_entry *o, enum monitor_type *type, int *id, *format = hm->format; return (1); } + +/* Get hook monitor firing count. */ +u_int +hooks_monitor_get_fire_count(struct options_entry *o) +{ + struct hooks_monitor *hm = options_get_monitor_data(o); + + if (hm == NULL) + return (0); + return (monitor_get_fire_count(hm->set, options_name(o))); +} + +/* Get hook monitor firing time. */ +time_t +hooks_monitor_get_fire_time(struct options_entry *o) +{ + struct hooks_monitor *hm = options_get_monitor_data(o); + + if (hm == NULL) + return (0); + return (monitor_get_fire_time(hm->set, options_name(o))); +} diff --git a/monitor.c b/monitor.c index d6338e878..ff0c6e5ed 100644 --- a/monitor.c +++ b/monitor.c @@ -1,4 +1,4 @@ -/* $OpenBSD: monitor.c,v 1.6 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: monitor.c,v 1.7 2026/07/27 19:15:58 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -59,6 +59,9 @@ struct monitor_item { struct monitor_panes panes; struct monitor_windows windows; + u_int fire_count; + time_t fire_time; + RB_ENTRY(monitor_item) entry; }; RB_HEAD(monitor_items, monitor_item); @@ -180,6 +183,9 @@ monitor_report(struct monitor_set *ms, struct monitor_item *me, log_debug("%s: %s changed to %s", __func__, me->name, value); + me->fire_count++; + me->fire_time = current_time; + change.name = me->name; change.value = value; change.last = last; @@ -659,3 +665,25 @@ monitor_remove(struct monitor_set *ms, const char *name) if (RB_EMPTY(&ms->items) && evtimer_initialized(&ms->timer)) evtimer_del(&ms->timer); } + +/* Get subscription firing count. */ +u_int +monitor_get_fire_count(struct monitor_set *ms, const char *name) +{ + struct monitor_item *me, find = { .name = (char *)name }; + + if ((me = RB_FIND(monitor_items, &ms->items, &find)) == NULL) + return (0); + return (me->fire_count); +} + +/* Get subscription firing time. */ +time_t +monitor_get_fire_time(struct monitor_set *ms, const char *name) +{ + struct monitor_item *me, find = { .name = (char *)name }; + + if ((me = RB_FIND(monitor_items, &ms->items, &find)) == NULL) + return (0); + return (me->fire_time); +} diff --git a/options.c b/options.c index c3d948020..fa10d8a7d 100644 --- a/options.c +++ b/options.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options.c,v 1.91 2026/07/23 09:38:27 nicm Exp $ */ +/* $OpenBSD: options.c,v 1.92 2026/07/27 19:15:58 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -107,7 +107,10 @@ struct options_entry { int cached; struct style style; + void *monitor_data; + u_int fire_count; + time_t fire_time; RB_ENTRY(options_entry) entry; }; @@ -440,6 +443,25 @@ options_set_monitor_data(struct options_entry *o, void *data) o->monitor_data = data; } +void +options_hook_fired(struct options_entry *o) +{ + o->fire_count++; + o->fire_time = current_time; +} + +u_int +options_get_fire_count(struct options_entry *o) +{ + return (o->fire_count); +} + +time_t +options_get_fire_time(struct options_entry *o) +{ + return (o->fire_time); +} + const struct options_table_entry * options_table_entry(struct options_entry *o) { diff --git a/tmux.1 b/tmux.1 index 52b74c9e5..b12a9fbdc 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1149 2026/07/23 09:38:27 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1150 2026/07/27 19:15:58 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 23 2026 $ +.Dd $Mdocdate: July 27 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -7283,6 +7283,8 @@ The following variables are available, where appropriate: .It Li "history_size" Ta "" Ta "Size of history in lines" .It Li "hook" Ta "" Ta "Name of running hook, if any" .It Li "hook_client" Ta "" Ta "Name of client where hook was run, if any" +.It Li "hook_fire_count" Ta "" Ta "Number of times hook has fired" +.It Li "hook_fire_time" Ta "" Ta "Time hook last fired" .It Li "hook_last" Ta "" Ta "Previous value for a monitor hook" .It Li "hook_monitor_format" Ta "" Ta "Format for a monitor hook" .It Li "hook_monitor_target" Ta "" Ta "Target for a monitor hook" diff --git a/tmux.h b/tmux.h index 368813635..0f3969a1c 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1415 2026/07/23 09:38:27 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1416 2026/07/27 19:15:58 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2745,6 +2745,8 @@ void hooks_monitor_free(void *); char *hooks_monitor_to_string(struct options_entry *); int hooks_monitor_get(struct options_entry *, enum monitor_type *, int *, const char **); +u_int hooks_monitor_get_fire_count(struct options_entry *); +time_t hooks_monitor_get_fire_time(struct options_entry *); /* options.c */ struct options *options_create(struct options *); @@ -2762,6 +2764,9 @@ const char *options_name(struct options_entry *); struct options *options_owner(struct options_entry *); void *options_get_monitor_data(struct options_entry *); void options_set_monitor_data(struct options_entry *, void *); +void options_hook_fired(struct options_entry *); +u_int options_get_fire_count(struct options_entry *); +time_t options_get_fire_time(struct options_entry *); const struct options_table_entry *options_table_entry(struct options_entry *); struct options_entry *options_get_only(struct options *, const char *); struct options_entry *options_get(struct options *, const char *); @@ -3957,6 +3962,8 @@ int monitor_parse(const char *, char **, enum monitor_type *, int *, void monitor_add(struct monitor_set *, const char *, enum monitor_type, int, const char *, int); void monitor_remove(struct monitor_set *, const char *); +u_int monitor_get_fire_count(struct monitor_set *, const char *); +time_t monitor_get_fire_time(struct monitor_set *, const char *); /* control.c */ void control_discard(struct client *); From 3c1b2969a8d7e4861985a271513e318bf6700188 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 28 Jul 2026 08:08:26 +0100 Subject: [PATCH 42/85] Tests for hooks time and count. --- regress/hooks.sh | 5 +++++ regress/set-hook-B.sh | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/regress/hooks.sh b/regress/hooks.sh index e99b2e768..96a5064d0 100644 --- a/regress/hooks.sh +++ b/regress/hooks.sh @@ -85,6 +85,11 @@ echo "$shown" | grep -q '^session-created\[0\]' || fail "missing first array item: $shown" echo "$shown" | grep -q '^session-created\[1\]' || fail "missing second array item: $shown" +shown=$($TMUX show-hooks -gF '#{option_name}:#{hook_fire_count}:#{t/p:hook_fire_time}' \ + session-created) || + fail "show-hooks -gF failed" +echo "$shown" | grep -q '^session-created:[1-9][0-9]*:[^-][^ ]*$' || + fail "missing hook fire formats: $shown" # User hooks are options, but show-hooks should list only registered @ hooks # and not ordinary user options. diff --git a/regress/set-hook-B.sh b/regress/set-hook-B.sh index a6b93ce04..11f8d7f86 100644 --- a/regress/set-hook-B.sh +++ b/regress/set-hook-B.sh @@ -81,6 +81,11 @@ assert_unchanged @seen 0 $TMUX rename-session two || fail "rename-session two failed" wait_for @seen '@session-name:two' +shown=$($TMUX show-hooks -g -BF '#{hook_fire_count}:#{t/p:hook_fire_time}' \ + @session-name) || + fail "show-hooks -BF fire failed" +echo "$shown" | grep -q '^1:[^-][^ ]*$' || + fail "unexpected show-hooks -BF fire output: $shown" $TMUX set -g @seen-last 0 || fail "set @seen-last failed" $TMUX set-hook -g -B '@session-name::#{session_name}' \ From a5ad1914b8b9dae3d1e8f26b2e1440d7355340a3 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 28 Jul 2026 07:04:18 +0000 Subject: [PATCH 43/85] Add a warning about shell metacharacters, from Nikolas Skarlatos. --- tmux.1 | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tmux.1 b/tmux.1 index b12a9fbdc..ed8f1b5be 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1150 2026/07/27 19:15:58 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1151 2026/07/28 07:04:18 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 27 2026 $ +.Dd $Mdocdate: July 28 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -7034,6 +7034,20 @@ escape command arguments; with .Ql s use single quotes. +.Pp +Some variables such as +.Ic pane_title +and +.Ic pane_path +are set by escape sequences received from pane and may contain arbitrary +content including shell metacharacters. +.Ql q:\& +should be used to escape these with commands passed to +.Ic run\-shell , +.Ic if\-shell , +or +.Ic pipe\-pane . +.Pp .Ql E:\& will expand the format twice, for example .Ql #{E:status\-left} From 9c402fa7b70c5328ea183fa2f18a7eaf44c0857d Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 28 Jul 2026 11:35:36 +0100 Subject: [PATCH 44/85] Test hook time in customize mode. --- regress/customize-mode.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/regress/customize-mode.sh b/regress/customize-mode.sh index 89ebc6a42..a80b7e2a5 100644 --- a/regress/customize-mode.sh +++ b/regress/customize-mode.sh @@ -280,6 +280,7 @@ test_user_hook() settle assert_equals "$($TMUX show-option -qv @cm_hook)" \ "display-message hook" "add user hook" + $TMUX set-hook -E @cm_hook || fail "fire user hook failed" send q wait_mode 0 @@ -287,7 +288,10 @@ test_user_hook() repeat_key j 3 send Right j settle - assert_contains "$(capture)" "@cm_hook" "user hook shown as hook" + screen=$(capture) + assert_contains "$screen" "@cm_hook" "user hook shown as hook" + assert_contains "$screen" "This hook has been fired 1 times, last " \ + "user hook fire count and time" send u y settle @@ -302,10 +306,17 @@ test_hook_array() $TMUX set-hook -g 'after-new-session[0]' 'display-message old' || fail "set hook array failed" + $TMUX set-hook -g -R after-new-session || fail "fire hook array failed" open_customize '#{==:#{option_name},after-new-session}' repeat_key j 3 - send Right j Right j + send Right j + settle + screen=$(capture) + assert_contains "$screen" "This hook has been fired " \ + "array hook fire count" + assert_contains "$screen" ", last " "array hook fire time" + send Right j settle send a From 6a13d61b48d01ed288ad033f661df5fdb27cf501 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 28 Jul 2026 10:35:31 +0000 Subject: [PATCH 45/85] Show hook count and time in customize mode. --- window-customize.c | 56 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/window-customize.c b/window-customize.c index 624ef21d7..1bf311ff2 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-customize.c,v 1.35 2026/07/15 12:45:39 nicm Exp $ */ +/* $OpenBSD: window-customize.c,v 1.36 2026/07/28 10:35:31 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -276,6 +276,36 @@ window_customize_scope_text(enum window_customize_scope scope, return (s); } +static int +window_customize_write_hook_fire(struct screen_write_ctx *ctx, u_int cx, + u_int sx, u_int sy, struct options_entry *o) +{ + char *fire_time_string; + u_int fire_count; + time_t fire_time; + + if (options_get_monitor_data(o) != NULL) { + fire_count = hooks_monitor_get_fire_count(o); + fire_time = hooks_monitor_get_fire_time(o); + } else { + fire_count = options_get_fire_count(o); + fire_time = options_get_fire_time(o); + } + if (fire_time != 0) { + fire_time_string = format_pretty_time(fire_time, 0); + if (!screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, + "This hook has been fired %u times, last %s.", fire_count, + fire_time_string)) { + free(fire_time_string); + return (0); + } + free(fire_time_string); + return (1); + } + return (screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, + "This hook has been fired %u times.", fire_count)); +} + static struct window_customize_itemdata * window_customize_add_item(struct window_customize_modedata *data) { @@ -654,7 +684,7 @@ window_customize_build_option(struct window_customize_modedata *data, char *text, *expanded, *value; int global = 0, array = 0; int is_hook = 0, is_monitor = 0; - int is_user_hook = 0; + int is_user_hook = 0, is_any_hook; uint64_t tag; if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)) @@ -663,13 +693,14 @@ window_customize_build_option(struct window_customize_modedata *data, is_monitor = 1; if (*name == '@' && hooks_is_event(name)) is_user_hook = 1; + is_any_hook = (is_hook || is_monitor || is_user_hook); switch (type) { case WINDOW_CUSTOMIZE_OPTIONS: - if (is_hook || is_monitor || is_user_hook) + if (is_any_hook) return (0); break; case WINDOW_CUSTOMIZE_HOOKS: - if (!is_hook && !is_monitor && !is_user_hook) + if (!is_any_hook) return (0); break; } @@ -1188,6 +1219,7 @@ window_customize_draw_option(struct window_customize_modedata *data, struct cmd_find_state fs; struct format_tree *ft; int is_hook, is_monitor, is_user_hook; + int is_any_hook; if (!window_customize_check_item(data, item, &fs)) return; @@ -1201,6 +1233,7 @@ window_customize_draw_option(struct window_customize_modedata *data, is_hook = (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)); is_monitor = (options_get_monitor_data(o) != NULL); is_user_hook = (*name == '@' && hooks_is_event(name)); + is_any_hook = (is_hook || is_monitor || is_user_hook); if (oe != NULL && oe->unit != NULL) { space = " "; @@ -1267,8 +1300,16 @@ monitor: } if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) { if (is_hook) { - if (array_key == NULL) + if (array_key == NULL) { + if (!screen_write_text(ctx, cx, sx, + sy - (s->cy - cy), 0, &grid_default_cell, + "This is an array hook.")) + goto out; + if (!window_customize_write_hook_fire(ctx, cx, + sx, sy - (s->cy - cy), o)) + goto out; goto out; + } } else if (array_key != NULL) { if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, @@ -1294,10 +1335,13 @@ monitor: default_value = NULL; } } - if (is_hook || is_monitor || is_user_hook) { + if (is_any_hook) { if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), 0, "Hook command: ", "%s%s%s", value, space, unit)) goto out; + if (!window_customize_write_hook_fire(ctx, cx, sx, + sy - (s->cy - cy), o)) + goto out; } else { if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), 0, "Option value: ", "%s%s%s", value, space, unit)) From 7cc0c11cde1b9e9ad193cbd2d10e01b0d6dbaa55 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 28 Jul 2026 14:18:02 +0100 Subject: [PATCH 46/85] Add test for mouse menu position. --- regress/menu-mouse.sh | 66 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 regress/menu-mouse.sh diff --git a/regress/menu-mouse.sh b/regress/menu-mouse.sh new file mode 100644 index 000000000..e1b53b55f --- /dev/null +++ b/regress/menu-mouse.sh @@ -0,0 +1,66 @@ +#!/bin/sh + +# Check that mouse selection in an active menu uses the correct coordinates +# when the status line is at the top. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server >/dev/null 2>&1 + $TMUX2 kill-server >/dev/null 2>&1 +} +fail() +{ + echo "$*" >&2 + cleanup + exit 1 +} + +# click COL ROW +# +# Write an SGR mouse press then release at a 1-based position to the outer pane +# holding the inner client. +click() +{ + col="$1" + row="$2" + + seq=$(printf '\033[<0;%s;%sM\033[<0;%s;%sm' \ + "$col" "$row" "$col" "$row") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} + +cleanup + +$TMUX new-session -d -s inner -x 80 -y 24 'sleep 100' || exit 1 +$TMUX set -g mouse on || exit 1 +$TMUX set -g status-position top || exit 1 +$TMUX set -g @menu-choice '' || exit 1 + +$TMUX2 new-session -d -x 80 -y 24 "$TMUX attach -t inner" || exit 1 +sleep 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." + +$TMUX display-menu -M -x 5 -y 7 \ + "First item" f "set -g @menu-choice first" \ + "Second item" s "set -g @menu-choice second" || exit 1 +sleep 1 + +# -y is the bottom of the menu, so with four menu lines this puts the menu at +# window y=3. The first item is then at window y=4. With one status line at the +# top, this is terminal row 6 in SGR's 1-based coordinates. +click 8 6 + +choice=$($TMUX show -gv @menu-choice 2>/dev/null) +[ "$choice" = "first" ] || fail "got '$choice', expected 'first'" + +cleanup +exit 0 From 9f2c535909ae0a369e6006b9cf32d8186c6a3788 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 28 Jul 2026 13:17:45 +0000 Subject: [PATCH 47/85] Store status position in new mouse event for menus so the position is correct later. --- server-client.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server-client.c b/server-client.c index 081862e43..d389dacef 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.499 2026/07/22 08:19:14 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.500 2026/07/28 13:17:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -1687,6 +1687,9 @@ server_client_handle_menu_key(struct client *c, struct key_event *event) memcpy(&new_event, event, sizeof new_event); if (KEYC_IS_MOUSE(event->key)) { m = &new_event.m; + m->statusat = status_at_line(c); + m->statuslines = status_line_size(c); + tty_window_offset(&c->tty, &ox, &oy, &sx, &sy); m->x += ox; if (m->statusat == 0) { @@ -1694,8 +1697,7 @@ server_client_handle_menu_key(struct client *c, struct key_event *event) m->x = m->y = UINT_MAX; else m->y = m->y - m->statuslines + oy; - } else if (m->statusat > 0 && - m->y >= (u_int)m->statusat) + } else if (m->statusat > 0 && m->y >= (u_int)m->statusat) m->x = m->y = UINT_MAX; else m->y += oy; From 6f41dac8983cbab517413d4200fde232828eb576 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 29 Jul 2026 08:58:33 +0000 Subject: [PATCH 48/85] Fix #1 -> #{1} in tmux1, from Rasmus Thystrup Karstensen. --- tmux.1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tmux.1 b/tmux.1 index ed8f1b5be..cb5e1aee7 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1151 2026/07/28 07:04:18 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1152 2026/07/29 08:58:33 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 28 2026 $ +.Dd $Mdocdate: July 29 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -8750,7 +8750,7 @@ values are given, they are available as and so on. For example: .Bd -literal -offset indent -run-shell 'myscript.sh #1 #2' foo bar +run-shell 'myscript.sh #{1} #{2}' foo bar .Ed .Pp With From d6c37ce314667c71c6c80cd194706daabf62ae0b Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 29 Jul 2026 11:10:49 +0100 Subject: [PATCH 49/85] Note SIXEL at end of configure as well. --- configure.ac | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/configure.ac b/configure.ac index 549f4a3b3..b09bc5d4e 100644 --- a/configure.ac +++ b/configure.ac @@ -1137,6 +1137,11 @@ if test "x$enable_asan" = xyes; then else AC_MSG_NOTICE([ASAN: off]) fi +if test "x$enable_sixel" = xyes; then + AC_MSG_NOTICE([SIXEL: on]) +else + AC_MSG_NOTICE([SIXEL: off]) +fi if test "x$enable_debug" = xyes; then AC_MSG_NOTICE([debug: on]) else From 415d265728d044a3b7e83cb1fcdae772e08a0fd3 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 29 Jul 2026 14:06:32 +0000 Subject: [PATCH 50/85] 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 51/85] 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 52/85] 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 53/85] 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 54/85] 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 55/85] 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 56/85] 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 57/85] 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 58/85] 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 59/85] 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 60/85] 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 61/85] 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 62/85] 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 63/85] 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 64/85] 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 65/85] 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 66/85] 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 67/85] 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 68/85] 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 69/85] 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 70/85] 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 71/85] 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 72/85] 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 73/85] 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 74/85] 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 75/85] 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 76/85] 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 77/85] =?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 78/85] 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; } From f466d6289ebeed6fb0d234527761279dd54fd655 Mon Sep 17 00:00:00 2001 From: r1w1s1 Date: Mon, 10 Aug 2026 15:38:55 -0300 Subject: [PATCH 79/85] compat: use socklen_t for getsockopt length --- compat/getpeereid.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compat/getpeereid.c b/compat/getpeereid.c index b79f420ad..ed2ed8743 100644 --- a/compat/getpeereid.c +++ b/compat/getpeereid.c @@ -31,7 +31,7 @@ getpeereid(int s, uid_t *uid, gid_t *gid) { #ifdef HAVE_SO_PEERCRED struct ucred uc; - int len = sizeof uc; + socklen_t len = sizeof uc; if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &uc, &len) == -1) return (-1); From 0ac8138ce388624641b495386b61dba75a1af5c9 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 17 Aug 2026 08:06:09 +0100 Subject: [PATCH 80/85] Add a break-pane test for customize mode. --- regress/mode-mutation.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/regress/mode-mutation.sh b/regress/mode-mutation.sh index 96927e995..aedeaca19 100644 --- a/regress/mode-mutation.sh +++ b/regress/mode-mutation.sh @@ -269,6 +269,22 @@ test_customize_mode() assert_alive "customize-mode exit" } +test_customize_break_pane() +{ + start_client option-break + side=$($TMUX split-window -d -P -F '#{pane_id}' \ + -t option-break:0 'cat') || fail "customize split failed" + + $TMUX customize-mode -t "$side" || fail "customize-mode failed" + wait_mode "$side" 1 + $TMUX break-pane -d -s "$side" || fail "customize break-pane failed" + + assert_alive "customize-mode break-pane" + wait_mode "$side" 1 + $TMUX send-keys -t "$side" q || fail "customize-mode quit failed" + wait_mode "$side" 0 +} + test_copy_mode() { start_client copy-a 'i=0; while [ $i -lt 200 ]; do echo "copy mutation line $i"; i=$((i + 1)); done; cat' @@ -302,6 +318,7 @@ test_choose_tree test_choose_buffer test_choose_client test_customize_mode +test_customize_break_pane test_copy_mode cleanup exit 0 From ec34ff52cc3aeddac6d68c92ede60d05ea261cf3 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 06:45:16 +0000 Subject: [PATCH 81/85] Do not copy too many positions when deleting, GitHub issue 5478 from Uzair Aftab. --- prompt.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prompt.c b/prompt.c index c12a249a9..cc808a54e 100644 --- a/prompt.c +++ b/prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD: prompt.c,v 1.5 2026/07/13 10:29:17 nicm Exp $ */ +/* $OpenBSD: prompt.c,v 1.6 2026/08/17 06:45:16 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -1334,7 +1334,7 @@ process_key: if (pr->index != size) { memmove(pr->buffer + pr->index, pr->buffer + pr->index + 1, - (size + 1 - pr->index) * + (size - pr->index) * sizeof *pr->buffer); goto changed; } From a0d3585c6c5200ebce6e48b1dee06cc4b5467ff5 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 07:04:45 +0000 Subject: [PATCH 82/85] Attach window to session earlier so resize cannot cause customize mode to blow up. Reported by Marcel Partap. --- cmd-break-pane.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd-break-pane.c b/cmd-break-pane.c index dcd4e8188..f2f1aff25 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-break-pane.c,v 1.75 2026/07/23 09:38:27 nicm Exp $ */ +/* $OpenBSD: cmd-break-pane.c,v 1.76 2026/08/17 07:04:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -174,13 +174,14 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) } window_set_fill_cells(w); + if (idx == -1) + idx = -1 - options_get_number(dst_s->options, "base-index"); + wl = session_attach(dst_s, w, idx, &cause); /* can't fail */ + layout_init(w, wp); wp->flags |= PANE_CHANGED; colour_palette_from_option(&wp->palette, wp->options); - if (idx == -1) - idx = -1 - options_get_number(dst_s->options, "base-index"); - wl = session_attach(dst_s, w, idx, &cause); /* can't fail */ window_remove_ref(w, __func__); events_fire_window("window-created", w); window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx); From d384557b636e51bd8b2cab63b52c2a748ee5a134 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 07:33:55 +0000 Subject: [PATCH 83/85] Remove flags from both keys for menus, GitHub issue 5484. --- menu.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/menu.c b/menu.c index aab25d7c5..2816056a4 100644 --- a/menu.c +++ b/menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD: menu.c,v 1.69 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: menu.c,v 1.70 2026/08/17 07:33:55 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -337,6 +337,7 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event) struct cmdq_state *state; enum cmd_parse_status status; char *error; + key_code key; if (KEYC_IS_MOUSE(event->key)) { /* @@ -387,7 +388,8 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event) name = menu->items[i].name; if (name == NULL || *name == '-') continue; - if ((event->key & ~KEYC_MASK_FLAGS) == menu->items[i].key) { + key = (event->key & ~KEYC_MASK_FLAGS); + if (key == (menu->items[i].key & ~KEYC_MASK_FLAGS)) { md->choice = i; goto chosen; } From 9fdd04ae61db00dbf754289ecceb70a84af4e8c1 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 07:52:16 +0000 Subject: [PATCH 84/85] Add default terminal features for Rio, GitHub issue 5489 from Raphael Amorim. --- tty-features.c | 14 +++++++++++++- tty-keys.c | 4 +++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tty-features.c b/tty-features.c index 614c63f62..62ec327b8 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-features.c,v 1.40 2026/07/01 06:17:58 nicm Exp $ */ +/* $OpenBSD: tty-features.c,v 1.41 2026/08/17 07:52:16 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -586,6 +586,18 @@ tty_default_features(int *feat, const char *name, u_int version) "usstyle," "progressbar" }, + { .name = "Rio", + .features = TTY_FEATURES_BASE_MODERN_XTERM "," + "ccolour," + "cstyle," + "focus," + "overline," + "hyperlinks," + "osc7," + "sync," + "usstyle," + "progressbar" + }, { .name = "XTerm", /* * xterm also supports DECSLRM and DECFRA, but they can be diff --git a/tty-keys.c b/tty-keys.c index 2d4454b6c..7ece78639 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-keys.c,v 1.211 2026/07/21 07:12:49 nicm Exp $ */ +/* $OpenBSD: tty-keys.c,v 1.212 2026/08/17 07:52:16 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1667,6 +1667,8 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, tty_default_features(features, "WezTerm", 0); else if (strncmp(tmp, "ghostty ", 8) == 0) tty_default_features(features, "ghostty", 0); + else if (strncmp(tmp, "Rio ", 4) == 0) + tty_default_features(features, "Rio", 0); log_debug("%s: received extended DA %.*s", c->name, (int)*size, buf); free(c->term_type); From 1729bb8e8af4a1deddd772761e8c058092cd4cfe Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 07:56:56 +0000 Subject: [PATCH 85/85] If writing a file fails, propagate the error to the server via a new message. Use a client flag rather than bumping the protocol version. GitHub issue 5451. --- client.c | 4 +-- file.c | 86 ++++++++++++++++++++++++++++++++++++++----------- server-client.c | 6 +++- tmux-protocol.h | 10 ++++-- tmux.h | 5 +-- 5 files changed, 86 insertions(+), 25 deletions(-) diff --git a/client.c b/client.c index f413dc819..a59216823 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: client.c,v 1.166 2026/07/10 15:45:11 nicm Exp $ */ +/* $OpenBSD: client.c,v 1.167 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -277,7 +277,7 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, proc_set_signals(client_proc, client_signal); /* Save the flags. */ - client_flags = flags; + client_flags = flags|CLIENT_WRITE_ACK; log_debug("flags are %#llx", (unsigned long long)client_flags); /* Initialize the client socket and start the server. */ diff --git a/file.c b/file.c index 4207f8053..fd73f98df 100644 --- a/file.c +++ b/file.c @@ -1,4 +1,4 @@ -/* $OpenBSD: file.c,v 1.21 2026/07/26 15:08:15 nicm Exp $ */ +/* $OpenBSD: file.c,v 1.22 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -505,7 +505,8 @@ file_push(struct client_file *cf) } else if (cf->stream > 2) { close.stream = cf->stream; proc_send(cf->peer, MSG_WRITE_CLOSE, -1, &close, sizeof close); - file_fire_done(cf); + if (cf->c == NULL || (~cf->c->flags & CLIENT_WRITE_ACK)) + file_fire_done(cf); } free(msg); } @@ -530,14 +531,48 @@ file_write_left(struct client_files *files) return (waiting != 0); } +/* Finish writing a client file. */ +static void +file_write_finished(struct client_file *cf) +{ + struct msg_write_done msg; + + if (cf->event != NULL) { + bufferevent_free(cf->event); + cf->event = NULL; + } + if (cf->fd != -1) { + if (close(cf->fd) != 0 && cf->error == 0) + cf->error = errno; + cf->fd = -1; + } + + msg.stream = cf->stream; + msg.error = cf->error; + proc_send(cf->peer, MSG_WRITE_DONE, -1, &msg, sizeof msg); + + if (cf->cb != NULL) + cf->cb(NULL, NULL, 0, -1, NULL, cf->data); + file_free(cf); +} + /* Client file write error callback. */ static void -file_write_error_callback(__unused struct bufferevent *bev, __unused short what, +file_write_error_callback(__unused struct bufferevent *bev, short what, void *arg) { struct client_file *cf = arg; + int error; + + if (what & EVBUFFER_ERROR) + error = errno; + else + error = EIO; + if (error == 0) + error = EIO; log_debug("write error file %d", cf->stream); + cf->error = error; bufferevent_free(cf->event); cf->event = NULL; @@ -545,7 +580,9 @@ file_write_error_callback(__unused struct bufferevent *bev, __unused short what, close(cf->fd); cf->fd = -1; - if (cf->cb != NULL) + if (cf->closed) + file_write_finished(cf); + else if (cf->cb != NULL) cf->cb(NULL, NULL, 0, -1, NULL, cf->data); } @@ -557,15 +594,10 @@ file_write_callback(__unused struct bufferevent *bev, void *arg) log_debug("write check file %d", cf->stream); - if (cf->cb != NULL) + if (cf->closed && EVBUFFER_LENGTH(cf->event->output) == 0) + file_write_finished(cf); + else if (cf->cb != NULL) cf->cb(NULL, NULL, 0, -1, NULL, cf->data); - - if (cf->closed && EVBUFFER_LENGTH(cf->event->output) == 0) { - bufferevent_free(cf->event); - close(cf->fd); - RB_REMOVE(client_files, cf->tree, cf); - file_free(cf); - } } /* Handle a file write open message (client). */ @@ -666,14 +698,10 @@ file_write_close(struct client_files *files, struct imsg *imsg) if ((cf = RB_FIND(client_files, files, &find)) == NULL) fatalx("unknown stream number"); log_debug("close file %d", cf->stream); + cf->closed = 1; if (cf->event == NULL || EVBUFFER_LENGTH(cf->event->output) == 0) { - if (cf->event != NULL) - bufferevent_free(cf->event); - if (cf->fd != -1) - close(cf->fd); - RB_REMOVE(client_files, files, cf); - file_free(cf); + file_write_finished(cf); } } @@ -832,6 +860,28 @@ file_write_ready(struct client_files *files, struct imsg *imsg) return (0); } +/* Handle a write done message (server). */ +int +file_write_done(struct client_files *files, struct imsg *imsg) +{ + struct msg_write_done *msg = imsg->data; + size_t msglen = imsg->hdr.len - IMSG_HEADER_SIZE; + struct client_file find, *cf; + + if (msglen != sizeof *msg) + return (-1); + find.stream = msg->stream; + if ((cf = RB_FIND(client_files, files, &find)) == NULL) + return (0); + if (cf->c == NULL || (~cf->c->flags & CLIENT_WRITE_ACK)) + return (0); + + log_debug("file %d write done", cf->stream); + cf->error = msg->error; + file_fire_done(cf); + return (0); +} + /* Handle read data message (server). */ int file_read_data(struct client_files *files, struct imsg *imsg) diff --git a/server-client.c b/server-client.c index 27b7688fe..4fe4917ad 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.502 2026/08/04 11:18:22 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.503 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -2700,6 +2700,10 @@ server_client_dispatch(struct imsg *imsg, void *arg) if (file_write_ready(&c->files, imsg) != 0) goto bad; break; + case MSG_WRITE_DONE: + if (file_write_done(&c->files, imsg) != 0) + goto bad; + break; case MSG_READ: if (file_read_data(&c->files, imsg) != 0) goto bad; diff --git a/tmux-protocol.h b/tmux-protocol.h index d823ce7e2..396f10bc9 100644 --- a/tmux-protocol.h +++ b/tmux-protocol.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux-protocol.h,v 1.2 2023/01/06 07:09:27 nicm Exp $ */ +/* $OpenBSD: tmux-protocol.h,v 1.3 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2021 Nicholas Marriott @@ -67,7 +67,8 @@ enum msgtype { MSG_WRITE, MSG_WRITE_READY, MSG_WRITE_CLOSE, - MSG_READ_CANCEL + MSG_READ_CANCEL, + MSG_WRITE_DONE }; /* @@ -116,4 +117,9 @@ struct msg_write_close { int stream; }; +struct msg_write_done { + int stream; + int error; +}; + #endif /* TMUX_PROTOCOL_H */ diff --git a/tmux.h b/tmux.h index 15b997b76..666f901e4 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1422 2026/08/05 08:54:56 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1423 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2252,7 +2252,7 @@ struct client { /* 0x800000000ULL unused */ #define CLIENT_BRACKETPASTING 0x1000000000ULL #define CLIENT_ASSUMEPASTING 0x2000000000ULL -/* 0x4000000000ULL unused */ +#define CLIENT_WRITE_ACK 0x4000000000ULL #define CLIENT_NO_DETACH_ON_DESTROY 0x8000000000ULL #define CLIENT_ALLREDRAWFLAGS \ (CLIENT_REDRAWWINDOW| \ @@ -3235,6 +3235,7 @@ void file_write_close(struct client_files *, struct imsg *); void file_read_open(struct client_files *, struct tmuxpeer *, struct imsg *, int, int, client_file_cb, void *); int file_write_ready(struct client_files *, struct imsg *); +int file_write_done(struct client_files *, struct imsg *); int file_read_data(struct client_files *, struct imsg *); int file_read_done(struct client_files *, struct imsg *); void file_read_cancel(struct client_files *, struct imsg *);