/** * KHAELOR * File: prototypes/tui-spike/shared/ansi.ts * Description: ANSI helpers shared by both spike candidates — strip, width, wrap, truncate. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ export const CSI = "\x1b["; export const SYNC_ON = "\x1b[?2026h"; export const SYNC_OFF = "\x1b[?2026l"; export const HIDE_CURSOR = "\x1b[?25l"; export const SHOW_CURSOR = "\x1b[?25h"; const ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b./g; export function stripAnsi(s: string): string { return s.replace(ANSI_RE, ""); } /** Visible width in columns. Demo content is width-1 code points only, so code-point count is exact here. */ export function visibleWidth(s: string): number { return [...stripAnsi(s)].length; } export function truncate(s: string, w: number): string { const cp = [...s]; return cp.length <= w ? s : cp.slice(0, w).join(""); } /** Greedy word wrap; hard-splits words longer than the width. Splits embedded newlines first. */ export function wrapText(text: string, w: number): string[] { const out: string[] = []; for (const raw of text.split("\n")) { if ([...raw].length <= w) { out.push(raw); continue; } let line = ""; for (const word of raw.split(" ")) { const candidate = line === "" ? word : line + " " + word; if ([...candidate].length <= w) { line = candidate; } else { if (line !== "") out.push(line); let rest = word; while ([...rest].length > w) { out.push([...rest].slice(0, w).join("")); rest = [...rest].slice(w).join(""); } line = rest; } } out.push(line); } return out; } export function countMatches(s: string, re: RegExp): number { let n = 0; re.lastIndex = 0; while (re.exec(s) !== null) n++; return n; }