fix(tui): reset cursor-key and keypad mode on terminal teardown
omp took full ownership of the TTY and emitted a comprehensive private-mode reset menu on teardown, but never restored normal cursor-key (DECCKM) or numeric-keypad mode (terminfo `rmkx` = `\x1b[?1l\x1b>`). The native key decoder accepts both CSI (`\x1b[A`) and SS3 (`\x1bOA`) arrow encodings, so omp never enabled application mode itself and never bothered to reset it -- but a program that shares the TTY (or inherited state) could leave the terminal in application-cursor-keys mode. Arrows then arrived as SS3 and the parent shell's Up/Down history navigation stayed broken after omp exited. Emit `rmkx` in ProcessTerminal.start() (normalize inherited state so arrows use the predictable CSI form inside omp), ProcessTerminal.stop(), and the blind branch of emergencyTerminalRestore() (restore normal mode so the shell handoff is sane), symmetric with every other terminal mode omp manages. Fixes #6374
This commit is contained in:
@@ -312,6 +312,7 @@ export function emergencyTerminalRestore(): void {
|
||||
process.stdout.write(
|
||||
"\x1b[?2026l" + // End synchronized output
|
||||
"\x1b[?7h" + // Restore autowrap
|
||||
"\x1b[?1l\x1b>" + // Restore normal cursor-key + keypad mode (rmkx, #6374)
|
||||
"\x1b[?2004l" + // Disable bracketed paste
|
||||
"\x1b[?2031l" + // Disable Mode 2031 appearance notifications
|
||||
"\x1b[?2048l" + // Disable in-band resize notifications
|
||||
@@ -625,6 +626,15 @@ export class ProcessTerminal implements Terminal {
|
||||
// Enable bracketed paste mode - terminal will wrap pastes in \x1b[200~ ... \x1b[201~
|
||||
this.#safeWrite("\x1b[?2004h");
|
||||
|
||||
// Force normal cursor-key (DECCKM) and numeric-keypad mode (terminfo
|
||||
// `rmkx` = "\x1b[?1l\x1b>"). omp decodes both CSI ("\x1b[A") and SS3
|
||||
// ("\x1bOA") arrow encodings, so it never enables application mode
|
||||
// itself — but a prior program that left the TTY in application-cursor-
|
||||
// keys mode makes arrows arrive as SS3. Normalizing on entry keeps input
|
||||
// in the predictable default state; stop() restores the same on exit.
|
||||
// See #6374.
|
||||
this.#safeWrite("\x1b[?1l\x1b>");
|
||||
|
||||
// Set up resize handler immediately. The OS refreshes process.stdout
|
||||
// dimensions before firing `resize`, so it is authoritative for geometry:
|
||||
// reconcile any stale cached DEC 2048 report before notifying the renderer.
|
||||
@@ -1376,6 +1386,13 @@ export class ProcessTerminal implements Terminal {
|
||||
// begin/end halves of a frame. Safe no-ops on terminals that ignored them.
|
||||
this.#safeWrite("\x1b[?2026l\x1b[?7h");
|
||||
|
||||
// Restore normal cursor-key (DECCKM) and numeric-keypad mode (terminfo
|
||||
// `rmkx`). Symmetric with the normalize in start(): a TTY-sharing child
|
||||
// can leave the terminal in application-cursor-keys mode, and without
|
||||
// this reset the parent shell inherits SS3 arrows so Up/Down history
|
||||
// navigation stays broken after omp exits (#6374).
|
||||
this.#safeWrite("\x1b[?1l\x1b>");
|
||||
|
||||
// Disable bracketed paste mode
|
||||
this.#safeWrite("\x1b[?2004l");
|
||||
this.#safeWrite("\x1b[?5522l");
|
||||
|
||||
@@ -93,4 +93,30 @@ describe("ProcessTerminal headless suppression", () => {
|
||||
setTerminalHeadless(previous);
|
||||
}
|
||||
});
|
||||
|
||||
// #6374: arrows stopped working inside omp and stayed broken in the shell
|
||||
// after exit — a missing cursor-key/keypad reset. omp owns the TTY and emits
|
||||
// a full private-mode reset menu, but never restored normal cursor-key
|
||||
// (DECCKM) / numeric-keypad mode (terminfo `rmkx` = "\x1b[?1l\x1b>"). If the
|
||||
// terminal was left in application-cursor-keys mode, arrows arrived as SS3
|
||||
// and the parent shell's Up/Down history navigation broke. start() must
|
||||
// normalize the state and stop() must restore it.
|
||||
it("emits rmkx on start and stop to normalize/restore cursor-key + keypad mode (#6374)", () => {
|
||||
const previous = setTerminalHeadless(false);
|
||||
const terminal = new ProcessTerminal();
|
||||
try {
|
||||
terminal.start(
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
expect(writes.join("")).toContain("\x1b[?1l\x1b>");
|
||||
|
||||
writes.length = 0;
|
||||
terminal.stop();
|
||||
expect(writes.join("")).toContain("\x1b[?1l\x1b>");
|
||||
} finally {
|
||||
terminal.stop();
|
||||
setTerminalHeadless(previous);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user