From 2345f71df917f88191f94fa6aa01a5a9fc3946be Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Thu, 1 Oct 2026 16:03:16 +0100 Subject: [PATCH] Bugfixes. Co-Authored-By: Claude Opus 5.5 --- cmd-resize-pane.c | 8 +- regress/pane-hidden.sh | 177 +++++++++++++++++++++++++++++++++++++++++ regress/pane-states.sh | 41 +++++++--- tmux.h | 10 ++- window.c | 59 +++++++++++--- 5 files changed, 266 insertions(+), 29 deletions(-) diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index d074c3ab1..9455c7687 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -115,9 +115,13 @@ cmd_resize_pane_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'H')) { if (args_has(args, 'a')) cmd_resize_pane_desktop(w); - else if (wp->flags & PANE_HIDDEN) + else if (wp->flags & PANE_HIDDEN) { window_set_active_pane(w, wp, 1); - else + if (wp->flags & PANE_HIDDEN) { + cmdq_error(item, "no space to show pane"); + return (CMD_RETURN_ERROR); + } + } else window_hide_pane(wp); server_redraw_window(w); return (CMD_RETURN_NORMAL); diff --git a/regress/pane-hidden.sh b/regress/pane-hidden.sh index 7e97f5b16..bee110a43 100644 --- a/regress/pane-hidden.sh +++ b/regress/pane-hidden.sh @@ -371,4 +371,181 @@ run select-pane -L -t "$A" check "$A" '#{pane_active}' 1 hidden "$X" 1 +# Mixed vertical and horizontal splits, including three or more panes side by +# side. Hiding and showing each pane puts every pane back where it was. Hiding +# several panes and showing them again in the reverse order they were hidden +# leaves every pane within two cells of where it was. In the same order the panes +# have the wrong neighbours when they are shown so their sizes cannot be exact, +# but every pane must be back, tiled and not squeezed. Floating panes, including +# ones that were never tiled, are part of this: they are not moved by hiding +# other panes and tile somewhere in the layout without squeezing it. +# +# geom: id, left, top, width and height of every pane, sorted by id. +geom() +{ + run list-panes -F '#{pane_id} #{pane_left} #{pane_top} #{pane_width} #{pane_height}' | + sort +} + +# near $name $base $now $tolerance +near() +{ + echo "$2 +$3" | awk -v tol="$4" -v name="$1" ' + NF == 5 && ($1 in seen) { + for (i = 2; i <= 5; i++) { + d = $i - seen[$1, i] + if (d < 0) d = -d + if (d > tol) { + printf "%s: %s differs by %d (%s became %s)\n", name, $1, d, seen[$1, 0], $0 + bad = 1 + } + } + next + } + NF == 5 { seen[$1] = 1; for (i = 2; i <= 5; i++) seen[$1, i] = $i; seen[$1, 0] = $0 } + END { exit bad }' || fail "layout not restored" +} + +# usable $name $panes $minimum: the panes are tiled, shown and at least the +# minimum size in both directions. +usable() +{ + for p in $2; do + check "$p" '#{pane_floating_flag}:#{pane_hidden_flag}' 0:0 + w=$(run display-message -p -t "$p" '#{pane_width}') || exit 1 + h=$(run display-message -p -t "$p" '#{pane_height}') || exit 1 + [ "$w" -ge "$3" ] && [ "$h" -ge "$3" ] || + fail "$1: $p is only ${w}x$h" + done +} + +# build $size $splits...: a window made of splits, each a flag and a target. +build() +{ + size=$1 + shift + $TMUX kill-server 2>/dev/null + run new-session -d -x "${size%x*}" -y "${size#*x}" cat + while [ $# -gt 0 ]; do + run split-window -d "$1" -t "$2" '' + shift 2 + done +} + +mixed() +{ + name=$1 + size=$2 + shift 2 + splits="$*" + build $size $splits + base=$(geom) + ids=$(run list-panes -F '#{pane_id}' | sort) + n=$(echo "$ids" | wc -l) + last=$(echo "$ids" | tail -1) + + # Each pane in turn. + for p in $ids; do + run resize-pane -H -t "$p" + hidden "$p" 1 + run resize-pane -H -t "$p" + hidden "$p" 0 + near "$name hiding $p" "$base" "$(geom)" 0 + done + + # Every pane but the last, shown in the reverse and in the same order. + for order in reverse forward; do + hide= + for p in $ids; do + [ "$p" = "$last" ] && continue + hide="$hide $p" + run resize-pane -H -t "$p" + done + show=$hide + if [ "$order" = reverse ]; then + show= + for p in $hide; do + show="$p $show" + done + fi + for p in $show; do + run resize-pane -H -t "$p" + done + for p in $ids; do + hidden "$p" 0 + done + if [ "$order" = reverse ]; then + near "$name hiding $((n - 1)) panes, $order" "$base" "$(geom)" 2 + else + usable "$name hiding $((n - 1)) panes, $order" "$ids" 1 + fi + done + + # Any two panes, hidden and shown in the same order, even if they are + # next to each other: nothing is squeezed. + if [ "$size" = 120x40 ]; then + for p in $ids; do + for q in $ids; do + [ "$p" = "$q" ] && continue + build $size $splits + run resize-pane -H -t "$p" + run resize-pane -H -t "$q" + run resize-pane -H -t "$p" + run resize-pane -H -t "$q" + usable "$name hiding $p and $q" "$ids" 2 + done + done + fi + + # A floating pane that was never tiled: hiding and showing it, or any + # tiled pane, changes nothing else. + build $size $splits + f=$(run new-pane -dPF '#{pane_id}' -x 20 -y 6 -X 5 -Y 5 '') || exit 1 + base=$(geom) + for p in $ids $f; do + run resize-pane -H -t "$p" + run resize-pane -H -t "$p" + near "$name with a float, hiding $p" "$base" "$(geom)" 0 + done + check "$f" '#{pane_floating_flag}' 1 + + # Tiling it puts it in the layout without squeezing the others, and it + # can be floated and tiled again. + run join-pane -s "$f" -t "$f" + usable "$name tiling a new float" "$ids $f" 1 + run break-pane -W -s "$f" + check "$f" '#{pane_floating_flag}' 1 + run join-pane -s "$f" -t "$f" + usable "$name tiling the float again" "$ids $f" 1 + + # Floating, hiding and tiling panes together. + for p in $ids; do + [ "$p" = "$last" ] && continue + build $size $splits + f=$(run new-pane -dPF '#{pane_id}' -x 20 -y 6 -X 5 -Y 5 '') || exit 1 + run break-pane -d -W -s "$p" + run resize-pane -H -t "$last" + run resize-pane -H -t "$last" + run join-pane -d -s "$p" -t "$p" + run join-pane -d -s "$f" -t "$f" + usable "$name floating $p, hiding $last, tiling both" "$ids $f" 1 + done +} + +for size in 80x24 120x40; do + mixed "A|(B/C) $size" $size -h %0 -v %1 + mixed "A/(B|C) $size" $size -v %0 -h %1 + mixed "(A|C)/B $size" $size -v %0 -h %0 + mixed "(A/C)|B $size" $size -h %0 -v %0 + mixed "2x2 $size" $size -v %0 -h %0 -h %1 + mixed "A|(B/(C|D)) $size" $size -h %0 -v %1 -h %2 + mixed "A/(B|(C/D)) $size" $size -v %0 -h %1 -v %2 +done +# Three or more side by side need room. +mixed "A/(B|C|D)/E 120x40" 120x40 -v %0 -v %1 -v %2 -h %1 -h %1 +mixed "(A/B/C)|(D|E) 120x40" 120x40 -h %0 -v %0 -v %0 -h %1 +mixed "A|(B/C/D)|E 120x40" 120x40 -h %0 -h %1 -v %1 -v %1 +mixed "A/(B|(C/D|E))/F 120x40" 120x40 -v %0 -v %1 -h %1 -v %3 -h %3 + exit 0 diff --git a/regress/pane-states.sh b/regress/pane-states.sh index 3f3309b2d..5c49759ed 100644 --- a/regress/pane-states.sh +++ b/regress/pane-states.sh @@ -14,6 +14,9 @@ # - split-window, new-pane and kill-pane; # - next-layout, select-layout, rotate-window and resize-window. # +# The tiled panes are laid out in different shapes: split top to bottom or side +# by side, and for three panes stacked, in a row or nested. +# # It also checks properties that must hold after every command: the active pane # is not hidden if any pane is not, there is one active pane, a window with one # pane is not zoomed, window_zoomed_flag matches the panes and visible panes have @@ -282,12 +285,22 @@ run_one() tgt=$4 RUN=$((RUN + 1)) + # The tiled panes are split in different ways: top to bottom or side by + # side for two, and stacked, in a row or nested for three. set -- new-session -d -x 80 -y 24 cat - i=1 - while [ $i -lt "$n" ]; do - set -- "$@" ';' split-window -d '' - i=$((i + 1)) - done + if [ "$n" -eq 2 ]; then + shape=$((RUN % 2)) + else + shape=$((RUN % 3)) + fi + case "$n$shape" in + 20) set -- "$@" ';' split-window -d '' ;; + 21) set -- "$@" ';' split-window -d -h '' ;; + 30) set -- "$@" ';' split-window -d '' ';' split-window -d '' ;; + 31) set -- "$@" ';' split-window -d -h '' ';' split-window -d -h '' ;; + 32) set -- "$@" ';' split-window -d -h -t %0 '' \ + ';' split-window -d -v -t %1 '' ;; + esac want= for stage in 1 2 4; do i=0 @@ -381,7 +394,7 @@ while [ $s -lt 64 ]; do s=$((s + 1)) done -# Three panes: every state, and one operation on each, rotating so that every +# Three panes: every state, and two operations on each, rotating so that every # operation and target is used with many different states. ALL="$GLOBAL" for op in $PER_PANE; do @@ -391,13 +404,15 @@ set -- $ALL M=$# s=0 while [ $s -lt 512 ]; do - set -- $ALL - shift $(( (s * 7 + s / M) % M )) - item=$1 - case $item in - *:*) run_one 3 $s "${item%:*}" "%${item#*:}" ;; - *) run_one 3 $s "$item" %0 ;; - esac + for k in $(( (s * 7 + s / M) % M )) $(( (s * 5 + 11) % M )); do + set -- $ALL + shift $k + item=$1 + case $item in + *:*) run_one 3 $s "${item%:*}" "%${item#*:}" ;; + *) run_one 3 $s "$item" %0 ;; + esac + done s=$((s + 1)) done diff --git a/tmux.h b/tmux.h index db5a5f8ad..ee679163a 100644 --- a/tmux.h +++ b/tmux.h @@ -1581,11 +1581,15 @@ struct layout_geometry { int yoff; }; -/* Size of a tiled cell and of the window when its pane was hidden. */ +/* + * Size of a tiled cell, of its parent cell and the number of tiled cells next to + * it when its pane was hidden. + */ struct layout_hidden { struct layout_geometry g; - u_int wsx; - u_int wsy; + u_int psx; + u_int psy; + u_int nsib; }; /* Layout cells queue. */ diff --git a/window.c b/window.c index c44c194e5..478b80dce 100644 --- a/window.c +++ b/window.c @@ -766,7 +766,8 @@ window_activate_pane(struct window *w, struct window_pane *wp, int notify, if (w->modal != NULL && wp != w->modal) return (0); if (wp->flags & PANE_HIDDEN) { - window_show_pane(wp); + if (window_show_pane(wp) != 0) + return (0); if (wp == w->active) return (1); } @@ -1017,6 +1018,23 @@ window_raise_pane(struct window_pane *wp) redraw_invalidate_scene(w); } +/* Count the tiled cells next to a cell. */ +static u_int +window_count_tiled_siblings(struct layout_cell *lc) +{ + struct layout_cell *lcsib; + u_int n = 0; + + TAILQ_FOREACH(lcsib, &lc->parent->cells, entry) { + if (lcsib == lc) + continue; + if (layout_cell_is_tiled(lcsib) || + layout_cell_has_tiled_child(lcsib)) + n++; + } + return (n); +} + /* * Hide a pane. It keeps its place in the layout and in the stacking order and * stays zoomed or floating, so showing it again puts it back as it was. @@ -1031,11 +1049,18 @@ window_hide_pane(struct window_pane *wp) if ((wp->flags & PANE_HIDDEN) || wp == w->modal || lc == NULL) return; - /* A tiled pane gives its space to a neighbour. */ + /* + * A tiled pane gives its space to a neighbour. Its size is only worth + * keeping if it shares the space with one. + */ if (layout_cell_is_tiled(lc)) { - memcpy(&lc->hidden.g, &lc->g, sizeof lc->hidden.g); - lc->hidden.wsx = w->sx; - lc->hidden.wsy = w->sy; + if (layout_cell_get_neighbour(lc) != NULL) { + memcpy(&lc->hidden.g, &lc->g, sizeof lc->hidden.g); + lc->hidden.psx = lc->parent->g.sx; + lc->hidden.psy = lc->parent->g.sy; + lc->hidden.nsib = window_count_tiled_siblings(lc); + } else + lc->hidden.g.sx = UINT_MAX; layout_remove_tile(w, lc); } wp->flags |= PANE_HIDDEN; @@ -1068,27 +1093,39 @@ int window_show_pane(struct window_pane *wp) { struct window *w = wp->window; - struct layout_cell *lc = wp->layout_cell; + struct layout_cell *lc = wp->layout_cell, *lcneighbour = NULL; enum layout_type type; - u_int size, current; + u_int size, current, nsib = 0; int tiled; if (~wp->flags & PANE_HIDDEN) return (0); tiled = (lc != NULL && !window_pane_is_floating(wp)); + if (tiled) { + lcneighbour = layout_cell_get_neighbour(lc); + nsib = window_count_tiled_siblings(lc); + } if (tiled && layout_insert_tile(w, lc) != 0) return (-1); wp->flags &= ~(PANE_HIDDEN|PANE_HIDDENALL); - /* Try to get back the size the pane had, scaled to the window. */ - if (tiled && lc->parent != NULL && lc->hidden.g.sx != UINT_MAX) { + /* + * Try to get back the size the pane had, scaled to how much its parent + * has changed. If it has no neighbour it fills the space around it, and + * the size is only right if it has the same neighbours as before. + */ + if (lcneighbour != NULL && + lc->hidden.g.sx != UINT_MAX && + lc->hidden.nsib == nsib) { type = lc->parent->type; if (type == LAYOUT_LEFTRIGHT) { - size = lc->hidden.g.sx * w->sx / lc->hidden.wsx; + size = lc->hidden.g.sx * lc->parent->g.sx / + lc->hidden.psx; current = lc->g.sx; } else { - size = lc->hidden.g.sy * w->sy / lc->hidden.wsy; + size = lc->hidden.g.sy * lc->parent->g.sy / + lc->hidden.psy; current = lc->g.sy; } if (size < PANE_MINIMUM)