aboutsummaryrefslogtreecommitdiffstats
path: root/cli/src/core
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-13 21:19:22 +0200
committeruser <user@clank>2026-08-13 21:19:22 +0200
commit94a1fd85e524bbe079b6ce3c8bb17295b706834c (patch)
treecea7cd815ebb15ebac204155b4873a02bd350a52 /cli/src/core
parentsl0p(bar): draw the bar as a framed box, and a single line when collapsed (diff)
downloadsl0ppi-94a1fd85e524bbe079b6ce3c8bb17295b706834c.tar.gz
sl0ppi-94a1fd85e524bbe079b6ce3c8bb17295b706834c.tar.xz
sl0ppi-94a1fd85e524bbe079b6ce3c8bb17295b706834c.zip
cli: carry pane templates into the generated layout
`sl0ppi up` died on every session: × Failed to parse Zellij configuration 43 │ sl0pbar size=3 · ╰── Invalid tab property: sl0pbar The CLI writes its own layout file and inlines the tab template into each tab -- a default_tab_template would destroy the tab purposes that are the CLI's notion of identity -- so the generated file is standalone. The moment the status bar became a pane_template, the inlined body referred to a name only layouts/default.kdl knew about. extractPaneTemplates pulls those definitions out with the same brace scanner that already had to respect strings and comments, and both generated layouts emit them once at the top. The result is smaller than before, too: one copy of the bar's configuration per session instead of one per tab. packaging/checks/sl0ppi-up-live.py runs the real thing -- CLI, binary, session, attached client -- because the CLI's own tests are string assertions and were all green while this was broken. It fails with exactly the user's error on the parent commit. Not done, deliberately, and the check says why in a comment: copying the user's default.swap.kdl next to the generated layout to make the bar collapse on a narrow terminal. It appears to work, and then auto_layout swaps the tab to one of the other swap layouts, whose bare `tab` nodes expect a default_tab_template that a generated layout does not have -- and the bar disappears entirely. Measured on screen both ways, and the check now pins "the bar survives a rearrangement" so nobody re-derives it from JSON. Which is the other lesson: `action list-panes` stops reporting the borderless plugin pane once a client is attached. It had me chasing a bar that was on screen the whole time. dump-layout and the screen agree; list-panes does not.
Diffstat (limited to 'cli/src/core')
-rw-r--r--cli/src/core/layout.ts145
1 files changed, 103 insertions, 42 deletions
diff --git a/cli/src/core/layout.ts b/cli/src/core/layout.ts
index ec5f0da..6c2319e 100644
--- a/cli/src/core/layout.ts
+++ b/cli/src/core/layout.ts
@@ -35,57 +35,101 @@ export function kdlString(value: string): string {
* middle and produces a layout that will not parse.
*/
export function extractDefaultTabTemplate(layoutKdl: string): string | undefined {
- const marker = /(^|\n)\s*default_tab_template\s*\{/.exec(layoutKdl);
- if (!marker) return undefined;
+ const found = extractBlocks(layoutKdl, /(^|\n)[ \t]*default_tab_template[ \t]*\{/g);
+ return found[0];
+}
- const start = marker.index + marker[0].length - 1; // at the '{'
- let depth = 0;
- let inString = false;
- let inComment = false;
+/**
+ * Find `<head> { ... }` nodes matching `marker`, with their braces balanced.
+ *
+ * Brace-matched by hand rather than parsed, but it must respect strings and
+ * comments: a zjstatus template is full of `{index}` and `{name}` inside quoted
+ * format strings, and counting those as braces truncates the block in the
+ * middle and produces a layout that will not parse.
+ */
+function extractBlocks(layoutKdl: string, marker: RegExp): string[] {
+ const blocks: string[] = [];
+ for (const match of layoutKdl.matchAll(marker)) {
+ const head = match[0].replace(/^\n/, "").trim(); // e.g. `pane_template name="x" {`
+ const start = (match.index ?? 0) + match[0].length - 1; // at the '{'
+ let depth = 0;
+ let inString = false;
+ let inComment = false;
- for (let i = start; i < layoutKdl.length; i++) {
- const ch = layoutKdl[i] as string;
- const next = layoutKdl[i + 1];
+ for (let i = start; i < layoutKdl.length; i++) {
+ const ch = layoutKdl[i] as string;
+ const next = layoutKdl[i + 1];
- if (inComment) {
- if (ch === "\n") inComment = false;
- continue;
- }
- if (inString) {
- if (ch === "\\") i++; // skip the escaped character
- else if (ch === '"') inString = false;
- continue;
- }
- if (ch === "/" && next === "/") {
- inComment = true;
- continue;
- }
- if (ch === '"') {
- inString = true;
- continue;
- }
- if (ch === "{") depth++;
- else if (ch === "}") {
- depth--;
- if (depth === 0) {
- return `default_tab_template ${layoutKdl.slice(start, i + 1)}`;
+ if (inComment) {
+ if (ch === "\n") inComment = false;
+ continue;
+ }
+ if (inString) {
+ if (ch === "\\") i++; // skip the escaped character
+ else if (ch === '"') inString = false;
+ continue;
+ }
+ if (ch === "/" && next === "/") {
+ inComment = true;
+ continue;
+ }
+ if (ch === '"') {
+ inString = true;
+ continue;
+ }
+ if (ch === "{") depth++;
+ else if (ch === "}") {
+ depth--;
+ if (depth === 0) {
+ const body = layoutKdl.slice(start, i + 1);
+ blocks.push(`${head.slice(0, -1).trim()} ${body}`);
+ break;
+ }
}
}
+ // unbalanced: better to drop this block than to emit a broken layout
}
- return undefined; // unbalanced: better no template than a broken layout
+ return blocks;
}
-/** The template from the user's own default layout, if they have one. */
-export function defaultTabTemplateFrom(configDir: string): string | undefined {
+/**
+ * The `pane_template` definitions from a layout file.
+ *
+ * A generated session layout is a FILE OF ITS OWN: it does not inherit
+ * anything from `layouts/default.kdl`, so a template body that says `sl0pbar
+ * size=3` produces "Invalid tab property: sl0pbar" unless the definition comes
+ * along with it. That is exactly how `sl0ppi up` broke the day the status bar
+ * became a pane_template -- the tab template referenced a name only the
+ * original file knew.
+ */
+export function extractPaneTemplates(layoutKdl: string): string[] {
+ return extractBlocks(layoutKdl, /(^|\n)[ \t]*pane_template[ \t][^\n{]*\{/g);
+}
+
+/** The tab template and the pane templates it may reference. */
+export interface LayoutTemplates {
+ tabTemplate?: string;
+ paneTemplates: string[];
+}
+
+/** The templates from the user's own default layout, if they have one. */
+export function templatesFrom(configDir: string): LayoutTemplates {
try {
- return extractDefaultTabTemplate(
- readFileSync(join(configDir, "layouts", "default.kdl"), "utf8"),
- );
+ const kdl = readFileSync(join(configDir, "layouts", "default.kdl"), "utf8");
+ return {
+ tabTemplate: extractDefaultTabTemplate(kdl),
+ paneTemplates: extractPaneTemplates(kdl),
+ };
} catch {
- return undefined;
+ return { paneTemplates: [] };
}
}
+/** The template from the user's own default layout, if they have one. */
+export function defaultTabTemplateFrom(configDir: string): string | undefined {
+ return templatesFrom(configDir).tabTemplate;
+}
+
/**
* What a project gets when it has a `.sl0ppi/` marker but no `layout.kdl`.
*
@@ -166,13 +210,30 @@ export function tabKdl(spec: TabSpec, indentBy = 4, template?: string): string {
return indent(`${head}\n${indent(fragment, 4)}\n}`, indentBy);
}
+/**
+ * Definitions the generated tabs refer to, emitted once at the top of the
+ * layout. Anything a tab body names has to be defined in the same file.
+ */
+function preamble(paneTemplates?: string[]): string {
+ if (!paneTemplates || paneTemplates.length === 0) return "";
+ return `${paneTemplates.map((template) => indent(template, 4)).join("\n")}\n`;
+}
+
/** A whole-session layout: what `up` passes to `--layout` when creating. */
-export function sessionLayoutKdl(specs: TabSpec[], template?: string): string {
+export function sessionLayoutKdl(
+ specs: TabSpec[],
+ template?: string,
+ paneTemplates?: string[],
+): string {
const tabs = specs.map((spec) => tabKdl(spec, 4, template)).join("\n");
- return `layout {\n${tabs}\n}\n`;
+ return `layout {\n${preamble(paneTemplates)}${tabs}\n}\n`;
}
/** A one-tab layout: what `up` passes to `new-tab --layout-string`. */
-export function singleTabLayoutKdl(spec: TabSpec, template?: string): string {
- return `layout {\n${tabKdl(spec, 4, template)}\n}\n`;
+export function singleTabLayoutKdl(
+ spec: TabSpec,
+ template?: string,
+ paneTemplates?: string[],
+): string {
+ return `layout {\n${preamble(paneTemplates)}${tabKdl(spec, 4, template)}\n}\n`;
}