@nutthead/cc-statusline 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -37
- package/bin/cc-statusline.js +72 -47
- package/biome.json +34 -0
- package/bunfig.toml +2 -0
- package/index.ts +8 -9
- package/package.json +7 -2
- package/src/cli.ts +110 -61
- package/src/logging.ts +3 -1
- package/src/schema/statusLine.ts +45 -0
- package/src/themes/defaultTheme.ts +86 -0
- package/src/{utils.ts → utils/git.ts} +4 -88
- package/src/utils/model.ts +23 -0
- package/src/utils/path.ts +95 -0
- package/src/utils/term.ts +20 -0
- package/src/defaultTheme.ts +0 -46
- package/src/statusLineSchema.ts +0 -39
package/README.md
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# statusline
|
|
2
|
+
|
|
3
|
+
Themeable status line provider for Claude Code.
|
|
4
|
+
|
|
5
|
+
## Preview
|
|
6
|
+
|
|
7
|
+
The default theme renders a two-row status line:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
🤖 opus-4-5 📃 93aba123-d123-4a6b-b1b5-2f3e7d111317 🗂️ ~/a/…/statusline
|
|
11
|
+
🌿 main 0.38%
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- **Row 1** (left to right):
|
|
15
|
+
- 🤖 Model ID (abbreviated)
|
|
16
|
+
- 📃 Session ID
|
|
17
|
+
- 🗂️ Project directory (compressed and telescoped)
|
|
18
|
+
|
|
19
|
+
- **Row 2** (left to right):
|
|
20
|
+
- 🌿 Git branch name (or commit hash if detached, 💾 if not a repo, 💥 on error)
|
|
21
|
+
- Context window usage percentage
|
|
2
22
|
|
|
3
23
|
## Install
|
|
4
24
|
|
|
@@ -6,11 +26,7 @@
|
|
|
6
26
|
bunx @nutthead/cc-statusline install
|
|
7
27
|
```
|
|
8
28
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## Configure
|
|
12
|
-
|
|
13
|
-
Add to `~/.claude/settings.json`:
|
|
29
|
+
Then add to `~/.claude/settings.json`:
|
|
14
30
|
|
|
15
31
|
```json
|
|
16
32
|
{
|
|
@@ -21,45 +37,68 @@ Add to `~/.claude/settings.json`:
|
|
|
21
37
|
}
|
|
22
38
|
```
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
1. Create a directory for the custom theme:
|
|
40
|
+
Use `--overwrite` to replace an existing installation.
|
|
27
41
|
|
|
28
|
-
|
|
29
|
-
mkdir ~/.config/cc-statusline
|
|
30
|
-
```
|
|
42
|
+
## Custom Themes
|
|
31
43
|
|
|
32
|
-
|
|
44
|
+
Create a JS file that default-exports a theme function (e.g. `~/.config/cc-statusline/theme.js`):
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// parse input
|
|
38
|
-
const json = JSON.parse(input);
|
|
46
|
+
```js
|
|
47
|
+
export default function theme(input) {
|
|
48
|
+
if (!input) return "";
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
const status = JSON.parse(input);
|
|
51
|
+
const dir = status.workspace.current_dir;
|
|
52
|
+
const model = status.model.display_name;
|
|
53
|
+
const ctx = status.context_window.used_percentage ?? 0;
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return "";
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
55
|
+
return `${model} | ${dir} | ctx: ${Math.round(ctx)}%`;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
Then point to it in `~/.claude/settings.json`:
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"statusLine": {
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "~/.claude/statusline --theme ~/.config/cc-statusline/theme.js"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
61
69
|
|
|
62
|
-
##
|
|
70
|
+
## Available Fields
|
|
71
|
+
|
|
72
|
+
The JSON object passed to your theme function contains these fields:
|
|
73
|
+
|
|
74
|
+
| Field | Example |
|
|
75
|
+
| ---------------------------------------------------------- | --------------------------------- |
|
|
76
|
+
| `session_id` | `"f9abcdef-1a2b-..."` |
|
|
77
|
+
| `transcript_path` | `"/home/user/.claude/transcript"` |
|
|
78
|
+
| `cwd` | `"/home/user/project"` |
|
|
79
|
+
| `model.id` | `"claude-opus-4-6"` |
|
|
80
|
+
| `model.display_name` | `"Claude Opus 4.6"` |
|
|
81
|
+
| `workspace.current_dir` | `"/home/user/project"` |
|
|
82
|
+
| `workspace.project_dir` | `"/home/user/project"` |
|
|
83
|
+
| `version` | `"2.1.39"` |
|
|
84
|
+
| `output_style.name` | `"Explanatory"` |
|
|
85
|
+
| `context_window.total_input_tokens` | `12345` |
|
|
86
|
+
| `context_window.total_output_tokens` | `6789` |
|
|
87
|
+
| `context_window.context_window_size` | `200000` |
|
|
88
|
+
| `context_window.current_usage` | `{ ... }` or `null` |
|
|
89
|
+
| `context_window.current_usage.input_tokens` | `1024` |
|
|
90
|
+
| `context_window.current_usage.output_tokens` | `512` |
|
|
91
|
+
| `context_window.current_usage.cache_creation_input_tokens` | `256` |
|
|
92
|
+
| `context_window.current_usage.cache_read_input_tokens` | `128` |
|
|
93
|
+
| `context_window.used_percentage` | `42.5` or `null` |
|
|
94
|
+
| `context_window.remaining_percentage` | `57.5` or `null` |
|
|
95
|
+
| `context_window.vim.mode` | `"INSERT"` or `"NORMAL"` |
|
|
96
|
+
| `context_window.agent.name` | `"claude-code"` |
|
|
97
|
+
| `context_window.agent.type` | `"main"` |
|
|
98
|
+
|
|
99
|
+
See [`src/schema/statusLine.ts`](src/schema/statusLine.ts) for the full schema.
|
|
100
|
+
|
|
101
|
+
## Troubleshooting
|
|
63
102
|
|
|
64
103
|
Execution logs are stored in `~/.local/state/statusline/app.log`.
|
|
65
104
|
|
package/bin/cc-statusline.js
CHANGED
|
@@ -9408,8 +9408,8 @@ var meow = (helpText, options = {}) => {
|
|
|
9408
9408
|
};
|
|
9409
9409
|
|
|
9410
9410
|
// src/cli.ts
|
|
9411
|
-
import {
|
|
9412
|
-
import {
|
|
9411
|
+
import { spawnSync } from "node:child_process";
|
|
9412
|
+
import { access, mkdir, copyFile, unlink } from "node:fs/promises";
|
|
9413
9413
|
import { homedir } from "node:os";
|
|
9414
9414
|
import { join } from "node:path";
|
|
9415
9415
|
var BINARY_NAME = "statusline";
|
|
@@ -9417,7 +9417,7 @@ var CLAUDE_DIR = join(homedir(), ".claude");
|
|
|
9417
9417
|
var TARGET_PATH = join(CLAUDE_DIR, BINARY_NAME);
|
|
9418
9418
|
var cli = meow(`
|
|
9419
9419
|
Usage
|
|
9420
|
-
$ cc-statusline <command>
|
|
9420
|
+
$ bunx cc-statusline <command>
|
|
9421
9421
|
|
|
9422
9422
|
Commands
|
|
9423
9423
|
install Build and install statusline to ~/.claude/
|
|
@@ -9426,8 +9426,8 @@ var cli = meow(`
|
|
|
9426
9426
|
--overwrite Overwrite existing file if it exists
|
|
9427
9427
|
|
|
9428
9428
|
Examples
|
|
9429
|
-
$ cc-statusline install
|
|
9430
|
-
$ cc-statusline install --overwrite
|
|
9429
|
+
$ bunx @nutthead/cc-statusline install
|
|
9430
|
+
$ bunx @nutthead/cc-statusline install --overwrite
|
|
9431
9431
|
`, {
|
|
9432
9432
|
importMeta: import.meta,
|
|
9433
9433
|
flags: {
|
|
@@ -9437,53 +9437,78 @@ var cli = meow(`
|
|
|
9437
9437
|
}
|
|
9438
9438
|
}
|
|
9439
9439
|
});
|
|
9440
|
-
function
|
|
9440
|
+
async function fileExists(path3) {
|
|
9441
|
+
try {
|
|
9442
|
+
await access(path3);
|
|
9443
|
+
return true;
|
|
9444
|
+
} catch {
|
|
9445
|
+
return false;
|
|
9446
|
+
}
|
|
9447
|
+
}
|
|
9448
|
+
var defaultFs = {
|
|
9449
|
+
exists: fileExists,
|
|
9450
|
+
mkdir: async (path3, options) => {
|
|
9451
|
+
await mkdir(path3, options);
|
|
9452
|
+
},
|
|
9453
|
+
copy: async (src, dest) => {
|
|
9454
|
+
await copyFile(src, dest);
|
|
9455
|
+
},
|
|
9456
|
+
remove: async (path3) => {
|
|
9457
|
+
await unlink(path3);
|
|
9458
|
+
}
|
|
9459
|
+
};
|
|
9460
|
+
async function build() {
|
|
9441
9461
|
console.log("Building statusline binary...");
|
|
9442
|
-
|
|
9462
|
+
await mkdir("target", { recursive: true });
|
|
9463
|
+
const result = spawnSync("bun", ["build", "--compile", "./index.ts", "--outfile", "target/statusline"], { stdio: "inherit" });
|
|
9464
|
+
if (result.error) {
|
|
9465
|
+
throw result.error;
|
|
9466
|
+
}
|
|
9467
|
+
if (result.status !== 0) {
|
|
9468
|
+
throw new Error(`Build failed with exit code ${result.status}`);
|
|
9469
|
+
}
|
|
9443
9470
|
console.log("Build complete.");
|
|
9444
9471
|
}
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
};
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
}
|
|
9460
|
-
if (deps.existsSync(deps.targetPath)) {
|
|
9461
|
-
if (!overwrite) {
|
|
9462
|
-
console.error(`Error: ${deps.targetPath} already exists.`);
|
|
9463
|
-
console.error("Use --overwrite to replace the existing file.");
|
|
9464
|
-
process.exit(1);
|
|
9465
|
-
}
|
|
9466
|
-
console.log(`Overwriting existing file at ${deps.targetPath}...`);
|
|
9467
|
-
deps.unlinkSync(deps.targetPath);
|
|
9468
|
-
}
|
|
9469
|
-
deps.copyFileSync(deps.sourcePath, deps.targetPath);
|
|
9470
|
-
console.log(`Installed statusline to ${deps.targetPath}`);
|
|
9472
|
+
async function installBinary(options, deps) {
|
|
9473
|
+
await deps.build();
|
|
9474
|
+
if (!await deps.fs.exists(options.claudeDir)) {
|
|
9475
|
+
await deps.fs.mkdir(options.claudeDir, { recursive: true });
|
|
9476
|
+
}
|
|
9477
|
+
if (await deps.fs.exists(options.targetPath)) {
|
|
9478
|
+
if (!options.overwrite) {
|
|
9479
|
+
throw new Error(`${options.targetPath} already exists. Use --overwrite to replace the existing file.`);
|
|
9480
|
+
}
|
|
9481
|
+
console.log(`Overwriting existing file at ${options.targetPath}...`);
|
|
9482
|
+
await deps.fs.remove(options.targetPath);
|
|
9483
|
+
}
|
|
9484
|
+
await deps.fs.copy(options.sourcePath, options.targetPath);
|
|
9485
|
+
console.log(`Installed statusline to ${options.targetPath}`);
|
|
9471
9486
|
}
|
|
9472
|
-
function install(overwrite) {
|
|
9473
|
-
installBinary(
|
|
9487
|
+
async function install(overwrite) {
|
|
9488
|
+
await installBinary({
|
|
9489
|
+
overwrite,
|
|
9490
|
+
claudeDir: CLAUDE_DIR,
|
|
9491
|
+
targetPath: TARGET_PATH,
|
|
9492
|
+
sourcePath: join(process.cwd(), "target", BINARY_NAME)
|
|
9493
|
+
}, { fs: defaultFs, build });
|
|
9474
9494
|
}
|
|
9475
|
-
function main() {
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
install
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9495
|
+
async function main() {
|
|
9496
|
+
try {
|
|
9497
|
+
const command = cli.input[0];
|
|
9498
|
+
switch (command) {
|
|
9499
|
+
case "install":
|
|
9500
|
+
await install(cli.flags.overwrite);
|
|
9501
|
+
break;
|
|
9502
|
+
case undefined:
|
|
9503
|
+
cli.showHelp();
|
|
9504
|
+
break;
|
|
9505
|
+
default:
|
|
9506
|
+
console.error(`Unknown command: ${command}`);
|
|
9507
|
+
cli.showHelp(1);
|
|
9508
|
+
}
|
|
9509
|
+
} catch (error) {
|
|
9510
|
+
console.error(`Error: ${error instanceof Error ? error.message : error}`);
|
|
9511
|
+
process.exit(1);
|
|
9487
9512
|
}
|
|
9488
9513
|
}
|
|
9489
9514
|
if (__require.main == __require.module) {
|
package/biome.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.3.15/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false
|
|
10
|
+
},
|
|
11
|
+
"formatter": {
|
|
12
|
+
"enabled": true,
|
|
13
|
+
"indentStyle": "space"
|
|
14
|
+
},
|
|
15
|
+
"linter": {
|
|
16
|
+
"enabled": true,
|
|
17
|
+
"rules": {
|
|
18
|
+
"recommended": true
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"javascript": {
|
|
22
|
+
"formatter": {
|
|
23
|
+
"quoteStyle": "double"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"assist": {
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"actions": {
|
|
29
|
+
"source": {
|
|
30
|
+
"organizeImports": "on"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
package/bunfig.toml
ADDED
package/index.ts
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import meow from "meow";
|
|
2
2
|
import { configure } from "@logtape/logtape";
|
|
3
3
|
import { log, logtapeConfig } from "./src/logging";
|
|
4
|
-
import { defaultTheme } from "./src/defaultTheme";
|
|
4
|
+
import { defaultTheme } from "./src/themes/defaultTheme";
|
|
5
5
|
import { loadTheme } from "./src/theme/loadTheme";
|
|
6
6
|
|
|
7
7
|
await configure(logtapeConfig);
|
|
8
8
|
|
|
9
9
|
const cli = meow(
|
|
10
10
|
`
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
Usage
|
|
12
|
+
$ cc-statusline
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
Options
|
|
15
|
+
--theme, -t Use a custom theme
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
$ cc-statusline --rainbow
|
|
17
|
+
Examples
|
|
19
18
|
$ cc-statusline --theme ~/.config/cc-statusline/basic.js
|
|
20
|
-
|
|
21
19
|
`,
|
|
22
20
|
{
|
|
23
21
|
importMeta: import.meta, // This is required
|
|
@@ -31,7 +29,8 @@ const cli = meow(
|
|
|
31
29
|
},
|
|
32
30
|
);
|
|
33
31
|
|
|
34
|
-
const resolvedTheme =
|
|
32
|
+
const resolvedTheme =
|
|
33
|
+
(cli.flags.theme && (await loadTheme(cli.flags.theme))) || defaultTheme;
|
|
35
34
|
|
|
36
35
|
const input = await Bun.stdin.stream().json();
|
|
37
36
|
log.debug("input: {input}", input);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nutthead/cc-statusline",
|
|
3
3
|
"description": "Status Line for Claude Code",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cc-statusline": "bin/cc-statusline.js"
|
|
@@ -30,9 +30,12 @@
|
|
|
30
30
|
"build:binary": "mkdir -p target && bun build --compile ./index.ts --outfile target/statusline",
|
|
31
31
|
"build:cli": "bun build ./src/cli.ts --outfile ./bin/cc-statusline.js --target node",
|
|
32
32
|
"prepublishOnly": "bun run build:cli",
|
|
33
|
-
"install:binary": "cp target/statusline ~/.claude/"
|
|
33
|
+
"install:binary": "cp target/statusline ~/.claude/",
|
|
34
|
+
"biome:format": "bunx biome format --write",
|
|
35
|
+
"biome:lint": "bunx biome lint --fix"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
38
|
+
"@biomejs/biome": "^2.3.15",
|
|
36
39
|
"@types/bun": "latest"
|
|
37
40
|
},
|
|
38
41
|
"peerDependencies": {
|
|
@@ -43,7 +46,9 @@
|
|
|
43
46
|
"@logtape/logtape": "^1.3.6",
|
|
44
47
|
"ansi-colors": "^4.1.3",
|
|
45
48
|
"meow": "^14.0.0",
|
|
49
|
+
"neverthrow": "^8.2.0",
|
|
46
50
|
"simple-git": "^3.30.0",
|
|
51
|
+
"terminal-size": "^4.0.1",
|
|
47
52
|
"ts-pattern": "^5.9.0",
|
|
48
53
|
"type-fest": "^5.3.1",
|
|
49
54
|
"zod": "^4.3.5"
|
package/src/cli.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import meow from "meow";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { access, mkdir, copyFile, unlink } from "node:fs/promises";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
@@ -10,10 +10,29 @@ const BINARY_NAME = "statusline";
|
|
|
10
10
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
11
11
|
const TARGET_PATH = join(CLAUDE_DIR, BINARY_NAME);
|
|
12
12
|
|
|
13
|
+
interface FileSystem {
|
|
14
|
+
exists(path: string): Promise<boolean>;
|
|
15
|
+
mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
|
|
16
|
+
copy(src: string, dest: string): Promise<void>;
|
|
17
|
+
remove(path: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface InstallOptions {
|
|
21
|
+
overwrite: boolean;
|
|
22
|
+
claudeDir: string;
|
|
23
|
+
targetPath: string;
|
|
24
|
+
sourcePath: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface InstallDeps {
|
|
28
|
+
fs: FileSystem;
|
|
29
|
+
build: () => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
const cli = meow(
|
|
14
33
|
`
|
|
15
34
|
Usage
|
|
16
|
-
$ cc-statusline <command>
|
|
35
|
+
$ bunx cc-statusline <command>
|
|
17
36
|
|
|
18
37
|
Commands
|
|
19
38
|
install Build and install statusline to ~/.claude/
|
|
@@ -22,8 +41,8 @@ const cli = meow(
|
|
|
22
41
|
--overwrite Overwrite existing file if it exists
|
|
23
42
|
|
|
24
43
|
Examples
|
|
25
|
-
$ cc-statusline install
|
|
26
|
-
$ cc-statusline install --overwrite
|
|
44
|
+
$ bunx @nutthead/cc-statusline install
|
|
45
|
+
$ bunx @nutthead/cc-statusline install --overwrite
|
|
27
46
|
`,
|
|
28
47
|
{
|
|
29
48
|
importMeta: import.meta,
|
|
@@ -36,81 +55,111 @@ const cli = meow(
|
|
|
36
55
|
},
|
|
37
56
|
);
|
|
38
57
|
|
|
39
|
-
|
|
58
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
59
|
+
try {
|
|
60
|
+
await access(path);
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const defaultFs: FileSystem = {
|
|
68
|
+
exists: fileExists,
|
|
69
|
+
mkdir: async (path, options) => {
|
|
70
|
+
await mkdir(path, options);
|
|
71
|
+
},
|
|
72
|
+
copy: async (src, dest) => {
|
|
73
|
+
await copyFile(src, dest);
|
|
74
|
+
},
|
|
75
|
+
remove: async (path) => {
|
|
76
|
+
await unlink(path);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
async function build(): Promise<void> {
|
|
40
81
|
console.log("Building statusline binary...");
|
|
41
|
-
|
|
42
|
-
|
|
82
|
+
await mkdir("target", { recursive: true });
|
|
83
|
+
const result = spawnSync(
|
|
84
|
+
"bun",
|
|
85
|
+
["build", "--compile", "./index.ts", "--outfile", "target/statusline"],
|
|
43
86
|
{ stdio: "inherit" },
|
|
44
87
|
);
|
|
88
|
+
if (result.error) {
|
|
89
|
+
throw result.error;
|
|
90
|
+
}
|
|
91
|
+
if (result.status !== 0) {
|
|
92
|
+
throw new Error(`Build failed with exit code ${result.status}`);
|
|
93
|
+
}
|
|
45
94
|
console.log("Build complete.");
|
|
46
95
|
}
|
|
47
96
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
existsSync: (path: string) => boolean;
|
|
54
|
-
mkdirSync: (path: string, options?: { recursive?: boolean }) => void;
|
|
55
|
-
copyFileSync: (src: string, dest: string) => void;
|
|
56
|
-
unlinkSync: (path: string) => void;
|
|
57
|
-
}
|
|
97
|
+
async function installBinary(
|
|
98
|
+
options: InstallOptions,
|
|
99
|
+
deps: InstallDeps,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
await deps.build();
|
|
58
102
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
targetPath: TARGET_PATH,
|
|
62
|
-
sourcePath: join(process.cwd(), "target", BINARY_NAME),
|
|
63
|
-
doBuild: build,
|
|
64
|
-
existsSync,
|
|
65
|
-
mkdirSync,
|
|
66
|
-
copyFileSync,
|
|
67
|
-
unlinkSync,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export function installBinary(
|
|
71
|
-
overwrite: boolean,
|
|
72
|
-
deps: InstallDeps = defaultDeps,
|
|
73
|
-
): void {
|
|
74
|
-
deps.doBuild();
|
|
75
|
-
|
|
76
|
-
if (!deps.existsSync(deps.claudeDir)) {
|
|
77
|
-
deps.mkdirSync(deps.claudeDir, { recursive: true });
|
|
103
|
+
if (!(await deps.fs.exists(options.claudeDir))) {
|
|
104
|
+
await deps.fs.mkdir(options.claudeDir, { recursive: true });
|
|
78
105
|
}
|
|
79
106
|
|
|
80
|
-
if (deps.
|
|
81
|
-
if (!overwrite) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
if (await deps.fs.exists(options.targetPath)) {
|
|
108
|
+
if (!options.overwrite) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`${options.targetPath} already exists. Use --overwrite to replace the existing file.`,
|
|
111
|
+
);
|
|
85
112
|
}
|
|
86
|
-
console.log(`Overwriting existing file at ${
|
|
87
|
-
deps.
|
|
113
|
+
console.log(`Overwriting existing file at ${options.targetPath}...`);
|
|
114
|
+
await deps.fs.remove(options.targetPath);
|
|
88
115
|
}
|
|
89
116
|
|
|
90
|
-
deps.
|
|
91
|
-
console.log(`Installed statusline to ${
|
|
117
|
+
await deps.fs.copy(options.sourcePath, options.targetPath);
|
|
118
|
+
console.log(`Installed statusline to ${options.targetPath}`);
|
|
92
119
|
}
|
|
93
120
|
|
|
94
|
-
|
|
95
|
-
installBinary(
|
|
121
|
+
async function install(overwrite: boolean): Promise<void> {
|
|
122
|
+
await installBinary(
|
|
123
|
+
{
|
|
124
|
+
overwrite,
|
|
125
|
+
claudeDir: CLAUDE_DIR,
|
|
126
|
+
targetPath: TARGET_PATH,
|
|
127
|
+
sourcePath: join(process.cwd(), "target", BINARY_NAME),
|
|
128
|
+
},
|
|
129
|
+
{ fs: defaultFs, build },
|
|
130
|
+
);
|
|
96
131
|
}
|
|
97
132
|
|
|
98
|
-
function main(): void {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
install
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
133
|
+
async function main(): Promise<void> {
|
|
134
|
+
try {
|
|
135
|
+
const command = cli.input[0];
|
|
136
|
+
|
|
137
|
+
switch (command) {
|
|
138
|
+
case "install":
|
|
139
|
+
await install(cli.flags.overwrite);
|
|
140
|
+
break;
|
|
141
|
+
case undefined:
|
|
142
|
+
cli.showHelp();
|
|
143
|
+
break;
|
|
144
|
+
default:
|
|
145
|
+
console.error(`Unknown command: ${command}`);
|
|
146
|
+
cli.showHelp(1);
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error(`Error: ${error instanceof Error ? error.message : error}`);
|
|
150
|
+
process.exit(1);
|
|
111
151
|
}
|
|
112
152
|
}
|
|
113
153
|
|
|
114
154
|
if (import.meta.main) {
|
|
115
155
|
main();
|
|
116
156
|
}
|
|
157
|
+
|
|
158
|
+
export {
|
|
159
|
+
build,
|
|
160
|
+
install,
|
|
161
|
+
installBinary,
|
|
162
|
+
type FileSystem,
|
|
163
|
+
type InstallDeps,
|
|
164
|
+
type InstallOptions,
|
|
165
|
+
};
|
package/src/logging.ts
CHANGED
|
@@ -4,7 +4,9 @@ import { homedir } from "node:os";
|
|
|
4
4
|
|
|
5
5
|
const logtapeConfig: Config<"file", string> = {
|
|
6
6
|
sinks: {
|
|
7
|
-
file: getFileSink(`${homedir()}/.local/state/statusline/app.log
|
|
7
|
+
file: getFileSink(`${homedir()}/.local/state/statusline/app.log`, {
|
|
8
|
+
lazy: true,
|
|
9
|
+
}),
|
|
8
10
|
},
|
|
9
11
|
loggers: [
|
|
10
12
|
{
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const statusSchema = z.object({
|
|
4
|
+
session_id: z.string(),
|
|
5
|
+
transcript_path: z.string(),
|
|
6
|
+
cwd: z.string(),
|
|
7
|
+
model: z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
display_name: z.string(),
|
|
10
|
+
}),
|
|
11
|
+
workspace: z.object({
|
|
12
|
+
current_dir: z.string(),
|
|
13
|
+
project_dir: z.string(),
|
|
14
|
+
}),
|
|
15
|
+
version: z.string(),
|
|
16
|
+
output_style: z.object({
|
|
17
|
+
name: z.string(),
|
|
18
|
+
}),
|
|
19
|
+
context_window: z.object({
|
|
20
|
+
total_input_tokens: z.number(),
|
|
21
|
+
total_output_tokens: z.number(),
|
|
22
|
+
context_window_size: z.number(),
|
|
23
|
+
current_usage: z
|
|
24
|
+
.object({
|
|
25
|
+
input_tokens: z.number(),
|
|
26
|
+
output_tokens: z.number(),
|
|
27
|
+
cache_creation_input_tokens: z.number(),
|
|
28
|
+
cache_read_input_tokens: z.number(),
|
|
29
|
+
})
|
|
30
|
+
.nullable(),
|
|
31
|
+
used_percentage: z.number().nullable(),
|
|
32
|
+
remaining_percentage: z.number().nullable(),
|
|
33
|
+
vim: z.object({
|
|
34
|
+
mode: z.enum(["INSERT", "NORMAL"]),
|
|
35
|
+
}).optional(),
|
|
36
|
+
agent: z.object({
|
|
37
|
+
name: z.string(),
|
|
38
|
+
type: z.string(),
|
|
39
|
+
}).optional(),
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
type Status = z.infer<typeof statusSchema>;
|
|
44
|
+
|
|
45
|
+
export { statusSchema, type Status };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ZodError } from "zod";
|
|
2
|
+
import { log } from "../logging";
|
|
3
|
+
import { statusSchema, type Status } from "../schema/statusLine";
|
|
4
|
+
import { abbreviateModelId } from "../utils/model";
|
|
5
|
+
import { compress, telescope } from "../utils/path";
|
|
6
|
+
import { getDisplayWidth } from "../utils/term";
|
|
7
|
+
import terminalSize from 'terminal-size';
|
|
8
|
+
import { currentBranchName } from "../utils/git";
|
|
9
|
+
import { match } from "ts-pattern";
|
|
10
|
+
|
|
11
|
+
const HORIZONTAL_PADDING = 4;
|
|
12
|
+
|
|
13
|
+
async function renderLine1(status: Status) : Promise<string> {
|
|
14
|
+
const modelId = abbreviateModelId(status.model.id);
|
|
15
|
+
const modelStatus = `🤖 ${modelId}`;
|
|
16
|
+
|
|
17
|
+
const sessionStatus = `📃 ${status.session_id}`;
|
|
18
|
+
|
|
19
|
+
const projectDir = telescope(compress(status.workspace.project_dir));
|
|
20
|
+
const projectStatus = `🗂️ ${projectDir}`;
|
|
21
|
+
|
|
22
|
+
const statusWidth = terminalSize().columns - HORIZONTAL_PADDING;
|
|
23
|
+
const modelWidth = getDisplayWidth(modelStatus);
|
|
24
|
+
const sessionWidth = getDisplayWidth(sessionStatus);
|
|
25
|
+
const projectWidth = getDisplayWidth(projectStatus);
|
|
26
|
+
|
|
27
|
+
const remainingSpace = statusWidth - modelWidth - sessionWidth - projectWidth;
|
|
28
|
+
const leftGap = Math.floor(remainingSpace / 2);
|
|
29
|
+
const rightGap = Math.ceil(remainingSpace / 2);
|
|
30
|
+
|
|
31
|
+
return modelStatus + " ".repeat(leftGap) + sessionStatus + " ".repeat(rightGap) + projectStatus;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function renderLine2(status: Status) : Promise<string> {
|
|
35
|
+
const branch = await currentBranchName();
|
|
36
|
+
const branchStatus = match(branch)
|
|
37
|
+
.with({status: "none"}, () => {
|
|
38
|
+
return `💾`;
|
|
39
|
+
})
|
|
40
|
+
.with({status: "branch"}, ({name}) => {
|
|
41
|
+
return `🌿 ${name}`;
|
|
42
|
+
})
|
|
43
|
+
.with({status: "detached"}, ({commit}) => {
|
|
44
|
+
return ` ${commit}`;
|
|
45
|
+
})
|
|
46
|
+
.with({status: "error"}, () => {
|
|
47
|
+
return `💥`;
|
|
48
|
+
})
|
|
49
|
+
.exhaustive();
|
|
50
|
+
|
|
51
|
+
const usedPercentage = status.context_window.used_percentage ?? 0;
|
|
52
|
+
const usageStatus = usedPercentage === 0 ? '' : `${usedPercentage}%`
|
|
53
|
+
|
|
54
|
+
const statusWidth = terminalSize().columns - HORIZONTAL_PADDING;
|
|
55
|
+
const branchWidth = getDisplayWidth(branchStatus);
|
|
56
|
+
const usageWidth = getDisplayWidth(usageStatus);
|
|
57
|
+
|
|
58
|
+
const gap = statusWidth - branchWidth - usageWidth;
|
|
59
|
+
|
|
60
|
+
return branchStatus + " ".repeat(gap - 1) + usageStatus;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function renderTheme(status: Status): Promise<string> {
|
|
64
|
+
const line1 = await renderLine1(status);
|
|
65
|
+
const line2 = await renderLine2(status);
|
|
66
|
+
return [line1, line2].filter(Boolean).join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function defaultTheme(input?: string): Promise<string> {
|
|
70
|
+
if (input) {
|
|
71
|
+
try {
|
|
72
|
+
const status = statusSchema.parse(input);
|
|
73
|
+
return renderTheme(status);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (e instanceof ZodError) {
|
|
76
|
+
log.error("Failed to parse input: {error}", {
|
|
77
|
+
error: JSON.stringify(e.issues),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { defaultTheme };
|
|
@@ -1,45 +1,5 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
1
|
import { simpleGit, type SimpleGit } from "simple-git";
|
|
3
2
|
import { match } from "ts-pattern";
|
|
4
|
-
import type { Status } from "./statusLineSchema";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Abbreviates a path by reducing all segments except the last to their first character.
|
|
8
|
-
* If the path starts with the home directory, it's replaced with ~.
|
|
9
|
-
* @example abbreviatePath("/home/user/projects/myapp") // "~/p/myapp"
|
|
10
|
-
* @example abbreviatePath("/foo/bar/baz/etc/last") // "/f/b/b/e/last"
|
|
11
|
-
*/
|
|
12
|
-
function abbreviatePath(path: string): string {
|
|
13
|
-
const home = homedir();
|
|
14
|
-
|
|
15
|
-
// Replace homedir with ~ if path starts with it
|
|
16
|
-
let normalizedPath = path;
|
|
17
|
-
let prefix = "";
|
|
18
|
-
if (path.startsWith(home)) {
|
|
19
|
-
normalizedPath = path.slice(home.length);
|
|
20
|
-
prefix = "~";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const segments = normalizedPath.split("/");
|
|
24
|
-
if (segments.length <= 1) return prefix + normalizedPath;
|
|
25
|
-
|
|
26
|
-
const abbreviated = segments.map((segment, index) => {
|
|
27
|
-
// Keep last segment full, abbreviate others to first char (if non-empty)
|
|
28
|
-
if (index === segments.length - 1) return segment;
|
|
29
|
-
return segment.length > 0 ? segment[0] : segment;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
return prefix + abbreviated.join("/");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Removes the "claude-" prefix from a model name if present.
|
|
37
|
-
* @example abbreviateModelId("claude-opus-4.5") // "opus-4.5"
|
|
38
|
-
* @example abbreviateModelId("opus-4.5") // "opus-4.5"
|
|
39
|
-
*/
|
|
40
|
-
function abbreviateModelId(model: string): string {
|
|
41
|
-
return model.startsWith("claude-") ? model.slice(7) : model;
|
|
42
|
-
}
|
|
43
3
|
|
|
44
4
|
/**
|
|
45
5
|
* Result type for currentBranchName function.
|
|
@@ -48,7 +8,7 @@ function abbreviateModelId(model: string): string {
|
|
|
48
8
|
* - `error`: Failed to get branch info (not a git repo, etc.)
|
|
49
9
|
*/
|
|
50
10
|
type BranchResult =
|
|
51
|
-
| { status: "
|
|
11
|
+
| { status: "none" }
|
|
52
12
|
| { status: "branch"; name: string }
|
|
53
13
|
| { status: "detached"; commit: string }
|
|
54
14
|
| { status: "error"; message: string };
|
|
@@ -66,7 +26,7 @@ async function currentBranchName(cwd?: string): Promise<BranchResult> {
|
|
|
66
26
|
// Check if we're in a git repository first
|
|
67
27
|
const isRepo = await git.checkIsRepo();
|
|
68
28
|
if (!isRepo) {
|
|
69
|
-
return { status: "
|
|
29
|
+
return { status: "none" };
|
|
70
30
|
}
|
|
71
31
|
|
|
72
32
|
const branchSummary = await git.branch();
|
|
@@ -118,56 +78,12 @@ async function currentGitStatus() {
|
|
|
118
78
|
const gitStatus = match(gitBranch)
|
|
119
79
|
.with({ status: "branch" }, ({ name }) => `🌿 ${name}`)
|
|
120
80
|
.with({ status: "detached" }, ({ commit }) => ` ${commit}`)
|
|
121
|
-
.with({ status: "
|
|
81
|
+
.with({ status: "none" }, () => "💾")
|
|
122
82
|
.with({ status: "error" }, () => "💥")
|
|
123
83
|
.exhaustive();
|
|
124
84
|
|
|
125
85
|
return gitStatus;
|
|
126
86
|
}
|
|
127
87
|
|
|
128
|
-
|
|
129
|
-
* Returns a formatted model status string with the Claude icon.
|
|
130
|
-
* Strips the "claude-" prefix from the model ID for brevity.
|
|
131
|
-
*
|
|
132
|
-
* @param status - The Status object containing model information
|
|
133
|
-
* @returns A formatted string like "⏣ opus-4.5" or "⏣ sonnet-4"
|
|
134
|
-
*/
|
|
135
|
-
function currentModelStatus(status: Status) {
|
|
136
|
-
return `⏣ ${abbreviateModelId(status.model.id)}`;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Returns a formatted directory status string showing workspace location.
|
|
141
|
-
* Both paths are abbreviated (e.g., "/home/user/projects" → "~/p").
|
|
142
|
-
*
|
|
143
|
-
* @param status - The Status object containing workspace information
|
|
144
|
-
* @returns Either the abbreviated project directory alone (when current dir matches),
|
|
145
|
-
* or "projectDir/currentDir" format when projectDir/currentDir don't match
|
|
146
|
-
* @example currentDirStatus({...}) // "🗂️ ~/p/myapp" or "🗂️ ~/p/myapp 📂 ~/s/components"
|
|
147
|
-
*/
|
|
148
|
-
function currentDirStatus(status: Status) {
|
|
149
|
-
const projectDir = abbreviatePath(status.workspace.project_dir);
|
|
150
|
-
const currentDir = abbreviatePath(status.workspace.current_dir);
|
|
151
|
-
const dirStatus =
|
|
152
|
-
projectDir === currentDir
|
|
153
|
-
? `🗂️ ${projectDir}`
|
|
154
|
-
: `🗂️ ${projectDir} 📂 ${currentDir}`;
|
|
155
|
-
|
|
156
|
-
return dirStatus;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function currentSessionId(status: Status) {
|
|
160
|
-
return `📝 ${status.session_id}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export {
|
|
164
|
-
abbreviateModelId,
|
|
165
|
-
abbreviatePath,
|
|
166
|
-
currentBranchName,
|
|
167
|
-
currentDirStatus,
|
|
168
|
-
currentGitStatus,
|
|
169
|
-
currentModelStatus,
|
|
170
|
-
currentSessionId,
|
|
171
|
-
};
|
|
172
|
-
|
|
88
|
+
export { currentBranchName, currentGitStatus };
|
|
173
89
|
export type { BranchResult };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abbreviates a model ID by stripping the "claude-" prefix and truncating
|
|
3
|
+
* to `tail` characters (using `…` prefix when truncated).
|
|
4
|
+
*
|
|
5
|
+
* @param model - The model ID string
|
|
6
|
+
* @param options.tail - Maximum character length of the result (default: 12).
|
|
7
|
+
*
|
|
8
|
+
* @example abbreviateModelId("claude-opus-4.5") // "opus-4.5"
|
|
9
|
+
* @example abbreviateModelId("some-very-long-model-name") // "…-model-name"
|
|
10
|
+
*/
|
|
11
|
+
function abbreviateModelId(model: string, options?: { tail?: number }): string {
|
|
12
|
+
const tail = options?.tail ?? 12;
|
|
13
|
+
|
|
14
|
+
// Step 1: Strip "claude-" prefix
|
|
15
|
+
const name = model.replace(/^claude-/, "");
|
|
16
|
+
|
|
17
|
+
// Step 2: Truncate if needed, keeping the last (tail - 1) chars
|
|
18
|
+
if (name.length <= tail) return name;
|
|
19
|
+
if (tail <= 1) return "…";
|
|
20
|
+
return `…${name.slice(-(tail - 1))}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { abbreviateModelId };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
|
|
3
|
+
const HORIZONTAL_ELLIPSIS = "\u2026";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compresses a path by converting all segments except the last to a single character.
|
|
7
|
+
*
|
|
8
|
+
* @param path - The path to compress
|
|
9
|
+
* @returns The compressed path
|
|
10
|
+
*
|
|
11
|
+
* @example compress("/home/username/foo/bar/baz") // "/h/u/f/b/baz"
|
|
12
|
+
* @example compress("/foo/bar/baz") // "/f/b/baz"
|
|
13
|
+
* @example compress("~/projects/myapp") // "~/p/myapp"
|
|
14
|
+
* @example compress("a/b/c/d") // "a/b/c/d"
|
|
15
|
+
*/
|
|
16
|
+
function compress(path: string): string {
|
|
17
|
+
const segments = path.split("/");
|
|
18
|
+
|
|
19
|
+
if (segments.length <= 1) {
|
|
20
|
+
return path;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const compressed = segments.map((segment, index) => {
|
|
24
|
+
// Keep last segment full, compress others to first char
|
|
25
|
+
if (index === segments.length - 1) return segment;
|
|
26
|
+
|
|
27
|
+
// For empty segments (absolute path root), keep empty
|
|
28
|
+
if (segment === "") return segment;
|
|
29
|
+
|
|
30
|
+
return segment[0];
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return compressed.join("/");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Converts a path starting with the home directory to use `~`.
|
|
38
|
+
*
|
|
39
|
+
* @param path - The path to convert
|
|
40
|
+
* @returns The path with home directory replaced by `~`, or the original path if it doesn't start with home
|
|
41
|
+
*
|
|
42
|
+
* @example tildify("/home/user/projects/myapp") // "~/projects/myapp"
|
|
43
|
+
* @example tildify("/home/user") // "~"
|
|
44
|
+
* @example tildify("/etc/nginx") // "/etc/nginx"
|
|
45
|
+
*/
|
|
46
|
+
function tildify(path: string): string {
|
|
47
|
+
const home = homedir();
|
|
48
|
+
|
|
49
|
+
if (path === home) {
|
|
50
|
+
return "~";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (path.startsWith(`${home}/`)) {
|
|
54
|
+
return `~${path.slice(home.length)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return path;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Telescopes a path by keeping only the first and last segments,
|
|
62
|
+
* with a horizontal ellipsis in between.
|
|
63
|
+
*
|
|
64
|
+
* First tildifies the path, then applies the telescoping transformation.
|
|
65
|
+
* If the path has 2 or fewer segments, it is returned unchanged.
|
|
66
|
+
*
|
|
67
|
+
* @param path - The path to telescope
|
|
68
|
+
* @returns The telescoped path
|
|
69
|
+
*
|
|
70
|
+
* @example telescope("/home/user/projects/myapp") // "~/…/myapp"
|
|
71
|
+
* @example telescope("/a/b/c/d") // "/a/…/d"
|
|
72
|
+
* @example telescope("~/a/b/c") // "~/…/c"
|
|
73
|
+
* @example telescope("a/b/c") // "a/…/c"
|
|
74
|
+
* @example telescope("~/foo") // "~/foo"
|
|
75
|
+
*/
|
|
76
|
+
function telescope(path: string): string {
|
|
77
|
+
const tildified = tildify(path);
|
|
78
|
+
const segments = tildified.split("/");
|
|
79
|
+
|
|
80
|
+
if (segments.length <= 2) {
|
|
81
|
+
return tildified;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const first = segments[0];
|
|
85
|
+
const last = segments[segments.length - 1];
|
|
86
|
+
|
|
87
|
+
// Handle absolute paths: first segment is empty string
|
|
88
|
+
if (first === "" && segments.length > 1) {
|
|
89
|
+
return `/${segments[1]}/${HORIZONTAL_ELLIPSIS}/${last}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return `${first}/${HORIZONTAL_ELLIPSIS}/${last}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { compress, tildify, telescope, HORIZONTAL_ELLIPSIS };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculates the display width of a string, accounting for emojis
|
|
3
|
+
* which occupy 2 character columns in terminal displays.
|
|
4
|
+
*
|
|
5
|
+
* @param str - The string to measure
|
|
6
|
+
* @returns The display width in columns
|
|
7
|
+
*/
|
|
8
|
+
function getDisplayWidth(str: string): number {
|
|
9
|
+
// Remove ANSI codes for width calculation
|
|
10
|
+
const cleanStr = str.replace(/\u001b\[[0-9;]*m/g, "");
|
|
11
|
+
// Count regular characters
|
|
12
|
+
const charCount = Array.from(cleanStr).length;
|
|
13
|
+
// Count emojis (each emoji counts as 2 characters)
|
|
14
|
+
const emojiRegex = /\p{Extended_Pictographic}/gu;
|
|
15
|
+
const emojiCount = (cleanStr.match(emojiRegex) || []).length;
|
|
16
|
+
// Total width = characters + extra count for emojis (since each emoji is 2 wide)
|
|
17
|
+
return charCount + emojiCount;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { getDisplayWidth };
|
package/src/defaultTheme.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { log } from "./logging";
|
|
3
|
-
import { statusSchema } from "./statusLineSchema";
|
|
4
|
-
import {
|
|
5
|
-
currentDirStatus,
|
|
6
|
-
currentGitStatus,
|
|
7
|
-
currentModelStatus,
|
|
8
|
-
currentSessionId,
|
|
9
|
-
} from "./utils";
|
|
10
|
-
|
|
11
|
-
import c from "ansi-colors";
|
|
12
|
-
|
|
13
|
-
async function defaultTheme(input?: string): Promise<string> {
|
|
14
|
-
let statusLine = null;
|
|
15
|
-
|
|
16
|
-
if (input) {
|
|
17
|
-
const result = statusSchema.safeParse(input);
|
|
18
|
-
|
|
19
|
-
if (result.success) {
|
|
20
|
-
const status = result.data;
|
|
21
|
-
const dirStatus = c.blue(currentDirStatus(status));
|
|
22
|
-
const gitStatus = c.green(await currentGitStatus());
|
|
23
|
-
const modelStatus = c.magenta(currentModelStatus(status));
|
|
24
|
-
const sessionId = c.blue(currentSessionId(status));
|
|
25
|
-
const separator = c.bold.gray(" ⋮ ");
|
|
26
|
-
|
|
27
|
-
statusLine = [
|
|
28
|
-
[dirStatus, gitStatus],
|
|
29
|
-
[modelStatus, sessionId],
|
|
30
|
-
]
|
|
31
|
-
.map((row) => row.join(separator))
|
|
32
|
-
.join("\n");
|
|
33
|
-
} else {
|
|
34
|
-
log.error("Failed to parse input: {error}", {
|
|
35
|
-
error: JSON.stringify(z.treeifyError(result.error)),
|
|
36
|
-
});
|
|
37
|
-
statusLine = `[malformed status]`;
|
|
38
|
-
}
|
|
39
|
-
} else {
|
|
40
|
-
statusLine = `[no status]`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return statusLine;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export { defaultTheme };
|
package/src/statusLineSchema.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const statusSchema = z.object({
|
|
4
|
-
hook_event_name: z.string().optional(),
|
|
5
|
-
session_id: z.string(),
|
|
6
|
-
transcript_path: z.string(),
|
|
7
|
-
cwd: z.string(),
|
|
8
|
-
model: z.object({
|
|
9
|
-
id: z.string(),
|
|
10
|
-
display_name: z.string(),
|
|
11
|
-
}),
|
|
12
|
-
workspace: z.object({
|
|
13
|
-
current_dir: z.string(),
|
|
14
|
-
project_dir: z.string(),
|
|
15
|
-
}),
|
|
16
|
-
version: z.string(),
|
|
17
|
-
output_style: z
|
|
18
|
-
.object({
|
|
19
|
-
name: z.string(),
|
|
20
|
-
})
|
|
21
|
-
.optional(),
|
|
22
|
-
cost: z.object({
|
|
23
|
-
total_cost_usd: z.number(),
|
|
24
|
-
total_duration_ms: z.number(),
|
|
25
|
-
total_api_duration_ms: z.number(),
|
|
26
|
-
total_lines_added: z.number(),
|
|
27
|
-
total_lines_removed: z.number(),
|
|
28
|
-
}),
|
|
29
|
-
context_window: z
|
|
30
|
-
.object({
|
|
31
|
-
input_tokens: z.number().optional(),
|
|
32
|
-
output_tokens: z.number().optional(),
|
|
33
|
-
cache_creation_input_tokens: z.number().optional(),
|
|
34
|
-
cache_read_input_tokens: z.number().optional(),
|
|
35
|
-
})
|
|
36
|
-
.nullable(),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
export type Status = z.infer<typeof statusSchema>;
|