From bbd4768bb62354796bbb5fb7ab978c436f808559 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 8 Jun 2026 21:08:37 +0100 Subject: [PATCH 01/38] 3.7-rc version. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 093d855bb..09451ed2f 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ # configure.ac -AC_INIT([tmux], next-3.7) +AC_INIT([tmux], 3.7-rc) AC_PREREQ([2.60]) AC_CONFIG_AUX_DIR(etc) From 8771b6051f35d6fcd1a001d6b999e32901826c29 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 8 Jun 2026 23:06:21 +0000 Subject: [PATCH 02/38] Fix mouse events on tiled pane status line - when panes share a border, prefer the pane for which the border is the status line. With Dane Jensen. --- server-client.c | 8 ++------ window.c | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/server-client.c b/server-client.c index 25719f366..20536804e 100644 --- a/server-client.c +++ b/server-client.c @@ -695,12 +695,8 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, bdr_bottom = fwp->yoff + fwp->sy; if (py == bdr_bottom) break; - if (window_pane_is_floating(wp)) { - /* Floating pane, check top border. */ - bdr_top = fwp->yoff - 1; - if (py == bdr_top) - break; - } + if (py == bdr_top) + break; } } if (fwp != NULL) diff --git a/window.c b/window.c index 0d86ac508..314415889 100644 --- a/window.c +++ b/window.c @@ -628,12 +628,32 @@ window_get_active_at(struct window *w, u_int x, u_int y) pane_status = options_get_number(w->options, "pane-border-status"); + if (pane_status == PANE_STATUS_TOP) { + /* + * Prefer a pane's top border status line over the pane above's + * bottom border. + */ + TAILQ_FOREACH(wp, &w->z_index, zentry) { + if (!window_pane_visible(wp) || window_pane_is_floating(wp)) + continue; + + window_pane_full_size_offset(wp, &xoff, &yoff, &sx, &sy); + if ((int)x < xoff || x > xoff + sx) + continue; + if ((int)y == yoff - 1) + return (wp); + } + } + TAILQ_FOREACH(wp, &w->z_index, zentry) { if (!window_pane_visible(wp)) continue; window_pane_full_size_offset(wp, &xoff, &yoff, &sx, &sy); if (!window_pane_is_floating(wp)) { - /* Tiled - to and including bottom or right border. */ + /* + * Tiled - to and including the right border, excluding + * the bottom border. + */ if ((int)x < xoff || x > xoff + sx) continue; if (pane_status == PANE_STATUS_TOP) { From aa1f0653e902e98b394b41f05860387c8b709818 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 9 Jun 2026 09:47:18 +0100 Subject: [PATCH 03/38] Bump version to 3.7-rc2. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 09451ed2f..63541c2df 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ # configure.ac -AC_INIT([tmux], 3.7-rc) +AC_INIT([tmux], 3.7-rc2) AC_PREREQ([2.60]) AC_CONFIG_AUX_DIR(etc) From cc87db74c78c28a6f9c6619ceae3ae97a0b95c53 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 9 Jun 2026 11:49:36 +0000 Subject: [PATCH 04/38] Two fixes for RI codepoints. Firstly, do not combine more than two of them - previously we were ending up with four codepoints in one cell which tmux believed to be width 2, but terminals considered width 4. Secondly, invalidate cursor position before redrawing the cell when the second codepoint is received, terminals vary in how they manage backspace and cursor movement across these characters, so it is better to use absolute rather than relative positioning. GitHub issue 4853. --- tty.c | 2 ++ utf8-combined.c | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tty.c b/tty.c index f201ba1d1..5fd29130b 100644 --- a/tty.c +++ b/tty.c @@ -2051,6 +2051,8 @@ tty_cmd_cell(struct tty *tty, const struct tty_ctx *ctx) tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_margin_off(tty); + if (ctx->flags & TTY_CTX_CELL_INVALIDATE) + tty_invalidate(tty); tty_cursor_pane_unless_wrap(tty, ctx, ctx->ocx, ctx->ocy); tty_cell(tty, ctx->cell, &ctx->defaults, ctx->palette, diff --git a/utf8-combined.c b/utf8-combined.c index 65ecf9cdc..923342b43 100644 --- a/utf8-combined.c +++ b/utf8-combined.c @@ -82,6 +82,23 @@ utf8_is_hangul_filler(const struct utf8_data *ud) return (memcmp(ud->data, "\343\205\244", 3) == 0); } +/* Count regional indicator characters. */ +static u_int +utf8_regional_count(const struct utf8_data *ud) +{ + u_int count = 0, i; + + for (i = 0; i + 4 <= ud->size; i++) { + if (ud->data[i] == 0xf0 && + ud->data[i + 1] == 0x9f && + ud->data[i + 2] == 0x87 && + ud->data[i + 3] >= 0xa6 && + ud->data[i + 3] <= 0xbf) + count++; + } + return (count); +} + /* Should these two characters combine? */ int utf8_should_combine(const struct utf8_data *with, const struct utf8_data *add) @@ -94,8 +111,13 @@ utf8_should_combine(const struct utf8_data *with, const struct utf8_data *add) return (0); /* Regional indicators. */ - if ((a >= 0x1F1E6 && a <= 0x1F1FF) && (w >= 0x1F1E6 && w <= 0x1F1FF)) + if ((a >= 0x1F1E6 && a <= 0x1F1FF) && (w >= 0x1F1E6 && w <= 0x1F1FF)) { + if (utf8_regional_count(with) != 1) + return (0); + if (utf8_regional_count(add) != 1) + return (0); return (1); + } /* Emoji skin tone modifiers. */ switch (a) { From dabaae1534f206dd8513429b882481e6221a6afd Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 11 Jun 2026 23:01:31 +0000 Subject: [PATCH 05/38] Make buffer creation time sort oldest first like it used to, but change windows to match sessions and panes as newest first. --- sort.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sort.c b/sort.c index fda6a90ba..212da22ea 100644 --- a/sort.c +++ b/sort.c @@ -65,7 +65,12 @@ sort_buffer_cmp(const void *a0, const void *b0) result = strcmp(pa->name, pb->name); break; case SORT_CREATION: - result = pa->order - pb->order; + if (pa->order > pb->order) + result = -1; + else if (pa->order < pb->order) + result = 1; + else + result = 0; break; case SORT_SIZE: result = pa->size - pb->size; @@ -251,11 +256,11 @@ sort_winlink_cmp(const void *a0, const void *b0) break; case SORT_CREATION: if (timercmp(&wa->creation_time, &wb->creation_time, >)) { - result = -1; + result = 1; break; } if (timercmp(&wa->creation_time, &wb->creation_time, <)) { - result = 1; + result = -1; break; } break; From 83e45343875bb39c5854abf2c88856a10375c7c2 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 14 Jun 2026 20:37:57 +0000 Subject: [PATCH 06/38] Skip floating panes when working out the top or bottom cell. Fixes missing bottom status pane status line when floating panes exist. --- layout.c | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/layout.c b/layout.c index 28c66f1eb..da98b590e 100644 --- a/layout.c +++ b/layout.c @@ -292,15 +292,22 @@ layout_fix_offsets(struct window *w) static int layout_cell_is_top(struct window *w, struct layout_cell *lc) { - struct layout_cell *next; + struct layout_cell *next, *edge; while (lc != w->layout_root) { next = lc->parent; if (next == NULL) return (0); - if (next->type == LAYOUT_TOPBOTTOM && - lc != TAILQ_FIRST(&next->cells)) - return (0); + if (next->type == LAYOUT_TOPBOTTOM) { + edge = TAILQ_FIRST(&next->cells); + while (edge != NULL) { + if (~edge->flags & LAYOUT_CELL_FLOATING) + break; + edge = TAILQ_NEXT(edge, entry); + } + if (lc != edge) + return (0); + } lc = next; } return (1); @@ -310,15 +317,22 @@ layout_cell_is_top(struct window *w, struct layout_cell *lc) static int layout_cell_is_bottom(struct window *w, struct layout_cell *lc) { - struct layout_cell *next; + struct layout_cell *next, *edge; while (lc != w->layout_root) { next = lc->parent; if (next == NULL) return (0); - if (next->type == LAYOUT_TOPBOTTOM && - lc != TAILQ_LAST(&next->cells, layout_cells)) - return (0); + if (next->type == LAYOUT_TOPBOTTOM) { + edge = TAILQ_LAST(&next->cells, layout_cells); + while (edge != NULL) { + if (~edge->flags & LAYOUT_CELL_FLOATING) + break; + edge = TAILQ_PREV(edge, layout_cells, entry); + } + if (lc != edge) + return (0); + } lc = next; } return (1); From 78afc24736483941d27288708a051b275596d253 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 14 Jun 2026 20:53:20 +0000 Subject: [PATCH 07/38] Fix various errors in redrawing: - Fix the active pane colour when only two panes and scrollbars enabled. - Clip left and right scrollbars the same for floating panes. - Do not subtract scrollbar width twice when working out width of status line. - Check if a character is inside a visible range correctly (do not include the next position outside the range). --- screen-redraw.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/screen-redraw.c b/screen-redraw.c index 159c86f8b..ca6cf0cd1 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -211,7 +211,7 @@ screen_redraw_pane_border(struct screen_redraw_ctx *ctx, struct window_pane *wp, } /* Top/bottom borders. */ - if (vsplit && pane_status == PANE_STATUS_OFF && sb_w == 0) { + if (vsplit && pane_status == PANE_STATUS_OFF) { if (wp->yoff == 0 && py == sy && px <= sx / 2) return (SCREEN_REDRAW_BORDER_BOTTOM); if (wp->yoff != 0 && py == wp->yoff - 1 && px > sx / 2) @@ -220,9 +220,10 @@ screen_redraw_pane_border(struct screen_redraw_ctx *ctx, struct window_pane *wp, if (sb_pos == PANE_SCROLLBARS_LEFT) { if ((wp->xoff - sb_w == 0 || px >= wp->xoff - sb_w) && (px <= ex || (sb_w != 0 && px < ex + sb_w))) { - if (wp->yoff != 0 && py == wp->yoff - 1) + if (pane_status != PANE_STATUS_BOTTOM && + wp->yoff != 0 && py == wp->yoff - 1) return (SCREEN_REDRAW_BORDER_TOP); - if (py == ey) + if (pane_status != PANE_STATUS_TOP && py == ey) return (SCREEN_REDRAW_BORDER_BOTTOM); } } else { /* sb_pos == PANE_SCROLLBARS_RIGHT */ @@ -617,7 +618,7 @@ screen_redraw_make_pane_status(struct client *c, struct window_pane *wp, width = 0; else width = wp->sx + sb_w - 2; - max_width = (int)w->sx - (wp->xoff + 2) - sb_w; + max_width = (int)w->sx - (wp->xoff + 2); if (max_width < 0) max_width = 0; if (width > (u_int)max_width) @@ -701,7 +702,7 @@ screen_redraw_draw_pane_status(struct screen_redraw_ctx *ctx) width = size; } else if (xoff < ctx->ox && xoff + size > ctx->ox + ctx->sx) { /* Both left and right not visible. */ - l = ctx->ox; + l = ctx->ox - xoff; x = 0; width = ctx->sx; } else if (xoff < ctx->ox) { @@ -1124,7 +1125,7 @@ screen_redraw_is_visible(struct visible_ranges *r, u_int px) return (1); for (i = 0; i < r->used; i++) { ri = &r->ranges[i]; - if (ri->nx != 0 && px >= ri->px && px <= ri->px + ri->nx) + if (ri->nx != 0 && px >= ri->px && px < ri->px + ri->nx) return (1); } return (0); @@ -1146,11 +1147,13 @@ screen_redraw_get_visible_ranges(struct window_pane *base_wp, int px, u_int lb, rb, tb, bb; u_int i, s; - if (px + width <= 0 || py < 0) + if (py < 0 || width == 0) goto empty; if (px < 0) { + if ((u_int)-px >= width) + goto empty; + width -= (u_int)-px; px = 0; - width += px; } if (base_wp == NULL) { @@ -1197,7 +1200,8 @@ screen_redraw_get_visible_ranges(struct window_pane *base_wp, int px, (u_int)py < tb || (u_int)py > bb) continue; - if (!window_pane_is_floating(wp) && (u_int)py == bb) + if (!window_pane_is_floating(wp) && + ((u_int)py == tb || (u_int)py == bb)) continue; sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; From f98aaf544d42ed34e068f9aa67ad71aab1866d3f Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 15 Jun 2026 07:40:45 +0000 Subject: [PATCH 08/38] When redrawing a whole pane line, fall into tty_draw_line for any characters that are not plain ASCII. Fixes redraw issues with partial tabs and other wide characters when a pane is partly outside the window. --- screen-write.c | 61 ++++++++++++++++++++++++++++++++++++++++---------- tmux.h | 1 + tty.c | 20 +++++++++++++++++ 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/screen-write.c b/screen-write.c index ceec45a34..cee1afdbc 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1122,6 +1122,25 @@ screen_write_backspace(struct screen_write_ctx *ctx) screen_write_set_cursor(ctx, cx, cy); } +/* Is this cell a single ASCII character? */ +static int +screen_write_cell_is_single(const struct grid_cell *gc) +{ + if (gc->data.width != 1) + return (0); + if (gc->data.size != 1) + return (0); + if (*gc->data.data < 0x20 || *gc->data.data == 0x7f) + return (0); + if (gc->flags & GRID_FLAG_CLEARED) + return (0); + if (gc->flags & GRID_FLAG_PADDING) + return (0); + if (gc->flags & GRID_FLAG_TAB) + return (0); + return (1); +} + /* Redraw all visible cells on a line. */ static void screen_write_redraw_line(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, @@ -1130,11 +1149,14 @@ screen_write_redraw_line(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, struct window_pane *wp = ctx->wp; struct screen *s = ctx->s; struct grid_cell gc, ngc; - u_int sx = screen_size_x(s), cx, i, n; + u_int sx = screen_size_x(s), cx, i; int xoff = wp->xoff, yoff = wp->yoff; struct visible_ranges *r; struct visible_range *ri; + if (s->mode & MODE_SYNC) + return; + r = screen_redraw_get_visible_ranges(wp, xoff, yoff + yy, sx, NULL); for (i = 0; i < r->used; i++) { ri = &r->ranges[i]; @@ -1142,19 +1164,34 @@ screen_write_redraw_line(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, continue; cx = ri->px - xoff; - for (n = 0; n < ri->nx && cx < sx; n++, cx++) { - grid_view_get_cell(s->grid, cx, yy, &gc); - if (~gc.flags & GRID_FLAG_SELECTED) - ttyctx->cell = &gc; - else { - screen_select_cell(s, &ngc, &gc); - ttyctx->cell = &ngc; - } + if (cx >= sx) + continue; + if (cx + ri->nx > sx) + ttyctx->n = sx - cx; + else + ttyctx->n = ri->nx; + if (ttyctx->n == 0) + continue; + ttyctx->ocx = cx; + ttyctx->ocy = yy; - ttyctx->ocx = cx; - ttyctx->ocy = yy; - tty_write(tty_cmd_cell, ttyctx); + if (ttyctx->n != 1) { + tty_write(tty_cmd_redrawline, ttyctx); + continue; } + + grid_view_get_cell(s->grid, cx, yy, &gc); + if (!screen_write_cell_is_single(&gc)) { + tty_write(tty_cmd_redrawline, ttyctx); + continue; + } + if (~gc.flags & GRID_FLAG_SELECTED) + ttyctx->cell = &gc; + else { + screen_select_cell(s, &ngc, &gc); + ttyctx->cell = &ngc; + } + tty_write(tty_cmd_cell, ttyctx); } } diff --git a/tmux.h b/tmux.h index 870e778be..c7a7405c7 100644 --- a/tmux.h +++ b/tmux.h @@ -2727,6 +2727,7 @@ void tty_write(void (*)(struct tty *, const struct tty_ctx *), void tty_cmd_alignmenttest(struct tty *, const struct tty_ctx *); void tty_cmd_cell(struct tty *, const struct tty_ctx *); void tty_cmd_cells(struct tty *, const struct tty_ctx *); +void tty_cmd_redrawline(struct tty *, const struct tty_ctx *); void tty_cmd_clearendofline(struct tty *, const struct tty_ctx *); void tty_cmd_clearendofscreen(struct tty *, const struct tty_ctx *); void tty_cmd_clearline(struct tty *, const struct tty_ctx *); diff --git a/tty.c b/tty.c index 5fd29130b..82cb602d2 100644 --- a/tty.c +++ b/tty.c @@ -1431,6 +1431,26 @@ tty_draw_pane(struct tty *tty, const struct tty_ctx *ctx, u_int py) } } +void +tty_cmd_redrawline(struct tty *tty, const struct tty_ctx *ctx) +{ + u_int i, x, rx, ry, j; + struct visible_ranges *r; + struct visible_range *rr; + + if (tty_clamp_line(tty, ctx, ctx->ocx, ctx->ocy, ctx->n, + &i, &x, &rx, &ry)) { + r = tty_check_overlay_range(tty, x, ry, rx); + for (j = 0; j < r->used; j++) { + rr = &r->ranges[j]; + if (rr->nx == 0) + continue; + tty_draw_line(tty, ctx->s, i + rr->px - x, + ctx->ocy, rr->nx, rr->px, ry, &ctx->style_ctx); + } + } +} + /* Check if character needs to be mapped for codeset. */ const struct grid_cell * tty_check_codeset(struct tty *tty, const struct grid_cell *gc) From 9cf8de099ca9b03206cfd04cca94c2673d8c4569 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 15 Jun 2026 14:56:30 +0000 Subject: [PATCH 09/38] Convert cursor position back to pane coordinates for tty_cmd_cell. --- screen-write.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/screen-write.c b/screen-write.c index cee1afdbc..c711e7b24 100644 --- a/screen-write.c +++ b/screen-write.c @@ -2643,7 +2643,7 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) if (ri->nx == 0) continue; for (n = 0; n < ri->nx; n++) { - ttyctx.ocx = ri->px + n; + ttyctx.ocx = (int)ri->px - xoff + (int)n; tty_write(tty_cmd_cell, &ttyctx); } } From bae5c14941095865b37ac42622296a03549b3db8 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 15 Jun 2026 12:07:49 +0000 Subject: [PATCH 10/38] Tidy up screen_redraw_get_visible_ranges by using a couple of temporaries for start and end of range. --- screen-redraw.c | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/screen-redraw.c b/screen-redraw.c index ca6cf0cd1..d7e81a866 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1144,7 +1144,7 @@ screen_redraw_get_visible_ranges(struct window_pane *base_wp, int px, struct visible_range *ri; static struct visible_ranges sr = { NULL, 0, 0 }; int found_self, sb, sb_w, sb_pos; - u_int lb, rb, tb, bb; + int lb, rb, tb, bb, sx, ex; u_int i, s; if (py < 0 || width == 0) @@ -1197,11 +1197,10 @@ screen_redraw_get_visible_ranges(struct window_pane *base_wp, int px, bb = wp->yoff + wp->sy; if (!found_self || !window_pane_visible(wp) || - (u_int)py < tb || - (u_int)py > bb) + py < tb || + py > bb) continue; - if (!window_pane_is_floating(wp) && - ((u_int)py == tb || (u_int)py == bb)) + if (!window_pane_is_floating(wp) && (py == tb || py == bb)) continue; sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; @@ -1225,34 +1224,29 @@ screen_redraw_get_visible_ranges(struct window_pane *base_wp, int px, rb = wp->xoff + wp->sx; else /* PANE_SCROLLBARS_RIGHT or none. */ rb = wp->xoff + wp->sx + sb_w; - if (rb > w->sx) + if (rb > (int)w->sx) rb = w->sx - 1; - if (lb > ri->px && - lb < ri->px + ri->nx && - rb >= ri->px + ri->nx) - { + sx = ri->px; + ex = sx + ri->nx - 1; + if (lb > sx && lb <= ex && rb > ex) { /* * If the left edge of floating pane falls * inside this range and right edge covers up * to right of range, then shrink left edge of * range. */ - ri->nx = lb - ri->px; - } - else if (rb >= ri->px && - rb < ri->px + ri->nx && - lb <= ri->px) { + ri->nx = lb - sx; + } else if (rb >= sx && rb <= ex && lb <= sx) { /* * Else if the right edge of floating pane falls * inside of this range and left edge covers * the left of range, then move px forward to * right edge of pane. */ - ri->nx = ri->nx - (rb + 1 - ri->px); - ri->px = ri->px + (rb + 1 - ri->px); - } - else if (lb > ri->px && rb < ri->px + ri->nx) { + ri->nx = ex - rb; + ri->px = rb + 1; + } else if (lb > sx && rb <= ex) { /* * Else if pane fully inside range then split * into 2 ranges. @@ -1264,11 +1258,11 @@ screen_redraw_get_visible_ranges(struct window_pane *base_wp, int px, } ri = &r->ranges[i]; r->ranges[i + 1].px = rb + 1; - r->ranges[i + 1].nx = ri->px + ri->nx - (rb + 1); + r->ranges[i + 1].nx = ex - rb; /* ri->px was copied, unchanged. */ - ri->nx = lb - ri->px; + ri->nx = lb - sx; r->used++; - } else if (lb <= ri->px && rb >= ri->px + ri->nx) { + } else if (lb <= sx && rb > ex) { /* * If floating pane completely covers this range * then delete it (make it 0 length). From 2d9afa63485911f56f869516079da0ee0f404e3b Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 15 Jun 2026 11:45:51 +0000 Subject: [PATCH 11/38] Use correct x position when drawing clipped line. --- tty.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tty.c b/tty.c index 82cb602d2..4db26287f 100644 --- a/tty.c +++ b/tty.c @@ -1445,7 +1445,7 @@ tty_cmd_redrawline(struct tty *tty, const struct tty_ctx *ctx) rr = &r->ranges[j]; if (rr->nx == 0) continue; - tty_draw_line(tty, ctx->s, i + rr->px - x, + tty_draw_line(tty, ctx->s, ctx->ocx + i + rr->px - x, ctx->ocy, rr->nx, rr->px, ry, &ctx->style_ctx); } } From 2c4df765c56a052288d92b5f50f8580d0ee29ec0 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 16 Jun 2026 08:53:14 +0000 Subject: [PATCH 12/38] Fix missing border when drawing floating panes. From Michael Grant. --- screen-redraw.c | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/screen-redraw.c b/screen-redraw.c index d7e81a866..17085a411 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -127,7 +127,7 @@ screen_redraw_pane_border(struct screen_redraw_ctx *ctx, struct window_pane *wp, int ex = wp->xoff + wp->sx, ey = wp->yoff + wp->sy; int hsplit = 0, vsplit = 0, pane_status = ctx->pane_status; int pane_scrollbars = ctx->pane_scrollbars, sb_w = 0; - int sb_pos, sx = wp->sx, sy = wp->sy; + int sb_pos, sx = wp->sx, sy = wp->sy, left, right; enum layout_type split_type; if (pane_scrollbars != 0) @@ -145,20 +145,19 @@ screen_redraw_pane_border(struct screen_redraw_ctx *ctx, struct window_pane *wp, /* Floating pane borders. */ if (window_pane_is_floating(wp)) { + left = wp->xoff - 1; + right = wp->xoff + sx; + if (sb_pos == PANE_SCROLLBARS_LEFT) + left -= sb_w; + else + right += sb_w; if (py >= wp->yoff - 1 && py <= wp->yoff + sy) { - if (sb_pos == PANE_SCROLLBARS_LEFT) { - if (px == wp->xoff - 1 - sb_w) - return (SCREEN_REDRAW_BORDER_LEFT); - if (px == wp->xoff + sx) - return (SCREEN_REDRAW_BORDER_RIGHT); - } else { /* PANE_SCROLLBARS_RIGHT or none. */ - if (px == wp->xoff - 1) - return (SCREEN_REDRAW_BORDER_LEFT); - if (px == wp->xoff + sx + sb_w) - return (SCREEN_REDRAW_BORDER_RIGHT); - } + if (px == left) + return (SCREEN_REDRAW_BORDER_LEFT); + if (px == right) + return (SCREEN_REDRAW_BORDER_RIGHT); } - if (px >= wp->xoff && px <= wp->xoff + sx) { + if (px > left && px <= right) { if (py == wp->yoff - 1) return (SCREEN_REDRAW_BORDER_TOP); if (py == wp->yoff + sy) From baac29debc5b7fd3273e0a87874f187c15f89791 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 16 Jun 2026 09:48:04 +0100 Subject: [PATCH 13/38] Prefer libtinfow if it is available, GitHub issue 5224 from Lars Wendler. --- configure.ac | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/configure.ac b/configure.ac index 63541c2df..67d16fdf0 100644 --- a/configure.ac +++ b/configure.ac @@ -297,24 +297,24 @@ fi # Look for ncurses or curses. Try pkg-config first then directly for the # library. PKG_CHECK_MODULES( - LIBTINFO, - tinfo, + LIBTINFOW, + tinfow, [ - AM_CPPFLAGS="$LIBTINFO_CFLAGS $AM_CPPFLAGS" - CPPFLAGS="$LIBTINFO_CFLAGS $SAVED_CPPFLAGS" - LIBS="$LIBTINFO_LIBS $LIBS" + AM_CPPFLAGS="$LIBTINFOW_CFLAGS $AM_CPPFLAGS" + CPPFLAGS="$LIBTINFOW_CFLAGS $SAVED_CPPFLAGS" + LIBS="$LIBTINFOW_LIBS $LIBS" found_ncurses=yes ], found_ncurses=no ) if test "x$found_ncurses" = xno; then PKG_CHECK_MODULES( - LIBNCURSES, - ncurses, + LIBTINFO, + tinfo, [ - AM_CPPFLAGS="$LIBNCURSES_CFLAGS $AM_CPPFLAGS" - CPPFLAGS="$LIBNCURSES_CFLAGS $SAVED_CPPFLAGS" - LIBS="$LIBNCURSES_LIBS $LIBS" + AM_CPPFLAGS="$LIBTINFO_CFLAGS $AM_CPPFLAGS" + CPPFLAGS="$LIBTINFO_CFLAGS $SAVED_CPPFLAGS" + LIBS="$LIBTINFO_LIBS $LIBS" found_ncurses=yes ], found_ncurses=no @@ -333,10 +333,23 @@ if test "x$found_ncurses" = xno; then found_ncurses=no ) fi +if test "x$found_ncurses" = xno; then + PKG_CHECK_MODULES( + LIBNCURSES, + ncurses, + [ + AM_CPPFLAGS="$LIBNCURSES_CFLAGS $AM_CPPFLAGS" + CPPFLAGS="$LIBNCURSES_CFLAGS $SAVED_CPPFLAGS" + LIBS="$LIBNCURSES_LIBS $LIBS" + found_ncurses=yes + ], + found_ncurses=no + ) +fi if test "x$found_ncurses" = xno; then AC_SEARCH_LIBS( setupterm, - [tinfo terminfo ncurses ncursesw], + [tinfow tinfo terminfo ncursesw ncurses], found_ncurses=yes, found_ncurses=no ) From 2f28d9a1685a72fee3cce72d043c767429292c9d Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 16 Jun 2026 08:57:07 +0000 Subject: [PATCH 14/38] Fix resizing floating pane with a left scrollbar, from Michael Grant. --- cmd-resize-pane.c | 24 ++++++++++++++++++------ server-client.c | 22 ++++++++++++++++------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index bae6a691b..d1a8f6723 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -181,6 +181,7 @@ cmd_resize_pane_mouse_update_floating(struct client *c, struct mouse_event *m) struct window_pane *wp; struct layout_cell *lc; int y, ly, x, lx, sx, sy, new_sx, new_sy; + int scrollbars, sb_pos, left, right; int new_xoff, new_yoff, resizes = 0; wp = cmd_mouse_pane(m, NULL, &wl); @@ -192,6 +193,17 @@ cmd_resize_pane_mouse_update_floating(struct client *c, struct mouse_event *m) lc = wp->layout_cell; sx = wp->sx; sy = wp->sy; + scrollbars = options_get_number(w->options, "pane-scrollbars"); + sb_pos = options_get_number(w->options, "pane-scrollbars-position"); + left = wp->xoff - 1; + right = wp->xoff + sx; + if (window_pane_show_scrollbar(wp, scrollbars) && + sb_pos == PANE_SCROLLBARS_LEFT) { + left -= wp->scrollbar_style.width + wp->scrollbar_style.pad; + } else if (window_pane_show_scrollbar(wp, scrollbars) && + sb_pos == PANE_SCROLLBARS_RIGHT) { + right += wp->scrollbar_style.width + wp->scrollbar_style.pad; + } y = m->y + m->oy; x = m->x + m->ox; if (m->statusat == 0 && y >= (int)m->statuslines) @@ -204,7 +216,7 @@ cmd_resize_pane_mouse_update_floating(struct client *c, struct mouse_event *m) else if (m->statusat > 0 && ly >= m->statusat) ly = m->statusat - 1; - if ((lx == wp->xoff - 1 || lx == wp->xoff) && ly == wp->yoff - 1) { + if ((lx == left || lx == left + 1) && ly == wp->yoff - 1) { /* Top left corner. */ new_sx = lc->sx + (lx - x); if (new_sx < PANE_MINIMUM) @@ -216,7 +228,7 @@ cmd_resize_pane_mouse_update_floating(struct client *c, struct mouse_event *m) new_yoff = y + 1; layout_set_size(lc, new_sx, new_sy, new_xoff, new_yoff); resizes++; - } else if ((lx == wp->xoff + sx + 1 || lx == wp->xoff + sx) && + } else if ((lx == right + 1 || lx == right) && ly == wp->yoff - 1) { /* Top right corner. */ new_sx = x - lc->xoff; @@ -228,7 +240,7 @@ cmd_resize_pane_mouse_update_floating(struct client *c, struct mouse_event *m) new_yoff = y + 1; layout_set_size(lc, new_sx, new_sy, lc->xoff, new_yoff); resizes++; - } else if ((lx == wp->xoff - 1 || lx == wp->xoff) && + } else if ((lx == left || lx == left + 1) && ly == wp->yoff + sy) { /* Bottom left corner. */ new_sx = lc->sx + (lx - x); @@ -240,7 +252,7 @@ cmd_resize_pane_mouse_update_floating(struct client *c, struct mouse_event *m) new_xoff = x + 1; layout_set_size(lc, new_sx, new_sy, new_xoff, lc->yoff); resizes++; - } else if ((lx == wp->xoff + sx + 1 || lx == wp->xoff + sx) && + } else if ((lx == right + 1 || lx == right) && ly == wp->yoff + sy) { /* Bottom right corner. */ new_sx = x - lc->xoff; @@ -251,14 +263,14 @@ cmd_resize_pane_mouse_update_floating(struct client *c, struct mouse_event *m) new_sy = PANE_MINIMUM; layout_set_size(lc, new_sx, new_sy, lc->xoff, lc->yoff); resizes++; - } else if (lx == wp->xoff + sx + 1) { + } else if (lx == right) { /* Right border. */ new_sx = x - lc->xoff; if (new_sx < PANE_MINIMUM) return; layout_set_size(lc, new_sx, lc->sy, lc->xoff, lc->yoff); resizes++; - } else if (lx == wp->xoff - 1) { + } else if (lx == left) { /* Left border. */ new_sx = lc->sx + (lx - x); if (new_sx < PANE_MINIMUM) diff --git a/server-client.c b/server-client.c index 20536804e..cf1e824c7 100644 --- a/server-client.c +++ b/server-client.c @@ -627,6 +627,9 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, pane_status_line = wp->yoff + wp->sy; else pane_status_line = -1; /* not used */ + bdr_left = wp->xoff - 1; + if (sb_pos == PANE_SCROLLBARS_LEFT) + bdr_left -= sb_pad + sb_w; /* Check if point is within the pane or scrollbar. */ if (((pane_status != PANE_STATUS_OFF && @@ -656,7 +659,7 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, } else /* py > sl_bottom */ return (KEYC_MOUSE_LOCATION_SCROLLBAR_DOWN); } else if (window_pane_is_floating(wp) && - (px == wp->xoff - 1 || + (px == bdr_left || py == wp->yoff - 1 || py == wp->yoff + (int)wp->sy)) { /* Floating pane left, bottom or top border. */ @@ -671,11 +674,20 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, if ((w->flags & WINDOW_ZOOMED) && (~fwp->flags & PANE_ZOOMED)) continue; + if (window_pane_show_scrollbar(fwp, sb)) { + sb_w = fwp->scrollbar_style.width; + sb_pad = fwp->scrollbar_style.pad; + } else { + sb_w = 0; + sb_pad = 0; + } bdr_top = fwp->yoff - 1; bdr_bottom = fwp->yoff + fwp->sy; - if (sb_pos == PANE_SCROLLBARS_LEFT) + bdr_left = fwp->xoff - 1; + if (sb_pos == PANE_SCROLLBARS_LEFT) { + bdr_left -= sb_pad + sb_w; bdr_right = fwp->xoff + fwp->sx; - else { + } else { /* PANE_SCROLLBARS_RIGHT or none. */ bdr_right = fwp->xoff + fwp->sx + sb_pad + sb_w; } @@ -685,13 +697,11 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, break; if (window_pane_is_floating(wp)) { /* Floating pane, check left border. */ - bdr_left = fwp->xoff - 1; if (px == bdr_left) break; } } - if (px >= fwp->xoff - 1 && - px <= fwp->xoff + (int)fwp->sx) { + if (px >= bdr_left && px <= fwp->xoff + (int)fwp->sx) { bdr_bottom = fwp->yoff + fwp->sy; if (py == bdr_bottom) break; From 1596f9c9f58c0e67ca626be9d76363a4f0c58f37 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 16 Jun 2026 10:22:51 +0100 Subject: [PATCH 15/38] Bump version again. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 67d16fdf0..0cf37da5b 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ # configure.ac -AC_INIT([tmux], 3.7-rc2) +AC_INIT([tmux], 3.7-rc3) AC_PREREQ([2.60]) AC_CONFIG_AUX_DIR(etc) From c809aea278ca1b2bc4827e616024997f579f05f1 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 16 Jun 2026 10:24:06 +0100 Subject: [PATCH 16/38] Fix a merge problem. --- tty.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tty.c b/tty.c index 4db26287f..05919ba1b 100644 --- a/tty.c +++ b/tty.c @@ -1446,7 +1446,8 @@ tty_cmd_redrawline(struct tty *tty, const struct tty_ctx *ctx) if (rr->nx == 0) continue; tty_draw_line(tty, ctx->s, ctx->ocx + i + rr->px - x, - ctx->ocy, rr->nx, rr->px, ry, &ctx->style_ctx); + ctx->ocy, rr->nx, rr->px, ry, &ctx->defaults, + ctx->palette); } } } From 762e503978cfa8db9b0459453a58ef42145a208a Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 16 Jun 2026 09:28:17 +0000 Subject: [PATCH 17/38] Allow rectangle selection to extend past end of current line to behave the same as vi with virtualedit=block set. From Mark Kelly in GitHub issue 5227. --- window-copy.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/window-copy.c b/window-copy.c index 39ae18816..916d12eff 100644 --- a/window-copy.c +++ b/window-copy.c @@ -5172,8 +5172,12 @@ window_copy_update_cursor(struct window_mode_entry *wme, u_int cx, u_int cy) u_int maxx; int allow_onemore; - allow_onemore = (data->screen.sel != NULL && data->rectflag); - if (cy < screen_size_y(s)) { + /* + * Allow rectangle selection to extend past end of current line to + * behave the same as vi with virtualedit=block set. + */ + if (!data->rectflag && cy < screen_size_y(s)) { + allow_onemore = (data->screen.sel != NULL && data->rectflag); py = screen_hsize(data->backing) + cy - data->oy; maxx = window_copy_cursor_limit(wme, py, allow_onemore); if (cx > maxx) From 64e83caf04944f9a45113c32ecaff588a951fb99 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 15 Jun 2026 21:41:39 +0000 Subject: [PATCH 18/38] Be more strict about what names and titles we allow and reject them immediately when possible, but allow # again for those directly set by commands (but not escape sequences). From Barrett Ruth in GitHub issue 5175. --- cmd-break-pane.c | 23 +++++++++++------- cmd-new-session.c | 58 ++++++++++++++++++++++++++------------------ cmd-new-window.c | 33 +++++++++++++++++++------ cmd-rename-session.c | 6 ++--- cmd-rename-window.c | 14 ++++++++--- cmd-select-pane.c | 2 +- cmd-split-window.c | 5 ++-- input.c | 10 ++++---- names.c | 4 +-- paste.c | 2 -- popup.c | 2 +- screen.c | 14 ++++++++--- session.c | 1 - spawn.c | 16 ++++-------- tmux.c | 19 +++++++++++++++ tmux.h | 13 +++++++--- window.c | 6 ++--- 17 files changed, 146 insertions(+), 82 deletions(-) diff --git a/cmd-break-pane.c b/cmd-break-pane.c index 4be989c3e..0d8d83fd9 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -58,9 +58,14 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) struct session *dst_s = target->s; struct window_pane *wp = source->wp; struct window *w = wl->window; - char *name, *cause, *cp; + char *newname, *cause, *cp; int idx = target->idx, before; - const char *template; + const char *template, *name = args_get(args, 'n'); + + if (name != NULL && !check_name(name, WINDOW_NAME_FORBID)) { + cmdq_error(item, "invalid window name: %s", name); + return (CMD_RETURN_ERROR); + } before = args_has(args, 'b'); if (args_has(args, 'a') || before) { @@ -80,8 +85,8 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) free(cause); return (CMD_RETURN_ERROR); } - if (args_has(args, 'n')) { - window_set_name(w, args_get(args, 'n')); + if (name != NULL) { + window_set_name(w, name, WINDOW_NAME_FORBID); options_set_number(w->options, "automatic-rename", 0); } server_unlink_window(src_s, wl); @@ -109,12 +114,12 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) w->active = wp; w->latest = tc; - if (!args_has(args, 'n')) { - name = default_window_name(w); - window_set_name(w, name); - free(name); + if (name != NULL) { + newname = default_window_name(w); + window_set_name(w, newname, WINDOW_NAME_FORBID); + free(newname); } else { - window_set_name(w, args_get(args, 'n')); + window_set_name(w, name, 0); options_set_number(w->options, "automatic-rename", 0); } diff --git a/cmd-new-session.c b/cmd-new-session.c index a397ccf9d..dbee4e559 100644 --- a/cmd-new-session.c +++ b/cmd-new-session.c @@ -77,8 +77,8 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) struct termios tio, *tiop; struct session_group *sg = NULL; const char *errstr, *template, *group, *tmp; - char *cause, *cwd = NULL, *cp, *newname = NULL; - char *name, *prefix = NULL; + char *cause, *cwd = NULL, *cp, *ename; + char *wname = NULL, *sname = NULL, *prefix = NULL; int detached, already_attached, is_control = 0; u_int sx, sy, dsx, dsy, count = args_count(args); struct spawn_context sc = { 0 }; @@ -99,20 +99,29 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_ERROR); } - tmp = args_get(args, 's'); - if (tmp != NULL) { - name = format_single(item, tmp, c, NULL, NULL, NULL); - newname = clean_name(name, "#:."); - if (newname == NULL) { - cmdq_error(item, "invalid session: %s", name); - free(name); + if ((tmp = args_get(args, 'n')) != NULL) { + ename = format_single(item, tmp, c, NULL, NULL, NULL); + if (!check_name(ename, WINDOW_NAME_FORBID)) { + cmdq_error(item, "invalid window name: %s", ename); + free(ename); return (CMD_RETURN_ERROR); } - free(name); + wname = clean_name(ename, WINDOW_NAME_FORBID); + free(ename); + } + if ((tmp = args_get(args, 's')) != NULL) { + ename = format_single(item, tmp, c, NULL, NULL, NULL); + if (!check_name(ename, SESSION_NAME_FORBID)) { + cmdq_error(item, "invalid session name: %s", ename); + free(ename); + goto fail; + } + sname = clean_name(ename, SESSION_NAME_FORBID); + free(ename); } if (args_has(args, 'A')) { - if (newname != NULL) - as = session_find(newname); + if (sname != NULL) + as = session_find(sname); else as = target->s; if (as != NULL) { @@ -120,12 +129,13 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) args_has(args, 'D'), args_has(args, 'X'), 0, args_get(args, 'c'), args_has(args, 'E'), args_get(args, 'f')); - free(newname); + free(wname); + free(sname); return (retval); } } - if (newname != NULL && session_find(newname) != NULL) { - cmdq_error(item, "duplicate session: %s", newname); + if (sname != NULL && session_find(sname) != NULL) { + cmdq_error(item, "duplicate session: %s", sname); goto fail; } @@ -142,12 +152,12 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) else if (groupwith != NULL) prefix = xstrdup(groupwith->name); else { - prefix = clean_name(group, "#:."); - if (prefix == NULL) { - cmdq_error(item, "invalid session group: %s", - group); + if (!check_name(group, SESSION_NAME_FORBID)) { + cmdq_error(item, + "invalid session group name: %s", group); goto fail; } + prefix = clean_name(group, SESSION_NAME_FORBID); } } @@ -276,7 +286,7 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) environ_put(env, av->string, 0); av = args_next_value(av); } - s = session_create(prefix, newname, cwd, env, oo, tiop); + s = session_create(prefix, sname, cwd, env, oo, tiop); /* Spawn the initial window. */ sc.item = item; @@ -284,7 +294,7 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) if (!detached) sc.tc = c; - sc.name = args_get(args, 'n'); + sc.name = wname; args_to_vector(args, &sc.argc, &sc.argv); sc.idx = -1; @@ -357,7 +367,8 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) if (sc.argv != NULL) cmd_free_argv(sc.argc, sc.argv); free(cwd); - free(newname); + free(wname); + free(sname); free(prefix); return (CMD_RETURN_NORMAL); @@ -365,7 +376,8 @@ fail: if (sc.argv != NULL) cmd_free_argv(sc.argc, sc.argv); free(cwd); - free(newname); + free(wname); + free(sname); free(prefix); return (CMD_RETURN_ERROR); } diff --git a/cmd-new-window.c b/cmd-new-window.c index dd64baab0..c7a203e2b 100644 --- a/cmd-new-window.c +++ b/cmd-new-window.c @@ -61,7 +61,7 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) struct session *s = target->s; struct winlink *wl = target->wl, *new_wl = NULL; int idx = target->idx, before; - char *cause = NULL, *cp, *expanded; + char *cause = NULL, *cp, *expanded, *wname; const char *template, *name; struct cmd_find_state fs; struct args_value *av; @@ -71,8 +71,18 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) * name already exists, select it. */ name = args_get(args, 'n'); - if (args_has(args, 'S') && name != NULL && target->idx == -1) { + if (name != NULL) { expanded = format_single(item, name, c, s, NULL, NULL); + if (!check_name(expanded, WINDOW_NAME_FORBID)) { + cmdq_error(item, "invalid window name: %s", expanded); + free(expanded); + return (CMD_RETURN_ERROR); + } + wname = clean_name(expanded, WINDOW_NAME_FORBID); + free(expanded); + } + if (args_has(args, 'S') && wname != NULL && target->idx == -1) { + expanded = format_single(item, wname, c, s, NULL, NULL); RB_FOREACH(wl, winlinks, &s->windows) { if (strcmp(wl->window->name, expanded) != 0) continue; @@ -80,12 +90,14 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) new_wl = wl; continue; } - cmdq_error(item, "multiple windows named %s", name); + cmdq_error(item, "multiple windows named %s", wname); + free(wname); free(expanded); return (CMD_RETURN_ERROR); } free(expanded); if (new_wl != NULL) { + free(wname); if (args_has(args, 'd')) return (CMD_RETURN_NORMAL); if (session_set_current(s, new_wl) == 0) @@ -108,7 +120,7 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) sc.s = s; sc.tc = tc; - sc.name = args_get(args, 'n'); + sc.name = wname; args_to_vector(args, &sc.argc, &sc.argv); sc.environ = environ_create(); @@ -130,10 +142,7 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) if ((new_wl = spawn_window(&sc, &cause)) == NULL) { cmdq_error(item, "create window failed: %s", cause); free(cause); - if (sc.argv != NULL) - cmd_free_argv(sc.argc, sc.argv); - environ_free(sc.environ); - return (CMD_RETURN_ERROR); + goto fail; } if (!args_has(args, 'd') || new_wl == s->curw) { cmd_find_from_winlink(current, new_wl, 0); @@ -156,5 +165,13 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) if (sc.argv != NULL) cmd_free_argv(sc.argc, sc.argv); environ_free(sc.environ); + free(wname); return (CMD_RETURN_NORMAL); + +fail: + if (sc.argv != NULL) + cmd_free_argv(sc.argc, sc.argv); + environ_free(sc.environ); + free(wname); + return (CMD_RETURN_ERROR); } diff --git a/cmd-rename-session.c b/cmd-rename-session.c index 37b261006..ca6a2eacb 100644 --- a/cmd-rename-session.c +++ b/cmd-rename-session.c @@ -52,12 +52,12 @@ cmd_rename_session_exec(struct cmd *self, struct cmdq_item *item) char *newname, *tmp; tmp = format_single_from_target(item, args_string(args, 0)); - newname = clean_name(tmp, "#:."); - if (newname == NULL) { - cmdq_error(item, "invalid session: %s", tmp); + if (!check_name(tmp, SESSION_NAME_FORBID)) { + cmdq_error(item, "invalid session name: %s", tmp); free(tmp); return (CMD_RETURN_ERROR); } + newname = clean_name(tmp, SESSION_NAME_FORBID); free(tmp); if (strcmp(newname, s->name) == 0) { free(newname); diff --git a/cmd-rename-window.c b/cmd-rename-window.c index 472b571ba..8428e23f3 100644 --- a/cmd-rename-window.c +++ b/cmd-rename-window.c @@ -48,15 +48,21 @@ cmd_rename_window_exec(struct cmd *self, struct cmdq_item *item) struct args *args = cmd_get_args(self); struct cmd_find_state *target = cmdq_get_target(item); struct winlink *wl = target->wl; - char *newname; + char *name; - newname = format_single_from_target(item, args_string(args, 0)); - window_set_name(wl->window, newname); + name = format_single_from_target(item, args_string(args, 0)); + if (!check_name(name, WINDOW_NAME_FORBID)) { + cmdq_error(item, "invalid window name: %s", name); + free(name); + return (CMD_RETURN_ERROR); + } + + window_set_name(wl->window, name, WINDOW_NAME_FORBID); options_set_number(wl->window->options, "automatic-rename", 0); + free(name); server_redraw_window_borders(wl->window); server_status_window(wl->window); - free(newname); return (CMD_RETURN_NORMAL); } diff --git a/cmd-select-pane.c b/cmd-select-pane.c index e7fee1d63..f3c2cc124 100644 --- a/cmd-select-pane.c +++ b/cmd-select-pane.c @@ -217,7 +217,7 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'T')) { title = format_single_from_target(item, args_get(args, 'T')); - if (screen_set_title(&wp->base, title)) { + if (screen_set_title(&wp->base, title, 0)) { notify_pane("pane-title-changed", wp); server_redraw_window_borders(wp->window); server_status_window(wp->window); diff --git a/cmd-split-window.c b/cmd-split-window.c index 97fa9ae34..2e17c2636 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -191,8 +191,9 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) options_set_number(new_wp->options, "remain-on-exit", 3); if (args_has(args, 'm')) options_set_string(new_wp->options, - "remain-on-exit-format", - 0, "%s", args_get(args, 'm')); + "remain-on-exit-format", 0, "%s", + args_get(args, 'm')); + } } if (input) { diff --git a/input.c b/input.c index 6c9acd07a..4710d08a2 100644 --- a/input.c +++ b/input.c @@ -2701,7 +2701,7 @@ input_exit_osc(struct input_ctx *ictx) case 2: if (wp != NULL && options_get_number(wp->options, "allow-set-title") && - screen_set_title(sctx->s, p)) { + screen_set_title(sctx->s, p, 1)) { notify_pane("pane-title-changed", wp); server_redraw_window_borders(wp->window); server_status_window(wp->window); @@ -2711,7 +2711,7 @@ input_exit_osc(struct input_ctx *ictx) input_osc_4(ictx, p); break; case 7: - if (screen_set_path(sctx->s, p) && wp != NULL) { + if (wp != NULL && screen_set_path(sctx->s, p, 1)) { server_redraw_window_borders(wp->window); server_status_window(wp->window); } @@ -2779,7 +2779,7 @@ input_exit_apc(struct input_ctx *ictx) if (wp != NULL && options_get_number(wp->options, "allow-set-title") && - screen_set_title(sctx->s, ictx->input_buf)) { + screen_set_title(sctx->s, ictx->input_buf, 1)) { notify_pane("pane-title-changed", wp); server_redraw_window_borders(wp->window); server_status_window(wp->window); @@ -2822,10 +2822,10 @@ input_exit_rename(struct input_ctx *ictx) if (o != NULL) options_remove_or_default(o, -1, NULL); if (!options_get_number(w->options, "automatic-rename")) - window_set_name(w, ""); + window_set_name(w, "", WINDOW_NAME_FORBID_EXT); } else { options_set_number(w->options, "automatic-rename", 0); - window_set_name(w, ictx->input_buf); + window_set_name(w, ictx->input_buf, WINDOW_NAME_FORBID_EXT); } server_redraw_window_borders(w); server_status_window(w); diff --git a/names.c b/names.c index 8a04df73f..756af5b07 100644 --- a/names.c +++ b/names.c @@ -95,7 +95,7 @@ check_window_name(struct window *w) name = format_window_name(w); if (strcmp(name, w->name) != 0) { log_debug("@%u new name %s (was %s)", w->id, name, w->name); - window_set_name(w, name); + window_set_name(w, name, WINDOW_NAME_FORBID_EXT); server_redraw_window_borders(w); server_status_window(w); } else @@ -166,7 +166,7 @@ parse_window_name(const char *in) if (*name == '/') name = basename(name); - name = clean_name(name, "#"); + name = clean_name(name, WINDOW_NAME_FORBID); free(copy); if (name == NULL) return (xstrdup("")); diff --git a/paste.c b/paste.c index 6461d9ba2..081e3dbeb 100644 --- a/paste.c +++ b/paste.c @@ -176,7 +176,6 @@ paste_add(const char *prefix, char *data, size_t size) } pb = xmalloc(sizeof *pb); - pb->name = NULL; do { free(pb->name); @@ -296,7 +295,6 @@ paste_set(char *data, size_t size, const char *name, char **cause) } pb = xmalloc(sizeof *pb); - pb->name = newname; pb->data = data; diff --git a/popup.c b/popup.c index cb8dbf060..908b2a726 100644 --- a/popup.c +++ b/popup.c @@ -435,7 +435,7 @@ popup_make_pane(struct popup_data *pd, enum layout_type type) pd->job = NULL; } - screen_set_title(&pd->s, new_wp->base.title); + screen_set_title(&pd->s, new_wp->base.title, 0); screen_free(&new_wp->base); memcpy(&new_wp->base, &pd->s, sizeof wp->base); screen_resize(&new_wp->base, new_wp->sx, new_wp->sy, 1); diff --git a/screen.c b/screen.c index 2c78c5ac9..418af8071 100644 --- a/screen.c +++ b/screen.c @@ -245,11 +245,14 @@ screen_set_cursor_colour(struct screen *s, int colour) /* Set screen title. */ int -screen_set_title(struct screen *s, const char *title) +screen_set_title(struct screen *s, const char *title, int untrusted) { char *new_title; - new_title = clean_name(title, "#"); + if (untrusted) + new_title = clean_name(title, "#"); + else + new_title = clean_name(title, ""); if (new_title == NULL) return (0); free(s->title); @@ -259,11 +262,14 @@ screen_set_title(struct screen *s, const char *title) /* Set screen path. */ int -screen_set_path(struct screen *s, const char *path) +screen_set_path(struct screen *s, const char *path, int untrusted) { char *new_path; - new_path = clean_name(path, "#"); + if (untrusted) + new_path = clean_name(path, "#"); + else + new_path = clean_name(path, ""); if (new_path == NULL) return (0); free(s->path); diff --git a/session.c b/session.c index 1e558977a..445897e2a 100644 --- a/session.c +++ b/session.c @@ -116,7 +116,6 @@ session_create(const char *prefix, const char *name, const char *cwd, s = xcalloc(1, sizeof *s); s->references = 1; s->flags = 0; - s->cwd = xstrdup(cwd); TAILQ_INIT(&s->lastw); diff --git a/spawn.c b/spawn.c index b0a4f7dfb..ddeca0d34 100644 --- a/spawn.c +++ b/spawn.c @@ -74,15 +74,12 @@ spawn_log(const char *from, struct spawn_context *sc) struct winlink * spawn_window(struct spawn_context *sc, char **cause) { - struct cmdq_item *item = sc->item; - struct client *c = cmdq_get_client(item); struct session *s = sc->s; struct window *w; struct window_pane *wp; struct winlink *wl; int idx = sc->idx; u_int sx, sy, xpixel, ypixel; - char *name; spawn_log(__func__, sc); @@ -180,15 +177,12 @@ spawn_window(struct spawn_context *sc, char **cause) /* Set the name of the new window. */ if (~sc->flags & SPAWN_RESPAWN) { free(w->name); - if (sc->name != NULL) { - name = format_single(item, sc->name, c, s, NULL, NULL); - w->name = clean_name(name, "#"); - free(name); - if (w->name == NULL) - w->name = xstrdup(""); - options_set_number(w->options, "automatic-rename", 0); - } else + if (sc->name == NULL || *sc->name == '\0') w->name = default_window_name(w); + else { + w->name = xstrdup(sc->name); + options_set_number(w->options, "automatic-rename", 0); + } } /* Switch to the new window if required. */ diff --git a/tmux.c b/tmux.c index 251b03368..c06a24350 100644 --- a/tmux.c +++ b/tmux.c @@ -298,6 +298,25 @@ clean_name(const char *name, const char* forbid) return (new_name); } +/* + * Check a name given by a command: reject it if it is empty, not valid UTF-8, + * or contains a forbidden character. Other characters that clean_name would + * change (for example with utf8_stravis) are allowed and fixed silently. + */ +int +check_name(const char *name, const char *forbid) +{ + const char *cp; + + if (*name == '\0' || !utf8_isvalid(name)) + return (0); + for (cp = name; *cp != '\0'; cp++) { + if (strchr(forbid, *cp) != NULL) + return (0); + } + return (1); +} + const char * sig2name(int signo) { diff --git a/tmux.h b/tmux.h index c7a7405c7..56dd2e537 100644 --- a/tmux.h +++ b/tmux.h @@ -96,6 +96,12 @@ struct winlink; #define TMUX_LOCK_CMD "lock -np" #endif +/* Forbidden characters in names. */ +#define WINDOW_NAME_FORBID ":." +#define WINDOW_NAME_FORBID_EXT ":.#" +#define SESSION_NAME_FORBID ":." +#define SESSION_NAME_FORBID_EXT ":.#" + /* Minimum layout cell size, NOT including border lines. */ #define PANE_MINIMUM 1 @@ -2404,6 +2410,7 @@ void setblocking(int, int); char *shell_argv0(const char *, int); uint64_t get_timer(void); char *clean_name(const char *, const char *); +int check_name(const char *, const char *); const char *sig2name(int); const char *find_cwd(void); const char *find_home(void); @@ -3375,8 +3382,8 @@ void screen_reset_hyperlinks(struct screen *); void screen_set_default_cursor(struct screen *, struct options *); void screen_set_cursor_style(u_int, enum screen_cursor_style *, int *); void screen_set_cursor_colour(struct screen *, int); -int screen_set_title(struct screen *, const char *); -int screen_set_path(struct screen *, const char *); +int screen_set_title(struct screen *, const char *, int); +int screen_set_path(struct screen *, const char *, int); void screen_push_title(struct screen *); void screen_pop_title(struct screen *); void screen_set_progress_bar(struct screen *, enum progress_bar_state, int); @@ -3480,7 +3487,7 @@ void window_pane_stack_push(struct window_panes *, struct window_pane *); void window_pane_stack_remove(struct window_panes *, struct window_pane *); -void window_set_name(struct window *, const char *); +void window_set_name(struct window *, const char *, const char *); void window_add_ref(struct window *, const char *); void window_remove_ref(struct window *, const char *); void winlink_clear_flags(struct winlink *); diff --git a/window.c b/window.c index 314415889..89c28b8e6 100644 --- a/window.c +++ b/window.c @@ -401,11 +401,11 @@ window_remove_ref(struct window *w, const char *from) } void -window_set_name(struct window *w, const char *new_name) +window_set_name(struct window *w, const char *new_name, const char *forbid) { char *name; - name = clean_name(new_name, "#"); + name = clean_name(new_name, forbid); if (name != NULL) { free(w->name); w->name = name; @@ -1079,7 +1079,7 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit) style_ranges_init(&wp->border_status_line.ranges); if (gethostname(host, sizeof host) == 0) - screen_set_title(&wp->base, host); + screen_set_title(&wp->base, host, 0); return (wp); } From 3b8e25fef4523b196a97185db6d836179c1e6aeb Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 16 Jun 2026 09:00:25 +0000 Subject: [PATCH 19/38] Initialize wname so it we are not freeing garbage if it is not used. --- cmd-new-window.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd-new-window.c b/cmd-new-window.c index c7a203e2b..c65745a26 100644 --- a/cmd-new-window.c +++ b/cmd-new-window.c @@ -61,7 +61,7 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) struct session *s = target->s; struct winlink *wl = target->wl, *new_wl = NULL; int idx = target->idx, before; - char *cause = NULL, *cp, *expanded, *wname; + char *cause = NULL, *cp, *expanded, *wname = NULL; const char *template, *name; struct cmd_find_state fs; struct args_value *av; From e7f414416f52e8c90e10ee34cf113222e832766e Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 22 Jun 2026 10:17:08 +0000 Subject: [PATCH 20/38] Set check callback for menus so they aren't overwritten by sync. --- menu.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/menu.c b/menu.c index 4f1228319..31f4529ec 100644 --- a/menu.c +++ b/menu.c @@ -638,7 +638,7 @@ menu_display(struct menu *menu, int flags, int starting_choice, style, selected_style, border_style, fs, cb, data); if (md == NULL) return (-1); - server_client_set_overlay(c, 0, NULL, menu_mode_cb, menu_draw_cb, - menu_key_cb, menu_free_cb, menu_resize_cb, md); + server_client_set_overlay(c, 0, menu_check_cb, menu_mode_cb, + menu_draw_cb, menu_key_cb, menu_free_cb, menu_resize_cb, md); return (0); } From 4fc4644652793be253fcb401452164141e311d13 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 26 Jun 2026 18:50:42 +0100 Subject: [PATCH 21/38] Tweak. --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index cb92e2ff0..8d3c244b8 100644 --- a/CHANGES +++ b/CHANGES @@ -5,6 +5,7 @@ CHANGES FROM 3.6b TO 3.7 the same escape sequence support). Floating panes are created with the new-pane command, bound to * by default. + This is an early release of this feature and they are relatively limited. Currently floating panes can only be moved and resized using the mouse. The default second status line (if status-format is set to 2) has changed to show a list of panes. Many obvious features are not yet available for floating From 4eafb27b897444e1f25a8a7051bb683d5b8554cd Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 26 Jun 2026 18:50:58 +0100 Subject: [PATCH 22/38] tmux 3.7. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 0cf37da5b..5396cb6c6 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ # configure.ac -AC_INIT([tmux], 3.7-rc3) +AC_INIT([tmux], 3.7) AC_PREREQ([2.60]) AC_CONFIG_AUX_DIR(etc) From 81f88f8517c9fc5371b56cf117530c6b477c96ac Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 26 Jun 2026 18:55:59 +0100 Subject: [PATCH 23/38] Add a missing {. --- cmd-split-window.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd-split-window.c b/cmd-split-window.c index 2e17c2636..5c4946637 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -189,7 +189,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) } if (args_has(args, 'k') || args_has(args, 'm')) { options_set_number(new_wp->options, "remain-on-exit", 3); - if (args_has(args, 'm')) + if (args_has(args, 'm')) { options_set_string(new_wp->options, "remain-on-exit-format", 0, "%s", args_get(args, 'm')); From 84291b021f19de442f14d2e9378794a9b94ef21d Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sat, 27 Jun 2026 10:58:39 +0100 Subject: [PATCH 24/38] Fix check so as to not use NULL name. --- CHANGES | 4 ++++ cmd-break-pane.c | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 8d3c244b8..d3e43b1e7 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,7 @@ +CHANGES FROM 3.7 to 3.7a + +* Fix crash in break-pane when no name is provided. + CHANGES FROM 3.6b TO 3.7 * Add floating panes. These are panes which sit above the layout ("tiled diff --git a/cmd-break-pane.c b/cmd-break-pane.c index 0d8d83fd9..da15e8b5f 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -114,7 +114,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) w->active = wp; w->latest = tc; - if (name != NULL) { + if (name == NULL) { newname = default_window_name(w); window_set_name(w, newname, WINDOW_NAME_FORBID); free(newname); From 6c2ef7568195577f2864cdaa0c1f2eaa6436343d Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 28 Jun 2026 16:54:51 +0100 Subject: [PATCH 25/38] Bump version. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 5396cb6c6..57dc391a6 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ # configure.ac -AC_INIT([tmux], 3.7) +AC_INIT([tmux], 3.7a) AC_PREREQ([2.60]) AC_CONFIG_AUX_DIR(etc) From 4e612612dc06d973baa56796f653985e9f7d5dad Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 28 Jun 2026 15:53:18 +0000 Subject: [PATCH 26/38] Only forbid #( in names and titles (styles are #[ and are useful). --- tmux.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tmux.c b/tmux.c index c06a24350..c2557226d 100644 --- a/tmux.c +++ b/tmux.c @@ -290,7 +290,10 @@ clean_name(const char *name, const char* forbid) return (NULL); copy = xstrdup(name); for (cp = copy; *cp != '\0'; cp++) { - if (strchr(forbid, *cp) != NULL) + if (*cp == '#' && strchr(forbid, '#') != NULL) { + if (cp[1] == '(') + *cp = '_'; + } else if (strchr(forbid, *cp) != NULL) *cp = '_'; } utf8_stravis(&new_name, copy, VIS_OCTAL|VIS_CSTYLE|VIS_TAB|VIS_NL); From 132a63d1da0e056d3b182eeabe3042b390f331f8 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 29 Jun 2026 16:20:45 +0000 Subject: [PATCH 27/38] Allow empty window and session names. --- tmux.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tmux.c b/tmux.c index c2557226d..912cd0d1c 100644 --- a/tmux.c +++ b/tmux.c @@ -286,7 +286,7 @@ clean_name(const char *name, const char* forbid) { char *copy, *cp, *new_name; - if (*name == '\0' || !utf8_isvalid(name)) + if (!utf8_isvalid(name)) return (NULL); copy = xstrdup(name); for (cp = copy; *cp != '\0'; cp++) { @@ -311,7 +311,7 @@ check_name(const char *name, const char *forbid) { const char *cp; - if (*name == '\0' || !utf8_isvalid(name)) + if (!utf8_isvalid(name)) return (0); for (cp = name; *cp != '\0'; cp++) { if (strchr(forbid, *cp) != NULL) From 166267c87a6c99639642e04314a9f9e0889f6222 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 29 Jun 2026 18:17:28 +0000 Subject: [PATCH 28/38] Allow :. in names again, forbidding them is overly pernickety. Only forbid invalid UTF-8 and #(. --- cmd-break-pane.c | 6 +++--- cmd-new-session.c | 12 ++++++------ cmd-new-window.c | 4 ++-- cmd-rename-session.c | 4 ++-- cmd-rename-window.c | 4 ++-- input.c | 14 ++++++-------- names.c | 4 ++-- paste.c | 4 ++-- screen.c | 10 ++-------- tmux.c | 20 +++----------------- tmux.h | 12 +++--------- window.c | 4 ++-- 12 files changed, 35 insertions(+), 63 deletions(-) diff --git a/cmd-break-pane.c b/cmd-break-pane.c index da15e8b5f..39f530f6d 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -62,7 +62,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) int idx = target->idx, before; const char *template, *name = args_get(args, 'n'); - if (name != NULL && !check_name(name, WINDOW_NAME_FORBID)) { + if (name != NULL && !check_name(name)) { cmdq_error(item, "invalid window name: %s", name); return (CMD_RETURN_ERROR); } @@ -86,7 +86,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_ERROR); } if (name != NULL) { - window_set_name(w, name, WINDOW_NAME_FORBID); + window_set_name(w, name, 0); options_set_number(w->options, "automatic-rename", 0); } server_unlink_window(src_s, wl); @@ -116,7 +116,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) if (name == NULL) { newname = default_window_name(w); - window_set_name(w, newname, WINDOW_NAME_FORBID); + window_set_name(w, newname, 0); free(newname); } else { window_set_name(w, name, 0); diff --git a/cmd-new-session.c b/cmd-new-session.c index dbee4e559..ba02c72d4 100644 --- a/cmd-new-session.c +++ b/cmd-new-session.c @@ -101,22 +101,22 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) if ((tmp = args_get(args, 'n')) != NULL) { ename = format_single(item, tmp, c, NULL, NULL, NULL); - if (!check_name(ename, WINDOW_NAME_FORBID)) { + if (!check_name(ename)) { cmdq_error(item, "invalid window name: %s", ename); free(ename); return (CMD_RETURN_ERROR); } - wname = clean_name(ename, WINDOW_NAME_FORBID); + wname = clean_name(ename, 0); free(ename); } if ((tmp = args_get(args, 's')) != NULL) { ename = format_single(item, tmp, c, NULL, NULL, NULL); - if (!check_name(ename, SESSION_NAME_FORBID)) { + if (!check_name(ename)) { cmdq_error(item, "invalid session name: %s", ename); free(ename); goto fail; } - sname = clean_name(ename, SESSION_NAME_FORBID); + sname = clean_name(ename, 0); free(ename); } if (args_has(args, 'A')) { @@ -152,12 +152,12 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) else if (groupwith != NULL) prefix = xstrdup(groupwith->name); else { - if (!check_name(group, SESSION_NAME_FORBID)) { + if (!check_name(group)) { cmdq_error(item, "invalid session group name: %s", group); goto fail; } - prefix = clean_name(group, SESSION_NAME_FORBID); + prefix = clean_name(group, 0); } } diff --git a/cmd-new-window.c b/cmd-new-window.c index c65745a26..bb6113fd6 100644 --- a/cmd-new-window.c +++ b/cmd-new-window.c @@ -73,12 +73,12 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) name = args_get(args, 'n'); if (name != NULL) { expanded = format_single(item, name, c, s, NULL, NULL); - if (!check_name(expanded, WINDOW_NAME_FORBID)) { + if (!check_name(expanded)) { cmdq_error(item, "invalid window name: %s", expanded); free(expanded); return (CMD_RETURN_ERROR); } - wname = clean_name(expanded, WINDOW_NAME_FORBID); + wname = clean_name(expanded, 0); free(expanded); } if (args_has(args, 'S') && wname != NULL && target->idx == -1) { diff --git a/cmd-rename-session.c b/cmd-rename-session.c index ca6a2eacb..b399483b8 100644 --- a/cmd-rename-session.c +++ b/cmd-rename-session.c @@ -52,12 +52,12 @@ cmd_rename_session_exec(struct cmd *self, struct cmdq_item *item) char *newname, *tmp; tmp = format_single_from_target(item, args_string(args, 0)); - if (!check_name(tmp, SESSION_NAME_FORBID)) { + if (!check_name(tmp)) { cmdq_error(item, "invalid session name: %s", tmp); free(tmp); return (CMD_RETURN_ERROR); } - newname = clean_name(tmp, SESSION_NAME_FORBID); + newname = clean_name(tmp, 0); free(tmp); if (strcmp(newname, s->name) == 0) { free(newname); diff --git a/cmd-rename-window.c b/cmd-rename-window.c index 8428e23f3..8ab9b658a 100644 --- a/cmd-rename-window.c +++ b/cmd-rename-window.c @@ -51,13 +51,13 @@ cmd_rename_window_exec(struct cmd *self, struct cmdq_item *item) char *name; name = format_single_from_target(item, args_string(args, 0)); - if (!check_name(name, WINDOW_NAME_FORBID)) { + if (!check_name(name)) { cmdq_error(item, "invalid window name: %s", name); free(name); return (CMD_RETURN_ERROR); } - window_set_name(wl->window, name, WINDOW_NAME_FORBID); + window_set_name(wl->window, name, 0); options_set_number(wl->window->options, "automatic-rename", 0); free(name); diff --git a/input.c b/input.c index 4710d08a2..788663e8f 100644 --- a/input.c +++ b/input.c @@ -1953,16 +1953,14 @@ input_csi_dispatch_rm_private(struct input_ctx *ictx) case 2004: screen_write_mode_clear(sctx, MODE_BRACKETPASTE); break; + case 2026: + screen_write_stop_sync(ictx->wp); + break; case 2031: screen_write_mode_clear(sctx, MODE_THEME_UPDATES); if (ictx->wp != NULL) ictx->wp->flags &= ~PANE_THEMECHANGED; break; - case 2026: /* synchronized output */ - screen_write_stop_sync(ictx->wp); - if (ictx->wp != NULL) - ictx->wp->flags |= PANE_REDRAW; - break; default: log_debug("%s: unknown '%c'", __func__, ictx->ch); break; @@ -2065,7 +2063,7 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx) ictx->wp->flags &= ~PANE_THEMECHANGED; } break; - case 2026: /* synchronized output */ + case 2026: screen_write_start_sync(ictx->wp); break; default: @@ -2822,10 +2820,10 @@ input_exit_rename(struct input_ctx *ictx) if (o != NULL) options_remove_or_default(o, -1, NULL); if (!options_get_number(w->options, "automatic-rename")) - window_set_name(w, "", WINDOW_NAME_FORBID_EXT); + window_set_name(w, "", 1); } else { options_set_number(w->options, "automatic-rename", 0); - window_set_name(w, ictx->input_buf, WINDOW_NAME_FORBID_EXT); + window_set_name(w, ictx->input_buf, 1); } server_redraw_window_borders(w); server_status_window(w); diff --git a/names.c b/names.c index 756af5b07..cee310366 100644 --- a/names.c +++ b/names.c @@ -95,7 +95,7 @@ check_window_name(struct window *w) name = format_window_name(w); if (strcmp(name, w->name) != 0) { log_debug("@%u new name %s (was %s)", w->id, name, w->name); - window_set_name(w, name, WINDOW_NAME_FORBID_EXT); + window_set_name(w, name, 1); server_redraw_window_borders(w); server_status_window(w); } else @@ -166,7 +166,7 @@ parse_window_name(const char *in) if (*name == '/') name = basename(name); - name = clean_name(name, WINDOW_NAME_FORBID); + name = clean_name(name, 0); free(copy); if (name == NULL) return (xstrdup("")); diff --git a/paste.c b/paste.c index 081e3dbeb..5ee80afd1 100644 --- a/paste.c +++ b/paste.c @@ -219,7 +219,7 @@ paste_rename(const char *oldname, const char *newname, char **cause) return (-1); } - name = clean_name(newname, ""); + name = clean_name(newname, 0); if (name == NULL) { if (cause != NULL) xasprintf(cause, "invalid buffer name: %s", newname); @@ -287,7 +287,7 @@ paste_set(char *data, size_t size, const char *name, char **cause) return (-1); } - newname = clean_name(name, ""); + newname = clean_name(name, 0); if (newname == NULL) { if (cause != NULL) xasprintf(cause, "invalid buffer name: %s", name); diff --git a/screen.c b/screen.c index 418af8071..b1472765d 100644 --- a/screen.c +++ b/screen.c @@ -249,10 +249,7 @@ screen_set_title(struct screen *s, const char *title, int untrusted) { char *new_title; - if (untrusted) - new_title = clean_name(title, "#"); - else - new_title = clean_name(title, ""); + new_title = clean_name(title, untrusted); if (new_title == NULL) return (0); free(s->title); @@ -266,10 +263,7 @@ screen_set_path(struct screen *s, const char *path, int untrusted) { char *new_path; - if (untrusted) - new_path = clean_name(path, "#"); - else - new_path = clean_name(path, ""); + new_path = clean_name(path, untrusted); if (new_path == NULL) return (0); free(s->path); diff --git a/tmux.c b/tmux.c index 912cd0d1c..132c39217 100644 --- a/tmux.c +++ b/tmux.c @@ -282,7 +282,7 @@ get_timer(void) } char * -clean_name(const char *name, const char* forbid) +clean_name(const char *name, int untrusted) { char *copy, *cp, *new_name; @@ -290,10 +290,7 @@ clean_name(const char *name, const char* forbid) return (NULL); copy = xstrdup(name); for (cp = copy; *cp != '\0'; cp++) { - if (*cp == '#' && strchr(forbid, '#') != NULL) { - if (cp[1] == '(') - *cp = '_'; - } else if (strchr(forbid, *cp) != NULL) + if (untrusted && cp[0] == '#' && cp[1] == '(') *cp = '_'; } utf8_stravis(&new_name, copy, VIS_OCTAL|VIS_CSTYLE|VIS_TAB|VIS_NL); @@ -301,22 +298,11 @@ clean_name(const char *name, const char* forbid) return (new_name); } -/* - * Check a name given by a command: reject it if it is empty, not valid UTF-8, - * or contains a forbidden character. Other characters that clean_name would - * change (for example with utf8_stravis) are allowed and fixed silently. - */ int -check_name(const char *name, const char *forbid) +check_name(const char *name) { - const char *cp; - if (!utf8_isvalid(name)) return (0); - for (cp = name; *cp != '\0'; cp++) { - if (strchr(forbid, *cp) != NULL) - return (0); - } return (1); } diff --git a/tmux.h b/tmux.h index 56dd2e537..0fbc4424e 100644 --- a/tmux.h +++ b/tmux.h @@ -96,12 +96,6 @@ struct winlink; #define TMUX_LOCK_CMD "lock -np" #endif -/* Forbidden characters in names. */ -#define WINDOW_NAME_FORBID ":." -#define WINDOW_NAME_FORBID_EXT ":.#" -#define SESSION_NAME_FORBID ":." -#define SESSION_NAME_FORBID_EXT ":.#" - /* Minimum layout cell size, NOT including border lines. */ #define PANE_MINIMUM 1 @@ -2409,8 +2403,8 @@ int checkshell(const char *); void setblocking(int, int); char *shell_argv0(const char *, int); uint64_t get_timer(void); -char *clean_name(const char *, const char *); -int check_name(const char *, const char *); +char *clean_name(const char *, int); +int check_name(const char *); const char *sig2name(int); const char *find_cwd(void); const char *find_home(void); @@ -3487,7 +3481,7 @@ void window_pane_stack_push(struct window_panes *, struct window_pane *); void window_pane_stack_remove(struct window_panes *, struct window_pane *); -void window_set_name(struct window *, const char *, const char *); +void window_set_name(struct window *, const char *, int); void window_add_ref(struct window *, const char *); void window_remove_ref(struct window *, const char *); void winlink_clear_flags(struct winlink *); diff --git a/window.c b/window.c index 89c28b8e6..45d8d2f68 100644 --- a/window.c +++ b/window.c @@ -401,11 +401,11 @@ window_remove_ref(struct window *w, const char *from) } void -window_set_name(struct window *w, const char *new_name, const char *forbid) +window_set_name(struct window *w, const char *new_name, int untrusted) { char *name; - name = clean_name(new_name, forbid); + name = clean_name(new_name, untrusted); if (name != NULL) { free(w->name); w->name = name; From dbe50934b1cef66417de663f82bdda9728a8d0a6 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 30 Jun 2026 07:41:02 +0100 Subject: [PATCH 29/38] Add caching of scrollbar options to 3.7a branch. --- cmd-resize-pane.c | 12 +++---- layout.c | 19 ++++------- options.c | 7 +++- screen-redraw.c | 85 ++++++++++++++++++++++------------------------- server-client.c | 23 ++++++------- tmux.h | 5 ++- window.c | 20 +++++------ 7 files changed, 80 insertions(+), 91 deletions(-) diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index d1a8f6723..8ad2655d2 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -181,7 +181,7 @@ cmd_resize_pane_mouse_update_floating(struct client *c, struct mouse_event *m) struct window_pane *wp; struct layout_cell *lc; int y, ly, x, lx, sx, sy, new_sx, new_sy; - int scrollbars, sb_pos, left, right; + int left, right; int new_xoff, new_yoff, resizes = 0; wp = cmd_mouse_pane(m, NULL, &wl); @@ -193,15 +193,13 @@ cmd_resize_pane_mouse_update_floating(struct client *c, struct mouse_event *m) lc = wp->layout_cell; sx = wp->sx; sy = wp->sy; - scrollbars = options_get_number(w->options, "pane-scrollbars"); - sb_pos = options_get_number(w->options, "pane-scrollbars-position"); left = wp->xoff - 1; right = wp->xoff + sx; - if (window_pane_show_scrollbar(wp, scrollbars) && - sb_pos == PANE_SCROLLBARS_LEFT) { + if (window_pane_show_scrollbar(wp) && + w->sb_pos == PANE_SCROLLBARS_LEFT) { left -= wp->scrollbar_style.width + wp->scrollbar_style.pad; - } else if (window_pane_show_scrollbar(wp, scrollbars) && - sb_pos == PANE_SCROLLBARS_RIGHT) { + } else if (window_pane_show_scrollbar(wp) && + w->sb_pos == PANE_SCROLLBARS_RIGHT) { right += wp->scrollbar_style.width + wp->scrollbar_style.pad; } diff --git a/layout.c b/layout.c index da98b590e..70226d6af 100644 --- a/layout.c +++ b/layout.c @@ -359,12 +359,10 @@ layout_fix_panes(struct window *w, struct window_pane *skip) { struct window_pane *wp; struct layout_cell *lc; - int status, scrollbars, sb_pos, sb_w, sb_pad; + int status, sb_w, sb_pad; u_int sx, sy; status = options_get_number(w->options, "pane-border-status"); - scrollbars = options_get_number(w->options, "pane-scrollbars"); - sb_pos = options_get_number(w->options, "pane-scrollbars-position"); TAILQ_FOREACH(wp, &w->panes, entry) { if ((lc = wp->layout_cell) == NULL || wp == skip) @@ -382,14 +380,14 @@ layout_fix_panes(struct window *w, struct window_pane *skip) sy--; } - if (window_pane_show_scrollbar(wp, scrollbars)) { + if (window_pane_show_scrollbar(wp)) { sb_w = wp->scrollbar_style.width; sb_pad = wp->scrollbar_style.pad; if (sb_w < 1) sb_w = 1; if (sb_pad < 0) sb_pad = 0; - if (sb_pos == PANE_SCROLLBARS_LEFT) { + if (w->sb_pos == PANE_SCROLLBARS_LEFT) { if ((int)sx - sb_w < PANE_MINIMUM) { wp->xoff = wp->xoff + (int)sx - PANE_MINIMUM; @@ -398,7 +396,7 @@ layout_fix_panes(struct window *w, struct window_pane *skip) sx = sx - sb_w - sb_pad; wp->xoff = wp->xoff + sb_w + sb_pad; } - } else /* sb_pos == PANE_SCROLLBARS_RIGHT */ + } else /* w->sb_pos == PANE_SCROLLBARS_RIGHT */ if ((int)sx - sb_w - sb_pad < PANE_MINIMUM) sx = PANE_MINIMUM; else @@ -438,16 +436,15 @@ layout_resize_check(struct window *w, struct layout_cell *lc, struct layout_cell *lcchild; struct style *sb_style = &w->active->scrollbar_style; u_int available, minimum; - int status, scrollbars; + int status; status = options_get_number(w->options, "pane-border-status"); - scrollbars = options_get_number(w->options, "pane-scrollbars"); if (lc->type == LAYOUT_WINDOWPANE) { /* Space available in this cell only. */ if (type == LAYOUT_LEFTRIGHT) { available = lc->sx; - if (scrollbars) + if (w->sb != PANE_SCROLLBARS_OFF) minimum = PANE_MINIMUM + sb_style->width + sb_style->pad; else @@ -1003,7 +1000,6 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, u_int sx, sy, xoff, yoff, size1, size2, minimum; u_int new_size, saved_size, resize_first = 0; int full_size = (flags & SPAWN_FULLSIZE), status; - int scrollbars; /* * If full_size is specified, add a new cell at the top of the window @@ -1014,7 +1010,6 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, else lc = wp->layout_cell; status = options_get_number(wp->window->options, "pane-border-status"); - scrollbars = options_get_number(wp->window->options, "pane-scrollbars"); /* Copy the old cell size. */ sx = lc->sx; @@ -1025,7 +1020,7 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, /* Check there is enough space for the two new panes. */ switch (type) { case LAYOUT_LEFTRIGHT: - if (scrollbars) { + if (wp->window->sb != PANE_SCROLLBARS_OFF) { minimum = PANE_MINIMUM * 2 + sb_style->width + sb_style->pad; } else diff --git a/options.c b/options.c index a847ad9c1..e38056c34 100644 --- a/options.c +++ b/options.c @@ -1237,8 +1237,13 @@ options_push_changes(const char *name) if (strcmp(name, "pane-border-status") == 0 || strcmp(name, "pane-scrollbars") == 0 || strcmp(name, "pane-scrollbars-position") == 0) { - RB_FOREACH(w, windows, &windows) + RB_FOREACH(w, windows, &windows) { + w->sb = options_get_number(w->options, + "pane-scrollbars"); + w->sb_pos = options_get_number(w->options, + "pane-scrollbars-position"); layout_fix_panes(w, NULL); + } } if (strcmp(name, "pane-scrollbars-style") == 0) { RB_FOREACH(wp, window_pane_tree, &all_window_panes) { diff --git a/screen-redraw.c b/screen-redraw.c index 17085a411..e1d8c6289 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -123,31 +123,28 @@ static enum screen_redraw_border_type screen_redraw_pane_border(struct screen_redraw_ctx *ctx, struct window_pane *wp, int px, int py) { - struct options *oo = wp->window->options; + struct window *w = wp->window; + struct options *oo = w->options; int ex = wp->xoff + wp->sx, ey = wp->yoff + wp->sy; int hsplit = 0, vsplit = 0, pane_status = ctx->pane_status; - int pane_scrollbars = ctx->pane_scrollbars, sb_w = 0; - int sb_pos, sx = wp->sx, sy = wp->sy, left, right; + int sb_w = 0; + int sx = wp->sx, sy = wp->sy, left, right; enum layout_type split_type; - if (pane_scrollbars != 0) - sb_pos = ctx->pane_scrollbars_pos; - else - sb_pos = 0; - /* Inside pane. */ if (px >= wp->xoff && px < ex && py >= wp->yoff && py < ey) return (SCREEN_REDRAW_INSIDE); /* Are scrollbars enabled? */ - if (window_pane_show_scrollbar(wp, pane_scrollbars)) + if (window_pane_show_scrollbar(wp)) sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; /* Floating pane borders. */ if (window_pane_is_floating(wp)) { left = wp->xoff - 1; right = wp->xoff + sx; - if (sb_pos == PANE_SCROLLBARS_LEFT) + if (w->sb != PANE_SCROLLBARS_OFF && + w->sb_pos == PANE_SCROLLBARS_LEFT) left -= sb_w; else right += sb_w; @@ -182,7 +179,8 @@ screen_redraw_pane_border(struct screen_redraw_ctx *ctx, struct window_pane *wp, * active window's border when there are two panes. */ if ((wp->yoff == 0 || py >= wp->yoff - 1) && py <= ey) { - if (sb_pos == PANE_SCROLLBARS_LEFT) { + if (w->sb != PANE_SCROLLBARS_OFF && + w->sb_pos == PANE_SCROLLBARS_LEFT) { if (wp->xoff - sb_w == 0 && px == sx + sb_w) { if (!hsplit || (hsplit && py <= sy / 2)) return (SCREEN_REDRAW_BORDER_RIGHT); @@ -194,7 +192,7 @@ screen_redraw_pane_border(struct screen_redraw_ctx *ctx, struct window_pane *wp, if (px == wp->xoff + sx + sb_w - 1) return (SCREEN_REDRAW_BORDER_RIGHT); } - } else { /* sb_pos == PANE_SCROLLBARS_RIGHT or disabled */ + } else { /* w->sb_pos == PANE_SCROLLBARS_RIGHT or disabled */ if (wp->xoff == 0 && px == sx + sb_w) { if (!hsplit || (hsplit && py <= sy / 2)) return (SCREEN_REDRAW_BORDER_RIGHT); @@ -216,7 +214,8 @@ screen_redraw_pane_border(struct screen_redraw_ctx *ctx, struct window_pane *wp, if (wp->yoff != 0 && py == wp->yoff - 1 && px > sx / 2) return (SCREEN_REDRAW_BORDER_TOP); } else { - if (sb_pos == PANE_SCROLLBARS_LEFT) { + if (w->sb != PANE_SCROLLBARS_OFF && + w->sb_pos == PANE_SCROLLBARS_LEFT) { if ((wp->xoff - sb_w == 0 || px >= wp->xoff - sb_w) && (px <= ex || (sb_w != 0 && px < ex + sb_w))) { if (pane_status != PANE_STATUS_BOTTOM && @@ -225,7 +224,7 @@ screen_redraw_pane_border(struct screen_redraw_ctx *ctx, struct window_pane *wp, if (pane_status != PANE_STATUS_TOP && py == ey) return (SCREEN_REDRAW_BORDER_BOTTOM); } - } else { /* sb_pos == PANE_SCROLLBARS_RIGHT */ + } else { /* w->sb_pos == PANE_SCROLLBARS_RIGHT */ if ((wp->xoff == 0 || px >= wp->xoff) && (px <= ex || (sb_w != 0 && px < ex + sb_w))) { if (pane_status != PANE_STATUS_BOTTOM && @@ -278,17 +277,15 @@ screen_redraw_cell_border(struct screen_redraw_ctx *ctx, struct window_pane *wp, struct client *c = ctx->c; struct window *w = c->session->curw->window; struct window_pane *wp2; - int sx = w->sx, sy = w->sy, sb_w, sb_pos, n; + int sx = w->sx, sy = w->sy, sb_w, n; - if (ctx->pane_scrollbars != 0) - sb_pos = ctx->pane_scrollbars_pos; - else - sb_pos = 0; sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; /* For floating panes, only check the pane itself. */ if (window_pane_is_floating(wp)) { - n = screen_redraw_cell_border1(ctx, sb_pos, sb_w, wp, px, py); + n = screen_redraw_cell_border1(ctx, + w->sb != PANE_SCROLLBARS_OFF ? w->sb_pos : 0, + sb_w, wp, px, py); if (n == -1) return (0); return (n); @@ -311,7 +308,9 @@ screen_redraw_cell_border(struct screen_redraw_ctx *ctx, struct window_pane *wp, TAILQ_FOREACH(wp2, &w->z_index, zentry) { if (!window_pane_visible(wp2) || window_pane_is_floating(wp2)) continue; - n = screen_redraw_cell_border1(ctx, sb_pos, sb_w, wp2, px, py); + n = screen_redraw_cell_border1(ctx, + w->sb != PANE_SCROLLBARS_OFF ? w->sb_pos : 0, + sb_w, wp2, px, py); if (n != -1) return (n); } @@ -435,9 +434,9 @@ screen_redraw_check_cell(struct screen_redraw_ctx *ctx, int px, int py, struct window_pane *wp, *start; int sx = w->sx, sy = w->sy; int pane_status = ctx->pane_status; - int border, pane_scrollbars = ctx->pane_scrollbars; + int border; int pane_status_line, tiled_only = 0, left, right; - int sb_pos = ctx->pane_scrollbars_pos, sb_w; + int sb_w; *wpp = NULL; @@ -452,7 +451,8 @@ screen_redraw_check_cell(struct screen_redraw_ctx *ctx, int px, int py, continue; } sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; - if (sb_pos == PANE_SCROLLBARS_LEFT) { + if (w->sb != PANE_SCROLLBARS_OFF && + w->sb_pos == PANE_SCROLLBARS_LEFT) { if ((px >= wp->xoff - 1 - sb_w && px <= wp->xoff + (int)wp->sx) && (py >= wp->yoff - 1 && @@ -492,7 +492,8 @@ screen_redraw_check_cell(struct screen_redraw_ctx *ctx, int px, int py, *wpp = wp; sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; - if (sb_pos == PANE_SCROLLBARS_LEFT) { + if (w->sb != PANE_SCROLLBARS_OFF && + w->sb_pos == PANE_SCROLLBARS_LEFT) { if ((px < wp->xoff - 1 - sb_w || px > wp->xoff + (int)wp->sx) && (py < wp->yoff - 1 || @@ -524,7 +525,7 @@ screen_redraw_check_cell(struct screen_redraw_ctx *ctx, int px, int py, } /* Check if CELL_SCROLLBAR. */ - if (window_pane_show_scrollbar(wp, pane_scrollbars)) { + if (window_pane_show_scrollbar(wp)) { /* * Check if py could lie within a scrollbar. If the * pane is at the top then py == 0 to sy; if the pane @@ -536,10 +537,10 @@ screen_redraw_check_cell(struct screen_redraw_ctx *ctx, int px, int py, (py >= wp->yoff && py < wp->yoff + (int)wp->sy)) { /* Check if px lies within a scrollbar. */ - if ((sb_pos == PANE_SCROLLBARS_RIGHT && + if ((w->sb_pos == PANE_SCROLLBARS_RIGHT && (px >= wp->xoff + (int)wp->sx && px < wp->xoff + (int)wp->sx + sb_w)) || - (sb_pos == PANE_SCROLLBARS_LEFT && + (w->sb_pos == PANE_SCROLLBARS_LEFT && (px >= wp->xoff - sb_w && px < wp->xoff))) return (CELL_SCROLLBAR); @@ -593,13 +594,12 @@ screen_redraw_make_pane_status(struct client *c, struct window_pane *wp, struct style_line_entry *sle = &wp->border_status_line; char *expanded; int pane_status = rctx->pane_status, sb_w = 0; - int pane_scrollbars = rctx->pane_scrollbars; int max_width; u_int width, i, cell_type, px, py; struct screen_write_ctx ctx; struct screen old; - if (window_pane_show_scrollbar(wp, pane_scrollbars)) + if (window_pane_show_scrollbar(wp)) sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; ft = format_create(c, NULL, FORMAT_PANE|wp->id, FORMAT_STATUS); @@ -791,10 +791,6 @@ screen_redraw_set_context(struct client *c, struct screen_redraw_ctx *ctx) ctx->pane_status = options_get_number(wo, "pane-border-status"); ctx->pane_lines = options_get_number(wo, "pane-border-lines"); - ctx->pane_scrollbars = options_get_number(wo, "pane-scrollbars"); - ctx->pane_scrollbars_pos = options_get_number(wo, - "pane-scrollbars-position"); - tty_window_offset(&c->tty, &ctx->ox, &ctx->oy, &ctx->sx, &ctx->sy); log_debug("%s: %s @%u ox=%u oy=%u sx=%u sy=%u %u/%d", __func__, c->name, @@ -863,7 +859,7 @@ screen_redraw_pane(struct client *c, struct window_pane *wp, if (!redraw_scrollbar_only) screen_redraw_draw_pane(&ctx, wp); - if (window_pane_show_scrollbar(wp, ctx.pane_scrollbars)) + if (window_pane_show_scrollbar(wp)) screen_redraw_draw_pane_scrollbar(&ctx, wp); tty_reset(&c->tty); @@ -1142,7 +1138,7 @@ screen_redraw_get_visible_ranges(struct window_pane *base_wp, int px, struct window *w; struct visible_range *ri; static struct visible_ranges sr = { NULL, 0, 0 }; - int found_self, sb, sb_w, sb_pos; + int found_self, sb_w; int lb, rb, tb, bb, sx, ex; u_int i, s; @@ -1182,9 +1178,6 @@ screen_redraw_get_visible_ranges(struct window_pane *base_wp, int px, r->used = 1; } - sb = options_get_number(w->options, "pane-scrollbars"); - sb_pos = options_get_number(w->options, "pane-scrollbars-position"); - found_self = 0; TAILQ_FOREACH_REVERSE(wp, &w->z_index, window_panes_zindex, zentry) { if (wp == base_wp) { @@ -1203,12 +1196,12 @@ screen_redraw_get_visible_ranges(struct window_pane *base_wp, int px, continue; sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; - if (!window_pane_show_scrollbar(wp, sb)) - sb_w = sb_pos = 0; + if (!window_pane_show_scrollbar(wp)) + sb_w = 0; for (i = 0; i < r->used; i++) { ri = &r->ranges[i]; - if (sb_pos == PANE_SCROLLBARS_LEFT) { + if (w->sb_pos == PANE_SCROLLBARS_LEFT) { if (wp->xoff > sb_w) lb = wp->xoff - 1 - sb_w; else @@ -1219,7 +1212,7 @@ screen_redraw_get_visible_ranges(struct window_pane *base_wp, int px, else lb = 0; } - if (sb_pos == PANE_SCROLLBARS_LEFT) + if (w->sb_pos == PANE_SCROLLBARS_LEFT) rb = wp->xoff + wp->sx; else /* PANE_SCROLLBARS_RIGHT or none. */ rb = wp->xoff + wp->sx + sb_w; @@ -1407,7 +1400,7 @@ screen_redraw_draw_pane_scrollbars(struct screen_redraw_ctx *ctx) log_debug("%s: %s @%u", __func__, c->name, w->id); TAILQ_FOREACH(wp, &w->panes, entry) { - if (window_pane_show_scrollbar(wp, ctx->pane_scrollbars) && + if (window_pane_show_scrollbar(wp) && window_pane_visible(wp)) screen_redraw_draw_pane_scrollbar(ctx, wp); } @@ -1420,8 +1413,8 @@ screen_redraw_draw_pane_scrollbar(struct screen_redraw_ctx *ctx, { struct screen *s = wp->screen; double percent_view; - u_int sb = ctx->pane_scrollbars, total_height, sb_h = wp->sy; - u_int sb_pos = ctx->pane_scrollbars_pos, slider_h, slider_y; + u_int sb = wp->window->sb, total_height, sb_h = wp->sy; + u_int sb_pos = wp->window->sb_pos, slider_h, slider_y; int sb_w = wp->scrollbar_style.width; int sb_pad = wp->scrollbar_style.pad; int cm_y, cm_size, xoff = wp->xoff; diff --git a/server-client.c b/server-client.c index cf1e824c7..52886f014 100644 --- a/server-client.c +++ b/server-client.c @@ -603,17 +603,14 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, u_int *sl_mpos) { struct window *w = wp->window; - struct options *wo = w->options; struct window_pane *fwp; - int pane_status, sb, sb_pos, sb_w, sb_pad; + int pane_status, sb_w, sb_pad; int pane_status_line, sl_top, sl_bottom; int bdr_bottom, bdr_top, bdr_left, bdr_right; - sb = options_get_number(wo, "pane-scrollbars"); - sb_pos = options_get_number(wo, "pane-scrollbars-position"); - pane_status = options_get_number(wo, "pane-border-status"); + pane_status = options_get_number(w->options, "pane-border-status"); - if (window_pane_show_scrollbar(wp, sb)) { + if (window_pane_show_scrollbar(wp)) { sb_w = wp->scrollbar_style.width; sb_pad = wp->scrollbar_style.pad; } else { @@ -628,7 +625,7 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, else pane_status_line = -1; /* not used */ bdr_left = wp->xoff - 1; - if (sb_pos == PANE_SCROLLBARS_LEFT) + if (w->sb_pos == PANE_SCROLLBARS_LEFT) bdr_left -= sb_pad + sb_w; /* Check if point is within the pane or scrollbar. */ @@ -636,15 +633,15 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, py != pane_status_line && py != wp->yoff + (int)wp->sy) || (wp->yoff == 0 && py < (int)wp->sy) || (py >= wp->yoff && py < wp->yoff + (int)wp->sy)) && - ((sb_pos == PANE_SCROLLBARS_RIGHT && + ((w->sb_pos == PANE_SCROLLBARS_RIGHT && px < wp->xoff + (int)wp->sx + sb_pad + sb_w) || - (sb_pos == PANE_SCROLLBARS_LEFT && + (w->sb_pos == PANE_SCROLLBARS_LEFT && px < wp->xoff + (int)wp->sx - sb_pad - sb_w))) { /* Check if in the scrollbar. */ - if ((sb_pos == PANE_SCROLLBARS_RIGHT && + if ((w->sb_pos == PANE_SCROLLBARS_RIGHT && (px >= wp->xoff + (int)wp->sx + sb_pad && px < wp->xoff + (int)wp->sx + sb_pad + sb_w)) || - (sb_pos == PANE_SCROLLBARS_LEFT && + (w->sb_pos == PANE_SCROLLBARS_LEFT && (px >= wp->xoff - sb_pad - sb_w && px < wp->xoff - sb_pad))) { /* Check where inside the scrollbar. */ @@ -674,7 +671,7 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, if ((w->flags & WINDOW_ZOOMED) && (~fwp->flags & PANE_ZOOMED)) continue; - if (window_pane_show_scrollbar(fwp, sb)) { + if (window_pane_show_scrollbar(fwp)) { sb_w = fwp->scrollbar_style.width; sb_pad = fwp->scrollbar_style.pad; } else { @@ -684,7 +681,7 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, bdr_top = fwp->yoff - 1; bdr_bottom = fwp->yoff + fwp->sy; bdr_left = fwp->xoff - 1; - if (sb_pos == PANE_SCROLLBARS_LEFT) { + if (w->sb_pos == PANE_SCROLLBARS_LEFT) { bdr_left -= sb_pad + sb_w; bdr_right = fwp->xoff + fwp->sx; } else { diff --git a/tmux.h b/tmux.h index 0fbc4424e..5dc86b2b1 100644 --- a/tmux.h +++ b/tmux.h @@ -1390,6 +1390,9 @@ struct window { u_int last_new_pane_x; u_int last_new_pane_y; + int sb; + int sb_pos; + struct utf8_data *fill_character; int flags; #define WINDOW_BELL 0x1 @@ -3495,7 +3498,7 @@ void window_pane_update_used_data(struct window_pane *, 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 *, int); +int window_pane_show_scrollbar(struct window_pane *); int window_pane_get_bg(struct window_pane *); int window_pane_get_fg(struct window_pane *); int window_pane_get_fg_control_client(struct window_pane *); diff --git a/window.c b/window.c index 45d8d2f68..00ed1a079 100644 --- a/window.c +++ b/window.c @@ -1471,20 +1471,16 @@ window_pane_full_size_offset(struct window_pane *wp, int *xoff, int *yoff, u_int *sx, u_int *sy) { struct window *w = wp->window; - int pane_scrollbars; - u_int sb_w, sb_pos; + u_int sb_w; - pane_scrollbars = options_get_number(w->options, "pane-scrollbars"); - sb_pos = options_get_number(w->options, "pane-scrollbars-position"); - - if (window_pane_show_scrollbar(wp, pane_scrollbars)) + if (window_pane_show_scrollbar(wp)) sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; else sb_w = 0; - if (sb_pos == PANE_SCROLLBARS_LEFT) { + if (w->sb_pos == PANE_SCROLLBARS_LEFT) { *xoff = wp->xoff - sb_w; *sx = wp->sx + sb_w; - } else { /* sb_pos == PANE_SCROLLBARS_RIGHT */ + } else { /* w->sb_pos == PANE_SCROLLBARS_RIGHT */ *xoff = wp->xoff; *sx = wp->sx + sb_w; } @@ -1899,12 +1895,14 @@ window_pane_mode(struct window_pane *wp) /* Return 1 if scrollbar is or should be displayed. */ int -window_pane_show_scrollbar(struct window_pane *wp, int sb_option) +window_pane_show_scrollbar(struct window_pane *wp) { + struct window *w = wp->window; + if (SCREEN_IS_ALTERNATE(&wp->base)) return (0); - if (sb_option == PANE_SCROLLBARS_ALWAYS || - (sb_option == PANE_SCROLLBARS_MODAL && + if (w->sb == PANE_SCROLLBARS_ALWAYS || + (w->sb == PANE_SCROLLBARS_MODAL && window_pane_mode(wp) != WINDOW_PANE_NO_MODE)) return (1); return (0); From 78a2145a47441cc2fb6d0283a1f4d5e12e4a6556 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 30 Jun 2026 07:42:25 +0100 Subject: [PATCH 30/38] Update CHANGES. --- CHANGES | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES b/CHANGES index d3e43b1e7..172a8d533 100644 --- a/CHANGES +++ b/CHANGES @@ -2,6 +2,11 @@ CHANGES FROM 3.7 to 3.7a * Fix crash in break-pane when no name is provided. +* Scrollbar options are now cached rather than being looked up for every redraw + (issue 5298). + +* Only forbid #( in names, allow #[, empty names, : and . + CHANGES FROM 3.6b TO 3.7 * Add floating panes. These are panes which sit above the layout ("tiled From a97f643bd3b82a713476d3eea56b7fdf0cff5e2d Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 30 Jun 2026 06:44:46 +0000 Subject: [PATCH 31/38] Remove C-r from help since it does not in fact exist. --- mode-tree.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/mode-tree.c b/mode-tree.c index fae0f3d8a..3a126129c 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -206,8 +206,6 @@ static const char* mode_tree_help_start[] = { "#[fg=themelightgrey]" " C-s #[#{E:tree-mode-border-style},acs]x#[default] Search forward", "#[fg=themelightgrey]" - " C-r #[#{E:tree-mode-border-style},acs]x#[default] Search backward", - "#[fg=themelightgrey]" " n #[#{E:tree-mode-border-style},acs]x#[default] Repeat search forward", "#[fg=themelightgrey]" " N #[#{E:tree-mode-border-style},acs]x#[default] Repeat search backward", From 0e418b62d259ce8da8970f75732cc6632ee4c3a0 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 30 Jun 2026 07:44:51 +0100 Subject: [PATCH 32/38] Remove C-r from help. --- mode-tree.c | 1 - 1 file changed, 1 deletion(-) diff --git a/mode-tree.c b/mode-tree.c index 825354970..fa868ef19 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -148,7 +148,6 @@ static const char* mode_tree_help_start[] = { "\r\033[1m T \033[0m\016x\017 \033[0mUntag all %1s\n", "\r\033[1m C-t \033[0m\016x\017 \033[0mTag all %1s\n", "\r\033[1m C-s \033[0m\016x\017 \033[0mSearch forward\n", - "\r\033[1m C-r \033[0m\016x\017 \033[0mSearch backward\n", "\r\033[1m n \033[0m\016x\017 \033[0mRepeat search forward\n", "\r\033[1m N \033[0m\016x\017 \033[0mRepeat search backward\n", "\r\033[1m f \033[0m\016x\017 \033[0mFilter %1s\n", From abefc3f7054c4330018bc928c3595841042021a7 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 30 Jun 2026 09:20:43 +0000 Subject: [PATCH 33/38] Redraw when sync stops again (accidentally turned off), from Japin Li in GitHub issue 5304. --- screen-write.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/screen-write.c b/screen-write.c index 31d177ef0..4a3a11c83 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1011,6 +1011,8 @@ screen_write_stop_sync(struct window_pane *wp) evtimer_del(&wp->sync_timer); wp->base.mode &= ~MODE_SYNC; + wp->flags |= PANE_REDRAW; + log_debug("%s: %%%u stopped sync mode", __func__, wp->id); } From f8674cc993dd09cb355fe920eb95973198a3b254 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 30 Jun 2026 10:07:05 +0000 Subject: [PATCH 34/38] Add default features for Ghostty, GitHub issue 5305 from Glib Shpychka. --- tty-features.c | 12 ++++++++++++ tty-keys.c | 2 ++ 2 files changed, 14 insertions(+) diff --git a/tty-features.c b/tty-features.c index b321af279..28b6d0f64 100644 --- a/tty-features.c +++ b/tty-features.c @@ -573,6 +573,18 @@ tty_default_features(int *feat, const char *name, u_int version) "hyperlinks," "usstyle" }, + { .name = "ghostty", + .features = TTY_FEATURES_BASE_MODERN_XTERM "," + "ccolour," + "cstyle," + "extkeys," + "focus," + "hyperlinks," + "osc7," + "sync," + "usstyle," + "progressbar" + }, { .name = "XTerm", /* * xterm also supports DECSLRM and DECFRA, but they can be diff --git a/tty-keys.c b/tty-keys.c index d3a93f10f..f48fb1b71 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1663,6 +1663,8 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, tty_default_features(features, "foot", 0); else if (strncmp(tmp, "WezTerm ", 7) == 0) tty_default_features(features, "WezTerm", 0); + else if (strncmp(tmp, "ghostty ", 8) == 0) + tty_default_features(features, "ghostty", 0); log_debug("%s: received extended DA %.*s", c->name, (int)*size, buf); free(c->term_type); From eda7e563d01ca4e1c5154fb16a35a19e0df56f91 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 30 Jun 2026 20:27:36 +0000 Subject: [PATCH 35/38] Change a malloc to calloc. --- grid.c | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/grid.c b/grid.c index 44efa574d..3b1b96a38 100644 --- a/grid.c +++ b/grid.c @@ -308,27 +308,16 @@ grid_create(u_int sx, u_int sy, u_int hlimit) { struct grid *gd; - gd = xmalloc(sizeof *gd); + gd = xcalloc(1, sizeof *gd); gd->sx = sx; gd->sy = sy; if (hlimit != 0) gd->flags = GRID_HISTORY; - else - gd->flags = 0; - - gd->hscrolled = 0; - gd->hsize = 0; gd->hlimit = hlimit; - gd->scroll_added = 0; - gd->scroll_collected = 0; - gd->scroll_generation = 0; - if (gd->sy != 0) gd->linedata = xcalloc(gd->sy, sizeof *gd->linedata); - else - gd->linedata = NULL; return (gd); } From 928c7a97715b384c1fc039f7ad032cbc6ee0f1c0 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 30 Jun 2026 21:28:28 +0100 Subject: [PATCH 36/38] More checks for macOS. --- grid.c | 35 +++++++++++++++++++++++++++++++++++ screen.c | 2 +- tmux.h | 1 + 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/grid.c b/grid.c index 28680cbdd..da5c69177 100644 --- a/grid.c +++ b/grid.c @@ -74,11 +74,45 @@ grid_check_lines(struct grid *gd) } } } + +void +grid_check_is_clear(struct grid *gd) +{ + struct grid_line *gl; + u_int yy, ny; + + assert(gd != NULL); + + if (gd->sy == 0) { + assert(gd->linedata == NULL); + return; + } + + assert(gd->linedata != NULL); + + ny = gd->hsize + gd->sy; + for (yy = 0; yy < ny; yy++) { + gl = &gd->linedata[yy]; + + assert(gl->celldata == NULL); + assert(gl->cellused == 0); + assert(gl->cellsize == 0); + assert(gl->extddata == NULL); + assert(gl->extdsize == 0); + assert(gl->flags == 0); + assert(gl->time == 0); + } +} #else static void grid_check_lines(__unused struct grid *gd) { } + +void +grid_check_is_clear(__unused struct grid *gd) +{ +} #endif /* Store cell in entry. */ @@ -363,6 +397,7 @@ grid_create(u_int sx, u_int sy, u_int hlimit) else gd->linedata = NULL; + grid_check_is_clear(gd); return (gd); } diff --git a/screen.c b/screen.c index 8e513758e..f3fdfcbd4 100644 --- a/screen.c +++ b/screen.c @@ -123,7 +123,7 @@ screen_reinit(struct screen *s) s->saved_cy = UINT_MAX; screen_reset_tabs(s); - + grid_check_is_clear(s->grid); grid_clear_lines(s->grid, s->grid->hsize, s->grid->sy, 8); screen_clear_selection(s); diff --git a/tmux.h b/tmux.h index 863a41eca..175c9b78e 100644 --- a/tmux.h +++ b/tmux.h @@ -3323,6 +3323,7 @@ bitstr_t *fuzzy_match(const char *, const char *, u_int, u_int *); /* grid.c */ extern const struct grid_cell grid_default_cell; +void grid_check_is_clear(struct grid *); void grid_empty_line(struct grid *, u_int, u_int); void grid_set_tab(struct grid_cell *, u_int); int grid_cells_equal(const struct grid_cell *, const struct grid_cell *); From 74a5069c0a25a5207ceca6b22dc6c9b135bf118a Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 30 Jun 2026 22:44:21 +0000 Subject: [PATCH 37/38] Fix some indentation. --- server-client.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server-client.c b/server-client.c index 3dc294c99..e0a9d08b8 100644 --- a/server-client.c +++ b/server-client.c @@ -2093,10 +2093,10 @@ server_client_reset_state(struct client *c) mode |= MODE_MOUSE_ALL; } } - if (options_get_number(oo, "focus-follows-mouse") || - w->sb == PANE_SCROLLBARS_MODAL || - w->sb == PANE_SCROLLBARS_AUTOHIDE) - mode |= MODE_MOUSE_ALL; + if (options_get_number(oo, "focus-follows-mouse") || + w->sb == PANE_SCROLLBARS_MODAL || + w->sb == PANE_SCROLLBARS_AUTOHIDE) + mode |= MODE_MOUSE_ALL; else if (~mode & MODE_MOUSE_ALL) mode |= MODE_MOUSE_BUTTON; } From 1e9a16a748776cec6980f5d575d3235e2caf11f7 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 30 Jun 2026 22:48:12 +0000 Subject: [PATCH 38/38] And some other indentation. --- server-client.c | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/server-client.c b/server-client.c index e0a9d08b8..c25c7ca19 100644 --- a/server-client.c +++ b/server-client.c @@ -2052,19 +2052,18 @@ server_client_reset_state(struct client *c) if (!window_position_is_visible(r, cx)) cursor = 0; - if (window_pane_scrollbar_overlay_visible(wp)) { - sb_w = wp->scrollbar_style.width; - if (sb_w > wp->sx) - sb_w = wp->sx; - if (sb_w != 0 && - w->sb_pos == - PANE_SCROLLBARS_LEFT) { - if (s->cx < sb_w) + if (window_pane_scrollbar_overlay_visible(wp)) { + sb_w = wp->scrollbar_style.width; + if (sb_w > wp->sx) + sb_w = wp->sx; + if (sb_w != 0 && + w->sb_pos == PANE_SCROLLBARS_LEFT) { + if (s->cx < sb_w) + cursor = 0; + } else if (sb_w != 0 && + s->cx >= wp->sx - sb_w) cursor = 0; - } else if (sb_w != 0 && - s->cx >= wp->sx - sb_w) - cursor = 0; - } + } if (status_at_line(c) == 0) cy += status_line_size(c);