Simplify man page and move doc on the formats to layout-custom.c.

This commit is contained in:
Michael Grant
2026-07-06 17:15:08 +01:00
parent cd6cd323ea
commit c3e7738cc5
3 changed files with 117 additions and 177 deletions

View File

@@ -25,6 +25,63 @@
#include "tmux.h"
/*
* Layout string syntax
* --------------------
*
* A layout has one outer cell and may have a four-digit hexadecimal checksum:
*
* layout = [checksum,](v2:new-cell | old-cell)
*
* The checksum covers all non-whitespace text after its comma, including the
* version. Checksums before nested cells are also accepted, although they are
* not generated. The v2: prefix is accepted only at the outer level and
* requires the new cell form. The new cell form requires v2: and unknown
* versions are rejected.
*
* New form:
*
* geometry = widthxheight[x-position[y-position]]
* container = geometry{new-cell;new-cell;...}
* | geometry[new-cell;new-cell;...]
* pane = %pane-id,z-index:geometry[:flags]
* new-cell = container | pane
*
* Braces arrange children from left to right and square brackets from top to
* bottom. Semicolons separate children. Positions use X geometry: +N and ++N
* are absolute, +-N is negative, and -N places the right or bottom edge N
* cells from the right or bottom. A missing position is +0; if only one is
* present, the second is +0. Relative right or bottom positions are permitted
* only for floating panes. Widths and heights are between PANE_MINIMUM and
* PANE_MAXIMUM; resolved positions are between -PANE_MAXIMUM and
* PANE_MAXIMUM.
*
* Pane IDs must be unique. Z-indexes must be unique and contiguous from zero,
* with floating panes before tiled panes. Flags are f (floating), h (hidden)
* and z (zoomed); h and z cannot be combined. Hidden panes contain their
* restoration geometry.
*
* Old form:
*
* geometry = widthxheight,x-position,y-position
* old-cell = geometry{old-cell,old-cell,...}
* | geometry[old-cell,old-cell,...]
* | geometry[,pane-id]
*
* Old pane IDs are optional and ignored. Old panes are tiled, visible and
* unzoomed. A layout must not mix the two cell forms or their separators.
* Whitespace may appear between tokens and is ignored by the checksum.
*
* Layouts rearrange existing panes; they do not create panes. New pane IDs are
* used when all match the target window, tree order is used when none match,
* and a partial match is rejected. Surplus cells are removed from the end when
* the target has fewer panes; a target with more panes is rejected.
*
* cmd-select-layout.c also accepts multiple window records in this form:
*
* @window-id:layout [@window-id:layout ...]
*/
#define LAYOUT_STRING_MAX 8192
#define LAYOUT_DEPTH_MAX 64
@@ -949,7 +1006,8 @@ layout_prepare(struct window *w, const char *layout, char **cause)
layout_free_cell(root, 0);
return (NULL);
}
if (version == 1 && ctx.format != LAYOUT_FORMAT) {
if ((version == 1 && ctx.format != LAYOUT_FORMAT) ||
(version == 0 && ctx.format == LAYOUT_FORMAT)) {
*cause = xstrdup("invalid layout version");
layout_free_cell(root, 0);
return (NULL);

View File

@@ -53,10 +53,9 @@ layout=$($TMUX display-message -p '#{window_layout}')
must_equal "$layout" \
'cc3c,v2: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"
# Checksums are optional, but the new form requires its version.
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0}'
$TMUX select-layout \
'v2:80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0}' || \
fail "checksumless versioned layout was rejected"
@@ -109,8 +108,8 @@ $TMUX resize-pane -t %0 -Z || fail "tiled unzoom failed"
$TMUX select-layout "$layout" || fail "layout could not be restored"
# Whitespace and newlines between tokens do not affect the checksum.
indented='93fc,
80x24 +0 +0 {
indented='cc3c,
v2: 80x24 +0 +0 {
%0, 0: 39x24 +0 +0;
%1, 1: 40x24 +40 +0
}'
@@ -137,12 +136,12 @@ $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}'
nested='v2: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}'
badnested='v2: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"
@@ -152,23 +151,23 @@ $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}'
disjoint='v2: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}'
identity='v2: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}'
'v2: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}'
'v2: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}'
fewer='v2: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 '#{window_panes}')" 2
must_equal "$($TMUX display-message -p -t %0 '#{pane_left},#{pane_width}')" \
@@ -183,40 +182,40 @@ esac
# The complete tree must be structurally valid before surplus cells are
# removed; the last child has the wrong height here.
must_fail $TMUX select-layout \
'80x24+0+0{%100,0:39x24+0+0;%101,1:20x24+40+0;%102,2:19x10+61+0}'
must_fail $TMUX select-layout '%100,0:80x24+0+0'
'v2:80x24+0+0{%100,0:39x24+0+0;%101,1:20x24+40+0;%102,2:19x10+61+0}'
must_fail $TMUX select-layout 'v2:%100,0:80x24+0+0'
must_fail $TMUX select-layout '80x24,0,0,0'
$TMUX select-layout "$legacy" || fail "legacy layout could not be restored"
# Mixed legacy and new syntax, malformed containers, duplicate z-indices,
# non-contiguous z-indices, invalid z-ordering and unknown flags fail.
must_fail $TMUX select-layout \
'93ed,80x24+0+0{%0,0:39x24+0+0,%1,1:40x24+40+0}'
'v2:80x24+0+0{%0,0:39x24+0+0,%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24,0,0{39x24,0,0,0;%1,1:40x24+40+0}'
must_fail $TMUX select-layout '80x24+0+0{}'
must_fail $TMUX select-layout 'v2:80x24+0+0{}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+0+0;}'
'v2:80x24+0+0{%0,0:39x24+0+0;}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+0+0|%1,1:40x24+40+0}'
'v2: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}'
'v2:80x24+0+0{%0,0:39x24+0+0;%1,0:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+0+0;%1,2:40x24+40+0}'
'v2:80x24+0+0{%0,0:39x24+0+0;%1,2:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0:f}'
'v2:80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0:f}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:38x24+0+0;%1,1:40x24+39+0}'
'v2:80x24+0+0{%0,0:38x24+0+0;%1,1:40x24+39+0}'
must_fail $TMUX select-layout \
'0e42,80x24+0+0{%0,0:39x24+0+0:q;%1,1:40x24+40+0}'
'v2:80x24+0+0{%0,0:39x24+0+0:q;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+0+0:ff;%1,1:40x24+40+0}'
'v2:80x24+0+0{%0,0:39x24+0+0:ff;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+0+0:hh;%1,1:40x24+40+0}'
'v2:80x24+0+0{%0,0:39x24+0+0:hh;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+0+0:zz;%1,1:40x24+40+0}'
'v2:80x24+0+0{%0,0:39x24+0+0:zz;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+0+0:z;%1,1:40x24+40+0:z}'
'v2:80x24+0+0{%0,0:39x24+0+0:z;%1,1:40x24+40+0:z}'
must_fail $TMUX select-layout \
'80x24,0,0{39x24,0,0,0,40x24,40,0,}'
@@ -225,22 +224,22 @@ 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}'
'v2: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}'
'v2: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}'
'v2:80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+-10001+0}'
must_fail $TMUX select-layout \
'80x24-1+0{%0,0:39x24+0+0;%1,1:40x24+40+0}'
'v2:80x24-1+0{%0,0:39x24+0+0;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24-1+0;%1,1:40x24+40+0}'
'v2:80x24+0+0{%0,0:39x24-1+0;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+;%1,1:40x24+40+0}'
'v2:80x24+0+0{%0,0:39x24+;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24+0+;%1,1:40x24+40+0}'
'v2:80x24+0+0{%0,0:39x24+0+;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'80x24+0+0{%0,0:39x24++ +0;%1,1:40x24+40+0}'
long='80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0}'
'v2:80x24+0+0{%0,0:39x24++ +0;%1,1:40x24+40+0}'
long='v2:80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0}'
i=0
while [ $i -lt 9000 ]; do
long="${long}x"
@@ -252,7 +251,7 @@ $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}'
'v2: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"
@@ -269,8 +268,8 @@ while [ $i -lt 64 ]; do
deep="1x1+0+0{$deep}"
i=$((i + 1))
done
$TMUX select-layout "$deep" || fail "maximum layout depth was rejected"
must_fail $TMUX select-layout "1x1+0+0{$deep}"
$TMUX select-layout "v2:$deep" || fail "maximum layout depth was rejected"
must_fail $TMUX select-layout "v2:1x1+0+0{$deep}"
# X-style geometry supports right/bottom-relative offsets, absolute negative
# offsets, doubled plus signs, and omitted positions.
@@ -289,14 +288,14 @@ case "$($TMUX display-message -p '#{window_layout}')" in
*) fail "negative floating pane was not serialized with +- offsets" ;;
esac
relative='1442,120x40+0+0{%0,1:120x40+0+0;%1,0:30x10-10-20:f}'
relative='v2: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}')" \
"$(versioned '120x40+0+0{%0,1:120x40+0+0;%1,0:30x10+80+10:f}')"
bottomright='120x40+0+0{%0,1:120x40+0+0;%1,0:30x10-0-0:f}'
bottomright='v2:120x40+0+0{%0,1:120x40+0+0;%1,0:30x10-0-0:f}'
$TMUX select-layout "$bottomright" || fail "bottom-right geometry failed"
must_equal "$($TMUX display-message -p -t %1 '#{pane_left},#{pane_top}')" \
'90,30'
@@ -304,32 +303,32 @@ must_equal "$($TMUX display-message -p '#{window_layout}')" \
"$(versioned '120x40+0+0{%0,1:120x40+0+0;%1,0:30x10+90+30:f}')"
absolute_body='120x40+0+0{%0,1:120x40+0+0;%1,0:20x8+-10+-20:f}'
$TMUX select-layout "3a22,$absolute_body" || \
$TMUX select-layout "v2:$absolute_body" || \
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}')" \
"$(versioned "$absolute_body")"
hiddenfloat='120x40+0+0{%0,1:120x40+0+0;%1,0:20x8+-10+-20:fh}'
hiddenfloat='v2:120x40+0+0{%0,1:120x40+0+0;%1,0:20x8+-10+-20:fh}'
$TMUX select-layout "$hiddenfloat" || fail "hidden floating layout failed"
must_equal "$($TMUX display-message -p -t %1 '#{pane_left},#{pane_top},#{pane_width},#{pane_height}')" \
'-10,-20,20,8'
must_equal "$($TMUX display-message -p '#{window_layout}')" \
"$(versioned '120x40+0+0{%0,1:120x40+0+0;%1,0:20x8+-10+-20:fh}')"
must_fail $TMUX select-layout \
'120x40+0+0{%0,1:120x40+0+0;%1,0:20x8+-10+-20:fhz}'
'v2:120x40+0+0{%0,1:120x40+0+0;%1,0:20x8+-10+-20:fhz}'
must_equal "$($TMUX display-message -p '#{window_layout}')" \
"$(versioned '120x40+0+0{%0,1:120x40+0+0;%1,0:20x8+-10+-20:fh}')"
must_fail $TMUX select-layout \
'120x40+0+0{%0,1:120x40+0+0;%1,0:10000x8-10000+0:f}'
'v2:120x40+0+0{%0,1:120x40+0+0;%1,0:10000x8-10000+0:f}'
defaults='fcb6,120x40{%0,1:120x40;%1,0:30x10++80+10:f}'
defaults='v2: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}')" \
"$(versioned '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}'
oneoffset='v2: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}')" \
"$(versioned '120x40+0+0{%0,1:120x40+0+0;%1,0:30x10+80+0:f}')"

139
tmux.1
View File

@@ -3975,142 +3975,25 @@ applies the last set layout if possible (undoes the most recent layout change).
spreads the current pane and any panes next to it out evenly.
.Pp
.Ar layout\-name
may be a layout description with an optional outer checksum followed by one
root layout cell:
.Bd -literal -offset indent
layout = [checksum,][v2:]cell
.Ed
.Pp
The
may be a layout description in either a new or old form, optionally beginning
with a four-digit hexadecimal checksum and a comma.
The new form is identified by
.Ql v2:
prefix identifies the new form and is included in generated layouts using the
new form.
It may be omitted when reading a layout in the new form for compatibility
with older output.
An explicit
.Ql v2:
prefix may not be used with the old cell form and unknown versions are
rejected.
.Pp
A layout cell is either a container cell or a pane cell.
Geometry gives a cell's size and position.
.Nm
accepts X Window System-style geometry specifications:
.Bd -literal -offset indent
geometry = widthxheight[x-offset[y-offset]]
x-offset = +N | ++N | +-N | -N
y-offset = +N | ++N | +-N | -N
.Ed
.Pp
.Ql +N
is an absolute offset from the left or top of the window.
.Ql -N
is accepted only for floating pane cells and places the pane's right or bottom
edge relative to the right or bottom of the window.
To specify a negative offset to place a floating pane off the left or above the
top of the window, specify it as:
.Ql +-N .
.Ql ++N
is equivalent to
.Ql +N .
If both offsets are omitted,
.Ql +0+0
is assumed; if only the first is present, the second defaults to
.Ql +0 .
Widths and heights must be between 1 and 10,000, and resolved offsets must be
between -10,000 and 10,000.
Neither
.Ic list\-windows
nor
.Ic list\-panes
currently generates
.Ql -N
relative offsets.
.Pp
Container cells begin with geometry and hold other layout cells.
Braces arrange the child cells from left to right and square brackets arrange
them from top to bottom.
Pane cells are leaves and begin with a pane ID and z-index, followed by their
geometry and optional flags.
Semicolons separate child cells.
The complete cell forms are:
.Bd -literal -offset indent
cell = container | pane
container = geometry{cell;cell;...}
| geometry[cell;cell;...]
pane = %pane-id,z-index:geometry[:flags]
.Ed
.Pp
Pane IDs in the new form must be unique.
If the layout and target window have the same number of panes and the pane IDs
exactly match those in the target window, panes are assigned by ID.
If none match, panes are assigned in tree order, allowing a layout from another
window to be reused; a partial match is rejected as ambiguous.
If the target window has fewer panes, cells are removed from the end of the
layout tree until the counts match, remaining z-indexes are compacted, and
panes are assigned in tree order.
A layout cannot be applied to a target window with more panes than it has pane
cells.
.Pp
If a window has only one pane, the root layout cell is itself a pane cell, so
the pane ID, z-index, geometry and any flags immediately follow the
.Ql v2:
prefix.
The z-index is zero for the front pane and increases towards the back.
It is present for every pane, including tiled panes.
Z-indexes must be unique and contiguous from zero, with all floating panes
before any tiled panes.
The flags are
.Ql f
for floating,
.Ql h
for hidden and
.Ql z
for zoomed.
An absent
.Ql f
flag means tiled and absent
.Ql h
and
.Ql z
flags mean visible and unzoomed.
The
.Ql h
and
.Ql z
flags cannot be combined.
.Pp
For example, this layout contains two tiled panes and one floating pane:
after the checksum, uses semicolons between cells, and begins pane cells with
.Ql % .
For example:
.Bd -literal -offset indent
7749,v2:120x40+0+0{%0,1:60x40+0+0;%1,2:59x40+61+0;%2,0:28x8+11+6:f}
.Ed
.Pp
The old form uses commas rather than semicolons to separate child cells
and does not include z-indexes or flags:
The old form has no
.Ql v2:
prefix, uses commas, and does not include z-indexes or flags.
For example:
.Bd -literal -offset indent
old-geometry = widthxheight,x,y
cell = old-geometry{cell,cell,...}
| old-geometry[cell,cell,...]
| old-geometry[,pane-id]
89f5,80x24,0,0{39x24,0,0,0,40x24,40,0,1}
.Ed
.Pp
Pane IDs are optional in old layouts and are ignored when assigning panes.
This permits layouts from versions of
.Nm
before pane IDs were added.
.Pp
The checksum is optional.
If present, it is four hexadecimal digits and covers all non-whitespace text
after its comma, including any version prefix; a layout with an incorrect
checksum is rejected.
Whitespace, including newlines, may appear between layout tokens and does not
change the checksum, so a layout may be indented for readability.
A checksum may also appear before any nested cell; nested checksums are
accepted and checked but are not generated by
.Ic list\-windows .
Version prefixes are accepted only on the outer layout, not on nested cells.
A layout must use one form consistently.
In an old layout, panes are treated as tiled, visible and unzoomed.
Outside control mode,
.Ql window_layout
and