refactor(tui): split keyboard enhancement into enter/exit pair
Renamed the buffer-switch helper to keyboardEnhancementEnterSequence and added a sibling keyboardEnhancementExitSequence so callers can balance push/pop generically instead of hard-coding the kitty pop. ProcessTerminal returns the matching kitty pop on exit when kitty is active, and null when only the xterm modifyOtherKeys fallback is active: modifyOtherKeys is a global terminal flag with no per-screen stack, so emitting >4;0m on overlay exit would clear it on the normal screen and break composer typing between overlays. terminal.stop() and the emergency-restore path still disable it globally on graceful teardown. TUI alt-screen entry/exit and resize alt-screen helpers now route through the new pair with fallbacks to kittyEnableSequence for custom Terminal implementations predating the new properties. Fixes #3705
This commit is contained in:
@@ -336,10 +336,15 @@ export interface Terminal {
|
||||
// so the TUI re-pushes this after entering the alternate screen.
|
||||
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. Optional so
|
||||
// custom Terminals built against older pi-tui versions keep working.
|
||||
readonly keyboardEnhancementSequence?: string | null;
|
||||
// The active modified-key reporting sequence to reassert on alternate-screen
|
||||
// entry, or null when no enhanced keyboard mode is active. Optional so custom
|
||||
// Terminals built against older pi-tui versions keep working.
|
||||
readonly keyboardEnhancementEnterSequence?: string | null;
|
||||
|
||||
// The sequence that cleanly disables the active enhanced keyboard mode on
|
||||
// alternate-screen exit, or null when no exit handshake is required. Optional
|
||||
// so custom Terminals built against older pi-tui versions keep working.
|
||||
readonly keyboardEnhancementExitSequence?: string | null;
|
||||
|
||||
// Cursor positioning (relative to current position)
|
||||
moveBy(lines: number): void; // Move cursor up (negative) or down (positive) by N lines
|
||||
@@ -477,11 +482,20 @@ export class ProcessTerminal implements Terminal {
|
||||
return this.#kittyProtocolActive ? this.#kittyEnableSeq : null;
|
||||
}
|
||||
|
||||
get keyboardEnhancementSequence(): string | null {
|
||||
get keyboardEnhancementEnterSequence(): string | null {
|
||||
if (this.#kittyProtocolActive) return this.#kittyEnableSeq;
|
||||
return this.#modifyOtherKeysActive ? "\x1b[>4;2m" : null;
|
||||
}
|
||||
|
||||
get keyboardEnhancementExitSequence(): string | null {
|
||||
// kitty is a stack push (per-screen), so the matching pop balances alt-screen
|
||||
// entry. xterm modifyOtherKeys is a single global flag with no per-screen
|
||||
// stack — emitting `>4;0m` here would clear it on the normal screen too,
|
||||
// breaking the composer between overlays. terminal.stop() still disables it
|
||||
// globally on graceful exit; the emergency-restore path mirrors that.
|
||||
return this.#kittyProtocolActive ? "\x1b[<u" : null;
|
||||
}
|
||||
|
||||
get appearance(): TerminalAppearance | undefined {
|
||||
return this.#appearance;
|
||||
}
|
||||
|
||||
+26
-11
@@ -1745,8 +1745,8 @@ export class TUI extends Container {
|
||||
this.terminal.write(this.#leaveResizeAltSequence());
|
||||
}
|
||||
if (this.#altActive) {
|
||||
const kittyPop = this.terminal.kittyEnableSequence ? "\x1b[<u" : "";
|
||||
this.terminal.write(`${MOUSE_TRACKING_OFF}${kittyPop}\x1b[?1049l`);
|
||||
const enhancementExit = this.#keyboardEnhancementExit();
|
||||
this.terminal.write(`${MOUSE_TRACKING_OFF}${enhancementExit}\x1b[?1049l`);
|
||||
setAltScreenActive(false);
|
||||
this.#altActive = false;
|
||||
this.#altPreviousLines = [];
|
||||
@@ -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.#keyboardEnhancementSequence()}${MOUSE_TRACKING_ON}`);
|
||||
this.terminal.write(`\x1b[?1049h${this.#keyboardEnhancementEnter()}${MOUSE_TRACKING_ON}`);
|
||||
setAltScreenActive(true);
|
||||
this.terminal.hideCursor();
|
||||
this.#forgetHardwareCursorState();
|
||||
@@ -2502,8 +2502,8 @@ export class TUI extends Container {
|
||||
this.#altEnterWidth = width;
|
||||
this.#altEnterHeight = height;
|
||||
} else if (!wantAlt && this.#altActive) {
|
||||
const kittyPop = this.terminal.kittyEnableSequence ? "\x1b[<u" : "";
|
||||
this.terminal.write(`${MOUSE_TRACKING_OFF}${kittyPop}\x1b[?1049l`);
|
||||
const enhancementExit = this.#keyboardEnhancementExit();
|
||||
this.terminal.write(`${MOUSE_TRACKING_OFF}${enhancementExit}\x1b[?1049l`);
|
||||
setAltScreenActive(false);
|
||||
this.#forgetHardwareCursorState();
|
||||
this.#altActive = false;
|
||||
@@ -3339,9 +3339,24 @@ export class TUI extends Container {
|
||||
return { window: this.#prepareLinesArray(window, width), contentRows: count };
|
||||
}
|
||||
|
||||
/** Enter or leave the alternate screen borrowed for transient resize frames. */
|
||||
#keyboardEnhancementSequence(): string {
|
||||
return this.terminal.keyboardEnhancementSequence ?? this.terminal.kittyEnableSequence ?? "";
|
||||
/**
|
||||
* Resolve the active keyboard-enhancement enter sequence. Falls back to the
|
||||
* legacy `kittyEnableSequence` when a custom Terminal predates the
|
||||
* `keyboardEnhancementEnterSequence` property.
|
||||
*/
|
||||
#keyboardEnhancementEnter(): string {
|
||||
return this.terminal.keyboardEnhancementEnterSequence ?? this.terminal.kittyEnableSequence ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the active keyboard-enhancement exit sequence. Falls back to popping
|
||||
* kitty whenever a custom Terminal exposes its push sequence but predates the
|
||||
* `keyboardEnhancementExitSequence` property.
|
||||
*/
|
||||
#keyboardEnhancementExit(): string {
|
||||
const exit = this.terminal.keyboardEnhancementExitSequence;
|
||||
if (exit !== undefined) return exit ?? "";
|
||||
return this.terminal.kittyEnableSequence ? "\x1b[<u" : "";
|
||||
}
|
||||
|
||||
#enterResizeAltSequence(): string {
|
||||
@@ -3350,16 +3365,16 @@ export class TUI extends Container {
|
||||
setAltScreenActive(true);
|
||||
this.#forgetHardwareCursorState();
|
||||
this.#recordHardwareCursorHidden();
|
||||
return `${ALT_SCREEN_ENTER}${this.#keyboardEnhancementSequence()}`;
|
||||
return `${ALT_SCREEN_ENTER}${this.#keyboardEnhancementEnter()}`;
|
||||
}
|
||||
|
||||
#leaveResizeAltSequence(): string {
|
||||
if (!this.#resizeAltActive) return "";
|
||||
const kittyPop = this.terminal.kittyEnableSequence ? "\x1b[<u" : "";
|
||||
const enhancementExit = this.#keyboardEnhancementExit();
|
||||
this.#resizeAltActive = false;
|
||||
setAltScreenActive(false);
|
||||
this.#forgetHardwareCursorState();
|
||||
return `${kittyPop}${ALT_SCREEN_EXIT}`;
|
||||
return `${enhancementExit}${ALT_SCREEN_EXIT}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,7 +28,11 @@ class CaptureTerminal implements Terminal {
|
||||
return null;
|
||||
}
|
||||
|
||||
get keyboardEnhancementSequence(): string | null {
|
||||
get keyboardEnhancementEnterSequence(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
get keyboardEnhancementExitSequence(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,8 +92,45 @@ describe("ProcessTerminal kitty keyboard progressive-enhancement ordering", () =
|
||||
});
|
||||
await harness.settle();
|
||||
|
||||
const out = harness.writes.join("");
|
||||
expect(out).toContain("\x1b[?1049h\x1b[>4;2m");
|
||||
const enterOut = harness.writes.join("");
|
||||
expect(enterOut).toContain("\x1b[?1049h\x1b[>4;2m");
|
||||
harness.writes.length = 0;
|
||||
|
||||
overlay.hide();
|
||||
await harness.settle();
|
||||
|
||||
const exitOut = harness.writes.join("");
|
||||
// xterm modifyOtherKeys is a single global flag (no per-screen stack),
|
||||
// so the overlay exit must NOT emit `>4;0m` — that would clear it on the
|
||||
// normal screen and break the composer between overlays. Only the kitty
|
||||
// pop is per-screen and safe to emit on exit.
|
||||
expect(exitOut).toContain("\x1b[?1049l");
|
||||
expect(exitOut).not.toContain("\x1b[>4;0m");
|
||||
});
|
||||
|
||||
it("pops the kitty keyboard frame on fullscreen overlay exit", async () => {
|
||||
harness = createProcessTerminalRenderHarness(100, 30);
|
||||
await harness.settle();
|
||||
await harness.feed("\x1b[?0u", "\x1b[?1;2c");
|
||||
expect(harness.terminal.kittyProtocolActive).toBe(true);
|
||||
harness.writes.length = 0;
|
||||
|
||||
const overlay = harness.tui.showOverlay(new ModalProbe(), {
|
||||
fullscreen: true,
|
||||
width: "100%",
|
||||
maxHeight: "100%",
|
||||
margin: 0,
|
||||
});
|
||||
await harness.settle();
|
||||
expect(harness.writes.join("")).toContain("\x1b[?1049h\x1b[>1u");
|
||||
harness.writes.length = 0;
|
||||
|
||||
overlay.hide();
|
||||
await harness.settle();
|
||||
|
||||
const exitOut = harness.writes.join("");
|
||||
// Kitty keyboard flags are per-screen, so the matching pop must precede
|
||||
// the alt-screen exit to balance the push from overlay entry.
|
||||
expect(exitOut).toContain("\x1b[<u\x1b[?1049l");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,8 @@ class MinimalTerminal implements Terminal {
|
||||
rows = 24;
|
||||
kittyProtocolActive = false;
|
||||
kittyEnableSequence: string | null = null;
|
||||
keyboardEnhancementSequence: string | null = null;
|
||||
keyboardEnhancementEnterSequence: string | null = null;
|
||||
keyboardEnhancementExitSequence: string | null = null;
|
||||
appearance: TerminalAppearance | undefined;
|
||||
#onInput: ((data: string) => void) | undefined;
|
||||
#onResize: (() => void) | undefined;
|
||||
|
||||
@@ -118,7 +118,11 @@ class CountingViewportTerminal extends VirtualTerminal {
|
||||
}
|
||||
|
||||
class LegacyKeyboardVirtualTerminal extends VirtualTerminal {
|
||||
get keyboardEnhancementSequence(): string | null {
|
||||
get keyboardEnhancementEnterSequence(): string | null {
|
||||
return undefined as unknown as string | null;
|
||||
}
|
||||
|
||||
get keyboardEnhancementExitSequence(): string | null {
|
||||
return undefined as unknown as string | null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,10 +208,14 @@ export class VirtualTerminal implements Terminal {
|
||||
return "\x1b[>1u";
|
||||
}
|
||||
|
||||
get keyboardEnhancementSequence(): string | null {
|
||||
get keyboardEnhancementEnterSequence(): string | null {
|
||||
return "\x1b[>1u";
|
||||
}
|
||||
|
||||
get keyboardEnhancementExitSequence(): string | null {
|
||||
return "\x1b[<u";
|
||||
}
|
||||
|
||||
get appearance(): TerminalAppearance | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user