From 7383c04205fc37c18f0e047a07d7273790784473 Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 27 Jun 2026 10:05:38 +0000 Subject: [PATCH 01/14] Preseve prompt flags across line clear, from Aung Myo Kyaw. --- cmd-split-window.c | 4 ++-- screen-write.c | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd-split-window.c b/cmd-split-window.c index cdc42cefe..5d21895c6 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -41,8 +41,8 @@ const struct cmd_entry cmd_new_pane_entry = { .args = { "bB:c:de:EfF:hIkl:Lm:p:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, .usage = "[-bdefhIklPvWZ] [-B border-lines] " - "[-c start-directory] [-e environment] " - "[-F format] [-l size] [-m message] [-p percentage] " + "[-c start-directory] [-e environment] " + "[-F format] [-l size] [-m message] [-p percentage] " "[-s style] [-S active-border-style] " "[-R inactive-border-style] [-T title] [-x width] [-y height] " "[-X x-position] [-Y y-position] " CMD_TARGET_PANE_USAGE " " diff --git a/screen-write.c b/screen-write.c index 45d126a6f..ba7ffd930 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1494,12 +1494,16 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg) struct grid_line *gl; u_int sx = screen_size_x(s); struct screen_write_citem *ci = ctx->item; + u_int flags; gl = grid_get_line(s->grid, s->grid->hsize + s->cy); if (gl->cellsize == 0 && COLOUR_DEFAULT(bg)) return; + flags = gl->flags & (GRID_LINE_START_PROMPT|GRID_LINE_START_OUTPUT); grid_view_clear(s->grid, 0, s->cy, sx, 1, bg); + gl = grid_get_line(s->grid, s->grid->hsize + s->cy); + gl->flags |= flags; screen_write_collect_clear(ctx, s->cy, 1); ci->x = 0; From dd02989e88b700b18ffa0230b49e245ca340ee48 Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 27 Jun 2026 10:16:15 +0000 Subject: [PATCH 02/14] Invalidate scene when changing window Z index, from Dane Jensen. --- window.c | 1 + 1 file changed, 1 insertion(+) diff --git a/window.c b/window.c index ac71a72bd..1965685b6 100644 --- a/window.c +++ b/window.c @@ -624,6 +624,7 @@ window_redraw_active_switch(struct window *w, struct window_pane *wp) TAILQ_REMOVE(&w->z_index, wp, zentry); TAILQ_INSERT_HEAD(&w->z_index, wp, zentry); wp->flags |= PANE_REDRAW; + redraw_invalidate_scene(w); } wp = w->active; From e24969b4227266e5775bdc9688c14c937e2713df Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 27 Jun 2026 10:18:32 +0000 Subject: [PATCH 03/14] Add a helper to swap a node into the place of a leaf, from Dane Jensen. --- layout.c | 55 ++++++++++++++++++++++++++++++++++--------------------- tmux.h | 2 ++ 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/layout.c b/layout.c index 569616f1f..d5dcf1ccf 100644 --- a/layout.c +++ b/layout.c @@ -1184,6 +1184,32 @@ layout_resize_child_cells(struct window *w, struct layout_cell *lc) } } +/* + * Replaces the provided layout cell with a new node of the specified type and + * inserts the cell into it. Used when creating new cells requires a different + * layout type, or when the root layout is a window pane. + */ +struct layout_cell * +layout_replace_with_node(struct window *w, struct layout_cell *lc, + enum layout_type type) +{ + struct layout_cell *lcparent; + + lcparent = layout_create_cell(lc->parent); + layout_make_node(lcparent, type); + layout_set_size(lcparent, lc->sx, lc->sy, lc->xoff, lc->yoff); + if (lc->parent == NULL) + w->layout_root = lcparent; + else + TAILQ_REPLACE(&lc->parent->cells, lc, lcparent, entry); + + /* Insert the old cell. */ + lc->parent = lcparent; + TAILQ_INSERT_HEAD(&lcparent->cells, lc, entry); + + return (lcparent); +} + /* * Split a pane into two. size is a hint, or -1 for default half/half * split. This must be followed by layout_assign_pane before much else happens! @@ -1315,17 +1341,7 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, */ /* Create and insert the replacement parent. */ - lcparent = layout_create_cell(lc->parent); - layout_make_node(lcparent, type); - layout_set_size(lcparent, sx, sy, xoff, yoff); - if (lc->parent == NULL) - wp->window->layout_root = lcparent; - else - TAILQ_REPLACE(&lc->parent->cells, lc, lcparent, entry); - - /* Insert the old cell. */ - lc->parent = lcparent; - TAILQ_INSERT_HEAD(&lcparent->cells, lc, entry); + lcparent = layout_replace_with_node(wp->window, lc, type); /* Create the new child cell. */ lcnew = layout_create_cell(lcparent); @@ -1384,14 +1400,7 @@ layout_floating_pane(struct window *w, struct window_pane *wp, u_int sx, * Adding a pane to a root that isn't node. Must create and * insert a new root. */ - lcparent = layout_create_cell(NULL); - layout_make_node(lcparent, LAYOUT_TOPBOTTOM); - layout_set_size(lcparent, w->sx, w->sy, 0, 0); - w->layout_root = lcparent; - - /* Insert the old cell. */ - lc->parent = lcparent; - TAILQ_INSERT_HEAD(&lcparent->cells, lc, entry); + lcparent = layout_replace_with_node(w, lc, LAYOUT_TOPBOTTOM); } lcnew = layout_create_cell(lcparent); @@ -1712,7 +1721,11 @@ layout_remove_tile(struct window *w, struct layout_cell *lc) 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); + /* + * Zeroing out the cell geometry until the cell is retiled unless this + * is the top level node. + */ + if (lc->parent != NULL) + layout_set_size(lc, 0, 0, 0, 0); return (1); } diff --git a/tmux.h b/tmux.h index d80e9b793..b03525c7e 100644 --- a/tmux.h +++ b/tmux.h @@ -3643,6 +3643,8 @@ int layout_resize_floating_pane_to(struct window_pane *, enum layout_type, u_int, char **); void layout_assign_pane(struct layout_cell *, struct window_pane *, int); +struct layout_cell *layout_replace_with_node(struct window *, + struct layout_cell *, enum layout_type); struct layout_cell *layout_split_pane(struct window_pane *, enum layout_type, int, int); struct layout_cell *layout_floating_pane(struct window *, struct window_pane *, From 5e0b0a2aa3cc5da7f724ef03eab5c60636545890 Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 27 Jun 2026 10:21:26 +0000 Subject: [PATCH 04/14] Pulled code out of layout_split_pane for reuse and added layout_insert_tile. From Dane Jensen. --- layout.c | 221 ++++++++++++++++++++++++++++++++++++++++++++----------- tmux.h | 7 ++ 2 files changed, 184 insertions(+), 44 deletions(-) diff --git a/layout.c b/layout.c index d5dcf1ccf..279f504af 100644 --- a/layout.c +++ b/layout.c @@ -31,8 +31,9 @@ * a cell which contains a list of cells, and 'leaf' to refer to a cell that * contains a window pane. A leaf is considered to be 'tiled' if it is to be * drawn as a part of the tiled layout. A 'neighbour' is a sibling that is also - * tiled. A cell's 'split' size refers to the side that is shortened when - * splitting it, determined by the parent's type. + * tiled or a node that contains a tiled leaf in a subtree. A cell's 'split' + * size refers to the side that is shortened when splitting it, determined by + * the parent's type. * * Each window has a pointer to the root of its layout tree (containing its * panes), every pane has a pointer back to the cell containing it, and each @@ -296,6 +297,27 @@ layout_cell_is_first_tiled(struct layout_cell *lc) return (lcchild == lc); } +static struct layout_cell * +layout_cell_get_first_tiled(struct layout_cell *lc) +{ + struct layout_cell *lcchild, *lcchild2; + + if (layout_cell_is_tiled(lc)) + return (lc); + if (lc->type == LAYOUT_WINDOWPANE) + return (NULL); + + TAILQ_FOREACH(lcchild, &lc->cells, entry) { + if (layout_cell_is_tiled(lcchild)) + return (lcchild); + if (lcchild->type != LAYOUT_WINDOWPANE) { + lcchild2 = layout_cell_get_first_tiled(lcchild); + if (lcchild2 != NULL) + return (lcchild2); + } + } + return (NULL); +} /* Fix cell offsets for a child cell. */ static void @@ -609,6 +631,20 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, } } +/* Resizes a cell to a specified size */ +void +layout_resize_set_size(struct window *w, struct layout_cell *lc, + enum layout_type type, u_int size) +{ + int change; + + if (type == LAYOUT_LEFTRIGHT) + change = size - lc->sx; + else + change = size - lc->sy; + layout_resize_adjust(w, lc, type, change); +} + /* 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) @@ -1210,6 +1246,75 @@ layout_replace_with_node(struct window *w, struct layout_cell *lc, return (lcparent); } +/* Checks if there is enough space for two new panes. */ +int +layout_split_check_space(struct window_pane *wp, struct layout_cell *lc, + enum layout_type type) +{ + struct style *sb_style = &wp->scrollbar_style; + u_int minimum, sx = lc->sx, sy = lc->sy; + int scrollbars, status; + + if (lc->flags & LAYOUT_CELL_FLOATING) + fatalx("floating cells cannot be split"); + + status = window_get_pane_status(wp->window); + scrollbars = options_get_number(wp->window->options, "pane-scrollbars"); + + switch (type) { + case LAYOUT_LEFTRIGHT: + if (scrollbars) { + minimum = PANE_MINIMUM * 2 + sb_style->width + + sb_style->pad; + } else + minimum = PANE_MINIMUM * 2 + 1; + if (sx < minimum) + return (0); + break; + case LAYOUT_TOPBOTTOM: + if (layout_add_horizontal_border(wp->window, lc, status)) + minimum = PANE_MINIMUM * 2 + 2; + else + minimum = PANE_MINIMUM * 2 + 1; + if (sy < minimum) + return (0); + break; + default: + fatalx("bad layout type"); + } + + return (1); +} + +/* Calculates the new cell sizes when splitting a pane. */ +void +layout_split_sizes(struct layout_cell *lc, int size, int before, + enum layout_type type, u_int *size1, u_int *size2, u_int *saved_size) +{ + u_int s1, s2, ss; + u_int sx = lc->sx, sy = lc->sy; + + if (type == LAYOUT_LEFTRIGHT) + ss = sx; + else + ss = sy; + if (size < 0) + s2 = ((ss + 1) / 2) - 1; + else if (before) + s2 = ss - size - 1; + else + s2 = size; + if (s2 < PANE_MINIMUM) + s2 = PANE_MINIMUM; + else if (s2 > sx - 2) + s2 = ss - 2; + s1 = ss - 1 - s2; + + *size1 = s1; + *size2 = s2; + *saved_size = ss; +} + /* * Split a pane into two. size is a hint, or -1 for default half/half * split. This must be followed by layout_assign_pane before much else happens! @@ -1219,11 +1324,10 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, int flags) { struct layout_cell *lc, *lcparent, *lcnew, *lc1, *lc2; - struct style *sb_style = &wp->scrollbar_style; - u_int sx, sy, xoff, yoff, size1, size2, minimum; + u_int sx, sy, xoff, yoff, size1, size2; u_int new_size, saved_size, resize_first = 0; - int full_size = (flags & SPAWN_FULLSIZE), status; - int scrollbars; + int full_size = (flags & SPAWN_FULLSIZE); + int before = (flags & SPAWN_BEFORE); /* * If full_size is specified, add a new cell at the top of the window @@ -1233,8 +1337,6 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, lc = wp->window->layout_root; else lc = wp->layout_cell; - status = window_get_pane_status(wp->window); - scrollbars = options_get_number(wp->window->options, "pane-scrollbars"); /* Copy the old cell size. */ sx = lc->sx; @@ -1243,47 +1345,14 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, yoff = lc->yoff; /* Check there is enough space for the two new panes. */ - switch (type) { - case LAYOUT_LEFTRIGHT: - if (scrollbars) { - minimum = PANE_MINIMUM * 2 + sb_style->width + - sb_style->pad; - } else - minimum = PANE_MINIMUM * 2 + 1; - if (sx < minimum) - return (NULL); - break; - case LAYOUT_TOPBOTTOM: - if (layout_add_horizontal_border(wp->window, lc, status)) - minimum = PANE_MINIMUM * 2 + 2; - else - minimum = PANE_MINIMUM * 2 + 1; - if (sy < minimum) - return (NULL); - break; - default: - fatalx("bad layout type"); - } + if (!layout_split_check_space(wp, lc, type)) + return (NULL); /* * Calculate new cell sizes. size is the target size or -1 for middle * split, size1 is the size of the top/left and size2 the bottom/right. */ - if (type == LAYOUT_LEFTRIGHT) - saved_size = sx; - else - saved_size = sy; - if (size < 0) - size2 = ((saved_size + 1) / 2) - 1; - else if (flags & SPAWN_BEFORE) - size2 = saved_size - size - 1; - else - size2 = size; - if (size2 < PANE_MINIMUM) - size2 = PANE_MINIMUM; - else if (size2 > saved_size - 2) - size2 = saved_size - 2; - size1 = saved_size - 1 - size2; + layout_split_sizes(lc, size, before, type, &size1, &size2, &saved_size); /* Which size are we using? */ if (flags & SPAWN_BEFORE) @@ -1729,3 +1798,67 @@ layout_remove_tile(struct window *w, struct layout_cell *lc) layout_set_size(lc, 0, 0, 0, 0); return (1); } + +/* + * Inserts a cell back into the tiled layout by taking half the space from its + * nearest neighbour. + */ +int +layout_insert_tile(struct window *w, struct layout_cell *lc) +{ + struct layout_cell *lcneighbour, *lctiled, *lcparent; + enum layout_type type; + u_int size1, size2, saved_size; + + if (lc == NULL) + fatalx("layout cell cannot be null when tiling"); + + lcparent = lc->parent; + if (lc->flags & LAYOUT_CELL_FLOATING) + return (1); + + if (lcparent == NULL) { + /* Only pane in the layout. */ + layout_set_size(lc, w->sx, w->sy, 0, 0); + return (1); + } + + type = lcparent->type; + lcneighbour = layout_cell_get_neighbour(lc); + if (lcneighbour == NULL) { + /* + * This will become the only visible cell in the parent. + * Tile the parent, then set the child's 'split' size. + */ + layout_insert_tile(w, lcparent); + if (type == LAYOUT_LEFTRIGHT) + size1 = lcparent->sx; + else + size1 = lcparent->sy; + layout_resize_set_size(w, lc, type, size1); + } else { + /* + * If the neighbour is a node, a tiled child in the subtree of + * the neighbour is needed to check for space. + */ + lctiled = layout_cell_get_first_tiled(lcneighbour); + if (!layout_split_check_space(lctiled->wp, lcneighbour, type)) + return (0); + layout_split_sizes(lcneighbour, -1, 0, type, &size1, &size2, + &saved_size); + layout_resize_set_size(w, lc, type, size1); + layout_resize_set_size(w, lcneighbour, type, size2); + } + + /* Setting opposite of the 'split' size to that of the parent. */ + if (lcparent->type == LAYOUT_LEFTRIGHT) { + size1 = lcparent->sy; + type = LAYOUT_TOPBOTTOM; + } else { + size1 = lcparent->sx; + type = LAYOUT_LEFTRIGHT; + } + layout_resize_set_size(w, lc, type, size1); + + return (1); +} diff --git a/tmux.h b/tmux.h index b03525c7e..869936e68 100644 --- a/tmux.h +++ b/tmux.h @@ -3629,6 +3629,8 @@ 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); +void layout_resize_set_size(struct window *, struct layout_cell *, + enum layout_type, u_int); struct layout_cell *layout_cell_get_neighbour(struct layout_cell *); void layout_init(struct window *, struct window_pane *); void layout_free(struct window *); @@ -3643,6 +3645,10 @@ int layout_resize_floating_pane_to(struct window_pane *, enum layout_type, u_int, char **); void layout_assign_pane(struct layout_cell *, struct window_pane *, int); +int layout_split_check_space(struct window_pane *, + struct layout_cell *, enum layout_type); +void layout_split_sizes(struct layout_cell *, int, int, + enum layout_type, u_int *, u_int *, u_int *); struct layout_cell *layout_replace_with_node(struct window *, struct layout_cell *, enum layout_type); struct layout_cell *layout_split_pane(struct window_pane *, enum layout_type, @@ -3661,6 +3667,7 @@ int layout_floating_args_parse(struct cmdq_item *, struct args *, enum pane_lines, struct window *, u_int *, u_int *, int *, int *, char **); int layout_remove_tile(struct window *, struct layout_cell *); +int layout_insert_tile(struct window *, struct layout_cell *); /* layout-custom.c */ char *layout_dump(struct window *, struct layout_cell *); From 79f0f97495ff9a53b14cec8b058312c3f506ea67 Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 27 Jun 2026 10:23:14 +0000 Subject: [PATCH 05/14] Make split-window create a new floating pane if used in a floating pane, rather than just complaining. Also improve documentation. From Dane Jensen. --- cmd-split-window.c | 2 +- tmux.1 | 219 ++++++++++++++++++++++++++------------------- 2 files changed, 128 insertions(+), 93 deletions(-) diff --git a/cmd-split-window.c b/cmd-split-window.c index 5d21895c6..6c2c61252 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -96,7 +96,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (cmd_get_entry(self) == &cmd_new_pane_entry) is_floating = !args_has(args, 'L'); else - is_floating = 0; + is_floating = window_pane_is_floating(wp); flags = is_floating ? SPAWN_FLOATING : 0; if (args_has(args, 'b')) diff --git a/tmux.1 b/tmux.1 index a0ec81784..63809bac1 100644 --- a/tmux.1 +++ b/tmux.1 @@ -2742,6 +2742,15 @@ and .Fl Y options set the position of the upper-left corner of the pane. If omitted, new floating panes are cascaded from the top-left of the window. +The +.Fl x , +.Fl y , +.Fl X , +and +.Fl Y +options may be followed by +.Ql % +to specify a percentage of the window size. .Pp If the pane had previously been floating, the position and sizes are restored from the saved values not specified by the @@ -3586,7 +3595,7 @@ but a different format may be specified with .Fl F . .Tg newp .It Xo Ic new\-pane -.Op Fl bdefhIkPvWZ +.Op Fl bdefhIkLPvWZ .Op Fl B Ar border\-lines .Op Fl c Ar start\-directory .Op Fl e Ar environment @@ -3599,101 +3608,49 @@ but a different format may be specified with .Op Fl S Ar active\-border\-style .Op Fl t Ar target\-pane .Op Fl T Ar title +.Op Fl x Ar width +.Op Fl y Ar height +.Op Fl X Ar x-position +.Op Fl Y Ar y-position .Op Ar shell\-command Op Ar argument ... .Xc .D1 Pq alias: Ic newp -Create a new pane. -The new pane is created by splitting -.Ar target\-pane . -If -.Fl d -is given, the session does not make the new pane the current pane. -.Fl Z -zooms if the window is not zoomed, or keeps it zoomed if already zoomed. -.Fl s -sets the style for the pane content. -.Fl S -sets the border style when the pane is active and -.Fl R -sets the border style when the pane is inactive (see -.Sx STYLES ) . -.Fl T -sets the pane title. +Creates a new floating pane. +The +.Fl x +and +.Fl y +options set the width and height of the floating pane in columns and lines +respectively. +The default is half the window width and a quarter the window height. +The +.Fl X +and +.Fl Y +options set the position of the upper-left corner of the pane. +If omitted, new floating panes are cascaded from the top-left of the window. +The +.Fl x , +.Fl y , +.Fl X , +and +.Fl Y +options may be followed by +.Ql % +to specify a percentage of the window size. .Fl B sets the pane border lines for floating panes; see .Ic pane\-border\-lines . .Pp -.Fl h -does a horizontal split and -.Fl v -a vertical split; if neither is specified, -.Fl v -is assumed. The -.Fl l -option specifies the size of the new pane in lines (for vertical split) or in -columns (for horizontal split); -.Ar size -may be followed by -.Ql % -to specify a percentage of the available space. -.Fl p -is a shorthand option for this. -The -.Fl b -option causes the new pane to be created to the left of or above -.Ar target\-pane . -The -.Fl f -option creates a new pane spanning the full window height (with -.Fl h ) -or full window width (with -.Fl v ) , -instead of splitting the active pane. -.Pp -.Fl k -keeps the pane open after the optional -.Ar shell\-command -exits and waits for a key to be pressed before closing it. -The message shown is controlled by the -.Ic remain\-on\-exit\-format -option. -.Fl m Ar message -is equivalent to -.Fl k -but also sets the -.Ic remain\-on\-exit\-format -option for this pane to -.Ar message . -.Pp -.Fl W -Waits until -.Ar shell\-command -exits, then returns its exit status. -For example: -.Bd -literal -offset indent -$ tmux new-pane -W 'vi afile' -$ echo $? -0 -.Ed -.Pp -.Fl E , -or an empty -.Ar shell\-command , -(\[aq]\[aq]) will create an empty pane with no command running in it; -.Ic display-message -.Fl I -can write to an empty pane. -The -.Fl I -flag will create an empty pane and forward any output from stdin to it. -For example: -.Bd -literal -offset indent -$ make 2>&1|tmux new\-pane \-dI & -.Ed +.Fl L +option makes +.Ic new\-pane +behave like +.Ic split\-window . .Pp All other options have the same meaning as for the -.Ic new\-window +.Ic split\-window command. .Tg nextl .It Ic next\-layout Op Fl t Ar target\-window @@ -4048,14 +4005,92 @@ the command behaves like .Op Ar shell\-command Op Ar argument ... .Xc .D1 Pq alias: Ic splitw -Creates a new pane by splitting +Create a new pane by splitting .Ar target\-pane . -Shares behavior with -.Ic new\-pane . +If +.Fl d +is given, the session does not make the new pane the current pane. +.Fl Z +zooms if the window is not zoomed, or keeps it zoomed if already zoomed. +.Fl s +sets the style for the pane content. +.Fl S +sets the border style when the pane is active and +.Fl R +sets the border style when the pane is inactive (see +.Sx STYLES ) . +.Fl T +sets the pane title. +.Pp +.Fl h +does a horizontal split and +.Fl v +a vertical split; if neither is specified, +.Fl v +is assumed. +The +.Fl l +option specifies the size of the new pane in lines (for vertical split) or in +columns (for horizontal split); +.Ar size +may be followed by +.Ql % +to specify a percentage of the available space. +.Fl p +is a shorthand option for this. +The +.Fl b +option causes the new pane to be created to the left of or above +.Ar target\-pane . +The +.Fl f +option creates a new pane spanning the full window height (with +.Fl h ) +or full window width (with +.Fl v ) , +instead of splitting the active pane. +.Pp +.Fl k +keeps the pane open after the optional +.Ar shell\-command +exits and waits for a key to be pressed before closing it. +The message shown is controlled by the +.Ic remain\-on\-exit\-format +option. +.Fl m Ar message +is equivalent to +.Fl k +but also sets the +.Ic remain\-on\-exit\-format +option for this pane to +.Ar message . +.Pp +.Fl W +Waits until +.Ar shell\-command +exits, then returns its exit status. +For example: +.Bd -literal -offset indent +$ tmux new-pane -W 'vi afile' +$ echo $? +0 +.Ed +.Pp +.Fl E , +or an empty +.Ar shell\-command , +(\[aq]\[aq]) will create an empty pane with no command running in it; +.Ic display-message +.Fl I +can write to an empty pane. +The +.Fl I +flag will create an empty pane and forward any output from stdin to it. +For example: +.Bd -literal -offset indent +$ make 2>&1|tmux new\-pane \-dI & +.Ed .Pp -See -.Ic new\-pane -for more details. .Tg swapp .It Xo Ic swap\-pane .Op Fl dDUZ From e0dd40f812fdbc9a8f7dfa2810d8db3666ce1c66 Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 27 Jun 2026 12:27:43 +0000 Subject: [PATCH 06/14] Clear lines before writing in copy mode to avoid leaving stray text when new line is shorter than old. --- window-copy.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/window-copy.c b/window-copy.c index 4ba791e75..0a37c5e9d 100644 --- a/window-copy.c +++ b/window-copy.c @@ -5154,6 +5154,9 @@ window_copy_write_line(struct window_mode_entry *wme, else content_sx = sx; + screen_write_cursormove(ctx, 0, py, 0); + screen_write_clearline(ctx, 8); + ft = format_create_defaults(NULL, NULL, NULL, NULL, wp); style_apply(&gc, oo, "copy-mode-position-style", ft); From 7cd33ce0dd3d3e8afe50815a237ad14457d343fc Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 27 Jun 2026 12:37:36 +0000 Subject: [PATCH 07/14] Set view name immediately when entering mode. --- window-tree.c | 1 + 1 file changed, 1 insertion(+) diff --git a/window-tree.c b/window-tree.c index 19bd5569f..c37082c91 100644 --- a/window-tree.c +++ b/window-tree.c @@ -1138,6 +1138,7 @@ window_tree_init(struct window_mode_entry *wme, struct cmd_find_state *fs, window_tree_get_key, window_tree_swap, window_tree_sort, window_tree_help, data, window_tree_menu_items, &s); mode_tree_zoom(data->data, args); + mode_tree_view_name(data->data, "preview"); mode_tree_build(data->data); mode_tree_draw(data->data); From b0db912bde8f77eefa112e00385e6fee8fdbd447 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 28 Jun 2026 16:52:28 +0100 Subject: [PATCH 08/14] Update check-names test. --- regress/check-names.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/regress/check-names.sh b/regress/check-names.sh index 104ff01b8..21e185ac1 100644 --- a/regress/check-names.sh +++ b/regress/check-names.sh @@ -110,20 +110,26 @@ must_fail $TMUX set-buffer -b "bad${invalid}name" data # Titles set by commands allow '#', ':' and '.'. $TMUX select-pane -T 'title#:.ok' || fail "command title rejected" must_equal "$($TMUX display-message -p '#{pane_title}')" 'title#:.ok' +$TMUX send-keys "printf '\\033]2;title#[fg=red]ok\\007'" Enter || exit 1 +sleep 1 +must_equal "$($TMUX display-message -p '#{pane_title}')" 'title#[fg=red]ok' +$TMUX send-keys "printf '\\033]2;title#(bad)\\007'" Enter || exit 1 +sleep 1 +must_equal "$($TMUX display-message -p '#{pane_title}')" 'title_(bad)' # Buffer names allow '#', ':' and '.'. $TMUX set-buffer -b 'buffer#:.ok' data || fail "buffer name rejected" must_equal "$($TMUX list-buffers -F '#{buffer_name}')" 'buffer#:.ok' -# Window names from escape sequences reject '#', ':' and '.' by cleaning them. +# Window names from escape sequences allow '#' except in '#('. $TMUX send-keys "printf '\\033kescape#:.ok\\033\\\\'" Enter || exit 1 sleep 1 -must_equal "$($TMUX display-message -p '#{window_name}')" 'escape___ok' +must_equal "$($TMUX display-message -p '#{window_name}')" 'escape#__ok' # Titles from escape sequences reject only '#'. $TMUX send-keys "printf '\\033]2;escape#:.ok\\007'" Enter || exit 1 sleep 1 -must_equal "$($TMUX display-message -p '#{pane_title}')" 'escape_:.ok' +must_equal "$($TMUX display-message -p '#{pane_title}')" 'escape#:.ok' # Invalid UTF-8 from escape sequences is ignored. $TMUX rename-window 'before-invalid' || exit 1 From 96899dc527999ee9130246eec86d76d79b1a8d97 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 28 Jun 2026 15:53:18 +0000 Subject: [PATCH 09/14] 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 bc018da88..7236360bb 100644 --- a/tmux.c +++ b/tmux.c @@ -295,7 +295,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 2dc704abb6f6b72e3a5d452c821e53084ddcae2c Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 28 Jun 2026 17:01:52 +0100 Subject: [PATCH 10/14] Always set SIXEL aspect ratio to 1:1, GitHub issue 5291 from James Holderness. --- image-sixel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image-sixel.c b/image-sixel.c index 2e6f247d2..a004d78f8 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -586,7 +586,7 @@ sixel_print(struct sixel_image *si, struct sixel_image *map, size_t *size) len = 8192; buf = xmalloc(len); - tmplen = xsnprintf(tmp, sizeof tmp, "\033P0;%uq", si->p2); + tmplen = xsnprintf(tmp, sizeof tmp, "\033P9;%uq", si->p2); sixel_print_add(&buf, &len, &used, tmp, tmplen); if (si->set_ra) { From c29b41e5279f246669aae32f0c15c20cfa82c3c6 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 29 Jun 2026 07:45:09 +0000 Subject: [PATCH 11/14] Make pane scrollbars able to auto-hide after a short timeout. This replaces the previous "modal" behaviour where the pane would be resized, which tended to make a mess. Instead, the scrollbar appears when scrolling or when hovered over with the mouse and disappears a (configurable) short period later. From Michael Grant. --- cmd-resize-pane.c | 4 +- layout.c | 6 +-- options-table.c | 14 ++++- options.c | 5 ++ regsub.c | 3 +- screen-redraw.c | 41 ++++++++++---- screen-write.c | 6 ++- server-client.c | 123 +++++++++++++++++++++++++++++++++++++++--- sort.c | 4 +- tmux.1 | 35 ++++++++---- tmux.h | 12 +++++ window-copy.c | 44 +++++++++++---- window-visible.c | 2 +- window.c | 133 +++++++++++++++++++++++++++++++++++++++++++++- 14 files changed, 379 insertions(+), 53 deletions(-) diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index 734280af1..185844f2e 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -251,10 +251,10 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c, 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) && + if (window_pane_scrollbar_reserve(wp, scrollbars) && sb_pos == PANE_SCROLLBARS_LEFT) { left -= wp->scrollbar_style.width + wp->scrollbar_style.pad; - } else if (window_pane_show_scrollbar(wp, scrollbars) && + } else if (window_pane_scrollbar_reserve(wp, scrollbars) && sb_pos == PANE_SCROLLBARS_RIGHT) { right += wp->scrollbar_style.width + wp->scrollbar_style.pad; } diff --git a/layout.c b/layout.c index 279f504af..a65cd9d86 100644 --- a/layout.c +++ b/layout.c @@ -462,7 +462,7 @@ layout_fix_panes(struct window *w, struct window_pane *skip) sy--; } - if (window_pane_show_scrollbar(wp, scrollbars)) { + if (window_pane_scrollbar_reserve(wp, scrollbars)) { sb_w = wp->scrollbar_style.width; sb_pad = wp->scrollbar_style.pad; if (sb_w < 1) @@ -535,7 +535,7 @@ layout_resize_check(struct window *w, struct layout_cell *lc, /* Space available in this cell only. */ if (type == LAYOUT_LEFTRIGHT) { available = lc->sx; - if (scrollbars) + if (scrollbars == PANE_SCROLLBARS_ALWAYS) minimum = PANE_MINIMUM + sb_style->width + sb_style->pad; else @@ -1263,7 +1263,7 @@ layout_split_check_space(struct window_pane *wp, struct layout_cell *lc, switch (type) { case LAYOUT_LEFTRIGHT: - if (scrollbars) { + if (scrollbars == PANE_SCROLLBARS_ALWAYS) { minimum = PANE_MINIMUM * 2 + sb_style->width + sb_style->pad; } else diff --git a/options-table.c b/options-table.c index ddd8b4634..5019bb244 100644 --- a/options-table.c +++ b/options-table.c @@ -65,7 +65,7 @@ static const char *options_table_cursor_style_list[] = { "blinking-bar", "bar", NULL }; static const char *options_table_pane_scrollbars_list[] = { - "off", "modal", "on", NULL + "off", "modal", "on", "auto-hide", NULL }; static const char *options_table_pane_scrollbars_position_list[] = { "right", "left", NULL @@ -1595,7 +1595,17 @@ const struct options_table_entry options_table[] = { .scope = OPTIONS_TABLE_WINDOW, .choices = options_table_pane_scrollbars_list, .default_num = PANE_SCROLLBARS_OFF, - .text = "Pane scrollbar state." + .text = "Pane scrollbar state: off, on, modal, or auto-hide." + }, + + { .name = "pane-scrollbars-timeout", + .type = OPTIONS_TABLE_NUMBER, + .scope = OPTIONS_TABLE_WINDOW, + .minimum = 0, + .maximum = INT_MAX, + .default_num = 500, + .unit = "milliseconds", + .text = "Time before modal and auto-hide pane scrollbars disappear." }, { .name = "pane-scrollbars-style", diff --git a/options.c b/options.c index db0f221a3..e05930463 100644 --- a/options.c +++ b/options.c @@ -1264,6 +1264,7 @@ options_push_changes(const char *name) strcmp(name, "pane-border-lines") == 0 || strcmp(name, "pane-border-status") == 0 || strcmp(name, "pane-scrollbars") == 0 || + strcmp(name, "pane-scrollbars-timeout") == 0 || strcmp(name, "pane-scrollbars-position") == 0 || strcmp(name, "pane-scrollbars-style") == 0) redraw_invalidate_all_scenes(); @@ -1288,6 +1289,10 @@ options_push_changes(const char *name) RB_FOREACH(w, windows, &windows) layout_fix_panes(w, NULL); } + if (strcmp(name, "pane-scrollbars") == 0) { + RB_FOREACH(wp, window_pane_tree, &all_window_panes) + window_pane_scrollbar_hide(wp); + } if (strcmp(name, "pane-scrollbars-style") == 0) { RB_FOREACH(wp, window_pane_tree, &all_window_panes) { style_set_scrollbar_style_from_option( diff --git a/regsub.c b/regsub.c index a863f9fd8..62d750d8a 100644 --- a/regsub.c +++ b/regsub.c @@ -24,7 +24,8 @@ #include "tmux.h" static void -regsub_copy(char **buf, ssize_t *len, const char *text, size_t start, size_t end) +regsub_copy(char **buf, ssize_t *len, const char *text, size_t start, + size_t end) { size_t add = end - start; diff --git a/screen-redraw.c b/screen-redraw.c index e8dd8ec0f..813d4e563 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -81,6 +81,7 @@ enum redraw_span_type { #define REDRAW_BORDER_IS_ARROW 0x1 #define REDRAW_SCROLLBAR_LEFT 0x2 #define REDRAW_SCROLLBAR_RIGHT 0x4 +#define REDRAW_SCROLLBAR_OVERLAY 0x8 /* Draw operations. */ #define REDRAW_PANE 0x1 @@ -447,7 +448,7 @@ redraw_mark_pane_inside(struct redraw_build_ctx *bctx, struct window_pane *wp) /* Mark scrollbar data. */ static void redraw_mark_pane_scrollbar(struct redraw_build_ctx *bctx, - struct window_pane *wp, int sb_w, int sb_left) + struct window_pane *wp, int sb_w, int sb_left, int overlay) { struct redraw_build_cell *bc; u_int x, y; @@ -457,7 +458,13 @@ redraw_mark_pane_scrollbar(struct redraw_build_ctx *bctx, if (sb_w == 0) return; - if (sb_left) { + if (overlay && sb_left) { + sx = wp->xoff; + ex = sx + sb_w - 1; + } else if (overlay) { + ex = wp->xoff + (int)wp->sx - 1; + sx = ex - sb_w + 1; + } else if (sb_left) { sx = wp->xoff - sb_w; ex = wp->xoff - 1; } else { @@ -481,6 +488,8 @@ redraw_mark_pane_scrollbar(struct redraw_build_ctx *bctx, bc->data.sb.flags |= REDRAW_SCROLLBAR_LEFT; else bc->data.sb.flags |= REDRAW_SCROLLBAR_RIGHT; + if (overlay) + bc->data.sb.flags |= REDRAW_SCROLLBAR_OVERLAY; } } } @@ -755,19 +764,30 @@ redraw_mark_pane_borders(struct redraw_build_ctx *bctx, struct window_pane *wp, static void redraw_mark_pane(struct redraw_build_ctx *bctx, struct window_pane *wp) { - int sb_w = 0, sb_left = 0; + int sb_w = 0, sb_left = 0, overlay = 0; if (!window_pane_is_visible(wp)) return; - if (window_pane_show_scrollbar(wp, bctx->sb)) - sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; + if (window_pane_scrollbar_visible(wp, bctx->sb)) { + overlay = window_pane_scrollbar_overlay(wp, bctx->sb); + if (overlay) { + sb_w = wp->scrollbar_style.width + + wp->scrollbar_style.pad; + if (sb_w > (int)wp->sx) { + sb_w = wp->scrollbar_style.width; + if (sb_w > (int)wp->sx) + sb_w = wp->sx; + } + } else + sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; + } if (sb_w != 0 && bctx->sbp == PANE_SCROLLBARS_LEFT) sb_left = 1; redraw_mark_pane_inside(bctx, wp); - redraw_mark_pane_borders(bctx, wp, sb_w, sb_left); - redraw_mark_pane_scrollbar(bctx, wp, sb_w, sb_left); + redraw_mark_pane_borders(bctx, wp, overlay ? 0 : sb_w, sb_left); + redraw_mark_pane_scrollbar(bctx, wp, sb_w, sb_left, overlay); } /* Choose the pane that will provide the border style for two-pane layouts. */ @@ -1220,7 +1240,7 @@ redraw_draw_scrollbar_span(struct redraw_draw_ctx *dctx, struct screen *s = wp->screen; struct tty *tty = &scene->c->tty; struct style *sb_style = &wp->scrollbar_style; - struct grid_cell gc, slgc, *gcp; + struct grid_cell gc, slgc, pad_gc, *gcp; double pct_view; u_int total_height, slider_h, slider_y; u_int sb_h = span->data.sb.height; @@ -1260,6 +1280,7 @@ redraw_draw_scrollbar_span(struct redraw_draw_ctx *dctx, memcpy(&slgc, &gc, sizeof slgc); slgc.fg = gc.bg; slgc.bg = gc.fg; + tty_default_colours(&pad_gc, wp, NULL); sb_w = sb_style->width; sb_pad = sb_style->pad; @@ -1269,12 +1290,12 @@ redraw_draw_scrollbar_span(struct redraw_draw_ctx *dctx, for (i = 0; i < n; i++) { if (span->data.sb.flags & REDRAW_SCROLLBAR_LEFT) { if (off + i >= sb_w && off + i < sb_w + sb_pad) { - tty_cell(tty, &grid_default_cell, NULL); + tty_cell(tty, &pad_gc, NULL); continue; } } else { if (off + i < sb_pad) { - tty_cell(tty, &grid_default_cell, NULL); + tty_cell(tty, &pad_gc, NULL); continue; } } diff --git a/screen-write.c b/screen-write.c index ba7ffd930..31d177ef0 100644 --- a/screen-write.c +++ b/screen-write.c @@ -2096,6 +2096,10 @@ screen_write_collect_flush_scrolled(struct screen_write_ctx *ctx) screen_write_redraw_pane(ctx, &ttyctx); return 0; } + if (wp != NULL && window_pane_scrollbar_overlay_visible(wp)) { + wp->flags |= PANE_REDRAW; + return 0; + } log_debug("%s: scrolled %u (region %u-%u)", __func__, ctx->scrolled, s->rupper, s->rlower); @@ -2109,7 +2113,7 @@ screen_write_collect_flush_scrolled(struct screen_write_ctx *ctx) tty_write(tty_cmd_scrollup, &ttyctx); if (wp != NULL) - wp->flags |= PANE_REDRAWSCROLLBAR; + window_pane_scrollbar_redraw(wp); return 1; } diff --git a/server-client.c b/server-client.c index 5b2651658..43bf7600d 100644 --- a/server-client.c +++ b/server-client.c @@ -52,6 +52,8 @@ static void server_client_dispatch(struct imsg *, void *); static int server_client_dispatch_command(struct client *, struct imsg *); static int server_client_dispatch_identify(struct client *, struct imsg *); static int server_client_dispatch_shell(struct client *); +static void server_client_update_scrollbar_hover(struct client *, int, int, + int); static void server_client_report_theme(struct client *, enum client_theme); /* Compare client windows. */ @@ -605,6 +607,60 @@ server_client_exec(struct client *c, const char *cmd) free(msg); } +/* Is this point inside the auto-hide scrollbar interaction area? */ +static int +server_client_in_scrollbar_area(struct window_pane *wp, int px, int py) +{ + struct window *w = wp->window; + u_int width, pad, total; + int sb, sb_pos, start, end; + + sb = options_get_number(w->options, "pane-scrollbars"); + if (!window_pane_scrollbar_overlay(wp, sb)) + return (0); + if (py < wp->yoff || py >= wp->yoff + (int)wp->sy) + return (0); + + width = wp->scrollbar_style.width; + pad = wp->scrollbar_style.pad; + total = width + pad; + if (total == 0 || total > wp->sx) + total = wp->sx; + + sb_pos = options_get_number(w->options, "pane-scrollbars-position"); + if (sb_pos == PANE_SCROLLBARS_LEFT) { + start = wp->xoff; + end = wp->xoff + (int)total - 1; + } else { + end = wp->xoff + (int)wp->sx - 1; + start = end - (int)total + 1; + } + return (px >= start && px <= end); +} + +/* Update auto-hide scrollbars for a mouse movement. */ +static void +server_client_update_scrollbar_hover(struct client *c, int type, int px, int py) +{ + struct window *w = c->session->curw->window; + struct window_pane *wp; + + if (type != KEYC_TYPE_MOUSEMOVE) + return; + + TAILQ_FOREACH(wp, &w->panes, entry) { + if (!window_pane_is_visible(wp)) + continue; + if (server_client_in_scrollbar_area(wp, px, py)) { + wp->sb_auto_hover = 1; + window_pane_scrollbar_show(wp, 1); + } else { + wp->sb_auto_hover = 0; + window_pane_scrollbar_start_timer(wp); + } + } +} + /* Is the mouse inside a pane? */ static enum key_code_mouse_location server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, @@ -615,14 +671,18 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, int pane_status, sb, sb_pos, sb_w, sb_pad; int pane_status_line, sl_top, sl_bottom; int bdr_bottom, bdr_top, bdr_left, bdr_right; + int sb_start, sb_end, sb_overlay; sb = options_get_number(w->options, "pane-scrollbars"); sb_pos = options_get_number(w->options, "pane-scrollbars-position"); pane_status = window_pane_get_pane_status(wp); + sb_overlay = window_pane_scrollbar_overlay(wp, sb); - if (window_pane_show_scrollbar(wp, sb)) { + if (window_pane_scrollbar_visible(wp, sb)) { sb_w = wp->scrollbar_style.width; sb_pad = wp->scrollbar_style.pad; + if (sb_overlay && sb_w > (int)wp->sx) + sb_w = wp->sx; } else { sb_w = 0; sb_pad = 0; @@ -635,9 +695,34 @@ 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 (!sb_overlay && sb_pos == PANE_SCROLLBARS_LEFT) bdr_left -= sb_pad + sb_w; + if (sb_overlay && sb_w != 0 && + py >= wp->yoff && py < wp->yoff + (int)wp->sy && + px >= wp->xoff && px < wp->xoff + (int)wp->sx) { + if (sb_pos == PANE_SCROLLBARS_LEFT) { + sb_start = wp->xoff; + sb_end = sb_start + sb_w - 1; + } else { + sb_end = wp->xoff + (int)wp->sx - 1; + sb_start = sb_end - sb_w + 1; + } + if (px >= sb_start && px <= sb_end) { + sl_top = wp->yoff + wp->sb_slider_y; + sl_bottom = (wp->yoff + wp->sb_slider_y + + wp->sb_slider_h - 1); + if (py < sl_top) + return (KEYC_MOUSE_LOCATION_SCROLLBAR_UP); + else if (py >= sl_top && py <= sl_bottom) { + *sl_mpos = (py - wp->sb_slider_y - wp->yoff); + return (KEYC_MOUSE_LOCATION_SCROLLBAR_SLIDER); + } else + return (KEYC_MOUSE_LOCATION_SCROLLBAR_DOWN); + } + return (KEYC_MOUSE_LOCATION_PANE); + } + /* Check if point is within the pane or scrollbar. */ if (((pane_status != PANE_STATUS_OFF && py != pane_status_line && py != wp->yoff + (int)wp->sy) || @@ -685,7 +770,7 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, if (window_pane_is_floating(fwp) && window_pane_get_pane_lines(fwp) == PANE_LINES_NONE) continue; - if (window_pane_show_scrollbar(fwp, sb)) { + if (window_pane_scrollbar_reserve(fwp, sb)) { sb_w = fwp->scrollbar_style.width; sb_pad = fwp->scrollbar_style.pad; } else { @@ -915,10 +1000,14 @@ have_event: tty_window_offset(&c->tty, &m->ox, &m->oy, &sx, &sy); log_debug("mouse window @%u at %u,%u (%ux%u)", w->id, m->ox, m->oy, sx, sy); - if (px > sx || py > sy) + if (px > sx || py > sy) { + server_client_update_scrollbar_hover(c, type, + -1, -1); return (KEYC_UNKNOWN); + } px = px + m->ox; py = py + m->oy; + server_client_update_scrollbar_hover(c, type, px, py); if (type == KEYC_TYPE_MOUSEDRAG && lwp != NULL) { /* Use pane from last mouse event. */ @@ -951,7 +1040,8 @@ have_event: m->wp = wp->id; m->w = wp->window->id; } - } + } else + server_client_update_scrollbar_hover(c, type, -1, -1); /* Reset click type or add a click timer if needed. */ if (type == KEYC_TYPE_MOUSEDOWN || @@ -1911,8 +2001,9 @@ server_client_reset_state(struct client *c) struct window_pane *wp = server_client_get_pane(c), *loop; struct screen *s = NULL; struct options *oo = c->session->options; - int mode = 0, cursor, flags, pane_mode = 0; + int mode = 0, cursor, flags, pane_mode = 0, sb; u_int cx = 0, cy = 0, ox, oy, sx, sy, prompt = 0; + u_int sb_w; struct visible_ranges *r; if (c->flags & (CLIENT_CONTROL|CLIENT_SUSPENDED)) @@ -1965,6 +2056,21 @@ 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 && + options_get_number(w->options, + "pane-scrollbars-position") == + PANE_SCROLLBARS_LEFT) { + if (s->cx < 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); } @@ -1992,7 +2098,10 @@ server_client_reset_state(struct client *c) mode |= MODE_MOUSE_ALL; } } - if (options_get_number(oo, "focus-follows-mouse")) + sb = options_get_number(w->options, "pane-scrollbars"); + if (options_get_number(oo, "focus-follows-mouse") || + sb == PANE_SCROLLBARS_MODAL || + sb == PANE_SCROLLBARS_AUTOHIDE) mode |= MODE_MOUSE_ALL; else if (~mode & MODE_MOUSE_ALL) mode |= MODE_MOUSE_BUTTON; diff --git a/sort.c b/sort.c index 212da22ea..c416b8425 100644 --- a/sort.c +++ b/sort.c @@ -26,8 +26,8 @@ static struct sort_criteria *sort_criteria; static void -sort_qsort(void *l, u_int len, u_int size, int (*cmp)(const void *, const void *), - struct sort_criteria *sort_crit) +sort_qsort(void *l, u_int len, u_int size, int (*cmp)(const void *, + const void *), struct sort_criteria *sort_crit) { u_int i; void *tmp, **ll; diff --git a/tmux.1 b/tmux.1 index 63809bac1..8f74205f8 100644 --- a/tmux.1 +++ b/tmux.1 @@ -5893,7 +5893,7 @@ and will fall back to standard ACS line drawing when UTF\-8 is not supported. .Pp .It Xo Ic pane\-scrollbars -.Op Ic off | modal | on +.Op Ic off | modal | on | auto\-hide .Xc When enabled, a character based scrollbar appears on the left or right of each pane. @@ -5903,19 +5903,29 @@ represents the position and size of the visible part of the pane content. .Pp If set to .Ic on -the scrollbar is visible all the time. +the scrollbar is visible all the time and the pane is narrowed by the width of +the scrollbar. If set to .Ic modal -the scrollbar only appears when the pane is in copy mode or view mode. -When the scrollbar is visible, the pane is narrowed by the width of the -scrollbar and the text in the pane is reflowed. +the scrollbar only appears when the pane is in copy mode or view mode and +auto-hides when not in use. If set to -.Ic modal , -the pane is narrowed only when the scrollbar is visible. +.Ic auto\-hide +the scrollbar is available all the time but auto-hides when not in use. +With +.Ic modal +and +.Ic auto\-hide , +the scrollbar overlays the pane and does not narrow or reflow the pane. .Pp See also .Ic pane\-scrollbars\-style . .Pp +.It Xo Ic pane\-scrollbars\-position +.Op Ic left | right +.Xc +Sets which side of the pane to display pane scrollbars on. +.Pp .It Ic pane\-scrollbars\-style Ar style Set the scrollbars style. For how to specify @@ -5932,10 +5942,13 @@ attribute sets the width of the scrollbar and the attribute the padding between the scrollbar and the pane. Other attributes are ignored. .Pp -.It Xo Ic pane\-scrollbars\-position -.Op Ic left | right -.Xc -Sets which side of the pane to display pane scrollbars on. +.It Ic pane\-scrollbars\-timeout Ar time +Set the time in milliseconds before scrollbars are hidden when +.Ic pane\-scrollbars +is +.Ic modal +or +.Ic auto\-hide . .Pp .It Ic pane\-status\-current\-style Ar style Set status line style for the currently active pane. diff --git a/tmux.h b/tmux.h index 869936e68..f34b3b324 100644 --- a/tmux.h +++ b/tmux.h @@ -1266,6 +1266,9 @@ struct window_pane { u_int sb_slider_y; u_int sb_slider_h; + int sb_auto_visible; + int sb_auto_hover; + struct event sb_auto_timer; int argc; char **argv; @@ -1440,6 +1443,7 @@ TAILQ_HEAD(winlink_stack, winlink); #define PANE_SCROLLBARS_OFF 0 #define PANE_SCROLLBARS_MODAL 1 #define PANE_SCROLLBARS_ALWAYS 2 +#define PANE_SCROLLBARS_AUTOHIDE 3 /* Pane scrollbars position option. */ #define PANE_SCROLLBARS_RIGHT 0 @@ -3581,6 +3585,14 @@ 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_scrollbar_reserve(struct window_pane *, int); +int window_pane_scrollbar_visible(struct window_pane *, int); +int window_pane_scrollbar_overlay(struct window_pane *, int); +int window_pane_scrollbar_overlay_visible(struct window_pane *); +void window_pane_scrollbar_show(struct window_pane *, int); +void window_pane_scrollbar_hide(struct window_pane *); +void window_pane_scrollbar_start_timer(struct window_pane *); +void window_pane_scrollbar_redraw(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-copy.c b/window-copy.c index 0a37c5e9d..aedbba99b 100644 --- a/window-copy.c +++ b/window-copy.c @@ -61,8 +61,10 @@ static int window_copy_line_number_mode(struct window_mode_entry *); static int window_copy_line_number_is_absolute(struct window_mode_entry *); static int window_copy_line_numbers_active(struct window_mode_entry *); static u_int window_copy_line_number_width(struct window_mode_entry *); -static u_int window_copy_cursor_offset(struct window_mode_entry *, u_int, u_int); -static u_int window_copy_cursor_unoffset(struct window_mode_entry *, u_int, u_int); +static u_int window_copy_cursor_offset(struct window_mode_entry *, u_int, + u_int); +static u_int window_copy_cursor_unoffset(struct window_mode_entry *, u_int, + u_int); static void window_copy_write_line(struct window_mode_entry *, struct screen_write_ctx *, u_int); static void window_copy_write_lines(struct window_mode_entry *, @@ -860,6 +862,7 @@ window_copy_scroll1(struct window_mode_entry *wme, struct window_pane *wp, if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection(wme, 1, 0); + window_pane_scrollbar_show(wp, 1); window_copy_redraw_screen(wme); } @@ -913,6 +916,7 @@ window_copy_pageup1(struct window_mode_entry *wme, int half_page) if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection(wme, 1, 0); + window_pane_scrollbar_show(wme->wp, 1); window_copy_redraw_screen(wme); } @@ -973,6 +977,7 @@ window_copy_pagedown1(struct window_mode_entry *wme, int half_page, if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection(wme, 1, 0); + window_pane_scrollbar_show(wme->wp, 1); window_copy_redraw_screen(wme); return (0); } @@ -1789,7 +1794,7 @@ window_copy_cmd_history_bottom(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; struct window_copy_mode_data *data = wme->data; struct screen *s = data->backing; - u_int oy; + u_int oy, old_oy = data->oy; oy = screen_hsize(s) + data->cy - data->oy; if (data->lineflag == LINE_SEL_RIGHT_LEFT && oy == data->endsely) @@ -1802,6 +1807,8 @@ window_copy_cmd_history_bottom(struct window_copy_cmd_state *cs) if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection(wme, 1, 0); + if (data->oy != old_oy) + window_pane_scrollbar_show(wme->wp, 1); return (WINDOW_COPY_CMD_REDRAW); } @@ -1810,7 +1817,7 @@ window_copy_cmd_history_top(struct window_copy_cmd_state *cs) { struct window_mode_entry *wme = cs->wme; struct window_copy_mode_data *data = wme->data; - u_int oy; + u_int oy, old_oy = data->oy; oy = screen_hsize(data->backing) + data->cy - data->oy; if (data->lineflag == LINE_SEL_LEFT_RIGHT && oy == data->sely) @@ -1823,6 +1830,8 @@ window_copy_cmd_history_top(struct window_copy_cmd_state *cs) if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection(wme, 1, 0); + if (data->oy != old_oy) + window_pane_scrollbar_show(wme->wp, 1); return (WINDOW_COPY_CMD_REDRAW); } @@ -3772,7 +3781,7 @@ window_copy_scroll_to(struct window_mode_entry *wme, u_int px, u_int py, { struct window_copy_mode_data *data = wme->data; struct grid *gd = data->backing->grid; - u_int offset, gap; + u_int offset, gap, old_oy = data->oy; data->cx = px; @@ -3796,6 +3805,8 @@ window_copy_scroll_to(struct window_mode_entry *wme, u_int px, u_int py, if (!no_redraw && data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection(wme, 1, 0); + if (data->oy != old_oy) + window_pane_scrollbar_show(wme->wp, 1); if (!no_redraw) window_copy_redraw_screen(wme); } @@ -5278,7 +5289,10 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) return; } - screen_write_start_pane(&ctx, wp, NULL); + if (window_pane_scrollbar_overlay_visible(wp)) + screen_write_start(&ctx, &data->screen); + else + screen_write_start_pane(&ctx, wp, NULL); for (i = py; i < py + ny; i++) window_copy_write_line(wme, &ctx, i); screen_write_cursormove(&ctx, @@ -5286,7 +5300,7 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) 0); screen_write_stop(&ctx); - wp->flags |= PANE_REDRAWSCROLLBAR; + window_pane_scrollbar_redraw(wp); } static void @@ -6542,6 +6556,7 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny) if (ny == 0) return; data->oy -= ny; + window_pane_scrollbar_show(wp, 1); if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); @@ -6573,7 +6588,10 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny) return; } - screen_write_start_pane(&ctx, wp, NULL); + if (window_pane_scrollbar_overlay_visible(wp)) + screen_write_start(&ctx, &data->screen); + else + screen_write_start_pane(&ctx, wp, NULL); screen_write_cursormove(&ctx, 0, 0, 0); screen_write_deleteline(&ctx, ny, 8); window_copy_write_lines(wme, &ctx, screen_size_y(s) - ny, ny); @@ -6588,7 +6606,7 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny) window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); screen_write_stop(&ctx); - wp->flags |= PANE_REDRAWSCROLLBAR; + window_pane_scrollbar_redraw(wp); } static void @@ -6607,6 +6625,7 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) if (ny == 0) return; data->oy += ny; + window_pane_scrollbar_show(wp, 1); if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); @@ -6633,7 +6652,10 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) return; } - screen_write_start_pane(&ctx, wp, NULL); + if (window_pane_scrollbar_overlay_visible(wp)) + screen_write_start(&ctx, &data->screen); + else + screen_write_start_pane(&ctx, wp, NULL); screen_write_cursormove(&ctx, 0, 0, 0); screen_write_insertline(&ctx, ny, 8); window_copy_write_lines(wme, &ctx, 0, ny); @@ -6644,7 +6666,7 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); screen_write_stop(&ctx); - wp->flags |= PANE_REDRAWSCROLLBAR; + window_pane_scrollbar_redraw(wp); } static void diff --git a/window-visible.c b/window-visible.c index f5b034475..7ec7fcfb5 100644 --- a/window-visible.c +++ b/window-visible.c @@ -126,7 +126,7 @@ window_visible_ranges(struct window_pane *base_wp, int px, int py, u_int width, continue; sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; - if (!window_pane_show_scrollbar(wp, sb)) + if (!window_pane_scrollbar_reserve(wp, sb)) sb_w = sb_pos = 0; for (i = 0; i < r->used; i++) { diff --git a/window.c b/window.c index 1965685b6..a3c228039 100644 --- a/window.c +++ b/window.c @@ -73,6 +73,7 @@ struct window_pane_input_data { static struct window_pane *window_pane_create(struct window *, u_int, u_int, u_int); static void window_pane_destroy(struct window_pane *); +static void window_pane_scrollbar_timer(int, short, void *); static void window_pane_full_size_offset(struct window_pane *wp, int *xoff, int *yoff, u_int *sx, u_int *sy); @@ -1105,6 +1106,7 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit) screen_init(&wp->status_screen, 1, 1, 0); style_ranges_init(&wp->border_status_line.ranges); + evtimer_set(&wp->sb_auto_timer, window_pane_scrollbar_timer, wp); if (gethostname(host, sizeof host) == 0) screen_set_title(&wp->base, host, 0); @@ -1151,6 +1153,52 @@ window_pane_free_modes(struct window_pane *wp) wp->screen = &wp->base; } +static void +window_pane_scrollbar_timer(__unused int fd, __unused short events, void *arg) +{ + struct window_pane *wp = arg; + + wp->sb_auto_hover = 0; + window_pane_scrollbar_hide(wp); +} + +static int +window_pane_scrollbar_auto_hide(struct window_pane *wp) +{ + int sb; + + sb = options_get_number(wp->window->options, "pane-scrollbars"); + return (sb == PANE_SCROLLBARS_MODAL || sb == PANE_SCROLLBARS_AUTOHIDE); +} + +int +window_pane_scrollbar_overlay_visible(struct window_pane *wp) +{ + int sb; + + sb = options_get_number(wp->window->options, "pane-scrollbars"); + return (window_pane_scrollbar_overlay(wp, sb) && + window_pane_scrollbar_visible(wp, sb)); +} + +void +window_pane_scrollbar_redraw(struct window_pane *wp) +{ + if (window_pane_scrollbar_overlay_visible(wp)) { + wp->flags |= PANE_REDRAW; + return; + } + wp->flags |= PANE_REDRAWSCROLLBAR; +} + +static void +window_pane_scrollbar_redraw_visibility(struct window_pane *wp) +{ + redraw_invalidate_scene(wp->window); + wp->flags |= PANE_REDRAW; + server_redraw_window(wp->window); +} + static void window_pane_destroy(struct window_pane *wp) { @@ -1182,6 +1230,8 @@ window_pane_destroy(struct window_pane *wp) event_del(&wp->resize_timer); if (event_initialized(&wp->sync_timer)) event_del(&wp->sync_timer); + if (event_initialized(&wp->sb_auto_timer)) + event_del(&wp->sb_auto_timer); window_pane_clear_resizes(wp, NULL); RB_REMOVE(window_pane_tree, &all_window_panes, wp); @@ -1251,7 +1301,8 @@ window_pane_set_event(struct window_pane *wp) } void -window_pane_clear_resizes(struct window_pane *wp, struct window_pane_resize *except) +window_pane_clear_resizes(struct window_pane *wp, + struct window_pane_resize *except) { struct window_pane_resize *r, *r1; @@ -1724,7 +1775,7 @@ window_pane_full_size_offset(struct window_pane *wp, int *xoff, int *yoff, 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_scrollbar_reserve(wp, pane_scrollbars)) sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad; else sb_w = 0; @@ -2151,12 +2202,90 @@ window_pane_show_scrollbar(struct window_pane *wp, int sb_option) if (SCREEN_IS_ALTERNATE(&wp->base)) return (0); if (sb_option == PANE_SCROLLBARS_ALWAYS || + sb_option == PANE_SCROLLBARS_AUTOHIDE || (sb_option == PANE_SCROLLBARS_MODAL && window_pane_mode(wp) != WINDOW_PANE_NO_MODE)) return (1); return (0); } +int +window_pane_scrollbar_reserve(struct window_pane *wp, int sb_option) +{ + if (!window_pane_show_scrollbar(wp, sb_option)) + return (0); + return (sb_option == PANE_SCROLLBARS_ALWAYS); +} + +int +window_pane_scrollbar_overlay(struct window_pane *wp, int sb_option) +{ + if (!window_pane_show_scrollbar(wp, sb_option)) + return (0); + return (window_pane_scrollbar_auto_hide(wp)); +} + +int +window_pane_scrollbar_visible(struct window_pane *wp, int sb_option) +{ + if (!window_pane_show_scrollbar(wp, sb_option)) + return (0); + if (!window_pane_scrollbar_auto_hide(wp)) + return (1); + return (wp->sb_auto_visible); +} + +void +window_pane_scrollbar_start_timer(struct window_pane *wp) +{ + struct timeval tv; + u_int delay; + + if (!window_pane_scrollbar_auto_hide(wp) || !wp->sb_auto_visible) + return; + + delay = options_get_number(wp->window->options, + "pane-scrollbars-timeout"); + tv.tv_sec = delay / 1000; + tv.tv_usec = (delay % 1000) * 1000L; + evtimer_del(&wp->sb_auto_timer); + evtimer_add(&wp->sb_auto_timer, &tv); +} + +void +window_pane_scrollbar_show(struct window_pane *wp, int start_timer) +{ + int changed = 0; + int sb; + + if (!window_pane_scrollbar_auto_hide(wp)) + return; + sb = options_get_number(wp->window->options, "pane-scrollbars"); + if (!window_pane_show_scrollbar(wp, sb)) + return; + if (!wp->sb_auto_visible) { + wp->sb_auto_visible = 1; + changed = 1; + } + evtimer_del(&wp->sb_auto_timer); + if (start_timer) + window_pane_scrollbar_start_timer(wp); + if (changed) + window_pane_scrollbar_redraw_visibility(wp); +} + +void +window_pane_scrollbar_hide(struct window_pane *wp) +{ + if (event_initialized(&wp->sb_auto_timer)) + evtimer_del(&wp->sb_auto_timer); + wp->sb_auto_hover = 0; + if (!wp->sb_auto_visible) + return; + wp->sb_auto_visible = 0; + window_pane_scrollbar_redraw_visibility(wp); +} + int window_pane_get_bg(struct window_pane *wp) { From ad74c5ed7f62913d38543ff6843f38d14689b56d Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 29 Jun 2026 10:20:38 +0100 Subject: [PATCH 12/14] Add additional grid consistency checks for macOS. --- grid.c | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/grid.c b/grid.c index 44efa574d..28680cbdd 100644 --- a/grid.c +++ b/grid.c @@ -18,6 +18,9 @@ #include +#ifdef __APPLE__ +#include +#endif #include #include @@ -56,6 +59,28 @@ static const struct grid_cell_entry grid_cleared_entry = { { .data = { 0, 8, 8, ' ' } }, GRID_FLAG_CLEARED }; +#ifdef __APPLE__ +static void +grid_check_lines(struct grid *gd) +{ + u_int i, j; + + for (i = 0; i < gd->hsize + gd->sy; i++) { + for (j = i + 1; j < gd->hsize + gd->sy; j++) { + if (gd->linedata[i].celldata != NULL) + assert(gd->linedata[i].celldata != gd->linedata[j].celldata); + if (gd->linedata[i].extddata != NULL) + assert(gd->linedata[i].extddata != gd->linedata[j].extddata); + } + } +} +#else +static void +grid_check_lines(__unused struct grid *gd) +{ +} +#endif + /* Store cell in entry. */ static void grid_store_cell(struct grid_cell_entry *gce, const struct grid_cell *gc, @@ -287,9 +312,17 @@ grid_set_tab(struct grid_cell *gc, u_int width) static void grid_free_line(struct grid *gd, u_int py) { - free(gd->linedata[py].celldata); - free(gd->linedata[py].extddata); - memset(&gd->linedata[py], 0, sizeof gd->linedata[py]); + struct grid_line *gl = &gd->linedata[py]; + +#ifdef __APPLE__ + assert(gl->cellused <= gl->cellsize); + assert(gl->extdsize == 0 || gl->extddata != NULL); + assert(gl->cellsize == 0 || gl->celldata != NULL); +#endif + + free(gl->celldata); + free(gl->extddata); + memset(gl, 0, sizeof *gl); } /* Free several lines. */ @@ -449,6 +482,8 @@ grid_scroll_history(struct grid *gd, u_int bg) gd->linedata[gd->hsize].time = current_time; gd->hsize++; gd->scroll_added++; + + grid_check_lines(gd); } /* Clear the history. */ @@ -498,6 +533,8 @@ grid_scroll_history_region(struct grid *gd, u_int upper, u_int lower, u_int bg) gd->hscrolled++; gd->hsize++; gd->scroll_added++; + + grid_check_lines(gd); } /* Expand line to fit to cell. */ @@ -759,6 +796,8 @@ grid_move_lines(struct grid *gd, u_int dy, u_int py, u_int ny, u_int bg) } if (py != 0 && (py < dy || py >= dy + ny)) gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED; + + grid_check_lines(gd); } /* Move a group of cells. */ @@ -1248,6 +1287,8 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy, sy++; dy++; } + + grid_check_lines(dst); } /* Mark line as dead. */ @@ -1544,6 +1585,8 @@ grid_reflow(struct grid *gd, u_int sx) gd->linedata = target->linedata; free(target); gd->scroll_generation++; + + grid_check_lines(gd); } /* Convert to position based on wrapped lines. */ From 5df51a06f5f830ee53ab78a50ed51d4901b4f72c Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 29 Jun 2026 16:20:45 +0000 Subject: [PATCH 13/14] 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 7236360bb..9ea6b7fea 100644 --- a/tmux.c +++ b/tmux.c @@ -291,7 +291,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++) { @@ -316,7 +316,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 95719e342df89cf5c9e0c5f14994c6b681905dd6 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 29 Jun 2026 16:44:06 +0000 Subject: [PATCH 14/14] Limit hyperlink URIs to 1024 bytes which seems enough and allows us not to have to worry about gigantic URIs in styles, part of a change from Moritz Angermann. --- hyperlinks.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/hyperlinks.c b/hyperlinks.c index c8fd0ca2f..a71827272 100644 --- a/hyperlinks.c +++ b/hyperlinks.c @@ -42,6 +42,7 @@ */ #define MAX_HYPERLINKS 5000 +#define MAX_HYPERLINK_URI 1024 static long long hyperlinks_next_external_id = 1; static u_int global_hyperlinks_count; @@ -146,9 +147,13 @@ hyperlinks_put(struct hyperlinks *hl, const char *uri_in, internal_id_in = ""; utf8_stravis(&uri, uri_in, VIS_OCTAL|VIS_CSTYLE); - utf8_stravis(&internal_id, internal_id_in, VIS_OCTAL|VIS_CSTYLE); + if (strlen(uri) > MAX_HYPERLINK_URI) { + free(uri); + return (0); + } - if (*internal_id_in != '\0') { + utf8_stravis(&internal_id, internal_id_in, VIS_OCTAL|VIS_CSTYLE); + if (*internal_id != '\0') { find.uri = uri; find.internal_id = internal_id;