@hasna/statusline 0.0.8 → 0.0.9
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 +13 -1
- package/dist/cli.js +122 -5
- package/dist/disk.d.ts +51 -0
- package/dist/format.d.ts +7 -0
- package/dist/index.js +122 -5
- package/dist/mcp/index.js +273 -156
- package/dist/providers/types.d.ts +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ That's it — Claude Code picks it up on the next status refresh (no restart nee
|
|
|
22
22
|
## Segments
|
|
23
23
|
|
|
24
24
|
`statusline list` shows a compact, enabled-first summary of your segments (use
|
|
25
|
-
`statusline list --all` for every row). The table below is the full catalog — all
|
|
25
|
+
`statusline list --all` for every row). The table below is the full catalog — all 31
|
|
26
26
|
segment ids from `src/segments/index.ts`.
|
|
27
27
|
|
|
28
28
|
`renderLine` walks your configured segment order, renders each one, and **drops**
|
|
@@ -36,6 +36,7 @@ anything that returns `null` or throws. A failed segment never breaks the host U
|
|
|
36
36
|
| `git-branch` | off | Current Git branch | `git branch --show-current` in `cwd` | outside git or detached | `main` |
|
|
37
37
|
| `commit-age` | on | Time since last commit (compact) | `git log -1` epoch vs now | no commits / not a repo | `12h` |
|
|
38
38
|
| `loc` | on | Lines of code tracked by Git (compact) | `git ls-files` + `wc -l` in repo root | outside git or zero lines | `1.2k` |
|
|
39
|
+
| `disk-free` | off | Free space on the volume holding the working directory | one `fs.statfsSync` syscall on `StatusContext.cwd` — no subprocess, no polling, no cache | the volume cannot be read (missing cwd, unsupported filesystem) | `disk 24.1G` · `disk 8G low` |
|
|
39
40
|
| `current-dir` | off | Current working directory (basename) | `StatusContext.cwd` | never (always renders basename) | `statusline` |
|
|
40
41
|
| `cwd` | off | Current working directory (full path) | `StatusContext.cwd`, home directory abbreviated to `~`, head elided past 40 chars | no cwd in the payload | `~/work/statusline` |
|
|
41
42
|
| `model` | off | Current model name (no context tag) | `StatusContext.model.id` via friendly formatter | no model id | `fable 5` |
|
|
@@ -126,6 +127,9 @@ separators are never coloured.
|
|
|
126
127
|
80% and above (the same shape the rate-limit segments use);
|
|
127
128
|
- the rate-limit segments escalate from yellow to red at 80%;
|
|
128
129
|
- `cwd` uses a calm blue accent and elides the head of long paths;
|
|
130
|
+
- `disk-free` stays dim while the volume has headroom, turns yellow below 20%
|
|
131
|
+
free and red below 10%, and appends a `low` marker so the warning survives
|
|
132
|
+
`NO_COLOR`;
|
|
129
133
|
- model, cost and duration render dim, so they never compete with a warning.
|
|
130
134
|
|
|
131
135
|
```bash
|
|
@@ -231,6 +235,14 @@ The `newline` segment starts a new row; empty rows are removed. Segments
|
|
|
231
235
|
that need more than the payload offers (e.g. `context-remaining`) read the session
|
|
232
236
|
transcript to compute it. A segment failure is never fatal — it's simply omitted.
|
|
233
237
|
|
|
238
|
+
There is no long-running statusline process, so nothing polls. `disk-free` reads
|
|
239
|
+
free space once per render with `fs.statfsSync` (about half a microsecond), the
|
|
240
|
+
same way the host re-runs the command for each refresh; shelling out to `df`
|
|
241
|
+
would fork a process every time and cost roughly a millisecond. That one reading
|
|
242
|
+
is shared by the segment's text and its colour, so they cannot disagree. The
|
|
243
|
+
segment adds under 2 µs to an in-process render, which is below the noise of the
|
|
244
|
+
~17 ms process invocation.
|
|
245
|
+
|
|
234
246
|
## SDK
|
|
235
247
|
|
|
236
248
|
The package root is a side-effect-free SDK surface:
|
package/dist/cli.js
CHANGED
|
@@ -157,6 +157,23 @@ function compactNum(n) {
|
|
|
157
157
|
return `${trimmed(n / 1000)}k`;
|
|
158
158
|
return `${n}`;
|
|
159
159
|
}
|
|
160
|
+
var BYTE_UNITS = ["K", "M", "G", "T", "P"];
|
|
161
|
+
function compactBytes(bytes) {
|
|
162
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
163
|
+
return "";
|
|
164
|
+
if (bytes < 1024)
|
|
165
|
+
return `${Math.floor(bytes)}B`;
|
|
166
|
+
let value = bytes / 1024;
|
|
167
|
+
let unit = 0;
|
|
168
|
+
while (unit < BYTE_UNITS.length - 1) {
|
|
169
|
+
const rounded = Number(value.toFixed(1));
|
|
170
|
+
if (rounded < 1024)
|
|
171
|
+
break;
|
|
172
|
+
value /= 1024;
|
|
173
|
+
unit += 1;
|
|
174
|
+
}
|
|
175
|
+
return `${trimmed(value)}${BYTE_UNITS[unit]}`;
|
|
176
|
+
}
|
|
160
177
|
function money(n) {
|
|
161
178
|
return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
162
179
|
}
|
|
@@ -416,6 +433,95 @@ function windowSize(ctx) {
|
|
|
416
433
|
return ctx.model?.id?.includes("[1m]") ? 1e6 : 200000;
|
|
417
434
|
}
|
|
418
435
|
|
|
436
|
+
// src/disk.ts
|
|
437
|
+
import { statfsSync } from "fs";
|
|
438
|
+
var DISK_MEMO = Symbol("statusline.disk-free");
|
|
439
|
+
var DISK_WARN_PERCENT = 20;
|
|
440
|
+
var DISK_CRITICAL_PERCENT = 10;
|
|
441
|
+
function finiteNonNegative(value) {
|
|
442
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
443
|
+
}
|
|
444
|
+
function readDiskStats(path) {
|
|
445
|
+
if (typeof statfsSync !== "function")
|
|
446
|
+
return null;
|
|
447
|
+
try {
|
|
448
|
+
const stats = statfsSync(path);
|
|
449
|
+
if (!stats)
|
|
450
|
+
return null;
|
|
451
|
+
return {
|
|
452
|
+
bsize: Number(stats.bsize),
|
|
453
|
+
blocks: Number(stats.blocks),
|
|
454
|
+
bavail: Number(stats.bavail)
|
|
455
|
+
};
|
|
456
|
+
} catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function diskFree(path, reader = readDiskStats) {
|
|
461
|
+
let stats;
|
|
462
|
+
try {
|
|
463
|
+
stats = reader(path);
|
|
464
|
+
} catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
if (!stats)
|
|
468
|
+
return null;
|
|
469
|
+
const bsize = finiteNonNegative(stats.bsize);
|
|
470
|
+
const blocks = finiteNonNegative(stats.blocks);
|
|
471
|
+
const bavail = finiteNonNegative(stats.bavail);
|
|
472
|
+
if (bsize === null || blocks === null || bavail === null)
|
|
473
|
+
return null;
|
|
474
|
+
if (bsize <= 0 || blocks <= 0)
|
|
475
|
+
return null;
|
|
476
|
+
const available = Math.min(bavail, blocks);
|
|
477
|
+
const totalBytes = bsize * blocks;
|
|
478
|
+
const freeBytes = bsize * available;
|
|
479
|
+
if (!Number.isFinite(totalBytes) || !Number.isFinite(freeBytes))
|
|
480
|
+
return null;
|
|
481
|
+
return {
|
|
482
|
+
freeBytes,
|
|
483
|
+
totalBytes,
|
|
484
|
+
freePercent: available / blocks * 100
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function diskFreeFor(path, memo, reader = readDiskStats) {
|
|
488
|
+
if (!memo)
|
|
489
|
+
return diskFree(path, reader);
|
|
490
|
+
const hit = memo.get(DISK_MEMO);
|
|
491
|
+
if (hit && hit.path === path)
|
|
492
|
+
return hit.value;
|
|
493
|
+
const value = diskFree(path, reader);
|
|
494
|
+
memo.set(DISK_MEMO, { path, value });
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
function diskLevel(freePercent) {
|
|
498
|
+
if (!Number.isFinite(freePercent))
|
|
499
|
+
return null;
|
|
500
|
+
if (freePercent < DISK_CRITICAL_PERCENT)
|
|
501
|
+
return "critical";
|
|
502
|
+
if (freePercent < DISK_WARN_PERCENT)
|
|
503
|
+
return "low";
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
function formatDiskFree(disk) {
|
|
507
|
+
const value = compactBytes(disk.freeBytes);
|
|
508
|
+
if (!value)
|
|
509
|
+
return "";
|
|
510
|
+
return diskLevel(disk.freePercent) ? `disk ${value} low` : `disk ${value}`;
|
|
511
|
+
}
|
|
512
|
+
function diskColor(disk) {
|
|
513
|
+
if (!disk)
|
|
514
|
+
return null;
|
|
515
|
+
switch (diskLevel(disk.freePercent)) {
|
|
516
|
+
case "critical":
|
|
517
|
+
return "red";
|
|
518
|
+
case "low":
|
|
519
|
+
return "yellow";
|
|
520
|
+
default:
|
|
521
|
+
return "dim";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
419
525
|
// src/accounts.ts
|
|
420
526
|
import { existsSync as existsSync5, readFileSync as readFileSync4, realpathSync } from "fs";
|
|
421
527
|
import { homedir as homedir4 } from "os";
|
|
@@ -804,6 +910,16 @@ var segments = [
|
|
|
804
910
|
return n ? compactNum(n) : null;
|
|
805
911
|
}
|
|
806
912
|
},
|
|
913
|
+
{
|
|
914
|
+
id: "disk-free",
|
|
915
|
+
description: "Free space on the volume holding the working directory",
|
|
916
|
+
defaultEnabled: false,
|
|
917
|
+
color: (ctx, memo) => diskColor(diskFreeFor(dir(ctx), memo)),
|
|
918
|
+
render(ctx, memo) {
|
|
919
|
+
const disk = diskFreeFor(dir(ctx), memo);
|
|
920
|
+
return disk ? formatDiskFree(disk) || null : null;
|
|
921
|
+
}
|
|
922
|
+
},
|
|
807
923
|
{
|
|
808
924
|
id: "current-dir",
|
|
809
925
|
description: "Current working directory (basename)",
|
|
@@ -1051,13 +1167,14 @@ function colorsEnabled(cfg, env = process.env) {
|
|
|
1051
1167
|
return false;
|
|
1052
1168
|
return cfg.colors !== false;
|
|
1053
1169
|
}
|
|
1054
|
-
function paint(segment, text, ctx) {
|
|
1055
|
-
const color = typeof segment.color === "function" ? segment.color(ctx) : segment.color;
|
|
1170
|
+
function paint(segment, text, ctx, memo) {
|
|
1171
|
+
const color = typeof segment.color === "function" ? segment.color(ctx, memo) : segment.color;
|
|
1056
1172
|
const code = color ? ANSI[color] : undefined;
|
|
1057
1173
|
return code ? `${code}${text}${RESET}` : text;
|
|
1058
1174
|
}
|
|
1059
1175
|
async function renderLine(ctx, cfg) {
|
|
1060
1176
|
const colorize = colorsEnabled(cfg);
|
|
1177
|
+
const memo = new Map;
|
|
1061
1178
|
const rows = [[]];
|
|
1062
1179
|
for (const id of cfg.segments) {
|
|
1063
1180
|
if (id === "newline") {
|
|
@@ -1068,9 +1185,9 @@ async function renderLine(ctx, cfg) {
|
|
|
1068
1185
|
if (!segment)
|
|
1069
1186
|
continue;
|
|
1070
1187
|
try {
|
|
1071
|
-
const out = await segment.render(ctx);
|
|
1188
|
+
const out = await segment.render(ctx, memo);
|
|
1072
1189
|
if (out)
|
|
1073
|
-
rows[rows.length - 1].push(colorize ? paint(segment, out, ctx) : out);
|
|
1190
|
+
rows[rows.length - 1].push(colorize ? paint(segment, out, ctx, memo) : out);
|
|
1074
1191
|
} catch {}
|
|
1075
1192
|
}
|
|
1076
1193
|
return rows.map((parts) => parts.join(cfg.separator)).filter((row) => row.length > 0).join(`
|
|
@@ -1079,7 +1196,7 @@ async function renderLine(ctx, cfg) {
|
|
|
1079
1196
|
// package.json
|
|
1080
1197
|
var package_default = {
|
|
1081
1198
|
name: "@hasna/statusline",
|
|
1082
|
-
version: "0.0.
|
|
1199
|
+
version: "0.0.9",
|
|
1083
1200
|
description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
|
|
1084
1201
|
license: "Apache-2.0",
|
|
1085
1202
|
type: "module",
|
package/dist/disk.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RenderMemo, SegmentColor } from "./providers/types.js";
|
|
2
|
+
/** The statfs fields this module needs; node's StatFs carries more. */
|
|
3
|
+
export interface DiskStats {
|
|
4
|
+
bsize: number;
|
|
5
|
+
blocks: number;
|
|
6
|
+
bavail: number;
|
|
7
|
+
}
|
|
8
|
+
/** Injectable reader: tests pass a fake, the render uses the real syscall. */
|
|
9
|
+
export type DiskStatfsReader = (path: string) => DiskStats | null;
|
|
10
|
+
export interface DiskFree {
|
|
11
|
+
freeBytes: number;
|
|
12
|
+
totalBytes: number;
|
|
13
|
+
/** Blocks available to an unprivileged user, as a percentage. */
|
|
14
|
+
freePercent: number;
|
|
15
|
+
}
|
|
16
|
+
/** Below 20% free a disk is worth flagging; below 10% it is critical. */
|
|
17
|
+
export declare const DISK_WARN_PERCENT = 20;
|
|
18
|
+
export declare const DISK_CRITICAL_PERCENT = 10;
|
|
19
|
+
export type DiskLevel = "low" | "critical";
|
|
20
|
+
/**
|
|
21
|
+
* Read a filesystem's stats with one syscall. Returns null when the runtime
|
|
22
|
+
* has no `statfs` or the call fails (missing path, permission, unsupported
|
|
23
|
+
* filesystem), so the caller can omit the segment instead of guessing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readDiskStats(path: string): DiskStats | null;
|
|
26
|
+
/**
|
|
27
|
+
* Free bytes for `path`'s volume, or null when the reading is unusable.
|
|
28
|
+
*
|
|
29
|
+
* Unreadable or missing fields are null, never zero: a rendered zero has to
|
|
30
|
+
* mean "the disk really is full", not "this runtime could not tell me".
|
|
31
|
+
*/
|
|
32
|
+
export declare function diskFree(path: string, reader?: DiskStatfsReader): DiskFree | null;
|
|
33
|
+
/**
|
|
34
|
+
* Free space for `path`, read at most once per render.
|
|
35
|
+
*
|
|
36
|
+
* The memo belongs to a single `renderLine` call and is shared by a segment's
|
|
37
|
+
* text and colour, so the volume is read once instead of once per consumer. It
|
|
38
|
+
* is discarded when that render ends: the next invocation reads afresh, so this
|
|
39
|
+
* is not a cache and cannot show a figure from a previous render.
|
|
40
|
+
*/
|
|
41
|
+
export declare function diskFreeFor(path: string, memo?: RenderMemo, reader?: DiskStatfsReader): DiskFree | null;
|
|
42
|
+
/** Severity for a free percentage; null while the volume has headroom. */
|
|
43
|
+
export declare function diskLevel(freePercent: number): DiskLevel | null;
|
|
44
|
+
/**
|
|
45
|
+
* `disk 24G` while healthy, `disk 8.2G low` below the warning line. The
|
|
46
|
+
* textual marker is deliberate: colour alone disappears under `NO_COLOR`,
|
|
47
|
+
* where a nearly-full disk still has to be visible at a glance.
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatDiskFree(disk: DiskFree): string;
|
|
50
|
+
/** Healthy disks stay dim; low space escalates yellow, then red. */
|
|
51
|
+
export declare function diskColor(disk: DiskFree | null): SegmentColor | null;
|
package/dist/format.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export declare function compactAge(seconds: number): string;
|
|
2
2
|
export declare function compactNum(n: number): string;
|
|
3
|
+
/**
|
|
4
|
+
* Byte count in binary units, one decimal at most: `461k`, `24.1G`, `512B`.
|
|
5
|
+
* Unit letters match `df -h` (K/M/G/T are 1024-based here) so the number
|
|
6
|
+
* agrees with what an operator sees in a shell. An unusable value — negative,
|
|
7
|
+
* NaN, Infinity — returns "" rather than a plausible-looking number.
|
|
8
|
+
*/
|
|
9
|
+
export declare function compactBytes(bytes: number): string;
|
|
3
10
|
export declare function money(n: number): string;
|
|
4
11
|
export declare function compactDuration(ms: number): string;
|
package/dist/index.js
CHANGED
|
@@ -156,6 +156,23 @@ function compactNum(n) {
|
|
|
156
156
|
return `${trimmed(n / 1000)}k`;
|
|
157
157
|
return `${n}`;
|
|
158
158
|
}
|
|
159
|
+
var BYTE_UNITS = ["K", "M", "G", "T", "P"];
|
|
160
|
+
function compactBytes(bytes) {
|
|
161
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
162
|
+
return "";
|
|
163
|
+
if (bytes < 1024)
|
|
164
|
+
return `${Math.floor(bytes)}B`;
|
|
165
|
+
let value = bytes / 1024;
|
|
166
|
+
let unit = 0;
|
|
167
|
+
while (unit < BYTE_UNITS.length - 1) {
|
|
168
|
+
const rounded = Number(value.toFixed(1));
|
|
169
|
+
if (rounded < 1024)
|
|
170
|
+
break;
|
|
171
|
+
value /= 1024;
|
|
172
|
+
unit += 1;
|
|
173
|
+
}
|
|
174
|
+
return `${trimmed(value)}${BYTE_UNITS[unit]}`;
|
|
175
|
+
}
|
|
159
176
|
function money(n) {
|
|
160
177
|
return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
161
178
|
}
|
|
@@ -415,6 +432,95 @@ function windowSize(ctx) {
|
|
|
415
432
|
return ctx.model?.id?.includes("[1m]") ? 1e6 : 200000;
|
|
416
433
|
}
|
|
417
434
|
|
|
435
|
+
// src/disk.ts
|
|
436
|
+
import { statfsSync } from "fs";
|
|
437
|
+
var DISK_MEMO = Symbol("statusline.disk-free");
|
|
438
|
+
var DISK_WARN_PERCENT = 20;
|
|
439
|
+
var DISK_CRITICAL_PERCENT = 10;
|
|
440
|
+
function finiteNonNegative(value) {
|
|
441
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
442
|
+
}
|
|
443
|
+
function readDiskStats(path) {
|
|
444
|
+
if (typeof statfsSync !== "function")
|
|
445
|
+
return null;
|
|
446
|
+
try {
|
|
447
|
+
const stats = statfsSync(path);
|
|
448
|
+
if (!stats)
|
|
449
|
+
return null;
|
|
450
|
+
return {
|
|
451
|
+
bsize: Number(stats.bsize),
|
|
452
|
+
blocks: Number(stats.blocks),
|
|
453
|
+
bavail: Number(stats.bavail)
|
|
454
|
+
};
|
|
455
|
+
} catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function diskFree(path, reader = readDiskStats) {
|
|
460
|
+
let stats;
|
|
461
|
+
try {
|
|
462
|
+
stats = reader(path);
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
if (!stats)
|
|
467
|
+
return null;
|
|
468
|
+
const bsize = finiteNonNegative(stats.bsize);
|
|
469
|
+
const blocks = finiteNonNegative(stats.blocks);
|
|
470
|
+
const bavail = finiteNonNegative(stats.bavail);
|
|
471
|
+
if (bsize === null || blocks === null || bavail === null)
|
|
472
|
+
return null;
|
|
473
|
+
if (bsize <= 0 || blocks <= 0)
|
|
474
|
+
return null;
|
|
475
|
+
const available = Math.min(bavail, blocks);
|
|
476
|
+
const totalBytes = bsize * blocks;
|
|
477
|
+
const freeBytes = bsize * available;
|
|
478
|
+
if (!Number.isFinite(totalBytes) || !Number.isFinite(freeBytes))
|
|
479
|
+
return null;
|
|
480
|
+
return {
|
|
481
|
+
freeBytes,
|
|
482
|
+
totalBytes,
|
|
483
|
+
freePercent: available / blocks * 100
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function diskFreeFor(path, memo, reader = readDiskStats) {
|
|
487
|
+
if (!memo)
|
|
488
|
+
return diskFree(path, reader);
|
|
489
|
+
const hit = memo.get(DISK_MEMO);
|
|
490
|
+
if (hit && hit.path === path)
|
|
491
|
+
return hit.value;
|
|
492
|
+
const value = diskFree(path, reader);
|
|
493
|
+
memo.set(DISK_MEMO, { path, value });
|
|
494
|
+
return value;
|
|
495
|
+
}
|
|
496
|
+
function diskLevel(freePercent) {
|
|
497
|
+
if (!Number.isFinite(freePercent))
|
|
498
|
+
return null;
|
|
499
|
+
if (freePercent < DISK_CRITICAL_PERCENT)
|
|
500
|
+
return "critical";
|
|
501
|
+
if (freePercent < DISK_WARN_PERCENT)
|
|
502
|
+
return "low";
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
function formatDiskFree(disk) {
|
|
506
|
+
const value = compactBytes(disk.freeBytes);
|
|
507
|
+
if (!value)
|
|
508
|
+
return "";
|
|
509
|
+
return diskLevel(disk.freePercent) ? `disk ${value} low` : `disk ${value}`;
|
|
510
|
+
}
|
|
511
|
+
function diskColor(disk) {
|
|
512
|
+
if (!disk)
|
|
513
|
+
return null;
|
|
514
|
+
switch (diskLevel(disk.freePercent)) {
|
|
515
|
+
case "critical":
|
|
516
|
+
return "red";
|
|
517
|
+
case "low":
|
|
518
|
+
return "yellow";
|
|
519
|
+
default:
|
|
520
|
+
return "dim";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
418
524
|
// src/accounts.ts
|
|
419
525
|
import { existsSync as existsSync5, readFileSync as readFileSync4, realpathSync } from "fs";
|
|
420
526
|
import { homedir as homedir4 } from "os";
|
|
@@ -803,6 +909,16 @@ var segments = [
|
|
|
803
909
|
return n ? compactNum(n) : null;
|
|
804
910
|
}
|
|
805
911
|
},
|
|
912
|
+
{
|
|
913
|
+
id: "disk-free",
|
|
914
|
+
description: "Free space on the volume holding the working directory",
|
|
915
|
+
defaultEnabled: false,
|
|
916
|
+
color: (ctx, memo) => diskColor(diskFreeFor(dir(ctx), memo)),
|
|
917
|
+
render(ctx, memo) {
|
|
918
|
+
const disk = diskFreeFor(dir(ctx), memo);
|
|
919
|
+
return disk ? formatDiskFree(disk) || null : null;
|
|
920
|
+
}
|
|
921
|
+
},
|
|
806
922
|
{
|
|
807
923
|
id: "current-dir",
|
|
808
924
|
description: "Current working directory (basename)",
|
|
@@ -1050,13 +1166,14 @@ function colorsEnabled(cfg, env = process.env) {
|
|
|
1050
1166
|
return false;
|
|
1051
1167
|
return cfg.colors !== false;
|
|
1052
1168
|
}
|
|
1053
|
-
function paint(segment, text, ctx) {
|
|
1054
|
-
const color = typeof segment.color === "function" ? segment.color(ctx) : segment.color;
|
|
1169
|
+
function paint(segment, text, ctx, memo) {
|
|
1170
|
+
const color = typeof segment.color === "function" ? segment.color(ctx, memo) : segment.color;
|
|
1055
1171
|
const code = color ? ANSI[color] : undefined;
|
|
1056
1172
|
return code ? `${code}${text}${RESET}` : text;
|
|
1057
1173
|
}
|
|
1058
1174
|
async function renderLine(ctx, cfg) {
|
|
1059
1175
|
const colorize = colorsEnabled(cfg);
|
|
1176
|
+
const memo = new Map;
|
|
1060
1177
|
const rows = [[]];
|
|
1061
1178
|
for (const id of cfg.segments) {
|
|
1062
1179
|
if (id === "newline") {
|
|
@@ -1067,9 +1184,9 @@ async function renderLine(ctx, cfg) {
|
|
|
1067
1184
|
if (!segment)
|
|
1068
1185
|
continue;
|
|
1069
1186
|
try {
|
|
1070
|
-
const out = await segment.render(ctx);
|
|
1187
|
+
const out = await segment.render(ctx, memo);
|
|
1071
1188
|
if (out)
|
|
1072
|
-
rows[rows.length - 1].push(colorize ? paint(segment, out, ctx) : out);
|
|
1189
|
+
rows[rows.length - 1].push(colorize ? paint(segment, out, ctx, memo) : out);
|
|
1073
1190
|
} catch {}
|
|
1074
1191
|
}
|
|
1075
1192
|
return rows.map((parts) => parts.join(cfg.separator)).filter((row) => row.length > 0).join(`
|
|
@@ -1078,7 +1195,7 @@ async function renderLine(ctx, cfg) {
|
|
|
1078
1195
|
// package.json
|
|
1079
1196
|
var package_default = {
|
|
1080
1197
|
name: "@hasna/statusline",
|
|
1081
|
-
version: "0.0.
|
|
1198
|
+
version: "0.0.9",
|
|
1082
1199
|
description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
|
|
1083
1200
|
license: "Apache-2.0",
|
|
1084
1201
|
type: "module",
|