fix(tui): requested base-layout keys for shortcuts

Requested Kitty flag 4 alongside escape-code disambiguation so Ghostty reports base-layout key codepoints for non-Latin keyboard layouts.

Added negotiation and shortcut-matching regression coverage.

Fixes #7320
This commit is contained in:
roboomp
2026-08-01 20:36:22 +00:00
parent d3ca0184de
commit 422aeeea49
6 changed files with 25 additions and 13 deletions
+1
View File
@@ -6,6 +6,7 @@
- Fixed animated Loader paints saturating a CPU core on slow WSL/ConPTY terminals by applying cost-aware cadence backpressure while preserving 30fps on cheap frames ([#7290](https://github.com/can1357/oh-my-pi/issues/7290)).
- Fixed interactive terminals suppressing all output and input when the host project sets `NODE_ENV=test` or `BUN_ENV=test` ([#7261](https://github.com/can1357/oh-my-pi/issues/7261)).
- Fixed Kitty/Ghostty shortcuts on non-Latin keyboard layouts by requesting base-layout key reporting from the terminal ([#7320](https://github.com/can1357/oh-my-pi/issues/7320)).
## [17.2.2] - 2026-07-31
+4 -4
View File
@@ -404,7 +404,7 @@ export interface Terminal {
// Whether Kitty keyboard protocol is active
get kittyProtocolActive(): boolean;
// The exact kitty keyboard push sequence in effect ("\x1b[>1u" or "\x1b[>7u"),
// The exact kitty keyboard push sequence in effect ("\x1b[>5u" or "\x1b[>7u"),
// or null when the protocol is not active. Kitty keyboard flags are per-screen,
// so the TUI re-pushes this after entering the alternate screen.
get kittyEnableSequence(): string | null;
@@ -1106,9 +1106,9 @@ export class ProcessTerminal implements Terminal {
this.#kittyEnableSeq = "\x1b[>7u";
this.#safeWrite(this.#kittyEnableSeq);
} else {
// Level 1 (disambiguate escape codes) — enough for Shift+Enter
// without the modifyOtherKeys fallback that caused regression #3259.
this.#kittyEnableSeq = "\x1b[>1u";
// Disambiguate escape codes and report base-layout keys for physical
// shortcut matching, without event reporting that caused regression #3259.
this.#kittyEnableSeq = "\x1b[>5u";
this.#safeWrite(this.#kittyEnableSeq);
}
return;
@@ -127,7 +127,7 @@ describe("emergencyTerminalRestore alt-screen gating", () => {
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");
expect(terminal.kittyEnableSequence).toBe("\x1b[>5u");
terminal.write(`\x1b[?1049h${terminal.kittyEnableSequence}`);
setAltScreenActive(true);
+11
View File
@@ -56,6 +56,17 @@ describe("matchesKey", () => {
expect(matchesKey(dvorakCtrlSlash, "ctrl+[")).toBe(false);
setKittyProtocolActive(false);
});
it("matches non-Latin shortcuts by their base-layout key", () => {
setKittyProtocolActive(true);
try {
expect(matchesKey("\x1b[1089::99;5u", "ctrl+c")).toBe(true);
expect(matchesKey("\x1b[1079::112;5u", "ctrl+p")).toBe(true);
expect(matchesKey("\x1b[1057::99;6u", "ctrl+shift+c")).toBe(true);
} finally {
setKittyProtocolActive(false);
}
});
it("ignores Kitty release events while still matching repeats", () => {
setKittyProtocolActive(true);
expect(matchesKey("\x1b[127u", "backspace")).toBe(true);
@@ -46,7 +46,7 @@ describe("ProcessTerminal kitty keyboard progressive-enhancement ordering", () =
Object.defineProperty(TERMINAL, "id", { value: originalTerminalId, configurable: true });
});
it("enables kitty when the kitty reply arrives before the DA1 sentinel", async () => {
it("requests alternate keys when the kitty reply arrives before the DA1 sentinel", async () => {
harness = createProcessTerminalRenderHarness(100, 30);
await harness.settle();
expect(harness.writes.join("")).toContain("\x1b[?u\x1b[c");
@@ -56,7 +56,7 @@ describe("ProcessTerminal kitty keyboard progressive-enhancement ordering", () =
const out = harness.writes.join("");
expect(harness.terminal.kittyProtocolActive).toBe(true);
expect(out).toContain("\x1b[>1u");
expect(out).toContain("\x1b[>5u");
expect(out).not.toContain("\x1b[>4;2m");
});
@@ -71,10 +71,10 @@ describe("ProcessTerminal kitty keyboard progressive-enhancement ordering", () =
const out = harness.writes.join("");
expect(harness.terminal.kittyProtocolActive).toBe(true);
expect(out).toContain("\x1b[>1u");
expect(out).toContain("\x1b[>5u");
const enableIdx = out.indexOf("\x1b[>4;2m");
const disableIdx = out.indexOf("\x1b[>4;0m");
const kittyIdx = out.indexOf("\x1b[>1u");
const kittyIdx = out.indexOf("\x1b[>5u");
expect(enableIdx).toBeGreaterThanOrEqual(0);
expect(disableIdx).toBeGreaterThan(enableIdx);
expect(kittyIdx).toBeGreaterThan(enableIdx);
@@ -92,7 +92,7 @@ describe("ProcessTerminal kitty keyboard progressive-enhancement ordering", () =
const out = harness.writes.join("");
expect(harness.terminal.kittyProtocolActive).toBe(false);
expect(out).toContain("\x1b[>4;2m");
expect(out).not.toContain("\x1b[>1u");
expect(out).not.toContain("\x1b[>5u");
});
it("skips modifyOtherKeys fallback for SSH_CONNECTION-only unknown terminals", async () => {
@@ -131,7 +131,7 @@ describe("ProcessTerminal kitty keyboard progressive-enhancement ordering", () =
const out = harness.writes.join("");
expect(harness.terminal.kittyProtocolActive).toBe(false);
expect(out).toContain("\x1b[>4;2m");
expect(out).not.toContain("\x1b[>1u");
expect(out).not.toContain("\x1b[>5u");
expect(harness.terminal.keyboardEnhancementEnterSequence).toBe("\x1b[>4;2m");
});
@@ -182,7 +182,7 @@ describe("ProcessTerminal kitty keyboard progressive-enhancement ordering", () =
margin: 0,
});
await harness.settle();
expect(harness.writes.join("")).toContain("\x1b[?1049h\x1b[>1u");
expect(harness.writes.join("")).toContain("\x1b[?1049h\x1b[>5u");
harness.writes.length = 0;
overlay.hide();
@@ -601,7 +601,7 @@ describe("ProcessTerminal OSC 11 appearance detection", () => {
// Simulate kitty-capable terminal reply (level >=1).
process.stdin.emit("data", "\x1b[?1u");
const pushes = writes.filter(w => w === "\x1b[>1u" || w === "\x1b[>7u" || w === "\x1b[>31u").length;
const pushes = writes.filter(w => w === "\x1b[>5u" || w === "\x1b[>7u" || w === "\x1b[>31u").length;
expect(pushes).toBe(1);
terminal.stop();