Download src/tui/log-pane.ts from bolikoto/zcode-api: direct link, hf CLI and curl.
- Browser
- Download file 2.81 kB
-
https://huggingface.co/spaces/bolikoto/zcode-api/resolve/main/src/tui/log-pane.ts
- Command line
-
hf download hf://spaces/bolikoto/zcode-api/src/tui/log-pane.ts
-
curl -L -o log-pane.ts https://huggingface.co/spaces/bolikoto/zcode-api/resolve/main/src/tui/log-pane.ts
2.81 kB
| /** | |
| * The log pane: a bounded ring of leveled log lines with tail-following and | |
| * scrollback, mirroring the Android app's Logs card (LazyColumn that sticks | |
| * to the newest line). Pure state — the frame renderer and terminal glue | |
| * read it, tests drive it directly. | |
| */ | |
| import { stripAnsi } from "./width.js"; | |
| export type LogLevel = "info" | "warn" | "error"; | |
| export interface LogLine { | |
| readonly seq: number; | |
| readonly level: LogLevel; | |
| readonly text: string; | |
| } | |
| export interface LogView { | |
| /** The lines to draw, oldest first, at most `height` of them. */ | |
| readonly lines: readonly LogLine[]; | |
| /** Total lines retained (for the "Logs (N)" header). */ | |
| readonly total: number; | |
| /** Lines hidden BELOW the viewport toward the tail (0 while following). */ | |
| readonly fromBottom: number; | |
| } | |
| export class LogPane { | |
| private lines: LogLine[] = []; | |
| private nextSeq = 0; | |
| /** Lines hidden below the viewport. 0 = follow the tail. */ | |
| private offset = 0; | |
| constructor(private readonly capacity = 2000) {} | |
| /** | |
| * Append a log record. ANSI escape sequences are stripped (proxy logs carry | |
| * color codes; the pane applies its own) and embedded newlines split into | |
| * separate rows so multi-line records scroll naturally. | |
| */ | |
| push(text: string, level: LogLevel = "info"): void { | |
| const clean = stripAnsi(String(text)).replace(/\r/g, ""); | |
| const parts = clean.split("\n"); | |
| if (parts.length > 1 && parts[parts.length - 1] === "") parts.pop(); | |
| for (const part of parts) { | |
| this.lines.push({ seq: this.nextSeq++, level, text: part }); | |
| } | |
| if (this.lines.length > this.capacity) { | |
| this.lines.splice(0, this.lines.length - this.capacity); | |
| } | |
| } | |
| get count(): number { | |
| return this.lines.length; | |
| } | |
| /** True when the viewport is pinned to the newest lines. */ | |
| get following(): boolean { | |
| return this.offset === 0; | |
| } | |
| scrollUp(n: number): void { | |
| this.offset = Math.min(this.lines.length, this.offset + Math.max(0, n)); | |
| } | |
| scrollDown(n: number): void { | |
| this.offset = Math.max(0, this.offset - Math.max(0, n)); | |
| } | |
| /** Jump back to the tail (re-enable following). */ | |
| followBottom(): void { | |
| this.offset = 0; | |
| } | |
| clear(): void { | |
| this.lines = []; | |
| this.offset = 0; | |
| } | |
| /** Slice of lines currently visible in a viewport `height` rows tall. */ | |
| view(height: number): LogView { | |
| const rows = Math.max(1, height); | |
| // At max scrollback (offset === length) the viewport pins to the oldest | |
| // rows instead of going empty. | |
| const end = Math.max(Math.min(rows, this.lines.length), this.lines.length - this.offset); | |
| const start = Math.max(0, end - rows); | |
| return { | |
| lines: this.lines.slice(start, end), | |
| total: this.lines.length, | |
| fromBottom: this.lines.length - end, | |
| }; | |
| } | |
| } | |