feat: add kitty query response filter and extend useInput Key interface

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-05-27 14:43:18 +08:00
parent 377e34ef37
commit 387791c142
2 changed files with 31 additions and 4 deletions
+24 -4
View File
@@ -19,6 +19,11 @@ export interface Key {
backspace: boolean; backspace: boolean;
delete: boolean; delete: boolean;
meta: boolean; meta: boolean;
super: boolean;
hyper: boolean;
capsLock: boolean;
numLock: boolean;
eventType?: "press" | "repeat" | "release";
} }
export interface UseInputOptions { export interface UseInputOptions {
@@ -37,6 +42,7 @@ export function useInput(
function listener(data: string) { function listener(data: string) {
const keypress = parseKeypress(data); const keypress = parseKeypress(data);
if (keypress.ignore) return;
const key: Key = { const key: Key = {
upArrow: keypress.name === "up", upArrow: keypress.name === "up",
@@ -55,20 +61,32 @@ export function useInput(
backspace: keypress.name === "backspace", backspace: keypress.name === "backspace",
delete: keypress.name === "delete", delete: keypress.name === "delete",
meta: keypress.meta, meta: keypress.meta,
super: keypress.super ?? false,
hyper: keypress.hyper ?? false,
capsLock: keypress.capsLock ?? false,
numLock: keypress.numLock ?? false,
eventType: keypress.eventType,
}; };
let input: string; let input: string;
if (keypress.ctrl) { if (keypress.isKittyProtocol) {
if (keypress.isPrintable) {
input = keypress.text ?? keypress.name;
} else if (keypress.ctrl && keypress.name.length === 1) {
input = keypress.name;
} else {
input = "";
}
} else if (keypress.ctrl) {
input = keypress.name ?? ""; input = keypress.name ?? "";
} else { } else {
input = keypress.sequence; input = keypress.sequence;
} }
if (nonAlphanumericKeys.includes(keypress.name)) { if (!keypress.isKittyProtocol && nonAlphanumericKeys.includes(keypress.name)) {
input = ""; input = "";
} }
// Strip escape prefix from incomplete sequences
if (input.startsWith("\x1b")) { if (input.startsWith("\x1b")) {
input = input.slice(1); input = input.slice(1);
} }
@@ -77,8 +95,10 @@ export function useInput(
key.shift = true; key.shift = true;
} }
// exitOnCtrlC skip: don't call user handler for Ctrl+C when intercepted // exitOnCtrlC: intercept kitty Ctrl+C (\x1b[3;5u). Legacy \x03 is caught
// by emitInput in createStdinController and never reaches useInput.
if (input === "c" && key.ctrl && stdin?.internal_exitOnCtrlC) { if (input === "c" && key.ctrl && stdin?.internal_exitOnCtrlC) {
app!.exit();
return; return;
} }
@@ -7,6 +7,8 @@ const metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/;
const fnKeyRe = /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/; const fnKeyRe = /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/;
const kittyQueryResponseRe = /^\x1b\[\?\d+u$/;
const keyName: Record<string, string> = { const keyName: Record<string, string> = {
/* xterm/gnome ESC O letter */ /* xterm/gnome ESC O letter */
OP: "f1", OP: "f1",
@@ -131,6 +133,7 @@ export interface Keypress {
* Only set by the kitty protocol parser. * Only set by the kitty protocol parser.
*/ */
isPrintable?: boolean; isPrintable?: boolean;
ignore?: boolean;
} }
// Kitty keyboard protocol modifier bits. // Kitty keyboard protocol modifier bits.
@@ -421,6 +424,10 @@ export function parseKeypress(s: Uint8Array | string = ""): Keypress {
s = ""; s = "";
} }
if (kittyQueryResponseRe.test(s)) {
return { name: "", ctrl: false, meta: false, shift: false, sequence: s, raw: s, ignore: true };
}
// Try kitty keyboard protocol parsers first // Try kitty keyboard protocol parsers first
const kittyResult = parseKittyKeypress(s); const kittyResult = parseKittyKeypress(s);
if (kittyResult) return kittyResult; if (kittyResult) return kittyResult;