fix(tui): balanced keyboard frames on emergency exit

- Popped the alternate-screen keyboard frame before restoring the main screen.
- Reset the main-screen keyboard state in live and blind emergency cleanup.
- Added regression coverage for both restore paths.

Fixes #6810
This commit is contained in:
roboomp
2026-07-27 14:23:27 +00:00
parent c60b6971db
commit 384394eea0
3 changed files with 32 additions and 9 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed emergency exits from fullscreen overlays leaving omp's main-screen Kitty keyboard frame active, which could corrupt Arrow Up input in the terminal pane after omp exited ([#6810](https://github.com/can1357/oh-my-pi/issues/6810)).
## [17.1.4] - 2026-07-26
### Fixed
+7 -9
View File
@@ -275,17 +275,15 @@ export function emergencyTerminalRestore(): void {
restoreTerminalStderr();
const terminal = activeTerminal;
if (terminal) {
terminal.stop();
// stop() never touches the alternate screen — the TUI owns that
// state and exits it on the normal shutdown path. Only crash paths
// with a fullscreen overlay still hold the alt buffer here. The
// leave sequence is gated on the tracked state because it is NOT a
// universally safe no-op: Windows' VT dispatcher homes the cursor
// on DECRST 1049 even when the alt buffer is inactive.
// Keyboard enhancement state is screen-local: pop the alt-screen
// frame before leaving it, then let stop() pop omp's main-screen frame.
if (altScreenActive) {
terminal.write("\x1b[?1049l");
const keyboardExit =
terminal.keyboardEnhancementExitSequence ?? (terminal.kittyEnableSequence ? "\x1b[<u" : "");
terminal.write(`${keyboardExit}\x1b[?1049l`);
altScreenActive = false;
}
terminal.stop();
terminal.showCursor();
} else if (terminalEverStarted && !isTerminalHeadless()) {
// Blind restore only if we know a terminal was started but lost track of it
@@ -305,7 +303,7 @@ export function emergencyTerminalRestore(): void {
// actually holds it — on Windows, DECRST 1049 on the main
// buffer homes the cursor (unconditional CursorRestoreState
// with no prior save), corrupting the shell handoff on exit.
(altScreenActive ? "\x1b[?1049l" : "") +
(altScreenActive ? "\x1b[?1049l\x1b[?1l\x1b>\x1b[<u" : "") + // Leave alt; reset main keyboard
"\x1b[?25h", // Show cursor
);
altScreenActive = false;
@@ -92,6 +92,8 @@ describe("emergencyTerminalRestore alt-screen gating", () => {
emergencyTerminalRestore();
const firstRestore = writes.join("");
expect(firstRestore).toContain("\x1b[?1049l");
const altExit = firstRestore.indexOf("\x1b[?1049l");
expect(firstRestore.indexOf("\x1b[<u", altExit + 1)).toBeGreaterThan(altExit);
expect(firstRestore).toContain("\x1b[?1006l");
expect(firstRestore).toContain("\x1b[?1003l");
expect(firstRestore).toContain("\x1b[?1000l");
@@ -122,4 +124,23 @@ describe("emergencyTerminalRestore alt-screen gating", () => {
expect(activeRestore).toContain("\x1b[?1003l");
expect(activeRestore).toContain("\x1b[?1000l");
});
it("pops keyboard enhancement frames on both screens when crashing from a fullscreen overlay", () => {
const { terminal, writes } = startCapturedTerminal();
process.stdin.emit("data", "\x1b[?0u");
expect(terminal.kittyEnableSequence).toBe("\x1b[>1u");
terminal.write(`\x1b[?1049h${terminal.kittyEnableSequence}`);
setAltScreenActive(true);
writes.length = 0;
emergencyTerminalRestore();
const restored = writes.join("");
const altPop = restored.indexOf("\x1b[<u");
const altExit = restored.indexOf("\x1b[?1049l");
const mainPop = restored.indexOf("\x1b[<u", altExit + 1);
expect(altPop).toBeGreaterThanOrEqual(0);
expect(altPop).toBeLessThan(altExit);
expect(mainPop).toBeGreaterThan(altExit);
});
});