@sushant-kum/ccstatus 0.0.0-stage → 0.1.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.
@@ -0,0 +1,1393 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ COLORS,
4
+ configPath,
5
+ importCcstatusline,
6
+ isValidWhen,
7
+ loadConfigFile,
8
+ readSnapshot,
9
+ registry,
10
+ render,
11
+ rendererBg,
12
+ rendererColor,
13
+ saveConfigFile,
14
+ snapshotPath
15
+ } from "./chunk-XYVU45PI.js";
16
+
17
+ // src/app.tsx
18
+ import { Box as Box12, Text as Text12, useApp, useInput as useInput10 } from "ink";
19
+ import { useEffect, useMemo as useMemo2, useRef, useState as useState10 } from "react";
20
+
21
+ // src/mod-status.ts
22
+ import { execFile } from "child_process";
23
+ var MARKETPLACE_SLUG = "sushant-kum/ccstatus";
24
+ var PLUGIN_ID = "ccstatus@ccstatus";
25
+ var runCmd = (argv) => new Promise((resolve) => {
26
+ const [command, ...args] = argv;
27
+ if (command === void 0) {
28
+ resolve({ code: -1, stdout: "", stderr: "" });
29
+ return;
30
+ }
31
+ execFile(command, args, { timeout: 1e4, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
32
+ const errCode = err?.code;
33
+ const code = typeof errCode === "number" ? errCode : err ? -1 : 0;
34
+ resolve({ code, stdout: stdout ?? "", stderr: stderr ?? "" });
35
+ });
36
+ });
37
+ async function detectModStatus(run = runCmd) {
38
+ let res;
39
+ try {
40
+ res = await run(["claude", "plugin", "list", "--json"]);
41
+ } catch {
42
+ return "unknown";
43
+ }
44
+ if (res.code !== 0) {
45
+ return "unknown";
46
+ }
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(res.stdout);
50
+ } catch {
51
+ return "unknown";
52
+ }
53
+ if (!Array.isArray(parsed)) {
54
+ return "unknown";
55
+ }
56
+ const entry = parsed.find(
57
+ (p) => !!p && typeof p === "object" && typeof p.id === "string" && p.id.startsWith("ccstatus@")
58
+ );
59
+ if (!entry) {
60
+ return "absent";
61
+ }
62
+ return entry.enabled === false ? "disabled" : "installed";
63
+ }
64
+ var firstLine = (s) => s.split("\n").map((l) => l.trim()).find(Boolean) ?? "";
65
+ async function installMod(run = runCmd) {
66
+ const add = await run(["claude", "plugin", "marketplace", "add", MARKETPLACE_SLUG]);
67
+ if (add.code !== 0) {
68
+ return {
69
+ ok: false,
70
+ message: `marketplace add failed: ${firstLine(add.stderr || add.stdout) || `exit ${add.code}`}`
71
+ };
72
+ }
73
+ const inst = await run(["claude", "plugin", "install", PLUGIN_ID, "-y"]);
74
+ if (inst.code !== 0) {
75
+ return {
76
+ ok: false,
77
+ message: `install failed: ${firstLine(inst.stderr || inst.stdout) || `exit ${inst.code}`}`
78
+ };
79
+ }
80
+ return {
81
+ ok: true,
82
+ message: "mod installed \u2014 restart Claude Code (or run /reload-plugins) to see the bar"
83
+ };
84
+ }
85
+
86
+ // src/preview.tsx
87
+ import { Box as Box2, Text as Text2 } from "ink";
88
+
89
+ // src/paint.tsx
90
+ import { Box, Text } from "ink";
91
+ import { jsx } from "react/jsx-runtime";
92
+ var inkColor = (fg) => rendererColor(fg);
93
+ var inkBg = (bg) => rendererBg(bg);
94
+ function paintModel(model) {
95
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: model.lines.map((line, i) => /* @__PURE__ */ jsx(Box, { children: line.segments.map((s, j) => /* @__PURE__ */ jsx(Text, { color: inkColor(s.fg), backgroundColor: inkBg(s.bg), children: s.text }, j)) }, i)) });
96
+ }
97
+
98
+ // src/preview.tsx
99
+ import { jsxs } from "react/jsx-runtime";
100
+ function Preview({
101
+ config,
102
+ snapshot,
103
+ source,
104
+ width,
105
+ surface = "band"
106
+ }) {
107
+ const model = render(config, snapshot, { surface, width });
108
+ return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
109
+ /* @__PURE__ */ jsxs(Text2, { color: "gray", children: [
110
+ "live preview \xB7 ",
111
+ surface,
112
+ " \xB7 ",
113
+ width,
114
+ " cols \xB7 ",
115
+ source,
116
+ " data"
117
+ ] }),
118
+ paintModel(model)
119
+ ] });
120
+ }
121
+
122
+ // src/screens/defaults.tsx
123
+ import { Box as Box3, Text as Text3, useInput } from "ink";
124
+ import { useState } from "react";
125
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
126
+ var SEPARATORS = ["powerline", "space", "none"];
127
+ var FIELDS = ["separator", "padding"];
128
+ function Defaults({ config, setConfig, goHome }) {
129
+ const [focus, setFocus] = useState(0);
130
+ const d = config.defaults;
131
+ const cycle = (dir) => {
132
+ const field = FIELDS[focus];
133
+ if (field === "separator") {
134
+ const i = SEPARATORS.indexOf(d.separator);
135
+ const next = SEPARATORS[(i + dir + SEPARATORS.length) % SEPARATORS.length] ?? d.separator;
136
+ setConfig({ ...config, defaults: { ...d, separator: next } });
137
+ } else {
138
+ const next = Math.min(4, Math.max(0, d.padding + dir));
139
+ if (next !== d.padding) {
140
+ setConfig({ ...config, defaults: { ...d, padding: next } });
141
+ }
142
+ }
143
+ };
144
+ useInput((_i, key) => {
145
+ if (key.escape) {
146
+ goHome();
147
+ } else if (key.upArrow) {
148
+ setFocus((f) => Math.max(0, f - 1));
149
+ } else if (key.downArrow) {
150
+ setFocus((f) => Math.min(FIELDS.length - 1, f + 1));
151
+ } else if (key.leftArrow) {
152
+ cycle(-1);
153
+ } else if (key.rightArrow) {
154
+ cycle(1);
155
+ }
156
+ });
157
+ const row = (idx, name, value) => /* @__PURE__ */ jsxs2(Text3, { color: focus === idx ? "cyan" : void 0, children: [
158
+ focus === idx ? "\u25B8 " : " ",
159
+ name,
160
+ ": ",
161
+ value
162
+ ] });
163
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
164
+ /* @__PURE__ */ jsx2(Text3, { bold: true, children: "Global defaults" }),
165
+ row(0, "separator", d.separator),
166
+ row(1, "padding", String(d.padding)),
167
+ /* @__PURE__ */ jsxs2(Text3, { color: "gray", children: [
168
+ " align: ",
169
+ d.align,
170
+ " (stored, not yet applied)"
171
+ ] }),
172
+ /* @__PURE__ */ jsx2(Text3, { dimColor: true, children: "\u2191/\u2193 field \xB7 \u2190/\u2192 change \xB7 esc back" })
173
+ ] });
174
+ }
175
+
176
+ // src/screens/import.tsx
177
+ import { readFileSync } from "fs";
178
+ import { homedir } from "os";
179
+ import { join } from "path";
180
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
181
+ import { useMemo, useState as useState2 } from "react";
182
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
183
+ function defaultReadFile(path) {
184
+ try {
185
+ return readFileSync(path, "utf8");
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+ function detectCcstatuslinePath() {
191
+ const base = process.env["XDG_CONFIG_HOME"] || join(homedir(), ".config");
192
+ return join(base, "ccstatusline", "settings.json");
193
+ }
194
+ function load(path, readFile) {
195
+ if (!path.trim()) {
196
+ return { ok: false, message: "enter a path" };
197
+ }
198
+ let text;
199
+ try {
200
+ text = readFile(path);
201
+ } catch {
202
+ text = null;
203
+ }
204
+ if (text === null) {
205
+ return { ok: false, message: `cannot read ${path}` };
206
+ }
207
+ let parsed;
208
+ try {
209
+ parsed = JSON.parse(text);
210
+ } catch {
211
+ return { ok: false, message: `invalid JSON in ${path}` };
212
+ }
213
+ try {
214
+ const r = importCcstatusline(parsed);
215
+ if (r.warnings.some((w) => w.startsWith("input is not a ccstatusline config"))) {
216
+ return { ok: false, message: "not a ccstatusline config" };
217
+ }
218
+ return { ok: true, imported: r.config, warnings: r.warnings };
219
+ } catch {
220
+ return { ok: false, message: "import failed" };
221
+ }
222
+ }
223
+ function Import({
224
+ config,
225
+ setConfig,
226
+ goHome,
227
+ readFile = defaultReadFile
228
+ }) {
229
+ const detected = useMemo(detectCcstatuslinePath, []);
230
+ const [focus, setFocus] = useState2(0);
231
+ const [custom, setCustom] = useState2("");
232
+ const path = focus === 0 ? detected : custom;
233
+ const result = useMemo(() => load(path, readFile), [path, readFile]);
234
+ useInput2((input, key) => {
235
+ if (key.escape) {
236
+ goHome();
237
+ return;
238
+ }
239
+ if (key.upArrow) {
240
+ setFocus(0);
241
+ return;
242
+ }
243
+ if (key.downArrow) {
244
+ setFocus(1);
245
+ return;
246
+ }
247
+ if (key.return) {
248
+ if (result.ok) {
249
+ setConfig({
250
+ ...config,
251
+ surfaces: { ...config.surfaces, band: result.imported.surfaces.band }
252
+ });
253
+ }
254
+ return;
255
+ }
256
+ if (focus === 1) {
257
+ if (key.backspace || key.delete) {
258
+ setCustom((c) => c.slice(0, -1));
259
+ } else if (input && !key.ctrl && !key.meta) {
260
+ setCustom((c) => c + input);
261
+ }
262
+ }
263
+ });
264
+ const dropped = result.ok ? result.warnings.length : 0;
265
+ const items = result.ok ? result.imported.surfaces.band.lines.map((l) => l.map((i) => i.type).join(", ")) : [];
266
+ const mapped = items.reduce((n, l) => n + (l ? l.split(", ").length : 0), 0);
267
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
268
+ /* @__PURE__ */ jsx3(Text4, { bold: true, children: "Import from ccstatusline" }),
269
+ /* @__PURE__ */ jsxs3(Text4, { color: focus === 0 ? "cyan" : void 0, children: [
270
+ focus === 0 ? "\u25B8 " : " ",
271
+ "detected: ",
272
+ detected
273
+ ] }),
274
+ /* @__PURE__ */ jsxs3(Text4, { color: focus === 1 ? "cyan" : void 0, children: [
275
+ focus === 1 ? "\u25B8 " : " ",
276
+ "custom path: ",
277
+ custom,
278
+ focus === 1 ? "\u258F" : ""
279
+ ] }),
280
+ result.ok ? /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
281
+ /* @__PURE__ */ jsxs3(Text4, { children: [
282
+ "mapped ",
283
+ mapped,
284
+ " item(s), dropped ",
285
+ dropped
286
+ ] }),
287
+ items.map((l, i) => /* @__PURE__ */ jsxs3(Text4, { children: [
288
+ " ",
289
+ "line ",
290
+ i + 1,
291
+ ": ",
292
+ l
293
+ ] }, i)),
294
+ result.warnings.map((w, i) => /* @__PURE__ */ jsxs3(Text4, { color: "yellow", children: [
295
+ " ",
296
+ "! ",
297
+ w
298
+ ] }, i))
299
+ ] }) : /* @__PURE__ */ jsxs3(Text4, { color: "yellow", children: [
300
+ "warning: ",
301
+ result.message
302
+ ] }),
303
+ /* @__PURE__ */ jsx3(Text4, { dimColor: true, children: "\u2191/\u2193 source \xB7 \u23CE import (replaces band) \xB7 esc back" })
304
+ ] });
305
+ }
306
+
307
+ // src/screens/items.tsx
308
+ import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
309
+ import { useState as useState3 } from "react";
310
+
311
+ // src/named-colors.ts
312
+ var all = [...COLORS];
313
+ var NAMED_COLORS = [
314
+ "(none)",
315
+ ...all.filter((c) => !c.startsWith("bg")),
316
+ ...all.filter((c) => c.startsWith("bg"))
317
+ ];
318
+
319
+ // src/screens/items.tsx
320
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
321
+ var SURFACES = ["band", "statusline", "pane"];
322
+ var FIELDS2 = ["fg", "bg", "raw", "merge", "align"];
323
+ var ALIGNS = ["left", "center", "right"];
324
+ var FG = NAMED_COLORS.filter((c) => !c.startsWith("bg"));
325
+ var BG = ["(none)", ...NAMED_COLORS.filter((c) => c.startsWith("bg"))];
326
+ var WIDGETS = Object.values(registry);
327
+ var wrap = (i, n) => (i + n) % n;
328
+ var isStylable = (item) => item.type !== "flex-separator";
329
+ function Chip({ item, selected }) {
330
+ const fg = isStylable(item) ? item.fg : void 0;
331
+ const bg = isStylable(item) ? item.bg : void 0;
332
+ return /* @__PURE__ */ jsxs4(Text5, { inverse: selected, color: inkColor(fg), backgroundColor: inkBg(bg), children: [
333
+ "[ ",
334
+ item.type,
335
+ " ]"
336
+ ] });
337
+ }
338
+ function Swatch({ name, bg }) {
339
+ if (!name) {
340
+ return /* @__PURE__ */ jsx4(Text5, { dimColor: true, children: "(none)" });
341
+ }
342
+ return /* @__PURE__ */ jsxs4(Text5, { color: bg ? void 0 : inkColor(name), backgroundColor: bg ? inkBg(name) : void 0, children: [
343
+ " ",
344
+ name,
345
+ " "
346
+ ] });
347
+ }
348
+ function Items({ config, setConfig, goHome }) {
349
+ const [surface, setSurface] = useState3("band");
350
+ const [sel, setSel] = useState3(0);
351
+ const [mode, setMode] = useState3("strip");
352
+ const [field, setField] = useState3(0);
353
+ const [pick, setPick] = useState3(0);
354
+ const items = config.surfaces[surface].lines[0] ?? [];
355
+ const cur = items[Math.min(sel, items.length - 1)];
356
+ const idx = cur ? items.indexOf(cur) : -1;
357
+ const commit = (next) => {
358
+ const s = config.surfaces[surface];
359
+ const lines = [next, ...s.lines.slice(1)];
360
+ const c = { ...config, surfaces: { ...config.surfaces, [surface]: { ...s, lines } } };
361
+ setConfig(c);
362
+ };
363
+ const patch = (p) => {
364
+ const base = items[idx];
365
+ if (idx < 0 || !base || !isStylable(base)) {
366
+ return;
367
+ }
368
+ const item = { ...base, ...p };
369
+ for (const k of Object.keys(p)) {
370
+ if (item[k] === void 0) {
371
+ delete item[k];
372
+ }
373
+ }
374
+ commit(items.map((it, i) => i === idx ? item : it));
375
+ };
376
+ const cycleColor = (list, cur2, dir) => {
377
+ const n = list[wrap(Math.max(0, list.indexOf(cur2 ?? "(none)")) + dir + list.length, list.length)] ?? "(none)";
378
+ return n === "(none)" ? void 0 : n;
379
+ };
380
+ const change = (dir) => {
381
+ if (!cur || !isStylable(cur)) {
382
+ return;
383
+ }
384
+ const f = FIELDS2[field];
385
+ if (f === "fg") {
386
+ patch({ fg: cycleColor(FG, cur.fg, dir) });
387
+ } else if (f === "bg") {
388
+ patch({ bg: cycleColor(BG, cur.bg, dir) });
389
+ } else if (f === "raw") {
390
+ patch({ rawValue: !cur.rawValue });
391
+ } else if (f === "merge") {
392
+ patch({ merge: !cur.merge });
393
+ } else {
394
+ patch({ align: ALIGNS[wrap(ALIGNS.indexOf(cur.align ?? "left") + dir + 3, 3)] ?? "left" });
395
+ }
396
+ };
397
+ const move = (dir) => {
398
+ const j = idx + dir;
399
+ if (idx < 0 || j < 0 || j >= items.length) {
400
+ return;
401
+ }
402
+ const next = [...items];
403
+ const a = next[idx];
404
+ const b = next[j];
405
+ if (a === void 0 || b === void 0) {
406
+ return;
407
+ }
408
+ next[idx] = b;
409
+ next[j] = a;
410
+ commit(next);
411
+ setSel(j);
412
+ };
413
+ useInput3((input, key) => {
414
+ if (mode === "picker") {
415
+ if (key.escape) {
416
+ setMode("strip");
417
+ } else if (key.upArrow) {
418
+ setPick((p) => wrap(p - 1 + WIDGETS.length, WIDGETS.length));
419
+ } else if (key.downArrow) {
420
+ setPick((p) => wrap(p + 1, WIDGETS.length));
421
+ } else if (key.return) {
422
+ const picked = WIDGETS[pick];
423
+ if (picked) {
424
+ const type = picked.type;
425
+ const id = `${type}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
426
+ const newItem = type === "custom-text" ? { id, type, text: "" } : type === "custom-command" ? { id, type, command: "" } : type === "flex-separator" ? { id, type } : { id, type };
427
+ commit([...items, newItem]);
428
+ setSel(items.length);
429
+ setMode("strip");
430
+ }
431
+ }
432
+ } else if (mode === "editor") {
433
+ if (key.escape) {
434
+ setMode("strip");
435
+ } else if (key.upArrow) {
436
+ setField((f) => Math.max(0, f - 1));
437
+ } else if (key.downArrow) {
438
+ setField((f) => Math.min(FIELDS2.length - 1, f + 1));
439
+ } else if (key.leftArrow) {
440
+ change(-1);
441
+ } else if (key.rightArrow) {
442
+ change(1);
443
+ }
444
+ } else {
445
+ if (key.escape) {
446
+ goHome();
447
+ } else if (key.tab) {
448
+ setSurface((s) => SURFACES[wrap(SURFACES.indexOf(s) + 1, SURFACES.length)] ?? s);
449
+ setSel(0);
450
+ } else if (key.leftArrow) {
451
+ setSel(Math.max(0, idx - 1));
452
+ } else if (key.rightArrow) {
453
+ setSel(Math.min(items.length - 1, idx + 1));
454
+ } else if (input === "[") {
455
+ move(-1);
456
+ } else if (input === "]") {
457
+ move(1);
458
+ } else if (input === "a") {
459
+ setPick(0);
460
+ setMode("picker");
461
+ } else if (input === "x" && idx >= 0) {
462
+ commit(items.filter((_, i) => i !== idx));
463
+ setSel(Math.max(0, idx - 1));
464
+ } else if (input === "e" && idx >= 0) {
465
+ setField(0);
466
+ setMode("editor");
467
+ }
468
+ }
469
+ });
470
+ const row = (i, name, value) => /* @__PURE__ */ jsxs4(Box5, { children: [
471
+ /* @__PURE__ */ jsxs4(Text5, { color: mode === "editor" && field === i ? "cyan" : void 0, children: [
472
+ mode === "editor" && field === i ? "\u25B8 " : " ",
473
+ name,
474
+ ":",
475
+ " "
476
+ ] }),
477
+ value
478
+ ] }, name);
479
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
480
+ /* @__PURE__ */ jsx4(Box5, { children: SURFACES.map((s) => /* @__PURE__ */ jsxs4(Text5, { bold: s === surface, color: s === surface ? "cyan" : void 0, children: [
481
+ " ",
482
+ s === surface ? `[${s}]` : s,
483
+ " "
484
+ ] }, s)) }),
485
+ /* @__PURE__ */ jsx4(Box5, { children: items.length === 0 ? /* @__PURE__ */ jsx4(Text5, { dimColor: true, children: "(no items \u2014 press a)" }) : items.map((it, i) => /* @__PURE__ */ jsx4(Chip, { item: it, selected: i === idx }, it.id)) }),
486
+ mode === "picker" ? /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, children: [
487
+ /* @__PURE__ */ jsx4(Text5, { bold: true, children: "Add widget" }),
488
+ WIDGETS.map((w, i) => /* @__PURE__ */ jsxs4(Text5, { color: i === pick ? "cyan" : void 0, children: [
489
+ i === pick ? "\u25B8 " : " ",
490
+ w.type,
491
+ " ",
492
+ /* @__PURE__ */ jsx4(Text5, { dimColor: true, children: w.label })
493
+ ] }, w.type))
494
+ ] }) : cur && (isStylable(cur) ? /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, borderStyle: "round", paddingX: 1, children: [
495
+ /* @__PURE__ */ jsxs4(Text5, { children: [
496
+ "type: ",
497
+ cur.type
498
+ ] }),
499
+ row(0, "fg", /* @__PURE__ */ jsx4(Swatch, { name: cur.fg })),
500
+ row(1, "bg", /* @__PURE__ */ jsx4(Swatch, { name: cur.bg, bg: true })),
501
+ row(2, "raw", /* @__PURE__ */ jsx4(Text5, { children: cur.rawValue ? "on" : "off" })),
502
+ row(3, "merge", /* @__PURE__ */ jsx4(Text5, { children: cur.merge ? "on" : "off" })),
503
+ row(4, "align", /* @__PURE__ */ jsx4(Text5, { children: cur.align ?? "left" }))
504
+ ] }) : /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginTop: 1, borderStyle: "round", paddingX: 1, children: [
505
+ /* @__PURE__ */ jsxs4(Text5, { children: [
506
+ "type: ",
507
+ cur.type
508
+ ] }),
509
+ /* @__PURE__ */ jsx4(Text5, { dimColor: true, children: "(flex separator \u2014 no style options)" })
510
+ ] })),
511
+ /* @__PURE__ */ jsx4(Text5, { dimColor: true, children: mode === "strip" ? "\u2190/\u2192 select \xB7 [/] move \xB7 a add \xB7 x remove \xB7 e edit \xB7 tab surface \xB7 esc back" : mode === "editor" ? "\u2191/\u2193 field \xB7 \u2190/\u2192 change \xB7 esc done" : "\u2191/\u2193 choose \xB7 enter add \xB7 esc cancel" })
512
+ ] });
513
+ }
514
+
515
+ // src/screens/menu.tsx
516
+ import { Box as Box6, Text as Text6, useInput as useInput4 } from "ink";
517
+ import { useState as useState4 } from "react";
518
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
519
+ var ITEMS = [
520
+ ["items", "Edit items"],
521
+ ["themes", "Themes"],
522
+ ["powerline", "Powerline & separators"],
523
+ ["surfaces", "Surfaces"],
524
+ ["statusline", "Native status line"],
525
+ ["defaults", "Global defaults"],
526
+ ["preview", "Preview"],
527
+ ["import", "Import from ccstatusline"],
528
+ ["save", "Save & quit"],
529
+ ["quit", "Quit without saving"]
530
+ ];
531
+ function Menu({ onSelect }) {
532
+ const [i, setI] = useState4(0);
533
+ useInput4((input, key) => {
534
+ if (key.upArrow) {
535
+ setI((v) => Math.max(0, v - 1));
536
+ } else if (key.downArrow) {
537
+ setI((v) => Math.min(ITEMS.length - 1, v + 1));
538
+ } else if (key.return) {
539
+ const entry = ITEMS[i];
540
+ if (entry) {
541
+ onSelect(entry[0]);
542
+ }
543
+ }
544
+ });
545
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
546
+ /* @__PURE__ */ jsx5(Text6, { bold: true, children: "ccstatus \xB7 configurator" }),
547
+ ITEMS.map(([id, label], idx) => /* @__PURE__ */ jsxs5(Text6, { color: idx === i ? "cyan" : void 0, children: [
548
+ idx === i ? "\u25B8 " : " ",
549
+ label
550
+ ] }, id))
551
+ ] });
552
+ }
553
+
554
+ // src/screens/powerline.tsx
555
+ import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
556
+ import { useState as useState5 } from "react";
557
+
558
+ // src/separators.ts
559
+ var SEPARATOR_PRESETS = [
560
+ { char: "\uE0B0", name: "Triangle Right", code: "U+E0B0" },
561
+ { char: "\uE0B2", name: "Triangle Left", code: "U+E0B2" },
562
+ { char: "\uE0B4", name: "Round Right", code: "U+E0B4" },
563
+ { char: "\uE0B6", name: "Round Left", code: "U+E0B6" },
564
+ { char: "\uE0BA", name: "Lower Triangle", code: "U+E0BA" },
565
+ { char: "\uE0BE", name: "Diagonal", code: "U+E0BE" }
566
+ ];
567
+
568
+ // src/screens/powerline.tsx
569
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
570
+ var MODES = ["powerline", "space", "none"];
571
+ var CUSTOM = SEPARATOR_PRESETS.length;
572
+ var SEP_ROW = 0;
573
+ var GLYPH_ROW = (i) => 1 + i;
574
+ var INVERT_ROW = 1 + SEPARATOR_PRESETS.length + 1;
575
+ var LAST_ROW = INVERT_ROW;
576
+ var codeOf = (g) => {
577
+ const cp = g.codePointAt(0);
578
+ return cp === void 0 ? "" : "U+" + cp.toString(16).toUpperCase().padStart(4, "0");
579
+ };
580
+ function Powerline({ config, setConfig, goHome }) {
581
+ const [focus, setFocus] = useState5(0);
582
+ const [editing, setEditing] = useState5(false);
583
+ const [draft, setDraft] = useState5("");
584
+ const d = config.defaults;
585
+ const update = (patch) => setConfig({ ...config, defaults: { ...d, ...patch } });
586
+ const isPreset = SEPARATOR_PRESETS.some((p) => p.char === d.glyph);
587
+ const hasCustom = d.glyph !== "" && !isPreset;
588
+ const activate = () => {
589
+ if (focus === INVERT_ROW) {
590
+ update({ invert: !d.invert });
591
+ } else if (focus > SEP_ROW) {
592
+ const i = focus - 1;
593
+ if (i === CUSTOM) {
594
+ setDraft("");
595
+ setEditing(true);
596
+ } else {
597
+ const preset = SEPARATOR_PRESETS[i];
598
+ if (preset) {
599
+ update({ glyph: preset.char });
600
+ }
601
+ }
602
+ }
603
+ };
604
+ useInput5((input, key) => {
605
+ if (editing) {
606
+ if (key.escape) {
607
+ setEditing(false);
608
+ } else if (key.return) {
609
+ if (draft) {
610
+ update({ glyph: draft });
611
+ }
612
+ setEditing(false);
613
+ } else if (key.backspace || key.delete) {
614
+ setDraft((t) => Array.from(t).slice(0, -1).join(""));
615
+ } else if (input && !key.ctrl && !key.meta) {
616
+ setDraft((t) => t + input);
617
+ }
618
+ return;
619
+ }
620
+ if (key.escape) {
621
+ goHome();
622
+ } else if (key.upArrow) {
623
+ setFocus((f) => Math.max(0, f - 1));
624
+ } else if (key.downArrow) {
625
+ setFocus((f) => Math.min(LAST_ROW, f + 1));
626
+ } else if ((key.leftArrow || key.rightArrow) && focus === SEP_ROW) {
627
+ const dir = key.rightArrow ? 1 : -1;
628
+ update({
629
+ separator: MODES[(MODES.indexOf(d.separator) + dir + MODES.length) % MODES.length] ?? d.separator
630
+ });
631
+ } else if (key.return || input === " ") {
632
+ activate();
633
+ }
634
+ });
635
+ const cursor = (idx) => focus === idx ? "\u25B8 " : " ";
636
+ const color = (idx) => focus === idx ? "cyan" : void 0;
637
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
638
+ /* @__PURE__ */ jsx6(Text7, { bold: true, children: "Powerline" }),
639
+ /* @__PURE__ */ jsxs6(Text7, { color: color(SEP_ROW), children: [
640
+ cursor(SEP_ROW),
641
+ "separator: ",
642
+ d.separator
643
+ ] }),
644
+ /* @__PURE__ */ jsx6(Text7, { dimColor: true, children: " glyph" }),
645
+ SEPARATOR_PRESETS.map((p, i) => /* @__PURE__ */ jsxs6(Text7, { color: color(GLYPH_ROW(i)), children: [
646
+ cursor(GLYPH_ROW(i)),
647
+ d.glyph === p.char ? "(\u2022)" : "( )",
648
+ " ",
649
+ p.name,
650
+ " ",
651
+ p.code,
652
+ " ",
653
+ p.char
654
+ ] }, p.code)),
655
+ /* @__PURE__ */ jsxs6(Text7, { color: color(GLYPH_ROW(CUSTOM)), children: [
656
+ cursor(GLYPH_ROW(CUSTOM)),
657
+ hasCustom ? "(\u2022)" : "( )",
658
+ " ",
659
+ editing ? `Custom: ${draft}_ (enter to set, esc to cancel)` : hasCustom ? `Custom (${codeOf(d.glyph)}) ${d.glyph}` : "Custom\u2026"
660
+ ] }),
661
+ /* @__PURE__ */ jsxs6(Text7, { color: color(INVERT_ROW), children: [
662
+ cursor(INVERT_ROW),
663
+ "invert background: ",
664
+ d.invert ? "on" : "off",
665
+ /* @__PURE__ */ jsx6(Text7, { color: "gray", children: " (stored, not applied)" })
666
+ ] }),
667
+ /* @__PURE__ */ jsx6(Text7, { dimColor: true, children: "\u2191/\u2193 move \xB7 \u2190/\u2192 separator \xB7 enter select \xB7 esc back" })
668
+ ] });
669
+ }
670
+
671
+ // src/screens/preview-screen.tsx
672
+ import { Box as Box8, Text as Text8, useInput as useInput6 } from "ink";
673
+ import { useState as useState6 } from "react";
674
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
675
+ var SURFACES2 = ["band", "statusline", "pane"];
676
+ function PreviewScreen({
677
+ config,
678
+ snapshot,
679
+ source,
680
+ goHome
681
+ }) {
682
+ const termWidth = process.stdout.columns || 120;
683
+ const widths = [80, 120, 160, termWidth];
684
+ const [wi, setWi] = useState6(0);
685
+ const [si, setSi] = useState6(0);
686
+ useInput6((input, key) => {
687
+ if (key.escape) {
688
+ goHome();
689
+ } else if (input === "w") {
690
+ setWi((v) => (v + 1) % widths.length);
691
+ } else if (input === "s") {
692
+ setSi((v) => (v + 1) % SURFACES2.length);
693
+ }
694
+ });
695
+ const width = widths[wi] ?? termWidth;
696
+ const surface = SURFACES2[si] ?? "band";
697
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
698
+ /* @__PURE__ */ jsx7(Text8, { bold: true, children: "Preview" }),
699
+ /* @__PURE__ */ jsxs7(Text8, { color: "gray", children: [
700
+ "w width (",
701
+ width,
702
+ wi === widths.length - 1 ? " = terminal" : "",
703
+ ") \xB7 s surface (",
704
+ surface,
705
+ ") \xB7 esc back"
706
+ ] }),
707
+ /* @__PURE__ */ jsx7(
708
+ Preview,
709
+ {
710
+ config,
711
+ snapshot,
712
+ source,
713
+ width,
714
+ surface
715
+ }
716
+ )
717
+ ] });
718
+ }
719
+
720
+ // src/screens/statusline.tsx
721
+ import { Box as Box9, Text as Text9, useInput as useInput7 } from "ink";
722
+ import { useState as useState7 } from "react";
723
+
724
+ // src/statusline-setup.ts
725
+ import {
726
+ copyFileSync,
727
+ existsSync,
728
+ mkdirSync,
729
+ readFileSync as readFileSync2,
730
+ renameSync,
731
+ writeFileSync
732
+ } from "fs";
733
+ import { dirname } from "path";
734
+ var STATUSLINE_COMMAND = "npx @sushant-kum/ccstatus statusline";
735
+ function settingsPath(scope, env = process.env) {
736
+ if (scope === "project") {
737
+ return `${process.cwd()}/.claude/settings.json`;
738
+ }
739
+ return `${env["HOME"] ?? ""}/.claude/settings.json`;
740
+ }
741
+ var nodeIo = {
742
+ // `read` returns null only for an absent file; a real read error (EACCES, a
743
+ // directory, transient I/O) propagates so callers refuse rather than treat an
744
+ // unreadable settings.json as empty and overwrite it (cf. config-store.ts).
745
+ read: (path) => existsSync(path) ? readFileSync2(path, "utf8") : null,
746
+ write: (path, data) => {
747
+ mkdirSync(dirname(path), { recursive: true });
748
+ if (existsSync(path)) {
749
+ try {
750
+ copyFileSync(path, path + ".bak");
751
+ } catch {
752
+ }
753
+ }
754
+ const tmp = path + ".tmp";
755
+ writeFileSync(tmp, data);
756
+ renameSync(tmp, path);
757
+ }
758
+ };
759
+ var isOurs = (sl) => {
760
+ if (!sl || typeof sl !== "object") {
761
+ return false;
762
+ }
763
+ const c = sl.command;
764
+ return typeof c === "string" && (c === STATUSLINE_COMMAND || /(^|\/)ccstatus statusline$/.test(c));
765
+ };
766
+ function detectStatusline(scope, io = nodeIo, env = process.env) {
767
+ let text;
768
+ try {
769
+ text = io.read(settingsPath(scope, env));
770
+ } catch {
771
+ return { state: "unknown" };
772
+ }
773
+ if (text === null) {
774
+ return { state: "absent" };
775
+ }
776
+ let parsed;
777
+ try {
778
+ parsed = JSON.parse(text);
779
+ } catch {
780
+ return { state: "unknown" };
781
+ }
782
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
783
+ return { state: "unknown" };
784
+ }
785
+ const sl = parsed.statusLine;
786
+ if (sl === void 0) {
787
+ return { state: "absent" };
788
+ }
789
+ if (isOurs(sl)) {
790
+ return { state: "ours" };
791
+ }
792
+ const command = sl?.command;
793
+ if (typeof command !== "string") {
794
+ return { state: "unknown" };
795
+ }
796
+ return { state: "other", command };
797
+ }
798
+ function enableStatusline(scope, opts = {}, io = nodeIo, env = process.env) {
799
+ const path = settingsPath(scope, env);
800
+ let text;
801
+ try {
802
+ text = io.read(path);
803
+ } catch {
804
+ return { ok: false, message: "settings.json exists but could not be read; left unchanged" };
805
+ }
806
+ let settings = {};
807
+ if (text !== null) {
808
+ try {
809
+ const parsed = JSON.parse(text);
810
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
811
+ return { ok: false, message: "settings.json is not a JSON object; left unchanged" };
812
+ }
813
+ settings = parsed;
814
+ } catch {
815
+ return { ok: false, message: "settings.json is not valid JSON; left unchanged" };
816
+ }
817
+ }
818
+ const existing = settings["statusLine"];
819
+ if (existing !== void 0 && !isOurs(existing) && !opts.replaceExisting) {
820
+ return { ok: false, message: "a different statusLine is configured" };
821
+ }
822
+ settings["statusLine"] = { type: "command", command: STATUSLINE_COMMAND, padding: 0 };
823
+ io.write(path, JSON.stringify(settings, null, 2) + "\n");
824
+ return { ok: true, message: `status line enabled in ${path}` };
825
+ }
826
+ function disableStatusline(scope, io = nodeIo, env = process.env) {
827
+ const path = settingsPath(scope, env);
828
+ let text;
829
+ try {
830
+ text = io.read(path);
831
+ } catch {
832
+ return { ok: false, message: "settings.json exists but could not be read; left unchanged" };
833
+ }
834
+ if (text === null) {
835
+ return { ok: true, message: "no settings.json; nothing to disable" };
836
+ }
837
+ let settings;
838
+ try {
839
+ const parsed = JSON.parse(text);
840
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
841
+ return { ok: false, message: "settings.json is not a JSON object; left unchanged" };
842
+ }
843
+ settings = parsed;
844
+ } catch {
845
+ return { ok: false, message: "settings.json is not valid JSON; left unchanged" };
846
+ }
847
+ if (!isOurs(settings["statusLine"])) {
848
+ return { ok: false, message: "the configured statusLine is not ccstatus; left unchanged" };
849
+ }
850
+ delete settings["statusLine"];
851
+ io.write(path, JSON.stringify(settings, null, 2) + "\n");
852
+ return { ok: true, message: `status line disabled in ${path}` };
853
+ }
854
+
855
+ // src/screens/statusline.tsx
856
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
857
+ function Statusline({
858
+ goHome,
859
+ detect = (s) => detectStatusline(s),
860
+ enable = (s, o) => enableStatusline(s, o),
861
+ disable = (s) => disableStatusline(s)
862
+ }) {
863
+ const [scope, setScope] = useState7("user");
864
+ const [info, setInfo] = useState7(() => detect("user"));
865
+ const [msg, setMsg] = useState7(null);
866
+ const [pendingScope, setPendingScope] = useState7(null);
867
+ const doEnable = (s, replaceExisting) => {
868
+ setScope(s);
869
+ const current = detect(s);
870
+ if (current.state === "other" && !replaceExisting) {
871
+ setPendingScope(s);
872
+ setMsg(`A different statusLine is set (${current.command}). Press r to replace it.`);
873
+ return;
874
+ }
875
+ const r = enable(s, { replaceExisting });
876
+ setMsg(r.message);
877
+ setPendingScope(null);
878
+ setInfo(detect(s));
879
+ };
880
+ useInput7((input, key) => {
881
+ if (key.escape) {
882
+ goHome();
883
+ return;
884
+ }
885
+ if (input === "u") {
886
+ doEnable("user", false);
887
+ } else if (input === "p") {
888
+ doEnable("project", false);
889
+ } else if (input === "r" && pendingScope) {
890
+ doEnable(pendingScope, true);
891
+ } else if (input === "d") {
892
+ setPendingScope(null);
893
+ const r = disable(scope);
894
+ setMsg(r.message);
895
+ setInfo(detect(scope));
896
+ }
897
+ });
898
+ const state = info.state === "ours" ? "configured (ccstatus)" : info.state === "other" ? `another command: ${info.command}` : info.state === "unknown" ? "settings.json unreadable" : "not configured";
899
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", children: [
900
+ /* @__PURE__ */ jsx8(Text9, { bold: true, children: "Native status line" }),
901
+ /* @__PURE__ */ jsxs8(Text9, { children: [
902
+ "Current: ",
903
+ state
904
+ ] }),
905
+ /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
906
+ "u: enable (user) \xB7 p: enable (project) \xB7 d: disable (",
907
+ scope,
908
+ ") \xB7 esc: back"
909
+ ] }),
910
+ pendingScope && /* @__PURE__ */ jsx8(Text9, { color: "yellow", children: "r: confirm replace" }),
911
+ msg && /* @__PURE__ */ jsx8(Text9, { color: "cyan", children: msg })
912
+ ] });
913
+ }
914
+
915
+ // src/screens/surfaces.tsx
916
+ import { Box as Box10, Text as Text10, useInput as useInput8 } from "ink";
917
+ import { useState as useState8 } from "react";
918
+
919
+ // src/toast-presets.ts
920
+ var TOAST_PRESETS = [
921
+ { label: "Context high", when: "ctxPct>80", text: "Context over 80%" },
922
+ { label: "Context critical", when: "ctxPct>95", text: "Context over 95%" },
923
+ { label: "Session limit near", when: "fivePct>90", text: "Session limit near" },
924
+ { label: "Weekly limit near", when: "weekPct>80", text: "Weekly limit near" }
925
+ ];
926
+
927
+ // src/screens/surfaces.tsx
928
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
929
+ var SURFACES3 = ["band", "statusline", "pane", "toasts"];
930
+ function Surfaces({ config, setConfig, goHome }) {
931
+ const [focus, setFocus] = useState8(0);
932
+ const [mode, setMode] = useState8({ kind: "nav" });
933
+ const s = config.surfaces;
934
+ const rules = s.toasts.rules;
935
+ const total = SURFACES3.length + rules.length;
936
+ const setRules = (next) => setConfig({ ...config, surfaces: { ...s, toasts: { ...s.toasts, rules: next } } });
937
+ useInput8((input, key) => {
938
+ if (mode.kind === "preset") {
939
+ const n = TOAST_PRESETS.length + 1;
940
+ if (key.escape) {
941
+ setMode({ kind: "nav" });
942
+ } else if (key.upArrow) {
943
+ setMode({ kind: "preset", idx: (mode.idx + n - 1) % n });
944
+ } else if (key.downArrow) {
945
+ setMode({ kind: "preset", idx: (mode.idx + 1) % n });
946
+ } else if (key.return) {
947
+ const p = TOAST_PRESETS[mode.idx];
948
+ if (p) {
949
+ setRules([...rules, { when: p.when, text: p.text, once: true }]);
950
+ setFocus(SURFACES3.length + rules.length);
951
+ setMode({ kind: "nav" });
952
+ } else {
953
+ setMode({ kind: "edit", target: "new", ruleIdx: -1, buf: "" });
954
+ }
955
+ }
956
+ return;
957
+ }
958
+ if (mode.kind === "edit") {
959
+ if (key.escape) {
960
+ setMode({ kind: "nav" });
961
+ } else if (key.return) {
962
+ if (mode.target === "new") {
963
+ if (!isValidWhen(mode.buf)) {
964
+ return;
965
+ }
966
+ setRules([...rules, { when: mode.buf.trim(), text: "Custom alert", once: true }]);
967
+ setFocus(SURFACES3.length + rules.length);
968
+ } else {
969
+ setRules(
970
+ rules.map((r, i) => i === mode.ruleIdx ? { ...r, [mode.target]: mode.buf } : r)
971
+ );
972
+ }
973
+ setMode({ kind: "nav" });
974
+ } else if (key.backspace || key.delete) {
975
+ setMode({ ...mode, buf: mode.buf.slice(0, -1) });
976
+ } else if (input && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow && !key.tab) {
977
+ setMode({ ...mode, buf: mode.buf + input });
978
+ }
979
+ return;
980
+ }
981
+ const ruleIdx = focus - SURFACES3.length;
982
+ if (key.escape) {
983
+ goHome();
984
+ } else if (key.upArrow) {
985
+ setFocus((f) => Math.max(0, f - 1));
986
+ } else if (key.downArrow) {
987
+ setFocus((f) => Math.min(total - 1, f + 1));
988
+ } else if (input === "a") {
989
+ setMode({ kind: "preset", idx: 0 });
990
+ } else if (input === " ") {
991
+ if (ruleIdx < 0) {
992
+ const name = SURFACES3[focus];
993
+ if (name) {
994
+ setConfig({
995
+ ...config,
996
+ surfaces: { ...s, [name]: { ...s[name], enabled: !s[name].enabled } }
997
+ });
998
+ }
999
+ } else if (rules[ruleIdx]) {
1000
+ setRules(rules.map((r, i) => i === ruleIdx ? { ...r, once: !r.once } : r));
1001
+ }
1002
+ } else if (ruleIdx >= 0) {
1003
+ const r = rules[ruleIdx];
1004
+ if (r && input === "w") {
1005
+ setMode({ kind: "edit", target: "when", ruleIdx, buf: r.when });
1006
+ } else if (r && input === "t") {
1007
+ setMode({ kind: "edit", target: "text", ruleIdx, buf: r.text });
1008
+ } else if (r && input === "x") {
1009
+ setRules(rules.filter((_, i) => i !== ruleIdx));
1010
+ setFocus((f) => Math.max(0, f - 1));
1011
+ }
1012
+ }
1013
+ });
1014
+ const cur = (i) => focus === i ? "\u25B8 " : " ";
1015
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", children: [
1016
+ /* @__PURE__ */ jsx9(Text10, { bold: true, children: "Surfaces" }),
1017
+ SURFACES3.map((name, i) => /* @__PURE__ */ jsxs9(Text10, { color: focus === i ? "cyan" : void 0, children: [
1018
+ cur(i),
1019
+ "[",
1020
+ s[name].enabled ? "x" : " ",
1021
+ "] ",
1022
+ name
1023
+ ] }, name)),
1024
+ /* @__PURE__ */ jsx9(Text10, { bold: true, children: "Toast rules" }),
1025
+ rules.length === 0 && /* @__PURE__ */ jsx9(Text10, { dimColor: true, children: " (none \u2014 press a to add)" }),
1026
+ rules.map((r, i) => {
1027
+ const idx = SURFACES3.length + i;
1028
+ const ok = isValidWhen(r.when);
1029
+ const editing = mode.kind === "edit" && mode.ruleIdx === i;
1030
+ return /* @__PURE__ */ jsxs9(Box10, { children: [
1031
+ /* @__PURE__ */ jsx9(Text10, { color: focus === idx ? "cyan" : void 0, children: cur(idx) }),
1032
+ /* @__PURE__ */ jsxs9(Text10, { color: ok ? void 0 : "red", children: [
1033
+ "when: ",
1034
+ editing && mode.target === "when" ? mode.buf + "_" : r.when,
1035
+ ok ? "" : " \u26A0 invalid"
1036
+ ] }),
1037
+ /* @__PURE__ */ jsxs9(Text10, { color: focus === idx ? "cyan" : void 0, children: [
1038
+ " ",
1039
+ "text: ",
1040
+ editing && mode.target === "text" ? mode.buf + "_" : `"${r.text}"`,
1041
+ " [",
1042
+ r.once ? "x" : " ",
1043
+ "] once"
1044
+ ] })
1045
+ ] }, i);
1046
+ }),
1047
+ mode.kind === "preset" && /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", children: [
1048
+ /* @__PURE__ */ jsx9(Text10, { bold: true, children: "Add rule \u2014 pick a preset" }),
1049
+ [...TOAST_PRESETS.map((p) => `${p.label} (${p.when})`), "Custom\u2026"].map((l, i) => /* @__PURE__ */ jsxs9(Text10, { color: mode.idx === i ? "cyan" : void 0, children: [
1050
+ mode.idx === i ? "\u25B8 " : " ",
1051
+ l
1052
+ ] }, l))
1053
+ ] }),
1054
+ mode.kind === "edit" && mode.target === "new" && /* @__PURE__ */ jsxs9(Text10, { children: [
1055
+ "when (e.g. ctxPct>80): ",
1056
+ mode.buf,
1057
+ "_",
1058
+ mode.buf && !isValidWhen(mode.buf) ? " \u26A0 invalid" : ""
1059
+ ] }),
1060
+ /* @__PURE__ */ jsx9(Text10, { dimColor: true, children: "\u2191/\u2193 nav \xB7 space toggle \xB7 a add \xB7 w when \xB7 t text \xB7 x remove \xB7 esc back" })
1061
+ ] });
1062
+ }
1063
+
1064
+ // src/screens/themes.tsx
1065
+ import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
1066
+ import { useState as useState9 } from "react";
1067
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1068
+ var FG2 = NAMED_COLORS.filter((c) => !c.startsWith("bg"));
1069
+ var BG2 = NAMED_COLORS.filter((c) => c.startsWith("bg"));
1070
+ var FIELDS3 = ["fg", "bg"];
1071
+ function uniqueName(base, themes) {
1072
+ if (!(base in themes)) {
1073
+ return base;
1074
+ }
1075
+ let i = 2;
1076
+ while (`${base}-${i}` in themes) {
1077
+ i++;
1078
+ }
1079
+ return `${base}-${i}`;
1080
+ }
1081
+ function Themes({ config, setConfig, goHome }) {
1082
+ const names = Object.keys(config.themes);
1083
+ const [sel, setSel] = useState9(Math.max(0, names.indexOf(config.theme)));
1084
+ const [mode, setMode] = useState9("list");
1085
+ const [w, setW] = useState9(0);
1086
+ const [field, setField] = useState9(0);
1087
+ const [pick, setPick] = useState9(0);
1088
+ const name = names[Math.min(sel, names.length - 1)];
1089
+ const theme = name && config.themes[name] || {};
1090
+ const widgets = Object.keys(theme);
1091
+ const widget = widgets[Math.min(w, widgets.length - 1)];
1092
+ const list = FIELDS3[field] === "fg" ? FG2 : BG2;
1093
+ const setThemes = (themes, active = config.theme) => setConfig({ ...config, theme: active, themes });
1094
+ useInput9((input, key) => {
1095
+ if (mode === "list") {
1096
+ if (key.escape) {
1097
+ goHome();
1098
+ } else if (key.upArrow) {
1099
+ setSel((s) => Math.max(0, s - 1));
1100
+ } else if (key.downArrow) {
1101
+ setSel((s) => Math.min(names.length - 1, s + 1));
1102
+ } else if (key.return) {
1103
+ if (name) {
1104
+ setConfig({ ...config, theme: name });
1105
+ }
1106
+ } else if (key.rightArrow) {
1107
+ if (widgets.length) {
1108
+ setW(0);
1109
+ setMode("colors");
1110
+ }
1111
+ } else if (input === "n") {
1112
+ const n = uniqueName("custom", config.themes);
1113
+ setThemes({ ...config.themes, [n]: {} });
1114
+ setSel(names.length);
1115
+ } else if (input === "d" && name) {
1116
+ const n = uniqueName(`${name}-copy`, config.themes);
1117
+ setThemes({ ...config.themes, [n]: structuredClone(theme) });
1118
+ setSel(names.length);
1119
+ }
1120
+ } else if (mode === "colors") {
1121
+ if (key.escape || key.leftArrow && field === 0) {
1122
+ setMode("list");
1123
+ } else if (key.upArrow) {
1124
+ setW((i) => Math.max(0, i - 1));
1125
+ } else if (key.downArrow) {
1126
+ setW((i) => Math.min(widgets.length - 1, i + 1));
1127
+ } else if (key.leftArrow) {
1128
+ setField(0);
1129
+ } else if (key.rightArrow) {
1130
+ setField(1);
1131
+ } else if (key.return && widget) {
1132
+ const cur = theme[widget]?.[FIELDS3[field] ?? "fg"];
1133
+ setPick(Math.max(0, list.indexOf(cur ?? "")));
1134
+ setMode("picker");
1135
+ }
1136
+ } else {
1137
+ if (key.escape) {
1138
+ setMode("colors");
1139
+ } else if (key.upArrow) {
1140
+ setPick((p) => Math.max(0, p - 1));
1141
+ } else if (key.downArrow) {
1142
+ setPick((p) => Math.min(list.length - 1, p + 1));
1143
+ } else if (key.return && name && widget) {
1144
+ const f = FIELDS3[field] ?? "fg";
1145
+ const chosen = list[pick];
1146
+ if (chosen) {
1147
+ const entry = { ...theme[widget], [f]: chosen };
1148
+ setThemes({ ...config.themes, [name]: { ...theme, [widget]: entry } });
1149
+ setMode("colors");
1150
+ }
1151
+ }
1152
+ }
1153
+ });
1154
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", children: [
1155
+ /* @__PURE__ */ jsx10(Text11, { bold: true, children: "Themes" }),
1156
+ names.map((n, i) => /* @__PURE__ */ jsxs10(Text11, { color: i === sel && mode === "list" ? "cyan" : void 0, children: [
1157
+ i === sel ? "\u25B8 " : " ",
1158
+ n,
1159
+ n === config.theme ? " (active)" : ""
1160
+ ] }, n)),
1161
+ /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
1162
+ name,
1163
+ " colors:"
1164
+ ] }),
1165
+ widgets.map((wt, i) => {
1166
+ const c = theme[wt] ?? {};
1167
+ const on = mode !== "list" && i === w;
1168
+ return /* @__PURE__ */ jsxs10(Text11, { children: [
1169
+ on ? "\u25B8 " : " ",
1170
+ wt.padEnd(20),
1171
+ /* @__PURE__ */ jsxs10(Text11, { color: inkColor(c.fg), inverse: on && field === 0, children: [
1172
+ " ",
1173
+ c.fg ?? "(none)",
1174
+ " "
1175
+ ] }),
1176
+ /* @__PURE__ */ jsxs10(Text11, { backgroundColor: inkBg(c.bg), inverse: on && field === 1, children: [
1177
+ " ",
1178
+ c.bg ?? "(none)",
1179
+ " "
1180
+ ] })
1181
+ ] }, wt);
1182
+ }),
1183
+ mode === "picker" && /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", children: [
1184
+ /* @__PURE__ */ jsxs10(Text11, { bold: true, children: [
1185
+ "Pick ",
1186
+ FIELDS3[field],
1187
+ " for ",
1188
+ widget
1189
+ ] }),
1190
+ list.map((c, i) => /* @__PURE__ */ jsxs10(Text11, { color: i === pick ? "cyan" : void 0, children: [
1191
+ i === pick ? "\u25B8 " : " ",
1192
+ /* @__PURE__ */ jsxs10(
1193
+ Text11,
1194
+ {
1195
+ color: FIELDS3[field] === "fg" ? inkColor(c) : void 0,
1196
+ backgroundColor: FIELDS3[field] === "bg" ? inkBg(c) : void 0,
1197
+ children: [
1198
+ " ",
1199
+ c,
1200
+ " "
1201
+ ]
1202
+ }
1203
+ )
1204
+ ] }, c))
1205
+ ] }),
1206
+ /* @__PURE__ */ jsx10(Text11, { dimColor: true, children: mode === "list" ? "\u2191/\u2193 select \xB7 \u23CE activate \xB7 \u2192 colors \xB7 n new \xB7 d duplicate \xB7 esc back" : mode === "colors" ? "\u2191/\u2193 widget \xB7 \u2190/\u2192 fg/bg \xB7 \u23CE pick \xB7 esc back" : "\u2191/\u2193 choose \xB7 \u23CE set \xB7 esc cancel" })
1207
+ ] });
1208
+ }
1209
+
1210
+ // src/app.tsx
1211
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1212
+ function Placeholder({ id, goHome }) {
1213
+ useInput10((_i, key) => {
1214
+ if (key.escape) {
1215
+ goHome();
1216
+ }
1217
+ });
1218
+ return /* @__PURE__ */ jsxs11(Text12, { children: [
1219
+ id,
1220
+ " \u2026coming soon (esc)\u2026"
1221
+ ] });
1222
+ }
1223
+ function PlaceholderOrScreen({
1224
+ id,
1225
+ snapshot,
1226
+ source,
1227
+ ...props
1228
+ }) {
1229
+ switch (id) {
1230
+ case "defaults":
1231
+ return /* @__PURE__ */ jsx11(Defaults, { ...props });
1232
+ case "themes":
1233
+ return /* @__PURE__ */ jsx11(Themes, { ...props });
1234
+ case "powerline":
1235
+ return /* @__PURE__ */ jsx11(Powerline, { ...props });
1236
+ case "items":
1237
+ return /* @__PURE__ */ jsx11(Items, { ...props });
1238
+ case "surfaces":
1239
+ return /* @__PURE__ */ jsx11(Surfaces, { ...props });
1240
+ case "statusline":
1241
+ return /* @__PURE__ */ jsx11(Statusline, { ...props });
1242
+ case "import":
1243
+ return /* @__PURE__ */ jsx11(Import, { ...props });
1244
+ case "preview":
1245
+ return /* @__PURE__ */ jsx11(PreviewScreen, { ...props, snapshot, source });
1246
+ default:
1247
+ return /* @__PURE__ */ jsx11(Placeholder, { id, goHome: props.goHome });
1248
+ }
1249
+ }
1250
+ function App({ modProbe } = {}) {
1251
+ const { exit } = useApp();
1252
+ const probe = modProbe ?? {
1253
+ detect: () => detectModStatus(),
1254
+ install: () => installMod()
1255
+ };
1256
+ const loaded = useMemo2(() => loadConfigFile(configPath()), []);
1257
+ const [config, setConfig] = useState10(loaded.config);
1258
+ const [screen, setScreen] = useState10("menu");
1259
+ const [saveError, setSaveError] = useState10(null);
1260
+ const [saveWarnings, setSaveWarnings] = useState10([]);
1261
+ const [saved, setSaved] = useState10(false);
1262
+ const [modStatus, setModStatus] = useState10(null);
1263
+ const [installing, setInstalling] = useState10(false);
1264
+ const [installMsg, setInstallMsg] = useState10(null);
1265
+ const [installOk, setInstallOk] = useState10(true);
1266
+ const installStartedRef = useRef(false);
1267
+ const mountedRef = useRef(true);
1268
+ useEffect(
1269
+ () => () => {
1270
+ mountedRef.current = false;
1271
+ },
1272
+ []
1273
+ );
1274
+ const snap = readSnapshot(snapshotPath());
1275
+ const width = process.stdout.columns || 120;
1276
+ useEffect(() => {
1277
+ probe.detect().then((s) => {
1278
+ if (mountedRef.current) {
1279
+ setModStatus(s);
1280
+ }
1281
+ }).catch(() => {
1282
+ if (mountedRef.current) {
1283
+ setModStatus("unknown");
1284
+ }
1285
+ });
1286
+ }, []);
1287
+ useInput10((input) => {
1288
+ if (screen !== "menu") {
1289
+ return;
1290
+ }
1291
+ if (input === "q") {
1292
+ exit();
1293
+ return;
1294
+ }
1295
+ if (input === "i" && modStatus === "absent" && !installStartedRef.current) {
1296
+ installStartedRef.current = true;
1297
+ setInstalling(true);
1298
+ probe.install().then((r) => {
1299
+ if (!mountedRef.current) {
1300
+ return;
1301
+ }
1302
+ setInstallMsg(r.message);
1303
+ setInstallOk(r.ok);
1304
+ if (r.ok) {
1305
+ setModStatus("installed");
1306
+ }
1307
+ }).catch((e) => {
1308
+ if (!mountedRef.current) {
1309
+ return;
1310
+ }
1311
+ setInstallMsg(e instanceof Error ? e.message : String(e));
1312
+ setInstallOk(false);
1313
+ }).finally(() => {
1314
+ installStartedRef.current = false;
1315
+ if (mountedRef.current) {
1316
+ setInstalling(false);
1317
+ }
1318
+ });
1319
+ }
1320
+ });
1321
+ const save = () => {
1322
+ try {
1323
+ setSaveWarnings(saveConfigFile(configPath(), config));
1324
+ setSaveError(null);
1325
+ setSaved(true);
1326
+ return true;
1327
+ } catch (e) {
1328
+ setSaveError(e instanceof Error ? e.message : String(e));
1329
+ return false;
1330
+ }
1331
+ };
1332
+ const common = { config, setConfig, goHome: () => setScreen("menu") };
1333
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 1, children: [
1334
+ screen === "menu" ? /* @__PURE__ */ jsx11(
1335
+ Menu,
1336
+ {
1337
+ onSelect: (id) => {
1338
+ if (id === "quit") {
1339
+ exit();
1340
+ } else if (id === "save") {
1341
+ if (save()) {
1342
+ exit();
1343
+ }
1344
+ } else {
1345
+ setScreen(id);
1346
+ }
1347
+ }
1348
+ }
1349
+ ) : /* @__PURE__ */ jsx11(
1350
+ PlaceholderOrScreen,
1351
+ {
1352
+ id: screen,
1353
+ ...common,
1354
+ snapshot: snap.snapshot,
1355
+ source: snap.source
1356
+ }
1357
+ ),
1358
+ !saved && loaded.warnings.length > 0 && /* @__PURE__ */ jsxs11(Text12, { color: "yellow", children: [
1359
+ "\u26A0 existing config: ",
1360
+ loaded.warnings.join("; "),
1361
+ " \u2014 editing from defaults; saving replaces it (a .bak is kept)"
1362
+ ] }),
1363
+ saveWarnings.length > 0 && /* @__PURE__ */ jsxs11(Text12, { color: "yellow", children: [
1364
+ "\u26A0 saved with changes: ",
1365
+ saveWarnings.join("; ")
1366
+ ] }),
1367
+ saveError && /* @__PURE__ */ jsxs11(Text12, { color: "red", children: [
1368
+ "Could not save config: ",
1369
+ saveError
1370
+ ] }),
1371
+ installMsg ? /* @__PURE__ */ jsxs11(Text12, { color: installOk ? "green" : "red", children: [
1372
+ installOk ? "\u2713 " : "\u26A0 ",
1373
+ installMsg
1374
+ ] }) : modStatus === "absent" ? /* @__PURE__ */ jsxs11(Text12, { color: "yellow", children: [
1375
+ "\u26A0 ccstatus mod not installed \u2014 press i to install (",
1376
+ PLUGIN_ID,
1377
+ ")",
1378
+ installing ? " \u2026 installing" : ""
1379
+ ] }) : modStatus === "disabled" ? /* @__PURE__ */ jsxs11(Text12, { color: "yellow", children: [
1380
+ "\u26A0 ccstatus mod installed but disabled \u2014 enable it: claude plugin enable ",
1381
+ PLUGIN_ID
1382
+ ] }) : modStatus === "unknown" ? /* @__PURE__ */ jsxs11(Text12, { color: "yellow", children: [
1383
+ "\u26A0 couldn't check if the mod is installed \u2014 if the bar isn't showing, run: claude plugin marketplace add ",
1384
+ MARKETPLACE_SLUG,
1385
+ " && claude plugin install ",
1386
+ PLUGIN_ID
1387
+ ] }) : null,
1388
+ /* @__PURE__ */ jsx11(Preview, { config, snapshot: snap.snapshot, source: snap.source, width })
1389
+ ] });
1390
+ }
1391
+ export {
1392
+ App
1393
+ };