ccstatusline 1.0.0 → 1.0.2
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 +164 -0
- package/dist/ccstatusline.js +86 -47
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# ccstatusline
|
|
2
|
+
|
|
3
|
+
A customizable status line formatter for Claude Code CLI that displays model info, git branch, token usage, and other metrics in your terminal.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 📊 **Real-time metrics** - Display model name, git branch, token usage, and more
|
|
8
|
+
- 🎨 **Fully customizable** - Choose what to display and customize colors
|
|
9
|
+
- 🖥️ **Interactive TUI** - Built-in configuration interface using React/Ink
|
|
10
|
+
- 🚀 **Cross-platform** - Works with both Bun and Node.js
|
|
11
|
+
- 📏 **80-character format** - Perfectly sized for Claude Code CLI integration
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
No installation needed! Use directly with npx:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Run the configuration TUI
|
|
19
|
+
npx ccstatusline
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
### Configure ccstatusline
|
|
25
|
+
|
|
26
|
+
Run the interactive configuration tool:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx ccstatusline
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This launches a TUI where you can:
|
|
33
|
+
- Add/remove status line items
|
|
34
|
+
- Reorder items with arrow keys
|
|
35
|
+
- Customize colors for each element
|
|
36
|
+
- Preview your status line in real-time
|
|
37
|
+
|
|
38
|
+
Your settings are saved to `~/.config/ccstatusline/settings.json`.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
Once configured, ccstatusline automatically formats your Claude Code status line. The status line appears at the bottom of your terminal during Claude Code sessions.
|
|
43
|
+
|
|
44
|
+
### Available Status Items
|
|
45
|
+
|
|
46
|
+
- **Model Name** - Shows the current Claude model (e.g., "Claude 3.5 Sonnet")
|
|
47
|
+
- **Git Branch** - Displays current git branch name
|
|
48
|
+
- **Token Usage** - Shows input/output/total tokens used
|
|
49
|
+
- **Time** - Current time in HH:MM:SS format
|
|
50
|
+
- **Custom Text** - Add your own static text
|
|
51
|
+
- **Separator** - Visual divider between items
|
|
52
|
+
- **Flex Separator** - Expands to fill available space
|
|
53
|
+
|
|
54
|
+
## Configuration File
|
|
55
|
+
|
|
56
|
+
The configuration file at `~/.config/ccstatusline/settings.json` looks like:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"items": [
|
|
61
|
+
{
|
|
62
|
+
"type": "model",
|
|
63
|
+
"color": "cyan"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"type": "separator",
|
|
67
|
+
"text": " │ ",
|
|
68
|
+
"color": "gray"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"type": "git_branch",
|
|
72
|
+
"color": "green"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"type": "separator",
|
|
76
|
+
"text": " │ ",
|
|
77
|
+
"color": "gray"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"type": "tokens",
|
|
81
|
+
"color": "yellow"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"type": "flex_separator",
|
|
85
|
+
"text": "─",
|
|
86
|
+
"color": "gray"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"type": "time",
|
|
90
|
+
"color": "blue"
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Color Options
|
|
97
|
+
|
|
98
|
+
Available colors:
|
|
99
|
+
- `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`
|
|
100
|
+
- `brightBlack`, `brightRed`, `brightGreen`, `brightYellow`, `brightBlue`, `brightMagenta`, `brightCyan`, `brightWhite`
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
### Prerequisites
|
|
105
|
+
|
|
106
|
+
- [Bun](https://bun.sh)
|
|
107
|
+
- Git
|
|
108
|
+
|
|
109
|
+
### Setup
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Clone the repository
|
|
113
|
+
git clone https://github.com/yourusername/ccstatusline.git
|
|
114
|
+
cd ccstatusline
|
|
115
|
+
|
|
116
|
+
# Install dependencies
|
|
117
|
+
bun install
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Development Commands
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Run in TUI mode (configuration)
|
|
124
|
+
bun run src/ccstatusline.ts
|
|
125
|
+
|
|
126
|
+
# Build for distribution
|
|
127
|
+
bun run build
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Project Structure
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
ccstatusline/
|
|
134
|
+
├── src/
|
|
135
|
+
│ ├── ccstatusline.ts # Main entry point
|
|
136
|
+
│ ├── tui.tsx # React/Ink configuration UI
|
|
137
|
+
│ ├── config.ts # Settings management
|
|
138
|
+
│ └── claude-settings.ts # Claude Code settings integration
|
|
139
|
+
├── dist/ # Built files (generated)
|
|
140
|
+
├── package.json
|
|
141
|
+
├── tsconfig.json
|
|
142
|
+
└── README.md
|
|
143
|
+
```
|
|
144
|
+
## Contributing
|
|
145
|
+
|
|
146
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
147
|
+
|
|
148
|
+
1. Fork the repository
|
|
149
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
150
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
151
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
152
|
+
5. Open a Pull Request
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
157
|
+
|
|
158
|
+
## Author
|
|
159
|
+
|
|
160
|
+
Matthew Breedlove
|
|
161
|
+
|
|
162
|
+
## Acknowledgments
|
|
163
|
+
|
|
164
|
+
Built for use with [Claude Code CLI](https://claude.ai/code) by Anthropic.
|
package/dist/ccstatusline.js
CHANGED
|
@@ -103,13 +103,13 @@ async function saveClaudeSettings(settings) {
|
|
|
103
103
|
}
|
|
104
104
|
async function isInstalled() {
|
|
105
105
|
const settings = await loadClaudeSettings();
|
|
106
|
-
return settings.statusLine?.command === "npx ccstatusline";
|
|
106
|
+
return settings.statusLine?.command === "npx -y ccstatusline";
|
|
107
107
|
}
|
|
108
108
|
async function installStatusLine() {
|
|
109
109
|
const settings = await loadClaudeSettings();
|
|
110
110
|
settings.statusLine = {
|
|
111
111
|
type: "command",
|
|
112
|
-
command: "npx ccstatusline",
|
|
112
|
+
command: "npx -y ccstatusline",
|
|
113
113
|
padding: 1
|
|
114
114
|
};
|
|
115
115
|
await saveClaudeSettings(settings);
|
|
@@ -180,12 +180,13 @@ var StatusLinePreview = ({ items, terminalWidth }) => {
|
|
|
180
180
|
const parts = [[]];
|
|
181
181
|
let currentPart = 0;
|
|
182
182
|
for (let i = 0;i < items.length; i++) {
|
|
183
|
-
|
|
183
|
+
const item = items[i];
|
|
184
|
+
if (item && item.type === "flex-separator") {
|
|
184
185
|
currentPart++;
|
|
185
186
|
parts[currentPart] = [];
|
|
186
187
|
} else {
|
|
187
188
|
const element = elements[i];
|
|
188
|
-
if (element !== "FLEX") {
|
|
189
|
+
if (element !== "FLEX" && parts[currentPart]) {
|
|
189
190
|
parts[currentPart].push(element);
|
|
190
191
|
}
|
|
191
192
|
}
|
|
@@ -201,7 +202,10 @@ var StatusLinePreview = ({ items, terminalWidth }) => {
|
|
|
201
202
|
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
|
|
202
203
|
statusLine = "";
|
|
203
204
|
for (let i = 0;i < parts.length; i++) {
|
|
204
|
-
|
|
205
|
+
const part = parts[i];
|
|
206
|
+
if (part) {
|
|
207
|
+
statusLine += part.join("");
|
|
208
|
+
}
|
|
205
209
|
if (i < parts.length - 1) {
|
|
206
210
|
const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
|
|
207
211
|
statusLine += " ".repeat(spaces);
|
|
@@ -288,12 +292,20 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
288
292
|
if (moveMode) {
|
|
289
293
|
if (key.upArrow && selectedIndex > 0) {
|
|
290
294
|
const newItems = [...items];
|
|
291
|
-
|
|
295
|
+
const temp = newItems[selectedIndex];
|
|
296
|
+
const prev = newItems[selectedIndex - 1];
|
|
297
|
+
if (temp && prev) {
|
|
298
|
+
[newItems[selectedIndex], newItems[selectedIndex - 1]] = [prev, temp];
|
|
299
|
+
}
|
|
292
300
|
onUpdate(newItems);
|
|
293
301
|
setSelectedIndex(selectedIndex - 1);
|
|
294
302
|
} else if (key.downArrow && selectedIndex < items.length - 1) {
|
|
295
303
|
const newItems = [...items];
|
|
296
|
-
|
|
304
|
+
const temp = newItems[selectedIndex];
|
|
305
|
+
const next = newItems[selectedIndex + 1];
|
|
306
|
+
if (temp && next) {
|
|
307
|
+
[newItems[selectedIndex], newItems[selectedIndex + 1]] = [next, temp];
|
|
308
|
+
}
|
|
297
309
|
onUpdate(newItems);
|
|
298
310
|
setSelectedIndex(selectedIndex + 1);
|
|
299
311
|
} else if (key.escape || key.return) {
|
|
@@ -317,12 +329,18 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
317
329
|
"context-length",
|
|
318
330
|
"context-percentage"
|
|
319
331
|
];
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
332
|
+
const currentItem = items[selectedIndex];
|
|
333
|
+
if (currentItem) {
|
|
334
|
+
const currentType = currentItem.type;
|
|
335
|
+
const currentIndex = types.indexOf(currentType);
|
|
336
|
+
const prevIndex = currentIndex === 0 ? types.length - 1 : currentIndex - 1;
|
|
337
|
+
const newItems = [...items];
|
|
338
|
+
const prevType = types[prevIndex];
|
|
339
|
+
if (prevType) {
|
|
340
|
+
newItems[selectedIndex] = { ...currentItem, type: prevType };
|
|
341
|
+
onUpdate(newItems);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
326
344
|
} else if (key.rightArrow && items.length > 0) {
|
|
327
345
|
const types = [
|
|
328
346
|
"model",
|
|
@@ -336,12 +354,18 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
336
354
|
"context-length",
|
|
337
355
|
"context-percentage"
|
|
338
356
|
];
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
357
|
+
const currentItem = items[selectedIndex];
|
|
358
|
+
if (currentItem) {
|
|
359
|
+
const currentType = currentItem.type;
|
|
360
|
+
const currentIndex = types.indexOf(currentType);
|
|
361
|
+
const nextIndex = (currentIndex + 1) % types.length;
|
|
362
|
+
const newItems = [...items];
|
|
363
|
+
const nextType = types[nextIndex];
|
|
364
|
+
if (nextType) {
|
|
365
|
+
newItems[selectedIndex] = { ...currentItem, type: nextType };
|
|
366
|
+
onUpdate(newItems);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
345
369
|
} else if (key.return && items.length > 0) {
|
|
346
370
|
setMoveMode(true);
|
|
347
371
|
} else if (input === "a") {
|
|
@@ -408,17 +432,17 @@ var ItemsEditor = ({ items, onUpdate, onBack }) => {
|
|
|
408
432
|
]
|
|
409
433
|
}, undefined, true, undefined, this),
|
|
410
434
|
moveMode ? /* @__PURE__ */ jsxDEV(Text, {
|
|
411
|
-
|
|
435
|
+
dimColor: true,
|
|
412
436
|
children: "↑↓ to move item, ESC or Enter to exit move mode"
|
|
413
437
|
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV(Text, {
|
|
414
|
-
|
|
438
|
+
dimColor: true,
|
|
415
439
|
children: "↑↓ select, ←→ change type, Enter to move, (a)dd, (i)nsert, (d)elete, ESC back"
|
|
416
440
|
}, undefined, false, undefined, this),
|
|
417
441
|
/* @__PURE__ */ jsxDEV(Box, {
|
|
418
442
|
marginTop: 1,
|
|
419
443
|
flexDirection: "column",
|
|
420
444
|
children: items.length === 0 ? /* @__PURE__ */ jsxDEV(Text, {
|
|
421
|
-
|
|
445
|
+
dimColor: true,
|
|
422
446
|
children: "No items. Press 'a' to add one."
|
|
423
447
|
}, undefined, false, undefined, this) : items.map((item, index) => /* @__PURE__ */ jsxDEV(Box, {
|
|
424
448
|
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
@@ -451,20 +475,29 @@ var ColorMenu = ({ items, onUpdate, onBack }) => {
|
|
|
451
475
|
bold: true,
|
|
452
476
|
children: "Configure Colors"
|
|
453
477
|
}, undefined, false, undefined, this),
|
|
454
|
-
/* @__PURE__ */ jsxDEV(
|
|
455
|
-
dim: true,
|
|
478
|
+
/* @__PURE__ */ jsxDEV(Box, {
|
|
456
479
|
marginTop: 1,
|
|
457
|
-
children:
|
|
480
|
+
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
481
|
+
dimColor: true,
|
|
482
|
+
children: "No colorable items in the status line."
|
|
483
|
+
}, undefined, false, undefined, this)
|
|
458
484
|
}, undefined, false, undefined, this),
|
|
459
485
|
/* @__PURE__ */ jsxDEV(Text, {
|
|
460
|
-
|
|
486
|
+
dimColor: true,
|
|
461
487
|
children: "Add a Model or Git Branch item first."
|
|
462
488
|
}, undefined, false, undefined, this),
|
|
463
|
-
/* @__PURE__ */ jsxDEV(
|
|
489
|
+
/* @__PURE__ */ jsxDEV(Box, {
|
|
464
490
|
marginTop: 1,
|
|
465
|
-
children:
|
|
491
|
+
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
492
|
+
children: "Press any key to go back..."
|
|
493
|
+
}, undefined, false, undefined, this)
|
|
466
494
|
}, undefined, false, undefined, this),
|
|
467
|
-
|
|
495
|
+
(() => {
|
|
496
|
+
useInput(() => {
|
|
497
|
+
onBack();
|
|
498
|
+
});
|
|
499
|
+
return null;
|
|
500
|
+
})()
|
|
468
501
|
]
|
|
469
502
|
}, undefined, true, undefined, this);
|
|
470
503
|
}
|
|
@@ -553,20 +586,22 @@ var ColorMenu = ({ items, onUpdate, onBack }) => {
|
|
|
553
586
|
children: "Configure Colors"
|
|
554
587
|
}, undefined, false, undefined, this),
|
|
555
588
|
/* @__PURE__ */ jsxDEV(Text, {
|
|
556
|
-
|
|
589
|
+
dimColor: true,
|
|
557
590
|
children: "↑↓ to select item, Enter to cycle color, ESC to go back"
|
|
558
591
|
}, undefined, false, undefined, this),
|
|
559
|
-
selectedItem && /* @__PURE__ */ jsxDEV(
|
|
592
|
+
selectedItem && /* @__PURE__ */ jsxDEV(Box, {
|
|
560
593
|
marginTop: 1,
|
|
561
|
-
children:
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
594
|
+
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
595
|
+
children: [
|
|
596
|
+
"Current color (",
|
|
597
|
+
colorNumber,
|
|
598
|
+
"/",
|
|
599
|
+
colors.length,
|
|
600
|
+
"): ",
|
|
601
|
+
colorDisplay
|
|
602
|
+
]
|
|
603
|
+
}, undefined, true, undefined, this)
|
|
604
|
+
}, undefined, false, undefined, this),
|
|
570
605
|
/* @__PURE__ */ jsxDEV(Box, {
|
|
571
606
|
marginTop: 1,
|
|
572
607
|
children: /* @__PURE__ */ jsxDEV(SelectInput, {
|
|
@@ -633,12 +668,12 @@ var App = () => {
|
|
|
633
668
|
} else {
|
|
634
669
|
const existing = await getExistingStatusLine();
|
|
635
670
|
let message;
|
|
636
|
-
if (existing && existing !== "npx ccstatusline") {
|
|
671
|
+
if (existing && existing !== "npx -y ccstatusline") {
|
|
637
672
|
message = `This will modify ~/.claude/settings.json
|
|
638
673
|
|
|
639
674
|
A status line is already configured: "${existing}"
|
|
640
|
-
Replace it with ccstatusline?`;
|
|
641
|
-
} else if (existing === "npx ccstatusline") {
|
|
675
|
+
Replace it with npx -y ccstatusline?`;
|
|
676
|
+
} else if (existing === "npx -y ccstatusline") {
|
|
642
677
|
message = `ccstatusline is already installed in ~/.claude/settings.json
|
|
643
678
|
Reinstall it?`;
|
|
644
679
|
} else {
|
|
@@ -697,7 +732,7 @@ Continue?`;
|
|
|
697
732
|
/* @__PURE__ */ jsxDEV(Box, {
|
|
698
733
|
marginBottom: 1,
|
|
699
734
|
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
700
|
-
|
|
735
|
+
dimColor: true,
|
|
701
736
|
children: "Preview:"
|
|
702
737
|
}, undefined, false, undefined, this)
|
|
703
738
|
}, undefined, false, undefined, this),
|
|
@@ -877,7 +912,8 @@ async function renderStatusLine(data) {
|
|
|
877
912
|
}
|
|
878
913
|
break;
|
|
879
914
|
case "separator":
|
|
880
|
-
|
|
915
|
+
const lastElement = elements[elements.length - 1];
|
|
916
|
+
if (elements.length > 0 && lastElement && lastElement.type !== "separator") {
|
|
881
917
|
const sepColor = chalk2[settings.colors.separator] || chalk2.dim;
|
|
882
918
|
elements.push({ content: sepColor(" | "), type: "separator" });
|
|
883
919
|
}
|
|
@@ -913,7 +949,10 @@ async function renderStatusLine(data) {
|
|
|
913
949
|
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
|
|
914
950
|
statusLine = "";
|
|
915
951
|
for (let i = 0;i < parts.length; i++) {
|
|
916
|
-
|
|
952
|
+
const part = parts[i];
|
|
953
|
+
if (part) {
|
|
954
|
+
statusLine += part.join("");
|
|
955
|
+
}
|
|
917
956
|
if (i < parts.length - 1) {
|
|
918
957
|
const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
|
|
919
958
|
statusLine += " ".repeat(spaces);
|
|
@@ -952,7 +991,7 @@ async function main() {
|
|
|
952
991
|
process.exit(1);
|
|
953
992
|
}
|
|
954
993
|
} else {
|
|
955
|
-
|
|
994
|
+
runTUI();
|
|
956
995
|
}
|
|
957
996
|
}
|
|
958
997
|
main();
|