{"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../src/terminal.ts"],"names":[],"mappings":"AAQA;;GAEG;AACH,MAAM,WAAW,QAAQ;IAExB,KAAK,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAGnE,IAAI,IAAI,IAAI,CAAC;IAEb;;;;;OAKG;IACH,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAG3D,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAG1B,IAAI,OAAO,IAAI,MAAM,CAAC;IACtB,IAAI,IAAI,IAAI,MAAM,CAAC;IAGnB,IAAI,mBAAmB,IAAI,OAAO,CAAC;IAGnC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAG5B,UAAU,IAAI,IAAI,CAAC;IACnB,UAAU,IAAI,IAAI,CAAC;IAGnB,SAAS,IAAI,IAAI,CAAC;IAClB,eAAe,IAAI,IAAI,CAAC;IACxB,WAAW,IAAI,IAAI,CAAC;IAGpB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAG9B,cAAc,IAAI,IAAI,CAAC;IACvB,cAAc,IAAI,IAAI,CAAC;IACvB,mBAAmB,IAAI,IAAI,CAAC;IAC5B,oBAAoB,IAAI,IAAI,CAAC;IAC7B,KAAK,IAAI,OAAO,CAAC;IAGjB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC9F;AAED;;GAEG;AACH,qBAAa,eAAgB,YAAW,QAAQ;IAC/C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAC,CAAyB;IAC9C,OAAO,CAAC,aAAa,CAAC,CAAa;IACnC,OAAO,CAAC,oBAAoB,CAAS;IACrC,OAAO,CAAC,sBAAsB,CAAS;IACvC,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,oBAAoB,CAAS;IACrC,OAAO,CAAC,WAAW,CAAC,CAAc;IAClC,OAAO,CAAC,gBAAgB,CAAC,CAAyB;IAClD,OAAO,CAAC,YAAY,CAIX;IACT,OAAO,CAAC,cAAc,CAGb;IACT,OAAO,CAAC,wBAAwB,CAAC,CAAuB;IACxD,OAAO,CAAC,yBAAyB,CAAC,CAA4B;IAC9D,OAAO,CAAC,YAAY,CAaf;IAEL,IAAI,mBAAmB,IAAI,OAAO,CAEjC;IAED,KAAK,IAAI,OAAO,CAEf;IAED,cAAc,IAAI,IAAI,CAQrB;IAED,cAAc,IAAI,IAAI,CAMrB;IAED,mBAAmB,IAAI,IAAI,CAM1B;IAED,oBAAoB,IAAI,IAAI,CAI3B;IAEK,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWlG;IAED,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,qBAAqB;IAsC7B,OAAO,CAAC,oBAAoB;IAe5B,KAAK,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAqCjE;IAED;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IAgDxB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,2BAA2B;IAYnC;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAuBtB,UAAU,CAAC,KAAK,SAAO,EAAE,MAAM,SAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCzD;IAED,IAAI,IAAI,IAAI,CA6DX;IAED,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CASxB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAS1B;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,SAAS,IAAI,IAAI,CAEhB;IAED,eAAe,IAAI,IAAI,CAEtB;IAED,WAAW,IAAI,IAAI,CAElB;IAED,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAG5B;CACD","sourcesContent":["import * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { setKittyProtocolActive } from \"./keys.js\";\nimport { StdinBuffer } from \"./stdin-buffer.js\";\n\nconst cjsRequire = createRequire(import.meta.url);\n\n/**\n * Minimal terminal interface for TUI\n */\nexport interface Terminal {\n\t// Start the terminal with input and resize handlers\n\tstart(onInput: (data: string) => void, onResize: () => void): void;\n\n\t// Stop the terminal and restore state\n\tstop(): void;\n\n\t/**\n\t * Drain stdin before exiting to prevent Kitty key release events from\n\t * leaking to the parent shell over slow SSH connections.\n\t * @param maxMs - Maximum time to drain (default: 1000ms)\n\t * @param idleMs - Exit early if no input arrives within this time (default: 50ms)\n\t */\n\tdrainInput(maxMs?: number, idleMs?: number): Promise<void>;\n\n\t// Write output to terminal\n\twrite(data: string): void;\n\n\t// Get terminal dimensions\n\tget columns(): number;\n\tget rows(): number;\n\n\t// Whether Kitty keyboard protocol is active\n\tget kittyProtocolActive(): boolean;\n\n\t// Cursor positioning (relative to current position)\n\tmoveBy(lines: number): void; // Move cursor up (negative) or down (positive) by N lines\n\n\t// Cursor visibility\n\thideCursor(): void; // Hide the cursor\n\tshowCursor(): void; // Show the cursor\n\n\t// Clear operations\n\tclearLine(): void; // Clear current line\n\tclearFromCursor(): void; // Clear from cursor to end of screen\n\tclearScreen(): void; // Clear entire screen and move cursor to (0,0)\n\n\t// Title operations\n\tsetTitle(title: string): void; // Set terminal window title\n\n\t// Alt-screen + mouse (optional, no-op on non-TTY implementations)\n\tenterAltScreen(): void;\n\tleaveAltScreen(): void;\n\tenableMouseTracking(): void;\n\tdisableMouseTracking(): void;\n\tisTTY(): boolean;\n\n\t// Query an OSC sequence and read the response within a timeout.\n\tqueryOsc(sequence: string, responsePrefix: string, timeoutMs: number): Promise<string | null>;\n}\n\n/**\n * Real terminal using process.stdin/stdout\n */\nexport class ProcessTerminal implements Terminal {\n\tprivate wasRaw = false;\n\tprivate inputHandler?: (data: string) => void;\n\tprivate resizeHandler?: () => void;\n\tprivate _kittyProtocolActive = false;\n\tprivate _modifyOtherKeysActive = false;\n\tprivate _altScreenActive = false;\n\tprivate _mouseTrackingActive = false;\n\tprivate stdinBuffer?: StdinBuffer;\n\tprivate stdinDataHandler?: (data: string) => void;\n\tprivate oscListeners: {\n\t\tprefix: string;\n\t\tresolve: (value: string | null) => void;\n\t\ttimer: NodeJS.Timeout;\n\t}[] = [];\n\tprivate signalHandlers: {\n\t\tsignal: \"SIGINT\" | \"SIGTERM\" | \"SIGHUP\";\n\t\thandler: () => void;\n\t}[] = [];\n\tprivate uncaughtExceptionHandler?: (err: Error) => void;\n\tprivate unhandledRejectionHandler?: (reason: unknown) => void;\n\tprivate writeLogPath = (() => {\n\t\tconst env = process.env.PI_TUI_WRITE_LOG || \"\";\n\t\tif (!env) return \"\";\n\t\ttry {\n\t\t\tif (fs.statSync(env).isDirectory()) {\n\t\t\t\tconst now = new Date();\n\t\t\t\tconst ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}-${String(now.getDate()).padStart(2, \"0\")}_${String(now.getHours()).padStart(2, \"0\")}-${String(now.getMinutes()).padStart(2, \"0\")}-${String(now.getSeconds()).padStart(2, \"0\")}`;\n\t\t\t\treturn path.join(env, `tui-${ts}-${process.pid}.log`);\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not an existing directory - use as-is (file path)\n\t\t}\n\t\treturn env;\n\t})();\n\n\tget kittyProtocolActive(): boolean {\n\t\treturn this._kittyProtocolActive;\n\t}\n\n\tisTTY(): boolean {\n\t\treturn Boolean(process.stdout.isTTY);\n\t}\n\n\tenterAltScreen(): void {\n\t\tif (this._altScreenActive || !this.isTTY()) return;\n\t\t// CSI ?1049h: save primary screen, enter alt screen, position cursor at 1,1\n\t\t// CSI 2J: clear visible area\n\t\t// CSI H: move cursor home\n\t\t// CSI ?25l: hide cursor\n\t\tprocess.stdout.write(\"\\x1b[?1049h\\x1b[2J\\x1b[H\\x1b[?25l\");\n\t\tthis._altScreenActive = true;\n\t}\n\n\tleaveAltScreen(): void {\n\t\tif (!this._altScreenActive) return;\n\t\t// CSI ?25h: show cursor\n\t\t// CSI ?1049l: leave alt screen, restore primary\n\t\tprocess.stdout.write(\"\\x1b[?25h\\x1b[?1049l\");\n\t\tthis._altScreenActive = false;\n\t}\n\n\tenableMouseTracking(): void {\n\t\tif (this._mouseTrackingActive || !this.isTTY()) return;\n\t\t// CSI ?1000h: send mouse press/release events\n\t\t// CSI ?1006h: SGR-encoded mouse events (wider coordinate range)\n\t\tprocess.stdout.write(\"\\x1b[?1000h\\x1b[?1006h\");\n\t\tthis._mouseTrackingActive = true;\n\t}\n\n\tdisableMouseTracking(): void {\n\t\tif (!this._mouseTrackingActive) return;\n\t\tprocess.stdout.write(\"\\x1b[?1006l\\x1b[?1000l\");\n\t\tthis._mouseTrackingActive = false;\n\t}\n\n\tasync queryOsc(sequence: string, responsePrefix: string, timeoutMs: number): Promise<string | null> {\n\t\tif (!this.isTTY()) return null;\n\t\treturn new Promise<string | null>((resolve) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tconst idx = this.oscListeners.findIndex((l) => l.resolve === resolve);\n\t\t\t\tif (idx !== -1) this.oscListeners.splice(idx, 1);\n\t\t\t\tresolve(null);\n\t\t\t}, timeoutMs);\n\t\t\tthis.oscListeners.push({ prefix: responsePrefix, resolve, timer });\n\t\t\tprocess.stdout.write(sequence);\n\t\t});\n\t}\n\n\tprivate handleOscResponse(data: string): boolean {\n\t\tfor (let i = 0; i < this.oscListeners.length; i++) {\n\t\t\tif (data.includes(this.oscListeners[i].prefix)) {\n\t\t\t\tconst { resolve, timer } = this.oscListeners[i];\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tthis.oscListeners.splice(i, 1);\n\t\t\t\tresolve(data);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate installSignalHandlers(): void {\n\t\tconst teardown = (exitCode: number | null) => {\n\t\t\ttry {\n\t\t\t\tthis.stop();\n\t\t\t} catch {\n\t\t\t\t// best-effort\n\t\t\t}\n\t\t\tif (exitCode !== null) {\n\t\t\t\tprocess.exit(exitCode);\n\t\t\t}\n\t\t};\n\t\tconst signals: Array<\"SIGINT\" | \"SIGTERM\" | \"SIGHUP\"> = [\"SIGINT\", \"SIGTERM\", \"SIGHUP\"];\n\t\tconst signalCodes: Record<string, number> = { SIGINT: 130, SIGTERM: 143, SIGHUP: 129 };\n\t\tfor (const sig of signals) {\n\t\t\tconst handler = () => teardown(signalCodes[sig] ?? 128);\n\t\t\tprocess.on(sig, handler);\n\t\t\tthis.signalHandlers.push({ signal: sig, handler });\n\t\t}\n\t\tthis.uncaughtExceptionHandler = (err: Error) => {\n\t\t\ttry {\n\t\t\t\tthis.stop();\n\t\t\t} catch {}\n\t\t\t// Log after teardown so it actually shows.\n\t\t\tprocess.stderr.write(`\\nUncaught exception: ${err?.stack ?? err}\\n`);\n\t\t\tprocess.exit(1);\n\t\t};\n\t\tthis.unhandledRejectionHandler = (reason: unknown) => {\n\t\t\ttry {\n\t\t\t\tthis.stop();\n\t\t\t} catch {}\n\t\t\tconst msg = reason instanceof Error ? (reason.stack ?? reason.message) : String(reason);\n\t\t\tprocess.stderr.write(`\\nUnhandled rejection: ${msg}\\n`);\n\t\t\tprocess.exit(1);\n\t\t};\n\t\tprocess.on(\"uncaughtException\", this.uncaughtExceptionHandler);\n\t\tprocess.on(\"unhandledRejection\", this.unhandledRejectionHandler);\n\t}\n\n\tprivate removeSignalHandlers(): void {\n\t\tfor (const { signal, handler } of this.signalHandlers) {\n\t\t\tprocess.removeListener(signal, handler);\n\t\t}\n\t\tthis.signalHandlers = [];\n\t\tif (this.uncaughtExceptionHandler) {\n\t\t\tprocess.removeListener(\"uncaughtException\", this.uncaughtExceptionHandler);\n\t\t\tthis.uncaughtExceptionHandler = undefined;\n\t\t}\n\t\tif (this.unhandledRejectionHandler) {\n\t\t\tprocess.removeListener(\"unhandledRejection\", this.unhandledRejectionHandler);\n\t\t\tthis.unhandledRejectionHandler = undefined;\n\t\t}\n\t}\n\n\tstart(onInput: (data: string) => void, onResize: () => void): void {\n\t\tthis.inputHandler = onInput;\n\t\tthis.resizeHandler = onResize;\n\n\t\t// Save previous state and enable raw mode\n\t\tthis.wasRaw = process.stdin.isRaw || false;\n\t\tif (process.stdin.setRawMode) {\n\t\t\tprocess.stdin.setRawMode(true);\n\t\t}\n\t\tprocess.stdin.setEncoding(\"utf8\");\n\t\tprocess.stdin.resume();\n\n\t\t// Enable bracketed paste mode - terminal will wrap pastes in \\x1b[200~ ... \\x1b[201~\n\t\tprocess.stdout.write(\"\\x1b[?2004h\");\n\n\t\t// Set up resize handler immediately\n\t\tprocess.stdout.on(\"resize\", this.resizeHandler);\n\n\t\t// Refresh terminal dimensions - they may be stale after suspend/resume\n\t\t// (SIGWINCH is lost while process is stopped). Unix only.\n\t\tif (process.platform !== \"win32\") {\n\t\t\tprocess.kill(process.pid, \"SIGWINCH\");\n\t\t}\n\n\t\t// On Windows, enable ENABLE_VIRTUAL_TERMINAL_INPUT so the console sends\n\t\t// VT escape sequences (e.g. \\x1b[Z for Shift+Tab) instead of raw console\n\t\t// events that lose modifier information. Must run AFTER setRawMode(true)\n\t\t// since that resets console mode flags.\n\t\tthis.enableWindowsVTInput();\n\n\t\t// Query and enable Kitty keyboard protocol\n\t\t// The query handler intercepts input temporarily, then installs the user's handler\n\t\t// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/\n\t\tthis.queryAndEnableKittyProtocol();\n\n\t\t// Install signal + uncaught handlers so we always restore terminal state on exit.\n\t\tthis.installSignalHandlers();\n\t}\n\n\t/**\n\t * Set up StdinBuffer to split batched input into individual sequences.\n\t * This ensures components receive single events, making matchesKey/isKeyRelease work correctly.\n\t *\n\t * Also watches for Kitty protocol response and enables it when detected.\n\t * This is done here (after stdinBuffer parsing) rather than on raw stdin\n\t * to handle the case where the response arrives split across multiple events.\n\t */\n\tprivate setupStdinBuffer(): void {\n\t\tthis.stdinBuffer = new StdinBuffer({ timeout: 10 });\n\n\t\t// Kitty protocol response pattern: \\x1b[?<flags>u\n\t\tconst kittyResponsePattern = /^\\x1b\\[\\?(\\d+)u$/;\n\n\t\t// Forward individual sequences to the input handler\n\t\tthis.stdinBuffer.on(\"data\", (sequence) => {\n\t\t\t// Check for Kitty protocol response (only if not already enabled)\n\t\t\tif (!this._kittyProtocolActive) {\n\t\t\t\tconst match = sequence.match(kittyResponsePattern);\n\t\t\t\tif (match) {\n\t\t\t\t\tthis._kittyProtocolActive = true;\n\t\t\t\t\tsetKittyProtocolActive(true);\n\n\t\t\t\t\t// Enable Kitty keyboard protocol (push flags)\n\t\t\t\t\t// Flag 1 = disambiguate escape codes\n\t\t\t\t\t// Flag 2 = report event types (press/repeat/release)\n\t\t\t\t\t// Flag 4 = report alternate keys (shifted key, base layout key)\n\t\t\t\t\t// Base layout key enables shortcuts to work with non-Latin keyboard layouts\n\t\t\t\t\tprocess.stdout.write(\"\\x1b[>7u\");\n\t\t\t\t\treturn; // Don't forward protocol response to TUI\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Intercept OSC responses (e.g. background color query)\n\t\t\tif (this.oscListeners.length > 0 && this.handleOscResponse(sequence)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (this.inputHandler) {\n\t\t\t\tthis.inputHandler(sequence);\n\t\t\t}\n\t\t});\n\n\t\t// Re-wrap paste content with bracketed paste markers for existing editor handling\n\t\tthis.stdinBuffer.on(\"paste\", (content) => {\n\t\t\tif (this.inputHandler) {\n\t\t\t\tthis.inputHandler(`\\x1b[200~${content}\\x1b[201~`);\n\t\t\t}\n\t\t});\n\n\t\t// Handler that pipes stdin data through the buffer\n\t\tthis.stdinDataHandler = (data: string) => {\n\t\t\tthis.stdinBuffer!.process(data);\n\t\t};\n\t}\n\n\t/**\n\t * Query terminal for Kitty keyboard protocol support and enable if available.\n\t *\n\t * Sends CSI ? u to query current flags. If terminal responds with CSI ? <flags> u,\n\t * it supports the protocol and we enable it with CSI > 1 u.\n\t *\n\t * If no Kitty response arrives shortly after startup, fall back to enabling\n\t * xterm modifyOtherKeys mode 2. This is needed for tmux, which can forward\n\t * modified enter keys as CSI-u when extended-keys is enabled, but may not\n\t * answer the Kitty protocol query.\n\t *\n\t * The response is detected in setupStdinBuffer's data handler, which properly\n\t * handles the case where the response arrives split across multiple stdin events.\n\t */\n\tprivate queryAndEnableKittyProtocol(): void {\n\t\tthis.setupStdinBuffer();\n\t\tprocess.stdin.on(\"data\", this.stdinDataHandler!);\n\t\tprocess.stdout.write(\"\\x1b[?u\");\n\t\tsetTimeout(() => {\n\t\t\tif (!this._kittyProtocolActive && !this._modifyOtherKeysActive) {\n\t\t\t\tprocess.stdout.write(\"\\x1b[>4;2m\");\n\t\t\t\tthis._modifyOtherKeysActive = true;\n\t\t\t}\n\t\t}, 150);\n\t}\n\n\t/**\n\t * On Windows, add ENABLE_VIRTUAL_TERMINAL_INPUT (0x0200) to the stdin\n\t * console handle so the terminal sends VT sequences for modified keys\n\t * (e.g. \\x1b[Z for Shift+Tab). Without this, libuv's ReadConsoleInputW\n\t * discards modifier state and Shift+Tab arrives as plain \\t.\n\t */\n\tprivate enableWindowsVTInput(): void {\n\t\tif (process.platform !== \"win32\") return;\n\t\ttry {\n\t\t\t// Dynamic require to avoid bundling koffi's 74MB of cross-platform\n\t\t\t// native binaries into every compiled binary. Koffi is only needed\n\t\t\t// on Windows for VT input support.\n\t\t\tconst koffi = cjsRequire(\"koffi\");\n\t\t\tconst k32 = koffi.load(\"kernel32.dll\");\n\t\t\tconst GetStdHandle = k32.func(\"void* __stdcall GetStdHandle(int)\");\n\t\t\tconst GetConsoleMode = k32.func(\"bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)\");\n\t\t\tconst SetConsoleMode = k32.func(\"bool __stdcall SetConsoleMode(void*, uint32_t)\");\n\n\t\t\tconst STD_INPUT_HANDLE = -10;\n\t\t\tconst ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;\n\t\t\tconst handle = GetStdHandle(STD_INPUT_HANDLE);\n\t\t\tconst mode = new Uint32Array(1);\n\t\t\tGetConsoleMode(handle, mode);\n\t\t\tSetConsoleMode(handle, mode[0]! | ENABLE_VIRTUAL_TERMINAL_INPUT);\n\t\t} catch {\n\t\t\t// koffi not available — Shift+Tab won't be distinguishable from Tab\n\t\t}\n\t}\n\n\tasync drainInput(maxMs = 1000, idleMs = 50): Promise<void> {\n\t\tif (this._kittyProtocolActive) {\n\t\t\t// Disable Kitty keyboard protocol first so any late key releases\n\t\t\t// do not generate new Kitty escape sequences.\n\t\t\tprocess.stdout.write(\"\\x1b[<u\");\n\t\t\tthis._kittyProtocolActive = false;\n\t\t\tsetKittyProtocolActive(false);\n\t\t}\n\t\tif (this._modifyOtherKeysActive) {\n\t\t\tprocess.stdout.write(\"\\x1b[>4;0m\");\n\t\t\tthis._modifyOtherKeysActive = false;\n\t\t}\n\n\t\tconst previousHandler = this.inputHandler;\n\t\tthis.inputHandler = undefined;\n\n\t\tlet lastDataTime = Date.now();\n\t\tconst onData = () => {\n\t\t\tlastDataTime = Date.now();\n\t\t};\n\n\t\tprocess.stdin.on(\"data\", onData);\n\t\tconst endTime = Date.now() + maxMs;\n\n\t\ttry {\n\t\t\twhile (true) {\n\t\t\t\tconst now = Date.now();\n\t\t\t\tconst timeLeft = endTime - now;\n\t\t\t\tif (timeLeft <= 0) break;\n\t\t\t\tif (now - lastDataTime >= idleMs) break;\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, Math.min(idleMs, timeLeft)));\n\t\t\t}\n\t\t} finally {\n\t\t\tprocess.stdin.removeListener(\"data\", onData);\n\t\t\tthis.inputHandler = previousHandler;\n\t\t}\n\t}\n\n\tstop(): void {\n\t\t// Cancel any pending OSC listeners\n\t\tfor (const listener of this.oscListeners) {\n\t\t\tclearTimeout(listener.timer);\n\t\t\tlistener.resolve(null);\n\t\t}\n\t\tthis.oscListeners = [];\n\n\t\t// Disable mouse tracking\n\t\tthis.disableMouseTracking();\n\n\t\t// Disable bracketed paste mode\n\t\tprocess.stdout.write(\"\\x1b[?2004l\");\n\n\t\t// Disable Kitty keyboard protocol if not already done by drainInput()\n\t\tif (this._kittyProtocolActive) {\n\t\t\tprocess.stdout.write(\"\\x1b[<u\");\n\t\t\tthis._kittyProtocolActive = false;\n\t\t\tsetKittyProtocolActive(false);\n\t\t}\n\t\tif (this._modifyOtherKeysActive) {\n\t\t\tprocess.stdout.write(\"\\x1b[>4;0m\");\n\t\t\tthis._modifyOtherKeysActive = false;\n\t\t}\n\n\t\t// Clean up StdinBuffer\n\t\tif (this.stdinBuffer) {\n\t\t\tthis.stdinBuffer.destroy();\n\t\t\tthis.stdinBuffer = undefined;\n\t\t}\n\n\t\t// Remove event handlers\n\t\tif (this.stdinDataHandler) {\n\t\t\tprocess.stdin.removeListener(\"data\", this.stdinDataHandler);\n\t\t\tthis.stdinDataHandler = undefined;\n\t\t}\n\t\tthis.inputHandler = undefined;\n\t\tif (this.resizeHandler) {\n\t\t\tprocess.stdout.removeListener(\"resize\", this.resizeHandler);\n\t\t\tthis.resizeHandler = undefined;\n\t\t}\n\n\t\t// Pause stdin to prevent any buffered input (e.g., Ctrl+D) from being\n\t\t// re-interpreted after raw mode is disabled. This fixes a race condition\n\t\t// where Ctrl+D could close the parent shell over SSH.\n\t\tprocess.stdin.pause();\n\n\t\t// Restore raw mode state\n\t\tif (process.stdin.setRawMode) {\n\t\t\tprocess.stdin.setRawMode(this.wasRaw);\n\t\t}\n\n\t\t// Leave alt screen AFTER restoring stdin so terminal state transitions atomically.\n\t\tthis.leaveAltScreen();\n\n\t\t// Reset any leftover SGR state in the restored primary buffer.\n\t\tif (this.isTTY()) {\n\t\t\tprocess.stdout.write(\"\\x1b[0m\");\n\t\t}\n\n\t\tthis.removeSignalHandlers();\n\t}\n\n\twrite(data: string): void {\n\t\tprocess.stdout.write(data);\n\t\tif (this.writeLogPath) {\n\t\t\ttry {\n\t\t\t\tfs.appendFileSync(this.writeLogPath, data, { encoding: \"utf8\" });\n\t\t\t} catch {\n\t\t\t\t// Ignore logging errors\n\t\t\t}\n\t\t}\n\t}\n\n\tget columns(): number {\n\t\treturn process.stdout.columns || 80;\n\t}\n\n\tget rows(): number {\n\t\treturn process.stdout.rows || 24;\n\t}\n\n\tmoveBy(lines: number): void {\n\t\tif (lines > 0) {\n\t\t\t// Move down\n\t\t\tprocess.stdout.write(`\\x1b[${lines}B`);\n\t\t} else if (lines < 0) {\n\t\t\t// Move up\n\t\t\tprocess.stdout.write(`\\x1b[${-lines}A`);\n\t\t}\n\t\t// lines === 0: no movement\n\t}\n\n\thideCursor(): void {\n\t\tprocess.stdout.write(\"\\x1b[?25l\");\n\t}\n\n\tshowCursor(): void {\n\t\tprocess.stdout.write(\"\\x1b[?25h\");\n\t}\n\n\tclearLine(): void {\n\t\tprocess.stdout.write(\"\\x1b[K\");\n\t}\n\n\tclearFromCursor(): void {\n\t\tprocess.stdout.write(\"\\x1b[J\");\n\t}\n\n\tclearScreen(): void {\n\t\tprocess.stdout.write(\"\\x1b[2J\\x1b[H\"); // Clear screen and move to home (1,1)\n\t}\n\n\tsetTitle(title: string): void {\n\t\t// OSC 0;title BEL - set terminal window title\n\t\tprocess.stdout.write(`\\x1b]0;${title}\\x07`);\n\t}\n}\n"]}