/** * KHAELOR * File: src/tui/renderer/terminal.ts * Description: Terminal session guard — raw mode + bracketed paste on mount, guaranteed restore on exit and on crash. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { BRACKETED_PASTE_OFF, BRACKETED_PASTE_ON, SHOW_CURSOR, SYNC_OFF, } from "./ansi.js"; export interface TerminalSession { /** Restore the terminal to its pre-mount state. Idempotent. */ restore(): void; } /** * Enter KHAELOR's terminal state: raw mode, bracketed paste. Installs * process-level handlers so the terminal is restored on normal exit, on * SIGTERM/SIGHUP, and on crash (uncaughtException / unhandledRejection) — * a dead KHAELOR must never leave the user's shell in raw mode with a * hidden cursor (CLAUDE.md §14). */ export function enterTerminal( stdin: NodeJS.ReadStream, stdout: NodeJS.WriteStream, ): TerminalSession { const wasRaw = stdin.isTTY ? stdin.isRaw : false; if (stdin.isTTY) stdin.setRawMode(true); stdin.resume(); stdout.write(BRACKETED_PASTE_ON); let restored = false; const restore = (): void => { if (restored) return; restored = true; // Undo everything a paint could have left half-done. stdout.write(SYNC_OFF + SHOW_CURSOR + BRACKETED_PASTE_OFF); if (stdin.isTTY) stdin.setRawMode(wasRaw); stdin.pause(); process.off("exit", onExit); process.off("SIGTERM", onSignal); process.off("SIGHUP", onSignal); process.off("uncaughtException", onFatal); process.off("unhandledRejection", onFatal); }; const onExit = (): void => { restore(); }; const onSignal = (): void => { restore(); process.exit(1); }; const onFatal = (error: unknown): void => { restore(); const message = error instanceof Error ? (error.stack ?? error.message) : String(error); process.stderr.write(`\nkhaelor: fatal error\n${message}\n`); process.exit(1); }; process.on("exit", onExit); process.on("SIGTERM", onSignal); process.on("SIGHUP", onSignal); process.on("uncaughtException", onFatal); process.on("unhandledRejection", onFatal); return { restore }; }