Skip to content

Commit f24db04

Browse files
authored
fix: intercept data events instead of keypress to fix Node.js v25 crash (#15)
The previous approach patched stdin.emit and re-emitted synthetic keypress events with partial key objects. In Node.js v25, readline's internal architecture changed such that synthetic keypress events route back through ReadStream.onData → emitKeys generator, crashing on incomplete key data. Fix by intercepting at the raw data layer: replace single-byte vim keys (j/k/g/G/q) with their terminal escape sequences before readline processes them. readline's emitKeys generator receives valid escape sequences and produces correct keypress events naturally. Fixes #14
1 parent 4914fca commit f24db04

1 file changed

Lines changed: 16 additions & 15 deletions

File tree

src/core/vim-keys.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
/**
22
* Vim keybindings support
3-
* Maps j/k/g/G/q to arrow keys and actions in clack menus.
3+
* Intercepts raw data events and replaces vim keys with terminal escape sequences
4+
* before readline processes them. This avoids patching the keypress layer which
5+
* breaks in Node.js v25+ (emitKeys generator crash).
46
*/
57

6-
type KeyEvent = { name: string; shift?: boolean; ctrl?: boolean; meta?: boolean; [k: string]: unknown };
7-
8-
const KEY_MAP: Array<{ match: (k: KeyEvent) => boolean; mapped: Partial<KeyEvent> }> = [
9-
{ match: k => k.name === 'j' && !k.ctrl && !k.meta, mapped: { name: 'down' } },
10-
{ match: k => k.name === 'k' && !k.ctrl && !k.meta, mapped: { name: 'up' } },
11-
{ match: k => k.name === 'g' && !k.shift && !k.ctrl && !k.meta, mapped: { name: 'home' } },
12-
{ match: k => k.name === 'g' && !!k.shift && !k.ctrl && !k.meta, mapped: { name: 'end' } },
13-
{ match: k => k.name === 'q' && !k.ctrl && !k.meta, mapped: { name: 'c', ctrl: true } },
14-
];
8+
// Maps single-byte vim keys to terminal escape sequences
9+
const VIM_TO_SEQ: Record<string, Buffer> = {
10+
j: Buffer.from('\u001b[B'), // down arrow
11+
k: Buffer.from('\u001b[A'), // up arrow
12+
g: Buffer.from('\u001b[H'), // home
13+
G: Buffer.from('\u001b[F'), // end
14+
q: Buffer.from('\u0003'), // ctrl+c (cancel)
15+
};
1516

1617
export function enableVimKeys(): void {
1718
const stdin = process.stdin;
@@ -20,11 +21,11 @@ export function enableVimKeys(): void {
2021
const originalEmit = stdin.emit.bind(stdin);
2122

2223
(stdin.emit as any) = function (event: string, ...args: any[]) {
23-
if (event === 'keypress') {
24-
const key: KeyEvent = args[1];
25-
if (key?.name) {
26-
const mapping = KEY_MAP.find(m => m.match(key));
27-
if (mapping) return originalEmit('keypress', null, mapping.mapped);
24+
if (event === 'data') {
25+
const chunk = args[0];
26+
if (Buffer.isBuffer(chunk) && chunk.length === 1) {
27+
const seq = VIM_TO_SEQ[String.fromCharCode(chunk[0] as number)];
28+
if (seq) return originalEmit('data', seq);
2829
}
2930
}
3031
return originalEmit(event, ...args);

0 commit comments

Comments
 (0)