From 1b870ae1b8f86cecca2432073fa53611c32631a6 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 20 Aug 2026 08:41:58 +0000 Subject: [PATCH] tui: drop the win32 console-mode ffi shim, rely on setRawMode --- packages/tui/package.json | 5 - packages/tui/src/app.tsx | 3 - packages/tui/src/terminal-win32.bun.ts | 130 ------------------------ packages/tui/src/terminal-win32.node.ts | 77 -------------- packages/tui/src/terminal-win32.ts | 1 - 5 files changed, 216 deletions(-) delete mode 100644 packages/tui/src/terminal-win32.bun.ts delete mode 100644 packages/tui/src/terminal-win32.node.ts delete mode 100644 packages/tui/src/terminal-win32.ts diff --git a/packages/tui/package.json b/packages/tui/package.json index 4ac1e3817929..140b4ad8c4e1 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -59,11 +59,6 @@ "node": "./src/attention-sounds.node.ts", "default": "./src/attention-sounds.bun.ts" }, - "#terminal-win32": { - "bun": "./src/terminal-win32.bun.ts", - "node": "./src/terminal-win32.node.ts", - "default": "./src/terminal-win32.bun.ts" - }, "#string-width": { "bun": "./src/util/string-width.bun.ts", "node": "./src/util/string-width.node.ts", diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index aee75c67b883..710a01b8d641 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -96,7 +96,6 @@ import { CommandPaletteDialog } from "./component/command-palette" import { COMMAND_PALETTE_COMMAND, Keymap, type KeymapCommand } from "./context/keymap" import { DialogVariant } from "./component/dialog-variant" -import { win32DisableProcessedInput, win32FlushInputBuffer } from "./terminal-win32" import { destroyRenderer } from "./util/renderer" import { cliErrorMessage, errorFormat } from "./util/error" import { AttentionProvider } from "./context/attention" @@ -266,7 +265,6 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { Effect.catch((error) => Effect.sync(() => log("error", "Failed to dispose TUI clipboard", { error }))), ), ) - win32DisableProcessedInput() const finalizers = new Set<() => Promise>() yield* Effect.addFinalizer(() => Effect.promise(async () => { @@ -450,7 +448,6 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { }), ) yield* Effect.sync(() => { - win32FlushInputBuffer() if (result.reason !== undefined) process.stderr.write((cliErrorMessage(result.reason) ?? errorFormat(result.reason)) + "\n") if (result.epilogue) process.stdout.write(result.epilogue + "\n") diff --git a/packages/tui/src/terminal-win32.bun.ts b/packages/tui/src/terminal-win32.bun.ts deleted file mode 100644 index 1aaa80aecd69..000000000000 --- a/packages/tui/src/terminal-win32.bun.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { dlopen, ptr } from "bun:ffi" -import type { ReadStream } from "node:tty" - -const STD_INPUT_HANDLE = -10 -const ENABLE_PROCESSED_INPUT = 0x0001 - -const kernel = () => - dlopen("kernel32.dll", { - GetStdHandle: { args: ["i32"], returns: "ptr" }, - GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" }, - SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" }, - FlushConsoleInputBuffer: { args: ["ptr"], returns: "i32" }, - }) - -let k32: ReturnType | undefined - -function load() { - if (process.platform !== "win32") return false - try { - k32 ??= kernel() - return true - } catch { - return false - } -} - -/** - * Clear ENABLE_PROCESSED_INPUT on the console stdin handle. - */ -export function win32DisableProcessedInput() { - if (process.platform !== "win32") return - if (!process.stdin.isTTY) return - if (!load()) return - - const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) - const buf = new Uint32Array(1) - if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return - - const mode = buf[0]! - if ((mode & ENABLE_PROCESSED_INPUT) === 0) return - k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) -} - -/** - * Discard any queued console input (mouse events, key presses, etc.). - */ -export function win32FlushInputBuffer() { - if (process.platform !== "win32") return - if (!process.stdin.isTTY) return - if (!load()) return - - const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) - k32!.symbols.FlushConsoleInputBuffer(handle) -} - -let unhook: (() => void) | undefined - -/** - * Keep ENABLE_PROCESSED_INPUT disabled. - * - * On Windows, Ctrl+C becomes a CTRL_C_EVENT (instead of stdin input) when - * ENABLE_PROCESSED_INPUT is set. Various runtimes can re-apply console modes - * (sometimes on a later tick), and the flag is console-global, not per-process. - * - * We combine: - * - A `setRawMode(...)` hook to re-clear after known raw-mode toggles. - * - A low-frequency poll as a backstop for native/external mode changes. - */ -export function win32InstallCtrlCGuard() { - if (process.platform !== "win32") return - if (!process.stdin.isTTY) return - if (!load()) return - if (unhook) return unhook - - const stdin = process.stdin as ReadStream - const original = stdin.setRawMode - - const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) - const buf = new Uint32Array(1) - - if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return - const initial = buf[0]! - - const enforce = () => { - if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return - const mode = buf[0]! - if ((mode & ENABLE_PROCESSED_INPUT) === 0) return - k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) - } - - // Some runtimes can re-apply console modes on the next tick; enforce twice. - const later = () => { - enforce() - setImmediate(enforce) - } - - let wrapped: ReadStream["setRawMode"] | undefined - - if (typeof original === "function") { - wrapped = (mode: boolean) => { - const result = original.call(stdin, mode) - later() - return result - } - - stdin.setRawMode = wrapped - } - - // Ensure it's cleared immediately too (covers any earlier mode changes). - later() - - const interval = setInterval(enforce, 100) - interval.unref() - - let done = false - unhook = () => { - if (done) return - done = true - - clearInterval(interval) - if (wrapped && stdin.setRawMode === wrapped) { - stdin.setRawMode = original - } - - k32!.symbols.SetConsoleMode(handle, initial) - unhook = undefined - } - - return unhook -} diff --git a/packages/tui/src/terminal-win32.node.ts b/packages/tui/src/terminal-win32.node.ts deleted file mode 100644 index 11f2f9c9930b..000000000000 --- a/packages/tui/src/terminal-win32.node.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { dlopen } from "node:ffi" -import type { ReadStream } from "node:tty" - -const STD_INPUT_HANDLE = -10 -const ENABLE_PROCESSED_INPUT = 0x0001 - -const kernel = () => - dlopen("kernel32.dll", { - GetStdHandle: { arguments: ["i32"], return: "pointer" }, - GetConsoleMode: { arguments: ["pointer", "pointer"], return: "i32" }, - SetConsoleMode: { arguments: ["pointer", "u32"], return: "i32" }, - FlushConsoleInputBuffer: { arguments: ["pointer"], return: "i32" }, - }).functions - -let k32: ReturnType | undefined - -function load() { - if (process.platform !== "win32") return false - try { - k32 ??= kernel() - return true - } catch { - return false - } -} - -export function win32DisableProcessedInput() { - if (process.platform !== "win32" || !process.stdin.isTTY || !load()) return - const handle = k32!.GetStdHandle(STD_INPUT_HANDLE) - const buffer = new Uint32Array(1) - if (k32!.GetConsoleMode(handle, buffer) === 0) return - const mode = buffer[0]! - if ((mode & ENABLE_PROCESSED_INPUT) === 0) return - k32!.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) -} - -export function win32FlushInputBuffer() { - if (process.platform !== "win32" || !process.stdin.isTTY || !load()) return - k32!.FlushConsoleInputBuffer(k32!.GetStdHandle(STD_INPUT_HANDLE)) -} - -let unhook: (() => void) | undefined - -export function win32InstallCtrlCGuard() { - if (process.platform !== "win32" || !process.stdin.isTTY || !load() || unhook) return unhook - const stdin = process.stdin as ReadStream - const original = stdin.setRawMode - const handle = k32!.GetStdHandle(STD_INPUT_HANDLE) - const buffer = new Uint32Array(1) - if (k32!.GetConsoleMode(handle, buffer) === 0) return - const initial = buffer[0]! - const enforce = () => { - if (k32!.GetConsoleMode(handle, buffer) === 0) return - const mode = buffer[0]! - if ((mode & ENABLE_PROCESSED_INPUT) !== 0) k32!.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) - } - const later = () => { - enforce() - setImmediate(enforce) - } - const wrapped: ReadStream["setRawMode"] = (mode) => { - const result = original.call(stdin, mode) - later() - return result - } - stdin.setRawMode = wrapped - later() - const interval = setInterval(enforce, 100) - interval.unref() - unhook = () => { - clearInterval(interval) - if (stdin.setRawMode === wrapped) stdin.setRawMode = original - k32!.SetConsoleMode(handle, initial) - unhook = undefined - } - return unhook -} diff --git a/packages/tui/src/terminal-win32.ts b/packages/tui/src/terminal-win32.ts deleted file mode 100644 index e58c0e61489a..000000000000 --- a/packages/tui/src/terminal-win32.ts +++ /dev/null @@ -1 +0,0 @@ -export { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "#terminal-win32"