-
Notifications
You must be signed in to change notification settings - Fork 494
bug: mouse escape sequences garbled after renderer destroy — cleanupBeforeDestroy() disables raw mode before mouse tracking #904
Description
Description
When a renderer is destroyed (via destroy() → finalizeDestroy() → cleanupBeforeDestroy()), mouse escape sequences leak into the terminal as garbled text. This happens because cleanupBeforeDestroy() calls stdin.setRawMode(false) before mouse tracking is disabled.
Root Cause
In packages/core/src/renderer.ts, cleanupBeforeDestroy() (line ~2210-2213):
this.stdin.removeListener("data", this.stdinListener)
if (this.stdin.setRawMode) {
this.stdin.setRawMode(false) // ← ECHO re-enabled! Mouse still active!
}Later in finalizeDestroy(), this.lib.destroyRenderer(this.rendererPtr) sends mouse-disable sequences — but by then, setRawMode(false) has already re-enabled terminal ECHO. Any mouse events arriving between these two calls get echoed as raw bytes, appearing as garbled text like:
35;89;19M35;84;20M35;76;22M35;71;23M35;67;23M35;67;24M35...
The more complex the UI (i.e., the longer root.destroyRecursively() takes), the wider this garbling window becomes.
The Correct Pattern Already Exists
suspend() (line ~2052-2077) does this correctly:
this.disableMouse() // 1. mouse off (raw mode still on, no ECHO)
// ... cleanup ...
this.lib.suspendRenderer(...) // 2. native suspend
this.stdin.setRawMode(false) // 3. raw mode off LAST (safe, mouse already disabled)Proposed Fix
Add disableMouse() + stdin drain to cleanupBeforeDestroy(), matching the suspend() pattern:
// Disable mouse tracking before disabling raw mode to prevent
// mouse events from being echoed as garbled text
if (this._useMouse) {
this.disableMouse()
}
this.stdin.removeListener("data", this.stdinListener)
while (this.stdin.read() !== null) {} // drain buffered mouse events
if (this.stdin.setRawMode) {
this.stdin.setRawMode(false)
}This ensures:
- Mouse disable sequences (
\x1b[?1000l,\x1b[?1003l,\x1b[?1006l) are sent while raw mode is still on (no ECHO) - Stdin is drained of buffered mouse events before ECHO is re-enabled
- Then raw mode is safely disabled
Safety: disableMouse() accesses stdinParser and rendererPtr which are not yet destroyed at this point in the cleanup. The _useMouse guard skips the call if mouse was never enabled. The stdin drain pattern matches resume().
Reproduction
Observed in opencode (which uses @opentui/core@0.1.95):
- Run a TUI application with mouse tracking enabled
- Move the mouse while the TUI is active
- Exit the TUI (Ctrl+C, quit command, or any path that triggers
destroy()) - Move the mouse in the shell immediately after exit
- Garbled escape sequences appear in the terminal
Affects both @opentui/core@0.1.90 and @opentui/core@0.1.95.
Related
- opencode PR with temporary patch workaround: fix(tui): patch StdinParser to prevent garbled text from fragmented mouse sequences opencode#19520