Summary
OpenTUI/OpenCode can receive raw PTY input under Perry via process.stdin.setRawMode(true) and process.stdin.on("data", cb), but a TUI cannot cleanly detach stdin and let the process quiesce after teardown. Today the practical workaround is to call process.exit(0) explicitly.
This is separate from the older raw-mode input work: raw input exists, but the lifecycle/removal surface is still incomplete for long-running/full-screen TUI cleanup.
Duplicate check
Searched existing issues for:
stdin setRawMode
process.stdin on data
readline raw mode stdin
stdin removeListener
stdin unref destroy pause resume
Closest matches:
None of those appears to track process.stdin listener removal, detach, or natural process quiescence after TUI teardown.
Evidence on current main
Checked against origin/main at 5f138457b.
Source evidence:
crates/perry-runtime/src/os_process_streams.rs builds TTY fd 0 with fields roughly write, fd, emit, on, once, writable, isRaw, isTTY. It does not install removeListener, off, pause, resume, unref, or destroy for process.stdin.
crates/perry-hir/src/lower/expr_call/module_class_static.rs special-cases only process.stdin.setRawMode(...) and process.stdin.on(...).
crates/perry-stdlib/src/readline.rs::js_readline_stdin_on stores singleton data / keypress callbacks and returns undefined; there is no matching removal API for those callbacks.
crates/perry-stdlib/src/readline.rs::js_readline_has_active keeps the event loop alive while the stdin reader has started and EOF has not been reached, regardless of raw-mode teardown.
Observed shape from the OpenTUI probe was consistent with that source state:
setRawMode:function
on:function
removeListener:undefined
off:undefined
pause:undefined
resume:undefined
unref:undefined
destroy:undefined
Repro sketch
const onData = () => {};
process.stdin.setRawMode?.(true);
process.stdin.on("data", onData);
console.log("removeListener", typeof process.stdin.removeListener);
console.log("off", typeof process.stdin.off);
console.log("pause", typeof process.stdin.pause);
console.log("unref", typeof process.stdin.unref);
console.log("destroy", typeof process.stdin.destroy);
process.stdin.setRawMode?.(false);
process.stdin.removeListener?.("data", onData);
process.stdin.pause?.();
process.stdin.unref?.();
Expected for TUI cleanup: after listeners/timers are removed and raw mode is disabled, the Perry process can exit naturally without process.exit(0).
Needed behavior
process.stdin.on("data", cb) should be EventEmitter/Readable-compatible enough that removeListener / off can remove that callback.
pause() / resume() should at least gate data dispatch for stdin.
unref() or an equivalent detach path should allow the stdin reader to stop keeping the event loop alive after teardown.
destroy() should clear listeners/pending stdin activity and restore a closed/destroyed stream state where feasible.
- Unsupported pieces should fail with a precise diagnostic rather than silently leaving the process alive.
Summary
OpenTUI/OpenCode can receive raw PTY input under Perry via
process.stdin.setRawMode(true)andprocess.stdin.on("data", cb), but a TUI cannot cleanly detach stdin and let the process quiesce after teardown. Today the practical workaround is to callprocess.exit(0)explicitly.This is separate from the older raw-mode input work: raw input exists, but the lifecycle/removal surface is still incomplete for long-running/full-screen TUI cleanup.
Duplicate check
Searched existing issues for:
stdin setRawModeprocess.stdin on datareadline raw mode stdinstdin removeListenerstdin unref destroy pause resumeClosest matches:
None of those appears to track
process.stdinlistener removal, detach, or natural process quiescence after TUI teardown.Evidence on current main
Checked against
origin/mainat5f138457b.Source evidence:
crates/perry-runtime/src/os_process_streams.rsbuilds TTY fd 0 with fields roughlywrite,fd,emit,on,once,writable,isRaw,isTTY. It does not installremoveListener,off,pause,resume,unref, ordestroyforprocess.stdin.crates/perry-hir/src/lower/expr_call/module_class_static.rsspecial-cases onlyprocess.stdin.setRawMode(...)andprocess.stdin.on(...).crates/perry-stdlib/src/readline.rs::js_readline_stdin_onstores singletondata/keypresscallbacks and returnsundefined; there is no matching removal API for those callbacks.crates/perry-stdlib/src/readline.rs::js_readline_has_activekeeps the event loop alive while the stdin reader has started and EOF has not been reached, regardless of raw-mode teardown.Observed shape from the OpenTUI probe was consistent with that source state:
Repro sketch
Expected for TUI cleanup: after listeners/timers are removed and raw mode is disabled, the Perry process can exit naturally without
process.exit(0).Needed behavior
process.stdin.on("data", cb)should be EventEmitter/Readable-compatible enough thatremoveListener/offcan remove that callback.pause()/resume()should at least gate data dispatch for stdin.unref()or an equivalent detach path should allow the stdin reader to stop keeping the event loop alive after teardown.destroy()should clear listeners/pending stdin activity and restore a closed/destroyed stream state where feasible.