Skip to content
Merged
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
6 changes: 3 additions & 3 deletions src/tests/promptInputKeys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ test("parseTerminalInput keeps BS payload for meta+backspace", () => {

test("parseTerminalInput recognizes shifted return sequences", () => {
const { input, key } = parseTerminalInput("\u001B\r");
assert.equal(input, "\r");
assert.equal(input, "");
assert.equal(key.return, true);
assert.equal(key.shift, true);
assert.equal(key.meta, false);
Expand Down Expand Up @@ -108,8 +108,8 @@ test("parseTerminalInput recognizes alternate shifted return sequences", () => {
});

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

test("parseTerminalInput recognizes terminal focus events", () => {
Expand Down
6 changes: 4 additions & 2 deletions src/ui/prompt/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ function disableTerminalFocusReporting(): string {
return "\u001B[?1004l";
}

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

export function disableTerminalExtendedKeys(): string {
return "\u001B[>4;0m";
return "\u001B[>4;0m\u001B[<u";
}

export function getPromptCursorPlacement(
Expand Down
56 changes: 52 additions & 4 deletions src/ui/prompt/useTerminalInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,55 @@ const BACKSPACE_BYTES = new Set(["\u007F", "\b"]);
const FORWARD_DELETE_SEQUENCES = new Set(["\u001B[3~", "\u001B[P"]);
const HOME_SEQUENCES = new Set(["\u001B[H", "\u001B[1~", "\u001B[7~", "\u001BOH"]);
const END_SEQUENCES = new Set(["\u001B[F", "\u001B[4~", "\u001B[8~", "\u001BOF"]);
const SHIFT_RETURN_SEQUENCES = new Set(["\u001B\r", "\u001B[13;2u", "\u001B[13;2~", "\u001B[27;2;13~"]);
// Known exact Shift+Enter sequences (both xterm modifyOtherKeys and Kitty protocol).
const SHIFT_RETURN_SEQUENCES = new Set([
"\u001B\r",
"\u001B[13;2u",
"\u001B[13;1u",
"\u001B[13;2~",
"\u001B[13;1~",
"\u001B[27;2;13~",
"\u001B[27;1;13~",
]);

// CSI u format: ESC [ keycode ; modifier u
// CSI ~ format: ESC [ keycode ; modifier ~
// Extended: ESC [ 27 ; modifier ; keycode ~
const CSI_SHIFT_RETURN_RE = /^\u001B\[13;(\d+)[u~]$/;
const CSI_EXTENDED_SHIFT_RETURN_RE = /^\u001B\[27;(\d+);13~$/;

// Check whether a raw sequence represents Shift+Enter by parsing the modifier
// parameter dynamically. This handles terminals (e.g. Windows Terminal) that
// set extra flags on the modifier (e.g. 130 = 128 + 2) while the existing
// SHIFT_RETURN_SEQUENCES Set only covers the canonical values (2 and 1).
function isShiftReturn(raw: string): boolean {
if (SHIFT_RETURN_SEQUENCES.has(raw)) return true;

let m: RegExpMatchArray | null;
if ((m = raw.match(CSI_SHIFT_RETURN_RE)) !== null) {
const mod = parseInt(m[1], 10);
// xterm: Shift=2 (bit 1); Kitty: Shift=1 (bit 0)
return (mod & 2) !== 0 || (mod & 1) !== 0;
}
if ((m = raw.match(CSI_EXTENDED_SHIFT_RETURN_RE)) !== null) {
const mod = parseInt(m[1], 10);
return (mod & 2) !== 0 || (mod & 1) !== 0;
}
return false;
}

// Any CSI sequence with keycode=13 (Enter) — with or without modifiers.
// Kitty progressive enhancement (ESC[>1u) sends plain Enter as ESC[13u
// or ESC[13;NUMBERu with extra flags; xterm sends ESC[13;2u for Shift.
const CSI_RETURN_RE = /^\u001B\[13;(\d+)[u~]$/;
const CSI_EXTENDED_RETURN_RE = /^\u001B\[27;(\d+);13~$/;

function isReturn(raw: string): boolean {
if (raw === "\r") return true;
if (SHIFT_RETURN_SEQUENCES.has(raw)) return true;
if (META_RETURN_SEQUENCES.has(raw)) return true;
return CSI_RETURN_RE.test(raw) || CSI_EXTENDED_RETURN_RE.test(raw);
}
const META_RETURN_SEQUENCES = new Set(["\u001B[13;3u", "\u001B[13;4u"]);
const CTRL_LEFT_SEQUENCES = new Set(["\u001B[1;5D", "\u001B[5D"]);
const CTRL_RIGHT_SEQUENCES = new Set(["\u001B[1;5C", "\u001B[5C"]);
Expand Down Expand Up @@ -113,10 +161,10 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key:
end: END_SEQUENCES.has(raw),
pageDown: raw === "\u001B[6~",
pageUp: raw === "\u001B[5~",
return: raw === "\r" || SHIFT_RETURN_SEQUENCES.has(raw) || META_RETURN_SEQUENCES.has(raw),
return: isReturn(raw),
escape: raw === "\u001B",
ctrl: CTRL_LEFT_SEQUENCES.has(raw) || CTRL_RIGHT_SEQUENCES.has(raw),
shift: SHIFT_RETURN_SEQUENCES.has(raw),
shift: isShiftReturn(raw),
tab: raw === "\t" || raw === "\u001B[Z",
backspace: BACKSPACE_BYTES.has(raw),
delete: FORWARD_DELETE_SEQUENCES.has(raw),
Expand Down Expand Up @@ -162,7 +210,7 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key:
key.shift = true;
}

if (key.tab || key.backspace || key.delete) {
if (key.tab || key.backspace || key.delete || key.return) {
input = "";
}

Expand Down