From 588013bb44231d3873dc58483c2cc75d93df5b91 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 3 Feb 2026 08:53:58 +0000 Subject: [PATCH 01/20] Refresh copy mode when style changes, from Josh Cooper in GitHub issue 4830. --- server-client.c | 18 +++++++++++++++--- tmux.h | 1 + window-copy.c | 13 +++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/server-client.c b/server-client.c index 050ca10a..c2044398 100644 --- a/server-client.c +++ b/server-client.c @@ -2709,14 +2709,26 @@ server_client_handle_key(struct client *c, struct key_event *event) void server_client_loop(void) { - struct client *c; - struct window *w; - struct window_pane *wp; + struct client *c; + struct window *w; + struct window_pane *wp; + struct window_mode_entry *wme; /* Check for window resize. This is done before redrawing. */ RB_FOREACH(w, windows, &windows) server_client_check_window_resize(w); + /* Notify modes that pane styles may have changed. */ + RB_FOREACH(w, windows, &windows) { + TAILQ_FOREACH(wp, &w->panes, entry) { + if (wp->flags & PANE_STYLECHANGED) { + wme = TAILQ_FIRST(&wp->modes); + if (wme != NULL && wme->mode->style_changed != NULL) + wme->mode->style_changed(wme); + } + } + } + /* Check clients. */ TAILQ_FOREACH(c, &clients, entry) { server_client_check_exit(c); diff --git a/tmux.h b/tmux.h index be4632c0..34958d4b 100644 --- a/tmux.h +++ b/tmux.h @@ -1069,6 +1069,7 @@ struct window_mode { void (*free)(struct window_mode_entry *); void (*resize)(struct window_mode_entry *, u_int, u_int); void (*update)(struct window_mode_entry *); + void (*style_changed)(struct window_mode_entry *); void (*key)(struct window_mode_entry *, struct client *, struct session *, struct winlink *, key_code, struct mouse_event *); diff --git a/window-copy.c b/window-copy.c index 04cfe85d..3a147c85 100644 --- a/window-copy.c +++ b/window-copy.c @@ -51,6 +51,7 @@ static void window_copy_redraw_selection(struct window_mode_entry *, u_int); static void window_copy_redraw_lines(struct window_mode_entry *, u_int, u_int); static void window_copy_redraw_screen(struct window_mode_entry *); +static void window_copy_style_changed(struct window_mode_entry *); 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 *, @@ -158,6 +159,7 @@ const struct window_mode window_copy_mode = { .init = window_copy_init, .free = window_copy_free, .resize = window_copy_resize, + .style_changed = window_copy_style_changed, .key_table = window_copy_key_table, .command = window_copy_command, .formats = window_copy_formats, @@ -170,6 +172,7 @@ const struct window_mode window_view_mode = { .init = window_copy_view_init, .free = window_copy_free, .resize = window_copy_resize, + .style_changed = window_copy_style_changed, .key_table = window_copy_key_table, .command = window_copy_command, .formats = window_copy_formats, @@ -4595,6 +4598,16 @@ window_copy_redraw_screen(struct window_mode_entry *wme) window_copy_redraw_lines(wme, 0, screen_size_y(&data->screen)); } +static void +window_copy_style_changed(struct window_mode_entry *wme) +{ + struct window_copy_mode_data *data = wme->data; + + if (data->screen.sel != NULL) + window_copy_set_selection(wme, 0, 1); + window_copy_redraw_screen(wme); +} + static void window_copy_synchronize_cursor_end(struct window_mode_entry *wme, int begin, int no_reset) From 62944da74b7add5c7ac73527d8eb5d9d51ebbd60 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 3 Feb 2026 09:07:44 +0000 Subject: [PATCH 02/20] Make OSC 52 work in popups, from gogongxt at 163 dot com in GitHub issue 4797. --- input.c | 89 +++++++++++++++++++++++++++++++++------------------ popup.c | 2 +- tmux.h | 2 +- window-copy.c | 2 +- window.c | 2 +- 5 files changed, 62 insertions(+), 35 deletions(-) diff --git a/input.c b/input.c index 45b70db6..c8ae3544 100644 --- a/input.c +++ b/input.c @@ -101,6 +101,7 @@ struct input_ctx { struct bufferevent *event; struct screen_write_ctx ctx; struct colour_palette *palette; + struct client *c; struct input_cell cell; struct input_cell old_cell; @@ -854,7 +855,7 @@ input_restore_state(struct input_ctx *ictx) /* Initialise input parser. */ struct input_ctx * input_init(struct window_pane *wp, struct bufferevent *bev, - struct colour_palette *palette) + struct colour_palette *palette, struct client *c) { struct input_ctx *ictx; @@ -862,6 +863,7 @@ input_init(struct window_pane *wp, struct bufferevent *bev, ictx->wp = wp; ictx->event = bev; ictx->palette = palette; + ictx->c = c; ictx->input_space = INPUT_BUF_START; ictx->input_buf = xmalloc(INPUT_BUF_START); @@ -3073,31 +3075,28 @@ input_osc_52_reply(struct input_ctx *ictx) input_add_request(ictx, INPUT_REQUEST_CLIPBOARD, ictx->input_end); } -/* Handle the OSC 52 sequence for setting the clipboard. */ -static void -input_osc_52(struct input_ctx *ictx, const char *p) +/* + * Parse and decode OSC 52 clipboard data. Returns 0 on failure or if handled + * as a query. On success, returns 1 and sets *out, *outlen, and *flags (caller + * must free *out). + */ +static int +input_osc_52_parse(struct input_ctx *ictx, const char *p, u_char **out, + int *outlen, char *flags) { - struct window_pane *wp = ictx->wp; - size_t len; - char *end; - u_char *out; - int outlen, state; - struct screen_write_ctx ctx; - const char* allow = "cpqs01234567"; - char flags[sizeof "cpqs01234567"] = ""; - u_int i, j = 0; + char *end; + size_t len; + const char *allow = "cpqs01234567"; + u_int i, j = 0; - if (wp == NULL) - return; - state = options_get_number(global_options, "set-clipboard"); - if (state != 2) - return; + if (options_get_number(global_options, "set-clipboard") != 2) + return (0); if ((end = strchr(p, ';')) == NULL) - return; + return (0); end++; if (*end == '\0') - return; + return (0); log_debug("%s: %s", __func__, end); for (i = 0; p + i != end; i++) { @@ -3108,25 +3107,53 @@ input_osc_52(struct input_ctx *ictx, const char *p) if (strcmp(end, "?") == 0) { input_osc_52_reply(ictx); - return; + return (0); } len = (strlen(end) / 4) * 3; if (len == 0) - return; + return (0); - out = xmalloc(len); - if ((outlen = b64_pton(end, out, len)) == -1) { - free(out); - return; + *out = xmalloc(len); + if ((*outlen = b64_pton(end, *out, len)) == -1) { + free(*out); + *out = NULL; + return (0); } - screen_write_start_pane(&ctx, wp, NULL); - screen_write_setselection(&ctx, flags, out, outlen); - screen_write_stop(&ctx); - notify_pane("pane-set-clipboard", wp); + return (1); +} - paste_add(NULL, out, outlen); +/* Handle the OSC 52 sequence for setting the clipboard. */ +static void +input_osc_52(struct input_ctx *ictx, const char *p) +{ + struct window_pane *wp = ictx->wp; + struct screen_write_ctx ctx; + u_char *out; + int outlen; + char flags[sizeof "cpqs01234567"] = ""; + + if (!input_osc_52_parse(ictx, p, &out, &outlen, flags)) + return; + + if (wp == NULL) { + /* Popup window. */ + if (ictx->c == NULL) { + free(out); + return; + } + tty_set_selection(&ictx->c->tty, flags, out, outlen); + paste_add(NULL, out, outlen); + } else { + /* Normal window. */ + screen_write_start_pane(&ctx, wp, NULL); + screen_write_setselection(&ctx, flags, out, outlen); + screen_write_stop(&ctx); + notify_pane("pane-set-clipboard", wp); + paste_add(NULL, out, outlen); + } + free(out); } /* Handle the OSC 104 sequence for unsetting (multiple) palette entries. */ diff --git a/popup.c b/popup.c index 70f4def4..04584b80 100644 --- a/popup.c +++ b/popup.c @@ -851,7 +851,7 @@ popup_display(int flags, enum box_lines lines, struct cmdq_item *item, u_int px, pd->job = job_run(shellcmd, argc, argv, env, s, cwd, popup_job_update_cb, popup_job_complete_cb, NULL, pd, JOB_NOWAIT|JOB_PTY|JOB_KEEPWRITE|JOB_DEFAULTSHELL, jx, jy); - pd->ictx = input_init(NULL, job_get_event(pd->job), &pd->palette); + pd->ictx = input_init(NULL, job_get_event(pd->job), &pd->palette, c); server_client_set_overlay(c, 0, popup_check_cb, popup_mode_cb, popup_draw_cb, popup_key_cb, popup_free_cb, popup_resize_cb, pd); diff --git a/tmux.h b/tmux.h index 34958d4b..0f84398b 100644 --- a/tmux.h +++ b/tmux.h @@ -3011,7 +3011,7 @@ void recalculate_sizes_now(int); /* input.c */ #define INPUT_BUF_DEFAULT_SIZE 1048576 struct input_ctx *input_init(struct window_pane *, struct bufferevent *, - struct colour_palette *); + struct colour_palette *, struct client *); void input_free(struct input_ctx *); void input_reset(struct input_ctx *, int); struct evbuffer *input_pending(struct input_ctx *); diff --git a/window-copy.c b/window-copy.c index 3a147c85..c372c4b1 100644 --- a/window-copy.c +++ b/window-copy.c @@ -494,7 +494,7 @@ window_copy_view_init(struct window_mode_entry *wme, data->backing = xmalloc(sizeof *data->backing); screen_init(data->backing, sx, screen_size_y(base), UINT_MAX); - data->ictx = input_init(NULL, NULL, NULL); + data->ictx = input_init(NULL, NULL, NULL, NULL); data->mx = data->cx; data->my = screen_hsize(data->backing) + data->cy - data->oy; data->showmark = 0; diff --git a/window.c b/window.c index 9581c64f..31a8a8ee 100644 --- a/window.c +++ b/window.c @@ -1057,7 +1057,7 @@ window_pane_set_event(struct window_pane *wp) NULL, window_pane_error_callback, wp); if (wp->event == NULL) fatalx("out of memory"); - wp->ictx = input_init(wp, wp->event, &wp->palette); + wp->ictx = input_init(wp, wp->event, &wp->palette, NULL); bufferevent_enable(wp->event, EV_READ|EV_WRITE); } From 5865001e4a1e524c492d614a5dad6e9701a91926 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 6 Feb 2026 10:23:26 +0000 Subject: [PATCH 03/20] Also check PANE_STATUSREADY for pane_dead format to match pane_dead_status. GitHub issue 4841 from Joshua Pollack. --- format.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/format.c b/format.c index 5804869d..96b6585b 100644 --- a/format.c +++ b/format.c @@ -1998,8 +1998,10 @@ format_cb_pane_bottom(struct format_tree *ft) static void * format_cb_pane_dead(struct format_tree *ft) { - if (ft->wp != NULL) { - if (ft->wp->fd == -1) + struct window_pane *wp = ft->wp; + + if (wp != NULL) { + if (wp->fd == -1 && (wp->flags & PANE_STATUSREADY)) return (xstrdup("1")); return (xstrdup("0")); } From 7b1c503086d8e2911f62a4e4475c715c0bb6a0d7 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 6 Feb 2026 10:28:42 +0000 Subject: [PATCH 04/20] Clear search counts when clearing marks in case of repeated search, reported by Daniel Pereira in GitHub issue 4817. --- window-copy.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/window-copy.c b/window-copy.c index c372c4b1..04f62643 100644 --- a/window-copy.c +++ b/window-copy.c @@ -4271,6 +4271,9 @@ window_copy_clear_marks(struct window_mode_entry *wme) { struct window_copy_mode_data *data = wme->data; + data->searchcount = -1; + data->searchmore = 0; + free(data->searchmark); data->searchmark = NULL; } From f016e0815397e4b88252c9ed7475df9acac97413 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 10 Feb 2026 08:27:17 +0000 Subject: [PATCH 05/20] Fix wrong TAILQ member in input_cancel_requests from Conor Taylor in GitHub issue 4848. --- input.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input.c b/input.c index c8ae3544..ee92a7cf 100644 --- a/input.c +++ b/input.c @@ -3424,7 +3424,7 @@ input_cancel_requests(struct client *c) { struct input_request *ir, *ir1; - TAILQ_FOREACH_SAFE(ir, &c->input_requests, entry, ir1) + TAILQ_FOREACH_SAFE(ir, &c->input_requests, centry, ir1) input_free_request(ir); } From 19b9a34c4863e668c1221ab6bb6d36967dda0b1e Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 10 Feb 2026 08:28:53 +0000 Subject: [PATCH 06/20] Only loop over clients if table actually found, from Conor Taylor in GitHub issue 4848. --- key-bindings.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/key-bindings.c b/key-bindings.c index 5e6449b7..d30c3cdb 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -297,12 +297,12 @@ key_bindings_remove_table(const char *name) table = key_bindings_get_table(name, 0); if (table != NULL) { RB_REMOVE(key_tables, &key_tables, table); + TAILQ_FOREACH(c, &clients, entry) { + if (c->keytable == table) + server_client_set_key_table(c, NULL); + } key_bindings_unref_table(table); } - TAILQ_FOREACH(c, &clients, entry) { - if (c->keytable == table) - server_client_set_key_table(c, NULL); - } } void From b7939eb26693a462672d96ff2ce0659866d569ed Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 10 Feb 2026 08:30:21 +0000 Subject: [PATCH 07/20] Don't call event_add again if the event is already pending, from Conor Taylor in GitHub issue 4848. --- tty.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tty.c b/tty.c index 6e413572..5325f38c 100644 --- a/tty.c +++ b/tty.c @@ -628,7 +628,8 @@ tty_add(struct tty *tty, const char *buf, size_t len) if (tty_log_fd != -1) write(tty_log_fd, buf, len); - if (tty->flags & TTY_STARTED) + if ((tty->flags & TTY_STARTED) && + !event_pending(&tty->event_out, EV_WRITE, NULL)) event_add(&tty->event_out, NULL); } From d8794e2b30f92db606a9f6e80316696d90240793 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 10 Feb 2026 08:31:45 +0000 Subject: [PATCH 08/20] Check log level before log_debug in tty_draw_line, from Conor Taylor in GitHub issue 4848. --- tty-draw.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tty-draw.c b/tty-draw.c index c506853d..eaf56d2b 100644 --- a/tty-draw.c +++ b/tty-draw.c @@ -262,10 +262,12 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, else next_state = TTY_DRAW_LINE_NEW1; } - log_debug("%s: cell %u empty %u, bg %u; state: current %s, " - "next %s", __func__, px + i, empty, gcp->bg, - tty_draw_line_states[current_state], - tty_draw_line_states[next_state]); + if (log_get_level() != 0) { + log_debug("%s: cell %u empty %u, bg %u; state: " + "current %s, next %s", __func__, px + i, empty, + gcp->bg, tty_draw_line_states[current_state], + tty_draw_line_states[next_state]); + } /* If the state has changed, flush any collected data. */ if (next_state != current_state) { From aa03706ed01abaa6c793ad90c73b48783a074bb1 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 10 Feb 2026 08:34:43 +0000 Subject: [PATCH 09/20] Remove redundant call to tty_attributes, from Conor Taylor in GitHub issue 4848. --- tty.c | 1 - 1 file changed, 1 deletion(-) diff --git a/tty.c b/tty.c index 5325f38c..29369598 100644 --- a/tty.c +++ b/tty.c @@ -2064,7 +2064,6 @@ tty_cell(struct tty *tty, const struct grid_cell *gc, /* If it is a single character, write with putc to handle ACS. */ if (gcp->data.size == 1) { - tty_attributes(tty, gcp, defaults, palette, hl); if (*gcp->data.data < 0x20 || *gcp->data.data == 0x7f) return; tty_putc(tty, *gcp->data.data); From 1cf17b06aee30ab64cc2e90617099d63a30f88eb Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 10 Feb 2026 08:40:03 +0000 Subject: [PATCH 10/20] Batch printable output in control mode, from Conor Taylor in GitHub issue 4848. --- control.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/control.c b/control.c index 8b44a274..c02b2041 100644 --- a/control.c +++ b/control.c @@ -611,7 +611,7 @@ control_append_data(struct client *c, struct control_pane *cp, uint64_t age, struct evbuffer *message, struct window_pane *wp, size_t size) { u_char *new_data; - size_t new_size; + size_t new_size, start; u_int i; if (message == NULL) { @@ -630,10 +630,16 @@ control_append_data(struct client *c, struct control_pane *cp, uint64_t age, if (new_size < size) fatalx("not enough data: %zu < %zu", new_size, size); for (i = 0; i < size; i++) { - if (new_data[i] < ' ' || new_data[i] == '\\') + if (new_data[i] < ' ' || new_data[i] == '\\') { evbuffer_add_printf(message, "\\%03o", new_data[i]); - else - evbuffer_add_printf(message, "%c", new_data[i]); + } else { + start = i; + while (i + 1 < size && + new_data[i + 1] >= ' ' && + new_data[i + 1] != '\\') + i++; + evbuffer_add(message, new_data + start, i - start + 1); + } } window_pane_update_used_data(wp, &cp->offset, size); return (message); From 5a33616e650458ab392ab32c7e38fb3bf49d933f Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 10 Feb 2026 09:00:30 +0000 Subject: [PATCH 11/20] Check for no window when updating clients, GitHub issue 4851. --- server-client.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server-client.c b/server-client.c index c2044398..db5c02ff 100644 --- a/server-client.c +++ b/server-client.c @@ -2723,7 +2723,8 @@ server_client_loop(void) TAILQ_FOREACH(wp, &w->panes, entry) { if (wp->flags & PANE_STYLECHANGED) { wme = TAILQ_FIRST(&wp->modes); - if (wme != NULL && wme->mode->style_changed != NULL) + if (wme != NULL && + wme->mode->style_changed != NULL) wme->mode->style_changed(wme); } } @@ -2732,7 +2733,7 @@ server_client_loop(void) /* Check clients. */ TAILQ_FOREACH(c, &clients, entry) { server_client_check_exit(c); - if (c->session != NULL) { + if (c->session != NULL && c->session->curw != NULL) { server_client_check_modes(c); server_client_check_redraw(c); server_client_reset_state(c); From 25f6d8b1e9b23ee367af94b3a548b4f3da8aa04b Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 10 Feb 2026 09:55:53 +0000 Subject: [PATCH 12/20] Implement some obvious missing sort orders, from Dane Jensen in GitHub issue 4813. --- sort.c | 40 +++++++++++++++++++++++++++------------- tmux.1 | 10 +++++++++- tmux.h | 1 + window.c | 3 +++ 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/sort.c b/sort.c index a1d85891..fb3f89ee 100644 --- a/sort.c +++ b/sort.c @@ -197,23 +197,25 @@ sort_pane_cmp(const void *a0, const void *b0) case SORT_CREATION: result = a->id - b->id; break; - case SORT_INDEX: - case SORT_NAME: - case SORT_ORDER: case SORT_SIZE: - case SORT_END: + result = a->sx * a->sy - b->sx * b->sy; break; - } - if (result == 0) { - /* - * Panes don't have names, so use number order for any other - * sort field. - */ + case SORT_INDEX: window_pane_index(a, &ai); window_pane_index(b, &bi); result = ai - bi; + break; + case SORT_NAME: + result = strcmp(a->screen->title, b->screen->title); + break; + case SORT_ORDER: + case SORT_END: + break; } + if (result == 0) + result = strcmp(a->screen->title, b->screen->title); + if (sort_crit->reversed) result = -result; return (result); @@ -235,6 +237,16 @@ sort_winlink_cmp(const void *a0, const void *b0) case SORT_INDEX: result = wla->idx - wlb->idx; break; + case SORT_CREATION: + if (timercmp(&wa->creation_time, &wb->creation_time, >)) { + result = -1; + break; + } + if (timercmp(&wa->creation_time, &wb->creation_time, <)) { + result = 1; + break; + } + break; case SORT_ACTIVITY: if (timercmp(&wa->activity_time, &wb->activity_time, >)) { result = -1; @@ -248,9 +260,10 @@ sort_winlink_cmp(const void *a0, const void *b0) case SORT_NAME: result = strcmp(wa->name, wb->name); break; - case SORT_CREATION: - case SORT_ORDER: case SORT_SIZE: + result = wa->sx * wa->sy - wb->sx * wb->sy; + break; + case SORT_ORDER: case SORT_END: break; } @@ -295,7 +308,8 @@ sort_order_from_string(const char* order) return (SORT_CREATION); if (strcasecmp(order, "index") == 0) return (SORT_INDEX); - if (strcasecmp(order, "name") == 0) + if (strcasecmp(order, "name") == 0 || + strcasecmp(order, "title") == 0) return (SORT_NAME); if (strcasecmp(order, "order") == 0) return (SORT_ORDER); diff --git a/tmux.1 b/tmux.1 index 48751888..2229fc95 100644 --- a/tmux.1 +++ b/tmux.1 @@ -3090,7 +3090,11 @@ See the section. .Fl O specifies the sort order: one of -.Ql name , +.Ql name +(title), +.Ql index , +.Ql size +(area), .Ql creation (time), or .Ql activity @@ -3123,6 +3127,10 @@ section. specifies the sort order: one of .Ql index , .Ql name , +.Ql size +(area), +.Ql creation +(time), or .Ql activity (time). diff --git a/tmux.h b/tmux.h index 0f84398b..fdbf55c1 100644 --- a/tmux.h +++ b/tmux.h @@ -1254,6 +1254,7 @@ struct window { struct event offset_timer; struct timeval activity_time; + struct timeval creation_time; struct window_pane *active; struct window_panes last_panes; diff --git a/window.c b/window.c index 31a8a8ee..60f2204a 100644 --- a/window.c +++ b/window.c @@ -330,6 +330,9 @@ window_create(u_int sx, u_int sy, u_int xpixel, u_int ypixel) RB_INSERT(windows, &windows, w); window_set_fill_character(w); + + if (gettimeofday(&w->creation_time, NULL) != 0) + fatal("gettimeofday failed"); window_update_activity(w); log_debug("%s: @%u create %ux%u (%ux%u)", __func__, w->id, sx, sy, From 7d41761e8441d38eea9d752d4c12ccdfeeb9991b Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 10 Feb 2026 10:02:11 +0000 Subject: [PATCH 13/20] Fix clients_calculate_size for manual type when window is NULL. From Elias Oenal in GitHub issue 4849. --- resize.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resize.c b/resize.c index 9a61c300..6cbc938d 100644 --- a/resize.c +++ b/resize.c @@ -250,7 +250,7 @@ skip: /* Return whether a suitable size was found. */ if (type == WINDOW_SIZE_MANUAL) { log_debug("%s: type is manual", __func__); - return (1); + return (w != NULL); } if (type == WINDOW_SIZE_LARGEST) { log_debug("%s: type is largest", __func__); From 5b455abecc334b614990052506373d01f59cec04 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 11 Feb 2026 08:23:21 +0000 Subject: [PATCH 14/20] Do not write before buffer when parsing empty clipboard or palette replies, or try to allocate zero bytes with an empty clipboard sequence. Reported by DongHan Kim. --- tty-keys.c | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tty-keys.c b/tty-keys.c index e3e20eec..361de3df 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1377,6 +1377,10 @@ tty_keys_clipboard(struct tty *tty, const char *buf, size_t len, size_t *size) /* Convert from base64. */ needed = (end / 4) * 3; + if (needed == 0) { + free(copy); + return (0); + } out = xmalloc(needed); if ((outlen = b64_pton(copy, out, len)) == -1) { free(out); @@ -1612,8 +1616,10 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, } if (i == (sizeof tmp) - 1) return (-1); - tmp[i - 1] = '\0'; *size = 5 + i; + if (i == 0) + return (0); + tmp[i - 1] = '\0'; /* Add terminal features. */ if (strncmp(tmp, "iTerm2 ", 7) == 0) @@ -1686,11 +1692,13 @@ tty_keys_colours(struct tty *tty, const char *buf, size_t len, size_t *size, } if (i == (sizeof tmp) - 1) return (-1); + *size = 6 + i; + if (i == 0) + return (0); if (tmp[i - 1] == '\033') tmp[i - 1] = '\0'; else tmp[i] = '\0'; - *size = 6 + i; /* Work out the colour. */ n = colour_parseX11(tmp); @@ -1755,11 +1763,13 @@ tty_keys_palette(struct tty *tty, const char *buf, size_t len, size_t *size) } if (i == (sizeof tmp) - 1) return (-1); + *size = 5 + i; + if (i == 0) + return (0); if (tmp[i - 1] == '\033') tmp[i - 1] = '\0'; else tmp[i] = '\0'; - *size = 5 + i; /* Parse index. */ idx = strtol(tmp, &endptr, 10); From 7e50eb0e835cc052a5fdda1765b101a80c07a7d9 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 11 Feb 2026 08:30:37 +0000 Subject: [PATCH 15/20] Make paste_get_top return a copy of the buffer name which is more sensible and avoids a double free pointed out by DongHan Kim. --- cmd-set-buffer.c | 40 +++++++++++++++++++++------------------- paste.c | 4 ++-- tmux.h | 2 +- window-copy.c | 5 +++-- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/cmd-set-buffer.c b/cmd-set-buffer.c index 0b0ec3a2..5b66dbee 100644 --- a/cmd-set-buffer.c +++ b/cmd-set-buffer.c @@ -58,29 +58,31 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) struct args *args = cmd_get_args(self); struct client *tc = cmdq_get_target_client(item); struct paste_buffer *pb; - char *bufdata, *cause; - const char *bufname, *olddata; - size_t bufsize, newsize; + char *bufname, *bufdata = NULL, *cause = NULL; + const char *olddata; + size_t bufsize = 0, newsize; - bufname = args_get(args, 'b'); - if (bufname == NULL) + if (args_get(args, 'b') == NULL) pb = NULL; - else + else { + bufname = xstrdup(args_get(args, 'b')); pb = paste_get_name(bufname); + } if (cmd_get_entry(self) == &cmd_delete_buffer_entry) { if (pb == NULL) { if (bufname != NULL) { cmdq_error(item, "unknown buffer: %s", bufname); - return (CMD_RETURN_ERROR); + goto fail; } pb = paste_get_top(&bufname); } if (pb == NULL) { cmdq_error(item, "no buffer"); - return (CMD_RETURN_ERROR); + goto fail; } paste_free(pb); + free(bufname); return (CMD_RETURN_NORMAL); } @@ -88,32 +90,28 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) if (pb == NULL) { if (bufname != NULL) { cmdq_error(item, "unknown buffer: %s", bufname); - return (CMD_RETURN_ERROR); + goto fail; } pb = paste_get_top(&bufname); } if (pb == NULL) { cmdq_error(item, "no buffer"); - return (CMD_RETURN_ERROR); + goto fail; } if (paste_rename(bufname, args_get(args, 'n'), &cause) != 0) { cmdq_error(item, "%s", cause); - free(cause); - return (CMD_RETURN_ERROR); + goto fail; } return (CMD_RETURN_NORMAL); } if (args_count(args) != 1) { cmdq_error(item, "no data specified"); - return (CMD_RETURN_ERROR); + goto fail; } if ((newsize = strlen(args_string(args, 0))) == 0) return (CMD_RETURN_NORMAL); - bufsize = 0; - bufdata = NULL; - if (args_has(args, 'a') && pb != NULL) { olddata = paste_buffer_data(pb, &bufsize); bufdata = xmalloc(bufsize); @@ -126,12 +124,16 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) if (paste_set(bufdata, bufsize, bufname, &cause) != 0) { cmdq_error(item, "%s", cause); - free(bufdata); - free(cause); - return (CMD_RETURN_ERROR); + goto fail; } if (args_has(args, 'w') && tc != NULL) tty_set_selection(&tc->tty, "", bufdata, bufsize); return (CMD_RETURN_NORMAL); + +fail: + free(bufdata); + free(bufname); + free(cause); + return (CMD_RETURN_ERROR); } diff --git a/paste.c b/paste.c index a35bc4b3..509e2aa3 100644 --- a/paste.c +++ b/paste.c @@ -107,7 +107,7 @@ paste_is_empty(void) /* Get the most recent automatic buffer. */ struct paste_buffer * -paste_get_top(const char **name) +paste_get_top(char **name) { struct paste_buffer *pb; @@ -117,7 +117,7 @@ paste_get_top(const char **name) if (pb == NULL) return (NULL); if (name != NULL) - *name = pb->name; + *name = xstrdup(pb->name); return (pb); } diff --git a/tmux.h b/tmux.h index fdbf55c1..4c40cf83 100644 --- a/tmux.h +++ b/tmux.h @@ -2320,7 +2320,7 @@ time_t paste_buffer_created(struct paste_buffer *); const char *paste_buffer_data(struct paste_buffer *, size_t *); struct paste_buffer *paste_walk(struct paste_buffer *); int paste_is_empty(void); -struct paste_buffer *paste_get_top(const char **); +struct paste_buffer *paste_get_top(char **); struct paste_buffer *paste_get_name(const char *); void paste_free(struct paste_buffer *); void paste_add(const char *, char *, size_t); diff --git a/window-copy.c b/window-copy.c index 04f62643..f3a8507d 100644 --- a/window-copy.c +++ b/window-copy.c @@ -5052,9 +5052,9 @@ static void window_copy_append_selection(struct window_mode_entry *wme) { struct window_pane *wp = wme->wp; - char *buf; + char *buf, *bufname = NULL; struct paste_buffer *pb; - const char *bufdata, *bufname = NULL; + const char *bufdata; size_t len, bufsize; struct screen_write_ctx ctx; @@ -5079,6 +5079,7 @@ window_copy_append_selection(struct window_mode_entry *wme) } if (paste_set(buf, len, bufname, NULL) != 0) free(buf); + free(bufname); } static void From 1f0c54f7eaf33b892e1e1e6ad1d0274cc11c21a0 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 15 Feb 2026 17:43:26 +0000 Subject: [PATCH 16/20] Initialize bufname, reported by Mark Kelly. --- cmd-set-buffer.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cmd-set-buffer.c b/cmd-set-buffer.c index 5b66dbee..eef2978e 100644 --- a/cmd-set-buffer.c +++ b/cmd-set-buffer.c @@ -57,14 +57,12 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) { struct args *args = cmd_get_args(self); struct client *tc = cmdq_get_target_client(item); - struct paste_buffer *pb; - char *bufname, *bufdata = NULL, *cause = NULL; + struct paste_buffer *pb = NULL; + char *bufname = NULL, *bufdata = NULL, *cause = NULL; const char *olddata; size_t bufsize = 0, newsize; - if (args_get(args, 'b') == NULL) - pb = NULL; - else { + if (args_get(args, 'b') != NULL) { bufname = xstrdup(args_get(args, 'b')); pb = paste_get_name(bufname); } From f2184639762abed3887058416d10918e1366cb82 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 16 Feb 2026 08:02:04 +0000 Subject: [PATCH 17/20] grid_peek_cell can return NULL, so check for it. From Conor Taylor in GitHub issue 4848. --- grid.c | 6 +++++- window-copy.c | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/grid.c b/grid.c index c783f7b7..258561ce 100644 --- a/grid.c +++ b/grid.c @@ -1089,12 +1089,16 @@ grid_string_cells(struct grid *gd, u_int px, u_int py, u_int nx, off = 0; gl = grid_peek_line(gd, py); + if (gl == NULL) { + buf[0] = '\0'; + return (buf); + } if (flags & GRID_STRING_EMPTY_CELLS) end = gl->cellsize; else end = gl->cellused; for (xx = px; xx < px + nx; xx++) { - if (gl == NULL || xx >= end) + if (xx >= end) break; grid_get_cell(gd, xx, py, &gc); if (gc.flags & GRID_FLAG_PADDING) diff --git a/window-copy.c b/window-copy.c index f3a8507d..c4f10b37 100644 --- a/window-copy.c +++ b/window-copy.c @@ -355,7 +355,7 @@ window_copy_clone_screen(struct screen *src, struct screen *hint, u_int *cx, if (trim) { while (sy > screen_hsize(src)) { gl = grid_peek_line(src->grid, sy - 1); - if (gl->cellused != 0) + if (gl == NULL || gl->cellused != 0) break; sy--; } @@ -3625,6 +3625,10 @@ window_copy_stringify(struct grid *gd, u_int py, u_int first, u_int last, buf = xrealloc(buf, bufsize); gl = grid_peek_line(gd, py); + if (gl == NULL) { + buf[*size - 1] = '\0'; + return (buf); + } bx = *size - 1; for (ax = first; ax < last; ax++) { d = window_copy_cellstring(gl, ax, &dlen, &allocated); @@ -3670,6 +3674,10 @@ window_copy_cstrtocellpos(struct grid *gd, u_int ncells, u_int *ppx, u_int *ppy, px = *ppx; pywrap = *ppy; gl = grid_peek_line(gd, pywrap); + if (gl == NULL) { + free(cells); + return; + } while (cell < ncells) { cells[cell].d = window_copy_cellstring(gl, px, &cells[cell].dlen, &cells[cell].allocated); @@ -3679,6 +3687,8 @@ window_copy_cstrtocellpos(struct grid *gd, u_int ncells, u_int *ppx, u_int *ppy, px = 0; pywrap++; gl = grid_peek_line(gd, pywrap); + if (gl == NULL) + break; } } @@ -4080,7 +4090,7 @@ window_copy_visible_lines(struct window_copy_mode_data *data, u_int *start, for (*start = gd->hsize - data->oy; *start > 0; (*start)--) { gl = grid_peek_line(gd, (*start) - 1); - if (~gl->flags & GRID_LINE_WRAPPED) + if (gl == NULL || ~gl->flags & GRID_LINE_WRAPPED) break; } *end = gd->hsize - data->oy + gd->sy; From c9162837a09b34d47ada24c3be00f7af080bbb16 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 16 Feb 2026 08:42:57 +0000 Subject: [PATCH 18/20] Pull format allocation outside of loop for control subs, from Conor Taylor in GitHub issue 4848. --- control.c | 190 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 116 insertions(+), 74 deletions(-) diff --git a/control.c b/control.c index c02b2041..05fed7c0 100644 --- a/control.c +++ b/control.c @@ -847,15 +847,13 @@ control_stop(struct client *c) /* Check session subscription. */ static void -control_check_subs_session(struct client *c, struct control_sub *csub) +control_check_subs_session(struct client *c, struct control_sub *csub, + struct format_tree *ft) { struct session *s = c->session; - struct format_tree *ft; char *value; - ft = format_create_defaults(NULL, c, s, NULL, NULL); value = format_expand(ft, csub->format); - format_free(ft); if (csub->last != NULL && strcmp(value, csub->last) == 0) { free(value); @@ -916,48 +914,38 @@ control_check_subs_pane(struct client *c, struct control_sub *csub) } } -/* Check all panes subscription. */ +/* Check all-panes subscription for a pane. */ static void -control_check_subs_all_panes(struct client *c, struct control_sub *csub) +control_check_subs_all_panes_one(struct client *c, struct control_sub *csub, + struct format_tree *ft, struct winlink *wl, struct window_pane *wp) { struct session *s = c->session; - struct window_pane *wp; - struct window *w; - struct winlink *wl; - struct format_tree *ft; + struct window *w = wl->window; char *value; struct control_sub_pane *csp, find; - RB_FOREACH(wl, winlinks, &s->windows) { - w = wl->window; - TAILQ_FOREACH(wp, &w->panes, entry) { - ft = format_create_defaults(NULL, c, s, wl, wp); - value = format_expand(ft, csub->format); - format_free(ft); + value = format_expand(ft, csub->format); - find.pane = wp->id; - find.idx = wl->idx; + find.pane = wp->id; + find.idx = wl->idx; - csp = RB_FIND(control_sub_panes, &csub->panes, &find); - if (csp == NULL) { - csp = xcalloc(1, sizeof *csp); - csp->pane = wp->id; - csp->idx = wl->idx; - RB_INSERT(control_sub_panes, &csub->panes, csp); - } - - if (csp->last != NULL && - strcmp(value, csp->last) == 0) { - free(value); - continue; - } - control_write(c, - "%%subscription-changed %s $%u @%u %u %%%u : %s", - csub->name, s->id, w->id, wl->idx, wp->id, value); - free(csp->last); - csp->last = value; - } + csp = RB_FIND(control_sub_panes, &csub->panes, &find); + if (csp == NULL) { + csp = xcalloc(1, sizeof *csp); + csp->pane = wp->id; + csp->idx = wl->idx; + RB_INSERT(control_sub_panes, &csub->panes, csp); } + + if (csp->last != NULL && strcmp(value, csp->last) == 0) { + free(value); + return; + } + control_write(c, + "%%subscription-changed %s $%u @%u %u %%%u : %s", + csub->name, s->id, w->id, wl->idx, wp->id, value); + free(csp->last); + csp->last = value; } /* Check window subscription. */ @@ -1006,45 +994,38 @@ control_check_subs_window(struct client *c, struct control_sub *csub) } } -/* Check all windows subscription. */ +/* Check all-windows subscription for a window. */ static void -control_check_subs_all_windows(struct client *c, struct control_sub *csub) +control_check_subs_all_windows_one(struct client *c, struct control_sub *csub, + struct format_tree *ft, struct winlink *wl) { struct session *s = c->session; - struct window *w; - struct winlink *wl; - struct format_tree *ft; + struct window *w = wl->window; char *value; struct control_sub_window *csw, find; - RB_FOREACH(wl, winlinks, &s->windows) { - w = wl->window; + value = format_expand(ft, csub->format); - ft = format_create_defaults(NULL, c, s, wl, NULL); - value = format_expand(ft, csub->format); - format_free(ft); + find.window = w->id; + find.idx = wl->idx; - find.window = w->id; - find.idx = wl->idx; - - csw = RB_FIND(control_sub_windows, &csub->windows, &find); - if (csw == NULL) { - csw = xcalloc(1, sizeof *csw); - csw->window = w->id; - csw->idx = wl->idx; - RB_INSERT(control_sub_windows, &csub->windows, csw); - } - - if (csw->last != NULL && strcmp(value, csw->last) == 0) { - free(value); - continue; - } - control_write(c, - "%%subscription-changed %s $%u @%u %u - : %s", - csub->name, s->id, w->id, wl->idx, value); - free(csw->last); - csw->last = value; + csw = RB_FIND(control_sub_windows, &csub->windows, &find); + if (csw == NULL) { + csw = xcalloc(1, sizeof *csw); + csw->window = w->id; + csw->idx = wl->idx; + RB_INSERT(control_sub_windows, &csub->windows, csw); } + + if (csw->last != NULL && strcmp(value, csw->last) == 0) { + free(value); + return; + } + control_write(c, + "%%subscription-changed %s $%u @%u %u - : %s", + csub->name, s->id, w->id, wl->idx, value); + free(csw->last); + csw->last = value; } /* Check subscriptions timer. */ @@ -1054,30 +1035,91 @@ control_check_subs_timer(__unused int fd, __unused short events, void *data) struct client *c = data; struct control_state *cs = c->control_state; struct control_sub *csub, *csub1; + struct session *s = c->session; + struct format_tree *ft; + struct winlink *wl; + struct window_pane *wp; struct timeval tv = { .tv_sec = 1 }; + int have_session = 0, have_all_panes = 0; + int have_all_windows = 0; log_debug("%s: timer fired", __func__); evtimer_add(&cs->subs_timer, &tv); - RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) { + /* Find which subscription types are present. */ + RB_FOREACH(csub, control_subs, &cs->subs) { switch (csub->type) { case CONTROL_SUB_SESSION: - control_check_subs_session(c, csub); - break; - case CONTROL_SUB_PANE: - control_check_subs_pane(c, csub); + have_session = 1; break; case CONTROL_SUB_ALL_PANES: - control_check_subs_all_panes(c, csub); + have_all_panes = 1; + break; + case CONTROL_SUB_ALL_WINDOWS: + have_all_windows = 1; + break; + default: + break; + } + } + + /* Check session subscriptions. */ + if (have_session) { + ft = format_create_defaults(NULL, c, s, NULL, NULL); + RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) { + if (csub->type == CONTROL_SUB_SESSION) + control_check_subs_session(c, csub, ft); + } + format_free(ft); + } + + /* Check pane and window subscriptions. */ + RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) { + switch (csub->type) { + case CONTROL_SUB_PANE: + control_check_subs_pane(c, csub); break; case CONTROL_SUB_WINDOW: control_check_subs_window(c, csub); break; + case CONTROL_SUB_SESSION: + case CONTROL_SUB_ALL_PANES: case CONTROL_SUB_ALL_WINDOWS: - control_check_subs_all_windows(c, csub); break; } } + + /* Check all-panes subscriptions. */ + if (have_all_panes) { + RB_FOREACH(wl, winlinks, &s->windows) { + TAILQ_FOREACH(wp, &wl->window->panes, entry) { + ft = format_create_defaults(NULL, c, s, wl, wp); + RB_FOREACH_SAFE(csub, control_subs, &cs->subs, + csub1) { + if (csub->type != CONTROL_SUB_ALL_PANES) + continue; + control_check_subs_all_panes_one(c, + csub, ft, wl, wp); + } + format_free(ft); + } + } + } + + /* Check all-windows subscriptions. */ + if (have_all_windows) { + RB_FOREACH(wl, winlinks, &s->windows) { + ft = format_create_defaults(NULL, c, s, wl, NULL); + RB_FOREACH_SAFE(csub, control_subs, &cs->subs, + csub1) { + if (csub->type != CONTROL_SUB_ALL_WINDOWS) + continue; + control_check_subs_all_windows_one(c, csub, ft, + wl); + } + format_free(ft); + } + } } /* Add a subscription. */ From 55cedc24fa6763be9f0c1d3e332c870cdb43c828 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 16 Feb 2026 08:45:38 +0000 Subject: [PATCH 19/20] Format layout change string once per window in control notify, from Conor Taylor in GitHub issue 4848. --- control-notify.c | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/control-notify.c b/control-notify.c index 30f94194..ba6a8355 100644 --- a/control-notify.c +++ b/control-notify.c @@ -51,29 +51,24 @@ control_notify_window_layout_changed(struct window *w) template = "%layout-change #{window_id} #{window_layout} " "#{window_visible_layout} #{window_raw_flags}"; + /* + * When the last pane in a window is closed it won't have a layout root + * and we don't need to inform the client about the layout change + * because the whole window will go away soon. + */ + wl = TAILQ_FIRST(&w->winlinks); + if (wl == NULL || w->layout_root == NULL) + return; + cp = format_single(NULL, template, NULL, NULL, wl, NULL); + TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL) continue; s = c->session; - - if (winlink_find_by_window_id(&s->windows, w->id) == NULL) - continue; - - /* - * When the last pane in a window is closed it won't have a - * layout root and we don't need to inform the client about the - * layout change because the whole window will go away soon. - */ - if (w->layout_root == NULL) - continue; - - wl = winlink_find_by_window(&s->windows, w); - if (wl != NULL) { - cp = format_single(NULL, template, c, NULL, wl, NULL); + if (winlink_find_by_window_id(&s->windows, w->id) != NULL) control_write(c, "%s", cp); - free(cp); - } } + free(cp); } void From fedd4440f0760ba55a0aff2f917ccc033b930ade Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 17 Feb 2026 07:42:58 +0000 Subject: [PATCH 20/20] Reuse extended entry when clearing RGB cell, to prevent memory growth when cells are repeatedly cleared (they are only compacted when scrolled off screen). From Michael K Darling in GitHub issue 4862. --- grid.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/grid.c b/grid.c index 258561ce..28a15d77 100644 --- a/grid.c +++ b/grid.c @@ -205,11 +205,17 @@ grid_clear_cell(struct grid *gd, u_int px, u_int py, u_int bg) struct grid_line *gl = &gd->linedata[py]; struct grid_cell_entry *gce = &gl->celldata[px]; struct grid_extd_entry *gee; + u_int old_offset = gce->offset; + int had_extd = (gce->flags & GRID_FLAG_EXTENDED); memcpy(gce, &grid_cleared_entry, sizeof *gce); if (bg != 8) { if (bg & COLOUR_FLAG_RGB) { - grid_get_extended_cell(gl, gce, gce->flags); + if (had_extd && old_offset < gl->extdsize) { + gce->flags |= GRID_FLAG_EXTENDED; + gce->offset = old_offset; + } else + grid_get_extended_cell(gl, gce, gce->flags); gee = grid_extended_cell(gl, gce, &grid_cleared_cell); gee->bg = bg; } else {