Add a version number (,v2) to the list-windows serialization string.

This commit is contained in:
Michael Grant
2026-07-05 21:27:05 +01:00
parent 21dce04dca
commit 1b88870490
4 changed files with 108 additions and 30 deletions

View File

@@ -61,6 +61,7 @@ static int layout_append_geometry(char *, size_t, u_int,
static int layout_check(struct layout_cell *);
static int layout_check_geometry(u_int, u_int, int, int);
static int layout_parse_char(struct layout_parse_ctx *, char);
static int layout_parse_version(struct layout_parse_ctx *);
static int layout_construct(struct layout_parse_ctx *,
struct layout_cell *, struct layout_cell **);
static void layout_assign(struct window *, struct window_pane **,
@@ -137,7 +138,7 @@ layout_dump(struct window *w, struct layout_cell *root)
{
char layout[LAYOUT_STRING_MAX], *out;
*layout = '\0';
strlcpy(layout, "v2:", sizeof layout);
if (layout_append(w, root, layout, sizeof layout) != 0)
return (NULL);
@@ -585,6 +586,21 @@ layout_parse_checksum(struct layout_parse_ctx *ctx, u_short *csum)
return (1);
}
/* Parse an optional outer layout version. */
static int
layout_parse_version(struct layout_parse_ctx *ctx)
{
layout_skip_space(ctx);
if (ctx->ptr == ctx->end || *ctx->ptr != 'v')
return (0);
if ((size_t)(ctx->end - ctx->ptr) < 3 || ctx->ptr[1] != '2' ||
ctx->ptr[2] != ':')
return (-1);
ctx->ptr += 3;
layout_skip_space(ctx);
return (1);
}
/* Construct one cell, accepting an optional checksum before nested cells. */
static int
layout_construct(struct layout_parse_ctx *ctx, struct layout_cell *parent,
@@ -900,7 +916,7 @@ layout_prepare(struct window *w, const char *layout, char **cause)
const char *body;
u_int npanes, ncells, old_ncells;
u_short csum;
int has_checksum, pane_ids = 0;
int has_checksum, pane_ids = 0, version;
if (strlen(layout) >= LAYOUT_STRING_MAX) {
*cause = xstrdup("invalid layout");
@@ -917,6 +933,11 @@ layout_prepare(struct window *w, const char *layout, char **cause)
*cause = xstrdup("invalid layout checksum");
return (NULL);
}
version = layout_parse_version(&ctx);
if (version == -1) {
*cause = xstrdup("invalid layout version");
return (NULL);
}
if (layout_construct(&ctx, NULL, &root) != 0) {
*cause = xstrdup("invalid layout");
layout_free_cell(root, 0);
@@ -928,6 +949,11 @@ layout_prepare(struct window *w, const char *layout, char **cause)
layout_free_cell(root, 0);
return (NULL);
}
if (version == 1 && ctx.format != LAYOUT_FORMAT) {
*cause = xstrdup("invalid layout version");
layout_free_cell(root, 0);
return (NULL);
}
if (layout_resolve_relative(root,
root->type == LAYOUT_WINDOWPANE ? w->sx : root->sx,
root->type == LAYOUT_WINDOWPANE ? w->sy : root->sy) != 0) {

View File

@@ -38,11 +38,12 @@ sleep 1
# Default control output is legacy; window-layout-new output is current.
awk '/^layout:/ { print }' "$OUT1" | while IFS= read -r line; do
case "$line" in
*window-layout-new*|*%0,*|*';'*) exit 1 ;;
*window-layout-new*|*v2:*|*%0,*|*';'*) exit 1 ;;
esac
done || exit 1
[ "$(grep -ac '^layout:' "$OUT1")" -eq 6 ] || exit 1
grep -a '^layout:.*window-layout-new.*%0,' "$OUT2" >/dev/null || exit 1
grep -a '^layout:.*window-layout-new.*,v2:.*%0,' "$OUT2" >/dev/null ||
exit 1
# Toggling the flag changes subsequent format expansion for the same client.
printf '%s\n' "refresh-client -f window-layout-new" >&3
@@ -50,7 +51,8 @@ printf '%s\n' "display-message -p 'new:#{client_flags}:#{window_layout}'" >&3
printf '%s\n' "refresh-client -f '!window-layout-new'" >&3
printf '%s\n' "display-message -p 'legacy:#{client_flags}:#{window_layout}'" >&3
sleep 1
grep -a '^new:.*window-layout-new.*%0,' "$OUT1" >/dev/null || exit 1
grep -a '^new:.*window-layout-new.*,v2:.*%0,' "$OUT1" >/dev/null ||
exit 1
grep -a '^legacy:' "$OUT1" | tail -1 | grep -v '%0,' >/dev/null || exit 1
# One layout change is formatted independently for each connected client.
@@ -62,10 +64,12 @@ LEGACY=$(grep -a '^%layout-change ' "$OUT1" | tail -1)
CURRENT=$(grep -a '^%layout-change ' "$OUT2" | tail -1)
[ -n "$LEGACY" ] || exit 1
[ -n "$CURRENT" ] || exit 1
case "$LEGACY" in *%0,*|*';'*) exit 1 ;; esac
case "$CURRENT" in *%0,*) ;; *) exit 1 ;; esac
case "$LEGACY" in *v2:*|*%0,*|*';'*) exit 1 ;; esac
case "$CURRENT" in *v2:*%0,*) ;; *) exit 1 ;; esac
[ "$(printf '%s' "$CURRENT" | awk '{ print gsub(/%0,/, "") }')" -ge 2 ] ||
exit 1
[ "$(printf '%s' "$CURRENT" | awk '{ print gsub(/v2:/, "") }')" -ge 2 ] ||
exit 1
exec 3>&-
exec 4>&-

View File

@@ -26,6 +26,21 @@ must_fail()
return 0
}
versioned()
{
body="v2:$1"
checksum=$(printf %s "$body" | od -An -tu1 | awk '
{ for (i = 1; i <= NF; i++) {
if ($i == 9 || $i == 10 || $i == 11 || $i == 12 ||
$i == 13 || $i == 32)
continue
checksum = int(checksum / 2) + (checksum % 2) * 32768
checksum = (checksum + $i) % 65536
} }
END { printf "%04x", checksum }')
printf '%s,%s' "$checksum" "$body"
}
$TMUX kill-server 2>/dev/null
sleep 0.5
$TMUX new-session -d -x 80 -y 24 || exit 1
@@ -36,20 +51,37 @@ 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}'
'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"
$TMUX select-layout \
'v2:80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0}' || \
fail "checksumless versioned 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}'
must_fail $TMUX select-layout \
'0000,v2:80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0}'
# The old checksum for the unversioned cell does not validate v2:.
must_fail $TMUX select-layout \
'93fc,v2:80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'v1:80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'v3:80x24+0+0{%0,0:39x24+0+0;%1,1:40x24+40+0}'
must_fail $TMUX select-layout \
'v2:80x24,0,0{39x24,0,0,0,40x24,40,0,1}'
must_fail $TMUX select-layout \
'80x24+0+0{v2:%0,0:39x24+0+0;%1,1:40x24+40+0}'
# Top-bottom containers are accepted in both current and legacy formats.
topbottom='081e,80x24+0+0[%0,0:80x11+0+0;%1,1:80x12+0+12]'
topbottom=$(versioned '80x24+0+0[%0,0:80x11+0+0;%1,1:80x12+0+12]')
$TMUX select-layout "$topbottom" || fail "top-bottom layout was rejected"
must_equal "$($TMUX display-message -p '#{window_layout}')" "$topbottom"
must_equal "$($TMUX list-panes -F '#{pane_width}x#{pane_height},#{pane_left},#{pane_top}')" \
@@ -72,7 +104,7 @@ $TMUX select-layout "$layout" || fail "layout could not be restored"
# tiled geometry.
$TMUX resize-pane -t %0 -Z || fail "tiled zoom failed"
must_equal "$($TMUX display-message -p '#{window_layout}')" \
'8e46,80x24+0+0{%0,0:39x24+0+0:z;%1,1:40x24+40+0}'
"$(versioned '80x24+0+0{%0,0:39x24+0+0:z;%1,1:40x24+40+0}')"
$TMUX resize-pane -t %0 -Z || fail "tiled unzoom failed"
$TMUX select-layout "$layout" || fail "layout could not be restored"
@@ -115,7 +147,7 @@ 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}'
hidden=$(versioned '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"
@@ -228,7 +260,7 @@ $TMUX resize-pane -t %0 -Z || fail "unzoom failed"
$TMUX kill-server 2>/dev/null
sleep 0.5
$TMUX new-session -d -x 80 -y 24 || exit 1
legacy_negative='a9af,%0,0:20x8+0+0'
legacy_negative=$(versioned '%0,0:20x8+0+0')
$TMUX select-layout '20x8,-10,-20,0' || fail "legacy negative layout failed"
must_equal "$($TMUX display-message -p '#{window_layout}')" "$legacy_negative"
deep='%0,0:1x1+0+0'
@@ -262,43 +294,45 @@ $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}'
"$(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}'
$TMUX select-layout "$bottomright" || fail "bottom-right geometry failed"
must_equal "$($TMUX display-message -p -t %1 '#{pane_left},#{pane_top}')" \
'90,30'
must_equal "$($TMUX display-message -p '#{window_layout}')" \
'2242,120x40+0+0{%0,1:120x40+0+0;%1,0:30x10+90+30:f}'
"$(versioned '120x40+0+0{%0,1:120x40+0+0;%1,0:30x10+90+30: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"
absolute_body='120x40+0+0{%0,1:120x40+0+0;%1,0:20x8+-10+-20:f}'
$TMUX select-layout "3a22,$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}')" "$absolute"
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}'
$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}')" \
'9d83,120x40+0+0{%0,1:120x40+0+0;%1,0:20x8+-10+-20:fh}'
"$(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}'
must_equal "$($TMUX display-message -p '#{window_layout}')" \
'9d83,120x40+0+0{%0,1:120x40+0+0;%1,0:20x8+-10+-20:fh}'
"$(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}'
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}'
"$(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}'
$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}'
"$(versioned '120x40+0+0{%0,1:120x40+0+0;%1,0:30x10+80+0:f}')"
$TMUX kill-server 2>/dev/null
sleep 0.5
@@ -338,7 +372,7 @@ $TMUX new-pane -d -x 24 -y 8 -X 50 -Y 12 'sleep 100' || \
layout=$($TMUX display-message -p '#{window_layout}')
must_equal "$layout" \
'a6f2,120x40+0+0{%0,2:60x40+0+0;%1,3:59x40+61+0;%3,0:22x6+51+13:f;%2,1:28x8+11+6:f}'
"$(versioned '120x40+0+0{%0,2:60x40+0+0;%1,3:59x40+61+0;%3,0:22x6+51+13:f;%2,1:28x8+11+6:f}')"
case "$layout" in
*'<'*|*'>'*) fail "obsolete floating delimiters were emitted" ;;
esac

30
tmux.1
View File

@@ -2684,8 +2684,8 @@ For example:
.Bd -literal -offset indent
$ tmux list\-windows
0: ksh [159x48]
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]
layout: aadd,v2:159x48+0+0{%0,0:79x48+0+0;%1,1:79x48+80+0}
$ tmux select\-layout \[aq]aadd,v2:159x48+0+0{%0,0:79x48+0+0;%1,1:79x48+80+0}\[aq]
.Ed
.Pp
.Nm
@@ -3979,9 +3979,20 @@ spreads the current pane and any panes next to it out evenly.
may be a serialized layout with an optional outer checksum followed by one
root layout cell:
.Bd -literal -offset indent
layout = [checksum,]cell
layout = [checksum,][v2:]cell
.Ed
.Pp
The
.Ql v2:
prefix identifies the current format and is included in generated current
layouts.
It may be omitted when reading a current layout for compatibility with older
output.
An explicit
.Ql v2:
prefix may not be used with legacy cell syntax 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
@@ -4043,8 +4054,9 @@ 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 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
@@ -4071,7 +4083,7 @@ 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}
7749,v2: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
@@ -4090,12 +4102,14 @@ 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; a layout with an incorrect checksum is rejected.
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 a legacy layout, panes are treated as tiled, visible and unzoomed.
Outside control mode,
@@ -4120,7 +4134,7 @@ 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.
Each window layout retains its own checksum and version prefix.
All records are checked before any window is changed.
Unknown or duplicate window IDs are rejected.
.Tg selectp