From 78c4a29cbed062dd8dba8570e555bb8d0ce8edc2 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sun, 28 Jun 2026 06:18:50 +0000 Subject: [PATCH] fix(tui): preserved legacy terminal keyboard fallback Kept Terminal.keyboardEnhancementSequence optional and fell back to kittyEnableSequence when custom Terminal implementations do not expose the new property. Covered the legacy custom Terminal path in the fullscreen overlay regression tests. Fixes #3705 --- packages/tui/src/terminal.ts | 5 +-- packages/tui/src/tui.ts | 8 +++-- packages/tui/test/render-regressions.test.ts | 32 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index c7cfbc155..2465c76e6 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -337,8 +337,9 @@ export interface Terminal { get kittyEnableSequence(): string | null; // The active modified-key reporting sequence to reassert after terminal buffer - // switches, or null when no enhanced keyboard mode is active. - get keyboardEnhancementSequence(): string | null; + // switches, or null when no enhanced keyboard mode is active. Optional so + // custom Terminals built against older pi-tui versions keep working. + readonly keyboardEnhancementSequence?: string | null; // Cursor positioning (relative to current position) moveBy(lines: number): void; // Move cursor up (negative) or down (positive) by N lines diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 28c4eafaa..cb7b69887 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -2492,7 +2492,7 @@ export class TUI extends Container { // modified-key reporting sequence on the freshly entered alternate // screen, or Esc/modified keys revert to legacy encoding inside // fullscreen overlays (Ghostty/kitty/iTerm2). - this.terminal.write(`\x1b[?1049h${this.terminal.keyboardEnhancementSequence ?? ""}${MOUSE_TRACKING_ON}`); + this.terminal.write(`\x1b[?1049h${this.#keyboardEnhancementSequence()}${MOUSE_TRACKING_ON}`); setAltScreenActive(true); this.terminal.hideCursor(); this.#forgetHardwareCursorState(); @@ -3340,13 +3340,17 @@ export class TUI extends Container { } /** Enter or leave the alternate screen borrowed for transient resize frames. */ + #keyboardEnhancementSequence(): string { + return this.terminal.keyboardEnhancementSequence ?? this.terminal.kittyEnableSequence ?? ""; + } + #enterResizeAltSequence(): string { if (this.#resizeAltActive || this.#altActive) return ""; this.#resizeAltActive = true; setAltScreenActive(true); this.#forgetHardwareCursorState(); this.#recordHardwareCursorHidden(); - return `${ALT_SCREEN_ENTER}${this.terminal.keyboardEnhancementSequence ?? ""}`; + return `${ALT_SCREEN_ENTER}${this.#keyboardEnhancementSequence()}`; } #leaveResizeAltSequence(): string { diff --git a/packages/tui/test/render-regressions.test.ts b/packages/tui/test/render-regressions.test.ts index 8d45683ca..e9e127225 100644 --- a/packages/tui/test/render-regressions.test.ts +++ b/packages/tui/test/render-regressions.test.ts @@ -117,6 +117,12 @@ class CountingViewportTerminal extends VirtualTerminal { } } +class LegacyKeyboardVirtualTerminal extends VirtualTerminal { + get keyboardEnhancementSequence(): string | null { + return undefined as unknown as string | null; + } +} + function rows(prefix: string, count: number): string[] { return Array.from({ length: count }, (_v, i) => `${prefix}${i}`); } @@ -3281,6 +3287,32 @@ describe("TUI terminal-state regressions", () => { } }); + it("falls back to kittyEnableSequence for legacy custom terminals", async () => { + const term = new LegacyKeyboardVirtualTerminal(40, 8, 200); + const writes = captureWrites(term); + const tui = new TUI(term); + tui.addChild(new MutableLinesComponent(rows("base-", 8))); + + try { + tui.start(); + await settle(term); + + const showFrom = writes.length; + tui.showOverlay(new MutableLinesComponent(["MODAL-0"]), { + width: "100%", + maxHeight: "100%", + margin: 0, + fullscreen: true, + }); + await settle(term); + + const modalWrites = writes.slice(showFrom).join(""); + expect(modalWrites).toContain("\x1b[?1049h\x1b[>1u"); + } finally { + tui.stop(); + } + }); + it("leaves native scrollback untouched across the modal lifetime", async () => { const term = new VirtualTerminal(40, 6, 200); const tui = new TUI(term);