From 74f60951cc0f098265fc58336f001bc006c6585c Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 19 Dec 2025 08:46:25 +0000 Subject: [PATCH 01/20] Do not leak items if not using them because of synchronized update. --- screen-write.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/screen-write.c b/screen-write.c index 86eb45c2..aed6582f 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1738,8 +1738,16 @@ screen_write_collect_flush(struct screen_write_ctx *ctx, int scroll_only, u_int y, cx, cy, last, items = 0; struct tty_ctx ttyctx; - if (s->mode & MODE_SYNC) + if (s->mode & MODE_SYNC) { + for (y = 0; y < screen_size_y(s); y++) { + cl = &ctx->s->write_list[y]; + TAILQ_FOREACH_SAFE(ci, &cl->items, entry, tmp) { + TAILQ_REMOVE(&cl->items, ci, entry); + screen_write_free_citem(ci); + } + } return; + } if (ctx->scrolled != 0) { log_debug("%s: scrolled %u (region %u-%u)", __func__, From 188f963fe081274a498dfcd992c2475b6df83a9f Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 22 Dec 2025 08:35:04 +0000 Subject: [PATCH 02/20] Add {current}/{active} for -t for current window or active pane, from Manuel Einfalt in GitHub issue 4766. --- cmd-find.c | 15 +++++++++++++++ key-bindings.c | 2 ++ tmux.1 | 2 ++ 3 files changed, 19 insertions(+) diff --git a/cmd-find.c b/cmd-find.c index c651448d..761f133f 100644 --- a/cmd-find.c +++ b/cmd-find.c @@ -925,6 +925,7 @@ cmd_find_target(struct cmd_find_state *fs, struct cmdq_item *item, const char *target, enum cmd_find_type type, int flags) { struct mouse_event *m; + struct client *c; struct cmd_find_state current; char *colon, *period, *copy = NULL, tmp[256]; const char *session, *window, *pane, *s; @@ -991,6 +992,20 @@ cmd_find_target(struct cmd_find_state *fs, struct cmdq_item *item, if (target == NULL || *target == '\0') goto current; + if (strcmp(target, "@") == 0 || + strcmp(target, "{active}") == 0 || + strcmp(target, "{current}") == 0) { + c = cmdq_get_client(item); + if (c == NULL) { + cmdq_error(item, "no current client"); + goto error; + } + fs->wl = c->session->curw; + fs->wp = c->session->curw->window->active; + fs->w = c->session->curw->window; + goto found; + } + /* Mouse target is a plain = or {mouse}. */ if (strcmp(target, "=") == 0 || strcmp(target, "{mouse}") == 0) { m = &cmdq_get_event(item)->m; diff --git a/key-bindings.c b/key-bindings.c index 22e5cf59..2dd2b742 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -439,6 +439,7 @@ key_bindings_init(void) /* Mouse button 1 down on pane. */ "bind -n MouseDown1Pane { select-pane -t=; send -M }", + "bind -n C-MouseDown1Pane { swap-pane -s@ }", /* Mouse button 1 drag on pane. */ "bind -n MouseDrag1Pane { if -F '#{||:#{pane_in_mode},#{mouse_any_flag}}' { send -M } { copy-mode -M } }", @@ -460,6 +461,7 @@ key_bindings_init(void) /* Mouse button 1 down on status line. */ "bind -n MouseDown1Status { switch-client -t= }", + "bind -n C-MouseDown1Status { swap-window -t@ }", /* Mouse wheel down on status line. */ "bind -n WheelDownStatus { next-window }", diff --git a/tmux.1 b/tmux.1 index 6e5b70f7..2af98b8f 100644 --- a/tmux.1 +++ b/tmux.1 @@ -839,6 +839,7 @@ Each has a single-character alternative form. .It Li "{last}" Ta "!" Ta "The last (previously current) window" .It Li "{next}" Ta "+" Ta "The next window by number" .It Li "{previous}" Ta "-" Ta "The previous window by number" +.It Li "{current}" Ta "@" Ta "The current window" .El .Pp .Ar target-pane @@ -871,6 +872,7 @@ The following special tokens are available for the pane index: .It Li "{down-of}" Ta "" Ta "The pane below the active pane" .It Li "{left-of}" Ta "" Ta "The pane to the left of the active pane" .It Li "{right-of}" Ta "" Ta "The pane to the right of the active pane" +.It Li "{active}" Ta "@" Ta "The active pane" .El .Pp The tokens From a22ec275b4ea01992114fa560671046c6aa88a9c Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 22 Dec 2025 08:39:35 +0000 Subject: [PATCH 03/20] With status-keys vi, move the cursor left by one when pressing Escape to enter command mode, like vi. GitHub issue 4767 from Joshua Cooper. --- status.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/status.c b/status.c index 0551b547..3cda11fb 100644 --- a/status.c +++ b/status.c @@ -936,6 +936,8 @@ status_prompt_translate_key(struct client *c, key_code key, key_code *new_key) return (1); case '\033': /* Escape */ c->prompt_mode = PROMPT_COMMAND; + if (c->prompt_index != 0) + c->prompt_index--; c->flags |= CLIENT_REDRAWSTATUS; return (0); } @@ -961,10 +963,11 @@ status_prompt_translate_key(struct client *c, key_code key, key_code *new_key) *new_key = 'u'|KEYC_CTRL; return (1); case 'i': - case '\033': /* Escape */ c->prompt_mode = PROMPT_ENTRY; c->flags |= CLIENT_REDRAWSTATUS; return (0); + case '\033': /* Escape */ + return (0); } switch (key) { From d005803934c1bb5e6ad9658b19d5704971e0401b Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 22 Dec 2025 08:41:01 +0000 Subject: [PATCH 04/20] Add prompt-command-cursor-style, from Joshua Cooper in GitHub issue 4765. --- options-table.c | 9 +++++++++ status.c | 5 ++++- tmux.1 | 6 ++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/options-table.c b/options-table.c index 4385c9fd..f056966d 100644 --- a/options-table.c +++ b/options-table.c @@ -971,6 +971,15 @@ const struct options_table_entry options_table[] = { .text = "Style of the cursor when in the command prompt." }, + { .name = "prompt-command-cursor-style", + .type = OPTIONS_TABLE_CHOICE, + .scope = OPTIONS_TABLE_SESSION, + .choices = options_table_cursor_style_list, + .default_num = 0, + .text = "Style of the cursor in the command prompt when in command " + "mode, if 'status-keys' is set to 'vi'." + }, + { .name = "session-status-current-style", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW, diff --git a/status.c b/status.c index 3cda11fb..79ce9cad 100644 --- a/status.c +++ b/status.c @@ -804,7 +804,10 @@ status_prompt_redraw(struct client *c) n = options_get_number(s->options, "prompt-cursor-colour"); sl->active->default_ccolour = n; - n = options_get_number(s->options, "prompt-cursor-style"); + if (c->prompt_mode == PROMPT_COMMAND) + n = options_get_number(s->options, "prompt-command-cursor-style"); + else + n = options_get_number(s->options, "prompt-cursor-style"); screen_set_cursor_style(n, &sl->active->default_cstyle, &sl->active->default_mode); diff --git a/tmux.1 b/tmux.1 index 2af98b8f..fe228dd2 100644 --- a/tmux.1 +++ b/tmux.1 @@ -4696,6 +4696,12 @@ Set the style of the cursor in the command prompt. See the .Ic cursor-style options for available styles. +.It Ic prompt-command-cursor-style Ar style +Set the style of the cursor in the command prompt when vi keys are enabled and +the prompt is in command mode. +See the +.Ic cursor-style +options for available styles. .It Xo Ic renumber-windows .Op Ic on | off .Xc From b5c33ca2b769e87e86e1ed6d2c19dfce14c242c0 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 25 Dec 2025 18:05:15 +0000 Subject: [PATCH 05/20] Add selection_mode format variable for copy mode, from Mike Jonkmans in GitHub issue 4773. --- tmux.1 | 1 + window-copy.c | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/tmux.1 b/tmux.1 index fe228dd2..a93a002e 100644 --- a/tmux.1 +++ b/tmux.1 @@ -6238,6 +6238,7 @@ The following variables are available, where appropriate: .It Li "selection_active" Ta "" Ta "1 if selection started and changes with the cursor in copy mode" .It Li "selection_end_x" Ta "" Ta "X position of the end of the selection" .It Li "selection_end_y" Ta "" Ta "Y position of the end of the selection" +.It Li "selection_mode" Ta "" Ta "Selection mode" .It Li "selection_present" Ta "" Ta "1 if selection started in copy mode" .It Li "selection_start_x" Ta "" Ta "X position of the start of the selection" .It Li "selection_start_y" Ta "" Ta "Y position of the start of the selection" diff --git a/window-copy.c b/window-copy.c index f8700555..87d0dea8 100644 --- a/window-copy.c +++ b/window-copy.c @@ -956,6 +956,18 @@ window_copy_formats(struct window_mode_entry *wme, struct format_tree *ft) format_add(ft, "selection_present", "0"); } + switch (data->selflag) { + case SEL_CHAR: + format_add(ft, "selection_mode", "char"); + break; + case SEL_WORD: + format_add(ft, "selection_mode", "word"); + break; + case SEL_LINE: + format_add(ft, "selection_mode", "line"); + break; + } + format_add(ft, "search_present", "%d", data->searchmark != NULL); format_add(ft, "search_timed_out", "%d", data->timeout); if (data->searchcount != -1) { From f72832cc085be4fdc0220be340087b2110feb4cf Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 25 Dec 2025 18:07:00 +0000 Subject: [PATCH 06/20] Add focus-follows-mouse option, from Barry Wasdell in GitHub issue 4771. --- options-table.c | 7 +++++++ server-client.c | 17 ++++++++++++++--- tmux.1 | 6 ++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/options-table.c b/options-table.c index f056966d..78299328 100644 --- a/options-table.c +++ b/options-table.c @@ -665,6 +665,13 @@ const struct options_table_entry options_table[] = { .text = "Time for which status line messages should appear." }, + { .name = "focus-follows-mouse", + .type = OPTIONS_TABLE_FLAG, + .scope = OPTIONS_TABLE_SESSION, + .default_num = 0, + .text = "Whether moving the mouse into a pane selects it." + }, + { .name = "history-limit", .type = OPTIONS_TABLE_NUMBER, .scope = OPTIONS_TABLE_SESSION, diff --git a/server-client.c b/server-client.c index 2732f1fe..4cef9083 100644 --- a/server-client.c +++ b/server-client.c @@ -1064,8 +1064,16 @@ have_event: case NOTYPE: break; case MOVE: - if (where == PANE) + if (where == PANE) { key = KEYC_MOUSEMOVE_PANE; + if (wp != NULL && + wp != w->active && + options_get_number(s->options, "focus-follows-mouse")) { + window_set_active_pane(w, wp, 1); + server_redraw_window_borders(w); + server_status_window(w); + } + } if (where == STATUS) key = KEYC_MOUSEMOVE_STATUS; if (where == STATUS_LEFT) @@ -2973,7 +2981,8 @@ server_client_reset_state(struct client *c) /* * Set mouse mode if requested. To support dragging, always use button - * mode. + * mode. For focus-follows-mouse, we need all-motion mode to receive + * movement events. */ if (options_get_number(oo, "mouse")) { if (c->overlay_draw == NULL) { @@ -2983,7 +2992,9 @@ server_client_reset_state(struct client *c) mode |= MODE_MOUSE_ALL; } } - if (~mode & MODE_MOUSE_ALL) + if (options_get_number(oo, "focus-follows-mouse")) + mode |= MODE_MOUSE_ALL; + else if (~mode & MODE_MOUSE_ALL) mode |= MODE_MOUSE_BUTTON; } diff --git a/tmux.1 b/tmux.1 index a93a002e..4b88e203 100644 --- a/tmux.1 +++ b/tmux.1 @@ -4242,6 +4242,12 @@ passed through to applications running in .Nm . Attached clients should be detached and attached again after changing this option. +.It Xo Ic focus-follows-mouse +.Op Ic on | off +.Xc +When enabled and +.Ic mouse +is on, moving the mouse into a pane selects it. .It Xo Ic get-clipboard .Op Ic both | request | buffer | off .Xc From 6ef7375adebaf3276a947937ff6ad75af21b24b9 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 4 Jan 2026 08:05:14 +0000 Subject: [PATCH 07/20] Add some missing logging bits for themes. --- input.c | 7 ++++++- key-string.c | 8 ++++++++ screen.c | 2 ++ window.c | 5 +++-- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/input.c b/input.c index b3f67b23..0fd02dce 100644 --- a/input.c +++ b/input.c @@ -3395,14 +3395,19 @@ input_cancel_requests(struct client *c) static void input_report_current_theme(struct input_ctx *ictx) { - switch (window_pane_get_theme(ictx->wp)) { + struct window_pane *wp = ictx->wp; + + switch (window_pane_get_theme(wp)) { case THEME_DARK: + log_debug("%s: %%%u dark theme", __func__, wp->id); input_reply(ictx, 0, "\033[?997;1n"); break; case THEME_LIGHT: + log_debug("%s: %%%u light theme", __func__, wp->id); input_reply(ictx, 0, "\033[?997;2n"); break; case THEME_UNKNOWN: + log_debug("%s: %%%u unknown theme", __func__, wp->id); break; } } diff --git a/key-string.c b/key-string.c index a171b0cb..8b9b5604 100644 --- a/key-string.c +++ b/key-string.c @@ -382,6 +382,14 @@ key_string_lookup_key(key_code key, int with_flags) s = "PasteEnd"; goto append; } + if (key == KEYC_REPORT_DARK_THEME) { + s = "ReportDarkTheme"; + goto append; + } + if (key == KEYC_REPORT_LIGHT_THEME) { + s = "ReportLightTheme"; + goto append; + } if (key == KEYC_MOUSE) { s = "Mouse"; goto append; diff --git a/screen.c b/screen.c index 2b73cbce..12a55097 100644 --- a/screen.c +++ b/screen.c @@ -739,6 +739,8 @@ screen_mode_to_string(int mode) strlcat(tmp, "KEYS_EXTENDED,", sizeof tmp); if (mode & MODE_KEYS_EXTENDED_2) strlcat(tmp, "KEYS_EXTENDED_2,", sizeof tmp); + if (mode & MODE_THEME_UPDATES) + strlcat(tmp, "THEME_UPDATES,", sizeof tmp); tmp[strlen(tmp) - 1] = '\0'; return (tmp); } diff --git a/window.c b/window.c index 24bc5936..f3b8b8a8 100644 --- a/window.c +++ b/window.c @@ -1932,17 +1932,18 @@ window_pane_send_theme_update(struct window_pane *wp) return; if (~wp->screen->mode & MODE_THEME_UPDATES) return; - switch (window_pane_get_theme(wp)) { case THEME_LIGHT: + log_debug("%s: %%%u light theme", __func__, wp->id); input_key_pane(wp, KEYC_REPORT_LIGHT_THEME, NULL); break; case THEME_DARK: + log_debug("%s: %%%u dark theme", __func__, wp->id); input_key_pane(wp, KEYC_REPORT_DARK_THEME, NULL); break; case THEME_UNKNOWN: + log_debug("%s: %%%u unknown theme", __func__, wp->id); break; } - wp->flags &= ~PANE_THEMECHANGED; } From 45f23f3a59c91ddaf0b358b98c9dbe2bc1e83257 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 4 Jan 2026 08:38:26 +0000 Subject: [PATCH 08/20] Minor tidying of cmd_list_print from Pavel Roskin. --- cmd.c | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/cmd.c b/cmd.c index f02aa625..119b685a 100644 --- a/cmd.c +++ b/cmd.c @@ -682,6 +682,9 @@ cmd_list_print(const struct cmd_list *cmdlist, int escaped) struct cmd *cmd, *next; char *buf, *this; size_t len; + const char *separator; + const char *single_separator = escaped ? " \\; " : " ; "; + const char *double_separator = escaped ? " \\;\\; " : " ;; "; len = 1; buf = xcalloc(1, len); @@ -696,17 +699,11 @@ cmd_list_print(const struct cmd_list *cmdlist, int escaped) next = TAILQ_NEXT(cmd, qentry); if (next != NULL) { - if (cmd->group != next->group) { - if (escaped) - strlcat(buf, " \\;\\; ", len); - else - strlcat(buf, " ;; ", len); - } else { - if (escaped) - strlcat(buf, " \\; ", len); - else - strlcat(buf, " ; ", len); - } + if (cmd->group != next->group) + separator = double_separator; + else + separator = single_separator; + strlcat(buf, separator, len); } free(this); From 356d0aedbd49f1525b4d0aca24829059db4eed29 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 4 Jan 2026 08:40:46 +0000 Subject: [PATCH 09/20] Remove an unused declaration (from OpenBSD, same change also sent by Mike Jonkmans). --- compat/imsg-buffer.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/compat/imsg-buffer.c b/compat/imsg-buffer.c index 3e314bab..e516b4ab 100644 --- a/compat/imsg-buffer.c +++ b/compat/imsg-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: imsg-buffer.c,v 1.35 2025/06/04 09:06:56 claudio Exp $ */ +/* $OpenBSD: imsg-buffer.c,v 1.36 2025/08/25 08:29:49 claudio Exp $ */ /* * Copyright (c) 2023 Claudio Jeker @@ -699,8 +699,6 @@ msgbuf_queuelen(struct msgbuf *msgbuf) void msgbuf_clear(struct msgbuf *msgbuf) { - struct ibuf *buf; - /* write side */ ibufq_flush(&msgbuf->bufs); From baa3b51b3e576a07b0bb1abfc8473a678e175b79 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 5 Jan 2026 08:30:30 +0000 Subject: [PATCH 10/20] Do not use client if there isn't one, GitHub issue 4789. --- format.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/format.c b/format.c index ec55fc91..6628ef20 100644 --- a/format.c +++ b/format.c @@ -4460,7 +4460,9 @@ format_loop_sessions(struct format_expand_state *es, const char *fmt) for (i = 0; i < n; i++) { s = l[i]; format_log(es, "session loop: $%u", s->id); - if (active != NULL && s->id == ft->c->session->id) + if (active != NULL && + ft->c != NULL && + s->id == ft->c->session->id) use = active; else use = all; From 6a7cd79a63ab45bb99887615fe9f820d21bf6c42 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 5 Jan 2026 08:32:19 +0000 Subject: [PATCH 11/20] Redraw pane borders when entering or leaving alternate screen, from Mike Jonkmans in GitHub issue 4788. --- screen-write.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/screen-write.c b/screen-write.c index aed6582f..4f126868 100644 --- a/screen-write.c +++ b/screen-write.c @@ -2282,8 +2282,10 @@ screen_write_alternateon(struct screen_write_ctx *ctx, struct grid_cell *gc, screen_write_collect_flush(ctx, 0, __func__); screen_alternate_on(ctx->s, gc, cursor); - if (wp != NULL) + if (wp != NULL) { layout_fix_panes(wp->window, NULL); + server_redraw_window_borders(wp->window); + } screen_write_initctx(ctx, &ttyctx, 1); if (ttyctx.redraw_cb != NULL) @@ -2304,8 +2306,10 @@ screen_write_alternateoff(struct screen_write_ctx *ctx, struct grid_cell *gc, screen_write_collect_flush(ctx, 0, __func__); screen_alternate_off(ctx->s, gc, cursor); - if (wp != NULL) + if (wp != NULL) { layout_fix_panes(wp->window, NULL); + server_redraw_window_borders(wp->window); + } screen_write_initctx(ctx, &ttyctx, 1); if (ttyctx.redraw_cb != NULL) From ccd4dd7ff2a24eec2e3a5e12aaf96563fe659139 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 6 Jan 2026 09:11:15 +0000 Subject: [PATCH 12/20] Do not log theme if pane is NULL. --- input.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/input.c b/input.c index 0fd02dce..4f5bd38f 100644 --- a/input.c +++ b/input.c @@ -3397,6 +3397,8 @@ input_report_current_theme(struct input_ctx *ictx) { struct window_pane *wp = ictx->wp; + if (wp == NULL) + return; switch (window_pane_get_theme(wp)) { case THEME_DARK: log_debug("%s: %%%u dark theme", __func__, wp->id); From 5f9dac8abcc82d45a0cc38310059f86f840f5b75 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 6 Jan 2026 10:17:29 +0000 Subject: [PATCH 13/20] Do not use ;;s in list-keys output as it is confusing and cannot be parsed on input, from Patrick Motard in GitHub issue 4750. --- cmd-list-keys.c | 6 ++++-- cmd.c | 6 ++++-- tmux.h | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cmd-list-keys.c b/cmd-list-keys.c index ddfc0e0c..f25b0636 100644 --- a/cmd-list-keys.c +++ b/cmd-list-keys.c @@ -109,7 +109,8 @@ cmd_list_keys_print_notes(struct cmdq_item *item, struct args *args, key = key_string_lookup_key(bd->key, 0); if (bd->note == NULL || *bd->note == '\0') - note = cmd_list_print(bd->cmdlist, 1); + note = cmd_list_print(bd->cmdlist, + CMD_LIST_PRINT_ESCAPED|CMD_LIST_PRINT_NO_GROUPS); else note = xstrdup(bd->note); tmp = utf8_padcstr(key, keywidth + 1); @@ -288,7 +289,8 @@ cmd_list_keys_exec(struct cmd *self, struct cmdq_item *item) tmpused = strlcat(tmp, " ", tmpsize); free(cp); - cp = cmd_list_print(bd->cmdlist, 1); + cp = cmd_list_print(bd->cmdlist, + CMD_LIST_PRINT_ESCAPED|CMD_LIST_PRINT_NO_GROUPS); cplen = strlen(cp); while (tmpused + cplen + 1 >= tmpsize) { tmpsize *= 2; diff --git a/cmd.c b/cmd.c index 119b685a..e5ec9cc5 100644 --- a/cmd.c +++ b/cmd.c @@ -677,12 +677,14 @@ cmd_list_copy(const struct cmd_list *cmdlist, int argc, char **argv) /* Get a command list as a string. */ char * -cmd_list_print(const struct cmd_list *cmdlist, int escaped) +cmd_list_print(const struct cmd_list *cmdlist, int flags) { struct cmd *cmd, *next; char *buf, *this; size_t len; const char *separator; + int escaped = flags & CMD_LIST_PRINT_ESCAPED; + int no_groups = flags & CMD_LIST_PRINT_NO_GROUPS; const char *single_separator = escaped ? " \\; " : " ; "; const char *double_separator = escaped ? " \\;\\; " : " ;; "; @@ -699,7 +701,7 @@ cmd_list_print(const struct cmd_list *cmdlist, int escaped) next = TAILQ_NEXT(cmd, qentry); if (next != NULL) { - if (cmd->group != next->group) + if (!no_groups && cmd->group != next->group) separator = double_separator; else separator = single_separator; diff --git a/tmux.h b/tmux.h index 180208e3..c738fb67 100644 --- a/tmux.h +++ b/tmux.h @@ -2689,6 +2689,8 @@ void cmd_list_append(struct cmd_list *, struct cmd *); void cmd_list_append_all(struct cmd_list *, struct cmd_list *); void cmd_list_move(struct cmd_list *, struct cmd_list *); void cmd_list_free(struct cmd_list *); +#define CMD_LIST_PRINT_ESCAPED 0x1 +#define CMD_LIST_PRINT_NO_GROUPS 0x2 char *cmd_list_print(const struct cmd_list *, int); struct cmd *cmd_list_first(struct cmd_list *); struct cmd *cmd_list_next(struct cmd *); From 035a2f35d40628dcfe235179220fc0ede848a195 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 6 Jan 2026 14:33:05 +0000 Subject: [PATCH 14/20] Clear trimmed lines after moving, from Antony Raj in GitHub issue 4790. --- grid.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/grid.c b/grid.c index 63eea68d..7ad6770b 100644 --- a/grid.c +++ b/grid.c @@ -361,9 +361,13 @@ grid_compare(struct grid *ga, struct grid *gb) static void grid_trim_history(struct grid *gd, u_int ny) { + u_int remaining; + grid_free_lines(gd, 0, ny); + remaining = gd->hsize + gd->sy - ny; memmove(&gd->linedata[0], &gd->linedata[ny], - (gd->hsize + gd->sy - ny) * (sizeof *gd->linedata)); + remaining * (sizeof *gd->linedata)); + memset(&gd->linedata[remaining], 0, ny * (sizeof *gd->linedata)); } /* From f2268041490fefabc1f8930c2c8492d4fc37444f Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 6 Jan 2026 20:05:57 +0000 Subject: [PATCH 15/20] Do not send theme unless it has changed, and do not send immediately when updates are enabled. GitHub issue 5787. --- input-keys.c | 6 ------ input.c | 15 ++++++++++++--- tmux.h | 21 +++++++++++---------- window.c | 18 +++++++++++++----- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/input-keys.c b/input-keys.c index 7d7263fb..6a872731 100644 --- a/input-keys.c +++ b/input-keys.c @@ -314,12 +314,6 @@ static struct input_key_entry input_key_defaults[] = { { .key = KEYC_DC|KEYC_BUILD_MODIFIERS, .data = "\033[3;_~" }, - { .key = KEYC_REPORT_DARK_THEME, - .data = "\033[?997;1n" - }, - { .key = KEYC_REPORT_LIGHT_THEME, - .data = "\033[?997;2n" - }, }; static const key_code input_key_modifiers[] = { 0, diff --git a/input.c b/input.c index 4f5bd38f..43d5e9a7 100644 --- a/input.c +++ b/input.c @@ -1898,6 +1898,8 @@ input_csi_dispatch_rm_private(struct input_ctx *ictx) break; case 2031: screen_write_mode_clear(sctx, MODE_THEME_UPDATES); + if (ictx->wp != NULL) + ictx->wp->flags &= ~PANE_THEMECHANGED; break; case 2026: /* synchronized output */ screen_write_stop_sync(ictx->wp); @@ -2001,6 +2003,10 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx) break; case 2031: screen_write_mode_set(sctx, MODE_THEME_UPDATES); + if (ictx->wp != NULL) { + ictx->wp->last_theme = window_pane_get_theme(ictx->wp); + ictx->wp->flags &= ~PANE_THEMECHANGED; + } break; case 2026: /* synchronized output */ screen_write_start_sync(ictx->wp); @@ -3397,9 +3403,11 @@ input_report_current_theme(struct input_ctx *ictx) { struct window_pane *wp = ictx->wp; - if (wp == NULL) - return; - switch (window_pane_get_theme(wp)) { + if (wp != NULL) { + wp->last_theme = window_pane_get_theme(wp); + wp->flags &= ~PANE_THEMECHANGED; + + switch (wp->last_theme) { case THEME_DARK: log_debug("%s: %%%u dark theme", __func__, wp->id); input_reply(ictx, 0, "\033[?997;1n"); @@ -3411,5 +3419,6 @@ input_report_current_theme(struct input_ctx *ictx) case THEME_UNKNOWN: log_debug("%s: %%%u unknown theme", __func__, wp->id); break; + } } } diff --git a/tmux.h b/tmux.h index c738fb67..de7421f5 100644 --- a/tmux.h +++ b/tmux.h @@ -1135,6 +1135,16 @@ struct window_pane_resize { }; TAILQ_HEAD(window_pane_resizes, window_pane_resize); +/* + * Client theme, this is worked out from the background colour if not reported + * by terminal. + */ +enum client_theme { + THEME_UNKNOWN, + THEME_LIGHT, + THEME_DARK +}; + /* Child window structure. */ struct window_pane { u_int id; @@ -1198,6 +1208,7 @@ struct window_pane { struct grid_cell cached_gc; struct grid_cell cached_active_gc; struct colour_palette palette; + enum client_theme last_theme; int pipe_fd; struct bufferevent *pipe_event; @@ -1876,16 +1887,6 @@ struct overlay_ranges { u_int nx[OVERLAY_MAX_RANGES]; }; -/* - * Client theme, this is worked out from the background colour if not reported - * by terminal. - */ -enum client_theme { - THEME_UNKNOWN, - THEME_LIGHT, - THEME_DARK -}; - /* Client connection. */ typedef int (*prompt_input_cb)(struct client *, void *, const char *, int); typedef void (*prompt_free_cb)(void *); diff --git a/window.c b/window.c index f3b8b8a8..9581c64f 100644 --- a/window.c +++ b/window.c @@ -926,7 +926,7 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit) wp = xcalloc(1, sizeof *wp); wp->window = w; wp->options = options_create(w->options); - wp->flags = (PANE_STYLECHANGED|PANE_THEMECHANGED); + wp->flags = PANE_STYLECHANGED; wp->id = next_window_pane_id++; RB_INSERT(window_pane_tree, &all_window_panes, wp); @@ -1926,24 +1926,32 @@ window_pane_get_theme(struct window_pane *wp) void window_pane_send_theme_update(struct window_pane *wp) { + enum client_theme theme; + if (wp == NULL || window_pane_exited(wp)) return; if (~wp->flags & PANE_THEMECHANGED) return; if (~wp->screen->mode & MODE_THEME_UPDATES) return; - switch (window_pane_get_theme(wp)) { + + theme = window_pane_get_theme(wp); + if (theme == wp->last_theme) + return; + wp->last_theme = theme; + wp->flags &= ~PANE_THEMECHANGED; + + switch (theme) { case THEME_LIGHT: log_debug("%s: %%%u light theme", __func__, wp->id); - input_key_pane(wp, KEYC_REPORT_LIGHT_THEME, NULL); + bufferevent_write(wp->event, "\033[?997;2n", 9); break; case THEME_DARK: log_debug("%s: %%%u dark theme", __func__, wp->id); - input_key_pane(wp, KEYC_REPORT_DARK_THEME, NULL); + bufferevent_write(wp->event, "\033[?997;1n", 9); break; case THEME_UNKNOWN: log_debug("%s: %%%u unknown theme", __func__, wp->id); break; } - wp->flags &= ~PANE_THEMECHANGED; } From f6c90520827c6baf627cd8dc5c7b5d3200f2098a Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 6 Jan 2026 20:09:42 +0000 Subject: [PATCH 16/20] If cannot find a terminator for palette responses, treat as a partial key not complete. GitHub issue 4749. --- tty-keys.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tty-keys.c b/tty-keys.c index c59e6a68..2fa05146 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1750,7 +1750,9 @@ tty_keys_palette(struct tty *tty, const char *buf, size_t len, size_t *size) /* Copy the rest up to \033\ or \007. */ start = (endptr - buf) + 1; - for (i = start; i < len && i - start < sizeof tmp; i++) { + for (i = start; i - start < sizeof tmp; i++) { + if (i == len) + return (1); if (buf[i - 1] == '\033' && buf[i] == '\\') break; if (buf[i] == '\007') From 583f12ea7128cfac1fab94b740106e850fb917c1 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 7 Jan 2026 08:16:20 +0000 Subject: [PATCH 17/20] Work out the default command from the queue in case it has been set from the config file, GitHub issue 4791. --- options.c | 2 +- server-client.c | 38 +++++++++++++++++++++++++++----------- tmux.h | 2 +- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/options.c b/options.c index f39fbf2a..bfbec5c7 100644 --- a/options.c +++ b/options.c @@ -749,7 +749,7 @@ options_get_number(struct options *oo, const char *name) return (o->value.number); } -const struct cmd_list * +struct cmd_list * options_get_command(struct options *oo, const char *name) { struct options_entry *o; diff --git a/server-client.c b/server-client.c index 4cef9083..af67a327 100644 --- a/server-client.c +++ b/server-client.c @@ -3447,6 +3447,24 @@ server_client_read_only(struct cmdq_item *item, __unused void *data) return (CMD_RETURN_ERROR); } +/* Callback for default command. */ +static enum cmd_retval +server_client_default_command(struct cmdq_item *item, __unused void *data) +{ + struct client *c = cmdq_get_client(item); + struct cmd_list *cmdlist; + struct cmdq_item *new_item; + + cmdlist = options_get_command(global_options, "default-client-command"); + if ((c->flags & CLIENT_READONLY) && + !cmd_list_all_have(cmdlist, CMD_READONLY)) + new_item = cmdq_get_callback(server_client_read_only, NULL); + else + new_item = cmdq_get_command(cmdlist, NULL); + cmdq_insert_after(item, new_item); + return (CMD_RETURN_NORMAL); +} + /* Callback when command is done. */ static enum cmd_retval server_client_command_done(struct cmdq_item *item, __unused void *data) @@ -3475,7 +3493,6 @@ server_client_dispatch_command(struct client *c, struct imsg *imsg) struct cmd_parse_result *pr; struct args_value *values; struct cmdq_item *new_item; - struct cmd_list *cmdlist; if (c->flags & CLIENT_EXIT) return (0); @@ -3496,8 +3513,8 @@ server_client_dispatch_command(struct client *c, struct imsg *imsg) argc = data.argc; if (argc == 0) { - cmdlist = cmd_list_copy(options_get_command(global_options, - "default-client-command"), 0, NULL); + new_item = cmdq_get_callback(server_client_default_command, + NULL); } else { values = args_from_vector(argc, argv); pr = cmd_parse_from_arguments(values, argc, NULL); @@ -3511,18 +3528,17 @@ server_client_dispatch_command(struct client *c, struct imsg *imsg) args_free_values(values, argc); free(values); cmd_free_argv(argc, argv); - cmdlist = pr->cmdlist; + if ((c->flags & CLIENT_READONLY) && + !cmd_list_all_have(pr->cmdlist, CMD_READONLY)) { + new_item = cmdq_get_callback(server_client_read_only, + NULL); + } else + new_item = cmdq_get_command(pr->cmdlist, NULL); + cmd_list_free(pr->cmdlist); } - - if ((c->flags & CLIENT_READONLY) && - !cmd_list_all_have(cmdlist, CMD_READONLY)) - new_item = cmdq_get_callback(server_client_read_only, NULL); - else - new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); cmdq_append(c, cmdq_get_callback(server_client_command_done, NULL)); - cmd_list_free(cmdlist); return (0); error: diff --git a/tmux.h b/tmux.h index de7421f5..36549125 100644 --- a/tmux.h +++ b/tmux.h @@ -2403,7 +2403,7 @@ struct options_entry *options_match_get(struct options *, const char *, int *, int, int *); const char *options_get_string(struct options *, const char *); long long options_get_number(struct options *, const char *); -const struct cmd_list *options_get_command(struct options *, const char *); +struct cmd_list *options_get_command(struct options *, const char *); struct options_entry * printflike(4, 5) options_set_string(struct options *, const char *, int, const char *, ...); struct options_entry *options_set_number(struct options *, const char *, From e2afaaea753e94b94a286310d1b3a7cedff88527 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 7 Jan 2026 18:29:15 +0000 Subject: [PATCH 18/20] Correct redrawing of wide characters when overwritten. Reported by Jake Stewart in GitHub issue 4737. --- screen-write.c | 40 +++++++++++++++++++++++++++++++++------- tty.c | 6 ++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/screen-write.c b/screen-write.c index 4f126868..47b370ca 100644 --- a/screen-write.c +++ b/screen-write.c @@ -61,9 +61,9 @@ screen_write_get_citem(void) ci = TAILQ_FIRST(&screen_write_citem_freelist); if (ci != NULL) { - TAILQ_REMOVE(&screen_write_citem_freelist, ci, entry); - memset(ci, 0, sizeof *ci); - return (ci); + TAILQ_REMOVE(&screen_write_citem_freelist, ci, entry); + memset(ci, 0, sizeof *ci); + return (ci); } return (xcalloc(1, sizeof *ci)); } @@ -71,7 +71,7 @@ screen_write_get_citem(void) static void screen_write_free_citem(struct screen_write_citem *ci) { - TAILQ_INSERT_TAIL(&screen_write_citem_freelist, ci, entry); + TAILQ_INSERT_TAIL(&screen_write_citem_freelist, ci, entry); } static void @@ -1809,7 +1809,7 @@ void screen_write_collect_end(struct screen_write_ctx *ctx) { struct screen *s = ctx->s; - struct screen_write_citem *ci = ctx->item, *before; + struct screen_write_citem *ci = ctx->item, *nci, *before; struct screen_write_cline *cl = &s->write_list[s->cy]; struct grid_cell gc; u_int xx; @@ -1838,6 +1838,8 @@ screen_write_collect_end(struct screen_write_ctx *ctx) break; grid_view_set_cell(s->grid, xx, s->cy, &grid_default_cell); + log_debug("%s: padding erased (before) at %u", + __func__, xx); } if (xx != s->cx) { if (xx == 0) @@ -1845,8 +1847,19 @@ screen_write_collect_end(struct screen_write_ctx *ctx) if (gc.data.width > 1) { grid_view_set_cell(s->grid, xx, s->cy, &grid_default_cell); + log_debug("%s: padding erased (before) at %u", + __func__, xx); } } + if (xx != s->cx) { + nci = ctx->item; + nci->type = CLEAR; + nci->x = xx; + nci->bg = 8; + nci->used = s->cx - xx; + TAILQ_INSERT_BEFORE(ci, nci, entry); + ctx->item = screen_write_get_citem(); + } } grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x, @@ -1858,6 +1871,16 @@ screen_write_collect_end(struct screen_write_ctx *ctx) if (~gc.flags & GRID_FLAG_PADDING) break; grid_view_set_cell(s->grid, xx, s->cy, &grid_default_cell); + log_debug("%s: padding erased (after) at %u", __func__, xx); + } + if (xx != s->cx) { + nci = ctx->item; + nci->type = CLEAR; + nci->x = s->cx; + nci->bg = 8; + nci->used = xx - s->cx; + TAILQ_INSERT_AFTER(&cl->items, ci, nci, entry); + ctx->item = screen_write_get_citem(); } } @@ -1928,7 +1951,7 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) struct tty_ctx ttyctx; u_int sx = screen_size_x(s), sy = screen_size_y(s); u_int width = ud->width, xx, not_wrap; - int selected, skip = 1; + int selected, skip = 1, redraw = 0; /* Ignore padding cells. */ if (gc->flags & GRID_FLAG_PADDING) @@ -1970,8 +1993,10 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) gl = grid_get_line(s->grid, s->grid->hsize + s->cy); if (gl->flags & GRID_LINE_EXTENDED) { grid_view_get_cell(gd, s->cx, s->cy, &now_gc); - if (screen_write_overwrite(ctx, &now_gc, width)) + if (screen_write_overwrite(ctx, &now_gc, width)) { + redraw = 1; skip = 0; + } } /* @@ -2048,6 +2073,7 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) ttyctx.cell = &tmp_gc; } else ttyctx.cell = gc; + ttyctx.num = redraw ? 2 : 0; tty_write(tty_cmd_cell, &ttyctx); } } diff --git a/tty.c b/tty.c index 177d4953..8b670b3f 100644 --- a/tty.c +++ b/tty.c @@ -2112,6 +2112,12 @@ tty_cmd_cell(struct tty *tty, const struct tty_ctx *ctx) (gcp->data.width == 1 && !tty_check_overlay(tty, px, py))) return; + if (ctx->num == 2) { + tty_draw_line(tty, s, 0, s->cy, screen_size_x(s), + ctx->xoff - ctx->wox, py, &ctx->defaults, ctx->palette); + return; + } + /* Handle partially obstructed wide characters. */ if (gcp->data.width > 1) { tty_check_overlay_range(tty, px, py, gcp->data.width, &r); From ff4f6b90660dd5477f1c4242f3e8628ef3854a74 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 7 Jan 2026 20:03:34 +0000 Subject: [PATCH 19/20] Extend escape timeout if there are active forwarded requests not just tmux's own requests. GitHub issue 4793. --- tty-keys.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tty-keys.c b/tty-keys.c index 2fa05146..33de5cbb 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -956,7 +956,8 @@ partial_key: if (delay == 0) delay = 1; if ((tty->flags & (TTY_WAITFG|TTY_WAITBG) || - (tty->flags & TTY_ALL_REQUEST_FLAGS) != TTY_ALL_REQUEST_FLAGS)) { + (tty->flags & TTY_ALL_REQUEST_FLAGS) != TTY_ALL_REQUEST_FLAGS) || + !TAILQ_EMPTY(&c->input_requests)) { log_debug("%s: increasing delay for active query", c->name); if (delay < 500) delay = 500; From c8ccd420be6ff6fad19346cbc1c85ef1fe1634c7 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 7 Jan 2026 20:16:32 +0000 Subject: [PATCH 20/20] Reduce request timeout to 500 milliseconds to match the extended escape-time, and discard palette requests if receiving a reply for a different index. --- input.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/input.c b/input.c index 43d5e9a7..45b70db6 100644 --- a/input.c +++ b/input.c @@ -63,7 +63,7 @@ struct input_request { struct input_ctx *ictx; enum input_request_type type; - time_t t; + uint64_t t; enum input_end_type end; int idx; @@ -72,7 +72,7 @@ struct input_request { TAILQ_ENTRY(input_request) entry; TAILQ_ENTRY(input_request) centry; }; -#define INPUT_REQUEST_TIMEOUT 2 +#define INPUT_REQUEST_TIMEOUT 500 /* Input parser cell. */ struct input_cell { @@ -3206,7 +3206,7 @@ input_request_timer_callback(__unused int fd, __unused short events, void *arg) { struct input_ctx *ictx = arg; struct input_request *ir, *ir1; - time_t t = time(NULL); + uint64_t t = get_timer(); TAILQ_FOREACH_SAFE(ir, &ictx->requests, entry, ir1) { if (ir->t >= t - INPUT_REQUEST_TIMEOUT) @@ -3223,7 +3223,7 @@ input_request_timer_callback(__unused int fd, __unused short events, void *arg) static void input_start_request_timer(struct input_ctx *ictx) { - struct timeval tv = { .tv_sec = 0, .tv_usec = 500000 }; + struct timeval tv = { .tv_sec = 0, .tv_usec = 100000 }; event_del(&ictx->request_timer); event_add(&ictx->request_timer, &tv); @@ -3238,7 +3238,7 @@ input_make_request(struct input_ctx *ictx, enum input_request_type type) ir = xcalloc (1, sizeof *ir); ir->type = type; ir->ictx = ictx; - ir->t = time(NULL); + ir->t = get_timer(); if (++ictx->request_count == 1) input_start_request_timer(ictx); @@ -3359,7 +3359,11 @@ input_request_reply(struct client *c, enum input_request_type type, void *data) input_free_request(ir); continue; } - if (type == INPUT_REQUEST_PALETTE && pd->idx == ir->idx) { + if (type == INPUT_REQUEST_PALETTE) { + if (pd->idx != ir->idx) { + input_free_request(ir); + continue; + } found = ir; break; }