More bugfixes and test updates.

This commit is contained in:
Dane Jensen
2026-07-25 14:06:26 -07:00
parent 2c44f97152
commit e550034c1b
2 changed files with 506 additions and 143 deletions

View File

@@ -93,14 +93,14 @@ static struct layout_cell *layout_parse_json_layout(struct json_node *,
struct layout_cell *,
struct layout_parse_ctx *);
/* Compare cell contexts in descending order of index. */
/* Compare cell contexts in ascending order of index. */
static int
layout_parse_index_cmp(const void *a, const void *b)
{
const struct layout_parse_cell_ctx *cca = a;
const struct layout_parse_cell_ctx *ccb = b;
return (ccb->index - cca->index);
return (cca->index - ccb->index);
}
/* Compare cell contexts in descending order of z-index. */
@@ -114,7 +114,7 @@ layout_parse_zindex_cmp(const void *a, const void *b)
}
/*
* Compare cell contexts in descending order of z-index. The active pane has
* Compare cell contexts in descending order of last. The active pane has
* index of -1.
*/
static int
@@ -196,7 +196,7 @@ layout_parse_remove_cctx(struct layout_parse_ctx *pctx, struct layout_cell *lc)
for (i = 0; i < pctx->clen; i++) {
if (lc == pctx->cctxs[i].lc) {
cctx = &pctx->cctxs[--pctx->clen];
memcpy(&pctx->cctxs[i], cctx, sizeof *cctx);
memmove(&pctx->cctxs[i], cctx, sizeof *cctx);
return (0);
}
}
@@ -461,7 +461,8 @@ layout_parse(struct window *w, const char *layout, char **cause)
* remain.
*/
lcchild = layout_find_bottomright(lc);
if (layout_parse_remove_cctx(&pctx, lc) != 0)
if (pctx.version != 1 && layout_parse_remove_cctx(&pctx,
lcchild) != 0)
goto fail;
layout_destroy_cell(w, lcchild, &lc);
}
@@ -516,7 +517,8 @@ layout_parse(struct window *w, const char *layout, char **cause)
/* Assign the panes into the cells. */
layout_assign(w, &pctx);
layout_parse_apply_ctx(w, &pctx);
if (pctx.version != 1)
layout_parse_apply_ctx(w, &pctx);
/* Update pane offsets and sizes. */
layout_fix_offsets(w);
@@ -524,7 +526,8 @@ layout_parse(struct window *w, const char *layout, char **cause)
recalculate_sizes();
layout_print_cell(lc, __func__, 0);
events_fire_window("window-layout-changed", w);
if (pctx.version == 1) /* backwards compatibility. */
events_fire_window("window-layout-changed", w);
return (0);
@@ -581,7 +584,7 @@ layout_assign_fallback(struct window_pane **wp, struct layout_cell *lc)
static void
layout_assign(struct window *w, struct layout_parse_ctx *pctx)
{
struct window_pane *wp = NULL;
struct window_pane *wp = TAILQ_FIRST(&w->panes);
if (pctx->clen > 0)
return (layout_assign_from_ctx(w, pctx));
@@ -716,7 +719,7 @@ layout_parse_json(struct json_node *json, struct layout_parse_ctx *pctx)
}
pctx->root = layout_parse_json_layout(field,
NULL, pctx);
if (pctx->root== NULL)
if (pctx->root == NULL)
goto fail;
}
break;
@@ -724,8 +727,10 @@ layout_parse_json(struct json_node *json, struct layout_parse_ctx *pctx)
break;
}
}
if (pctx->root == NULL)
if (pctx->root == NULL) {
*cause = xstrdup("missing layout");
goto fail;
}
json_destroy_node(json);
return (0);
@@ -733,9 +738,10 @@ fail:
json_destroy_node(json);
if (pctx->root != NULL)
layout_free_cell(pctx->root, 0);
pctx->root = NULL;
return (-1);
}
// TODO: add causes for failures.
/* Parse nodes into layout cells. */
static struct layout_cell *
layout_parse_json_layout(struct json_node *node, struct layout_cell *lcparent,
@@ -745,7 +751,7 @@ layout_parse_json_layout(struct json_node *node, struct layout_cell *lcparent,
struct layout_cell *lc = layout_create_cell(lcparent), *lcchild;
enum layout_type type = LAYOUT_WINDOWPANE;
const char *numstr;
char *endptr;
char *endptr, *tmp, **cause = pctx->cause;
u_int sx = UINT_MAX, sy = UINT_MAX;
int xoff = INT_MAX, yoff = INT_MAX;
int active = -1, last = -1, id = -1, index = -1;
@@ -764,6 +770,8 @@ layout_parse_json_layout(struct json_node *node, struct layout_cell *lcparent,
yoff = field->val.num;
else if (json_key_is_eq(field, "l"))
last = field->val.num;
else if (json_key_is_eq(field, "i"))
index = field->val.num;
else if (json_key_is_eq(field, "z")) {
zindex = field->val.num;
lc->flags |= LAYOUT_CELL_FLOATING;
@@ -781,25 +789,32 @@ layout_parse_json_layout(struct json_node *node, struct layout_cell *lcparent,
type = LAYOUT_TOPBOTTOM;
else if (json_val_is_eq(field, "p"))
type = LAYOUT_WINDOWPANE;
else
else {
tmp = xstrndup(field->val.jstr.ptr,
field->val.jstr.len);
xasprintf(cause, "invalid cell type %s",
tmp);
free(tmp);
goto fail;
}
lc->type = type;
} else if (json_key_is_eq(field, "I")) {
errno = 0;
numstr = field->val.jstr.ptr;
numlen = field->val.jstr.len;
if (*numstr != '%')
if (*numstr != '%') {
xasprintf(cause, "pane id must be "
"prefixed by '%%'");
goto fail;
}
id = strtol(numstr + 1, &endptr, 10);
if (errno != 0 || endptr != numstr + numlen)
goto fail;
} else if (json_key_is_eq(field, "i")) {
errno = 0;
numstr = field->val.jstr.ptr;
numlen = field->val.jstr.len;
index = strtol(numstr, &endptr, 10);
if (errno != 0 || endptr != numstr + numlen)
if (errno != 0 || endptr != numstr + numlen) {
tmp = xstrndup(numstr, numlen);
xasprintf(cause, "invalid pane id: %s",
tmp);
free(tmp);
goto fail;
}
}
break;
case NODE_ARRAY:
@@ -817,14 +832,19 @@ layout_parse_json_layout(struct json_node *node, struct layout_cell *lcparent,
break;
}
}
if (type == LAYOUT_WINDOWPANE && !TAILQ_EMPTY(&lc->cells))
if (type == LAYOUT_WINDOWPANE && !TAILQ_EMPTY(&lc->cells)) {
xasprintf(cause, "pane cells cannot have children");
goto fail;
if (type != LAYOUT_WINDOWPANE && TAILQ_EMPTY(&lc->cells))
} else if (type != LAYOUT_WINDOWPANE && TAILQ_EMPTY(&lc->cells)) {
xasprintf(cause, "non-pane cells must have children");
goto fail;
}
if (sx == UINT_MAX || sy == UINT_MAX || xoff == INT_MAX ||
yoff == INT_MAX)
yoff == INT_MAX) {
xasprintf(cause, "cell geometry must be fully specified");
goto fail;
}
layout_set_size(lc, sx, sy, xoff, yoff);
if (lc->type == LAYOUT_WINDOWPANE)
@@ -843,7 +863,6 @@ fail:
return (NULL);
}
/* Construct a layout root from a formatted string. */
static int
layout_construct(const char *layout, struct layout_parse_ctx *pctx)
@@ -865,14 +884,22 @@ layout_construct(const char *layout, struct layout_parse_ctx *pctx)
*pctx->cause = xstrdup("invalid layout checksum");
return (-1);
}
pctx->root = layout_construct_v1(NULL, &layout);
if ((pctx->root = layout_construct_v1(NULL, &layout)) == NULL) {
*pctx->cause = xstrdup("invalid layout");
return (-1);
}
pctx->version = 1;
} else {
if ((json = json_parse(layout, pctx->cause)) == NULL)
return (-1);
if (layout_parse_json(json, pctx) != 0)
return (-1);
if (pctx->version != 2)
if (pctx->version != 2) {
*pctx->cause = xstrdup("version mismatch.");
return (-1);
}
}
return (0);
@@ -886,6 +913,9 @@ layout_parse_apply_ctx(struct window *w, struct layout_parse_ctx *pctx)
struct window_pane *wp;
int i;
if (pctx->clen == 0)
return;
/* Apply z-indexes. */
while (!TAILQ_EMPTY(&w->z_index)) {
wp = TAILQ_FIRST(&w->z_index);
@@ -901,10 +931,19 @@ layout_parse_apply_ctx(struct window *w, struct layout_parse_ctx *pctx)
TAILQ_INSERT_HEAD(&w->z_index, wp, zentry);
}
/* Apply active/last. */
/* Set the active pane. */
for (i = 0; i < pctx->clen; i++) {
cctx = &pctx->cctxs[i];
if (cctx->active == 1) {
window_set_active_pane(w, cctx->lc->wp, 1);
break;
}
}
/* Apply last panes. */
while (!TAILQ_EMPTY(&w->last_panes)) {
wp = TAILQ_FIRST(&w->last_panes);
TAILQ_REMOVE(&w->last_panes, wp, sentry);
window_pane_stack_remove(&w->last_panes, wp);
}
qsort(pctx->cctxs, pctx->clen, sizeof pctx->cctxs[0],
@@ -913,10 +952,9 @@ layout_parse_apply_ctx(struct window *w, struct layout_parse_ctx *pctx)
for (i = 0; i < pctx->clen; i++) {
cctx = &pctx->cctxs[i];
wp = cctx->lc->wp;
if (cctx->active) {
window_set_active_pane(w, wp, 1);
if (cctx->last < 0 || cctx->active == 1) {
continue;
}
TAILQ_INSERT_HEAD(&w->last_panes, wp, sentry);
window_pane_stack_push(&w->last_panes, wp);
}
}

View File

@@ -1,10 +1,17 @@
#!/bin/sh
# Tests of the custom layout dumper and parser in layout-custom.c. layout_dump
# is reached through the #{window_layout} and #{window_visible_layout} formats
# and layout_parse through "select-layout <layout>".
# Tests of the custom layout dumper and evaluator in layout-custom.c, and of
# the JSON tokenizer and parser in json.c that the current layout format is
# built on.
#
# Both formats are covered:
# layout_dump is reached through the #{window_layout} and
# #{window_visible_layout} formats and layout_parse through
# "select-layout <layout>". json.c has no command of its own either:
# layout_construct sniffs the first non-blank character and hands anything
# starting with '{' to json_parse, so select-layout is the only way into it
# from the shell as well.
#
# Both layout formats are covered:
# - the current (v2) JSON format, which is what every client except an old
# control client sees;
# - the legacy (v1) format, which is still produced for a control client that
@@ -15,20 +22,29 @@
# - dumping a single pane, a split, the "a" (active) and "l" (last pane) keys
# and the "z" key of a floating pane;
# - #{window_visible_layout} agreeing with #{window_layout};
# - the JSON syntax itself: insignificant whitespace, backslash escapes inside
# strings, the number and boolean forms, and one failure for each error
# json.c can report;
# - a dump being parsed back to exactly the same layout (round trip), after
# another layout has been applied in between;
# - parsing a hand-written v2 layout, including insignificant whitespace;
# - parsing a hand-written v2 layout and the panes being assigned to its cells
# in order;
# - the same layouts with their fields in reversed and scrambled orders,
# including "c" before "t" (children parsed before the cell type is known)
# and "V" after "L";
# - parsing a v1 layout, with the checksum computed here independently of
# layout_checksum(), and dumping the same layout as v1 to a control client;
# - a layout with more cells than the window has panes having the bottom right
# cells dropped;
# - failures: a bad v1 header or checksum, a malformed v1 body, unterminated
# and malformed JSON, a wrong version, a missing or duplicated root cell,
# missing sizes, bad cell types and pane ids, leaf cells with children and
# node cells without, too few cells for the panes and inconsistent sizes.
# cells dropped, in both formats;
# - a layout naming no active or last pane leaving both as they were;
# - parsing a v1 layout and dumping it back as v1 through a control client,
# with the checksum computed here independently of layout_checksum(), and a
# v1 layout leaving the active pane and last pane stack untouched;
# - the %layout-change notification, in both formats at once: two control
# clients watching one layout change, only one of which has asked for new
# layouts, and the number of notifications a change produces in each format;
# - failures: a bad v1 header, checksum or body, a wrong version, a missing or
# duplicated root cell, missing sizes, bad cell types and pane ids, leaf
# cells with children and node cells without, too few cells for the panes and
# inconsistent sizes.
PATH=/bin:/usr/bin
TERM=screen
@@ -140,7 +156,14 @@ v1()
}'
}
ONE='{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"a":true,"i":"%N"}}'
# A pane cell is dumped as its geometry, then "a" if it is the active pane or
# "l" with its position on the last pane stack if it is on it, then "i" with
# its pane index, then "z" if it is floating, then "I" with its pane id.
ONE='{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"a":true,"i":0,"I":"%N"}}'
# A single leaf cell filling the window, without the keys that only the dumper
# writes. Used by the JSON checks, which care about the syntax around it.
LEAF='{"t":"p","w":80,"h":24,"x":0,"y":0}'
$TMUX new-session -d -s L -x 80 -y 24 -n one || exit 1
@@ -159,11 +182,145 @@ must_equal 'Single pane visible layout' "$(visible_layout L:one)" "$ONE"
# The bottom right cells are closed until as many are left as there are panes,
# so a two cell layout applied to a one pane window collapses back to the
# single pane filling the window.
# single pane filling the window: the cell that is left takes the space of the
# one that was closed.
check_ok select-layout -t L:one \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0},{"t":"p","w":80,"h":12,"x":0,"y":12}]}}'
must_equal 'Trimmed layout' "$(layout L:one)" "$ONE"
# ---------------------------------------------------------------------------
# The JSON syntax.
#
# These run on the one pane window and are written so that what they prove
# depends on json.c rather than on the layout evaluation in layout-custom.c:
# an accepted layout is only required to leave the window as its single pane,
# and values that are not part of the layout format are carried on keys
# layout-custom.c never looks at ("n", "b" and so on), which it skips, so
# numbers, booleans and escapes can be exercised on their own.
#
# Objects nested in an array nested in an object are not checked here: every
# split layout below is one.
#
# Two of json.c's messages cannot be reached from the shell and so are not
# covered. "expected object" is unreachable because layout_construct() only
# calls json_parse() once the string already starts with '{', and "invalid
# boolean" is unreachable because json_parse_boolean() is only called after the
# value has already matched "true" or "false".
# check_json_ok $what $layout
#
# select-layout must parse $layout and leave the window as its single pane.
check_json_ok()
{
check_ok select-layout -t L:one "$2"
must_equal "Layout after '$1'" "$(layout L:one)" "$ONE"
}
# check_json_fail $what $reason $layout
#
# select-layout must reject $layout with an error beginning with $reason.
# json_error() appends up to ERROR_CTX_LEN characters of context from the point
# of failure and cmd-select-layout.c then appends the layout itself, so only
# the reason is matched.
check_json_fail()
{
out=$($TMUX select-layout -t L:one "$3" 2>&1) &&
fail "$1: select-layout succeeded (expected failure)"
case "$out" in
"$2"*) ;;
*) fail "$1: expected '$2...' but got '$out'";;
esac
}
# Whitespace between tokens is skipped. A number is scanned up to the ',', ']',
# '}' or whitespace that ends it, so a space after a number is fine but one
# inside it is not.
check_json_ok 'Spaces between tokens' \
'{ "V" : 2 , "L" : { "t" : "p" , "w" : 80 , "h" : 24 , "x" : 0 , "y" : 0 } }'
check_json_ok 'Newlines and tabs between tokens' "$(printf '{
\t"V": 2,
\t"L": {
\t\t"t": "p",
\t\t"w": 80,
\t\t"h": 24,
\t\t"x": 0,
\t\t"y": 0
\t}
}')"
check_json_ok 'Carriage returns between tokens' \
"$(printf '{\r"V":2,\r"L":%s\r}' "$LEAF")"
# A backslash makes the tokenizer consume the next character whatever it is, so
# an escaped quote does not end the string. The key is not one that
# layout-custom.c looks at, so all that is being checked is that the string
# ended in the right place and the object still parsed.
check_json_ok 'Escaped quote in a string' \
'{"V":2,"a\"b":0,"L":'"$LEAF"'}'
# An escaped backslash immediately before the closing quote: the escape has to
# be cleared again so that the quote after it does end the string.
check_json_ok 'Escaped backslash before the closing quote' \
'{"V":2,"a\\":0,"L":'"$LEAF"'}'
# Numbers and booleans, again on keys layout-custom.c ignores, so only json.c
# decides whether they are accepted.
check_json_ok 'Zero' '{"V":2,"n":0,"L":'"$LEAF"'}'
check_json_ok 'Several digits' '{"V":2,"n":1234567,"L":'"$LEAF"'}'
check_json_ok 'Negative number' '{"V":2,"n":-42,"L":'"$LEAF"'}'
check_json_ok 'Booleans' '{"V":2,"b":true,"d":false,"L":'"$LEAF"'}'
# Tokenizer failures. A value that runs to the end of the input has no
# terminator, so it is the tokenizer rather than the parser that gives up. Both
# the number scan and the string scan have to notice this, and with the closing
# quote escaped there is no terminator left either.
check_json_fail 'Unterminated number' 'tokenization error' '{"V":2'
check_json_fail 'Unterminated string' 'tokenization error' '{"V":"x'
check_json_fail 'Escaped closing quote' 'tokenization error' \
'{"V":2,"L":{"t":"p\"}}'
# Something that is not a quoted string where a key belongs.
check_json_fail 'Missing key' 'invalid key' '{"V":2,,"L":'"$LEAF"'}'
# A key not followed by ':'.
check_json_fail 'Missing colon' 'missing colon' '{"V","L":2}'
# A bare word that is neither "true", "false" nor a number. This is where
# "null" ends up.
check_json_fail 'Unknown literal' 'invalid value' '{"V":null,"L":'"$LEAF"'}'
# A ':' with no value after it, so the token where the value belongs is one the
# object parser has no case for.
check_json_fail 'Missing value' 'unsupported object token' '{"V":}'
# A ',' with nothing after it, and a value with no ',' before the next key.
check_json_fail 'Trailing comma in an object' 'invalid object' \
'{"V":2,"L":'"$LEAF"',}'
check_json_fail 'Missing comma in an object' 'invalid object' \
'{"V":2 "L":'"$LEAF"'}'
# Arrays hold objects and nothing else.
check_json_fail 'Non-object in an array' 'invalid array member' \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":["x"]}}'
check_json_fail 'Trailing comma in an array' 'invalid array' \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0},]}}'
# An empty string is two adjacent quotes with no value token between them,
# which the string parser does not accept.
check_json_fail 'Empty string' 'invalid string' '{"V":2,"L":""}'
# A number token that strtoll does not consume all of.
check_json_fail 'Number with trailing characters' 'invalid number' \
'{"V":8a,"L":'"$LEAF"'}'
# Anything after the top level object.
check_json_fail 'Data after the top level object' 'unexpected trailing data' \
'{"V":2,"L":'"$LEAF"'}{}'
# None of the rejections touched the layout.
must_equal 'Layout after rejected parses' "$(layout L:one)" "$ONE"
# ---------------------------------------------------------------------------
# Dumping a split.
@@ -174,8 +331,11 @@ q0=$($TMUX display-message -p -t L:two.0 '#{pane_id}')
# one for the border between them. With -d the top pane stays active.
check_ok split-window -d -v -l 12 -t L:two.0
q1=$($TMUX display-message -p -t L:two.1 '#{pane_id}')
# Nothing has changed the active pane, so the last pane stack is still empty
# and the bottom pane has neither "a" nor "l".
must_equal 'Split layout' "$(layout L:two)" \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"a":true,"i":"%N"},{"t":"p","w":80,"h":12,"x":0,"y":12,"i":"%N"}]}}'
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":12,"x":0,"y":12,"i":1,"I":"%N"}]}}'
# ---------------------------------------------------------------------------
# The active and last pane keys.
@@ -184,11 +344,12 @@ must_equal 'Split layout' "$(layout L:two)" \
# last pane stack, where it is at index 0.
check_ok select-pane -t "$q1"
must_equal 'Layout after select-pane' "$(layout L:two)" \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"l":0,"i":"%N"},{"t":"p","w":80,"h":12,"x":0,"y":12,"a":true,"i":"%N"}]}}'
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"l":0,"i":0,"I":"%N"},{"t":"p","w":80,"h":12,"x":0,"y":12,"a":true,"i":1,"I":"%N"}]}}'
# Selecting the top pane again swaps the two keys over.
# Selecting the top pane again swaps the two keys over. "i" and "I" do not
# move: they are the pane's position in the window and its id.
check_ok select-pane -t "$q0"
SPLIT='{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"a":true,"i":"%N"},{"t":"p","w":80,"h":12,"x":0,"y":12,"l":0,"i":"%N"}]}}'
SPLIT='{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":12,"x":0,"y":12,"l":0,"i":1,"I":"%N"}]}}'
must_equal 'Layout after select-pane back' "$(layout L:two)" "$SPLIT"
# ---------------------------------------------------------------------------
@@ -199,32 +360,40 @@ must_equal 'Layout after select-pane back' "$(layout L:two)" "$SPLIT"
# The zoomed case is deliberately not covered here. While a pane is zoomed
# #{window_layout} dumps the saved (unzoomed) layout and
# #{window_visible_layout} the zoomed one, but that depends on how zooming
# stashes the layout root rather than on anything in layout-custom.c. Add it
# back once zooming has settled.
# stashes the layout root rather than on anything in layout-custom.c.
must_equal 'Visible layout' "$(visible_layout L:two)" "$SPLIT"
# ---------------------------------------------------------------------------
# Round trip.
# Make the two panes obviously uneven so that the layout applied in between
# cannot be mistaken for the saved one.
# cannot be mistaken for the saved one. A resize shows up in the dump as the
# new cell sizes and offsets.
check_ok resize-pane -t "$q0" -y 5
saved=$(raw_layout L:two)
must_equal 'Resized layout' "$(layout L:two)" \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":5,"x":0,"y":0,"a":true,"i":"%N"},{"t":"p","w":80,"h":18,"x":0,"y":6,"l":0,"i":"%N"}]}}'
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":5,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":18,"x":0,"y":6,"l":0,"i":1,"I":"%N"}]}}'
check_ok select-layout -t L:two even-vertical
must_differ 'Layout after even-vertical' "$(raw_layout L:two)" "$saved"
# Parsing a dump gives back exactly the same dump, pane ids included.
# Parsing a dump gives back exactly the same dump, pane ids included. The panes
# go back into the cells that named them: the cells are ordered by "i" and then
# given the window's panes in order, so a cell dumped with "i":k must come back
# the k'th.
check_ok select-layout -t L:two "$saved"
must_equal 'Round tripped layout' "$(raw_layout L:two)" "$saved"
# ---------------------------------------------------------------------------
# Parsing a hand-written layout.
# Whitespace between tokens is skipped. Note that a number is scanned up to the
# ',', ']' or '}' that ends it, so there is deliberately no space there.
# Laid out over several lines to keep it readable; that the whitespace is
# skipped at all is json.c's business, what matters here is that the cells come
# out of it in the right shape.
#
# "a" and "l" are given on the cells so that the active pane and the last pane
# stack are pinned by the layout rather than left to whatever a layout that
# names neither happens to produce.
check_ok select-layout -t L:two '{
"V": 2,
"L": {
@@ -234,13 +403,13 @@ check_ok select-layout -t L:two '{
"x": 0,
"y": 0,
"c": [
{"t": "p", "w": 30, "h": 24, "x": 0, "y": 0},
{"t": "p", "w": 49, "h": 24, "x": 31, "y": 0}
{"t": "p", "w": 30, "h": 24, "x": 0, "y": 0, "a": true},
{"t": "p", "w": 49, "h": 24, "x": 31, "y": 0, "l": 0}
]
}
}'
must_equal 'Hand-written layout' "$(layout L:two)" \
'{"V":2,"L":{"t":"h","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":30,"h":24,"x":0,"y":0,"a":true,"i":"%N"},{"t":"p","w":49,"h":24,"x":31,"y":0,"l":0,"i":"%N"}]}}'
'{"V":2,"L":{"t":"h","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":30,"h":24,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":49,"h":24,"x":31,"y":0,"l":0,"i":1,"I":"%N"}]}}'
# The panes are assigned to the cells in order.
must_equal 'First pane width' \
@@ -255,58 +424,31 @@ must_equal 'Second pane width' \
# every object has its keys reversed: "c" comes before "t", so the children are
# evaluated while the cell type is still the default, and "V" comes after "L",
# so the version is only known once the layout has been built.
check_ok select-layout -t L:two '{"L":{"c":[{"y":0,"x":0,"h":8,"w":80,"t":"p"},{"y":9,"x":0,"h":15,"w":80,"t":"p"}],"y":0,"x":0,"h":24,"w":80,"t":"v"},"V":2}'
check_ok select-layout -t L:two '{"L":{"c":[{"a":true,"y":0,"x":0,"h":8,"w":80,"t":"p"},{"l":0,"y":9,"x":0,"h":15,"w":80,"t":"p"}],"y":0,"x":0,"h":24,"w":80,"t":"v"},"V":2}'
must_equal 'Reversed field order' "$(layout L:two)" \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":8,"x":0,"y":0,"a":true,"i":"%N"},{"t":"p","w":80,"h":15,"x":0,"y":9,"l":0,"i":"%N"}]}}'
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":8,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":15,"x":0,"y":9,"l":0,"i":1,"I":"%N"}]}}'
# Keys interleaved rather than simply reversed, with "c" in the middle. The
# "a" here is on the second cell but is ignored on parsing, so the dump still
# marks the first cell active: the active pane comes from the window, not the
# layout.
# Keys interleaved rather than simply reversed, with "c" in the middle. This
# time "a" is on the second cell, so the second pane becomes the active one:
# which pane is active comes from the layout, while "i" and "I" still come from
# the window. The first cell names neither "a" nor "l", so its pane is neither
# active nor on the last pane stack and the dump gives it neither key.
check_ok select-layout -t L:two '{"V":2,"L":{"h":24,"c":[{"w":40,"t":"p","y":0,"h":24,"x":0},{"a":true,"h":24,"w":39,"y":0,"t":"p","x":41}],"w":80,"y":0,"t":"h","x":0}}'
must_equal 'Scrambled field order' "$(layout L:two)" \
'{"V":2,"L":{"t":"h","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":40,"h":24,"x":0,"y":0,"a":true,"i":"%N"},{"t":"p","w":39,"h":24,"x":41,"y":0,"l":0,"i":"%N"}]}}'
# The same rejections apply whatever order the fields are in: a leaf with
# children when "c" is seen first, a node with no children when "t" is last, a
# bad cell type when "t" is last, and a bad pane id when "i" is first.
check_fail 'invalid layout: {"V":2,"L":{"c":[{"t":"p","w":80,"h":24,"x":0,"y":0}],"t":"p","w":80,"h":24,"x":0,"y":0}}' \
select-layout -t L:two '{"V":2,"L":{"c":[{"t":"p","w":80,"h":24,"x":0,"y":0}],"t":"p","w":80,"h":24,"x":0,"y":0}}'
check_fail 'invalid layout: {"V":2,"L":{"w":80,"h":24,"x":0,"y":0,"t":"v"}}' \
select-layout -t L:two '{"V":2,"L":{"w":80,"h":24,"x":0,"y":0,"t":"v"}}'
check_fail 'invalid layout: {"V":2,"L":{"w":80,"h":24,"x":0,"y":0,"t":"q"}}' \
select-layout -t L:two '{"V":2,"L":{"w":80,"h":24,"x":0,"y":0,"t":"q"}}'
check_fail 'invalid layout: {"V":2,"L":{"i":"0","t":"p","w":80,"h":24,"x":0,"y":0}}' \
select-layout -t L:two '{"V":2,"L":{"i":"0","t":"p","w":80,"h":24,"x":0,"y":0}}'
# A child that fails after some children have already been added, with "c"
# before "t" so the parent's type is still the default when it gives up. This
# is the case the cleanup at the end of layout_evaluate_layout exists for: the
# already-built children have to be freed even though the parent does not yet
# look like a node. The second child has no "y".
check_fail 'invalid layout: {"V":2,"L":{"c":[{"t":"p","w":80,"h":11,"x":0,"y":0},{"t":"p","w":80,"h":12,"x":0}],"t":"v","w":80,"h":24,"x":0,"y":0}}' \
select-layout -t L:two '{"V":2,"L":{"c":[{"t":"p","w":80,"h":11,"x":0,"y":0},{"t":"p","w":80,"h":12,"x":0}],"t":"v","w":80,"h":24,"x":0,"y":0}}'
# A wrong version after a layout that is otherwise fine, so the built cells
# have to be thrown away once "V" is finally seen.
check_fail 'invalid layout: {"L":{"t":"p","w":80,"h":24,"x":0,"y":0},"V":1}' \
select-layout -t L:two '{"L":{"t":"p","w":80,"h":24,"x":0,"y":0},"V":1}'
'{"V":2,"L":{"t":"h","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":40,"h":24,"x":0,"y":0,"i":0,"I":"%N"},{"t":"p","w":39,"h":24,"x":41,"y":0,"a":true,"i":1,"I":"%N"}]}}'
# ---------------------------------------------------------------------------
# The legacy (v1) format.
body="80x24,0,0[80x11,0,0,${q0#%},80x12,0,12,${q1#%}]"
# The layout just applied, in v1: a left/right cell is written with braces and
# a top/bottom cell with brackets, and each leaf carries its pane id without
# the leading %.
v1body="80x24,0,0{40x24,0,0,${q0#%},39x24,41,0,${q1#%}}"
# A v1 layout with a correct checksum is accepted and gives the same layout as
# the equivalent v2 one.
check_ok select-layout -t L:two "$(v1 "$body")"
must_equal 'Layout parsed from v1' "$(layout L:two)" "$SPLIT"
# A control client that has not asked for new layouts is dumped v1, which must
# be the string that was just parsed. Its output is wrapped in %begin/%end
# guard lines.
# A control client that has not asked for new layouts is dumped v1. Its output
# is wrapped in %begin/%end guard lines.
got=$($TMUX -C display-message -p -t L:two '#{window_layout}' | grep -v '^%')
must_equal 'v1 dump' "$got" "$(v1 "$body")"
must_equal 'v1 dump' "$got" "$(v1 "$v1body")"
# With the new-layouts flag the same client is dumped v2 instead. The flag is
# set with "attach -f" rather than refresh-client because refresh-client needs
@@ -315,8 +457,54 @@ got=$(printf "display-message -p -t L:two '#{window_layout}'\n" |
$TMUX -C attach -f new-layouts -t L 2>&1 | grep -v '^%')
must_contain 'v2 dump for control client' "$got" '{"V":2,"L":'
# A v1 layout with a correct checksum is parsed, and dumping v1 again gives
# back the same string. That is the whole of what v1 carries: the cells take
# the sizes and offsets from the body, and the panes are assigned to them in
# order, which is what puts the same two ids back in the same two places. It is
# checked in v1 rather than against a v2 dump so that nothing v1 has no opinion
# on - the active pane, the last pane stack, the pane index - comes into it.
v1vsplit="80x24,0,0[80x11,0,0,${q0#%},80x12,0,12,${q1#%}]"
check_ok select-layout -t L:two "$(v1 "$v1vsplit")"
got=$($TMUX -C display-message -p -t L:two '#{window_layout}' | grep -v '^%')
must_equal 'v1 round trip' "$got" "$(v1 "$v1vsplit")"
# v1 names no active pane, last pane or z-index and must disturb none of them.
# Applying the v1 form of the layout the window already has therefore leaves
# even the v2 dump the same byte for byte, last pane stack included.
check_ok select-pane -t "$q1"
check_ok select-pane -t "$q0"
before=$(raw_layout L:two)
check_ok select-layout -t L:two "$(v1 "$v1vsplit")"
must_equal 'v1 leaves the active and last panes alone' \
"$(raw_layout L:two)" "$before"
# A v1 layout with more cells than the window has panes is trimmed like any
# other: the bottom right cell is closed and the cell above it takes its eight
# rows and the border between them, leaving 16. Pane ids in a v1 body are not
# used to place panes, so the third cell can carry any id.
v1three="80x24,0,0[80x7,0,0,${q0#%},80x7,0,8,${q1#%},80x8,0,16,999]"
check_ok select-layout -t L:two "$(v1 "$v1three")"
got=$($TMUX -C display-message -p -t L:two '#{window_layout}' | grep -v '^%')
must_equal 'v1 layout trimmed' "$got" \
"$(v1 "80x24,0,0[80x7,0,0,${q0#%},80x16,0,8,${q1#%}]")"
# ---------------------------------------------------------------------------
# Failures.
# Cells that name no active or last pane.
# "a" and "l" are the only things that decide which pane is active and what is
# on the last pane stack, so a layout naming neither leaves the active pane
# where it was and puts nothing on the stack. Here the top pane is active and
# the bottom one is at index 0 of the stack beforehand; afterwards the top pane
# is still active and the bottom pane is on no stack, so it has no "l".
check_ok select-pane -t "$q1"
check_ok select-pane -t "$q0"
check_ok select-layout -t L:two \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":9,"x":0,"y":0},{"t":"p","w":80,"h":14,"x":0,"y":10}]}}'
must_equal 'Layout naming no active pane' "$(layout L:two)" \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":9,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":14,"x":0,"y":10,"i":1,"I":"%N"}]}}'
# ---------------------------------------------------------------------------
# Failures with a message.
# check_layout_fail $cause $layout
#
@@ -326,6 +514,9 @@ check_layout_fail()
check_fail "$1: $2" select-layout -t L:two "$2"
}
# A rejected layout must leave the window alone, whatever it was.
unchanged=$(raw_layout L:two)
# Not JSON and not a checksum.
check_layout_fail 'malformed layout header' 'garbage'
@@ -333,47 +524,12 @@ check_layout_fail 'malformed layout header' 'garbage'
good=$(v1 '80x24,0,0')
check_layout_fail 'invalid layout checksum' "${good%%,*},80x24,0,1"
# A correct checksum over a body that is not a layout.
# A correct checksum over a body that is not a layout: a cell with no offsets,
# and a top to bottom cell closed with '}' instead of ']'. layout_construct_v1
# returns NULL for both and layout_construct() reports it.
check_layout_fail 'invalid layout' "$(v1 '80x24')"
check_layout_fail 'invalid layout' "$(v1 '80x24,0,0[80x11,0,0,80x12,0,12}')"
# The value of "V" runs to the end of the string without a terminator, so
# tokenizing fails.
check_layout_fail 'invalid layout characters' '{"V":2'
# Structurally invalid JSON: an unclosed object, a trailing comma, a second
# object after the layout, a number that is not one, and an array member that
# is not an object.
check_layout_fail 'invalid layout json' '{"V":2,"L":{"t":"p"'
check_layout_fail 'invalid layout json' \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0},}'
check_layout_fail 'invalid layout json' \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0}}{}'
check_layout_fail 'invalid layout json' \
'{"V":2,"L":{"t":"p","w":8a,"h":24,"x":0,"y":0}}'
check_layout_fail 'invalid layout json' \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"c":["x"]}}'
# Valid JSON that is not a valid layout: the wrong version, no root cell, two
# root cells, a missing "y", an unknown cell type and a pane id without its %.
check_layout_fail 'invalid layout' \
'{"V":1,"L":{"t":"p","w":80,"h":24,"x":0,"y":0}}'
check_layout_fail 'invalid layout' '{"V":2}'
check_layout_fail 'invalid layout' \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0},"L":{"t":"p","w":80,"h":24,"x":0,"y":0}}'
check_layout_fail 'invalid layout' '{"V":2,"L":{"t":"p","w":80,"h":24,"x":0}}'
check_layout_fail 'invalid layout' \
'{"V":2,"L":{"t":"q","w":80,"h":24,"x":0,"y":0}}'
check_layout_fail 'invalid layout' \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":"0"}}'
# A node cell must have children and a leaf cell must not.
check_layout_fail 'invalid layout' '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0}}'
check_layout_fail 'invalid layout' \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[]}}'
check_layout_fail 'invalid layout' \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":24,"x":0,"y":0}]}}'
# Fewer cells than the window has panes; unlike the other way around this
# cannot be fixed up.
check_layout_fail 'have 2 panes but need 1' \
@@ -383,8 +539,72 @@ check_layout_fail 'have 2 panes but need 1' \
check_layout_fail 'size mismatch after applying layout' \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0},{"t":"p","w":40,"h":12,"x":0,"y":12}]}}'
# The rest are valid JSON, so it is layout_parse_json() and
# layout_parse_json_layout() doing the rejecting rather than json.c, and their
# own cause reaches the client.
# Two root cells.
check_layout_fail 'duplicate layout' \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0},"L":{"t":"p","w":80,"h":24,"x":0,"y":0}}'
# A missing "y". A cell needs all four of "w", "h", "x" and "y".
check_layout_fail 'cell geometry must be fully specified' \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0}}'
# An unknown cell type: only "h", "v" and "p" exist.
check_layout_fail 'invalid cell type q' \
'{"V":2,"L":{"t":"q","w":80,"h":24,"x":0,"y":0}}'
# A pane id without its %, and one with trailing rubbish after the number. Note
# it is "I" that carries the pane id and requires the %; "i" is the pane index
# and takes a plain number.
check_layout_fail "pane id must be prefixed by '%'" \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"I":"0"}}'
check_layout_fail 'invalid pane id: %1x' \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"I":"%1x"}}'
# A node cell must have children and a leaf cell must not.
check_layout_fail 'non-pane cells must have children' \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0}}'
check_layout_fail 'non-pane cells must have children' \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[]}}'
check_layout_fail 'pane cells cannot have children' \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":24,"x":0,"y":0}]}}'
# The same rejections apply whatever order the fields are in: a leaf with
# children when "c" is seen first, a node with no children when "t" is last, a
# bad cell type when "t" is last, and a bad pane id when "I" is first.
check_layout_fail 'pane cells cannot have children' \
'{"V":2,"L":{"c":[{"t":"p","w":80,"h":24,"x":0,"y":0}],"t":"p","w":80,"h":24,"x":0,"y":0}}'
check_layout_fail 'non-pane cells must have children' \
'{"V":2,"L":{"w":80,"h":24,"x":0,"y":0,"t":"v"}}'
check_layout_fail 'invalid cell type q' '{"V":2,"L":{"w":80,"h":24,"x":0,"y":0,"t":"q"}}'
check_layout_fail "pane id must be prefixed by '%'" \
'{"V":2,"L":{"I":"0","t":"p","w":80,"h":24,"x":0,"y":0}}'
# A child that fails after some children have already been added, with "c"
# before "t" so the parent's type is still the default when it gives up. This
# is the case the cleanup at the end of layout_parse_json_layout exists for:
# the already-built children have to be freed even though the parent does not
# yet look like a node. The second child has no "y", and its cause is the one
# that comes back.
check_layout_fail 'cell geometry must be fully specified' \
'{"V":2,"L":{"c":[{"t":"p","w":80,"h":11,"x":0,"y":0},{"t":"p","w":80,"h":12,"x":0}],"t":"v","w":80,"h":24,"x":0,"y":0}}'
# No root cell at all. Every other rejection above comes from a cell that
# failed to parse; this one is the check after the loop, reached when no "L"
# was seen at all.
check_layout_fail 'missing layout' '{"V":2}'
# The wrong version, and the wrong version after a layout that is otherwise
# fine so that the built cells have to be thrown away once "V" is finally seen.
check_layout_fail 'version mismatch.' \
'{"V":1,"L":{"t":"p","w":80,"h":24,"x":0,"y":0}}'
check_layout_fail 'version mismatch.' \
'{"L":{"t":"p","w":80,"h":24,"x":0,"y":0},"V":1}'
# None of that touched the layout.
must_equal 'Layout after failures' "$(layout L:two)" "$SPLIT"
must_equal 'Layout after failures' "$(raw_layout L:two)" "$unchanged"
# ---------------------------------------------------------------------------
# Floating panes.
@@ -399,6 +619,111 @@ must_contain 'Floating layout' "$(layout L:float)" '"z":'
check_ok select-layout -t L:float "$(raw_layout L:float)"
must_contain 'Floating layout after round trip' "$(layout L:float)" '"z":'
# ---------------------------------------------------------------------------
# Control mode notifications.
#
# %layout-change is what a control client actually reads a layout from, and it
# carries both #{window_layout} and #{window_visible_layout}. Its template is
# expanded once per client (control-notify.c), so two clients watching the same
# window must be told about the same change in different formats: v1 for the
# one that has not asked for new layouts, v2 for the one that has.
#
# The dumps above go through "-C display-message", which only ever reaches the
# format callbacks for the client asking. This needs clients that stay
# attached while something else changes the layout, so they go on the end of
# fifos and the change is made from outside.
DIR=$(mktemp -d) || exit 1
OLDIN="$DIR/old-in"
OLDOUT="$DIR/old-out"
NEWIN="$DIR/new-in"
NEWOUT="$DIR/new-out"
OLDPID=
NEWPID=
cleanup()
{
[ -n "$OLDPID" ] && kill "$OLDPID" 2>/dev/null
[ -n "$NEWPID" ] && kill "$NEWPID" 2>/dev/null
rm -rf "$DIR"
}
trap cleanup EXIT
# wait_for $file $text
#
# Wait for $text to appear in a control client's output.
wait_for()
{
i=0
while [ "$i" -lt 6 ]; do
grep -F -- "$2" "$1" >/dev/null 2>&1 && return 0
sleep 1
i=$((i + 1))
done
echo "missing from $1: $2" >&2
cat "$1" >&2
return 1
}
mkfifo "$OLDIN" "$NEWIN" || exit 1
: >"$OLDOUT"
: >"$NEWOUT"
$TMUX -C attach -t L <"$OLDIN" >"$OLDOUT" 2>&1 &
OLDPID=$!
exec 4>"$OLDIN"
$TMUX -C attach -f new-layouts -t L <"$NEWIN" >"$NEWOUT" 2>&1 &
NEWPID=$!
exec 5>"$NEWIN"
# Both clients have to be attached before the layout changes, or they miss the
# notification entirely.
printf 'display-message -p ready\n' >&4
printf 'display-message -p ready\n' >&5
wait_for "$OLDOUT" ready || fail 'Control client without new-layouts did not attach'
wait_for "$NEWOUT" ready || fail 'Control client with new-layouts did not attach'
wid=$($TMUX display-message -p -t L:two '#{window_id}')
# One layout change, made by a third client so that neither of the two is the
# one running the command. 8 lines for the top pane leaves 15 for the bottom
# and one for the border.
check_ok resize-pane -t "$q0" -y 8
# Nothing is zoomed, so both fields of the notification carry the same layout.
# The v2 one is compared against the dump rather than a literal so that it is
# the two formats being checked and not the geometry again.
v2now=$(raw_layout L:two)
v1now=$(v1 "80x24,0,0[80x8,0,0,${q0#%},80x15,0,9,${q1#%}]")
wait_for "$NEWOUT" "%layout-change $wid $v2now $v2now " ||
fail 'No v2 %layout-change for the client with new-layouts'
wait_for "$OLDOUT" "%layout-change $wid $v1now $v1now " ||
fail 'No v1 %layout-change for the client without new-layouts'
# How many notifications one layout change produces, which differs by format
# on purpose. cmd_select_layout_exec() fires window-layout-changed for any
# layout it applies, and layout_parse() fires it again for a v1 one, so v1
# arrives twice - which is what master does for every layout, and what control
# clients written against it expect. v2 is new and has no such clients, so it
# gets the single notification. Counting the delta rather than the total, with
# a settle in between, keeps this independent of what has already been sent.
n1=$(grep -c "%layout-change $wid " "$OLDOUT")
check_ok select-layout -t L:two \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":9,"x":0,"y":0},{"t":"p","w":80,"h":14,"x":0,"y":10}]}}'
sleep 2
n2=$(grep -c "%layout-change $wid " "$OLDOUT")
must_equal 'Notifications for a v2 layout' "$((n2 - n1))" '1'
check_ok select-layout -t L:two "$(v1 "$v1vsplit")"
sleep 2
n3=$(grep -c "%layout-change $wid " "$OLDOUT")
must_equal 'Notifications for a v1 layout' "$((n3 - n2))" '2'
# And the client that did not ask for new layouts must never have been sent
# one, in that notification or any other.
grep -F '{"V":2,' "$OLDOUT" >/dev/null 2>&1 &&
fail 'Control client without new-layouts was sent a v2 layout'
if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then
echo "Server died." >&2
exit 1