From 399fcac46c04025b693d844fb226a79b0882aa52 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Mon, 12 Oct 2026 07:56:37 +0200 Subject: [PATCH] core: keep parseKeypress from changing the caller's Buffer (#1653) `parseKeypress` decoded a single byte above 127 (the legacy 8-bit meta encoding) by subtracting 128 from the caller's Buffer in place. Parsing the same Buffer again returned the key without meta, and a caller that logs, retries, or forwards the raw bytes saw changed data. The renderer was not affected, because the stdin parser passes a fresh one-byte Buffer, so only direct callers of the exported function saw it. It now decodes the byte with `String.fromCharCode(byte - 128)` and leaves the Buffer alone. The returned key is the same as before for every input; no other code in `parseKeypress` or its callers writes into an input Buffer. The Buffer tests in `parse.keypress.test.ts` are now one table that checks the key and that the Buffer is byte for byte unchanged after the call: ASCII, a two-byte character, 127, 128, the high-bit meta bytes 160, 225, and 255, a Kitty sequence, and an SGR mouse report. Restoring the in-place write, decoding the wrong byte, or moving the bounds of the single-byte check each fail it. Fixes #1638 --- packages/core/src/lib/parse.keypress.test.ts | 54 +++++++++----------- packages/core/src/lib/parse.keypress.ts | 3 +- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/packages/core/src/lib/parse.keypress.test.ts b/packages/core/src/lib/parse.keypress.test.ts index 59862829..5cec0d67 100644 --- a/packages/core/src/lib/parse.keypress.test.ts +++ b/packages/core/src/lib/parse.keypress.test.ts @@ -887,36 +887,30 @@ test("parseKeypress - backspace key with modifiers (Kitty keyboard protocol)", ( expect(shiftBackspace.source).toBe("kitty") }) -test("parseKeypress - Buffer input", () => { - const buf = Buffer.from("a") - expect(parseKeypress(buf)).toEqual({ - eventType: "press", - name: "a", - ctrl: false, - meta: false, - shift: false, - option: false, - number: false, - sequence: "a", - raw: "a", - source: "raw", - }) -}) - -test("parseKeypress - high byte buffer handling", () => { - const buf = Buffer.from([160]) // 128 + 32, should become \x1b + " " - expect(parseKeypress(buf)).toEqual({ - eventType: "press", - name: "space", - ctrl: false, - meta: true, - shift: false, - option: false, - number: false, - sequence: "\x1b ", - raw: "\x1b ", - source: "raw", - }) +test("parseKeypress - Buffer input leaves the buffer unchanged", () => { + const press = { ctrl: false, shift: false, option: false, number: false, eventType: "press", source: "raw" } as const + const key = (name: string, sequence: string, meta = false) => ({ ...press, name, meta, sequence, raw: sequence }) + const kittyFlags = { super: false, hyper: false, capsLock: false, numLock: false } + const cases: Array<[Buffer, boolean, ParsedKey | null]> = [ + [Buffer.from("a"), false, key("a", "a")], + [Buffer.from("é"), false, key("é", "é")], // two bytes, the first above 127 + [Buffer.from([127]), false, key("backspace", "\x7f")], + [Buffer.from([128]), false, { ...key("space", "\x1b\x00", true), ctrl: true }], + [Buffer.from([160]), false, key("space", "\x1b ", true)], // 128 + 32 + [Buffer.from([225]), false, key("a", "\x1ba", true)], + [Buffer.from([255]), false, key("backspace", "\x1b\x7f", true)], + [ + Buffer.from("\x1b[97;5u"), + true, + { ...key("a", "a"), ...kittyFlags, ctrl: true, raw: "\x1b[97;5u", source: "kitty" }, + ], + [Buffer.from("\x1b[<0;1;1M"), false, null], + ] + for (const [buf, useKittyKeyboard, expected] of cases) { + const before = Buffer.from(buf) + expect(parseKeypress(buf, { useKittyKeyboard })).toEqual(expected) + expect(buf).toEqual(before) + } }) test("parseKeypress - empty input", () => { diff --git a/packages/core/src/lib/parse.keypress.ts b/packages/core/src/lib/parse.keypress.ts index 58d46ecf..ec4c7d9e 100644 --- a/packages/core/src/lib/parse.keypress.ts +++ b/packages/core/src/lib/parse.keypress.ts @@ -216,8 +216,7 @@ export const parseKeypress = (s: Buffer | string = "", options: ParseKeypressOpt if (Buffer.isBuffer(s)) { if (s[0]! > 127 && s[1] === undefined) { - ;(s[0] as unknown as number) -= 128 - s = "\x1b" + String(s) + s = "\x1b" + String.fromCharCode(s[0]! - 128) } else { s = String(s) }