@nutthead/cc-statusline 0.3.0 → 0.4.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 +22 -21
- package/assets/default-preview.png +0 -0
- package/assets/powerline-preview.png +0 -0
- package/bin/cc-statusline.js +6 -4
- package/index.ts +37 -8
- package/package.json +2 -2
- package/src/cli.ts +2 -2
- package/src/logging.ts +2 -2
- package/src/schema/statusLine.ts +11 -7
- package/src/theme/loadTheme.ts +1 -1
- package/src/themes/defaultTheme.ts +49 -20
- package/src/themes/powerlineTheme.ts +150 -0
- package/src/utils/git.ts +1 -1
- package/src/utils/term.ts +9 -8
package/README.md
CHANGED
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Themeable status line provider for Claude Code.
|
|
4
4
|
|
|
5
|
+
Requires [Bun](https://bun.com).
|
|
6
|
+
|
|
5
7
|
## Preview
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
### Default Theme
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
Two-row layout with model, session, project directory, git status, and context usage:
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
### Powerline Theme
|
|
16
|
+
|
|
17
|
+
Single-row powerline-style segments with muted dark backgrounds and arrow separators:
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
- 🤖 Model ID (abbreviated)
|
|
16
|
-
- 📃 Session ID
|
|
17
|
-
- 🗂️ Project directory (compressed and telescoped)
|
|
19
|
+

|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
- 🌿 Git branch name (or commit hash if detached, 💾 if not a repo, 💥 on error)
|
|
21
|
-
- Context window usage percentage
|
|
21
|
+
Wraps to multiple lines when segments exceed terminal width. Select with `--theme powerline`.
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
@@ -39,20 +39,21 @@ Then add to `~/.claude/settings.json`:
|
|
|
39
39
|
|
|
40
40
|
Use `--overwrite` to replace an existing installation.
|
|
41
41
|
|
|
42
|
+
## Themes
|
|
43
|
+
|
|
44
|
+
Built-in themes: `default` (two-row), `powerline` (single-row with powerline arrows).
|
|
45
|
+
|
|
46
|
+
Use `--theme powerline` to select a built-in theme.
|
|
47
|
+
|
|
42
48
|
## Custom Themes
|
|
43
49
|
|
|
44
|
-
Create a JS file that default-exports
|
|
50
|
+
Create a JS file that default-exports an async theme function (e.g. `~/.config/cc-statusline/theme.js`):
|
|
45
51
|
|
|
46
52
|
```js
|
|
47
|
-
export default function theme(input) {
|
|
53
|
+
export default async function theme(input) {
|
|
48
54
|
if (!input) return "";
|
|
49
|
-
|
|
50
55
|
const status = JSON.parse(input);
|
|
51
|
-
|
|
52
|
-
const model = status.model.display_name;
|
|
53
|
-
const ctx = status.context_window.used_percentage ?? 0;
|
|
54
|
-
|
|
55
|
-
return `${model} | ${dir} | ctx: ${Math.round(ctx)}%`;
|
|
56
|
+
return `${status.model.display_name} | ${status.workspace.current_dir}`;
|
|
56
57
|
}
|
|
57
58
|
```
|
|
58
59
|
|
|
@@ -62,7 +63,7 @@ Then point to it in `~/.claude/settings.json`:
|
|
|
62
63
|
{
|
|
63
64
|
"statusLine": {
|
|
64
65
|
"type": "command",
|
|
65
|
-
"command": "~/.claude/statusline --theme ~/.config/cc-statusline/theme.js"
|
|
66
|
+
"command": "~/.claude/statusline --theme-file ~/.config/cc-statusline/theme.js"
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
```
|
|
Binary file
|
|
Binary file
|
package/bin/cc-statusline.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
|
+
// src/cli.ts
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { access, copyFile, mkdir, unlink } from "node:fs/promises";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
5
11
|
// node_modules/meow/build/index.js
|
|
6
12
|
import process4 from "node:process";
|
|
7
13
|
|
|
@@ -9408,10 +9414,6 @@ var meow = (helpText, options = {}) => {
|
|
|
9408
9414
|
};
|
|
9409
9415
|
|
|
9410
9416
|
// src/cli.ts
|
|
9411
|
-
import { spawnSync } from "node:child_process";
|
|
9412
|
-
import { access, mkdir, copyFile, unlink } from "node:fs/promises";
|
|
9413
|
-
import { homedir } from "node:os";
|
|
9414
|
-
import { join } from "node:path";
|
|
9415
9417
|
var BINARY_NAME = "statusline";
|
|
9416
9418
|
var CLAUDE_DIR = join(homedir(), ".claude");
|
|
9417
9419
|
var TARGET_PATH = join(CLAUDE_DIR, BINARY_NAME);
|
package/index.ts
CHANGED
|
@@ -1,37 +1,66 @@
|
|
|
1
|
-
import meow from "meow";
|
|
2
1
|
import { configure } from "@logtape/logtape";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import meow from "meow";
|
|
3
4
|
import { log, logtapeConfig } from "./src/logging";
|
|
4
|
-
import { defaultTheme } from "./src/themes/defaultTheme";
|
|
5
5
|
import { loadTheme } from "./src/theme/loadTheme";
|
|
6
|
+
import { defaultTheme } from "./src/themes/defaultTheme";
|
|
7
|
+
import { powerlineTheme } from "./src/themes/powerlineTheme";
|
|
8
|
+
|
|
9
|
+
// Force truecolor output — chalk auto-detection fails when invoked via piped stdin
|
|
10
|
+
chalk.level = 3;
|
|
6
11
|
|
|
7
12
|
await configure(logtapeConfig);
|
|
8
13
|
|
|
14
|
+
const BUILTIN_THEMES: Record<string, (input?: string) => Promise<string>> = {
|
|
15
|
+
default: defaultTheme,
|
|
16
|
+
powerline: powerlineTheme,
|
|
17
|
+
};
|
|
18
|
+
|
|
9
19
|
const cli = meow(
|
|
10
20
|
`
|
|
11
21
|
Usage
|
|
12
22
|
$ cc-statusline
|
|
13
23
|
|
|
14
24
|
Options
|
|
15
|
-
|
|
25
|
+
--theme, -t Use a built-in theme (powerline)
|
|
26
|
+
--theme-file, -f Use a custom theme file
|
|
16
27
|
|
|
17
28
|
Examples
|
|
18
29
|
$ cc-statusline --theme ~/.config/cc-statusline/basic.js
|
|
19
30
|
`,
|
|
20
31
|
{
|
|
21
|
-
importMeta: import.meta,
|
|
32
|
+
importMeta: import.meta,
|
|
22
33
|
flags: {
|
|
23
34
|
theme: {
|
|
24
35
|
type: "string",
|
|
25
36
|
shortFlag: "t",
|
|
26
37
|
isRequired: false,
|
|
27
38
|
},
|
|
39
|
+
themeFile: {
|
|
40
|
+
type: "string",
|
|
41
|
+
shortFlag: "f",
|
|
42
|
+
isRequired: false,
|
|
43
|
+
},
|
|
28
44
|
},
|
|
29
45
|
},
|
|
30
46
|
);
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
(
|
|
48
|
+
if (cli.flags.theme && cli.flags.themeFile) {
|
|
49
|
+
console.error("Error: --theme and --theme-file are mutually exclusive");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let resolvedTheme: (input?: string) => Promise<string>;
|
|
54
|
+
if (cli.flags.theme) {
|
|
55
|
+
const selectedTheme = cli.flags.theme;
|
|
56
|
+
resolvedTheme = BUILTIN_THEMES[selectedTheme] ?? defaultTheme;
|
|
57
|
+
} else if (cli.flags.themeFile) {
|
|
58
|
+
const selectedTheme = cli.flags.themeFile;
|
|
59
|
+
resolvedTheme = (await loadTheme(selectedTheme)) || defaultTheme;
|
|
60
|
+
} else {
|
|
61
|
+
resolvedTheme = defaultTheme;
|
|
62
|
+
}
|
|
34
63
|
|
|
35
|
-
const input = await Bun.stdin.stream().
|
|
36
|
-
log.debug("input: {input}", input);
|
|
64
|
+
const input = await Bun.stdin.stream().text();
|
|
65
|
+
log.debug("input: {input}", { input });
|
|
37
66
|
console.log(await resolvedTheme(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.4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cc-statusline": "bin/cc-statusline.js"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@logtape/file": "^1.3.6",
|
|
46
46
|
"@logtape/logtape": "^1.3.6",
|
|
47
|
-
"
|
|
47
|
+
"chalk": "^5.6.2",
|
|
48
48
|
"meow": "^14.0.0",
|
|
49
49
|
"neverthrow": "^8.2.0",
|
|
50
50
|
"simple-git": "^3.30.0",
|
package/src/cli.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import meow from "meow";
|
|
4
3
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { access,
|
|
4
|
+
import { access, copyFile, mkdir, unlink } from "node:fs/promises";
|
|
6
5
|
import { homedir } from "node:os";
|
|
7
6
|
import { join } from "node:path";
|
|
7
|
+
import meow from "meow";
|
|
8
8
|
|
|
9
9
|
const BINARY_NAME = "statusline";
|
|
10
10
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
package/src/logging.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getFileSink } from "@logtape/file";
|
|
2
|
-
import { getLogger, type Config } from "@logtape/logtape";
|
|
3
1
|
import { homedir } from "node:os";
|
|
2
|
+
import { getFileSink } from "@logtape/file";
|
|
3
|
+
import { type Config, getLogger } from "@logtape/logtape";
|
|
4
4
|
|
|
5
5
|
const logtapeConfig: Config<"file", string> = {
|
|
6
6
|
sinks: {
|
package/src/schema/statusLine.ts
CHANGED
|
@@ -30,13 +30,17 @@ const statusSchema = z.object({
|
|
|
30
30
|
.nullable(),
|
|
31
31
|
used_percentage: z.number().nullable(),
|
|
32
32
|
remaining_percentage: z.number().nullable(),
|
|
33
|
-
vim: z
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
vim: z
|
|
34
|
+
.object({
|
|
35
|
+
mode: z.enum(["INSERT", "NORMAL"]),
|
|
36
|
+
})
|
|
37
|
+
.optional(),
|
|
38
|
+
agent: z
|
|
39
|
+
.object({
|
|
40
|
+
name: z.string(),
|
|
41
|
+
type: z.string(),
|
|
42
|
+
})
|
|
43
|
+
.optional(),
|
|
40
44
|
}),
|
|
41
45
|
});
|
|
42
46
|
|
package/src/theme/loadTheme.ts
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import terminalSize from "terminal-size";
|
|
3
|
+
import { match } from "ts-pattern";
|
|
1
4
|
import { ZodError } from "zod";
|
|
2
5
|
import { log } from "../logging";
|
|
3
|
-
import {
|
|
6
|
+
import { type Status, statusSchema } from "../schema/statusLine";
|
|
7
|
+
import { currentBranchName } from "../utils/git";
|
|
4
8
|
import { abbreviateModelId } from "../utils/model";
|
|
5
9
|
import { compress, telescope } from "../utils/path";
|
|
6
10
|
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
|
|
|
11
12
|
const HORIZONTAL_PADDING = 4;
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
function colorizeUsageStatus(usedPercentage: number) {
|
|
15
|
+
if (usedPercentage === 0) {
|
|
16
|
+
return "";
|
|
17
|
+
} else if (usedPercentage <= 50) {
|
|
18
|
+
return chalk.green(`${usedPercentage}%`);
|
|
19
|
+
} else if (usedPercentage <= 75) {
|
|
20
|
+
return chalk.blue(`${usedPercentage}%`);
|
|
21
|
+
} else if (usedPercentage <= 87.5) {
|
|
22
|
+
return chalk.yellow(`${usedPercentage}%`);
|
|
23
|
+
} else {
|
|
24
|
+
return chalk.red(`${usedPercentage}%`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function renderLine1(status: Status): Promise<string> {
|
|
14
29
|
const modelId = abbreviateModelId(status.model.id);
|
|
15
|
-
const modelStatus = `🤖 ${modelId}`;
|
|
30
|
+
const modelStatus = `🤖 ${modelId} (${status.version})`;
|
|
16
31
|
|
|
17
32
|
const sessionStatus = `📃 ${status.session_id}`;
|
|
18
33
|
|
|
@@ -28,28 +43,34 @@ async function renderLine1(status: Status) : Promise<string> {
|
|
|
28
43
|
const leftGap = Math.floor(remainingSpace / 2);
|
|
29
44
|
const rightGap = Math.ceil(remainingSpace / 2);
|
|
30
45
|
|
|
31
|
-
return
|
|
46
|
+
return (
|
|
47
|
+
modelStatus +
|
|
48
|
+
" ".repeat(leftGap) +
|
|
49
|
+
sessionStatus +
|
|
50
|
+
" ".repeat(rightGap) +
|
|
51
|
+
projectStatus
|
|
52
|
+
);
|
|
32
53
|
}
|
|
33
54
|
|
|
34
|
-
async function renderLine2(status: Status)
|
|
55
|
+
async function renderLine2(status: Status): Promise<string> {
|
|
35
56
|
const branch = await currentBranchName();
|
|
36
57
|
const branchStatus = match(branch)
|
|
37
|
-
.with({status: "none"}, () => {
|
|
58
|
+
.with({ status: "none" }, () => {
|
|
38
59
|
return `💾`;
|
|
39
60
|
})
|
|
40
|
-
.with({status: "branch"}, ({name}) => {
|
|
61
|
+
.with({ status: "branch" }, ({ name }) => {
|
|
41
62
|
return `🌿 ${name}`;
|
|
42
63
|
})
|
|
43
|
-
.with({status: "detached"}, ({commit}) => {
|
|
64
|
+
.with({ status: "detached" }, ({ commit }) => {
|
|
44
65
|
return ` ${commit}`;
|
|
45
66
|
})
|
|
46
|
-
.with({status: "error"}, () => {
|
|
67
|
+
.with({ status: "error" }, () => {
|
|
47
68
|
return `💥`;
|
|
48
69
|
})
|
|
49
70
|
.exhaustive();
|
|
50
71
|
|
|
51
72
|
const usedPercentage = status.context_window.used_percentage ?? 0;
|
|
52
|
-
const usageStatus = usedPercentage === 0 ?
|
|
73
|
+
const usageStatus = usedPercentage === 0 ? "" : `${usedPercentage}%`;
|
|
53
74
|
|
|
54
75
|
const statusWidth = terminalSize().columns - HORIZONTAL_PADDING;
|
|
55
76
|
const branchWidth = getDisplayWidth(branchStatus);
|
|
@@ -57,7 +78,9 @@ async function renderLine2(status: Status) : Promise<string> {
|
|
|
57
78
|
|
|
58
79
|
const gap = statusWidth - branchWidth - usageWidth;
|
|
59
80
|
|
|
60
|
-
return
|
|
81
|
+
return (
|
|
82
|
+
branchStatus + " ".repeat(gap - 1) + colorizeUsageStatus(usedPercentage)
|
|
83
|
+
);
|
|
61
84
|
}
|
|
62
85
|
|
|
63
86
|
async function renderTheme(status: Status): Promise<string> {
|
|
@@ -69,14 +92,20 @@ async function renderTheme(status: Status): Promise<string> {
|
|
|
69
92
|
async function defaultTheme(input?: string): Promise<string> {
|
|
70
93
|
if (input) {
|
|
71
94
|
try {
|
|
72
|
-
const
|
|
95
|
+
const parsed = JSON.parse(input);
|
|
96
|
+
const status = statusSchema.parse(parsed);
|
|
73
97
|
return renderTheme(status);
|
|
74
98
|
} catch (e) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
const error =
|
|
100
|
+
e instanceof ZodError
|
|
101
|
+
? JSON.stringify(e.issues)
|
|
102
|
+
: e instanceof Error
|
|
103
|
+
? e.message
|
|
104
|
+
: JSON.stringify(e);
|
|
105
|
+
|
|
106
|
+
log.error("Failed to parse input: {error}", {
|
|
107
|
+
error: error,
|
|
108
|
+
});
|
|
80
109
|
}
|
|
81
110
|
}
|
|
82
111
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import terminalSize from "terminal-size";
|
|
3
|
+
import { match } from "ts-pattern";
|
|
4
|
+
import { ZodError } from "zod";
|
|
5
|
+
import { log } from "../logging";
|
|
6
|
+
import { type Status, statusSchema } from "../schema/statusLine";
|
|
7
|
+
import { currentBranchName } from "../utils/git";
|
|
8
|
+
import { abbreviateModelId } from "../utils/model";
|
|
9
|
+
import { compress, telescope } from "../utils/path";
|
|
10
|
+
import { getDisplayWidth } from "../utils/term";
|
|
11
|
+
|
|
12
|
+
// Right-pointing solid triangle (filled separator)
|
|
13
|
+
const RPST = "\uE0B0";
|
|
14
|
+
|
|
15
|
+
type Rgb = [number, number, number];
|
|
16
|
+
|
|
17
|
+
interface Segment {
|
|
18
|
+
text: string;
|
|
19
|
+
bg: Rgb;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Muted dark background colors for each segment type
|
|
23
|
+
const BG_MODEL: Rgb = [30, 40, 80];
|
|
24
|
+
const BG_SESSION: Rgb = [60, 30, 70];
|
|
25
|
+
const BG_PROJECT: Rgb = [25, 65, 75];
|
|
26
|
+
const BG_GIT: Rgb = [30, 65, 40];
|
|
27
|
+
const BG_USAGE: Rgb = [85, 70, 20];
|
|
28
|
+
|
|
29
|
+
/** Apply white foreground and RGB background color to text. */
|
|
30
|
+
function styleContent(text: string, bg: Rgb): string {
|
|
31
|
+
return chalk.bgRgb(...bg)(chalk.white(text));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Render a separator arrow transitioning from one bg color to another (or to default). */
|
|
35
|
+
function styleSep(from: Rgb, to?: Rgb): string {
|
|
36
|
+
const arrow = chalk.rgb(...from)(RPST);
|
|
37
|
+
return to ? chalk.bgRgb(...to)(arrow) : arrow;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Render an array of segments into a single powerline bar. */
|
|
41
|
+
function renderBar(parts: Segment[]): string {
|
|
42
|
+
let result = "";
|
|
43
|
+
let prevBg: Rgb | undefined;
|
|
44
|
+
for (const seg of parts) {
|
|
45
|
+
if (prevBg) {
|
|
46
|
+
result += styleSep(prevBg, seg.bg);
|
|
47
|
+
}
|
|
48
|
+
result += styleContent(` ${seg.text} `, seg.bg);
|
|
49
|
+
prevBg = seg.bg;
|
|
50
|
+
}
|
|
51
|
+
if (prevBg) {
|
|
52
|
+
result += styleSep(prevBg);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Lay out segments across lines, wrapping when a segment would exceed maxWidth. */
|
|
58
|
+
function layoutSegments(segments: Segment[], maxWidth: number): string {
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
let currentParts: Segment[] = [];
|
|
61
|
+
let projectedWidth = 0;
|
|
62
|
+
|
|
63
|
+
for (const seg of segments) {
|
|
64
|
+
const contentWidth = getDisplayWidth(` ${seg.text} `);
|
|
65
|
+
|
|
66
|
+
if (projectedWidth === 0) {
|
|
67
|
+
// First segment on this line
|
|
68
|
+
projectedWidth = contentWidth + 1;
|
|
69
|
+
currentParts.push(seg);
|
|
70
|
+
} else if (projectedWidth + contentWidth + 1 <= maxWidth) {
|
|
71
|
+
// Fits on current line
|
|
72
|
+
projectedWidth += contentWidth + 1;
|
|
73
|
+
currentParts.push(seg);
|
|
74
|
+
} else {
|
|
75
|
+
// Doesn't fit — close current line and start a new one
|
|
76
|
+
lines.push(renderBar(currentParts));
|
|
77
|
+
currentParts = [seg];
|
|
78
|
+
projectedWidth = contentWidth + 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (currentParts.length > 0) {
|
|
83
|
+
lines.push(renderBar(currentParts));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function renderLine1(status: Status): Promise<string> {
|
|
90
|
+
const modelId = abbreviateModelId(status.model.id);
|
|
91
|
+
const modelText = `🤖 ${modelId} (${status.version})`;
|
|
92
|
+
|
|
93
|
+
const sessionText = `📃 ${status.session_id}`;
|
|
94
|
+
|
|
95
|
+
const projectDir = compress(telescope(status.workspace.project_dir));
|
|
96
|
+
const projectText = `🗂️ ${projectDir}`;
|
|
97
|
+
|
|
98
|
+
const branch = await currentBranchName();
|
|
99
|
+
const branchText = match(branch)
|
|
100
|
+
.with({ status: "none" }, () => `💾`)
|
|
101
|
+
.with({ status: "branch" }, ({ name }) => `🌿 ${name}`)
|
|
102
|
+
.with({ status: "detached" }, ({ commit }) => ` ${commit}`)
|
|
103
|
+
.with({ status: "error" }, () => `💥`)
|
|
104
|
+
.exhaustive();
|
|
105
|
+
|
|
106
|
+
const usedPercentage = status.context_window.used_percentage ?? 0;
|
|
107
|
+
|
|
108
|
+
const segments: Segment[] = [
|
|
109
|
+
{ text: modelText, bg: BG_MODEL },
|
|
110
|
+
{ text: sessionText, bg: BG_SESSION },
|
|
111
|
+
{ text: projectText, bg: BG_PROJECT },
|
|
112
|
+
{ text: branchText, bg: BG_GIT },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
if (usedPercentage > 0) {
|
|
116
|
+
segments.push({ text: `${usedPercentage}%`, bg: BG_USAGE });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const maxWidth = terminalSize().columns;
|
|
120
|
+
return layoutSegments(segments, maxWidth);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function renderTheme(status: Status): Promise<string> {
|
|
124
|
+
return renderLine1(status);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function powerlineTheme(input?: string): Promise<string> {
|
|
128
|
+
if (input) {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(input);
|
|
131
|
+
const status = statusSchema.parse(parsed);
|
|
132
|
+
return renderTheme(status);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
const error =
|
|
135
|
+
e instanceof ZodError
|
|
136
|
+
? JSON.stringify(e.issues)
|
|
137
|
+
: e instanceof Error
|
|
138
|
+
? e.message
|
|
139
|
+
: JSON.stringify(e);
|
|
140
|
+
|
|
141
|
+
log.error("Failed to parse input: {error}", {
|
|
142
|
+
error: error,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { powerlineTheme };
|
package/src/utils/git.ts
CHANGED
package/src/utils/term.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const EMOJI_REGEX = /\p{Extended_Pictographic}/gu;
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Calculates the display width of a string, accounting for emojis
|
|
3
5
|
* which occupy 2 character columns in terminal displays.
|
|
@@ -6,14 +8,13 @@
|
|
|
6
8
|
* @returns The display width in columns
|
|
7
9
|
*/
|
|
8
10
|
function getDisplayWidth(str: string): number {
|
|
9
|
-
//
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// Total width = characters + extra count for emojis (since each emoji is 2 wide)
|
|
11
|
+
// 1. Count regular characters
|
|
12
|
+
const charCount = Array.from(str).length;
|
|
13
|
+
|
|
14
|
+
// 2. Count emojis (each emoji counts as 2 characters)
|
|
15
|
+
const emojiCount = (str.match(EMOJI_REGEX) || []).length;
|
|
16
|
+
|
|
17
|
+
// 3. Total width = characters + extra count for emojis (since each emoji is 2 wide)
|
|
17
18
|
return charCount + emojiCount;
|
|
18
19
|
}
|
|
19
20
|
|