claude-doom-statusbar 0.7.0 → 0.7.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/package.json +1 -1
- package/presets/full.toml +3 -0
- package/src/render.js +45 -9
- package/src/statusline.js +8 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-doom-statusbar",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
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": {
|
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
|
package/src/render.js
CHANGED
|
@@ -66,6 +66,28 @@ export function vlen(s) {
|
|
|
66
66
|
return n;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// OSC8 hyperlink helpers — long hyperlinked labels (cwd, branch) can't be column-sliced
|
|
70
|
+
// as-is (slicing corrupts the escape), so we operate on the visible text and re-wrap it
|
|
71
|
+
// with the same URL. Matches the format emitted by statusline's _link().
|
|
72
|
+
const OSC8_RE = /^\x1b\]8;;([^\x1b\x07]*)(?:\x1b\\|\x07)([\s\S]*?)\x1b\]8;;(?:\x1b\\|\x07)$/;
|
|
73
|
+
function splitLink(s) {
|
|
74
|
+
const m = String(s).match(OSC8_RE);
|
|
75
|
+
return m ? { url: m[1], inner: m[2] } : null;
|
|
76
|
+
}
|
|
77
|
+
function wrapLink(text, url) {
|
|
78
|
+
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
79
|
+
}
|
|
80
|
+
// First `width` visible columns of `text`, no padding, never splitting a 2-col glyph.
|
|
81
|
+
function headCols(text, width) {
|
|
82
|
+
let col = 0, out = "";
|
|
83
|
+
for (const ch of [...String(text)]) {
|
|
84
|
+
const cw = vlen(ch);
|
|
85
|
+
if (col + cw > width) break;
|
|
86
|
+
out += ch; col += cw;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
69
91
|
function threshold(pct) {
|
|
70
92
|
return pct < 60 ? OK : pct < 85 ? WARN : CRIT;
|
|
71
93
|
}
|
|
@@ -213,7 +235,11 @@ const ESC_RE = /\x1b/;
|
|
|
213
235
|
function capLen(s, textCap) {
|
|
214
236
|
const str = String(s);
|
|
215
237
|
const w = vlen(str);
|
|
216
|
-
|
|
238
|
+
// plain text and OSC8 hyperlinks are column-sliceable (marquee/clip operate on the
|
|
239
|
+
// visible text), so cap them to textCap. Other escapes (raw SGR we can't safely slice)
|
|
240
|
+
// keep their full width as a hard floor.
|
|
241
|
+
const sliceable = !ESC_RE.test(str) || OSC8_RE.test(str);
|
|
242
|
+
return sliceable ? Math.min(w, textCap) : w;
|
|
217
243
|
}
|
|
218
244
|
|
|
219
245
|
export function metricFixedWidth(entry, textCap = TEXTCAP_MAX) {
|
|
@@ -304,11 +330,18 @@ function sliceCols(text, off, width) {
|
|
|
304
330
|
|
|
305
331
|
// Fit `text` into exactly `width` display columns. Fits -> left-aligned + padded.
|
|
306
332
|
// Overflows -> ping-pong marquee window for the current `tick`.
|
|
307
|
-
export function marquee(text, width, tick = 0) {
|
|
333
|
+
export function marquee(text, width, tick = 0, mode = "scroll") {
|
|
308
334
|
text = String(text);
|
|
309
335
|
if (width <= 0) return "";
|
|
336
|
+
const link = splitLink(text); // hyperlink: fit the visible text, re-wrap
|
|
337
|
+
if (link) return wrapLink(marquee(link.inner, width, tick, mode), link.url);
|
|
310
338
|
const tw = vlen(text);
|
|
311
339
|
if (tw <= width) return text + " ".repeat(width - tw);
|
|
340
|
+
if (mode === "clip") { // static truncation with an ellipsis
|
|
341
|
+
if (width === 1) return "…";
|
|
342
|
+
const h = headCols(text, width - 1);
|
|
343
|
+
return h + "…" + " ".repeat(Math.max(0, width - vlen(h) - 1));
|
|
344
|
+
}
|
|
312
345
|
return sliceCols(text, marqueeOffset(tw - width, tick), width);
|
|
313
346
|
}
|
|
314
347
|
|
|
@@ -428,8 +461,10 @@ export function resolvePreset(chosenCfg, target, loadByName, spriteFor) {
|
|
|
428
461
|
return last; // nothing fit -> smallest reached
|
|
429
462
|
}
|
|
430
463
|
|
|
431
|
-
export function buildBar(cfg, target, spriteFor, tick = 0) {
|
|
464
|
+
export function buildBar(cfg, target, spriteFor, tick = 0, overflow) {
|
|
432
465
|
if (!spriteFor) spriteFor = (hp) => `STFST${hp}1`;
|
|
466
|
+
// How overflowing text fits its box: "scroll" (ping-pong marquee) or "clip" (static …).
|
|
467
|
+
const ovf = overflow || cfg.text_overflow || "scroll";
|
|
433
468
|
|
|
434
469
|
const { style, headers, segs, totalRows, hp, face, faceW } = layoutContext(cfg, spriteFor);
|
|
435
470
|
const bar = cfg.bar || {};
|
|
@@ -464,10 +499,10 @@ export function buildBar(cfg, target, spriteFor, tick = 0) {
|
|
|
464
499
|
if (Array.isArray(item) && item.length === 2) {
|
|
465
500
|
const right = f(TEXT) + String(item[1]);
|
|
466
501
|
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);
|
|
502
|
+
const left = lbl + f(TEXT) + marquee(String(item[0]), budget, tick, ovf);
|
|
468
503
|
body = left + " ".repeat(Math.max(0, w - vlen(left) - vlen(String(item[1])))) + right;
|
|
469
504
|
} else {
|
|
470
|
-
body = lbl + f(TEXT) + marquee(String(item), Math.max(0, w - vlen(lbl)), tick);
|
|
505
|
+
body = lbl + f(TEXT) + marquee(String(item), Math.max(0, w - vlen(lbl)), tick, ovf);
|
|
471
506
|
}
|
|
472
507
|
col.push(bgsgrBox(boxRgb) + " " + body + " " + RESET);
|
|
473
508
|
}
|
|
@@ -492,7 +527,7 @@ export function buildBar(cfg, target, spriteFor, tick = 0) {
|
|
|
492
527
|
const right = f(TEXT) + String(item[1]) + (marker ? f(TEXT) + tail : "");
|
|
493
528
|
const rightW = vlen(String(item[1])) + tailW;
|
|
494
529
|
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);
|
|
530
|
+
const left = lbl + f(TEXT) + marquee(String(item[0]), labelMax, tick, ovf);
|
|
496
531
|
const room = Math.max(0, w - vlen(left) - rightW);
|
|
497
532
|
body = left + " ".repeat(room) + right;
|
|
498
533
|
} else { // {mark, markRgb, text} (tasks)
|
|
@@ -501,7 +536,7 @@ export function buildBar(cfg, target, spriteFor, tick = 0) {
|
|
|
501
536
|
const mPad = m + (vlen(m) < 2 ? " " : ""); // normalize mark to 2 cols so text aligns
|
|
502
537
|
const head = markCol + mPad + " " + f(TEXT);
|
|
503
538
|
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);
|
|
539
|
+
body = head + marquee(String(item.text), max, tick, ovf);
|
|
505
540
|
body += " ".repeat(Math.max(0, w - tailW - vlen(body)));
|
|
506
541
|
if (tail) body += f(TEXT) + tail;
|
|
507
542
|
}
|
|
@@ -520,11 +555,12 @@ export function buildBar(cfg, target, spriteFor, tick = 0) {
|
|
|
520
555
|
const raw = String(VALUES[m.id]);
|
|
521
556
|
const lbl = m.icon ? m.icon + " " : "";
|
|
522
557
|
const budget = w - vlen(lbl) - vlen(rhs);
|
|
523
|
-
|
|
558
|
+
const sliceable = !ESC_RE.test(raw) || OSC8_RE.test(raw); // plain or hyperlink
|
|
559
|
+
if (sliceable && budget > 0 && vlen(raw) > budget) {
|
|
524
560
|
let col;
|
|
525
561
|
if (m.color === "threshold") col = threshold(parseInt(raw.replace(/\D/g, "") || "0", 10));
|
|
526
562
|
else col = m.color ? rgbOf(m.color) : TEXT;
|
|
527
|
-
body = lbl + f(col) + marquee(raw, budget, tick);
|
|
563
|
+
body = lbl + f(col) + marquee(raw, budget, tick, ovf);
|
|
528
564
|
}
|
|
529
565
|
}
|
|
530
566
|
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}`;
|
|
@@ -471,7 +473,9 @@ function main() {
|
|
|
471
473
|
catch { return null; }
|
|
472
474
|
};
|
|
473
475
|
const selected = resolvePreset(cfg, target, loadByName, spriteFor);
|
|
474
|
-
|
|
476
|
+
// Text overflow behavior: env wins, then the preset's text_overflow, default "scroll".
|
|
477
|
+
const overflow = process.env.DOOMBAR_TEXT_OVERFLOW || selected.text_overflow || cfg.text_overflow || "scroll";
|
|
478
|
+
const res = buildBar(selected, target, spriteFor, tick, overflow);
|
|
475
479
|
process.stdout.write(res.lines.join("\n") + "\n");
|
|
476
480
|
}
|
|
477
481
|
|