From b2de803871e579565a6f79296f386115142593ed Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 20:04:00 +0000 Subject: [PATCH 01/30] Flush output before ending sync. GitHub issue 5495 from xiangzhedev at gmail dot com. --- input.c | 4 ++-- screen-write.c | 15 ++++++++++++++- tmux.h | 3 ++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/input.c b/input.c index ecf07dd61..5f9c5f4ab 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.269 2026/07/20 11:16:33 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.270 2026/08/17 20:04:00 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1974,7 +1974,7 @@ input_csi_dispatch_rm_private(struct input_ctx *ictx) screen_write_mode_clear(sctx, MODE_BRACKETPASTE); break; case 2026: - screen_write_stop_sync(ictx->wp); + screen_write_end_sync(sctx); break; case 2031: screen_write_mode_clear(sctx, MODE_THEME_UPDATES); diff --git a/screen-write.c b/screen-write.c index 71dc99200..990ef4e0f 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.286 2026/08/03 12:58:53 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.287 2026/08/17 20:04:00 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1059,6 +1059,19 @@ screen_write_stop_sync(struct window_pane *wp) log_debug("%s: %%%u stopped sync mode", __func__, wp->id); } +/* Flush pending output before clearing sync mode. */ +void +screen_write_end_sync(struct screen_write_ctx *ctx) +{ + struct window_pane *wp = ctx->wp; + + if (wp == NULL) + return; + if (wp->base.mode & MODE_SYNC) + screen_write_collect_flush(ctx, 0, __func__); + screen_write_stop_sync(wp); +} + /* Cursor up by ny. */ void screen_write_cursorup(struct screen_write_ctx *ctx, u_int ny) diff --git a/tmux.h b/tmux.h index 6cd104b50..11d9e3d4d 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1424 2026/08/17 14:47:41 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1425 2026/08/17 20:04:00 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3554,6 +3554,7 @@ void screen_write_mode_set(struct screen_write_ctx *, int); void screen_write_mode_clear(struct screen_write_ctx *, int); void screen_write_start_sync(struct window_pane *); void screen_write_stop_sync(struct window_pane *); +void screen_write_end_sync(struct screen_write_ctx *); void screen_write_clear_dirty(struct window_pane *); void screen_write_cursorup(struct screen_write_ctx *, u_int); void screen_write_cursordown(struct screen_write_ctx *, u_int); From db194ced5b41b35107e58933c77d02a22d2ccb9a Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 17 Aug 2026 20:14:31 +0000 Subject: [PATCH 02/30] Log unknown message types. --- client.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client.c b/client.c index a59216823..bc0d36da5 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: client.c,v 1.167 2026/08/17 07:56:56 nicm Exp $ */ +/* $OpenBSD: client.c,v 1.168 2026/08/17 20:14:31 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -705,6 +705,9 @@ client_dispatch_wait(struct imsg *imsg) fprintf(stderr, "server version is too old for client\n"); proc_exit(client_proc); break; + default: + log_debug("unknown message type %u", imsg->hdr.type); + break; } } @@ -791,5 +794,8 @@ client_dispatch_attached(struct imsg *imsg) system(data); proc_send(client_peer, MSG_UNLOCK, -1, NULL, 0); break; + default: + log_debug("unknown message type %u", imsg->hdr.type); + break; } } From 19a085a6727650ae660196a38e1a412f1b386615 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 18 Aug 2026 08:23:16 +0100 Subject: [PATCH 03/30] Fix socket names for macOS. --- regress/sync-output-atomic.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/regress/sync-output-atomic.sh b/regress/sync-output-atomic.sh index d0979ebe1..842813d16 100644 --- a/regress/sync-output-atomic.sh +++ b/regress/sync-output-atomic.sh @@ -14,8 +14,8 @@ DIR=$(mktemp -d) || exit 1 TMUX_TMPDIR=$DIR export TMUX_TMPDIR -INNER="$TEST_TMUX -Lsync-output-inner-$$ -f/dev/null" -OUTER="$TEST_TMUX -Lsync-output-outer-$$ -f/dev/null" +INNER="$TEST_TMUX -Li$$ -f/dev/null" +OUTER="$TEST_TMUX -Lo$$ -f/dev/null" CLIENT_BYTES=$DIR/client-bytes CONTROL=$DIR/control EMITTER=$DIR/emitter.pl @@ -112,7 +112,7 @@ $INNER set-option -g window-size manual || exit 1 $INNER set-option -as terminal-features '*:sync' || exit 1 $OUTER new-session -d -s outer -x 80 -y 24 \ - "$TEST_TMUX -Lsync-output-inner-$$ -f/dev/null attach-session -t inner" || + "$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" || exit 1 $OUTER set-option -g status off || exit 1 $OUTER set-option -g window-size manual || exit 1 From da641e4e8d2b0547487e8c6f28aca0e5981d5a92 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 18 Aug 2026 08:45:11 +0100 Subject: [PATCH 04/30] Add test for GitHub issue 5498 from Alex Rattray. --- regress/respawn-pane-control-lag.sh | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 regress/respawn-pane-control-lag.sh diff --git a/regress/respawn-pane-control-lag.sh b/regress/respawn-pane-control-lag.sh new file mode 100644 index 000000000..b71ce1946 --- /dev/null +++ b/regress/respawn-pane-control-lag.sh @@ -0,0 +1,87 @@ +#!/bin/sh + +# respawn-pane frees the pane's buffer and creates an empty one. The pane +# offsets and each control client's offsets used to keep pointing into the old +# buffer, so if an attached control client had not caught up on the pane's +# output, the next read from the new process ran off the end of the new buffer +# and the server crashed in input_parse. +# +# Two ways for a client to be behind are tested: a control client whose output +# goes down a fifo that is never read (a second, healthy control client keeps +# the pane being read), and a healthy control client viewing a session that +# the pane's window has been moved out of. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest" +$TMUX kill-server 2>/dev/null + +DIR=$(mktemp -d) +FIFO=$DIR/fifo +SERVER= + +mkfifo "$FIFO" || exit 1 + +cleanup() { + [ -n "$SERVER" ] && kill -9 "$SERVER" 2>/dev/null + $TMUX kill-server 2>/dev/null + exec 8<&- 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +alive() { + if ! kill -0 "$SERVER" 2>/dev/null; then + SERVER= + echo "server died after $1" + exit 1 + fi + $TMUX has -t rt || exit 1 +} + +wait_clients() { + n=0 + while [ $n -lt 50 ]; do + [ "$($TMUX lsc 2>/dev/null | wc -l)" -ge $1 ] && return + sleep 0.1 + n=$((n + 1)) + done + echo "control clients did not attach"; exit 1 +} + +# A detached session with a pane that writes a line every 10 milliseconds. +$TMUX -f/dev/null new -d -x 80 -y 24 -s rt \ + 'while :; do echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; sleep 0.01; done' || + exit 1 +SERVER=$($TMUX display -pt rt '#{pid}') + +# One control client with output down the unread fifo and one reading +# normally; stdin of both held open by sleep so they stay attached. +( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 & +exec 8<"$FIFO" +( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >/dev/null 2>&1 & +wait_clients 2 + +# Let the first client fall behind, then respawn the pane. The new process +# writes at once and the server must survive reading it. +sleep 3 +$TMUX respawn-pane -k -t rt:0 'echo respawned; sleep 60' || exit 1 +sleep 1 +alive "respawn-pane with a lagging control client" + +# Now a pane whose window is moved out of the session both clients view: their +# offsets for it stop advancing, so they fall behind until it is respawned. +$TMUX neww -d -t rt \ + 'while :; do echo yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy; sleep 0.01; done' || + exit 1 +sleep 1 +$TMUX new -d -s other || exit 1 +$TMUX movew -d -s rt:1 -t other: || exit 1 +sleep 2 +$TMUX respawn-pane -k -t other:1 'echo respawned; sleep 60' || exit 1 +sleep 1 +alive "respawn-pane on a window moved out of the clients' session" + +exit 0 From 6238793ad71f0bc0a6062c6575d866c180b18d08 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 18 Aug 2026 07:24:49 +0000 Subject: [PATCH 05/30] Restore umask if bind fails, from nachalsa at naver dot com. --- server.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server.c b/server.c index ffd5b4ddf..bc2fcdd92 100644 --- a/server.c +++ b/server.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server.c,v 1.215 2026/08/03 10:07:57 nicm Exp $ */ +/* $OpenBSD: server.c,v 1.216 2026/08/18 07:24:49 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -131,6 +131,7 @@ server_create_socket(uint64_t flags, char **cause) mask = umask(S_IXUSR|S_IRWXG|S_IRWXO); if (bind(fd, (struct sockaddr *)&sa, sizeof sa) == -1) { saved_errno = errno; + umask(mask); close(fd); errno = saved_errno; goto fail; From 72c5037f06625f8dff165ea42c660ee696e9015e Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 18 Aug 2026 07:32:09 +0000 Subject: [PATCH 06/30] Correctly clear clients referring to a session with destroy-unattached, GitHub issue 5497 from Jeong, Heon. --- server-fn.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server-fn.c b/server-fn.c index 149d048ad..503516afe 100644 --- a/server-fn.c +++ b/server-fn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-fn.c,v 1.149 2026/07/27 14:25:46 nicm Exp $ */ +/* $OpenBSD: server-fn.c,v 1.150 2026/08/18 07:32:09 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -548,6 +548,7 @@ server_check_unattached(void) continue; break; } + server_destroy_session(s); session_destroy(s, 1, __func__); } } From 4714f478fed87ae2c510ecd035aa89c5a90dedf8 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 18 Aug 2026 07:43:44 +0000 Subject: [PATCH 07/30] When a pane is respawned, reset the control mode offsets. GitHub issue 5498 from Alex Rattray. --- control.c | 21 ++++++++++++++++++++- spawn.c | 18 ++++++++++++++++-- tmux.h | 3 ++- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/control.c b/control.c index 7f76ff9e3..c3bb82b01 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.65 2026/08/04 11:18:22 nicm Exp $ */ +/* $OpenBSD: control.c,v 1.66 2026/08/18 07:43:44 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -388,6 +388,25 @@ control_pause_pane(struct client *c, struct window_pane *wp) } } +/* + * Reset a pane after its buffer has been replaced: drop any output still + * queued from the old buffer and start again from the pane's own offset. + */ +void +control_reset_pane(struct client *c, struct window_pane *wp) +{ + struct control_pane *cp; + + if (c->control_state == NULL) + return; + cp = control_get_pane(c, wp); + if (cp == NULL) + return; + control_discard_pane(c, cp); + memcpy(&cp->offset, &wp->offset, sizeof cp->offset); + memcpy(&cp->queued, &wp->offset, sizeof cp->queued); +} + /* Write an already-formatted line, queueing it behind %output if needed. */ static void control_write_line(struct client *c, char *line) diff --git a/spawn.c b/spawn.c index 6379e01af..9e162217f 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spawn.c,v 1.50 2026/07/23 09:38:27 nicm Exp $ */ +/* $OpenBSD: spawn.c,v 1.51 2026/08/18 07:43:44 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -245,7 +245,7 @@ struct window_pane * spawn_pane(struct spawn_context *sc, char **cause) { struct cmdq_item *item = sc->item; - struct client *c; + struct client *c, *loop; struct session *s = sc->s; struct session *ts; struct window *w = sc->wl->window; @@ -333,6 +333,20 @@ spawn_pane(struct spawn_context *sc, char **cause) input_free(sc->wp0->ictx); sc->wp0->ictx = NULL; } + + /* + * The old buffer is gone and the new one starts empty, so + * offsets into the old buffer no longer mean anything. Reset + * them, and drop output control clients still had queued. + */ + sc->wp0->offset.used = 0; + sc->wp0->base_offset = 0; + sc->wp0->pipe_offset.used = 0; + TAILQ_FOREACH(loop, &clients, entry) { + if (loop->flags & CLIENT_CONTROL) + control_reset_pane(loop, sc->wp0); + } + new_wp = sc->wp0; new_wp->flags &= ~(PANE_STATUSREADY|PANE_STATUSDRAWN); } else { diff --git a/tmux.h b/tmux.h index 11d9e3d4d..ea03cb463 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1425 2026/08/17 20:04:00 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1426 2026/08/18 07:43:44 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3986,6 +3986,7 @@ void control_set_pane_on(struct client *, struct window_pane *); void control_set_pane_off(struct client *, struct window_pane *); void control_continue_pane(struct client *, struct window_pane *); void control_pause_pane(struct client *, struct window_pane *); +void control_reset_pane(struct client *, struct window_pane *); void control_set_window_size(struct client *, u_int, u_int, u_int); int control_get_window_size(struct client *, u_int, u_int *, u_int *); void control_clear_window_size(struct client *, u_int); From b170c192742761a83ec3c8e1490692414d298860 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 18 Aug 2026 08:05:05 +0000 Subject: [PATCH 08/30] Change correct depth counter for source-file when used from hooks, GitHub issue 5437. --- cmd-source-file.c | 60 ++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/cmd-source-file.c b/cmd-source-file.c index e5dfba543..9c48c6f17 100644 --- a/cmd-source-file.c +++ b/cmd-source-file.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-source-file.c,v 1.62 2025/11/18 08:42:09 nicm Exp $ */ +/* $OpenBSD: cmd-source-file.c,v 1.63 2026/08/18 08:05:05 nicm Exp $ */ /* * Copyright (c) 2008 Tiago Cunha @@ -51,6 +51,7 @@ const struct cmd_entry cmd_source_file_entry = { struct cmd_source_file_data { struct cmdq_item *item; + struct client *client; int flags; struct cmdq_item *after; @@ -61,10 +62,24 @@ struct cmd_source_file_data { u_int nfiles; }; -static enum cmd_retval -cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data) +static void +cmd_source_file_free_data(struct cmd_source_file_data *cdata) { - struct client *c = cmdq_get_client(item); + u_int i; + + for (i = 0; i < cdata->nfiles; i++) + free(cdata->files[i]); + free(cdata->files); + if (cdata->client != NULL) + server_client_unref(cdata->client); + free(cdata); +} + +static enum cmd_retval +cmd_source_file_complete_cb(struct cmdq_item *item, void *data) +{ + struct cmd_source_file_data *cdata = data; + struct client *c = cdata->client; if (c == NULL) { cmd_source_file_depth--; @@ -75,36 +90,36 @@ cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data) } cfg_print_causes(item); + cmd_source_file_free_data(cdata); return (CMD_RETURN_NORMAL); } static void -cmd_source_file_complete(struct client *c, struct cmd_source_file_data *cdata) +cmd_source_file_complete(struct cmd_source_file_data *cdata) { + struct client *c = cdata->client; struct cmdq_item *new_item; - u_int i; - if (cfg_finished) { - if (cdata->retval == CMD_RETURN_ERROR && - c != NULL && - c->session == NULL) - c->retval = 1; - new_item = cmdq_get_callback(cmd_source_file_complete_cb, NULL); - cmdq_insert_after(cdata->after, new_item); + if (!cfg_finished) { + cmd_source_file_free_data(cdata); + return; } - for (i = 0; i < cdata->nfiles; i++) - free(cdata->files[i]); - free(cdata->files); - free(cdata); + if (cdata->retval == CMD_RETURN_ERROR && + c != NULL && + c->session == NULL) + c->retval = 1; + new_item = cmdq_get_callback(cmd_source_file_complete_cb, cdata); + cmdq_insert_after(cdata->after, new_item); } static void -cmd_source_file_done(struct client *c, const char *path, int error, - int closed, struct evbuffer *buffer, void *data) +cmd_source_file_done(__unused struct client *oc, const char *path, + int error, int closed, struct evbuffer *buffer, void *data) { struct cmd_source_file_data *cdata = data; struct cmdq_item *item = cdata->item; + struct client *c = cdata->client; void *bdata = EVBUFFER_DATA(buffer); size_t bsize = EVBUFFER_LENGTH(buffer); u_int n; @@ -128,7 +143,7 @@ cmd_source_file_done(struct client *c, const char *path, int error, if (n < cdata->nfiles) file_read(c, cdata->files[n], cmd_source_file_done, cdata); else { - cmd_source_file_complete(c, cdata); + cmd_source_file_complete(cdata); cmdq_continue(item); } } @@ -188,6 +203,9 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item) cdata = xcalloc(1, sizeof *cdata); cdata->item = item; + cdata->client = c; + if (c != NULL) + c->references++; if (args_has(args, 'q')) cdata->flags |= CMD_PARSE_QUIET; @@ -250,7 +268,7 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item) file_read(c, cdata->files[0], cmd_source_file_done, cdata); retval = CMD_RETURN_WAIT; } else - cmd_source_file_complete(c, cdata); + cmd_source_file_complete(cdata); free(cwd); return (retval); From c5017e6da84e1e5b5db9f41f47def3fe7ba56308 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 18 Aug 2026 11:49:25 +0100 Subject: [PATCH 09/30] Bump time limit. --- .github/workflows/regress.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 3cc036425..a84478357 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -17,7 +17,7 @@ jobs: name: ${{ matrix.name }} runs-on: ${{ matrix.runner }} if: github.repository == 'tmux/tmux' - timeout-minutes: 45 + timeout-minutes: 300 strategy: fail-fast: false From e94fcd96f9384808a404995b935d722fe3ed4358 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 18 Aug 2026 09:01:20 +0000 Subject: [PATCH 10/30] Use DECRQM to detect mode 2026, from Michael Grant. --- tmux.h | 5 +++-- tty-keys.c | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++- tty.c | 4 +++- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/tmux.h b/tmux.h index ea03cb463..68d2044b0 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1426 2026/08/18 07:43:44 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1427 2026/08/18 09:01:20 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1774,8 +1774,9 @@ struct tty { #define TTY_WAITFG 0x2000 #define TTY_WAITBG 0x4000 #define TTY_BRACKETPASTE 0x8000 +#define TTY_HAVESYNC 0x10000 #define TTY_ALL_REQUEST_FLAGS \ - (TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA) + (TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA|TTY_HAVESYNC) int flags; struct tty_term *term; diff --git a/tty-keys.c b/tty-keys.c index 6c2289495..8462e0d9b 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-keys.c,v 1.213 2026/08/17 14:47:41 nicm Exp $ */ +/* $OpenBSD: tty-keys.c,v 1.214 2026/08/18 09:01:20 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -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_sync(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. */ @@ -771,6 +772,17 @@ tty_keys_next(struct tty *tty) goto partial_key; } + /* Is this a synchronized update mode response? */ + switch (tty_keys_sync(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 primary device attributes response? */ switch (tty_keys_device_attributes(tty, buf, len, &size)) { case 0: /* yes */ @@ -1521,6 +1533,54 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, return (0); } +/* + * Handle a synchronized update mode response. Returns 0 for success, -1 for + * failure, 1 for partial. + */ +static int +tty_keys_sync(struct tty *tty, const char *buf, size_t len, size_t *size) +{ + struct client *c = tty->client; + static const char prefix[] = "\033[?2026;"; + size_t i; + int status; + + *size = 0; + if (tty->flags & TTY_HAVESYNC) + return (-1); + + /* The response is always \033[?2026;Ps$y. */ + for (i = 0; i < (sizeof prefix) - 1; i++) { + if (i == len) + return (1); + if (buf[i] != prefix[i]) + return (-1); + } + if (i == len) + return (1); + if (buf[i] < '0' || buf[i] > '4') + return (-1); + status = buf[i++] - '0'; + if (i == len) + return (1); + if (buf[i++] != '$') + return (-1); + if (i == len) + return (1); + if (buf[i++] != 'y') + return (-1); + *size = i; + + if (status == 1 || status == 2 || status == 3) { + tty_parse_client_features(c, "sync", ","); + tty_update_features(tty); + } + log_debug("%s: received DECRPM %.*s", c->name, (int)*size, buf); + tty->flags |= TTY_HAVESYNC; + + return (0); +} + /* * Handle secondary device attributes input. Returns 0 for success, -1 for * failure, 1 for partial. diff --git a/tty.c b/tty.c index 8922efa07..bf2671b33 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.478 2026/08/17 14:47:41 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.479 2026/08/18 09:01:20 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -398,6 +398,8 @@ tty_send_requests(struct tty *tty) tty_puts(tty, "\033[>c"); if (~tty->flags & TTY_HAVEXDA) tty_puts(tty, "\033[>q"); + if (~tty->flags & TTY_HAVESYNC) + tty_puts(tty, "\033[?2026$p"); tty_puts(tty, "\033]10;?\033\\\033]11;?\033\\"); tty->flags |= (TTY_WAITBG|TTY_WAITFG); } else From 8499d8007e90fcc4e7c0f3b3e35490a025b09662 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 19 Aug 2026 11:56:27 +0100 Subject: [PATCH 11/30] new-pane -K test bits. --- regress/modal-pane.sh | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/regress/modal-pane.sh b/regress/modal-pane.sh index cd891281d..d02a75683 100644 --- a/regress/modal-pane.sh +++ b/regress/modal-pane.sh @@ -105,6 +105,24 @@ drag() sleep 1 } +meta_drag() +{ + scol="$1" + srow="$2" + ecol="$3" + erow="$4" + + seq=$(printf '\033[<8;%s;%sM' "$scol" "$srow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<40;%s;%sM' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<8;%s;%sm' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} + cleanup check_ok new-session -d -s modal -x 80 -y 24 'cat' @@ -304,5 +322,35 @@ sleep 1 must_equal "$(fmt modal:0 '#{window_modal_pane}')" '' must_equal "$(fmt modal:0 '#{pane_id}')" "$p0" +ignored=$($TMUX new-pane -KdPF '#{pane_id}' -t "$p0" 'cat') || + fail "new-pane -K without -O failed" +check_ok kill-pane -t "$ignored" + +$TMUX set -g @modal-prefix no +$TMUX set -g @modal-root no +$TMUX bind -n z set -g @modal-root yes + +modal=$($TMUX new-pane -OKPF '#{pane_id}' -t "$p0" \ + -x 20 -y 5 -X 20 -Y 10 'cat') || + fail "new-pane -OK failed" +sleep 1 +$TMUX2 send-keys -t "$OUTER" C-b x z Enter +sleep 1 +must_equal "$($TMUX show -gv @modal-prefix)" no +must_equal "$($TMUX show -gv @modal-root)" no +case "$($TMUX capture-pane -pt "$modal")" in +*xz*) ;; +*) fail "keys did not reach key-capturing modal pane" ;; +esac +left=$(fmt "$modal" '#{pane_left}') +top=$(fmt "$modal" '#{pane_top}') +meta_drag $((left + 2)) $((top + 2)) $((left + 7)) $((top + 4)) +new_left=$(fmt "$modal" '#{pane_left}') +new_top=$(fmt "$modal" '#{pane_top}') +[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] || + fail "key-capturing modal pane did not move" +check_ok kill-pane -t "$modal" +sleep 1 + cleanup exit 0 From b28b1b7b9a628c6c1dfa435a51f28e990d1cd8e1 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 19 Aug 2026 10:56:10 +0000 Subject: [PATCH 12/30] Add a -K flag to new-pane to make a modal pane capture all keys like popups used to. --- cmd-split-window.c | 8 ++++--- server-client.c | 56 ++++++++++++++++++++++++++++++++++++---------- tmux.1 | 9 +++++--- tmux.h | 3 ++- 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/cmd-split-window.c b/cmd-split-window.c index e919b6f03..4980d6a9b 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.148 2026/08/07 08:10:53 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.149 2026/08/19 10:56:10 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -41,8 +41,8 @@ const struct cmd_entry cmd_new_pane_entry = { .name = "new-pane", .alias = "newp", - .args = { "bB:Cc:de:EfF:hIkl:LMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, - .usage = "[-bCdefhIklMOPvWZ] [-B border-lines] " + .args = { "bB:Cc:de:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, + .usage = "[-bCdefhIkKLMOPvWZ] [-B border-lines] " "[-c start-directory] [-e environment] " "[-F format] [-l size] [-m message] [-p percentage] " "[-s style] [-S active-border-style] " @@ -203,6 +203,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) */ goto fail; } + if (args_has(args, 'K') && args_has(args, 'O')) + new_wp->flags |= PANE_CAPTUREALLKEYS; if (args_has(args, 'C') && args_has(args, 'O')) new_wp->flags |= PANE_CLOSEONCLICK; diff --git a/server-client.c b/server-client.c index 4fe4917ad..a631a90a9 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.503 2026/08/17 07:56:56 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.504 2026/08/19 10:56:10 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -50,6 +50,7 @@ static void server_client_set_path(struct client *); static void server_client_set_progress_bar(struct client *); static void server_client_reset_state(struct client *); static void server_client_update_latest(struct client *); +static int server_client_handle_dead_key(struct window_pane *, key_code); static void server_client_dispatch(struct imsg *, void *); static int server_client_dispatch_command(struct client *, struct imsg *); static int server_client_dispatch_identify(struct client *, struct imsg *); @@ -1393,6 +1394,21 @@ server_client_repeat_time(struct client *c, struct key_binding *bd) return (repeat); } +/* Handle a key press on a dead pane waiting for a key. */ +static int +server_client_handle_dead_key(struct window_pane *wp, key_code key) +{ + if (wp == NULL || + (~wp->flags & PANE_EXITED) || + KEYC_IS_MOUSE(key) || + KEYC_IS_PASTE(key) || + options_get_number(wp->options, "remain-on-exit") != 3) + return (0); + options_set_number(wp->options, "remain-on-exit", 0); + server_destroy_pane(wp, 0); + return (1); +} + /* * Handle data key input from client. This owns and can modify the key event it * is given and is responsible for freeing it. @@ -1478,6 +1494,14 @@ server_client_key_callback(struct cmdq_item *item, void *data) server_client_is_assume_paste(c)) goto paste_key; + /* Forward keys directly if this pane is capturing all keys. */ + if (wp != NULL && + (wp->flags & PANE_CAPTUREALLKEYS) && + (~wp->flags & PANE_EXITED) && + !KEYC_IS_MOUSE(key) && + TAILQ_EMPTY(&wp->modes)) + goto forward_key; + /* * Work out the current key table. If the pane is in a mode, use * the mode table instead of the default key table. @@ -1642,15 +1666,8 @@ try_again: } forward_key: - if (wp != NULL && - (wp->flags & PANE_EXITED) && - !KEYC_IS_MOUSE(key) && - !KEYC_IS_PASTE(key) && - options_get_number(wp->options, "remain-on-exit") == 3) { - options_set_number(wp->options, "remain-on-exit", 0); - server_destroy_pane(wp, 0); + if (server_client_handle_dead_key(wp, key)) goto out; - } if (c->flags & CLIENT_READONLY) goto out; if (wp != NULL) @@ -1738,9 +1755,9 @@ server_client_handle_key0(struct client *c, struct key_event *event, } /* - * Key presses in overlay mode and the command prompt are a special - * case. The queue might be blocked so they need to be processed - * immediately rather than queued. + * Key presses in overlay mode, for panes capturing all keys and in the + * command prompt are a special case. The queue might be blocked so they + * need to be processed immediately rather than queued. */ if (~c->flags & CLIENT_READONLY) { if (c->message_string != NULL) { @@ -1760,6 +1777,21 @@ server_client_handle_key0(struct client *c, struct key_event *event, } server_client_clear_overlay(c); + + wp = s->curw->window->active; + if (wp != NULL && + (wp->flags & PANE_CAPTUREALLKEYS) && + TAILQ_EMPTY(&wp->modes) && + !KEYC_IS_MOUSE(event->key)) { + if (server_client_handle_dead_key(wp, event->key)) + return (0); + if (~wp->flags & PANE_EXITED) { + window_pane_key(wp, c, s, s->curw, event->key, + &event->m); + return (0); + } + } + if (server_client_handle_menu_key(c, event)) return (0); if (c->prompt != NULL) { diff --git a/tmux.1 b/tmux.1 index de987b22d..1b692f05a 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1156 2026/08/17 14:47:41 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1157 2026/08/19 10:56:10 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: August 17 2026 $ +.Dd $Mdocdate: August 19 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -3642,7 +3642,7 @@ but a different format may be specified with .Fl F . .Tg newp .It Xo Ic new\-pane -.Op Fl bCdefhIkLMOPvWZ +.Op Fl bCdefhIkKLMOPvWZ .Op Fl B Ar border\-lines .Op Fl c Ar start\-directory .Op Fl e Ar environment @@ -3699,6 +3699,9 @@ A modal pane is always the active pane and prevents interaction with any other panes while it is active. A window can only have one modal pane and it must be a floating pane. With +.Fl K , +all keys, including the prefix key, are passed directly to the modal pane. +With .Fl C , the modal pane is closed when the mouse is clicked outside it. .Pp diff --git a/tmux.h b/tmux.h index 68d2044b0..b295e2313 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1427 2026/08/18 09:01:20 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1428 2026/08/19 10:56:10 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1301,6 +1301,7 @@ struct window_pane { #define PANE_CMDRUNNING 0x20000 #define PANE_ACTIVITY 0x40000 #define PANE_CLOSEONCLICK 0x80000 +#define PANE_CAPTUREALLKEYS 0x100000 bitstr_t *sync_dirty; u_int sync_dirty_size; From 50d0348b648258f11d75ec751581220884ff32a3 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 19 Aug 2026 19:55:04 +0000 Subject: [PATCH 13/30] Stop at the right end line when walking wrapped lines, GitHub issue 5479. --- window-copy.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/window-copy.c b/window-copy.c index 716a0f037..8c492c42e 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.425 2026/08/05 12:23:25 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.426 2026/08/19 19:55:04 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -4093,7 +4093,7 @@ window_copy_search_lr_regex(struct grid *gd, u_int *ppx, u_int *psx, u_int py, endline = gd->hsize + gd->sy - 1; pywrap = py; while (buf != NULL && - pywrap <= endline && + pywrap < endline && len < WINDOW_COPY_SEARCH_MAX_LINE) { gl = grid_get_line(gd, pywrap); if (~gl->flags & GRID_LINE_WRAPPED) @@ -4152,7 +4152,7 @@ window_copy_search_rl_regex(struct grid *gd, u_int *ppx, u_int *psx, u_int py, endline = gd->hsize + gd->sy - 1; pywrap = py; while (buf != NULL && - pywrap <= endline && + pywrap < endline && len < WINDOW_COPY_SEARCH_MAX_LINE) { gl = grid_get_line(gd, pywrap); if (~gl->flags & GRID_LINE_WRAPPED) @@ -4353,6 +4353,7 @@ window_copy_cstrtocellpos(struct grid *gd, u_int ncells, u_int *ppx, u_int *ppy, break; } } + ncells = cell; /* Locate starting cell. */ cell = 0; From ac23f21a2c222357fbcca1e1b22f38c382060f55 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 20 Aug 2026 09:18:58 +0100 Subject: [PATCH 14/30] More regress tests for zoom. --- regress/pane-ops.sh | 166 +++++++++++++++++++++++++++++++++++++++++- regress/window-ops.sh | 21 +++++- 2 files changed, 182 insertions(+), 5 deletions(-) diff --git a/regress/pane-ops.sh b/regress/pane-ops.sh index 41a6a2a45..c69362cee 100644 --- a/regress/pane-ops.sh +++ b/regress/pane-ops.sh @@ -19,7 +19,10 @@ # - respawn-pane/respawn-window refusing a live pane without -k, working on a # dead pane (remain-on-exit) and killing with -k; # - resize-pane -x/-y in cells and percent, -L/-R/-U/-D adjustments and -Z -# zoom/unzoom (including implicit unzoom on split). +# zoom/unzoom, layout round trips, zoom target selection, pane removal, +# window resizing and implicit unzoom on layout changes; +# - swap-pane preserving one or two zoomed windows, including cross-window +# swaps of hidden panes and zoom targets. # # window-ops.sh covers window-level commands and buffers.sh paste buffers. @@ -144,7 +147,8 @@ check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" # break-pane and join-pane. # break-pane moves a pane to a new window; -P -F prints where it went and -n -# names the new window. +# names the new window. Moving a pane out of a zoomed window unzooms it first. +check_ok resize-pane -Z -t "$p0" out=$($TMUX break-pane -d -P -F '#{window_index}:#{pane_id}' -n broken \ -s "$p1" -t P:) if [ "$out" != "1:$p1" ]; then @@ -153,10 +157,14 @@ if [ "$out" != "1:$p1" ]; then fi check_fmt 'P:1' '#{window_name}:#{window_panes}' 'broken:1' check_fmt 'P:0' '#{window_panes}' '3' +check_fmt 'P:0' '#{window_zoomed_flag}' '0' -# join-pane -v moves it back (the source window, left empty, is destroyed). +# join-pane -v moves it back (the source window, left empty, is destroyed) and +# also unzooms the destination before changing its layout. +check_ok resize-pane -Z -t "$p0" check_ok join-pane -d -v -s P:broken.0 -t "$p2" check_fmt 'P:0' '#{window_panes}' '4' +check_fmt 'P:0' '#{window_zoomed_flag}' '0' if $TMUX has-session -t P:broken 2>/dev/null; then echo "Window 'broken' still exists after join-pane." exit 1 @@ -299,6 +307,122 @@ check_fmt 'P:0' '#{window_panes}' '5' p6=$($TMUX display-message -p -t P:0.2 '#{pane_id}') check_ok kill-pane -t "$p6" +# Zoom and unzoom preserve the exact tiled layout. Selecting another pane +# without -Z unzooms, while -Z transfers zoom to the selected pane. +layout=$($TMUX display-message -p -t P:0 '#{window_layout}') +check_ok select-pane -t "$p0" +check_ok resize-pane -Z -t "$p0" +check_ok select-pane -t "$p2" +check_fmt "$p2" '#{window_zoomed_flag}:#{pane_active}' '0:1' +check_fmt P:0 '#{window_layout}' "$layout" + +check_ok select-pane -t "$p0" +check_ok resize-pane -Z -t "$p0" +check_ok select-pane -Z -t "$p2" +check_fmt "$p2" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '1:1:1' +check_ok resize-pane -Z -t "$p2" +check_fmt P:0 '#{window_layout}' "$layout" + +# Directional selection temporarily restores the full layout to find its +# neighbour, then follows the same unzoom or -Z transfer rules. +check_ok select-pane -t "$p0" +check_ok resize-pane -Z -t "$p0" +check_ok select-pane -t "$p0" +check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '1:1:1' +check_ok select-pane -D -t "$p0" +check_fmt "$p2" '#{window_zoomed_flag}:#{pane_active}' '0:1' + +check_ok select-pane -t "$p0" +check_ok resize-pane -Z -t "$p0" +check_ok select-pane -D -Z -t "$p0" +check_fmt "$p2" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '1:1:1' +check_ok resize-pane -Z -t "$p2" +check_fmt P:0 '#{window_layout}' "$layout" + +# The last-pane path has separate zoom handling, both with and without -Z. +check_ok select-pane -t "$p0" +check_ok select-pane -t "$p2" +check_ok resize-pane -Z -t "$p2" +check_ok select-pane -l -t P:0 +check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '0:0:1' + +check_ok select-pane -t "$p0" +check_ok select-pane -t "$p2" +check_ok resize-pane -Z -t "$p2" +check_ok select-pane -l -Z -t P:0 +check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '1:1:1' +check_ok resize-pane -Z -t "$p0" +check_fmt P:0 '#{window_layout}' "$layout" + +# Killing either a hidden ordinary pane or the zoom target unzooms. +check_ok new-window -d -t P:12 -n zoom-kill 'cat' +zk0=$($TMUX display-message -p -t P:12.0 '#{pane_id}') +zk1=$($TMUX split-window -d -P -F '#{pane_id}' -t P:12.0 'cat') +zk2=$($TMUX split-window -d -P -F '#{pane_id}' -t P:12.0 'cat') +check_ok resize-pane -Z -t "$zk0" +check_ok kill-pane -t "$zk1" +check_fmt "$zk0" '#{window_panes}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \ + '2:0:0' +check_ok resize-pane -Z -t "$zk0" +check_ok kill-pane -t "$zk0" +check_fmt "$zk2" '#{window_panes}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \ + '1:0:0' +check_ok kill-window -t P:12 + +# The same cases through natural process exit exercise server_destroy_pane. +check_ok new-window -d -t P:13 -n zoom-exit 'cat' +ze0=$($TMUX display-message -p -t P:13.0 '#{pane_id}') +ze1=$($TMUX split-window -d -P -F '#{pane_id}' -t P:13.0 'cat') +ze2=$($TMUX split-window -d -P -F '#{pane_id}' -t P:13.0 'cat') +check_ok resize-pane -Z -t "$ze0" +check_ok send-keys -t "$ze1" C-d +i=0 +while [ "$($TMUX display-message -p -t P:13 '#{window_panes}')" != 2 ]; do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "Hidden pane did not exit."; exit 1; } + sleep 0.1 +done +check_fmt "$ze0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0' +check_ok resize-pane -Z -t "$ze2" +check_ok send-keys -t "$ze2" C-d +i=0 +while [ "$($TMUX display-message -p -t P:13 '#{window_panes}')" != 1 ]; do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "Zoomed pane did not exit."; exit 1; } + sleep 0.1 +done +check_fmt "$ze0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0' +check_ok kill-window -t P:13 + +# resize-window restores both tiled and floating zoom targets after rebuilding +# the layout at the new size. +check_ok new-window -d -t P:14 -n zoom-resize 'cat' +zr0=$($TMUX display-message -p -t P:14.0 '#{pane_id}') +check_ok split-window -d -t P:14.0 'cat' +check_ok resize-pane -Z -t "$zr0" +check_ok resize-window -t P:14 -x 90 -y 30 +check_fmt "$zr0" \ + '#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \ + '90x30:1:1' +check_ok resize-pane -Z -t "$zr0" +zrf=$($TMUX new-pane -dP -F '#{pane_id}' -t P:14 -x 20 -y 6 'cat') +zrf_size=$($TMUX display-message -p -t "$zrf" \ + '#{pane_width}x#{pane_height}') +check_ok resize-pane -Z -t "$zrf" +check_ok resize-window -t P:14 -x 100 -y 32 +check_fmt "$zrf" \ + '#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \ + '100x32:1:1' +check_ok resize-pane -Z -t "$zrf" +check_fmt "$zrf" '#{pane_floating_flag}:#{pane_width}x#{pane_height}' \ + "1:$zrf_size" +check_ok kill-window -t P:14 + # --------------------------------------------------------------------------- # kill-pane. @@ -307,8 +431,10 @@ check_ok kill-pane -t "$p3" check_panes P:0 "0:$p0 1:$p2 2:$p1" # -a kills every pane except the target. +check_ok resize-pane -Z -t "$p0" check_ok kill-pane -a -t "$p0" check_panes P:0 "0:$p0" +check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0' # Killing the last pane in a window kills the window. check_ok new-window -d -t P:7 -n goner @@ -516,6 +642,10 @@ check_panes P:3 "0:$r0 1:$r1 2:$r2" # Swapping a pane with itself quietly does nothing. check_ok swap-pane -d -s "$r1" -t "$r1" check_panes P:3 "0:$r0 1:$r1 2:$r2" +check_ok resize-pane -Z -t "$r1" +check_ok swap-pane -d -Z -s "$r1" -t "$r1" +check_fmt "$r1" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '1:1' +check_ok resize-pane -Z -t "$r1" # Panes can be swapped between different windows. check_ok swap-pane -d -s "$o0" -t "$r1" @@ -525,10 +655,38 @@ check_ok swap-pane -d -s "$r1" -t "$o0" check_panes P:3 "0:$r0 1:$r1 2:$r2" check_panes P:5 "0:$o0" +# With both windows zoomed, -Z preserves each window's zoom when hidden panes +# are exchanged and when the zoom targets themselves are exchanged. +o1=$($TMUX split-window -dP -F '#{pane_id}' -t P:5.0) +check_ok resize-pane -Z -t "$r0" +check_ok resize-pane -Z -t "$o0" +check_ok swap-pane -d -Z -s "$r1" -t "$o1" +check_fmt "$r0" \ + '#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'swaps:1:1' +check_fmt "$o0" \ + '#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'other:1:1' +check_fmt "$r1" '#{window_name}' 'other' +check_fmt "$o1" '#{window_name}' 'swaps' + +check_ok swap-pane -d -Z -s "$r0" -t "$o0" +check_fmt "$o0" \ + '#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'swaps:1:1' +check_fmt "$r0" \ + '#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'other:1:1' +check_ok resize-pane -Z -t "$o0" +check_ok resize-pane -Z -t "$r0" + +# Restore the original panes before the remaining swap tests. +check_ok swap-pane -d -s "$r0" -t "$o0" +check_ok swap-pane -d -s "$r1" -t "$o1" +check_ok kill-pane -t "$o1" +check_panes P:3 "0:$r0 1:$r1 2:$r2" +check_panes P:5 "0:$o0" + # -Z keeps the window zoomed across the swap. check_ok resize-pane -Z -t "$r0" check_ok swap-pane -d -Z -s "$r0" -t "$r1" -check_fmt 'P:3' '#{window_zoomed_flag}' '1' +check_fmt "$r0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '1:1' check_ok resize-pane -Z -t P:3 check_panes P:3 "0:$r1 1:$r0 2:$r2" diff --git a/regress/window-ops.sh b/regress/window-ops.sh index bdb1b5fb9..25cbd8db0 100644 --- a/regress/window-ops.sh +++ b/regress/window-ops.sh @@ -17,7 +17,8 @@ # unlink the last link without -k; # - swap-window within and between sessions, -d keeping the active window, # and the grouped-sessions error; -# - rotate-window -U/-D rotating pane positions; +# - rotate-window -U/-D rotating pane positions, with and without preserving +# zoom using -Z; # - kill-window switching to the last (previously current) window, kill-window # -a killing all other windows and the "-f only valid with -a" guard. # @@ -306,6 +307,24 @@ check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p1" check_ok rotate-window -D -t R:0 check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p0" +# Rotation without -Z unzooms. With -Z it preserves zoom and transfers it to +# the pane which arrives at the active position. +layout=$($TMUX display-message -p -t R:0 '#{window_layout}') +check_ok resize-pane -Z -t "$p0" +check_ok rotate-window -U -t R:0 +check_fmt "$p1" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '0:0:1' +check_ok rotate-window -D -t R:0 +check_fmt 'R:0' '#{window_layout}' "$layout" + +check_ok resize-pane -Z -t "$p0" +check_ok rotate-window -U -Z -t R:0 +check_fmt "$p1" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '1:1:1' +check_ok resize-pane -Z -t "$p1" +check_ok rotate-window -D -t R:0 +check_fmt 'R:0' '#{window_layout}' "$layout" + # --------------------------------------------------------------------------- # kill-window. From 554c4bbde418e5e9b8220e4d4ceea5753db53a41 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 20 Aug 2026 10:19:19 +0100 Subject: [PATCH 15/30] Tests for modal panes on top. --- regress/modal-pane.sh | 136 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/regress/modal-pane.sh b/regress/modal-pane.sh index d02a75683..b4217aa0a 100644 --- a/regress/modal-pane.sh +++ b/regress/modal-pane.sh @@ -145,12 +145,16 @@ case "$(fmt "$modal" '#{pane_flags}')" in *O*) ;; *) fail "modal pane flags do not include O" ;; esac +case "$(fmt "$modal" '#{pane_flags}')" in +*A*) ;; +*) fail "modal pane flags do not include A" ;; +esac case "$(fmt modal:0 '#{window_flags}')" in *O*) ;; *) fail "modal window flags do not include O" ;; esac must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal" -must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 0 +must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1 check_fail "window already has a modal pane" \ new-pane -O -x 10 -y 4 'cat' @@ -177,12 +181,14 @@ under=$($TMUX split-window -PF '#{pane_id}' -t "$p0" 'cat') || sleep 1 must_equal "$(fmt modal:0 '#{pane_id}')" "$modal" must_equal "$(fmt "$under" '#{pane_active}')" 0 +must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1 float=$($TMUX new-pane -PF '#{pane_id}' -x 10 -y 4 -X 5 -Y 3 'cat') || fail "new floating pane under modal failed" sleep 1 must_equal "$(fmt modal:0 '#{pane_id}')" "$modal" must_equal "$(fmt "$float" '#{pane_active}')" 0 +must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1 check_ok new-window -d -t modal: -n other 'cat' check_ok select-window -t modal:other @@ -321,6 +327,7 @@ check_ok kill-pane -t "$modal" sleep 1 must_equal "$(fmt modal:0 '#{window_modal_pane}')" '' must_equal "$(fmt modal:0 '#{pane_id}')" "$p0" +must_equal "$(fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 0:0 ignored=$($TMUX new-pane -KdPF '#{pane_id}' -t "$p0" 'cat') || fail "new-pane -K without -O failed" @@ -352,5 +359,132 @@ new_top=$(fmt "$modal" '#{pane_top}') check_ok kill-pane -t "$modal" sleep 1 +# A nonmodal floating pane may remain above zoom, and switching between it and +# the zoomed tiled pane must not unzoom the window. +check_ok new-window -d -t modal: -n float-over-zoom 'cat' +base=$(fmt modal:float-over-zoom '#{pane_id}') +check_ok split-window -dh -t "$base" 'cat' +check_ok resize-pane -Z -t "$base" +over=$($TMUX new-pane -APF '#{pane_id}' -t "$base" \ + -x 20 -y 5 -X 20 -Y 10 'cat') || + fail "new-pane -A failed" +must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_active}')" 1:1 +case "$(fmt "$over" '#{pane_flags}')" in +*A*) ;; +*) fail "float-over-zoom pane flags do not include A" ;; +esac +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1 +check_ok select-pane -t "$base" +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_active}')" 1:1 +check_ok select-pane -t "$over" +must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1 +client=$($TMUX list-clients -F '#{client_name}' | head -1) +[ -n "$client" ] || fail "no client for switch-client test" +check_ok switch-client -c "$client" -t "$base" +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_active}')" 1:1 +check_ok switch-client -c "$client" -t "$over" +must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1 +check_ok kill-pane -t "$over" +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1 +check_ok resize-pane -Z -t "$base" + +ignored=$($TMUX new-pane -ALdPF '#{pane_id}' -t "$base" 'cat') || + fail "new-pane -A -L failed" +case "$(fmt "$ignored" '#{pane_flags}')" in +*A*) fail "tiled pane flags include A" ;; +*) ;; +esac + +# Existing floating panes are filtered when zoom begins: -A panes remain in the +# visible layout and ordinary floating panes do not. +check_ok new-window -d -t modal: -n existing-over-zoom 'cat' +base=$(fmt modal:existing-over-zoom '#{pane_id}') +check_ok split-window -dh -t "$base" 'cat' +over=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \ + -x 20 -y 5 -X 20 -Y 10 'cat') || + fail "pre-existing new-pane -A failed" +under=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \ + -x 15 -y 4 -X 2 -Y 2 'cat') || + fail "pre-existing ordinary new-pane failed" +check_ok select-window -t modal:existing-over-zoom +check_ok select-pane -t "$base" +check_ok resize-pane -Z -t "$base" +must_equal "$(fmt "$over" '#{pane_floating_flag}')" 1 +must_equal "$(fmt "$under" '#{pane_floating_flag}')" 0 +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1 + +# Geometry changed in the visible zoom layout is copied back when unzooming. +check_ok select-pane -t "$over" +left=$(fmt "$over" '#{pane_left}') +top=$(fmt "$over" '#{pane_top}') +meta_drag $((left + 2)) $((top + 2)) $((left + 7)) $((top + 4)) +new_left=$(fmt "$over" '#{pane_left}') +new_top=$(fmt "$over" '#{pane_top}') +[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] || + fail "float-over-zoom pane did not move" +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1 +check_ok resize-pane -Z -t "$base" +must_equal "$(fmt "$over" '#{pane_left}:#{pane_top}')" \ + "$new_left:$new_top" +must_equal "$(fmt "$under" '#{pane_floating_flag}')" 1 + +# Resizing with the over-zoom pane active restores the original zoom target. +check_ok resize-pane -Z -t "$base" +check_ok select-pane -t "$over" +check_ok resize-window -t modal:existing-over-zoom -x 90 -y 30 +must_equal "$(fmt "$base" \ + '#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}')" \ + 90x30:1:1 +must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_active}')" 1:1 + +# Natural pane exit uses a different removal path from kill-pane and must also +# preserve zoom. +dying=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \ + -x 12 -y 4 -X 4 -Y 3 'true') || + fail "short-lived new-pane -A failed" +i=0 +while $TMUX list-panes -a -F '#{pane_id}' | grep -qx "$dying"; do + i=$((i + 1)) + [ $i -gt 50 ] && fail "short-lived float-over-zoom pane did not exit" + sleep 0.1 +done +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1 + +# A pane with -A is also above a zoom target which was itself floating. The +# temporary tiled target must sit behind retained floating panes, then return +# to the normal floating z order when unzoomed. +check_ok new-window -d -t modal: -n floating-zoom-target 'cat' +base=$(fmt modal:floating-zoom-target '#{pane_id}') +check_ok split-window -dh -t "$base" 'cat' +target=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \ + -x 30 -y 10 -X 10 -Y 5 'cat') || + fail "floating zoom target creation failed" +over=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \ + -x 15 -y 5 -X 15 -Y 8 'cat') || + fail "float-over-zoom pane creation failed" +check_ok select-window -t modal:floating-zoom-target +check_ok select-pane -t "$target" +must_equal "$(fmt "$target" '#{pane_z}')" 0 +must_equal "$(fmt "$over" '#{pane_z}')" 1 + +check_ok resize-pane -Z -t "$target" +must_equal "$(fmt "$target" \ + '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_floating_flag}:#{pane_z}')" \ + 1:1:0:2 +must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_z}')" 1:0 +check_ok select-pane -t "$over" +must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1 +check_ok resize-pane -Z -t "$target" +must_equal "$(fmt "$over" '#{pane_active}:#{pane_z}')" 1:0 +must_equal "$(fmt "$target" '#{pane_floating_flag}:#{pane_z}')" 1:1 + +# If the target remains active, it returns to the front on unzoom. +check_ok select-pane -t "$target" +check_ok resize-pane -Z -t "$target" +must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_z}')" 1:0 +check_ok resize-pane -Z -t "$target" +must_equal "$(fmt "$target" '#{pane_active}:#{pane_z}')" 1:0 +must_equal "$(fmt "$over" '#{pane_z}')" 1 + cleanup exit 0 From 95a71707098b1cf60a626adae4b2fbcaa8da4dee Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 20 Aug 2026 09:19:24 +0000 Subject: [PATCH 16/30] Add a way for floating panes to stay above zoom (-A) flag. Use by default for modal panes to match popups. --- cmd-select-pane.c | 19 +++++-- cmd-split-window.c | 33 ++++++++--- cmd-switch-client.c | 12 ++-- layout.c | 13 +++-- resize.c | 14 ++--- server-client.c | 5 +- server-fn.c | 11 ++-- spawn.c | 13 +++-- tmux.1 | 10 ++-- tmux.h | 10 ++-- window.c | 130 ++++++++++++++++++++++++++++++++++---------- 11 files changed, 191 insertions(+), 79 deletions(-) diff --git a/cmd-select-pane.c b/cmd-select-pane.c index dbf147cb5..e23ca0adc 100644 --- a/cmd-select-pane.c +++ b/cmd-select-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-select-pane.c,v 1.77 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: cmd-select-pane.c,v 1.78 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -160,6 +160,7 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) char *title; const char *style; struct options_entry *o; + int visible, Zflag = args_has(args, 'Z'); if (entry == &cmd_last_pane_entry || args_has(args, 'l')) { /* @@ -185,14 +186,18 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) server_redraw_window_borders(lastwp->window); server_status_window(lastwp->window); } else { - if (window_push_zoom(w, 0, args_has(args, 'Z'))) + if (w->modal != NULL && lastwp != w->modal) + visible = 1; + else + visible = window_pane_is_visible(lastwp); + if (!visible && window_push_zoom(w, 0, Zflag)) server_redraw_window(w); window_redraw_active_switch(w, lastwp); if (window_set_active_pane(w, lastwp, 1)) { cmd_find_from_winlink(current, wl, 0); cmd_select_pane_redraw(w); } - if (window_pop_zoom(w)) + if (!visible && window_pop_zoom(w)) server_redraw_window(w); } return (CMD_RETURN_NORMAL); @@ -268,14 +273,18 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) if (wp == w->active) return (CMD_RETURN_NORMAL); - if (window_push_zoom(w, 0, args_has(args, 'Z'))) + if (w->modal != NULL && wp != w->modal) + visible = 1; + else + visible = window_pane_is_visible(wp); + if (!visible && window_push_zoom(w, 0, Zflag)) server_redraw_window(w); window_redraw_active_switch(w, wp); if (window_set_active_pane(w, wp, 1)) cmd_find_from_winlink_pane(current, wl, wp, 0); cmdq_insert_hook(s, item, current, "after-select-pane"); cmd_select_pane_redraw(w); - if (window_pop_zoom(w)) + if (!visible && window_pop_zoom(w)) server_redraw_window(w); return (CMD_RETURN_NORMAL); diff --git a/cmd-split-window.c b/cmd-split-window.c index 4980d6a9b..b41f8e6aa 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.149 2026/08/19 10:56:10 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.150 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -41,8 +41,8 @@ const struct cmd_entry cmd_new_pane_entry = { .name = "new-pane", .alias = "newp", - .args = { "bB:Cc:de:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, - .usage = "[-bCdefhIkKLMOPvWZ] [-B border-lines] " + .args = { "AbB:Cc:de:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, + .usage = "[-AbCdefhIkKLMOPvWZ] [-B border-lines] " "[-c start-directory] [-e environment] " "[-F format] [-l size] [-m message] [-p percentage] " "[-s style] [-S active-border-style] " @@ -90,6 +90,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) struct cmd_find_state fs; struct key_event *event = cmdq_get_event(item); int input, empty, is_floating, flags = 0; + int restore_zoom = 0; const char *template, *style, *value; char *cause = NULL, *cp, *title; const struct options_table_entry *oe; @@ -97,10 +98,16 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) enum pane_lines lines; u_int count = args_count(args); + if (window_active_pane_is_over_zoom(w)) + restore_zoom = 1; + if (cmd_get_entry(self) == &cmd_new_pane_entry) is_floating = !args_has(args, 'L'); else { - window_unzoom(w, 1); + if (!window_pane_is_visible(wp)) + restore_zoom = 0; + if (!restore_zoom) + window_unzoom(w, 1); is_floating = window_pane_is_floating(wp); flags |= SPAWN_SPLIT; } @@ -134,7 +141,11 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'Z')) flags |= SPAWN_ZOOM; if (args_has(args, 'O')) - flags |= SPAWN_MODAL; + flags |= SPAWN_MODAL|SPAWN_FLOATOVERZOOM; + if (is_floating && args_has(args, 'A')) + flags |= SPAWN_FLOATOVERZOOM; + if ((w->flags & WINDOW_ZOOMED) && (flags & SPAWN_FLOATOVERZOOM)) + restore_zoom = 1; input = args_has(args, 'I'); if (input || (count == 1 && *args_string(args, 0) == '\0')) @@ -170,6 +181,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (cause != NULL) { cmdq_error(item, "%s", cause); free(cause); + if (restore_zoom) + window_pop_zoom(w); return (CMD_RETURN_ERROR); } @@ -274,7 +287,10 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (~flags & SPAWN_DETACHED) cmd_find_from_winlink_pane(current, wl, new_wp, 0); - if ((~flags & SPAWN_FLOATING) && !args_has(args, 'O')) { + if (restore_zoom) { + window_pop_zoom(wp->window); + server_redraw_window(wp->window); + } else if ((~flags & SPAWN_FLOATING) && !args_has(args, 'O')) { window_pop_zoom(wp->window); server_redraw_window(wp->window); } @@ -325,8 +341,9 @@ fail: if (!is_floating) layout_close_pane(new_wp); window_remove_pane(wp->window, new_wp); - } else if (args_has(args, 'O')) - window_pop_modal_zoom(wp->window); + } + if (restore_zoom || (~flags & SPAWN_FLOATING)) + window_pop_zoom(wp->window); if (sc.argv != NULL) cmd_free_argv(sc.argc, sc.argv); environ_free(sc.environ); diff --git a/cmd-switch-client.c b/cmd-switch-client.c index 177107b47..44ea43cbc 100644 --- a/cmd-switch-client.c +++ b/cmd-switch-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-switch-client.c,v 1.74 2026/05/22 15:22:43 nicm Exp $ */ +/* $OpenBSD: cmd-switch-client.c,v 1.75 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -53,7 +53,7 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item) struct cmd_find_state target; const char *tflag = args_get(args, 't'); enum cmd_find_type type; - int flags; + int flags, visible, Zflag = args_has(args, 'Z'); struct client *c = cmdq_get_client(item); struct client *tc = cmdq_get_target_client(item); struct session *s; @@ -139,11 +139,15 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); if (wl != NULL && wp != NULL && wp != wl->window->active) { w = wl->window; - if (window_push_zoom(w, 0, args_has(args, 'Z'))) + if (w->modal != NULL && wp != w->modal) + visible = 1; + else + visible = window_pane_is_visible(wp); + if (!visible && window_push_zoom(w, 0, Zflag)) server_redraw_window(w); window_redraw_active_switch(w, wp); window_set_active_pane(w, wp, 1); - if (window_pop_zoom(w)) + if (!visible && window_pop_zoom(w)) server_redraw_window(w); } if (wl != NULL) { diff --git a/layout.c b/layout.c index b8ba1c2ef..02a4b681a 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout.c,v 1.96 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: layout.c,v 1.97 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -1650,7 +1650,10 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args, return (NULL); } - window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM)); + if (window_active_pane_is_over_zoom(w)) + window_push_zoom(w, 0, 1); + else + window_push_zoom(w, 1, (flags & SPAWN_ZOOM)); lc = layout_split_pane(wp, type, size, flags); if (lc == NULL) *cause = xstrdup("no space for a new pane"); @@ -1677,8 +1680,10 @@ layout_get_floating_cell(struct cmdq_item *item, struct args *args, return (NULL); } - if (flags & SPAWN_MODAL) - window_push_modal_zoom(w); + if (flags & SPAWN_FLOATOVERZOOM) + window_push_zoom(wp->window, 0, 1); + else if (window_active_pane_is_over_zoom(w)) + window_push_zoom(wp->window, 0, 1); else window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM)); lcnew = layout_floating_pane(w, wp, &fg); diff --git a/resize.c b/resize.c index 4f7ee490a..67562522d 100644 --- a/resize.c +++ b/resize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: resize.c,v 1.58 2026/07/17 08:37:29 nicm Exp $ */ +/* $OpenBSD: resize.c,v 1.59 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -42,8 +42,8 @@ resize_fire_window_resized(struct window *w, u_int old_sx, u_int old_sy) void resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) { - u_int old_sx = w->sx, old_sy = w->sy; - int zoomed; + struct window_pane *zwp; + u_int old_sx = w->sx, old_sy = w->sy; /* Check size limits. */ if (sx < WINDOW_MINIMUM) @@ -56,8 +56,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) sy = WINDOW_MAXIMUM; /* If the window is zoomed, unzoom. */ - zoomed = w->flags & WINDOW_ZOOMED; - if (zoomed) + zwp = window_zoomed_pane(w); + if (zwp != NULL) window_unzoom(w, 1); /* Resize the layout first. */ @@ -73,8 +73,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) sx, sy, w->layout_root->g.sx, w->layout_root->g.sy); /* Restore the window zoom state. */ - if (zoomed) - window_zoom(w->active); + if (zwp != NULL && window_has_pane(w, zwp)) + window_zoom(zwp); tty_update_window_offset(w); server_redraw_window(w); diff --git a/server-client.c b/server-client.c index a631a90a9..f1cdd8736 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.504 2026/08/19 10:56:10 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.505 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -806,8 +806,7 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, } else { /* Try the pane borders. */ TAILQ_FOREACH(fwp, &w->panes, entry) { - if ((w->flags & WINDOW_ZOOMED) && - (~fwp->flags & PANE_ZOOMED)) + if (!window_pane_is_visible(fwp)) continue; if (window_pane_is_floating(fwp) && window_pane_get_pane_lines(fwp) == PANE_LINES_NONE) diff --git a/server-fn.c b/server-fn.c index 503516afe..c833b0add 100644 --- a/server-fn.c +++ b/server-fn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-fn.c,v 1.150 2026/08/18 07:32:09 nicm Exp $ */ +/* $OpenBSD: server-fn.c,v 1.151 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -227,10 +227,11 @@ server_kill_pane(struct window_pane *wp) server_kill_window(w, 1); recalculate_sizes(); } else { - server_unzoom_window(w); + window_push_zoom(w, 0, wp->flags & PANE_FLOATOVERZOOM); server_client_remove_pane(wp); layout_close_pane(wp); window_remove_pane(w, wp); + window_pop_zoom(w); server_redraw_window(w); } } @@ -418,15 +419,17 @@ server_destroy_pane(struct window_pane *wp, int notify) if (notify) server_fire_pane_exit("pane-exited", wp); - server_unzoom_window(w); + window_push_zoom(w, 0, wp->flags & PANE_FLOATOVERZOOM); server_client_remove_pane(wp); layout_close_pane(wp); window_remove_pane(w, wp); if (TAILQ_EMPTY(&w->panes)) server_kill_window(w, 1); - else + else { + window_pop_zoom(w); server_redraw_window(w); + } } static void diff --git a/spawn.c b/spawn.c index 9e162217f..290b5da6f 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spawn.c,v 1.51 2026/08/18 07:43:44 nicm Exp $ */ +/* $OpenBSD: spawn.c,v 1.52 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -362,6 +362,8 @@ spawn_pane(struct spawn_context *sc, char **cause) } if (sc->flags & SPAWN_FLOATING) new_wp->layout_cell->flags |= LAYOUT_CELL_FLOATING; + if (sc->flags & SPAWN_FLOATOVERZOOM) + new_wp->flags |= PANE_FLOATOVERZOOM; /* * If window currently zoomed, window_set_active_pane calls @@ -725,10 +727,10 @@ spawn_editor(struct client *c, const char *buf, size_t len, lg.sy = w->sy * 9 / 10; lg.xoff = w->sx / 2 - lg.sx / 2; lg.yoff = w->sy / 2 - lg.sy / 2; - window_push_modal_zoom(w); + window_push_zoom(w, 0, 1); lc = layout_floating_pane(w, NULL, &lg); if (lc == NULL) { - window_pop_modal_zoom(w); + window_pop_zoom(w); spawn_editor_free(es); return (NULL); } @@ -745,17 +747,18 @@ spawn_editor(struct client *c, const char *buf, size_t len, sc.environ = env; sc.idx = -1; sc.cwd = _PATH_TMP; - sc.flags = SPAWN_FLOATING|SPAWN_MODAL; + sc.flags = SPAWN_FLOATING|SPAWN_MODAL|SPAWN_FLOATOVERZOOM; wp = spawn_pane(&sc, &cause); free(cmd); environ_free(env); if (wp == NULL) { free(cause); - window_pop_modal_zoom(w); + window_pop_zoom(w); spawn_editor_free(es); return (NULL); } + window_pop_zoom(w); options_set_number(wp->options, "remain-on-exit", 0); es->pid = wp->pid; wp->editor = es; diff --git a/tmux.1 b/tmux.1 index 1b692f05a..244daf9fc 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1157 2026/08/19 10:56:10 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1158 2026/08/20 09:19:24 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: August 19 2026 $ +.Dd $Mdocdate: August 20 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -3642,7 +3642,7 @@ but a different format may be specified with .Fl F . .Tg newp .It Xo Ic new\-pane -.Op Fl bCdefhIkKLMOPvWZ +.Op Fl AbCdefhIkKLMOPvWZ .Op Fl B Ar border\-lines .Op Fl c Ar start\-directory .Op Fl e Ar environment @@ -3691,8 +3691,8 @@ sets the pane border lines for floating panes; see .Fl M may be used when bound to a mouse drag key; the new floating pane is resized to follow the mouse drag. -It has no effect with -.Fl L . +.Fl A +creates a floating pane which remains visible above a zoomed pane. .Fl O creates a modal pane. A modal pane is always the active pane and prevents interaction with any other diff --git a/tmux.h b/tmux.h index b295e2313..c7fb2539e 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1428 2026/08/19 10:56:10 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1429 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1302,6 +1302,7 @@ struct window_pane { #define PANE_ACTIVITY 0x40000 #define PANE_CLOSEONCLICK 0x80000 #define PANE_CAPTUREALLKEYS 0x100000 +#define PANE_FLOATOVERZOOM 0x200000 bitstr_t *sync_dirty; u_int sync_dirty_size; @@ -1408,6 +1409,7 @@ struct window { struct window_pane *active; struct window_pane *modal; struct window_pane *modal_last; + struct window_pane *was_zoomed; struct window_panes last_panes; struct window_panes z_index; struct window_panes panes; @@ -1450,7 +1452,6 @@ struct window { #define WINDOW_ZOOMED 0x8 #define WINDOW_WASZOOMED 0x10 #define WINDOW_RESIZE 0x20 -#define WINDOW_WASMODALZOOMED 0x40 #define WINDOW_ALERTFLAGS (WINDOW_BELL|WINDOW_ACTIVITY|WINDOW_SILENCE) int alerts_queued; @@ -2494,6 +2495,7 @@ struct spawn_context { #define SPAWN_HORIZONTAL 0x200 #define SPAWN_SPLIT 0x400 #define SPAWN_MODAL 0x800 +#define SPAWN_FLOATOVERZOOM 0x1000 }; /* Paste buffer. */ @@ -3682,8 +3684,8 @@ void window_resize(struct window *, u_int, u_int, int, int); void window_pane_send_resize(struct window_pane *, u_int, u_int); int window_zoom(struct window_pane *); int window_unzoom(struct window *, int); -void window_push_modal_zoom(struct window *); -int window_pop_modal_zoom(struct window *); +int window_active_pane_is_over_zoom(struct window *); +struct window_pane *window_zoomed_pane(struct window *); int window_push_zoom(struct window *, int, int); int window_pop_zoom(struct window *); void window_lost_pane(struct window *, struct window_pane *); diff --git a/window.c b/window.c index 5067ab596..852c8e666 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.369 2026/07/29 14:06:32 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.370 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -711,7 +711,7 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify) return (0); if (w->modal != NULL && wp != w->modal) return (0); - if (w->flags & WINDOW_ZOOMED) + if ((w->flags & WINDOW_ZOOMED) && !window_pane_is_visible(wp)) window_unzoom(w, 1); lastwp = w->active; @@ -911,13 +911,18 @@ window_zoom(struct window_pane *wp) { struct window *w = wp->window; struct window_pane *wp1; + struct layout_cell *lc; + struct layout_geometry lg; if (w->flags & WINDOW_ZOOMED) return (-1); if (window_count_panes(w, 1) == 1) return (-1); - if (w->active != wp) + if (w->active != wp && + (w->active == NULL || + (~w->active->flags & PANE_FLOATOVERZOOM) || + !window_pane_is_floating(w->active))) window_set_active_pane(w, wp, 1); wp->flags |= PANE_ZOOMED; @@ -928,6 +933,22 @@ window_zoom(struct window_pane *wp) w->saved_layout_root = w->layout_root; layout_init(w, wp); + TAILQ_FOREACH(wp1, &w->panes, entry) { + lc = wp1->saved_layout_cell; + if (wp1 == wp || + (~wp1->flags & PANE_FLOATOVERZOOM) || + lc == NULL || + (~lc->flags & LAYOUT_CELL_FLOATING)) + continue; + memcpy(&lg, &lc->g, sizeof lg); + lc = layout_floating_pane(w, wp, &lg); + layout_assign_pane(lc, wp1, 0); + } + /* A floating zoom target is now tiled, so put it behind the floats. */ + if (wp->saved_layout_cell->flags & LAYOUT_CELL_FLOATING) { + TAILQ_REMOVE(&w->z_index, wp, zentry); + TAILQ_INSERT_TAIL(&w->z_index, wp, zentry); + } w->flags |= WINDOW_ZOOMED; events_fire_window("window-zoomed", w); events_fire_window("window-layout-changed", w); @@ -939,11 +960,26 @@ window_zoom(struct window_pane *wp) int window_unzoom(struct window *w, int notify) { - struct window_pane *wp; + struct window_pane *wp, *zoomed = NULL; + struct layout_cell *slc; - if (!(w->flags & WINDOW_ZOOMED)) + if (~w->flags & WINDOW_ZOOMED) return (-1); + TAILQ_FOREACH(wp, &w->panes, entry) { + if (wp->flags & PANE_ZOOMED) + zoomed = wp; + if (~wp->flags & PANE_FLOATOVERZOOM) + continue; + if (wp->flags & PANE_ZOOMED) + continue; + slc = wp->saved_layout_cell; + if (slc == NULL || wp->layout_cell == NULL) + continue; + memcpy(&slc->g, &wp->layout_cell->g, sizeof slc->g); + memcpy(&slc->fg, &wp->layout_cell->fg, sizeof slc->fg); + } + w->flags &= ~WINDOW_ZOOMED; layout_free(w, 0); w->layout_root = w->saved_layout_root; @@ -954,6 +990,22 @@ window_unzoom(struct window *w, int notify) wp->saved_layout_cell = NULL; wp->flags &= ~PANE_ZOOMED; } + /* Put a floating zoom target back into the floating part of the list. */ + if (zoomed != NULL && window_pane_is_floating(zoomed)) { + TAILQ_REMOVE(&w->z_index, zoomed, zentry); + if (zoomed == w->active) + TAILQ_INSERT_HEAD(&w->z_index, zoomed, zentry); + else { + TAILQ_FOREACH(wp, &w->z_index, zentry) { + if (!window_pane_is_floating(wp)) + break; + } + if (wp == NULL) + TAILQ_INSERT_TAIL(&w->z_index, zoomed, zentry); + else + TAILQ_INSERT_BEFORE(wp, zoomed, zentry); + } + } layout_fix_panes(w, NULL); if (notify) { @@ -965,48 +1017,69 @@ window_unzoom(struct window *w, int notify) return (0); } -void -window_push_modal_zoom(struct window *w) +struct window_pane * +window_zoomed_pane(struct window *w) { - if (w->flags & WINDOW_ZOOMED) - w->flags |= WINDOW_WASMODALZOOMED; - else - w->flags &= ~WINDOW_WASMODALZOOMED; - window_unzoom(w, 1); + struct window_pane *wp; + + if (~w->flags & WINDOW_ZOOMED) + return (NULL); + TAILQ_FOREACH_REVERSE(wp, &w->z_index, window_panes_zindex, zentry) { + if (wp->layout_cell != NULL && !window_pane_is_floating(wp)) + return (wp); + } + return (NULL); } int -window_pop_modal_zoom(struct window *w) +window_active_pane_is_over_zoom(struct window *w) { - struct window_pane *wp = w->active; - - if (~w->flags & WINDOW_WASMODALZOOMED) + if (~w->flags & WINDOW_ZOOMED) return (0); - w->flags &= ~WINDOW_WASMODALZOOMED; - if (wp != NULL && window_has_pane(w, wp)) - return (window_zoom(wp) == 0); - return (0); + if (w->active == NULL) + return (0); + if (~w->active->flags & PANE_FLOATOVERZOOM) + return (0); + return (window_pane_is_floating(w->active)); } int window_push_zoom(struct window *w, int always, int flag) { + struct window_pane *wp = window_zoomed_pane(w); + log_debug("%s: @%u %d", __func__, w->id, flag && (w->flags & WINDOW_ZOOMED)); if (flag && (always || (w->flags & WINDOW_ZOOMED))) w->flags |= WINDOW_WASZOOMED; else w->flags &= ~WINDOW_WASZOOMED; + if (w->flags & WINDOW_WASZOOMED) + w->was_zoomed = wp; + else + w->was_zoomed = NULL; return (window_unzoom(w, 1) == 0); } int window_pop_zoom(struct window *w) { + struct window_pane *wp = w->was_zoomed; + log_debug("%s: @%u %d", __func__, w->id, !!(w->flags & WINDOW_WASZOOMED)); - if (w->flags & WINDOW_WASZOOMED) - return (window_zoom(w->active) == 0); + if (w->flags & WINDOW_WASZOOMED) { + w->flags &= ~WINDOW_WASZOOMED; + w->was_zoomed = NULL; + if (w->active != NULL && + ((~w->active->flags & PANE_FLOATOVERZOOM) || + !window_pane_is_floating(w->active))) + wp = w->active; + if (wp == NULL || !window_has_pane(w, wp)) + wp = w->active; + if (wp != NULL) + return (window_zoom(wp) == 0); + } return (0); } @@ -1058,8 +1131,8 @@ window_lost_pane(struct window *w, struct window_pane *wp) server_clear_marked(); if (wp == w->modal_last) w->modal_last = NULL; - if (w->modal_last == NULL) - w->flags &= ~WINDOW_WASMODALZOOMED; + if (wp == w->was_zoomed) + w->was_zoomed = NULL; window_pane_stack_remove(&w->last_panes, wp); if (wp == w->active) { @@ -1086,7 +1159,6 @@ window_lost_pane(struct window *w, struct window_pane *wp) } } else if (wp == w->modal) { w->modal = w->modal_last = NULL; - w->flags &= ~WINDOW_WASMODALZOOMED; } redraw_invalidate_scene(w); } @@ -1094,13 +1166,9 @@ window_lost_pane(struct window *w, struct window_pane *wp) void window_remove_pane(struct window *w, struct window_pane *wp) { - int pop = (wp == w->modal); - window_lost_pane(w, wp); TAILQ_REMOVE(&w->panes, wp, entry); TAILQ_REMOVE(&w->z_index, wp, zentry); - if (pop && window_pop_modal_zoom(w)) - server_redraw_window(w); redraw_invalidate_scene(w); window_pane_destroy(wp); } @@ -1256,6 +1324,8 @@ window_pane_printable_flags(struct window_pane *wp) flags[pos++] = 'Z'; if (window_pane_is_floating(wp)) flags[pos++] = 'F'; + if (wp->flags & PANE_FLOATOVERZOOM) + flags[pos++] = 'A'; if (wp == w->modal) flags[pos++] = 'O'; flags[pos] = '\0'; @@ -1958,7 +2028,7 @@ window_pane_is_visible(struct window_pane *wp) { if (~wp->window->flags & WINDOW_ZOOMED) return (1); - return (wp == wp->window->active); + return (wp->layout_cell != NULL); } int From 936bb6e7cdff4fc7700a4f36a6b828b87d9f8241 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 21 Aug 2026 10:53:59 +0100 Subject: [PATCH 17/30] Regress for GitHub issue 5511 from Francesc Rosas. --- regress/tty-draw-line.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/regress/tty-draw-line.sh b/regress/tty-draw-line.sh index 09a956312..aa33982c9 100644 --- a/regress/tty-draw-line.sh +++ b/regress/tty-draw-line.sh @@ -105,6 +105,21 @@ sleep 1 CLIENT=$($TMUX2 list-clients -F '#{client_name}' | head -1) [ -n "$CLIENT" ] || fail "no inner client" +# A variation selector which widens the previous character must use window +# coordinates when checking visibility in a pane with a nonzero x offset. +$TMUX2 set -s variation-selector-always-wide on || exit 1 +WINDOW=$($TMUX2 neww -dPF '#{window_id}' "exec sleep 100") || exit 1 +PANE=$($TMUX2 splitw -dhPF '#{pane_id}' -t "$WINDOW" \ + "exec sleep 100") || exit 1 +$TMUX2 selectw -t "$WINDOW" || exit 1 +$TMUX2 respawnp -k -t "$PANE" \ + "printf '\nA\342\234\217\357\270\217B'; exec sleep 100" || exit 1 +sleep 1 +$TMUX2 capturep -p -t "$PANE" >$TMP || exit 1 +EXPECTED=$(printf 'A\342\234\217\357\270\217B') +check_line 2 "$EXPECTED" +$TMUX2 killw -t "$WINDOW" || exit 1 + # Long line, then short line: default cells after cellsize must clear stale text. capture check_line 1 "abcdefghijklmnopqrst" From 696a16cca27408f7d073d097efa4f763b614aef9 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 21 Aug 2026 09:53:04 +0000 Subject: [PATCH 18/30] Use correct X position when combining characters, GitHub issue 5511 from Francesc Rosas. --- screen-write.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/screen-write.c b/screen-write.c index 990ef4e0f..22b555bd5 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.287 2026/08/17 20:04:00 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.288 2026/08/21 09:53:04 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2760,10 +2760,11 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) struct grid *gd = s->grid; const struct utf8_data *ud = &gc->data; struct options *oo = global_options; - u_int i, n, cx = s->cx, cy = s->cy, vis, yoff = 0; + u_int i, n, cx = s->cx, cy = s->cy, vis; struct grid_cell last; struct tty_ctx ttyctx; int force_wide = 0, zero_width = 0; + int xoff = 0, yoff = 0; struct visible_ranges *r; /* Ignore U+3164 HANGUL_FILLER entirely. */ @@ -2857,9 +2858,11 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) * obscured in the middle, only on left or right, but there could be an * empty range in the visible ranges so we add them all up. */ - if (wp != NULL) + if (wp != NULL) { + xoff = wp->xoff; yoff = wp->yoff; - r = window_visible_ranges(wp, cx - n, cy + yoff, n, NULL); + } + r = window_visible_ranges(wp, xoff + cx - n, cy + yoff, n, NULL); for (i = 0, vis = 0; i < r->used; i++) vis += r->ranges[i].nx; if (vis < n) { From 3d44f75d0959313b1fc6a4e556d89927308a8cbe Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 07:05:23 +0000 Subject: [PATCH 19/30] Do not leak buffer name on failure, from Jeong, Heon. --- cmd-set-buffer.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd-set-buffer.c b/cmd-set-buffer.c index 6e640fec6..3e540f706 100644 --- a/cmd-set-buffer.c +++ b/cmd-set-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-set-buffer.c,v 1.37 2026/02/15 17:43:26 nicm Exp $ */ +/* $OpenBSD: cmd-set-buffer.c,v 1.38 2026/08/24 07:05:23 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -100,6 +100,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) cmdq_error(item, "%s", cause); goto fail; } + free(bufname); return (CMD_RETURN_NORMAL); } @@ -107,8 +108,10 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) cmdq_error(item, "no data specified"); goto fail; } - if ((newsize = strlen(args_string(args, 0))) == 0) + if ((newsize = strlen(args_string(args, 0))) == 0) { + free(bufname); return (CMD_RETURN_NORMAL); + } if (args_has(args, 'a') && pb != NULL) { olddata = paste_buffer_data(pb, &bufsize); @@ -127,6 +130,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'w') && tc != NULL) tty_set_selection(&tc->tty, "", bufdata, bufsize); + free(bufname); return (CMD_RETURN_NORMAL); fail: From 995b39579506049aadbe321fbc4fb9818c50e2a3 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 07:06:58 +0000 Subject: [PATCH 20/30] Do not leak format when drawing preview, from Jeong, Heon. --- window-tree.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/window-tree.c b/window-tree.c index 9c00e9e8a..3fdf968fb 100644 --- a/window-tree.c +++ b/window-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-tree.c,v 1.94 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window-tree.c,v 1.95 2026/08/24 07:06:58 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -642,6 +642,7 @@ window_tree_draw_session(struct window_tree_modedata *data, struct session *s, } free(label); } + format_free(ft); if (loop != end - 1) { screen_write_cursormove(ctx, cx + offset + width, cy, @@ -790,6 +791,7 @@ window_tree_draw_window(struct window_tree_modedata *data, struct session *s, } free(label); } + format_free(ft); if (loop != end - 1) { screen_write_cursormove(ctx, cx + offset + width, cy, From 4cabc2ae85eea7d80a4d7f7fc75c8191f4c72c86 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 07:08:19 +0000 Subject: [PATCH 21/30] Do not leak path when destroying client, from Jeong, Heon. --- server-client.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server-client.c b/server-client.c index f1cdd8736..e5b15fc3a 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.505 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.506 2026/08/24 07:08:19 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -529,6 +529,7 @@ server_client_lost(struct client *c) input_cancel_requests(c); free(c->title); + free(c->path); free((void *)c->cwd); evtimer_del(&c->repeat_timer); From 7792a74c76c03ad0074880d9e6bbdc27cf3afb7f Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 07:10:38 +0000 Subject: [PATCH 22/30] Free pane fallback range when freeing pane, from Jeong, Heon. --- window.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/window.c b/window.c index 852c8e666..a61e2652d 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.370 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.371 2026/08/24 07:10:38 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1534,10 +1534,9 @@ window_pane_free(struct window_pane *wp) log_debug("pane %%%u freed (%d references)", wp->id, wp->references); free(wp->searchstr); - screen_free(&wp->status_screen); screen_free(&wp->base); - + free(wp->r.ranges); options_free(wp->options); free((void *)wp->cwd); free(wp->shell); From c9f5ecb4b55dcdb80af914eb2320f71f0f1234ec Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 07:14:54 +0000 Subject: [PATCH 23/30] Do not leak cached status line, from Jeong, Heon. --- window.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/window.c b/window.c index a61e2652d..d1e74980b 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.371 2026/08/24 07:10:38 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.372 2026/08/24 07:14:54 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1543,6 +1543,7 @@ window_pane_free(struct window_pane *wp) cmd_free_argv(wp->argc, wp->argv); colour_palette_free(&wp->palette); style_ranges_free(&wp->border_status_line.ranges); + free(wp->border_status_line.expanded); free(wp); } From 41e701ad8408a3411bb59198a067c4ef471abba8 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 24 Aug 2026 15:27:25 +0100 Subject: [PATCH 24/30] Return early if image is zero height when generating fallback, otherwise we can allocated too little space. Reported by Vivek Parikh. --- image.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/image.c b/image.c index f0543dc4f..5613cf920 100644 --- a/image.c +++ b/image.c @@ -84,6 +84,11 @@ image_fallback(char **ret, u_int sx, u_int sy) char *buf, *label; u_int py, size, lsize; + if (sy == 0) { + *ret = xstrdup(""); + return; + } + /* Allocate first line. */ lsize = xasprintf(&label, "SIXEL IMAGE (%ux%u)\r\n", sx, sy) + 1; if (sx < lsize - 3) From 402d366bba94cf13eaddd793a889f56f91cfcb61 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 07:26:43 +0000 Subject: [PATCH 25/30] Add {} to shell escape characters (special in bash/ksh/csh), GitHub issue 5514. --- format.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/format.c b/format.c index 0eb5b394a..928eb078e 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.412 2026/08/05 07:31:08 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.413 2026/08/24 07:26:43 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -4369,7 +4369,7 @@ format_quote_shell(const char *s) at = out = xmalloc(strlen(s) * 2 + 1); for (cp = s; *cp != '\0'; cp++) { - if (strchr("|&;<>()$`\\\"'*?[# =%\n\t", *cp) != NULL) + if (strchr("|&;<>(){}$`\\\"'*?[# =%\n\t", *cp) != NULL) *at++ = '\\'; *at++ = *cp; } From 7d358208b9e2dce3b041b17d611bb285f1363673 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 14:07:56 +0000 Subject: [PATCH 26/30] Free any remaining items when destroying screen, from Jeong, Heon. --- screen-write.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/screen-write.c b/screen-write.c index 22b555bd5..c94ece65a 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.288 2026/08/21 09:53:04 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.289 2026/08/24 14:07:56 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -338,10 +338,18 @@ screen_write_make_list(struct screen *s) void screen_write_free_list(struct screen *s) { - u_int y; + struct screen_write_cline *cl; + struct screen_write_citem *ci, *ci1; + u_int y; - for (y = 0; y < screen_size_y(s); y++) - free(s->write_list[y].data); + for (y = 0; y < screen_size_y(s); y++) { + cl = &s->write_list[y]; + TAILQ_FOREACH_SAFE(ci, &cl->items, entry, ci1) { + TAILQ_REMOVE(&cl->items, ci, entry); + screen_write_free_citem(ci); + } + free(cl->data); + } free(s->write_list); } From 5b54c7d41af85cd7f15f0c272d8980db945e2033 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 15:05:26 +0000 Subject: [PATCH 27/30] Always use synchronized updates in copy mode (like when not in the active pane), since copy mode will almost always moved the cursor. From Artem Goldenberg in GitHub issue 5525. --- screen-write.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/screen-write.c b/screen-write.c index c94ece65a..cf7a662c8 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.289 2026/08/24 14:07:56 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.290 2026/08/24 15:05:26 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -305,12 +305,14 @@ screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, if (~ctx->flags & SCREEN_WRITE_SYNC) { /* - * For the active pane or for an overlay (no pane), we want to - * only use synchronized updates if requested (commands that - * move the cursor); for other panes, always use it, since the - * cursor will have to move. + * For the active pane showing its base screen or for an + * overlay (no pane), only use synchronized updates if + * requested (commands that move the cursor); for other panes + * or a pane in a mode, always use it, since the cursor will + * have to move. */ - if (ctx->wp != NULL && ctx->wp != ctx->wp->window->active) + if (ctx->wp != NULL && (ctx->wp != ctx->wp->window->active || + ctx->wp->screen != &ctx->wp->base)) ttyctx->flags |= TTY_CTX_SYNC; else { if (ctx->wp == NULL) From dc78d8f9ea78c30d31d622da37246d464d725bcc Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 20:34:26 +0000 Subject: [PATCH 28/30] Do not add messages to cfg_causes after config has finished, GitHub issue 5528. --- cmd-queue.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cmd-queue.c b/cmd-queue.c index dc1b8cc63..4ecec8f7d 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-queue.c,v 1.122 2026/08/03 13:38:42 nicm Exp $ */ +/* $OpenBSD: cmd-queue.c,v 1.123 2026/08/24 20:34:26 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -851,7 +851,18 @@ cmdq_error(struct cmdq_item *item, const char *fmt, ...) if (c == NULL) { cmd_get_source(cmd, &file, &line); - cfg_add_cause("%s:%u: %s", file, line, msg); + if (!cfg_finished) { + if (file != NULL) + cfg_add_cause("%s:%u: %s", file, line, msg); + else + cfg_add_cause("%s", msg); + } else { + if (file != NULL) { + server_add_message("message: %s:%u: %s", file, + line, msg); + } else + server_add_message("message: %s", msg); + } } else if (c->session == NULL || (c->flags & CLIENT_CONTROL)) { server_add_message("%s message: %s", c->name, msg); if (~c->flags & CLIENT_UTF8) { From 2d5328a8604b026d9b143c6c46d85283c0676ed5 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 21:17:19 +0000 Subject: [PATCH 29/30] If a pane has no scrollbar, don't bother to redraw it, and if redraw is deferred change scrollbar redraws into redraw-all-scrollbars not redraw entire window. GitHub issue 5529. --- server-client.c | 20 ++++++++++++++------ tmux.h | 4 ++-- window.c | 4 +++- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/server-client.c b/server-client.c index e5b15fc3a..819a57dad 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.506 2026/08/24 07:08:19 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.507 2026/08/24 21:17:19 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -2489,7 +2489,7 @@ server_client_check_redraw(struct client *c) /* Work out if a redraw is actually needed. */ needed = 0; - if (c->flags & CLIENT_ALLREDRAWFLAGS) + if (c->flags & (CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS)) needed = 1; else if (server_client_any_pane_redraw(c)) needed = 1; @@ -2515,8 +2515,14 @@ server_client_check_redraw(struct client *c) log_debug("redraw timer started"); evtimer_add(&ev, &tv); } - if (server_client_any_pane_redraw(c)) - c->flags |= CLIENT_REDRAWWINDOW; + TAILQ_FOREACH(wp, &w->panes, entry) { + if (wp->flags & PANE_REDRAW) { + c->flags |= CLIENT_REDRAWWINDOW; + break; + } + if (wp->flags & PANE_REDRAWSCROLLBAR) + c->flags |= CLIENT_REDRAWSCROLLBARS; + } return; } @@ -2535,7 +2541,8 @@ server_client_check_redraw(struct client *c) log_debug("%s: redraw pane %%%u", __func__, wp->id); redraw_pane(c, wp); - } else if (wp->flags & PANE_REDRAWSCROLLBAR) { + } else if ((wp->flags & PANE_REDRAWSCROLLBAR) || + (c->flags & CLIENT_REDRAWSCROLLBARS)) { log_debug("%s: redraw scrollbar %%%u", __func__, wp->id); redraw_pane_scrollbar(c, wp); @@ -2565,7 +2572,8 @@ server_client_check_redraw(struct client *c) * All the redraw flags can now be cleared. Also record how many bytes * were written. */ - c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_STATUSFORCE); + c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS| + CLIENT_STATUSFORCE); c->redraw = EVBUFFER_LENGTH(tty->out); log_debug("%s: redraw added %zu bytes", c->name, c->redraw); } diff --git a/tmux.h b/tmux.h index c7fb2539e..1366b57ea 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1429 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1430 2026/08/24 21:17:19 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2249,7 +2249,7 @@ struct client { #define CLIENT_STARTSERVER 0x10000000 #define CLIENT_REDRAWMENU 0x20000000 #define CLIENT_NOFORK 0x40000000 -/* 0x80000000ULL unused */ +#define CLIENT_REDRAWSCROLLBARS 0x80000000ULL #define CLIENT_CONTROL_PAUSEAFTER 0x100000000ULL #define CLIENT_CONTROL_WAITEXIT 0x200000000ULL #define CLIENT_WINDOWSIZECHANGED 0x400000000ULL diff --git a/window.c b/window.c index d1e74980b..9feb1e71c 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.372 2026/08/24 07:14:54 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.373 2026/08/24 21:17:19 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1471,6 +1471,8 @@ window_pane_scrollbar_overlay_visible(struct window_pane *wp) void window_pane_scrollbar_redraw(struct window_pane *wp) { + if (!window_pane_scrollbar_visible(wp)) + return; if (window_pane_scrollbar_overlay_visible(wp)) { wp->flags |= PANE_REDRAW; return; From 50ea32162f8ebf2f6843c2f89e15abf9e85d828f Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 24 Aug 2026 21:19:40 +0000 Subject: [PATCH 30/30] Correct a comparison ('\0' should be 0). --- window-buffer.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/window-buffer.c b/window-buffer.c index e19f6205d..f68e76f85 100644 --- a/window-buffer.c +++ b/window-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-buffer.c,v 1.51 2026/07/15 12:45:39 nicm Exp $ */ +/* $OpenBSD: window-buffer.c,v 1.52 2026/08/24 21:19:40 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -567,9 +567,7 @@ window_buffer_edit_close_cb(char *buf, size_t len, void *arg) } oldbuf = paste_buffer_data(pb, &oldlen); - if (oldlen != '\0' && - oldbuf[oldlen - 1] != '\n' && - buf[len - 1] == '\n') + if (oldlen != 0 && oldbuf[oldlen - 1] != '\n' && buf[len - 1] == '\n') len--; if (len != 0) paste_replace(pb, buf, len);