Description
When a process using @opentui/core receives SIGTSTP from an external source (e.g., code-server terminal management, job control, kill -TSTP), the process is suspended by the kernel without any terminal cleanup. Mouse tracking remains enabled, causing mouse events to appear as garbled escape sequences in the shell.
Root Cause
opentui's exitSignals list includes SIGINT, SIGTERM, SIGQUIT, SIGABRT, SIGHUP, SIGBREAK, SIGPIPE, SIGBUS, SIGFPE — but no SIGTSTP.
There is no process.on("SIGTSTP") handler anywhere in the renderer. When SIGTSTP arrives:
- Mouse tracking (
\x1b[?1000h, \x1b[?1003h, \x1b[?1006h) stays enabled
- Kitty keyboard protocol stays enabled
- Raw mode stays on (until the shell overrides)
- The shell receives mouse events as raw escape sequences → garbled text
The Correct Pattern Already Exists
The manual suspend() method does this correctly:
suspend() {
this._suspendedMouseEnabled = this._useMouse
this.disableMouse() // 1. disable mouse
this.removeExitListeners()
this.stdinParser?.reset()
this.stdin.removeListener("data", this.stdinListener)
this.lib.suspendRenderer(...) // 2. native suspend
this.stdin.setRawMode(false) // 3. raw mode off
this.stdin.pause()
}
But this is only called from consumer code (e.g., a keybind handler). An external SIGTSTP bypasses it entirely.
Proposed Fix
Register SIGTSTP/SIGCONT handlers in the renderer that call suspend()/resume():
private sigtstpHandler = () => {
this.suspend()
// Remove handler to allow default behavior (suspend)
process.removeListener("SIGTSTP", this.sigtstpHandler)
// Re-raise to actually suspend the process
process.kill(process.pid, "SIGTSTP")
}
private sigcontHandler = () => {
// Re-register SIGTSTP handler
process.on("SIGTSTP", this.sigtstpHandler)
this.resume()
}
Register in setupTerminal(), remove in cleanupBeforeDestroy().
Note: In Node/Bun, registering a SIGTSTP handler overrides the default behavior (immediate suspend). The handler must remove itself and re-raise SIGTSTP to actually suspend. The SIGCONT handler re-registers it.
Reproduction
- Run any opentui TUI application with mouse tracking enabled
- In another terminal:
kill -TSTP $(pgrep -f <app>)
- Move the mouse in the shell
- Garbled escape sequences appear:
35;89;19M35;84;20M35...
Observed in opencode running in code-server (VS Code web terminal) where the terminal management layer can send SIGTSTP during inactivity.
Related
Description
When a process using @opentui/core receives SIGTSTP from an external source (e.g., code-server terminal management, job control,
kill -TSTP), the process is suspended by the kernel without any terminal cleanup. Mouse tracking remains enabled, causing mouse events to appear as garbled escape sequences in the shell.Root Cause
opentui's
exitSignalslist includes SIGINT, SIGTERM, SIGQUIT, SIGABRT, SIGHUP, SIGBREAK, SIGPIPE, SIGBUS, SIGFPE — but no SIGTSTP.There is no
process.on("SIGTSTP")handler anywhere in the renderer. When SIGTSTP arrives:\x1b[?1000h,\x1b[?1003h,\x1b[?1006h) stays enabledThe Correct Pattern Already Exists
The manual
suspend()method does this correctly:But this is only called from consumer code (e.g., a keybind handler). An external SIGTSTP bypasses it entirely.
Proposed Fix
Register SIGTSTP/SIGCONT handlers in the renderer that call
suspend()/resume():Register in
setupTerminal(), remove incleanupBeforeDestroy().Note: In Node/Bun, registering a SIGTSTP handler overrides the default behavior (immediate suspend). The handler must remove itself and re-raise SIGTSTP to actually suspend. The SIGCONT handler re-registers it.
Reproduction
kill -TSTP $(pgrep -f <app>)35;89;19M35;84;20M35...Observed in opencode running in code-server (VS Code web terminal) where the terminal management layer can send SIGTSTP during inactivity.
Related