Merge PR #6812: fix(tui): balance keyboard frames on emergency exit (@roboomp)
# Conflicts: # packages/tui/src/terminal.ts
This commit is contained in:
@@ -24,6 +24,9 @@
|
||||
- Fixed the multi-line prompt editor bypassing the keybindings registry for word/line delete and yank: `ctrl+backspace` (a declared default of `tui.editor.deleteWordBackward`) never fired and `keybindings.yml` remaps of `deleteWordBackward`, `deleteWordForward`, `deleteToLineStart`, `deleteToLineEnd`, `yank`, and `yankPop` were ignored, because those actions were matched with hardcoded chords instead of `keybindings.matches(...)` like cursor motion and the single-line `Input` already do ([#6782](https://github.com/can1357/oh-my-pi/issues/6782)).
|
||||
- Restored the Windows Terminal raw `0x08` → `ctrl+backspace` disambiguation (`WT_SESSION` set, `SSH_*` unset) by routing the exported `matchesRawBackspace` helper through the `matchesKey`/`parseKey` seam. Remote SSH/container sessions where terminal identity is unavailable can opt in with `PI_TUI_RAW_BACKSPACE_IS_CTRL=1` ([#6782](https://github.com/can1357/oh-my-pi/issues/6782)).
|
||||
- Fixed plain Backspace deleting a whole word inside tmux/GNU screen/Zellij panes launched from Windows Terminal: multiplexers inherit `WT_SESSION` but emit raw `0x08` for plain Backspace, so the automatic raw-backspace → `ctrl+backspace` heuristic misfired. The heuristic now skips multiplexer sessions (`TMUX`/`STY`/`ZELLIJ` or `TERM` starting with `tmux`/`screen`); `PI_TUI_RAW_BACKSPACE_IS_CTRL=1` remains the explicit opt-in everywhere ([#6784](https://github.com/can1357/oh-my-pi/pull/6784)).
|
||||
### 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
|
||||
|
||||
|
||||
@@ -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(true);
|
||||
} 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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user