aboutsummaryrefslogtreecommitdiffstats
path: root/cli/src/commands
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-13 13:06:44 +0200
committeruser <user@clank>2026-08-13 13:06:44 +0200
commit6d9e46274c0d7613441338e10fea6972e4d335dd (patch)
tree02a22eeb155f1a9e3182eb9a21caf034a0bb8d40 /cli/src/commands
parentcli: create sessions by attaching, to dodge the phantom-client bug (diff)
downloadsl0ppi-6d9e46274c0d7613441338e10fea6972e4d335dd.tar.gz
sl0ppi-6d9e46274c0d7613441338e10fea6972e4d335dd.tar.xz
sl0ppi-6d9e46274c0d7613441338e10fea6972e4d335dd.zip
cli: keep the status bar, so other projects are visible
Reported: only one tab for proj_a, where was proj_b? Both tabs existed -- list-tabs showed proj_b at position 1 the whole time -- but there was nothing on screen to say so, because a generated session had NO tab bar and NO status bar. sl0ppi passes its own `--layout` when creating a session, which replaces the user's default layout, and the shipped config gets its chrome from `layouts/default.kdl`'s `default_tab_template` (the zjstatus pane, which is what draws the tab names). So a root with five projects looked exactly like a root with one. The obvious fix is to emit that `default_tab_template` into the generated layout. It does not work: a templated tab comes back with `purpose=None`, because the fork hangs a tab's purpose on the tab's root layout and templating replaces it. Identity is load-bearing here -- with no purposes `sl0ppi ls` sees no project tabs and `up` would happily create a second set of them. Caught by checking list-tabs after the change rather than trusting the tab bar appearing. So the template is INLINED instead: its body is taken, the project's panes are substituted for its `children` node, and the result becomes the tab's contents. The `tab` node keeps its own name/purpose/cwd, and the status bar comes along. A template with no `children` node is skipped rather than mangled. The extractor brace-matches by hand but respects strings and comments: a real zjstatus template is full of `{index}` and `{name}` inside quoted format strings, and counting those would truncate the block and emit a layout that does not parse. It reads the live config, so a user's own default.kdl is honoured. Verified on the test box: tab bar reads "1 proj_a 2 proj_b", tab purposes are project:proj_a / project:proj_b, `sl0ppi ls` sees both, `up` still says "up to date", and no phantom-client marker.
Diffstat (limited to 'cli/src/commands')
-rw-r--r--cli/src/commands/workspace.ts34
1 files changed, 29 insertions, 5 deletions
diff --git a/cli/src/commands/workspace.ts b/cli/src/commands/workspace.ts
index 4eec946..f688ce4 100644
--- a/cli/src/commands/workspace.ts
+++ b/cli/src/commands/workspace.ts
@@ -14,7 +14,12 @@ import { tmpdir } from "node:os";
import { checkFragment } from "./doctor.ts";
import { discoverProjects, LAYOUT_FILE, MARKER_DIR } from "../core/discover.ts";
import { UserError } from "../core/errors.ts";
-import { sessionLayoutKdl, singleTabLayoutKdl, type TabSpec } from "../core/layout.ts";
+import {
+ defaultTabTemplateFrom,
+ sessionLayoutKdl,
+ singleTabLayoutKdl,
+ type TabSpec,
+} from "../core/layout.ts";
import { isNoOp, planTabs, type Plan } from "../core/plan.ts";
import { projectPurpose } from "../core/purpose.ts";
import { rememberRoot, rootExists, type ResolvedRoot } from "../core/root.ts";
@@ -32,9 +37,28 @@ export interface Context {
* it BY ATTACHING to it. See `UpResult.createWith`.
*/
deferCreate?: boolean;
+ /** Cached tab template; "" means "looked, found none". */
+ tabTemplate?: string;
log: (message: string) => void;
}
+/**
+ * The tab template every generated tab is wrapped in, so sessions keep the
+ * status/tab bar the shipped config provides. Without it a multi-project
+ * session shows no tab bar at all and the other projects are invisible.
+ */
+function tabTemplate(ctx: Context): string | undefined {
+ if (ctx.tabTemplate === undefined) {
+ ctx.tabTemplate = defaultTabTemplateFrom(sl0ppijConfigDir()) ?? "";
+ }
+ return ctx.tabTemplate === "" ? undefined : ctx.tabTemplate;
+}
+
+export function sl0ppijConfigDir(env: NodeJS.ProcessEnv = process.env): string {
+ if (env.SL0PPIJ_CONFIG_DIR) return env.SL0PPIJ_CONFIG_DIR;
+ return join(env.HOME ?? "", ".config", "sl0ppij");
+}
+
function fragmentFor(root: string, relPath: string): string | undefined {
const path = join(root, relPath, MARKER_DIR, LAYOUT_FILE);
try {
@@ -165,7 +189,7 @@ export function up(ctx: Context): UpResult {
// one rather than an error, so `sl0ppi` still gives you a terminal.
const layout =
specs.length > 0
- ? sessionLayoutKdl(specs)
+ ? sessionLayoutKdl(specs, tabTemplate(ctx))
: "layout {\n tab name=\"scratch\" {\n pane\n }\n}\n";
const dir = mkdtempSync(join(tmpdir(), "sl0ppi-layout-"));
const layoutPath = join(dir, "session.kdl");
@@ -217,7 +241,7 @@ export function up(ctx: Context): UpResult {
for (const spec of specs) {
try {
- client.newTab(singleTabLayoutKdl(spec));
+ client.newTab(singleTabLayoutKdl(spec, tabTemplate(ctx)));
ctx.log(`+ ${spec.relPath}`);
} catch (error) {
const message = error instanceof Error ? error.message.split("\n")[0] : String(error);
@@ -299,7 +323,7 @@ export function open(ctx: Context, name: string): TabInfo {
const purpose = projectPurpose(relPath);
let tab = client.listTabs().find((t) => t.purpose === purpose);
if (!tab) {
- const position = client.newTab(singleTabLayoutKdl(specFor(root, relPath)));
+ const position = client.newTab(singleTabLayoutKdl(specFor(root, relPath), tabTemplate(ctx)));
ctx.log(`+ ${relPath}`);
tab = client.listTabs().find((t) => t.position === position) ?? {
position,
@@ -353,5 +377,5 @@ function emptyPlan(): Plan {
/** The layout sl0ppi would use for a project — for eyeballing and debugging. */
export function print(ctx: Context, name: string): string {
const relPath = resolveProjectName(ctx, name);
- return singleTabLayoutKdl(specFor(ctx.resolved.root, relPath));
+ return singleTabLayoutKdl(specFor(ctx.resolved.root, relPath), tabTemplate(ctx));
}