From c4582e9831bdaed6dfe2cc079f1d42e926fef39b Mon Sep 17 00:00:00 2001 From: Jarrod Watts <35651410+jarrodwatts@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:13:32 +1100 Subject: [PATCH] fix(render): keep hud to one line (#105) --- README.md | 15 ++-- dist/render/index.d.ts.map | 2 +- dist/render/index.js | 94 ++++++++++++++++---- dist/render/index.js.map | 2 +- src/render/index.ts | 105 +++++++++++++++++++---- tests/fixtures/expected/render-basic.txt | 6 +- tests/render.test.js | 9 +- 7 files changed, 179 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 9dbb6b9..9feb604 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ Claude HUD gives you better insights into what's happening in your Claude Code s ## What Each Line Shows +The HUD is rendered as a single statusline; the sections below are shown inline, separated by `|`. + ### Session Info ``` [Opus | Pro] █████░░░░░ 45% | my-project git:(main) | 2 CLAUDE.md | 5h: 25% | ⏱️ 5m @@ -187,17 +189,14 @@ To disable usage display, set `display.showUsage` to `false` in your config. ### Layout Options -**Default layout** — All info on first line: +**Default layout** — Single-line HUD: ``` -[Opus] ████░░░░░░ 42% | my-project git:(main) | 2 rules | ⏱️ 5m -✓ Read ×3 | ✓ Edit ×1 +[Opus] ████░░░░░░ 42% | my-project git:(main) | 2 rules | ⏱️ 5m | ✓ Read ×3 | ✓ Edit ×1 ``` -**Separators layout** — Visual separator below header when activity exists: +**Separators layout** — Inline separator before activity: ``` -[Opus] ████░░░░░░ 42% | my-project git:(main) | 2 rules | ⏱️ 5m -────────────────────────────────────────────────────────────── -✓ Read ×3 | ✓ Edit ×1 +[Opus] ████░░░░░░ 42% | my-project git:(main) | 2 rules | ⏱️ 5m | --- | ✓ Read ×3 | ✓ Edit ×1 ``` ### Example Configuration @@ -290,4 +289,4 @@ MIT — see [LICENSE](LICENSE) ## Star History -[![Star History Chart](https://api.star-history.com/svg?repos=jarrodwatts/claude-hud&type=Date)](https://star-history.com/#jarrodwatts/claude-hud&Date) \ No newline at end of file +[![Star History Chart](https://api.star-history.com/svg?repos=jarrodwatts/claude-hud&type=Date)](https://star-history.com/#jarrodwatts/claude-hud&Date) diff --git a/dist/render/index.d.ts.map b/dist/render/index.d.ts.map index 7a1c63e..1626085 100644 --- a/dist/render/index.d.ts.map +++ b/dist/render/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/render/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA4FjD,wBAAgB,MAAM,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAuB/C"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/render/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA+IjD,wBAAgB,MAAM,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAuC/C"} \ No newline at end of file diff --git a/dist/render/index.js b/dist/render/index.js index e111959..b57a659 100644 --- a/dist/render/index.js +++ b/dist/render/index.js @@ -4,12 +4,58 @@ import { renderAgentsLine } from './agents-line.js'; import { renderTodosLine } from './todos-line.js'; import { renderIdentityLine, renderProjectLine, renderEnvironmentLine, renderUsageLine, } from './lines/index.js'; import { dim, RESET } from './colors.js'; -function visualLength(str) { +function stripAnsi(str) { // eslint-disable-next-line no-control-regex - return str.replace(/\x1b\[[0-9;]*m/g, '').length; + return str.replace(/\x1b\[[0-9;]*m/g, ''); } -function makeSeparator(length) { - return dim('─'.repeat(Math.max(length, 20))); +function visualLength(str) { + return stripAnsi(str).length; +} +function getTerminalWidth() { + const columns = process.stdout.columns; + if (typeof columns === 'number' && Number.isFinite(columns) && columns > 0) { + return columns; + } + const envColumns = Number.parseInt(process.env.COLUMNS ?? '', 10); + if (Number.isFinite(envColumns) && envColumns > 0) { + return envColumns; + } + return null; +} +function truncateLine(line, maxWidth) { + if (maxWidth <= 0) + return ''; + if (maxWidth <= 3) + return '.'.repeat(maxWidth); + if (visualLength(line) <= maxWidth) + return line; + const limit = Math.max(0, maxWidth - 3); + let visible = 0; + let result = ''; + const ansiPattern = /\x1b\[[0-9;]*m/g; + let lastIndex = 0; + let match; + while ((match = ansiPattern.exec(line)) !== null) { + const chunk = line.slice(lastIndex, match.index); + for (const char of chunk) { + if (visible >= limit) { + return result + '...'; + } + result += char; + visible += 1; + } + result += match[0]; + lastIndex = ansiPattern.lastIndex; + } + const remaining = line.slice(lastIndex); + for (const char of remaining) { + if (visible >= limit) { + return result + '...'; + } + result += char; + visible += 1; + } + return result + '...'; } function collectActivityLines(ctx) { const activityLines = []; @@ -70,19 +116,37 @@ function renderExpanded(ctx) { export function render(ctx) { const lineLayout = ctx.config?.lineLayout ?? 'expanded'; const showSeparators = ctx.config?.showSeparators ?? false; - const headerLines = lineLayout === 'expanded' - ? renderExpanded(ctx) - : renderCompact(ctx); + const headerLines = lineLayout === 'expanded' ? renderExpanded(ctx) : renderCompact(ctx); const activityLines = collectActivityLines(ctx); - const lines = [...headerLines]; - if (showSeparators && activityLines.length > 0) { - const maxWidth = Math.max(...headerLines.map(visualLength), 20); - lines.push(makeSeparator(maxWidth)); + const headerSegments = []; + for (const line of headerLines) { + if (!line) + continue; + const split = line.split('\n').filter((part) => part.length > 0); + headerSegments.push(...split); } - lines.push(...activityLines); - for (const line of lines) { - const outputLine = `${RESET}${line.replace(/ /g, '\u00A0')}`; - console.log(outputLine); + const activitySegments = []; + for (const line of activityLines) { + if (!line) + continue; + const split = line.split('\n').filter((part) => part.length > 0); + activitySegments.push(...split); } + const segments = [...headerSegments]; + if (showSeparators && headerSegments.length > 0 && activitySegments.length > 0) { + segments.push(dim('---')); + } + segments.push(...activitySegments); + if (segments.length === 0) { + return; + } + // Keep HUD to a single terminal line to avoid focusable rows in the UI. + let line = segments.join(' | '); + const maxWidth = getTerminalWidth(); + if (maxWidth) { + line = truncateLine(line, maxWidth); + } + const outputLine = `${RESET}${line}${RESET}`.replace(/ /g, '\u00A0'); + console.log(outputLine); } //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/render/index.js.map b/dist/render/index.js.map index 257be45..19cb535 100644 --- a/dist/render/index.js.map +++ b/dist/render/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/render/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/render/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/src/render/index.ts b/src/render/index.ts index 0fa4735..d768343 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -11,13 +11,64 @@ import { } from './lines/index.js'; import { dim, RESET } from './colors.js'; -function visualLength(str: string): number { +function stripAnsi(str: string): string { // eslint-disable-next-line no-control-regex - return str.replace(/\x1b\[[0-9;]*m/g, '').length; + return str.replace(/\x1b\[[0-9;]*m/g, ''); } -function makeSeparator(length: number): string { - return dim('─'.repeat(Math.max(length, 20))); +function visualLength(str: string): number { + return stripAnsi(str).length; +} + +function getTerminalWidth(): number | null { + const columns = process.stdout.columns; + if (typeof columns === 'number' && Number.isFinite(columns) && columns > 0) { + return columns; + } + + const envColumns = Number.parseInt(process.env.COLUMNS ?? '', 10); + if (Number.isFinite(envColumns) && envColumns > 0) { + return envColumns; + } + + return null; +} + +function truncateLine(line: string, maxWidth: number): string { + if (maxWidth <= 0) return ''; + if (maxWidth <= 3) return '.'.repeat(maxWidth); + if (visualLength(line) <= maxWidth) return line; + + const limit = Math.max(0, maxWidth - 3); + let visible = 0; + let result = ''; + const ansiPattern = /\x1b\[[0-9;]*m/g; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = ansiPattern.exec(line)) !== null) { + const chunk = line.slice(lastIndex, match.index); + for (const char of chunk) { + if (visible >= limit) { + return result + '...'; + } + result += char; + visible += 1; + } + result += match[0]; + lastIndex = ansiPattern.lastIndex; + } + + const remaining = line.slice(lastIndex); + for (const char of remaining) { + if (visible >= limit) { + return result + '...'; + } + result += char; + visible += 1; + } + + return result + '...'; } function collectActivityLines(ctx: RenderContext): string[] { @@ -93,24 +144,40 @@ function renderExpanded(ctx: RenderContext): string[] { export function render(ctx: RenderContext): void { const lineLayout = ctx.config?.lineLayout ?? 'expanded'; const showSeparators = ctx.config?.showSeparators ?? false; - - const headerLines = lineLayout === 'expanded' - ? renderExpanded(ctx) - : renderCompact(ctx); - + const headerLines = lineLayout === 'expanded' ? renderExpanded(ctx) : renderCompact(ctx); const activityLines = collectActivityLines(ctx); - const lines: string[] = [...headerLines]; - - if (showSeparators && activityLines.length > 0) { - const maxWidth = Math.max(...headerLines.map(visualLength), 20); - lines.push(makeSeparator(maxWidth)); + const headerSegments: string[] = []; + for (const line of headerLines) { + if (!line) continue; + const split = line.split('\n').filter((part) => part.length > 0); + headerSegments.push(...split); } - lines.push(...activityLines); - - for (const line of lines) { - const outputLine = `${RESET}${line.replace(/ /g, '\u00A0')}`; - console.log(outputLine); + const activitySegments: string[] = []; + for (const line of activityLines) { + if (!line) continue; + const split = line.split('\n').filter((part) => part.length > 0); + activitySegments.push(...split); } + + const segments: string[] = [...headerSegments]; + if (showSeparators && headerSegments.length > 0 && activitySegments.length > 0) { + segments.push(dim('---')); + } + segments.push(...activitySegments); + + if (segments.length === 0) { + return; + } + + // Keep HUD to a single terminal line to avoid focusable rows in the UI. + let line = segments.join(' | '); + const maxWidth = getTerminalWidth(); + if (maxWidth) { + line = truncateLine(line, maxWidth); + } + + const outputLine = `${RESET}${line}${RESET}`.replace(/ /g, '\u00A0'); + console.log(outputLine); } diff --git a/tests/fixtures/expected/render-basic.txt b/tests/fixtures/expected/render-basic.txt index 4295a33..bc088af 100644 --- a/tests/fixtures/expected/render-basic.txt +++ b/tests/fixtures/expected/render-basic.txt @@ -1,5 +1 @@ -[Opus] █████░░░░░ 45% -my-project -◐ Edit: .../authentication.ts | ✓ Read ×1 -✓ explore [haiku]: Finding auth code (<1s) -▸ Add tests (1/2) +[Opus] █████░░░░░ 45% | my-project | ◐ Edit: .../authentication.ts | ✓ Read ×1 | ✓ explore [haiku]: Finding auth code (<1s) | ▸ Add tests (1/2) diff --git a/tests/render.test.js b/tests/render.test.js index 91dc1d6..915a34a 100644 --- a/tests/render.test.js +++ b/tests/render.test.js @@ -536,8 +536,8 @@ test('render adds separator line when showSeparators is true and activity exists console.log = originalLog; } - assert.ok(logs.length >= 2, 'should have at least 2 lines'); - assert.ok(logs.some(l => l.includes('─')), 'should include separator character'); + assert.equal(logs.length, 1, 'should render a single line'); + assert.ok(logs.some(l => l.includes('---')), 'should include separator marker'); }); test('render omits separator when showSeparators is true but no activity', () => { @@ -553,8 +553,8 @@ test('render omits separator when showSeparators is true but no activity', () => console.log = originalLog; } - assert.equal(logs.length, 1, 'should only have session line'); - assert.ok(!logs.some(l => l.includes('─')), 'should not include separator'); + assert.equal(logs.length, 1, 'should render a single line'); + assert.ok(!logs.some(l => l.includes('---')), 'should not include separator'); }); // fileStats tests @@ -634,4 +634,3 @@ test('renderSessionLine combines showFileStats with showDirty and showAheadBehin assert.ok(line.includes('!3'), 'expected modified count'); assert.ok(line.includes('✘1'), 'expected deleted count'); }); -