From e550034c1be80b4da2b59a419d7a007f4ab567dd Mon Sep 17 00:00:00 2001 From: Dane Jensen Date: Sat, 25 Jul 2026 14:06:26 -0700 Subject: [PATCH] More bugfixes and test updates. --- layout-custom.c | 104 +++++--- regress/layout-custom.sh | 545 +++++++++++++++++++++++++++++++-------- 2 files changed, 506 insertions(+), 143 deletions(-) diff --git a/layout-custom.c b/layout-custom.c index d309c7f05..449605516 100644 --- a/layout-custom.c +++ b/layout-custom.c @@ -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); } } diff --git a/regress/layout-custom.sh b/regress/layout-custom.sh index 833d74007..96893d0d8 100644 --- a/regress/layout-custom.sh +++ b/regress/layout-custom.sh @@ -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 ". +# 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 ". 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