diff --git a/packages/nuxt-cli/src/dev/tui/overlay.ts b/packages/nuxt-cli/src/dev/tui/overlay.ts index 2433f67b0..98a97fc4a 100644 --- a/packages/nuxt-cli/src/dev/tui/overlay.ts +++ b/packages/nuxt-cli/src/dev/tui/overlay.ts @@ -5,7 +5,7 @@ import type { OverlayEntry } from './screen' import { styleText } from 'node:util' -import { truncate } from '../../utils/formatting' +import { truncate, visibleWidth } from '../../utils/formatting' import { MUTED, paint } from '../../utils/terminal-theme' import { formatHints, ScreenOverlay } from './screen' @@ -154,16 +154,21 @@ export function formatEvent(event: DevLogEvent, columns: number, sameRequest = f const date = new Date(event.time) const clock = [date.getHours(), date.getMinutes(), date.getSeconds()].map(part => String(part).padStart(2, '0')).join(':') const time = styleText(MUTED, clock) - const tag = event.tag ? styleText(MUTED, `[${event.tag}] `) : '' + const budget = columns - messageColumn - 1 const indent = ' '.repeat(messageColumn) const marker = event.repeats && event.repeats > 1 ? styleText(MUTED, ` ×${event.repeats}`) : '' const markerWidth = event.repeats && event.repeats > 1 ? ` ×${event.repeats}`.length : 0 + const firstLineWidth = visibleWidth(event.message.split('\n', 1)[0] ?? '') + const tagRoom = Math.max(Math.floor(budget / 2), budget - markerWidth - firstLineWidth - 1) + const tag = event.tag && !event.request ? styleText(MUTED, `[${truncate(event.tag, Math.max(1, tagRoom - 2))}] `) : '' + const tagWidth = visibleWidth(tag) + const lines = event.message .split('\n') .map((line, index) => { - const room = Math.max(20, columns - messageColumn - 1 - (index === 0 ? markerWidth : 0)) + const room = Math.max(1, budget - (index === 0 ? markerWidth + tagWidth : 0)) const text = colorBySeverity(truncate(line, room), event) return index === 0 ? `${text}${marker}` : text }) diff --git a/packages/nuxt-cli/src/dev/tui/screen.ts b/packages/nuxt-cli/src/dev/tui/screen.ts index aaf0c8ba8..0eeb2c6d3 100644 --- a/packages/nuxt-cli/src/dev/tui/screen.ts +++ b/packages/nuxt-cli/src/dev/tui/screen.ts @@ -310,12 +310,12 @@ export abstract class ScreenOverlay { const visible = rows.slice(this.#top, this.#top + bodyRows) const frame = [ - truncate(this.renderTitle(columns), columns), + this.renderTitle(columns), styleText(MUTED, '─'.repeat(Math.max(0, columns))), ...visible, ...Array.from({ length: bodyRows - visible.length }).fill('') as string[], this.#hintLine(), - ] + ].map(row => truncate(row, columns)) this.#write(`\u001B[H\u001B[2J${frame.join('\n')}`) } diff --git a/packages/nuxt-cli/test/unit/dev-tui.spec.ts b/packages/nuxt-cli/test/unit/dev-tui.spec.ts index fd5738b1a..eeb0426b7 100644 --- a/packages/nuxt-cli/test/unit/dev-tui.spec.ts +++ b/packages/nuxt-cli/test/unit/dev-tui.spec.ts @@ -905,6 +905,49 @@ describe('log overlay', () => { expect(lines.join('\n')).toContain(coloured) }) + it('counts the tag against the width a message is cut to', () => { + const [line] = formatEvent(event({ tag: '@nuxt/content', message: 'Processed 2 collections and 0 files in 59.89ms (0 cached, 0 parsed)' }), 60) + expect(strip(line!).length).toBeLessThanOrEqual(60) + expect(strip(line!)).toMatch(/^\d{2}:\d{2}:\d{2} \[@nuxt\/content\] Processed .*…$/) + }) + + it('keeps the repeat marker when a long tag leaves little room', () => { + const [line] = formatEvent(event({ tag: 'nuxt:devtools:config-retriever', message: 'defines Vite-specific options', repeats: 12 }), 38) + expect(strip(line!).length).toBeLessThanOrEqual(38) + expect(strip(line!)).toMatch(/^\d{2}:\d{2}:\d{2} \[nuxt:\S*…\] \S.* ×12$/) + }) + + it('keeps a long tag whole when the message leaves room for it', () => { + const [line] = formatEvent(event({ tag: 'nuxt:devtools:config-retriever', message: 'ok' }), 58) + expect(strip(line!)).toMatch(/^\d{2}:\d{2}:\d{2} \[nuxt:devtools:config-retriever\] ok$/) + }) + + it('never paints a row wider than the terminal', () => { + const events = new DevEventLog() + events.push(event({ tag: 'a'.repeat(60), message: 'b'.repeat(60) })) + events.push(event({ request: `GET /${'c'.repeat(120)}`, requestId: '1', message: 'boom' })) + const columns = Object.getOwnPropertyDescriptor(process.stdout, 'columns') + Object.defineProperty(process.stdout, 'columns', { value: 20, configurable: true }) + const { overlay, lastFrame } = create(events) + try { + overlay.open() + overlay.handleKey({ name: 'y' }) + expect(strip(lastFrame())).toContain('nothing selected') + for (const row of strip(lastFrame()).split('\n')) { + expect(row.length).toBeLessThanOrEqual(20) + } + } + finally { + overlay.handleKey({ name: 'q' }) + if (columns) { + Object.defineProperty(process.stdout, 'columns', columns) + } + else { + Reflect.deleteProperty(process.stdout, 'columns') + } + } + }) + it('opens focused on the newest error', () => { const events = new DevEventLog() events.push(event({ message: 'all fine' }))