// Low-level terminal utilities for flicker-free full-screen rendering. // Instead of clearing and rewriting (which blinks), we position the cursor // at home and overwrite in-place — content is never erased before being replaced. const ESC = "\x1b"; // --- ANSI helpers --- export const style = { bold: (s: string) => `${ESC}[1m${s}${ESC}[22m`, dim: (s: string) => `${ESC}[2m${s}${ESC}[22m`, inverse: (s: string) => `${ESC}[7m${s}${ESC}[27m`, yellow: (s: string) => `${ESC}[33m${s}${ESC}[39m`, red: (s: string) => `${ESC}[31m${s}${ESC}[39m`, green: (s: string) => `${ESC}[32m${s}${ESC}[39m`, cyan: (s: string) => `${ESC}[36m${s}${ESC}[39m`, boldYellow: (s: string) => `${ESC}[1;33m${s}${ESC}[22;39m`, blue: (s: string) => `${ESC}[38;2;60;110;253m${s}${ESC}[39m`, }; // --- Screen control --- export function enterFullScreen(): void { process.stdout.write(`${ESC}[?1049h`); // alternate screen buffer process.stdout.write(`${ESC}[?2004h`); // enable bracketed paste mode process.stdout.write(`${ESC}[>1u`); // enable kitty keyboard protocol (disambiguate mode) process.stdout.write(`${ESC}[?25l`); // hide cursor process.stdout.write(`${ESC}[H`); // cursor home } export function exitFullScreen(): void { process.stdout.write(`${ESC}[?25h`); // show cursor process.stdout.write(`${ESC}[ 1 && !s.startsWith(ESC)) { const normalized = s.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); return { raw: normalized, name: "paste", shift: false, ctrl: false }; } return { raw: s, name: s, shift: false, ctrl: false }; } /** Strip ANSI escape codes to get visible character count */ export function stripAnsi(s: string): string { return s.replace(/\x1b\[[0-9;]*m/g, ""); } /** Skip `offset` visible characters, preserving ANSI state, then return the rest */ export function ansiSlice(s: string, offset: number): string { if (offset <= 0) return s; // Collect ANSI sequences encountered while skipping so we can replay them let activeAnsi = ""; let count = 0; let i = 0; while (i < s.length && count < offset) { if (s[i] === "\x1b") { const end = s.indexOf("m", i); if (end >= 0) { activeAnsi += s.slice(i, end + 1); i = end + 1; continue; } } count++; i++; } // Consume any ANSI codes right at the boundary while (i < s.length && s[i] === "\x1b") { const end = s.indexOf("m", i); if (end >= 0) { activeAnsi += s.slice(i, end + 1); i = end + 1; } else break; } return activeAnsi + s.slice(i); } /** Pad/truncate a string to a visible width (ANSI-aware) */ export function fitWidth(s: string, width: number): string { const visible = stripAnsi(s); if (visible.length >= width) { // Truncate — need to be careful with ANSI codes let count = 0; let i = 0; while (i < s.length && count < width) { if (s[i] === "\x1b") { const end = s.indexOf("m", i); if (end >= 0) { i = end + 1; continue; } } count++; i++; } // Include any trailing ANSI reset codes const rest = s.slice(i); const resets = rest.match(/^(\x1b\[[0-9;]*m)*/)?.[0] || ""; return s.slice(0, i) + resets; } return s + " ".repeat(width - visible.length); }