@bastra-recall/statusline 0.9.2 → 1.0.1
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/dist/.build-revision +2 -0
- package/dist/browser.d.ts +100 -102
- package/dist/browser.js +3 -3
- package/dist/index.mjs +14 -14
- package/package.json +4 -4
- package/src/config/defaults.ts +5 -1
- package/src/powerline-layout.ts +174 -0
- package/src/powerline-segments.ts +374 -0
- package/src/powerline-theme.ts +323 -0
- package/src/powerline.ts +53 -835
- package/src/segments/git.ts +3 -3
- package/src/segments/pricing.ts +78 -1
- package/src/segments/renderer-bars.ts +83 -0
- package/src/segments/renderer-format.ts +89 -0
- package/src/segments/renderer-types.ts +171 -0
- package/src/segments/renderer.ts +80 -328
- package/src/segments/session.ts +29 -5
- package/src/segments/today.ts +11 -13
- package/src/tui/sections-bars.ts +215 -0
- package/src/tui/sections-parts.ts +604 -0
- package/src/tui/sections-shared.ts +23 -0
- package/src/tui/sections.ts +67 -819
- package/src/utils/budget.ts +1 -1
- package/src/utils/claude.ts +55 -16
- package/src/utils/formatters.ts +12 -5
- package/src/utils/terminal.ts +5 -5
package/src/utils/budget.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function calculateBudgetPercentage(
|
|
|
18
18
|
budget: number | undefined,
|
|
19
19
|
): number | null {
|
|
20
20
|
if (!budget || budget <= 0 || cost < 0) return null;
|
|
21
|
-
return
|
|
21
|
+
return (cost / budget) * 100;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export function getBudgetStatus(
|
package/src/utils/claude.ts
CHANGED
|
@@ -71,6 +71,21 @@ export interface ClaudeHookData {
|
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// A rate-limit window whose percentage is not a finite number carries no
|
|
75
|
+
// usage data; drop it so no renderer reads null as 0%.
|
|
76
|
+
export function dropUnreadableRateLimits(
|
|
77
|
+
hookData: ClaudeHookData,
|
|
78
|
+
): ClaudeHookData {
|
|
79
|
+
const limits = hookData.rate_limits;
|
|
80
|
+
if (!limits) return hookData;
|
|
81
|
+
const readable = (w?: { used_percentage: number }) =>
|
|
82
|
+
typeof w?.used_percentage === "number" && Number.isFinite(w.used_percentage);
|
|
83
|
+
const rate_limits = { ...limits };
|
|
84
|
+
if (!readable(limits.five_hour)) delete rate_limits.five_hour;
|
|
85
|
+
if (!readable(limits.seven_day)) delete rate_limits.seven_day;
|
|
86
|
+
return { ...hookData, rate_limits };
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
export function getEffortLevel(hookData: ClaudeHookData): string | null {
|
|
75
90
|
const level = hookData.effort?.level;
|
|
76
91
|
if (typeof level !== "string") return null;
|
|
@@ -140,6 +155,24 @@ export async function findProjectPaths(
|
|
|
140
155
|
return projectPaths;
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
// Whether the usage source itself can be read. Only then does "no entries"
|
|
159
|
+
// mean nothing was spent; a missing or unreadable projects directory means
|
|
160
|
+
// the usage is unknown.
|
|
161
|
+
export async function canReadProjects(): Promise<boolean> {
|
|
162
|
+
let readable = false;
|
|
163
|
+
|
|
164
|
+
for (const claudePath of getClaudePaths()) {
|
|
165
|
+
try {
|
|
166
|
+
await readdir(join(claudePath, "projects"));
|
|
167
|
+
readable = true;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if ((error as { code?: string }).code !== "ENOENT") return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return readable;
|
|
174
|
+
}
|
|
175
|
+
|
|
143
176
|
export async function findTranscriptFile(
|
|
144
177
|
sessionId: string,
|
|
145
178
|
): Promise<string | null> {
|
|
@@ -296,28 +329,34 @@ const STREAMING_THRESHOLD_BYTES = 1024 * 1024;
|
|
|
296
329
|
|
|
297
330
|
export async function parseJsonlFile(filePath: string): Promise<ParsedEntry[]> {
|
|
298
331
|
try {
|
|
299
|
-
|
|
300
|
-
const fileSizeBytes = stats.size;
|
|
301
|
-
let entries: ParsedEntry[];
|
|
302
|
-
|
|
303
|
-
if (fileSizeBytes > STREAMING_THRESHOLD_BYTES) {
|
|
304
|
-
debug(
|
|
305
|
-
`Using streaming parser for large file ${filePath} (${Math.round(fileSizeBytes / 1024)}KB)`,
|
|
306
|
-
);
|
|
307
|
-
entries = await parseJsonlFileStreaming(filePath);
|
|
308
|
-
} else {
|
|
309
|
-
entries = await parseJsonlFileInMemory(filePath);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
debug(`Parsed ${entries.length} entries from ${filePath}`);
|
|
313
|
-
|
|
314
|
-
return entries;
|
|
332
|
+
return await readJsonlFile(filePath);
|
|
315
333
|
} catch (error) {
|
|
316
334
|
debug(`Failed to read file ${filePath}:`, error);
|
|
317
335
|
return [];
|
|
318
336
|
}
|
|
319
337
|
}
|
|
320
338
|
|
|
339
|
+
// parseJsonlFile for callers that must tell an unreadable file from an empty
|
|
340
|
+
// one: a read error throws instead of yielding no entries.
|
|
341
|
+
export async function readJsonlFile(filePath: string): Promise<ParsedEntry[]> {
|
|
342
|
+
const stats = await stat(filePath);
|
|
343
|
+
const fileSizeBytes = stats.size;
|
|
344
|
+
let entries: ParsedEntry[];
|
|
345
|
+
|
|
346
|
+
if (fileSizeBytes > STREAMING_THRESHOLD_BYTES) {
|
|
347
|
+
debug(
|
|
348
|
+
`Using streaming parser for large file ${filePath} (${Math.round(fileSizeBytes / 1024)}KB)`,
|
|
349
|
+
);
|
|
350
|
+
entries = await parseJsonlFileStreaming(filePath);
|
|
351
|
+
} else {
|
|
352
|
+
entries = await parseJsonlFileInMemory(filePath);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
debug(`Parsed ${entries.length} entries from ${filePath}`);
|
|
356
|
+
|
|
357
|
+
return entries;
|
|
358
|
+
}
|
|
359
|
+
|
|
321
360
|
async function parseJsonlFileInMemory(
|
|
322
361
|
filePath: string,
|
|
323
362
|
): Promise<ParsedEntry[]> {
|
package/src/utils/formatters.ts
CHANGED
|
@@ -6,13 +6,14 @@ interface TokenBreakdown {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function formatCost(cost: number | null): string {
|
|
9
|
-
if (cost === null) return "
|
|
9
|
+
if (cost === null) return "--";
|
|
10
|
+
if (cost === 0) return "$0.00";
|
|
10
11
|
if (cost < 0.01) return "<$0.01";
|
|
11
12
|
return `$${cost.toFixed(2)}`;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function formatTokens(tokens: number | null): string {
|
|
15
|
-
if (tokens === null) return "
|
|
16
|
+
if (tokens === null) return "--";
|
|
16
17
|
if (tokens === 0) return "0 tokens";
|
|
17
18
|
if (tokens >= 1_000_000) {
|
|
18
19
|
return `${(tokens / 1_000_000).toFixed(1)}M tokens`;
|
|
@@ -23,7 +24,7 @@ export function formatTokens(tokens: number | null): string {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function formatTokenBreakdown(breakdown: TokenBreakdown | null): string {
|
|
26
|
-
if (!breakdown) return "
|
|
27
|
+
if (!breakdown) return "--";
|
|
27
28
|
|
|
28
29
|
const parts: string[] = [];
|
|
29
30
|
|
|
@@ -130,8 +131,14 @@ export function collapseHome(dirPath: string, homeDir?: string): string {
|
|
|
130
131
|
homeDir ??
|
|
131
132
|
globalThis.process?.env?.HOME ??
|
|
132
133
|
globalThis.process?.env?.USERPROFILE;
|
|
133
|
-
if (
|
|
134
|
-
|
|
134
|
+
if (
|
|
135
|
+
home &&
|
|
136
|
+
dirPath.startsWith(home) &&
|
|
137
|
+
(dirPath.length === home.length ||
|
|
138
|
+
dirPath[home.length] === "/" ||
|
|
139
|
+
dirPath[home.length] === "\\")
|
|
140
|
+
) {
|
|
141
|
+
return `~${dirPath.slice(home.length)}`;
|
|
135
142
|
}
|
|
136
143
|
return dirPath;
|
|
137
144
|
}
|
package/src/utils/terminal.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const ESC = String.fromCharCode(27);
|
|
2
2
|
const ANSI_REGEX = new RegExp(`${ESC}\\[[0-9;]*m`, "g");
|
|
3
3
|
export const ANSI_SPLIT = new RegExp(`(${ESC}\\[[0-9;]*m)`);
|
|
4
|
+
const COMBINING_MARK = /[\p{Mn}\p{Me}]/u;
|
|
4
5
|
|
|
5
6
|
export function stripAnsi(str: string): string {
|
|
6
7
|
return str.replace(ANSI_REGEX, "");
|
|
@@ -14,12 +15,11 @@ export function stripAnsi(str: string): string {
|
|
|
14
15
|
*/
|
|
15
16
|
export function codePointWidth(cp: number): number {
|
|
16
17
|
if (
|
|
17
|
-
(cp >= 0x0300 && cp <= 0x036f) || // combining diacritics
|
|
18
|
-
(cp >= 0x1ab0 && cp <= 0x1aff) ||
|
|
19
|
-
(cp >= 0x1dc0 && cp <= 0x1dff) ||
|
|
20
|
-
(cp >= 0x20d0 && cp <= 0x20ff) || // combining for symbols
|
|
21
18
|
(cp >= 0xfe00 && cp <= 0xfe0f) || // variation selectors
|
|
22
|
-
cp === 0x200b || cp === 0x200c || cp === 0x200d || cp === 0xfeff
|
|
19
|
+
cp === 0x200b || cp === 0x200c || cp === 0x200d || cp === 0xfeff ||
|
|
20
|
+
// Unicode mark categories avoid a fixed list of scripts. Mc spacing
|
|
21
|
+
// marks still occupy a column; Mn/Me marks do not.
|
|
22
|
+
(cp >= 0x0300 && cp <= 0x10ffff && COMBINING_MARK.test(String.fromCodePoint(cp)))
|
|
23
23
|
) {
|
|
24
24
|
return 0;
|
|
25
25
|
}
|