From 9c0ba357adc9034e7071302e83b9b79d054aec8d Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 14 Mar 2026 03:58:28 +0100 Subject: [PATCH] fix(tui): distinguish ctrl+backspace from backspace on Windows Terminal 0x08 (BS) was treated as plain backspace alongside 0x7f (DEL). On Windows Terminal, Backspace sends 0x7f and Ctrl+Backspace sends 0x08, making ctrl+backspace bindings unreachable. Now 0x08 matches ctrl+backspace (and ctrl+h, same byte) but not plain backspace. 0x7f remains plain backspace. fixes #2139 --- packages/tui/CHANGELOG.md | 4 ++++ packages/tui/src/keys.ts | 11 +++++++++-- packages/tui/test/keys.test.ts | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 62d9f6d7..0a3979cf 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `ctrl+backspace` being indistinguishable from plain `backspace` on Windows Terminal. `0x08` is now recognized as `ctrl+backspace` instead of `backspace`, making `ctrl+backspace` bindable on terminals where it produces a distinct byte ([#2139](https://github.com/badlogic/pi-mono/issues/2139)) + ## [0.58.0] - 2026-03-14 ### Added diff --git a/packages/tui/src/keys.ts b/packages/tui/src/keys.ts index 5d107d5a..dfd0e9bc 100644 --- a/packages/tui/src/keys.ts +++ b/packages/tui/src/keys.ts @@ -846,8 +846,14 @@ export function matchesKey(data: string, keyId: KeyId): boolean { } return matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt); } + if (ctrl && !alt && !shift) { + // Legacy: 0x08 (BS) is sent by Windows Terminal for Ctrl+Backspace. + // Also matches Ctrl+H (same byte, ambiguous in legacy terminals). + if (data === "\x08") return true; + return matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.ctrl); + } if (modifier === 0) { - return data === "\x7f" || data === "\x08" || matchesKittySequence(data, CODEPOINTS.backspace, 0); + return data === "\x7f" || matchesKittySequence(data, CODEPOINTS.backspace, 0); } return matchesKittySequence(data, CODEPOINTS.backspace, modifier); @@ -1165,7 +1171,8 @@ export function parseKey(data: string): string | undefined { if (data === "\r" || (!_kittyProtocolActive && data === "\n") || data === "\x1bOM") return "enter"; if (data === "\x00") return "ctrl+space"; if (data === " ") return "space"; - if (data === "\x7f" || data === "\x08") return "backspace"; + if (data === "\x7f") return "backspace"; + if (data === "\x08") return "ctrl+backspace"; if (data === "\x1b[Z") return "shift+tab"; if (!_kittyProtocolActive && data === "\x1b\r") return "alt+enter"; if (!_kittyProtocolActive && data === "\x1b ") return "alt+space"; diff --git a/packages/tui/test/keys.test.ts b/packages/tui/test/keys.test.ts index 87926728..6a7e53ca 100644 --- a/packages/tui/test/keys.test.ts +++ b/packages/tui/test/keys.test.ts @@ -252,6 +252,20 @@ describe("matchesKey", () => { assert.strictEqual(parseKey("\x1b\x1f"), "ctrl+alt+-"); }); + it("should distinguish backspace (0x7f) from ctrl+backspace (0x08)", () => { + setKittyProtocolActive(false); + // 0x7f is plain backspace + assert.strictEqual(matchesKey("\x7f", "backspace"), true); + assert.strictEqual(matchesKey("\x7f", "ctrl+backspace"), false); + assert.strictEqual(parseKey("\x7f"), "backspace"); + // 0x08 is ctrl+backspace (Windows Terminal sends this for Ctrl+Backspace) + assert.strictEqual(matchesKey("\x08", "ctrl+backspace"), true); + assert.strictEqual(matchesKey("\x08", "backspace"), false); + assert.strictEqual(parseKey("\x08"), "ctrl+backspace"); + // 0x08 also matches ctrl+h (same byte, ambiguous in legacy terminals) + assert.strictEqual(matchesKey("\x08", "ctrl+h"), true); + }); + it("should parse legacy alt-prefixed sequences when kitty inactive", () => { setKittyProtocolActive(false); assert.strictEqual(matchesKey("\x1b ", "alt+space"), true);