From 418ddada23d290ab02417b62213b402a08f4d9b5 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 21 Jul 2026 07:12:49 +0000 Subject: [PATCH 01/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] =?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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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))