Skip to content

process.stdin: support listener removal and lifecycle detach for TUI teardown #3962

Description

@andrewtdiz

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions