Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 63 additions & 4 deletions packages/core/src/lib/parse.keypress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests should probably use the same method as parse.keypress if it's not mocking anything?

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",
Expand Down Expand Up @@ -111,19 +118,34 @@ test("parseKeypress - special keys", () => {
source: "raw",
})

expect(parseKeypress("\b")).toEqual({
expect(parseKeypress("\x7f")).toEqual({
eventType: "press",
name: "backspace",
ctrl: false,
meta: false,
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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
33 changes: 29 additions & 4 deletions packages/core/src/lib/parse.keypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
Expand Down
Loading