@oh-my-pi/pi-tui 3.15.0 → 3.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/keys.ts CHANGED
@@ -1,87 +1,263 @@
1
1
  /**
2
- * Kitty keyboard protocol key sequence helpers.
2
+ * Keyboard input handling for terminal applications.
3
3
  *
4
- * The Kitty keyboard protocol sends enhanced escape sequences in the format:
5
- * \x1b[<codepoint>;<modifier>u
4
+ * Supports both legacy terminal sequences and Kitty keyboard protocol.
5
+ * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
6
6
  *
7
- * Modifier bits (before adding 1 for transmission):
8
- * - Shift: 1 (value 2)
9
- * - Alt: 2 (value 3)
10
- * - Ctrl: 4 (value 5)
11
- * - Super: 8 (value 9)
12
- * - Hyper: 16
13
- * - Meta: 32
14
- * - Caps_Lock: 64
15
- * - Num_Lock: 128
7
+ * Symbol keys are also supported, however some ctrl+symbol combos
8
+ * overlap with ASCII codes, e.g. ctrl+[ = ESC.
9
+ * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-ctrl-mapping-of-ascii-keys
10
+ * Those can still be * used for ctrl+shift combos
16
11
  *
17
- * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
12
+ * API:
13
+ * - matchesKey(data, keyId) - Check if input matches a key identifier
14
+ * - parseKey(data) - Parse input and return the key identifier
15
+ * - Key - Helper object for creating typed key identifiers
16
+ */
17
+
18
+ // =============================================================================
19
+ // Type-Safe Key Identifiers
20
+ // =============================================================================
21
+
22
+ type Letter =
23
+ | "a"
24
+ | "b"
25
+ | "c"
26
+ | "d"
27
+ | "e"
28
+ | "f"
29
+ | "g"
30
+ | "h"
31
+ | "i"
32
+ | "j"
33
+ | "k"
34
+ | "l"
35
+ | "m"
36
+ | "n"
37
+ | "o"
38
+ | "p"
39
+ | "q"
40
+ | "r"
41
+ | "s"
42
+ | "t"
43
+ | "u"
44
+ | "v"
45
+ | "w"
46
+ | "x"
47
+ | "y"
48
+ | "z";
49
+
50
+ type SymbolKey =
51
+ | "`"
52
+ | "-"
53
+ | "="
54
+ | "["
55
+ | "]"
56
+ | "\\"
57
+ | ";"
58
+ | "'"
59
+ | ","
60
+ | "."
61
+ | "/"
62
+ | "!"
63
+ | "@"
64
+ | "#"
65
+ | "$"
66
+ | "%"
67
+ | "^"
68
+ | "&"
69
+ | "*"
70
+ | "("
71
+ | ")"
72
+ | "_"
73
+ | "+"
74
+ | "|"
75
+ | "~"
76
+ | "{"
77
+ | "}"
78
+ | ":"
79
+ | "<"
80
+ | ">"
81
+ | "?";
82
+
83
+ type SpecialKey =
84
+ | "escape"
85
+ | "esc"
86
+ | "enter"
87
+ | "return"
88
+ | "tab"
89
+ | "space"
90
+ | "backspace"
91
+ | "delete"
92
+ | "home"
93
+ | "end"
94
+ | "up"
95
+ | "down"
96
+ | "left"
97
+ | "right";
98
+
99
+ type BaseKey = Letter | SymbolKey | SpecialKey;
100
+
101
+ /**
102
+ * Union type of all valid key identifiers.
103
+ * Provides autocomplete and catches typos at compile time.
104
+ */
105
+ export type KeyId =
106
+ | BaseKey
107
+ | `ctrl+${BaseKey}`
108
+ | `shift+${BaseKey}`
109
+ | `alt+${BaseKey}`
110
+ | `ctrl+shift+${BaseKey}`
111
+ | `shift+ctrl+${BaseKey}`
112
+ | `ctrl+alt+${BaseKey}`
113
+ | `alt+ctrl+${BaseKey}`
114
+ | `shift+alt+${BaseKey}`
115
+ | `alt+shift+${BaseKey}`
116
+ | `ctrl+shift+alt+${BaseKey}`
117
+ | `ctrl+alt+shift+${BaseKey}`
118
+ | `shift+ctrl+alt+${BaseKey}`
119
+ | `shift+alt+ctrl+${BaseKey}`
120
+ | `alt+ctrl+shift+${BaseKey}`
121
+ | `alt+shift+ctrl+${BaseKey}`;
122
+
123
+ /**
124
+ * Helper object for creating typed key identifiers with autocomplete.
18
125
  *
19
- * NOTE: Some terminals (e.g., Ghostty on Linux) include lock key states
20
- * (Caps Lock, Num Lock) in the modifier field. We mask these out when
21
- * checking for key combinations since they shouldn't affect behavior.
126
+ * Usage:
127
+ * - Key.escape, Key.enter, Key.tab, etc. for special keys
128
+ * - Key.backtick, Key.comma, Key.period, etc. for symbol keys
129
+ * - Key.ctrl("c"), Key.alt("x") for single modifier
130
+ * - Key.ctrlShift("p"), Key.ctrlAlt("x") for combined modifiers
22
131
  */
23
-
24
- // Common codepoints
25
- const CODEPOINTS = {
26
- // Letters (lowercase ASCII)
27
- a: 97,
28
- c: 99,
29
- d: 100,
30
- e: 101,
31
- g: 103,
32
- k: 107,
33
- l: 108,
34
- o: 111,
35
- p: 112,
36
- t: 116,
37
- u: 117,
38
- v: 118,
39
- w: 119,
40
- z: 122,
41
-
132
+ export const Key = {
42
133
  // Special keys
43
- escape: 27,
44
- tab: 9,
45
- enter: 13,
46
- backspace: 127,
134
+ escape: "escape" as const,
135
+ esc: "esc" as const,
136
+ enter: "enter" as const,
137
+ return: "return" as const,
138
+ tab: "tab" as const,
139
+ space: "space" as const,
140
+ backspace: "backspace" as const,
141
+ delete: "delete" as const,
142
+ home: "home" as const,
143
+ end: "end" as const,
144
+ up: "up" as const,
145
+ down: "down" as const,
146
+ left: "left" as const,
147
+ right: "right" as const,
148
+
149
+ // Symbol keys
150
+ backtick: "`" as const,
151
+ hyphen: "-" as const,
152
+ equals: "=" as const,
153
+ leftbracket: "[" as const,
154
+ rightbracket: "]" as const,
155
+ backslash: "\\" as const,
156
+ semicolon: ";" as const,
157
+ quote: "'" as const,
158
+ comma: "," as const,
159
+ period: "." as const,
160
+ slash: "/" as const,
161
+ exclamation: "!" as const,
162
+ at: "@" as const,
163
+ hash: "#" as const,
164
+ dollar: "$" as const,
165
+ percent: "%" as const,
166
+ caret: "^" as const,
167
+ ampersand: "&" as const,
168
+ asterisk: "*" as const,
169
+ leftparen: "(" as const,
170
+ rightparen: ")" as const,
171
+ underscore: "_" as const,
172
+ plus: "+" as const,
173
+ pipe: "|" as const,
174
+ tilde: "~" as const,
175
+ leftbrace: "{" as const,
176
+ rightbrace: "}" as const,
177
+ colon: ":" as const,
178
+ lessthan: "<" as const,
179
+ greaterthan: ">" as const,
180
+ question: "?" as const,
181
+
182
+ // Single modifiers
183
+ ctrl: <K extends BaseKey>(key: K): `ctrl+${K}` => `ctrl+${key}`,
184
+ shift: <K extends BaseKey>(key: K): `shift+${K}` => `shift+${key}`,
185
+ alt: <K extends BaseKey>(key: K): `alt+${K}` => `alt+${key}`,
186
+
187
+ // Combined modifiers
188
+ ctrlShift: <K extends BaseKey>(key: K): `ctrl+shift+${K}` => `ctrl+shift+${key}`,
189
+ shiftCtrl: <K extends BaseKey>(key: K): `shift+ctrl+${K}` => `shift+ctrl+${key}`,
190
+ ctrlAlt: <K extends BaseKey>(key: K): `ctrl+alt+${K}` => `ctrl+alt+${key}`,
191
+ altCtrl: <K extends BaseKey>(key: K): `alt+ctrl+${K}` => `alt+ctrl+${key}`,
192
+ shiftAlt: <K extends BaseKey>(key: K): `shift+alt+${K}` => `shift+alt+${key}`,
193
+ altShift: <K extends BaseKey>(key: K): `alt+shift+${K}` => `alt+shift+${key}`,
194
+
195
+ // Triple modifiers
196
+ ctrlShiftAlt: <K extends BaseKey>(key: K): `ctrl+shift+alt+${K}` => `ctrl+shift+alt+${key}`,
47
197
  } as const;
48
198
 
49
- // Lock key bits to ignore when matching (Caps Lock + Num Lock)
50
- const LOCK_MASK = 64 + 128; // 192
199
+ // =============================================================================
200
+ // Constants
201
+ // =============================================================================
202
+
203
+ const SYMBOL_KEYS = new Set([
204
+ "`",
205
+ "-",
206
+ "=",
207
+ "[",
208
+ "]",
209
+ "\\",
210
+ ";",
211
+ "'",
212
+ ",",
213
+ ".",
214
+ "/",
215
+ "!",
216
+ "@",
217
+ "#",
218
+ "$",
219
+ "%",
220
+ "^",
221
+ "&",
222
+ "*",
223
+ "(",
224
+ ")",
225
+ "_",
226
+ "+",
227
+ "|",
228
+ "~",
229
+ "{",
230
+ "}",
231
+ ":",
232
+ "<",
233
+ ">",
234
+ "?",
235
+ ]);
51
236
 
52
- // Modifier bits (before adding 1)
53
237
  const MODIFIERS = {
54
238
  shift: 1,
55
239
  alt: 2,
56
240
  ctrl: 4,
57
- super: 8,
58
241
  } as const;
59
242
 
60
- /**
61
- * Build a Kitty keyboard protocol sequence for a key with modifier.
62
- */
63
- function kittySequence(codepoint: number, modifier: number): string {
64
- return `\x1b[${codepoint};${modifier + 1}u`;
65
- }
243
+ const LOCK_MASK = 64 + 128; // Caps Lock + Num Lock
66
244
 
67
- /**
68
- * Parsed Kitty keyboard protocol sequence.
69
- */
70
- interface ParsedKittySequence {
71
- codepoint: number;
72
- modifier: number; // Actual modifier bits (after subtracting 1)
73
- }
245
+ const CODEPOINTS = {
246
+ escape: 27,
247
+ tab: 9,
248
+ enter: 13,
249
+ space: 32,
250
+ backspace: 127,
251
+ kpEnter: 57414, // Numpad Enter (Kitty protocol)
252
+ } as const;
253
+
254
+ const ARROW_CODEPOINTS = {
255
+ up: -1,
256
+ down: -2,
257
+ right: -3,
258
+ left: -4,
259
+ } as const;
74
260
 
75
- /**
76
- * Parse a Kitty keyboard protocol sequence.
77
- * Handles formats:
78
- * - \x1b[<codepoint>u (no modifier)
79
- * - \x1b[<codepoint>;<modifier>u (with modifier)
80
- * - \x1b[1;<modifier>A/B/C/D (arrow keys with modifier)
81
- *
82
- * Returns null if not a valid Kitty sequence.
83
- */
84
- // Virtual codepoints for functional keys (negative to avoid conflicts)
85
261
  const FUNCTIONAL_CODEPOINTS = {
86
262
  delete: -10,
87
263
  insert: -11,
@@ -91,8 +267,17 @@ const FUNCTIONAL_CODEPOINTS = {
91
267
  end: -15,
92
268
  } as const;
93
269
 
270
+ // =============================================================================
271
+ // Kitty Protocol Parsing
272
+ // =============================================================================
273
+
274
+ interface ParsedKittySequence {
275
+ codepoint: number;
276
+ modifier: number;
277
+ }
278
+
94
279
  function parseKittySequence(data: string): ParsedKittySequence | null {
95
- // Match CSI u format: \x1b[<num>u or \x1b[<num>;<mod>u
280
+ // CSI u format: \x1b[<num>u or \x1b[<num>;<mod>u
96
281
  const csiUMatch = data.match(/^\x1b\[(\d+)(?:;(\d+))?u$/);
97
282
  if (csiUMatch) {
98
283
  const codepoint = parseInt(csiUMatch[1]!, 10);
@@ -100,30 +285,26 @@ function parseKittySequence(data: string): ParsedKittySequence | null {
100
285
  return { codepoint, modifier: modValue - 1 };
101
286
  }
102
287
 
103
- // Match arrow keys with modifier: \x1b[1;<mod>A/B/C/D
288
+ // Arrow keys with modifier: \x1b[1;<mod>A/B/C/D
104
289
  const arrowMatch = data.match(/^\x1b\[1;(\d+)([ABCD])$/);
105
290
  if (arrowMatch) {
106
291
  const modValue = parseInt(arrowMatch[1]!, 10);
107
- // Map arrow letters to virtual codepoints for easier matching
108
292
  const arrowCodes: Record<string, number> = { A: -1, B: -2, C: -3, D: -4 };
109
- const codepoint = arrowCodes[arrowMatch[2]!]!;
110
- return { codepoint, modifier: modValue - 1 };
293
+ return { codepoint: arrowCodes[arrowMatch[2]!]!, modifier: modValue - 1 };
111
294
  }
112
295
 
113
- // Match functional keys with ~ terminator: \x1b[<num>~ or \x1b[<num>;<mod>~
114
- // DELETE=3, INSERT=2, PAGEUP=5, PAGEDOWN=6, etc.
296
+ // Functional keys: \x1b[<num>~ or \x1b[<num>;<mod>~
115
297
  const funcMatch = data.match(/^\x1b\[(\d+)(?:;(\d+))?~$/);
116
298
  if (funcMatch) {
117
299
  const keyNum = parseInt(funcMatch[1]!, 10);
118
300
  const modValue = funcMatch[2] ? parseInt(funcMatch[2], 10) : 1;
119
- // Map functional key numbers to virtual codepoints
120
301
  const funcCodes: Record<number, number> = {
121
302
  2: FUNCTIONAL_CODEPOINTS.insert,
122
303
  3: FUNCTIONAL_CODEPOINTS.delete,
123
304
  5: FUNCTIONAL_CODEPOINTS.pageUp,
124
305
  6: FUNCTIONAL_CODEPOINTS.pageDown,
125
- 7: FUNCTIONAL_CODEPOINTS.home, // Alternative home
126
- 8: FUNCTIONAL_CODEPOINTS.end, // Alternative end
306
+ 7: FUNCTIONAL_CODEPOINTS.home,
307
+ 8: FUNCTIONAL_CODEPOINTS.end,
127
308
  };
128
309
  const codepoint = funcCodes[keyNum];
129
310
  if (codepoint !== undefined) {
@@ -131,7 +312,7 @@ function parseKittySequence(data: string): ParsedKittySequence | null {
131
312
  }
132
313
  }
133
314
 
134
- // Match Home/End with modifier: \x1b[1;<mod>H/F
315
+ // Home/End with modifier: \x1b[1;<mod>H/F
135
316
  const homeEndMatch = data.match(/^\x1b\[1;(\d+)([HF])$/);
136
317
  if (homeEndMatch) {
137
318
  const modValue = parseInt(homeEndMatch[1]!, 10);
@@ -142,419 +323,462 @@ function parseKittySequence(data: string): ParsedKittySequence | null {
142
323
  return null;
143
324
  }
144
325
 
145
- /**
146
- * Check if a Kitty sequence matches the expected codepoint and modifier,
147
- * ignoring lock key bits (Caps Lock, Num Lock).
148
- */
149
326
  function matchesKittySequence(data: string, expectedCodepoint: number, expectedModifier: number): boolean {
150
327
  const parsed = parseKittySequence(data);
151
328
  if (!parsed) return false;
152
-
153
- // Mask out lock bits from both sides for comparison
154
329
  const actualMod = parsed.modifier & ~LOCK_MASK;
155
330
  const expectedMod = expectedModifier & ~LOCK_MASK;
156
-
157
331
  return parsed.codepoint === expectedCodepoint && actualMod === expectedMod;
158
332
  }
159
333
 
160
- // Pre-built sequences for common key combinations
161
- export const Keys = {
162
- // Ctrl+<letter> combinations
163
- CTRL_A: kittySequence(CODEPOINTS.a, MODIFIERS.ctrl),
164
- CTRL_C: kittySequence(CODEPOINTS.c, MODIFIERS.ctrl),
165
- CTRL_D: kittySequence(CODEPOINTS.d, MODIFIERS.ctrl),
166
- CTRL_E: kittySequence(CODEPOINTS.e, MODIFIERS.ctrl),
167
- CTRL_G: kittySequence(CODEPOINTS.g, MODIFIERS.ctrl),
168
- CTRL_K: kittySequence(CODEPOINTS.k, MODIFIERS.ctrl),
169
- CTRL_L: kittySequence(CODEPOINTS.l, MODIFIERS.ctrl),
170
- CTRL_O: kittySequence(CODEPOINTS.o, MODIFIERS.ctrl),
171
- CTRL_P: kittySequence(CODEPOINTS.p, MODIFIERS.ctrl),
172
- CTRL_T: kittySequence(CODEPOINTS.t, MODIFIERS.ctrl),
173
- CTRL_U: kittySequence(CODEPOINTS.u, MODIFIERS.ctrl),
174
- CTRL_V: kittySequence(CODEPOINTS.v, MODIFIERS.ctrl),
175
- CTRL_W: kittySequence(CODEPOINTS.w, MODIFIERS.ctrl),
176
- CTRL_Z: kittySequence(CODEPOINTS.z, MODIFIERS.ctrl),
177
-
178
- // Enter combinations
179
- SHIFT_ENTER: kittySequence(CODEPOINTS.enter, MODIFIERS.shift),
180
- ALT_ENTER: kittySequence(CODEPOINTS.enter, MODIFIERS.alt),
181
- CTRL_ENTER: kittySequence(CODEPOINTS.enter, MODIFIERS.ctrl),
182
-
183
- // Tab combinations
184
- SHIFT_TAB: kittySequence(CODEPOINTS.tab, MODIFIERS.shift),
185
-
186
- // Backspace combinations
187
- ALT_BACKSPACE: kittySequence(CODEPOINTS.backspace, MODIFIERS.alt),
188
- } as const;
334
+ // =============================================================================
335
+ // Generic Key Matching
336
+ // =============================================================================
189
337
 
190
- /**
191
- * Check if input matches a Kitty protocol Ctrl+<key> sequence.
192
- * Ignores lock key bits (Caps Lock, Num Lock).
193
- * @param data - The input data to check
194
- * @param key - Single lowercase letter (e.g., 'c' for Ctrl+C)
195
- */
196
- export function isKittyCtrl(data: string, key: string): boolean {
197
- if (key.length !== 1) return false;
198
- const codepoint = key.charCodeAt(0);
199
- // Check exact match first (fast path)
200
- if (data === kittySequence(codepoint, MODIFIERS.ctrl)) return true;
201
- // Check with lock bits masked out
202
- return matchesKittySequence(data, codepoint, MODIFIERS.ctrl);
338
+ function rawCtrlChar(letter: string): string {
339
+ const code = letter.toLowerCase().charCodeAt(0) - 96;
340
+ return String.fromCharCode(code);
203
341
  }
204
342
 
205
- /**
206
- * Check if input matches a Kitty protocol key sequence with specific modifier.
207
- * Ignores lock key bits (Caps Lock, Num Lock).
208
- * @param data - The input data to check
209
- * @param codepoint - ASCII codepoint of the key
210
- * @param modifier - Modifier value (use MODIFIERS constants)
211
- */
212
- export function isKittyKey(data: string, codepoint: number, modifier: number): boolean {
213
- // Check exact match first (fast path)
214
- if (data === kittySequence(codepoint, modifier)) return true;
215
- // Check with lock bits masked out
216
- return matchesKittySequence(data, codepoint, modifier);
217
- }
218
-
219
- // Raw control character codes
220
- const RAW = {
221
- CTRL_A: "\x01",
222
- CTRL_C: "\x03",
223
- CTRL_D: "\x04",
224
- CTRL_E: "\x05",
225
- CTRL_G: "\x07",
226
- CTRL_K: "\x0b",
227
- CTRL_L: "\x0c",
228
- CTRL_O: "\x0f",
229
- CTRL_P: "\x10",
230
- CTRL_T: "\x14",
231
- CTRL_U: "\x15",
232
- CTRL_V: "\x16",
233
- CTRL_W: "\x17",
234
- CTRL_Z: "\x1a",
235
- ALT_BACKSPACE: "\x1b\x7f",
236
- SHIFT_TAB: "\x1b[Z",
237
- } as const;
343
+ function parseKeyId(keyId: string): { key: string; ctrl: boolean; shift: boolean; alt: boolean } | null {
344
+ const parts = keyId.toLowerCase().split("+");
345
+ const key = parts[parts.length - 1];
346
+ if (!key) return null;
347
+ return {
348
+ key,
349
+ ctrl: parts.includes("ctrl"),
350
+ shift: parts.includes("shift"),
351
+ alt: parts.includes("alt"),
352
+ };
353
+ }
238
354
 
239
355
  /**
240
- * Check if input matches Ctrl+A (raw byte or Kitty protocol).
241
- * Ignores lock key bits.
356
+ * Match input data against a key identifier string.
357
+ *
358
+ * Supported key identifiers:
359
+ * - Single keys: "escape", "tab", "enter", "backspace", "delete", "home", "end", "space"
360
+ * - Arrow keys: "up", "down", "left", "right"
361
+ * - Ctrl combinations: "ctrl+c", "ctrl+z", etc.
362
+ * - Shift combinations: "shift+tab", "shift+enter"
363
+ * - Alt combinations: "alt+enter", "alt+backspace"
364
+ * - Combined modifiers: "shift+ctrl+p", "ctrl+alt+x"
365
+ *
366
+ * Use the Key helper for autocomplete: Key.ctrl("c"), Key.escape, Key.ctrlShift("p")
367
+ *
368
+ * @param data - Raw input data from terminal
369
+ * @param keyId - Key identifier (e.g., "ctrl+c", "escape", Key.ctrl("c"))
242
370
  */
243
- export function isCtrlA(data: string): boolean {
244
- return data === RAW.CTRL_A || data === Keys.CTRL_A || matchesKittySequence(data, CODEPOINTS.a, MODIFIERS.ctrl);
371
+ export function matchesKey(data: string, keyId: KeyId): boolean {
372
+ const parsed = parseKeyId(keyId);
373
+ if (!parsed) return false;
374
+
375
+ const { key, ctrl, shift, alt } = parsed;
376
+ let modifier = 0;
377
+ if (shift) modifier |= MODIFIERS.shift;
378
+ if (alt) modifier |= MODIFIERS.alt;
379
+ if (ctrl) modifier |= MODIFIERS.ctrl;
380
+
381
+ switch (key) {
382
+ case "escape":
383
+ case "esc":
384
+ if (modifier !== 0) return false;
385
+ return data === "\x1b" || matchesKittySequence(data, CODEPOINTS.escape, 0);
386
+
387
+ case "space":
388
+ if (modifier === 0) {
389
+ return data === " " || matchesKittySequence(data, CODEPOINTS.space, 0);
390
+ }
391
+ return matchesKittySequence(data, CODEPOINTS.space, modifier);
392
+
393
+ case "tab":
394
+ if (shift && !ctrl && !alt) {
395
+ return data === "\x1b[Z" || matchesKittySequence(data, CODEPOINTS.tab, MODIFIERS.shift);
396
+ }
397
+ if (modifier === 0) {
398
+ return data === "\t" || matchesKittySequence(data, CODEPOINTS.tab, 0);
399
+ }
400
+ return matchesKittySequence(data, CODEPOINTS.tab, modifier);
401
+
402
+ case "enter":
403
+ case "return":
404
+ if (shift && !ctrl && !alt) {
405
+ return (
406
+ data === "\x1b\r" || // Legacy: some terminals send ESC+CR for shift+enter
407
+ matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.shift) ||
408
+ matchesKittySequence(data, CODEPOINTS.kpEnter, MODIFIERS.shift)
409
+ );
410
+ }
411
+ if (alt && !ctrl && !shift) {
412
+ return (
413
+ matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.alt) ||
414
+ matchesKittySequence(data, CODEPOINTS.kpEnter, MODIFIERS.alt)
415
+ );
416
+ }
417
+ if (modifier === 0) {
418
+ return (
419
+ data === "\r" ||
420
+ data === "\x1bOM" || // SS3 M (numpad enter in some terminals)
421
+ matchesKittySequence(data, CODEPOINTS.enter, 0) ||
422
+ matchesKittySequence(data, CODEPOINTS.kpEnter, 0)
423
+ );
424
+ }
425
+ return (
426
+ matchesKittySequence(data, CODEPOINTS.enter, modifier) ||
427
+ matchesKittySequence(data, CODEPOINTS.kpEnter, modifier)
428
+ );
429
+
430
+ case "backspace":
431
+ if (alt && !ctrl && !shift) {
432
+ return data === "\x1b\x7f" || matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt);
433
+ }
434
+ if (modifier === 0) {
435
+ return data === "\x7f" || data === "\x08" || matchesKittySequence(data, CODEPOINTS.backspace, 0);
436
+ }
437
+ return matchesKittySequence(data, CODEPOINTS.backspace, modifier);
438
+
439
+ case "delete":
440
+ if (modifier === 0) {
441
+ return data === "\x1b[3~" || matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.delete, 0);
442
+ }
443
+ return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.delete, modifier);
444
+
445
+ case "home":
446
+ if (modifier === 0) {
447
+ return (
448
+ data === "\x1b[H" ||
449
+ data === "\x1b[1~" ||
450
+ data === "\x1b[7~" ||
451
+ matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.home, 0)
452
+ );
453
+ }
454
+ return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.home, modifier);
455
+
456
+ case "end":
457
+ if (modifier === 0) {
458
+ return (
459
+ data === "\x1b[F" ||
460
+ data === "\x1b[4~" ||
461
+ data === "\x1b[8~" ||
462
+ matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.end, 0)
463
+ );
464
+ }
465
+ return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.end, modifier);
466
+
467
+ case "up":
468
+ if (modifier === 0) {
469
+ return data === "\x1b[A" || matchesKittySequence(data, ARROW_CODEPOINTS.up, 0);
470
+ }
471
+ return matchesKittySequence(data, ARROW_CODEPOINTS.up, modifier);
472
+
473
+ case "down":
474
+ if (modifier === 0) {
475
+ return data === "\x1b[B" || matchesKittySequence(data, ARROW_CODEPOINTS.down, 0);
476
+ }
477
+ return matchesKittySequence(data, ARROW_CODEPOINTS.down, modifier);
478
+
479
+ case "left":
480
+ if (alt && !ctrl && !shift) {
481
+ return (
482
+ data === "\x1b[1;3D" ||
483
+ data === "\x1bb" ||
484
+ matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.alt)
485
+ );
486
+ }
487
+ if (ctrl && !alt && !shift) {
488
+ return data === "\x1b[1;5D" || matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.ctrl);
489
+ }
490
+ if (modifier === 0) {
491
+ return data === "\x1b[D" || matchesKittySequence(data, ARROW_CODEPOINTS.left, 0);
492
+ }
493
+ return matchesKittySequence(data, ARROW_CODEPOINTS.left, modifier);
494
+
495
+ case "right":
496
+ if (alt && !ctrl && !shift) {
497
+ return (
498
+ data === "\x1b[1;3C" ||
499
+ data === "\x1bf" ||
500
+ matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.alt)
501
+ );
502
+ }
503
+ if (ctrl && !alt && !shift) {
504
+ return data === "\x1b[1;5C" || matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.ctrl);
505
+ }
506
+ if (modifier === 0) {
507
+ return data === "\x1b[C" || matchesKittySequence(data, ARROW_CODEPOINTS.right, 0);
508
+ }
509
+ return matchesKittySequence(data, ARROW_CODEPOINTS.right, modifier);
510
+ }
511
+
512
+ // Handle single letter keys (a-z) and some symbols
513
+ if (key.length === 1 && ((key >= "a" && key <= "z") || SYMBOL_KEYS.has(key))) {
514
+ const codepoint = key.charCodeAt(0);
515
+
516
+ if (ctrl && !shift && !alt) {
517
+ const raw = rawCtrlChar(key);
518
+ if (data === raw) return true;
519
+ if (data.length > 0 && data.charCodeAt(0) === raw.charCodeAt(0)) return true;
520
+ return matchesKittySequence(data, codepoint, MODIFIERS.ctrl);
521
+ }
522
+
523
+ if (ctrl && shift && !alt) {
524
+ return matchesKittySequence(data, codepoint, MODIFIERS.shift + MODIFIERS.ctrl);
525
+ }
526
+
527
+ if (shift && !ctrl && !alt) {
528
+ // Legacy: shift+letter produces uppercase
529
+ if (data === key.toUpperCase()) return true;
530
+ return matchesKittySequence(data, codepoint, MODIFIERS.shift);
531
+ }
532
+
533
+ if (modifier !== 0) {
534
+ return matchesKittySequence(data, codepoint, modifier);
535
+ }
536
+
537
+ return data === key;
538
+ }
539
+
540
+ return false;
245
541
  }
246
542
 
247
543
  /**
248
- * Check if input matches Ctrl+C (raw byte or Kitty protocol).
249
- * Ignores lock key bits.
250
- */
251
- export function isCtrlC(data: string): boolean {
252
- return data === RAW.CTRL_C || data === Keys.CTRL_C || matchesKittySequence(data, CODEPOINTS.c, MODIFIERS.ctrl);
544
+ * Parse input data and return the key identifier if recognized.
545
+ *
546
+ * @param data - Raw input data from terminal
547
+ * @returns Key identifier string (e.g., "ctrl+c") or undefined
548
+ */
549
+ export function parseKey(data: string): string | undefined {
550
+ const kitty = parseKittySequence(data);
551
+ if (kitty) {
552
+ const { codepoint, modifier } = kitty;
553
+ const mods: string[] = [];
554
+ const effectiveMod = modifier & ~LOCK_MASK;
555
+ if (effectiveMod & MODIFIERS.shift) mods.push("shift");
556
+ if (effectiveMod & MODIFIERS.ctrl) mods.push("ctrl");
557
+ if (effectiveMod & MODIFIERS.alt) mods.push("alt");
558
+
559
+ let keyName: string | undefined;
560
+ if (codepoint === CODEPOINTS.escape) keyName = "escape";
561
+ else if (codepoint === CODEPOINTS.tab) keyName = "tab";
562
+ else if (codepoint === CODEPOINTS.enter || codepoint === CODEPOINTS.kpEnter) keyName = "enter";
563
+ else if (codepoint === CODEPOINTS.space) keyName = "space";
564
+ else if (codepoint === CODEPOINTS.backspace) keyName = "backspace";
565
+ else if (codepoint === FUNCTIONAL_CODEPOINTS.delete) keyName = "delete";
566
+ else if (codepoint === FUNCTIONAL_CODEPOINTS.home) keyName = "home";
567
+ else if (codepoint === FUNCTIONAL_CODEPOINTS.end) keyName = "end";
568
+ else if (codepoint === ARROW_CODEPOINTS.up) keyName = "up";
569
+ else if (codepoint === ARROW_CODEPOINTS.down) keyName = "down";
570
+ else if (codepoint === ARROW_CODEPOINTS.left) keyName = "left";
571
+ else if (codepoint === ARROW_CODEPOINTS.right) keyName = "right";
572
+ else if (codepoint >= 97 && codepoint <= 122) keyName = String.fromCharCode(codepoint);
573
+ else if (SYMBOL_KEYS.has(String.fromCharCode(codepoint))) keyName = String.fromCharCode(codepoint);
574
+
575
+ if (keyName) {
576
+ return mods.length > 0 ? `${mods.join("+")}+${keyName}` : keyName;
577
+ }
578
+ }
579
+
580
+ // Legacy sequences
581
+ if (data === "\x1b") return "escape";
582
+ if (data === "\t") return "tab";
583
+ if (data === "\r" || data === "\x1bOM") return "enter";
584
+ if (data === " ") return "space";
585
+ if (data === "\x7f" || data === "\x08") return "backspace";
586
+ if (data === "\x1b[Z") return "shift+tab";
587
+ if (data === "\x1b\r") return "shift+enter"; // Legacy: ESC+CR for shift+enter
588
+ if (data === "\x1b\x7f") return "alt+backspace";
589
+ if (data === "\x1b[A") return "up";
590
+ if (data === "\x1b[B") return "down";
591
+ if (data === "\x1b[C") return "right";
592
+ if (data === "\x1b[D") return "left";
593
+ if (data === "\x1b[H") return "home";
594
+ if (data === "\x1b[F") return "end";
595
+ if (data === "\x1b[3~") return "delete";
596
+
597
+ // Raw Ctrl+letter
598
+ if (data.length === 1) {
599
+ const code = data.charCodeAt(0);
600
+ if (code >= 1 && code <= 26) {
601
+ return `ctrl+${String.fromCharCode(code + 96)}`;
602
+ }
603
+ if (code >= 32 && code <= 126) {
604
+ return data;
605
+ }
606
+ }
607
+
608
+ return undefined;
253
609
  }
254
610
 
255
- /**
256
- * Check if input matches Ctrl+D (raw byte or Kitty protocol).
257
- * Ignores lock key bits.
258
- */
259
- export function isCtrlD(data: string): boolean {
260
- return data === RAW.CTRL_D || data === Keys.CTRL_D || matchesKittySequence(data, CODEPOINTS.d, MODIFIERS.ctrl);
611
+ // =============================================================================
612
+ // Legacy helper wrappers (for compatibility)
613
+ // =============================================================================
614
+
615
+ export function isArrowUp(data: string): boolean {
616
+ return matchesKey(data, "up");
261
617
  }
262
618
 
263
- /**
264
- * Check if input matches Ctrl+E (raw byte or Kitty protocol).
265
- * Ignores lock key bits.
266
- */
267
- export function isCtrlE(data: string): boolean {
268
- return data === RAW.CTRL_E || data === Keys.CTRL_E || matchesKittySequence(data, CODEPOINTS.e, MODIFIERS.ctrl);
619
+ export function isArrowDown(data: string): boolean {
620
+ return matchesKey(data, "down");
269
621
  }
270
622
 
271
- /**
272
- * Check if input matches Ctrl+G (raw byte or Kitty protocol).
273
- * Ignores lock key bits.
274
- */
275
- export function isCtrlG(data: string): boolean {
276
- return data === RAW.CTRL_G || data === Keys.CTRL_G || matchesKittySequence(data, CODEPOINTS.g, MODIFIERS.ctrl);
623
+ export function isArrowLeft(data: string): boolean {
624
+ return matchesKey(data, "left");
277
625
  }
278
626
 
279
- /**
280
- * Check if input matches Ctrl+K (raw byte or Kitty protocol).
281
- * Ignores lock key bits.
282
- * Also checks if first byte is 0x0b for compatibility with terminals
283
- * that may send trailing bytes.
284
- */
285
- export function isCtrlK(data: string): boolean {
286
- return (
287
- data === RAW.CTRL_K ||
288
- (data.length > 0 && data.charCodeAt(0) === 0x0b) ||
289
- data === Keys.CTRL_K ||
290
- matchesKittySequence(data, CODEPOINTS.k, MODIFIERS.ctrl)
291
- );
627
+ export function isArrowRight(data: string): boolean {
628
+ return matchesKey(data, "right");
292
629
  }
293
630
 
294
- /**
295
- * Check if input matches Ctrl+L (raw byte or Kitty protocol).
296
- * Ignores lock key bits.
297
- */
298
- export function isCtrlL(data: string): boolean {
299
- return data === RAW.CTRL_L || data === Keys.CTRL_L || matchesKittySequence(data, CODEPOINTS.l, MODIFIERS.ctrl);
631
+ export function isEscape(data: string): boolean {
632
+ return matchesKey(data, "escape") || matchesKey(data, "esc");
300
633
  }
301
634
 
302
- /**
303
- * Check if input matches Ctrl+O (raw byte or Kitty protocol).
304
- * Ignores lock key bits.
305
- */
306
- export function isCtrlO(data: string): boolean {
307
- return data === RAW.CTRL_O || data === Keys.CTRL_O || matchesKittySequence(data, CODEPOINTS.o, MODIFIERS.ctrl);
635
+ export function isEnter(data: string): boolean {
636
+ return matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n";
308
637
  }
309
638
 
310
- /**
311
- * Check if input matches Shift+Ctrl+O (Kitty protocol only).
312
- * Ignores lock key bits.
313
- */
314
- export function isShiftCtrlO(data: string): boolean {
315
- return matchesKittySequence(data, CODEPOINTS.o, MODIFIERS.shift + MODIFIERS.ctrl);
639
+ export function isTab(data: string): boolean {
640
+ return matchesKey(data, "tab");
316
641
  }
317
642
 
318
- /**
319
- * Check if input matches Ctrl+P (raw byte or Kitty protocol).
320
- * Ignores lock key bits.
321
- */
322
- export function isCtrlP(data: string): boolean {
323
- return data === RAW.CTRL_P || data === Keys.CTRL_P || matchesKittySequence(data, CODEPOINTS.p, MODIFIERS.ctrl);
643
+ export function isShiftTab(data: string): boolean {
644
+ return matchesKey(data, "shift+tab");
324
645
  }
325
646
 
326
- /**
327
- * Check if input matches Shift+Ctrl+P (Kitty protocol only).
328
- * Ignores lock key bits.
329
- */
330
- export function isShiftCtrlP(data: string): boolean {
331
- return matchesKittySequence(data, CODEPOINTS.p, MODIFIERS.shift + MODIFIERS.ctrl);
647
+ export function isShiftEnter(data: string): boolean {
648
+ return matchesKey(data, "shift+enter");
332
649
  }
333
650
 
334
- /**
335
- * Check if input matches Shift+Ctrl+D (Kitty protocol only, for debug).
336
- * Ignores lock key bits.
337
- */
338
- export function isShiftCtrlD(data: string): boolean {
339
- return matchesKittySequence(data, CODEPOINTS.d, MODIFIERS.shift + MODIFIERS.ctrl);
651
+ export function isAltEnter(data: string): boolean {
652
+ return matchesKey(data, "alt+enter");
340
653
  }
341
654
 
342
- /**
343
- * Check if input matches Ctrl+T (raw byte or Kitty protocol).
344
- * Ignores lock key bits.
345
- */
346
- export function isCtrlT(data: string): boolean {
347
- return data === RAW.CTRL_T || data === Keys.CTRL_T || matchesKittySequence(data, CODEPOINTS.t, MODIFIERS.ctrl);
655
+ export function isAltBackspace(data: string): boolean {
656
+ return matchesKey(data, "alt+backspace");
348
657
  }
349
658
 
350
- /**
351
- * Check if input matches Ctrl+U (raw byte or Kitty protocol).
352
- * Ignores lock key bits.
353
- */
354
- export function isCtrlU(data: string): boolean {
355
- return data === RAW.CTRL_U || data === Keys.CTRL_U || matchesKittySequence(data, CODEPOINTS.u, MODIFIERS.ctrl);
659
+ export function isBackspace(data: string): boolean {
660
+ return matchesKey(data, "backspace");
356
661
  }
357
662
 
358
- /**
359
- * Check if input matches Ctrl+V (raw byte or Kitty protocol).
360
- * Ignores lock key bits.
361
- * Note: In most terminals, Ctrl+V triggers paste which sends bracketed paste markers,
362
- * but raw mode can intercept the key before the terminal processes it.
363
- */
364
- export function isCtrlV(data: string): boolean {
365
- return data === RAW.CTRL_V || data === Keys.CTRL_V || matchesKittySequence(data, CODEPOINTS.v, MODIFIERS.ctrl);
663
+ export function isDelete(data: string): boolean {
664
+ return matchesKey(data, "delete");
366
665
  }
367
666
 
368
- /**
369
- * Check if input matches Ctrl+W (raw byte or Kitty protocol).
370
- * Ignores lock key bits.
371
- */
372
- export function isCtrlW(data: string): boolean {
373
- return data === RAW.CTRL_W || data === Keys.CTRL_W || matchesKittySequence(data, CODEPOINTS.w, MODIFIERS.ctrl);
667
+ export function isHome(data: string): boolean {
668
+ return matchesKey(data, "home");
374
669
  }
375
670
 
376
- /**
377
- * Check if input matches Ctrl+Z (raw byte or Kitty protocol).
378
- * Ignores lock key bits.
379
- */
380
- export function isCtrlZ(data: string): boolean {
381
- return data === RAW.CTRL_Z || data === Keys.CTRL_Z || matchesKittySequence(data, CODEPOINTS.z, MODIFIERS.ctrl);
671
+ export function isEnd(data: string): boolean {
672
+ return matchesKey(data, "end");
382
673
  }
383
674
 
384
- /**
385
- * Check if input matches Alt+Backspace (legacy or Kitty protocol).
386
- * Ignores lock key bits.
387
- */
388
- export function isAltBackspace(data: string): boolean {
389
- return (
390
- data === RAW.ALT_BACKSPACE ||
391
- data === Keys.ALT_BACKSPACE ||
392
- matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt)
393
- );
675
+ export function isCtrlA(data: string): boolean {
676
+ return matchesKey(data, "ctrl+a");
394
677
  }
395
678
 
396
- /**
397
- * Check if input matches Shift+Tab (legacy or Kitty protocol).
398
- * Ignores lock key bits.
399
- */
400
- export function isShiftTab(data: string): boolean {
401
- return (
402
- data === RAW.SHIFT_TAB || data === Keys.SHIFT_TAB || matchesKittySequence(data, CODEPOINTS.tab, MODIFIERS.shift)
403
- );
679
+ export function isCtrlC(data: string): boolean {
680
+ return matchesKey(data, "ctrl+c");
404
681
  }
405
682
 
406
- /**
407
- * Check if input matches the Escape key (raw byte or Kitty protocol).
408
- * Raw: \x1b (single byte)
409
- * Kitty: \x1b[27u (codepoint 27 = escape)
410
- * Ignores lock key bits.
411
- */
412
- export function isEscape(data: string): boolean {
413
- return data === "\x1b" || data === `\x1b[${CODEPOINTS.escape}u` || matchesKittySequence(data, CODEPOINTS.escape, 0);
683
+ export function isCtrlD(data: string): boolean {
684
+ return matchesKey(data, "ctrl+d");
414
685
  }
415
686
 
416
- // Arrow key virtual codepoints (negative to avoid conflicts with real codepoints)
417
- const ARROW_CODEPOINTS = {
418
- up: -1,
419
- down: -2,
420
- right: -3,
421
- left: -4,
422
- } as const;
687
+ export function isCtrlE(data: string): boolean {
688
+ return matchesKey(data, "ctrl+e");
689
+ }
423
690
 
424
- /**
425
- * Check if input matches Arrow Up key.
426
- * Handles both legacy (\x1b[A) and Kitty protocol with modifiers.
427
- */
428
- export function isArrowUp(data: string): boolean {
429
- return data === "\x1b[A" || matchesKittySequence(data, ARROW_CODEPOINTS.up, 0);
691
+ export function isCtrlG(data: string): boolean {
692
+ return matchesKey(data, "ctrl+g");
430
693
  }
431
694
 
432
- /**
433
- * Check if input matches Arrow Down key.
434
- * Handles both legacy (\x1b[B) and Kitty protocol with modifiers.
435
- */
436
- export function isArrowDown(data: string): boolean {
437
- return data === "\x1b[B" || matchesKittySequence(data, ARROW_CODEPOINTS.down, 0);
695
+ export function isCtrlK(data: string): boolean {
696
+ return matchesKey(data, "ctrl+k");
438
697
  }
439
698
 
440
- /**
441
- * Check if input matches Arrow Right key.
442
- * Handles both legacy (\x1b[C) and Kitty protocol with modifiers.
443
- */
444
- export function isArrowRight(data: string): boolean {
445
- return data === "\x1b[C" || matchesKittySequence(data, ARROW_CODEPOINTS.right, 0);
699
+ export function isCtrlL(data: string): boolean {
700
+ return matchesKey(data, "ctrl+l");
446
701
  }
447
702
 
448
- /**
449
- * Check if input matches Arrow Left key.
450
- * Handles both legacy (\x1b[D) and Kitty protocol with modifiers.
451
- */
452
- export function isArrowLeft(data: string): boolean {
453
- return data === "\x1b[D" || matchesKittySequence(data, ARROW_CODEPOINTS.left, 0);
703
+ export function isCtrlO(data: string): boolean {
704
+ return matchesKey(data, "ctrl+o");
454
705
  }
455
706
 
456
- /**
457
- * Check if input matches plain Tab key (no modifiers).
458
- * Handles both legacy (\t) and Kitty protocol.
459
- */
460
- export function isTab(data: string): boolean {
461
- return data === "\t" || matchesKittySequence(data, CODEPOINTS.tab, 0);
707
+ export function isCtrlP(data: string): boolean {
708
+ return matchesKey(data, "ctrl+p");
462
709
  }
463
710
 
464
- /**
465
- * Check if input matches plain Enter/Return key (no modifiers).
466
- * Handles both legacy (\r) and Kitty protocol.
467
- */
468
- export function isEnter(data: string): boolean {
469
- return data === "\r" || matchesKittySequence(data, CODEPOINTS.enter, 0);
711
+ export function isCtrlT(data: string): boolean {
712
+ return matchesKey(data, "ctrl+t");
470
713
  }
471
714
 
472
- /**
473
- * Check if input matches plain Backspace key (no modifiers).
474
- * Handles both legacy (\x7f, \x08) and Kitty protocol.
475
- */
476
- export function isBackspace(data: string): boolean {
477
- return data === "\x7f" || data === "\x08" || matchesKittySequence(data, CODEPOINTS.backspace, 0);
715
+ export function isCtrlU(data: string): boolean {
716
+ return matchesKey(data, "ctrl+u");
478
717
  }
479
718
 
480
- /**
481
- * Check if input matches Shift+Enter.
482
- * Ignores lock key bits.
483
- */
484
- export function isShiftEnter(data: string): boolean {
485
- return data === Keys.SHIFT_ENTER || matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.shift);
719
+ export function isCtrlV(data: string): boolean {
720
+ return matchesKey(data, "ctrl+v");
486
721
  }
487
722
 
488
- /**
489
- * Check if input matches Alt+Enter.
490
- * Ignores lock key bits.
491
- */
492
- export function isAltEnter(data: string): boolean {
493
- return data === Keys.ALT_ENTER || data === "\x1b\r" || matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.alt);
723
+ export function isCtrlW(data: string): boolean {
724
+ return matchesKey(data, "ctrl+w");
494
725
  }
495
726
 
496
- /**
497
- * Check if input matches Option/Alt+Left (word navigation).
498
- * Handles multiple formats including Kitty protocol.
499
- */
500
- export function isAltLeft(data: string): boolean {
501
- return data === "\x1b[1;3D" || data === "\x1bb" || matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.alt);
727
+ export function isCtrlY(data: string): boolean {
728
+ return matchesKey(data, "ctrl+y");
502
729
  }
503
730
 
504
- /**
505
- * Check if input matches Option/Alt+Right (word navigation).
506
- * Handles multiple formats including Kitty protocol.
507
- */
508
- export function isAltRight(data: string): boolean {
509
- return data === "\x1b[1;3C" || data === "\x1bf" || matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.alt);
731
+ export function isCtrlZ(data: string): boolean {
732
+ return matchesKey(data, "ctrl+z");
510
733
  }
511
734
 
512
- /**
513
- * Check if input matches Ctrl+Left (word navigation).
514
- * Handles multiple formats including Kitty protocol.
515
- */
516
735
  export function isCtrlLeft(data: string): boolean {
517
- return data === "\x1b[1;5D" || matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.ctrl);
736
+ return matchesKey(data, "ctrl+left");
518
737
  }
519
738
 
520
- /**
521
- * Check if input matches Ctrl+Right (word navigation).
522
- * Handles multiple formats including Kitty protocol.
523
- */
524
739
  export function isCtrlRight(data: string): boolean {
525
- return data === "\x1b[1;5C" || matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.ctrl);
740
+ return matchesKey(data, "ctrl+right");
526
741
  }
527
742
 
528
- /**
529
- * Check if input matches Home key.
530
- * Handles legacy formats and Kitty protocol with lock key modifiers.
531
- */
532
- export function isHome(data: string): boolean {
533
- return (
534
- data === "\x1b[H" ||
535
- data === "\x1b[1~" ||
536
- data === "\x1b[7~" ||
537
- matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.home, 0)
538
- );
743
+ export function isAltLeft(data: string): boolean {
744
+ return matchesKey(data, "alt+left");
539
745
  }
540
746
 
541
- /**
542
- * Check if input matches End key.
543
- * Handles legacy formats and Kitty protocol with lock key modifiers.
544
- */
545
- export function isEnd(data: string): boolean {
546
- return (
547
- data === "\x1b[F" ||
548
- data === "\x1b[4~" ||
549
- data === "\x1b[8~" ||
550
- matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.end, 0)
551
- );
747
+ export function isAltRight(data: string): boolean {
748
+ return matchesKey(data, "alt+right");
749
+ }
750
+
751
+ export function isShiftCtrlD(data: string): boolean {
752
+ return matchesKey(data, "shift+ctrl+d") || matchesKey(data, "ctrl+shift+d");
753
+ }
754
+
755
+ export function isShiftCtrlO(data: string): boolean {
756
+ return matchesKey(data, "shift+ctrl+o") || matchesKey(data, "ctrl+shift+o");
757
+ }
758
+
759
+ export function isShiftCtrlP(data: string): boolean {
760
+ return matchesKey(data, "shift+ctrl+p") || matchesKey(data, "ctrl+shift+p");
761
+ }
762
+
763
+ export function isShiftBackspace(data: string): boolean {
764
+ return matchesKey(data, "shift+backspace");
765
+ }
766
+
767
+ export function isShiftDelete(data: string): boolean {
768
+ return matchesKey(data, "shift+delete");
769
+ }
770
+
771
+ export function isShiftSpace(data: string): boolean {
772
+ return matchesKey(data, "shift+space");
552
773
  }
553
774
 
554
775
  /**
555
- * Check if input matches Delete key (forward delete).
556
- * Handles legacy format and Kitty protocol with lock key modifiers.
776
+ * Check if input indicates Caps Lock state change.
777
+ * Kitty protocol reports Caps Lock via modifier bit 64.
557
778
  */
558
- export function isDelete(data: string): boolean {
559
- return data === "\x1b[3~" || matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.delete, 0);
779
+ export function isCapsLock(data: string): boolean {
780
+ const parsed = parseKittySequence(data);
781
+ if (!parsed) return false;
782
+ // Caps Lock is modifier bit 64
783
+ return (parsed.modifier & 64) !== 0;
560
784
  }