From f31a2d229c059664791736fba46c077764cb0bd2 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 27 Oct 2025 20:31:40 +0000 Subject: [PATCH 01/27] Do not play games with lines for view mode output since it stops them wrapping, GitHub issue 4462. --- window-copy.c | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/window-copy.c b/window-copy.c index f93e548b..09939d4c 100644 --- a/window-copy.c +++ b/window-copy.c @@ -234,7 +234,6 @@ struct window_copy_mode_data { struct screen *backing; int backing_written; /* backing display started */ - struct screen *writing; struct input_ctx *ictx; int viewmode; /* view mode entered */ @@ -492,8 +491,6 @@ 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->writing = xmalloc(sizeof *data->writing); - screen_init(data->writing, sx, screen_size_y(base), 0); data->ictx = input_init(NULL, NULL, NULL); data->mx = data->cx; data->my = screen_hsize(data->backing) + data->cy - data->oy; @@ -513,10 +510,6 @@ window_copy_free(struct window_mode_entry *wme) free(data->searchstr); free(data->jumpchar); - if (data->writing != NULL) { - screen_free(data->writing); - free(data->writing); - } if (data->ictx != NULL) input_free(data->ictx); screen_free(data->backing); @@ -553,22 +546,11 @@ window_copy_vadd(struct window_pane *wp, int parse, const char *fmt, va_list ap) struct window_mode_entry *wme = TAILQ_FIRST(&wp->modes); struct window_copy_mode_data *data = wme->data; struct screen *backing = data->backing; - struct screen *writing = data->writing; - struct screen_write_ctx writing_ctx, backing_ctx, ctx; + struct screen_write_ctx backing_ctx, ctx; struct grid_cell gc; u_int old_hsize, old_cy; - u_int sx = screen_size_x(backing); char *text; - if (parse) { - vasprintf(&text, fmt, ap); - screen_write_start(&writing_ctx, writing); - screen_write_reset(&writing_ctx); - input_parse_screen(data->ictx, writing, window_copy_init_ctx_cb, - data, text, strlen(text)); - free(text); - } - old_hsize = screen_hsize(data->backing); screen_write_start(&backing_ctx, backing); if (data->backing_written) { @@ -581,9 +563,12 @@ window_copy_vadd(struct window_pane *wp, int parse, const char *fmt, va_list ap) } else data->backing_written = 1; old_cy = backing->cy; - if (parse) - screen_write_fast_copy(&backing_ctx, writing, 0, 0, sx, 1); - else { + if (parse) { + vasprintf(&text, fmt, ap); + input_parse_screen(data->ictx, backing, window_copy_init_ctx_cb, + data, text, strlen(text)); + free(text); + } else { memcpy(&gc, &grid_default_cell, sizeof gc); screen_write_vnputs(&backing_ctx, 0, &gc, fmt, ap); } From aa420cd54a5813cefd4bda16c07a4ba379956034 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 28 Oct 2025 07:32:26 +0000 Subject: [PATCH 02/27] Tweak error messages so that file name isn't modified when we capitalize the first letter, from Pavel Roskin in GitHub issue 4666. --- cmd-load-buffer.c | 2 +- cmd-save-buffer.c | 2 +- cmd-source-file.c | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd-load-buffer.c b/cmd-load-buffer.c index 70fd7ed9..82ac2fd4 100644 --- a/cmd-load-buffer.c +++ b/cmd-load-buffer.c @@ -66,7 +66,7 @@ cmd_load_buffer_done(__unused struct client *c, const char *path, int error, return; if (error != 0) - cmdq_error(item, "%s: %s", path, strerror(error)); + cmdq_error(item, "%s: %s", strerror(error), path); else if (bsize != 0) { copy = xmalloc(bsize); memcpy(copy, bdata, bsize); diff --git a/cmd-save-buffer.c b/cmd-save-buffer.c index baaa3467..d4266231 100644 --- a/cmd-save-buffer.c +++ b/cmd-save-buffer.c @@ -66,7 +66,7 @@ cmd_save_buffer_done(__unused struct client *c, const char *path, int error, return; if (error != 0) - cmdq_error(item, "%s: %s", path, strerror(error)); + cmdq_error(item, "%s: %s", strerror(error), path); cmdq_continue(item); } diff --git a/cmd-source-file.c b/cmd-source-file.c index 3da3f0da..b46e6b94 100644 --- a/cmd-source-file.c +++ b/cmd-source-file.c @@ -115,7 +115,7 @@ cmd_source_file_done(struct client *c, const char *path, int error, return; if (error != 0) - cmdq_error(item, "%s: %s", path, strerror(error)); + cmdq_error(item, "%s: %s", strerror(error), path); else if (bsize != 0) { if (load_cfg_from_buffer(bdata, bsize, path, c, cdata->after, target, cdata->flags, &new_item) < 0) @@ -234,7 +234,7 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item) error = strerror(ENOMEM); else error = strerror(EINVAL); - cmdq_error(item, "%s: %s", path, error); + cmdq_error(item, "%s: %s", error, path); retval = CMD_RETURN_ERROR; } globfree(&g); From 7cbb96528ce9ec00b54f4e575b91c4336bf1db57 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 28 Oct 2025 09:01:12 +0000 Subject: [PATCH 03/27] Do not try to reflow if line is NULL. --- grid.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid.c b/grid.c index a0a52a0e..63eea68d 100644 --- a/grid.c +++ b/grid.c @@ -1304,7 +1304,7 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy, if (!wrapped || want != from->cellused || width == sx) break; } - if (lines == 0) + if (lines == 0 || from == NULL) return; /* From b4ba6e49af0459f0ea528df38c45795dbf313ee6 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 28 Oct 2025 10:51:30 +0000 Subject: [PATCH 04/27] Do not realpath() everything since it is pointless and breaks symlinks. GitHub issue 4427. --- cmd-source-file.c | 9 --------- tmux.c | 16 ++++++++-------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/cmd-source-file.c b/cmd-source-file.c index b46e6b94..8f8b2504 100644 --- a/cmd-source-file.c +++ b/cmd-source-file.c @@ -136,16 +136,7 @@ cmd_source_file_done(struct client *c, const char *path, int error, static void cmd_source_file_add(struct cmd_source_file_data *cdata, const char *path) { - char resolved[PATH_MAX]; - - if (realpath(path, resolved) == NULL) { - log_debug("%s: realpath(\"%s\") failed: %s", __func__, - path, strerror(errno)); - } else - path = resolved; - log_debug("%s: %s", __func__, path); - cdata->files = xreallocarray(cdata->files, cdata->nfiles + 1, sizeof *cdata->files); cdata->files[cdata->nfiles++] = xstrdup(path); diff --git a/tmux.c b/tmux.c index 28cb9f70..261cfd57 100644 --- a/tmux.c +++ b/tmux.c @@ -143,7 +143,7 @@ expand_path(const char *path, const char *home) } static void -expand_paths(const char *s, char ***paths, u_int *n, int ignore_errors) +expand_paths(const char *s, char ***paths, u_int *n, int no_realpath) { const char *home = find_home(); char *copy, *next, *tmp, resolved[PATH_MAX], *expanded; @@ -160,15 +160,15 @@ expand_paths(const char *s, char ***paths, u_int *n, int ignore_errors) log_debug("%s: invalid path: %s", __func__, next); continue; } - if (realpath(expanded, resolved) == NULL) { - log_debug("%s: realpath(\"%s\") failed: %s", __func__, - expanded, strerror(errno)); - if (ignore_errors) { + if (no_realpath) + path = expanded; + else { + if (realpath(expanded, resolved) == NULL) { + log_debug("%s: realpath(\"%s\") failed: %s", __func__, + expanded, strerror(errno)); free(expanded); continue; } - path = expanded; - } else { path = xstrdup(resolved); free(expanded); } @@ -200,7 +200,7 @@ make_label(const char *label, char **cause) label = "default"; uid = getuid(); - expand_paths(TMUX_SOCK, &paths, &n, 1); + expand_paths(TMUX_SOCK, &paths, &n, 0); if (n == 0) { xasprintf(cause, "no suitable socket path"); return (NULL); From 9bf8ca58567314d02f2d3f9910613ca8eb2ce584 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 28 Oct 2025 14:21:06 +0000 Subject: [PATCH 05/27] Support case insensitive search in modes in the same way as copy mode (like emacs, so all-lowercase means case insensitive). GitHub issue 4396. --- mode-tree.c | 91 +++++++++++++++++++++++++++++++------------------ tmux.h | 2 +- window-buffer.c | 43 ++++++++++++++++++++--- window-tree.c | 14 ++++++-- 4 files changed, 108 insertions(+), 42 deletions(-) diff --git a/mode-tree.c b/mode-tree.c index 1d929952..a3620b4f 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -82,6 +82,7 @@ struct mode_tree_data { char *filter; int no_matches; enum mode_tree_search_dir search_dir; + int search_icase; }; struct mode_tree_item { @@ -132,6 +133,17 @@ static const struct menu_item mode_tree_menu_items[] = { { NULL, KEYC_NONE, NULL } }; +static int +mode_tree_is_lowercase(const char *ptr) +{ + while (*ptr != '\0') { + if (*ptr != tolower((u_char)*ptr)) + return (0); + ++ptr; + } + return (1); +} + static struct mode_tree_item * mode_tree_find_item(struct mode_tree_list *mtl, uint64_t tag) { @@ -881,49 +893,57 @@ done: static struct mode_tree_item * mode_tree_search_backward(struct mode_tree_data *mtd) { - struct mode_tree_item *mti, *last, *prev; + struct mode_tree_item *mti, *last, *prev; + int icase = mtd->search_icase; - if (mtd->search == NULL) - return (NULL); + if (mtd->search == NULL) + return (NULL); - mti = last = mtd->line_list[mtd->current].item; - for (;;) { - if ((prev = TAILQ_PREV(mti, mode_tree_list, entry)) != NULL) { - /* Point to the last child in the previous subtree. */ - while (!TAILQ_EMPTY(&prev->children)) - prev = TAILQ_LAST(&prev->children, mode_tree_list); - mti = prev; - } else { - /* If prev is NULL, jump to the parent. */ - mti = mti->parent; - } + mti = last = mtd->line_list[mtd->current].item; + for (;;) { + if ((prev = TAILQ_PREV(mti, mode_tree_list, entry)) != NULL) { + /* Point to the last child in the previous subtree. */ + while (!TAILQ_EMPTY(&prev->children)) { + prev = TAILQ_LAST(&prev->children, + mode_tree_list); + } + mti = prev; + } else { + /* If prev is NULL, jump to the parent. */ + mti = mti->parent; + } - if (mti == NULL) { - /* Point to the last child in the last root subtree. */ - prev = TAILQ_LAST(&mtd->children, mode_tree_list); - while (!TAILQ_EMPTY(&prev->children)) - prev = TAILQ_LAST(&prev->children, mode_tree_list); - mti = prev; - } - if (mti == last) - break; + if (mti == NULL) { + /* Point to the last child in the last root subtree. */ + prev = TAILQ_LAST(&mtd->children, mode_tree_list); + while (!TAILQ_EMPTY(&prev->children)) { + prev = TAILQ_LAST(&prev->children, + mode_tree_list); + } + mti = prev; + } + if (mti == last) + break; - if (mtd->searchcb == NULL) { - if (strstr(mti->name, mtd->search) != NULL) + if (mtd->searchcb == NULL) { + if (!icase && strstr(mti->name, mtd->search) != NULL) + return (mti); + if (icase && strcasestr(mti->name, mtd->search) != NULL) + return (mti); + continue; + } + if (mtd->searchcb(mtd->modedata, mti->itemdata, mtd->search, + icase)) return (mti); - continue; } - if (mtd->searchcb(mtd->modedata, mti->itemdata, mtd->search)) - return (mti); - } - return (NULL); + return (NULL); } - static struct mode_tree_item * mode_tree_search_forward(struct mode_tree_data *mtd) { struct mode_tree_item *mti, *last, *next; + int icase = mtd->search_icase; if (mtd->search == NULL) return (NULL); @@ -951,11 +971,14 @@ mode_tree_search_forward(struct mode_tree_data *mtd) break; if (mtd->searchcb == NULL) { - if (strstr(mti->name, mtd->search) != NULL) + if (!icase && strstr(mti->name, mtd->search) != NULL) + return (mti); + if (icase && strcasestr(mti->name, mtd->search) != NULL) return (mti); continue; } - if (mtd->searchcb(mtd->modedata, mti->itemdata, mtd->search)) + if (mtd->searchcb(mtd->modedata, mti->itemdata, mtd->search, + icase)) return (mti); } return (NULL); @@ -1002,6 +1025,7 @@ mode_tree_search_callback(__unused struct client *c, void *data, const char *s, return (0); } mtd->search = xstrdup(s); + mtd->search_icase = mode_tree_is_lowercase(s); mode_tree_search_set(mtd); return (0); @@ -1309,6 +1333,7 @@ mode_tree_key(struct mode_tree_data *mtd, struct client *c, key_code *key, case '/': case 's'|KEYC_CTRL: mtd->references++; + mtd->search_dir = MODE_TREE_SEARCH_FORWARD; status_prompt_set(c, NULL, "(search) ", "", mode_tree_search_callback, mode_tree_search_free, mtd, PROMPT_NOFORMAT, PROMPT_TYPE_SEARCH); diff --git a/tmux.h b/tmux.h index 02d8d1fb..e386185c 100644 --- a/tmux.h +++ b/tmux.h @@ -3290,7 +3290,7 @@ typedef void (*mode_tree_build_cb)(void *, struct mode_tree_sort_criteria *, uint64_t *, const char *); typedef void (*mode_tree_draw_cb)(void *, void *, struct screen_write_ctx *, u_int, u_int); -typedef int (*mode_tree_search_cb)(void *, void *, const char *); +typedef int (*mode_tree_search_cb)(void *, void *, const char *, int); typedef void (*mode_tree_menu_cb)(void *, struct client *, key_code); typedef u_int (*mode_tree_height_cb)(void *, u_int); typedef key_code (*mode_tree_key_cb)(void *, void *, u_int); diff --git a/window-buffer.c b/window-buffer.c index a13193cd..782a7854 100644 --- a/window-buffer.c +++ b/window-buffer.c @@ -18,6 +18,7 @@ #include +#include #include #include #include @@ -255,7 +256,30 @@ window_buffer_draw(__unused void *modedata, void *itemdata, } static int -window_buffer_search(__unused void *modedata, void *itemdata, const char *ss) +window_buffer_find(const void *data, size_t datalen, const void *find, + size_t findlen, int icase) +{ + const u_char *udata = data, *ufind = find; + size_t i, j; + + if (findlen == 0 || datalen < findlen) + return (0); + for (i = 0; i + findlen <= datalen; i++) { + for (j = 0; j < findlen; j++) { + if (!icase && udata[i + j] != ufind[j]) + break; + if (icase && tolower(udata[i + j]) != tolower(ufind[j])) + break; + } + if (j == findlen) + return (1); + } + return (0); +} + +static int +window_buffer_search(__unused void *modedata, void *itemdata, const char *ss, + int icase) { struct window_buffer_itemdata *item = itemdata; struct paste_buffer *pb; @@ -264,10 +288,19 @@ window_buffer_search(__unused void *modedata, void *itemdata, const char *ss) if ((pb = paste_get_name(item->name)) == NULL) return (0); - if (strstr(item->name, ss) != NULL) - return (1); - bufdata = paste_buffer_data(pb, &bufsize); - return (memmem(bufdata, bufsize, ss, strlen(ss)) != NULL); + if (icase) { + if (strcasestr(item->name, ss) != NULL) + return (1); + bufdata = paste_buffer_data(pb, &bufsize); + return (window_buffer_find(bufdata, bufsize, ss, strlen(ss), + icase)); + } else { + if (strstr(item->name, ss) != NULL) + return (1); + bufdata = paste_buffer_data(pb, &bufsize); + return (window_buffer_find(bufdata, bufsize, ss, strlen(ss), + icase)); + } } static void diff --git a/window-tree.c b/window-tree.c index 78ed52be..0a2460f1 100644 --- a/window-tree.c +++ b/window-tree.c @@ -836,7 +836,8 @@ window_tree_draw(void *modedata, void *itemdata, struct screen_write_ctx *ctx, } static int -window_tree_search(__unused void *modedata, void *itemdata, const char *ss) +window_tree_search(__unused void *modedata, void *itemdata, const char *ss, + int icase) { struct window_tree_itemdata *item = itemdata; struct session *s; @@ -853,10 +854,14 @@ window_tree_search(__unused void *modedata, void *itemdata, const char *ss) case WINDOW_TREE_SESSION: if (s == NULL) return (0); - return (strstr(s->name, ss) != NULL); + if (icase) + return (strcasestr(s->name, ss) != NULL); + return (strstr(s->name, ss) != NULL); case WINDOW_TREE_WINDOW: if (s == NULL || wl == NULL) return (0); + if (icase) + return (strcasestr(wl->window->name, ss) != NULL); return (strstr(wl->window->name, ss) != NULL); case WINDOW_TREE_PANE: if (s == NULL || wl == NULL || wp == NULL) @@ -866,7 +871,10 @@ window_tree_search(__unused void *modedata, void *itemdata, const char *ss) free(cmd); return (0); } - retval = (strstr(cmd, ss) != NULL); + if (icase) + retval = (strcasestr(cmd, ss) != NULL); + else + retval = (strstr(cmd, ss) != NULL); free(cmd); return (retval); } From 0c5abfefd36aa716f82d9507d8630fdad723341c Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 28 Oct 2025 16:36:52 +0000 Subject: [PATCH 06/27] Add commands to centre the cursor in copy mode, from m-einfalt at gmx dot de in GitHub issue 4662. --- key-bindings.c | 2 ++ tmux.1 | 10 ++++++++++ window-copy.c | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/key-bindings.c b/key-bindings.c index fb766a02..1fb8189e 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -493,6 +493,8 @@ key_bindings_init(void) "bind -Tcopy-mode C-b { send -X cursor-left }", "bind -Tcopy-mode C-g { send -X clear-selection }", "bind -Tcopy-mode C-k { send -X copy-pipe-end-of-line-and-cancel }", + "bind -Tcopy-mode C-l { send -X cursor-centre-vertical }", + "bind -Tcopy-mode M-l { send -X cursor-centre-horizontal }", "bind -Tcopy-mode C-n { send -X cursor-down }", "bind -Tcopy-mode C-p { send -X cursor-up }", "bind -Tcopy-mode C-r { command-prompt -T search -ip'(search up)' -I'#{pane_search_string}' { send -X search-backward-incremental -- '%%' } }", diff --git a/tmux.1 b/tmux.1 index 0c3688df..c483499e 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1951,6 +1951,16 @@ Move the cursor right. .Xc Move the cursor up. .It Xo +.Ic cursor-centre-vertical +(emacs: C-l) +.Xc +Moves the cursor to the vertical centre of the pane. +.It Xo +.Ic cursor-centre-horizontal +(emacs: M-l) +.Xc +Moves the cursor to the horizontal centre of the pane. +.It Xo .Ic end-of-line (vi: $) (emacs: C-e) diff --git a/window-copy.c b/window-copy.c index 09939d4c..3b0f11dc 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1497,6 +1497,28 @@ window_copy_cmd_cursor_up(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_NOTHING); } +static enum window_copy_cmd_action +window_copy_cmd_centre_vertical(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct window_copy_mode_data *data = wme->data; + + window_copy_update_cursor(wme, data->cx, wme->wp->sy / 2); + window_copy_update_selection(wme, 1, 0); + return (WINDOW_COPY_CMD_REDRAW); +} + +static enum window_copy_cmd_action +window_copy_cmd_centre_horizontal(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct window_copy_mode_data *data = wme->data; + + window_copy_update_cursor(wme, wme->wp->sx / 2, data->cy); + window_copy_update_selection(wme, 1, 0); + return (WINDOW_COPY_CMD_REDRAW); +} + static enum window_copy_cmd_action window_copy_cmd_end_of_line(struct window_copy_cmd_state *cs) { @@ -2785,6 +2807,16 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_EMACS_ONLY, .f = window_copy_cmd_cursor_up }, + { .command = "cursor-centre-vertical", + .args = { "", 0, 0, NULL }, + .clear = WINDOW_COPY_CMD_CLEAR_EMACS_ONLY, + .f = window_copy_cmd_centre_vertical, + }, + { .command = "cursor-centre-horizontal", + .args = { "", 0, 0, NULL }, + .clear = WINDOW_COPY_CMD_CLEAR_EMACS_ONLY, + .f = window_copy_cmd_centre_horizontal, + }, { .command = "end-of-line", .args = { "", 0, 0, NULL }, .clear = WINDOW_COPY_CMD_CLEAR_EMACS_ONLY, From f812b8d30456c071ee23e9a02d6ce16036fd3f68 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 29 Oct 2025 08:47:45 +0000 Subject: [PATCH 07/27] Cast to avoid warnings on 32-bit architectures. GitHub issue 4597. --- compat/utf8proc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compat/utf8proc.c b/compat/utf8proc.c index dd4ab27f..147da696 100644 --- a/compat/utf8proc.c +++ b/compat/utf8proc.c @@ -48,7 +48,7 @@ utf8proc_mbtowc(wchar_t *pwc, const char *s, size_t n) * *pwc == -1 indicates invalid codepoint * slen < 0 indicates an error */ - slen = utf8proc_iterate(s, n, pwc); + slen = utf8proc_iterate(s, n, (utf8proc_int32_t*)pwc); if (*pwc == (wchar_t)-1 || slen < 0) return (-1); return (slen); From 1a419609e37e6b41ad8c1466afa02cfcd6869df8 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 29 Oct 2025 09:33:20 +0000 Subject: [PATCH 08/27] Don't enter copy mode on mouse wheel in alternate screen, GitHub issue 3705. --- key-bindings.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/key-bindings.c b/key-bindings.c index 1fb8189e..7f8b20e0 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -441,10 +441,10 @@ key_bindings_init(void) "bind -n MouseDown1Pane { select-pane -t=; send -M }", /* Mouse button 1 drag on pane. */ - "bind -n MouseDrag1Pane { if -F '#{||:#{pane_in_mode},#{mouse_any_flag}}' { send -M } { copy-mode -M } }", + "bind -n MouseDrag1Pane { if -F '#{||:#{alternate_on},#{pane_in_mode},#{mouse_any_flag}}' { send -M } { copy-mode -M } }", /* Mouse wheel up on pane. */ - "bind -n WheelUpPane { if -F '#{||:#{pane_in_mode},#{mouse_any_flag}}' { send -M } { copy-mode -e } }", + "bind -n WheelUpPane { if -F '#{||:#{alternate_on},#{pane_in_mode},#{mouse_any_flag}}' { send -M } { copy-mode -e } }", /* Mouse button 2 down on pane. */ "bind -n MouseDown2Pane { select-pane -t=; if -F '#{||:#{pane_in_mode},#{mouse_any_flag}}' { send -M } { paste -p } }", From 2c08960f4e54dae97327eb7b78c21245abb368cb Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 29 Oct 2025 09:37:36 +0000 Subject: [PATCH 09/27] Add -l flag to command-prompt to disable splitting into multiple prompts, GitHub issue 4483. --- cmd-command-prompt.c | 46 +++++++++++++++++++++++++------------------- tmux.1 | 12 ++++++++++-- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/cmd-command-prompt.c b/cmd-command-prompt.c index 36a1f03c..85a1bf8a 100644 --- a/cmd-command-prompt.c +++ b/cmd-command-prompt.c @@ -42,8 +42,8 @@ const struct cmd_entry cmd_command_prompt_entry = { .name = "command-prompt", .alias = NULL, - .args = { "1bFkiI:Np:t:T:", 0, 1, cmd_command_prompt_args_parse }, - .usage = "[-1bFkiN] [-I inputs] [-p prompts] " CMD_TARGET_CLIENT_USAGE + .args = { "1bFkliI:Np:t:T:", 0, 1, cmd_command_prompt_args_parse }, + .usage = "[-1bFkliN] [-I inputs] [-p prompts] " CMD_TARGET_CLIENT_USAGE " [-T prompt-type] [template]", .flags = CMD_CLIENT_TFLAG, @@ -117,27 +117,33 @@ cmd_command_prompt_exec(struct cmd *self, struct cmdq_item *item) next_input = inputs = xstrdup(s); else next_input = NULL; - while ((prompt = strsep(&next_prompt, ",")) != NULL) { - cdata->prompts = xreallocarray(cdata->prompts, cdata->count + 1, - sizeof *cdata->prompts); - if (!space) - tmp = xstrdup(prompt); - else - xasprintf(&tmp, "%s ", prompt); - cdata->prompts[cdata->count].prompt = tmp; + if (args_has(args, 'l')) { + cdata->prompts = xcalloc(1, sizeof *cdata->prompts); + cdata->prompts[0].prompt = prompts; + cdata->prompts[0].input = inputs; + cdata->count = 1; + } else { + while ((prompt = strsep(&next_prompt, ",")) != NULL) { + cdata->prompts = xreallocarray(cdata->prompts, + cdata->count + 1, sizeof *cdata->prompts); + if (!space) + tmp = xstrdup(prompt); + else + xasprintf(&tmp, "%s ", prompt); + cdata->prompts[cdata->count].prompt = tmp; - if (next_input != NULL) { - input = strsep(&next_input, ","); - if (input == NULL) + if (next_input != NULL) { + input = strsep(&next_input, ","); + if (input == NULL) + input = ""; + } else input = ""; - } else - input = ""; - cdata->prompts[cdata->count].input = xstrdup(input); - - cdata->count++; + cdata->prompts[cdata->count].input = xstrdup(input); + cdata->count++; + } + free(inputs); + free(prompts); } - free(inputs); - free(prompts); if ((type = args_get(args, 'T')) != NULL) { cdata->prompt_type = status_prompt_type(type); diff --git a/tmux.1 b/tmux.1 index c483499e..f6b630dd 100644 --- a/tmux.1 +++ b/tmux.1 @@ -6670,7 +6670,7 @@ See for possible values for .Ar prompt-type . .It Xo Ic command-prompt -.Op Fl 1bFikN +.Op Fl 1bFiklN .Op Fl I Ar inputs .Op Fl p Ar prompts .Op Fl t Ar target-client @@ -6690,8 +6690,10 @@ With .Ar template is expanded as a format. .Pp -If present, +If .Fl I +is present, +.Ar inputs is a comma-separated list of the initial text for each prompt. If .Fl p @@ -6703,6 +6705,12 @@ a single prompt is displayed, constructed from if it is present, or .Ql \&: if not. +.Fl l +disables splitting of +.Ar inputs +and +.Ar prompts +at commas and treats them literally. .Pp Before the command is executed, the first occurrence of the string .Ql %% From 1e61e524003f7fa0f8e1b2b8c0a21cce000d3fa6 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 30 Oct 2025 07:41:19 +0000 Subject: [PATCH 10/27] If tmux receives a palette request (OSC 4) in a pane and the palette entry has not been set, send a request to the most recently used client and forward any response instead. Based on change from Tim Culverhouse in GitHub issue 4665. --- input.c | 272 ++++++++++++++++++++++++++++++++++++++++-------- server-client.c | 8 ++ tmux.h | 26 +++++ tty-keys.c | 76 ++++++++++++++ tty.c | 6 +- 5 files changed, 340 insertions(+), 48 deletions(-) diff --git a/input.c b/input.c index 4a9a1881..bf6cb48a 100644 --- a/input.c +++ b/input.c @@ -51,6 +51,20 @@ * be passed to the underlying terminals. */ +/* Request sent by a pane. */ +struct input_request { + struct client *c; + struct input_ctx *ictx; + + enum input_request_type type; + int idx; + time_t t; + + TAILQ_ENTRY(input_request) entry; + TAILQ_ENTRY(input_request) centry; +}; +#define INPUT_REQUEST_TIMEOUT 5 + /* Input parser cell. */ struct input_cell { struct grid_cell cell; @@ -72,61 +86,69 @@ struct input_param { }; }; +/* Type of terminator. */ +enum input_end_type { + INPUT_END_ST, + INPUT_END_BEL +}; + /* Input parser context. */ struct input_ctx { - struct window_pane *wp; - struct bufferevent *event; - struct screen_write_ctx ctx; - struct colour_palette *palette; + struct window_pane *wp; + struct bufferevent *event; + struct screen_write_ctx ctx; + struct colour_palette *palette; - struct input_cell cell; + struct input_cell cell; + struct input_cell old_cell; + u_int old_cx; + u_int old_cy; + int old_mode; - struct input_cell old_cell; - u_int old_cx; - u_int old_cy; - int old_mode; + u_char interm_buf[4]; + size_t interm_len; - u_char interm_buf[4]; - size_t interm_len; - - u_char param_buf[64]; - size_t param_len; + u_char param_buf[64]; + size_t param_len; #define INPUT_BUF_START 32 - u_char *input_buf; - size_t input_len; - size_t input_space; - enum { - INPUT_END_ST, - INPUT_END_BEL - } input_end; + u_char *input_buf; + size_t input_len; + size_t input_space; + enum input_end_type input_end; - struct input_param param_list[24]; - u_int param_list_len; + struct input_param param_list[24]; + u_int param_list_len; - struct utf8_data utf8data; - int utf8started; + struct utf8_data utf8data; + int utf8started; - int ch; - struct utf8_data last; + int ch; + struct utf8_data last; - int flags; + const struct input_state *state; + int flags; #define INPUT_DISCARD 0x1 #define INPUT_LAST 0x2 - const struct input_state *state; - - struct event timer; + struct input_requests requests[INPUT_REQUEST_TYPES]; + u_int request_count; + struct event request_timer; /* * All input received since we were last in the ground state. Sent to * control clients on connection. */ - struct evbuffer *since_ground; + struct evbuffer *since_ground; + struct event ground_timer; }; /* Helper functions. */ struct input_transition; +static void input_request_timer_callback(int, short, void *); +static void input_start_request_timer(struct input_ctx *); +static int input_add_request(struct input_ctx *, enum input_request_type, + int); static int input_split(struct input_ctx *); static int input_get(struct input_ctx *, u_int, int, int); static void printflike(2, 3) input_reply(struct input_ctx *, const char *, ...); @@ -766,7 +788,7 @@ input_stop_utf8(struct input_ctx *ictx) * long, so reset to ground. */ static void -input_timer_callback(__unused int fd, __unused short events, void *arg) +input_ground_timer_callback(__unused int fd, __unused short events, void *arg) { struct input_ctx *ictx = arg; @@ -776,12 +798,12 @@ input_timer_callback(__unused int fd, __unused short events, void *arg) /* Start the timer. */ static void -input_start_timer(struct input_ctx *ictx) +input_start_ground_timer(struct input_ctx *ictx) { struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; - event_del(&ictx->timer); - event_add(&ictx->timer, &tv); + event_del(&ictx->ground_timer); + event_add(&ictx->ground_timer, &tv); } /* Reset cell state to default. */ @@ -830,6 +852,7 @@ input_init(struct window_pane *wp, struct bufferevent *bev, struct colour_palette *palette) { struct input_ctx *ictx; + u_int i; ictx = xcalloc(1, sizeof *ictx); ictx->wp = wp; @@ -842,8 +865,11 @@ input_init(struct window_pane *wp, struct bufferevent *bev, ictx->since_ground = evbuffer_new(); if (ictx->since_ground == NULL) fatalx("out of memory"); + evtimer_set(&ictx->ground_timer, input_ground_timer_callback, ictx); - evtimer_set(&ictx->timer, input_timer_callback, ictx); + for (i = 0; i < INPUT_REQUEST_TYPES; i++) + TAILQ_INIT(&ictx->requests[i]); + evtimer_set(&ictx->request_timer, input_request_timer_callback, ictx); input_reset(ictx, 0); return (ictx); @@ -853,17 +879,30 @@ input_init(struct window_pane *wp, struct bufferevent *bev, void input_free(struct input_ctx *ictx) { - u_int i; + struct input_request_list *irl; + struct input_request *ir, *ir1; + u_int i; for (i = 0; i < ictx->param_list_len; i++) { if (ictx->param_list[i].type == INPUT_STRING) free(ictx->param_list[i].str); } - event_del(&ictx->timer); + for (i = 0; i < INPUT_REQUEST_TYPES; i++) { + TAILQ_FOREACH_SAFE(ir, &ictx->requests[i], entry, ir1) { + log_debug("%s: req %p: client %s, pane %%%u, type %d", + __func__, ir, ir->c->name, ictx->wp->id, i); + irl = &ir->c->input_requests[i]; + TAILQ_REMOVE(&ictx->requests[i], ir, entry); + TAILQ_REMOVE(&irl->requests, ir, centry); + free(ir); + } + } + event_del(&ictx->request_timer); free(ictx->input_buf); evbuffer_free(ictx->since_ground); + event_del(&ictx->ground_timer); free(ictx); } @@ -1122,7 +1161,7 @@ input_reply(struct input_ctx *ictx, const char *fmt, ...) static void input_clear(struct input_ctx *ictx) { - event_del(&ictx->timer); + event_del(&ictx->ground_timer); *ictx->interm_buf = '\0'; ictx->interm_len = 0; @@ -1142,7 +1181,7 @@ input_clear(struct input_ctx *ictx) static void input_ground(struct input_ctx *ictx) { - event_del(&ictx->timer); + event_del(&ictx->ground_timer); evbuffer_drain(ictx->since_ground, EVBUFFER_LENGTH(ictx->since_ground)); if (ictx->input_space > INPUT_BUF_START) { @@ -2385,7 +2424,7 @@ input_enter_dcs(struct input_ctx *ictx) log_debug("%s", __func__); input_clear(ictx); - input_start_timer(ictx); + input_start_ground_timer(ictx); ictx->flags &= ~INPUT_LAST; } @@ -2511,7 +2550,7 @@ input_enter_osc(struct input_ctx *ictx) log_debug("%s", __func__); input_clear(ictx); - input_start_timer(ictx); + input_start_ground_timer(ictx); ictx->flags &= ~INPUT_LAST; } @@ -2606,7 +2645,7 @@ input_enter_apc(struct input_ctx *ictx) log_debug("%s", __func__); input_clear(ictx); - input_start_timer(ictx); + input_start_ground_timer(ictx); ictx->flags &= ~INPUT_LAST; } @@ -2637,7 +2676,7 @@ input_enter_rename(struct input_ctx *ictx) log_debug("%s", __func__); input_clear(ictx); - input_start_timer(ictx); + input_start_ground_timer(ictx); ictx->flags &= ~INPUT_LAST; } @@ -2767,8 +2806,12 @@ input_osc_4(struct input_ctx *ictx, const char *p) s = strsep(&next, ";"); if (strcmp(s, "?") == 0) { c = colour_palette_get(palette, idx|COLOUR_FLAG_256); - if (c != -1) + if (c != -1) { input_osc_colour_reply(ictx, 4, idx, c); + s = next; + continue; + } + input_add_request(ictx, INPUT_REQUEST_PALETTE, idx); s = next; continue; } @@ -3110,6 +3153,143 @@ input_set_buffer_size(size_t buffer_size) input_buffer_size = buffer_size; } +/* Request timer. Remove any requests that are too old. */ +static void +input_request_timer_callback(__unused int fd, __unused short events, void *arg) +{ + struct input_ctx *ictx = arg; + struct input_request *ir, *ir1; + struct input_request_list *irl; + u_int i; + time_t t = time(NULL); + + for (i = 0; i < INPUT_REQUEST_TYPES; i++) { + TAILQ_FOREACH_SAFE(ir, &ictx->requests[i], entry, ir1) { + if (ir->t >= t - INPUT_REQUEST_TIMEOUT) + continue; + log_debug("%s: req %p: client %s, pane %%%u, type %d", + __func__, ir, ir->c->name, ictx->wp->id, ir->type); + irl = &ir->c->input_requests[i]; + TAILQ_REMOVE(&ictx->requests[i], ir, entry); + TAILQ_REMOVE(&irl->requests, ir, centry); + ictx->request_count--; + free(ir); + } + } + if (ictx->request_count != 0) + input_start_request_timer(ictx); +} + +/* Start the request timer. */ +static void +input_start_request_timer(struct input_ctx *ictx) +{ + struct timeval tv = { .tv_sec = 1, .tv_usec = 0 }; + + event_del(&ictx->request_timer); + event_add(&ictx->request_timer, &tv); +} + +/* Add a request. */ +static int +input_add_request(struct input_ctx *ictx, enum input_request_type type, int idx) +{ + struct window_pane *wp = ictx->wp; + struct window *w; + struct client *c = NULL, *loop; + struct input_request *ir; + char s[64]; + + if (wp == NULL) + return (-1); + w = wp->window; + + TAILQ_FOREACH(loop, &clients, entry) { + if (loop->flags & CLIENT_UNATTACHEDFLAGS) + continue; + if (loop->session == NULL || !session_has(loop->session, w)) + continue; + if (~loop->tty.flags & TTY_STARTED) + continue; + if (c == NULL) + c = loop; + else if (timercmp(&loop->activity_time, &c->activity_time, >)) + c = loop; + } + if (c == NULL) + return (-1); + + ir = xcalloc (1, sizeof *ir); + ir->c = c; + ir->ictx = ictx; + ir->type = type; + ir->idx = idx; + ir->t = time(NULL); + TAILQ_INSERT_TAIL(&ictx->requests[type], ir, entry); + TAILQ_INSERT_TAIL(&c->input_requests[type].requests, ir, centry); + if (++ictx->request_count == 1) + input_start_request_timer(ictx); + log_debug("%s: req %p: client %s, pane %%%u, type %d", __func__, ir, + c->name, wp->id, type); + + switch (type) { + case INPUT_REQUEST_PALETTE: + xsnprintf(s, sizeof s, "\033]4;%d;?\033\\", idx); + tty_puts(&c->tty, s); + break; + } + + return (0); +} + +/* Handle a reply to a request. */ +void +input_request_reply(struct client *c, enum input_request_type type, void *data) +{ + struct input_request_list *irl = &c->input_requests[type]; + struct input_request *ir, *ir1; + struct input_request_palette_data *pd = data; + + TAILQ_FOREACH_SAFE(ir, &irl->requests, centry, ir1) { + log_debug("%s: req %p: client %s, pane %%%u, type %d", + __func__, ir, c->name, ir->ictx->wp->id, ir->type); + switch (type) { + case INPUT_REQUEST_PALETTE: + if (pd->idx != ir->idx) + continue; + input_osc_colour_reply(ir->ictx, 4, pd->idx, pd->c); + break; + } + TAILQ_REMOVE(&ir->ictx->requests[type], ir, entry); + TAILQ_REMOVE(&irl->requests, ir, centry); + ir->ictx->request_count--; + free(ir); + break; + } +} + +/* Cancel pending requests for client. */ +void +input_cancel_requests(struct client *c) +{ + struct input_request_list *irl; + struct input_request *ir, *ir1; + u_int i; + + for (i = 0; i < INPUT_REQUEST_TYPES; i++) { + irl = &c->input_requests[i]; + TAILQ_FOREACH_SAFE(ir, &irl->requests, entry, ir1) { + log_debug("%s: req %p: client %s, pane %%%u, type %d", + __func__, ir, c->name, ir->ictx->wp->id, ir->type); + TAILQ_REMOVE(&ir->ictx->requests[i], ir, entry); + TAILQ_REMOVE(&irl->requests, ir, centry); + ir->ictx->request_count--; + free(ir); + } + } +} + +/* Report current theme. */ static void input_report_current_theme(struct input_ctx *ictx) { diff --git a/server-client.c b/server-client.c index 34240cdb..d2a5a80c 100644 --- a/server-client.c +++ b/server-client.c @@ -282,6 +282,7 @@ struct client * server_client_create(int fd) { struct client *c; + u_int i; setblocking(fd, 0); @@ -315,6 +316,12 @@ server_client_create(int fd) evtimer_set(&c->repeat_timer, server_client_repeat_timer, c); evtimer_set(&c->click_timer, server_client_click_timer, c); + for (i = 0; i < INPUT_REQUEST_TYPES; i++) { + c->input_requests[i].c = c; + c->input_requests[i].type = i; + TAILQ_INIT(&c->input_requests[i].requests); + } + TAILQ_INSERT_TAIL(&clients, c, entry); log_debug("new client %p", c); return (c); @@ -463,6 +470,7 @@ server_client_lost(struct client *c) tty_term_free_list(c->term_caps, c->term_ncaps); status_free(c); + input_cancel_requests(c); free(c->title); free((void *)c->cwd); diff --git a/tmux.h b/tmux.h index e386185c..2ec3bf23 100644 --- a/tmux.h +++ b/tmux.h @@ -53,6 +53,8 @@ struct format_tree; struct hyperlinks_uri; struct hyperlinks; struct input_ctx; +struct input_request; +struct input_requests; struct job; struct menu_data; struct mode_tree_data; @@ -1092,6 +1094,26 @@ struct window_mode_entry { TAILQ_ENTRY(window_mode_entry) entry; }; +/* Type of request to client. */ +enum input_request_type { + INPUT_REQUEST_PALETTE +}; +#define INPUT_REQUEST_TYPES (1) + +/* Palette request reply data. */ +struct input_request_palette_data { + int idx; + int c; +}; + +/* Request sent to client on behalf of pane. */ +TAILQ_HEAD(input_requests, input_request); +struct input_request_list { + struct client *c; + enum input_request_type type; + struct input_requests requests; +}; + /* Offsets into pane buffer. */ struct window_pane_offset { size_t used; @@ -1920,6 +1942,8 @@ struct client { struct status_line status; enum client_theme theme; + struct input_request_list input_requests[INPUT_REQUEST_TYPES]; + #define CLIENT_TERMINAL 0x1 #define CLIENT_LOGIN 0x2 #define CLIENT_EXIT 0x4 @@ -2922,6 +2946,8 @@ void input_parse_screen(struct input_ctx *, struct screen *, void input_reply_clipboard(struct bufferevent *, const char *, size_t, const char *); void input_set_buffer_size(size_t); +void input_request_reply(struct client *, enum input_request_type, void *); +void input_cancel_requests(struct client *); /* input-key.c */ void input_key_build(void); diff --git a/tty-keys.c b/tty-keys.c index a367d022..77254591 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -59,6 +59,7 @@ static int tty_keys_device_attributes2(struct tty *, const char *, size_t, size_t *); static int tty_keys_extended_device_attributes(struct tty *, const char *, size_t, size_t *); +static int tty_keys_palette(struct tty *, const char *, size_t, size_t *); /* A key tree entry. */ struct tty_key { @@ -804,6 +805,17 @@ tty_keys_next(struct tty *tty) goto partial_key; } + /* Is this a palette response? */ + switch (tty_keys_palette(tty, buf, len, &size)) { + case 0: /* yes */ + key = KEYC_UNKNOWN; + goto complete_key; + case -1: /* no, or not valid */ + break; + case 1: /* partial */ + goto partial_key; + } + /* Is this a mouse key press? */ switch (tty_keys_mouse(tty, buf, len, &size, &m)) { case 0: /* yes */ @@ -1699,3 +1711,67 @@ tty_keys_colours(struct tty *tty, const char *buf, size_t len, size_t *size, return (0); } + +/* Handle OSC 4 palette colour responses. */ +static int +tty_keys_palette(struct tty *tty, const char *buf, size_t len, size_t *size) +{ + struct client *c = tty->client; + u_int i, start; + char tmp[128], *endptr; + int idx; + struct input_request_palette_data pd; + + *size = 0; + + /* First three bytes are always \033]4. */ + if (buf[0] != '\033') + return (-1); + if (len == 1) + return (1); + if (buf[1] != ']') + return (-1); + if (len == 2) + return (1); + if (buf[2] != '4') + return (-1); + if (len == 3) + return (1); + if (buf[3] != ';') + return (-1); + if (len == 4) + return (1); + + /* Parse index. */ + idx = strtol(buf + 4, &endptr, 10); + if (endptr == buf + 4 || *endptr != ';') + return (-1); + if (idx < 0 || idx > 255) + return (-1); + + /* Copy the rest up to \033\ or \007. */ + start = (endptr - buf) + 1; + for (i = start; i < len && i - start < sizeof tmp; i++) { + if (buf[i - 1] == '\033' && buf[i] == '\\') + break; + if (buf[i] == '\007') + break; + tmp[i - start] = buf[i]; + } + if (i - start == sizeof tmp) + return (-1); + if (i > 0 && buf[i - 1] == '\033') + tmp[i - start - 1] = '\0'; + else + tmp[i - start] = '\0'; + *size = i + 1; + + /* Work out the colour. */ + pd.c = colour_parseX11(tmp); + if (pd.c == -1) + return (0); + pd.idx = idx; + input_request_reply(c, INPUT_REQUEST_PALETTE, &pd); + + return (0); +} diff --git a/tty.c b/tty.c index ac28cd60..bd0c6b37 100644 --- a/tty.c +++ b/tty.c @@ -414,10 +414,12 @@ tty_repeat_requests(struct tty *tty, int force) return; if (!force && n <= TTY_REQUEST_LIMIT) { - log_debug("%s: not repeating requests (%u seconds)", c->name, n); + log_debug("%s: not repeating requests (%u seconds)", c->name, + n); return; } - log_debug("%s: %srepeating requests (%u seconds)", c->name, force ? "(force) " : "" , n); + log_debug("%s: %srepeating requests (%u seconds)", c->name, + force ? "(force) " : "" , n); tty->last_requests = t; if (tty->term->flags & TERM_VT100LIKE) { From 29db8ac36eeefcb67eb87d80900b97eb515f2c80 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 30 Oct 2025 11:52:25 +0000 Subject: [PATCH 11/27] Set and check COLORTERM as a hint for RGB colour. --- environ.c | 6 ++++++ tty-term.c | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/environ.c b/environ.c index 5abf383c..fb1c4902 100644 --- a/environ.c +++ b/environ.c @@ -262,6 +262,12 @@ environ_for_session(struct session *s, int no_TERM) environ_set(env, "TERM", 0, "%s", value); environ_set(env, "TERM_PROGRAM", 0, "%s", "tmux"); environ_set(env, "TERM_PROGRAM_VERSION", 0, "%s", getversion()); + environ_set(env, "COLORTERM", 0, "truecolor"); + } else { + environ_unset(env, "TERM"); + environ_unset(env, "TERM_PROGRAM"); + environ_unset(env, "TERM_PROGRAM_VERSION"); + environ_unset(env, "COLORTERM"); } if (s != NULL) diff --git a/tty-term.c b/tty-term.c index 460b761f..b6b0dfce 100644 --- a/tty-term.c +++ b/tty-term.c @@ -532,6 +532,7 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, size_t offset, namelen; char *first; int n; + struct environ_entry *envent; log_debug("adding term %s", name); @@ -595,6 +596,17 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, a = options_array_next(a); } + /* Check for COLORTERM. */ + envent = environ_find(tty->client->environ, "COLORTERM"); + if (envent != NULL) { + log_debug("%s COLORTERM=%s", tty->client->name, envent->value); + if (strcasecmp(envent->value, "truecolor") == 0 || + strcasecmp(envent->value, "24bit") == 0) + tty_add_features(feat, "RGB", ","); + else if (strstr(s, "256") != NULL) + tty_add_features(feat, "256", ","); + } + /* Apply overrides so any capabilities used for features are changed. */ tty_term_apply_overrides(term); From e774b89062c414b10d10ccc4ea7eeaa812386cf1 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 30 Oct 2025 13:52:08 +0000 Subject: [PATCH 12/27] Preserve marked pane with swap-window and move-window, GitHub issue 3443. --- cmd-swap-window.c | 2 ++ server-fn.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/cmd-swap-window.c b/cmd-swap-window.c index b765112b..d3135398 100644 --- a/cmd-swap-window.c +++ b/cmd-swap-window.c @@ -77,6 +77,8 @@ cmd_swap_window_exec(struct cmd *self, struct cmdq_item *item) wl_src->window = w_dst; TAILQ_INSERT_TAIL(&w_dst->winlinks, wl_src, wentry); + if (marked_pane.wl == wl_src) + marked_pane.wl = wl_dst; if (args_has(args, 'd')) { session_select(dst, wl_dst->idx); if (src != dst) diff --git a/server-fn.c b/server-fn.c index 2180d8bb..72c0f8f8 100644 --- a/server-fn.c +++ b/server-fn.c @@ -289,6 +289,8 @@ server_link_window(struct session *src, struct winlink *srcwl, if (dstwl == NULL) return (-1); + if (marked_pane.wl == srcwl) + marked_pane.wl = dstwl; if (selectflag) session_select(dst, dstwl->idx); server_redraw_session_group(dst); From e5ab5995dbb78c73347a4088ff6ac3db51e9dfca Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 31 Oct 2025 09:00:26 +0000 Subject: [PATCH 13/27] Fix now-incorrect text about {}s. --- tmux.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tmux.1 b/tmux.1 index f6b630dd..8b057120 100644 --- a/tmux.1 +++ b/tmux.1 @@ -597,7 +597,7 @@ This is called line continuation and applies both inside and outside quoted strings and in comments, but not inside braces. .Pp Command arguments may be specified as strings surrounded by single (\[aq]) -quotes, double quotes (\[dq]) or braces ({}). +quotes or double quotes (\[dq]), or as command lists surrounded by braces ({}). .\" " This is required when the argument contains any special character. Single and double quoted strings cannot span multiple lines except with line From 8cb2805eb75903f1889766f7198fd7088b7b7ec7 Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 1 Nov 2025 16:42:59 +0000 Subject: [PATCH 14/27] Handle ~ correctly when loading a file, GitHub issue 3518. --- file.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/file.c b/file.c index 988ac7dd..407a7dc7 100644 --- a/file.c +++ b/file.c @@ -44,13 +44,21 @@ RB_GENERATE(client_files, client_file, entry, file_cmp); static char * file_get_path(struct client *c, const char *file) { - char *path; + const char *home; + char *path, *full_path; - if (*file == '/') + if (strncmp(file, "~/", 2) != 0) path = xstrdup(file); - else - xasprintf(&path, "%s/%s", server_client_get_cwd(c, NULL), file); - return (path); + else { + home = find_home(); + if (home == NULL) + home = ""; + xasprintf(&path, "%s%s", home, file + 1); + } + if (*path == '/') + return (path); + xasprintf(&full_path, "%s/%s", server_client_get_cwd(c, NULL), path); + return (full_path); } /* Tree comparison function. */ From d90b414223021378966f8b6988b725872e237793 Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 1 Nov 2025 16:44:24 +0000 Subject: [PATCH 15/27] Handle regional indicators and emoji modifiers in a better way, GitHub issue 3998. --- screen-write.c | 6 +-- tmux.h | 3 +- utf8-combined.c | 124 +++++++++++++++++++++++++++++++++--------------- utf8.c | 52 ++++++++++---------- 4 files changed, 117 insertions(+), 68 deletions(-) diff --git a/screen-write.c b/screen-write.c index f1b37421..1014ae80 100644 --- a/screen-write.c +++ b/screen-write.c @@ -2050,11 +2050,9 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) case HANGULJAMO_STATE_COMPOSABLE: break; case HANGULJAMO_STATE_NOT_HANGULJAMO: - if (utf8_is_modifier(ud)) { - if (last.data.size < 2) - return (0); + if (utf8_should_combine(&last.data, ud)) force_wide = 1; - } else if (!utf8_has_zwj(&last.data)) + else if (!utf8_has_zwj(&last.data)) return (0); break; } diff --git a/tmux.h b/tmux.h index 2ec3bf23..aa25a408 100644 --- a/tmux.h +++ b/tmux.h @@ -3499,7 +3499,8 @@ int utf8_cstrhas(const char *, const struct utf8_data *); int utf8_has_zwj(const struct utf8_data *); int utf8_is_zwj(const struct utf8_data *); int utf8_is_vs(const struct utf8_data *); -int utf8_is_modifier(const struct utf8_data *); +int utf8_should_combine(const struct utf8_data *, + const struct utf8_data *); enum hanguljamo_state hanguljamo_check_state(const struct utf8_data *, const struct utf8_data *); diff --git a/utf8-combined.c b/utf8-combined.c index 885dd6a4..91ddaf75 100644 --- a/utf8-combined.c +++ b/utf8-combined.c @@ -73,49 +73,99 @@ utf8_is_vs(const struct utf8_data *ud) return (memcmp(ud->data, "\357\270\217", 3) == 0); } -/* Is this in the modifier table? */ +/* Should these two characters combine? */ int -utf8_is_modifier(const struct utf8_data *ud) +utf8_should_combine(const struct utf8_data *with, const struct utf8_data *add) { - wchar_t wc; + wchar_t w, a; - if (utf8_towc(ud, &wc) != UTF8_DONE) + if (utf8_towc(with, &w) != UTF8_DONE) return (0); - switch (wc) { - case 0x1F1E6: - case 0x1F1E7: - case 0x1F1E8: - case 0x1F1E9: - case 0x1F1EA: - case 0x1F1EB: - case 0x1F1EC: - case 0x1F1ED: - case 0x1F1EE: - case 0x1F1EF: - case 0x1F1F0: - case 0x1F1F1: - case 0x1F1F2: - case 0x1F1F3: - case 0x1F1F4: - case 0x1F1F5: - case 0x1F1F6: - case 0x1F1F7: - case 0x1F1F8: - case 0x1F1F9: - case 0x1F1FA: - case 0x1F1FB: - case 0x1F1FC: - case 0x1F1FD: - case 0x1F1FE: - case 0x1F1FF: - case 0x1F3FB: - case 0x1F3FC: - case 0x1F3FD: - case 0x1F3FE: - case 0x1F3FF: + if (utf8_towc(add, &a) != UTF8_DONE) + return (0); + + /* Regional indicators. */ + if ((a >= 0x1F1E6 && a <= 0x1F1FF) && (w >= 0x1F1E6 && w <= 0x1F1FF)) return (1); + + /* Emoji skin tone modifiers. */ + switch (a) { + case 0x1F44B: + case 0x1F44C: + case 0x1F44D: + case 0x1F44E: + case 0x1F44F: + case 0x1F450: + case 0x1F466: + case 0x1F467: + case 0x1F468: + case 0x1F469: + case 0x1F46E: + case 0x1F470: + case 0x1F471: + case 0x1F472: + case 0x1F473: + case 0x1F474: + case 0x1F475: + case 0x1F476: + case 0x1F477: + case 0x1F478: + case 0x1F47C: + case 0x1F481: + case 0x1F482: + case 0x1F485: + case 0x1F486: + case 0x1F487: + case 0x1F4AA: + case 0x1F575: + case 0x1F57A: + case 0x1F590: + case 0x1F595: + case 0x1F596: + case 0x1F645: + case 0x1F646: + case 0x1F647: + case 0x1F64B: + case 0x1F64C: + case 0x1F64D: + case 0x1F64E: + case 0x1F64F: + case 0x1F6B4: + case 0x1F6B5: + case 0x1F6B6: + case 0x1F926: + case 0x1F937: + case 0x1F938: + case 0x1F939: + case 0x1F93D: + case 0x1F93E: + case 0x1F9B5: + case 0x1F9B6: + case 0x1F9B8: + case 0x1F9B9: + case 0x1F9CD: + case 0x1F9CE: + case 0x1F9CF: + case 0x1F9D1: + case 0x1F9D2: + case 0x1F9D3: + case 0x1F9D4: + case 0x1F9D5: + case 0x1F9D6: + case 0x1F9D7: + case 0x1F9D8: + case 0x1F9D9: + case 0x1F9DA: + case 0x1F9DB: + case 0x1F9DC: + case 0x1F9DD: + case 0x1F9DE: + case 0x1F9DF: + if (w >= 0x1F3FB && w <= 0x1F3FF) + return (1); + break; } - return (0); + return 0; } static enum hanguljamo_subclass diff --git a/utf8.c b/utf8.c index d9e69df4..e877f2d0 100644 --- a/utf8.c +++ b/utf8.c @@ -56,32 +56,32 @@ static struct utf8_width_item utf8_default_width_cache[] = { { .wc = 0x0270B, .width = 2 }, { .wc = 0x0270C, .width = 2 }, { .wc = 0x0270D, .width = 2 }, - { .wc = 0x1F1E6, .width = 2 }, - { .wc = 0x1F1E7, .width = 2 }, - { .wc = 0x1F1E8, .width = 2 }, - { .wc = 0x1F1E9, .width = 2 }, - { .wc = 0x1F1EA, .width = 2 }, - { .wc = 0x1F1EB, .width = 2 }, - { .wc = 0x1F1EC, .width = 2 }, - { .wc = 0x1F1ED, .width = 2 }, - { .wc = 0x1F1EE, .width = 2 }, - { .wc = 0x1F1EF, .width = 2 }, - { .wc = 0x1F1F0, .width = 2 }, - { .wc = 0x1F1F1, .width = 2 }, - { .wc = 0x1F1F2, .width = 2 }, - { .wc = 0x1F1F3, .width = 2 }, - { .wc = 0x1F1F4, .width = 2 }, - { .wc = 0x1F1F5, .width = 2 }, - { .wc = 0x1F1F6, .width = 2 }, - { .wc = 0x1F1F7, .width = 2 }, - { .wc = 0x1F1F8, .width = 2 }, - { .wc = 0x1F1F9, .width = 2 }, - { .wc = 0x1F1FA, .width = 2 }, - { .wc = 0x1F1FB, .width = 2 }, - { .wc = 0x1F1FC, .width = 2 }, - { .wc = 0x1F1FD, .width = 2 }, - { .wc = 0x1F1FE, .width = 2 }, - { .wc = 0x1F1FF, .width = 2 }, + { .wc = 0x1F1E6, .width = 1 }, + { .wc = 0x1F1E7, .width = 1 }, + { .wc = 0x1F1E8, .width = 1 }, + { .wc = 0x1F1E9, .width = 1 }, + { .wc = 0x1F1EA, .width = 1 }, + { .wc = 0x1F1EB, .width = 1 }, + { .wc = 0x1F1EC, .width = 1 }, + { .wc = 0x1F1ED, .width = 1 }, + { .wc = 0x1F1EE, .width = 1 }, + { .wc = 0x1F1EF, .width = 1 }, + { .wc = 0x1F1F0, .width = 1 }, + { .wc = 0x1F1F1, .width = 1 }, + { .wc = 0x1F1F2, .width = 1 }, + { .wc = 0x1F1F3, .width = 1 }, + { .wc = 0x1F1F4, .width = 1 }, + { .wc = 0x1F1F5, .width = 1 }, + { .wc = 0x1F1F6, .width = 1 }, + { .wc = 0x1F1F7, .width = 1 }, + { .wc = 0x1F1F8, .width = 1 }, + { .wc = 0x1F1F9, .width = 1 }, + { .wc = 0x1F1FA, .width = 1 }, + { .wc = 0x1F1FB, .width = 1 }, + { .wc = 0x1F1FC, .width = 1 }, + { .wc = 0x1F1FD, .width = 1 }, + { .wc = 0x1F1FE, .width = 1 }, + { .wc = 0x1F1FF, .width = 1 }, { .wc = 0x1F385, .width = 2 }, { .wc = 0x1F3C2, .width = 2 }, { .wc = 0x1F3C3, .width = 2 }, From 3051076dd16643a683a860c8ff5a24b47559261e Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 3 Nov 2025 09:27:06 +0000 Subject: [PATCH 16/27] Ignore Hangul filler character. There doesn't seem to be much agreement on what to do with this but ignoring it seems rightand does improve things. GitHub issue 3998. --- screen-write.c | 4 ++++ tmux.h | 1 + utf8-combined.c | 13 +++++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/screen-write.c b/screen-write.c index 1014ae80..8e04681a 100644 --- a/screen-write.c +++ b/screen-write.c @@ -2006,6 +2006,10 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) struct tty_ctx ttyctx; int force_wide = 0, zero_width = 0; + /* Ignore U+3164 HANGUL_FILLER entirely. */ + if (utf8_is_hangul_filler(ud)) + return (1); + /* * Is this character which makes no sense without being combined? If * this is true then flag it here and discard the character (return 1) diff --git a/tmux.h b/tmux.h index aa25a408..356d1157 100644 --- a/tmux.h +++ b/tmux.h @@ -3499,6 +3499,7 @@ int utf8_cstrhas(const char *, const struct utf8_data *); int utf8_has_zwj(const struct utf8_data *); int utf8_is_zwj(const struct utf8_data *); int utf8_is_vs(const struct utf8_data *); +int utf8_is_hangul_filler(const struct utf8_data *); int utf8_should_combine(const struct utf8_data *, const struct utf8_data *); enum hanguljamo_state hanguljamo_check_state(const struct utf8_data *, diff --git a/utf8-combined.c b/utf8-combined.c index 91ddaf75..635ae92c 100644 --- a/utf8-combined.c +++ b/utf8-combined.c @@ -55,7 +55,7 @@ utf8_has_zwj(const struct utf8_data *ud) return (memcmp(ud->data + ud->size - 3, "\342\200\215", 3) == 0); } -/* Is this a zero width joiner? */ +/* Is this zero width joiner U+200D? */ int utf8_is_zwj(const struct utf8_data *ud) { @@ -64,7 +64,7 @@ utf8_is_zwj(const struct utf8_data *ud) return (memcmp(ud->data, "\342\200\215", 3) == 0); } -/* Is this a variation selector? */ +/* Is this variation selector U+FE0F? */ int utf8_is_vs(const struct utf8_data *ud) { @@ -73,6 +73,15 @@ utf8_is_vs(const struct utf8_data *ud) return (memcmp(ud->data, "\357\270\217", 3) == 0); } +/* Is this Hangul filler U+3164? */ +int +utf8_is_hangul_filler(const struct utf8_data *ud) +{ + if (ud->size != 3) + return (0); + return (memcmp(ud->data, "\343\205\244", 3) == 0); +} + /* Should these two characters combine? */ int utf8_should_combine(const struct utf8_data *with, const struct utf8_data *add) From c1667bd85ea7674151addadc725bb65be53d01ce Mon Sep 17 00:00:00 2001 From: jsg Date: Fri, 7 Nov 2025 12:30:36 +0000 Subject: [PATCH 17/27] consistently use tabs for indentation found with smatch, ok nicm@ --- window-copy.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/window-copy.c b/window-copy.c index 3b0f11dc..137c97b2 100644 --- a/window-copy.c +++ b/window-copy.c @@ -567,8 +567,8 @@ window_copy_vadd(struct window_pane *wp, int parse, const char *fmt, va_list ap) vasprintf(&text, fmt, ap); input_parse_screen(data->ictx, backing, window_copy_init_ctx_cb, data, text, strlen(text)); - free(text); - } else { + free(text); + } else { memcpy(&gc, &grid_default_cell, sizeof gc); screen_write_vnputs(&backing_ctx, 0, &gc, fmt, ap); } From d65bac979db317cfc6e0db1b48d03f668eaa80e9 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 7 Nov 2025 12:36:07 +0000 Subject: [PATCH 18/27] Do not try to allocate zero size colours, GitHub issue 4674. --- image-sixel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image-sixel.c b/image-sixel.c index fac7eab9..1c93d7a7 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -474,7 +474,7 @@ sixel_scale(struct sixel_image *si, u_int xpixel, u_int ypixel, u_int ox, } } - if (colours) { + if (colours && si->ncolours != 0) { new->colours = xmalloc(si->ncolours * sizeof *new->colours); for (i = 0; i < si->ncolours; i++) new->colours[i] = si->colours[i]; From 0d0ef5d0d235831dac7750c954d50ee0e1f3a056 Mon Sep 17 00:00:00 2001 From: jsg Date: Fri, 7 Nov 2025 12:43:47 +0000 Subject: [PATCH 19/27] correct test for COLORTERM containing 256 found with smatch, ok nicm@ --- tty-term.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tty-term.c b/tty-term.c index b6b0dfce..f664260a 100644 --- a/tty-term.c +++ b/tty-term.c @@ -603,7 +603,7 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, if (strcasecmp(envent->value, "truecolor") == 0 || strcasecmp(envent->value, "24bit") == 0) tty_add_features(feat, "RGB", ","); - else if (strstr(s, "256") != NULL) + else if (strstr(envent->value, "256") != NULL) tty_add_features(feat, "256", ","); } From c77d49f67eaaa311ea36e5c451740011e3e2c2f4 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 12 Nov 2025 07:49:17 +0000 Subject: [PATCH 20/27] Save and restore images in alternate screen, GitHub issue 3732. --- screen.c | 10 ++++++++++ tmux.h | 1 + 2 files changed, 11 insertions(+) diff --git a/screen.c b/screen.c index 8bfb7071..a7a13af2 100644 --- a/screen.c +++ b/screen.c @@ -91,6 +91,7 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit) #ifdef ENABLE_SIXEL TAILQ_INIT(&s->images); + TAILQ_INIT(&s->saved_images); #endif s->write_list = NULL; @@ -643,6 +644,10 @@ screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor) } memcpy(&s->saved_cell, gc, sizeof s->saved_cell); +#ifdef ENABLE_SIXEL + TAILQ_CONCAT(&s->saved_images, &s->images, entry); +#endif + grid_view_clear(s->grid, 0, 0, sx, sy, 8); s->saved_flags = s->grid->flags; @@ -697,6 +702,11 @@ screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor) grid_destroy(s->saved_grid); s->saved_grid = NULL; +#ifdef ENABLE_SIXEL + image_free_all(s); + TAILQ_CONCAT(&s->images, &s->saved_images, entry); +#endif + if (s->cx > screen_size_x(s) - 1) s->cx = screen_size_x(s) - 1; if (s->cy > screen_size_y(s) - 1) diff --git a/tmux.h b/tmux.h index 414c2819..d7ea3aad 100644 --- a/tmux.h +++ b/tmux.h @@ -984,6 +984,7 @@ struct screen { #ifdef ENABLE_SIXEL struct images images; + struct images saved_images; #endif struct screen_write_cline *write_list; From 9e21f426c0d5f24bda6c8a2e066c71388824716f Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 12 Nov 2025 07:53:22 +0000 Subject: [PATCH 21/27] Add a command to explcitly set the selection mode in copy mode, GitHub issue 3842. --- tmux.1 | 5 +++++ window-copy.c | 28 +++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/tmux.1 b/tmux.1 index 8b057120..e4c55d33 100644 --- a/tmux.1 +++ b/tmux.1 @@ -2293,6 +2293,11 @@ Select the current line. .Xc Select the current word. .It Xo +.Ic selection-mode +.Op Ic char | word | line +.Xc +Change the selection mode. +.It Xo .Ic set-mark (vi: X) (emacs: X) diff --git a/window-copy.c b/window-copy.c index 137c97b2..f056cebb 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1970,6 +1970,24 @@ window_copy_cmd_other_end(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_NOTHING); } +static enum window_copy_cmd_action +window_copy_cmd_selection_mode(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct options *so = cs->s->options; + struct window_copy_mode_data *data = wme->data; + const char *s = args_string(cs->wargs, 0); + + if (s == NULL || strcasecmp(s, "char") == 0 || strcasecmp(s, "c") == 0) + data->selflag = SEL_CHAR; + else if (strcasecmp(s, "word") == 0 || strcasecmp(s, "w") == 0) { + data->separators = options_get_string(so, "word-separators"); + data->selflag = SEL_WORD; + } else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0) + data->selflag = SEL_LINE; + return (WINDOW_COPY_CMD_NOTHING); +} + static enum window_copy_cmd_action window_copy_cmd_page_down(struct window_copy_cmd_state *cs) { @@ -2186,7 +2204,7 @@ static enum window_copy_cmd_action window_copy_cmd_select_word(struct window_copy_cmd_state *cs) { struct window_mode_entry *wme = cs->wme; - struct options *session_options = cs->s->options; + struct options *so = cs->s->options; struct window_copy_mode_data *data = wme->data; u_int px, py, nextx, nexty; @@ -2196,8 +2214,7 @@ window_copy_cmd_select_word(struct window_copy_cmd_state *cs) data->dx = data->cx; data->dy = screen_hsize(data->backing) + data->cy - data->oy; - data->separators = options_get_string(session_options, - "word-separators"); + data->separators = options_get_string(so, "word-separators"); window_copy_cursor_previous_word(wme, data->separators, 0); px = data->cx; py = screen_hsize(data->backing) + data->cy - data->oy; @@ -3087,6 +3104,11 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, .f = window_copy_cmd_select_word }, + { .command = "selection-mode", + .args = { "", 0, 1, NULL }, + .clear = 0, + .f = window_copy_cmd_selection_mode + }, { .command = "set-mark", .args = { "", 0, 0, NULL }, .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, From 12497ecadeb6d28c9b393016a822fe3c32217238 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 12 Nov 2025 07:54:52 +0000 Subject: [PATCH 22/27] Convert all keys for backspace, not just A-Z. --- input-keys.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/input-keys.c b/input-keys.c index 91415064..7d7263fb 100644 --- a/input-keys.c +++ b/input-keys.c @@ -606,7 +606,9 @@ input_key(struct screen *s, struct bufferevent *bev, key_code key) ud.data[0] = newkey; else if ((newkey & KEYC_MASK_MODIFIERS) == KEYC_CTRL) { newkey &= KEYC_MASK_KEY; - if (newkey >= 'A' && newkey <= 'Z') + if (newkey == '?') + ud.data[0] = 0x7f; + else if (newkey >= '@' && newkey <= '_') ud.data[0] = newkey - 0x40; else if (newkey >= 'a' && newkey <= 'z') ud.data[0] = newkey - 0x60; From 40600eebfaedef4bbae633308241d5d2f70edfdb Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 12 Nov 2025 08:06:33 +0000 Subject: [PATCH 23/27] Do not send theme updates to panes that have exited, GitHub issue 4671. --- window.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/window.c b/window.c index 2429d7b7..16f1ecb3 100644 --- a/window.c +++ b/window.c @@ -1922,6 +1922,8 @@ window_pane_get_theme(struct window_pane *wp) void window_pane_send_theme_update(struct window_pane *wp) { + if (wp == NULL || window_pane_exited(wp)) + return; if (~wp->flags & PANE_THEMECHANGED) return; if (~wp->screen->mode & MODE_THEME_UPDATES) From 3c9e10139f88b462c1de0a81d4c58fc4ea50f8b3 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 12 Nov 2025 13:47:51 +0000 Subject: [PATCH 24/27] If display-popup is used inside a popup, modify that popup. From m-einfalt at gmx dot de in GitHub issue 4678. --- cmd-display-menu.c | 148 ++++++++++++++++++++++++++------------------- popup.c | 50 +++++++++++++++ tmux.1 | 19 +++++- tmux.h | 3 + 4 files changed, 157 insertions(+), 63 deletions(-) diff --git a/cmd-display-menu.c b/cmd-display-menu.c index bd5012f2..894a2f68 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -55,8 +55,8 @@ const struct cmd_entry cmd_display_popup_entry = { .name = "display-popup", .alias = "popup", - .args = { "Bb:Cc:d:e:Eh:ks:S:t:T:w:x:y:", 0, -1, NULL }, - .usage = "[-BCEk] [-b border-lines] [-c target-client] " + .args = { "Bb:Cc:d:e:Eh:kNs:S:t:T:w:x:y:", 0, -1, NULL }, + .usage = "[-BCEkN] [-b border-lines] [-c target-client] " "[-d start-directory] [-e environment] [-h height] " "[-s style] [-S border-style] " CMD_TARGET_PANE_USAGE " [-T title] [-w width] [-x position] [-y position] " @@ -93,7 +93,7 @@ cmd_display_menu_args_parse(struct args *args, u_int idx, __unused char **cause) } static int -cmd_display_menu_get_position(struct client *tc, struct cmdq_item *item, +cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item, struct args *args, u_int *px, u_int *py, u_int w, u_int h) { struct tty *tty = &tc->tty; @@ -354,8 +354,8 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item) menu_free(menu); return (CMD_RETURN_NORMAL); } - if (!cmd_display_menu_get_position(tc, item, args, &px, &py, - menu->width + 4, menu->count + 2)) { + if (!cmd_display_menu_get_pos(tc, item, args, &px, &py, menu->width + 4, + menu->count + 2)) { menu_free(menu); return (CMD_RETURN_NORMAL); } @@ -393,8 +393,10 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item) const char *value, *shell, *shellcmd = NULL; const char *style = args_get(args, 's'); const char *border_style = args_get(args, 'S'); - char *cwd, *cause = NULL, **argv = NULL, *title; - int flags = 0, argc = 0; + char *cwd = NULL, *cause = NULL, **argv = NULL; + char *title; + int modify = popup_present(tc); + int flags = -1, argc = 0; enum box_lines lines = BOX_LINES_DEFAULT; u_int px, py, w, h, count = args_count(args); struct args_value *av; @@ -406,36 +408,68 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item) server_client_clear_overlay(tc); return (CMD_RETURN_NORMAL); } - if (tc->overlay_draw != NULL) + if (!modify && tc->overlay_draw != NULL) return (CMD_RETURN_NORMAL); - h = tty->sy / 2; - if (args_has(args, 'h')) { - h = args_percentage(args, 'h', 1, tty->sy, tty->sy, &cause); - if (cause != NULL) { - cmdq_error(item, "height %s", cause); - free(cause); - return (CMD_RETURN_ERROR); + if (!modify) { + h = tty->sy / 2; + if (args_has(args, 'h')) { + h = args_percentage(args, 'h', 1, tty->sy, tty->sy, + &cause); + if (cause != NULL) { + cmdq_error(item, "height %s", cause); + free(cause); + return (CMD_RETURN_ERROR); + } + } + + w = tty->sx / 2; + if (args_has(args, 'w')) { + w = args_percentage(args, 'w', 1, tty->sx, tty->sx, + &cause); + if (cause != NULL) { + cmdq_error(item, "width %s", cause); + free(cause); + return (CMD_RETURN_ERROR); + } + } + + if (w > tty->sx) + w = tty->sx; + if (h > tty->sy) + h = tty->sy; + if (!cmd_display_menu_get_pos(tc, item, args, &px, &py, w, h)) + return (CMD_RETURN_NORMAL); + + value = args_get(args, 'd'); + if (value != NULL) + cwd = format_single_from_target(item, value); + else + cwd = xstrdup(server_client_get_cwd(tc, s)); + if (count == 0) { + shellcmd = options_get_string(s->options, + "default-command"); + } else if (count == 1) + shellcmd = args_string(args, 0); + if (count <= 1 && (shellcmd == NULL || *shellcmd == '\0')) { + shellcmd = NULL; + shell = options_get_string(s->options, "default-shell"); + if (!checkshell(shell)) + shell = _PATH_BSHELL; + cmd_append_argv(&argc, &argv, shell); + } else + args_to_vector(args, &argc, &argv); + + if (args_has(args, 'e') >= 1) { + env = environ_create(); + av = args_first_value(args, 'e'); + while (av != NULL) { + environ_put(env, av->string, 0); + av = args_next_value(av); + } } } - w = tty->sx / 2; - if (args_has(args, 'w')) { - w = args_percentage(args, 'w', 1, tty->sx, tty->sx, &cause); - if (cause != NULL) { - cmdq_error(item, "width %s", cause); - free(cause); - return (CMD_RETURN_ERROR); - } - } - - if (w > tty->sx) - w = tty->sx; - if (h > tty->sy) - h = tty->sy; - if (!cmd_display_menu_get_position(tc, item, args, &px, &py, w, h)) - return (CMD_RETURN_NORMAL); - value = args_get(args, 'b'); if (args_has(args, 'B')) lines = BOX_LINES_NONE; @@ -450,43 +484,33 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item) } } - value = args_get(args, 'd'); - if (value != NULL) - cwd = format_single_from_target(item, value); - else - cwd = xstrdup(server_client_get_cwd(tc, s)); - if (count == 0) - shellcmd = options_get_string(s->options, "default-command"); - else if (count == 1) - shellcmd = args_string(args, 0); - if (count <= 1 && (shellcmd == NULL || *shellcmd == '\0')) { - shellcmd = NULL; - shell = options_get_string(s->options, "default-shell"); - if (!checkshell(shell)) - shell = _PATH_BSHELL; - cmd_append_argv(&argc, &argv, shell); - } else - args_to_vector(args, &argc, &argv); - - if (args_has(args, 'e') >= 1) { - env = environ_create(); - av = args_first_value(args, 'e'); - while (av != NULL) { - environ_put(env, av->string, 0); - av = args_next_value(av); - } - } - if (args_has(args, 'T')) title = format_single_from_target(item, args_get(args, 'T')); else title = xstrdup(""); - if (args_has(args, 'E') > 1) + + if (args_has(args, 'N') || !modify) + flags = 0; + if (args_has(args, 'E') > 1) { + if (flags == -1) + flags = 0; flags |= POPUP_CLOSEEXITZERO; - else if (args_has(args, 'E')) + } else if (args_has(args, 'E')) { + if (flags == -1) + flags = 0; flags |= POPUP_CLOSEEXIT; - if (args_has(args, 'k')) + } + if (args_has(args, 'k')) { + if (flags == -1) + flags = 0; flags |= POPUP_CLOSEANYKEY; + } + + if (modify) { + popup_modify(tc, title, style, border_style, lines, flags); + free(title); + return (CMD_RETURN_NORMAL); + } if (popup_display(flags, lines, item, px, py, w, h, env, shellcmd, argc, argv, cwd, title, tc, s, style, border_style, NULL, NULL) != 0) { cmd_free_argv(argc, argv); diff --git a/popup.c b/popup.c index 619a32ce..12cd7362 100644 --- a/popup.c +++ b/popup.c @@ -640,6 +640,56 @@ popup_job_complete_cb(struct job *job) server_client_clear_overlay(pd->c); } +int +popup_present(struct client *c) +{ + return (c->overlay_draw == popup_draw_cb); +} + +int +popup_modify(struct client *c, const char *title, const char *style, + const char *border_style, enum box_lines lines, int flags) +{ + struct popup_data *pd = c->overlay_data; + struct style sytmp; + + if (title != NULL) { + if (pd->title != NULL) + free(pd->title); + pd->title = xstrdup(title); + } + if (border_style != NULL) { + style_set(&sytmp, &pd->border_cell); + if (style_parse(&sytmp, &pd->border_cell, border_style) == 0) { + pd->border_cell.fg = sytmp.gc.fg; + pd->border_cell.bg = sytmp.gc.bg; + } + } + if (style != NULL) { + style_set(&sytmp, &pd->defaults); + if (style_parse(&sytmp, &pd->defaults, style) == 0) { + pd->defaults.fg = sytmp.gc.fg; + pd->defaults.bg = sytmp.gc.bg; + } + } + if (lines != BOX_LINES_DEFAULT) { + if (lines == BOX_LINES_NONE && pd->border_lines != lines) { + screen_resize(&pd->s, pd->sx, pd->sy, 1); + job_resize(pd->job, pd->sx, pd->sy); + } else if (pd->border_lines == BOX_LINES_NONE && pd->border_lines != lines) { + screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 1); + job_resize(pd->job, pd->sx - 2, pd->sy - 2); + } + pd->border_lines = lines; + tty_resize(&c->tty); + } + if (flags != -1) + pd->flags = flags; + + server_redraw_client(c); + return (0); +} + int popup_display(int flags, enum box_lines lines, struct cmdq_item *item, u_int px, u_int py, u_int sx, u_int sy, struct environ *env, const char *shellcmd, diff --git a/tmux.1 b/tmux.1 index e4c55d33..9a602927 100644 --- a/tmux.1 +++ b/tmux.1 @@ -6999,7 +6999,7 @@ forwards any input read from stdin to the empty pane given by .Ar target-pane . .Tg popup .It Xo Ic display-popup -.Op Fl BCEk +.Op Fl BCEkN .Op Fl b Ar border-lines .Op Fl c Ar target-client .Op Fl d Ar start-directory @@ -7023,6 +7023,20 @@ when omitted) on .Ar target-client . A popup is a rectangular box drawn over the top of any panes. Panes are not updated while a popup is present. +If the command is run inside an existing popup, that popup is modified. +Only the +.Fl b , +.Fl B , +.Fl C , +.Fl E , +.Fl EE , +.Fl K , +.Fl N , +.Fl s , +and +.Fl S +options are accepted in this case; +all others are ignored. .Pp .Fl E closes the popup automatically when @@ -7086,6 +7100,9 @@ is a format for the popup title (see The .Fl C flag closes any popup on the client. +.Pp +.Fl N +disables any previously specified -E, -EE, or -k option. .Tg showphist .It Xo Ic show-prompt-history .Op Fl T Ar prompt-type diff --git a/tmux.h b/tmux.h index 356d1157..736d0f55 100644 --- a/tmux.h +++ b/tmux.h @@ -3561,6 +3561,9 @@ int popup_display(int, enum box_lines, struct cmdq_item *, u_int, popup_close_cb, void *); int popup_editor(struct client *, const char *, size_t, popup_finish_edit_cb, void *); +int popup_present(struct client *); +int popup_modify(struct client *, const char *, const char *, + const char *, enum box_lines, int); /* style.c */ int style_parse(struct style *,const struct grid_cell *, From 66279c124eaaa179d89fc145a654c3fbbd5f0a60 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 12 Nov 2025 20:41:06 +0000 Subject: [PATCH 25/27] Make requests to the external terminal one queue instead of one by type, and include any other requests as well so that ordering is maintained. --- input.c | 244 ++++++++++++++++++++++++++---------------------- server-client.c | 7 +- tmux.h | 11 +-- 3 files changed, 137 insertions(+), 125 deletions(-) diff --git a/input.c b/input.c index bf6cb48a..f56f90c2 100644 --- a/input.c +++ b/input.c @@ -59,11 +59,12 @@ struct input_request { enum input_request_type type; int idx; time_t t; + void *data; TAILQ_ENTRY(input_request) entry; TAILQ_ENTRY(input_request) centry; }; -#define INPUT_REQUEST_TIMEOUT 5 +#define INPUT_REQUEST_TIMEOUT 2 /* Input parser cell. */ struct input_cell { @@ -131,7 +132,7 @@ struct input_ctx { #define INPUT_DISCARD 0x1 #define INPUT_LAST 0x2 - struct input_requests requests[INPUT_REQUEST_TYPES]; + struct input_requests requests; u_int request_count; struct event request_timer; @@ -147,11 +148,13 @@ struct input_ctx { struct input_transition; static void input_request_timer_callback(int, short, void *); static void input_start_request_timer(struct input_ctx *); +static struct input_request *input_make_request(struct input_ctx *, + enum input_request_type); +static void input_free_request(struct input_request *); static int input_add_request(struct input_ctx *, enum input_request_type, int); static int input_split(struct input_ctx *); static int input_get(struct input_ctx *, u_int, int, int); -static void printflike(2, 3) input_reply(struct input_ctx *, const char *, ...); static void input_set_state(struct input_ctx *, const struct input_transition *); static void input_reset_cell(struct input_ctx *); @@ -852,7 +855,6 @@ input_init(struct window_pane *wp, struct bufferevent *bev, struct colour_palette *palette) { struct input_ctx *ictx; - u_int i; ictx = xcalloc(1, sizeof *ictx); ictx->wp = wp; @@ -867,8 +869,7 @@ input_init(struct window_pane *wp, struct bufferevent *bev, fatalx("out of memory"); evtimer_set(&ictx->ground_timer, input_ground_timer_callback, ictx); - for (i = 0; i < INPUT_REQUEST_TYPES; i++) - TAILQ_INIT(&ictx->requests[i]); + TAILQ_INIT(&ictx->requests); evtimer_set(&ictx->request_timer, input_request_timer_callback, ictx); input_reset(ictx, 0); @@ -879,25 +880,16 @@ input_init(struct window_pane *wp, struct bufferevent *bev, void input_free(struct input_ctx *ictx) { - struct input_request_list *irl; - struct input_request *ir, *ir1; - u_int i; + struct input_request *ir, *ir1; + u_int i; for (i = 0; i < ictx->param_list_len; i++) { if (ictx->param_list[i].type == INPUT_STRING) free(ictx->param_list[i].str); } - for (i = 0; i < INPUT_REQUEST_TYPES; i++) { - TAILQ_FOREACH_SAFE(ir, &ictx->requests[i], entry, ir1) { - log_debug("%s: req %p: client %s, pane %%%u, type %d", - __func__, ir, ir->c->name, ictx->wp->id, i); - irl = &ir->c->input_requests[i]; - TAILQ_REMOVE(&ictx->requests[i], ir, entry); - TAILQ_REMOVE(&irl->requests, ir, centry); - free(ir); - } - } + TAILQ_FOREACH_SAFE(ir, &ictx->requests, entry, ir1) + input_free_request(ir); event_del(&ictx->request_timer); free(ictx->input_buf); @@ -1137,24 +1129,37 @@ input_get(struct input_ctx *ictx, u_int validx, int minval, int defval) return (retval); } -/* Reply to terminal query. */ +/* Send reply. */ static void -input_reply(struct input_ctx *ictx, const char *fmt, ...) +input_send_reply(struct input_ctx *ictx, const char *reply) { struct bufferevent *bev = ictx->event; + + if (bev != NULL) { + log_debug("%s: %s", __func__, reply); + bufferevent_write(bev, reply, strlen(reply)); + } +} + +/* Reply to terminal query. */ +static void printflike(3, 4) +input_reply(struct input_ctx *ictx, int add, const char *fmt, ...) +{ + struct input_request *ir; va_list ap; char *reply; - if (bev == NULL) - return; - va_start(ap, fmt); xvasprintf(&reply, fmt, ap); va_end(ap); - log_debug("%s: %s", __func__, reply); - bufferevent_write(bev, reply, strlen(reply)); - free(reply); + if (add && !TAILQ_EMPTY(&ictx->requests)) { + ir = input_make_request(ictx, INPUT_REQUEST_QUEUE); + ir->data = reply; + } else { + input_send_reply(ictx, reply); + free(reply); + } } /* Clear saved state. */ @@ -1547,7 +1552,7 @@ input_csi_dispatch(struct input_ctx *ictx) case -1: break; case 0: - input_reply(ictx, "\033[?1;2c"); + input_reply(ictx, 1, "\033[?1;2c"); break; default: log_debug("%s: unknown '%c'", __func__, ictx->ch); @@ -1559,7 +1564,7 @@ input_csi_dispatch(struct input_ctx *ictx) case -1: break; case 0: - input_reply(ictx, "\033[>84;0;0c"); + input_reply(ictx, 1, "\033[>84;0;0c"); break; default: log_debug("%s: unknown '%c'", __func__, ictx->ch); @@ -1610,22 +1615,22 @@ input_csi_dispatch(struct input_ctx *ictx) /* blink for 1,3,5; steady for 0,2,4,6 */ n = (p == 1 || p == 3 || p == 5) ? 1 : 2; } - input_reply(ictx, "\033[?12;%d$y", n); + input_reply(ictx, 1, "\033[?12;%d$y", n); break; case 2004: /* bracketed paste */ n = (s->mode & MODE_BRACKETPASTE) ? 1 : 2; - input_reply(ictx, "\033[?2004;%d$y", n); + input_reply(ictx, 1, "\033[?2004;%d$y", n); break; case 1004: /* focus reporting */ n = (s->mode & MODE_FOCUSON) ? 1 : 2; - input_reply(ictx, "\033[?1004;%d$y", n); + input_reply(ictx, 1, "\033[?1004;%d$y", n); break; case 1006: /* SGR mouse */ n = (s->mode & MODE_MOUSE_SGR) ? 1 : 2; - input_reply(ictx, "\033[?1006;%d$y", n); + input_reply(ictx, 1, "\033[?1006;%d$y", n); break; case 2031: - input_reply(ictx, "\033[?2031;2$y"); + input_reply(ictx, 1, "\033[?2031;2$y"); break; } break; @@ -1634,10 +1639,11 @@ input_csi_dispatch(struct input_ctx *ictx) case -1: break; case 5: - input_reply(ictx, "\033[0n"); + input_reply(ictx, 1, "\033[0n"); break; case 6: - input_reply(ictx, "\033[%u;%uR", s->cy + 1, s->cx + 1); + input_reply(ictx, 1, "\033[%u;%uR", s->cy + 1, + s->cx + 1); break; default: log_debug("%s: unknown '%c'", __func__, ictx->ch); @@ -1792,8 +1798,10 @@ input_csi_dispatch(struct input_ctx *ictx) break; case INPUT_CSI_XDA: n = input_get(ictx, 0, 0, 0); - if (n == 0) - input_reply(ictx, "\033P>|tmux %s\033\\", getversion()); + if (n == 0) { + input_reply(ictx, 1, "\033P>|tmux %s\033\\", + getversion()); + } break; } @@ -2042,26 +2050,26 @@ input_csi_dispatch_winops(struct input_ctx *ictx) case 14: if (w == NULL) break; - input_reply(ictx, "\033[4;%u;%ut", y * w->ypixel, + input_reply(ictx, 1, "\033[4;%u;%ut", y * w->ypixel, x * w->xpixel); break; case 15: if (w == NULL) break; - input_reply(ictx, "\033[5;%u;%ut", y * w->ypixel, + input_reply(ictx, 1, "\033[5;%u;%ut", y * w->ypixel, x * w->xpixel); break; case 16: if (w == NULL) break; - input_reply(ictx, "\033[6;%u;%ut", w->ypixel, + input_reply(ictx, 1, "\033[6;%u;%ut", w->ypixel, w->xpixel); break; case 18: - input_reply(ictx, "\033[8;%u;%ut", y, x); + input_reply(ictx, 1, "\033[8;%u;%ut", y, x); break; case 19: - input_reply(ictx, "\033[9;%u;%ut", y, x); + input_reply(ictx, 1, "\033[9;%u;%ut", y, x); break; case 22: m++; @@ -2485,12 +2493,12 @@ input_handle_decrqss(struct input_ctx *ictx) log_debug("%s: DECRQSS cursor -> Ps=%d (cstyle=%d mode=%#x)", __func__, ps, s->cstyle, s->mode); - input_reply(ictx, "\033P1$r q%d q\033\\", ps); + input_reply(ictx, 1, "\033P1$r q%d q\033\\", ps); return (0); not_recognized: /* Unrecognized DECRQSS: send DCS 0 $ r Pt ST. */ - input_reply(ictx, "\033P0$r\033\\"); + input_reply(ictx, 1, "\033P0$r\033\\"); return (0); } @@ -2755,7 +2763,7 @@ input_top_bit_set(struct input_ctx *ictx) /* Reply to a colour request. */ static void -input_osc_colour_reply(struct input_ctx *ictx, u_int n, int idx, int c) +input_osc_colour_reply(struct input_ctx *ictx, int add, u_int n, int idx, int c) { u_char r, g, b; const char *end; @@ -2772,11 +2780,11 @@ input_osc_colour_reply(struct input_ctx *ictx, u_int n, int idx, int c) end = "\033\\"; if (n == 4) { - input_reply(ictx, + input_reply(ictx, add, "\033]%u;%d;rgb:%02hhx%02hhx/%02hhx%02hhx/%02hhx%02hhx%s", n, idx, r, r, g, g, b, b, end); } else { - input_reply(ictx, + input_reply(ictx, add, "\033]%u;rgb:%02hhx%02hhx/%02hhx%02hhx/%02hhx%02hhx%s", n, r, r, g, g, b, b, end); } @@ -2807,7 +2815,7 @@ input_osc_4(struct input_ctx *ictx, const char *p) if (strcmp(s, "?") == 0) { c = colour_palette_get(palette, idx|COLOUR_FLAG_256); if (c != -1) { - input_osc_colour_reply(ictx, 4, idx, c); + input_osc_colour_reply(ictx, 1, 4, idx, c); s = next; continue; } @@ -2891,7 +2899,7 @@ input_osc_10(struct input_ctx *ictx, const char *p) else c = defaults.fg; } - input_osc_colour_reply(ictx, 10, 0, c); + input_osc_colour_reply(ictx, 1, 10, 0, c); return; } @@ -2934,7 +2942,7 @@ input_osc_11(struct input_ctx *ictx, const char *p) if (wp == NULL) return; c = window_pane_get_bg(wp); - input_osc_colour_reply(ictx, 11, 0, c); + input_osc_colour_reply(ictx, 1, 11, 0, c); return; } @@ -2978,7 +2986,7 @@ input_osc_12(struct input_ctx *ictx, const char *p) c = ictx->ctx.s->ccolour; if (c == -1) c = ictx->ctx.s->default_ccolour; - input_osc_colour_reply(ictx, 12, 0, c); + input_osc_colour_reply(ictx, 1, 12, 0, c); } return; } @@ -3157,24 +3165,16 @@ input_set_buffer_size(size_t buffer_size) static void input_request_timer_callback(__unused int fd, __unused short events, void *arg) { - struct input_ctx *ictx = arg; - struct input_request *ir, *ir1; - struct input_request_list *irl; - u_int i; - time_t t = time(NULL); + struct input_ctx *ictx = arg; + struct input_request *ir, *ir1; + time_t t = time(NULL); - for (i = 0; i < INPUT_REQUEST_TYPES; i++) { - TAILQ_FOREACH_SAFE(ir, &ictx->requests[i], entry, ir1) { - if (ir->t >= t - INPUT_REQUEST_TIMEOUT) - continue; - log_debug("%s: req %p: client %s, pane %%%u, type %d", - __func__, ir, ir->c->name, ictx->wp->id, ir->type); - irl = &ir->c->input_requests[i]; - TAILQ_REMOVE(&ictx->requests[i], ir, entry); - TAILQ_REMOVE(&irl->requests, ir, centry); - ictx->request_count--; - free(ir); - } + TAILQ_FOREACH_SAFE(ir, &ictx->requests, entry, ir1) { + if (ir->t >= t - INPUT_REQUEST_TIMEOUT) + continue; + if (ir->type == INPUT_REQUEST_QUEUE) + input_send_reply(ir->ictx, ir->data); + input_free_request(ir); } if (ictx->request_count != 0) input_start_request_timer(ictx); @@ -3184,12 +3184,46 @@ 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 = 1, .tv_usec = 0 }; + struct timeval tv = { .tv_sec = 0, .tv_usec = 500000 }; event_del(&ictx->request_timer); event_add(&ictx->request_timer, &tv); } +/* Create a request. */ +static struct input_request * +input_make_request(struct input_ctx *ictx, enum input_request_type type) +{ + struct input_request *ir; + + ir = xcalloc (1, sizeof *ir); + ir->type = type; + ir->ictx = ictx; + ir->t = time(NULL); + + if (++ictx->request_count == 1) + input_start_request_timer(ictx); + TAILQ_INSERT_TAIL(&ictx->requests, ir, entry); + + return (ir); +} + +/* Free a request. */ +static void +input_free_request(struct input_request *ir) +{ + struct input_ctx *ictx = ir->ictx; + + if (ir->c != NULL) + TAILQ_REMOVE(&ir->c->input_requests, ir, centry); + + ictx->request_count--; + TAILQ_REMOVE(&ictx->requests, ir, entry); + + free(ir->data); + free(ir); +} + /* Add a request. */ static int input_add_request(struct input_ctx *ictx, enum input_request_type type, int idx) @@ -3219,24 +3253,18 @@ input_add_request(struct input_ctx *ictx, enum input_request_type type, int idx) if (c == NULL) return (-1); - ir = xcalloc (1, sizeof *ir); + ir = input_make_request(ictx, type); ir->c = c; - ir->ictx = ictx; - ir->type = type; ir->idx = idx; - ir->t = time(NULL); - TAILQ_INSERT_TAIL(&ictx->requests[type], ir, entry); - TAILQ_INSERT_TAIL(&c->input_requests[type].requests, ir, centry); - if (++ictx->request_count == 1) - input_start_request_timer(ictx); - log_debug("%s: req %p: client %s, pane %%%u, type %d", __func__, ir, - c->name, wp->id, type); + TAILQ_INSERT_TAIL(&c->input_requests, ir, centry); switch (type) { case INPUT_REQUEST_PALETTE: xsnprintf(s, sizeof s, "\033]4;%d;?\033\\", idx); tty_puts(&c->tty, s); break; + case INPUT_REQUEST_QUEUE: + break; } return (0); @@ -3246,25 +3274,30 @@ input_add_request(struct input_ctx *ictx, enum input_request_type type, int idx) void input_request_reply(struct client *c, enum input_request_type type, void *data) { - struct input_request_list *irl = &c->input_requests[type]; - struct input_request *ir, *ir1; + struct input_request *ir, *ir1, *found = NULL; struct input_request_palette_data *pd = data; + int complete = 0; - TAILQ_FOREACH_SAFE(ir, &irl->requests, centry, ir1) { - log_debug("%s: req %p: client %s, pane %%%u, type %d", - __func__, ir, c->name, ir->ictx->wp->id, ir->type); - switch (type) { - case INPUT_REQUEST_PALETTE: - if (pd->idx != ir->idx) - continue; - input_osc_colour_reply(ir->ictx, 4, pd->idx, pd->c); + TAILQ_FOREACH_SAFE(ir, &c->input_requests, centry, ir1) { + if (ir->type == type && pd->idx == ir->idx) { + found = ir; break; } - TAILQ_REMOVE(&ir->ictx->requests[type], ir, entry); - TAILQ_REMOVE(&irl->requests, ir, centry); - ir->ictx->request_count--; - free(ir); - break; + input_free_request(ir); + } + if (found == NULL) + return; + + TAILQ_FOREACH_SAFE(ir, &found->ictx->requests, entry, ir1) { + if (complete && ir->type != INPUT_REQUEST_QUEUE) + break; + if (ir->type == INPUT_REQUEST_QUEUE) + input_send_reply(ir->ictx, ir->data); + else if (ir == found && ir->type == INPUT_REQUEST_PALETTE) { + input_osc_colour_reply(ir->ictx, 0, 4, pd->idx, pd->c); + complete = 1; + } + input_free_request(ir); } } @@ -3272,21 +3305,10 @@ input_request_reply(struct client *c, enum input_request_type type, void *data) void input_cancel_requests(struct client *c) { - struct input_request_list *irl; - struct input_request *ir, *ir1; - u_int i; + struct input_request *ir, *ir1; - for (i = 0; i < INPUT_REQUEST_TYPES; i++) { - irl = &c->input_requests[i]; - TAILQ_FOREACH_SAFE(ir, &irl->requests, entry, ir1) { - log_debug("%s: req %p: client %s, pane %%%u, type %d", - __func__, ir, c->name, ir->ictx->wp->id, ir->type); - TAILQ_REMOVE(&ir->ictx->requests[i], ir, entry); - TAILQ_REMOVE(&irl->requests, ir, centry); - ir->ictx->request_count--; - free(ir); - } - } + TAILQ_FOREACH_SAFE(ir, &c->input_requests, entry, ir1) + input_free_request(ir); } /* Report current theme. */ @@ -3295,10 +3317,10 @@ input_report_current_theme(struct input_ctx *ictx) { switch (window_pane_get_theme(ictx->wp)) { case THEME_DARK: - input_reply(ictx, "\033[?997;1n"); + input_reply(ictx, 0, "\033[?997;1n"); break; case THEME_LIGHT: - input_reply(ictx, "\033[?997;2n"); + input_reply(ictx, 0, "\033[?997;2n"); break; case THEME_UNKNOWN: break; diff --git a/server-client.c b/server-client.c index d2a5a80c..14ebe5fb 100644 --- a/server-client.c +++ b/server-client.c @@ -282,7 +282,6 @@ struct client * server_client_create(int fd) { struct client *c; - u_int i; setblocking(fd, 0); @@ -316,11 +315,7 @@ server_client_create(int fd) evtimer_set(&c->repeat_timer, server_client_repeat_timer, c); evtimer_set(&c->click_timer, server_client_click_timer, c); - for (i = 0; i < INPUT_REQUEST_TYPES; i++) { - c->input_requests[i].c = c; - c->input_requests[i].type = i; - TAILQ_INIT(&c->input_requests[i].requests); - } + TAILQ_INIT(&c->input_requests); TAILQ_INSERT_TAIL(&clients, c, entry); log_debug("new client %p", c); diff --git a/tmux.h b/tmux.h index 736d0f55..6aa9bc57 100644 --- a/tmux.h +++ b/tmux.h @@ -1096,9 +1096,9 @@ struct window_mode_entry { /* Type of request to client. */ enum input_request_type { - INPUT_REQUEST_PALETTE + INPUT_REQUEST_PALETTE, + INPUT_REQUEST_QUEUE }; -#define INPUT_REQUEST_TYPES (1) /* Palette request reply data. */ struct input_request_palette_data { @@ -1108,11 +1108,6 @@ struct input_request_palette_data { /* Request sent to client on behalf of pane. */ TAILQ_HEAD(input_requests, input_request); -struct input_request_list { - struct client *c; - enum input_request_type type; - struct input_requests requests; -}; /* Offsets into pane buffer. */ struct window_pane_offset { @@ -1942,7 +1937,7 @@ struct client { struct status_line status; enum client_theme theme; - struct input_request_list input_requests[INPUT_REQUEST_TYPES]; + struct input_requests input_requests; #define CLIENT_TERMINAL 0x1 #define CLIENT_LOGIN 0x2 From 768042d29d5151c6024f8be1b01ed388237a34c1 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 13 Nov 2025 07:18:53 +0000 Subject: [PATCH 26/27] Fix build afte merge, from Vladimir Lomov. --- input.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/input.c b/input.c index 91d3b320..9504c925 100644 --- a/input.c +++ b/input.c @@ -2017,10 +2017,11 @@ input_csi_dispatch_sm_graphics(__unused struct input_ctx *ictx) m = input_get(ictx, 1, 0, 0); o = input_get(ictx, 2, 0, 0); - if (n == 1 && (m == 1 || m == 2 || m == 4)) - input_reply(ictx, "\033[?%d;0;%uS", n, SIXEL_COLOUR_REGISTERS); - else - input_reply(ictx, "\033[?%d;3;%dS", n, o); + if (n == 1 && (m == 1 || m == 2 || m == 4)) { + input_reply(ictx, 1, "\033[?%d;0;%uS", n, + SIXEL_COLOUR_REGISTERS); + } else + input_reply(ictx, 1, "\033[?%d;3;%dS", n, o); #endif } From 113aaf37fc5c3b278a50b94c568782f32ae03ef4 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 13 Nov 2025 13:08:44 +0000 Subject: [PATCH 27/27] When copying a line from one screen to another, stop only when both source and target are beyond the allocated line length. Fixes problems with trailing spaces on pane-border-format. GitHub issue 4688 from Dmitry Ryabkov. --- screen-write.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/screen-write.c b/screen-write.c index 8e04681a..43afb50d 100644 --- a/screen-write.c +++ b/screen-write.c @@ -583,7 +583,8 @@ screen_write_fast_copy(struct screen_write_ctx *ctx, struct screen *src, if (wp != NULL) screen_write_initctx(ctx, &ttyctx, 0); for (xx = px; xx < px + nx; xx++) { - if (xx >= grid_get_line(gd, yy)->cellsize) + if (xx >= grid_get_line(gd, yy)->cellsize && + s->cx >= grid_get_line(ctx->s->grid, s->cy)->cellsize) break; grid_get_cell(gd, xx, yy, &gc); if (xx + gc.data.width > px + nx)