diff --git a/layout-custom.c b/layout-custom.c index 703dd55ef..7c9c68bdc 100644 --- a/layout-custom.c +++ b/layout-custom.c @@ -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) { diff --git a/regress/control-client-layout.sh b/regress/control-client-layout.sh index f0cd72efc..f4d8460d4 100644 --- a/regress/control-client-layout.sh +++ b/regress/control-client-layout.sh @@ -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>&- diff --git a/regress/layout-custom.sh b/regress/layout-custom.sh index 3846dca64..a1bf6d15d 100755 --- a/regress/layout-custom.sh +++ b/regress/layout-custom.sh @@ -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 diff --git a/tmux.1 b/tmux.1 index 027b3cd77..70514aed0 100644 --- a/tmux.1 +++ b/tmux.1 @@ -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