From 43092f8d541908009e79d1b51ebfe9cbee225f11 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 17 Jul 2026 09:13:33 +0100 Subject: [PATCH 01/19] Test for client lost during config file. --- regress/cfg-client-lost.sh | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100755 regress/cfg-client-lost.sh diff --git a/regress/cfg-client-lost.sh b/regress/cfg-client-lost.sh new file mode 100755 index 000000000..4593576bd --- /dev/null +++ b/regress/cfg-client-lost.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +TMPDIR=$(mktemp -d) || exit 1 +TMUX="$TEST_TMUX -S$TMPDIR/tmux.sock" +CONF="$TMPDIR/tmux.conf" +READY="$TMPDIR/ready" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + rm -rf "$TMPDIR" +} +trap cleanup 0 1 15 + +cat <$CONF +new-session -d -s keep 'sleep 1000' +run-shell -b 'touch $READY' +run-shell -d 1 true +source-file /dev/null +EOF + +$TMUX -f$CONF new-session -d -s client 'sleep 100' & +pid=$! +i=0 +while [ ! -f "$READY" ]; do + [ "$i" -eq 50 ] && exit 1 + i=$((i + 1)) + sleep 0.1 +done +kill "$pid" 2>/dev/null +wait "$pid" 2>/dev/null + +sleep 2 +$TMUX has-session -t keep || exit 1 + +exit 0 From 5cd43626ef66b9854625d860f3b68c0c47e362d3 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 17 Jul 2026 09:29:40 +0100 Subject: [PATCH 02/19] cmd_template_replace test. --- regress/cmd-template-replace.sh | 181 ++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 regress/cmd-template-replace.sh diff --git a/regress/cmd-template-replace.sh b/regress/cmd-template-replace.sh new file mode 100644 index 000000000..00730abc3 --- /dev/null +++ b/regress/cmd-template-replace.sh @@ -0,0 +1,181 @@ +#!/bin/sh + +# Exercise cmd_template_replace through command-prompt, which passes prompt +# input as argv to args_make_commands. This covers the quoting modes, indexed +# replacements, and cases where replacements are intentionally not made. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMP=$(mktemp -d) || exit 1 +TMUX_TMPDIR="$TMP" +export TMUX_TMPDIR + +OUT="$TEST_TMUX -LtestA$$ -f/dev/null" +IN="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $OUT kill-server 2>/dev/null + $IN kill-server 2>/dev/null + rm -rf "$TMP" +} +trap cleanup EXIT + +fail() +{ + echo "[FAIL] $1" + exit 1 +} + +settle() +{ + sleep 0.5 +} + +wait_option() +{ + option=$1 + expected=$2 + i=0 + + while [ "$i" -lt 30 ]; do + value=$($IN show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +accept_prompt() +{ + key=$1 + value=$2 + + $OUT send-keys "$key" || exit 1 + settle + $OUT send-keys -l "$value" || exit 1 + $OUT send-keys Enter || exit 1 + settle +} + +accept_two_prompts() +{ + key=$1 + first=$2 + second=$3 + + $OUT send-keys "$key" || exit 1 + settle + $OUT send-keys -l "$first" || exit 1 + $OUT send-keys Enter || exit 1 + settle + $OUT send-keys -l "$second" || exit 1 + $OUT send-keys Enter || exit 1 + settle +} + +reset_options() +{ + for option in \ + @r @first @second @plain @raw_tail @one @two @two_again \ + @double_index @marker; do + $IN set -g "$option" SENTINEL || exit 1 + done + $IN set -g @marker unchanged || exit 1 +} + +$IN new -d -x80 -y24 "sh -c 'exec sleep 1000'" || exit 1 +$IN set -g status on || exit 1 +$IN set -g status-keys emacs || exit 1 +$IN set -g window-size manual || exit 1 + +$IN bind -n M-s command-prompt -p '(single)' "set -g @r '%%'" || + exit 1 +$IN bind -n M-f command-prompt -p '(first)' \ + "set -g @first '%%' ; set -g @second '%%'" || + exit 1 +$IN bind -n M-d command-prompt -p '(double)' 'set -g @r "%%%"' || + exit 1 +$IN bind -n M-r command-prompt -p '(raw)' "set -g @r %1" || + exit 1 +$IN bind -n M-a command-prompt -p '(all)' \ + "set -g @first %1 ; set -g @second %1" || + exit 1 +$IN bind -n M-m command-prompt -p 'one,two' \ + "set -g @one %1 ; set -g @two %2 ; set -g @two_again %2" || + exit 1 +$IN bind -n M-i command-prompt -p 'one,two' 'set -g @double_index "%2%"' || + exit 1 +$IN bind -n M-n command-prompt -p '(none)' \ + "set -g @plain no-template-markers" || + exit 1 + +$OUT new -d -x80 -y24 || exit 1 +$OUT set -g status off || exit 1 +$OUT set -g window-size manual || exit 1 +$OUT send-keys -l "$IN attach" || exit 1 +$OUT send-keys Enter || exit 1 +sleep 1 + +reset_options + +# %% is for templates already inside single quotes. A single quote in the +# replacement must stay data and must not close the surrounding quotes. +payload="can't ; set -g @marker changed ; done" +accept_prompt M-s "$payload" +wait_option @r "$payload" +wait_option @marker unchanged + +# Only the first %% is replaced. +reset_options +payload="first'value" +accept_prompt M-f "$payload" +wait_option @first "$payload" +wait_option @second %% + +# %%% keeps the previous double-quote escaping behaviour. +reset_options +payload='a"$;~\z' +accept_prompt M-d "$payload" +wait_option @r "$payload" + +# %1 is intentionally left raw as an escape hatch. +reset_options +$IN set -g @raw_tail unchanged || exit 1 +accept_prompt M-r 'raw ; set -g @raw_tail yes' +wait_option @r raw +wait_option @raw_tail yes + +# All instances of %1 are replaced. +reset_options +accept_prompt M-a repeat +wait_option @first repeat +wait_option @second repeat + +# %1 and %2 select different prompt values, and all instances of %2 are +# replaced. +reset_options +accept_two_prompts M-m one two +wait_option @one one +wait_option @two two +wait_option @two_again two + +# %idx% uses double-quote escaping for indexed replacements. +reset_options +payload='b"$;~\y' +accept_two_prompts M-i ignored "$payload" +wait_option @double_index "$payload" + +# A template without replacement markers is left alone. +reset_options +accept_prompt M-n 'unused ; set -g @marker changed' +wait_option @plain no-template-markers +wait_option @marker unchanged + +exit 0 From b46684427ffbc1c20686731ef01beeaec0ba53f1 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 17 Jul 2026 09:37:32 +0100 Subject: [PATCH 03/19] Extend control client size test. --- regress/control-client-size.sh | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/regress/control-client-size.sh b/regress/control-client-size.sh index 3099b873f..140b0068f 100644 --- a/regress/control-client-size.sh +++ b/regress/control-client-size.sh @@ -1,8 +1,9 @@ #!/bin/sh # 947 -# size in control mode should change after refresh-client -C, and -x and -y -# should work without -d for control clients +# size in control mode should change after refresh-client -C, per-window +# control client sizes should work, and -x and -y should work without -d for +# control clients PATH=/bin:/usr/bin TERM=screen @@ -47,4 +48,21 @@ $TMUX ls -F':#{window_width} #{window_height}' >>$OUT printf ":100 50\n:80 24\n"|cmp -s $OUT - || exit 1 $TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-4 -f/dev/null" +$TMUX -f/dev/null new -d || exit 1 +$TMUX neww || exit 1 +w0=$($TMUX display -p -t :0 '#{window_id}') +w1=$($TMUX display -p -t :1 '#{window_id}') +cat <$TMP +refresh -C 80,24 +refresh -C ${w0}:40x10 +display -p -t ${w0} 'w0 #{window_width} #{window_height}' +display -p -t ${w1} 'w1 #{window_width} #{window_height}' +refresh -C ${w0}: +display -p -t ${w0} 'w0c #{window_width} #{window_height}' +EOF +grep ^w $TMP >$OUT +printf "w0 40 10\nw1 80 24\nw0c 80 24\n"|cmp -s $OUT - || exit 1 +$TMUX kill-server 2>/dev/null + exit 0 From d5b1f3ad890259f6b91309f72dafcdeb4612bbcb Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 17 Jul 2026 08:13:23 +0000 Subject: [PATCH 04/19] Clear cfg_client if the client is destroyed to prevent UAF found by Ilya Grigoriev. --- server-client.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server-client.c b/server-client.c index eb516187b..e9c62a745 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.494 2026/07/15 14:14:50 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.495 2026/07/17 08:13:23 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -502,6 +502,8 @@ server_client_lost(struct client *c) struct client_file *cf, *cf1; struct client_window *cw, *cw1; + if (cfg_client == c) + cfg_client = NULL; c->flags |= CLIENT_DEAD; server_client_clear_overlay(c); From a177d0f590e894ada7ee003b9efc976547550b35 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 17 Jul 2026 08:29:34 +0000 Subject: [PATCH 05/19] Change %% to escape single quotes (%1 remains with no quoting and %% with double quotes) to avoid a single quote being able to terminate quoted sections of commands. Fixes unexpected behaviour with session names containing single quotes reported by Aliz Hammond. --- cmd.c | 52 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/cmd.c b/cmd.c index 3b7586084..46bb2600a 100644 --- a/cmd.c +++ b/cmd.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd.c,v 1.188 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: cmd.c,v 1.189 2026/07/17 08:29:34 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -841,14 +841,19 @@ cmd_mouse_pane(struct mouse_event *m, struct session **sp, return (wp); } -/* Replace the first %% or %idx in template by s. */ +/* + * Replace the first %% or any %idx in template by s. %% is intended for use in + * single quotes, so ' is escaped. %%% and %idx% are for double quotes, so a + * list of special characters is escaped. %idx is left unescaped. + */ char * cmd_template_replace(const char *template, const char *s, int idx) { - char ch, *buf; - const char *ptr, *cp, quote[] = "\"\\$;~"; - int replaced, quoted; - size_t len, slen; + char ch, *buf; + const char *ptr, *cp, dquote[] = "\"\\$;~"; + int replaced; + size_t len, slen; + enum { NQ, SQ, DQ } quote; if (strchr(template, '%') == NULL) return (xstrdup(template)); @@ -862,23 +867,39 @@ cmd_template_replace(const char *template, const char *s, int idx) while (*ptr != '\0') { switch (ch = *ptr++) { case '%': - if (*ptr < '1' || *ptr > '9' || *ptr - '0' != idx) { + if (*ptr >= '1' && *ptr <= '9' && *ptr - '0' == idx) { + ptr++; + quote = NQ; + if (*ptr == '%') { + quote = DQ; + ptr++; + } + } else { if (*ptr != '%' || replaced) break; replaced = 1; - } - ptr++; - - quoted = (*ptr == '%'); - if (quoted) ptr++; + quote = SQ; + if (*ptr == '%') { + quote = DQ; + ptr++; + } + } slen = strlen(s); - if (slen >= SIZE_MAX / 3 || len > SIZE_MAX - (slen * 3) - 1) + if (slen >= SIZE_MAX / 4 || + len > SIZE_MAX - (slen * 4) - 1) fatalx("argument too long"); - buf = xrealloc(buf, len + (slen * 3) + 1); + buf = xrealloc(buf, len + (slen * 4) + 1); for (cp = s; *cp != '\0'; cp++) { - if (quoted && strchr(quote, *cp) != NULL) + if (quote == SQ && *cp == '\'') { + buf[len++] = '\''; + buf[len++] = '\\'; + buf[len++] = '\''; + buf[len++] = '\''; + continue; + } + if (quote == DQ && strchr(dquote, *cp) != NULL) buf[len++] = '\\'; buf[len++] = *cp; } @@ -892,5 +913,6 @@ cmd_template_replace(const char *template, const char *s, int idx) buf[len] = '\0'; } + log_debug("%s: %s -> %s", __func__, template, buf); return (buf); } From 2711780dffe2a5348f8d0e02095acd34a532a5bb Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 17 Jul 2026 08:37:29 +0000 Subject: [PATCH 06/19] Move per-client window sizes into control.c since the only user is for control mode. --- cmd-refresh-client.c | 19 +++---- control.c | 91 +++++++++++++++++++++++++++++++- resize.c | 36 +++++-------- server-client.c | 122 +++++++++++++++++++++++++++---------------- tmux.h | 22 ++------ 5 files changed, 190 insertions(+), 100 deletions(-) diff --git a/cmd-refresh-client.c b/cmd-refresh-client.c index da2a27976..11b43bf22 100644 --- a/cmd-refresh-client.c +++ b/cmd-refresh-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-refresh-client.c,v 1.54 2026/07/05 08:24:00 nicm Exp $ */ +/* $OpenBSD: cmd-refresh-client.c,v 1.55 2026/07/17 08:37:29 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -66,7 +66,6 @@ cmd_refresh_client_control_client_size(struct cmd *self, struct cmdq_item *item) struct client *tc = cmdq_get_target_client(item); const char *size = args_get(args, 'C'); u_int w, x, y; - struct client_window *cw; if (sscanf(size, "@%u:%ux%u", &w, &x, &y) == 3) { if (x < WINDOW_MINIMUM || x > WINDOW_MAXIMUM || @@ -76,22 +75,16 @@ cmd_refresh_client_control_client_size(struct cmd *self, struct cmdq_item *item) } log_debug("%s: client %s window @%u: size %ux%u", __func__, tc->name, w, x, y); - cw = server_client_add_client_window(tc, w); - cw->sx = x; - cw->sy = y; + control_set_window_size(tc, w, x, y); tc->flags |= CLIENT_WINDOWSIZECHANGED; recalculate_sizes_now(1); return (CMD_RETURN_NORMAL); } if (sscanf(size, "@%u:", &w) == 1) { - cw = server_client_get_client_window(tc, w); - if (cw != NULL) { - log_debug("%s: client %s window @%u: no size", __func__, - tc->name, w); - cw->sx = 0; - cw->sy = 0; - recalculate_sizes_now(1); - } + log_debug("%s: client %s window @%u: no size", __func__, + tc->name, w); + control_clear_window_size(tc, w); + recalculate_sizes_now(1); return (CMD_RETURN_NORMAL); } diff --git a/control.c b/control.c index 8b4ad3884..92645a8bc 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.61 2026/07/10 15:45:11 nicm Exp $ */ +/* $OpenBSD: control.c,v 1.62 2026/07/17 08:37:29 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -78,9 +78,20 @@ struct control_pane { }; RB_HEAD(control_panes, control_pane); +/* Control client window size. */ +struct control_window { + u_int window; + u_int sx; + u_int sy; + + RB_ENTRY(control_window) entry; +}; +RB_HEAD(control_windows, control_window); + /* Control client state. */ struct control_state { struct control_panes panes; + struct control_windows windows; TAILQ_HEAD(, control_pane) pending_list; u_int pending_count; @@ -120,6 +131,18 @@ control_pane_cmp(struct control_pane *cp1, struct control_pane *cp2) } RB_GENERATE_STATIC(control_panes, control_pane, entry, control_pane_cmp); +/* Compare control windows. */ +static int +control_window_cmp(struct control_window *cw1, struct control_window *cw2) +{ + if (cw1->window < cw2->window) + return (-1); + if (cw1->window > cw2->window) + return (1); + return (0); +} +RB_GENERATE_STATIC(control_windows, control_window, entry, control_window_cmp); + /* Free a block. */ static void control_free_block(struct control_state *cs, struct control_block *cb) @@ -161,6 +184,66 @@ control_add_pane(struct client *c, struct window_pane *wp) return (cp); } +/* Get window for this client. */ +static struct control_window * +control_get_window(struct client *c, u_int window) +{ + struct control_state *cs = c->control_state; + struct control_window cw = { .window = window }; + + if (cs == NULL) + return (NULL); + return (RB_FIND(control_windows, &cs->windows, &cw)); +} + +/* Set window size for this client. */ +void +control_set_window_size(struct client *c, u_int window, u_int sx, u_int sy) +{ + struct control_state *cs = c->control_state; + struct control_window *cw; + + if (cs == NULL) + return; + cw = control_get_window(c, window); + if (cw == NULL) { + cw = xcalloc(1, sizeof *cw); + cw->window = window; + RB_INSERT(control_windows, &cs->windows, cw); + } + cw->sx = sx; + cw->sy = sy; +} + +/* Get window size for this client. */ +int +control_get_window_size(struct client *c, u_int window, u_int *sx, u_int *sy) +{ + struct control_window *cw; + + if ((cw = control_get_window(c, window)) == NULL) + return (0); + *sx = cw->sx; + *sy = cw->sy; + return (1); +} + +/* Clear window size for this client. */ +void +control_clear_window_size(struct client *c, u_int window) +{ + struct control_state *cs = c->control_state; + struct control_window *cw; + + if (cs == NULL) + return; + cw = control_get_window(c, window); + if (cw != NULL) { + RB_REMOVE(control_windows, &cs->windows, cw); + free(cw); + } +} + /* Discard output for a pane. */ static void control_discard_pane(struct client *c, struct control_pane *cp) @@ -749,6 +832,7 @@ control_start(struct client *c) cs = c->control_state = xcalloc(1, sizeof *cs); RB_INIT(&cs->panes); + RB_INIT(&cs->windows); TAILQ_INIT(&cs->pending_list); TAILQ_INIT(&cs->all_blocks); cs->subs = monitor_create_client(c, control_sub_change, NULL); @@ -800,6 +884,7 @@ control_stop(struct client *c) { struct control_state *cs = c->control_state; struct control_block *cb, *cb1; + struct control_window *cw, *cw1; if (cs == NULL) return; @@ -811,6 +896,10 @@ control_stop(struct client *c) bufferevent_free(cs->read_event); control_reset_offsets(c); + RB_FOREACH_SAFE(cw, control_windows, &cs->windows, cw1) { + RB_REMOVE(control_windows, &cs->windows, cw); + free(cw); + } TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1) control_free_block(cs, cb); diff --git a/resize.c b/resize.c index 1d83e4b70..4f7ee490a 100644 --- a/resize.c +++ b/resize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: resize.c,v 1.57 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: resize.c,v 1.58 2026/07/17 08:37:29 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -134,9 +134,8 @@ clients_calculate_size(int type, int current, struct client *c, int, int, struct session *, struct window *), u_int *sx, u_int *sy, u_int *xpixel, u_int *ypixel) { - struct client *loop; - struct client_window *cw; - u_int cx, cy, n = 0; + struct client *loop; + u_int cx, cy, n = 0; /* * Start comparing with 0 for largest and UINT_MAX for smallest or @@ -187,20 +186,10 @@ clients_calculate_size(int type, int current, struct client *c, continue; } - /* - * If the client has a per-window size, use this instead if it is - * smaller. - */ - if (w != NULL) - cw = server_client_get_client_window(loop, w->id); - else - cw = NULL; - /* Work out this client's size. */ - if (cw != NULL && cw->sx != 0 && cw->sy != 0) { - cx = cw->sx; - cy = cw->sy; - } else { + if (w == NULL || + !control_get_window_size(loop, w->id, &cx, &cy) || + cx == 0 || cy == 0) { cx = loop->tty.sx; cy = loop->tty.sy - status_line_size(loop); } @@ -247,17 +236,16 @@ skip: /* Look up per-window size if any. */ if (~loop->flags & CLIENT_WINDOWSIZECHANGED) continue; - cw = server_client_get_client_window(loop, w->id); - if (cw == NULL) + if (!control_get_window_size(loop, w->id, &cx, &cy)) continue; /* Clamp the size. */ log_debug("%s: %s size for @%u is %ux%u", __func__, - loop->name, w->id, cw->sx, cw->sy); - if (cw->sx != 0 && *sx > cw->sx) - *sx = cw->sx; - if (cw->sy != 0 && *sy > cw->sy) - *sy = cw->sy; + loop->name, w->id, cx, cy); + if (cx != 0 && *sx > cx) + *sx = cx; + if (cy != 0 && *sy > cy) + *sy = cy; } } if (*sx != UINT_MAX && *sy != UINT_MAX) diff --git a/server-client.c b/server-client.c index e9c62a745..b4f39c1d7 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.495 2026/07/17 08:13:23 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.496 2026/07/17 08:37:29 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -48,24 +49,46 @@ 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 void server_client_remove_active_pane_client(struct client *); static void server_client_dispatch(struct imsg *, void *); static int server_client_dispatch_command(struct client *, struct imsg *); static int server_client_dispatch_identify(struct client *, struct imsg *); static int server_client_dispatch_shell(struct client *); static void server_client_report_theme(struct client *, enum client_theme); -/* Compare client windows. */ +/* Client active pane. */ +struct client_active_pane { + struct client *client; + u_int window; + struct window_pane *pane; + + RB_ENTRY(client_active_pane) entry; +}; +RB_HEAD(client_active_panes, client_active_pane); + +/* Compare client active panes. */ static int -server_client_window_cmp(struct client_window *cw1, - struct client_window *cw2) +server_client_active_pane_cmp(struct client_active_pane *cap1, + struct client_active_pane *cap2) { - if (cw1->window < cw2->window) + uintptr_t c1 = (uintptr_t)cap1->client; + uintptr_t c2 = (uintptr_t)cap2->client; + + if (c1 < c2) return (-1); - if (cw1->window > cw2->window) + if (c1 > c2) + return (1); + if (cap1->window < cap2->window) + return (-1); + if (cap1->window > cap2->window) return (1); return (0); } -RB_GENERATE(client_windows, client_window, entry, server_client_window_cmp); +RB_GENERATE_STATIC(client_active_panes, client_active_pane, entry, + server_client_active_pane_cmp); + +static struct client_active_panes client_active_panes = + RB_INITIALIZER(&client_active_panes); /* Number of attached clients. */ u_int @@ -309,7 +332,6 @@ server_client_create(int fd) c->out_fd = -1; c->queue = cmdq_new(); - RB_INIT(&c->windows); RB_INIT(&c->files); c->tty.sx = 80; @@ -500,7 +522,6 @@ void server_client_lost(struct client *c) { struct client_file *cf, *cf1; - struct client_window *cw, *cw1; if (cfg_client == c) cfg_client = NULL; @@ -509,15 +530,12 @@ server_client_lost(struct client *c) server_client_clear_overlay(c); status_prompt_clear(c); status_message_clear(c); + server_client_remove_active_pane_client(c); RB_FOREACH_SAFE(cf, client_files, &c->files, cf1) { cf->error = EINTR; file_fire_done(cf); } - RB_FOREACH_SAFE(cw, client_windows, &c->windows, cw1) { - RB_REMOVE(client_windows, &c->windows, cw); - free(cw); - } TAILQ_REMOVE(&clients, c, entry); log_debug("lost client %p", c); @@ -3083,28 +3101,29 @@ server_client_get_flags(struct client *c) return (s); } -/* Get client window. */ -struct client_window * -server_client_get_client_window(struct client *c, u_int id) +/* Get client active pane. */ +static struct client_active_pane * +server_client_get_active_pane(struct client *c, u_int id) { - struct client_window cw = { .window = id }; + struct client_active_pane cap = { .client = c, .window = id }; - return (RB_FIND(client_windows, &c->windows, &cw)); + return (RB_FIND(client_active_panes, &client_active_panes, &cap)); } -/* Add client window. */ -struct client_window * -server_client_add_client_window(struct client *c, u_int id) +/* Add client active pane. */ +static struct client_active_pane * +server_client_add_active_pane(struct client *c, u_int id) { - struct client_window *cw; + struct client_active_pane *cap; - cw = server_client_get_client_window(c, id); - if (cw == NULL) { - cw = xcalloc(1, sizeof *cw); - cw->window = id; - RB_INSERT(client_windows, &c->windows, cw); + cap = server_client_get_active_pane(c, id); + if (cap == NULL) { + cap = xcalloc(1, sizeof *cap); + cap->client = c; + cap->window = id; + RB_INSERT(client_active_panes, &client_active_panes, cap); } - return (cw); + return (cap); } /* Get client active pane. */ @@ -3113,7 +3132,7 @@ server_client_get_pane(struct client *c) { struct session *s = c->session; struct window *w; - struct client_window *cw; + struct client_active_pane *cap; if (s == NULL) return (NULL); @@ -3123,10 +3142,10 @@ server_client_get_pane(struct client *c) return (w->modal); if (~c->flags & CLIENT_ACTIVEPANE) return (w->active); - cw = server_client_get_client_window(c, w->id); - if (cw == NULL) + cap = server_client_get_active_pane(c, w->id); + if (cap == NULL) return (w->active); - return (cw->pane); + return (cap->pane); } /* Set client active pane. */ @@ -3134,32 +3153,47 @@ void server_client_set_pane(struct client *c, struct window_pane *wp) { struct session *s = c->session; - struct client_window *cw; + struct client_active_pane *cap; if (s == NULL) return; if (wp->window->modal != NULL && wp != wp->window->modal) return; - cw = server_client_add_client_window(c, s->curw->window->id); - cw->pane = wp; + cap = server_client_add_active_pane(c, s->curw->window->id); + cap->pane = wp; log_debug("%s pane now %%%u", c->name, wp->id); } -/* Remove pane from client lists. */ +/* Remove active pane entries for a client. */ +static void +server_client_remove_active_pane_client(struct client *c) +{ + struct client_active_pane *cap, *cap1; + + RB_FOREACH_SAFE(cap, client_active_panes, &client_active_panes, cap1) { + if (cap->client == c) { + RB_REMOVE(client_active_panes, &client_active_panes, cap); + free(cap); + } + } +} + +/* Remove pane from client state. */ void server_client_remove_pane(struct window_pane *wp) { - struct client *c; - struct window *w = wp->window; - struct client_window *cw; + struct client *c; + struct client_active_pane *cap, *cap1; + + RB_FOREACH_SAFE(cap, client_active_panes, &client_active_panes, cap1) { + if (cap->pane == wp) { + RB_REMOVE(client_active_panes, &client_active_panes, cap); + free(cap); + } + } TAILQ_FOREACH(c, &clients, entry) { - cw = server_client_get_client_window(c, w->id); - if (cw != NULL && cw->pane == wp) { - RB_REMOVE(client_windows, &c->windows, cw); - free(cw); - } if (c->tty.mouse_last_pane == (int)wp->id) { c->tty.mouse_last_pane = -1; c->tty.mouse_drag_update = NULL; diff --git a/tmux.h b/tmux.h index 947401b4e..3d3cae5d8 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1406 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1407 2026/07/17 08:37:29 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2033,18 +2033,6 @@ struct client_file { }; RB_HEAD(client_files, client_file); -/* Client window. */ -struct client_window { - u_int window; - struct window_pane *pane; - - u_int sx; - u_int sy; - - RB_ENTRY(client_window) entry; -}; -RB_HEAD(client_windows, client_window); - /* Maximum time to be pasting. */ #define CLIENT_PASTE_TIME_LIMIT 5 @@ -2148,8 +2136,6 @@ struct client { const char *user; struct cmdq_list *queue; - struct client_windows windows; - struct control_state *control_state; u_int pause_age; @@ -3237,7 +3223,6 @@ void printflike(1, 2) server_add_message(const char *, ...); int server_create_socket(uint64_t, char **); /* server-client.c */ -RB_PROTOTYPE(client_windows, client_window, entry, server_client_window_cmp); u_int server_client_how_many(void); void server_client_set_overlay(struct client *, u_int, overlay_check_cb, overlay_mode_cb, overlay_draw_cb, overlay_key_cb, @@ -3265,8 +3250,6 @@ void server_client_loop(void); const char *server_client_get_cwd(struct client *, struct session *); void server_client_set_flags(struct client *, const char *); const char *server_client_get_flags(struct client *); -struct client_window *server_client_get_client_window(struct client *, u_int); -struct client_window *server_client_add_client_window(struct client *, u_int); struct window_pane *server_client_get_pane(struct client *); void server_client_set_pane(struct client *, struct window_pane *); void server_client_remove_pane(struct window_pane *); @@ -3959,6 +3942,9 @@ 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_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); struct window_pane_offset *control_pane_offset(struct client *, struct window_pane *, int *); void control_reset_offsets(struct client *); From 1ce000062af83bb1cb0aaef9ce6c5e80e67c3d58 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 17 Jul 2026 12:42:51 +0000 Subject: [PATCH 07/19] Remove the active-pane flag for now, there are some gaps in how this works and I don't like it. May come back in a different form (maybe just for windows). --- cmd-find.c | 4 +- cmd-run-shell.c | 6 +-- cmd-select-pane.c | 15 ++---- screen-redraw.c | 4 +- server-client.c | 132 ++-------------------------------------------- tmux.1 | 13 +---- tmux.h | 6 +-- tty.c | 4 +- window-border.c | 4 +- 9 files changed, 22 insertions(+), 166 deletions(-) diff --git a/cmd-find.c b/cmd-find.c index 5173591f9..b872d9eff 100644 --- a/cmd-find.c +++ b/cmd-find.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-find.c,v 1.86 2026/07/02 08:47:25 nicm Exp $ */ +/* $OpenBSD: cmd-find.c,v 1.87 2026/07/17 12:42:51 nicm Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott @@ -885,7 +885,7 @@ cmd_find_from_client(struct cmd_find_state *fs, struct client *c, int flags) if (c->session != NULL) { cmd_find_clear_state(fs, flags); - fs->wp = server_client_get_pane(c); + fs->wp = c->session->curw->window->active; if (fs->wp == NULL) { cmd_find_from_session(fs, c->session, flags); return (0); diff --git a/cmd-run-shell.c b/cmd-run-shell.c index 7f5514475..59d5aeea6 100644 --- a/cmd-run-shell.c +++ b/cmd-run-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-run-shell.c,v 1.92 2026/07/14 17:17:17 nicm Exp $ */ +/* $OpenBSD: cmd-run-shell.c,v 1.93 2026/07/17 12:42:51 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -90,8 +90,8 @@ cmd_run_shell_print(struct job *job, const char *msg) cmdq_print(cdata->item, "%s", msg); return; } - if (cdata->item != NULL && cdata->client != NULL) - wp = server_client_get_pane(cdata->client); + if (cdata->client != NULL && cdata->client->session != NULL) + wp = cdata->client->session->curw->window->active; if (wp == NULL && cmd_find_from_nothing(&fs, 0) == 0) wp = fs.wp; if (wp == NULL) diff --git a/cmd-select-pane.c b/cmd-select-pane.c index 91a36cddb..dbf147cb5 100644 --- a/cmd-select-pane.c +++ b/cmd-select-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-select-pane.c,v 1.76 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-select-pane.c,v 1.77 2026/07/17 12:42:51 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -150,13 +150,12 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) const struct cmd_entry *entry = cmd_get_entry(self); struct cmd_find_state *current = cmdq_get_current(item); struct cmd_find_state *target = cmdq_get_target(item); - struct client *c = cmdq_get_client(item); struct event_payload *ep; struct cmd_find_state fs; struct winlink *wl = target->wl; struct window *w = wl->window; struct session *s = target->s; - struct window_pane *wp = target->wp, *activewp, *lastwp; + struct window_pane *wp = target->wp, *lastwp; struct options *oo = wp->options; char *title; const char *style; @@ -267,18 +266,12 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); } - if (c != NULL && c->session != NULL && (c->flags & CLIENT_ACTIVEPANE)) - activewp = server_client_get_pane(c); - else - activewp = w->active; - if (wp == activewp) + if (wp == w->active) return (CMD_RETURN_NORMAL); if (window_push_zoom(w, 0, args_has(args, 'Z'))) server_redraw_window(w); window_redraw_active_switch(w, wp); - if (c != NULL && c->session != NULL && (c->flags & CLIENT_ACTIVEPANE)) - server_client_set_pane(c, wp); - else if (window_set_active_pane(w, wp, 1)) + 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); diff --git a/screen-redraw.c b/screen-redraw.c index 310974f56..6bf8dd8b8 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-redraw.c,v 1.152 2026/07/16 10:52:32 nicm Exp $ */ +/* $OpenBSD: screen-redraw.c,v 1.153 2026/07/17 12:42:51 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -1599,7 +1599,7 @@ redraw_set_draw_context(struct redraw_draw_ctx *dctx, if (server_is_marked(s, s->curw, marked_pane.wp)) dctx->marked = marked_pane.wp; - dctx->active = server_client_get_pane(c); + dctx->active = s->curw->window->active; lines = status_line_size(c); if (options_get_number(oo, "status-position") == 0) diff --git a/server-client.c b/server-client.c index b4f39c1d7..c64c57d2e 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.496 2026/07/17 08:37:29 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.497 2026/07/17 12:42:51 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -49,47 +49,12 @@ 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 void server_client_remove_active_pane_client(struct client *); static void server_client_dispatch(struct imsg *, void *); static int server_client_dispatch_command(struct client *, struct imsg *); static int server_client_dispatch_identify(struct client *, struct imsg *); static int server_client_dispatch_shell(struct client *); static void server_client_report_theme(struct client *, enum client_theme); -/* Client active pane. */ -struct client_active_pane { - struct client *client; - u_int window; - struct window_pane *pane; - - RB_ENTRY(client_active_pane) entry; -}; -RB_HEAD(client_active_panes, client_active_pane); - -/* Compare client active panes. */ -static int -server_client_active_pane_cmp(struct client_active_pane *cap1, - struct client_active_pane *cap2) -{ - uintptr_t c1 = (uintptr_t)cap1->client; - uintptr_t c2 = (uintptr_t)cap2->client; - - if (c1 < c2) - return (-1); - if (c1 > c2) - return (1); - if (cap1->window < cap2->window) - return (-1); - if (cap1->window > cap2->window) - return (1); - return (0); -} -RB_GENERATE_STATIC(client_active_panes, client_active_pane, entry, - server_client_active_pane_cmp); - -static struct client_active_panes client_active_panes = - RB_INITIALIZER(&client_active_panes); - /* Number of attached clients. */ u_int server_client_how_many(void) @@ -530,7 +495,6 @@ server_client_lost(struct client *c) server_client_clear_overlay(c); status_prompt_clear(c); status_message_clear(c); - server_client_remove_active_pane_client(c); RB_FOREACH_SAFE(cf, client_files, &c->files, cf1) { cf->error = EINTR; @@ -2142,7 +2106,7 @@ server_client_reset_state(struct client *c) { struct tty *tty = &c->tty; struct window *w = c->session->curw->window; - struct window_pane *wp = server_client_get_pane(c), *loop; + struct window_pane *wp = w->active, *loop; struct screen *s = NULL; struct options *oo = c->session->options; int mode = 0, cursor, flags, pane_mode = 0; @@ -3040,8 +3004,6 @@ server_client_set_flags(struct client *c, const char *flags) flag = CLIENT_READONLY; else if (strcmp(next, "ignore-size") == 0) flag = CLIENT_IGNORESIZE; - else if (strcmp(next, "active-pane") == 0) - flag = CLIENT_ACTIVEPANE; else if (strcmp(next, "no-detach-on-destroy") == 0) flag = CLIENT_NO_DETACH_ON_DESTROY; if (flag == 0) @@ -3090,8 +3052,6 @@ server_client_get_flags(struct client *c) } if (c->flags & CLIENT_READONLY) strlcat(s, "read-only,", sizeof s); - if (c->flags & CLIENT_ACTIVEPANE) - strlcat(s, "active-pane,", sizeof s); if (c->flags & CLIENT_SUSPENDED) strlcat(s, "suspended,", sizeof s); if (c->flags & CLIENT_UTF8) @@ -3101,97 +3061,11 @@ server_client_get_flags(struct client *c) return (s); } -/* Get client active pane. */ -static struct client_active_pane * -server_client_get_active_pane(struct client *c, u_int id) -{ - struct client_active_pane cap = { .client = c, .window = id }; - - return (RB_FIND(client_active_panes, &client_active_panes, &cap)); -} - -/* Add client active pane. */ -static struct client_active_pane * -server_client_add_active_pane(struct client *c, u_int id) -{ - struct client_active_pane *cap; - - cap = server_client_get_active_pane(c, id); - if (cap == NULL) { - cap = xcalloc(1, sizeof *cap); - cap->client = c; - cap->window = id; - RB_INSERT(client_active_panes, &client_active_panes, cap); - } - return (cap); -} - -/* Get client active pane. */ -struct window_pane * -server_client_get_pane(struct client *c) -{ - struct session *s = c->session; - struct window *w; - struct client_active_pane *cap; - - if (s == NULL) - return (NULL); - - w = s->curw->window; - if (w->modal != NULL) - return (w->modal); - if (~c->flags & CLIENT_ACTIVEPANE) - return (w->active); - cap = server_client_get_active_pane(c, w->id); - if (cap == NULL) - return (w->active); - return (cap->pane); -} - -/* Set client active pane. */ -void -server_client_set_pane(struct client *c, struct window_pane *wp) -{ - struct session *s = c->session; - struct client_active_pane *cap; - - if (s == NULL) - return; - if (wp->window->modal != NULL && wp != wp->window->modal) - return; - - cap = server_client_add_active_pane(c, s->curw->window->id); - cap->pane = wp; - log_debug("%s pane now %%%u", c->name, wp->id); -} - -/* Remove active pane entries for a client. */ -static void -server_client_remove_active_pane_client(struct client *c) -{ - struct client_active_pane *cap, *cap1; - - RB_FOREACH_SAFE(cap, client_active_panes, &client_active_panes, cap1) { - if (cap->client == c) { - RB_REMOVE(client_active_panes, &client_active_panes, cap); - free(cap); - } - } -} - /* Remove pane from client state. */ void server_client_remove_pane(struct window_pane *wp) { struct client *c; - struct client_active_pane *cap, *cap1; - - RB_FOREACH_SAFE(cap, client_active_panes, &client_active_panes, cap1) { - if (cap->pane == wp) { - RB_REMOVE(client_active_panes, &client_active_panes, cap); - free(cap); - } - } TAILQ_FOREACH(c, &clients, entry) { if (c->tty.mouse_last_pane == (int)wp->id) { @@ -3246,7 +3120,7 @@ server_client_print(struct client *c, int parse, struct evbuffer *evb) goto out; } - wp = server_client_get_pane(c); + wp = c->session->curw->window->active; wme = TAILQ_FIRST(&wp->modes); if (wme == NULL || wme->mode != &window_view_mode) window_pane_set_mode(wp, NULL, &window_view_mode, NULL, NULL, diff --git a/tmux.1 b/tmux.1 index 2d3087ea2..59339d365 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1142 2026/07/15 13:02:33 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1143 2026/07/17 12:42:51 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: July 15 2026 $ +.Dd $Mdocdate: July 17 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -1110,8 +1110,6 @@ detaching the client, typically causing it to exit. sets a comma-separated list of client flags. The flags are: .Bl -tag -width Ds -.It active\-pane -the client has an independent active pane .It ignore\-size the client does not affect the size of other clients .It no\-detach\-on\-destroy @@ -1141,13 +1139,6 @@ When a client is read-only, only keys bound to the or .Ic switch\-client commands have any effect. -A client with the -.Ar active\-pane -flag allows the active pane to be selected independently of the window's active -pane used by clients without the flag. -This only affects the cursor position and commands issued from the client; -other features such as hooks and styles continue to use the window's active -pane. .Pp If no server is started, .Ic attach\-session diff --git a/tmux.h b/tmux.h index 3d3cae5d8..47f8ffd85 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1407 2026/07/17 08:37:29 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1408 2026/07/17 12:42:51 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2216,7 +2216,7 @@ struct client { #define CLIENT_STARTSERVER 0x10000000 #define CLIENT_REDRAWMENU 0x20000000 #define CLIENT_NOFORK 0x40000000 -#define CLIENT_ACTIVEPANE 0x80000000ULL +/* 0x80000000ULL unused */ #define CLIENT_CONTROL_PAUSEAFTER 0x100000000ULL #define CLIENT_CONTROL_WAITEXIT 0x200000000ULL #define CLIENT_WINDOWSIZECHANGED 0x400000000ULL @@ -3250,8 +3250,6 @@ void server_client_loop(void); const char *server_client_get_cwd(struct client *, struct session *); void server_client_set_flags(struct client *, const char *); const char *server_client_get_flags(struct client *); -struct window_pane *server_client_get_pane(struct client *); -void server_client_set_pane(struct client *, struct window_pane *); void server_client_remove_pane(struct window_pane *); void server_client_print(struct client *, int, struct evbuffer *); diff --git a/tty.c b/tty.c index 66ca0d6cf..4f2c6a33e 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.476 2026/06/26 11:36:22 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.477 2026/07/17 12:42:51 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -955,7 +955,7 @@ tty_window_offset1(struct tty *tty, u_int *ox, u_int *oy, u_int *sx, u_int *sy) { struct client *c = tty->client; struct window *w = c->session->curw->window; - struct window_pane *wp = server_client_get_pane(c); + struct window_pane *wp = w->active; u_int cx, cy, lines; lines = status_line_size(c); diff --git a/window-border.c b/window-border.c index 0419015f4..5cd88c2bb 100644 --- a/window-border.c +++ b/window-border.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-border.c,v 1.1 2026/06/22 08:47:46 nicm Exp $ */ +/* $OpenBSD: window-border.c,v 1.2 2026/07/17 12:42:51 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -93,7 +93,7 @@ window_pane_get_border_style(struct window_pane *wp, struct client *c, struct grid_cell *saved; int *flag; - if (wp == server_client_get_pane(c)) { + if (wp == c->session->curw->window->active) { flag = &wp->active_border_gc_set; saved = &wp->active_border_gc; option = "pane-active-border-style"; From 481d48b53bb7d4ff0dae525e1aea39e627c1317e Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 17 Jul 2026 17:44:43 +0100 Subject: [PATCH 08/19] 3.8 changes. --- CHANGES | 162 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ tmux.1 | 2 +- 2 files changed, 163 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 7b1a2a54d..eaf7576da 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,165 @@ +CHANGES FROM 3.7b TO 3.8 + +* Many improvements to floating panes: + + - new-pane and split-window can now set the title with -T and border lines + with -B, and new-pane -W waits for the pane command to exit; + + - dragging with Ctrl now creates a new floating pane; + + - modal panes may be created with new-pane -O; a window can have one modal + pane and it prevents interaction with the other panes while it is active. + A modal pane is now used for the editor in buffer mode; + + - move-pane and resize-pane can now move and resize floating panes, including + mouse dragging; + + - split-window in a floating pane creates a new floating pane that does not + overlap the active pane, if possible; + + - break-pane can float a tiled pane, join-pane can tile a floating pane, and + swap-pane works with floating panes; + + - choose-tree -h and choose-client -h hide the pane containing the mode, and + -k for choose commands kills the pane when the mode exits, which is useful + for modes in floating panes; + + - pane-border-status has top-floating and bottom-floating, and there are new + default bindings under C-b g for common move and resize operations; + + - floating panes are unzoomed before creation to avoid a crash. + +* Add support for themes and improve default colours: + + - tmux now has builtin light and dark colour themes for terminals with 256 or + more colours; a new theme option controls whether tmux detects the terminal + theme, uses terminal colours, or forces light or dark (set to terminal for + the old colours); + + - styles can use new theme colour names such as themeblack, themewhite and + themegreen, and colours in styles are expanded as formats; + + - the terminal's own theme is reported to panes instead of being guessed from + the background, and inferred more accurately when theme reporting is not + supported (Ayman Bagabas, issue 5343); + + - tree-mode-selection-style has been added, tree mode selection styling now + covers the whole line, and built-in modes use improved theme colours. + +* Improvements to customize mode: + + - hooks and environment variables are now shown; + + - e opens the selected value in an editor; + + - C toggles showing only changed items; + + - array item keys can now be changed. + +* Hooks (and control mode notifications) are now handled internally by using + events, each of which may carry keys and values as a payload. The control + mode subscriptions mechanism has been extended to allow general-purpose + "monitors" which check a format every second and trigger a hook when it + changes or becomes true: + + - set-hook -B adds a monitor hook using the same subscription syntax as + refresh-client -B, and show-hooks -B lists them. -T runs the hook only when + the format is true; + + - wait-for -E waits for hooks, notifications or user events, and set-hook -E + fires an event immediately; + + - hooks have additional keys available. wait-for -v can be used to see the + keys sent with a hook event; + + - new hooks to cover areas including client create and destroy, pane + activity, pane mode and prompt changes, pane movement and resizing, session + group changes and window creation, close and zoom; + + - OSC 133 escape sequences now trigger events: pane-command-started, + pane-command-finished and pane-shell-prompt. + +* Add switch-mode, a fast switcher bound by default to Tab for windows and BTab + (S-Tab) for sessions. + +* Pane scrollbars now support auto-hide. This replaces the previous modal + behaviour where the pane was resized. With auto-hide, the scrollbar appears + while scrolling or while hovered and disappears after pane-scrollbars-timeout + (Michael Grant). + +* Change command templates so %% is escaped for single quotes while %1 remains + unquoted, preventing single quotes in session or target names from + terminating quoted command sections (reported by Aliz Hammond). + +* display-panes is now a mode rather than an overlay, and can be run inside + another pane. The -b flag has been removed. + +* Add new-window -E, respawn-pane -E and respawn-window -E as more convenient + methods to create an empty pane (rather than using '' for the command). + +* Menus now belong to the window, so appear on all clients. + +* Dragging over an existing copy mode selection now adjusts it (Michael Grant). + +* Rectangle selection in copy mode may extend past the end of the current line + to match vi's virtualedit=block behaviour (Mark Kelly, issue 5227). + +* With mode-keys set to vi, scrolling in copy mode now keeps the cursor in the + same position relative to the text (Arseniy Simonov, issue 5216). + +* Copy mode no longer exits at the bottom while a selection is in progress + (issue 5349). + +* Fix scrollbar initial state so scrollbars appear on new windows (issue 5339). + +* Add style attributes for dimming colours (dim=) and for hyperlinks (link= and + nolink) (issues 4842 and 4280 from Moritz Angermann). + +* New format modifiers: O: loops over options, V: loops over environments, + the I modifier reports client terminal information, t/d gives a time + difference in seconds, c/f and c/b emit colours, and the m operator supports + multiple terms and fuzzy matching. + +* Add or improve format variables including client_colours, + pane_start_command_list, window_manual_width, window_manual_height, + pane_last_output_time, pane_modal_flag and window_modal_pane. + +* Add additional pane sort orders, and a z sort order for choose-tree so + floating panes can be sorted by z-index. + +* Add -f filters to kill-pane -a, kill-window -a and kill-session -a (issue + 4782). + +* Allow server ACLs to use groups as well as users (issue 4917). + +* The mouse option now defaults to on. + +* Fix send-keys -K so keys are inserted in the correct place in the input + queue, like keys from key bindings (issue 3476). + +* Fix control mode clients hanging on exit if pty data was still queued (Ben + Maurer, issue 5356), and avoid sending notifications to clients which are + already exiting (Ben Maurer, issue 5357). + +* Fix grouped sessions sometimes being left as unusable command targets while + they are being killed (Bryce Miller, issue 5180). + +* Fix choose mode filtering when more than one pane exists (issue 5326), and + fix an infinite loop in customize mode when a filter does not match. + +* Fix regsub when the pattern is anchored at the start (issue 5341). + +* Fix redraw issues around synchronized updates, copy mode, tabs, padding, + status lines, menus, popups, wide characters and floating panes. This + includes fixes for flicker with alternate screen applications and scrollbars + (Michael Grant, issue 5351; Noam Stolero, issue 5350; Aung Myo Kyaw, issue + 5098). + +* Do not crash looking for the next or previous session (issue 5344), or when + no client is available. + +* Check time periodically in loops rather than for every item (issue 5367). + CHANGES FROM 3.7a TO 3.7b * Fix so that the end of a synchronized update again triggers a redraw. diff --git a/tmux.1 b/tmux.1 index c5d6cf86d..fe3951735 100644 --- a/tmux.1 +++ b/tmux.1 @@ -6600,7 +6600,7 @@ shows the subscriptions installed with .Sh MOUSE SUPPORT If the .Ic mouse -option is on (the default is off), +option is on, .Nm allows mouse events to be bound as keys. The name of each key is made up of a mouse event (such as From 377a8101683ac808ed53e18a5beb9d2a45564443 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 17 Jul 2026 15:24:30 +0000 Subject: [PATCH 09/19] Add some missing calls to invalidate scene. --- cmd-join-pane.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd-join-pane.c b/cmd-join-pane.c index df8b345d2..fd4355daa 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-join-pane.c,v 1.72 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: cmd-join-pane.c,v 1.73 2026/07/17 15:24:30 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -187,6 +187,7 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl, lc->g.yoff = yoff; layout_fix_panes(w, NULL); } + redraw_invalidate_scene(w); events_fire_window("window-layout-changed", w); server_redraw_window(w); @@ -359,6 +360,7 @@ cmd_join_pane_zindex(struct cmdq_item *item, struct winlink *wl, else TAILQ_INSERT_TAIL(&w->z_index, wp, zentry); + redraw_invalidate_scene(w); events_fire_window("window-layout-changed", w); server_redraw_window(w); @@ -398,6 +400,7 @@ cmd_join_pane_tile(struct cmdq_item *item, struct args *args, struct window *w, window_set_active_pane(w, wp, 1); layout_fix_offsets(w); layout_fix_panes(w, NULL); + redraw_invalidate_scene(w); events_fire_window("window-layout-changed", w); server_redraw_window(w); From 496bfdce3879ccf5d057edabdc388c854a1466a4 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 17 Jul 2026 16:03:15 +0000 Subject: [PATCH 10/19] Prevent display-panes from shifting the active pane scroll position by copying the screen from before zoom, also do not let resize consume blank lines. --- tmux.h | 3 ++- window-panes.c | 59 ++++++++++++++++++++++++++++++++++++++++---------- window.c | 13 +++++++++-- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/tmux.h b/tmux.h index 47f8ffd85..42469f406 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1408 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1409 2026/07/17 16:03:15 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1277,6 +1277,7 @@ struct window_pane { #define PANE_DESTROYED 0x10000 #define PANE_CMDRUNNING 0x20000 #define PANE_ACTIVITY 0x40000 +#define PANE_NORESIZETRIM 0x80000 bitstr_t *sync_dirty; u_int sync_dirty_size; diff --git a/window-panes.c b/window-panes.c index c262c82c3..059005aa5 100644 --- a/window-panes.c +++ b/window-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-panes.c,v 1.1 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window-panes.c,v 1.2 2026/07/17 16:03:15 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -57,6 +57,7 @@ struct window_panes_modedata { u_int source_session; u_int source_window; struct screen screen; + struct screen *preview; struct event timer; @@ -103,6 +104,24 @@ window_panes_get_source(struct window_panes_modedata *data, struct session **sp, return (1); } +static void +window_panes_set_preview(struct window_panes_modedata *data) +{ + struct window_pane *wp = data->wp; + struct screen *src = &wp->base, *dst; + struct screen_write_ctx ctx; + u_int sx = screen_size_x(src), sy = screen_size_y(src); + + data->preview = dst = xmalloc(sizeof *data->preview); + screen_init(dst, sx, sy, 0); + screen_write_start(&ctx, dst); + screen_write_fast_copy(&ctx, src, 0, src->grid->hsize, sx, sy); + screen_write_stop(&ctx); + dst->mode = src->mode; + dst->cx = src->cx; + dst->cy = src->cy; +} + static void window_panes_free_areas(struct window_panes_modedata *data) { @@ -379,7 +398,7 @@ window_panes_mark_pane_status_borders(u_char *map, struct window *w, } static int -window_panes_get_floating_frame(struct window_pane *wp, u_int osx, u_int osy, +window_panes_get_floating_borders(struct window_pane *wp, u_int osx, u_int osy, u_int dsx, u_int dsy, int *xp, int *yp, int *x2p, int *y2p) { struct layout_cell *lc; @@ -404,12 +423,12 @@ window_panes_get_floating_frame(struct window_pane *wp, u_int osx, u_int osy, } static int -window_panes_clip_floating_body(struct window_pane *wp, u_int osx, u_int osy, +window_panes_clip_floating_pane(struct window_pane *wp, u_int osx, u_int osy, u_int dsx, u_int dsy, u_int *xp, u_int *yp, u_int *sxp, u_int *syp) { int x, y, x2, y2, bx, by, bx2, by2; - if (!window_panes_get_floating_frame(wp, osx, osy, dsx, dsy, &x, &y, + if (!window_panes_get_floating_borders(wp, osx, osy, dsx, dsy, &x, &y, &x2, &y2)) return (1); @@ -601,7 +620,7 @@ window_panes_draw_floating_border(struct screen_write_ctx *ctx, if (dsx == 0 || dsy == 0) return; - if (!window_panes_get_floating_frame(wp, osx, osy, dsx, dsy, &x, &y, + if (!window_panes_get_floating_borders(wp, osx, osy, dsx, dsy, &x, &y, &x2, &y2)) return; @@ -634,7 +653,7 @@ window_panes_clear_floating_area(struct screen_write_ctx *ctx, struct grid_cell gc; int x, y, x2, y2, xx, yy; - if (!window_panes_get_floating_frame(wp, osx, osy, dsx, dsy, &x, &y, + if (!window_panes_get_floating_borders(wp, osx, osy, dsx, dsy, &x, &y, &x2, &y2)) return; @@ -778,6 +797,7 @@ window_panes_draw_pane(struct window_panes_modedata *data, struct screen_write_ctx *ctx, struct window_pane *wp, struct layout_cell *root, u_int osx, u_int osy, u_int dsx, u_int dsy) { + struct screen *s = &wp->base; u_int pane, x, y, sx, sy; if (!window_panes_pane_visible(wp)) @@ -785,7 +805,7 @@ window_panes_draw_pane(struct window_panes_modedata *data, if (!window_panes_get_geometry(wp, root, osx, osy, dsx, dsy, &x, &y, &sx, &sy)) return; - if (!window_panes_clip_floating_body(wp, osx, osy, dsx, dsy, &x, &y, + if (!window_panes_clip_floating_pane(wp, osx, osy, dsx, dsy, &x, &y, &sx, &sy)) return; if (window_pane_index(wp, &pane) != 0) @@ -793,11 +813,15 @@ window_panes_draw_pane(struct window_panes_modedata *data, window_panes_add_area(data, wp, x, y, sx, sy); screen_write_cursormove(ctx, x, y, 0); + if (data->preview != NULL && + wp == data->wp && + sx <= screen_size_x(data->preview) && + sy <= screen_size_y(data->preview)) + s = data->preview; if (osx <= dsx && osy <= dsy) - screen_write_fast_copy(ctx, &wp->base, 0, wp->base.grid->hsize, - sx, sy); + screen_write_fast_copy(ctx, s, 0, s->grid->hsize, sx, sy); else - screen_write_preview(ctx, &wp->base, sx, sy); + screen_write_preview(ctx, s, sx, sy); window_panes_draw_number(data, ctx, wp, pane, x, y, sx, sy); } @@ -917,6 +941,8 @@ window_panes_init(struct window_mode_entry *wme, struct cmdq_item *item, data->zoomed = -1; else { data->zoomed = (w->flags & WINDOW_ZOOMED); + if (!data->zoomed) + window_panes_set_preview(data); if (!data->zoomed && window_zoom(wp) == 0) server_redraw_window(w); } @@ -940,8 +966,10 @@ window_panes_free(struct window_mode_entry *wme) evtimer_del(&data->timer); - if (data->zoomed == 0) + if (data->zoomed == 0) { + wme->wp->flags |= PANE_NORESIZETRIM; server_unzoom_window(w); + } server_redraw_window(w); server_redraw_window_borders(w); server_status_window(w); @@ -949,6 +977,10 @@ window_panes_free(struct window_mode_entry *wme) if (data->state != NULL) args_make_commands_free(data->state); window_panes_free_areas(data); + if (data->preview != NULL) { + screen_free(data->preview); + free(data->preview); + } screen_free(&data->screen); free(data); } @@ -1060,8 +1092,11 @@ window_panes_key(struct window_mode_entry *wme, struct client *c, return; } - if (wp->window->flags & WINDOW_ZOOMED) + if (wp->window->flags & WINDOW_ZOOMED) { + if (data->zoomed == 0) + wp->flags |= PANE_NORESIZETRIM; window_unzoom(wp->window, 1); + } window_panes_run_command(data, c, target); window_pane_reset_mode(wp); } diff --git a/window.c b/window.c index e8fc60e38..193c7103a 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.364 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.365 2026/07/17 16:03:15 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1556,6 +1556,12 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) struct window_pane_resize *r; struct event_payload *ep; struct cmd_find_state fs; + int trim = 1; + + if (wp->flags & PANE_NORESIZETRIM) { + wp->flags &= ~PANE_NORESIZETRIM; + trim = 0; + } if (sx == wp->sx && sy == wp->sy) return; @@ -1573,7 +1579,10 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) wp->sy = sy; log_debug("%s: %%%u resize %ux%u", __func__, wp->id, sx, sy); - screen_resize(&wp->base, sx, sy, wp->base.saved_grid == NULL); + if (wp->base.saved_grid == NULL) + screen_resize_cursor(&wp->base, sx, sy, 1, trim, 1); + else + screen_resize_cursor(&wp->base, sx, sy, 0, trim, 1); wme = TAILQ_FIRST(&wp->modes); if (wme != NULL && wme->mode->resize != NULL) From 2a2bce3dd413274894c42caaaa59bd817a0c1ed2 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 19 Jul 2026 18:34:18 +0100 Subject: [PATCH 11/19] Fix tty-keys.sh test. --- regress/tty-keys.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regress/tty-keys.sh b/regress/tty-keys.sh index 1b300b995..19b1623dd 100644 --- a/regress/tty-keys.sh +++ b/regress/tty-keys.sh @@ -21,7 +21,7 @@ exit_status=0 format_string () { case $1 in *\') - printf '"%%%%"' + printf '"%%%%%%"' ;; *) printf "'%%%%'" From 6ef280fb0f138b3830fbfedbdff4736b2da07707 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 19 Jul 2026 17:25:38 +0000 Subject: [PATCH 12/19] Guard x as well as y to avoid overflow, GitHub issue 5409 from Park Seongjae. --- window-visible.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/window-visible.c b/window-visible.c index 360e4fc88..a49bfa2ef 100644 --- a/window-visible.c +++ b/window-visible.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-visible.c,v 1.4 2026/06/29 19:03:34 nicm Exp $ */ +/* $OpenBSD: window-visible.c,v 1.5 2026/07/19 17:25:38 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -81,7 +81,7 @@ window_visible_ranges(struct window_pane *base_wp, int px, int py, u_int width, } w = base_wp->window; - if ((u_int)py >= w->sy) + if ((u_int)py >= w->sy || (u_int)px >= w->sx) goto empty; if (px + width > w->sx) width = w->sx - px; From c766669b3b01d24b2fd60e6a2c4b316910abff77 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 19 Jul 2026 17:36:38 +0000 Subject: [PATCH 13/19] Add unzoomed width and height formats and use them for the sizes shown in panes mode, reported by tb@. --- format.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++++- options-table.c | 5 ++-- tmux.1 | 6 ++-- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/format.c b/format.c index 9354b5a24..233626e81 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.405 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.406 2026/07/19 17:36:38 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -2564,6 +2564,75 @@ format_cb_pane_tty(struct format_tree *ft) return (NULL); } +/* Callback for pane_unzoomed_height. */ +static void * +format_cb_pane_unzoomed_height(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + struct window *w; + struct layout_cell *lc, *root; + int status; + u_int sy; + + if (wp == NULL) + return (NULL); + w = wp->window; + + lc = wp->saved_layout_cell; + if (lc == NULL) + lc = wp->layout_cell; + if (lc == NULL) + return (NULL); + sy = lc->g.sy; + + root = w->saved_layout_root; + if (root == NULL) + root = w->layout_root; + status = window_pane_get_pane_status(wp); + if (!window_pane_is_floating(wp) && + root != NULL && + layout_add_horizontal_border(root, lc, status) && + sy > 1) + sy--; + + return (format_printf("%u", sy)); +} + +/* Callback for pane_unzoomed_width. */ +static void * +format_cb_pane_unzoomed_width(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + struct layout_cell *lc; + int sb_w, sb_pad; + u_int sx; + + if (wp == NULL) + return (NULL); + + lc = wp->saved_layout_cell; + if (lc == NULL) + lc = wp->layout_cell; + if (lc == NULL) + return (NULL); + sx = lc->g.sx; + + if (window_pane_scrollbar_reserve(wp)) { + sb_w = wp->scrollbar_style.width; + sb_pad = wp->scrollbar_style.pad; + if (sb_w < 1) + sb_w = 1; + if (sb_pad < 0) + sb_pad = 0; + if ((int)sx - sb_w - sb_pad < PANE_MINIMUM) + sx = PANE_MINIMUM; + else + sx -= sb_w + sb_pad; + } + + return (format_printf("%u", sx)); +} + /* Callback for pane_width. */ static void * format_cb_pane_width(struct format_tree *ft) @@ -3746,6 +3815,12 @@ static const struct format_table_entry format_table[] = { { "pane_unseen_changes", FORMAT_TABLE_STRING, format_cb_pane_unseen_changes }, + { "pane_unzoomed_height", FORMAT_TABLE_STRING, + format_cb_pane_unzoomed_height + }, + { "pane_unzoomed_width", FORMAT_TABLE_STRING, + format_cb_pane_unzoomed_width + }, { "pane_width", FORMAT_TABLE_STRING, format_cb_pane_width }, diff --git a/options-table.c b/options-table.c index dabf53934..82ae2e5ff 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.238 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.239 2026/07/19 17:36:38 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1349,7 +1349,8 @@ const struct options_table_entry options_table[] = { { .name = "display-panes-format", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW, - .default_str = "#[align=right]#{pane_width}x#{pane_height}", + .default_str = "#[align=right]" + "#{pane_unzoomed_width}x#{pane_unzoomed_height}", .text = "Format of text shown by 'display-panes', expanded for each " "pane." }, diff --git a/tmux.1 b/tmux.1 index 59339d365..8d433131b 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1143 2026/07/17 12:42:51 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1144 2026/07/19 17:36:38 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: July 17 2026 $ +.Dd $Mdocdate: July 19 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -7333,6 +7333,8 @@ The following variables are available, where appropriate: .It Li "pane_top" Ta "" Ta "Top of pane" .It Li "pane_tty" Ta "" Ta "Pseudo terminal of pane" .It Li "pane_unseen_changes" Ta "" Ta "1 if there were changes in pane while in mode" +.It Li "pane_unzoomed_height" Ta "" Ta "Height of pane when not zoomed" +.It Li "pane_unzoomed_width" Ta "" Ta "Width of pane when not zoomed" .It Li "pane_width" Ta "" Ta "Width of pane" .It Li "pane_x" Ta "" Ta "X position of pane" .It Li "pane_y" Ta "" Ta "Y position of pane" From c750c014b2054c059d20024d8fa2ad474c0caa15 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 19 Jul 2026 20:58:20 +0100 Subject: [PATCH 14/19] Extend test. --- regress/display-panes.sh | 53 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/regress/display-panes.sh b/regress/display-panes.sh index 83d21848b..2d3faccd5 100644 --- a/regress/display-panes.sh +++ b/regress/display-panes.sh @@ -73,11 +73,14 @@ $TMUX new-session -d -s m -x 80 -y 24 'cat' || exit 1 $TMUX split-window -t m:0 'cat' || exit 1 p0=$($TMUX display-message -p -t m:0.0 '#{pane_id}') p1=$($TMUX display-message -p -t m:0.1 '#{pane_id}') -$TMUX set -g display-panes-format 'P#{pane_index}' || +$TMUX set -g display-panes-format \ + 'P#{pane_index}:#{pane_unzoomed_width}x#{pane_unzoomed_height}' || fail "set display-panes-format failed" $TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t m" || exit 1 wait_clients 1 +p0_size=$($TMUX display-message -p -t "$p0" '#{pane_width}x#{pane_height}') +p1_size=$($TMUX display-message -p -t "$p1" '#{pane_width}x#{pane_height}') capture() { @@ -100,11 +103,38 @@ wait_capture() $TMUX display-panes -d 0 -t "$p0" || fail "display-panes failed" wait_format "$p0" '#{pane_mode}' 'panes-mode' wait_format "$p0" '#{window_zoomed_flag}' '1' -wait_capture 'P0' -wait_capture 'P1' +wait_format "$p0" '#{pane_unzoomed_width}x#{pane_unzoomed_height}' \ + "$p0_size" +wait_capture "P0:$p0_size" +wait_capture "P1:$p1_size" $TMUX send-keys -t "$p0" q || fail "exit panes mode failed" wait_format "$p0" '#{pane_in_mode}' '0' wait_format "$p0" '#{window_zoomed_flag}' '0' +$TMUX set -g display-panes-format 'P#{pane_index}' || + fail "reset display-panes-format failed" + +# Unzoomed sizes match the visible pane size when scrollbars reserve space. +$TMUX set -w -t m:0 pane-scrollbars on || + fail "set pane-scrollbars failed" +$TMUX set -w -t m:0 pane-scrollbars-style "width=2,pad=1" || + fail "set pane-scrollbars-style failed" +p0_size=$($TMUX display-message -p -t "$p0" '#{pane_width}x#{pane_height}') +p1_size=$($TMUX display-message -p -t "$p1" '#{pane_width}x#{pane_height}') +$TMUX set -g display-panes-format \ + 'S#{pane_index}:#{pane_unzoomed_width}x#{pane_unzoomed_height}' || + fail "set scrollbar display-panes-format failed" +$TMUX display-panes -d 0 -t "$p0" || fail "display-panes scrollbar failed" +wait_format "$p0" '#{window_zoomed_flag}' '1' +wait_format "$p0" '#{pane_unzoomed_width}x#{pane_unzoomed_height}' \ + "$p0_size" +wait_capture "S0:$p0_size" +wait_capture "S1:$p1_size" +$TMUX send-keys -t "$p0" q || fail "exit scrollbar panes mode failed" +wait_format "$p0" '#{pane_in_mode}' '0' +$TMUX set -w -t m:0 pane-scrollbars off || + fail "reset pane-scrollbars failed" +$TMUX set -g display-panes-format 'P#{pane_index}' || + fail "reset display-panes-format after scrollbars failed" # -Z starts unzoomed. $TMUX display-panes -Zd 0 -t "$p0" || fail "display-panes -Z failed" @@ -192,13 +222,21 @@ $TMUX set -g display-panes-format 'P#{pane_index}' || # pane content into the status row. $TMUX set -w -t m:0 pane-border-status top || fail "set pane-border-status failed" -$TMUX set -g display-panes-format '' || fail "clear display-panes-format failed" +status_p0_size=$($TMUX display-message -p -t "$p0" \ + '#{pane_width}x#{pane_height}') +status_p1_size=$($TMUX display-message -p -t "$p1" \ + '#{pane_width}x#{pane_height}') +$TMUX set -g display-panes-format \ + '#[align=right]T#{pane_index}:#{pane_unzoomed_width}x#{pane_unzoomed_height}' || + fail "set status display-panes-format failed" $TMUX respawn-pane -k -t "$p0" 'printf "STATUS-TOP\n"; exec cat' || fail "write status marker failed" wait_capture 'STATUS-TOP' $TMUX display-panes -d 0 -t "$p0" || fail "display-panes status failed" wait_format "$p0" '#{pane_mode}' 'panes-mode' wait_capture 'STATUS-TOP' +wait_capture "T0:$status_p0_size" +wait_capture "T1:$status_p1_size" CAPTURED=$(capture) first=$(printf '%s\n' "$CAPTURED" | sed -n '1p') second=$(printf '%s\n' "$CAPTURED" | sed -n '2p') @@ -230,13 +268,18 @@ wait_format "$fp" '#{pane_mode}' 'panes-mode' wait_format "$fp" '#{window_zoomed_flag}' '1' wait_capture '┌' wait_capture '┘' +fp_size=$($TMUX display-message -p -t "$fp" \ + '#{pane_unzoomed_width}x#{pane_unzoomed_height}') +[ "$fp_size" = "30x8" ] || + fail "expected floating unzoomed size 30x8, got $fp_size" $TMUX send-keys -t "$fp" q || fail "exit zoomed floating panes mode failed" wait_format "$fp" '#{pane_in_mode}' '0' wait_format "$fp" '#{window_zoomed_flag}' '0' $TMUX resize-pane -t "$fp" -x 48 -y 14 || fail "resize floating pane larger failed" -$TMUX set -g display-panes-format '#[align=right]FP#{pane_width}x#{pane_height}' || +$TMUX set -g display-panes-format \ + '#[align=right]FP#{pane_unzoomed_width}x#{pane_unzoomed_height}' || fail "set floating display-panes-format failed" $TMUX display-panes -Zd 0 -t "$fp" || fail "display-panes floating format failed" wait_format "$fp" '#{pane_mode}' 'panes-mode' From 8024fab1f2365b86a52f7cc2881790033d33344c Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 19 Jul 2026 19:09:30 +0000 Subject: [PATCH 15/19] Copy the home directory if it comes from getpwuid. --- tmux.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tmux.c b/tmux.c index 8a90c3ff9..822e9efd4 100644 --- a/tmux.c +++ b/tmux.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.c,v 1.221 2026/06/29 18:17:28 nicm Exp $ */ +/* $OpenBSD: tmux.c,v 1.222 2026/07/19 19:09:30 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -360,7 +360,7 @@ find_home(void) if (home == NULL || *home == '\0') { pw = getpwuid(getuid()); if (pw != NULL) - home = pw->pw_dir; + home = xstrdup(pw->pw_dir); else home = NULL; } From aea209b1423d2cd6d6b2cf958c517ad1c13ff9f1 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 19 Jul 2026 19:53:11 +0000 Subject: [PATCH 16/19] Remove bits to disable removing empty lines since actually we want to do that - zoom added them in the first place. --- tmux.h | 3 +-- window-panes.c | 11 +++-------- window.c | 13 ++----------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/tmux.h b/tmux.h index 42469f406..a9739285c 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1409 2026/07/17 16:03:15 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1410 2026/07/19 19:53:11 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1277,7 +1277,6 @@ struct window_pane { #define PANE_DESTROYED 0x10000 #define PANE_CMDRUNNING 0x20000 #define PANE_ACTIVITY 0x40000 -#define PANE_NORESIZETRIM 0x80000 bitstr_t *sync_dirty; u_int sync_dirty_size; diff --git a/window-panes.c b/window-panes.c index 059005aa5..981121460 100644 --- a/window-panes.c +++ b/window-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-panes.c,v 1.2 2026/07/17 16:03:15 nicm Exp $ */ +/* $OpenBSD: window-panes.c,v 1.3 2026/07/19 19:53:11 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -966,10 +966,8 @@ window_panes_free(struct window_mode_entry *wme) evtimer_del(&data->timer); - if (data->zoomed == 0) { - wme->wp->flags |= PANE_NORESIZETRIM; + if (data->zoomed == 0) server_unzoom_window(w); - } server_redraw_window(w); server_redraw_window_borders(w); server_status_window(w); @@ -1092,11 +1090,8 @@ window_panes_key(struct window_mode_entry *wme, struct client *c, return; } - if (wp->window->flags & WINDOW_ZOOMED) { - if (data->zoomed == 0) - wp->flags |= PANE_NORESIZETRIM; + if (wp->window->flags & WINDOW_ZOOMED) window_unzoom(wp->window, 1); - } window_panes_run_command(data, c, target); window_pane_reset_mode(wp); } diff --git a/window.c b/window.c index 193c7103a..5f06c1a14 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.365 2026/07/17 16:03:15 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.366 2026/07/19 19:53:11 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1556,12 +1556,6 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) struct window_pane_resize *r; struct event_payload *ep; struct cmd_find_state fs; - int trim = 1; - - if (wp->flags & PANE_NORESIZETRIM) { - wp->flags &= ~PANE_NORESIZETRIM; - trim = 0; - } if (sx == wp->sx && sy == wp->sy) return; @@ -1579,10 +1573,7 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) wp->sy = sy; log_debug("%s: %%%u resize %ux%u", __func__, wp->id, sx, sy); - if (wp->base.saved_grid == NULL) - screen_resize_cursor(&wp->base, sx, sy, 1, trim, 1); - else - screen_resize_cursor(&wp->base, sx, sy, 0, trim, 1); + screen_resize(&wp->base, sx, sy, wp->base.saved_grid == NULL); wme = TAILQ_FIRST(&wp->modes); if (wme != NULL && wme->mode->resize != NULL) From efcfc555356b9d02c4ee6899554831e316e900d9 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 20 Jul 2026 07:42:13 +0000 Subject: [PATCH 17/19] Correctly include status line in pane height. --- format.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/format.c b/format.c index 233626e81..a3b1acfa7 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.406 2026/07/19 17:36:38 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.407 2026/07/20 07:42:13 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -2571,7 +2571,7 @@ format_cb_pane_unzoomed_height(struct format_tree *ft) struct window_pane *wp = ft->wp; struct window *w; struct layout_cell *lc, *root; - int status; + int status, floating; u_int sy; if (wp == NULL) @@ -2584,12 +2584,16 @@ format_cb_pane_unzoomed_height(struct format_tree *ft) if (lc == NULL) return (NULL); sy = lc->g.sy; + floating = (lc->flags & LAYOUT_CELL_FLOATING); root = w->saved_layout_root; if (root == NULL) root = w->layout_root; - status = window_pane_get_pane_status(wp); - if (!window_pane_is_floating(wp) && + if (lc == wp->saved_layout_cell && !floating) + status = window_get_pane_status(w); + else + status = window_pane_get_pane_status(wp); + if (!floating && root != NULL && layout_add_horizontal_border(root, lc, status) && sy > 1) From d13b69ece4eda6fba458711f514db9dd4dfe326c Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 20 Jul 2026 11:16:33 +0000 Subject: [PATCH 18/19] Change cellused/size to 16 bits and time to 32 bits in grid_line and add the OSC 133 positions (size stays the same). --- cmd-capture-pane.c | 20 ++++++-- grid.c | 31 +++++++++++-- input.c | 113 +++++++++++++++++++++++++++++++++------------ screen-write.c | 7 ++- tmux.h | 38 +++++++++++---- window-copy.c | 6 ++- 6 files changed, 166 insertions(+), 49 deletions(-) diff --git a/cmd-capture-pane.c b/cmd-capture-pane.c index d83590f90..baf6fc62d 100644 --- a/cmd-capture-pane.c +++ b/cmd-capture-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-capture-pane.c,v 1.67 2026/07/02 10:34:14 nicm Exp $ */ +/* $OpenBSD: cmd-capture-pane.c,v 1.68 2026/07/20 11:16:33 nicm Exp $ */ /* * Copyright (c) 2009 Jonathan Alvarado @@ -135,6 +135,7 @@ cmd_capture_pane_grid(struct window_pane *wp, size_t *len) struct screen *s = &wp->base; struct grid *gd = s->grid; struct grid_line *gl; + struct osc133_data *od; char *buf = xstrdup(""), *line; char p[11]; u_int yy, xx, total = gd->hsize + gd->sy; @@ -150,9 +151,20 @@ cmd_capture_pane_grid(struct window_pane *wp, size_t *len) snprintf(p, sizeof p, "-"); else snprintf(p, sizeof p, "%u", yy - gd->hsize); - xasprintf(&line, "\tL %u (%s) flags=%s[%x] %u/%u\n", yy, - p, grid_line_flags_string(gl->flags), gl->flags, - gl->cellused, gl->cellsize); + od = &gl->osc133_data; + if (gl->flags & GRID_LINE_OSC133_FLAGS) { + xasprintf(&line, "\tL %u (%s) flags=%s[%x] " + "%u/%u osc133=%u,%u,%u,%u,%u\n", yy, p, + grid_line_flags_string(gl->flags), gl->flags, + gl->cellused, gl->cellsize, od->prompt_col, + od->cmd_col, od->out_start_col, + od->out_end_col, od->exit_status); + } else { + xasprintf(&line, "\tL %u (%s) flags=%s[%x] " + "%u/%u\n", yy, p, + grid_line_flags_string(gl->flags), gl->flags, + gl->cellused, gl->cellsize); + } buf = cmd_capture_pane_append(buf, len, line, strlen(line)); free(line); diff --git a/grid.c b/grid.c index 878c01bab..fa0c94ffe 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.153 2026/07/02 08:51:05 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.154 2026/07/20 11:16:33 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -194,6 +194,25 @@ grid_get_line(struct grid *gd, u_int line) return (&gd->linedata[line]); } +/* Get line time. */ +time_t +grid_line_time(const struct grid_line *gl) +{ + if (gl->time == 0) + return (0); + return (start_time.tv_sec + gl->time - 1); +} + +/* Set line time. */ +static void +grid_line_set_time(struct grid_line *gl) +{ + if (current_time == 0) + gl->time = 0; + else + gl->time = current_time - start_time.tv_sec + 1; +} + /* Adjust number of lines. */ void grid_adjust_lines(struct grid *gd, u_int lines) @@ -435,7 +454,7 @@ grid_scroll_history(struct grid *gd, u_int bg) gd->hscrolled++; grid_compact_line(&gd->linedata[gd->hsize]); - gd->linedata[gd->hsize].time = current_time; + grid_line_set_time(&gd->linedata[gd->hsize]); gd->hsize++; gd->scroll_added++; } @@ -477,7 +496,7 @@ grid_scroll_history_region(struct grid *gd, u_int upper, u_int lower, u_int bg) /* Move the line into the history. */ memcpy(gl_history, gl_upper, sizeof *gl_history); - gl_history->time = current_time; + grid_line_set_time(gl_history); /* Then move the region up and clear the bottom line. */ memmove(gl_upper, gl_upper + 1, (lower - upper) * sizeof *gl_upper); @@ -1650,8 +1669,14 @@ grid_line_flags_string(int flags) strlcat(s, "DEAD,", sizeof s); if (flags & GRID_LINE_START_PROMPT) strlcat(s, "START_PROMPT,", sizeof s); + if (flags & GRID_LINE_SECOND_PROMPT) + strlcat(s, "SECOND_PROMPT,", sizeof s); + if (flags & GRID_LINE_START_COMMAND) + strlcat(s, "START_COMMAND,", sizeof s); if (flags & GRID_LINE_START_OUTPUT) strlcat(s, "START_OUTPUT,", sizeof s); + if (flags & GRID_LINE_END_OUTPUT) + strlcat(s, "END_OUTPUT,", sizeof s); if (flags & GRID_LINE_HYPERLINK) strlcat(s, "HYPERLINK,", sizeof s); if (*s == '\0') diff --git a/input.c b/input.c index 66f65e01c..ecf07dd61 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.268 2026/07/13 15:03:03 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.269 2026/07/20 11:16:33 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -174,8 +174,6 @@ static void input_osc_110(struct input_ctx *, const char *); static void input_osc_111(struct input_ctx *, const char *); static void input_osc_112(struct input_ctx *, const char *); static void input_osc_133(struct input_ctx *, const char *); -static void input_fire_pane_title_changed(struct window_pane *, - const char *); /* Transition entry/exit handlers. */ static void input_clear(struct input_ctx *); @@ -212,21 +210,6 @@ static int input_end_bel(struct input_ctx *); /* Command table comparison function. */ static int input_table_compare(const void *, const void *); -static void -input_fire_pane_title_changed(struct window_pane *wp, const char *title) -{ - struct event_payload *ep; - struct cmd_find_state fs; - - ep = event_payload_create(); - cmd_find_from_pane(&fs, wp, 0); - event_payload_set_target(ep, &fs); - event_payload_set_pane(ep, "pane", wp); - event_payload_set_window(ep, "window", wp->window); - event_payload_set_string(ep, "new_title", "%s", title); - events_fire("pane-title-changed", ep); -} - /* Command table entry. */ struct input_table_entry { int ch; @@ -809,6 +792,22 @@ input_stop_utf8(struct input_ctx *ictx) ictx->utf8started = 0; } +/* Fire a title changed event. */ +static void +input_fire_pane_title_changed(struct window_pane *wp, const char *title) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_string(ep, "new_title", "%s", title); + events_fire("pane-title-changed", ep); +} + /* * Timer - if this expires then have been waiting for a terminator for too * long, so reset to ground. @@ -3155,6 +3154,35 @@ input_osc_112(struct input_ctx *ictx, const char *p) screen_set_cursor_colour(ictx->ctx.s, -1); } +/* Parse the OSC 133 D exit status. */ +static int +input_osc_133_exit_status(const char *p) +{ + const char *end; + char *copy; + const char *errstr; + long long status; + + if (p[1] != ';' || p[2] == '\0' || strchr(p + 2, '=') == p + 2) + return (0); + end = strchr(p + 2, ';'); + if (end == p + 2) + return (0); + if (end == NULL) + copy = xstrdup(p + 2); + else + copy = xstrndup(p + 2, end - (p + 2)); + if (strchr(copy, '=') != NULL) { + free(copy); + return (0); + } + status = strtonum(copy, 0, 255, &errstr); + free(copy); + if (errstr != NULL) + return (255); + return (status); +} + /* Fire an OSC 133 command event. */ static void input_fire_command_event(struct window_pane *wp, const char *name) @@ -3200,27 +3228,51 @@ static void input_osc_133(struct input_ctx *ictx, const char *p) { struct window_pane *wp = ictx->wp; - struct grid *gd = ictx->ctx.s->grid; - u_int line = ictx->ctx.s->cy + gd->hsize; + struct screen *s = ictx->ctx.s; + struct grid *gd = s->grid; + u_int line = s->cy + gd->hsize; struct grid_line *gl = NULL; - const char *errstr; + const char *cp; int status; - if (line <= gd->hsize + gd->sy - 1) + if (line < gd->hsize + gd->sy) gl = grid_get_line(gd, line); switch (*p) { case 'A': - if (gl != NULL) + case 'N': + if (gl != NULL) { + memset(&gl->osc133_data, 0, sizeof gl->osc133_data); + gl->osc133_data.prompt_col = s->cx; gl->flags |= GRID_LINE_START_PROMPT; + } if (wp != NULL) { wp->last_prompt_time = time(NULL); events_fire_pane("pane-shell-prompt", wp); } break; + case 'P': + if (gl != NULL) { + cp = strstr(p, ";k=s"); + if (cp != NULL && (cp[4] == ';' || cp[4] == '\0')) + gl->flags |= GRID_LINE_SECOND_PROMPT; + else + gl->flags |= GRID_LINE_START_PROMPT; + gl->osc133_data.prompt_col = s->cx; + } + break; + case 'B': + case 'I': + if (gl != NULL) { + gl->flags |= GRID_LINE_START_COMMAND; + gl->osc133_data.cmd_col = s->cx; + } + break; case 'C': - if (gl != NULL) + if (gl != NULL) { gl->flags |= GRID_LINE_START_OUTPUT; + gl->osc133_data.out_start_col = s->cx; + } if (wp != NULL) { wp->cmd_start_time = time(NULL); wp->cmd_end_time = 0; @@ -3230,17 +3282,18 @@ input_osc_133(struct input_ctx *ictx, const char *p) } break; case 'D': + status = input_osc_133_exit_status(p); if (wp != NULL) { wp->cmd_end_time = time(NULL); wp->flags &= ~PANE_CMDRUNNING; - wp->cmd_status = -1; - if (p[1] == ';' && p[2] != '\0') { - status = strtonum(p + 2, 0, INT_MAX, &errstr); - if (errstr == NULL) - wp->cmd_status = status; - } + wp->cmd_status = status; input_fire_command_event(wp, "pane-command-finished"); } + if (gl != NULL) { + gl->flags |= GRID_LINE_END_OUTPUT; + gl->osc133_data.out_end_col = s->cx; + gl->osc133_data.exit_status = status; + } break; } } diff --git a/screen-write.c b/screen-write.c index a7a16149c..1ba5bed70 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.283 2026/07/09 07:35:05 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.284 2026/07/20 11:16:33 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1576,16 +1576,19 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg) struct grid_line *gl; u_int sx = screen_size_x(s); struct screen_write_citem *ci = ctx->item; + struct osc133_data od; u_int flags; gl = grid_get_line(s->grid, s->grid->hsize + s->cy); if (gl->cellsize == 0 && COLOUR_DEFAULT(bg)) return; - flags = gl->flags & (GRID_LINE_START_PROMPT|GRID_LINE_START_OUTPUT); + flags = gl->flags & GRID_LINE_OSC133_FLAGS; + memcpy(&od, &gl->osc133_data, sizeof od); grid_view_clear(s->grid, 0, s->cy, sx, 1, bg); gl = grid_get_line(s->grid, s->grid->hsize + s->cy); gl->flags |= flags; + memcpy(&gl->osc133_data, &od, sizeof gl->osc133_data); screen_write_collect_clear(ctx, s->cy, 1); ci->x = 0; diff --git a/tmux.h b/tmux.h index a9739285c..597d169b7 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1410 2026/07/19 19:53:11 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1411 2026/07/20 11:16:33 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -800,8 +800,19 @@ struct colour_palette { #define GRID_LINE_EXTENDED 0x2 #define GRID_LINE_DEAD 0x4 #define GRID_LINE_START_PROMPT 0x8 -#define GRID_LINE_START_OUTPUT 0x10 -#define GRID_LINE_HYPERLINK 0x20 +#define GRID_LINE_SECOND_PROMPT 0x10 +#define GRID_LINE_START_COMMAND 0x20 +#define GRID_LINE_START_OUTPUT 0x40 +#define GRID_LINE_END_OUTPUT 0x80 +#define GRID_LINE_HYPERLINK 0x100 + +/* All OSC 133 flags. */ +#define GRID_LINE_OSC133_FLAGS \ + (GRID_LINE_START_PROMPT| \ + GRID_LINE_SECOND_PROMPT| \ + GRID_LINE_START_COMMAND| \ + GRID_LINE_START_OUTPUT| \ + GRID_LINE_END_OUTPUT) /* Grid string flags. */ #define GRID_STRING_WITH_SEQUENCES 0x1 @@ -870,17 +881,27 @@ struct grid_cell_entry { u_char flags; } __packed; +/* OSC 133 data for a grid line. */ +struct osc133_data { + u_short prompt_col; + u_short cmd_col; + u_short out_start_col; + u_short out_end_col; + u_char exit_status; +}; + /* Grid line. */ struct grid_line { struct grid_cell_entry *celldata; - u_int cellused; - u_int cellsize; - struct grid_extd_entry *extddata; + + u_short cellused; + u_short cellsize; u_int extdsize; - int flags; - time_t time; + u_int time; + struct osc133_data osc133_data; + u_short flags; }; /* Entire grid of cells. */ @@ -3405,6 +3426,7 @@ int grid_compare(struct grid *, struct grid *); const char *grid_line_flags_string(int); const char *grid_cell_flags_string(int); const char *grid_cell_attr_string(int); +time_t grid_line_time(const struct grid_line *); void grid_collect_history(struct grid *, int); void grid_remove_history(struct grid *, u_int ); void grid_scroll_history(struct grid *, u_int); diff --git a/window-copy.c b/window-copy.c index a6909a8d4..4a8009b14 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.421 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.422 2026/07/20 11:16:33 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1107,9 +1107,11 @@ window_copy_formats(struct window_mode_entry *wme, struct format_tree *ft) u_int hsize = screen_hsize(data->backing); u_int position, limit; struct grid_line *gl; + time_t t; gl = grid_get_line(data->backing->grid, hsize - data->oy); - format_add(ft, "top_line_time", "%llu", (unsigned long long)gl->time); + t = grid_line_time(gl); + format_add(ft, "top_line_time", "%llu", (unsigned long long)t); format_add(ft, "scroll_position", "%d", data->oy); if (window_copy_line_number_is_absolute(wme)) { From 8aaae25e56a3498c13149f5581808b8b65143bad Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 21 Jul 2026 08:00:39 +0100 Subject: [PATCH 19/19] Fix a merge and add test bits. --- regress/input-modes.sh | 19 ++++++++++++++----- screen-write.c | 4 +--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/regress/input-modes.sh b/regress/input-modes.sh index 9bbe44429..8ae905f89 100644 --- a/regress/input-modes.sh +++ b/regress/input-modes.sh @@ -5,11 +5,20 @@ start_pane alternate 10 3 'MAIN\033[?1049hALT\033[?1049lZ\n' check_capture alternate 'MAINZ' -start_pane osc133 10 4 '\033]133;A\007prompt\n\033]133;C\007output\n' -check_capture osc133 'prompt -output' -check_flags osc133 'P prompt -O output' +start_pane osc133 20 8 'xx\033]133;A\007p>\033]133;B\007cmd\nxy\033]133;P;k=s\007more\nzz\033]133;C\007out\033]133;D;7\007\nq\033]133;C\007bad\033]133;D;-1\007\nqq\033]133;C\007big\033]133;D;300\007\nzzz\033]133;C\007ok\033]133;D\007\n' +check_capture osc133 'xxp>cmd +xymore +zzout +qbad +qqbig +zzzok' +check_raw_matches osc133 \ + 'L 0 \(0\) flags=START_PROMPT,START_COMMAND\[[0-9a-f]+\].* osc133=2,4,0,0,0' \ + 'L 1 \(1\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \ + 'L 2 \(2\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,2,5,7' \ + 'L 3 \(3\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,1,4,255' \ + 'L 4 \(4\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,2,5,255' \ + 'L 5 \(5\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,3,5,0' $TMUX kill-server 2>/dev/null exit $exit_status diff --git a/screen-write.c b/screen-write.c index 8cb6d338c..1b888849f 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1618,9 +1618,7 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg) ctx->wp->flags |= PANE_REDRAW; #endif - flags = gl->flags & - (GRID_LINE_START_PROMPT|GRID_LINE_START_OUTPUT| - GRID_LINE_OSC133_FLAGS); + flags = gl->flags & GRID_LINE_OSC133_FLAGS; memcpy(&od, &gl->osc133_data, sizeof od); grid_view_clear(s->grid, 0, s->cy, sx, 1, bg); gl = grid_get_line(s->grid, s->grid->hsize + s->cy);