ccstatusline 1.0.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.
Binary file
@@ -0,0 +1,958 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/ccstatusline.ts
4
+ import chalk2 from "chalk";
5
+ import { execSync } from "child_process";
6
+
7
+ // src/tui.tsx
8
+ import { useState, useEffect } from "react";
9
+ import { render, Box, Text, useInput, useApp } from "ink";
10
+ import SelectInput from "ink-select-input";
11
+ import chalk from "chalk";
12
+
13
+ // src/config.ts
14
+ import * as fs from "fs";
15
+ import * as path from "path";
16
+ import * as os from "os";
17
+ import { promisify } from "util";
18
+ var readFile2 = fs.promises?.readFile || promisify(fs.readFile);
19
+ var writeFile2 = fs.promises?.writeFile || promisify(fs.writeFile);
20
+ var mkdir2 = fs.promises?.mkdir || promisify(fs.mkdir);
21
+ var CONFIG_DIR = path.join(os.homedir(), ".config", "ccstatusline");
22
+ var SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
23
+ var DEFAULT_SETTINGS = {
24
+ items: [
25
+ { id: "1", type: "model", color: "cyan" },
26
+ { id: "2", type: "separator" },
27
+ { id: "3", type: "git-branch", color: "magenta" }
28
+ ],
29
+ colors: {
30
+ model: "cyan",
31
+ gitBranch: "magenta",
32
+ separator: "dim"
33
+ }
34
+ };
35
+ async function loadSettings() {
36
+ try {
37
+ if (!fs.existsSync(SETTINGS_PATH)) {
38
+ return DEFAULT_SETTINGS;
39
+ }
40
+ const content = await readFile2(SETTINGS_PATH, "utf-8");
41
+ const loaded = JSON.parse(content);
42
+ if (loaded.elements || loaded.layout) {
43
+ return migrateOldSettings(loaded);
44
+ }
45
+ return { ...DEFAULT_SETTINGS, ...loaded };
46
+ } catch {
47
+ return DEFAULT_SETTINGS;
48
+ }
49
+ }
50
+ function migrateOldSettings(old) {
51
+ const items = [];
52
+ let id = 1;
53
+ if (old.elements?.model) {
54
+ items.push({ id: String(id++), type: "model", color: old.colors?.model });
55
+ }
56
+ if (items.length > 0 && old.elements?.gitBranch) {
57
+ items.push({ id: String(id++), type: "separator" });
58
+ }
59
+ if (old.elements?.gitBranch) {
60
+ items.push({ id: String(id++), type: "git-branch", color: old.colors?.gitBranch });
61
+ }
62
+ if (old.layout?.expandingSeparators) {
63
+ items.forEach((item) => {
64
+ if (item.type === "separator") {
65
+ item.type = "flex-separator";
66
+ }
67
+ });
68
+ }
69
+ return {
70
+ items,
71
+ colors: old.colors || DEFAULT_SETTINGS.colors
72
+ };
73
+ }
74
+ async function saveSettings(settings) {
75
+ await mkdir2(CONFIG_DIR, { recursive: true });
76
+ await writeFile2(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
77
+ }
78
+
79
+ // src/claude-settings.ts
80
+ import * as fs2 from "fs";
81
+ import * as path2 from "path";
82
+ import * as os2 from "os";
83
+ import { promisify as promisify2 } from "util";
84
+ var readFile4 = fs2.promises?.readFile || promisify2(fs2.readFile);
85
+ var writeFile4 = fs2.promises?.writeFile || promisify2(fs2.writeFile);
86
+ var mkdir4 = fs2.promises?.mkdir || promisify2(fs2.mkdir);
87
+ var CLAUDE_SETTINGS_PATH = path2.join(os2.homedir(), ".claude", "settings.json");
88
+ async function loadClaudeSettings() {
89
+ try {
90
+ if (!fs2.existsSync(CLAUDE_SETTINGS_PATH)) {
91
+ return {};
92
+ }
93
+ const content = await readFile4(CLAUDE_SETTINGS_PATH, "utf-8");
94
+ return JSON.parse(content);
95
+ } catch {
96
+ return {};
97
+ }
98
+ }
99
+ async function saveClaudeSettings(settings) {
100
+ const dir = path2.dirname(CLAUDE_SETTINGS_PATH);
101
+ await mkdir4(dir, { recursive: true });
102
+ await writeFile4(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
103
+ }
104
+ async function isInstalled() {
105
+ const settings = await loadClaudeSettings();
106
+ return settings.statusLine?.command === "npx ccstatusline";
107
+ }
108
+ async function installStatusLine() {
109
+ const settings = await loadClaudeSettings();
110
+ settings.statusLine = {
111
+ type: "command",
112
+ command: "npx ccstatusline",
113
+ padding: 1
114
+ };
115
+ await saveClaudeSettings(settings);
116
+ }
117
+ async function uninstallStatusLine() {
118
+ const settings = await loadClaudeSettings();
119
+ if (settings.statusLine) {
120
+ delete settings.statusLine;
121
+ await saveClaudeSettings(settings);
122
+ }
123
+ }
124
+ async function getExistingStatusLine() {
125
+ const settings = await loadClaudeSettings();
126
+ return settings.statusLine?.command || null;
127
+ }
128
+
129
+ // src/tui.tsx
130
+ import { jsxDEV } from "react/jsx-dev-runtime";
131
+ var StatusLinePreview = ({ items, terminalWidth }) => {
132
+ const width = 80;
133
+ const elements = [];
134
+ let hasFlexSeparator = false;
135
+ items.forEach((item) => {
136
+ switch (item.type) {
137
+ case "model":
138
+ const modelColor = chalk[item.color || "cyan"] || chalk.cyan;
139
+ elements.push(modelColor("Model: Claude"));
140
+ break;
141
+ case "git-branch":
142
+ const branchColor = chalk[item.color || "magenta"] || chalk.magenta;
143
+ elements.push(branchColor("⎇ main"));
144
+ break;
145
+ case "tokens-input":
146
+ const inputColor = chalk[item.color || "yellow"] || chalk.yellow;
147
+ elements.push(inputColor("In: 15.2k"));
148
+ break;
149
+ case "tokens-output":
150
+ const outputColor = chalk[item.color || "green"] || chalk.green;
151
+ elements.push(outputColor("Out: 3.4k"));
152
+ break;
153
+ case "tokens-cached":
154
+ const cachedColor = chalk[item.color || "blue"] || chalk.blue;
155
+ elements.push(cachedColor("Cached: 12k"));
156
+ break;
157
+ case "tokens-total":
158
+ const totalColor = chalk[item.color || "white"] || chalk.white;
159
+ elements.push(totalColor("Total: 30.6k"));
160
+ break;
161
+ case "context-length":
162
+ const ctxColor = chalk[item.color || "cyan"] || chalk.cyan;
163
+ elements.push(ctxColor("Ctx: 18.6k"));
164
+ break;
165
+ case "context-percentage":
166
+ const ctxPctColor = chalk[item.color || "cyan"] || chalk.cyan;
167
+ elements.push(ctxPctColor("Ctx: 9.3%"));
168
+ break;
169
+ case "separator":
170
+ elements.push(chalk.dim(" | "));
171
+ break;
172
+ case "flex-separator":
173
+ elements.push("FLEX");
174
+ hasFlexSeparator = true;
175
+ break;
176
+ }
177
+ });
178
+ let statusLine = "";
179
+ if (hasFlexSeparator) {
180
+ const parts = [[]];
181
+ let currentPart = 0;
182
+ for (let i = 0;i < items.length; i++) {
183
+ if (items[i].type === "flex-separator") {
184
+ currentPart++;
185
+ parts[currentPart] = [];
186
+ } else {
187
+ const element = elements[i];
188
+ if (element !== "FLEX") {
189
+ parts[currentPart].push(element);
190
+ }
191
+ }
192
+ }
193
+ const partLengths = parts.map((part) => {
194
+ const joined = part.join("");
195
+ return joined.replace(/\x1b\[[0-9;]*m/g, "").length;
196
+ });
197
+ const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
198
+ const flexCount = parts.length - 1;
199
+ const totalSpace = Math.max(0, width - totalContentLength);
200
+ const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
201
+ const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
202
+ statusLine = "";
203
+ for (let i = 0;i < parts.length; i++) {
204
+ statusLine += parts[i].join("");
205
+ if (i < parts.length - 1) {
206
+ const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
207
+ statusLine += " ".repeat(spaces);
208
+ }
209
+ }
210
+ } else {
211
+ statusLine = elements.filter((e) => e !== "FLEX").join("");
212
+ }
213
+ const boxWidth = Math.min(terminalWidth - 4, process.stdout.columns - 4 || 76);
214
+ const topLine = chalk.dim("╭" + "─".repeat(Math.max(0, boxWidth - 2)) + "╮");
215
+ const middleLine = chalk.dim("│") + " > " + " ".repeat(Math.max(0, boxWidth - 5)) + chalk.dim("│");
216
+ const bottomLine = chalk.dim("╰" + "─".repeat(Math.max(0, boxWidth - 2)) + "╯");
217
+ return /* @__PURE__ */ jsxDEV(Box, {
218
+ flexDirection: "column",
219
+ children: [
220
+ /* @__PURE__ */ jsxDEV(Text, {
221
+ children: topLine
222
+ }, undefined, false, undefined, this),
223
+ /* @__PURE__ */ jsxDEV(Text, {
224
+ children: middleLine
225
+ }, undefined, false, undefined, this),
226
+ /* @__PURE__ */ jsxDEV(Text, {
227
+ children: bottomLine
228
+ }, undefined, false, undefined, this),
229
+ /* @__PURE__ */ jsxDEV(Text, {
230
+ children: statusLine
231
+ }, undefined, false, undefined, this)
232
+ ]
233
+ }, undefined, true, undefined, this);
234
+ };
235
+ var ConfirmDialog = ({ message, onConfirm, onCancel }) => {
236
+ const items = [
237
+ { label: "✅ Yes", value: "yes" },
238
+ { label: "❌ No", value: "no" }
239
+ ];
240
+ return /* @__PURE__ */ jsxDEV(Box, {
241
+ flexDirection: "column",
242
+ children: [
243
+ /* @__PURE__ */ jsxDEV(Text, {
244
+ children: message
245
+ }, undefined, false, undefined, this),
246
+ /* @__PURE__ */ jsxDEV(Box, {
247
+ marginTop: 1,
248
+ children: /* @__PURE__ */ jsxDEV(SelectInput, {
249
+ items,
250
+ onSelect: (item) => item.value === "yes" ? onConfirm() : onCancel()
251
+ }, undefined, false, undefined, this)
252
+ }, undefined, false, undefined, this)
253
+ ]
254
+ }, undefined, true, undefined, this);
255
+ };
256
+ var MainMenu = ({ onSelect, isClaudeInstalled, hasChanges }) => {
257
+ const items = [
258
+ { label: "\uD83D\uDCDD Edit Status Line Items", value: "items" },
259
+ { label: "\uD83C\uDFA8 Configure Colors", value: "colors" },
260
+ { label: isClaudeInstalled ? "\uD83D\uDDD1️ Uninstall from Claude Code" : "\uD83D\uDCE6 Install to Claude Code", value: "install" }
261
+ ];
262
+ if (hasChanges) {
263
+ items.push({ label: "\uD83D\uDCBE Save & Exit", value: "save" }, { label: "❌ Exit without saving", value: "exit" });
264
+ } else {
265
+ items.push({ label: "\uD83D\uDEAA Exit", value: "exit" });
266
+ }
267
+ return /* @__PURE__ */ jsxDEV(Box, {
268
+ flexDirection: "column",
269
+ children: [
270
+ /* @__PURE__ */ jsxDEV(Text, {
271
+ bold: true,
272
+ children: "Main Menu"
273
+ }, undefined, false, undefined, this),
274
+ /* @__PURE__ */ jsxDEV(Box, {
275
+ marginTop: 1,
276
+ children: /* @__PURE__ */ jsxDEV(SelectInput, {
277
+ items,
278
+ onSelect: (item) => onSelect(item.value)
279
+ }, undefined, false, undefined, this)
280
+ }, undefined, false, undefined, this)
281
+ ]
282
+ }, undefined, true, undefined, this);
283
+ };
284
+ var ItemsEditor = ({ items, onUpdate, onBack }) => {
285
+ const [selectedIndex, setSelectedIndex] = useState(0);
286
+ const [moveMode, setMoveMode] = useState(false);
287
+ useInput((input, key) => {
288
+ if (moveMode) {
289
+ if (key.upArrow && selectedIndex > 0) {
290
+ const newItems = [...items];
291
+ [newItems[selectedIndex], newItems[selectedIndex - 1]] = [newItems[selectedIndex - 1], newItems[selectedIndex]];
292
+ onUpdate(newItems);
293
+ setSelectedIndex(selectedIndex - 1);
294
+ } else if (key.downArrow && selectedIndex < items.length - 1) {
295
+ const newItems = [...items];
296
+ [newItems[selectedIndex], newItems[selectedIndex + 1]] = [newItems[selectedIndex + 1], newItems[selectedIndex]];
297
+ onUpdate(newItems);
298
+ setSelectedIndex(selectedIndex + 1);
299
+ } else if (key.escape || key.return) {
300
+ setMoveMode(false);
301
+ }
302
+ } else {
303
+ if (key.upArrow) {
304
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
305
+ } else if (key.downArrow) {
306
+ setSelectedIndex(Math.min(items.length - 1, selectedIndex + 1));
307
+ } else if (key.leftArrow && items.length > 0) {
308
+ const types = [
309
+ "model",
310
+ "git-branch",
311
+ "separator",
312
+ "flex-separator",
313
+ "tokens-input",
314
+ "tokens-output",
315
+ "tokens-cached",
316
+ "tokens-total",
317
+ "context-length",
318
+ "context-percentage"
319
+ ];
320
+ const currentType = items[selectedIndex].type;
321
+ const currentIndex = types.indexOf(currentType);
322
+ const prevIndex = currentIndex === 0 ? types.length - 1 : currentIndex - 1;
323
+ const newItems = [...items];
324
+ newItems[selectedIndex] = { ...newItems[selectedIndex], type: types[prevIndex] };
325
+ onUpdate(newItems);
326
+ } else if (key.rightArrow && items.length > 0) {
327
+ const types = [
328
+ "model",
329
+ "git-branch",
330
+ "separator",
331
+ "flex-separator",
332
+ "tokens-input",
333
+ "tokens-output",
334
+ "tokens-cached",
335
+ "tokens-total",
336
+ "context-length",
337
+ "context-percentage"
338
+ ];
339
+ const currentType = items[selectedIndex].type;
340
+ const currentIndex = types.indexOf(currentType);
341
+ const nextIndex = (currentIndex + 1) % types.length;
342
+ const newItems = [...items];
343
+ newItems[selectedIndex] = { ...newItems[selectedIndex], type: types[nextIndex] };
344
+ onUpdate(newItems);
345
+ } else if (key.return && items.length > 0) {
346
+ setMoveMode(true);
347
+ } else if (input === "a") {
348
+ const newItem = {
349
+ id: Date.now().toString(),
350
+ type: "separator"
351
+ };
352
+ onUpdate([...items, newItem]);
353
+ } else if (input === "i") {
354
+ const newItem = {
355
+ id: Date.now().toString(),
356
+ type: "separator"
357
+ };
358
+ const newItems = [...items];
359
+ newItems.splice(selectedIndex + 1, 0, newItem);
360
+ onUpdate(newItems);
361
+ setSelectedIndex(selectedIndex + 1);
362
+ } else if (input === "d" && items.length > 0) {
363
+ const newItems = items.filter((_, i) => i !== selectedIndex);
364
+ onUpdate(newItems);
365
+ if (selectedIndex >= newItems.length && selectedIndex > 0) {
366
+ setSelectedIndex(selectedIndex - 1);
367
+ }
368
+ } else if (key.escape) {
369
+ onBack();
370
+ }
371
+ }
372
+ });
373
+ const getItemDisplay = (item) => {
374
+ switch (item.type) {
375
+ case "model":
376
+ return chalk.cyan("Model");
377
+ case "git-branch":
378
+ return chalk.magenta("Git Branch");
379
+ case "separator":
380
+ return chalk.dim("Separator |");
381
+ case "flex-separator":
382
+ return chalk.yellow("Flex Separator ─────");
383
+ case "tokens-input":
384
+ return chalk.yellow("Tokens Input");
385
+ case "tokens-output":
386
+ return chalk.green("Tokens Output");
387
+ case "tokens-cached":
388
+ return chalk.blue("Tokens Cached");
389
+ case "tokens-total":
390
+ return chalk.white("Tokens Total");
391
+ case "context-length":
392
+ return chalk.cyan("Context Length");
393
+ case "context-percentage":
394
+ return chalk.cyan("Context %");
395
+ }
396
+ };
397
+ return /* @__PURE__ */ jsxDEV(Box, {
398
+ flexDirection: "column",
399
+ children: [
400
+ /* @__PURE__ */ jsxDEV(Text, {
401
+ bold: true,
402
+ children: [
403
+ "Edit Status Line Items ",
404
+ moveMode && /* @__PURE__ */ jsxDEV(Text, {
405
+ color: "yellow",
406
+ children: "[MOVE MODE]"
407
+ }, undefined, false, undefined, this)
408
+ ]
409
+ }, undefined, true, undefined, this),
410
+ moveMode ? /* @__PURE__ */ jsxDEV(Text, {
411
+ dim: true,
412
+ children: "↑↓ to move item, ESC or Enter to exit move mode"
413
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV(Text, {
414
+ dim: true,
415
+ children: "↑↓ select, ←→ change type, Enter to move, (a)dd, (i)nsert, (d)elete, ESC back"
416
+ }, undefined, false, undefined, this),
417
+ /* @__PURE__ */ jsxDEV(Box, {
418
+ marginTop: 1,
419
+ flexDirection: "column",
420
+ children: items.length === 0 ? /* @__PURE__ */ jsxDEV(Text, {
421
+ dim: true,
422
+ children: "No items. Press 'a' to add one."
423
+ }, undefined, false, undefined, this) : items.map((item, index) => /* @__PURE__ */ jsxDEV(Box, {
424
+ children: /* @__PURE__ */ jsxDEV(Text, {
425
+ color: index === selectedIndex ? moveMode ? "yellow" : "green" : undefined,
426
+ children: [
427
+ index === selectedIndex ? moveMode ? "◆ " : "▶ " : " ",
428
+ index + 1,
429
+ ". ",
430
+ getItemDisplay(item)
431
+ ]
432
+ }, undefined, true, undefined, this)
433
+ }, item.id, false, undefined, this))
434
+ }, undefined, false, undefined, this)
435
+ ]
436
+ }, undefined, true, undefined, this);
437
+ };
438
+ var ColorMenu = ({ items, onUpdate, onBack }) => {
439
+ const colorableItems = items.filter((item) => ["model", "git-branch", "tokens-input", "tokens-output", "tokens-cached", "tokens-total", "context-length", "context-percentage"].includes(item.type));
440
+ const [selectedIndex, setSelectedIndex] = useState(0);
441
+ useInput((input, key) => {
442
+ if (key.escape) {
443
+ onBack();
444
+ }
445
+ });
446
+ if (colorableItems.length === 0) {
447
+ return /* @__PURE__ */ jsxDEV(Box, {
448
+ flexDirection: "column",
449
+ children: [
450
+ /* @__PURE__ */ jsxDEV(Text, {
451
+ bold: true,
452
+ children: "Configure Colors"
453
+ }, undefined, false, undefined, this),
454
+ /* @__PURE__ */ jsxDEV(Text, {
455
+ dim: true,
456
+ marginTop: 1,
457
+ children: "No colorable items in the status line."
458
+ }, undefined, false, undefined, this),
459
+ /* @__PURE__ */ jsxDEV(Text, {
460
+ dim: true,
461
+ children: "Add a Model or Git Branch item first."
462
+ }, undefined, false, undefined, this),
463
+ /* @__PURE__ */ jsxDEV(Text, {
464
+ marginTop: 1,
465
+ children: "Press any key to go back..."
466
+ }, undefined, false, undefined, this),
467
+ useInput(() => onBack())
468
+ ]
469
+ }, undefined, true, undefined, this);
470
+ }
471
+ const getItemLabel = (item) => {
472
+ switch (item.type) {
473
+ case "model":
474
+ return "Model";
475
+ case "git-branch":
476
+ return "Git Branch";
477
+ case "tokens-input":
478
+ return "Tokens Input";
479
+ case "tokens-output":
480
+ return "Tokens Output";
481
+ case "tokens-cached":
482
+ return "Tokens Cached";
483
+ case "tokens-total":
484
+ return "Tokens Total";
485
+ case "context-length":
486
+ return "Context Length";
487
+ case "context-percentage":
488
+ return "Context Percentage";
489
+ default:
490
+ return item.type;
491
+ }
492
+ };
493
+ const menuItems = colorableItems.map((item, index) => {
494
+ const color = item.color || "white";
495
+ const colorFunc = chalk[color] || chalk.white;
496
+ return {
497
+ label: colorFunc(`${getItemLabel(item)} #${index + 1}`),
498
+ value: item.id
499
+ };
500
+ });
501
+ menuItems.push({ label: "← Back", value: "back" });
502
+ const handleSelect = (selected) => {
503
+ if (selected.value === "back") {
504
+ onBack();
505
+ } else {
506
+ const newItems = items.map((item) => {
507
+ if (item.id === selected.value) {
508
+ const currentColorIndex = colors.indexOf(item.color || "white");
509
+ const nextColor = colors[(currentColorIndex + 1) % colors.length];
510
+ return { ...item, color: nextColor };
511
+ }
512
+ return item;
513
+ });
514
+ onUpdate(newItems);
515
+ }
516
+ };
517
+ const handleHighlight = (item) => {
518
+ if (item.value !== "back") {
519
+ const itemIndex = colorableItems.findIndex((i) => i.id === item.value);
520
+ if (itemIndex !== -1) {
521
+ setSelectedIndex(itemIndex);
522
+ }
523
+ }
524
+ };
525
+ const colors = [
526
+ "black",
527
+ "red",
528
+ "green",
529
+ "yellow",
530
+ "blue",
531
+ "magenta",
532
+ "cyan",
533
+ "white",
534
+ "gray",
535
+ "redBright",
536
+ "greenBright",
537
+ "yellowBright",
538
+ "blueBright",
539
+ "magentaBright",
540
+ "cyanBright",
541
+ "whiteBright"
542
+ ];
543
+ const selectedItem = selectedIndex < colorableItems.length ? colorableItems[selectedIndex] : null;
544
+ const currentColor = selectedItem ? selectedItem.color || "white" : "white";
545
+ const colorIndex = colors.indexOf(currentColor);
546
+ const colorNumber = colorIndex === -1 ? 8 : colorIndex + 1;
547
+ const colorDisplay = chalk[currentColor] ? chalk[currentColor](currentColor) : chalk.white(currentColor);
548
+ return /* @__PURE__ */ jsxDEV(Box, {
549
+ flexDirection: "column",
550
+ children: [
551
+ /* @__PURE__ */ jsxDEV(Text, {
552
+ bold: true,
553
+ children: "Configure Colors"
554
+ }, undefined, false, undefined, this),
555
+ /* @__PURE__ */ jsxDEV(Text, {
556
+ dim: true,
557
+ children: "↑↓ to select item, Enter to cycle color, ESC to go back"
558
+ }, undefined, false, undefined, this),
559
+ selectedItem && /* @__PURE__ */ jsxDEV(Text, {
560
+ marginTop: 1,
561
+ children: [
562
+ "Current color (",
563
+ colorNumber,
564
+ "/",
565
+ colors.length,
566
+ "): ",
567
+ colorDisplay
568
+ ]
569
+ }, undefined, true, undefined, this),
570
+ /* @__PURE__ */ jsxDEV(Box, {
571
+ marginTop: 1,
572
+ children: /* @__PURE__ */ jsxDEV(SelectInput, {
573
+ items: menuItems,
574
+ onSelect: handleSelect,
575
+ onHighlight: handleHighlight,
576
+ initialIndex: selectedIndex
577
+ }, undefined, false, undefined, this)
578
+ }, undefined, false, undefined, this)
579
+ ]
580
+ }, undefined, true, undefined, this);
581
+ };
582
+ var App = () => {
583
+ const { exit } = useApp();
584
+ const [settings, setSettings] = useState(null);
585
+ const [originalSettings, setOriginalSettings] = useState(null);
586
+ const [hasChanges, setHasChanges] = useState(false);
587
+ const [screen, setScreen] = useState("main");
588
+ const [confirmDialog, setConfirmDialog] = useState(null);
589
+ const [isClaudeInstalled, setIsClaudeInstalled] = useState(false);
590
+ const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
591
+ useEffect(() => {
592
+ loadSettings().then((loadedSettings) => {
593
+ setSettings(loadedSettings);
594
+ setOriginalSettings(JSON.parse(JSON.stringify(loadedSettings)));
595
+ });
596
+ isInstalled().then(setIsClaudeInstalled);
597
+ const handleResize = () => {
598
+ setTerminalWidth(process.stdout.columns || 80);
599
+ };
600
+ process.stdout.on("resize", handleResize);
601
+ return () => {
602
+ process.stdout.off("resize", handleResize);
603
+ };
604
+ }, []);
605
+ useEffect(() => {
606
+ if (settings && originalSettings) {
607
+ const hasAnyChanges = JSON.stringify(settings) !== JSON.stringify(originalSettings);
608
+ setHasChanges(hasAnyChanges);
609
+ }
610
+ }, [settings, originalSettings]);
611
+ useInput((input, key) => {
612
+ if (key.ctrl && input === "c") {
613
+ exit();
614
+ }
615
+ });
616
+ if (!settings) {
617
+ return /* @__PURE__ */ jsxDEV(Text, {
618
+ children: "Loading settings..."
619
+ }, undefined, false, undefined, this);
620
+ }
621
+ const handleInstallUninstall = async () => {
622
+ if (isClaudeInstalled) {
623
+ setConfirmDialog({
624
+ message: "This will remove ccstatusline from ~/.claude/settings.json. Continue?",
625
+ action: async () => {
626
+ await uninstallStatusLine();
627
+ setIsClaudeInstalled(false);
628
+ setScreen("main");
629
+ setConfirmDialog(null);
630
+ }
631
+ });
632
+ setScreen("confirm");
633
+ } else {
634
+ const existing = await getExistingStatusLine();
635
+ let message;
636
+ if (existing && existing !== "npx ccstatusline") {
637
+ message = `This will modify ~/.claude/settings.json
638
+
639
+ A status line is already configured: "${existing}"
640
+ Replace it with ccstatusline?`;
641
+ } else if (existing === "npx ccstatusline") {
642
+ message = `ccstatusline is already installed in ~/.claude/settings.json
643
+ Reinstall it?`;
644
+ } else {
645
+ message = `This will modify ~/.claude/settings.json to add ccstatusline.
646
+ Continue?`;
647
+ }
648
+ setConfirmDialog({
649
+ message,
650
+ action: async () => {
651
+ await installStatusLine();
652
+ setIsClaudeInstalled(true);
653
+ setScreen("main");
654
+ setConfirmDialog(null);
655
+ }
656
+ });
657
+ setScreen("confirm");
658
+ }
659
+ };
660
+ const handleMainMenuSelect = async (value) => {
661
+ switch (value) {
662
+ case "items":
663
+ setScreen("items");
664
+ break;
665
+ case "colors":
666
+ setScreen("colors");
667
+ break;
668
+ case "install":
669
+ await handleInstallUninstall();
670
+ break;
671
+ case "save":
672
+ await saveSettings(settings);
673
+ setOriginalSettings(JSON.parse(JSON.stringify(settings)));
674
+ setHasChanges(false);
675
+ exit();
676
+ break;
677
+ case "exit":
678
+ exit();
679
+ break;
680
+ }
681
+ };
682
+ const updateItems = (items) => {
683
+ setSettings({ ...settings, items });
684
+ };
685
+ return /* @__PURE__ */ jsxDEV(Box, {
686
+ flexDirection: "column",
687
+ padding: 1,
688
+ children: [
689
+ /* @__PURE__ */ jsxDEV(Box, {
690
+ marginBottom: 1,
691
+ children: /* @__PURE__ */ jsxDEV(Text, {
692
+ bold: true,
693
+ color: "cyan",
694
+ children: "\uD83C\uDFA8 CCStatusline Configuration"
695
+ }, undefined, false, undefined, this)
696
+ }, undefined, false, undefined, this),
697
+ /* @__PURE__ */ jsxDEV(Box, {
698
+ marginBottom: 1,
699
+ children: /* @__PURE__ */ jsxDEV(Text, {
700
+ dim: true,
701
+ children: "Preview:"
702
+ }, undefined, false, undefined, this)
703
+ }, undefined, false, undefined, this),
704
+ /* @__PURE__ */ jsxDEV(StatusLinePreview, {
705
+ items: settings.items,
706
+ terminalWidth
707
+ }, undefined, false, undefined, this),
708
+ /* @__PURE__ */ jsxDEV(Box, {
709
+ marginTop: 2,
710
+ children: [
711
+ screen === "main" && /* @__PURE__ */ jsxDEV(MainMenu, {
712
+ onSelect: handleMainMenuSelect,
713
+ isClaudeInstalled,
714
+ hasChanges
715
+ }, undefined, false, undefined, this),
716
+ screen === "items" && /* @__PURE__ */ jsxDEV(ItemsEditor, {
717
+ items: settings.items,
718
+ onUpdate: updateItems,
719
+ onBack: () => setScreen("main")
720
+ }, undefined, false, undefined, this),
721
+ screen === "colors" && /* @__PURE__ */ jsxDEV(ColorMenu, {
722
+ items: settings.items,
723
+ onUpdate: updateItems,
724
+ onBack: () => setScreen("main")
725
+ }, undefined, false, undefined, this),
726
+ screen === "confirm" && confirmDialog && /* @__PURE__ */ jsxDEV(ConfirmDialog, {
727
+ message: confirmDialog.message,
728
+ onConfirm: confirmDialog.action,
729
+ onCancel: () => {
730
+ setScreen("main");
731
+ setConfirmDialog(null);
732
+ }
733
+ }, undefined, false, undefined, this)
734
+ ]
735
+ }, undefined, true, undefined, this)
736
+ ]
737
+ }, undefined, true, undefined, this);
738
+ };
739
+ function runTUI() {
740
+ render(/* @__PURE__ */ jsxDEV(App, {}, undefined, false, undefined, this));
741
+ }
742
+
743
+ // src/ccstatusline.ts
744
+ import * as fs3 from "fs";
745
+ import { promisify as promisify3 } from "util";
746
+ var readFile6 = fs3.promises?.readFile || promisify3(fs3.readFile);
747
+ chalk2.level = 3;
748
+ async function readStdin() {
749
+ if (process.stdin.isTTY) {
750
+ return null;
751
+ }
752
+ const chunks = [];
753
+ try {
754
+ if (typeof Bun !== "undefined" && Bun.stdin) {
755
+ const decoder = new TextDecoder;
756
+ for await (const chunk of Bun.stdin.stream()) {
757
+ chunks.push(decoder.decode(chunk));
758
+ }
759
+ } else {
760
+ process.stdin.setEncoding("utf8");
761
+ for await (const chunk of process.stdin) {
762
+ chunks.push(chunk);
763
+ }
764
+ }
765
+ return chunks.join("");
766
+ } catch {
767
+ return null;
768
+ }
769
+ }
770
+ function getGitBranch() {
771
+ try {
772
+ const branch = execSync("git branch --show-current 2>/dev/null", {
773
+ encoding: "utf8",
774
+ stdio: ["pipe", "pipe", "ignore"]
775
+ }).trim();
776
+ return branch || null;
777
+ } catch {
778
+ return null;
779
+ }
780
+ }
781
+ async function getTokenMetrics(transcriptPath) {
782
+ try {
783
+ if (!fs3.existsSync(transcriptPath)) {
784
+ return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
785
+ }
786
+ const content = await readFile6(transcriptPath, "utf-8");
787
+ const lines = content.trim().split(`
788
+ `);
789
+ let inputTokens = 0;
790
+ let outputTokens = 0;
791
+ let cachedTokens = 0;
792
+ for (const line of lines) {
793
+ try {
794
+ const data = JSON.parse(line);
795
+ if (data.message?.usage) {
796
+ inputTokens += data.message.usage.input_tokens || 0;
797
+ outputTokens += data.message.usage.output_tokens || 0;
798
+ cachedTokens += data.message.usage.cache_read_input_tokens || 0;
799
+ cachedTokens += data.message.usage.cache_creation_input_tokens || 0;
800
+ }
801
+ } catch {}
802
+ }
803
+ const totalTokens = inputTokens + outputTokens + cachedTokens;
804
+ const contextLength = inputTokens + outputTokens;
805
+ return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
806
+ } catch {
807
+ return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
808
+ }
809
+ }
810
+ async function renderStatusLine(data) {
811
+ const settings = await loadSettings();
812
+ const terminalWidth = 80;
813
+ const elements = [];
814
+ let hasFlexSeparator = false;
815
+ const hasTokenItems = settings.items.some((item) => ["tokens-input", "tokens-output", "tokens-cached", "tokens-total", "context-length", "context-percentage"].includes(item.type));
816
+ let tokenMetrics = null;
817
+ if (hasTokenItems && data.transcript_path) {
818
+ tokenMetrics = await getTokenMetrics(data.transcript_path);
819
+ }
820
+ const formatTokens = (count) => {
821
+ if (count >= 1e6)
822
+ return `${(count / 1e6).toFixed(1)}M`;
823
+ if (count >= 1000)
824
+ return `${(count / 1000).toFixed(1)}k`;
825
+ return count.toString();
826
+ };
827
+ for (const item of settings.items) {
828
+ switch (item.type) {
829
+ case "model":
830
+ if (data.model) {
831
+ const color = chalk2[item.color || settings.colors.model] || chalk2.cyan;
832
+ elements.push({ content: color(`Model: ${data.model.display_name}`), type: "model" });
833
+ }
834
+ break;
835
+ case "git-branch":
836
+ const branch = getGitBranch();
837
+ if (branch) {
838
+ const color = chalk2[item.color || settings.colors.gitBranch] || chalk2.magenta;
839
+ elements.push({ content: color(`⎇ ${branch}`), type: "git-branch" });
840
+ }
841
+ break;
842
+ case "tokens-input":
843
+ if (tokenMetrics) {
844
+ const color = chalk2[item.color || "yellow"] || chalk2.yellow;
845
+ elements.push({ content: color(`In: ${formatTokens(tokenMetrics.inputTokens)}`), type: "tokens-input" });
846
+ }
847
+ break;
848
+ case "tokens-output":
849
+ if (tokenMetrics) {
850
+ const color = chalk2[item.color || "green"] || chalk2.green;
851
+ elements.push({ content: color(`Out: ${formatTokens(tokenMetrics.outputTokens)}`), type: "tokens-output" });
852
+ }
853
+ break;
854
+ case "tokens-cached":
855
+ if (tokenMetrics) {
856
+ const color = chalk2[item.color || "blue"] || chalk2.blue;
857
+ elements.push({ content: color(`Cached: ${formatTokens(tokenMetrics.cachedTokens)}`), type: "tokens-cached" });
858
+ }
859
+ break;
860
+ case "tokens-total":
861
+ if (tokenMetrics) {
862
+ const color = chalk2[item.color || "white"] || chalk2.white;
863
+ elements.push({ content: color(`Total: ${formatTokens(tokenMetrics.totalTokens)}`), type: "tokens-total" });
864
+ }
865
+ break;
866
+ case "context-length":
867
+ if (tokenMetrics) {
868
+ const color = chalk2[item.color || "cyan"] || chalk2.cyan;
869
+ elements.push({ content: color(`Ctx: ${formatTokens(tokenMetrics.contextLength)}`), type: "context-length" });
870
+ }
871
+ break;
872
+ case "context-percentage":
873
+ if (tokenMetrics) {
874
+ const percentage = Math.min(100, tokenMetrics.contextLength / 200000 * 100);
875
+ const color = chalk2[item.color || "cyan"] || chalk2.cyan;
876
+ elements.push({ content: color(`Ctx: ${percentage.toFixed(1)}%`), type: "context-percentage" });
877
+ }
878
+ break;
879
+ case "separator":
880
+ if (elements.length > 0 && elements[elements.length - 1].type !== "separator") {
881
+ const sepColor = chalk2[settings.colors.separator] || chalk2.dim;
882
+ elements.push({ content: sepColor(" | "), type: "separator" });
883
+ }
884
+ break;
885
+ case "flex-separator":
886
+ elements.push({ content: "FLEX", type: "flex-separator" });
887
+ hasFlexSeparator = true;
888
+ break;
889
+ }
890
+ }
891
+ if (elements.length === 0)
892
+ return;
893
+ let statusLine = "";
894
+ if (hasFlexSeparator) {
895
+ const parts = [[]];
896
+ let currentPart = 0;
897
+ for (const elem of elements) {
898
+ if (elem.type === "flex-separator") {
899
+ currentPart++;
900
+ parts[currentPart] = [];
901
+ } else {
902
+ parts[currentPart].push(elem.content);
903
+ }
904
+ }
905
+ const partLengths = parts.map((part) => {
906
+ const joined = part.join("");
907
+ return joined.replace(/\x1b\[[0-9;]*m/g, "").length;
908
+ });
909
+ const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
910
+ const flexCount = parts.length - 1;
911
+ const totalSpace = Math.max(0, terminalWidth - totalContentLength);
912
+ const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
913
+ const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
914
+ statusLine = "";
915
+ for (let i = 0;i < parts.length; i++) {
916
+ statusLine += parts[i].join("");
917
+ if (i < parts.length - 1) {
918
+ const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
919
+ statusLine += " ".repeat(spaces);
920
+ }
921
+ }
922
+ } else {
923
+ statusLine = elements.map((e) => e.content).join("");
924
+ const contentLength = statusLine.replace(/\x1b\[[0-9;]*m/g, "").length;
925
+ const remainingSpace = terminalWidth - contentLength;
926
+ if (remainingSpace > 0) {
927
+ statusLine = statusLine + " ".repeat(remainingSpace);
928
+ }
929
+ }
930
+ const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, "").length;
931
+ if (plainLength > 80) {
932
+ const visibleText = statusLine.replace(/\x1b\[[0-9;]*m/g, "");
933
+ const truncated = visibleText.substring(0, 77) + "...";
934
+ console.log(truncated);
935
+ } else {
936
+ console.log(statusLine);
937
+ }
938
+ }
939
+ async function main() {
940
+ if (!process.stdin.isTTY) {
941
+ const input = await readStdin();
942
+ if (input && input.trim() !== "") {
943
+ try {
944
+ const data = JSON.parse(input);
945
+ await renderStatusLine(data);
946
+ } catch (error) {
947
+ console.error("Error parsing JSON:", error);
948
+ process.exit(1);
949
+ }
950
+ } else {
951
+ console.error("No input received");
952
+ process.exit(1);
953
+ }
954
+ } else {
955
+ await runTUI();
956
+ }
957
+ }
958
+ main();
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "ccstatusline",
3
+ "version": "1.0.0",
4
+ "description": "A customizable status line formatter for Claude Code CLI",
5
+ "module": "src/ccstatusline.ts",
6
+ "type": "module",
7
+ "bin": {
8
+ "ccstatusline": "dist/ccstatusline.js"
9
+ },
10
+ "files": [
11
+ "dist/"
12
+ ],
13
+ "scripts": {
14
+ "start": "bun run src/ccstatusline.ts",
15
+ "build": "bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --packages=external --target-version=14",
16
+ "prepublishOnly": "bun run build"
17
+ },
18
+ "devDependencies": {
19
+ "@types/bun": "latest"
20
+ },
21
+ "peerDependencies": {
22
+ "typescript": "^5"
23
+ },
24
+ "dependencies": {
25
+ "@types/react": "^19.1.9",
26
+ "chalk": "^5.5.0",
27
+ "ink": "^6.1.0",
28
+ "ink-select-input": "^6.2.0",
29
+ "ink-text-input": "^6.0.0",
30
+ "react": "^19.1.1"
31
+ },
32
+ "keywords": [
33
+ "claude",
34
+ "claude-code",
35
+ "cli",
36
+ "status-line",
37
+ "terminal"
38
+ ],
39
+ "author": "",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/sirmalloc/ccstatusline.git"
44
+ },
45
+ "engines": {
46
+ "node": ">=14.0.0"
47
+ }
48
+ }