mirror of
https://github.com/tmux/tmux.git
synced 2026-08-28 17:41:35 +00:00
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
231 lines
9.6 KiB
Bash
Executable File
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
|