From 309119f659472c9e18ae20026cf9c444db1e341f Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 10:54:27 +0100 Subject: [PATCH 01/36] Remove worflow from master --- .../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 }} From b2e2fe824dbea78585946a229d09b784fb567acf Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 11:32:28 +0100 Subject: [PATCH 02/36] Add scroll regress. --- regress/copy-mode-selection-scroll.sh | 113 ++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 regress/copy-mode-selection-scroll.sh diff --git a/regress/copy-mode-selection-scroll.sh b/regress/copy-mode-selection-scroll.sh new file mode 100644 index 000000000..7e7aee9ce --- /dev/null +++ b/regress/copy-mode-selection-scroll.sh @@ -0,0 +1,113 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} +fail() +{ + echo "$1" + cleanup + exit 1 +} +expect_buffer() +{ + expected=$1 + actual=$($TMUX show-buffer) + [ "$actual" = "$expected" ] || + fail "unexpected buffer: expected [$expected], got [$actual]" +} +wheel() +{ + button=$1 + col=$2 + row=$3 + seq=$(printf '\033[<%s;%s;%sM' "$button" "$col" "$row") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} +trap cleanup 0 +trap 'exit 1' 1 2 3 15 + +$TMUX new -d -x40 -y10 \ + 'i=0; while [ $i -lt 80 ]; do printf "line %02d xxxxxxxxxx\n" $i; i=$((i + 1)); done; cat' || + exit 1 +$TMUX set -g window-size manual || exit 1 +$TMUX set -g mouse on || exit 1 + +$TMUX copy-mode || exit 1 +$TMUX send-keys -X history-top || exit 1 +$TMUX send-keys -N10 -X cursor-down || exit 1 +$TMUX send-keys -X start-of-line || exit 1 +$TMUX send-keys -X begin-selection || exit 1 +$TMUX send-keys -N2 -X cursor-down || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 + +initial=$(printf 'line 10 xxxxxxxxxx\nline 11 xxxxxxxxxx') +expect_buffer "$initial" + +$TMUX send-keys -X stop-selection || exit 1 +$TMUX send-keys -N3 -X scroll-down || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$initial" + +$TMUX send-keys -N2 -X scroll-up || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$initial" + +$TMUX send-keys -X scroll-middle || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$initial" + +$TMUX send-keys -X scroll-bottom || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$initial" + +$TMUX send-keys -X scroll-top || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$initial" + +$TMUX send-keys -X recentre-top-bottom || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$initial" + +$TMUX send-keys -X other-end || exit 1 +$TMUX send-keys -X cursor-down || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 + +extended_end=$(printf 'line 10 xxxxxxxxxx\nline 11 xxxxxxxxxx\nline 12 xxxxxxxxxx') +expect_buffer "$extended_end" + +$TMUX send-keys -X stop-selection || exit 1 +$TMUX send-keys -X other-end || exit 1 +$TMUX send-keys -X other-end || exit 1 +$TMUX send-keys -X cursor-up || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 + +extended_start=$(printf 'line 09 xxxxxxxxxx\nline 10 xxxxxxxxxx\nline 11 xxxxxxxxxx\nline 12 xxxxxxxxxx') +expect_buffer "$extended_start" + +$TMUX2 new-session -d -x40 -y10 "$TMUX attach" || exit 1 +sleep 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "no outer pane" + +wheel 65 5 5 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$extended_start" + +wheel 64 5 5 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$extended_start" + +exit 0 From 687c6b9cc0a32348ac42cc85d816579f19bf9e64 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 13:57:31 +0100 Subject: [PATCH 03/36] Regress for more hooks. --- regress/hooks-notify.sh | 257 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 249 insertions(+), 8 deletions(-) diff --git a/regress/hooks-notify.sh b/regress/hooks-notify.sh index 37d1cd651..045b80af6 100644 --- a/regress/hooks-notify.sh +++ b/regress/hooks-notify.sh @@ -11,11 +11,13 @@ OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" fail() { echo "$*" >&2 $TMUX kill-server 2>/dev/null || true + $TMUX2 kill-server 2>/dev/null || true rm -rf "$OUT" exit 1 } @@ -23,6 +25,7 @@ fail() cleanup() { $TMUX kill-server 2>/dev/null || true + $TMUX2 kill-server 2>/dev/null || true rm -rf "$OUT" } trap cleanup EXIT @@ -92,6 +95,23 @@ 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-created and window-closed are actual window lifetime hooks. +$TMUX set -g @wc 0 || fail "set @wc failed" +$TMUX set-hook -g window-created \ + 'set -gF @wc "#{@wc}|#{hook}:#{hook_window}:#{hook_window_name}"' || + fail "set-hook window-created failed" +$TMUX set-hook -g window-closed \ + 'set -gF @wc "#{@wc}|#{hook}:#{hook_window}:#{hook_window_name}"' || + fail "set-hook window-closed failed" +$TMUX neww -d -t main: -n lifetime || fail "new-window lifetime failed" +window=$($TMUX display -pt main:lifetime '#{window_id}') || + fail "display lifetime window failed" +wait_for @wc "0|window-created:$window:lifetime" +$TMUX kill-window -t main:lifetime || fail "kill-window lifetime failed" +wait_for @wc "0|window-created:$window:lifetime|window-closed:$window:lifetime" +$TMUX set-hook -gu window-created || fail "unset window-created failed" +$TMUX set-hook -gu window-closed || fail "unset window-closed failed" + # window-renamed sees the window id. window=$($TMUX display -pt main:0 '#{window_id}') || fail "display window_id failed" @@ -114,26 +134,75 @@ $TMUX set-hook -gu window-layout-changed || fail "unset window-layout-changed failed" # window-pane-changed from select-pane. +old_pane=$($TMUX display -pt main:0.0 '#{pane_id}') || + fail "display old pane failed" +new_pane=$($TMUX display -pt main:0.1 '#{pane_id}') || + fail "display new pane failed" $TMUX set -g @p 0 || fail "set @p failed" $TMUX set-hook -g window-pane-changed \ - 'set -gF @p "#{hook}:#{hook_window}"' || + 'set -gF @p "#{hook}:#{hook_window}:#{hook_pane}:#{hook_old_pane}:#{hook_new_pane}"' || fail "set-hook window-pane-changed failed" $TMUX selectp -t main:0.1 || fail "select-pane failed" -wait_for @p "window-pane-changed:$window" +wait_for @p "window-pane-changed:$window:$new_pane:$old_pane:$new_pane" + +# window-pane-changed from removing the active pane. +$TMUX set -g @p 0 || fail "reset @p failed" +$TMUX killp -t "$new_pane" || fail "kill active pane failed" +wait_for @p "window-pane-changed:$window:$old_pane:$new_pane:$old_pane" $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" +old_window=$($TMUX display -pt main:0 '#{window_id}') || + fail "display old window failed" +new_window=$($TMUX display -pt main:w2 '#{window_id}') || + fail "display new window failed" +old_index=$($TMUX display -pt main:0 '#{window_index}') || + fail "display old window index failed" +new_index=$($TMUX display -pt main:w2 '#{window_index}') || + fail "display new window index failed" $TMUX set -g @c 0 || fail "set @c failed" $TMUX set-hook -g session-window-changed \ - 'set -gF @c "#{hook}:#{hook_session_name}"' || + 'set -gF @c "#{hook}:#{hook_session_name}:#{hook_window}:#{hook_old_window}:#{hook_new_window}:#{hook_old_window_index}->#{hook_new_window_index}"' || fail "set-hook session-window-changed failed" $TMUX selectw -t main:w2 || fail "select-window failed" -wait_for @c 'session-window-changed:main' +wait_for @c "session-window-changed:main:$new_window:$old_window:$new_window:$old_index->$new_index" $TMUX set-hook -gu session-window-changed || fail "unset session-window-changed failed" +# session-added-to-group and session-removed-from-group carry group details. +$TMUX set -g @sg 0 || fail "set @sg failed" +$TMUX set-hook -g session-added-to-group \ + 'set -gF @sg "#{@sg}|#{hook}:#{hook_session_name}:#{hook_group}:#{hook_group_size}"' || + fail "set-hook session-added-to-group failed" +$TMUX set-hook -g session-removed-from-group \ + 'set -gF @sg "#{@sg}|#{hook}:#{hook_session_name}:#{hook_group}:#{hook_group_size}"' || + fail "set-hook session-removed-from-group failed" +$TMUX new -d -s group-a || fail "new-session group-a failed" +$TMUX new -d -s group-b -t group-a || fail "new-session group-b failed" +wait_for @sg "0|session-added-to-group:group-a:group-a:1|session-added-to-group:group-b:group-a:2" +$TMUX kill-session -t group-b || fail "kill-session group-b failed" +wait_for @sg "0|session-added-to-group:group-a:group-a:1|session-added-to-group:group-b:group-a:2|session-removed-from-group:group-b:group-a:2" +$TMUX set-hook -gu session-added-to-group || + fail "unset session-added-to-group failed" +$TMUX set-hook -gu session-removed-from-group || + fail "unset session-removed-from-group failed" +$TMUX kill-session -t group-a || fail "kill-session group-a failed" + +# window-resized carries old and new dimensions. +old_size=$($TMUX display -pt main:0 '#{window_width},#{window_height}') || + fail "display old window size failed" +$TMUX set -g @wr 0 || fail "set @wr failed" +$TMUX set-hook -g window-resized \ + 'set -gF @wr "#{hook}:#{hook_window}:#{hook_old_width},#{hook_old_height}->#{hook_width},#{hook_height}"' || + fail "set-hook window-resized failed" +$TMUX resizew -t main:0 -x 70 -y 20 || fail "resize-window failed" +new_size=$($TMUX display -pt main:0 '#{window_width},#{window_height}') || + fail "display new window size failed" +wait_for @wr "window-resized:$old_window:$old_size->$new_size" +$TMUX set-hook -gu window-resized || fail "unset window-resized 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}') || @@ -168,18 +237,44 @@ $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. +# pane-title-changed from split-window -T. $TMUX set -g @t 0 || fail "set @t failed" $TMUX set-hook -g pane-title-changed \ - 'set -gF @t "#{hook}:#{hook_pane}:#{pane_title}"' || + 'set -gF @t "#{hook}:#{hook_pane}:#{hook_new_title}:#{pane_title}"' || fail "set-hook pane-title-changed failed" +pane=$($TMUX splitw -d -t main:0 -T splittitle -P -F '#{pane_id}' \ + 'sleep 30') || + fail "split-window title option failed" +wait_for @t "pane-title-changed:$pane:splittitle:splittitle" +$TMUX killp -t "$pane" || fail "kill-pane title option failed" + +# pane-title-changed from select-pane -T. +$TMUX set -g @t 0 || fail "reset @t select title failed" +pane=$($TMUX display -pt main:0.0 '#{pane_id}') || + fail "display select title pane failed" +$TMUX selectp -t "$pane" -T selecttitle || fail "select-pane title failed" +wait_for @t "pane-title-changed:$pane:selecttitle:selecttitle" + +# pane-title-changed when a pane sets its title. +$TMUX set -g @t 0 || fail "reset @t OSC title 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" +wait_for @t "pane-title-changed:$pane:mytitle:mytitle" +$TMUX killp -t "$pane" || fail "kill-pane title failed" + +# pane-title-changed from popping the title stack. +$TMUX set -g @t 0 || fail "reset @t title pop failed" +$TMUX set-hook -g pane-title-changed \ + 'set -gF @t "#{@t}|#{hook_new_title}"' || + fail "set-hook pane-title-changed pop failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' \ + 'printf "\033]2;stackbase\007\033[22;0t\033]2;stacktemp\007\033[23;0t"; sleep 30') || + fail "split-window title pop failed" +wait_for @t "0|stackbase|stacktemp|stackbase" $TMUX set-hook -gu pane-title-changed || fail "unset pane-title-changed failed" -$TMUX killp -t "$pane" || fail "kill-pane title failed" +$TMUX killp -t "$pane" || fail "kill-pane title pop failed" # pane-created payload from a command pane, an empty pane, and a respawn. $TMUX set -g @pc 0 || fail "set @pc failed" @@ -204,6 +299,82 @@ 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-moved from break-pane and join-pane. +$TMUX neww -d -t main: -n move-a 'sleep 30' || + fail "new-window move-a failed" +pane=$($TMUX splitw -d -t main:move-a -P -F '#{pane_id}' 'sleep 30') || + fail "split-window pane-moved failed" +target=$($TMUX display -pt main:move-a.0 '#{pane_id}') || + fail "display pane-moved target failed" +old_window=$($TMUX display -pt "$pane" '#{window_id}') || + fail "display pane-moved old window failed" +old_index=$($TMUX display -pt "$pane" '#{window_index}') || + fail "display pane-moved old index failed" +$TMUX set -g @pm 0 || fail "set @pm failed" +$TMUX set-hook -g pane-moved \ + 'set -gF @pm "#{hook}:#{hook_pane}:#{hook_old_window}:#{hook_old_window_index}->#{hook_new_window}:#{hook_new_window_index}"' || + fail "set-hook pane-moved failed" +$TMUX breakp -d -s "$pane" -n move-b -t main: || + fail "break-pane pane-moved failed" +new_window=$($TMUX display -pt "$pane" '#{window_id}') || + fail "display pane-moved break new window failed" +new_index=$($TMUX display -pt "$pane" '#{window_index}') || + fail "display pane-moved break new index failed" +wait_for @pm "pane-moved:$pane:$old_window:$old_index->$new_window:$new_index" + +$TMUX set -g @pm 0 || fail "reset @pm join failed" +old_window=$new_window +old_index=$new_index +$TMUX joinp -d -s "$pane" -t "$target" || + fail "join-pane pane-moved failed" +new_window=$($TMUX display -pt "$pane" '#{window_id}') || + fail "display pane-moved join new window failed" +new_index=$($TMUX display -pt "$pane" '#{window_index}') || + fail "display pane-moved join new index failed" +wait_for @pm "pane-moved:$pane:$old_window:$old_index->$new_window:$new_index" +$TMUX kill-window -t main:move-a || fail "kill-window move-a failed" + +# pane-moved from cross-window swap-pane fires once for each pane. +$TMUX neww -d -t main: -n swap-a 'sleep 30' || + fail "new-window swap-a failed" +$TMUX neww -d -t main: -n swap-b 'sleep 30' || + fail "new-window swap-b failed" +pane1=$($TMUX display -pt main:swap-a.0 '#{pane_id}') || + fail "display swap pane1 failed" +pane2=$($TMUX display -pt main:swap-b.0 '#{pane_id}') || + fail "display swap pane2 failed" +window1=$($TMUX display -pt "$pane1" '#{window_id}') || + fail "display swap window1 failed" +window2=$($TMUX display -pt "$pane2" '#{window_id}') || + fail "display swap window2 failed" +index1=$($TMUX display -pt "$pane1" '#{window_index}') || + fail "display swap index1 failed" +index2=$($TMUX display -pt "$pane2" '#{window_index}') || + fail "display swap index2 failed" +$TMUX set -g @pm 0 || fail "reset @pm swap failed" +$TMUX set-hook -g pane-moved \ + 'set -gF @pm "#{@pm}|#{hook_pane}:#{hook_old_window}:#{hook_old_window_index}->#{hook_new_window}:#{hook_new_window_index}"' || + fail "set-hook pane-moved swap failed" +$TMUX swapp -d -s "$pane1" -t "$pane2" || + fail "swap-pane pane-moved failed" +wait_for @pm "0|$pane1:$window1:$index1->$window2:$index2|$pane2:$window2:$index2->$window1:$index1" +$TMUX set-hook -gu pane-moved || fail "unset pane-moved failed" +$TMUX kill-window -t main:swap-a || fail "kill-window swap-a failed" +$TMUX kill-window -t main:swap-b || fail "kill-window swap-b failed" + +# pane-bell fires for the actual pane even when bell alerts are disabled. +pane=$($TMUX neww -d -t main: -n raw-bell -P -F '#{pane_id}' 'cat') || + fail "new-window pane-bell failed" +$TMUX setw -t main:raw-bell monitor-bell off || + fail "set monitor-bell off failed" +$TMUX set -g @pb 0 || fail "set @pb failed" +$TMUX set-hook -g pane-bell 'set -gF @pb "#{hook}:#{hook_pane}"' || + fail "set-hook pane-bell failed" +$TMUX send-keys -t "$pane" -H 07 0a || fail "send-keys pane-bell failed" +wait_for @pb "pane-bell:$pane" +$TMUX set-hook -gu pane-bell || fail "unset pane-bell failed" +$TMUX kill-window -t main:raw-bell || fail "kill-window pane-bell 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" @@ -276,6 +447,13 @@ $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 -g @cl 0 || fail "set @cl failed" +$TMUX set-hook -g client-created \ + 'set -gF @cl "#{@cl}|#{hook}:#{hook_client}"' || + fail "set-hook client-created failed" +$TMUX set-hook -g client-closed \ + 'set -gF @cl "#{@cl}|#{hook}:#{hook_client}"' || + fail "set-hook client-closed 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}"' || @@ -284,9 +462,72 @@ 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' + +# client-session-changed carries old and new sessions. +$TMUX new -d -s client-target || fail "new-session client target failed" +client=$($TMUX list-clients -F '#{client_name} #{client_control_mode}' | + awk '$2 == 1 { print $1; exit }') || + fail "list control client failed" +[ -n "$client" ] || fail "no control client found" +wait_for @cl "0|client-created:$client" +old_session=$($TMUX display -pt main '#{session_id}') || + fail "display old client session failed" +new_session=$($TMUX display -pt client-target '#{session_id}') || + fail "display new client session failed" +$TMUX set -g @cs 0 || fail "set @cs failed" +$TMUX set-hook -g client-session-changed \ + 'set -gF @cs "#{hook}:#{hook_client}:#{hook_session}:#{hook_old_session}:#{hook_new_session}"' || + fail "set-hook client-session-changed failed" +$TMUX switch-client -c "$client" -t client-target || + fail "switch-client session failed" +wait_for @cs "client-session-changed:$client:$new_session:$old_session:$new_session" +$TMUX set-hook -gu client-session-changed || + fail "unset client-session-changed failed" + exec 3>&- wait_for @a 'client-detached' +wait_for @cl "0|client-created:$client|client-closed:$client" +$TMUX set-hook -gu client-created || fail "unset client-created failed" +$TMUX set-hook -gu client-closed || fail "unset client-closed failed" $TMUX set-hook -gu client-attached || fail "unset client-attached failed" $TMUX set-hook -gu client-detached || fail "unset client-detached failed" +# client-resized carries old and new dimensions. +$TMUX2 new -d -s resize -x 80 -y 24 'sleep 30' || + fail "new-session client-resized failed" +$TMUX2 set -g @cr 0 || fail "set @cr failed" +$TMUX2 set-hook -g client-resized \ + 'set -gF @cr "#{hook}:#{hook_client}:#{hook_old_width},#{hook_old_height}->#{hook_width},#{hook_height}"' || + fail "set-hook client-resized failed" +$TMUX neww -d -n resize-client "$TMUX2 attach -t resize" || + fail "new outer window for client-resized failed" +i=0 +client= +while [ $i -lt 30 ]; do + client=$($TMUX2 list-clients -F '#{client_name}' 2>/dev/null | + awk 'NR == 1 { print; exit }') + [ -n "$client" ] && break + i=$((i + 1)) + sleep 0.2 +done +[ -n "$client" ] || fail "inner client did not attach" +old_size=$($TMUX2 list-clients -F '#{client_width},#{client_height}') || + fail "display old client size failed" +$TMUX resizew -t resize-client -x 70 -y 20 || + fail "resize outer client window failed" +i=0 +while [ $i -lt 30 ]; do + value=$($TMUX2 show -gqv @cr 2>/dev/null || true) + new_size=$($TMUX2 list-clients -F '#{client_width},#{client_height}' \ + 2>/dev/null || true) + expected="client-resized:$client:$old_size->$new_size" + [ "$new_size" != "$old_size" ] && [ "$value" = "$expected" ] && + break + i=$((i + 1)) + sleep 0.2 +done +[ "$value" = "$expected" ] || + fail "expected @cr to be '$expected' but got '$value'" +$TMUX2 set-hook -gu client-resized || fail "unset client-resized failed" + exit 0 From 447d804a3a23f38b76c2014e270186cdb28cea33 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 09:37:39 +0000 Subject: [PATCH 04/36] Size comparison should be >= not >, from Yayo Razo. --- cmd.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd.c b/cmd.c index d2e98d466..01f11834b 100644 --- a/cmd.c +++ b/cmd.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd.c,v 1.186 2026/07/12 20:35:52 nicm Exp $ */ +/* $OpenBSD: cmd.c,v 1.187 2026/07/13 09:37:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -872,7 +872,7 @@ cmd_template_replace(const char *template, const char *s, int idx) ptr++; slen = strlen(s); - if (slen > SIZE_MAX / 3 || len > SIZE_MAX - (slen * 3) - 1) + 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++) { From 6d227a84791536449213f44cf0354b7bf9c86f25 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 09:42:12 +0000 Subject: [PATCH 05/36] Set other cell when creating layout set, from Dane Jensen. --- layout-set.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/layout-set.c b/layout-set.c index 7e944d32f..65cb38918 100644 --- a/layout-set.c +++ b/layout-set.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout-set.c,v 1.39 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: layout-set.c,v 1.40 2026/07/13 09:42:12 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -135,6 +135,21 @@ layout_set_first_tiled(struct window *w) return (NULL); } +static void +layout_set_link_floating(struct window *w, struct layout_cell *lcroot) +{ + struct window_pane *wp; + struct layout_cell *lc; + + TAILQ_FOREACH(wp, &w->panes, entry) { + lc = wp->layout_cell; + if (!layout_cell_is_tiled(lc)) { + TAILQ_INSERT_TAIL(&lcroot->cells, lc, entry); + lc->parent = lcroot; + } + } +} + static void layout_set_even(struct window *w, enum layout_type type) { @@ -267,6 +282,8 @@ layout_set_main_h(struct window *w) wp = TAILQ_NEXT(wp, entry); TAILQ_INSERT_TAIL(&lcroot->cells, wp->layout_cell, entry); wp->layout_cell->parent = lcroot; + layout_set_size(wp->layout_cell, sx, otherh, 0, 0); + layout_set_link_floating(w, lcroot); } else { lcother = layout_create_cell(lcroot); layout_set_size(lcother, sx, otherh, 0, 0); @@ -364,6 +381,8 @@ layout_set_main_h_mirrored(struct window *w) wp = TAILQ_NEXT(wp, entry); TAILQ_INSERT_HEAD(&lcroot->cells, wp->layout_cell, entry); wp->layout_cell->parent = lcroot; + layout_set_size(wp->layout_cell, sx, otherh, 0, 0); + layout_set_link_floating(w, lcroot); } else { lcother = layout_create_cell(lcroot); layout_set_size(lcother, sx, otherh, 0, 0); @@ -461,6 +480,8 @@ layout_set_main_v(struct window *w) wp = TAILQ_NEXT(wp, entry); TAILQ_INSERT_TAIL(&lcroot->cells, wp->layout_cell, entry); wp->layout_cell->parent = lcroot; + layout_set_size(wp->layout_cell, otherw, sy, 0, 0); + layout_set_link_floating(w, lcroot); } else { lcother = layout_create_cell(lcroot); layout_make_node(lcother, LAYOUT_TOPBOTTOM); @@ -559,6 +580,8 @@ layout_set_main_v_mirrored(struct window *w) wp = TAILQ_NEXT(wp, entry); TAILQ_INSERT_HEAD(&lcroot->cells, wp->layout_cell, entry); wp->layout_cell->parent = lcroot; + layout_set_size(wp->layout_cell, otherw, sy, 0, 0); + layout_set_link_floating(w, lcroot); } else { lcother = layout_create_cell(lcroot); layout_make_node(lcother, LAYOUT_TOPBOTTOM); @@ -700,6 +723,7 @@ layout_set_tiled(struct window *w) w->sy - used); } + layout_set_link_floating(w, lcroot); layout_fix_offsets(w); layout_fix_panes(w, NULL); From 2b091d91d38c88b9dc9bbfaea08b6f80b56f6573 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 10:03:27 +0000 Subject: [PATCH 06/36] Make split-window inside a floating pane work more nicely and not immediately create an overlapping pane, from Dane Jensen. --- cmd-join-pane.c | 9 ++- cmd-split-window.c | 35 +++++----- layout.c | 158 ++++++++++++++++++++++++++++++++++++++++----- tmux.1 | 19 +++++- tmux.h | 9 ++- 5 files changed, 194 insertions(+), 36 deletions(-) diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 1b290a01e..01fbfbe5b 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-join-pane.c,v 1.69 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-join-pane.c,v 1.70 2026/07/13 10:03:27 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -459,6 +459,13 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_ERROR); } + if (args_has(args, 'h')) + flags |= SPAWN_HORIZONTAL; + if (args_has(args, 'b')) + flags |= SPAWN_BEFORE; + if (args_has(args, 'f')) + flags |= SPAWN_FULLSIZE; + lc = layout_get_tiled_cell(item, args, dst_w, dst_wp, flags, &cause); if (cause != NULL) { cmdq_error(item, "size or position %s", cause); diff --git a/cmd-split-window.c b/cmd-split-window.c index 64fa6b877..22c346c8b 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.141 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.142 2026/07/13 10:03:27 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -62,8 +62,8 @@ const struct cmd_entry cmd_split_window_entry = { .usage = "[-bdefhIklPvWZ] [-c start-directory] [-e environment] " "[-F format] [-l size] [-m message] [-p percentage] " "[-s style] [-S active-border-style] " - "[-R inactive-border-style] [-T title] " CMD_TARGET_PANE_USAGE " " - "[shell-command [argument ...]]", + "[-R inactive-border-style] [-T title] " + CMD_TARGET_PANE_USAGE " [shell-command [argument ...]]", .target = { 't', CMD_FIND_PANE, 0 }, @@ -96,14 +96,23 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (cmd_get_entry(self) == &cmd_new_pane_entry) is_floating = !args_has(args, 'L'); - else + else { is_floating = window_pane_is_floating(wp); + flags |= SPAWN_SPLIT; + } - flags = is_floating ? SPAWN_FLOATING : 0; + if (is_floating) + flags |= SPAWN_FLOATING; + if (args_has(args, 'h')) + flags |= SPAWN_HORIZONTAL; if (args_has(args, 'b')) flags |= SPAWN_BEFORE; if (args_has(args, 'f')) flags |= SPAWN_FULLSIZE; + if (args_has(args, 'd')) + flags |= SPAWN_DETACHED; + if (args_has(args, 'Z')) + flags |= SPAWN_ZOOM; input = args_has(args, 'I'); if (input || (count == 1 && *args_string(args, 0) == '\0')) @@ -131,9 +140,10 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) } } - if (is_floating) - lc = layout_get_floating_cell(item, args, lines, w, wp, &cause); - else + if (flags & SPAWN_FLOATING) { + lc = layout_get_floating_cell(item, args, lines, w, wp, flags, + &cause); + } else lc = layout_get_tiled_cell(item, args, w, wp, flags, &cause); if (cause != NULL) { cmdq_error(item, "%s", cause); @@ -159,12 +169,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) sc.idx = -1; sc.cwd = args_get(args, 'c'); - sc.flags = flags; - if (args_has(args, 'd')) - sc.flags |= SPAWN_DETACHED; - if (args_has(args, 'Z')) - sc.flags |= SPAWN_ZOOM; if ((new_wp = spawn_pane(&sc, &cause)) == NULL) { cmdq_error(item, "create pane failed: %s", cause); @@ -240,10 +245,10 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) break; } } - if (!args_has(args, 'd')) + if (~flags & SPAWN_DETACHED) cmd_find_from_winlink_pane(current, wl, new_wp, 0); - if (!is_floating) { + if (~flags & SPAWN_FLOATING) { window_pop_zoom(wp->window); server_redraw_window(wp->window); } diff --git a/layout.c b/layout.c index 177d2f4cf..bddad3ab8 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout.c,v 1.93 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: layout.c,v 1.94 2026/07/13 10:03:27 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -1598,7 +1598,7 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args, struct window *w, struct window_pane *wp, int flags, char **cause) { struct layout_cell *lc; - enum layout_type type; + enum layout_type type = LAYOUT_TOPBOTTOM; u_int curval; int size = -1; char *error = NULL; @@ -1608,12 +1608,11 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args, return (NULL); } - type = LAYOUT_TOPBOTTOM; - if (args_has(args, 'h')) + if (flags & SPAWN_HORIZONTAL) type = LAYOUT_LEFTRIGHT; if (args_has(args, 'l') || args_has(args, 'p')) { - if (args_has(args, 'f')) { + if (flags & SPAWN_FULLSIZE) { if (type == LAYOUT_TOPBOTTOM) curval = w->sy; else @@ -1641,12 +1640,7 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args, return (NULL); } - if (args_has(args, 'b')) - flags |= SPAWN_BEFORE; - if (args_has(args, 'f')) - flags |= SPAWN_FULLSIZE; - - window_push_zoom(wp->window, 1, args_has(args, 'Z')); + window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM)); lc = layout_split_pane(wp, type, size, flags); if (lc == NULL) *cause = xstrdup("no space for a new pane"); @@ -1656,17 +1650,24 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args, struct layout_cell * layout_get_floating_cell(struct cmdq_item *item, struct args *args, - enum pane_lines lines, struct window *w, struct window_pane *wp, + enum pane_lines lines, struct window *w, struct window_pane *wp, int flags, char **cause) { - struct layout_cell *lcnew; + struct layout_cell *lcnew, *lc = wp->layout_cell; struct layout_geometry fg; layout_geometry_init(&fg); - if (layout_floating_args_parse(item, args, lines, w, &fg, cause) != 0) - return (NULL); + if (flags & SPAWN_SPLIT) { + if (layout_split_floating_cell(lc, w, &fg, lines, flags, cause) + != 0) + return (NULL); + } else { + if (layout_floating_args_parse(item, args, lines, w, &fg, cause) + != 0) + return (NULL); + } - window_push_zoom(wp->window, 1, args_has(args, 'Z')); + window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM)); lcnew = layout_floating_pane(w, wp, &fg); return (lcnew); } @@ -1766,6 +1767,131 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args, return (0); } +int +layout_split_floating_cell(struct layout_cell *lc, struct window *w, + struct layout_geometry *out, enum pane_lines lines, int flags, + char **cause) +{ + struct layout_geometry old, new; + int tborder = 1, bborder = w->sy - 1; + int lborder = 3, rborder = w->sx - 3; + int border = lines != PANE_LINES_NONE ? 1 : 0; + int size, space; + + /* First, move the target cell in-bounds. */ + memcpy(&old, &lc->g, sizeof old); + if (lborder > old.xoff - border) + old.xoff = lborder + border; + if (rborder < old.xoff + (int)old.sx + border) + old.xoff = rborder - (int)old.sx - border; + if (tborder > old.yoff - border) + old.yoff = tborder + border; + if (bborder < old.yoff + (int)old.sy + border) + old.yoff = bborder - (int)old.sy - border; + + /* Move the new cell to its ideal position. */ + memcpy(&new, &old, sizeof new); + if (flags & SPAWN_HORIZONTAL) { + if (flags & SPAWN_BEFORE) + new.xoff -= old.sx + 2 * border; + else + new.xoff += old.sx + 2 * border; + } else { + if (flags & SPAWN_BEFORE) + new.yoff -= old.sy + 2 * border; + else + new.yoff += old.sy + 2 * border; + } + + /* + * The position of the new cell is checked to see if it is in bounds. + * If it isn't, the availible space is split and equally given to both + * cells. Only one border is check because the target cell is in bounds + * already. + */ + if (lborder > new.xoff - border) { + /* + * The space for both panes is calculated. Since the offsets are + * associated to where pane contents start, we remove pane + * borders from the space. '1' is added in case the space is + * odd. + */ + space = old.xoff + old.sx - lborder - 3 * border + 1; + size = space / 2; + new.sx = size; + old.sx = size; + new.xoff = lborder + border; + old.xoff = new.xoff + new.sx + 2 * border; + /* + * If the original space was to be odd (now even), subtract 1 + * from the rightmost cell + */ + if (space % 2 == 0) + old.sx -= 1; + } else if (rborder < new.xoff + (int)new.sx + border) { + space = rborder - old.xoff - 3 * border + 1; + size = space / 2; + new.sx = size; + old.sx = size; + new.xoff = old.xoff + old.sx + 2 * border; + if (space % 2 == 0) + new.sx -= 1; + } else if (tborder > new.yoff - border) { + space = old.sy + old.yoff - tborder - 3 * border + 1; + size = space / 2; + new.sy = size; + old.sy = size; + new.yoff = tborder + border; + old.yoff = new.yoff + new.sy + 2 * border; + if (space % 2 == 0) + old.sy -= 1; + } else if (bborder < new.yoff + (int)new.sy + border) { + space = bborder - old.yoff - 3 * border + 1; + size = space / 2; + new.sy = size; + old.sy = size; + new.yoff = old.yoff + old.sy + 2 * border; + if (space % 2 == 0) + new.sy -= 1; + } + + /* + * Expand the cell to occupy the whole availible space where it was + * spawned. + */ + if (flags & SPAWN_FULLSIZE) { + if (flags & SPAWN_HORIZONTAL) { + new.yoff = tborder + border; + new.sy = bborder - tborder - 2 * border; + if (flags & SPAWN_BEFORE) { + new.xoff = lborder + border; + new.sx = old.xoff - new.xoff - 2 * border; + } else { + new.sx = rborder - new.xoff - border; + } + } else { + new.xoff = lborder + border; + new.sx = rborder - lborder - 2 * border; + if (flags & SPAWN_BEFORE) { + new.yoff = tborder + border; + new.sy = old.yoff - new.yoff - 2 * border; + } else { + new.sy = bborder - new.yoff - border; + } + } + } + + if (new.sx < PANE_MINIMUM || new.sy < PANE_MINIMUM || + old.sx < PANE_MINIMUM || old.sy < PANE_MINIMUM) { + *cause = xstrdup("no space for a new pane"); + return (-1); + } + + layout_set_size(lc, old.sx, old.sy, old.xoff, old.yoff); + memcpy(out, &new, sizeof *out); + return (0); +} + /* * Removes a cell from the tiled layout by giving the cell's space to the * nearest neighbour. diff --git a/tmux.1 b/tmux.1 index 262187889..01d654a25 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1132 2026/07/10 15:20:06 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1133 2026/07/13 10:03:27 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: July 10 2026 $ +.Dd $Mdocdate: July 13 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -4075,6 +4075,21 @@ or full window width (with .Fl v ) , instead of splitting the active pane. .Pp +If +.Ar target\-pane +is floating, a new floating pane is created adjacent to +.Ar target\-pane +with equal sizes. +The position is determined by the +.Fl h , +.Fl v , +and +.Fl b +flags. +If any of the new pane would not be visible on the screen, +.Ar target\-pane +and the new pane are resized to fit. +.Pp .Fl k keeps the pane open after the optional .Ar shell\-command diff --git a/tmux.h b/tmux.h index 5031a581f..387911558 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1396 2026/07/10 15:45:11 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1397 2026/07/13 10:03:27 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2455,6 +2455,8 @@ struct spawn_context { #define SPAWN_EMPTY 0x40 #define SPAWN_ZOOM 0x80 #define SPAWN_FLOATING 0x100 +#define SPAWN_HORIZONTAL 0x200 +#define SPAWN_SPLIT 0x400 }; /* Paste buffer. */ @@ -3790,10 +3792,13 @@ 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 **); + int, char **); int layout_floating_args_parse(struct cmdq_item *, struct args *, enum pane_lines, struct window *, struct layout_geometry *, char **); +int layout_split_floating_cell(struct layout_cell *, + struct window *, struct layout_geometry *, enum pane_lines, + int, char **); int layout_remove_tile(struct window *, struct layout_cell *); int layout_insert_tile(struct window *, struct layout_cell *); From fe1b877410c9f4b5ad351427b9de9097d606e927 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 10:22:52 +0000 Subject: [PATCH 07/36] Allow drag on a selection to modify it, from Michael Grant. --- window-copy.c | 159 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 151 insertions(+), 8 deletions(-) diff --git a/window-copy.c b/window-copy.c index 997cb8888..0e27af5c5 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.413 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.414 2026/07/13 10:22:52 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -103,8 +103,12 @@ static void window_copy_goto_line(struct window_mode_entry *, const char *); static void window_copy_update_cursor(struct window_mode_entry *, u_int, u_int); static void window_copy_start_selection(struct window_mode_entry *); +static int window_copy_mouse_in_selection(struct window_mode_entry *, + u_int, u_int, int *, int *); static int window_copy_adjust_selection(struct window_mode_entry *, u_int *, u_int *); +static int window_copy_update_selection_view(struct window_mode_entry *, + int, int); static int window_copy_set_selection(struct window_mode_entry *, int, int); static int window_copy_update_selection(struct window_mode_entry *, int, int); @@ -862,7 +866,7 @@ window_copy_scroll1(struct window_mode_entry *wme, struct window_pane *wp, if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); - window_copy_update_selection(wme, 1, 0); + window_copy_update_selection_view(wme, 1, 0); window_pane_scrollbar_show(wp, 1); window_copy_redraw_screen(wme, 1); } @@ -1648,7 +1652,7 @@ window_copy_cmd_scroll_to(struct window_copy_cmd_state *cs, u_int to) data->cy += delta; } - window_copy_update_selection(wme, 0, 0); + window_copy_update_selection_view(wme, 0, 0); return (WINDOW_COPY_CMD_REDRAW); } @@ -2361,6 +2365,7 @@ window_copy_cmd_scroll_down(struct window_copy_cmd_state *cs) 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) @@ -2374,6 +2379,17 @@ window_copy_cmd_scroll_down_and_cancel(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; + int dragging; + + /* With a selection but no active drag, only scroll the view. */ + dragging = (cs->c != NULL && cs->c->tty.mouse_drag_flag != 0); + if (data->screen.sel != NULL && !dragging) { + data->cursordrag = CURSORDRAG_NONE; + data->lineflag = LINE_SEL_NONE; + /* Move down in the history. */ + window_copy_scroll_up(wme, np); + return (WINDOW_COPY_CMD_NOTHING); + } for (; np != 0; np--) window_copy_cursor_down(wme, 1); @@ -2388,6 +2404,7 @@ 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; + int dragging; /* * If at the top, nothing can change, so return WINDOW_COPY_CMD_NOTHING @@ -2395,6 +2412,17 @@ window_copy_cmd_scroll_up(struct window_copy_cmd_state *cs) */ if (data->oy == screen_hsize(data->backing)) return (WINDOW_COPY_CMD_NOTHING); + + /* With a selection but no active drag, only scroll the view. */ + dragging = (cs->c != NULL && cs->c->tty.mouse_drag_flag != 0); + if (data->screen.sel != NULL && !dragging) { + data->cursordrag = CURSORDRAG_NONE; + data->lineflag = LINE_SEL_NONE; + /* Move up in the history. */ + window_copy_scroll_down(wme, np); + return (WINDOW_COPY_CMD_NOTHING); + } + for (; np != 0; np--) window_copy_cursor_up(wme, 1); return (WINDOW_COPY_CMD_MOVE); @@ -3117,7 +3145,7 @@ window_copy_cmd_recentre_top_bottom(struct window_copy_cmd_state *cs) data->cy = cy + (data->oy - oy); break; } - window_copy_update_selection(wme, 0, 0); + window_copy_update_selection_view(wme, 0, 0); return (WINDOW_COPY_CMD_REDRAW); } @@ -5504,6 +5532,76 @@ window_copy_start_selection(struct window_mode_entry *wme) window_copy_set_selection(wme, 1, 0); } +/* + * Return whether x,y is inside the selection and optionally identify the + * endpoint. + */ +static int +window_copy_mouse_in_selection(struct window_mode_entry *wme, u_int x, u_int y, + int *on_start, int *on_end) +{ + struct window_copy_mode_data *data = wme->data; + u_int sx = screen_size_x(&data->screen); + u_int sy = screen_size_y(&data->screen); + u_int hsize, screeny; + u_int mx, my, selx, sely, endselx, endsely; + u_int cursorx, cursory; + long long mpos, spos, epos, dstart, dend; + + if (on_start != NULL) + *on_start = 0; + if (on_end != NULL) + *on_end = 0; + if (data->screen.sel == NULL) + return (0); + + hsize = screen_hsize(data->backing); + screeny = hsize - data->oy; + + selx = window_copy_cursor_offset(wme, data->selx, sx); + sely = data->sely - screeny; + if (data->sely >= screeny && sely < sy && x == selx && y == sely) { + if (on_start != NULL) + *on_start = 1; + return (1); + } + + endselx = window_copy_cursor_offset(wme, data->endselx, sx); + endsely = data->endsely - screeny; + if (data->endsely >= screeny && + endsely < sy && + x == endselx && y == endsely) { + if (on_end != NULL) + *on_end = 1; + return (1); + } + + cursorx = window_copy_cursor_offset(wme, data->cx, + sx); cursory = data->cy; + if (x != cursorx || y != cursory) { + if (!screen_check_selection(&data->screen, x, y)) + return (0); + } + + if (on_start != NULL || on_end != NULL) { + mx = window_copy_cursor_unoffset(wme, x, sx); + my = screeny + y; + mpos = (long long)my * (sx + 1) + mx; + spos = (long long)data->sely * (sx + 1) + data->selx; + epos = (long long)data->endsely * (sx + 1) + data->endselx; + dstart = llabs(mpos - spos); + dend = llabs(mpos - epos); + if (dstart <= dend) { + if (on_start != NULL) + *on_start = 1; + } else { + if (on_end != NULL) + *on_end = 1; + } + } + return (1); +} + static int window_copy_adjust_selection(struct window_mode_entry *wme, u_int *selx, u_int *sely) @@ -5549,6 +5647,35 @@ window_copy_update_selection(struct window_mode_entry *wme, int may_redraw, return (window_copy_set_selection(wme, may_redraw, no_reset)); } +/* + * Update the visible selection after the view has changed. If the mouse is + * not dragging, keep the selection endpoints anchored to the same text. + */ +static int +window_copy_update_selection_view(struct window_mode_entry *wme, int may_redraw, + int no_reset) +{ + struct window_copy_mode_data *data = wme->data; + u_int selx, sely, endselx, endsely; + int changed; + + if (data->cursordrag != CURSORDRAG_NONE) + return (window_copy_update_selection(wme, may_redraw, no_reset)); + + selx = data->selx; + sely = data->sely; + endselx = data->endselx; + endsely = data->endsely; + + changed = window_copy_update_selection(wme, may_redraw, 1); + + data->selx = selx; + data->sely = sely; + data->endselx = endselx; + data->endsely = endsely; + return (changed); +} + static int window_copy_set_selection(struct window_mode_entry *wme, int may_redraw, int no_reset) @@ -6578,7 +6705,7 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny) if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); - window_copy_update_selection(wme, 0, 0); + window_copy_update_selection_view(wme, 0, 0); if (window_copy_line_numbers_active(wme)) { if (window_copy_line_number_mode(wme) != WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { @@ -6649,7 +6776,7 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); - window_copy_update_selection(wme, 0, 0); + window_copy_update_selection_view(wme, 0, 0); if (window_copy_line_numbers_active(wme)) { if (window_copy_line_number_mode(wme) != WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { @@ -6738,6 +6865,7 @@ window_copy_start_drag(struct client *c, struct mouse_event *m) struct window_mode_entry *wme; struct window_copy_mode_data *data; u_int x, y, yg; + int inside_selection, on_start, on_end; if (c == NULL) return; @@ -6758,10 +6886,16 @@ window_copy_start_drag(struct client *c, struct mouse_event *m) c->tty.mouse_drag_release = window_copy_drag_release; data = wme->data; + on_start = on_end = 0; + inside_selection = window_copy_mouse_in_selection(wme, x, y, + &on_start, &on_end); x = window_copy_cursor_unoffset(wme, x, screen_size_x(&data->screen)); yg = screen_hsize(data->backing) + y - data->oy; - if (x < data->selrx || x > data->endselrx || yg != data->selry) + if (on_start || on_end || !inside_selection || + x < data->selrx || x > data->endselrx || yg != data->selry) { + data->lineflag = LINE_SEL_NONE; data->selflag = SEL_CHAR; + } switch (data->selflag) { case SEL_WORD: if (data->separators != NULL) { @@ -6777,7 +6911,15 @@ window_copy_start_drag(struct client *c, struct mouse_event *m) break; case SEL_CHAR: window_copy_update_cursor(wme, x, y); - window_copy_start_selection(wme); + if (!inside_selection) + window_copy_start_selection(wme); + else { + if (on_start) + data->cursordrag = CURSORDRAG_SEL; + else if (on_end) + data->cursordrag = CURSORDRAG_ENDSEL; + window_copy_update_selection(wme, 1, 0); + } break; } @@ -6853,6 +6995,7 @@ window_copy_drag_release(struct client *c, struct mouse_event *m) data = wme->data; if (window_copy_line_numbers_active(wme)) window_copy_drag_update(c, m); + data->cursordrag = CURSORDRAG_NONE; evtimer_del(&data->dragtimer); } From 4a06a1f2960605536bf10ef834d8dbf80df4018e Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 10:29:17 +0000 Subject: [PATCH 08/36] Work out a single layout for the entire prompt so it can all be drawn together, fixes centered prompts. GitHub issue 5328. --- prompt.c | 301 +++++++++++++++++++++++++++++++++++++++---------------- tmux.h | 4 +- 2 files changed, 218 insertions(+), 87 deletions(-) diff --git a/prompt.c b/prompt.c index 6187a7a12..c12a249a9 100644 --- a/prompt.c +++ b/prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD: prompt.c,v 1.4 2026/06/26 14:40:30 nicm Exp $ */ +/* $OpenBSD: prompt.c,v 1.5 2026/07/13 10:29:17 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -42,6 +42,8 @@ struct prompt { char *word_separators; struct grid_cell style; struct grid_cell command_style; + char *style_str; + char *command_style_str; enum screen_cursor_style cstyle; enum screen_cursor_style command_cstyle; int ccolour; @@ -61,11 +63,30 @@ struct prompt { char *complete_display; }; +struct prompt_layout { + u_int area_x; + u_int area_width; + + u_int content_x; + u_int content_width; + + u_int label_width; + u_int input_x; + u_int cursor_x; + + u_int input_offset; + u_int input_width; +}; + static char *prompt_complete(struct prompt *, const char *, u_int); static void prompt_clear_complete(struct prompt *); -static char *prompt_expand(struct prompt *); +static struct format_tree *prompt_format_tree(struct prompt *); +static char *prompt_expand1(struct prompt *, struct format_tree *); +static void prompt_effective_style(struct prompt *, struct style *, + struct format_tree *); +static void prompt_layout(struct prompt *, u_int, u_int, + struct prompt_layout *, char **, struct style *); static int prompt_replace_complete(struct prompt *, const char *); -static u_int prompt_width(struct prompt *, u_int); /* Get prompt flags as a string. */ static const char * @@ -120,6 +141,8 @@ prompt_set_options(struct prompt_create_data *pd, struct session *s) style_apply(&pd->style, oo, "message-style", NULL); style_apply(&pd->command_style, oo, "message-command-style", NULL); + pd->style_str = options_get_string(oo, "message-style"); + pd->command_style_str = options_get_string(oo, "message-command-style"); n = options_get_number(oo, "prompt-cursor-style"); screen_set_cursor_style(n, &pd->cstyle, &pd->cmode); n = options_get_number(oo, "prompt-command-cursor-style"); @@ -179,6 +202,8 @@ prompt_create(const struct prompt_create_data *pd) memcpy(&pr->style, &pd->style, sizeof pr->style); memcpy(&pr->command_style, &pd->command_style, sizeof pr->command_style); + pr->style_str = xstrdup(pd->style_str); + pr->command_style_str = xstrdup(pd->command_style_str); pr->cstyle = pd->cstyle; pr->command_cstyle = pd->command_cstyle; pr->ccolour = pd->ccolour; @@ -201,6 +226,8 @@ prompt_free(struct prompt *pr) if (pr->freecb != NULL && pr->data != NULL) pr->freecb(pr->data); free(pr->message_format); + free(pr->style_str); + free(pr->command_style_str); free(pr->word_separators); free(pr->last); free(pr->string); @@ -321,16 +348,17 @@ prompt_redraw_character(struct screen_write_ctx *ctx, u_int offset, * 0 otherwise. */ static int -prompt_redraw_quote(const struct prompt *pr, u_int pcursor, - struct screen_write_ctx *ctx, u_int offset, u_int pwidth, u_int *width, +prompt_redraw_quote(const struct prompt *pr, u_int pcursor, u_int input_x, + struct screen_write_ctx *ctx, u_int offset, u_int pw, u_int *w, struct grid_cell *gc) { struct utf8_data ud; - if (pr->flags & PROMPT_QUOTENEXT && ctx->s->cx == pcursor + 1) { + if (pr->flags & PROMPT_QUOTENEXT && + pcursor >= offset && + ctx->s->cx == input_x + pcursor - offset) { utf8_set(&ud, '^'); - return (prompt_redraw_character(ctx, offset, pwidth, - width, gc, &ud)); + return (prompt_redraw_character(ctx, offset, pw, w, gc, &ud)); } return (1); } @@ -368,12 +396,12 @@ prompt_draw_complete(struct prompt *pr, struct screen_write_ctx *ctx, u_int ax, free(ud); } -/* Expand prompt string using the current input. */ -static char * -prompt_expand(struct prompt *pr) +/* Create the prompt format tree using the current input. */ +static struct format_tree * +prompt_format_tree(struct prompt *pr) { struct format_tree *ft; - char *expanded, *prompt, *tmp; + char *tmp; if (cmd_find_valid_state(&pr->state)) ft = format_create_from_state(NULL, NULL, &pr->state); @@ -385,31 +413,145 @@ prompt_expand(struct prompt *pr) format_add(ft, "prompt_flags", "%s", prompt_flags_to_string(pr->flags)); format_add(ft, "prompt_type", "%s", prompt_type_string(pr->type)); - prompt = format_expand_time(ft, pr->string); - format_add(ft, "message", "%s", prompt); if (pr->flags & PROMPT_COMMANDMODE) format_add(ft, "command_prompt", "1"); else format_add(ft, "command_prompt", "0"); + return (ft); +} + +/* Expand prompt string using the current input. */ +static char * +prompt_expand1(struct prompt *pr, struct format_tree *ft) +{ + char *expanded, *prompt; + + prompt = format_expand_time(ft, pr->string); + format_add(ft, "message", "%s", prompt); expanded = format_expand_time(ft, pr->message_format); free(prompt); - format_free(ft); return (expanded); } -/* Work out the width used by the prompt string. */ -static u_int -prompt_width(struct prompt *pr, u_int aw) +/* Get the effective message style for the current prompt. */ +static void +prompt_effective_style(struct prompt *pr, struct style *sy, + struct format_tree *ft) { - char *expanded; - u_int start; + const char *s; + struct grid_cell *gc; + char *expanded; - expanded = prompt_expand(pr); - start = format_width(expanded); - if (start > aw) - start = aw; - free(expanded); - return (start); + if (pr->flags & PROMPT_COMMANDMODE) { + s = pr->command_style_str; + gc = &pr->command_style; + } else { + s = pr->style_str; + gc = &pr->style; + } + + style_set(sy, gc); + if (s != NULL) { + expanded = format_expand_time(ft, s); + if (style_parse(sy, &grid_default_cell, expanded) != 0) + style_set(sy, gc); + free(expanded); + } +} + +/* Work out where the editable prompt content appears. */ +static void +prompt_layout(struct prompt *pr, u_int ax, u_int aw, struct prompt_layout *pl, + char **expanded, struct style *sy) +{ + char *local = NULL; + struct format_tree *ft; + u_int pcursor, pwidth, end, width, offset, avail; + + memset(pl, 0, sizeof *pl); + pl->area_x = ax; + pl->area_width = aw; + + ft = prompt_format_tree(pr); + if (sy != NULL) + prompt_effective_style(pr, sy, ft); + if (expanded != NULL) + *expanded = prompt_expand1(pr, ft); + else { + local = prompt_expand1(pr, ft); + expanded = &local; + } + format_free(ft); + + if (aw == 0) { + free(local); + return; + } + + pl->label_width = format_width(*expanded); + if (pl->label_width > aw) + pl->label_width = aw; + + pcursor = utf8_strwidth(pr->buffer, pr->index); + pwidth = utf8_strwidth(pr->buffer, -1); + if (pr->flags & PROMPT_QUOTENEXT) + pwidth++; + + avail = aw - pl->label_width; + if (avail == 0) { + pl->input_offset = 0; + pl->input_width = 0; + pl->cursor_x = pl->label_width; + } else { + if (pcursor >= avail) { + offset = (pcursor - avail) + 1; + width = avail; + } else { + offset = 0; + width = pwidth; + } + if (width > avail) + width = avail; + + pl->input_offset = offset; + pl->input_width = width; + pl->cursor_x = pl->label_width + pcursor - offset; + } + + pl->content_width = pl->label_width + pl->input_width; + if (pr->complete_display != NULL && + pr->index == utf8_strlen(pr->buffer) && + pl->cursor_x < aw) { + avail = aw - pl->cursor_x; + width = utf8_cstrwidth(pr->complete_display); + if (width > avail) + width = avail; + end = pl->cursor_x + width; + if (end > pl->content_width) + pl->content_width = end; + } + if (pl->content_width > aw) + pl->content_width = aw; + + if (sy != NULL) { + switch (sy->align) { + case STYLE_ALIGN_CENTRE: + case STYLE_ALIGN_ABSOLUTE_CENTRE: + pl->content_x = ax + (aw - pl->content_width) / 2; + break; + case STYLE_ALIGN_RIGHT: + pl->content_x = ax + aw - pl->content_width; + break; + default: + pl->content_x = ax; + break; + } + } else + pl->content_x = ax; + + pl->input_x = pl->content_x + pl->label_width; + pl->cursor_x += pl->content_x; + free(local); } /* Choose a completion from a mouse position. */ @@ -460,70 +602,59 @@ prompt_draw(struct prompt *pr, struct prompt_draw_data *pd) { struct screen_write_ctx *ctx = pd->ctx; struct screen *s = ctx->s; - u_int ax = pd->area_x, py = pd->prompt_line; - u_int aw = pd->area_width, *cx = pd->cursor_x; + u_int ax = pd->area_x, py = pd->prompt_line, *cx; + u_int aw = pd->area_width; struct grid_cell gc; - u_int i, offset, left, start, width; - u_int pcursor, pwidth; + struct prompt_layout pl; + struct style sy; + u_int i, width, pcursor; char *expanded; - /* Choose the cursor colour and style for this prompt. */ if (pr->flags & PROMPT_COMMANDMODE) { - memcpy(&gc, &pr->command_style, sizeof gc); s->default_cstyle = pr->command_cstyle; s->default_mode = pr->command_cmode; s->default_ccolour = pr->command_ccolour; } else { - memcpy(&gc, &pr->style, sizeof gc); s->default_cstyle = pr->cstyle; s->default_mode = pr->cmode; s->default_ccolour = pr->ccolour; } - expanded = prompt_expand(pr); - start = format_width(expanded); - if (start > aw) - start = aw; - *cx = ax + start; + prompt_layout(pr, ax, aw, &pl, &expanded, &sy); + memcpy(&gc, &sy.gc, sizeof gc); + cx = pd->cursor_x; + *cx = pl.cursor_x; screen_write_cursormove(ctx, ax, py, 0); - format_draw(ctx, &gc, aw, expanded, NULL, 0); - screen_write_cursormove(ctx, ax + start, py, 0); - free(expanded); - - left = aw - start; - if (left == 0) - return; + if (sy.fill != 8) + screen_write_clearcharacter(ctx, aw, sy.fill); pcursor = utf8_strwidth(pr->buffer, pr->index); - pwidth = utf8_strwidth(pr->buffer, -1); - if (pr->flags & PROMPT_QUOTENEXT) - pwidth++; - if (pcursor >= left) { - /* - * The cursor would be outside the screen so start drawing - * with it on the right. - */ - offset = (pcursor - left) + 1; - pwidth = left; - } else - offset = 0; - if (pwidth > left) - pwidth = left; - *cx = ax + start + pcursor - offset; - width = 0; - for (i = 0; pr->buffer[i].size != 0; i++) { - if (!prompt_redraw_quote(pr, pcursor, ctx, offset, pwidth, - &width, &gc)) - break; - if (!prompt_redraw_character(ctx, offset, pwidth, &width, &gc, - &pr->buffer[i])) - break; + if (pl.content_width != 0) { + screen_write_cursormove(ctx, pl.content_x, py, 0); + if (pl.label_width != 0) + format_draw(ctx, &gc, pl.label_width, expanded, NULL, + 0); + + screen_write_cursormove(ctx, pl.input_x, py, 0); + width = 0; + for (i = 0; pr->buffer[i].size != 0; i++) { + if (!prompt_redraw_quote(pr, pcursor, pl.input_x, + ctx, pl.input_offset, pl.input_width, &width, + &gc)) + break; + if (!prompt_redraw_character(ctx, pl.input_offset, + pl.input_width, &width, &gc, &pr->buffer[i])) + break; + } + prompt_redraw_quote(pr, pcursor, pl.input_x, ctx, + pl.input_offset, pl.input_width, &width, &gc); + + prompt_draw_complete(pr, ctx, pl.content_x, pl.content_width, + pl.cursor_x, py, &gc); } - prompt_redraw_quote(pr, pcursor, ctx, offset, pwidth, &width, &gc); - - prompt_draw_complete(pr, ctx, ax, aw, *cx, py, &gc); + free(expanded); } /* Move cursor in prompt from a mouse position. */ @@ -532,36 +663,34 @@ prompt_mouse(struct prompt *pr, u_int x, u_int ax, u_int aw, int *redraw) { struct utf8_data *ud; enum prompt_key_result result; - u_int cx, start, left, pcursor, pwidth, offset, width; - u_int target; + struct prompt_layout pl; + struct style sy; + char *expanded; + u_int pwidth, width, target; size_t idx; if (x < ax || x >= ax + aw) return (PROMPT_KEY_NOT_HANDLED); - start = prompt_width(pr, aw); - left = aw - start; - if (left == 0) + prompt_layout(pr, ax, aw, &pl, &expanded, &sy); + free(expanded); + + if (pl.input_width == 0) return (PROMPT_KEY_HANDLED); - pcursor = utf8_strwidth(pr->buffer, pr->index); pwidth = utf8_strwidth(pr->buffer, -1); if (pr->flags & PROMPT_QUOTENEXT) pwidth++; - if (pcursor >= left) - offset = (pcursor - left) + 1; - else - offset = 0; - cx = ax + start + pcursor - offset; - result = prompt_mouse_complete(pr, x, cx, ax, aw, redraw); + result = prompt_mouse_complete(pr, x, pl.cursor_x, pl.content_x, + pl.content_width, redraw); if (result != PROMPT_KEY_NOT_HANDLED) return (result); - if (x <= ax + start) - target = offset; + if (x <= pl.input_x) + target = pl.input_offset; else - target = offset + x - (ax + start); + target = pl.input_offset + x - pl.input_x; if (target > pwidth) target = pwidth; diff --git a/tmux.h b/tmux.h index 387911558..ea357d08c 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1397 2026/07/13 10:03:27 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1398 2026/07/13 10:29:17 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2086,6 +2086,8 @@ struct prompt_create_data { struct grid_cell style; struct grid_cell command_style; + const char *style_str; + const char *command_style_str; enum screen_cursor_style cstyle; enum screen_cursor_style command_cstyle; int ccolour; From 006cce5e4d7ab6f9c09b988b03b1e040231e3f5c Mon Sep 17 00:00:00 2001 From: jsg Date: Mon, 13 Jul 2026 12:53:56 +0000 Subject: [PATCH 09/36] fix window_copy_mouse_in_selection() indentation; ok nicm@ --- window-copy.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/window-copy.c b/window-copy.c index 0e27af5c5..6d0390fe9 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.414 2026/07/13 10:22:52 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.415 2026/07/13 12:53:56 jsg Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -5576,8 +5576,8 @@ window_copy_mouse_in_selection(struct window_mode_entry *wme, u_int x, u_int y, return (1); } - cursorx = window_copy_cursor_offset(wme, data->cx, - sx); cursory = data->cy; + cursorx = window_copy_cursor_offset(wme, data->cx, sx); + cursory = data->cy; if (x != cursorx || y != cursory) { if (!screen_check_selection(&data->screen, x, y)) return (0); From e00a1a3e2656bdb1aa2b4bb10edf30a3c864adf1 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 13:01:14 +0000 Subject: [PATCH 10/36] Add some additional simple events. --- cmd-break-pane.c | 9 ++++--- cmd-join-pane.c | 3 ++- cmd-swap-pane.c | 9 ++++++- input.c | 8 +++--- options-table.c | 10 ++++++- resize.c | 22 +++++++++++++-- server-client.c | 69 +++++++++++++++++++++++++++++++++++++++++++++--- session.c | 51 ++++++++++++++++++++++++++++++++--- spawn.c | 6 +++-- tmux.1 | 18 ++++++++++++- tmux.h | 4 ++- window.c | 30 +++++++++++++++++++-- 12 files changed, 216 insertions(+), 23 deletions(-) diff --git a/cmd-break-pane.c b/cmd-break-pane.c index 8d85c57b2..dcc5a6af4 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-break-pane.c,v 1.72 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-break-pane.c,v 1.73 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -98,9 +98,9 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) struct session *src_s = source->s; struct session *dst_s = target->s; struct window_pane *wp = source->wp; - struct window *w = wl->window; + struct window *w = wl->window, *old_w = w; char *cause, *cp; - int idx = target->idx, before; + int idx = target->idx, before, old_idx = wl->idx; const char *template, *name = args_get(args, 'n'); if (args_has(args, 'W')) @@ -137,6 +137,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) wl = winlink_find_by_window(&dst_s->windows, w); if (wl == NULL) return (CMD_RETURN_ERROR); + window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx); goto out; } if (idx != -1 && winlink_find_by_index(&dst_s->windows, idx) != NULL) { @@ -175,6 +176,8 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) 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__); + events_fire_window("window-created", w); + window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx); if (!args_has(args, 'd')) { session_select(dst_s, wl->idx); cmd_find_from_session(current, dst_s, 0); diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 01fbfbe5b..927ccda43 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-join-pane.c,v 1.70 2026/07/13 10:03:27 nicm Exp $ */ +/* $OpenBSD: cmd-join-pane.c,v 1.71 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -506,6 +506,7 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) } else server_status_session(dst_s); + window_fire_pane_moved(src_wp, src_w, src_wl->idx, dst_w, dst_idx); if (window_count_panes(src_w, 1) == 0) server_kill_window(src_w, 1); else diff --git a/cmd-swap-pane.c b/cmd-swap-pane.c index da7ea3706..b2e53f315 100644 --- a/cmd-swap-pane.c +++ b/cmd-swap-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-swap-pane.c,v 1.53 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-swap-pane.c,v 1.54 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -68,11 +68,14 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) struct window_pane *tmp_wp, *src_wp, *dst_wp; struct layout_cell *src_lc, *dst_lc; u_int sx, sy, xoff, yoff; + int src_idx, dst_idx; dst_w = target->wl->window; dst_wp = target->wp; + dst_idx = target->wl->idx; src_w = source->wl->window; src_wp = source->wp; + src_idx = source->wl->idx; if (window_push_zoom(dst_w, 0, args_has(args, 'Z'))) server_redraw_window(dst_w); @@ -180,6 +183,10 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) redraw_invalidate_scene(dst_w); server_redraw_window(dst_w); + if (src_w != dst_w) { + window_fire_pane_moved(src_wp, src_w, src_idx, dst_w, dst_idx); + window_fire_pane_moved(dst_wp, dst_w, dst_idx, src_w, src_idx); + } events_fire_window("window-layout-changed", src_w); if (src_w != dst_w) events_fire_window("window-layout-changed", dst_w); diff --git a/input.c b/input.c index fe7297dae..46e7b91aa 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.266 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.267 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1316,8 +1316,10 @@ input_c0_dispatch(struct input_ctx *ictx) case '\000': /* NUL */ break; case '\007': /* BEL */ - if (wp != NULL) + if (wp != NULL) { + events_fire_pane("pane-bell", wp); alerts_queue(wp->window, WINDOW_BELL); + } break; case '\010': /* BS */ screen_write_backspace(sctx); @@ -2180,7 +2182,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); + input_fire_pane_title_changed(wp, sctx->s->title); server_redraw_window_borders(w); server_status_window(w); break; diff --git a/options-table.c b/options-table.c index 49cf9730f..3e8dd5b46 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.233 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.234 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1925,6 +1925,8 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("alert-silence", ""), OPTIONS_TABLE_HOOK("client-active", ""), OPTIONS_TABLE_HOOK("client-attached", ""), + OPTIONS_TABLE_HOOK("client-created", ""), + OPTIONS_TABLE_HOOK("client-closed", ""), OPTIONS_TABLE_HOOK("client-detached", ""), OPTIONS_TABLE_HOOK("client-focus-in", ""), OPTIONS_TABLE_HOOK("client-focus-out", ""), @@ -1934,6 +1936,7 @@ 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-bell", ""), OPTIONS_TABLE_PANE_HOOK("pane-command-finished", ""), OPTIONS_TABLE_PANE_HOOK("pane-command-started", ""), OPTIONS_TABLE_PANE_HOOK("pane-created", ""), @@ -1944,6 +1947,7 @@ const struct options_table_entry options_table[] = { 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-moved", ""), OPTIONS_TABLE_PANE_HOOK("pane-prompt-closed", ""), OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", ""), OPTIONS_TABLE_PANE_HOOK("pane-resized", ""), @@ -1952,8 +1956,12 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_PANE_HOOK("pane-title-changed", ""), OPTIONS_TABLE_HOOK("session-closed", ""), OPTIONS_TABLE_HOOK("session-created", ""), + OPTIONS_TABLE_HOOK("session-added-to-group", ""), OPTIONS_TABLE_HOOK("session-renamed", ""), + OPTIONS_TABLE_HOOK("session-removed-from-group", ""), OPTIONS_TABLE_HOOK("session-window-changed", ""), + OPTIONS_TABLE_WINDOW_HOOK("window-created", ""), + OPTIONS_TABLE_WINDOW_HOOK("window-closed", ""), OPTIONS_TABLE_WINDOW_HOOK("window-layout-changed", ""), OPTIONS_TABLE_HOOK("window-linked", ""), OPTIONS_TABLE_WINDOW_HOOK("window-pane-changed", ""), diff --git a/resize.c b/resize.c index c8257cec0..1d83e4b70 100644 --- a/resize.c +++ b/resize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: resize.c,v 1.56 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: resize.c,v 1.57 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -22,9 +22,27 @@ #include "tmux.h" +static void +resize_fire_window_resized(struct window *w, u_int old_sx, u_int old_sy) +{ + 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_uint(ep, "width", w->sx); + event_payload_set_uint(ep, "height", w->sy); + event_payload_set_uint(ep, "old_width", old_sx); + event_payload_set_uint(ep, "old_height", old_sy); + events_fire("window-resized", ep); +} + void resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) { + u_int old_sx = w->sx, old_sy = w->sy; int zoomed; /* Check size limits. */ @@ -61,7 +79,7 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) tty_update_window_offset(w); server_redraw_window(w); events_fire_window("window-layout-changed", w); - events_fire_window("window-resized", w); + resize_fire_window_resized(w, old_sx, old_sy); w->flags &= ~WINDOW_RESIZE; } diff --git a/server-client.c b/server-client.c index 86360de20..971b8d1aa 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.486 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.487 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -407,6 +407,62 @@ server_client_attached_lost(struct client *c) } } +/* Fire client session changed. */ +static void +server_client_fire_session_changed(struct client *c, struct session *old) +{ + 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); + event_payload_set_session(ep, "new_session", fs.s); + } + if (old != NULL) + event_payload_set_session(ep, "old_session", old); + 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("client-session-changed", ep); +} + +/* Fire client resized. */ +static void +server_client_fire_resized(struct client *c, u_int old_sx, u_int old_sy) +{ + 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); + event_payload_set_uint(ep, "width", c->tty.sx); + event_payload_set_uint(ep, "height", c->tty.sy); + event_payload_set_uint(ep, "old_width", old_sx); + event_payload_set_uint(ep, "old_height", old_sy); + events_fire("client-resized", ep); +} + /* Set client session. */ void server_client_set_session(struct client *c, struct session *s) @@ -433,7 +489,7 @@ server_client_set_session(struct client *c, struct session *s) alerts_check_session(s); tty_update_client_offset(c); status_timer_start(c); - events_fire_client("client-session-changed", c); + server_client_fire_session_changed(c, old); server_redraw_client(c); } @@ -470,6 +526,8 @@ server_client_lost(struct client *c) server_client_attached_lost(c); events_fire_client("client-detached", c); } + if (c->name != NULL && (c->flags & (CLIENT_CONTROL|CLIENT_TERMINAL))) + events_fire_client("client-closed", c); if (c->flags & CLIENT_CONTROL) control_stop(c); @@ -2425,6 +2483,7 @@ server_client_dispatch(struct imsg *imsg, void *arg) struct client *c = arg; ssize_t datalen; struct session *s; + u_int old_sx, old_sy; if (c->flags & CLIENT_DEAD) return; @@ -2463,6 +2522,8 @@ server_client_dispatch(struct imsg *imsg, void *arg) if (c->flags & CLIENT_CONTROL) break; server_client_update_latest(c); + old_sx = c->tty.sx; + old_sy = c->tty.sy; tty_resize(&c->tty); tty_repeat_requests(&c->tty, 0); recalculate_sizes(); @@ -2472,7 +2533,7 @@ server_client_dispatch(struct imsg *imsg, void *arg) c->overlay_resize(c, c->overlay_data); server_redraw_client(c); if (c->session != NULL) - events_fire_client("client-resized", c); + server_client_fire_resized(c, old_sx, old_sy); break; case MSG_EXITING: if (datalen != 0) @@ -2774,6 +2835,8 @@ server_client_dispatch_identify(struct client *c, struct imsg *imsg) close(c->out_fd); c->out_fd = -1; } + if (c->flags & (CLIENT_CONTROL|CLIENT_TERMINAL)) + events_fire_client("client-created", c); /* If pasting has taken too long, turn it off. */ if (c->flags & (CLIENT_BRACKETPASTING|CLIENT_ASSUMEPASTING) && diff --git a/session.c b/session.c index 9c23c4b50..f51df3bbd 100644 --- a/session.c +++ b/session.c @@ -1,4 +1,4 @@ -/* $OpenBSD: session.c,v 1.105 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: session.c,v 1.106 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -473,6 +473,29 @@ session_last(struct session *s) return (session_set_current(s, wl)); } +/* Fire session window changed. */ +static void +session_fire_window_changed(struct session *s, struct winlink *wl, + struct winlink *old) +{ + 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", s); + event_payload_set_window(ep, "window", wl->window); + event_payload_set_window(ep, "new_window", wl->window); + event_payload_set_int(ep, "window_index", wl->idx); + event_payload_set_int(ep, "new_window_index", wl->idx); + if (old != NULL) { + event_payload_set_window(ep, "old_window", old->window); + event_payload_set_int(ep, "old_window_index", old->idx); + } + events_fire("session-window-changed", ep); +} + /* Set current winlink to wl .*/ int session_set_current(struct session *s, struct winlink *wl) @@ -495,7 +518,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); - events_fire_session("session-window-changed", s); + session_fire_window_changed(s, wl, old); return (0); } @@ -542,12 +565,33 @@ session_group_new(const char *name) return (sg); } +/* Fire session group changed. */ +static void +session_group_fire(const char *name, struct session_group *sg, + 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); + event_payload_set_string(ep, "group", "%s", sg->name); + event_payload_set_uint(ep, "group_size", session_group_count(sg)); + events_fire(name, ep); +} + /* Add a session to a session group. */ void session_group_add(struct session_group *sg, struct session *s) { - if (session_group_contains(s) == NULL) + if (session_group_contains(s) == NULL) { TAILQ_INSERT_TAIL(&sg->sessions, s, gentry); + session_group_fire("session-added-to-group", sg, s); + } } /* Remove a session from its group and destroy the group if empty. */ @@ -558,6 +602,7 @@ session_group_remove(struct session *s) if ((sg = session_group_contains(s)) == NULL) return; + session_group_fire("session-removed-from-group", sg, s); TAILQ_REMOVE(&sg->sessions, s, gentry); if (TAILQ_EMPTY(&sg->sessions)) { RB_REMOVE(session_groups, &session_groups, sg); diff --git a/spawn.c b/spawn.c index 21fc25937..5d020b297 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spawn.c,v 1.47 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: spawn.c,v 1.48 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -231,8 +231,10 @@ spawn_window(struct spawn_context *sc, char **cause) session_select(s, sc->wl->idx); /* Fire notification if new window. */ - if (~sc->flags & SPAWN_RESPAWN) + if (~sc->flags & SPAWN_RESPAWN) { + events_fire_window("window-created", w); events_fire_winlink("window-linked", sc->wl); + } session_group_synchronize_from(s); return (sc->wl); diff --git a/tmux.1 b/tmux.1 index 01d654a25..3b4b228f6 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1133 2026/07/13 10:03:27 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1134 2026/07/13 13:01:14 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -6356,6 +6356,10 @@ See Run when a client becomes the latest active client of its session. .It client\-attached Run when a client is attached. +.It client\-closed +Run when a client is closed. +.It client\-created +Run when a client is created. .It client\-detached Run when a client is detached. .It client\-focus\-in @@ -6374,6 +6378,8 @@ 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\-bell +Run when a pane receives a bell. .It pane\-command\-finished Run when an OSC 133 command finishes in a pane. The event payload includes @@ -6407,6 +6413,8 @@ Run when a pane changes mode. Run when a pane enters a mode. .It pane\-mode\-exited Run when a pane exits a mode. +.It pane\-moved +Run when a pane is moved to another window. .It pane\-prompt\-closed Run when a prompt in a pane is closed. .It pane\-prompt\-opened @@ -6425,10 +6433,18 @@ Run when a pane title is changed. Run when a session is closed. .It session\-created Run when a new session is created. +.It session\-added\-to\-group +Run when a session is added to a session group. +.It session\-removed\-from\-group +Run when a session is removed from a session group. .It session\-renamed Run when a session is renamed. .It session\-window\-changed Run when a session changes its active window. +.It window\-closed +Run when a window is closed. +.It window\-created +Run when a window is created. .It window\-layout\-changed Run when a window layout is changed. .It window\-linked diff --git a/tmux.h b/tmux.h index ea357d08c..db6d4cf4e 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1398 2026/07/13 10:29:17 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1399 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3624,6 +3624,8 @@ int window_has_floating_panes(struct window *); int window_has_pane(struct window *, struct window_pane *); int window_set_active_pane(struct window *, struct window_pane *, int); +void window_fire_pane_moved(struct window_pane *, struct window *, + int, struct window *, int); void window_update_focus(struct window *); void window_pane_update_focus(struct window_pane *); void window_redraw_active_switch(struct window *, diff --git a/window.c b/window.c index f6e1a1f23..ca2eca389 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.360 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.361 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -130,6 +130,29 @@ window_fire_pane_changed(struct window *w, struct window_pane *wp, events_fire("window-pane-changed", ep); } +void +window_fire_pane_moved(struct window_pane *wp, struct window *old_w, + int old_idx, struct window *new_w, int new_idx) +{ + 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", new_w); + event_payload_set_window(ep, "old_window", old_w); + event_payload_set_window(ep, "new_window", new_w); + if (old_idx != -1) + event_payload_set_int(ep, "old_window_index", old_idx); + if (new_idx != -1) { + event_payload_set_int(ep, "window_index", new_idx); + event_payload_set_int(ep, "new_window_index", new_idx); + } + events_fire("pane-moved", ep); +} + static void window_fire_pane_mode_changed(const char *name, struct window_pane *wp, const char *previous, const char *current, int entered) @@ -490,6 +513,9 @@ window_add_ref(struct window *w, const char *from) void window_remove_ref(struct window *w, const char *from) { + if (w->references == 1) + events_fire_window("window-closed", w); + w->references--; log_debug("%s: @%u %s, now %d", __func__, w->id, from, w->references); @@ -972,7 +998,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); + window_fire_pane_changed(w, w->active, wp); window_update_focus(w); } } From 4cd39ee33e9d80f41d06ffb6801df893a2fddfb0 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 17:10:04 +0100 Subject: [PATCH 11/36] new-pane mouse test. --- regress/new-pane-mouse.sh | 81 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 regress/new-pane-mouse.sh diff --git a/regress/new-pane-mouse.sh b/regress/new-pane-mouse.sh new file mode 100644 index 000000000..d2ae9ee97 --- /dev/null +++ b/regress/new-pane-mouse.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +# Test new-pane -M creates a floating pane from a mouse drag. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server >/dev/null 2>&1 + $TMUX2 kill-server >/dev/null 2>&1 +} +fail() +{ + echo "$*" >&2 + cleanup + exit 1 +} +must_equal() +{ + got=$1 + want=$2 + [ "$got" = "$want" ] || fail "got '$got', expected '$want'" +} + +# drag STARTCOL STARTROW ENDCOL ENDROW +# +# Write an SGR Ctrl-mouse press, drag update and release at 1-based positions +# to the outer pane holding the inner client. +drag() +{ + scol="$1" + srow="$2" + ecol="$3" + erow="$4" + + seq=$(printf '\033[<16;%s;%sM' "$scol" "$srow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<48;%s;%sM' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<16;%s;%sm' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} + +cleanup + +$TMUX new-session -d -s inner -x 80 -y 24 || exit 1 +$TMUX set -g mouse on +$TMUX set -g default-command 'sleep 100' + +$TMUX2 new-session -d -x 80 -y 24 "$TMUX attach -t inner" || exit 1 +sleep 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." +BASE=$($TMUX list-panes -F '#{pane_id}' | head -1) +[ -n "$BASE" ] || fail "No base pane." + +# Drag from SGR column 3,row 1 to column 10,row 5. These are window positions +# 2,0 to 9,4. With the default border, the floating pane content is inset by +# one cell and has size 6x3. +drag 3 1 10 5 + +id=$($TMUX list-panes -F '#{?pane_floating_flag,#{pane_id},}' | tail -n 1) +[ -n "$id" ] || fail "no floating pane created" + +must_equal "$($TMUX display-message -p -t "$id" '#{pane_left}')" 3 +must_equal "$($TMUX display-message -p -t "$id" '#{pane_top}')" 1 +must_equal "$($TMUX display-message -p -t "$id" '#{pane_width}')" 6 +must_equal "$($TMUX display-message -p -t "$id" '#{pane_height}')" 3 + +$TMUX new-pane -d -M -L -t "$BASE" 'sleep 100' || fail "new-pane -M -L failed" + +cleanup +exit 0 From b91086a5c80fd854f28f47d93aff5d85a886a7ff Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 15:03:03 +0000 Subject: [PATCH 12/36] Add pane-activity event. --- input.c | 6 +++++- options-table.c | 3 ++- server-client.c | 5 +++-- tmux.1 | 11 +++-------- tmux.h | 3 ++- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/input.c b/input.c index 46e7b91aa..66f65e01c 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.267 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.268 2026/07/13 15:03:03 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1051,6 +1051,10 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len) return; window_update_activity(wp->window); + if (~wp->flags & PANE_ACTIVITY) { + wp->flags |= PANE_ACTIVITY; + events_fire_pane("pane-activity", wp); + } wp->flags |= PANE_CHANGED; /* Flag new input while in a mode. */ diff --git a/options-table.c b/options-table.c index 3e8dd5b46..a36901dcb 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.234 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.235 2026/07/13 15:03:03 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1936,6 +1936,7 @@ 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-activity", ""), OPTIONS_TABLE_PANE_HOOK("pane-bell", ""), OPTIONS_TABLE_PANE_HOOK("pane-command-finished", ""), OPTIONS_TABLE_PANE_HOOK("pane-command-started", ""), diff --git a/server-client.c b/server-client.c index 971b8d1aa..f28c80f85 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.487 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.488 2026/07/13 15:03:03 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -1812,7 +1812,8 @@ server_client_loop(void) server_client_check_pane_resize(wp); server_client_check_pane_buffer(wp); } - wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR); + wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR| + PANE_ACTIVITY); } check_window_name(w); } diff --git a/tmux.1 b/tmux.1 index 3b4b228f6..cddeaac3b 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1134 2026/07/13 13:01:14 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1135 2026/07/13 15:03:03 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -6378,17 +6378,12 @@ 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\-activity +Run when there is new output in a pane. .It pane\-bell Run when a pane receives a bell. .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 diff --git a/tmux.h b/tmux.h index db6d4cf4e..d954d6388 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1399 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1400 2026/07/13 15:03:03 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1268,6 +1268,7 @@ struct window_pane { #define PANE_REDRAWSCROLLBAR 0x8000 #define PANE_DESTROYED 0x10000 #define PANE_CMDRUNNING 0x20000 +#define PANE_ACTIVITY 0x40000 bitstr_t *sync_dirty; u_int sync_dirty_size; From 658e087f7aa426b122fa256b0199428ac499c80e Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 16:07:47 +0000 Subject: [PATCH 13/36] Add new-pane -M to create a new pane from a mouse binding and bind to C-MouseDrag1Pane, removing the old ctrl-click-to-swap-pane binding. --- cmd-split-window.c | 90 ++++++++++++++++++++++++++++++++++++++++++++-- key-bindings.c | 4 +-- server-client.c | 6 +++- tmux.1 | 9 +++-- tmux.h | 4 ++- 5 files changed, 104 insertions(+), 9 deletions(-) diff --git a/cmd-split-window.c b/cmd-split-window.c index 22c346c8b..a36f05603 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.142 2026/07/13 10:03:27 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.143 2026/07/13 16:07:47 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -34,13 +34,15 @@ #define SPLIT_WINDOW_TEMPLATE "#{session_name}:#{window_index}.#{pane_index}" static enum cmd_retval cmd_split_window_exec(struct cmd *, struct cmdq_item *); +static void cmd_split_window_mouse_resize(struct client *, + struct mouse_event *); const struct cmd_entry cmd_new_pane_entry = { .name = "new-pane", .alias = "newp", - .args = { "bB:c:de:EfF:hIkl:Lm:p:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, - .usage = "[-bdefhIklPvWZ] [-B border-lines] " + .args = { "bB:c:de:EfF:hIkl:LMm:p:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, + .usage = "[-bdefhIklMPvWZ] [-B border-lines] " "[-c start-directory] [-e environment] " "[-F format] [-l size] [-m message] [-p percentage] " "[-s style] [-S active-border-style] " @@ -86,6 +88,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) struct layout_cell *lc = NULL; struct event_payload *ep; struct cmd_find_state fs; + struct key_event *event = cmdq_get_event(item); int input, empty, is_floating, flags = 0; const char *template, *style, *value; char *cause = NULL, *cp, *title; @@ -101,6 +104,11 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) flags |= SPAWN_SPLIT; } + if (args_has(args, 'M') && is_floating) { + if (event == NULL || !event->m.valid || tc == NULL) + return (CMD_RETURN_NORMAL); + } + if (is_floating) flags |= SPAWN_FLOATING; if (args_has(args, 'h')) @@ -254,6 +262,12 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) } server_redraw_session(s); + if (args_has(args, 'M') && is_floating) { + tc->tty.mouse_last_pane = new_wp->id; + tc->tty.mouse_drag_update = cmd_split_window_mouse_resize; + cmd_split_window_mouse_resize(tc, &event->m); + } + if (args_has(args, 'P')) { if ((template = args_get(args, 'F')) == NULL) template = SPLIT_WINDOW_TEMPLATE; @@ -301,3 +315,73 @@ fail: return (CMD_RETURN_ERROR); } + +static void +cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m) +{ + struct window_pane *wp; + struct window *w; + struct layout_cell *lc; + enum pane_lines lines; + u_int sx, sy; + int x, y, xoff, yoff, border; + + if (c->tty.mouse_last_pane == -1) + return; + wp = window_pane_find_by_id(c->tty.mouse_last_pane); + if (wp == NULL || !window_pane_is_floating(wp)) { + c->tty.mouse_drag_update = NULL; + return; + } + w = wp->window; + lc = wp->layout_cell; + + x = m->x + m->ox; + y = m->y + m->oy; + if (m->statusat == 0 && y >= (int)m->statuslines) + y -= m->statuslines; + else if (m->statusat > 0 && y >= m->statusat) + y = m->statusat - 1; + + lines = window_pane_get_pane_lines(wp); + border = (lines != PANE_LINES_NONE); + + if (x >= (int)c->tty.mouse_drag_x) { + xoff = c->tty.mouse_drag_x + border; + sx = x - c->tty.mouse_drag_x + 1; + } else { + sx = c->tty.mouse_drag_x - x + 1; + xoff = c->tty.mouse_drag_x - sx + 1; + if (border) + xoff++; + } + if (y >= (int)c->tty.mouse_drag_y) { + yoff = c->tty.mouse_drag_y + border; + sy = y - c->tty.mouse_drag_y + 1; + } else { + sy = c->tty.mouse_drag_y - y + 1; + yoff = c->tty.mouse_drag_y - sy + 1; + if (border) + yoff++; + } + + if (border) { + if (sx <= 2) + sx = PANE_MINIMUM; + else + sx -= 2; + if (sy <= 2) + sy = PANE_MINIMUM; + else + sy -= 2; + } + if (sx < PANE_MINIMUM) + sx = PANE_MINIMUM; + if (sy < PANE_MINIMUM) + sy = PANE_MINIMUM; + + layout_set_size(lc, sx, sy, xoff, yoff); + layout_fix_panes(w, NULL); + server_redraw_window(w); + server_redraw_window_borders(w); +} diff --git a/key-bindings.c b/key-bindings.c index 92d417c5b..359a7b5cb 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.186 2026/07/07 12:30:36 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.187 2026/07/13 16:07:47 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -496,10 +496,10 @@ key_bindings_init(void) /* Mouse button 1 down on pane. */ "bind -n MouseDown1Pane { select-pane -t=; send -M }", - "bind -n C-MouseDown1Pane { swap-pane -s@ }", /* Mouse button 1 drag on pane. */ "bind -n MouseDrag1Pane { if -F '#{||:#{pane_in_mode},#{mouse_any_flag}}' { send -M } { copy-mode -M } }", + "bind -n C-MouseDrag1Pane { new-pane -M }", "bind -n M-MouseDrag1Pane { move-pane -M }", /* Mouse wheel up on pane. */ diff --git a/server-client.c b/server-client.c index f28c80f85..1e85f0688 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.488 2026/07/13 15:03:03 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.489 2026/07/13 16:07:47 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -1173,6 +1173,10 @@ have_event: * the scrollbar, store the relative position in the slider * where the user grabbed. */ + if (c->tty.mouse_drag_flag == 0) { + c->tty.mouse_drag_x = px; + c->tty.mouse_drag_y = py; + } c->tty.mouse_drag_flag = MOUSE_BUTTONS(b) + 1; /* Only change pane if not already dragging a pane border. */ diff --git a/tmux.1 b/tmux.1 index cddeaac3b..029f1ee26 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1135 2026/07/13 15:03:03 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1136 2026/07/13 16:07:47 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -3614,7 +3614,7 @@ but a different format may be specified with .Fl F . .Tg newp .It Xo Ic new\-pane -.Op Fl bdefhIkLPvWZ +.Op Fl bdefhIkLMPvWZ .Op Fl B Ar border\-lines .Op Fl c Ar start\-directory .Op Fl e Ar environment @@ -3660,6 +3660,11 @@ to specify a percentage of the window size. .Fl B sets the pane border lines for floating panes; see .Ic pane\-border\-lines . +.Fl M +may be used when bound to a mouse drag key; the new floating pane is resized to +follow the mouse drag. +It has no effect with +.Fl L . .Pp The .Fl L diff --git a/tmux.h b/tmux.h index d954d6388..8e548be3c 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1400 2026/07/13 15:03:03 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1401 2026/07/13 16:07:47 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1743,6 +1743,8 @@ struct tty { u_int mouse_last_y; u_int mouse_last_b; int mouse_drag_flag; + u_int mouse_drag_x; + u_int mouse_drag_y; int mouse_scrolling_flag; int mouse_slider_mpos; int mouse_last_pane; From 2dc05ae3286dd1bf018696c33d2e8df0d7f0264b Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 19:52:27 +0100 Subject: [PATCH 14/36] Add to alerts tests. --- regress/alerts.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/regress/alerts.sh b/regress/alerts.sh index cfb84c711..2b9e4f495 100644 --- a/regress/alerts.sh +++ b/regress/alerts.sh @@ -254,6 +254,19 @@ assert_unchanged @log '' $TMUX set -wut mon:w0 monitor-activity || fail "unset monitor-activity w0 failed" +# Creating a non-current window counts as activity when monitor-activity is +# enabled in the window defaults. +$TMUX set -wg monitor-activity on || fail "set global monitor-activity failed" +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX neww -d -t mon: -n newact cat || fail "new-window newact failed" +wait_for @log '|alert-activity:mon:newact' +wait_for_fmt mon:newact '#{window_activity_flag}' 1 +flags_have mon:newact '#' +$TMUX set -wug monitor-activity || + fail "unset global monitor-activity failed" +$TMUX selectw -t mon:newact || fail "select-window newact failed" +$TMUX selectw -t mon:w0 || fail "select-window 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 @@ -273,6 +286,22 @@ wait_for_fmt mon:silw '#{window_silence_flag}' 0 flags_lack mon:silw '~' $TMUX selectw -t mon:w0 || fail "select-window w0 failed" +# Activity resets the silence timer: output before the original deadline +# prevents an alert until a full quiet interval has passed afterwards. +$TMUX neww -d -t mon: -n silreset cat || fail "new-window silreset failed" +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX set -wt mon:silreset monitor-silence 2 || + fail "set monitor-silence silreset failed" +sleep 1 +activity mon:silreset +assert_unchanged @log '' +wait_for @log '|alert-silence:mon:silreset' +wait_for_fmt mon:silreset '#{window_silence_flag}' 1 +$TMUX set -wt mon:silreset monitor-silence 0 || + fail "reset monitor-silence silreset failed" +$TMUX selectw -t mon:silreset || fail "select-window silreset failed" +$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. From 8fbbde703595c55506bd7ce8f8eb6e667414c53b Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 16:30:28 +0000 Subject: [PATCH 15/36] Add keys and menu items for copy mode line numbers, and a menu item for auto refresh. --- key-bindings.c | 6 +++- tmux.1 | 17 ++++++++++- window-copy.c | 82 +++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 96 insertions(+), 9 deletions(-) diff --git a/key-bindings.c b/key-bindings.c index 359a7b5cb..f90307fc1 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.187 2026/07/13 16:07:47 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.188 2026/07/13 16:30:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -49,6 +49,9 @@ " '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Top,}' '<' {send -X history-top}" \ " '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}' '>' {send -X history-bottom}" \ " ''" \ + " '#{?#{==:#{pane_mode},copy-mode},#{?copy_line_numbers,Hide Line Numbers,Show Line Numbers},}' 'L' {send -X line-numbers-toggle}" \ + " '#{?#{==:#{pane_mode},copy-mode},#{?refresh_active,Refresh Off,Refresh On},}' 'r' {send -X refresh-toggle}" \ + " ''" \ " '#{?#{&&:#{buffer_size},#{!:#{pane_in_mode}}},Paste #[underscore]#{=/9/...:buffer_sample},}' 'p' {paste-buffer}" \ " ''" \ " '#{?mouse_word,Search For #[underscore]#{=/9/...:mouse_word},}' 'C-r' {if -F '#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}' 'copy-mode -t='; send -Xt= search-backward -- \"#{q:mouse_word}\"}" \ @@ -576,6 +579,7 @@ key_bindings_init(void) "bind -Tcopy-mode , { send -X jump-reverse }", "bind -Tcopy-mode \\; { send -X jump-again }", "bind -Tcopy-mode F { command-prompt -P -1p'(jump backward)' { send -X jump-backward -- '%%' } }", + "bind -Tcopy-mode L { send -X line-numbers-toggle }", "bind -Tcopy-mode N { send -X search-reverse }", "bind -Tcopy-mode P { send -X toggle-position }", "bind -Tcopy-mode R { send -X rectangle-toggle }", diff --git a/tmux.1 b/tmux.1 index 029f1ee26..52e83ed98 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1136 2026/07/13 16:07:47 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1137 2026/07/13 16:30:28 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -2158,6 +2158,19 @@ before the target. .Xc Jump to the last mark. .It Xo +.Ic line\-numbers\-on +.Xc +Show line numbers in copy mode. +.It Xo +.Ic line\-numbers\-off +.Xc +Hide line numbers in copy mode. +.It Xo +.Ic line\-numbers\-toggle +(emacs: L) +.Xc +Toggle line numbers in copy mode. +.It Xo .Ic middle\-line (vi: M) (emacs: M\-r) @@ -7175,6 +7188,7 @@ The following variables are available, where appropriate: .It Li "copy_cursor_word" Ta "" Ta "Word under cursor in copy mode" .It Li "copy_cursor_x" Ta "" Ta "Cursor X position in copy mode" .It Li "copy_cursor_y" Ta "" Ta "Cursor Y position in copy mode" +.It Li "copy_line_numbers" Ta "" Ta "1 if line numbers are shown in copy mode" .It Li "copy_position" Ta "" Ta "Position shown in the copy mode indicator" .It Li "copy_position_limit" Ta "" Ta "Limit shown in the copy mode indicator" .It Li "current_file" Ta "" Ta "Current configuration file" @@ -7294,6 +7308,7 @@ The following variables are available, where appropriate: .It Li "prev_window_active" Ta "" Ta "1 if previous window in W: loop is active" .It Li "prev_window_index" Ta "" Ta "Index of previous window in W: loop" .It Li "rectangle_toggle" Ta "" Ta "1 if rectangle selection is activated" +.It Li "refresh_active" Ta "" Ta "1 if copy mode refresh is active" .It Li "scroll_position" Ta "" Ta "Scroll position in copy mode" .It Li "scroll_region_lower" Ta "" Ta "Bottom of scroll region in pane" .It Li "scroll_region_upper" Ta "" Ta "Top of scroll region in pane" diff --git a/window-copy.c b/window-copy.c index 6d0390fe9..2213dcebb 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.415 2026/07/13 12:53:56 jsg Exp $ */ +/* $OpenBSD: window-copy.c,v 1.416 2026/07/13 16:30:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -57,6 +57,8 @@ static void window_copy_refresh_arm(struct window_mode_entry *); static void window_copy_refresh_start(struct window_mode_entry *); static void window_copy_refresh_stop(struct window_mode_entry *); static void window_copy_style_changed(struct window_mode_entry *); +static void window_copy_set_line_numbers1(struct window_mode_entry *, int, + int); static int window_copy_line_number_mode(struct window_mode_entry *); static int window_copy_line_number_is_absolute(struct window_mode_entry *); static int window_copy_line_numbers_active(struct window_mode_entry *); @@ -296,7 +298,7 @@ struct window_copy_mode_data { int rectflag; /* in rectangle copy mode? */ int scroll_exit; /* exit on scroll to end? */ int hide_position; /* hide position marker */ - int line_numbers; + int line_numbers; /* 0 off, 1 from option, 2 default */ enum { SEL_CHAR, /* select one char at a time */ @@ -1117,6 +1119,9 @@ window_copy_formats(struct window_mode_entry *wme, struct format_tree *ft) } format_add(ft, "copy_position", "%u", position); format_add(ft, "copy_position_limit", "%u", limit); + format_add(ft, "copy_line_numbers", "%d", + window_copy_line_numbers_active(wme)); + format_add(ft, "refresh_active", "%d", data->refresh_active); format_add(ft, "rectangle_toggle", "%d", data->rectflag); format_add(ft, "copy_cursor_x", "%d", data->cx); @@ -3150,6 +3155,28 @@ window_copy_cmd_recentre_top_bottom(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_REDRAW); } +static enum window_copy_cmd_action +window_copy_cmd_line_numbers_on(struct window_copy_cmd_state *cs) +{ + window_copy_set_line_numbers1(cs->wme, 1, 1); + return (WINDOW_COPY_CMD_NOTHING); +} + +static enum window_copy_cmd_action +window_copy_cmd_line_numbers_off(struct window_copy_cmd_state *cs) +{ + window_copy_set_line_numbers1(cs->wme, 0, 0); + return (WINDOW_COPY_CMD_NOTHING); +} + +static enum window_copy_cmd_action +window_copy_cmd_line_numbers_toggle(struct window_copy_cmd_state *cs) +{ + window_copy_set_line_numbers1(cs->wme, + !window_copy_line_numbers_active(cs->wme), 1); + return (WINDOW_COPY_CMD_NOTHING); +} + static const struct { const char *command; u_int minargs; @@ -3414,6 +3441,24 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, .f = window_copy_cmd_jump_to_mark }, + { .command = "line-numbers-on", + .args = { "", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_line_numbers_on + }, + { .command = "line-numbers-off", + .args = { "", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_line_numbers_off + }, + { .command = "line-numbers-toggle", + .args = { "", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_line_numbers_toggle + }, { .command = "next-prompt", .args = { "o", 0, 0, NULL }, .flags = WINDOW_COPY_CMD_FLAG_READONLY, @@ -5066,10 +5111,14 @@ window_copy_line_number_mode(struct window_mode_entry *wme) struct window_pane *wp = wme->wp; struct window_copy_mode_data *data = wme->data; struct options *oo = wp->window->options; + int mode; if (!data->line_numbers) return (WINDOW_COPY_LINE_NUMBERS_OFF); - return (options_get_number(oo, "copy-mode-line-numbers")); + mode = options_get_number(oo, "copy-mode-line-numbers"); + if (data->line_numbers == 2 && mode == WINDOW_COPY_LINE_NUMBERS_OFF) + return (WINDOW_COPY_LINE_NUMBERS_DEFAULT); + return (mode); } static int @@ -5155,14 +5204,33 @@ void window_copy_set_line_numbers(struct window_pane *wp, int enabled) { struct window_mode_entry *wme = TAILQ_FIRST(&wp->modes); - struct window_copy_mode_data *data; if (wme == NULL || wme->mode != &window_copy_mode) return; - data = wme->data; - if (data == NULL || data->line_numbers == enabled) + window_copy_set_line_numbers1(wme, enabled, 0); +} + +static void +window_copy_set_line_numbers1(struct window_mode_entry *wme, int enabled, + int force) +{ + struct window_copy_mode_data *data = wme->data; + struct options *oo = wme->wp->window->options; + int active, line_numbers; + + if (data == NULL) return; - data->line_numbers = enabled; + active = window_copy_line_numbers_active(wme); + if (!enabled) + line_numbers = 0; + else if (force && options_get_number(oo, "copy-mode-line-numbers") == + WINDOW_COPY_LINE_NUMBERS_OFF) + line_numbers = 2; + else + line_numbers = 1; + if (data->line_numbers == line_numbers && active == enabled) + return; + data->line_numbers = line_numbers; window_copy_redraw_screen(wme, 1); } From c3e38b317d47c667e9132b5f2785cf71cb4b92c4 Mon Sep 17 00:00:00 2001 From: Yayo Razo Date: Mon, 13 Jul 2026 13:03:58 -0600 Subject: [PATCH 16/36] fix(compat): drop fgetln compat shim, migrate prompt-history.c to getline Following up on the comment on #5365 - this drops fgetln from prompt-history.c (its only remaining caller) in favor of getline, and removes the fgetln compat shim entirely since nothing references it anymore: the compat.h declaration, compat/fgetln.c itself, and its entry in configure.ac's AC_REPLACE_FUNCS list. fgetln doesn't null-terminate its returned buffer when the final line in the file lacks a trailing newline, so the old loop had a second branch to heap-copy and manually terminate that case. getline always null-terminates regardless, so that branch goes away and the loop becomes uniform. Verified with a real build (ubuntu-24.04 container, same --enable-utf8proc --enable-asan configure CI uses): crafted a history file with a normal entry, a blank line, and a final entry with no trailing newline at EOF (the exact case the removed branch existed for), loaded it through a real ASan-built tmux via show-prompt-history, all three entries came back correctly. Full regress suite is otherwise clean against this change. --- compat.h | 5 ---- compat/fgetln.c | 61 ------------------------------------------------ configure.ac | 1 - prompt-history.c | 27 +++++++-------------- 4 files changed, 9 insertions(+), 85 deletions(-) delete mode 100644 compat/fgetln.c diff --git a/compat.h b/compat.h index 8d71a9cf1..66a22640b 100644 --- a/compat.h +++ b/compat.h @@ -410,11 +410,6 @@ int asprintf(char **, const char *, ...); int vasprintf(char **, const char *, va_list); #endif -#ifndef HAVE_FGETLN -/* fgetln.c */ -char *fgetln(FILE *, size_t *); -#endif - #ifndef HAVE_GETLINE /* getline.c */ ssize_t getline(char **, size_t *, FILE *); diff --git a/compat/fgetln.c b/compat/fgetln.c deleted file mode 100644 index c71918cbb..000000000 --- a/compat/fgetln.c +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2015 Joerg Jung - * - * 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. - */ - -/* - * portable fgetln() version, NOT reentrant - */ - -#include -#include -#include - -#include "compat.h" - -char * -fgetln(FILE *fp, size_t *len) -{ - static char *buf = NULL; - static size_t bufsz = 0; - size_t r = 0; - char *p; - int c, e; - - if (!fp || !len) { - errno = EINVAL; - return NULL; - } - if (!buf) { - if (!(buf = calloc(1, BUFSIZ))) - return NULL; - bufsz = BUFSIZ; - } - while ((c = getc(fp)) != EOF) { - buf[r++] = c; - if (r == bufsz) { - if (!(p = reallocarray(buf, 2, bufsz))) { - e = errno; - free(buf); - errno = e; - buf = NULL, bufsz = 0; - return NULL; - } - buf = p, bufsz = 2 * bufsz; - } - if (c == '\n') - break; - } - return (*len = r) ? buf : NULL; -} diff --git a/configure.ac b/configure.ac index 6abb2d9d5..f65563c3e 100644 --- a/configure.ac +++ b/configure.ac @@ -177,7 +177,6 @@ AC_REPLACE_FUNCS([ \ clock_gettime \ closefrom \ explicit_bzero \ - fgetln \ freezero \ getdtablecount \ getdtablesize \ diff --git a/prompt-history.c b/prompt-history.c index ac99bdb95..4453a53a8 100644 --- a/prompt-history.c +++ b/prompt-history.c @@ -79,8 +79,9 @@ void prompt_load_history(void) { FILE *f; - char *history_file, *line, *tmp; - size_t length; + char *history_file, *line = NULL; + size_t length = 0; + ssize_t got; if ((history_file = prompt_find_history_file()) == NULL) return; @@ -94,23 +95,13 @@ prompt_load_history(void) } free(history_file); - for (;;) { - if ((line = fgetln(f, &length)) == NULL) - break; - - if (length > 0) { - if (line[length - 1] == '\n') { - line[length - 1] = '\0'; - prompt_add_typed_history(line); - } else { - tmp = xmalloc(length + 1); - memcpy(tmp, line, length); - tmp[length] = '\0'; - prompt_add_typed_history(tmp); - free(tmp); - } - } + while ((got = getline(&line, &length, f)) != -1) { + if (got > 0 && line[got - 1] == '\n') + line[got - 1] = '\0'; + if (got > 0) + prompt_add_typed_history(line); } + free(line); fclose(f); } From e4dbd11d44f13122719d598c5b091758dda42bc3 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 20:27:02 +0100 Subject: [PATCH 17/36] doc: rewrite SYNCING and reformat to markdown Rewrite the SYNCING document to reflect that everything runs on Github. Add examples on how to deal with merge conflicts. Also reformat to markdown so any examples are more obvious in different renderers. --- README.ja | 2 +- SYNCING | 173 ------------------------------------------------------ 2 files changed, 1 insertion(+), 174 deletions(-) delete mode 100644 SYNCING diff --git a/README.ja b/README.ja index aeb6704ae..3d3d0b43c 100644 --- a/README.ja +++ b/README.ja @@ -56,7 +56,7 @@ gitコミットについての連絡先 tmux-users@googlegroups.com -本ファイル、CHANGES、 FAQ、SYNCINGそしてTODOはISC licenseで保護されています。 +本ファイル、CHANGES、 FAQ、SYNCING.mdそしてTODOはISC licenseで保護されています。 その他のファイルのライセンスや著作権については、ファイルの上部に明記されています。 -- Nicholas Marriott diff --git a/SYNCING b/SYNCING deleted file mode 100644 index 51b780100..000000000 --- a/SYNCING +++ /dev/null @@ -1,173 +0,0 @@ -Preamble -======== - -Tmux portable relies on repositories "tmux" and "tmux-obsd". -Here's a description of them: - -* "tmux" is the portable version, the one which contains code for other - operating systems, and autotools, etc., which isn't found or needed in the - OpenBSD base system. - -* "tmux-obsd" is the version of tmux in OpenBSD base system which provides - the basis of the portable tmux version. - -Note: The "tmux-obsd" repository is actually handled by "git cvsimport" -running at 15 minute intervals, so a commit made to OpenBSD's tmux CVS -repository will take at least that long to appear in this git repository. -(It might take longer, depending on the CVS mirror used to import the -OpenBSD code). - -If you've never used git before, git tracks meta-data about the committer -and the author, as part of a commit, hence: - -% git config [--global] user.name "Your name" -% git config [--global] user.email "you@yourdomain.com" - -Note that, if you already have this in the global ~/.gitconfig option, then -this will be used. Setting this per-repository would involve not using the -"--global" flag above. If you wish to use the same credentials always, -pass the "--global" option, as shown. - -This is a one-off operation once the repository has been cloned, assuming -this information has ever been set before. - -Cloning repositories -==================== - -This involves having both tmux and tmux-obsd cloned, as in: - -% cd /some/where/useful -% git clone https://github.com/tmux/tmux.git -% git clone https://github.com/ThomasAdam/tmux-obsd.git - -Note that you do not need additional checkouts to manage the sync -- an -existing clone of either repositories will suffice. So if you already have -these checkouts existing, skip that. - -Adding in git-remotes -===================== - -Because the portable "tmux" git repository and the "tmux-obsd" -repository do not inherently share any history between each other, the -history has been faked between them. This "faking of history" is something -which has to be told to git for the purposes of comparing the "tmux" and -"tmux-obsd" repositories for syncing. To do this, we must reference the -clone of the "tmux-obsd" repository from the "tmux" repository, as -shown by the following command: - -% cd /path/to/tmux -% git remote add obsd-tmux file:///path/to/tmux-obsd - -So that now, the remote "obsd-tmux" can be used to reference branches and -commits from the "tmux-obsd" repository, but from the context of the -portable "tmux" repository, which makes sense because it's the "tmux" -repository which will have the updates applied to them. - -Fetching updates -================ - -To ensure the latest commits from "tmux-obsd" can be found from within -"tmux", we have to ensure the "master" branch from "tmux-obsd" is -up-to-date first, and then reference that update in "tmux", as in: - -% cd /path/to/tmux-obsd -% git checkout master -% git pull - -Then back in "tmux": - -% cd /path/to/tmux -% git fetch obsd-tmux - -Creating the necessary branches -=============================== - -Now that "tmux" can see commits and branches from "tmux-obsd" by way -of the remote name "obsd-tmux", we can now create the master branch from -"tmux-obsd" in the "tmux" repository: - -% git checkout -b obsd-master obsd-tmux/master - -Adding in the fake history points -================================= - -To tie both the "master" branch from "tmux" and the "obsd-master" -branch from "tmux-obsd" together, the fake history points added to the -"tmux" repository need to be added. To do this, we must add an -additional refspec line, as in: - -% cd /path/to/tmux -% git config --add remote.origin.fetch '+refs/replace/*:refs/replace/*' -% git fetch origin - -Performing the Sync -=================== - -Make sure the "master" branch is checked out: - -% git checkout master - -The following will show commits on OpenBSD not yet synched with "tmux": - -% git log master..obsd-master - -From there, merge the result in, fixing up any conflicts which might arise. - -% git merge obsd-master - -Then ensure things look correct by BUILDING the result of that sync: - -% make clean && ./autogen.sh && ./configure && make - -Compare the git merge result with what's on origin/master -- that is, check -which commits you're about to push: - -% git log origin/master..master - -And if happy: - -% git push origin master - -Keeping an eye on libutil in OpenBSD -==================================== - -A lot of the compat/ code in tmux comes from libutil, especially imsg. -Sometimes the API can change, etc., which might cause interesting problems -trying to run the portable version of tmux. It's worth checking -periodically for any changes to libutil in OpenBSD and syncing those files -to compat/ as and when appropriate. - -Release tmux for next version -============================= - -1. Update and commit README and CHANGES. The former should be checked for - anything outdated and updated with a list of things that might break - upgrades and the latter should mention all the major changes since - the last version. - -2. Make sure configure.ac has the new version number. - -3. Tag with: - - % git tag -a 2.X - - Where "2.X" is the next version. - - Push the tag out with: - - % git push --tags - -4. Build the tarball with 'make dist'. - -5. Check the tarball. If it's good, go here to select the tag just pushed: - - https://github.com/tmux/tmux/tags - - Click the "Add release notes", upload the tarball and add a link in the - description field to the CHANGES file. - -6. Clone the tmux.github.io repository, and change the RELEASE version in the - Makefile. Commit it, and run 'make' to replace %%RELEASE%%. Push the - result out. - -7. Change version back to master in configure.ac. From 9c9477ae76ebb8f7ab4f1058e434d5e21fa3481c Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 20:30:47 +0100 Subject: [PATCH 18/36] Tweak previous; include SYNCING.md --- SYNCING.md | 234 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 SYNCING.md diff --git a/SYNCING.md b/SYNCING.md new file mode 100644 index 000000000..9b710a463 --- /dev/null +++ b/SYNCING.md @@ -0,0 +1,234 @@ +# Preamble + +tmux portable is maintained from two repositories: + +* `tmux` is the portable repository. It contains the portability layer, + autotools build files, regression tests, documentation, and code needed for + platforms outside OpenBSD. +* `tmux-openbsd-cutover` is the OpenBSD tmux import repository. Its `master` + branch is the OpenBSD tmux history after the OpenBSD Git cutover. Portable + merges this branch when importing OpenBSD changes. + +To create the commits in the tmux-openbsd-cutover repo, the source repo is the +Github mirror of OpenBSD's src repo: + +```text +https://github.com/openbsd/src.git +``` + +The update automation filters the OpenBSD source tree to `usr.bin/tmux/`, +publishes that filtered history as `openbsd-git` in the cutover repository, +cherry-picks new filtered commits onto cutover `master`, then merges cutover +`master` into portable `master`. + +If you've never used git before, configure your identity before committing: + +```sh +git config [--global] user.name "Your name" +git config [--global] user.email "you@yourdomain.com" +``` + +# Repository layout + +The usual local layout is: + +```sh +cd /some/where/useful +git clone https://github.com/tmux/tmux.git tmux-portable +git clone https://github.com/ThomasAdam/tmux-obsd.git tmux-openbsd-cutover +``` + +The exact directory names do not matter, but the examples below use: + +```text +/path/to/tmux-portable +/path/to/tmux-openbsd-cutover +``` + +The cutover repository has three important branches: + +* `master` is the branch portable consumes. It should contain tmux source + only, not automation files. +* `openbsd-git` is the raw filtered OpenBSD tmux branch generated from + OpenBSD src `master`. +* `automation` is an orphan branch containing the GitHub Actions workflow. It + is separate so that workflow files are not merged into portable. + +# Adding the OpenBSD remote to portable + +In the portable repository, add the cutover repository as a remote: + +```sh +cd /path/to/tmux-portable +git remote add tmux-openbsd /path/to/tmux-openbsd-cutover +git config remote.tmux-openbsd.tagOpt --no-tags +``` + +If the remote already exists, update it instead: + +```sh +git remote set-url tmux-openbsd /path/to/tmux-openbsd-cutover +git config remote.tmux-openbsd.tagOpt --no-tags +``` + +Fetch the cutover master branch explicitly: + +```sh +git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master +``` + +# Automated syncing + +The normal sync is performed by the GitHub Actions workflow in the +`automation` branch of the cutover repository. That workflow: + +1. Fetches or clones OpenBSD src using a blobless clone. +2. Filters `usr.bin/tmux/` into a local `tmux-openbsd` branch. +3. Updates `tmux-openbsd-cutover/openbsd-git`. +4. Cherry-picks new `openbsd-git` commits onto cutover `master`. +5. Merges cutover `master` into portable `master`. +6. Pushes the changed repositories. + +The workflow deliberately runs from the orphan `automation` branch but checks +out cutover `master` as the branch to update. This keeps `.github/` out of +cutover `master`, so portable does not import workflow files when it merges +OpenBSD changes. + +# Manual portable merge + +If the workflow fails while merging into portable, do the merge locally and +push the result. + +Start from an up-to-date portable master: + +```sh +cd /path/to/tmux-portable +git fetch origin +git checkout master +git pull --ff-only origin master +``` + +Fetch the cutover branch: + +```sh +git remote add tmux-openbsd /path/to/tmux-openbsd-cutover 2>/dev/null || true +git config remote.tmux-openbsd.tagOpt --no-tags +git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master +``` + +Merge it: + +```sh +git merge --no-ff --log refs/remotes/tmux-openbsd/master +``` + +Resolve conflicts by deciding whether portable or OpenBSD owns the file. + +Useful commands: + +```sh +git checkout --ours path/to/file +git add path/to/file +``` + +This keeps the portable version of a conflicted file. + +```sh +git checkout --theirs path/to/file +git add path/to/file +``` + +This takes the OpenBSD/cutover version of a conflicted file. + +Before committing, inspect the result: + +```sh +git status +git diff --check +git diff --cached --stat +``` + +Then finish and publish: + +```sh +git commit +git push origin master +``` + +If the merge attempt is wrong, abort it: + +```sh +git merge --abort +``` + +After pushing a manually resolved merge, rerun the workflow. It should either +no-op or continue from the now-merged portable state. + +# Manual cutover update + +Normally this is handled by the workflow. If it must be done manually, update +`openbsd-git` in the cutover repository from a filtered OpenBSD src checkout, +then cherry-pick the new filtered commits onto cutover `master`. + +The important range is: + +```text +origin/openbsd-git..openbsd-git +``` + +where `origin/openbsd-git` is the previously published filtered OpenBSD tmux +branch and `openbsd-git` is the newly generated filtered branch. + +In the cutover repository: + +```sh +cd /path/to/tmux-openbsd-cutover +git checkout master +git cherry-pick origin/openbsd-git..openbsd-git +git push origin master openbsd-git +``` + +Do not put workflow files on cutover `master`; keep them on the orphan +`automation` branch. + +# Keeping an eye on libutil in OpenBSD + +A lot of the `compat/` code in tmux comes from OpenBSD libraries, especially +imsg. Sometimes APIs change in OpenBSD in ways that require corresponding +portable changes. It is worth checking periodically for relevant OpenBSD +libutil changes and syncing those files into `compat/` as appropriate. + +# Release tmux for next version + +1. Update and commit README and CHANGES. The former should be checked for + anything outdated and updated with a list of things that might break + upgrades and the latter should mention all the major changes since the last + version. +2. Make sure `configure.ac` has the new version number. +3. Tag with: + + ```sh + % git tag -a 2.X + ``` + + Where `2.X` is the next version. + + Push the tag out with: + + ```sh + % git push --tags + ``` + +4. Build the tarball with `make dist`. +5. Check the tarball. If it is good, go here to select the tag just pushed: + + ```text + https://github.com/tmux/tmux/tags + ``` + + Click "Add release notes", upload the tarball and add a link in the + description field to the CHANGES file. +6. Clone the tmux.github.io repository, and change the RELEASE version in the + Makefile. Commit it, and run `make` to replace `%%RELEASE%%`. Push the + result out. +7. Change version back to master in `configure.ac`. From fba5083b5896a8ec209e188017914a5850770113 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 19:32:28 +0000 Subject: [PATCH 19/36] Add hooks to customize mode. --- options-table.c | 255 ++++++++++++++++++++++++++++----------------- window-customize.c | 123 ++++++++++++++++++---- 2 files changed, 263 insertions(+), 115 deletions(-) diff --git a/options-table.c b/options-table.c index a36901dcb..ddbb9404d 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.235 2026/07/13 15:03:03 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.236 2026/07/13 19:32:28 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -240,33 +240,40 @@ static const char *options_table_status_format_default[] = { }; /* Helpers for hook options. */ -#define OPTIONS_TABLE_HOOK(hook_name, default_value) \ +#define OPTIONS_TABLE_HOOK(hook_name, default_value, hook_text) \ { .name = hook_name, \ .type = OPTIONS_TABLE_COMMAND, \ .scope = OPTIONS_TABLE_SESSION, \ .flags = OPTIONS_TABLE_IS_ARRAY|OPTIONS_TABLE_IS_HOOK, \ .default_str = default_value, \ - .separator = "" \ + .separator = "", \ + .text = hook_text \ } -#define OPTIONS_TABLE_PANE_HOOK(hook_name, default_value) \ +#define OPTIONS_TABLE_PANE_HOOK(hook_name, default_value, hook_text) \ { .name = hook_name, \ .type = OPTIONS_TABLE_COMMAND, \ .scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE, \ .flags = OPTIONS_TABLE_IS_ARRAY|OPTIONS_TABLE_IS_HOOK, \ .default_str = default_value, \ - .separator = "" \ + .separator = "", \ + .text = hook_text \ } -#define OPTIONS_TABLE_WINDOW_HOOK(hook_name, default_value) \ +#define OPTIONS_TABLE_WINDOW_HOOK(hook_name, default_value, hook_text) \ { .name = hook_name, \ .type = OPTIONS_TABLE_COMMAND, \ .scope = OPTIONS_TABLE_WINDOW, \ .flags = OPTIONS_TABLE_IS_ARRAY|OPTIONS_TABLE_IS_HOOK, \ .default_str = default_value, \ - .separator = "" \ + .separator = "", \ + .text = hook_text \ } +#define OPTIONS_TABLE_AFTER_HOOK(command_name) \ + OPTIONS_TABLE_HOOK("after-" command_name, "", \ + "Run after the " command_name " command completes.") + /* Map of name conversions. */ const struct options_name_map options_other_names[] = { { "display-panes-color", "display-panes-colour" }, @@ -1882,95 +1889,151 @@ const struct options_table_entry options_table[] = { }, /* Hook options. */ - OPTIONS_TABLE_HOOK("after-bind-key", ""), - OPTIONS_TABLE_HOOK("after-capture-pane", ""), - OPTIONS_TABLE_HOOK("after-copy-mode", ""), - OPTIONS_TABLE_HOOK("after-display-message", ""), - OPTIONS_TABLE_HOOK("after-display-panes", ""), - OPTIONS_TABLE_HOOK("after-kill-pane", ""), - OPTIONS_TABLE_HOOK("after-list-buffers", ""), - OPTIONS_TABLE_HOOK("after-list-clients", ""), - OPTIONS_TABLE_HOOK("after-list-keys", ""), - OPTIONS_TABLE_HOOK("after-list-panes", ""), - OPTIONS_TABLE_HOOK("after-list-sessions", ""), - OPTIONS_TABLE_HOOK("after-list-windows", ""), - OPTIONS_TABLE_HOOK("after-load-buffer", ""), - OPTIONS_TABLE_HOOK("after-lock-server", ""), - OPTIONS_TABLE_HOOK("after-new-session", ""), - OPTIONS_TABLE_HOOK("after-new-window", ""), - OPTIONS_TABLE_HOOK("after-paste-buffer", ""), - OPTIONS_TABLE_HOOK("after-pipe-pane", ""), - OPTIONS_TABLE_HOOK("after-queue", ""), - OPTIONS_TABLE_HOOK("after-refresh-client", ""), - OPTIONS_TABLE_HOOK("after-rename-session", ""), - OPTIONS_TABLE_HOOK("after-rename-window", ""), - OPTIONS_TABLE_HOOK("after-resize-pane", ""), - OPTIONS_TABLE_HOOK("after-resize-window", ""), - OPTIONS_TABLE_HOOK("after-save-buffer", ""), - OPTIONS_TABLE_HOOK("after-select-layout", ""), - OPTIONS_TABLE_HOOK("after-select-pane", ""), - OPTIONS_TABLE_HOOK("after-select-window", ""), - OPTIONS_TABLE_HOOK("after-send-keys", ""), - OPTIONS_TABLE_HOOK("after-set-buffer", ""), - OPTIONS_TABLE_HOOK("after-set-environment", ""), - OPTIONS_TABLE_HOOK("after-set-hook", ""), - OPTIONS_TABLE_HOOK("after-set-option", ""), - OPTIONS_TABLE_HOOK("after-show-environment", ""), - OPTIONS_TABLE_HOOK("after-show-messages", ""), - OPTIONS_TABLE_HOOK("after-show-options", ""), - OPTIONS_TABLE_HOOK("after-split-window", ""), - OPTIONS_TABLE_HOOK("after-unbind-key", ""), - OPTIONS_TABLE_HOOK("alert-activity", ""), - OPTIONS_TABLE_HOOK("alert-bell", ""), - OPTIONS_TABLE_HOOK("alert-silence", ""), - OPTIONS_TABLE_HOOK("client-active", ""), - OPTIONS_TABLE_HOOK("client-attached", ""), - OPTIONS_TABLE_HOOK("client-created", ""), - OPTIONS_TABLE_HOOK("client-closed", ""), - OPTIONS_TABLE_HOOK("client-detached", ""), - OPTIONS_TABLE_HOOK("client-focus-in", ""), - OPTIONS_TABLE_HOOK("client-focus-out", ""), - OPTIONS_TABLE_HOOK("client-resized", ""), - OPTIONS_TABLE_HOOK("client-session-changed", ""), - 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-activity", ""), - OPTIONS_TABLE_PANE_HOOK("pane-bell", ""), - 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-moved", ""), - 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", ""), - OPTIONS_TABLE_HOOK("session-added-to-group", ""), - OPTIONS_TABLE_HOOK("session-renamed", ""), - OPTIONS_TABLE_HOOK("session-removed-from-group", ""), - OPTIONS_TABLE_HOOK("session-window-changed", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-created", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-closed", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-layout-changed", ""), - OPTIONS_TABLE_HOOK("window-linked", ""), - 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", ""), + OPTIONS_TABLE_AFTER_HOOK("bind-key"), + OPTIONS_TABLE_AFTER_HOOK("capture-pane"), + OPTIONS_TABLE_AFTER_HOOK("copy-mode"), + OPTIONS_TABLE_AFTER_HOOK("display-message"), + OPTIONS_TABLE_AFTER_HOOK("display-panes"), + OPTIONS_TABLE_AFTER_HOOK("kill-pane"), + OPTIONS_TABLE_AFTER_HOOK("list-buffers"), + OPTIONS_TABLE_AFTER_HOOK("list-clients"), + OPTIONS_TABLE_AFTER_HOOK("list-keys"), + OPTIONS_TABLE_AFTER_HOOK("list-panes"), + OPTIONS_TABLE_AFTER_HOOK("list-sessions"), + OPTIONS_TABLE_AFTER_HOOK("list-windows"), + OPTIONS_TABLE_AFTER_HOOK("load-buffer"), + OPTIONS_TABLE_AFTER_HOOK("lock-server"), + OPTIONS_TABLE_AFTER_HOOK("new-session"), + OPTIONS_TABLE_AFTER_HOOK("new-window"), + OPTIONS_TABLE_AFTER_HOOK("paste-buffer"), + OPTIONS_TABLE_AFTER_HOOK("pipe-pane"), + OPTIONS_TABLE_AFTER_HOOK("refresh-client"), + OPTIONS_TABLE_AFTER_HOOK("rename-session"), + OPTIONS_TABLE_AFTER_HOOK("rename-window"), + OPTIONS_TABLE_AFTER_HOOK("resize-pane"), + OPTIONS_TABLE_AFTER_HOOK("resize-window"), + OPTIONS_TABLE_AFTER_HOOK("save-buffer"), + OPTIONS_TABLE_AFTER_HOOK("select-layout"), + OPTIONS_TABLE_AFTER_HOOK("select-pane"), + OPTIONS_TABLE_AFTER_HOOK("select-window"), + OPTIONS_TABLE_AFTER_HOOK("send-keys"), + OPTIONS_TABLE_AFTER_HOOK("set-buffer"), + OPTIONS_TABLE_AFTER_HOOK("set-environment"), + OPTIONS_TABLE_AFTER_HOOK("set-hook"), + OPTIONS_TABLE_AFTER_HOOK("set-option"), + OPTIONS_TABLE_AFTER_HOOK("show-environment"), + OPTIONS_TABLE_AFTER_HOOK("show-messages"), + OPTIONS_TABLE_AFTER_HOOK("show-options"), + OPTIONS_TABLE_AFTER_HOOK("split-window"), + OPTIONS_TABLE_AFTER_HOOK("unbind-key"), + OPTIONS_TABLE_HOOK("alert-activity", "", + "Run when a window has activity."), + OPTIONS_TABLE_HOOK("alert-bell", "", + "Run when a window has received a bell."), + OPTIONS_TABLE_HOOK("alert-silence", "", + "Run when a window has been silent."), + OPTIONS_TABLE_HOOK("client-active", "", + "Run when a client becomes the latest active client of its " + "session."), + OPTIONS_TABLE_HOOK("client-attached", "", + "Run when a client is attached."), + OPTIONS_TABLE_HOOK("client-created", "", + "Run when a client is created."), + OPTIONS_TABLE_HOOK("client-closed", "", + "Run when a client is closed."), + OPTIONS_TABLE_HOOK("client-detached", "", + "Run when a client is detached."), + OPTIONS_TABLE_HOOK("client-focus-in", "", + "Run when focus enters a client."), + OPTIONS_TABLE_HOOK("client-focus-out", "", + "Run when focus exits a client."), + OPTIONS_TABLE_HOOK("client-resized", "", + "Run when a client is resized."), + OPTIONS_TABLE_HOOK("client-session-changed", "", + "Run when a client's attached session is changed."), + OPTIONS_TABLE_HOOK("client-light-theme", "", + "Run when a client switches to a light theme."), + OPTIONS_TABLE_HOOK("client-dark-theme", "", + "Run when a client switches to a dark theme."), + OPTIONS_TABLE_HOOK("command-error", "", + "Run when a command fails."), + OPTIONS_TABLE_HOOK("marked-pane-changed", "", + "Run when the marked pane is set or cleared."), + OPTIONS_TABLE_PANE_HOOK("pane-activity", "", + "Run when there is new output in a pane."), + OPTIONS_TABLE_PANE_HOOK("pane-bell", "", + "Run when a pane receives a bell."), + OPTIONS_TABLE_PANE_HOOK("pane-command-finished", "", + "Run when an OSC 133 command finishes in a pane."), + OPTIONS_TABLE_PANE_HOOK("pane-command-started", "", + "Run when an OSC 133 command starts in a pane."), + OPTIONS_TABLE_PANE_HOOK("pane-created", "", + "Run when a pane is created or respawned."), + OPTIONS_TABLE_PANE_HOOK("pane-died", "", + "Run when the program running in a pane exits, but remain-on-exit " + "is on so the pane has not closed."), + OPTIONS_TABLE_PANE_HOOK("pane-exited", "", + "Run when the program running in a pane exits."), + OPTIONS_TABLE_PANE_HOOK("pane-focus-in", "", + "Run when the focus enters a pane, if the focus-events option is " + "on."), + OPTIONS_TABLE_PANE_HOOK("pane-focus-out", "", + "Run when the focus exits a pane, if the focus-events option is " + "on."), + OPTIONS_TABLE_PANE_HOOK("pane-mode-changed", "", + "Run when a pane changes mode."), + OPTIONS_TABLE_PANE_HOOK("pane-mode-entered", "", + "Run when a pane enters a mode."), + OPTIONS_TABLE_PANE_HOOK("pane-mode-exited", "", + "Run when a pane exits a mode."), + OPTIONS_TABLE_PANE_HOOK("pane-moved", "", + "Run when a pane is moved to another window."), + OPTIONS_TABLE_PANE_HOOK("pane-prompt-closed", "", + "Run when a prompt in a pane is closed."), + OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", "", + "Run when a prompt is opened in a pane."), + OPTIONS_TABLE_PANE_HOOK("pane-resized", "", + "Run when a pane is resized."), + OPTIONS_TABLE_PANE_HOOK("pane-set-clipboard", "", + "Run when the terminal clipboard is set using the xterm escape " + "sequence."), + OPTIONS_TABLE_PANE_HOOK("pane-shell-prompt", "", + "Run when an OSC 133 shell prompt starts in a pane."), + OPTIONS_TABLE_PANE_HOOK("pane-title-changed", "", + "Run when a pane title is changed."), + OPTIONS_TABLE_HOOK("session-closed", "", + "Run when a session is closed."), + OPTIONS_TABLE_HOOK("session-created", "", + "Run when a new session is created."), + OPTIONS_TABLE_HOOK("session-added-to-group", "", + "Run when a session is added to a session group."), + OPTIONS_TABLE_HOOK("session-renamed", "", + "Run when a session is renamed."), + OPTIONS_TABLE_HOOK("session-removed-from-group", "", + "Run when a session is removed from a session group."), + OPTIONS_TABLE_HOOK("session-window-changed", "", + "Run when a session changes its active window."), + OPTIONS_TABLE_WINDOW_HOOK("window-created", "", + "Run when a window is created."), + OPTIONS_TABLE_WINDOW_HOOK("window-closed", "", + "Run when a window is closed."), + OPTIONS_TABLE_WINDOW_HOOK("window-layout-changed", "", + "Run when a window layout is changed."), + OPTIONS_TABLE_HOOK("window-linked", "", + "Run when a window is linked into a session."), + OPTIONS_TABLE_WINDOW_HOOK("window-pane-changed", "", + "Run when a window changes its active pane."), + OPTIONS_TABLE_WINDOW_HOOK("window-renamed", "", + "Run when a window is renamed."), + OPTIONS_TABLE_WINDOW_HOOK("window-resized", "", + "Run when a window is resized. This may be after the " + "client-resized hook is run."), + OPTIONS_TABLE_WINDOW_HOOK("window-unzoomed", "", + "Run when a window is unzoomed."), + OPTIONS_TABLE_WINDOW_HOOK("window-zoomed", "", + "Run when a window is zoomed."), + OPTIONS_TABLE_HOOK("window-unlinked", "", + "Run when a window is unlinked from a session."), { .name = NULL } }; diff --git a/window-customize.c b/window-customize.c index 9f84540ed..4a61eed72 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-customize.c,v 1.30 2026/07/06 14:40:57 nicm Exp $ */ +/* $OpenBSD: window-customize.c,v 1.31 2026/07/13 19:32:28 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -81,6 +81,11 @@ enum window_customize_change { WINDOW_CUSTOMIZE_RESET, }; +enum window_customize_option_type { + WINDOW_CUSTOMIZE_OPTIONS, + WINDOW_CUSTOMIZE_HOOKS +}; + struct window_customize_itemdata { struct window_customize_modedata *data; enum window_customize_scope scope; @@ -274,7 +279,8 @@ static void window_customize_build_option(struct window_customize_modedata *data, struct mode_tree_item *top, enum window_customize_scope scope, struct options_entry *o, struct format_tree *ft, - const char *filter, struct cmd_find_state *fs) + const char *filter, struct cmd_find_state *fs, + enum window_customize_option_type type) { const struct options_table_entry *oe = options_table_entry(o); struct options *oo = options_owner(o); @@ -282,10 +288,23 @@ window_customize_build_option(struct window_customize_modedata *data, struct window_customize_itemdata *item; char *text, *expanded, *value; int global = 0, array = 0; + int is_hook = 0, is_monitor = 0; uint64_t tag; if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)) - return; + is_hook = 1; + if (options_get_monitor_data(o) != NULL) + is_monitor = 1; + switch (type) { + case WINDOW_CUSTOMIZE_OPTIONS: + if (is_hook || is_monitor) + return; + break; + case WINDOW_CUSTOMIZE_HOOKS: + if (!is_hook && !is_monitor) + return; + break; + } if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) array = 1; @@ -299,6 +318,8 @@ window_customize_build_option(struct window_customize_modedata *data, format_add(ft, "option_name", "%s", name); format_add(ft, "option_is_global", "%d", global); format_add(ft, "option_is_array", "%d", array); + format_add(ft, "option_is_hook", "%d", is_hook); + format_add(ft, "option_is_monitor", "%d", is_monitor); text = window_customize_scope_text(scope, fs); format_add(ft, "option_scope", "%s", text); @@ -309,6 +330,16 @@ window_customize_build_option(struct window_customize_modedata *data, else format_add(ft, "option_unit", "%s", ""); + if (is_monitor) { + value = hooks_monitor_to_string(o); + if (value != NULL) { + format_add(ft, "option_monitor", "%s", value); + free(value); + } else + format_add(ft, "option_monitor", "%s", ""); + } else + format_add(ft, "option_monitor", "%s", ""); + if (!array) { value = options_to_string(o, NULL, 0); format_add(ft, "option_value", "%s", value); @@ -376,7 +407,8 @@ window_customize_build_options(struct window_customize_modedata *data, enum window_customize_scope scope0, struct options *oo0, enum window_customize_scope scope1, struct options *oo1, enum window_customize_scope scope2, struct options *oo2, - struct format_tree *ft, const char *filter, struct cmd_find_state *fs) + struct format_tree *ft, const char *filter, struct cmd_find_state *fs, + enum window_customize_option_type type) { struct mode_tree_item *top; struct options_entry *o = NULL, *loop; @@ -400,6 +432,7 @@ window_customize_build_options(struct window_customize_modedata *data, window_customize_find_user_options(oo2, &list, &size); for (i = 0; i < size; i++) { + o = NULL; if (oo2 != NULL) o = options_get(oo2, list[i]); if (o == NULL && oo1 != NULL) @@ -413,7 +446,7 @@ window_customize_build_options(struct window_customize_modedata *data, else scope = scope0; window_customize_build_option(data, top, scope, o, ft, filter, - fs); + fs, type); } free(list); @@ -437,7 +470,7 @@ window_customize_build_options(struct window_customize_modedata *data, else scope = scope0; window_customize_build_option(data, top, scope, o, ft, filter, - fs); + fs, type); loop = options_next(loop); } } @@ -556,19 +589,31 @@ window_customize_build(void *modedata, WINDOW_CUSTOMIZE_SERVER, global_options, WINDOW_CUSTOMIZE_NONE, NULL, WINDOW_CUSTOMIZE_NONE, NULL, - ft, filter, &fs); + ft, filter, &fs, WINDOW_CUSTOMIZE_OPTIONS); window_customize_build_options(data, "Session Options", (3ULL << 62)|(OPTIONS_TABLE_SESSION << 1)|1, WINDOW_CUSTOMIZE_GLOBAL_SESSION, global_s_options, WINDOW_CUSTOMIZE_SESSION, fs.s->options, WINDOW_CUSTOMIZE_NONE, NULL, - ft, filter, &fs); + ft, filter, &fs, WINDOW_CUSTOMIZE_OPTIONS); window_customize_build_options(data, "Window & Pane Options", (3ULL << 62)|(OPTIONS_TABLE_WINDOW << 1)|1, WINDOW_CUSTOMIZE_GLOBAL_WINDOW, global_w_options, WINDOW_CUSTOMIZE_WINDOW, fs.w->options, WINDOW_CUSTOMIZE_PANE, fs.wp->options, - ft, filter, &fs); + ft, filter, &fs, WINDOW_CUSTOMIZE_OPTIONS); + window_customize_build_options(data, "Session Hooks", + (3ULL << 62)|(1ULL << 8)|(OPTIONS_TABLE_SESSION << 1)|1, + WINDOW_CUSTOMIZE_GLOBAL_SESSION, global_s_options, + WINDOW_CUSTOMIZE_SESSION, fs.s->options, + WINDOW_CUSTOMIZE_NONE, NULL, + ft, filter, &fs, WINDOW_CUSTOMIZE_HOOKS); + window_customize_build_options(data, "Window & Pane Hooks", + (3ULL << 62)|(1ULL << 8)|(OPTIONS_TABLE_WINDOW << 1)|1, + WINDOW_CUSTOMIZE_GLOBAL_WINDOW, global_w_options, + WINDOW_CUSTOMIZE_WINDOW, fs.w->options, + WINDOW_CUSTOMIZE_PANE, fs.wp->options, + ft, filter, &fs, WINDOW_CUSTOMIZE_HOOKS); format_free(ft); ft = format_create_from_state(NULL, NULL, &fs); @@ -662,10 +707,12 @@ window_customize_draw_option(struct window_customize_modedata *data, const char *array_key; const char *space = "", *unit = ""; char *value = NULL, *expanded; + char *monitor; char *default_value = NULL; char choices[256] = ""; struct cmd_find_state fs; struct format_tree *ft; + int is_hook, is_monitor; if (!window_customize_check_item(data, item, &fs)) return; @@ -676,6 +723,8 @@ window_customize_draw_option(struct window_customize_modedata *data, if (o == NULL) return; oe = options_table_entry(o); + is_hook = (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)); + is_monitor = (options_get_monitor_data(o) != NULL); if (oe != NULL && oe->unit != NULL) { space = " "; @@ -683,9 +732,11 @@ window_customize_draw_option(struct window_customize_modedata *data, } ft = format_create_from_state(NULL, NULL, &fs); - if (oe == NULL || oe->text == NULL) - text = "This option doesn't have a description."; - else + if (oe == NULL || oe->text == NULL) { + text = is_monitor ? + "This hook runs when a monitor changes." : + "This option doesn't have a description."; + } else text = oe->text; if (!screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, "%s", text)) @@ -694,6 +745,12 @@ window_customize_draw_option(struct window_customize_modedata *data, if (s->cy >= cy + sy - 1) goto out; + if (is_monitor) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "This is a monitor hook.")) + goto out; + goto monitor; + } if (oe == NULL) text = "user"; else if ((oe->scope & (OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE)) == @@ -705,11 +762,31 @@ window_customize_draw_option(struct window_customize_modedata *data, text = "session"; else text = "server"; - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "This is a %s option.", text)) - goto out; + if (is_hook) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "This is a %s hook.", text)) + goto out; + } else { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "This is a %s option.", text)) + goto out; + } + +monitor: + monitor = hooks_monitor_to_string(o); + if (monitor != NULL) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Monitor: %s", monitor)) { + free(monitor); + goto out; + } + free(monitor); + } if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) { - if (array_key != NULL) { + if (is_hook) { + if (array_key == NULL) + goto out; + } else if (array_key != NULL) { if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, "This is an array option, key %s.", array_key)) @@ -734,9 +811,17 @@ window_customize_draw_option(struct window_customize_modedata *data, default_value = NULL; } } - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "Option value: %s%s%s", value, space, unit)) - goto out; + if (is_hook || is_monitor) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Hook command: %s%s%s", value, space, + unit)) + goto out; + } else { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Option value: %s%s%s", value, space, + unit)) + goto out; + } if (oe == NULL || oe->type == OPTIONS_TABLE_STRING) { expanded = format_expand(ft, value); if (strcmp(expanded, value) != 0) { From b1c347ee0ca31031efcea9c873017d72248b23b0 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 19:35:22 +0000 Subject: [PATCH 20/36] Replace last use of fgetln with getline, GitHub issue 5389 from Yayo Razo. --- prompt-history.c | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/prompt-history.c b/prompt-history.c index ad1ab4047..109043aa7 100644 --- a/prompt-history.c +++ b/prompt-history.c @@ -1,4 +1,4 @@ -/* $OpenBSD: prompt-history.c,v 1.1 2026/06/25 11:39:11 nicm Exp $ */ +/* $OpenBSD: prompt-history.c,v 1.2 2026/07/13 19:35:22 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -79,8 +79,9 @@ void prompt_load_history(void) { FILE *f; - char *history_file, *line, *tmp; - size_t length; + char *history_file, *line = NULL; + size_t length = 0; + ssize_t got; if ((history_file = prompt_find_history_file()) == NULL) return; @@ -94,23 +95,13 @@ prompt_load_history(void) } free(history_file); - for (;;) { - if ((line = fgetln(f, &length)) == NULL) - break; - - if (length > 0) { - if (line[length - 1] == '\n') { - line[length - 1] = '\0'; - prompt_add_typed_history(line); - } else { - tmp = xmalloc(length + 1); - memcpy(tmp, line, length); - tmp[length] = '\0'; - prompt_add_typed_history(tmp); - free(tmp); - } - } + while ((got = getline(&line, &length, f)) != -1) { + if (got > 0 && line[got - 1] == '\n') + line[got - 1] = '\0'; + if (got > 0) + prompt_add_typed_history(line); } + free(line); fclose(f); } From 9c65d349e8ca2dca2010c87733fe7c0a317432ce Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 22:08:58 +0100 Subject: [PATCH 21/36] Tweak times in tests. --- regress/alerts.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/regress/alerts.sh b/regress/alerts.sh index 2b9e4f495..60d9a05f7 100644 --- a/regress/alerts.sh +++ b/regress/alerts.sh @@ -46,7 +46,7 @@ wait_for() value=$($TMUX show -gqv "$option" 2>/dev/null || true) [ "$value" = "$expected" ] && return 0 i=$((i + 1)) - sleep 0.2 + sleep 0.5 done fail "expected $option to be '$expected' but got '$value'" } @@ -62,7 +62,7 @@ assert_unchanged() [ "$value" = "$expected" ] || \ fail "expected $option to remain '$expected' but got '$value'" i=$((i + 1)) - sleep 0.2 + sleep 0.5 done } @@ -77,7 +77,7 @@ wait_for_fmt() value=$($TMUX display -pt "$target" "$fmt" 2>/dev/null || true) [ "$value" = "$expected" ] && return 0 i=$((i + 1)) - sleep 0.2 + sleep 0.5 done fail "expected $fmt for $target to be '$expected' but got '$value'" } @@ -94,7 +94,7 @@ assert_fmt_unchanged() [ "$value" = "$expected" ] || \ fail "expected $fmt for $target to remain '$expected' but got '$value'" i=$((i + 1)) - sleep 0.2 + sleep 0.5 done } @@ -290,9 +290,9 @@ $TMUX selectw -t mon:w0 || fail "select-window w0 failed" # prevents an alert until a full quiet interval has passed afterwards. $TMUX neww -d -t mon: -n silreset cat || fail "new-window silreset failed" $TMUX set -g @log '' || fail "reset @log failed" -$TMUX set -wt mon:silreset monitor-silence 2 || +$TMUX set -wt mon:silreset monitor-silence 10 || fail "set monitor-silence silreset failed" -sleep 1 +sleep 3 activity mon:silreset assert_unchanged @log '' wait_for @log '|alert-silence:mon:silreset' From f0f59bc2fd47e0ef6f3f1635091e7ae81cf454ce Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 22:52:25 +0100 Subject: [PATCH 22/36] Fix a test. --- regress/hooks-notify.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/regress/hooks-notify.sh b/regress/hooks-notify.sh index 045b80af6..6eb6ae49e 100644 --- a/regress/hooks-notify.sh +++ b/regress/hooks-notify.sh @@ -449,10 +449,10 @@ $TMUX killp -t "$pane" || fail "kill-pane zoom failed" $TMUX set -g @a 0 || fail "set @a failed" $TMUX set -g @cl 0 || fail "set @cl failed" $TMUX set-hook -g client-created \ - 'set -gF @cl "#{@cl}|#{hook}:#{hook_client}"' || + 'set -gF @cl "#{?#{m:client-*,#{hook_client}},#{@cl}|#{hook}:#{hook_client},#{@cl}}"' || fail "set-hook client-created failed" $TMUX set-hook -g client-closed \ - 'set -gF @cl "#{@cl}|#{hook}:#{hook_client}"' || + 'set -gF @cl "#{?#{m:client-*,#{hook_client}},#{@cl}|#{hook}:#{hook_client},#{@cl}}"' || fail "set-hook client-closed failed" $TMUX set-hook -g client-attached 'set -gF @a "#{hook}"' || fail "set-hook client-attached failed" From 37da97fbe618efadc9c7a2f2d9a013c8e9155f87 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 20:02:00 +0000 Subject: [PATCH 23/36] Add environment to customize mode also --- window-customize.c | 342 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 322 insertions(+), 20 deletions(-) diff --git a/window-customize.c b/window-customize.c index 4a61eed72..ba1c060cd 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-customize.c,v 1.31 2026/07/13 19:32:28 nicm Exp $ */ +/* $OpenBSD: window-customize.c,v 1.32 2026/07/13 20:02:00 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -39,7 +39,11 @@ static void window_customize_key(struct window_mode_entry *, "#[fg=themelightgrey]#[ignore]#{option_value}" \ "#{?option_unit, #{option_unit},}" \ "," \ - "#{key}" \ + "#{?is_environment," \ + "#[fg=themelightgrey]#[ignore]#{environment_value}" \ + "," \ + "#{key}" \ + "}" \ "}" static const struct menu_item window_customize_menu_items[] = { @@ -73,7 +77,9 @@ enum window_customize_scope { WINDOW_CUSTOMIZE_SESSION, WINDOW_CUSTOMIZE_GLOBAL_WINDOW, WINDOW_CUSTOMIZE_WINDOW, - WINDOW_CUSTOMIZE_PANE + WINDOW_CUSTOMIZE_PANE, + WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT, + WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT }; enum window_customize_change { @@ -86,14 +92,23 @@ enum window_customize_option_type { WINDOW_CUSTOMIZE_HOOKS }; +enum window_customize_item_type { + WINDOW_CUSTOMIZE_ITEM_OPTION, + WINDOW_CUSTOMIZE_ITEM_KEY, + WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT +}; + struct window_customize_itemdata { struct window_customize_modedata *data; + enum window_customize_item_type type; enum window_customize_scope scope; char *table; key_code key; struct options *oo; + struct environ *environ; + int environ_flags; char *name; char *array_key; }; @@ -149,10 +164,27 @@ window_customize_get_tree(enum window_customize_scope scope, return (fs->w->options); case WINDOW_CUSTOMIZE_PANE: return (fs->wp->options); + case WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT: + case WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT: + return (NULL); } return (NULL); } +static struct environ * +window_customize_get_environment(enum window_customize_scope scope, + struct cmd_find_state *fs) +{ + switch (scope) { + case WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT: + return (global_environ); + case WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT: + return (fs->s->environ); + default: + return (NULL); + } +} + static int window_customize_check_item(struct window_customize_modedata *data, struct window_customize_itemdata *item, struct cmd_find_state *fsp) @@ -166,6 +198,10 @@ window_customize_check_item(struct window_customize_modedata *data, cmd_find_copy_state(fsp, &data->fs); else cmd_find_from_pane(fsp, data->wp, 0); + if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) { + return (item->environ == + window_customize_get_environment(item->scope, fsp)); + } return (item->oo == window_customize_get_tree(item->scope, fsp)); } @@ -203,6 +239,7 @@ window_customize_scope_text(enum window_customize_scope scope, xasprintf(&s, "pane %u", idx); break; case WINDOW_CUSTOMIZE_SESSION: + case WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT: xasprintf(&s, "session %s", fs->s->name); break; case WINDOW_CUSTOMIZE_WINDOW: @@ -258,6 +295,7 @@ window_customize_build_array(struct window_customize_modedata *data, format_add(ft, "option_value", "%s", value); item = window_customize_add_item(data); + item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; item->scope = scope; item->oo = oo; item->name = xstrdup(options_name(o)); @@ -355,6 +393,7 @@ window_customize_build_option(struct window_customize_modedata *data, free(expanded); } item = window_customize_add_item(data); + item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; item->oo = oo; item->scope = scope; item->name = xstrdup(name); @@ -497,6 +536,7 @@ window_customize_build_keys(struct window_customize_modedata *data, ft = format_create_from_state(NULL, NULL, fs); format_add(ft, "is_option", "0"); format_add(ft, "is_key", "1"); + format_add(ft, "is_environment", "0"); bd = key_bindings_first(kt); while (bd != NULL) { @@ -514,6 +554,7 @@ window_customize_build_keys(struct window_customize_modedata *data, } item = window_customize_add_item(data); + item->type = WINDOW_CUSTOMIZE_ITEM_KEY; item->scope = WINDOW_CUSTOMIZE_KEY; item->table = xstrdup(kt->name); item->key = bd->key; @@ -558,6 +599,80 @@ window_customize_build_keys(struct window_customize_modedata *data, format_free(ft); } +static void +window_customize_build_environment(struct window_customize_modedata *data, + const char *title, uint64_t tag, enum window_customize_scope scope, + struct environ *env, struct format_tree *ft, const char *filter, + struct cmd_find_state *fs) +{ + struct mode_tree_item *top; + struct window_customize_itemdata *item; + struct environ_entry *envent; + char *name, *text, *expanded, *value; + uint64_t item_tag; + int global; + + top = mode_tree_add(data->data, NULL, NULL, tag, title, NULL, 0); + mode_tree_no_tag(top); + + global = (scope == WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT); + format_add(ft, "is_option", "0"); + format_add(ft, "is_key", "0"); + format_add(ft, "is_environment", "1"); + format_add(ft, "environment_is_global", "%d", global); + + text = window_customize_scope_text(scope, fs); + format_add(ft, "environment_scope", "%s", text); + free(text); + + envent = environ_first(env); + while (envent != NULL) { + format_add(ft, "environment_name", "%s", envent->name); + format_add(ft, "environment_hidden", "%d", + !!(envent->flags & ENVIRON_HIDDEN)); + format_add(ft, "environment_removed", "%d", + envent->value == NULL); + if (envent->value == NULL) + value = xstrdup(""); + else + value = xstrdup(envent->value); + format_add(ft, "environment_value", "%s", value); + + if (filter != NULL) { + expanded = format_expand(ft, filter); + if (!format_true(expanded)) { + free(expanded); + free(value); + envent = environ_next(envent); + continue; + } + free(expanded); + } + + item = window_customize_add_item(data); + item->type = WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT; + item->scope = scope; + item->environ = env; + item->environ_flags = envent->flags; + item->name = xstrdup(envent->name); + + if (envent->value == NULL) { + xasprintf(&name, "-%s", envent->name); + text = NULL; + } else { + name = xstrdup(envent->name); + text = format_expand(ft, data->format); + } + item_tag = (2ULL << 62)|((uint64_t)(uintptr_t)envent); + mode_tree_add(data->data, top, item, item_tag, name, text, 0); + free(name); + free(text); + free(value); + + envent = environ_next(envent); + } +} + static void window_customize_build(void *modedata, __unused struct sort_criteria *sort_crit, __unused uint64_t *tag, @@ -583,6 +698,7 @@ window_customize_build(void *modedata, ft = format_create_from_state(NULL, NULL, &fs); format_add(ft, "is_option", "1"); format_add(ft, "is_key", "0"); + format_add(ft, "is_environment", "0"); window_customize_build_options(data, "Server Options", (3ULL << 62)|(OPTIONS_TABLE_SERVER << 1)|1, @@ -614,9 +730,18 @@ window_customize_build(void *modedata, WINDOW_CUSTOMIZE_WINDOW, fs.w->options, WINDOW_CUSTOMIZE_PANE, fs.wp->options, ft, filter, &fs, WINDOW_CUSTOMIZE_HOOKS); + window_customize_build_environment(data, "Global Environment", + (3ULL << 62)|(2ULL << 8)|1, + WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT, global_environ, ft, filter, + &fs); + window_customize_build_environment(data, "Session Environment", + (3ULL << 62)|(2ULL << 8)|3, + WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT, fs.s->environ, ft, filter, + &fs); format_free(ft); ft = format_create_from_state(NULL, NULL, &fs); + format_add(ft, "is_environment", "0"); i = 0; kt = key_bindings_first_table(); @@ -931,6 +1056,67 @@ out: format_free(ft); } +static void +window_customize_draw_environment(struct window_customize_modedata *data, + struct window_customize_itemdata *item, struct screen_write_ctx *ctx, + u_int sx, u_int sy) +{ + struct screen *s = ctx->s; + u_int cx = s->cx, cy = s->cy; + struct environ_entry *envent, *parent; + struct cmd_find_state fs; + const char *text; + + if (!window_customize_check_item(data, item, &fs)) + return; + envent = environ_find(item->environ, item->name); + if (envent == NULL) + return; + + if (item->scope == WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT) + text = "global"; + else + text = "session"; + if (!screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, + "This is a %s environment variable.", text)) + return; + if (envent->flags & ENVIRON_HIDDEN) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "This variable is hidden.")) + return; + } + + screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */ + if (s->cy >= cy + sy - 1) + return; + + if (envent->value == NULL) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Variable is removed.")) + return; + } else { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Variable value: %s", envent->value)) + return; + } + + if (item->scope != WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT) + return; + + parent = environ_find(global_environ, item->name); + if (parent == NULL) + return; + if (parent->value == NULL) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Global variable is removed.")) + return; + } else { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Global value: %s", parent->value)) + return; + } +} + static void window_customize_draw(void *modedata, void *itemdata, struct screen_write_ctx *ctx, u_int sx, u_int sy) @@ -941,8 +1127,10 @@ window_customize_draw(void *modedata, void *itemdata, if (item == NULL) return; - if (item->scope == WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_draw_key(data, item, ctx, sx, sy); + else if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) + window_customize_draw_environment(data, item, ctx, sx, sy); else window_customize_draw_option(data, item, ctx, sx, sy); } @@ -994,7 +1182,7 @@ static const char** window_customize_help(u_int *width, const char **item) { *width = 52; - *item = "option"; + *item = "item"; return (window_customize_help_lines); } @@ -1139,6 +1327,85 @@ fail: return (PROMPT_CLOSE); } +static enum prompt_result +window_customize_set_environment_callback(__unused 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 = item->data; + struct environ_entry *envent; + int flags; + + if (s == NULL || data->dead) + return (PROMPT_CLOSE); + if (item == NULL || !window_customize_check_item(data, item, NULL)) + return (PROMPT_CLOSE); + + flags = item->environ_flags; + envent = environ_find(item->environ, item->name); + if (envent != NULL) + flags = envent->flags; + environ_set(item->environ, item->name, flags, "%s", s); + + mode_tree_build(data->data); + mode_tree_draw(data->data); + data->wp->flags |= PANE_REDRAW; + + return (PROMPT_CLOSE); +} + +static void +window_customize_set_environment(struct client *c, + struct window_customize_modedata *data, + struct window_customize_itemdata *item, int global) +{ + struct window_customize_itemdata *new_item; + struct environ_entry *envent; + struct environ *env; + enum window_customize_scope scope; + struct cmd_find_state fs; + const char *space = ""; + char *prompt, *text; + + if (item == NULL || !window_customize_check_item(data, item, &fs)) + return; + envent = environ_find(item->environ, item->name); + if (envent == NULL) + return; + + if (global) { + scope = WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT; + env = global_environ; + } else { + scope = item->scope; + env = item->environ; + } + + text = window_customize_scope_text(scope, &fs); + if (*text != '\0') + space = ", for "; + else if (scope == WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT) + space = ", global"; + xasprintf(&prompt, "(%s%s%s) ", item->name, space, text); + free(text); + + new_item = xcalloc(1, sizeof *new_item); + new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT; + new_item->scope = scope; + new_item->environ = env; + new_item->environ_flags = envent->flags; + new_item->name = xstrdup(item->name); + + data->references++; + mode_tree_set_prompt(data->data, c, prompt, + envent->value == NULL ? "" : envent->value, PROMPT_TYPE_COMMAND, + PROMPT_NOFORMAT, window_customize_set_environment_callback, + window_customize_free_item_callback, new_item); + + free(prompt); +} + static void window_customize_set_option(struct client *c, struct window_customize_modedata *data, @@ -1176,6 +1443,8 @@ window_customize_set_option(struct client *c, case WINDOW_CUSTOMIZE_SERVER: case WINDOW_CUSTOMIZE_GLOBAL_SESSION: case WINDOW_CUSTOMIZE_GLOBAL_WINDOW: + case WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT: + case WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT: scope = item->scope; break; case WINDOW_CUSTOMIZE_SESSION: @@ -1210,6 +1479,10 @@ window_customize_set_option(struct client *c, else scope = WINDOW_CUSTOMIZE_WINDOW; break; + case WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT: + case WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT: + scope = item->scope; + break; } } if (scope == item->scope) @@ -1250,6 +1523,7 @@ window_customize_set_option(struct client *c, new_item = xcalloc(1, sizeof *new_item); new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; new_item->scope = scope; new_item->oo = oo; new_item->name = xstrdup(name); @@ -1331,6 +1605,7 @@ window_customize_set_array_key(struct client *c, new_item = xcalloc(1, sizeof *new_item); new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; new_item->scope = item->scope; new_item->oo = item->oo; new_item->name = xstrdup(item->name); @@ -1345,6 +1620,19 @@ window_customize_set_array_key(struct client *c, free(prompt); } +static void +window_customize_unset_environment(struct window_customize_modedata *data, + struct window_customize_itemdata *item) +{ + if (item == NULL || !window_customize_check_item(data, item, NULL)) + return; + if (environ_find(item->environ, item->name) == NULL) + return; + if (item == mode_tree_get_current(data->data)) + mode_tree_up(data->data, 0); + environ_unset(item->environ, item->name); +} + static void window_customize_unset_option(struct window_customize_modedata *data, struct window_customize_itemdata *item) @@ -1469,6 +1757,7 @@ window_customize_set_key(struct client *c, new_item = xcalloc(1, sizeof *new_item); new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_KEY; new_item->scope = item->scope; new_item->table = xstrdup(item->table); new_item->key = key; @@ -1485,6 +1774,7 @@ window_customize_set_key(struct client *c, new_item = xcalloc(1, sizeof *new_item); new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_KEY; new_item->scope = item->scope; new_item->table = xstrdup(item->table); new_item->key = key; @@ -1545,19 +1835,21 @@ window_customize_change_each(void *modedata, void *itemdata, switch (data->change) { case WINDOW_CUSTOMIZE_UNSET: - if (item->scope == WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_unset_key(data, item); + else if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) + window_customize_unset_environment(data, item); else window_customize_unset_option(data, item); break; case WINDOW_CUSTOMIZE_RESET: - if (item->scope == WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_reset_key(data, item); - else + else if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) window_customize_reset_option(data, item); break; } - if (item->scope != WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) options_push_changes(item->name); } @@ -1576,19 +1868,21 @@ window_customize_change_current_callback(__unused struct client *c, item = mode_tree_get_current(data->data); switch (data->change) { case WINDOW_CUSTOMIZE_UNSET: - if (item->scope == WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_unset_key(data, item); + else if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) + window_customize_unset_environment(data, item); else window_customize_unset_option(data, item); break; case WINDOW_CUSTOMIZE_RESET: - if (item->scope == WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_reset_key(data, item); - else + else if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) window_customize_reset_option(data, item); break; } - if (item->scope != WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) options_push_changes(item->name); mode_tree_build(data->data); mode_tree_draw(data->data); @@ -1636,7 +1930,7 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, switch (key) { case 'a': - if (item == NULL || item->scope == WINDOW_CUSTOMIZE_KEY) + if (item == NULL || item->type != WINDOW_CUSTOMIZE_ITEM_OPTION) break; window_customize_set_array_key(c, data, item); break; @@ -1644,8 +1938,10 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, case 's': if (item == NULL) break; - if (item->scope == WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_set_key(c, data, item); + else if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) + window_customize_set_environment(c, data, item, 0); else { window_customize_set_option(c, data, item, 0, 1); options_push_changes(item->name); @@ -1653,7 +1949,7 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, mode_tree_build(data->data); break; case 'w': - if (item == NULL || item->scope == WINDOW_CUSTOMIZE_KEY) + if (item == NULL || item->type != WINDOW_CUSTOMIZE_ITEM_OPTION) break; window_customize_set_option(c, data, item, 0, 0); options_push_changes(item->name); @@ -1661,14 +1957,20 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, break; case 'S': case 'W': - if (item == NULL || item->scope == WINDOW_CUSTOMIZE_KEY) + if (item == NULL || item->type == WINDOW_CUSTOMIZE_ITEM_KEY) break; - window_customize_set_option(c, data, item, 1, 0); - options_push_changes(item->name); + if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) + window_customize_set_environment(c, data, item, 1); + else { + window_customize_set_option(c, data, item, 1, 0); + options_push_changes(item->name); + } mode_tree_build(data->data); break; case 'd': - if (item == NULL || item->array_key != NULL) + if (item == NULL || + item->type != WINDOW_CUSTOMIZE_ITEM_OPTION || + item->array_key != NULL) break; xasprintf(&prompt, "Reset %s to default? ", item->name); data->references++; From df6ed20271a80d93783d2efc7f7bdd688c5cbb1b Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 23:03:33 +0100 Subject: [PATCH 24/36] Add customize mode test. --- regress/customize-mode.sh | 746 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 746 insertions(+) create mode 100644 regress/customize-mode.sh diff --git a/regress/customize-mode.sh b/regress/customize-mode.sh new file mode 100644 index 000000000..89ebc6a42 --- /dev/null +++ b/regress/customize-mode.sh @@ -0,0 +1,746 @@ +#!/bin/sh + +# Exercise customize-mode-specific mutations. mode-mutation.sh covers stale +# mode-tree data while customize-mode is open; this covers the actions unique to +# customize mode: adding, setting, editing, resetting, renaming array keys and +# unsetting options, hooks, environment variables and key bindings. + +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" +OUT="$TEST_TMUX -LtestB$$ -f/dev/null" +FORMAT='CM #{?is_option,O,#{?is_environment,E,K}} #{option_name}#{environment_name}#{key}=#{option_value}#{environment_value}' + +cleanup_servers() +{ + $TMUX kill-server 2>/dev/null + $OUT kill-server 2>/dev/null + sleep 0.5 +} + +cleanup() +{ + cleanup_servers + rm -rf "$TMP" +} +trap cleanup EXIT + +fail() +{ + echo "$1" >&2 + cleanup + exit 1 +} + +capture() +{ + $OUT capture-pane -p -t out:0 2>/dev/null +} + +settle() +{ + sleep 0.3 +} + +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() +{ + want=$1 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t cm:0 '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane mode state is $got, expected $want" +} + +assert_alive() +{ + $TMUX display-message -p 'alive' >/dev/null 2>&1 || + fail "$1: server exited" +} + +start_client() +{ + cleanup_servers + $TMUX new-session -d -s cm -n main -x 80 -y 24 'cat' || + fail "new-session failed" + $OUT new-session -d -s out -x 80 -y 24 "$TMUX attach -t cm" || + fail "outer client failed" + wait_clients 1 +} + +open_customize() +{ + if [ "$#" -eq 0 ]; then + $TMUX customize-mode -t cm:0 -F "$FORMAT" || + fail "customize-mode failed" + else + $TMUX customize-mode -t cm:0 -F "$FORMAT" -f "$1" || + fail "customize-mode failed" + fi + wait_mode 1 + settle +} + +send() +{ + $TMUX send-keys -t cm:0 "$@" || fail "send-keys failed: $*" +} + +send_literal() +{ + $TMUX send-keys -t cm:0 -l "$1" || fail "send literal failed: $1" +} + +repeat_key() +{ + key=$1 + count=$2 + + i=0 + while [ "$i" -lt "$count" ]; do + send "$key" + i=$((i + 1)) + done +} + +replace_prompt() +{ + send C-u + send_literal "$1" + send Enter + settle +} + +assert_equals() +{ + got=$1 + want=$2 + what=$3 + + [ "$got" = "$want" ] || + fail "$what: got '$got', expected '$want'" +} + +assert_empty() +{ + got=$1 + what=$2 + + [ -z "$got" ] || fail "$what: got '$got', expected empty" +} + +assert_contains() +{ + text=$1 + needle=$2 + what=$3 + + echo "$text" | grep -F -q "$needle" || + fail "$what: missing '$needle' in '$text'" +} + +assert_not_contains() +{ + text=$1 + needle=$2 + what=$3 + + echo "$text" | grep -F -q "$needle" && + fail "$what: found '$needle' in '$text'" + return 0 +} + +test_user_option() +{ + start_client + + open_customize + send Enter + settle + send_literal "cm_add added" + send Enter + settle + assert_equals "$($TMUX show-options -sqv @cm_add)" "added" \ + "added server user option" + + send q + wait_mode 0 + open_customize '#{==:#{option_name},@cm_add}' + send Right j Enter + settle + replace_prompt "changed" + assert_equals "$($TMUX show-options -sqv @cm_add)" "changed" \ + "set user option" + + send u y + settle + assert_empty "$($TMUX show-options -sqv @cm_add)" "unset user option" + send q + wait_mode 0 +} + +test_reset_window_pane_option() +{ + start_client + + $TMUX set-option -g window-style fg=red || + fail "set global window-style failed" + $TMUX set-window-option -t cm:0 window-style fg=green || + fail "set window window-style failed" + $TMUX set-option -p -t cm:0.0 window-style fg=blue || + fail "set pane window-style failed" + + open_customize '#{==:#{option_name},window-style}' + send j j Right j + settle + send d y + settle + + assert_equals "$($TMUX show-option -gqv window-style)" "default" \ + "reset global window-style" + assert_empty "$($TMUX show-options -wqv -t cm:0 window-style)" \ + "reset window window-style" + assert_empty "$($TMUX show-option -p -t cm:0.0 -qv window-style)" \ + "reset pane window-style" + send q + wait_mode 0 +} + +test_environment() +{ + start_client + + open_customize + repeat_key j 6 + send Enter + settle + send_literal "CM_ENV=one" + send Enter + settle + assert_equals "$($TMUX show-environment -t cm CM_ENV)" \ + "CM_ENV=one" "add session environment" + + send q + wait_mode 0 + open_customize '#{==:#{environment_name},CM_ENV}' + repeat_key j 6 + send Right j Enter + settle + replace_prompt "two" + assert_equals "$($TMUX show-environment -t cm CM_ENV)" \ + "CM_ENV=two" "set session environment" + + send S + settle + replace_prompt "global" + assert_equals "$($TMUX show-environment -g CM_ENV)" \ + "CM_ENV=global" "set global environment" + + send u y + settle + $TMUX show-environment -t cm CM_ENV >/dev/null 2>&1 && + fail "unset session environment: variable still exists" + send q + wait_mode 0 +} + +test_user_hook() +{ + start_client + + open_customize + repeat_key j 3 + send Enter + settle + send_literal "cm_hook display-message hook" + send Enter + settle + assert_equals "$($TMUX show-option -qv @cm_hook)" \ + "display-message hook" "add user hook" + + send q + wait_mode 0 + open_customize '#{==:#{option_name},@cm_hook}' + repeat_key j 3 + send Right j + settle + assert_contains "$(capture)" "@cm_hook" "user hook shown as hook" + + send u y + settle + assert_empty "$($TMUX show-option -qv @cm_hook)" "unset user hook" + send q + wait_mode 0 +} + +test_hook_array() +{ + start_client + + $TMUX set-hook -g 'after-new-session[0]' 'display-message old' || + fail "set hook array failed" + + open_customize '#{==:#{option_name},after-new-session}' + repeat_key j 3 + send Right j Right j + settle + + send a + settle + replace_prompt "1" + hooks=$($TMUX show-hooks -g after-new-session) + assert_contains "$hooks" "after-new-session[1] display-message old" \ + "renamed hook array key" + assert_not_contains "$hooks" "after-new-session[0]" \ + "old hook array key removed" + + send Enter + settle + replace_prompt "display-message new" + hooks=$($TMUX show-hooks -g after-new-session) + assert_contains "$hooks" "after-new-session[1] display-message new" \ + "set hook array value" + + send u y + settle + hooks=$($TMUX show-hooks -g after-new-session) + assert_not_contains "$hooks" "after-new-session[1]" \ + "unset hook array item" + send q + wait_mode 0 +} + +test_changed_only() +{ + start_client + + $TMUX set-option -s @cm_changed yes || + fail "set changed server option failed" + $TMUX bind-key -T prefix User1 display-message changed-only || + fail "set changed key failed" + + open_customize + send C + settle + send M-+ + settle + screen=$(capture) + assert_contains "$screen" "@cm_changed" "changed-only user option" + assert_contains "$screen" "User1" "changed-only key" + assert_not_contains "$screen" "Global Environment" \ + "changed-only hides environment" + send q + wait_mode 0 +} + +test_reset_key() +{ + start_client + + orig=$($TMUX list-keys -T prefix -F '#{key_command}' c) || + fail "default c binding not found" + $TMUX bind-key -T prefix c display-message changed || + fail "change c binding failed" + + open_customize '#{==:#{key},c}' + repeat_key j 10 + send Right j d y + settle + after=$($TMUX list-keys -T prefix -F '#{key_command}' c) || + fail "reset c binding not found" + assert_equals "$after" "$orig" "reset key binding" + send q + wait_mode 0 +} + +test_tagged_unset() +{ + start_client + + $TMUX set-option -s @cm_tag_a one || fail "set tag option a failed" + $TMUX set-option -s @cm_tag_b two || fail "set tag option b failed" + + open_customize '#{m/r:^@cm_tag_,#{option_name}}' + send Right j t j t U y + settle + assert_empty "$($TMUX show-options -sqv @cm_tag_a)" \ + "tagged unset option a" + assert_empty "$($TMUX show-options -sqv @cm_tag_b)" \ + "tagged unset option b" + send q + wait_mode 0 +} + +test_key_binding() +{ + start_client + + open_customize + repeat_key j 10 + send Enter + settle + send_literal "User0 display-message before" + send Enter + settle + keys=$($TMUX list-keys -T prefix User0 2>/dev/null) || + fail "added key binding not found" + assert_contains "$keys" "display-message before" "add key binding" + + send q + wait_mode 0 + open_customize '#{==:#{key},User0}' + repeat_key j 10 + send Right j Right j + settle + + send s + settle + replace_prompt "display-message after" + keys=$($TMUX list-keys -T prefix User0 2>/dev/null) || + fail "edited key binding not found" + assert_contains "$keys" "display-message after" "set key command" + + send j s + settle + replace_prompt "custom note" + note=$($TMUX list-keys -T prefix -F '#{key_note}' User0) + assert_equals "$note" "custom note" "set key note" + + send j s + settle + keys=$($TMUX list-keys -T prefix User0 2>/dev/null) || + fail "repeat key binding not found" + assert_contains "$keys" "bind-key -r" "toggle key repeat" + + send u y + settle + $TMUX list-keys -T prefix User0 >/dev/null 2>&1 && + fail "unset key binding: binding still exists" + send q + wait_mode 0 +} + +test_editor() +{ + start_client + + editor="$TMP/editor.sh" + editor_value="$TMP/editor-value" + { + echo '#!/bin/sh' + printf 'cat "%s" > "$1"\n' "$editor_value" + } > "$editor" || fail "write editor helper failed" + chmod +x "$editor" || fail "chmod editor helper failed" + + $TMUX set-option -g editor "$editor" || fail "set editor failed" + $TMUX set-option -g @cm_edit old || fail "set edit option failed" + + printf 'edited' > "$editor_value" || fail "write editor value failed" + open_customize '#{==:#{option_name},@cm_edit}' + send j Right j e + + i=0 + while [ "$i" -lt 50 ]; do + value=$($TMUX show-option -gqv @cm_edit) + [ "$value" = "edited" ] && break + sleep 0.2 + i=$((i + 1)) + done + assert_equals "$value" "edited" "edit option in editor" + send q + wait_mode 0 + + $TMUX bind-key -T prefix User2 display-message old || + fail "set editor key failed" + printf 'display-message edited' > "$editor_value" || + fail "write editor key command failed" + open_customize '#{==:#{key},User2}' + repeat_key j 10 + send Right j Right j e + i=0 + while [ "$i" -lt 50 ]; do + value=$($TMUX list-keys -T prefix -F '#{key_command}' User2) + [ "$value" = "display-message edited" ] && break + sleep 0.2 + i=$((i + 1)) + done + assert_equals "$value" "display-message edited" \ + "edit key command in editor" + + printf 'edited note' > "$editor_value" || + fail "write editor key note failed" + send j e + i=0 + while [ "$i" -lt 50 ]; do + value=$($TMUX list-keys -T prefix -F '#{key_note}' User2) + [ "$value" = "edited note" ] && break + sleep 0.2 + i=$((i + 1)) + done + assert_equals "$value" "edited note" "edit key note in editor" + send q + wait_mode 0 + + $TMUX set-environment -t cm CM_EDIT_ENV old || + fail "set editor environment failed" + printf 'env-edited' > "$editor_value" || + fail "write editor environment failed" + open_customize '#{==:#{environment_name},CM_EDIT_ENV}' + repeat_key j 6 + send Right j e + i=0 + while [ "$i" -lt 50 ]; do + value=$($TMUX show-environment -t cm CM_EDIT_ENV) + [ "$value" = "CM_EDIT_ENV=env-edited" ] && break + sleep 0.2 + i=$((i + 1)) + done + assert_equals "$value" "CM_EDIT_ENV=env-edited" \ + "edit environment in editor" + send q + wait_mode 0 +} + +test_stale_current_items() +{ + start_client + + $TMUX set-option -s @cm_stale_option old || + fail "set stale option failed" + open_customize '#{==:#{option_name},@cm_stale_option}' + send Right j + settle + $TMUX set-option -su @cm_stale_option || + fail "external unset stale option failed" + send Enter + send u y + send d y + settle + assert_alive "stale option current item" + send q + wait_mode 0 + + $TMUX set-environment -t cm CM_STALE_ENV old || + fail "set stale environment failed" + open_customize '#{==:#{environment_name},CM_STALE_ENV}' + repeat_key j 6 + send Right j + settle + $TMUX set-environment -t cm -u CM_STALE_ENV || + fail "external unset stale environment failed" + send Enter + send S + send u y + settle + assert_alive "stale environment current item" + send q + wait_mode 0 + + $TMUX bind-key -T prefix User3 display-message stale || + fail "set stale key failed" + open_customize '#{==:#{key},User3}' + repeat_key j 10 + send Right j + settle + $TMUX unbind-key -T prefix User3 || + fail "external unbind stale key failed" + send s + send d y + send u y + settle + assert_alive "stale key current item" + send q + wait_mode 0 +} + +test_prompt_invalidation() +{ + start_client + + $TMUX set-option -s @cm_prompt_stale old || + fail "set prompt stale option failed" + open_customize '#{==:#{option_name},@cm_prompt_stale}' + send Right j Enter + settle + $TMUX set-option -su @cm_prompt_stale || + fail "external unset prompt option failed" + replace_prompt "new" + assert_empty "$($TMUX show-options -sqv @cm_prompt_stale)" \ + "prompt stale option should not be recreated" + assert_alive "stale option prompt" + send q + wait_mode 0 + + $TMUX bind-key -T prefix User4 display-message old || + fail "set prompt stale key failed" + open_customize '#{==:#{key},User4}' + repeat_key j 10 + send Right j Right j s + settle + $TMUX unbind-key -T prefix User4 || + fail "external unbind prompt key failed" + replace_prompt "display-message new" + $TMUX list-keys -T prefix User4 >/dev/null 2>&1 && + fail "prompt stale key should stay unbound" + assert_alive "stale key prompt" + send q + wait_mode 0 + + $TMUX set-hook -g 'after-new-session[0]' 'display-message old' || + fail "set prompt stale hook array failed" + $TMUX set-hook -g 'after-new-session[1]' 'display-message taken' || + fail "set prompt stale hook array target failed" + open_customize '#{==:#{option_name},after-new-session}' + repeat_key j 3 + send Right j Right j a + settle + replace_prompt "1" + hooks=$($TMUX show-hooks -g after-new-session) + assert_contains "$hooks" "after-new-session[0] display-message old" \ + "array rename to existing key keeps old key" + assert_contains "$hooks" "after-new-session[1] display-message taken" \ + "array rename to existing key keeps target key" + assert_alive "existing array key prompt" + send q + wait_mode 0 +} + +test_editor_invalidation() +{ + start_client + + editor="$TMP/slow-editor.sh" + { + echo '#!/bin/sh' + echo 'sleep 1' + echo 'printf edited > "$1"' + } > "$editor" || fail "write slow editor helper failed" + chmod +x "$editor" || fail "chmod slow editor helper failed" + $TMUX set-option -g editor "$editor" || fail "set slow editor failed" + + $TMUX set-option -g @cm_editor_stale old || + fail "set editor stale option failed" + open_customize '#{==:#{option_name},@cm_editor_stale}' + send j Right j e + settle + $TMUX set-option -gu @cm_editor_stale || + fail "external unset editor option failed" + sleep 2 + assert_empty "$($TMUX show-option -gqv @cm_editor_stale)" \ + "editor stale option should not be recreated" + assert_alive "stale option editor" + send q + wait_mode 0 + + $TMUX bind-key -T prefix User5 display-message old || + fail "set editor stale key failed" + open_customize '#{==:#{key},User5}' + repeat_key j 10 + send Right j Right j e + settle + $TMUX unbind-key -T prefix User5 || + fail "external unbind editor key failed" + sleep 2 + $TMUX list-keys -T prefix User5 >/dev/null 2>&1 && + fail "editor stale key should stay unbound" + assert_alive "stale key editor" + send q + wait_mode 0 + + $TMUX set-environment -t cm CM_EDITOR_STALE old || + fail "set editor stale environment failed" + open_customize '#{==:#{environment_name},CM_EDITOR_STALE}' + repeat_key j 6 + send Right j e + settle + $TMUX set-environment -t cm -u CM_EDITOR_STALE || + fail "external unset editor environment failed" + sleep 2 + assert_equals "$($TMUX show-environment -t cm CM_EDITOR_STALE)" \ + "CM_EDITOR_STALE=edited" "editor stale environment is reset" + assert_alive "stale environment editor" + send q + wait_mode 0 +} + +test_invalid_prompts() +{ + start_client + + open_customize + send Enter + settle + send_literal "bad value" + send Enter + settle + assert_empty "$($TMUX show-options -sqv bad)" "invalid user option name" + assert_alive "invalid user option prompt" + send q + wait_mode 0 + + open_customize + repeat_key j 10 + send Enter + settle + send_literal "NoSuchKey display-message invalid" + send Enter + settle + assert_alive "invalid key prompt" + send q + wait_mode 0 + + $TMUX bind-key -T prefix User6 display-message valid || + fail "set invalid command key failed" + open_customize '#{==:#{key},User6}' + repeat_key j 10 + send Right j Right j s + settle + replace_prompt "not-a-command -Z" + keys=$($TMUX list-keys -T prefix -F '#{key_command}' User6) + assert_equals "$keys" "display-message valid" \ + "invalid key command keeps old command" + assert_alive "invalid key command prompt" + send q + wait_mode 0 +} + +cleanup_servers +test_user_option +test_reset_window_pane_option +test_environment +test_user_hook +test_hook_array +test_changed_only +test_reset_key +test_tagged_unset +test_key_binding +test_editor +test_stale_current_items +test_prompt_invalidation +test_editor_invalidation +test_invalid_prompts +cleanup +exit 0 From 2a75cc22366286980a3d941276103bc349a337ea Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 21:22:45 +0000 Subject: [PATCH 25/36] Apply dragging change to scroll-down as well as scroll-up. --- window-copy.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/window-copy.c b/window-copy.c index 2213dcebb..3d9194c69 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.416 2026/07/13 16:30:28 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.417 2026/07/13 21:22:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2359,6 +2359,7 @@ window_copy_cmd_scroll_down(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; + int dragging; /* * If at the bottom and scroll_exit is active with no selection, @@ -2371,6 +2372,16 @@ window_copy_cmd_scroll_down(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_NOTHING); } + /* With a selection but no active drag, only scroll the view. */ + dragging = (cs->c != NULL && cs->c->tty.mouse_drag_flag != 0); + if (data->screen.sel != NULL && !dragging) { + data->cursordrag = CURSORDRAG_NONE; + data->lineflag = LINE_SEL_NONE; + /* Move down in the history. */ + window_copy_scroll_up(wme, np); + 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) From 09d5977263224f7cb9f94cf6969c2de11db97a59 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 22:03:08 +0000 Subject: [PATCH 26/36] Add e key to open editor to customize mode (like buffer mode) and a key to show only changed items. --- hooks.c | 15 +- tmux.1 | 3 +- tmux.h | 3 +- window-customize.c | 1035 +++++++++++++++++++++++++++++++++++++++----- 4 files changed, 954 insertions(+), 102 deletions(-) diff --git a/hooks.c b/hooks.c index fd55c7273..ae7d90de4 100644 --- a/hooks.c +++ b/hooks.c @@ -1,4 +1,4 @@ -/* $OpenBSD: hooks.c,v 1.13 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: hooks.c,v 1.14 2026/07/13 22:03:08 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -264,6 +264,19 @@ hooks_add_event(const char *name) TAILQ_INSERT_TAIL(&hooks_events, he, entry); } +/* Return if a hook event sink exists. */ +int +hooks_is_event(const char *name) +{ + struct hooks_event *he; + + TAILQ_FOREACH(he, &hooks_events, entry) { + if (strcmp(he->name, name) == 0) + return (1); + } + return (0); +} + /* Return if an event name can be fired through the hooks path. */ int hooks_valid_event_name(const char *name) diff --git a/tmux.1 b/tmux.1 index 52e83ed98..0620f2485 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1137 2026/07/13 16:30:28 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1138 2026/07/13 22:03:08 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -3087,6 +3087,7 @@ The following keys may be used in customize mode: .It Li "w" Ta "Set window option value, if option is for pane and window" .It Li "d" Ta "Set an option or key to the default" .It Li "D" Ta "Set tagged options and tagged keys to the default" +.It Li "e" Ta "Open the value in an editor" .It Li "u" Ta "Unset an option (set to default value if global) or unbind a key" .It Li "U" Ta "Unset tagged options and unbind tagged keys" .It Li "C\-s" Ta "Search by name" diff --git a/tmux.h b/tmux.h index 8e548be3c..fc9153e19 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1401 2026/07/13 16:07:47 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1402 2026/07/13 22:03:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2709,6 +2709,7 @@ char *format_trim_right(const char *, u_int); /* hooks.c */ void hooks_add_event(const char *); +int hooks_is_event(const char *); int hooks_valid_event_name(const char *); void hooks_build_events(void); void hooks_run(struct cmdq_item *, const char *); diff --git a/window-customize.c b/window-customize.c index ba1c060cd..d4b7e7d86 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-customize.c,v 1.32 2026/07/13 20:02:00 nicm Exp $ */ +/* $OpenBSD: window-customize.c,v 1.33 2026/07/13 22:03:08 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -29,6 +29,7 @@ static struct screen *window_customize_init(struct window_mode_entry *, static void window_customize_free(struct window_mode_entry *); static void window_customize_resize(struct window_mode_entry *, u_int, u_int); +static void window_customize_update(struct window_mode_entry *); static void window_customize_key(struct window_mode_entry *, struct client *, struct session *, struct winlink *, key_code, struct mouse_event *); @@ -48,12 +49,15 @@ static void window_customize_key(struct window_mode_entry *, static const struct menu_item window_customize_menu_items[] = { { "Select", '\r', NULL }, + { "Edit", 'e', NULL }, { "Expand", KEYC_RIGHT, NULL }, { "", KEYC_NONE, NULL }, { "Tag", 't', NULL }, { "Tag All", '\024', NULL }, { "Tag None", 'T', NULL }, { "", KEYC_NONE, NULL }, + { "Changed Only", 'C', NULL }, + { "", KEYC_NONE, NULL }, { "Cancel", 'q', NULL }, { NULL, KEYC_NONE, NULL } @@ -66,6 +70,7 @@ const struct window_mode window_customize_mode = { .init = window_customize_init, .free = window_customize_free, .resize = window_customize_resize, + .update = window_customize_update, .key = window_customize_key, }; @@ -98,9 +103,17 @@ enum window_customize_item_type { WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT }; +enum window_customize_edit_type { + WINDOW_CUSTOMIZE_EDIT_OPTION, + WINDOW_CUSTOMIZE_EDIT_KEY_COMMAND, + WINDOW_CUSTOMIZE_EDIT_KEY_NOTE, + WINDOW_CUSTOMIZE_EDIT_ENVIRONMENT +}; + struct window_customize_itemdata { struct window_customize_modedata *data; - enum window_customize_item_type type; + enum window_customize_item_type type; + enum window_customize_option_type option_type; enum window_customize_scope scope; char *table; @@ -119,8 +132,11 @@ struct window_customize_modedata { int references; struct mode_tree_data *data; + struct spawn_editor_state *editor; + struct window_customize_editdata *edit; char *format; int hide_global; + int hide_default; int prompt_flags; struct window_customize_itemdata **item_list; @@ -130,6 +146,13 @@ struct window_customize_modedata { enum window_customize_change change; }; +struct window_customize_editdata { + u_int wp_id; + enum window_customize_edit_type edit_type; + struct window_customize_itemdata *item; + struct spawn_editor_state *editor; +}; + static uint64_t window_customize_get_tag(struct options_entry *o, struct options_array_item *a, const struct options_table_entry *oe) @@ -263,6 +286,38 @@ window_customize_add_item(struct window_customize_modedata *data) return (item); } +static int printflike(7, 8) +window_customize_write_value(struct screen_write_ctx *ctx, u_int cx, + u_int sx, u_int sy, int more, const char *label, const char *fmt, ...) +{ + struct screen *s = ctx->s; + struct grid_cell gc; + va_list ap; + char *value; + u_int cy = s->cy; + int retval; + + if (sy == 0) + return (0); + if (!screen_write_text(ctx, cx, sx, sy, 1, &grid_default_cell, "%s", + label)) + return (0); + if (s->cy - cy >= sy) + return (0); + sy -= s->cy - cy; + + memcpy(&gc, &grid_default_cell, sizeof gc); + gc.fg = COLOUR_THEME_LIGHT_GREY|COLOUR_FLAG_THEME; + + va_start(ap, fmt); + xvasprintf(&value, fmt, ap); + va_end(ap); + + retval = screen_write_text(ctx, cx, sx, sy, more, &gc, "%s", value); + free(value); + return (retval); +} + static void window_customize_free_item(struct window_customize_itemdata *item) { @@ -272,7 +327,266 @@ window_customize_free_item(struct window_customize_itemdata *item) free(item); } +static struct window_customize_itemdata * +window_customize_copy_item(struct window_customize_itemdata *item) +{ + struct window_customize_itemdata *new_item; + + new_item = xcalloc(1, sizeof *new_item); + new_item->data = item->data; + new_item->type = item->type; + new_item->option_type = item->option_type; + new_item->scope = item->scope; + new_item->key = item->key; + new_item->oo = item->oo; + new_item->environ = item->environ; + new_item->environ_flags = item->environ_flags; + if (item->table != NULL) + new_item->table = xstrdup(item->table); + if (item->name != NULL) + new_item->name = xstrdup(item->name); + if (item->array_key != NULL) + new_item->array_key = xstrdup(item->array_key); + return (new_item); +} + static void +window_customize_finish_edit(struct window_customize_editdata *ed) +{ + window_customize_free_item(ed->item); + free(ed); +} + +static void +window_customize_draw_waiting(struct window_customize_modedata *data) +{ + struct screen_write_ctx ctx; + struct screen *s = data->wp->screen; + struct grid_cell gc; + char text[128]; + u_int sx, sy, box_w, box_h, x, y, text_x; + size_t textlen; + pid_t pid; + + if (data->editor == NULL) + return; + sx = screen_size_x(s); + sy = screen_size_y(s); + if (sx == 0 || sy == 0) + return; + + pid = spawn_get_editor_pid(data->editor); + if (pid == -1) + xsnprintf(text, sizeof text, "WAITING FOR EDITOR"); + else { + xsnprintf(text, sizeof text, "WAITING FOR EDITOR (PID %ld)", + (long)pid); + } + + textlen = strlen(text); + box_w = textlen + 4; + box_h = 3; + if (sx < box_w || sy < box_h) + return; + x = (sx - box_w) / 2; + y = (sy - box_h) / 2; + text_x = x + (box_w - textlen) / 2; + + memcpy(&gc, &grid_default_cell, sizeof gc); + screen_write_start(&ctx, s); + screen_write_cursormove(&ctx, x, y, 0); + screen_write_box(&ctx, box_w, box_h, BOX_LINES_DEFAULT, &gc, NULL); + screen_write_cursormove(&ctx, text_x, y + 1, 0); + screen_write_nputs(&ctx, box_w - 2, &gc, "%s", text); + screen_write_stop(&ctx); +} + +static int +window_customize_set_option_value(struct window_customize_itemdata *item, + const char *s, char **cause) +{ + struct options_entry *o; + const struct options_table_entry *oe; + struct options *oo = item->oo; + const char *name = item->name; + const char *array_key = item->array_key; + u_int idx; + char keybuf[32]; + + o = options_get(oo, name); + if (o == NULL) + return (-1); + oe = options_table_entry(o); + + if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) { + if (array_key == NULL) { + for (idx = 0; idx < INT_MAX; idx++) { + if (options_array_getv(o, "%u", idx) == NULL) + break; + } + xsnprintf(keybuf, sizeof keybuf, "%u", idx); + array_key = keybuf; + } + if (options_array_set(o, array_key, s, 0, cause) != 0) + return (-1); + } else { + if (options_from_string(oo, oe, name, s, 0, cause) != 0) + return (-1); + } + if (item->option_type == WINDOW_CUSTOMIZE_HOOKS && *name == '@') + hooks_add_event(name); + + options_push_changes(item->name); + return (0); +} + +static int +window_customize_option_editable(struct window_customize_modedata *data, + struct window_customize_itemdata *item) +{ + struct options_entry *o; + const struct options_table_entry *oe; + + if (item->type != WINDOW_CUSTOMIZE_ITEM_OPTION || + !window_customize_check_item(data, item, NULL)) + return (0); + + o = options_get(item->oo, item->name); + if (o == NULL) + return (0); + oe = options_table_entry(o); + if (oe == NULL) + return (1); + if (oe->type == OPTIONS_TABLE_FLAG || oe->type == OPTIONS_TABLE_CHOICE) + return (0); + return (1); +} + +static int +window_customize_set_command_value(struct window_customize_itemdata *item, + const char *s, char **cause) +{ + struct key_binding *bd; + struct cmd_parse_result *pr; + + if (!window_customize_get_key(item, NULL, &bd)) + return (-1); + + pr = cmd_parse_from_string(s, NULL); + switch (pr->status) { + case CMD_PARSE_ERROR: + *cause = pr->error; + return (-1); + case CMD_PARSE_SUCCESS: + break; + } + cmd_list_free(bd->cmdlist); + bd->cmdlist = pr->cmdlist; + return (0); +} + +static int +window_customize_set_note_value(struct window_customize_itemdata *item, + const char *s) +{ + struct key_binding *bd; + + if (!window_customize_get_key(item, NULL, &bd)) + return (-1); + + free((void *)bd->note); + if (*s == '\0') + bd->note = NULL; + else + bd->note = xstrdup(s); + return (0); +} + +static void +window_customize_set_environment_value(struct window_customize_itemdata *item, + const char *s) +{ + struct environ_entry *envent; + int flags; + + flags = item->environ_flags; + envent = environ_find(item->environ, item->name); + if (envent != NULL) + flags = envent->flags; + environ_set(item->environ, item->name, flags, "%s", s); +} + +static int +window_customize_option_is_changed(struct options_entry *o, + const char *array_key) +{ + const struct options_table_entry *oe = options_table_entry(o); + struct options *oo; + struct options_entry *defaults; + union options_value *ov, *default_ov; + char *value, *default_value; + int changed; + + if (oe == NULL || options_get_monitor_data(o) != NULL) + return (1); + if (*options_name(o) == '@' && hooks_is_event(options_name(o))) + return (1); + + if (oe->flags & OPTIONS_TABLE_IS_ARRAY) { + oo = options_create(NULL); + defaults = options_default(oo, oe); + if (array_key != NULL) { + ov = options_array_get(o, array_key); + default_ov = options_array_get(defaults, array_key); + if (ov == NULL || default_ov == NULL) { + changed = (ov != default_ov); + options_free(oo); + return (changed); + } + } + value = options_to_string(o, array_key, 0); + default_value = options_to_string(defaults, array_key, 0); + changed = (strcmp(value, default_value) != 0); + free(value); + free(default_value); + options_free(oo); + return (changed); + } + + value = options_to_string(o, NULL, 0); + default_value = options_default_to_string(oe); + changed = (strcmp(value, default_value) != 0); + free(value); + free(default_value); + return (changed); +} + +static int +window_customize_key_is_changed(struct key_table *kt, struct key_binding *bd) +{ + struct key_binding *default_bd; + char *cmd, *default_cmd; + int changed; + + default_bd = key_bindings_get_default(kt, bd->key); + if (default_bd == NULL) + return (1); + if (bd->flags != default_bd->flags) + return (1); + if ((bd->note == NULL) != (default_bd->note == NULL)) + return (1); + if (bd->note != NULL && strcmp(bd->note, default_bd->note) != 0) + return (1); + + cmd = cmd_list_print(bd->cmdlist, 0); + default_cmd = cmd_list_print(default_bd->cmdlist, 0); + changed = (strcmp(cmd, default_cmd) != 0); + free(cmd); + free(default_cmd); + return (changed); +} + +static u_int window_customize_build_array(struct window_customize_modedata *data, struct mode_tree_item *top, enum window_customize_scope scope, struct options_entry *o, struct format_tree *ft) @@ -284,10 +598,16 @@ window_customize_build_array(struct window_customize_modedata *data, char *name, *value, *text; uint64_t tag; const char *array_key; + u_int count = 0; ai = options_array_first(o); while (ai != NULL) { array_key = options_array_item_key(ai); + if (data->hide_default && + !window_customize_option_is_changed(o, array_key)) { + ai = options_array_next(ai); + continue; + } xasprintf(&name, "%s[%s]", options_name(o), array_key); format_add(ft, "option_name", "%s", name); @@ -296,6 +616,8 @@ window_customize_build_array(struct window_customize_modedata *data, item = window_customize_add_item(data); item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; + if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)) + item->option_type = WINDOW_CUSTOMIZE_HOOKS; item->scope = scope; item->oo = oo; item->name = xstrdup(options_name(o)); @@ -308,12 +630,14 @@ window_customize_build_array(struct window_customize_modedata *data, free(name); free(value); + count++; ai = options_array_next(ai); } + return (count); } -static void +static u_int window_customize_build_option(struct window_customize_modedata *data, struct mode_tree_item *top, enum window_customize_scope scope, struct options_entry *o, struct format_tree *ft, @@ -327,20 +651,23 @@ window_customize_build_option(struct window_customize_modedata *data, char *text, *expanded, *value; int global = 0, array = 0; int is_hook = 0, is_monitor = 0; + int is_user_hook = 0; uint64_t tag; if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)) is_hook = 1; if (options_get_monitor_data(o) != NULL) is_monitor = 1; + if (*name == '@' && hooks_is_event(name)) + is_user_hook = 1; switch (type) { case WINDOW_CUSTOMIZE_OPTIONS: - if (is_hook || is_monitor) - return; + if (is_hook || is_monitor || is_user_hook) + return (0); break; case WINDOW_CUSTOMIZE_HOOKS: - if (!is_hook && !is_monitor) - return; + if (!is_hook && !is_monitor && !is_user_hook) + return (0); break; } if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) @@ -351,7 +678,9 @@ window_customize_build_option(struct window_customize_modedata *data, scope == WINDOW_CUSTOMIZE_GLOBAL_WINDOW) global = 1; if (data->hide_global && global) - return; + return (0); + if (data->hide_default && !window_customize_option_is_changed(o, NULL)) + return (0); format_add(ft, "option_name", "%s", name); format_add(ft, "option_is_global", "%d", global); @@ -388,12 +717,13 @@ window_customize_build_option(struct window_customize_modedata *data, expanded = format_expand(ft, filter); if (!format_true(expanded)) { free(expanded); - return; + return (0); } free(expanded); } item = window_customize_add_item(data); item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; + item->option_type = type; item->oo = oo; item->scope = scope; item->name = xstrdup(name); @@ -406,8 +736,9 @@ window_customize_build_option(struct window_customize_modedata *data, top = mode_tree_add(data->data, top, item, tag, name, text, 0); free(text); - if (array) - window_customize_build_array(data, top, scope, o, ft); + if (!array) + return (1); + return (1 + window_customize_build_array(data, top, scope, o, ft)); } static void @@ -452,7 +783,7 @@ window_customize_build_options(struct window_customize_modedata *data, struct mode_tree_item *top; struct options_entry *o = NULL, *loop; const char **list = NULL, *name; - u_int size = 0, i; + u_int size = 0, i, count = 0; enum window_customize_scope scope; top = mode_tree_add(data->data, NULL, NULL, tag, title, NULL, 0); @@ -484,8 +815,8 @@ window_customize_build_options(struct window_customize_modedata *data, scope = scope1; else scope = scope0; - window_customize_build_option(data, top, scope, o, ft, filter, - fs, type); + count += window_customize_build_option(data, top, scope, o, ft, + filter, fs, type); } free(list); @@ -508,28 +839,35 @@ window_customize_build_options(struct window_customize_modedata *data, scope = scope1; else scope = scope0; - window_customize_build_option(data, top, scope, o, ft, filter, - fs, type); + count += window_customize_build_option(data, top, scope, o, ft, + filter, fs, type); loop = options_next(loop); } + if (data->hide_default && count == 0) + mode_tree_remove(data->data, top); +} + +static uint64_t +window_customize_key_tag(const void *ptr, u_int type) +{ + return ((uint64_t)(uintptr_t)ptr|type); } static void window_customize_build_keys(struct window_customize_modedata *data, struct key_table *kt, struct format_tree *ft, const char *filter, - struct cmd_find_state *fs, u_int number) + struct cmd_find_state *fs) { struct mode_tree_item *top, *child, *mti; struct window_customize_itemdata *item; struct key_binding *bd; char *title, *text, *tmp, *expanded; const char *flag; - uint64_t tag; - - tag = (1ULL << 62)|((uint64_t)number << 54)|1; + u_int count = 0; xasprintf(&title, "Key Table - %s", kt->name); - top = mode_tree_add(data->data, NULL, NULL, tag, title, NULL, 0); + top = mode_tree_add(data->data, NULL, NULL, + window_customize_key_tag(kt, 0), title, NULL, 0); mode_tree_no_tag(top); free(title); @@ -540,6 +878,12 @@ window_customize_build_keys(struct window_customize_modedata *data, bd = key_bindings_first(kt); while (bd != NULL) { + if (data->hide_default && + !window_customize_key_is_changed(kt, bd)) { + bd = key_bindings_next(kt, bd); + continue; + } + format_add(ft, "key", "%s", key_string_lookup_key(bd->key, 0)); if (bd->note != NULL) format_add(ft, "key_note", "%s", bd->note); @@ -561,25 +905,26 @@ window_customize_build_keys(struct window_customize_modedata *data, item->name = xstrdup(key_string_lookup_key(item->key, 0)); expanded = format_expand(ft, data->format); - child = mode_tree_add(data->data, top, item, (uint64_t)bd, - expanded, NULL, 0); + child = mode_tree_add(data->data, top, item, + window_customize_key_tag(bd, 0), expanded, NULL, 0); free(expanded); tmp = cmd_list_print(bd->cmdlist, 0); - xasprintf(&text, "#[ignore]%s", tmp); + xasprintf(&text, "#[fg=themelightgrey]#[ignore]%s", tmp); free(tmp); mti = mode_tree_add(data->data, child, item, - tag|(bd->key << 3)|(0 << 1)|1, "Command", text, -1); + window_customize_key_tag(bd, 1), "Command", text, -1); mode_tree_draw_as_parent(mti); mode_tree_no_tag(mti); free(text); - if (bd->note != NULL) - xasprintf(&text, "#[ignore]%s", bd->note); - else + if (bd->note != NULL) { + xasprintf(&text, "#[fg=themelightgrey]#[ignore]%s", + bd->note); + } else text = xstrdup(""); mti = mode_tree_add(data->data, child, item, - tag|(bd->key << 3)|(1 << 1)|1, "Note", text, -1); + window_customize_key_tag(bd, 2), "Note", text, -1); mode_tree_draw_as_parent(mti); mode_tree_no_tag(mti); free(text); @@ -588,15 +933,20 @@ window_customize_build_keys(struct window_customize_modedata *data, flag = "on"; else flag = "off"; + xasprintf(&text, "#[fg=themelightgrey]#[ignore]%s", flag); mti = mode_tree_add(data->data, child, item, - tag|(bd->key << 3)|(2 << 1)|1, "Repeat", flag, -1); + window_customize_key_tag(bd, 3), "Repeat", text, -1); mode_tree_draw_as_parent(mti); mode_tree_no_tag(mti); + free(text); + count++; bd = key_bindings_next(kt, bd); } format_free(ft); + if (data->hide_default && count == 0) + mode_tree_remove(data->data, top); } static void @@ -612,6 +962,9 @@ window_customize_build_environment(struct window_customize_modedata *data, uint64_t item_tag; int global; + if (data->hide_default) + return; + top = mode_tree_add(data->data, NULL, NULL, tag, title, NULL, 0); mode_tree_no_tag(top); @@ -743,15 +1096,10 @@ window_customize_build(void *modedata, ft = format_create_from_state(NULL, NULL, &fs); format_add(ft, "is_environment", "0"); - i = 0; kt = key_bindings_first_table(); while (kt != NULL) { - if (!RB_EMPTY(&kt->key_bindings)) { - window_customize_build_keys(data, kt, ft, filter, &fs, - i); - if (++i == 256) - break; - } + if (!RB_EMPTY(&kt->key_bindings)) + window_customize_build_keys(data, kt, ft, filter, &fs); kt = key_bindings_next_table(kt); } @@ -788,17 +1136,16 @@ window_customize_draw_key(__unused struct window_customize_modedata *data, if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, "This key is in the %s table.", kt->name)) return; - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "This key %s repeat.", - (bd->flags & KEY_BINDING_REPEAT) ? "does" : "does not")) + if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), 0, + "Repeat: ", "%s", (bd->flags & KEY_BINDING_REPEAT) ? "on" : "off")) return; screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */ if (s->cy >= cy + sy - 1) return; cmd = cmd_list_print(bd->cmdlist, 0); - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "Command: %s", cmd)) { + if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), 0, + "Command: ", "%s", cmd)) { free(cmd); return; } @@ -806,8 +1153,8 @@ window_customize_draw_key(__unused struct window_customize_modedata *data, if (default_bd != NULL) { default_cmd = cmd_list_print(default_bd->cmdlist, 0); if (strcmp(cmd, default_cmd) != 0 && - !screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "The default is: %s", default_cmd)) { + !window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), + 0, "The default is: ", "%s", default_cmd)) { free(default_cmd); free(cmd); return; @@ -832,12 +1179,12 @@ window_customize_draw_option(struct window_customize_modedata *data, const char *array_key; const char *space = "", *unit = ""; char *value = NULL, *expanded; - char *monitor; + char *monitor, label[64]; char *default_value = NULL; char choices[256] = ""; struct cmd_find_state fs; struct format_tree *ft; - int is_hook, is_monitor; + int is_hook, is_monitor, is_user_hook; if (!window_customize_check_item(data, item, &fs)) return; @@ -850,6 +1197,7 @@ window_customize_draw_option(struct window_customize_modedata *data, oe = options_table_entry(o); is_hook = (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)); is_monitor = (options_get_monitor_data(o) != NULL); + is_user_hook = (*name == '@' && hooks_is_event(name)); if (oe != NULL && oe->unit != NULL) { space = " "; @@ -858,9 +1206,12 @@ window_customize_draw_option(struct window_customize_modedata *data, ft = format_create_from_state(NULL, NULL, &fs); if (oe == NULL || oe->text == NULL) { - text = is_monitor ? - "This hook runs when a monitor changes." : - "This option doesn't have a description."; + if (is_monitor) + text = "This hook runs when a monitor changes."; + else if (is_user_hook) + text = "This hook doesn't have a description."; + else + text = "This option doesn't have a description."; } else text = oe->text; if (!screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, "%s", @@ -887,7 +1238,11 @@ window_customize_draw_option(struct window_customize_modedata *data, text = "session"; else text = "server"; - if (is_hook) { + if (is_user_hook) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "This is a user hook.")) + goto out; + } else if (is_hook) { if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, "This is a %s hook.", text)) goto out; @@ -900,8 +1255,8 @@ window_customize_draw_option(struct window_customize_modedata *data, monitor: monitor = hooks_monitor_to_string(o); if (monitor != NULL) { - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "Monitor: %s", monitor)) { + if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), + 0, "Monitor: ", "%s", monitor)) { free(monitor); goto out; } @@ -936,22 +1291,20 @@ monitor: default_value = NULL; } } - if (is_hook || is_monitor) { - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "Hook command: %s%s%s", value, space, - unit)) + if (is_hook || is_monitor || is_user_hook) { + if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), + 0, "Hook command: ", "%s%s%s", value, space, unit)) goto out; } else { - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "Option value: %s%s%s", value, space, - unit)) + if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), + 0, "Option value: ", "%s%s%s", value, space, unit)) goto out; } if (oe == NULL || oe->type == OPTIONS_TABLE_STRING) { expanded = format_expand(ft, value); if (strcmp(expanded, value) != 0) { - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), - 0, &grid_default_cell, "This expands to: %s", + if (!window_customize_write_value(ctx, cx, sx, + sy - (s->cy - cy), 0, "This expands to: ", "%s", expanded)) { free(expanded); goto out; @@ -965,8 +1318,8 @@ monitor: strlcat(choices, ", ", sizeof choices); } choices[strlen(choices) - 2] = '\0'; - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "Available values are: %s", + if (!window_customize_write_value(ctx, cx, sx, + sy - (s->cy - cy), 0, "Available values are: ", "%s", choices)) goto out; } @@ -999,9 +1352,8 @@ monitor: goto out; } if (default_value != NULL) { - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "The default is: %s%s%s", default_value, - space, unit)) + if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), + 0, "The default is: ", "%s%s%s", default_value, space, unit)) goto out; } @@ -1032,10 +1384,11 @@ monitor: parent = options_get_only(wo, name); if (parent != NULL) { 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, - value, space, unit)) + xsnprintf(label, sizeof label, + "Window value (from window %u): ", fs.wl->idx); + if (!window_customize_write_value(ctx, s->cx, sx, + sy - (s->cy - cy), 0, label, "%s%s%s", value, space, + unit)) goto out; } } @@ -1043,9 +1396,9 @@ monitor: parent = options_get_only(go, name); if (parent != NULL) { 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)) + if (!window_customize_write_value(ctx, s->cx, sx, + sy - (s->cy - cy), 0, "Global value: ", "%s%s%s", + value, space, unit)) goto out; } } @@ -1095,8 +1448,8 @@ window_customize_draw_environment(struct window_customize_modedata *data, &grid_default_cell, "Variable is removed.")) return; } else { - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "Variable value: %s", envent->value)) + if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), + 0, "Variable value: ", "%s", envent->value)) return; } @@ -1111,8 +1464,8 @@ window_customize_draw_environment(struct window_customize_modedata *data, &grid_default_cell, "Global variable is removed.")) return; } else { - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "Global value: %s", parent->value)) + if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), + 0, "Global value: ", "%s", parent->value)) return; } } @@ -1172,8 +1525,12 @@ static const char* window_customize_help_lines[] = { "#[fg=themelightgrey]" " a #[#{E:tree-mode-border-style},acs]x#[default] Change array key", "#[fg=themelightgrey]" + " e #[#{E:tree-mode-border-style},acs]x#[default] Open %1 value in editor", + "#[fg=themelightgrey]" " f #[#{E:tree-mode-border-style},acs]x#[default] Enter a filter", "#[fg=themelightgrey]" + " C #[#{E:tree-mode-border-style},acs]x#[default] Toggle only changed items", + "#[fg=themelightgrey]" " v #[#{E:tree-mode-border-style},acs]x#[default] Toggle information", NULL }; @@ -1245,6 +1602,10 @@ window_customize_free(struct window_mode_entry *wme) return; data->dead = 1; + if (data->editor != NULL) { + spawn_cancel_editor(data->editor); + window_customize_finish_edit(data->edit); + } mode_tree_free(data->data); window_customize_destroy(data); } @@ -1257,6 +1618,14 @@ window_customize_resize(struct window_mode_entry *wme, u_int sx, u_int sy) mode_tree_resize(data->data, sx, sy); } +static void +window_customize_update(struct window_mode_entry *wme) +{ + struct window_customize_modedata *data = wme->data; + + window_customize_draw_waiting(data); +} + static void window_customize_free_callback(void *modedata) { @@ -1312,6 +1681,8 @@ window_customize_set_option_callback(struct client *c, void *itemdata, if (options_from_string(oo, oe, name, s, 0, &cause) != 0) goto fail; } + if (item->option_type == WINDOW_CUSTOMIZE_HOOKS && *name == '@') + hooks_add_event(name); options_push_changes(item->name); mode_tree_build(data->data); @@ -1406,6 +1777,296 @@ window_customize_set_environment(struct client *c, free(prompt); } +static enum prompt_result +window_customize_add_option_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 = item->data; + char *copy, *name, *array_key; + const char *value; + const char *what; + int ambiguous; + size_t namelen; + + if (s == NULL || *s == '\0' || data->dead) + return (PROMPT_CLOSE); + if (item == NULL || !window_customize_check_item(data, item, NULL)) + return (PROMPT_CLOSE); + + namelen = strcspn(s, " \t"); + if (namelen == 0 || s[namelen] == '\0') { + status_message_set(c, -1, 1, 0, 0, + "User option must be @name value"); + return (PROMPT_CLOSE); + } + + value = s + namelen; + while (*value == ' ' || *value == '\t') + value++; + if (*value == '\0') { + status_message_set(c, -1, 1, 0, 0, + "User option must be @name value"); + return (PROMPT_CLOSE); + } + + copy = xstrndup(s, namelen); + name = options_match(copy, &array_key, &ambiguous); + free(copy); + if (name == NULL || *name != '@' || array_key != NULL) { + what = (item->option_type == WINDOW_CUSTOMIZE_HOOKS) ? + "hook" : "option"; + status_message_set(c, -1, 1, 0, 0, + "User %s name must start with @", what); + free(name); + free(array_key); + return (PROMPT_CLOSE); + } + + options_set_string(item->oo, name, 0, "%s", value); + if (item->option_type == WINDOW_CUSTOMIZE_HOOKS) + hooks_add_event(name); + options_push_changes(name); + + mode_tree_build(data->data); + mode_tree_draw(data->data); + data->wp->flags |= PANE_REDRAW; + + free(name); + free(array_key); + return (PROMPT_CLOSE); +} + +static void +window_customize_add_option(struct client *c, + struct window_customize_modedata *data, enum window_customize_scope scope, + struct options *oo, enum window_customize_option_type type) +{ + struct window_customize_itemdata *new_item; + char *prompt; + const char *what; + + what = (type == WINDOW_CUSTOMIZE_HOOKS) ? "hook" : "option"; + xasprintf(&prompt, "New user %s: ", what); + + new_item = xcalloc(1, sizeof *new_item); + new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; + new_item->option_type = type; + new_item->scope = scope; + new_item->oo = oo; + + data->references++; + mode_tree_set_prompt(data->data, c, prompt, "@", PROMPT_TYPE_COMMAND, + PROMPT_NOFORMAT, window_customize_add_option_callback, + window_customize_free_item_callback, new_item); + + free(prompt); +} + +static enum prompt_result +window_customize_add_environment_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 = item->data; + char *name; + const char *value; + + if (s == NULL || *s == '\0' || data->dead) + return (PROMPT_CLOSE); + if (item == NULL || !window_customize_check_item(data, item, NULL)) + return (PROMPT_CLOSE); + + if (*s == '-') { + if (s[1] == '\0' || strchr(s + 1, '=') != NULL) { + status_message_set(c, -1, 1, 0, 0, + "Bad environment variable: %s", s); + return (PROMPT_CLOSE); + } + environ_clear(item->environ, s + 1); + } else { + value = strchr(s, '='); + if (value == NULL || value == s) { + status_message_set(c, -1, 1, 0, 0, + "Environment variable must be NAME=value"); + return (PROMPT_CLOSE); + } + + name = xstrdup(s); + name[strcspn(name, "=")] = '\0'; + environ_set(item->environ, name, 0, "%s", value + 1); + free(name); + } + + mode_tree_build(data->data); + mode_tree_draw(data->data); + data->wp->flags |= PANE_REDRAW; + + return (PROMPT_CLOSE); +} + +static void +window_customize_add_environment(struct client *c, + struct window_customize_modedata *data, enum window_customize_scope scope, + struct environ *env) +{ + struct window_customize_itemdata *new_item; + + new_item = xcalloc(1, sizeof *new_item); + new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT; + new_item->scope = scope; + new_item->environ = env; + + data->references++; + mode_tree_set_prompt(data->data, c, "New environment: ", "", + PROMPT_TYPE_COMMAND, PROMPT_NOFORMAT, + window_customize_add_environment_callback, + window_customize_free_item_callback, new_item); +} + +static void +window_customize_edit_close_cb(char *buf, size_t len, void *arg) +{ + struct window_customize_editdata *ed = arg; + struct window_customize_itemdata *item = ed->item; + struct window_pane *wp; + struct window_mode_entry *wme; + struct window_customize_modedata *data = NULL; + char *value, *cause = NULL; + + wp = window_pane_find_by_id(ed->wp_id); + if (wp != NULL) { + wme = TAILQ_FIRST(&wp->modes); + if (wme != NULL && wme->mode == &window_customize_mode) { + data = wme->data; + if (data->editor == ed->editor) { + data->editor = NULL; + data->edit = NULL; + } + } + } + + if (buf == NULL || len == 0 || data == NULL || data->dead) { + free(buf); + window_customize_finish_edit(ed); + return; + } + if (buf[len - 1] == '\n') + len--; + value = xmalloc(len + 1); + memcpy(value, buf, len); + value[len] = '\0'; + free(buf); + + switch (ed->edit_type) { + case WINDOW_CUSTOMIZE_EDIT_OPTION: + if (window_customize_option_editable(data, item) && + window_customize_set_option_value(item, value, &cause) != 0) { + free(cause); + goto out; + } + break; + case WINDOW_CUSTOMIZE_EDIT_KEY_COMMAND: + if (window_customize_set_command_value(item, value, &cause) != 0) { + free(cause); + goto out; + } + break; + case WINDOW_CUSTOMIZE_EDIT_KEY_NOTE: + if (window_customize_set_note_value(item, value) != 0) + goto out; + break; + case WINDOW_CUSTOMIZE_EDIT_ENVIRONMENT: + if (!window_customize_check_item(data, item, NULL)) + goto out; + window_customize_set_environment_value(item, value); + break; + } + + mode_tree_build(data->data); + mode_tree_draw(data->data); + wp->flags |= PANE_REDRAW; + +out: + free(value); + window_customize_finish_edit(ed); +} + +static void +window_customize_start_edit(struct window_customize_modedata *data, + struct window_customize_itemdata *item, struct client *c) +{ + struct window_customize_editdata *ed; + struct options_entry *o; + struct environ_entry *envent; + struct key_binding *bd; + const char *name; + const char *buf; + char *value; + size_t len; + enum window_customize_edit_type edit_type; + + if (data->editor != NULL || item == NULL) + return; + + if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) { + if (!window_customize_option_editable(data, item)) + return; + o = options_get(item->oo, item->name); + if (o == NULL) + return; + value = options_to_string(o, item->array_key, 0); + edit_type = WINDOW_CUSTOMIZE_EDIT_OPTION; + } else if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) { + name = mode_tree_get_current_name(data->data); + if (!window_customize_get_key(item, NULL, &bd)) + return; + if (strcmp(name, "Command") == 0) { + value = cmd_list_print(bd->cmdlist, 0); + edit_type = WINDOW_CUSTOMIZE_EDIT_KEY_COMMAND; + } else if (strcmp(name, "Note") == 0) { + if (bd->note == NULL) + value = xstrdup(""); + else + value = xstrdup(bd->note); + edit_type = WINDOW_CUSTOMIZE_EDIT_KEY_NOTE; + } else + return; + } else if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) { + if (!window_customize_check_item(data, item, NULL)) + return; + envent = environ_find(item->environ, item->name); + if (envent == NULL || envent->value == NULL) + return; + value = xstrdup(envent->value); + edit_type = WINDOW_CUSTOMIZE_EDIT_ENVIRONMENT; + } else + return; + + ed = xcalloc(1, sizeof *ed); + ed->wp_id = data->wp->id; + ed->edit_type = edit_type; + ed->item = window_customize_copy_item(item); + + buf = value; + len = strlen(value); + if (len == 0) { + buf = "\n"; + len = 1; + } + ed->editor = spawn_editor(c, buf, len, window_customize_edit_close_cb, + ed); + free(value); + if (ed->editor == NULL) + window_customize_finish_edit(ed); + else { + data->editor = ed->editor; + data->edit = ed; + } +} + static void window_customize_set_option(struct client *c, struct window_customize_modedata *data, @@ -1524,6 +2185,7 @@ window_customize_set_option(struct client *c, new_item = xcalloc(1, sizeof *new_item); new_item->data = data; new_item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; + new_item->option_type = item->option_type; new_item->scope = scope; new_item->oo = oo; new_item->name = xstrdup(name); @@ -1606,6 +2268,7 @@ window_customize_set_array_key(struct client *c, new_item = xcalloc(1, sizeof *new_item); new_item->data = data; new_item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; + new_item->option_type = item->option_type; new_item->scope = item->scope; new_item->oo = item->oo; new_item->name = xstrdup(item->name); @@ -1665,7 +2328,7 @@ window_customize_reset_option(struct window_customize_modedata *data, oo = item->oo; while (oo != NULL) { - o = options_get_only(item->oo, item->name); + o = options_get_only(oo, item->name); if (o != NULL) options_remove_or_default(o, NULL, NULL); oo = options_get_parent(oo); @@ -1789,6 +2452,92 @@ window_customize_set_key(struct client *c, } } +static enum prompt_result +window_customize_add_key_callback(struct client *c, void *itemdata, + const char *s, __unused enum prompt_key_result key0) +{ + struct window_customize_itemdata *item = itemdata; + struct window_customize_modedata *data = item->data; + key_code key; + struct cmd_parse_result *pr; + const char *command; + char *error, *keystr; + size_t keylen; + + if (s == NULL || *s == '\0' || data->dead) + return (PROMPT_CLOSE); + + keylen = strcspn(s, " \t"); + if (keylen == 0 || s[keylen] == '\0') { + status_message_set(c, -1, 1, 0, 0, + "Key binding must be key command"); + return (PROMPT_CLOSE); + } + + command = s + keylen; + while (*command == ' ' || *command == '\t') + command++; + if (*command == '\0') { + status_message_set(c, -1, 1, 0, 0, + "Key binding must be key command"); + return (PROMPT_CLOSE); + } + + keystr = xstrndup(s, keylen); + key = key_string_lookup_string(keystr); + if (key == KEYC_NONE || key == KEYC_UNKNOWN) { + status_message_set(c, -1, 1, 0, 0, "Unknown key: %s", keystr); + free(keystr); + return (PROMPT_CLOSE); + } + free(keystr); + + pr = cmd_parse_from_string(command, NULL); + switch (pr->status) { + case CMD_PARSE_ERROR: + error = pr->error; + goto fail; + case CMD_PARSE_SUCCESS: + break; + } + key_bindings_add(item->table, key, NULL, 0, pr->cmdlist); + + mode_tree_build(data->data); + mode_tree_draw(data->data); + data->wp->flags |= PANE_REDRAW; + + return (PROMPT_CLOSE); + +fail: + *error = toupper((u_char)*error); + status_message_set(c, -1, 1, 0, 0, "%s", error); + free(error); + return (PROMPT_CLOSE); +} + +static void +window_customize_add_key(struct client *c, + struct window_customize_modedata *data, const char *table) +{ + struct window_customize_itemdata *new_item; + char *prompt; + + xasprintf(&prompt, "New key in %s: ", table); + + new_item = xcalloc(1, sizeof *new_item); + new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_KEY; + new_item->scope = WINDOW_CUSTOMIZE_KEY; + new_item->table = xstrdup(table); + + data->references++; + mode_tree_set_prompt(data->data, c, prompt, "", PROMPT_TYPE_COMMAND, + PROMPT_NOFORMAT, window_customize_add_key_callback, + window_customize_free_item_callback, new_item); + + free(prompt); +} + static void window_customize_unset_key(struct window_customize_modedata *data, struct window_customize_itemdata *item) @@ -1799,10 +2548,8 @@ window_customize_unset_key(struct window_customize_modedata *data, if (item == NULL || !window_customize_get_key(item, &kt, &bd)) return; - if (item == mode_tree_get_current(data->data)) { - mode_tree_collapse_current(data->data); + if (item == mode_tree_get_current(data->data)) mode_tree_up(data->data, 0); - } key_bindings_remove(kt->name, bd->key); } @@ -1819,10 +2566,8 @@ window_customize_reset_key(struct window_customize_modedata *data, dd = key_bindings_get_default(kt, bd->key); if (dd != NULL && bd->cmdlist == dd->cmdlist) return; - if (dd == NULL && item == mode_tree_get_current(data->data)) { - mode_tree_collapse_current(data->data); + if (dd == NULL && item == mode_tree_get_current(data->data)) mode_tree_up(data->data, 0); - } key_bindings_reset(kt->name, bd->key); } @@ -1832,25 +2577,30 @@ window_customize_change_each(void *modedata, void *itemdata, { struct window_customize_modedata *data = modedata; struct window_customize_itemdata *item = itemdata; + enum window_customize_item_type type = item->type; + char *name = NULL; + if (type == WINDOW_CUSTOMIZE_ITEM_OPTION) + name = xstrdup(item->name); switch (data->change) { case WINDOW_CUSTOMIZE_UNSET: - if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) + if (type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_unset_key(data, item); - else if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) + else if (type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) window_customize_unset_environment(data, item); else window_customize_unset_option(data, item); break; case WINDOW_CUSTOMIZE_RESET: - if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) + if (type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_reset_key(data, item); - else if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) + else if (type == WINDOW_CUSTOMIZE_ITEM_OPTION) window_customize_reset_option(data, item); break; } - if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) - options_push_changes(item->name); + if (type == WINDOW_CUSTOMIZE_ITEM_OPTION) + options_push_changes(name); + free(name); } static enum prompt_result @@ -1859,6 +2609,8 @@ window_customize_change_current_callback(__unused struct client *c, { struct window_customize_modedata *data = modedata; struct window_customize_itemdata *item; + enum window_customize_item_type type; + char *name = NULL; if (s == NULL || *s == '\0' || data->dead) return (PROMPT_CLOSE); @@ -1866,24 +2618,30 @@ window_customize_change_current_callback(__unused struct client *c, return (PROMPT_CLOSE); item = mode_tree_get_current(data->data); + if (item == NULL) + return (PROMPT_CLOSE); + type = item->type; + if (type == WINDOW_CUSTOMIZE_ITEM_OPTION) + name = xstrdup(item->name); switch (data->change) { case WINDOW_CUSTOMIZE_UNSET: - if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) + if (type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_unset_key(data, item); - else if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) + else if (type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) window_customize_unset_environment(data, item); else window_customize_unset_option(data, item); break; case WINDOW_CUSTOMIZE_RESET: - if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) + if (type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_reset_key(data, item); - else if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) + else if (type == WINDOW_CUSTOMIZE_ITEM_OPTION) window_customize_reset_option(data, item); break; } - if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) - options_push_changes(item->name); + if (type == WINDOW_CUSTOMIZE_ITEM_OPTION) + options_push_changes(name); + free(name); mode_tree_build(data->data); mode_tree_draw(data->data); data->wp->flags |= PANE_REDRAW; @@ -1911,6 +2669,63 @@ window_customize_change_tagged_callback(struct client *c, void *modedata, return (PROMPT_CLOSE); } +static int +window_customize_add_current(struct client *c, + struct window_customize_modedata *data) +{ + struct cmd_find_state fs; + const char *name, *table; + + name = mode_tree_get_current_name(data->data); + if (cmd_find_valid_state(&data->fs)) + cmd_find_copy_state(&fs, &data->fs); + else + cmd_find_from_pane(&fs, data->wp, 0); + + if (strcmp(name, "Server Options") == 0) { + window_customize_add_option(c, data, WINDOW_CUSTOMIZE_SERVER, + global_options, WINDOW_CUSTOMIZE_OPTIONS); + return (1); + } + if (strcmp(name, "Session Options") == 0) { + window_customize_add_option(c, data, WINDOW_CUSTOMIZE_SESSION, + fs.s->options, WINDOW_CUSTOMIZE_OPTIONS); + return (1); + } + if (strcmp(name, "Window & Pane Options") == 0) { + window_customize_add_option(c, data, WINDOW_CUSTOMIZE_PANE, + fs.wp->options, WINDOW_CUSTOMIZE_OPTIONS); + return (1); + } + if (strcmp(name, "Session Hooks") == 0) { + window_customize_add_option(c, data, WINDOW_CUSTOMIZE_SESSION, + fs.s->options, WINDOW_CUSTOMIZE_HOOKS); + return (1); + } + if (strcmp(name, "Window & Pane Hooks") == 0) { + window_customize_add_option(c, data, WINDOW_CUSTOMIZE_PANE, + fs.wp->options, WINDOW_CUSTOMIZE_HOOKS); + return (1); + } + if (strcmp(name, "Global Environment") == 0) { + window_customize_add_environment(c, data, + WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT, global_environ); + return (1); + } + if (strcmp(name, "Session Environment") == 0) { + window_customize_add_environment(c, data, + WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT, fs.s->environ); + return (1); + } + + if (strncmp(name, "Key Table - ", 12) == 0) { + table = name + 12; + window_customize_add_key(c, data, table); + return (1); + } + return (0); +} + static void window_customize_key(struct window_mode_entry *wme, struct client *c, __unused struct session *s, __unused struct winlink *wl, key_code key, @@ -1924,11 +2739,22 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, u_int tagged; item = mode_tree_get_current(data->data); + if (data->editor != NULL) { + if (key == 'q' || key == '\033' || key == '\003') + finished = 1; + else + finished = 0; + goto out; + } + finished = mode_tree_key(data->data, c, &key, m, NULL, NULL); if (item != (new_item = mode_tree_get_current(data->data))) item = new_item; switch (key) { + case 'e': + window_customize_start_edit(data, item, c); + break; case 'a': if (item == NULL || item->type != WINDOW_CUSTOMIZE_ITEM_OPTION) break; @@ -1936,8 +2762,11 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, break; case '\r': case 's': - if (item == NULL) + if (item == NULL) { + if (window_customize_add_current(c, data)) + mode_tree_build(data->data); break; + } if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_set_key(c, data, item); else if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) @@ -1969,8 +2798,9 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, break; case 'd': if (item == NULL || - item->type != WINDOW_CUSTOMIZE_ITEM_OPTION || - item->array_key != NULL) + (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION && + item->array_key != NULL) || + item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) break; xasprintf(&prompt, "Reset %s to default? ", item->name); data->references++; @@ -2031,11 +2861,18 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, data->hide_global = !data->hide_global; mode_tree_build(data->data); break; + case 'C': + data->hide_default = !data->hide_default; + mode_tree_build(data->data); + break; } + +out: if (finished) window_pane_reset_mode(wp); else { mode_tree_draw(data->data); + window_customize_draw_waiting(data); wp->flags |= PANE_REDRAW; } } From cf110d9d1de2046372e18d5eac8447bf7fbb251c Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 14 Jul 2026 10:07:51 +0100 Subject: [PATCH 27/36] Copy mode redraw test. --- regress/copy-mode-redraw.sh | 125 ++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 regress/copy-mode-redraw.sh diff --git a/regress/copy-mode-redraw.sh b/regress/copy-mode-redraw.sh new file mode 100644 index 000000000..541f38afb --- /dev/null +++ b/regress/copy-mode-redraw.sh @@ -0,0 +1,125 @@ +#!/bin/sh + +# Exercise copy mode redraw through a real client. An inner tmux is attached +# inside an outer tmux pane; the outer pane is captured to inspect what the +# inner client actually drew. + +PATH=/bin:/usr/bin +TERM=screen +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" + +fail() { + echo "$*" >&2 + exit 1 +} + +capture() { + $TMUX capturep -pS0 -E- >$TMP || exit 1 +} + +check_line() { + line=$1 + want=$2 + got=$(sed -n "$line"p $TMP) + [ "$got" = "$want" ] || fail "line $line: expected '$want', got '$got'" +} + +check_grep() { + pattern=$1 + grep -Fq "$pattern" $TMP || fail "missing pattern: $pattern" +} + +check_no_grep() { + pattern=$1 + grep -Fq "$pattern" $TMP && fail "unexpected pattern: $pattern" +} + +redraw() { + $TMUX2 send -X cursor-right || exit 1 + sleep 1 + capture +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +TMP=$(mktemp) +trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" 0 1 15 + +$TMUX2 -f/dev/null new -d -x48 -y8 -s test \ + "awk 'BEGIN { for (i = 0; i < 90; i++) { if (i % 2 == 0) printf \"L%03d-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-END\\n\", i; else printf \"S%03d\\n\", i } }'; exec sleep 100" || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 set -g mode-keys vi || exit 1 +$TMUX2 set -g copy-mode-line-numbers off || exit 1 +$TMUX2 set -g copy-mode-position-format "#[align=right][23/100-LONGTAIL]" || exit 1 + +$TMUX -f/dev/null new -d -x48 -y8 || exit 1 +$TMUX set -g status off || exit 1 +$TMUX send -l "$TMUX2 attach" || exit 1 +$TMUX send Enter || exit 1 +sleep 1 + +CLIENT=$($TMUX2 list-clients -F '#{client_name}' | head -1) +[ -n "$CLIENT" ] || fail "no inner client" + +$TMUX2 copy-mode || exit 1 +$TMUX2 send -X history-top || exit 1 +sleep 1 + +# Shrinking and moving the position indicator should not leave stale text from +# the previous indicator, for different widths and alignments. +for size in 20 48; do + $TMUX resizew -x$size -y8 || exit 1 + $TMUX2 refresh -t"$CLIENT" -c || exit 1 + sleep 1 + for align in left centre right; do + $TMUX2 set -g copy-mode-position-format "#[align=$align][23/100-LONGTAIL]" || exit 1 + redraw + check_grep "[23/100-LONGTAIL]" + + $TMUX2 set -g copy-mode-position-format "#[align=$align][1/100]" || exit 1 + redraw + check_grep "[1/100]" + check_no_grep "LONGTAIL" + check_no_grep "[1/100]]" + done + + $TMUX2 set -g copy-mode-position-format "#[align=right][1/100]" || exit 1 + redraw + $TMUX2 set -g copy-mode-position-format "#[align=right][23/100]" || exit 1 + redraw + check_grep "[23/100]" +done + +# Scrolling from long pane content to short pane content should clear the +# remainder of the old line without relying on a full pre-clear. +$TMUX resizew -x48 -y8 || exit 1 +$TMUX2 refresh -t"$CLIENT" -c || exit 1 +$TMUX2 send -X cancel || exit 1 +$TMUX2 copy-mode -H || exit 1 +$TMUX2 send -X history-top || exit 1 +sleep 1 +capture +check_line 1 "L000-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-END" + +$TMUX2 send -X scroll-down || exit 1 +sleep 1 +capture +check_line 1 "S001" + +$TMUX2 send -X scroll-down || exit 1 +sleep 1 +capture +check_line 1 "L002-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-END" + +$TMUX2 send -X scroll-down || exit 1 +sleep 1 +capture +check_line 1 "S003" + +exit 0 From c39d69f961d7bac9482be956a702aa3a6f33052f Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 14 Jul 2026 12:05:06 +0100 Subject: [PATCH 28/36] More copy mode tests. --- regress/copy-mode-redraw.sh | 46 ++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/regress/copy-mode-redraw.sh b/regress/copy-mode-redraw.sh index 541f38afb..af7fcc34f 100644 --- a/regress/copy-mode-redraw.sh +++ b/regress/copy-mode-redraw.sh @@ -49,7 +49,9 @@ $TMUX kill-server 2>/dev/null $TMUX2 kill-server 2>/dev/null TMP=$(mktemp) -trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" 0 1 15 +BEFORE=$(mktemp) +AFTER=$(mktemp) +trap "rm -f $TMP $BEFORE $AFTER; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" 0 1 15 $TMUX2 -f/dev/null new -d -x48 -y8 -s test \ "awk 'BEGIN { for (i = 0; i < 90; i++) { if (i % 2 == 0) printf \"L%03d-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-END\\n\", i; else printf \"S%03d\\n\", i } }'; exec sleep 100" || exit 1 @@ -122,4 +124,46 @@ sleep 1 capture check_line 1 "S003" +# Scrolling from long content to a tabbed short line should still clear the old +# line tail. +$TMUX2 send -X cancel || exit 1 +$TMUX resizew -x40 -y8 || exit 1 +$TMUX2 refresh -t"$CLIENT" -c || exit 1 +$TMUX2 new-window \ + "printf 'LONGTAIL-ABCDEFGHIJKLMNOPQRSTUV\nA\tB\nLONGTAIL-123456789012345678\nS\n'; i=0; while [ \$i -lt 20 ]; do printf 'FILLER-%02d\n' \$i; i=\$((i + 1)); done; exec sleep 100" || \ + exit 1 +sleep 1 +$TMUX2 copy-mode -H || exit 1 +$TMUX2 send -X history-top || exit 1 +sleep 1 +capture +check_line 1 "LONGTAIL-ABCDEFGHIJKLMNOPQRSTUV" + +$TMUX2 send -X scroll-down || exit 1 +sleep 1 +capture +check_line 1 "A B" +sed -n 1p $TMP | grep -Fq "LONGTAIL" && \ + fail "short tabbed line left stale tail" + +# Reflow can leave padding from tabs at the start of wrapped lines. Entering +# copy mode should redraw those padding cells as spaces, not skip them and move +# the following text left. +$TMUX2 send -X cancel || exit 1 +$TMUX resizew -x80 -y24 || exit 1 +$TMUX2 refresh -t"$CLIENT" -c || exit 1 +$TMUX2 new-window "cat ../tmux.c; exec sleep 100" || exit 1 +sleep 1 +$TMUX2 splitw -hd || exit 1 +sleep 1 +$TMUX capturep -pS0 -E- >$BEFORE || exit 1 +$TMUX2 copy-mode -H -t:.0 || exit 1 +sleep 1 +$TMUX capturep -pS0 -E- >$AFTER || exit 1 + +if ! cmp -s "$BEFORE" "$AFTER"; then + diff -u "$BEFORE" "$AFTER" >&2 + fail "copy-mode redraw moved reflowed tab padding" +fi + exit 0 From f88189cd59096b58babd8688449ecc79cac77527 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 14 Jul 2026 09:07:24 +0000 Subject: [PATCH 29/36] Do not clear lines when we are just going to overwrite them in copy mode. --- window-copy.c | 149 ++++++++++++++++++++++++-------------------------- 1 file changed, 71 insertions(+), 78 deletions(-) diff --git a/window-copy.c b/window-copy.c index 3d9194c69..283d3deba 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.417 2026/07/13 21:22:45 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.418 2026/07/14 09:07:24 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -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, int); -static void window_copy_redraw_screen(struct window_mode_entry *, int); + u_int); +static void window_copy_redraw_screen(struct window_mode_entry *); 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 *); @@ -68,7 +68,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, int); + struct screen_write_ctx *, u_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 *); @@ -635,7 +635,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, 1); + window_copy_write_line(wme, &ctx, i); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(&data->screen)), data->cy, 0); screen_write_stop(&ctx); @@ -749,10 +749,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, 1); + window_copy_redraw_lines(wme, 0, 1); /* Write the new lines. */ - window_copy_redraw_lines(wme, old_cy, backing->cy - old_cy + 1, 1); + window_copy_redraw_lines(wme, old_cy, backing->cy - old_cy + 1); screen_write_stop(&ctx); } @@ -870,7 +870,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_view(wme, 1, 0); window_pane_scrollbar_show(wp, 1); - window_copy_redraw_screen(wme, 1); + window_copy_redraw_screen(wme); } void @@ -924,7 +924,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, 1); + window_copy_redraw_screen(wme); } void @@ -985,7 +985,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, 1); + window_copy_redraw_screen(wme); return (0); } @@ -1233,7 +1233,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, 1); + window_copy_redraw_screen(wme); } static const char * @@ -2543,7 +2543,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, 1); + window_copy_redraw_lines(wme, data->cy, 1); } data->endselrx = data->cx; data->endselry = screen_hsize(data->backing) + data->cy - data->oy; @@ -3051,7 +3051,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, 1); + window_copy_redraw_screen(wme); /* The timer runs outside key handling, so force a repaint. */ wp->flags |= PANE_REDRAW; wp->flags &= ~PANE_UNSEENCHANGES; @@ -3865,13 +3865,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, 1); + window_copy_redraw_screen(wme); else if (action == WINDOW_COPY_CMD_MOVE) { /* * 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, 0); + window_copy_redraw_lines(wme, 0, 1); } } @@ -3908,7 +3908,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, 1); + window_copy_redraw_screen(wme); } static int @@ -4696,7 +4696,7 @@ window_copy_search(struct window_mode_entry *wme, int direction, int regex) } } } - window_copy_redraw_screen(wme, 1); + window_copy_redraw_screen(wme); screen_free(&ss); return (found); @@ -4949,7 +4949,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, 1); + window_copy_redraw_screen(wme); } static void @@ -5112,7 +5112,8 @@ window_copy_write_one(struct window_mode_entry *wme, window_copy_update_style(wme, fx, fy, &gc, mgc, cgc, mkgc); screen_write_cell(ctx, &gc); - } + } else + screen_write_putc(ctx, &grid_default_cell, ' '); } } @@ -5242,7 +5243,7 @@ window_copy_set_line_numbers1(struct window_mode_entry *wme, int enabled, if (data->line_numbers == line_numbers && active == enabled) return; data->line_numbers = line_numbers; - window_copy_redraw_screen(wme, 1); + window_copy_redraw_screen(wme); } int @@ -5264,7 +5265,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, int clear) + struct screen_write_ctx *ctx, u_int py) { struct window_pane *wp = wme->wp; struct window_copy_mode_data *data = wme->data; @@ -5289,8 +5290,6 @@ window_copy_write_line(struct window_mode_entry *wme, content_sx = sx; screen_write_cursormove(ctx, 0, py, 0); - if (clear) - screen_write_clearline(ctx, 8); ft = format_create_defaults(NULL, NULL, NULL, NULL, wp); @@ -5361,7 +5360,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, 1); + window_copy_write_line(wme, ctx, yy); } static void @@ -5389,12 +5388,11 @@ 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, 0); + window_copy_redraw_lines(wme, start, end - start + 1); } static void -window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny, - int clear) +window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) { struct window_pane *wp = wme->wp; struct window_copy_mode_data *data = wme->data; @@ -5405,7 +5403,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, clear); + window_copy_write_line(wme, &ctx, i); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); @@ -5419,7 +5417,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, clear); + window_copy_write_line(wme, &ctx, i); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); @@ -5429,11 +5427,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, int clear) +window_copy_redraw_screen(struct window_mode_entry *wme) { struct window_copy_mode_data *data = wme->data; - window_copy_redraw_lines(wme, 0, screen_size_y(&data->screen), clear); + window_copy_redraw_lines(wme, 0, screen_size_y(&data->screen)); } static void @@ -5443,7 +5441,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, 1); + window_copy_redraw_screen(wme); } static void @@ -5564,7 +5562,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, 0); + window_copy_redraw_screen(wme); return; } if (width >= screen_size_x(s)) @@ -5572,7 +5570,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, 0); + window_copy_redraw_screen(wme); return; } screen_write_start_pane(&ctx, wp, NULL); @@ -5583,9 +5581,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, 1); + window_copy_redraw_lines(wme, old_cy, 1); if (data->cx == screen_size_x(s)) - window_copy_redraw_lines(wme, data->cy, 1, 1); + window_copy_redraw_lines(wme, data->cy, 1); else { screen_write_start_pane(&ctx, wp, NULL); screen_write_cursormove(&ctx, @@ -5811,16 +5809,14 @@ 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, 1); + window_copy_redraw_lines(wme, sy, cy - sy + 1); else - window_copy_redraw_lines(wme, cy, sy - cy + 1, 1); + window_copy_redraw_lines(wme, cy, sy - cy + 1); } else { if (endsy < cy) { - window_copy_redraw_lines(wme, endsy, - cy - endsy + 1, 1); + window_copy_redraw_lines(wme, endsy, cy - endsy + 1); } else { - window_copy_redraw_lines(wme, cy, - endsy - cy + 1, 1); + window_copy_redraw_lines(wme, cy, endsy - cy + 1); } } } @@ -6300,7 +6296,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, 1); + window_copy_redraw_screen(wme); } static void @@ -6377,9 +6373,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, 0); + window_copy_redraw_lines(wme, data->cy, 1); else - window_copy_redraw_lines(wme, data->cy, 2, 0); + window_copy_redraw_lines(wme, data->cy, 2); } } else { if (norectsel) { @@ -6389,9 +6385,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, 1); + window_copy_redraw_lines(wme, data->cy, 1); else - window_copy_redraw_lines(wme, data->cy, 2, 1); + window_copy_redraw_lines(wme, data->cy, 2); } } @@ -6403,7 +6399,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, 1); + window_copy_redraw_lines(wme, data->cy, 1); } } @@ -6416,13 +6412,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, 1); + window_copy_redraw_lines(wme, data->cy, 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, 1); + window_copy_redraw_lines(wme, data->cy, 1); } } @@ -6456,7 +6452,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, 0); + window_copy_redraw_lines(wme, data->cy - 1, 2); } else { if (norectsel) { window_copy_update_cursor(wme, data->lastcx, @@ -6464,7 +6460,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, 1); + window_copy_redraw_lines(wme, data->cy - 1, 2); } if (norectsel) { @@ -6475,7 +6471,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, 1); + window_copy_redraw_lines(wme, data->cy, 1); } } @@ -6488,13 +6484,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, 1); + window_copy_redraw_lines(wme, data->cy, 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, 1); + window_copy_redraw_lines(wme, data->cy, 1); } } @@ -6764,7 +6760,7 @@ window_copy_cursor_prompt(struct window_mode_entry *wme, int direction, } window_copy_update_selection(wme, 1, 0); - window_copy_redraw_screen(wme, 1); + window_copy_redraw_screen(wme); } static void @@ -6788,22 +6784,20 @@ 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, 0); + window_copy_redraw_screen(wme); 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, 0); + window_copy_write_line(wme, &ctx, 0); if (screen_size_y(s) > 1) - window_copy_write_line(wme, &ctx, 1, 0); + window_copy_write_line(wme, &ctx, 1); if (screen_size_y(s) > 3) - window_copy_write_line(wme, &ctx, - screen_size_y(s) - 2, 0); + window_copy_write_line(wme, &ctx, screen_size_y(s) - 2); if (s->sel != NULL && screen_size_y(s) > ny) { - window_copy_write_line(wme, &ctx, - screen_size_y(s) - ny - 1, 0); + window_copy_write_line(wme, &ctx, screen_size_y(s) - ny - 1); } screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), @@ -6820,14 +6814,13 @@ 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, 0); + window_copy_write_line(wme, &ctx, 0); if (screen_size_y(s) > 1) - window_copy_write_line(wme, &ctx, 1, 0); + window_copy_write_line(wme, &ctx, 1); if (screen_size_y(s) > 3) - window_copy_write_line(wme, &ctx, screen_size_y(s) - 2, 0); + window_copy_write_line(wme, &ctx, screen_size_y(s) - 2); if (s->sel != NULL && screen_size_y(s) > ny) - window_copy_write_line(wme, &ctx, screen_size_y(s) - ny - 1, - 0); + window_copy_write_line(wme, &ctx, screen_size_y(s) - ny - 1); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); @@ -6859,7 +6852,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, 0); + window_copy_redraw_screen(wme); return; } screen_write_start(&ctx, &data->screen); @@ -6867,9 +6860,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, 0); + window_copy_write_line(wme, &ctx, ny); else if (ny == 1) - window_copy_write_line(wme, &ctx, 1, 0); + window_copy_write_line(wme, &ctx, 1); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); @@ -6886,9 +6879,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, 0); + window_copy_write_line(wme, &ctx, ny); else if (ny == 1) /* nuke position */ - window_copy_write_line(wme, &ctx, 1, 0); + window_copy_write_line(wme, &ctx, 1); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); screen_write_stop(&ctx); @@ -6909,7 +6902,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, 1); + window_copy_redraw_screen(wme); } static void @@ -7002,7 +6995,7 @@ window_copy_start_drag(struct client *c, struct mouse_event *m) break; } - window_copy_redraw_screen(wme, 1); + window_copy_redraw_screen(wme); window_copy_drag_update(c, m); } @@ -7098,7 +7091,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, 1); + window_copy_redraw_screen(wme); } /* Scroll up if the cursor went off the visible screen. */ @@ -7124,7 +7117,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, 1); + window_copy_redraw_lines(wme, cy, nd); } /* Scroll down if the cursor went off the visible screen. */ @@ -7153,5 +7146,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, 1); + window_copy_redraw_lines(wme, oldy, nd); } From a10252dad26ce7fad9569440d2b7fa7b1626d499 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 14 Jul 2026 11:08:36 +0000 Subject: [PATCH 30/36] Handle padding and tab redraw correctly in copy mode. --- window-copy.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/window-copy.c b/window-copy.c index 283d3deba..d1b83a6bf 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.418 2026/07/14 09:07:24 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.419 2026/07/14 11:08:36 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -5103,7 +5103,7 @@ window_copy_write_one(struct window_mode_entry *wme, struct window_copy_mode_data *data = wme->data; struct grid *gd = data->backing->grid; struct grid_cell gc; - u_int fx; + u_int fx, i, width; screen_write_cursormove(ctx, px, py, 0); for (fx = 0; fx < nx; fx++) { @@ -5111,7 +5111,20 @@ window_copy_write_one(struct window_mode_entry *wme, if (fx + gc.data.width <= nx) { window_copy_update_style(wme, fx, fy, &gc, mgc, cgc, mkgc); - screen_write_cell(ctx, &gc); + if (gc.flags & GRID_FLAG_PADDING) { + if (ctx->s->cy == py && ctx->s->cx <= px + fx) { + gc.flags &= ~GRID_FLAG_PADDING; + screen_write_cursormove(ctx, px + fx, py, + 0); + screen_write_putc(ctx, &gc, ' '); + } + } else if (gc.flags & GRID_FLAG_TAB) { + width = gc.data.width; + gc.flags &= ~GRID_FLAG_TAB; + for (i = 0; i < width; i++) + screen_write_putc(ctx, &gc, ' '); + } else + screen_write_cell(ctx, &gc); } else screen_write_putc(ctx, &grid_default_cell, ' '); } From d705471aa5aa5a54f6f1749d27424ae0d137ccdf Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 14 Jul 2026 18:18:52 +0100 Subject: [PATCH 31/36] Regression tests for display-panes changes. --- regress/display-panes.sh | 254 +++++++++++++++++++++++++++++++++++++++ regress/mode-kill.sh | 13 +- 2 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 regress/display-panes.sh diff --git a/regress/display-panes.sh b/regress/display-panes.sh new file mode 100644 index 000000000..83d21848b --- /dev/null +++ b/regress/display-panes.sh @@ -0,0 +1,254 @@ +#!/bin/sh + +# Tests for display-panes as panes-mode. + +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 +} + +wait_format() +{ + target=$1 + format=$2 + want=$3 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t "$target" "$format" 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "$target: expected '$want' for $format, got '$got'" +} + +wait_option() +{ + option=$1 + want=$2 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX show -gv "$option" 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "$option: expected '$want', got '$got'" +} + +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" +} + +$TMUX new-session -d -s m -x 80 -y 24 'cat' || exit 1 +$TMUX split-window -t m:0 'cat' || exit 1 +p0=$($TMUX display-message -p -t m:0.0 '#{pane_id}') +p1=$($TMUX display-message -p -t m:0.1 '#{pane_id}') +$TMUX set -g display-panes-format 'P#{pane_index}' || + fail "set display-panes-format failed" + +$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t m" || exit 1 +wait_clients 1 + +capture() +{ + $TMUX2 capture-pane -p -t out:0 2>/dev/null +} + +wait_capture() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + printf '%s\n' "$CAPTURED" | grep -F -q "$1" && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1' in capture" +} + +# Entry, default zoom, and exit. +$TMUX display-panes -d 0 -t "$p0" || fail "display-panes failed" +wait_format "$p0" '#{pane_mode}' 'panes-mode' +wait_format "$p0" '#{window_zoomed_flag}' '1' +wait_capture 'P0' +wait_capture 'P1' +$TMUX send-keys -t "$p0" q || fail "exit panes mode failed" +wait_format "$p0" '#{pane_in_mode}' '0' +wait_format "$p0" '#{window_zoomed_flag}' '0' + +# -Z starts unzoomed. +$TMUX display-panes -Zd 0 -t "$p0" || fail "display-panes -Z failed" +wait_format "$p0" '#{pane_mode}' 'panes-mode' +wait_format "$p0" '#{window_zoomed_flag}' '0' +$TMUX send-keys -t "$p0" q || fail "exit unzoomed panes mode failed" +wait_format "$p0" '#{pane_in_mode}' '0' + +# Selection keys run the command template and close the mode. +$TMUX set -g @picked none || fail "set @picked failed" +$TMUX display-panes -d 0 -t "$p0" 'set -g @picked %%' || + fail "display-panes selection failed" +wait_format "$p0" '#{pane_mode}' 'panes-mode' +$TMUX send-keys -t "$p0" 1 || fail "select pane failed" +wait_format "$p0" '#{pane_in_mode}' '0' +wait_option @picked "$p1" + +# Commands after display-panes run immediately while the mode remains. +$TMUX set -g @after none || fail "set @after failed" +$TMUX display-panes -Nd 500 -t "$p0" \; set -g @after fast || + fail "display-panes immediate command failed" +wait_option @after fast +wait_format "$p0" '#{pane_mode}' 'panes-mode' +wait_format "$p0" '#{pane_in_mode}' '0' + +# Existing zoom is restored on exit. +$TMUX select-pane -t "$p0" || fail "select p0 failed" +$TMUX resize-pane -Z -t "$p0" || fail "zoom failed" +wait_format "$p0" '#{window_zoomed_flag}' '1' +$TMUX display-panes -d 0 -t "$p0" || fail "display-panes zoomed failed" +wait_format "$p0" '#{pane_mode}' 'panes-mode' +$TMUX send-keys -t "$p0" q || fail "exit zoomed panes mode failed" +wait_format "$p0" '#{pane_in_mode}' '0' +wait_format "$p0" '#{window_zoomed_flag}' '1' +$TMUX resize-pane -Z -t "$p0" || fail "unzoom cleanup failed" + +# Panes mode can be stacked above another mode and returns to it on exit. +$TMUX copy-mode -t "$p0" || fail "copy-mode failed" +wait_format "$p0" '#{pane_mode}' 'copy-mode' +$TMUX display-panes -d 0 -t "$p0" 'set -g @picked %%' || + fail "display-panes over copy-mode failed" +wait_format "$p0" '#{pane_mode}' 'panes-mode' +$TMUX send-keys -t "$p0" 1 || fail "select from stacked mode failed" +wait_format "$p0" '#{pane_mode}' 'copy-mode' +wait_option @picked "$p1" +$TMUX send-keys -t "$p0" -X cancel || fail "copy-mode cancel failed" +wait_format "$p0" '#{pane_in_mode}' '0' + +# Panes mode is not kept underneath another mode. +$TMUX display-panes -d 0 -t "$p0" || fail "display-panes no-stack failed" +wait_format "$p0" '#{pane_mode}' 'panes-mode' +$TMUX copy-mode -t "$p0" || fail "copy-mode over panes-mode failed" +wait_format "$p0" '#{pane_mode}' 'copy-mode' +$TMUX send-keys -t "$p0" -X cancel || fail "copy-mode cancel failed" +wait_format "$p0" '#{pane_in_mode}' '0' +wait_format "$p0" '#{window_zoomed_flag}' '0' + +# -s shows panes from a different source window but enters the mode in the +# target pane. +$TMUX new-window -d -t m: -n src 'cat' || fail "new source window failed" +$TMUX split-window -t m:src 'cat' || fail "split source window failed" +sp0=$($TMUX display-message -p -t m:src.0 '#{pane_id}') +sp1=$($TMUX display-message -p -t m:src.1 '#{pane_id}') +$TMUX respawn-pane -k -t "$sp0" 'printf "SOURCE-PANE\n"; exec cat' || + fail "write source marker failed" +$TMUX set -g display-panes-format '' || fail "clear source display format failed" +$TMUX set -w -t m:src display-panes-format 'SOURCE-ONLY' || + fail "set source window display format failed" +$TMUX set -g @picked none || fail "reset @picked failed" +$TMUX display-panes -d 0 -s m:src -t "$p0" 'set -g @picked %%' || + fail "display-panes source window failed" +wait_format "$p0" '#{pane_mode}' 'panes-mode' +wait_capture 'SOURCE-PANE' +CAPTURED=$(capture) +printf '%s\n' "$CAPTURED" | grep -F -q 'SOURCE-ONLY' && + fail "used source window display-panes-format" +$TMUX send-keys -t "$p0" 1 || fail "select source pane failed" +wait_format "$p0" '#{pane_in_mode}' '0' +wait_option @picked "$sp1" +$TMUX kill-window -t m:src || fail "kill source window failed" +$TMUX set -g display-panes-format 'P#{pane_index}' || + fail "reset display-panes-format after source failed" + +# pane-border-status rows are drawn as plain preview borders and do not shift +# pane content into the status row. +$TMUX set -w -t m:0 pane-border-status top || + fail "set pane-border-status failed" +$TMUX set -g display-panes-format '' || fail "clear display-panes-format failed" +$TMUX respawn-pane -k -t "$p0" 'printf "STATUS-TOP\n"; exec cat' || + fail "write status marker failed" +wait_capture 'STATUS-TOP' +$TMUX display-panes -d 0 -t "$p0" || fail "display-panes status failed" +wait_format "$p0" '#{pane_mode}' 'panes-mode' +wait_capture 'STATUS-TOP' +CAPTURED=$(capture) +first=$(printf '%s\n' "$CAPTURED" | sed -n '1p') +second=$(printf '%s\n' "$CAPTURED" | sed -n '2p') +printf '%s\n' "$first" | grep -F -q 'STATUS-TOP' && + fail "pane content drawn in status border row" +printf '%s\n' "$second" | grep -F -q 'STATUS-TOP' || + fail "pane content not drawn below status border row" +$TMUX send-keys -t "$p0" q || fail "exit status panes mode failed" +wait_format "$p0" '#{pane_in_mode}' '0' +$TMUX set -w -t m:0 pane-border-status off || + fail "reset pane-border-status failed" +$TMUX set -g display-panes-format 'P#{pane_index}' || + fail "reset display-panes-format failed" + +# Floating panes are always framed in panes-mode, independent of their real +# pane-border-lines setting. +fp=$($TMUX new-pane -dPF '#{pane_id}' -B none -x 30 -y 8 -X 8 -Y 3 \ + -t m:0 'cat') || fail "new borderless floating pane failed" +$TMUX set -g display-panes-format '' || fail "clear display-panes-format failed" +$TMUX display-panes -Zd 0 -t "$fp" || fail "display-panes floating failed" +wait_format "$fp" '#{pane_mode}' 'panes-mode' +wait_capture '┌' +wait_capture '┘' +$TMUX send-keys -t "$fp" q || fail "exit floating panes mode failed" +wait_format "$fp" '#{pane_in_mode}' '0' + +$TMUX display-panes -d 0 -t "$fp" || fail "display-panes floating zoomed failed" +wait_format "$fp" '#{pane_mode}' 'panes-mode' +wait_format "$fp" '#{window_zoomed_flag}' '1' +wait_capture '┌' +wait_capture '┘' +$TMUX send-keys -t "$fp" q || fail "exit zoomed floating panes mode failed" +wait_format "$fp" '#{pane_in_mode}' '0' +wait_format "$fp" '#{window_zoomed_flag}' '0' + +$TMUX resize-pane -t "$fp" -x 48 -y 14 || + fail "resize floating pane larger failed" +$TMUX set -g display-panes-format '#[align=right]FP#{pane_width}x#{pane_height}' || + fail "set floating display-panes-format failed" +$TMUX display-panes -Zd 0 -t "$fp" || fail "display-panes floating format failed" +wait_format "$fp" '#{pane_mode}' 'panes-mode' +wait_capture 'FP48x14' +$TMUX resize-pane -t "$fp" -x 30 -y 10 || + fail "resize floating pane smaller failed" +wait_capture 'FP30x10' +$TMUX send-keys -t "$fp" q || fail "exit floating format panes mode failed" +wait_format "$fp" '#{pane_in_mode}' '0' + +$TMUX kill-pane -t "$fp" || fail "kill floating pane failed" +$TMUX set -g display-panes-format 'P#{pane_index}' || + fail "reset display-panes-format after floating failed" + +exit 0 diff --git a/regress/mode-kill.sh b/regress/mode-kill.sh index a3e0d09b9..d9fb4dc64 100755 --- a/regress/mode-kill.sh +++ b/regress/mode-kill.sh @@ -8,9 +8,11 @@ # - copy-mode -k (window-copy.c); # - choose-tree -k (window-tree.c); # - choose-buffer -k (window-buffer.c). +# - display-panes -k (window-panes.c). # # choose-tree and choose-buffer share cmd_choose_tree_exec(), which also backs -# choose-client and customize-mode, so those are not repeated. +# choose-client, customize-mode, and display-panes, so choose-client and +# customize-mode 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 @@ -147,7 +149,7 @@ 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 ---------------------- +# --- choose-tree -k, choose-buffer -k, and display-panes -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 @@ -168,4 +170,11 @@ wait_for 'BUFMARK' $TMUX send-keys -t m:w q || fail "choose-buffer exit failed" check_killed 'choose-buffer -k' +open_window +$TMUX set -g display-panes-format 'DPMARK' || fail "set display-panes-format" +$TMUX display-panes -k -d 0 -t m:w || fail "display-panes -k failed" +wait_for 'DPMARK' +$TMUX send-keys -t m:w q || fail "display-panes exit failed" +check_killed 'display-panes -k' + exit 0 From 34a11db6ca92c0bb524987062182b51ae2e23aa3 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 14 Jul 2026 15:06:54 +0000 Subject: [PATCH 32/36] Typos and doc fixes, from Ben Boeckel. --- cmd-split-window.c | 10 +++++----- format.c | 8 ++++---- tmux.1 | 11 +++++++++-- window-copy.c | 8 ++++---- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cmd-split-window.c b/cmd-split-window.c index a36f05603..caaa7c02e 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.143 2026/07/13 16:07:47 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.144 2026/07/14 15:06:54 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -60,10 +60,10 @@ const struct cmd_entry cmd_split_window_entry = { .name = "split-window", .alias = "splitw", - .args = { "bc:de:EfF:hIkl:m:p:PR:s:S:t:T:vWZ", 0, -1, NULL }, - .usage = "[-bdefhIklPvWZ] [-c start-directory] [-e environment] " - "[-F format] [-l size] [-m message] [-p percentage] " - "[-s style] [-S active-border-style] " + .args = { "bB:c:de:EfF:hIkl:m:p:PR:s:S:t:T:vWZ", 0, -1, NULL }, + .usage = "[-bdefhIklPvWZ] [-B border-lines] [-c start-directory] " + "[-e environment] [-F format] [-l size] [-m message] " + "[-p percentage] [-s style] [-S active-border-style] " "[-R inactive-border-style] [-T title] " CMD_TARGET_PANE_USAGE " [shell-command [argument ...]]", diff --git a/format.c b/format.c index 6175f0d0d..77be4edb9 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.403 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.404 2026/07/14 15:06:54 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -85,13 +85,13 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2) return (strcmp(fj1->cmd, fj2->cmd)); } -/* Maimum pad and trim width. */ +/* Maximum pad and trim width. */ #define FORMAT_MAX_WIDTH 10000 -/* Maimum repeat size. */ +/* Maximum repeat size. */ #define FORMAT_MAX_REPEAT 10000 -/* Maimum precision. */ +/* Maximum precision. */ #define FORMAT_MAX_PRECISION 100 /* Format modifiers. */ diff --git a/tmux.1 b/tmux.1 index 0620f2485..ca0ba018b 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1138 2026/07/13 22:03:08 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1139 2026/07/14 15:06:54 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: July 13 2026 $ +.Dd $Mdocdate: July 14 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -848,6 +848,7 @@ Each has a single-character alternative form. .It Li "{next}" Ta "+" Ta "The next window by number" .It Li "{previous}" Ta "\-" Ta "The previous window by number" .It Li "{current}" Ta "@" Ta "The current window" +.It Li "{mouse}" Ta "=" Ta "The window where the mouse event happened" .El .Pp .Ar target\-pane @@ -881,6 +882,8 @@ The following special tokens are available for the pane index: .It Li "{left\-of}" Ta "" Ta "The pane to the left of the active pane" .It Li "{right\-of}" Ta "" Ta "The pane to the right of the active pane" .It Li "{active}" Ta "@" Ta "The active pane" +.It Li "{marked}" Ta "~" Ta "The marked pane" +.It Li "{mouse}" Ta "=" Ta "The pane where the mouse event happened" .El .Pp The tokens @@ -4035,6 +4038,7 @@ the command behaves like .Tg splitw .It Xo Ic split\-window .Op Fl bdefhIkPvZ +.Op Fl B Ar border\-lines .Op Fl c Ar start\-directory .Op Fl e Ar environment .Op Fl F Ar format @@ -4082,6 +4086,9 @@ may be followed by to specify a percentage of the available space. .Fl p is a shorthand option for this. +.Fl B +sets the pane border lines for floating panes; see +.Ic pane\-border\-lines . The .Fl b option causes the new pane to be created to the left of or above diff --git a/window-copy.c b/window-copy.c index d1b83a6bf..0991a387c 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.419 2026/07/14 11:08:36 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.420 2026/07/14 15:06:54 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -5573,7 +5573,8 @@ window_copy_update_cursor(struct window_mode_entry *wme, u_int cx, u_int cy) if (window_copy_line_numbers_active(wme)) { width = window_copy_line_number_width(wme); - if (s->sel != NULL || data->lineflag != LINE_SEL_NONE || + if (s->sel != NULL || + data->lineflag != LINE_SEL_NONE || old_cy != data->cy) { window_copy_redraw_screen(wme); return; @@ -6809,9 +6810,8 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny) window_copy_write_line(wme, &ctx, 1); if (screen_size_y(s) > 3) window_copy_write_line(wme, &ctx, screen_size_y(s) - 2); - if (s->sel != NULL && screen_size_y(s) > ny) { + if (s->sel != NULL && screen_size_y(s) > ny) window_copy_write_line(wme, &ctx, screen_size_y(s) - ny - 1); - } screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); From 1a02c9957ca3d7c49f2756a794dce645a8af4f12 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 14 Jul 2026 17:17:17 +0000 Subject: [PATCH 33/36] Convert display-panes away from an overlay and into a mode. This is another step on the road of getting rid of overlays altogether (they are full of special cases; popups and menus are to go also eventually). We lose some of the styling of borders but gain the ability to run display-panes in another pane (perhaps not hugely useful but one never knows). The -b flag goes away as no longer useful. --- Makefile | 6 +- cfg.c | 5 +- cmd-choose-tree.c | 21 +- cmd-copy-mode.c | 8 +- cmd-display-panes.c | 393 ---------------- cmd-find-window.c | 5 +- cmd-queue.c | 9 +- cmd-run-shell.c | 5 +- layout.c | 76 +-- options-table.c | 79 ++-- server-client.c | 5 +- tmux.1 | 88 ++-- tmux.h | 17 +- window-buffer.c | 8 +- window-client.c | 8 +- window-clock.c | 8 +- window-copy.c | 12 +- window-customize.c | 8 +- window-panes.c | 1067 +++++++++++++++++++++++++++++++++++++++++++ window-switch.c | 8 +- window-tree.c | 8 +- window.c | 31 +- 22 files changed, 1333 insertions(+), 542 deletions(-) delete mode 100644 cmd-display-panes.c create mode 100644 window-panes.c diff --git a/Makefile b/Makefile index d73312013..d7673eab2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# $OpenBSD: Makefile,v 1.120 2026/07/10 13:38:45 nicm Exp $ +# $OpenBSD: Makefile,v 1.121 2026/07/14 17:17:17 nicm Exp $ PROG= tmux SRCS= alerts.c \ @@ -17,7 +17,6 @@ SRCS= alerts.c \ cmd-detach-client.c \ cmd-display-menu.c \ cmd-display-message.c \ - cmd-display-panes.c \ cmd-find-window.c \ cmd-find.c \ cmd-if-shell.c \ @@ -137,9 +136,10 @@ SRCS= alerts.c \ window-clock.c \ window-copy.c \ window-customize.c \ + window-panes.c \ window-switch.c \ - window-visible.c \ window-tree.c \ + window-visible.c \ window.c \ xmalloc.c diff --git a/cfg.c b/cfg.c index 7a79a6025..8e67105a0 100644 --- a/cfg.c +++ b/cfg.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cfg.c,v 1.89 2026/06/25 11:39:11 nicm Exp $ */ +/* $OpenBSD: cfg.c,v 1.90 2026/07/14 17:17:17 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -269,7 +269,8 @@ cfg_show_causes(struct session *s) wme = TAILQ_FIRST(&wp->modes); if (wme == NULL || wme->mode != &window_view_mode) - window_pane_set_mode(wp, NULL, &window_view_mode, NULL, NULL); + window_pane_set_mode(wp, NULL, &window_view_mode, NULL, NULL, + NULL); for (i = 0; i < cfg_ncauses; i++) { window_copy_add(wp, 0, "%s", cfg_causes[i]); free(cfg_causes[i]); diff --git a/cmd-choose-tree.c b/cmd-choose-tree.c index f9ce7d3b7..2dc304ff2 100644 --- a/cmd-choose-tree.c +++ b/cmd-choose-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-choose-tree.c,v 1.57 2026/06/26 14:40:30 nicm Exp $ */ +/* $OpenBSD: cmd-choose-tree.c,v 1.58 2026/07/14 17:17:17 nicm Exp $ */ /* * Copyright (c) 2012 Thomas Adam @@ -97,6 +97,21 @@ const struct cmd_entry cmd_switch_mode_entry = { .exec = cmd_choose_tree_exec }; +const struct cmd_entry cmd_display_panes_entry = { + .name = "display-panes", + .alias = "displayp", + + .args = { "d:kNs:t:Z", 0, 1, cmd_choose_tree_args_parse }, + .usage = "[-kNZ] [-d duration] [-s source-window] " + CMD_TARGET_PANE_USAGE " [template]", + + .source = { 's', CMD_FIND_WINDOW, 0 }, + .target = { 't', CMD_FIND_PANE, 0 }, + + .flags = CMD_AFTERHOOK, + .exec = cmd_choose_tree_exec +}; + static enum args_parse_type cmd_choose_tree_args_parse(__unused struct args *args, __unused u_int idx, __unused char **cause) @@ -131,9 +146,11 @@ cmd_choose_tree_exec(struct cmd *self, struct cmdq_item *item) mode = &window_customize_mode; else if (cmd_get_entry(self) == &cmd_switch_mode_entry) mode = &window_switch_mode; + else if (cmd_get_entry(self) == &cmd_display_panes_entry) + mode = &window_panes_mode; else mode = &window_tree_mode; - window_pane_set_mode(wp, NULL, mode, target, args); + window_pane_set_mode(wp, NULL, mode, item, target, args); return (CMD_RETURN_NORMAL); } diff --git a/cmd-copy-mode.c b/cmd-copy-mode.c index 051cb58a2..e6888ca7e 100644 --- a/cmd-copy-mode.c +++ b/cmd-copy-mode.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-copy-mode.c,v 1.53 2026/07/08 08:07:42 nicm Exp $ */ +/* $OpenBSD: cmd-copy-mode.c,v 1.54 2026/07/14 17:17:17 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -79,7 +79,8 @@ cmd_copy_mode_exec(struct cmd *self, struct cmdq_item *item) } if (cmd_get_entry(self) == &cmd_clock_mode_entry) { - window_pane_set_mode(wp, NULL, &window_clock_mode, NULL, NULL); + window_pane_set_mode(wp, NULL, &window_clock_mode, item, NULL, + NULL); return (CMD_RETURN_NORMAL); } @@ -90,7 +91,8 @@ cmd_copy_mode_exec(struct cmd *self, struct cmdq_item *item) line_numbers = 1; if (event != NULL && KEYC_IS_MOUSE(event->key)) line_numbers = 0; - if (!window_pane_set_mode(wp, swp, &window_copy_mode, NULL, args)) { + if (!window_pane_set_mode(wp, swp, &window_copy_mode, item, NULL, + args)) { window_copy_set_line_numbers(wp, line_numbers); if (args_has(args, 'M')) window_copy_start_drag(c, &event->m); diff --git a/cmd-display-panes.c b/cmd-display-panes.c deleted file mode 100644 index 176238a1f..000000000 --- a/cmd-display-panes.c +++ /dev/null @@ -1,393 +0,0 @@ -/* $OpenBSD: cmd-display-panes.c,v 1.56 2026/06/25 16:32:42 nicm Exp $ */ - -/* - * 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" - -/* - * Display panes on a client. - */ - -static enum args_parse_type cmd_display_panes_args_parse(struct args *, - u_int, char **); -static enum cmd_retval cmd_display_panes_exec(struct cmd *, - struct cmdq_item *); - -const struct cmd_entry cmd_display_panes_entry = { - .name = "display-panes", - .alias = "displayp", - - .args = { "bd:Nt:", 0, 1, cmd_display_panes_args_parse }, - .usage = "[-bN] [-d duration] " CMD_TARGET_CLIENT_USAGE " [template]", - - .flags = CMD_AFTERHOOK|CMD_CLIENT_TFLAG, - .exec = cmd_display_panes_exec -}; - -struct cmd_display_panes_data { - struct cmdq_item *item; - struct args_command_state *state; -}; - -struct cmd_display_panes_ctx { - struct client *c; - int ox; - int oy; - u_int sx; - u_int sy; - u_int statuslines; - int statustop; -}; - -static enum args_parse_type -cmd_display_panes_args_parse(__unused struct args *args, __unused u_int idx, - __unused char **cause) -{ - return (ARGS_PARSE_COMMANDS_OR_STRING); -} - -static void -cmd_display_panes_put(struct cmd_display_panes_ctx *ctx, - struct window_pane *wp, u_int cx, u_int cy, const char *buf, size_t len) -{ - struct client *c = ctx->c; - struct tty *tty = &c->tty; - struct visible_ranges *r; - struct visible_range *ri; - u_int i, j; - - r = window_visible_ranges(wp, ctx->ox + cx, ctx->oy + cy, len, NULL); - for (i = 0; i < r->used; i++) { - ri = &r->ranges[i]; - for (j = ri->px; j < ri->px + ri->nx; j++) { - tty_cursor(tty, j - ctx->ox, cy); - tty_putn(tty, &buf[j - ri->px], 1, 1); - } - } -} - -static void -cmd_display_panes_draw_format(struct cmd_display_panes_ctx *ctx, - struct window_pane *wp, u_int xoff, u_int yoff, u_int sx, - const struct grid_cell *gc) -{ - struct client *c = ctx->c; - struct tty *tty = &c->tty; - struct session *s = c->session; - struct screen *sc = wp->screen, screen; - struct screen_write_ctx sctx; - struct visible_ranges *r; - struct visible_range *ri; - const char *format; - char *expanded; - u_int i, px = ctx->ox + xoff; - - format = options_get_string(s->options, "display-panes-format"); - expanded = format_single(NULL, format, c, s, s->curw, wp); - - screen_init(&screen, sx, 1, 0); - screen_write_start(&sctx, &screen); - screen_write_fast_copy(&sctx, sc, 0, sc->grid->hsize, sx, 1); - screen_write_cursormove(&sctx, 0, 0, 0); - format_draw(&sctx, gc, sx, expanded, NULL, 0); - screen_write_stop(&sctx); - free(expanded); - - r = window_visible_ranges(wp, px, wp->yoff, sx, NULL); - for (i = 0; i < r->used; i++) { - ri = &r->ranges[i]; - tty_draw_line(tty, &screen, ri->px - px, 0, ri->nx, - ri->px - ctx->ox, yoff, NULL); - } - screen_free(&screen); -} - -static void -cmd_display_panes_draw_pane(struct cmd_display_panes_ctx *ctx, - struct window_pane *wp) -{ - struct client *c = ctx->c; - struct tty *tty = &c->tty; - struct session *s = c->session; - struct options *oo = s->options; - struct window *w = wp->window; - struct grid_cell fgc, bgc; - u_int pane, idx, px, py, i, j, xoff, yoff, sx, sy; - u_int cx, cy; - const char *name; - struct format_tree *ft; - char buf[16], lbuf[16], *ptr; - size_t len, llen; - - if (wp->xoff + (int)wp->sx <= ctx->ox || - wp->xoff >= ctx->ox + (int)ctx->sx || - wp->yoff + (int)wp->sy <= ctx->oy || - wp->yoff >= ctx->oy + (int)ctx->sy) - return; - - if (wp->xoff >= ctx->ox && wp->xoff + wp->sx <= ctx->ox + ctx->sx) { - /* All visible. */ - xoff = wp->xoff - ctx->ox; - sx = wp->sx; - } else if (wp->xoff < ctx->ox && - wp->xoff + wp->sx > ctx->ox + ctx->sx) { - /* Both left and right not visible. */ - xoff = 0; - sx = ctx->sx; - } else if (wp->xoff < ctx->ox) { - /* Left not visible. */ - xoff = 0; - sx = wp->sx - (ctx->ox - wp->xoff); - } else { - /* Right not visible. */ - xoff = wp->xoff - ctx->ox; - sx = wp->sx - xoff; - } - if (wp->yoff >= ctx->oy && wp->yoff + wp->sy <= ctx->oy + ctx->sy) { - /* All visible. */ - yoff = wp->yoff - ctx->oy; - sy = wp->sy; - } else if (wp->yoff < ctx->oy && - wp->yoff + wp->sy > ctx->oy + ctx->sy) { - /* Both top and bottom not visible. */ - yoff = 0; - sy = ctx->sy; - } else if (wp->yoff < ctx->oy) { - /* Top not visible. */ - yoff = 0; - sy = wp->sy - (ctx->oy - wp->yoff); - } else { - /* Bottom not visible. */ - yoff = wp->yoff - ctx->oy; - sy = wp->sy - yoff; - } - - if (ctx->statustop) - yoff += ctx->statuslines; - px = sx / 2; - py = sy / 2; - - if (window_pane_index(wp, &pane) != 0) - fatalx("index not found"); - len = xsnprintf(buf, sizeof buf, "%u", pane); - - if (sx < len) - return; - if (w->active == wp) - name = "display-panes-active-colour"; - else - name = "display-panes-colour"; - ft = format_create_defaults(NULL, c, s, NULL, wp); - style_apply(&fgc, oo, name, ft); - format_free(ft); - - memcpy(&bgc, &grid_default_cell, sizeof bgc); - bgc.bg = fgc.fg; - - if (pane > 9 && pane < 35) - llen = xsnprintf(lbuf, sizeof lbuf, "%c", 'a' + (pane - 10)); - else - llen = 0; - - if (sx < len * 6 || sy < 5) { - tty_attributes(tty, &fgc, NULL); - if (sx >= len + llen + 1) { - len += llen + 1; - cx = xoff + px - len / 2; - cy = yoff + py; - cmd_display_panes_put(ctx, wp, cx, cy, buf, len); - cx += len; - cmd_display_panes_put(ctx, wp, cx, cy, " ", 1); - cx++; - cmd_display_panes_put(ctx, wp, cx, cy, lbuf, llen); - } else { - cx = xoff + px - len / 2; - cy = yoff + py; - cmd_display_panes_put(ctx, wp, cx, cy, buf, len); - } - goto out; - } - - px -= len * 3; - py -= 2; - - tty_attributes(tty, &bgc, NULL); - for (ptr = buf; *ptr != '\0'; ptr++) { - if (*ptr < '0' || *ptr > '9') - continue; - idx = *ptr - '0'; - - for (j = 0; j < 5; j++) { - for (i = px; i < px + 5; i++) { - if (!window_clock_table[idx][j][i - px]) - continue; - cx = xoff + i; - cy = yoff + py + j; - cmd_display_panes_put(ctx, wp, cx, cy, " ", 1); - } - } - px += 6; - } - - if (sy <= 6) - goto out; - cmd_display_panes_draw_format(ctx, wp, xoff, yoff, sx, &fgc); - if (llen != 0) { - tty_attributes(tty, &fgc, NULL); - cx = xoff + sx / 2 + len * 3 - llen - 1; - cy = yoff + py + 5; - cmd_display_panes_put(ctx, wp, cx, cy, lbuf, llen); - } - -out: - tty_cursor(tty, 0, 0); -} - -static void -cmd_display_panes_draw(struct client *c, __unused void *data) -{ - struct session *s = c->session; - struct window *w = s->curw->window; - struct window_pane *wp; - struct cmd_display_panes_ctx ctx; - u_int lines; - - log_debug("%s: %s @%u", __func__, c->name, w->id); - - memset(&ctx, 0, sizeof ctx); - ctx.c = c; - tty_window_offset(&c->tty, &ctx.ox, &ctx.oy, &ctx.sx, &ctx.sy); - if (options_get_number(s->options, "status-position") == 0) { - lines = status_line_size(c); - if (c->message_string != NULL || c->prompt != NULL) - lines = (lines == 0 ? 1 : lines); - ctx.statuslines = lines; - ctx.statustop = 1; - } - - TAILQ_FOREACH(wp, &w->panes, entry) { - if (window_pane_is_visible(wp)) - cmd_display_panes_draw_pane(&ctx, wp); - } -} - -static void -cmd_display_panes_free(__unused struct client *c, void *data) -{ - struct cmd_display_panes_data *cdata = data; - - if (cdata->item != NULL) - cmdq_continue(cdata->item); - args_make_commands_free(cdata->state); - free(cdata); -} - -static int -cmd_display_panes_key(struct client *c, void *data, struct key_event *event) -{ - struct cmd_display_panes_data *cdata = data; - char *expanded, *error; - struct cmdq_item *item = cdata->item, *new_item; - struct cmd_list *cmdlist; - struct window *w = c->session->curw->window; - struct window_pane *wp; - u_int index; - key_code key; - - if (event->key >= '0' && event->key <= '9') - index = event->key - '0'; - else if ((event->key & KEYC_MASK_MODIFIERS) == 0) { - key = (event->key & KEYC_MASK_KEY); - if (key >= 'a' && key <= 'z') - index = 10 + (key - 'a'); - else - return (-1); - } else - return (-1); - - wp = window_pane_at_index(w, index); - if (wp == NULL) - return (1); - window_unzoom(w, 1); - - xasprintf(&expanded, "%%%u", wp->id); - - cmdlist = args_make_commands(cdata->state, 1, &expanded, &error); - if (cmdlist == NULL) { - cmdq_append(c, cmdq_get_error(error)); - free(error); - } else if (item == NULL) { - new_item = cmdq_get_command(cmdlist, NULL); - cmdq_append(c, new_item); - } else { - new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); - cmdq_insert_after(item, new_item); - } - - free(expanded); - return (1); -} - -static enum cmd_retval -cmd_display_panes_exec(struct cmd *self, struct cmdq_item *item) -{ - struct args *args = cmd_get_args(self); - struct client *tc = cmdq_get_target_client(item); - struct session *s = tc->session; - u_int delay; - char *cause; - struct cmd_display_panes_data *cdata; - int wait = !args_has(args, 'b'); - - if (tc->overlay_draw != NULL) - return (CMD_RETURN_NORMAL); - - if (args_has(args, 'd')) { - delay = args_strtonum(args, 'd', 0, UINT_MAX, &cause); - if (cause != NULL) { - cmdq_error(item, "delay %s", cause); - free(cause); - return (CMD_RETURN_ERROR); - } - } else - delay = options_get_number(s->options, "display-panes-time"); - - cdata = xcalloc(1, sizeof *cdata); - if (wait) - cdata->item = item; - cdata->state = args_make_commands_prepare(self, item, 0, - "select-pane -t \"%%%\"", wait, 0); - - if (args_has(args, 'N')) { - server_client_set_overlay(tc, delay, NULL, NULL, - cmd_display_panes_draw, NULL, cmd_display_panes_free, NULL, - cdata); - } else { - server_client_set_overlay(tc, delay, NULL, NULL, - cmd_display_panes_draw, cmd_display_panes_key, - cmd_display_panes_free, NULL, cdata); - } - - if (!wait) - return (CMD_RETURN_NORMAL); - return (CMD_RETURN_WAIT); -} diff --git a/cmd-find-window.c b/cmd-find-window.c index 28a8949cd..0091ae907 100644 --- a/cmd-find-window.c +++ b/cmd-find-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-find-window.c,v 1.56 2023/12/27 20:42:01 nicm Exp $ */ +/* $OpenBSD: cmd-find-window.c,v 1.57 2026/07/14 17:17:17 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -109,7 +109,8 @@ cmd_find_window_exec(struct cmd *self, struct cmdq_item *item) args_set(new_args, 'Z', NULL, 0); args_set(new_args, 'f', filter, 0); - window_pane_set_mode(wp, NULL, &window_tree_mode, target, new_args); + window_pane_set_mode(wp, NULL, &window_tree_mode, item, target, + new_args); args_free(new_args); return (CMD_RETURN_NORMAL); diff --git a/cmd-queue.c b/cmd-queue.c index 463d82b3f..973b28e37 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-queue.c,v 1.120 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-queue.c,v 1.121 2026/07/14 17:17:17 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -148,6 +148,13 @@ cmdq_get_name(struct cmdq_item *item) return (item->name); } +/* Get item command. */ +struct cmd * +cmdq_get_cmd(struct cmdq_item *item) +{ + return (item->cmd); +} + /* Get item client. */ struct client * cmdq_get_client(struct cmdq_item *item) diff --git a/cmd-run-shell.c b/cmd-run-shell.c index b440828fb..7f5514475 100644 --- a/cmd-run-shell.c +++ b/cmd-run-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-run-shell.c,v 1.91 2026/06/01 08:27:37 nicm Exp $ */ +/* $OpenBSD: cmd-run-shell.c,v 1.92 2026/07/14 17:17:17 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -100,7 +100,8 @@ cmd_run_shell_print(struct job *job, const char *msg) wme = TAILQ_FIRST(&wp->modes); if (wme == NULL || wme->mode != &window_view_mode) - window_pane_set_mode(wp, NULL, &window_view_mode, NULL, NULL); + window_pane_set_mode(wp, NULL, &window_view_mode, NULL, NULL, + NULL); window_copy_add(wp, 1, "%s", msg); } diff --git a/layout.c b/layout.c index bddad3ab8..c3554718c 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout.c,v 1.94 2026/07/13 10:03:27 nicm Exp $ */ +/* $OpenBSD: layout.c,v 1.95 2026/07/14 17:17:17 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -370,13 +370,30 @@ layout_fix_offsets(struct window *w) layout_fix_offsets1(lc); } +static int +layout_cell_is_last_tiled(struct layout_cell *lc) +{ + struct layout_cell *lcchild, *lcparent = lc->parent; + + if (lcparent == NULL) + return (layout_cell_is_tiled(lc)); + + TAILQ_FOREACH_REVERSE(lcchild, &lcparent->cells, layout_cells, entry) { + if (layout_cell_is_tiled(lcchild) || + layout_cell_has_tiled_child(lcchild)) + break; + } + + return (lcchild == lc); +} + /* Is this a top cell? */ static int -layout_cell_is_top(struct window *w, struct layout_cell *lc) +layout_cell_is_top(struct layout_cell *root, struct layout_cell *lc) { struct layout_cell *next; - while (lc != w->layout_root) { + while (lc != root) { next = lc->parent; if (next == NULL) return (0); @@ -390,24 +407,17 @@ layout_cell_is_top(struct window *w, struct layout_cell *lc) /* Is this a bottom cell? */ static int -layout_cell_is_bottom(struct window *w, struct layout_cell *lc) +layout_cell_is_bottom(struct layout_cell *root, struct layout_cell *lc) { - struct layout_cell *next, *edge; + struct layout_cell *next; - while (lc != w->layout_root) { + while (lc != root) { next = lc->parent; if (next == NULL) return (0); - if (next->type == LAYOUT_TOPBOTTOM) { - edge = TAILQ_LAST(&next->cells, layout_cells); - while (edge != NULL) { - if (~edge->flags & LAYOUT_CELL_FLOATING) - break; - edge = TAILQ_PREV(edge, layout_cells, entry); - } - if (lc != edge) - return (0); - } + if (next->type == LAYOUT_TOPBOTTOM && + !layout_cell_is_last_tiled(lc)) + return (0); lc = next; } return (1); @@ -417,14 +427,14 @@ layout_cell_is_bottom(struct window *w, struct layout_cell *lc) * Returns 1 if we need to add an extra line for the pane status line. This is * the case for the most upper or lower panes only. */ -static int -layout_add_horizontal_border(struct window *w, struct layout_cell *lc, +int +layout_add_horizontal_border(struct layout_cell *root, struct layout_cell *lc, int status) { if (status == PANE_STATUS_TOP) - return (layout_cell_is_top(w, lc)); + return (layout_cell_is_top(root, lc)); if (status == PANE_STATUS_BOTTOM) - return (layout_cell_is_bottom(w, lc)); + return (layout_cell_is_bottom(root, lc)); return (0); } @@ -433,13 +443,11 @@ void layout_fix_panes(struct window *w, struct window_pane *skip) { struct window_pane *wp; - struct layout_cell *lc; + struct layout_cell *lc, *root = w->layout_root; int status, sb_w, sb_pad; int old_xoff, old_yoff, changed = 0; u_int sx, sy, old_sx, old_sy; - status = window_get_pane_status(w); - TAILQ_FOREACH(wp, &w->panes, entry) { if ((lc = wp->layout_cell) == NULL || wp == skip) continue; @@ -454,8 +462,9 @@ layout_fix_panes(struct window *w, struct window_pane *skip) sx = lc->g.sx; sy = lc->g.sy; + status = window_pane_get_pane_status(wp); if (!window_pane_is_floating(wp) && - layout_add_horizontal_border(w, lc, status)) { + layout_add_horizontal_border(root, lc, status)) { if (status == PANE_STATUS_TOP) wp->yoff++; if (sy > 1) @@ -523,7 +532,7 @@ static u_int layout_resize_check(struct window *w, struct layout_cell *lc, enum layout_type type) { - struct layout_cell *lcchild; + struct layout_cell *lcchild, *root = w->layout_root; struct style *sb_style = &w->active->scrollbar_style; u_int available, minimum; int status; @@ -541,7 +550,7 @@ layout_resize_check(struct window *w, struct layout_cell *lc, minimum = PANE_MINIMUM; } else { available = lc->g.sy; - if (layout_add_horizontal_border(w, lc, status)) + if (layout_add_horizontal_border(root, lc, status)) minimum = PANE_MINIMUM + 1; else minimum = PANE_MINIMUM; @@ -1256,9 +1265,10 @@ int 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->g.sx, sy = lc->g.sy; - int status; + struct layout_cell *root = wp->window->layout_root; + struct style *sb_style = &wp->scrollbar_style; + u_int minimum, sx = lc->g.sx, sy = lc->g.sy; + int status; if (lc->flags & LAYOUT_CELL_FLOATING) fatalx("floating cells cannot be split"); @@ -1276,7 +1286,7 @@ layout_split_check_space(struct window_pane *wp, struct layout_cell *lc, return (0); break; case LAYOUT_TOPBOTTOM: - if (layout_add_horizontal_border(wp->window, lc, status)) + if (layout_add_horizontal_border(root, lc, status)) minimum = PANE_MINIMUM * 2 + 2; else minimum = PANE_MINIMUM * 2 + 1; @@ -1509,7 +1519,7 @@ layout_close_pane(struct window_pane *wp) int layout_spread_cell(struct window *w, struct layout_cell *parent) { - struct layout_cell *lc; + struct layout_cell *lc, *root = w->layout_root; u_int number, each, size, this, remainder; int change, changed, status; @@ -1524,7 +1534,7 @@ layout_spread_cell(struct window *w, struct layout_cell *parent) if (parent->type == LAYOUT_LEFTRIGHT) size = parent->g.sx; else if (parent->type == LAYOUT_TOPBOTTOM) { - if (layout_add_horizontal_border(w, parent, status)) + if (layout_add_horizontal_border(root, parent, status)) size = parent->g.sy - 1; else size = parent->g.sy; @@ -1555,7 +1565,7 @@ layout_spread_cell(struct window *w, struct layout_cell *parent) } layout_resize_adjust(w, lc, LAYOUT_LEFTRIGHT, change); } else if (parent->type == LAYOUT_TOPBOTTOM) { - if (layout_add_horizontal_border(w, lc, status)) + if (layout_add_horizontal_border(root, lc, status)) this = each + 1; else this = each; diff --git a/options-table.c b/options-table.c index ddbb9404d..6a33b1151 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.236 2026/07/13 19:32:28 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.237 2026/07/14 17:17:17 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -815,40 +815,6 @@ const struct options_table_entry options_table[] = { "the client to another session if any exist." }, - { .name = "display-panes-active-colour", - .type = OPTIONS_TABLE_STRING, - .scope = OPTIONS_TABLE_SESSION, - .flags = OPTIONS_TABLE_IS_COLOUR, - .default_str = "themered", - .text = "Colour of the active pane for 'display-panes'." - }, - - { .name = "display-panes-colour", - .type = OPTIONS_TABLE_STRING, - .scope = OPTIONS_TABLE_SESSION, - .flags = OPTIONS_TABLE_IS_COLOUR, - .default_str = "themeblue", - .text = "Colour of not active panes for 'display-panes'." - }, - - { .name = "display-panes-format", - .type = OPTIONS_TABLE_STRING, - .scope = OPTIONS_TABLE_SESSION, - .default_str = "#[align=right]#{pane_width}x#{pane_height}", - .text = "Format of text shown by 'display-panes', expanded for each " - "pane." - }, - - { .name = "display-panes-time", - .type = OPTIONS_TABLE_NUMBER, - .scope = OPTIONS_TABLE_SESSION, - .minimum = 1, - .maximum = INT_MAX, - .default_num = 1000, - .unit = "milliseconds", - .text = "Time for which 'display-panes' should show pane numbers." - }, - { .name = "display-time", .type = OPTIONS_TABLE_NUMBER, .scope = OPTIONS_TABLE_SESSION, @@ -1355,6 +1321,49 @@ const struct options_table_entry options_table[] = { .text = "Time format of the clock in clock mode." }, + { .name = "display-panes-active-colour", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_WINDOW, + .flags = OPTIONS_TABLE_IS_COLOUR, + .default_str = "themered", + .text = "Colour of the active pane for 'display-panes'." + }, + + { .name = "display-panes-border-style", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_WINDOW, + .default_str = "bg=themedarkgrey,fg=themelightgrey", + .flags = OPTIONS_TABLE_IS_STYLE, + .separator = ",", + .text = "Style of the pane borders in 'display-panes'." + }, + + { .name = "display-panes-colour", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_WINDOW, + .flags = OPTIONS_TABLE_IS_COLOUR, + .default_str = "themeblue", + .text = "Colour of not active panes for 'display-panes'." + }, + + { .name = "display-panes-format", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_WINDOW, + .default_str = "#[align=right]#{pane_width}x#{pane_height}", + .text = "Format of text shown by 'display-panes', expanded for each " + "pane." + }, + + { .name = "display-panes-time", + .type = OPTIONS_TABLE_NUMBER, + .scope = OPTIONS_TABLE_WINDOW, + .minimum = 1, + .maximum = INT_MAX, + .default_num = 1000, + .unit = "milliseconds", + .text = "Time for which 'display-panes' should show pane numbers." + }, + { .name = "copy-mode-match-style", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW, diff --git a/server-client.c b/server-client.c index 1e85f0688..c78015f2c 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.489 2026/07/13 16:07:47 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.490 2026/07/14 17:17:17 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -3125,7 +3125,8 @@ server_client_print(struct client *c, int parse, struct evbuffer *evb) wp = server_client_get_pane(c); wme = TAILQ_FIRST(&wp->modes); if (wme == NULL || wme->mode != &window_view_mode) - window_pane_set_mode(wp, NULL, &window_view_mode, NULL, NULL); + window_pane_set_mode(wp, NULL, &window_view_mode, NULL, NULL, + NULL); if (parse) { do { line = evbuffer_readln(evb, NULL, EVBUFFER_EOL_LF); diff --git a/tmux.1 b/tmux.1 index ca0ba018b..a393c5954 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1139 2026/07/14 15:06:54 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1140 2026/07/14 17:17:18 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -3169,19 +3169,31 @@ option. .It Xo .Tg displayp .Ic display\-panes -.Op Fl bN +.Op Fl kNZ .Op Fl d Ar duration -.Op Fl t Ar target\-client +.Op Fl s Ar source\-window +.Op Fl t Ar target\-pane .Op Ar template .Xc .D1 Pq alias: Ic displayp -Display a visible indicator of each pane shown by -.Ar target\-client . +Put +.Ar target\-pane +into panes mode, which displays a preview of the window and a visible +indicator of each pane. +If +.Fl s +is given, the panes in +.Ar source\-window +are displayed instead of those in the window containing +.Ar target\-pane . See the -.Ic display\-panes\-colour +.Ic display\-panes\-colour , +.Ic display\-panes\-active\-colour , +.Ic display\-panes\-border\-style , +.Ic display\-panes\-format and -.Ic display\-panes\-active\-colour -session options. +.Ic display\-panes\-time +window options. The indicator is closed when a key is pressed (unless .Fl N is given) or @@ -3193,11 +3205,16 @@ is not given, .Ic display\-panes\-time is used. A duration of zero means the indicator stays until a key is pressed. +The mode is zoomed by default; +.Fl Z +starts it unzoomed. +.Fl k +kills the pane when the mode is exited. While the indicator is on screen, a pane may be chosen with the .Ql 0 to .Ql 9 -keys, which will cause +keys or by clicking it with the mouse, which will cause .Ar template to be executed as a command with .Ql %% @@ -3205,9 +3222,6 @@ substituted by the pane ID. The default .Ar template is "select\-pane \-t \[aq]%%\[aq]". -With -.Fl b , -other commands are not blocked from running until the indicator is closed. .Tg findw .It Xo Ic find\-window .Op Fl iCNrTZ @@ -5231,24 +5245,6 @@ If or .Ic next , the client is switched to the previous or next session in alphabetical order. -.It Ic display\-panes\-active\-colour Ar colour -Set the colour used by the -.Ic display\-panes -command to show the indicator for the active pane. -.It Ic display\-panes\-colour Ar colour -Set the colour used by the -.Ic display\-panes -command to show the indicators for inactive panes. -.It Ic display\-panes\-format Ar format -Set the -.Ar format -of the text shown by the -.Ic display\-panes -command, expanded for each pane. -.It Ic display\-panes\-time Ar time -Set the time in milliseconds for which the indicators shown by the -.Ic display\-panes -command appear. .It Ic display\-time Ar time Set the amount of time for which status line messages and other on-screen indicators are displayed. @@ -5680,6 +5676,38 @@ Set clock colour. .Xc Set clock hour format. .Pp +.It Ic display\-panes\-active\-colour Ar colour +Set the colour used by the +.Ic display\-panes +command to show the indicator for the active pane. +.Pp +.It Ic display\-panes\-border\-style Ar style +Set the style of borders in panes mode used by the +.Ic display\-panes +command. +For how to specify +.Ar style , +see the +.Sx STYLES +section. +.Pp +.It Ic display\-panes\-colour Ar colour +Set the colour used by the +.Ic display\-panes +command to show the indicators for inactive panes. +.Pp +.It Ic display\-panes\-format Ar format +Set the +.Ar format +of the text shown by the +.Ic display\-panes +command, expanded for each pane. +.Pp +.It Ic display\-panes\-time Ar time +Set the time in milliseconds for which the indicators shown by the +.Ic display\-panes +command appear. +.Pp .It Ic fill\-character Ar character Set the character used to fill areas of the terminal unused by a window. .Pp diff --git a/tmux.h b/tmux.h index fc9153e19..5ee3b26f6 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1402 2026/07/13 22:03:08 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1403 2026/07/14 17:17:18 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1133,8 +1133,13 @@ struct window_mode { const char *name; const char *default_format; + int flags; +#define WINDOW_MODE_HIDE_PANE_STATUS 0x1 +#define WINDOW_MODE_NO_STACK 0x2 + struct screen *(*init)(struct window_mode_entry *, - struct cmd_find_state *, struct args *); + struct cmdq_item *, struct cmd_find_state *, + struct args *); void (*free)(struct window_mode_entry *); void (*resize)(struct window_mode_entry *, u_int, u_int); void (*update)(struct window_mode_entry *); @@ -3109,6 +3114,7 @@ void cmdq_merge_formats(struct cmdq_item *, struct format_tree *); struct cmdq_list *cmdq_new(void); void cmdq_free(struct cmdq_list *); const char *cmdq_get_name(struct cmdq_item *); +struct cmd *cmdq_get_cmd(struct cmdq_item *); struct client *cmdq_get_client(struct cmdq_item *); struct client *cmdq_get_target_client(struct cmdq_item *); struct cmdq_state *cmdq_get_state(struct cmdq_item *); @@ -3661,7 +3667,7 @@ void window_pane_clear_resizes(struct window_pane *, struct window_pane_resize *); int window_pane_set_mode(struct window_pane *, struct window_pane *, const struct window_mode *, - struct cmd_find_state *, struct args *); + struct cmdq_item *, struct cmd_find_state *, struct args *); void window_pane_reset_mode(struct window_pane *); void window_pane_reset_mode_all(struct window_pane *); int window_pane_key(struct window_pane *, struct client *, @@ -3763,6 +3769,8 @@ 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 *); int layout_cell_is_tiled(struct layout_cell *); +int layout_add_horizontal_border(struct layout_cell *, + struct layout_cell *, int); void layout_fix_offsets(struct window *); void layout_fix_panes(struct window *, struct window_pane *); void layout_resize_adjust(struct window *, struct layout_cell *, @@ -3886,6 +3894,9 @@ extern const struct window_mode window_switch_mode; extern const struct window_mode window_clock_mode; extern const char window_clock_table[14][5][5]; +/* window-panes.c */ +extern const struct window_mode window_panes_mode; + /* window-client.c */ extern const struct window_mode window_client_mode; diff --git a/window-buffer.c b/window-buffer.c index f3116f8da..17eae8f3c 100644 --- a/window-buffer.c +++ b/window-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-buffer.c,v 1.49 2026/06/26 08:10:49 nicm Exp $ */ +/* $OpenBSD: window-buffer.c,v 1.50 2026/07/14 17:17:18 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -28,7 +28,8 @@ #include "tmux.h" static struct screen *window_buffer_init(struct window_mode_entry *, - struct cmd_find_state *, struct args *); + struct cmdq_item *, struct cmd_find_state *, + struct args *); static void window_buffer_free(struct window_mode_entry *); static void window_buffer_resize(struct window_mode_entry *, u_int, u_int); @@ -362,7 +363,8 @@ window_buffer_help(u_int *width, const char **item) } static struct screen * -window_buffer_init(struct window_mode_entry *wme, struct cmd_find_state *fs, +window_buffer_init(struct window_mode_entry *wme, + __unused struct cmdq_item *item, struct cmd_find_state *fs, struct args *args) { struct window_pane *wp = wme->wp; diff --git a/window-client.c b/window-client.c index 64e38cb81..b94c22119 100644 --- a/window-client.c +++ b/window-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-client.c,v 1.46 2026/07/04 22:09:06 nicm Exp $ */ +/* $OpenBSD: window-client.c,v 1.47 2026/07/14 17:17:18 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -26,7 +26,8 @@ #include "tmux.h" static struct screen *window_client_init(struct window_mode_entry *, - struct cmd_find_state *, struct args *); + struct cmdq_item *, struct cmd_find_state *, + struct args *); static void window_client_free(struct window_mode_entry *); static void window_client_resize(struct window_mode_entry *, u_int, u_int); @@ -421,7 +422,8 @@ window_client_help(u_int *width, const char **item) static struct screen * window_client_init(struct window_mode_entry *wme, - __unused struct cmd_find_state *fs, struct args *args) + __unused struct cmdq_item *item, __unused struct cmd_find_state *fs, + struct args *args) { struct window_pane *wp = wme->wp; struct window_client_modedata *data; diff --git a/window-clock.c b/window-clock.c index acd4382b3..2dc4ef332 100644 --- a/window-clock.c +++ b/window-clock.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-clock.c,v 1.34 2026/06/25 16:32:42 nicm Exp $ */ +/* $OpenBSD: window-clock.c,v 1.35 2026/07/14 17:17:18 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -25,7 +25,8 @@ #include "tmux.h" static struct screen *window_clock_init(struct window_mode_entry *, - struct cmd_find_state *, struct args *); + struct cmdq_item *, struct cmd_find_state *, + struct args *); static void window_clock_free(struct window_mode_entry *); static void window_clock_resize(struct window_mode_entry *, u_int, u_int); static void window_clock_key(struct window_mode_entry *, struct client *, @@ -169,7 +170,8 @@ window_clock_timer_callback(__unused int fd, __unused short events, void *arg) static struct screen * window_clock_init(struct window_mode_entry *wme, - __unused struct cmd_find_state *fs, __unused struct args *args) + __unused struct cmdq_item *item, __unused struct cmd_find_state *fs, + __unused struct args *args) { struct window_pane *wp = wme->wp; struct window_clock_mode_data *data; diff --git a/window-copy.c b/window-copy.c index 0991a387c..a6909a8d4 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.420 2026/07/14 15:06:54 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.421 2026/07/14 17:17:18 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -33,9 +33,9 @@ static void window_copy_command(struct window_mode_entry *, struct client *, struct session *, struct winlink *, struct args *, struct mouse_event *); static struct screen *window_copy_init(struct window_mode_entry *, - struct cmd_find_state *, struct args *); + struct cmdq_item *, struct cmd_find_state *, struct args *); static struct screen *window_copy_view_init(struct window_mode_entry *, - struct cmd_find_state *, struct args *); + struct cmdq_item *, struct cmd_find_state *, struct args *); static void window_copy_free(struct window_mode_entry *); static void window_copy_resize(struct window_mode_entry *, u_int, u_int); static void window_copy_formats(struct window_mode_entry *, @@ -597,7 +597,8 @@ window_copy_common_init(struct window_mode_entry *wme) static struct screen * window_copy_init(struct window_mode_entry *wme, - __unused struct cmd_find_state *fs, struct args *args) + __unused struct cmdq_item *item, __unused struct cmd_find_state *fs, + struct args *args) { struct window_pane *wp = wme->swp; struct window_copy_mode_data *data; @@ -648,7 +649,8 @@ window_copy_init(struct window_mode_entry *wme, static struct screen * window_copy_view_init(struct window_mode_entry *wme, - __unused struct cmd_find_state *fs, __unused struct args *args) + __unused struct cmdq_item *item, __unused struct cmd_find_state *fs, + __unused struct args *args) { struct window_pane *wp = wme->wp; struct window_copy_mode_data *data; diff --git a/window-customize.c b/window-customize.c index d4b7e7d86..4f5ff8a9f 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-customize.c,v 1.33 2026/07/13 22:03:08 nicm Exp $ */ +/* $OpenBSD: window-customize.c,v 1.34 2026/07/14 17:17:18 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -25,7 +25,8 @@ #include "tmux.h" static struct screen *window_customize_init(struct window_mode_entry *, - struct cmd_find_state *, struct args *); + struct cmdq_item *, struct cmd_find_state *, + struct args *); static void window_customize_free(struct window_mode_entry *); static void window_customize_resize(struct window_mode_entry *, u_int, u_int); @@ -1544,7 +1545,8 @@ window_customize_help(u_int *width, const char **item) } static struct screen * -window_customize_init(struct window_mode_entry *wme, struct cmd_find_state *fs, +window_customize_init(struct window_mode_entry *wme, + __unused struct cmdq_item *item, struct cmd_find_state *fs, struct args *args) { struct window_pane *wp = wme->wp; diff --git a/window-panes.c b/window-panes.c new file mode 100644 index 000000000..c262c82c3 --- /dev/null +++ b/window-panes.c @@ -0,0 +1,1067 @@ +/* $OpenBSD: window-panes.c,v 1.1 2026/07/14 17:17:18 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 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 struct screen *window_panes_init(struct window_mode_entry *, + struct cmdq_item *, struct cmd_find_state *, + struct args *); +static void window_panes_free(struct window_mode_entry *); +static void window_panes_resize(struct window_mode_entry *, u_int, + u_int); +static void window_panes_key(struct window_mode_entry *, + struct client *, struct session *, + struct winlink *, key_code, struct mouse_event *); + +const struct window_mode window_panes_mode = { + .name = "panes-mode", + .flags = WINDOW_MODE_HIDE_PANE_STATUS|WINDOW_MODE_NO_STACK, + + .init = window_panes_init, + .free = window_panes_free, + .resize = window_panes_resize, + .key = window_panes_key, +}; + +struct window_panes_area { + u_int id; + u_int x; + u_int y; + u_int sx; + u_int sy; +}; + +struct window_panes_modedata { + struct window_pane *wp; + struct session *session; + u_int source_session; + u_int source_window; + struct screen screen; + + struct event timer; + + struct args_command_state *state; + u_int delay; + int ignore_keys; + int zoomed; + + struct window_panes_area *areas; + u_int areas_size; +}; + +#define WINDOW_PANES_BORDER_L 0x1 +#define WINDOW_PANES_BORDER_R 0x2 +#define WINDOW_PANES_BORDER_U 0x4 +#define WINDOW_PANES_BORDER_D 0x8 + +static int +window_panes_get_source(struct window_panes_modedata *data, struct session **sp, + struct winlink **wlp, struct window **wp) +{ + struct session *s; + struct window *w; + struct winlink *wl = NULL; + + w = window_find_by_id(data->source_window); + if (w == NULL) + return (0); + + s = session_find_by_id(data->source_session); + if (s != NULL) + wl = winlink_find_by_window(&s->windows, w); + if (wl == NULL) + s = data->session; + if (s != NULL) + wl = winlink_find_by_window(&s->windows, w); + + if (sp != NULL) + *sp = s; + if (wlp != NULL) + *wlp = wl; + if (wp != NULL) + *wp = w; + return (1); +} + +static void +window_panes_free_areas(struct window_panes_modedata *data) +{ + free(data->areas); + data->areas = NULL; + data->areas_size = 0; +} + +static void +window_panes_add_area(struct window_panes_modedata *data, + struct window_pane *wp, u_int x, u_int y, u_int sx, u_int sy) +{ + struct window_panes_area *area; + + data->areas = xreallocarray(data->areas, data->areas_size + 1, + sizeof *data->areas); + area = &data->areas[data->areas_size++]; + area->id = wp->id; + area->x = x; + area->y = y; + area->sx = sx; + area->sy = sy; +} + +static int +window_panes_pane_floating(struct window_pane *wp) +{ + struct layout_cell *lc = wp->saved_layout_cell; + + if (lc == NULL) + lc = wp->layout_cell; + if (lc == NULL || (~lc->flags & LAYOUT_CELL_FLOATING)) + return (0); + return (1); +} + +static int +window_panes_pane_visible(struct window_pane *wp) +{ + if (wp->saved_layout_cell != NULL) + return (1); + return (window_pane_is_visible(wp)); +} + +static int +window_panes_get_geometry(struct window_pane *wp, struct layout_cell *root, + u_int osx, u_int osy, u_int dsx, u_int dsy, u_int *xp, u_int *yp, + u_int *sxp, u_int *syp) +{ + struct layout_cell *lc = wp->saved_layout_cell; + int status; + u_int x, y, sx, sy, x2, y2; + + if (lc == NULL) + lc = wp->layout_cell; + if (lc == NULL || osx == 0 || osy == 0 || dsx == 0 || dsy == 0) + return (0); + + if (osx <= dsx && osy <= dsy) { + x = lc->g.xoff; + y = lc->g.yoff; + x2 = x + lc->g.sx; + y2 = y + lc->g.sy; + } else { + x = (u_int)lc->g.xoff * dsx / osx; + y = (u_int)lc->g.yoff * dsy / osy; + x2 = ((u_int)lc->g.xoff + lc->g.sx) * dsx / osx; + y2 = ((u_int)lc->g.yoff + lc->g.sy) * dsy / osy; + } + + if (x >= dsx || y >= dsy) + return (0); + if (x2 <= x) + x2 = x + 1; + if (y2 <= y) + y2 = y + 1; + if (x2 > dsx) + x2 = dsx; + if (y2 > dsy) + y2 = dsy; + + sx = x2 - x; + sy = y2 - y; + if (sx == 0 || sy == 0) + return (0); + + status = window_get_pane_status(wp->window); + if (layout_add_horizontal_border(root, lc, status) && sy > 1) { + if (status == PANE_STATUS_TOP) + y++; + sy--; + } + + *xp = x; + *yp = y; + *sxp = sx; + *syp = sy; + return (1); +} + +static void +window_panes_get_border_cell(struct window_panes_modedata *data, + struct grid_cell *gc) +{ + struct format_tree *ft; + struct window_pane *wp = data->wp; + struct session *s = data->session; + + memcpy(gc, &grid_default_cell, sizeof *gc); + ft = format_create_defaults(NULL, NULL, s, s->curw, wp); + style_apply(gc, wp->window->options, "display-panes-border-style", ft); + format_free(ft); +} + +static int +window_panes_map_x(u_int x, u_int osx, u_int dsx) +{ + if (osx <= dsx) + return (x); + return (x * dsx / osx); +} + +static int +window_panes_map_y(u_int y, u_int osy, u_int dsy) +{ + if (osy <= dsy) + return (y); + return (y * dsy / osy); +} + +static struct layout_cell * +window_panes_next_tiled_cell(struct layout_cell *lc) +{ + struct layout_cell *next; + + next = TAILQ_NEXT(lc, entry); + while (next != NULL) { + if (~next->flags & LAYOUT_CELL_FLOATING) + return (next); + next = TAILQ_NEXT(next, entry); + } + return (NULL); +} + +static void +window_panes_mark_border(u_char *map, u_int dsx, u_int dsy, u_int x, u_int y, + u_char mask) +{ + if (x < dsx && y < dsy) + map[y * dsx + x] |= mask; +} + +static void +window_panes_mark_vline(u_char *map, u_int dsx, u_int dsy, int x, int y, int y2) +{ + u_char mask; + int yy; + + if (x < 0 || (u_int)x >= dsx || y2 <= y) + return; + if (y < 0) + y = 0; + if ((u_int)y2 > dsy) + y2 = dsy; + + for (yy = y; yy < y2; yy++) { + mask = 0; + if (yy > y) + mask |= WINDOW_PANES_BORDER_U; + if (yy + 1 < y2) + mask |= WINDOW_PANES_BORDER_D; + if (mask == 0) + mask = WINDOW_PANES_BORDER_U|WINDOW_PANES_BORDER_D; + window_panes_mark_border(map, dsx, dsy, x, yy, mask); + } +} + +static void +window_panes_mark_hline(u_char *map, u_int dsx, u_int dsy, int x, int x2, int y) +{ + u_char mask; + int xx; + + if (y < 0 || (u_int)y >= dsy || x2 <= x) + return; + if (x < 0) + x = 0; + if ((u_int)x2 > dsx) + x2 = dsx; + + for (xx = x; xx < x2; xx++) { + mask = 0; + if (xx > x) + mask |= WINDOW_PANES_BORDER_L; + if (xx + 1 < x2) + mask |= WINDOW_PANES_BORDER_R; + if (mask == 0) + mask = WINDOW_PANES_BORDER_L|WINDOW_PANES_BORDER_R; + window_panes_mark_border(map, dsx, dsy, xx, y, mask); + } +} + +static void +window_panes_mark_borders_cell(u_char *map, struct layout_cell *lc, u_int osx, + u_int osy, u_int dsx, u_int dsy) +{ + struct layout_cell *lcchild, *lcnext; + int x, y, x2, y2; + + if (lc->type == LAYOUT_WINDOWPANE) + return; + + TAILQ_FOREACH(lcchild, &lc->cells, entry) { + window_panes_mark_borders_cell(map, lcchild, osx, osy, dsx, + dsy); + if (lcchild->flags & LAYOUT_CELL_FLOATING) + continue; + lcnext = window_panes_next_tiled_cell(lcchild); + if (lcnext == NULL) + continue; + + if (lc->type == LAYOUT_LEFTRIGHT) { + x = window_panes_map_x(lcchild->g.xoff + lcchild->g.sx, + osx, dsx); + y = window_panes_map_y(lc->g.yoff, osy, dsy); + y2 = window_panes_map_y(lc->g.yoff + lc->g.sy, osy, + dsy); + window_panes_mark_vline(map, dsx, dsy, x, y, y2); + } else { + x = window_panes_map_x(lc->g.xoff, osx, dsx); + x2 = window_panes_map_x(lc->g.xoff + lc->g.sx, osx, + dsx); + y = window_panes_map_y(lcchild->g.yoff + lcchild->g.sy, + osy, dsy); + window_panes_mark_hline(map, dsx, dsy, x, x2, y); + } + } +} + +static void +window_panes_mark_pane_status_borders(u_char *map, struct window *w, + struct layout_cell *root, u_int osx, u_int osy, u_int dsx, u_int dsy) +{ + struct window_pane *wp; + struct layout_cell *lc; + int status, x, y, x2, y2; + + status = window_get_pane_status(w); + if (status != PANE_STATUS_TOP && status != PANE_STATUS_BOTTOM) + return; + + TAILQ_FOREACH(wp, &w->panes, entry) { + if (!window_panes_pane_visible(wp)) + continue; + + lc = wp->saved_layout_cell; + if (lc == NULL) + lc = wp->layout_cell; + if (lc == NULL || + !layout_add_horizontal_border(root, lc, status)) + continue; + + x = window_panes_map_x(lc->g.xoff, osx, dsx); + x2 = window_panes_map_x(lc->g.xoff + lc->g.sx, osx, dsx); + if (status == PANE_STATUS_TOP) + y = window_panes_map_y(lc->g.yoff, osy, dsy); + else { + y2 = window_panes_map_y(lc->g.yoff + lc->g.sy, osy, + dsy); + y = y2 - 1; + } + window_panes_mark_hline(map, dsx, dsy, x, x2, y); + } +} + +static int +window_panes_get_floating_frame(struct window_pane *wp, u_int osx, u_int osy, + u_int dsx, u_int dsy, int *xp, int *yp, int *x2p, int *y2p) +{ + struct layout_cell *lc; + + lc = wp->saved_layout_cell; + if (lc == NULL) + lc = wp->layout_cell; + if (lc == NULL || (~lc->flags & LAYOUT_CELL_FLOATING)) + return (0); + + if (lc->g.xoff == 0) + *xp = -1; + else + *xp = window_panes_map_x(lc->g.xoff - 1, osx, dsx); + if (lc->g.yoff == 0) + *yp = -1; + else + *yp = window_panes_map_y(lc->g.yoff - 1, osy, dsy); + *x2p = window_panes_map_x(lc->g.xoff + lc->g.sx, osx, dsx); + *y2p = window_panes_map_y(lc->g.yoff + lc->g.sy, osy, dsy); + return (1); +} + +static int +window_panes_clip_floating_body(struct window_pane *wp, u_int osx, u_int osy, + u_int dsx, u_int dsy, u_int *xp, u_int *yp, u_int *sxp, u_int *syp) +{ + int x, y, x2, y2, bx, by, bx2, by2; + + if (!window_panes_get_floating_frame(wp, osx, osy, dsx, dsy, &x, &y, + &x2, &y2)) + return (1); + + bx = *xp; + by = *yp; + bx2 = bx + *sxp - 1; + by2 = by + *syp - 1; + + if (x >= 0 && bx <= x) + bx = x + 1; + if (y >= 0 && by <= y) + by = y + 1; + if ((u_int)x2 < dsx && bx2 >= x2) + bx2 = x2 - 1; + if ((u_int)y2 < dsy && by2 >= y2) + by2 = y2 - 1; + if (bx2 < bx || by2 < by) + return (0); + + *xp = bx; + *yp = by; + *sxp = bx2 - bx + 1; + *syp = by2 - by + 1; + return (1); +} + +static int +window_panes_border_cell_type(u_char mask) +{ + switch (mask) { + case WINDOW_PANES_BORDER_L|WINDOW_PANES_BORDER_R| + WINDOW_PANES_BORDER_U|WINDOW_PANES_BORDER_D: + return (CELL_LRUD); + case WINDOW_PANES_BORDER_L|WINDOW_PANES_BORDER_R|WINDOW_PANES_BORDER_U: + return (CELL_LRU); + case WINDOW_PANES_BORDER_L|WINDOW_PANES_BORDER_R|WINDOW_PANES_BORDER_D: + return (CELL_LRD); + case WINDOW_PANES_BORDER_L|WINDOW_PANES_BORDER_R: + case WINDOW_PANES_BORDER_L: + case WINDOW_PANES_BORDER_R: + return (CELL_LR); + case WINDOW_PANES_BORDER_L|WINDOW_PANES_BORDER_U|WINDOW_PANES_BORDER_D: + return (CELL_ULD); + case WINDOW_PANES_BORDER_L|WINDOW_PANES_BORDER_U: + return (CELL_LU); + case WINDOW_PANES_BORDER_L|WINDOW_PANES_BORDER_D: + return (CELL_LD); + case WINDOW_PANES_BORDER_R|WINDOW_PANES_BORDER_U|WINDOW_PANES_BORDER_D: + return (CELL_URD); + case WINDOW_PANES_BORDER_R|WINDOW_PANES_BORDER_U: + return (CELL_RU); + case WINDOW_PANES_BORDER_R|WINDOW_PANES_BORDER_D: + return (CELL_RD); + case WINDOW_PANES_BORDER_U|WINDOW_PANES_BORDER_D: + case WINDOW_PANES_BORDER_U: + case WINDOW_PANES_BORDER_D: + return (CELL_UD); + } + return (CELL_NONE); +} + +static int +window_panes_border_has_horizontal(u_char mask) +{ + return ((mask & (WINDOW_PANES_BORDER_L|WINDOW_PANES_BORDER_R)) != 0); +} + +static int +window_panes_border_has_vertical(u_char mask) +{ + return ((mask & (WINDOW_PANES_BORDER_U|WINDOW_PANES_BORDER_D)) != 0); +} + +static void +window_panes_mark_border_joins_cell(u_char *map, struct layout_cell *lc, + u_int osx, u_int osy, u_int dsx, u_int dsy) +{ + struct layout_cell *lcchild, *lcnext; + int x, y, x2, y2; + + if (lc->type == LAYOUT_WINDOWPANE) + return; + + TAILQ_FOREACH(lcchild, &lc->cells, entry) { + window_panes_mark_border_joins_cell(map, lcchild, osx, osy, + dsx, dsy); + if (lcchild->flags & LAYOUT_CELL_FLOATING) + continue; + lcnext = window_panes_next_tiled_cell(lcchild); + if (lcnext == NULL) + continue; + + if (lc->type == LAYOUT_LEFTRIGHT) { + x = window_panes_map_x(lcchild->g.xoff + lcchild->g.sx, + osx, dsx); + y = window_panes_map_y(lc->g.yoff, osy, dsy); + y2 = window_panes_map_y(lc->g.yoff + lc->g.sy, osy, + dsy); + if (x < 0 || (u_int)x >= dsx) + continue; + if (y > 0 && + window_panes_border_has_horizontal( + map[(y - 1) * dsx + x])) { + window_panes_mark_border(map, dsx, dsy, x, + y - 1, WINDOW_PANES_BORDER_D); + window_panes_mark_border(map, dsx, dsy, x, y, + WINDOW_PANES_BORDER_U); + } + if ((u_int)y2 < dsy && + window_panes_border_has_horizontal( + map[y2 * dsx + x])) { + window_panes_mark_border(map, dsx, dsy, x, y2, + WINDOW_PANES_BORDER_U); + window_panes_mark_border(map, dsx, dsy, x, + y2 - 1, WINDOW_PANES_BORDER_D); + } + } else { + x = window_panes_map_x(lc->g.xoff, osx, dsx); + x2 = window_panes_map_x(lc->g.xoff + lc->g.sx, osx, + dsx); + y = window_panes_map_y(lcchild->g.yoff + lcchild->g.sy, + osy, dsy); + if (y < 0 || (u_int)y >= dsy) + continue; + if (x > 0 && + window_panes_border_has_vertical( + map[y * dsx + x - 1])) { + window_panes_mark_border(map, dsx, dsy, x - 1, + y, WINDOW_PANES_BORDER_R); + window_panes_mark_border(map, dsx, dsy, x, y, + WINDOW_PANES_BORDER_L); + } + if ((u_int)x2 < dsx && + window_panes_border_has_vertical( + map[y * dsx + x2])) { + window_panes_mark_border(map, dsx, dsy, x2, y, + WINDOW_PANES_BORDER_L); + window_panes_mark_border(map, dsx, dsy, x2 - 1, + y, WINDOW_PANES_BORDER_R); + } + } + } +} + +static void +window_panes_draw_borders(struct screen_write_ctx *ctx, struct window *w, + struct layout_cell *lc, const struct grid_cell *gc, u_int osx, u_int osy, + u_int dsx, u_int dsy) +{ + struct grid_cell border_gc; + u_char *map; + u_int xx, yy; + int cell_type; + + if (dsx == 0 || dsy == 0) + return; + + map = xcalloc(dsx, dsy); + window_panes_mark_borders_cell(map, lc, osx, osy, dsx, dsy); + window_panes_mark_pane_status_borders(map, w, lc, osx, osy, dsx, + dsy); + window_panes_mark_border_joins_cell(map, lc, osx, osy, dsx, dsy); + + for (yy = 0; yy < dsy; yy++) { + for (xx = 0; xx < dsx; xx++) { + if (map[yy * dsx + xx] == 0) + continue; + cell_type = window_panes_border_cell_type( + map[yy * dsx + xx]); + memcpy(&border_gc, gc, sizeof border_gc); + border_gc.attr |= GRID_ATTR_CHARSET; + utf8_set(&border_gc.data, CELL_BORDERS[cell_type]); + screen_write_cursormove(ctx, xx, yy, 0); + screen_write_cell(ctx, &border_gc); + } + } + free(map); +} + +static void +window_panes_draw_floating_border(struct screen_write_ctx *ctx, + struct window_pane *wp, const struct grid_cell *gc, u_int osx, u_int osy, + u_int dsx, u_int dsy) +{ + struct grid_cell border_gc; + u_char *map; + u_int xx, yy; + int x, y, x2, y2, cell_type; + + if (dsx == 0 || dsy == 0) + return; + if (!window_panes_get_floating_frame(wp, osx, osy, dsx, dsy, &x, &y, + &x2, &y2)) + return; + + map = xcalloc(dsx, dsy); + window_panes_mark_hline(map, dsx, dsy, x, x2 + 1, y); + window_panes_mark_hline(map, dsx, dsy, x, x2 + 1, y2); + window_panes_mark_vline(map, dsx, dsy, x, y, y2 + 1); + window_panes_mark_vline(map, dsx, dsy, x2, y, y2 + 1); + + for (yy = 0; yy < dsy; yy++) { + for (xx = 0; xx < dsx; xx++) { + if (map[yy * dsx + xx] == 0) + continue; + cell_type = window_panes_border_cell_type( + map[yy * dsx + xx]); + memcpy(&border_gc, gc, sizeof border_gc); + border_gc.attr |= GRID_ATTR_CHARSET; + utf8_set(&border_gc.data, CELL_BORDERS[cell_type]); + screen_write_cursormove(ctx, xx, yy, 0); + screen_write_cell(ctx, &border_gc); + } + } + free(map); +} + +static void +window_panes_clear_floating_area(struct screen_write_ctx *ctx, + struct window_pane *wp, u_int osx, u_int osy, u_int dsx, u_int dsy) +{ + struct grid_cell gc; + int x, y, x2, y2, xx, yy; + + if (!window_panes_get_floating_frame(wp, osx, osy, dsx, dsy, &x, &y, + &x2, &y2)) + return; + + memcpy(&gc, &grid_default_cell, sizeof gc); + if (x < 0) + x = 0; + if (y < 0) + y = 0; + if ((u_int)x2 >= dsx) + x2 = dsx - 1; + if ((u_int)y2 >= dsy) + y2 = dsy - 1; + if (x2 < x || y2 < y) + return; + + for (yy = y; yy <= y2; yy++) { + screen_write_cursormove(ctx, x, yy, 0); + for (xx = x; xx <= x2; xx++) + screen_write_putc(ctx, &gc, ' '); + } +} + +static void +window_panes_draw_format(struct window_panes_modedata *data, + struct screen_write_ctx *ctx, struct window_pane *wp, u_int x, u_int y, + u_int sx, const struct grid_cell *gc) +{ + struct session *s = data->session; + struct winlink *wl = s->curw; + const char *format; + char *expanded; + + if (sx == 0) + return; + format = options_get_string(data->wp->window->options, + "display-panes-format"); + if (*format == '\0') + return; + + window_panes_get_source(data, &s, &wl, NULL); + if (s == NULL) + return; + + expanded = format_single(NULL, format, NULL, s, wl, wp); + if (*expanded != '\0') { + screen_write_cursormove(ctx, x, y, 0); + format_draw(ctx, gc, sx, expanded, NULL, 0); + } + free(expanded); +} + +static void +window_panes_draw_number(struct window_panes_modedata *data, + struct screen_write_ctx *ctx, struct window_pane *wp, u_int pane, u_int x, + u_int y, u_int sx, u_int sy) +{ + struct session *s = data->session; + struct window *w = wp->window; + struct winlink *wl = s->curw; + struct options *oo = data->wp->window->options; + struct grid_cell fgc, bgc; + struct format_tree *ft; + const char *name; + char buf[16], lbuf[16] = { 0 }, *ptr; + size_t len, llen = 0, width; + u_int cx, cy, px, py, idx, i, j, format; + + len = xsnprintf(buf, sizeof buf, "%u", pane); + if (pane > 9 && pane < 35) + llen = xsnprintf(lbuf, sizeof lbuf, "%c", 'a' + (pane - 10)); + if (sx < len) + return; + + window_panes_get_source(data, &s, &wl, NULL); + if (s != NULL) { + if (wl == NULL) + wl = s->curw; + } + if (w->active == wp) + name = "display-panes-active-colour"; + else + name = "display-panes-colour"; + ft = format_create_defaults(NULL, NULL, s, wl, wp); + style_apply(&fgc, oo, name, ft); + format_free(ft); + + memcpy(&bgc, &grid_default_cell, sizeof bgc); + bgc.bg = fgc.fg; + + format = 0; + if (options_get_string(oo, "display-panes-format")[0] != '\0') + format = 1; + width = (len * 6) - 1; + if (sx < width || sy < (format ? 7 : 5)) { + width = len; + if (llen != 0 && sx >= len + llen + 1) + width += llen + 1; + cx = x + (sx - width) / 2; + cy = y + sy / 2; + screen_write_cursormove(ctx, cx, cy, 0); + screen_write_puts(ctx, &fgc, "%s", buf); + if (width > len) + screen_write_puts(ctx, &fgc, " %s", lbuf); + if (format && sy > 1) + window_panes_draw_format(data, ctx, wp, x, y, sx, &fgc); + return; + } + + px = (sx - width) / 2; + py = (sy - 5) / 2; + for (ptr = buf; *ptr != '\0'; ptr++) { + if (*ptr < '0' || *ptr > '9') + continue; + idx = *ptr - '0'; + + for (j = 0; j < 5; j++) { + for (i = 0; i < 5; i++) { + if (!window_clock_table[idx][j][i]) + continue; + screen_write_cursormove(ctx, x + px + i, + y + py + j, 0); + screen_write_putc(ctx, &bgc, ' '); + } + } + px += 6; + } + + if (sy <= 6) + return; + window_panes_draw_format(data, ctx, wp, x, y, sx, &fgc); + if (llen != 0) { + cx = x + px - llen - 1; + cy = y + py + 5; + screen_write_cursormove(ctx, cx, cy, 0); + screen_write_puts(ctx, &fgc, "%s", lbuf); + } +} + +static void +window_panes_draw_pane(struct window_panes_modedata *data, + struct screen_write_ctx *ctx, struct window_pane *wp, + struct layout_cell *root, u_int osx, u_int osy, u_int dsx, u_int dsy) +{ + u_int pane, x, y, sx, sy; + + if (!window_panes_pane_visible(wp)) + return; + if (!window_panes_get_geometry(wp, root, osx, osy, dsx, dsy, &x, &y, + &sx, &sy)) + return; + if (!window_panes_clip_floating_body(wp, osx, osy, dsx, dsy, &x, &y, + &sx, &sy)) + return; + if (window_pane_index(wp, &pane) != 0) + return; + window_panes_add_area(data, wp, x, y, sx, sy); + + screen_write_cursormove(ctx, x, y, 0); + if (osx <= dsx && osy <= dsy) + screen_write_fast_copy(ctx, &wp->base, 0, wp->base.grid->hsize, + sx, sy); + else + screen_write_preview(ctx, &wp->base, sx, sy); + window_panes_draw_number(data, ctx, wp, pane, x, y, sx, sy); +} + +static void +window_panes_draw_screen(struct window_mode_entry *wme) +{ + struct window_panes_modedata *data = wme->data; + struct window *w; + struct window_pane *wp; + struct screen_write_ctx ctx; + struct layout_cell *root; + struct grid_cell border_gc; + u_int osx, osy, sx, sy; + + if (!window_panes_get_source(data, NULL, NULL, &w)) + return; + root = w->saved_layout_root; + if (root == NULL) + root = w->layout_root; + if (root == NULL) + return; + + osx = root->g.sx; + osy = root->g.sy; + sx = screen_size_x(&data->screen); + sy = screen_size_y(&data->screen); + + window_panes_free_areas(data); + screen_write_start(&ctx, &data->screen); + screen_write_clearscreen(&ctx, 8); + TAILQ_FOREACH(wp, &w->panes, entry) { + if (window_panes_pane_floating(wp)) + continue; + window_panes_draw_pane(data, &ctx, wp, root, osx, osy, sx, sy); + } + window_panes_get_border_cell(data, &border_gc); + window_panes_draw_borders(&ctx, w, root, &border_gc, osx, osy, sx, sy); + TAILQ_FOREACH_REVERSE(wp, &w->z_index, window_panes_zindex, zentry) { + if (!window_panes_pane_floating(wp)) + continue; + window_panes_clear_floating_area(&ctx, wp, osx, osy, sx, sy); + window_panes_draw_pane(data, &ctx, wp, root, osx, osy, sx, sy); + window_panes_draw_floating_border(&ctx, wp, &border_gc, osx, + osy, sx, sy); + } + screen_write_stop(&ctx); + + data->wp->flags |= PANE_REDRAW; +} + +static void +window_panes_timer_callback(__unused int fd, __unused short events, void *arg) +{ + struct window_mode_entry *wme = arg; + + window_pane_reset_mode(wme->wp); +} + +static struct screen * +window_panes_init(struct window_mode_entry *wme, struct cmdq_item *item, + __unused struct cmd_find_state *fs, struct args *args) +{ + struct window_pane *wp = wme->wp; + struct window *w = wp->window; + struct window_panes_modedata *data; + struct cmd *self; + struct cmd_find_state *source, *target; + struct session *s; + struct timeval tv; + char *cause; + u_int sx = screen_size_x(&wp->base); + u_int sy = screen_size_y(&wp->base); + u_int delay; + + if (item == NULL) + return (NULL); + self = cmdq_get_cmd(item); + if (self == NULL) + return (NULL); + + source = cmdq_get_source(item); + target = cmdq_get_target(item); + s = target->s; + + if (!args_has(args, 'd')) + delay = options_get_number(w->options, "display-panes-time"); + else { + delay = args_strtonum(args, 'd', 0, UINT_MAX, &cause); + if (cause != NULL) { + cmdq_error(item, "delay %s", cause); + free(cause); + return (NULL); + } + } + + wme->data = data = xcalloc(1, sizeof *data); + data->wp = wp; + data->session = s; + + screen_init(&data->screen, sx, sy, 0); + data->screen.mode &= ~MODE_CURSOR; + + data->state = args_make_commands_prepare(self, item, 0, + "select-pane -t \"%%%\"", 0, 0); + if (args_has(args, 's')) { + data->source_session = source->s->id; + data->source_window = source->w->id; + } else { + data->source_session = target->s->id; + data->source_window = target->w->id; + } + + data->delay = delay; + data->ignore_keys = args_has(args, 'N'); + + if (args_has(args, 'Z')) + data->zoomed = -1; + else { + data->zoomed = (w->flags & WINDOW_ZOOMED); + if (!data->zoomed && window_zoom(wp) == 0) + server_redraw_window(w); + } + + evtimer_set(&data->timer, window_panes_timer_callback, wme); + if (data->delay != 0) { + tv.tv_sec = data->delay / 1000; + tv.tv_usec = (data->delay % 1000) * 1000; + evtimer_add(&data->timer, &tv); + } + + window_panes_draw_screen(wme); + return (&data->screen); +} + +static void +window_panes_free(struct window_mode_entry *wme) +{ + struct window_panes_modedata *data = wme->data; + struct window *w = wme->wp->window; + + evtimer_del(&data->timer); + + if (data->zoomed == 0) + server_unzoom_window(w); + server_redraw_window(w); + server_redraw_window_borders(w); + server_status_window(w); + + if (data->state != NULL) + args_make_commands_free(data->state); + window_panes_free_areas(data); + screen_free(&data->screen); + free(data); +} + +static void +window_panes_resize(struct window_mode_entry *wme, u_int sx, u_int sy) +{ + struct window_panes_modedata *data = wme->data; + + screen_resize(&data->screen, sx, sy, 0); + window_panes_draw_screen(wme); +} + +static void +window_panes_run_command(struct window_panes_modedata *data, struct client *c, + struct window_pane *wp) +{ + struct cmdq_item *new_item; + struct cmd_list *cmdlist; + char *expanded, *error; + + xasprintf(&expanded, "%%%u", wp->id); + cmdlist = args_make_commands(data->state, 1, &expanded, &error); + if (cmdlist == NULL) { + cmdq_append(c, cmdq_get_error(error)); + free(error); + } else { + new_item = cmdq_get_command(cmdlist, NULL); + cmdq_append(c, new_item); + } + free(expanded); +} + +static struct window_pane * +window_panes_find_pane(struct window_panes_modedata *data, u_int x, u_int y) +{ + struct window_panes_area *area; + u_int i; + + for (i = data->areas_size; i > 0; i--) { + area = &data->areas[i - 1]; + if (x < area->x || x >= area->x + area->sx) + continue; + if (y < area->y || y >= area->y + area->sy) + continue; + return (window_pane_find_by_id(area->id)); + } + return (NULL); +} + +static struct window_pane * +window_panes_key_pane(struct window_panes_modedata *data, key_code key) +{ + struct window *w; + u_int index; + + if (key >= '0' && key <= '9') + index = key - '0'; + else if ((key & KEYC_MASK_MODIFIERS) == 0) { + key &= KEYC_MASK_KEY; + if (key < 'a' || key > 'z') + return (NULL); + index = 10 + (key - 'a'); + } else + return (NULL); + if (!window_panes_get_source(data, NULL, NULL, &w)) + return (NULL); + return (window_pane_at_index(w, index)); +} + +static struct window_pane * +window_panes_get_target(struct window_mode_entry *wme, key_code key, + struct mouse_event *m) +{ + struct window_panes_modedata *data = wme->data; + u_int x, y; + + if (data->ignore_keys) + return (NULL); + + if (KEYC_IS_MOUSE(key)) { + if (key != KEYC_MOUSEDOWN1_PANE || + m == NULL || + cmd_mouse_at(wme->wp, m, &x, &y, 0) != 0) + return (NULL); + return (window_panes_find_pane(data, x, y)); + } + + return (window_panes_key_pane(data, key)); +} + +static void +window_panes_key(struct window_mode_entry *wme, struct client *c, + __unused struct session *s, __unused struct winlink *wl, key_code key, + struct mouse_event *m) +{ + struct window_pane *wp = wme->wp, *target = NULL; + struct window_panes_modedata *data = wme->data; + + if (key == '\033' || key == 'q') { + window_pane_reset_mode(wp); + return; + } + + target = window_panes_get_target(wme, key, m); + if (target == NULL) { + if (!data->ignore_keys && !KEYC_IS_MOUSE(key)) + window_pane_reset_mode(wp); + return; + } + + if (wp->window->flags & WINDOW_ZOOMED) + window_unzoom(wp->window, 1); + window_panes_run_command(data, c, target); + window_pane_reset_mode(wp); +} diff --git a/window-switch.c b/window-switch.c index d458f30af..6712d8a89 100644 --- a/window-switch.c +++ b/window-switch.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-switch.c,v 1.1 2026/06/26 14:40:30 nicm Exp $ */ +/* $OpenBSD: window-switch.c,v 1.2 2026/07/14 17:17:18 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -25,7 +25,8 @@ #include "tmux.h" static struct screen *window_switch_init(struct window_mode_entry *, - struct cmd_find_state *, struct args *); + struct cmdq_item *, struct cmd_find_state *, + struct args *); static void window_switch_free(struct window_mode_entry *); static void window_switch_resize(struct window_mode_entry *, u_int, u_int); @@ -333,7 +334,8 @@ window_switch_draw_screen(struct window_mode_entry *wme) static struct screen * window_switch_init(struct window_mode_entry *wme, - struct cmd_find_state *fs, struct args *args) + __unused struct cmdq_item *item, struct cmd_find_state *fs, + struct args *args) { struct window_pane *wp = wme->wp; struct window_switch_modedata *data; diff --git a/window-tree.c b/window-tree.c index 1b19602fe..9c00e9e8a 100644 --- a/window-tree.c +++ b/window-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-tree.c,v 1.93 2026/07/04 22:09:06 nicm Exp $ */ +/* $OpenBSD: window-tree.c,v 1.94 2026/07/14 17:17:18 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -25,7 +25,8 @@ #include "tmux.h" static struct screen *window_tree_init(struct window_mode_entry *, - struct cmd_find_state *, struct args *); + struct cmdq_item *, struct cmd_find_state *, + struct args *); static void window_tree_free(struct window_mode_entry *); static void window_tree_resize(struct window_mode_entry *, u_int, u_int); @@ -1097,7 +1098,8 @@ window_tree_help(u_int *width, const char **item) } static struct screen * -window_tree_init(struct window_mode_entry *wme, struct cmd_find_state *fs, +window_tree_init(struct window_mode_entry *wme, + __unused struct cmdq_item *item, struct cmd_find_state *fs, struct args *args) { struct window_pane *wp = wme->wp; diff --git a/window.c b/window.c index ca2eca389..7c7a7b788 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.361 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.362 2026/07/14 17:17:18 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1496,18 +1496,21 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) int window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, - const struct window_mode *mode, struct cmd_find_state *fs, - struct args *args) + const struct window_mode *mode, struct cmdq_item *item, + struct cmd_find_state *fs, struct args *args) { struct window_mode_entry *wme; struct window *w = wp->window; - const char *name = mode->name, *p = NULL; + const char *name = mode->name, *oname = NULL; if (!TAILQ_EMPTY(&wp->modes)) { if (TAILQ_FIRST(&wp->modes)->mode == mode) return (1); - p = TAILQ_FIRST(&wp->modes)->mode->name; + if (TAILQ_FIRST(&wp->modes)->mode->flags & WINDOW_MODE_NO_STACK) + window_pane_reset_mode(wp); } + if (!TAILQ_EMPTY(&wp->modes)) + oname = TAILQ_FIRST(&wp->modes)->mode->name; TAILQ_FOREACH(wme, &wp->modes, entry) { if (wme->mode == mode) @@ -1523,7 +1526,12 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, wme->mode = mode; wme->prefix = 1; TAILQ_INSERT_HEAD(&wp->modes, wme, entry); - wme->screen = wme->mode->init(wme, fs, args); + wme->screen = wme->mode->init(wme, item, fs, args); + if (wme->screen == NULL) { + TAILQ_REMOVE(&wp->modes, wme, entry); + free(wme); + return (1); + } } wme->kill = args != NULL ? args_has(args, 'k') : 0; wp->screen = wme->screen; @@ -1534,8 +1542,8 @@ 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); + window_fire_pane_mode_changed("pane-mode-entered", wp, oname, name, 1); + window_fire_pane_mode_changed("pane-mode-changed", wp, oname, name, 1); return (0); } @@ -2684,8 +2692,15 @@ window_get_pane_status(struct window *w) int window_pane_get_pane_status(struct window_pane *wp) { + struct window_mode_entry *wme; int status; + wme = TAILQ_FIRST(&wp->modes); + if (wme != NULL && + (wme->mode->flags & WINDOW_MODE_HIDE_PANE_STATUS) && + (wp->flags & PANE_ZOOMED)) + return (PANE_STATUS_OFF); + if (!window_pane_is_floating(wp)) return (window_get_pane_status(wp->window)); if (window_pane_get_pane_lines(wp) == PANE_LINES_NONE) From e54785ddd6629788e5fa8ebd9f843c784f60aa5e Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 14 Jul 2026 20:07:07 +0100 Subject: [PATCH 34/36] Add menu tests. --- regress/screen-redraw-menus.sh | 133 ++++++++++++++++++ .../screen-redraw-results/menu-basic.result | 14 ++ .../screen-redraw-results/menu-clipped.result | 6 + .../screen-redraw-results/menu-double.result | 14 ++ .../menu-noborder.result | 14 ++ .../menu-over-split.result | 14 ++ .../menu-restyled.result | 14 ++ 7 files changed, 209 insertions(+) create mode 100644 regress/screen-redraw-menus.sh create mode 100644 regress/screen-redraw-results/menu-basic.result create mode 100644 regress/screen-redraw-results/menu-clipped.result create mode 100644 regress/screen-redraw-results/menu-double.result create mode 100644 regress/screen-redraw-results/menu-noborder.result create mode 100644 regress/screen-redraw-results/menu-over-split.result create mode 100644 regress/screen-redraw-results/menu-restyled.result diff --git a/regress/screen-redraw-menus.sh b/regress/screen-redraw-menus.sh new file mode 100644 index 000000000..1bd3bc99b --- /dev/null +++ b/regress/screen-redraw-menus.sh @@ -0,0 +1,133 @@ +#!/bin/sh + +# Exercise drawing of window menus in the screen-redraw.c scene. Menus are +# window-owned and drawn as REDRAW_MENU spans over panes. Captures use -e so +# menu-style, menu-selected-style, and menu-border-style are included. +# +# Run with GENERATE=1 to (re)create the golden files. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX= +TMUX2= +SETUP=0 +RESULTS=screen-redraw-results + +TMP=$(mktemp) +TMP2=$(mktemp) + +cleanup() { + rm -f "$TMP" "$TMP2" + [ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null + [ -n "$TMUX2" ] && $TMUX2 kill-server 2>/dev/null +} +trap cleanup 0 1 15 + +fail() { + echo "$*" >&2 + exit 1 +} + +trim() { + awk ' + { lines[NR] = $0 } + END { + n = NR + while (n > 0 && lines[n] == "") + n-- + for (i = 1; i <= n; i++) + print lines[i] + } + ' "$TMP" >"$TMP2" || exit 1 + mv "$TMP2" "$TMP" || exit 1 +} + +compare() { + sleep 1 + $TMUX capturep -pe >$TMP || exit 1 + trim + if [ -n "$GENERATE" ]; then + cp $TMP "$RESULTS/$1.result" || exit 1 + echo "generated $1" + else + cmp -s $TMP "$RESULTS/$1.result" || \ + fail "scene $1 differs from $RESULTS/$1.result" + fi +} + +C="sh -c 'i=0; while [ \$i -lt 13 ]; do printf \"MENU%02d abcdefghij\n\" \$i; i=\$((i + 1)); done; exec sleep 100'" + +setup() { + [ -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 -x"$1" -y"$2" "$C" || exit 1 + $TMUX2 set -g status off || exit 1 + $TMUX2 set -g window-size manual || exit 1 + $TMUX2 resizew -x"$1" -y"$2" || exit 1 + $TMUX2 setw menu-style "fg=colour250,bg=colour235" || exit 1 + $TMUX2 setw menu-selected-style "fg=colour16,bg=colour220" || exit 1 + $TMUX2 setw menu-border-style "fg=colour45,bg=colour235" || exit 1 + + $TMUX new -d -x"$1" -y"$2" || 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 +} + +menu() { + $TMUX2 display-menu -T "Menu" -C 1 "$@" \ + "Alpha item" a "" \ + "Beta item" b "" \ + "" "" "" \ + "Gamma item" g "" || exit 1 + sleep 1 +} + +# Basic menu over a single pane. +setup 40 14 +menu -x6 -y8 +compare menu-basic + +# Menu over a split: drawn on top of the pane border. +setup 40 14 +$TMUX2 splitw -h "$C" || exit 1 +menu -x8 -y9 +compare menu-over-split + +# Menu with no border lines. +setup 40 14 +menu -b none -x6 -y8 +compare menu-noborder + +# Menu with double border lines. +setup 40 14 +menu -b double -x6 -y8 +compare menu-double + +# Window menu styles are re-expanded from window options when redrawn. +setup 40 14 +menu -x6 -y8 +$TMUX2 setw menu-style "fg=colour196,bg=colour22" || exit 1 +$TMUX2 setw menu-selected-style "fg=colour231,bg=colour88" || exit 1 +$TMUX2 setw menu-border-style "fg=colour51,bg=colour22" || exit 1 +$TMUX2 refresh-client -S || exit 1 +compare menu-restyled + +# Menu wider than the window: it should be accepted and clipped by the scene. +setup 12 6 +$TMUX2 display-menu -T "Menu" -C 0 -x0 -y5 \ + "This item is much wider than the window" t "" \ + "Second item is also too wide" s "" || exit 1 +compare menu-clipped + +exit 0 diff --git a/regress/screen-redraw-results/menu-basic.result b/regress/screen-redraw-results/menu-basic.result new file mode 100644 index 000000000..bd1a16640 --- /dev/null +++ b/regress/screen-redraw-results/menu-basic.result @@ -0,0 +1,14 @@ +MENU00 abcdefghij +MENU01 abcdefghij +MENU02┌─Menu───────────┐ +MENU03│ Alpha item (a) │ +MENU04│ Beta item (b) │ +MENU05├────────────────┤ +MENU06│ Gamma item (g) │ +MENU07└────────────────┘ +MENU08 abcdefghij +MENU09 abcdefghij +MENU10 abcdefghij +MENU11 abcdefghij +MENU12 abcdefghij + diff --git a/regress/screen-redraw-results/menu-clipped.result b/regress/screen-redraw-results/menu-clipped.result new file mode 100644 index 000000000..e720c9a2e --- /dev/null +++ b/regress/screen-redraw-results/menu-clipped.result @@ -0,0 +1,6 @@ +fghij +┌─Menu─────┐ +│ window> │ +│ oo wide> │ +└──────────┘ + diff --git a/regress/screen-redraw-results/menu-double.result b/regress/screen-redraw-results/menu-double.result new file mode 100644 index 000000000..311adb644 --- /dev/null +++ b/regress/screen-redraw-results/menu-double.result @@ -0,0 +1,14 @@ +MENU00 abcdefghij +MENU01 abcdefghij +MENU02╔═Menu═══════════╗ +MENU03║ Alpha item (a) ║ +MENU04║ Beta item (b) ║ +MENU05╠════════════════╣ +MENU06║ Gamma item (g) ║ +MENU07╚════════════════╝ +MENU08 abcdefghij +MENU09 abcdefghij +MENU10 abcdefghij +MENU11 abcdefghij +MENU12 abcdefghij + diff --git a/regress/screen-redraw-results/menu-noborder.result b/regress/screen-redraw-results/menu-noborder.result new file mode 100644 index 000000000..b06587dec --- /dev/null +++ b/regress/screen-redraw-results/menu-noborder.result @@ -0,0 +1,14 @@ +MENU00 abcdefghij +MENU01 abcdefghij +MENU02 Menu  +MENU03  Alpha item (a)   +MENU04  Beta item (b)   +MENU05  +MENU06  Gamma item (g)   +MENU07  +MENU08 abcdefghij +MENU09 abcdefghij +MENU10 abcdefghij +MENU11 abcdefghij +MENU12 abcdefghij + diff --git a/regress/screen-redraw-results/menu-over-split.result b/regress/screen-redraw-results/menu-over-split.result new file mode 100644 index 000000000..9d8714a67 --- /dev/null +++ b/regress/screen-redraw-results/menu-over-split.result @@ -0,0 +1,14 @@ +MENU00 abcdefghij │MENU00 abcdefghij +MENU01 abcdefghij │MENU01 abcdefghij +MENU02 abcdefghij │MENU02 abcdefghij +MENU03 a┌─Menu───────────┐3 abcdefghij +MENU04 a│ Alpha item (a) │4 abcdefghij +MENU05 a│ Beta item (b) │5 abcdefghij +MENU06 a├────────────────┤6 abcdefghij +MENU07 a│ Gamma item (g) │7 abcdefghij +MENU08 a└────────────────┘8 abcdefghij +MENU09 abcdefghij │MENU09 abcdefghij +MENU10 abcdefghij │MENU10 abcdefghij +MENU11 abcdefghij │MENU11 abcdefghij +MENU12 abcdefghij │MENU12 abcdefghij + │ diff --git a/regress/screen-redraw-results/menu-restyled.result b/regress/screen-redraw-results/menu-restyled.result new file mode 100644 index 000000000..ad8bf2b2e --- /dev/null +++ b/regress/screen-redraw-results/menu-restyled.result @@ -0,0 +1,14 @@ +MENU00 abcdefghij +MENU01 abcdefghij +MENU02┌─Menu───────────┐ +MENU03│ Alpha item (a) │ +MENU04│ Beta item (b) │ +MENU05├────────────────┤ +MENU06│ Gamma item (g) │ +MENU07└────────────────┘ +MENU08 abcdefghij +MENU09 abcdefghij +MENU10 abcdefghij +MENU11 abcdefghij +MENU12 abcdefghij + From 11496010c027a06262ecd9bfaa776a0109e7daee Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Tue, 14 Jul 2026 22:15:08 +0100 Subject: [PATCH 35/36] Update test outputs. --- regress/screen-redraw-results/menu-basic.result | 1 - regress/screen-redraw-results/menu-clipped.result | 1 - regress/screen-redraw-results/menu-double.result | 1 - regress/screen-redraw-results/menu-noborder.result | 1 - regress/screen-redraw-results/menu-restyled.result | 1 - 5 files changed, 5 deletions(-) diff --git a/regress/screen-redraw-results/menu-basic.result b/regress/screen-redraw-results/menu-basic.result index bd1a16640..8ee309b45 100644 --- a/regress/screen-redraw-results/menu-basic.result +++ b/regress/screen-redraw-results/menu-basic.result @@ -11,4 +11,3 @@ MENU09 abcdefghij MENU10 abcdefghij MENU11 abcdefghij MENU12 abcdefghij - diff --git a/regress/screen-redraw-results/menu-clipped.result b/regress/screen-redraw-results/menu-clipped.result index e720c9a2e..db9d06f2b 100644 --- a/regress/screen-redraw-results/menu-clipped.result +++ b/regress/screen-redraw-results/menu-clipped.result @@ -3,4 +3,3 @@ fghij │ window> │ │ oo wide> │ └──────────┘ - diff --git a/regress/screen-redraw-results/menu-double.result b/regress/screen-redraw-results/menu-double.result index 311adb644..7ab65b7b6 100644 --- a/regress/screen-redraw-results/menu-double.result +++ b/regress/screen-redraw-results/menu-double.result @@ -11,4 +11,3 @@ MENU09 abcdefghij MENU10 abcdefghij MENU11 abcdefghij MENU12 abcdefghij - diff --git a/regress/screen-redraw-results/menu-noborder.result b/regress/screen-redraw-results/menu-noborder.result index b06587dec..d6479edba 100644 --- a/regress/screen-redraw-results/menu-noborder.result +++ b/regress/screen-redraw-results/menu-noborder.result @@ -11,4 +11,3 @@ MENU09 abcdefghij MENU10 abcdefghij MENU11 abcdefghij MENU12 abcdefghij - diff --git a/regress/screen-redraw-results/menu-restyled.result b/regress/screen-redraw-results/menu-restyled.result index ad8bf2b2e..04d144b72 100644 --- a/regress/screen-redraw-results/menu-restyled.result +++ b/regress/screen-redraw-results/menu-restyled.result @@ -11,4 +11,3 @@ MENU09 abcdefghij MENU10 abcdefghij MENU11 abcdefghij MENU12 abcdefghij - From ad6832e6972663c3aafad890ce983eb213401b7f Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 14 Jul 2026 19:07:03 +0000 Subject: [PATCH 36/36] Switch menus also to be attached to the window instead of an overlay and drawn as part of the client's scene. This means removing the popup menu. --- cmd-display-menu.c | 199 +++++++++++++++++++++++++++++-- menu.c | 291 +++++++++++++++++++++++++-------------------- mode-tree.c | 4 +- popup.c | 217 ++------------------------------- screen-redraw.c | 105 +++++++++++++++- server-client.c | 70 +++++++++-- server-fn.c | 15 ++- tmux.h | 36 +++--- window.c | 11 +- 9 files changed, 567 insertions(+), 381 deletions(-) diff --git a/cmd-display-menu.c b/cmd-display-menu.c index c2d83ff27..19ad3a9b4 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-display-menu.c,v 1.51 2026/07/07 09:45:09 nicm Exp $ */ +/* $OpenBSD: cmd-display-menu.c,v 1.52 2026/07/14 19:07:03 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -93,7 +93,7 @@ cmd_display_menu_args_parse(struct args *args, u_int idx, __unused char **cause) } static int -cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item, +cmd_display_menu_get_popup_pos(struct client *tc, struct cmdq_item *item, struct args *args, u_int *px, u_int *py, u_int w, u_int h) { struct tty *tty = &tc->tty; @@ -128,8 +128,8 @@ cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item, } /* 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); + format_add(ft, "popup_last_x", "%u", target->w->menu_last_px); + format_add(ft, "popup_last_y", "%u", target->w->menu_last_py + h); /* * If there are any status lines, add this window position and the @@ -289,6 +289,184 @@ cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item, return (1); } +static int +cmd_display_menu_get_menu_pos(struct client *tc, struct cmdq_item *item, + struct args *args, u_int *px, u_int *py, u_int w, u_int h) +{ + struct cmd_find_state *target = cmdq_get_target(item); + struct key_event *event = cmdq_get_event(item); + struct session *s = tc->session; + struct winlink *wl = target->wl; + struct window *window = target->w; + struct window_pane *wp = target->wp; + struct style_ranges *ranges = NULL; + struct style_range *sr = NULL; + const char *xp, *yp; + char *p; + u_int line, ox, oy, sx, sy, lines, position; + long n, max_x, max_y, mouse_x = 0, mouse_y = 0; + struct format_tree *ft; + + max_x = window->sx > w ? window->sx - w : 0; + max_y = window->sy > h ? window->sy - h : 0; + + tty_window_offset(&tc->tty, &ox, &oy, &sx, &sy); + + ft = format_create_from_target(item); + if (event->m.valid) { + mouse_x = event->m.x + ox; + if (event->m.statusat == 0) { + if (event->m.y >= event->m.statuslines) + mouse_y = event->m.y - event->m.statuslines + oy; + else + mouse_y = oy; + } else if (event->m.statusat > 0 && + event->m.y >= (u_int)event->m.statusat) { + mouse_y = oy + sy - 1; + } else + mouse_y = event->m.y + oy; + format_add(ft, "popup_mouse_x", "%ld", mouse_x); + format_add(ft, "popup_mouse_y", "%ld", mouse_y); + } + + format_add(ft, "popup_last_x", "%u", window->menu_last_px); + format_add(ft, "popup_last_y", "%u", window->menu_last_py + h); + + lines = status_line_size(tc); + position = options_get_number(s->options, "status-position"); + if (status_at_line(tc) != -1 && lines != 0) { + for (line = 0; line < lines; line++) { + ranges = &tc->status.entries[line].ranges; + TAILQ_FOREACH(sr, ranges, entry) { + if (sr->type != STYLE_RANGE_WINDOW) + continue; + if (sr->argument == (u_int)wl->idx) + break; + } + if (sr != NULL) + break; + } + if (sr != NULL) { + format_add(ft, "popup_window_status_line_x", "%u", + sr->start + ox); + if (position == 0) { + format_add(ft, "popup_window_status_line_y", + "%u", h); + } else { + format_add(ft, "popup_window_status_line_y", + "%u", window->sy); + } + } + if (position == 0) + format_add(ft, "popup_status_line_y", "%u", h); + else + format_add(ft, "popup_status_line_y", "%u", window->sy); + } + + format_add(ft, "popup_width", "%u", w); + format_add(ft, "popup_height", "%u", h); + + n = ((long)window->sx - 1) / 2 - w / 2; + if (n < 0) + format_add(ft, "popup_centre_x", "%u", 0); + else + format_add(ft, "popup_centre_x", "%ld", n); + n = ((long)window->sy - 1) / 2 + h / 2; + if (n >= window->sy) + format_add(ft, "popup_centre_y", "%ld", max_y); + else + format_add(ft, "popup_centre_y", "%ld", n); + + if (event->m.valid) { + n = mouse_x - w / 2; + if (n < 0) + format_add(ft, "popup_mouse_centre_x", "%u", 0); + else + format_add(ft, "popup_mouse_centre_x", "%ld", n); + n = mouse_y - h / 2; + if (n + h >= window->sy) + format_add(ft, "popup_mouse_centre_y", "%ld", max_y); + else + format_add(ft, "popup_mouse_centre_y", "%ld", n); + n = mouse_y + h; + if (n >= window->sy) + format_add(ft, "popup_mouse_top", "%u", window->sy - 1); + else + format_add(ft, "popup_mouse_top", "%ld", n); + n = mouse_y - h; + if (n < 0) + format_add(ft, "popup_mouse_bottom", "%u", 0); + else + format_add(ft, "popup_mouse_bottom", "%ld", n); + } + + n = wp->yoff + h; + if (n >= window->sy) + format_add(ft, "popup_pane_top", "%ld", max_y); + else + format_add(ft, "popup_pane_top", "%ld", n); + format_add(ft, "popup_pane_bottom", "%u", wp->yoff + wp->sy); + format_add(ft, "popup_pane_left", "%u", wp->xoff); + n = (long)wp->xoff + wp->sx - w; + if (n < 0) + format_add(ft, "popup_pane_right", "%u", 0); + else + format_add(ft, "popup_pane_right", "%ld", n); + + xp = args_get(args, 'x'); + if (xp == NULL || strcmp(xp, "C") == 0) + xp = "#{popup_centre_x}"; + else if (strcmp(xp, "R") == 0) + xp = "#{popup_pane_right}"; + else if (strcmp(xp, "P") == 0) + 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); + n = strtol(p, NULL, 10); + if (n < 0) + n = 0; + if (n > max_x) + n = max_x; + *px = n; + log_debug("%s: -x: %s = %s = %u (-w %u)", __func__, xp, p, *px, w); + free(p); + + yp = args_get(args, 'y'); + if (yp == NULL || strcmp(yp, "C") == 0) + yp = "#{popup_centre_y}"; + else if (strcmp(yp, "P") == 0) + 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) + yp = "#{popup_window_status_line_y}"; + p = format_expand(ft, yp); + n = strtol(p, NULL, 10); + if (n < h) + n = 0; + else + n -= h; + if (n < 0) + n = 0; + if (n > max_y) + n = max_y; + *py = n; + log_debug("%s: -y: %s = %s = %u (-h %u)", __func__, yp, p, *py, h); + free(p); + + format_free(ft); + return (1); +} + static enum cmd_retval cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item) { @@ -309,10 +487,6 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item) struct options *o = target->s->curw->window->options; struct options_entry *oe; - - if (tc->overlay_draw != NULL) - return (CMD_RETURN_NORMAL); - if (args_has(args, 'C')) { if (strcmp(args_get(args, 'C'), "-") == 0) starting_choice = -1; @@ -358,8 +532,8 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item) } if (menu->count == 0) goto out; - if (!cmd_display_menu_get_pos(tc, item, args, &px, &py, menu->width + 4, - menu->count + 2)) + if (!cmd_display_menu_get_menu_pos(tc, item, args, &px, &py, + menu->width + 4, menu->count + 2)) goto out; value = args_get(args, 'b'); @@ -380,7 +554,7 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item) if (menu_display(menu, flags, starting_choice, item, px, py, tc, lines, style, selected_style, border_style, target, NULL, NULL) != 0) goto out; - return (CMD_RETURN_WAIT); + return (CMD_RETURN_NORMAL); out: menu_free(menu); @@ -446,7 +620,8 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item) w = tty->sx; if (h > tty->sy) h = tty->sy; - if (!cmd_display_menu_get_pos(tc, item, args, &px, &py, w, h)) + if (!cmd_display_menu_get_popup_pos(tc, item, args, &px, &py, + w, h)) goto out; value = args_get(args, 'd'); diff --git a/menu.c b/menu.c index ec7b9781b..aab25d7c5 100644 --- a/menu.c +++ b/menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD: menu.c,v 1.68 2026/07/07 09:45:09 nicm Exp $ */ +/* $OpenBSD: menu.c,v 1.69 2026/07/14 19:07:03 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -24,7 +24,7 @@ #include "tmux.h" struct menu_data { - struct cmdq_item *item; + struct window *w; int flags; char *style; @@ -37,8 +37,9 @@ struct menu_data { enum box_lines border_lines; struct cmd_find_state fs; + key_code key; + struct mouse_event m; struct screen s; - struct visible_ranges r; u_int px; u_int py; @@ -175,46 +176,14 @@ menu_free(struct menu *menu) free(menu); } -struct screen * -menu_mode_cb(__unused struct client *c, void *data, u_int *cx, u_int *cy) -{ - struct menu_data *md = data; - - *cx = md->px + 2; - if (md->choice == -1) - *cy = md->py; - else - *cy = md->py + 1 + md->choice; - - return (&md->s); -} - -/* Return parts of the input range which are not obstructed by the menu. */ -struct visible_ranges * -menu_check_cb(__unused struct client *c, void *data, u_int px, u_int py, - u_int nx) -{ - struct menu_data *md = data; - struct menu *menu = md->menu; - - server_client_overlay_range(md->px, md->py, menu->width + 4, - menu->count + 2, px, py, nx, &md->r); - return (&md->r); -} - static void -menu_reapply_styles(struct menu_data *md, struct client *c) +menu_reapply_styles(struct menu_data *md) { - struct session *s = c->session; - struct options *o; + struct options *o = md->w->options; struct format_tree *ft; struct style sytmp; - if (s == NULL) - return; - o = s->curw->window->options; - - ft = format_create_defaults(NULL, c, s, s->curw, NULL); + ft = format_create_defaults(NULL, NULL, md->fs.s, md->fs.wl, md->fs.wp); /* Reapply menu style from options. */ memcpy(&md->style_gc, &grid_default_cell, sizeof md->style_gc); @@ -257,16 +226,13 @@ menu_reapply_styles(struct menu_data *md, struct client *c) } void -menu_draw_cb(struct client *c, void *data) +menu_update(struct menu_data *md) { - struct menu_data *md = data; - struct tty *tty = &c->tty; struct screen *s = &md->s; struct menu *menu = md->menu; struct screen_write_ctx ctx; - u_int i, px = md->px, py = md->py; - menu_reapply_styles(md, c); + menu_reapply_styles(md); screen_write_start(&ctx, s); screen_write_clearscreen(&ctx, 8); @@ -279,43 +245,95 @@ menu_draw_cb(struct client *c, void *data) screen_write_menu(&ctx, menu, md->choice, md->border_lines, &md->style_gc, &md->border_style_gc, &md->selected_style_gc); screen_write_stop(&ctx); +} - for (i = 0; i < screen_size_y(&md->s); i++) - tty_draw_line(tty, s, 0, i, menu->width + 4, px, py + i, NULL); +static void +menu_free_data(struct menu_data *md) +{ + if (md != NULL) { + if (md->cb != NULL) + md->cb(md->menu, UINT_MAX, KEYC_NONE, md->data); + + screen_free(&md->s); + menu_free(md->menu); + free(md->style); + free(md->selected_style); + free(md->border_style); + free(md); + } } void -menu_free_cb(__unused struct client *c, void *data) +menu_close(struct window *w) { - struct menu_data *md = data; + if (w->menu != NULL) { + menu_free_data(w->menu); + w->menu = NULL; - if (md->item != NULL) - cmdq_continue(md->item); + redraw_invalidate_scene(w); + window_update_focus(w); + server_redraw_window(w); + } +} - if (md->cb != NULL) - md->cb(md->menu, UINT_MAX, KEYC_NONE, md->data); +void +menu_destroy(struct window *w) +{ + menu_free_data(w->menu); + w->menu = NULL; +} - free(md->r.ranges); - screen_free(&md->s); +void +menu_get_cursor(struct menu_data *md, u_int *cx, u_int *cy) +{ + *cx = md->px + 2; + if (md->choice == -1) + *cy = md->py; + else + *cy = md->py + 1 + md->choice; +} - menu_free(md->menu); - free(md->style); - free(md->selected_style); - free(md->border_style); - free(md); +struct screen * +menu_screen(struct menu_data *md) +{ + return (&md->s); +} + +u_int +menu_width(struct menu_data *md) +{ + return (md->menu->width + 4); +} + +u_int +menu_height(struct menu_data *md) +{ + return (md->menu->count + 2); +} + +u_int +menu_x(struct menu_data *md) +{ + return (md->px); +} + +u_int +menu_y(struct menu_data *md) +{ + return (md->py); } int -menu_key_cb(struct client *c, void *data, struct key_event *event) +menu_key(struct client *c, struct menu_data *md, struct key_event *event) { - struct menu_data *md = data; struct menu *menu = md->menu; struct mouse_event *m = &event->m; u_int i; - int count = menu->count, old = md->choice; + int n = menu->count, old = md->choice; int move; const char *name = NULL; const struct menu_item *item; + struct key_event saved_event; struct cmdq_state *state; enum cmd_parse_status status; char *error; @@ -337,7 +355,7 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) if (m->x < md->px || m->x > md->px + 4 + menu->width || m->y < md->py + 1 || - m->y > md->py + 1 + count - 1) { + m->y > md->py + 1 + n - 1) { if (~md->flags & MENU_STAYOPEN) { if (!move && MOUSE_RELEASE(m->b)) return (1); @@ -349,7 +367,7 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) } if (md->choice != -1) { md->choice = -1; - c->flags |= CLIENT_REDRAWOVERLAY; + server_redraw_window_menu(md->w); } return (0); } @@ -362,10 +380,10 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) } md->choice = m->y - (md->py + 1); if (md->choice != old) - c->flags |= CLIENT_REDRAWOVERLAY; + server_redraw_window_menu(md->w); return (0); } - for (i = 0; i < (u_int)count; i++) { + for (i = 0; i < (u_int)n; i++) { name = menu->items[i].name; if (name == NULL || *name == '-') continue; @@ -382,12 +400,12 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) old = 0; do { if (md->choice == -1 || md->choice == 0) - md->choice = count - 1; + md->choice = n - 1; else md->choice--; name = menu->items[md->choice].name; } while ((name == NULL || *name == '-') && md->choice != old); - c->flags |= CLIENT_REDRAWOVERLAY; + server_redraw_window_menu(md->w); return (0); case KEYC_BSPACE: if (~md->flags & MENU_TAB) @@ -396,7 +414,7 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) case '\011': /* Tab */ if (~md->flags & MENU_TAB) break; - if (md->choice == count - 1) + if (md->choice == n - 1) return (1); /* FALLTHROUGH */ case KEYC_DOWN: @@ -404,13 +422,13 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) if (old == -1) old = 0; do { - if (md->choice == -1 || md->choice == count - 1) + if (md->choice == -1 || md->choice == n - 1) md->choice = 0; else md->choice++; name = menu->items[md->choice].name; } while ((name == NULL || *name == '-') && md->choice != old); - c->flags |= CLIENT_REDRAWOVERLAY; + server_redraw_window_menu(md->w); return (0); case KEYC_PPAGE: case 'b'|KEYC_CTRL: @@ -428,21 +446,21 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) break; } } - c->flags |= CLIENT_REDRAWOVERLAY; + server_redraw_window_menu(md->w); break; case KEYC_NPAGE: - if (md->choice > count - 6) { - md->choice = count - 1; + if (md->choice > n - 6) { + md->choice = n - 1; name = menu->items[md->choice].name; } else { i = 5; while (i > 0) { md->choice++; name = menu->items[md->choice].name; - if (md->choice != count - 1 && + if (md->choice != n - 1 && (name != NULL && *name != '-')) i--; - else if (md->choice == count - 1) + else if (md->choice == n - 1) break; } } @@ -450,27 +468,27 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) md->choice--; name = menu->items[md->choice].name; } - c->flags |= CLIENT_REDRAWOVERLAY; + server_redraw_window_menu(md->w); break; case 'g': case KEYC_HOME: md->choice = 0; name = menu->items[md->choice].name; - while ((name == NULL || *name == '-') && md->choice != count - 1) { + while ((name == NULL || *name == '-') && md->choice != n - 1) { md->choice++; name = menu->items[md->choice].name; } - c->flags |= CLIENT_REDRAWOVERLAY; + server_redraw_window_menu(md->w); break; case 'G': case KEYC_END: - md->choice = count - 1; + md->choice = n - 1; name = menu->items[md->choice].name; while ((name == NULL || *name == '-') && md->choice != 0) { md->choice--; name = menu->items[md->choice].name; } - c->flags |= CLIENT_REDRAWOVERLAY; + server_redraw_window_menu(md->w); break; case 'f'|KEYC_CTRL: break; @@ -495,14 +513,17 @@ chosen: return (1); } if (md->cb != NULL) { - md->cb(md->menu, md->choice, item->key, md->data); - md->cb = NULL; - return (1); + md->cb(md->menu, md->choice, item->key, md->data); + md->cb = NULL; + return (1); } - if (md->item != NULL) - event = cmdq_get_event(md->item); - else + if (md->key != KEYC_NONE) { + memset(&saved_event, 0, sizeof saved_event); + saved_event.key = md->key; + memcpy(&saved_event.m, &md->m, sizeof saved_event.m); + event = &saved_event; + } else event = NULL; state = cmdq_new_state(&md->fs, event, 0); @@ -516,11 +537,10 @@ chosen: return (1); } -static void -menu_resize_cb(struct client *c, void *data) +void +menu_resize(struct menu_data *md, struct window *w) { - struct menu_data *md = data; - u_int nx, ny, w, h; + u_int nx, ny, sx, sy; if (md == NULL) return; @@ -528,56 +548,74 @@ menu_resize_cb(struct client *c, void *data) nx = md->px; ny = md->py; - w = md->menu->width + 4; - h = md->menu->count + 2; + sx = md->menu->width + 4; + sy = md->menu->count + 2; - if (nx + w > c->tty.sx) { - if (c->tty.sx <= w) + if (nx + sx > w->sx) { + if (w->sx <= sx) nx = 0; else - nx = c->tty.sx - w; + nx = w->sx - sx; } - if (ny + h > c->tty.sy) { - if (c->tty.sy <= h) + if (ny + sy > w->sy) { + if (w->sy <= sy) ny = 0; else - ny = c->tty.sy - h; + ny = w->sy - sy; } md->px = nx; md->py = ny; } -struct menu_data * -menu_prepare(struct menu *menu, int flags, int starting_choice, +int +menu_display(struct menu *menu, int flags, int starting_choice, struct cmdq_item *item, u_int px, u_int py, struct client *c, enum box_lines lines, const char *style, const char *selected_style, const char *border_style, struct cmd_find_state *fs, menu_choice_cb cb, void *data) { struct menu_data *md; + struct key_event *event; int choice; const char *name; - struct options *o = c->session->curw->window->options; + u_int sx, sy; + struct window *w; + struct options *o; - if (c->tty.sx < menu->width + 4 || c->tty.sy < menu->count + 2) - return (NULL); - if (px + menu->width + 4 > c->tty.sx) - px = c->tty.sx - menu->width - 4; - if (py + menu->count + 2 > c->tty.sy) - py = c->tty.sy - menu->count - 2; + if (fs == NULL) + w = c->session->curw->window; + else + w = fs->w; + o = w->options; - /* Remember where this menu is so -x/-y L can reuse the position. */ - c->menu_last_px = px; - c->menu_last_py = py; + sx = menu->width + 4; + sy = menu->count + 2; + if (sx >= w->sx) + px = 0; + else if (px + sx > w->sx) + px = w->sx - sx; + if (sy >= w->sy) + py = 0; + else if (py + sy > w->sy) + py = w->sy - sy; + w->menu_last_px = px; + w->menu_last_py = py; if (lines == BOX_LINES_DEFAULT) lines = options_get_number(o, "menu-border-lines"); md = xcalloc(1, sizeof *md); - md->item = item; + md->w = w; md->flags = flags; md->border_lines = lines; + md->key = KEYC_NONE; + + if (item != NULL) { + event = cmdq_get_event(item); + md->key = event->key; + memcpy(&md->m, &event->m, sizeof md->m); + } if (style != NULL) md->style = xstrdup(style); @@ -588,7 +626,9 @@ menu_prepare(struct menu *menu, int flags, int starting_choice, if (fs != NULL) cmd_find_copy_state(&md->fs, fs); - screen_init(&md->s, menu->width + 4, menu->count + 2, 0); + else if (cmd_find_from_window(&md->fs, w, 0) != 0) + cmd_find_clear_state(&md->fs, 0); + screen_init(&md->s, sx, sy, 0); if (~md->flags & MENU_NOMOUSE) md->s.mode |= (MODE_MOUSE_ALL|MODE_MOUSE_BUTTON); md->s.mode &= ~MODE_CURSOR; @@ -599,6 +639,9 @@ menu_prepare(struct menu *menu, int flags, int starting_choice, md->menu = menu; md->choice = -1; + md->cb = cb; + md->data = data; + if (md->flags & MENU_NOMOUSE) { if (starting_choice >= (int)menu->count) { starting_choice = menu->count - 1; @@ -630,25 +673,11 @@ menu_prepare(struct menu *menu, int flags, int starting_choice, } } - md->cb = cb; - md->data = data; - return (md); -} + menu_close(md->w); + md->w->menu = md; -int -menu_display(struct menu *menu, int flags, int starting_choice, - struct cmdq_item *item, u_int px, u_int py, struct client *c, - enum box_lines lines, const char *style, const char *selected_style, - const char *border_style, struct cmd_find_state *fs, menu_choice_cb cb, - void *data) -{ - struct menu_data *md; - - md = menu_prepare(menu, flags, starting_choice, item, px, py, c, lines, - style, selected_style, border_style, fs, cb, data); - if (md == NULL) - return (-1); - server_client_set_overlay(c, 0, menu_check_cb, menu_mode_cb, - menu_draw_cb, menu_key_cb, menu_free_cb, menu_resize_cb, md); + redraw_invalidate_scene(md->w); + window_update_focus(md->w); + server_redraw_window(md->w); return (0); } diff --git a/mode-tree.c b/mode-tree.c index ef859b15e..defd587bb 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: mode-tree.c,v 1.99 2026/07/06 12:08:52 nicm Exp $ */ +/* $OpenBSD: mode-tree.c,v 1.100 2026/07/14 19:07:03 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -1409,6 +1409,8 @@ mode_tree_display_menu(struct mode_tree_data *mtd, struct client *c, u_int x, x -= (menu->width + 4) / 2; else x = 0; + x += mtd->wp->xoff; + y += mtd->wp->yoff; if (menu_display(menu, 0, 0, NULL, x, y, c, BOX_LINES_DEFAULT, NULL, NULL, NULL, NULL, mode_tree_menu_callback, mtm) != 0) { mode_tree_remove_ref(mtd); diff --git a/popup.c b/popup.c index c43b46d0a..5e037922f 100644 --- a/popup.c +++ b/popup.c @@ -1,4 +1,4 @@ -/* $OpenBSD: popup.c,v 1.75 2026/07/09 07:32:58 nicm Exp $ */ +/* $OpenBSD: popup.c,v 1.76 2026/07/14 19:07:03 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -43,7 +43,6 @@ struct popup_data { struct colour_palette palette; struct visible_ranges r; - struct visible_ranges or[2]; struct job *job; struct input_ctx *ictx; @@ -51,8 +50,6 @@ struct popup_data { popup_close_cb cb; void *arg; - struct menu *menu; - struct menu_data *md; int close; /* Current position and size. */ @@ -76,19 +73,6 @@ struct popup_data { u_int lb; }; -static const struct menu_item popup_menu_items[] = { - { "Close", 'q', NULL }, - { "#{?buffer_name,Paste #[underscore]#{buffer_name},}", 'p', NULL }, - { "", KEYC_NONE, NULL }, - { "Fill Space", 'F', NULL }, - { "Centre", 'C', NULL }, - { "", KEYC_NONE, NULL }, - { "To Horizontal Pane", 'h', NULL }, - { "To Vertical Pane", 'v', NULL }, - - { NULL, KEYC_NONE, NULL } -}; - static void popup_free(struct popup_data *pd) { @@ -98,8 +82,6 @@ popup_free(struct popup_data *pd) job_free(pd->job); input_free(pd->ictx); - free(pd->or[0].ranges); - free(pd->or[1].ranges); free(pd->r.ranges); screen_free(&pd->s); colour_palette_free(&pd->palette); @@ -206,9 +188,6 @@ popup_mode_cb(__unused struct client *c, void *data, u_int *cx, u_int *cy) { struct popup_data *pd = data; - if (pd->md != NULL) - return (menu_mode_cb(c, pd->md, cx, cy)); - if (pd->border_lines == BOX_LINES_NONE) { *cx = pd->px + pd->s.cx; *cy = pd->py + pd->s.cy; @@ -221,52 +200,11 @@ popup_mode_cb(__unused struct client *c, void *data, u_int *cx, u_int *cy) /* Return parts of the input range which are not obstructed by the popup. */ static struct visible_ranges * -popup_check_cb(struct client* c, void *data, u_int px, u_int py, u_int nx) +popup_check_cb(__unused struct client* c, void *data, u_int px, u_int py, + u_int nx) { struct popup_data *pd = data; struct visible_ranges *r = &pd->r; - struct visible_ranges *mr; - u_int i, j, k = 0; - - if (pd->md != NULL) { - /* - * Work out the visible ranges for the menu (that is, the - * ranges not covered by the menu). A menu should have at most - * two ranges and we rely on this being the case. - */ - mr = menu_check_cb(c, pd->md, px, py, nx); - if (mr->used > 2) - fatalx("too many menu ranges"); - - /* - * Walk the ranges still visible under the menu and check if - * each is visible under the popup as well. At most there can be - * three total ranges if popup and menu do not intersect. - */ - for (i = 0; i < mr->used; i++) { - server_client_overlay_range(pd->px, pd->py, pd->sx, - pd->sy, mr->ranges[i].px, py, mr->ranges[i].nx, - &pd->or[i]); - } - - /* - * We now have nonoverlapping ranges from left to right. - * Combine them together into the output. - */ - server_client_ensure_ranges(r, 3); - for (i = 0; i < mr->used; i++) { - for (j = 0; j < pd->or[i].used; j++) { - if (pd->or[i].ranges[j].nx == 0) - continue; - if (k >= 3) - fatalx("too many popup & menu ranges"); - r->ranges[k].px = pd->or[i].ranges[j].px; - r->ranges[k].nx = pd->or[i].ranges[j].nx; - k++; - } - } - return (r); - } server_client_overlay_range(pd->px, pd->py, pd->sx, pd->sy, px, py, nx, r); @@ -316,34 +254,21 @@ popup_draw_cb(struct client *c, void *data) style_ctx.dim = 0; style_ctx.hyperlinks = s.hyperlinks; - if (pd->md != NULL) { - c->overlay_check = menu_check_cb; - c->overlay_data = pd->md; - } else { - c->overlay_check = NULL; - c->overlay_data = NULL; - } + c->overlay_check = NULL; + c->overlay_data = NULL; for (i = 0; i < pd->sy; i++) tty_draw_line(tty, &s, 0, i, pd->sx, px, py + i, &style_ctx); screen_free(&s); - if (pd->md != NULL) { - c->overlay_check = NULL; - c->overlay_data = NULL; - menu_draw_cb(c, pd->md); - } c->overlay_check = popup_check_cb; c->overlay_data = pd; } static void -popup_free_cb(struct client *c, void *data) +popup_free_cb(__unused struct client *c, void *data) { struct popup_data *pd = data; struct cmdq_item *item = pd->item; - if (pd->md != NULL) - menu_free_cb(c, pd->md); - if (pd->cb != NULL) pd->cb(pd->status, pd->arg); @@ -365,8 +290,6 @@ popup_resize_cb(__unused struct client *c, void *data) if (pd == NULL) return; - if (pd->md != NULL) - menu_free_cb(c, pd->md); /* Adjust position and size. */ if (pd->psy > tty->sy) @@ -398,96 +321,6 @@ popup_resize_cb(__unused struct client *c, void *data) } } -static void -popup_make_pane(struct popup_data *pd, enum layout_type type) -{ - struct client *c = pd->c; - struct session *s = c->session; - struct window *w = s->curw->window; - struct layout_cell *lc; - struct window_pane *wp = w->active, *new_wp; - u_int hlimit; - const char *shell; - - window_unzoom(w, 1); - - lc = layout_split_pane(wp, type, -1, 0); - if (lc == NULL) - return; - hlimit = options_get_number(s->options, "history-limit"); - new_wp = window_add_pane(wp->window, NULL, hlimit, 0); - layout_assign_pane(lc, new_wp, 0); - - if (pd->job != NULL) { - new_wp->fd = job_transfer(pd->job, &new_wp->pid, new_wp->tty, - sizeof new_wp->tty); - pd->job = NULL; - } - - screen_set_title(&pd->s, new_wp->base.title, 0); - screen_free(&new_wp->base); - memcpy(&new_wp->base, &pd->s, sizeof wp->base); - screen_resize(&new_wp->base, new_wp->sx, new_wp->sy, 1); - screen_init(&pd->s, 1, 1, 0); - - shell = options_get_string(s->options, "default-shell"); - if (!checkshell(shell)) - shell = _PATH_BSHELL; - new_wp->shell = xstrdup(shell); - - window_pane_set_event(new_wp); - window_set_active_pane(w, new_wp, 1); - new_wp->flags |= PANE_CHANGED; - - pd->close = 1; -} - -static void -popup_menu_done(__unused struct menu *menu, __unused u_int choice, - key_code key, void *data) -{ - struct popup_data *pd = data; - struct client *c = pd->c; - struct paste_buffer *pb; - const char *buf; - size_t len; - - pd->md = NULL; - pd->menu = NULL; - server_redraw_client(pd->c); - - switch (key) { - case 'p': - pb = paste_get_top(NULL); - if (pb != NULL) { - buf = paste_buffer_data(pb, &len); - bufferevent_write(job_get_event(pd->job), buf, len); - } - break; - case 'F': - pd->sx = c->tty.sx; - pd->sy = c->tty.sy; - pd->px = 0; - pd->py = 0; - server_redraw_client(c); - break; - case 'C': - pd->px = c->tty.sx / 2 - pd->sx / 2; - pd->py = c->tty.sy / 2 - pd->sy / 2; - server_redraw_client(c); - break; - case 'h': - popup_make_pane(pd, LAYOUT_LEFTRIGHT); - break; - case 'v': - popup_make_pane(pd, LAYOUT_TOPBOTTOM); - break; - case 'q': - pd->close = 1; - break; - } -} - static void popup_handle_drag(struct client *c, struct popup_data *pd, struct mouse_event *m) @@ -553,21 +386,9 @@ popup_key_cb(struct client *c, void *data, struct key_event *event) struct mouse_event *m = &event->m; const char *buf; size_t len; - u_int px, py, x; + u_int px, py; enum { NONE, LEFT, RIGHT, TOP, BOTTOM } border = NONE; - if (pd->md != NULL) { - if (menu_key_cb(c, pd->md, event) == 1) { - pd->md = NULL; - pd->menu = NULL; - if (pd->close) - server_client_clear_overlay(c); - else - server_redraw_client(c); - } - return (0); - } - if (KEYC_IS_MOUSE(event->key)) { if (pd->dragging != OFF) { popup_handle_drag(c, pd, m); @@ -577,8 +398,6 @@ popup_key_cb(struct client *c, void *data, struct key_event *event) m->x > pd->px + pd->sx - 1 || m->y < pd->py || m->y > pd->py + pd->sy - 1) { - if (MOUSE_BUTTONS(m->b) == MOUSE_BUTTON_3) - goto menu; return (0); } if (pd->border_lines != BOX_LINES_NONE) { @@ -594,7 +413,7 @@ popup_key_cb(struct client *c, void *data, struct key_event *event) if ((m->b & MOUSE_MASK_MODIFIERS) == 0 && MOUSE_BUTTONS(m->b) == MOUSE_BUTTON_3 && (border == LEFT || border == TOP)) - goto menu; + goto out; if (((m->b & MOUSE_MASK_MODIFIERS) == MOUSE_MASK_META) || (border != NONE && !MOUSE_DRAG(m->lb))) { if (!MOUSE_DRAG(m->b)) @@ -634,17 +453,6 @@ popup_key_cb(struct client *c, void *data, struct key_event *event) } return (0); -menu: - pd->menu = menu_create(""); - menu_add_items(pd->menu, popup_menu_items, NULL, c, NULL); - if (m->x >= (pd->menu->width + 4) / 2) - x = m->x - (pd->menu->width + 4) / 2; - else - x = 0; - pd->md = menu_prepare(pd->menu, 0, 0, NULL, x, m->y, c, - BOX_LINES_DEFAULT, NULL, NULL, NULL, NULL, popup_menu_done, pd); - c->flags |= CLIENT_REDRAWOVERLAY; - out: pd->lx = m->x; pd->ly = m->y; @@ -665,13 +473,8 @@ popup_job_update_cb(struct job *job) if (size == 0) return; - if (pd->md != NULL) { - c->overlay_check = menu_check_cb; - c->overlay_data = pd->md; - } else { - c->overlay_check = NULL; - c->overlay_data = NULL; - } + c->overlay_check = NULL; + c->overlay_data = NULL; input_parse_screen(pd->ictx, s, popup_init_ctx_cb, pd, data, size); c->overlay_check = popup_check_cb; c->overlay_data = pd; diff --git a/screen-redraw.c b/screen-redraw.c index 10a5acaa9..c7cd3e1f9 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-redraw.c,v 1.150 2026/07/09 07:32:58 nicm Exp $ */ +/* $OpenBSD: screen-redraw.c,v 1.151 2026/07/14 19:07:03 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -68,8 +68,9 @@ enum redraw_span_type { REDRAW_SPAN_STATUS, /* pane status line */ REDRAW_SPAN_BORDER, /* pane border */ REDRAW_SPAN_SCROLLBAR, /* pane scrollbar */ + REDRAW_SPAN_MENU, /* window menu */ }; -#define REDRAW_SPAN_TYPES 6 +#define REDRAW_SPAN_TYPES 7 /* Border connections to adjacent cells. */ #define REDRAW_BORDER_L 0x1 @@ -91,7 +92,8 @@ enum redraw_span_type { #define REDRAW_PANE_STATUS 0x10 #define REDRAW_PANE_SCROLLBAR 0x20 #define REDRAW_STATUS 0x40 -#define REDRAW_OVERLAY 0x80 +#define REDRAW_MENU 0x80 +#define REDRAW_OVERLAY 0x100 /* Draw everything. */ #define REDRAW_ALL 0x7fffffff @@ -163,6 +165,15 @@ struct redraw_span_data { /* Flags for this span. */ int flags; } sb; /* pane scrollbar */ + + struct { + /* Menu this span belongs to. */ + struct menu_data *md; + + /* Position of span inside the menu. */ + u_int px; + u_int py; + } m; /* menu */ }; }; @@ -251,6 +262,8 @@ redraw_flags_to_string(int flags) strlcat(s, "pane-status ", sizeof s); if (flags & REDRAW_PANE_SCROLLBAR) strlcat(s, "scrollbar ", sizeof s); + if (flags & REDRAW_MENU) + strlcat(s, "menu ", sizeof s); if (flags & REDRAW_OVERLAY) strlcat(s, "overlay ", sizeof s); if (REDRAW_IS_ALL(flags)) @@ -828,6 +841,34 @@ redraw_mark_two_pane_colours(struct redraw_build_ctx *bctx) } } +/* Mark the window menu above all panes. */ +static void +redraw_mark_menu(struct redraw_build_ctx *bctx) +{ + struct menu_data *md = bctx->w->menu; + struct redraw_build_cell *bc; + u_int px, py, x, y, sx, sy; + + if (md == NULL) + return; + + sx = menu_width(md); + sy = menu_height(md); + for (py = 0; py < sy; py++) { + for (px = 0; px < sx; px++) { + if (!redraw_window_to_scene(bctx, menu_x(md) + px, + menu_y(md) + py, &x, &y)) + continue; + bc = redraw_get_build_cell(bctx, x, y); + memset(bc, 0, sizeof *bc); + bc->data.type = REDRAW_SPAN_MENU; + bc->data.m.md = md; + bc->data.m.px = px; + bc->data.m.py = py; + } + } +} + /* Return true if two adjacent build cells can be joined into one span. */ static int redraw_compare_data(struct redraw_build_cell *a, struct redraw_build_cell *b) @@ -874,6 +915,12 @@ redraw_compare_data(struct redraw_build_cell *a, struct redraw_build_cell *b) ad->sb.flags != bd->sb.flags) return (0); return (1); + case REDRAW_SPAN_MENU: + if (ad->m.md != bd->m.md || + ad->m.py != bd->m.py || + ad->m.px + 1 != bd->m.px) + return (0); + return (1); case REDRAW_SPAN_OUTSIDE: case REDRAW_SPAN_EMPTY: return (1); @@ -908,6 +955,7 @@ redraw_build_cells(struct redraw_build_ctx *bctx) TAILQ_FOREACH_REVERSE(wp, &w->z_index, window_panes_zindex, zentry) redraw_mark_pane(bctx, wp); redraw_mark_two_pane_colours(bctx); + redraw_mark_menu(bctx); } /* @@ -1303,6 +1351,20 @@ redraw_draw_scrollbar_span(struct redraw_draw_ctx *dctx, } } +/* Draw a menu span. */ +static void +redraw_draw_menu_span(struct redraw_draw_ctx *dctx, + struct redraw_span *span, u_int x, u_int y, u_int n) +{ + struct redraw_scene *scene = dctx->scene; + struct tty *tty = &scene->c->tty; + struct screen *s = menu_screen(span->data.m.md); + u_int px; + + px = span->data.m.px + (x - span->x); + tty_draw_line(tty, s, px, span->data.m.py, n, x, y, NULL); +} + /* Draw a span. */ static void redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span, @@ -1343,6 +1405,9 @@ redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span, case REDRAW_SPAN_SCROLLBAR: redraw_draw_scrollbar_span(dctx, span, x, y, n); break; + case REDRAW_SPAN_MENU: + redraw_draw_menu_span(dctx, span, x, y, n); + break; } } } @@ -1434,6 +1499,10 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags) if (~flags & REDRAW_PANE_SCROLLBAR) continue; break; + case REDRAW_SPAN_MENU: + if (~flags & REDRAW_MENU) + continue; + break; default: continue; } @@ -1445,6 +1514,26 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags) } } +/* Draw menu spans. */ +static void +redraw_draw_menu_lines(struct redraw_draw_ctx *dctx) +{ + struct redraw_scene *scene = dctx->scene; + struct redraw_line *line; + struct redraw_span *span; + u_int y, cy; + + for (y = 0; y < scene->sy; y++) { + line = &scene->lines[y]; + if (dctx->flags & REDRAW_STATUS_TOP) + cy = dctx->status_lines + y; + else + cy = y; + TAILQ_FOREACH(span, &line->spans[REDRAW_SPAN_MENU], entry) + redraw_draw_span(dctx, span, cy); + } +} + /* Get line for pane status line. */ static int redraw_pane_status_line(struct redraw_draw_ctx *dctx, @@ -1620,6 +1709,8 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) if (scene == NULL) return; redraw_set_draw_context(&dctx, scene); + if (w->menu != NULL) + menu_update(w->menu); if (flags & (REDRAW_PANE_BORDER|REDRAW_PANE_STATUS)) { TAILQ_FOREACH(loop, &scene->w->panes, entry) { @@ -1686,6 +1777,8 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) } } } + if (w->menu != NULL && (flags & REDRAW_MENU)) + redraw_draw_menu_lines(&dctx); if (flags & REDRAW_STATUS) { lines = dctx.status_lines; @@ -1766,6 +1859,10 @@ redraw_screen(struct client *c) flags |= (REDRAW_STATUS|REDRAW_PANE_STATUS); if (c->flags & CLIENT_REDRAWOVERLAY) flags |= REDRAW_OVERLAY; + if (c->flags & CLIENT_REDRAWMENU) + flags |= REDRAW_MENU; + if (c->session->curw->window->menu != NULL) + flags |= REDRAW_MENU; if (flags != 0) redraw_draw(c, NULL, flags); } @@ -1776,6 +1873,8 @@ void redraw_pane(struct client *c, struct window_pane *wp) { redraw_draw(c, wp, REDRAW_PANE|REDRAW_PANE_SCROLLBAR); + if (c->session->curw->window->menu != NULL) + redraw_draw(c, NULL, REDRAW_MENU); } /* Draw a pane's scrollbar. */ diff --git a/server-client.c b/server-client.c index c78015f2c..0a85dcb03 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.490 2026/07/14 17:17:17 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.491 2026/07/14 19:07:03 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -52,8 +52,6 @@ static void server_client_dispatch(struct imsg *, void *); static int server_client_dispatch_command(struct client *, struct imsg *); static int server_client_dispatch_identify(struct client *, struct imsg *); static int server_client_dispatch_shell(struct client *); -static void server_client_update_scrollbar_hover(struct client *, int, int, - int); static void server_client_report_theme(struct client *, enum client_theme); /* Compare client windows. */ @@ -1655,6 +1653,40 @@ out: return (CMD_RETURN_NORMAL); } +/* Handle a key event for the active window menu, if any. */ +static int +server_client_handle_menu_key(struct client *c, struct key_event *event) +{ + struct window *w = c->session->curw->window; + struct key_event new_event; + struct mouse_event *m; + u_int ox, oy, sx, sy; + + if (w->menu == NULL) + return (0); + + memcpy(&new_event, event, sizeof new_event); + if (KEYC_IS_MOUSE(event->key)) { + m = &new_event.m; + tty_window_offset(&c->tty, &ox, &oy, &sx, &sy); + m->x += ox; + if (m->statusat == 0) { + if (m->y < m->statuslines) + m->x = m->y = UINT_MAX; + else + m->y = m->y - m->statuslines + oy; + } else if (m->statusat > 0 && + m->y >= (u_int)m->statusat) + m->x = m->y = UINT_MAX; + else + m->y += oy; + } + + if (menu_key(c, w->menu, &new_event) == 1) + menu_close(w); + return (1); +} + /* Handle a key event. */ static int server_client_handle_key0(struct client *c, struct key_event *event, @@ -1704,6 +1736,8 @@ server_client_handle_key0(struct client *c, struct key_event *event, } server_client_clear_overlay(c); + if (server_client_handle_menu_key(c, event)) + return (0); if (c->prompt != NULL) { switch (status_prompt_key(c, event->key, &event->m)) { case PROMPT_KEY_HANDLED: @@ -1726,7 +1760,8 @@ server_client_handle_key0(struct client *c, struct key_event *event, if (wp != NULL && window_pane_has_prompt(wp) && window_pane_is_visible(wp)) { - switch (window_pane_prompt_key(wp, c, event->key, &event->m)) { + switch (window_pane_prompt_key(wp, c, event->key, + &event->m)) { case PROMPT_KEY_HANDLED: case PROMPT_KEY_CLOSE: case PROMPT_KEY_MOVE: @@ -2078,6 +2113,9 @@ server_client_reset_state(struct client *c) if (c->overlay_draw != NULL) { if (c->overlay_mode != NULL) s = c->overlay_mode(c, c->overlay_data, &cx, &cy); + } else if (w->menu != NULL) { + menu_get_cursor(w->menu, &cx, &cy); + s = menu_screen(w->menu); } else if (wp != NULL && c->prompt == NULL) s = wp->screen; else @@ -2098,7 +2136,22 @@ server_client_reset_state(struct client *c) prompt = 1; status_prompt_cursor(c, &cx, &cy); } else if (wp != NULL && c->overlay_draw == NULL) { - prompt = server_client_prompt_cursor(c, wp, &mode, &cx, &cy); + if (w->menu != NULL) { + tty_window_offset(tty, &ox, &oy, &sx, &sy); + if (cx < ox || cx >= ox + sx || + cy < oy || cy >= oy + sy) + mode &= ~MODE_CURSOR; + else { + cx -= ox; + cy -= oy; + if (status_at_line(c) == 0) + cy += status_line_size(c); + } + prompt = 1; + } else { + prompt = server_client_prompt_cursor(c, wp, &mode, &cx, + &cy); + } if (!prompt) { cursor = 0; pane_mode = wp->base.mode; @@ -2150,7 +2203,7 @@ server_client_reset_state(struct client *c) * movement events. */ if (options_get_number(oo, "mouse")) { - if (c->overlay_draw == NULL) { + if (c->overlay_draw == NULL && w->menu == NULL) { mode &= ~ALL_MOUSE_MODES; TAILQ_FOREACH(loop, &w->panes, entry) { if (loop->screen->mode & MODE_MOUSE_ALL) @@ -2329,11 +2382,12 @@ server_client_check_redraw(struct client *c) if (c->flags & (CLIENT_CONTROL|CLIENT_SUSPENDED)) return; if (c->flags & CLIENT_ALLREDRAWFLAGS) { - log_debug("%s: redraw%s%s%s%s", c->name, + log_debug("%s: redraw%s%s%s%s%s", c->name, (c->flags & CLIENT_REDRAWWINDOW) ? " window" : "", (c->flags & CLIENT_REDRAWSTATUS) ? " status" : "", (c->flags & CLIENT_REDRAWBORDERS) ? " borders" : "", - (c->flags & CLIENT_REDRAWOVERLAY) ? " overlay" : ""); + (c->flags & CLIENT_REDRAWOVERLAY) ? " overlay" : "", + (c->flags & CLIENT_REDRAWMENU) ? " menu" : ""); } /* Work out if a redraw is actually needed. */ diff --git a/server-fn.c b/server-fn.c index e3823dcbb..70e01860c 100644 --- a/server-fn.c +++ b/server-fn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-fn.c,v 1.147 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: server-fn.c,v 1.148 2026/07/14 19:07:03 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -129,6 +129,19 @@ server_redraw_window(struct window *w) } } +void +server_redraw_window_menu(struct window *w) +{ + struct client *c; + + TAILQ_FOREACH(c, &clients, entry) { + if (c->session != NULL && + c->session->curw != NULL && + c->session->curw->window == w) + c->flags |= CLIENT_REDRAWMENU; + } +} + void server_redraw_window_borders(struct window *w) { diff --git a/tmux.h b/tmux.h index 5ee3b26f6..535e77613 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1403 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1404 2026/07/14 19:07:03 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1401,6 +1401,10 @@ struct window { uint64_t redraw_scene_generation; + struct menu_data *menu; + u_int menu_last_px; + u_int menu_last_py; + u_int last_new_pane_x; u_int last_new_pane_y; @@ -2218,7 +2222,7 @@ struct client { #define CLIENT_CONTROL_NOOUTPUT 0x4000000 #define CLIENT_DEFAULTSOCKET 0x8000000 #define CLIENT_STARTSERVER 0x10000000 -/* 0x20000000 unused */ +#define CLIENT_REDRAWMENU 0x20000000 #define CLIENT_NOFORK 0x40000000 #define CLIENT_ACTIVEPANE 0x80000000ULL #define CLIENT_CONTROL_PAUSEAFTER 0x100000000ULL @@ -2234,7 +2238,8 @@ struct client { CLIENT_REDRAWSTATUS| \ CLIENT_REDRAWSTATUSALWAYS| \ CLIENT_REDRAWBORDERS| \ - CLIENT_REDRAWOVERLAY) + CLIENT_REDRAWOVERLAY| \ + CLIENT_REDRAWMENU) #define CLIENT_UNATTACHEDFLAGS \ (CLIENT_DEAD| \ CLIENT_SUSPENDED| \ @@ -2288,9 +2293,6 @@ 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; @@ -3272,6 +3274,7 @@ void server_redraw_session_group(struct session *); void server_status_session(struct session *); void server_status_session_group(struct session *); void server_redraw_window(struct window *); +void server_redraw_window_menu(struct window *); void server_redraw_window_borders(struct window *); void server_status_window(struct window *); void server_lock(void); @@ -4063,20 +4066,21 @@ void menu_add_item(struct menu *, const struct menu_item *, struct cmdq_item *, struct client *, struct cmd_find_state *); void menu_free(struct menu *); -struct menu_data *menu_prepare(struct menu *, int, int, struct cmdq_item *, - u_int, u_int, struct client *, enum box_lines, const char *, - const char *, const char *, struct cmd_find_state *, - menu_choice_cb, void *); int menu_display(struct menu *, int, int, struct cmdq_item *, u_int, u_int, struct client *, enum box_lines, const char *, const char *, const char *, struct cmd_find_state *, menu_choice_cb, void *); -struct screen *menu_mode_cb(struct client *, void *, u_int *, u_int *); -struct visible_ranges *menu_check_cb(struct client *, void *, u_int, u_int, - u_int); -void menu_draw_cb(struct client *, void *); -void menu_free_cb(struct client *, void *); -int menu_key_cb(struct client *, void *, struct key_event *); +void menu_close(struct window *); +void menu_destroy(struct window *); +void menu_update(struct menu_data *); +struct screen *menu_screen(struct menu_data *); +u_int menu_width(struct menu_data *); +u_int menu_height(struct menu_data *); +u_int menu_x(struct menu_data *); +u_int menu_y(struct menu_data *); +void menu_get_cursor(struct menu_data *, u_int *, u_int *); +void menu_resize(struct menu_data *, struct window *); +int menu_key(struct client *, struct menu_data *, struct key_event *); /* popup.c */ #define POPUP_CLOSEEXIT 0x1 diff --git a/window.c b/window.c index 7c7a7b788..487aaa3f7 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.362 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.363 2026/07/14 19:07:03 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -462,6 +462,7 @@ window_destroy(struct window *w) layout_free_cell(w->saved_layout_root, 0); free(w->old_layout); + menu_destroy(w); window_destroy_panes(w); if (event_initialized(&w->name_event)) @@ -570,6 +571,11 @@ window_resize(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) ypixel == -1 ? w->ypixel : (u_int)ypixel); w->sx = sx; w->sy = sy; + if (w->menu != NULL) { + menu_resize(w->menu, w); + redraw_invalidate_scene(w); + server_redraw_window(w); + } if (xpixel != -1) w->xpixel = xpixel; if (ypixel != -1) @@ -644,7 +650,8 @@ window_pane_update_focus(struct window_pane *wp) c->session->attached != 0 && (c->flags & CLIENT_FOCUSED) && c->session->curw->window == wp->window && - c->overlay_draw == NULL) { + c->overlay_draw == NULL && + wp->window->menu == NULL) { focused = 1; break; }