Files
tmux/regress/layout-custom.sh
Michael Grant 64db144425 Update select-layout and list-windows to work with new layouts,
floating panes, and use a more modern serialization format.

  ## Serialization

  layout-custom.c now emits a canonical current format with:

  - Pane syntax: %pane-id,z-index:geometry[:flags]
  - Semicolons between child cells.
  - X-style geometry: WxH+X+Y
  - Every pane represented inside layout_root, including floating panes.
  - No legacy <...> floating-pane wrapper.
  - Z-index serialized for tiled and floating panes.
  - Flags:
      - f: floating
      - h: hidden
      - z: zoomed

  - Hidden panes use their saved restoration geometry.
  - Single-pane windows serialize as a pane root cell.

  window_layout and list-windows continue emitting a four-digit checksum.

  ## Geometry

  Current layouts accept:

  - +N: absolute positive offset
  - ++N: equivalent to +N
  - +-N: absolute negative offset
  - -N: right/bottom-relative offset for floating panes
  - Omitted offsets: +0+0
  - One offset: second defaults to +0

  Relative offsets are resolved immediately. Output always contains canonical absolute
  coordinates.

  Widths and heights are restricted to 1..PANE_MAXIMUM; offsets and resolved offsets are
  restricted to -PANE_MAXIMUM..PANE_MAXIMUM.

  ## Checksums and whitespace

  - The outer checksum is optional on input for current and legacy layouts.
  - If supplied, it must be correct.
  - Output still includes it.
  - Checksums cover all non-whitespace layout text.
  - Whitespace and newlines may be inserted between tokens.
  - Optional nested checksums are accepted and validated but not emitted.

  ## Legacy compatibility

  Legacy comma-separated layouts remain accepted:

  WxH,x,y{cell,cell,...}
  WxH,x,y,pane-id

  Additional compatibility behavior includes:

  - The reported e6db,113x28,... layout works and resizes existing panes correctly.
  - Older inconsistent root dimensions are corrected safely.
  - Legacy layouts with surplus cells continue pruning cells until the target pane count
    matches.

  - Legacy panes remain assigned in tree order.
  - Legacy input is re-emitted in the current canonical format.

  Mixed legacy/current syntax is rejected.

  ## Pane matching and counts

  For current layouts:

  - Pane IDs must be unique.
  - With matching pane counts:
      - An exact ID set maps panes by identity.
      - A completely disjoint ID set maps panes in tree order.
      - A partial ID match is rejected as ambiguous.

  - If the target has fewer panes:
      - Cells are removed from the end of the tree.
      - Remaining z-indexes are compacted while preserving order.
      - Existing panes are assigned in tree order.
      - No panes are killed or created.

  - A target with more panes than layout cells is rejected.

  Layouts are therefore snapshots that rearrange existing panes and windows, not complete
  session-restoration data.

  ## Validation and safety

  Parsing now constructs and validates a temporary tree before replacing the active layout:

  - Maximum input length: 8192 bytes.
  - Maximum nesting depth: 64.
  - Checked signed and unsigned numeric parsing.
  - Dimension and offset bounds.
  - Container size consistency.
  - Exact trailing-input checks.
  - Unique, contiguous z-indexes starting at zero.
  - Floating panes must precede tiled panes in z-order.
  - Unknown and duplicate flags are rejected.
  - Hidden and zoomed flags cannot be combined.
  - Only one pane may be zoomed.
  - Relative positioning is accepted only for floating panes.
  - Overflow-safe size aggregation.

  Invalid single-window layouts are validated before the window is unzoomed. Multi-window
  layouts are all validated before any window is changed.

  ## Multi-window layouts

  cmd-select-layout.c accepts:

  @window-id:layout[@window-id:layout...]

  Records may be adjacent or separated by whitespace/newlines. Unknown and duplicate window IDs
  are rejected.

  This allows direct reuse of:

  tmux list-windows -F '#{window_id}:#{window_layout}'

  ## Supporting runtime changes

  - tmux.h adds parsed pane IDs, z-indexes, hidden/zoomed/relative cell flags, and
    layout_validate.

  - layout.c initializes parsed metadata safely.
  - window.c treats hidden cells, including saved cells, as non-visible.

  ## Documentation

  tmux.1 now documents:

  - Current and legacy grammars.
  - Geometry and flags.
  - Z-index and pane-ID rules.
  - Optional checksums.
  - Multi-window wrappers.
  - Pane-count pruning.
  - Snapshot versus session-restoration semantics.

  The authoritative description is under select-layout; list-windows and list-panes reference
  it.

  ## Tests

  Added regress/layout-custom.sh covering serialization, compatibility, validation, geometry,
  IDs, pruning, flags, zoom, checksums, whitespace, and multi-window application.

  Updated regress/control-client-sanity.sh for canonical output.

  Verified:

  - Debug build
  - Layout regression
  - Floating-pane geometry regression
  - Control-client regression
  - Man-page rendering
  - git diff --check
2026-06-29 17:47:28 +02:00

231 lines
9.6 KiB
Bash
Executable File

#!/bin/sh
# Test legacy and extended custom layout strings.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Llayout-custom-test -f/dev/null"
fail()
{
echo "$*" >&2
$TMUX kill-server 2>/dev/null
exit 1
}
must_equal()
{
[ "$1" = "$2" ] || fail "got '$1', expected '$2'"
}
must_fail()
{
"$@" >/dev/null 2>&1 && fail "unexpected success: $*"
return 0
}
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -x 80 -y 24 || exit 1
$TMUX split-window -h || fail "split-window failed"
# Legacy comma-separated layouts are accepted and emitted in the new form.
legacy='89f5,80x24,0,0{39x24,0,0,0,40x24,40,0,1}'
$TMUX select-layout "$legacy" || fail "legacy layout was rejected"
layout=$($TMUX display-message -p '#{window_layout}')
must_equal "$layout" \
'93fc,80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0}'
# Both current and legacy layouts may omit the outer checksum.
$TMUX select-layout '80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0}' || \
fail "checksumless current layout was rejected"
must_equal "$($TMUX display-message -p '#{window_layout}')" "$layout"
$TMUX select-layout '80x24,0,0{39x24,0,0,0,40x24,40,0,1}' || \
fail "checksumless legacy layout was rejected"
must_equal "$($TMUX display-message -p '#{window_layout}')" "$layout"
must_fail $TMUX select-layout \
'0000,80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0}'
# Whitespace and newlines between tokens do not affect the checksum.
indented='93fc,
80x24 +0 +0 {
%0, 0: 39x24 +0 +0;
%1, 1: 40x24 +40 +0
}'
$TMUX select-layout "$indented" || fail "indented layout was rejected"
must_equal "$($TMUX display-message -p '#{window_layout}')" "$layout"
# Reported legacy compatibility case: resize two existing panes to 113x28.
reported='e6db,113x28,0,0{56x28,0,0,0,56x28,57,0,1}'
$TMUX select-layout "$reported" || fail "reported legacy layout was rejected"
must_equal "$($TMUX display-message -p '#{window_width}x#{window_height}')" \
'113x28'
must_equal "$($TMUX list-panes -F '#{pane_width}x#{pane_height},#{pane_left},#{pane_top}')" \
'56x28,0,0
56x28,57,0'
# Return to the smaller layout for the remaining parser tests.
$TMUX select-layout "$legacy" || fail "legacy layout could not be reapplied"
layout=$($TMUX display-message -p '#{window_layout}')
# A checksum may be attached to each nested cell but is not emitted.
nested='73a1,80x24+0+0{6806,%0,0:39x24+0+0;6bda,%1,1:40x24+40+0}'
$TMUX select-layout "$nested" || fail "nested checksums were rejected"
must_equal "$($TMUX display-message -p '#{window_layout}')" "$layout"
# A bad nested checksum is rejected without changing the current layout.
badnested='2fa4,80x24+0+0{dead,%0,0:39x24+0+0;6bda,%1,1:40x24+40+0}'
must_fail $TMUX select-layout "$badnested"
must_equal "$($TMUX display-message -p '#{window_layout}')" "$layout"
# Hidden state is retained in the serialized cell flags.
hidden='659f,80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0:h}'
$TMUX select-layout "$hidden" || fail "hidden layout was rejected"
must_equal "$($TMUX display-message -p '#{window_layout}')" "$hidden"
# Pane IDs map by identity when all match and by tree order when none match.
disjoint='80x24+0+0{%100,0:39x24+0+0;%101,1:40x24+40+0}'
$TMUX select-layout "$disjoint" || fail "disjoint pane IDs were rejected"
must_equal "$($TMUX display-message -p '#{window_layout}')" "$layout"
identity='80x24+0+0{%1,0:39x24+0+0;%0,1:40x24+40+0}'
$TMUX select-layout "$identity" || fail "matching pane IDs were rejected"
must_equal "$($TMUX display-message -p -t %0 '#{pane_left}')" 40
$TMUX select-layout "$legacy" || fail "legacy layout could not be restored"
# Duplicate or partially matching IDs fail when pane counts match.
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+0+0;%100,1:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%100,0:39x24+0+0;%100,1:40x24+40+0}'
# A target with fewer panes drops cells from the end, compacts z-indexes and
# assigns the remaining cells in tree order. A target with more panes fails.
fewer='80x24+0+0{%100,1:26x24+0+0;%101,2:26x24+27+0;%102,0:26x24+54+0:z}'
$TMUX select-layout "$fewer" || fail "surplus current cells were not removed"
must_equal "$($TMUX display-message -p -t %0 '#{pane_left},#{pane_width}')" \
'0,26'
must_equal "$($TMUX display-message -p -t %1 '#{pane_left},#{pane_width}')" \
'27,53'
must_equal "$($TMUX display-message -p -t %0 '#{pane_zoomed_flag}')" 0
case "$($TMUX display-message -p '#{window_layout}')" in
*'%0,0:26x24+0+0;%1,1:53x24+27+0}'*) ;;
*) fail "z-indexes were not compacted after removing a cell" ;;
esac
must_fail $TMUX select-layout '%100,0:80x24+0+0'
$TMUX select-layout "$legacy" || fail "legacy layout could not be restored"
# Mixed legacy and new separators, duplicate z-indices and unknown flags fail.
must_fail $TMUX select-layout \
'93ed,80x24+0+0{%0,0:39x24+0+0,%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'93ec,80x24+0+0{%0,0:39x24+0+0;%1,0:40x24+40+0}'
must_fail $TMUX select-layout \
'0e42,80x24+0+0{%0,0:39x24+0+0:q;%1,1:40x24+40+0}'
# Truncated and overflowing values must fail without terminating the server.
must_fail $TMUX select-layout '0000,'
must_fail $TMUX select-layout \
'c523,999999999999999999999999x24,0,0,0'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:0x24+0+0;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+2147483647+0;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+-10001+0}'
$TMUX list-windows >/dev/null || fail "server exited after invalid layouts"
# Invalid custom layouts must not unzoom the target window.
$TMUX resize-pane -t %0 -Z || fail "zoom failed"
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:0x24+0+0;%1,1:40x24+40+0}'
must_equal "$($TMUX display-message -p -t %0 '#{pane_zoomed_flag}')" 1
$TMUX resize-pane -t %0 -Z || fail "unzoom failed"
# X-style geometry supports right/bottom-relative offsets, absolute negative
# offsets, doubled plus signs, and omitted positions.
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -x 120 -y 40 || exit 1
$TMUX new-pane -d -x 20 -y 8 'sleep 100' || fail "floating pane failed"
relative='1442,120x40+0+0{%0,1:120x40+0+0;%1,0:30x10-10-20:f}'
$TMUX select-layout "$relative" || fail "relative geometry was rejected"
must_equal "$($TMUX display-message -p -t %1 '#{pane_left},#{pane_top}')" \
'80,10'
must_equal "$($TMUX display-message -p '#{window_layout}')" \
'0041,120x40+0+0{%0,1:120x40+0+0;%1,0:30x10+80+10:f}'
absolute='3a22,120x40+0+0{%0,1:120x40+0+0;%1,0:20x8+-10+-20:f}'
$TMUX select-layout "$absolute" || fail "absolute negative geometry failed"
must_equal "$($TMUX display-message -p -t %1 '#{pane_left},#{pane_top}')" \
'-10,-20'
must_equal "$($TMUX display-message -p '#{window_layout}')" "$absolute"
defaults='fcb6,120x40{%0,1:120x40;%1,0:30x10++80+10:f}'
$TMUX select-layout "$defaults" || fail "default or doubled offsets failed"
must_equal "$($TMUX display-message -p '#{window_layout}')" \
'0041,120x40+0+0{%0,1:120x40+0+0;%1,0:30x10+80+10:f}'
oneoffset='8f93,120x40{%0,1:120x40;%1,0:30x10+80:f}'
$TMUX select-layout "$oneoffset" || fail "single offset default failed"
must_equal "$($TMUX display-message -p '#{window_layout}')" \
'5fb9,120x40+0+0{%0,1:120x40+0+0;%1,0:30x10+80+0:f}'
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -x 80 -y 24 || exit 1
$TMUX split-window -h || fail "split-window failed"
# Multiple @window-id:layout records may be applied in one command. The
# newline produced by list-windows is insignificant whitespace.
$TMUX new-window -d -n second || fail "second window failed"
$TMUX split-window -d -h -t @1 || fail "second window split failed"
layouts=$($TMUX list-windows -F '#{window_id}:#{window_layout}')
$TMUX select-layout "$layouts" || fail "multiple-window layout was rejected"
must_equal "$($TMUX list-windows -F '#{window_id}:#{window_layout}')" \
"$layouts"
compact=$(printf %s "$layouts" | tr -d '\n')
$TMUX select-layout "$compact" || fail "compact multiple-window layout failed"
# All records are validated before any window is changed.
before=$($TMUX display-message -p -t @0 '#{window_width}x#{window_height}')
must_fail $TMUX select-layout "@0:$reported@1:0000,"
must_equal "$($TMUX display-message -p -t @0 '#{window_width}x#{window_height}')" \
"$before"
must_fail $TMUX select-layout "@0:$legacy@0:$legacy"
must_fail $TMUX select-layout "@999999:$legacy"
# Add floating panes and verify exact z-order, semicolon output and zoom state.
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -x 120 -y 40 || exit 1
$TMUX split-window -h || fail "split-window failed"
$TMUX new-pane -d -x 30 -y 10 -X 10 -Y 5 'sleep 100' || \
fail "first floating pane failed"
$TMUX new-pane -d -x 24 -y 8 -X 50 -Y 12 'sleep 100' || \
fail "second floating pane failed"
layout=$($TMUX display-message -p '#{window_layout}')
case "$layout" in
*'<'*|*'>'*) fail "obsolete floating delimiters were emitted" ;;
esac
case "$layout" in
*';'*',0:'*':f'*) ;;
*) fail "new layout does not contain semicolons and front z-index: $layout" ;;
esac
case "$layout" in
*',1:'*':f'*) ;;
*) fail "new layout does not contain the second floating z-index: $layout" ;;
esac
$TMUX select-layout "$layout" || fail "new layout did not round trip"
must_equal "$($TMUX display-message -p '#{window_layout}')" "$layout"
$TMUX resize-pane -t %2 -Z || fail "zoom failed"
zoomed=$($TMUX display-message -p '#{window_layout}')
case "$zoomed" in
*':fz'*) ;;
*) fail "zoom flag was not emitted: $zoomed" ;;
esac
$TMUX select-layout "$zoomed" || fail "zoomed layout did not round trip"
must_equal "$($TMUX display-message -p '#{window_layout}')" "$zoomed"
must_equal "$($TMUX display-message -p -t %2 '#{pane_zoomed_flag}')" 1
$TMUX kill-server 2>/dev/null