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);