Skip to content

Commit efde004

Browse files
fix: use dynamic modifier parsing for Shift+Enter recognition
Replace exact string matching in SHIFT_RETURN_SEQUENCES with CSI parameter parsing that checks modifier bits. Windows Terminal sends ESC[13;130u (modifier=128+2) where 128 is a terminal-specific flag — the old code only matched modifier=2 exactly. Also enable Kitty progressive enhancement (ESC[>1u) alongside xterm modifyOtherKeys, since Windows Terminal requires the Kitty protocol to report modified keys.
1 parent bbc1ed5 commit efde004

3 files changed

Lines changed: 52 additions & 10 deletions

File tree

src/tests/promptInputKeys.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ test("parseTerminalInput recognizes alternate shifted return sequences", () => {
108108
});
109109

110110
test("terminal extended key helpers request and restore modifyOtherKeys mode", () => {
111-
assert.equal(enableTerminalExtendedKeys(), "\u001B[>4;1m");
112-
assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m");
111+
assert.equal(enableTerminalExtendedKeys(), "\u001B[>4;1m\u001B[>1u");
112+
assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m\u001B[<u");
113113
});
114114

115115
test("parseTerminalInput recognizes terminal focus events", () => {

src/ui/prompt/cursor.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@ function disableTerminalFocusReporting(): string {
4040
return "\u001B[?1004l";
4141
}
4242

43+
// xterm modifyOtherKeys + Kitty progressive enhancement.
44+
// Both are needed: some terminals (incl. Windows Terminal) only respond to Kitty.
4345
export function enableTerminalExtendedKeys(): string {
44-
return "\u001B[>4;1m";
46+
return "\u001B[>4;1m\u001B[>1u";
4547
}
4648

4749
export function disableTerminalExtendedKeys(): string {
48-
return "\u001B[>4;0m";
50+
return "\u001B[>4;0m\u001B[<u";
4951
}
5052

5153
export function getPromptCursorPlacement(

src/ui/prompt/useTerminalInput.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,55 @@ const BACKSPACE_BYTES = new Set(["\u007F", "\b"]);
2626
const FORWARD_DELETE_SEQUENCES = new Set(["\u001B[3~", "\u001B[P"]);
2727
const HOME_SEQUENCES = new Set(["\u001B[H", "\u001B[1~", "\u001B[7~", "\u001BOH"]);
2828
const END_SEQUENCES = new Set(["\u001B[F", "\u001B[4~", "\u001B[8~", "\u001BOF"]);
29+
// Known exact Shift+Enter sequences (both xterm modifyOtherKeys and Kitty protocol).
2930
const SHIFT_RETURN_SEQUENCES = new Set([
3031
"\u001B\r",
31-
"\u001B[13;2u", // xterm modifyOtherKeys: keycode=13 (Enter), modifier=2 (Shift)
32-
"\u001B[13;1u", // Kitty keyboard protocol: keycode=13, modifier=1 (Shift)
32+
"\u001B[13;2u",
33+
"\u001B[13;1u",
3334
"\u001B[13;2~",
34-
"\u001B[13;1~", // tmux / alternate terminals may use ~ terminator
35+
"\u001B[13;1~",
3536
"\u001B[27;2;13~",
36-
"\u001B[27;1;13~", // extended format, Kitty encoding
37+
"\u001B[27;1;13~",
3738
]);
39+
40+
// CSI u format: ESC [ keycode ; modifier u
41+
// CSI ~ format: ESC [ keycode ; modifier ~
42+
// Extended: ESC [ 27 ; modifier ; keycode ~
43+
const CSI_SHIFT_RETURN_RE = /^\u001B\[13;(\d+)[u~]$/;
44+
const CSI_EXTENDED_SHIFT_RETURN_RE = /^\u001B\[27;(\d+);13~$/;
45+
46+
// Check whether a raw sequence represents Shift+Enter by parsing the modifier
47+
// parameter dynamically. This handles terminals (e.g. Windows Terminal) that
48+
// set extra flags on the modifier (e.g. 130 = 128 + 2) while the existing
49+
// SHIFT_RETURN_SEQUENCES Set only covers the canonical values (2 and 1).
50+
function isShiftReturn(raw: string): boolean {
51+
if (SHIFT_RETURN_SEQUENCES.has(raw)) return true;
52+
53+
let m: RegExpMatchArray | null;
54+
if ((m = raw.match(CSI_SHIFT_RETURN_RE)) !== null) {
55+
const mod = parseInt(m[1], 10);
56+
// xterm: Shift=2 (bit 1); Kitty: Shift=1 (bit 0)
57+
return (mod & 2) !== 0 || (mod & 1) !== 0;
58+
}
59+
if ((m = raw.match(CSI_EXTENDED_SHIFT_RETURN_RE)) !== null) {
60+
const mod = parseInt(m[1], 10);
61+
return (mod & 2) !== 0 || (mod & 1) !== 0;
62+
}
63+
return false;
64+
}
65+
66+
// Any CSI sequence with keycode=13 (Enter) — with or without modifiers.
67+
// Kitty progressive enhancement (ESC[>1u) sends plain Enter as ESC[13u
68+
// or ESC[13;NUMBERu with extra flags; xterm sends ESC[13;2u for Shift.
69+
const CSI_RETURN_RE = /^\u001B\[13;(\d+)[u~]$/;
70+
const CSI_EXTENDED_RETURN_RE = /^\u001B\[27;(\d+);13~$/;
71+
72+
function isReturn(raw: string): boolean {
73+
if (raw === "\r") return true;
74+
if (SHIFT_RETURN_SEQUENCES.has(raw)) return true;
75+
if (META_RETURN_SEQUENCES.has(raw)) return true;
76+
return CSI_RETURN_RE.test(raw) || CSI_EXTENDED_RETURN_RE.test(raw);
77+
}
3878
const META_RETURN_SEQUENCES = new Set(["\u001B[13;3u", "\u001B[13;4u"]);
3979
const CTRL_LEFT_SEQUENCES = new Set(["\u001B[1;5D", "\u001B[5D"]);
4080
const CTRL_RIGHT_SEQUENCES = new Set(["\u001B[1;5C", "\u001B[5C"]);
@@ -121,10 +161,10 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key:
121161
end: END_SEQUENCES.has(raw),
122162
pageDown: raw === "\u001B[6~",
123163
pageUp: raw === "\u001B[5~",
124-
return: raw === "\r" || SHIFT_RETURN_SEQUENCES.has(raw) || META_RETURN_SEQUENCES.has(raw),
164+
return: isReturn(raw),
125165
escape: raw === "\u001B",
126166
ctrl: CTRL_LEFT_SEQUENCES.has(raw) || CTRL_RIGHT_SEQUENCES.has(raw),
127-
shift: SHIFT_RETURN_SEQUENCES.has(raw),
167+
shift: isShiftReturn(raw),
128168
tab: raw === "\t" || raw === "\u001B[Z",
129169
backspace: BACKSPACE_BYTES.has(raw),
130170
delete: FORWARD_DELETE_SEQUENCES.has(raw),

0 commit comments

Comments
 (0)