claude-doom-statusbar 0.3.0 → 0.4.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 +8 -32
- package/assets/images/hud.png +0 -0
- package/package.json +2 -2
- package/presets/default.toml +8 -0
- package/presets/full.toml +8 -0
- package/src/render.js +1 -0
- package/src/statusline.js +61 -7
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# claude-doom-statusbar
|
|
2
2
|
|
|
3
|
-
A DOOM-inspired status bar for the [Claude Code](https://docs.claude.com/en/docs/claude-code) CLI. Your session, read off the Doomguy HUD: a mugshot whose face tracks your health, boxes for usage, model, project and system, and
|
|
3
|
+
A DOOM-inspired status bar for the [Claude Code](https://docs.claude.com/en/docs/claude-code) CLI. Your session, read off the Doomguy HUD: a mugshot whose face tracks your health, boxes for usage, model, project and system, and live lists of running agents and tasks.
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<img src="assets/images/hud.png" alt="claude-doom-statusbar HUD: MODEL, USAGE, PROJECT, the DOOM mugshot, ACTIVITY,
|
|
6
|
+
<img src="assets/images/hud.png" alt="claude-doom-statusbar HUD: MODEL, USAGE, PROJECT, the DOOM mugshot, ACTIVITY, AGENTS, TASKS and SYS boxes">
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
The mugshot is the real DOOM (1993) status-face sprite, rasterised into the terminal at runtime — not ASCII art of it.
|
|
@@ -15,9 +15,10 @@ The HUD is a row of boxes centred on the mugshot. Each box is configurable; the
|
|
|
15
15
|
- **mugshot** — the Doomguy face. Its HP (how bloodied it looks) follows your *usage headroom* — `min(5h, 7d) rate-limit room`, context as a fallback. It glances around when idle, winces on errors, snarls on writes, grins on a clean finish, dies when you're tapped out, and flashes invulnerable just after an advisor consult.
|
|
16
16
|
- **MODEL** — model name + reasoning effort (a waxing-moon→sun icon), thinking/fast toggles, output style, and the configured `/advisor` model.
|
|
17
17
|
- **USAGE** — context window (HP bar), the 5h / 7d rate-limit bars (with reset countdowns), RAM, session cost.
|
|
18
|
-
- **PROJECT** — cwd, git branch, ahead/behind,
|
|
19
|
-
- **ACTIVITY** — a tool-activity "geiger" sparkline (duty-cycle over the last 30 s), running-
|
|
20
|
-
- **
|
|
18
|
+
- **PROJECT** — session name, cwd, git branch, a merged work line (changed files + ahead/behind), lines added/removed, PR state. The cwd, branch and PR are **clickable** (OSC 8 hyperlinks): Ctrl/Cmd-click to open the folder, the branch on the host, or the pull request. Long names are clipped to 24 chars so the box can't blow up.
|
|
19
|
+
- **ACTIVITY** — a tool-activity "geiger" sparkline (duty-cycle over the last 30 s), running-agent count, task progress, error count.
|
|
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
|
+
- **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.
|
|
21
22
|
- **SYS** — CPU, disk, session length, wall clock.
|
|
22
23
|
|
|
23
24
|
Anything the session can't supply is hidden automatically, so the same config degrades cleanly.
|
|
@@ -54,32 +55,6 @@ npm i -g claude-doom-statusbar@latest # global install
|
|
|
54
55
|
npx claude-doom-statusbar@latest install
|
|
55
56
|
```
|
|
56
57
|
|
|
57
|
-
### Manual wiring
|
|
58
|
-
|
|
59
|
-
If you'd rather edit `~/.claude/settings.json` by hand, point it at the package's `src/` (use the absolute install path; on Windows use forward slashes):
|
|
60
|
-
|
|
61
|
-
```json
|
|
62
|
-
{
|
|
63
|
-
"env": { "DOOMBAR_PRESET": "/abs/path/claude-doom-statusbar/presets/full.toml", "FORCE_HYPERLINK": "1" },
|
|
64
|
-
"statusLine": {
|
|
65
|
-
"type": "command",
|
|
66
|
-
"command": "node \"/abs/path/claude-doom-statusbar/src/statusline.js\"",
|
|
67
|
-
"refreshInterval": 1
|
|
68
|
-
},
|
|
69
|
-
"hooks": {
|
|
70
|
-
"PreToolUse": [{ "hooks": [{ "type": "command", "command": "node \"/abs/path/claude-doom-statusbar/src/hook.js\"" }] }],
|
|
71
|
-
"PostToolUse": [{ "hooks": [{ "type": "command", "command": "node \"/abs/path/claude-doom-statusbar/src/hook.js\"" }] }],
|
|
72
|
-
"PostToolUseFailure": [{ "hooks": [{ "type": "command", "command": "node \"/abs/path/claude-doom-statusbar/src/hook.js\"" }] }],
|
|
73
|
-
"Stop": [{ "hooks": [{ "type": "command", "command": "node \"/abs/path/claude-doom-statusbar/src/hook.js\"" }] }],
|
|
74
|
-
"PermissionDenied": [{ "hooks": [{ "type": "command", "command": "node \"/abs/path/claude-doom-statusbar/src/hook.js\"" }] }],
|
|
75
|
-
"SubagentStart": [{ "hooks": [{ "type": "command", "command": "node \"/abs/path/claude-doom-statusbar/src/hook.js\"" }] }],
|
|
76
|
-
"SubagentStop": [{ "hooks": [{ "type": "command", "command": "node \"/abs/path/claude-doom-statusbar/src/hook.js\"" }] }],
|
|
77
|
-
"TaskCreated": [{ "hooks": [{ "type": "command", "command": "node \"/abs/path/claude-doom-statusbar/src/hook.js\"" }] }],
|
|
78
|
-
"TaskCompleted": [{ "hooks": [{ "type": "command", "command": "node \"/abs/path/claude-doom-statusbar/src/hook.js\"" }] }]
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
58
|
### Clickable links
|
|
84
59
|
|
|
85
60
|
The cwd / branch / PR are emitted as OSC 8 hyperlinks. They render in any terminal but only click in ones Claude Code detects as hyperlink-capable (iTerm2, kitty, WezTerm, …). **Windows Terminal isn't auto-detected** — launch with `FORCE_HYPERLINK=1` to enable them:
|
|
@@ -96,7 +71,7 @@ $env:FORCE_HYPERLINK = "1"; claude
|
|
|
96
71
|
- **`default`** — balanced HUD.
|
|
97
72
|
- **`full`** — every box, the look in the screenshot above.
|
|
98
73
|
|
|
99
|
-
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`, 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`, `ammo`, `list`, `scroll`, or a `group`. Copy one and rearrange the boxes, swap icons, or change which metrics show.
|
|
100
75
|
|
|
101
76
|
## How it works
|
|
102
77
|
|
|
@@ -110,6 +85,7 @@ See [`docs/ideation/`](docs/ideation/) for the full design write-up.
|
|
|
110
85
|
|
|
111
86
|
- The status-face sprites are from **DOOM** (1993), id Software.
|
|
112
87
|
- Mugshot rasterisation by **[chafa](https://hpjansson.org/chafa/)** (Hans Petter Jansson).
|
|
88
|
+
- Prior art that shaped what this HUD shows: **[claude-hud](https://github.com/jarrodwatts/claude-hud)** and **[ccstatusline](https://github.com/sirmalloc/ccstatusline)**.
|
|
113
89
|
|
|
114
90
|
## License
|
|
115
91
|
|
package/assets/images/hud.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-doom-statusbar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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/e2e-tasks.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/statusline-savings.test.mjs && node test/e2e-tasks.test.mjs",
|
|
19
19
|
"preversion": "npm test",
|
|
20
20
|
"postversion": "git push --follow-tags",
|
|
21
21
|
"prepublishOnly": "npm test"
|
package/presets/default.toml
CHANGED
|
@@ -17,6 +17,14 @@ metric = [
|
|
|
17
17
|
{ id = "ratelimit.7d", render = "bar", icon = "📅", color = "threshold" },
|
|
18
18
|
]
|
|
19
19
|
|
|
20
|
+
[[segment]]
|
|
21
|
+
type = "box"
|
|
22
|
+
title = "SAVE"
|
|
23
|
+
metric = [
|
|
24
|
+
{ id = "save.leanctx", render = "text", icon = "🪶" },
|
|
25
|
+
{ id = "save.lingua", render = "text", icon = "📜" },
|
|
26
|
+
]
|
|
27
|
+
|
|
20
28
|
[[segment]]
|
|
21
29
|
type = "mugshot"
|
|
22
30
|
|
package/presets/full.toml
CHANGED
|
@@ -30,6 +30,14 @@ metric = [
|
|
|
30
30
|
{ id = "cost.total", render = "number", icon = "💰" },
|
|
31
31
|
]
|
|
32
32
|
|
|
33
|
+
[[segment]]
|
|
34
|
+
type = "box"
|
|
35
|
+
title = "SAVE"
|
|
36
|
+
metric = [
|
|
37
|
+
{ id = "save.leanctx", render = "text", icon = "🪶" },
|
|
38
|
+
{ id = "save.lingua", render = "text", icon = "📜" },
|
|
39
|
+
]
|
|
40
|
+
|
|
33
41
|
[[segment]]
|
|
34
42
|
type = "box"
|
|
35
43
|
title = "PROJECT"
|
package/src/render.js
CHANGED
|
@@ -47,6 +47,7 @@ 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
|
+
"save.leanctx": "8.3k 63%", "save.lingua": "1.2k 1.3x",
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
let VALUES = SAMPLE;
|
package/src/statusline.js
CHANGED
|
@@ -45,6 +45,10 @@ function git(cwd, ...args) {
|
|
|
45
45
|
// so an oversized repo or branch name can't blow up the PROJECT box width.
|
|
46
46
|
const clip = (s, n) => ([...String(s)].length > n ? [...String(s)].slice(0, n - 1).join("") + "…" : String(s));
|
|
47
47
|
|
|
48
|
+
// Human-readable token count: 8263 -> "8.3k", 1200000 -> "1.2M", 512 -> "512".
|
|
49
|
+
// Lowercase k/M — the savings rows read softer than model.window's uppercase K/M.
|
|
50
|
+
const k = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : `${n}`);
|
|
51
|
+
|
|
48
52
|
function _dur(secsF) {
|
|
49
53
|
let secs = Math.max(0, Math.trunc(secsF));
|
|
50
54
|
const d = Math.floor(secs / 86400); secs %= 86400;
|
|
@@ -228,6 +232,60 @@ function sysValues(cwd) {
|
|
|
228
232
|
return v;
|
|
229
233
|
}
|
|
230
234
|
|
|
235
|
+
// Token-savings rows read from the small JSON files context-optimization tools already
|
|
236
|
+
// persist. No plugin patching, no binary spawn — just a cheap read each refresh. Paths are
|
|
237
|
+
// env-overridable (DOOMBAR_*) so tests can point at fixtures, mirroring statePath/MUGSHOT_STATE.
|
|
238
|
+
const leanCtxPath = () => process.env.DOOMBAR_LEANCTX || path.join(os.homedir(), ".lean-ctx", "mcp-live.json");
|
|
239
|
+
const llmlinguaPath = () => process.env.DOOMBAR_LLMLINGUA || path.join(os.homedir(), ".llmlingua-stats.json");
|
|
240
|
+
|
|
241
|
+
// Defensive read: missing file or malformed JSON -> null (the row simply never appears).
|
|
242
|
+
function readJson(p) {
|
|
243
|
+
try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// One entry per savings source; extract returns the display string or null (omit the row).
|
|
247
|
+
// Adding a source later is one entry here plus one preset line — not an adapter framework.
|
|
248
|
+
const SAVINGS_SOURCES = [
|
|
249
|
+
{
|
|
250
|
+
key: "save.leanctx",
|
|
251
|
+
path: leanCtxPath,
|
|
252
|
+
extract: (d) => {
|
|
253
|
+
if (!(d.tokens_saved > 0)) return null;
|
|
254
|
+
// compression_rate is a 0-100 percentage (verified against historical data).
|
|
255
|
+
// Round it — a fractional value would render "63.45%" and shift the box width.
|
|
256
|
+
return typeof d.compression_rate === "number"
|
|
257
|
+
? `${k(d.tokens_saved)} ${Math.round(d.compression_rate)}%`
|
|
258
|
+
: k(d.tokens_saved);
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
key: "save.lingua",
|
|
263
|
+
path: llmlinguaPath,
|
|
264
|
+
extract: (d) => {
|
|
265
|
+
// Prefer the nested session schema (smart-read). The flat lifetime-only shape
|
|
266
|
+
// (llmlingua_logged.py: tokens_saved_total, no session) is absent for the session view.
|
|
267
|
+
const s = d.session;
|
|
268
|
+
if (!s || !(s.tokens_saved > 0)) return null;
|
|
269
|
+
// Round/clamp the secondary figure so a many-decimal value can't shift box width.
|
|
270
|
+
if (s.last_saved_pct != null) return `${k(s.tokens_saved)} ${Math.round(s.last_saved_pct)}%`;
|
|
271
|
+
// No original-token count in the session block, so a percent isn't derivable; show the ratio.
|
|
272
|
+
if (s.last_ratio != null) return `${k(s.tokens_saved)} ${Number(s.last_ratio).toFixed(1)}x`;
|
|
273
|
+
return k(s.tokens_saved);
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
export function statsValues() {
|
|
279
|
+
const v = {};
|
|
280
|
+
for (const src of SAVINGS_SOURCES) {
|
|
281
|
+
const data = readJson(src.path());
|
|
282
|
+
if (!data) continue;
|
|
283
|
+
const out = src.extract(data);
|
|
284
|
+
if (out) v[src.key] = out; // omit on null/zero -> render.js available() drops the row
|
|
285
|
+
}
|
|
286
|
+
return v;
|
|
287
|
+
}
|
|
288
|
+
|
|
231
289
|
const PERM = { plan: "📋 plan", auto: "⏩ auto", acceptEdits: "⏩ auto", bypassPermissions: "⏩ bypass" };
|
|
232
290
|
const OK_RGB = [96, 200, 104]; // matches render.js OK (done, green)
|
|
233
291
|
const CRIT_RGB = [224, 84, 64]; // matches render.js CRIT (deleted, red)
|
|
@@ -259,11 +317,7 @@ export function activityValues(st, now) {
|
|
|
259
317
|
const squad = st.squad || {};
|
|
260
318
|
v["act.agents"] = String(Object.keys(squad).length);
|
|
261
319
|
const agents = Object.values(squad).sort((a, b) => a.start - b.start);
|
|
262
|
-
v["act.subagents"] = agents.map((a) =>
|
|
263
|
-
let label = a.desc || a.type || "agent";
|
|
264
|
-
if ([...label].length > 20) label = [...label].slice(0, 19).join("") + "…";
|
|
265
|
-
return [label, _dur(now - a.start)];
|
|
266
|
-
});
|
|
320
|
+
v["act.subagents"] = agents.map((a) => [clip(a.desc || a.type || "agent", 24), _dur(now - a.start)]);
|
|
267
321
|
|
|
268
322
|
const tasks = st.tasks && typeof st.tasks === "object" ? Object.values(st.tasks) : [];
|
|
269
323
|
const live = tasks.filter((t) => t.status !== "deleted");
|
|
@@ -274,7 +328,7 @@ export function activityValues(st, now) {
|
|
|
274
328
|
.sort((a, b) => (TASK_ORDER[a.status] - TASK_ORDER[b.status]) || (a.ts - b.ts));
|
|
275
329
|
v["act.tasklist"] = ordered.map((t) => {
|
|
276
330
|
const [mark, markRgb] = TASK_MARK[t.status] || ["🎯", null];
|
|
277
|
-
return { mark, markRgb, text: t.title };
|
|
331
|
+
return { mark, markRgb, text: clip(t.title, 24) };
|
|
278
332
|
});
|
|
279
333
|
|
|
280
334
|
if ("errors" in st) v["act.errors"] = String(st.errors);
|
|
@@ -291,7 +345,7 @@ function main() {
|
|
|
291
345
|
const now = Date.now() / 1000;
|
|
292
346
|
const st = readState(data);
|
|
293
347
|
const cwd = data.cwd || (data.workspace || {}).current_dir;
|
|
294
|
-
const values = { ...buildValues(data), ...activityValues(st, now), ...sysValues(cwd) };
|
|
348
|
+
const values = { ...buildValues(data), ...activityValues(st, now), ...sysValues(cwd), ...statsValues() };
|
|
295
349
|
const [advModel, advTs] = advisorInfo(data.transcript_path || "");
|
|
296
350
|
if (advModel) values["advisor.model"] = advModel;
|
|
297
351
|
const god_until = godFlash(data, advTs, now);
|