@v1nvn/statusline 0.27.2 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -27
- package/dist/index.js +751 -556
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
|
-
import { Command
|
|
5
|
+
import { Command } from "commander";
|
|
6
6
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
//#region ../core/dist/index.js
|
|
@@ -23,6 +23,113 @@ function printUsageAndExit(program) {
|
|
|
23
23
|
}
|
|
24
24
|
Number.MAX_SAFE_INTEGER;
|
|
25
25
|
//#endregion
|
|
26
|
+
//#region src/themes.ts
|
|
27
|
+
var THEME_NAMES = [
|
|
28
|
+
"classic",
|
|
29
|
+
"custom",
|
|
30
|
+
"lean",
|
|
31
|
+
"quiet",
|
|
32
|
+
"rich"
|
|
33
|
+
];
|
|
34
|
+
var CUSTOM_SEEDS = {
|
|
35
|
+
branch: "last",
|
|
36
|
+
cwd: "base",
|
|
37
|
+
model: "zen",
|
|
38
|
+
style: "bare"
|
|
39
|
+
};
|
|
40
|
+
function offers(item, variant) {
|
|
41
|
+
return item.default === variant || item.alternatives.includes(variant);
|
|
42
|
+
}
|
|
43
|
+
function absenceSeed(item) {
|
|
44
|
+
if (offers(item, "none")) return "none";
|
|
45
|
+
if (offers(item, "hidden")) return "hidden";
|
|
46
|
+
const named = CUSTOM_SEEDS[item.item];
|
|
47
|
+
if (named === void 0) throw new Error(`no absence variant for '${item.item}' — name its seed`);
|
|
48
|
+
return named;
|
|
49
|
+
}
|
|
50
|
+
function themesFor(runtime) {
|
|
51
|
+
function variantsOf(pick) {
|
|
52
|
+
return Object.fromEntries(runtime.items.map((item) => [item.item, pick(item)]));
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
quiet: {
|
|
56
|
+
layout: "{model cwd}",
|
|
57
|
+
summary: "model and directory, nothing else",
|
|
58
|
+
variants: {
|
|
59
|
+
cwd: "tail",
|
|
60
|
+
model: "zen",
|
|
61
|
+
style: "bare"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
classic: {
|
|
65
|
+
layout: runtime.defaultLayout,
|
|
66
|
+
summary: "the shipped defaults, named",
|
|
67
|
+
variants: variantsOf((item) => item.default)
|
|
68
|
+
},
|
|
69
|
+
lean: {
|
|
70
|
+
layout: runtime.defaultLayout,
|
|
71
|
+
summary: "text only, no graphics",
|
|
72
|
+
variants: {
|
|
73
|
+
model: "plain",
|
|
74
|
+
effort: "dim",
|
|
75
|
+
state: "none",
|
|
76
|
+
cwd: "init",
|
|
77
|
+
branch: "initials",
|
|
78
|
+
status: "counts",
|
|
79
|
+
ahead: "arrows",
|
|
80
|
+
pr: "badge",
|
|
81
|
+
bar: "percent",
|
|
82
|
+
tokens: "full",
|
|
83
|
+
cache: "hit",
|
|
84
|
+
cost: "plain",
|
|
85
|
+
duration: "clock",
|
|
86
|
+
lines: "diffstat",
|
|
87
|
+
rate: "none",
|
|
88
|
+
style: "dots"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
rich: {
|
|
92
|
+
layout: runtime.defaultLayout,
|
|
93
|
+
summary: "every gauge and counter",
|
|
94
|
+
variants: {
|
|
95
|
+
model: "pill",
|
|
96
|
+
effort: "plain",
|
|
97
|
+
state: "pills",
|
|
98
|
+
cwd: "icon",
|
|
99
|
+
branch: "icon",
|
|
100
|
+
status: "icons",
|
|
101
|
+
ahead: "arrows",
|
|
102
|
+
pr: "badge",
|
|
103
|
+
bar: "gauge",
|
|
104
|
+
tokens: "full",
|
|
105
|
+
cache: "fuse",
|
|
106
|
+
cost: "burn",
|
|
107
|
+
duration: "clock",
|
|
108
|
+
lines: "diffstat",
|
|
109
|
+
rate: "strip",
|
|
110
|
+
style: "plain"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
custom: {
|
|
114
|
+
layout: runtime.defaultLayout,
|
|
115
|
+
summary: "bare; you decide everything",
|
|
116
|
+
variants: variantsOf(absenceSeed)
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/live-theme.ts
|
|
122
|
+
function sameAssignments(key, theme) {
|
|
123
|
+
return Object.entries(key).every(([item, variant]) => theme[item] === variant) && Object.entries(theme).every(([item, variant]) => key[item] === variant);
|
|
124
|
+
}
|
|
125
|
+
function liveTheme(key, runtime) {
|
|
126
|
+
const themes = themesFor(runtime);
|
|
127
|
+
for (const name of Object.keys(themes)) {
|
|
128
|
+
const { layout, variants } = themes[name];
|
|
129
|
+
if (key.layout === layout && sameAssignments(key.values, variants)) return name;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
26
133
|
//#region src/resolve.ts
|
|
27
134
|
var DATA_REL = join(".claude", "plugins", "data", "statusline-agentic");
|
|
28
135
|
function capturePath(home, surface) {
|
|
@@ -136,34 +243,46 @@ function readDefaultLayout(dir) {
|
|
|
136
243
|
if (match === null) throw new Error(`lib.sh in ${dir} declares no DEFAULT_LAYOUT`);
|
|
137
244
|
return match[1];
|
|
138
245
|
}
|
|
139
|
-
function
|
|
140
|
-
const pluginDir = newestCacheDir(join(home, ".claude", "plugins", "cache", "agentic", "statusline"));
|
|
141
|
-
const dir = pluginDir === null ? null : join(pluginDir, "runtime");
|
|
142
|
-
if (dir === null || !existsSync(join(dir, "statusline.sh"))) throw new Error(`no statusline runtime under ${home} — install the plugin first: claude plugin install statusline@agentic`);
|
|
246
|
+
function resolveRuntimeDir(dir) {
|
|
143
247
|
return {
|
|
144
248
|
defaultLayout: readDefaultLayout(dir),
|
|
145
249
|
dir,
|
|
146
250
|
items: readItems(dir)
|
|
147
251
|
};
|
|
148
252
|
}
|
|
253
|
+
function resolveRuntime({ home }) {
|
|
254
|
+
const pluginDir = newestCacheDir(join(home, ".claude", "plugins", "cache", "agentic", "statusline"));
|
|
255
|
+
const dir = pluginDir === null ? null : join(pluginDir, "runtime");
|
|
256
|
+
if (dir === null || !existsSync(join(dir, "statusline.sh"))) throw new Error(`no statusline runtime under ${home} — install the plugin first: claude plugin install statusline@agentic`);
|
|
257
|
+
return resolveRuntimeDir(dir);
|
|
258
|
+
}
|
|
149
259
|
//#endregion
|
|
150
260
|
//#region src/catalog.ts
|
|
151
261
|
function catalog(options) {
|
|
262
|
+
if (options.themes === true && options.items !== void 0) throw new Error("--themes cannot combine with item flags");
|
|
152
263
|
const runtime = resolveRuntime({ home: options.home });
|
|
153
|
-
const
|
|
264
|
+
const key = readKeyConfig(options.home);
|
|
265
|
+
const live = liveTheme(key, runtime);
|
|
266
|
+
const block = Object.entries(themesFor(runtime)).map(([name, theme]) => `${name}${live === name ? "*" : ""}: ${theme.summary}`);
|
|
267
|
+
if (options.themes === true) return block.join("\n");
|
|
154
268
|
const byItem = new Map(runtime.items.map((item) => [item.item, item]));
|
|
155
|
-
|
|
269
|
+
const itemLines = (options.items ?? runtime.items.map((item) => item.item)).map((item) => {
|
|
156
270
|
const entry = byItem.get(item);
|
|
157
271
|
if (entry === void 0) throw new Error(`unknown item '${item}' — valid items: ${[...byItem.keys()].join(" ")}`);
|
|
158
|
-
const current =
|
|
272
|
+
const current = key.values[item] ?? entry.default;
|
|
159
273
|
return `${item}: ${entry.alternatives.map((alt) => alt === current ? `${alt}*` : alt).join(" | ")}`;
|
|
160
|
-
})
|
|
274
|
+
});
|
|
275
|
+
return [
|
|
276
|
+
...block,
|
|
277
|
+
"",
|
|
278
|
+
...itemLines
|
|
279
|
+
].join("\n");
|
|
161
280
|
}
|
|
162
281
|
//#endregion
|
|
163
282
|
//#region src/cli.ts
|
|
164
283
|
var VERSION = {
|
|
165
284
|
name: "@v1nvn/statusline",
|
|
166
|
-
version: "0.
|
|
285
|
+
version: "0.28.0",
|
|
167
286
|
description: "Browse statusline variants and configure the live status line and agent panel — the statusline CLI behind the statusline plugin.",
|
|
168
287
|
type: "module",
|
|
169
288
|
bin: "dist/index.js",
|
|
@@ -236,17 +355,22 @@ function quiet(name, command) {
|
|
|
236
355
|
}).configureOutput(QUIET).allowExcessArguments(false);
|
|
237
356
|
}
|
|
238
357
|
function buildProgram(onSubcommand) {
|
|
239
|
-
const catalog = quiet("catalog", new Command("catalog")).description("print one line per item — * marks the live variant").option("--home <dir>", "operate on this home instead of $HOME");
|
|
358
|
+
const catalog = quiet("catalog", new Command("catalog")).description("print the themes block (* marks the live theme) then one line per item — * marks the live variant").option("--home <dir>", "operate on this home instead of $HOME").option("--themes", "cut the listing to the themes block");
|
|
240
359
|
for (const item of ITEM_IDS) catalog.option(`--${item}`, `cut the listing to the ${item} item`);
|
|
241
360
|
catalog.action((options) => onSubcommand?.("catalog", options));
|
|
242
|
-
const
|
|
361
|
+
const LAYOUT_HELP = "brace clusters of item ids, e.g. '{cwd branch} {model effort}'";
|
|
362
|
+
const THEME_HELP = "base design the item flags override: quiet, lean, classic, rich, custom";
|
|
363
|
+
const configure = quiet("configure", new Command("configure")).description("write both settings keys with the inline lab commands").option("--home <dir>", "operate on this home instead of $HOME").option("--layout <spec>", LAYOUT_HELP).option("--theme <name>", THEME_HELP).option("--force", "take over foreign settings keys");
|
|
243
364
|
for (const item of ITEM_IDS) configure.option(`--${item} <alt>`, `variant for the ${item} item`);
|
|
244
365
|
configure.action((options) => onSubcommand?.("configure", options));
|
|
366
|
+
const preview = quiet("preview", new Command("preview")).description("render a candidate line and panel row; nothing is written").option("--home <dir>", "operate on this home instead of $HOME").option("--layout <spec>", LAYOUT_HELP).option("--theme <name>", THEME_HELP).option("--plain", "strip the color escapes so glyphs survive chat");
|
|
367
|
+
for (const item of ITEM_IDS) preview.option(`--${item} <alt>`, `variant for the ${item} item`);
|
|
368
|
+
preview.action((options) => onSubcommand?.("preview", options));
|
|
245
369
|
const restore = quiet("restore", new Command("restore")).description("revert both settings keys to their pre-lab values, clean the data dir").option("--home <dir>", "operate on this home instead of $HOME").option("--dry-run", "print the plan, write nothing").option("--force", "splice the saved value over a key changed after the takeover");
|
|
246
370
|
restore.action((options) => onSubcommand?.("restore", options));
|
|
247
371
|
const status = quiet("status", new Command("status")).description("check install, keys, config, and backup — exit 0 healthy, 1 when a row needs action").option("--home <dir>", "operate on this home instead of $HOME");
|
|
248
372
|
status.action((options) => onSubcommand?.("status", options));
|
|
249
|
-
return new Command().name("statusline").description("Configure the status line and agent panel designs").option("-V, --version", "print the version and exit").action(() => void 0).addCommand(catalog).addCommand(configure).addCommand(restore).addCommand(status);
|
|
373
|
+
return new Command().name("statusline").description("Configure the status line and agent panel designs").option("-V, --version", "print the version and exit").action(() => void 0).addCommand(catalog).addCommand(configure).addCommand(preview).addCommand(restore).addCommand(status);
|
|
250
374
|
}
|
|
251
375
|
function subcommandHelp(name) {
|
|
252
376
|
const command = buildProgram().commands.find((c) => c.name() === name);
|
|
@@ -277,6 +401,7 @@ function parseArgs(args) {
|
|
|
277
401
|
version: false,
|
|
278
402
|
command: "catalog",
|
|
279
403
|
...items.length > 0 ? { items } : {},
|
|
404
|
+
...typeof options.themes === "boolean" ? { themes: options.themes } : {},
|
|
280
405
|
...typeof options.home === "string" ? { home: options.home } : {}
|
|
281
406
|
};
|
|
282
407
|
}
|
|
@@ -294,316 +419,84 @@ function parseArgs(args) {
|
|
|
294
419
|
};
|
|
295
420
|
const variants = {};
|
|
296
421
|
for (const item of ITEM_IDS) if (typeof options[item] === "string") variants[item] = options[item];
|
|
297
|
-
|
|
422
|
+
const common = {
|
|
298
423
|
version: false,
|
|
299
|
-
command:
|
|
424
|
+
command: chosen.command,
|
|
300
425
|
...Object.keys(variants).length > 0 ? { variants } : {},
|
|
301
|
-
...typeof options.dryRun === "boolean" ? { dryRun: options.dryRun } : {},
|
|
302
|
-
...typeof options.fallback === "string" ? { fallback: options.fallback } : {},
|
|
303
|
-
...typeof options.force === "boolean" ? { force: options.force } : {},
|
|
304
426
|
...typeof options.home === "string" ? { home: options.home } : {},
|
|
305
|
-
...typeof options.layout === "string" ? { layout: options.layout } : {}
|
|
427
|
+
...typeof options.layout === "string" ? { layout: options.layout } : {},
|
|
428
|
+
...typeof options.theme === "string" ? { theme: options.theme } : {}
|
|
429
|
+
};
|
|
430
|
+
return chosen.command === "configure" ? {
|
|
431
|
+
...common,
|
|
432
|
+
...typeof options.force === "boolean" ? { force: options.force } : {}
|
|
433
|
+
} : {
|
|
434
|
+
...common,
|
|
435
|
+
...typeof options.plain === "boolean" ? { plain: options.plain } : {}
|
|
306
436
|
};
|
|
307
437
|
}
|
|
308
438
|
//#endregion
|
|
309
|
-
//#region src/
|
|
310
|
-
var
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
var API_INIT = "export const api = {\n host: 'demo.local',\n};\n";
|
|
318
|
-
function materializeDemoRepo(home) {
|
|
319
|
-
const dir = join(home, "demo", "atlas-web");
|
|
320
|
-
for (const sub of [
|
|
321
|
-
"src",
|
|
322
|
-
"docs",
|
|
323
|
-
"scratch",
|
|
324
|
-
"scripts"
|
|
325
|
-
]) mkdirSync(join(dir, sub), { recursive: true });
|
|
326
|
-
function git(args, date) {
|
|
327
|
-
return execFileSync("git", [
|
|
328
|
-
"-c",
|
|
329
|
-
"commit.gpgsign=false",
|
|
330
|
-
...args
|
|
331
|
-
], {
|
|
332
|
-
cwd: dir,
|
|
333
|
-
encoding: "utf8",
|
|
334
|
-
stdio: [
|
|
335
|
-
"ignore",
|
|
336
|
-
"pipe",
|
|
337
|
-
"pipe"
|
|
338
|
-
],
|
|
339
|
-
env: {
|
|
340
|
-
PATH: process.env.PATH ?? "",
|
|
341
|
-
HOME: home,
|
|
342
|
-
LC_ALL: "C",
|
|
343
|
-
TZ: "UTC",
|
|
344
|
-
GIT_AUTHOR_NAME: NAME,
|
|
345
|
-
GIT_AUTHOR_EMAIL: EMAIL,
|
|
346
|
-
GIT_COMMITTER_NAME: NAME,
|
|
347
|
-
GIT_COMMITTER_EMAIL: EMAIL,
|
|
348
|
-
...date ? {
|
|
349
|
-
GIT_AUTHOR_DATE: date,
|
|
350
|
-
GIT_COMMITTER_DATE: date
|
|
351
|
-
} : {}
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
function write(rel, body) {
|
|
356
|
-
writeFileSync(join(dir, rel), body);
|
|
357
|
-
}
|
|
358
|
-
function commit(message, date) {
|
|
359
|
-
git(["add", "-A"]);
|
|
360
|
-
git([
|
|
361
|
-
"commit",
|
|
362
|
-
"-m",
|
|
363
|
-
message
|
|
364
|
-
], date);
|
|
439
|
+
//#region src/configure.ts
|
|
440
|
+
var SETTINGS_KEYS = ["statusLine", "subagentStatusLine"];
|
|
441
|
+
function layoutItems(layout, valid) {
|
|
442
|
+
const known = new Set(valid);
|
|
443
|
+
const items = [];
|
|
444
|
+
for (const cluster of parseClusters(layout)) for (const word of cluster) {
|
|
445
|
+
if (!known.has(word)) throw new Error(`unknown layout item '${word}' — valid items: ${valid.join(" ")}`);
|
|
446
|
+
if (!items.includes(word)) items.push(word);
|
|
365
447
|
}
|
|
366
|
-
|
|
367
|
-
"init",
|
|
368
|
-
"-b",
|
|
369
|
-
"main"
|
|
370
|
-
]);
|
|
371
|
-
git([
|
|
372
|
-
"config",
|
|
373
|
-
"user.name",
|
|
374
|
-
NAME
|
|
375
|
-
]);
|
|
376
|
-
git([
|
|
377
|
-
"config",
|
|
378
|
-
"user.email",
|
|
379
|
-
EMAIL
|
|
380
|
-
]);
|
|
381
|
-
write("README.md", "atlas-web demo repo\n");
|
|
382
|
-
write("src/index.js", "export function start() {}\n");
|
|
383
|
-
write("src/api.js", API_INIT);
|
|
384
|
-
write("src/style.css", "body { margin: 0; }\n");
|
|
385
|
-
write("src/util.js", "export const id = (x) => x;\n");
|
|
386
|
-
commit("init", D_INIT);
|
|
387
|
-
write("src/api.js", `${API_INIT}// remote: pinned host\n`);
|
|
388
|
-
commit("remote1", D_REMOTE);
|
|
389
|
-
const originMain = git(["rev-parse", "HEAD"]).trim();
|
|
390
|
-
git([
|
|
391
|
-
"checkout",
|
|
392
|
-
"-b",
|
|
393
|
-
"feature/login-flow",
|
|
394
|
-
"HEAD~1"
|
|
395
|
-
]);
|
|
396
|
-
write("src/util.js", "export const id = (x) => x;\nexport const tag = (x) => x;\n");
|
|
397
|
-
commit("local1", D_LOCAL1);
|
|
398
|
-
write("src/render.js", "export function render() {}\n");
|
|
399
|
-
commit("local2", D_LOCAL2);
|
|
400
|
-
git([
|
|
401
|
-
"update-ref",
|
|
402
|
-
"refs/remotes/origin/main",
|
|
403
|
-
originMain
|
|
404
|
-
]);
|
|
405
|
-
git([
|
|
406
|
-
"config",
|
|
407
|
-
"remote.origin.url",
|
|
408
|
-
"../atlas-web.git"
|
|
409
|
-
]);
|
|
410
|
-
git([
|
|
411
|
-
"config",
|
|
412
|
-
"remote.origin.fetch",
|
|
413
|
-
"+refs/heads/*:refs/remotes/origin/*"
|
|
414
|
-
]);
|
|
415
|
-
git([
|
|
416
|
-
"config",
|
|
417
|
-
"branch.feature/login-flow.remote",
|
|
418
|
-
"origin"
|
|
419
|
-
]);
|
|
420
|
-
git([
|
|
421
|
-
"config",
|
|
422
|
-
"branch.feature/login-flow.merge",
|
|
423
|
-
"refs/heads/main"
|
|
424
|
-
]);
|
|
425
|
-
write("README.md", "atlas-web demo repo (wip)\n");
|
|
426
|
-
git([
|
|
427
|
-
"stash",
|
|
428
|
-
"push",
|
|
429
|
-
"-m",
|
|
430
|
-
"wip: context refactor",
|
|
431
|
-
"--",
|
|
432
|
-
"README.md"
|
|
433
|
-
], D_STASH);
|
|
434
|
-
write("README.md", "atlas-web demo repo\n\nstaged: pin the render loop\n");
|
|
435
|
-
write("src/index.js", "export function start() {\n return Promise.resolve();\n}\n");
|
|
436
|
-
git([
|
|
437
|
-
"add",
|
|
438
|
-
"README.md",
|
|
439
|
-
"src/index.js"
|
|
440
|
-
]);
|
|
441
|
-
write("src/api.js", `${API_INIT}// local tweak\n`);
|
|
442
|
-
write("src/style.css", "body { margin: 0; padding: 0; }\n");
|
|
443
|
-
write("docs/wip.txt", "");
|
|
444
|
-
write("scratch/todo.md", "");
|
|
445
|
-
write("scripts/deploy.sh", "");
|
|
446
|
-
write("note1.md", "");
|
|
447
|
-
write("note2.md", "");
|
|
448
|
-
return dir;
|
|
448
|
+
return items;
|
|
449
449
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
};
|
|
460
|
-
var PANEL_STEP_S = 300;
|
|
461
|
-
function anchoredP1(repoDir, now) {
|
|
462
|
-
const payload = JSON.parse(readFileSync(join(PAYLOADS_DIR, "p1.json"), "utf8"));
|
|
463
|
-
const cache = payload.prompt_cache;
|
|
464
|
-
if (isObject(cache)) {
|
|
465
|
-
if (cache.warm === true) {
|
|
466
|
-
cache.expires_at = now + P1_WARM_IN;
|
|
467
|
-
cache.last_miss_at = now + P1_WARM_IN - 3900;
|
|
468
|
-
} else {
|
|
469
|
-
cache.expires_at = now - 10;
|
|
470
|
-
delete cache.last_miss_at;
|
|
450
|
+
function parseClusters(layout) {
|
|
451
|
+
const clusters = [];
|
|
452
|
+
let words = [];
|
|
453
|
+
let word = "";
|
|
454
|
+
let open = false;
|
|
455
|
+
function pushWord() {
|
|
456
|
+
if (word !== "") {
|
|
457
|
+
words.push(word);
|
|
458
|
+
word = "";
|
|
471
459
|
}
|
|
472
460
|
}
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
461
|
+
for (const c of layout) if (c === "{") {
|
|
462
|
+
if (open) throw new Error(`layout '${layout}': '{' inside a cluster`);
|
|
463
|
+
open = true;
|
|
464
|
+
words = [];
|
|
465
|
+
} else if (c === "}") {
|
|
466
|
+
if (!open) throw new Error(`layout '${layout}': '}' outside a cluster`);
|
|
467
|
+
pushWord();
|
|
468
|
+
open = false;
|
|
469
|
+
if (words.length > 0) clusters.push(words);
|
|
470
|
+
} else if (c === " ") {
|
|
471
|
+
if (open) pushWord();
|
|
472
|
+
else if (word !== "") throw new Error(`layout '${layout}': '${word}' sits outside a cluster`);
|
|
473
|
+
} else if (/[a-z0-9]/.test(c)) word += c;
|
|
474
|
+
else throw new Error(`layout '${layout}': '${c}' is not layout grammar (braces, item ids, spaces)`);
|
|
475
|
+
if (open) throw new Error(`layout '${layout}': unterminated cluster`);
|
|
476
|
+
if (word !== "") throw new Error(`layout '${layout}': '${word}' sits outside a cluster`);
|
|
477
|
+
if (clusters.length === 0) throw new Error(`layout '${layout}': no clusters`);
|
|
478
|
+
return clusters;
|
|
479
|
+
}
|
|
480
|
+
function isObject(value) {
|
|
481
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
482
|
+
}
|
|
483
|
+
function readOrNull(file) {
|
|
484
|
+
try {
|
|
485
|
+
return readFileSync(file, "utf8");
|
|
486
|
+
} catch (e) {
|
|
487
|
+
if (e.code === "ENOENT") return null;
|
|
488
|
+
throw e;
|
|
477
489
|
}
|
|
478
|
-
payload.workspace.current_dir = repoDir;
|
|
479
|
-
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
480
490
|
}
|
|
481
|
-
function
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
tasks: tasks.map((task) => {
|
|
491
|
-
if (!hasStart(task.startTime)) return task;
|
|
492
|
-
started += 1;
|
|
493
|
-
return {
|
|
494
|
-
...task,
|
|
495
|
-
startTime: now - PANEL_STEP_S * started
|
|
496
|
-
};
|
|
497
|
-
})
|
|
498
|
-
};
|
|
499
|
-
return `${JSON.stringify(staggered, null, 2)}\n`;
|
|
500
|
-
}
|
|
501
|
-
function previewSources(home, now) {
|
|
502
|
-
const mainCapture = capturePath(home, "main");
|
|
503
|
-
if (existsSync(mainCapture)) {
|
|
504
|
-
const tickCapture = capturePath(home, "tick");
|
|
505
|
-
return {
|
|
506
|
-
cleanup: () => void 0,
|
|
507
|
-
main: readFileSync(mainCapture, "utf8"),
|
|
508
|
-
tick: existsSync(tickCapture) ? readFileSync(tickCapture, "utf8") : anchoredTick(now)
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
const demoHome = mkdtempSync(join(tmpdir(), "statusline-preview-"));
|
|
512
|
-
return {
|
|
513
|
-
cleanup: () => {
|
|
514
|
-
rmSync(demoHome, {
|
|
515
|
-
recursive: true,
|
|
516
|
-
force: true
|
|
517
|
-
});
|
|
518
|
-
},
|
|
519
|
-
main: anchoredP1(materializeDemoRepo(demoHome), now),
|
|
520
|
-
tick: anchoredTick(now)
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
function firstPanelRow(output) {
|
|
524
|
-
const [row] = output.split("\n");
|
|
525
|
-
if (row === "") return "";
|
|
526
|
-
const content = JSON.parse(row).content;
|
|
527
|
-
return typeof content === "string" ? content : "";
|
|
528
|
-
}
|
|
529
|
-
function runtimeRenderer(spec) {
|
|
530
|
-
const run = spawnSync("bash", [spec.bin], {
|
|
531
|
-
input: spec.stdin,
|
|
532
|
-
env: {
|
|
533
|
-
PATH: process.env.PATH ?? "",
|
|
534
|
-
...spec.env,
|
|
535
|
-
LC_ALL: "C",
|
|
536
|
-
TZ: "UTC"
|
|
537
|
-
},
|
|
538
|
-
timeout: 3e4
|
|
539
|
-
});
|
|
540
|
-
if (run.status !== 0) throw new Error(`render failed: ${run.stderr.toString("utf8")}`);
|
|
541
|
-
const stdout = run.stdout.toString("utf8");
|
|
542
|
-
const warn = run.stderr.toString("utf8").trim();
|
|
543
|
-
return warn === "" ? stdout : `${stdout}${warn}\n`;
|
|
544
|
-
}
|
|
545
|
-
//#endregion
|
|
546
|
-
//#region src/configure.ts
|
|
547
|
-
var SETTINGS_KEYS = ["statusLine", "subagentStatusLine"];
|
|
548
|
-
function layoutItems(layout, valid) {
|
|
549
|
-
const known = new Set(valid);
|
|
550
|
-
const items = [];
|
|
551
|
-
for (const cluster of parseClusters(layout)) for (const word of cluster) {
|
|
552
|
-
if (!known.has(word)) throw new Error(`unknown layout item '${word}' — valid items: ${valid.join(" ")}`);
|
|
553
|
-
if (!items.includes(word)) items.push(word);
|
|
554
|
-
}
|
|
555
|
-
return items;
|
|
556
|
-
}
|
|
557
|
-
function parseClusters(layout) {
|
|
558
|
-
const clusters = [];
|
|
559
|
-
let words = [];
|
|
560
|
-
let word = "";
|
|
561
|
-
let open = false;
|
|
562
|
-
function pushWord() {
|
|
563
|
-
if (word !== "") {
|
|
564
|
-
words.push(word);
|
|
565
|
-
word = "";
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
for (const c of layout) if (c === "{") {
|
|
569
|
-
if (open) throw new Error(`layout '${layout}': '{' inside a cluster`);
|
|
570
|
-
open = true;
|
|
571
|
-
words = [];
|
|
572
|
-
} else if (c === "}") {
|
|
573
|
-
if (!open) throw new Error(`layout '${layout}': '}' outside a cluster`);
|
|
574
|
-
pushWord();
|
|
575
|
-
open = false;
|
|
576
|
-
if (words.length > 0) clusters.push(words);
|
|
577
|
-
} else if (c === " ") {
|
|
578
|
-
if (open) pushWord();
|
|
579
|
-
else if (word !== "") throw new Error(`layout '${layout}': '${word}' sits outside a cluster`);
|
|
580
|
-
} else if (/[a-z0-9]/.test(c)) word += c;
|
|
581
|
-
else throw new Error(`layout '${layout}': '${c}' is not layout grammar (braces, item ids, spaces)`);
|
|
582
|
-
if (open) throw new Error(`layout '${layout}': unterminated cluster`);
|
|
583
|
-
if (word !== "") throw new Error(`layout '${layout}': '${word}' sits outside a cluster`);
|
|
584
|
-
if (clusters.length === 0) throw new Error(`layout '${layout}': no clusters`);
|
|
585
|
-
return clusters;
|
|
586
|
-
}
|
|
587
|
-
function isObject(value) {
|
|
588
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
589
|
-
}
|
|
590
|
-
function readOrNull(file) {
|
|
591
|
-
try {
|
|
592
|
-
return readFileSync(file, "utf8");
|
|
593
|
-
} catch (e) {
|
|
594
|
-
if (e.code === "ENOENT") return null;
|
|
595
|
-
throw e;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
function parseSettings(file, raw) {
|
|
599
|
-
let parsed;
|
|
600
|
-
try {
|
|
601
|
-
parsed = JSON.parse(raw);
|
|
602
|
-
} catch (e) {
|
|
603
|
-
throw new Error(`${file} is not valid JSON`, { cause: e });
|
|
604
|
-
}
|
|
605
|
-
if (!isObject(parsed)) throw new Error(`${file} does not hold a settings object`);
|
|
606
|
-
return parsed;
|
|
491
|
+
function parseSettings(file, raw) {
|
|
492
|
+
let parsed;
|
|
493
|
+
try {
|
|
494
|
+
parsed = JSON.parse(raw);
|
|
495
|
+
} catch (e) {
|
|
496
|
+
throw new Error(`${file} is not valid JSON`, { cause: e });
|
|
497
|
+
}
|
|
498
|
+
if (!isObject(parsed)) throw new Error(`${file} does not hold a settings object`);
|
|
499
|
+
return parsed;
|
|
607
500
|
}
|
|
608
501
|
function memberCommand(value) {
|
|
609
502
|
if (!isObject(value) || value.type !== "command") return null;
|
|
@@ -682,191 +575,445 @@ function skipValue(raw, at) {
|
|
|
682
575
|
while (i < raw.length && !",}]".includes(raw[i] ?? "") && !/\s/.test(raw[i] ?? "~")) i += 1;
|
|
683
576
|
return i;
|
|
684
577
|
}
|
|
685
|
-
function rootMembers(raw) {
|
|
686
|
-
const members = [];
|
|
687
|
-
let i = raw.indexOf("{") + 1;
|
|
688
|
-
while (i < raw.length) {
|
|
689
|
-
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
690
|
-
if (raw[i] === "}") break;
|
|
691
|
-
if (raw[i] !== "\"") throw new Error("malformed member in settings.json");
|
|
692
|
-
const nameStart = i;
|
|
693
|
-
const nameEnd = skipString(raw, i);
|
|
694
|
-
const name = JSON.parse(raw.slice(i, nameEnd));
|
|
695
|
-
i = nameEnd;
|
|
696
|
-
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
697
|
-
if (raw[i] !== ":") throw new Error("malformed member in settings.json");
|
|
698
|
-
i += 1;
|
|
699
|
-
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
700
|
-
const valueStart = i;
|
|
701
|
-
const valueEnd = skipValue(raw, i);
|
|
702
|
-
members.push({
|
|
703
|
-
name,
|
|
704
|
-
nameStart,
|
|
705
|
-
valueEnd,
|
|
706
|
-
valueStart
|
|
707
|
-
});
|
|
708
|
-
i = valueEnd;
|
|
709
|
-
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
710
|
-
if (raw[i] === ",") i += 1;
|
|
711
|
-
else if (raw[i] !== "}") throw new Error("malformed member in settings.json");
|
|
578
|
+
function rootMembers(raw) {
|
|
579
|
+
const members = [];
|
|
580
|
+
let i = raw.indexOf("{") + 1;
|
|
581
|
+
while (i < raw.length) {
|
|
582
|
+
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
583
|
+
if (raw[i] === "}") break;
|
|
584
|
+
if (raw[i] !== "\"") throw new Error("malformed member in settings.json");
|
|
585
|
+
const nameStart = i;
|
|
586
|
+
const nameEnd = skipString(raw, i);
|
|
587
|
+
const name = JSON.parse(raw.slice(i, nameEnd));
|
|
588
|
+
i = nameEnd;
|
|
589
|
+
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
590
|
+
if (raw[i] !== ":") throw new Error("malformed member in settings.json");
|
|
591
|
+
i += 1;
|
|
592
|
+
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
593
|
+
const valueStart = i;
|
|
594
|
+
const valueEnd = skipValue(raw, i);
|
|
595
|
+
members.push({
|
|
596
|
+
name,
|
|
597
|
+
nameStart,
|
|
598
|
+
valueEnd,
|
|
599
|
+
valueStart
|
|
600
|
+
});
|
|
601
|
+
i = valueEnd;
|
|
602
|
+
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
603
|
+
if (raw[i] === ",") i += 1;
|
|
604
|
+
else if (raw[i] !== "}") throw new Error("malformed member in settings.json");
|
|
605
|
+
}
|
|
606
|
+
return members;
|
|
607
|
+
}
|
|
608
|
+
function rootMemberValueSpan(raw, key) {
|
|
609
|
+
const member = rootMembers(raw).find((m) => m.name === key);
|
|
610
|
+
return member === void 0 ? null : [member.valueStart, member.valueEnd];
|
|
611
|
+
}
|
|
612
|
+
function removeMembers(raw, keys) {
|
|
613
|
+
let text = raw;
|
|
614
|
+
for (const key of keys) {
|
|
615
|
+
const members = rootMembers(text);
|
|
616
|
+
const at = members.findIndex((m) => m.name === key);
|
|
617
|
+
if (at === -1) throw new Error(`cannot find the "${key}" member to remove in settings.json`);
|
|
618
|
+
if (members.length === 1) return "{}";
|
|
619
|
+
const member = members[at];
|
|
620
|
+
if (at > 0) {
|
|
621
|
+
let start = member.nameStart;
|
|
622
|
+
while (/\s/.test(text[start - 1] ?? "~")) start -= 1;
|
|
623
|
+
text = `${text.slice(0, start - 1)}${text.slice(member.valueEnd)}`;
|
|
624
|
+
} else text = `${text.slice(0, member.nameStart)}${text.slice(members[1].nameStart)}`;
|
|
625
|
+
}
|
|
626
|
+
return text;
|
|
627
|
+
}
|
|
628
|
+
function insertMembers(raw, members) {
|
|
629
|
+
const close = raw.lastIndexOf("}");
|
|
630
|
+
if (close === -1) throw new Error("settings.json has no closing brace to splice into");
|
|
631
|
+
let at = close;
|
|
632
|
+
while (/\s/.test(raw[at - 1] ?? "~")) at -= 1;
|
|
633
|
+
const comma = raw.replace(/\s/g, "") === "{}" ? "" : ",";
|
|
634
|
+
return `${raw.slice(0, at)}${comma}\n ${renderMembers(members)}${raw.slice(at)}`;
|
|
635
|
+
}
|
|
636
|
+
function renderMembers(members) {
|
|
637
|
+
return members.map(([key, command]) => `"${key}": ${settingsValue(command)}`).join(",\n ");
|
|
638
|
+
}
|
|
639
|
+
/** Splice each rendered value over its root member. */
|
|
640
|
+
function repointRootMembers(raw, entries) {
|
|
641
|
+
let text = raw;
|
|
642
|
+
for (const { key, value } of entries) {
|
|
643
|
+
const span = rootMemberValueSpan(text, key);
|
|
644
|
+
if (span === null) throw new Error(`cannot find the "${key}" member in settings.json`);
|
|
645
|
+
text = `${text.slice(0, span[0])}${value}${text.slice(span[1])}`;
|
|
646
|
+
}
|
|
647
|
+
return text;
|
|
648
|
+
}
|
|
649
|
+
function splicedSettings(raw, plan) {
|
|
650
|
+
return repointRootMembers(plan.adds.length > 0 ? insertMembers(raw, plan.adds) : raw, plan.repoints.map(([key, command]) => ({
|
|
651
|
+
key,
|
|
652
|
+
value: settingsValue(command)
|
|
653
|
+
})));
|
|
654
|
+
}
|
|
655
|
+
function writeBackupIfAbsent(home, plan) {
|
|
656
|
+
const file = backupPath(home);
|
|
657
|
+
if (existsSync(file)) return;
|
|
658
|
+
const keys = {};
|
|
659
|
+
if (plan.raw !== null) for (const key of SETTINGS_KEYS) {
|
|
660
|
+
const span = rootMemberValueSpan(plan.raw, key);
|
|
661
|
+
if (span === null) continue;
|
|
662
|
+
const memberRaw = plan.raw.slice(span[0], span[1]);
|
|
663
|
+
if (isOurMember(key, JSON.parse(memberRaw))) continue;
|
|
664
|
+
keys[key] = memberRaw;
|
|
665
|
+
}
|
|
666
|
+
const dir = dirname(file);
|
|
667
|
+
mkdirSync(dir, { recursive: true });
|
|
668
|
+
const tmp = `${file}.tmp`;
|
|
669
|
+
writeFileSync(tmp, `${JSON.stringify({
|
|
670
|
+
createdFile: plan.raw === null,
|
|
671
|
+
keys
|
|
672
|
+
}, null, 2)}\n`);
|
|
673
|
+
renameSync(tmp, file);
|
|
674
|
+
}
|
|
675
|
+
function commitSettings(plan) {
|
|
676
|
+
if (plan.raw === null) {
|
|
677
|
+
mkdirSync(dirname(plan.file), { recursive: true });
|
|
678
|
+
const members = plan.adds.map(([key, command]) => `"${key}": ${settingsValue(command)}`).join(",\n ");
|
|
679
|
+
writeFileSync(plan.file, `{\n ${members}\n}\n`);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (plan.adds.length === 0 && plan.repoints.length === 0) return;
|
|
683
|
+
writeFileSync(plan.file, splicedSettings(plan.raw, plan));
|
|
684
|
+
}
|
|
685
|
+
function themeNamed(name, themes) {
|
|
686
|
+
const theme = themes[name];
|
|
687
|
+
if (theme === void 0) throw new Error(`unknown theme '${name}' — valid themes: ${THEME_NAMES.join(" ")}`);
|
|
688
|
+
return theme;
|
|
689
|
+
}
|
|
690
|
+
function resolveSelection(runtime, options) {
|
|
691
|
+
const variants = options.variants ?? {};
|
|
692
|
+
const themes = themesFor(runtime);
|
|
693
|
+
const theme = options.theme === void 0 ? void 0 : themeNamed(options.theme, themes);
|
|
694
|
+
const layout = options.layout ?? theme?.layout;
|
|
695
|
+
if (layout === void 0) throw new Error("no layout — pass --layout <spec> or --theme <name>");
|
|
696
|
+
const byItem = new Map(runtime.items.map((item) => [item.item, item]));
|
|
697
|
+
const items = layoutItems(layout, runtime.items.map((item) => item.item));
|
|
698
|
+
const values = {
|
|
699
|
+
...theme?.variants ?? {},
|
|
700
|
+
...variants
|
|
701
|
+
};
|
|
702
|
+
const missing = items.filter((item) => !(item in values));
|
|
703
|
+
if (missing.length > 0) {
|
|
704
|
+
const hints = missing.map((item) => `--${item} <alt>`).join(" ");
|
|
705
|
+
const names = missing.map((item) => `'${item}'`).join(" ");
|
|
706
|
+
const plural = missing.length > 1 ? "s" : "";
|
|
707
|
+
throw new Error(theme === void 0 ? `no variant for layout item${plural} ${names} — pass ${hints}, or name a theme: --theme ${THEME_NAMES.join("|")}` : `theme '${options.theme}' has no pick for layout item${plural} ${names} — pass ${hints}, or drop ${missing.length > 1 ? "them" : "it"} from the layout`);
|
|
708
|
+
}
|
|
709
|
+
for (const [item, alt] of Object.entries(values)) {
|
|
710
|
+
const entry = byItem.get(item);
|
|
711
|
+
if (entry === void 0) throw new Error(`unknown item '${item}' — valid items: ${[...byItem.keys()].join(" ")}`);
|
|
712
|
+
if (!entry.alternatives.includes(alt)) throw new Error(`unknown variant '${alt}' for item '${item}' — valid: ${entry.alternatives.join(" | ")}`);
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
layout,
|
|
716
|
+
ordered: [...items, ...runtime.items.map((entry) => entry.item).filter((item) => !items.includes(item) && item in values)],
|
|
717
|
+
values
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
function configure(options) {
|
|
721
|
+
const runtime = resolveRuntime({ home: options.home });
|
|
722
|
+
if (options.theme === void 0 && options.layout === void 0 && Object.keys(options.variants ?? {}).length === 0) throw new Error("no theme and no item flags — on a terminal run 'statusline configure' for the wizard; in Claude Code use the /lab skill");
|
|
723
|
+
const { layout, ordered, values } = resolveSelection(runtime, options);
|
|
724
|
+
const assignments = ordered.map((item) => `STATUSLINE_LAB_${item.toUpperCase()}=${values[item]}`);
|
|
725
|
+
const plan = planSettings(options.home, mainKeyValue(layout, assignments), options.force ?? false);
|
|
726
|
+
if (plan.adds.length > 0 || plan.repoints.length > 0) writeBackupIfAbsent(options.home, plan);
|
|
727
|
+
commitSettings(plan);
|
|
728
|
+
}
|
|
729
|
+
//#endregion
|
|
730
|
+
//#region src/demo-repo.ts
|
|
731
|
+
var NAME = "Lab";
|
|
732
|
+
var EMAIL = "lab@local";
|
|
733
|
+
var D_INIT = "2026-09-01T10:00:00+00:00";
|
|
734
|
+
var D_REMOTE = "2026-09-01T11:00:00+00:00";
|
|
735
|
+
var D_LOCAL1 = "2026-09-02T09:00:00+00:00";
|
|
736
|
+
var D_LOCAL2 = "2026-09-02T09:30:00+00:00";
|
|
737
|
+
var D_STASH = "2026-09-02T10:00:00+00:00";
|
|
738
|
+
var API_INIT = "export const api = {\n host: 'demo.local',\n};\n";
|
|
739
|
+
function materializeDemoRepo(home) {
|
|
740
|
+
const dir = join(home, "demo", "atlas-web");
|
|
741
|
+
for (const sub of [
|
|
742
|
+
"src",
|
|
743
|
+
"docs",
|
|
744
|
+
"scratch",
|
|
745
|
+
"scripts"
|
|
746
|
+
]) mkdirSync(join(dir, sub), { recursive: true });
|
|
747
|
+
function git(args, date) {
|
|
748
|
+
return execFileSync("git", [
|
|
749
|
+
"-c",
|
|
750
|
+
"commit.gpgsign=false",
|
|
751
|
+
...args
|
|
752
|
+
], {
|
|
753
|
+
cwd: dir,
|
|
754
|
+
encoding: "utf8",
|
|
755
|
+
stdio: [
|
|
756
|
+
"ignore",
|
|
757
|
+
"pipe",
|
|
758
|
+
"pipe"
|
|
759
|
+
],
|
|
760
|
+
env: {
|
|
761
|
+
PATH: process.env.PATH ?? "",
|
|
762
|
+
HOME: home,
|
|
763
|
+
LC_ALL: "C",
|
|
764
|
+
TZ: "UTC",
|
|
765
|
+
GIT_AUTHOR_NAME: NAME,
|
|
766
|
+
GIT_AUTHOR_EMAIL: EMAIL,
|
|
767
|
+
GIT_COMMITTER_NAME: NAME,
|
|
768
|
+
GIT_COMMITTER_EMAIL: EMAIL,
|
|
769
|
+
...date ? {
|
|
770
|
+
GIT_AUTHOR_DATE: date,
|
|
771
|
+
GIT_COMMITTER_DATE: date
|
|
772
|
+
} : {}
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
function write(rel, body) {
|
|
777
|
+
writeFileSync(join(dir, rel), body);
|
|
778
|
+
}
|
|
779
|
+
function commit(message, date) {
|
|
780
|
+
git(["add", "-A"]);
|
|
781
|
+
git([
|
|
782
|
+
"commit",
|
|
783
|
+
"-m",
|
|
784
|
+
message
|
|
785
|
+
], date);
|
|
786
|
+
}
|
|
787
|
+
git([
|
|
788
|
+
"init",
|
|
789
|
+
"-b",
|
|
790
|
+
"main"
|
|
791
|
+
]);
|
|
792
|
+
git([
|
|
793
|
+
"config",
|
|
794
|
+
"user.name",
|
|
795
|
+
NAME
|
|
796
|
+
]);
|
|
797
|
+
git([
|
|
798
|
+
"config",
|
|
799
|
+
"user.email",
|
|
800
|
+
EMAIL
|
|
801
|
+
]);
|
|
802
|
+
write("README.md", "atlas-web demo repo\n");
|
|
803
|
+
write("src/index.js", "export function start() {}\n");
|
|
804
|
+
write("src/api.js", API_INIT);
|
|
805
|
+
write("src/style.css", "body { margin: 0; }\n");
|
|
806
|
+
write("src/util.js", "export const id = (x) => x;\n");
|
|
807
|
+
commit("init", D_INIT);
|
|
808
|
+
write("src/api.js", `${API_INIT}// remote: pinned host\n`);
|
|
809
|
+
commit("remote1", D_REMOTE);
|
|
810
|
+
const originMain = git(["rev-parse", "HEAD"]).trim();
|
|
811
|
+
git([
|
|
812
|
+
"checkout",
|
|
813
|
+
"-b",
|
|
814
|
+
"feature/login-flow",
|
|
815
|
+
"HEAD~1"
|
|
816
|
+
]);
|
|
817
|
+
write("src/util.js", "export const id = (x) => x;\nexport const tag = (x) => x;\n");
|
|
818
|
+
commit("local1", D_LOCAL1);
|
|
819
|
+
write("src/render.js", "export function render() {}\n");
|
|
820
|
+
commit("local2", D_LOCAL2);
|
|
821
|
+
git([
|
|
822
|
+
"update-ref",
|
|
823
|
+
"refs/remotes/origin/main",
|
|
824
|
+
originMain
|
|
825
|
+
]);
|
|
826
|
+
git([
|
|
827
|
+
"config",
|
|
828
|
+
"remote.origin.url",
|
|
829
|
+
"../atlas-web.git"
|
|
830
|
+
]);
|
|
831
|
+
git([
|
|
832
|
+
"config",
|
|
833
|
+
"remote.origin.fetch",
|
|
834
|
+
"+refs/heads/*:refs/remotes/origin/*"
|
|
835
|
+
]);
|
|
836
|
+
git([
|
|
837
|
+
"config",
|
|
838
|
+
"branch.feature/login-flow.remote",
|
|
839
|
+
"origin"
|
|
840
|
+
]);
|
|
841
|
+
git([
|
|
842
|
+
"config",
|
|
843
|
+
"branch.feature/login-flow.merge",
|
|
844
|
+
"refs/heads/main"
|
|
845
|
+
]);
|
|
846
|
+
write("README.md", "atlas-web demo repo (wip)\n");
|
|
847
|
+
git([
|
|
848
|
+
"stash",
|
|
849
|
+
"push",
|
|
850
|
+
"-m",
|
|
851
|
+
"wip: context refactor",
|
|
852
|
+
"--",
|
|
853
|
+
"README.md"
|
|
854
|
+
], D_STASH);
|
|
855
|
+
write("README.md", "atlas-web demo repo\n\nstaged: pin the render loop\n");
|
|
856
|
+
write("src/index.js", "export function start() {\n return Promise.resolve();\n}\n");
|
|
857
|
+
git([
|
|
858
|
+
"add",
|
|
859
|
+
"README.md",
|
|
860
|
+
"src/index.js"
|
|
861
|
+
]);
|
|
862
|
+
write("src/api.js", `${API_INIT}// local tweak\n`);
|
|
863
|
+
write("src/style.css", "body { margin: 0; padding: 0; }\n");
|
|
864
|
+
write("docs/wip.txt", "");
|
|
865
|
+
write("scratch/todo.md", "");
|
|
866
|
+
write("scripts/deploy.sh", "");
|
|
867
|
+
write("note1.md", "");
|
|
868
|
+
write("note2.md", "");
|
|
869
|
+
return dir;
|
|
870
|
+
}
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/payloads.ts
|
|
873
|
+
var PAYLOADS_DIR = fileURLToPath(new URL("../assets/payloads", import.meta.url));
|
|
874
|
+
var TICKS_DIR = fileURLToPath(new URL("../assets/ticks", import.meta.url));
|
|
875
|
+
var P1_WARM_IN = 1920;
|
|
876
|
+
var P1_RESETS_IN = {
|
|
877
|
+
five_hour: 13830,
|
|
878
|
+
seven_day: 518400,
|
|
879
|
+
spend_limit: 950400
|
|
880
|
+
};
|
|
881
|
+
var PANEL_STEP_S = 300;
|
|
882
|
+
function anchoredP1(repoDir, now) {
|
|
883
|
+
const payload = JSON.parse(readFileSync(join(PAYLOADS_DIR, "p1.json"), "utf8"));
|
|
884
|
+
const cache = payload.prompt_cache;
|
|
885
|
+
if (isObject(cache)) {
|
|
886
|
+
if (cache.warm === true) {
|
|
887
|
+
cache.expires_at = now + P1_WARM_IN;
|
|
888
|
+
cache.last_miss_at = now + P1_WARM_IN - 3900;
|
|
889
|
+
} else {
|
|
890
|
+
cache.expires_at = now - 10;
|
|
891
|
+
delete cache.last_miss_at;
|
|
892
|
+
}
|
|
712
893
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
return member === void 0 ? null : [member.valueStart, member.valueEnd];
|
|
718
|
-
}
|
|
719
|
-
function removeMembers(raw, keys) {
|
|
720
|
-
let text = raw;
|
|
721
|
-
for (const key of keys) {
|
|
722
|
-
const members = rootMembers(text);
|
|
723
|
-
const at = members.findIndex((m) => m.name === key);
|
|
724
|
-
if (at === -1) throw new Error(`cannot find the "${key}" member to remove in settings.json`);
|
|
725
|
-
if (members.length === 1) return "{}";
|
|
726
|
-
const member = members[at];
|
|
727
|
-
if (at > 0) {
|
|
728
|
-
let start = member.nameStart;
|
|
729
|
-
while (/\s/.test(text[start - 1] ?? "~")) start -= 1;
|
|
730
|
-
text = `${text.slice(0, start - 1)}${text.slice(member.valueEnd)}`;
|
|
731
|
-
} else text = `${text.slice(0, member.nameStart)}${text.slice(members[1].nameStart)}`;
|
|
894
|
+
const limits = payload.rate_limits;
|
|
895
|
+
if (isObject(limits)) for (const [limitKey, offset] of Object.entries(P1_RESETS_IN)) {
|
|
896
|
+
const limit = limits[limitKey];
|
|
897
|
+
if (isObject(limit)) limit.resets_at = now + offset;
|
|
732
898
|
}
|
|
733
|
-
|
|
899
|
+
payload.workspace.current_dir = repoDir;
|
|
900
|
+
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
734
901
|
}
|
|
735
|
-
function
|
|
736
|
-
|
|
737
|
-
if (close === -1) throw new Error("settings.json has no closing brace to splice into");
|
|
738
|
-
let at = close;
|
|
739
|
-
while (/\s/.test(raw[at - 1] ?? "~")) at -= 1;
|
|
740
|
-
const comma = raw.replace(/\s/g, "") === "{}" ? "" : ",";
|
|
741
|
-
return `${raw.slice(0, at)}${comma}\n ${renderMembers(members)}${raw.slice(at)}`;
|
|
902
|
+
function hasStart(value) {
|
|
903
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
742
904
|
}
|
|
743
|
-
function
|
|
744
|
-
|
|
905
|
+
function anchoredTick(now) {
|
|
906
|
+
const tick = JSON.parse(readFileSync(join(TICKS_DIR, "multi.json"), "utf8"));
|
|
907
|
+
const tasks = Array.isArray(tick.tasks) ? tick.tasks : [];
|
|
908
|
+
let started = 0;
|
|
909
|
+
const staggered = {
|
|
910
|
+
...tick,
|
|
911
|
+
tasks: tasks.map((task) => {
|
|
912
|
+
if (!hasStart(task.startTime)) return task;
|
|
913
|
+
started += 1;
|
|
914
|
+
return {
|
|
915
|
+
...task,
|
|
916
|
+
startTime: now - PANEL_STEP_S * started
|
|
917
|
+
};
|
|
918
|
+
})
|
|
919
|
+
};
|
|
920
|
+
return `${JSON.stringify(staggered, null, 2)}\n`;
|
|
745
921
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
922
|
+
function previewSources(home, now) {
|
|
923
|
+
const mainCapture = capturePath(home, "main");
|
|
924
|
+
if (existsSync(mainCapture)) {
|
|
925
|
+
const tickCapture = capturePath(home, "tick");
|
|
926
|
+
return {
|
|
927
|
+
cleanup: () => void 0,
|
|
928
|
+
main: readFileSync(mainCapture, "utf8"),
|
|
929
|
+
tick: existsSync(tickCapture) ? readFileSync(tickCapture, "utf8") : anchoredTick(now)
|
|
930
|
+
};
|
|
753
931
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
932
|
+
const demoHome = mkdtempSync(join(tmpdir(), "statusline-preview-"));
|
|
933
|
+
return {
|
|
934
|
+
cleanup: () => {
|
|
935
|
+
rmSync(demoHome, {
|
|
936
|
+
recursive: true,
|
|
937
|
+
force: true
|
|
938
|
+
});
|
|
939
|
+
},
|
|
940
|
+
main: anchoredP1(materializeDemoRepo(demoHome), now),
|
|
941
|
+
tick: anchoredTick(now)
|
|
942
|
+
};
|
|
761
943
|
}
|
|
762
|
-
function
|
|
763
|
-
const
|
|
764
|
-
if (
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
const span = rootMemberValueSpan(plan.raw, key);
|
|
768
|
-
if (span === null) continue;
|
|
769
|
-
const memberRaw = plan.raw.slice(span[0], span[1]);
|
|
770
|
-
if (isOurMember(key, JSON.parse(memberRaw))) continue;
|
|
771
|
-
keys[key] = memberRaw;
|
|
772
|
-
}
|
|
773
|
-
const dir = dirname(file);
|
|
774
|
-
mkdirSync(dir, { recursive: true });
|
|
775
|
-
const tmp = `${file}.tmp`;
|
|
776
|
-
writeFileSync(tmp, `${JSON.stringify({
|
|
777
|
-
createdFile: plan.raw === null,
|
|
778
|
-
keys
|
|
779
|
-
}, null, 2)}\n`);
|
|
780
|
-
renameSync(tmp, file);
|
|
944
|
+
function firstPanelRow(output) {
|
|
945
|
+
const [row] = output.split("\n");
|
|
946
|
+
if (row === "") return "";
|
|
947
|
+
const content = JSON.parse(row).content;
|
|
948
|
+
return typeof content === "string" ? content : "";
|
|
781
949
|
}
|
|
782
|
-
function
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
950
|
+
function runtimeRenderer(spec) {
|
|
951
|
+
const run = spawnSync("bash", [spec.bin], {
|
|
952
|
+
input: spec.stdin,
|
|
953
|
+
env: {
|
|
954
|
+
PATH: process.env.PATH ?? "",
|
|
955
|
+
...spec.env,
|
|
956
|
+
LC_ALL: "C",
|
|
957
|
+
TZ: "UTC"
|
|
958
|
+
},
|
|
959
|
+
timeout: 3e4
|
|
960
|
+
});
|
|
961
|
+
if (run.status !== 0) throw new Error(`render failed: ${run.stderr.toString("utf8")}`);
|
|
962
|
+
const stdout = run.stdout.toString("utf8");
|
|
963
|
+
const warn = run.stderr.toString("utf8").trim();
|
|
964
|
+
return warn === "" ? stdout : `${stdout}${warn}\n`;
|
|
791
965
|
}
|
|
792
|
-
function renderPreview(
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
966
|
+
function renderPreview(bag) {
|
|
967
|
+
const variantEnv = Object.fromEntries(Object.entries(bag.values).map(([item, alt]) => [`STATUSLINE_LAB_${item.toUpperCase()}`, alt]));
|
|
968
|
+
const plain = bag.plain === true || (process.env.NO_COLOR ?? "") !== "";
|
|
969
|
+
const env = {
|
|
970
|
+
...variantEnv,
|
|
971
|
+
...plain ? { NO_COLOR: "1" } : {}
|
|
972
|
+
};
|
|
973
|
+
return {
|
|
974
|
+
line: runtimeRenderer({
|
|
975
|
+
bin: join(bag.runtime.dir, "statusline.sh"),
|
|
798
976
|
env: {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
977
|
+
...env,
|
|
978
|
+
COLUMNS: String(bag.width ?? 200),
|
|
979
|
+
HOME: bag.home,
|
|
980
|
+
NOW: bag.now,
|
|
981
|
+
STATUSLINE_LAB_LAYOUT: bag.layout
|
|
804
982
|
},
|
|
805
|
-
stdin:
|
|
806
|
-
}).replace(/\n+$/, "")
|
|
807
|
-
|
|
983
|
+
stdin: bag.main
|
|
984
|
+
}).replace(/\n+$/, ""),
|
|
985
|
+
panel: firstPanelRow(runtimeRenderer({
|
|
986
|
+
bin: join(bag.runtime.dir, "subagent.sh"),
|
|
808
987
|
env: {
|
|
809
|
-
|
|
810
|
-
HOME: home,
|
|
811
|
-
NOW: now
|
|
988
|
+
...env,
|
|
989
|
+
HOME: bag.home,
|
|
990
|
+
NOW: bag.now
|
|
812
991
|
},
|
|
813
|
-
stdin:
|
|
814
|
-
}).replace(/\n+$/, ""))
|
|
815
|
-
}
|
|
816
|
-
sources.cleanup();
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
function printedConfig(runtime, home) {
|
|
820
|
-
const existing = readKeyConfig(home);
|
|
821
|
-
return [...[`layout='${existing.layout ?? runtime.defaultLayout}'`, ...runtime.items.map((item) => `${item.item}=${existingValue(existing.values, item.item) ?? item.default}`)], "nothing written — pass variants (`statusline configure --model block`) or run bare on a TTY for the wizard"].join("\n");
|
|
822
|
-
}
|
|
823
|
-
function existingValue(values, item) {
|
|
824
|
-
return item in values ? values[item] : void 0;
|
|
992
|
+
stdin: bag.tick
|
|
993
|
+
}).replace(/\n+$/, ""))
|
|
994
|
+
};
|
|
825
995
|
}
|
|
826
|
-
|
|
996
|
+
//#endregion
|
|
997
|
+
//#region src/preview.ts
|
|
998
|
+
function preview(options) {
|
|
827
999
|
const runtime = resolveRuntime({ home: options.home });
|
|
828
|
-
const
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
let base;
|
|
845
|
-
if (options.fallback === "default") base = byItem.get(item)?.default;
|
|
846
|
-
else if (options.fallback === "existing") base = existingValue(existing.values, item);
|
|
847
|
-
else if (!explicit) base = existingValue(existing.values, item) ?? byItem.get(item)?.default;
|
|
848
|
-
if (base !== void 0) values[item] = base;
|
|
849
|
-
}
|
|
850
|
-
const missing = items.filter((item) => !(item in values) && !(item in variants));
|
|
851
|
-
if (missing.length > 0) throw new Error(`no variant for layout item${missing.length > 1 ? "s" : ""} ${missing.join(" ")} — pass --<item> <alt> for each or use --fallback=default|existing`);
|
|
852
|
-
for (const [item, alt] of Object.entries(variants)) values[item] = alt;
|
|
853
|
-
for (const item of items) {
|
|
854
|
-
const alt = values[item];
|
|
855
|
-
const offered = byItem.get(item)?.alternatives ?? [];
|
|
856
|
-
if (!offered.includes(alt)) throw new Error(`unknown variant '${alt}' for item '${item}' — valid: ${offered.join(" | ")}`);
|
|
857
|
-
}
|
|
858
|
-
if (options.dryRun) {
|
|
859
|
-
const now = String(Math.floor(Date.now() / 1e3));
|
|
860
|
-
return {
|
|
861
|
-
mode: "dry-run",
|
|
862
|
-
text: renderPreview(runtime, options.home, values, layout, now)
|
|
863
|
-
};
|
|
1000
|
+
const { layout, values } = resolveSelection(runtime, options);
|
|
1001
|
+
const sources = previewSources(options.home, Number(options.now));
|
|
1002
|
+
try {
|
|
1003
|
+
const { line, panel } = renderPreview({
|
|
1004
|
+
home: options.home,
|
|
1005
|
+
layout,
|
|
1006
|
+
main: sources.main,
|
|
1007
|
+
now: options.now,
|
|
1008
|
+
plain: options.plain ?? !process.stdout.isTTY,
|
|
1009
|
+
runtime,
|
|
1010
|
+
tick: sources.tick,
|
|
1011
|
+
values
|
|
1012
|
+
});
|
|
1013
|
+
return `preview at 200 columns — nothing written\n${line}\npanel ${panel}\n`;
|
|
1014
|
+
} finally {
|
|
1015
|
+
sources.cleanup();
|
|
864
1016
|
}
|
|
865
|
-
const assignments = items.map((item) => `STATUSLINE_LAB_${item.toUpperCase()}=${values[item]}`);
|
|
866
|
-
const plan = planSettings(options.home, mainKeyValue(layout, assignments), options.force ?? false);
|
|
867
|
-
if (plan.adds.length > 0 || plan.repoints.length > 0) writeBackupIfAbsent(options.home, plan);
|
|
868
|
-
commitSettings(plan);
|
|
869
|
-
return { mode: "written" };
|
|
870
1017
|
}
|
|
871
1018
|
//#endregion
|
|
872
1019
|
//#region src/restore.ts
|
|
@@ -998,8 +1145,8 @@ function keyState(key, value) {
|
|
|
998
1145
|
}
|
|
999
1146
|
function keyRow(key, state, detail = "") {
|
|
1000
1147
|
if (state.kind === "ours") return `${key}: ours${detail === "" ? "" : ` — ${detail}`}`;
|
|
1001
|
-
if (state.kind === "absent") return `${key}: absent — fix: rerun configure --
|
|
1002
|
-
return `${key}: foreign${state.command === null ? "" : ` (${state.command})`} — fix: rerun configure --force --
|
|
1148
|
+
if (state.kind === "absent") return `${key}: absent — fix: rerun configure --theme classic`;
|
|
1149
|
+
return `${key}: foreign${state.command === null ? "" : ` (${state.command})`} — fix: rerun configure --force --theme classic`;
|
|
1003
1150
|
}
|
|
1004
1151
|
function layoutItemsOf(layout) {
|
|
1005
1152
|
const items = [];
|
|
@@ -1040,10 +1187,9 @@ function runtimeRow(runtime) {
|
|
|
1040
1187
|
if (runtime === null) return `runtime: missing — fix: ${INSTALL_FIX}`;
|
|
1041
1188
|
return `runtime: ${basename(dirname(runtime.dir))} — ${runtime.items.length} items`;
|
|
1042
1189
|
}
|
|
1043
|
-
function configRow(findings
|
|
1190
|
+
function configRow(findings) {
|
|
1044
1191
|
if (findings.length === 0) return "config: no drift";
|
|
1045
|
-
|
|
1046
|
-
return `config: drift — ${findings.map(findingText).join(", ")} — fix: ${fix}`;
|
|
1192
|
+
return `config: drift — ${findings.map(findingText).join(", ")} — fix: rerun configure --theme classic`;
|
|
1047
1193
|
}
|
|
1048
1194
|
function backupRow(home) {
|
|
1049
1195
|
let backup;
|
|
@@ -1092,12 +1238,13 @@ function status(options) {
|
|
|
1092
1238
|
const main = keyState("statusLine", members.statusLine);
|
|
1093
1239
|
const subagent = keyState("subagentStatusLine", members.subagentStatusLine);
|
|
1094
1240
|
const config = readKeyConfig(options.home);
|
|
1241
|
+
const theme = runtime === null ? void 0 : liveTheme(config, runtime);
|
|
1095
1242
|
const findings = runtime === null || main.kind !== "ours" ? [] : driftFindings(config, runtime);
|
|
1096
1243
|
const rows = [
|
|
1097
1244
|
runtimeRow(runtime),
|
|
1098
1245
|
keyRow("statusLine", main, configDetail(config)),
|
|
1099
1246
|
keyRow("subagentStatusLine", subagent),
|
|
1100
|
-
...runtime !== null && main.kind === "ours" ? [configRow(findings,
|
|
1247
|
+
...runtime !== null && main.kind === "ours" ? [configRow(findings), ...theme === void 0 ? [] : [`theme: ${theme}`]] : [],
|
|
1101
1248
|
backupRow(options.home),
|
|
1102
1249
|
capturesRow(options.home)
|
|
1103
1250
|
];
|
|
@@ -1165,8 +1312,8 @@ function terminalDeps(input = process.stdin, output = process.stdout) {
|
|
|
1165
1312
|
render(frame) {
|
|
1166
1313
|
output.write(`\x1b[2J\x1b[H${frame}\n`);
|
|
1167
1314
|
},
|
|
1168
|
-
preview(
|
|
1169
|
-
return
|
|
1315
|
+
preview(bag) {
|
|
1316
|
+
return renderPreview(bag);
|
|
1170
1317
|
}
|
|
1171
1318
|
};
|
|
1172
1319
|
}
|
|
@@ -1177,41 +1324,54 @@ var WIDTHS = [
|
|
|
1177
1324
|
120,
|
|
1178
1325
|
200
|
|
1179
1326
|
];
|
|
1180
|
-
var
|
|
1327
|
+
var THEME_KEYMAP = "j/k focus · w width · enter pick · q cancel";
|
|
1328
|
+
var REFINE_KEYMAP = "j/k move · h/l variant · s none · t themes · w width · enter save · q cancel";
|
|
1329
|
+
function opensWizard(args, isTty) {
|
|
1330
|
+
return isTty && args.layout === void 0 && args.theme === void 0 && args.variants === void 0;
|
|
1331
|
+
}
|
|
1181
1332
|
async function createWizard(options, deps) {
|
|
1182
1333
|
const runtime = resolveRuntime({ home: options.home });
|
|
1183
|
-
const
|
|
1184
|
-
const
|
|
1334
|
+
const themes = themesFor(runtime);
|
|
1335
|
+
const names = Object.keys(themes);
|
|
1185
1336
|
const byItem = new Map(runtime.items.map((item) => [item.item, item]));
|
|
1186
|
-
const names = layoutItems(layout, runtime.items.map((item) => item.item));
|
|
1187
|
-
const offered = [];
|
|
1188
|
-
const draft = /* @__PURE__ */ new Map();
|
|
1189
|
-
for (const name of names) {
|
|
1190
|
-
const entry = byItem.get(name);
|
|
1191
|
-
if (entry === void 0) continue;
|
|
1192
|
-
offered.push({
|
|
1193
|
-
alternatives: entry.alternatives,
|
|
1194
|
-
item: name
|
|
1195
|
-
});
|
|
1196
|
-
draft.set(name, name in existing.values && entry.alternatives.includes(existing.values[name]) ? existing.values[name] : entry.default);
|
|
1197
|
-
}
|
|
1198
1337
|
const sources = previewSources(options.home, Number(options.now));
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1338
|
+
const tickBase = JSON.parse(sources.tick);
|
|
1339
|
+
let pass = "themes";
|
|
1340
|
+
let themeAt = 0;
|
|
1341
|
+
let draftLayout = "";
|
|
1342
|
+
let offered = [];
|
|
1343
|
+
const draft = /* @__PURE__ */ new Map();
|
|
1201
1344
|
let focus = 0;
|
|
1202
1345
|
let widthAt = 0;
|
|
1203
|
-
function
|
|
1204
|
-
return Object.fromEntries([...draft].map(([item, alt]) => [`STATUSLINE_LAB_${item.toUpperCase()}`, alt]));
|
|
1205
|
-
}
|
|
1206
|
-
function env() {
|
|
1346
|
+
function bag(layout, values) {
|
|
1207
1347
|
return {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1348
|
+
home: options.home,
|
|
1349
|
+
layout,
|
|
1350
|
+
main: sources.main,
|
|
1351
|
+
now: options.now,
|
|
1352
|
+
runtime,
|
|
1353
|
+
tick: `${JSON.stringify({
|
|
1354
|
+
...tickBase,
|
|
1355
|
+
columns: WIDTHS[widthAt]
|
|
1356
|
+
}, null, 2)}\n`,
|
|
1357
|
+
values,
|
|
1358
|
+
width: WIDTHS[widthAt]
|
|
1213
1359
|
};
|
|
1214
1360
|
}
|
|
1361
|
+
function enterRefine(name) {
|
|
1362
|
+
const theme = themes[name];
|
|
1363
|
+
draftLayout = theme.layout;
|
|
1364
|
+
offered = layoutItems(theme.layout, runtime.items.map((item) => item.item)).flatMap((item) => {
|
|
1365
|
+
const entry = byItem.get(item);
|
|
1366
|
+
return entry === void 0 ? [] : [{
|
|
1367
|
+
alternatives: entry.alternatives,
|
|
1368
|
+
item
|
|
1369
|
+
}];
|
|
1370
|
+
});
|
|
1371
|
+
draft.clear();
|
|
1372
|
+
for (const [item, alt] of Object.entries(theme.variants)) draft.set(item, alt);
|
|
1373
|
+
focus = 0;
|
|
1374
|
+
}
|
|
1215
1375
|
function cycle(delta) {
|
|
1216
1376
|
const focused = offered.at(focus);
|
|
1217
1377
|
if (focused === void 0 || focused.alternatives.length === 0) return;
|
|
@@ -1220,63 +1380,79 @@ async function createWizard(options, deps) {
|
|
|
1220
1380
|
draft.set(focused.item, alts[(at + delta + alts.length) % alts.length]);
|
|
1221
1381
|
}
|
|
1222
1382
|
function step(key) {
|
|
1223
|
-
|
|
1224
|
-
|
|
1383
|
+
const down = key === "\x1B[B" || key === "j";
|
|
1384
|
+
const up = key === "\x1B[A" || key === "k";
|
|
1385
|
+
if (key === "w") widthAt = (widthAt + 1) % WIDTHS.length;
|
|
1386
|
+
else if (pass === "themes") {
|
|
1387
|
+
if (down) themeAt = (themeAt + 1) % names.length;
|
|
1388
|
+
else if (up) themeAt = (themeAt + names.length - 1) % names.length;
|
|
1389
|
+
} else if (key === "t") return "themes";
|
|
1390
|
+
else if (down) focus = (focus + 1) % offered.length;
|
|
1391
|
+
else if (up) focus = (focus + offered.length - 1) % offered.length;
|
|
1225
1392
|
else if (key === "\x1B[C" || key === "l") cycle(1);
|
|
1226
1393
|
else if (key === "\x1B[D" || key === "h") cycle(-1);
|
|
1227
1394
|
else if (key === "s") {
|
|
1228
1395
|
const focused = offered.at(focus);
|
|
1229
1396
|
if (focused?.alternatives.includes("none")) draft.set(focused.item, "none");
|
|
1230
|
-
}
|
|
1397
|
+
}
|
|
1398
|
+
return pass;
|
|
1231
1399
|
}
|
|
1232
|
-
function
|
|
1400
|
+
function drawThemes() {
|
|
1401
|
+
const rows = [];
|
|
1402
|
+
let panel = "";
|
|
1403
|
+
for (let at = 0; at < names.length; at += 1) {
|
|
1404
|
+
const name = names[at];
|
|
1405
|
+
const theme = themes[name];
|
|
1406
|
+
const surfaces = deps.preview(bag(theme.layout, theme.variants));
|
|
1407
|
+
const focused = at === themeAt;
|
|
1408
|
+
if (focused) panel = surfaces.panel;
|
|
1409
|
+
rows.push(`${focused ? ">" : " "} ${name}: ${theme.summary}`);
|
|
1410
|
+
rows.push(` ${surfaces.line}`);
|
|
1411
|
+
}
|
|
1412
|
+
deps.render([
|
|
1413
|
+
`statusline configure · ${WIDTHS[widthAt]} columns (w cycles)`,
|
|
1414
|
+
"",
|
|
1415
|
+
...rows,
|
|
1416
|
+
"",
|
|
1417
|
+
`panel ${panel}`,
|
|
1418
|
+
"",
|
|
1419
|
+
THEME_KEYMAP
|
|
1420
|
+
].join("\n"));
|
|
1421
|
+
}
|
|
1422
|
+
function drawRefine() {
|
|
1233
1423
|
const focused = offered.at(focus);
|
|
1234
1424
|
if (focused === void 0) return;
|
|
1235
|
-
const
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
},
|
|
1242
|
-
stdin: sources.main
|
|
1243
|
-
}).replace(/\n+$/, ""));
|
|
1244
|
-
const line = deps.preview({
|
|
1245
|
-
bin: mainBin,
|
|
1246
|
-
env: env(),
|
|
1247
|
-
stdin: sources.main
|
|
1248
|
-
}).replace(/\n+$/, "");
|
|
1249
|
-
const tick = JSON.parse(sources.tick);
|
|
1250
|
-
tick.columns = WIDTHS[widthAt];
|
|
1251
|
-
const panel = deps.preview({
|
|
1252
|
-
bin: panelBin,
|
|
1253
|
-
env: {
|
|
1254
|
-
COLUMNS: String(WIDTHS[widthAt]),
|
|
1255
|
-
HOME: options.home,
|
|
1256
|
-
NOW: options.now
|
|
1257
|
-
},
|
|
1258
|
-
stdin: `${JSON.stringify(tick, null, 2)}\n`
|
|
1259
|
-
}).replace(/\n+$/, "");
|
|
1425
|
+
const values = Object.fromEntries(draft);
|
|
1426
|
+
const samples = focused.alternatives.map((alt) => deps.preview(bag(`{${focused.item}}`, {
|
|
1427
|
+
...values,
|
|
1428
|
+
[focused.item]: alt
|
|
1429
|
+
})).line);
|
|
1430
|
+
const full = deps.preview(bag(draftLayout, values));
|
|
1260
1431
|
const rows = offered.map((entry, at) => `${at === focus ? ">" : " "} ${entry.item.padEnd(9)} ${draft.get(entry.item)}`);
|
|
1261
1432
|
deps.render([
|
|
1262
1433
|
`statusline configure · ${WIDTHS[widthAt]} columns (w cycles)`,
|
|
1263
1434
|
"",
|
|
1264
|
-
` ${line}`,
|
|
1265
|
-
` panel ${
|
|
1435
|
+
` ${full.line}`,
|
|
1436
|
+
` panel ${full.panel}`,
|
|
1266
1437
|
"",
|
|
1267
1438
|
`${focused.item}: ${focused.alternatives.join(" | ")}`,
|
|
1268
1439
|
...focused.alternatives.map((alt, at) => ` ${alt === draft.get(focused.item) ? "*" : " "} ${samples[at]}`),
|
|
1269
1440
|
"",
|
|
1270
1441
|
...rows,
|
|
1271
1442
|
"",
|
|
1272
|
-
|
|
1443
|
+
REFINE_KEYMAP
|
|
1273
1444
|
].join("\n"));
|
|
1274
1445
|
}
|
|
1446
|
+
function draw() {
|
|
1447
|
+
if (pass === "themes") drawThemes();
|
|
1448
|
+
else drawRefine();
|
|
1449
|
+
}
|
|
1275
1450
|
function finish() {
|
|
1276
1451
|
try {
|
|
1277
1452
|
configure({
|
|
1453
|
+
force: options.force,
|
|
1278
1454
|
home: options.home,
|
|
1279
|
-
layout,
|
|
1455
|
+
layout: draftLayout,
|
|
1280
1456
|
variants: Object.fromEntries(draft)
|
|
1281
1457
|
});
|
|
1282
1458
|
} catch (e) {
|
|
@@ -1290,9 +1466,17 @@ async function createWizard(options, deps) {
|
|
|
1290
1466
|
const keys = deps.readKeys();
|
|
1291
1467
|
draw();
|
|
1292
1468
|
for await (const key of keys) {
|
|
1293
|
-
if (key === "\r")
|
|
1469
|
+
if (key === "\r") {
|
|
1470
|
+
if (pass === "themes") {
|
|
1471
|
+
enterRefine(names[themeAt]);
|
|
1472
|
+
pass = "refine";
|
|
1473
|
+
draw();
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
return finish();
|
|
1477
|
+
}
|
|
1294
1478
|
if (key === "q" || key === "") break;
|
|
1295
|
-
step(key);
|
|
1479
|
+
pass = step(key);
|
|
1296
1480
|
draw();
|
|
1297
1481
|
}
|
|
1298
1482
|
deps.render("cancelled — nothing written\n");
|
|
@@ -1322,28 +1506,39 @@ else if (parsed.version) console.log(VERSION);
|
|
|
1322
1506
|
else if (parsed.command === "catalog") run(() => {
|
|
1323
1507
|
console.log(catalog({
|
|
1324
1508
|
home: homeOf(parsed.home),
|
|
1325
|
-
items: parsed.items
|
|
1509
|
+
items: parsed.items,
|
|
1510
|
+
themes: parsed.themes
|
|
1326
1511
|
}));
|
|
1327
1512
|
});
|
|
1328
1513
|
else if (parsed.command === "configure") {
|
|
1329
1514
|
const home = homeOf(parsed.home);
|
|
1330
|
-
if (parsed
|
|
1515
|
+
if (opensWizard(parsed, process.stdin.isTTY)) {
|
|
1331
1516
|
if (await createWizard({
|
|
1517
|
+
force: parsed.force,
|
|
1332
1518
|
home,
|
|
1333
1519
|
now: String(Math.floor(Date.now() / 1e3))
|
|
1334
1520
|
}, terminalDeps()) === "save-failed") process.exitCode = 1;
|
|
1335
1521
|
} else run(() => {
|
|
1336
|
-
|
|
1337
|
-
dryRun: parsed.dryRun,
|
|
1338
|
-
fallback: parsed.fallback,
|
|
1522
|
+
configure({
|
|
1339
1523
|
force: parsed.force,
|
|
1340
1524
|
home,
|
|
1341
1525
|
layout: parsed.layout,
|
|
1526
|
+
theme: parsed.theme,
|
|
1342
1527
|
variants: parsed.variants
|
|
1343
1528
|
});
|
|
1344
|
-
console.log(
|
|
1529
|
+
console.log("configured — live on the next paint");
|
|
1345
1530
|
});
|
|
1346
|
-
} else if (parsed.command === "
|
|
1531
|
+
} else if (parsed.command === "preview") run(() => {
|
|
1532
|
+
console.log(preview({
|
|
1533
|
+
home: homeOf(parsed.home),
|
|
1534
|
+
layout: parsed.layout,
|
|
1535
|
+
now: String(Math.floor(Date.now() / 1e3)),
|
|
1536
|
+
plain: parsed.plain,
|
|
1537
|
+
theme: parsed.theme,
|
|
1538
|
+
variants: parsed.variants
|
|
1539
|
+
}));
|
|
1540
|
+
});
|
|
1541
|
+
else if (parsed.command === "restore") run(() => {
|
|
1347
1542
|
const result = restore({
|
|
1348
1543
|
dryRun: parsed.dryRun,
|
|
1349
1544
|
force: parsed.force,
|