claude-doom-statusbar 0.7.0 → 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 +5 -1
- package/src/render.js +85 -9
- package/src/statusline.js +41 -13
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
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
# full — DOOM panel, everything on
|
|
2
|
+
# text_overflow = "scroll" # how labels longer than their box behave: "scroll" (ping-pong
|
|
3
|
+
# marquee, default) or "clip" (static, truncated with …).
|
|
4
|
+
# Env override: DOOMBAR_TEXT_OVERFLOW.
|
|
2
5
|
[bar]
|
|
3
6
|
border_style = "vertical"
|
|
4
7
|
border_color = "term-bg" # seamless cuts through the panel
|
|
@@ -84,7 +87,8 @@ metric = [
|
|
|
84
87
|
type = "box"
|
|
85
88
|
title = "SYS"
|
|
86
89
|
metric = [
|
|
87
|
-
{ id = "sys.cpu", render = "number",
|
|
90
|
+
{ id = "sys.cpu", render = "number", icon = "🔥" },
|
|
91
|
+
{ id = "sys.cores", render = "equalizer", icon = "🎚", color = "threshold" },
|
|
88
92
|
{ id = "sys.disk", render = "bar", icon = "💿", color = "threshold" },
|
|
89
93
|
{ id = "sys.session", render = "text", icon = "🕙" },
|
|
90
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
|
|
|
@@ -66,6 +68,28 @@ export function vlen(s) {
|
|
|
66
68
|
return n;
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
// OSC8 hyperlink helpers — long hyperlinked labels (cwd, branch) can't be column-sliced
|
|
72
|
+
// as-is (slicing corrupts the escape), so we operate on the visible text and re-wrap it
|
|
73
|
+
// with the same URL. Matches the format emitted by statusline's _link().
|
|
74
|
+
const OSC8_RE = /^\x1b\]8;;([^\x1b\x07]*)(?:\x1b\\|\x07)([\s\S]*?)\x1b\]8;;(?:\x1b\\|\x07)$/;
|
|
75
|
+
function splitLink(s) {
|
|
76
|
+
const m = String(s).match(OSC8_RE);
|
|
77
|
+
return m ? { url: m[1], inner: m[2] } : null;
|
|
78
|
+
}
|
|
79
|
+
function wrapLink(text, url) {
|
|
80
|
+
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
81
|
+
}
|
|
82
|
+
// First `width` visible columns of `text`, no padding, never splitting a 2-col glyph.
|
|
83
|
+
function headCols(text, width) {
|
|
84
|
+
let col = 0, out = "";
|
|
85
|
+
for (const ch of [...String(text)]) {
|
|
86
|
+
const cw = vlen(ch);
|
|
87
|
+
if (col + cw > width) break;
|
|
88
|
+
out += ch; col += cw;
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
function threshold(pct) {
|
|
70
94
|
return pct < 60 ? OK : pct < 85 ? WARN : CRIT;
|
|
71
95
|
}
|
|
@@ -125,6 +149,12 @@ const SPARK_BRAILLE = [
|
|
|
125
149
|
"⠀⢀⢠⢰⢸", "⡀⣀⣠⣰⣸", "⡄⣄⣤⣴⣼", "⡆⣆⣦⣶⣾", "⡇⣇⣧⣷⣿",
|
|
126
150
|
].map((r) => [...r]);
|
|
127
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
|
|
128
158
|
|
|
129
159
|
function rSpark(values, style = "block", boxRgb = TERM_RGB, vmax = null) {
|
|
130
160
|
const empty = [0, 1, 2].map((i) => Math.floor((boxRgb[i] + TERM_RGB[i]) / 2));
|
|
@@ -156,6 +186,36 @@ function rSpark(values, style = "block", boxRgb = TERM_RGB, vmax = null) {
|
|
|
156
186
|
return bg + f(SPARK) + body + bgsgrBox(boxRgb);
|
|
157
187
|
}
|
|
158
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
|
+
|
|
159
219
|
export function renderValue(entry, cells, boxRgb) {
|
|
160
220
|
const icon = entry.icon || "";
|
|
161
221
|
const label = icon ? icon + " " : "";
|
|
@@ -176,6 +236,7 @@ export function renderValue(entry, cells, boxRgb) {
|
|
|
176
236
|
}
|
|
177
237
|
if (render === "ammo") return label + rAmmo(val, color || "threshold");
|
|
178
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);
|
|
179
240
|
if (render === "list") {
|
|
180
241
|
const items = VALUES[entry.id] || [];
|
|
181
242
|
return label + f(TEXT) + items.map((x) => (Array.isArray(x) ? `${x[0]} ${x[1]}` : String(x))).join(" ");
|
|
@@ -213,7 +274,11 @@ const ESC_RE = /\x1b/;
|
|
|
213
274
|
function capLen(s, textCap) {
|
|
214
275
|
const str = String(s);
|
|
215
276
|
const w = vlen(str);
|
|
216
|
-
|
|
277
|
+
// plain text and OSC8 hyperlinks are column-sliceable (marquee/clip operate on the
|
|
278
|
+
// visible text), so cap them to textCap. Other escapes (raw SGR we can't safely slice)
|
|
279
|
+
// keep their full width as a hard floor.
|
|
280
|
+
const sliceable = !ESC_RE.test(str) || OSC8_RE.test(str);
|
|
281
|
+
return sliceable ? Math.min(w, textCap) : w;
|
|
217
282
|
}
|
|
218
283
|
|
|
219
284
|
export function metricFixedWidth(entry, textCap = TEXTCAP_MAX) {
|
|
@@ -227,6 +292,7 @@ export function metricFixedWidth(entry, textCap = TEXTCAP_MAX) {
|
|
|
227
292
|
return lw + capLen(entry.group.filter((i) => i in VALUES).map((i) => String(VALUES[i])).join(sep), textCap) + rextra;
|
|
228
293
|
}
|
|
229
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);
|
|
230
296
|
if (r === "ammo") return lw + 5 + vlen(" " + (entry.id in VALUES ? VALUES[entry.id] : 0) + "%");
|
|
231
297
|
if (r === "list") {
|
|
232
298
|
const items = VALUES[entry.id] || [];
|
|
@@ -304,11 +370,18 @@ function sliceCols(text, off, width) {
|
|
|
304
370
|
|
|
305
371
|
// Fit `text` into exactly `width` display columns. Fits -> left-aligned + padded.
|
|
306
372
|
// Overflows -> ping-pong marquee window for the current `tick`.
|
|
307
|
-
export function marquee(text, width, tick = 0) {
|
|
373
|
+
export function marquee(text, width, tick = 0, mode = "scroll") {
|
|
308
374
|
text = String(text);
|
|
309
375
|
if (width <= 0) return "";
|
|
376
|
+
const link = splitLink(text); // hyperlink: fit the visible text, re-wrap
|
|
377
|
+
if (link) return wrapLink(marquee(link.inner, width, tick, mode), link.url);
|
|
310
378
|
const tw = vlen(text);
|
|
311
379
|
if (tw <= width) return text + " ".repeat(width - tw);
|
|
380
|
+
if (mode === "clip") { // static truncation with an ellipsis
|
|
381
|
+
if (width === 1) return "…";
|
|
382
|
+
const h = headCols(text, width - 1);
|
|
383
|
+
return h + "…" + " ".repeat(Math.max(0, width - vlen(h) - 1));
|
|
384
|
+
}
|
|
312
385
|
return sliceCols(text, marqueeOffset(tw - width, tick), width);
|
|
313
386
|
}
|
|
314
387
|
|
|
@@ -428,8 +501,10 @@ export function resolvePreset(chosenCfg, target, loadByName, spriteFor) {
|
|
|
428
501
|
return last; // nothing fit -> smallest reached
|
|
429
502
|
}
|
|
430
503
|
|
|
431
|
-
export function buildBar(cfg, target, spriteFor, tick = 0) {
|
|
504
|
+
export function buildBar(cfg, target, spriteFor, tick = 0, overflow) {
|
|
432
505
|
if (!spriteFor) spriteFor = (hp) => `STFST${hp}1`;
|
|
506
|
+
// How overflowing text fits its box: "scroll" (ping-pong marquee) or "clip" (static …).
|
|
507
|
+
const ovf = overflow || cfg.text_overflow || "scroll";
|
|
433
508
|
|
|
434
509
|
const { style, headers, segs, totalRows, hp, face, faceW } = layoutContext(cfg, spriteFor);
|
|
435
510
|
const bar = cfg.bar || {};
|
|
@@ -464,10 +539,10 @@ export function buildBar(cfg, target, spriteFor, tick = 0) {
|
|
|
464
539
|
if (Array.isArray(item) && item.length === 2) {
|
|
465
540
|
const right = f(TEXT) + String(item[1]);
|
|
466
541
|
const budget = Math.max(0, w - vlen(lbl) - vlen(String(item[1])) - 1); // 1 = min gap
|
|
467
|
-
const left = lbl + f(TEXT) + marquee(String(item[0]), budget, tick);
|
|
542
|
+
const left = lbl + f(TEXT) + marquee(String(item[0]), budget, tick, ovf);
|
|
468
543
|
body = left + " ".repeat(Math.max(0, w - vlen(left) - vlen(String(item[1])))) + right;
|
|
469
544
|
} else {
|
|
470
|
-
body = lbl + f(TEXT) + marquee(String(item), Math.max(0, w - vlen(lbl)), tick);
|
|
545
|
+
body = lbl + f(TEXT) + marquee(String(item), Math.max(0, w - vlen(lbl)), tick, ovf);
|
|
471
546
|
}
|
|
472
547
|
col.push(bgsgrBox(boxRgb) + " " + body + " " + RESET);
|
|
473
548
|
}
|
|
@@ -492,7 +567,7 @@ export function buildBar(cfg, target, spriteFor, tick = 0) {
|
|
|
492
567
|
const right = f(TEXT) + String(item[1]) + (marker ? f(TEXT) + tail : "");
|
|
493
568
|
const rightW = vlen(String(item[1])) + tailW;
|
|
494
569
|
const labelMax = Math.max(0, w - vlen(lbl) - rightW - 1); // 1 = min gap
|
|
495
|
-
const left = lbl + f(TEXT) + marquee(String(item[0]), labelMax, tick);
|
|
570
|
+
const left = lbl + f(TEXT) + marquee(String(item[0]), labelMax, tick, ovf);
|
|
496
571
|
const room = Math.max(0, w - vlen(left) - rightW);
|
|
497
572
|
body = left + " ".repeat(room) + right;
|
|
498
573
|
} else { // {mark, markRgb, text} (tasks)
|
|
@@ -501,7 +576,7 @@ export function buildBar(cfg, target, spriteFor, tick = 0) {
|
|
|
501
576
|
const mPad = m + (vlen(m) < 2 ? " " : ""); // normalize mark to 2 cols so text aligns
|
|
502
577
|
const head = markCol + mPad + " " + f(TEXT);
|
|
503
578
|
const max = Math.max(0, w - vlen(mPad) - 1 - tailW); // reserve gap + marker on the right
|
|
504
|
-
body = head + marquee(String(item.text), max, tick);
|
|
579
|
+
body = head + marquee(String(item.text), max, tick, ovf);
|
|
505
580
|
body += " ".repeat(Math.max(0, w - tailW - vlen(body)));
|
|
506
581
|
if (tail) body += f(TEXT) + tail;
|
|
507
582
|
}
|
|
@@ -520,11 +595,12 @@ export function buildBar(cfg, target, spriteFor, tick = 0) {
|
|
|
520
595
|
const raw = String(VALUES[m.id]);
|
|
521
596
|
const lbl = m.icon ? m.icon + " " : "";
|
|
522
597
|
const budget = w - vlen(lbl) - vlen(rhs);
|
|
523
|
-
|
|
598
|
+
const sliceable = !ESC_RE.test(raw) || OSC8_RE.test(raw); // plain or hyperlink
|
|
599
|
+
if (sliceable && budget > 0 && vlen(raw) > budget) {
|
|
524
600
|
let col;
|
|
525
601
|
if (m.color === "threshold") col = threshold(parseInt(raw.replace(/\D/g, "") || "0", 10));
|
|
526
602
|
else col = m.color ? rgbOf(m.color) : TEXT;
|
|
527
|
-
body = lbl + f(col) + marquee(raw, budget, tick);
|
|
603
|
+
body = lbl + f(col) + marquee(raw, budget, tick, ovf);
|
|
528
604
|
}
|
|
529
605
|
}
|
|
530
606
|
body += " ".repeat(Math.max(0, w - vlen(body) - vlen(rhs))) + rhs;
|
package/src/statusline.js
CHANGED
|
@@ -145,15 +145,17 @@ export function buildValues(data, git) {
|
|
|
145
145
|
if (repo.host && repo.owner && repo.name) repoUrl = `https://${repo.host}/${repo.owner}/${repo.name}`;
|
|
146
146
|
|
|
147
147
|
const sname = data.session_name || data.session_id; // session_name only set via /rename or --name
|
|
148
|
-
|
|
148
|
+
// Clip generously, not to box width: the renderer fits each field to its box (marquee or
|
|
149
|
+
// clip per text_overflow). A tight clip here would truncate before the renderer ever sees it.
|
|
150
|
+
if (sname) v["session.name"] = clip(sname, 60);
|
|
149
151
|
|
|
150
152
|
const cwd = data.cwd || (data.workspace || {}).current_dir;
|
|
151
153
|
if (cwd) {
|
|
152
|
-
const name = clip(path.basename(cwd.replace(/[/\\]+$/, "")) || cwd,
|
|
154
|
+
const name = clip(path.basename(cwd.replace(/[/\\]+$/, "")) || cwd, 60);
|
|
153
155
|
try { v["loc.cwd"] = _link(name, pathToFileURL(cwd).href); } catch { v["loc.cwd"] = name; }
|
|
154
156
|
// git fields come from the folded snapshot the async hook wrote, not a live spawn.
|
|
155
157
|
const { br = null, lr = null, st = null } = git || {};
|
|
156
|
-
if (br) { const brLbl = clip(br,
|
|
158
|
+
if (br) { const brLbl = clip(br, 60); v["git.branch"] = repoUrl ? _link(brLbl, `${repoUrl}/tree/${br}`) : brLbl; }
|
|
157
159
|
if (lr && lr.includes("\t")) {
|
|
158
160
|
const [behind, ahead] = lr.split("\t");
|
|
159
161
|
v["git.behind"] = `↓${behind}`; v["git.ahead"] = `↑${ahead}`;
|
|
@@ -237,26 +239,50 @@ function ramPercent() {
|
|
|
237
239
|
try { return pyround((1 - os.freemem() / os.totalmem()) * 100); } catch { return null; }
|
|
238
240
|
}
|
|
239
241
|
|
|
240
|
-
|
|
241
|
-
|
|
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;
|
|
242
264
|
try {
|
|
243
|
-
|
|
244
|
-
|
|
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 };
|
|
245
272
|
const cache = path.join(TMP, "mugshot_cpu.json");
|
|
246
273
|
let prev = null;
|
|
247
274
|
try { prev = JSON.parse(readFileSync(cache, "utf8")); } catch { /* none */ }
|
|
248
|
-
try { writeFileSync(cache, JSON.stringify(
|
|
249
|
-
|
|
250
|
-
const dt = total - prev.total, di = idle - prev.idle;
|
|
251
|
-
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);
|
|
252
277
|
}
|
|
253
278
|
|
|
254
279
|
function sysValues(cwd) {
|
|
255
280
|
const v = {};
|
|
256
281
|
const ram = ramPercent();
|
|
257
282
|
if (ram !== null) v["sys.ram"] = ram;
|
|
258
|
-
const cpu =
|
|
283
|
+
const { cpu, cores } = cpuMetrics();
|
|
259
284
|
if (cpu !== null) v["sys.cpu"] = `${cpu}%`;
|
|
285
|
+
if (cores) v["sys.cores"] = cores;
|
|
260
286
|
try {
|
|
261
287
|
const stat = statfsSync(cwd || process.cwd());
|
|
262
288
|
v["sys.disk"] = pyround(((stat.blocks - stat.bfree) / stat.blocks) * 100);
|
|
@@ -471,7 +497,9 @@ function main() {
|
|
|
471
497
|
catch { return null; }
|
|
472
498
|
};
|
|
473
499
|
const selected = resolvePreset(cfg, target, loadByName, spriteFor);
|
|
474
|
-
|
|
500
|
+
// Text overflow behavior: env wins, then the preset's text_overflow, default "scroll".
|
|
501
|
+
const overflow = process.env.DOOMBAR_TEXT_OVERFLOW || selected.text_overflow || cfg.text_overflow || "scroll";
|
|
502
|
+
const res = buildBar(selected, target, spriteFor, tick, overflow);
|
|
475
503
|
process.stdout.write(res.lines.join("\n") + "\n");
|
|
476
504
|
}
|
|
477
505
|
|