/** * KHAELOR * File: src/tui/renderer/capabilities.ts * Description: Terminal capability detection — color depth ladder, NO_COLOR, DEC 2026 probe, OSC 11 background query. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ export type ColorDepth = "truecolor" | "ansi256" | "ansi16" | "mono"; export type Background = "dark" | "light"; export interface TerminalCapabilities { colorDepth: ColorDepth; /** DEC 2026 synchronized-output supported (probed; false when unknown). */ syncUpdates: boolean; background: Background; } /** * Color capability ladder (TUI_DESIGN §12): truecolor → ANSI-256 → ANSI-16 → * monochrome. `NO_COLOR` and `TERM=dumb` are honored absolutely; non-TTY * output is monochrome. */ export function detectColorDepth(env: NodeJS.ProcessEnv, isTty: boolean): ColorDepth { if (!isTty) return "mono"; if (env["NO_COLOR"] !== undefined && env["NO_COLOR"] !== "") return "mono"; const term = env["TERM"] ?? ""; if (term === "dumb" || term === "") return "mono"; const colorterm = env["COLORTERM"] ?? ""; if (colorterm === "truecolor" || colorterm === "24bit") return "truecolor"; if (term.includes("256color")) return "ansi256"; return "ansi16"; } /** * Parse a DECRQM reply for mode 2026: `CSI ? 2026 ; Ps $ y`. * Ps 1 (set) / 2 (reset) / 3 (permanently set) mean the mode is recognized → * synchronized updates are supported. Ps 0 (unrecognized) / 4 (permanently * reset) mean unsupported. Returns null when no complete reply is present. */ export function parseDecrqmReply(data: string): boolean | null { const m = /\x1b\[\?2026;(\d+)\$y/.exec(data); if (!m) return null; const ps = Number(m[1]); return ps === 1 || ps === 2 || ps === 3; } /** * Parse an OSC 11 background-color reply (`OSC 11 ; rgb:RRRR/GGGG/BBBB ST|BEL`) * into light/dark via relative luma. Returns null when no complete reply is present. */ export function parseOsc11Reply(data: string): Background | null { const m = /\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)(?:\x07|\x1b\\)/.exec(data); if (!m) return null; const channel = (hex: string): number => { const max = Math.pow(16, hex.length) - 1; return parseInt(hex, 16) / max; }; const r = channel(m[1] as string); const g = channel(m[2] as string); const b = channel(m[3] as string); const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; return luma > 0.5 ? "light" : "dark"; } export interface ProbeStreams { stdin: NodeJS.ReadStream; stdout: NodeJS.WriteStream; } /** * Probe the terminal for DEC 2026 support and background color. Requires raw * mode to already be active (replies arrive on stdin without a newline). * Resolves within `timeoutMs` (default 100 ms — TUI_DESIGN §12) with whatever * was learned; absence of a reply degrades to `{ syncUpdates: false, * background: null }` — never a hang, never a guess presented as fact. */ export function probeTerminal( streams: ProbeStreams, timeoutMs = 100, ): Promise<{ syncUpdates: boolean; background: Background | null }> { const { stdin, stdout } = streams; if (!stdin.isTTY || !stdout.isTTY) { return Promise.resolve({ syncUpdates: false, background: null }); } return new Promise((resolve) => { let buffer = ""; let sync: boolean | null = null; let background: Background | null = null; let done = false; const finish = (): void => { if (done) return; done = true; stdin.off("data", onData); clearTimeout(timer); resolve({ syncUpdates: sync ?? false, background }); }; const onData = (chunk: Buffer | string): void => { buffer += chunk.toString("utf8"); if (sync === null) sync = parseDecrqmReply(buffer); if (background === null) background = parseOsc11Reply(buffer); if (sync !== null && background !== null) finish(); }; const timer = setTimeout(finish, timeoutMs); timer.unref(); stdin.on("data", onData); // DECRQM for mode 2026, then OSC 11 background query. stdout.write("\x1b[?2026$p\x1b]11;?\x07"); }); }