diff --git a/packages/core/src/lib/parse.keypress.test.ts b/packages/core/src/lib/parse.keypress.test.ts index 670568201..4298c82de 100644 --- a/packages/core/src/lib/parse.keypress.test.ts +++ b/packages/core/src/lib/parse.keypress.test.ts @@ -2,6 +2,13 @@ import { test, expect } from "bun:test" import { parseKeypress, nonAlphanumericKeys, type ParsedKey, type KeyEventType } from "./parse.keypress" import { Buffer } from "node:buffer" +const isWindowsTerminal = (): boolean => { + if (process.platform === "win32") return true + if (process.env.WSL_DISTRO_NAME) return true + if (process.env.WSL_INTEROP) return true + return false +} + test("parseKeypress - basic letters", () => { expect(parseKeypress("a")).toEqual({ name: "a", @@ -111,7 +118,7 @@ test("parseKeypress - special keys", () => { source: "raw", }) - expect(parseKeypress("\b")).toEqual({ + expect(parseKeypress("\x7f")).toEqual({ eventType: "press", name: "backspace", ctrl: false, @@ -119,11 +126,26 @@ test("parseKeypress - special keys", () => { shift: false, option: false, number: false, - sequence: "\b", - raw: "\b", + sequence: "\x7f", + raw: "\x7f", source: "raw", }) + const backspaceB = parseKeypress("\b")! + expect(backspaceB.ctrl).toBe(true) + expect(backspaceB.meta).toBe(false) + expect(backspaceB.shift).toBe(false) + expect(backspaceB.option).toBe(false) + expect(backspaceB.number).toBe(false) + expect(backspaceB.sequence).toBe("\b") + expect(backspaceB.raw).toBe("\b") + expect(backspaceB.source).toBe("raw") + if (isWindowsTerminal()) { + expect(backspaceB.name).toBe("backspace") + } else { + expect(backspaceB.name).toBe("h") + } + expect(parseKeypress("\x1b")).toEqual({ name: "escape", ctrl: false, @@ -752,6 +774,38 @@ test("parseKeypress - backspace key with modifiers (modifyOtherKeys format)", () expect(metaBackspace.shift).toBe(false) }) +test("parseKeypress - raw backspace sequences (platform-aware)", () => { + const ctrlHOrBackspace = parseKeypress("\b")! + if (isWindowsTerminal()) { + expect(ctrlHOrBackspace.name).toBe("backspace") + expect(ctrlHOrBackspace.ctrl).toBe(true) + } else { + expect(ctrlHOrBackspace.name).toBe("h") + expect(ctrlHOrBackspace.ctrl).toBe(true) + } + expect(ctrlHOrBackspace.meta).toBe(false) + + const altCtrlHOrBackspace = parseKeypress("\x1b\b")! + if (isWindowsTerminal()) { + expect(altCtrlHOrBackspace.name).toBe("backspace") + expect(altCtrlHOrBackspace.ctrl).toBe(true) + } else { + expect(altCtrlHOrBackspace.name).toBe("h") + expect(altCtrlHOrBackspace.ctrl).toBe(true) + } + expect(altCtrlHOrBackspace.meta).toBe(true) + + const regularBackspace = parseKeypress("\x7f")! + expect(regularBackspace.name).toBe("backspace") + expect(regularBackspace.ctrl).toBe(false) + expect(regularBackspace.meta).toBe(false) + + const altBackspace = parseKeypress("\x1b\x7f")! + expect(altBackspace.name).toBe("backspace") + expect(altBackspace.ctrl).toBe(false) + expect(altBackspace.meta).toBe(true) +}) + test("parseKeypress - backspace key with modifiers (Kitty keyboard protocol)", () => { // Backspace key in Kitty protocol uses code 127 // Ctrl+Backspace: \x1b[127;5u @@ -1419,7 +1473,12 @@ test("parseKeypress - does not filter valid key sequences that might look simila const backspace = parseKeypress("\b") expect(backspace).not.toBeNull() - expect(backspace?.name).toBe("backspace") + if (isWindowsTerminal()) { + expect(backspace?.name).toBe("backspace") + } else { + expect(backspace?.name).toBe("h") + } + expect(backspace?.ctrl).toBe(true) const backspace2 = parseKeypress("\x7f") expect(backspace2).not.toBeNull() diff --git a/packages/core/src/lib/parse.keypress.ts b/packages/core/src/lib/parse.keypress.ts index a11f91cf3..fd11a6a11 100644 --- a/packages/core/src/lib/parse.keypress.ts +++ b/packages/core/src/lib/parse.keypress.ts @@ -106,6 +106,20 @@ const isCtrlKey = (code: string) => { return ["Oa", "Ob", "Oc", "Od", "Oe", "[2^", "[3^", "[5^", "[6^", "[7^", "[8^"].includes(code) } +/** + * Detect if running on Windows or in WSL (Windows Subsystem for Linux). + * In WSL, process.platform returns 'linux' but the terminal emulator (Windows Terminal) + * still sends Windows-style key sequences like \b for Ctrl+Backspace. + */ +const isWindowsTerminal = (): boolean => { + if (process.platform === "win32") return true + // Check for WSL via environment variable (more reliable than reading /proc/version) + if (process.env.WSL_DISTRO_NAME) return true + // Also check for WSL_INTEROP which is set in WSL2 + if (process.env.WSL_INTEROP) return true + return false +} + export type KeyEventType = "press" | "repeat" | "release" export interface ParsedKey { @@ -267,11 +281,22 @@ export const parseKeypress = (s: Buffer | string = "", options: ParseKeypressOpt } else if (s === "\t") { // tab key.name = "tab" - } else if (s === "\b" || s === "\x1b\b" || s === "\x7f" || s === "\x1b\x7f") { - // backspace or ctrl+h - // On OSX, \x7f is also backspace + } else if (s === "\x7f" || s === "\x1b\x7f") { + // Regular backspace (\x7f = ASCII 127 DEL) + // On macOS and most terminals, regular Backspace sends \x7f key.name = "backspace" - key.meta = s.charAt(0) === "\x1b" + key.meta = s.length === 2 // \x1b\x7f = Alt+Backspace + } else if (s === "\b" || s === "\x1b\b") { + // \b (ASCII 8): Windows Terminal sends this for Ctrl+Backspace, Unix terminals for Ctrl+H + if (isWindowsTerminal()) { + key.name = "backspace" + key.ctrl = true + key.meta = s.length === 2 + } else { + key.name = "h" + key.ctrl = true + key.meta = s.length === 2 + } } else if (s === "\x1b" || s === "\x1b\x1b") { // escape key key.name = "escape"