From 1a6de01210cf34a7e677ae3f86b21ece182fcabe Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 18 Jun 2026 08:56:07 +0000 Subject: [PATCH 1/7] Check the panes again if deferred redraw to make sure the flag is always set. Also add a helper for the loop. --- server-client.c | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/server-client.c b/server-client.c index 0aa27970..a3773358 100644 --- a/server-client.c +++ b/server-client.c @@ -2018,16 +2018,32 @@ server_client_check_modes(struct client *c) } } +/* Check if any panes need to be redrawn. */ +static int +server_client_any_pane_redraw(struct client *c) +{ + struct session *s = c->session; + struct window *w = s->curw->window; + struct window_pane *wp; + + if (c->flags & CLIENT_REDRAWWINDOW) + return (1); + TAILQ_FOREACH(wp, &w->panes, entry) { + if (wp->flags & (PANE_REDRAW|PANE_REDRAWSCROLLBAR)) + return (1); + } + return (0); +} + /* Check for client redraws. */ static void server_client_check_redraw(struct client *c) { struct session *s = c->session; struct tty *tty = &c->tty; - struct window *w = c->session->curw->window; + struct window *w = s->curw->window; struct window_pane *wp; int needed, tflags, mode = tty->mode; - uint64_t client_flags = 0; struct timeval tv = { .tv_usec = 1000 }; static struct event ev; size_t n; @@ -2046,20 +2062,8 @@ server_client_check_redraw(struct client *c) needed = 0; if (c->flags & CLIENT_ALLREDRAWFLAGS) needed = 1; - else { - TAILQ_FOREACH(wp, &w->panes, entry) { - if (wp->flags & PANE_REDRAW) { - needed = 1; - client_flags |= CLIENT_REDRAWWINDOW; - break; - } - if (wp->flags & PANE_REDRAWSCROLLBAR) { - needed = 1; - client_flags |= CLIENT_REDRAWWINDOW; - /* no break - later panes may need redraw */ - } - } - } + else if (server_client_any_pane_redraw(c)) + needed = 1; if (!needed) { c->flags &= ~CLIENT_STATUSFORCE; return; @@ -2082,7 +2086,8 @@ server_client_check_redraw(struct client *c) log_debug("redraw timer started"); evtimer_add(&ev, &tv); } - c->flags |= client_flags; + if (server_client_any_pane_redraw(c)) + c->flags |= CLIENT_REDRAWWINDOW; return; } From b1b184cdb8c9b8e695fd3394148be157eb671fe0 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 18 Jun 2026 09:11:24 +0000 Subject: [PATCH 2/7] Add functions to work out cell neighbours, and to remove a tiled cell. From Dane Jensen. --- layout.c | 113 +++++++++++++++++++++++++++++++++++++++++++++++-------- tmux.h | 6 ++- 2 files changed, 101 insertions(+), 18 deletions(-) diff --git a/layout.c b/layout.c index cc9f2351..93fa412a 100644 --- a/layout.c +++ b/layout.c @@ -588,12 +588,57 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, } } +/* Find and return the nearest neighbour to a cell in a specific direction. */ +static struct layout_cell * +layout_cell_get_neighbour_direction(struct layout_cell *lc, int direction) +{ + struct layout_cell *lcn = lc; + + while (1) { + if (direction) + lcn = TAILQ_NEXT(lcn, entry); + else + lcn = TAILQ_PREV(lcn, layout_cells, entry); + + if (lcn == NULL || + layout_cell_is_tiled(lcn) || + layout_cell_has_tiled_child(lcn)) + return (lcn); + } +} + +/* + * Find and return the nearest neighbour. Prefers cells "after" the specified + * cell. This behavior defines how cell dimensions are redistributed when a cell + * is hidden/shown and floated/tiled. + */ +struct layout_cell * +layout_cell_get_neighbour(struct layout_cell *lc) +{ + struct layout_cell *lcother, *lcparent = lc->parent; + int direction = 1; + + if (lcparent == NULL) + return (NULL); + + if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) + direction = !direction; + + lcother = layout_cell_get_neighbour_direction(lc, direction); + if (lcother == NULL) + lcother = layout_cell_get_neighbour_direction(lc, !direction); + + return (lcother); +} + + /* Destroy a cell and redistribute the space. */ void layout_destroy_cell(struct window *w, struct layout_cell *lc, struct layout_cell **lcroot) { - struct layout_cell *lcother = NULL, *lcparent; + struct layout_cell *lcother = NULL, *lcparent; + int change; /* If no parent, this is the last pane in a window. */ lcparent = lc->parent; @@ -604,27 +649,27 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc, return; } - if (~lc->flags & LAYOUT_CELL_FLOATING) { - /* Merge the space into the previous or next cell. */ - if (lc == TAILQ_FIRST(&lcparent->cells)) - lcother = TAILQ_NEXT(lc, entry); + if (!layout_cell_is_tiled(lc)) { + TAILQ_REMOVE(&lcparent->cells, lc, entry); + layout_free_cell(lc); + goto out; + } + + lcother = layout_cell_get_neighbour(lc); + if (lcother != NULL) { + if (lcparent->type == LAYOUT_LEFTRIGHT) + change = lc->sx + 1; else - lcother = TAILQ_PREV(lc, layout_cells, entry); - } - if (lcother != NULL && (~lcother->flags & LAYOUT_CELL_FLOATING)) { - if (lcparent->type == LAYOUT_LEFTRIGHT) { - layout_resize_adjust(w, lcother, lcparent->type, - lc->sx + 1); - } else { - layout_resize_adjust(w, lcother, lcparent->type, - lc->sy + 1); - } - } + change = lc->sy + 1; + layout_resize_adjust(w, lcother, lcparent->type, change); + } else + layout_remove_tile(w, lcparent); /* Remove this from the parent's list. */ TAILQ_REMOVE(&lcparent->cells, lc, entry); layout_free_cell(lc); +out: /* * If the parent now has one cell, remove the parent from the tree and * replace it by that cell. @@ -1564,3 +1609,39 @@ layout_get_floating_cell(struct cmdq_item *item, struct args *args, lcnew = layout_floating_pane(w, sx, sy, ox, oy); return (lcnew); } + +/* + * Removes a cell from the tiled layout by giving the cell's space to the + * nearest neighbour. + */ +int +layout_remove_tile(struct window *w, struct layout_cell *lc) +{ + struct layout_cell *lcneighbour, *lcparent; + enum layout_type type; + int change; + + if (lc->flags & LAYOUT_CELL_FLOATING) + return (0); + + lcneighbour = layout_cell_get_neighbour(lc); + if (lcneighbour == NULL) { + if (lc->parent != NULL) + layout_remove_tile(w, lc->parent); + } else if ((lcparent = lcneighbour->parent) != NULL) { + type = lcparent->type; + /* + * Adding the size of the layout cell plus its border to the + * neighbour. + */ + if (type == LAYOUT_TOPBOTTOM) + change = lc->sy + 1; + else + change = lc->sx + 1; + layout_resize_adjust(w, lcneighbour, type, change); + } + + /* Zeroing out the cell geometry until the cell is retiled. */ + layout_set_size(lc, 0, 0, 0, 0); + return (1); +} diff --git a/tmux.h b/tmux.h index 7398fec1..578910f8 100644 --- a/tmux.h +++ b/tmux.h @@ -3504,6 +3504,7 @@ void layout_fix_offsets(struct window *); void layout_fix_panes(struct window *, struct window_pane *); void layout_resize_adjust(struct window *, struct layout_cell *, enum layout_type, int); +struct layout_cell *layout_cell_get_neighbour(struct layout_cell *); void layout_init(struct window *, struct window_pane *); void layout_free(struct window *); void layout_resize(struct window *, u_int, u_int); @@ -3524,10 +3525,11 @@ struct layout_cell *layout_floating_pane(struct window *, u_int, u_int, int, void layout_close_pane(struct window_pane *); int layout_spread_cell(struct window *, struct layout_cell *); void layout_spread_out(struct window_pane *); -struct layout_cell *layout_get_floating_cell(struct cmdq_item *, struct args *, - struct window *, struct window_pane *, char **); struct layout_cell *layout_get_tiled_cell(struct cmdq_item *, struct args *, struct window *, struct window_pane *, int, char **); +struct layout_cell *layout_get_floating_cell(struct cmdq_item *, struct args *, + struct window *, struct window_pane *, char **); +int layout_remove_tile(struct window *, struct layout_cell *); /* layout-custom.c */ char *layout_dump(struct window *, struct layout_cell *); From d92a479d2f3489d97268b3fd6592e7300837a29d Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 18 Jun 2026 09:59:55 +0000 Subject: [PATCH 3/7] Add a dim= style attribute to dim the colours (as best as tmux is able to). GitHub issue 4842. --- colour.c | 22 +++++++++++++++++ format.c | 4 +-- input.c | 2 +- popup.c | 1 + screen-redraw.c | 2 +- screen-write.c | 3 ++- style.c | 16 +++++++++++- tmux.1 | 3 +++ tmux.h | 9 +++++-- tty.c | 65 +++++++++++++++++++++++++++++++++++++++++++------ window.c | 4 ++- 11 files changed, 115 insertions(+), 16 deletions(-) diff --git a/colour.c b/colour.c index 88198e96..1cad01f9 100644 --- a/colour.c +++ b/colour.c @@ -120,6 +120,28 @@ colour_force_rgb(int c) return (-1); } +/* Dim colour by a percentage. */ +int +colour_dim(int c, u_int dim) +{ + u_char r, g, b; + + if (dim == 0 || COLOUR_DEFAULT(c)) + return (c); + if (dim >= 100) + return (colour_join_rgb(0, 0, 0)); + + c = colour_force_rgb(c); + if (c == -1) + return (-1); + colour_split_rgb(c, &r, &g, &b); + + r = (r * (100 - dim)) / 100; + g = (g * (100 - dim)) / 100; + b = (b * (100 - dim)) / 100; + return (colour_join_rgb(r, g, b)); +} + /* Convert colour to a string. */ const char * colour_tostring(int c) diff --git a/format.c b/format.c index 4d58c40b..38b444ea 100644 --- a/format.c +++ b/format.c @@ -1006,7 +1006,7 @@ format_cb_pane_fg(struct format_tree *ft) if (wp == NULL) return (NULL); - tty_default_colours(&gc, wp); + tty_default_colours(&gc, wp, NULL); return (xstrdup(colour_tostring(gc.fg))); } @@ -1043,7 +1043,7 @@ format_cb_pane_bg(struct format_tree *ft) if (wp == NULL) return (NULL); - tty_default_colours(&gc, wp); + tty_default_colours(&gc, wp, NULL); return (xstrdup(colour_tostring(gc.bg))); } diff --git a/input.c b/input.c index 6d39850e..cbf54060 100644 --- a/input.c +++ b/input.c @@ -3018,7 +3018,7 @@ input_osc_10(struct input_ctx *ictx, const char *p) return; c = window_pane_get_fg_control_client(wp); if (c == -1) { - tty_default_colours(&defaults, wp); + tty_default_colours(&defaults, wp, NULL); if (COLOUR_DEFAULT(defaults.fg)) c = window_pane_get_fg(wp); else diff --git a/popup.c b/popup.c index aa463729..229a1d16 100644 --- a/popup.c +++ b/popup.c @@ -328,6 +328,7 @@ popup_draw_cb(struct client *c, void *data, struct screen_redraw_ctx *rctx) defaults.bg = pd->palette.bg; style_ctx.defaults = &defaults; style_ctx.palette = &pd->palette; + style_ctx.dim = 0; style_ctx.hyperlinks = s.hyperlinks; if (pd->md != NULL) { diff --git a/screen-redraw.c b/screen-redraw.c index 11e24369..cae6edb4 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1212,7 +1212,7 @@ screen_redraw_draw_pane(struct screen_redraw_ctx *ctx, struct window_pane *wp) } /* Set up the default style. */ - tty_default_colours(&defaults, wp); + tty_default_colours(&defaults, wp, &style_ctx.dim); style_ctx.defaults = &defaults; style_ctx.palette = palette; style_ctx.hyperlinks = s->hyperlinks; diff --git a/screen-write.c b/screen-write.c index 3fcd787d..f239be9a 100644 --- a/screen-write.c +++ b/screen-write.c @@ -252,7 +252,8 @@ screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, } else { ttyctx->redraw_cb = screen_write_redraw_cb; if (ctx->wp != NULL) { - tty_default_colours(&ttyctx->defaults, ctx->wp); + tty_default_colours(&ttyctx->defaults, ctx->wp, + &ttyctx->style_ctx.dim); ttyctx->style_ctx.palette = &ctx->wp->palette; ttyctx->set_client_cb = screen_write_set_client_cb; ttyctx->arg = ctx->wp; diff --git a/style.c b/style.c index 586e9f93..b79d3291 100644 --- a/style.c +++ b/style.c @@ -32,6 +32,7 @@ static struct style style_default = { { { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0 }, 0, + 0, 8, STYLE_ALIGN_DEFAULT, @@ -201,6 +202,13 @@ style_parse(struct style *sy, const struct grid_cell *base, const char *in) if ((value = colour_fromstring(tmp + 5)) == -1) goto error; sy->fill = value; + } else if (end > 4 && strncasecmp(tmp, "dim=", 4) == 0) { + if (tmp[end - 1] == '%') + tmp[end - 1] = '\0'; + n = strtonum(tmp + 4, 0, 100, &errstr); + if (errstr != NULL) + goto error; + sy->dim = n; } else if (end > 3 && strncasecmp(tmp + 1, "g=", 2) == 0) { if ((value = colour_fromstring(tmp + 3)) == -1) goto error; @@ -346,6 +354,11 @@ style_tostring(struct style *sy) colour_tostring(sy->fill)); comma = ","; } + if (sy->dim != 0) { + off += xsnprintf(s + off, sizeof s - off, "%sdim=%d%%", comma, + sy->dim); + comma = ","; + } if (gc->fg != 8) { off += xsnprintf(s + off, sizeof s - off, "%sfg=%s", comma, colour_tostring(gc->fg)); @@ -386,7 +399,7 @@ style_tostring(struct style *sy) } /* Apply a style on top of the given style. */ -void +struct style * style_add(struct grid_cell *gc, struct options *oo, const char *name, struct format_tree *ft) { @@ -409,6 +422,7 @@ style_add(struct grid_cell *gc, struct options *oo, const char *name, if (ft0 != NULL) format_free(ft0); + return (sy); } /* Apply a style on top of the default style. */ diff --git a/tmux.1 b/tmux.1 index 1842ebca..3f77e48f 100644 --- a/tmux.1 +++ b/tmux.1 @@ -7011,6 +7011,9 @@ is the terminal alternate character set. Align text to the left, centre or right of the available space if appropriate. .It Ic fill=colour Fill the available space with a background colour if appropriate. +.It Ic dim=percentage +Dim foreground and background colours by +.Ar percentage . .It Ic width=N Set the width of the styled area. .Ar N diff --git a/tmux.h b/tmux.h index 578910f8..7e7db3d1 100644 --- a/tmux.h +++ b/tmux.h @@ -946,6 +946,7 @@ enum style_default_type { struct style { struct grid_cell gc; int ignore; + int dim; int fill; enum style_align align; @@ -1289,6 +1290,8 @@ struct window_pane { struct grid_cell cached_gc; struct grid_cell cached_active_gc; + u_int cached_dim; + u_int cached_active_dim; struct colour_palette palette; enum client_theme last_theme; struct style_line_entry border_status_line; @@ -1629,6 +1632,7 @@ LIST_HEAD(tty_terms, tty_term); struct tty_style_ctx { const struct grid_cell *defaults; struct colour_palette *palette; + u_int dim; struct hyperlinks *hyperlinks; }; @@ -2720,7 +2724,7 @@ void tty_cmd_reverseindex(struct tty *, const struct tty_ctx *); void tty_cmd_setselection(struct tty *, const struct tty_ctx *); void tty_cmd_rawstring(struct tty *, const struct tty_ctx *); void tty_cmd_syncstart(struct tty *, const struct tty_ctx *); -void tty_default_colours(struct grid_cell *, struct window_pane *); +void tty_default_colours(struct grid_cell *, struct window_pane *, u_int *); /* tty-term.c */ extern struct tty_terms tty_terms; @@ -3157,6 +3161,7 @@ int colour_find_rgb(u_char, u_char, u_char); int colour_join_rgb(u_char, u_char, u_char); void colour_split_rgb(int, u_char *, u_char *, u_char *); int colour_force_rgb(int); +int colour_dim(int, u_int); const char *colour_tostring(int); enum client_theme colour_totheme(int); int colour_fromstring(const char *); @@ -3806,7 +3811,7 @@ int popup_modify(struct client *, const char *, const char *, int style_parse(struct style *,const struct grid_cell *, const char *); const char *style_tostring(struct style *); -void style_add(struct grid_cell *, struct options *, +struct style *style_add(struct grid_cell *, struct options *, const char *, struct format_tree *); void style_apply(struct grid_cell *, struct options *, const char *, struct format_tree *); diff --git a/tty.c b/tty.c index 5b66e2d1..15cab4bf 100644 --- a/tty.c +++ b/tty.c @@ -80,7 +80,7 @@ static int tty_check_overlay(struct tty *, u_int, u_int); #define TTY_REQUEST_LIMIT 30 static struct tty_style_ctx tty_default_style_ctx = { - &grid_default_cell, NULL, NULL + &grid_default_cell, NULL, 0, NULL }; void @@ -2467,16 +2467,39 @@ tty_hyperlink(struct tty *tty, const struct grid_cell *gc, tty_putcode_ss(tty, TTYC_HLS, id, uri); } +static int +tty_dim_default_colour(struct tty *tty, int c, int foreground) +{ + enum client_theme theme; + + if (!COLOUR_DEFAULT(c)) + return (c); + + if (foreground && tty->fg != -1) + return (tty->fg); + if (!foreground && tty->bg != -1) + return (tty->bg); + + theme = tty->client->theme; + if (theme == THEME_DARK) + return (foreground ? 7 : 0); + if (theme == THEME_LIGHT) + return (foreground ? 0 : 7); + return (c); +} + void tty_attributes(struct tty *tty, const struct grid_cell *gc, const struct tty_style_ctx *style_ctx) { struct grid_cell *tc = &tty->cell, gc2; + struct colour_palette *palette; int changed; /* Use default style if not given. */ if (style_ctx == NULL) style_ctx = &tty_default_style_ctx; + palette = style_ctx->palette; /* Copy cell and update default colours. */ memcpy(&gc2, gc, sizeof gc2); @@ -2485,6 +2508,24 @@ tty_attributes(struct tty *tty, const struct grid_cell *gc, gc2.fg = style_ctx->defaults->fg; if (gc2.bg == 8) gc2.bg = style_ctx->defaults->bg; + if (palette != NULL) { + changed = colour_palette_get(palette, gc2.fg); + if (changed != -1) + gc2.fg = changed; + changed = colour_palette_get(palette, gc2.bg); + if (changed != -1) + gc2.bg = changed; + } + } + if (style_ctx->dim != 0) { + gc2.fg = tty_dim_default_colour(tty, gc2.fg, 1); + gc2.bg = tty_dim_default_colour(tty, gc2.bg, 0); + changed = colour_dim(gc2.fg, style_ctx->dim); + if (changed != -1) + gc2.fg = changed; + changed = colour_dim(gc2.bg, style_ctx->dim); + if (changed != -1) + gc2.bg = changed; } /* Ignore cell if it is the same as the last one. */ @@ -2511,9 +2552,9 @@ tty_attributes(struct tty *tty, const struct grid_cell *gc, } /* Fix up the colours if necessary. */ - tty_check_fg(tty, style_ctx->palette, &gc2); - tty_check_bg(tty, style_ctx->palette, &gc2); - tty_check_us(tty, style_ctx->palette, &gc2); + tty_check_fg(tty, palette, &gc2); + tty_check_bg(tty, palette, &gc2); + tty_check_us(tty, palette, &gc2); /* * If any bits are being cleared or the underline colour is now default, @@ -2925,6 +2966,7 @@ tty_style_changed(struct window_pane *wp) { struct options *oo = wp->options; struct format_tree *ft; + struct style *sy; log_debug("%%%u: style changed", wp->id); wp->flags &= ~PANE_STYLECHANGED; @@ -2933,16 +2975,18 @@ tty_style_changed(struct window_pane *wp) format_defaults(ft, NULL, NULL, NULL, wp); tty_window_default_style(&wp->cached_active_gc, wp); - style_add(&wp->cached_active_gc, oo, "window-active-style", ft); + sy = style_add(&wp->cached_active_gc, oo, "window-active-style", ft); + wp->cached_active_dim = sy->dim; tty_window_default_style(&wp->cached_gc, wp); - style_add(&wp->cached_gc, oo, "window-style", ft); + sy = style_add(&wp->cached_gc, oo, "window-style", ft); + wp->cached_dim = sy->dim; format_free(ft); } void -tty_default_colours(struct grid_cell *gc, struct window_pane *wp) +tty_default_colours(struct grid_cell *gc, struct window_pane *wp, u_int *dim) { if (wp->flags & PANE_STYLECHANGED) tty_style_changed (wp); @@ -2956,6 +3000,13 @@ tty_default_colours(struct grid_cell *gc, struct window_pane *wp) gc->bg = wp->cached_active_gc.bg; else gc->bg = wp->cached_gc.bg; + + if (dim != NULL) { + if (wp == wp->window->active) + *dim = wp->cached_active_dim; + else + *dim = wp->cached_dim; + } } void diff --git a/window.c b/window.c index c6f0d9d3..5361884d 100644 --- a/window.c +++ b/window.c @@ -592,6 +592,8 @@ window_redraw_active_switch(struct window *w, struct window_pane *wp) gc2 = &wp->cached_active_gc; if (!grid_cells_look_equal(gc1, gc2)) wp->flags |= PANE_REDRAW; + else if (wp->cached_dim != wp->cached_active_dim) + wp->flags |= PANE_REDRAW; else { c1 = window_pane_get_palette(wp, gc1->fg); c2 = window_pane_get_palette(wp, gc2->fg); @@ -1963,7 +1965,7 @@ window_pane_get_bg(struct window_pane *wp) c = window_pane_get_bg_control_client(wp); if (c == -1) { - tty_default_colours(&defaults, wp); + tty_default_colours(&defaults, wp, NULL); if (COLOUR_DEFAULT(defaults.bg)) c = window_get_bg_client(wp); else From 0a6c8460919e3b293a1358a91c523687d27429c6 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 18 Jun 2026 10:56:22 +0000 Subject: [PATCH 4/7] Use evbuffer for building S, W, L lists, also add an xmemdup helper instead of misusing asprintf. --- format.c | 86 +++++++++++++++++++++++++++++++++---------------------- xmalloc.c | 12 ++++++++ xmalloc.h | 1 + 3 files changed, 64 insertions(+), 35 deletions(-) diff --git a/format.c b/format.c index 38b444ea..18d9c180 100644 --- a/format.c +++ b/format.c @@ -559,7 +559,7 @@ format_cb_session_attached_list(struct format_tree *ft) } if ((size = EVBUFFER_LENGTH(buffer)) != 0) - xasprintf(&value, "%.*s", size, EVBUFFER_DATA(buffer)); + value = xmemdup(EVBUFFER_DATA(buffer), size); evbuffer_free(buffer); return (value); } @@ -698,7 +698,7 @@ format_cb_window_linked_sessions_list(struct format_tree *ft) } if ((size = EVBUFFER_LENGTH(buffer)) != 0) - xasprintf(&value, "%.*s", size, EVBUFFER_DATA(buffer)); + value = xmemdup(EVBUFFER_DATA(buffer), size); evbuffer_free(buffer); return (value); } @@ -752,7 +752,7 @@ format_cb_window_active_sessions_list(struct format_tree *ft) } if ((size = EVBUFFER_LENGTH(buffer)) != 0) - xasprintf(&value, "%.*s", size, EVBUFFER_DATA(buffer)); + value = xmemdup(EVBUFFER_DATA(buffer), size); evbuffer_free(buffer); return (value); } @@ -816,7 +816,7 @@ format_cb_window_active_clients_list(struct format_tree *ft) } if ((size = EVBUFFER_LENGTH(buffer)) != 0) - xasprintf(&value, "%.*s", size, EVBUFFER_DATA(buffer)); + value = xmemdup(EVBUFFER_DATA(buffer), size); evbuffer_free(buffer); return (value); } @@ -991,7 +991,7 @@ format_cb_pane_tabs(struct format_tree *ft) evbuffer_add_printf(buffer, "%u", i); } if ((size = EVBUFFER_LENGTH(buffer)) != 0) - xasprintf(&value, "%.*s", size, EVBUFFER_DATA(buffer)); + value = xmemdup(EVBUFFER_DATA(buffer), size); evbuffer_free(buffer); return (value); } @@ -1075,7 +1075,7 @@ format_cb_session_group_list(struct format_tree *ft) } if ((size = EVBUFFER_LENGTH(buffer)) != 0) - xasprintf(&value, "%.*s", size, EVBUFFER_DATA(buffer)); + value = xmemdup(EVBUFFER_DATA(buffer), size); evbuffer_free(buffer); return (value); } @@ -1115,7 +1115,7 @@ format_cb_session_group_attached_list(struct format_tree *ft) } if ((size = EVBUFFER_LENGTH(buffer)) != 0) - xasprintf(&value, "%.*s", size, EVBUFFER_DATA(buffer)); + value = xmemdup(EVBUFFER_DATA(buffer), size); evbuffer_free(buffer); return (value); } @@ -4653,7 +4653,8 @@ format_loop_sessions(struct format_expand_state *es, const char *fmt) struct format_tree *nft; struct format_expand_state next; char *all, *active, *use, *expanded, *value; - size_t valuelen; + struct evbuffer *buffer; + size_t size; struct session *s, **l; int i, n, last = 0; @@ -4662,8 +4663,9 @@ format_loop_sessions(struct format_expand_state *es, const char *fmt) active = NULL; } - value = xcalloc(1, 1); - valuelen = 1; + buffer = evbuffer_new(); + if (buffer == NULL) + fatalx("out of memory"); l = sort_get_sessions(&n, sc); for (i = 0; i < n; i++) { @@ -4684,16 +4686,18 @@ format_loop_sessions(struct format_expand_state *es, const char *fmt) expanded = format_expand1(&next, use); format_free(next.ft); - valuelen += strlen(expanded); - value = xrealloc(value, valuelen); - - strlcat(value, expanded, valuelen); + evbuffer_add(buffer, expanded, strlen(expanded)); free(expanded); } free(active); free(all); + if ((size = EVBUFFER_LENGTH(buffer)) != 0) + value = xmemdup(EVBUFFER_DATA(buffer), size); + else + value = xstrdup(""); + evbuffer_free(buffer); return (value); } @@ -4763,7 +4767,8 @@ format_loop_windows(struct format_expand_state *es, const char *fmt) struct format_tree *nft; struct format_expand_state next; char *all, *active, *use, *expanded, *value; - size_t valuelen; + struct evbuffer *buffer; + size_t size; struct winlink *wl, **l; struct window *w; int i, n, last = 0; @@ -4778,8 +4783,9 @@ format_loop_windows(struct format_expand_state *es, const char *fmt) active = NULL; } - value = xcalloc(1, 1); - valuelen = 1; + buffer = evbuffer_new(); + if (buffer == NULL) + fatalx("out of memory"); l = sort_get_winlinks_session(ft->s, &n, sc); for (i = 0; i < n; i++) { @@ -4811,16 +4817,18 @@ format_loop_windows(struct format_expand_state *es, const char *fmt) expanded = format_expand1(&next, use); format_free(nft); - valuelen += strlen(expanded); - value = xrealloc(value, valuelen); - - strlcat(value, expanded, valuelen); + evbuffer_add(buffer, expanded, strlen(expanded)); free(expanded); } free(active); free(all); + if ((size = EVBUFFER_LENGTH(buffer)) != 0) + value = xmemdup(EVBUFFER_DATA(buffer), size); + else + value = xstrdup(""); + evbuffer_free(buffer); return (value); } @@ -4835,7 +4843,8 @@ format_loop_panes(struct format_expand_state *es, const char *fmt) struct format_tree *nft; struct format_expand_state next; char *all, *active, *use, *expanded, *value; - size_t valuelen; + struct evbuffer *buffer; + size_t size; struct window_pane *wp, **l; int i, n, last = 0; @@ -4849,8 +4858,9 @@ format_loop_panes(struct format_expand_state *es, const char *fmt) active = NULL; } - value = xcalloc(1, 1); - valuelen = 1; + buffer = evbuffer_new(); + if (buffer == NULL) + fatalx("out of memory"); l = sort_get_panes_window(ft->w, &n, sc); for (i = 0; i < n; i++) { @@ -4870,16 +4880,18 @@ format_loop_panes(struct format_expand_state *es, const char *fmt) expanded = format_expand1(&next, use); format_free(nft); - valuelen += strlen(expanded); - value = xrealloc(value, valuelen); - - strlcat(value, expanded, valuelen); + evbuffer_add(buffer, expanded, strlen(expanded)); free(expanded); } free(active); free(all); + if ((size = EVBUFFER_LENGTH(buffer)) != 0) + value = xmemdup(EVBUFFER_DATA(buffer), size); + else + value = xstrdup(""); + evbuffer_free(buffer); return (value); } @@ -4894,11 +4906,13 @@ format_loop_clients(struct format_expand_state *es, const char *fmt) struct format_tree *nft; struct format_expand_state next; char *expanded, *value; - size_t valuelen; + struct evbuffer *buffer; + size_t size; int i, n, last = 0; - value = xcalloc(1, 1); - valuelen = 1; + buffer = evbuffer_new(); + if (buffer == NULL) + fatalx("out of memory"); l = sort_get_clients(&n, sc); for (i = 0; i < n; i++) { @@ -4913,13 +4927,15 @@ format_loop_clients(struct format_expand_state *es, const char *fmt) expanded = format_expand1(&next, fmt); format_free(nft); - valuelen += strlen(expanded); - value = xrealloc(value, valuelen); - - strlcat(value, expanded, valuelen); + evbuffer_add(buffer, expanded, strlen(expanded)); free(expanded); } + if ((size = EVBUFFER_LENGTH(buffer)) != 0) + value = xmemdup(EVBUFFER_DATA(buffer), size); + else + value = xstrdup(""); + evbuffer_free(buffer); return (value); } diff --git a/xmalloc.c b/xmalloc.c index d11d8dc7..14e805ed 100644 --- a/xmalloc.c +++ b/xmalloc.c @@ -105,6 +105,18 @@ xstrndup(const char *str, size_t maxlen) return cp; } +char * +xmemdup(const void *ptr, size_t len) +{ + char *cp; + + cp = xmalloc(len + 1); + if (len != 0) + memcpy(cp, ptr, len); + cp[len] = '\0'; + return cp; +} + int xasprintf(char **ret, const char *fmt, ...) { diff --git a/xmalloc.h b/xmalloc.h index b4dcb535..93a4155e 100644 --- a/xmalloc.h +++ b/xmalloc.h @@ -26,6 +26,7 @@ void *xreallocarray(void *, size_t, size_t); void *xrecallocarray(void *, size_t, size_t, size_t); char *xstrdup(const char *); char *xstrndup(const char *, size_t); +char *xmemdup(const void *, size_t); int xasprintf(char **, const char *, ...) __attribute__((__format__ (printf, 2, 3))) __attribute__((__nonnull__ (2))); From cba4ba9cdcdae3e782d9ac247d2c83ee885207f6 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 18 Jun 2026 11:45:08 +0000 Subject: [PATCH 5/7] Check time periodically in loops rather than every one. --- format.c | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/format.c b/format.c index 18d9c180..8972d7d2 100644 --- a/format.c +++ b/format.c @@ -128,6 +128,9 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2) /* Limit on time taken (milliseconds). */ #define FORMAT_TIME_LIMIT 100 +/* How often to check the time in long loops. */ +#define FORMAT_TIME_LOOP_CHECK 10000 + /* Format expand flags. */ #define FORMAT_EXPAND_TIME 0x1 #define FORMAT_EXPAND_NOJOBS 0x2 @@ -4219,10 +4222,14 @@ found: /* Check if format has not taken too long. */ static int -format_check_time(struct format_expand_state *es) +format_check_time(struct format_expand_state *es, u_int *check) { - uint64_t t = get_timer(); + uint64_t t; + if (check != NULL && ++*check % FORMAT_TIME_LOOP_CHECK != 0) + return (1); + + t = get_timer(); if (t - es->start_time < FORMAT_TIME_LIMIT) return (1); t -= es->start_time; @@ -4237,10 +4244,11 @@ format_unescape(struct format_expand_state *es, const char *s) { char *out, *cp; int brackets = 0; + u_int check = 0; cp = out = xmalloc(strlen(s) + 1); for (; *s != '\0'; s++) { - if (!format_check_time(es)){ + if (!format_check_time(es, &check)) { free(out); return (xstrdup("")); } @@ -4266,10 +4274,11 @@ format_strip(struct format_expand_state *es, const char *s) { char *out, *cp; int brackets = 0; + u_int check = 0; cp = out = xmalloc(strlen(s) + 1); for (; *s != '\0'; s++) { - if (!format_check_time(es)){ + if (!format_check_time(es, &check)) { free(out); return (xstrdup("")); } @@ -4293,9 +4302,10 @@ static const char * format_skip1(struct format_expand_state *es, const char *s, const char *end) { int brackets = 0; + u_int check = 0; for (; *s != '\0'; s++) { - if (es != NULL && !format_check_time(es)) + if (es != NULL && !format_check_time(es, &check)) return (NULL); if (*s == '#' && s[1] == '{') brackets++; @@ -5101,7 +5111,7 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, struct format_modifier *list, *cmp = NULL, *search = NULL; struct format_modifier **sub = NULL, *mexp = NULL, *fm; struct format_modifier *bool_op_n = NULL; - u_int i, count, nsub = 0, nrep; + u_int i, count, nsub = 0, nrep, check = 0; struct format_expand_state next; struct environ_entry *envent; @@ -5434,7 +5444,7 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, else { value = xstrdup(""); for (i = 0; i < nrep; i++) { - if (!format_check_time(es)) { + if (!format_check_time(es, &check)) { free(right); free(left); free(value); @@ -5714,7 +5724,7 @@ format_expand1(struct format_expand_state *es, const char *fmt) int ch, brackets; char expanded[8192]; - if (fmt == NULL || *fmt == '\0' || !format_check_time(es)) + if (fmt == NULL || *fmt == '\0' || !format_check_time(es, NULL)) return (xstrdup("")); if (es->loop == FORMAT_LOOP_LIMIT) { From 14fc4a06a6c5e986ea630911d7bea553e104b037 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 18 Jun 2026 11:54:37 +0000 Subject: [PATCH 6/7] Unescape arguments before passing to format_expand1 so that escaping :s etc actually works. --- format.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/format.c b/format.c index 8972d7d2..0bd8f58c 100644 --- a/format.c +++ b/format.c @@ -4240,22 +4240,24 @@ format_check_time(struct format_expand_state *es, u_int *check) /* Unescape escaped characters. */ static char * -format_unescape(struct format_expand_state *es, const char *s) +format_unescape(struct format_expand_state *es, const char *s, size_t n) { - char *out, *cp; - int brackets = 0; - u_int check = 0; + const char *end = s + n; + char *out, *cp; + int brackets = 0; + u_int check = 0; - cp = out = xmalloc(strlen(s) + 1); - for (; *s != '\0'; s++) { + cp = out = xmalloc(n + 1); + for (; s != end; s++) { if (!format_check_time(es, &check)) { free(out); return (xstrdup("")); } - if (*s == '#' && s[1] == '{') + if (*s == '#' && s + 1 != end && s[1] == '{') brackets++; if (brackets == 0 && *s == '#' && + s + 1 != end && strchr(",#{}:", s[1]) != NULL) { *cp++ = *++s; continue; @@ -4476,7 +4478,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, break; argv = xcalloc(1, sizeof *argv); - value = xstrndup(cp + 1, end - (cp + 1)); + value = format_unescape(es, cp + 1, end - (cp + 1)); argv[0] = format_expand1(es, value); free(value); argc = 1; @@ -4500,7 +4502,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, cp++; argv = xreallocarray(argv, argc + 1, sizeof *argv); - value = xstrndup(cp, end - cp); + value = format_unescape(es, cp, end - cp); argv[argc++] = format_expand1(es, value); free(value); @@ -5368,7 +5370,7 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, /* Is this a literal string? */ if (modifiers & FORMAT_LITERAL) { format_log(es, "literal string is '%s'", copy); - value = format_unescape(es, copy); + value = format_unescape(es, copy, strlen(copy)); goto done; } From 6d9fbb0e97097c5e5c8dc44a5c78f7cf83a7769d Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 18 Jun 2026 12:55:13 +0100 Subject: [PATCH 7/7] Update regress for format changes. --- regress/format-strings.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/regress/format-strings.sh b/regress/format-strings.sh index 8d192ed2..bae66ab0 100644 --- a/regress/format-strings.sh +++ b/regress/format-strings.sh @@ -67,6 +67,7 @@ $TMUX set @true 1 || exit 1 $TMUX set @false 0 || exit 1 $TMUX set @warm Summer || exit 1 $TMUX set @cold Winter || exit 1 +$TMUX set @v 'foo:bar' || exit 1 # Plain string without substitutions et al test_format "abc xyz" "abc xyz" @@ -74,6 +75,8 @@ test_format "abc xyz" "abc xyz" # Test basic escapes for "#", "{", "#{" "}", "#}", "," test_format "##" "#" test_format "#," "," +test_format "#{s/#:/_/:@v}" "foo_bar" +test_format "#{s/o/#:/:@v}" "f:::bar" test_format "{" "{" test_format "##{" "#{" test_format "#}" "}"