hook better instead of 100ms loop lol

This commit is contained in:
LukeParkerDev
2026-02-11 12:57:01 +10:00
parent 943bf316b6
commit 56dfbbbc93
3 changed files with 53 additions and 19 deletions
+3 -5
View File
@@ -3,7 +3,7 @@ import { Clipboard } from "@tui/util/clipboard"
import { TextAttributes } from "@opentui/core"
import { RouteProvider, useRoute } from "@tui/context/route"
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
import { win32DisableProcessedInput, win32IgnoreCtrlC, win32EnforceCtrlCGuard } from "./win32"
import { win32DisableProcessedInput, win32IgnoreCtrlC, win32InstallCtrlCGuard } from "./win32"
import { Installation } from "@/installation"
import { Flag } from "@/flag/flag"
import { DialogProvider, useDialog } from "@tui/ui/dialog"
@@ -111,6 +111,7 @@ export function tui(input: {
}) {
// promise to prevent immediate exit
return new Promise<void>(async (resolve) => {
const unguard = win32InstallCtrlCGuard()
win32DisableProcessedInput()
win32IgnoreCtrlC()
@@ -121,6 +122,7 @@ export function tui(input: {
win32DisableProcessedInput()
const onExit = async () => {
unguard?.()
await input.onExit?.()
resolve()
}
@@ -247,10 +249,6 @@ function App() {
const args = useArgs()
onMount(() => {
// opentui reconfigures console mode via native calls which re-enable
// ENABLE_PROCESSED_INPUT. Poll and re-clear it so the parent `bun run`
// wrapper isn't killed by CTRL_C_EVENT.
win32EnforceCtrlCGuard()
batch(() => {
if (args.agent) local.agent.set(args.agent)
if (args.model) {
+5 -1
View File
@@ -9,7 +9,7 @@ import { Log } from "@/util/log"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import type { Event } from "@opencode-ai/sdk/v2"
import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput } from "./win32"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
declare global {
const OPENCODE_WORKER_PATH: string
@@ -78,6 +78,10 @@ export const TuiThreadCommand = cmd({
describe: "agent to use",
}),
handler: async (args) => {
// Keep ENABLE_PROCESSED_INPUT cleared even if other code flips it.
// (Important when running under `bun run` wrappers on Windows.)
win32InstallCtrlCGuard()
// Must be the very first thing — disables CTRL_C_EVENT before any Worker
// spawn or async work so the OS cannot kill the process group.
win32DisableProcessedInput()
+45 -13
View File
@@ -55,32 +55,64 @@ export function win32IgnoreCtrlC() {
k32!.symbols.SetConsoleCtrlHandler(null, 1)
}
let unhook: (() => void) | undefined
/**
* Continuously enforce ENABLE_PROCESSED_INPUT=off on the console.
* Keep ENABLE_PROCESSED_INPUT disabled.
*
* opentui reconfigures the console mode through native calls (not
* process.stdin.setRawMode) so we cannot intercept them. Instead we
* poll at a low frequency and re-clear the flag when needed.
* 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.
*
* Because ENABLE_PROCESSED_INPUT is a console-level flag (not per-process),
* keeping it cleared protects every process attached to this console,
* including the parent `bun run` wrapper that we can't otherwise control.
*
* The fast-path (GetConsoleMode + bitmask check) is sub-microsecond;
* SetConsoleMode only fires when something re-enabled the flag.
* 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 win32EnforceCtrlCGuard() {
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 any
const original = stdin.setRawMode
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
const buf = new Uint32Array(1)
setInterval(() => {
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)
}, 100)
}
// Some runtimes can re-apply console modes on the next tick; enforce twice.
const later = () => {
enforce()
setImmediate(enforce)
}
if (typeof original === "function") {
stdin.setRawMode = (mode: boolean) => {
const result = original.call(stdin, mode)
later()
return result
}
}
// Ensure it's cleared immediately too (covers any earlier mode changes).
later()
const interval = setInterval(enforce, 100)
unhook = () => {
clearInterval(interval)
if (typeof original === "function") {
stdin.setRawMode = original
}
unhook = undefined
}
return unhook
}