diff --git a/cmd-select-layout.c b/cmd-select-layout.c index 86effb03c..95f60e6ca 100644 --- a/cmd-select-layout.c +++ b/cmd-select-layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-select-layout.c,v 1.44 2026/09/08 10:20:08 nicm Exp $ */ +/* $OpenBSD: cmd-select-layout.c,v 1.45 2026/09/09 07:03:39 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -73,12 +73,13 @@ cmd_select_layout_exec(struct cmd *self, struct cmdq_item *item) { struct args *args = cmd_get_args(self); struct cmd_find_state *target = cmdq_get_target(item); + struct client *c = cmdq_get_target_client(item); struct winlink *wl = target->wl; struct window *w = wl->window; struct window_pane *wp = target->wp; const char *layoutname; - char *oldlayout, *cause; - int next, previous, layout; + char *oldlayout, *cause = NULL; + int next, previous, layout, flags = 0; server_unzoom_window(w); @@ -89,8 +90,12 @@ cmd_select_layout_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'p')) previous = 1; + if (c != NULL && + (c->flags & CLIENT_CONTROL) && + (~c->flags & CLIENT_CONTROL_NEWLAYOUTS)) + flags |= LAYOUT_CUSTOM_OLD_FORMAT; oldlayout = w->old_layout; - w->old_layout = layout_dump(w, w->layout_root, 0); + w->old_layout = layout_dump(w, w->layout_root, flags); if (next || previous) { if (next) diff --git a/layout-custom.c b/layout-custom.c index 3fa30347e..7f6da285c 100644 --- a/layout-custom.c +++ b/layout-custom.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout-custom.c,v 1.39 2026/09/08 10:20:08 nicm Exp $ */ +/* $OpenBSD: layout-custom.c,v 1.40 2026/09/09 07:03:39 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott @@ -19,18 +19,238 @@ #include #include +#include +#include +#include +#include #include #include "tmux.h" +/* + * Layouts can be represented as strings in a JSON format (v2). The legacy + * format (v1) will be removed in the future and should no longer be used. + * + * The current (v2) format is JSON. The top level has two keys: + * "V": version number, currently 2 + * "L": root layout cell + * + * Each cell is an object with: + * "t": cell type: + * "h": horizontal + * "v": vertical + * "p": pane + * "w": cell width + * "h": cell height + * "x": horizontal position + * "y": vertical position + * + * If the cell is a node cell (with child cells), it additionally has: + * "c": array of child cells + * + * If the cell is a leaf cell (that is, containing a pane and no child cells), + * it additionally has: + * "I": pane ID as %n (currently ignored on parse) + * "l": index into last panes list if visited and not the active pane + * "a": true if the active pane + * "i": pane index + * "z": z-index, if a floating pane + */ + +/* Layout string. */ +struct layout_string { + char *dat; + size_t size; /* length written, not including terminator */ + size_t capacity; /* bytes allocated */ +}; + +/* Layout parse cell context. */ +struct layout_parse_cell_ctx { + struct layout_cell *lc; + int active; + int last; + int index; + int zindex; +}; + +/* Layout parse context. */ +struct layout_parse_ctx { + int64_t version; + int num_active; + struct layout_cell *root; + char **cause; + + int size; /* number used */ + int capacity; /* number allocated */ + struct layout_parse_cell_ctx *cctxs; +}; + static struct layout_cell *layout_find_bottomright(struct layout_cell *); static u_short layout_checksum(const char *); -static int layout_append(struct layout_cell *, char *, - size_t); -static int layout_construct(struct layout_cell *, - const char **, struct layout_cell **); -static void layout_assign(struct window_pane **, - struct layout_cell *, int); +static int layout_append(struct layout_cell *, + struct layout_string *, int); +static int layout_construct(const char *, + struct layout_parse_ctx *); +static void layout_assign(struct window *, + struct layout_parse_ctx *); +static void layout_parse_apply_ctx(struct window *, + struct layout_parse_ctx *); +static struct layout_cell *layout_parse_json_layout(struct json_node *, + struct layout_cell *, + struct layout_parse_ctx *); +static int layout_parse_ctx_check_indexes( + struct layout_parse_ctx *); + +/* 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; + int retval = 0; + + if (cca->index < ccb->index) + retval = -1; + if (cca->index > ccb->index) + retval = 1; + return (retval); +} + +/* Compare cell contexts in descending order of z-index. */ +static int +layout_parse_zindex_cmp(const void *a, const void *b) +{ + const struct layout_parse_cell_ctx *cca = a; + const struct layout_parse_cell_ctx *ccb = b; + int retval = 0; + + if (cca->zindex > ccb->zindex) + retval = -1; + if (cca->zindex < ccb->zindex) + retval = 1; + return (retval); +} + +/* Compare cell contexts in descending order of last. */ +static int +layout_parse_last_cmp(const void *a, const void *b) +{ + const struct layout_parse_cell_ctx *cca = a; + const struct layout_parse_cell_ctx *ccb = b; + int retval = 0; + + if (cca->last > ccb->last) + retval = -1; + if (cca->last < ccb->last) + retval = 1; + return (retval); +} + +/* Initialize a layout string. */ +static void +layout_string_init(struct layout_string *ls) +{ + ls->capacity = 1024; + ls->dat = xmalloc(ls->capacity); + ls->dat[0] = '\0'; + ls->size = 0; +} + +/* Free a layout string. */ +static void +layout_string_free(struct layout_string *ls) +{ + free(ls->dat); + ls->dat = NULL; + ls->size = 0; + ls->capacity = 0; +} + +/* Write an optionally formatted string to the end of the layout string. */ +static void printflike(2, 3) +layout_string_write(struct layout_string *ls, const char *fmt, ...) +{ + va_list ap; + char *s; + int slen; + + va_start(ap, fmt); + slen = xvasprintf(&s, fmt, ap); + va_end(ap); + + while (ls->size + slen + 1 > ls->capacity) { + ls->dat = xreallocarray(ls->dat, 2, ls->capacity); + ls->capacity *= 2; + } + memcpy(ls->dat + ls->size, s, slen); + ls->size += slen; + ls->dat[ls->size] = '\0'; + + free(s); +} + +/* Initialize a parse context. */ +static void +layout_parse_init_ctx(struct layout_parse_ctx *pctx, char **cause) +{ + pctx->version = -1; + pctx->num_active = 0; + pctx->root = NULL; + pctx->cause = cause; + pctx->size = 0; + pctx->capacity = 64; + pctx->cctxs = xcalloc(pctx->capacity, sizeof *pctx->cctxs); +} + +/* Free a parse context. */ +static void +layout_parse_free_ctx(struct layout_parse_ctx *pctx) +{ + layout_free_cell(pctx->root, 0); + pctx->root = NULL; + free(pctx->cctxs); + pctx->cctxs = NULL; + pctx->size = 0; + pctx->capacity = 0; +} + +/* Add a cell context to the parse context. */ +static void +layout_parse_add_cctx(struct layout_parse_ctx *pctx, struct layout_cell *lc, + int active, int last, int index, int zindex) +{ + struct layout_parse_cell_ctx *cctx; + + if (pctx->size >= pctx->capacity) { + pctx->capacity *= 2; + pctx->cctxs = xreallocarray(pctx->cctxs, pctx->capacity, + sizeof *pctx->cctxs); + } + cctx = &pctx->cctxs[pctx->size++]; + + cctx->lc = lc; + cctx->active = active; + cctx->last = last; + cctx->index = index; + cctx->zindex = zindex; +} + +/* Remove a cell context from the parse context. Does not preserve ordering. */ +static int +layout_parse_remove_cctx(struct layout_parse_ctx *pctx, struct layout_cell *lc) +{ + struct layout_parse_cell_ctx *cctx; + int i; + + for (i = 0; i < pctx->size; i++) { + if (lc == pctx->cctxs[i].lc) { + cctx = &pctx->cctxs[--pctx->size]; + memmove(&pctx->cctxs[i], cctx, sizeof *cctx); + return (0); + } + } + return (-1); +} /* Find the bottom-right cell. */ static struct layout_cell * @@ -58,73 +278,121 @@ layout_checksum(const char *layout) /* Dump layout as a string. */ char * -layout_dump(struct window *w, struct layout_cell *root, __unused int flags) +layout_dump(__unused struct window *w, struct layout_cell *lcroot, int flags) { - char layout[8192], *out; - int bracket = 0; - struct window_pane *wp; + struct layout_string layout_string; + char *out = NULL; - *layout = '\0'; - if (layout_append(root, layout, sizeof layout) != 0) - return (NULL); + if (lcroot == NULL) + return NULL; - TAILQ_FOREACH(wp, &w->z_index, zentry) { - if (!window_pane_is_floating(wp)) - break; - if (!bracket) { - strlcat(layout, "<", sizeof layout); - bracket = 1; - } - if (layout_append(wp->layout_cell, layout, sizeof layout) != 0) - return (NULL); - strlcat(layout, ",", sizeof layout); + layout_string_init(&layout_string); + + if (layout_append(lcroot, &layout_string, flags) == 0) { + if (flags & LAYOUT_CUSTOM_OLD_FORMAT) + xasprintf(&out, "%04hx,%s", + layout_checksum(layout_string.dat), + layout_string.dat); + else + xasprintf(&out, "{\"V\":2,\"L\":%s}", + layout_string.dat); } - if (bracket) - layout[strlen(layout) - 1] = '>'; + layout_string_free(&layout_string); - xasprintf(&out, "%04hx,%s", layout_checksum(layout), layout); return (out); } -/* Append information for a single cell. */ +/* Append information for a single cell in a JSON (v2) format. */ static int -layout_append(struct layout_cell *lc, char *buf, size_t len) +layout_append_v2(struct layout_cell *lc, struct layout_string *ls) { - struct layout_cell *lcchild; - char tmp[64]; - size_t tmplen; - const char *brackets = "]["; + struct layout_cell *lcchild; + struct window_pane *wp; + enum layout_type type; + char c; + u_int i, n; - if (len == 0) - return (-1); if (lc == NULL) - return (0); - if (lc->wp != NULL) { - tmplen = xsnprintf(tmp, sizeof tmp, "%ux%u,%d,%d,%u", - lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff, lc->wp->id); - } else { - tmplen = xsnprintf(tmp, sizeof tmp, "%ux%u,%d,%d", - lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff); - } - if (tmplen > (sizeof tmp) - 1) - return (-1); - if (strlcat(buf, tmp, len) >= len) return (-1); + type = lc->type; + if (type == LAYOUT_TOPBOTTOM) + c = 'v'; + else if (type == LAYOUT_LEFTRIGHT) + c = 'h'; + else if (type == LAYOUT_WINDOWPANE) + c = 'p'; + else + return (-1); + + layout_string_write(ls, "{\"t\":\"%c\",\"w\":%u,\"h\":%u,\"x\":%d" + ",\"y\":%d", c, lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff); + if (type != LAYOUT_WINDOWPANE) { + layout_string_write(ls, ",\"c\":["); + n = 0; + TAILQ_FOREACH(lcchild, &lc->cells, entry) { + if (layout_append_v2(lcchild, ls) != 0) + return (-1); + layout_string_write(ls, ","); + n++; + } + if (n == 0) + return (-1); + ls->dat[--ls->size] = '\0'; /* removing trailing comma */ + layout_string_write(ls, "]"); + } else { + wp = lc->wp; + if (wp == NULL) + return (-1); + if (wp == wp->window->active) + layout_string_write(ls, ",\"a\":true"); + else if (window_pane_last_index(wp, &i) == 0) + layout_string_write(ls, ",\"l\":%u", i); + if (window_pane_index(wp, &i) != 0) + return (-1); + layout_string_write(ls, ",\"i\":%u", i); + if ((lc->flags & LAYOUT_CELL_FLOATING) && + window_pane_zindex(wp, &i) == 0) + layout_string_write(ls, ",\"z\":%u", i); + layout_string_write(ls, ",\"I\":\"%%%u\"", wp->id); + } + + layout_string_write(ls, "}"); + + return (0); +} + +/* Append information for a single cell in the legacy (v1) format. */ +static int +layout_append_v1(struct layout_cell *lc, struct layout_string *ls) +{ + struct layout_cell *lcchild; + const char *brackets = "[]"; + + if (lc == NULL) + return (-1); + + if (lc->wp != NULL) { + layout_string_write(ls, "%ux%u,%d,%d,%u", lc->g.sx, lc->g.sy, + lc->g.xoff, lc->g.yoff, lc->wp->id); + } else { + layout_string_write(ls, "%ux%u,%d,%d", lc->g.sx, lc->g.sy, + lc->g.xoff, lc->g.yoff); + } switch (lc->type) { case LAYOUT_LEFTRIGHT: - brackets = "}{"; + brackets = "{}"; /* FALLTHROUGH */ case LAYOUT_TOPBOTTOM: - if (strlcat(buf, &brackets[1], len) >= len) - return (-1); + layout_string_write(ls, "%c", brackets[0]); TAILQ_FOREACH(lcchild, &lc->cells, entry) { - if (layout_append(lcchild, buf, len) != 0) - return (-1); - if (strlcat(buf, ",", len) >= len) + if (layout_append_v1(lcchild, ls) != 0) return (-1); + layout_string_write(ls, ","); } - buf[strlen(buf) - 1] = brackets[0]; + + ls->dat[--ls->size] = '\0'; /* removing trailing comma */ + layout_string_write(ls, "%c", brackets[1]); break; case LAYOUT_WINDOWPANE: break; @@ -133,6 +401,121 @@ layout_append(struct layout_cell *lc, char *buf, size_t len) return (0); } +/* + * Copies the tiled part of a layout. Only populates what is necessary to dump a + * V1 layout string. + */ +static struct layout_cell * +layout_custom_copy_layout(struct layout_cell *lc) +{ + struct layout_cell *lcchild, *lcnewchild, *lconly; + struct layout_cell *lcnew; + + if (lc->type == LAYOUT_WINDOWPANE && + (lc->flags & LAYOUT_CELL_FLOATING)) + return (NULL); + + lcnew = layout_create_cell(NULL); + + lcnew->type = lc->type; + lcnew->flags = lc->flags; + if (lc->type == LAYOUT_WINDOWPANE) + lcnew->wp = lc->wp; + layout_set_size(lcnew, lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff); + + switch (lc->type) { + case LAYOUT_WINDOWPANE: + break; + case LAYOUT_TOPBOTTOM: + case LAYOUT_LEFTRIGHT: + TAILQ_FOREACH(lcchild, &lc->cells, entry) { + lcnewchild = layout_custom_copy_layout(lcchild); + if (lcnewchild == NULL) + continue; + TAILQ_INSERT_TAIL(&lcnew->cells, lcnewchild, entry); + lcnewchild->parent = lcnew; + } + + lconly = TAILQ_FIRST(&lcnew->cells); + if (lconly == NULL) { + layout_free_cell(lcnew, 0); + return (NULL); + } + if (TAILQ_NEXT(lconly, entry) == NULL) { + TAILQ_REMOVE(&lcnew->cells, lconly, entry); + lconly->parent = NULL; + layout_free_cell(lcnew, 0); + return (lconly); + } + break; + } + + return (lcnew); +} + +/* Create a compatibility layout for dumping a V1 layout string. */ +static struct layout_cell * +layout_custom_create_compat(struct layout_cell *lcroot) +{ + struct layout_cell *lccompat; + + lccompat = layout_custom_copy_layout(lcroot); + if (lccompat != NULL && layout_cell_is_tiled(lccompat)) { + lccompat->g.xoff = 0; + lccompat->g.yoff = 0; + } + + return (lccompat); +} + +/* Unlinks all panes from the given layout. */ +static void +layout_custom_unlink_panes(struct layout_cell *lc) +{ + struct layout_cell *lcchild; + + switch (lc->type) { + case LAYOUT_WINDOWPANE: + lc->wp = NULL; + break; + case LAYOUT_LEFTRIGHT: + case LAYOUT_TOPBOTTOM: + TAILQ_FOREACH(lcchild, &lc->cells, entry) + layout_custom_unlink_panes(lcchild); + break; + } +} + +/* Frees the compatibility layout. */ +static void +layout_custom_free_compat(struct layout_cell *lcroot) +{ + if (lcroot == NULL) + return; + layout_custom_unlink_panes(lcroot); + layout_free_cell(lcroot, 0); +} + +/* Dispatch to append the appropriate version. */ +static int +layout_append(struct layout_cell *lcroot, struct layout_string *ls, int flags) +{ + struct layout_cell *lccompat; + int result; + + if (flags & LAYOUT_CUSTOM_OLD_FORMAT) { + if (!layout_cell_is_tiled(lcroot) && + !layout_cell_has_tiled_child(lcroot)) + return (-1); + lccompat = layout_custom_create_compat(lcroot); + result = layout_append_v1(lccompat, ls); + layout_custom_free_compat(lccompat); + } else + result = layout_append_v2(lcroot, ls); + + return (result); +} + /* Check layout sizes fit. */ static int layout_check(struct layout_cell *lc) @@ -145,24 +528,30 @@ layout_check(struct layout_cell *lc) break; case LAYOUT_LEFTRIGHT: TAILQ_FOREACH(lcchild, &lc->cells, entry) { + if (!layout_cell_is_tiled(lcchild) && + !layout_cell_has_tiled_child(lcchild)) + continue; if (lcchild->g.sy != lc->g.sy) return (0); if (!layout_check(lcchild)) return (0); n += lcchild->g.sx + 1; } - if (n - 1 != lc->g.sx) + if (n != 0 && n - 1 != lc->g.sx) return (0); break; case LAYOUT_TOPBOTTOM: TAILQ_FOREACH(lcchild, &lc->cells, entry) { + if (!layout_cell_is_tiled(lcchild) && + !layout_cell_has_tiled_child(lcchild)) + continue; if (lcchild->g.sx != lc->g.sx) return (0); if (!layout_check(lcchild)) return (0); n += lcchild->g.sy + 1; } - if (n - 1 != lc->g.sy) + if (n != 0 && n - 1 != lc->g.sy) return (0); break; } @@ -171,45 +560,30 @@ layout_check(struct layout_cell *lc) /* Parse a layout string and arrange window as layout. */ int -layout_parse(struct window *w, const char *layout, char **cause) +layout_parse(struct window *w, const char *input, char **cause) { - struct layout_cell *lcchild, *tiled_lc = NULL; struct window_pane *wp; + struct layout_cell *lcchild, *lc = NULL; + struct layout_parse_ctx pctx; u_int npanes, ncells, sx = 0, sy = 0; - u_short csum; - int n = 0; - - /* Check validity. */ - if (sscanf(layout, "%hx,%n", &csum, &n) != 1 || n != 5) { - *cause = xstrdup("invalid layout"); - return (-1); - } - layout += n; - if (csum != layout_checksum(layout)) { - *cause = xstrdup("invalid layout"); - return (-1); - } + int with_floating; /* Build the layout. */ - if (layout_construct(NULL, &layout, &tiled_lc) != 0) { - *cause = xstrdup("invalid layout"); + layout_parse_init_ctx(&pctx, cause); + if (layout_construct(input, &pctx) != 0) { + layout_parse_free_ctx(&pctx); return (-1); } - if (tiled_lc == NULL) { - /* A stub layout cell for an empty window. */ - tiled_lc = layout_create_cell(NULL); - tiled_lc->type = LAYOUT_LEFTRIGHT; - layout_set_size(tiled_lc, w->sx, w->sy, 0, 0); - } - if (*layout != '\0') { - *cause = xstrdup("invalid layout"); - goto fail; - } + with_floating = pctx.version > 1; /* Check this window will fit into the layout. */ - npanes = window_count_panes(w, 1); + npanes = window_count_panes(w, with_floating); + if (npanes == 0) { + xasprintf(cause, "window @%u has no panes", w->id); + goto fail; + } for (;;) { - ncells = layout_count_cells(tiled_lc); + ncells = layout_count_cells(pctx.root, with_floating); if (npanes > ncells) { xasprintf(cause, "have %u panes but need %u", npanes, ncells); @@ -222,82 +596,127 @@ layout_parse(struct window *w, const char *layout, char **cause) * Fewer panes than cells, close the bottom right until none * remain. */ - lcchild = layout_find_bottomright(tiled_lc); - layout_destroy_cell(w, lcchild, &tiled_lc); + lcchild = layout_find_bottomright(pctx.root); + if (pctx.version > 1 && layout_parse_remove_cctx(&pctx, + lcchild) != 0) { + *cause = xstrdup("empty/missing layout parse context"); + goto fail; + } + layout_destroy_cell(NULL, lcchild, &pctx.root); } + /* The root is now owned by lc. */ + lc = pctx.root; + pctx.root = NULL; + /* * It appears older versions of tmux were able to generate layouts with * an incorrect top cell size - if it is larger than the top child then * correct that (if this is still wrong the check code will catch it). */ - - switch (tiled_lc->type) { + switch (lc->type) { case LAYOUT_WINDOWPANE: break; case LAYOUT_LEFTRIGHT: - TAILQ_FOREACH(lcchild, &tiled_lc->cells, entry) { - sy = lcchild->g.sy + 1; - sx += lcchild->g.sx + 1; + TAILQ_FOREACH(lcchild, &lc->cells, entry) { + if (layout_cell_is_tiled(lcchild) || + layout_cell_has_tiled_child(lcchild)) { + sy = lcchild->g.sy + 1; + sx += lcchild->g.sx + 1; + } } break; case LAYOUT_TOPBOTTOM: - TAILQ_FOREACH(lcchild, &tiled_lc->cells, entry) { - sx = lcchild->g.sx + 1; - sy += lcchild->g.sy + 1; + TAILQ_FOREACH(lcchild, &lc->cells, entry) { + if (layout_cell_is_tiled(lcchild) || + layout_cell_has_tiled_child(lcchild)) { + sx = lcchild->g.sx + 1; + sy += lcchild->g.sy + 1; + } } break; } - if (tiled_lc->type != LAYOUT_WINDOWPANE && - (tiled_lc->g.sx != sx || tiled_lc->g.sy != sy)) { - layout_print_cell(tiled_lc, __func__, 0); - tiled_lc->g.sx = sx - 1; tiled_lc->g.sy = sy - 1; + if (lc->type != LAYOUT_WINDOWPANE && sx != 0 && sy != 0 && + (lc->g.sx != sx || lc->g.sy != sy)) { + layout_print_cell(lc, __func__, 0); + lc->g.sx = sx - 1; lc->g.sy = sy - 1; } /* Check the new layout. */ - if (!layout_check(tiled_lc)) { + if (!layout_check(lc)) { *cause = xstrdup("size mismatch after applying layout"); goto fail; } /* Resize window to the layout size. */ - if (sx != 0 && sy != 0) - window_resize(w, tiled_lc->g.sx, tiled_lc->g.sy, -1, -1); + if (layout_cell_is_tiled(lc) || + layout_cell_has_tiled_child(lc)) + window_resize(w, lc->g.sx, lc->g.sy, -1, -1); + + /* Preserve floating panes for version 1. */ + if (pctx.version == 1) { + TAILQ_FOREACH(wp, &w->panes, entry) { + if (!window_pane_is_floating(wp)) + continue; + lcchild = wp->layout_cell; + TAILQ_REMOVE(&lcchild->parent->cells, lcchild, entry); + lcchild->parent = NULL; + } + } /* Destroy the old layout and swap to the new. */ layout_free_cell(w->layout_root, 0); - w->layout_root = tiled_lc; + w->layout_root = lc; /* Assign the panes into the cells. */ - wp = TAILQ_FIRST(&w->panes); - if (tiled_lc != NULL) - layout_assign(&wp, tiled_lc, 0); + layout_assign(w, &pctx); - /* Fix pane z-indexes. */ - while (!TAILQ_EMPTY(&w->z_index)) { - wp = TAILQ_FIRST(&w->z_index); - TAILQ_REMOVE(&w->z_index, wp, zentry); - } - layout_fix_zindexes(w, tiled_lc); - - /* Update pane offsets and sizes. */ + /* Update pane attributes. */ layout_fix_offsets(w); layout_fix_panes(w, NULL); + if (pctx.version > 1) + layout_parse_apply_ctx(w, &pctx); recalculate_sizes(); - layout_print_cell(tiled_lc, __func__, 0); + layout_print_cell(lc, __func__, 0); - events_fire_window("window-layout-changed", w); + /* Backwards compatibility. */ + if (pctx.version == 1) + events_fire_window("window-layout-changed", w); + layout_parse_free_ctx(&pctx); return (0); fail: - layout_free_cell(tiled_lc, 0); + layout_free_cell(lc, 0); + layout_parse_free_ctx(&pctx); return (-1); } -/* Assign panes into cells. */ +/* Assign panes into cells from the cell contexts. */ static void -layout_assign(struct window_pane **wp, struct layout_cell *lc, int flags) +layout_assign_from_ctx(struct window *w, struct layout_parse_ctx *pctx) +{ + struct layout_cell *lc; + struct window_pane *wp; + int i; + + qsort(pctx->cctxs, pctx->size, sizeof pctx->cctxs[0], + layout_parse_index_cmp); + + wp = TAILQ_FIRST(&w->panes); + for (i = 0; i < pctx->size; i++) { + lc = pctx->cctxs[i].lc; + layout_make_leaf(lc, wp); + wp = TAILQ_NEXT(wp, entry); + } +} + +/* + * Assign tiled cells to available panes. Panes that already have a cell are + * floating and are skipped over. + */ +static void +layout_assign_fallback_tiled(struct window_pane **wp, struct layout_cell *lc) { struct layout_cell *lcchild; @@ -306,18 +725,60 @@ layout_assign(struct window_pane **wp, struct layout_cell *lc, int flags) switch (lc->type) { case LAYOUT_WINDOWPANE: + while (*wp != NULL && (*wp)->layout_cell != NULL) + *wp = TAILQ_NEXT(*wp, entry); + if (*wp == NULL) + return; layout_make_leaf(lc, *wp); - lc->flags |= flags; *wp = TAILQ_NEXT(*wp, entry); return; case LAYOUT_LEFTRIGHT: case LAYOUT_TOPBOTTOM: - TAILQ_FOREACH(lcchild, &lc->cells, entry) - layout_assign(wp, lcchild, flags); + TAILQ_FOREACH(lcchild, &lc->cells, entry) { + layout_assign_fallback_tiled(wp, lcchild); + } return; } } +/* + * Assign panes into cells when there are no cell contexts. This will be removed + * when the non-JSON format is deprecated. + */ +static void +layout_assign_fallback(struct window *w, struct layout_cell *lcroot) +{ + struct window_pane *wp = TAILQ_FIRST(&w->panes); + struct layout_cell *lc; + + layout_assign_fallback_tiled(&wp, lcroot); + + if (window_count_panes(w, 1) > 1 && + lcroot->type == LAYOUT_WINDOWPANE) + lcroot = layout_replace_with_node(w, lcroot, LAYOUT_TOPBOTTOM); + + wp = TAILQ_FIRST(&w->panes); + while (wp != NULL) { + if (window_pane_is_floating(wp)) { + lc = wp->layout_cell; + lc->parent = lcroot; + TAILQ_INSERT_TAIL(&lcroot->cells, lc, entry); + } + wp = TAILQ_NEXT(wp, entry); + } +} + +/* Assign panes into cells. Number of cells must match the number of panes. */ +static void +layout_assign(struct window *w, struct layout_parse_ctx *pctx) +{ + if (pctx->size > 0) + layout_assign_from_ctx(w, pctx); + else + layout_assign_fallback(w, w->layout_root); +} + +/* Construct a cell from the legacy (v1) format. */ static struct layout_cell * layout_construct_cell(struct layout_cell *lcparent, const char **layout) { @@ -366,34 +827,27 @@ layout_construct_cell(struct layout_cell *lcparent, const char **layout) return (lc); } -/* - * Given a character string layout, recursively construct cells. - * Possible return values: - * lc LAYOUT_WINDOWPANE, no children - * lc LAYOUT_LEFTRIGHT or LAYOUT_TOPBOTTOM, with children - */ -static int -layout_construct(struct layout_cell *lcparent, const char **layout, - struct layout_cell **lc) +/* Construct a layout from the legacy (v1) format. */ +static struct layout_cell * +layout_construct_v1(struct layout_cell *lcparent, const char **layout) { - struct layout_cell *lcchild; + struct layout_cell *lc, *lcchild; - *lc = layout_construct_cell(lcparent, layout); - if (*lc == NULL) - return (-1); + lc = layout_construct_cell(lcparent, layout); + if (lc == NULL) + return (NULL); switch (**layout) { case ',': case '}': case ']': - case '>': case '\0': - return (0); + return (lc); case '{': - (*lc)->type = LAYOUT_LEFTRIGHT; + lc->type = LAYOUT_LEFTRIGHT; break; case '[': - (*lc)->type = LAYOUT_TOPBOTTOM; + lc->type = LAYOUT_TOPBOTTOM; break; default: goto fail; @@ -401,12 +855,13 @@ layout_construct(struct layout_cell *lcparent, const char **layout, do { (*layout)++; - if (layout_construct(*lc, layout, &lcchild) != 0) + lcchild = layout_construct_v1(lc, layout); + if (lcchild == NULL) goto fail; - TAILQ_INSERT_TAIL(&(*lc)->cells, lcchild, entry); + TAILQ_INSERT_TAIL(&lc->cells, lcchild, entry); } while (**layout == ','); - switch ((*lc)->type) { + switch (lc->type) { case LAYOUT_LEFTRIGHT: if (**layout != '}') goto fail; @@ -420,9 +875,337 @@ layout_construct(struct layout_cell *lcparent, const char **layout, } (*layout)++; + return (lc); + +fail: + layout_free_cell(lc, 0); + return (NULL); +} + +/* + * Evaluate parsed JSON. Check metadata at the top level and return the new + * layout root. Consumes json input. + */ +static int +layout_parse_json(struct json_node *jnroot, struct layout_parse_ctx *pctx) +{ + struct json_node *jn, *object; + int64_t num; + char **cause = pctx->cause; + + if (json_get_object(jnroot, &jn) != 0) { + *cause = xstrdup("invalid layout json"); + goto fail; + } + + if (json_find_number(jn, "V", &num, cause) != 0) + goto fail; + pctx->version = num; + + if (json_find_object(jn, "L", &object, cause) != 0) + goto fail; + pctx->root = layout_parse_json_layout(object, NULL, pctx); + if (pctx->root == NULL) + goto fail; + + json_destroy_node(jnroot); + return (0); fail: - layout_free_cell(*lc, 0); + json_destroy_node(jnroot); + if (pctx->root != NULL) + layout_free_cell(pctx->root, 0); + pctx->root = NULL; return (-1); } + +/* Parse nodes into layout cells. */ +static struct layout_cell * +layout_parse_json_layout(struct json_node *node, struct layout_cell *lcparent, + struct layout_parse_ctx *pctx) +{ + struct json_node *member, *array; + struct layout_cell *lc = layout_create_cell(lcparent), *lcchild; + const char *str; + int64_t num; + char **cause = pctx->cause; + int boolean, index, zindex, active = -1; + int last = -1; + + if (json_find_string(node, "t", &str, cause) != 0) + goto fail; + if (strcmp(str, "p") == 0) + lc->type = LAYOUT_WINDOWPANE; + else if (strcmp(str, "v") == 0) + lc->type = LAYOUT_TOPBOTTOM; + else if (strcmp(str, "h") == 0) + lc->type = LAYOUT_LEFTRIGHT; + else { + xasprintf(cause, "unknown cell type \"%s\"", str); + goto fail; + } + + if (json_find_number(node, "w", &num, cause) != 0) + goto fail; + if (num < PANE_MINIMUM || num > PANE_MAXIMUM) { + xasprintf(cause, "invalid width %lld", (long long)num); + goto fail; + } + lc->g.sx = num; + + if (json_find_number(node, "h", &num, cause) != 0) + goto fail; + if (num < PANE_MINIMUM || num > PANE_MAXIMUM) { + xasprintf(cause, "invalid height %lld", (long long)num); + goto fail; + } + lc->g.sy = num; + + if (json_find_number(node, "x", &num, cause) != 0) + goto fail; + if (num < -WINDOW_MAXIMUM || num > WINDOW_MAXIMUM) { + xasprintf(cause, "invalid x-offset %lld", (long long)num); + goto fail; + } + lc->g.xoff = num; + + if (json_find_number(node, "y", &num, cause) != 0) + goto fail; + if (num < -WINDOW_MAXIMUM || num > WINDOW_MAXIMUM) { + xasprintf(cause, "invalid y-offset %lld", (long long)num); + goto fail; + } + lc->g.yoff = num; + + if (lc->type == LAYOUT_WINDOWPANE) { /* "I" is currently ignored */ + if (json_find(node, "c") != NULL) { + *cause = xstrdup("panes cannot have children"); + goto fail; + } + if (json_find_number(node, "i", &num, cause) != 0) + goto fail; + if (num < 0 || num > INT_MAX) { + xasprintf(cause, "invalid index %lld", (long long)num); + goto fail; + } + index = num; + + if (json_find(node, "a") != NULL) { + if (json_find_boolean(node, "a", &boolean, cause) != 0) + goto fail; + active = boolean; + if (active) + pctx->num_active++; + } else if (json_find(node, "l") != NULL) { + if (json_find_number(node, "l", &num, cause) != 0) + goto fail; + if (num < 0 || num > INT_MAX) { + xasprintf(cause, "invalid last %lld", + (long long)num); + goto fail; + } + last = num; + } + + if (json_find(node, "z") != NULL) { + if (json_find_number(node, "z", &num, cause) != 0) + goto fail; + if (num < 0 || num > INT_MAX - 1) { + xasprintf(cause, "invalid floating zindex %lld", + (long long)num); + goto fail; + } + zindex = num; + lc->flags |= LAYOUT_CELL_FLOATING; + } else + zindex = INT_MAX; + + layout_parse_add_cctx(pctx, lc, active, last, index, zindex); + } else { + if (json_find_array(node, "c", &array, cause) != 0) + goto fail; + if ((member = json_array_first(array)) == NULL || + json_array_next(member) == NULL) { + *cause = xstrdup("nodes must have more than one child"); + goto fail; + } + while (member != NULL) { + lcchild = layout_parse_json_layout(member, lc, + pctx); + if (lcchild == NULL) + goto fail; + TAILQ_INSERT_TAIL(&lc->cells, lcchild, entry); + member = json_array_next(member); + } + } + + return (lc); + +fail: + layout_free_cell(lc, 0); + return (NULL); +} + +/* Construct a layout root from a formatted string. */ +static int +layout_construct(const char *input, struct layout_parse_ctx *pctx) +{ + struct json_node *json; + u_short csum; + int n = 0; + + while (isspace((u_char) *input)) + input++; + + if (*input != '{') { /* sniffing version */ + if (sscanf(input, "%hx,%n", &csum, &n) != 1 || n != 5) { + *pctx->cause = xstrdup("malformed layout header"); + return (-1); + } + input += n; + if (csum != layout_checksum(input)) { + *pctx->cause = xstrdup("invalid layout checksum"); + return (-1); + } + if ((pctx->root = layout_construct_v1(NULL, &input)) == NULL) { + *pctx->cause = xstrdup("invalid layout"); + return (-1); + } + if (*input != '\0') { + *pctx->cause = xstrdup("trailing data"); + return (-1); + } + pctx->version = 1; + } else { + if ((json = json_parse(input, pctx->cause)) == NULL) + return (-1); + + if (layout_parse_json(json, pctx) != 0) + return (-1); + + if (pctx->version != 2) { + *pctx->cause = xstrdup("version mismatch"); + return (-1); + } + if (pctx->num_active > 1) { + *pctx->cause = xstrdup("more than one active pane"); + return (-1); + } + if (pctx->size == 0) { + *pctx->cause = xstrdup("no panes"); + return (-1); + } + if (!layout_parse_ctx_check_indexes(pctx)) + return (-1); + } + + return (0); +} + +/* Apply the remaining context to the layout. */ +static void +layout_parse_apply_ctx(struct window *w, struct layout_parse_ctx *pctx) +{ + struct layout_parse_cell_ctx *cctx; + struct window_pane *wp, *wpnext; + int i; + + /* Apply z-indexes. */ + wp = TAILQ_FIRST(&w->z_index); + while (wp != NULL) { + wpnext = TAILQ_NEXT(wp, zentry); + if (window_pane_is_floating(wp)) + TAILQ_REMOVE(&w->z_index, wp, zentry); + wp = wpnext; + } + + qsort(pctx->cctxs, pctx->size, sizeof pctx->cctxs[0], + layout_parse_zindex_cmp); + + for (i = 0; i < pctx->size; i++) { + cctx = &pctx->cctxs[i]; + wp = cctx->lc->wp; + if (window_pane_is_floating(wp)) + TAILQ_INSERT_HEAD(&w->z_index, wp, zentry); + } + + /* Set the active pane. */ + for (i = 0; i < pctx->size; 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); + window_pane_stack_remove(&w->last_panes, wp); + } + + qsort(pctx->cctxs, pctx->size, sizeof pctx->cctxs[0], + layout_parse_last_cmp); + + for (i = 0; i < pctx->size; i++) { + cctx = &pctx->cctxs[i]; + wp = cctx->lc->wp; + if (cctx->last < 0 || cctx->active == 1) + continue; + window_pane_stack_push(&w->last_panes, wp); + } +} + +/* Checks for duplicate pane indexes, z-indexes, and last indexes. */ +static int +layout_parse_ctx_check_indexes(struct layout_parse_ctx *pctx) +{ + int i, n; + + qsort(pctx->cctxs, pctx->size, sizeof pctx->cctxs[0], + layout_parse_index_cmp); + + for (i = 1; i < pctx->size; i++) { + if (pctx->cctxs[i].index == pctx->cctxs[i - 1].index) { + *pctx->cause = xstrdup("duplicate pane index"); + return (0); + } + } + + qsort(pctx->cctxs, pctx->size, sizeof pctx->cctxs[0], + layout_parse_zindex_cmp); + + /* + * Sorted in descending order, so the panes without a z-index come first + * and the floating panes run to the end. + */ + n = 0; + while (n < pctx->size && pctx->cctxs[n].zindex == INT_MAX) + n++; + for (i = n + 1; i < pctx->size; i++) { + if (pctx->cctxs[i].zindex == pctx->cctxs[i - 1].zindex) { + *pctx->cause = xstrdup("duplicate pane z-index"); + return (0); + } + } + + qsort(pctx->cctxs, pctx->size, sizeof pctx->cctxs[0], + layout_parse_last_cmp); + + /* + * Sorted in descending order, so the panes without a last index come + * last. + */ + n = 0; + while (n < pctx->size && pctx->cctxs[n].last >= 0) + n++; + for (i = 1; i < n; i++) { + if (pctx->cctxs[i].last == pctx->cctxs[i - 1].last) { + *pctx->cause = xstrdup("duplicate last pane index"); + return (0); + } + } + + return (1); +} diff --git a/layout.c b/layout.c index ac87f30e5..21d78880c 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout.c,v 1.98 2026/08/25 18:38:05 nicm Exp $ */ +/* $OpenBSD: layout.c,v 1.99 2026/09/09 07:03:39 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -106,7 +106,7 @@ layout_free_cell(struct layout_cell *lc, int only_nodes) } break; case LAYOUT_WINDOWPANE: - if (lc->wp != NULL) { + if (lc->wp != NULL && lc->wp->layout_cell != NULL) { lc->wp->layout_cell->parent = NULL; lc->wp->layout_cell = NULL; } @@ -233,29 +233,6 @@ layout_make_node(struct layout_cell *lc, enum layout_type type) lc->wp = NULL; } -/* Fix z-indexes. */ -void -layout_fix_zindexes(struct window *w, struct layout_cell *lc) -{ - struct layout_cell *lcchild; - - if (lc == NULL) - return; - - switch (lc->type) { - case LAYOUT_WINDOWPANE: - TAILQ_INSERT_TAIL(&w->z_index, lc->wp, zentry); - break; - case LAYOUT_LEFTRIGHT: - case LAYOUT_TOPBOTTOM: - TAILQ_FOREACH(lcchild, &lc->cells, entry) - layout_fix_zindexes(w, lcchild); - return; - default: - fatalx("bad layout type"); - } -} - int layout_cell_is_tiled(struct layout_cell *lc) { @@ -265,7 +242,7 @@ layout_cell_is_tiled(struct layout_cell *lc) return is_leaf && !is_floating; } -static int +int layout_cell_has_tiled_child(struct layout_cell *lc) { struct layout_cell *lcchild; @@ -509,18 +486,20 @@ layout_fix_panes(struct window *w, struct window_pane *skip) /* Count the number of available cells in a layout. */ u_int -layout_count_cells(struct layout_cell *lc) +layout_count_cells(struct layout_cell *lc, int with_floating) { struct layout_cell *lcchild; u_int count = 0; switch (lc->type) { case LAYOUT_WINDOWPANE: + if (lc->flags & LAYOUT_CELL_FLOATING && !with_floating) + return 0; return (1); case LAYOUT_LEFTRIGHT: case LAYOUT_TOPBOTTOM: TAILQ_FOREACH(lcchild, &lc->cells, entry) - count += layout_count_cells(lcchild); + count += layout_count_cells(lcchild, with_floating); return (count); default: fatalx("bad layout type"); @@ -721,7 +700,7 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc, /* If no parent, this is the last pane in a window. */ lcparent = lc->parent; if (lcparent == NULL) { - if (lc->wp != NULL) + if (*lcroot == lc) *lcroot = NULL; layout_free_cell(lc, 0); return; diff --git a/tmux.1 b/tmux.1 index bd562178b..22780e2ce 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1167 2026/09/08 10:20:08 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1168 2026/09/09 07:03:39 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: September 8 2026 $ +.Dd $Mdocdate: September 9 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -2699,8 +2699,8 @@ For example: .Bd -literal -offset indent $ tmux list\-windows 0: ksh [159x48] - layout: bb62,159x48,0,0{79x48,0,0,79x48,80,0} -$ tmux select\-layout \[aq]bb62,159x48,0,0{79x48,0,0,79x48,80,0}\[aq] + layout: {"V":2,"L":{"t":"h","w":159,"h":48,"x":0,"y":0,"c":[{"t":"p","w":79,"h":48,"x":0,"y":0,"l":0,"i":0,"I":"%0"},{"t":"p","w":79,"h":48,"x":80,"y":0,"a":true,"i":1,"I":"%2"}]}} +$ tmux select\-layout \[aq]{"V":2,"L":{"t":"h","w":159,"h":48,"x":0,"y":0,"c":[{"t":"p","w":79,"h":48,"x":0,"y":0,"l":0,"i":0,"I":"%0"},{"t":"p","w":79,"h":48,"x":80,"y":0,"a":true,"i":1,"I":"%2"}]}}\[aq] .Ed .Pp .Nm @@ -9070,7 +9070,7 @@ and flags (currently not used). For example: .Bd -literal -offset indent %begin 1363006971 2 1 -0: ksh* (1 panes) [80x24] [layout b25f,80x24,0,0,2] @2 (active) +0: ksh* (1 panes) [80x24] [layout {"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"a":true,"i":0,"I":"%2"}}] @2 (active) %end 1363006971 2 1 .Ed .Pp @@ -9131,10 +9131,15 @@ The layout of a window with ID .Ar window\-id changed. The new layout is -.Ar window\-layout . -The window's visible layout is -.Ar window\-visible\-layout -and the window flags are +.Ar window\-layout +and the window's visible layout is +.Ar window\-visible\-layout . +If the +.Ar new\-layouts +flag is set, both layout fields use the new format string; see +.Ic refresh\-client +.Fl f . +The window flags are .Ar window\-flags . .It Ic %message Ar message A message sent with the diff --git a/tmux.h b/tmux.h index ac195fecb..ff2158e02 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1436 2026/09/08 10:20:08 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1437 2026/09/09 07:03:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3800,7 +3800,7 @@ struct visible_ranges *window_visible_ranges(struct window_pane *, int, int, u_int, struct visible_ranges *); /* layout.c */ -u_int layout_count_cells(struct layout_cell *); +u_int layout_count_cells(struct layout_cell *, int); struct layout_cell *layout_create_cell(struct layout_cell *); void layout_free_cell(struct layout_cell *, int); void layout_print_cell(struct layout_cell *, const char *, u_int); @@ -3812,8 +3812,8 @@ struct layout_cell *layout_search_by_border(struct layout_cell *, u_int, u_int); void layout_set_size(struct layout_cell *, u_int, u_int, int, int); void layout_make_leaf(struct layout_cell *, struct window_pane *); void layout_make_node(struct layout_cell *, enum layout_type); -void layout_fix_zindexes(struct window *, struct layout_cell *); int layout_cell_is_tiled(struct layout_cell *); +int layout_cell_has_tiled_child(struct layout_cell *); int layout_add_horizontal_border(struct layout_cell *, struct layout_cell *, int); void layout_fix_offsets(struct window *);