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
This commit is contained in:
Michael Grant
2026-06-29 17:47:28 +02:00
parent 91e30f4f22
commit 64db144425
8 changed files with 1366 additions and 264 deletions

View File

@@ -18,7 +18,9 @@
#include <sys/types.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include "tmux.h"
@@ -28,6 +30,13 @@
static enum cmd_retval cmd_select_layout_exec(struct cmd *,
struct cmdq_item *);
static enum cmd_retval cmd_select_layout_exec_multiple(struct cmdq_item *,
const char *);
struct cmd_select_layout_record {
struct window *w;
char *layout;
};
const struct cmd_entry cmd_select_layout_entry = {
.name = "select-layout",
@@ -68,6 +77,120 @@ const struct cmd_entry cmd_previous_layout_entry = {
.exec = cmd_select_layout_exec
};
static void
cmd_select_layout_free_records(struct cmd_select_layout_record *records,
u_int nrecords)
{
u_int i;
for (i = 0; i < nrecords; i++)
free(records[i].layout);
free(records);
}
/* Apply a list of @window-id:layout records. */
static enum cmd_retval
cmd_select_layout_exec_multiple(struct cmdq_item *item, const char *input)
{
struct cmd_select_layout_record *records = NULL;
struct window *w;
const char *ptr = input, *idstart, *idend;
const char *layoutstart, *layoutend;
char *id, *cause, *oldlayout;
u_int i, nrecords = 0;
for (;;) {
while (*ptr != '\0' && isspace((u_char)*ptr))
ptr++;
if (*ptr == '\0')
break;
if (*ptr != '@')
goto invalid;
idstart = ptr++;
if (!isdigit((u_char)*ptr))
goto invalid;
while (isdigit((u_char)*ptr))
ptr++;
idend = ptr;
while (*ptr != '\0' && isspace((u_char)*ptr))
ptr++;
if (*ptr++ != ':')
goto invalid;
id = xstrndup(idstart, idend - idstart);
w = window_find_by_id_str(id);
if (w == NULL) {
cmdq_error(item, "unknown window: %s", id);
free(id);
goto fail;
}
free(id);
for (i = 0; i < nrecords; i++) {
if (records[i].w == w) {
cmdq_error(item, "duplicate window: @%u", w->id);
goto fail;
}
}
layoutstart = ptr;
while (*ptr != '\0' && *ptr != '@')
ptr++;
layoutend = ptr;
while (layoutend != layoutstart &&
isspace((u_char)layoutend[-1]))
layoutend--;
if (layoutend == layoutstart)
goto invalid;
records = xreallocarray(records, nrecords + 1,
sizeof *records);
records[nrecords].w = w;
records[nrecords].layout = xstrndup(layoutstart,
layoutend - layoutstart);
nrecords++;
}
if (nrecords == 0)
goto invalid;
/* Validate every record before changing any window. */
for (i = 0; i < nrecords; i++) {
if (layout_validate(records[i].w, records[i].layout,
&cause) != 0) {
cmdq_error(item, "@%u: %s", records[i].w->id, cause);
free(cause);
goto fail;
}
}
for (i = 0; i < nrecords; i++) {
w = records[i].w;
server_unzoom_window(w);
oldlayout = w->old_layout;
w->old_layout = layout_dump(w, w->layout_root);
if (layout_parse(w, records[i].layout, &cause) != 0) {
cmdq_error(item, "@%u: %s", w->id, cause);
free(cause);
free(w->old_layout);
w->old_layout = oldlayout;
goto fail;
}
free(oldlayout);
recalculate_sizes();
server_redraw_window(w);
notify_window("window-layout-changed", w);
}
cmd_select_layout_free_records(records, nrecords);
return (CMD_RETURN_NORMAL);
invalid:
cmdq_error(item, "invalid multiple-window layout");
fail:
cmd_select_layout_free_records(records, nrecords);
return (CMD_RETURN_ERROR);
}
static enum cmd_retval
cmd_select_layout_exec(struct cmd *self, struct cmdq_item *item)
{
@@ -79,6 +202,24 @@ cmd_select_layout_exec(struct cmd *self, struct cmdq_item *item)
const char *layoutname;
char *oldlayout, *cause;
int next, previous, layout;
const char *ptr;
if (cmd_get_entry(self) == &cmd_select_layout_entry &&
args_count(args) != 0 && !args_has(args, 'E') &&
!args_has(args, 'n') && !args_has(args, 'o') &&
!args_has(args, 'p')) {
ptr = args_string(args, 0);
while (*ptr != '\0' && isspace((u_char)*ptr))
ptr++;
if (*ptr == '@')
return (cmd_select_layout_exec_multiple(item, ptr));
if (layout_set_lookup(ptr) == -1 &&
layout_validate(w, ptr, &cause) != 0) {
cmdq_error(item, "%s: %s", cause, ptr);
free(cause);
return (CMD_RETURN_ERROR);
}
}
server_unzoom_window(w);

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,8 @@ layout_create_cell(struct layout_cell *lcparent)
lc = xmalloc(sizeof *lc);
lc->type = LAYOUT_WINDOWPANE;
lc->flags = 0;
lc->z_index = UINT_MAX;
lc->pane_id = UINT_MAX;
lc->parent = lcparent;
TAILQ_INIT(&lc->cells);

View File

@@ -32,10 +32,10 @@ sleep 1
$TMUX has || exit 1
$TMUX lsp -aF '#{pane_id} #{window_layout}' >$TMP || exit 1
cat <<EOF|cmp -s $TMP - || exit 1
%0 f5ab,200x200,0,0[200x50,0,0,0,200x149,0,51,3]
%3 f5ab,200x200,0,0[200x50,0,0,0,200x149,0,51,3]
%2 dcbd,200x200,0,0[200x100,0,0,2,200x99,0,101,4]
%4 dcbd,200x200,0,0[200x100,0,0,2,200x99,0,101,4]
%0 3cf7,200x200+0+0[%0,0:200x50+0+0;%3,1:200x149+0+51]
%3 3cf7,200x200+0+0[%0,0:200x50+0+0;%3,1:200x149+0+51]
%2 e179,200x200+0+0[%2,0:200x100+0+0;%4,1:200x99+0+101]
%4 e179,200x200+0+0[%2,0:200x100+0+0;%4,1:200x99+0+101]
EOF
$TMUX kill-server 2>/dev/null

230
regress/layout-custom.sh Executable file
View File

@@ -0,0 +1,230 @@
#!/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

169
tmux.1
View File

@@ -2681,14 +2681,14 @@ For example:
.Bd -literal -offset indent
$ tmux list\-windows
0: ksh [159x48]
layout: bb62,159x48,0,0{79x48,0,0,79x48,80,0}
$ tmux select\-layout \[aq]bb62,159x48,0,0{79x48,0,0,79x48,80,0}\[aq]
layout: 8ebd,159x48+0+0{%0,0:79x48+0+0;%1,1:79x48+80+0}
$ tmux select\-layout \[aq]8ebd,159x48+0+0{%0,0:79x48+0+0;%1,1:79x48+80+0}\[aq]
.Ed
.Pp
.Nm
automatically adjusts the size of the layout for the current window size.
Note that a layout cannot be applied to a window with more panes than that
from which the layout was originally defined.
A current or legacy layout can be applied to a window with the same number or
fewer panes; surplus layout cells are discarded.
.Pp
Commands related to windows and panes are as follows:
.Bl -tag -width Ds
@@ -3371,6 +3371,13 @@ specifies the sort order: one of
(time).
.Fl r
reverses the sort order.
.Pp
The layout of the window containing each pane may be included using the
.Ql window_layout
format variable.
It is repeated for each pane from the same window.
Its format is described under
.Ic select\-layout .
.Tg lsw
.It Xo Ic list\-windows
.Op Fl ar
@@ -3406,6 +3413,22 @@ or
(time).
.Fl r
reverses the sort order.
.Pp
The default format includes the layout of each window, also available with the
.Ql window_layout
format variable.
The format is described under
.Ic select\-layout .
.Pp
Layouts for multiple windows may be produced in a form suitable for
.Ic select\-layout
using
.Fl F .
For example, they may be captured and reapplied directly:
.Bd -literal -offset indent
$ layouts=\[dq]$(tmux list\-windows \-F \[aq]#{window_id}:#{window_layout}\[aq])\[dq]
$ tmux select\-layout \[dq]$layouts\[dq]
.Ed
.Tg movep
.It Xo Ic move\-pane
.Op Fl bdfhMv
@@ -3965,6 +3988,142 @@ commands.
applies the last set layout if possible (undoes the most recent layout change).
.Fl E
spreads the current pane and any panes next to it out evenly.
.Pp
.Ar layout\-name
may be a serialized layout with an optional outer checksum followed by one
root layout cell:
.Bd -literal -offset indent
layout = [checksum,]cell
.Ed
.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 current format 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 outer
checksum.
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:
.Bd -literal -offset indent
6939,120x40+0+0{%0,1:60x40+0+0;%1,2:59x40+61+0;%2,0:28x8+11+6:f}
.Ed
.Pp
The legacy format uses commas rather than semicolons to separate child cells
and does not include z-indexes or flags:
.Bd -literal -offset indent
legacy-geometry = widthxheight,x,y
cell = legacy-geometry{cell,cell,...}
| legacy-geometry[cell,cell,...]
| legacy-geometry,pane-id
.Ed
.Pp
The checksum is optional.
If present, it is four hexadecimal digits and covers all non-whitespace text
after its comma; 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 .
A layout must use one form consistently.
In a legacy layout, panes are treated as tiled, visible and unzoomed.
.Ic list\-windows
and the
.Ql window_layout
format variable generate the current format with a checksum.
.Pp
A serialized layout is a layout snapshot, not a complete session restoration.
It rearranges existing panes and windows but does not create panes or windows.
.Pp
Layouts for multiple windows may be prefixed by their window IDs and joined:
.Bd -literal -offset indent
window-layouts = @window-id:layout [@window-id:layout ...]
.Ed
.Pp
Whitespace, including newlines, may appear between the window records.
Each window layout retains its own checksum.
All records are checked before any window is changed.
Unknown or duplicate window IDs are rejected.
.Tg selectp
.It Xo Ic select\-pane
.Op Fl DdeLlMmRUZ
@@ -7111,7 +7270,7 @@ The following variables are available, where appropriate:
.It Li "window_id" Ta "" Ta "Unique window ID"
.It Li "window_index" Ta "#I" Ta "Index of window"
.It Li "window_last_flag" Ta "" Ta "1 if window is the last used"
.It Li "window_layout" Ta "" Ta "Window layout description, ignoring zoomed window panes"
.It Li "window_layout" Ta "" Ta "Window layout description, including zoom and pane flags"
.It Li "window_linked" Ta "" Ta "1 if window is linked across sessions"
.It Li "window_linked_sessions" Ta "" Ta "Number of sessions this window is linked to"
.It Li "window_linked_sessions_list" Ta "" Ta "List of sessions this window is linked to"

7
tmux.h
View File

@@ -1502,7 +1502,13 @@ struct layout_cell {
enum layout_type type;
#define LAYOUT_CELL_FLOATING 0x1
#define LAYOUT_CELL_HIDDEN 0x2
#define LAYOUT_CELL_ZOOMED 0x4 /* only while parsing a layout */
#define LAYOUT_CELL_X_RELATIVE 0x8 /* only while parsing a layout */
#define LAYOUT_CELL_Y_RELATIVE 0x10 /* only while parsing a layout */
int flags;
u_int z_index; /* only while parsing a layout */
u_int pane_id; /* only while parsing a layout */
struct layout_cell *parent;
@@ -3711,6 +3717,7 @@ int layout_remove_tile(struct window *, struct layout_cell *);
/* layout-custom.c */
char *layout_dump(struct window *, struct layout_cell *);
int layout_validate(struct window *, const char *, char **);
int layout_parse(struct window *, const char *, char **);
/* layout-set.c */

View File

@@ -1641,6 +1641,12 @@ window_pane_key(struct window_pane *wp, struct client *c, struct session *s,
int
window_pane_is_visible(struct window_pane *wp)
{
struct layout_cell *lc = wp->layout_cell;
if (lc == NULL)
lc = wp->saved_layout_cell;
if (lc != NULL && (lc->flags & LAYOUT_CELL_HIDDEN))
return (0);
if (~wp->window->flags & WINDOW_ZOOMED)
return (1);
return (wp == wp->window->active);