| 1 | import { Box, Text } from 'ink'; |
| 2 | import { memo } from 'react'; |
| 3 | import type { ReactElement } from 'react'; |
| 4 | import { html } from './html.ts'; |
| 5 | import type { LogEntry } from '../browser-shim.ts'; |
| 6 | |
| 7 | /** A log line plus the identity the list needs to render it stably. */ |
| 8 | export interface LogRecord extends LogEntry { |
| 9 | id: number; |
| 10 | /** How many times this message repeated in a row. Absent means once. */ |
| 11 | count?: number; |
| 12 | } |
| 13 | |
| 14 | // superdough and @strudel/core both prefix their messages with a bracketed |
| 15 | // origin ("[sampler] load sound ..."). Dimming the prefix keeps the origin |
| 16 | // available without letting it dominate a pane that's mostly sample loading. |
| 17 | const PREFIX = /^(\[[^\]]+\])\s*([\s\S]*)$/; |
| 18 | |
| 19 | const COLORS: Record<string, string> = { error: 'red', warning: 'yellow' }; |
| 20 | |
| 21 | function LogLine({ entry }: { entry: LogRecord }): ReactElement { |
| 22 | const match = PREFIX.exec(entry.message); |
| 23 | const color = entry.type ? COLORS[entry.type] : undefined; |
| 24 | const repeat = entry.count && entry.count > 1 |
| 25 | ? html`<${Text} dimColor> ×${entry.count}<//>` |
| 26 | : null; |
| 27 | |
| 28 | if (!match) { |
| 29 | return html` |
| 30 | <${Text} wrap="truncate-end"> |
| 31 | <${Text} color=${color}>${entry.message}<//>${repeat} |
| 32 | <//> |
| 33 | `; |
| 34 | } |
| 35 | |
| 36 | return html` |
| 37 | <${Text} wrap="truncate-end"> |
| 38 | <${Text} dimColor>${match[1]} <//> |
| 39 | <${Text} color=${color}>${match[2]}<//>${repeat} |
| 40 | <//> |
| 41 | `; |
| 42 | } |
| 43 | |
| 44 | // Memoised because the app re-renders on every animation frame while the log |
| 45 | // itself changes only when something is logged. Ink still repaints the frame, |
| 46 | // but there is no reason to rebuild this subtree twenty times a second. |
| 47 | export const LogPane = memo(function LogPane({ entries, height }: { |
| 48 | entries: readonly LogRecord[]; |
| 49 | height: number; |
| 50 | }): ReactElement { |
| 51 | const shown = entries.slice(-height); |
| 52 | return html` |
| 53 | <${Box} flexDirection="column" height=${height}> |
| 54 | ${shown.map((entry) => html`<${LogLine} key=${entry.id} entry=${entry} />`)} |
| 55 | <//> |
| 56 | `; |
| 57 | }); |