claude-doom-statusbar 0.7.1 → 0.8.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 +2 -2
- package/package.json +2 -2
- package/presets/full.toml +2 -1
- package/src/render.js +40 -0
- package/src/statusline.js +33 -9
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ The HUD is a row of boxes centred on the mugshot. Each box is configurable; the
|
|
|
19
19
|
- **ACTIVITY** — a tool-activity "geiger" sparkline (duty-cycle over the last 30 s), running-agent count, task progress, error count.
|
|
20
20
|
- **AGENTS** — a live list of running subagents (type/description + ticking runtime), always visible. Long lists scroll within the box height, with ↑/↓ markers counting the rows hidden off-screen.
|
|
21
21
|
- **TASKS** — the session's todo list: settled items (✅ done, ❌ removed) on top, open items (⏩ in-progress, 🎯 pending) below. Scrolls like AGENTS, anchored on the open/settled boundary.
|
|
22
|
-
- **SYS** — CPU, disk, session length, wall clock.
|
|
22
|
+
- **SYS** — CPU, a per-core CPU equalizer (one threshold-coloured column per core), disk, session length, wall clock.
|
|
23
23
|
|
|
24
24
|
Anything the session can't supply is hidden automatically, so the same config degrades cleanly.
|
|
25
25
|
|
|
@@ -71,7 +71,7 @@ $env:FORCE_HYPERLINK = "1"; claude
|
|
|
71
71
|
- **`standard`** — balanced HUD.
|
|
72
72
|
- **`full`** — every box, the look in the screenshot above.
|
|
73
73
|
|
|
74
|
-
A preset is TOML: a `[bar]` style block, a `[mugshot]` block, and a list of `[[segment]]` boxes. Each box lists metrics with a render type — `bar`, `number`, `text`, `spark`, `ammo`, `list`, `scroll`, or a `group`. Copy one and rearrange the boxes, swap icons, or change which metrics show.
|
|
74
|
+
A preset is TOML: a `[bar]` style block, a `[mugshot]` block, and a list of `[[segment]]` boxes. Each box lists metrics with a render type — `bar`, `number`, `text`, `spark`, `equalizer`, `ammo`, `list`, `scroll`, or a `group`. (`equalizer` draws an array of `0..1` values as a one-row VU-meter: one block column per channel, each coloured by its own value — e.g. `sys.cores` for per-core CPU.) Copy one and rearrange the boxes, swap icons, or change which metrics show.
|
|
75
75
|
|
|
76
76
|
### Responsive width
|
|
77
77
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-doom-statusbar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "DOOM-inspired status bar for the Claude Code CLI — a mugshot that tracks session health, plus usage, model, project, system, and a live subagent list.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"statusline": "node src/statusline.js",
|
|
17
17
|
"hook": "node src/hook.js",
|
|
18
|
-
"test": "node test/installer.test.mjs && node test/smoke.test.mjs && node test/hook-tasks.test.mjs && node test/statusline-tasks.test.mjs && node test/render-scroll.test.mjs && node test/statusline-savings.test.mjs && node test/e2e-tasks.test.mjs && node test/marquee.test.mjs && node test/layout.test.mjs && node test/resolve-preset.test.mjs && node test/preset-bands.test.mjs && node test/journal.test.mjs && node test/git-event.test.mjs",
|
|
18
|
+
"test": "node test/installer.test.mjs && node test/smoke.test.mjs && node test/hook-tasks.test.mjs && node test/statusline-tasks.test.mjs && node test/render-scroll.test.mjs && node test/render-equalizer.test.mjs && node test/statusline-savings.test.mjs && node test/statusline-cores.test.mjs && node test/e2e-tasks.test.mjs && node test/marquee.test.mjs && node test/layout.test.mjs && node test/resolve-preset.test.mjs && node test/preset-bands.test.mjs && node test/journal.test.mjs && node test/git-event.test.mjs",
|
|
19
19
|
"preversion": "npm test",
|
|
20
20
|
"postversion": "git push --follow-tags",
|
|
21
21
|
"prepublishOnly": "npm test"
|
package/presets/full.toml
CHANGED
|
@@ -87,7 +87,8 @@ metric = [
|
|
|
87
87
|
type = "box"
|
|
88
88
|
title = "SYS"
|
|
89
89
|
metric = [
|
|
90
|
-
{ id = "sys.cpu", render = "number",
|
|
90
|
+
{ id = "sys.cpu", render = "number", icon = "🔥" },
|
|
91
|
+
{ id = "sys.cores", render = "equalizer", icon = "🎚", color = "threshold" },
|
|
91
92
|
{ id = "sys.disk", render = "bar", icon = "💿", color = "threshold" },
|
|
92
93
|
{ id = "sys.session", render = "text", icon = "🕙" },
|
|
93
94
|
{ id = "sys.clock", render = "text", icon = "🕓" },
|
package/src/render.js
CHANGED
|
@@ -47,6 +47,8 @@ export const SAMPLE = {
|
|
|
47
47
|
"act.geiger": [0, .25, .5, 1, .75, 1, .5, .6, .3, .1, .4, 1, .8, .4],
|
|
48
48
|
"act.tasks": "2/5", "act.errors": "0", "sys.ram": 47, "sys.cpu": "12%",
|
|
49
49
|
"sys.disk": 63, "sys.clock": "14:23",
|
|
50
|
+
"sys.cores": [0.12, 0.30, 0.08, 0.95, 0.45, 0.18, 0.62, 0.22],
|
|
51
|
+
|
|
50
52
|
"save.leanctx": "8.3k 63%", "save.lingua": "1.2k 1.3x",
|
|
51
53
|
};
|
|
52
54
|
|
|
@@ -147,6 +149,12 @@ const SPARK_BRAILLE = [
|
|
|
147
149
|
"⠀⢀⢠⢰⢸", "⡀⣀⣠⣰⣸", "⡄⣄⣤⣴⣼", "⡆⣆⣦⣶⣾", "⡇⣇⣧⣷⣿",
|
|
148
150
|
].map((r) => [...r]);
|
|
149
151
|
const BLOCK_RAMP = [..."▁▂▃▄▅▆▇"];
|
|
152
|
+
// The equalizer's OWN 9-level height ramp (empty 0/8 .. full 8/8), so an idle
|
|
153
|
+
// channel reads as empty and a maxed one as a full block -- deliberately wider at
|
|
154
|
+
// both ends than spark's BLOCK_RAMP, which never shows empty or full. Index via
|
|
155
|
+
// pyround(clamp(v,0,1) * 8).
|
|
156
|
+
const EQ_RAMP = [..." ▁▂▃▄▅▆▇█"];
|
|
157
|
+
const EQ_MAX = 16; // column cap: more channels than this densify by averaging
|
|
150
158
|
|
|
151
159
|
function rSpark(values, style = "block", boxRgb = TERM_RGB, vmax = null) {
|
|
152
160
|
const empty = [0, 1, 2].map((i) => Math.floor((boxRgb[i] + TERM_RGB[i]) / 2));
|
|
@@ -178,6 +186,36 @@ function rSpark(values, style = "block", boxRgb = TERM_RGB, vmax = null) {
|
|
|
178
186
|
return bg + f(SPARK) + body + bgsgrBox(boxRgb);
|
|
179
187
|
}
|
|
180
188
|
|
|
189
|
+
// Densify N channel values into exactly K = min(N, EQ_MAX) columns by averaging
|
|
190
|
+
// each column's contiguous slice. K columns by construction, so the rendered width
|
|
191
|
+
// always equals metricFixedWidth's min(N, EQ_MAX) -- the box-layout invariant.
|
|
192
|
+
function eqColumns(values) {
|
|
193
|
+
const n = values.length;
|
|
194
|
+
const k = Math.min(n, EQ_MAX);
|
|
195
|
+
const cols = [];
|
|
196
|
+
for (let i = 0; i < k; i++) {
|
|
197
|
+
const lo = Math.floor((i * n) / k), hi = Math.floor(((i + 1) * n) / k);
|
|
198
|
+
let sum = 0;
|
|
199
|
+
for (let j = lo; j < hi; j++) sum += values[j];
|
|
200
|
+
cols.push(sum / (hi - lo));
|
|
201
|
+
}
|
|
202
|
+
return cols;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// One-row VU-meter: one block column per channel (densified past EQ_MAX), each
|
|
206
|
+
// column coloured by its OWN value via threshold(). Absolute 0..1 scale (no
|
|
207
|
+
// span-normalize), 9-level EQ_RAMP. Fixed-width: takes no `cells` budget.
|
|
208
|
+
function rEqualizer(values, boxRgb) {
|
|
209
|
+
if (!values || values.length === 0) return f(SPARK);
|
|
210
|
+
const empty = [0, 1, 2].map((i) => Math.floor((boxRgb[i] + TERM_RGB[i]) / 2));
|
|
211
|
+
let body = sgrBg(empty);
|
|
212
|
+
for (const raw of eqColumns(values)) {
|
|
213
|
+
const v = Math.max(0, Math.min(1, raw));
|
|
214
|
+
body += f(threshold(v * 100)) + EQ_RAMP[pyround(v * 8)];
|
|
215
|
+
}
|
|
216
|
+
return body + bgsgrBox(boxRgb);
|
|
217
|
+
}
|
|
218
|
+
|
|
181
219
|
export function renderValue(entry, cells, boxRgb) {
|
|
182
220
|
const icon = entry.icon || "";
|
|
183
221
|
const label = icon ? icon + " " : "";
|
|
@@ -198,6 +236,7 @@ export function renderValue(entry, cells, boxRgb) {
|
|
|
198
236
|
}
|
|
199
237
|
if (render === "ammo") return label + rAmmo(val, color || "threshold");
|
|
200
238
|
if (render === "spark") return label + rSpark(val, entry.spark_style || "block", boxRgb, entry.spark_max);
|
|
239
|
+
if (render === "equalizer") return label + rEqualizer(val, boxRgb);
|
|
201
240
|
if (render === "list") {
|
|
202
241
|
const items = VALUES[entry.id] || [];
|
|
203
242
|
return label + f(TEXT) + items.map((x) => (Array.isArray(x) ? `${x[0]} ${x[1]}` : String(x))).join(" ");
|
|
@@ -253,6 +292,7 @@ export function metricFixedWidth(entry, textCap = TEXTCAP_MAX) {
|
|
|
253
292
|
return lw + capLen(entry.group.filter((i) => i in VALUES).map((i) => String(VALUES[i])).join(sep), textCap) + rextra;
|
|
254
293
|
}
|
|
255
294
|
if (r === "spark") return lw + Math.floor(((VALUES[entry.id] || []).length + 1) / 2);
|
|
295
|
+
if (r === "equalizer") return lw + Math.min((VALUES[entry.id] || []).length, EQ_MAX);
|
|
256
296
|
if (r === "ammo") return lw + 5 + vlen(" " + (entry.id in VALUES ? VALUES[entry.id] : 0) + "%");
|
|
257
297
|
if (r === "list") {
|
|
258
298
|
const items = VALUES[entry.id] || [];
|
package/src/statusline.js
CHANGED
|
@@ -239,26 +239,50 @@ function ramPercent() {
|
|
|
239
239
|
try { return pyround((1 - os.freemem() / os.totalmem()) * 100); } catch { return null; }
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
// Per-core idle fraction over a cumulative-time delta; null when no time elapsed.
|
|
243
|
+
const cpuUtil = (dt, di) => (dt > 0 ? Math.max(0, Math.min(1, 1 - di / dt)) : null);
|
|
244
|
+
|
|
245
|
+
// Pure: turn two cumulative CPU snapshots into the aggregate percent (sys.cpu, 0..100)
|
|
246
|
+
// and the per-core utilisation array (sys.cores, 0..1 each). Cold start (no prev) or a
|
|
247
|
+
// core-count mismatch (e.g. an old cache without `cores`) yields null for that field, so
|
|
248
|
+
// the metric simply doesn't render that refresh. A core with no elapsed time reads 0.
|
|
249
|
+
export function cpuDeltas(prev, cur) {
|
|
250
|
+
if (!prev) return { cpu: null, cores: null };
|
|
251
|
+
const cpu = cpuUtil(cur.total - prev.total, cur.idle - prev.idle);
|
|
252
|
+
let cores = null;
|
|
253
|
+
if (Array.isArray(prev.cores) && Array.isArray(cur.cores) && prev.cores.length === cur.cores.length) {
|
|
254
|
+
cores = cur.cores.map((c, i) => {
|
|
255
|
+
const u = cpuUtil(c.total - prev.cores[i].total, c.idle - prev.cores[i].idle);
|
|
256
|
+
return u === null ? 0 : u;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return { cpu: cpu === null ? null : pyround(cpu * 100), cores };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function cpuMetrics() {
|
|
263
|
+
let cores;
|
|
244
264
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
265
|
+
cores = os.cpus().map((c) => {
|
|
266
|
+
let total = 0; for (const k in c.times) total += c.times[k];
|
|
267
|
+
return { total, idle: c.times.idle };
|
|
268
|
+
});
|
|
269
|
+
} catch { return { cpu: null, cores: null }; }
|
|
270
|
+
const agg = cores.reduce((a, c) => ({ total: a.total + c.total, idle: a.idle + c.idle }), { total: 0, idle: 0 });
|
|
271
|
+
const cur = { total: agg.total, idle: agg.idle, cores };
|
|
247
272
|
const cache = path.join(TMP, "mugshot_cpu.json");
|
|
248
273
|
let prev = null;
|
|
249
274
|
try { prev = JSON.parse(readFileSync(cache, "utf8")); } catch { /* none */ }
|
|
250
|
-
try { writeFileSync(cache, JSON.stringify(
|
|
251
|
-
|
|
252
|
-
const dt = total - prev.total, di = idle - prev.idle;
|
|
253
|
-
return dt > 0 ? pyround(Math.max(0, Math.min(100, 100 * (1 - di / dt)))) : null;
|
|
275
|
+
try { writeFileSync(cache, JSON.stringify(cur)); } catch { /* ignore */ }
|
|
276
|
+
return cpuDeltas(prev, cur);
|
|
254
277
|
}
|
|
255
278
|
|
|
256
279
|
function sysValues(cwd) {
|
|
257
280
|
const v = {};
|
|
258
281
|
const ram = ramPercent();
|
|
259
282
|
if (ram !== null) v["sys.ram"] = ram;
|
|
260
|
-
const cpu =
|
|
283
|
+
const { cpu, cores } = cpuMetrics();
|
|
261
284
|
if (cpu !== null) v["sys.cpu"] = `${cpu}%`;
|
|
285
|
+
if (cores) v["sys.cores"] = cores;
|
|
262
286
|
try {
|
|
263
287
|
const stat = statfsSync(cwd || process.cwd());
|
|
264
288
|
v["sys.disk"] = pyround(((stat.blocks - stat.bfree) / stat.blocks) * 100);
|