From 59d714569f6844af928878b8c717e6092d3884a1 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Thu, 1 Oct 2026 08:13:25 +0100 Subject: [PATCH] Scale pane on unhiding to window when necessary Co-Authored-By: Claude Opus 5.5 --- cmd-join-pane.c | 2 +- cmd-resize-pane.c | 18 +- layout.c | 2 +- regress/pane-hidden.sh | 342 ++++++++++++++++++++++++++++++ regress/status-pane-list-click.sh | 92 ++++++++ tmux.h | 9 +- window.c | 26 ++- 7 files changed, 472 insertions(+), 19 deletions(-) create mode 100644 regress/pane-hidden.sh create mode 100644 regress/status-pane-list-click.sh diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 7443af56a..56f64f878 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -433,7 +433,7 @@ cmd_join_pane_tile(struct cmdq_item *item, struct args *args, struct window *w, /* It takes its space when it is shown. */ if (lc->parent != NULL) layout_set_size(lc, 0, 0, 0, 0); - lc->tg.sx = UINT_MAX; + lc->hidden.g.sx = UINT_MAX; } else if (layout_insert_tile(w, lc) != 0) { cmdq_error(item, "no space for a new pane"); return (CMD_RETURN_ERROR); diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index dd27b83bf..d074c3ab1 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -59,18 +59,18 @@ static void cmd_resize_pane_desktop(struct window *w) { struct window_pane *wp; - int marked = 0; + int shown = 0; TAILQ_FOREACH(wp, &w->panes, entry) { - if (wp->flags & PANE_HIDDENALL) - marked = 1; - } - TAILQ_FOREACH(wp, &w->panes, entry) { - if (marked) { - if (wp->flags & PANE_HIDDENALL) - window_show_pane(wp); + if (~wp->flags & PANE_HIDDENALL) continue; - } + window_show_pane(wp); + shown = 1; + } + if (shown) + return; + + TAILQ_FOREACH(wp, &w->panes, entry) { if (wp == w->modal || (wp->flags & PANE_HIDDEN)) continue; if (!window_pane_is_raised(wp)) diff --git a/layout.c b/layout.c index 723e61c51..486134fc8 100644 --- a/layout.c +++ b/layout.c @@ -79,7 +79,7 @@ layout_create_cell(struct layout_cell *lcparent) layout_geometry_init(&lc->g); layout_geometry_init(&lc->fg); - layout_geometry_init(&lc->tg); + layout_geometry_init(&lc->hidden.g); return (lc); } diff --git a/regress/pane-hidden.sh b/regress/pane-hidden.sh new file mode 100644 index 000000000..315dec95d --- /dev/null +++ b/regress/pane-hidden.sh @@ -0,0 +1,342 @@ +#!/bin/sh + +# A pane can be hidden (minimised) and shown again, returning to exactly its +# previous state: tiled, floating and zoomed panes alike. +# +# This exercises: +# - resize-pane -H toggling a tiled pane, with its space given to a neighbour +# and returned unchanged when it is shown; +# - hiding floating and zoomed panes and hiding every pane in a window; +# - activating a hidden pane showing it, and next/previous skipping hidden +# panes; +# - resize-pane -a -H hiding every visible float and zoomed pane and showing +# only the panes it hid; +# - floating, tiling, swapping and killing hidden panes. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null +trap '$TMUX kill-server 2>/dev/null' 0 +trap 'exit 1' 1 2 15 + +fail() +{ + echo "$*" >&2 + exit 1 +} + +run() +{ + $TMUX "$@" || fail "failed: $*" +} + +# check $target $format $expected +check() +{ + got=$(run display-message -p -t "$1" "$2") || exit 1 + [ "$got" = "$3" ] || fail "$1 $2: got '$got', expected '$3'" +} + +# hidden $pane $expected +hidden() +{ + check "$1" '#{pane_hidden_flag}' "$2" +} + +# geometry $pane $expected +geometry() +{ + check "$1" '#{pane_left}:#{pane_top}:#{pane_width}:#{pane_height}' "$2" +} + +# layout $target: the window layout ignoring which panes are active or last. +layout() +{ + got=$(run display-message -p -t "$1" '#{window_layout}') || exit 1 + echo "$got" | sed 's/"a":true,//g; s/"l":[0-9]*,//g' +} + +# same_layout $target: is the layout the same as when the test started? +same_layout() +{ + got=$(layout "$1") + [ "$got" = "$layout" ] || fail "layout changed: $got, expected $layout" +} + +reset() +{ + $TMUX kill-server 2>/dev/null + run new-session -d -x 80 -y 24 + A=$(run display-message -p '#{pane_id}') || exit 1 + B=$(run split-window -dPF '#{pane_id}') || exit 1 + layout=$(layout "$A") +} + +# A tiled pane gives its space away and gets it back unchanged. +reset +hidden "$B" 0 +run resize-pane -H -t "$B" +hidden "$B" 1 +check "$A" '#{pane_height}' 24 +run resize-pane -H -t "$B" +hidden "$B" 0 +same_layout "$A" + +# Selecting a hidden pane shows it and makes it active. +run resize-pane -H -t "$B" +run select-pane -t "$B" +hidden "$B" 0 +check "$B" '#{pane_active}' 1 +same_layout "$A" + +# Hiding the active pane moves the focus to a visible pane. +run resize-pane -H -t "$B" +check "$A" '#{pane_active}' 1 + +# Next and previous skip hidden panes. +reset +C=$(run split-window -dPF '#{pane_id}') || exit 1 +run select-pane -t "$A" +run resize-pane -H -t "$B" +run select-pane -t :.+ +check "$C" '#{pane_active}' 1 +run select-pane -t :.- +check "$A" '#{pane_active}' 1 + +# Every tiled pane can be hidden and shown again. +reset +run resize-pane -H -t "$B" +run resize-pane -H -t "$A" +hidden "$A" 1 +hidden "$B" 1 +run display-message -p ok >/dev/null +run select-pane -t "$A" +run select-pane -t "$B" +hidden "$A" 0 +hidden "$B" 0 +same_layout "$A" + +# A floating pane keeps its geometry and stacking position. +reset +X=$(run new-pane -dPF '#{pane_id}' -t "$A" -x 20 -y 8 -X 8 -Y 3 '') || exit 1 +Y=$(run new-pane -dPF '#{pane_id}' -t "$A" -x 20 -y 8 -X 30 -Y 6 '') || exit 1 +gx=$(run display-message -p -t "$X" '#{pane_left}:#{pane_top}:#{pane_width}:#{pane_height}') +zx=$(run display-message -p -t "$X" '#{pane_z}') +run resize-pane -H -t "$X" +hidden "$X" 1 +check "$X" '#{pane_floating_flag}' 1 +run select-pane -t "$X" +hidden "$X" 0 +check "$X" '#{pane_active}' 1 +geometry "$X" "$gx" +check "$Y" '#{pane_z}' 1 + +# A zoomed pane stays zoomed while hidden and when shown. +reset +run resize-pane -Z -t "$A" +run resize-pane -H -t "$A" +hidden "$A" 1 +check "$A" '#{pane_zoomed_flag}' 1 +run resize-pane -H -t "$A" +hidden "$A" 0 +check "$A" '#{pane_zoomed_flag}:#{window_zoomed_flag}' '1:1' + +# Hiding every pane in a window, floating ones included, leaves an empty +# window that still works. +reset +X=$(run new-pane -dPF '#{pane_id}' -t "$A" -x 20 -y 8 -X 8 -Y 3 '') || exit 1 +for p in "$X" "$A" "$B"; do + run resize-pane -H -t "$p" +done +for p in "$X" "$A" "$B"; do + hidden "$p" 1 +done +run display-message -p '#{window_layout}' >/dev/null +run select-pane -t "$B" +hidden "$B" 0 +check "$B" '#{pane_active}' 1 + +# resize-pane -a -H hides every visible float and zoomed pane and a second +# call shows only those, leaving panes that were already hidden alone. +reset +X=$(run new-pane -dPF '#{pane_id}' -t "$A" -x 20 -y 8 -X 8 -Y 3 '') || exit 1 +Y=$(run new-pane -dPF '#{pane_id}' -t "$A" -x 20 -y 8 -X 30 -Y 6 '') || exit 1 +run resize-pane -H -t "$Y" +run resize-pane -a -H -t "$A" +hidden "$X" 1 +hidden "$Y" 1 +hidden "$A" 0 +hidden "$B" 0 +run resize-pane -a -H -t "$A" +hidden "$X" 0 +hidden "$Y" 1 +hidden "$A" 0 +run resize-pane -Z -t "$B" +run resize-pane -a -H -t "$A" +hidden "$B" 1 +hidden "$X" 1 +run resize-pane -a -H -t "$A" +hidden "$B" 0 +check "$B" '#{pane_zoomed_flag}' 1 + +# Floating and tiling a hidden pane keeps it hidden. +reset +run resize-pane -H -t "$B" +run break-pane -W -s "$B" +hidden "$B" 1 +check "$B" '#{pane_floating_flag}' 1 +run join-pane -s "$B" -t "$B" +hidden "$B" 1 +check "$B" '#{pane_floating_flag}' 0 +run select-pane -t "$B" +hidden "$B" 0 +check "$B" '#{pane_floating_flag}' 0 + +# A hidden pane can be swapped and killed. The hidden state belongs to the +# position in the layout. +reset +C=$(run split-window -dPF '#{pane_id}') || exit 1 +run resize-pane -H -t "$B" +run swap-pane -s "$A" -t "$B" +hidden "$A" 1 +hidden "$B" 0 +run kill-pane -t "$A" +run display-message -p -t "$B" ok >/dev/null +check "$B" '#{window_panes}' 2 + +# Splitting a hidden tiled pane shows it first. +reset +run resize-pane -H -t "$B" +run split-window -d -t "$B" +hidden "$B" 0 +check "$B" '#{window_panes}' 3 + +# Resizing a hidden tiled pane shows it first, but a hidden float is resized +# without being shown. +reset +run resize-pane -H -t "$B" +run resize-pane -t "$B" -y 5 +hidden "$B" 0 +X=$(run new-pane -dPF '#{pane_id}' -t "$A" -x 20 -y 8 -X 8 -Y 3 '') || exit 1 +run resize-pane -H -t "$X" +run resize-pane -t "$X" -x 30 +hidden "$X" 1 +run select-pane -t "$X" +check "$X" '#{pane_width}' 28 + +# A hidden pane moved to another window is shown there. +reset +C=$(run split-window -dPF '#{pane_id}') || exit 1 +run resize-pane -H -t "$C" +run break-pane -d -s "$C" +hidden "$C" 0 +check "$C" '#{window_panes}' 1 +check "$A" '#{window_panes}' 2 + +# Layouts record hidden floating panes and leave hidden tiled panes out; a +# saved layout puts the hidden state back. +reset +X=$(run new-pane -dPF '#{pane_id}' -t "$A" -x 20 -y 8 -X 8 -Y 3 '') || exit 1 +run resize-pane -H -t "$X" +saved=$(run display-message -p -t "$A" '#{window_layout}') || exit 1 +case "$saved" in +*'"H":true'*) ;; +*) fail "hidden floating pane not in layout: $saved" ;; +esac +run select-pane -t "$X" +hidden "$X" 0 +run select-layout -t "$A" "$saved" +hidden "$X" 1 +run resize-pane -H -t "$B" +out=$(run display-message -p -t "$A" '#{window_layout}') || exit 1 +case "$out" in +*"\"$B\""*) fail "hidden tiled pane in layout: $out" ;; +*) ;; +esac + +# Swapping with the next or previous pane when the only tiled pane is hidden +# has nothing to swap with and does nothing. +reset +X=$(run new-pane -dPF '#{pane_id}' -t "$A" -x 20 -y 8 -X 8 -Y 3 '') || exit 1 +run kill-pane -t "$B" +run resize-pane -H -t "$A" +run swap-pane -D -t "$A" +run swap-pane -U -t "$A" +hidden "$A" 1 +check "$X" '#{window_panes}' 2 + +# Preset layouts lay out only the tiled panes that are not hidden and leave the +# hidden ones hidden. +reset +C=$(run split-window -dPF '#{pane_id}') || exit 1 +run resize-pane -H -t "$B" +for layout in tiled even-horizontal even-vertical main-horizontal \ + main-vertical; do + run select-layout -t "$A" $layout + hidden "$B" 1 + hidden "$A" 0 + hidden "$C" 0 +done +run next-layout -t "$A" +hidden "$B" 1 +run resize-pane -H -t "$C" +run select-layout -t "$A" tiled +run next-layout -t "$A" +hidden "$C" 1 +check "$A" '#{pane_width}:#{pane_height}' '80:24' + +# The size a hidden tiled pane gets back follows the window size, so showing it +# after the window shrank does not squeeze its neighbours out. +reset +run kill-server +run new-session -d -x 80 -y 30 +A=$(run display-message -p '#{pane_id}') || exit 1 +B=$(run split-window -dPF '#{pane_id}') || exit 1 +C=$(run split-window -dPF '#{pane_id}') || exit 1 +run select-layout even-vertical +before=$(run display-message -p -t "$B" '#{pane_height}') || exit 1 +run resize-pane -H -t "$B" +run resize-window -x 80 -y 16 +run select-pane -t "$B" +hidden "$B" 0 +for p in "$A" "$B" "$C"; do + h=$(run display-message -p -t "$p" '#{pane_height}') || exit 1 + [ "$h" -ge 3 ] || fail "$p is only $h lines high after showing" +done +h=$(run display-message -p -t "$B" '#{pane_height}') || exit 1 +[ "$h" -lt "$before" ] || fail "shown pane is $h lines, was $before" + +# Without the window changing it gets back exactly the size it had. +run resize-window -x 80 -y 30 +run select-layout even-vertical +run resize-pane -t "$A" -y 6 +sizes=$(run list-panes -F '#{pane_id}:#{pane_height}') || exit 1 +run resize-pane -H -t "$B" +run resize-pane -H -t "$B" +check "$A" '#{pane_height}' 6 +after=$(run list-panes -F '#{pane_id}:#{pane_height}') || exit 1 +[ "$sizes" = "$after" ] || fail "sizes changed: '$sizes' became '$after'" + +# Showing a hidden pane only resizes it when it does not already have the right +# size, so a plain hide and show announces a layout change once each. +$TMUX kill-server 2>/dev/null +run new-session -d -x 80 -y 23 +A=$(run display-message -p '#{pane_id}') || exit 1 +B=$(run split-window -dPF '#{pane_id}') || exit 1 +sizes=$(run list-panes -F '#{pane_id}:#{pane_height}') || exit 1 +run set -g @count 0 +run set-hook -g window-layout-changed "set -gF @count '#{e|+:#{@count},1}'" +run resize-pane -H -t "$B" +count=$(run show -gv @count) || exit 1 +run resize-pane -H -t "$B" +got=$(run show -gv @count) || exit 1 +[ $((got - count)) -eq 1 ] || + fail "showing the pane fired $((got - count)) layout changes, expected 1" +after=$(run list-panes -F '#{pane_id}:#{pane_height}') || exit 1 +[ "$sizes" = "$after" ] || fail "sizes changed: '$sizes' became '$after'" +run set-hook -gu window-layout-changed + +exit 0 diff --git a/regress/status-pane-list-click.sh b/regress/status-pane-list-click.sh new file mode 100644 index 000000000..db67a8f6a --- /dev/null +++ b/regress/status-pane-list-click.sh @@ -0,0 +1,92 @@ +#!/bin/sh + +# Clicking a pane in the pane list on the second status line selects it, hides +# it if it is already the active pane and shows it again if it is hidden. The +# button at the right hides every floating and zoomed pane and, clicked again, +# shows just those. + +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 +} + +# click COL ROW +# +# Write an SGR mouse press then release at a 1-based position to the outer pane +# holding the inner client. +click() +{ + seq=$(printf '\033[<0;%s;%sM\033[<0;%s;%sm' "$1" "$2" "$1" "$2") + $TMUX2 send-keys -t "$OUTER" -l "$seq" || fail "send-keys failed." + sleep 1 +} + +# check PANE FORMAT EXPECTED +check() +{ + got=$($TMUX display-message -p -t "$1" "$2") + [ "$got" = "$3" ] || fail "$1 $2: got '$got', expected '$3'" +} + +cleanup + +$TMUX new-session -d -s inner -x 60 -y 12 'sleep 100' || exit 1 +$TMUX set -g mouse on || exit 1 +$TMUX set -g status 2 || exit 1 +a=$($TMUX display-message -p '#{pane_id}') || exit 1 +b=$($TMUX split-window -dPF '#{pane_id}' 'sleep 100') || exit 1 +f=$($TMUX new-pane -dPF '#{pane_id}' -x 20 -y 4 -X 20 -Y 2 'sleep 100') || + exit 1 + +$TMUX2 new-session -d -x 60 -y 12 "$TMUX attach -t inner" || exit 1 +sleep 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." + +# The second status line is the last row: " P: " then one entry per pane in +# pane order (a, b, f), each about twelve columns wide, and the button at the +# far right. +row=12 + +entry_b=24 +button=59 + +# Clicking another pane selects it. +check "$a" '#{pane_active}' 1 +click $entry_b $row +check "$b" '#{pane_active}:#{pane_hidden_flag}' '1:0' + +# Clicking the active pane hides it and the focus moves to a visible pane. +click $entry_b $row +check "$b" '#{pane_hidden_flag}' 1 +check "$b" '#{pane_active}' 0 + +# Clicking a hidden pane shows it and selects it. +click $entry_b $row +check "$b" '#{pane_hidden_flag}:#{pane_active}' '0:1' + +# The button hides the floating pane but not the tiled ones, then shows it. +check "$f" '#{pane_hidden_flag}' 0 +click $button $row +check "$f" '#{pane_hidden_flag}' 1 +check "$a" '#{pane_hidden_flag}' 0 +check "$b" '#{pane_hidden_flag}' 0 +click $button $row +check "$f" '#{pane_hidden_flag}' 0 + +cleanup +exit 0 diff --git a/tmux.h b/tmux.h index bf115b420..db5a5f8ad 100644 --- a/tmux.h +++ b/tmux.h @@ -1581,6 +1581,13 @@ struct layout_geometry { int yoff; }; +/* Size of a tiled cell and of the window when its pane was hidden. */ +struct layout_hidden { + struct layout_geometry g; + u_int wsx; + u_int wsy; +}; + /* Layout cells queue. */ TAILQ_HEAD(layout_cells, layout_cell); @@ -1595,7 +1602,7 @@ struct layout_cell { struct layout_geometry g; struct layout_geometry fg; /* saved floating pane */ - struct layout_geometry tg; /* saved tiled pane, when hidden */ + struct layout_hidden hidden; struct window_pane *wp; struct layout_cells cells; diff --git a/window.c b/window.c index 68344d7f7..5357f9d18 100644 --- a/window.c +++ b/window.c @@ -1030,7 +1030,9 @@ window_hide_pane(struct window_pane *wp) /* A tiled pane gives its space to a neighbour. */ if (layout_cell_is_tiled(lc)) { - memcpy(&lc->tg, &lc->g, sizeof lc->tg); + memcpy(&lc->hidden.g, &lc->g, sizeof lc->hidden.g); + lc->hidden.wsx = w->sx; + lc->hidden.wsy = w->sy; layout_remove_tile(w, lc); } wp->flags |= PANE_HIDDEN; @@ -1064,6 +1066,8 @@ window_show_pane(struct window_pane *wp) { struct window *w = wp->window; struct layout_cell *lc = wp->layout_cell; + enum layout_type type; + u_int size, current; int tiled; if (~wp->flags & PANE_HIDDEN) @@ -1074,12 +1078,20 @@ window_show_pane(struct window_pane *wp) return (-1); wp->flags &= ~(PANE_HIDDEN|PANE_HIDDENALL); - /* Try to get back the size the pane had. */ - if (tiled && lc->parent != NULL && lc->tg.sx != UINT_MAX) { - if (lc->parent->type == LAYOUT_LEFTRIGHT) - layout_resize_pane_to(wp, lc->parent->type, lc->tg.sx); - else - layout_resize_pane_to(wp, lc->parent->type, lc->tg.sy); + /* Try to get back the size the pane had, scaled to the window. */ + if (tiled && lc->parent != NULL && lc->hidden.g.sx != UINT_MAX) { + type = lc->parent->type; + if (type == LAYOUT_LEFTRIGHT) { + size = lc->hidden.g.sx * w->sx / lc->hidden.wsx; + current = lc->g.sx; + } else { + size = lc->hidden.g.sy * w->sy / lc->hidden.wsy; + current = lc->g.sy; + } + if (size < PANE_MINIMUM) + size = PANE_MINIMUM; + if (size != current) + layout_resize_pane_to(wp, type, size); } layout_fix_offsets(w); layout_fix_panes(w, NULL);