From 6ebef7450ab2835bbf9a1c7c4acf3cec28976be5 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 2 Jul 2026 21:40:05 +0000 Subject: [PATCH 001/127] Do not double free or leak pane on failure, from Uzair Aftab in GitHub issue 5316. --- cmd-split-window.c | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/cmd-split-window.c b/cmd-split-window.c index 6c2c61252..276138c19 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -82,7 +82,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) struct session *s = target->s; struct winlink *wl = target->wl; struct window *w = wl->window; - struct window_pane *wp = target->wp, *new_wp; + struct window_pane *wp = target->wp, *new_wp = NULL; struct layout_cell *lc = NULL; struct cmd_find_state fs; int input, empty, is_floating, flags = 0; @@ -168,6 +168,11 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if ((new_wp = spawn_pane(&sc, &cause)) == NULL) { cmdq_error(item, "create pane failed: %s", cause); free(cause); + /* + * spawn_pane has already torn the half-built pane down (its + * fork-failure path removes the pane and destroys the layout + * cell), so new_wp is NULL and there is nothing for fail to do. + */ goto fail; } @@ -220,10 +225,6 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (input) { switch (window_pane_start_input(new_wp, item, &cause)) { case -1: - server_client_remove_pane(new_wp); - if (!is_floating) - layout_close_pane(new_wp); - window_remove_pane(wp->window, new_wp); cmdq_error(item, "%s", cause); free(cause); goto fail; @@ -270,10 +271,20 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); fail: + /* + * If the pane was spawned before we failed, tear it down here; this + * also destroys its layout cell. spawn_pane's own failure path has + * already done this, so new_wp is NULL in that case. + */ + if (new_wp != NULL) { + server_client_remove_pane(new_wp); + if (!is_floating) + layout_close_pane(new_wp); + window_remove_pane(wp->window, new_wp); + } if (sc.argv != NULL) cmd_free_argv(sc.argc, sc.argv); environ_free(sc.environ); - layout_destroy_cell(w, lc, &w->layout_root); return (CMD_RETURN_ERROR); From 93039db58c6e3c5897e655edc319fafa3480dc5d Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 2 Jul 2026 21:45:36 +0000 Subject: [PATCH 002/127] Use message-style as default for message-format again. GitHub issue 5315. --- status.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/status.c b/status.c index bb0c3bef9..0d9fc3e4e 100644 --- a/status.c +++ b/status.c @@ -488,7 +488,7 @@ status_message_redraw(struct client *c) status_message_area(c, &ax, &aw); ft = format_create_defaults(NULL, c, NULL, NULL, NULL); - memcpy(&gc, &grid_default_cell, sizeof gc); + style_apply(&gc, s->options, "message-style", ft); /* * Set #{message} in the format tree. If styles should be ignored in From fa2a3b679ad4267031549b159bb70932afc873fc Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 2 Jul 2026 22:55:57 +0100 Subject: [PATCH 003/127] Use ASAN in tests. --- .github/workflows/regress.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 883ee720e..6282943b0 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -25,12 +25,15 @@ jobs: - name: ubuntu-24.04-x64 runner: ubuntu-24.04 make: make + configure: --enable-utf8proc --enable-asan # - name: ubuntu-24.04-arm64 # runner: ubuntu-24.04-arm # make: make + # configure: --enable-utf8proc --enable-asan # - name: macos-26-arm64 # runner: macos-26 # make: gmake + # configure: --enable-utf8proc steps: - name: checkout @@ -66,10 +69,12 @@ jobs: - name: build run: | sh autogen.sh - ./configure --enable-utf8proc + ./configure ${{ matrix.configure }} ${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)" - name: test - run: | - cd regress - ${{ matrix.make }} + env: + ASAN_OPTIONS: abort_on_error=1:detect_leaks=0 + run: | + cd regress + ${{ matrix.make }} From 228c9c927711c702a130936b95b6344e6df1bf06 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 2 Jul 2026 22:57:14 +0100 Subject: [PATCH 004/127] Tweak. --- .github/workflows/regress.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 6282943b0..3d4625847 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -70,11 +70,10 @@ jobs: run: | sh autogen.sh ./configure ${{ matrix.configure }} + export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0" ${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)" - name: test - env: - ASAN_OPTIONS: abort_on_error=1:detect_leaks=0 run: | cd regress ${{ matrix.make }} From bf0eee35f9b5b1b8013d2e138ea13066f6686eb4 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 2 Jul 2026 22:57:55 +0100 Subject: [PATCH 005/127] Fix. --- .github/workflows/regress.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 3d4625847..aaaf54067 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -67,11 +67,11 @@ jobs: pkg-config - name: build - run: | - sh autogen.sh - ./configure ${{ matrix.configure }} - export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0" - ${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)" + run: | + sh autogen.sh + ./configure ${{ matrix.configure }} + export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0" + ${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)" - name: test run: | From 352b1173c8c258bd33d652e157627239d00c50af Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 2 Jul 2026 22:59:23 +0100 Subject: [PATCH 006/127] Fix. --- .github/workflows/regress.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index aaaf54067..ce5b0d761 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -67,13 +67,13 @@ jobs: pkg-config - name: build - run: | - sh autogen.sh - ./configure ${{ matrix.configure }} - export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0" - ${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)" + run: | + sh autogen.sh + ./configure ${{ matrix.configure }} + export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0" + ${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)" - name: test - run: | - cd regress - ${{ matrix.make }} + run: | + cd regress + ${{ matrix.make }} From f592be00fa954950c931f41e56146ee4bff87c59 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 2 Jul 2026 23:01:19 +0100 Subject: [PATCH 007/127] Do not require debug for ASAN. --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index eeb24f821..23dfc2ee6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -41,11 +41,11 @@ endif AM_CPPFLAGS += -DDEBUG endif AM_CPPFLAGS += -iquote. +endif if IS_ASAN AM_CFLAGS += -fsanitize=address AM_LDFLAGS += -fsanitize=address endif -endif # Set flags for Solaris. if IS_SUNOS From 351bef58a31889d873c50ef35eefad121c2e7b4f Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 2 Jul 2026 23:06:46 +0100 Subject: [PATCH 008/127] Undo last. --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 23dfc2ee6..eeb24f821 100644 --- a/Makefile.am +++ b/Makefile.am @@ -41,11 +41,11 @@ endif AM_CPPFLAGS += -DDEBUG endif AM_CPPFLAGS += -iquote. -endif if IS_ASAN AM_CFLAGS += -fsanitize=address AM_LDFLAGS += -fsanitize=address endif +endif # Set flags for Solaris. if IS_SUNOS From abcf8dca3ab9c02d3f4da9e69c201eb6f0c29f1f Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 2 Jul 2026 23:18:50 +0100 Subject: [PATCH 009/127] Put ASAN_OPTIONS in the right place. --- .github/workflows/regress.yml | 2 +- regress/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index ce5b0d761..4d064623d 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -70,10 +70,10 @@ jobs: run: | sh autogen.sh ./configure ${{ matrix.configure }} - export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0" ${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)" - name: test run: | cd regress + export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0" ${{ matrix.make }} diff --git a/regress/Makefile b/regress/Makefile index 1a9ba55cb..5f8d50c8f 100644 --- a/regress/Makefile +++ b/regress/Makefile @@ -8,7 +8,7 @@ all: for test in $(TESTS); do \ printf '%-40s ' "$$test"; \ start=$$(date +%s); \ - if sh "$$test" >/dev/null 2>&1; then \ + if env -i ASAN_OPTIONS="$$ASAN_OPTIONS" sh "$$test" >/dev/null 2>&1; then \ end=$$(date +%s); \ echo "PASS ($$((end - start))s)"; \ else \ From dbb70b394eca35c79ac85ccd0ea240a2b831766a Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 2 Jul 2026 23:28:15 +0100 Subject: [PATCH 010/127] Fix test. --- regress/format-modifiers.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/regress/format-modifiers.sh b/regress/format-modifiers.sh index 131b6527a..4244ad07b 100644 --- a/regress/format-modifiers.sh +++ b/regress/format-modifiers.sh @@ -12,7 +12,9 @@ PATH=/bin:/usr/bin TERM=screen TZ=UTC -export TZ +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export TZ LANG LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) TMUX="$TEST_TMUX -Ltest -f/dev/null" From 2b8c3c1362532367eb0270de3844f9bf09b24f18 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 2 Jul 2026 22:31:25 +0000 Subject: [PATCH 011/127] Enable swap-pane on floating panes again but not -U/-D. From Dane Jensen. --- cmd-swap-pane.c | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/cmd-swap-pane.c b/cmd-swap-pane.c index f7519a0dd..7f5160e65 100644 --- a/cmd-swap-pane.c +++ b/cmd-swap-pane.c @@ -42,6 +42,22 @@ const struct cmd_entry cmd_swap_pane_entry = { .exec = cmd_swap_pane_exec }; +static struct window_pane * +cmd_swap_pane_next_tiled_pane(struct window_pane *wp) +{ + while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell)) + wp = TAILQ_NEXT(wp, entry); + return (wp); +} + +static struct window_pane * +cmd_swap_pane_prev_tiled_pane(struct window_pane *wp) +{ + while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell)) + wp = TAILQ_PREV(wp, window_panes, entry); + return (wp); +} + static enum cmd_retval cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) { @@ -62,15 +78,29 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) server_redraw_window(dst_w); if (args_has(args, 'D')) { + if (window_pane_is_floating(dst_wp)) { + cmdq_error(item, "cannot swap down on floating pane"); + return (CMD_RETURN_ERROR); + } src_w = dst_w; src_wp = TAILQ_NEXT(dst_wp, entry); - if (src_wp == NULL) + src_wp = cmd_swap_pane_next_tiled_pane(src_wp); + if (src_wp == NULL) { src_wp = TAILQ_FIRST(&dst_w->panes); + src_wp = cmd_swap_pane_next_tiled_pane(src_wp); + } } else if (args_has(args, 'U')) { + if (window_pane_is_floating(dst_wp)) { + cmdq_error(item, "cannot swap up on floating pane"); + return (CMD_RETURN_ERROR); + } src_w = dst_w; src_wp = TAILQ_PREV(dst_wp, window_panes, entry); - if (src_wp == NULL) + src_wp = cmd_swap_pane_prev_tiled_pane(src_wp); + if (src_wp == NULL) { src_wp = TAILQ_LAST(&dst_w->panes, window_panes); + src_wp = cmd_swap_pane_prev_tiled_pane(src_wp); + } } if (src_w != dst_w && window_push_zoom(src_w, 0, args_has(args, 'Z'))) @@ -79,12 +109,6 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) if (src_wp == dst_wp) goto out; - if (window_pane_is_floating(src_wp) || - window_pane_is_floating(dst_wp)) { - cmdq_error(item, "cannot swap floating panes"); - return (CMD_RETURN_ERROR); - } - server_client_remove_pane(src_wp); server_client_remove_pane(dst_wp); From f645f0abe8ba3d7ca3bc0226c1b6f08910fb6cfb Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 3 Jul 2026 07:13:20 +0100 Subject: [PATCH 012/127] A few more tests. --- regress/buffers.sh | 287 +++++++++++++++++++++ regress/pane-ops.sh | 562 +++++++++++++++++++++++++++++++++++++++++ regress/session-ops.sh | 214 ++++++++++++++++ regress/window-ops.sh | 403 +++++++++++++++++++++++++++++ 4 files changed, 1466 insertions(+) create mode 100644 regress/buffers.sh create mode 100644 regress/pane-ops.sh create mode 100644 regress/session-ops.sh create mode 100644 regress/window-ops.sh diff --git a/regress/buffers.sh b/regress/buffers.sh new file mode 100644 index 000000000..0b3267610 --- /dev/null +++ b/regress/buffers.sh @@ -0,0 +1,287 @@ +#!/bin/sh + +# Tests of paste buffer command semantics, as implemented in cmd-set-buffer.c +# (set-buffer and delete-buffer), cmd-paste-buffer.c, cmd-load-buffer.c, +# cmd-save-buffer.c (save-buffer and show-buffer), cmd-list-buffers.c and +# paste.c. +# +# This exercises: +# - set-buffer creating automatic buffers (buffer0, buffer1, ... with the +# most recent first), -b creating/replacing a named buffer, -a appending, +# -n renaming and the error paths (no data, unknown buffer); +# - show-buffer for the top and for named buffers; +# - delete-buffer for the top and named buffers, and when nothing exists; +# - list-buffers -F custom formats and -f filters; +# - paste-buffer into a pane: newline-to-CR translation by default, -r raw, +# -s custom separator, -d delete-after-paste, unknown buffer error; +# - the buffer-limit option evicting the oldest automatic buffers but not +# named buffers; +# - load-buffer/save-buffer round trips including control characters and +# UTF-8, save-buffer -a appending and errors for missing files/buffers. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest -f/dev/null" +$TMUX kill-server 2>/dev/null + +TMP=$(mktemp) +TMP2=$(mktemp) +trap 'rm -f "$TMP" "$TMP2"; $TMUX kill-server 2>/dev/null' 0 1 15 + +# check_ok $cmd... +# +# Run a command and require that it succeeds. +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +# check_fail $expected_error $cmd... +# +# Run a command and require that it fails with the given error message. +check_fail() +{ + exp="$1" + shift + out=$($TMUX "$@" 2>&1) + if [ $? -eq 0 ]; then + echo "Command succeeded (expected failure): $*" + exit 1 + fi + if [ "$out" != "$exp" ]; then + echo "Wrong error for: $*" + echo "Expected: '$exp'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_buffers $expected +# +# Compare the buffer list (as "name=content ...", most recent first) with +# $expected. +check_buffers() +{ + out=$(echo $($TMUX list-buffers -F \ + '#{buffer_name}=#{buffer_sample}')) + if [ "$out" != "$1" ]; then + echo "Buffer list wrong." + echo "Expected: '$1'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_show $args $expected +# +# Compare show-buffer output with $expected. +check_show() +{ + out=$($TMUX show-buffer $1 2>&1) + if [ "$out" != "$2" ]; then + echo "show-buffer $1 wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then + echo "Server died: $1" + exit 1 + fi +} + +check_ok new-session -d -s B -x 80 -y 24 + +# --------------------------------------------------------------------------- +# set-buffer, show-buffer, delete-buffer, list-buffers. + +# Automatic buffers stack with the most recent first. +check_ok set-buffer one +check_ok set-buffer two +check_buffers 'buffer1=two buffer0=one' +check_show '' 'two' +check_show '-b buffer0' 'one' + +# -a only appends to a buffer named with -b: without -b it creates a new +# automatic buffer. Empty data is silently ignored. +check_ok set-buffer -a '!' +check_buffers 'buffer2=! buffer1=two buffer0=one' +check_ok delete-buffer +check_ok set-buffer '' +check_buffers 'buffer1=two buffer0=one' + +# -b names a buffer explicitly; setting it again replaces the content; +# -a appends to it. +check_ok set-buffer -b named abc +check_buffers 'named=abc buffer1=two buffer0=one' +check_ok set-buffer -b named xyz +check_ok set-buffer -a -b named 123 +check_buffers 'named=xyz123 buffer1=two buffer0=one' +check_show '-b named' 'xyz123' + +# -n renames; renaming to a bad source is an error, as is no data at all. +check_ok set-buffer -b named -n other +check_buffers 'other=xyz123 buffer1=two buffer0=one' +check_fail 'unknown buffer: nosuch' set-buffer -b nosuch -n foo +check_fail 'no data specified' set-buffer -b other +check_fail 'no buffer nosuch' show-buffer -b nosuch + +# list-buffers -f filters. +out=$($TMUX list-buffers -f '#{==:#{buffer_name},other}' -F '#{buffer_name}') +if [ "$out" != "other" ]; then + echo "list-buffers -f wrong: '$out'" + exit 1 +fi + +# delete-buffer -b removes one buffer; without -b the most recent automatic +# buffer goes - named buffers are not candidates for the top, for +# show-buffer and delete-buffer alike. +check_ok delete-buffer -b buffer1 +check_buffers 'other=xyz123 buffer0=one' +check_fail 'unknown buffer: buffer1' delete-buffer -b buffer1 +check_ok delete-buffer +check_buffers 'other=xyz123' +check_fail 'no buffers' show-buffer +check_fail 'no buffer' delete-buffer +check_ok delete-buffer -b other +check_fail 'no buffers' show-buffer + +# --------------------------------------------------------------------------- +# buffer-limit. + +# Only automatic buffers count against buffer-limit and the oldest are +# evicted; named buffers survive. (Automatic buffer numbers keep counting +# up over the life of the server, so compare content only.) +check_ok set-option -g buffer-limit 3 +check_ok set-buffer -b keepme precious +check_ok set-buffer a1 +check_ok set-buffer a2 +check_ok set-buffer a3 +check_ok set-buffer a4 +out=$(echo $($TMUX list-buffers -F '#{buffer_sample}')) +if [ "$out" != 'a4 a3 a2 precious' ]; then + echo "buffer-limit eviction wrong: '$out'" + exit 1 +fi +check_ok set-option -g buffer-limit 50 +check_ok delete-buffer -b keepme +check_ok delete-buffer; check_ok delete-buffer; check_ok delete-buffer + +# --------------------------------------------------------------------------- +# paste-buffer. + +# Paste into a raw, echo-free pane running cat -v so control characters are +# visible; a fresh window per paste keeps assertions simple. + +# paste_line $bufdata $pasteargs $expected +# +# Set a buffer, paste it into a fresh cat -v pane and compare the first +# screen line with $expected. +paste_line() +{ + $TMUX kill-window -t B:9 2>/dev/null + check_ok new-window -d -t B:9 'stty raw -echo && exec cat -v' + i=0 + while [ "$($TMUX display-message -p -t B:9.0 \ + '#{pane_current_command}')" != "cat" ]; do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "cat did not start."; exit 1; } + sleep 0.1 + done + check_ok set-buffer -b paste "$1" + check_ok paste-buffer $2 -b paste -t B:9.0 + i=0 + while out=$($TMUX capture-pane -p -t B:9.0 | sed -n 1p) && \ + [ "$out" != "$3" ]; do + i=$((i + 1)) + if [ $i -gt 50 ]; then + echo "Paste of '$1' ($2) wrong." + echo "Expected: '$3'" + echo "But got: '$out'" + exit 1 + fi + sleep 0.1 + done +} + +# By default linefeeds are replaced with carriage returns (shown as ^M by +# cat -v); -r pastes raw and -s sets an explicit separator. +paste_line 'one +two' '' 'one^Mtwo' +paste_line 'one +two' '-r' 'one' +paste_line 'one +two' '-s |' 'one|two' +paste_line 'one +two' '-s XX' 'oneXXtwo' + +# -d deletes the buffer after pasting. +paste_line 'gone' '-d' 'gone' +check_fail 'no buffer paste' show-buffer -b paste + +# Unknown buffer is an error. +check_fail 'no buffer nosuch' paste-buffer -b nosuch -t B:9.0 +check_ok kill-window -t B:9 + +# --------------------------------------------------------------------------- +# load-buffer and save-buffer. + +# Round trip a file with control characters and UTF-8 through load-buffer +# and save-buffer. +printf 'line1\tx\033[31m\001\002\303\251\n' >"$TMP" +check_ok load-buffer -b file "$TMP" +check_ok save-buffer -b file "$TMP2" +if ! cmp -s "$TMP" "$TMP2"; then + echo "load-buffer/save-buffer round trip differs." + exit 1 +fi + +# save-buffer -a appends. +check_ok save-buffer -a -b file "$TMP2" +cat "$TMP" "$TMP" >"$TMP".x +if ! cmp -s "$TMP".x "$TMP2"; then + rm -f "$TMP".x + echo "save-buffer -a did not append." + exit 1 +fi +rm -f "$TMP".x + +# show-buffer prints the loaded content (text form). +check_ok delete-buffer -b file + +# load-buffer of a missing file and save-buffer of a missing buffer or to a +# bad path are errors. +check_fail "No such file or directory: $TMP.nosuch" \ + load-buffer -b x "$TMP.nosuch" +check_fail 'no buffer nosuch' save-buffer -b nosuch "$TMP2" +check_ok set-buffer -b sb data +out=$($TMUX save-buffer -b sb /nonexistent/dir/file 2>&1) +if [ $? -eq 0 ]; then + echo "save-buffer to bad path succeeded." + exit 1 +fi + +# save-buffer - writes to stdout and load-buffer - reads from stdin. +out=$($TMUX save-buffer -b sb -) +if [ "$out" != "data" ]; then + echo "save-buffer - wrong: '$out'" + exit 1 +fi +check_ok delete-buffer -b sb +printf 'from stdin' | $TMUX load-buffer -b stdinbuf - +check_show '-b stdinbuf' 'from stdin' +check_ok delete-buffer -b stdinbuf + +assert_alive + +$TMUX kill-server 2>/dev/null +exit 0 diff --git a/regress/pane-ops.sh b/regress/pane-ops.sh new file mode 100644 index 000000000..ee0894830 --- /dev/null +++ b/regress/pane-ops.sh @@ -0,0 +1,562 @@ +#!/bin/sh + +# Tests of pane management command semantics (not parsing), as implemented in +# cmd-split-window.c, cmd-break-pane.c, cmd-join-pane.c (join-pane and +# move-pane), cmd-swap-pane.c, cmd-kill-pane.c, cmd-respawn-pane.c, +# cmd-respawn-window.c, cmd-resize-pane.c and cmd-select-pane.c. +# +# This exercises: +# - split-window -h/-v with -l in cells and percent, -b placing the new pane +# before (left/top of) the target and -f spanning the full window size; +# - break-pane moving a pane into a new window (-d, -n name, -a after, -P -F +# printing the new location); +# - join-pane moving a window's only pane into another window (destroying the +# source window), -b before, -l size, and the identical-panes error; +# - move-pane as an alias for join-pane; +# - swap-pane -U/-D/-s/-t, -d keeping the active pane, and the marked pane +# (select-pane -m/-M) as the default swap source; +# - kill-pane, kill-pane -a keeping only the target; +# - 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). +# +# window-ops.sh covers window-level commands and buffers.sh paste buffers. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest -f/dev/null" +$TMUX kill-server 2>/dev/null + +# check_ok $cmd... +# +# Run a command and require that it succeeds. +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +# check_fail $expected_error $cmd... +# +# Run a command and require that it fails with the given error message. +check_fail() +{ + exp="$1" + shift + out=$($TMUX "$@" 2>&1) + if [ $? -eq 0 ]; then + echo "Command succeeded (expected failure): $*" + exit 1 + fi + if [ "$out" != "$exp" ]; then + echo "Wrong error for: $*" + echo "Expected: '$exp'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_fmt $target $format $expected +# +# Expand a format in a target's context and compare with $expected. +check_fmt() +{ + out=$($TMUX display-message -p -t "$1" "$2" 2>&1) + if [ "$out" != "$3" ]; then + echo "Format '$2' for '$1' wrong." + echo "Expected: '$3'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_panes $target $expected +# +# Compare the pane list of a window (as "index:id ...", in index order) with +# $expected. +check_panes() +{ + out=$(echo $($TMUX list-panes -t "$1" -F '#{pane_index}:#{pane_id}')) + if [ "$out" != "$2" ]; then + echo "Pane list of '$1' wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then + echo "Server died: $1" + exit 1 + fi +} + +# --------------------------------------------------------------------------- +# split-window geometry. + +check_ok new-session -d -s P -x 80 -y 24 -n main +p0=$($TMUX display-message -p -t P:0.0 '#{pane_id}') + +# Horizontal split with -l in cells: new pane gets exactly that width and the +# old pane the rest minus the separator line. +check_ok split-window -d -h -l 20 -t "$p0" +p1=$($TMUX display-message -p -t P:0.1 '#{pane_id}') +check_fmt "$p1" '#{pane_width}x#{pane_height}' '20x24' +check_fmt "$p0" '#{pane_width}x#{pane_height}' '59x24' + +# Vertical split with a percentage of the pane being split. +check_ok split-window -d -v -l 25% -t "$p0" +p2=$($TMUX display-message -p -t P:0.1 '#{pane_id}') +check_fmt "$p2" '#{pane_width}x#{pane_height}' '59x6' +check_fmt "$p0" '#{pane_width}x#{pane_height}' '59x17' + +# -b puts the new pane to the left of the target; -f makes it span the full +# window height. +check_ok split-window -d -h -b -f -l 10 -t "$p0" +p3=$($TMUX display-message -p -t P:0.0 '#{pane_id}') +check_fmt "$p3" '#{pane_width}x#{pane_height}' '10x24' +check_fmt "$p3" '#{pane_left},#{pane_top}' '0,0' +check_fmt "$p0" '#{pane_width}x#{pane_height}' '50x17' +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# The new pane becomes active unless -d is given. +check_ok select-pane -t "$p0" +check_ok split-window -d -v -t "$p0" +check_fmt 'P:0' '#{pane_id}' "$p0" +p4=$($TMUX display-message -p -t P:0.2 '#{pane_id}') +check_ok split-window -v -t "$p4" +p5=$($TMUX display-message -p -t 'P:0' '#{pane_id}') +check_ok kill-pane -t "$p5" +check_ok kill-pane -t "$p4" +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. +out=$($TMUX break-pane -d -P -F '#{window_index}:#{pane_id}' -n broken \ + -s "$p1" -t P:) +if [ "$out" != "1:$p1" ]; then + echo "break-pane -P output wrong: '$out'" + exit 1 +fi +check_fmt 'P:1' '#{window_name}:#{window_panes}' 'broken:1' +check_fmt 'P:0' '#{window_panes}' '3' + +# join-pane -v moves it back (the source window, left empty, is destroyed). +check_ok join-pane -d -v -s P:broken.0 -t "$p2" +check_fmt 'P:0' '#{window_panes}' '4' +if $TMUX has-session -t P:broken 2>/dev/null; then + echo "Window 'broken' still exists after join-pane." + exit 1 +fi + +# The joined pane is below the target (-v, no -b). +top=$($TMUX display-message -p -t "$p2" '#{pane_bottom}') +joined=$($TMUX display-message -p -t "$p1" '#{pane_top}') +if [ "$joined" -le "$top" ]; then + echo "Joined pane is not below target ($joined <= $top)." + exit 1 +fi + +# join-pane -h -b puts the source to the left of the target; -l sets size. +check_ok break-pane -d -n broken -s "$p1" -t P: +check_ok join-pane -d -h -b -l 30 -s P:broken.0 -t "$p2" +check_fmt "$p1" '#{pane_width}' '30' +l1=$($TMUX display-message -p -t "$p1" '#{pane_left}') +l2=$($TMUX display-message -p -t "$p2" '#{pane_left}') +if [ "$l1" -ge "$l2" ]; then + echo "Joined pane is not left of target ($l1 >= $l2)." + exit 1 +fi + +# Joining a pane to itself is an error. +check_fail 'source and target panes must be different' \ + join-pane -d -s "$p0" -t "$p0" + +# break-pane to an occupied window index or with an invalid (non-UTF-8) name +# is an error. +check_fail 'index in use: 0' break-pane -d -s "$p1" -t P:0 +check_fail "invalid window name: $(printf 'a\377b')" \ + break-pane -d -n "$(printf 'a\377b')" -s "$p1" -t P: + +# join-pane can move a pane from one window to another without destroying +# the source window if other panes remain. (On this branch move-pane is +# reserved for floating panes, covered by floating-pane-geometry.sh.) +check_ok new-window -d -t P:5 -n other +check_ok join-pane -d -s "$p1" -t P:5.0 +check_fmt 'P:5' '#{window_panes}' '2' +check_fmt 'P:0' '#{window_panes}' '3' +check_ok join-pane -d -v -s "$p1" -t "$p2" +check_fmt 'P:0' '#{window_panes}' '4' +check_fmt 'P:5' '#{window_panes}' '1' + +# --------------------------------------------------------------------------- +# swap-pane. + +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# -s/-t swap two panes. +check_ok swap-pane -d -s "$p3" -t "$p1" +check_panes P:0 "0:$p1 1:$p0 2:$p2 3:$p3" +check_ok swap-pane -d -s "$p3" -t "$p1" +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# -U swaps the target pane with the previous pane, -D with the next; without +# -s the target is the active pane. +check_ok swap-pane -d -U -t "$p0" +check_panes P:0 "0:$p0 1:$p3 2:$p2 3:$p1" +check_ok swap-pane -d -D -t "$p0" +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# Without -d the target pane becomes the active pane (it arrives at the +# source pane's position). +check_ok select-pane -t "$p0" +check_ok swap-pane -s "$p0" -t "$p2" +check_fmt 'P:0' '#{pane_id}' "$p2" +check_panes P:0 "0:$p3 1:$p2 2:$p0 3:$p1" +check_ok swap-pane -s "$p2" -t "$p0" +check_fmt 'P:0' '#{pane_id}' "$p0" +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# With a marked pane and no -s, the marked pane is the swap source. +check_ok select-pane -m -t "$p3" +check_fmt "$p3" '#{pane_marked}' '1' +check_ok swap-pane -d -t "$p1" +check_panes P:0 "0:$p1 1:$p0 2:$p2 3:$p3" +check_ok swap-pane -d -t "$p1" +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# select-pane -M clears the mark. +check_ok select-pane -M +check_fmt "$p3" '#{pane_marked}' '0' +check_fmt 'P:0' '#{pane_marked_set}' '0' + +# --------------------------------------------------------------------------- +# resize-pane and zoom. + +# Absolute -x on a horizontal split and percentage. +check_ok resize-pane -t "$p3" -x 20 +check_fmt "$p3" '#{pane_width}' '20' +check_ok resize-pane -t "$p3" -x 25% +check_fmt "$p3" '#{pane_width}' '20' +check_ok resize-pane -t "$p3" -x 10 +check_fmt "$p3" '#{pane_width}' '10' + +# Relative adjustments: -R grows a left pane, -L shrinks it back; a count may +# be given. +check_ok resize-pane -t "$p3" -R +check_fmt "$p3" '#{pane_width}' '11' +check_ok resize-pane -t "$p3" -L +check_fmt "$p3" '#{pane_width}' '10' +check_ok resize-pane -t "$p3" -R 5 +check_fmt "$p3" '#{pane_width}' '15' +check_ok resize-pane -t "$p3" -L 5 +check_fmt "$p3" '#{pane_width}' '10' + +# -y on a vertical split. +check_ok resize-pane -t "$p2" -y 10 +check_fmt "$p2" '#{pane_height}' '10' + +# p2 is the bottom pane, so its bottom border cannot move down: -D instead +# grows it by taking lines from the pane above and -U gives them back. +check_ok resize-pane -t "$p2" -D 2 +check_fmt "$p2" '#{pane_height}' '12' +check_ok resize-pane -t "$p2" -U 2 +check_fmt "$p2" '#{pane_height}' '10' + +# Bad adjustment, width and height are errors. +check_fail 'adjustment invalid' resize-pane -t "$p2" -U nonsense +check_fail 'width invalid' resize-pane -t "$p2" -x nonsense +check_fail 'height invalid' resize-pane -t "$p2" -y nonsense + +# -Z zooms: the pane temporarily fills the window and the flags show it. +check_ok resize-pane -Z -t "$p0" +check_fmt "$p0" '#{window_zoomed_flag}:#{pane_width}x#{pane_height}' \ + '1:80x24' + +# Zoom is transparent to pane commands on other panes, and -Z again unzooms. +check_ok resize-pane -Z -t "$p0" +check_fmt "$p0" '#{window_zoomed_flag}' '0' + +# Splitting while zoomed unzooms first. +check_ok resize-pane -Z -t "$p0" +check_fmt 'P:0' '#{window_zoomed_flag}' '1' +check_ok split-window -d -v -t "$p0" +check_fmt 'P:0' '#{window_zoomed_flag}' '0' +check_fmt 'P:0' '#{window_panes}' '5' +p6=$($TMUX display-message -p -t P:0.2 '#{pane_id}') +check_ok kill-pane -t "$p6" + +# --------------------------------------------------------------------------- +# kill-pane. + +check_fmt 'P:0' '#{window_panes}' '4' +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 kill-pane -a -t "$p0" +check_panes P:0 "0:$p0" + +# Killing the last pane in a window kills the window. +check_ok new-window -d -t P:7 -n goner +check_ok kill-pane -t P:7.0 +if $TMUX has-session -t P:goner 2>/dev/null; then + echo "Window 'goner' still exists after killing its only pane." + exit 1 +fi + +# --------------------------------------------------------------------------- +# respawn-pane and respawn-window. + +# Respawning a pane whose process is alive fails without -k. +check_fail "respawn pane failed: pane P:0.0 still active" \ + respawn-pane -t P:0.0 +check_fail "respawn window failed: window P:0 still active" \ + respawn-window -t P:0 + +# With remain-on-exit a pane whose command exited stays as a dead pane and +# may be respawned without -k. +check_ok set-option -g remain-on-exit on +check_ok new-window -d -t P:8 -n dead 'true' +i=0 +while [ "$($TMUX display-message -p -t P:8.0 '#{pane_dead}')" != "1" ]; do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "Pane did not die."; exit 1; } + sleep 0.1 +done +check_ok respawn-pane -t P:8.0 'sleep 100' +check_fmt 'P:8.0' '#{pane_dead}' '0' + +# -k kills the live process and respawns. +check_ok respawn-pane -k -t P:8.0 'sleep 200' +check_fmt 'P:8.0' '#{pane_dead}' '0' + +# respawn-window -k replaces the whole window (all panes) with one pane. +check_ok split-window -d -t P:8 +check_fmt 'P:8' '#{window_panes}' '2' +check_ok respawn-window -k -t P:8 'sleep 300' +check_fmt 'P:8' '#{window_panes}' '1' +check_fmt 'P:8.0' '#{pane_dead}' '0' +check_ok set-option -g remain-on-exit off + +# --------------------------------------------------------------------------- +# select-pane. + +# A 2x2-ish arrangement: q0 on top, q1 bottom-left, q2 bottom-right. +check_ok new-window -d -t P:2 -n sel +q0=$($TMUX display-message -p -t P:2.0 '#{pane_id}') +check_ok split-window -d -v -t "$q0" +q1=$($TMUX display-message -p -t P:2.1 '#{pane_id}') +check_ok split-window -d -h -t "$q1" +q2=$($TMUX display-message -p -t P:2.2 '#{pane_id}') + +# Directional selection: -D, -R and -U move by pane position. +check_ok select-pane -t "$q0" +check_ok select-pane -D -t P:2 +check_fmt 'P:2' '#{pane_id}' "$q1" +check_ok select-pane -R -t P:2 +check_fmt 'P:2' '#{pane_id}' "$q2" +check_ok select-pane -U -t P:2 +check_fmt 'P:2' '#{pane_id}' "$q0" + +# -l returns to the previously active pane; a window that never had another +# active pane has no last pane. +check_ok select-pane -l -t P:2 +check_fmt 'P:2' '#{pane_id}' "$q2" +check_fail 'no last pane' select-pane -l -t P:8.0 + +# -d disables input to a pane, -e enables it again and -T sets the title. +check_ok select-pane -d -t "$q0" +check_fmt "$q0" '#{pane_input_off}' '1' +check_ok select-pane -e -t "$q0" +check_fmt "$q0" '#{pane_input_off}' '0' +check_ok select-pane -T mytitle -t "$q0" +check_fmt "$q0" '#{pane_title}' 'mytitle' +check_ok kill-window -t P:2 + +# --------------------------------------------------------------------------- +# more split-window variants. + +check_ok new-window -d -t P:2 -n splits + +# -E splits with an empty pane, running no command; giving one is an error. +check_ok split-window -d -E -t P:2.0 +check_fmt 'P:2' '#{window_panes}' '2' +check_fail 'command cannot be given for empty pane' \ + split-window -d -E -t P:2.0 'sleep 5' + +# -e adds to the new pane's environment. +eid=$($TMUX split-window -d -P -F '#{pane_id}' -e GREETING=hello -t P:2.0 \ + 'echo $GREETING; exec cat') +i=0 +while out=$($TMUX capture-pane -p -t "$eid" | sed -n 1p) && \ + [ "$out" != "hello" ]; do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "split-window -e wrong: '$out'"; exit 1; } + sleep 0.1 +done + +# A bad -l size is an error. +check_fail 'invalid tiled geometry invalid' \ + split-window -d -v -l invalid -t P:2.0 + +# -Z zooms the new pane. +check_ok split-window -d -Z -t P:2.0 +check_fmt 'P:2' '#{window_zoomed_flag}' '1' +check_ok kill-window -t P:2 + +# --------------------------------------------------------------------------- +# more swap-pane: wrapping, cross-window, self and zoomed swaps. + +check_ok new-window -d -t P:3 -n swaps +check_ok split-window -d -v -t P:3.0 +check_ok split-window -d -v -t P:3.0 +r0=$($TMUX display-message -p -t P:3.0 '#{pane_id}') +r1=$($TMUX display-message -p -t P:3.1 '#{pane_id}') +r2=$($TMUX display-message -p -t P:3.2 '#{pane_id}') +o0=$($TMUX display-message -p -t P:5.0 '#{pane_id}') + +# -D on the last pane and -U on the first wrap around to the other end. +check_ok swap-pane -d -D -t "$r2" +check_panes P:3 "0:$r2 1:$r1 2:$r0" +check_ok swap-pane -d -s "$r0" -t "$r2" +check_ok swap-pane -d -U -t "$r0" +check_panes P:3 "0:$r2 1:$r1 2:$r0" +check_ok swap-pane -d -s "$r0" -t "$r2" +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" + +# Panes can be swapped between different windows. +check_ok swap-pane -d -s "$o0" -t "$r1" +check_panes P:3 "0:$r0 1:$o0 2:$r2" +check_panes P:5 "0:$r1" +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" + +# -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_ok resize-pane -Z -t P:3 +check_panes P:3 "0:$r1 1:$r0 2:$r2" + +# kill-pane -a -f only kills other panes matching the filter. +check_ok kill-pane -a -f '#{==:#{pane_id},'"$r2"'}' -t "$r1" +check_panes P:3 "0:$r1 1:$r0" +check_ok kill-window -t P:3 + +# --------------------------------------------------------------------------- +# split-window -I and -s. + +check_ok new-window -d -t P:3 -n splits2 + +# -I fills the new (empty) pane from standard input. +printf 'stdin-stuff' | $TMUX split-window -d -I -t P:3.0 +if [ $? -ne 0 ]; then + echo "split-window -I failed." + exit 1 +fi +i=0 +while out=$($TMUX capture-pane -p -t P:3.1 | sed -n 1p) && \ + [ "$out" != "stdin-stuff" ]; do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "split-window -I wrong: '$out'"; exit 1; } + sleep 0.1 +done + +# -s sets the new pane's window-style. +sid=$($TMUX split-window -d -P -F '#{pane_id}' -s 'bg=red' -t P:3.0) +out=$($TMUX show-options -v -p -t "$sid" window-style) +if [ "$out" != "bg=red" ]; then + echo "split-window -s style wrong: '$out'" + exit 1 +fi +check_ok kill-window -t P:3 + +# --------------------------------------------------------------------------- +# more break-pane: -a insertion, selection and single-pane windows. + +# check_windows $session $expected +# +# Compare the window list of a session (as "index:name ...") with $expected. +check_windows() +{ + out=$(echo $($TMUX list-windows -t "$1" -F \ + '#{window_index}:#{window_name}')) + if [ "$out" != "$2" ]; then + echo "Window list of '$1' wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +check_ok new-session -d -s Q -x 80 -y 24 -n q0 + +# -a breaks into a new window inserted after the target index, shuffling the +# following windows up. +check_ok new-window -d -t Q:1 -n q1 +check_ok split-window -d -t Q:1 +check_ok break-pane -d -a -s Q:1.1 -n qa -t Q:0 +check_windows Q '0:q0 1:qa 2:q1' + +# Without -d the new window is selected. +check_ok split-window -d -t Q:2 +check_ok select-window -t Q:0 +check_ok break-pane -s Q:2.1 -n qcur -t Q: +check_fmt 'Q:' '#{window_name}' 'qcur' +check_windows Q '0:q0 1:qa 2:q1 3:qcur' + +# Breaking the only pane of a window relinks the window at a new index; -n +# still renames it. +check_ok new-window -d -t Q:5 -n qsolo +out=$($TMUX break-pane -d -P -F '#{window_name}' -s Q:5.0 -n qmoved -t Q:) +if [ "$out" != "qmoved" ]; then + echo "single-pane break-pane output wrong: '$out'" + exit 1 +fi +if $TMUX has-session -t Q:qsolo 2>/dev/null; then + echo "Window 'qsolo' still exists after single-pane break-pane." + exit 1 +fi +check_ok has-session -t Q:qmoved +check_ok kill-session -t Q + +# --------------------------------------------------------------------------- +# resize-pane -T. + +# -T trims the history: lines below the cursor position are removed and the +# cursor moves to the bottom. seq writes 100 lines (leaving 77 in history on +# a 24-line screen) and the escape sequence puts the cursor on line 5. +check_ok new-window -d -t P:2 'seq 1 100; printf "\033[5;1H"; exec cat' +i=0 +while [ "$($TMUX display-message -p -t P:2.0 '#{history_size}')" != "77" ] +do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "History did not fill."; exit 1; } + sleep 0.1 +done +check_fmt 'P:2.0' '#{cursor_y}' '4' +check_ok resize-pane -T -t P:2.0 +check_fmt 'P:2.0' '#{history_size}' '58' +check_fmt 'P:2.0' '#{cursor_y}' '23' +check_ok kill-window -t P:2 + +assert_alive + +$TMUX kill-server 2>/dev/null +exit 0 diff --git a/regress/session-ops.sh b/regress/session-ops.sh new file mode 100644 index 000000000..2da14b959 --- /dev/null +++ b/regress/session-ops.sh @@ -0,0 +1,214 @@ +#!/bin/sh + +# Tests of session management command semantics, as implemented in +# cmd-new-session.c, cmd-rename-session.c, cmd-kill-session.c and +# cmd-has-session.c, plus grouped sessions (new-session -t). +# +# This exercises: +# - new-session naming: explicit -s, invalid and duplicate names, automatic +# numeric names, -n naming the initial window and -A attaching to (here: +# not duplicating) an existing session; +# - session_id/session_name/session_windows formats and has-session; +# - rename-session, including duplicate and invalid names, and that the +# session keeps its id when renamed; +# - kill-session, kill-session -a (all but target), the "-f only valid with +# -a" guard, and that killing the last session stops the server; +# - grouped sessions: new-session -t shares the window list (a window made +# in one session appears in the other; killed windows disappear), current +# windows are tracked independently and destroying one grouped session +# leaves the windows in the other. +# +# session-group-resize.sh covers sizing of grouped sessions. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest -f/dev/null" +$TMUX kill-server 2>/dev/null + +# check_ok $cmd... +# +# Run a command and require that it succeeds. +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +# check_fail $expected_error $cmd... +# +# Run a command and require that it fails with the given error message. +check_fail() +{ + exp="$1" + shift + out=$($TMUX "$@" 2>&1) + if [ $? -eq 0 ]; then + echo "Command succeeded (expected failure): $*" + exit 1 + fi + if [ "$out" != "$exp" ]; then + echo "Wrong error for: $*" + echo "Expected: '$exp'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_fmt $target $format $expected +# +# Expand a format in a target's context and compare with $expected. +check_fmt() +{ + out=$($TMUX display-message -p -t "$1" "$2" 2>&1) + if [ "$out" != "$3" ]; then + echo "Format '$2' for '$1' wrong." + echo "Expected: '$3'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_sessions $expected +# +# Compare the session list (as "name name ...", sorted by name) with +# $expected. +check_sessions() +{ + out=$(echo $($TMUX list-sessions -F '#{session_name}' | LC_ALL=C sort)) + if [ "$out" != "$1" ]; then + echo "Session list wrong." + echo "Expected: '$1'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_windows $session $expected +# +# Compare the window list of a session (as "index:name ...") with $expected. +check_windows() +{ + out=$(echo $($TMUX list-windows -t "$1" -F \ + '#{window_index}:#{window_name}')) + if [ "$out" != "$2" ]; then + echo "Window list of '$1' wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +# --------------------------------------------------------------------------- +# new-session and has-session. + +check_ok new-session -d -s S1 -x 80 -y 24 -n first +check_fmt 'S1:' '#{session_name}:#{window_name}:#{session_windows}' \ + 'S1:first:1' + +# A duplicate name is an error. Only invalid UTF-8 is rejected as a name: +# colons, periods and even an empty string are allowed (such sessions can +# only be targeted by id). +check_fail 'duplicate session: S1' new-session -d -s S1 +badname=$(printf 'a\377b') +check_fail "invalid session name: $badname" new-session -d -s "$badname" +oddid=$($TMUX new-session -d -s 'a:b.c' -x 80 -y 24 -P -F '#{session_id}') +check_fmt "$oddid" '#{session_name}' 'a:b.c' +check_ok kill-session -t "$oddid" +emptyid=$($TMUX new-session -d -s '' -x 80 -y 24 -P -F '#{session_id}') +check_fmt "$emptyid" '#{session_name}' '' +check_ok kill-session -t "$emptyid" + +# Without -s, sessions get a numeric name matching their id counter. +autoname=$($TMUX new-session -d -x 80 -y 24 -P -F '#{session_name}') +autoid=$($TMUX display-message -p -t "=$autoname:" '#{session_id}') +if [ "\$$autoname" != "$autoid" ]; then + echo "Automatic session name '$autoname' does not match id '$autoid'." + exit 1 +fi +check_ok has-session -t "=$autoname" +check_ok rename-session -t "=$autoname" S2 +check_sessions 'S1 S2' + +# -A creates the session only if it does not exist; if it does, -A means +# attach, which a detached client without a terminal cannot do. +check_ok new-session -d -A -s S3 +check_sessions 'S1 S2 S3' +check_fail 'open terminal failed: not a terminal' new-session -d -A -s S3 +check_sessions 'S1 S2 S3' +check_ok kill-session -t S3 + +# has-session fails for a missing session. +check_fail "can't find session: nosuch" has-session -t nosuch + +# --------------------------------------------------------------------------- +# rename-session. + +# The id survives a rename and the old name is gone. +id=$($TMUX display-message -p -t S2: '#{session_id}') +check_ok rename-session -t S2 newname +check_sessions 'S1 newname' +check_fmt "$id" '#{session_name}' 'newname' +check_fail "can't find session: S2" has-session -t S2 + +# Renaming to an existing or invalid name is an error. +check_fail 'duplicate session: S1' rename-session -t newname S1 +check_fail "invalid session name: $badname" rename-session -t newname \ + "$badname" +check_ok rename-session -t newname S2 + +# --------------------------------------------------------------------------- +# grouped sessions (new-session -t). + +check_ok new-session -d -s G1 -x 80 -y 24 -n shared +check_ok new-session -d -s G2 -t G1 +check_fmt 'G1:' '#{session_grouped}:#{session_group_size}' '1:2' +check_fmt 'G2:' '#{session_grouped}:#{session_group_list}' '1:G1,G2' + +# The window list is shared: windows created or killed in one session +# appear and disappear in the other. +check_ok new-window -d -t G2: -n added +check_windows G1 '0:shared 1:added' +check_windows G2 '0:shared 1:added' +check_ok kill-window -t G1:added +check_windows G2 '0:shared' + +# The current window is tracked per session. +check_ok new-window -d -t G2:1 -n other +check_ok select-window -t G1:0 +check_ok select-window -t G2:1 +check_fmt 'G1:' '#{window_name}' 'shared' +check_fmt 'G2:' '#{window_name}' 'other' + +# Killing one grouped session leaves the windows in the other (the group +# itself survives with a single member). +check_ok kill-session -t G2 +check_windows G1 '0:shared 1:other' +check_fmt 'G1:' '#{session_grouped}:#{session_group_size}' '1:1' +check_ok kill-window -t G1:other + +# --------------------------------------------------------------------------- +# kill-session. + +check_sessions 'G1 S1 S2' +check_fail '-f only valid with -a' kill-session -f 'x' -t S1 + +# -C only clears alerts; the session survives. +check_ok kill-session -C -t S2 +check_ok has-session -t S2 + +# -a kills every other session. +check_ok kill-session -a -t S1 +check_sessions 'S1' + +# Killing the last session stops the server. +check_ok kill-session -t S1 +if $TMUX has-session -t S1 2>/dev/null; then + echo "Server still up after killing the last session." + exit 1 +fi + +exit 0 diff --git a/regress/window-ops.sh b/regress/window-ops.sh new file mode 100644 index 000000000..3dba68edd --- /dev/null +++ b/regress/window-ops.sh @@ -0,0 +1,403 @@ +#!/bin/sh + +# Tests of window management command semantics (not parsing), as implemented +# in cmd-new-window.c, cmd-move-window.c (move-window and link-window), +# cmd-unlink-window.c, cmd-swap-window.c, cmd-rotate-window.c, +# cmd-kill-window.c and cmd-select-window.c. +# +# This exercises: +# - new-window placement: next free index, explicit index, index in use with +# and without -k, -a (after) and -b (before) insertion with shuffling, and +# -S selecting an existing window by name instead of creating; +# - move-window to a free index, to an occupied index with and without -k, +# -a insertion and -r renumbering (including base-index); +# - renumber-windows closing gaps; +# - link-window sharing a window between two sessions (window_linked and +# window_linked_sessions), unlink-window removing one link and refusing to +# 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; +# - kill-window switching to the last (previously current) window, kill-window +# -a killing all other windows and the "-f only valid with -a" guard. +# +# pane-ops.sh covers pane-level commands and buffers.sh covers paste buffers. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest -f/dev/null" +$TMUX kill-server 2>/dev/null + +# check_ok $cmd... +# +# Run a command and require that it succeeds. +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +# check_fail $expected_error $cmd... +# +# Run a command and require that it fails with the given error message. +check_fail() +{ + exp="$1" + shift + out=$($TMUX "$@" 2>&1) + if [ $? -eq 0 ]; then + echo "Command succeeded (expected failure): $*" + exit 1 + fi + if [ "$out" != "$exp" ]; then + echo "Wrong error for: $*" + echo "Expected: '$exp'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_windows $session $expected +# +# Compare the window list of a session (as "index:name index:name ...", in +# index order) with $expected. +check_windows() +{ + out=$(echo $($TMUX list-windows -t "$1" -F \ + '#{window_index}:#{window_name}')) + if [ "$out" != "$2" ]; then + echo "Window list of '$1' wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_fmt $target $format $expected +# +# Expand a format in a target's context and compare with $expected. +check_fmt() +{ + out=$($TMUX display-message -p -t "$1" "$2" 2>&1) + if [ "$out" != "$3" ]; then + echo "Format '$2' for '$1' wrong." + echo "Expected: '$3'" + echo "But got: '$out'" + exit 1 + fi +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then + echo "Server died: $1" + exit 1 + fi +} + +# --------------------------------------------------------------------------- +# new-window placement. + +check_ok new-session -d -s W -x 80 -y 24 -n w0 + +# Next free index. +check_ok new-window -d -t W: -n w1 +check_ok new-window -d -t W: -n w2 +check_windows W '0:w0 1:w1 2:w2' + +# Explicit index, then the next new window fills the first free index, not +# one past the highest. +check_ok new-window -d -t W:9 -n w9 +check_ok new-window -d -t W: -n w3 +check_windows W '0:w0 1:w1 2:w2 3:w3 9:w9' + +# Occupied index fails without -k and replaces with -k. +check_fail 'create window failed: index 9 in use' \ + new-window -d -t W:9 -n dup +check_ok new-window -d -k -t W:9 -n w9k +check_windows W '0:w0 1:w1 2:w2 3:w3 9:w9k' + +# -a inserts after the target, shuffling the following windows up. +check_ok new-window -d -a -t W:1 -n wA +check_windows W '0:w0 1:w1 2:wA 3:w2 4:w3 9:w9k' + +# -b inserts before the target, shuffling the target and followers up. +check_ok new-window -d -b -t W:0 -n wB +check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k' + +# -S selects an existing window with the same name instead of creating (with +# -d it would not switch, so no -d here). +check_ok select-window -t W:0 +check_ok new-window -S -t W: -n w3 +check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k' +check_fmt 'W:' '#{window_index}:#{window_name}' '5:w3' + +# Clean up to a known arrangement. +check_ok kill-window -t W:wB +check_ok kill-window -t W:wA +check_ok kill-window -t W:w9k +check_ok move-window -r -t W: +check_windows W '0:w0 1:w1 2:w2 3:w3' + +# --------------------------------------------------------------------------- +# move-window. + +# To a free index. +check_ok move-window -d -s W:2 -t W:7 +check_windows W '0:w0 1:w1 3:w3 7:w2' + +# To an occupied index, without and with -k. +check_fail 'index in use: 7' move-window -d -s W:3 -t W:7 +check_ok move-window -d -k -s W:3 -t W:7 +check_windows W '0:w0 1:w1 7:w3' + +# -a inserts after the target and shuffles. +check_ok move-window -d -a -s W:7 -t W:0 +check_windows W '0:w0 1:w3 2:w1' + +# -r renumbers in order, respecting base-index. +check_ok move-window -d -s W:2 -t W:8 +check_ok set-option -t W base-index 5 +check_ok move-window -r -t W: +check_windows W '5:w0 6:w3 7:w1' +check_ok set-option -t W base-index 0 +check_ok move-window -r -t W: +check_windows W '0:w0 1:w3 2:w1' + +# With the renumber-windows option on, killing a window renumbers the rest +# automatically. +check_ok set-option -t W renumber-windows on +check_ok new-window -d -t W:9 -n wtmp +check_windows W '0:w0 1:w3 2:w1 9:wtmp' +check_ok kill-window -t W:1 +check_windows W '0:w0 1:w1 2:wtmp' +check_ok kill-window -t W:2 +check_ok set-option -t W renumber-windows off +check_ok new-window -d -t W:2 -n w3 +check_windows W '0:w0 1:w1 2:w3' + +# Without -s, the current window of the client/session moves. +check_ok select-window -t W:2 +check_ok move-window -d -t W:6 +check_windows W '0:w0 1:w1 6:w3' +check_ok move-window -r -t W: +check_windows W '0:w0 1:w1 2:w3' + +# --------------------------------------------------------------------------- +# link-window and unlink-window. + +check_ok new-session -d -s L -x 80 -y 24 -n l0 + +# Link a window from W into L and check it is shared. +check_ok link-window -d -s W:w1 -t L:5 +check_windows L '0:l0 5:w1' +check_fmt 'W:w1' '#{window_linked}' '1' +check_fmt 'L:5' '#{window_linked_sessions}' '2' + +# The linked window is the same window: renaming in one session shows in the +# other. +check_ok rename-window -t L:5 shared +check_windows W '0:w0 1:shared 2:w3' +check_ok rename-window -t W:1 w1 + +# Linking again to an occupied index fails without -k. +check_fail 'index in use: 0' link-window -d -s W:w3 -t L:0 + +# Unlink removes one link; the window survives in the other session. +check_ok unlink-window -t L:5 +check_windows L '0:l0' +check_windows W '0:w0 1:w1 2:w3' +check_fmt 'W:w1' '#{window_linked}' '0' + +# Unlinking a window linked to only one session needs -k. +check_fail 'window only linked to one session' unlink-window -t W:w3 +check_ok unlink-window -k -t W:w3 +check_windows W '0:w0 1:w1' + +# --------------------------------------------------------------------------- +# swap-window. + +check_ok new-window -d -t W:2 -n w2 +check_ok new-window -d -t W:3 -n w3 + +# Swap within a session: indices are exchanged. +check_ok swap-window -d -s W:0 -t W:3 +check_windows W '0:w3 1:w1 2:w2 3:w0' +check_ok swap-window -d -s W:0 -t W:3 +check_windows W '0:w0 1:w1 2:w2 3:w3' + +# Without -d the current index does not change, so the window that arrives +# there becomes current; with -d the swapped windows are selected, so the +# source window stays current at its new index. +check_ok select-window -t W:0 +check_ok swap-window -s W:0 -t W:3 +check_fmt 'W:' '#{window_index}:#{window_name}' '0:w3' +check_ok swap-window -s W:3 -t W:0 +check_fmt 'W:' '#{window_index}:#{window_name}' '0:w0' +check_ok swap-window -d -s W:0 -t W:3 +check_fmt 'W:' '#{window_index}:#{window_name}' '3:w0' +check_ok swap-window -d -s W:3 -t W:0 +check_fmt 'W:' '#{window_index}:#{window_name}' '0:w0' + +# Swap between two different sessions. +check_ok swap-window -d -s W:w2 -t L:l0 +check_windows W '0:w0 1:w1 2:l0 3:w3' +check_windows L '0:w2' +check_ok swap-window -d -s W:2 -t L:0 +check_windows W '0:w0 1:w1 2:w2 3:w3' +check_windows L '0:l0' + +# Swapping between two sessions in the same group is an error. +check_ok new-session -d -s WG -t W +check_fail "can't move window, sessions are grouped" \ + swap-window -d -s W:0 -t WG:1 +check_ok kill-session -t WG +check_windows W '0:w0 1:w1 2:w2 3:w3' + +# --------------------------------------------------------------------------- +# rotate-window. + +check_ok new-session -d -s R -x 80 -y 24 +check_ok split-window -d -t R:0 +check_ok split-window -d -t R:0 +p0=$($TMUX display-message -p -t R:0.0 '#{pane_id}') +p1=$($TMUX display-message -p -t R:0.1 '#{pane_id}') +p2=$($TMUX display-message -p -t R:0.2 '#{pane_id}') + +# check_panes $target $expected +# +# Compare the pane list of a window (as "index:id ...") with $expected. +check_panes() +{ + out=$(echo $($TMUX list-panes -t "$1" -F \ + '#{pane_index}:#{pane_id}')) + if [ "$out" != "$2" ]; then + echo "Pane list of '$1' wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +check_panes R:0 "0:$p0 1:$p1 2:$p2" + +# -U rotates panes up (each pane moves to the previous position); -D rotates +# down. -U then -D restores the original order. +check_ok rotate-window -U -t R:0 +check_panes R:0 "0:$p1 1:$p2 2:$p0" +check_ok rotate-window -D -t R:0 +check_panes R:0 "0:$p0 1:$p1 2:$p2" +check_ok rotate-window -D -t R:0 +check_panes R:0 "0:$p2 1:$p0 2:$p1" +check_ok rotate-window -U -t R:0 + +# The active position is preserved across rotation: the pane that arrives at +# the active position becomes the active pane. +check_ok select-pane -t R:0.0 +check_ok rotate-window -U -t R:0 +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" + +# --------------------------------------------------------------------------- +# kill-window. + +# Killing the current window switches to the last (previously current) +# window. +check_ok select-window -t W:1 +check_ok select-window -t W:3 +check_fmt 'W:' '#{window_index}' '3' +check_ok kill-window -t W:3 +check_fmt 'W:' '#{window_index}' '1' +check_windows W '0:w0 1:w1 2:w2' + +# -f is only valid with -a. +check_fail '-f only valid with -a' kill-window -f 'x' -t W:0 + +# -a kills every window except the target. +check_ok kill-window -a -t W:w1 +check_windows W '1:w1' + +# --------------------------------------------------------------------------- +# select-window, next-window, previous-window. + +# -P prints where the new window went; an invalid (non-UTF-8) name is an +# error. +out=$($TMUX new-window -d -t W:0 -n wa -P -F '#{window_index}:#{window_name}') +if [ "$out" != "0:wa" ]; then + echo "new-window -P output wrong: '$out'" + exit 1 +fi +check_fail "invalid window name: $(printf 'a\377b')" \ + new-window -d -t W: -n "$(printf 'a\377b')" +check_ok new-window -d -t W:2 -n wc +check_windows W '0:wa 1:w1 2:wc' + +# -n and -p select the next and previous window, wrapping at the ends. +check_ok select-window -t W:0 +check_ok select-window -n -t W: +check_fmt 'W:' '#{window_index}' '1' +check_ok select-window -n -t W: +check_fmt 'W:' '#{window_index}' '2' +check_ok select-window -n -t W: +check_fmt 'W:' '#{window_index}' '0' +check_ok select-window -p -t W: +check_fmt 'W:' '#{window_index}' '2' + +# next-window and previous-window are the same code. +check_ok next-window -t W: +check_fmt 'W:' '#{window_index}' '0' +check_ok previous-window -t W: +check_fmt 'W:' '#{window_index}' '2' + +# -l selects the previously current window and select-window -T on the +# already-current window does the same. +check_ok select-window -t W:1 +check_ok select-window -l -t W: +check_fmt 'W:' '#{window_index}' '2' +check_ok select-window -T -t W:2 +check_fmt 'W:' '#{window_index}' '1' +check_ok select-window -T -t W:2 +check_fmt 'W:' '#{window_index}' '2' + +# With -a, next-window looks for a window with an alert and fails if there +# is none; a fresh session has no last window. +check_fail 'no next window' next-window -a -t W: +check_fail 'no previous window' previous-window -a -t W: +check_ok new-session -d -s F -x 80 -y 24 +check_fail 'no last window' select-window -l -t F: +check_fail 'no last window' select-window -T -t F:0 +check_ok kill-session -t F + +# --------------------------------------------------------------------------- +# more kill-window -a: no-op, filters and multiply-linked windows. + +# -a with a single window in the session does nothing. +check_ok kill-window -a -t L:0 +check_windows L '0:l0' + +# -a -f only kills other windows matching the filter. +check_ok kill-window -a -f '#{==:#{window_name},wc}' -t W:0 +check_windows W '0:wa 1:w1' + +# If the current window is linked into the session more than once, -a kills +# it too - taking the whole session with it here. +check_ok new-session -d -s D -x 80 -y 24 -n d0 +check_ok link-window -d -s D:0 -t D:5 +check_ok new-window -d -t D:1 -n dx +check_ok select-window -t D:0 +check_ok kill-window -a -t D:0 +if $TMUX has-session -t D 2>/dev/null; then + echo "Session D survived kill-window -a on multiply-linked window." + exit 1 +fi + +check_fmt 'R:0' '#{window_panes}' '3' +assert_alive + +$TMUX kill-server 2>/dev/null +exit 0 From fa2c9f45513b11e9b3e3b5ca98fff915e303b170 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 3 Jul 2026 10:47:32 +0000 Subject: [PATCH 013/127] Allow join-pane to tile floating panes, from Dane Jensen. --- cmd-join-pane.c | 40 ++++++++++++++++++++++++++++++++++++++++ layout.c | 16 ++++++++-------- tmux.1 | 9 +++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 6cb37d7eb..9e67f1187 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -364,6 +364,44 @@ cmd_join_pane_zindex(struct cmdq_item *item, struct winlink *wl, return (CMD_RETURN_NORMAL); } +static enum cmd_retval +cmd_join_pane_tile(struct cmdq_item *item, struct args *args, struct window *w, + struct window_pane *wp) +{ + struct layout_cell *lc = wp->layout_cell; + + if (!window_pane_is_floating(wp)) { + cmdq_error(item, "pane is not floating"); + return (CMD_RETURN_ERROR); + } + if (w->flags & WINDOW_ZOOMED) { + cmdq_error(item, "can't tile a pane while window is zoomed"); + return (CMD_RETURN_ERROR); + } + + lc->saved_sx = lc->sx; + lc->saved_sy = lc->sy; + lc->saved_xoff = lc->xoff; + lc->saved_yoff = lc->yoff; + if (layout_insert_tile(w, lc) != 0) { + cmdq_error(item, "no space for a new pane"); + return (CMD_RETURN_ERROR); + } + lc->flags &= ~LAYOUT_CELL_FLOATING; + + TAILQ_REMOVE(&w->z_index, wp, zentry); + TAILQ_INSERT_TAIL(&w->z_index, wp, zentry); + + if (!args_has(args, 'd')) + window_set_active_pane(w, wp, 1); + layout_fix_offsets(w); + layout_fix_panes(w, NULL); + notify_window("window-layout-changed", w); + server_redraw_window(w); + + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) { @@ -413,6 +451,8 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) server_unzoom_window(src_w); if (src_wp == dst_wp) { + if (window_pane_is_floating(src_wp)) + return (cmd_join_pane_tile(item, args, src_w, src_wp)); cmdq_error(item, "source and target panes must be different"); return (CMD_RETURN_ERROR); } diff --git a/layout.c b/layout.c index 2a605d6cc..11c292bba 100644 --- a/layout.c +++ b/layout.c @@ -1774,7 +1774,7 @@ layout_remove_tile(struct window *w, struct layout_cell *lc) int change; if (lc->flags & LAYOUT_CELL_FLOATING) - return (0); + return (-1); lcneighbour = layout_cell_get_neighbour(lc); if (lcneighbour == NULL) { @@ -1799,7 +1799,7 @@ layout_remove_tile(struct window *w, struct layout_cell *lc) */ if (lc->parent != NULL) layout_set_size(lc, 0, 0, 0, 0); - return (1); + return (0); } /* @@ -1816,14 +1816,14 @@ layout_insert_tile(struct window *w, struct layout_cell *lc) if (lc == NULL) fatalx("layout cell cannot be null when tiling"); - lcparent = lc->parent; - if (lc->flags & LAYOUT_CELL_FLOATING) - return (1); + if (layout_cell_is_tiled(lc)) + return (-1); + lcparent = lc->parent; if (lcparent == NULL) { /* Only pane in the layout. */ layout_set_size(lc, w->sx, w->sy, 0, 0); - return (1); + return (0); } type = lcparent->type; @@ -1846,7 +1846,7 @@ layout_insert_tile(struct window *w, struct layout_cell *lc) */ lctiled = layout_cell_get_first_tiled(lcneighbour); if (!layout_split_check_space(lctiled->wp, lcneighbour, type)) - return (0); + return (-1); layout_split_sizes(lcneighbour, -1, 0, type, &size1, &size2, &saved_size); layout_resize_set_size(w, lc, type, size1); @@ -1863,5 +1863,5 @@ layout_insert_tile(struct window *w, struct layout_cell *lc) } layout_resize_set_size(w, lc, type, size1); - return (1); + return (0); } diff --git a/tmux.1 b/tmux.1 index 9e891ac23..6061fe9a0 100644 --- a/tmux.1 +++ b/tmux.1 @@ -3247,6 +3247,15 @@ is omitted and a marked pane is present (see .Ic select\-pane .Fl m ) , the marked pane is used rather than the current pane. +.Pp +If +.Ar src\-pane +is floating and +.Ar dst\-pane +is either unspecified or equal to +.Ar src\-pane, +.Ar src\-pane +is returned to its previous position in the layout. .Tg killp .It Xo Ic kill\-pane .Op Fl a From 89a59d4df57d4c7bc5ae42e115006a1022d804ed Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 3 Jul 2026 11:50:12 +0100 Subject: [PATCH 014/127] Bump version. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index e2a5f169c..6abb2d9d5 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ # configure.ac -AC_INIT([tmux], next-3.7) +AC_INIT([tmux], next-3.8) AC_PREREQ([2.60]) AC_CONFIG_AUX_DIR(etc) From cceca5a1584881ee5e3a34011efc7178a5e7b7ef Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 3 Jul 2026 14:48:07 +0000 Subject: [PATCH 015/127] Add single quote operator. --- format.c | 31 +++++++++++++++++++++++++++++++ tmux.1 | 4 +++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/format.c b/format.c index 4055efa5d..f73a6caa0 100644 --- a/format.c +++ b/format.c @@ -123,6 +123,7 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2) #define FORMAT_CLIENT_ENVIRON 0x4000000 #define FORMAT_COLOUR_ESC_FG 0x8000000 #define FORMAT_COLOUR_ESC_BG 0x10000000 +#define FORMAT_QUOTE_SHELL_SQ 0x20000000 /* Limit on recursion. */ #define FORMAT_LOOP_LIMIT 100 @@ -4009,6 +4010,29 @@ format_quote_shell(const char *s) return (out); } +/* Quote string with POSIX shell single quotes. */ +static char * +format_quote_shell_single(const char *s) +{ + const char *cp; + char *out, *at; + + at = out = xmalloc(strlen(s) * 4 + 3); + *at++ = '\''; + for (cp = s; *cp != '\0'; cp++) { + if (*cp == '\'') { + *at++ = '\''; + *at++ = '\\'; + *at++ = '\''; + *at++ = '\''; + } else + *at++ = *cp; + } + *at++ = '\''; + *at = '\0'; + return (out); +} + /* Quote #s in string. */ static char * format_quote_style(const char *s) @@ -4227,6 +4251,11 @@ found: found = format_quote_shell(saved); free(saved); } + if (modifiers & FORMAT_QUOTE_SHELL_SQ) { + saved = found; + found = format_quote_shell_single(saved); + free(saved); + } if (modifiers & FORMAT_QUOTE_STYLE) { saved = found; found = format_quote_style(saved); @@ -5297,6 +5326,8 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, case 'q': if (fm->argc < 1) modifiers |= FORMAT_QUOTE_SHELL; + else if (strchr(fm->argv[0], 's') != NULL) + modifiers |= FORMAT_QUOTE_SHELL_SQ; else if (strchr(fm->argv[0], 'e') != NULL || strchr(fm->argv[0], 'h') != NULL) modifiers |= FORMAT_QUOTE_STYLE; diff --git a/tmux.1 b/tmux.1 index 6061fe9a0..f016d9db9 100644 --- a/tmux.1 +++ b/tmux.1 @@ -6820,7 +6820,9 @@ or with .Ql a escape .Nm -command arguments. +command arguments; with +.Ql s +use single quotes. .Ql E:\& will expand the format twice, for example .Ql #{E:status\-left} From 47b90a4e0303b5a3a12515ccd7461e482f319cf6 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 3 Jul 2026 15:48:07 +0100 Subject: [PATCH 016/127] Add single quote test. --- regress/format-modifiers.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/regress/format-modifiers.sh b/regress/format-modifiers.sh index 4244ad07b..81f9eacc1 100644 --- a/regress/format-modifiers.sh +++ b/regress/format-modifiers.sh @@ -100,6 +100,9 @@ $TMUX set -g @host 'myhost' || exit 1 $TMUX set -g @ts '1000000000' || exit 1 # 2001-09-09 01:46:40 UTC $TMUX set -g @sp 'a b$c' || exit 1 # shell-special characters for q: $TMUX set -g @hash 'a#b' || exit 1 # a "#" for q/e: +$TMUX set -g @sq "a'b" || exit 1 # a single quote for q/s: +$TMUX set -g @nl "$(printf 'a\nb')" || exit 1 +q_s_nl=$(printf "'a\nb'") # --- Comparisons and matching -------------------------------------------- @@ -153,6 +156,10 @@ test_format "#{!!:non-empty}" "1" # q: escapes shell special characters with a backslash. test_format "#{q:@sp}" 'a\ b\$c' +# q/s quotes with POSIX shell single quotes. +test_format "#{q/s:@sp}" "'a b\$c'" +test_format "#{q/s:@sq}" "'a'\\''b'" +test_format "#{q/s:@nl}" "$q_s_nl" # q/e and q/h escape "#" for the format/style parser by doubling it. test_format "#{q/e:@hash}" 'a##b' test_format "#{q/h:@hash}" 'a##b' From e1fc6b31eed435877351817c70ad71f7212081c2 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 3 Jul 2026 15:10:07 +0000 Subject: [PATCH 017/127] Add manual width and height window formats. --- format.c | 32 ++++++++++++++++++++++++++++++++ tmux.1 | 2 ++ 2 files changed, 34 insertions(+) diff --git a/format.c b/format.c index f73a6caa0..dd6961b95 100644 --- a/format.c +++ b/format.c @@ -2844,6 +2844,19 @@ format_cb_window_height(struct format_tree *ft) return (NULL); } +/* Callback for window_manual_height. */ +static void * +format_cb_window_manual_height(struct format_tree *ft) +{ + struct window *w = ft->w; + + if (w == NULL) + return (NULL); + if (options_get_number(w->options, "window-size") != WINDOW_SIZE_MANUAL) + return (xstrdup("")); + return (format_printf("%u", w->manual_sy)); +} + /* Callback for window_id. */ static void * format_cb_window_id(struct format_tree *ft) @@ -3024,6 +3037,19 @@ format_cb_window_width(struct format_tree *ft) return (NULL); } +/* Callback for window_manual_width. */ +static void * +format_cb_window_manual_width(struct format_tree *ft) +{ + struct window *w = ft->w; + + if (w == NULL) + return (NULL); + if (options_get_number(w->options, "window-size") != WINDOW_SIZE_MANUAL) + return (xstrdup("")); + return (format_printf("%u", w->manual_sx)); +} + /* Callback for window_zoomed_flag. */ static void * format_cb_window_zoomed_flag(struct format_tree *ft) @@ -3730,6 +3756,12 @@ static const struct format_table_entry format_table[] = { { "window_linked_sessions_list", FORMAT_TABLE_STRING, format_cb_window_linked_sessions_list }, + { "window_manual_height", FORMAT_TABLE_STRING, + format_cb_window_manual_height + }, + { "window_manual_width", FORMAT_TABLE_STRING, + format_cb_window_manual_width + }, { "window_marked_flag", FORMAT_TABLE_STRING, format_cb_window_marked_flag }, diff --git a/tmux.1 b/tmux.1 index f016d9db9..83c6bf042 100644 --- a/tmux.1 +++ b/tmux.1 @@ -7180,6 +7180,8 @@ The following variables are available, where appropriate: .It Li "window_linked" Ta "" Ta "1 if window is linked across sessions" .It Li "window_linked_sessions" Ta "" Ta "Number of sessions this window is linked to" .It Li "window_linked_sessions_list" Ta "" Ta "List of sessions this window is linked to" +.It Li "window_manual_height" Ta "" Ta "Manual height of window, if set" +.It Li "window_manual_width" Ta "" Ta "Manual width of window, if set" .It Li "window_marked_flag" Ta "" Ta "1 if window contains the marked pane" .It Li "window_name" Ta "#W" Ta "Name of window" .It Li "window_offset_x" Ta "" Ta "X offset into window if larger than client" From 6967f547745547149ec9460a073601e1e6789391 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 3 Jul 2026 15:35:51 +0000 Subject: [PATCH 018/127] Add additional pane sort orders. --- format.c | 6 ++++++ tmux.1 | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/format.c b/format.c index dd6961b95..337fe18ec 100644 --- a/format.c +++ b/format.c @@ -5426,6 +5426,12 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, sc->reversed = 0; break; } + if (strchr(fm->argv[0], 'i') != NULL) + sc->order = SORT_INDEX; + else if (strchr(fm->argv[0], 'z') != NULL) + sc->order = SORT_Z; + else + sc->order = SORT_CREATION; if (strchr(fm->argv[0], 'r') != NULL) sc->reversed = 1; else diff --git a/tmux.1 b/tmux.1 index 83c6bf042..def0780b8 100644 --- a/tmux.1 +++ b/tmux.1 @@ -6856,7 +6856,13 @@ to sort in reverse order. .Ql /r\& can also be used with .Ql P:\& -to reverse the sort order by pane index. +to reverse the sort order; by default panes are sorted by creation order. +.Ql P:\& +can also take +.Ql /i\& +to sort by pane index or +.Ql /z\& +to sort by z-index. For example, .Ql S/nr:\& to sort sessions by name in reverse order. From 310d0e93d6e033aba63939b131be623b792339a2 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 3 Jul 2026 16:37:33 +0100 Subject: [PATCH 019/127] Pane sort tests. --- regress/format-modifiers.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/regress/format-modifiers.sh b/regress/format-modifiers.sh index 81f9eacc1..21203b6a9 100644 --- a/regress/format-modifiers.sh +++ b/regress/format-modifiers.sh @@ -526,11 +526,17 @@ $TMUX split-window -h -t zeta:charlie $TMUX split-window -h -t zeta:charlie test_format "#{P:#{pane_index}}" "012" "zeta:charlie" test_format "#{P/r:#{pane_index}}" "210" "zeta:charlie" -# A pane-sort argument is accepted; for panes only the r (reverse) suffix has an -# effect, so these all keep the count and exercise the argument branch. +# Pane sort accepts i (pane-list order) and z (z-order). Other sort letters +# fall back to the default creation order; r reverses whichever order is used. test_format "#{P/i:x}" "xxx" "zeta:charlie" +test_format "#{P/i:#{pane_index}}" "012" "zeta:charlie" +test_format "#{P/z:x}" "xxx" "zeta:charlie" test_format "#{P/n:x}" "xxx" "zeta:charlie" test_format "#{P/t:x}" "xxx" "zeta:charlie" +$TMUX new-pane -d -t zeta:charlie -x 20 -y 10 -X 1 -Y 1 +test_format "#{P/i:#{pane_index}}" "0123" "zeta:charlie" +test_format "#{P/z:#{pane_index}}" "3012" "zeta:charlie" +test_format "#{P/zr:#{pane_index}}" "0123" "zeta:charlie" # Verbose expansion of the loops, to exercise their logging paths. $TMUX display-message -v -p "#{S:#{session_name}}" >/dev/null 2>&1 From 29f0939a5b760d1ca8cdcfdf641704302b66b58c Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 3 Jul 2026 17:01:16 +0100 Subject: [PATCH 020/127] Update tests for -E. --- regress/pane-ops.sh | 55 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/regress/pane-ops.sh b/regress/pane-ops.sh index ee0894830..40e4bb77c 100644 --- a/regress/pane-ops.sh +++ b/regress/pane-ops.sh @@ -389,6 +389,61 @@ check_ok kill-window -t P:2 check_ok new-window -d -t P:2 -n splits +# new-window -E creates an empty initial pane, running no command. +check_ok new-window -d -E -t P:9 -n empty +check_fmt 'P:9.0' '#{pane_dead}' '0' +check_fmt 'P:9.0' '#{pane_pid}' '' +check_fail 'command cannot be given for empty pane' \ + new-window -d -E -t P:10 -n empty 'true' + +# respawn-pane -E stores the command and cwd without starting it. +tmp=${TMPDIR:-/tmp}/tmux-pane-ops-empty-$$ +rm -f "$tmp" +check_ok new-window -d -E -t P:10 -n empty-respawn +check_ok respawn-pane -E -c /tmp -t P:10.0 "pwd > $tmp" +if [ -e "$tmp" ]; then + echo "respawn-pane -E started command unexpectedly" + exit 1 +fi +check_ok respawn-pane -t P:10.0 +i=0 +while [ ! -e "$tmp" ]; do + i=$((i + 1)) + [ $i -gt 50 ] && echo "respawn-pane did not start stored command" && \ + exit 1 + sleep 0.1 +done +if [ "$(cat "$tmp")" != "/tmp" ]; then + echo "respawn-pane did not use stored cwd" + exit 1 +fi +rm -f "$tmp" +check_ok kill-window -t P:9 + +# respawn-window -E stores the command and cwd without starting it. +tmp=${TMPDIR:-/tmp}/tmux-window-ops-empty-$$ +rm -f "$tmp" +check_ok new-window -d -E -t P:11 -n empty-respawn-window +check_ok respawn-window -E -c /tmp -t P:11 "pwd > $tmp" +check_fmt 'P:11.0' '#{pane_pid}' '' +if [ -e "$tmp" ]; then + echo "respawn-window -E started command unexpectedly" + exit 1 +fi +check_ok respawn-window -t P:11 +i=0 +while [ ! -e "$tmp" ]; do + i=$((i + 1)) + [ $i -gt 50 ] && echo "respawn-window did not start stored command" && \ + exit 1 + sleep 0.1 +done +if [ "$(cat "$tmp")" != "/tmp" ]; then + echo "respawn-window did not use stored cwd" + exit 1 +fi +rm -f "$tmp" + # -E splits with an empty pane, running no command; giving one is an error. check_ok split-window -d -E -t P:2.0 check_fmt 'P:2' '#{window_panes}' '2' From 820067826a07cc8f36e65943b4b23217ed2e8dd2 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 3 Jul 2026 16:09:49 +0000 Subject: [PATCH 021/127] Add -E to new-window and respawn-pane and respawn-window to create empty panes. --- cmd-new-window.c | 15 ++++++++++++--- cmd-respawn-pane.c | 6 ++++-- cmd-respawn-window.c | 6 ++++-- spawn.c | 13 ++++++++++--- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/cmd-new-window.c b/cmd-new-window.c index bb6113fd6..6e6d70ccc 100644 --- a/cmd-new-window.c +++ b/cmd-new-window.c @@ -38,8 +38,8 @@ const struct cmd_entry cmd_new_window_entry = { .name = "new-window", .alias = "neww", - .args = { "abc:de:F:kn:PSt:", 0, -1, NULL }, - .usage = "[-abdkPS] [-c start-directory] [-e environment] [-F format] " + .args = { "abc:de:EF:kn:PSt:", 0, -1, NULL }, + .usage = "[-abdEkPS] [-c start-directory] [-e environment] [-F format] " "[-n window-name] " CMD_TARGET_WINDOW_USAGE " [shell-command [argument ...]]", @@ -60,12 +60,19 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) struct client *tc = cmdq_get_target_client(item); struct session *s = target->s; struct winlink *wl = target->wl, *new_wl = NULL; - int idx = target->idx, before; + int idx = target->idx, before, count = args_count(args); char *cause = NULL, *cp, *expanded, *wname = NULL; const char *template, *name; struct cmd_find_state fs; struct args_value *av; + if (args_has(args, 'E') && + count != 0 && + (count != 1 || *args_string(args, 0) != '\0')) { + cmdq_error(item, "command cannot be given for empty pane"); + return (CMD_RETURN_ERROR); + } + /* * If -S and -n are given and -t is not and a single window with this * name already exists, select it. @@ -134,6 +141,8 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) sc.cwd = args_get(args, 'c'); sc.flags = 0; + if (args_has(args, 'E')) + sc.flags |= SPAWN_EMPTY; if (args_has(args, 'd')) sc.flags |= SPAWN_DETACHED; if (args_has(args, 'k')) diff --git a/cmd-respawn-pane.c b/cmd-respawn-pane.c index 1e1dd4c6c..bf058a267 100644 --- a/cmd-respawn-pane.c +++ b/cmd-respawn-pane.c @@ -34,8 +34,8 @@ const struct cmd_entry cmd_respawn_pane_entry = { .name = "respawn-pane", .alias = "respawnp", - .args = { "c:e:kt:", 0, -1, NULL }, - .usage = "[-k] [-c start-directory] [-e environment] " + .args = { "c:e:Ekt:", 0, -1, NULL }, + .usage = "[-Ek] [-c start-directory] [-e environment] " CMD_TARGET_PANE_USAGE " [shell-command [argument ...]]", .target = { 't', CMD_FIND_PANE, 0 }, @@ -75,6 +75,8 @@ cmd_respawn_pane_exec(struct cmd *self, struct cmdq_item *item) sc.cwd = args_get(args, 'c'); sc.flags = SPAWN_RESPAWN; + if (args_has(args, 'E')) + sc.flags |= SPAWN_EMPTY; if (args_has(args, 'k')) sc.flags |= SPAWN_KILL; diff --git a/cmd-respawn-window.c b/cmd-respawn-window.c index e1eae0afc..e67757e1b 100644 --- a/cmd-respawn-window.c +++ b/cmd-respawn-window.c @@ -34,8 +34,8 @@ const struct cmd_entry cmd_respawn_window_entry = { .name = "respawn-window", .alias = "respawnw", - .args = { "c:e:kt:", 0, -1, NULL }, - .usage = "[-k] [-c start-directory] [-e environment] " + .args = { "c:e:Ekt:", 0, -1, NULL }, + .usage = "[-Ek] [-c start-directory] [-e environment] " CMD_TARGET_WINDOW_USAGE " [shell-command [argument ...]]", .target = { 't', CMD_FIND_WINDOW, 0 }, @@ -74,6 +74,8 @@ cmd_respawn_window_exec(struct cmd *self, struct cmdq_item *item) sc.cwd = args_get(args, 'c'); sc.flags = SPAWN_RESPAWN; + if (args_has(args, 'E')) + sc.flags |= SPAWN_EMPTY; if (args_has(args, 'k')) sc.flags |= SPAWN_KILL; diff --git a/spawn.c b/spawn.c index 4f7a9611e..9f0c87db9 100644 --- a/spawn.c +++ b/spawn.c @@ -266,14 +266,20 @@ spawn_pane(struct spawn_context *sc, char **cause) free(cwd); return (NULL); } - if (sc->wp0->fd != -1) { + if (sc->wp0->event != NULL) { bufferevent_free(sc->wp0->event); + sc->wp0->event = NULL; + } + if (sc->wp0->fd != -1) { close(sc->wp0->fd); + sc->wp0->fd = -1; } window_pane_reset_mode_all(sc->wp0); screen_reinit(&sc->wp0->base); - input_free(sc->wp0->ictx); - sc->wp0->ictx = NULL; + if (sc->wp0->ictx != NULL) { + input_free(sc->wp0->ictx); + sc->wp0->ictx = NULL; + } new_wp = sc->wp0; new_wp->flags &= ~(PANE_STATUSREADY|PANE_STATUSDRAWN); } else { @@ -389,6 +395,7 @@ spawn_pane(struct spawn_context *sc, char **cause) new_wp->base.mode |= MODE_CRLF; goto complete; } + new_wp->flags &= ~PANE_EMPTY; /* Store current working directory and change to new one. */ if (getcwd(path, sizeof path) != NULL) { From e03a937c71b9e415e1d74da85efbb6c5685d3c73 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 3 Jul 2026 16:10:25 +0000 Subject: [PATCH 022/127] Add a quoted pane_start_command_list which does not lose argument information. --- format.c | 37 ++++++++++++++++++++++++++++++++++++- tmux.1 | 16 +++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/format.c b/format.c index 337fe18ec..e16a8e2c3 100644 --- a/format.c +++ b/format.c @@ -42,6 +42,7 @@ struct format_expand_state; static char *format_job_get(struct format_expand_state *, const char *); +static char *format_quote_shell_single(const char *); static char *format_expand1(struct format_expand_state *, const char *); static int format_replace(struct format_expand_state *, const char *, size_t, char **, size_t *, size_t *); @@ -865,6 +866,37 @@ format_cb_start_command(struct format_tree *ft) return (cmd_stringify_argv(wp->argc, wp->argv)); } +/* Callback for pane_start_command_list. */ +static void * +format_cb_start_command_list(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + char *buf = NULL, *s; + size_t len = 0; + int i; + + if (wp == NULL) + return (NULL); + if (wp->argc == 0) + return (xstrdup("")); + + for (i = 0; i < wp->argc; i++) { + s = format_quote_shell_single(wp->argv[i]); + + len += strlen(s) + 1; + buf = xrealloc(buf, len); + + if (i == 0) + *buf = '\0'; + else + strlcat(buf, " ", len); + strlcat(buf, s, len); + + free(s); + } + return (buf); +} + /* Callback for pane_start_path. */ static void * format_cb_start_path(struct format_tree *ft) @@ -2293,7 +2325,7 @@ format_cb_pane_path(struct format_tree *ft) static void * format_cb_pane_pid(struct format_tree *ft) { - if (ft->wp != NULL) + if (ft->wp != NULL && ft->wp->fd != -1) return (format_printf("%ld", (long)ft->wp->pid)); return (NULL); } @@ -3540,6 +3572,9 @@ static const struct format_table_entry format_table[] = { { "pane_start_command", FORMAT_TABLE_STRING, format_cb_start_command }, + { "pane_start_command_list", FORMAT_TABLE_STRING, + format_cb_start_command_list + }, { "pane_start_path", FORMAT_TABLE_STRING, format_cb_start_path }, diff --git a/tmux.1 b/tmux.1 index def0780b8..f672c0202 100644 --- a/tmux.1 +++ b/tmux.1 @@ -3523,7 +3523,7 @@ the option. .Tg neww .It Xo Ic new\-window -.Op Fl abdkPS +.Op Fl abdEkPS .Op Fl c Ar start\-directory .Op Fl e Ar environment .Op Fl F Ar format @@ -3547,6 +3547,9 @@ is the new window location. If .Fl d is given, the session does not make the new window the current window. +If +.Fl E +is given, the initial pane is created without a running command. .Ar target\-window represents the window to be created; if the target already exists an error is shown, unless the @@ -3846,7 +3849,7 @@ This command will automatically set to manual in the window options. .Tg respawnp .It Xo Ic respawn\-pane -.Op Fl k +.Op Fl Ek .Op Fl c Ar start\-directory .Op Fl e Ar environment .Op Fl t Ar target\-pane @@ -3863,6 +3866,9 @@ executed. The pane must be already inactive, unless .Fl k is given, in which case any existing command is killed. +If +.Fl E +is given, the pane is left without a running command. .Fl c specifies a new working directory for the pane. The @@ -3872,7 +3878,7 @@ option has the same meaning as for the command. .Tg respawnw .It Xo Ic respawn\-window -.Op Fl k +.Op Fl Ek .Op Fl c Ar start\-directory .Op Fl e Ar environment .Op Fl t Ar target\-window @@ -3889,6 +3895,9 @@ executed. The window must be already inactive, unless .Fl k is given, in which case any existing command is killed. +If +.Fl E +is given, the window is left with one pane and without a running command. .Fl c specifies a new working directory for the window. The @@ -7101,6 +7110,7 @@ The following variables are available, where appropriate: .It Li "pane_right" Ta "" Ta "Right of pane" .It Li "pane_search_string" Ta "" Ta "Last search string in copy mode" .It Li "pane_start_command" Ta "" Ta "Command pane started with" +.It Li "pane_start_command_list" Ta "" Ta "Command pane started with, quoted" .It Li "pane_start_path" Ta "" Ta "Path pane started with" .It Li "pane_synchronized" Ta "" Ta "1 if pane is synchronized" .It Li "pane_tabs" Ta "" Ta "Pane tab positions" From 44304bf9e4b9e93d06fed7a4471b06dd0c442971 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 3 Jul 2026 17:10:35 +0100 Subject: [PATCH 023/127] Add pane_start_command_list to tests. --- regress/format-variables.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/regress/format-variables.sh b/regress/format-variables.sh index dccc0e390..fa51e0216 100644 --- a/regress/format-variables.sh +++ b/regress/format-variables.sh @@ -133,6 +133,7 @@ pane_pipe_pid pane_right pane_search_string pane_start_command +pane_start_command_list pane_start_path pane_synchronized pane_tabs @@ -343,6 +344,8 @@ test_var pane_at_top "1" -t "$TGT" test_var pane_at_left "1" -t "$TGT" test_var last_window_index "1" -t "$TGT" test_var pid "$($TMUX display-message -p '#{pid}')" -t "$TGT" +test_var pane_start_command "cat" -t "$TGT" +test_var pane_start_command_list "'cat'" -t "$TGT" # The grouped session is reported as such. test_var session_grouped "1" -t "cov:" @@ -384,5 +387,29 @@ esac [ -n "$($TMUX display-message -p '#{t/r:start_time}')" ] || fail "Empty #{t/r:start_time}." +# pane_start_command_list quotes each argv word for sh, so evaluating the +# expansion reconstructs the original argv exactly - including words with +# quotes, spaces, newlines and empty words. sh -c ignores the extra words +# (they become positional parameters), so the pane stays alive. -u stops +# the server sanitizing the newline away when printing to a non-UTF-8 +# client (the test runs without a locale in the environment). +$TMUX new-session -d -s quot -x 80 -y 24 -- sh -c 'sleep 100' arg0 \ + "it's a 'test'" 'two words' '' 'new +line' || fail "Failed to create quoting test session." +LIST=$($TMUX -u display-message -t 'quot:0.0' -p '#{pane_start_command_list}') +eval "set -- $LIST" +GOT=$(for a; do printf '<%s>' "$a"; done) +EXP=$(for a in sh -c 'sleep 100' arg0 "it's a 'test'" 'two words' '' 'new +line'; do printf '<%s>' "$a"; done) +if [ "$GOT" != "$EXP" ]; then + echo "pane_start_command_list did not round-trip." + echo "Expected: $EXP" + echo "But got: $GOT" + fail "Expansion was: $LIST" +fi +# A pane started with the default shell has an empty start command. +$TMUX new-window -d -t 'quot:' || fail "Failed to create shell window." +test_var pane_start_command_list "" -t "quot:1.0" + cleanup exit 0 From b5a6d9bcaba731ebd5f9412430d4ab89dcc3d3f3 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 3 Jul 2026 17:43:14 +0000 Subject: [PATCH 024/127] Fix some spelling and indentation. --- format.c | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/format.c b/format.c index e16a8e2c3..9ec36c4c9 100644 --- a/format.c +++ b/format.c @@ -4898,9 +4898,9 @@ format_window_name(struct format_expand_state *es, const char *fmt) return (xstrdup("0")); } -/* Add neighbor window variables to the format tree. */ +/* Add neighbour window variables to the format tree. */ static void -format_add_window_neighbor(struct format_tree *nft, struct winlink *wl, +format_add_window_neighbour(struct format_tree *nft, struct winlink *wl, struct session *s, const char *prefix) { struct options_entry *o; @@ -4933,20 +4933,21 @@ format_add_window_neighbor(struct format_tree *nft, struct winlink *wl, static char * format_loop_windows(struct format_expand_state *es, const char *fmt) { - struct sort_criteria *sc = &sort_crit; - struct format_tree *ft = es->ft; - struct client *c = ft->client; - struct cmdq_item *item = ft->item; - struct format_tree *nft; - struct format_expand_state next; - char *all, *active, *use, *expanded, *value; - struct evbuffer *buffer; - size_t size; - struct winlink *wl, **l; - struct window *w; - int i, n; + struct sort_criteria *sc = &sort_crit; + struct format_tree *ft = es->ft; + struct client *c = ft->client; + struct session *s = ft->s; + struct cmdq_item *item = ft->item; + struct format_tree *nft; + struct format_expand_state next; + char *all, *active, *use, *expanded, *value; + struct evbuffer *buffer; + size_t size; + struct winlink *wl, **l; + struct window *w; + int i, n; - if (ft->s == NULL) { + if (s == NULL) { format_log(es, "window loop but no session"); return (NULL); } @@ -4960,12 +4961,12 @@ format_loop_windows(struct format_expand_state *es, const char *fmt) if (buffer == NULL) fatalx("out of memory"); - l = sort_get_winlinks_session(ft->s, &n, sc); + l = sort_get_winlinks_session(s, &n, sc); for (i = 0; i < n; i++) { wl = l[i]; w = wl->window; format_log(es, "window loop: %u @%u", wl->idx, w->id); - if (active != NULL && wl == ft->s->curw) + if (active != NULL && wl == s->curw) use = active; else use = all; @@ -4973,17 +4974,17 @@ format_loop_windows(struct format_expand_state *es, const char *fmt) ft->flags); format_add(nft, "loop_index", "%d", i); format_add(nft, "loop_last_flag", "%d", i == n - 1); - format_defaults(nft, ft->c, ft->s, wl, NULL); + format_defaults(nft, ft->c, s, wl, NULL); - /* Add neighbor window data to the format tree. */ + /* Add neighbour window data to the format tree. */ format_add(nft, "window_after_active", "%d", - i > 0 && l[i - 1] == ft->s->curw); + i > 0 && l[i - 1] == s->curw); format_add(nft, "window_before_active", "%d", - i + 1 < n && l[i + 1] == ft->s->curw); + i + 1 < n && l[i + 1] == s->curw); if (i + 1 < n) - format_add_window_neighbor(nft, l[i + 1], ft->s, "next"); + format_add_window_neighbour(nft, l[i + 1], s, "next"); if (i > 0) - format_add_window_neighbor(nft, l[i - 1], ft->s, "prev"); + format_add_window_neighbour(nft, l[i - 1], s, "prev"); format_copy_state(&next, es, 0); next.ft = nft; From 4378a17fbec453a356f723ed971bd46fa8bbe33e Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 3 Jul 2026 18:14:45 +0000 Subject: [PATCH 025/127] Add floating panes key bindings and menu options, from Dane Jensen. --- key-bindings.c | 6 +++++- options-table.c | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/key-bindings.c b/key-bindings.c index e5a58a94e..641957103 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -59,12 +59,14 @@ " '#{?mouse_hyperlink,Type #[underscore]#{=/9/...:mouse_hyperlink},}' 'C-h' {copy-mode -q; send-keys -l -- \"#{q:mouse_hyperlink}\"}" \ " '#{?mouse_hyperlink,Copy #[underscore]#{=/9/...:mouse_hyperlink},}' 'h' {copy-mode -q; set-buffer -- \"#{q:mouse_hyperlink}\"}" \ " ''" \ + " '#{?#{#{pane_floating_flag}},Tile,}' 't' { join-pane }" \ + " '#{?#{!:#{pane_floating_flag}},Float,}' 'f' { break-pane -W }" \ " '#{?#{!:#{pane_floating_flag}},Horizontal Split,}' 'h' {split-window -h}" \ " '#{?#{!:#{pane_floating_flag}},Vertical Split,}' 'v' {split-window -v}" \ " ''" \ " '#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Up,}' 'u' {swap-pane -U}" \ " '#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Down,}' 'd' {swap-pane -D}" \ - " '#{?#{!:#{pane_floating_flag}},#{?pane_marked_set,,-}Swap Marked,}' 's' {swap-pane}" \ + " '#{?pane_marked_set,,-}Swap Marked' 's' {swap-pane}" \ " ''" \ " 'Kill' 'X' {kill-pane}" \ " 'Respawn' 'R' {respawn-pane -k}" \ @@ -362,6 +364,7 @@ key_bindings_init(void) "bind -N 'Kill current window' & { confirm-before -p\"kill-window #W? (y/n)\" kill-window }", "bind -N 'Prompt for window index to select' \"'\" { command-prompt -T window-target -pindex { select-window -t ':%%' } }", "bind -N 'New floating pane' * { new-pane }", + "bind -N 'Toggle pane between floating and tiled' @ { if -F '#{pane_floating_flag}' { join-pane } { break-pane -W } }", "bind -N 'Switch to previous client' ( { switch-client -p }", "bind -N 'Switch to next client' ) { switch-client -n }", "bind -N 'Rename current window' , { command-prompt -I'#W' { rename-window -- '%%' } }", @@ -479,6 +482,7 @@ key_bindings_init(void) /* Mouse button 1 down on default pane-border-format */ "bind -n MouseDown1Control9 { display-menu -t= -xM -yM -O -T 'Kill pane #{pane_index}?' 'Yes' 'y' { kill-pane -t= } 'No' 'n' {}}", "bind -n MouseDown1Control8 { resize-pane -Z }", + "bind -n MouseDown1Control7 { if -Ft= '#{pane_floating_flag}' { join-pane } { break-pane -W } }", /* Mouse wheel down on status line. */ "bind -n WheelDownStatus { next-window }", diff --git a/options-table.c b/options-table.c index 5019bb244..a5aff6a1f 100644 --- a/options-table.c +++ b/options-table.c @@ -1539,6 +1539,9 @@ const struct options_table_entry options_table[] = { "\"#{pane_title}\"" "#{?#{mouse}," "#[align=right]" + "#[range=control|7][" + "#{?#{pane_floating_flag},t,f}" + "]#[norange]" "#[range=control|8][" "#{?#{window_zoomed_flag},u,z}" "]#[norange]" From d512a65d4ccc64061821afb8843e57dc7dbdaf14 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 3 Jul 2026 18:28:57 +0000 Subject: [PATCH 026/127] Add O and V loops in formats for options and environment. --- format.c | 327 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- tmux.1 | 35 +++++- 2 files changed, 343 insertions(+), 19 deletions(-) diff --git a/format.c b/format.c index 9ec36c4c9..6757a4276 100644 --- a/format.c +++ b/format.c @@ -125,6 +125,8 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2) #define FORMAT_COLOUR_ESC_FG 0x8000000 #define FORMAT_COLOUR_ESC_BG 0x10000000 #define FORMAT_QUOTE_SHELL_SQ 0x20000000 +#define FORMAT_OPTIONS 0x40000000 +#define FORMAT_ENVIRON 0x80000000ULL /* Limit on recursion. */ #define FORMAT_LOOP_LIMIT 100 @@ -4203,7 +4205,7 @@ format_relative_time(time_t t) /* Find a format entry. */ static char * -format_find(struct format_tree *ft, const char *key, int modifiers, +format_find(struct format_tree *ft, const char *key, uint64_t modifiers, const char *time_format) { const struct format_table_entry *fte; @@ -4533,7 +4535,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, /* * Modifiers are a ; separated list of the forms: - * l,m,C,a,b,c,d,I,n,t,w,q,E,T,S,W,P,R,<,> + * l,m,C,a,b,c,d,I,n,t,w,q,E,T,S,W,P,O,V,R,<,> * =a * =/a * =/a/ @@ -4552,7 +4554,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, break; /* Check single character modifiers with no arguments. */ - if (strchr("labdnwETSWPL!<>", cp[0]) != NULL && + if (strchr("labdnwETSWPOVL!<>", cp[0]) != NULL && format_is_end(cp[1])) { format_add_modifier(&list, count, cp, 1, NULL, 0); cp++; @@ -4574,7 +4576,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, } /* Now try single character with arguments. */ - if (strchr("ImCLNPSst=pReqWc", cp[0]) == NULL) + if (strchr("ImCLNPSOVst=pReqWc", cp[0]) == NULL) break; c = cp[0]; @@ -4851,11 +4853,14 @@ format_loop_sessions(struct format_expand_state *es, const char *fmt) else use = all; nft = format_create(c, item, FORMAT_NONE, ft->flags); + format_add(nft, "loop_index", "%d", i); format_add(nft, "loop_last_flag", "%d", i == n - 1); + format_defaults(nft, ft->c, s, NULL, NULL); format_copy_state(&next, es, 0); next.ft = nft; + expanded = format_expand1(&next, use); format_free(next.ft); @@ -4970,24 +4975,29 @@ format_loop_windows(struct format_expand_state *es, const char *fmt) use = active; else use = all; - nft = format_create(c, item, FORMAT_WINDOW|w->id, - ft->flags); + nft = format_create(c, item, FORMAT_WINDOW|w->id, ft->flags); + format_add(nft, "loop_index", "%d", i); format_add(nft, "loop_last_flag", "%d", i == n - 1); - format_defaults(nft, ft->c, s, wl, NULL); /* Add neighbour window data to the format tree. */ - format_add(nft, "window_after_active", "%d", - i > 0 && l[i - 1] == s->curw); - format_add(nft, "window_before_active", "%d", - i + 1 < n && l[i + 1] == s->curw); + if (i > 0 && l[i - 1] == s->curw) + format_add(nft, "window_after_active", "1"); + else + format_add(nft, "window_after_active", "0"); + if (i + 1 < n && l[i + 1] == s->curw) + format_add(nft, "window_before_active", "1"); + else + format_add(nft, "window_before_active", "0"); if (i + 1 < n) format_add_window_neighbour(nft, l[i + 1], s, "next"); if (i > 0) format_add_window_neighbour(nft, l[i - 1], s, "prev"); + format_defaults(nft, ft->c, s, wl, NULL); format_copy_state(&next, es, 0); next.ft = nft; + expanded = format_expand1(&next, use); format_free(nft); @@ -5044,13 +5054,15 @@ format_loop_panes(struct format_expand_state *es, const char *fmt) use = active; else use = all; - nft = format_create(c, item, FORMAT_PANE|wp->id, - ft->flags); + nft = format_create(c, item, FORMAT_PANE|wp->id, ft->flags); + format_add(nft, "loop_index", "%d", i); format_add(nft, "loop_last_flag", "%d", i == n - 1); + format_defaults(nft, ft->c, ft->s, ft->wl, wp); format_copy_state(&next, es, 0); next.ft = nft; + expanded = format_expand1(&next, use); format_free(nft); @@ -5069,6 +5081,269 @@ format_loop_panes(struct format_expand_state *es, const char *fmt) return (value); } +/* Add an option to an options loop. */ +static void +format_loop_add_option(struct format_expand_state *es, const char *fmt, + struct evbuffer *buffer, struct options_entry *o, u_int n, u_int i) +{ + struct format_tree *ft = es->ft, *nft; + struct format_expand_state next; + const struct options_table_entry *oe = options_table_entry(o); + const char *name = options_name(o); + char *expanded, *s; + int is_array = options_is_array(o); + + format_log(es, "option loop: %s", name); + nft = format_create(ft->client, ft->item, FORMAT_NONE, ft->flags); + + format_add(nft, "option_name", "%s", name); + s = options_to_string(o, -1, 0); + format_add(nft, "option_value", "%s", s); + free(s); + + format_add(nft, "option_is_array", "%d", is_array); + format_add(nft, "option_array_index", "%s", ""); + format_add(nft, "option_array_first", "%d", is_array); + format_add(nft, "option_array_last", "%d", is_array); + format_add(nft, "option_array_count", "%u", n); + + if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)) + format_add(nft, "option_is_hook", "1"); + else + format_add(nft, "option_is_hook", "0"); + format_add(nft, "option_is_user", "%d", oe == NULL); + + if (options_next(o) == NULL) + format_add(nft, "loop_last_flag", "1"); + else + format_add(nft, "loop_last_flag", "0"); + format_add(nft, "loop_index", "%u", i); + + format_defaults(nft, ft->c, ft->s, ft->wl, ft->wp); + format_copy_state(&next, es, 0); + next.ft = nft; + + expanded = format_expand1(&next, fmt); + format_free(nft); + evbuffer_add(buffer, expanded, strlen(expanded)); + free(expanded); +} + +/* Add an array option item to an options loop. */ +static void +format_loop_add_array_item(struct format_expand_state *es, const char *fmt, + struct evbuffer *buffer, struct options_entry *o, + struct options_array_item *a, int n, u_int i) +{ + struct format_tree *ft = es->ft, *nft; + struct format_expand_state next; + const struct options_table_entry *oe = options_table_entry(o); + const char *name = options_name(o); + char *expanded, *s; + u_int idx; + + idx = options_array_item_index(a); + format_log(es, "option loop: %s[%u]", name, idx); + nft = format_create(ft->client, ft->item, FORMAT_NONE, ft->flags); + + format_add(nft, "option_name", "%s", name); + s = options_to_string(o, idx, 0); + format_add(nft, "option_value", "%s", s); + free(s); + + format_add(nft, "option_is_array", "1"); + format_add(nft, "option_array_index", "%u", idx); + if (a == options_array_first(o)) + format_add(nft, "option_array_first", "1"); + else + format_add(nft, "option_array_first", "0"); + if (options_array_next(a) == NULL) + format_add(nft, "option_array_last", "1"); + else + format_add(nft, "option_array_last", "0"); + format_add(nft, "option_array_count", "%u", n); + + if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)) + format_add(nft, "option_is_hook", "1"); + else + format_add(nft, "option_is_hook", "0"); + format_add(nft, "option_is_user", "%d", oe == NULL); + + if (options_array_next(a) == NULL && options_next(o) == NULL) + format_add(nft, "loop_last_flag", "1"); + else + format_add(nft, "loop_last_flag", "0"); + format_add(nft, "loop_index", "%u", i); + + format_defaults(nft, ft->c, ft->s, ft->wl, ft->wp); + format_copy_state(&next, es, 0); + next.ft = nft; + + expanded = format_expand1(&next, fmt); + format_free(nft); + evbuffer_add(buffer, expanded, strlen(expanded)); + free(expanded); +} + +/* Loop over options. */ +static char * +format_loop_options(struct format_expand_state *es, const char *fmt, + const char *flags) +{ + struct format_tree *ft = es->ft; + struct options *oo = NULL; + struct options_entry *o; + struct options_array_item *a; + char *value; + struct evbuffer *buffer; + size_t size; + u_int i = 0, n; + int global = 0; + + if (flags == NULL || *flags == '\0') + flags = "s"; + if (strchr(flags, 'v') != NULL) + oo = global_options; + else { + if (strchr(flags, 'g') != NULL) + global = 1; + if (strchr(flags, 'w') != NULL) { + if (global) + oo = global_w_options; + else if (ft->w != NULL) + oo = ft->w->options; + } else if (strchr(flags, 's') != NULL) { + if (global) + oo = global_s_options; + else if (ft->s != NULL) + oo = ft->s->options; + } else if (strchr(flags, 'p') != NULL) { + if (global) + /* invalid */; + else if (ft->wp != NULL) + oo = ft->wp->options; + } else if (global) + oo = global_s_options; + } + if (oo == NULL) + return (xstrdup("")); + + buffer = evbuffer_new(); + if (buffer == NULL) + fatalx("out of memory"); + + o = options_first(oo); + while (o != NULL) { + + n = 0; + if (options_is_array(o)) { + a = options_array_first(o); + while (a != NULL) { + n++; + a = options_array_next(a); + } + } + + if (!options_is_array(o) || n == 0) { + format_loop_add_option(es, fmt, buffer, o, n, i); + i++; + o = options_next(o); + continue; + } + + a = options_array_first(o); + while (a != NULL) { + format_loop_add_array_item(es, fmt, buffer, o, a, n, i); + i++; + a = options_array_next(a); + } + o = options_next(o); + } + + if ((size = EVBUFFER_LENGTH(buffer)) != 0) + value = xmemdup(EVBUFFER_DATA(buffer), size); + else + value = xstrdup(""); + evbuffer_free(buffer); + return (value); +} + +/* Loop over an environment. */ +static char * +format_loop_environ(struct format_expand_state *es, const char *fmt, + const char *flags) +{ + struct format_tree *ft = es->ft, *nft; + struct client *c = ft->client; + struct cmdq_item *item = ft->item; + struct format_expand_state next; + struct environ *env = NULL; + struct environ_entry *envent; + char *expanded, *value; + struct evbuffer *buffer; + size_t size; + u_int i = 0; + + if (flags == NULL || *flags == '\0' || strcmp(flags, "s") == 0) { + if (ft->s != NULL) + env = ft->s->environ; + } else if (strcmp(flags, "g") == 0) + env = global_environ; + else if (strcmp(flags, "c") == 0) { + if (ft->client != NULL) + env = ft->client->environ; + } + if (env == NULL) + return (xstrdup("")); + + buffer = evbuffer_new(); + if (buffer == NULL) + fatalx("out of memory"); + + envent = environ_first(env); + while (envent != NULL) { + format_log(es, "environment loop: %s", envent->name); + nft = format_create(c, item, FORMAT_NONE, ft->flags); + + format_add(nft, "environ_name", "%s", envent->name); + if (envent->value == NULL) + format_add(nft, "environ_value", "%s", ""); + else + format_add(nft, "environ_value", "%s", envent->value); + + if (envent->flags & ENVIRON_HIDDEN) + format_add(nft, "environ_hidden", "1"); + else + format_add(nft, "environ_hidden", "0"); + format_add(nft, "environ_removed", "%d", envent->value == NULL); + + if (environ_next(envent) == NULL) + format_add(nft, "loop_last_flag", "1"); + else + format_add(nft, "loop_last_flag", "0"); + format_add(nft, "loop_index", "%u", i); + + format_defaults(nft, ft->c, ft->s, ft->wl, ft->wp); + format_copy_state(&next, es, 0); + next.ft = nft; + + expanded = format_expand1(&next, fmt); + format_free(nft); + evbuffer_add(buffer, expanded, strlen(expanded)); + free(expanded); + + i++; + envent = environ_next(envent); + } + + if ((size = EVBUFFER_LENGTH(buffer)) != 0) + value = xmemdup(EVBUFFER_DATA(buffer), size); + else + value = xstrdup(""); + evbuffer_free(buffer); + return (value); +} + /* Loop over clients. */ static char * format_loop_clients(struct format_expand_state *es, const char *fmt) @@ -5093,14 +5368,16 @@ format_loop_clients(struct format_expand_state *es, const char *fmt) c = l[i]; format_log(es, "client loop: %s", c->name); nft = format_create(c, item, 0, ft->flags); + format_add(nft, "loop_index", "%d", i); format_add(nft, "loop_last_flag", "%d", i == n - 1); + format_defaults(nft, c, ft->s, ft->wl, ft->wp); format_copy_state(&next, es, 0); next.ft = nft; + expanded = format_expand1(&next, fmt); format_free(nft); - evbuffer_add(buffer, expanded, strlen(expanded)); free(expanded); } @@ -5270,12 +5547,14 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, char *copy0, *condition, *found, *new; char *value, *left, *right; size_t valuelen; - int modifiers = 0, limit = 0, width = 0; + uint64_t modifiers = 0; + int limit = 0, width = 0; int j, c; struct format_modifier *list, *cmp = NULL, *search = NULL; struct format_modifier **sub = NULL, *mexp = NULL, *fm; struct format_modifier *bool_op_n = NULL; u_int i, count, nsub = 0, nrep, check = 0; + const char *loop_flags = ""; struct format_expand_state next; struct environ_entry *envent; @@ -5473,6 +5752,16 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, else sc->reversed = 0; break; + case 'O': + modifiers |= FORMAT_OPTIONS; + if (fm->argc == 1) + loop_flags = fm->argv[0]; + break; + case 'V': + modifiers |= FORMAT_ENVIRON; + if (fm->argc == 1) + loop_flags = fm->argv[0]; + break; case 'L': modifiers |= FORMAT_CLIENTS; if (fm->argc < 1) { @@ -5608,6 +5897,14 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, value = format_loop_clients(es, copy); if (value == NULL) goto fail; + } else if (modifiers & FORMAT_OPTIONS) { + value = format_loop_options(es, copy, loop_flags); + if (value == NULL) + goto fail; + } else if (modifiers & FORMAT_ENVIRON) { + value = format_loop_environ(es, copy, loop_flags); + if (value == NULL) + goto fail; } else if (modifiers & FORMAT_WINDOW_NAME) { value = format_window_name(es, copy); if (value == NULL) diff --git a/tmux.1 b/tmux.1 index f672c0202..4eb9d9f08 100644 --- a/tmux.1 +++ b/tmux.1 @@ -3253,7 +3253,7 @@ If is floating and .Ar dst\-pane is either unspecified or equal to -.Ar src\-pane, +.Ar src\-pane , .Ar src\-pane is returned to its previous position in the layout. .Tg killp @@ -3849,7 +3849,7 @@ This command will automatically set to manual in the window options. .Tg respawnp .It Xo Ic respawn\-pane -.Op Fl Ek +.Op Fl \&Ek .Op Fl c Ar start\-directory .Op Fl e Ar environment .Op Fl t Ar target\-pane @@ -3878,7 +3878,7 @@ option has the same meaning as for the command. .Tg respawnw .It Xo Ic respawn\-window -.Op Fl Ek +.Op Fl \&Ek .Op Fl c Ar start\-directory .Op Fl e Ar environment .Op Fl t Ar target\-window @@ -6771,7 +6771,7 @@ will use shorter but less accurate time format for times in the past. .Ql r .Pq Ql t/r will show the time relative to the current time, for example -.Ql \1m +.Ql \&1m or .Ql 2m23s . A custom format may be given using an @@ -6897,6 +6897,33 @@ prefix, for example a user option on the next window is available as .Ql next_@color . .Pp +.Ql O:\& +will loop over each option; +array options are looped once for each array item. +.Ql O:\& +may be given a flag to choose the options table: +.Bl -column "Flag" "Table" -offset indent +.It Sy "Flag" Ta Sy "Table" +.It Li "s" Ta "session" +.It Li "w" Ta "window" +.It Li "p" Ta "pane" +.It Li "v" Ta "server" +.El +.Pp +.Ql g +chooses global options. +The default is +.Ql s . +.Ql V:\& +will loop over each environment variable. +Its flags are: +.Bl -column "Flag" "Environment" -offset indent +.It Sy "Flag" Ta Sy "Environment" +.It Li "s" Ta "session environment" +.It Li "g" Ta "global environment" +.It Li "c" Ta "client environment" +.El +.Pp .Ql N:\& checks if a window (without any suffix or with the .Ql w From 8e03310afe4afc02072b774ca3d36ae73b8f9df6 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 3 Jul 2026 22:58:23 +0000 Subject: [PATCH 027/127] Move monitor stuff out of control.c into its own file for reuse. --- Makefile | 1 + cmd-refresh-client.c | 12 +- control.c | 452 +++---------------------------------- monitor.c | 526 +++++++++++++++++++++++++++++++++++++++++++ tmux.h | 35 ++- 5 files changed, 590 insertions(+), 436 deletions(-) create mode 100644 monitor.c diff --git a/Makefile b/Makefile index ec1f2ff56..165d0d47c 100644 --- a/Makefile +++ b/Makefile @@ -95,6 +95,7 @@ SRCS= alerts.c \ log.c \ menu.c \ mode-tree.c \ + monitor.c \ names.c \ notify.c \ options-table.c \ diff --git a/cmd-refresh-client.c b/cmd-refresh-client.c index 13fcbb1dd..16269e249 100644 --- a/cmd-refresh-client.c +++ b/cmd-refresh-client.c @@ -47,7 +47,7 @@ static void cmd_refresh_client_update_subscription(struct client *tc, const char *value) { char *copy, *split, *name, *what; - enum control_sub_type subtype; + enum monitor_type subtype; int subid = -1; copy = name = xstrdup(value); @@ -63,15 +63,15 @@ cmd_refresh_client_update_subscription(struct client *tc, const char *value) *split++ = '\0'; if (strcmp(what, "%*") == 0) - subtype = CONTROL_SUB_ALL_PANES; + subtype = MONITOR_ALL_PANES; else if (sscanf(what, "%%%d", &subid) == 1 && subid >= 0) - subtype = CONTROL_SUB_PANE; + subtype = MONITOR_PANE; else if (strcmp(what, "@*") == 0) - subtype = CONTROL_SUB_ALL_WINDOWS; + subtype = MONITOR_ALL_WINDOWS; else if (sscanf(what, "@%d", &subid) == 1 && subid >= 0) - subtype = CONTROL_SUB_WINDOW; + subtype = MONITOR_WINDOW; else - subtype = CONTROL_SUB_SESSION; + subtype = MONITOR_SESSION; control_add_sub(tc, name, subtype, subid, split); out: diff --git a/control.c b/control.c index ab461ff10..eb2db98ef 100644 --- a/control.c +++ b/control.c @@ -76,42 +76,6 @@ struct control_pane { }; RB_HEAD(control_panes, control_pane); -/* Subscription pane. */ -struct control_sub_pane { - u_int pane; - u_int idx; - char *last; - - RB_ENTRY(control_sub_pane) entry; -}; -RB_HEAD(control_sub_panes, control_sub_pane); - -/* Subscription window. */ -struct control_sub_window { - u_int window; - u_int idx; - char *last; - - RB_ENTRY(control_sub_window) entry; -}; -RB_HEAD(control_sub_windows, control_sub_window); - -/* Control client subscription. */ -struct control_sub { - char *name; - char *format; - - enum control_sub_type type; - u_int id; - - char *last; - struct control_sub_panes panes; - struct control_sub_windows windows; - - RB_ENTRY(control_sub) entry; -}; -RB_HEAD(control_subs, control_sub); - /* Control client state. */ struct control_state { struct control_panes panes; @@ -124,8 +88,7 @@ struct control_state { struct bufferevent *read_event; struct bufferevent *write_event; - struct control_subs subs; - struct event subs_timer; + struct monitor_set *subs; }; /* Low and high watermarks. */ @@ -155,75 +118,6 @@ control_pane_cmp(struct control_pane *cp1, struct control_pane *cp2) } RB_GENERATE_STATIC(control_panes, control_pane, entry, control_pane_cmp); -/* Compare client subs. */ -static int -control_sub_cmp(struct control_sub *csub1, struct control_sub *csub2) -{ - return (strcmp(csub1->name, csub2->name)); -} -RB_GENERATE_STATIC(control_subs, control_sub, entry, control_sub_cmp); - -/* Compare client subscription panes. */ -static int -control_sub_pane_cmp(struct control_sub_pane *csp1, - struct control_sub_pane *csp2) -{ - if (csp1->pane < csp2->pane) - return (-1); - if (csp1->pane > csp2->pane) - return (1); - if (csp1->idx < csp2->idx) - return (-1); - if (csp1->idx > csp2->idx) - return (1); - return (0); -} -RB_GENERATE_STATIC(control_sub_panes, control_sub_pane, entry, - control_sub_pane_cmp); - -/* Compare client subscription windows. */ -static int -control_sub_window_cmp(struct control_sub_window *csw1, - struct control_sub_window *csw2) -{ - if (csw1->window < csw2->window) - return (-1); - if (csw1->window > csw2->window) - return (1); - if (csw1->idx < csw2->idx) - return (-1); - if (csw1->idx > csw2->idx) - return (1); - return (0); -} -RB_GENERATE_STATIC(control_sub_windows, control_sub_window, entry, - control_sub_window_cmp); - -/* Free a subscription. */ -static void -control_free_sub(struct control_state *cs, struct control_sub *csub) -{ - struct control_sub_pane *csp, *csp1; - struct control_sub_window *csw, *csw1; - - RB_FOREACH_SAFE(csp, control_sub_panes, &csub->panes, csp1) { - RB_REMOVE(control_sub_panes, &csub->panes, csp); - free(csp->last); - free(csp); - } - RB_FOREACH_SAFE(csw, control_sub_windows, &csub->windows, csw1) { - RB_REMOVE(control_sub_windows, &csub->windows, csw); - free(csw->last); - free(csw); - } - free(csub->last); - - RB_REMOVE(control_subs, &cs->subs, csub); - free(csub->name); - free(csub->format); - free(csub); -} - /* Free a block. */ static void control_free_block(struct control_state *cs, struct control_block *cb) @@ -767,6 +661,30 @@ control_write_callback(__unused struct bufferevent *bufev, void *data) bufferevent_disable(cs->write_event, EV_WRITE); } +/* Write a subscription change. */ +static void +control_sub_change(struct monitor_change *change, __unused void *data) +{ + struct client *c = change->c; + struct session *s = change->s; + struct winlink *wl = change->wl; + struct window_pane *wp = change->wp; + struct window *w; + + if (wp != NULL) { + w = wp->window; + control_write(c, "%%subscription-changed %s $%u @%u %u %%%u : %s", + change->name, s->id, w->id, wl->idx, wp->id, change->value); + } else if (wl != NULL) { + w = wl->window; + control_write(c, "%%subscription-changed %s $%u @%u %u - : %s", + change->name, s->id, w->id, wl->idx, change->value); + } else { + control_write(c, "%%subscription-changed %s $%u - - - : %s", + change->name, s->id, change->value); + } +} + /* Initialize for control mode. */ void control_start(struct client *c) @@ -784,7 +702,7 @@ control_start(struct client *c) RB_INIT(&cs->panes); TAILQ_INIT(&cs->pending_list); TAILQ_INIT(&cs->all_blocks); - RB_INIT(&cs->subs); + cs->subs = monitor_create(c, control_sub_change, NULL); cs->read_event = bufferevent_new(c->fd, control_read_callback, control_write_callback, control_error_callback, c); @@ -833,20 +751,16 @@ control_stop(struct client *c) { struct control_state *cs = c->control_state; struct control_block *cb, *cb1; - struct control_sub *csub, *csub1; if (cs == NULL) return; + monitor_destroy(cs->subs); + if (~c->flags & CLIENT_CONTROLCONTROL) bufferevent_free(cs->write_event); bufferevent_free(cs->read_event); - RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) - control_free_sub(cs, csub); - if (evtimer_initialized(&cs->subs_timer)) - evtimer_del(&cs->subs_timer); - control_reset_offsets(c); TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1) control_free_block(cs, cb); @@ -855,313 +769,14 @@ control_stop(struct client *c) free(cs); } -/* Check session subscription. */ -static void -control_check_subs_session(struct client *c, struct control_sub *csub, - struct format_tree *ft) -{ - struct session *s = c->session; - char *value; - - value = format_expand(ft, csub->format); - - if (csub->last != NULL && strcmp(value, csub->last) == 0) { - free(value); - return; - } - control_write(c, - "%%subscription-changed %s $%u - - - : %s", - csub->name, s->id, value); - free(csub->last); - csub->last = value; -} - -/* Check pane subscription. */ -static void -control_check_subs_pane(struct client *c, struct control_sub *csub) -{ - struct session *s = c->session; - struct window_pane *wp; - struct window *w; - struct winlink *wl; - struct format_tree *ft; - char *value; - struct control_sub_pane *csp, find; - - wp = window_pane_find_by_id(csub->id); - if (wp == NULL || wp->fd == -1) - return; - w = wp->window; - - TAILQ_FOREACH(wl, &w->winlinks, wentry) { - if (wl->session != s) - continue; - - ft = format_create_defaults(NULL, c, s, wl, wp); - value = format_expand(ft, csub->format); - format_free(ft); - - find.pane = wp->id; - find.idx = wl->idx; - - csp = RB_FIND(control_sub_panes, &csub->panes, &find); - if (csp == NULL) { - csp = xcalloc(1, sizeof *csp); - csp->pane = wp->id; - csp->idx = wl->idx; - RB_INSERT(control_sub_panes, &csub->panes, csp); - } - - if (csp->last != NULL && strcmp(value, csp->last) == 0) { - free(value); - continue; - } - control_write(c, - "%%subscription-changed %s $%u @%u %u %%%u : %s", - csub->name, s->id, w->id, wl->idx, wp->id, value); - free(csp->last); - csp->last = value; - } -} - -/* Check all-panes subscription for a pane. */ -static void -control_check_subs_all_panes_one(struct client *c, struct control_sub *csub, - struct format_tree *ft, struct winlink *wl, struct window_pane *wp) -{ - struct session *s = c->session; - struct window *w = wl->window; - char *value; - struct control_sub_pane *csp, find; - - value = format_expand(ft, csub->format); - - find.pane = wp->id; - find.idx = wl->idx; - - csp = RB_FIND(control_sub_panes, &csub->panes, &find); - if (csp == NULL) { - csp = xcalloc(1, sizeof *csp); - csp->pane = wp->id; - csp->idx = wl->idx; - RB_INSERT(control_sub_panes, &csub->panes, csp); - } - - if (csp->last != NULL && strcmp(value, csp->last) == 0) { - free(value); - return; - } - control_write(c, - "%%subscription-changed %s $%u @%u %u %%%u : %s", - csub->name, s->id, w->id, wl->idx, wp->id, value); - free(csp->last); - csp->last = value; -} - -/* Check window subscription. */ -static void -control_check_subs_window(struct client *c, struct control_sub *csub) -{ - struct session *s = c->session; - struct window *w; - struct winlink *wl; - struct format_tree *ft; - char *value; - struct control_sub_window *csw, find; - - w = window_find_by_id(csub->id); - if (w == NULL) - return; - - TAILQ_FOREACH(wl, &w->winlinks, wentry) { - if (wl->session != s) - continue; - - ft = format_create_defaults(NULL, c, s, wl, NULL); - value = format_expand(ft, csub->format); - format_free(ft); - - find.window = w->id; - find.idx = wl->idx; - - csw = RB_FIND(control_sub_windows, &csub->windows, &find); - if (csw == NULL) { - csw = xcalloc(1, sizeof *csw); - csw->window = w->id; - csw->idx = wl->idx; - RB_INSERT(control_sub_windows, &csub->windows, csw); - } - - if (csw->last != NULL && strcmp(value, csw->last) == 0) { - free(value); - continue; - } - control_write(c, - "%%subscription-changed %s $%u @%u %u - : %s", - csub->name, s->id, w->id, wl->idx, value); - free(csw->last); - csw->last = value; - } -} - -/* Check all-windows subscription for a window. */ -static void -control_check_subs_all_windows_one(struct client *c, struct control_sub *csub, - struct format_tree *ft, struct winlink *wl) -{ - struct session *s = c->session; - struct window *w = wl->window; - char *value; - struct control_sub_window *csw, find; - - value = format_expand(ft, csub->format); - - find.window = w->id; - find.idx = wl->idx; - - csw = RB_FIND(control_sub_windows, &csub->windows, &find); - if (csw == NULL) { - csw = xcalloc(1, sizeof *csw); - csw->window = w->id; - csw->idx = wl->idx; - RB_INSERT(control_sub_windows, &csub->windows, csw); - } - - if (csw->last != NULL && strcmp(value, csw->last) == 0) { - free(value); - return; - } - control_write(c, - "%%subscription-changed %s $%u @%u %u - : %s", - csub->name, s->id, w->id, wl->idx, value); - free(csw->last); - csw->last = value; -} - -/* Check subscriptions timer. */ -static void -control_check_subs_timer(__unused int fd, __unused short events, void *data) -{ - struct client *c = data; - struct control_state *cs = c->control_state; - struct control_sub *csub, *csub1; - struct session *s = c->session; - struct format_tree *ft; - struct winlink *wl; - struct window_pane *wp; - struct timeval tv = { .tv_sec = 1 }; - int have_session = 0, have_all_panes = 0; - int have_all_windows = 0; - - log_debug("%s: timer fired", __func__); - evtimer_add(&cs->subs_timer, &tv); - - if (s == NULL) - return; - - /* Find which subscription types are present. */ - RB_FOREACH(csub, control_subs, &cs->subs) { - switch (csub->type) { - case CONTROL_SUB_SESSION: - have_session = 1; - break; - case CONTROL_SUB_ALL_PANES: - have_all_panes = 1; - break; - case CONTROL_SUB_ALL_WINDOWS: - have_all_windows = 1; - break; - default: - break; - } - } - - /* Check session subscriptions. */ - if (have_session) { - ft = format_create_defaults(NULL, c, s, NULL, NULL); - RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) { - if (csub->type == CONTROL_SUB_SESSION) - control_check_subs_session(c, csub, ft); - } - format_free(ft); - } - - /* Check pane and window subscriptions. */ - RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) { - switch (csub->type) { - case CONTROL_SUB_PANE: - control_check_subs_pane(c, csub); - break; - case CONTROL_SUB_WINDOW: - control_check_subs_window(c, csub); - break; - case CONTROL_SUB_SESSION: - case CONTROL_SUB_ALL_PANES: - case CONTROL_SUB_ALL_WINDOWS: - break; - } - } - - /* Check all-panes subscriptions. */ - if (have_all_panes) { - RB_FOREACH(wl, winlinks, &s->windows) { - TAILQ_FOREACH(wp, &wl->window->panes, entry) { - ft = format_create_defaults(NULL, c, s, wl, wp); - RB_FOREACH_SAFE(csub, control_subs, &cs->subs, - csub1) { - if (csub->type != CONTROL_SUB_ALL_PANES) - continue; - control_check_subs_all_panes_one(c, - csub, ft, wl, wp); - } - format_free(ft); - } - } - } - - /* Check all-windows subscriptions. */ - if (have_all_windows) { - RB_FOREACH(wl, winlinks, &s->windows) { - ft = format_create_defaults(NULL, c, s, wl, NULL); - RB_FOREACH_SAFE(csub, control_subs, &cs->subs, - csub1) { - if (csub->type != CONTROL_SUB_ALL_WINDOWS) - continue; - control_check_subs_all_windows_one(c, csub, ft, - wl); - } - format_free(ft); - } - } -} - /* Add a subscription. */ void -control_add_sub(struct client *c, const char *name, enum control_sub_type type, +control_add_sub(struct client *c, const char *name, enum monitor_type type, int id, const char *format) { struct control_state *cs = c->control_state; - struct control_sub *csub, find; - struct timeval tv = { .tv_sec = 1 }; - find.name = (char *)name; - if ((csub = RB_FIND(control_subs, &cs->subs, &find)) != NULL) - control_free_sub(cs, csub); - - csub = xcalloc(1, sizeof *csub); - csub->name = xstrdup(name); - csub->type = type; - csub->id = id; - csub->format = xstrdup(format); - RB_INSERT(control_subs, &cs->subs, csub); - - RB_INIT(&csub->panes); - RB_INIT(&csub->windows); - - if (!evtimer_initialized(&cs->subs_timer)) - evtimer_set(&cs->subs_timer, control_check_subs_timer, c); - if (!evtimer_pending(&cs->subs_timer, NULL)) - evtimer_add(&cs->subs_timer, &tv); + monitor_add(cs->subs, name, type, id, format); } /* Remove a subscription. */ @@ -1169,11 +784,6 @@ void control_remove_sub(struct client *c, const char *name) { struct control_state *cs = c->control_state; - struct control_sub *csub, find; - find.name = (char *)name; - if ((csub = RB_FIND(control_subs, &cs->subs, &find)) != NULL) - control_free_sub(cs, csub); - if (RB_EMPTY(&cs->subs)) - evtimer_del(&cs->subs_timer); + monitor_remove(cs->subs, name); } diff --git a/monitor.c b/monitor.c new file mode 100644 index 000000000..512331e6a --- /dev/null +++ b/monitor.c @@ -0,0 +1,526 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include + +#include "tmux.h" + +/* Subscription pane. */ +struct monitor_pane { + u_int pane; + u_int idx; + char *last; + + RB_ENTRY(monitor_pane) entry; +}; +RB_HEAD(monitor_panes, monitor_pane); + +/* Subscription window. */ +struct monitor_window { + u_int window; + u_int idx; + char *last; + + RB_ENTRY(monitor_window) entry; +}; +RB_HEAD(monitor_windows, monitor_window); + +/* Subscription. */ +struct monitor_item { + char *name; + char *format; + + enum monitor_type type; + u_int id; + + char *last; + struct monitor_panes panes; + struct monitor_windows windows; + + RB_ENTRY(monitor_item) entry; +}; +RB_HEAD(monitor_items, monitor_item); + +/* Monitored subscription set. */ +struct monitor_set { + struct client *client; + monitor_cb cb; + void *data; + + struct monitor_items items; + struct event timer; +}; + +static void monitor_timer(__unused int, __unused short, void *); + +/* Compare subscriptions. */ +static int +monitor_item_cmp(struct monitor_item *m1, struct monitor_item *m2) +{ + return (strcmp(m1->name, m2->name)); +} +RB_GENERATE_STATIC(monitor_items, monitor_item, entry, monitor_item_cmp); + +/* Compare subscription panes. */ +static int +monitor_pane_cmp(struct monitor_pane *mp1, struct monitor_pane *mp2) +{ + if (mp1->pane < mp2->pane) + return (-1); + if (mp1->pane > mp2->pane) + return (1); + if (mp1->idx < mp2->idx) + return (-1); + if (mp1->idx > mp2->idx) + return (1); + return (0); +} +RB_GENERATE_STATIC(monitor_panes, monitor_pane, entry, monitor_pane_cmp); + +/* Compare subscription windows. */ +static int +monitor_window_cmp(struct monitor_window *mw1, struct monitor_window *mw2) +{ + if (mw1->window < mw2->window) + return (-1); + if (mw1->window > mw2->window) + return (1); + if (mw1->idx < mw2->idx) + return (-1); + if (mw1->idx > mw2->idx) + return (1); + return (0); +} +RB_GENERATE_STATIC(monitor_windows, monitor_window, entry, monitor_window_cmp); + +/* Free a subscription. */ +static void +monitor_free_item(struct monitor_set *ms, struct monitor_item *me) +{ + struct monitor_pane *mp, *mp1; + struct monitor_window *mw, *mw1; + + RB_FOREACH_SAFE(mp, monitor_panes, &me->panes, mp1) { + RB_REMOVE(monitor_panes, &me->panes, mp); + free(mp->last); + free(mp); + } + RB_FOREACH_SAFE(mw, monitor_windows, &me->windows, mw1) { + RB_REMOVE(monitor_windows, &me->windows, mw); + free(mw->last); + free(mw); + } + free(me->last); + + RB_REMOVE(monitor_items, &ms->items, me); + free(me->name); + free(me->format); + free(me); +} + +/* Report a changed value. */ +static void +monitor_report(struct monitor_set *ms, struct monitor_item *me, + struct session *s, struct winlink *wl, struct window_pane *wp, + const char *value) +{ + struct monitor_change change; + + change.name = me->name; + change.value = value; + change.c = ms->client; + change.s = s; + change.wl = wl; + change.wp = wp; + log_debug("%s: %s changed to %s", __func__, me->name, value); + ms->cb(&change, ms->data); +} + +/* Check session subscription. */ +static void +monitor_check_session(struct monitor_set *ms, struct monitor_item *me, + struct format_tree *ft) +{ + struct session *s = ms->client->session; + char *value; + + value = format_expand(ft, me->format); + + if (me->last != NULL && strcmp(value, me->last) == 0) { + free(value); + return; + } + + monitor_report(ms, me, s, NULL, NULL, value); + free(me->last); + me->last = value; +} + +/* Check pane subscription. */ +static void +monitor_check_pane(struct monitor_set *ms, struct monitor_item *me) +{ + struct client *c = ms->client; + struct session *s = c->session; + struct window_pane *wp; + struct window *w; + struct winlink *wl; + struct format_tree *ft; + char *value; + struct monitor_pane *mp, find; + + wp = window_pane_find_by_id(me->id); + if (wp == NULL || wp->fd == -1) + return; + w = wp->window; + + TAILQ_FOREACH(wl, &w->winlinks, wentry) { + if (wl->session != s) + continue; + + ft = format_create_defaults(NULL, c, s, wl, wp); + value = format_expand(ft, me->format); + format_free(ft); + + find.pane = wp->id; + find.idx = wl->idx; + mp = RB_FIND(monitor_panes, &me->panes, &find); + if (mp == NULL) { + mp = xcalloc(1, sizeof *mp); + mp->pane = wp->id; + mp->idx = wl->idx; + RB_INSERT(monitor_panes, &me->panes, mp); + } + + if (mp->last != NULL && strcmp(value, mp->last) == 0) { + free(value); + continue; + } + + monitor_report(ms, me, s, wl, wp, value); + free(mp->last); + mp->last = value; + } +} + +/* Check one all-panes subscription. */ +static void +monitor_check_all_panes_one(struct monitor_set *ms, struct monitor_item *me, + struct format_tree *ft, struct winlink *wl, struct window_pane *wp) +{ + struct session *s = ms->client->session; + char *value; + struct monitor_pane *mp, find; + + value = format_expand(ft, me->format); + + find.pane = wp->id; + find.idx = wl->idx; + mp = RB_FIND(monitor_panes, &me->panes, &find); + if (mp == NULL) { + mp = xcalloc(1, sizeof *mp); + mp->pane = wp->id; + mp->idx = wl->idx; + RB_INSERT(monitor_panes, &me->panes, mp); + } + + if (mp->last != NULL && strcmp(value, mp->last) == 0) { + free(value); + return; + } + + monitor_report(ms, me, s, wl, wp, value); + free(mp->last); + mp->last = value; +} + +/* Check window subscription. */ +static void +monitor_check_window(struct monitor_set *ms, struct monitor_item *me) +{ + struct client *c = ms->client; + struct session *s = c->session; + struct window *w; + struct winlink *wl; + struct format_tree *ft; + char *value; + struct monitor_window *mw, find; + + w = window_find_by_id(me->id); + if (w == NULL) + return; + + TAILQ_FOREACH(wl, &w->winlinks, wentry) { + if (wl->session != s) + continue; + + ft = format_create_defaults(NULL, c, s, wl, NULL); + value = format_expand(ft, me->format); + format_free(ft); + + find.window = w->id; + find.idx = wl->idx; + mw = RB_FIND(monitor_windows, &me->windows, &find); + if (mw == NULL) { + mw = xcalloc(1, sizeof *mw); + mw->window = w->id; + mw->idx = wl->idx; + RB_INSERT(monitor_windows, &me->windows, mw); + } + + if (mw->last != NULL && strcmp(value, mw->last) == 0) { + free(value); + continue; + } + + monitor_report(ms, me, s, wl, NULL, value); + free(mw->last); + mw->last = value; + } +} + +/* Check one all-windows subscription. */ +static void +monitor_check_all_windows_one(struct monitor_set *ms, struct monitor_item *me, + struct format_tree *ft, struct winlink *wl) +{ + struct session *s = ms->client->session; + struct window *w = wl->window; + char *value; + struct monitor_window *mw, find; + + value = format_expand(ft, me->format); + + find.window = w->id; + find.idx = wl->idx; + mw = RB_FIND(monitor_windows, &me->windows, &find); + if (mw == NULL) { + mw = xcalloc(1, sizeof *mw); + mw->window = w->id; + mw->idx = wl->idx; + RB_INSERT(monitor_windows, &me->windows, mw); + } + + if (mw->last != NULL && strcmp(value, mw->last) == 0) { + free(value); + return; + } + + monitor_report(ms, me, s, wl, NULL, value); + free(mw->last); + mw->last = value; +} + +/* Check session subscriptions. */ +static void +monitor_check_sessions(struct monitor_set *ms) +{ + struct client *c = ms->client; + struct session *s = c->session; + struct monitor_item *me, *me1; + struct format_tree *ft; + + ft = format_create_defaults(NULL, c, s, NULL, NULL); + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { + if (me->type == MONITOR_SESSION) + monitor_check_session(ms, me, ft); + } + format_free(ft); +} + +/* Check pane and window subscriptions. */ +static void +monitor_check_panes_windows(struct monitor_set *ms) +{ + struct monitor_item *me, *me1; + + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { + switch (me->type) { + case MONITOR_PANE: + monitor_check_pane(ms, me); + break; + case MONITOR_WINDOW: + monitor_check_window(ms, me); + break; + case MONITOR_SESSION: + case MONITOR_ALL_PANES: + case MONITOR_ALL_WINDOWS: + break; + } + } +} + +/* Check all-panes subscriptions. */ +static void +monitor_check_all_panes(struct monitor_set *ms) +{ + struct client *c = ms->client; + struct session *s = c->session; + struct monitor_item *me, *me1; + struct window_pane *wp; + struct format_tree *ft; + struct winlink *wl; + + RB_FOREACH(wl, winlinks, &s->windows) { + TAILQ_FOREACH(wp, &wl->window->panes, entry) { + ft = format_create_defaults(NULL, c, s, wl, wp); + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { + if (me->type != MONITOR_ALL_PANES) + continue; + monitor_check_all_panes_one(ms, me, ft, wl, wp); + } + format_free(ft); + } + } +} + +/* Check all-windows subscriptions. */ +static void +monitor_check_all_windows(struct monitor_set *ms) +{ + struct client *c = ms->client; + struct session *s = c->session; + struct monitor_item *me, *me1; + struct format_tree *ft; + struct winlink *wl; + + RB_FOREACH(wl, winlinks, &s->windows) { + ft = format_create_defaults(NULL, c, s, wl, NULL); + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { + if (me->type != MONITOR_ALL_WINDOWS) + continue; + monitor_check_all_windows_one(ms, me, ft, wl); + } + format_free(ft); + } +} + +/* Check subscriptions. */ +static void +monitor_timer(__unused int fd, __unused short events, void *data) +{ + struct monitor_set *ms = data; + struct client *c = ms->client; + struct monitor_item *me; + struct timeval tv = { .tv_sec = 1 }; + int have_session = 0, have_all_panes = 0; + int have_all_windows = 0; + + log_debug("%s: timer fired", __func__); + evtimer_add(&ms->timer, &tv); + + if (c->session == NULL) + return; + + RB_FOREACH(me, monitor_items, &ms->items) { + switch (me->type) { + case MONITOR_SESSION: + have_session = 1; + break; + case MONITOR_ALL_PANES: + have_all_panes = 1; + break; + case MONITOR_ALL_WINDOWS: + have_all_windows = 1; + break; + case MONITOR_PANE: + case MONITOR_WINDOW: + break; + } + } + + if (have_session) + monitor_check_sessions(ms); + monitor_check_panes_windows(ms); + if (have_all_panes) + monitor_check_all_panes(ms); + if (have_all_windows) + monitor_check_all_windows(ms); +} + +/* Create a monitor set. */ +struct monitor_set * +monitor_create(struct client *c, monitor_cb cb, void *data) +{ + struct monitor_set *ms; + + ms = xcalloc(1, sizeof *ms); + ms->client = c; + ms->cb = cb; + ms->data = data; + RB_INIT(&ms->items); + return (ms); +} + +/* Destroy a monitor set. */ +void +monitor_destroy(struct monitor_set *ms) +{ + struct monitor_item *me, *me1; + + if (ms == NULL) + return; + + if (evtimer_initialized(&ms->timer)) + evtimer_del(&ms->timer); + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) + monitor_free_item(ms, me); + free(ms); +} + +/* Add a subscription. */ +void +monitor_add(struct monitor_set *ms, const char *name, enum monitor_type type, + int id, const char *format) +{ + struct monitor_item *me, find = { .name = (char *)name }; + struct timeval tv = { .tv_sec = 1 }; + + if ((me = RB_FIND(monitor_items, &ms->items, &find)) != NULL) + monitor_free_item(ms, me); + + me = xcalloc(1, sizeof *me); + me->name = xstrdup(name); + me->format = xstrdup(format); + me->type = type; + me->id = id; + RB_INIT(&me->panes); + RB_INIT(&me->windows); + RB_INSERT(monitor_items, &ms->items, me); + + if (!evtimer_initialized(&ms->timer)) + evtimer_set(&ms->timer, monitor_timer, ms); + if (!evtimer_pending(&ms->timer, NULL)) + evtimer_add(&ms->timer, &tv); +} + +/* Remove a subscription. */ +void +monitor_remove(struct monitor_set *ms, const char *name) +{ + struct monitor_item *me, find = { .name = (char *)name }; + + if ((me = RB_FIND(monitor_items, &ms->items, &find)) != NULL) + monitor_free_item(ms, me); + if (RB_EMPTY(&ms->items) && evtimer_initialized(&ms->timer)) + evtimer_del(&ms->timer); +} diff --git a/tmux.h b/tmux.h index 62763a6f4..9fa174a82 100644 --- a/tmux.h +++ b/tmux.h @@ -2274,14 +2274,24 @@ struct client { }; TAILQ_HEAD(clients, client); -/* Control mode subscription type. */ -enum control_sub_type { - CONTROL_SUB_SESSION, - CONTROL_SUB_PANE, - CONTROL_SUB_ALL_PANES, - CONTROL_SUB_WINDOW, - CONTROL_SUB_ALL_WINDOWS +/* Monitor. */ +enum monitor_type { + MONITOR_SESSION, + MONITOR_PANE, + MONITOR_ALL_PANES, + MONITOR_WINDOW, + MONITOR_ALL_WINDOWS }; +struct monitor_change { + const char *name; + const char *value; + + struct client *c; + struct session *s; + struct winlink *wl; + struct window_pane *wp; +}; +typedef void (*monitor_cb)(struct monitor_change *, void *); /* Key binding and key table. */ struct key_binding { @@ -3794,6 +3804,13 @@ void check_window_name(struct window *); char *default_window_name(struct window *); char *parse_window_name(const char *); +/* monitor.c */ +struct monitor_set *monitor_create(struct client *, monitor_cb, void *); +void monitor_destroy(struct monitor_set *); +void monitor_add(struct monitor_set *, const char *, enum monitor_type, int, + const char *); +void monitor_remove(struct monitor_set *, const char *); + /* control.c */ void control_discard(struct client *); void control_start(struct client *); @@ -3809,8 +3826,8 @@ void control_reset_offsets(struct client *); void printflike(2, 3) control_write(struct client *, const char *, ...); void control_write_output(struct client *, struct window_pane *); int control_all_done(struct client *); -void control_add_sub(struct client *, const char *, enum control_sub_type, - int, const char *); +void control_add_sub(struct client *, const char *, enum monitor_type, int, + const char *); void control_remove_sub(struct client *, const char *); /* control-notify.c */ From 1d7021130de92e87676dc10a6c202d3eb037cb3c Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sat, 4 Jul 2026 09:15:32 +0100 Subject: [PATCH 028/127] Set ASAN_OPTIONS in regress Makefile. --- regress/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/regress/Makefile b/regress/Makefile index 5f8d50c8f..ff58eef40 100644 --- a/regress/Makefile +++ b/regress/Makefile @@ -8,6 +8,7 @@ all: for test in $(TESTS); do \ printf '%-40s ' "$$test"; \ start=$$(date +%s); \ + ASAN_OPTIONS="abort_on_error=1:detect_leaks=0:$$ASAN_OPTIONS"; \ if env -i ASAN_OPTIONS="$$ASAN_OPTIONS" sh "$$test" >/dev/null 2>&1; then \ end=$$(date +%s); \ echo "PASS ($$((end - start))s)"; \ From c5a7309966e916ff33f6cdf7e292f33965b0a6a5 Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 4 Jul 2026 08:34:16 +0000 Subject: [PATCH 029/127] Dirty ED 2 correctly, GiHub issue 5322 from git at twilligon dot com. --- screen-write.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/screen-write.c b/screen-write.c index b725af6e5..c1192e8ae 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1992,7 +1992,7 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg) screen_write_collect_clear(ctx, 0, sy); - if (!screen_write_should_draw_lines(ctx, s->cy, sy - s->cy)) + if (!screen_write_should_draw_lines(ctx, 0, sy)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED) { tty_write(tty_cmd_clearscreen, &ttyctx); From a02e25ba01ebe85125c198b5115195e0d837d756 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sat, 4 Jul 2026 17:00:28 +0100 Subject: [PATCH 030/127] Do not invoke clear check on respawn. --- grid.c | 30 ------------------------------ screen.c | 7 ++++--- spawn.c | 2 +- tmux.h | 2 +- 4 files changed, 6 insertions(+), 35 deletions(-) diff --git a/grid.c b/grid.c index bc7abd4ed..d03352f08 100644 --- a/grid.c +++ b/grid.c @@ -60,21 +60,6 @@ static const struct grid_cell_entry grid_cleared_entry = { }; #ifdef __APPLE__ -static void -grid_check_lines(struct grid *gd) -{ - u_int i, j; - - for (i = 0; i < gd->hsize + gd->sy; i++) { - for (j = i + 1; j < gd->hsize + gd->sy; j++) { - if (gd->linedata[i].celldata != NULL) - assert(gd->linedata[i].celldata != gd->linedata[j].celldata); - if (gd->linedata[i].extddata != NULL) - assert(gd->linedata[i].extddata != gd->linedata[j].extddata); - } - } -} - void grid_check_is_clear(struct grid *gd) { @@ -104,11 +89,6 @@ grid_check_is_clear(struct grid *gd) } } #else -static void -grid_check_lines(__unused struct grid *gd) -{ -} - void grid_check_is_clear(__unused struct grid *gd) { @@ -506,8 +486,6 @@ grid_scroll_history(struct grid *gd, u_int bg) gd->linedata[gd->hsize].time = current_time; gd->hsize++; gd->scroll_added++; - - grid_check_lines(gd); } /* Clear the history. */ @@ -557,8 +535,6 @@ grid_scroll_history_region(struct grid *gd, u_int upper, u_int lower, u_int bg) gd->hscrolled++; gd->hsize++; gd->scroll_added++; - - grid_check_lines(gd); } /* Expand line to fit to cell. */ @@ -820,8 +796,6 @@ grid_move_lines(struct grid *gd, u_int dy, u_int py, u_int ny, u_int bg) } if (py != 0 && (py < dy || py >= dy + ny)) gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED; - - grid_check_lines(gd); } /* Move a group of cells. */ @@ -1311,8 +1285,6 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy, sy++; dy++; } - - grid_check_lines(dst); } /* Mark line as dead. */ @@ -1609,8 +1581,6 @@ grid_reflow(struct grid *gd, u_int sx) gd->linedata = target->linedata; free(target); gd->scroll_generation++; - - grid_check_lines(gd); } /* Convert to position based on wrapped lines. */ diff --git a/screen.c b/screen.c index f3fdfcbd4..62a079270 100644 --- a/screen.c +++ b/screen.c @@ -99,12 +99,12 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit) s->write_list = NULL; s->hyperlinks = NULL; - screen_reinit(s); + screen_reinit(s, 1); } /* Reinitialise screen. */ void -screen_reinit(struct screen *s) +screen_reinit(struct screen *s, int check) { s->cx = 0; s->cy = 0; @@ -123,7 +123,8 @@ screen_reinit(struct screen *s) s->saved_cy = UINT_MAX; screen_reset_tabs(s); - grid_check_is_clear(s->grid); + if (check) + grid_check_is_clear(s->grid); grid_clear_lines(s->grid, s->grid->hsize, s->grid->sy, 8); screen_clear_selection(s); diff --git a/spawn.c b/spawn.c index e1ef0090b..3ae9153b8 100644 --- a/spawn.c +++ b/spawn.c @@ -273,7 +273,7 @@ spawn_pane(struct spawn_context *sc, char **cause) sc->wp0->fd = -1; } window_pane_reset_mode_all(sc->wp0); - screen_reinit(&sc->wp0->base); + screen_reinit(&sc->wp0->base, 0); if (sc->wp0->ictx != NULL) { input_free(sc->wp0->ictx); sc->wp0->ictx = NULL; diff --git a/tmux.h b/tmux.h index 503c551a1..f49ab377d 100644 --- a/tmux.h +++ b/tmux.h @@ -3502,7 +3502,7 @@ int redraw_get_status_border_cell_type(struct redraw_span **, u_int); /* screen.c */ void screen_init(struct screen *, u_int, u_int, u_int); -void screen_reinit(struct screen *); +void screen_reinit(struct screen *, int); void screen_free(struct screen *); void screen_reset_tabs(struct screen *); void screen_reset_hyperlinks(struct screen *); From 21e91cfa0079e2abcda9b5cc3ac177c54ac46657 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Sat, 4 Jul 2026 17:07:31 +0100 Subject: [PATCH 031/127] portable: add monitor.c --- Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile.am b/Makefile.am index eeb24f821..4eda7151b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -178,6 +178,7 @@ dist_tmux_SOURCES = \ log.c \ menu.c \ mode-tree.c \ + monitor.c \ names.c \ notify.c \ options-table.c \ From 646637f53ab7c64eb8edcb86eb0f73fc4c3a19b8 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 3 Jul 2026 23:45:22 +0100 Subject: [PATCH 032/127] Add control subs test. --- regress/control-subscriptions.sh | 172 +++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 regress/control-subscriptions.sh diff --git a/regress/control-subscriptions.sh b/regress/control-subscriptions.sh new file mode 100644 index 000000000..25c732ac0 --- /dev/null +++ b/regress/control-subscriptions.sh @@ -0,0 +1,172 @@ +#!/bin/sh + +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) +TMUX="$TEST_TMUX -Ltest -f/dev/null" + +TMPDIR=$(mktemp -d) +IN="$TMPDIR/in" +OUT="$TMPDIR/out" +PID= + +cleanup() +{ + [ -n "$PID" ] && kill "$PID" 2>/dev/null + $TMUX kill-server 2>/dev/null + rm -rf "$TMPDIR" +} +trap cleanup EXIT + +wait_for() +{ + pattern=$1 + timeout=${2:-6} + i=0 + + while [ "$i" -lt "$timeout" ]; do + if grep -F -- "$pattern" "$OUT" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + i=$((i + 1)) + done + echo "missing: $pattern" + cat "$OUT" + return 1 +} + +reject_for() +{ + pattern=$1 + timeout=${2:-3} + i=0 + + while [ "$i" -lt "$timeout" ]; do + if grep -F -- "$pattern" "$OUT" >/dev/null 2>&1; then + echo "unexpected: $pattern" + cat "$OUT" + return 1 + fi + sleep 1 + i=$((i + 1)) + done + return 0 +} + +wait_tmux() +{ + target=$1 + timeout=${2:-6} + i=0 + + while [ "$i" -lt "$timeout" ]; do + if $TMUX display-message -p -t "$target" '#{window_id}' \ + >/dev/null 2>&1; then + return 0 + fi + sleep 1 + i=$((i + 1)) + done + return 1 +} + +window_fields() +{ + $TMUX display-message -p -t "$1" '#{window_id} #{window_index}' +} + +pane_fields() +{ + $TMUX display-message -p -t "$1" '#{window_id} #{window_index} #{pane_id}' +} + +send() +{ + printf '%s\n' "$*" >&3 +} + +$TMUX kill-server 2>/dev/null +$TMUX new-session -d -s subs -x 80 -y 24 || exit 1 +sid=$($TMUX display-message -p -t subs '#{session_id}') + +mkfifo "$IN" +: >"$OUT" +$TMUX -C attach-session -t subs <"$IN" >"$OUT" 2>&1 & +PID=$! +exec 3>"$IN" + +send 'display-message -p ready' +wait_for 'ready' 3 || exit 1 + +send "refresh-client -B 'sw::#{session_windows}'" +wait_for "%subscription-changed sw $sid - - - : 1" || exit 1 + +send 'new-window' +wait_for "%subscription-changed sw $sid - - - : 2" || exit 1 + +send 'refresh-client -B sw' +send 'new-window' +reject_for "%subscription-changed sw $sid - - - : 3" || exit 1 + +send 'new-window -n pane-test' +wait_tmux subs:pane-test || exit 1 +set -- $(pane_fields subs:pane-test) +wid=$1 +widx=$2 +pane=$3 + +send "refresh-client -B 'ap:%*:#{pane_id}'" +wait_for "%subscription-changed ap $sid $wid $widx $pane : $pane" || exit 1 + +send 'split-window -t subs:pane-test' +i=0 +while [ "$i" -lt 6 ]; do + [ "$($TMUX list-panes -t subs:pane-test | wc -l)" -eq 2 ] && break + sleep 1 + i=$((i + 1)) +done +[ "$i" -lt 6 ] || exit 1 +newpane=$($TMUX list-panes -t subs:pane-test -F '#{pane_id}' | tail -n 1) +wait_for "%subscription-changed ap $sid $wid $widx $newpane : $newpane" || + exit 1 + +$TMUX new-session -d -s other || exit 1 +$TMUX link-window -s subs:pane-test -t other:1 || exit 1 + +send "refresh-client -B 'sp:$newpane:#{pane_id}:#{window_id}'" +wait_for "%subscription-changed sp $sid $wid $widx $newpane : $newpane:$wid" || + exit 1 + +send "refresh-client -B 'cw:$wid:#{window_id}:#{window_index}'" +wait_for "%subscription-changed cw $sid $wid $widx - : $wid:$widx" || + exit 1 + +send "refresh-client -B 'aw:@*:#{window_id}'" +send 'new-window -n window-test' +wait_tmux subs:window-test || exit 1 +set -- $(window_fields subs:window-test) +awid=$1 +awidx=$2 +wait_for "%subscription-changed aw $sid $awid $awidx - : $awid" || exit 1 + +send "refresh-client -B 'dup::#{session_windows}'" +wcount=$($TMUX display-message -p -t subs '#{session_windows}') +wait_for "%subscription-changed dup $sid - - - : $wcount" || exit 1 +send "refresh-client -B 'dup::#{session_name}'" +wait_for "%subscription-changed dup $sid - - - : subs" || exit 1 +send 'new-window -n dup-test' +wait_tmux subs:dup-test || exit 1 +wcount=$($TMUX display-message -p -t subs '#{session_windows}') +reject_for "%subscription-changed dup $sid - - - : $wcount" || exit 1 + +send "refresh-client -B 'missing-pane:%999999:#{pane_id}'" +send "refresh-client -B 'missing-window:@999999:#{window_id}'" +reject_for '%subscription-changed missing-pane' || exit 1 +reject_for '%subscription-changed missing-window' || exit 1 + +exit 0 From a19b961427d036f78d897e79a7144880d7a34ce3 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sat, 4 Jul 2026 18:37:53 +0100 Subject: [PATCH 033/127] Fix test. --- regress/session-ops.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/regress/session-ops.sh b/regress/session-ops.sh index 2da14b959..3b4f10073 100644 --- a/regress/session-ops.sh +++ b/regress/session-ops.sh @@ -32,7 +32,7 @@ $TMUX kill-server 2>/dev/null # Run a command and require that it succeeds. check_ok() { - if ! $TMUX "$@"; then + if ! $TMUX "$@" &1) + out=$($TMUX "$@" &1) if [ $? -eq 0 ]; then echo "Command succeeded (expected failure): $*" exit 1 From 0f77d9b30a63b70072557e4199d662af205e624d Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sat, 4 Jul 2026 18:52:53 +0100 Subject: [PATCH 034/127] Fix lengths in test. --- regress/input-replies.sh | 46 ++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/regress/input-replies.sh b/regress/input-replies.sh index 9baed827d..3435cd943 100644 --- a/regress/input-replies.sh +++ b/regress/input-replies.sh @@ -7,7 +7,7 @@ TERM=screen TMUX="$TEST_TMUX -Ltest -f/dev/null" $TMUX kill-server 2>/dev/null -sleep 0.1 +sleep 0.5 TMP=$(mktemp) EXP=$(mktemp) @@ -18,7 +18,7 @@ $TMUX new-session -d -x 80 -y 24 -s replies \; \ $TMUX set-option -s set-clipboard on || exit 1 $TMUX set-option -s get-clipboard buffer || exit 1 printf Hello | $TMUX load-buffer - -sleep 0.3 +sleep 0.5 exit_status=0 @@ -53,7 +53,7 @@ query_timeout() $TMUX respawn-window -k -t replies:0 \ "stty raw -echo min 0 time 5; printf '$setup'; printf '$seq'; sleep 0.1; dd bs=1 count=128 2>/dev/null | cat -v >$TMP" - sleep 0.7 + sleep 1 printf "%s" "$expected" >"$EXP" cmp "$TMP" "$EXP" || fail "$name" } @@ -62,26 +62,26 @@ query "dsr-ok" '^[[0n' '\033[5n' 4 '' query "dsr-cursor" '^[[1;1R' '\033[6n' 6 '' query "da-primary" '^[[?1;2c' '\033[c' 7 '' query "da-secondary" '^[[>84;0;0c' '\033[>c' 10 '' -query "decrqm-irm-reset" '^[[4;2$y' '\033[4$p' 8 '' -query "decrqm-irm-set" '^[[4;1$y' '\033[4$p' 8 '\033[4h' -query "decrqm-cursor-keys-reset" '^[[?1;2$y' '\033[?1$p' 9 '' -query "decrqm-cursor-keys-set" '^[[?1;1$y' '\033[?1$p' 9 '\033[?1h' -query "decrqm-columns" '^[[?3;4$y' '\033[?3$p' 9 '' -query "decrqm-origin-reset" '^[[?6;2$y' '\033[?6$p' 9 '' -query "decrqm-origin-set" '^[[?6;1$y' '\033[?6$p' 9 '\033[?6h' -query "decrqm-wrap-set" '^[[?7;1$y' '\033[?7$p' 9 '' -query "decrqm-wrap-reset" '^[[?7;2$y' '\033[?7$p' 9 '\033[?7l' -query "decrqm-cursor-visible-set" '^[[?25;1$y' '\033[?25$p' 10 '' -query "decrqm-cursor-visible-reset" '^[[?25;2$y' '\033[?25$p' 10 '\033[?25l' -query "decrqm-mouse-standard-set" '^[[?1000;1$y' '\033[?1000$p' 12 '\033[?1000h' -query "decrqm-mouse-button-set" '^[[?1002;1$y' '\033[?1002$p' 12 '\033[?1002h' -query "decrqm-mouse-all-set" '^[[?1003;1$y' '\033[?1003$p' 12 '\033[?1003h' -query "decrqm-focus-set" '^[[?1004;1$y' '\033[?1004$p' 12 '\033[?1004h' -query "decrqm-mouse-utf8-set" '^[[?1005;1$y' '\033[?1005$p' 12 '\033[?1005h' -query "decrqm-mouse-sgr-set" '^[[?1006;1$y' '\033[?1006$p' 12 '\033[?1006h' -query "decrqm-bracket-paste-set" '^[[?2004;1$y' '\033[?2004$p' 12 '\033[?2004h' -query "decrqm-theme-updates-set" '^[[?2031;1$y' '\033[?2031$p' 12 '\033[?2031h' -query "decrqss-cursor-style" '^[P1$r q0 q' '\033P$q q\033\\' 10 '' +query "decrqm-irm-reset" '^[[4;2$y' '\033[4$p' 7 '' +query "decrqm-irm-set" '^[[4;1$y' '\033[4$p' 7 '\033[4h' +query "decrqm-cursor-keys-reset" '^[[?1;2$y' '\033[?1$p' 8 '' +query "decrqm-cursor-keys-set" '^[[?1;1$y' '\033[?1$p' 8 '\033[?1h' +query "decrqm-columns" '^[[?3;4$y' '\033[?3$p' 8 '' +query "decrqm-origin-reset" '^[[?6;2$y' '\033[?6$p' 8 '' +query "decrqm-origin-set" '^[[?6;1$y' '\033[?6$p' 8 '\033[?6h' +query "decrqm-wrap-set" '^[[?7;1$y' '\033[?7$p' 8 '' +query "decrqm-wrap-reset" '^[[?7;2$y' '\033[?7$p' 8 '\033[?7l' +query "decrqm-cursor-visible-set" '^[[?25;1$y' '\033[?25$p' 9 '' +query "decrqm-cursor-visible-reset" '^[[?25;2$y' '\033[?25$p' 9 '\033[?25l' +query "decrqm-mouse-standard-set" '^[[?1000;1$y' '\033[?1000$p' 11 '\033[?1000h' +query "decrqm-mouse-button-set" '^[[?1002;1$y' '\033[?1002$p' 11 '\033[?1002h' +query "decrqm-mouse-all-set" '^[[?1003;1$y' '\033[?1003$p' 11 '\033[?1003h' +query "decrqm-focus-set" '^[[?1004;1$y' '\033[?1004$p' 11 '\033[?1004h' +query "decrqm-mouse-utf8-set" '^[[?1005;1$y' '\033[?1005$p' 11 '\033[?1005h' +query "decrqm-mouse-sgr-set" '^[[?1006;1$y' '\033[?1006$p' 11 '\033[?1006h' +query "decrqm-bracket-paste-set" '^[[?2004;1$y' '\033[?2004$p' 11 '\033[?2004h' +query "decrqm-theme-updates-set" '^[[?2031;1$y' '\033[?2031$p' 11 '\033[?2031h' +query "decrqss-cursor-style" '^[P1$r q0 q^[\' '\033P$q q\033\\' 12 '' query_timeout "osc-10-query" '^[]10;rgb:ffff/0000/0000^G' '\033]10;?\007' '\033]10;red\007' query_timeout "osc-11-query" '^[]11;rgb:0000/0000/ffff^G' '\033]11;?\007' '\033]11;blue\007' From 1b4015226f6274008b3cd1061016cb43415a6945 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sat, 4 Jul 2026 19:01:48 +0100 Subject: [PATCH 035/127] Use correct counts for this test also. --- regress/input-requests.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/regress/input-requests.sh b/regress/input-requests.sh index d9186c673..5e7b3781a 100644 --- a/regress/input-requests.sh +++ b/regress/input-requests.sh @@ -85,7 +85,7 @@ try: palette_out = f.name respawn("stty raw -echo min 1 time 50; " "printf '\\033]4;99;?\\033\\\\'; " - "dd bs=1 count=64 2>/dev/null | cat -v >%s; sleep 1" % + "dd bs=1 count=27 2>/dev/null | cat -v >%s; sleep 1" % palette_out) read_until(fd, b"\033]4;99;?\033\\") os.write(fd, b"\033]4;99;rgb:0101/0202/0303\033\\") @@ -101,7 +101,7 @@ try: clip_out = f.name respawn("stty raw -echo min 1 time 50; " "printf '\\033]52;c;?\\033\\\\'; " - "dd bs=1 count=64 2>/dev/null | cat -v >%s; sleep 1" % + "dd bs=1 count=21 2>/dev/null | cat -v >%s; sleep 1" % clip_out) data = read_until(fd, b"]52;") if b"?" not in data: From 219dc7f99e3b57e0cbc5557b3f9c8eb9977bd079 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sat, 4 Jul 2026 19:13:53 +0100 Subject: [PATCH 036/127] Locale breaks tests. --- regress/Makefile | 4 +++- regress/pane-ops.sh | 3 +++ regress/session-ops.sh | 3 +++ regress/window-ops.sh | 3 +++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/regress/Makefile b/regress/Makefile index ff58eef40..c354a8187 100644 --- a/regress/Makefile +++ b/regress/Makefile @@ -9,7 +9,9 @@ all: printf '%-40s ' "$$test"; \ start=$$(date +%s); \ ASAN_OPTIONS="abort_on_error=1:detect_leaks=0:$$ASAN_OPTIONS"; \ - if env -i ASAN_OPTIONS="$$ASAN_OPTIONS" sh "$$test" >/dev/null 2>&1; then \ + env -i LC_CTYPE=C.UTF-8 ASAN_OPTIONS="$$ASAN_OPTIONS" \ + sh "$$test" >/dev/null 2>&1; \ + if [ $$? -eq 0 ]; then \ end=$$(date +%s); \ echo "PASS ($$((end - start))s)"; \ else \ diff --git a/regress/pane-ops.sh b/regress/pane-ops.sh index 40e4bb77c..46984bc27 100644 --- a/regress/pane-ops.sh +++ b/regress/pane-ops.sh @@ -25,6 +25,9 @@ PATH=/bin:/usr/bin TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export TERM LANG LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) TMUX="$TEST_TMUX -Ltest -f/dev/null" diff --git a/regress/session-ops.sh b/regress/session-ops.sh index 3b4f10073..4938ee22c 100644 --- a/regress/session-ops.sh +++ b/regress/session-ops.sh @@ -22,6 +22,9 @@ PATH=/bin:/usr/bin TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export TERM LANG LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) TMUX="$TEST_TMUX -Ltest -f/dev/null" diff --git a/regress/window-ops.sh b/regress/window-ops.sh index 3dba68edd..dde7fc957 100644 --- a/regress/window-ops.sh +++ b/regress/window-ops.sh @@ -25,6 +25,9 @@ PATH=/bin:/usr/bin TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export TERM LANG LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) TMUX="$TEST_TMUX -Ltest -f/dev/null" From 2efd2d4970f9a924ed08d62c5ff6ae13b3be9240 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sat, 4 Jul 2026 19:25:39 +0100 Subject: [PATCH 037/127] Enable macOS regress. --- .github/workflows/regress.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 4d064623d..b40105d01 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -26,14 +26,14 @@ jobs: runner: ubuntu-24.04 make: make configure: --enable-utf8proc --enable-asan - # - name: ubuntu-24.04-arm64 - # runner: ubuntu-24.04-arm - # make: make - # configure: --enable-utf8proc --enable-asan - # - name: macos-26-arm64 - # runner: macos-26 - # make: gmake - # configure: --enable-utf8proc + - name: ubuntu-24.04-arm64 + runner: ubuntu-24.04-arm + make: make + configure: --enable-utf8proc --enable-asan + - name: macos-26-arm64 + runner: macos-26 + make: gmake + configure: --enable-utf8proc steps: - name: checkout From 6e9574c715a59c52d8cbcfe59bb50c2940800477 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sat, 4 Jul 2026 19:40:28 +0100 Subject: [PATCH 038/127] Linux arm64 fails. --- .github/workflows/regress.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index b40105d01..0b42dd688 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -26,10 +26,10 @@ jobs: runner: ubuntu-24.04 make: make configure: --enable-utf8proc --enable-asan - - name: ubuntu-24.04-arm64 - runner: ubuntu-24.04-arm - make: make - configure: --enable-utf8proc --enable-asan + #- name: ubuntu-24.04-arm64 + # runner: ubuntu-24.04-arm + # make: make + # configure: --enable-utf8proc --enable-asan - name: macos-26-arm64 runner: macos-26 make: gmake From 898a91903af87e2a75cb7dbc2fa71a4289ee91a0 Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 4 Jul 2026 18:54:18 +0000 Subject: [PATCH 039/127] Add some missing checks which make new pane positions off, from Dane Jensen. --- layout.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/layout.c b/layout.c index 11c292bba..f020ef8ba 100644 --- a/layout.c +++ b/layout.c @@ -1730,7 +1730,7 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args, ox = 4; } w->last_new_pane_x = ox; - } else + } else if (args_has(args, 'X')) if (lines != PANE_LINES_NONE) ox += 1; if (oy == INT_MAX) { @@ -1742,7 +1742,7 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args, oy = 2; } w->last_new_pane_y = oy; - } else + } else if (args_has(args, 'Y')) if (lines != PANE_LINES_NONE) oy += 1; From b05f0abeebd9e35b7fce82ec32ad8a6d69dcc80c Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 4 Jul 2026 20:17:53 +0000 Subject: [PATCH 040/127] Do not ignore filter when >1 pane. GitHub issue 5326. --- window-tree.c | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/window-tree.c b/window-tree.c index c37082c91..8a9749009 100644 --- a/window-tree.c +++ b/window-tree.c @@ -326,8 +326,8 @@ window_tree_build_window(struct session *s, struct winlink *wl, struct window_tree_itemdata *item; struct mode_tree_item *mti; char *name, *text; - struct window_pane *wp, **l; - u_int n, i; + struct window_pane **l; + u_int n, i, found; int expanded; struct format_tree *ft; @@ -354,29 +354,23 @@ window_tree_build_window(struct session *s, struct winlink *wl, free(name); mode_tree_align(mti, 1); - if ((wp = TAILQ_FIRST(&wl->window->panes)) == NULL) - goto empty; - if (TAILQ_NEXT(wp, entry) == NULL) { - if (!window_tree_filter_pane(s, wl, wp, filter)) - goto empty; - } - l = sort_get_panes_window(wl->window, &n, sort_crit); - if (n == 0) - goto empty; + found = 0; for (i = 0; i < n; i++) { + if (!window_tree_filter_pane(s, wl, l[i], filter)) + continue; + found++; if (data->hide_preview_this_pane && l[i] == data->wp) continue; - if (window_tree_filter_pane(s, wl, l[i], filter)) - window_tree_build_pane(s, wl, l[i], modedata, mti); + window_tree_build_pane(s, wl, l[i], modedata, mti); + } + if (found == 0) { + window_tree_free_item(item); + data->item_size--; + mode_tree_remove(data->data, mti); + return (0); } return (1); - -empty: - window_tree_free_item(item); - data->item_size--; - mode_tree_remove(data->data, mti); - return (0); } static void From 2f588047170b881e16b1e5e82da5018e80e613a3 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sat, 4 Jul 2026 21:18:31 +0100 Subject: [PATCH 041/127] Add some basic choose mode tests. --- regress/choose-buffer.sh | 199 +++++++++++++++++++++++++++++++ regress/choose-client.sh | 140 ++++++++++++++++++++++ regress/choose-tree.sh | 247 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 586 insertions(+) create mode 100644 regress/choose-buffer.sh create mode 100644 regress/choose-client.sh create mode 100644 regress/choose-tree.sh diff --git a/regress/choose-buffer.sh b/regress/choose-buffer.sh new file mode 100644 index 000000000..0f3e0d8da --- /dev/null +++ b/regress/choose-buffer.sh @@ -0,0 +1,199 @@ +#!/bin/sh + +# Tests of buffer mode (window-buffer.c) as driven by choose-buffer: that the +# -f filter removes buffers that do not match (by name and by content), that +# a filter matching nothing falls back to showing everything, that -O and -r +# change the sort order, that d deletes the selected buffer and C-t and D +# delete all tagged buffers, and that Enter runs the default command +# (paste-buffer) with the selected buffer. +# +# The list is drawn on a mode screen which capture-pane does not show, so a +# second server provides a client: an inner "tmux attach" runs inside a pane +# of the second server, and that pane is captured to read what the inner +# client rendered. Each choose-buffer call uses a distinct -F marker so a +# capture can be tied to the call it belongs to. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} +fail() +{ + echo "$1" + cleanup + exit 1 +} + +# capture the screen rendered by the inner client +capture() +{ + $TMUX2 capture-pane -p -t out:0 2>/dev/null +} + +# wait_for $marker +# +# Wait (up to ~10s) until the rendered screen contains $marker, so the +# capture is known to show the mode instance under test. +wait_for() +{ + i=0 + while [ "$i" -lt 50 ]; do + if capture | grep -q "$1"; then + sleep 0.2 + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1'" +} + +# wait_buffers $n +# +# Wait (up to ~10s) until the test server has exactly $n paste buffers. +wait_buffers() +{ + i=0 + while [ "$i" -lt 50 ]; do + c=$($TMUX list-buffers -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "expected $1 buffers, have $c" +} + +# wait_clients $n +# +# Wait (up to ~10s) until the test server has exactly $n clients attached. +wait_clients() +{ + i=0 + while [ "$i" -lt 10 ]; do + c=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 1 + i=$((i + 1)) + done + return 1 +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +$TMUX new-session -d -s aaa -x 80 -y 24 || exit 1 + +$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1 +wait_clients 1 || fail "no client attached to test server" + +# Two named buffers with distinct contents; bufa is created first. +$TMUX set-buffer -b bufa "hello buffer" || exit 1 +$TMUX set-buffer -b bufz "other buffer" || exit 1 + +# --- filter by buffer name --------------------------------------------------- +$TMUX choose-buffer -t aaa:0 -F 'B1' -f '#{==:#{buffer_name},bufa}' || exit 1 +wait_for 'B1' +out=$(capture) +echo "$out" | grep -q 'bufa: B1' || fail "bufa missing when it matches" +echo "$out" | grep -q 'bufz: B1' && fail "bufz shown but does not match" +[ "$(echo "$out" | grep -c ': B1')" -eq 1 ] || fail "expected 1 buffer" +$TMUX send-keys -t aaa:0 q + +# --- filter by buffer content ------------------------------------------------ +$TMUX choose-buffer -t aaa:0 -F 'B2' -f '#{m:*hello*,#{buffer_sample}}' || \ + exit 1 +wait_for 'B2' +out=$(capture) +echo "$out" | grep -q 'bufa: B2' || fail "bufa missing when content matches" +echo "$out" | grep -q 'bufz: B2' && fail "bufz shown but content not matched" +$TMUX send-keys -t aaa:0 q + +# --- no filter shows both buffers --------------------------------------------- +$TMUX choose-buffer -t aaa:0 -F 'B3' || exit 1 +wait_for 'B3' +out=$(capture) +echo "$out" | grep -q 'bufa: B3' || fail "bufa missing with no filter" +echo "$out" | grep -q 'bufz: B3' || fail "bufz missing with no filter" +[ "$(echo "$out" | grep -c ': B3')" -eq 2 ] || fail "expected 2 buffers" +$TMUX send-keys -t aaa:0 q + +# --- filter matching nothing --------------------------------------------------- +# +# Everything is shown and the filter indicator reports no matches. +$TMUX choose-buffer -t aaa:0 -F 'B4' -f '#{==:#{buffer_name},nosuch}' || \ + exit 1 +wait_for 'B4' +out=$(capture) +echo "$out" | grep -q 'bufa: B4' || fail "bufa missing with no-match filter" +echo "$out" | grep -q 'bufz: B4' || fail "bufz missing with no-match filter" +echo "$out" | grep -q 'no matches' || fail "no matches indicator missing" +$TMUX send-keys -t aaa:0 q + +# --- sort orders --------------------------------------------------------------- +# +# By name bufa sorts first and -r reverses. +$TMUX choose-buffer -t aaa:0 -F 'B5' -O name || exit 1 +wait_for 'B5' +capture | grep ': B5' | head -1 | grep -q 'bufa: B5' || \ + fail "bufa not first with -O name" +$TMUX send-keys -t aaa:0 q + +$TMUX choose-buffer -t aaa:0 -F 'B6' -O name -r || exit 1 +wait_for 'B6' +capture | grep ': B6' | head -1 | grep -q 'bufz: B6' || \ + fail "bufz not first with -O name -r" +$TMUX send-keys -t aaa:0 q + +# --- d deletes the selected buffer -------------------------------------------- +# +# The filter leaves only bufz listed and selected; d deletes it. +$TMUX choose-buffer -t aaa:0 -F 'G1' -f '#{==:#{buffer_name},bufz}' || exit 1 +wait_for 'bufz: G1' +$TMUX send-keys -t aaa:0 d +wait_buffers 1 +$TMUX list-buffers -F '#{buffer_name}' | grep -q 'bufa' || \ + fail "wrong buffer deleted" +$TMUX send-keys -t aaa:0 q + +# --- C-t tags all buffers and D deletes the tagged ------------------------------ +$TMUX set-buffer -b bufz "other buffer" || exit 1 +$TMUX set-buffer -b bufb "third buffer" || exit 1 +$TMUX choose-buffer -t aaa:0 -F 'G2' || exit 1 +wait_for ': G2' +$TMUX send-keys -t aaa:0 C-t D +wait_buffers 0 +$TMUX send-keys -t aaa:0 q + +# --- Enter runs the default command (paste-buffer) ------------------------------ +# +# The only buffer is listed and selected; Enter leaves the mode and pastes it +# into the shell in the pane, where it appears on the screen. +$TMUX set-buffer -b bufa "hello buffer" || exit 1 +$TMUX choose-buffer -t aaa:0 -F 'G3' || exit 1 +wait_for 'bufa: G3' +$TMUX send-keys -t aaa:0 Enter +i=0 +while [ "$i" -lt 50 ]; do + [ "$($TMUX display -p -t aaa:0 '#{pane_in_mode}')" = "0" ] && break + sleep 0.2 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "mode did not exit after Enter" +i=0 +while [ "$i" -lt 50 ]; do + $TMUX capture-pane -p -t aaa:0 | grep -q 'hello buffer' && break + sleep 0.2 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "buffer not pasted into pane" + +cleanup +exit 0 diff --git a/regress/choose-client.sh b/regress/choose-client.sh new file mode 100644 index 000000000..e299b9852 --- /dev/null +++ b/regress/choose-client.sh @@ -0,0 +1,140 @@ +#!/bin/sh + +# Tests of client mode (window-client.c) as driven by choose-client: that the +# -f filter removes clients that do not match, that a filter matching nothing +# falls back to showing everything, and that Enter runs the default command +# (detach-client) on the selected client. Sort orders are not tested here +# because clients are named after their ttys, which are not predictable. +# +# The list is drawn on a mode screen which capture-pane does not show, so a +# second server provides the clients: two inner "tmux attach" commands run in +# panes of the second server, and the pane holding the client the mode is +# displayed on is captured. Each choose-client call uses a distinct -F marker +# so a capture can be tied to the call it belongs to. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} +fail() +{ + echo "$1" + cleanup + exit 1 +} + +# capture the screen rendered by the inner client attached to aaa +capture() +{ + $TMUX2 capture-pane -p -t out:0 2>/dev/null +} + +# wait_for $marker +# +# Wait (up to ~10s) until the rendered screen contains $marker, so the +# capture is known to show the mode instance under test. +wait_for() +{ + i=0 + while [ "$i" -lt 50 ]; do + if capture | grep -q "$1"; then + sleep 0.2 + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1'" +} + +# wait_clients $n +# +# Wait (up to ~10s) until the test server has exactly $n clients attached. +wait_clients() +{ + i=0 + while [ "$i" -lt 10 ]; do + c=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 1 + i=$((i + 1)) + done + return 1 +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +# One client attached to each of two sessions; the mode is displayed on the +# client attached to aaa (in window 0 of the outer server) and the filters +# tell the clients apart by their attached session. +$TMUX new-session -d -s aaa -x 80 -y 24 || exit 1 +$TMUX new-session -d -s bbb -x 80 -y 24 || exit 1 + +$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1 +$TMUX2 new-window -d -t out: "$TMUX attach -t bbb" || exit 1 +wait_clients 2 || fail "expected two clients attached to test server" + +# --- filter keeping only the aaa client ------------------------------------- +$TMUX choose-client -t aaa:0 -F 'C1=#{client_session}' \ + -f '#{==:#{client_session},aaa}' || exit 1 +wait_for 'C1=' +out=$(capture) +echo "$out" | grep -q 'C1=aaa' || fail "aaa client missing when it matches" +echo "$out" | grep -q 'C1=bbb' && fail "bbb client shown but does not match" +[ "$(echo "$out" | grep -c 'C1=')" -eq 1 ] || fail "expected 1 client" +$TMUX send-keys -t aaa:0 q + +# --- filter keeping only the bbb client ------------------------------------- +$TMUX choose-client -t aaa:0 -F 'C2=#{client_session}' \ + -f '#{==:#{client_session},bbb}' || exit 1 +wait_for 'C2=' +out=$(capture) +echo "$out" | grep -q 'C2=bbb' || fail "bbb client missing when it matches" +echo "$out" | grep -q 'C2=aaa' && fail "aaa client shown but does not match" +[ "$(echo "$out" | grep -c 'C2=')" -eq 1 ] || fail "expected 1 client" +$TMUX send-keys -t aaa:0 q + +# --- no filter shows both clients ------------------------------------------- +$TMUX choose-client -t aaa:0 -F 'C3=#{client_session}' || exit 1 +wait_for 'C3=' +out=$(capture) +echo "$out" | grep -q 'C3=aaa' || fail "aaa client missing with no filter" +echo "$out" | grep -q 'C3=bbb' || fail "bbb client missing with no filter" +[ "$(echo "$out" | grep -c 'C3=')" -eq 2 ] || fail "expected 2 clients" +$TMUX send-keys -t aaa:0 q + +# --- filter matching nothing ------------------------------------------------ +# +# Everything is shown and the filter indicator reports no matches. +$TMUX choose-client -t aaa:0 -F 'C4=#{client_session}' \ + -f '#{==:#{client_session},nosuch}' || exit 1 +wait_for 'C4=' +out=$(capture) +echo "$out" | grep -q 'C4=aaa' || fail "aaa client missing with no-match filter" +echo "$out" | grep -q 'C4=bbb' || fail "bbb client missing with no-match filter" +echo "$out" | grep -q 'no matches' || fail "no matches indicator missing" +$TMUX send-keys -t aaa:0 q + +# --- Enter runs the default command (detach-client) -------------------------- +# +# The filter leaves only the bbb client listed and selected; Enter detaches +# it, leaving only the aaa client attached. +$TMUX choose-client -t aaa:0 -F 'G1=#{client_session}' \ + -f '#{==:#{client_session},bbb}' || exit 1 +wait_for 'G1=bbb' +$TMUX send-keys -t aaa:0 Enter +wait_clients 1 || fail "bbb client did not detach" +[ "$($TMUX list-clients -F '#{client_session}')" = "aaa" ] || \ + fail "wrong client detached" + +cleanup +exit 0 diff --git a/regress/choose-tree.sh b/regress/choose-tree.sh new file mode 100644 index 000000000..a82e42c20 --- /dev/null +++ b/regress/choose-tree.sh @@ -0,0 +1,247 @@ +#!/bin/sh + +# Tests of tree mode (window-tree.c) as driven by choose-tree. +# +# Filtering: the -f filter is applied per pane and removes panes, windows and +# sessions with no matching panes (a window with more than one pane and no +# matching panes must disappear - GitHub issue 5326); -h keeps a window +# listed when its only matching pane is the pane the tree is drawn in; a +# filter matching nothing falls back to showing everything. +# +# Sorting: -O and -r change the sort order. +# +# Keys: h and l collapse and expand; f prompts for a filter and c clears it; +# g goes to the top; Enter runs the default command (switch-client); x kills +# the current item after a confirmation prompt. +# +# The tree is drawn on a mode screen which capture-pane does not show, so - as +# in environ-update.sh - a second server provides a client: an inner "tmux +# attach" runs inside a pane of the second server, and that pane is captured +# to read what the inner client rendered. Each choose-tree call uses a +# distinct -F marker so a capture can be tied to the call it belongs to. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} +fail() +{ + echo "$1" + cleanup + exit 1 +} + +# capture the screen rendered by the inner client +capture() +{ + $TMUX2 capture-pane -p -t out:0 2>/dev/null +} + +# wait_for $marker +# +# Wait (up to ~10s) until the rendered screen contains $marker, so the +# capture is known to show the mode instance under test. +wait_for() +{ + i=0 + while [ "$i" -lt 50 ]; do + if capture | grep -q "$1"; then + sleep 0.2 + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1'" +} + +# wait_count $marker $n +# +# Wait (up to ~10s) until exactly $n rendered lines contain $marker. +wait_count() +{ + i=0 + while [ "$i" -lt 50 ]; do + [ "$(capture | grep -c "$1")" -eq "$2" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for $2 lines of '$1' (have $(capture | grep -c "$1"))" +} + +# wait_clients $n +# +# Wait (up to ~10s) until the test server has exactly $n clients attached. +wait_clients() +{ + i=0 + while [ "$i" -lt 10 ]; do + c=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 1 + i=$((i + 1)) + done + return 1 +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +# Session zzz is created first, so it sorts first by index, and has a +# two-pane window 0 and a single-pane window 1. Session aaa has one window +# with one pane and is where the tree is displayed. With everything expanded +# and no filter the tree is nine lines: +# +# 0 zzz 1 window 0 2 pane 0 3 pane 1 4 window 1 5 pane 0 +# 6 aaa 7 window 0 8 pane 0 +$TMUX new-session -d -s zzz -x 80 -y 24 || exit 1 +$TMUX split-window -t zzz:0 || exit 1 +$TMUX new-window -t zzz || exit 1 +$TMUX new-session -d -s aaa -x 80 -y 24 || exit 1 + +$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1 +wait_clients 1 || fail "no client attached to test server" + +# --- filter keeping only aaa ------------------------------------------------ +# +# zzz must disappear entirely: its single-pane window 1 and - the GitHub 5326 +# regression - its two-pane window 0. aaa contributes exactly three lines +# (session, window, pane). +$TMUX choose-tree -t aaa:0 -F 'F1' -f '#{==:#{session_name},aaa}' || exit 1 +wait_count 'F1' 3 +out=$(capture) +echo "$out" | grep -q 'aaa: F1' || fail "aaa missing when filter matches it" +echo "$out" | grep -q 'zzz: F1' && fail "zzz shown but no pane matches" +$TMUX send-keys -t aaa:0 q + +# --- filter keeping only zzz ------------------------------------------------ +# +# zzz contributes six lines (session, two windows, three panes); aaa must +# disappear. +$TMUX choose-tree -t aaa:0 -F 'F2' -f '#{==:#{session_name},zzz}' || exit 1 +wait_count 'F2' 6 +out=$(capture) +echo "$out" | grep -q 'zzz: F2' || fail "zzz missing when filter matches it" +echo "$out" | grep -q 'aaa: F2' && fail "aaa shown but no pane matches" +$TMUX send-keys -t aaa:0 q + +# --- filter matching a single pane ------------------------------------------ +# +# Only pane 1 of zzz:0 matches, so the tree is exactly session zzz, window 0 +# and that pane; zzz:1 and all of aaa must disappear. +$TMUX choose-tree -t aaa:0 -F 'F3' -f '#{==:#{pane_index},1}' || exit 1 +wait_count 'F3' 3 +out=$(capture) +echo "$out" | grep -q 'zzz: F3' || fail "zzz missing when its pane matches" +echo "$out" | grep -q 'aaa: F3' && fail "aaa shown but no pane matches" +echo "$out" | grep -q '1: F3' || fail "matching pane missing" +$TMUX send-keys -t aaa:0 q + +# --- filter matching nothing ------------------------------------------------ +# +# Everything is shown and the filter indicator reports no matches. +$TMUX choose-tree -t aaa:0 -F 'F4' -f '#{==:#{session_name},nosuch}' || exit 1 +wait_for 'F4' +out=$(capture) +echo "$out" | grep -q 'aaa: F4' || fail "aaa missing with no-match filter" +echo "$out" | grep -q 'zzz: F4' || fail "zzz missing with no-match filter" +echo "$out" | grep -q 'no matches' || fail "no matches indicator missing" +$TMUX send-keys -t aaa:0 q + +# --- -h with the tree pane as the only match -------------------------------- +# +# With -h the pane the tree is drawn in is hidden, but it still counts as a +# match, so session and window aaa stay listed: two lines, no pane line. +$TMUX choose-tree -h -t aaa:0 -F 'F5' -f '#{==:#{session_name},aaa}' || \ + exit 1 +wait_count 'F5' 2 +capture | grep -q 'aaa: F5' || fail "aaa missing with -h" +$TMUX send-keys -t aaa:0 q + +# --- sort orders ------------------------------------------------------------ +# +# By index zzz (created first) sorts first, by name aaa does, and -r reverses. +$TMUX choose-tree -t aaa:0 -F 'F6' -O index || exit 1 +wait_for 'F6' +capture | grep 'F6' | head -1 | grep -q 'zzz: F6' || \ + fail "zzz not first with -O index" +$TMUX send-keys -t aaa:0 q + +$TMUX choose-tree -t aaa:0 -F 'F7' -O name || exit 1 +wait_for 'F7' +capture | grep 'F7' | head -1 | grep -q 'aaa: F7' || \ + fail "aaa not first with -O name" +$TMUX send-keys -t aaa:0 q + +$TMUX choose-tree -t aaa:0 -F 'F8' -O name -r || exit 1 +wait_for 'F8' +capture | grep 'F8' | head -1 | grep -q 'zzz: F8' || \ + fail "zzz not first with -O name -r" +$TMUX send-keys -t aaa:0 q + +# --- collapse and expand with h and l ----------------------------------------- +# +# g moves to the top (session zzz); h collapses it, hiding its five children; +# l expands it again. +$TMUX choose-tree -t aaa:0 -F 'G1' -O index || exit 1 +wait_count 'G1' 9 +$TMUX send-keys -t aaa:0 g h +wait_count 'G1' 4 +$TMUX send-keys -t aaa:0 l +wait_count 'G1' 9 +$TMUX send-keys -t aaa:0 q + +# --- filter entered at the prompt with f, cleared with c ---------------------- +$TMUX choose-tree -t aaa:0 -F 'G2' -O index || exit 1 +wait_count 'G2' 9 +$TMUX send-keys -t aaa:0 f +$TMUX send-keys -t aaa:0 -l '#{==:#{session_name},aaa}' +$TMUX send-keys -t aaa:0 Enter +wait_count 'G2' 3 +out=$(capture) +echo "$out" | grep -q 'aaa: G2' || fail "aaa missing with prompt filter" +echo "$out" | grep -q 'zzz: G2' && fail "zzz shown with prompt filter" +$TMUX send-keys -t aaa:0 c +wait_count 'G2' 9 +$TMUX send-keys -t aaa:0 q + +# --- Enter runs the default command (switch-client) ---------------------------- +# +# g selects session zzz and Enter switches the client to it. +$TMUX choose-tree -t aaa:0 -F 'G3' -O index || exit 1 +wait_count 'G3' 9 +$TMUX send-keys -t aaa:0 g Enter +i=0 +while [ "$i" -lt 50 ]; do + [ "$($TMUX list-clients -F '#{client_session}')" = "zzz" ] && break + sleep 0.2 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "client did not switch to zzz" +$TMUX switch-client -c "$($TMUX list-clients -F '#{client_name}')" -t aaa || \ + exit 1 + +# --- x kills the current item after confirmation ------------------------------- +# +# g and four times j select window 1 of zzz; x asks for confirmation and y +# kills it, leaving zzz with one window and the tree with seven lines. +$TMUX choose-tree -t aaa:0 -F 'G4' -O index || exit 1 +wait_count 'G4' 9 +$TMUX send-keys -t aaa:0 g j j j j x +wait_for 'Kill window 1' +$TMUX send-keys -t aaa:0 y +wait_count 'G4' 7 +[ "$($TMUX list-windows -t zzz -F x | grep -c x)" -eq 1 ] || \ + fail "window 1 of zzz not killed" +$TMUX send-keys -t aaa:0 q + +cleanup +exit 0 From 415885ecb9bff67fa5610ec9048dc78737baaee7 Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 4 Jul 2026 22:06:45 +0000 Subject: [PATCH 042/127] Infer the terminal theme from the background colour correctly, if theme reporting is not supported. --- server-client.c | 2 ++ tty-keys.c | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/server-client.c b/server-client.c index c25c7ca19..8344ae190 100644 --- a/server-client.c +++ b/server-client.c @@ -1197,6 +1197,8 @@ server_client_update_theme_colours(struct client *c) format_defaults(ft, c, NULL, NULL, NULL); theme = c->theme; + if (theme == THEME_UNKNOWN) + theme = colour_totheme(c->tty.bg); if (option == 2) theme = THEME_LIGHT; else if (option == 3) diff --git a/tty-keys.c b/tty-keys.c index f48fb1b71..209bbeeab 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -751,7 +751,7 @@ tty_keys_next(struct tty *tty) const char *buf; size_t len, size; cc_t bspace; - int delay, expired = 0, n; + int delay, expired = 0, n, bg = tty->bg; key_code key, onlykey; struct mouse_event m = { 0 }; struct key_event *event; @@ -811,11 +811,15 @@ tty_keys_next(struct tty *tty) switch (tty_keys_colours(tty, buf, len, &size, &tty->fg, &tty->bg)) { case 0: /* yes */ key = KEYC_UNKNOWN; + if (tty->bg != bg) + server_client_update_theme_colours(c); session_theme_changed(c->session); goto complete_key; case -1: /* no, or not valid */ break; case 1: /* partial */ + if (tty->bg != bg) + server_client_update_theme_colours(c); session_theme_changed(c->session); goto partial_key; } From 7a8f8f5a74527517dcd8777cac916caf798aa5c8 Mon Sep 17 00:00:00 2001 From: nicm Date: Sat, 4 Jul 2026 22:09:06 +0000 Subject: [PATCH 043/127] Do not use stale pointers in modes. --- window-client.c | 5 ++++- window-tree.c | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/window-client.c b/window-client.c index a780222e3..5a8c71862 100644 --- a/window-client.c +++ b/window-client.c @@ -164,6 +164,7 @@ const struct window_mode window_client_mode = { struct window_client_itemdata { struct client *c; + char *ttyname; }; struct window_client_modedata { @@ -204,6 +205,7 @@ static void window_client_free_item(struct window_client_itemdata *item) { server_client_unref(item->c); + free(item->ttyname); free(item); } @@ -231,6 +233,7 @@ window_client_build(void *modedata, struct sort_criteria *sort_crit, item = window_client_add_item(data); item->c = l[i]; + item->ttyname = xstrdup(l[i]->ttyname); l[i]->references++; } @@ -552,7 +555,7 @@ window_client_key(struct window_mode_entry *wme, struct client *c, break; case '\r': item = mode_tree_get_current(mtd); - mode_tree_run_command(c, NULL, data->command, item->c->ttyname); + mode_tree_run_command(c, NULL, data->command, item->ttyname); finished = 1; break; } diff --git a/window-tree.c b/window-tree.c index 8a9749009..2d15eb536 100644 --- a/window-tree.c +++ b/window-tree.c @@ -330,6 +330,7 @@ window_tree_build_window(struct session *s, struct winlink *wl, u_int n, i, found; int expanded; struct format_tree *ft; + uint64_t tag = FORMAT_NONE; item = window_tree_add_item(data); item->type = WINDOW_TREE_WINDOW; @@ -337,7 +338,9 @@ window_tree_build_window(struct session *s, struct winlink *wl, item->winlink = wl->idx; item->pane = -1; - ft = format_create(NULL, NULL, FORMAT_PANE|wl->window->active->id, 0); + if (wl->window != NULL && wl->window->active != NULL) + tag = FORMAT_PANE|wl->window->active->id; + ft = format_create(NULL, NULL, tag, 0); format_defaults(ft, NULL, s, wl, NULL); text = format_expand(ft, data->format); xasprintf(&name, "%u", wl->idx); @@ -385,6 +388,7 @@ window_tree_build_session(struct session *s, void *modedata, u_int n, i, empty; int expanded; struct format_tree *ft; + uint64_t tag = FORMAT_NONE; item = window_tree_add_item(data); item->type = WINDOW_TREE_SESSION; @@ -392,7 +396,9 @@ window_tree_build_session(struct session *s, void *modedata, item->winlink = -1; item->pane = -1; - ft = format_create(NULL, NULL, FORMAT_PANE|wl->window->active->id, 0); + if (wl != NULL && wl->window != NULL && wl->window->active != NULL) + tag = FORMAT_PANE|wl->window->active->id; + ft = format_create(NULL, NULL, tag, 0); format_defaults(ft, NULL, s, NULL, NULL); text = format_expand(ft, data->format); format_free(ft); From 92028ed6e2073977faef1e07da26b63849929df7 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sat, 4 Jul 2026 23:09:16 +0100 Subject: [PATCH 044/127] Add a few more tests. --- regress/format-render-contexts.sh | 281 ++++++++++++++++++++++++++++ regress/input-reflow-stress.sh | 281 ++++++++++++++++++++++++++++ regress/lifecycle-deferred.sh | 216 ++++++++++++++++++++++ regress/mode-mutation.sh | 297 ++++++++++++++++++++++++++++++ 4 files changed, 1075 insertions(+) create mode 100644 regress/format-render-contexts.sh create mode 100644 regress/input-reflow-stress.sh create mode 100644 regress/lifecycle-deferred.sh create mode 100644 regress/mode-mutation.sh diff --git a/regress/format-render-contexts.sh b/regress/format-render-contexts.sh new file mode 100644 index 000000000..81f1de462 --- /dev/null +++ b/regress/format-render-contexts.sh @@ -0,0 +1,281 @@ +#!/bin/sh +# Exercise format/style rendering in live contexts. These paths use +# format_draw rather than plain format expansion, so they have historically +# found different crashes and runaway output. + +PATH=/bin:/usr/bin +TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export TERM LANG LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +LIMIT=20000 + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} + +fail() +{ + echo "$1" >&2 + cleanup + exit 1 +} + +run_cmd() +{ + if command -v timeout >/dev/null 2>&1; then + timeout 10 "$@" + else + "$@" + fi +} + +bounded() +{ + name="$1" + text="$2" + n=$(printf '%s' "$text" | wc -c | tr -d ' ') + + [ "$n" -le "$LIMIT" ] || fail "$name produced $n bytes" +} + +tmux_run() +{ + name="$1" + shift + + out=$(run_cmd $TMUX "$@" 2>&1) + rc=$? + bounded "$name" "$out" + [ "$rc" -eq 0 ] || fail "$name failed ($rc): $out" + + printf '%s' "$out" +} + +capture() +{ + out=$(run_cmd $TMUX2 capture-pane -p -t out:0 2>/dev/null) + rc=$? + bounded "capture-pane" "$out" + [ "$rc" -eq 0 ] || fail "capture-pane failed" + printf '%s\n' "$out" +} + +capture_esc() +{ + out=$(run_cmd $TMUX2 capture-pane -Cep -t out:0 2>/dev/null) + rc=$? + bounded "capture-pane -e" "$out" + [ "$rc" -eq 0 ] || fail "capture-pane -e failed" + printf '%s\n' "$out" +} + +assert_alive() +{ + out=$(tmux_run "server alive ($1)" display-message -p alive) + [ "$out" = "alive" ] || fail "server not alive after $1" +} + +wait_for() +{ + marker="$1" + i=0 + while [ "$i" -lt 50 ]; do + if capture | grep -q "$marker"; then + sleep 0.1 + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for $marker" +} + +render_status_options() +{ + label="$1" + fmt="$2" + + tmux_run "$label status-left" \ + set-option -g status-left "SL$label: $fmt" >/dev/null + tmux_run "$label status-right" \ + set-option -g status-right "SR$label: $fmt" >/dev/null + tmux_run "$label status-format left/right" \ + set-option -g status-format[0] \ + '#{E:status-left}#[align=right]#{E:status-right}' >/dev/null + wait_for "SL$label" + assert_alive "$label status-left/right" +} + +render_status_format() +{ + label="$1" + fmt="$2" + + tmux_run "$label clear status-left" set-option -g status-left '' >/dev/null + tmux_run "$label clear status-right" set-option -g status-right '' >/dev/null + tmux_run "$label status-format" \ + set-option -g status-format[0] "SF$label: $fmt" >/dev/null + wait_for "SF$label" + assert_alive "$label status-format" +} + +render_message() +{ + label="$1" + fmt="$2" + + tmux_run "$label display-message" \ + display-message -t fmt:0 -d 1000 "DM$label: $fmt" >/dev/null + wait_for "DM$label" + assert_alive "$label display-message" +} + +render_choose_tree() +{ + label="$1" + fmt="$2" + + tmux_run "$label choose-tree" \ + choose-tree -t fmt:0 -F "CT$label: $fmt" >/dev/null + wait_for "CT$label" + tmux_run "$label quit choose-tree" send-keys -t fmt:0 q >/dev/null + assert_alive "$label choose-tree" +} + +render_customize() +{ + label="$1" + fmt="$2" + + tmux_run "$label customize-mode" \ + customize-mode -t fmt:0 -F "CM$label: $fmt" >/dev/null + sleep 0.5 + out=$(capture) + bounded "$label customize-mode capture" "$out" + tmux_run "$label quit customize-mode" send-keys -t fmt:0 q >/dev/null + assert_alive "$label customize-mode" +} + +render_list_output() +{ + label="$1" + fmt="$2" + + tmux_run "$label bind-key" \ + bind-key -T root F12 display-message "LK$label: $fmt" >/dev/null + out=$(tmux_run "$label list-keys" list-keys -T root F12) + bounded "$label list-keys output" "$out" + + tmux_run "$label option value" \ + set-option -g status-left "LO$label: $fmt" >/dev/null + out=$(tmux_run "$label list-options" show-options -g status-left) + bounded "$label list-options output" "$out" + + assert_alive "$label list-keys/list-options" +} + +check_sgr_sanity() +{ + tmux_run "sgr status style" \ + set-option -g status-style fg=default,bg=default >/dev/null + tmux_run "sgr status-format" \ + set-option -g status-format[0] \ + '#[fg=colour1,bg=default]SGR-STATUS#[default]' >/dev/null + wait_for "SGR-STATUS" + out=$(capture_esc | tail -n 1) + bounded "sgr status capture" "$out" + case "$out" in + *SGR-STATUS*) ;; + *) fail "status SGR capture lost text: $out" ;; + esac + case "$out" in + *"\\033["*) ;; + *) fail "status SGR capture has no escape reset/style data: $out" ;; + esac + + tmux_run "sgr message style" \ + set-option -g message-style fg=colour2,bg=default >/dev/null + tmux_run "sgr display-message" \ + display-message -t fmt:0 -d 1000 \ + '#[bg=default]SGR-MESSAGE#[default]' >/dev/null + wait_for "SGR-MESSAGE" + out=$(capture_esc | tail -n 1) + bounded "sgr message capture" "$out" + case "$out" in + *SGR-MESSAGE*) ;; + *) fail "message SGR capture lost text: $out" ;; + esac + case "$out" in + *"\\033["*) ;; + *) fail "message SGR capture has no escape reset/style data: $out" ;; + esac + + assert_alive "SGR sanity" +} + +run_corpus() +{ + label="$1" + fmt="$2" + + render_status_options "$label" "$fmt" + render_status_format "$label" "$fmt" + render_message "$label" "$fmt" + render_choose_tree "$label" "$fmt" + render_customize "$label" "$fmt" + render_list_output "$label" "$fmt" +} + +trap cleanup 0 1 15 +cleanup + +tmux_run "new inner session" \ + new-session -d -s fmt -x 100 -y 30 "exec sleep 1000" >/dev/null +tmux_run "new inner window" \ + new-window -t fmt -n second "exec sleep 1000" >/dev/null +tmux_run "select visible window" select-window -t fmt:0 >/dev/null +tmux_run "set status interval" set-option -g status-interval 1 >/dev/null +tmux_run "set base message style" \ + set-option -g message-style fg=default,bg=default >/dev/null +tmux_run "set nested option" \ + set-option -g @nested 'NESTED-#{session_name}-#{window_index}' >/dev/null + +run_cmd $TMUX2 new-session -d -s out -x 100 -y 30 "$TMUX attach -t fmt" \ + >/dev/null 2>&1 || fail "failed to start outer client" + +i=0 +while [ "$i" -lt 50 ]; do + c=$(tmux_run "wait clients" list-clients -F x | grep -c x) + [ "$c" -eq 1 ] && break + sleep 0.2 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "inner client did not attach" + +run_corpus A \ + '#[bold,dim,underscore,blink,reverse,italics,strikethrough,double-underscore,curly-underscore,dotted-underscore,dashed-underscore,overline,fg=colour196,bg=colour22,us=colour45]LONGSTYLE#[default]plain' +run_corpus B \ + 'wide: Ελληνικά 中文 😀 #{=12:中文😀abc}' +run_corpus C \ + '#[fg=colour2,bg=colour4]base#[push-default]#[fg=colour1,bg=default]push#[pop-default]pop#[default]' +run_corpus D \ + '#[list=on]list#[list=focus]focus#[nolist] #[align=centre]centre#[noalign] #[range=left]L#[range=right]R#[norange]' +run_corpus E \ + '#{E:@nested}:#{T:%%H:%%M}:#{?#{==:#{session_name},fmt},yes,no}:#{?0,bad,}' +run_corpus F \ + 'job:#(printf job-ok)' +run_corpus G \ + 'empty:#{E:}:#{T:}:#{definitely_not_a_variable}:#{?0,no,}' + +check_sgr_sanity + +cleanup +exit 0 diff --git a/regress/input-reflow-stress.sh b/regress/input-reflow-stress.sh new file mode 100644 index 000000000..c673b3472 --- /dev/null +++ b/regress/input-reflow-stress.sh @@ -0,0 +1,281 @@ +#!/bin/sh + +WIDTHS="80 40 20 10 7 5 4 3 2 1 2 3 4 5 7 10 20 40 80" +HISTORY_LIMIT=220 +HISTORY_BOUND=12000 +JOINED_BOUND=500000 +RAW_BOUND=5000000 + +record_fail() +{ + echo "FAIL: $1" + exit_status=1 +} + +make_payload() +{ + n=0 + i=0 + while [ "$i" -lt 16 ]; do + printf 'L%04d|ascii|abcdefghijklmnopqrstuvwxyz\n' "$n"; n=$((n + 1)) + printf 'L%04d|wrap|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' "$n"; n=$((n + 1)) + printf 'L%04d|wide|AあB界C\n' "$n"; n=$((n + 1)) + printf 'L%04d|wide-edge|abcdあZ\n' "$n"; n=$((n + 1)) + printf 'L%04d|combining|é é é\n' "$n"; n=$((n + 1)) + printf 'L%04d|orphan-combining|́A\n' "$n"; n=$((n + 1)) + printf 'L%04d|variation|✔️ ⚠️ 🏝️\n' "$n"; n=$((n + 1)) + printf 'L%04d|emoji|🙂🙂🙂\n' "$n"; n=$((n + 1)) + printf 'L%04d|flag|🇬🇧 🇺🇸 🇯🇵\n' "$n"; n=$((n + 1)) + printf 'L%04d|mixed|Aあé✔️B\n' "$n"; n=$((n + 1)) + printf 'L%04d|style|plain \033[31mred\033[0m plain\n' "$n"; n=$((n + 1)) + printf 'L%04d|hyperlink|\033]8;;https://example.com/%04d\033\\LINK\033]8;;\033\\\n' "$n" "$n"; n=$((n + 1)) + printf 'L%04d|tabs|a\tb\tc\n' "$n"; n=$((n + 1)) + printf 'L%04d|backspace|abc\bX\n' "$n"; n=$((n + 1)) + printf 'carriage-return-%04d|abcdef\rL%04d|carriage-return|XY\n' "$n" "$n"; n=$((n + 1)) + i=$((i + 1)) + done +} + +make_alternate_payload() +{ + printf '\033[?1049h' + printf 'ALT000|wide|AあB界C\n' + printf 'ALT001|combining|é é é\n' + printf 'ALT002|style|plain \033[32mgreen\033[0m plain\n' + printf 'ALT003|emoji|🙂🙂🙂\n' +} + +load_and_paste() +{ + buffer=$1 + shift + + "$@" | $TMUX load-buffer -b "$buffer" - || exit 1 + $TMUX paste-buffer -d -b "$buffer" -t stress:0.0 || exit 1 + sleep 0.3 +} + +assert_alive() +{ + alive=$($TMUX display-message -p -t stress: alive 2>&1) || { + record_fail "server died after $1" + return + } + [ "$alive" = alive ] || record_fail "server died after $1" +} + +capture_joined() +{ + $TMUX capture-pane -pNJ -t stress: -S - -E - >"$TMP" +} + +assert_joined_sane() +{ + label=$1 + check_sentinels=${2:-yes} + + capture_joined || { + record_fail "joined capture failed after $label" + return + } + + bytes=$(wc -c <"$TMP") + if [ "$bytes" -gt "$JOINED_BOUND" ]; then + record_fail "joined capture too large after $label: $bytes" + fi + + if grep -q "$(printf '\357\277\275')" "$TMP"; then + record_fail "replacement character in joined capture after $label" + fi + + awk -v label="$label" ' + { + line = $0 + seen_on_line = 0 + while (match(line, /L[0-9][0-9][0-9][0-9]\|/)) { + id = substr(line, RSTART + 1, 4) + 0 + seen_on_line++ + seen[id]++ + if (id < last) { + printf("IDs out of order after %s: L%04d after L%04d\n", + label, id, last) + exit 1 + } + if (seen[id] > 1) { + printf("duplicate ID after %s: L%04d\n", label, id) + exit 1 + } + last = id + found = 1 + line = substr(line, RSTART + RLENGTH) + } + if (seen_on_line > 1) { + printf("fused IDs after %s: %s\n", label, $0) + exit 1 + } + } + END { + if (!found) { + printf("no line IDs after %s\n", label) + exit 1 + } + } + ' "$TMP" || { + cp "$TMP" "$EXP" + record_fail "joined structure failed after $label" + } + + if [ "$check_sentinels" = yes ]; then + for expected in \ + 'L0234|mixed|' \ + 'L0236|hyperlink|' \ + 'L0239|carriage-return|XY'; do + if ! grep -Fq "$expected" "$TMP"; then + printf '%s\n' "$expected" >"$EXP" + record_fail "missing sentinel after $label" + fi + done + fi +} + +assert_raw_sane() +{ + label=$1 + width=$2 + + $TMUX capture-pane -pR -t stress: >"$TMP" || { + record_fail "raw capture failed after $label" + return + } + + bytes=$(wc -c <"$TMP") + if [ "$bytes" -gt "$RAW_BOUND" ]; then + record_fail "raw capture too large after $label: $bytes" + fi + + awk -v label="$label" -v width="$width" ' + /^C / { + cell = $0 + sub(/^C /, "", cell) + split(cell, fields, " ") + split(fields[1], position, ",") + row = position[1] + 0 + col = position[2] + 0 + if (col >= width) { + printf("cell column outside pane after %s: %s\n", label, $0) + exit 1 + } + + data = $0 + if (data !~ /data=\(/) + next + sub(/^.*data=\(/, "", data) + split(data, data_fields, ",") + cell_width = data_fields[1] + 0 + padding = ($0 ~ /flags=PADDING/) + + if (!padding && cell_width == 0) { + printf("zero-width non-padding cell after %s: %s\n", label, $0) + exit 1 + } + if (padding && !(last_row == row && last_width > 1 && !last_padding)) { + printf("padding without preceding wide cell after %s: %s\n", label, $0) + exit 1 + } + + last_row = row + last_width = cell_width + last_padding = padding + } + ' "$TMP" || { + cp "$TMP" "$EXP" + record_fail "raw structure failed after $label" + } +} + +assert_history_sane() +{ + label=$1 + + size=$($TMUX display-message -p -t stress: '#{history_size}') || { + record_fail "history-size failed after $label" + return + } + case "$size" in + ''|*[!0-9]*) + record_fail "bad history size after $label: $size" + ;; + *) + if [ "$size" -gt "$HISTORY_BOUND" ]; then + record_fail "history too large after $label: $size" + fi + ;; + esac +} + +assert_copy_mode_sane() +{ + label=$1 + + $TMUX capture-pane -pM -t stress: >"$TMP" || { + record_fail "copy-mode capture failed after $label" + return + } + + bytes=$(wc -c <"$TMP") + if [ "$bytes" -gt 20000 ]; then + record_fail "copy-mode capture too large after $label" + fi + if grep -q "$(printf '\357\277\275')" "$TMP"; then + record_fail "replacement character in copy-mode after $label" + fi + if ! grep -Eq 'L[0-9][0-9][0-9][0-9]\|' "$TMP"; then + record_fail "no line ID in copy-mode after $label" + fi +} + +run_resize_checks() +{ + for width in $WIDTHS; do + $TMUX resize-window -t stress: -x "$width" -y 8 || exit 1 + sleep 0.1 + assert_alive "resize to $width" + assert_joined_sane "resize to $width" + assert_raw_sane "resize to $width" "$width" + assert_history_sane "resize to $width" + done +} + +$TMUX kill-server 2>/dev/null +sleep 0.1 +$TMUX new-session -d -x 1 -y 1 -s test-setup "sleep 2" || exit 1 +$TMUX set-option -g history-limit "$HISTORY_LIMIT" || exit 1 +$TMUX new-session -d -x 80 -y 8 -s stress "stty -echo; cat" || exit 1 +$TMUX kill-session -t test-setup +sleep 0.3 + +load_and_paste stress-data make_payload +run_resize_checks + +$TMUX copy-mode -H -t stress: || exit 1 +for cmd in history-top page-down halfpage-down halfpage-up page-up history-bottom; do + $TMUX send-keys -t stress: -X "$cmd" || exit 1 + sleep 0.1 + assert_copy_mode_sane "$cmd" +done +$TMUX send-keys -t stress: -X cancel || exit 1 + +load_and_paste stress-alt make_alternate_payload +for width in 10 4 1 4 10 80; do + $TMUX resize-window -t stress: -x "$width" -y 8 || exit 1 + sleep 0.1 + assert_alive "alternate resize to $width" + assert_raw_sane "alternate resize to $width" "$width" +done +printf '\033[?1049l' | $TMUX load-buffer -b stress-alt-exit - || exit 1 +$TMUX paste-buffer -d -b stress-alt-exit -t stress:0.0 || exit 1 +sleep 0.3 +assert_joined_sane "alternate screen exit" no + +$TMUX kill-server 2>/dev/null +exit $exit_status diff --git a/regress/lifecycle-deferred.sh b/regress/lifecycle-deferred.sh new file mode 100644 index 000000000..2650f1827 --- /dev/null +++ b/regress/lifecycle-deferred.sh @@ -0,0 +1,216 @@ +#!/bin/sh + +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) + +TMUX="$TEST_TMUX -Llifecycle-deferred -f/dev/null" +TMUX2="$TEST_TMUX -Llifecycle-deferred-outer -f/dev/null" + +TMPDIR=$(mktemp -d) +IN="$TMPDIR/in" +OUT="$TMPDIR/out" +CONTROL_PID= + +cleanup() +{ + exec 3>&- 2>/dev/null + [ -n "$CONTROL_PID" ] && kill "$CONTROL_PID" 2>/dev/null + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + rm -rf "$TMPDIR" +} + +fail() +{ + echo "$1" >&2 + [ -s "$OUT" ] && sed -n '1,120p' "$OUT" >&2 + cleanup + exit 1 +} + +run_tmux() +{ + out= + if command -v timeout >/dev/null 2>&1; then + out=$(timeout 10 $TMUX "$@" 2>&1) + else + out=$($TMUX "$@" 2>&1) + fi + rc=$? + [ "$rc" -eq 0 ] || fail "tmux $* failed ($rc): $out" + printf '%s' "$out" +} + +send_control() +{ + printf '%s\n' "$1" >&3 || fail "failed to write control command: $1" +} + +wait_clients() +{ + want=$1 + i=0 + + while [ "$i" -lt 50 ]; do + have=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$have" -eq "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + return 1 +} + +wait_format() +{ + target=$1 + format=$2 + want=$3 + i=0 + + while [ "$i" -lt 50 ]; do + have=$($TMUX display-message -p -t "$target" "$format" 2>/dev/null) + [ "$have" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + return 1 +} + +assert_alive() +{ + run_tmux has-session -t life >/dev/null + fields=$(run_tmux display-message -p -t life \ + '#{session_name}:#{window_id}:#{pane_id}:#{session_windows}:#{window_panes}') + case "$fields" in + life:@*:%*:*) ;; + *) fail "bad current fields after $1: $fields" ;; + esac +} + +check_control_output() +{ + sleep 1 + + if grep -E '(^%error |server exited|lost server|\(null\)|no current)' \ + "$OUT" >/dev/null 2>&1; then + fail "control client reported an error or invalid object" + fi + + awk ' + $1 == "%session-window-changed" { + if (NF != 3 || $2 !~ /^\$[0-9]+$/ || $3 !~ /^@[0-9]+$/) + bad = 1 + } + $1 == "%subscription-changed" { + colon = 0 + for (i = 2; i <= NF; i++) + if ($i == ":") + colon = 1 + if (NF < 7 || !colon) + bad = 1 + } + END { exit bad } + ' "$OUT" || fail "control client received a malformed notification" +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +run_tmux new-session -d -s life -n prompt -x 80 -y 24 'sleep 1000' \ + >/dev/null +run_tmux set-option -g detach-on-destroy off >/dev/null +run_tmux new-window -t life -n tree 'sleep 1000' >/dev/null +run_tmux new-window -t life -n work 'sleep 1000' >/dev/null +run_tmux select-window -t life:prompt >/dev/null + +run_tmux set-hook -g after-new-window \ + 'display-message -p "hook-new #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g after-split-window \ + 'display-message -p "hook-split #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g after-kill-pane \ + 'display-message -p "hook-kill #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g pane-exited \ + 'display-message -p "hook-exit #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g window-layout-changed \ + 'display-message -p "hook-layout #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g session-window-changed \ + 'display-message -p "hook-current #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux bind-key -n M-p command-prompt -P -p '(life)' \ + "set -g @lifecycle-prompt '%% #{session_name}:#{window_id}:#{pane_id}'" \ + >/dev/null + +$TMUX2 new-session -d -s outer -n prompt -x 80 -y 24 "$TMUX attach -t life" \ + || fail "failed to start first attached client" +$TMUX2 new-window -t outer -n tree "$TMUX attach -t life" \ + || fail "failed to start second attached client" +wait_clients 2 || fail "normal clients did not attach" + +$TMUX2 send-keys -t outer:prompt M-p || fail "failed to open command prompt" +sleep 1 +run_tmux choose-tree -t life:tree.0 \ + -F 'tree #{session_name}:#{window_id}:#{pane_id}' >/dev/null +wait_format life:tree.0 '#{pane_mode}' tree-mode || \ + fail "choose-tree did not enter tree-mode" + +mkfifo "$IN" || fail "failed to create control fifo" +(cat "$IN" | $TMUX -C attach -t life >"$OUT" 2>&1) & +CONTROL_PID=$! +exec 3>"$IN" +wait_clients 3 || fail "control client did not attach" + +CONTROL_CLIENT=$($TMUX list-clients -F '#{client_name} #{client_control_mode}' | + awk '$2 == 1 { print $1; exit }') +[ -n "$CONTROL_CLIENT" ] || fail "missing control client" + +send_control "refresh-client -B 'all:%*:#{session_name}:#{window_id}:#{pane_id}:#{session_windows}:#{window_panes}'" +send_control "refresh-client -B 'windows:@*:#{session_name}:#{window_id}:#{window_index}:#{window_panes}'" +sleep 1 + +run_tmux kill-pane -t life:prompt.0 >/dev/null +run_tmux kill-window -t life:tree >/dev/null +assert_alive "killing prompt and tree panes" + +i=1 +while [ "$i" -le 20 ]; do + # Keep this sequence fixed: failures should reproduce on the same pass. + s=ld$i + ctl=ctl$i + idx=$((50 + i)) + + run_tmux new-session -d -s "$s" -n base 'sleep 1000' >/dev/null + run_tmux split-window -t "$s:base" 'sleep 1000' >/dev/null + run_tmux respawn-pane -k -t "$s:base.1" 'sleep 1000' >/dev/null + run_tmux kill-pane -t "$s:base.1" >/dev/null + + run_tmux new-window -t "$s" -n second 'sleep 1000' >/dev/null + run_tmux link-window -s "$s:second" -t "life:$idx" >/dev/null + run_tmux unlink-window -t "life:$idx" >/dev/null + + run_tmux new-window -t "$s" -n single 'sleep 1000' >/dev/null + run_tmux kill-pane -t "$s:single.0" >/dev/null + run_tmux kill-window -t "$s:base" >/dev/null + + run_tmux new-session -d -s "$ctl" -n ctl 'sleep 1000' >/dev/null + run_tmux switch-client -c "$CONTROL_CLIENT" -t "$ctl" >/dev/null + run_tmux kill-session -t "$ctl" >/dev/null + + run_tmux kill-session -t "$s" >/dev/null + assert_alive "iteration $i" + + i=$((i + 1)) +done + +check_control_output +cleanup +exit 0 diff --git a/regress/mode-mutation.sh b/regress/mode-mutation.sh new file mode 100644 index 000000000..cab8e72c8 --- /dev/null +++ b/regress/mode-mutation.sh @@ -0,0 +1,297 @@ +#!/bin/sh + +# Exercise modes while their backing objects are changed from outside the +# client displaying the mode. This catches stale selection indexes and pointers +# after a mode list is shrunk or rebuilt. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + sleep 0.5 +} + +fail() +{ + echo "$1" + cleanup + exit 1 +} + +capture() +{ + $TMUX2 capture-pane -p -t out:0 2>/dev/null +} + +assert_alive() +{ + $TMUX display-message -p 'alive' >/dev/null 2>&1 || \ + fail "$1: server exited" +} + +wait_clients() +{ + i=0 + while [ "$i" -lt 50 ]; do + c=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "expected $1 clients, have $c" +} + +wait_for() +{ + i=0 + while [ "$i" -lt 50 ]; do + capture | grep -q "$1" && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1'" +} + +wait_mode() +{ + t=$1 + want=$2 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane $t mode state is $got, expected $want" +} + +repeat_key() +{ + t=$1 + key=$2 + count=$3 + + i=0 + while [ "$i" -lt "$count" ]; do + $TMUX send-keys -t "$t" "$key" || \ + fail "failed to send $key to $t" + i=$((i + 1)) + done +} + +start_client() +{ + s=$1 + cmd=${2:-cat} + + cleanup + $TMUX new-session -d -s "$s" -n main -x 80 -y 24 "$cmd" || \ + fail "$s: new-session failed" + $TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t $s" || \ + fail "$s: outer client failed" + wait_clients 1 +} + +new_outer_client() +{ + s=$1 + + $TMUX2 new-window -d -t out: "$TMUX attach -t $s" || \ + fail "$s: outer client failed" +} + +client_for_session() +{ + $TMUX list-clients -F '#{client_name} #{client_session}' | + awk -v s="$1" '$2 == s { print $1; exit }' +} + +test_choose_tree() +{ + start_client tree-a + $TMUX new-session -d -s tree-b -n one 'cat' || fail "tree-b failed" + $TMUX new-window -d -t tree-b -n two 'cat' || fail "tree-b:1 failed" + $TMUX new-session -d -s tree-c -n one 'cat' || fail "tree-c failed" + $TMUX new-session -d -s tree-d -n one 'cat' || fail "tree-d failed" + $TMUX split-window -d -t tree-a:0 'cat' || fail "tree split failed" + + $TMUX choose-tree -t tree-a:0 -O index -F 'MT #{session_name}:#{window_index}.#{pane_index}' || \ + fail "choose-tree failed" + wait_for 'MT ' + repeat_key tree-a:0 j 40 + + $TMUX kill-session -t tree-d || fail "tree kill-session failed" + $TMUX kill-session -t tree-c || fail "tree kill-session failed" + $TMUX rename-session -t tree-b tree-renamed || fail "tree rename failed" + $TMUX rename-window -t tree-renamed:0 renamed || fail "tree rename-window failed" + $TMUX kill-window -t tree-renamed:1 || fail "tree kill-window failed" + side=$($TMUX split-window -d -P -F '#{pane_id}' -t tree-a:0 'cat') || \ + fail "tree side split failed" + $TMUX break-pane -d -s "$side" || fail "tree break-pane failed" + $TMUX join-pane -d -s "$side" -t tree-a:0.0 || \ + fail "tree join-pane failed" + i=0 + while [ "$i" -lt 12 ]; do + $TMUX new-window -d -t tree-a -n "new$i" 'cat' || \ + fail "tree new-window failed" + i=$((i + 1)) + done + + assert_alive "choose-tree mutation" + $TMUX send-keys -t tree-a:0 k j l h Enter || \ + fail "choose-tree keys failed" + wait_mode tree-a:0 0 + assert_alive "choose-tree exit" +} + +test_choose_buffer() +{ + start_client buffer-a + + i=0 + while [ "$i" -lt 30 ]; do + $TMUX set-buffer -b "mbuf$i" "buffer mutation $i" || \ + fail "set-buffer failed" + i=$((i + 1)) + done + + $TMUX choose-buffer -t buffer-a:0 -F 'MB #{buffer_name}' || \ + fail "choose-buffer failed" + wait_for 'MB ' + repeat_key buffer-a:0 j 40 + + i=8 + while [ "$i" -lt 30 ]; do + $TMUX delete-buffer -b "mbuf$i" || fail "delete-buffer failed" + i=$((i + 1)) + done + i=30 + while [ "$i" -lt 50 ]; do + $TMUX set-buffer -b "mbuf$i" "new buffer mutation $i" || \ + fail "new set-buffer failed" + i=$((i + 1)) + done + + assert_alive "choose-buffer mutation" + $TMUX send-keys -t buffer-a:0 k j Enter || \ + fail "choose-buffer keys failed" + wait_mode buffer-a:0 0 + assert_alive "choose-buffer exit" +} + +test_choose_client() +{ + start_client client-a + $TMUX new-session -d -s client-b -n main 'cat' || fail "client-b failed" + $TMUX new-session -d -s client-c -n main 'cat' || fail "client-c failed" + new_outer_client client-b + new_outer_client client-c + wait_clients 3 + + $TMUX choose-client -t client-a:0 -F 'MC #{client_session}' || \ + fail "choose-client failed" + wait_for 'MC ' + repeat_key client-a:0 j 20 + + c=$(client_for_session client-c) + [ -n "$c" ] || fail "client-c client not found" + $TMUX detach-client -t "$c" || fail "detach client-c failed" + c=$(client_for_session client-b) + [ -n "$c" ] || fail "client-b client not found" + $TMUX detach-client -t "$c" || fail "detach client-b failed" + + $TMUX new-session -d -s client-d -n main 'cat' || fail "client-d failed" + new_outer_client client-d + wait_clients 2 + + assert_alive "choose-client mutation" + $TMUX send-keys -t client-a:0 k j Enter || \ + fail "choose-client keys failed" + wait_mode client-a:0 0 + assert_alive "choose-client exit" +} + +test_customize_mode() +{ + start_client option-a + + i=0 + while [ "$i" -lt 30 ]; do + $TMUX set-option -g "@mode_mut_$i" "$i" || \ + fail "set option failed" + i=$((i + 1)) + done + + $TMUX customize-mode -t option-a:0 -F 'MO #{option_name}=#{option_value}' || \ + fail "customize-mode failed" + wait_mode option-a:0 1 + repeat_key option-a:0 j 80 + + i=10 + while [ "$i" -lt 30 ]; do + $TMUX set-option -gu "@mode_mut_$i" || fail "unset option failed" + i=$((i + 1)) + done + i=30 + while [ "$i" -lt 55 ]; do + $TMUX set-option -g "@mode_mut_$i" "$i" || \ + fail "new option failed" + i=$((i + 1)) + done + $TMUX set-option -g status-left 'mutated' || fail "status-left failed" + $TMUX rename-session -t option-a option-renamed || fail "option rename failed" + + assert_alive "customize-mode mutation" + $TMUX send-keys -t option-renamed:0 k j C-d C-u q || \ + fail "customize-mode keys failed" + wait_mode option-renamed:0 0 + assert_alive "customize-mode exit" +} + +test_copy_mode() +{ + start_client copy-a 'i=0; while [ $i -lt 200 ]; do echo "copy mutation line $i"; i=$((i + 1)); done; cat' + $TMUX set-window-option -g mode-keys vi || fail "mode-keys failed" + $TMUX split-window -d -t copy-a:0 'cat' || fail "copy split failed" + + $TMUX copy-mode -t copy-a:0 || fail "copy-mode failed" + wait_mode copy-a:0 1 + repeat_key copy-a:0 k 20 + + $TMUX rename-window -t copy-a:0 renamed || fail "copy rename-window failed" + side=$($TMUX split-window -d -P -F '#{pane_id}' -t copy-a:renamed 'cat') || \ + fail "copy side split failed" + $TMUX break-pane -d -s "$side" || fail "copy break-pane failed" + $TMUX join-pane -d -s "$side" -t copy-a:renamed.0 || \ + fail "copy join-pane failed" + $TMUX kill-pane -t copy-a:renamed.1 || fail "copy kill-pane failed" + $TMUX new-window -d -t copy-a -n extra 'cat' || fail "copy new-window failed" + $TMUX kill-window -t copy-a:extra || fail "copy kill-window failed" + $TMUX rename-session -t copy-a copy-renamed || fail "copy rename failed" + + assert_alive "copy-mode mutation" + $TMUX send-keys -t copy-renamed:renamed.0 j k C-d C-u q || \ + fail "copy-mode keys failed" + wait_mode copy-renamed:renamed.0 0 + assert_alive "copy-mode exit" +} + +cleanup +test_choose_tree +test_choose_buffer +test_choose_client +test_customize_mode +test_copy_mode +cleanup +exit 0 From 9634538106b1a9777cceb85faecf0c05b5433657 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 5 Jul 2026 08:17:32 +0100 Subject: [PATCH 045/127] Fix test. --- regress/input-reflow-stress.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/regress/input-reflow-stress.sh b/regress/input-reflow-stress.sh index c673b3472..9b12f73d2 100644 --- a/regress/input-reflow-stress.sh +++ b/regress/input-reflow-stress.sh @@ -1,5 +1,12 @@ #!/bin/sh +PATH=/bin:/usr/bin TERM=screen +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMP=$(mktemp) +EXP=$(mktemp) +trap 'rm -f "$TMP" "$EXP"; $TMUX kill-server 2>/dev/null' 0 1 15 + WIDTHS="80 40 20 10 7 5 4 3 2 1 2 3 4 5 7 10 20 40 80" HISTORY_LIMIT=220 HISTORY_BOUND=12000 From 7ff972773a1ec46bac40ee55688453fd99bd921a Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 5 Jul 2026 09:06:19 +0100 Subject: [PATCH 046/127] Bump some times. --- regress/choose-buffer.sh | 10 +++++----- regress/choose-client.sh | 4 ++-- regress/choose-tree.sh | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/regress/choose-buffer.sh b/regress/choose-buffer.sh index 0f3e0d8da..7d689f523 100644 --- a/regress/choose-buffer.sh +++ b/regress/choose-buffer.sh @@ -47,10 +47,10 @@ wait_for() i=0 while [ "$i" -lt 50 ]; do if capture | grep -q "$1"; then - sleep 0.2 + sleep 0.5 return 0 fi - sleep 0.2 + sleep 0.5 i=$((i + 1)) done fail "timed out waiting for '$1'" @@ -65,7 +65,7 @@ wait_buffers() while [ "$i" -lt 50 ]; do c=$($TMUX list-buffers -F x 2>/dev/null | grep -c x) [ "$c" -eq "$1" ] && return 0 - sleep 0.2 + sleep 0.5 i=$((i + 1)) done fail "expected $1 buffers, have $c" @@ -183,14 +183,14 @@ $TMUX send-keys -t aaa:0 Enter i=0 while [ "$i" -lt 50 ]; do [ "$($TMUX display -p -t aaa:0 '#{pane_in_mode}')" = "0" ] && break - sleep 0.2 + sleep 0.5 i=$((i + 1)) done [ "$i" -lt 50 ] || fail "mode did not exit after Enter" i=0 while [ "$i" -lt 50 ]; do $TMUX capture-pane -p -t aaa:0 | grep -q 'hello buffer' && break - sleep 0.2 + sleep 0.5 i=$((i + 1)) done [ "$i" -lt 50 ] || fail "buffer not pasted into pane" diff --git a/regress/choose-client.sh b/regress/choose-client.sh index e299b9852..1e8423415 100644 --- a/regress/choose-client.sh +++ b/regress/choose-client.sh @@ -46,10 +46,10 @@ wait_for() i=0 while [ "$i" -lt 50 ]; do if capture | grep -q "$1"; then - sleep 0.2 + sleep 0.5 return 0 fi - sleep 0.2 + sleep 0.5 i=$((i + 1)) done fail "timed out waiting for '$1'" diff --git a/regress/choose-tree.sh b/regress/choose-tree.sh index a82e42c20..15add9f3f 100644 --- a/regress/choose-tree.sh +++ b/regress/choose-tree.sh @@ -54,10 +54,10 @@ wait_for() i=0 while [ "$i" -lt 50 ]; do if capture | grep -q "$1"; then - sleep 0.2 + sleep 0.5 return 0 fi - sleep 0.2 + sleep 0.5 i=$((i + 1)) done fail "timed out waiting for '$1'" @@ -71,7 +71,7 @@ wait_count() i=0 while [ "$i" -lt 50 ]; do [ "$(capture | grep -c "$1")" -eq "$2" ] && return 0 - sleep 0.2 + sleep 0.5 i=$((i + 1)) done fail "timed out waiting for $2 lines of '$1' (have $(capture | grep -c "$1"))" @@ -222,7 +222,7 @@ $TMUX send-keys -t aaa:0 g Enter i=0 while [ "$i" -lt 50 ]; do [ "$($TMUX list-clients -F '#{client_session}')" = "zzz" ] && break - sleep 0.2 + sleep 0.5 i=$((i + 1)) done [ "$i" -lt 50 ] || fail "client did not switch to zzz" From de024e04d7c5f65c95bc41e2e90cd137f81ef1de Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 5 Jul 2026 08:24:00 +0000 Subject: [PATCH 047/127] Add set-hook -B install a subscription like in control mode. Subscriptions are formats which are checked once a second and if changed invoke a hook. show-hooks -B lists. --- cmd-refresh-client.c | 37 ++----- cmd-set-option.c | 101 +++++++++++++++++- cmd-show-options.c | 47 ++++++++- control.c | 4 +- monitor.c | 244 ++++++++++++++++++++++++++++++++----------- notify.c | 218 +++++++++++++++++++++++++++++++++++--- options.c | 15 +++ tmux.1 | 41 +++++++- tmux.h | 17 ++- 9 files changed, 607 insertions(+), 117 deletions(-) diff --git a/cmd-refresh-client.c b/cmd-refresh-client.c index 16269e249..6f22da83e 100644 --- a/cmd-refresh-client.c +++ b/cmd-refresh-client.c @@ -46,36 +46,17 @@ const struct cmd_entry cmd_refresh_client_entry = { static void cmd_refresh_client_update_subscription(struct client *tc, const char *value) { - char *copy, *split, *name, *what; - enum monitor_type subtype; - int subid = -1; + char *name, *format; + enum monitor_type type; + int id; - copy = name = xstrdup(value); - if ((split = strchr(copy, ':')) == NULL) { - control_remove_sub(tc, copy); - goto out; + if (monitor_parse(value, &name, &type, &id, &format) != 0) { + control_remove_sub(tc, value); + return; } - *split++ = '\0'; - - what = split; - if ((split = strchr(what, ':')) == NULL) - goto out; - *split++ = '\0'; - - if (strcmp(what, "%*") == 0) - subtype = MONITOR_ALL_PANES; - else if (sscanf(what, "%%%d", &subid) == 1 && subid >= 0) - subtype = MONITOR_PANE; - else if (strcmp(what, "@*") == 0) - subtype = MONITOR_ALL_WINDOWS; - else if (sscanf(what, "@%d", &subid) == 1 && subid >= 0) - subtype = MONITOR_WINDOW; - else - subtype = MONITOR_SESSION; - control_add_sub(tc, name, subtype, subid, split); - -out: - free(copy); + control_add_sub(tc, name, type, id, format); + free(name); + free(format); } static enum cmd_retval diff --git a/cmd-set-option.c b/cmd-set-option.c index fbe32cfae..5b5ea3d88 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -31,6 +31,8 @@ static enum args_parse_type cmd_set_option_args_parse(struct args *, u_int, char **); static enum cmd_retval cmd_set_option_exec(struct cmd *, struct cmdq_item *); +static enum cmd_retval cmd_set_hook_monitor_exec(struct cmdq_item *, + struct args *, int); const struct cmd_entry cmd_set_option_entry = { .name = "set-option", @@ -62,8 +64,9 @@ const struct cmd_entry cmd_set_hook_entry = { .name = "set-hook", .alias = NULL, - .args = { "agpRt:uw", 1, 2, cmd_set_option_args_parse }, - .usage = "[-agpRuw] " CMD_TARGET_PANE_USAGE " hook [command]", + .args = { "agpRt:uB:w", 0, 2, cmd_set_option_args_parse }, + .usage = "[-agpRuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " + "[hook] [command]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -72,14 +75,100 @@ const struct cmd_entry cmd_set_hook_entry = { }; static enum args_parse_type -cmd_set_option_args_parse(__unused struct args *args, u_int idx, +cmd_set_option_args_parse(struct args *args, u_int idx, __unused char **cause) { + if (args_has(args, 'B')) + return (ARGS_PARSE_COMMANDS_OR_STRING); if (idx == 1) return (ARGS_PARSE_COMMANDS_OR_STRING); return (ARGS_PARSE_STRING); } +static enum cmd_retval +cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) +{ + struct cmd_find_state *target = cmdq_get_target(item), fs; + struct options *oo; + struct options_entry *o; + char *cause = NULL, *name = NULL, *format = NULL; + char *expanded = NULL, *newvalue = NULL; + const char *value, *old; + enum monitor_type type; + int id, scope; + + if (args_count(args) > 1) { + cmdq_error(item, "too many arguments"); + return (CMD_RETURN_ERROR); + } + + value = args_get(args, 'B'); + if (args_has(args, 'u')) { + if (monitor_parse(value, &name, &type, &id, &format) != 0) + name = xstrdup(value); + free(format); + format = NULL; + } else { + if (monitor_parse(value, &name, &type, &id, &format) != 0) { + cmdq_error(item, "invalid subscription: %s", value); + return (CMD_RETURN_ERROR); + } + } + + if (*name != '@') { + cmdq_error(item, "monitor hook name must start with @"); + goto fail; + } + + scope = options_scope_from_name(args, window, name, target, &oo, + &cause); + if (scope == OPTIONS_TABLE_NONE) { + cmdq_error(item, "%s", cause); + free(cause); + goto fail; + } + cmd_find_copy_state(&fs, target); + + if (args_has(args, 'u')) { + notify_monitor_remove(oo, name); + goto out; + } + + if (args_count(args) != 0) { + value = args_string(args, 0); + if (args_has(args, 'F')) { + expanded = format_single_from_target(item, value); + value = expanded; + } + o = options_get_only(oo, name); + if (!args_has(args, 'o') || o == NULL) { + if (args_has(args, 'a') && o != NULL) { + old = options_get_string(oo, name); + xasprintf(&newvalue, "%s%s", old, value); + value = newvalue; + } + options_set_string(oo, name, 0, "%s", value); + options_push_changes(name); + } + } + + notify_monitor_add(item, oo, name, type, id, format, &fs, target->s); + +out: + free(newvalue); + free(expanded); + free(name); + free(format); + return (CMD_RETURN_NORMAL); + +fail: + free(newvalue); + free(expanded); + free(name); + free(format); + return (CMD_RETURN_ERROR); +} + static enum cmd_retval cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) { @@ -96,6 +185,12 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) int scope; window = (cmd_get_entry(self) == &cmd_set_window_option_entry); + if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'B')) + return (cmd_set_hook_monitor_exec(item, args, window)); + if (args_count(args) == 0) { + cmdq_error(item, "missing argument"); + return (CMD_RETURN_ERROR); + } /* Expand argument. */ argument = format_single_from_target(item, args_string(args, 0)); diff --git a/cmd-show-options.c b/cmd-show-options.c index 684940607..0a898156a 100644 --- a/cmd-show-options.c +++ b/cmd-show-options.c @@ -32,6 +32,10 @@ static enum cmd_retval cmd_show_options_exec(struct cmd *, struct cmdq_item *); static void cmd_show_options_print(struct cmd *, struct cmdq_item *, struct options_entry *, int, int); +static void cmd_show_hooks_print_monitor(struct cmdq_item *, + struct options_entry *); +static enum cmd_retval cmd_show_hooks_monitor(struct cmd *, struct cmdq_item *, + int, struct options *); static enum cmd_retval cmd_show_options_all(struct cmd *, struct cmdq_item *, int, struct options *); @@ -65,8 +69,8 @@ const struct cmd_entry cmd_show_hooks_entry = { .name = "show-hooks", .alias = NULL, - .args = { "gpt:w", 0, 1, NULL }, - .usage = "[-gpw] " CMD_TARGET_PANE_USAGE " [hook]", + .args = { "Bgpt:w", 0, 1, NULL }, + .usage = "[-Bgpw] " CMD_TARGET_PANE_USAGE " [hook]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -96,6 +100,9 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) free(cause); return (CMD_RETURN_ERROR); } + if (cmd_get_entry(self) == &cmd_show_hooks_entry && + args_has(args, 'B')) + return (cmd_show_hooks_monitor(self, item, scope, oo)); return (cmd_show_options_all(self, item, scope, oo)); } argument = format_single_from_target(item, args_string(args, 0)); @@ -125,8 +132,13 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) parent = 1; } else parent = 0; - if (o != NULL) - cmd_show_options_print(self, item, o, idx, parent); + if (o != NULL) { + if (cmd_get_entry(self) == &cmd_show_hooks_entry && + args_has(args, 'B')) + cmd_show_hooks_print_monitor(item, o); + else + cmd_show_options_print(self, item, o, idx, parent); + } else if (*name == '@') { if (args_has(args, 'q')) goto out; @@ -196,6 +208,33 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item, free(tmp); } +static void +cmd_show_hooks_print_monitor(struct cmdq_item *item, struct options_entry *o) +{ + char *value; + + value = notify_monitor_to_string(o); + if (value == NULL) + return; + cmdq_print(item, "%s", value); + free(value); +} + +/* Show all hook monitors. */ +static enum cmd_retval +cmd_show_hooks_monitor(__unused struct cmd *self, struct cmdq_item *item, + __unused int scope, struct options *oo) +{ + struct options_entry *o; + + o = options_first(oo); + while (o != NULL) { + cmd_show_hooks_print_monitor(item, o); + o = options_next(o); + } + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, struct options *oo) diff --git a/control.c b/control.c index eb2db98ef..1b026e849 100644 --- a/control.c +++ b/control.c @@ -702,7 +702,7 @@ control_start(struct client *c) RB_INIT(&cs->panes); TAILQ_INIT(&cs->pending_list); TAILQ_INIT(&cs->all_blocks); - cs->subs = monitor_create(c, control_sub_change, NULL); + cs->subs = monitor_create_client(c, control_sub_change, NULL); cs->read_event = bufferevent_new(c->fd, control_read_callback, control_write_callback, control_error_callback, c); @@ -776,7 +776,7 @@ control_add_sub(struct client *c, const char *name, enum monitor_type type, { struct control_state *cs = c->control_state; - monitor_add(cs->subs, name, type, id, format); + monitor_add(cs->subs, name, type, id, format, MONITOR_NOTIFY_INITIAL); } /* Remove a subscription. */ diff --git a/monitor.c b/monitor.c index 512331e6a..5a2408009 100644 --- a/monitor.c +++ b/monitor.c @@ -29,6 +29,7 @@ struct monitor_pane { u_int pane; u_int idx; char *last; + u_int generation; RB_ENTRY(monitor_pane) entry; }; @@ -39,6 +40,7 @@ struct monitor_window { u_int window; u_int idx; char *last; + u_int generation; RB_ENTRY(monitor_window) entry; }; @@ -51,6 +53,7 @@ struct monitor_item { enum monitor_type type; u_int id; + u_int flags; char *last; struct monitor_panes panes; @@ -63,15 +66,33 @@ RB_HEAD(monitor_items, monitor_item); /* Monitored subscription set. */ struct monitor_set { struct client *client; + struct session *session; monitor_cb cb; void *data; struct monitor_items items; struct event timer; + u_int generation; }; static void monitor_timer(__unused int, __unused short, void *); +/* Get the session for this monitor set. */ +static struct session * +monitor_get_session(struct monitor_set *ms) +{ + struct session *s; + + if (ms->client != NULL) + return (ms->client->session); + s = ms->session; + if (s == NULL) + return (RB_MIN(sessions, &sessions)); + if (session_find_by_id(s->id) != s) + return (NULL); + return (s); +} + /* Compare subscriptions. */ static int monitor_item_cmp(struct monitor_item *m1, struct monitor_item *m2) @@ -141,38 +162,56 @@ monitor_free_item(struct monitor_set *ms, struct monitor_item *me) static void monitor_report(struct monitor_set *ms, struct monitor_item *me, struct session *s, struct winlink *wl, struct window_pane *wp, - const char *value) + const char *value, const char *last) { - struct monitor_change change; + struct monitor_change change = { 0 }; + + log_debug("%s: %s changed to %s", __func__, me->name, value); change.name = me->name; change.value = value; + change.last = last; change.c = ms->client; change.s = s; change.wl = wl; change.wp = wp; - log_debug("%s: %s changed to %s", __func__, me->name, value); ms->cb(&change, ms->data); } +/* Check a value against its last value and report if changed. */ +static void +monitor_check_value(struct monitor_set *ms, struct monitor_item *me, + struct session *s, struct winlink *wl, struct window_pane *wp, + char *value, char **last) +{ + if (*last == NULL) { + *last = value; + if (me->flags & MONITOR_NOTIFY_INITIAL) + monitor_report(ms, me, s, wl, wp, value, NULL); + return; + } + + if (strcmp(value, *last) == 0) { + free(value); + return; + } + + monitor_report(ms, me, s, wl, wp, value, *last); + free(*last); + *last = value; +} + /* Check session subscription. */ static void monitor_check_session(struct monitor_set *ms, struct monitor_item *me, struct format_tree *ft) { - struct session *s = ms->client->session; + struct session *s = monitor_get_session(ms); char *value; value = format_expand(ft, me->format); - if (me->last != NULL && strcmp(value, me->last) == 0) { - free(value); - return; - } - - monitor_report(ms, me, s, NULL, NULL, value); - free(me->last); - me->last = value; + monitor_check_value(ms, me, s, NULL, NULL, value, &me->last); } /* Check pane subscription. */ @@ -180,7 +219,7 @@ static void monitor_check_pane(struct monitor_set *ms, struct monitor_item *me) { struct client *c = ms->client; - struct session *s = c->session; + struct session *s = monitor_get_session(ms); struct window_pane *wp; struct window *w; struct winlink *wl; @@ -211,14 +250,7 @@ monitor_check_pane(struct monitor_set *ms, struct monitor_item *me) RB_INSERT(monitor_panes, &me->panes, mp); } - if (mp->last != NULL && strcmp(value, mp->last) == 0) { - free(value); - continue; - } - - monitor_report(ms, me, s, wl, wp, value); - free(mp->last); - mp->last = value; + monitor_check_value(ms, me, s, wl, wp, value, &mp->last); } } @@ -227,7 +259,7 @@ static void monitor_check_all_panes_one(struct monitor_set *ms, struct monitor_item *me, struct format_tree *ft, struct winlink *wl, struct window_pane *wp) { - struct session *s = ms->client->session; + struct session *s = monitor_get_session(ms); char *value; struct monitor_pane *mp, find; @@ -242,15 +274,24 @@ monitor_check_all_panes_one(struct monitor_set *ms, struct monitor_item *me, mp->idx = wl->idx; RB_INSERT(monitor_panes, &me->panes, mp); } + mp->generation = ms->generation; - if (mp->last != NULL && strcmp(value, mp->last) == 0) { - free(value); - return; + monitor_check_value(ms, me, s, wl, wp, value, &mp->last); +} + +/* Remove all-panes entries not seen during the current scan. */ +static void +monitor_sweep_all_panes(struct monitor_item *me, u_int generation) +{ + struct monitor_pane *mp, *mp1; + + RB_FOREACH_SAFE(mp, monitor_panes, &me->panes, mp1) { + if (mp->generation == generation) + continue; + RB_REMOVE(monitor_panes, &me->panes, mp); + free(mp->last); + free(mp); } - - monitor_report(ms, me, s, wl, wp, value); - free(mp->last); - mp->last = value; } /* Check window subscription. */ @@ -258,7 +299,7 @@ static void monitor_check_window(struct monitor_set *ms, struct monitor_item *me) { struct client *c = ms->client; - struct session *s = c->session; + struct session *s = monitor_get_session(ms); struct window *w; struct winlink *wl; struct format_tree *ft; @@ -287,14 +328,7 @@ monitor_check_window(struct monitor_set *ms, struct monitor_item *me) RB_INSERT(monitor_windows, &me->windows, mw); } - if (mw->last != NULL && strcmp(value, mw->last) == 0) { - free(value); - continue; - } - - monitor_report(ms, me, s, wl, NULL, value); - free(mw->last); - mw->last = value; + monitor_check_value(ms, me, s, wl, NULL, value, &mw->last); } } @@ -303,7 +337,7 @@ static void monitor_check_all_windows_one(struct monitor_set *ms, struct monitor_item *me, struct format_tree *ft, struct winlink *wl) { - struct session *s = ms->client->session; + struct session *s = monitor_get_session(ms); struct window *w = wl->window; char *value; struct monitor_window *mw, find; @@ -319,15 +353,24 @@ monitor_check_all_windows_one(struct monitor_set *ms, struct monitor_item *me, mw->idx = wl->idx; RB_INSERT(monitor_windows, &me->windows, mw); } + mw->generation = ms->generation; - if (mw->last != NULL && strcmp(value, mw->last) == 0) { - free(value); - return; + monitor_check_value(ms, me, s, wl, NULL, value, &mw->last); +} + +/* Remove all-windows entries not seen during the current scan. */ +static void +monitor_sweep_all_windows(struct monitor_item *me, u_int generation) +{ + struct monitor_window *mw, *mw1; + + RB_FOREACH_SAFE(mw, monitor_windows, &me->windows, mw1) { + if (mw->generation == generation) + continue; + RB_REMOVE(monitor_windows, &me->windows, mw); + free(mw->last); + free(mw); } - - monitor_report(ms, me, s, wl, NULL, value); - free(mw->last); - mw->last = value; } /* Check session subscriptions. */ @@ -335,7 +378,7 @@ static void monitor_check_sessions(struct monitor_set *ms) { struct client *c = ms->client; - struct session *s = c->session; + struct session *s = monitor_get_session(ms); struct monitor_item *me, *me1; struct format_tree *ft; @@ -374,12 +417,14 @@ static void monitor_check_all_panes(struct monitor_set *ms) { struct client *c = ms->client; - struct session *s = c->session; + struct session *s = monitor_get_session(ms); struct monitor_item *me, *me1; struct window_pane *wp; struct format_tree *ft; struct winlink *wl; + if (++ms->generation == 0) + ms->generation = 1; RB_FOREACH(wl, winlinks, &s->windows) { TAILQ_FOREACH(wp, &wl->window->panes, entry) { ft = format_create_defaults(NULL, c, s, wl, wp); @@ -391,6 +436,10 @@ monitor_check_all_panes(struct monitor_set *ms) format_free(ft); } } + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { + if (me->type == MONITOR_ALL_PANES) + monitor_sweep_all_panes(me, ms->generation); + } } /* Check all-windows subscriptions. */ @@ -398,11 +447,13 @@ static void monitor_check_all_windows(struct monitor_set *ms) { struct client *c = ms->client; - struct session *s = c->session; + struct session *s = monitor_get_session(ms); struct monitor_item *me, *me1; struct format_tree *ft; struct winlink *wl; + if (++ms->generation == 0) + ms->generation = 1; RB_FOREACH(wl, winlinks, &s->windows) { ft = format_create_defaults(NULL, c, s, wl, NULL); RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { @@ -412,6 +463,10 @@ monitor_check_all_windows(struct monitor_set *ms) } format_free(ft); } + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { + if (me->type == MONITOR_ALL_WINDOWS) + monitor_sweep_all_windows(me, ms->generation); + } } /* Check subscriptions. */ @@ -419,7 +474,6 @@ static void monitor_timer(__unused int fd, __unused short events, void *data) { struct monitor_set *ms = data; - struct client *c = ms->client; struct monitor_item *me; struct timeval tv = { .tv_sec = 1 }; int have_session = 0, have_all_panes = 0; @@ -428,7 +482,7 @@ monitor_timer(__unused int fd, __unused short events, void *data) log_debug("%s: timer fired", __func__); evtimer_add(&ms->timer, &tv); - if (c->session == NULL) + if (monitor_get_session(ms) == NULL) return; RB_FOREACH(me, monitor_items, &ms->items) { @@ -458,39 +512,104 @@ monitor_timer(__unused int fd, __unused short events, void *data) } /* Create a monitor set. */ -struct monitor_set * -monitor_create(struct client *c, monitor_cb cb, void *data) +static struct monitor_set * +monitor_create(monitor_cb cb, void *data) { struct monitor_set *ms; ms = xcalloc(1, sizeof *ms); - ms->client = c; ms->cb = cb; ms->data = data; RB_INIT(&ms->items); return (ms); } +/* Create a client monitor set. */ +struct monitor_set * +monitor_create_client(struct client *c, monitor_cb cb, void *data) +{ + struct monitor_set *ms; + + ms = monitor_create(cb, data); + ms->client = c; + return (ms); +} + +/* Create a monitor set for a session. */ +struct monitor_set * +monitor_create_session(struct session *s, monitor_cb cb, void *data) +{ + struct monitor_set *ms; + + ms = monitor_create(cb, data); + ms->session = s; + if (s != NULL) + session_add_ref(s, __func__); + return (ms); +} + /* Destroy a monitor set. */ void monitor_destroy(struct monitor_set *ms) { struct monitor_item *me, *me1; - if (ms == NULL) - return; + if (ms != NULL) { + if (evtimer_initialized(&ms->timer)) + evtimer_del(&ms->timer); + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) + monitor_free_item(ms, me); + if (ms->session != NULL) + session_remove_ref(ms->session, __func__); + free(ms); + } +} - if (evtimer_initialized(&ms->timer)) - evtimer_del(&ms->timer); - RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) - monitor_free_item(ms, me); - free(ms); +/* Parse a subscription. */ +int +monitor_parse(const char *value, char **name, enum monitor_type *type, int *id, + char **format) +{ + char *copy, *what, *split; + + copy = xstrdup(value); + *id = -1; + + what = strchr(copy, ':'); + if (what == NULL) + goto fail; + *what++ = '\0'; + + split = strchr(what, ':'); + if (split == NULL) + goto fail; + *split++ = '\0'; + + if (strcmp(what, "%*") == 0) + *type = MONITOR_ALL_PANES; + else if (sscanf(what, "%%%d", id) == 1 && *id >= 0) + *type = MONITOR_PANE; + else if (strcmp(what, "@*") == 0) + *type = MONITOR_ALL_WINDOWS; + else if (sscanf(what, "@%d", id) == 1 && *id >= 0) + *type = MONITOR_WINDOW; + else + *type = MONITOR_SESSION; + *name = xstrdup(copy); + *format = xstrdup(split); + + free(copy); + return (0); + +fail: + free(copy); + return (-1); } /* Add a subscription. */ void monitor_add(struct monitor_set *ms, const char *name, enum monitor_type type, - int id, const char *format) + int id, const char *format, u_int flags) { struct monitor_item *me, find = { .name = (char *)name }; struct timeval tv = { .tv_sec = 1 }; @@ -503,6 +622,7 @@ monitor_add(struct monitor_set *ms, const char *name, enum monitor_type type, me->format = xstrdup(format); me->type = type; me->id = id; + me->flags = flags; RB_INIT(&me->panes); RB_INIT(&me->windows); RB_INSERT(monitor_items, &ms->items, me); diff --git a/notify.c b/notify.c index ac5d2e924..50bb55022 100644 --- a/notify.c +++ b/notify.c @@ -28,12 +28,25 @@ struct notify_entry { const char *name; struct cmd_find_state fs; struct format_tree *formats; + struct options *oo; struct client *client; struct session *session; struct window *window; int pane; const char *pbname; + int expand; +}; + +struct notify_monitor { + struct options *oo; + + struct monitor_set *set; + struct cmd_find_state fs; + + enum monitor_type type; + int id; + char *format; }; static struct cmdq_item * @@ -51,7 +64,31 @@ notify_insert_one_hook(struct cmdq_item *item, struct notify_entry *ne, free(s); } new_item = cmdq_get_command(cmdlist, state); - return (cmdq_insert_after(item, new_item)); + if (item != NULL) + return (cmdq_insert_after(item, new_item)); + return (cmdq_append(NULL, new_item)); +} + +static struct cmd_parse_result * +notify_parse_hook(struct notify_entry *ne, struct cmd_find_state *fs, + const char *value) +{ + struct cmd_parse_result *pr; + struct format_tree *ft; + char *expanded; + + if (!ne->expand) + return (cmd_parse_from_string(value, NULL)); + + ft = format_create_defaults(NULL, ne->client, fs->s, fs->wl, fs->wp); + if (ne->formats != NULL) + format_merge(ft, ne->formats); + expanded = format_expand(ft, value); + format_free(ft); + + pr = cmd_parse_from_string(expanded, NULL); + free(expanded); + return (pr); } static void @@ -74,18 +111,23 @@ notify_insert_hook(struct cmdq_item *item, struct notify_entry *ne) else cmd_find_copy_state(&fs, &ne->fs); - if (fs.s == NULL) - oo = global_s_options; - else - oo = fs.s->options; - o = options_get(oo, ne->name); - if (o == NULL && fs.wp != NULL) { - oo = fs.wp->options; - o = options_get(oo, ne->name); - } - if (o == NULL && fs.wl != NULL) { - oo = fs.wl->window->options; + if (ne->oo != NULL) { + oo = ne->oo; + o = options_get_only(oo, ne->name); + } else { + if (fs.s == NULL) + oo = global_s_options; + else + oo = fs.s->options; o = options_get(oo, ne->name); + if (o == NULL && fs.wp != NULL) { + oo = fs.wp->options; + o = options_get(oo, ne->name); + } + if (o == NULL && fs.wl != NULL) { + oo = fs.wl->window->options; + o = options_get(oo, ne->name); + } } if (o == NULL) { log_debug("%s: hook %s not found", __func__, ne->name); @@ -97,7 +139,7 @@ notify_insert_hook(struct cmdq_item *item, struct notify_entry *ne) if (*ne->name == '@') { value = options_get_string(oo, ne->name); - pr = cmd_parse_from_string(value, NULL); + pr = notify_parse_hook(ne, &fs, value); switch (pr->status) { case CMD_PARSE_ERROR: log_debug("%s: can't parse hook %s: %s", __func__, @@ -111,8 +153,24 @@ notify_insert_hook(struct cmdq_item *item, struct notify_entry *ne) } else { a = options_array_first(o); while (a != NULL) { - cmdlist = options_array_item_value(a)->cmdlist; - item = notify_insert_one_hook(item, ne, cmdlist, state); + if (ne->expand) { + value = options_array_item_value(a)->string; + pr = notify_parse_hook(ne, &fs, value); + switch (pr->status) { + case CMD_PARSE_ERROR: + if (pr->error != NULL) + cmdq_error(item, "%s", pr->error); + break; + case CMD_PARSE_SUCCESS: + item = notify_insert_one_hook(item, ne, + pr->cmdlist, state); + break; + } + } else { + cmdlist = options_array_item_value(a)->cmdlist; + item = notify_insert_one_hook(item, ne, cmdlist, + state); + } a = options_array_next(a); } } @@ -176,6 +234,136 @@ notify_callback(struct cmdq_item *item, void *data) return (CMD_RETURN_NORMAL); } +void +notify_monitor_free(void *data) +{ + struct notify_monitor *nhm = data; + + monitor_destroy(nhm->set); + free(nhm->format); + free(nhm); +} + +void +notify_monitor_remove(struct options *oo, const char *name) +{ + struct options_entry *o; + struct notify_monitor *nhm; + + o = options_get_only(oo, name); + if (o == NULL) + return; + + nhm = options_get_monitor_data(o); + if (nhm != NULL) { + options_set_monitor_data(o, NULL); + notify_monitor_free(nhm); + } +} + +static void +notify_monitor_cb(struct monitor_change *change, void *data) +{ + struct notify_monitor *nhm = data; + struct notify_entry ne; + struct cmdq_item *item; + struct window *w; + + item = cmdq_running(NULL); + if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) + return; + + memset(&ne, 0, sizeof ne); + ne.name = change->name; + ne.oo = nhm->oo; + ne.client = change->c; + ne.expand = 1; + if (change->wp != NULL && change->wl != NULL) + cmd_find_from_winlink_pane(&ne.fs, change->wl, change->wp, 0); + else if (change->wl != NULL) + cmd_find_from_winlink(&ne.fs, change->wl, 0); + else if (change->s != NULL) + cmd_find_from_session(&ne.fs, change->s, 0); + else + cmd_find_copy_state(&ne.fs, &nhm->fs); + ne.formats = format_create(change->c, item, FORMAT_NONE, FORMAT_NOJOBS); + format_add(ne.formats, "hook", "%s", change->name); + format_add(ne.formats, "hook_value", "%s", change->value); + format_add(ne.formats, "hook_last", "%s", + change->last == NULL ? "" : change->last); + if (change->s != NULL) { + format_add(ne.formats, "hook_session", "$%u", change->s->id); + format_add(ne.formats, "hook_session_name", "%s", change->s->name); + } + if (change->wl != NULL) { + w = change->wl->window; + format_add(ne.formats, "hook_window", "@%u", w->id); + format_add(ne.formats, "hook_window_name", "%s", w->name); + format_add(ne.formats, "hook_window_index", "%d", change->wl->idx); + } + if (change->wp != NULL) { + format_add(ne.formats, "hook_pane", "%%%u", change->wp->id); + } + + notify_insert_hook(item, &ne); + format_free(ne.formats); +} + +void +notify_monitor_add(__unused struct cmdq_item *item, struct options *oo, + const char *name, enum monitor_type type, int id, const char *format, + struct cmd_find_state *fs, struct session *s) +{ + struct options_entry *o; + struct notify_monitor *nhm; + + notify_monitor_remove(oo, name); + o = options_get_only(oo, name); + if (o == NULL) + o = options_set_string(oo, name, 0, "%s", ""); + + nhm = xcalloc(1, sizeof *nhm); + nhm->oo = oo; + cmd_find_copy_state(&nhm->fs, fs); + nhm->type = type; + nhm->id = id; + nhm->format = xstrdup(format); + nhm->set = monitor_create_session(s, notify_monitor_cb, nhm); + options_set_monitor_data(o, nhm); + monitor_add(nhm->set, name, type, id, format, 0); +} + +/* Convert a hook monitor to its value. */ +char * +notify_monitor_to_string(struct options_entry *o) +{ + struct notify_monitor *nhm = options_get_monitor_data(o); + const char *name = options_name(o); + char *s; + + if (nhm == NULL) + return (NULL); + + switch (nhm->type) { + case MONITOR_SESSION: + xasprintf(&s, "%s::%s", name, nhm->format); + break; + case MONITOR_PANE: + xasprintf(&s, "%s:%%%d:%s", name, nhm->id, nhm->format); + break; + case MONITOR_ALL_PANES: + xasprintf(&s, "%s:%%*:%s", name, nhm->format); + break; + case MONITOR_WINDOW: + xasprintf(&s, "%s:@%d:%s", name, nhm->id, nhm->format); + break; + case MONITOR_ALL_WINDOWS: + xasprintf(&s, "%s:@*:%s", name, nhm->format); + break; + } + return (s); +} + static void notify_add(const char *name, struct cmd_find_state *fs, struct client *c, struct session *s, struct window *w, struct window_pane *wp, diff --git a/options.c b/options.c index e34320869..19da38886 100644 --- a/options.c +++ b/options.c @@ -56,6 +56,7 @@ struct options_entry { int cached; struct style style; + void *monitor_data; RB_ENTRY(options_entry) entry; }; @@ -354,6 +355,8 @@ options_remove(struct options_entry *o) options_array_clear(o); else options_value_free(o, &o->value); + if (o->monitor_data != NULL) + notify_monitor_free(o->monitor_data); RB_REMOVE(options_tree, &oo->tree, o); free((void *)o->name); free(o); @@ -371,6 +374,18 @@ options_owner(struct options_entry *o) return (o->owner); } +void * +options_get_monitor_data(struct options_entry *o) +{ + return (o->monitor_data); +} + +void +options_set_monitor_data(struct options_entry *o, void *data) +{ + o->monitor_data = data; +} + const struct options_table_entry * options_table_entry(struct options_entry *o) { diff --git a/tmux.1 b/tmux.1 index 4eb9d9f08..54d30072b 100644 --- a/tmux.1 +++ b/tmux.1 @@ -6395,6 +6395,7 @@ Hooks are managed with these commands: .Bl -tag -width Ds .It Xo Ic set\-hook .Op Fl agpRuw +.Op Fl B Ar name:what:format .Op Fl t Ar target\-pane .Ar hook\-name .Op Ar command @@ -6411,18 +6412,53 @@ The flags are the same as for .Ic set\-option . .Pp With +.Fl B , +.Ar name:what:format +uses the same subscription syntax as +.Ic refresh\-client +.Fl B : +.Ar name +is the hook to run, +.Ar what +selects the session, pane, all panes, window, or all windows, and +.Ar format +is expanded once a second. +For monitor hooks, +.Ar name +must begin with +.Ql @ . +If +.Ar command +is given, it is stored as the +.Ql @ +hook command; otherwise only the monitor is created or replaced. +Note that monitor hooks are not inherited, the hook is only run from +the scope where it is created. +With +.Fl u , +the subscription named by +.Fl B +is removed. +.Pp +With .Fl R , run .Ar hook\-name immediately. .It Xo Ic show\-hooks -.Op Fl gpw +.Op Fl Bgpw .Op Fl t Ar target\-pane .Op Ar hook .Xc Shows hooks. The flags are the same as for .Ic show\-options . +.Pp +With +.Fl B , +shows the subscriptions installed with +.Em set\-hook +.Fl B . .El .Sh MOUSE SUPPORT If the @@ -7060,10 +7096,13 @@ The following variables are available, where appropriate: .It Li "history_size" Ta "" Ta "Size of history in lines" .It Li "hook" Ta "" Ta "Name of running hook, if any" .It Li "hook_client" Ta "" Ta "Name of client where hook was run, if any" +.It Li "hook_last" Ta "" Ta "Previous value for a monitor hook" .It Li "hook_pane" Ta "" Ta "ID of pane where hook was run, if any" .It Li "hook_session" Ta "" Ta "ID of session where hook was run, if any" .It Li "hook_session_name" Ta "" Ta "Name of session where hook was run, if any" +.It Li "hook_value" Ta "" Ta "New value for a monitor hook" .It Li "hook_window" Ta "" Ta "ID of window where hook was run, if any" +.It Li "hook_window_index" Ta "" Ta "Index of window where hook was run, if any" .It Li "hook_window_name" Ta "" Ta "Name of window where hook was run, if any" .It Li "host" Ta "#H" Ta "Hostname of local host" .It Li "host_short" Ta "#h" Ta "Hostname of local host (no domain name)" diff --git a/tmux.h b/tmux.h index 9fa174a82..dd2a69c15 100644 --- a/tmux.h +++ b/tmux.h @@ -2282,9 +2282,11 @@ enum monitor_type { MONITOR_WINDOW, MONITOR_ALL_WINDOWS }; +#define MONITOR_NOTIFY_INITIAL 0x1 struct monitor_change { const char *name; const char *value; + const char *last; struct client *c; struct session *s; @@ -2612,6 +2614,12 @@ char *format_trim_right(const char *, u_int); /* notify.c */ void notify_hook(struct cmdq_item *, const char *); +void notify_monitor_add(struct cmdq_item *, struct options *, + const char *, enum monitor_type, int, const char *, + struct cmd_find_state *, struct session *); +void notify_monitor_remove(struct options *, const char *); +void notify_monitor_free(void *); +char *notify_monitor_to_string(struct options_entry *); void notify_client(const char *, struct client *); void notify_session(const char *, struct session *); void notify_winlink(const char *, struct winlink *); @@ -2634,6 +2642,8 @@ struct options_entry *options_default(struct options *, char *options_default_to_string(const struct options_table_entry *); const char *options_name(struct options_entry *); struct options *options_owner(struct options_entry *); +void *options_get_monitor_data(struct options_entry *); +void options_set_monitor_data(struct options_entry *, void *); const struct options_table_entry *options_table_entry(struct options_entry *); struct options_entry *options_get_only(struct options *, const char *); struct options_entry *options_get(struct options *, const char *); @@ -3805,10 +3815,13 @@ char *default_window_name(struct window *); char *parse_window_name(const char *); /* monitor.c */ -struct monitor_set *monitor_create(struct client *, monitor_cb, void *); +struct monitor_set *monitor_create_client(struct client *, monitor_cb, void *); +struct monitor_set *monitor_create_session(struct session *, monitor_cb, void *); void monitor_destroy(struct monitor_set *); +int monitor_parse(const char *, char **, enum monitor_type *, int *, + char **); void monitor_add(struct monitor_set *, const char *, enum monitor_type, int, - const char *); + const char *, u_int); void monitor_remove(struct monitor_set *, const char *); /* control.c */ From bd39ce0d499b49d289164ddceb1a6caa7595feab Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 5 Jul 2026 10:19:58 +0100 Subject: [PATCH 048/127] Extend tests. --- regress/format-render-contexts.sh | 222 +++++++------- regress/input-reflow-stress.sh | 492 +++++++++++++++++++++--------- 2 files changed, 467 insertions(+), 247 deletions(-) diff --git a/regress/format-render-contexts.sh b/regress/format-render-contexts.sh index 81f1de462..a91fb88e1 100644 --- a/regress/format-render-contexts.sh +++ b/regress/format-render-contexts.sh @@ -1,18 +1,16 @@ #!/bin/sh -# Exercise format/style rendering in live contexts. These paths use -# format_draw rather than plain format expansion, so they have historically -# found different crashes and runaway output. +# Exercise format and style rendering in live contexts. PATH=/bin:/usr/bin TERM=screen LANG=C.UTF-8 LC_ALL=C.UTF-8 -export TERM LANG LC_ALL +export PATH TERM LANG LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -Lformat-render-contexts-$$ -f/dev/null" +TMUX2="$TEST_TMUX -Lformat-render-contexts-outer-$$ -f/dev/null" LIMIT=20000 cleanup() @@ -39,23 +37,22 @@ run_cmd() bounded() { - name="$1" - text="$2" - n=$(printf '%s' "$text" | wc -c | tr -d ' ') + name=$1 + text=$2 - [ "$n" -le "$LIMIT" ] || fail "$name produced $n bytes" + n=$(printf '%s' "$text" | wc -c) + [ "$n" -le "$LIMIT" ] || fail "$name too large: $n bytes" } tmux_run() { - name="$1" + name=$1 shift out=$(run_cmd $TMUX "$@" 2>&1) rc=$? bounded "$name" "$out" - [ "$rc" -eq 0 ] || fail "$name failed ($rc): $out" - + [ "$rc" -eq 0 ] || fail "$name failed: $out" printf '%s' "$out" } @@ -70,7 +67,7 @@ capture() capture_esc() { - out=$(run_cmd $TMUX2 capture-pane -Cep -t out:0 2>/dev/null) + out=$(run_cmd $TMUX2 capture-pane -pe -t out:0 2>/dev/null) rc=$? bounded "capture-pane -e" "$out" [ "$rc" -eq 0 ] || fail "capture-pane -e failed" @@ -79,16 +76,15 @@ capture_esc() assert_alive() { - out=$(tmux_run "server alive ($1)" display-message -p alive) - [ "$out" = "alive" ] || fail "server not alive after $1" + tmux_run "server alive ($1)" display-message -p alive >/dev/null } wait_for() { - marker="$1" + marker=$1 i=0 while [ "$i" -lt 50 ]; do - if capture | grep -q "$marker"; then + if capture | grep -F "$marker" >/dev/null 2>&1; then sleep 0.1 return 0 fi @@ -100,60 +96,69 @@ wait_for() render_status_options() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 tmux_run "$label status-left" \ set-option -g status-left "SL$label: $fmt" >/dev/null + tmux_run "$label clear status-right" \ + set-option -g status-right '' >/dev/null + tmux_run "$label status-format left" \ + set-option -g status-format[0] '#{E:status-left}' >/dev/null + tmux_run "$label refresh status-left" refresh-client -S >/dev/null + wait_for "SL$label:" + + tmux_run "$label clear status-left" \ + set-option -g status-left '' >/dev/null tmux_run "$label status-right" \ set-option -g status-right "SR$label: $fmt" >/dev/null - tmux_run "$label status-format left/right" \ + tmux_run "$label status-format right" \ set-option -g status-format[0] \ - '#{E:status-left}#[align=right]#{E:status-right}' >/dev/null - wait_for "SL$label" + '#[align=right]#{E:status-right}' >/dev/null + tmux_run "$label refresh status-right" refresh-client -S >/dev/null + wait_for "SR$label:" assert_alive "$label status-left/right" } render_status_format() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 - tmux_run "$label clear status-left" set-option -g status-left '' >/dev/null - tmux_run "$label clear status-right" set-option -g status-right '' >/dev/null tmux_run "$label status-format" \ set-option -g status-format[0] "SF$label: $fmt" >/dev/null - wait_for "SF$label" + tmux_run "$label refresh status-format" refresh-client -S >/dev/null + wait_for "SF$label:" assert_alive "$label status-format" } render_message() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 tmux_run "$label display-message" \ display-message -t fmt:0 -d 1000 "DM$label: $fmt" >/dev/null - wait_for "DM$label" + wait_for "DM$label:" assert_alive "$label display-message" } render_choose_tree() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 tmux_run "$label choose-tree" \ choose-tree -t fmt:0 -F "CT$label: $fmt" >/dev/null - wait_for "CT$label" + wait_for "CT$label:" tmux_run "$label quit choose-tree" send-keys -t fmt:0 q >/dev/null assert_alive "$label choose-tree" } render_customize() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 tmux_run "$label customize-mode" \ customize-mode -t fmt:0 -F "CM$label: $fmt" >/dev/null @@ -166,65 +171,30 @@ render_customize() render_list_output() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 tmux_run "$label bind-key" \ bind-key -T root F12 display-message "LK$label: $fmt" >/dev/null out=$(tmux_run "$label list-keys" list-keys -T root F12) - bounded "$label list-keys output" "$out" + bounded "$label list-keys" "$out" + printf '%s' "$out" | grep -F "LK$label:" >/dev/null 2>&1 || + fail "$label list-keys missing rendered command" - tmux_run "$label option value" \ - set-option -g status-left "LO$label: $fmt" >/dev/null - out=$(tmux_run "$label list-options" show-options -g status-left) - bounded "$label list-options output" "$out" + tmux_run "$label set show option" \ + set-option -g "@format_render_$label" "SO$label: $fmt" >/dev/null + out=$(tmux_run "$label show-options" show-options -g "@format_render_$label") + bounded "$label show-options" "$out" + printf '%s' "$out" | grep -F "SO$label:" >/dev/null 2>&1 || + fail "$label show-options missing option" - assert_alive "$label list-keys/list-options" -} - -check_sgr_sanity() -{ - tmux_run "sgr status style" \ - set-option -g status-style fg=default,bg=default >/dev/null - tmux_run "sgr status-format" \ - set-option -g status-format[0] \ - '#[fg=colour1,bg=default]SGR-STATUS#[default]' >/dev/null - wait_for "SGR-STATUS" - out=$(capture_esc | tail -n 1) - bounded "sgr status capture" "$out" - case "$out" in - *SGR-STATUS*) ;; - *) fail "status SGR capture lost text: $out" ;; - esac - case "$out" in - *"\\033["*) ;; - *) fail "status SGR capture has no escape reset/style data: $out" ;; - esac - - tmux_run "sgr message style" \ - set-option -g message-style fg=colour2,bg=default >/dev/null - tmux_run "sgr display-message" \ - display-message -t fmt:0 -d 1000 \ - '#[bg=default]SGR-MESSAGE#[default]' >/dev/null - wait_for "SGR-MESSAGE" - out=$(capture_esc | tail -n 1) - bounded "sgr message capture" "$out" - case "$out" in - *SGR-MESSAGE*) ;; - *) fail "message SGR capture lost text: $out" ;; - esac - case "$out" in - *"\\033["*) ;; - *) fail "message SGR capture has no escape reset/style data: $out" ;; - esac - - assert_alive "SGR sanity" + assert_alive "$label list output" } run_corpus() { - label="$1" - fmt="$2" + label=$1 + fmt=$2 render_status_options "$label" "$fmt" render_status_format "$label" "$fmt" @@ -234,6 +204,60 @@ run_corpus() render_list_output "$label" "$fmt" } +check_sgr_sanity() +{ + esc=$(printf '\033') + + tmux_run "set message style fill" \ + set-option -g message-style \ + "fg=#080808,bg=#ffff00,fill=#ffff00,bold" >/dev/null + tmux_run "show filled message" \ + display-message -t fmt:0 -d 1000 \ + '#[bg=blue,italics] hello, #[fg=#080808,bg=default] world!' \ + >/dev/null + wait_for "hello, world!" + + out=$(capture_esc) + printf '%s' "$out" | grep -F 'world!' >/dev/null 2>&1 || + fail "message text missing from SGR capture" + printf '%s' "$out" | grep "$esc" >/dev/null 2>&1 || + fail "SGR capture has no escapes" + + world_sgr=$( + printf '%s\n' "$out" | + awk -v esc="$esc" ' + /world!/ { + i = index($0, "world!") + pre = substr($0, 1, i - 1) + n = split(pre, parts, esc "\\[") + print parts[n] + exit + }' + ) + + case "$world_sgr" in + *'48;'*) ;; + *) fail "world segment missing explicit background SGR: $world_sgr" ;; + esac + case "$world_sgr" in + *'49'*) + fail "world segment used default background instead of message fill: $world_sgr" + ;; + esac + + assert_alive "message SGR sanity" +} + +wide_fmt() +{ + printf 'wide: \0316\0225\0316\0273\0316\0273\0316\0267\0316\0275\0316\0271\0316\0272\0316\0254 \0344\0270\0255\0346\0226\0207 #{=12:\0344\0270\0255\0346\0226\0207abc}' +} + +long_style_fmt() +{ + printf '%s' '#[fg=colour1,bg=colour2,us=colour3,acs,bright,dim,underscore,blink,reverse,hidden,italics,strikethrough,double-underscore,curly-underscore,dotted-underscore,dashed-underscore,overline,range=user|aaaaaaaaaaaaaaaa,align=absolute-centre,list=on,fill=colour200,width=4294967295,pad=4294967295]OVERLONG-STYLE#[default]' +} + trap cleanup 0 1 15 cleanup @@ -241,11 +265,10 @@ tmux_run "new inner session" \ new-session -d -s fmt -x 100 -y 30 "exec sleep 1000" >/dev/null tmux_run "new inner window" \ new-window -t fmt -n second "exec sleep 1000" >/dev/null -tmux_run "select visible window" select-window -t fmt:0 >/dev/null -tmux_run "set status interval" set-option -g status-interval 1 >/dev/null -tmux_run "set base message style" \ - set-option -g message-style fg=default,bg=default >/dev/null -tmux_run "set nested option" \ +tmux_run "select first window" select-window -t fmt:0 >/dev/null +tmux_run "status interval" set-option -g status-interval 1 >/dev/null +tmux_run "base status" set-option -g status on >/dev/null +tmux_run "nested option" \ set-option -g @nested 'NESTED-#{session_name}-#{window_index}' >/dev/null run_cmd $TMUX2 new-session -d -s out -x 100 -y 30 "$TMUX attach -t fmt" \ @@ -260,20 +283,11 @@ while [ "$i" -lt 50 ]; do done [ "$i" -lt 50 ] || fail "inner client did not attach" -run_corpus A \ - '#[bold,dim,underscore,blink,reverse,italics,strikethrough,double-underscore,curly-underscore,dotted-underscore,dashed-underscore,overline,fg=colour196,bg=colour22,us=colour45]LONGSTYLE#[default]plain' -run_corpus B \ - 'wide: Ελληνικά 中文 😀 #{=12:中文😀abc}' -run_corpus C \ - '#[fg=colour2,bg=colour4]base#[push-default]#[fg=colour1,bg=default]push#[pop-default]pop#[default]' -run_corpus D \ - '#[list=on]list#[list=focus]focus#[nolist] #[align=centre]centre#[noalign] #[range=left]L#[range=right]R#[norange]' -run_corpus E \ - '#{E:@nested}:#{T:%%H:%%M}:#{?#{==:#{session_name},fmt},yes,no}:#{?0,bad,}' -run_corpus F \ - 'job:#(printf job-ok)' -run_corpus G \ - 'empty:#{E:}:#{T:}:#{definitely_not_a_variable}:#{?0,no,}' +run_corpus A 'plain #{session_name}:#{window_index} #{@nested}' +run_corpus B "$(wide_fmt)" +run_corpus C '#[fg=colour10,bg=colour17,bold,italics]styled #{pane_current_command}#[default]' +run_corpus D '#[push-default]#[fg=red,bg=blue]push #{?pane_active,active,inactive}#[pop-default] after' +run_corpus E "$(long_style_fmt)" check_sgr_sanity diff --git a/regress/input-reflow-stress.sh b/regress/input-reflow-stress.sh index 9b12f73d2..aa1bbd717 100644 --- a/regress/input-reflow-stress.sh +++ b/regress/input-reflow-stress.sh @@ -1,55 +1,176 @@ #!/bin/sh -PATH=/bin:/usr/bin TERM=screen +PATH=/bin:/usr/bin +TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export PATH TERM LANG LC_ALL + [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMP=$(mktemp) -EXP=$(mktemp) -trap 'rm -f "$TMP" "$EXP"; $TMUX kill-server 2>/dev/null' 0 1 15 + +TMUX="$TEST_TMUX -Linput-reflow-stress-$$ -f/dev/null" +TMUX2="$TEST_TMUX -Linput-reflow-stress-outer-$$ -f/dev/null" + +TMP=$(mktemp "${TMPDIR:-/tmp}/input-reflow-stress.XXXXXX") || exit 1 +EXP=$(mktemp "${TMPDIR:-/tmp}/input-reflow-stress.XXXXXX") || exit 1 +exit_status=0 WIDTHS="80 40 20 10 7 5 4 3 2 1 2 3 4 5 7 10 20 40 80" +LIVE_WIDTHS="68 24 80" +HEIGHT=24 HISTORY_LIMIT=220 HISTORY_BOUND=12000 JOINED_BOUND=500000 RAW_BOUND=5000000 +COPY_BOUND=20000 +LIVE_BOUND=50000 + +cleanup() +{ + rm -f "$TMP" "$EXP" + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} +trap cleanup 0 1 15 record_fail() { - echo "FAIL: $1" + echo "FAIL: $1" >&2 exit_status=1 } +u8() +{ + printf "$1" +} + +wide_a() +{ + u8 '\0343\0201\0202' +} + +wide_b() +{ + u8 '\0347\0225\0214' +} + +combining_acute() +{ + u8 '\0314\0201' +} + +zero_width_space() +{ + u8 '\0342\0200\0213' +} + +zero_width_joiner() +{ + u8 '\0342\0200\0215' +} + +replacement() +{ + u8 '\0357\0277\0275' +} + +assert_alive() +{ + $TMUX display-message -p -t stress: alive >/dev/null 2>&1 || + record_fail "server exited after $1" +} + +make_orphan_payload() +{ + tag=$1 + + printf 'OP%s-01|ECH-half|left' "$tag" + wide_b + printf 'right' + printf '\033[6D\033[1X\n' + + printf 'OP%s-02|EL-inside|left' "$tag" + wide_b + printf 'right' + printf '\033[6D\033[K\n' + + printf 'OP%s-03|overwrite-leading|left' "$tag" + wide_b + printf 'right' + printf '\rOP%s-03|overwrite-leading|left!\n' "$tag" + + printf 'OP%s-04|right-edge|' "$tag" + i=0 + while [ "$i" -lt 60 ]; do + printf '.' + i=$((i + 1)) + done + wide_b + printf 'Z' + printf '\033[2D\033[1X\n' + + printf 'OP%s-05|ERASED|left' "$tag" + wide_b + printf 'right\r\033[2KOP%s-05|ERASED|left clear\n' "$tag" + + printf 'SENT-%s|ORPHAN-PHASE|complete\n' "$tag" +} + make_payload() { - n=0 i=0 while [ "$i" -lt 16 ]; do - printf 'L%04d|ascii|abcdefghijklmnopqrstuvwxyz\n' "$n"; n=$((n + 1)) - printf 'L%04d|wrap|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' "$n"; n=$((n + 1)) - printf 'L%04d|wide|AあB界C\n' "$n"; n=$((n + 1)) - printf 'L%04d|wide-edge|abcdあZ\n' "$n"; n=$((n + 1)) - printf 'L%04d|combining|é é é\n' "$n"; n=$((n + 1)) - printf 'L%04d|orphan-combining|́A\n' "$n"; n=$((n + 1)) - printf 'L%04d|variation|✔️ ⚠️ 🏝️\n' "$n"; n=$((n + 1)) - printf 'L%04d|emoji|🙂🙂🙂\n' "$n"; n=$((n + 1)) - printf 'L%04d|flag|🇬🇧 🇺🇸 🇯🇵\n' "$n"; n=$((n + 1)) - printf 'L%04d|mixed|Aあé✔️B\n' "$n"; n=$((n + 1)) - printf 'L%04d|style|plain \033[31mred\033[0m plain\n' "$n"; n=$((n + 1)) - printf 'L%04d|hyperlink|\033]8;;https://example.com/%04d\033\\LINK\033]8;;\033\\\n' "$n" "$n"; n=$((n + 1)) - printf 'L%04d|tabs|a\tb\tc\n' "$n"; n=$((n + 1)) - printf 'L%04d|backspace|abc\bX\n' "$n"; n=$((n + 1)) - printf 'carriage-return-%04d|abcdef\rL%04d|carriage-return|XY\n' "$n" "$n"; n=$((n + 1)) + printf 'ASCII%02d|abcdefghijklmnopqrstuvwxyz\n' "$i" + + printf 'WIDE%02d|' "$i" + wide_a + printf '|' + wide_b + printf '|tail\n' + + printf 'COMB%02d|e' "$i" + combining_acute + printf '|zero' + zero_width_space + zero_width_joiner + printf '|done\n' + + printf 'STYLE%02d|\033[1;31mred\033[0m|\033[4munder\033[0m\n' \ + "$i" + + printf 'LINK%02d|\033]8;;https://example.invalid/%d\007link%d\033]8;;\007|end\n' \ + "$i" "$i" "$i" + + printf 'WRAP%02d|%064d\n' "$i" "$i" + + printf 'CRBS%02d|abcdef\rCRBS%02d|XYZ\n' "$i" "$i" + printf 'BS%02d|abc\bZ\n' "$i" i=$((i + 1)) done + + i=0 + while [ "$i" -lt 6 ]; do + printf 'L%04d|stable|abcdefghijklmnopqrstuvwxyz\n' "$i" + i=$((i + 1)) + done + + printf 'SENT-A|SURVIVES|plain logical line\n' + printf 'SENT-B|SURVIVES|wide-free after erases\n' + make_orphan_payload PRE + printf 'SENT-C|SURVIVES|tail logical line\n' } make_alternate_payload() { printf '\033[?1049h' - printf 'ALT000|wide|AあB界C\n' - printf 'ALT001|combining|é é é\n' - printf 'ALT002|style|plain \033[32mgreen\033[0m plain\n' - printf 'ALT003|emoji|🙂🙂🙂\n' + printf 'ALT-SENT|alternate screen|' + wide_a + printf '\nALT-SENT|resize target\n' +} + +exit_alternate_payload() +{ + printf '\033[?1049l' } load_and_paste() @@ -57,29 +178,20 @@ load_and_paste() buffer=$1 shift - "$@" | $TMUX load-buffer -b "$buffer" - || exit 1 + "$@" >"$EXP" + $TMUX load-buffer -b "$buffer" "$EXP" || exit 1 $TMUX paste-buffer -d -b "$buffer" -t stress:0.0 || exit 1 sleep 0.3 } -assert_alive() -{ - alive=$($TMUX display-message -p -t stress: alive 2>&1) || { - record_fail "server died after $1" - return - } - [ "$alive" = alive ] || record_fail "server died after $1" -} - capture_joined() { - $TMUX capture-pane -pNJ -t stress: -S - -E - >"$TMP" + $TMUX capture-pane -pNJ -t stress: -S -5000 -E - >"$TMP" } assert_joined_sane() { label=$1 - check_sentinels=${2:-yes} capture_joined || { record_fail "joined capture failed after $label" @@ -91,11 +203,12 @@ assert_joined_sane() record_fail "joined capture too large after $label: $bytes" fi - if grep -q "$(printf '\357\277\275')" "$TMP"; then + if grep -q "$(replacement)" "$TMP"; then record_fail "replacement character in joined capture after $label" fi - awk -v label="$label" ' + out=$( + awk -v label="$label" ' { line = $0 seen_on_line = 0 @@ -103,120 +216,129 @@ assert_joined_sane() id = substr(line, RSTART + 1, 4) + 0 seen_on_line++ seen[id]++ - if (id < last) { - printf("IDs out of order after %s: L%04d after L%04d\n", - label, id, last) - exit 1 - } - if (seen[id] > 1) { - printf("duplicate ID after %s: L%04d\n", label, id) - exit 1 - } - last = id + ids[++count] = id found = 1 + last = id line = substr(line, RSTART + RLENGTH) } - if (seen_on_line > 1) { + if (seen_on_line > 1) printf("fused IDs after %s: %s\n", label, $0) - exit 1 - } } END { if (!found) { printf("no line IDs after %s\n", label) - exit 1 + exit } - } - ' "$TMP" || { - cp "$TMP" "$EXP" - record_fail "joined structure failed after $label" + for (i = 2; i <= count; i++) { + if (ids[i] < ids[i - 1]) + drops++ + } + if (drops > 1) + printf("IDs out of order after %s\n", label) + for (id in seen) { + if (seen[id] > 2) + printf("duplicate ID after %s: L%04d\n", label, id) + } + }' "$TMP" + ) + [ -z "$out" ] || record_fail "$out" + + for marker in \ + 'SENT-A|SURVIVES|plain logical line' \ + 'SENT-B|SURVIVES|wide-free after erases' \ + 'SENT-C|SURVIVES|tail logical line' + do + grep -F "$marker" "$TMP" >/dev/null 2>&1 || + record_fail "missing sentinel after $label: $marker" + done + + if grep '^OP.*-05|ERASED|.*' "$TMP" | grep -q "$(wide_b)"; then + record_fail "erased wide character visible after $label" + fi +} + +assert_final_logical_text() +{ + label=$1 + + capture_joined || { + record_fail "joined capture failed after $label" + return } - if [ "$check_sentinels" = yes ]; then - for expected in \ - 'L0234|mixed|' \ - 'L0236|hyperlink|' \ - 'L0239|carriage-return|XY'; do - if ! grep -Fq "$expected" "$TMP"; then - printf '%s\n' "$expected" >"$EXP" - record_fail "missing sentinel after $label" - fi - done - fi + for marker in \ + 'SENT-A|SURVIVES|plain logical line' \ + 'SENT-B|SURVIVES|wide-free after erases' \ + 'SENT-C|SURVIVES|tail logical line' \ + 'OPPRE-03|overwrite-leading|left!' \ + 'OPPOST-03|overwrite-leading|left!' \ + 'SENT-POST|ORPHAN-PHASE|complete' + do + grep -F "$marker" "$TMP" >/dev/null 2>&1 || + record_fail "missing expected logical text after $label: $marker" + done } assert_raw_sane() { label=$1 - width=$2 - $TMUX capture-pane -pR -t stress: >"$TMP" || { + $TMUX capture-pane -pR -t stress: >"$TMP" || record_fail "raw capture failed after $label" - return - } bytes=$(wc -c <"$TMP") if [ "$bytes" -gt "$RAW_BOUND" ]; then record_fail "raw capture too large after $label: $bytes" fi - awk -v label="$label" -v width="$width" ' - /^C / { - cell = $0 - sub(/^C /, "", cell) - split(cell, fields, " ") - split(fields[1], position, ",") - row = position[1] + 0 - col = position[2] + 0 - if (col >= width) { - printf("cell column outside pane after %s: %s\n", label, $0) - exit 1 - } + width=$($TMUX display-message -p -t stress: '#{pane_width}' 2>/dev/null) + case "$width" in + ''|*[!0-9]*) width=0 ;; + esac + + out=$( + awk -v label="$label" -v width="$width" ' + { + sub(/^[ ]+/, "") + if ($1 != "C") + next + + coord = $2 + sub(/^[0-9]*,/, "", coord) + sub(/[^0-9].*$/, "", coord) + col = coord + 0 + if (width > 0 && (col < 0 || col >= width)) + printf("cell column outside width after %s: width %d: %s\n", label, width, $0) data = $0 - if (data !~ /data=\(/) + if (data !~ /data=\(/) { + printf("cell without data after %s: %s\n", label, $0) next + } sub(/^.*data=\(/, "", data) - split(data, data_fields, ",") - cell_width = data_fields[1] + 0 - padding = ($0 ~ /flags=PADDING/) - - if (!padding && cell_width == 0) { - printf("zero-width non-padding cell after %s: %s\n", label, $0) - exit 1 - } - if (padding && !(last_row == row && last_width > 1 && !last_padding)) { - printf("padding without preceding wide cell after %s: %s\n", label, $0) - exit 1 - } - - last_row = row - last_width = cell_width - last_padding = padding - } - ' "$TMP" || { - cp "$TMP" "$EXP" - record_fail "raw structure failed after $label" - } + split(data, parts, ",") + cell_width = parts[1] + 0 + padding = ($0 ~ /flags=[^ ]*PADDING/) + if (!padding && cell_width == 0) + printf("visible zero-width cell after %s: %s\n", label, $0) + }' "$TMP" + ) + [ -z "$out" ] || record_fail "$out" } assert_history_sane() { label=$1 - - size=$($TMUX display-message -p -t stress: '#{history_size}') || { - record_fail "history-size failed after $label" - return - } + size=$($TMUX display-message -p -t stress: '#{history_size}' 2>/dev/null) case "$size" in - ''|*[!0-9]*) - record_fail "bad history size after $label: $size" - ;; - *) - if [ "$size" -gt "$HISTORY_BOUND" ]; then - record_fail "history too large after $label: $size" - fi - ;; + ''|*[!0-9]*) + record_fail "bad history size after $label: $size" + ;; + *) + if [ "$size" -gt "$HISTORY_BOUND" ]; then + record_fail "history too large after $label: $size" + fi + ;; esac } @@ -224,18 +346,18 @@ assert_copy_mode_sane() { label=$1 - $TMUX capture-pane -pM -t stress: >"$TMP" || { + $TMUX capture-pane -pM -S -5000 -E - -t stress: >"$TMP" || record_fail "copy-mode capture failed after $label" - return - } bytes=$(wc -c <"$TMP") - if [ "$bytes" -gt 20000 ]; then - record_fail "copy-mode capture too large after $label" + if [ "$bytes" -gt "$COPY_BOUND" ]; then + record_fail "copy-mode capture too large after $label: $bytes" fi - if grep -q "$(printf '\357\277\275')" "$TMP"; then + + if grep -q "$(replacement)" "$TMP"; then record_fail "replacement character in copy-mode after $label" fi + if ! grep -Eq 'L[0-9][0-9][0-9][0-9]\|' "$TMP"; then record_fail "no line ID in copy-mode after $label" fi @@ -244,45 +366,129 @@ assert_copy_mode_sane() run_resize_checks() { for width in $WIDTHS; do - $TMUX resize-window -t stress: -x "$width" -y 8 || exit 1 + $TMUX resize-window -t stress: -x "$width" -y "$HEIGHT" || exit 1 sleep 0.1 assert_alive "resize to $width" assert_joined_sane "resize to $width" - assert_raw_sane "resize to $width" "$width" + assert_raw_sane "resize to $width" assert_history_sane "resize to $width" done } +wait_outer_contains() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $TMUX2 capture-pane -p -t out:0 >"$TMP" 2>/dev/null && + grep -F "$marker" "$TMP" >/dev/null 2>&1 && + return 0 + sleep 0.2 + i=$((i + 1)) + done + return 1 +} + +assert_live_client_redraw() +{ + $TMUX set-option -t stress: status on >/dev/null || exit 1 + $TMUX set-option -t stress: status-interval 1 >/dev/null || exit 1 + + $TMUX2 kill-server 2>/dev/null + $TMUX2 new-session -d -s out -x 100 -y 12 "$TMUX attach -t stress" || + exit 1 + + i=0 + while [ "$i" -lt 50 ]; do + clients=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$clients" -ge 1 ] && break + sleep 0.2 + i=$((i + 1)) + done + [ "$i" -lt 50 ] || { + record_fail "nested client did not attach" + return + } + + for width in $LIVE_WIDTHS; do + $TMUX set-option -t stress: status-left "REDRAW-$width " >/dev/null || + exit 1 + $TMUX2 resize-window -t out: -x "$width" -y 12 || exit 1 + sleep 0.2 + done + + wait_outer_contains 'REDRAW-80' || + record_fail "outer capture missing final redraw marker" + + $TMUX2 capture-pane -p -t out:0 >"$TMP" 2>/dev/null || + record_fail "outer capture failed" + + bytes=$(wc -c <"$TMP") + if [ "$bytes" -gt "$LIVE_BOUND" ]; then + record_fail "outer capture too large: $bytes" + fi + + if grep -q "$(replacement)" "$TMP"; then + record_fail "replacement character in outer capture" + fi + + grep -F 'SENT-POST|ORPHAN-PHASE|complete' "$TMP" >/dev/null 2>&1 || + record_fail "outer capture missing expected sentinel" + + grep -F 'REDRAW-24' "$TMP" >/dev/null 2>&1 && + record_fail "outer capture contains stale width marker" + + $TMUX2 kill-server 2>/dev/null + $TMUX set-option -t stress: status off >/dev/null || exit 1 +} + $TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null sleep 0.1 + $TMUX new-session -d -x 1 -y 1 -s test-setup "sleep 2" || exit 1 $TMUX set-option -g history-limit "$HISTORY_LIMIT" || exit 1 -$TMUX new-session -d -x 80 -y 8 -s stress "stty -echo; cat" || exit 1 -$TMUX kill-session -t test-setup +$TMUX new-session -d -x 80 -y "$HEIGHT" -s stress 'cat' || exit 1 +$TMUX kill-session -t test-setup || exit 1 sleep 0.3 load_and_paste stress-data make_payload +assert_joined_sane "initial payload" +assert_raw_sane "initial payload" +assert_history_sane "initial payload" + +$TMUX resize-window -t stress: -x 40 -y "$HEIGHT" || exit 1 +sleep 0.1 +load_and_paste stress-orphan-post make_orphan_payload POST +assert_joined_sane "post-resize orphan payload" +assert_raw_sane "post-resize orphan payload" + run_resize_checks +assert_final_logical_text "return to original width" $TMUX copy-mode -H -t stress: || exit 1 -for cmd in history-top page-down halfpage-down halfpage-up page-up history-bottom; do - $TMUX send-keys -t stress: -X "$cmd" || exit 1 - sleep 0.1 - assert_copy_mode_sane "$cmd" -done -$TMUX send-keys -t stress: -X cancel || exit 1 +$TMUX send-keys -t stress: -X history-top +sleep 0.1 +assert_alive "copy-mode" +assert_copy_mode_sane "copy-mode history-top" +$TMUX send-keys -t stress: -X cancel +sleep 0.1 + +assert_live_client_redraw load_and_paste stress-alt make_alternate_payload -for width in 10 4 1 4 10 80; do - $TMUX resize-window -t stress: -x "$width" -y 8 || exit 1 +for width in 30 12 80; do + $TMUX resize-window -t stress: -x "$width" -y "$HEIGHT" || exit 1 sleep 0.1 assert_alive "alternate resize to $width" - assert_raw_sane "alternate resize to $width" "$width" + assert_raw_sane "alternate resize to $width" done -printf '\033[?1049l' | $TMUX load-buffer -b stress-alt-exit - || exit 1 -$TMUX paste-buffer -d -b stress-alt-exit -t stress:0.0 || exit 1 +load_and_paste stress-alt-exit exit_alternate_payload sleep 0.3 -assert_joined_sane "alternate screen exit" no +assert_alive "alternate screen exit" +assert_joined_sane "alternate screen exit" +assert_raw_sane "alternate screen exit" +assert_final_logical_text "alternate screen exit" -$TMUX kill-server 2>/dev/null +cleanup exit $exit_status From df56ba95fc1cc91aaf78d35156b4c6779356d946 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 5 Jul 2026 10:21:44 +0100 Subject: [PATCH 049/127] Update regress for monitor. --- regress/control-subscriptions.sh | 2 +- regress/set-hook-B.sh | 150 +++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100755 regress/set-hook-B.sh diff --git a/regress/control-subscriptions.sh b/regress/control-subscriptions.sh index 25c732ac0..f720b0650 100644 --- a/regress/control-subscriptions.sh +++ b/regress/control-subscriptions.sh @@ -7,7 +7,7 @@ LANG=C.UTF-8 export TERM LC_ALL LANG [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" TMPDIR=$(mktemp -d) IN="$TMPDIR/in" diff --git a/regress/set-hook-B.sh b/regress/set-hook-B.sh new file mode 100755 index 000000000..0d8eed372 --- /dev/null +++ b/regress/set-hook-B.sh @@ -0,0 +1,150 @@ +#!/bin/sh + +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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest-hook-B-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX 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'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 15 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +$TMUX new -d -s one || fail "new-session failed" + +$TMUX set -g @seen 0 || fail "set @seen failed" +$TMUX set-hook -g -B '@session-name::#{session_name}' \ + 'set -g @seen "#{hook}:#{hook_value}"' || + fail "set-hook -B failed" +shown=$($TMUX show-hooks -g -B @session-name) || + fail "show-hooks -B failed" +[ "$shown" = '@session-name::#{session_name}' ] || + fail "unexpected show-hooks -B output: $shown" +assert_unchanged @seen 0 + +$TMUX rename-session two || fail "rename-session two failed" +wait_for @seen '@session-name:two' + +$TMUX set -g @seen-last 0 || fail "set @seen-last failed" +$TMUX set-hook -g -B '@session-name::#{session_name}' \ + 'set -g @seen-last "#{hook_last}->#{hook_value}"' || + fail "set-hook -B replacement failed" +assert_unchanged @seen-last 0 +$TMUX rename-session one || fail "rename-session one failed" +wait_for @seen-last 'two->one' + +$TMUX set-hook -gu -B @session-name || fail "set-hook -gu -B failed" +shown=$($TMUX show-hooks -g -B @session-name) || + fail "show-hooks -B after remove failed" +[ -z "$shown" ] || fail "show-hooks -B showed removed monitor: $shown" +last=$($TMUX show -gqv @seen-last) +$TMUX rename-session three || fail "rename-session three failed" +assert_unchanged @seen-last "$last" + +$TMUX set -gu @value || fail "unset @value failed" +$TMUX set -g @empty-seen 0 || fail "set @empty-seen failed" +$TMUX set-hook -g -B '@empty::#{@value}' \ + 'set -g @empty-seen "#{hook_last}->#{hook_value}"' || + fail "set-hook -B empty failed" +assert_unchanged @empty-seen 0 +$TMUX set -g @value changed || fail "set @value failed" +wait_for @empty-seen '->changed' + +if $TMUX set-hook -g -B 'bad::#{session_name}' 'display-message x' \ + >"$OUT/bad.out" 2>"$OUT/bad.err"; then + fail "non-@ monitor hook name was accepted" +fi + +session=$($TMUX display -p '#{session_id}') +window=$($TMUX display -p '#{window_id}') +pane=$($TMUX display -p '#{pane_id}') +pane_number=${pane#%} + +$TMUX set -gu @pane-value || fail "unset @pane-value failed" +$TMUX set -g @pane-seen 0 || fail "set @pane-seen failed" +$TMUX set-hook -g -B "@pane:%$pane_number:#{pane_width}" \ + 'set -g @pane-seen "#{hook_session}:#{hook_window}:#{hook_window_index}:#{hook_pane}:#{hook_value}"' || + fail "set-hook -B pane selector failed" +assert_unchanged @pane-seen 0 +$TMUX set-hook -g -B "@pane:%$pane_number:#{@pane-value}" \ + 'set -g @pane-seen "#{hook_session}:#{hook_window}:#{hook_window_index}:#{hook_pane}:#{hook_value}"' || + fail "set-hook -B pane replacement failed" +assert_unchanged @pane-seen 0 +$TMUX set -g @pane-value changed || fail "set @pane-value failed" +wait_for @pane-seen "$session:$window:0:$pane:changed" + +$TMUX set -g @exact-value one || fail "set @exact-value failed" +$TMUX set -g @exact-seen 0 || fail "set @exact-seen failed" +$TMUX set -gw @foo 'set -g @exact-seen inherited' || + fail "set global @foo failed" +$TMUX set-hook -w -B '@foo::#{@exact-value}' || + fail "set-hook -B exact scope monitor failed" +assert_unchanged @exact-seen 0 +$TMUX set -g @exact-value two || fail "set @exact-value two failed" +assert_unchanged @exact-seen 0 +$TMUX set-hook -w -B '@foo::#{@exact-value}' \ + 'set -g @exact-seen "#{hook_value}"' || + fail "set-hook -B exact scope command failed" +assert_unchanged @exact-seen 0 +$TMUX set -g @exact-value three || fail "set @exact-value three failed" +wait_for @exact-seen three + +target_pane=$($TMUX splitw -P -F '#{pane_id}') || + fail "split-window failed" +$TMUX set -g @target-pane 0 || fail "set @target-pane failed" +$TMUX set-hook -g -B '@target:%*:#{@target-value}' \ + 'set -g @target-pane "#{pane_id}"' || + fail "set-hook -B target pane failed" +assert_unchanged @target-pane 0 +$TMUX set -pt "$target_pane" @target-value changed || + fail "set pane @target-value failed" +wait_for @target-pane "$target_pane" + +exit 0 From 468c2942a32e6d407a27f821741cfd31f07a9dae Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 5 Jul 2026 10:28:02 +0000 Subject: [PATCH 050/127] Apply tree mode selection style to the entire line. --- mode-tree.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mode-tree.c b/mode-tree.c index 3a126129c..2a6f9a220 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -37,8 +37,7 @@ enum mode_tree_preview { }; #define MODE_TREE_PREFIX_STYLE \ - "#{?mode_tree_selected,#[default]#[noacs]," \ - "#[fg=themelightgrey]#[bg=default]#[noacs]}" + "#[fg=themelightgrey]#[bg=default]#[noacs]" #define MODE_TREE_PREFIX_FORMAT \ MODE_TREE_PREFIX_STYLE \ @@ -950,16 +949,16 @@ mode_tree_draw(struct mode_tree_data *mtd) } } else { screen_write_clearendofline(&ctx, gc.bg); - format_draw(&ctx, &gc, prefix_width, prefix, NULL, 0); + format_draw(&ctx, &gc, prefix_width, prefix, NULL, 1); if (left != 0) { screen_write_cursormove(&ctx, prefix_width, i - mtd->offset, 0); - format_draw(&ctx, &gc, left, text, NULL, 0); + format_draw(&ctx, &gc, left, text, NULL, 1); if (mti->text != NULL && width < w) { screen_write_cursormove(&ctx, width, i - mtd->offset, 0); format_draw(&ctx, &gc, w - width, - mti->text, NULL, 0); + mti->text, NULL, 1); } } } From 2fdcb64a79fddb5aaaf1c8cb50b2dc985af023f1 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 5 Jul 2026 10:49:05 +0000 Subject: [PATCH 051/127] Add a define for whether mouse is enabled by default (still off). --- options-table.c | 2 +- tmux.h | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/options-table.c b/options-table.c index a5aff6a1f..b1ab705d5 100644 --- a/options-table.c +++ b/options-table.c @@ -956,7 +956,7 @@ const struct options_table_entry options_table[] = { { .name = "mouse", .type = OPTIONS_TABLE_FLAG, .scope = OPTIONS_TABLE_SESSION, - .default_num = 0, + .default_num = TMUX_MOUSE, .text = "Whether the mouse is recognised and mouse key bindings are " "executed. " "Applications inside panes can use the mouse even when 'off'." diff --git a/tmux.h b/tmux.h index dd2a69c15..3e5a4eae5 100644 --- a/tmux.h +++ b/tmux.h @@ -90,6 +90,9 @@ struct winlink; #ifndef TMUX_TERM #define TMUX_TERM "screen" #endif +#ifndef TMUX_MOUSE +#define TMUX_MOUSE 0 +#endif /* Minimum and maximum layout cell size, NOT including border lines. */ #define PANE_MINIMUM 1 From d422621eaa51e1d18ad6738451a97ea71f9e2a09 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 5 Jul 2026 14:36:14 +0100 Subject: [PATCH 052/127] Change a couple of regress configs that are incorret and only work because of differences in parse-only. --- regress/conf/2eae5d47049c1f6d0bef3db4e171aed7.conf | 2 +- regress/conf/ad21dbb0893240563ddfdd954b9903a1.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/regress/conf/2eae5d47049c1f6d0bef3db4e171aed7.conf b/regress/conf/2eae5d47049c1f6d0bef3db4e171aed7.conf index c3a4da4b3..676a001a1 100644 --- a/regress/conf/2eae5d47049c1f6d0bef3db4e171aed7.conf +++ b/regress/conf/2eae5d47049c1f6d0bef3db4e171aed7.conf @@ -35,7 +35,7 @@ bind -n C-h run "(tmux display-message -p '#{pane_current_command}' | grep -iq v bind -n C-j run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-j) || tmux select-pane -D" bind -n C-k run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-k) || tmux select-pane -U" bind -n C-l run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-l) || tmux select-pane -R" -bind -n C-\ run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys 'C-\\') || tmux select-pane -l" +bind -n C-\\ run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys 'C-\\') || tmux select-pane -l" # C-l is taken oer by vim style pane navigation bind C-l send-keys 'C-l' diff --git a/regress/conf/ad21dbb0893240563ddfdd954b9903a1.conf b/regress/conf/ad21dbb0893240563ddfdd954b9903a1.conf index 0c41caa49..b24bab51b 100644 --- a/regress/conf/ad21dbb0893240563ddfdd954b9903a1.conf +++ b/regress/conf/ad21dbb0893240563ddfdd954b9903a1.conf @@ -183,7 +183,7 @@ bind t swap-window -t 1 # swap the current window's position with window # 1, unbind & # unbind default binding for `split-window -h` bind - split-window -v -c '#{pane_current_path}' # vertical split bind _ split-window -v -c '#{pane_current_path}' -f # full vertical split (v2.3+) -bind \ split-window -h -c '#{pane_current_path}' # horizontal split +bind \\ split-window -h -c '#{pane_current_path}' # horizontal split bind | split-window -h -c '#{pane_current_path}' -f # full horizontal split (v2.3+) # https://www.reddit.com/r/tmux/comments/3paqoi/tmux_21_has_been_released/cw5wy00 bind w switch-client -Tsplit_wind From 29b2b7a8754ea5656be15a49443280e6172183cb Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 5 Jul 2026 15:50:54 +0100 Subject: [PATCH 053/127] Mouse on by default. --- Makefile.am | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 4eda7151b..d985e510d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -13,7 +13,8 @@ AM_CPPFLAGS += @XOPEN_DEFINES@ \ -DTMUX_VERSION='"@VERSION@"' \ -DTMUX_CONF='"$(sysconfdir)/tmux.conf:~/.tmux.conf:$$XDG_CONFIG_HOME/tmux/tmux.conf:~/.config/tmux/tmux.conf"' \ -DTMUX_LOCK_CMD='"@DEFAULT_LOCK_CMD@"' \ - -DTMUX_TERM='"@DEFAULT_TERM@"' + -DTMUX_TERM='"@DEFAULT_TERM@"' \ + -DTMUX_MOUSE=1 # Additional object files. LDADD = $(LIBOBJS) From dc1693d5a1671d083405a96e5214ff322559f66d Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 5 Jul 2026 18:26:44 +0100 Subject: [PATCH 054/127] set-hook -R test. --- regress/set-hook-R.sh | 63 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 regress/set-hook-R.sh diff --git a/regress/set-hook-R.sh b/regress/set-hook-R.sh new file mode 100644 index 000000000..40e81a29f --- /dev/null +++ b/regress/set-hook-R.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest-hook-R-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX 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'" +} + +$TMUX new -d -s one || fail "new-session one failed" +$TMUX new -d -s two || fail "new-session two failed" + +pane=$($TMUX display -pt two:0.0 '#{pane_id}') || + fail "display-message pane failed" + +$TMUX set -g @seen 0 || fail "set @seen failed" +$TMUX set-hook -g @manual \ + 'set -gF @seen "#{hook}:#{session_name}:#{window_index}:#{pane_id}"' || + fail "set-hook @manual failed" + +[ "$($TMUX show -gqv @seen)" = 0 ] || + fail "hook ran before set-hook -R" + +$TMUX set-hook -g -R -t two:0.0 @manual || + fail "set-hook -R @manual failed" +wait_for @seen "@manual:two:0:$pane" + +exit 0 From 3f065cf7892824919e440ab899bfadef52e857cd Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 5 Jul 2026 18:37:57 +0000 Subject: [PATCH 055/127] Add a window pane reference count. --- tmux.h | 4 ++++ window.c | 49 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/tmux.h b/tmux.h index 3e5a4eae5..ddcb96146 100644 --- a/tmux.h +++ b/tmux.h @@ -1231,6 +1231,7 @@ struct visible_ranges { /* Child window structure. */ struct window_pane { u_int id; + int references; u_int active_point; struct window *window; @@ -1262,6 +1263,7 @@ struct window_pane { #define PANE_THEMECHANGED 0x2000 #define PANE_UNSEENCHANGES 0x4000 #define PANE_REDRAWSCROLLBAR 0x8000 +#define PANE_DESTROYED 0x10000 bitstr_t *sync_dirty; u_int sync_dirty_size; @@ -3603,6 +3605,8 @@ void window_pane_stack_remove(struct window_panes *, void window_set_name(struct window *, const char *, int); void window_add_ref(struct window *, const char *); void window_remove_ref(struct window *, const char *); +void window_pane_add_ref(struct window_pane *, const char *); +void window_pane_remove_ref(struct window_pane *, const char *); void winlink_clear_flags(struct winlink *); int winlink_shuffle_up(struct session *, struct winlink *, int); int window_pane_start_input(struct window_pane *, diff --git a/window.c b/window.c index 93cb1cc74..8035e6b40 100644 --- a/window.c +++ b/window.c @@ -73,6 +73,7 @@ struct window_pane_input_data { static struct window_pane *window_pane_create(struct window *, u_int, u_int, u_int); static void window_pane_destroy(struct window_pane *); +static void window_pane_free(struct window_pane *); static void window_pane_scrollbar_timer(int, short, void *); static void window_pane_full_size_offset(struct window_pane *wp, int *xoff, int *yoff, u_int *sx, u_int *sy); @@ -421,6 +422,25 @@ window_remove_ref(struct window *w, const char *from) window_destroy(w); } +void +window_pane_add_ref(struct window_pane *wp, const char *from) +{ + wp->references++; + log_debug("%s: %%%u %s, now %d", __func__, wp->id, from, + wp->references); +} + +void +window_pane_remove_ref(struct window_pane *wp, const char *from) +{ + wp->references--; + log_debug("%s: %%%u %s, now %d", __func__, wp->id, from, + wp->references); + + if (wp->references == 0) + window_pane_free(wp); +} + void window_set_name(struct window *w, const char *new_name, int untrusted) { @@ -1073,6 +1093,7 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit) char host[HOST_NAME_MAX + 1]; wp = xcalloc(1, sizeof *wp); + wp->references = 1; wp->window = w; wp->options = options_create(w->options); wp->flags = PANE_STYLECHANGED; @@ -1201,25 +1222,28 @@ window_pane_destroy(struct window_pane *wp) spawn_editor_finish(wp); window_pane_clear_prompt(wp); + RB_REMOVE(window_pane_tree, &all_window_panes, wp); + wp->flags |= PANE_DESTROYED; window_pane_free_modes(wp); screen_write_clear_dirty(wp); - free(wp->searchstr); if (wp->fd != -1) { bufferevent_free(wp->event); + wp->event = NULL; close(wp->fd); + wp->fd = -1; } - if (wp->ictx != NULL) + if (wp->ictx != NULL) { input_free(wp->ictx); - - screen_free(&wp->status_screen); - - screen_free(&wp->base); + wp->ictx = NULL; + } if (wp->pipe_fd != -1) { bufferevent_free(wp->pipe_event); + wp->pipe_event = NULL; close(wp->pipe_fd); + wp->pipe_fd = -1; } if (event_initialized(&wp->resize_timer)) @@ -1230,7 +1254,18 @@ window_pane_destroy(struct window_pane *wp) event_del(&wp->sb_auto_timer); window_pane_clear_resizes(wp, NULL); - RB_REMOVE(window_pane_tree, &all_window_panes, wp); + window_pane_remove_ref(wp, __func__); +} + +static void +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); options_free(wp->options); free((void *)wp->cwd); From 2b2963859b70aa43c3367ae327b16b1904a7036b Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 5 Jul 2026 21:56:52 +0100 Subject: [PATCH 056/127] Add some hooks tests. --- regress/hooks-after.sh | 124 +++++++++++++++++++++++++ regress/hooks-notify.sh | 199 ++++++++++++++++++++++++++++++++++++++++ regress/hooks.sh | 155 +++++++++++++++++++++++++++++++ 3 files changed, 478 insertions(+) create mode 100644 regress/hooks-after.sh create mode 100644 regress/hooks-notify.sh create mode 100644 regress/hooks.sh diff --git a/regress/hooks-after.sh b/regress/hooks-after.sh new file mode 100644 index 000000000..8677db7e4 --- /dev/null +++ b/regress/hooks-after.sh @@ -0,0 +1,124 @@ +#!/bin/sh + +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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest-hooks-after-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX 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'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 10 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +$TMUX new -d -s one || fail "new-session one failed" +$TMUX new -d -s two || fail "new-session two failed" + +# An after hook fires with the command arguments as formats. +$TMUX set -g @after 0 || fail "set @after failed" +$TMUX set-hook -g after-rename-window \ + 'set -gF @after "#{hook}|#{hook_argument_0}|#{hook_flag_t}"' || + fail "set-hook -g after-rename-window failed" +$TMUX rename-window -t one:0 first || fail "rename-window first failed" +wait_for @after 'after-rename-window|first|one:0' + +# An appended hook command runs after the first. +$TMUX set -g @after2 0 || fail "set @after2 failed" +$TMUX set-hook -ga after-rename-window \ + 'set -gF @after2 "#{@after}+2"' || + fail "set-hook -ga after-rename-window failed" +$TMUX rename-window -t one:0 second || fail "rename-window second failed" +wait_for @after 'after-rename-window|second|one:0' +wait_for @after2 'after-rename-window|second|one:0+2' +$TMUX set-hook -gu after-rename-window || fail "set-hook -gu failed" + +# A session after hook only fires for commands targeting that session and +# the hook commands run with the command target as current state. +$TMUX set -g @safter 0 || fail "set @safter failed" +$TMUX set-hook -t two after-rename-window \ + 'set -gF @safter "#{hook}:#{session_name}"' || + fail "set-hook -t two after-rename-window failed" +$TMUX rename-window -t one:0 third || fail "rename-window third failed" +assert_unchanged @safter 0 +$TMUX rename-window -t two:0 fourth || fail "rename-window fourth failed" +wait_for @safter 'after-rename-window:two' +$TMUX set-hook -u -t two after-rename-window || + fail "set-hook -u -t two failed" + +# The command-error hook fires when a command fails. +$TMUX set -g @error 0 || fail "set @error failed" +$TMUX set-hook -g command-error 'set -gF @error "#{hook}"' || + fail "set-hook -g command-error failed" +if $TMUX rename-window -t nosuchsession:0 x 2>/dev/null; then + fail "rename-window to missing session succeeded" +fi +wait_for @error 'command-error' +$TMUX set-hook -gu command-error || fail "set-hook -gu command-error failed" + +# Commands run from a hook do not fire their own after hooks. +$TMUX set -g @copy 0 || fail "set @copy failed" +$TMUX set-hook -g after-copy-mode 'set -gF @copy "#{hook}"' || + fail "set-hook -g after-copy-mode failed" +$TMUX copy-mode -t one:0 || fail "copy-mode failed" +wait_for @copy 'after-copy-mode' +$TMUX send-keys -t one:0 -X cancel || fail "cancel failed" +$TMUX set -g @copy 0 || fail "reset @copy failed" +$TMUX set -g @ran 0 || fail "set @ran failed" +$TMUX set-hook -g after-rename-window 'copy-mode -t one:0' || + fail "set-hook -g after-rename-window nested failed" +$TMUX set-hook -ga after-rename-window 'set -g @ran 1' || + fail "set-hook -ga after-rename-window nested failed" +$TMUX rename-window -t one:0 fifth || fail "rename-window fifth failed" +wait_for @ran 1 +mode=$($TMUX display -pt one:0 '#{pane_in_mode}') || + fail "display pane_in_mode failed" +[ "$mode" = 1 ] || fail "hook did not enter copy mode" +assert_unchanged @copy 0 + +exit 0 diff --git a/regress/hooks-notify.sh b/regress/hooks-notify.sh new file mode 100644 index 000000000..66eac8735 --- /dev/null +++ b/regress/hooks-notify.sh @@ -0,0 +1,199 @@ +#!/bin/sh + +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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest-hooks-notify-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX 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'" +} + +$TMUX new -d -s main || fail "new-session main failed" + +# session-created, session-renamed, session-closed. +$TMUX set -g @s 0 || fail "set @s failed" +$TMUX set-hook -g session-created \ + 'set -gF @s "#{hook}:#{hook_session_name}"' || + fail "set-hook session-created failed" +$TMUX new -d -s tmp || fail "new-session tmp failed" +wait_for @s 'session-created:tmp' +$TMUX set-hook -gu session-created || fail "unset session-created failed" + +$TMUX set-hook -g session-renamed \ + 'set -gF @s "#{hook}:#{hook_session_name}"' || + fail "set-hook session-renamed failed" +$TMUX rename-session -t tmp tmp2 || fail "rename-session failed" +wait_for @s 'session-renamed:tmp2' +$TMUX set-hook -gu session-renamed || fail "unset session-renamed failed" + +$TMUX set-hook -g session-closed \ + 'set -gF @s "#{hook}:#{hook_session_name}"' || + fail "set-hook session-closed failed" +$TMUX kill-session -t tmp2 || fail "kill-session failed" +wait_for @s 'session-closed:tmp2' +$TMUX set-hook -gu session-closed || fail "unset session-closed failed" + +# window-linked and window-unlinked from new-window and kill-window. +$TMUX set -g @w 0 || fail "set @w failed" +$TMUX set-hook -g window-linked \ + 'set -gF @w "#{hook}:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook window-linked failed" +$TMUX set-hook -g window-unlinked \ + 'set -gF @w "#{hook}:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook window-unlinked failed" +$TMUX neww -d -t main: -n mywin || fail "new-window failed" +wait_for @w 'window-linked:main:mywin' + +# window-linked and window-unlinked from link-window and unlink-window. +$TMUX new -d -s other || fail "new-session other failed" +$TMUX set -g @w 0 || fail "reset @w failed" +$TMUX link-window -s main:mywin -t other:5 || fail "link-window failed" +wait_for @w 'window-linked:other:mywin' +$TMUX set -g @w 0 || fail "reset @w failed" +$TMUX unlink-window -t other:5 || fail "unlink-window failed" +wait_for @w 'window-unlinked:other:mywin' +$TMUX set -g @w 0 || fail "reset @w failed" +$TMUX kill-window -t main:mywin || fail "kill-window failed" +wait_for @w 'window-unlinked:main:mywin' +$TMUX set-hook -gu window-linked || fail "unset window-linked failed" +$TMUX set-hook -gu window-unlinked || fail "unset window-unlinked failed" + +# window-renamed sees the window id. +window=$($TMUX display -pt main:0 '#{window_id}') || + fail "display window_id failed" +$TMUX set -g @r 0 || fail "set @r failed" +$TMUX set-hook -g window-renamed \ + 'set -gF @r "#{hook}:#{hook_window}:#{hook_window_name}"' || + fail "set-hook window-renamed failed" +$TMUX rename-window -t main:0 renamed || fail "rename-window failed" +wait_for @r "window-renamed:$window:renamed" +$TMUX set-hook -gu window-renamed || fail "unset window-renamed failed" + +# window-layout-changed from split-window. +$TMUX set -g @l 0 || fail "set @l failed" +$TMUX set-hook -g window-layout-changed \ + 'set -gF @l "#{hook}:#{hook_window}"' || + fail "set-hook window-layout-changed failed" +$TMUX splitw -d -t main:0 || fail "split-window failed" +wait_for @l "window-layout-changed:$window" +$TMUX set-hook -gu window-layout-changed || + fail "unset window-layout-changed failed" + +# window-pane-changed from select-pane. +$TMUX set -g @p 0 || fail "set @p failed" +$TMUX set-hook -g window-pane-changed \ + 'set -gF @p "#{hook}:#{hook_window}"' || + fail "set-hook window-pane-changed failed" +$TMUX selectp -t main:0.1 || fail "select-pane failed" +wait_for @p "window-pane-changed:$window" +$TMUX set-hook -gu window-pane-changed || + fail "unset window-pane-changed failed" + +# session-window-changed from select-window. +$TMUX neww -d -t main: -n w2 || fail "new-window w2 failed" +$TMUX set -g @c 0 || fail "set @c failed" +$TMUX set-hook -g session-window-changed \ + 'set -gF @c "#{hook}:#{hook_session_name}"' || + fail "set-hook session-window-changed failed" +$TMUX selectw -t main:w2 || fail "select-window failed" +wait_for @c 'session-window-changed:main' +$TMUX set-hook -gu session-window-changed || + fail "unset session-window-changed failed" + +# pane-mode-changed from entering and leaving copy mode. +$TMUX set -g @m 0 || fail "set @m failed" +pane=$($TMUX display -pt main:0.0 '#{pane_id}') || + fail "display pane_id failed" +$TMUX set-hook -g pane-mode-changed \ + 'set -gF @m "#{hook}:#{hook_pane}:#{pane_in_mode}"' || + fail "set-hook pane-mode-changed failed" +$TMUX copy-mode -t main:0.0 || fail "copy-mode failed" +wait_for @m "pane-mode-changed:$pane:1" +$TMUX send-keys -t main:0.0 -X cancel || fail "cancel failed" +wait_for @m "pane-mode-changed:$pane:0" +$TMUX set-hook -gu pane-mode-changed || fail "unset pane-mode-changed failed" + +# pane-exited when a pane's command exits with remain-on-exit off. +$TMUX set -g @x 0 || fail "set @x failed" +$TMUX set-hook -g pane-exited 'set -gF @x "#{hook}:#{hook_pane}"' || + fail "set-hook pane-exited failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'true') || + fail "split-window true failed" +wait_for @x "pane-exited:$pane" +$TMUX set-hook -gu pane-exited || fail "unset pane-exited failed" + +# pane-died when a pane's command exits with remain-on-exit on. +$TMUX set -g remain-on-exit on || fail "set remain-on-exit failed" +$TMUX set -g @d 0 || fail "set @d failed" +$TMUX set-hook -g pane-died 'set -gF @d "#{hook}:#{hook_pane}"' || + fail "set-hook pane-died failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'true') || + fail "split-window remain-on-exit failed" +wait_for @d "pane-died:$pane" +$TMUX set-hook -gu pane-died || fail "unset pane-died failed" +$TMUX killp -t "$pane" || fail "kill-pane failed" +$TMUX set -g remain-on-exit off || fail "reset remain-on-exit failed" + +# pane-title-changed when a pane sets its title. +$TMUX set -g @t 0 || fail "set @t failed" +$TMUX set-hook -g pane-title-changed \ + 'set -gF @t "#{hook}:#{hook_pane}:#{pane_title}"' || + fail "set-hook pane-title-changed failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' \ + 'printf "\033]2;mytitle\007"; sleep 30') || + fail "split-window title failed" +wait_for @t "pane-title-changed:$pane:mytitle" +$TMUX set-hook -gu pane-title-changed || + fail "unset pane-title-changed failed" +$TMUX killp -t "$pane" || fail "kill-pane title failed" + +# client-attached and client-detached using a control client. +$TMUX set -g @a 0 || fail "set @a failed" +$TMUX set-hook -g client-attached 'set -gF @a "#{hook}"' || + fail "set-hook client-attached failed" +$TMUX set-hook -g client-detached 'set -gF @a "#{hook}"' || + fail "set-hook client-detached failed" +mkfifo "$OUT/fifo" || fail "mkfifo failed" +$TMUX -C attach -t main <"$OUT/fifo" >"$OUT/control.out" 2>&1 & +exec 3>"$OUT/fifo" +wait_for @a 'client-attached' +exec 3>&- +wait_for @a 'client-detached' +$TMUX set-hook -gu client-attached || fail "unset client-attached failed" +$TMUX set-hook -gu client-detached || fail "unset client-detached failed" + +exit 0 diff --git a/regress/hooks.sh b/regress/hooks.sh new file mode 100644 index 000000000..b64943d84 --- /dev/null +++ b/regress/hooks.sh @@ -0,0 +1,155 @@ +#!/bin/sh + +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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest-hooks-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX 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'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 10 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +$TMUX new -d -s one || fail "new-session one failed" + +# A global hook fires and sees the hook formats. +$TMUX set -g @created 0 || fail "set @created failed" +$TMUX set-hook -g session-created \ + 'set -gF @created "#{hook}:#{hook_session_name}"' || + fail "set-hook -g session-created failed" +$TMUX new -d -s two || fail "new-session two failed" +wait_for @created 'session-created:two' + +# Hooks are arrays: an appended command runs after the first. +$TMUX set -g @second 0 || fail "set @second failed" +$TMUX set-hook -ga session-created \ + 'set -gF @second "#{@created}+second"' || + fail "set-hook -ga session-created failed" +$TMUX new -d -s three || fail "new-session three failed" +wait_for @created 'session-created:three' +wait_for @second 'session-created:three+second' + +# show-hooks lists both commands. +shown=$($TMUX show-hooks -g session-created) || + fail "show-hooks -g failed" +[ "$(echo "$shown" | wc -l)" -eq 2 ] || + fail "unexpected show-hooks output: $shown" +echo "$shown" | grep -q '^session-created\[0\]' || + fail "missing first array item: $shown" +echo "$shown" | grep -q '^session-created\[1\]' || + fail "missing second array item: $shown" + +# Unsetting removes the whole hook. +$TMUX set-hook -gu session-created || fail "set-hook -gu failed" +shown=$($TMUX show-hooks -g session-created) || + fail "show-hooks -g after unset failed" +if echo "$shown" | grep -q '\['; then + fail "show-hooks showed removed hook: $shown" +fi +$TMUX set -g @created 0 || fail "reset @created failed" +$TMUX new -d -s four || fail "new-session four failed" +assert_unchanged @created 0 + +# An unknown hook name is rejected. +if $TMUX set-hook -g no-such-hook 'display x' 2>/dev/null; then + fail "unknown hook name was accepted" +fi + +# A session hook only fires for events in that session. +$TMUX set -g @renamed 0 || fail "set @renamed failed" +$TMUX set-hook -t one window-renamed \ + 'set -gF @renamed "#{hook}:#{hook_window_name}"' || + fail "set-hook -t one window-renamed failed" +$TMUX rename-window -t two:0 twoname || fail "rename-window two failed" +assert_unchanged @renamed 0 +$TMUX rename-window -t one:0 onename || fail "rename-window one failed" +wait_for @renamed 'window-renamed:onename' +$TMUX set-hook -u -t one window-renamed || fail "set-hook -u failed" + +# A pane hook only fires for events in that pane. +pane2=$($TMUX splitw -d -t one:0 -P -F '#{pane_id}') || + fail "split-window failed" +$TMUX set -g @mode 0 || fail "set @mode failed" +$TMUX set-hook -p -t "$pane2" pane-mode-changed \ + 'set -gF @mode "#{hook}:#{hook_pane}"' || + fail "set-hook -p pane-mode-changed failed" +$TMUX copy-mode -t one:0.0 || fail "copy-mode pane 0 failed" +assert_unchanged @mode 0 +$TMUX copy-mode -t "$pane2" || fail "copy-mode pane 1 failed" +wait_for @mode "pane-mode-changed:$pane2" +$TMUX send-keys -t "$pane2" -X cancel || fail "cancel failed" +$TMUX set-hook -pu -t "$pane2" pane-mode-changed || + fail "set-hook -pu failed" + +# A window hook only fires for events in that window. +$TMUX set -g @wmode 0 || fail "set @wmode failed" +$TMUX set-hook -w -t two:0 pane-mode-changed \ + 'set -gF @wmode "#{hook}:#{hook_window_name}"' || + fail "set-hook -w pane-mode-changed failed" +$TMUX copy-mode -t three:0.0 || fail "copy-mode three failed" +assert_unchanged @wmode 0 +$TMUX copy-mode -t two:0.0 || fail "copy-mode two failed" +wait_for @wmode 'pane-mode-changed:twoname' +$TMUX set-hook -wu -t two:0 pane-mode-changed || + fail "set-hook -wu failed" + +# Commands run from a hook do not fire hooks again. +$TMUX set -g @renames '' || fail "set @renames failed" +$TMUX set-hook -g window-renamed \ + 'set -gF @renames "#{@renames}x" ; rename-window -t three:0 inner' || + fail "set-hook -g window-renamed failed" +$TMUX rename-window -t three:0 outer || fail "rename-window outer failed" +wait_for @renames 'x' +assert_unchanged @renames 'x' +name=$($TMUX display -pt three:0 '#{window_name}') || + fail "display window_name failed" +[ "$name" = inner ] || fail "expected window name inner but got $name" + +exit 0 From fcce73af4224fd7e172936f402f8d398ae4fa1d9 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 5 Jul 2026 22:14:48 +0100 Subject: [PATCH 057/127] Add another test. --- regress/hooks-lifecycle.sh | 162 +++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 regress/hooks-lifecycle.sh diff --git a/regress/hooks-lifecycle.sh b/regress/hooks-lifecycle.sh new file mode 100644 index 000000000..15b8cda94 --- /dev/null +++ b/regress/hooks-lifecycle.sh @@ -0,0 +1,162 @@ +#!/bin/sh + +# End-of-life hooks: pane-exited, pane-died, window-unlinked and +# session-closed when the pane, window or session they refer to is being +# or has been destroyed. Each hook appends to @log so the order hooks fire +# in is checked as well as the hook formats for the dead objects. + +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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest-hooks-lifecycle-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX 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'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 10 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +$TMUX new -d -s main || fail "new-session main failed" + +$TMUX set -g @log '' || fail "set @log failed" +$TMUX set-hook -g pane-exited \ + 'set -gF @log "#{@log}|pane-exited:#{hook_pane}"' || + fail "set-hook pane-exited failed" +$TMUX set-hook -g window-unlinked \ + 'set -gF @log "#{@log}|window-unlinked:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook window-unlinked failed" +$TMUX set-hook -g session-closed \ + 'set -gF @log "#{@log}|session-closed:#{hook_session_name}"' || + fail "set-hook session-closed failed" + +# The only pane of the only window of a session exits: pane-exited, then +# window-unlinked, then session-closed, each seeing the dead object in the +# hook formats. +pane=$($TMUX new -d -s doomed -n dwin -P -F '#{pane_id}' 'true') || + fail "new-session doomed failed" +wait_for @log \ + "|pane-exited:$pane|window-unlinked:doomed:dwin|session-closed:doomed" +assert_unchanged @log \ + "|pane-exited:$pane|window-unlinked:doomed:dwin|session-closed:doomed" + +# The dead pane, window and session cannot be used as targets but the +# server survives. +if $TMUX select-pane -t "$pane" 2>/dev/null; then + fail "dead pane still a valid target" +fi +if $TMUX list-windows -t doomed >/dev/null 2>&1; then + fail "dead session still a valid target" +fi +$TMUX list-panes -sat main >/dev/null || fail "list-panes failed" +$TMUX has -t main || fail "server died after pane exit chain" + +# kill-window on the last window: window-unlinked then session-closed but +# no pane-exited for the panes in the killed window. +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX new -d -s doomed2 -n dwin2 || fail "new-session doomed2 failed" +$TMUX splitw -d -t doomed2:0 || fail "split-window doomed2 failed" +$TMUX kill-window -t doomed2:0 || fail "kill-window failed" +wait_for @log '|window-unlinked:doomed2:dwin2|session-closed:doomed2' +assert_unchanged @log '|window-unlinked:doomed2:dwin2|session-closed:doomed2' +$TMUX has -t main || fail "server died after kill-window chain" + +# kill-session: session-closed fires first, then window-unlinked for its +# windows. A window linked into another session survives. +$TMUX new -d -s shareA -n shared || fail "new-session shareA failed" +$TMUX new -d -s shareB -n bwin || fail "new-session shareB failed" +$TMUX link-window -s shareA:shared -t shareB:7 || fail "link-window failed" +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX kill-session -t shareA || fail "kill-session shareA failed" +wait_for @log '|session-closed:shareA|window-unlinked:shareA:shared' +name=$($TMUX display -pt shareB:7 '#{window_name}') || + fail "shared window did not survive" +[ "$name" = shared ] || fail "expected window shared but got $name" + +# Killing the surviving session destroys the shared window for real while +# the session is being destroyed. +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX kill-window -t shareB:0 || fail "kill-window bwin failed" +wait_for @log '|window-unlinked:shareB:bwin' +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX kill-session -t shareB || fail "kill-session shareB failed" +wait_for @log '|session-closed:shareB|window-unlinked:shareB:shared' +$TMUX has -t main || fail "server died after kill-session chain" + +# A pane-died hook can kill its own dead pane (the hook runs with the dead +# pane as current target). The kill-pane runs without hooks so pane-exited +# does not fire. +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX new -d -s roe -n rwin || fail "new-session roe failed" +$TMUX set -wt roe:0 remain-on-exit on || fail "set remain-on-exit failed" +$TMUX set-hook -g pane-died \ + 'set -gF @log "#{@log}|pane-died:#{hook_pane}" ; kill-pane' || + fail "set-hook pane-died failed" +pane=$($TMUX splitw -d -t roe:0 -P -F '#{pane_id}' 'true') || + fail "split-window roe failed" +wait_for @log "|pane-died:$pane" +assert_unchanged @log "|pane-died:$pane" +if $TMUX select-pane -t "$pane" 2>/dev/null; then + fail "pane-died hook did not kill its pane" +fi +$TMUX list-panes -t roe:0 >/dev/null || fail "surviving pane broken" +$TMUX has -t roe || fail "session roe died" +$TMUX set-hook -gu pane-died || fail "unset pane-died failed" +$TMUX kill-session -t roe || fail "kill-session roe failed" + +# Killing the last session with end-of-life hooks still set: the server +# runs the hooks and exits cleanly. +$TMUX kill-session -t main || fail "kill-session main failed" +i=0 +while $TMUX has 2>/dev/null; do + i=$((i + 1)) + [ $i -lt 30 ] || fail "server still running after last session killed" + sleep 0.2 +done + +exit 0 From 2c0f06782dcf4d6def19d1e188997b4ab9c7d912 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 6 Jul 2026 08:47:57 +0100 Subject: [PATCH 058/127] Add alerts test. --- regress/alerts.sh | 291 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 regress/alerts.sh diff --git a/regress/alerts.sh b/regress/alerts.sh new file mode 100644 index 000000000..51554c46a --- /dev/null +++ b/regress/alerts.sh @@ -0,0 +1,291 @@ +#!/bin/sh + +# monitor-activity, monitor-bell and monitor-silence: both the +# alert-activity, alert-bell and alert-silence hooks and the winlink alert +# flags (window_activity_flag, window_bell_flag, window_silence_flag and +# the #, !, ~ characters in window_flags). The sessions are detached so +# alert flags are set even on the current window; the *-action options +# still decide whether the hooks fire. Panes run cat: activity is +# generated by the tty echo of send-keys and a bell by sending a BEL and +# newline for cat to write back. + +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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest-alerts-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX 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'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 10 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +wait_for_fmt() +{ + target=$1 + fmt=$2 + expected=$3 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX display -pt "$target" "$fmt" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $fmt for $target to be '$expected' but got '$value'" +} + +assert_fmt_unchanged() +{ + target=$1 + fmt=$2 + expected=$3 + i=0 + + while [ $i -lt 10 ]; do + value=$($TMUX display -pt "$target" "$fmt" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $fmt for $target to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +flags_have() +{ + target=$1 + char=$2 + + flags=$($TMUX display -pt "$target" '#{window_flags}') || + fail "display window_flags failed" + case "$flags" in + *"$char"*) ;; + *) fail "expected '$char' in window_flags for $target: '$flags'" ;; + esac +} + +flags_lack() +{ + target=$1 + char=$2 + + flags=$($TMUX display -pt "$target" '#{window_flags}') || + fail "display window_flags failed" + case "$flags" in + *"$char"*) fail "unexpected '$char' in window_flags for $target: '$flags'" ;; + esac +} + +bell() +{ + $TMUX send-keys -t "$1" -H 07 0a || fail "send-keys bell failed" +} + +activity() +{ + $TMUX send-keys -t "$1" -l x || fail "send-keys activity failed" +} + +$TMUX new -d -s mon -n w0 cat || fail "new-session mon failed" + +$TMUX set -g @log '' || fail "set @log failed" +$TMUX set-hook -g alert-bell \ + 'set -gF @log "#{@log}|alert-bell:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook alert-bell failed" +$TMUX set-hook -g alert-activity \ + 'set -gF @log "#{@log}|alert-activity:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook alert-activity failed" +$TMUX set-hook -g alert-silence \ + 'set -gF @log "#{@log}|alert-silence:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook alert-silence failed" + +# A bell in a non-current window (monitor-bell defaults to on) fires +# alert-bell and sets the bell flag, shown as ! in window_flags. +$TMUX neww -d -t mon: -n bellw cat || fail "new-window bellw failed" +assert_fmt_unchanged mon:bellw '#{window_bell_flag}' 0 +flags_lack mon:bellw '!' +bell mon:bellw +wait_for @log '|alert-bell:mon:bellw' +wait_for_fmt mon:bellw '#{window_bell_flag}' 1 +flags_have mon:bellw '!' + +# Bells are not deduplicated: a second bell fires the hook again even +# though the flag is still set. +bell mon:bellw +wait_for @log '|alert-bell:mon:bellw|alert-bell:mon:bellw' + +# Selecting the window clears the alert flags. +$TMUX selectw -t mon:bellw || fail "select-window bellw failed" +wait_for_fmt mon:bellw '#{window_bell_flag}' 0 +flags_lack mon:bellw '!' +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" + +# A bell in the current window of a detached session still sets the flag +# and bell-action any (the default) fires the hook for the current window. +$TMUX set -g @log '' || fail "reset @log failed" +bell mon:w0 +wait_for @log '|alert-bell:mon:w0' +wait_for_fmt mon:w0 '#{window_bell_flag}' 1 +$TMUX selectw -t mon:bellw || fail "select-window bellw failed" +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" +wait_for_fmt mon:w0 '#{window_bell_flag}' 0 + +# bell-action none: the flag is still set but the hook does not fire. +$TMUX set -t mon bell-action none || fail "set bell-action none failed" +$TMUX set -g @log '' || fail "reset @log failed" +bell mon:bellw +wait_for_fmt mon:bellw '#{window_bell_flag}' 1 +assert_unchanged @log '' +$TMUX set -ut mon bell-action || fail "unset bell-action failed" +$TMUX selectw -t mon:bellw || fail "select-window bellw failed" +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" + +# monitor-bell off: neither the flag nor the hook. +$TMUX set -wt mon:bellw monitor-bell off || fail "set monitor-bell failed" +$TMUX set -g @log '' || fail "reset @log failed" +bell mon:bellw +assert_fmt_unchanged mon:bellw '#{window_bell_flag}' 0 +assert_unchanged @log '' + +# monitor-activity defaults to off: output sets neither flag nor hook. +$TMUX neww -d -t mon: -n actw cat || fail "new-window actw failed" +$TMUX set -g @log '' || fail "reset @log failed" +activity mon:actw +assert_fmt_unchanged mon:actw '#{window_activity_flag}' 0 +assert_unchanged @log '' + +# With monitor-activity on, output fires alert-activity and sets the +# activity flag, shown as # in window_flags. +$TMUX set -wt mon:actw monitor-activity on || + fail "set monitor-activity failed" +activity mon:actw +wait_for @log '|alert-activity:mon:actw' +wait_for_fmt mon:actw '#{window_activity_flag}' 1 +flags_have mon:actw '#' + +# While the flag is set further activity does not fire the hook again. +$TMUX set -g @log '' || fail "reset @log failed" +activity mon:actw +assert_unchanged @log '' + +# On a detached session selecting the window does not clear the activity +# flag: session_set_current treats the selection itself as activity and +# the flag is raised again at once because the session is not attached. +# No hook fires as activity-action other does not apply to the current +# window. +$TMUX selectw -t mon:actw || fail "select-window actw failed" +assert_fmt_unchanged mon:actw '#{window_activity_flag}' 1 +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" +assert_fmt_unchanged mon:actw '#{window_activity_flag}' 1 +assert_unchanged @log '' + +# With a client attached, selecting the window does clear the activity +# flag and it stays clear for the current window. +mkfifo "$OUT/fifo" || fail "mkfifo failed" +$TMUX -C attach -t mon <"$OUT/fifo" >"$OUT/control.out" 2>&1 & +exec 3>"$OUT/fifo" +wait_for_fmt mon: '#{session_attached}' 1 +$TMUX selectw -t mon:actw || fail "select-window actw failed" +wait_for_fmt mon:actw '#{window_activity_flag}' 0 +flags_lack mon:actw '#' +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" +exec 3>&- +wait_for_fmt mon: '#{session_attached}' 0 + +# Once the flag is clear the alert is re-armed. +activity mon:actw +wait_for @log '|alert-activity:mon:actw' + +# Activity in the current window of a detached session sets the flag but +# activity-action other (the default) does not fire the hook. +$TMUX set -wt mon:w0 monitor-activity on || + fail "set monitor-activity w0 failed" +$TMUX set -g @log '' || fail "reset @log failed" +activity mon:w0 +wait_for_fmt mon:w0 '#{window_activity_flag}' 1 +assert_unchanged @log '' +$TMUX set -wut mon:w0 monitor-activity || + fail "unset monitor-activity w0 failed" + +# monitor-silence: a quiet window fires alert-silence after the interval +# and sets the silence flag, shown as ~ in window_flags. The hook fires +# only once while the flag remains set even though the timer keeps +# running. +$TMUX neww -d -t mon: -n silw cat || fail "new-window silw failed" +assert_fmt_unchanged mon:silw '#{window_silence_flag}' 0 +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX set -wt mon:silw monitor-silence 1 || fail "set monitor-silence failed" +wait_for @log '|alert-silence:mon:silw' +wait_for_fmt mon:silw '#{window_silence_flag}' 1 +flags_have mon:silw '~' +assert_unchanged @log '|alert-silence:mon:silw' +$TMUX set -wt mon:silw monitor-silence 0 || + fail "reset monitor-silence failed" +$TMUX selectw -t mon:silw || fail "select-window silw failed" +wait_for_fmt mon:silw '#{window_silence_flag}' 0 +flags_lack mon:silw '~' +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" + +# A window linked into two sessions: the hook fires once per winlink and +# the flag is set in both sessions. Selecting the window in one session +# clears the flags on every winlink. +$TMUX new -d -s mon2 || fail "new-session mon2 failed" +$TMUX neww -d -t mon: -n shw cat || fail "new-window shw failed" +$TMUX link-window -d -s mon:shw -t mon2:5 || fail "link-window failed" +$TMUX set -g @log '' || fail "reset @log failed" +bell mon:shw +wait_for @log '|alert-bell:mon:shw|alert-bell:mon2:shw' +wait_for_fmt mon:shw '#{window_bell_flag}' 1 +wait_for_fmt mon2:5 '#{window_bell_flag}' 1 +$TMUX selectw -t mon2:5 || fail "select-window mon2:5 failed" +wait_for_fmt mon2:5 '#{window_bell_flag}' 0 +wait_for_fmt mon:shw '#{window_bell_flag}' 0 + +exit 0 From 913e503296172d00d0903dbde02a3f448906f4a6 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 6 Jul 2026 07:57:45 +0000 Subject: [PATCH 059/127] Unzoom before creating a floating pane (for now). --- layout.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/layout.c b/layout.c index f020ef8ba..baa6ebee8 100644 --- a/layout.c +++ b/layout.c @@ -580,6 +580,7 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, enum layout_type type, int change) { struct layout_cell *lcchild; + int changed; /* Adjust the cell size. */ if (type == LAYOUT_LEFTRIGHT) @@ -613,6 +614,7 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, * until no further change is possible. */ while (change != 0) { + changed = 0; TAILQ_FOREACH(lcchild, &lc->cells, entry) { if (change == 0) break; @@ -622,13 +624,17 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, if (change > 0) { layout_resize_adjust(w, lcchild, type, 1); change--; + changed = 1; continue; } if (layout_resize_check(w, lcchild, type) > 0) { layout_resize_adjust(w, lcchild, type, -1); change++; + changed = 1; } } + if (!changed) + break; } } @@ -1663,6 +1669,7 @@ layout_get_floating_cell(struct cmdq_item *item, struct args *args, cause) != 0) return (NULL); + window_push_zoom(wp->window, 1, args_has(args, 'Z')); lcnew = layout_floating_pane(w, wp, sx, sy, ox, oy); return (lcnew); } From 6a6822b906479aff5daa0d9e7811a57866d5114f Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 6 Jul 2026 09:27:17 +0100 Subject: [PATCH 060/127] Shorten socket names. --- regress/alerts.sh | 2 +- regress/hooks-after.sh | 2 +- regress/hooks-lifecycle.sh | 2 +- regress/hooks-notify.sh | 2 +- regress/hooks.sh | 2 +- regress/set-hook-B.sh | 2 +- regress/set-hook-R.sh | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/regress/alerts.sh b/regress/alerts.sh index 51554c46a..7a39ddefc 100644 --- a/regress/alerts.sh +++ b/regress/alerts.sh @@ -19,7 +19,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest-alerts-$$ -f/dev/null" +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" fail() { diff --git a/regress/hooks-after.sh b/regress/hooks-after.sh index 8677db7e4..ee7fbfe0e 100644 --- a/regress/hooks-after.sh +++ b/regress/hooks-after.sh @@ -10,7 +10,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest-hooks-after-$$ -f/dev/null" +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" fail() { diff --git a/regress/hooks-lifecycle.sh b/regress/hooks-lifecycle.sh index 15b8cda94..6fb271131 100644 --- a/regress/hooks-lifecycle.sh +++ b/regress/hooks-lifecycle.sh @@ -15,7 +15,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest-hooks-lifecycle-$$ -f/dev/null" +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" fail() { diff --git a/regress/hooks-notify.sh b/regress/hooks-notify.sh index 66eac8735..947dd6c68 100644 --- a/regress/hooks-notify.sh +++ b/regress/hooks-notify.sh @@ -10,7 +10,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest-hooks-notify-$$ -f/dev/null" +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" fail() { diff --git a/regress/hooks.sh b/regress/hooks.sh index b64943d84..537582295 100644 --- a/regress/hooks.sh +++ b/regress/hooks.sh @@ -10,7 +10,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest-hooks-$$ -f/dev/null" +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" fail() { diff --git a/regress/set-hook-B.sh b/regress/set-hook-B.sh index 0d8eed372..188bfdd24 100755 --- a/regress/set-hook-B.sh +++ b/regress/set-hook-B.sh @@ -10,7 +10,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest-hook-B-$$ -f/dev/null" +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" fail() { diff --git a/regress/set-hook-R.sh b/regress/set-hook-R.sh index 40e81a29f..b36edc323 100644 --- a/regress/set-hook-R.sh +++ b/regress/set-hook-R.sh @@ -10,7 +10,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest-hook-R-$$ -f/dev/null" +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" fail() { From 501e8da925309934a932e5160b7a9ff21a2db57f Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 6 Jul 2026 10:00:26 +0100 Subject: [PATCH 061/127] Update some tests and improve Makefile. --- .github/workflows/regress.yml | 8 +++ .gitignore | 1 + regress/Makefile | 22 +++--- regress/choose-buffer.sh | 118 +++++++++++++++++++++----------- regress/choose-client.sh | 90 +++++++++++++++++------- regress/choose-tree.sh | 124 +++++++++++++++++++++------------- 6 files changed, 244 insertions(+), 119 deletions(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 0b42dd688..33fe52896 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -77,3 +77,11 @@ jobs: cd regress export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0" ${{ matrix.make }} + + - name: upload regress logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: regress-logs-${{ matrix.name }} + path: regress/logs/*.log + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 46e3ad595..31ca7add0 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ configure core etc/ fuzz/*-fuzzer +regress/logs/ tags tmux tmux.1.* diff --git a/regress/Makefile b/regress/Makefile index c354a8187..6370c77c9 100644 --- a/regress/Makefile +++ b/regress/Makefile @@ -1,22 +1,30 @@ TESTS!= echo *.sh +LOGDIR=logs -.PHONY: all $(TESTS) -.NOTPARALLEL: all $(TESTS) +.PHONY: all +.NOTPARALLEL: all all: - @failed=0; failures=; \ + @mkdir -p "$(LOGDIR)"; \ + rm -f "$(LOGDIR)"/*.log; \ + failed=0; failures=; \ for test in $(TESTS); do \ + base=$${test##*/}; \ + log="$(LOGDIR)/$${base%.sh}.log"; \ + rm -f "$$log"; \ printf '%-40s ' "$$test"; \ start=$$(date +%s); \ ASAN_OPTIONS="abort_on_error=1:detect_leaks=0:$$ASAN_OPTIONS"; \ env -i LC_CTYPE=C.UTF-8 ASAN_OPTIONS="$$ASAN_OPTIONS" \ - sh "$$test" >/dev/null 2>&1; \ + sh -x "$$test" >"$$log" 2>&1; \ if [ $$? -eq 0 ]; then \ end=$$(date +%s); \ + rm -f "$$log"; \ echo "PASS ($$((end - start))s)"; \ else \ end=$$(date +%s); \ echo "FAIL ($$((end - start))s)"; \ + echo " log: $$log"; \ failed=1; \ failures="$$failures $$test"; \ fi; \ @@ -28,9 +36,7 @@ all: for test in $$failures; do \ echo " $$test"; \ done; \ + else \ + rmdir "$(LOGDIR)" 2>/dev/null || true; \ fi; \ exit $$failed - -$(TESTS): - sh $@ - sleep 1 diff --git a/regress/choose-buffer.sh b/regress/choose-buffer.sh index 7d689f523..a10d161eb 100644 --- a/regress/choose-buffer.sh +++ b/regress/choose-buffer.sh @@ -17,18 +17,23 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMP=$(mktemp -d) || exit 1 +TMUX_TMPDIR="$TMP" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" +TMUX2="$TEST_TMUX -Ltest2$$ -f/dev/null" cleanup() { $TMUX kill-server 2>/dev/null $TMUX2 kill-server 2>/dev/null + rm -rf "$TMP" } +trap cleanup EXIT + fail() { - echo "$1" - cleanup + echo "$1" >&2 exit 1 } @@ -46,16 +51,33 @@ wait_for() { i=0 while [ "$i" -lt 50 ]; do - if capture | grep -q "$1"; then - sleep 0.5 + CAPTURED=$(capture) + if echo "$CAPTURED" | grep -q "$1"; then return 0 fi - sleep 0.5 + sleep 0.2 i=$((i + 1)) done fail "timed out waiting for '$1'" } +# wait_count $marker $n +# +# Wait (up to ~10s) until exactly $n rendered lines contain $marker. The +# matching capture is left in CAPTURED. +wait_count() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + c=$(echo "$CAPTURED" | grep -c "$1") + [ "$c" -eq "$2" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for $2 lines of '$1' (have $c)" +} + # wait_buffers $n # # Wait (up to ~10s) until the test server has exactly $n paste buffers. @@ -86,8 +108,30 @@ wait_clients() return 1 } -$TMUX kill-server 2>/dev/null -$TMUX2 kill-server 2>/dev/null +# wait_mode $target $state +# +# Wait (up to ~10s) until a pane enters or leaves mode. +wait_mode() +{ + t=$1 + want=$2 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane $t mode state is $got, expected $want" +} + +exit_mode() +{ + $TMUX send-keys -t aaa:0 "$@" || fail "send-keys $* failed" + wait_mode aaa:0 0 +} $TMUX new-session -d -s aaa -x 80 -y 24 || exit 1 @@ -100,57 +144,57 @@ $TMUX set-buffer -b bufz "other buffer" || exit 1 # --- filter by buffer name --------------------------------------------------- $TMUX choose-buffer -t aaa:0 -F 'B1' -f '#{==:#{buffer_name},bufa}' || exit 1 -wait_for 'B1' -out=$(capture) +wait_count ': B1' 1 +out=$CAPTURED echo "$out" | grep -q 'bufa: B1' || fail "bufa missing when it matches" echo "$out" | grep -q 'bufz: B1' && fail "bufz shown but does not match" [ "$(echo "$out" | grep -c ': B1')" -eq 1 ] || fail "expected 1 buffer" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- filter by buffer content ------------------------------------------------ $TMUX choose-buffer -t aaa:0 -F 'B2' -f '#{m:*hello*,#{buffer_sample}}' || \ exit 1 -wait_for 'B2' -out=$(capture) +wait_count ': B2' 1 +out=$CAPTURED echo "$out" | grep -q 'bufa: B2' || fail "bufa missing when content matches" echo "$out" | grep -q 'bufz: B2' && fail "bufz shown but content not matched" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- no filter shows both buffers --------------------------------------------- $TMUX choose-buffer -t aaa:0 -F 'B3' || exit 1 -wait_for 'B3' -out=$(capture) +wait_count ': B3' 2 +out=$CAPTURED echo "$out" | grep -q 'bufa: B3' || fail "bufa missing with no filter" echo "$out" | grep -q 'bufz: B3' || fail "bufz missing with no filter" [ "$(echo "$out" | grep -c ': B3')" -eq 2 ] || fail "expected 2 buffers" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- filter matching nothing --------------------------------------------------- # # Everything is shown and the filter indicator reports no matches. $TMUX choose-buffer -t aaa:0 -F 'B4' -f '#{==:#{buffer_name},nosuch}' || \ exit 1 -wait_for 'B4' -out=$(capture) +wait_count ': B4' 2 +out=$CAPTURED echo "$out" | grep -q 'bufa: B4' || fail "bufa missing with no-match filter" echo "$out" | grep -q 'bufz: B4' || fail "bufz missing with no-match filter" echo "$out" | grep -q 'no matches' || fail "no matches indicator missing" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- sort orders --------------------------------------------------------------- # # By name bufa sorts first and -r reverses. $TMUX choose-buffer -t aaa:0 -F 'B5' -O name || exit 1 -wait_for 'B5' -capture | grep ': B5' | head -1 | grep -q 'bufa: B5' || \ +wait_count ': B5' 2 +echo "$CAPTURED" | grep ': B5' | head -1 | grep -q 'bufa: B5' || \ fail "bufa not first with -O name" -$TMUX send-keys -t aaa:0 q +exit_mode q $TMUX choose-buffer -t aaa:0 -F 'B6' -O name -r || exit 1 -wait_for 'B6' -capture | grep ': B6' | head -1 | grep -q 'bufz: B6' || \ +wait_count ': B6' 2 +echo "$CAPTURED" | grep ': B6' | head -1 | grep -q 'bufz: B6' || \ fail "bufz not first with -O name -r" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- d deletes the selected buffer -------------------------------------------- # @@ -161,16 +205,16 @@ $TMUX send-keys -t aaa:0 d wait_buffers 1 $TMUX list-buffers -F '#{buffer_name}' | grep -q 'bufa' || \ fail "wrong buffer deleted" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- C-t tags all buffers and D deletes the tagged ------------------------------ $TMUX set-buffer -b bufz "other buffer" || exit 1 $TMUX set-buffer -b bufb "third buffer" || exit 1 $TMUX choose-buffer -t aaa:0 -F 'G2' || exit 1 -wait_for ': G2' +wait_count ': G2' 3 $TMUX send-keys -t aaa:0 C-t D wait_buffers 0 -$TMUX send-keys -t aaa:0 q +exit_mode q # --- Enter runs the default command (paste-buffer) ------------------------------ # @@ -178,22 +222,14 @@ $TMUX send-keys -t aaa:0 q # into the shell in the pane, where it appears on the screen. $TMUX set-buffer -b bufa "hello buffer" || exit 1 $TMUX choose-buffer -t aaa:0 -F 'G3' || exit 1 -wait_for 'bufa: G3' -$TMUX send-keys -t aaa:0 Enter -i=0 -while [ "$i" -lt 50 ]; do - [ "$($TMUX display -p -t aaa:0 '#{pane_in_mode}')" = "0" ] && break - sleep 0.5 - i=$((i + 1)) -done -[ "$i" -lt 50 ] || fail "mode did not exit after Enter" +wait_count ': G3' 1 +exit_mode Enter i=0 while [ "$i" -lt 50 ]; do $TMUX capture-pane -p -t aaa:0 | grep -q 'hello buffer' && break - sleep 0.5 + sleep 0.2 i=$((i + 1)) done [ "$i" -lt 50 ] || fail "buffer not pasted into pane" -cleanup exit 0 diff --git a/regress/choose-client.sh b/regress/choose-client.sh index 1e8423415..9031f86bd 100644 --- a/regress/choose-client.sh +++ b/regress/choose-client.sh @@ -16,18 +16,23 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMP=$(mktemp -d) || exit 1 +TMUX_TMPDIR="$TMP" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" +TMUX2="$TEST_TMUX -Ltest2$$ -f/dev/null" cleanup() { $TMUX kill-server 2>/dev/null $TMUX2 kill-server 2>/dev/null + rm -rf "$TMP" } +trap cleanup EXIT + fail() { - echo "$1" - cleanup + echo "$1" >&2 exit 1 } @@ -45,16 +50,33 @@ wait_for() { i=0 while [ "$i" -lt 50 ]; do - if capture | grep -q "$1"; then - sleep 0.5 + CAPTURED=$(capture) + if echo "$CAPTURED" | grep -q "$1"; then return 0 fi - sleep 0.5 + sleep 0.2 i=$((i + 1)) done fail "timed out waiting for '$1'" } +# wait_count $marker $n +# +# Wait (up to ~10s) until exactly $n rendered lines contain $marker. The +# matching capture is left in CAPTURED. +wait_count() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + c=$(echo "$CAPTURED" | grep -c "$1") + [ "$c" -eq "$2" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for $2 lines of '$1' (have $c)" +} + # wait_clients $n # # Wait (up to ~10s) until the test server has exactly $n clients attached. @@ -70,8 +92,30 @@ wait_clients() return 1 } -$TMUX kill-server 2>/dev/null -$TMUX2 kill-server 2>/dev/null +# wait_mode $target $state +# +# Wait (up to ~10s) until a pane enters or leaves mode. +wait_mode() +{ + t=$1 + want=$2 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane $t mode state is $got, expected $want" +} + +exit_mode() +{ + $TMUX send-keys -t aaa:0 "$@" || fail "send-keys $* failed" + wait_mode aaa:0 0 +} # One client attached to each of two sessions; the mode is displayed on the # client attached to aaa (in window 0 of the outer server) and the filters @@ -86,43 +130,43 @@ wait_clients 2 || fail "expected two clients attached to test server" # --- filter keeping only the aaa client ------------------------------------- $TMUX choose-client -t aaa:0 -F 'C1=#{client_session}' \ -f '#{==:#{client_session},aaa}' || exit 1 -wait_for 'C1=' -out=$(capture) +wait_count 'C1=' 1 +out=$CAPTURED echo "$out" | grep -q 'C1=aaa' || fail "aaa client missing when it matches" echo "$out" | grep -q 'C1=bbb' && fail "bbb client shown but does not match" [ "$(echo "$out" | grep -c 'C1=')" -eq 1 ] || fail "expected 1 client" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- filter keeping only the bbb client ------------------------------------- $TMUX choose-client -t aaa:0 -F 'C2=#{client_session}' \ -f '#{==:#{client_session},bbb}' || exit 1 -wait_for 'C2=' -out=$(capture) +wait_count 'C2=' 1 +out=$CAPTURED echo "$out" | grep -q 'C2=bbb' || fail "bbb client missing when it matches" echo "$out" | grep -q 'C2=aaa' && fail "aaa client shown but does not match" [ "$(echo "$out" | grep -c 'C2=')" -eq 1 ] || fail "expected 1 client" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- no filter shows both clients ------------------------------------------- $TMUX choose-client -t aaa:0 -F 'C3=#{client_session}' || exit 1 -wait_for 'C3=' -out=$(capture) +wait_count 'C3=' 2 +out=$CAPTURED echo "$out" | grep -q 'C3=aaa' || fail "aaa client missing with no filter" echo "$out" | grep -q 'C3=bbb' || fail "bbb client missing with no filter" [ "$(echo "$out" | grep -c 'C3=')" -eq 2 ] || fail "expected 2 clients" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- filter matching nothing ------------------------------------------------ # # Everything is shown and the filter indicator reports no matches. $TMUX choose-client -t aaa:0 -F 'C4=#{client_session}' \ -f '#{==:#{client_session},nosuch}' || exit 1 -wait_for 'C4=' -out=$(capture) +wait_count 'C4=' 2 +out=$CAPTURED echo "$out" | grep -q 'C4=aaa' || fail "aaa client missing with no-match filter" echo "$out" | grep -q 'C4=bbb' || fail "bbb client missing with no-match filter" echo "$out" | grep -q 'no matches' || fail "no matches indicator missing" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- Enter runs the default command (detach-client) -------------------------- # @@ -131,10 +175,10 @@ $TMUX send-keys -t aaa:0 q $TMUX choose-client -t aaa:0 -F 'G1=#{client_session}' \ -f '#{==:#{client_session},bbb}' || exit 1 wait_for 'G1=bbb' -$TMUX send-keys -t aaa:0 Enter +$TMUX send-keys -t aaa:0 Enter || fail "send-keys Enter failed" wait_clients 1 || fail "bbb client did not detach" +wait_mode aaa:0 0 [ "$($TMUX list-clients -F '#{client_session}')" = "aaa" ] || \ fail "wrong client detached" -cleanup exit 0 diff --git a/regress/choose-tree.sh b/regress/choose-tree.sh index 15add9f3f..b34cc2bd2 100644 --- a/regress/choose-tree.sh +++ b/regress/choose-tree.sh @@ -24,18 +24,23 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMP=$(mktemp -d) || exit 1 +TMUX_TMPDIR="$TMP" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" +TMUX2="$TEST_TMUX -Ltest2$$ -f/dev/null" cleanup() { $TMUX kill-server 2>/dev/null $TMUX2 kill-server 2>/dev/null + rm -rf "$TMP" } +trap cleanup EXIT + fail() { - echo "$1" - cleanup + echo "$1" >&2 exit 1 } @@ -53,11 +58,11 @@ wait_for() { i=0 while [ "$i" -lt 50 ]; do - if capture | grep -q "$1"; then - sleep 0.5 + CAPTURED=$(capture) + if echo "$CAPTURED" | grep -q "$1"; then return 0 fi - sleep 0.5 + sleep 0.2 i=$((i + 1)) done fail "timed out waiting for '$1'" @@ -70,11 +75,13 @@ wait_count() { i=0 while [ "$i" -lt 50 ]; do - [ "$(capture | grep -c "$1")" -eq "$2" ] && return 0 - sleep 0.5 + CAPTURED=$(capture) + c=$(echo "$CAPTURED" | grep -c "$1") + [ "$c" -eq "$2" ] && return 0 + sleep 0.2 i=$((i + 1)) done - fail "timed out waiting for $2 lines of '$1' (have $(capture | grep -c "$1"))" + fail "timed out waiting for $2 lines of '$1' (have $c)" } # wait_clients $n @@ -92,8 +99,30 @@ wait_clients() return 1 } -$TMUX kill-server 2>/dev/null -$TMUX2 kill-server 2>/dev/null +# wait_mode $target $state +# +# Wait (up to ~10s) until a pane enters or leaves mode. +wait_mode() +{ + t=$1 + want=$2 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane $t mode state is $got, expected $want" +} + +exit_mode() +{ + $TMUX send-keys -t aaa:0 "$@" || fail "send-keys $* failed" + wait_mode aaa:0 0 +} # Session zzz is created first, so it sorts first by index, and has a # two-pane window 0 and a single-pane window 1. Session aaa has one window @@ -117,10 +146,10 @@ wait_clients 1 || fail "no client attached to test server" # (session, window, pane). $TMUX choose-tree -t aaa:0 -F 'F1' -f '#{==:#{session_name},aaa}' || exit 1 wait_count 'F1' 3 -out=$(capture) +out=$CAPTURED echo "$out" | grep -q 'aaa: F1' || fail "aaa missing when filter matches it" echo "$out" | grep -q 'zzz: F1' && fail "zzz shown but no pane matches" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- filter keeping only zzz ------------------------------------------------ # @@ -128,10 +157,10 @@ $TMUX send-keys -t aaa:0 q # disappear. $TMUX choose-tree -t aaa:0 -F 'F2' -f '#{==:#{session_name},zzz}' || exit 1 wait_count 'F2' 6 -out=$(capture) +out=$CAPTURED echo "$out" | grep -q 'zzz: F2' || fail "zzz missing when filter matches it" echo "$out" | grep -q 'aaa: F2' && fail "aaa shown but no pane matches" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- filter matching a single pane ------------------------------------------ # @@ -139,22 +168,22 @@ $TMUX send-keys -t aaa:0 q # and that pane; zzz:1 and all of aaa must disappear. $TMUX choose-tree -t aaa:0 -F 'F3' -f '#{==:#{pane_index},1}' || exit 1 wait_count 'F3' 3 -out=$(capture) +out=$CAPTURED echo "$out" | grep -q 'zzz: F3' || fail "zzz missing when its pane matches" echo "$out" | grep -q 'aaa: F3' && fail "aaa shown but no pane matches" echo "$out" | grep -q '1: F3' || fail "matching pane missing" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- filter matching nothing ------------------------------------------------ # # Everything is shown and the filter indicator reports no matches. $TMUX choose-tree -t aaa:0 -F 'F4' -f '#{==:#{session_name},nosuch}' || exit 1 -wait_for 'F4' -out=$(capture) +wait_count 'F4' 9 +out=$CAPTURED echo "$out" | grep -q 'aaa: F4' || fail "aaa missing with no-match filter" echo "$out" | grep -q 'zzz: F4' || fail "zzz missing with no-match filter" echo "$out" | grep -q 'no matches' || fail "no matches indicator missing" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- -h with the tree pane as the only match -------------------------------- # @@ -163,29 +192,29 @@ $TMUX send-keys -t aaa:0 q $TMUX choose-tree -h -t aaa:0 -F 'F5' -f '#{==:#{session_name},aaa}' || \ exit 1 wait_count 'F5' 2 -capture | grep -q 'aaa: F5' || fail "aaa missing with -h" -$TMUX send-keys -t aaa:0 q +echo "$CAPTURED" | grep -q 'aaa: F5' || fail "aaa missing with -h" +exit_mode q # --- sort orders ------------------------------------------------------------ # # By index zzz (created first) sorts first, by name aaa does, and -r reverses. $TMUX choose-tree -t aaa:0 -F 'F6' -O index || exit 1 -wait_for 'F6' -capture | grep 'F6' | head -1 | grep -q 'zzz: F6' || \ +wait_count 'F6' 9 +echo "$CAPTURED" | grep 'F6' | head -1 | grep -q 'zzz: F6' || \ fail "zzz not first with -O index" -$TMUX send-keys -t aaa:0 q +exit_mode q $TMUX choose-tree -t aaa:0 -F 'F7' -O name || exit 1 -wait_for 'F7' -capture | grep 'F7' | head -1 | grep -q 'aaa: F7' || \ +wait_count 'F7' 9 +echo "$CAPTURED" | grep 'F7' | head -1 | grep -q 'aaa: F7' || \ fail "aaa not first with -O name" -$TMUX send-keys -t aaa:0 q +exit_mode q $TMUX choose-tree -t aaa:0 -F 'F8' -O name -r || exit 1 -wait_for 'F8' -capture | grep 'F8' | head -1 | grep -q 'zzz: F8' || \ +wait_count 'F8' 9 +echo "$CAPTURED" | grep 'F8' | head -1 | grep -q 'zzz: F8' || \ fail "zzz not first with -O name -r" -$TMUX send-keys -t aaa:0 q +exit_mode q # --- collapse and expand with h and l ----------------------------------------- # @@ -193,41 +222,43 @@ $TMUX send-keys -t aaa:0 q # l expands it again. $TMUX choose-tree -t aaa:0 -F 'G1' -O index || exit 1 wait_count 'G1' 9 -$TMUX send-keys -t aaa:0 g h +$TMUX send-keys -t aaa:0 g h || fail "send-keys collapse failed" wait_count 'G1' 4 -$TMUX send-keys -t aaa:0 l +$TMUX send-keys -t aaa:0 l || fail "send-keys expand failed" wait_count 'G1' 9 -$TMUX send-keys -t aaa:0 q +exit_mode q # --- filter entered at the prompt with f, cleared with c ---------------------- $TMUX choose-tree -t aaa:0 -F 'G2' -O index || exit 1 wait_count 'G2' 9 -$TMUX send-keys -t aaa:0 f -$TMUX send-keys -t aaa:0 -l '#{==:#{session_name},aaa}' -$TMUX send-keys -t aaa:0 Enter +$TMUX send-keys -t aaa:0 f || fail "send-keys f failed" +$TMUX send-keys -t aaa:0 -l '#{==:#{session_name},aaa}' || \ + fail "send-keys filter failed" +$TMUX send-keys -t aaa:0 Enter || fail "send-keys Enter failed" wait_count 'G2' 3 -out=$(capture) +out=$CAPTURED echo "$out" | grep -q 'aaa: G2' || fail "aaa missing with prompt filter" echo "$out" | grep -q 'zzz: G2' && fail "zzz shown with prompt filter" -$TMUX send-keys -t aaa:0 c +$TMUX send-keys -t aaa:0 c || fail "send-keys c failed" wait_count 'G2' 9 -$TMUX send-keys -t aaa:0 q +exit_mode q # --- Enter runs the default command (switch-client) ---------------------------- # # g selects session zzz and Enter switches the client to it. $TMUX choose-tree -t aaa:0 -F 'G3' -O index || exit 1 wait_count 'G3' 9 -$TMUX send-keys -t aaa:0 g Enter +$TMUX send-keys -t aaa:0 g Enter || fail "send-keys Enter failed" i=0 while [ "$i" -lt 50 ]; do [ "$($TMUX list-clients -F '#{client_session}')" = "zzz" ] && break - sleep 0.5 + sleep 0.2 i=$((i + 1)) done [ "$i" -lt 50 ] || fail "client did not switch to zzz" $TMUX switch-client -c "$($TMUX list-clients -F '#{client_name}')" -t aaa || \ exit 1 +wait_mode aaa:0 0 # --- x kills the current item after confirmation ------------------------------- # @@ -235,13 +266,12 @@ $TMUX switch-client -c "$($TMUX list-clients -F '#{client_name}')" -t aaa || \ # kills it, leaving zzz with one window and the tree with seven lines. $TMUX choose-tree -t aaa:0 -F 'G4' -O index || exit 1 wait_count 'G4' 9 -$TMUX send-keys -t aaa:0 g j j j j x +$TMUX send-keys -t aaa:0 g j j j j x || fail "send-keys x failed" wait_for 'Kill window 1' -$TMUX send-keys -t aaa:0 y +$TMUX send-keys -t aaa:0 y || fail "send-keys y failed" wait_count 'G4' 7 [ "$($TMUX list-windows -t zzz -F x | grep -c x)" -eq 1 ] || \ fail "window 1 of zzz not killed" -$TMUX send-keys -t aaa:0 q +exit_mode q -cleanup exit 0 From 37e4b3cdb6e58f51d54db5eb2172dd08eed2606a Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 6 Jul 2026 10:12:24 +0100 Subject: [PATCH 062/127] Fix name. --- .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 33fe52896..96cee88b4 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -78,7 +78,7 @@ jobs: export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0" ${{ matrix.make }} - - name: upload regress logs + - name: logs if: failure() uses: actions/upload-artifact@v4 with: From 069201dd1a79006c75e44ea6ea8a62ad59063aa0 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 6 Jul 2026 11:22:56 +0100 Subject: [PATCH 063/127] Regression test fixes. --- regress/choose-buffer.sh | 90 +++++++++++++++++++--------- regress/choose-client.sh | 74 ++++++++++++++++------- regress/choose-tree.sh | 98 ++++++++++++++++++------------- regress/format-render-contexts.sh | 25 +++++++- regress/mode-mutation.sh | 24 +++++--- 5 files changed, 213 insertions(+), 98 deletions(-) diff --git a/regress/choose-buffer.sh b/regress/choose-buffer.sh index a10d161eb..60f15478f 100644 --- a/regress/choose-buffer.sh +++ b/regress/choose-buffer.sh @@ -52,7 +52,7 @@ wait_for() i=0 while [ "$i" -lt 50 ]; do CAPTURED=$(capture) - if echo "$CAPTURED" | grep -q "$1"; then + if printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then return 0 fi sleep 0.2 @@ -61,6 +61,23 @@ wait_for() fail "timed out waiting for '$1'" } +# wait_gone $marker +# +# Wait (up to ~10s) until the rendered screen no longer contains $marker. +wait_gone() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + if ! printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1' to disappear" +} + # wait_count $marker $n # # Wait (up to ~10s) until exactly $n rendered lines contain $marker. The @@ -70,7 +87,7 @@ wait_count() i=0 while [ "$i" -lt 50 ]; do CAPTURED=$(capture) - c=$(echo "$CAPTURED" | grep -c "$1") + c=$(printf '%s\n' "$CAPTURED" | grep -F -c "$1") [ "$c" -eq "$2" ] && return 0 sleep 0.2 i=$((i + 1)) @@ -129,11 +146,15 @@ wait_mode() exit_mode() { + marker=$1 + shift + $TMUX send-keys -t aaa:0 "$@" || fail "send-keys $* failed" wait_mode aaa:0 0 + wait_gone "$marker" } -$TMUX new-session -d -s aaa -x 80 -y 24 || exit 1 +$TMUX new-session -d -s aaa -x 80 -y 24 'cat' || exit 1 $TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1 wait_clients 1 || fail "no client attached to test server" @@ -146,28 +167,36 @@ $TMUX set-buffer -b bufz "other buffer" || exit 1 $TMUX choose-buffer -t aaa:0 -F 'B1' -f '#{==:#{buffer_name},bufa}' || exit 1 wait_count ': B1' 1 out=$CAPTURED -echo "$out" | grep -q 'bufa: B1' || fail "bufa missing when it matches" -echo "$out" | grep -q 'bufz: B1' && fail "bufz shown but does not match" -[ "$(echo "$out" | grep -c ': B1')" -eq 1 ] || fail "expected 1 buffer" -exit_mode q +printf '%s\n' "$out" | grep -F -q 'bufa: B1' || \ + fail "bufa missing when it matches" +printf '%s\n' "$out" | grep -F -q 'bufz: B1' && \ + fail "bufz shown but does not match" +[ "$(printf '%s\n' "$out" | grep -F -c ': B1')" -eq 1 ] || \ + fail "expected 1 buffer" +exit_mode ': B1' q # --- filter by buffer content ------------------------------------------------ $TMUX choose-buffer -t aaa:0 -F 'B2' -f '#{m:*hello*,#{buffer_sample}}' || \ exit 1 wait_count ': B2' 1 out=$CAPTURED -echo "$out" | grep -q 'bufa: B2' || fail "bufa missing when content matches" -echo "$out" | grep -q 'bufz: B2' && fail "bufz shown but content not matched" -exit_mode q +printf '%s\n' "$out" | grep -F -q 'bufa: B2' || \ + fail "bufa missing when content matches" +printf '%s\n' "$out" | grep -F -q 'bufz: B2' && \ + fail "bufz shown but content not matched" +exit_mode ': B2' q # --- no filter shows both buffers --------------------------------------------- $TMUX choose-buffer -t aaa:0 -F 'B3' || exit 1 wait_count ': B3' 2 out=$CAPTURED -echo "$out" | grep -q 'bufa: B3' || fail "bufa missing with no filter" -echo "$out" | grep -q 'bufz: B3' || fail "bufz missing with no filter" -[ "$(echo "$out" | grep -c ': B3')" -eq 2 ] || fail "expected 2 buffers" -exit_mode q +printf '%s\n' "$out" | grep -F -q 'bufa: B3' || \ + fail "bufa missing with no filter" +printf '%s\n' "$out" | grep -F -q 'bufz: B3' || \ + fail "bufz missing with no filter" +[ "$(printf '%s\n' "$out" | grep -F -c ': B3')" -eq 2 ] || \ + fail "expected 2 buffers" +exit_mode ': B3' q # --- filter matching nothing --------------------------------------------------- # @@ -176,25 +205,30 @@ $TMUX choose-buffer -t aaa:0 -F 'B4' -f '#{==:#{buffer_name},nosuch}' || \ exit 1 wait_count ': B4' 2 out=$CAPTURED -echo "$out" | grep -q 'bufa: B4' || fail "bufa missing with no-match filter" -echo "$out" | grep -q 'bufz: B4' || fail "bufz missing with no-match filter" -echo "$out" | grep -q 'no matches' || fail "no matches indicator missing" -exit_mode q +printf '%s\n' "$out" | grep -F -q 'bufa: B4' || \ + fail "bufa missing with no-match filter" +printf '%s\n' "$out" | grep -F -q 'bufz: B4' || \ + fail "bufz missing with no-match filter" +printf '%s\n' "$out" | grep -F -q 'no matches' || \ + fail "no matches indicator missing" +exit_mode ': B4' q # --- sort orders --------------------------------------------------------------- # # By name bufa sorts first and -r reverses. $TMUX choose-buffer -t aaa:0 -F 'B5' -O name || exit 1 wait_count ': B5' 2 -echo "$CAPTURED" | grep ': B5' | head -1 | grep -q 'bufa: B5' || \ +printf '%s\n' "$CAPTURED" | grep -F ': B5' | head -1 | \ + grep -F -q 'bufa: B5' || \ fail "bufa not first with -O name" -exit_mode q +exit_mode ': B5' q $TMUX choose-buffer -t aaa:0 -F 'B6' -O name -r || exit 1 wait_count ': B6' 2 -echo "$CAPTURED" | grep ': B6' | head -1 | grep -q 'bufz: B6' || \ +printf '%s\n' "$CAPTURED" | grep -F ': B6' | head -1 | \ + grep -F -q 'bufz: B6' || \ fail "bufz not first with -O name -r" -exit_mode q +exit_mode ': B6' q # --- d deletes the selected buffer -------------------------------------------- # @@ -203,9 +237,9 @@ $TMUX choose-buffer -t aaa:0 -F 'G1' -f '#{==:#{buffer_name},bufz}' || exit 1 wait_for 'bufz: G1' $TMUX send-keys -t aaa:0 d wait_buffers 1 -$TMUX list-buffers -F '#{buffer_name}' | grep -q 'bufa' || \ +$TMUX list-buffers -F '#{buffer_name}' | grep -F -q 'bufa' || \ fail "wrong buffer deleted" -exit_mode q +exit_mode ': G1' q # --- C-t tags all buffers and D deletes the tagged ------------------------------ $TMUX set-buffer -b bufz "other buffer" || exit 1 @@ -214,7 +248,8 @@ $TMUX choose-buffer -t aaa:0 -F 'G2' || exit 1 wait_count ': G2' 3 $TMUX send-keys -t aaa:0 C-t D wait_buffers 0 -exit_mode q +wait_mode aaa:0 0 +wait_gone ': G2' # --- Enter runs the default command (paste-buffer) ------------------------------ # @@ -223,10 +258,11 @@ exit_mode q $TMUX set-buffer -b bufa "hello buffer" || exit 1 $TMUX choose-buffer -t aaa:0 -F 'G3' || exit 1 wait_count ': G3' 1 -exit_mode Enter +exit_mode ': G3' Enter i=0 while [ "$i" -lt 50 ]; do - $TMUX capture-pane -p -t aaa:0 | grep -q 'hello buffer' && break + $TMUX capture-pane -p -t aaa:0 | grep -F -q 'hello buffer' && \ + break sleep 0.2 i=$((i + 1)) done diff --git a/regress/choose-client.sh b/regress/choose-client.sh index 9031f86bd..621cb608f 100644 --- a/regress/choose-client.sh +++ b/regress/choose-client.sh @@ -51,7 +51,7 @@ wait_for() i=0 while [ "$i" -lt 50 ]; do CAPTURED=$(capture) - if echo "$CAPTURED" | grep -q "$1"; then + if printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then return 0 fi sleep 0.2 @@ -60,6 +60,23 @@ wait_for() fail "timed out waiting for '$1'" } +# wait_gone $marker +# +# Wait (up to ~10s) until the rendered screen no longer contains $marker. +wait_gone() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + if ! printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1' to disappear" +} + # wait_count $marker $n # # Wait (up to ~10s) until exactly $n rendered lines contain $marker. The @@ -69,7 +86,7 @@ wait_count() i=0 while [ "$i" -lt 50 ]; do CAPTURED=$(capture) - c=$(echo "$CAPTURED" | grep -c "$1") + c=$(printf '%s\n' "$CAPTURED" | grep -F -c "$1") [ "$c" -eq "$2" ] && return 0 sleep 0.2 i=$((i + 1)) @@ -113,15 +130,19 @@ wait_mode() exit_mode() { + marker=$1 + shift + $TMUX send-keys -t aaa:0 "$@" || fail "send-keys $* failed" wait_mode aaa:0 0 + wait_gone "$marker" } # One client attached to each of two sessions; the mode is displayed on the # client attached to aaa (in window 0 of the outer server) and the filters # tell the clients apart by their attached session. -$TMUX new-session -d -s aaa -x 80 -y 24 || exit 1 -$TMUX new-session -d -s bbb -x 80 -y 24 || exit 1 +$TMUX new-session -d -s aaa -x 80 -y 24 'cat' || exit 1 +$TMUX new-session -d -s bbb -x 80 -y 24 'cat' || exit 1 $TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1 $TMUX2 new-window -d -t out: "$TMUX attach -t bbb" || exit 1 @@ -132,29 +153,38 @@ $TMUX choose-client -t aaa:0 -F 'C1=#{client_session}' \ -f '#{==:#{client_session},aaa}' || exit 1 wait_count 'C1=' 1 out=$CAPTURED -echo "$out" | grep -q 'C1=aaa' || fail "aaa client missing when it matches" -echo "$out" | grep -q 'C1=bbb' && fail "bbb client shown but does not match" -[ "$(echo "$out" | grep -c 'C1=')" -eq 1 ] || fail "expected 1 client" -exit_mode q +printf '%s\n' "$out" | grep -F -q 'C1=aaa' || \ + fail "aaa client missing when it matches" +printf '%s\n' "$out" | grep -F -q 'C1=bbb' && \ + fail "bbb client shown but does not match" +[ "$(printf '%s\n' "$out" | grep -F -c 'C1=')" -eq 1 ] || \ + fail "expected 1 client" +exit_mode 'C1=' q # --- filter keeping only the bbb client ------------------------------------- $TMUX choose-client -t aaa:0 -F 'C2=#{client_session}' \ -f '#{==:#{client_session},bbb}' || exit 1 wait_count 'C2=' 1 out=$CAPTURED -echo "$out" | grep -q 'C2=bbb' || fail "bbb client missing when it matches" -echo "$out" | grep -q 'C2=aaa' && fail "aaa client shown but does not match" -[ "$(echo "$out" | grep -c 'C2=')" -eq 1 ] || fail "expected 1 client" -exit_mode q +printf '%s\n' "$out" | grep -F -q 'C2=bbb' || \ + fail "bbb client missing when it matches" +printf '%s\n' "$out" | grep -F -q 'C2=aaa' && \ + fail "aaa client shown but does not match" +[ "$(printf '%s\n' "$out" | grep -F -c 'C2=')" -eq 1 ] || \ + fail "expected 1 client" +exit_mode 'C2=' q # --- no filter shows both clients ------------------------------------------- $TMUX choose-client -t aaa:0 -F 'C3=#{client_session}' || exit 1 wait_count 'C3=' 2 out=$CAPTURED -echo "$out" | grep -q 'C3=aaa' || fail "aaa client missing with no filter" -echo "$out" | grep -q 'C3=bbb' || fail "bbb client missing with no filter" -[ "$(echo "$out" | grep -c 'C3=')" -eq 2 ] || fail "expected 2 clients" -exit_mode q +printf '%s\n' "$out" | grep -F -q 'C3=aaa' || \ + fail "aaa client missing with no filter" +printf '%s\n' "$out" | grep -F -q 'C3=bbb' || \ + fail "bbb client missing with no filter" +[ "$(printf '%s\n' "$out" | grep -F -c 'C3=')" -eq 2 ] || \ + fail "expected 2 clients" +exit_mode 'C3=' q # --- filter matching nothing ------------------------------------------------ # @@ -163,10 +193,13 @@ $TMUX choose-client -t aaa:0 -F 'C4=#{client_session}' \ -f '#{==:#{client_session},nosuch}' || exit 1 wait_count 'C4=' 2 out=$CAPTURED -echo "$out" | grep -q 'C4=aaa' || fail "aaa client missing with no-match filter" -echo "$out" | grep -q 'C4=bbb' || fail "bbb client missing with no-match filter" -echo "$out" | grep -q 'no matches' || fail "no matches indicator missing" -exit_mode q +printf '%s\n' "$out" | grep -F -q 'C4=aaa' || \ + fail "aaa client missing with no-match filter" +printf '%s\n' "$out" | grep -F -q 'C4=bbb' || \ + fail "bbb client missing with no-match filter" +printf '%s\n' "$out" | grep -F -q 'no matches' || \ + fail "no matches indicator missing" +exit_mode 'C4=' q # --- Enter runs the default command (detach-client) -------------------------- # @@ -178,6 +211,7 @@ wait_for 'G1=bbb' $TMUX send-keys -t aaa:0 Enter || fail "send-keys Enter failed" wait_clients 1 || fail "bbb client did not detach" wait_mode aaa:0 0 +wait_gone 'G1=' [ "$($TMUX list-clients -F '#{client_session}')" = "aaa" ] || \ fail "wrong client detached" diff --git a/regress/choose-tree.sh b/regress/choose-tree.sh index b34cc2bd2..8346b4cae 100644 --- a/regress/choose-tree.sh +++ b/regress/choose-tree.sh @@ -59,7 +59,7 @@ wait_for() i=0 while [ "$i" -lt 50 ]; do CAPTURED=$(capture) - if echo "$CAPTURED" | grep -q "$1"; then + if printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then return 0 fi sleep 0.2 @@ -68,15 +68,15 @@ wait_for() fail "timed out waiting for '$1'" } -# wait_count $marker $n +# wait_count $row-marker $n # -# Wait (up to ~10s) until exactly $n rendered lines contain $marker. +# Wait (up to ~10s) until exactly $n rendered list rows contain $row-marker. wait_count() { i=0 while [ "$i" -lt 50 ]; do CAPTURED=$(capture) - c=$(echo "$CAPTURED" | grep -c "$1") + c=$(printf '%s\n' "$CAPTURED" | grep -F -c "$1") [ "$c" -eq "$2" ] && return 0 sleep 0.2 i=$((i + 1)) @@ -131,10 +131,10 @@ exit_mode() # # 0 zzz 1 window 0 2 pane 0 3 pane 1 4 window 1 5 pane 0 # 6 aaa 7 window 0 8 pane 0 -$TMUX new-session -d -s zzz -x 80 -y 24 || exit 1 -$TMUX split-window -t zzz:0 || exit 1 -$TMUX new-window -t zzz || exit 1 -$TMUX new-session -d -s aaa -x 80 -y 24 || exit 1 +$TMUX new-session -d -s zzz -x 80 -y 24 'cat' || exit 1 +$TMUX split-window -t zzz:0 'cat' || exit 1 +$TMUX new-window -t zzz 'cat' || exit 1 +$TMUX new-session -d -s aaa -x 80 -y 24 'cat' || exit 1 $TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1 wait_clients 1 || fail "no client attached to test server" @@ -145,10 +145,12 @@ wait_clients 1 || fail "no client attached to test server" # regression - its two-pane window 0. aaa contributes exactly three lines # (session, window, pane). $TMUX choose-tree -t aaa:0 -F 'F1' -f '#{==:#{session_name},aaa}' || exit 1 -wait_count 'F1' 3 +wait_count ': F1' 3 out=$CAPTURED -echo "$out" | grep -q 'aaa: F1' || fail "aaa missing when filter matches it" -echo "$out" | grep -q 'zzz: F1' && fail "zzz shown but no pane matches" +printf '%s\n' "$out" | grep -F -q 'aaa: F1' || \ + fail "aaa missing when filter matches it" +printf '%s\n' "$out" | grep -F -q 'zzz: F1' && \ + fail "zzz shown but no pane matches" exit_mode q # --- filter keeping only zzz ------------------------------------------------ @@ -156,10 +158,12 @@ exit_mode q # zzz contributes six lines (session, two windows, three panes); aaa must # disappear. $TMUX choose-tree -t aaa:0 -F 'F2' -f '#{==:#{session_name},zzz}' || exit 1 -wait_count 'F2' 6 +wait_count ': F2' 6 out=$CAPTURED -echo "$out" | grep -q 'zzz: F2' || fail "zzz missing when filter matches it" -echo "$out" | grep -q 'aaa: F2' && fail "aaa shown but no pane matches" +printf '%s\n' "$out" | grep -F -q 'zzz: F2' || \ + fail "zzz missing when filter matches it" +printf '%s\n' "$out" | grep -F -q 'aaa: F2' && \ + fail "aaa shown but no pane matches" exit_mode q # --- filter matching a single pane ------------------------------------------ @@ -167,22 +171,28 @@ exit_mode q # Only pane 1 of zzz:0 matches, so the tree is exactly session zzz, window 0 # and that pane; zzz:1 and all of aaa must disappear. $TMUX choose-tree -t aaa:0 -F 'F3' -f '#{==:#{pane_index},1}' || exit 1 -wait_count 'F3' 3 +wait_count ': F3' 3 out=$CAPTURED -echo "$out" | grep -q 'zzz: F3' || fail "zzz missing when its pane matches" -echo "$out" | grep -q 'aaa: F3' && fail "aaa shown but no pane matches" -echo "$out" | grep -q '1: F3' || fail "matching pane missing" +printf '%s\n' "$out" | grep -F -q 'zzz: F3' || \ + fail "zzz missing when its pane matches" +printf '%s\n' "$out" | grep -F -q 'aaa: F3' && \ + fail "aaa shown but no pane matches" +printf '%s\n' "$out" | grep -F -q '1: F3' || \ + fail "matching pane missing" exit_mode q # --- filter matching nothing ------------------------------------------------ # # Everything is shown and the filter indicator reports no matches. $TMUX choose-tree -t aaa:0 -F 'F4' -f '#{==:#{session_name},nosuch}' || exit 1 -wait_count 'F4' 9 +wait_count ': F4' 9 out=$CAPTURED -echo "$out" | grep -q 'aaa: F4' || fail "aaa missing with no-match filter" -echo "$out" | grep -q 'zzz: F4' || fail "zzz missing with no-match filter" -echo "$out" | grep -q 'no matches' || fail "no matches indicator missing" +printf '%s\n' "$out" | grep -F -q 'aaa: F4' || \ + fail "aaa missing with no-match filter" +printf '%s\n' "$out" | grep -F -q 'zzz: F4' || \ + fail "zzz missing with no-match filter" +printf '%s\n' "$out" | grep -F -q 'no matches' || \ + fail "no matches indicator missing" exit_mode q # --- -h with the tree pane as the only match -------------------------------- @@ -191,28 +201,32 @@ exit_mode q # match, so session and window aaa stay listed: two lines, no pane line. $TMUX choose-tree -h -t aaa:0 -F 'F5' -f '#{==:#{session_name},aaa}' || \ exit 1 -wait_count 'F5' 2 -echo "$CAPTURED" | grep -q 'aaa: F5' || fail "aaa missing with -h" +wait_count ': F5' 2 +printf '%s\n' "$CAPTURED" | grep -F -q 'aaa: F5' || \ + fail "aaa missing with -h" exit_mode q # --- sort orders ------------------------------------------------------------ # # By index zzz (created first) sorts first, by name aaa does, and -r reverses. $TMUX choose-tree -t aaa:0 -F 'F6' -O index || exit 1 -wait_count 'F6' 9 -echo "$CAPTURED" | grep 'F6' | head -1 | grep -q 'zzz: F6' || \ +wait_count ': F6' 9 +printf '%s\n' "$CAPTURED" | grep -F ': F6' | head -1 | \ + grep -F -q 'zzz: F6' || \ fail "zzz not first with -O index" exit_mode q $TMUX choose-tree -t aaa:0 -F 'F7' -O name || exit 1 -wait_count 'F7' 9 -echo "$CAPTURED" | grep 'F7' | head -1 | grep -q 'aaa: F7' || \ +wait_count ': F7' 9 +printf '%s\n' "$CAPTURED" | grep -F ': F7' | head -1 | \ + grep -F -q 'aaa: F7' || \ fail "aaa not first with -O name" exit_mode q $TMUX choose-tree -t aaa:0 -F 'F8' -O name -r || exit 1 -wait_count 'F8' 9 -echo "$CAPTURED" | grep 'F8' | head -1 | grep -q 'zzz: F8' || \ +wait_count ': F8' 9 +printf '%s\n' "$CAPTURED" | grep -F ': F8' | head -1 | \ + grep -F -q 'zzz: F8' || \ fail "zzz not first with -O name -r" exit_mode q @@ -221,33 +235,35 @@ exit_mode q # g moves to the top (session zzz); h collapses it, hiding its five children; # l expands it again. $TMUX choose-tree -t aaa:0 -F 'G1' -O index || exit 1 -wait_count 'G1' 9 +wait_count ': G1' 9 $TMUX send-keys -t aaa:0 g h || fail "send-keys collapse failed" -wait_count 'G1' 4 +wait_count ': G1' 4 $TMUX send-keys -t aaa:0 l || fail "send-keys expand failed" -wait_count 'G1' 9 +wait_count ': G1' 9 exit_mode q # --- filter entered at the prompt with f, cleared with c ---------------------- $TMUX choose-tree -t aaa:0 -F 'G2' -O index || exit 1 -wait_count 'G2' 9 +wait_count ': G2' 9 $TMUX send-keys -t aaa:0 f || fail "send-keys f failed" $TMUX send-keys -t aaa:0 -l '#{==:#{session_name},aaa}' || \ fail "send-keys filter failed" $TMUX send-keys -t aaa:0 Enter || fail "send-keys Enter failed" -wait_count 'G2' 3 +wait_count ': G2' 3 out=$CAPTURED -echo "$out" | grep -q 'aaa: G2' || fail "aaa missing with prompt filter" -echo "$out" | grep -q 'zzz: G2' && fail "zzz shown with prompt filter" +printf '%s\n' "$out" | grep -F -q 'aaa: G2' || \ + fail "aaa missing with prompt filter" +printf '%s\n' "$out" | grep -F -q 'zzz: G2' && \ + fail "zzz shown with prompt filter" $TMUX send-keys -t aaa:0 c || fail "send-keys c failed" -wait_count 'G2' 9 +wait_count ': G2' 9 exit_mode q # --- Enter runs the default command (switch-client) ---------------------------- # # g selects session zzz and Enter switches the client to it. $TMUX choose-tree -t aaa:0 -F 'G3' -O index || exit 1 -wait_count 'G3' 9 +wait_count ': G3' 9 $TMUX send-keys -t aaa:0 g Enter || fail "send-keys Enter failed" i=0 while [ "$i" -lt 50 ]; do @@ -265,11 +281,11 @@ wait_mode aaa:0 0 # g and four times j select window 1 of zzz; x asks for confirmation and y # kills it, leaving zzz with one window and the tree with seven lines. $TMUX choose-tree -t aaa:0 -F 'G4' -O index || exit 1 -wait_count 'G4' 9 +wait_count ': G4' 9 $TMUX send-keys -t aaa:0 g j j j j x || fail "send-keys x failed" wait_for 'Kill window 1' $TMUX send-keys -t aaa:0 y || fail "send-keys y failed" -wait_count 'G4' 7 +wait_count ': G4' 7 [ "$($TMUX list-windows -t zzz -F x | grep -c x)" -eq 1 ] || \ fail "window 1 of zzz not killed" exit_mode q diff --git a/regress/format-render-contexts.sh b/regress/format-render-contexts.sh index a91fb88e1..de68176e0 100644 --- a/regress/format-render-contexts.sh +++ b/regress/format-render-contexts.sh @@ -49,7 +49,13 @@ tmux_run() name=$1 shift - out=$(run_cmd $TMUX "$@" 2>&1) + out=$( + if command -v timeout >/dev/null 2>&1; then + timeout 10 $TMUX "$@" 2>&1 + else + $TMUX "$@" 2>&1 + fi + ) rc=$? bounded "$name" "$out" [ "$rc" -eq 0 ] || fail "$name failed: $out" @@ -58,7 +64,13 @@ tmux_run() capture() { - out=$(run_cmd $TMUX2 capture-pane -p -t out:0 2>/dev/null) + out=$( + if command -v timeout >/dev/null 2>&1; then + timeout 10 $TMUX2 capture-pane -p -t out:0 2>/dev/null + else + $TMUX2 capture-pane -p -t out:0 2>/dev/null + fi + ) rc=$? bounded "capture-pane" "$out" [ "$rc" -eq 0 ] || fail "capture-pane failed" @@ -67,7 +79,14 @@ capture() capture_esc() { - out=$(run_cmd $TMUX2 capture-pane -pe -t out:0 2>/dev/null) + out=$( + if command -v timeout >/dev/null 2>&1; then + timeout 10 $TMUX2 capture-pane -pe -t out:0 \ + 2>/dev/null + else + $TMUX2 capture-pane -pe -t out:0 2>/dev/null + fi + ) rc=$? bounded "capture-pane -e" "$out" [ "$rc" -eq 0 ] || fail "capture-pane -e failed" diff --git a/regress/mode-mutation.sh b/regress/mode-mutation.sh index cab8e72c8..cc73d426d 100644 --- a/regress/mode-mutation.sh +++ b/regress/mode-mutation.sh @@ -8,20 +8,30 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMP=$(mktemp -d) || exit 1 +TMUX_TMPDIR="$TMP" +export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -Lmode-mutation-$$ -f/dev/null" +TMUX2="$TEST_TMUX -Lmode-mutation-outer-$$ -f/dev/null" -cleanup() +cleanup_servers() { $TMUX kill-server 2>/dev/null $TMUX2 kill-server 2>/dev/null sleep 0.5 } +cleanup() +{ + cleanup_servers + rm -rf "$TMP" +} +trap cleanup EXIT + fail() { - echo "$1" + echo "$1" >&2 cleanup exit 1 } @@ -53,7 +63,7 @@ wait_for() { i=0 while [ "$i" -lt 50 ]; do - capture | grep -q "$1" && return 0 + capture | grep -F -q "$1" && return 0 sleep 0.2 i=$((i + 1)) done @@ -95,7 +105,7 @@ start_client() s=$1 cmd=${2:-cat} - cleanup + cleanup_servers $TMUX new-session -d -s "$s" -n main -x 80 -y 24 "$cmd" || \ fail "$s: new-session failed" $TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t $s" || \ @@ -287,7 +297,7 @@ test_copy_mode() assert_alive "copy-mode exit" } -cleanup +cleanup_servers test_choose_tree test_choose_buffer test_choose_client From eb82e8527acfe58628928e6bdecc94d32137c840 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 6 Jul 2026 11:50:58 +0100 Subject: [PATCH 064/127] Shorten all socket names because macOS is stupid. --- regress/alerts.sh | 2 +- regress/am-terminal.sh | 4 ++-- regress/border-arrows.sh | 4 ++-- regress/buffers.sh | 2 +- regress/capture-pane-hyperlink.sh | 2 +- regress/capture-pane-sgr0.sh | 2 +- regress/check-names.sh | 2 +- regress/choose-buffer.sh | 4 ++-- regress/choose-client.sh | 4 ++-- regress/choose-tree.sh | 4 ++-- regress/combine-test.sh | 2 +- regress/command-alias.sh | 2 +- regress/command-order.sh | 2 +- regress/conf-syntax.sh | 2 +- regress/control-client-sanity.sh | 2 +- regress/control-client-size.sh | 2 +- regress/control-subscriptions.sh | 2 +- regress/copy-mode-test-emacs.sh | 2 +- regress/copy-mode-test-vi.sh | 2 +- regress/cursor-test1.sh | 2 +- regress/cursor-test2.sh | 2 +- regress/cursor-test3.sh | 2 +- regress/cursor-test4.sh | 2 +- regress/decrqm-sync.sh | 2 +- regress/environ-update.sh | 4 ++-- regress/environ.sh | 4 ++-- regress/floating-pane-geometry.sh | 2 +- regress/format-fuzzy.sh | 2 +- regress/format-modifiers.sh | 2 +- regress/format-mouse.sh | 4 ++-- regress/format-render-contexts.sh | 4 ++-- regress/format-strings.sh | 2 +- regress/format-variables.sh | 4 ++-- regress/has-session-return.sh | 2 +- regress/hooks-after.sh | 2 +- regress/hooks-lifecycle.sh | 2 +- regress/hooks-notify.sh | 2 +- regress/hooks.sh | 2 +- regress/if-shell-TERM.sh | 2 +- regress/if-shell-error.sh | 2 +- regress/if-shell-nested.sh | 2 +- regress/input-common.inc | 2 +- regress/input-keys.sh | 2 +- regress/input-reflow-stress.sh | 4 ++-- regress/input-replies.sh | 2 +- regress/input-requests.sh | 5 +++-- regress/kill-session-process-exit.sh | 2 +- regress/lifecycle-deferred.sh | 4 ++-- regress/mode-mutation.sh | 4 ++-- regress/new-session-base-index.sh | 2 +- regress/new-session-command.sh | 2 +- regress/new-session-environment.sh | 2 +- regress/new-session-no-client.sh | 2 +- regress/new-session-size.sh | 2 +- regress/new-window-command.sh | 2 +- regress/options-array.sh | 2 +- regress/options-scope.sh | 2 +- regress/options-values.sh | 2 +- regress/osc-11colours.sh | 2 +- regress/pane-ops.sh | 2 +- regress/prompt-keys.sh | 4 ++-- regress/prompt-mechanics.sh | 4 ++-- regress/run-shell-output.sh | 2 +- regress/screen-redraw-bidi.sh | 4 ++-- regress/screen-redraw-cache.sh | 4 ++-- regress/screen-redraw-floating.sh | 4 ++-- regress/screen-redraw-indicators.sh | 4 ++-- regress/screen-redraw-outside.sh | 4 ++-- regress/screen-redraw-popups.sh | 4 ++-- regress/screen-redraw-scrollbars.sh | 4 ++-- regress/screen-redraw-status.sh | 4 ++-- regress/screen-redraw-tiled.sh | 4 ++-- regress/screen-redraw-window-style.sh | 4 ++-- regress/session-group-resize.sh | 2 +- regress/session-ops.sh | 2 +- regress/set-hook-B.sh | 2 +- regress/set-hook-R.sh | 2 +- regress/style-trim.sh | 4 ++-- regress/targets-panes.sh | 2 +- regress/targets.sh | 2 +- regress/tty-draw-line.sh | 4 ++-- regress/tty-keys.sh | 4 ++-- regress/utf8-test.sh | 2 +- regress/window-ops.sh | 2 +- 84 files changed, 114 insertions(+), 113 deletions(-) diff --git a/regress/alerts.sh b/regress/alerts.sh index 7a39ddefc..cfb84c711 100644 --- a/regress/alerts.sh +++ b/regress/alerts.sh @@ -19,7 +19,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" fail() { diff --git a/regress/am-terminal.sh b/regress/am-terminal.sh index bf1cabfe7..fae0e502d 100644 --- a/regress/am-terminal.sh +++ b/regress/am-terminal.sh @@ -4,9 +4,9 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" $TMUX2 kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/border-arrows.sh b/regress/border-arrows.sh index ea571b05a..a67171bf5 100644 --- a/regress/border-arrows.sh +++ b/regress/border-arrows.sh @@ -11,9 +11,9 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null -TMUX_OUTER="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX_OUTER="$TEST_TMUX -LtestB$$ -f/dev/null" $TMUX_OUTER kill-server 2>/dev/null trap "$TMUX kill-server 2>/dev/null; $TMUX_OUTER kill-server 2>/dev/null" 0 1 15 diff --git a/regress/buffers.sh b/regress/buffers.sh index 0b3267610..fdacacb5b 100644 --- a/regress/buffers.sh +++ b/regress/buffers.sh @@ -23,7 +23,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/capture-pane-hyperlink.sh b/regress/capture-pane-hyperlink.sh index 80092e7f2..ec89c6c5d 100644 --- a/regress/capture-pane-hyperlink.sh +++ b/regress/capture-pane-hyperlink.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" TMP=$(mktemp) TMP2=$(mktemp) trap "rm -f $TMP $TMP2" 0 1 15 diff --git a/regress/capture-pane-sgr0.sh b/regress/capture-pane-sgr0.sh index 7c9c32315..1e73e7191 100644 --- a/regress/capture-pane-sgr0.sh +++ b/regress/capture-pane-sgr0.sh @@ -7,7 +7,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/check-names.sh b/regress/check-names.sh index 886299f2d..be8ad617b 100644 --- a/regress/check-names.sh +++ b/regress/check-names.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null fail() diff --git a/regress/choose-buffer.sh b/regress/choose-buffer.sh index 60f15478f..ca9a26758 100644 --- a/regress/choose-buffer.sh +++ b/regress/choose-buffer.sh @@ -20,8 +20,8 @@ TERM=screen TMP=$(mktemp -d) || exit 1 TMUX_TMPDIR="$TMP" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" cleanup() { diff --git a/regress/choose-client.sh b/regress/choose-client.sh index 621cb608f..d325f5335 100644 --- a/regress/choose-client.sh +++ b/regress/choose-client.sh @@ -19,8 +19,8 @@ TERM=screen TMP=$(mktemp -d) || exit 1 TMUX_TMPDIR="$TMP" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" cleanup() { diff --git a/regress/choose-tree.sh b/regress/choose-tree.sh index 8346b4cae..132890730 100644 --- a/regress/choose-tree.sh +++ b/regress/choose-tree.sh @@ -27,8 +27,8 @@ TERM=screen TMP=$(mktemp -d) || exit 1 TMUX_TMPDIR="$TMP" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" cleanup() { diff --git a/regress/combine-test.sh b/regress/combine-test.sh index a4d4f3681..ac9f800b0 100644 --- a/regress/combine-test.sh +++ b/regress/combine-test.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/command-alias.sh b/regress/command-alias.sh index 632e959a2..aae434cc0 100644 --- a/regress/command-alias.sh +++ b/regress/command-alias.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null $TMUX new-session -d -sfoo || exit 1 diff --git a/regress/command-order.sh b/regress/command-order.sh index 775768a6a..9c957246a 100644 --- a/regress/command-order.sh +++ b/regress/command-order.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/conf-syntax.sh b/regress/conf-syntax.sh index 49b9710b3..4dbc384b3 100644 --- a/regress/conf-syntax.sh +++ b/regress/conf-syntax.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null for i in conf/*.conf; do diff --git a/regress/control-client-sanity.sh b/regress/control-client-sanity.sh index f6c36e1bd..32695e7c0 100644 --- a/regress/control-client-sanity.sh +++ b/regress/control-client-sanity.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/control-client-size.sh b/regress/control-client-size.sh index 7d5e1dc27..e6742608a 100644 --- a/regress/control-client-size.sh +++ b/regress/control-client-size.sh @@ -8,7 +8,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/control-subscriptions.sh b/regress/control-subscriptions.sh index f720b0650..8aebb2ea0 100644 --- a/regress/control-subscriptions.sh +++ b/regress/control-subscriptions.sh @@ -7,7 +7,7 @@ LANG=C.UTF-8 export TERM LC_ALL LANG [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" TMPDIR=$(mktemp -d) IN="$TMPDIR/in" diff --git a/regress/copy-mode-test-emacs.sh b/regress/copy-mode-test-emacs.sh index a5b69c675..3e9cbf50b 100644 --- a/regress/copy-mode-test-emacs.sh +++ b/regress/copy-mode-test-emacs.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null $TMUX new -d -x40 -y10 \ diff --git a/regress/copy-mode-test-vi.sh b/regress/copy-mode-test-vi.sh index 2d6bfc287..71810b006 100644 --- a/regress/copy-mode-test-vi.sh +++ b/regress/copy-mode-test-vi.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -f/dev/null -Ltest" +TMUX="$TEST_TMUX -f/dev/null -LtestA$$" $TMUX kill-server 2>/dev/null $TMUX new -d -x40 -y10 \ diff --git a/regress/cursor-test1.sh b/regress/cursor-test1.sh index 73a336be7..b098cdc15 100644 --- a/regress/cursor-test1.sh +++ b/regress/cursor-test1.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/cursor-test2.sh b/regress/cursor-test2.sh index 3de80fdac..8e9e7ec80 100644 --- a/regress/cursor-test2.sh +++ b/regress/cursor-test2.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/cursor-test3.sh b/regress/cursor-test3.sh index e4881c79a..213ed83ba 100644 --- a/regress/cursor-test3.sh +++ b/regress/cursor-test3.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/cursor-test4.sh b/regress/cursor-test4.sh index 8f4dfa5c3..0eb5f0c9a 100644 --- a/regress/cursor-test4.sh +++ b/regress/cursor-test4.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/decrqm-sync.sh b/regress/decrqm-sync.sh index 7f8c3f61d..f4daf36a9 100644 --- a/regress/decrqm-sync.sh +++ b/regress/decrqm-sync.sh @@ -9,7 +9,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null sleep 1 diff --git a/regress/environ-update.sh b/regress/environ-update.sh index 59f7d74c5..44e4e72c4 100644 --- a/regress/environ-update.sh +++ b/regress/environ-update.sh @@ -17,9 +17,9 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" # A second server on its own socket hosts the pane that runs the inner client. -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" cleanup() { diff --git a/regress/environ.sh b/regress/environ.sh index 244ec367d..2d5c0c031 100644 --- a/regress/environ.sh +++ b/regress/environ.sh @@ -19,7 +19,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null # check_value $args $expected @@ -151,7 +151,7 @@ cat > "$CONF" </dev/null $CTMUX new-session -d -s c -x 80 -y 24 || { rm -f "$CONF"; exit 1; } out=$($CTMUX show-environment -g CFGVAR 2>&1) diff --git a/regress/floating-pane-geometry.sh b/regress/floating-pane-geometry.sh index 7bd033ff6..7e80e8942 100644 --- a/regress/floating-pane-geometry.sh +++ b/regress/floating-pane-geometry.sh @@ -15,7 +15,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null fail() diff --git a/regress/format-fuzzy.sh b/regress/format-fuzzy.sh index 31a5b60cb..00bc28bd4 100644 --- a/regress/format-fuzzy.sh +++ b/regress/format-fuzzy.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" # test_format $format $expected_result test_format() diff --git a/regress/format-modifiers.sh b/regress/format-modifiers.sh index 21203b6a9..b6340b622 100644 --- a/regress/format-modifiers.sh +++ b/regress/format-modifiers.sh @@ -17,7 +17,7 @@ LC_ALL=C.UTF-8 export TZ LANG LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" ESC=$(printf '\033') diff --git a/regress/format-mouse.sh b/regress/format-mouse.sh index 8032f8a57..9a443d19c 100644 --- a/regress/format-mouse.sh +++ b/regress/format-mouse.sh @@ -18,8 +18,8 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" cleanup() { diff --git a/regress/format-render-contexts.sh b/regress/format-render-contexts.sh index de68176e0..2cceff2e3 100644 --- a/regress/format-render-contexts.sh +++ b/regress/format-render-contexts.sh @@ -9,8 +9,8 @@ export PATH TERM LANG LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Lformat-render-contexts-$$ -f/dev/null" -TMUX2="$TEST_TMUX -Lformat-render-contexts-outer-$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" LIMIT=20000 cleanup() diff --git a/regress/format-strings.sh b/regress/format-strings.sh index bae66ab00..877e18fec 100644 --- a/regress/format-strings.sh +++ b/regress/format-strings.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" # test_format $format $expected_result test_format() diff --git a/regress/format-variables.sh b/regress/format-variables.sh index fa51e0216..9131c2cf4 100644 --- a/regress/format-variables.sh +++ b/regress/format-variables.sh @@ -15,10 +15,10 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" # A second server on its own socket provides a real terminal (an inner client # attached inside one of its panes) so client terminal variables are populated. -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" # Every variable name in format_table[]. Kept as a plain word list so it can be # iterated with normal shell word splitting. diff --git a/regress/has-session-return.sh b/regress/has-session-return.sh index a7d76ad85..8b259ac76 100644 --- a/regress/has-session-return.sh +++ b/regress/has-session-return.sh @@ -7,7 +7,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null $TMUX -f/dev/null has -tfoo /dev/null && exit 1 diff --git a/regress/hooks-after.sh b/regress/hooks-after.sh index ee7fbfe0e..37934db8d 100644 --- a/regress/hooks-after.sh +++ b/regress/hooks-after.sh @@ -10,7 +10,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" fail() { diff --git a/regress/hooks-lifecycle.sh b/regress/hooks-lifecycle.sh index 6fb271131..e90f7ba3a 100644 --- a/regress/hooks-lifecycle.sh +++ b/regress/hooks-lifecycle.sh @@ -15,7 +15,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" fail() { diff --git a/regress/hooks-notify.sh b/regress/hooks-notify.sh index 947dd6c68..41916248c 100644 --- a/regress/hooks-notify.sh +++ b/regress/hooks-notify.sh @@ -10,7 +10,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" fail() { diff --git a/regress/hooks.sh b/regress/hooks.sh index 537582295..e1d75bf67 100644 --- a/regress/hooks.sh +++ b/regress/hooks.sh @@ -10,7 +10,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" fail() { diff --git a/regress/if-shell-TERM.sh b/regress/if-shell-TERM.sh index 21846fd10..d1db52f45 100644 --- a/regress/if-shell-TERM.sh +++ b/regress/if-shell-TERM.sh @@ -7,7 +7,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/if-shell-error.sh b/regress/if-shell-error.sh index 2eab8f7ca..3b2dac753 100644 --- a/regress/if-shell-error.sh +++ b/regress/if-shell-error.sh @@ -7,7 +7,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/if-shell-nested.sh b/regress/if-shell-nested.sh index 434775c43..e10be41c6 100644 --- a/regress/if-shell-nested.sh +++ b/regress/if-shell-nested.sh @@ -7,7 +7,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/input-common.inc b/regress/input-common.inc index 96eec1f09..100a7be0f 100644 --- a/regress/input-common.inc +++ b/regress/input-common.inc @@ -2,7 +2,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" TMP=$(mktemp) EXP=$(mktemp) diff --git a/regress/input-keys.sh b/regress/input-keys.sh index e2fc41f54..cb771d19a 100644 --- a/regress/input-keys.sh +++ b/regress/input-keys.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null sleep 1 $TMUX -f/dev/null new -x20 -y2 -d \; set -g escape-time 0 || exit 1 diff --git a/regress/input-reflow-stress.sh b/regress/input-reflow-stress.sh index aa1bbd717..926e8cdd0 100644 --- a/regress/input-reflow-stress.sh +++ b/regress/input-reflow-stress.sh @@ -8,8 +8,8 @@ export PATH TERM LANG LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Linput-reflow-stress-$$ -f/dev/null" -TMUX2="$TEST_TMUX -Linput-reflow-stress-outer-$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" TMP=$(mktemp "${TMPDIR:-/tmp}/input-reflow-stress.XXXXXX") || exit 1 EXP=$(mktemp "${TMPDIR:-/tmp}/input-reflow-stress.XXXXXX") || exit 1 diff --git a/regress/input-replies.sh b/regress/input-replies.sh index 3435cd943..5fc77f843 100644 --- a/regress/input-replies.sh +++ b/regress/input-replies.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null sleep 0.5 diff --git a/regress/input-requests.sh b/regress/input-requests.sh index 5e7b3781a..094ce2a8a 100644 --- a/regress/input-requests.sh +++ b/regress/input-requests.sh @@ -15,7 +15,8 @@ import tempfile import time tmux = sys.argv[1] -server = [tmux, "-Ltest", "-f/dev/null"] +label = "testA%d" % os.getpid() +server = [tmux, "-L" + label, "-f/dev/null"] def run(*args, check=True): return subprocess.run(server + list(args), check=check, @@ -25,7 +26,7 @@ def attach(): pid, fd = os.forkpty() if pid == 0: os.environ["TERM"] = "xterm-256color" - os.execl(tmux, tmux, "-Ltest", "-f/dev/null", "attach-session", + os.execl(tmux, tmux, "-L" + label, "-f/dev/null", "attach-session", "-t", "requests") os.set_blocking(fd, False) return pid, fd diff --git a/regress/kill-session-process-exit.sh b/regress/kill-session-process-exit.sh index 04617ca2c..405449be2 100644 --- a/regress/kill-session-process-exit.sh +++ b/regress/kill-session-process-exit.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null sleep 1 diff --git a/regress/lifecycle-deferred.sh b/regress/lifecycle-deferred.sh index 2650f1827..d2669ff46 100644 --- a/regress/lifecycle-deferred.sh +++ b/regress/lifecycle-deferred.sh @@ -8,8 +8,8 @@ export TERM LC_ALL LANG [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Llifecycle-deferred -f/dev/null" -TMUX2="$TEST_TMUX -Llifecycle-deferred-outer -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" TMPDIR=$(mktemp -d) IN="$TMPDIR/in" diff --git a/regress/mode-mutation.sh b/regress/mode-mutation.sh index cc73d426d..96927e995 100644 --- a/regress/mode-mutation.sh +++ b/regress/mode-mutation.sh @@ -12,8 +12,8 @@ TMP=$(mktemp -d) || exit 1 TMUX_TMPDIR="$TMP" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Lmode-mutation-$$ -f/dev/null" -TMUX2="$TEST_TMUX -Lmode-mutation-outer-$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" cleanup_servers() { diff --git a/regress/new-session-base-index.sh b/regress/new-session-base-index.sh index bb6dd5945..89fdb4de5 100644 --- a/regress/new-session-base-index.sh +++ b/regress/new-session-base-index.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/new-session-command.sh b/regress/new-session-command.sh index b2fc91db7..520c91e4f 100644 --- a/regress/new-session-command.sh +++ b/regress/new-session-command.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/new-session-environment.sh b/regress/new-session-environment.sh index e5404e67d..fc935687f 100644 --- a/regress/new-session-environment.sh +++ b/regress/new-session-environment.sh @@ -5,7 +5,7 @@ PATH=/bin:/usr/bin [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TERM=$($TMUX start \; show -gv default-terminal) diff --git a/regress/new-session-no-client.sh b/regress/new-session-no-client.sh index 84fddaecb..2f3da5abe 100644 --- a/regress/new-session-no-client.sh +++ b/regress/new-session-no-client.sh @@ -8,7 +8,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/new-session-size.sh b/regress/new-session-size.sh index 029bd6eed..6a35abbbd 100644 --- a/regress/new-session-size.sh +++ b/regress/new-session-size.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/new-window-command.sh b/regress/new-window-command.sh index 183491159..21a2661e8 100644 --- a/regress/new-window-command.sh +++ b/regress/new-window-command.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/options-array.sh b/regress/options-array.sh index 3b18ce977..c372e3461 100644 --- a/regress/options-array.sh +++ b/regress/options-array.sh @@ -20,7 +20,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null check_value() diff --git a/regress/options-scope.sh b/regress/options-scope.sh index bbc7168aa..ac6d62f77 100644 --- a/regress/options-scope.sh +++ b/regress/options-scope.sh @@ -17,7 +17,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null # check_value $args $expected diff --git a/regress/options-values.sh b/regress/options-values.sh index 28acd2b47..75da52e01 100644 --- a/regress/options-values.sh +++ b/regress/options-values.sh @@ -17,7 +17,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null check_value() diff --git a/regress/osc-11colours.sh b/regress/osc-11colours.sh index 2fba76d82..5cdc1be41 100644 --- a/regress/osc-11colours.sh +++ b/regress/osc-11colours.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null $TMUX new -d diff --git a/regress/pane-ops.sh b/regress/pane-ops.sh index 46984bc27..762bea200 100644 --- a/regress/pane-ops.sh +++ b/regress/pane-ops.sh @@ -30,7 +30,7 @@ LC_ALL=C.UTF-8 export TERM LANG LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null # check_ok $cmd... diff --git a/regress/prompt-keys.sh b/regress/prompt-keys.sh index 8c17581b1..6fde51b7b 100644 --- a/regress/prompt-keys.sh +++ b/regress/prompt-keys.sh @@ -7,8 +7,8 @@ LANG=C.UTF-8 export TERM LC_ALL LANG [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -OUT="$TEST_TMUX -Ltest -f/dev/null" # outer (host for the client) -IN="$TEST_TMUX -Ltest2 -f/dev/null" # inner (under test) +OUT="$TEST_TMUX -LtestA$$ -f/dev/null" # outer (host for the client) +IN="$TEST_TMUX -LtestB$$ -f/dev/null" # inner (under test) $OUT kill-server 2>/dev/null $IN kill-server 2>/dev/null diff --git a/regress/prompt-mechanics.sh b/regress/prompt-mechanics.sh index 6f3e5294e..55bd17a69 100644 --- a/regress/prompt-mechanics.sh +++ b/regress/prompt-mechanics.sh @@ -20,8 +20,8 @@ LANG=C.UTF-8 export TERM LC_ALL LANG [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -OUT="$TEST_TMUX -Ltest -f/dev/null" # outer (host for the client) -IN="$TEST_TMUX -Ltest2 -f/dev/null" # inner (under test) +OUT="$TEST_TMUX -LtestA$$ -f/dev/null" # outer (host for the client) +IN="$TEST_TMUX -LtestB$$ -f/dev/null" # inner (under test) $OUT kill-server 2>/dev/null $IN kill-server 2>/dev/null diff --git a/regress/run-shell-output.sh b/regress/run-shell-output.sh index 3b44504c2..926a520c9 100644 --- a/regress/run-shell-output.sh +++ b/regress/run-shell-output.sh @@ -7,7 +7,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/screen-redraw-bidi.sh b/regress/screen-redraw-bidi.sh index e4e962950..ee9c2fa16 100644 --- a/regress/screen-redraw-bidi.sh +++ b/regress/screen-redraw-bidi.sh @@ -17,8 +17,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-cache.sh b/regress/screen-redraw-cache.sh index 27c98e3b4..0e0abf9db 100644 --- a/regress/screen-redraw-cache.sh +++ b/regress/screen-redraw-cache.sh @@ -18,8 +18,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-floating.sh b/regress/screen-redraw-floating.sh index 82c36d364..67d213699 100644 --- a/regress/screen-redraw-floating.sh +++ b/regress/screen-redraw-floating.sh @@ -16,8 +16,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-indicators.sh b/regress/screen-redraw-indicators.sh index 60bf652b2..28a1dee6c 100644 --- a/regress/screen-redraw-indicators.sh +++ b/regress/screen-redraw-indicators.sh @@ -18,8 +18,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-outside.sh b/regress/screen-redraw-outside.sh index 53a2a72b1..3eeb94938 100644 --- a/regress/screen-redraw-outside.sh +++ b/regress/screen-redraw-outside.sh @@ -18,8 +18,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-popups.sh b/regress/screen-redraw-popups.sh index 1eb7e5897..8ad3ba2eb 100644 --- a/regress/screen-redraw-popups.sh +++ b/regress/screen-redraw-popups.sh @@ -17,8 +17,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-scrollbars.sh b/regress/screen-redraw-scrollbars.sh index f8298b5e1..914fe542c 100644 --- a/regress/screen-redraw-scrollbars.sh +++ b/regress/screen-redraw-scrollbars.sh @@ -16,8 +16,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-status.sh b/regress/screen-redraw-status.sh index 12ca20bb1..46cb5fcad 100644 --- a/regress/screen-redraw-status.sh +++ b/regress/screen-redraw-status.sh @@ -20,8 +20,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-tiled.sh b/regress/screen-redraw-tiled.sh index 94353f5f8..3b32478f0 100644 --- a/regress/screen-redraw-tiled.sh +++ b/regress/screen-redraw-tiled.sh @@ -24,8 +24,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-window-style.sh b/regress/screen-redraw-window-style.sh index 1d2b2b281..69ac824f2 100644 --- a/regress/screen-redraw-window-style.sh +++ b/regress/screen-redraw-window-style.sh @@ -13,8 +13,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/session-group-resize.sh b/regress/session-group-resize.sh index 28e7a85ed..f2b634a43 100644 --- a/regress/session-group-resize.sh +++ b/regress/session-group-resize.sh @@ -10,7 +10,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP1=$(mktemp) diff --git a/regress/session-ops.sh b/regress/session-ops.sh index 4938ee22c..08c5dfbae 100644 --- a/regress/session-ops.sh +++ b/regress/session-ops.sh @@ -27,7 +27,7 @@ LC_ALL=C.UTF-8 export TERM LANG LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null # check_ok $cmd... diff --git a/regress/set-hook-B.sh b/regress/set-hook-B.sh index 188bfdd24..4947af70c 100755 --- a/regress/set-hook-B.sh +++ b/regress/set-hook-B.sh @@ -10,7 +10,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" fail() { diff --git a/regress/set-hook-R.sh b/regress/set-hook-R.sh index b36edc323..49af68efc 100644 --- a/regress/set-hook-R.sh +++ b/regress/set-hook-R.sh @@ -10,7 +10,7 @@ export TERM LC_ALL LANG OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR -TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" fail() { diff --git a/regress/style-trim.sh b/regress/style-trim.sh index 5b03482d9..f42e0ffff 100644 --- a/regress/style-trim.sh +++ b/regress/style-trim.sh @@ -17,9 +17,9 @@ if command -v bash >/dev/null 2>&1; then fi [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" $TMUX2 kill-server 2>/dev/null $TMUX2 -f/dev/null new -d "$TMUX -f/dev/null new -- $shell" diff --git a/regress/targets-panes.sh b/regress/targets-panes.sh index db353403b..9ea936173 100644 --- a/regress/targets-panes.sh +++ b/regress/targets-panes.sh @@ -27,7 +27,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null # check $target $expected [format] diff --git a/regress/targets.sh b/regress/targets.sh index 346a86736..75ee3dbb8 100644 --- a/regress/targets.sh +++ b/regress/targets.sh @@ -29,7 +29,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null # check $target $expected [format] diff --git a/regress/tty-draw-line.sh b/regress/tty-draw-line.sh index b57de08cc..59331aa0d 100644 --- a/regress/tty-draw-line.sh +++ b/regress/tty-draw-line.sh @@ -10,8 +10,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" fail() { echo "$*" >&2 diff --git a/regress/tty-keys.sh b/regress/tty-keys.sh index 849f6f336..1b300b995 100644 --- a/regress/tty-keys.sh +++ b/regress/tty-keys.sh @@ -4,9 +4,9 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" $TMUX2 kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/utf8-test.sh b/regress/utf8-test.sh index b3cf41bd0..1836cc459 100644 --- a/regress/utf8-test.sh +++ b/regress/utf8-test.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" TMP=$(mktemp) trap "rm -f $TMP" 0 1 15 $TMUX kill-server 2>/dev/null diff --git a/regress/window-ops.sh b/regress/window-ops.sh index dde7fc957..bdb1b5fb9 100644 --- a/regress/window-ops.sh +++ b/regress/window-ops.sh @@ -30,7 +30,7 @@ LC_ALL=C.UTF-8 export TERM LANG LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null # check_ok $cmd... From 699b18414dbd798799c0399a66be832f9ab91de5 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 6 Jul 2026 11:17:28 +0000 Subject: [PATCH 065/127] Do not allow jobs in monitor formats. --- monitor.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/monitor.c b/monitor.c index 5a2408009..4c4a1d976 100644 --- a/monitor.c +++ b/monitor.c @@ -93,6 +93,18 @@ monitor_get_session(struct monitor_set *ms) return (s); } +/* Create a format tree for a subscription. */ +static struct format_tree * +monitor_create_formats(struct client *c, struct session *s, struct winlink *wl, + struct window_pane *wp) +{ + struct format_tree *ft; + + ft = format_create(NULL, NULL, FORMAT_NOJOBS, 0); + format_defaults(ft, c, s, wl, wp); + return (ft); +} + /* Compare subscriptions. */ static int monitor_item_cmp(struct monitor_item *m1, struct monitor_item *m2) @@ -236,7 +248,7 @@ monitor_check_pane(struct monitor_set *ms, struct monitor_item *me) if (wl->session != s) continue; - ft = format_create_defaults(NULL, c, s, wl, wp); + ft = monitor_create_formats(c, s, wl, wp); value = format_expand(ft, me->format); format_free(ft); @@ -314,7 +326,7 @@ monitor_check_window(struct monitor_set *ms, struct monitor_item *me) if (wl->session != s) continue; - ft = format_create_defaults(NULL, c, s, wl, NULL); + ft = monitor_create_formats(c, s, wl, NULL); value = format_expand(ft, me->format); format_free(ft); @@ -382,7 +394,7 @@ monitor_check_sessions(struct monitor_set *ms) struct monitor_item *me, *me1; struct format_tree *ft; - ft = format_create_defaults(NULL, c, s, NULL, NULL); + ft = monitor_create_formats(c, s, NULL, NULL); RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { if (me->type == MONITOR_SESSION) monitor_check_session(ms, me, ft); @@ -427,7 +439,7 @@ monitor_check_all_panes(struct monitor_set *ms) ms->generation = 1; RB_FOREACH(wl, winlinks, &s->windows) { TAILQ_FOREACH(wp, &wl->window->panes, entry) { - ft = format_create_defaults(NULL, c, s, wl, wp); + ft = monitor_create_formats(c, s, wl, wp); RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { if (me->type != MONITOR_ALL_PANES) continue; @@ -455,7 +467,7 @@ monitor_check_all_windows(struct monitor_set *ms) if (++ms->generation == 0) ms->generation = 1; RB_FOREACH(wl, winlinks, &s->windows) { - ft = format_create_defaults(NULL, c, s, wl, NULL); + ft = monitor_create_formats(c, s, wl, NULL); RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { if (me->type != MONITOR_ALL_WINDOWS) continue; From d565fd3b54ad0d2db916c5fd11133802ad441d89 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 6 Jul 2026 11:28:39 +0000 Subject: [PATCH 066/127] Do not create zero size panes, from Uzair Aftab. --- layout.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/layout.c b/layout.c index baa6ebee8..5e48610f9 100644 --- a/layout.c +++ b/layout.c @@ -461,7 +461,8 @@ layout_fix_panes(struct window *w, struct window_pane *skip) layout_add_horizontal_border(w, lc, status)) { if (status == PANE_STATUS_TOP) wp->yoff++; - sy--; + if (sy > 1) + sy--; } if (window_pane_scrollbar_reserve(wp)) { From 85e3ca5ae284505db1961a1aa49cf862eebae460 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 6 Jul 2026 12:08:52 +0000 Subject: [PATCH 067/127] Check for width/height 0, based on change from Uzair Aftab. --- mode-tree.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mode-tree.c b/mode-tree.c index 2a6f9a220..59d9e4fa9 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -834,6 +834,11 @@ mode_tree_draw(struct mode_tree_data *mtd) if (mtd->line_size == 0) return; + w = mtd->width; + h = mtd->height; + if (w == 0 || h == 0) + return; + memcpy(&gc0, &grid_default_cell, sizeof gc0); memcpy(&gc, &grid_default_cell, sizeof gc); style_apply(&gc, oo, "tree-mode-selection-style", NULL); @@ -843,9 +848,6 @@ mode_tree_draw(struct mode_tree_data *mtd) dfg = gc.fg; dfg0 = gc0.fg; - w = mtd->width; - h = mtd->height; - screen_write_start(&ctx, s); screen_write_clearscreen(&ctx, 8); ft = format_create_defaults(NULL, NULL, NULL, NULL, wp); From 79b83600af7181954f15baf3c61279298f350f91 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 6 Jul 2026 12:10:18 +0000 Subject: [PATCH 068/127] NOJOBS is a flag, use correct argument. --- monitor.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitor.c b/monitor.c index 4c4a1d976..6d1a747fd 100644 --- a/monitor.c +++ b/monitor.c @@ -100,7 +100,7 @@ monitor_create_formats(struct client *c, struct session *s, struct winlink *wl, { struct format_tree *ft; - ft = format_create(NULL, NULL, FORMAT_NOJOBS, 0); + ft = format_create(NULL, NULL, 0, FORMAT_NOJOBS); format_defaults(ft, c, s, wl, wp); return (ft); } From 1117caaf09dff7737896cadcee684003c5e74cc6 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 6 Jul 2026 14:29:10 +0000 Subject: [PATCH 069/127] Change array item keys to be strings instead of numbers which are painful to work with. --- cmd-show-options.c | 34 ++++++++++++++++++---------------- colour.c | 14 +++++++------- input.c | 2 +- status.c | 2 +- tmux.1 | 12 ++++++++---- tmux.h | 26 ++++++++++++++------------ tty-keys.c | 11 ++++------- 7 files changed, 53 insertions(+), 48 deletions(-) diff --git a/cmd-show-options.c b/cmd-show-options.c index 0a898156a..bff0634fe 100644 --- a/cmd-show-options.c +++ b/cmd-show-options.c @@ -31,7 +31,7 @@ static enum cmd_retval cmd_show_options_exec(struct cmd *, struct cmdq_item *); static void cmd_show_options_print(struct cmd *, struct cmdq_item *, - struct options_entry *, int, int); + struct options_entry *, const char *, int); static void cmd_show_hooks_print_monitor(struct cmdq_item *, struct options_entry *); static enum cmd_retval cmd_show_hooks_monitor(struct cmd *, struct cmdq_item *, @@ -85,7 +85,8 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) struct cmd_find_state *target = cmdq_get_target(item); struct options *oo; char *argument, *name = NULL, *cause; - int window, idx, ambiguous, parent, scope; + char *array_key = NULL; + int window, ambiguous, parent, scope; struct options_entry *o; window = (cmd_get_entry(self) == &cmd_show_window_options_entry); @@ -107,7 +108,7 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) } argument = format_single_from_target(item, args_string(args, 0)); - name = options_match(argument, &idx, &ambiguous); + name = options_match(argument, &array_key, &ambiguous); if (name == NULL) { if (args_has(args, 'q')) goto out; @@ -137,7 +138,7 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) args_has(args, 'B')) cmd_show_hooks_print_monitor(item, o); else - cmd_show_options_print(self, item, o, idx, parent); + cmd_show_options_print(self, item, o, array_key, parent); } else if (*name == '@') { if (args_has(args, 'q')) @@ -148,26 +149,28 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) out: free(name); + free(array_key); free(argument); return (CMD_RETURN_NORMAL); fail: free(name); + free(array_key); free(argument); return (CMD_RETURN_ERROR); } static void cmd_show_options_print(struct cmd *self, struct cmdq_item *item, - struct options_entry *o, int idx, int parent) + struct options_entry *o, const char *array_key, int parent) { struct args *args = cmd_get_args(self); struct options_array_item *a; const char *name = options_name(o); char *value, *tmp = NULL, *escaped; - if (idx != -1) { - xasprintf(&tmp, "%s[%d]", name, idx); + if (array_key != NULL) { + xasprintf(&tmp, "%s[%s]", name, array_key); name = tmp; } else { if (options_is_array(o)) { @@ -178,8 +181,8 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item, return; } while (a != NULL) { - idx = options_array_item_index(a); - cmd_show_options_print(self, item, o, idx, + array_key = options_array_item_key(a); + cmd_show_options_print(self, item, o, array_key, parent); a = options_array_next(a); } @@ -187,7 +190,7 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item, } } - value = options_to_string(o, idx, 0); + value = options_to_string(o, array_key, 0); if (args_has(args, 'v')) cmdq_print(item, "%s", value); else if (options_is_string(o)) { @@ -243,15 +246,14 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, const struct options_table_entry *oe; struct options_entry *o; struct options_array_item *a; - const char *name; - u_int idx; + const char *name, *array_key; int parent; if (cmd_get_entry(self) != &cmd_show_hooks_entry) { o = options_first(oo); while (o != NULL) { if (options_table_entry(o) == NULL) - cmd_show_options_print(self, item, o, -1, 0); + cmd_show_options_print(self, item, o, NULL, 0); o = options_next(o); } } @@ -278,7 +280,7 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, parent = 0; if (!options_is_array(o)) - cmd_show_options_print(self, item, o, -1, parent); + cmd_show_options_print(self, item, o, NULL, parent); else if ((a = options_array_first(o)) == NULL) { if (!args_has(args, 'v')) { name = options_name(o); @@ -289,8 +291,8 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, } } else { while (a != NULL) { - idx = options_array_item_index(a); - cmd_show_options_print(self, item, o, idx, + array_key = options_array_item_key(a); + cmd_show_options_print(self, item, o, array_key, parent); a = options_array_next(a); } diff --git a/colour.c b/colour.c index b37882a88..f563fd5aa 100644 --- a/colour.c +++ b/colour.c @@ -1294,7 +1294,8 @@ colour_palette_from_option(struct colour_palette *p, struct options *oo) { struct options_entry *o; struct options_array_item *a; - u_int i, n; + union options_value *ov; + u_int i; int c; if (p == NULL) @@ -1312,12 +1313,11 @@ colour_palette_from_option(struct colour_palette *p, struct options *oo) p->default_palette = xcalloc(256, sizeof *p->default_palette); for (i = 0; i < 256; i++) p->default_palette[i] = -1; - while (a != NULL) { - n = options_array_item_index(a); - if (n < 256) { - c = options_array_item_value(a)->number; - p->default_palette[n] = c; + for (i = 0; i < 256; i++) { + ov = options_array_getv(o, "%u", i); + if (ov != NULL) { + c = ov->number; + p->default_palette[i] = c; } - a = options_array_next(a); } } diff --git a/input.c b/input.c index cbedfba41..2438ea504 100644 --- a/input.c +++ b/input.c @@ -2781,7 +2781,7 @@ input_exit_rename(struct input_ctx *ictx) if (ictx->input_len == 0) { o = options_get_only(w->options, "automatic-rename"); if (o != NULL) - options_remove_or_default(o, -1, NULL); + options_remove_or_default(o, NULL, NULL); if (!options_get_number(w->options, "automatic-rename")) window_set_name(w, "", 1); } else { diff --git a/status.c b/status.c index 0d9fc3e4e..8f7c209e1 100644 --- a/status.c +++ b/status.c @@ -274,7 +274,7 @@ status_redraw(struct client *c) for (i = 0; i < lines; i++) { screen_write_cursormove(&ctx, 0, i, 0); - ov = options_array_get(o, i); + ov = options_array_getv(o, "%u", i); if (ov == NULL) { for (n = 0; n < width; n++) screen_write_putc(&ctx, &gc, ' '); diff --git a/tmux.1 b/tmux.1 index 54d30072b..03f653dd9 100644 --- a/tmux.1 +++ b/tmux.1 @@ -4574,6 +4574,10 @@ If .Fl g is given, the global session or window option is set. .Pp +Array options may be set by giving a key in square brackets after the option +name, for example +.Ql command-alias[zoom] . +.Pp .Fl F expands formats in the option value. The @@ -4685,7 +4689,7 @@ it is replaced with .Ar value . For example, after: .Pp -.Dl set \-s command\-alias[100] zoom=\[aq]resize\-pane \-Z\[aq] +.Dl set \-s command\-alias[zoom] zoom=\[aq]resize\-pane \-Z\[aq] .Pp Using: .Pp @@ -6286,11 +6290,11 @@ or .Fl H . The following two commands are equivalent: .Bd -literal -offset indent. -set\-hook \-g pane\-mode\-changed[42] \[aq]set \-g status\-left\-style bg=red\[aq] -set\-option \-g pane\-mode\-changed[42] \[aq]set \-g status\-left\-style bg=red\[aq] +set\-hook \-g pane\-mode\-changed[style] \[aq]display\-message changed\[aq] +set\-option \-g pane\-mode\-changed[style] \[aq]display\-message changed\[aq] .Ed .Pp -Setting a hook without specifying an array index clears the hook and sets the +Setting a hook without specifying an array key clears the hook and sets the first member of the array. .Pp A command's after diff --git a/tmux.h b/tmux.h index ddcb96146..e76bed7ab 100644 --- a/tmux.h +++ b/tmux.h @@ -2653,25 +2653,27 @@ const struct options_table_entry *options_table_entry(struct options_entry *); struct options_entry *options_get_only(struct options *, const char *); struct options_entry *options_get(struct options *, const char *); void options_array_clear(struct options_entry *); -union options_value *options_array_get(struct options_entry *, u_int); -int options_array_set(struct options_entry *, u_int, const char *, - int, char **); +union options_value *options_array_get(struct options_entry *, const char *); +union options_value * printflike(2, 3) options_array_getv( + struct options_entry *, const char *, ...); +int options_array_set(struct options_entry *, const char *, + const char *, int, char **); int options_array_assign(struct options_entry *, const char *, char **); struct options_array_item *options_array_first(struct options_entry *); struct options_array_item *options_array_next(struct options_array_item *); -u_int options_array_item_index(struct options_array_item *); +const char *options_array_item_key(struct options_array_item *); union options_value *options_array_item_value(struct options_array_item *); int options_is_array(struct options_entry *); int options_is_string(struct options_entry *); -char *options_to_string(struct options_entry *, int, int); -char *options_parse(const char *, int *); -struct options_entry *options_parse_get(struct options *, const char *, int *, - int); +char *options_to_string(struct options_entry *, const char *, int); +char *options_parse(const char *, char **); +struct options_entry *options_parse_get(struct options *, const char *, + char **, int); const struct options_table_entry *options_search(const char *); -char *options_match(const char *, int *, int *); -struct options_entry *options_match_get(struct options *, const char *, int *, - int, int *); +char *options_match(const char *, char **, int *); +struct options_entry *options_match_get(struct options *, const char *, + char **, int, int *); const char *options_get_string(struct options *, const char *); long long options_get_number(struct options *, const char *); struct cmd_list *options_get_command(struct options *, const char *); @@ -2694,7 +2696,7 @@ int options_from_string(struct options *, int options_find_choice(const struct options_table_entry *, const char *, char **); void options_push_changes(const char *); -int options_remove_or_default(struct options_entry *, int, +int options_remove_or_default(struct options_entry *, const char *, char **); /* options-table.c */ diff --git a/tty-keys.c b/tty-keys.c index 209bbeeab..d6a2610ea 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -495,7 +495,6 @@ tty_keys_build(struct tty *tty) u_int i, j; const char *s; struct options_entry *o; - struct options_array_item *a; union options_value *ov; char copy[16]; key_code key; @@ -532,12 +531,10 @@ tty_keys_build(struct tty *tty) o = options_get(global_options, "user-keys"); if (o != NULL) { - a = options_array_first(o); - while (a != NULL) { - i = options_array_item_index(a); - ov = options_array_item_value(a); - tty_keys_add(tty, ov->string, KEYC_USER + i); - a = options_array_next(a); + for (i = 0; i <= KEYC_NUSER; i++) { + ov = options_array_getv(o, "%u", i); + if (ov != NULL) + tty_keys_add(tty, ov->string, KEYC_USER + i); } } } From 20ef19b42fda7f5cbc39c62bee8c6e02ee315de2 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 6 Jul 2026 14:40:57 +0000 Subject: [PATCH 070/127] Add a key to change an array key in customize mode. --- cmd-set-option.c | 33 ++++---- format.c | 33 ++++---- options.c | 205 +++++++++++++++++++++++++++++++++++---------- window-customize.c | 189 ++++++++++++++++++++++++++++++----------- 4 files changed, 338 insertions(+), 122 deletions(-) diff --git a/cmd-set-option.c b/cmd-set-option.c index 5b5ea3d88..1b6de1e86 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -178,10 +178,10 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) struct window_pane *loop; struct options *oo; struct options_entry *parent, *o, *po; - char *name, *argument, *expanded = NULL; - char *cause; + char *name, *argument, *cause; + char *expanded = NULL, *array_key = NULL; const char *value; - int window, idx, already, error, ambiguous; + int window, already, error, ambiguous; int scope; window = (cmd_get_entry(self) == &cmd_set_window_option_entry); @@ -202,8 +202,8 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); } - /* Parse option name and index. */ - name = options_match(argument, &idx, &ambiguous); + /* Parse option name and array key. */ + name = options_match(argument, &array_key, &ambiguous); if (name == NULL) { if (args_has(args, 'q')) goto out; @@ -235,21 +235,23 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) o = options_get_only(oo, name); parent = options_get(oo, name); - /* Check that array options and indexes match up. */ - if (idx != -1 && (*name == '@' || !options_is_array(parent))) { + /* Check that array options and keys match up. */ + if (array_key != NULL && (*name == '@' || !options_is_array(parent))) { cmdq_error(item, "not an array: %s", argument); goto fail; } /* With -o, check this option is not already set. */ if (!args_has(args, 'u') && args_has(args, 'o')) { - if (idx == -1) + if (array_key == NULL) already = (o != NULL); else { if (o == NULL) already = 0; + else if (options_array_get(o, array_key) != NULL) + already = 1; else - already = (options_array_get(o, idx) != NULL); + already = 0; } if (already) { if (args_has(args, 'q')) @@ -265,7 +267,8 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) po = options_get_only(loop->options, name); if (po == NULL) continue; - if (options_remove_or_default(po, idx, &cause) != 0) { + if (options_remove_or_default(po, array_key, + &cause) != 0) { cmdq_error(item, "%s", cause); free(cause); goto fail; @@ -275,7 +278,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'u') || args_has(args, 'U')) { if (o == NULL) goto out; - if (options_remove_or_default(o, idx, &cause) != 0) { + if (options_remove_or_default(o, array_key, &cause) != 0) { cmdq_error(item, "%s", cause); free(cause); goto fail; @@ -286,7 +289,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) goto fail; } options_set_string(oo, name, append, "%s", value); - } else if (idx == -1 && !options_is_array(parent)) { + } else if (array_key == NULL && !options_is_array(parent)) { error = options_from_string(oo, options_table_entry(parent), options_table_entry(parent)->name, value, args_has(args, 'a'), &cause); @@ -302,7 +305,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) } if (o == NULL) o = options_empty(oo, options_table_entry(parent)); - if (idx == -1) { + if (array_key == NULL) { if (!append) options_array_clear(o); if (options_array_assign(o, value, &cause) != 0) { @@ -310,7 +313,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) free(cause); goto fail; } - } else if (options_array_set(o, idx, value, append, + } else if (options_array_set(o, array_key, value, append, &cause) != 0) { cmdq_error(item, "%s", cause); free(cause); @@ -324,11 +327,13 @@ out: free(argument); free(expanded); free(name); + free(array_key); return (CMD_RETURN_NORMAL); fail: free(argument); free(expanded); free(name); + free(array_key); return (CMD_RETURN_ERROR); } diff --git a/format.c b/format.c index 6757a4276..f2bc22a7a 100644 --- a/format.c +++ b/format.c @@ -4213,25 +4213,26 @@ format_find(struct format_tree *ft, const char *key, uint64_t modifiers, struct format_entry *fe, fe_find; struct environ_entry *envent; struct options_entry *o; - int idx; char *found = NULL, *saved, s[512]; + char *array_key = NULL; const char *errstr; time_t t = 0; struct tm tm; - o = options_parse_get(global_options, key, &idx, 0); + o = options_parse_get(global_options, key, &array_key, 0); if (o == NULL && ft->wp != NULL) - o = options_parse_get(ft->wp->options, key, &idx, 0); + o = options_parse_get(ft->wp->options, key, &array_key, 0); if (o == NULL && ft->w != NULL) - o = options_parse_get(ft->w->options, key, &idx, 0); + o = options_parse_get(ft->w->options, key, &array_key, 0); if (o == NULL) - o = options_parse_get(global_w_options, key, &idx, 0); + o = options_parse_get(global_w_options, key, &array_key, 0); if (o == NULL && ft->s != NULL) - o = options_parse_get(ft->s->options, key, &idx, 0); + o = options_parse_get(ft->s->options, key, &array_key, 0); if (o == NULL) - o = options_parse_get(global_s_options, key, &idx, 0); + o = options_parse_get(global_s_options, key, &array_key, 0); if (o != NULL) { - found = options_to_string(o, idx, 1); + found = options_to_string(o, array_key, 1); + free(array_key); goto found; } @@ -4925,7 +4926,7 @@ format_add_window_neighbour(struct format_tree *nft, struct winlink *wl, oname = options_name(o); if (*oname == '@') { xasprintf(&prefixed, "%s_%s", prefix, oname); - oval = options_to_string(o, -1, 1); + oval = options_to_string(o, NULL, 1); format_add(nft, prefixed, "%s", oval); free(oval); free(prefixed); @@ -5097,11 +5098,12 @@ format_loop_add_option(struct format_expand_state *es, const char *fmt, nft = format_create(ft->client, ft->item, FORMAT_NONE, ft->flags); format_add(nft, "option_name", "%s", name); - s = options_to_string(o, -1, 0); + s = options_to_string(o, NULL, 0); format_add(nft, "option_value", "%s", s); free(s); format_add(nft, "option_is_array", "%d", is_array); + format_add(nft, "option_array_key", "%s", ""); format_add(nft, "option_array_index", "%s", ""); format_add(nft, "option_array_first", "%d", is_array); format_add(nft, "option_array_last", "%d", is_array); @@ -5139,20 +5141,21 @@ format_loop_add_array_item(struct format_expand_state *es, const char *fmt, struct format_expand_state next; const struct options_table_entry *oe = options_table_entry(o); const char *name = options_name(o); + const char *array_key; char *expanded, *s; - u_int idx; - idx = options_array_item_index(a); - format_log(es, "option loop: %s[%u]", name, idx); + array_key = options_array_item_key(a); + format_log(es, "option loop: %s[%s]", name, array_key); nft = format_create(ft->client, ft->item, FORMAT_NONE, ft->flags); format_add(nft, "option_name", "%s", name); - s = options_to_string(o, idx, 0); + s = options_to_string(o, array_key, 0); format_add(nft, "option_value", "%s", s); free(s); format_add(nft, "option_is_array", "1"); - format_add(nft, "option_array_index", "%u", idx); + format_add(nft, "option_array_key", "%s", array_key); + format_add(nft, "option_array_index", "%s", array_key); if (a == options_array_first(o)) format_add(nft, "option_array_first", "1"); else diff --git a/options.c b/options.c index 19da38886..fd5f1b262 100644 --- a/options.c +++ b/options.c @@ -32,18 +32,69 @@ */ struct options_array_item { - u_int index; + char *key; union options_value value; RB_ENTRY(options_array_item) entry; }; + +static int +options_array_key_to_number(const char *key, u_int *idx) +{ + const char *errstr; + long long n; + + if (*key == '\0') + return (-1); + for (const char *cp = key; *cp != '\0'; cp++) { + if (!isdigit((u_char)*cp)) + return (0); + } + + n = strtonum(key, 0, UINT_MAX, &errstr); + if (errstr != NULL) + return (-1); + if (idx != NULL) + *idx = n; + return (1); +} + +static char * +options_array_correct_key(const char *key) +{ + u_int idx; + int numeric; + char *out; + + numeric = options_array_key_to_number(key, &idx); + if (numeric == -1) + return (NULL); + if (numeric == 1) { + xasprintf(&out, "%u", idx); + return (out); + } + return (xstrdup(key)); +} + static int options_array_cmp(struct options_array_item *a1, struct options_array_item *a2) { - if (a1->index < a2->index) + u_int i1, i2; + int n1, n2; + + n1 = options_array_key_to_number(a1->key, &i1); + n2 = options_array_key_to_number(a2->key, &i2); + if (n1 && n2) { + if (i1 < i2) + return (-1); + if (i1 > i2) + return (1); + return (0); + } + if (n1) return (-1); - if (a1->index > a2->index) + if (n2) return (1); - return (0); + return (strcmp(a1->key, a2->key)); } RB_GENERATE_STATIC(options_array, options_array_item, entry, options_array_cmp); @@ -260,6 +311,7 @@ options_default(struct options *oo, const struct options_table_entry *oe) { struct options_entry *o; union options_value *ov; + char key[32]; u_int i; struct cmd_parse_result *pr; @@ -271,8 +323,10 @@ options_default(struct options *oo, const struct options_table_entry *oe) options_array_assign(o, oe->default_str, NULL); return (o); } - for (i = 0; oe->default_arr[i] != NULL; i++) - options_array_set(o, i, oe->default_arr[i], 0, NULL); + for (i = 0; oe->default_arr[i] != NULL; i++) { + xsnprintf(key, sizeof key, "%u", i); + options_array_set(o, key, oe->default_arr[i], 0, NULL); + } return (o); } @@ -393,21 +447,21 @@ options_table_entry(struct options_entry *o) } static struct options_array_item * -options_array_item(struct options_entry *o, u_int idx) +options_array_item(struct options_entry *o, const char *key) { struct options_array_item a; - a.index = idx; + a.key = (char *)key; return (RB_FIND(options_array, &o->value.array, &a)); } static struct options_array_item * -options_array_new(struct options_entry *o, u_int idx) +options_array_new(struct options_entry *o, const char *key) { struct options_array_item *a; a = xcalloc(1, sizeof *a); - a->index = idx; + a->key = xstrdup(key); RB_INSERT(options_array, &o->value.array, a); return (a); } @@ -417,6 +471,7 @@ options_array_free(struct options_entry *o, struct options_array_item *a) { options_value_free(o, &a->value); RB_REMOVE(options_array, &o->value.array, a); + free(a->key); free(a); } @@ -433,24 +488,45 @@ options_array_clear(struct options_entry *o) } union options_value * -options_array_get(struct options_entry *o, u_int idx) +options_array_get(struct options_entry *o, const char *key) { struct options_array_item *a; + char *new_key; if (!OPTIONS_IS_ARRAY(o)) return (NULL); - a = options_array_item(o, idx); + new_key = options_array_correct_key(key); + if (new_key == NULL) + return (NULL); + a = options_array_item(o, new_key); + free(new_key); if (a == NULL) return (NULL); return (&a->value); } +union options_value * +options_array_getv(struct options_entry *o, const char *fmt, ...) +{ + union options_value *ov; + va_list ap; + char *key; + + va_start(ap, fmt); + xvasprintf(&key, fmt, ap); + va_end(ap); + + ov = options_array_get(o, key); + free(key); + return (ov); +} + int -options_array_set(struct options_entry *o, u_int idx, const char *value, +options_array_set(struct options_entry *o, const char *key, const char *value, int append, char **cause) { struct options_array_item *a; - char *new; + char *new, *new_key; struct cmd_parse_result *pr; long long number; @@ -460,10 +536,18 @@ options_array_set(struct options_entry *o, u_int idx, const char *value, return (-1); } + new_key = options_array_correct_key(key); + if (new_key == NULL) { + if (cause != NULL) + xasprintf(cause, "bad array key: %s", key); + return (-1); + } + if (value == NULL) { - a = options_array_item(o, idx); + a = options_array_item(o, new_key); if (a != NULL) options_array_free(o, a); + free(new_key); return (0); } @@ -475,50 +559,56 @@ options_array_set(struct options_entry *o, u_int idx, const char *value, *cause = pr->error; else free(pr->error); + free(new_key); return (-1); case CMD_PARSE_SUCCESS: break; } - a = options_array_item(o, idx); + a = options_array_item(o, new_key); if (a == NULL) - a = options_array_new(o, idx); + a = options_array_new(o, new_key); else options_value_free(o, &a->value); a->value.cmdlist = pr->cmdlist; + free(new_key); return (0); } if (OPTIONS_IS_STRING(o)) { - a = options_array_item(o, idx); + a = options_array_item(o, new_key); if (a != NULL && append) xasprintf(&new, "%s%s", a->value.string, value); else new = xstrdup(value); if (a == NULL) - a = options_array_new(o, idx); + a = options_array_new(o, new_key); else options_value_free(o, &a->value); a->value.string = new; + free(new_key); return (0); } if (o->tableentry->type == OPTIONS_TABLE_COLOUR) { if ((number = colour_fromstring(value)) == -1) { xasprintf(cause, "bad colour: %s", value); + free(new_key); return (-1); } - a = options_array_item(o, idx); + a = options_array_item(o, new_key); if (a == NULL) - a = options_array_new(o, idx); + a = options_array_new(o, new_key); else options_value_free(o, &a->value); a->value.number = number; + free(new_key); return (0); } if (cause != NULL) *cause = xstrdup("wrong array type"); + free(new_key); return (-1); } @@ -527,6 +617,7 @@ options_array_assign(struct options_entry *o, const char *s, char **cause) { const char *separator; char *copy, *next, *string; + char key[32]; u_int i; separator = o->tableentry->separator; @@ -536,10 +627,11 @@ options_array_assign(struct options_entry *o, const char *s, char **cause) if (*s == '\0') return (0); for (i = 0; i < UINT_MAX; i++) { - if (options_array_item(o, i) == NULL) + if (options_array_getv(o, "%u", i) == NULL) break; } - return (options_array_set(o, i, s, 0, cause)); + xsnprintf(key, sizeof key, "%u", i); + return (options_array_set(o, key, s, 0, cause)); } if (*s == '\0') @@ -549,12 +641,13 @@ options_array_assign(struct options_entry *o, const char *s, char **cause) if (*next == '\0') continue; for (i = 0; i < UINT_MAX; i++) { - if (options_array_item(o, i) == NULL) + if (options_array_getv(o, "%u", i) == NULL) break; } if (i == UINT_MAX) break; - if (options_array_set(o, i, next, 0, cause) != 0) { + xsnprintf(key, sizeof key, "%u", i); + if (options_array_set(o, key, next, 0, cause) != 0) { free(copy); return (-1); } @@ -577,10 +670,10 @@ options_array_next(struct options_array_item *a) return (RB_NEXT(options_array, &o->value.array, a)); } -u_int -options_array_item_index(struct options_array_item *a) +const char * +options_array_item_key(struct options_array_item *a) { - return (a->index); + return (a->key); } union options_value * @@ -602,15 +695,16 @@ options_is_string(struct options_entry *o) } char * -options_to_string(struct options_entry *o, int idx, int numeric) +options_to_string(struct options_entry *o, const char *key, int numeric) { struct options_array_item *a; char *result = NULL; char *last = NULL; char *next; + char *new_key; if (OPTIONS_IS_ARRAY(o)) { - if (idx == -1) { + if (key == NULL) { RB_FOREACH(a, options_array, &o->value.array) { next = options_value_to_string(o, &a->value, numeric); @@ -627,7 +721,11 @@ options_to_string(struct options_entry *o, int idx, int numeric) return (xstrdup("")); return (result); } - a = options_array_item(o, idx); + new_key = options_array_correct_key(key); + if (new_key == NULL) + return (xstrdup("")); + a = options_array_item(o, new_key); + free(new_key); if (a == NULL) return (xstrdup("")); return (options_value_to_string(o, &a->value, numeric)); @@ -636,37 +734,41 @@ options_to_string(struct options_entry *o, int idx, int numeric) } char * -options_parse(const char *name, int *idx) +options_parse(const char *name, char **key) { - char *copy, *cp, *end; + char *copy, *cp, *end, *raw, *new_key; if (*name == '\0') return (NULL); + *key = NULL; copy = xstrdup(name); if ((cp = strchr(copy, '[')) == NULL) { - *idx = -1; return (copy); } end = strchr(cp + 1, ']'); - if (end == NULL || end[1] != '\0' || !isdigit((u_char)end[-1])) { + if (end == NULL || end[1] != '\0' || end == cp + 1) { free(copy); return (NULL); } - if (sscanf(cp, "[%d]", idx) != 1 || *idx < 0) { + raw = xstrndup(cp + 1, end - (cp + 1)); + new_key = options_array_correct_key(raw); + free(raw); + if (new_key == NULL) { free(copy); return (NULL); } + *key = new_key; *cp = '\0'; return (copy); } struct options_entry * -options_parse_get(struct options *oo, const char *s, int *idx, int only) +options_parse_get(struct options *oo, const char *s, char **key, int only) { struct options_entry *o; char *name; - name = options_parse(s, idx); + name = options_parse(s, key); if (name == NULL) return (NULL); if (only) @@ -674,6 +776,10 @@ options_parse_get(struct options *oo, const char *s, int *idx, int only) else o = options_get(oo, name); free(name); + if (o == NULL) { + free(*key); + *key = NULL; + } return (o); } @@ -690,14 +796,14 @@ options_search(const char *name) } char * -options_match(const char *s, int *idx, int *ambiguous) +options_match(const char *s, char **key, int *ambiguous) { const struct options_table_entry *oe, *found; char *parsed; const char *name; size_t namelen; - parsed = options_parse(s, idx); + parsed = options_parse(s, key); if (parsed == NULL) return (NULL); if (*parsed == '@') { @@ -718,6 +824,8 @@ options_match(const char *s, int *idx, int *ambiguous) if (found != NULL) { *ambiguous = 1; free(parsed); + free(*key); + *key = NULL; return (NULL); } found = oe; @@ -726,19 +834,21 @@ options_match(const char *s, int *idx, int *ambiguous) free(parsed); if (found == NULL) { *ambiguous = 0; + free(*key); + *key = NULL; return (NULL); } return (xstrdup(found->name)); } struct options_entry * -options_match_get(struct options *oo, const char *s, int *idx, int only, +options_match_get(struct options *oo, const char *s, char **key, int only, int *ambiguous) { char *name; struct options_entry *o; - name = options_match(s, idx, ambiguous); + name = options_match(s, key, ambiguous); if (name == NULL) return (NULL); *ambiguous = 0; @@ -747,6 +857,10 @@ options_match_get(struct options *oo, const char *s, int *idx, int only, else o = options_get(oo, name); free(name); + if (o == NULL) { + free(*key); + *key = NULL; + } return (o); } @@ -1340,11 +1454,12 @@ options_push_changes(const char *name) } int -options_remove_or_default(struct options_entry *o, int idx, char **cause) +options_remove_or_default(struct options_entry *o, const char *key, + char **cause) { struct options *oo = o->owner; - if (idx == -1) { + if (key == NULL) { if (o->tableentry != NULL && (oo == global_options || oo == global_s_options || @@ -1352,7 +1467,7 @@ options_remove_or_default(struct options_entry *o, int idx, char **cause) options_default(oo, o->tableentry); else options_remove(o); - } else if (options_array_set(o, idx, NULL, 0, cause) != 0) + } else if (options_array_set(o, key, NULL, 0, cause) != 0) return (-1); return (0); } diff --git a/window-customize.c b/window-customize.c index 32caae9a3..09da37f2e 100644 --- a/window-customize.c +++ b/window-customize.c @@ -82,15 +82,15 @@ enum window_customize_change { }; struct window_customize_itemdata { - struct window_customize_modedata *data; - enum window_customize_scope scope; + struct window_customize_modedata *data; + enum window_customize_scope scope; - char *table; - key_code key; + char *table; + key_code key; - struct options *oo; - char *name; - int idx; + struct options *oo; + char *name; + char *array_key; }; struct window_customize_modedata { @@ -111,15 +111,17 @@ struct window_customize_modedata { }; static uint64_t -window_customize_get_tag(struct options_entry *o, int idx, +window_customize_get_tag(struct options_entry *o, struct options_array_item *a, const struct options_table_entry *oe) { uint64_t offset; + if (a != NULL) + return ((uint64_t)(uintptr_t)a); if (oe == NULL) return ((uint64_t)o); offset = ((char *)oe - (char *)options_table) / sizeof *options_table; - return ((2ULL << 62)|(offset << 32)|((idx + 1) << 1)|1); + return ((2ULL << 62)|(offset << 32)|1); } static struct options * @@ -224,6 +226,7 @@ window_customize_free_item(struct window_customize_itemdata *item) { free(item->table); free(item->name); + free(item->array_key); free(item); } @@ -237,26 +240,26 @@ window_customize_build_array(struct window_customize_modedata *data, struct window_customize_itemdata *item; struct options_array_item *ai; char *name, *value, *text; - u_int idx; uint64_t tag; + const char *array_key; ai = options_array_first(o); while (ai != NULL) { - idx = options_array_item_index(ai); + array_key = options_array_item_key(ai); - xasprintf(&name, "%s[%u]", options_name(o), idx); + xasprintf(&name, "%s[%s]", options_name(o), array_key); format_add(ft, "option_name", "%s", name); - value = options_to_string(o, idx, 0); + value = options_to_string(o, array_key, 0); format_add(ft, "option_value", "%s", value); item = window_customize_add_item(data); item->scope = scope; item->oo = oo; item->name = xstrdup(options_name(o)); - item->idx = idx; + item->array_key = xstrdup(array_key); text = format_expand(ft, data->format); - tag = window_customize_get_tag(o, idx, oe); + tag = window_customize_get_tag(o, ai, oe); mode_tree_add(data->data, top, item, tag, name, text, -1); free(text); @@ -307,7 +310,7 @@ window_customize_build_option(struct window_customize_modedata *data, format_add(ft, "option_unit", "%s", ""); if (!array) { - value = options_to_string(o, -1, 0); + value = options_to_string(o, NULL, 0); format_add(ft, "option_value", "%s", value); free(value); } @@ -324,13 +327,12 @@ window_customize_build_option(struct window_customize_modedata *data, item->oo = oo; item->scope = scope; item->name = xstrdup(name); - item->idx = -1; if (array) text = NULL; else text = format_expand(ft, data->format); - tag = window_customize_get_tag(o, -1, oe); + tag = window_customize_get_tag(o, NULL, oe); top = mode_tree_add(data->data, top, item, tag, name, text, 0); free(text); @@ -483,7 +485,6 @@ window_customize_build_keys(struct window_customize_modedata *data, item->table = xstrdup(kt->name); item->key = bd->key; item->name = xstrdup(key_string_lookup_key(item->key, 0)); - item->idx = -1; expanded = format_expand(ft, data->format); child = mode_tree_add(data->data, top, item, (uint64_t)bd, @@ -653,12 +654,12 @@ window_customize_draw_option(struct window_customize_modedata *data, { struct screen *s = ctx->s; u_int cx = s->cx, cy = s->cy; - int idx; struct options_entry *o, *parent; struct options *go, *wo; const struct options_table_entry *oe; struct grid_cell gc; const char **choice, *text, *name; + const char *array_key; const char *space = "", *unit = ""; char *value = NULL, *expanded; char *default_value = NULL; @@ -669,7 +670,7 @@ window_customize_draw_option(struct window_customize_modedata *data, if (!window_customize_check_item(data, item, &fs)) return; name = item->name; - idx = item->idx; + array_key = item->array_key; o = options_get(item->oo, name); if (o == NULL) @@ -708,25 +709,25 @@ window_customize_draw_option(struct window_customize_modedata *data, &grid_default_cell, "This is a %s option.", text)) goto out; if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) { - if (idx != -1) { + if (array_key != NULL) { if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, - "This is an array option, index %u.", idx)) + "This is an array option, key %s.", array_key)) goto out; } else { if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, "This is an array option.")) goto out; } - if (idx == -1) + if (array_key == NULL) goto out; } screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */ if (s->cy >= cy + sy - 1) goto out; - value = options_to_string(o, idx, 0); - if (oe != NULL && idx == -1) { + value = options_to_string(o, array_key, 0); + if (oe != NULL && array_key == NULL) { default_value = options_default_to_string(oe); if (strcmp(default_value, value) == 0) { free(default_value); @@ -820,7 +821,7 @@ window_customize_draw_option(struct window_customize_modedata *data, if (wo != NULL && options_owner(o) != wo) { parent = options_get_only(wo, name); if (parent != NULL) { - value = options_to_string(parent, -1 , 0); + value = options_to_string(parent, NULL, 0); if (!screen_write_text(ctx, s->cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, "Window value (from window %u): %s%s%s", fs.wl->idx, @@ -831,7 +832,7 @@ window_customize_draw_option(struct window_customize_modedata *data, if (go != NULL && options_owner(o) != go) { parent = options_get_only(go, name); if (parent != NULL) { - value = options_to_string(parent, -1 , 0); + value = options_to_string(parent, NULL, 0); if (!screen_write_text(ctx, s->cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, "Global value: %s%s%s", value, space, unit)) @@ -896,6 +897,8 @@ static const char* window_customize_help_lines[] = { "#[fg=themelightgrey]" " U #[#{E:tree-mode-border-style},acs]x#[default] Unset tagged %1s", "#[fg=themelightgrey]" + " a #[#{E:tree-mode-border-style},acs]x#[default] Change array key", + "#[fg=themelightgrey]" " f #[#{E:tree-mode-border-style},acs]x#[default] Enter a filter", "#[fg=themelightgrey]" " v #[#{E:tree-mode-border-style},acs]x#[default] Toggle information", @@ -1007,8 +1010,10 @@ window_customize_set_option_callback(struct client *c, void *itemdata, const struct options_table_entry *oe; struct options *oo = item->oo; const char *name = item->name; + const char *array_key = item->array_key; char *cause; - int idx = item->idx; + u_int idx; + char keybuf[32]; if (s == NULL || *s == '\0' || data->dead) return (PROMPT_CLOSE); @@ -1020,13 +1025,15 @@ window_customize_set_option_callback(struct client *c, void *itemdata, oe = options_table_entry(o); if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) { - if (idx == -1) { + if (array_key == NULL) { for (idx = 0; idx < INT_MAX; idx++) { - if (options_array_get(o, idx) == NULL) + if (options_array_getv(o, "%u", idx) == NULL) break; } + xsnprintf(keybuf, sizeof keybuf, "%u", idx); + array_key = keybuf; } - if (options_array_set(o, idx, s, 0, &cause) != 0) + if (options_array_set(o, array_key, s, 0, &cause) != 0) goto fail; } else { if (options_from_string(oo, oe, name, s, 0, &cause) != 0) @@ -1056,10 +1063,11 @@ window_customize_set_option(struct client *c, const struct options_table_entry *oe; struct options *oo; struct window_customize_itemdata *new_item; - int flag, idx = item->idx; + int flag; enum window_customize_scope scope = WINDOW_CUSTOMIZE_NONE; u_int choice; const char *name = item->name, *space = ""; + const char *array_key = item->array_key; char *prompt, *value, *text; struct cmd_find_state fs; @@ -1142,25 +1150,26 @@ window_customize_set_option(struct client *c, else if (scope != WINDOW_CUSTOMIZE_SERVER) space = ", global"; if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) { - if (idx == -1) { + if (array_key == NULL) { xasprintf(&prompt, "(%s[+]%s%s) ", name, space, text); } else { - xasprintf(&prompt, "(%s[%d]%s%s) ", name, idx, - space, text); + xasprintf(&prompt, "(%s[%s]%s%s) ", name, + array_key, space, text); } } else xasprintf(&prompt, "(%s%s%s) ", name, space, text); free(text); - value = options_to_string(o, idx, 0); + value = options_to_string(o, array_key, 0); new_item = xcalloc(1, sizeof *new_item); new_item->data = data; new_item->scope = scope; new_item->oo = oo; new_item->name = xstrdup(name); - new_item->idx = idx; + if (array_key != NULL) + new_item->array_key = xstrdup(array_key); data->references++; mode_tree_set_prompt(data->data, c, prompt, value, @@ -1173,6 +1182,84 @@ window_customize_set_option(struct client *c, } } +static enum prompt_result +window_customize_set_array_key_callback(struct client *c, void *itemdata, + const char *s, __unused enum prompt_key_result key) +{ + struct window_customize_itemdata *item = itemdata; + struct window_customize_modedata *data; + struct options_entry *o; + const char *name, *array_key; + char *value, *cause; + + if (item == NULL) + return (PROMPT_CLOSE); + data = item->data; + if (s == NULL || *s == '\0' || data->dead) + return (PROMPT_CLOSE); + name = item->name; + array_key = item->array_key; + if (array_key == NULL || !window_customize_check_item(data, item, NULL)) + return (PROMPT_CLOSE); + + o = options_get(item->oo, name); + if (o == NULL) + return (PROMPT_CLOSE); + if (options_array_get(o, s) != NULL) + return (PROMPT_CLOSE); + + value = options_to_string(o, array_key, 0); + if (options_array_set(o, s, value, 0, &cause) != 0) + goto fail; + free(value); + + options_array_set(o, array_key, NULL, 0, NULL); + options_push_changes(item->name); + mode_tree_build(data->data); + mode_tree_draw(data->data); + data->wp->flags |= PANE_REDRAW; + + return (PROMPT_CLOSE); + +fail: + free(value); + *cause = toupper((u_char)*cause); + status_message_set(c, -1, 1, 0, 0, "%s", cause); + free(cause); + return (PROMPT_CLOSE); +} + +static void +window_customize_set_array_key(struct client *c, + struct window_customize_modedata *data, + struct window_customize_itemdata *item) +{ + struct window_customize_itemdata *new_item; + char *prompt; + + if (item == NULL || + item->array_key == NULL || + !window_customize_check_item(data, item, NULL)) + return; + + xasprintf(&prompt, "(%s[%s]) ", item->name, item->array_key); + + new_item = xcalloc(1, sizeof *new_item); + new_item->data = data; + new_item->scope = item->scope; + new_item->oo = item->oo; + new_item->name = xstrdup(item->name); + new_item->array_key = xstrdup(item->array_key); + + data->references++; + mode_tree_set_prompt(data->data, c, prompt, item->array_key, + PROMPT_TYPE_COMMAND, PROMPT_NOFORMAT, + window_customize_set_array_key_callback, + window_customize_free_item_callback, new_item); + + free(prompt); +} + static void window_customize_unset_option(struct window_customize_modedata *data, struct window_customize_itemdata *item) @@ -1185,9 +1272,10 @@ window_customize_unset_option(struct window_customize_modedata *data, o = options_get(item->oo, item->name); if (o == NULL) return; - if (item->idx != -1 && item == mode_tree_get_current(data->data)) + if (item->array_key != NULL && + item == mode_tree_get_current(data->data)) mode_tree_up(data->data, 0); - options_remove_or_default(o, item->idx, NULL); + options_remove_or_default(o, item->array_key, NULL); } static void @@ -1199,14 +1287,14 @@ window_customize_reset_option(struct window_customize_modedata *data, if (item == NULL || !window_customize_check_item(data, item, NULL)) return; - if (item->idx != -1) + if (item->array_key != NULL) return; oo = item->oo; while (oo != NULL) { o = options_get_only(item->oo, item->name); if (o != NULL) - options_remove_or_default(o, -1, NULL); + options_remove_or_default(o, NULL, NULL); oo = options_get_parent(oo); } } @@ -1452,7 +1540,7 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, struct window_pane *wp = wme->wp; struct window_customize_modedata *data = wme->data; struct window_customize_itemdata *item, *new_item; - int finished, idx; + int finished; char *prompt; u_int tagged; @@ -1462,6 +1550,11 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, item = new_item; switch (key) { + case 'a': + if (item == NULL || item->scope == WINDOW_CUSTOMIZE_KEY) + break; + window_customize_set_array_key(c, data, item); + break; case '\r': case 's': if (item == NULL) @@ -1490,7 +1583,7 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, mode_tree_build(data->data); break; case 'd': - if (item == NULL || item->idx != -1) + if (item == NULL || item->array_key != NULL) break; xasprintf(&prompt, "Reset %s to default? ", item->name); data->references++; @@ -1519,10 +1612,10 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, case 'u': if (item == NULL) break; - idx = item->idx; - if (idx != -1) - xasprintf(&prompt, "Unset %s[%d]? ", item->name, idx); - else + if (item->array_key != NULL) { + xasprintf(&prompt, "Unset %s[%s]? ", item->name, + item->array_key); + } else xasprintf(&prompt, "Unset %s? ", item->name); data->references++; data->change = WINDOW_CUSTOMIZE_UNSET; From d33d5b7de81e0af71feabe3bb9ee2cfb040ee39c Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 6 Jul 2026 15:41:31 +0000 Subject: [PATCH 071/127] When sync is enabled, we need to mark the whole scroll region dirty if there has been any scrolling. GitHub issue 5330. --- screen-write.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/screen-write.c b/screen-write.c index c1192e8ae..ebc40b484 100644 --- a/screen-write.c +++ b/screen-write.c @@ -2314,6 +2314,10 @@ screen_write_collect_flush(struct screen_write_ctx *ctx, int scroll_only, if (wp != NULL && (wp->flags & (PANE_REDRAW|PANE_DROP))) goto discard; if (s->mode & MODE_SYNC) { + if (ctx->scrolled != 0) { + screen_write_should_draw_lines(ctx, s->rupper, + s->rlower + 1 - s->rupper); + } for (y = 0; y < screen_size_y(s); y++) { cl = &s->write_list[y]; if (!TAILQ_EMPTY(&cl->items)) From 3946e24eff275f7e8fde53bdc0c34a32a6e8e695 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 6 Jul 2026 18:13:25 +0100 Subject: [PATCH 072/127] Add array test. --- regress/options-array.sh | 58 ++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/regress/options-array.sh b/regress/options-array.sh index c372e3461..d9751c94c 100644 --- a/regress/options-array.sh +++ b/regress/options-array.sh @@ -3,12 +3,14 @@ # Tests of array options in the options engine (options_array_* in options.c # and the array handling in cmd-set-option.c / cmd-show-options.c). # -# Array options are indexed by integer. This exercises: setting a whole array -# from a separator-delimited string; per-index set with option[N]; -a append -# (which lands at the next free index); show ordering by ascending index and -# preservation of gaps; per-index unset with -u; show -v of a single index and -# of a missing index; and per-option separators (user-keys splits only on -# comma, update-environment on space or comma). +# Array options are keyed by string, with numeric-looking keys kept compatible +# with the old numeric forms. This exercises: setting a whole array from a +# separator-delimited string; per-key set with option[key]; -a append (which +# lands at the next free numeric key); show ordering by numeric keys first in +# ascending order followed by string keys in strcmp order; preservation of gaps; +# per-key unset with -u; show -v of a single key and of a missing key; and +# per-option separators (user-keys splits only on comma, update-environment on +# space or comma). # # update-environment (session), status-format (session), user-keys (server) # and command-alias (server) are used as representative array options. @@ -87,41 +89,59 @@ $TMUX new-session -d -s main -x 80 -y 24 || exit 1 # --- whole-array assignment splits on the separator ----------------------- # # update-environment has the default " ," separator, so a single string value -# is split into consecutive indices starting at 0. +# is split into consecutive numeric keys starting at 0. check_ok set -g update-environment "AAA BBB,CCC" check_array "-g update-environment" "update-environment[0] AAA update-environment[1] BBB update-environment[2] CCC" -# --- -a append goes to the next free index -------------------------------- +# --- -a append goes to the next free numeric key -------------------------- check_ok set -ga update-environment "DDD" check_array "-g update-environment" "update-environment[0] AAA update-environment[1] BBB update-environment[2] CCC update-environment[3] DDD" -# --- per-index unset leaves a gap; show preserves order and gaps ---------- +# --- per-key unset leaves a gap; show preserves order and gaps ------------ check_ok set -gu update-environment[1] check_array "-g update-environment" "update-environment[0] AAA update-environment[2] CCC update-environment[3] DDD" -# show -v of an existing index returns its value; a missing index is empty. +# show -v of an existing key returns its value; a missing key is empty. check_value "-gv update-environment[0]" "AAA" check_value "-gv update-environment[1]" "" +check_ok set -g update-environment[notify] "EEE" +check_ok set -ga update-environment "FFF" +check_array "-g update-environment" "update-environment[0] AAA +update-environment[1] FFF +update-environment[2] CCC +update-environment[3] DDD +update-environment[notify] EEE" -# --- explicit indexed set, including out-of-order and gaps ---------------- +# --- explicit keyed set, including out-of-order and gaps ------------------ # # status-format is a session array; assigning an empty string first clears its -# multi-index default, then set specific indices out of order and confirm show -# sorts by ascending index and keeps the gap at [1]. +# multi-index default, then set specific keys out of order and confirm show +# sorts by ascending numeric key and keeps the gap at [1]. check_ok set -g status-format "" check_array "-g status-format" "status-format" check_ok set -g status-format[5] "five" check_ok set -g status-format[0] "zero" check_ok set -g status-format[2] "two" +check_ok set -g status-format[01] "one" +check_ok set -g status-format[zoom] "zoom" +check_ok set -g status-format[foo-bar] "foo-bar" +check_ok set -g status-format[xterm-256color] "xterm" check_array "-g status-format" "status-format[0] zero +status-format[1] one status-format[2] two -status-format[5] five" +status-format[5] five +status-format[foo-bar] foo-bar +status-format[xterm-256color] xterm +status-format[zoom] zoom" +check_value "-gv status-format[01]" "one" +check_ok set -gu status-format[zoom] +check_value "-gv status-format[zoom]" "" # --- comma-only separator (user-keys) ------------------------------------- # @@ -133,11 +153,15 @@ user-keys[1] "Two Three"' # --- command-type array (a hook) ------------------------------------------ # -# Hooks are command arrays: an indexed value is parsed as a command when set +# Hooks are command arrays: a keyed value is parsed as a command when set # and re-printed from the parsed command list; a syntax error is reported. check_ok set -g alert-bell[0] "display-message hi" check_value "-gv alert-bell[0]" "display-message hi" check_fail "syntax error" set -g alert-bell[0] "if -x {" +check_ok set-hook -g window-renamed[notify] "display-message renamed" +check_value "-gv window-renamed[notify]" "display-message renamed" +check_ok set-hook -gu window-renamed[notify] +check_value "-gv window-renamed[notify]" "" # --- colour-type array ---------------------------------------------------- # @@ -146,11 +170,11 @@ check_ok set -w pane-colours[0] red check_value "-wv pane-colours[0]" "red" check_fail "bad colour: xxxyyy" set -w pane-colours[1] xxxyyy -# --- -o refuses to overwrite an already-set index ------------------------- +# --- -o refuses to overwrite an already-set key --------------------------- check_ok set -g command-alias[9] "x=list-keys" check_fail "already set: command-alias[9]" set -go command-alias[9] "y=list-keys" -# --- non-array option rejects index syntax -------------------------------- +# --- non-array option rejects key syntax ---------------------------------- # # status-left is a plain string; indexing it is an error. check_fail "not an array: status-left[0]" set -g status-left[0] "x" From ff09914327fe36fe0f29e62b18c632856f682f65 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 6 Jul 2026 21:44:26 +0000 Subject: [PATCH 073/127] Could of minor tidy ups. --- layout.c | 6 +----- options.c | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/layout.c b/layout.c index 5e48610f9..41fd09196 100644 --- a/layout.c +++ b/layout.c @@ -62,11 +62,9 @@ layout_create_cell(struct layout_cell *lcparent) { struct layout_cell *lc; - lc = xmalloc(sizeof *lc); + lc = xcalloc(1, sizeof *lc); lc->type = LAYOUT_WINDOWPANE; - lc->flags = 0; lc->parent = lcparent; - TAILQ_INIT(&lc->cells); lc->sx = UINT_MAX; @@ -81,8 +79,6 @@ layout_create_cell(struct layout_cell *lcparent) lc->saved_xoff = INT_MAX; lc->saved_yoff = INT_MAX; - lc->wp = NULL; - return (lc); } diff --git a/options.c b/options.c index fd5f1b262..3b81ed57d 100644 --- a/options.c +++ b/options.c @@ -667,7 +667,7 @@ options_array_first(struct options_entry *o) struct options_array_item * options_array_next(struct options_array_item *a) { - return (RB_NEXT(options_array, &o->value.array, a)); + return (RB_NEXT(options_array, , a)); } const char * From af35bcb64ffcea9c49ec3d5c653fbd4e35d5a5e5 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 7 Jul 2026 07:43:07 +0000 Subject: [PATCH 074/127] Restore { and } to swap-pane and for floating panes and use a separate key table accessed by g (short for "gove and/or gesize") for floating panes. So "C-b g Up" moves the pane to top-centre, "C-b g M-Up" moves and resizes it. This means we not only do not lose the surprisingly popular { and } bindings, but we can have more than four keys for moving and resizing panes. --- key-bindings.c | 25 +++++++++++++++++++++---- tmux.1 | 12 ++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/key-bindings.c b/key-bindings.c index 641957103..077580d1c 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -410,10 +410,8 @@ key_bindings_init(void) "bind -N 'Choose a window from a list' w { choose-tree -Zw }", "bind -N 'Kill the active pane' x { confirm-before -p\"kill-pane #P? (y/n)\" kill-pane }", "bind -N 'Zoom the active pane' z { resize-pane -Z }", - "bind -N 'Move pane to top-left corner' '{' { resize-pane -x50% -y50%; move-pane -P top-left }", - "bind -N 'Move pane to top-right corner' '}' { resize-pane -x50% -y50%; move-pane -P top-right }", - "bind -N 'Move pane to bottom-left corner' 'M-{' { resize-pane -x50% -y50%; move-pane -P bottom-left }", - "bind -N 'Move pane to bottom-right corner' 'M-}' { resize-pane -x50% -y50%; move-pane -P bottom-right }", + "bind -N 'Swap the active pane with the pane above' '{' { swap-pane -U }", + "bind -N 'Swap the active pane with the pane below' '}' { swap-pane -D }", "bind -N 'Show messages' '~' { show-messages }", "bind -N 'Enter copy mode and scroll up' PPage { copy-mode -u }", "bind -N 'Select the pane above the active pane' -r Up { select-pane -U }", @@ -444,6 +442,25 @@ key_bindings_init(void) "bind -N 'Resize the pane left' -r C-Left if -F '#{?floating_pane_flag}' { resizep -R-1 } { resize-pane -L }", "bind -N 'Resize the pane right' -r C-Right { resize-pane -R }", + /* Floating pane movement. */ + "bind -N 'Move a floating pane' g { switch-client -Tmove }", + "bind -Tmove -N 'Move pane to top-left corner' 1 { move-pane -P top-left }", + "bind -Tmove -N 'Move pane to top-right corner' 2 { move-pane -P top-right }", + "bind -Tmove -N 'Move pane to bottom-left corner' 3 { move-pane -P bottom-left }", + "bind -Tmove -N 'Move pane to bottom-right corner' 4 { move-pane -P bottom-right }", + "bind -Tmove -N 'Move pane to top-left corner and resize' M-1 { resize-pane -x50% -y50%; move-pane -P top-left }", + "bind -Tmove -N 'Move pane to top-right corner and resize' M-2 { resize-pane -x50% -y50%; move-pane -P top-right }", + "bind -Tmove -N 'Move pane to bottom-left corner and resize' M-3 { resize-pane -x50% -y50%; move-pane -P bottom-left }", + "bind -Tmove -N 'Move pane to bottom-right corner and resize' M-4 { resize-pane -x50% -y50%; move-pane -P bottom-right }", + "bind -Tmove -N 'Move pane to top' 'Up' { move-pane -P top-centre }", + "bind -Tmove -N 'Move pane to bottom' 'Down' { move-pane -P bottom-centre }", + "bind -Tmove -N 'Move pane to left' 'Left' { move-pane -P centre-left }", + "bind -Tmove -N 'Move pane to right' 'Right' { move-pane -P centre-right }", + "bind -Tmove -N 'Move pane to top and resize' 'M-Up' { resizep -x100% -y50%; move-pane -P top-centre }", + "bind -Tmove -N 'Move pane to bottom and resize' 'M-Down' { resizep -x100% -y50%; move-pane -P bottom-centre }", + "bind -Tmove -N 'Move pane to left and resize' 'M-Left' { resizep -x50% -y100%; move-pane -P centre-left }", + "bind -Tmove -N 'Move pane to right and reize' 'M-Right' { resizep -x50% -y100%; move-pane -P centre-right }", + /* Menu keys */ "bind -N 'Display window menu' < { display-menu -xW -yW -T '#[align=centre]#{window_index}:#{window_name}' " DEFAULT_WINDOW_MENU " }", "bind -N 'Display pane menu' > { display-menu -xP -yP -T '#[align=centre]#{pane_index} (#{pane_id})' " DEFAULT_PANE_MENU " }", diff --git a/tmux.1 b/tmux.1 index 03f653dd9..b8405057b 100644 --- a/tmux.1 +++ b/tmux.1 @@ -363,13 +363,13 @@ Toggle zoom state of the current pane. .It Tab Choose a new window and session by fuzzy matching. .It { -Move floating pane to top-left corner. +Swap the current pane with the previous pane. .It } -Move floating pane to top-right corner. -.It M-{ -Move floating pane to bottom-left corner. -.It M-} -Move floating pane to bottom-right corner. +Swap the current pane with the next pane. +.It g 1, 2, 3, 4 +.It g Up, Down, Left, Right +Move floating pane to corners or edges. +Meta resizes the pane as well. .It \[ti] Show previous messages from .Nm , From 1ea574e4caeddcf69a778b99fcb5661f3cf23c46 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 7 Jul 2026 07:47:03 +0000 Subject: [PATCH 075/127] Set initial scrollbar state, from Michael Grant. --- key-bindings.c | 2 +- window.c | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/key-bindings.c b/key-bindings.c index 077580d1c..9b5197e4e 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -459,7 +459,7 @@ key_bindings_init(void) "bind -Tmove -N 'Move pane to top and resize' 'M-Up' { resizep -x100% -y50%; move-pane -P top-centre }", "bind -Tmove -N 'Move pane to bottom and resize' 'M-Down' { resizep -x100% -y50%; move-pane -P bottom-centre }", "bind -Tmove -N 'Move pane to left and resize' 'M-Left' { resizep -x50% -y100%; move-pane -P centre-left }", - "bind -Tmove -N 'Move pane to right and reize' 'M-Right' { resizep -x50% -y100%; move-pane -P centre-right }", + "bind -Tmove -N 'Move pane to right and resize' 'M-Right' { resizep -x50% -y100%; move-pane -P centre-right }", /* Menu keys */ "bind -N 'Display window menu' < { display-menu -xW -yW -T '#[align=centre]#{window_index}:#{window_name}' " DEFAULT_WINDOW_MENU " }", diff --git a/window.c b/window.c index 8035e6b40..31abdd398 100644 --- a/window.c +++ b/window.c @@ -334,6 +334,8 @@ window_create(u_int sx, u_int sy, u_int xpixel, u_int ypixel) w->ypixel = ypixel; w->options = options_create(global_w_options); + w->sb = options_get_number(w->options, "pane-scrollbars"); + w->sb_pos = options_get_number(w->options, "pane-scrollbars-position"); w->references = 0; TAILQ_INIT(&w->winlinks); From 7188391a2283b506ee4ca50718406433abe18042 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 7 Jul 2026 08:07:26 +0000 Subject: [PATCH 076/127] Add a layout_geometry container struct, from Dane Jensen. --- cmd-break-pane.c | 8 +- cmd-join-pane.c | 32 +++--- cmd-resize-pane.c | 38 +++---- layout-custom.c | 38 +++---- layout-set.c | 16 +-- layout.c | 274 +++++++++++++++++++++++----------------------- resize.c | 10 +- spawn.c | 12 +- tmux.h | 41 ++++--- 9 files changed, 234 insertions(+), 235 deletions(-) diff --git a/cmd-break-pane.c b/cmd-break-pane.c index 7eb371c25..3891534a0 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -51,10 +51,9 @@ cmd_break_pane_float(struct cmdq_item *item, struct args *args, struct window *w, struct window_pane *wp) { struct layout_cell *lc = wp->layout_cell; - u_int sx = lc->saved_sx, sy = lc->saved_sy; - int ox = lc->saved_xoff, oy = lc->saved_yoff; char *cause = NULL; enum pane_lines lines = window_get_pane_lines(w); + struct layout_geometry *fg = &lc->fg; if (window_pane_is_floating(wp)) { cmdq_error(item, "pane is already floating"); @@ -65,14 +64,13 @@ cmd_break_pane_float(struct cmdq_item *item, struct args *args, return (CMD_RETURN_ERROR); } - if (layout_floating_args_parse(item, args, lines, w, &sx, &sy, &ox, &oy, - &cause) != 0) { + if (layout_floating_args_parse(item, args, lines, w, fg, &cause) != 0) { cmdq_error(item, "failed to float pane: %s", cause); free(cause); return (CMD_RETURN_ERROR); } layout_remove_tile(w, lc); - layout_set_size(lc, sx, sy, ox, oy); + layout_set_size(lc, fg->sx, fg->sy, fg->xoff, fg->yoff); lc->flags |= LAYOUT_CELL_FLOATING; TAILQ_REMOVE(&w->z_index, wp, zentry); diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 9e67f1187..122505d81 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -72,8 +72,9 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl, struct window *w = wl->window; struct layout_cell *lc = wp->layout_cell; struct window_pane *owp; - int wx = w->sx, wy = w->sy, px = lc->sx; - int py = lc->sy, xoff = lc->xoff, yoff = lc->yoff; + int wx = w->sx, wy = w->sy; + int px = lc->g.sx, py = lc->g.sy; + int xoff = lc->g.xoff, yoff = lc->g.yoff; int border = 1; if (window_pane_get_pane_lines(wp) == PANE_LINES_NONE) @@ -181,9 +182,9 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl, return (CMD_RETURN_ERROR); } - if (xoff != lc->xoff || yoff != lc->yoff) { - lc->xoff = xoff; - lc->yoff = yoff; + if (xoff != lc->g.xoff || yoff != lc->g.yoff) { + lc->g.xoff = xoff; + lc->g.yoff = yoff; layout_fix_panes(w, NULL); } notify_window("window-layout-changed", w); @@ -201,7 +202,7 @@ cmd_join_pane_move(struct cmdq_item *item, struct args *args, const char *errstr, *argval; const char flags[] = { 'U', 'D', 'L', 'R' }; char *cause = NULL, flag; - int xoff = lc->xoff, yoff = lc->yoff, adjust; + int xoff = lc->g.xoff, yoff = lc->g.yoff, adjust; u_int i; enum pane_lines lines = window_pane_get_pane_lines(wp); @@ -252,9 +253,9 @@ cmd_join_pane_move(struct cmdq_item *item, struct args *args, xoff += adjust; } - if (xoff != lc->xoff || yoff != lc->yoff) { - lc->xoff = xoff; - lc->yoff = yoff; + if (xoff != lc->g.xoff || yoff != lc->g.yoff) { + lc->g.xoff = xoff; + lc->g.yoff = yoff; layout_fix_panes(w, NULL); notify_window("window-layout-changed", w); server_redraw_window(w); @@ -320,8 +321,8 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m) ly = m->statusat - 1; if (x != lx || y != ly) { - lc->xoff += x - lx; - lc->yoff += y - ly; + lc->g.xoff += x - lx; + lc->g.yoff += y - ly; layout_fix_panes(w, NULL); server_redraw_window(w); server_redraw_window_borders(w); @@ -379,10 +380,11 @@ cmd_join_pane_tile(struct cmdq_item *item, struct args *args, struct window *w, return (CMD_RETURN_ERROR); } - lc->saved_sx = lc->sx; - lc->saved_sy = lc->sy; - lc->saved_xoff = lc->xoff; - lc->saved_yoff = lc->yoff; + lc->fg.sx = lc->g.sx; + lc->fg.sy = lc->g.sy; + lc->fg.xoff = lc->g.xoff; + lc->fg.yoff = lc->g.yoff; + if (layout_insert_tile(w, lc) != 0) { cmdq_error(item, "no space for a new pane"); return (CMD_RETURN_ERROR); diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index 348923528..3ce805115 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -270,10 +270,10 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c, if ((lx == left || lx == left + 1) && ly == wp->yoff - 1) { /* Top left corner. */ - new_sx = lc->sx + (lx - x); + new_sx = lc->g.sx + (lx - x); if (new_sx < PANE_MINIMUM) new_sx = PANE_MINIMUM; - new_sy = lc->sy + (ly - y); + new_sy = lc->g.sy + (ly - y); if (new_sy < PANE_MINIMUM) new_sy = PANE_MINIMUM; new_xoff = x + 1; /* because mouse is on border at xoff - 1 */ @@ -283,65 +283,65 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c, } else if ((lx == right + 1 || lx == right) && ly == wp->yoff - 1) { /* Top right corner. */ - new_sx = x - lc->xoff; + new_sx = x - lc->g.xoff; if (new_sx < PANE_MINIMUM) new_sx = PANE_MINIMUM; - new_sy = lc->sy + (ly - y); + new_sy = lc->g.sy + (ly - y); if (new_sy < PANE_MINIMUM) new_sy = PANE_MINIMUM; new_yoff = y + 1; - layout_set_size(lc, new_sx, new_sy, lc->xoff, new_yoff); + layout_set_size(lc, new_sx, new_sy, lc->g.xoff, new_yoff); resizes++; } else if ((lx == left || lx == left + 1) && ly == wp->yoff + sy) { /* Bottom left corner. */ - new_sx = lc->sx + (lx - x); + new_sx = lc->g.sx + (lx - x); if (new_sx < PANE_MINIMUM) new_sx = PANE_MINIMUM; - new_sy = y - lc->yoff; + new_sy = y - lc->g.yoff; if (new_sy < PANE_MINIMUM) return; new_xoff = x + 1; - layout_set_size(lc, new_sx, new_sy, new_xoff, lc->yoff); + layout_set_size(lc, new_sx, new_sy, new_xoff, lc->g.yoff); resizes++; } else if ((lx == right + 1 || lx == right) && ly == wp->yoff + sy) { /* Bottom right corner. */ - new_sx = x - lc->xoff; + new_sx = x - lc->g.xoff; if (new_sx < PANE_MINIMUM) new_sx = PANE_MINIMUM; - new_sy = y - lc->yoff; + new_sy = y - lc->g.yoff; if (new_sy < PANE_MINIMUM) new_sy = PANE_MINIMUM; - layout_set_size(lc, new_sx, new_sy, lc->xoff, lc->yoff); + layout_set_size(lc, new_sx, new_sy, lc->g.xoff, lc->g.yoff); resizes++; } else if (lx == right) { /* Right border. */ - new_sx = x - lc->xoff; + new_sx = x - lc->g.xoff; if (new_sx < PANE_MINIMUM) return; - layout_set_size(lc, new_sx, lc->sy, lc->xoff, lc->yoff); + layout_set_size(lc, new_sx, lc->g.sy, lc->g.xoff, lc->g.yoff); resizes++; } else if (lx == left) { /* Left border. */ - new_sx = lc->sx + (lx - x); + new_sx = lc->g.sx + (lx - x); if (new_sx < PANE_MINIMUM) return; new_xoff = x + 1; - layout_set_size(lc, new_sx, lc->sy, new_xoff, lc->yoff); + layout_set_size(lc, new_sx, lc->g.sy, new_xoff, lc->g.yoff); resizes++; } else if (ly == wp->yoff + sy) { /* Bottom border. */ - new_sy = y - lc->yoff; + new_sy = y - lc->g.yoff; if (new_sy < PANE_MINIMUM) return; - layout_set_size(lc, lc->sx, new_sy, lc->xoff, lc->yoff); + layout_set_size(lc, lc->g.sx, new_sy, lc->g.xoff, lc->g.yoff); resizes++; } else if (ly == wp->yoff - 1) { /* Top border (move instead of resize). */ - new_xoff = lc->xoff + (x - lx); + new_xoff = lc->g.xoff + (x - lx); new_yoff = y + 1; - layout_set_size(lc, lc->sx, lc->sy, new_xoff, new_yoff); + layout_set_size(lc, lc->g.sx, lc->g.sy, new_xoff, new_yoff); resizes++; } if (resizes != 0) { diff --git a/layout-custom.c b/layout-custom.c index c5a348ac0..65fb3780b 100644 --- a/layout-custom.c +++ b/layout-custom.c @@ -101,10 +101,10 @@ layout_append(struct layout_cell *lc, char *buf, size_t len) return (0); if (lc->wp != NULL) { tmplen = xsnprintf(tmp, sizeof tmp, "%ux%u,%d,%d,%u", - lc->sx, lc->sy, lc->xoff, lc->yoff, lc->wp->id); + lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff, lc->wp->id); } else { tmplen = xsnprintf(tmp, sizeof tmp, "%ux%u,%d,%d", - lc->sx, lc->sy, lc->xoff, lc->yoff); + lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff); } if (tmplen > (sizeof tmp) - 1) return (-1); @@ -145,24 +145,24 @@ layout_check(struct layout_cell *lc) break; case LAYOUT_LEFTRIGHT: TAILQ_FOREACH(lcchild, &lc->cells, entry) { - if (lcchild->sy != lc->sy) + if (lcchild->g.sy != lc->g.sy) return (0); if (!layout_check(lcchild)) return (0); - n += lcchild->sx + 1; + n += lcchild->g.sx + 1; } - if (n - 1 != lc->sx) + if (n - 1 != lc->g.sx) return (0); break; case LAYOUT_TOPBOTTOM: TAILQ_FOREACH(lcchild, &lc->cells, entry) { - if (lcchild->sx != lc->sx) + if (lcchild->g.sx != lc->g.sx) return (0); if (!layout_check(lcchild)) return (0); - n += lcchild->sy + 1; + n += lcchild->g.sy + 1; } - if (n - 1 != lc->sy) + if (n - 1 != lc->g.sy) return (0); break; } @@ -237,21 +237,21 @@ layout_parse(struct window *w, const char *layout, char **cause) break; case LAYOUT_LEFTRIGHT: TAILQ_FOREACH(lcchild, &tiled_lc->cells, entry) { - sy = lcchild->sy + 1; - sx += lcchild->sx + 1; + sy = lcchild->g.sy + 1; + sx += lcchild->g.sx + 1; } break; case LAYOUT_TOPBOTTOM: TAILQ_FOREACH(lcchild, &tiled_lc->cells, entry) { - sx = lcchild->sx + 1; - sy += lcchild->sy + 1; + sx = lcchild->g.sx + 1; + sy += lcchild->g.sy + 1; } break; } if (tiled_lc->type != LAYOUT_WINDOWPANE && - (tiled_lc->sx != sx || tiled_lc->sy != sy)) { + (tiled_lc->g.sx != sx || tiled_lc->g.sy != sy)) { layout_print_cell(tiled_lc, __func__, 0); - tiled_lc->sx = sx - 1; tiled_lc->sy = sy - 1; + tiled_lc->g.sx = sx - 1; tiled_lc->g.sy = sy - 1; } /* Check the new layout. */ @@ -262,7 +262,7 @@ layout_parse(struct window *w, const char *layout, char **cause) /* Resize window to the layout size. */ if (sx != 0 && sy != 0) - window_resize(w, tiled_lc->sx, tiled_lc->sy, -1, -1); + window_resize(w, tiled_lc->g.sx, tiled_lc->g.sy, -1, -1); /* Destroy the old layout and swap to the new. */ layout_free_cell(w->layout_root, 0); @@ -358,10 +358,10 @@ layout_construct_cell(struct layout_cell *lcparent, const char **layout) } lc = layout_create_cell(lcparent); - lc->sx = sx; - lc->sy = sy; - lc->xoff = xoff; - lc->yoff = yoff; + lc->g.sx = sx; + lc->g.sy = sy; + lc->g.xoff = xoff; + lc->g.yoff = yoff; return (lc); } diff --git a/layout-set.c b/layout-set.c index cf78585c2..2f164b01c 100644 --- a/layout-set.c +++ b/layout-set.c @@ -170,8 +170,8 @@ layout_set_even(struct window *w, enum layout_type type) TAILQ_INSERT_TAIL(&lcroot->cells, lcchild, entry); lcchild->parent = lcroot; if (layout_cell_is_tiled(lcchild)) { - lcchild->sx = w->sx; - lcchild->sy = w->sy; + lcchild->g.sx = w->sx; + lcchild->g.sy = w->sy; } } @@ -182,7 +182,7 @@ layout_set_even(struct window *w, enum layout_type type) layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lcroot->sx, lcroot->sy, -1, -1); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } @@ -291,7 +291,7 @@ layout_set_main_h(struct window *w) layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lcroot->sx, lcroot->sy, -1, -1); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } @@ -388,7 +388,7 @@ layout_set_main_h_mirrored(struct window *w) layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lcroot->sx, lcroot->sy, -1, -1); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } @@ -485,7 +485,7 @@ layout_set_main_v(struct window *w) layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lcroot->sx, lcroot->sy, -1, -1); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } @@ -583,7 +583,7 @@ layout_set_main_v_mirrored(struct window *w) layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lcroot->sx, lcroot->sy, -1, -1); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } @@ -705,7 +705,7 @@ layout_set_tiled(struct window *w) layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lcroot->sx, lcroot->sy, -1, -1); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } diff --git a/layout.c b/layout.c index 41fd09196..170f3c5fb 100644 --- a/layout.c +++ b/layout.c @@ -56,6 +56,16 @@ static int layout_set_size_check(struct window *, struct layout_cell *, static void layout_resize_child_cells(struct window *, struct layout_cell *); +/* Initializes cell geometry to sentinel values. */ +static void +layout_geometry_init(struct layout_geometry *lg) +{ + lg->sx = UINT_MAX; + lg->sy = UINT_MAX; + lg->xoff = INT_MAX; + lg->yoff = INT_MAX; +} + /* Create a new layout cell. */ struct layout_cell * layout_create_cell(struct layout_cell *lcparent) @@ -67,17 +77,8 @@ layout_create_cell(struct layout_cell *lcparent) lc->parent = lcparent; TAILQ_INIT(&lc->cells); - lc->sx = UINT_MAX; - lc->sy = UINT_MAX; - - lc->xoff = INT_MAX; - lc->yoff = INT_MAX; - - lc->saved_sx = UINT_MAX; - lc->saved_sy = UINT_MAX; - - lc->saved_xoff = INT_MAX; - lc->saved_yoff = INT_MAX; + layout_geometry_init(&lc->g); + layout_geometry_init(&lc->fg); return (lc); } @@ -140,8 +141,8 @@ layout_print_cell(struct layout_cell *lc, const char *hdr, u_int n) break; } log_debug("%s:%*s%p type %s [parent %p] wp=%p [%d,%d %ux%u]", hdr, n, - " ", lc, type, lc->parent, lc->wp, lc->xoff, lc->yoff, lc->sx, - lc->sy); + " ", lc, type, lc->parent, lc->wp, lc->g.xoff, lc->g.yoff, lc->g.sx, + lc->g.sy); switch (lc->type) { case LAYOUT_LEFTRIGHT: case LAYOUT_TOPBOTTOM: @@ -160,10 +161,10 @@ layout_search_by_border(struct layout_cell *lc, u_int x, u_int y) struct layout_cell *lcchild, *last = NULL; TAILQ_FOREACH(lcchild, &lc->cells, entry) { - if ((int)x >= lcchild->xoff && - (int)x < lcchild->xoff + (int)lcchild->sx && - (int)y >= lcchild->yoff && - (int)y < lcchild->yoff + (int)lcchild->sy) { + if ((int)x >= lcchild->g.xoff && + (int)x < lcchild->g.xoff + (int)lcchild->g.sx && + (int)y >= lcchild->g.yoff && + (int)y < lcchild->g.yoff + (int)lcchild->g.sy) { /* Inside the cell - recurse. */ return (layout_search_by_border(lcchild, x, y)); } @@ -175,13 +176,13 @@ layout_search_by_border(struct layout_cell *lc, u_int x, u_int y) switch (lc->type) { case LAYOUT_LEFTRIGHT: - if ((int)x < lcchild->xoff && - (int)x >= last->xoff + (int)last->sx) + if ((int)x < lcchild->g.xoff && + (int)x >= last->g.xoff + (int)last->g.sx) return (last); break; case LAYOUT_TOPBOTTOM: - if ((int)y < lcchild->yoff && - (int)y >= last->yoff + (int)last->sy) + if ((int)y < lcchild->g.yoff && + (int)y >= last->g.yoff + (int)last->g.sy) return (last); break; case LAYOUT_WINDOWPANE: @@ -198,11 +199,11 @@ layout_search_by_border(struct layout_cell *lc, u_int x, u_int y) void layout_set_size(struct layout_cell *lc, u_int sx, u_int sy, int xoff, int yoff) { - lc->sx = sx; - lc->sy = sy; + lc->g.sx = sx; + lc->g.sy = sy; - lc->xoff = xoff; - lc->yoff = yoff; + lc->g.xoff = xoff; + lc->g.yoff = yoff; } /* Make a cell a leaf cell. */ @@ -327,28 +328,28 @@ layout_fix_offsets1(struct layout_cell *lc) int xoff, yoff; if (lc->type == LAYOUT_LEFTRIGHT) { - xoff = lc->xoff; + xoff = lc->g.xoff; TAILQ_FOREACH(lcchild, &lc->cells, entry) { if (!layout_cell_is_tiled(lcchild) && !layout_cell_has_tiled_child(lcchild)) continue; - lcchild->xoff = xoff; - lcchild->yoff = lc->yoff; + lcchild->g.xoff = xoff; + lcchild->g.yoff = lc->g.yoff; if (lcchild->type != LAYOUT_WINDOWPANE) layout_fix_offsets1(lcchild); - xoff += lcchild->sx + 1; + xoff += lcchild->g.sx + 1; } } else { - yoff = lc->yoff; + yoff = lc->g.yoff; TAILQ_FOREACH(lcchild, &lc->cells, entry) { if (!layout_cell_is_tiled(lcchild) && !layout_cell_has_tiled_child(lcchild)) continue; - lcchild->xoff = lc->xoff; - lcchild->yoff = yoff; + lcchild->g.xoff = lc->g.xoff; + lcchild->g.yoff = yoff; if (lcchild->type != LAYOUT_WINDOWPANE) layout_fix_offsets1(lcchild); - yoff += lcchild->sy + 1; + yoff += lcchild->g.sy + 1; } } } @@ -363,8 +364,8 @@ layout_fix_offsets(struct window *w) if (lc->flags & LAYOUT_CELL_FLOATING) return; - lc->xoff = 0; - lc->yoff = 0; + lc->g.xoff = 0; + lc->g.yoff = 0; layout_fix_offsets1(lc); } @@ -448,10 +449,10 @@ layout_fix_panes(struct window *w, struct window_pane *skip) old_sx = wp->sx; old_sy = wp->sy; - wp->xoff = lc->xoff; - wp->yoff = lc->yoff; - sx = lc->sx; - sy = lc->sy; + wp->xoff = lc->g.xoff; + wp->yoff = lc->g.yoff; + sx = lc->g.sx; + sy = lc->g.sy; if (!window_pane_is_floating(wp) && layout_add_horizontal_border(w, lc, status)) { @@ -532,14 +533,14 @@ layout_resize_check(struct window *w, struct layout_cell *lc, if (lc->type == LAYOUT_WINDOWPANE) { /* Space available in this cell only. */ if (type == LAYOUT_LEFTRIGHT) { - available = lc->sx; + available = lc->g.sx; if (w->sb == PANE_SCROLLBARS_ALWAYS) minimum = PANE_MINIMUM + sb_style->width + sb_style->pad; else minimum = PANE_MINIMUM; } else { - available = lc->sy; + available = lc->g.sy; if (layout_add_horizontal_border(w, lc, status)) minimum = PANE_MINIMUM + 1; else @@ -581,9 +582,9 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, /* Adjust the cell size. */ if (type == LAYOUT_LEFTRIGHT) - lc->sx += change; + lc->g.sx += change; else - lc->sy += change; + lc->g.sy += change; /* If this is a leaf cell, that is all that is necessary. */ if (type == LAYOUT_WINDOWPANE) @@ -643,9 +644,9 @@ layout_resize_set_size(struct window *w, struct layout_cell *lc, int change; if (type == LAYOUT_LEFTRIGHT) - change = size - lc->sx; + change = size - lc->g.sx; else - change = size - lc->sy; + change = size - lc->g.sy; layout_resize_adjust(w, lc, type, change); } @@ -719,9 +720,9 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc, lcother = layout_cell_get_neighbour(lc); if (lcother != NULL) { if (lcparent->type == LAYOUT_LEFTRIGHT) - change = lc->sx + 1; + change = lc->g.sx + 1; else - change = lc->sy + 1; + change = lc->g.sy + 1; layout_resize_adjust(w, lcother, lcparent->type, change); } else layout_remove_tile(w, lcparent); @@ -742,8 +743,8 @@ out: lc->parent = lcparent->parent; if (lc->parent == NULL) { if (layout_cell_is_tiled(lc)) { - lc->xoff = 0; - lc->yoff = 0; + lc->g.xoff = 0; + lc->g.yoff = 0; } *lcroot = lc; } else @@ -794,29 +795,29 @@ layout_resize(struct window *w, u_int sx, u_int sy) */ if (lc->type == LAYOUT_WINDOWPANE && (lc->flags & LAYOUT_CELL_FLOATING)) return; - xchange = sx - lc->sx; + xchange = sx - lc->g.sx; xlimit = layout_resize_check(w, lc, LAYOUT_LEFTRIGHT); if (xchange < 0 && xchange < -xlimit) xchange = -xlimit; if (xlimit == 0) { - if (sx <= lc->sx) /* lc->sx is minimum possible */ + if (sx <= lc->g.sx) /* lc->g.sx is minimum possible */ xchange = 0; else - xchange = sx - lc->sx; + xchange = sx - lc->g.sx; } if (xchange != 0) layout_resize_adjust(w, lc, LAYOUT_LEFTRIGHT, xchange); /* Adjust vertically in a similar fashion. */ - ychange = sy - lc->sy; + ychange = sy - lc->g.sy; ylimit = layout_resize_check(w, lc, LAYOUT_TOPBOTTOM); if (ychange < 0 && ychange < -ylimit) ychange = -ylimit; if (ylimit == 0) { - if (sy <= lc->sy) /* lc->sy is minimum possible */ + if (sy <= lc->g.sy) /* lc->g.sy is minimum possible */ ychange = 0; else - ychange = sy - lc->sy; + ychange = sy - lc->g.sy; } if (ychange != 0) layout_resize_adjust(w, lc, LAYOUT_TOPBOTTOM, ychange); @@ -847,9 +848,9 @@ layout_resize_pane_to(struct window_pane *wp, enum layout_type type, /* Work out the size adjustment. */ if (type == LAYOUT_LEFTRIGHT) - size = lc->sx; + size = lc->g.sx; else - size = lc->sy; + size = lc->g.sy; if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) change = size - new_size; else @@ -880,13 +881,13 @@ layout_resize_floating_pane_to(struct window_pane *wp, enum layout_type type, } if (type == LAYOUT_TOPBOTTOM) { - if (lc->sy == size) + if (lc->g.sy == size) return (0); - lc->sy = size; + lc->g.sy = size; } else { - if (lc->sx == size) + if (lc->g.sx == size) return (0); - lc->sx = size; + lc->g.sx = size; } redraw_invalidate_scene(wp->window); return (0); @@ -908,23 +909,23 @@ layout_resize_floating_pane(struct window_pane *wp, enum layout_type type, return (0); if (type == LAYOUT_TOPBOTTOM) { - size = lc->sy + change; + size = lc->g.sy + change; if (size < PANE_MINIMUM || size > PANE_MAXIMUM) { *cause = xstrdup("change is too big or too small"); return (-1); } - lc->sy = size; + lc->g.sy = size; if (opposite) - lc->yoff -= change; + lc->g.yoff -= change; } else { - size = lc->sx + change; + size = lc->g.sx + change; if (size < PANE_MINIMUM || size > PANE_MAXIMUM) { *cause = xstrdup("change is too big or too small"); return (-1); } - lc->sx = size; + lc->g.sx = size; if (opposite) - lc->xoff -= change; + lc->g.xoff -= change; } redraw_invalidate_scene(wp->window); return (0); @@ -1090,13 +1091,13 @@ layout_new_pane_size(struct window *w, u_int previous, struct layout_cell *lc, */ min = (PANE_MINIMUM + 1) * (count_left - 1); if (type == LAYOUT_LEFTRIGHT) { - if (lc->sx - available > min) - min = lc->sx - available; - new_size = (lc->sx * size) / previous; + if (lc->g.sx - available > min) + min = lc->g.sx - available; + new_size = (lc->g.sx * size) / previous; } else { - if (lc->sy - available > min) - min = lc->sy - available; - new_size = (lc->sy * size) / previous; + if (lc->g.sy - available > min) + min = lc->g.sy - available; + new_size = (lc->g.sy * size) / previous; } /* Check against the maximum and minimum size. */ @@ -1132,9 +1133,9 @@ layout_set_size_check(struct window *w, struct layout_cell *lc, return (0); if (type == LAYOUT_LEFTRIGHT) - previous = lc->sx; + previous = lc->g.sx; else - previous = lc->sy; + previous = lc->g.sy; idx = 0; TAILQ_FOREACH(lcchild, &lc->cells, entry) { @@ -1170,32 +1171,32 @@ static void layout_resize_child_cells(struct window *w, struct layout_cell *lc) { struct layout_cell *lcchild; - u_int previous, available, count, idx; + u_int prev, available, count, idx; if (lc->type == LAYOUT_WINDOWPANE) return; /* What is the current size used? */ count = 0; - previous = 0; + prev = 0; TAILQ_FOREACH(lcchild, &lc->cells, entry) { if (!layout_cell_is_tiled(lcchild) && !layout_cell_has_tiled_child(lcchild)) continue; count++; if (lc->type == LAYOUT_LEFTRIGHT) - previous += lcchild->sx; + prev += lcchild->g.sx; else if (lc->type == LAYOUT_TOPBOTTOM) - previous += lcchild->sy; + prev += lcchild->g.sy; } - previous += (count - 1); + prev += (count - 1); /* And how much is available? */ available = 0; if (lc->type == LAYOUT_LEFTRIGHT) - available = lc->sx; + available = lc->g.sx; else if (lc->type == LAYOUT_TOPBOTTOM) - available = lc->sy; + available = lc->g.sy; /* Resize children into the new size. */ idx = 0; @@ -1204,20 +1205,20 @@ layout_resize_child_cells(struct window *w, struct layout_cell *lc) !layout_cell_has_tiled_child(lcchild)) continue; if (lc->type == LAYOUT_TOPBOTTOM) { - lcchild->sx = lc->sx; - lcchild->xoff = lc->xoff; + lcchild->g.sx = lc->g.sx; + lcchild->g.xoff = lc->g.xoff; } else { - lcchild->sx = layout_new_pane_size(w, previous, lcchild, - lc->type, lc->sx, count - idx, available); - available -= (lcchild->sx + 1); + lcchild->g.sx = layout_new_pane_size(w, prev, lcchild, + lc->type, lc->g.sx, count - idx, available); + available -= (lcchild->g.sx + 1); } if (lc->type == LAYOUT_LEFTRIGHT) { - lcchild->sy = lc->sy; - lcchild->yoff = lc->yoff; + lcchild->g.sy = lc->g.sy; + lcchild->g.yoff = lc->g.yoff; } else { - lcchild->sy = layout_new_pane_size(w, previous, lcchild, - lc->type, lc->sy, count - idx, available); - available -= (lcchild->sy + 1); + lcchild->g.sy = layout_new_pane_size(w, prev, lcchild, + lc->type, lc->g.sy, count - idx, available); + available -= (lcchild->g.sy + 1); } layout_resize_child_cells(w, lcchild); idx++; @@ -1237,7 +1238,7 @@ layout_replace_with_node(struct window *w, struct layout_cell *lc, lcparent = layout_create_cell(lc->parent); layout_make_node(lcparent, type); - layout_set_size(lcparent, lc->sx, lc->sy, lc->xoff, lc->yoff); + layout_set_size(lcparent, lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff); if (lc->parent == NULL) w->layout_root = lcparent; else @@ -1256,7 +1257,7 @@ layout_split_check_space(struct window_pane *wp, struct layout_cell *lc, enum layout_type type) { struct style *sb_style = &wp->scrollbar_style; - u_int minimum, sx = lc->sx, sy = lc->sy; + u_int minimum, sx = lc->g.sx, sy = lc->g.sy; int status; if (lc->flags & LAYOUT_CELL_FLOATING) @@ -1295,7 +1296,7 @@ layout_split_sizes(struct layout_cell *lc, int size, int before, enum layout_type type, u_int *size1, u_int *size2, u_int *saved_size) { u_int s1, s2, ss; - u_int sx = lc->sx, sy = lc->sy; + u_int sx = lc->g.sx, sy = lc->g.sy; if (type == LAYOUT_LEFTRIGHT) ss = sx; @@ -1342,10 +1343,10 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, lc = wp->layout_cell; /* Copy the old cell size. */ - sx = lc->sx; - sy = lc->sy; - xoff = lc->xoff; - yoff = lc->yoff; + sx = lc->g.sx; + sy = lc->g.sy; + xoff = lc->g.xoff; + yoff = lc->g.yoff; /* Check there is enough space for the two new panes. */ if (!layout_split_check_space(wp, lc, type)) @@ -1386,13 +1387,13 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, * must be resized before inserting the new cell. */ if (lc->type == LAYOUT_LEFTRIGHT) { - lc->sx = new_size; + lc->g.sx = new_size; layout_resize_child_cells(wp->window, lc); - lc->sx = saved_size; + lc->g.sx = saved_size; } else if (lc->type == LAYOUT_TOPBOTTOM) { - lc->sy = new_size; + lc->g.sy = new_size; layout_resize_child_cells(wp->window, lc); - lc->sy = saved_size; + lc->g.sy = saved_size; } resize_first = 1; @@ -1436,10 +1437,10 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, */ if (!resize_first && type == LAYOUT_LEFTRIGHT) { layout_set_size(lc1, size1, sy, xoff, yoff); - layout_set_size(lc2, size2, sy, xoff + lc1->sx + 1, yoff); + layout_set_size(lc2, size2, sy, xoff + lc1->g.sx + 1, yoff); } else if (!resize_first && type == LAYOUT_TOPBOTTOM) { layout_set_size(lc1, sx, size1, xoff, yoff); - layout_set_size(lc2, sx, size2, xoff, yoff + lc1->sy + 1); + layout_set_size(lc2, sx, size2, xoff, yoff + lc1->g.sy + 1); } if (full_size) { if (!resize_first) @@ -1456,8 +1457,8 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, * layout_assign_pane before much else happens! */ struct layout_cell * -layout_floating_pane(struct window *w, struct window_pane *wp, u_int sx, - u_int sy, int ox, int oy) +layout_floating_pane(struct window *w, struct window_pane *wp, + struct layout_geometry *lg) { struct layout_cell *lc, *lcnew, *lcparent; @@ -1469,16 +1470,16 @@ layout_floating_pane(struct window *w, struct window_pane *wp, u_int sx, if (lcparent == NULL) { /* - * Adding a pane to a root that isn't node. Must create and - * insert a new root. - */ + * Adding a pane to a root that isn't a node. Must create and + * insert a new root. + */ lcparent = layout_replace_with_node(w, lc, LAYOUT_TOPBOTTOM); } lcnew = layout_create_cell(lcparent); TAILQ_INSERT_AFTER(&lcparent->cells, lc, lcnew, entry); lcnew->flags |= LAYOUT_CELL_FLOATING; - layout_set_size(lcnew, sx, sy, ox, oy); + layout_set_size(lcnew, lg->sx, lg->sy, lg->xoff, lg->yoff); return (lcnew); } @@ -1521,12 +1522,12 @@ layout_spread_cell(struct window *w, struct layout_cell *parent) status = window_get_pane_status(w); if (parent->type == LAYOUT_LEFTRIGHT) - size = parent->sx; + size = parent->g.sx; else if (parent->type == LAYOUT_TOPBOTTOM) { if (layout_add_horizontal_border(w, parent, status)) - size = parent->sy - 1; + size = parent->g.sy - 1; else - size = parent->sy; + size = parent->g.sy; } else return (0); if (size < number - 1) @@ -1547,7 +1548,7 @@ layout_spread_cell(struct window *w, struct layout_cell *parent) continue; change = 0; if (parent->type == LAYOUT_LEFTRIGHT) { - change = each - (int)lc->sx; + change = each - (int)lc->g.sx; if (remainder > 0) { change++; remainder--; @@ -1562,7 +1563,7 @@ layout_spread_cell(struct window *w, struct layout_cell *parent) this++; remainder--; } - change = this - (int)lc->sy; + change = this - (int)lc->g.sy; layout_resize_adjust(w, lc, LAYOUT_TOPBOTTOM, change); } if (change != 0) @@ -1659,30 +1660,29 @@ layout_get_floating_cell(struct cmdq_item *item, struct args *args, char **cause) { struct layout_cell *lcnew; - u_int sx = UINT_MAX, sy = UINT_MAX; - int ox = INT_MAX, oy = INT_MAX; + struct layout_geometry fg; - if (layout_floating_args_parse(item, args, lines, w, &sx, &sy, &ox, &oy, - cause) != 0) + layout_geometry_init(&fg); + if (layout_floating_args_parse(item, args, lines, w, &fg, cause) != 0) return (NULL); window_push_zoom(wp->window, 1, args_has(args, 'Z')); - lcnew = layout_floating_pane(w, wp, sx, sy, ox, oy); + lcnew = layout_floating_pane(w, wp, &fg); return (lcnew); } int layout_floating_args_parse(struct cmdq_item *item, struct args *args, - enum pane_lines lines, struct window *w, u_int *sxp, u_int *syp, int *oxp, - int *oyp, char **cause) + enum pane_lines lines, struct window *w, struct layout_geometry *lg, + char **cause) { int sx, sy, ox, oy; char *error = NULL; - sx = *sxp == UINT_MAX ? w->sx / 2 : *sxp; - sy = *syp == UINT_MAX ? w->sy / 4 : *syp; - ox = *oxp == INT_MAX ? INT_MAX : *oxp; - oy = *oyp == INT_MAX ? INT_MAX : *oyp; + sx = lg->sx == UINT_MAX ? w->sx / 2 : lg->sx; + sy = lg->sy == UINT_MAX ? w->sy / 4 : lg->sy; + ox = lg->xoff; + oy = lg->yoff; if (args_has(args, 'x')) { sx = args_percentage_and_expand(args, 'x', 0, PANE_MAXIMUM, @@ -1759,10 +1759,10 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args, return (-1); } - *sxp = sx; - *syp = sy; - *oxp = ox; - *oyp = oy; + lg->sx = sx; + lg->sy = sy; + lg->xoff = ox; + lg->yoff = oy; return (0); } @@ -1791,9 +1791,9 @@ layout_remove_tile(struct window *w, struct layout_cell *lc) * neighbour. */ if (type == LAYOUT_TOPBOTTOM) - change = lc->sy + 1; + change = lc->g.sy + 1; else - change = lc->sx + 1; + change = lc->g.sx + 1; layout_resize_adjust(w, lcneighbour, type, change); } @@ -1839,9 +1839,9 @@ layout_insert_tile(struct window *w, struct layout_cell *lc) */ layout_insert_tile(w, lcparent); if (type == LAYOUT_LEFTRIGHT) - size1 = lcparent->sx; + size1 = lcparent->g.sx; else - size1 = lcparent->sy; + size1 = lcparent->g.sy; layout_resize_set_size(w, lc, type, size1); } else { /* @@ -1859,10 +1859,10 @@ layout_insert_tile(struct window *w, struct layout_cell *lc) /* Setting opposite of the 'split' size to that of the parent. */ if (lcparent->type == LAYOUT_LEFTRIGHT) { - size1 = lcparent->sy; + size1 = lcparent->g.sy; type = LAYOUT_TOPBOTTOM; } else { - size1 = lcparent->sx; + size1 = lcparent->g.sx; type = LAYOUT_LEFTRIGHT; } layout_resize_set_size(w, lc, type, size1); diff --git a/resize.c b/resize.c index 6cbc938dd..19a940d61 100644 --- a/resize.c +++ b/resize.c @@ -46,13 +46,13 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) layout_resize(w, sx, sy); /* Resize the window, it can be no smaller than the layout. */ - if (sx < w->layout_root->sx) - sx = w->layout_root->sx; - if (sy < w->layout_root->sy) - sy = w->layout_root->sy; + if (sx < w->layout_root->g.sx) + sx = w->layout_root->g.sx; + if (sy < w->layout_root->g.sy) + sy = w->layout_root->g.sy; window_resize(w, sx, sy, xpixel, ypixel); log_debug("%s: @%u resized to %ux%u; layout %ux%u", __func__, w->id, - sx, sy, w->layout_root->sx, w->layout_root->sy); + sx, sy, w->layout_root->g.sx, w->layout_root->g.sy); /* Restore the window zoom state. */ if (zoomed) diff --git a/spawn.c b/spawn.c index 9f0c87db9..3a211e3c5 100644 --- a/spawn.c +++ b/spawn.c @@ -612,13 +612,13 @@ spawn_editor(struct client *c, const char *buf, size_t len, struct window *w = wl->window; struct window_pane *wp; struct layout_cell *lc; + struct layout_geometry lg; struct environ *env; FILE *f; char *cmd, *cause = NULL; char path[] = _PATH_TMP "tmux.XXXXXXXX"; const char *editor; int fd; - u_int px, py, sx, sy; editor = options_get_string(global_options, "editor"); fd = mkstemp(path); @@ -642,12 +642,12 @@ spawn_editor(struct client *c, const char *buf, size_t len, es->cb = cb; es->arg = arg; - sx = w->sx * 9 / 10; - sy = w->sy * 9 / 10; - px = w->sx / 2 - sx / 2; - py = w->sy / 2 - sy / 2; + lg.sx = w->sx * 9 / 10; + lg.sy = w->sy * 9 / 10; + lg.xoff = w->sx / 2 - lg.sx / 2; + lg.yoff = w->sy / 2 - lg.sy / 2; window_push_zoom(w, 1, 0); - lc = layout_floating_pane(w, NULL, sx, sy, px, py); + lc = layout_floating_pane(w, NULL, &lg); if (lc == NULL) { spawn_editor_free(es); return (NULL); diff --git a/tmux.h b/tmux.h index e76bed7ab..179ed3091 100644 --- a/tmux.h +++ b/tmux.h @@ -1471,32 +1471,31 @@ enum layout_type { LAYOUT_WINDOWPANE }; +/* Layout cell sizes and position. */ +struct layout_geometry { + u_int sx; + u_int sy; + int xoff; + int yoff; +}; + /* Layout cells queue. */ TAILQ_HEAD(layout_cells, layout_cell); /* Layout cell. */ struct layout_cell { - enum layout_type type; + enum layout_type type; + int flags; #define LAYOUT_CELL_FLOATING 0x1 - int flags; - struct layout_cell *parent; + struct layout_cell *parent; - u_int sx; - u_int sy; + struct layout_geometry g; + struct layout_geometry fg; /* saved floating pane */ - int xoff; - int yoff; - - u_int saved_sx; - u_int saved_sy; - - int saved_xoff; - int saved_yoff; - - struct window_pane *wp; - struct layout_cells cells; + struct window_pane *wp; + struct layout_cells cells; TAILQ_ENTRY(layout_cell) entry; }; @@ -3669,7 +3668,7 @@ void layout_destroy_cell(struct window *, struct layout_cell *, void layout_resize_layout(struct window *, struct layout_cell *, enum layout_type, int, int); struct layout_cell *layout_search_by_border(struct layout_cell *, u_int, u_int); -void layout_set_size(struct layout_cell *, u_int, u_int, int, int); +void layout_set_size(struct layout_cell *, u_int, u_int, int, int); void layout_make_leaf(struct layout_cell *, struct window_pane *); void layout_make_node(struct layout_cell *, enum layout_type); void layout_fix_zindexes(struct window *, struct layout_cell *); @@ -3703,7 +3702,7 @@ struct layout_cell *layout_replace_with_node(struct window *, struct layout_cell *layout_split_pane(struct window_pane *, enum layout_type, int, int); struct layout_cell *layout_floating_pane(struct window *, struct window_pane *, - u_int, u_int, int, int); + struct layout_geometry *); void layout_close_pane(struct window_pane *); int layout_spread_cell(struct window *, struct layout_cell *); void layout_spread_out(struct window_pane *); @@ -3711,10 +3710,10 @@ struct layout_cell *layout_get_tiled_cell(struct cmdq_item *, struct args *, struct window *, struct window_pane *, int, char **); struct layout_cell *layout_get_floating_cell(struct cmdq_item *, struct args *, enum pane_lines, struct window *, struct window_pane *, - char **cause); + char **); int layout_floating_args_parse(struct cmdq_item *, struct args *, - enum pane_lines, struct window *, u_int *, u_int *, int *, - int *, char **); + enum pane_lines, struct window *, struct layout_geometry *, + char **); int layout_remove_tile(struct window *, struct layout_cell *); int layout_insert_tile(struct window *, struct layout_cell *); From 76b693aa818ad580140c7b54b81509bc66ee7c5d Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 7 Jul 2026 09:45:09 +0000 Subject: [PATCH 077/127] Add C-b g , and C-b g . for move and resize menus, also add to pane menu (and fix to make them work). --- cmd-display-menu.c | 8 ++++++++ key-bindings.c | 29 +++++++++++++++++++++++++++++ menu.c | 17 +++++++++++++++-- tmux.1 | 3 +++ tmux.h | 3 +++ 5 files changed, 58 insertions(+), 2 deletions(-) diff --git a/cmd-display-menu.c b/cmd-display-menu.c index 8dfbfa55a..97973b8bd 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -127,6 +127,10 @@ cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item, format_add(ft, "popup_mouse_y", "%u", event->m.y); } + /* Position of the previous menu, for -x/-y L. */ + format_add(ft, "popup_last_x", "%u", tc->menu_last_px); + format_add(ft, "popup_last_y", "%u", tc->menu_last_py + h); + /* * If there are any status lines, add this window position and the * status line position. @@ -239,6 +243,8 @@ cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item, xp = "#{popup_pane_left}"; else if (strcmp(xp, "M") == 0) xp = "#{popup_mouse_centre_x}"; + else if (strcmp(xp, "L") == 0) + xp = "#{popup_last_x}"; else if (strcmp(xp, "W") == 0) xp = "#{popup_window_status_line_x}"; p = format_expand(ft, xp); @@ -259,6 +265,8 @@ cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item, yp = "#{popup_pane_bottom}"; else if (strcmp(yp, "M") == 0) yp = "#{popup_mouse_top}"; + else if (strcmp(yp, "L") == 0) + yp = "#{popup_last_y}"; else if (strcmp(yp, "S") == 0) yp = "#{popup_status_line_y}"; else if (strcmp(yp, "W") == 0) diff --git a/key-bindings.c b/key-bindings.c index 9b5197e4e..27cbb0e86 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -59,6 +59,8 @@ " '#{?mouse_hyperlink,Type #[underscore]#{=/9/...:mouse_hyperlink},}' 'C-h' {copy-mode -q; send-keys -l -- \"#{q:mouse_hyperlink}\"}" \ " '#{?mouse_hyperlink,Copy #[underscore]#{=/9/...:mouse_hyperlink},}' 'h' {copy-mode -q; set-buffer -- \"#{q:mouse_hyperlink}\"}" \ " ''" \ + " '#{?#{#{pane_floating_flag}},Move,}' '' {display-menu -xL -yL -T '#[align=centre]Move' " DEFAULT_MOVE_MENU " }" \ + " '#{?#{#{pane_floating_flag}},Move & Resize,}' '' {display-menu -xL -yL -T '#[align=centre]Move & Resize' " DEFAULT_MOVE_RESIZE_MENU " }" \ " '#{?#{#{pane_floating_flag}},Tile,}' 't' { join-pane }" \ " '#{?#{!:#{pane_floating_flag}},Float,}' 'f' { break-pane -W }" \ " '#{?#{!:#{pane_floating_flag}},Horizontal Split,}' 'h' {split-window -h}" \ @@ -72,6 +74,30 @@ " 'Respawn' 'R' {respawn-pane -k}" \ " '#{?pane_marked,Unmark,Mark}' 'm' {select-pane -m}" \ " '#{?#{>:#{window_panes},1},,-}#{?window_zoomed_flag,Unzoom,Zoom}' 'z' {resize-pane -Z}" +#define DEFAULT_MOVE_MENU \ + " 'Centre' 'c' {move-pane -P centre}" \ + " ''" \ + " 'Top Left' '1' {move-pane -P top-left}" \ + " 'Top Right' '2' {move-pane -P top-right}" \ + " 'Bottom Left' '3' {move-pane -P bottom-left}" \ + " 'Bottom Right' '4' {move-pane -P bottom-right}" \ + " ''" \ + " 'Top' 't' {move-pane -P top-centre}" \ + " 'Bottom' 'b' {move-pane -P bottom-centre}" \ + " 'Left' 'l' {move-pane -P centre-left}" \ + " 'Right' 'r' {move-pane -P centre-right}" +#define DEFAULT_MOVE_RESIZE_MENU \ + " 'Fill' '0' {resize-pane -x100% -y100%; move-pane -P top-left}" \ + " ''" \ + " 'Top Left' '1' {resize-pane -x50% -y50%; move-pane -P top-left}" \ + " 'Top Right' '2' {resize-pane -x50% -y50%; move-pane -P top-right}" \ + " 'Bottom Left' '3' {resize-pane -x50% -y50%; move-pane -P bottom-left}" \ + " 'Bottom Right' '4' {resize-pane -x50% -y50%; move-pane -P bottom-right}" \ + " ''" \ + " 'Top' 't' {resize-pane -x100% -y50%; move-pane -P top-centre}" \ + " 'Bottom' 'b' {resize-pane -x100% -y50%; move-pane -P bottom-centre}" \ + " 'Left' 'l' {resize-pane -x50% -y100%; move-pane -P centre-left}" \ + " 'Right' 'r' {resize-pane -x50% -y100%; move-pane -P centre-right}" static int key_bindings_cmp(struct key_binding *, struct key_binding *); RB_GENERATE_STATIC(key_bindings, key_binding, entry, key_bindings_cmp); @@ -460,6 +486,9 @@ key_bindings_init(void) "bind -Tmove -N 'Move pane to bottom and resize' 'M-Down' { resizep -x100% -y50%; move-pane -P bottom-centre }", "bind -Tmove -N 'Move pane to left and resize' 'M-Left' { resizep -x50% -y100%; move-pane -P centre-left }", "bind -Tmove -N 'Move pane to right and resize' 'M-Right' { resizep -x50% -y100%; move-pane -P centre-right }", + "bind -Tmove -N 'Move pane to fill the window' 0 { resize-pane -x100% -y100%; move-pane -P top-left }", + "bind -Tmove -N 'Display move menu' , { if -F '#{pane_floating_flag}' { display-menu -xP -yP -T '#[align=centre]Move' " DEFAULT_MOVE_MENU " } }", + "bind -Tmove -N 'Display move and resize menu' . { if -F '#{pane_floating_flag}' { display-menu -xP -yP -T '#[align=centre]Move & Resize' " DEFAULT_MOVE_RESIZE_MENU " } }", /* Menu keys */ "bind -N 'Display window menu' < { display-menu -xW -yW -T '#[align=centre]#{window_index}:#{window_name}' " DEFAULT_WINDOW_MENU " }", diff --git a/menu.c b/menu.c index 756bb5693..257452fd7 100644 --- a/menu.c +++ b/menu.c @@ -313,6 +313,7 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) struct mouse_event *m = &event->m; u_int i; int count = menu->count, old = md->choice; + int move; const char *name = NULL; const struct menu_item *item; struct cmdq_state *state; @@ -320,6 +321,14 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) char *error; if (KEYC_IS_MOUSE(event->key)) { + /* + * A mouse move with no button held reports as a release, so + * treat it as highlight-only: it must never select or close the + * menu, otherwise a menu opened without a button already down + * (such as a submenu opened from another menu) would vanish as + * soon as the mouse moved over it. + */ + move = MOUSE_DRAG(m->b) && MOUSE_RELEASE(m->b); if (md->flags & MENU_NOMOUSE) { if (MOUSE_BUTTONS(m->b) != MOUSE_BUTTON_1) return (1); @@ -330,7 +339,7 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) m->y < md->py + 1 || m->y > md->py + 1 + count - 1) { if (~md->flags & MENU_STAYOPEN) { - if (MOUSE_RELEASE(m->b)) + if (!move && MOUSE_RELEASE(m->b)) return (1); } else { if (!MOUSE_RELEASE(m->b) && @@ -345,7 +354,7 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) return (0); } if (~md->flags & MENU_STAYOPEN) { - if (MOUSE_RELEASE(m->b)) + if (!move && MOUSE_RELEASE(m->b)) goto chosen; } else { if (!MOUSE_WHEEL(m->b) && !MOUSE_DRAG(m->b)) @@ -558,6 +567,10 @@ menu_prepare(struct menu *menu, int flags, int starting_choice, if (py + menu->count + 2 > c->tty.sy) py = c->tty.sy - menu->count - 2; + /* Remember where this menu is so -x/-y L can reuse the position. */ + c->menu_last_px = px; + c->menu_last_py = py; + if (lines == BOX_LINES_DEFAULT) lines = options_get_number(o, "menu-border-lines"); diff --git a/tmux.1 b/tmux.1 index b8405057b..ceeed5d8c 100644 --- a/tmux.1 +++ b/tmux.1 @@ -7957,6 +7957,7 @@ Both may be a row or column number, or one of the following special values: .It Li "R" Ta Fl x Ta "The right side of the terminal" .It Li "P" Ta "Both" Ta "The bottom left of the pane" .It Li "M" Ta "Both" Ta "The mouse position" +.It Li "L" Ta "Both" Ta "The position of the last menu" .It Li "W" Ta "Both" Ta "The window position on the status line" .It Li "S" Ta Fl y Ta "The line above or below the status line" .El @@ -7967,6 +7968,8 @@ Or a format, which is expanded including the following additional variables: .It Li "popup_centre_x" Ta "Centered in the client" .It Li "popup_centre_y" Ta "Centered in the client" .It Li "popup_height" Ta "Height of menu or popup" +.It Li "popup_last_x" Ta "Left of the last menu" +.It Li "popup_last_y" Ta "Bottom of the last menu" .It Li "popup_mouse_bottom" Ta "Bottom of at the mouse" .It Li "popup_mouse_centre_x" Ta "Horizontal centre at the mouse" .It Li "popup_mouse_centre_y" Ta "Vertical centre at the mouse" diff --git a/tmux.h b/tmux.h index 179ed3091..b153310cf 100644 --- a/tmux.h +++ b/tmux.h @@ -2268,6 +2268,9 @@ struct client { void *overlay_data; struct event overlay_timer; + u_int menu_last_px; + u_int menu_last_py; + struct client_files files; u_int source_file_depth; From 7b02cb6d03d99769fe853992f18f22e9f1085398 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 7 Jul 2026 12:30:36 +0000 Subject: [PATCH 078/127] Fix . and ; bindings, reported by tb. --- key-bindings.c | 4 ++-- tmux.1 | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/key-bindings.c b/key-bindings.c index 27cbb0e86..c73fd7c33 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -388,14 +388,14 @@ key_bindings_init(void) "bind -N 'Rename current session' '$' { command-prompt -I'#S' { rename-session -- '%%' } }", "bind -N 'Split window horizontally' % { split-window -h }", "bind -N 'Kill current window' & { confirm-before -p\"kill-window #W? (y/n)\" kill-window }", - "bind -N 'Prompt for window index to select' \"'\" { command-prompt -T window-target -pindex { select-window -t ':%%' } }", + "bind -N 'Prompt for window index to select' \"'\" { command-prompt -pindex { select-window -t ':%%' } }", "bind -N 'New floating pane' * { new-pane }", "bind -N 'Toggle pane between floating and tiled' @ { if -F '#{pane_floating_flag}' { join-pane } { break-pane -W } }", "bind -N 'Switch to previous client' ( { switch-client -p }", "bind -N 'Switch to next client' ) { switch-client -n }", "bind -N 'Rename current window' , { command-prompt -I'#W' { rename-window -- '%%' } }", "bind -N 'Delete the most recent paste buffer' - { delete-buffer }", - "bind -N 'Move the current window' . { command-prompt -T target { move-window -t '%%' } }", + "bind -N 'Move the current window' . { command-prompt { move-window -t '%%' } }", "bind -N 'Describe key binding' '/' { command-prompt -kpkey { list-keys -1N '%%' } }", "bind -N 'Select window 0' 0 { select-window -t:=0 }", "bind -N 'Select window 1' 1 { select-window -t:=1 }", diff --git a/tmux.1 b/tmux.1 index ceeed5d8c..fcf702fee 100644 --- a/tmux.1 +++ b/tmux.1 @@ -7819,10 +7819,7 @@ This affects what completions are offered when is pressed. Available types are: .Ql command , -.Ql search , -.Ql target -and -.Ql window\-target . +.Ql search . .Pp The following keys have a special meaning in the command prompt, depending on the value of the From 28c2005a2ac6c363aa94711d5df597099f2562f6 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 8 Jul 2026 07:55:17 +0000 Subject: [PATCH 079/127] Fix a typo (use right var), from Dane Jensen. --- layout.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layout.c b/layout.c index 170f3c5fb..507dd63c1 100644 --- a/layout.c +++ b/layout.c @@ -1310,7 +1310,7 @@ layout_split_sizes(struct layout_cell *lc, int size, int before, s2 = size; if (s2 < PANE_MINIMUM) s2 = PANE_MINIMUM; - else if (s2 > sx - 2) + else if (s2 > ss - 2) s2 = ss - 2; s1 = ss - 1 - s2; From 25060c3fa550c176f2f01881fb20f0a1a28dfe1c Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 8 Jul 2026 08:07:42 +0000 Subject: [PATCH 080/127] Fix empty argument to splitw/newp and add -k to copy-mode. --- cmd-copy-mode.c | 4 ++-- cmd-new-window.c | 2 +- cmd-split-window.c | 2 +- tmux.1 | 5 ++++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cmd-copy-mode.c b/cmd-copy-mode.c index fce6d3fbd..cfa60f59f 100644 --- a/cmd-copy-mode.c +++ b/cmd-copy-mode.c @@ -30,8 +30,8 @@ const struct cmd_entry cmd_copy_mode_entry = { .name = "copy-mode", .alias = NULL, - .args = { "deHMqSs:t:u", 0, 0, NULL }, - .usage = "[-deHMqSu] [-s src-pane] " CMD_TARGET_PANE_USAGE, + .args = { "dekHMqSs:t:u", 0, 0, NULL }, + .usage = "[-dekHMqSu] [-s src-pane] " CMD_TARGET_PANE_USAGE, .source = { 's', CMD_FIND_PANE, 0 }, .target = { 't', CMD_FIND_PANE, 0 }, diff --git a/cmd-new-window.c b/cmd-new-window.c index 6e6d70ccc..a54aaaa20 100644 --- a/cmd-new-window.c +++ b/cmd-new-window.c @@ -141,7 +141,7 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) sc.cwd = args_get(args, 'c'); sc.flags = 0; - if (args_has(args, 'E')) + if (args_has(args, 'E') || (count == 1 && *args_string(args, 0) == '\0')) sc.flags |= SPAWN_EMPTY; if (args_has(args, 'd')) sc.flags |= SPAWN_DETACHED; diff --git a/cmd-split-window.c b/cmd-split-window.c index 276138c19..09cfbfa86 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -105,7 +105,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) flags |= SPAWN_FULLSIZE; input = args_has(args, 'I'); - if (input) + if (input || (count == 1 && *args_string(args, 0) == '\0')) empty = 1; else empty = args_has(args, 'E'); diff --git a/tmux.1 b/tmux.1 index fcf702fee..593d4de06 100644 --- a/tmux.1 +++ b/tmux.1 @@ -2585,7 +2585,7 @@ The synopsis for the command is: .Bl -tag -width Ds .It Xo Ic copy\-mode -.Op Fl deHMqSu +.Op Fl dekHMqSu .Op Fl s Ar src\-pane .Op Fl t Ar target\-pane .Xc @@ -2625,6 +2625,9 @@ example with: bind PageUp copy\-mode \-eu bind PageDown copy\-mode \-ed .Ed +.Pp +.Fl k +kills the pane when the mode is exited. .El .Pp A number of preset arrangements of panes are available, these are called From e77d9ad0435cc168d3a66b9900b46cb28329cd3a Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 8 Jul 2026 09:07:48 +0100 Subject: [PATCH 081/127] -k/empty panes tests. --- regress/mode-kill.sh | 171 +++++++++++++++++++++++++++++++++++++++++++ regress/pane-ops.sh | 20 +++++ 2 files changed, 191 insertions(+) create mode 100755 regress/mode-kill.sh diff --git a/regress/mode-kill.sh b/regress/mode-kill.sh new file mode 100755 index 000000000..a3e0d09b9 --- /dev/null +++ b/regress/mode-kill.sh @@ -0,0 +1,171 @@ +#!/bin/sh + +# Tests of the -k flag on the mode-entering commands (cmd-copy-mode.c and +# cmd-choose-tree.c). With -k the pane is killed when the mode is exited: this +# is stored on the mode entry in window_pane_set_mode() and acted on in +# window_pane_reset_mode() (window.c). It is exercised here for: +# +# - copy-mode -k (window-copy.c); +# - choose-tree -k (window-tree.c); +# - choose-buffer -k (window-buffer.c). +# +# choose-tree and choose-buffer share cmd_choose_tree_exec(), which also backs +# choose-client and customize-mode, so those are not repeated. +# +# Each mode is entered in the active pane of a two-pane window: exiting with +# -k must remove that pane and leave the other. copy-mode is left with the +# server-side "-X cancel" and needs no client, so those tests run first. The +# tree modes only act on a key once the client has drawn the mode, so - as in +# choose-tree.sh - a second server then provides a client (an inner "tmux +# attach") and its pane is captured to wait until the mode has rendered before +# the exit key is sent. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMP=$(mktemp -d) || exit 1 +TMUX_TMPDIR="$TMP" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + rm -rf "$TMP" +} +trap cleanup EXIT + +fail() +{ + echo "$1" >&2 + exit 1 +} + +# capture the screen rendered by the inner client. +capture() +{ + $TMUX2 capture-pane -p -t out:0 2>/dev/null +} + +# wait_clients $n: wait until the test server has exactly $n clients. +wait_clients() +{ + i=0 + while [ "$i" -lt 50 ]; do + c=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "expected $1 clients, have $c" +} + +# wait_mode $target $state: wait until a pane enters (1) or leaves (0) mode. +wait_mode() +{ + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t "$1" '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$2" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane $1 mode state is '$got', expected '$2'" +} + +# wait_for $marker: wait until the rendered screen contains $marker. +wait_for() +{ + i=0 + while [ "$i" -lt 50 ]; do + capture | grep -F -q "$1" && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1' to render" +} + +# pane_gone $pane-id: true if the pane no longer exists. (display-message -t +# on a missing pane id falls back to a default target and succeeds, so the +# pane list is searched instead.) +pane_gone() +{ + ! $TMUX list-panes -s -t m -F '#{pane_id}' 2>/dev/null | \ + grep -q -x "$1" +} + +# open_window: make a fresh two-pane window and leave the new (active) pane +# as $active and the other as $other. +open_window() +{ + $TMUX new-window -t m: -n w 'cat' || fail "new-window failed" + $TMUX split-window -t m:w 'cat' || fail "split-window failed" + other=$($TMUX display-message -p -t m:w.0 '#{pane_id}') + active=$($TMUX display-message -p -t m:w '#{pane_id}') +} + +# check_killed $label: wait for the active pane to be killed, leaving only the +# other pane, then drop the window. +check_killed() +{ + i=0 + while [ "$i" -lt 50 ]; do + pane_gone "$active" && break + sleep 0.2 + i=$((i + 1)) + done + pane_gone "$active" || fail "$1: pane not killed on exit" + panes=$($TMUX list-panes -t m:w -F '#{pane_id}' | tr '\n' ' ') + [ "$panes" = "$other " ] || \ + fail "$1: expected only $other left, have $panes" + $TMUX kill-window -t m:w 2>/dev/null +} + +# Session m; window 0 keeps a live pane so the session (and later the client) +# survives each test killing a pane. +$TMUX new-session -d -s m -x 80 -y 24 'cat' || exit 1 + +# --- copy-mode -k kills the pane, plain copy-mode does not ------------------- +# +# copy-mode is exited with the server-side "-X cancel", so no client is needed +# and none is attached yet. +open_window +$TMUX copy-mode -k -t m:w || fail "copy-mode -k failed" +wait_mode "$active" 1 +$TMUX send-keys -t m:w -X cancel || fail "copy cancel failed" +check_killed 'copy-mode -k' + +open_window +$TMUX copy-mode -t m:w || fail "copy-mode failed" +wait_mode "$active" 1 +$TMUX send-keys -t m:w -X cancel || fail "copy cancel failed" +wait_mode "$active" 0 +pane_gone "$active" && fail 'copy-mode: pane killed without -k' +$TMUX kill-window -t m:w 2>/dev/null + +# --- choose-tree -k and choose-buffer -k kill the pane ---------------------- +# +# These need the client to draw the mode before a key acts, so attach one now. +# A paste buffer is needed for choose-buffer to have something to show, and a +# distinct -F marker per call is waited for in the capture so the exit key is +# only sent once the mode is drawn. +$TMUX set-buffer 'mode-kill buffer' || exit 1 +$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t m" || exit 1 +wait_clients 1 +open_window +$TMUX choose-tree -k -F 'TREEMARK' -t m:w || fail "choose-tree -k failed" +wait_for 'TREEMARK' +$TMUX send-keys -t m:w q || fail "choose-tree exit failed" +check_killed 'choose-tree -k' + +open_window +$TMUX choose-buffer -k -F 'BUFMARK' -t m:w || fail "choose-buffer -k failed" +wait_for 'BUFMARK' +$TMUX send-keys -t m:w q || fail "choose-buffer exit failed" +check_killed 'choose-buffer -k' + +exit 0 diff --git a/regress/pane-ops.sh b/regress/pane-ops.sh index 762bea200..29c79e2e2 100644 --- a/regress/pane-ops.sh +++ b/regress/pane-ops.sh @@ -399,6 +399,19 @@ check_fmt 'P:9.0' '#{pane_pid}' '' check_fail 'command cannot be given for empty pane' \ new-window -d -E -t P:10 -n empty 'true' +# An empty string as the sole argument is equivalent to -E: the pane is +# created empty, running no command. +check_ok new-window -d -t P:12 -n empty-str '' +check_fmt 'P:12.0' '#{pane_dead}' '0' +check_fmt 'P:12.0' '#{pane_pid}' '' +check_ok kill-window -t P:12 + +# A missing command (rather than an empty one) runs the default command, so +# the pane is not empty and has a process. +check_ok new-window -d -t P:12 -n default-cmd +check_fmt 'P:12.0' '#{?pane_pid,live,empty}' 'live' +check_ok kill-window -t P:12 + # respawn-pane -E stores the command and cwd without starting it. tmp=${TMPDIR:-/tmp}/tmux-pane-ops-empty-$$ rm -f "$tmp" @@ -453,6 +466,13 @@ check_fmt 'P:2' '#{window_panes}' '2' check_fail 'command cannot be given for empty pane' \ split-window -d -E -t P:2.0 'sleep 5' +# An empty string as the sole argument splits with an empty pane, like -E. +eid=$($TMUX split-window -d -P -F '#{pane_id}' -t P:2.0 '') +check_fmt "$eid" '#{pane_dead}' '0' +check_fmt "$eid" '#{pane_pid}' '' +check_ok kill-pane -t "$eid" +check_fmt 'P:2' '#{window_panes}' '2' + # -e adds to the new pane's environment. eid=$($TMUX split-window -d -P -F '#{pane_id}' -e GREETING=hello -t P:2.0 \ 'echo $GREETING; exec cat') From 27c26e88e5a674150ae8bdf298e1567d76fa0653 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 8 Jul 2026 09:16:26 +0100 Subject: [PATCH 082/127] detach-on-destroy next/previous tests. --- regress/session-ops.sh | 130 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/regress/session-ops.sh b/regress/session-ops.sh index 08c5dfbae..7fc2b65cb 100644 --- a/regress/session-ops.sh +++ b/regress/session-ops.sh @@ -105,6 +105,104 @@ check_windows() fi } +# wait_client_session $expected +# +# Wait for the only test client to be attached to $expected. +wait_client_session() +{ + expected=$1 + i=0 + + while [ $i -lt 30 ]; do + out=$($TMUX list-clients -F '#{client_session}' 2>/dev/null || + true) + if [ "$out" = "$expected" ]; then + return + fi + i=$((i + 1)) + sleep 0.1 + done + + echo "Client session wrong." + echo "Expected: '$expected'" + echo "But got: '$out'" + exit 1 +} + +# attach_control_client $session +# +# Attach a control client to $session and keep its input open on fd 9. +attach_control_client() +{ + fifo=$(mktemp -u) + + mkfifo "$fifo" || exit 1 + $TMUX -C attach-session -t "$1" <"$fifo" >/dev/null 2>&1 & + control_pid=$! + exec 9>"$fifo" + rm -f "$fifo" + wait_client_session "$1" +} + +# check_attached_destroy $mode +# +# Run an attached tmux inside a pane and destroy its only session. +check_attached_destroy() +{ + mode=$1 + outer=DODouter$mode + outdir=$(mktemp -d) || exit 1 + script=$outdir/inner.sh + rcfile=$outdir/rc + + cat >"$script" <<-EOF + #!/bin/sh + "$TEST_TMUX" -LtestInner$$-$mode -f/dev/null new \\; \ + set -g detach-on-destroy $mode \\; send exit Enter + printf '%s\n' \$? >"$rcfile" + EOF + chmod +x "$script" || exit 1 + + check_ok new-session -d -s "$outer" -x 80 -y 24 + check_ok send-keys -t "$outer:0.0" "sh $script" Enter + + i=0 + while [ $i -lt 50 ]; do + [ -f "$rcfile" ] && break + i=$((i + 1)) + sleep 0.1 + done + + pane=$($TMUX capture-pane -pt "$outer:0.0" -S -) + if [ ! -f "$rcfile" ]; then + echo "Inner tmux did not exit." + echo "$pane" + $TMUX kill-session -t "$outer" 2>/dev/null || true + rm -rf "$outdir" + exit 1 + fi + rc=$(cat "$rcfile") + if [ "$rc" != 0 ]; then + echo "Inner tmux exited with status $rc." + echo "$pane" + $TMUX kill-session -t "$outer" 2>/dev/null || true + rm -rf "$outdir" + exit 1 + fi + case "$pane" in + *"server exited unexpectedly"*) + echo "Inner tmux server crashed." + echo "$pane" + $TMUX kill-session -t "$outer" 2>/dev/null || true + rm -rf "$outdir" + exit 1 + ;; + esac + + check_ok kill-session -t "$outer" + rm -rf "$outdir" +} + # --------------------------------------------------------------------------- # new-session and has-session. @@ -203,6 +301,38 @@ check_fail '-f only valid with -a' kill-session -f 'x' -t S1 check_ok kill-session -C -t S2 check_ok has-session -t S2 +# detach-on-destroy previous and next move attached clients in +# alphabetical order and must not crash when a session is destroyed. +check_ok new-session -d -s DODa -x 80 -y 24 +check_ok new-session -d -s DODb -x 80 -y 24 +check_ok new-session -d -s DODc -x 80 -y 24 +check_ok set-option -t DODb detach-on-destroy previous +attach_control_client DODb +check_ok kill-session -t DODb +wait_client_session DODa +exec 9>&- +wait "$control_pid" 2>/dev/null || true +check_ok kill-session -t DODa +check_ok kill-session -t DODc + +check_ok new-session -d -s DODa -x 80 -y 24 +check_ok new-session -d -s DODb -x 80 -y 24 +check_ok new-session -d -s DODc -x 80 -y 24 +check_ok set-option -t DODb detach-on-destroy next +attach_control_client DODb +check_ok kill-session -t DODb +wait_client_session DODc +exec 9>&- +wait "$control_pid" 2>/dev/null || true +check_ok kill-session -t DODa +check_ok kill-session -t DODc + +# With only one session, previous and next have no replacement session; the +# attached client must exit cleanly rather than being moved to the dying +# session. +check_attached_destroy previous +check_attached_destroy next + # -a kills every other session. check_ok kill-session -a -t S1 check_sessions 'S1' From ac723bf2df3d5c0ee29441a2857774094a009391 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 8 Jul 2026 08:15:49 +0000 Subject: [PATCH 083/127] Do not crash looking for next or previous session. GitHub issue 5344. --- server-fn.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server-fn.c b/server-fn.c index 585a01f9c..7560e9da2 100644 --- a/server-fn.c +++ b/server-fn.c @@ -435,6 +435,7 @@ server_destroy_session(struct session *s) { struct client *c; struct session *s_new = NULL, *cs_new = NULL, *use_s; + struct sort_criteria sort_crit = { .order = SORT_NAME }; int detach_on_destroy; detach_on_destroy = options_get_number(s->options, "detach-on-destroy"); @@ -443,9 +444,11 @@ server_destroy_session(struct session *s) else if (detach_on_destroy == 2) s_new = server_find_session(s, server_newer_detached_session); else if (detach_on_destroy == 3) - s_new = session_previous_session(s, NULL); + s_new = session_previous_session(s, &sort_crit); else if (detach_on_destroy == 4) - s_new = session_next_session(s, NULL); + s_new = session_next_session(s, &sort_crit); + if (s_new == s) + s_new = NULL; /* * If no suitable new session was found above, then look for any From dac6514147285e6a955b73787c036e00ef90c964 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 8 Jul 2026 08:21:53 +0000 Subject: [PATCH 084/127] Report the terminal's own theme to panes rather than guessing from the background. GitHub issue 5343 from Ayman Bagabas. --- window.c | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/window.c b/window.c index 31abdd398..109f002ff 100644 --- a/window.c +++ b/window.c @@ -2412,7 +2412,6 @@ window_pane_get_theme(struct window_pane *wp) { struct window *w; struct client *loop; - enum client_theme theme; int found_light = 0, found_dark = 0; if (wp == NULL) @@ -2420,14 +2419,9 @@ window_pane_get_theme(struct window_pane *wp) w = wp->window; /* - * Derive theme from pane background color, if it's not the default - * colour. + * Prefer a theme reported by an attached client with mode 2031 or DSR + * 996: the terminal knows its own light or dark mode. */ - theme = colour_totheme(window_pane_get_bg(wp)); - if (theme != THEME_UNKNOWN) - return (theme); - - /* Try to find a client that has a theme. */ TAILQ_FOREACH(loop, &clients, entry) { if (loop->flags & CLIENT_UNATTACHEDFLAGS) continue; @@ -2444,12 +2438,16 @@ window_pane_get_theme(struct window_pane *wp) break; } } - if (found_dark && !found_light) return (THEME_DARK); if (found_light && !found_dark) return (THEME_LIGHT); - return (THEME_UNKNOWN); + + /* + * Otherwise guess from the pane background colour, for terminals which + * do not report a theme themselves. + */ + return (colour_totheme(window_pane_get_bg(wp))); } void From e601bfb3627c00fdcc70f01eed5a2c4c659b0348 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 8 Jul 2026 09:22:32 +0100 Subject: [PATCH 085/127] Theme report test from Ayman Bagabas. --- regress/theme-report.sh | 57 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100755 regress/theme-report.sh diff --git a/regress/theme-report.sh b/regress/theme-report.sh new file mode 100755 index 000000000..4c42244b4 --- /dev/null +++ b/regress/theme-report.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +# Test that the theme reported to a pane (mode 2031 / DSR 996) follows the +# terminal's reported theme, not a guess from the background colour. +# +# An inner client is attached inside an outer tmux pane. The outer pane has a +# light background, so the outer server answers the inner client's DSR 996 with +# light. The inner server is given a dark window background, so a guess from the +# background colour would say dark. A pane in the inner server then queries DSR +# 996: the answer must be light (2), following the client's reported theme, not +# dark (1) from the background. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +INNER="$TEST_TMUX -LtestI$$ -f/dev/null" +OUTER="$TEST_TMUX -LtestO$$ -f/dev/null" +$INNER kill-server 2>/dev/null +$OUTER kill-server 2>/dev/null + +TMP=$(mktemp) +trap "rm -f $TMP; $INNER kill-server 2>/dev/null; $OUTER kill-server 2>/dev/null" \ + 0 1 15 + +# Inner server, dark background, keep panes alive across the query respawn. +$INNER new-session -d -x80 -y24 || exit 1 +$INNER set -g remain-on-exit on +$INNER set -g window-style 'bg=black' + +# Outer server with a light background, running the inner client attached so the +# inner client has a real terminal that reports a theme. +$OUTER new-session -d -x80 -y24 || exit 1 +$OUTER set -g window-style 'bg=white' +$OUTER new-window "$INNER attach" || exit 1 +sleep 2 + +# Query DSR 996 from an inner pane and capture the CSI ? 997 ; Ps n reply. +$INNER respawnw -k -t:0 -- sh -c " + exec 2>/dev/null + stty raw -echo + printf '\033[?996n' + dd bs=1 count=9 2>/dev/null | cat -v > $TMP + sleep 1 +" +sleep 2 + +actual=$(cat "$TMP") +expected='^[[?997;2n' # 2 = light (from the terminal), not 1 = dark (from bg) + +if [ "$actual" = "$expected" ]; then + [ -n "$VERBOSE" ] && echo "[PASS] reported terminal theme ($actual)" + exit 0 +fi + +echo "[FAIL] expected '$expected' (light, from terminal), got '$actual'" +exit 1 From c830710e11425c0657547495eb232e4615c0b43f Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 8 Jul 2026 09:55:21 +0100 Subject: [PATCH 086/127] More sub tests. --- regress/format-modifiers.sh | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/regress/format-modifiers.sh b/regress/format-modifiers.sh index b6340b622..631f1e10e 100644 --- a/regress/format-modifiers.sh +++ b/regress/format-modifiers.sh @@ -101,6 +101,8 @@ $TMUX set -g @ts '1000000000' || exit 1 # 2001-09-09 01:46:40 UTC $TMUX set -g @sp 'a b$c' || exit 1 # shell-special characters for q: $TMUX set -g @hash 'a#b' || exit 1 # a "#" for q/e: $TMUX set -g @sq "a'b" || exit 1 # a single quote for q/s: +$TMUX set -g @sub 'abABab' || exit 1 +$TMUX set -g @slash 'foo/bar foo/' || exit 1 $TMUX set -g @nl "$(printf 'a\nb')" || exit 1 q_s_nl=$(printf "'a\nb'") @@ -426,9 +428,23 @@ test_format "#{=/3/#,:@s}" "abc," # escaped comma in the marker # The truncation marker is itself expanded as a format. test_format "#{=/3/#{l:>}:@s}" "abc>" -# Substitution flags: a third argument of "i" is case-insensitive; an invalid -# regular expression leaves the text unchanged. +# Substitution, including regular expressions, back references, different +# delimiters, empty matches and the "i" case-insensitive flag. +test_format "#{s/z/X/:@s}" "abcdefghij" +test_format "#{s/[bd]/X/:@s}" "aXcXefghij" test_format "#{s/A/X/i:@s}" "Xbcdefghij" +test_format "#{s/a(.)/\\1x/i:@sub}" "bxBxbx" +test_format "#{s/(.)(.)/\\2\\1/:@s}" "badcfehgji" +test_format "#{s|foo/|bar/|:@slash}" "bar/bar bar/" +test_format "#{s/^abc/ABC/:@s}" "ABCdefghij" +test_format "#{s/^(.)(.)/\\2\\1/:@s}" "bacdefghij" +test_format "#{s/^x*//:@s}" "abcdefghij" +test_format "#{s/^/X/:@s}" "Xabcdefghij" +test_format "#{s/^x*/X/:@s}" "Xabcdefghij" +test_format "#{s/$/X/:@s}" "abcdefghijX" +test_format "#{s/x*//:@s}" "abcdefghij" +test_format "#{s/x*/X/:@s}" "aXbXcXdXeXfXgXhXiXjX" +# An invalid regular expression leaves the text unchanged. test_format "#{s/[/X/:@s}" "abcdefghij" From 8e76565c9cb274ab5ac7d77095c872b77c953da1 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 8 Jul 2026 09:23:10 +0000 Subject: [PATCH 087/127] Keep all modes except cursor during redraw, GitHub issue 5337 from Ayman Bagabas. --- screen-redraw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/screen-redraw.c b/screen-redraw.c index 9e389be91..6e8b65b76 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1667,7 +1667,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) } } tty_sync_start(tty); - tty_update_mode(tty, 0, NULL); + tty_update_mode(tty, tty->mode & ~CURSOR_MODES, NULL); if (wp != NULL) redraw_draw_pane_lines(&dctx, wp, flags); From e37a2e54ab7229b332c903ed380297b99963e429 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 8 Jul 2026 11:04:51 +0000 Subject: [PATCH 088/127] Fix regsub with anchor at start of pattern, GitHub issue 5341. --- regsub.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/regsub.c b/regsub.c index 62d750d8a..9e33a88ab 100644 --- a/regsub.c +++ b/regsub.c @@ -90,6 +90,15 @@ regsub(const char *pattern, const char *with, const char *text, int flags) */ regsub_copy(&buf, &len, text, last, m[0].rm_so + start); + /* For anchored patterns, replace the first match only. */ + if (*pattern == '^') { + regsub_expand(&buf, &len, with, text + start, m, + nitems(m)); + last = start + m[0].rm_eo; + regsub_copy(&buf, &len, text, last, end); + break; + } + /* * If the last match was empty and this one isn't (it is either * later or has matched text), expand this match. If it is @@ -109,12 +118,6 @@ regsub(const char *pattern, const char *with, const char *text, int flags) start += m[0].rm_eo + 1; empty = 1; } - - /* Stop now if anchored to start. */ - if (*pattern == '^') { - regsub_copy(&buf, &len, text, start, end); - break; - } } buf[len] = '\0'; From f92cb43ce4dc0127c139c01937a8a623b7c22950 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 8 Jul 2026 12:36:19 +0100 Subject: [PATCH 089/127] t/d test. --- regress/format-modifiers.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/regress/format-modifiers.sh b/regress/format-modifiers.sh index 631f1e10e..58c02af7c 100644 --- a/regress/format-modifiers.sh +++ b/regress/format-modifiers.sh @@ -291,6 +291,15 @@ if [ -z "$($TMUX display-message -p '#{t/r:@ts}')" ]; then echo "Format test failed for '#{t/r:@ts}': empty result" exit 1 fi +# t/d: difference from the current time in seconds. +diff=$($TMUX display-message -p '#{t/d:@ts}') +case "$diff" in +[0-9]*) ;; +*) + echo "Format test failed for '#{t/d:@ts}': expected positive value, got '$diff'" + exit 1 + ;; +esac # t/f: custom strftime format applied to the variable's time. Tested in a # format_expand context (list-windows -F), where a single strftime specifier is @@ -326,6 +335,14 @@ done # A time in the future has no relative form. $TMUX set -g @future "$((now + 100000))" test_format "#{t/r:@future}" "" +diff=$($TMUX display-message -p '#{t/d:@future}') +case "$diff" in +-[0-9]*) ;; +*) + echo "Format test failed for '#{t/d:@future}': expected negative value, got '$diff'" + exit 1 + ;; +esac # --- Content search (C) -------------------------------------------------- From 1fefb8044545115cf745f56b90d996a043836a30 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 9 Jul 2026 06:33:19 +0000 Subject: [PATCH 090/127] Add t/d for time difference in seconds. --- format.c | 16 ++++++++++++++++ tmux.1 | 6 +++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/format.c b/format.c index f2bc22a7a..7ec4f06b9 100644 --- a/format.c +++ b/format.c @@ -127,6 +127,7 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2) #define FORMAT_QUOTE_SHELL_SQ 0x20000000 #define FORMAT_OPTIONS 0x40000000 #define FORMAT_ENVIRON 0x80000000ULL +#define FORMAT_DIFFERENCE 0x100000000ULL /* Limit on recursion. */ #define FORMAT_LOOP_LIMIT 100 @@ -4203,6 +4204,17 @@ format_relative_time(time_t t) return (xstrdup(out)); } +/* Make a time difference in seconds. */ +static char * +format_time_difference(time_t t) +{ + time_t now = time(NULL); + char *out; + + xasprintf(&out, "%ld", (long)now - (long)t); + return (out); +} + /* Find a format entry. */ static char * format_find(struct format_tree *ft, const char *key, uint64_t modifiers, @@ -4287,6 +4299,8 @@ found: return (NULL); if (modifiers & FORMAT_RELATIVE) found = format_relative_time(t); + else if (modifiers & FORMAT_DIFFERENCE) + found = format_time_difference(t); else if (modifiers & FORMAT_PRETTY) found = format_pretty_time(t, 0); else { @@ -5666,6 +5680,8 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, modifiers |= FORMAT_PRETTY; else if (strchr(fm->argv[0], 'r') != NULL) modifiers |= FORMAT_RELATIVE; + else if (strchr(fm->argv[0], 'd') != NULL) + modifiers |= FORMAT_DIFFERENCE; else if (fm->argc >= 2 && strchr(fm->argv[0], 'f') != NULL) { free(time_format); diff --git a/tmux.1 b/tmux.1 index 593d4de06..dbfb7a30a 100644 --- a/tmux.1 +++ b/tmux.1 @@ -6813,10 +6813,14 @@ Adding will use shorter but less accurate time format for times in the past. .Ql r .Pq Ql t/r -will show the time relative to the current time, for example +will give the time relative to the current time, for example .Ql \&1m or .Ql 2m23s . +.Ql d +.Pq Ql t/d +will give the difference from the time to current time in seconds (future +times are negative). A custom format may be given using an .Ql f suffix (note that From dea309cf8d818665043aa9df0b3f1adb63ab0010 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 9 Jul 2026 07:19:24 +0000 Subject: [PATCH 091/127] Allow empty name with new-window again, from Jacob Koziej. --- spawn.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spawn.c b/spawn.c index 3a211e3c5..09eec948d 100644 --- a/spawn.c +++ b/spawn.c @@ -179,7 +179,7 @@ spawn_window(struct spawn_context *sc, char **cause) /* Set the name of the new window. */ if (~sc->flags & SPAWN_RESPAWN) { free(w->name); - if (sc->name == NULL || *sc->name == '\0') + if (sc->name == NULL) w->name = default_window_name(w); else { w->name = xstrdup(sc->name); From e242da168bb6d50c687ccd1897b82c1d282a651a Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 9 Jul 2026 07:32:58 +0000 Subject: [PATCH 092/127] Clip the status line and menu against an open popup overlay, GitHub issue 5350 from Noam Stolero. --- popup.c | 2 +- screen-redraw.c | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/popup.c b/popup.c index 72a8259cb..913c6c527 100644 --- a/popup.c +++ b/popup.c @@ -245,7 +245,7 @@ popup_check_cb(struct client* c, void *data, u_int px, u_int py, u_int nx) */ for (i = 0; i < mr->used; i++) { server_client_overlay_range(pd->px, pd->py, pd->sx, - pd->sy, r->ranges[i].px, py, r->ranges[i].nx, + pd->sy, mr->ranges[i].px, py, mr->ranges[i].nx, &pd->or[i]); } diff --git a/screen-redraw.c b/screen-redraw.c index 6e8b65b76..70a830e6a 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1588,8 +1588,10 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) struct screen *sl; struct redraw_scene *scene; struct window_pane *loop; - u_int width, i, y, lines; + u_int width, i, y, lines, j; struct redraw_span *first; + struct visible_ranges *r; + struct visible_range *rr; int redraw; if (c->flags & CLIENT_SUSPENDED) @@ -1694,8 +1696,16 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) else y = c->tty.sy - lines; sl = c->status.active; - for (i = 0; i < lines; i++) - tty_draw_line(tty, sl, 0, i, UINT_MAX, 0, y + i, NULL); + for (i = 0; i < lines; i++) { + r = tty_check_overlay_range(tty, 0, y + i, tty->sx); + for (j = 0; j < r->used; j++) { + rr = &r->ranges[j]; + if (rr->nx == 0) + continue; + tty_draw_line(tty, sl, rr->px, i, rr->nx, + rr->px, y + i, NULL); + } + } } if (c->overlay_draw != NULL && (flags & REDRAW_OVERLAY)) c->overlay_draw(c, c->overlay_data); From 161dbaf1ced65dc0d89df5fb3b99fb091cf34b7a Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 9 Jul 2026 07:35:05 +0000 Subject: [PATCH 093/127] Cancel pending resizes and resize immediately when entering alternate screen, fixes flickering with scrollbars. GitHub issue 5351 from Michael Grant. --- screen-write.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/screen-write.c b/screen-write.c index ebc40b484..4d4f90900 100644 --- a/screen-write.c +++ b/screen-write.c @@ -2962,7 +2962,13 @@ screen_write_alternateon(struct screen_write_ctx *ctx, struct grid_cell *gc, if (wp != NULL) { window_pane_clear_resizes(wp, NULL); + if (event_initialized(&wp->resize_timer)) + evtimer_del(&wp->resize_timer); layout_fix_panes(wp->window, NULL); + if (!TAILQ_EMPTY(&wp->resize_queue)) { + window_pane_send_resize(wp, wp->sx, wp->sy); + window_pane_clear_resizes(wp, NULL); + } server_redraw_window_borders(wp->window); } From 3a6c717bb81ee5b219d56e2250ad563ce1523025 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 9 Jul 2026 07:54:07 +0000 Subject: [PATCH 094/127] Do not exit at bottom if a selection is in progress, GitHub issue 5349 from xiaomublue at gmail dot com. --- tmux.1 | 4 +++- window-copy.c | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tmux.1 b/tmux.1 index dbfb7a30a..50d398b7c 100644 --- a/tmux.1 +++ b/tmux.1 @@ -2340,7 +2340,8 @@ but also exit copy mode if the cursor reaches the bottom. .It Xo .Ic scroll\-exit\-on .Xc -Turn on exiting copy mode when scrolling to the end of the buffer. +Turn on exiting copy mode when scrolling to the end of the buffer, unless a +selection is present. .It Xo .Ic scroll\-exit\-off .Xc @@ -2617,6 +2618,7 @@ instead of .Fl e specifies that scrolling to the bottom of the history (to the visible screen) should exit copy mode. +This will not happen if a selection is present. While in copy mode, pressing a key other than those used for scrolling will disable this behaviour. This is intended to allow fast scrolling through a pane's history, for diff --git a/window-copy.c b/window-copy.c index aedbba99b..33743cea3 100644 --- a/window-copy.c +++ b/window-copy.c @@ -854,7 +854,7 @@ window_copy_scroll1(struct window_mode_entry *wme, struct window_pane *wp, window_copy_cursor_end_of_line(wme); } - if (scroll_exit && data->oy == 0) { + if (scroll_exit && data->oy == 0 && data->screen.sel == NULL) { window_pane_reset_mode(wp); return; } @@ -972,7 +972,7 @@ window_copy_pagedown1(struct window_mode_entry *wme, int half_page, window_copy_cursor_end_of_line(wme); } - if (scroll_exit && data->oy == 0) + if (scroll_exit && data->oy == 0 && data->screen.sel == NULL) return (1); if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); @@ -2352,7 +2352,7 @@ window_copy_cmd_scroll_down(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_down(wme, 1); - if (data->scroll_exit && data->oy == 0) + if (data->scroll_exit && data->oy == 0 && data->screen.sel == NULL) return (WINDOW_COPY_CMD_CANCEL); return (WINDOW_COPY_CMD_NOTHING); } From 041b6a6bc75b1735f91243527bad7cf394d58fba Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 9 Jul 2026 08:56:06 +0100 Subject: [PATCH 095/127] Add test from xiaomublue at gmail dot com. --- regress/copy-mode-scroll-exit.sh | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 regress/copy-mode-scroll-exit.sh diff --git a/regress/copy-mode-scroll-exit.sh b/regress/copy-mode-scroll-exit.sh new file mode 100644 index 000000000..0f9e2d335 --- /dev/null +++ b/regress/copy-mode-scroll-exit.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +cleanup() +{ + $TMUX kill-server 2>/dev/null +} +trap cleanup 0 +trap 'exit 1' 1 2 3 15 + +$TMUX new -d -x40 -y10 \ + 'i=0; while [ $i -lt 80 ]; do echo "line $i"; i=$((i + 1)); done; cat' || + exit 1 +$TMUX set -g window-size manual || exit 1 + +$TMUX copy-mode -e || exit 1 +$TMUX send-keys -X history-top || exit 1 +$TMUX send-keys -X start-of-line || exit 1 +$TMUX send-keys -X begin-selection || exit 1 +$TMUX send-keys -X cursor-down || exit 1 + +[ "$($TMUX display-message -p '#{selection_present}')" = "1" ] || exit 1 +$TMUX send-keys -N200 -X scroll-down || exit 1 +[ "$($TMUX display-message -p '#{pane_in_mode} #{scroll_position}')" = "1 0" ] || + exit 1 + +$TMUX send-keys -X clear-selection || exit 1 +$TMUX send-keys -X scroll-down || exit 1 +[ "$($TMUX display-message -p '#{pane_in_mode}')" = "0" ] || exit 1 + +exit 0 From ec31f4566dca2315dee04c9c23834c0020a27b86 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Thu, 9 Jul 2026 09:39:44 +0100 Subject: [PATCH 096/127] Tweak test. --- regress/input-reflow-stress.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regress/input-reflow-stress.sh b/regress/input-reflow-stress.sh index 926e8cdd0..767f08bec 100644 --- a/regress/input-reflow-stress.sh +++ b/regress/input-reflow-stress.sh @@ -448,7 +448,7 @@ sleep 0.1 $TMUX new-session -d -x 1 -y 1 -s test-setup "sleep 2" || exit 1 $TMUX set-option -g history-limit "$HISTORY_LIMIT" || exit 1 -$TMUX new-session -d -x 80 -y "$HEIGHT" -s stress 'cat' || exit 1 +$TMUX new-session -d -x 80 -y "$HEIGHT" -s stress 'stty -echo; exec cat' || exit 1 $TMUX kill-session -t test-setup || exit 1 sleep 0.3 From bcc73e0db4573f51f45f525a47f88862b0da7740 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 10 Jul 2026 07:13:05 +0000 Subject: [PATCH 097/127] Change NOTHING (which means only redraw the indicator) to MOVE. And redefine NOTHING as /actually/ nothing. Use it for commands which actually make no change. Fixes flickering due to excessive redrawing of indicator lines. Mostly from Michael Grant. --- window-copy.c | 300 +++++++++++++++++++++++++++----------------------- 1 file changed, 160 insertions(+), 140 deletions(-) diff --git a/window-copy.c b/window-copy.c index 33743cea3..8b117be92 100644 --- a/window-copy.c +++ b/window-copy.c @@ -49,8 +49,8 @@ static void window_copy_next_paragraph(struct window_mode_entry *); static void window_copy_previous_paragraph(struct window_mode_entry *); static void window_copy_redraw_selection(struct window_mode_entry *, u_int); static void window_copy_redraw_lines(struct window_mode_entry *, u_int, - u_int); -static void window_copy_redraw_screen(struct window_mode_entry *); + u_int, int); +static void window_copy_redraw_screen(struct window_mode_entry *, int); static void window_copy_do_refresh(struct window_mode_entry *, int); static void window_copy_refresh_timer(int, short, void *); static void window_copy_refresh_arm(struct window_mode_entry *); @@ -66,7 +66,7 @@ static u_int window_copy_cursor_offset(struct window_mode_entry *, u_int, static u_int window_copy_cursor_unoffset(struct window_mode_entry *, u_int, u_int); static void window_copy_write_line(struct window_mode_entry *, - struct screen_write_ctx *, u_int); + struct screen_write_ctx *, u_int, int); static void window_copy_write_lines(struct window_mode_entry *, struct screen_write_ctx *, u_int, u_int); static char *window_copy_match_at_cursor(struct window_copy_mode_data *); @@ -212,6 +212,7 @@ enum { enum window_copy_cmd_action { WINDOW_COPY_CMD_NOTHING, + WINDOW_COPY_CMD_MOVE, WINDOW_COPY_CMD_REDRAW, WINDOW_COPY_CMD_CANCEL, }; @@ -628,7 +629,7 @@ window_copy_init(struct window_mode_entry *wme, screen_write_start(&ctx, &data->screen); for (i = 0; i < screen_size_y(&data->screen); i++) - window_copy_write_line(wme, &ctx, i); + window_copy_write_line(wme, &ctx, i, 1); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(&data->screen)), data->cy, 0); screen_write_stop(&ctx); @@ -742,10 +743,10 @@ window_copy_vadd(struct window_pane *wp, int parse, const char *fmt, va_list ap) * (If there's any history at all, it has changed.) */ if (screen_hsize(data->backing)) - window_copy_redraw_lines(wme, 0, 1); + window_copy_redraw_lines(wme, 0, 1, 1); /* Write the new lines. */ - window_copy_redraw_lines(wme, old_cy, backing->cy - old_cy + 1); + window_copy_redraw_lines(wme, old_cy, backing->cy - old_cy + 1, 1); screen_write_stop(&ctx); } @@ -863,7 +864,7 @@ window_copy_scroll1(struct window_mode_entry *wme, struct window_pane *wp, window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection(wme, 1, 0); window_pane_scrollbar_show(wp, 1); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } void @@ -917,7 +918,7 @@ window_copy_pageup1(struct window_mode_entry *wme, int half_page) window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection(wme, 1, 0); window_pane_scrollbar_show(wme->wp, 1); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } void @@ -978,7 +979,7 @@ window_copy_pagedown1(struct window_mode_entry *wme, int half_page, window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection(wme, 1, 0); window_pane_scrollbar_show(wme->wp, 1); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); return (0); } @@ -1223,7 +1224,7 @@ window_copy_resize(struct window_mode_entry *wme, u_int sx, u_int sy) } window_copy_size_changed(wme); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static const char * @@ -1292,7 +1293,7 @@ window_copy_cmd_back_to_indentation(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; window_copy_cursor_back_to_indentation(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1305,7 +1306,7 @@ window_copy_cmd_begin_selection(struct window_copy_cmd_state *cs) if (m != NULL) { window_copy_start_drag(c, m); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } data->lineflag = LINE_SEL_NONE; @@ -1323,7 +1324,7 @@ window_copy_cmd_stop_selection(struct window_copy_cmd_state *cs) data->cursordrag = CURSORDRAG_NONE; data->lineflag = LINE_SEL_NONE; data->selflag = SEL_CHAR; - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1579,7 +1580,7 @@ window_copy_cmd_cursor_down(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_down(wme, 0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1594,7 +1595,7 @@ window_copy_cmd_cursor_down_and_cancel(struct window_copy_cmd_state *cs) window_copy_cursor_down(wme, 0); if (cy == data->cy && data->oy == 0) return (WINDOW_COPY_CMD_CANCEL); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1605,7 +1606,7 @@ window_copy_cmd_cursor_left(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_left(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1619,7 +1620,7 @@ window_copy_cmd_cursor_right(struct window_copy_cmd_state *cs) window_copy_cursor_right(wme, data->screen.sel != NULL && data->rectflag); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } /* Scroll line containing the cursor to the given position. */ @@ -1687,7 +1688,7 @@ window_copy_cmd_scroll_to_mouse(struct window_copy_cmd_state *cs) tty_window_offset(&c->tty, &tty_ox, &tty_oy, &tty_sx, &tty_sy); window_copy_scroll(wp, c->tty.mouse_slider_mpos, m->y, tty_oy, scroll_exit); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } /* Scroll line containing the cursor to the top. */ @@ -1705,7 +1706,7 @@ window_copy_cmd_cursor_up(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_up(wme, 0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1736,7 +1737,7 @@ window_copy_cmd_end_of_line(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; window_copy_cursor_end_of_line(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1750,7 +1751,7 @@ window_copy_cmd_halfpage_down(struct window_copy_cmd_state *cs) if (window_copy_pagedown1(wme, 1, data->scroll_exit)) return (WINDOW_COPY_CMD_CANCEL); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1764,7 +1765,7 @@ window_copy_cmd_halfpage_down_and_cancel(struct window_copy_cmd_state *cs) if (window_copy_pagedown1(wme, 1, 1)) return (WINDOW_COPY_CMD_CANCEL); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1775,7 +1776,7 @@ window_copy_cmd_halfpage_up(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_pageup1(wme, 1); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1860,7 +1861,7 @@ window_copy_cmd_jump_again(struct window_copy_cmd_state *cs) window_copy_cursor_jump_to_back(wme); break; } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1888,7 +1889,7 @@ window_copy_cmd_jump_reverse(struct window_copy_cmd_state *cs) window_copy_cursor_jump_to(wme); break; } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1987,7 +1988,7 @@ window_copy_cmd_previous_matching_bracket(struct window_copy_cmd_state *cs) window_copy_scroll_to(wme, px, py, 0); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2108,7 +2109,7 @@ window_copy_cmd_next_matching_bracket(struct window_copy_cmd_state *cs) window_copy_scroll_to(wme, px, py, 0); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2119,7 +2120,7 @@ window_copy_cmd_next_paragraph(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_next_paragraph(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2130,7 +2131,7 @@ window_copy_cmd_next_space(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_next_word(wme, ""); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2141,7 +2142,7 @@ window_copy_cmd_next_space_end(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_next_word_end(wme, "", 0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2155,7 +2156,7 @@ window_copy_cmd_next_word(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_next_word(wme, separators); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2169,7 +2170,7 @@ window_copy_cmd_next_word_end(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_next_word_end(wme, separators, 0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2182,7 +2183,7 @@ window_copy_cmd_other_end(struct window_copy_cmd_state *cs) data->selflag = SEL_CHAR; if ((np % 2) != 0) window_copy_other_end(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2200,7 +2201,7 @@ window_copy_cmd_selection_mode(struct window_copy_cmd_state *cs) data->selflag = SEL_WORD; } else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0) data->selflag = SEL_LINE; - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2214,7 +2215,7 @@ window_copy_cmd_page_down(struct window_copy_cmd_state *cs) if (window_copy_pagedown1(wme, 0, data->scroll_exit)) return (WINDOW_COPY_CMD_CANCEL); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2227,7 +2228,7 @@ window_copy_cmd_page_down_and_cancel(struct window_copy_cmd_state *cs) if (window_copy_pagedown1(wme, 0, 1)) return (WINDOW_COPY_CMD_CANCEL); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2238,7 +2239,7 @@ window_copy_cmd_page_up(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_pageup1(wme, 0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2249,7 +2250,7 @@ window_copy_cmd_previous_paragraph(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_previous_paragraph(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2260,7 +2261,7 @@ window_copy_cmd_previous_space(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_previous_word(wme, "", 1); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2274,7 +2275,7 @@ window_copy_cmd_previous_word(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_previous_word(wme, separators, 1); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2286,7 +2287,7 @@ window_copy_cmd_rectangle_on(struct window_copy_cmd_state *cs) data->lineflag = LINE_SEL_NONE; window_copy_rectangle_set(wme, 1); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2298,7 +2299,7 @@ window_copy_cmd_rectangle_off(struct window_copy_cmd_state *cs) data->lineflag = LINE_SEL_NONE; window_copy_rectangle_set(wme, 0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2310,7 +2311,7 @@ window_copy_cmd_rectangle_toggle(struct window_copy_cmd_state *cs) data->lineflag = LINE_SEL_NONE; window_copy_rectangle_set(wme, !data->rectflag); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2320,7 +2321,7 @@ window_copy_cmd_scroll_exit_on(struct window_copy_cmd_state *cs) data->scroll_exit = 1; - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2330,7 +2331,7 @@ window_copy_cmd_scroll_exit_off(struct window_copy_cmd_state *cs) data->scroll_exit = 0; - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2340,7 +2341,7 @@ window_copy_cmd_scroll_exit_toggle(struct window_copy_cmd_state *cs) data->scroll_exit = !data->scroll_exit; - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2350,11 +2351,21 @@ window_copy_cmd_scroll_down(struct window_copy_cmd_state *cs) struct window_copy_mode_data *data = wme->data; u_int np = wme->prefix; + /* + * If at the bottom and scroll_exit is active with no selection, + * cancel/exit copy-mode. Otherwise nothing can change, so return + * WINDOW_COPY_CMD_NOTHING. + */ + if (data->oy == 0) { + if (data->scroll_exit && data->screen.sel == NULL) + return (WINDOW_COPY_CMD_CANCEL); + return (WINDOW_COPY_CMD_NOTHING); + } for (; np != 0; np--) window_copy_cursor_down(wme, 1); if (data->scroll_exit && data->oy == 0 && data->screen.sel == NULL) return (WINDOW_COPY_CMD_CANCEL); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2368,18 +2379,25 @@ window_copy_cmd_scroll_down_and_cancel(struct window_copy_cmd_state *cs) window_copy_cursor_down(wme, 1); if (data->oy == 0) return (WINDOW_COPY_CMD_CANCEL); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action window_copy_cmd_scroll_up(struct window_copy_cmd_state *cs) { struct window_mode_entry *wme = cs->wme; + struct window_copy_mode_data *data = wme->data; u_int np = wme->prefix; + /* + * If at the top, nothing can change, so return WINDOW_COPY_CMD_NOTHING + * and do not repaint anything. + */ + if (data->oy == screen_hsize(data->backing)) + return (WINDOW_COPY_CMD_NOTHING); for (; np != 0; np--) window_copy_cursor_up(wme, 1); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2396,7 +2414,7 @@ window_copy_cmd_search_again(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_search_down(wme, data->searchregex); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2413,7 +2431,7 @@ window_copy_cmd_search_reverse(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_search_up(wme, data->searchregex); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2481,7 +2499,7 @@ window_copy_cmd_select_word(struct window_copy_cmd_state *cs) else { window_copy_update_cursor(wme, px, data->cy); if (window_copy_update_selection(wme, 1, 1)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } data->endselrx = data->cx; data->endselry = screen_hsize(data->backing) + data->cy - data->oy; @@ -2511,7 +2529,7 @@ window_copy_cmd_start_of_line(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; window_copy_cursor_start_of_line(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2590,7 +2608,7 @@ window_copy_cmd_pipe_no_clear(struct window_copy_cmd_state *cs) window_copy_pipe(wme, s, command); free(command); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2621,7 +2639,7 @@ window_copy_cmd_goto_line(struct window_copy_cmd_state *cs) if (*arg0 != '\0') window_copy_goto_line(wme, arg0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2639,7 +2657,7 @@ window_copy_cmd_jump_backward(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_jump_back(wme); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2657,7 +2675,7 @@ window_copy_cmd_jump_forward(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_jump(wme); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2675,7 +2693,7 @@ window_copy_cmd_jump_to_backward(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_jump_to_back(wme); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2693,7 +2711,7 @@ window_copy_cmd_jump_to_forward(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_jump_to(wme); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2702,7 +2720,7 @@ window_copy_cmd_jump_to_mark(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; window_copy_jump_to_mark(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2711,7 +2729,7 @@ window_copy_cmd_next_prompt(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; window_copy_cursor_prompt(wme, 1, args_has(cs->wargs, 'o')); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2720,7 +2738,7 @@ window_copy_cmd_previous_prompt(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; window_copy_cursor_prompt(wme, 0, args_has(cs->wargs, 'o')); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2731,7 +2749,7 @@ window_copy_cmd_search_backward(struct window_copy_cmd_state *cs) u_int np = wme->prefix; if (!window_copy_expand_search_string(cs)) - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); if (data->searchstr != NULL) { data->searchtype = WINDOW_COPY_SEARCHUP; @@ -2740,7 +2758,7 @@ window_copy_cmd_search_backward(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_search_up(wme, 1); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2751,7 +2769,7 @@ window_copy_cmd_search_backward_text(struct window_copy_cmd_state *cs) u_int np = wme->prefix; if (!window_copy_expand_search_string(cs)) - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); if (data->searchstr != NULL) { data->searchtype = WINDOW_COPY_SEARCHUP; @@ -2760,7 +2778,7 @@ window_copy_cmd_search_backward_text(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_search_up(wme, 0); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2771,7 +2789,7 @@ window_copy_cmd_search_forward(struct window_copy_cmd_state *cs) u_int np = wme->prefix; if (!window_copy_expand_search_string(cs)) - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); if (data->searchstr != NULL) { data->searchtype = WINDOW_COPY_SEARCHDOWN; @@ -2780,7 +2798,7 @@ window_copy_cmd_search_forward(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_search_down(wme, 1); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2791,7 +2809,7 @@ window_copy_cmd_search_forward_text(struct window_copy_cmd_state *cs) u_int np = wme->prefix; if (!window_copy_expand_search_string(cs)) - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); if (data->searchstr != NULL) { data->searchtype = WINDOW_COPY_SEARCHDOWN; @@ -2800,7 +2818,7 @@ window_copy_cmd_search_forward_text(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_search_down(wme, 0); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2811,7 +2829,7 @@ window_copy_cmd_search_backward_incremental(struct window_copy_cmd_state *cs) const char *arg0 = args_string(cs->wargs, 0); const char *ss = data->searchstr; char prefix; - enum window_copy_cmd_action action = WINDOW_COPY_CMD_NOTHING; + enum window_copy_cmd_action action = WINDOW_COPY_CMD_MOVE; data->timeout = 0; @@ -2868,7 +2886,7 @@ window_copy_cmd_search_forward_incremental(struct window_copy_cmd_state *cs) const char *arg0 = args_string(cs->wargs, 0); const char *ss = data->searchstr; char prefix; - enum window_copy_cmd_action action = WINDOW_COPY_CMD_NOTHING; + enum window_copy_cmd_action action = WINDOW_COPY_CMD_MOVE; data->timeout = 0; @@ -2989,7 +3007,7 @@ window_copy_refresh_timer(__unused int fd, __unused short events, void *arg) follow = (data->oy == 0 && data->cy == screen_size_y(&data->screen) - 1); window_copy_do_refresh(wme, follow); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); /* The timer runs outside key handling, so force a repaint. */ wp->flags |= PANE_REDRAW; wp->flags &= ~PANE_UNSEENCHANGES; @@ -3026,14 +3044,14 @@ static enum window_copy_cmd_action window_copy_cmd_refresh_on(struct window_copy_cmd_state *cs) { window_copy_refresh_start(cs->wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action window_copy_cmd_refresh_off(struct window_copy_cmd_state *cs) { window_copy_refresh_stop(cs->wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -3045,7 +3063,7 @@ window_copy_cmd_refresh_toggle(struct window_copy_cmd_state *cs) window_copy_refresh_stop(cs->wme); else window_copy_refresh_start(cs->wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -3716,7 +3734,7 @@ window_copy_command(struct window_mode_entry *wme, struct client *c, cs.s = s; cs.wl = wl; - action = WINDOW_COPY_CMD_NOTHING; + action = WINDOW_COPY_CMD_MOVE; for (i = 0; i < nitems(window_copy_cmd_table); i++) { if (strcmp(window_copy_cmd_table[i].command, command) == 0) { flags = window_copy_cmd_table[i].flags; @@ -3755,7 +3773,7 @@ window_copy_command(struct window_mode_entry *wme, struct client *c, window_copy_clear_marks(wme); data->searchx = data->searchy = -1; } - if (action == WINDOW_COPY_CMD_NOTHING) + if (action == WINDOW_COPY_CMD_MOVE) action = WINDOW_COPY_CMD_REDRAW; } wme->prefix = 1; @@ -3763,15 +3781,13 @@ window_copy_command(struct window_mode_entry *wme, struct client *c, if (action == WINDOW_COPY_CMD_CANCEL) window_pane_reset_mode(wp); else if (action == WINDOW_COPY_CMD_REDRAW) - window_copy_redraw_screen(wme); - else if (action == WINDOW_COPY_CMD_NOTHING) { + window_copy_redraw_screen(wme, 1); + else if (action == WINDOW_COPY_CMD_MOVE) { /* - * Nothing is not actually nothing - most commands at least - * move the cursor (what would be the point of a command that - * literally does nothing?) and in that case we need to redraw - * the first line to update the indicator. + * If the cursor moves or a similar trivial change is made, + * only redraw the first line to update the indicator. */ - window_copy_redraw_lines(wme, 0, 1); + window_copy_redraw_lines(wme, 0, 1, 0); } } @@ -3808,7 +3824,7 @@ window_copy_scroll_to(struct window_mode_entry *wme, u_int px, u_int py, if (data->oy != old_oy) window_pane_scrollbar_show(wme->wp, 1); if (!no_redraw) - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static int @@ -4596,7 +4612,7 @@ window_copy_search(struct window_mode_entry *wme, int direction, int regex) } } } - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); screen_free(&ss); return (found); @@ -4849,7 +4865,7 @@ window_copy_goto_line(struct window_mode_entry *wme, const char *linestr) } window_copy_update_selection(wme, 1, 0); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static void @@ -5119,7 +5135,7 @@ window_copy_set_line_numbers(struct window_pane *wp, int enabled) if (data == NULL || data->line_numbers == enabled) return; data->line_numbers = enabled; - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } int @@ -5141,7 +5157,7 @@ window_copy_get_current_offset(struct window_pane *wp, u_int *offset, static void window_copy_write_line(struct window_mode_entry *wme, - struct screen_write_ctx *ctx, u_int py) + struct screen_write_ctx *ctx, u_int py, int clear) { struct window_pane *wp = wme->wp; struct window_copy_mode_data *data = wme->data; @@ -5166,7 +5182,8 @@ window_copy_write_line(struct window_mode_entry *wme, content_sx = sx; screen_write_cursormove(ctx, 0, py, 0); - screen_write_clearline(ctx, 8); + if (clear) + screen_write_clearline(ctx, 8); ft = format_create_defaults(NULL, NULL, NULL, NULL, wp); @@ -5237,7 +5254,7 @@ window_copy_write_lines(struct window_mode_entry *wme, u_int yy; for (yy = py; yy < py + ny; yy++) - window_copy_write_line(wme, ctx, yy); + window_copy_write_line(wme, ctx, yy, 1); } static void @@ -5265,11 +5282,12 @@ window_copy_redraw_selection(struct window_mode_entry *wme, u_int old_y) if (end < gd->sy + data->oy - 1) end++; } - window_copy_redraw_lines(wme, start, end - start + 1); + window_copy_redraw_lines(wme, start, end - start + 1, 0); } static void -window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) +window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny, + int clear) { struct window_pane *wp = wme->wp; struct window_copy_mode_data *data = wme->data; @@ -5280,7 +5298,7 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) if (window_copy_line_number_width(wme) != 0) { screen_write_start(&ctx, &data->screen); for (i = py; i < py + ny; i++) - window_copy_write_line(wme, &ctx, i); + window_copy_write_line(wme, &ctx, i, clear); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); @@ -5294,7 +5312,7 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) else screen_write_start_pane(&ctx, wp, NULL); for (i = py; i < py + ny; i++) - window_copy_write_line(wme, &ctx, i); + window_copy_write_line(wme, &ctx, i, clear); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); @@ -5304,11 +5322,11 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) } static void -window_copy_redraw_screen(struct window_mode_entry *wme) +window_copy_redraw_screen(struct window_mode_entry *wme, int clear) { struct window_copy_mode_data *data = wme->data; - window_copy_redraw_lines(wme, 0, screen_size_y(&data->screen)); + window_copy_redraw_lines(wme, 0, screen_size_y(&data->screen), clear); } static void @@ -5318,7 +5336,7 @@ window_copy_style_changed(struct window_mode_entry *wme) if (data->screen.sel != NULL) window_copy_set_selection(wme, 0, 1); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static void @@ -5439,7 +5457,7 @@ window_copy_update_cursor(struct window_mode_entry *wme, u_int cx, u_int cy) if (s->sel != NULL || data->lineflag != LINE_SEL_NONE || old_cy != data->cy) { - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 0); return; } if (width >= screen_size_x(s)) @@ -5447,7 +5465,7 @@ window_copy_update_cursor(struct window_mode_entry *wme, u_int cx, u_int cy) else content_sx = screen_size_x(s) - width; if (old_cx >= content_sx || data->cx >= content_sx) { - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 0); return; } screen_write_start_pane(&ctx, wp, NULL); @@ -5458,9 +5476,9 @@ window_copy_update_cursor(struct window_mode_entry *wme, u_int cx, u_int cy) return; } if (old_cx == screen_size_x(s)) - window_copy_redraw_lines(wme, old_cy, 1); + window_copy_redraw_lines(wme, old_cy, 1, 1); if (data->cx == screen_size_x(s)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); else { screen_write_start_pane(&ctx, wp, NULL); screen_write_cursormove(&ctx, @@ -5587,16 +5605,16 @@ window_copy_set_selection(struct window_mode_entry *wme, int may_redraw, cy = data->cy; if (data->cursordrag == CURSORDRAG_ENDSEL) { if (sy < cy) - window_copy_redraw_lines(wme, sy, cy - sy + 1); + window_copy_redraw_lines(wme, sy, cy - sy + 1, 1); else - window_copy_redraw_lines(wme, cy, sy - cy + 1); + window_copy_redraw_lines(wme, cy, sy - cy + 1, 1); } else { if (endsy < cy) { window_copy_redraw_lines(wme, endsy, - cy - endsy + 1); + cy - endsy + 1, 1); } else { window_copy_redraw_lines(wme, cy, - endsy - cy + 1); + endsy - cy + 1, 1); } } } @@ -6076,7 +6094,7 @@ window_copy_other_end(struct window_mode_entry *wme) data->cx = hsize; window_copy_update_selection(wme, 1, 1); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static void @@ -6153,9 +6171,9 @@ window_copy_cursor_up(struct window_mode_entry *wme, int scroll_only) window_copy_scroll_down(wme, 1); if (scroll_only) { if (data->cy == screen_size_y(s) - 1) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 0); else - window_copy_redraw_lines(wme, data->cy, 2); + window_copy_redraw_lines(wme, data->cy, 2, 0); } } else { if (norectsel) { @@ -6165,9 +6183,9 @@ window_copy_cursor_up(struct window_mode_entry *wme, int scroll_only) window_copy_update_cursor(wme, data->cx, data->cy - 1); if (window_copy_update_selection(wme, 1, 0)) { if (data->cy == screen_size_y(s) - 1) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); else - window_copy_redraw_lines(wme, data->cy, 2); + window_copy_redraw_lines(wme, data->cy, 2, 1); } } @@ -6179,7 +6197,7 @@ window_copy_cursor_up(struct window_mode_entry *wme, int scroll_only) { window_copy_update_cursor(wme, px, data->cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } } @@ -6192,13 +6210,13 @@ window_copy_cursor_up(struct window_mode_entry *wme, int scroll_only) px = window_copy_find_length(wme, py); window_copy_update_cursor(wme, px, data->cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } else if (data->lineflag == LINE_SEL_RIGHT_LEFT) { window_copy_update_cursor(wme, 0, data->cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } } @@ -6232,7 +6250,7 @@ window_copy_cursor_down(struct window_mode_entry *wme, int scroll_only) data->cx = data->lastcx; window_copy_scroll_up(wme, 1); if (scroll_only && data->cy > 0) - window_copy_redraw_lines(wme, data->cy - 1, 2); + window_copy_redraw_lines(wme, data->cy - 1, 2, 0); } else { if (norectsel) { window_copy_update_cursor(wme, data->lastcx, @@ -6240,7 +6258,7 @@ window_copy_cursor_down(struct window_mode_entry *wme, int scroll_only) } else window_copy_update_cursor(wme, data->cx, data->cy + 1); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy - 1, 2); + window_copy_redraw_lines(wme, data->cy - 1, 2, 1); } if (norectsel) { @@ -6251,7 +6269,7 @@ window_copy_cursor_down(struct window_mode_entry *wme, int scroll_only) { window_copy_update_cursor(wme, px, data->cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } } @@ -6264,13 +6282,13 @@ window_copy_cursor_down(struct window_mode_entry *wme, int scroll_only) px = window_copy_find_length(wme, py); window_copy_update_cursor(wme, px, data->cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } else if (data->lineflag == LINE_SEL_RIGHT_LEFT) { window_copy_update_cursor(wme, 0, data->cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } } @@ -6540,7 +6558,7 @@ window_copy_cursor_prompt(struct window_mode_entry *wme, int direction, } window_copy_update_selection(wme, 1, 0); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static void @@ -6564,21 +6582,22 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny) if (window_copy_line_numbers_active(wme)) { if (window_copy_line_number_mode(wme) != WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 0); return; } screen_write_start(&ctx, &data->screen); screen_write_cursormove(&ctx, 0, 0, 0); screen_write_deleteline(&ctx, ny, 8); window_copy_write_lines(wme, &ctx, screen_size_y(s) - ny, ny); - window_copy_write_line(wme, &ctx, 0); + window_copy_write_line(wme, &ctx, 0, 0); if (screen_size_y(s) > 1) - window_copy_write_line(wme, &ctx, 1); + window_copy_write_line(wme, &ctx, 1, 0); if (screen_size_y(s) > 3) - window_copy_write_line(wme, &ctx, screen_size_y(s) - 2); + window_copy_write_line(wme, &ctx, + screen_size_y(s) - 2, 0); if (s->sel != NULL && screen_size_y(s) > ny) { window_copy_write_line(wme, &ctx, - screen_size_y(s) - ny - 1); + screen_size_y(s) - ny - 1, 0); } screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), @@ -6595,13 +6614,14 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny) screen_write_cursormove(&ctx, 0, 0, 0); screen_write_deleteline(&ctx, ny, 8); window_copy_write_lines(wme, &ctx, screen_size_y(s) - ny, ny); - window_copy_write_line(wme, &ctx, 0); + window_copy_write_line(wme, &ctx, 0, 0); if (screen_size_y(s) > 1) - window_copy_write_line(wme, &ctx, 1); + window_copy_write_line(wme, &ctx, 1, 0); if (screen_size_y(s) > 3) - window_copy_write_line(wme, &ctx, screen_size_y(s) - 2); + window_copy_write_line(wme, &ctx, screen_size_y(s) - 2, 0); if (s->sel != NULL && screen_size_y(s) > ny) - window_copy_write_line(wme, &ctx, screen_size_y(s) - ny - 1); + window_copy_write_line(wme, &ctx, screen_size_y(s) - ny - 1, + 0); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); @@ -6633,7 +6653,7 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) if (window_copy_line_numbers_active(wme)) { if (window_copy_line_number_mode(wme) != WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 0); return; } screen_write_start(&ctx, &data->screen); @@ -6641,9 +6661,9 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) screen_write_insertline(&ctx, ny, 8); window_copy_write_lines(wme, &ctx, 0, ny); if (s->sel != NULL && screen_size_y(s) > ny) - window_copy_write_line(wme, &ctx, ny); + window_copy_write_line(wme, &ctx, ny, 0); else if (ny == 1) - window_copy_write_line(wme, &ctx, 1); + window_copy_write_line(wme, &ctx, 1, 0); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); @@ -6660,9 +6680,9 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) screen_write_insertline(&ctx, ny, 8); window_copy_write_lines(wme, &ctx, 0, ny); if (s->sel != NULL && screen_size_y(s) > ny) - window_copy_write_line(wme, &ctx, ny); + window_copy_write_line(wme, &ctx, ny, 0); else if (ny == 1) /* nuke position */ - window_copy_write_line(wme, &ctx, 1); + window_copy_write_line(wme, &ctx, 1, 0); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); screen_write_stop(&ctx); @@ -6683,7 +6703,7 @@ window_copy_rectangle_set(struct window_mode_entry *wme, int rectflag) window_copy_update_cursor(wme, px, data->cy); window_copy_update_selection(wme, 1, 0); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static void @@ -6761,7 +6781,7 @@ window_copy_start_drag(struct client *c, struct mouse_event *m) break; } - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); window_copy_drag_update(c, m); } @@ -6856,7 +6876,7 @@ window_copy_jump_to_mark(struct window_mode_entry *wme) data->my = tmy; data->showmark = 1; window_copy_update_selection(wme, 0, 0); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } /* Scroll up if the cursor went off the visible screen. */ @@ -6882,7 +6902,7 @@ window_copy_acquire_cursor_up(struct window_mode_entry *wme, u_int hsize, } window_copy_update_cursor(wme, px, cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, cy, nd); + window_copy_redraw_lines(wme, cy, nd, 1); } /* Scroll down if the cursor went off the visible screen. */ @@ -6911,5 +6931,5 @@ window_copy_acquire_cursor_down(struct window_mode_entry *wme, u_int hsize, else window_copy_update_cursor(wme, px, cy); if (window_copy_update_selection(wme, 1, no_reset)) - window_copy_redraw_lines(wme, oldy, nd); + window_copy_redraw_lines(wme, oldy, nd, 1); } From 1508bec957cddbb8be4d8ecee06a2ed11e5d401d Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 10 Jul 2026 07:25:05 +0000 Subject: [PATCH 098/127] Don't bother to send notifications to clients with CLIENT_EXIT, GitHub issue 5357 from Ben Maurer. --- control-notify.c | 1 + control.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/control-notify.c b/control-notify.c index 021f9884a..e85660297 100644 --- a/control-notify.c +++ b/control-notify.c @@ -25,6 +25,7 @@ #define CONTROL_SHOULD_NOTIFY_CLIENT(c) \ ((c) != NULL && ((c)->flags & CLIENT_CONTROL) && \ + (~(c)->flags & CLIENT_EXIT) && \ (c)->control_state != NULL) void diff --git a/control.c b/control.c index 1b026e849..c1cc79e25 100644 --- a/control.c +++ b/control.c @@ -376,7 +376,7 @@ control_write_output(struct client *c, struct window_pane *wp) if (winlink_find_by_window(&c->session->windows, wp->window) == NULL) return; - if (c->flags & CONTROL_IGNORE_FLAGS) { + if (c->flags & (CONTROL_IGNORE_FLAGS|CLIENT_EXIT)) { cp = control_get_pane(c, wp); if (cp != NULL) goto ignore; From d29aa12117a7348f04d60350f8f2794b7087e016 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 10 Jul 2026 13:38:45 +0000 Subject: [PATCH 099/127] Replace the notification system with events. Events can carry a payload of additional payload (to reduce problems with lifetime of objects) and are delivered to one or more event sinks. This is more powerful and reduces the complex dependencies between control mode and hooks. Events are now used for hooks, control mode notifications and for monitors (set-hook -B). wait-for can now wait for an event to fire (-E flag, with -F to for filter), with -v to print the payload, as well as listing (-l) waiting clients on an event and forcing one to wake (-w). A few additional hooks are also now available (pane-created, pane-resized, etc) and some of the existing ones have additional format variables available. --- Makefile | 6 +- alerts.c | 8 +- arguments.c | 2 +- attributes.c | 2 +- cfg.c | 2 +- client.c | 2 +- cmd-attach-session.c | 4 +- cmd-bind-key.c | 2 +- cmd-break-pane.c | 19 +- cmd-capture-pane.c | 2 +- cmd-choose-tree.c | 2 +- cmd-command-prompt.c | 2 +- cmd-confirm-before.c | 2 +- cmd-copy-mode.c | 2 +- cmd-detach-client.c | 2 +- cmd-display-menu.c | 2 +- cmd-display-message.c | 2 +- cmd-display-panes.c | 2 +- cmd-find-window.c | 2 +- cmd-find.c | 2 +- cmd-has-session.c | 46 --- cmd-if-shell.c | 2 +- cmd-join-pane.c | 14 +- cmd-kill-pane.c | 2 +- cmd-kill-server.c | 2 +- cmd-kill-session.c | 2 +- cmd-kill-window.c | 2 +- cmd-list-buffers.c | 2 +- cmd-list-clients.c | 2 +- cmd-list-commands.c | 2 +- cmd-list-keys.c | 2 +- cmd-list-panes.c | 2 +- cmd-list-sessions.c | 2 +- cmd-list-windows.c | 2 +- cmd-list.c | 103 ------ cmd-load-buffer.c | 2 +- cmd-lock-server.c | 2 +- cmd-move-window.c | 2 +- cmd-new-session.c | 4 +- cmd-new-window.c | 2 +- cmd-parse.y | 2 +- cmd-paste-buffer.c | 2 +- cmd-pipe-pane.c | 2 +- cmd-queue.c | 70 +--- cmd-refresh-client.c | 2 +- cmd-rename-session.c | 13 +- cmd-rename-window.c | 2 +- cmd-resize-pane.c | 4 +- cmd-resize-window.c | 2 +- cmd-respawn-pane.c | 2 +- cmd-respawn-window.c | 2 +- cmd-rotate-window.c | 2 +- cmd-run-shell.c | 2 +- cmd-save-buffer.c | 2 +- cmd-select-layout.c | 4 +- cmd-select-pane.c | 111 +++++-- cmd-select-window.c | 2 +- cmd-send-keys.c | 2 +- cmd-send-prefix.c | 57 ---- cmd-server-access.c | 2 +- cmd-set-buffer.c | 2 +- cmd-set-environment.c | 2 +- cmd-set-option.c | 68 +++- cmd-show-buffer.c | 112 ------- cmd-show-environment.c | 2 +- cmd-show-messages.c | 2 +- cmd-show-options.c | 4 +- cmd-show-prompt-history.c | 2 +- cmd-source-file.c | 2 +- cmd-split-window.c | 11 +- cmd-swap-pane.c | 6 +- cmd-swap-window.c | 2 +- cmd-switch-client.c | 2 +- cmd-unbind-key.c | 2 +- cmd-wait-for.c | 261 ++++++++++++++- cmd.c | 2 +- colour.c | 2 +- control-notify.c | 214 ++++++++---- control.c | 2 +- environ.c | 2 +- events-payload.c | 672 ++++++++++++++++++++++++++++++++++++++ events.c | 199 +++++++++++ file.c | 2 +- format-draw.c | 2 +- format.c | 2 +- fuzzy.c | 2 +- grid-reader.c | 2 +- grid-view.c | 2 +- grid.c | 2 +- hooks.c | 461 ++++++++++++++++++++++++++ hyperlinks.c | 2 +- input-keys.c | 2 +- input.c | 27 +- job.c | 2 +- key-bindings.c | 2 +- key-string.c | 2 +- layout-custom.c | 4 +- layout-set.c | 14 +- layout.c | 6 +- log.c | 2 +- menu.c | 2 +- mode-key.c | 261 --------------- mode-tree.c | 2 +- monitor.c | 4 +- names.c | 2 +- notify.c | 516 ----------------------------- options-table.c | 11 +- options.c | 4 +- paste.c | 24 +- popup.c | 2 +- proc.c | 2 +- procname.c | 2 +- prompt-history.c | 2 +- prompt.c | 2 +- regsub.c | 2 +- resize.c | 6 +- screen-redraw.c | 2 +- screen-write.c | 2 +- screen.c | 2 +- server-acl.c | 2 +- server-client.c | 14 +- server-fn.c | 36 +- server.c | 4 +- session.c | 16 +- sort.c | 2 +- spawn.c | 49 ++- status.c | 2 +- style.c | 2 +- tmux-protocol.h | 2 +- tmux.1 | 101 +++++- tmux.c | 2 +- tmux.h | 126 +++++-- tty-acs.c | 2 +- tty-draw.c | 2 +- tty-features.c | 2 +- tty-keys.c | 6 +- tty-term.c | 2 +- tty.c | 2 +- utf8-combined.c | 2 +- utf8.c | 2 +- window-border.c | 2 +- window-buffer.c | 2 +- window-client.c | 2 +- window-clock.c | 2 +- window-copy.c | 6 +- window-customize.c | 2 +- window-switch.c | 2 +- window-tree.c | 2 +- window-visible.c | 2 +- window.c | 158 +++++++-- xmalloc.c | 2 +- xmalloc.h | 2 +- xterm-keys.c | 252 -------------- 153 files changed, 2543 insertions(+), 1787 deletions(-) delete mode 100644 cmd-has-session.c delete mode 100644 cmd-list.c delete mode 100644 cmd-send-prefix.c delete mode 100644 cmd-show-buffer.c create mode 100644 events-payload.c create mode 100644 events.c create mode 100644 hooks.c delete mode 100644 mode-key.c delete mode 100644 notify.c delete mode 100644 xterm-keys.c diff --git a/Makefile b/Makefile index 165d0d47c..d73312013 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# $OpenBSD$ +# $OpenBSD: Makefile,v 1.120 2026/07/10 13:38:45 nicm Exp $ PROG= tmux SRCS= alerts.c \ @@ -76,6 +76,8 @@ SRCS= alerts.c \ control-notify.c \ control.c \ environ.c \ + events.c \ + events-payload.c \ file.c \ format.c \ fuzzy.c \ @@ -83,6 +85,7 @@ SRCS= alerts.c \ grid-reader.c \ grid-view.c \ grid.c \ + hooks.c \ hyperlinks.c \ input-keys.c \ input.c \ @@ -97,7 +100,6 @@ SRCS= alerts.c \ mode-tree.c \ monitor.c \ names.c \ - notify.c \ options-table.c \ options.c \ paste.c \ diff --git a/alerts.c b/alerts.c index a2b4d6541..824fb7381 100644 --- a/alerts.c +++ b/alerts.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: alerts.c,v 1.35 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott @@ -206,7 +206,7 @@ alerts_check_bell(struct window *w) } if (!alerts_action_applies(wl, "bell-action")) continue; - notify_winlink("alert-bell", wl); + events_fire_winlink("alert-bell", wl); if (s->flags & SESSION_ALERTED) continue; @@ -242,7 +242,7 @@ alerts_check_activity(struct window *w) } if (!alerts_action_applies(wl, "activity-action")) continue; - notify_winlink("alert-activity", wl); + events_fire_winlink("alert-activity", wl); if (s->flags & SESSION_ALERTED) continue; @@ -278,7 +278,7 @@ alerts_check_silence(struct window *w) } if (!alerts_action_applies(wl, "silence-action")) continue; - notify_winlink("alert-silence", wl); + events_fire_winlink("alert-silence", wl); if (s->flags & SESSION_ALERTED) continue; diff --git a/arguments.c b/arguments.c index cab40aa6e..baafd6d52 100644 --- a/arguments.c +++ b/arguments.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: arguments.c,v 1.66 2026/06/26 09:54:56 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott diff --git a/attributes.c b/attributes.c index b0c9ef518..d853b8db0 100644 --- a/attributes.c +++ b/attributes.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: attributes.c,v 1.13 2026/07/02 08:51:05 nicm Exp $ */ /* * Copyright (c) 2009 Joshua Elsasser diff --git a/cfg.c b/cfg.c index 450ab88fb..7a79a6025 100644 --- a/cfg.c +++ b/cfg.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cfg.c,v 1.89 2026/06/25 11:39:11 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/client.c b/client.c index fcc49c395..1b7fdb043 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: client.c,v 1.165 2025/04/25 12:25:32 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-attach-session.c b/cmd-attach-session.c index 32b1e0cbd..050c7ca38 100644 --- a/cmd-attach-session.c +++ b/cmd-attach-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-attach-session.c,v 1.91 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -163,7 +163,7 @@ cmd_attach_session(struct cmdq_item *item, const char *tflag, int dflag, if (~c->flags & CLIENT_CONTROL) proc_send(c->peer, MSG_READY, -1, NULL, 0); - notify_client("client-attached", c); + events_fire_client("client-attached", c); c->flags |= CLIENT_ATTACHED; } diff --git a/cmd-bind-key.c b/cmd-bind-key.c index 01f8e9614..0d7636ff7 100644 --- a/cmd-bind-key.c +++ b/cmd-bind-key.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-bind-key.c,v 1.47 2025/04/09 07:03:04 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-break-pane.c b/cmd-break-pane.c index 3891534a0..8d85c57b2 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-break-pane.c,v 1.72 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -80,7 +80,7 @@ cmd_break_pane_float(struct cmdq_item *item, struct args *args, window_set_active_pane(w, wp, 1); layout_fix_offsets(w); layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); @@ -99,7 +99,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) struct session *dst_s = target->s; struct window_pane *wp = source->wp; struct window *w = wl->window; - char *newname, *cause, *cp; + char *cause, *cp; int idx = target->idx, before; const char *template, *name = args_get(args, 'n'); @@ -151,6 +151,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) layout_close_pane(wp); w = wp->window = window_create(w->sx, w->sy, w->xpixel, w->ypixel); + window_add_ref(w, __func__); options_set_parent(wp->options, w->options); wp->flags |= (PANE_STYLECHANGED|PANE_THEMECHANGED); TAILQ_INSERT_HEAD(&w->panes, wp, entry); @@ -158,12 +159,11 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) w->active = wp; w->latest = tc; - if (name == NULL) { - newname = default_window_name(w); - window_set_name(w, newname, 0); - free(newname); - } else { - window_set_name(w, name, 0); + free(w->name); + if (name == NULL) + w->name = default_window_name(w); + else { + w->name = clean_name(name, 0); options_set_number(w->options, "automatic-rename", 0); } @@ -174,6 +174,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) if (idx == -1) idx = -1 - options_get_number(dst_s->options, "base-index"); wl = session_attach(dst_s, w, idx, &cause); /* can't fail */ + window_remove_ref(w, __func__); if (!args_has(args, 'd')) { session_select(dst_s, wl->idx); cmd_find_from_session(current, dst_s, 0); diff --git a/cmd-capture-pane.c b/cmd-capture-pane.c index febf29697..d83590f90 100644 --- a/cmd-capture-pane.c +++ b/cmd-capture-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-capture-pane.c,v 1.67 2026/07/02 10:34:14 nicm Exp $ */ /* * Copyright (c) 2009 Jonathan Alvarado diff --git a/cmd-choose-tree.c b/cmd-choose-tree.c index 4a6bd152b..f9ce7d3b7 100644 --- a/cmd-choose-tree.c +++ b/cmd-choose-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-choose-tree.c,v 1.57 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2012 Thomas Adam diff --git a/cmd-command-prompt.c b/cmd-command-prompt.c index 045ef0b12..ac1117347 100644 --- a/cmd-command-prompt.c +++ b/cmd-command-prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-command-prompt.c,v 1.75 2026/06/25 11:39:11 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-confirm-before.c b/cmd-confirm-before.c index 54bc9a2a1..fda3af772 100644 --- a/cmd-confirm-before.c +++ b/cmd-confirm-before.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-confirm-before.c,v 1.61 2026/06/24 10:55:39 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-copy-mode.c b/cmd-copy-mode.c index cfa60f59f..051cb58a2 100644 --- a/cmd-copy-mode.c +++ b/cmd-copy-mode.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-copy-mode.c,v 1.53 2026/07/08 08:07:42 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-detach-client.c b/cmd-detach-client.c index b5a4cf1c1..82ce9a00b 100644 --- a/cmd-detach-client.c +++ b/cmd-detach-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-detach-client.c,v 1.39 2026/05/22 15:22:43 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-display-menu.c b/cmd-display-menu.c index 97973b8bd..c2d83ff27 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-display-menu.c,v 1.51 2026/07/07 09:45:09 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/cmd-display-message.c b/cmd-display-message.c index 3138d9223..f786006c3 100644 --- a/cmd-display-message.c +++ b/cmd-display-message.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-display-message.c,v 1.65 2026/02/23 08:46:57 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-display-panes.c b/cmd-display-panes.c index f8991dfe1..176238a1f 100644 --- a/cmd-display-panes.c +++ b/cmd-display-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-display-panes.c,v 1.56 2026/06/25 16:32:42 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-find-window.c b/cmd-find-window.c index 560998370..28a8949cd 100644 --- a/cmd-find-window.c +++ b/cmd-find-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-find-window.c,v 1.56 2023/12/27 20:42:01 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-find.c b/cmd-find.c index 7ce714583..5173591f9 100644 --- a/cmd-find.c +++ b/cmd-find.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-find.c,v 1.86 2026/07/02 08:47:25 nicm Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott diff --git a/cmd-has-session.c b/cmd-has-session.c deleted file mode 100644 index a873b2046..000000000 --- a/cmd-has-session.c +++ /dev/null @@ -1,46 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2007 Nicholas Marriott - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include "tmux.h" - -/* - * Cause client to report an error and exit with 1 if session doesn't exist. - */ - -enum cmd_retval cmd_has_session_exec(struct cmd *, struct cmd_q *); - -const struct cmd_entry cmd_has_session_entry = { - "has-session", "has", - "t:", 0, 0, - CMD_TARGET_SESSION_USAGE, - 0, - cmd_has_session_exec -}; - -enum cmd_retval -cmd_has_session_exec(struct cmd *self, struct cmd_q *cmdq) -{ - struct args *args = self->args; - - if (cmd_find_session(cmdq, args_get(args, 't'), 0) == NULL) - return (CMD_RETURN_ERROR); - - return (CMD_RETURN_NORMAL); -} diff --git a/cmd-if-shell.c b/cmd-if-shell.c index f0c3637cc..49b92a996 100644 --- a/cmd-if-shell.c +++ b/cmd-if-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-if-shell.c,v 1.86 2025/08/01 09:05:51 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 122505d81..1b290a01e 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-join-pane.c,v 1.69 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -187,7 +187,7 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl, lc->g.yoff = yoff; layout_fix_panes(w, NULL); } - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); @@ -257,7 +257,7 @@ cmd_join_pane_move(struct cmdq_item *item, struct args *args, lc->g.xoff = xoff; lc->g.yoff = yoff; layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -359,7 +359,7 @@ cmd_join_pane_zindex(struct cmdq_item *item, struct winlink *wl, else TAILQ_INSERT_TAIL(&w->z_index, wp, zentry); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); @@ -398,7 +398,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); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); @@ -502,8 +502,8 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) if (window_count_panes(src_w, 1) == 0) server_kill_window(src_w, 1); else - notify_window("window-layout-changed", src_w); - notify_window("window-layout-changed", dst_w); + events_fire_window("window-layout-changed", src_w); + events_fire_window("window-layout-changed", dst_w); return (CMD_RETURN_NORMAL); } diff --git a/cmd-kill-pane.c b/cmd-kill-pane.c index 836551f01..f634a7cd7 100644 --- a/cmd-kill-pane.c +++ b/cmd-kill-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-kill-pane.c,v 1.34 2026/06/09 21:22:22 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-kill-server.c b/cmd-kill-server.c index 7bb79e3d4..8b70be3d8 100644 --- a/cmd-kill-server.c +++ b/cmd-kill-server.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-kill-server.c,v 1.20 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-kill-session.c b/cmd-kill-session.c index 4bf36d785..7edd2a30a 100644 --- a/cmd-kill-session.c +++ b/cmd-kill-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-kill-session.c,v 1.31 2026/06/09 12:57:40 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-kill-window.c b/cmd-kill-window.c index 0253605f1..e7a6ec32e 100644 --- a/cmd-kill-window.c +++ b/cmd-kill-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-kill-window.c,v 1.30 2026/06/09 12:57:40 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-buffers.c b/cmd-list-buffers.c index 347c69341..82ee58947 100644 --- a/cmd-list-buffers.c +++ b/cmd-list-buffers.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-buffers.c,v 1.41 2026/02/27 08:25:12 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-clients.c b/cmd-list-clients.c index ae235d83c..77f3912ca 100644 --- a/cmd-list-clients.c +++ b/cmd-list-clients.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-clients.c,v 1.42 2026/02/27 08:25:12 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-commands.c b/cmd-list-commands.c index 0b0f912d8..5e9c4430c 100644 --- a/cmd-list-commands.c +++ b/cmd-list-commands.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-commands.c,v 1.15 2026/02/24 08:22:13 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-keys.c b/cmd-list-keys.c index 7aa38c61e..e8617c2e3 100644 --- a/cmd-list-keys.c +++ b/cmd-list-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-keys.c,v 1.78 2026/07/01 13:12:17 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-panes.c b/cmd-list-panes.c index 094ca850c..54c8db7d3 100644 --- a/cmd-list-panes.c +++ b/cmd-list-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-panes.c,v 1.40 2026/06/01 14:01:09 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-list-sessions.c b/cmd-list-sessions.c index 79a1ab620..07a622979 100644 --- a/cmd-list-sessions.c +++ b/cmd-list-sessions.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-sessions.c,v 1.36 2026/02/27 08:25:12 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-windows.c b/cmd-list-windows.c index 927976398..40f8c2db5 100644 --- a/cmd-list-windows.c +++ b/cmd-list-windows.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-windows.c,v 1.50 2026/02/27 08:25:12 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list.c b/cmd-list.c deleted file mode 100644 index 82ffe55c0..000000000 --- a/cmd-list.c +++ /dev/null @@ -1,103 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2009 Nicholas Marriott - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include -#include - -#include "tmux.h" - -static u_int cmd_list_next_group = 1; - -struct cmd_list * -cmd_list_new(void) -{ - struct cmd_list *cmdlist; - - cmdlist = xcalloc(1, sizeof *cmdlist); - cmdlist->references = 1; - cmdlist->group = cmd_list_next_group++; - TAILQ_INIT(&cmdlist->list); - return (cmdlist); -} - -void -cmd_list_append(struct cmd_list *cmdlist, struct cmd *cmd) -{ - cmd->group = cmdlist->group; - TAILQ_INSERT_TAIL(&cmdlist->list, cmd, qentry); -} - -void -cmd_list_move(struct cmd_list *cmdlist, struct cmd_list *from) -{ - struct cmd *cmd, *cmd1; - - TAILQ_FOREACH_SAFE(cmd, &from->list, qentry, cmd1) { - TAILQ_REMOVE(&from->list, cmd, qentry); - TAILQ_INSERT_TAIL(&cmdlist->list, cmd, qentry); - } - cmdlist->group = cmd_list_next_group++; -} - -void -cmd_list_free(struct cmd_list *cmdlist) -{ - struct cmd *cmd, *cmd1; - - if (--cmdlist->references != 0) - return; - - TAILQ_FOREACH_SAFE(cmd, &cmdlist->list, qentry, cmd1) { - TAILQ_REMOVE(&cmdlist->list, cmd, qentry); - cmd_free(cmd); - } - - free(cmdlist); -} - -char * -cmd_list_print(struct cmd_list *cmdlist, int escaped) -{ - struct cmd *cmd; - char *buf, *this; - size_t len; - - len = 1; - buf = xcalloc(1, len); - - TAILQ_FOREACH(cmd, &cmdlist->list, qentry) { - this = cmd_print(cmd); - - len += strlen(this) + 4; - buf = xrealloc(buf, len); - - strlcat(buf, this, len); - if (TAILQ_NEXT(cmd, qentry) != NULL) { - if (escaped) - strlcat(buf, " \\; ", len); - else - strlcat(buf, " ; ", len); - } - - free(this); - } - - return (buf); -} diff --git a/cmd-load-buffer.c b/cmd-load-buffer.c index 82ac2fd47..df35b82c1 100644 --- a/cmd-load-buffer.c +++ b/cmd-load-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-load-buffer.c,v 1.66 2025/10/28 07:32:26 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-lock-server.c b/cmd-lock-server.c index bd61dcffd..1feac03f8 100644 --- a/cmd-lock-server.c +++ b/cmd-lock-server.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-lock-server.c,v 1.31 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-move-window.c b/cmd-move-window.c index 4b90e70f8..1cd4f10a8 100644 --- a/cmd-move-window.c +++ b/cmd-move-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-move-window.c,v 1.34 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-new-session.c b/cmd-new-session.c index ba02c72d4..56d4f9779 100644 --- a/cmd-new-session.c +++ b/cmd-new-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-new-session.c,v 1.152 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -325,7 +325,7 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) session_group_synchronize_to(s); session_select(s, RB_MIN(winlinks, &s->windows)->idx); } - notify_session("session-created", s); + events_fire_session("session-created", s); /* * Set the client to the new session. If a command client exists, it is diff --git a/cmd-new-window.c b/cmd-new-window.c index a54aaaa20..d1e2ab4e5 100644 --- a/cmd-new-window.c +++ b/cmd-new-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-new-window.c,v 1.103 2026/07/08 08:07:42 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-parse.y b/cmd-parse.y index 0b32f5d34..29b7780e0 100644 --- a/cmd-parse.y +++ b/cmd-parse.y @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-parse.y,v 1.58 2026/04/27 12:31:11 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/cmd-paste-buffer.c b/cmd-paste-buffer.c index 03fcb0d40..fbe598b04 100644 --- a/cmd-paste-buffer.c +++ b/cmd-paste-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-paste-buffer.c,v 1.44 2026/03/04 07:19:32 tb Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-pipe-pane.c b/cmd-pipe-pane.c index 13fe679e3..f18added6 100644 --- a/cmd-pipe-pane.c +++ b/cmd-pipe-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-pipe-pane.c,v 1.63 2026/04/28 08:47:55 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-queue.c b/cmd-queue.c index a6dfc5925..463d82b3f 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-queue.c,v 1.120 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -351,74 +351,53 @@ cmdq_insert_after(struct cmdq_item *after, struct cmdq_item *item) /* Insert a hook. */ void -cmdq_insert_hook(struct session *s, struct cmdq_item *item, +cmdq_insert_hook(__unused struct session *s, struct cmdq_item *item, struct cmd_find_state *current, const char *fmt, ...) { - struct cmdq_state *state = item->state; struct cmd *cmd = item->cmd; struct args *args = cmd_get_args(cmd); struct args_entry *ae; struct args_value *av; - struct options *oo; + struct event_payload *ep; va_list ap; char *name, tmp[32], flag, *arguments; u_int i; const char *value; - struct cmdq_item *new_item; - struct cmdq_state *new_state; - struct options_entry *o; - struct options_array_item *a; - struct cmd_list *cmdlist; if (item->state->flags & CMDQ_STATE_NOHOOKS) return; - if (s == NULL) - oo = global_s_options; - else - oo = s->options; va_start(ap, fmt); xvasprintf(&name, fmt, ap); va_end(ap); - o = options_get(oo, name); - if (o == NULL) { - free(name); - return; - } - log_debug("running hook %s (parent %p)", name, item); - - /* - * The hooks get a new state because they should not update the current - * target or formats for any subsequent commands. - */ - new_state = cmdq_new_state(current, &state->event, CMDQ_STATE_NOHOOKS); - cmdq_add_format(new_state, "hook", "%s", name); + ep = event_payload_create(); + if (current != NULL) + event_payload_set_target(ep, current); + event_payload_set_pointer(ep, "_cmdq_item", item, NULL, NULL); arguments = args_print(args); - cmdq_add_format(new_state, "hook_arguments", "%s", arguments); + event_payload_set_string(ep, "arguments", "%s", arguments); free(arguments); for (i = 0; i < args_count(args); i++) { - xsnprintf(tmp, sizeof tmp, "hook_argument_%d", i); - cmdq_add_format(new_state, tmp, "%s", args_string(args, i)); + xsnprintf(tmp, sizeof tmp, "argument_%u", i); + event_payload_set_string(ep, tmp, "%s", args_string(args, i)); } flag = args_first(args, &ae); while (flag != 0) { value = args_get(args, flag); - if (value == NULL) { - xsnprintf(tmp, sizeof tmp, "hook_flag_%c", flag); - cmdq_add_format(new_state, tmp, "1"); - } else { - xsnprintf(tmp, sizeof tmp, "hook_flag_%c", flag); - cmdq_add_format(new_state, tmp, "%s", value); - } + xsnprintf(tmp, sizeof tmp, "flag_%c", flag); + if (value == NULL) + event_payload_set_string(ep, tmp, "1"); + else + event_payload_set_string(ep, tmp, "%s", value); i = 0; av = args_first_value(args, flag); while (av != NULL) { - xsnprintf(tmp, sizeof tmp, "hook_flag_%c_%d", flag, i); - cmdq_add_format(new_state, tmp, "%s", av->string); + xsnprintf(tmp, sizeof tmp, "flag_%c_%u", flag, i); + event_payload_set_string(ep, tmp, "%s", av->string); i++; av = args_next_value(av); } @@ -426,20 +405,7 @@ cmdq_insert_hook(struct session *s, struct cmdq_item *item, flag = args_next(&ae); } - a = options_array_first(o); - while (a != NULL) { - cmdlist = options_array_item_value(a)->cmdlist; - if (cmdlist != NULL) { - new_item = cmdq_get_command(cmdlist, new_state); - if (item != NULL) - item = cmdq_insert_after(item, new_item); - else - item = cmdq_append(NULL, new_item); - } - a = options_array_next(a); - } - - cmdq_free_state(new_state); + events_fire(name, ep); free(name); } diff --git a/cmd-refresh-client.c b/cmd-refresh-client.c index 6f22da83e..da2a27976 100644 --- a/cmd-refresh-client.c +++ b/cmd-refresh-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-refresh-client.c,v 1.54 2026/07/05 08:24:00 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-rename-session.c b/cmd-rename-session.c index b399483b8..35719c564 100644 --- a/cmd-rename-session.c +++ b/cmd-rename-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-rename-session.c,v 1.39 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -49,6 +49,8 @@ cmd_rename_session_exec(struct cmd *self, struct cmdq_item *item) struct args *args = cmd_get_args(self); struct cmd_find_state *target = cmdq_get_target(item); struct session *s = target->s; + struct event_payload *ep; + struct cmd_find_state fs; char *newname, *tmp; tmp = format_single_from_target(item, args_string(args, 0)); @@ -69,13 +71,20 @@ cmd_rename_session_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_ERROR); } + ep = event_payload_create(); + cmd_find_from_session(&fs, s, 0); + event_payload_set_target(ep, &fs); + event_payload_set_session(ep, "session", s); + event_payload_set_string(ep, "old_name", "%s", s->name); + event_payload_set_string(ep, "new_name", "%s", newname); + RB_REMOVE(sessions, &sessions, s); free(s->name); s->name = newname; RB_INSERT(sessions, &sessions, s); server_status_session(s); - notify_session("session-renamed", s); + events_fire("session-renamed", ep); return (CMD_RETURN_NORMAL); } diff --git a/cmd-rename-window.c b/cmd-rename-window.c index 8ab9b658a..f55f8a18f 100644 --- a/cmd-rename-window.c +++ b/cmd-rename-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-rename-window.c,v 1.30 2026/06/29 18:17:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index 3ce805115..01ddf424a 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-resize-pane.c,v 1.67 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -182,7 +182,7 @@ cmd_resize_pane_exec(struct cmd *self, struct cmdq_item *item) if (lc->parent != NULL) layout_fix_offsets(w); layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); diff --git a/cmd-resize-window.c b/cmd-resize-window.c index c420451cf..a89750607 100644 --- a/cmd-resize-window.c +++ b/cmd-resize-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-resize-window.c,v 1.10 2023/06/30 13:19:32 nicm Exp $ */ /* * Copyright (c) 2018 Nicholas Marriott diff --git a/cmd-respawn-pane.c b/cmd-respawn-pane.c index bf058a267..66a5a0be4 100644 --- a/cmd-respawn-pane.c +++ b/cmd-respawn-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-respawn-pane.c,v 1.40 2026/07/03 16:09:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-respawn-window.c b/cmd-respawn-window.c index e67757e1b..e10469649 100644 --- a/cmd-respawn-window.c +++ b/cmd-respawn-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-respawn-window.c,v 1.51 2026/07/03 16:09:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-rotate-window.c b/cmd-rotate-window.c index c7459a9ae..d62fd0fa4 100644 --- a/cmd-rotate-window.c +++ b/cmd-rotate-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-rotate-window.c,v 1.34 2026/06/22 08:47:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-run-shell.c b/cmd-run-shell.c index 6114f68a1..b440828fb 100644 --- a/cmd-run-shell.c +++ b/cmd-run-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-run-shell.c,v 1.91 2026/06/01 08:27:37 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-save-buffer.c b/cmd-save-buffer.c index d4266231f..903016c5d 100644 --- a/cmd-save-buffer.c +++ b/cmd-save-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-save-buffer.c,v 1.57 2025/10/28 07:32:26 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-select-layout.c b/cmd-select-layout.c index 417797472..741dc63f4 100644 --- a/cmd-select-layout.c +++ b/cmd-select-layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-select-layout.c,v 1.43 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -139,7 +139,7 @@ changed: free(oldlayout); recalculate_sizes(); server_redraw_window(w); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); return (CMD_RETURN_NORMAL); error: diff --git a/cmd-select-pane.c b/cmd-select-pane.c index d146ea6cf..91a36cddb 100644 --- a/cmd-select-pane.c +++ b/cmd-select-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-select-pane.c,v 1.76 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -80,6 +80,69 @@ cmd_select_pane_redraw(struct window *w) } } +static enum cmd_retval +cmd_select_pane_marked_pane(struct cmd *self, struct cmdq_item *item) +{ + struct args *args = cmd_get_args(self); + struct cmd_find_state *target = cmdq_get_target(item); + struct event_payload *ep; + struct cmd_find_state fs; + struct winlink *wl = target->wl; + struct window_pane *wp = target->wp, *lwp = NULL, *mwp; + struct session *s = target->s; + + if (args_has(args, 'm') && !window_pane_is_visible(wp)) + return (CMD_RETURN_NORMAL); + if (server_check_marked()) + lwp = marked_pane.wp; + + if (args_has(args, 'M') || server_is_marked(s, wl, wp)) + server_clear_marked(); + else + server_set_marked(s, wl, wp); + mwp = marked_pane.wp; + + ep = event_payload_create(); + if (mwp != NULL) + cmd_find_from_pane(&fs, mwp, 0); + else if (lwp != NULL) + cmd_find_from_pane(&fs, lwp, 0); + else + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + if (mwp != NULL) { + event_payload_set_pane(ep, "pane", mwp); + event_payload_set_pane(ep, "new_pane", mwp); + event_payload_set_window(ep, "window", mwp->window); + } else if (lwp != NULL) { + event_payload_set_pane(ep, "pane", lwp); + event_payload_set_window(ep, "window", lwp->window); + } else { + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + } + if (lwp != NULL) + event_payload_set_pane(ep, "old_pane", lwp); + event_payload_set_int(ep, "marked", mwp != NULL); + events_fire("marked-pane-changed", ep); + + if (lwp != NULL) { + lwp->flags |= (PANE_REDRAW|PANE_STYLECHANGED|PANE_THEMECHANGED); + server_redraw_window_borders(lwp->window); + server_status_window(lwp->window); + } + if (mwp != NULL) { + mwp->flags |= (PANE_REDRAW|PANE_STYLECHANGED|PANE_THEMECHANGED); + server_redraw_window_borders(mwp->window); + server_status_window(mwp->window); + } + if (window_pane_is_floating(wp)) { + window_redraw_active_switch(wp->window, wp); + window_set_active_pane(wp->window, wp, 1); + } + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) { @@ -88,10 +151,12 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) 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, *markedwp; + struct window_pane *wp = target->wp, *activewp, *lastwp; struct options *oo = wp->options; char *title; const char *style; @@ -134,38 +199,8 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); } - if (args_has(args, 'm') || args_has(args, 'M')) { - if (args_has(args, 'm') && !window_pane_is_visible(wp)) - return (CMD_RETURN_NORMAL); - if (server_check_marked()) - lastwp = marked_pane.wp; - else - lastwp = NULL; - - if (args_has(args, 'M') || server_is_marked(s, wl, wp)) - server_clear_marked(); - else - server_set_marked(s, wl, wp); - markedwp = marked_pane.wp; - - if (lastwp != NULL) { - lastwp->flags |= (PANE_REDRAW|PANE_STYLECHANGED| - PANE_THEMECHANGED); - server_redraw_window_borders(lastwp->window); - server_status_window(lastwp->window); - } - if (markedwp != NULL) { - markedwp->flags |= (PANE_REDRAW|PANE_STYLECHANGED| - PANE_THEMECHANGED); - server_redraw_window_borders(markedwp->window); - server_status_window(markedwp->window); - } - if (window_pane_is_floating(wp)) { - window_redraw_active_switch(w, wp); - window_set_active_pane(w, wp, 1); - } - return (CMD_RETURN_NORMAL); - } + if (args_has(args, 'm') || args_has(args, 'M')) + return (cmd_select_pane_marked_pane(self, item)); style = args_get(args, 'P'); if (style != NULL) { @@ -218,7 +253,13 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'T')) { title = format_single_from_target(item, args_get(args, 'T')); if (screen_set_title(&wp->base, title, 0)) { - notify_pane("pane-title-changed", wp); + 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); server_redraw_window_borders(wp->window); server_status_window(wp->window); } diff --git a/cmd-select-window.c b/cmd-select-window.c index 4aca3e60e..32b4821bf 100644 --- a/cmd-select-window.c +++ b/cmd-select-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-select-window.c,v 1.30 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-send-keys.c b/cmd-send-keys.c index 6be3f2ead..06043bddb 100644 --- a/cmd-send-keys.c +++ b/cmd-send-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-send-keys.c,v 1.81 2026/06/11 19:13:34 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-send-prefix.c b/cmd-send-prefix.c deleted file mode 100644 index 04556507b..000000000 --- a/cmd-send-prefix.c +++ /dev/null @@ -1,57 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2007 Nicholas Marriott - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include "tmux.h" - -/* - * Send prefix key as a key. - */ - -enum cmd_retval cmd_send_prefix_exec(struct cmd *, struct cmd_ctx *); - -const struct cmd_entry cmd_send_prefix_entry = { - "send-prefix", NULL, - "2t:", 0, 0, - "[-2] " CMD_TARGET_PANE_USAGE, - 0, - NULL, - NULL, - cmd_send_prefix_exec -}; - -enum cmd_retval -cmd_send_prefix_exec(struct cmd *self, struct cmd_ctx *ctx) -{ - struct args *args = self->args; - struct session *s; - struct window_pane *wp; - int key; - - if (cmd_find_pane(ctx, args_get(args, 't'), &s, &wp) == NULL) - return (CMD_RETURN_ERROR); - - if (args_has(args, '2')) - key = options_get_number(&s->options, "prefix2"); - else - key = options_get_number(&s->options, "prefix"); - window_pane_key(wp, s, key); - - return (CMD_RETURN_NORMAL); -} diff --git a/cmd-server-access.c b/cmd-server-access.c index 895e69417..e85a278a8 100644 --- a/cmd-server-access.c +++ b/cmd-server-access.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-server-access.c,v 1.6 2026/06/09 12:58:40 nicm Exp $ */ /* * Copyright (c) 2021 Dallas Lyons diff --git a/cmd-set-buffer.c b/cmd-set-buffer.c index eef2978e2..6e640fec6 100644 --- a/cmd-set-buffer.c +++ b/cmd-set-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-set-buffer.c,v 1.37 2026/02/15 17:43:26 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-set-environment.c b/cmd-set-environment.c index 78c32452b..9a6552cb5 100644 --- a/cmd-set-environment.c +++ b/cmd-set-environment.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-set-environment.c,v 1.29 2025/04/09 06:27:43 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-set-option.c b/cmd-set-option.c index 1b6de1e86..158cd5fac 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-set-option.c,v 1.145 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -31,6 +31,8 @@ static enum args_parse_type cmd_set_option_args_parse(struct args *, u_int, char **); static enum cmd_retval cmd_set_option_exec(struct cmd *, struct cmdq_item *); +static enum cmd_retval cmd_set_hook_event_exec(struct cmd *, + struct cmdq_item *); static enum cmd_retval cmd_set_hook_monitor_exec(struct cmdq_item *, struct args *, int); @@ -64,8 +66,8 @@ const struct cmd_entry cmd_set_hook_entry = { .name = "set-hook", .alias = NULL, - .args = { "agpRt:uB:w", 0, 2, cmd_set_option_args_parse }, - .usage = "[-agpRuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " + .args = { "agpERt:uB:w", 0, 2, cmd_set_option_args_parse }, + .usage = "[-agpERuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " "[hook] [command]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -85,12 +87,58 @@ cmd_set_option_args_parse(struct args *args, u_int idx, return (ARGS_PARSE_STRING); } +static enum cmd_retval +cmd_set_hook_event_exec(struct cmd *self, struct cmdq_item *item) +{ + struct args *args = cmd_get_args(self); + struct cmd_find_state *target = cmdq_get_target(item); + struct event_payload *ep; + struct client *c; + char *argument; + + if (args_count(args) == 0) { + cmdq_error(item, "missing argument"); + return (CMD_RETURN_ERROR); + } + if (args_count(args) != 1) { + cmdq_error(item, "too many arguments"); + return (CMD_RETURN_ERROR); + } + + argument = format_single_from_target(item, args_string(args, 0)); + if (*argument != '@') { + cmdq_error(item, "event name must start with @"); + free(argument); + return (CMD_RETURN_ERROR); + } + + ep = event_payload_create(); + event_payload_set_target(ep, target); + c = cmdq_get_client(item); + if (c != NULL) + event_payload_set_client(ep, "client", c); + if (target->s != NULL) + event_payload_set_session(ep, "session", target->s); + if (target->w != NULL) + event_payload_set_window(ep, "window", target->w); + if (target->wl != NULL) + event_payload_set_int(ep, "window_index", target->wl->idx); + else if (target->idx != -1) + event_payload_set_int(ep, "window_index", target->idx); + if (target->wp != NULL) + event_payload_set_pane(ep, "pane", target->wp); + events_fire(argument, ep); + free(argument); + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) { struct cmd_find_state *target = cmdq_get_target(item), fs; struct options *oo; struct options_entry *o; + struct session *s = NULL; char *cause = NULL, *name = NULL, *format = NULL; char *expanded = NULL, *newvalue = NULL; const char *value, *old; @@ -130,7 +178,7 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) cmd_find_copy_state(&fs, target); if (args_has(args, 'u')) { - notify_monitor_remove(oo, name); + hooks_monitor_remove(oo, name); goto out; } @@ -152,7 +200,11 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) } } - notify_monitor_add(item, oo, name, type, id, format, &fs, target->s); + if (oo != global_options && + oo != global_s_options && + oo != global_w_options) + s = target->s; + hooks_monitor_add(item, oo, name, type, id, format, &fs, s); out: free(newvalue); @@ -185,6 +237,8 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) int scope; window = (cmd_get_entry(self) == &cmd_set_window_option_entry); + if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'E')) + return (cmd_set_hook_event_exec(self, item)); if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'B')) return (cmd_set_hook_monitor_exec(item, args, window)); if (args_count(args) == 0) { @@ -197,7 +251,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) /* If set-hook -R, fire the hook straight away. */ if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'R')) { - notify_hook(item, argument); + hooks_run(item, argument); free(argument); return (CMD_RETURN_NORMAL); } @@ -289,6 +343,8 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) goto fail; } options_set_string(oo, name, append, "%s", value); + if (cmd_get_entry(self) == &cmd_set_hook_entry) + hooks_add_event(name); } else if (array_key == NULL && !options_is_array(parent)) { error = options_from_string(oo, options_table_entry(parent), options_table_entry(parent)->name, value, diff --git a/cmd-show-buffer.c b/cmd-show-buffer.c deleted file mode 100644 index 0af82972c..000000000 --- a/cmd-show-buffer.c +++ /dev/null @@ -1,112 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2007 Nicholas Marriott - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include -#include - -#include "tmux.h" - -/* - * Show a paste buffer. - */ - -enum cmd_retval cmd_show_buffer_exec(struct cmd *, struct cmd_ctx *); - -const struct cmd_entry cmd_show_buffer_entry = { - "show-buffer", "showb", - "b:", 0, 0, - CMD_BUFFER_USAGE, - 0, - NULL, - NULL, - cmd_show_buffer_exec -}; - -enum cmd_retval -cmd_show_buffer_exec(struct cmd *self, struct cmd_ctx *ctx) -{ - struct args *args = self->args; - struct session *s; - struct paste_buffer *pb; - int buffer; - char *in, *buf, *ptr, *cause; - size_t size, len; - u_int width; - - if ((s = cmd_find_session(ctx, NULL, 0)) == NULL) - return (CMD_RETURN_ERROR); - - if (!args_has(args, 'b')) { - if ((pb = paste_get_top(&global_buffers)) == NULL) { - ctx->error(ctx, "no buffers"); - return (CMD_RETURN_ERROR); - } - } else { - buffer = args_strtonum(args, 'b', 0, INT_MAX, &cause); - if (cause != NULL) { - ctx->error(ctx, "buffer %s", cause); - free(cause); - return (CMD_RETURN_ERROR); - } - - pb = paste_get_index(&global_buffers, buffer); - if (pb == NULL) { - ctx->error(ctx, "no buffer %d", buffer); - return (CMD_RETURN_ERROR); - } - } - - size = pb->size; - if (size > SIZE_MAX / 4 - 1) - size = SIZE_MAX / 4 - 1; - in = xmalloc(size * 4 + 1); - strvisx(in, pb->data, size, VIS_OCTAL|VIS_TAB); - - width = s->sx; - if (ctx->cmdclient != NULL) - width = ctx->cmdclient->tty.sx; - - buf = xmalloc(width + 1); - len = 0; - - ptr = in; - do { - buf[len++] = *ptr++; - - if (len == width || buf[len - 1] == '\n') { - if (buf[len - 1] == '\n') - len--; - buf[len] = '\0'; - - ctx->print(ctx, "%s", buf); - len = 0; - } - } while (*ptr != '\0'); - - if (len != 0) { - buf[len] = '\0'; - ctx->print(ctx, "%s", buf); - } - free(buf); - - free(in); - - return (CMD_RETURN_NORMAL); -} diff --git a/cmd-show-environment.c b/cmd-show-environment.c index 3440c33e4..692f09c32 100644 --- a/cmd-show-environment.c +++ b/cmd-show-environment.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-show-environment.c,v 1.29 2025/04/09 06:27:43 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-show-messages.c b/cmd-show-messages.c index 7c656fa99..f9e198c4c 100644 --- a/cmd-show-messages.c +++ b/cmd-show-messages.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-show-messages.c,v 1.37 2025/11/18 08:37:54 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-show-options.c b/cmd-show-options.c index bff0634fe..308912e00 100644 --- a/cmd-show-options.c +++ b/cmd-show-options.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-show-options.c,v 1.73 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -216,7 +216,7 @@ cmd_show_hooks_print_monitor(struct cmdq_item *item, struct options_entry *o) { char *value; - value = notify_monitor_to_string(o); + value = hooks_monitor_to_string(o); if (value == NULL) return; cmdq_print(item, "%s", value); diff --git a/cmd-show-prompt-history.c b/cmd-show-prompt-history.c index 741d22588..5b6af76fb 100644 --- a/cmd-show-prompt-history.c +++ b/cmd-show-prompt-history.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-show-prompt-history.c,v 1.5 2026/06/25 11:39:11 nicm Exp $ */ /* * Copyright (c) 2021 Anindya Mukherjee diff --git a/cmd-source-file.c b/cmd-source-file.c index cbd53960b..e5dfba543 100644 --- a/cmd-source-file.c +++ b/cmd-source-file.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-source-file.c,v 1.62 2025/11/18 08:42:09 nicm Exp $ */ /* * Copyright (c) 2008 Tiago Cunha diff --git a/cmd-split-window.c b/cmd-split-window.c index 09cfbfa86..64fa6b877 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-split-window.c,v 1.141 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -84,6 +84,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) struct window *w = wl->window; struct window_pane *wp = target->wp, *new_wp = NULL; struct layout_cell *lc = NULL; + struct event_payload *ep; struct cmd_find_state fs; int input, empty, is_floating, flags = 0; const char *template, *style, *value; @@ -218,7 +219,13 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'T')) { title = format_single_from_target(item, args_get(args, 'T')); screen_set_title(&new_wp->base, title, 0); - notify_pane("pane-title-changed", new_wp); + ep = event_payload_create(); + cmd_find_from_pane(&fs, new_wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", new_wp); + event_payload_set_window(ep, "window", new_wp->window); + event_payload_set_string(ep, "new_title", "%s", title); + events_fire("pane-title-changed", ep); free(title); } diff --git a/cmd-swap-pane.c b/cmd-swap-pane.c index 7f5160e65..da7ea3706 100644 --- a/cmd-swap-pane.c +++ b/cmd-swap-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-swap-pane.c,v 1.53 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -180,9 +180,9 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) redraw_invalidate_scene(dst_w); server_redraw_window(dst_w); - notify_window("window-layout-changed", src_w); + events_fire_window("window-layout-changed", src_w); if (src_w != dst_w) - notify_window("window-layout-changed", dst_w); + events_fire_window("window-layout-changed", dst_w); out: if (window_pop_zoom(src_w)) diff --git a/cmd-swap-window.c b/cmd-swap-window.c index d31353982..980da0395 100644 --- a/cmd-swap-window.c +++ b/cmd-swap-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-swap-window.c,v 1.29 2025/10/30 13:52:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-switch-client.c b/cmd-switch-client.c index 9865ac52c..177107b47 100644 --- a/cmd-switch-client.c +++ b/cmd-switch-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-switch-client.c,v 1.74 2026/05/22 15:22:43 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-unbind-key.c b/cmd-unbind-key.c index 6d91d7cca..f537fb576 100644 --- a/cmd-unbind-key.c +++ b/cmd-unbind-key.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-unbind-key.c,v 1.34 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-wait-for.c b/cmd-wait-for.c index 8a6aa259e..0e4130fcd 100644 --- a/cmd-wait-for.c +++ b/cmd-wait-for.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-wait-for.c,v 1.23 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -34,8 +34,8 @@ const struct cmd_entry cmd_wait_for_entry = { .name = "wait-for", .alias = "wait", - .args = { "LSU", 1, 1, NULL }, - .usage = "[-L|-S|-U] channel", + .args = { "EF:LSUlvw:", 1, 1, NULL }, + .usage = "[-ELSUlv] [-F format] [-w waiter] name", .flags = 0, .exec = cmd_wait_for_exec @@ -46,6 +46,17 @@ struct wait_item { TAILQ_ENTRY(wait_item) entry; }; +struct wait_event_item { + struct cmdq_item *item; + struct events_sink *sink; + char *name; + char *filter; + int verbose; + TAILQ_ENTRY(wait_event_item) entry; +}; +static TAILQ_HEAD(, wait_event_item) wait_event_items = + TAILQ_HEAD_INITIALIZER(wait_event_items); + struct wait_channel { const char *name; int locked; @@ -76,9 +87,28 @@ static enum cmd_retval cmd_wait_for_lock(struct cmdq_item *, const char *, struct wait_channel *); static enum cmd_retval cmd_wait_for_unlock(struct cmdq_item *, const char *, struct wait_channel *); +static enum cmd_retval cmd_wait_for_event(struct cmdq_item *, const char *, + struct args *); +static void cmd_wait_for_event_cb(const char *, + struct event_payload *, void *); +static void cmd_wait_for_event_free(struct wait_event_item *); +static enum cmd_retval cmd_wait_for_event_list(struct cmdq_item *, + const char *); +static enum cmd_retval cmd_wait_for_event_wake(struct cmdq_item *, + const char *, struct args *); +static enum cmd_retval cmd_wait_for_list(struct cmdq_item *, + struct wait_channel *); +static enum cmd_retval cmd_wait_for_wake(struct cmdq_item *, const char *, + struct args *, struct wait_channel *); static struct wait_channel *cmd_wait_for_add(const char *); static void cmd_wait_for_remove(struct wait_channel *); +static void cmd_wait_for_remove_empty( + struct wait_channel *); +static const char *cmd_wait_for_item_client_name( + struct cmdq_item *); +static const char *cmd_wait_for_client_name( + struct wait_event_item *); static struct wait_channel * cmd_wait_for_add(const char *name) @@ -117,16 +147,53 @@ cmd_wait_for_remove(struct wait_channel *wc) free(wc); } +static void +cmd_wait_for_remove_empty(struct wait_channel *wc) +{ + if (wc->locked || wc->woken) + return; + if (!TAILQ_EMPTY(&wc->waiters) || !TAILQ_EMPTY(&wc->lockers)) + return; + + log_debug("remove empty wait channel %s", wc->name); + + RB_REMOVE(wait_channels, &wait_channels, wc); + + free((void *)wc->name); + free(wc); +} + +static const char * +cmd_wait_for_item_client_name(struct cmdq_item *item) +{ + struct client *c = cmdq_get_client(item); + + if (c == NULL || c->name == NULL) + return (""); + return (c->name); +} + +static const char * +cmd_wait_for_client_name(struct wait_event_item *wei) +{ + return (cmd_wait_for_item_client_name(wei->item)); +} + static enum cmd_retval cmd_wait_for_exec(struct cmd *self, struct cmdq_item *item) { - struct args *args = cmd_get_args(self); + struct args *args = cmd_get_args(self); const char *name = args_string(args, 0); - struct wait_channel *wc, find; + struct wait_channel *wc, find = { .name = name }; + + if (args_has(args, 'E')) + return (cmd_wait_for_event(item, name, args)); - find.name = name; wc = RB_FIND(wait_channels, &wait_channels, &find); - + if (args_has(args, 'l')) + return (cmd_wait_for_list(item, wc)); + if (args_has(args, 'w')) + return (cmd_wait_for_wake(item, name, args, wc)); if (args_has(args, 'S')) return (cmd_wait_for_signal(item, name, wc)); if (args_has(args, 'L')) @@ -136,6 +203,179 @@ cmd_wait_for_exec(struct cmd *self, struct cmdq_item *item) return (cmd_wait_for_wait(item, name, wc)); } +static void +cmd_wait_for_event_print(struct wait_event_item *wei, struct event_payload *ep) +{ + struct event_payload_item *epi; + const char *key; + char *value; + + epi = event_payload_first(ep); + while (epi != NULL) { + key = event_payload_item_name(epi); + if (*key != '_') { + value = event_payload_item_print(epi); + cmdq_print(wei->item, "%s=%s", key, value); + free(value); + } + epi = event_payload_next(epi); + } +} + +static void +cmd_wait_for_event_cb(__unused const char *name, struct event_payload *ep, + void *item_data) +{ + struct wait_event_item *wei = item_data; + struct format_tree *ft; + char *expanded; + int flag; + + if (wei->verbose) + cmd_wait_for_event_print(wei, ep); + + if (wei->filter != NULL) { + ft = format_create(cmdq_get_client(wei->item), wei->item, + FORMAT_NONE, FORMAT_NOJOBS); + event_payload_add_formats(ep, ft, NULL); + expanded = format_expand(ft, wei->filter); + flag = format_true(expanded); + free(expanded); + format_free(ft); + + if (!flag) + return; + } + + TAILQ_REMOVE(&wait_event_items, wei, entry); + cmdq_continue(wei->item); + cmd_wait_for_event_free(wei); +} + +static void +cmd_wait_for_event_free(struct wait_event_item *wei) +{ + events_remove_sink(wei->sink); + free(wei->name); + free(wei->filter); + free(wei); +} + +static enum cmd_retval +cmd_wait_for_event(struct cmdq_item *item, const char *name, struct args *args) +{ + struct wait_event_item *wei; + const char *filter = args_get(args, 'F'); + + if (!hooks_valid_event_name(name)) { + cmdq_error(item, "invalid event: %s", name); + return (CMD_RETURN_ERROR); + } + if (args_has(args, 'l')) + return (cmd_wait_for_event_list(item, name)); + if (args_has(args, 'w')) + return (cmd_wait_for_event_wake(item, name, args)); + + if (cmdq_get_client(item) == NULL) { + cmdq_error(item, "not able to wait"); + return (CMD_RETURN_ERROR); + } + + wei = xcalloc(1, sizeof *wei); + wei->item = item; + wei->name = xstrdup(name); + wei->filter = (filter != NULL ? xstrdup(filter) : NULL); + wei->verbose = args_has(args, 'v'); + wei->sink = events_add_sink(name, cmd_wait_for_event_cb, wei); + TAILQ_INSERT_TAIL(&wait_event_items, wei, entry); + + return (CMD_RETURN_WAIT); +} + +static enum cmd_retval +cmd_wait_for_event_list(struct cmdq_item *item, const char *name) +{ + struct wait_event_item *wei; + + TAILQ_FOREACH(wei, &wait_event_items, entry) { + if (strcmp(wei->name, name) == 0) + cmdq_print(item, "%s", cmd_wait_for_client_name(wei)); + } + + return (CMD_RETURN_NORMAL); +} + +static enum cmd_retval +cmd_wait_for_event_wake(struct cmdq_item *item, const char *name, + struct args *args) +{ + struct wait_event_item *wei, *wei1; + const char *client_name = args_get(args, 'w'); + + TAILQ_FOREACH_SAFE(wei, &wait_event_items, entry, wei1) { + if (strcmp(wei->name, name) != 0) + continue; + if (strcmp(cmd_wait_for_client_name(wei), client_name) != 0) + continue; + + TAILQ_REMOVE(&wait_event_items, wei, entry); + cmdq_continue(wei->item); + cmd_wait_for_event_free(wei); + return (CMD_RETURN_NORMAL); + } + + cmdq_error(item, "waiter %s not found", client_name); + return (CMD_RETURN_ERROR); +} + +static enum cmd_retval +cmd_wait_for_list(struct cmdq_item *item, struct wait_channel *wc) +{ + struct wait_item *wi; + + if (wc == NULL) + return (CMD_RETURN_NORMAL); + + TAILQ_FOREACH(wi, &wc->waiters, entry) + cmdq_print(item, "%s", cmd_wait_for_item_client_name(wi->item)); + TAILQ_FOREACH(wi, &wc->lockers, entry) + cmdq_print(item, "%s", cmd_wait_for_item_client_name(wi->item)); + + return (CMD_RETURN_NORMAL); +} + +static enum cmd_retval +cmd_wait_for_wake(__unused struct cmdq_item *item, const char *name, + struct args *args, struct wait_channel *wc) +{ + struct wait_item *wi, *wi1; + const char *client_name = args_get(args, 'w'); + + if (wc != NULL) { + TAILQ_FOREACH_SAFE(wi, &wc->waiters, entry, wi1) { + name = cmd_wait_for_item_client_name(wi->item); + if (strcmp(name, client_name) != 0) + continue; + cmdq_continue(wi->item); + TAILQ_REMOVE(&wc->waiters, wi, entry); + free(wi); + cmd_wait_for_remove_empty(wc); + return (CMD_RETURN_NORMAL); + } + TAILQ_FOREACH_SAFE(wi, &wc->lockers, entry, wi1) { + name = cmd_wait_for_item_client_name(wi->item); + if (strcmp(name, client_name) != 0) + continue; + cmdq_continue(wi->item); + TAILQ_REMOVE(&wc->lockers, wi, entry); + free(wi); + cmd_wait_for_remove_empty(wc); + return (CMD_RETURN_NORMAL); + } + } + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_wait_for_signal(__unused struct cmdq_item *item, const char *name, struct wait_channel *wc) @@ -245,6 +485,13 @@ cmd_wait_for_flush(void) { struct wait_channel *wc, *wc1; struct wait_item *wi, *wi1; + struct wait_event_item *wei, *wei1; + + TAILQ_FOREACH_SAFE(wei, &wait_event_items, entry, wei1) { + TAILQ_REMOVE(&wait_event_items, wei, entry); + cmdq_continue(wei->item); + cmd_wait_for_event_free(wei); + } RB_FOREACH_SAFE(wc, wait_channels, &wait_channels, wc1) { TAILQ_FOREACH_SAFE(wi, &wc->waiters, entry, wi1) { diff --git a/cmd.c b/cmd.c index a2d3fad70..b9b8f1a4a 100644 --- a/cmd.c +++ b/cmd.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd.c,v 1.185 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/colour.c b/colour.c index f563fd5aa..411d77921 100644 --- a/colour.c +++ b/colour.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: colour.c,v 1.35 2026/07/06 14:29:10 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/control-notify.c b/control-notify.c index e85660297..6fec1683f 100644 --- a/control-notify.c +++ b/control-notify.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: control-notify.c,v 1.37 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -23,32 +23,56 @@ #include "tmux.h" +/* Should this client be sent events? */ #define CONTROL_SHOULD_NOTIFY_CLIENT(c) \ - ((c) != NULL && ((c)->flags & CLIENT_CONTROL) && \ + ((c) != NULL && \ + ((c)->flags & CLIENT_CONTROL) && \ (~(c)->flags & CLIENT_EXIT) && \ (c)->control_state != NULL) -void -control_notify_pane_mode_changed(int pane) +/* Notify control clients that pane mode changed. */ +static void +control_pane_mode_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { - struct client *c; + struct window_pane *wp; + struct client *c; + char *value; - TAILQ_FOREACH(c, &clients, entry) { - if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) - continue; - - control_write(c, "%%pane-mode-changed %%%u", pane); + wp = event_payload_get_pane(ep, "pane"); + if (wp != NULL) { + TAILQ_FOREACH(c, &clients, entry) { + if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) + continue; + control_write(c, "%%pane-mode-changed %%%u", wp->id); + } + return; } + + value = event_payload_print(ep, "pane"); + if (value == NULL) + return; + TAILQ_FOREACH(c, &clients, entry) { + if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) + control_write(c, "%%pane-mode-changed %s", value); + } + free(value); } -void -control_notify_window_layout_changed(struct window *w) +/* Notify control clients that window layout changed. */ +static void +control_window_layout_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { - struct client *c; - struct session *s; - struct winlink *wl; - const char *template; - char *cp; + struct client *c; + struct session *s; + struct winlink *wl; + struct window *w = event_payload_get_window(ep, "window"); + const char *template; + char *cp; + + if (w == NULL) + return; template = "%layout-change #{window_id} #{window_layout} " "#{window_visible_layout} #{window_raw_flags}"; @@ -73,12 +97,15 @@ control_notify_window_layout_changed(struct window *w) free(cp); } -void -control_notify_window_pane_changed(struct window *w) +/* Notify control clients that window pane changed. */ +static void +control_window_pane_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { struct client *c; + struct window *w = event_payload_get_window(ep, "window"); - if (w->active == NULL) + if (w == NULL || w->active == NULL) return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) @@ -89,12 +116,17 @@ control_notify_window_pane_changed(struct window *w) } } -void -control_notify_window_unlinked(__unused struct session *s, struct window *w) +/* Notify control clients that a window was unlinked. */ +static void +control_window_unlinked_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; - struct session *cs; + struct client *c; + struct session *cs; + struct window *w = event_payload_get_window(ep, "window"); + if (w == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL) continue; @@ -107,12 +139,17 @@ control_notify_window_unlinked(__unused struct session *s, struct window *w) } } -void -control_notify_window_linked(__unused struct session *s, struct window *w) +/* Notify control clients that a window was linked. */ +static void +control_window_linked_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; - struct session *cs; + struct client *c; + struct session *cs; + struct window *w = event_payload_get_window(ep, "window"); + if (w == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL) continue; @@ -125,12 +162,17 @@ control_notify_window_linked(__unused struct session *s, struct window *w) } } -void -control_notify_window_renamed(struct window *w) +/* Notify control clients that a window was renamed. */ +static void +control_window_renamed_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; - struct session *cs; + struct client *c; + struct session *cs; + struct window *w = event_payload_get_window(ep, "window"); + if (w == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL) continue; @@ -146,13 +188,16 @@ control_notify_window_renamed(struct window *w) } } -void -control_notify_client_session_changed(struct client *cc) +/* Notify control clients that a client changed session. */ +static void +control_client_session_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { - struct client *c; - struct session *s; + struct client *cc = event_payload_get_client(ep, "client"); + struct client *c; + struct session *s; - if (cc->session == NULL) + if (cc == NULL || cc->session == NULL) return; s = cc->session; @@ -170,22 +215,32 @@ control_notify_client_session_changed(struct client *cc) } } -void -control_notify_client_detached(struct client *cc) +/* Notify control clients that a client detached. */ +static void +control_client_detached_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; + struct client *cc = event_payload_get_client(ep, "client"); + struct client *c; + if (cc == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) control_write(c, "%%client-detached %s", cc->name); } } -void -control_notify_session_renamed(struct session *s) +/* Notify control clients that a session was renamed. */ +static void +control_session_renamed_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; + struct session *s = event_payload_get_session(ep, "session"); + struct client *c; + if (s == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; @@ -194,8 +249,10 @@ control_notify_session_renamed(struct session *s) } } -void -control_notify_session_created(__unused struct session *s) +/* Notify control clients that sessions changed. */ +static void +control_session_created_cb(__unused const char *name, + __unused struct event_payload *ep, __unused void *sink_data) { struct client *c; @@ -207,8 +264,10 @@ control_notify_session_created(__unused struct session *s) } } -void -control_notify_session_closed(__unused struct session *s) +/* Notify control clients that sessions changed. */ +static void +control_session_closed_cb(__unused const char *name, + __unused struct event_payload *ep, __unused void *sink_data) { struct client *c; @@ -220,9 +279,12 @@ control_notify_session_closed(__unused struct session *s) } } -void -control_notify_session_window_changed(struct session *s) +/* Notify control clients that the current window changed. */ +static void +control_session_window_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { + struct session *s = event_payload_get_session(ep, "session"); struct client *c; /* @@ -230,7 +292,7 @@ control_notify_session_window_changed(struct session *s) * session has been destroyed (which sets curw to NULL) but is kept * alive by the notification's reference. Skip the notification. */ - if (s->curw == NULL) + if (s == NULL || s->curw == NULL) return; TAILQ_FOREACH(c, &clients, entry) { @@ -242,28 +304,68 @@ control_notify_session_window_changed(struct session *s) } } -void -control_notify_paste_buffer_changed(const char *name) +/* Notify control clients that a paste buffer changed. */ +static void +control_paste_buffer_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { + const char *pbname = event_payload_get_string(ep, "paste_buffer"); struct client *c; + if (pbname == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - control_write(c, "%%paste-buffer-changed %s", name); + control_write(c, "%%paste-buffer-changed %s", pbname); } } -void -control_notify_paste_buffer_deleted(const char *name) +/* Notify control clients that a paste buffer was deleted. */ +static void +control_paste_buffer_deleted_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { + const char *pbname = event_payload_get_string(ep, "paste_buffer"); struct client *c; + if (pbname == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - control_write(c, "%%paste-buffer-deleted %s", name); + control_write(c, "%%paste-buffer-deleted %s", pbname); } } + +/* Build control event sinks. */ +void +control_build_events(void) +{ + /* Control event sink callbacks. */ + static struct { + const char *name; + events_cb cb; + } events[] = { + { "pane-mode-changed", control_pane_mode_changed_cb }, + { "window-layout-changed", control_window_layout_changed_cb }, + { "window-pane-changed", control_window_pane_changed_cb }, + { "window-unlinked", control_window_unlinked_cb }, + { "window-linked", control_window_linked_cb }, + { "window-renamed", control_window_renamed_cb }, + { "client-session-changed", control_client_session_changed_cb }, + { "client-detached", control_client_detached_cb }, + { "session-renamed", control_session_renamed_cb }, + { "session-created", control_session_created_cb }, + { "session-closed", control_session_closed_cb }, + { "session-window-changed", control_session_window_changed_cb }, + { "paste-buffer-changed", control_paste_buffer_changed_cb }, + { "paste-buffer-deleted", control_paste_buffer_deleted_cb } + }; + u_int i; + + for (i = 0; i < nitems(events); i++) + events_add_sink(events[i].name, events[i].cb, NULL); +} diff --git a/control.c b/control.c index c1cc79e25..220267b6c 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: control.c,v 1.60 2026/07/10 07:25:05 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott diff --git a/environ.c b/environ.c index f9bc8cab3..e4f94fd4b 100644 --- a/environ.c +++ b/environ.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: environ.c,v 1.31 2026/06/13 20:07:30 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/events-payload.c b/events-payload.c new file mode 100644 index 000000000..444836cf3 --- /dev/null +++ b/events-payload.c @@ -0,0 +1,672 @@ +/* $OpenBSD: events-payload.c,v 1.1 2026/07/10 13:38:45 nicm Exp $ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include +#include +#include + +#include "tmux.h" + +/* Event payload item. */ +struct event_payload_item { + char *name; + enum event_payload_type type; + + union { + char *string; + time_t time; + int number; + u_int unsigned_number; + struct client *client; + struct session *session; + struct window *window; + struct window_pane *pane; + struct { + void *ptr; + event_payload_free_cb free_cb; + event_payload_print_cb print_cb; + } pointer; + }; + + RB_ENTRY(event_payload_item) entry; +}; + +RB_HEAD(event_payload_tree, event_payload_item); + +struct event_payload { + struct event_payload_tree items; + struct cmd_find_state target; +}; + +static int +event_payload_cmp(struct event_payload_item *epi1, + struct event_payload_item *epi2) +{ + return (strcmp(epi1->name, epi2->name)); +} +RB_GENERATE_STATIC(event_payload_tree, event_payload_item, entry, + event_payload_cmp); + +/* Find an item. */ +static struct event_payload_item * +event_payload_find(struct event_payload *ep, const char *name) +{ + struct event_payload_item find = { .name = (char *)name }; + + return (RB_FIND(event_payload_tree, &ep->items, &find)); +} + +/* Free the target in a payload. */ +static void +event_payload_free_target(struct event_payload *ep) +{ + struct cmd_find_state *target = &ep->target; + + if (target->s != NULL) + session_remove_ref(target->s, __func__); + if (target->w != NULL) + window_remove_ref(target->w, __func__); + if (target->wp != NULL) + window_pane_remove_ref(target->wp, __func__); + + cmd_find_clear_state(target, 0); +} + +/* Free the value in an item. */ +static void +event_payload_free_value(struct event_payload_item *epi) +{ + switch (epi->type) { + case EVENT_PAYLOAD_STRING: + free(epi->string); + break; + case EVENT_PAYLOAD_CLIENT: + server_client_unref(epi->client); + break; + case EVENT_PAYLOAD_SESSION: + session_remove_ref(epi->session, __func__); + break; + case EVENT_PAYLOAD_WINDOW: + window_remove_ref(epi->window, __func__); + break; + case EVENT_PAYLOAD_PANE: + window_pane_remove_ref(epi->pane, __func__); + break; + case EVENT_PAYLOAD_POINTER: + if (epi->pointer.free_cb != NULL) + epi->pointer.free_cb(epi->pointer.ptr); + break; + case EVENT_PAYLOAD_INT: + case EVENT_PAYLOAD_UINT: + case EVENT_PAYLOAD_TIME: + break; + } +} + +/* Set an item. */ +static void +event_payload_set_item(struct event_payload *ep, const char *name, + struct event_payload_item *new) +{ + struct event_payload_item *old; + + new->name = xstrdup(name); + old = RB_INSERT(event_payload_tree, &ep->items, new); + if (old != NULL) { + RB_REMOVE(event_payload_tree, &ep->items, old); + event_payload_free_value(old); + free(old->name); + free(old); + RB_INSERT(event_payload_tree, &ep->items, new); + } +} + +/* Create an event payload. */ +struct event_payload * +event_payload_create(void) +{ + struct event_payload *ep; + + ep = xcalloc(1, sizeof *ep); + RB_INIT(&ep->items); + cmd_find_clear_state(&ep->target, 0); + return (ep); +} + +/* Free an event payload. */ +void +event_payload_free(struct event_payload *ep) +{ + struct event_payload_item *epi, *epi1; + + if (ep != NULL) { + RB_FOREACH_SAFE(epi, event_payload_tree, &ep->items, epi1) { + RB_REMOVE(event_payload_tree, &ep->items, epi); + event_payload_free_value(epi); + free(epi->name); + free(epi); + } + event_payload_free_target(ep); + free(ep); + } +} + +/* Set the target. */ +void +event_payload_set_target(struct event_payload *ep, struct cmd_find_state *fs) +{ + struct cmd_find_state *target = &ep->target; + + event_payload_free_target(ep); + + if (fs->s != NULL) { + session_add_ref(fs->s, __func__); + target->s = fs->s; + } + if (fs->wl != NULL) { + target->idx = fs->wl->idx; + if (target->s == NULL) { + session_add_ref(fs->wl->session, __func__); + target->s = fs->wl->session; + } + } else + target->idx = -1; + if (fs->w != NULL) { + window_add_ref(fs->w, __func__); + target->w = fs->w; + } else if (fs->wl != NULL) { + window_add_ref(fs->wl->window, __func__); + target->w = fs->wl->window; + } + if (fs->wp != NULL) { + window_pane_add_ref(fs->wp, __func__); + target->wp = fs->wp; + } +} + +/* Get the target. */ +int +event_payload_get_target(struct event_payload *ep, struct cmd_find_state *fs) +{ + struct cmd_find_state *t = &ep->target; + struct winlink *wl = NULL; + int flags = fs->flags; + + if (t->idx != -1 && + t->s != NULL && + t->w != NULL && + session_alive(t->s)) { + wl = winlink_find_by_index(&t->s->windows, t->idx); + if (wl != NULL && wl->window != t->w) + wl = NULL; + } + + cmd_find_clear_state(fs, flags); + fs->s = t->s; + fs->w = t->w; + fs->wp = t->wp; + fs->wl = wl; + fs->idx = (wl != NULL ? wl->idx : -1); + if (cmd_find_valid_state(fs)) + return (1); + + if (wl != NULL && + t->wp != NULL && + window_has_pane(wl->window, t->wp)) { + cmd_find_from_winlink_pane(fs, wl, t->wp, flags); + if (cmd_find_valid_state(fs)) + return (1); + } + + if (t->wp != NULL && + cmd_find_from_pane(fs, t->wp, flags) == 0 && + cmd_find_valid_state(fs)) + return (1); + + if (wl != NULL) { + cmd_find_from_winlink(fs, wl, flags); + if (cmd_find_valid_state(fs)) + return (1); + } + + if (t->s != NULL && + t->w != NULL && + session_alive(t->s) && + cmd_find_from_session_window(fs, t->s, t->w, flags) == 0 && + cmd_find_valid_state(fs)) + return (1); + + if (t->s != NULL && session_alive(t->s)) { + cmd_find_from_session(fs, t->s, flags); + if (cmd_find_valid_state(fs)) + return (1); + } + + if (cmd_find_from_nothing(fs, flags) == 0) + return (1); + + cmd_find_clear_state(fs, flags); + return (0); +} + +/* Set a string item. */ +void +event_payload_set_string(struct event_payload *ep, const char *name, + const char *fmt, ...) +{ + struct event_payload_item *epi; + va_list ap; + + va_start(ap, fmt); + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_STRING; + xvasprintf(&epi->string, fmt, ap); + event_payload_set_item(ep, name, epi); + + va_end(ap); +} + +/* Set a time item. */ +void +event_payload_set_time(struct event_payload *ep, const char *name, + time_t value) +{ + struct event_payload_item *epi; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_TIME; + epi->time = value; + event_payload_set_item(ep, name, epi); +} + +/* Set a number item. */ +void +event_payload_set_int(struct event_payload *ep, const char *name, int value) +{ + struct event_payload_item *epi; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_INT; + epi->number = value; + event_payload_set_item(ep, name, epi); +} + +/* Set an unsigned number item. */ +void +event_payload_set_uint(struct event_payload *ep, const char *name, u_int value) +{ + struct event_payload_item *epi; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_UINT; + epi->unsigned_number = value; + event_payload_set_item(ep, name, epi); +} + +/* Set a client item. */ +void +event_payload_set_client(struct event_payload *ep, const char *name, + struct client *c) +{ + struct event_payload_item *epi; + + c->references++; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_CLIENT; + epi->client = c; + event_payload_set_item(ep, name, epi); +} + +/* Set a session item. */ +void +event_payload_set_session(struct event_payload *ep, const char *name, + struct session *s) +{ + struct event_payload_item *epi; + + session_add_ref(s, __func__); + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_SESSION; + epi->session = s; + event_payload_set_item(ep, name, epi); +} + +/* Set a window item. */ +void +event_payload_set_window(struct event_payload *ep, const char *name, + struct window *w) +{ + struct event_payload_item *epi; + + window_add_ref(w, __func__); + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_WINDOW; + epi->window = w; + event_payload_set_item(ep, name, epi); +} + +/* Set a pane item. */ +void +event_payload_set_pane(struct event_payload *ep, const char *name, + struct window_pane *wp) +{ + struct event_payload_item *epi; + + window_pane_add_ref(wp, __func__); + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_PANE; + epi->pane = wp; + event_payload_set_item(ep, name, epi); +} + +/* Set a pointer item. */ +void +event_payload_set_pointer(struct event_payload *ep, const char *name, + void *ptr, event_payload_free_cb free_cb, event_payload_print_cb print_cb) +{ + struct event_payload_item *epi; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_POINTER; + epi->pointer.ptr = ptr; + epi->pointer.free_cb = free_cb; + epi->pointer.print_cb = print_cb; + event_payload_set_item(ep, name, epi); +} + +/* Get a string item. */ +const char * +event_payload_get_string(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_STRING) + return (NULL); + return (epi->string); +} + +/* Print a payload item. */ +static void +event_payload_add_item(struct event_payload_item *epi, struct evbuffer *evb) +{ + switch (epi->type) { + case EVENT_PAYLOAD_STRING: + evbuffer_add_printf(evb, "%s", epi->string); + break; + case EVENT_PAYLOAD_TIME: + evbuffer_add_printf(evb, "%lld", (long long)epi->time); + break; + case EVENT_PAYLOAD_INT: + evbuffer_add_printf(evb, "%d", epi->number); + break; + case EVENT_PAYLOAD_UINT: + evbuffer_add_printf(evb, "%u", epi->unsigned_number); + break; + case EVENT_PAYLOAD_CLIENT: + evbuffer_add_printf(evb, "%s", epi->client->name); + break; + case EVENT_PAYLOAD_SESSION: + evbuffer_add_printf(evb, "$%u", epi->session->id); + break; + case EVENT_PAYLOAD_WINDOW: + evbuffer_add_printf(evb, "@%u", epi->window->id); + break; + case EVENT_PAYLOAD_PANE: + evbuffer_add_printf(evb, "%%%u", epi->pane->id); + break; + case EVENT_PAYLOAD_POINTER: + if (epi->pointer.print_cb != NULL) + epi->pointer.print_cb(epi->pointer.ptr, evb); + else + evbuffer_add_printf(evb, "%p", epi->pointer.ptr); + break; + } +} + +/* Print a payload item. */ +char * +event_payload_item_print(struct event_payload_item *epi) +{ + struct evbuffer *evb; + char *value = NULL; + size_t size; + + evb = evbuffer_new(); + if (evb == NULL) + fatalx("out of memory"); + event_payload_add_item(epi, evb); + if ((size = EVBUFFER_LENGTH(evb)) != 0) + value = xmemdup(EVBUFFER_DATA(evb), size); + else + value = xstrdup(""); + evbuffer_free(evb); + return (value); +} + +/* Print an item value. */ +char * +event_payload_print(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL) + return (NULL); + return (event_payload_item_print(epi)); +} + +/* Add payload items as formats. */ +void +event_payload_add_formats(struct event_payload *ep, struct format_tree *ft, + const char *prefix) +{ + struct event_payload_item *epi; + char *name, *value; + const char *key; + + if (prefix == NULL) + prefix = ""; + + RB_FOREACH(epi, event_payload_tree, &ep->items) { + key = epi->name; + if (*key == '_') + continue; + + value = event_payload_item_print(epi); + xasprintf(&name, "%s%s", prefix, key); + format_add(ft, name, "%s", value); + free(name); + free(value); + + if (epi->type == EVENT_PAYLOAD_SESSION) { + xasprintf(&name, "%s%s_name", prefix, key); + format_add(ft, name, "%s", epi->session->name); + free(name); + } else if (epi->type == EVENT_PAYLOAD_WINDOW) { + xasprintf(&name, "%s%s_name", prefix, key); + format_add(ft, name, "%s", epi->window->name); + free(name); + } + } +} + +/* Get the first payload item. */ +struct event_payload_item * +event_payload_first(struct event_payload *ep) +{ + return (RB_MIN(event_payload_tree, &ep->items)); +} + +/* Get the next payload item. */ +struct event_payload_item * +event_payload_next(struct event_payload_item *epi) +{ + return (RB_NEXT(event_payload_tree, , epi)); +} + +/* Get a payload item name. */ +const char * +event_payload_item_name(struct event_payload_item *epi) +{ + return (epi->name); +} + +/* Get a payload item type. */ +enum event_payload_type +event_payload_item_type(struct event_payload_item *epi) +{ + return (epi->type); +} + +/* Log a payload. */ +void +event_payload_log(struct event_payload *ep, const char *fmt, ...) +{ + struct event_payload_item *epi; + struct evbuffer *evb; + va_list ap; + char *prefix; + + va_start(ap, fmt); + xvasprintf(&prefix, fmt, ap); + va_end(ap); + + evb = evbuffer_new(); + if (evb == NULL) + fatalx("out of memory"); + if (ep != NULL) { + RB_FOREACH(epi, event_payload_tree, &ep->items) { + if (EVBUFFER_LENGTH(evb) != 0) + evbuffer_add_printf(evb, ", "); + evbuffer_add_printf(evb, "%s=", epi->name); + event_payload_add_item(epi, evb); + } + } + log_debug("%s%.*s", prefix, (int)EVBUFFER_LENGTH(evb), + (char *)EVBUFFER_DATA(evb)); + evbuffer_free(evb); + free(prefix); +} + +/* Get a time item. */ +time_t +event_payload_get_time(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_TIME) + return (0); + return (epi->time); +} + +/* Get a number item. */ +int +event_payload_get_int(struct event_payload *ep, const char *name, int *value) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_INT) + return (-1); + *value = epi->number; + return (0); +} + +/* Get an unsigned number item. */ +int +event_payload_get_uint(struct event_payload *ep, const char *name, u_int *value) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_UINT) + return (-1); + *value = epi->unsigned_number; + return (0); +} + +/* Get a client item. */ +struct client * +event_payload_get_client(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_CLIENT) + return (NULL); + return (epi->client); +} + +/* Get a session item. */ +struct session * +event_payload_get_session(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_SESSION) + return (NULL); + return (epi->session); +} + +/* Get a window item. */ +struct window * +event_payload_get_window(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_WINDOW) + return (NULL); + return (epi->window); +} + +/* Get a pane item. */ +struct window_pane * +event_payload_get_pane(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_PANE) + return (NULL); + return (epi->pane); +} + +/* Get a pointer item. */ +void * +event_payload_get_pointer(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_POINTER) + return (NULL); + return (epi->pointer.ptr); +} diff --git a/events.c b/events.c new file mode 100644 index 000000000..8d6e3eed1 --- /dev/null +++ b/events.c @@ -0,0 +1,199 @@ +/* $OpenBSD: events.c,v 1.1 2026/07/10 13:38:45 nicm Exp $ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include "tmux.h" + +/* Event sink. */ +struct events_sink { + char *name; + events_cb cb; + void *data; + int dead; + u_int generation; + + TAILQ_ENTRY(events_sink) entry; +}; + +TAILQ_HEAD(events_sinks, events_sink); +static struct events_sinks events_sinks = TAILQ_HEAD_INITIALIZER(events_sinks); + +static u_int events_dispatching; +static u_int events_generation; + +/* Free an event sink. */ +static void +events_free_sink(struct events_sink *es) +{ + TAILQ_REMOVE(&events_sinks, es, entry); + free(es->name); + free(es); +} + +/* Free dead event sinks. */ +static void +events_free_dead(void) +{ + struct events_sink *es, *es1; + + TAILQ_FOREACH_SAFE(es, &events_sinks, entry, es1) { + if (es->dead) + events_free_sink(es); + } +} + +/* Add an event sink. */ +struct events_sink * +events_add_sink(const char *name, events_cb cb, void *data) +{ + struct events_sink *es; + + es = xcalloc(1, sizeof *es); + es->name = xstrdup(name); + es->cb = cb; + es->data = data; + es->generation = ++events_generation; + + TAILQ_INSERT_TAIL(&events_sinks, es, entry); + return (es); +} + +/* Remove an event sink. */ +void +events_remove_sink(struct events_sink *es) +{ + if (es != NULL && !es->dead) { + if (events_dispatching != 0) + es->dead = 1; + else + events_free_sink(es); + } +} + +/* Fire an event. */ +void +events_fire(const char *name, struct event_payload *ep) +{ + struct events_sink *es; + u_int generation = events_generation; + + event_payload_set_string(ep, "event", "%s", name); + + if (log_get_level() != 0) + event_payload_log(ep, "%s: %s: ", __func__, name); + + events_dispatching++; + TAILQ_FOREACH(es, &events_sinks, entry) { + if (es->dead || es->generation > generation) + continue; + if (strcmp(es->name, name) == 0) + es->cb(name, ep, es->data); + } + if (--events_dispatching == 0) + events_free_dead(); + event_payload_free(ep); +} + +/* Fire a client event. */ +void +events_fire_client(const char *name, struct client *c) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_client(&fs, c, 0); + event_payload_set_target(ep, &fs); + event_payload_set_client(ep, "client", c); + if (fs.s != NULL) + event_payload_set_session(ep, "session", fs.s); + if (fs.w != NULL) + event_payload_set_window(ep, "window", fs.w); + if (fs.wl != NULL) + event_payload_set_int(ep, "window_index", fs.wl->idx); + else if (fs.idx != -1) + event_payload_set_int(ep, "window_index", fs.idx); + if (fs.wp != NULL) + event_payload_set_pane(ep, "pane", fs.wp); + events_fire(name, ep); +} + +/* Fire a session event. */ +void +events_fire_session(const char *name, struct session *s) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + if (session_alive(s)) { + cmd_find_from_session(&fs, s, 0); + event_payload_set_target(ep, &fs); + } + event_payload_set_session(ep, "session", s); + events_fire(name, ep); +} + +/* Fire a window event. */ +void +events_fire_window(const char *name, struct window *w) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_window(&fs, w, 0); + event_payload_set_target(ep, &fs); + event_payload_set_window(ep, "window", w); + events_fire(name, ep); +} + +/* Fire a pane event. */ +void +events_fire_pane(const char *name, struct window_pane *wp) +{ + 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); + events_fire(name, ep); +} + +/* Fire a winlink event. */ +void +events_fire_winlink(const char *name, struct winlink *wl) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_winlink(&fs, wl, 0); + event_payload_set_target(ep, &fs); + event_payload_set_session(ep, "session", wl->session); + event_payload_set_window(ep, "window", wl->window); + event_payload_set_int(ep, "window_index", wl->idx); + events_fire(name, ep); +} diff --git a/file.c b/file.c index 38a692e0d..a9c393c04 100644 --- a/file.c +++ b/file.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: file.c,v 1.20 2026/05/17 10:54:01 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/format-draw.c b/format-draw.c index 47bf192e6..ea1da421a 100644 --- a/format-draw.c +++ b/format-draw.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: format-draw.c,v 1.33 2026/06/29 17:08:52 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/format.c b/format.c index 7ec4f06b9..da374d580 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: format.c,v 1.402 2026/07/09 06:33:19 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott diff --git a/fuzzy.c b/fuzzy.c index 00fc90687..83d34715c 100644 --- a/fuzzy.c +++ b/fuzzy.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: fuzzy.c,v 1.1 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/grid-reader.c b/grid-reader.c index 421c25181..9b40def6c 100644 --- a/grid-reader.c +++ b/grid-reader.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: grid-reader.c,v 1.10 2026/05/17 13:12:21 nicm Exp $ */ /* * Copyright (c) 2020 Anindya Mukherjee diff --git a/grid-view.c b/grid-view.c index 655ec9e46..86317526f 100644 --- a/grid-view.c +++ b/grid-view.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: grid-view.c,v 1.38 2026/01/22 08:55:01 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/grid.c b/grid.c index aabc3cf3f..878c01bab 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: grid.c,v 1.153 2026/07/02 08:51:05 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/hooks.c b/hooks.c new file mode 100644 index 000000000..0ac64b8bd --- /dev/null +++ b/hooks.c @@ -0,0 +1,461 @@ +/* $OpenBSD: hooks.c,v 1.12 2026/07/10 13:38:45 nicm Exp $ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * Copyright (c) 2012 George Nachman + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include "tmux.h" + +/* Hook monitor state owned by an option entry. */ +struct hook_monitor { + struct options *oo; + + struct monitor_set *set; + struct events_sink *sink; + struct cmd_find_state fs; + + enum monitor_type type; + int id; + char *format; +}; + +/* Hook command data built from an event payload. */ +struct hooks_data { + const char *name; + struct cmd_find_state fs; + struct format_tree *formats; + struct options *oo; + struct client *client; + int expand; +}; + +/* Hook event sink registered for a notify event name. */ +struct hooks_event { + char *name; + struct events_sink *sink; + TAILQ_ENTRY(hooks_event) entry; +}; +TAILQ_HEAD(hooks_events, hooks_event); +static struct hooks_events hooks_events = TAILQ_HEAD_INITIALIZER(hooks_events); + +/* Insert one hook command list. */ +static struct cmdq_item * +hooks_insert_one(struct cmdq_item *item, struct hooks_data *hd, + struct cmd_list *cmdlist, struct cmdq_state *state) +{ + struct cmdq_item *new_item; + char *s; + + if (cmdlist == NULL) + return (item); + if (log_get_level() != 0) { + s = cmd_list_print(cmdlist, 0); + log_debug("%s: hook %s is: %s", __func__, hd->name, s); + free(s); + } + new_item = cmdq_get_command(cmdlist, state); + if (item != NULL) + return (cmdq_insert_after(item, new_item)); + return (cmdq_append(NULL, new_item)); +} + +/* Parse a hook command. */ +static struct cmd_parse_result * +hooks_parse(struct hooks_data *hd, struct cmd_find_state *fs, + const char *value) +{ + struct cmd_parse_result *pr; + struct format_tree *ft; + char *expanded; + + if (!hd->expand) + return (cmd_parse_from_string(value, NULL)); + + ft = format_create_defaults(NULL, hd->client, fs->s, fs->wl, fs->wp); + if (hd->formats != NULL) + format_merge(ft, hd->formats); + expanded = format_expand(ft, value); + format_free(ft); + + pr = cmd_parse_from_string(expanded, NULL); + free(expanded); + return (pr); +} + +/* Insert commands for a hook. */ +static void +hooks_insert(struct cmdq_item *item, struct hooks_data *hd) +{ + struct cmd_find_state fs; + struct options *oo; + struct cmdq_state *state; + struct options_entry *o; + struct options_array_item *a; + struct cmd_list *cmdlist; + const char *value; + struct cmd_parse_result *pr; + + log_debug("%s: inserting hook %s", __func__, hd->name); + + cmd_find_clear_state(&fs, 0); + if (cmd_find_empty_state(&hd->fs) || !cmd_find_valid_state(&hd->fs)) + cmd_find_from_nothing(&fs, 0); + else + cmd_find_copy_state(&fs, &hd->fs); + + if (hd->oo != NULL) { + oo = hd->oo; + o = options_get_only(oo, hd->name); + } else { + if (fs.s == NULL) + oo = global_s_options; + else + oo = fs.s->options; + o = options_get(oo, hd->name); + if (o == NULL && fs.wp != NULL) { + oo = fs.wp->options; + o = options_get(oo, hd->name); + } + if (o == NULL && fs.wl != NULL) { + oo = fs.wl->window->options; + o = options_get(oo, hd->name); + } + } + if (o == NULL) { + log_debug("%s: hook %s not found", __func__, hd->name); + return; + } + + if (item == NULL) + state = cmdq_new_state(&fs, NULL, CMDQ_STATE_NOHOOKS); + else { + state = cmdq_new_state(&fs, cmdq_get_event(item), + CMDQ_STATE_NOHOOKS); + } + cmdq_add_formats(state, hd->formats); + + if (*hd->name == '@') { + value = options_get_string(oo, hd->name); + pr = hooks_parse(hd, &fs, value); + switch (pr->status) { + case CMD_PARSE_ERROR: + log_debug("%s: can't parse hook %s: %s", __func__, + hd->name, pr->error); + free(pr->error); + break; + case CMD_PARSE_SUCCESS: + hooks_insert_one(item, hd, pr->cmdlist, state); + break; + } + } else { + a = options_array_first(o); + while (a != NULL) { + if (hd->expand) { + value = options_array_item_value(a)->string; + pr = hooks_parse(hd, &fs, value); + switch (pr->status) { + case CMD_PARSE_ERROR: + if (pr->error != NULL) { + cmdq_error(item, "%s", + pr->error); + } + break; + case CMD_PARSE_SUCCESS: + item = hooks_insert_one(item, hd, + pr->cmdlist, state); + break; + } + } else { + cmdlist = options_array_item_value(a)->cmdlist; + item = hooks_insert_one(item, hd, cmdlist, + state); + } + a = options_array_next(a); + } + } + + cmdq_free_state(state); +} + +/* Insert commands for a hook event. */ +static void +hooks_insert_event(struct cmdq_item *item, const char *name, + struct event_payload *ep, struct options *oo, int expand) +{ + struct hooks_data hd; + struct format_tree *ft; + struct client *c; + + if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) + return; + + c = event_payload_get_client(ep, "client"); + ft = format_create(c, item, FORMAT_NONE, FORMAT_NOJOBS); + event_payload_add_formats(ep, ft, "hook_"); + format_add(ft, "hook", "%s", name); + format_log_debug(ft, __func__); + + memset(&hd, 0, sizeof hd); + hd.name = name; + cmd_find_clear_state(&hd.fs, 0); + event_payload_get_target(ep, &hd.fs); + hd.formats = ft; + hd.oo = oo; + hd.client = c; + hd.expand = expand; + + hooks_insert(item, &hd); + format_free(ft); +} + +/* Handle an event for hooks. */ +static void +hooks_event_cb(const char *name, struct event_payload *ep, + __unused void *sink_data) +{ + struct cmdq_item *item; + + if (event_payload_get_pointer(ep, "_hook_monitor") != NULL) + return; + + item = event_payload_get_pointer(ep, "_cmdq_item"); + if (item != NULL) { + hooks_insert_event(item, name, ep, NULL, 0); + return; + } + + item = cmdq_running(NULL); + if (item == NULL || (~cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) + hooks_insert_event(NULL, name, ep, NULL, 0); +} + +/* Add a hook event sink. */ +void +hooks_add_event(const char *name) +{ + struct hooks_event *he; + + TAILQ_FOREACH(he, &hooks_events, entry) { + if (strcmp(he->name, name) == 0) + return; + } + + he = xcalloc(1, sizeof *he); + he->name = xstrdup(name); + he->sink = events_add_sink(name, hooks_event_cb, NULL); + TAILQ_INSERT_TAIL(&hooks_events, he, entry); +} + +/* Return if an event name can be fired through the hooks path. */ +int +hooks_valid_event_name(const char *name) +{ + const struct options_table_entry *oe; + + if (*name == '@') + return (1); + oe = options_search(name); + return (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)); +} + +/* Add hook event sinks for all built-in hooks. */ +void +hooks_build_events(void) +{ + const struct options_table_entry *oe; + + for (oe = options_table; oe->name != NULL; oe++) { + if (oe->flags & OPTIONS_TABLE_IS_HOOK) + hooks_add_event(oe->name); + } +} + +/* Run a hook immediately. */ +void +hooks_run(struct cmdq_item *item, const char *name) +{ + struct cmd_find_state *target = cmdq_get_target(item); + struct hooks_data hd = { 0 }; + + hd.name = name; + cmd_find_copy_state(&hd.fs, target); + hd.client = cmdq_get_client(item); + + hd.formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS); + format_add(hd.formats, "hook", "%s", name); + format_log_debug(hd.formats, __func__); + + hooks_insert(item, &hd); + format_free(hd.formats); +} + +/* Free a hook monitor. */ +void +hooks_monitor_free(void *data) +{ + struct hook_monitor *hm = data; + + events_remove_sink(hm->sink); + monitor_destroy(hm->set); + free(hm->format); + free(hm); +} + +/* Remove a hook monitor. */ +void +hooks_monitor_remove(struct options *oo, const char *name) +{ + struct options_entry *o; + struct hook_monitor *hm; + + o = options_get_only(oo, name); + if (o == NULL) + return; + + hm = options_get_monitor_data(o); + if (hm != NULL) { + options_set_monitor_data(o, NULL); + hooks_monitor_free(hm); + } +} + +/* Handle a hook monitor event. */ +static void +hooks_monitor_hook_cb(const char *name, struct event_payload *ep, + void *sink_data) +{ + struct hook_monitor *hm = sink_data; + + if (event_payload_get_pointer(ep, "_hook_monitor") == hm) + hooks_insert_event(cmdq_running(NULL), name, ep, hm->oo, 1); +} + +/* Fire a hook monitor event. */ +static void +hooks_monitor_cb(struct monitor_change *change, void *data) +{ + struct hook_monitor *hm = data; + struct event_payload *ep; + struct winlink *wl = change->wl; + struct window_pane *wp = change->wp; + struct cmd_find_state fs; + + ep = event_payload_create(); + event_payload_set_pointer(ep, "_hook_monitor", data, NULL, NULL); + + cmd_find_clear_state(&fs, 0); + if (wl != NULL && wp != NULL && wp->window == wl->window) + cmd_find_from_winlink_pane(&fs, wl, wp, 0); + else if (wl != NULL) + cmd_find_from_winlink(&fs, wl, 0); + else if (wp != NULL) + cmd_find_from_pane(&fs, wp, 0); + else if (change->s != NULL) + cmd_find_from_session(&fs, change->s, 0); + else + cmd_find_copy_state(&fs, &hm->fs); + event_payload_set_target(ep, &fs); + + if (change->value != NULL) + event_payload_set_string(ep, "value", "%s", change->value); + else + event_payload_set_string(ep, "value", "%s", ""); + if (change->last != NULL) + event_payload_set_string(ep, "last", "%s", change->last); + else + event_payload_set_string(ep, "last", "%s", ""); + + if (change->c != NULL) + event_payload_set_client(ep, "client", change->c); + if (change->s != NULL) + event_payload_set_session(ep, "session", change->s); + if (wl != NULL) { + if (change->s == NULL) + event_payload_set_session(ep, "session", + wl->session); + event_payload_set_window(ep, "window", wl->window); + event_payload_set_int(ep, "window_index", wl->idx); + } + if (wp != NULL) { + event_payload_set_pane(ep, "pane", wp); + if (wl == NULL) + event_payload_set_window(ep, "window", wp->window); + } + + events_fire(change->name, ep); +} + +/* Add a hook monitor. */ +void +hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, + const char *name, enum monitor_type type, int id, const char *format, + struct cmd_find_state *fs, struct session *s) +{ + struct options_entry *o; + struct hook_monitor *hm; + + hooks_monitor_remove(oo, name); + o = options_get_only(oo, name); + if (o == NULL) + o = options_set_string(oo, name, 0, "%s", ""); + + hm = xcalloc(1, sizeof *hm); + hm->oo = oo; + cmd_find_copy_state(&hm->fs, fs); + hm->type = type; + hm->id = id; + hm->format = xstrdup(format); + hm->set = monitor_create_session(s, hooks_monitor_cb, hm); + hm->sink = events_add_sink(name, hooks_monitor_hook_cb, hm); + options_set_monitor_data(o, hm); + monitor_add(hm->set, name, type, id, format, 0); +} + +/* Convert a hook monitor to its value. */ +char * +hooks_monitor_to_string(struct options_entry *o) +{ + struct hook_monitor *hm = options_get_monitor_data(o); + const char *name = options_name(o); + char *s; + + if (hm == NULL) + return (NULL); + + switch (hm->type) { + case MONITOR_SESSION: + xasprintf(&s, "%s::%s", name, hm->format); + break; + case MONITOR_PANE: + xasprintf(&s, "%s:%%%d:%s", name, hm->id, hm->format); + break; + case MONITOR_ALL_PANES: + xasprintf(&s, "%s:%%*:%s", name, hm->format); + break; + case MONITOR_WINDOW: + xasprintf(&s, "%s:@%d:%s", name, hm->id, hm->format); + break; + case MONITOR_ALL_WINDOWS: + xasprintf(&s, "%s:@*:%s", name, hm->format); + break; + } + return (s); +} diff --git a/hyperlinks.c b/hyperlinks.c index a71827272..1820fdad2 100644 --- a/hyperlinks.c +++ b/hyperlinks.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: hyperlinks.c,v 1.5 2026/06/29 16:44:06 nicm Exp $ */ /* * Copyright (c) 2021 Will diff --git a/input-keys.c b/input-keys.c index 277c9e71e..3208a33c3 100644 --- a/input-keys.c +++ b/input-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: input-keys.c,v 1.114 2026/06/15 21:47:01 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/input.c b/input.c index 2438ea504..bfab97a7b 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: input.c,v 1.265 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -174,6 +174,8 @@ 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 *); @@ -210,6 +212,21 @@ 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; @@ -2161,7 +2178,7 @@ input_csi_dispatch_winops(struct input_ctx *ictx) screen_pop_title(sctx->s); if (wp == NULL) break; - notify_pane("pane-title-changed", wp); + events_fire_pane("pane-title-changed", wp); server_redraw_window_borders(w); server_status_window(w); break; @@ -2663,7 +2680,7 @@ input_exit_osc(struct input_ctx *ictx) if (wp != NULL && options_get_number(wp->options, "allow-set-title") && screen_set_title(sctx->s, p, 1)) { - notify_pane("pane-title-changed", wp); + input_fire_pane_title_changed(wp, p); server_redraw_window_borders(wp->window); server_status_window(wp->window); } @@ -2741,7 +2758,7 @@ input_exit_apc(struct input_ctx *ictx) if (wp != NULL && options_get_number(wp->options, "allow-set-title") && screen_set_title(sctx->s, ictx->input_buf, 1)) { - notify_pane("pane-title-changed", wp); + input_fire_pane_title_changed(wp, ictx->input_buf); server_redraw_window_borders(wp->window); server_status_window(wp->window); } @@ -3252,7 +3269,7 @@ input_osc_52(struct input_ctx *ictx, const char *p) screen_write_start_pane(&ctx, wp, NULL); screen_write_setselection(&ctx, clip, out, outlen); screen_write_stop(&ctx); - notify_pane("pane-set-clipboard", wp); + events_fire_pane("pane-set-clipboard", wp); paste_add(NULL, out, outlen); } } diff --git a/job.c b/job.c index 5ad179ffb..8400ea584 100644 --- a/job.c +++ b/job.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: job.c,v 1.74 2025/09/08 11:21:56 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/key-bindings.c b/key-bindings.c index c73fd7c33..92d417c5b 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: key-bindings.c,v 1.186 2026/07/07 12:30:36 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/key-string.c b/key-string.c index 3df9da4b9..ac7a1bf89 100644 --- a/key-string.c +++ b/key-string.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: key-string.c,v 1.77 2026/03/31 11:46:43 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/layout-custom.c b/layout-custom.c index 65fb3780b..9df0448ff 100644 --- a/layout-custom.c +++ b/layout-custom.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: layout-custom.c,v 1.37 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott @@ -286,7 +286,7 @@ layout_parse(struct window *w, const char *layout, char **cause) recalculate_sizes(); layout_print_cell(tiled_lc, __func__, 0); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); return (0); diff --git a/layout-set.c b/layout-set.c index 2f164b01c..7e944d32f 100644 --- a/layout-set.c +++ b/layout-set.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: layout-set.c,v 1.39 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -183,7 +183,7 @@ layout_set_even(struct window *w, enum layout_type type) layout_print_cell(w->layout_root, __func__, 1); window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -292,7 +292,7 @@ layout_set_main_h(struct window *w) layout_print_cell(w->layout_root, __func__, 1); window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -389,7 +389,7 @@ layout_set_main_h_mirrored(struct window *w) layout_print_cell(w->layout_root, __func__, 1); window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -486,7 +486,7 @@ layout_set_main_v(struct window *w) layout_print_cell(w->layout_root, __func__, 1); window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -584,7 +584,7 @@ layout_set_main_v_mirrored(struct window *w) layout_print_cell(w->layout_root, __func__, 1); window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -706,6 +706,6 @@ layout_set_tiled(struct window *w) layout_print_cell(w->layout_root, __func__, 1); window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } diff --git a/layout.c b/layout.c index 507dd63c1..177d2f4cf 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: layout.c,v 1.93 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -957,7 +957,7 @@ layout_resize_layout(struct window *w, struct layout_cell *lc, /* Fix cell offsets. */ layout_fix_offsets(w); layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); } /* Resize a single pane within the layout. */ @@ -1502,7 +1502,7 @@ layout_close_pane(struct window_pane *wp) layout_fix_offsets(w); layout_fix_panes(w, NULL); } - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); } /* Spread out cells inside a parent cell. */ diff --git a/log.c b/log.c index ede6e2577..e596088b2 100644 --- a/log.c +++ b/log.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: log.c,v 1.33 2026/05/17 16:02:33 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/menu.c b/menu.c index 257452fd7..ec7b9781b 100644 --- a/menu.c +++ b/menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: menu.c,v 1.68 2026/07/07 09:45:09 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/mode-key.c b/mode-key.c deleted file mode 100644 index 4245d173b..000000000 --- a/mode-key.c +++ /dev/null @@ -1,261 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2008 Nicholas Marriott - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include - -#include "tmux.h" - -/* - * Mode keys. These are the key bindings used when editing (status prompt), and - * in the modes. They are split into two sets of three tables, one set of three - * for vi and the other for emacs key bindings. The three tables are for - * editing, for menu-like modes (choice, more), and for copy modes (copy, - * scroll). - * - * The fixed tables of struct mode_key_entry below are the defaults: they are - * built into a tree of struct mode_key_binding by mode_key_init_trees, which - * can then be modified. - * - * vi command mode is handled by having a mode flag in the struct which allows - * two sets of bindings to be swapped between. A couple of editing commands - * (any matching MODEKEYEDIT_SWITCHMODE*) are special-cased to do this. - */ - -/* Command to string mapping. */ -struct mode_key_cmdstr { - enum mode_key_cmd cmd; - const char *name; -}; - -/* Entry in the default mode key tables. */ -struct mode_key_entry { - key_code key; - enum mode_key_cmd cmd; -}; - -/* Choice keys command strings. */ -static const struct mode_key_cmdstr mode_key_cmdstr_choice[] = { - { MODEKEYCHOICE_BACKSPACE, "backspace" }, - { MODEKEYCHOICE_BOTTOMLINE, "bottom-line"}, - { MODEKEYCHOICE_CANCEL, "cancel" }, - { MODEKEYCHOICE_CHOOSE, "choose" }, - { MODEKEYCHOICE_DOWN, "down" }, - { MODEKEYCHOICE_ENDOFLIST, "end-of-list"}, - { MODEKEYCHOICE_PAGEDOWN, "page-down" }, - { MODEKEYCHOICE_PAGEUP, "page-up" }, - { MODEKEYCHOICE_SCROLLDOWN, "scroll-down" }, - { MODEKEYCHOICE_SCROLLUP, "scroll-up" }, - { MODEKEYCHOICE_STARTNUMBERPREFIX, "start-number-prefix" }, - { MODEKEYCHOICE_STARTOFLIST, "start-of-list"}, - { MODEKEYCHOICE_TOPLINE, "top-line"}, - { MODEKEYCHOICE_TREE_COLLAPSE, "tree-collapse" }, - { MODEKEYCHOICE_TREE_COLLAPSE_ALL, "tree-collapse-all" }, - { MODEKEYCHOICE_TREE_EXPAND, "tree-expand" }, - { MODEKEYCHOICE_TREE_EXPAND_ALL, "tree-expand-all" }, - { MODEKEYCHOICE_TREE_TOGGLE, "tree-toggle" }, - { MODEKEYCHOICE_UP, "up" }, - - { 0, NULL } -}; - -/* vi choice selection keys. */ -static const struct mode_key_entry mode_key_vi_choice[] = { - { '0' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '1' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '2' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '3' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '4' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '5' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '6' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '7' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '8' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '9' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '\002' /* C-b */, MODEKEYCHOICE_PAGEUP }, - { '\003' /* C-c */, MODEKEYCHOICE_CANCEL }, - { '\005' /* C-e */, MODEKEYCHOICE_SCROLLDOWN }, - { '\006' /* C-f */, MODEKEYCHOICE_PAGEDOWN }, - { '\031' /* C-y */, MODEKEYCHOICE_SCROLLUP }, - { '\n', MODEKEYCHOICE_CHOOSE }, - { '\r', MODEKEYCHOICE_CHOOSE }, - { 'j', MODEKEYCHOICE_DOWN }, - { 'k', MODEKEYCHOICE_UP }, - { 'q', MODEKEYCHOICE_CANCEL }, - { KEYC_HOME, MODEKEYCHOICE_STARTOFLIST }, - { 'g', MODEKEYCHOICE_STARTOFLIST }, - { 'H', MODEKEYCHOICE_TOPLINE }, - { 'L', MODEKEYCHOICE_BOTTOMLINE }, - { 'G', MODEKEYCHOICE_ENDOFLIST }, - { KEYC_END, MODEKEYCHOICE_ENDOFLIST }, - { KEYC_BSPACE, MODEKEYCHOICE_BACKSPACE }, - { KEYC_DOWN | KEYC_CTRL, MODEKEYCHOICE_SCROLLDOWN }, - { KEYC_DOWN, MODEKEYCHOICE_DOWN }, - { KEYC_NPAGE, MODEKEYCHOICE_PAGEDOWN }, - { KEYC_PPAGE, MODEKEYCHOICE_PAGEUP }, - { KEYC_UP | KEYC_CTRL, MODEKEYCHOICE_SCROLLUP }, - { KEYC_UP, MODEKEYCHOICE_UP }, - { ' ', MODEKEYCHOICE_TREE_TOGGLE }, - { KEYC_LEFT, MODEKEYCHOICE_TREE_COLLAPSE }, - { KEYC_RIGHT, MODEKEYCHOICE_TREE_EXPAND }, - { KEYC_LEFT | KEYC_CTRL, MODEKEYCHOICE_TREE_COLLAPSE_ALL }, - { KEYC_RIGHT | KEYC_CTRL, MODEKEYCHOICE_TREE_EXPAND_ALL }, - { KEYC_MOUSEDOWN1_PANE, MODEKEYCHOICE_CHOOSE }, - { KEYC_MOUSEDOWN3_PANE, MODEKEYCHOICE_TREE_TOGGLE }, - { KEYC_WHEELUP_PANE, MODEKEYCHOICE_UP }, - { KEYC_WHEELDOWN_PANE, MODEKEYCHOICE_DOWN }, - - { KEYC_NONE, -1 } -}; -struct mode_key_tree mode_key_tree_vi_choice; - -/* emacs choice selection keys. */ -static const struct mode_key_entry mode_key_emacs_choice[] = { - { '0' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '1' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '2' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '3' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '4' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '5' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '6' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '7' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '8' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '9' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '\003' /* C-c */, MODEKEYCHOICE_CANCEL }, - { '\016' /* C-n */, MODEKEYCHOICE_DOWN }, - { '\020' /* C-p */, MODEKEYCHOICE_UP }, - { '\026' /* C-v */, MODEKEYCHOICE_PAGEDOWN }, - { '\033' /* Escape */, MODEKEYCHOICE_CANCEL }, - { '\n', MODEKEYCHOICE_CHOOSE }, - { '\r', MODEKEYCHOICE_CHOOSE }, - { 'q', MODEKEYCHOICE_CANCEL }, - { 'v' | KEYC_ESCAPE, MODEKEYCHOICE_PAGEUP }, - { KEYC_HOME, MODEKEYCHOICE_STARTOFLIST }, - { '<' | KEYC_ESCAPE, MODEKEYCHOICE_STARTOFLIST }, - { 'R' | KEYC_ESCAPE, MODEKEYCHOICE_TOPLINE }, - { '>' | KEYC_ESCAPE, MODEKEYCHOICE_ENDOFLIST }, - { KEYC_END, MODEKEYCHOICE_ENDOFLIST }, - { KEYC_BSPACE, MODEKEYCHOICE_BACKSPACE }, - { KEYC_DOWN | KEYC_CTRL, MODEKEYCHOICE_SCROLLDOWN }, - { KEYC_DOWN, MODEKEYCHOICE_DOWN }, - { KEYC_NPAGE, MODEKEYCHOICE_PAGEDOWN }, - { KEYC_PPAGE, MODEKEYCHOICE_PAGEUP }, - { KEYC_UP | KEYC_CTRL, MODEKEYCHOICE_SCROLLUP }, - { KEYC_UP, MODEKEYCHOICE_UP }, - { ' ', MODEKEYCHOICE_TREE_TOGGLE }, - { KEYC_LEFT, MODEKEYCHOICE_TREE_COLLAPSE }, - { KEYC_RIGHT, MODEKEYCHOICE_TREE_EXPAND }, - { KEYC_LEFT | KEYC_CTRL, MODEKEYCHOICE_TREE_COLLAPSE_ALL }, - { KEYC_RIGHT | KEYC_CTRL, MODEKEYCHOICE_TREE_EXPAND_ALL }, - { KEYC_MOUSEDOWN1_PANE, MODEKEYCHOICE_CHOOSE }, - { KEYC_MOUSEDOWN3_PANE, MODEKEYCHOICE_TREE_TOGGLE }, - { KEYC_WHEELUP_PANE, MODEKEYCHOICE_UP }, - { KEYC_WHEELDOWN_PANE, MODEKEYCHOICE_DOWN }, - - { KEYC_NONE, -1 } -}; -struct mode_key_tree mode_key_tree_emacs_choice; - -/* Table mapping key table names to default settings and trees. */ -static const struct mode_key_table mode_key_tables[] = { - { "vi-choice", mode_key_cmdstr_choice, - &mode_key_tree_vi_choice, mode_key_vi_choice }, - { "emacs-choice", mode_key_cmdstr_choice, - &mode_key_tree_emacs_choice, mode_key_emacs_choice }, - - { NULL, NULL, NULL, NULL } -}; - -RB_GENERATE(mode_key_tree, mode_key_binding, entry, mode_key_cmp); - -int -mode_key_cmp(struct mode_key_binding *mbind1, struct mode_key_binding *mbind2) -{ - if (mbind1->key < mbind2->key) - return (-1); - if (mbind1->key > mbind2->key) - return (1); - return (0); -} - -const char * -mode_key_tostring(const struct mode_key_cmdstr *cmdstr, enum mode_key_cmd cmd) -{ - for (; cmdstr->name != NULL; cmdstr++) { - if (cmdstr->cmd == cmd) - return (cmdstr->name); - } - return (NULL); -} - -enum mode_key_cmd -mode_key_fromstring(const struct mode_key_cmdstr *cmdstr, const char *name) -{ - for (; cmdstr->name != NULL; cmdstr++) { - if (strcasecmp(cmdstr->name, name) == 0) - return (cmdstr->cmd); - } - return (MODEKEY_NONE); -} - -const struct mode_key_table * -mode_key_findtable(const char *name) -{ - const struct mode_key_table *mtab; - - for (mtab = mode_key_tables; mtab->name != NULL; mtab++) { - if (strcasecmp(name, mtab->name) == 0) - return (mtab); - } - return (NULL); -} - -void -mode_key_init_trees(void) -{ - const struct mode_key_table *mtab; - const struct mode_key_entry *ment; - struct mode_key_binding *mbind; - - for (mtab = mode_key_tables; mtab->name != NULL; mtab++) { - RB_INIT(mtab->tree); - for (ment = mtab->table; ment->key != KEYC_NONE; ment++) { - mbind = xmalloc(sizeof *mbind); - mbind->key = ment->key; - mbind->cmd = ment->cmd; - RB_INSERT(mode_key_tree, mtab->tree, mbind); - } - } -} - -void -mode_key_init(struct mode_key_data *mdata, struct mode_key_tree *mtree) -{ - mdata->tree = mtree; -} - -enum mode_key_cmd -mode_key_lookup(struct mode_key_data *mdata, key_code key) -{ - struct mode_key_binding *mbind, mtmp; - - mtmp.key = key; - if ((mbind = RB_FIND(mode_key_tree, mdata->tree, &mtmp)) == NULL) - return (MODEKEY_OTHER); - return (mbind->cmd); -} diff --git a/mode-tree.c b/mode-tree.c index 59d9e4fa9..ef859b15e 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: mode-tree.c,v 1.99 2026/07/06 12:08:52 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott diff --git a/monitor.c b/monitor.c index 6d1a747fd..514af4e3c 100644 --- a/monitor.c +++ b/monitor.c @@ -1,7 +1,7 @@ -/* $OpenBSD$ */ +/* $OpenBSD: monitor.c,v 1.5 2026/07/10 13:38:45 nicm Exp $ */ /* - * Copyright (c) 2026 Nicholas Marriott + * Copyright (c) 2026 Nicholas Marriott * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above diff --git a/names.c b/names.c index cee310366..aa7e6006e 100644 --- a/names.c +++ b/names.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: names.c,v 1.48 2026/06/29 18:17:28 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/notify.c b/notify.c deleted file mode 100644 index 50bb55022..000000000 --- a/notify.c +++ /dev/null @@ -1,516 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2012 George Nachman - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include -#include - -#include -#include - -#include "tmux.h" - -struct notify_entry { - const char *name; - struct cmd_find_state fs; - struct format_tree *formats; - struct options *oo; - - struct client *client; - struct session *session; - struct window *window; - int pane; - const char *pbname; - int expand; -}; - -struct notify_monitor { - struct options *oo; - - struct monitor_set *set; - struct cmd_find_state fs; - - enum monitor_type type; - int id; - char *format; -}; - -static struct cmdq_item * -notify_insert_one_hook(struct cmdq_item *item, struct notify_entry *ne, - struct cmd_list *cmdlist, struct cmdq_state *state) -{ - struct cmdq_item *new_item; - char *s; - - if (cmdlist == NULL) - return (item); - if (log_get_level() != 0) { - s = cmd_list_print(cmdlist, 0); - log_debug("%s: hook %s is: %s", __func__, ne->name, s); - free(s); - } - new_item = cmdq_get_command(cmdlist, state); - if (item != NULL) - return (cmdq_insert_after(item, new_item)); - return (cmdq_append(NULL, new_item)); -} - -static struct cmd_parse_result * -notify_parse_hook(struct notify_entry *ne, struct cmd_find_state *fs, - const char *value) -{ - struct cmd_parse_result *pr; - struct format_tree *ft; - char *expanded; - - if (!ne->expand) - return (cmd_parse_from_string(value, NULL)); - - ft = format_create_defaults(NULL, ne->client, fs->s, fs->wl, fs->wp); - if (ne->formats != NULL) - format_merge(ft, ne->formats); - expanded = format_expand(ft, value); - format_free(ft); - - pr = cmd_parse_from_string(expanded, NULL); - free(expanded); - return (pr); -} - -static void -notify_insert_hook(struct cmdq_item *item, struct notify_entry *ne) -{ - struct cmd_find_state fs; - struct options *oo; - struct cmdq_state *state; - struct options_entry *o; - struct options_array_item *a; - struct cmd_list *cmdlist; - const char *value; - struct cmd_parse_result *pr; - - log_debug("%s: inserting hook %s", __func__, ne->name); - - cmd_find_clear_state(&fs, 0); - if (cmd_find_empty_state(&ne->fs) || !cmd_find_valid_state(&ne->fs)) - cmd_find_from_nothing(&fs, 0); - else - cmd_find_copy_state(&fs, &ne->fs); - - if (ne->oo != NULL) { - oo = ne->oo; - o = options_get_only(oo, ne->name); - } else { - if (fs.s == NULL) - oo = global_s_options; - else - oo = fs.s->options; - o = options_get(oo, ne->name); - if (o == NULL && fs.wp != NULL) { - oo = fs.wp->options; - o = options_get(oo, ne->name); - } - if (o == NULL && fs.wl != NULL) { - oo = fs.wl->window->options; - o = options_get(oo, ne->name); - } - } - if (o == NULL) { - log_debug("%s: hook %s not found", __func__, ne->name); - return; - } - - state = cmdq_new_state(&fs, NULL, CMDQ_STATE_NOHOOKS); - cmdq_add_formats(state, ne->formats); - - if (*ne->name == '@') { - value = options_get_string(oo, ne->name); - pr = notify_parse_hook(ne, &fs, value); - switch (pr->status) { - case CMD_PARSE_ERROR: - log_debug("%s: can't parse hook %s: %s", __func__, - ne->name, pr->error); - free(pr->error); - break; - case CMD_PARSE_SUCCESS: - notify_insert_one_hook(item, ne, pr->cmdlist, state); - break; - } - } else { - a = options_array_first(o); - while (a != NULL) { - if (ne->expand) { - value = options_array_item_value(a)->string; - pr = notify_parse_hook(ne, &fs, value); - switch (pr->status) { - case CMD_PARSE_ERROR: - if (pr->error != NULL) - cmdq_error(item, "%s", pr->error); - break; - case CMD_PARSE_SUCCESS: - item = notify_insert_one_hook(item, ne, - pr->cmdlist, state); - break; - } - } else { - cmdlist = options_array_item_value(a)->cmdlist; - item = notify_insert_one_hook(item, ne, cmdlist, - state); - } - a = options_array_next(a); - } - } - - cmdq_free_state(state); -} - -static enum cmd_retval -notify_callback(struct cmdq_item *item, void *data) -{ - struct notify_entry *ne = data; - - log_debug("%s: %s", __func__, ne->name); - - if (strcmp(ne->name, "pane-mode-changed") == 0) - control_notify_pane_mode_changed(ne->pane); - if (strcmp(ne->name, "window-layout-changed") == 0) - control_notify_window_layout_changed(ne->window); - if (strcmp(ne->name, "window-pane-changed") == 0) - control_notify_window_pane_changed(ne->window); - if (strcmp(ne->name, "window-unlinked") == 0) - control_notify_window_unlinked(ne->session, ne->window); - if (strcmp(ne->name, "window-linked") == 0) - control_notify_window_linked(ne->session, ne->window); - if (strcmp(ne->name, "window-renamed") == 0) - control_notify_window_renamed(ne->window); - if (strcmp(ne->name, "client-session-changed") == 0) - control_notify_client_session_changed(ne->client); - if (strcmp(ne->name, "client-detached") == 0) - control_notify_client_detached(ne->client); - if (strcmp(ne->name, "session-renamed") == 0) - control_notify_session_renamed(ne->session); - if (strcmp(ne->name, "session-created") == 0) - control_notify_session_created(ne->session); - if (strcmp(ne->name, "session-closed") == 0) - control_notify_session_closed(ne->session); - if (strcmp(ne->name, "session-window-changed") == 0) - control_notify_session_window_changed(ne->session); - if (strcmp(ne->name, "paste-buffer-changed") == 0) - control_notify_paste_buffer_changed(ne->pbname); - if (strcmp(ne->name, "paste-buffer-deleted") == 0) - control_notify_paste_buffer_deleted(ne->pbname); - - notify_insert_hook(item, ne); - - if (ne->client != NULL) - server_client_unref(ne->client); - if (ne->session != NULL) - session_remove_ref(ne->session, __func__); - if (ne->window != NULL) - window_remove_ref(ne->window, __func__); - - if (ne->fs.s != NULL) - session_remove_ref(ne->fs.s, __func__); - - format_free(ne->formats); - free((void *)ne->name); - free((void *)ne->pbname); - free(ne); - - return (CMD_RETURN_NORMAL); -} - -void -notify_monitor_free(void *data) -{ - struct notify_monitor *nhm = data; - - monitor_destroy(nhm->set); - free(nhm->format); - free(nhm); -} - -void -notify_monitor_remove(struct options *oo, const char *name) -{ - struct options_entry *o; - struct notify_monitor *nhm; - - o = options_get_only(oo, name); - if (o == NULL) - return; - - nhm = options_get_monitor_data(o); - if (nhm != NULL) { - options_set_monitor_data(o, NULL); - notify_monitor_free(nhm); - } -} - -static void -notify_monitor_cb(struct monitor_change *change, void *data) -{ - struct notify_monitor *nhm = data; - struct notify_entry ne; - struct cmdq_item *item; - struct window *w; - - item = cmdq_running(NULL); - if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) - return; - - memset(&ne, 0, sizeof ne); - ne.name = change->name; - ne.oo = nhm->oo; - ne.client = change->c; - ne.expand = 1; - if (change->wp != NULL && change->wl != NULL) - cmd_find_from_winlink_pane(&ne.fs, change->wl, change->wp, 0); - else if (change->wl != NULL) - cmd_find_from_winlink(&ne.fs, change->wl, 0); - else if (change->s != NULL) - cmd_find_from_session(&ne.fs, change->s, 0); - else - cmd_find_copy_state(&ne.fs, &nhm->fs); - ne.formats = format_create(change->c, item, FORMAT_NONE, FORMAT_NOJOBS); - format_add(ne.formats, "hook", "%s", change->name); - format_add(ne.formats, "hook_value", "%s", change->value); - format_add(ne.formats, "hook_last", "%s", - change->last == NULL ? "" : change->last); - if (change->s != NULL) { - format_add(ne.formats, "hook_session", "$%u", change->s->id); - format_add(ne.formats, "hook_session_name", "%s", change->s->name); - } - if (change->wl != NULL) { - w = change->wl->window; - format_add(ne.formats, "hook_window", "@%u", w->id); - format_add(ne.formats, "hook_window_name", "%s", w->name); - format_add(ne.formats, "hook_window_index", "%d", change->wl->idx); - } - if (change->wp != NULL) { - format_add(ne.formats, "hook_pane", "%%%u", change->wp->id); - } - - notify_insert_hook(item, &ne); - format_free(ne.formats); -} - -void -notify_monitor_add(__unused struct cmdq_item *item, struct options *oo, - const char *name, enum monitor_type type, int id, const char *format, - struct cmd_find_state *fs, struct session *s) -{ - struct options_entry *o; - struct notify_monitor *nhm; - - notify_monitor_remove(oo, name); - o = options_get_only(oo, name); - if (o == NULL) - o = options_set_string(oo, name, 0, "%s", ""); - - nhm = xcalloc(1, sizeof *nhm); - nhm->oo = oo; - cmd_find_copy_state(&nhm->fs, fs); - nhm->type = type; - nhm->id = id; - nhm->format = xstrdup(format); - nhm->set = monitor_create_session(s, notify_monitor_cb, nhm); - options_set_monitor_data(o, nhm); - monitor_add(nhm->set, name, type, id, format, 0); -} - -/* Convert a hook monitor to its value. */ -char * -notify_monitor_to_string(struct options_entry *o) -{ - struct notify_monitor *nhm = options_get_monitor_data(o); - const char *name = options_name(o); - char *s; - - if (nhm == NULL) - return (NULL); - - switch (nhm->type) { - case MONITOR_SESSION: - xasprintf(&s, "%s::%s", name, nhm->format); - break; - case MONITOR_PANE: - xasprintf(&s, "%s:%%%d:%s", name, nhm->id, nhm->format); - break; - case MONITOR_ALL_PANES: - xasprintf(&s, "%s:%%*:%s", name, nhm->format); - break; - case MONITOR_WINDOW: - xasprintf(&s, "%s:@%d:%s", name, nhm->id, nhm->format); - break; - case MONITOR_ALL_WINDOWS: - xasprintf(&s, "%s:@*:%s", name, nhm->format); - break; - } - return (s); -} - -static void -notify_add(const char *name, struct cmd_find_state *fs, struct client *c, - struct session *s, struct window *w, struct window_pane *wp, - const char *pbname) -{ - struct notify_entry *ne; - struct cmdq_item *item; - - item = cmdq_running(NULL); - if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) - return; - - ne = xcalloc(1, sizeof *ne); - ne->name = xstrdup(name); - - ne->client = c; - ne->session = s; - ne->window = w; - ne->pane = (wp != NULL ? (int)wp->id : -1); - ne->pbname = (pbname != NULL ? xstrdup(pbname) : NULL); - - ne->formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS); - format_add(ne->formats, "hook", "%s", name); - if (c != NULL) - format_add(ne->formats, "hook_client", "%s", c->name); - if (s != NULL) { - format_add(ne->formats, "hook_session", "$%u", s->id); - format_add(ne->formats, "hook_session_name", "%s", s->name); - } - if (w != NULL) { - format_add(ne->formats, "hook_window", "@%u", w->id); - format_add(ne->formats, "hook_window_name", "%s", w->name); - } - if (wp != NULL) { - format_add(ne->formats, "hook_pane", "%%%d", wp->id); - format_add(ne->formats, "hook_window", "@%u", wp->window->id); - format_add(ne->formats, "hook_window_name", "%s", - wp->window->name); - } - format_log_debug(ne->formats, __func__); - - if (c != NULL) - c->references++; - if (s != NULL) - session_add_ref(s, __func__); - if (w != NULL) - window_add_ref(w, __func__); - - cmd_find_copy_state(&ne->fs, fs); - if (ne->fs.s != NULL) /* cmd_find_valid_state needs session */ - session_add_ref(ne->fs.s, __func__); - - cmdq_append(NULL, cmdq_get_callback(notify_callback, ne)); -} - -void -notify_hook(struct cmdq_item *item, const char *name) -{ - struct cmd_find_state *target = cmdq_get_target(item); - struct notify_entry ne; - - memset(&ne, 0, sizeof ne); - - ne.name = name; - cmd_find_copy_state(&ne.fs, target); - - ne.client = cmdq_get_client(item); - ne.session = target->s; - ne.window = target->w; - ne.pane = (target->wp != NULL ? (int)target->wp->id : -1); - - ne.formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS); - format_add(ne.formats, "hook", "%s", name); - format_log_debug(ne.formats, __func__); - - notify_insert_hook(item, &ne); - format_free(ne.formats); -} - -void -notify_client(const char *name, struct client *c) -{ - struct cmd_find_state fs; - - cmd_find_from_client(&fs, c, 0); - notify_add(name, &fs, c, NULL, NULL, NULL, NULL); -} - -void -notify_session(const char *name, struct session *s) -{ - struct cmd_find_state fs; - - if (session_alive(s)) - cmd_find_from_session(&fs, s, 0); - else - cmd_find_from_nothing(&fs, 0); - notify_add(name, &fs, NULL, s, NULL, NULL, NULL); -} - -void -notify_winlink(const char *name, struct winlink *wl) -{ - struct cmd_find_state fs; - - cmd_find_from_winlink(&fs, wl, 0); - notify_add(name, &fs, NULL, wl->session, wl->window, NULL, NULL); -} - -void -notify_session_window(const char *name, struct session *s, struct window *w) -{ - struct cmd_find_state fs; - - cmd_find_from_session_window(&fs, s, w, 0); - notify_add(name, &fs, NULL, s, w, NULL, NULL); -} - -void -notify_window(const char *name, struct window *w) -{ - struct cmd_find_state fs; - - cmd_find_from_window(&fs, w, 0); - notify_add(name, &fs, NULL, NULL, w, NULL, NULL); -} - -void -notify_pane(const char *name, struct window_pane *wp) -{ - struct cmd_find_state fs; - - cmd_find_from_pane(&fs, wp, 0); - notify_add(name, &fs, NULL, NULL, NULL, wp, NULL); -} - -void -notify_paste_buffer(const char *pbname, int deleted) -{ - struct cmd_find_state fs; - - cmd_find_clear_state(&fs, 0); - if (deleted) { - notify_add("paste-buffer-deleted", &fs, NULL, NULL, NULL, NULL, - pbname); - } else { - notify_add("paste-buffer-changed", &fs, NULL, NULL, NULL, NULL, - pbname); - } -} diff --git a/options-table.c b/options-table.c index b1ab705d5..082ed06ff 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: options-table.c,v 1.232 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1933,11 +1933,18 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("client-light-theme", ""), OPTIONS_TABLE_HOOK("client-dark-theme", ""), OPTIONS_TABLE_HOOK("command-error", ""), + OPTIONS_TABLE_HOOK("marked-pane-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-created", ""), OPTIONS_TABLE_PANE_HOOK("pane-died", ""), OPTIONS_TABLE_PANE_HOOK("pane-exited", ""), OPTIONS_TABLE_PANE_HOOK("pane-focus-in", ""), OPTIONS_TABLE_PANE_HOOK("pane-focus-out", ""), OPTIONS_TABLE_PANE_HOOK("pane-mode-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-mode-entered", ""), + OPTIONS_TABLE_PANE_HOOK("pane-mode-exited", ""), + OPTIONS_TABLE_PANE_HOOK("pane-prompt-closed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", ""), + OPTIONS_TABLE_PANE_HOOK("pane-resized", ""), OPTIONS_TABLE_PANE_HOOK("pane-set-clipboard", ""), OPTIONS_TABLE_PANE_HOOK("pane-title-changed", ""), OPTIONS_TABLE_HOOK("session-closed", ""), @@ -1949,6 +1956,8 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_WINDOW_HOOK("window-pane-changed", ""), OPTIONS_TABLE_WINDOW_HOOK("window-renamed", ""), OPTIONS_TABLE_WINDOW_HOOK("window-resized", ""), + OPTIONS_TABLE_WINDOW_HOOK("window-unzoomed", ""), + OPTIONS_TABLE_WINDOW_HOOK("window-zoomed", ""), OPTIONS_TABLE_HOOK("window-unlinked", ""), { .name = NULL } diff --git a/options.c b/options.c index 3b81ed57d..b5238bc69 100644 --- a/options.c +++ b/options.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: options.c,v 1.90 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -410,7 +410,7 @@ options_remove(struct options_entry *o) else options_value_free(o, &o->value); if (o->monitor_data != NULL) - notify_monitor_free(o->monitor_data); + hooks_monitor_free(o->monitor_data); RB_REMOVE(options_tree, &oo->tree, o); free((void *)o->name); free(o); diff --git a/paste.c b/paste.c index 6b8a1cb47..0b9069f92 100644 --- a/paste.c +++ b/paste.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: paste.c,v 1.53 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -44,6 +44,16 @@ static int paste_cmp_times(const struct paste_buffer *, const struct paste_buffer *); RB_GENERATE_STATIC(paste_time_tree, paste_buffer, time_entry, paste_cmp_times); +static void +paste_fire_event(const char *name, const char *pbname) +{ + struct event_payload *ep; + + ep = event_payload_create(); + event_payload_set_string(ep, "paste_buffer", "%s", pbname); + events_fire(name, ep); +} + static int paste_cmp_names(const struct paste_buffer *a, const struct paste_buffer *b) { @@ -138,7 +148,7 @@ paste_get_name(const char *name) void paste_free(struct paste_buffer *pb) { - notify_paste_buffer(pb->name, 1); + paste_fire_event("paste-buffer-deleted", pb->name); RB_REMOVE(paste_name_tree, &paste_by_name, pb); RB_REMOVE(paste_time_tree, &paste_by_time, pb); @@ -196,7 +206,7 @@ paste_add(const char *prefix, char *data, size_t size) RB_INSERT(paste_name_tree, &paste_by_name, pb); RB_INSERT(paste_time_tree, &paste_by_time, pb); - notify_paste_buffer(pb->name, 0); + paste_fire_event("paste-buffer-changed", pb->name); } /* Rename a paste buffer. */ @@ -254,8 +264,8 @@ paste_rename(const char *oldname, const char *newname, char **cause) RB_INSERT(paste_name_tree, &paste_by_name, pb); - notify_paste_buffer(oldname, 1); - notify_paste_buffer(pb->name, 0); + paste_fire_event("paste-buffer-deleted", oldname); + paste_fire_event("paste-buffer-changed", pb->name); return (0); } @@ -312,7 +322,7 @@ paste_set(char *data, size_t size, const char *name, char **cause) RB_INSERT(paste_name_tree, &paste_by_name, pb); RB_INSERT(paste_time_tree, &paste_by_time, pb); - notify_paste_buffer(pb->name, 0); + paste_fire_event("paste-buffer-changed", pb->name); return (0); } @@ -325,7 +335,7 @@ paste_replace(struct paste_buffer *pb, char *data, size_t size) pb->data = data; pb->size = size; - notify_paste_buffer(pb->name, 0); + paste_fire_event("paste-buffer-changed", pb->name); } /* Convert start of buffer into a nice string. */ diff --git a/popup.c b/popup.c index 913c6c527..c43b46d0a 100644 --- a/popup.c +++ b/popup.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: popup.c,v 1.75 2026/07/09 07:32:58 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott diff --git a/proc.c b/proc.c index 21a747525..194371236 100644 --- a/proc.c +++ b/proc.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: proc.c,v 1.31 2026/06/08 21:38:19 nicm Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott diff --git a/procname.c b/procname.c index 7c0872fd0..0ef484fb8 100644 --- a/procname.c +++ b/procname.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: procname.c,v 1.20 2022/02/22 17:35:01 deraadt Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/prompt-history.c b/prompt-history.c index ac99bdb95..ad1ab4047 100644 --- a/prompt-history.c +++ b/prompt-history.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: prompt-history.c,v 1.1 2026/06/25 11:39:11 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/prompt.c b/prompt.c index 1ab447ebe..6187a7a12 100644 --- a/prompt.c +++ b/prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: prompt.c,v 1.4 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/regsub.c b/regsub.c index 9e33a88ab..c49c72bb9 100644 --- a/regsub.c +++ b/regsub.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: regsub.c,v 1.10 2026/07/08 11:04:51 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/resize.c b/resize.c index 19a940d61..c8257cec0 100644 --- a/resize.c +++ b/resize.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: resize.c,v 1.56 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -60,8 +60,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) tty_update_window_offset(w); server_redraw_window(w); - notify_window("window-layout-changed", w); - notify_window("window-resized", w); + events_fire_window("window-layout-changed", w); + events_fire_window("window-resized", w); w->flags &= ~WINDOW_RESIZE; } diff --git a/screen-redraw.c b/screen-redraw.c index 70a830e6a..10a5acaa9 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: screen-redraw.c,v 1.150 2026/07/09 07:32:58 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/screen-write.c b/screen-write.c index 4d4f90900..a7a16149c 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: screen-write.c,v 1.283 2026/07/09 07:35:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/screen.c b/screen.c index 8b988adee..4c754360c 100644 --- a/screen.c +++ b/screen.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: screen.c,v 1.105 2026/06/29 18:17:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/server-acl.c b/server-acl.c index db95a3c6b..03e758af5 100644 --- a/server-acl.c +++ b/server-acl.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server-acl.c,v 1.3 2026/06/08 21:38:19 nicm Exp $ */ /* * Copyright (c) 2021 Holland Schutte, Jayson Morberg diff --git a/server-client.c b/server-client.c index 8344ae190..86360de20 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server-client.c,v 1.486 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -433,7 +433,7 @@ server_client_set_session(struct client *c, struct session *s) alerts_check_session(s); tty_update_client_offset(c); status_timer_start(c); - notify_client("client-session-changed", c); + events_fire_client("client-session-changed", c); server_redraw_client(c); } @@ -468,7 +468,7 @@ server_client_lost(struct client *c) if (c->flags & CLIENT_ATTACHED) { server_client_attached_lost(c); - notify_client("client-detached", c); + events_fire_client("client-detached", c); } if (c->flags & CLIENT_CONTROL) @@ -1288,7 +1288,7 @@ server_client_update_latest(struct client *c) if (options_get_number(w->options, "window-size") == WINDOW_SIZE_LATEST) recalculate_size(w, 0); - notify_client("client-active", c); + events_fire_client("client-active", c); } /* Get repeat time. */ @@ -2472,7 +2472,7 @@ server_client_dispatch(struct imsg *imsg, void *arg) c->overlay_resize(c, c->overlay_data); server_redraw_client(c); if (c->session != NULL) - notify_client("client-resized", c); + events_fire_client("client-resized", c); break; case MSG_EXITING: if (datalen != 0) @@ -3087,10 +3087,10 @@ server_client_report_theme(struct client *c, enum client_theme theme) if (theme == THEME_LIGHT) { c->theme = THEME_LIGHT; - notify_client("client-light-theme", c); + events_fire_client("client-light-theme", c); } else { c->theme = THEME_DARK; - notify_client("client-dark-theme", c); + events_fire_client("client-dark-theme", c); } /* diff --git a/server-fn.c b/server-fn.c index 7560e9da2..e3823dcbb 100644 --- a/server-fn.c +++ b/server-fn.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server-fn.c,v 1.147 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -30,6 +30,31 @@ #include "tmux.h" static void server_destroy_session_group(struct session *); +static void server_fire_pane_exit(const char *, struct window_pane *); + +static void +server_fire_pane_exit(const char *name, struct window_pane *wp) +{ + struct event_payload *ep; + struct cmd_find_state fs; + int status = wp->status; + const char *signame; + + if (WIFSIGNALED(status)) + signame = sig2name(WTERMSIG(status)); + + 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); + if (WIFEXITED(status)) + event_payload_set_int(ep, "exit_status", WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + event_payload_set_string(ep, "exit_signal", "%s", signame); + event_payload_set_int(ep, "exit_success", status == 0); + events_fire(name, ep); +} void server_redraw_client(struct client *c) @@ -203,6 +228,7 @@ server_kill_window(struct window *w, int renumber) struct session *s, *s1; struct winlink *wl; + window_add_ref(w, __func__); RB_FOREACH_SAFE(s, sessions, &sessions, s1) { if (!session_has(s, w)) continue; @@ -220,6 +246,7 @@ server_kill_window(struct window *w, int renumber) server_renumber_session(s); } recalculate_sizes(); + window_remove_ref(w, __func__); } void @@ -273,8 +300,7 @@ server_link_window(struct session *src, struct winlink *srcwl, * Can't use session_detach as it will destroy session * if this makes it empty. */ - notify_session_window("window-unlinked", dst, - dstwl->window); + events_fire_winlink("window-unlinked", dstwl); dstwl->flags &= ~WINLINK_ALERTFLAGS; winlink_stack_remove(&dst->lastw, dstwl); winlink_remove(&dst->windows, dstwl); @@ -348,7 +374,7 @@ server_destroy_pane(struct window_pane *wp, int notify) gettimeofday(&wp->dead_time, NULL); if (notify) - notify_pane("pane-died", wp); + server_fire_pane_exit("pane-died", wp); s = options_get_string(wp->options, "remain-on-exit-format"); if (*s != '\0') { @@ -371,7 +397,7 @@ server_destroy_pane(struct window_pane *wp, int notify) } if (notify) - notify_pane("pane-exited", wp); + server_fire_pane_exit("pane-exited", wp); server_unzoom_window(w); server_client_remove_pane(wp); diff --git a/server.c b/server.c index edabc8a79..6fe7f2c7b 100644 --- a/server.c +++ b/server.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server.c,v 1.214 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -212,6 +212,8 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base, TAILQ_INIT(&clients); RB_INIT(&sessions); key_bindings_init(); + control_build_events(); + hooks_build_events(); TAILQ_INIT(&message_log); gettimeofday(&start_time, NULL); diff --git a/session.c b/session.c index 41909d2a6..9c23c4b50 100644 --- a/session.c +++ b/session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: session.c,v 1.105 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -208,7 +208,7 @@ session_destroy(struct session *s, int notify, const char *from) RB_REMOVE(sessions, &sessions, s); if (notify) - notify_session("session-closed", s); + events_fire_session("session-closed", s); free(s->tio); @@ -221,7 +221,7 @@ session_destroy(struct session *s, int notify, const char *from) winlink_stack_remove(&s->lastw, TAILQ_FIRST(&s->lastw)); while (!RB_EMPTY(&s->windows)) { wl = RB_ROOT(&s->windows); - notify_session_window("window-unlinked", s, wl->window); + events_fire_winlink("window-unlinked", wl); winlink_remove(&s->windows, wl); } @@ -332,7 +332,7 @@ session_attach(struct session *s, struct window *w, int idx, char **cause) } wl->session = s; winlink_set_window(wl, w); - notify_session_window("window-linked", s, w); + events_fire_winlink("window-linked", wl); session_group_synchronize_from(s); return (wl); @@ -348,7 +348,7 @@ session_detach(struct session *s, struct winlink *wl) session_next(s, 0); wl->flags &= ~WINLINK_ALERTFLAGS; - notify_session_window("window-unlinked", s, wl->window); + events_fire_winlink("window-unlinked", wl); winlink_stack_remove(&s->lastw, wl); winlink_remove(&s->windows, wl); @@ -495,7 +495,7 @@ session_set_current(struct session *s, struct winlink *wl) winlink_clear_flags(wl); window_update_activity(wl->window); tty_update_window_offset(wl->window); - notify_session("session-window-changed", s); + events_fire_session("session-window-changed", s); return (0); } @@ -659,7 +659,7 @@ session_group_synchronize1(struct session *target, struct session *s) wl2 = winlink_add(&s->windows, wl->idx); wl2->session = s; winlink_set_window(wl2, wl->window); - notify_session_window("window-linked", s, wl2->window); + events_fire_winlink("window-linked", wl2); wl2->flags |= wl->flags & WINLINK_ALERTFLAGS; } @@ -685,7 +685,7 @@ session_group_synchronize1(struct session *target, struct session *s) wl = RB_ROOT(&old_windows); wl2 = winlink_find_by_window_id(&s->windows, wl->window->id); if (wl2 == NULL) - notify_session_window("window-unlinked", s, wl->window); + events_fire_winlink("window-unlinked", wl); winlink_remove(&old_windows, wl); } } diff --git a/sort.c b/sort.c index c416b8425..31f6e82f1 100644 --- a/sort.c +++ b/sort.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: sort.c,v 1.9 2026/06/29 07:45:09 nicm Exp $ */ /* * Copyright (c) 2026 Dane Jensen diff --git a/spawn.c b/spawn.c index 09eec948d..21fc25937 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: spawn.c,v 1.47 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -73,6 +73,45 @@ spawn_log(const char *from, struct spawn_context *sc) log_debug("%s: s=$%u %s idx=%d", from, s->id, tmp, sc->idx); } +static void +spawn_fire_pane_created(struct spawn_context *sc, struct window_pane *wp) +{ + struct event_payload *ep; + struct cmd_find_state fs; + char *cmd = NULL; + const char *cwd = wp->cwd; + + ep = event_payload_create(); + cmd_find_from_winlink_pane(&fs, sc->wl, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_session(ep, "session", sc->s); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_int(ep, "window_index", sc->wl->idx); + event_payload_set_pane(ep, "pane", wp); + + if (wp->argc != 0) + cmd = cmd_stringify_argv(wp->argc, wp->argv); + if (cmd != NULL && *cmd != '\0') + event_payload_set_string(ep, "pane_command", "%s", cmd); + else if (wp->shell != NULL) + event_payload_set_string(ep, "pane_command", "%s", wp->shell); + free(cmd); + + if (cwd != NULL) + event_payload_set_string(ep, "pane_current_path", "%s", cwd); + + if (sc->flags & SPAWN_EMPTY) + event_payload_set_int(ep, "created_empty", 1); + else + event_payload_set_int(ep, "created_empty", 0); + if (sc->flags & SPAWN_RESPAWN) + event_payload_set_int(ep, "created_respawn", 1); + else + event_payload_set_int(ep, "created_respawn", 0); + + events_fire("pane-created", ep); +} + struct winlink * spawn_window(struct spawn_context *sc, char **cause) { @@ -133,7 +172,7 @@ spawn_window(struct spawn_context *sc, char **cause) * if this makes it empty. */ wl->flags &= ~WINLINK_ALERTFLAGS; - notify_session_window("window-unlinked", s, wl->window); + events_fire_winlink("window-unlinked", wl); winlink_stack_remove(&s->lastw, wl); winlink_remove(&s->windows, wl); @@ -193,7 +232,7 @@ spawn_window(struct spawn_context *sc, char **cause) /* Fire notification if new window. */ if (~sc->flags & SPAWN_RESPAWN) - notify_session_window("window-linked", s, w); + events_fire_winlink("window-linked", sc->wl); session_group_synchronize_from(s); return (sc->wl); @@ -499,8 +538,8 @@ complete: sigprocmask(SIG_SETMASK, &oldset, NULL); window_pane_set_event(new_wp); - environ_free(child); + spawn_fire_pane_created(sc, new_wp); if (sc->flags & SPAWN_RESPAWN) return (new_wp); @@ -511,7 +550,7 @@ complete: window_set_active_pane(w, new_wp, 1); } if (~sc->flags & SPAWN_NONOTIFY) - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); return (new_wp); } diff --git a/status.c b/status.c index 8f7c209e1..2f3c8f11e 100644 --- a/status.c +++ b/status.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: status.c,v 1.273 2026/07/06 14:29:10 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/style.c b/style.c index 2ca5e37af..55d08fe75 100644 --- a/style.c +++ b/style.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: style.c,v 1.47 2026/06/29 17:08:52 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/tmux-protocol.h b/tmux-protocol.h index 3cf00c094..d823ce7e2 100644 --- a/tmux-protocol.h +++ b/tmux-protocol.h @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tmux-protocol.h,v 1.2 2023/01/06 07:09:27 nicm Exp $ */ /* * Copyright (c) 2021 Nicholas Marriott diff --git a/tmux.1 b/tmux.1 index 50d398b7c..215878dea 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD$ +.\" $OpenBSD: tmux.1,v 1.1131 2026/07/10 13:38:45 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$ +.Dd $Mdocdate: July 10 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -6323,11 +6323,7 @@ For example, this could be used to write to a log file: set\-hook \-g command\-error "run\-shell \\"echo 'a tmux command failed' >>/tmp/log\\"" .Ed .Pp -All the notifications listed in the -.Sx CONTROL MODE -section are hooks (without any arguments), except -.Ic %exit . -The following additional hooks are available: +The following hooks are available in addition to after hooks: .Bl -tag -width "XXXXXXXXXXXXXXXXXXXXXX" .It alert\-activity Run when a window has activity. @@ -6346,11 +6342,11 @@ Run when a client becomes the latest active client of its session. .It client\-attached Run when a client is attached. .It client\-detached -Run when a client is detached +Run when a client is detached. .It client\-focus\-in -Run when focus enters a client +Run when focus enters a client. .It client\-focus\-out -Run when focus exits a client +Run when focus exits a client. .It client\-resized Run when a client is resized. .It client\-session\-changed @@ -6361,6 +6357,10 @@ Run when a client switches to a light theme. Run when a client switches to a dark theme. .It command\-error Run when a command fails. +.It marked\-pane\-changed +Run when the marked pane is set or cleared. +.It pane\-created +Run when a pane is created or respawned. .It pane\-died Run when the program running in a pane exits, but .Ic remain\-on\-exit @@ -6375,20 +6375,38 @@ option is on. Run when the focus exits a pane, if the .Ic focus\-events option is on. +.It pane\-mode\-changed +Run when a pane changes mode. +.It pane\-mode\-entered +Run when a pane enters a mode. +.It pane\-mode\-exited +Run when a pane exits a mode. +.It pane\-prompt\-closed +Run when a prompt in a pane is closed. +.It pane\-prompt\-opened +Run when a prompt is opened in a pane. +.It pane\-resized +Run when a pane is resized. .It pane\-set\-clipboard Run when the terminal clipboard is set using the .Xr xterm 1 escape sequence. -.It session\-created -Run when a new session created. +.It pane\-title\-changed +Run when a pane title is changed. .It session\-closed -Run when a session closed. +Run when a session is closed. +.It session\-created +Run when a new session is created. .It session\-renamed Run when a session is renamed. +.It session\-window\-changed +Run when a session changes its active window. .It window\-layout\-changed Run when a window layout is changed. .It window\-linked Run when a window is linked into a session. +.It window\-pane\-changed +Run when a window changes its active pane. .It window\-renamed Run when a window is renamed. .It window\-resized @@ -6396,6 +6414,10 @@ Run when a window is resized. This may be after the .Ar client\-resized hook is run. +.It window\-unzoomed +Run when a window is unzoomed. +.It window\-zoomed +Run when a window is zoomed. .It window\-unlinked Run when a window is unlinked from a session. .El @@ -6403,13 +6425,15 @@ Run when a window is unlinked from a session. Hooks are managed with these commands: .Bl -tag -width Ds .It Xo Ic set\-hook -.Op Fl agpRuw +.Op Fl agpERuw .Op Fl B Ar name:what:format .Op Fl t Ar target\-pane .Ar hook\-name .Op Ar command .Xc Without +.Fl E +or .Fl R , sets (or with .Fl u @@ -6450,6 +6474,14 @@ the subscription named by is removed. .Pp With +.Fl E , +fire the user event named +.Ar hook\-name . +.Ar hook\-name +must begin with +.Ql @ . +.Pp +With .Fl R , run .Ar hook\-name @@ -8560,8 +8592,10 @@ or the current pane if omitted) after the command finishes. If the command fails, the exit status is also displayed. .Tg wait .It Xo Ic wait\-for -.Op Fl L | S | U -.Ar channel +.Op Fl ELSUlv +.Op Fl F Ar format +.Op Fl w Ar waiter +.Ar name .Xc .D1 Pq alias: Ic wait When used without options, prevents the client from exiting until woken using @@ -8574,6 +8608,41 @@ is used, the channel is locked and any clients that try to lock the same channel are made to wait until the channel is unlocked with .Ic wait\-for .Fl U . +.Pp +With +.Fl E , +.Nm +waits for the next event with +.Ar name . +Events include hook and notification names, and user +.Ql @ +events generated by +.Ic set-hook +.Fl E +or +.Ic set-hook +.Fl B . +If +.Fl F +is given, +.Ar format +must also be true. +If +.Fl v +is given, event payload keys are printed (whether or not +.Ar format +is true). +.Pp +.Fl l +list the waiters for +.Ar name +and +.Fl w +wakes +.Ar waiter +on +.Ar name +immediately. .El .Sh EXIT MESSAGES When a diff --git a/tmux.c b/tmux.c index c08110c95..8a90c3ff9 100644 --- a/tmux.c +++ b/tmux.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tmux.c,v 1.221 2026/06/29 18:17:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/tmux.h b/tmux.h index b153310cf..076f4bf7c 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tmux.h,v 1.1394 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -48,6 +48,9 @@ struct cmdq_state; struct cmds; struct control_state; struct environ; +struct event_payload; +struct event_payload_item; +struct events_sink; struct format_job_tree; struct format_tree; struct hyperlinks_uri; @@ -2302,6 +2305,23 @@ struct monitor_change { }; typedef void (*monitor_cb)(struct monitor_change *, void *); +/* Event payload type. */ +enum event_payload_type { + EVENT_PAYLOAD_STRING, + EVENT_PAYLOAD_TIME, + EVENT_PAYLOAD_INT, + EVENT_PAYLOAD_UINT, + EVENT_PAYLOAD_CLIENT, + EVENT_PAYLOAD_SESSION, + EVENT_PAYLOAD_WINDOW, + EVENT_PAYLOAD_PANE, + EVENT_PAYLOAD_POINTER +}; + +/* Event payload callbacks. */ +typedef void (*event_payload_free_cb)(void *); +typedef void (*event_payload_print_cb)(void *, struct evbuffer *); + /* Key binding and key table. */ struct key_binding { key_code key; @@ -2611,29 +2631,78 @@ char *format_grid_hyperlink(struct grid *, u_int, u_int, struct screen *); char *format_grid_line(struct grid *, u_int); -/* format-draw.c */ -void format_draw(struct screen_write_ctx *, - const struct grid_cell *, u_int, const char *, - struct style_ranges *, int); -u_int format_width(const char *); -char *format_trim_left(const char *, u_int); -char *format_trim_right(const char *, u_int); +/* events-payload.c */ +struct event_payload *event_payload_create(void); +void event_payload_free(struct event_payload *); +void printflike(2, 3) event_payload_log(struct event_payload *, const char *, + ...); +char *event_payload_item_print(struct event_payload_item *); +void event_payload_set_target(struct event_payload *, + struct cmd_find_state *); +int event_payload_get_target(struct event_payload *, + struct cmd_find_state *); +void printflike(3, 4) event_payload_set_string(struct event_payload *, + const char *, const char *, ...); +void event_payload_set_time(struct event_payload *, const char *, time_t); +void event_payload_set_int(struct event_payload *, const char *, int); +void event_payload_set_uint(struct event_payload *, const char *, u_int); +void event_payload_set_client(struct event_payload *, const char *, + struct client *); +void event_payload_set_session(struct event_payload *, const char *, + struct session *); +void event_payload_set_window(struct event_payload *, const char *, + struct window *); +void event_payload_set_pane(struct event_payload *, const char *, + struct window_pane *); +void event_payload_set_pointer(struct event_payload *, const char *, void *, + event_payload_free_cb, event_payload_print_cb); +const char *event_payload_get_string(struct event_payload *, const char *); +char *event_payload_print(struct event_payload *, const char *); +void event_payload_add_formats(struct event_payload *, + struct format_tree *, const char *); +struct event_payload_item *event_payload_first(struct event_payload *); +struct event_payload_item *event_payload_next(struct event_payload_item *); +const char *event_payload_item_name(struct event_payload_item *); +enum event_payload_type event_payload_item_type(struct event_payload_item *); +time_t event_payload_get_time(struct event_payload *, const char *); +int event_payload_get_int(struct event_payload *, const char *, int *); +int event_payload_get_uint(struct event_payload *, const char *, u_int *); +struct client *event_payload_get_client(struct event_payload *, const char *); +struct session *event_payload_get_session(struct event_payload *, const char *); +struct window *event_payload_get_window(struct event_payload *, const char *); +struct window_pane *event_payload_get_pane(struct event_payload *, + const char *); +void *event_payload_get_pointer(struct event_payload *, const char *); -/* notify.c */ -void notify_hook(struct cmdq_item *, const char *); -void notify_monitor_add(struct cmdq_item *, struct options *, +/* events.c */ +typedef void (*events_cb)(const char *, struct event_payload *, void *); +struct events_sink *events_add_sink(const char *, events_cb, void *); +void events_remove_sink(struct events_sink *); +void events_fire(const char *, struct event_payload *); +void events_fire_client(const char *, struct client *); +void events_fire_session(const char *, struct session *); +void events_fire_window(const char *, struct window *); +void events_fire_pane(const char *, struct window_pane *); +void events_fire_winlink(const char *, struct winlink *); + +/* format-draw.c */ +void format_draw(struct screen_write_ctx *, const struct grid_cell *, + u_int, const char *, struct style_ranges *, int); +u_int format_width(const char *); +char *format_trim_left(const char *, u_int); +char *format_trim_right(const char *, u_int); + +/* hooks.c */ +void hooks_add_event(const char *); +int hooks_valid_event_name(const char *); +void hooks_build_events(void); +void hooks_run(struct cmdq_item *, const char *); +void hooks_monitor_add(struct cmdq_item *, struct options *, const char *, enum monitor_type, int, const char *, struct cmd_find_state *, struct session *); -void notify_monitor_remove(struct options *, const char *); -void notify_monitor_free(void *); -char *notify_monitor_to_string(struct options_entry *); -void notify_client(const char *, struct client *); -void notify_session(const char *, struct session *); -void notify_winlink(const char *, struct winlink *); -void notify_session_window(const char *, struct session *, struct window *); -void notify_window(const char *, struct window *); -void notify_pane(const char *, struct window_pane *); -void notify_paste_buffer(const char *, int); +void hooks_monitor_remove(struct options *, const char *); +void hooks_monitor_free(void *); +char *hooks_monitor_to_string(struct options_entry *); /* options.c */ struct options *options_create(struct options *); @@ -3855,20 +3924,7 @@ void control_add_sub(struct client *, const char *, enum monitor_type, int, void control_remove_sub(struct client *, const char *); /* control-notify.c */ -void control_notify_pane_mode_changed(int); -void control_notify_window_layout_changed(struct window *); -void control_notify_window_pane_changed(struct window *); -void control_notify_window_unlinked(struct session *, struct window *); -void control_notify_window_linked(struct session *, struct window *); -void control_notify_window_renamed(struct window *); -void control_notify_client_session_changed(struct client *); -void control_notify_client_detached(struct client *); -void control_notify_session_renamed(struct session *); -void control_notify_session_created(struct session *); -void control_notify_session_closed(struct session *); -void control_notify_session_window_changed(struct session *); -void control_notify_paste_buffer_changed(const char *); -void control_notify_paste_buffer_deleted(const char *); +void control_build_events(void); /* session.c */ extern struct sessions sessions; diff --git a/tty-acs.c b/tty-acs.c index 3dab31b6f..eedb79c2f 100644 --- a/tty-acs.c +++ b/tty-acs.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-acs.c,v 1.13 2023/08/08 07:19:48 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott diff --git a/tty-draw.c b/tty-draw.c index 825d81e49..d47be66bc 100644 --- a/tty-draw.c +++ b/tty-draw.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-draw.c,v 1.14 2026/06/19 10:38:29 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/tty-features.c b/tty-features.c index ea6769dc6..614c63f62 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-features.c,v 1.40 2026/07/01 06:17:58 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott diff --git a/tty-keys.c b/tty-keys.c index d6a2610ea..61e541165 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-keys.c,v 1.210 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1008,10 +1008,10 @@ complete_key: if (key == KEYC_FOCUS_OUT) { c->flags &= ~CLIENT_FOCUSED; window_update_focus(c->session->curw->window); - notify_client("client-focus-out", c); + events_fire_client("client-focus-out", c); } else if (key == KEYC_FOCUS_IN) { c->flags |= CLIENT_FOCUSED; - notify_client("client-focus-in", c); + events_fire_client("client-focus-in", c); window_update_focus(c->session->curw->window); } diff --git a/tty-term.c b/tty-term.c index bea34fc42..c8ceb2e56 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-term.c,v 1.106 2026/06/13 09:17:29 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/tty.c b/tty.c index ce5012191..66ca0d6cf 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty.c,v 1.476 2026/06/26 11:36:22 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/utf8-combined.c b/utf8-combined.c index 923342b43..02abc3518 100644 --- a/utf8-combined.c +++ b/utf8-combined.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: utf8-combined.c,v 1.10 2026/06/09 11:49:36 nicm Exp $ */ /* * Copyright (c) 2023 Nicholas Marriott diff --git a/utf8.c b/utf8.c index 4d6ebcbd8..e634b43f1 100644 --- a/utf8.c +++ b/utf8.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: utf8.c,v 1.71 2026/05/12 09:37:25 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/window-border.c b/window-border.c index 5420f4b97..0419015f4 100644 --- a/window-border.c +++ b/window-border.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-border.c,v 1.1 2026/06/22 08:47:46 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/window-buffer.c b/window-buffer.c index 6798e2027..f3116f8da 100644 --- a/window-buffer.c +++ b/window-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-buffer.c,v 1.49 2026/06/26 08:10:49 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott diff --git a/window-client.c b/window-client.c index 5a8c71862..64e38cb81 100644 --- a/window-client.c +++ b/window-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-client.c,v 1.46 2026/07/04 22:09:06 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott diff --git a/window-clock.c b/window-clock.c index cf7040009..acd4382b3 100644 --- a/window-clock.c +++ b/window-clock.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-clock.c,v 1.34 2026/06/25 16:32:42 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/window-copy.c b/window-copy.c index 8b117be92..997cb8888 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-copy.c,v 1.413 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -5764,7 +5764,7 @@ window_copy_copy_buffer(struct window_mode_entry *wme, const char *prefix, screen_write_setselection(&ctx, "", buf, len); screen_write_stop(&ctx); wp->flags |= redraw; - notify_pane("pane-set-clipboard", wp); + events_fire_pane("pane-set-clipboard", wp); } if (set_paste) @@ -5849,7 +5849,7 @@ window_copy_append_selection(struct window_mode_entry *wme) screen_write_start_pane(&ctx, wp, NULL); screen_write_setselection(&ctx, "", buf, len); screen_write_stop(&ctx); - notify_pane("pane-set-clipboard", wp); + events_fire_pane("pane-set-clipboard", wp); } pb = paste_get_top(&bufname); diff --git a/window-customize.c b/window-customize.c index 09da37f2e..9f84540ed 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-customize.c,v 1.30 2026/07/06 14:40:57 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott diff --git a/window-switch.c b/window-switch.c index 99ea19985..d458f30af 100644 --- a/window-switch.c +++ b/window-switch.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-switch.c,v 1.1 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/window-tree.c b/window-tree.c index 2d15eb536..1b19602fe 100644 --- a/window-tree.c +++ b/window-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-tree.c,v 1.93 2026/07/04 22:09:06 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott diff --git a/window-visible.c b/window-visible.c index f46967ec7..360e4fc88 100644 --- a/window-visible.c +++ b/window-visible.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-visible.c,v 1.4 2026/06/29 19:03:34 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/window.c b/window.c index 109f002ff..198296da4 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window.c,v 1.359 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -75,8 +75,8 @@ static struct window_pane *window_pane_create(struct window *, u_int, u_int, static void window_pane_destroy(struct window_pane *); static void window_pane_free(struct window_pane *); static void window_pane_scrollbar_timer(int, short, void *); -static void window_pane_full_size_offset(struct window_pane *wp, - int *xoff, int *yoff, u_int *sx, u_int *sy); +static void window_pane_full_size_offset(struct window_pane *, int *, int *, + u_int *, u_int *); RB_GENERATE(windows, window, entry, window_cmp); RB_GENERATE(winlinks, winlink, entry, winlink_cmp); @@ -88,6 +88,7 @@ struct window_pane_prompt { status_prompt_input_cb inputcb; prompt_free_cb freecb; void *data; + enum prompt_type type; }; int @@ -96,6 +97,78 @@ window_cmp(struct window *w1, struct window *w2) return (w1->id - w2->id); } +static void +window_fire_renamed(struct window *w, const char *old_name) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_window(&fs, w, 0); + event_payload_set_target(ep, &fs); + event_payload_set_window(ep, "window", w); + event_payload_set_string(ep, "old_name", "%s", old_name); + event_payload_set_string(ep, "new_name", "%s", w->name); + events_fire("window-renamed", ep); +} + +static void +window_fire_pane_changed(struct window *w, struct window_pane *wp, + struct window_pane *lastwp) +{ + 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_window(ep, "window", w); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_pane(ep, "new_pane", wp); + if (lastwp != NULL) + event_payload_set_pane(ep, "old_pane", lastwp); + events_fire("window-pane-changed", ep); +} + +static void +window_fire_pane_mode_changed(const char *name, struct window_pane *wp, + const char *previous, const char *current, int entered) +{ + 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); + + if (current != NULL) + event_payload_set_string(ep, "current_mode", "%s", current); + if (previous != NULL) + event_payload_set_string(ep, "previous_mode", "%s", previous); + event_payload_set_int(ep, "mode_entered", entered); + + events_fire(name, ep); +} + +static void +window_fire_pane_prompt(const char *name, struct window_pane *wp, + enum prompt_type type) +{ + struct event_payload *ep; + struct cmd_find_state fs; + const char *type_string = prompt_type_string(type); + + 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, "prompt_type", "%s", type_string); + events_fire(name, ep); +} + int winlink_cmp(struct winlink *wl1, struct winlink *wl2) { @@ -446,13 +519,15 @@ window_pane_remove_ref(struct window_pane *wp, const char *from) void window_set_name(struct window *w, const char *new_name, int untrusted) { - char *name; + char *last, *name; name = clean_name(new_name, untrusted); if (name != NULL) { + last = xstrdup(w->name); free(w->name); w->name = name; - notify_window("window-renamed", w); + window_fire_renamed(w, last); + free(last); } } @@ -553,13 +628,13 @@ window_pane_update_focus(struct window_pane *wp) log_debug("%s: %%%u focus out", __func__, wp->id); if (wp->base.mode & MODE_FOCUSON) bufferevent_write(wp->event, "\033[O", 3); - notify_pane("pane-focus-out", wp); + events_fire_pane("pane-focus-out", wp); wp->flags &= ~PANE_FOCUSED; } else if (focused && (~wp->flags & PANE_FOCUSED)) { log_debug("%s: %%%u focus in", __func__, wp->id); if (wp->base.mode & MODE_FOCUSON) bufferevent_write(wp->event, "\033[I", 3); - notify_pane("pane-focus-in", wp); + events_fire_pane("pane-focus-in", wp); wp->flags |= PANE_FOCUSED; } else log_debug("%s: %%%u focus unchanged", __func__, wp->id); @@ -595,7 +670,7 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify) server_redraw_window(w); if (notify) - notify_window("window-pane-changed", w); + window_fire_pane_changed(w, w->active, lastwp); return (1); } @@ -785,7 +860,8 @@ window_zoom(struct window_pane *wp) w->saved_layout_root = w->layout_root; layout_init(w, wp); w->flags |= WINDOW_ZOOMED; - notify_window("window-layout-changed", w); + events_fire_window("window-zoomed", w); + events_fire_window("window-layout-changed", w); redraw_invalidate_scene(w); return (0); @@ -811,8 +887,10 @@ window_unzoom(struct window *w, int notify) } layout_fix_panes(w, NULL); - if (notify) - notify_window("window-layout-changed", w); + if (notify) { + events_fire_window("window-unzoomed", w); + events_fire_window("window-layout-changed", w); + } redraw_invalidate_scene(w); return (0); @@ -894,7 +972,7 @@ window_lost_pane(struct window *w, struct window_pane *wp) if (w->active != NULL) { window_pane_stack_remove(&w->last_panes, w->active); w->active->flags |= PANE_CHANGED; - notify_window("window-pane-changed", w); + events_fire_window("window-pane-changed", w); window_update_focus(w); } } @@ -1223,9 +1301,9 @@ window_pane_destroy(struct window_pane *wp) window_pane_wait_finish(wp); spawn_editor_finish(wp); - window_pane_clear_prompt(wp); RB_REMOVE(window_pane_tree, &all_window_panes, wp); wp->flags |= PANE_DESTROYED; + window_pane_clear_prompt(wp); window_pane_free_modes(wp); screen_write_clear_dirty(wp); @@ -1352,6 +1430,8 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) { struct window_mode_entry *wme; struct window_pane_resize *r; + struct event_payload *ep; + struct cmd_find_state fs; if (sx == wp->sx && sy == wp->sy) return; @@ -1374,6 +1454,17 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) wme = TAILQ_FIRST(&wp->modes); if (wme != NULL && wme->mode->resize != NULL) wme->mode->resize(wme, sx, sy); + + 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_uint(ep, "width", sx); + event_payload_set_uint(ep, "height", sy); + event_payload_set_uint(ep, "old_width", r->osx); + event_payload_set_uint(ep, "old_height", r->osy); + events_fire("pane-resized", ep); } int @@ -1383,9 +1474,13 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, { struct window_mode_entry *wme; struct window *w = wp->window; + const char *name = mode->name, *p = NULL; - if (!TAILQ_EMPTY(&wp->modes) && TAILQ_FIRST(&wp->modes)->mode == mode) - return (1); + if (!TAILQ_EMPTY(&wp->modes)) { + if (TAILQ_FIRST(&wp->modes)->mode == mode) + return (1); + p = TAILQ_FIRST(&wp->modes)->mode->name; + } TAILQ_FOREACH(wme, &wp->modes, entry) { if (wme->mode == mode) @@ -1411,7 +1506,9 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, server_redraw_window_borders(wp->window); server_status_window(wp->window); - notify_pane("pane-mode-changed", wp); + + window_fire_pane_mode_changed("pane-mode-entered", wp, p, name, 1); + window_fire_pane_mode_changed("pane-mode-changed", wp, p, name, 1); return (0); } @@ -1422,11 +1519,13 @@ window_pane_reset_mode(struct window_pane *wp) struct window_mode_entry *wme, *next; struct window *w = wp->window; int kill; + const char *name, *p; if (TAILQ_EMPTY(&wp->modes)) return; wme = TAILQ_FIRST(&wp->modes); + p = wme->mode->name; kill = wme->kill; TAILQ_REMOVE(&wp->modes, wme, entry); wme->mode->free(wme); @@ -1443,13 +1542,16 @@ window_pane_reset_mode(struct window_pane *wp) if (next->mode->resize != NULL) next->mode->resize(next, wp->sx, wp->sy); } + name = (next == NULL ? NULL : next->mode->name); wp->flags |= (PANE_REDRAW|PANE_REDRAWSCROLLBAR|PANE_CHANGED); layout_fix_panes(w, NULL); server_redraw_window_borders(wp->window); server_status_window(wp->window); - notify_pane("pane-mode-changed", wp); + + window_fire_pane_mode_changed("pane-mode-exited", wp, p, name, 0); + window_fire_pane_mode_changed("pane-mode-changed", wp, p, name, 0); if (kill) server_kill_pane(wp); @@ -1512,6 +1614,7 @@ window_pane_set_prompt(struct window_pane *wp, struct client *c, wpp->inputcb = inputcb; wpp->freecb = freecb; wpp->data = data; + wpp->type = type; memset(&pd, 0, sizeof pd); prompt_set_options(&pd, s); @@ -1529,19 +1632,28 @@ window_pane_set_prompt(struct window_pane *wp, struct client *c, wp->flags |= PANE_REDRAW; prompt_incremental_start(wp->prompt); + window_fire_pane_prompt("pane-prompt-opened", wp, type); } /* Close a pane prompt. */ void window_pane_clear_prompt(struct window_pane *wp) { - struct prompt *prompt = wp->prompt; + struct prompt *prompt = wp->prompt; + struct window_pane_prompt *wpp = wp->prompt_data; + enum prompt_type type = PROMPT_TYPE_INVALID; - if (prompt == NULL) - return; - wp->prompt = NULL; - prompt_free(prompt); - wp->flags |= PANE_REDRAW; + if (prompt != NULL) { + if (wpp != NULL) + type = wpp->type; + + wp->prompt = NULL; + prompt_free(prompt); + wp->flags |= PANE_REDRAW; + + if (~wp->flags & PANE_DESTROYED) + window_fire_pane_prompt("pane-prompt-closed", wp, type); + } } /* Does this pane have an open prompt? */ diff --git a/xmalloc.c b/xmalloc.c index 14e805ed9..b60b9dbee 100644 --- a/xmalloc.c +++ b/xmalloc.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: xmalloc.c,v 1.14 2026/06/18 10:56:22 nicm Exp $ */ /* * Author: Tatu Ylonen diff --git a/xmalloc.h b/xmalloc.h index 93a4155e2..c9257b438 100644 --- a/xmalloc.h +++ b/xmalloc.h @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: xmalloc.h,v 1.5 2026/06/18 10:56:22 nicm Exp $ */ /* * Author: Tatu Ylonen diff --git a/xterm-keys.c b/xterm-keys.c deleted file mode 100644 index b10c10db7..000000000 --- a/xterm-keys.c +++ /dev/null @@ -1,252 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2009 Nicholas Marriott - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include - -#include "tmux.h" - -/* - * xterm-style function keys append one of the following values before the last - * character: - * - * 2 Shift - * 3 Alt - * 4 Shift + Alt - * 5 Ctrl - * 6 Shift + Ctrl - * 7 Alt + Ctrl - * 8 Shift + Alt + Ctrl - * - * Rather than parsing them, just match against a table. - * - * There are three forms for F1-F4 (\\033O_P and \\033O1;_P and \\033[1;_P). - * We accept any but always output the latter (it comes first in the table). - */ - -static int xterm_keys_match(const char *, const char *, size_t, size_t *, - key_code *); -static int xterm_keys_modifiers(const char *, size_t, size_t *, - key_code *); - -struct xterm_keys_entry { - key_code key; - const char *template; -}; - -static const struct xterm_keys_entry xterm_keys_table[] = { - { KEYC_F1, "\033[1;_P" }, - { KEYC_F1, "\033O1;_P" }, - { KEYC_F1, "\033O_P" }, - { KEYC_F2, "\033[1;_Q" }, - { KEYC_F2, "\033O1;_Q" }, - { KEYC_F2, "\033O_Q" }, - { KEYC_F3, "\033[1;_R" }, - { KEYC_F3, "\033O1;_R" }, - { KEYC_F3, "\033O_R" }, - { KEYC_F4, "\033[1;_S" }, - { KEYC_F4, "\033O1;_S" }, - { KEYC_F4, "\033O_S" }, - { KEYC_F5, "\033[15;_~" }, - { KEYC_F6, "\033[17;_~" }, - { KEYC_F7, "\033[18;_~" }, - { KEYC_F8, "\033[19;_~" }, - { KEYC_F9, "\033[20;_~" }, - { KEYC_F10, "\033[21;_~" }, - { KEYC_F11, "\033[23;_~" }, - { KEYC_F12, "\033[24;_~" }, - { KEYC_UP, "\033[1;_A" }, - { KEYC_DOWN, "\033[1;_B" }, - { KEYC_RIGHT, "\033[1;_C" }, - { KEYC_LEFT, "\033[1;_D" }, - { KEYC_HOME, "\033[1;_H" }, - { KEYC_END, "\033[1;_F" }, - { KEYC_PPAGE, "\033[5;_~" }, - { KEYC_NPAGE, "\033[6;_~" }, - { KEYC_IC, "\033[2;_~" }, - { KEYC_DC, "\033[3;_~" }, - - { '!', "\033[27;_;33~" }, - { '#', "\033[27;_;35~" }, - { '(', "\033[27;_;40~" }, - { ')', "\033[27;_;41~" }, - { '+', "\033[27;_;43~" }, - { ',', "\033[27;_;44~" }, - { '-', "\033[27;_;45~" }, - { '.', "\033[27;_;46~" }, - { '0', "\033[27;_;48~" }, - { '1', "\033[27;_;49~" }, - { '2', "\033[27;_;50~" }, - { '3', "\033[27;_;51~" }, - { '4', "\033[27;_;52~" }, - { '5', "\033[27;_;53~" }, - { '6', "\033[27;_;54~" }, - { '7', "\033[27;_;55~" }, - { '8', "\033[27;_;56~" }, - { '9', "\033[27;_;57~" }, - { ':', "\033[27;_;58~" }, - { ';', "\033[27;_;59~" }, - { '<', "\033[27;_;60~" }, - { '=', "\033[27;_;61~" }, - { '>', "\033[27;_;62~" }, - { '?', "\033[27;_;63~" }, - { '\'', "\033[27;_;39~" }, - { '\r', "\033[27;_;13~" }, - { '\t', "\033[27;_;9~" }, -}; - -/* - * Match key against buffer, treating _ as a wildcard. Return -1 for no match, - * 0 for match, 1 if the end of the buffer is reached (need more data). - */ -static int -xterm_keys_match(const char *template, const char *buf, size_t len, - size_t *size, key_code *modifiers) -{ - size_t pos; - int retval; - - *modifiers = 0; - - if (len == 0) - return (0); - - pos = 0; - do { - if (*template == '_') { - retval = xterm_keys_modifiers(buf, len, &pos, - modifiers); - if (retval != 0) - return (retval); - continue; - } - if (buf[pos] != *template) - return (-1); - pos++; - } while (*++template != '\0' && pos != len); - - if (*template != '\0') /* partial */ - return (1); - - *size = pos; - return (0); -} - -/* Find modifiers from buffer. */ -static int -xterm_keys_modifiers(const char *buf, size_t len, size_t *pos, - key_code *modifiers) -{ - u_int flags; - - if (len - *pos < 2) - return (1); - - if (buf[*pos] < '0' || buf[*pos] > '9') - return (-1); - flags = buf[(*pos)++] - '0'; - if (buf[*pos] >= '0' && buf[*pos] <= '9') - flags = (flags * 10) + (buf[(*pos)++] - '0'); - flags -= 1; - - *modifiers = 0; - if (flags & 1) - *modifiers |= KEYC_SHIFT; - if (flags & 2) - *modifiers |= KEYC_ESCAPE; - if (flags & 4) - *modifiers |= KEYC_CTRL; - if (flags & 8) - *modifiers |= KEYC_ESCAPE; - return (0); -} - -/* - * Lookup key from a buffer against the table. Returns 0 for found (and the - * key), -1 for not found, 1 for partial match. - */ -int -xterm_keys_find(const char *buf, size_t len, size_t *size, key_code *key) -{ - const struct xterm_keys_entry *entry; - u_int i; - int matched; - key_code modifiers; - - for (i = 0; i < nitems(xterm_keys_table); i++) { - entry = &xterm_keys_table[i]; - - matched = xterm_keys_match(entry->template, buf, len, size, - &modifiers); - if (matched == -1) - continue; - if (matched == 0) - *key = (entry->key|modifiers|KEYC_XTERM); - return (matched); - } - return (-1); -} - -/* Lookup a key number from the table. */ -char * -xterm_keys_lookup(key_code key) -{ - const struct xterm_keys_entry *entry; - u_int i; - key_code modifiers; - char *out; - - modifiers = 1; - if (key & KEYC_SHIFT) - modifiers += 1; - if (key & KEYC_ESCAPE) - modifiers += 2; - if (key & KEYC_CTRL) - modifiers += 4; - - /* - * If the key has no modifiers, return NULL and let it fall through to - * the normal lookup. - */ - if (modifiers == 1) - return (NULL); - - /* - * If this has the escape modifier, but was not originally an xterm - * key, it may be a genuine escape + key. So don't pass it through as - * an xterm key or programs like vi may be confused. - */ - if ((key & (KEYC_ESCAPE|KEYC_XTERM)) == KEYC_ESCAPE) - return (NULL); - - /* Otherwise, find the key in the table. */ - key &= KEYC_MASK_KEY; - for (i = 0; i < nitems(xterm_keys_table); i++) { - entry = &xterm_keys_table[i]; - if (key == entry->key) - break; - } - if (i == nitems(xterm_keys_table)) - return (NULL); - - /* Copy the template and replace the modifier. */ - out = xstrdup(entry->template); - out[strcspn(out, "_")] = '0' + modifiers; - return (out); -} From ad662d39262b3511a7dd9129a4232c0ebfc5517a Mon Sep 17 00:00:00 2001 From: Ben Maurer Date: Fri, 10 Jul 2026 07:46:00 -0700 Subject: [PATCH 100/127] Add a regression test for output queued to exiting control clients. Checks that once a control client is marked for exit the server stops queueing notifications for it (PR 5357). The client's output is drained only slowly so a backlog holds the exit handshake open; the client is then detached and the window renamed, and the %window-renamed notifications must not appear in the client's output stream. --- regress/control-client-exit.sh | 111 +++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 regress/control-client-exit.sh diff --git a/regress/control-client-exit.sh b/regress/control-client-exit.sh new file mode 100644 index 000000000..ac08db5cc --- /dev/null +++ b/regress/control-client-exit.sh @@ -0,0 +1,111 @@ +#!/bin/sh + +# Once a control client is marked for exit (by detach-client here, or by the +# "too far behind" eviction in control_check_age), the server must stop +# queueing new output and notifications for it. The exit handshake only waits +# for already-queued blocks to drain (control_all_done), so anything queued +# after that point is written to a client that will never read it, and can hold +# the handshake - and the pane buffer it pins - open. This checks that a +# notification generated while the client is exiting never reaches its output. +# +# To see this in seconds rather than waiting out CONTROL_MAXIMUM_AGE (300s) the +# control client's output goes down a fifo that a helper drains only slowly. +# The slow drain keeps a backlog stuck in the server so the exit handshake +# cannot complete; while it is stuck we detach the client (which sets the exit +# flag) and rename the window to a marker. Draining the fifo must then not turn +# up the marker as a %window-renamed line. + +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 +OUT=$DIR/out +MARKER=exitmarker +READER= + +mkfifo "$FIFO" || exit 1 +: >"$OUT" + +cleanup() { + [ -n "$READER" ] && kill "$READER" 2>/dev/null + $TMUX kill-server 2>/dev/null + exec 8<&- 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +# A detached session whose pane floods printable output forever. +$TMUX -f/dev/null new -d -x 80 -y 24 -s rt 'cat /dev/zero | tr "\000" x' || exit 1 +$TMUX setw -g automatic-rename off || exit 1 + +# Attach a control client: output to the fifo, stdin held open by sleep so it +# stays attached while we drive the session from outside. Open the read end +# after the client has opened the write end. +( sleep 30 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 & +exec 8<"$FIFO" + +n=0 +while [ $n -lt 50 ]; do + $TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . && break + sleep 0.1 + n=$((n + 1)) +done +$TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . || + { echo "control client did not attach"; exit 1; } + +# Drain the control stream slowly (~2.5KB/s). The pane floods far faster, so a +# backlog stays stuck in the server and the fifo stays full, which is what +# keeps the exit handshake from completing below. +( while :; do + dd bs=256 count=1 <&8 >>"$OUT" 2>/dev/null || exit 0 + sleep 0.1 +done ) & +READER=$! + +# Wait until the client is demonstrably backed up: the output only keeps +# growing at the slow-drain rate while the flood is outrunning the reader. +n=0 +while [ $n -lt 100 ]; do + [ "$(wc -c <"$OUT")" -ge 8000 ] && break + sleep 0.1 + n=$((n + 1)) +done +[ "$(wc -c <"$OUT")" -ge 8000 ] || { echo "client output did not back up"; exit 1; } + +# Detach the control client: this sets CLIENT_EXIT immediately. The stuck +# backlog holds the window between CLIENT_EXIT and the exit handshake open. +$TMUX detach-client -s rt + +# While the client is exiting, rename the window to the marker. Vanilla queues +# these %window-renamed notifications for the exiting client; the fix drops them. +i=0 +while [ $i -lt 5 ]; do + $TMUX rename-window -t rt:0 "$MARKER$i" + i=$((i + 1)) +done +sleep 0.5 + +# Drain fast (bounded) so the backlog, and any notification queued behind it, +# reaches OUT. A watchdog keeps this from blocking. +kill "$READER" 2>/dev/null +READER= +( dd bs=4096 count=128 <&8 >>"$OUT" 2>/dev/null ) & +DRAIN=$! +( sleep 3; kill "$DRAIN" 2>/dev/null ) & +WATCHDOG=$! +wait "$DRAIN" 2>/dev/null +kill "$WATCHDOG" 2>/dev/null + +# The marker must never appear as a notification to the exiting client. +if grep -q "%window-renamed .*$MARKER" "$OUT"; then + echo "notification queued to exiting control client:" + grep "%window-renamed .*$MARKER" "$OUT" + exit 1 +fi + +exit 0 From f3c6b4f1a35026c38b59d5db7021c5c4f44e32d3 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 10 Jul 2026 15:20:06 +0000 Subject: [PATCH 101/127] Add formats and events for OSC 133 commmands, as well as a -T flag to set-hook -B to only fire when the format is true. --- cmd-set-option.c | 70 ++++++- format.c | 121 +++++++++++++ hooks.c | 461 +++++++++++++++++++++++++++++++++++++++++++++++ input.c | 110 +++++++++-- monitor.c | 13 +- options-table.c | 12 ++ tmux.1 | 122 +++++++++++-- tmux.h | 136 ++++++++++---- window.c | 157 +++++++++++++--- 9 files changed, 1108 insertions(+), 94 deletions(-) create mode 100644 hooks.c diff --git a/cmd-set-option.c b/cmd-set-option.c index 1b6de1e86..1d95bddc9 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -31,6 +31,8 @@ static enum args_parse_type cmd_set_option_args_parse(struct args *, u_int, char **); static enum cmd_retval cmd_set_option_exec(struct cmd *, struct cmdq_item *); +static enum cmd_retval cmd_set_hook_event_exec(struct cmd *, + struct cmdq_item *); static enum cmd_retval cmd_set_hook_monitor_exec(struct cmdq_item *, struct args *, int); @@ -64,8 +66,8 @@ const struct cmd_entry cmd_set_hook_entry = { .name = "set-hook", .alias = NULL, - .args = { "agpRt:uB:w", 0, 2, cmd_set_option_args_parse }, - .usage = "[-agpRuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " + .args = { "agpERTt:uB:w", 0, 2, cmd_set_option_args_parse }, + .usage = "[-agpERTuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " "[hook] [command]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -85,17 +87,63 @@ cmd_set_option_args_parse(struct args *args, u_int idx, return (ARGS_PARSE_STRING); } +static enum cmd_retval +cmd_set_hook_event_exec(struct cmd *self, struct cmdq_item *item) +{ + struct args *args = cmd_get_args(self); + struct cmd_find_state *target = cmdq_get_target(item); + struct event_payload *ep; + struct client *c; + char *argument; + + if (args_count(args) == 0) { + cmdq_error(item, "missing argument"); + return (CMD_RETURN_ERROR); + } + if (args_count(args) != 1) { + cmdq_error(item, "too many arguments"); + return (CMD_RETURN_ERROR); + } + + argument = format_single_from_target(item, args_string(args, 0)); + if (*argument != '@') { + cmdq_error(item, "event name must start with @"); + free(argument); + return (CMD_RETURN_ERROR); + } + + ep = event_payload_create(); + event_payload_set_target(ep, target); + c = cmdq_get_client(item); + if (c != NULL) + event_payload_set_client(ep, "client", c); + if (target->s != NULL) + event_payload_set_session(ep, "session", target->s); + if (target->w != NULL) + event_payload_set_window(ep, "window", target->w); + if (target->wl != NULL) + event_payload_set_int(ep, "window_index", target->wl->idx); + else if (target->idx != -1) + event_payload_set_int(ep, "window_index", target->idx); + if (target->wp != NULL) + event_payload_set_pane(ep, "pane", target->wp); + events_fire(argument, ep); + free(argument); + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) { struct cmd_find_state *target = cmdq_get_target(item), fs; struct options *oo; struct options_entry *o; + struct session *s = NULL; char *cause = NULL, *name = NULL, *format = NULL; char *expanded = NULL, *newvalue = NULL; const char *value, *old; enum monitor_type type; - int id, scope; + int id, scope, flags = 0; if (args_count(args) > 1) { cmdq_error(item, "too many arguments"); @@ -130,7 +178,7 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) cmd_find_copy_state(&fs, target); if (args_has(args, 'u')) { - notify_monitor_remove(oo, name); + hooks_monitor_remove(oo, name); goto out; } @@ -152,7 +200,13 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) } } - notify_monitor_add(item, oo, name, type, id, format, &fs, target->s); + if (oo != global_options && + oo != global_s_options && + oo != global_w_options) + s = target->s; + if (args_has(args, 'T')) + flags |= MONITOR_NOTIFY_TRUE; + hooks_monitor_add(item, oo, name, type, id, format, flags, &fs, s); out: free(newvalue); @@ -185,6 +239,8 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) int scope; window = (cmd_get_entry(self) == &cmd_set_window_option_entry); + if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'E')) + return (cmd_set_hook_event_exec(self, item)); if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'B')) return (cmd_set_hook_monitor_exec(item, args, window)); if (args_count(args) == 0) { @@ -197,7 +253,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) /* If set-hook -R, fire the hook straight away. */ if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'R')) { - notify_hook(item, argument); + hooks_run(item, argument); free(argument); return (CMD_RETURN_NORMAL); } @@ -289,6 +345,8 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) goto fail; } options_set_string(oo, name, append, "%s", value); + if (cmd_get_entry(self) == &cmd_set_hook_entry) + hooks_add_event(name); } else if (array_key == NULL && !options_is_array(parent)) { error = options_from_string(oo, options_table_entry(parent), options_table_entry(parent)->name, value, diff --git a/format.c b/format.c index 7ec4f06b9..2cfe382fc 100644 --- a/format.c +++ b/format.c @@ -2173,6 +2173,106 @@ format_cb_pane_dead_time(struct format_tree *ft) return (NULL); } +/* Callback for pane_last_output_time. */ +static void * +format_cb_pane_last_output_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->last_output_time != 0) { + tv.tv_sec = wp->last_output_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_last_prompt_time. */ +static void * +format_cb_pane_last_prompt_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->last_prompt_time != 0) { + tv.tv_sec = wp->last_prompt_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_start_time. */ +static void * +format_cb_pane_command_start_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->cmd_start_time != 0) { + tv.tv_sec = wp->cmd_start_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_end_time. */ +static void * +format_cb_pane_command_end_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->cmd_end_time != 0) { + tv.tv_sec = wp->cmd_end_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_running. */ +static void * +format_cb_pane_command_running(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%d", !!(wp->flags & PANE_CMDRUNNING))); + return (NULL); +} + +/* Callback for pane_command_duration. */ +static void * +format_cb_pane_command_duration(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + time_t end; + + if (wp == NULL || wp->cmd_start_time == 0) + return (NULL); + if (wp->flags & PANE_CMDRUNNING) + end = time(NULL); + else + end = wp->cmd_end_time; + if (end < wp->cmd_start_time) + end = wp->cmd_start_time; + return (format_printf("%lld", (long long)(end - wp->cmd_start_time))); +} + +/* Callback for pane_command_status. */ +static void * +format_cb_pane_command_status(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL && wp->cmd_status != -1) + return (format_printf("%d", wp->cmd_status)); + return (NULL); +} + /* Callback for pane_format. */ static void * format_cb_pane_format(struct format_tree *ft) @@ -3485,6 +3585,21 @@ static const struct format_table_entry format_table[] = { { "pane_bottom", FORMAT_TABLE_STRING, format_cb_pane_bottom }, + { "pane_command_duration", FORMAT_TABLE_STRING, + format_cb_pane_command_duration + }, + { "pane_command_end_time", FORMAT_TABLE_TIME, + format_cb_pane_command_end_time + }, + { "pane_command_running", FORMAT_TABLE_STRING, + format_cb_pane_command_running + }, + { "pane_command_start_time", FORMAT_TABLE_TIME, + format_cb_pane_command_start_time + }, + { "pane_command_status", FORMAT_TABLE_STRING, + format_cb_pane_command_status + }, { "pane_current_command", FORMAT_TABLE_STRING, format_cb_current_command }, @@ -3536,6 +3651,12 @@ static const struct format_table_entry format_table[] = { { "pane_last", FORMAT_TABLE_STRING, format_cb_pane_last }, + { "pane_last_output_time", FORMAT_TABLE_TIME, + format_cb_pane_last_output_time + }, + { "pane_last_prompt_time", FORMAT_TABLE_TIME, + format_cb_pane_last_prompt_time + }, { "pane_left", FORMAT_TABLE_STRING, format_cb_pane_left }, diff --git a/hooks.c b/hooks.c new file mode 100644 index 000000000..097b768b4 --- /dev/null +++ b/hooks.c @@ -0,0 +1,461 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * Copyright (c) 2012 George Nachman + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include "tmux.h" + +/* Hook monitor state owned by an option entry. */ +struct hook_monitor { + struct options *oo; + + struct monitor_set *set; + struct events_sink *sink; + struct cmd_find_state fs; + + enum monitor_type type; + int id; + char *format; +}; + +/* Hook command data built from an event payload. */ +struct hooks_data { + const char *name; + struct cmd_find_state fs; + struct format_tree *formats; + struct options *oo; + struct client *client; + int expand; +}; + +/* Hook event sink registered for a notify event name. */ +struct hooks_event { + char *name; + struct events_sink *sink; + TAILQ_ENTRY(hooks_event) entry; +}; +TAILQ_HEAD(hooks_events, hooks_event); +static struct hooks_events hooks_events = TAILQ_HEAD_INITIALIZER(hooks_events); + +/* Insert one hook command list. */ +static struct cmdq_item * +hooks_insert_one(struct cmdq_item *item, struct hooks_data *hd, + struct cmd_list *cmdlist, struct cmdq_state *state) +{ + struct cmdq_item *new_item; + char *s; + + if (cmdlist == NULL) + return (item); + if (log_get_level() != 0) { + s = cmd_list_print(cmdlist, 0); + log_debug("%s: hook %s is: %s", __func__, hd->name, s); + free(s); + } + new_item = cmdq_get_command(cmdlist, state); + if (item != NULL) + return (cmdq_insert_after(item, new_item)); + return (cmdq_append(NULL, new_item)); +} + +/* Parse a hook command. */ +static struct cmd_parse_result * +hooks_parse(struct hooks_data *hd, struct cmd_find_state *fs, + const char *value) +{ + struct cmd_parse_result *pr; + struct format_tree *ft; + char *expanded; + + if (!hd->expand) + return (cmd_parse_from_string(value, NULL)); + + ft = format_create_defaults(NULL, hd->client, fs->s, fs->wl, fs->wp); + if (hd->formats != NULL) + format_merge(ft, hd->formats); + expanded = format_expand(ft, value); + format_free(ft); + + pr = cmd_parse_from_string(expanded, NULL); + free(expanded); + return (pr); +} + +/* Insert commands for a hook. */ +static void +hooks_insert(struct cmdq_item *item, struct hooks_data *hd) +{ + struct cmd_find_state fs; + struct options *oo; + struct cmdq_state *state; + struct options_entry *o; + struct options_array_item *a; + struct cmd_list *cmdlist; + const char *value; + struct cmd_parse_result *pr; + + log_debug("%s: inserting hook %s", __func__, hd->name); + + cmd_find_clear_state(&fs, 0); + if (cmd_find_empty_state(&hd->fs) || !cmd_find_valid_state(&hd->fs)) + cmd_find_from_nothing(&fs, 0); + else + cmd_find_copy_state(&fs, &hd->fs); + + if (hd->oo != NULL) { + oo = hd->oo; + o = options_get_only(oo, hd->name); + } else { + if (fs.s == NULL) + oo = global_s_options; + else + oo = fs.s->options; + o = options_get(oo, hd->name); + if (o == NULL && fs.wp != NULL) { + oo = fs.wp->options; + o = options_get(oo, hd->name); + } + if (o == NULL && fs.wl != NULL) { + oo = fs.wl->window->options; + o = options_get(oo, hd->name); + } + } + if (o == NULL) { + log_debug("%s: hook %s not found", __func__, hd->name); + return; + } + + if (item == NULL) + state = cmdq_new_state(&fs, NULL, CMDQ_STATE_NOHOOKS); + else { + state = cmdq_new_state(&fs, cmdq_get_event(item), + CMDQ_STATE_NOHOOKS); + } + cmdq_add_formats(state, hd->formats); + + if (*hd->name == '@') { + value = options_get_string(oo, hd->name); + pr = hooks_parse(hd, &fs, value); + switch (pr->status) { + case CMD_PARSE_ERROR: + log_debug("%s: can't parse hook %s: %s", __func__, + hd->name, pr->error); + free(pr->error); + break; + case CMD_PARSE_SUCCESS: + hooks_insert_one(item, hd, pr->cmdlist, state); + break; + } + } else { + a = options_array_first(o); + while (a != NULL) { + if (hd->expand) { + value = options_array_item_value(a)->string; + pr = hooks_parse(hd, &fs, value); + switch (pr->status) { + case CMD_PARSE_ERROR: + if (pr->error != NULL) { + cmdq_error(item, "%s", + pr->error); + } + break; + case CMD_PARSE_SUCCESS: + item = hooks_insert_one(item, hd, + pr->cmdlist, state); + break; + } + } else { + cmdlist = options_array_item_value(a)->cmdlist; + item = hooks_insert_one(item, hd, cmdlist, + state); + } + a = options_array_next(a); + } + } + + cmdq_free_state(state); +} + +/* Insert commands for a hook event. */ +static void +hooks_insert_event(struct cmdq_item *item, const char *name, + struct event_payload *ep, struct options *oo, int expand) +{ + struct hooks_data hd; + struct format_tree *ft; + struct client *c; + + if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) + return; + + c = event_payload_get_client(ep, "client"); + ft = format_create(c, item, FORMAT_NONE, FORMAT_NOJOBS); + event_payload_add_formats(ep, ft, "hook_"); + format_add(ft, "hook", "%s", name); + format_log_debug(ft, __func__); + + memset(&hd, 0, sizeof hd); + hd.name = name; + cmd_find_clear_state(&hd.fs, 0); + event_payload_get_target(ep, &hd.fs); + hd.formats = ft; + hd.oo = oo; + hd.client = c; + hd.expand = expand; + + hooks_insert(item, &hd); + format_free(ft); +} + +/* Handle an event for hooks. */ +static void +hooks_event_cb(const char *name, struct event_payload *ep, + __unused void *sink_data) +{ + struct cmdq_item *item; + + if (event_payload_get_pointer(ep, "_hook_monitor") != NULL) + return; + + item = event_payload_get_pointer(ep, "_cmdq_item"); + if (item != NULL) { + hooks_insert_event(item, name, ep, NULL, 0); + return; + } + + item = cmdq_running(NULL); + if (item == NULL || (~cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) + hooks_insert_event(NULL, name, ep, NULL, 0); +} + +/* Add a hook event sink. */ +void +hooks_add_event(const char *name) +{ + struct hooks_event *he; + + TAILQ_FOREACH(he, &hooks_events, entry) { + if (strcmp(he->name, name) == 0) + return; + } + + he = xcalloc(1, sizeof *he); + he->name = xstrdup(name); + he->sink = events_add_sink(name, hooks_event_cb, NULL); + TAILQ_INSERT_TAIL(&hooks_events, he, entry); +} + +/* Return if an event name can be fired through the hooks path. */ +int +hooks_valid_event_name(const char *name) +{ + const struct options_table_entry *oe; + + if (*name == '@') + return (1); + oe = options_search(name); + return (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)); +} + +/* Add hook event sinks for all built-in hooks. */ +void +hooks_build_events(void) +{ + const struct options_table_entry *oe; + + for (oe = options_table; oe->name != NULL; oe++) { + if (oe->flags & OPTIONS_TABLE_IS_HOOK) + hooks_add_event(oe->name); + } +} + +/* Run a hook immediately. */ +void +hooks_run(struct cmdq_item *item, const char *name) +{ + struct cmd_find_state *target = cmdq_get_target(item); + struct hooks_data hd = { 0 }; + + hd.name = name; + cmd_find_copy_state(&hd.fs, target); + hd.client = cmdq_get_client(item); + + hd.formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS); + format_add(hd.formats, "hook", "%s", name); + format_log_debug(hd.formats, __func__); + + hooks_insert(item, &hd); + format_free(hd.formats); +} + +/* Free a hook monitor. */ +void +hooks_monitor_free(void *data) +{ + struct hook_monitor *hm = data; + + events_remove_sink(hm->sink); + monitor_destroy(hm->set); + free(hm->format); + free(hm); +} + +/* Remove a hook monitor. */ +void +hooks_monitor_remove(struct options *oo, const char *name) +{ + struct options_entry *o; + struct hook_monitor *hm; + + o = options_get_only(oo, name); + if (o == NULL) + return; + + hm = options_get_monitor_data(o); + if (hm != NULL) { + options_set_monitor_data(o, NULL); + hooks_monitor_free(hm); + } +} + +/* Handle a hook monitor event. */ +static void +hooks_monitor_hook_cb(const char *name, struct event_payload *ep, + void *sink_data) +{ + struct hook_monitor *hm = sink_data; + + if (event_payload_get_pointer(ep, "_hook_monitor") == hm) + hooks_insert_event(cmdq_running(NULL), name, ep, hm->oo, 1); +} + +/* Fire a hook monitor event. */ +static void +hooks_monitor_cb(struct monitor_change *change, void *data) +{ + struct hook_monitor *hm = data; + struct event_payload *ep; + struct winlink *wl = change->wl; + struct window_pane *wp = change->wp; + struct cmd_find_state fs; + + ep = event_payload_create(); + event_payload_set_pointer(ep, "_hook_monitor", data, NULL, NULL); + + cmd_find_clear_state(&fs, 0); + if (wl != NULL && wp != NULL && wp->window == wl->window) + cmd_find_from_winlink_pane(&fs, wl, wp, 0); + else if (wl != NULL) + cmd_find_from_winlink(&fs, wl, 0); + else if (wp != NULL) + cmd_find_from_pane(&fs, wp, 0); + else if (change->s != NULL) + cmd_find_from_session(&fs, change->s, 0); + else + cmd_find_copy_state(&fs, &hm->fs); + event_payload_set_target(ep, &fs); + + if (change->value != NULL) + event_payload_set_string(ep, "value", "%s", change->value); + else + event_payload_set_string(ep, "value", "%s", ""); + if (change->last != NULL) + event_payload_set_string(ep, "last", "%s", change->last); + else + event_payload_set_string(ep, "last", "%s", ""); + + if (change->c != NULL) + event_payload_set_client(ep, "client", change->c); + if (change->s != NULL) + event_payload_set_session(ep, "session", change->s); + if (wl != NULL) { + if (change->s == NULL) + event_payload_set_session(ep, "session", + wl->session); + event_payload_set_window(ep, "window", wl->window); + event_payload_set_int(ep, "window_index", wl->idx); + } + if (wp != NULL) { + event_payload_set_pane(ep, "pane", wp); + if (wl == NULL) + event_payload_set_window(ep, "window", wp->window); + } + + events_fire(change->name, ep); +} + +/* Add a hook monitor. */ +void +hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, + const char *name, enum monitor_type type, int id, const char *format, + int flags, struct cmd_find_state *fs, struct session *s) +{ + struct options_entry *o; + struct hook_monitor *hm; + + hooks_monitor_remove(oo, name); + o = options_get_only(oo, name); + if (o == NULL) + o = options_set_string(oo, name, 0, "%s", ""); + + hm = xcalloc(1, sizeof *hm); + hm->oo = oo; + cmd_find_copy_state(&hm->fs, fs); + hm->type = type; + hm->id = id; + hm->format = xstrdup(format); + hm->set = monitor_create_session(s, hooks_monitor_cb, hm); + hm->sink = events_add_sink(name, hooks_monitor_hook_cb, hm); + options_set_monitor_data(o, hm); + monitor_add(hm->set, name, type, id, format, flags); +} + +/* Convert a hook monitor to its value. */ +char * +hooks_monitor_to_string(struct options_entry *o) +{ + struct hook_monitor *hm = options_get_monitor_data(o); + const char *name = options_name(o); + char *s; + + if (hm == NULL) + return (NULL); + + switch (hm->type) { + case MONITOR_SESSION: + xasprintf(&s, "%s::%s", name, hm->format); + break; + case MONITOR_PANE: + xasprintf(&s, "%s:%%%d:%s", name, hm->id, hm->format); + break; + case MONITOR_ALL_PANES: + xasprintf(&s, "%s:%%*:%s", name, hm->format); + break; + case MONITOR_WINDOW: + xasprintf(&s, "%s:@%d:%s", name, hm->id, hm->format); + break; + case MONITOR_ALL_WINDOWS: + xasprintf(&s, "%s:@*:%s", name, hm->format); + break; + } + return (s); +} diff --git a/input.c b/input.c index 2438ea504..4c33de2eb 100644 --- a/input.c +++ b/input.c @@ -174,6 +174,8 @@ 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 *); @@ -210,6 +212,21 @@ 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; @@ -1017,6 +1034,8 @@ input_parse_pane(struct window_pane *wp) size_t new_size; new_data = window_pane_get_new_data(wp, &wp->offset, &new_size); + if (new_size != 0) + wp->last_output_time = time(NULL); input_parse_buffer(wp, new_data, new_size); window_pane_update_used_data(wp, &wp->offset, new_size); } @@ -2161,7 +2180,7 @@ input_csi_dispatch_winops(struct input_ctx *ictx) screen_pop_title(sctx->s); if (wp == NULL) break; - notify_pane("pane-title-changed", wp); + events_fire_pane("pane-title-changed", wp); server_redraw_window_borders(w); server_status_window(w); break; @@ -2663,7 +2682,7 @@ input_exit_osc(struct input_ctx *ictx) if (wp != NULL && options_get_number(wp->options, "allow-set-title") && screen_set_title(sctx->s, p, 1)) { - notify_pane("pane-title-changed", wp); + input_fire_pane_title_changed(wp, p); server_redraw_window_borders(wp->window); server_status_window(wp->window); } @@ -2741,7 +2760,7 @@ input_exit_apc(struct input_ctx *ictx) if (wp != NULL && options_get_number(wp->options, "allow-set-title") && screen_set_title(sctx->s, ictx->input_buf, 1)) { - notify_pane("pane-title-changed", wp); + input_fire_pane_title_changed(wp, ictx->input_buf); server_redraw_window_borders(wp->window); server_status_window(wp->window); } @@ -3109,7 +3128,8 @@ input_osc_12(struct input_ctx *ictx, const char *p) c = ictx->ctx.s->ccolour; if (c == -1) c = ictx->ctx.s->default_ccolour; - input_osc_colour_reply(ictx, 1, 12, 0, c, ictx->input_end); + input_osc_colour_reply(ictx, 1, 12, 0, c, + ictx->input_end); } return; } @@ -3129,24 +3149,92 @@ input_osc_112(struct input_ctx *ictx, const char *p) screen_set_cursor_colour(ictx->ctx.s, -1); } +/* Fire an OSC 133 command event. */ +static void +input_fire_command_event(struct window_pane *wp, const char *name) +{ + struct event_payload *ep; + struct cmd_find_state fs; + time_t tstart = wp->cmd_start_time, end; + time_t tend = wp->cmd_end_time; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + if (fs.s != NULL) + event_payload_set_session(ep, "session", fs.s); + if (fs.wl != NULL) + event_payload_set_int(ep, "window_index", fs.wl->idx); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_pane(ep, "pane", wp); + + if (wp->cmd_status != -1) + event_payload_set_int(ep, "command_status", wp->cmd_status); + if (tstart != 0) + event_payload_set_time(ep, "command_start_time", tstart); + if (tend != 0) + event_payload_set_time(ep, "command_end_time", tend); + + if (tstart != 0) { + if (wp->flags & PANE_CMDRUNNING) + end = time(NULL); + else + end = tend; + if (end < tstart) + end = tstart; + end -= tstart; + event_payload_set_uint(ep, "command_duration", end); + } + + events_fire(name, ep); +} + /* Handle the OSC 133 sequence. */ 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 grid_line *gl; + struct grid_line *gl = NULL; + const char *errstr; + int status; - if (line > gd->hsize + gd->sy - 1) - return; - gl = grid_get_line(gd, line); + if (line <= gd->hsize + gd->sy - 1) + gl = grid_get_line(gd, line); switch (*p) { case 'A': - gl->flags |= GRID_LINE_START_PROMPT; + if (gl != NULL) + gl->flags |= GRID_LINE_START_PROMPT; + if (wp != NULL) { + wp->last_prompt_time = time(NULL); + events_fire_pane("pane-shell-prompt", wp); + } break; case 'C': - gl->flags |= GRID_LINE_START_OUTPUT; + if (gl != NULL) + gl->flags |= GRID_LINE_START_OUTPUT; + if (wp != NULL) { + wp->cmd_start_time = time(NULL); + wp->cmd_end_time = 0; + wp->flags |= PANE_CMDRUNNING; + wp->cmd_status = -1; + input_fire_command_event(wp, "pane-command-started"); + } + break; + case 'D': + 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; + } + input_fire_command_event(wp, "pane-command-finished"); + } break; } } @@ -3252,7 +3340,7 @@ input_osc_52(struct input_ctx *ictx, const char *p) screen_write_start_pane(&ctx, wp, NULL); screen_write_setselection(&ctx, clip, out, outlen); screen_write_stop(&ctx); - notify_pane("pane-set-clipboard", wp); + events_fire_pane("pane-set-clipboard", wp); paste_add(NULL, out, outlen); } } diff --git a/monitor.c b/monitor.c index 6d1a747fd..114f7b1da 100644 --- a/monitor.c +++ b/monitor.c @@ -1,7 +1,7 @@ /* $OpenBSD$ */ /* - * Copyright (c) 2026 Nicholas Marriott + * Copyright (c) 2026 Nicholas Marriott * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -53,7 +53,7 @@ struct monitor_item { enum monitor_type type; u_int id; - u_int flags; + int flags; char *last; struct monitor_panes panes; @@ -198,7 +198,9 @@ monitor_check_value(struct monitor_set *ms, struct monitor_item *me, { if (*last == NULL) { *last = value; - if (me->flags & MONITOR_NOTIFY_INITIAL) + if ((me->flags & MONITOR_NOTIFY_INITIAL) && + ((~me->flags & MONITOR_NOTIFY_TRUE) || + format_true(value))) monitor_report(ms, me, s, wl, wp, value, NULL); return; } @@ -208,7 +210,8 @@ monitor_check_value(struct monitor_set *ms, struct monitor_item *me, return; } - monitor_report(ms, me, s, wl, wp, value, *last); + if ((~me->flags & MONITOR_NOTIFY_TRUE) || format_true(value)) + monitor_report(ms, me, s, wl, wp, value, *last); free(*last); *last = value; } @@ -621,7 +624,7 @@ fail: /* Add a subscription. */ void monitor_add(struct monitor_set *ms, const char *name, enum monitor_type type, - int id, const char *format, u_int flags) + int id, const char *format, int flags) { struct monitor_item *me, find = { .name = (char *)name }; struct timeval tv = { .tv_sec = 1 }; diff --git a/options-table.c b/options-table.c index b1ab705d5..822499e7e 100644 --- a/options-table.c +++ b/options-table.c @@ -1933,12 +1933,22 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("client-light-theme", ""), OPTIONS_TABLE_HOOK("client-dark-theme", ""), OPTIONS_TABLE_HOOK("command-error", ""), + OPTIONS_TABLE_HOOK("marked-pane-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-command-finished", ""), + OPTIONS_TABLE_PANE_HOOK("pane-command-started", ""), + OPTIONS_TABLE_PANE_HOOK("pane-created", ""), OPTIONS_TABLE_PANE_HOOK("pane-died", ""), OPTIONS_TABLE_PANE_HOOK("pane-exited", ""), OPTIONS_TABLE_PANE_HOOK("pane-focus-in", ""), OPTIONS_TABLE_PANE_HOOK("pane-focus-out", ""), OPTIONS_TABLE_PANE_HOOK("pane-mode-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-mode-entered", ""), + OPTIONS_TABLE_PANE_HOOK("pane-mode-exited", ""), + OPTIONS_TABLE_PANE_HOOK("pane-prompt-closed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", ""), + OPTIONS_TABLE_PANE_HOOK("pane-resized", ""), OPTIONS_TABLE_PANE_HOOK("pane-set-clipboard", ""), + OPTIONS_TABLE_PANE_HOOK("pane-shell-prompt", ""), OPTIONS_TABLE_PANE_HOOK("pane-title-changed", ""), OPTIONS_TABLE_HOOK("session-closed", ""), OPTIONS_TABLE_HOOK("session-created", ""), @@ -1949,6 +1959,8 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_WINDOW_HOOK("window-pane-changed", ""), OPTIONS_TABLE_WINDOW_HOOK("window-renamed", ""), OPTIONS_TABLE_WINDOW_HOOK("window-resized", ""), + OPTIONS_TABLE_WINDOW_HOOK("window-unzoomed", ""), + OPTIONS_TABLE_WINDOW_HOOK("window-zoomed", ""), OPTIONS_TABLE_HOOK("window-unlinked", ""), { .name = NULL } diff --git a/tmux.1 b/tmux.1 index 50d398b7c..1afec4032 100644 --- a/tmux.1 +++ b/tmux.1 @@ -6323,11 +6323,7 @@ For example, this could be used to write to a log file: set\-hook \-g command\-error "run\-shell \\"echo 'a tmux command failed' >>/tmp/log\\"" .Ed .Pp -All the notifications listed in the -.Sx CONTROL MODE -section are hooks (without any arguments), except -.Ic %exit . -The following additional hooks are available: +The following hooks are available in addition to after hooks: .Bl -tag -width "XXXXXXXXXXXXXXXXXXXXXX" .It alert\-activity Run when a window has activity. @@ -6346,11 +6342,11 @@ Run when a client becomes the latest active client of its session. .It client\-attached Run when a client is attached. .It client\-detached -Run when a client is detached +Run when a client is detached. .It client\-focus\-in -Run when focus enters a client +Run when focus enters a client. .It client\-focus\-out -Run when focus exits a client +Run when focus exits a client. .It client\-resized Run when a client is resized. .It client\-session\-changed @@ -6361,6 +6357,21 @@ Run when a client switches to a light theme. Run when a client switches to a dark theme. .It command\-error Run when a command fails. +.It marked\-pane\-changed +Run when the marked pane is set or cleared. +.It pane\-command\-finished +Run when an OSC 133 command finishes in a pane. +The event payload includes +.Ql hook_command_start_time , +.Ql hook_command_end_time , +.Ql hook_command_duration +and +.Ql hook_command_status +if present. +.It pane\-command\-started +Run when an OSC 133 command starts in a pane. +.It pane\-created +Run when a pane is created or respawned. .It pane\-died Run when the program running in a pane exits, but .Ic remain\-on\-exit @@ -6375,20 +6386,40 @@ option is on. Run when the focus exits a pane, if the .Ic focus\-events option is on. +.It pane\-mode\-changed +Run when a pane changes mode. +.It pane\-mode\-entered +Run when a pane enters a mode. +.It pane\-mode\-exited +Run when a pane exits a mode. +.It pane\-prompt\-closed +Run when a prompt in a pane is closed. +.It pane\-prompt\-opened +Run when a prompt is opened in a pane. +.It pane\-resized +Run when a pane is resized. .It pane\-set\-clipboard Run when the terminal clipboard is set using the .Xr xterm 1 escape sequence. -.It session\-created -Run when a new session created. +.It pane\-shell\-prompt +Run when an OSC 133 shell prompt starts in a pane. +.It pane\-title\-changed +Run when a pane title is changed. .It session\-closed -Run when a session closed. +Run when a session is closed. +.It session\-created +Run when a new session is created. .It session\-renamed Run when a session is renamed. +.It session\-window\-changed +Run when a session changes its active window. .It window\-layout\-changed Run when a window layout is changed. .It window\-linked Run when a window is linked into a session. +.It window\-pane\-changed +Run when a window changes its active pane. .It window\-renamed Run when a window is renamed. .It window\-resized @@ -6396,6 +6427,10 @@ Run when a window is resized. This may be after the .Ar client\-resized hook is run. +.It window\-unzoomed +Run when a window is unzoomed. +.It window\-zoomed +Run when a window is zoomed. .It window\-unlinked Run when a window is unlinked from a session. .El @@ -6403,13 +6438,15 @@ Run when a window is unlinked from a session. Hooks are managed with these commands: .Bl -tag -width Ds .It Xo Ic set\-hook -.Op Fl agpRuw +.Op Fl agpERTuw .Op Fl B Ar name:what:format .Op Fl t Ar target\-pane .Ar hook\-name .Op Ar command .Xc Without +.Fl E +or .Fl R , sets (or with .Fl u @@ -6432,6 +6469,11 @@ is the hook to run, selects the session, pane, all panes, window, or all windows, and .Ar format is expanded once a second. +With +.Fl T , +the hook is run only when +.Ar format +is true. For monitor hooks, .Ar name must begin with @@ -6450,6 +6492,14 @@ the subscription named by is removed. .Pp With +.Fl E , +fire the user event named +.Ar hook\-name . +.Ar hook\-name +must begin with +.Ql @ . +.Pp +With .Fl R , run .Ar hook\-name @@ -7159,6 +7209,11 @@ The following variables are available, where appropriate: .It Li "pane_at_top" Ta "" Ta "1 if pane is at the top of window" .It Li "pane_bg" Ta "" Ta "Pane background colour" .It Li "pane_bottom" Ta "" Ta "Bottom of pane" +.It Li "pane_command_duration" Ta "" Ta "Current or most recent OSC 133 command duration in seconds" +.It Li "pane_command_end_time" Ta "" Ta "Time most recent OSC 133 command ended" +.It Li "pane_command_running" Ta "" Ta "1 if an OSC 133 command is running" +.It Li "pane_command_start_time" Ta "" Ta "Time current or most recent OSC 133 command started" +.It Li "pane_command_status" Ta "" Ta "Exit status from most recent OSC 133 command" .It Li "pane_current_command" Ta "" Ta "Current command if available" .It Li "pane_current_path" Ta "" Ta "Current path if available" .It Li "pane_dead" Ta "" Ta "1 if pane is dead" @@ -7176,6 +7231,8 @@ The following variables are available, where appropriate: .It Li "pane_input_off" Ta "" Ta "1 if input to pane is disabled" .It Li "pane_key_mode" Ta "" Ta "Extended key reporting mode in this pane" .It Li "pane_last" Ta "" Ta "1 if last pane" +.It Li "pane_last_output_time" Ta "" Ta "Time pane last produced output" +.It Li "pane_last_prompt_time" Ta "" Ta "Time most recent OSC 133 prompt began" .It Li "pane_left" Ta "" Ta "Left of pane" .It Li "pane_marked" Ta "" Ta "1 if this is the marked pane" .It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set" @@ -8560,8 +8617,10 @@ or the current pane if omitted) after the command finishes. If the command fails, the exit status is also displayed. .Tg wait .It Xo Ic wait\-for -.Op Fl L | S | U -.Ar channel +.Op Fl ELSUlv +.Op Fl F Ar format +.Op Fl w Ar waiter +.Ar name .Xc .D1 Pq alias: Ic wait When used without options, prevents the client from exiting until woken using @@ -8574,6 +8633,41 @@ is used, the channel is locked and any clients that try to lock the same channel are made to wait until the channel is unlocked with .Ic wait\-for .Fl U . +.Pp +With +.Fl E , +.Nm +waits for the next event with +.Ar name . +Events include hook and notification names, and user +.Ql @ +events generated by +.Ic set-hook +.Fl E +or +.Ic set-hook +.Fl B . +If +.Fl F +is given, +.Ar format +must also be true. +If +.Fl v +is given, event payload keys are printed (whether or not +.Ar format +is true). +.Pp +.Fl l +list the waiters for +.Ar name +and +.Fl w +wakes +.Ar waiter +on +.Ar name +immediately. .El .Sh EXIT MESSAGES When a diff --git a/tmux.h b/tmux.h index b153310cf..e698adf1b 100644 --- a/tmux.h +++ b/tmux.h @@ -48,6 +48,9 @@ struct cmdq_state; struct cmds; struct control_state; struct environ; +struct event_payload; +struct event_payload_item; +struct events_sink; struct format_job_tree; struct format_tree; struct hyperlinks_uri; @@ -1264,6 +1267,7 @@ struct window_pane { #define PANE_UNSEENCHANGES 0x4000 #define PANE_REDRAWSCROLLBAR 0x8000 #define PANE_DESTROYED 0x10000 +#define PANE_CMDRUNNING 0x20000 bitstr_t *sync_dirty; u_int sync_dirty_size; @@ -1286,6 +1290,12 @@ struct window_pane { struct cmdq_item *wait_item; /* new-pane -W: waiting for pane exit */ struct spawn_editor_state *editor; + time_t last_output_time; + time_t last_prompt_time; + time_t cmd_start_time; + time_t cmd_end_time; + int cmd_status; + int fd; struct bufferevent *event; @@ -2290,6 +2300,7 @@ enum monitor_type { MONITOR_ALL_WINDOWS }; #define MONITOR_NOTIFY_INITIAL 0x1 +#define MONITOR_NOTIFY_TRUE 0x2 struct monitor_change { const char *name; const char *value; @@ -2302,6 +2313,23 @@ struct monitor_change { }; typedef void (*monitor_cb)(struct monitor_change *, void *); +/* Event payload type. */ +enum event_payload_type { + EVENT_PAYLOAD_STRING, + EVENT_PAYLOAD_TIME, + EVENT_PAYLOAD_INT, + EVENT_PAYLOAD_UINT, + EVENT_PAYLOAD_CLIENT, + EVENT_PAYLOAD_SESSION, + EVENT_PAYLOAD_WINDOW, + EVENT_PAYLOAD_PANE, + EVENT_PAYLOAD_POINTER +}; + +/* Event payload callbacks. */ +typedef void (*event_payload_free_cb)(void *); +typedef void (*event_payload_print_cb)(void *, struct evbuffer *); + /* Key binding and key table. */ struct key_binding { key_code key; @@ -2611,29 +2639,78 @@ char *format_grid_hyperlink(struct grid *, u_int, u_int, struct screen *); char *format_grid_line(struct grid *, u_int); -/* format-draw.c */ -void format_draw(struct screen_write_ctx *, - const struct grid_cell *, u_int, const char *, - struct style_ranges *, int); -u_int format_width(const char *); -char *format_trim_left(const char *, u_int); -char *format_trim_right(const char *, u_int); +/* events-payload.c */ +struct event_payload *event_payload_create(void); +void event_payload_free(struct event_payload *); +void printflike(2, 3) event_payload_log(struct event_payload *, const char *, + ...); +char *event_payload_item_print(struct event_payload_item *); +void event_payload_set_target(struct event_payload *, + struct cmd_find_state *); +int event_payload_get_target(struct event_payload *, + struct cmd_find_state *); +void printflike(3, 4) event_payload_set_string(struct event_payload *, + const char *, const char *, ...); +void event_payload_set_time(struct event_payload *, const char *, time_t); +void event_payload_set_int(struct event_payload *, const char *, int); +void event_payload_set_uint(struct event_payload *, const char *, u_int); +void event_payload_set_client(struct event_payload *, const char *, + struct client *); +void event_payload_set_session(struct event_payload *, const char *, + struct session *); +void event_payload_set_window(struct event_payload *, const char *, + struct window *); +void event_payload_set_pane(struct event_payload *, const char *, + struct window_pane *); +void event_payload_set_pointer(struct event_payload *, const char *, void *, + event_payload_free_cb, event_payload_print_cb); +const char *event_payload_get_string(struct event_payload *, const char *); +char *event_payload_print(struct event_payload *, const char *); +void event_payload_add_formats(struct event_payload *, + struct format_tree *, const char *); +struct event_payload_item *event_payload_first(struct event_payload *); +struct event_payload_item *event_payload_next(struct event_payload_item *); +const char *event_payload_item_name(struct event_payload_item *); +enum event_payload_type event_payload_item_type(struct event_payload_item *); +time_t event_payload_get_time(struct event_payload *, const char *); +int event_payload_get_int(struct event_payload *, const char *, int *); +int event_payload_get_uint(struct event_payload *, const char *, u_int *); +struct client *event_payload_get_client(struct event_payload *, const char *); +struct session *event_payload_get_session(struct event_payload *, const char *); +struct window *event_payload_get_window(struct event_payload *, const char *); +struct window_pane *event_payload_get_pane(struct event_payload *, + const char *); +void *event_payload_get_pointer(struct event_payload *, const char *); -/* notify.c */ -void notify_hook(struct cmdq_item *, const char *); -void notify_monitor_add(struct cmdq_item *, struct options *, +/* events.c */ +typedef void (*events_cb)(const char *, struct event_payload *, void *); +struct events_sink *events_add_sink(const char *, events_cb, void *); +void events_remove_sink(struct events_sink *); +void events_fire(const char *, struct event_payload *); +void events_fire_client(const char *, struct client *); +void events_fire_session(const char *, struct session *); +void events_fire_window(const char *, struct window *); +void events_fire_pane(const char *, struct window_pane *); +void events_fire_winlink(const char *, struct winlink *); + +/* format-draw.c */ +void format_draw(struct screen_write_ctx *, const struct grid_cell *, + u_int, const char *, struct style_ranges *, int); +u_int format_width(const char *); +char *format_trim_left(const char *, u_int); +char *format_trim_right(const char *, u_int); + +/* hooks.c */ +void hooks_add_event(const char *); +int hooks_valid_event_name(const char *); +void hooks_build_events(void); +void hooks_run(struct cmdq_item *, const char *); +void hooks_monitor_add(struct cmdq_item *, struct options *, const char *, enum monitor_type, int, const char *, - struct cmd_find_state *, struct session *); -void notify_monitor_remove(struct options *, const char *); -void notify_monitor_free(void *); -char *notify_monitor_to_string(struct options_entry *); -void notify_client(const char *, struct client *); -void notify_session(const char *, struct session *); -void notify_winlink(const char *, struct winlink *); -void notify_session_window(const char *, struct session *, struct window *); -void notify_window(const char *, struct window *); -void notify_pane(const char *, struct window_pane *); -void notify_paste_buffer(const char *, int); + int, struct cmd_find_state *, struct session *); +void hooks_monitor_remove(struct options *, const char *); +void hooks_monitor_free(void *); +char *hooks_monitor_to_string(struct options_entry *); /* options.c */ struct options *options_create(struct options *); @@ -3832,7 +3909,7 @@ void monitor_destroy(struct monitor_set *); int monitor_parse(const char *, char **, enum monitor_type *, int *, char **); void monitor_add(struct monitor_set *, const char *, enum monitor_type, int, - const char *, u_int); + const char *, int); void monitor_remove(struct monitor_set *, const char *); /* control.c */ @@ -3855,20 +3932,7 @@ void control_add_sub(struct client *, const char *, enum monitor_type, int, void control_remove_sub(struct client *, const char *); /* control-notify.c */ -void control_notify_pane_mode_changed(int); -void control_notify_window_layout_changed(struct window *); -void control_notify_window_pane_changed(struct window *); -void control_notify_window_unlinked(struct session *, struct window *); -void control_notify_window_linked(struct session *, struct window *); -void control_notify_window_renamed(struct window *); -void control_notify_client_session_changed(struct client *); -void control_notify_client_detached(struct client *); -void control_notify_session_renamed(struct session *); -void control_notify_session_created(struct session *); -void control_notify_session_closed(struct session *); -void control_notify_session_window_changed(struct session *); -void control_notify_paste_buffer_changed(const char *); -void control_notify_paste_buffer_deleted(const char *); +void control_build_events(void); /* session.c */ extern struct sessions sessions; diff --git a/window.c b/window.c index 109f002ff..a57db1723 100644 --- a/window.c +++ b/window.c @@ -75,8 +75,8 @@ static struct window_pane *window_pane_create(struct window *, u_int, u_int, static void window_pane_destroy(struct window_pane *); static void window_pane_free(struct window_pane *); static void window_pane_scrollbar_timer(int, short, void *); -static void window_pane_full_size_offset(struct window_pane *wp, - int *xoff, int *yoff, u_int *sx, u_int *sy); +static void window_pane_full_size_offset(struct window_pane *, int *, int *, + u_int *, u_int *); RB_GENERATE(windows, window, entry, window_cmp); RB_GENERATE(winlinks, winlink, entry, winlink_cmp); @@ -88,6 +88,7 @@ struct window_pane_prompt { status_prompt_input_cb inputcb; prompt_free_cb freecb; void *data; + enum prompt_type type; }; int @@ -96,6 +97,78 @@ window_cmp(struct window *w1, struct window *w2) return (w1->id - w2->id); } +static void +window_fire_renamed(struct window *w, const char *old_name) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_window(&fs, w, 0); + event_payload_set_target(ep, &fs); + event_payload_set_window(ep, "window", w); + event_payload_set_string(ep, "old_name", "%s", old_name); + event_payload_set_string(ep, "new_name", "%s", w->name); + events_fire("window-renamed", ep); +} + +static void +window_fire_pane_changed(struct window *w, struct window_pane *wp, + struct window_pane *lastwp) +{ + 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_window(ep, "window", w); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_pane(ep, "new_pane", wp); + if (lastwp != NULL) + event_payload_set_pane(ep, "old_pane", lastwp); + events_fire("window-pane-changed", ep); +} + +static void +window_fire_pane_mode_changed(const char *name, struct window_pane *wp, + const char *previous, const char *current, int entered) +{ + 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); + + if (current != NULL) + event_payload_set_string(ep, "current_mode", "%s", current); + if (previous != NULL) + event_payload_set_string(ep, "previous_mode", "%s", previous); + event_payload_set_int(ep, "mode_entered", entered); + + events_fire(name, ep); +} + +static void +window_fire_pane_prompt(const char *name, struct window_pane *wp, + enum prompt_type type) +{ + struct event_payload *ep; + struct cmd_find_state fs; + const char *type_string = prompt_type_string(type); + + 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, "prompt_type", "%s", type_string); + events_fire(name, ep); +} + int winlink_cmp(struct winlink *wl1, struct winlink *wl2) { @@ -446,13 +519,15 @@ window_pane_remove_ref(struct window_pane *wp, const char *from) void window_set_name(struct window *w, const char *new_name, int untrusted) { - char *name; + char *last, *name; name = clean_name(new_name, untrusted); if (name != NULL) { + last = xstrdup(w->name); free(w->name); w->name = name; - notify_window("window-renamed", w); + window_fire_renamed(w, last); + free(last); } } @@ -553,13 +628,13 @@ window_pane_update_focus(struct window_pane *wp) log_debug("%s: %%%u focus out", __func__, wp->id); if (wp->base.mode & MODE_FOCUSON) bufferevent_write(wp->event, "\033[O", 3); - notify_pane("pane-focus-out", wp); + events_fire_pane("pane-focus-out", wp); wp->flags &= ~PANE_FOCUSED; } else if (focused && (~wp->flags & PANE_FOCUSED)) { log_debug("%s: %%%u focus in", __func__, wp->id); if (wp->base.mode & MODE_FOCUSON) bufferevent_write(wp->event, "\033[I", 3); - notify_pane("pane-focus-in", wp); + events_fire_pane("pane-focus-in", wp); wp->flags |= PANE_FOCUSED; } else log_debug("%s: %%%u focus unchanged", __func__, wp->id); @@ -595,7 +670,7 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify) server_redraw_window(w); if (notify) - notify_window("window-pane-changed", w); + window_fire_pane_changed(w, w->active, lastwp); return (1); } @@ -785,7 +860,8 @@ window_zoom(struct window_pane *wp) w->saved_layout_root = w->layout_root; layout_init(w, wp); w->flags |= WINDOW_ZOOMED; - notify_window("window-layout-changed", w); + events_fire_window("window-zoomed", w); + events_fire_window("window-layout-changed", w); redraw_invalidate_scene(w); return (0); @@ -811,8 +887,10 @@ window_unzoom(struct window *w, int notify) } layout_fix_panes(w, NULL); - if (notify) - notify_window("window-layout-changed", w); + if (notify) { + events_fire_window("window-unzoomed", w); + events_fire_window("window-layout-changed", w); + } redraw_invalidate_scene(w); return (0); @@ -894,7 +972,7 @@ window_lost_pane(struct window *w, struct window_pane *wp) if (w->active != NULL) { window_pane_stack_remove(&w->last_panes, w->active); w->active->flags |= PANE_CHANGED; - notify_window("window-pane-changed", w); + events_fire_window("window-pane-changed", w); window_update_focus(w); } } @@ -1099,6 +1177,7 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit) wp->window = w; wp->options = options_create(w->options); wp->flags = PANE_STYLECHANGED; + wp->cmd_status = -1; wp->id = next_window_pane_id++; RB_INSERT(window_pane_tree, &all_window_panes, wp); @@ -1223,9 +1302,9 @@ window_pane_destroy(struct window_pane *wp) window_pane_wait_finish(wp); spawn_editor_finish(wp); - window_pane_clear_prompt(wp); RB_REMOVE(window_pane_tree, &all_window_panes, wp); wp->flags |= PANE_DESTROYED; + window_pane_clear_prompt(wp); window_pane_free_modes(wp); screen_write_clear_dirty(wp); @@ -1352,6 +1431,8 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) { struct window_mode_entry *wme; struct window_pane_resize *r; + struct event_payload *ep; + struct cmd_find_state fs; if (sx == wp->sx && sy == wp->sy) return; @@ -1374,6 +1455,17 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) wme = TAILQ_FIRST(&wp->modes); if (wme != NULL && wme->mode->resize != NULL) wme->mode->resize(wme, sx, sy); + + 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_uint(ep, "width", sx); + event_payload_set_uint(ep, "height", sy); + event_payload_set_uint(ep, "old_width", r->osx); + event_payload_set_uint(ep, "old_height", r->osy); + events_fire("pane-resized", ep); } int @@ -1383,9 +1475,13 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, { struct window_mode_entry *wme; struct window *w = wp->window; + const char *name = mode->name, *p = NULL; - if (!TAILQ_EMPTY(&wp->modes) && TAILQ_FIRST(&wp->modes)->mode == mode) - return (1); + if (!TAILQ_EMPTY(&wp->modes)) { + if (TAILQ_FIRST(&wp->modes)->mode == mode) + return (1); + p = TAILQ_FIRST(&wp->modes)->mode->name; + } TAILQ_FOREACH(wme, &wp->modes, entry) { if (wme->mode == mode) @@ -1411,7 +1507,9 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, server_redraw_window_borders(wp->window); server_status_window(wp->window); - notify_pane("pane-mode-changed", wp); + + window_fire_pane_mode_changed("pane-mode-entered", wp, p, name, 1); + window_fire_pane_mode_changed("pane-mode-changed", wp, p, name, 1); return (0); } @@ -1422,11 +1520,13 @@ window_pane_reset_mode(struct window_pane *wp) struct window_mode_entry *wme, *next; struct window *w = wp->window; int kill; + const char *name, *p; if (TAILQ_EMPTY(&wp->modes)) return; wme = TAILQ_FIRST(&wp->modes); + p = wme->mode->name; kill = wme->kill; TAILQ_REMOVE(&wp->modes, wme, entry); wme->mode->free(wme); @@ -1443,13 +1543,16 @@ window_pane_reset_mode(struct window_pane *wp) if (next->mode->resize != NULL) next->mode->resize(next, wp->sx, wp->sy); } + name = (next == NULL ? NULL : next->mode->name); wp->flags |= (PANE_REDRAW|PANE_REDRAWSCROLLBAR|PANE_CHANGED); layout_fix_panes(w, NULL); server_redraw_window_borders(wp->window); server_status_window(wp->window); - notify_pane("pane-mode-changed", wp); + + window_fire_pane_mode_changed("pane-mode-exited", wp, p, name, 0); + window_fire_pane_mode_changed("pane-mode-changed", wp, p, name, 0); if (kill) server_kill_pane(wp); @@ -1512,6 +1615,7 @@ window_pane_set_prompt(struct window_pane *wp, struct client *c, wpp->inputcb = inputcb; wpp->freecb = freecb; wpp->data = data; + wpp->type = type; memset(&pd, 0, sizeof pd); prompt_set_options(&pd, s); @@ -1529,19 +1633,28 @@ window_pane_set_prompt(struct window_pane *wp, struct client *c, wp->flags |= PANE_REDRAW; prompt_incremental_start(wp->prompt); + window_fire_pane_prompt("pane-prompt-opened", wp, type); } /* Close a pane prompt. */ void window_pane_clear_prompt(struct window_pane *wp) { - struct prompt *prompt = wp->prompt; + struct prompt *prompt = wp->prompt; + struct window_pane_prompt *wpp = wp->prompt_data; + enum prompt_type type = PROMPT_TYPE_INVALID; - if (prompt == NULL) - return; - wp->prompt = NULL; - prompt_free(prompt); - wp->flags |= PANE_REDRAW; + if (prompt != NULL) { + if (wpp != NULL) + type = wpp->type; + + wp->prompt = NULL; + prompt_free(prompt); + wp->flags |= PANE_REDRAW; + + if (~wp->flags & PANE_DESTROYED) + window_fire_pane_prompt("pane-prompt-closed", wp, type); + } } /* Does this pane have an open prompt? */ From 512255b9a7b37fd571d33e6ecfaeb2ba4039fc2e Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 10 Jul 2026 15:20:06 +0000 Subject: [PATCH 102/127] Add formats and events for OSC 133 commmands, as well as a -T flag to set-hook -B to only fire when the format is true. --- cmd-set-option.c | 12 +++-- format.c | 123 ++++++++++++++++++++++++++++++++++++++++++++++- hooks.c | 6 +-- input.c | 87 ++++++++++++++++++++++++++++++--- monitor.c | 13 +++-- options-table.c | 5 +- tmux.1 | 29 ++++++++++- tmux.h | 14 ++++-- window.c | 3 +- 9 files changed, 263 insertions(+), 29 deletions(-) diff --git a/cmd-set-option.c b/cmd-set-option.c index 158cd5fac..335a54760 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-set-option.c,v 1.145 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-set-option.c,v 1.146 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -66,8 +66,8 @@ const struct cmd_entry cmd_set_hook_entry = { .name = "set-hook", .alias = NULL, - .args = { "agpERt:uB:w", 0, 2, cmd_set_option_args_parse }, - .usage = "[-agpERuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " + .args = { "agpERTt:uB:w", 0, 2, cmd_set_option_args_parse }, + .usage = "[-agpERTuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " "[hook] [command]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -143,7 +143,7 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) char *expanded = NULL, *newvalue = NULL; const char *value, *old; enum monitor_type type; - int id, scope; + int id, scope, flags = 0; if (args_count(args) > 1) { cmdq_error(item, "too many arguments"); @@ -204,7 +204,9 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) oo != global_s_options && oo != global_w_options) s = target->s; - hooks_monitor_add(item, oo, name, type, id, format, &fs, s); + if (args_has(args, 'T')) + flags |= MONITOR_NOTIFY_TRUE; + hooks_monitor_add(item, oo, name, type, id, format, flags, &fs, s); out: free(newvalue); diff --git a/format.c b/format.c index da374d580..6175f0d0d 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.402 2026/07/09 06:33:19 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.403 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -2173,6 +2173,106 @@ format_cb_pane_dead_time(struct format_tree *ft) return (NULL); } +/* Callback for pane_last_output_time. */ +static void * +format_cb_pane_last_output_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->last_output_time != 0) { + tv.tv_sec = wp->last_output_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_last_prompt_time. */ +static void * +format_cb_pane_last_prompt_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->last_prompt_time != 0) { + tv.tv_sec = wp->last_prompt_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_start_time. */ +static void * +format_cb_pane_command_start_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->cmd_start_time != 0) { + tv.tv_sec = wp->cmd_start_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_end_time. */ +static void * +format_cb_pane_command_end_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->cmd_end_time != 0) { + tv.tv_sec = wp->cmd_end_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_running. */ +static void * +format_cb_pane_command_running(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%d", !!(wp->flags & PANE_CMDRUNNING))); + return (NULL); +} + +/* Callback for pane_command_duration. */ +static void * +format_cb_pane_command_duration(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + time_t end; + + if (wp == NULL || wp->cmd_start_time == 0) + return (NULL); + if (wp->flags & PANE_CMDRUNNING) + end = time(NULL); + else + end = wp->cmd_end_time; + if (end < wp->cmd_start_time) + end = wp->cmd_start_time; + return (format_printf("%lld", (long long)(end - wp->cmd_start_time))); +} + +/* Callback for pane_command_status. */ +static void * +format_cb_pane_command_status(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL && wp->cmd_status != -1) + return (format_printf("%d", wp->cmd_status)); + return (NULL); +} + /* Callback for pane_format. */ static void * format_cb_pane_format(struct format_tree *ft) @@ -3485,6 +3585,21 @@ static const struct format_table_entry format_table[] = { { "pane_bottom", FORMAT_TABLE_STRING, format_cb_pane_bottom }, + { "pane_command_duration", FORMAT_TABLE_STRING, + format_cb_pane_command_duration + }, + { "pane_command_end_time", FORMAT_TABLE_TIME, + format_cb_pane_command_end_time + }, + { "pane_command_running", FORMAT_TABLE_STRING, + format_cb_pane_command_running + }, + { "pane_command_start_time", FORMAT_TABLE_TIME, + format_cb_pane_command_start_time + }, + { "pane_command_status", FORMAT_TABLE_STRING, + format_cb_pane_command_status + }, { "pane_current_command", FORMAT_TABLE_STRING, format_cb_current_command }, @@ -3536,6 +3651,12 @@ static const struct format_table_entry format_table[] = { { "pane_last", FORMAT_TABLE_STRING, format_cb_pane_last }, + { "pane_last_output_time", FORMAT_TABLE_TIME, + format_cb_pane_last_output_time + }, + { "pane_last_prompt_time", FORMAT_TABLE_TIME, + format_cb_pane_last_prompt_time + }, { "pane_left", FORMAT_TABLE_STRING, format_cb_pane_left }, diff --git a/hooks.c b/hooks.c index 0ac64b8bd..fd55c7273 100644 --- a/hooks.c +++ b/hooks.c @@ -1,4 +1,4 @@ -/* $OpenBSD: hooks.c,v 1.12 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: hooks.c,v 1.13 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -407,7 +407,7 @@ hooks_monitor_cb(struct monitor_change *change, void *data) void hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, const char *name, enum monitor_type type, int id, const char *format, - struct cmd_find_state *fs, struct session *s) + int flags, struct cmd_find_state *fs, struct session *s) { struct options_entry *o; struct hook_monitor *hm; @@ -426,7 +426,7 @@ hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, hm->set = monitor_create_session(s, hooks_monitor_cb, hm); hm->sink = events_add_sink(name, hooks_monitor_hook_cb, hm); options_set_monitor_data(o, hm); - monitor_add(hm->set, name, type, id, format, 0); + monitor_add(hm->set, name, type, id, format, flags); } /* Convert a hook monitor to its value. */ diff --git a/input.c b/input.c index bfab97a7b..fe7297dae 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.265 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.266 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1034,6 +1034,8 @@ input_parse_pane(struct window_pane *wp) size_t new_size; new_data = window_pane_get_new_data(wp, &wp->offset, &new_size); + if (new_size != 0) + wp->last_output_time = time(NULL); input_parse_buffer(wp, new_data, new_size); window_pane_update_used_data(wp, &wp->offset, new_size); } @@ -3126,7 +3128,8 @@ input_osc_12(struct input_ctx *ictx, const char *p) c = ictx->ctx.s->ccolour; if (c == -1) c = ictx->ctx.s->default_ccolour; - input_osc_colour_reply(ictx, 1, 12, 0, c, ictx->input_end); + input_osc_colour_reply(ictx, 1, 12, 0, c, + ictx->input_end); } return; } @@ -3146,24 +3149,92 @@ input_osc_112(struct input_ctx *ictx, const char *p) screen_set_cursor_colour(ictx->ctx.s, -1); } +/* Fire an OSC 133 command event. */ +static void +input_fire_command_event(struct window_pane *wp, const char *name) +{ + struct event_payload *ep; + struct cmd_find_state fs; + time_t tstart = wp->cmd_start_time, end; + time_t tend = wp->cmd_end_time; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + if (fs.s != NULL) + event_payload_set_session(ep, "session", fs.s); + if (fs.wl != NULL) + event_payload_set_int(ep, "window_index", fs.wl->idx); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_pane(ep, "pane", wp); + + if (wp->cmd_status != -1) + event_payload_set_int(ep, "command_status", wp->cmd_status); + if (tstart != 0) + event_payload_set_time(ep, "command_start_time", tstart); + if (tend != 0) + event_payload_set_time(ep, "command_end_time", tend); + + if (tstart != 0) { + if (wp->flags & PANE_CMDRUNNING) + end = time(NULL); + else + end = tend; + if (end < tstart) + end = tstart; + end -= tstart; + event_payload_set_uint(ep, "command_duration", end); + } + + events_fire(name, ep); +} + /* Handle the OSC 133 sequence. */ 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 grid_line *gl; + struct grid_line *gl = NULL; + const char *errstr; + int status; - if (line > gd->hsize + gd->sy - 1) - return; - gl = grid_get_line(gd, line); + if (line <= gd->hsize + gd->sy - 1) + gl = grid_get_line(gd, line); switch (*p) { case 'A': - gl->flags |= GRID_LINE_START_PROMPT; + if (gl != NULL) + gl->flags |= GRID_LINE_START_PROMPT; + if (wp != NULL) { + wp->last_prompt_time = time(NULL); + events_fire_pane("pane-shell-prompt", wp); + } break; case 'C': - gl->flags |= GRID_LINE_START_OUTPUT; + if (gl != NULL) + gl->flags |= GRID_LINE_START_OUTPUT; + if (wp != NULL) { + wp->cmd_start_time = time(NULL); + wp->cmd_end_time = 0; + wp->flags |= PANE_CMDRUNNING; + wp->cmd_status = -1; + input_fire_command_event(wp, "pane-command-started"); + } + break; + case 'D': + 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; + } + input_fire_command_event(wp, "pane-command-finished"); + } break; } } diff --git a/monitor.c b/monitor.c index 514af4e3c..d6338e878 100644 --- a/monitor.c +++ b/monitor.c @@ -1,4 +1,4 @@ -/* $OpenBSD: monitor.c,v 1.5 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: monitor.c,v 1.6 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -53,7 +53,7 @@ struct monitor_item { enum monitor_type type; u_int id; - u_int flags; + int flags; char *last; struct monitor_panes panes; @@ -198,7 +198,9 @@ monitor_check_value(struct monitor_set *ms, struct monitor_item *me, { if (*last == NULL) { *last = value; - if (me->flags & MONITOR_NOTIFY_INITIAL) + if ((me->flags & MONITOR_NOTIFY_INITIAL) && + ((~me->flags & MONITOR_NOTIFY_TRUE) || + format_true(value))) monitor_report(ms, me, s, wl, wp, value, NULL); return; } @@ -208,7 +210,8 @@ monitor_check_value(struct monitor_set *ms, struct monitor_item *me, return; } - monitor_report(ms, me, s, wl, wp, value, *last); + if ((~me->flags & MONITOR_NOTIFY_TRUE) || format_true(value)) + monitor_report(ms, me, s, wl, wp, value, *last); free(*last); *last = value; } @@ -621,7 +624,7 @@ fail: /* Add a subscription. */ void monitor_add(struct monitor_set *ms, const char *name, enum monitor_type type, - int id, const char *format, u_int flags) + int id, const char *format, int flags) { struct monitor_item *me, find = { .name = (char *)name }; struct timeval tv = { .tv_sec = 1 }; diff --git a/options-table.c b/options-table.c index 082ed06ff..49cf9730f 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.232 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.233 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1934,6 +1934,8 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("client-dark-theme", ""), OPTIONS_TABLE_HOOK("command-error", ""), OPTIONS_TABLE_HOOK("marked-pane-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-command-finished", ""), + OPTIONS_TABLE_PANE_HOOK("pane-command-started", ""), OPTIONS_TABLE_PANE_HOOK("pane-created", ""), OPTIONS_TABLE_PANE_HOOK("pane-died", ""), OPTIONS_TABLE_PANE_HOOK("pane-exited", ""), @@ -1946,6 +1948,7 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", ""), OPTIONS_TABLE_PANE_HOOK("pane-resized", ""), OPTIONS_TABLE_PANE_HOOK("pane-set-clipboard", ""), + OPTIONS_TABLE_PANE_HOOK("pane-shell-prompt", ""), OPTIONS_TABLE_PANE_HOOK("pane-title-changed", ""), OPTIONS_TABLE_HOOK("session-closed", ""), OPTIONS_TABLE_HOOK("session-created", ""), diff --git a/tmux.1 b/tmux.1 index 215878dea..262187889 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1131 2026/07/10 13:38:45 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1132 2026/07/10 15:20:06 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -6359,6 +6359,17 @@ Run when a client switches to a dark theme. Run when a command fails. .It marked\-pane\-changed Run when the marked pane is set or cleared. +.It pane\-command\-finished +Run when an OSC 133 command finishes in a pane. +The event payload includes +.Ql hook_command_start_time , +.Ql hook_command_end_time , +.Ql hook_command_duration +and +.Ql hook_command_status +if present. +.It pane\-command\-started +Run when an OSC 133 command starts in a pane. .It pane\-created Run when a pane is created or respawned. .It pane\-died @@ -6391,6 +6402,8 @@ Run when a pane is resized. Run when the terminal clipboard is set using the .Xr xterm 1 escape sequence. +.It pane\-shell\-prompt +Run when an OSC 133 shell prompt starts in a pane. .It pane\-title\-changed Run when a pane title is changed. .It session\-closed @@ -6425,7 +6438,7 @@ Run when a window is unlinked from a session. Hooks are managed with these commands: .Bl -tag -width Ds .It Xo Ic set\-hook -.Op Fl agpERuw +.Op Fl agpERTuw .Op Fl B Ar name:what:format .Op Fl t Ar target\-pane .Ar hook\-name @@ -6456,6 +6469,11 @@ is the hook to run, selects the session, pane, all panes, window, or all windows, and .Ar format is expanded once a second. +With +.Fl T , +the hook is run only when +.Ar format +is true. For monitor hooks, .Ar name must begin with @@ -7191,6 +7209,11 @@ The following variables are available, where appropriate: .It Li "pane_at_top" Ta "" Ta "1 if pane is at the top of window" .It Li "pane_bg" Ta "" Ta "Pane background colour" .It Li "pane_bottom" Ta "" Ta "Bottom of pane" +.It Li "pane_command_duration" Ta "" Ta "Current or most recent OSC 133 command duration in seconds" +.It Li "pane_command_end_time" Ta "" Ta "Time most recent OSC 133 command ended" +.It Li "pane_command_running" Ta "" Ta "1 if an OSC 133 command is running" +.It Li "pane_command_start_time" Ta "" Ta "Time current or most recent OSC 133 command started" +.It Li "pane_command_status" Ta "" Ta "Exit status from most recent OSC 133 command" .It Li "pane_current_command" Ta "" Ta "Current command if available" .It Li "pane_current_path" Ta "" Ta "Current path if available" .It Li "pane_dead" Ta "" Ta "1 if pane is dead" @@ -7208,6 +7231,8 @@ The following variables are available, where appropriate: .It Li "pane_input_off" Ta "" Ta "1 if input to pane is disabled" .It Li "pane_key_mode" Ta "" Ta "Extended key reporting mode in this pane" .It Li "pane_last" Ta "" Ta "1 if last pane" +.It Li "pane_last_output_time" Ta "" Ta "Time pane last produced output" +.It Li "pane_last_prompt_time" Ta "" Ta "Time most recent OSC 133 prompt began" .It Li "pane_left" Ta "" Ta "Left of pane" .It Li "pane_marked" Ta "" Ta "1 if this is the marked pane" .It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set" diff --git a/tmux.h b/tmux.h index 076f4bf7c..b3a3a9796 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1394 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1395 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1267,6 +1267,7 @@ struct window_pane { #define PANE_UNSEENCHANGES 0x4000 #define PANE_REDRAWSCROLLBAR 0x8000 #define PANE_DESTROYED 0x10000 +#define PANE_CMDRUNNING 0x20000 bitstr_t *sync_dirty; u_int sync_dirty_size; @@ -1289,6 +1290,12 @@ struct window_pane { struct cmdq_item *wait_item; /* new-pane -W: waiting for pane exit */ struct spawn_editor_state *editor; + time_t last_output_time; + time_t last_prompt_time; + time_t cmd_start_time; + time_t cmd_end_time; + int cmd_status; + int fd; struct bufferevent *event; @@ -2293,6 +2300,7 @@ enum monitor_type { MONITOR_ALL_WINDOWS }; #define MONITOR_NOTIFY_INITIAL 0x1 +#define MONITOR_NOTIFY_TRUE 0x2 struct monitor_change { const char *name; const char *value; @@ -2699,7 +2707,7 @@ void hooks_build_events(void); void hooks_run(struct cmdq_item *, const char *); void hooks_monitor_add(struct cmdq_item *, struct options *, const char *, enum monitor_type, int, const char *, - struct cmd_find_state *, struct session *); + int, struct cmd_find_state *, struct session *); void hooks_monitor_remove(struct options *, const char *); void hooks_monitor_free(void *); char *hooks_monitor_to_string(struct options_entry *); @@ -3901,7 +3909,7 @@ void monitor_destroy(struct monitor_set *); int monitor_parse(const char *, char **, enum monitor_type *, int *, char **); void monitor_add(struct monitor_set *, const char *, enum monitor_type, int, - const char *, u_int); + const char *, int); void monitor_remove(struct monitor_set *, const char *); /* control.c */ diff --git a/window.c b/window.c index 198296da4..f6e1a1f23 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.359 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.360 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1177,6 +1177,7 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit) wp->window = w; wp->options = options_create(w->options); wp->flags = PANE_STYLECHANGED; + wp->cmd_status = -1; wp->id = next_window_pane_id++; RB_INSERT(window_pane_tree, &all_window_panes, wp); From 0cc84b8cae70099b80eafd44afb9d624103a453a Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 10 Jul 2026 15:45:11 +0000 Subject: [PATCH 103/127] Do not make pty fds blocking again until all the data has been consumed or control mode clients can get stuck, GitHub issue 5356 from Ben Maurer. --- client.c | 27 ++++++++++----------------- control.c | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ tmux.h | 1 + 3 files changed, 60 insertions(+), 17 deletions(-) diff --git a/client.c b/client.c index fcc49c395..b315f9083 100644 --- a/client.c +++ b/client.c @@ -240,9 +240,8 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, pid_t ppid; enum msgtype msg; struct termios tio, saved_tio; - size_t size, linesize = 0; - ssize_t linelen; - char *line = NULL, **caps = NULL, *cause; + size_t size; + char **caps = NULL, *cause; u_int ncaps = 0; struct args_value *values; @@ -400,11 +399,6 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, client_exec(client_execshell, client_execcmd); } - /* Restore streams to blocking. */ - setblocking(STDIN_FILENO, 1); - setblocking(STDOUT_FILENO, 1); - setblocking(STDERR_FILENO, 1); - /* Print the exit message, if any, and exit. */ if (client_attached) { if (client_exitreason != CLIENT_EXIT_NONE) @@ -419,15 +413,8 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, else printf("%%exit\n"); fflush(stdout); - if (client_flags & CLIENT_CONTROL_WAITEXIT) { - setvbuf(stdin, NULL, _IOLBF, 0); - for (;;) { - linelen = getline(&line, &linesize, stdin); - if (linelen <= 1) - break; - } - free(line); - } + if (client_flags & CLIENT_CONTROL_WAITEXIT) + control_wait_exit(STDIN_FILENO); if (client_flags & CLIENT_CONTROLCONTROL) { printf("\033\\"); fflush(stdout); @@ -435,6 +422,12 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, } } else if (client_exitreason != CLIENT_EXIT_NONE) fprintf(stderr, "%s\n", client_exit_message()); + + /* Restore the streams to blocking. */ + setblocking(STDIN_FILENO, 1); + setblocking(STDOUT_FILENO, 1); + setblocking(STDERR_FILENO, 1); + return (client_exitval); } diff --git a/control.c b/control.c index c1cc79e25..a177c8b88 100644 --- a/control.c +++ b/control.c @@ -19,7 +19,9 @@ #include +#include #include +#include #include #include #include @@ -486,6 +488,53 @@ control_all_done(struct client *c) return (EVBUFFER_LENGTH(cs->write_event->output) == 0); } +/* + * Wait for the terminal to send an empty line or close, used by a control + * client after printing %exit so a wrapping terminal (such as iTerm2) can + * finish reading. + */ +void +control_wait_exit(int fd) +{ + struct pollfd pfd; + struct evbuffer *evb; + char *line; + int n; + + evb = evbuffer_new(); + if (evb == NULL) + fatalx("out of memory"); + + for (;;) { + line = evbuffer_readln(evb, NULL, EVBUFFER_EOL_LF); + if (line != NULL) { + if (*line == '\0') { /* empty line, stop */ + free(line); + break; + } + free(line); + continue; /* drain buffered lines first */ + } + + memset(&pfd, 0, sizeof pfd); + pfd.fd = fd; + pfd.events = POLLIN; + if (poll(&pfd, 1, INFTIM) == -1) { + if (errno == EINTR) + continue; + break; + } + + n = evbuffer_read(evb, fd, -1); + if (n == 0) + break; + if (n == -1 && errno != EAGAIN && errno != EINTR) + break; + } + + evbuffer_free(evb); +} + /* Flush all blocks until output. */ static void control_flush_all_blocks(struct client *c) diff --git a/tmux.h b/tmux.h index e698adf1b..b62e4ae23 100644 --- a/tmux.h +++ b/tmux.h @@ -3917,6 +3917,7 @@ void control_discard(struct client *); void control_start(struct client *); void control_ready(struct client *); void control_stop(struct client *); +void control_wait_exit(int); 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 *); From 7f2b0ae16efac76402055bd6a3b3b973f3eaa576 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 10 Jul 2026 15:45:11 +0000 Subject: [PATCH 104/127] Do not make pty fds blocking again until all the data has been consumed or control mode clients can get stuck, GitHub issue 5356 from Ben Maurer. --- client.c | 29 +++++++++++------------------ control.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- tmux.h | 3 ++- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/client.c b/client.c index 1b7fdb043..f413dc819 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: client.c,v 1.165 2025/04/25 12:25:32 nicm Exp $ */ +/* $OpenBSD: client.c,v 1.166 2026/07/10 15:45:11 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -240,9 +240,8 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, pid_t ppid; enum msgtype msg; struct termios tio, saved_tio; - size_t size, linesize = 0; - ssize_t linelen; - char *line = NULL, **caps = NULL, *cause; + size_t size; + char **caps = NULL, *cause; u_int ncaps = 0; struct args_value *values; @@ -400,11 +399,6 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, client_exec(client_execshell, client_execcmd); } - /* Restore streams to blocking. */ - setblocking(STDIN_FILENO, 1); - setblocking(STDOUT_FILENO, 1); - setblocking(STDERR_FILENO, 1); - /* Print the exit message, if any, and exit. */ if (client_attached) { if (client_exitreason != CLIENT_EXIT_NONE) @@ -419,15 +413,8 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, else printf("%%exit\n"); fflush(stdout); - if (client_flags & CLIENT_CONTROL_WAITEXIT) { - setvbuf(stdin, NULL, _IOLBF, 0); - for (;;) { - linelen = getline(&line, &linesize, stdin); - if (linelen <= 1) - break; - } - free(line); - } + if (client_flags & CLIENT_CONTROL_WAITEXIT) + control_wait_exit(STDIN_FILENO); if (client_flags & CLIENT_CONTROLCONTROL) { printf("\033\\"); fflush(stdout); @@ -435,6 +422,12 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, } } else if (client_exitreason != CLIENT_EXIT_NONE) fprintf(stderr, "%s\n", client_exit_message()); + + /* Restore the streams to blocking. */ + setblocking(STDIN_FILENO, 1); + setblocking(STDOUT_FILENO, 1); + setblocking(STDERR_FILENO, 1); + return (client_exitval); } diff --git a/control.c b/control.c index 220267b6c..8b4ad3884 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.60 2026/07/10 07:25:05 nicm Exp $ */ +/* $OpenBSD: control.c,v 1.61 2026/07/10 15:45:11 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -19,7 +19,9 @@ #include +#include #include +#include #include #include #include @@ -486,6 +488,53 @@ control_all_done(struct client *c) return (EVBUFFER_LENGTH(cs->write_event->output) == 0); } +/* + * Wait for the terminal to send an empty line or close, used by a control + * client after printing %exit so a wrapping terminal (such as iTerm2) can + * finish reading. + */ +void +control_wait_exit(int fd) +{ + struct pollfd pfd; + struct evbuffer *evb; + char *line; + int n; + + evb = evbuffer_new(); + if (evb == NULL) + fatalx("out of memory"); + + for (;;) { + line = evbuffer_readln(evb, NULL, EVBUFFER_EOL_LF); + if (line != NULL) { + if (*line == '\0') { /* empty line, stop */ + free(line); + break; + } + free(line); + continue; /* drain buffered lines first */ + } + + memset(&pfd, 0, sizeof pfd); + pfd.fd = fd; + pfd.events = POLLIN; + if (poll(&pfd, 1, INFTIM) == -1) { + if (errno == EINTR) + continue; + break; + } + + n = evbuffer_read(evb, fd, -1); + if (n == 0) + break; + if (n == -1 && errno != EAGAIN && errno != EINTR) + break; + } + + evbuffer_free(evb); +} + /* Flush all blocks until output. */ static void control_flush_all_blocks(struct client *c) diff --git a/tmux.h b/tmux.h index b3a3a9796..5031a581f 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1395 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1396 2026/07/10 15:45:11 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3917,6 +3917,7 @@ void control_discard(struct client *); void control_start(struct client *); void control_ready(struct client *); void control_stop(struct client *); +void control_wait_exit(int); 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 *); From e7fef1fd64e368da28309c7c55ba14d6ab413d0b Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Fri, 10 Jul 2026 16:45:58 +0100 Subject: [PATCH 105/127] Test for wait exit, GitHub issue 5356 from Ben Maurer. --- regress/control-client-wait-exit.sh | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 regress/control-client-wait-exit.sh diff --git a/regress/control-client-wait-exit.sh b/regress/control-client-wait-exit.sh new file mode 100644 index 000000000..63739b284 --- /dev/null +++ b/regress/control-client-wait-exit.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +# A control client with the wait-exit flag lingers after %exit until its input +# sends an empty line or closes. The terminating empty line must be honoured +# even when several lines arrive in a single read (as from a pipe or a raw +# terminal): reading input with stdio would pull them all into the FILE buffer +# and leave the empty line where poll never sees it, hanging the client. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest" +$TMUX kill-server 2>/dev/null + +FIFO=$(mktemp -u) +OUT=$(mktemp) +mkfifo "$FIFO" || exit 1 +trap "$TMUX kill-server 2>/dev/null; rm -f $FIFO $OUT" 0 1 15 + +# Start a control client that reads its input from the fifo. Keep the write +# end open (fd 3) so the client never sees EOF: it must exit on the empty +# line, not on the pipe closing. +$TMUX -f/dev/null -C new -s wait-exit <"$FIFO" >"$OUT" 2>&1 & +CLIENT=$! +exec 3>"$FIFO" +sleep 1 + +# Ask to linger after exit, then detach so the client prints %exit and enters +# the wait-exit loop. +printf 'refresh-client -f wait-exit\n' >&3 +sleep 1 +$TMUX detach-client -s wait-exit + +# Wait for the client to print %exit and enter the wait-exit loop. +i=0 +while [ $i -lt 5 ]; do + grep -q '^%exit' "$OUT" && break + sleep 1 + i=$((i + 1)) +done +grep -q '^%exit' "$OUT" || exit 1 + +# Deliver the terminating empty line in one write, after a non-empty line, so +# a single read returns "a\n" and "\n" together. The empty line must still end +# the wait. +printf 'a\n\n' >&3 + +# The client should exit promptly. If it is still alive after the timeout the +# empty line was lost in a buffer and the client has hung. +i=0 +while [ $i -lt 10 ]; do + kill -0 $CLIENT 2>/dev/null || break + sleep 1 + i=$((i + 1)) +done +if kill -0 $CLIENT 2>/dev/null; then + kill $CLIENT 2>/dev/null + exit 1 +fi +exec 3>&- + +exit 0 From 6fd99876326ccfe2b35573a0694c25ed8acb702a Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sat, 11 Jul 2026 04:37:04 +0100 Subject: [PATCH 106/127] Revert "Add formats and events for OSC 133 commmands, as well as a -T flag to" This reverts commit f3c6b4f1a35026c38b59d5db7021c5c4f44e32d3. --- cmd-set-option.c | 70 +------ format.c | 121 ------------- hooks.c | 461 ----------------------------------------------- input.c | 110 ++--------- monitor.c | 13 +- options-table.c | 12 -- tmux.1 | 122 ++----------- tmux.h | 134 ++++---------- window.c | 157 +++------------- 9 files changed, 93 insertions(+), 1107 deletions(-) delete mode 100644 hooks.c diff --git a/cmd-set-option.c b/cmd-set-option.c index 1d95bddc9..1b6de1e86 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -31,8 +31,6 @@ static enum args_parse_type cmd_set_option_args_parse(struct args *, u_int, char **); static enum cmd_retval cmd_set_option_exec(struct cmd *, struct cmdq_item *); -static enum cmd_retval cmd_set_hook_event_exec(struct cmd *, - struct cmdq_item *); static enum cmd_retval cmd_set_hook_monitor_exec(struct cmdq_item *, struct args *, int); @@ -66,8 +64,8 @@ const struct cmd_entry cmd_set_hook_entry = { .name = "set-hook", .alias = NULL, - .args = { "agpERTt:uB:w", 0, 2, cmd_set_option_args_parse }, - .usage = "[-agpERTuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " + .args = { "agpRt:uB:w", 0, 2, cmd_set_option_args_parse }, + .usage = "[-agpRuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " "[hook] [command]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -87,63 +85,17 @@ cmd_set_option_args_parse(struct args *args, u_int idx, return (ARGS_PARSE_STRING); } -static enum cmd_retval -cmd_set_hook_event_exec(struct cmd *self, struct cmdq_item *item) -{ - struct args *args = cmd_get_args(self); - struct cmd_find_state *target = cmdq_get_target(item); - struct event_payload *ep; - struct client *c; - char *argument; - - if (args_count(args) == 0) { - cmdq_error(item, "missing argument"); - return (CMD_RETURN_ERROR); - } - if (args_count(args) != 1) { - cmdq_error(item, "too many arguments"); - return (CMD_RETURN_ERROR); - } - - argument = format_single_from_target(item, args_string(args, 0)); - if (*argument != '@') { - cmdq_error(item, "event name must start with @"); - free(argument); - return (CMD_RETURN_ERROR); - } - - ep = event_payload_create(); - event_payload_set_target(ep, target); - c = cmdq_get_client(item); - if (c != NULL) - event_payload_set_client(ep, "client", c); - if (target->s != NULL) - event_payload_set_session(ep, "session", target->s); - if (target->w != NULL) - event_payload_set_window(ep, "window", target->w); - if (target->wl != NULL) - event_payload_set_int(ep, "window_index", target->wl->idx); - else if (target->idx != -1) - event_payload_set_int(ep, "window_index", target->idx); - if (target->wp != NULL) - event_payload_set_pane(ep, "pane", target->wp); - events_fire(argument, ep); - free(argument); - return (CMD_RETURN_NORMAL); -} - static enum cmd_retval cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) { struct cmd_find_state *target = cmdq_get_target(item), fs; struct options *oo; struct options_entry *o; - struct session *s = NULL; char *cause = NULL, *name = NULL, *format = NULL; char *expanded = NULL, *newvalue = NULL; const char *value, *old; enum monitor_type type; - int id, scope, flags = 0; + int id, scope; if (args_count(args) > 1) { cmdq_error(item, "too many arguments"); @@ -178,7 +130,7 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) cmd_find_copy_state(&fs, target); if (args_has(args, 'u')) { - hooks_monitor_remove(oo, name); + notify_monitor_remove(oo, name); goto out; } @@ -200,13 +152,7 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) } } - if (oo != global_options && - oo != global_s_options && - oo != global_w_options) - s = target->s; - if (args_has(args, 'T')) - flags |= MONITOR_NOTIFY_TRUE; - hooks_monitor_add(item, oo, name, type, id, format, flags, &fs, s); + notify_monitor_add(item, oo, name, type, id, format, &fs, target->s); out: free(newvalue); @@ -239,8 +185,6 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) int scope; window = (cmd_get_entry(self) == &cmd_set_window_option_entry); - if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'E')) - return (cmd_set_hook_event_exec(self, item)); if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'B')) return (cmd_set_hook_monitor_exec(item, args, window)); if (args_count(args) == 0) { @@ -253,7 +197,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) /* If set-hook -R, fire the hook straight away. */ if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'R')) { - hooks_run(item, argument); + notify_hook(item, argument); free(argument); return (CMD_RETURN_NORMAL); } @@ -345,8 +289,6 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) goto fail; } options_set_string(oo, name, append, "%s", value); - if (cmd_get_entry(self) == &cmd_set_hook_entry) - hooks_add_event(name); } else if (array_key == NULL && !options_is_array(parent)) { error = options_from_string(oo, options_table_entry(parent), options_table_entry(parent)->name, value, diff --git a/format.c b/format.c index 912f3a9db..a911fed59 100644 --- a/format.c +++ b/format.c @@ -2187,106 +2187,6 @@ format_cb_pane_dead_time(struct format_tree *ft) return (NULL); } -/* Callback for pane_last_output_time. */ -static void * -format_cb_pane_last_output_time(struct format_tree *ft) -{ - struct window_pane *wp = ft->wp; - static struct timeval tv; - - if (wp != NULL && wp->last_output_time != 0) { - tv.tv_sec = wp->last_output_time; - tv.tv_usec = 0; - return (&tv); - } - return (NULL); -} - -/* Callback for pane_last_prompt_time. */ -static void * -format_cb_pane_last_prompt_time(struct format_tree *ft) -{ - struct window_pane *wp = ft->wp; - static struct timeval tv; - - if (wp != NULL && wp->last_prompt_time != 0) { - tv.tv_sec = wp->last_prompt_time; - tv.tv_usec = 0; - return (&tv); - } - return (NULL); -} - -/* Callback for pane_command_start_time. */ -static void * -format_cb_pane_command_start_time(struct format_tree *ft) -{ - struct window_pane *wp = ft->wp; - static struct timeval tv; - - if (wp != NULL && wp->cmd_start_time != 0) { - tv.tv_sec = wp->cmd_start_time; - tv.tv_usec = 0; - return (&tv); - } - return (NULL); -} - -/* Callback for pane_command_end_time. */ -static void * -format_cb_pane_command_end_time(struct format_tree *ft) -{ - struct window_pane *wp = ft->wp; - static struct timeval tv; - - if (wp != NULL && wp->cmd_end_time != 0) { - tv.tv_sec = wp->cmd_end_time; - tv.tv_usec = 0; - return (&tv); - } - return (NULL); -} - -/* Callback for pane_command_running. */ -static void * -format_cb_pane_command_running(struct format_tree *ft) -{ - struct window_pane *wp = ft->wp; - - if (wp != NULL) - return (format_printf("%d", !!(wp->flags & PANE_CMDRUNNING))); - return (NULL); -} - -/* Callback for pane_command_duration. */ -static void * -format_cb_pane_command_duration(struct format_tree *ft) -{ - struct window_pane *wp = ft->wp; - time_t end; - - if (wp == NULL || wp->cmd_start_time == 0) - return (NULL); - if (wp->flags & PANE_CMDRUNNING) - end = time(NULL); - else - end = wp->cmd_end_time; - if (end < wp->cmd_start_time) - end = wp->cmd_start_time; - return (format_printf("%lld", (long long)(end - wp->cmd_start_time))); -} - -/* Callback for pane_command_status. */ -static void * -format_cb_pane_command_status(struct format_tree *ft) -{ - struct window_pane *wp = ft->wp; - - if (wp != NULL && wp->cmd_status != -1) - return (format_printf("%d", wp->cmd_status)); - return (NULL); -} - /* Callback for pane_format. */ static void * format_cb_pane_format(struct format_tree *ft) @@ -3603,21 +3503,6 @@ static const struct format_table_entry format_table[] = { { "pane_bottom", FORMAT_TABLE_STRING, format_cb_pane_bottom }, - { "pane_command_duration", FORMAT_TABLE_STRING, - format_cb_pane_command_duration - }, - { "pane_command_end_time", FORMAT_TABLE_TIME, - format_cb_pane_command_end_time - }, - { "pane_command_running", FORMAT_TABLE_STRING, - format_cb_pane_command_running - }, - { "pane_command_start_time", FORMAT_TABLE_TIME, - format_cb_pane_command_start_time - }, - { "pane_command_status", FORMAT_TABLE_STRING, - format_cb_pane_command_status - }, { "pane_current_command", FORMAT_TABLE_STRING, format_cb_current_command }, @@ -3669,12 +3554,6 @@ static const struct format_table_entry format_table[] = { { "pane_last", FORMAT_TABLE_STRING, format_cb_pane_last }, - { "pane_last_output_time", FORMAT_TABLE_TIME, - format_cb_pane_last_output_time - }, - { "pane_last_prompt_time", FORMAT_TABLE_TIME, - format_cb_pane_last_prompt_time - }, { "pane_left", FORMAT_TABLE_STRING, format_cb_pane_left }, diff --git a/hooks.c b/hooks.c deleted file mode 100644 index 097b768b4..000000000 --- a/hooks.c +++ /dev/null @@ -1,461 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2026 Nicholas Marriott - * Copyright (c) 2012 George Nachman - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include -#include - -#include "tmux.h" - -/* Hook monitor state owned by an option entry. */ -struct hook_monitor { - struct options *oo; - - struct monitor_set *set; - struct events_sink *sink; - struct cmd_find_state fs; - - enum monitor_type type; - int id; - char *format; -}; - -/* Hook command data built from an event payload. */ -struct hooks_data { - const char *name; - struct cmd_find_state fs; - struct format_tree *formats; - struct options *oo; - struct client *client; - int expand; -}; - -/* Hook event sink registered for a notify event name. */ -struct hooks_event { - char *name; - struct events_sink *sink; - TAILQ_ENTRY(hooks_event) entry; -}; -TAILQ_HEAD(hooks_events, hooks_event); -static struct hooks_events hooks_events = TAILQ_HEAD_INITIALIZER(hooks_events); - -/* Insert one hook command list. */ -static struct cmdq_item * -hooks_insert_one(struct cmdq_item *item, struct hooks_data *hd, - struct cmd_list *cmdlist, struct cmdq_state *state) -{ - struct cmdq_item *new_item; - char *s; - - if (cmdlist == NULL) - return (item); - if (log_get_level() != 0) { - s = cmd_list_print(cmdlist, 0); - log_debug("%s: hook %s is: %s", __func__, hd->name, s); - free(s); - } - new_item = cmdq_get_command(cmdlist, state); - if (item != NULL) - return (cmdq_insert_after(item, new_item)); - return (cmdq_append(NULL, new_item)); -} - -/* Parse a hook command. */ -static struct cmd_parse_result * -hooks_parse(struct hooks_data *hd, struct cmd_find_state *fs, - const char *value) -{ - struct cmd_parse_result *pr; - struct format_tree *ft; - char *expanded; - - if (!hd->expand) - return (cmd_parse_from_string(value, NULL)); - - ft = format_create_defaults(NULL, hd->client, fs->s, fs->wl, fs->wp); - if (hd->formats != NULL) - format_merge(ft, hd->formats); - expanded = format_expand(ft, value); - format_free(ft); - - pr = cmd_parse_from_string(expanded, NULL); - free(expanded); - return (pr); -} - -/* Insert commands for a hook. */ -static void -hooks_insert(struct cmdq_item *item, struct hooks_data *hd) -{ - struct cmd_find_state fs; - struct options *oo; - struct cmdq_state *state; - struct options_entry *o; - struct options_array_item *a; - struct cmd_list *cmdlist; - const char *value; - struct cmd_parse_result *pr; - - log_debug("%s: inserting hook %s", __func__, hd->name); - - cmd_find_clear_state(&fs, 0); - if (cmd_find_empty_state(&hd->fs) || !cmd_find_valid_state(&hd->fs)) - cmd_find_from_nothing(&fs, 0); - else - cmd_find_copy_state(&fs, &hd->fs); - - if (hd->oo != NULL) { - oo = hd->oo; - o = options_get_only(oo, hd->name); - } else { - if (fs.s == NULL) - oo = global_s_options; - else - oo = fs.s->options; - o = options_get(oo, hd->name); - if (o == NULL && fs.wp != NULL) { - oo = fs.wp->options; - o = options_get(oo, hd->name); - } - if (o == NULL && fs.wl != NULL) { - oo = fs.wl->window->options; - o = options_get(oo, hd->name); - } - } - if (o == NULL) { - log_debug("%s: hook %s not found", __func__, hd->name); - return; - } - - if (item == NULL) - state = cmdq_new_state(&fs, NULL, CMDQ_STATE_NOHOOKS); - else { - state = cmdq_new_state(&fs, cmdq_get_event(item), - CMDQ_STATE_NOHOOKS); - } - cmdq_add_formats(state, hd->formats); - - if (*hd->name == '@') { - value = options_get_string(oo, hd->name); - pr = hooks_parse(hd, &fs, value); - switch (pr->status) { - case CMD_PARSE_ERROR: - log_debug("%s: can't parse hook %s: %s", __func__, - hd->name, pr->error); - free(pr->error); - break; - case CMD_PARSE_SUCCESS: - hooks_insert_one(item, hd, pr->cmdlist, state); - break; - } - } else { - a = options_array_first(o); - while (a != NULL) { - if (hd->expand) { - value = options_array_item_value(a)->string; - pr = hooks_parse(hd, &fs, value); - switch (pr->status) { - case CMD_PARSE_ERROR: - if (pr->error != NULL) { - cmdq_error(item, "%s", - pr->error); - } - break; - case CMD_PARSE_SUCCESS: - item = hooks_insert_one(item, hd, - pr->cmdlist, state); - break; - } - } else { - cmdlist = options_array_item_value(a)->cmdlist; - item = hooks_insert_one(item, hd, cmdlist, - state); - } - a = options_array_next(a); - } - } - - cmdq_free_state(state); -} - -/* Insert commands for a hook event. */ -static void -hooks_insert_event(struct cmdq_item *item, const char *name, - struct event_payload *ep, struct options *oo, int expand) -{ - struct hooks_data hd; - struct format_tree *ft; - struct client *c; - - if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) - return; - - c = event_payload_get_client(ep, "client"); - ft = format_create(c, item, FORMAT_NONE, FORMAT_NOJOBS); - event_payload_add_formats(ep, ft, "hook_"); - format_add(ft, "hook", "%s", name); - format_log_debug(ft, __func__); - - memset(&hd, 0, sizeof hd); - hd.name = name; - cmd_find_clear_state(&hd.fs, 0); - event_payload_get_target(ep, &hd.fs); - hd.formats = ft; - hd.oo = oo; - hd.client = c; - hd.expand = expand; - - hooks_insert(item, &hd); - format_free(ft); -} - -/* Handle an event for hooks. */ -static void -hooks_event_cb(const char *name, struct event_payload *ep, - __unused void *sink_data) -{ - struct cmdq_item *item; - - if (event_payload_get_pointer(ep, "_hook_monitor") != NULL) - return; - - item = event_payload_get_pointer(ep, "_cmdq_item"); - if (item != NULL) { - hooks_insert_event(item, name, ep, NULL, 0); - return; - } - - item = cmdq_running(NULL); - if (item == NULL || (~cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) - hooks_insert_event(NULL, name, ep, NULL, 0); -} - -/* Add a hook event sink. */ -void -hooks_add_event(const char *name) -{ - struct hooks_event *he; - - TAILQ_FOREACH(he, &hooks_events, entry) { - if (strcmp(he->name, name) == 0) - return; - } - - he = xcalloc(1, sizeof *he); - he->name = xstrdup(name); - he->sink = events_add_sink(name, hooks_event_cb, NULL); - TAILQ_INSERT_TAIL(&hooks_events, he, entry); -} - -/* Return if an event name can be fired through the hooks path. */ -int -hooks_valid_event_name(const char *name) -{ - const struct options_table_entry *oe; - - if (*name == '@') - return (1); - oe = options_search(name); - return (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)); -} - -/* Add hook event sinks for all built-in hooks. */ -void -hooks_build_events(void) -{ - const struct options_table_entry *oe; - - for (oe = options_table; oe->name != NULL; oe++) { - if (oe->flags & OPTIONS_TABLE_IS_HOOK) - hooks_add_event(oe->name); - } -} - -/* Run a hook immediately. */ -void -hooks_run(struct cmdq_item *item, const char *name) -{ - struct cmd_find_state *target = cmdq_get_target(item); - struct hooks_data hd = { 0 }; - - hd.name = name; - cmd_find_copy_state(&hd.fs, target); - hd.client = cmdq_get_client(item); - - hd.formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS); - format_add(hd.formats, "hook", "%s", name); - format_log_debug(hd.formats, __func__); - - hooks_insert(item, &hd); - format_free(hd.formats); -} - -/* Free a hook monitor. */ -void -hooks_monitor_free(void *data) -{ - struct hook_monitor *hm = data; - - events_remove_sink(hm->sink); - monitor_destroy(hm->set); - free(hm->format); - free(hm); -} - -/* Remove a hook monitor. */ -void -hooks_monitor_remove(struct options *oo, const char *name) -{ - struct options_entry *o; - struct hook_monitor *hm; - - o = options_get_only(oo, name); - if (o == NULL) - return; - - hm = options_get_monitor_data(o); - if (hm != NULL) { - options_set_monitor_data(o, NULL); - hooks_monitor_free(hm); - } -} - -/* Handle a hook monitor event. */ -static void -hooks_monitor_hook_cb(const char *name, struct event_payload *ep, - void *sink_data) -{ - struct hook_monitor *hm = sink_data; - - if (event_payload_get_pointer(ep, "_hook_monitor") == hm) - hooks_insert_event(cmdq_running(NULL), name, ep, hm->oo, 1); -} - -/* Fire a hook monitor event. */ -static void -hooks_monitor_cb(struct monitor_change *change, void *data) -{ - struct hook_monitor *hm = data; - struct event_payload *ep; - struct winlink *wl = change->wl; - struct window_pane *wp = change->wp; - struct cmd_find_state fs; - - ep = event_payload_create(); - event_payload_set_pointer(ep, "_hook_monitor", data, NULL, NULL); - - cmd_find_clear_state(&fs, 0); - if (wl != NULL && wp != NULL && wp->window == wl->window) - cmd_find_from_winlink_pane(&fs, wl, wp, 0); - else if (wl != NULL) - cmd_find_from_winlink(&fs, wl, 0); - else if (wp != NULL) - cmd_find_from_pane(&fs, wp, 0); - else if (change->s != NULL) - cmd_find_from_session(&fs, change->s, 0); - else - cmd_find_copy_state(&fs, &hm->fs); - event_payload_set_target(ep, &fs); - - if (change->value != NULL) - event_payload_set_string(ep, "value", "%s", change->value); - else - event_payload_set_string(ep, "value", "%s", ""); - if (change->last != NULL) - event_payload_set_string(ep, "last", "%s", change->last); - else - event_payload_set_string(ep, "last", "%s", ""); - - if (change->c != NULL) - event_payload_set_client(ep, "client", change->c); - if (change->s != NULL) - event_payload_set_session(ep, "session", change->s); - if (wl != NULL) { - if (change->s == NULL) - event_payload_set_session(ep, "session", - wl->session); - event_payload_set_window(ep, "window", wl->window); - event_payload_set_int(ep, "window_index", wl->idx); - } - if (wp != NULL) { - event_payload_set_pane(ep, "pane", wp); - if (wl == NULL) - event_payload_set_window(ep, "window", wp->window); - } - - events_fire(change->name, ep); -} - -/* Add a hook monitor. */ -void -hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, - const char *name, enum monitor_type type, int id, const char *format, - int flags, struct cmd_find_state *fs, struct session *s) -{ - struct options_entry *o; - struct hook_monitor *hm; - - hooks_monitor_remove(oo, name); - o = options_get_only(oo, name); - if (o == NULL) - o = options_set_string(oo, name, 0, "%s", ""); - - hm = xcalloc(1, sizeof *hm); - hm->oo = oo; - cmd_find_copy_state(&hm->fs, fs); - hm->type = type; - hm->id = id; - hm->format = xstrdup(format); - hm->set = monitor_create_session(s, hooks_monitor_cb, hm); - hm->sink = events_add_sink(name, hooks_monitor_hook_cb, hm); - options_set_monitor_data(o, hm); - monitor_add(hm->set, name, type, id, format, flags); -} - -/* Convert a hook monitor to its value. */ -char * -hooks_monitor_to_string(struct options_entry *o) -{ - struct hook_monitor *hm = options_get_monitor_data(o); - const char *name = options_name(o); - char *s; - - if (hm == NULL) - return (NULL); - - switch (hm->type) { - case MONITOR_SESSION: - xasprintf(&s, "%s::%s", name, hm->format); - break; - case MONITOR_PANE: - xasprintf(&s, "%s:%%%d:%s", name, hm->id, hm->format); - break; - case MONITOR_ALL_PANES: - xasprintf(&s, "%s:%%*:%s", name, hm->format); - break; - case MONITOR_WINDOW: - xasprintf(&s, "%s:@%d:%s", name, hm->id, hm->format); - break; - case MONITOR_ALL_WINDOWS: - xasprintf(&s, "%s:@*:%s", name, hm->format); - break; - } - return (s); -} diff --git a/input.c b/input.c index 8e6184825..a85b555f8 100644 --- a/input.c +++ b/input.c @@ -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; @@ -1034,8 +1017,6 @@ input_parse_pane(struct window_pane *wp) size_t new_size; new_data = window_pane_get_new_data(wp, &wp->offset, &new_size); - if (new_size != 0) - wp->last_output_time = time(NULL); input_parse_buffer(wp, new_data, new_size); window_pane_update_used_data(wp, &wp->offset, new_size); } @@ -2199,7 +2180,7 @@ input_csi_dispatch_winops(struct input_ctx *ictx) screen_pop_title(sctx->s); if (wp == NULL) break; - events_fire_pane("pane-title-changed", wp); + notify_pane("pane-title-changed", wp); server_redraw_window_borders(w); server_status_window(w); break; @@ -2719,7 +2700,7 @@ input_exit_osc(struct input_ctx *ictx) if (wp != NULL && options_get_number(wp->options, "allow-set-title") && screen_set_title(sctx->s, p, 1)) { - input_fire_pane_title_changed(wp, p); + notify_pane("pane-title-changed", wp); server_redraw_window_borders(wp->window); server_status_window(wp->window); } @@ -2797,7 +2778,7 @@ input_exit_apc(struct input_ctx *ictx) if (wp != NULL && options_get_number(wp->options, "allow-set-title") && screen_set_title(sctx->s, ictx->input_buf, 1)) { - input_fire_pane_title_changed(wp, ictx->input_buf); + notify_pane("pane-title-changed", wp); server_redraw_window_borders(wp->window); server_status_window(wp->window); } @@ -3165,8 +3146,7 @@ input_osc_12(struct input_ctx *ictx, const char *p) c = ictx->ctx.s->ccolour; if (c == -1) c = ictx->ctx.s->default_ccolour; - input_osc_colour_reply(ictx, 1, 12, 0, c, - ictx->input_end); + input_osc_colour_reply(ictx, 1, 12, 0, c, ictx->input_end); } return; } @@ -3186,92 +3166,24 @@ input_osc_112(struct input_ctx *ictx, const char *p) screen_set_cursor_colour(ictx->ctx.s, -1); } -/* Fire an OSC 133 command event. */ -static void -input_fire_command_event(struct window_pane *wp, const char *name) -{ - struct event_payload *ep; - struct cmd_find_state fs; - time_t tstart = wp->cmd_start_time, end; - time_t tend = wp->cmd_end_time; - - ep = event_payload_create(); - cmd_find_from_pane(&fs, wp, 0); - event_payload_set_target(ep, &fs); - if (fs.s != NULL) - event_payload_set_session(ep, "session", fs.s); - if (fs.wl != NULL) - event_payload_set_int(ep, "window_index", fs.wl->idx); - event_payload_set_window(ep, "window", wp->window); - event_payload_set_pane(ep, "pane", wp); - - if (wp->cmd_status != -1) - event_payload_set_int(ep, "command_status", wp->cmd_status); - if (tstart != 0) - event_payload_set_time(ep, "command_start_time", tstart); - if (tend != 0) - event_payload_set_time(ep, "command_end_time", tend); - - if (tstart != 0) { - if (wp->flags & PANE_CMDRUNNING) - end = time(NULL); - else - end = tend; - if (end < tstart) - end = tstart; - end -= tstart; - event_payload_set_uint(ep, "command_duration", end); - } - - events_fire(name, ep); -} - /* Handle the OSC 133 sequence. */ 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 grid_line *gl = NULL; - const char *errstr; - int status; + struct grid_line *gl; - if (line <= gd->hsize + gd->sy - 1) - gl = grid_get_line(gd, line); + if (line > gd->hsize + gd->sy - 1) + return; + gl = grid_get_line(gd, line); switch (*p) { case 'A': - if (gl != NULL) - gl->flags |= GRID_LINE_START_PROMPT; - if (wp != NULL) { - wp->last_prompt_time = time(NULL); - events_fire_pane("pane-shell-prompt", wp); - } + gl->flags |= GRID_LINE_START_PROMPT; break; case 'C': - if (gl != NULL) - gl->flags |= GRID_LINE_START_OUTPUT; - if (wp != NULL) { - wp->cmd_start_time = time(NULL); - wp->cmd_end_time = 0; - wp->flags |= PANE_CMDRUNNING; - wp->cmd_status = -1; - input_fire_command_event(wp, "pane-command-started"); - } - break; - case 'D': - 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; - } - input_fire_command_event(wp, "pane-command-finished"); - } + gl->flags |= GRID_LINE_START_OUTPUT; break; } } @@ -3377,7 +3289,7 @@ input_osc_52(struct input_ctx *ictx, const char *p) screen_write_start_pane(&ctx, wp, NULL); screen_write_setselection(&ctx, clip, out, outlen); screen_write_stop(&ctx); - events_fire_pane("pane-set-clipboard", wp); + notify_pane("pane-set-clipboard", wp); paste_add(NULL, out, outlen); } } diff --git a/monitor.c b/monitor.c index 114f7b1da..6d1a747fd 100644 --- a/monitor.c +++ b/monitor.c @@ -1,7 +1,7 @@ /* $OpenBSD$ */ /* - * Copyright (c) 2026 Nicholas Marriott + * Copyright (c) 2026 Nicholas Marriott * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -53,7 +53,7 @@ struct monitor_item { enum monitor_type type; u_int id; - int flags; + u_int flags; char *last; struct monitor_panes panes; @@ -198,9 +198,7 @@ monitor_check_value(struct monitor_set *ms, struct monitor_item *me, { if (*last == NULL) { *last = value; - if ((me->flags & MONITOR_NOTIFY_INITIAL) && - ((~me->flags & MONITOR_NOTIFY_TRUE) || - format_true(value))) + if (me->flags & MONITOR_NOTIFY_INITIAL) monitor_report(ms, me, s, wl, wp, value, NULL); return; } @@ -210,8 +208,7 @@ monitor_check_value(struct monitor_set *ms, struct monitor_item *me, return; } - if ((~me->flags & MONITOR_NOTIFY_TRUE) || format_true(value)) - monitor_report(ms, me, s, wl, wp, value, *last); + monitor_report(ms, me, s, wl, wp, value, *last); free(*last); *last = value; } @@ -624,7 +621,7 @@ fail: /* Add a subscription. */ void monitor_add(struct monitor_set *ms, const char *name, enum monitor_type type, - int id, const char *format, int flags) + int id, const char *format, u_int flags) { struct monitor_item *me, find = { .name = (char *)name }; struct timeval tv = { .tv_sec = 1 }; diff --git a/options-table.c b/options-table.c index 9441a427a..afcfea18a 100644 --- a/options-table.c +++ b/options-table.c @@ -1932,22 +1932,12 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("client-light-theme", ""), OPTIONS_TABLE_HOOK("client-dark-theme", ""), OPTIONS_TABLE_HOOK("command-error", ""), - OPTIONS_TABLE_HOOK("marked-pane-changed", ""), - OPTIONS_TABLE_PANE_HOOK("pane-command-finished", ""), - OPTIONS_TABLE_PANE_HOOK("pane-command-started", ""), - OPTIONS_TABLE_PANE_HOOK("pane-created", ""), OPTIONS_TABLE_PANE_HOOK("pane-died", ""), OPTIONS_TABLE_PANE_HOOK("pane-exited", ""), OPTIONS_TABLE_PANE_HOOK("pane-focus-in", ""), OPTIONS_TABLE_PANE_HOOK("pane-focus-out", ""), OPTIONS_TABLE_PANE_HOOK("pane-mode-changed", ""), - OPTIONS_TABLE_PANE_HOOK("pane-mode-entered", ""), - OPTIONS_TABLE_PANE_HOOK("pane-mode-exited", ""), - OPTIONS_TABLE_PANE_HOOK("pane-prompt-closed", ""), - OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", ""), - OPTIONS_TABLE_PANE_HOOK("pane-resized", ""), OPTIONS_TABLE_PANE_HOOK("pane-set-clipboard", ""), - OPTIONS_TABLE_PANE_HOOK("pane-shell-prompt", ""), OPTIONS_TABLE_PANE_HOOK("pane-title-changed", ""), OPTIONS_TABLE_HOOK("session-closed", ""), OPTIONS_TABLE_HOOK("session-created", ""), @@ -1958,8 +1948,6 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_WINDOW_HOOK("window-pane-changed", ""), OPTIONS_TABLE_WINDOW_HOOK("window-renamed", ""), OPTIONS_TABLE_WINDOW_HOOK("window-resized", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-unzoomed", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-zoomed", ""), OPTIONS_TABLE_HOOK("window-unlinked", ""), { .name = NULL } diff --git a/tmux.1 b/tmux.1 index e4b708aaa..dfca7fe34 100644 --- a/tmux.1 +++ b/tmux.1 @@ -6325,7 +6325,11 @@ For example, this could be used to write to a log file: set\-hook \-g command\-error "run\-shell \\"echo 'a tmux command failed' >>/tmp/log\\"" .Ed .Pp -The following hooks are available in addition to after hooks: +All the notifications listed in the +.Sx CONTROL MODE +section are hooks (without any arguments), except +.Ic %exit . +The following additional hooks are available: .Bl -tag -width "XXXXXXXXXXXXXXXXXXXXXX" .It alert\-activity Run when a window has activity. @@ -6344,11 +6348,11 @@ Run when a client becomes the latest active client of its session. .It client\-attached Run when a client is attached. .It client\-detached -Run when a client is detached. +Run when a client is detached .It client\-focus\-in -Run when focus enters a client. +Run when focus enters a client .It client\-focus\-out -Run when focus exits a client. +Run when focus exits a client .It client\-resized Run when a client is resized. .It client\-session\-changed @@ -6359,21 +6363,6 @@ Run when a client switches to a light theme. Run when a client switches to a dark theme. .It command\-error Run when a command fails. -.It marked\-pane\-changed -Run when the marked pane is set or cleared. -.It pane\-command\-finished -Run when an OSC 133 command finishes in a pane. -The event payload includes -.Ql hook_command_start_time , -.Ql hook_command_end_time , -.Ql hook_command_duration -and -.Ql hook_command_status -if present. -.It pane\-command\-started -Run when an OSC 133 command starts in a pane. -.It pane\-created -Run when a pane is created or respawned. .It pane\-died Run when the program running in a pane exits, but .Ic remain\-on\-exit @@ -6388,40 +6377,20 @@ option is on. Run when the focus exits a pane, if the .Ic focus\-events option is on. -.It pane\-mode\-changed -Run when a pane changes mode. -.It pane\-mode\-entered -Run when a pane enters a mode. -.It pane\-mode\-exited -Run when a pane exits a mode. -.It pane\-prompt\-closed -Run when a prompt in a pane is closed. -.It pane\-prompt\-opened -Run when a prompt is opened in a pane. -.It pane\-resized -Run when a pane is resized. .It pane\-set\-clipboard Run when the terminal clipboard is set using the .Xr xterm 1 escape sequence. -.It pane\-shell\-prompt -Run when an OSC 133 shell prompt starts in a pane. -.It pane\-title\-changed -Run when a pane title is changed. -.It session\-closed -Run when a session is closed. .It session\-created -Run when a new session is created. +Run when a new session created. +.It session\-closed +Run when a session closed. .It session\-renamed Run when a session is renamed. -.It session\-window\-changed -Run when a session changes its active window. .It window\-layout\-changed Run when a window layout is changed. .It window\-linked Run when a window is linked into a session. -.It window\-pane\-changed -Run when a window changes its active pane. .It window\-renamed Run when a window is renamed. .It window\-resized @@ -6429,10 +6398,6 @@ Run when a window is resized. This may be after the .Ar client\-resized hook is run. -.It window\-unzoomed -Run when a window is unzoomed. -.It window\-zoomed -Run when a window is zoomed. .It window\-unlinked Run when a window is unlinked from a session. .El @@ -6440,15 +6405,13 @@ Run when a window is unlinked from a session. Hooks are managed with these commands: .Bl -tag -width Ds .It Xo Ic set\-hook -.Op Fl agpERTuw +.Op Fl agpRuw .Op Fl B Ar name:what:format .Op Fl t Ar target\-pane .Ar hook\-name .Op Ar command .Xc Without -.Fl E -or .Fl R , sets (or with .Fl u @@ -6471,11 +6434,6 @@ is the hook to run, selects the session, pane, all panes, window, or all windows, and .Ar format is expanded once a second. -With -.Fl T , -the hook is run only when -.Ar format -is true. For monitor hooks, .Ar name must begin with @@ -6494,14 +6452,6 @@ the subscription named by is removed. .Pp With -.Fl E , -fire the user event named -.Ar hook\-name . -.Ar hook\-name -must begin with -.Ql @ . -.Pp -With .Fl R , run .Ar hook\-name @@ -7211,11 +7161,6 @@ The following variables are available, where appropriate: .It Li "pane_at_top" Ta "" Ta "1 if pane is at the top of window" .It Li "pane_bg" Ta "" Ta "Pane background colour" .It Li "pane_bottom" Ta "" Ta "Bottom of pane" -.It Li "pane_command_duration" Ta "" Ta "Current or most recent OSC 133 command duration in seconds" -.It Li "pane_command_end_time" Ta "" Ta "Time most recent OSC 133 command ended" -.It Li "pane_command_running" Ta "" Ta "1 if an OSC 133 command is running" -.It Li "pane_command_start_time" Ta "" Ta "Time current or most recent OSC 133 command started" -.It Li "pane_command_status" Ta "" Ta "Exit status from most recent OSC 133 command" .It Li "pane_current_command" Ta "" Ta "Current command if available" .It Li "pane_current_path" Ta "" Ta "Current path if available" .It Li "pane_dead" Ta "" Ta "1 if pane is dead" @@ -7233,8 +7178,6 @@ The following variables are available, where appropriate: .It Li "pane_input_off" Ta "" Ta "1 if input to pane is disabled" .It Li "pane_key_mode" Ta "" Ta "Extended key reporting mode in this pane" .It Li "pane_last" Ta "" Ta "1 if last pane" -.It Li "pane_last_output_time" Ta "" Ta "Time pane last produced output" -.It Li "pane_last_prompt_time" Ta "" Ta "Time most recent OSC 133 prompt began" .It Li "pane_left" Ta "" Ta "Left of pane" .It Li "pane_marked" Ta "" Ta "1 if this is the marked pane" .It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set" @@ -8619,10 +8562,8 @@ or the current pane if omitted) after the command finishes. If the command fails, the exit status is also displayed. .Tg wait .It Xo Ic wait\-for -.Op Fl ELSUlv -.Op Fl F Ar format -.Op Fl w Ar waiter -.Ar name +.Op Fl L | S | U +.Ar channel .Xc .D1 Pq alias: Ic wait When used without options, prevents the client from exiting until woken using @@ -8635,41 +8576,6 @@ is used, the channel is locked and any clients that try to lock the same channel are made to wait until the channel is unlocked with .Ic wait\-for .Fl U . -.Pp -With -.Fl E , -.Nm -waits for the next event with -.Ar name . -Events include hook and notification names, and user -.Ql @ -events generated by -.Ic set-hook -.Fl E -or -.Ic set-hook -.Fl B . -If -.Fl F -is given, -.Ar format -must also be true. -If -.Fl v -is given, event payload keys are printed (whether or not -.Ar format -is true). -.Pp -.Fl l -list the waiters for -.Ar name -and -.Fl w -wakes -.Ar waiter -on -.Ar name -immediately. .El .Sh EXIT MESSAGES When a diff --git a/tmux.h b/tmux.h index 68202e4fc..ce4288350 100644 --- a/tmux.h +++ b/tmux.h @@ -49,9 +49,6 @@ struct cmdq_state; struct cmds; struct control_state; struct environ; -struct event_payload; -struct event_payload_item; -struct events_sink; struct format_job_tree; struct format_tree; struct hyperlinks_uri; @@ -1301,7 +1298,6 @@ struct window_pane { #define PANE_UNSEENCHANGES 0x4000 #define PANE_REDRAWSCROLLBAR 0x8000 #define PANE_DESTROYED 0x10000 -#define PANE_CMDRUNNING 0x20000 bitstr_t *sync_dirty; u_int sync_dirty_size; @@ -1324,12 +1320,6 @@ struct window_pane { struct cmdq_item *wait_item; /* new-pane -W: waiting for pane exit */ struct spawn_editor_state *editor; - time_t last_output_time; - time_t last_prompt_time; - time_t cmd_start_time; - time_t cmd_end_time; - int cmd_status; - int fd; struct bufferevent *event; @@ -2341,7 +2331,6 @@ enum monitor_type { MONITOR_ALL_WINDOWS }; #define MONITOR_NOTIFY_INITIAL 0x1 -#define MONITOR_NOTIFY_TRUE 0x2 struct monitor_change { const char *name; const char *value; @@ -2354,23 +2343,6 @@ struct monitor_change { }; typedef void (*monitor_cb)(struct monitor_change *, void *); -/* Event payload type. */ -enum event_payload_type { - EVENT_PAYLOAD_STRING, - EVENT_PAYLOAD_TIME, - EVENT_PAYLOAD_INT, - EVENT_PAYLOAD_UINT, - EVENT_PAYLOAD_CLIENT, - EVENT_PAYLOAD_SESSION, - EVENT_PAYLOAD_WINDOW, - EVENT_PAYLOAD_PANE, - EVENT_PAYLOAD_POINTER -}; - -/* Event payload callbacks. */ -typedef void (*event_payload_free_cb)(void *); -typedef void (*event_payload_print_cb)(void *, struct evbuffer *); - /* Key binding and key table. */ struct key_binding { key_code key; @@ -2680,78 +2652,29 @@ char *format_grid_hyperlink(struct grid *, u_int, u_int, struct screen *); char *format_grid_line(struct grid *, u_int); -/* events-payload.c */ -struct event_payload *event_payload_create(void); -void event_payload_free(struct event_payload *); -void printflike(2, 3) event_payload_log(struct event_payload *, const char *, - ...); -char *event_payload_item_print(struct event_payload_item *); -void event_payload_set_target(struct event_payload *, - struct cmd_find_state *); -int event_payload_get_target(struct event_payload *, - struct cmd_find_state *); -void printflike(3, 4) event_payload_set_string(struct event_payload *, - const char *, const char *, ...); -void event_payload_set_time(struct event_payload *, const char *, time_t); -void event_payload_set_int(struct event_payload *, const char *, int); -void event_payload_set_uint(struct event_payload *, const char *, u_int); -void event_payload_set_client(struct event_payload *, const char *, - struct client *); -void event_payload_set_session(struct event_payload *, const char *, - struct session *); -void event_payload_set_window(struct event_payload *, const char *, - struct window *); -void event_payload_set_pane(struct event_payload *, const char *, - struct window_pane *); -void event_payload_set_pointer(struct event_payload *, const char *, void *, - event_payload_free_cb, event_payload_print_cb); -const char *event_payload_get_string(struct event_payload *, const char *); -char *event_payload_print(struct event_payload *, const char *); -void event_payload_add_formats(struct event_payload *, - struct format_tree *, const char *); -struct event_payload_item *event_payload_first(struct event_payload *); -struct event_payload_item *event_payload_next(struct event_payload_item *); -const char *event_payload_item_name(struct event_payload_item *); -enum event_payload_type event_payload_item_type(struct event_payload_item *); -time_t event_payload_get_time(struct event_payload *, const char *); -int event_payload_get_int(struct event_payload *, const char *, int *); -int event_payload_get_uint(struct event_payload *, const char *, u_int *); -struct client *event_payload_get_client(struct event_payload *, const char *); -struct session *event_payload_get_session(struct event_payload *, const char *); -struct window *event_payload_get_window(struct event_payload *, const char *); -struct window_pane *event_payload_get_pane(struct event_payload *, - const char *); -void *event_payload_get_pointer(struct event_payload *, const char *); - -/* events.c */ -typedef void (*events_cb)(const char *, struct event_payload *, void *); -struct events_sink *events_add_sink(const char *, events_cb, void *); -void events_remove_sink(struct events_sink *); -void events_fire(const char *, struct event_payload *); -void events_fire_client(const char *, struct client *); -void events_fire_session(const char *, struct session *); -void events_fire_window(const char *, struct window *); -void events_fire_pane(const char *, struct window_pane *); -void events_fire_winlink(const char *, struct winlink *); - /* format-draw.c */ -void format_draw(struct screen_write_ctx *, const struct grid_cell *, - u_int, const char *, struct style_ranges *, int); -u_int format_width(const char *); -char *format_trim_left(const char *, u_int); -char *format_trim_right(const char *, u_int); +void format_draw(struct screen_write_ctx *, + const struct grid_cell *, u_int, const char *, + struct style_ranges *, int); +u_int format_width(const char *); +char *format_trim_left(const char *, u_int); +char *format_trim_right(const char *, u_int); -/* hooks.c */ -void hooks_add_event(const char *); -int hooks_valid_event_name(const char *); -void hooks_build_events(void); -void hooks_run(struct cmdq_item *, const char *); -void hooks_monitor_add(struct cmdq_item *, struct options *, +/* notify.c */ +void notify_hook(struct cmdq_item *, const char *); +void notify_monitor_add(struct cmdq_item *, struct options *, const char *, enum monitor_type, int, const char *, - int, struct cmd_find_state *, struct session *); -void hooks_monitor_remove(struct options *, const char *); -void hooks_monitor_free(void *); -char *hooks_monitor_to_string(struct options_entry *); + struct cmd_find_state *, struct session *); +void notify_monitor_remove(struct options *, const char *); +void notify_monitor_free(void *); +char *notify_monitor_to_string(struct options_entry *); +void notify_client(const char *, struct client *); +void notify_session(const char *, struct session *); +void notify_winlink(const char *, struct winlink *); +void notify_session_window(const char *, struct session *, struct window *); +void notify_window(const char *, struct window *); +void notify_pane(const char *, struct window_pane *); +void notify_paste_buffer(const char *, int); /* options.c */ struct options *options_create(struct options *); @@ -3959,7 +3882,7 @@ void monitor_destroy(struct monitor_set *); int monitor_parse(const char *, char **, enum monitor_type *, int *, char **); void monitor_add(struct monitor_set *, const char *, enum monitor_type, int, - const char *, int); + const char *, u_int); void monitor_remove(struct monitor_set *, const char *); /* control.c */ @@ -3983,7 +3906,20 @@ void control_add_sub(struct client *, const char *, enum monitor_type, int, void control_remove_sub(struct client *, const char *); /* control-notify.c */ -void control_build_events(void); +void control_notify_pane_mode_changed(int); +void control_notify_window_layout_changed(struct window *); +void control_notify_window_pane_changed(struct window *); +void control_notify_window_unlinked(struct session *, struct window *); +void control_notify_window_linked(struct session *, struct window *); +void control_notify_window_renamed(struct window *); +void control_notify_client_session_changed(struct client *); +void control_notify_client_detached(struct client *); +void control_notify_session_renamed(struct session *); +void control_notify_session_created(struct session *); +void control_notify_session_closed(struct session *); +void control_notify_session_window_changed(struct session *); +void control_notify_paste_buffer_changed(const char *); +void control_notify_paste_buffer_deleted(const char *); /* session.c */ extern struct sessions sessions; diff --git a/window.c b/window.c index 4b9c37d5a..298491604 100644 --- a/window.c +++ b/window.c @@ -73,8 +73,8 @@ static struct window_pane *window_pane_create(struct window *, u_int, u_int, static void window_pane_destroy(struct window_pane *); static void window_pane_free(struct window_pane *); static void window_pane_scrollbar_timer(int, short, void *); -static void window_pane_full_size_offset(struct window_pane *, int *, int *, - u_int *, u_int *); +static void window_pane_full_size_offset(struct window_pane *wp, + int *xoff, int *yoff, u_int *sx, u_int *sy); RB_GENERATE(windows, window, entry, window_cmp); RB_GENERATE(winlinks, winlink, entry, winlink_cmp); @@ -86,7 +86,6 @@ struct window_pane_prompt { status_prompt_input_cb inputcb; prompt_free_cb freecb; void *data; - enum prompt_type type; }; int @@ -95,78 +94,6 @@ window_cmp(struct window *w1, struct window *w2) return (w1->id - w2->id); } -static void -window_fire_renamed(struct window *w, const char *old_name) -{ - struct event_payload *ep; - struct cmd_find_state fs; - - ep = event_payload_create(); - cmd_find_from_window(&fs, w, 0); - event_payload_set_target(ep, &fs); - event_payload_set_window(ep, "window", w); - event_payload_set_string(ep, "old_name", "%s", old_name); - event_payload_set_string(ep, "new_name", "%s", w->name); - events_fire("window-renamed", ep); -} - -static void -window_fire_pane_changed(struct window *w, struct window_pane *wp, - struct window_pane *lastwp) -{ - 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_window(ep, "window", w); - event_payload_set_pane(ep, "pane", wp); - event_payload_set_pane(ep, "new_pane", wp); - if (lastwp != NULL) - event_payload_set_pane(ep, "old_pane", lastwp); - events_fire("window-pane-changed", ep); -} - -static void -window_fire_pane_mode_changed(const char *name, struct window_pane *wp, - const char *previous, const char *current, int entered) -{ - 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); - - if (current != NULL) - event_payload_set_string(ep, "current_mode", "%s", current); - if (previous != NULL) - event_payload_set_string(ep, "previous_mode", "%s", previous); - event_payload_set_int(ep, "mode_entered", entered); - - events_fire(name, ep); -} - -static void -window_fire_pane_prompt(const char *name, struct window_pane *wp, - enum prompt_type type) -{ - struct event_payload *ep; - struct cmd_find_state fs; - const char *type_string = prompt_type_string(type); - - 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, "prompt_type", "%s", type_string); - events_fire(name, ep); -} - int winlink_cmp(struct winlink *wl1, struct winlink *wl2) { @@ -517,15 +444,13 @@ window_pane_remove_ref(struct window_pane *wp, const char *from) void window_set_name(struct window *w, const char *new_name, int untrusted) { - char *last, *name; + char *name; name = clean_name(new_name, untrusted); if (name != NULL) { - last = xstrdup(w->name); free(w->name); w->name = name; - window_fire_renamed(w, last); - free(last); + notify_window("window-renamed", w); } } @@ -635,13 +560,13 @@ window_pane_update_focus(struct window_pane *wp) log_debug("%s: %%%u focus out", __func__, wp->id); if (wp->base.mode & MODE_FOCUSON) bufferevent_write(wp->event, "\033[O", 3); - events_fire_pane("pane-focus-out", wp); + notify_pane("pane-focus-out", wp); wp->flags &= ~PANE_FOCUSED; } else if (focused && (~wp->flags & PANE_FOCUSED)) { log_debug("%s: %%%u focus in", __func__, wp->id); if (wp->base.mode & MODE_FOCUSON) bufferevent_write(wp->event, "\033[I", 3); - events_fire_pane("pane-focus-in", wp); + notify_pane("pane-focus-in", wp); wp->flags |= PANE_FOCUSED; } else log_debug("%s: %%%u focus unchanged", __func__, wp->id); @@ -677,7 +602,7 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify) server_redraw_window(w); if (notify) - window_fire_pane_changed(w, w->active, lastwp); + notify_window("window-pane-changed", w); return (1); } @@ -867,8 +792,7 @@ window_zoom(struct window_pane *wp) w->saved_layout_root = w->layout_root; layout_init(w, wp); w->flags |= WINDOW_ZOOMED; - events_fire_window("window-zoomed", w); - events_fire_window("window-layout-changed", w); + notify_window("window-layout-changed", w); redraw_invalidate_scene(w); return (0); @@ -894,10 +818,8 @@ window_unzoom(struct window *w, int notify) } layout_fix_panes(w, NULL); - if (notify) { - events_fire_window("window-unzoomed", w); - events_fire_window("window-layout-changed", w); - } + if (notify) + notify_window("window-layout-changed", w); redraw_invalidate_scene(w); return (0); @@ -979,7 +901,7 @@ window_lost_pane(struct window *w, struct window_pane *wp) if (w->active != NULL) { window_pane_stack_remove(&w->last_panes, w->active); w->active->flags |= PANE_CHANGED; - events_fire_window("window-pane-changed", w); + notify_window("window-pane-changed", w); window_update_focus(w); } } @@ -1184,7 +1106,6 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit) wp->window = w; wp->options = options_create(w->options); wp->flags = PANE_STYLECHANGED; - wp->cmd_status = -1; wp->id = next_window_pane_id++; RB_INSERT(window_pane_tree, &all_window_panes, wp); @@ -1309,9 +1230,9 @@ window_pane_destroy(struct window_pane *wp) window_pane_wait_finish(wp); spawn_editor_finish(wp); + window_pane_clear_prompt(wp); RB_REMOVE(window_pane_tree, &all_window_panes, wp); wp->flags |= PANE_DESTROYED; - window_pane_clear_prompt(wp); window_pane_free_modes(wp); screen_write_clear_dirty(wp); @@ -1442,8 +1363,6 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) { struct window_mode_entry *wme; struct window_pane_resize *r; - struct event_payload *ep; - struct cmd_find_state fs; if (sx == wp->sx && sy == wp->sy) return; @@ -1466,17 +1385,6 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) wme = TAILQ_FIRST(&wp->modes); if (wme != NULL && wme->mode->resize != NULL) wme->mode->resize(wme, sx, sy); - - 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_uint(ep, "width", sx); - event_payload_set_uint(ep, "height", sy); - event_payload_set_uint(ep, "old_width", r->osx); - event_payload_set_uint(ep, "old_height", r->osy); - events_fire("pane-resized", ep); } int @@ -1486,13 +1394,9 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, { struct window_mode_entry *wme; struct window *w = wp->window; - const char *name = mode->name, *p = NULL; - if (!TAILQ_EMPTY(&wp->modes)) { - if (TAILQ_FIRST(&wp->modes)->mode == mode) - return (1); - p = TAILQ_FIRST(&wp->modes)->mode->name; - } + if (!TAILQ_EMPTY(&wp->modes) && TAILQ_FIRST(&wp->modes)->mode == mode) + return (1); TAILQ_FOREACH(wme, &wp->modes, entry) { if (wme->mode == mode) @@ -1518,9 +1422,7 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, server_redraw_window_borders(wp->window); server_status_window(wp->window); - - window_fire_pane_mode_changed("pane-mode-entered", wp, p, name, 1); - window_fire_pane_mode_changed("pane-mode-changed", wp, p, name, 1); + notify_pane("pane-mode-changed", wp); return (0); } @@ -1531,13 +1433,11 @@ window_pane_reset_mode(struct window_pane *wp) struct window_mode_entry *wme, *next; struct window *w = wp->window; int kill; - const char *name, *p; if (TAILQ_EMPTY(&wp->modes)) return; wme = TAILQ_FIRST(&wp->modes); - p = wme->mode->name; kill = wme->kill; TAILQ_REMOVE(&wp->modes, wme, entry); wme->mode->free(wme); @@ -1554,16 +1454,13 @@ window_pane_reset_mode(struct window_pane *wp) if (next->mode->resize != NULL) next->mode->resize(next, wp->sx, wp->sy); } - name = (next == NULL ? NULL : next->mode->name); wp->flags |= (PANE_REDRAW|PANE_REDRAWSCROLLBAR|PANE_CHANGED); layout_fix_panes(w, NULL); server_redraw_window_borders(wp->window); server_status_window(wp->window); - - window_fire_pane_mode_changed("pane-mode-exited", wp, p, name, 0); - window_fire_pane_mode_changed("pane-mode-changed", wp, p, name, 0); + notify_pane("pane-mode-changed", wp); if (kill) server_kill_pane(wp); @@ -1626,7 +1523,6 @@ window_pane_set_prompt(struct window_pane *wp, struct client *c, wpp->inputcb = inputcb; wpp->freecb = freecb; wpp->data = data; - wpp->type = type; memset(&pd, 0, sizeof pd); prompt_set_options(&pd, s); @@ -1644,28 +1540,19 @@ window_pane_set_prompt(struct window_pane *wp, struct client *c, wp->flags |= PANE_REDRAW; prompt_incremental_start(wp->prompt); - window_fire_pane_prompt("pane-prompt-opened", wp, type); } /* Close a pane prompt. */ void window_pane_clear_prompt(struct window_pane *wp) { - struct prompt *prompt = wp->prompt; - struct window_pane_prompt *wpp = wp->prompt_data; - enum prompt_type type = PROMPT_TYPE_INVALID; + struct prompt *prompt = wp->prompt; - if (prompt != NULL) { - if (wpp != NULL) - type = wpp->type; - - wp->prompt = NULL; - prompt_free(prompt); - wp->flags |= PANE_REDRAW; - - if (~wp->flags & PANE_DESTROYED) - window_fire_pane_prompt("pane-prompt-closed", wp, type); - } + if (prompt == NULL) + return; + wp->prompt = NULL; + prompt_free(prompt); + wp->flags |= PANE_REDRAW; } /* Does this pane have an open prompt? */ From 28a1231100b3e589400af8f3e5d2ff349e7b61e5 Mon Sep 17 00:00:00 2001 From: Yayo Razo Date: Sat, 11 Jul 2026 15:53:08 -0600 Subject: [PATCH 107/127] fix(screen): free alternate-screen images on screen destroy --- screen.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/screen.c b/screen.c index 62a079270..9f5177241 100644 --- a/screen.c +++ b/screen.c @@ -152,6 +152,10 @@ screen_reset_hyperlinks(struct screen *s) void screen_free(struct screen *s) { +#ifdef ENABLE_SIXEL + struct image *im; +#endif + free(s->sel); free(s->tabs); free(s->path); @@ -169,6 +173,14 @@ screen_free(struct screen *s) screen_free_titles(s); #ifdef ENABLE_SIXEL + /* + * Images saved when entering the alternate screen stay linked in the + * global list; move them back so they are freed and unlinked here, or + * a later eviction would write through this freed screen. + */ + TAILQ_CONCAT(&s->images, &s->saved_images, entry); + TAILQ_FOREACH(im, &s->images, entry) + im->list = &s->images; image_free_all(s); #endif } From 3ff24a8eb452272b72d627fb25fba38a6790b5ba Mon Sep 17 00:00:00 2001 From: Yayo Razo Date: Sat, 11 Jul 2026 14:22:48 -0600 Subject: [PATCH 108/127] fix(compat): guard buffer size multiplication in getline --- compat/getline.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/compat/getline.c b/compat/getline.c index 90437ccb9..feabdd43c 100644 --- a/compat/getline.c +++ b/compat/getline.c @@ -74,8 +74,14 @@ getdelim(char **buf, size_t *bufsiz, int delimiter, FILE *fp) } if (ptr + 2 >= eptr) { char *nbuf; - size_t nbufsiz = *bufsiz * 2; + size_t nbufsiz; ssize_t d = ptr - *buf; + + if (*bufsiz > SIZE_MAX / 2) { + errno = EOVERFLOW; + return -1; + } + nbufsiz = *bufsiz * 2; if ((nbuf = realloc(*buf, nbufsiz)) == NULL) return -1; *buf = nbuf; From 330bedfb47b114d69d0ca21c2802cacb295de08d Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 12 Jul 2026 13:26:25 +0100 Subject: [PATCH 109/127] Build with ASAN on macOS by default. --- Makefile.am | 3 ++- configure.ac | 2 +- grid.c | 3 +++ tmux.c | 18 ++++++++++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index d985e510d..84bdbdb2a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -43,7 +43,8 @@ AM_CPPFLAGS += -DDEBUG endif AM_CPPFLAGS += -iquote. if IS_ASAN -AM_CFLAGS += -fsanitize=address +AM_CPPFLAGS += -DASAN +AM_CFLAGS += -fsanitize=address -fno-omit-frame-pointer AM_LDFLAGS += -fsanitize=address endif endif diff --git a/configure.ac b/configure.ac index 6abb2d9d5..c118800b8 100644 --- a/configure.ac +++ b/configure.ac @@ -78,7 +78,7 @@ AC_ARG_ENABLE( asan, AS_HELP_STRING(--enable-asan, enable ASAN build flags), , - enable_asan=no + [case "x$host_os" in *darwin*) enable_asan=yes;; esac] ) AM_CONDITIONAL(IS_ASAN, test "x$enable_asan" = xyes) diff --git a/grid.c b/grid.c index d03352f08..99d5371a9 100644 --- a/grid.c +++ b/grid.c @@ -366,6 +366,9 @@ grid_create(u_int sx, u_int sy, u_int hlimit) if (gd->sy != 0) gd->linedata = xcalloc(gd->sy, sizeof *gd->linedata); +#ifdef __APPLE__ + assert(gd->hsize == 0); +#endif grid_check_is_clear(gd); return (gd); } diff --git a/tmux.c b/tmux.c index 132c39217..3739b6008 100644 --- a/tmux.c +++ b/tmux.c @@ -49,6 +49,24 @@ static char *make_label(const char *, char **); static int areshell(const char *); static const char *getshell(void); +#ifdef ASAN +__attribute__((used)) const char *__asan_default_options(void); +__attribute__((used)) const char * +__asan_default_options(void) +{ + return ( + "abort_on_error=1:" + "halt_on_error=1:" + "detect_leaks=0:" + "detect_stack_use_after_return=1:" + "strict_string_checks=1:" + "check_initialization_order=1:" + "log_path=/tmp/tmux-asan:" + "log_exe_name=1" + ); +} +#endif + static __dead void usage(int status) { From b602dc57b1cdca30bb359f5a28c7e3840cbcd6a5 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Sun, 12 Jul 2026 17:24:37 +0100 Subject: [PATCH 110/127] github actions: automate updates --- .../workflows/update-openbsd-and-portable.yml | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 .github/workflows/update-openbsd-and-portable.yml diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml new file mode 100644 index 000000000..a9e3e4e1b --- /dev/null +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -0,0 +1,165 @@ +name: Update OpenBSD tmux and portable + +on: + workflow_dispatch: + schedule: + - cron: "17 3 * * *" + +permissions: + contents: write + +concurrency: + group: update-openbsd-and-portable + cancel-in-progress: false + +env: + OPENBSD_SRC_URL: https://github.com/openbsd/src.git + OPENBSD_SRC_DIR: openbsd-src + CUTOVER_DIR: tmux-openbsd-cutover + PORTABLE_DIR: tmux-portable + PORTABLE_REPO: ${{ vars.TMUX_PORTABLE_REPO }} + +jobs: + update: + runs-on: ubuntu-latest + steps: + - name: Configure Git identity + run: | + git config --global user.name "tmux update bot" + git config --global user.email "actions@github.com" + + - name: Install git-filter-repo + run: python3 -m pip install --user git-filter-repo + + - name: Checkout cutover repository + uses: actions/checkout@v4 + with: + path: ${{ env.CUTOVER_DIR }} + fetch-depth: 0 + token: ${{ secrets.UPDATE_TOKEN || github.token }} + + - name: Check portable repo setting + run: | + if [ -z "$PORTABLE_REPO" ]; then + echo "Set repository variable TMUX_PORTABLE_REPO, for example owner/tmux-portable." >&2 + exit 1 + fi + + - name: Checkout portable repository + uses: actions/checkout@v4 + with: + repository: ${{ env.PORTABLE_REPO }} + path: ${{ env.PORTABLE_DIR }} + fetch-depth: 0 + token: ${{ secrets.UPDATE_TOKEN }} + + - name: Restore OpenBSD source clone cache + id: openbsd-cache + uses: actions/cache/restore@v4 + with: + path: ${{ env.OPENBSD_SRC_DIR }} + key: openbsd-src-blobless-${{ runner.os }}-${{ github.run_id }} + restore-keys: | + openbsd-src-blobless-${{ runner.os }}- + + - name: Refresh OpenBSD source clone + run: | + set -eu + + if [ ! -d "$OPENBSD_SRC_DIR/.git" ]; then + git clone --filter=blob:none --no-tags --single-branch --branch master \ + "$OPENBSD_SRC_URL" "$OPENBSD_SRC_DIR" + fi + + cd "$OPENBSD_SRC_DIR" + git remote set-url origin "$OPENBSD_SRC_URL" + git fetch --filter=blob:none --no-tags origin master + git switch -C master origin/master + + - name: Filter OpenBSD tmux history + run: | + set -eu + + cd "$OPENBSD_SRC_DIR" + git switch -C tmux-openbsd origin/master + git filter-repo \ + --force \ + --refs tmux-openbsd \ + --path usr.bin/tmux/ \ + --path-rename usr.bin/tmux/: \ + --replace-refs delete-no-add + + git push "../$CUTOVER_DIR" tmux-openbsd:refs/heads/openbsd-git + + - name: Update cutover master + id: cutover + run: | + set -eu + + cd "$CUTOVER_DIR" + git switch master + + base_ref=refs/openbsd/import-base + if ! git rev-parse --verify -q "$base_ref" >/dev/null; then + echo "$base_ref is missing; initialise it before running updates." >&2 + exit 1 + fi + + old_base=$(git rev-parse "$base_ref") + new_base=$(git rev-parse openbsd-git) + + if [ "$old_base" = "$new_base" ]; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No new OpenBSD tmux commits to import." + exit 0 + fi + + if ! git merge-base --is-ancestor "$old_base" "$new_base"; then + echo "openbsd-git is not a descendant of $base_ref; aborting." >&2 + exit 1 + fi + + git cherry-pick "$old_base..$new_base" + git update-ref "$base_ref" "$new_base" + echo "changed=true" >> "$GITHUB_OUTPUT" + + - name: Push cutover repository + if: steps.cutover.outputs.changed == 'true' + run: | + set -eu + + cd "$CUTOVER_DIR" + git push origin master openbsd-git + + - name: Merge cutover into portable + if: steps.cutover.outputs.changed == 'true' + run: | + set -eu + + cd "$PORTABLE_DIR" + git config remote.tmux-openbsd.tagOpt --no-tags + + if git remote get-url tmux-openbsd >/dev/null 2>&1; then + git remote set-url tmux-openbsd "../$CUTOVER_DIR" + else + git remote add tmux-openbsd "../$CUTOVER_DIR" + fi + + git fetch --no-tags tmux-openbsd + git switch master + git merge --no-ff --log tmux-openbsd/master + + - name: Push portable repository + if: steps.cutover.outputs.changed == 'true' + run: | + set -eu + + cd "$PORTABLE_DIR" + git push origin master + + - name: Save OpenBSD source clone cache + if: always() + uses: actions/cache/save@v4 + with: + path: ${{ env.OPENBSD_SRC_DIR }} + key: openbsd-src-blobless-${{ runner.os }}-${{ github.run_id }} From 4f981a25066ef95a5795397aa5a70b8fc7ef0d08 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 12 Jul 2026 20:59:11 +0100 Subject: [PATCH 111/127] Turn on ASAN for macOS regress. --- .github/workflows/regress.yml | 2 +- regress/Makefile | 3 +-- regress/format-variables.sh | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 96cee88b4..050acc072 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -33,7 +33,7 @@ jobs: - name: macos-26-arm64 runner: macos-26 make: gmake - configure: --enable-utf8proc + configure: --enable-utf8proc --enable-asan steps: - name: checkout diff --git a/regress/Makefile b/regress/Makefile index 6370c77c9..7a4387bd3 100644 --- a/regress/Makefile +++ b/regress/Makefile @@ -14,8 +14,7 @@ all: rm -f "$$log"; \ printf '%-40s ' "$$test"; \ start=$$(date +%s); \ - ASAN_OPTIONS="abort_on_error=1:detect_leaks=0:$$ASAN_OPTIONS"; \ - env -i LC_CTYPE=C.UTF-8 ASAN_OPTIONS="$$ASAN_OPTIONS" \ + env -i LC_CTYPE=C.UTF-8 MallocNanoZone=0 \ sh -x "$$test" >"$$log" 2>&1; \ if [ $$? -eq 0 ]; then \ end=$$(date +%s); \ diff --git a/regress/format-variables.sh b/regress/format-variables.sh index 9131c2cf4..449712e34 100644 --- a/regress/format-variables.sh +++ b/regress/format-variables.sh @@ -296,7 +296,7 @@ sleep 1 # write end of a FIFO open, so client_* variables have a client to read. rm -f "$FIFO" mkfifo "$FIFO" || exit 1 -sleep 30 >"$FIFO" & +while :; do sleep 1; done >"$FIFO" & HOLD=$! $TMUX -C attach -t cov <"$FIFO" >/dev/null 2>&1 & CC=$! From 5e3db12c07b23cb99cf04df49922fc0854e81407 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 12 Jul 2026 21:02:30 +0100 Subject: [PATCH 112/127] No need for ASAN_OPTIONS. --- .github/workflows/regress.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 050acc072..8f6633624 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -75,7 +75,6 @@ jobs: - name: test run: | cd regress - export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0" ${{ matrix.make }} - name: logs From 8ac3fcedcaa61bc70a47aebdc50d503034ff54e5 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 12 Jul 2026 22:03:16 +0100 Subject: [PATCH 113/127] Do not reuse sockets so much to avoid problems with slow server shutdown. --- regress/command-order.sh | 6 +++--- regress/conf-syntax.sh | 7 +++++-- regress/control-client-size.sh | 7 ++++--- regress/new-session-size.sh | 6 +++--- regress/screen-redraw-popups.sh | 21 +++++++++++++++------ 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/regress/command-order.sh b/regress/command-order.sh index 9c957246a..cddd9b29e 100644 --- a/regress/command-order.sh +++ b/regress/command-order.sh @@ -4,11 +4,10 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" -$TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-1 -f/dev/null" TMP=$(mktemp) -trap "rm -f $TMP" 0 1 15 +trap 'rm -f "$TMP"; $TMUX kill-server 2>/dev/null' 0 1 15 cat <$TMP new -sfoo -nfoo0; neww -nfoo1; neww -nfoo2 @@ -27,6 +26,7 @@ foo,foo1 foo,foo2 EOF +TMUX="$TEST_TMUX -LtestA$$-2 -f/dev/null" cat <$TMP new -sfoo -nfoo0 neww -nfoo1 diff --git a/regress/conf-syntax.sh b/regress/conf-syntax.sh index 4dbc384b3..562ac76ad 100644 --- a/regress/conf-syntax.sh +++ b/regress/conf-syntax.sh @@ -4,10 +4,13 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" -$TMUX kill-server 2>/dev/null +TMUX= +n=0 +trap '[ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null' 0 1 15 for i in conf/*.conf; do + n=$((n + 1)) + TMUX="$TEST_TMUX -LtestA$$-$n -f/dev/null" $TMUX -f/dev/null start \; source -n $i || exit 1 done diff --git a/regress/control-client-size.sh b/regress/control-client-size.sh index e6742608a..3099b873f 100644 --- a/regress/control-client-size.sh +++ b/regress/control-client-size.sh @@ -8,12 +8,11 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" -$TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-1 -f/dev/null" TMP=$(mktemp) OUT=$(mktemp) -trap "rm -f $TMP $OUT" 0 1 15 +trap 'rm -f "$TMP" "$OUT"; $TMUX kill-server 2>/dev/null' 0 1 15 $TMUX -f/dev/null new -d || exit 1 sleep 1 @@ -26,6 +25,7 @@ $TMUX ls -F':#{window_width} #{window_height}' >>$OUT printf ":80 24\n:100 50\n"|cmp -s $OUT - || exit 1 $TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-2 -f/dev/null" $TMUX -f/dev/null new -d || exit 1 sleep 1 cat <$TMP @@ -37,6 +37,7 @@ $TMUX ls -F':#{window_width} #{window_height}' >>$OUT printf ":80 24\n:80 24\n"|cmp -s $OUT - || exit 1 $TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-3 -f/dev/null" cat <$TMP ls -F':#{window_width} #{window_height}' refresh -C 80,24 diff --git a/regress/new-session-size.sh b/regress/new-session-size.sh index 6a35abbbd..422ac69fb 100644 --- a/regress/new-session-size.sh +++ b/regress/new-session-size.sh @@ -6,11 +6,10 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" -$TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-1 -f/dev/null" TMP=$(mktemp) -trap "rm -f $TMP" 0 1 15 +trap 'rm -f "$TMP"; $TMUX kill-server 2>/dev/null' 0 1 15 $TMUX -f/dev/null new -d $TMP printf "80 24\n"|cmp -s $TMP - || exit 1 $TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-2 -f/dev/null" $TMUX -f/dev/null new -d -x 100 -y 50 $TMP diff --git a/regress/screen-redraw-popups.sh b/regress/screen-redraw-popups.sh index 8ad3ba2eb..26e0f7175 100644 --- a/regress/screen-redraw-popups.sh +++ b/regress/screen-redraw-popups.sh @@ -17,13 +17,19 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" -TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" +TMUX= +TMUX2= +SETUP=0 RESULTS=screen-redraw-results TMP=$(mktemp) -trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" \ - 0 1 15 + +cleanup() { + rm -f "$TMP" + [ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null + [ -n "$TMUX2" ] && $TMUX2 kill-server 2>/dev/null +} +trap cleanup 0 1 15 fail() { echo "$*" >&2 @@ -46,8 +52,11 @@ C="sh -c 'i=0; while [ \$i -lt 13 ]; do printf \"POP%02d abcdefghij\n\" \$i; i=\ # setup: fresh inner window attached inside a fresh outer pane, 40x14. setup() { - $TMUX kill-server 2>/dev/null - $TMUX2 kill-server 2>/dev/null + [ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null + [ -n "$TMUX2" ] && $TMUX2 kill-server 2>/dev/null + SETUP=$((SETUP + 1)) + TMUX="$TEST_TMUX -LtestA$$-$SETUP -f/dev/null" + TMUX2="$TEST_TMUX -LtestB$$-$SETUP -f/dev/null" $TMUX2 new -d -x40 -y14 "$C" || exit 1 $TMUX2 set -g status off || exit 1 $TMUX2 set -g window-size manual || exit 1 From d77c9dc6aa021e4bc61f0da128c591af695e6466 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 12 Jul 2026 23:18:47 +0100 Subject: [PATCH 114/127] Tweak BIDI test. --- regress/screen-redraw-bidi.sh | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/regress/screen-redraw-bidi.sh b/regress/screen-redraw-bidi.sh index ee9c2fa16..aa8d21627 100644 --- a/regress/screen-redraw-bidi.sh +++ b/regress/screen-redraw-bidi.sh @@ -42,6 +42,25 @@ compare() { fi } +wait_for_pane() { + i=0 + while [ $i -lt 50 ]; do + case "$($TMUX2 capturep -p -t "$1")" in + *"$2"*) return ;; + esac + i=$((i + 1)) + sleep 0.1 + done + fail "pane $1 did not contain: $2" +} + +attach_scene() { + # Start with a fresh outer screen and attach only after the complete + # scene exists, so it receives one initial redraw. + $TMUX respawnp -k "$TMUX2 attach" || exit 1 + sleep 1 +} + new_scene() { $TMUX2 neww -d "sh -c 'printf \"BIDI-LEFT 12345\nBIDI-RIGHT 67890\"; exec sleep 100'" || exit 1 $TMUX2 selectw -t:\$ || exit 1 @@ -63,17 +82,19 @@ $TMUX new -d -x40 -y12 || exit 1 $TMUX set -g status off || exit 1 $TMUX set -g window-size manual || exit 1 $TMUX set -g default-terminal "tmux-256color" || exit 1 -$TMUX send -l "$TMUX2 attach" || exit 1 -$TMUX send Enter || exit 1 -sleep 1 # Single pane: content is wrapped in isolates. new_scene +wait_for_pane ':.0' 'BIDI-RIGHT 67890' +attach_scene compare bidi-single # Two panes: borders and both panes are isolated. new_scene $TMUX2 splitw -h "$C" || exit 1 +wait_for_pane ':.0' 'BIDI-RIGHT 67890' +wait_for_pane ':.1' 'BIDI-PANE 67890' +attach_scene compare bidi-split exit 0 From a662981f106b8c2ee9e4669f4134177a9f4966b3 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 07:08:05 +0100 Subject: [PATCH 115/127] Bump a time limit. --- regress/screen-redraw-tiled.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/regress/screen-redraw-tiled.sh b/regress/screen-redraw-tiled.sh index 3b32478f0..de7c57aff 100644 --- a/regress/screen-redraw-tiled.sh +++ b/regress/screen-redraw-tiled.sh @@ -49,16 +49,17 @@ compare() { fi } +C="sh -c 'exec sleep 1000'" + # Fresh inner window of fixed size, with one command running. new_scene() { $TMUX2 selectw -t:0 || exit 1 $TMUX2 kill-window -a 2>/dev/null - $TMUX2 neww -d "sh -c 'exec sleep 100'" || exit 1 + $TMUX2 neww -d "$C" || exit 1 $TMUX2 selectw -t:\$ || exit 1 $TMUX2 resizew -x40 -y14 || exit 1 } -C="sh -c 'exec sleep 100'" # Layouts. Each produces one kind of junction. Splits at fixed window size are # deterministic. From cc8e0f2f67cc8727bafc1dfde4c4387d7a11937c Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 08:50:49 +0100 Subject: [PATCH 116/127] actions: use published openbsd-git as base update --- .github/workflows/update-openbsd-and-portable.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml index a9e3e4e1b..f79d17545 100644 --- a/.github/workflows/update-openbsd-and-portable.yml +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -99,13 +99,12 @@ jobs: cd "$CUTOVER_DIR" git switch master - base_ref=refs/openbsd/import-base - if ! git rev-parse --verify -q "$base_ref" >/dev/null; then - echo "$base_ref is missing; initialise it before running updates." >&2 + if ! git rev-parse --verify -q origin/openbsd-git >/dev/null; then + echo "origin/openbsd-git is missing; publish the openbsd-git branch before running updates." >&2 exit 1 fi - old_base=$(git rev-parse "$base_ref") + old_base=$(git rev-parse origin/openbsd-git) new_base=$(git rev-parse openbsd-git) if [ "$old_base" = "$new_base" ]; then @@ -115,12 +114,11 @@ jobs: fi if ! git merge-base --is-ancestor "$old_base" "$new_base"; then - echo "openbsd-git is not a descendant of $base_ref; aborting." >&2 + echo "new openbsd-git is not a descendant of origin/openbsd-git; aborting." >&2 exit 1 fi git cherry-pick "$old_base..$new_base" - git update-ref "$base_ref" "$new_base" echo "changed=true" >> "$GITHUB_OUTPUT" - name: Push cutover repository From 3612a9a605f494f52ea316e00d23336165103d74 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 12 Jul 2026 20:16:20 +0000 Subject: [PATCH 117/127] Add missing calloc check and use fatal. Prompted by a similar change from Yayo Razo. --- xmalloc.c | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/xmalloc.c b/xmalloc.c index b60b9dbee..c6a2f6f13 100644 --- a/xmalloc.c +++ b/xmalloc.c @@ -1,4 +1,4 @@ -/* $OpenBSD: xmalloc.c,v 1.14 2026/06/18 10:56:22 nicm Exp $ */ +/* $OpenBSD: xmalloc.c,v 1.15 2026/07/12 20:16:20 nicm Exp $ */ /* * Author: Tatu Ylonen @@ -32,8 +32,7 @@ xmalloc(size_t size) fatalx("xmalloc: zero size"); ptr = malloc(size); if (ptr == NULL) - fatalx("xmalloc: allocating %zu bytes: %s", - size, strerror(errno)); + fatal("xmalloc: allocating %zu bytes", size); return ptr; } @@ -44,10 +43,11 @@ xcalloc(size_t nmemb, size_t size) if (size == 0 || nmemb == 0) fatalx("xcalloc: zero size"); + if (SIZE_MAX / nmemb < size) + fatalx("xcalloc: nmemb * size > SIZE_MAX"); ptr = calloc(nmemb, size); if (ptr == NULL) - fatalx("xcalloc: allocating %zu * %zu bytes: %s", - nmemb, size, strerror(errno)); + fatal("xcalloc: allocating %zu bytes", size * nmemb); return ptr; } @@ -66,8 +66,7 @@ xreallocarray(void *ptr, size_t nmemb, size_t size) fatalx("xreallocarray: zero size"); new_ptr = reallocarray(ptr, nmemb, size); if (new_ptr == NULL) - fatalx("xreallocarray: allocating %zu * %zu bytes: %s", - nmemb, size, strerror(errno)); + fatal("xreallocarray: allocating %zu bytes", size * nmemb); return new_ptr; } @@ -80,8 +79,7 @@ xrecallocarray(void *ptr, size_t oldnmemb, size_t nmemb, size_t size) fatalx("xrecallocarray: zero size"); new_ptr = recallocarray(ptr, oldnmemb, nmemb, size); if (new_ptr == NULL) - fatalx("xrecallocarray: allocating %zu * %zu bytes: %s", - nmemb, size, strerror(errno)); + fatal("xrecallocarray: allocating %zu bytes", size * nmemb); return new_ptr; } @@ -91,7 +89,7 @@ xstrdup(const char *str) char *cp; if ((cp = strdup(str)) == NULL) - fatalx("xstrdup: %s", strerror(errno)); + fatal("xstrdup"); return cp; } @@ -101,7 +99,7 @@ xstrndup(const char *str, size_t maxlen) char *cp; if ((cp = strndup(str, maxlen)) == NULL) - fatalx("xstrndup: %s", strerror(errno)); + fatal("xstrndup"); return cp; } @@ -126,7 +124,6 @@ xasprintf(char **ret, const char *fmt, ...) va_start(ap, fmt); i = xvasprintf(ret, fmt, ap); va_end(ap); - return i; } @@ -136,10 +133,8 @@ xvasprintf(char **ret, const char *fmt, va_list ap) int i; i = vasprintf(ret, fmt, ap); - - if (i == -1) - fatalx("xasprintf: %s", strerror(errno)); - + if (i == -1 || *ret == NULL) + fatal("xvasprintf"); return i; } @@ -152,7 +147,6 @@ xsnprintf(char *str, size_t len, const char *fmt, ...) va_start(ap, fmt); i = xvsnprintf(str, len, fmt, ap); va_end(ap); - return i; } @@ -163,11 +157,8 @@ xvsnprintf(char *str, size_t len, const char *fmt, va_list ap) if (len > INT_MAX) fatalx("xsnprintf: len > INT_MAX"); - i = vsnprintf(str, len, fmt, ap); - if (i < 0 || i >= (int)len) fatalx("xsnprintf: overflow"); - return i; } From 0925c9846b66dc807b1dfd97bcbc690e0b890069 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 12 Jul 2026 20:35:52 +0000 Subject: [PATCH 118/127] Use _exit in child after fork, from Yayo Razo in GitHub issue 5376. --- cmd.c | 11 ++++++++--- job.c | 18 +++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/cmd.c b/cmd.c index b9b8f1a4a..d2e98d466 100644 --- a/cmd.c +++ b/cmd.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd.c,v 1.185 2026/06/26 14:40:30 nicm Exp $ */ +/* $OpenBSD: cmd.c,v 1.186 2026/07/12 20:35:52 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -846,7 +846,7 @@ 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; + size_t len, slen; if (strchr(template, '%') == NULL) return (xstrdup(template)); @@ -871,7 +871,10 @@ cmd_template_replace(const char *template, const char *s, int idx) if (quoted) ptr++; - buf = xrealloc(buf, len + (strlen(s) * 3) + 1); + slen = strlen(s); + if (slen > SIZE_MAX / 3 || len > SIZE_MAX - (slen * 3) - 1) + fatalx("argument too long"); + buf = xrealloc(buf, len + (slen * 3) + 1); for (cp = s; *cp != '\0'; cp++) { if (quoted && strchr(quote, *cp) != NULL) buf[len++] = '\\'; @@ -880,6 +883,8 @@ cmd_template_replace(const char *template, const char *s, int idx) buf[len] = '\0'; continue; } + if (len > SIZE_MAX - 2) + fatalx("argument too long"); buf = xrealloc(buf, len + 2); buf[len++] = ch; buf[len] = '\0'; diff --git a/job.c b/job.c index 8400ea584..82e87b825 100644 --- a/job.c +++ b/job.c @@ -1,4 +1,4 @@ -/* $OpenBSD: job.c,v 1.74 2025/09/08 11:21:56 nicm Exp $ */ +/* $OpenBSD: job.c,v 1.75 2026/07/12 20:35:52 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -149,7 +149,7 @@ job_run(const char *cmd, int argc, char **argv, struct environ *e, else if (chdir("/") == 0) environ_set(env, "PWD", 0, "/"); else - fatal("chdir failed"); + _exit(1); } environ_push(env); @@ -157,21 +157,21 @@ job_run(const char *cmd, int argc, char **argv, struct environ *e, if (~flags & JOB_PTY) { if (dup2(out[1], STDIN_FILENO) == -1) - fatal("dup2 failed"); + _exit(1); do_close = do_close && out[1] != STDIN_FILENO; if (dup2(out[1], STDOUT_FILENO) == -1) - fatal("dup2 failed"); + _exit(1); do_close = do_close && out[1] != STDOUT_FILENO; if (flags & JOB_SHOWSTDERR) { if (dup2(out[1], STDERR_FILENO) == -1) - fatal("dup2 failed"); + _exit(1); do_close = do_close && out[1] != STDERR_FILENO; } else { nullfd = open(_PATH_DEVNULL, O_RDWR); if (nullfd == -1) - fatal("open failed"); + _exit(1); if (dup2(nullfd, STDERR_FILENO) == -1) - fatal("dup2 failed"); + _exit(1); if (nullfd != STDERR_FILENO) close(nullfd); } @@ -185,11 +185,11 @@ job_run(const char *cmd, int argc, char **argv, struct environ *e, if (flags & JOB_DEFAULTSHELL) setenv("SHELL", shell, 1); execl(shell, argv0, "-c", cmd, (char *)NULL); - fatal("execl failed"); + _exit(1); } else { argvp = cmd_copy_argv(argc, argv); execvp(argvp[0], argvp); - fatal("execvp failed"); + _exit(1); } } From f80d3f612392870cbd431a1d0fad3c315c035095 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 09:00:01 +0100 Subject: [PATCH 119/127] build: fetch cutover master before merge --- .github/workflows/update-openbsd-and-portable.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml index f79d17545..003a0318b 100644 --- a/.github/workflows/update-openbsd-and-portable.yml +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -143,9 +143,9 @@ jobs: git remote add tmux-openbsd "../$CUTOVER_DIR" fi - git fetch --no-tags tmux-openbsd + git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master git switch master - git merge --no-ff --log tmux-openbsd/master + git merge --no-ff --log refs/remotes/tmux-openbsd/master - name: Push portable repository if: steps.cutover.outputs.changed == 'true' From 0ce72059e6e7c724f00a7eb241a5c5e80e281e2e Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 09:05:10 +0100 Subject: [PATCH 120/127] build: capture openbsd-base before filtering --- .../workflows/update-openbsd-and-portable.yml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml index 003a0318b..c5b592dc9 100644 --- a/.github/workflows/update-openbsd-and-portable.yml +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -76,6 +76,18 @@ jobs: git fetch --filter=blob:none --no-tags origin master git switch -C master origin/master + - name: Capture published OpenBSD tmux base + id: openbsd-base + run: | + set -eu + + cd "$CUTOVER_DIR" + if ! git rev-parse --verify -q origin/openbsd-git >/dev/null; then + echo "origin/openbsd-git is missing; publish the openbsd-git branch before running updates." >&2 + exit 1 + fi + echo "sha=$(git rev-parse origin/openbsd-git)" >> "$GITHUB_OUTPUT" + - name: Filter OpenBSD tmux history run: | set -eu @@ -99,12 +111,7 @@ jobs: cd "$CUTOVER_DIR" git switch master - if ! git rev-parse --verify -q origin/openbsd-git >/dev/null; then - echo "origin/openbsd-git is missing; publish the openbsd-git branch before running updates." >&2 - exit 1 - fi - - old_base=$(git rev-parse origin/openbsd-git) + old_base="${{ steps.openbsd-base.outputs.sha }}" new_base=$(git rev-parse openbsd-git) if [ "$old_base" = "$new_base" ]; then From 8b2bc1a3841f34bb3ceade4e21c82a3a2bf5a2c2 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 09:10:24 +0100 Subject: [PATCH 121/127] build: always attempt portable merge --- .github/workflows/update-openbsd-and-portable.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml index c5b592dc9..1894f52b9 100644 --- a/.github/workflows/update-openbsd-and-portable.yml +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -137,7 +137,7 @@ jobs: git push origin master openbsd-git - name: Merge cutover into portable - if: steps.cutover.outputs.changed == 'true' + id: portable run: | set -eu @@ -153,9 +153,14 @@ jobs: git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master git switch master git merge --no-ff --log refs/remotes/tmux-openbsd/master + if git diff --quiet origin/master..master; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi - name: Push portable repository - if: steps.cutover.outputs.changed == 'true' + if: steps.portable.outputs.changed == 'true' run: | set -eu From e6eb26518bdb01c2d6fc6d046b35e57704dfcd9a Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 09:13:28 +0100 Subject: [PATCH 122/127] build: don't exit --- .../workflows/update-openbsd-and-portable.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml index 1894f52b9..de4c6ab20 100644 --- a/.github/workflows/update-openbsd-and-portable.yml +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -116,18 +116,17 @@ jobs: if [ "$old_base" = "$new_base" ]; then echo "changed=false" >> "$GITHUB_OUTPUT" - echo "No new OpenBSD tmux commits to import." - exit 0 - fi + echo "No new OpenBSD tmux commits to import; continuing to portable merge." + else + if ! git merge-base --is-ancestor "$old_base" "$new_base"; then + echo "new openbsd-git is not a descendant of origin/openbsd-git; aborting." >&2 + exit 1 + fi - if ! git merge-base --is-ancestor "$old_base" "$new_base"; then - echo "new openbsd-git is not a descendant of origin/openbsd-git; aborting." >&2 - exit 1 + git cherry-pick "$old_base..$new_base" + echo "changed=true" >> "$GITHUB_OUTPUT" fi - git cherry-pick "$old_base..$new_base" - echo "changed=true" >> "$GITHUB_OUTPUT" - - name: Push cutover repository if: steps.cutover.outputs.changed == 'true' run: | From 6126988781b50faca2f23d47bd89e8451342d8a7 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 09:22:53 +0100 Subject: [PATCH 123/127] portable: add Makefile.am --- Makefile.am | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 84bdbdb2a..261ee0908 100644 --- a/Makefile.am +++ b/Makefile.am @@ -160,6 +160,8 @@ dist_tmux_SOURCES = \ compat.h \ control-notify.c \ control.c \ + events.c \ + events-payload.c \ environ.c \ file.c \ format.c \ @@ -168,6 +170,7 @@ dist_tmux_SOURCES = \ grid-reader.c \ grid-view.c \ grid.c \ + hooks.c \ hyperlinks.c \ input-keys.c \ input.c \ @@ -182,7 +185,6 @@ dist_tmux_SOURCES = \ mode-tree.c \ monitor.c \ names.c \ - notify.c \ options-table.c \ options.c \ paste.c \ From d03de4986a85c245f2fc11e921aa06ba8d183692 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 09:27:05 +0100 Subject: [PATCH 124/127] build: run every fifteen minutes --- .github/workflows/update-openbsd-and-portable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml index de4c6ab20..c914da332 100644 --- a/.github/workflows/update-openbsd-and-portable.yml +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -3,7 +3,7 @@ name: Update OpenBSD tmux and portable on: workflow_dispatch: schedule: - - cron: "17 3 * * *" + - cron: "*/15 * * * *" permissions: contents: write From e0014bbc7cab17919485e0c5f62356955050e180 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 10:23:38 +0100 Subject: [PATCH 125/127] Add regress tests for events code. --- Makefile.am | 10 +- regress/control-notify-events.sh | 102 +++++++++++++++ regress/hooks-after.sh | 30 ++++- regress/hooks-lifecycle.sh | 14 +++ regress/hooks-notify.sh | 93 ++++++++++++++ regress/options-array.sh | 48 +++++++ regress/prompt-mechanics.sh | 22 ++++ regress/set-hook-B.sh | 10 ++ regress/set-hook-E.sh | 150 ++++++++++++++++++++++ regress/wait-for-E.sh | 206 +++++++++++++++++++++++++++++++ 10 files changed, 676 insertions(+), 9 deletions(-) create mode 100644 regress/control-notify-events.sh mode change 100755 => 100644 regress/set-hook-B.sh create mode 100755 regress/set-hook-E.sh create mode 100644 regress/wait-for-E.sh diff --git a/Makefile.am b/Makefile.am index 261ee0908..2edc7c36d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -160,12 +160,12 @@ dist_tmux_SOURCES = \ compat.h \ control-notify.c \ control.c \ - events.c \ - events-payload.c \ environ.c \ + events-payload.c \ + events.c \ file.c \ - format.c \ format-draw.c \ + format.c \ fuzzy.c \ grid-reader.c \ grid-view.c \ @@ -190,8 +190,8 @@ dist_tmux_SOURCES = \ paste.c \ popup.c \ proc.c \ - prompt.c \ prompt-history.c \ + prompt.c \ regsub.c \ resize.c \ screen-redraw.c \ @@ -206,9 +206,9 @@ dist_tmux_SOURCES = \ spawn.c \ status.c \ style.c \ + tmux-protocol.h \ tmux.c \ tmux.h \ - tmux-protocol.h \ tty-acs.c \ tty-draw.c \ tty-features.c \ diff --git a/regress/control-notify-events.sh b/regress/control-notify-events.sh new file mode 100644 index 000000000..ab696f5e8 --- /dev/null +++ b/regress/control-notify-events.sh @@ -0,0 +1,102 @@ +#!/bin/sh + +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) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +TMPDIR=$(mktemp -d) +IN="$TMPDIR/in" +OUT="$TMPDIR/out" +PID= + +cleanup() +{ + [ -n "$PID" ] && kill "$PID" 2>/dev/null + $TMUX kill-server 2>/dev/null + rm -rf "$TMPDIR" +} +trap cleanup EXIT + +wait_for() +{ + pattern=$1 + timeout=${2:-6} + i=0 + + while [ "$i" -lt "$timeout" ]; do + if grep -F -- "$pattern" "$OUT" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + i=$((i + 1)) + done + echo "missing: $pattern" + cat "$OUT" + return 1 +} + +wait_for_count() +{ + pattern=$1 + expected=$2 + timeout=${3:-6} + i=0 + + while [ "$i" -lt "$timeout" ]; do + count=$(grep -F -- "$pattern" "$OUT" 2>/dev/null | wc -l) + if [ "$count" -ge "$expected" ]; then + return 0 + fi + sleep 1 + i=$((i + 1)) + done + echo "missing count $expected for: $pattern" + cat "$OUT" + return 1 +} + +send() +{ + printf '%s\n' "$*" >&3 +} + +$TMUX kill-server 2>/dev/null +$TMUX new-session -d -s notify -x 80 -y 24 || exit 1 +wid=$($TMUX display-message -p -t notify:0 '#{window_id}') || exit 1 +pane=$($TMUX display-message -p -t notify:0.0 '#{pane_id}') || exit 1 + +mkfifo "$IN" +: >"$OUT" +$TMUX -C attach-session -t notify <"$IN" >"$OUT" 2>&1 & +PID=$! +exec 3>"$IN" + +send 'display-message -p ready' +wait_for 'ready' 3 || exit 1 + +send 'rename-window notify-renamed' +wait_for "%window-renamed $wid notify-renamed" || exit 1 + +send 'split-window -h' +wait_for "%layout-change $wid " || exit 1 + +send 'select-pane -t:.0' +wait_for "%window-pane-changed $wid $pane" || exit 1 + +send 'copy-mode -t:.0' +wait_for "%pane-mode-changed $pane" || exit 1 + +sessions_changed=$(grep -F -- '%sessions-changed' "$OUT" 2>/dev/null | \ + wc -l) +send 'new-session -d -s notify-extra' +wait_for_count '%sessions-changed' $((sessions_changed + 1)) || exit 1 + +send 'kill-session -t notify-extra' +wait_for_count '%sessions-changed' $((sessions_changed + 2)) || exit 1 + +exit 0 diff --git a/regress/hooks-after.sh b/regress/hooks-after.sh index 37934db8d..3c1ba7909 100644 --- a/regress/hooks-after.sh +++ b/regress/hooks-after.sh @@ -63,10 +63,10 @@ $TMUX new -d -s two || fail "new-session two failed" # An after hook fires with the command arguments as formats. $TMUX set -g @after 0 || fail "set @after failed" $TMUX set-hook -g after-rename-window \ - 'set -gF @after "#{hook}|#{hook_argument_0}|#{hook_flag_t}"' || + 'set -gF @after "#{hook}|#{hook_arguments}|#{hook_argument_0}|#{hook_flag_t}"' || fail "set-hook -g after-rename-window failed" $TMUX rename-window -t one:0 first || fail "rename-window first failed" -wait_for @after 'after-rename-window|first|one:0' +wait_for @after 'after-rename-window|-t one:0 first|first|one:0' # An appended hook command runs after the first. $TMUX set -g @after2 0 || fail "set @after2 failed" @@ -74,10 +74,32 @@ $TMUX set-hook -ga after-rename-window \ 'set -gF @after2 "#{@after}+2"' || fail "set-hook -ga after-rename-window failed" $TMUX rename-window -t one:0 second || fail "rename-window second failed" -wait_for @after 'after-rename-window|second|one:0' -wait_for @after2 'after-rename-window|second|one:0+2' +wait_for @after 'after-rename-window|-t one:0 second|second|one:0' +wait_for @after2 'after-rename-window|-t one:0 second|second|one:0+2' $TMUX set-hook -gu after-rename-window || fail "set-hook -gu failed" +# A hook command is inserted after the command that fired it, before the next +# command in the same command list. +$TMUX set -g @order '' || fail "set @order failed" +$TMUX set-hook -g after-rename-window \ + 'set -gF @order "#{@order}H"' || + fail "set-hook -g after-rename-window order failed" +$TMUX rename-window -t one:0 ordered \; set -gF @order '#{@order}N' || + fail "rename-window order failed" +wait_for @order 'HN' +$TMUX set-hook -gu after-rename-window || fail "set-hook -gu order failed" + +# Repeated flag values are available as numbered hook flag formats. +$TMUX set -g @flags 0 || fail "set @flags failed" +$TMUX set-hook -g after-split-window \ + 'set -gF @flags "#{hook_arguments}|#{hook_argument_0}|#{hook_flag_t}|#{hook_flag_e}|#{hook_flag_e_0}|#{hook_flag_e_1}"' || + fail "set-hook -g after-split-window failed" +$TMUX split-window -d -t one:0 -e A=1 -e B=2 'sleep 60' || + fail "split-window flags failed" +wait_for @flags '-d -e A=1 -e B=2 -t one:0 "sleep 60"|sleep 60|one:0|B=2|A=1|B=2' +$TMUX set-hook -gu after-split-window || + fail "set-hook -gu after-split-window failed" + # A session after hook only fires for commands targeting that session and # the hook commands run with the command target as current state. $TMUX set -g @safter 0 || fail "set @safter failed" diff --git a/regress/hooks-lifecycle.sh b/regress/hooks-lifecycle.sh index e90f7ba3a..3ec748ff4 100644 --- a/regress/hooks-lifecycle.sh +++ b/regress/hooks-lifecycle.sh @@ -96,6 +96,20 @@ fi $TMUX list-panes -sat main >/dev/null || fail "list-panes failed" $TMUX has -t main || fail "server died after pane exit chain" +# A pane-exited hook command can run after the pane has been removed. It +# should not retain only the event payload's temporary target references. +$TMUX set -g @queued-pane-exited 0 || fail "set @queued-pane-exited failed" +$TMUX set-hook -g pane-exited \ + 'display-message -p "queued #{hook_pane}" ; set -g @queued-pane-exited 1' || + fail "set-hook queued pane-exited failed" +pane=$($TMUX new -d -s queued -n qwin -P -F '#{pane_id}' 'true') || + fail "new-session queued failed" +wait_for @queued-pane-exited 1 +$TMUX has -t main || fail "server died after queued pane-exited hook" +$TMUX set-hook -g pane-exited \ + 'set -gF @log "#{@log}|pane-exited:#{hook_pane}"' || + fail "restore pane-exited hook failed" + # kill-window on the last window: window-unlinked then session-closed but # no pane-exited for the panes in the killed window. $TMUX set -g @log '' || fail "reset @log failed" diff --git a/regress/hooks-notify.sh b/regress/hooks-notify.sh index 41916248c..37d1cd651 100644 --- a/regress/hooks-notify.sh +++ b/regress/hooks-notify.sh @@ -181,6 +181,99 @@ $TMUX set-hook -gu pane-title-changed || fail "unset pane-title-changed failed" $TMUX killp -t "$pane" || fail "kill-pane title failed" +# pane-created payload from a command pane, an empty pane, and a respawn. +$TMUX set -g @pc 0 || fail "set @pc failed" +$TMUX set-hook -g pane-created \ + 'set -gF @pc "#{hook}:#{hook_pane}:#{hook_pane_command}:#{hook_created_empty}:#{hook_created_respawn}"' || + fail "set-hook pane-created failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window pane-created command failed" +wait_for @pc "pane-created:$pane:\"sleep 30\":0:0" +$TMUX killp -t "$pane" || fail "kill-pane pane-created command failed" + +$TMUX set -g @pc 0 || fail "reset @pc failed" +empty_command=$($TMUX show -gqv default-shell) || + fail "show default-shell failed" +pane=$($TMUX splitw -d -E -t main:0 -P -F '#{pane_id}') || + fail "split-window pane-created empty failed" +wait_for @pc "pane-created:$pane:$empty_command:1:0" + +$TMUX set -g @pc 0 || fail "reset @pc respawn failed" +$TMUX respawnp -k -t "$pane" 'sleep 30' || fail "respawn-pane failed" +wait_for @pc "pane-created:$pane:\"sleep 30\":0:1" +$TMUX killp -t "$pane" || fail "kill-pane pane-created respawn failed" +$TMUX set-hook -gu pane-created || fail "unset pane-created failed" + +# pane-resized carries old and new dimensions. +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window pane-resized failed" +old_size=$($TMUX display -pt "$pane" '#{pane_width},#{pane_height}') || + fail "display old pane size failed" +$TMUX set -g @pr 0 || fail "set @pr failed" +$TMUX set-hook -g pane-resized \ + 'set -gF @pr "#{hook}:#{hook_pane}:#{hook_old_width},#{hook_old_height}->#{hook_width},#{hook_height}"' || + fail "set-hook pane-resized failed" +$TMUX resizep -t "$pane" -x 20 -y 10 || fail "resize-pane failed" +new_size=$($TMUX display -pt "$pane" '#{pane_width},#{pane_height}') || + fail "display new pane size failed" +wait_for @pr "pane-resized:$pane:$old_size->$new_size" +$TMUX set-hook -gu pane-resized || fail "unset pane-resized failed" +$TMUX killp -t "$pane" || fail "kill-pane pane-resized failed" + +# pane-mode-entered and pane-mode-exited expose split transition payloads. +$TMUX set -g @me 0 || fail "set @me failed" +$TMUX set -g @mx 0 || fail "set @mx failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window pane-mode failed" +$TMUX set-hook -g pane-mode-entered \ + 'set -gF @me "#{hook}:#{hook_pane}:#{hook_mode_entered}:#{hook_current_mode}:#{hook_previous_mode}"' || + fail "set-hook pane-mode-entered failed" +$TMUX set-hook -g pane-mode-exited \ + 'set -gF @mx "#{hook}:#{hook_pane}:#{hook_mode_entered}:#{hook_current_mode}:#{hook_previous_mode}"' || + fail "set-hook pane-mode-exited failed" +$TMUX copy-mode -t "$pane" || fail "copy-mode split event failed" +wait_for @me "pane-mode-entered:$pane:1:copy-mode:" +$TMUX send-keys -t "$pane" -X cancel || fail "cancel split mode failed" +wait_for @mx "pane-mode-exited:$pane:0::copy-mode" +$TMUX set-hook -gu pane-mode-entered || fail "unset pane-mode-entered failed" +$TMUX set-hook -gu pane-mode-exited || fail "unset pane-mode-exited failed" +$TMUX killp -t "$pane" || fail "kill-pane pane-mode failed" + +# marked-pane-changed carries the new/old marked pane and marked flag. +$TMUX set -g @mk 0 || fail "set @mk failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window marked-pane failed" +$TMUX set-hook -g marked-pane-changed \ + 'set -gF @mk "#{hook}:#{hook_marked}:#{hook_pane}:#{hook_new_pane}:#{hook_old_pane}"' || + fail "set-hook marked-pane-changed failed" +$TMUX selectp -t "$pane" -m || fail "mark pane failed" +wait_for @mk "marked-pane-changed:1:$pane:$pane:" +$TMUX selectp -t "$pane" -m || fail "unmark pane failed" +wait_for @mk "marked-pane-changed:0:$pane::$pane" +$TMUX set-hook -gu marked-pane-changed || + fail "unset marked-pane-changed failed" +$TMUX killp -t "$pane" || fail "kill-pane marked-pane failed" + +# window-zoomed and window-unzoomed fire on resize-pane -Z. +$TMUX set -g @zoom 0 || fail "set @zoom failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window zoom failed" +window=$($TMUX display -pt "$pane" '#{window_id}') || + fail "display zoom window failed" +$TMUX set-hook -g window-zoomed \ + 'set -gF @zoom "#{hook}:#{hook_window}"' || + fail "set-hook window-zoomed failed" +$TMUX set-hook -g window-unzoomed \ + 'set -gF @zoom "#{hook}:#{hook_window}"' || + fail "set-hook window-unzoomed failed" +$TMUX resizep -Z -t "$pane" || fail "zoom pane failed" +wait_for @zoom "window-zoomed:$window" +$TMUX resizep -Z -t "$pane" || fail "unzoom pane failed" +wait_for @zoom "window-unzoomed:$window" +$TMUX set-hook -gu window-zoomed || fail "unset window-zoomed failed" +$TMUX set-hook -gu window-unzoomed || fail "unset window-unzoomed failed" +$TMUX killp -t "$pane" || fail "kill-pane zoom failed" + # client-attached and client-detached using a control client. $TMUX set -g @a 0 || fail "set @a failed" $TMUX set-hook -g client-attached 'set -gF @a "#{hook}"' || diff --git a/regress/options-array.sh b/regress/options-array.sh index d9751c94c..fbaff6ae2 100644 --- a/regress/options-array.sh +++ b/regress/options-array.sh @@ -36,6 +36,21 @@ check_value() fi } +check_wait_value() +{ + i=0 + while [ "$i" -lt 30 ]; do + out=$($TMUX show $1 2>&1) + [ "$out" = "$2" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + echo "show $1 failed." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 +} + # check_array $args $expected # # Compare the full (multi-line) show output for an array option with a @@ -162,6 +177,39 @@ check_ok set-hook -g window-renamed[notify] "display-message renamed" check_value "-gv window-renamed[notify]" "display-message renamed" check_ok set-hook -gu window-renamed[notify] check_value "-gv window-renamed[notify]" "" +check_ok set -g @hook_first 0 +check_ok set -g @hook_second 0 +check_ok set-hook -g window-renamed[first] "set -g @hook_first 1" +check_ok set-hook -g window-renamed[second] "set -g @hook_second 1" +shown=$($TMUX show-hooks -g window-renamed) || { + echo "show-hooks -g window-renamed failed" + exit 1 +} +echo "$shown" | grep -q '^window-renamed\[first\]' || { + echo "missing first hook key: $shown" + exit 1 +} +echo "$shown" | grep -q '^window-renamed\[second\]' || { + echo "missing second hook key: $shown" + exit 1 +} +check_ok rename-window -t main:0 array-hooks +check_wait_value "-gqv @hook_first" "1" +check_wait_value "-gqv @hook_second" "1" +check_ok set-hook -gu window-renamed[first] +shown=$($TMUX show-hooks -g window-renamed) || { + echo "show-hooks -g window-renamed after unset failed" + exit 1 +} +echo "$shown" | grep -q '^window-renamed\[first\]' && { + echo "first hook key was not unset: $shown" + exit 1 +} +echo "$shown" | grep -q '^window-renamed\[second\]' || { + echo "second hook key did not remain: $shown" + exit 1 +} +check_ok set-hook -gu window-renamed[second] # --- colour-type array ---------------------------------------------------- # diff --git a/regress/prompt-mechanics.sh b/regress/prompt-mechanics.sh index 55bd17a69..9e57d7a55 100644 --- a/regress/prompt-mechanics.sh +++ b/regress/prompt-mechanics.sh @@ -49,6 +49,19 @@ status_line() { got() { $IN show -gv @r } +wait_opt() { + 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'" +} reset() { $IN set -g @r "SENTINEL" || exit 1 } @@ -85,6 +98,12 @@ $IN bind -n M-j command-prompt -P -I hello -p '(pre)' "set -g @r '%%'" || e $IN bind -n M-m command-prompt -p 'first,second' "set -g @r '%1/%2'" || exit 1 $IN bind -n M-c command-prompt -p '(cmd)' "set -g @r '%%'" || exit 1 $IN bind -n M-h command-prompt -T search -p '(srch)' "set -g @r '%%'" || exit 1 +$IN set-hook -g pane-prompt-opened \ + 'set -gF @prompt_open "#{hook}:#{hook_pane}:#{hook_prompt_type}"' || + exit 1 +$IN set-hook -g pane-prompt-closed \ + 'set -gF @prompt_close "#{hook}:#{hook_pane}:#{hook_prompt_type}"' || + exit 1 # --- Outer session: attach the inner one inside its pane. ------------------- $OUT new -d -x80 -y24 || exit 1 @@ -111,14 +130,17 @@ settle # --- 1b. window.c: drawn over the pane, not on the status line. --- reset +pane=$($IN display-message -p '#{pane_id}') || exit 1 $OUT send-keys M-p || exit 1 settle +wait_opt @prompt_open "pane-prompt-opened:$pane:command" capture | grep -qF '(pane)' || fail "pane prompt not drawn in the pane" status_line | grep -qF '(pane)' && \ fail "pane prompt drawn on the status line, not over the pane" $OUT send-keys -l "deep" || exit 1 $OUT send-keys Enter || exit 1 settle +wait_opt @prompt_close "pane-prompt-closed:$pane:command" [ "$(got)" = "deep" ] || fail "pane prompt accept recovered '$(got)', wanted 'deep'" # --- 1c. mode-tree.c: search prompt drawn in the pane. --- diff --git a/regress/set-hook-B.sh b/regress/set-hook-B.sh old mode 100755 new mode 100644 index 4947af70c..0144884dc --- a/regress/set-hook-B.sh +++ b/regress/set-hook-B.sh @@ -147,4 +147,14 @@ $TMUX set -pt "$target_pane" @target-value changed || fail "set pane @target-value failed" wait_for @target-pane "$target_pane" +$TMUX new -d -s zzz-survivor || fail "new survivor session failed" +$TMUX set -g @global-after-destroy 0 || + fail "set @global-after-destroy failed" +$TMUX set-hook -g -t three -B '@global-after-destroy-session::#{session_name}' \ + 'set -g @global-after-destroy "#{hook_last}->#{hook_value}"' || + fail "set-hook -B global after destroy failed" +assert_unchanged @global-after-destroy 0 +$TMUX kill-session -t three || fail "kill destroyed monitor session failed" +wait_for @global-after-destroy 'three->zzz-survivor' + exit 0 diff --git a/regress/set-hook-E.sh b/regress/set-hook-E.sh new file mode 100755 index 000000000..f8d30d05e --- /dev/null +++ b/regress/set-hook-E.sh @@ -0,0 +1,150 @@ +#!/bin/sh + +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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_channel() +{ + channel=$1 + + if command -v timeout >/dev/null 2>&1; then + timeout 10 $TMUX wait-for "$channel" || + fail "wait-for $channel timed out" + return + fi + + $TMUX wait-for "$channel" & + pid=$! + i=0 + while kill -0 "$pid" 2>/dev/null; do + [ $i -lt 50 ] || { + kill "$pid" 2>/dev/null || true + fail "wait-for $channel timed out" + } + i=$((i + 1)) + sleep 0.2 + done + wait "$pid" || fail "wait-for $channel failed" +} + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX 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'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + count=${3:-15} + i=0 + + while [ $i -lt "$count" ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +wait_list() +{ + name=$1 + i=0 + + while [ $i -lt 50 ]; do + value=$($TMUX wait-for -E -l "$name" 2>/dev/null || true) + if [ -n "$value" ]; then + printf '%s\n' "$value" | sed -n '1p' + return + fi + i=$((i + 1)) + sleep 0.2 + done + fail "wait-for -E -l $name found no waiters" +} + +$TMUX new -d -s one || fail "new-session one failed" +$TMUX new -d -s two || fail "new-session two failed" + +$TMUX set -g @event_seen 0 || fail "set @event_seen failed" +$TMUX wait-for -E @manual-event \; set -g @event_seen 1 \; \ + wait-for -S she-event & +event_pid=$! +wait_list @manual-event >/dev/null + +$TMUX set-hook -E @manual-event || fail "set-hook -E @manual-event failed" +wait_channel she-event +wait "$event_pid" || fail "wait-for -E @manual-event failed" +wait_for @event_seen 1 + +$TMUX set-hook -E @no-sink || fail "set-hook -E @no-sink failed" + +pane=$($TMUX display -pt two:0.0 '#{pane_id}') || + fail "display-message pane failed" +$TMUX set -g @hook_seen 0 || fail "set @hook_seen failed" +$TMUX set-hook -g @manual-hook \ + 'set -gF @hook_seen "#{hook}:#{session_name}:#{window_index}:#{pane_id}"' || + fail "set-hook @manual-hook failed" + +$TMUX set-hook -E -t two:0.0 @manual-hook || + fail "set-hook -E @manual-hook failed" +wait_for @hook_seen "@manual-hook:two:0:$pane" + +$TMUX set -g @r_hook 0 || fail "set @r_hook failed" +$TMUX set -g @r_event 0 || fail "set @r_event failed" +$TMUX set-hook -g @manual-r 'set -g @r_hook 1' || + fail "set-hook @manual-r failed" +$TMUX wait-for -E @manual-r \; set -g @r_event 1 \; wait-for -S she-r & +r_pid=$! +r_client=$(wait_list @manual-r) + +$TMUX set-hook -R @manual-r || fail "set-hook -R @manual-r failed" +wait_for @r_hook 1 +assert_unchanged @r_event 0 5 + +$TMUX wait-for -E -w "$r_client" @manual-r || + fail "wait-for -E -w @manual-r failed" +wait_channel she-r +wait "$r_pid" || fail "wait-for -E @manual-r failed" + +if $TMUX set-hook -E window-renamed 2>/dev/null; then + fail "set-hook -E window-renamed succeeded" +fi + +exit 0 diff --git a/regress/wait-for-E.sh b/regress/wait-for-E.sh new file mode 100644 index 000000000..f58437a34 --- /dev/null +++ b/regress/wait-for-E.sh @@ -0,0 +1,206 @@ +#!/bin/sh + +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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_channel() +{ + channel=$1 + + if command -v timeout >/dev/null 2>&1; then + timeout 10 $TMUX wait-for "$channel" || + fail "wait-for $channel timed out" + return + fi + + $TMUX wait-for "$channel" & + pid=$! + i=0 + while kill -0 "$pid" 2>/dev/null; do + [ $i -lt 50 ] || { + kill "$pid" 2>/dev/null || true + fail "wait-for $channel timed out" + } + i=$((i + 1)) + sleep 0.2 + done + wait "$pid" || fail "wait-for $channel failed" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + count=${3:-15} + i=0 + + while [ $i -lt "$count" ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +wait_list() +{ + event=$1 + name=$2 + i=0 + + while [ $i -lt 50 ]; do + if [ "$event" = 1 ]; then + value=$($TMUX wait-for -E -l "$name" 2>/dev/null || true) + else + value=$($TMUX wait-for -l "$name" 2>/dev/null || true) + fi + if [ -n "$value" ]; then + printf '%s\n' "$value" | sed -n '1p' + return + fi + i=$((i + 1)) + sleep 0.2 + done + fail "wait-for -l $name found no waiters" +} + +$TMUX new -d -s wf || fail "new-session failed" + +$TMUX wait-for wf-list & +list_pid=$! +client=$(wait_list 0 wf-list) +$TMUX wait-for -w "$client" wf-list || fail "wait-for -w wf-list failed" +wait "$list_pid" || fail "wait-for -w did not wake channel waiter" + +$TMUX set -g @forced 0 || fail "set @forced failed" +$TMUX wait-for -E @forced-event \; set -g @forced 1 & +forced_pid=$! +client=$(wait_list 1 @forced-event) +$TMUX wait-for -E -w "$client" @forced-event || + fail "wait-for -E -w @forced-event failed" +wait "$forced_pid" || fail "wait-for -E -w did not wake event waiter" +[ "$($TMUX show -gqv @forced)" = 1 ] || + fail "wait-for -E -w did not continue event waiter" + +if $TMUX wait-for -E foobar 2>/dev/null; then + fail "wait-for -E accepted invalid event" +fi + +$TMUX wait-for -E @not-yet-fired & +not_yet_pid=$! +client=$(wait_list 1 @not-yet-fired) +$TMUX wait-for -E -w "$client" @not-yet-fired || + fail "wait-for -E -w @not-yet-fired failed" +wait "$not_yet_pid" || fail "wait-for -E @not-yet-fired failed" + +$TMUX set -g @wf_value 0 || fail "set @wf_value failed" +$TMUX set-hook -g -B '@wf::#{@wf_value}' 'wait-for -S wf-hook' || + fail "set-hook -B failed" + +$TMUX wait-for -E @wf \; wait-for -S wf-event & +event_pid=$! + +# Let the monitor take its first sample so the next change is reported. +sleep 1.5 + +$TMUX set -g @wf_value 1 || fail "set @wf_value 1 failed" + +wait_channel wf-event +wait_channel wf-hook +wait "$event_pid" || fail "wait-for -E command failed" + +$TMUX set -g @late 0 || fail "set @late failed" +$TMUX wait-for -E @wf \; set -g @late 1 \; wait-for -S wf-late & +late_pid=$! +assert_unchanged @late 0 5 + +$TMUX set -g @wf_value 2 || fail "set @wf_value 2 failed" +wait_channel wf-late +wait "$late_pid" || fail "late wait-for -E command failed" + +$TMUX set -g @filtered 0 || fail "set @filtered failed" +$TMUX wait-for -E -F '#{==:#{value},3}' @wf \; set -g @filtered 1 \; \ + wait-for -S wf-filtered & +filtered_pid=$! +assert_unchanged @filtered 0 5 + +$TMUX set -g @wf_value unmatched || fail "set @wf_value unmatched failed" +assert_unchanged @filtered 0 5 + +$TMUX set -g @wf_value 3 || fail "set @wf_value 3 failed" +wait_channel wf-filtered +wait "$filtered_pid" || fail "filtered wait-for -E command failed" + +verbose_file="$OUT/verbose" +$TMUX wait-for -E -v @wf \; wait-for -S wf-verbose >"$verbose_file" & +verbose_pid=$! + +sleep 0.5 +$TMUX set -g @wf_value 4 || fail "set @wf_value 4 failed" +wait_channel wf-verbose +wait "$verbose_pid" || fail "verbose wait-for -E command failed" +grep '^event=@wf$' "$verbose_file" >/dev/null || + fail "verbose wait-for -E did not print event payload" +grep '^value=4$' "$verbose_file" >/dev/null || + fail "verbose wait-for -E did not print value payload" +grep '^_hook_monitor=' "$verbose_file" >/dev/null && + fail "verbose wait-for -E printed private payload" + +$TMUX new -d -s wf2 || fail "new-session wf2 failed" + +$TMUX wait-for -E window-renamed \; wait-for -S wf-renamed & +renamed_pid=$! + +sleep 0.5 +$TMUX rename-window -t wf2:0 renamed || fail "rename-window failed" +wait_channel wf-renamed +wait "$renamed_pid" || fail "wait-for -E window-renamed failed" + +$TMUX set-hook -g window-renamed 'wait-for -S wf-hook-renamed' || + fail "set-hook window-renamed failed" +$TMUX rename-window -t wf2:0 renamed-again || + fail "rename-window renamed-again failed" +wait_channel wf-hook-renamed + +$TMUX set -g @builtin_filtered 0 || fail "set @builtin_filtered failed" +target=$($TMUX splitw -d -t wf2:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window target failed" +$TMUX wait-for -E -F "#{==:#{pane},$target}" pane-exited \; \ + set -g @builtin_filtered 1 \; wait-for -S wf-builtin-filtered & +builtin_filtered_pid=$! +assert_unchanged @builtin_filtered 0 5 + +$TMUX splitw -d -t wf2:0 'true' || fail "split-window nonmatching failed" +assert_unchanged @builtin_filtered 0 5 + +$TMUX send-keys -t "$target" C-c || fail "send C-c to target failed" +wait_channel wf-builtin-filtered +wait "$builtin_filtered_pid" || + fail "filtered wait-for -E pane-exited command failed" + +exit 0 From d9b953d5c6af08de2f4b1be97ef44a47a0dbafef Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 10:29:28 +0100 Subject: [PATCH 126/127] Create SECURITY.md --- SECURITY.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..56f11f8ae --- /dev/null +++ b/SECURITY.md @@ -0,0 +1 @@ +Please report security issues to nicholas.marriott@gmail.com. From 1f6385e7bb0fff61bc017f6840fd455d00aad4f6 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 11:03:46 +0100 Subject: [PATCH 127/127] build: remove openbsd sync job This only needs to run from the openbsd-cutover repository and will no longer be merged into thisd repo. --- .../workflows/update-openbsd-and-portable.yml | 174 ------------------ 1 file changed, 174 deletions(-) delete mode 100644 .github/workflows/update-openbsd-and-portable.yml diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml deleted file mode 100644 index c914da332..000000000 --- a/.github/workflows/update-openbsd-and-portable.yml +++ /dev/null @@ -1,174 +0,0 @@ -name: Update OpenBSD tmux and portable - -on: - workflow_dispatch: - schedule: - - cron: "*/15 * * * *" - -permissions: - contents: write - -concurrency: - group: update-openbsd-and-portable - cancel-in-progress: false - -env: - OPENBSD_SRC_URL: https://github.com/openbsd/src.git - OPENBSD_SRC_DIR: openbsd-src - CUTOVER_DIR: tmux-openbsd-cutover - PORTABLE_DIR: tmux-portable - PORTABLE_REPO: ${{ vars.TMUX_PORTABLE_REPO }} - -jobs: - update: - runs-on: ubuntu-latest - steps: - - name: Configure Git identity - run: | - git config --global user.name "tmux update bot" - git config --global user.email "actions@github.com" - - - name: Install git-filter-repo - run: python3 -m pip install --user git-filter-repo - - - name: Checkout cutover repository - uses: actions/checkout@v4 - with: - path: ${{ env.CUTOVER_DIR }} - fetch-depth: 0 - token: ${{ secrets.UPDATE_TOKEN || github.token }} - - - name: Check portable repo setting - run: | - if [ -z "$PORTABLE_REPO" ]; then - echo "Set repository variable TMUX_PORTABLE_REPO, for example owner/tmux-portable." >&2 - exit 1 - fi - - - name: Checkout portable repository - uses: actions/checkout@v4 - with: - repository: ${{ env.PORTABLE_REPO }} - path: ${{ env.PORTABLE_DIR }} - fetch-depth: 0 - token: ${{ secrets.UPDATE_TOKEN }} - - - name: Restore OpenBSD source clone cache - id: openbsd-cache - uses: actions/cache/restore@v4 - with: - path: ${{ env.OPENBSD_SRC_DIR }} - key: openbsd-src-blobless-${{ runner.os }}-${{ github.run_id }} - restore-keys: | - openbsd-src-blobless-${{ runner.os }}- - - - name: Refresh OpenBSD source clone - run: | - set -eu - - if [ ! -d "$OPENBSD_SRC_DIR/.git" ]; then - git clone --filter=blob:none --no-tags --single-branch --branch master \ - "$OPENBSD_SRC_URL" "$OPENBSD_SRC_DIR" - fi - - cd "$OPENBSD_SRC_DIR" - git remote set-url origin "$OPENBSD_SRC_URL" - git fetch --filter=blob:none --no-tags origin master - git switch -C master origin/master - - - name: Capture published OpenBSD tmux base - id: openbsd-base - run: | - set -eu - - cd "$CUTOVER_DIR" - if ! git rev-parse --verify -q origin/openbsd-git >/dev/null; then - echo "origin/openbsd-git is missing; publish the openbsd-git branch before running updates." >&2 - exit 1 - fi - echo "sha=$(git rev-parse origin/openbsd-git)" >> "$GITHUB_OUTPUT" - - - name: Filter OpenBSD tmux history - run: | - set -eu - - cd "$OPENBSD_SRC_DIR" - git switch -C tmux-openbsd origin/master - git filter-repo \ - --force \ - --refs tmux-openbsd \ - --path usr.bin/tmux/ \ - --path-rename usr.bin/tmux/: \ - --replace-refs delete-no-add - - git push "../$CUTOVER_DIR" tmux-openbsd:refs/heads/openbsd-git - - - name: Update cutover master - id: cutover - run: | - set -eu - - cd "$CUTOVER_DIR" - git switch master - - old_base="${{ steps.openbsd-base.outputs.sha }}" - new_base=$(git rev-parse openbsd-git) - - if [ "$old_base" = "$new_base" ]; then - echo "changed=false" >> "$GITHUB_OUTPUT" - echo "No new OpenBSD tmux commits to import; continuing to portable merge." - else - if ! git merge-base --is-ancestor "$old_base" "$new_base"; then - echo "new openbsd-git is not a descendant of origin/openbsd-git; aborting." >&2 - exit 1 - fi - - git cherry-pick "$old_base..$new_base" - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Push cutover repository - if: steps.cutover.outputs.changed == 'true' - run: | - set -eu - - cd "$CUTOVER_DIR" - git push origin master openbsd-git - - - name: Merge cutover into portable - id: portable - run: | - set -eu - - cd "$PORTABLE_DIR" - git config remote.tmux-openbsd.tagOpt --no-tags - - if git remote get-url tmux-openbsd >/dev/null 2>&1; then - git remote set-url tmux-openbsd "../$CUTOVER_DIR" - else - git remote add tmux-openbsd "../$CUTOVER_DIR" - fi - - git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master - git switch master - git merge --no-ff --log refs/remotes/tmux-openbsd/master - if git diff --quiet origin/master..master; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Push portable repository - if: steps.portable.outputs.changed == 'true' - run: | - set -eu - - cd "$PORTABLE_DIR" - git push origin master - - - name: Save OpenBSD source clone cache - if: always() - uses: actions/cache/save@v4 - with: - path: ${{ env.OPENBSD_SRC_DIR }} - key: openbsd-src-blobless-${{ runner.os }}-${{ github.run_id }}