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:
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user