Skip to content

Commit fe1a664

Browse files
committed
Reset terminal with escape sequences before exit always
1 parent f25f1bc commit fe1a664

File tree

1 file changed

+49
-0
lines changed

1 file changed

+49
-0
lines changed

cli/src/utils/renderer-cleanup.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,61 @@ import type { CliRenderer } from '@opentui/core'
22

33
let renderer: CliRenderer | null = null
44
let handlersInstalled = false
5+
let terminalStateReset = false
6+
7+
/**
8+
* Terminal escape sequences to reset terminal state.
9+
* These are written directly to stdout to ensure they're sent even if the renderer is in a bad state.
10+
*
11+
* Sequences:
12+
* - \x1b[?1000l: Disable X10 mouse mode
13+
* - \x1b[?1002l: Disable button event mouse mode
14+
* - \x1b[?1003l: Disable any-event mouse mode (all motion tracking)
15+
* - \x1b[?1006l: Disable SGR extended mouse mode
16+
* - \x1b[?1004l: Disable focus reporting
17+
* - \x1b[?2004l: Disable bracketed paste mode
18+
* - \x1b[?25h: Show cursor (safety measure)
19+
*/
20+
const TERMINAL_RESET_SEQUENCES =
21+
'\x1b[?1000l' + // Disable X10 mouse mode
22+
'\x1b[?1002l' + // Disable button event mouse mode
23+
'\x1b[?1003l' + // Disable any-event mouse mode (all motion)
24+
'\x1b[?1006l' + // Disable SGR extended mouse mode
25+
'\x1b[?1004l' + // Disable focus reporting
26+
'\x1b[?2004l' + // Disable bracketed paste mode
27+
'\x1b[?25h' // Show cursor
28+
29+
/**
30+
* Reset terminal state by writing escape sequences directly to stdout.
31+
* This is called BEFORE renderer.destroy() to ensure sequences are sent
32+
* even if the renderer is in a bad state.
33+
*
34+
* This is especially important on Windows where signals like SIGTERM and SIGHUP
35+
* don't work, so we rely on the 'exit' event which is guaranteed to run.
36+
*/
37+
function resetTerminalState(): void {
38+
if (terminalStateReset) return
39+
terminalStateReset = true
40+
41+
try {
42+
// Write directly to stdout - this is synchronous and will complete
43+
// before the process exits, ensuring the terminal is reset
44+
process.stdout.write(TERMINAL_RESET_SEQUENCES)
45+
} catch {
46+
// Ignore errors - stdout may already be closed
47+
}
48+
}
549

650
/**
751
* Clean up the renderer by calling destroy().
852
* This resets terminal state to prevent garbled output after exit.
953
*/
1054
function cleanup(): void {
55+
// FIRST: Reset terminal state by writing escape sequences directly to stdout.
56+
// This ensures mouse mode, focus reporting, etc. are disabled even if
57+
// renderer.destroy() fails or doesn't fully clean up.
58+
resetTerminalState()
59+
1160
if (renderer && !renderer.isDestroyed) {
1261
try {
1362
renderer.destroy()

0 commit comments

Comments
 (0)