@mariozechner/pi-tui 0.32.2 → 0.33.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/dist/keys.js CHANGED
@@ -1,73 +1,75 @@
1
1
  /**
2
- * Kitty keyboard protocol key sequence helpers.
3
- *
4
- * The Kitty keyboard protocol sends enhanced escape sequences in the format:
5
- * \x1b[<codepoint>;<modifier>u
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
2
+ * Keyboard input handling for terminal applications.
16
3
  *
4
+ * Supports both legacy terminal sequences and Kitty keyboard protocol.
17
5
  * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
18
6
  *
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.
7
+ * API:
8
+ * - matchesKey(data, keyId) - Check if input matches a key identifier
9
+ * - parseKey(data) - Parse input and return the key identifier
10
+ * - Key - Helper object for creating typed key identifiers
22
11
  */
23
- // Common codepoints
24
- const CODEPOINTS = {
25
- // Letters (lowercase ASCII)
26
- a: 97,
27
- c: 99,
28
- d: 100,
29
- e: 101,
30
- g: 103,
31
- k: 107,
32
- l: 108,
33
- o: 111,
34
- p: 112,
35
- t: 116,
36
- u: 117,
37
- w: 119,
38
- z: 122,
12
+ /**
13
+ * Helper object for creating typed key identifiers with autocomplete.
14
+ *
15
+ * Usage:
16
+ * - Key.escape, Key.enter, Key.tab, etc. for special keys
17
+ * - Key.ctrl("c"), Key.alt("x") for single modifier
18
+ * - Key.ctrlShift("p"), Key.ctrlAlt("x") for combined modifiers
19
+ */
20
+ export const Key = {
39
21
  // Special keys
22
+ escape: "escape",
23
+ esc: "esc",
24
+ enter: "enter",
25
+ return: "return",
26
+ tab: "tab",
27
+ space: "space",
28
+ backspace: "backspace",
29
+ delete: "delete",
30
+ home: "home",
31
+ end: "end",
32
+ up: "up",
33
+ down: "down",
34
+ left: "left",
35
+ right: "right",
36
+ // Single modifiers
37
+ ctrl: (key) => `ctrl+${key}`,
38
+ shift: (key) => `shift+${key}`,
39
+ alt: (key) => `alt+${key}`,
40
+ // Combined modifiers
41
+ ctrlShift: (key) => `ctrl+shift+${key}`,
42
+ shiftCtrl: (key) => `shift+ctrl+${key}`,
43
+ ctrlAlt: (key) => `ctrl+alt+${key}`,
44
+ altCtrl: (key) => `alt+ctrl+${key}`,
45
+ shiftAlt: (key) => `shift+alt+${key}`,
46
+ altShift: (key) => `alt+shift+${key}`,
47
+ // Triple modifiers
48
+ ctrlShiftAlt: (key) => `ctrl+shift+alt+${key}`,
49
+ };
50
+ // =============================================================================
51
+ // Constants
52
+ // =============================================================================
53
+ const MODIFIERS = {
54
+ shift: 1,
55
+ alt: 2,
56
+ ctrl: 4,
57
+ };
58
+ const LOCK_MASK = 64 + 128; // Caps Lock + Num Lock
59
+ const CODEPOINTS = {
40
60
  escape: 27,
41
61
  tab: 9,
42
62
  enter: 13,
43
63
  space: 32,
44
64
  backspace: 127,
65
+ kpEnter: 57414, // Numpad Enter (Kitty protocol)
45
66
  };
46
- // Lock key bits to ignore when matching (Caps Lock + Num Lock)
47
- const LOCK_MASK = 64 + 128; // 192
48
- // Modifier bits (before adding 1)
49
- const MODIFIERS = {
50
- shift: 1,
51
- alt: 2,
52
- ctrl: 4,
53
- super: 8,
67
+ const ARROW_CODEPOINTS = {
68
+ up: -1,
69
+ down: -2,
70
+ right: -3,
71
+ left: -4,
54
72
  };
55
- /**
56
- * Build a Kitty keyboard protocol sequence for a key with modifier.
57
- */
58
- function kittySequence(codepoint, modifier) {
59
- return `\x1b[${codepoint};${modifier + 1}u`;
60
- }
61
- /**
62
- * Parse a Kitty keyboard protocol sequence.
63
- * Handles formats:
64
- * - \x1b[<codepoint>u (no modifier)
65
- * - \x1b[<codepoint>;<modifier>u (with modifier)
66
- * - \x1b[1;<modifier>A/B/C/D (arrow keys with modifier)
67
- *
68
- * Returns null if not a valid Kitty sequence.
69
- */
70
- // Virtual codepoints for functional keys (negative to avoid conflicts)
71
73
  const FUNCTIONAL_CODEPOINTS = {
72
74
  delete: -10,
73
75
  insert: -11,
@@ -77,43 +79,39 @@ const FUNCTIONAL_CODEPOINTS = {
77
79
  end: -15,
78
80
  };
79
81
  function parseKittySequence(data) {
80
- // Match CSI u format: \x1b[<num>u or \x1b[<num>;<mod>u
82
+ // CSI u format: \x1b[<num>u or \x1b[<num>;<mod>u
81
83
  const csiUMatch = data.match(/^\x1b\[(\d+)(?:;(\d+))?u$/);
82
84
  if (csiUMatch) {
83
85
  const codepoint = parseInt(csiUMatch[1], 10);
84
86
  const modValue = csiUMatch[2] ? parseInt(csiUMatch[2], 10) : 1;
85
87
  return { codepoint, modifier: modValue - 1 };
86
88
  }
87
- // Match arrow keys with modifier: \x1b[1;<mod>A/B/C/D
89
+ // Arrow keys with modifier: \x1b[1;<mod>A/B/C/D
88
90
  const arrowMatch = data.match(/^\x1b\[1;(\d+)([ABCD])$/);
89
91
  if (arrowMatch) {
90
92
  const modValue = parseInt(arrowMatch[1], 10);
91
- // Map arrow letters to virtual codepoints for easier matching
92
93
  const arrowCodes = { A: -1, B: -2, C: -3, D: -4 };
93
- const codepoint = arrowCodes[arrowMatch[2]];
94
- return { codepoint, modifier: modValue - 1 };
94
+ return { codepoint: arrowCodes[arrowMatch[2]], modifier: modValue - 1 };
95
95
  }
96
- // Match functional keys with ~ terminator: \x1b[<num>~ or \x1b[<num>;<mod>~
97
- // DELETE=3, INSERT=2, PAGEUP=5, PAGEDOWN=6, etc.
96
+ // Functional keys: \x1b[<num>~ or \x1b[<num>;<mod>~
98
97
  const funcMatch = data.match(/^\x1b\[(\d+)(?:;(\d+))?~$/);
99
98
  if (funcMatch) {
100
99
  const keyNum = parseInt(funcMatch[1], 10);
101
100
  const modValue = funcMatch[2] ? parseInt(funcMatch[2], 10) : 1;
102
- // Map functional key numbers to virtual codepoints
103
101
  const funcCodes = {
104
102
  2: FUNCTIONAL_CODEPOINTS.insert,
105
103
  3: FUNCTIONAL_CODEPOINTS.delete,
106
104
  5: FUNCTIONAL_CODEPOINTS.pageUp,
107
105
  6: FUNCTIONAL_CODEPOINTS.pageDown,
108
- 7: FUNCTIONAL_CODEPOINTS.home, // Alternative home
109
- 8: FUNCTIONAL_CODEPOINTS.end, // Alternative end
106
+ 7: FUNCTIONAL_CODEPOINTS.home,
107
+ 8: FUNCTIONAL_CODEPOINTS.end,
110
108
  };
111
109
  const codepoint = funcCodes[keyNum];
112
110
  if (codepoint !== undefined) {
113
111
  return { codepoint, modifier: modValue - 1 };
114
112
  }
115
113
  }
116
- // Match Home/End with modifier: \x1b[1;<mod>H/F
114
+ // Home/End with modifier: \x1b[1;<mod>H/F
117
115
  const homeEndMatch = data.match(/^\x1b\[1;(\d+)([HF])$/);
118
116
  if (homeEndMatch) {
119
117
  const modValue = parseInt(homeEndMatch[1], 10);
@@ -122,381 +120,276 @@ function parseKittySequence(data) {
122
120
  }
123
121
  return null;
124
122
  }
125
- /**
126
- * Check if a Kitty sequence matches the expected codepoint and modifier,
127
- * ignoring lock key bits (Caps Lock, Num Lock).
128
- */
129
123
  function matchesKittySequence(data, expectedCodepoint, expectedModifier) {
130
124
  const parsed = parseKittySequence(data);
131
125
  if (!parsed)
132
126
  return false;
133
- // Mask out lock bits from both sides for comparison
134
127
  const actualMod = parsed.modifier & ~LOCK_MASK;
135
128
  const expectedMod = expectedModifier & ~LOCK_MASK;
136
129
  return parsed.codepoint === expectedCodepoint && actualMod === expectedMod;
137
130
  }
138
- // Pre-built sequences for common key combinations
139
- export const Keys = {
140
- // Ctrl+<letter> combinations
141
- CTRL_A: kittySequence(CODEPOINTS.a, MODIFIERS.ctrl),
142
- CTRL_C: kittySequence(CODEPOINTS.c, MODIFIERS.ctrl),
143
- CTRL_D: kittySequence(CODEPOINTS.d, MODIFIERS.ctrl),
144
- CTRL_E: kittySequence(CODEPOINTS.e, MODIFIERS.ctrl),
145
- CTRL_G: kittySequence(CODEPOINTS.g, MODIFIERS.ctrl),
146
- CTRL_K: kittySequence(CODEPOINTS.k, MODIFIERS.ctrl),
147
- CTRL_L: kittySequence(CODEPOINTS.l, MODIFIERS.ctrl),
148
- CTRL_O: kittySequence(CODEPOINTS.o, MODIFIERS.ctrl),
149
- CTRL_P: kittySequence(CODEPOINTS.p, MODIFIERS.ctrl),
150
- CTRL_T: kittySequence(CODEPOINTS.t, MODIFIERS.ctrl),
151
- CTRL_U: kittySequence(CODEPOINTS.u, MODIFIERS.ctrl),
152
- CTRL_W: kittySequence(CODEPOINTS.w, MODIFIERS.ctrl),
153
- CTRL_Z: kittySequence(CODEPOINTS.z, MODIFIERS.ctrl),
154
- // Enter combinations
155
- SHIFT_ENTER: kittySequence(CODEPOINTS.enter, MODIFIERS.shift),
156
- ALT_ENTER: kittySequence(CODEPOINTS.enter, MODIFIERS.alt),
157
- CTRL_ENTER: kittySequence(CODEPOINTS.enter, MODIFIERS.ctrl),
158
- // Tab combinations
159
- SHIFT_TAB: kittySequence(CODEPOINTS.tab, MODIFIERS.shift),
160
- // Backspace combinations
161
- ALT_BACKSPACE: kittySequence(CODEPOINTS.backspace, MODIFIERS.alt),
162
- };
163
- /**
164
- * Check if input matches a Kitty protocol Ctrl+<key> sequence.
165
- * Ignores lock key bits (Caps Lock, Num Lock).
166
- * @param data - The input data to check
167
- * @param key - Single lowercase letter (e.g., 'c' for Ctrl+C)
131
+ // =============================================================================
132
+ // Generic Key Matching
133
+ // =============================================================================
134
+ function rawCtrlChar(letter) {
135
+ const code = letter.toLowerCase().charCodeAt(0) - 96;
136
+ return String.fromCharCode(code);
137
+ }
138
+ function parseKeyId(keyId) {
139
+ const parts = keyId.toLowerCase().split("+");
140
+ const key = parts[parts.length - 1];
141
+ if (!key)
142
+ return null;
143
+ return {
144
+ key,
145
+ ctrl: parts.includes("ctrl"),
146
+ shift: parts.includes("shift"),
147
+ alt: parts.includes("alt"),
148
+ };
149
+ }
150
+ /**
151
+ * Match input data against a key identifier string.
152
+ *
153
+ * Supported key identifiers:
154
+ * - Single keys: "escape", "tab", "enter", "backspace", "delete", "home", "end", "space"
155
+ * - Arrow keys: "up", "down", "left", "right"
156
+ * - Ctrl combinations: "ctrl+c", "ctrl+z", etc.
157
+ * - Shift combinations: "shift+tab", "shift+enter"
158
+ * - Alt combinations: "alt+enter", "alt+backspace"
159
+ * - Combined modifiers: "shift+ctrl+p", "ctrl+alt+x"
160
+ *
161
+ * Use the Key helper for autocomplete: Key.ctrl("c"), Key.escape, Key.ctrlShift("p")
162
+ *
163
+ * @param data - Raw input data from terminal
164
+ * @param keyId - Key identifier (e.g., "ctrl+c", "escape", Key.ctrl("c"))
168
165
  */
169
- export function isKittyCtrl(data, key) {
170
- if (key.length !== 1)
166
+ export function matchesKey(data, keyId) {
167
+ const parsed = parseKeyId(keyId);
168
+ if (!parsed)
171
169
  return false;
172
- const codepoint = key.charCodeAt(0);
173
- // Check exact match first (fast path)
174
- if (data === kittySequence(codepoint, MODIFIERS.ctrl))
175
- return true;
176
- // Check with lock bits masked out
177
- return matchesKittySequence(data, codepoint, MODIFIERS.ctrl);
178
- }
179
- /**
180
- * Check if input matches a Kitty protocol key sequence with specific modifier.
181
- * Ignores lock key bits (Caps Lock, Num Lock).
182
- * @param data - The input data to check
183
- * @param codepoint - ASCII codepoint of the key
184
- * @param modifier - Modifier value (use MODIFIERS constants)
185
- */
186
- export function isKittyKey(data, codepoint, modifier) {
187
- // Check exact match first (fast path)
188
- if (data === kittySequence(codepoint, modifier))
189
- return true;
190
- // Check with lock bits masked out
191
- return matchesKittySequence(data, codepoint, modifier);
192
- }
193
- // Raw control character codes
194
- const RAW = {
195
- CTRL_A: "\x01",
196
- CTRL_C: "\x03",
197
- CTRL_D: "\x04",
198
- CTRL_E: "\x05",
199
- CTRL_G: "\x07",
200
- CTRL_K: "\x0b",
201
- CTRL_L: "\x0c",
202
- CTRL_O: "\x0f",
203
- CTRL_P: "\x10",
204
- CTRL_T: "\x14",
205
- CTRL_U: "\x15",
206
- CTRL_W: "\x17",
207
- CTRL_Z: "\x1a",
208
- ALT_BACKSPACE: "\x1b\x7f",
209
- SHIFT_TAB: "\x1b[Z",
210
- };
211
- /**
212
- * Check if input matches Ctrl+A (raw byte or Kitty protocol).
213
- * Ignores lock key bits.
214
- */
215
- export function isCtrlA(data) {
216
- return data === RAW.CTRL_A || data === Keys.CTRL_A || matchesKittySequence(data, CODEPOINTS.a, MODIFIERS.ctrl);
217
- }
218
- /**
219
- * Check if input matches Ctrl+C (raw byte or Kitty protocol).
220
- * Ignores lock key bits.
221
- */
222
- export function isCtrlC(data) {
223
- return data === RAW.CTRL_C || data === Keys.CTRL_C || matchesKittySequence(data, CODEPOINTS.c, MODIFIERS.ctrl);
224
- }
225
- /**
226
- * Check if input matches Ctrl+D (raw byte or Kitty protocol).
227
- * Ignores lock key bits.
228
- */
229
- export function isCtrlD(data) {
230
- return data === RAW.CTRL_D || data === Keys.CTRL_D || matchesKittySequence(data, CODEPOINTS.d, MODIFIERS.ctrl);
231
- }
232
- /**
233
- * Check if input matches Ctrl+E (raw byte or Kitty protocol).
234
- * Ignores lock key bits.
235
- */
236
- export function isCtrlE(data) {
237
- return data === RAW.CTRL_E || data === Keys.CTRL_E || matchesKittySequence(data, CODEPOINTS.e, MODIFIERS.ctrl);
238
- }
239
- /**
240
- * Check if input matches Ctrl+G (raw byte or Kitty protocol).
241
- * Ignores lock key bits.
242
- */
243
- export function isCtrlG(data) {
244
- return data === RAW.CTRL_G || data === Keys.CTRL_G || matchesKittySequence(data, CODEPOINTS.g, MODIFIERS.ctrl);
245
- }
246
- /**
247
- * Check if input matches Ctrl+K (raw byte or Kitty protocol).
248
- * Ignores lock key bits.
249
- * Also checks if first byte is 0x0b for compatibility with terminals
250
- * that may send trailing bytes.
251
- */
252
- export function isCtrlK(data) {
253
- return (data === RAW.CTRL_K ||
254
- (data.length > 0 && data.charCodeAt(0) === 0x0b) ||
255
- data === Keys.CTRL_K ||
256
- matchesKittySequence(data, CODEPOINTS.k, MODIFIERS.ctrl));
257
- }
258
- /**
259
- * Check if input matches Ctrl+L (raw byte or Kitty protocol).
260
- * Ignores lock key bits.
261
- */
262
- export function isCtrlL(data) {
263
- return data === RAW.CTRL_L || data === Keys.CTRL_L || matchesKittySequence(data, CODEPOINTS.l, MODIFIERS.ctrl);
264
- }
265
- /**
266
- * Check if input matches Ctrl+O (raw byte or Kitty protocol).
267
- * Ignores lock key bits.
268
- */
269
- export function isCtrlO(data) {
270
- return data === RAW.CTRL_O || data === Keys.CTRL_O || matchesKittySequence(data, CODEPOINTS.o, MODIFIERS.ctrl);
271
- }
272
- /**
273
- * Check if input matches Shift+Ctrl+O (Kitty protocol only).
274
- * Ignores lock key bits.
275
- */
276
- export function isShiftCtrlO(data) {
277
- return matchesKittySequence(data, CODEPOINTS.o, MODIFIERS.shift + MODIFIERS.ctrl);
278
- }
279
- /**
280
- * Check if input matches Ctrl+P (raw byte or Kitty protocol).
281
- * Ignores lock key bits.
282
- */
283
- export function isCtrlP(data) {
284
- return data === RAW.CTRL_P || data === Keys.CTRL_P || matchesKittySequence(data, CODEPOINTS.p, MODIFIERS.ctrl);
285
- }
286
- /**
287
- * Check if input matches Shift+Ctrl+P (Kitty protocol only).
288
- * Ignores lock key bits.
289
- */
290
- export function isShiftCtrlP(data) {
291
- return matchesKittySequence(data, CODEPOINTS.p, MODIFIERS.shift + MODIFIERS.ctrl);
292
- }
293
- /**
294
- * Check if input matches Shift+Ctrl+D (Kitty protocol only, for debug).
295
- * Ignores lock key bits.
296
- */
297
- export function isShiftCtrlD(data) {
298
- return matchesKittySequence(data, CODEPOINTS.d, MODIFIERS.shift + MODIFIERS.ctrl);
299
- }
300
- /**
301
- * Check if input matches Ctrl+T (raw byte or Kitty protocol).
302
- * Ignores lock key bits.
303
- */
304
- export function isCtrlT(data) {
305
- return data === RAW.CTRL_T || data === Keys.CTRL_T || matchesKittySequence(data, CODEPOINTS.t, MODIFIERS.ctrl);
306
- }
307
- /**
308
- * Check if input matches Ctrl+U (raw byte or Kitty protocol).
309
- * Ignores lock key bits.
310
- */
311
- export function isCtrlU(data) {
312
- return data === RAW.CTRL_U || data === Keys.CTRL_U || matchesKittySequence(data, CODEPOINTS.u, MODIFIERS.ctrl);
313
- }
314
- /**
315
- * Check if input matches Ctrl+W (raw byte or Kitty protocol).
316
- * Ignores lock key bits.
317
- */
318
- export function isCtrlW(data) {
319
- return data === RAW.CTRL_W || data === Keys.CTRL_W || matchesKittySequence(data, CODEPOINTS.w, MODIFIERS.ctrl);
320
- }
321
- /**
322
- * Check if input matches Ctrl+Z (raw byte or Kitty protocol).
323
- * Ignores lock key bits.
324
- */
325
- export function isCtrlZ(data) {
326
- return data === RAW.CTRL_Z || data === Keys.CTRL_Z || matchesKittySequence(data, CODEPOINTS.z, MODIFIERS.ctrl);
327
- }
328
- /**
329
- * Check if input matches Alt+Backspace (legacy or Kitty protocol).
330
- * Ignores lock key bits.
331
- */
332
- export function isAltBackspace(data) {
333
- return (data === RAW.ALT_BACKSPACE ||
334
- data === Keys.ALT_BACKSPACE ||
335
- matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt));
336
- }
337
- /**
338
- * Check if input matches Shift+Tab (legacy or Kitty protocol).
339
- * Ignores lock key bits.
340
- */
341
- export function isShiftTab(data) {
342
- return (data === RAW.SHIFT_TAB || data === Keys.SHIFT_TAB || matchesKittySequence(data, CODEPOINTS.tab, MODIFIERS.shift));
343
- }
344
- /**
345
- * Check if input matches the Escape key (raw byte or Kitty protocol).
346
- * Raw: \x1b (single byte)
347
- * Kitty: \x1b[27u (codepoint 27 = escape)
348
- * Ignores lock key bits.
349
- */
350
- export function isEscape(data) {
351
- return data === "\x1b" || data === `\x1b[${CODEPOINTS.escape}u` || matchesKittySequence(data, CODEPOINTS.escape, 0);
352
- }
353
- // Arrow key virtual codepoints (negative to avoid conflicts with real codepoints)
354
- const ARROW_CODEPOINTS = {
355
- up: -1,
356
- down: -2,
357
- right: -3,
358
- left: -4,
359
- };
360
- /**
361
- * Check if input matches Arrow Up key.
362
- * Handles both legacy (\x1b[A) and Kitty protocol with modifiers.
363
- */
364
- export function isArrowUp(data) {
365
- return data === "\x1b[A" || matchesKittySequence(data, ARROW_CODEPOINTS.up, 0);
366
- }
367
- /**
368
- * Check if input matches Arrow Down key.
369
- * Handles both legacy (\x1b[B) and Kitty protocol with modifiers.
370
- */
371
- export function isArrowDown(data) {
372
- return data === "\x1b[B" || matchesKittySequence(data, ARROW_CODEPOINTS.down, 0);
373
- }
374
- /**
375
- * Check if input matches Arrow Right key.
376
- * Handles both legacy (\x1b[C) and Kitty protocol with modifiers.
377
- */
378
- export function isArrowRight(data) {
379
- return data === "\x1b[C" || matchesKittySequence(data, ARROW_CODEPOINTS.right, 0);
380
- }
381
- /**
382
- * Check if input matches Arrow Left key.
383
- * Handles both legacy (\x1b[D) and Kitty protocol with modifiers.
384
- */
385
- export function isArrowLeft(data) {
386
- return data === "\x1b[D" || matchesKittySequence(data, ARROW_CODEPOINTS.left, 0);
387
- }
388
- /**
389
- * Check if input matches plain Tab key (no modifiers).
390
- * Handles both legacy (\t) and Kitty protocol.
391
- */
392
- export function isTab(data) {
393
- return data === "\t" || matchesKittySequence(data, CODEPOINTS.tab, 0);
394
- }
395
- /**
396
- * Check if input matches plain Enter/Return key (no modifiers).
397
- * Handles both legacy (\r) and Kitty protocol.
398
- */
399
- export function isEnter(data) {
400
- return data === "\r" || matchesKittySequence(data, CODEPOINTS.enter, 0);
401
- }
402
- /**
403
- * Check if input matches plain Backspace key (no modifiers).
404
- * Handles both legacy (\x7f, \x08) and Kitty protocol.
405
- */
406
- export function isBackspace(data) {
407
- return data === "\x7f" || data === "\x08" || matchesKittySequence(data, CODEPOINTS.backspace, 0);
408
- }
409
- /**
410
- * Check if input matches Shift+Backspace (Kitty protocol).
411
- * Returns true so caller can treat it as regular backspace.
412
- * Ignores lock key bits.
413
- */
414
- export function isShiftBackspace(data) {
415
- return matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.shift);
416
- }
417
- /**
418
- * Check if input matches Shift+Enter.
419
- * Ignores lock key bits.
420
- */
421
- export function isShiftEnter(data) {
422
- return data === Keys.SHIFT_ENTER || matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.shift);
423
- }
424
- /**
425
- * Check if input matches Alt+Enter.
426
- * Ignores lock key bits.
427
- */
428
- export function isAltEnter(data) {
429
- return data === Keys.ALT_ENTER || data === "\x1b\r" || matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.alt);
430
- }
431
- /**
432
- * Check if input matches Shift+Space (Kitty protocol).
433
- * Returns true so caller can insert a regular space.
434
- * Ignores lock key bits.
435
- */
436
- export function isShiftSpace(data) {
437
- return matchesKittySequence(data, CODEPOINTS.space, MODIFIERS.shift);
438
- }
439
- /**
440
- * Check if input matches Option/Alt+Left (word navigation).
441
- * Handles multiple formats including Kitty protocol.
442
- */
443
- export function isAltLeft(data) {
444
- return data === "\x1b[1;3D" || data === "\x1bb" || matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.alt);
445
- }
446
- /**
447
- * Check if input matches Option/Alt+Right (word navigation).
448
- * Handles multiple formats including Kitty protocol.
449
- */
450
- export function isAltRight(data) {
451
- return data === "\x1b[1;3C" || data === "\x1bf" || matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.alt);
452
- }
453
- /**
454
- * Check if input matches Ctrl+Left (word navigation).
455
- * Handles multiple formats including Kitty protocol.
456
- */
457
- export function isCtrlLeft(data) {
458
- return data === "\x1b[1;5D" || matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.ctrl);
459
- }
460
- /**
461
- * Check if input matches Ctrl+Right (word navigation).
462
- * Handles multiple formats including Kitty protocol.
463
- */
464
- export function isCtrlRight(data) {
465
- return data === "\x1b[1;5C" || matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.ctrl);
466
- }
467
- /**
468
- * Check if input matches Home key.
469
- * Handles legacy formats and Kitty protocol with lock key modifiers.
470
- */
471
- export function isHome(data) {
472
- return (data === "\x1b[H" ||
473
- data === "\x1b[1~" ||
474
- data === "\x1b[7~" ||
475
- matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.home, 0));
476
- }
477
- /**
478
- * Check if input matches End key.
479
- * Handles legacy formats and Kitty protocol with lock key modifiers.
480
- */
481
- export function isEnd(data) {
482
- return (data === "\x1b[F" ||
483
- data === "\x1b[4~" ||
484
- data === "\x1b[8~" ||
485
- matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.end, 0));
486
- }
487
- /**
488
- * Check if input matches Delete key (forward delete).
489
- * Handles legacy format and Kitty protocol with lock key modifiers.
490
- */
491
- export function isDelete(data) {
492
- return data === "\x1b[3~" || matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.delete, 0);
170
+ const { key, ctrl, shift, alt } = parsed;
171
+ let modifier = 0;
172
+ if (shift)
173
+ modifier |= MODIFIERS.shift;
174
+ if (alt)
175
+ modifier |= MODIFIERS.alt;
176
+ if (ctrl)
177
+ modifier |= MODIFIERS.ctrl;
178
+ switch (key) {
179
+ case "escape":
180
+ case "esc":
181
+ if (modifier !== 0)
182
+ return false;
183
+ return data === "\x1b" || matchesKittySequence(data, CODEPOINTS.escape, 0);
184
+ case "space":
185
+ if (modifier === 0) {
186
+ return data === " " || matchesKittySequence(data, CODEPOINTS.space, 0);
187
+ }
188
+ return matchesKittySequence(data, CODEPOINTS.space, modifier);
189
+ case "tab":
190
+ if (shift && !ctrl && !alt) {
191
+ return data === "\x1b[Z" || matchesKittySequence(data, CODEPOINTS.tab, MODIFIERS.shift);
192
+ }
193
+ if (modifier === 0) {
194
+ return data === "\t" || matchesKittySequence(data, CODEPOINTS.tab, 0);
195
+ }
196
+ return matchesKittySequence(data, CODEPOINTS.tab, modifier);
197
+ case "enter":
198
+ case "return":
199
+ if (shift && !ctrl && !alt) {
200
+ return (matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.shift) ||
201
+ matchesKittySequence(data, CODEPOINTS.kpEnter, MODIFIERS.shift));
202
+ }
203
+ if (alt && !ctrl && !shift) {
204
+ return (data === "\x1b\r" ||
205
+ matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.alt) ||
206
+ matchesKittySequence(data, CODEPOINTS.kpEnter, MODIFIERS.alt));
207
+ }
208
+ if (modifier === 0) {
209
+ return (data === "\r" ||
210
+ data === "\x1bOM" || // SS3 M (numpad enter in some terminals)
211
+ matchesKittySequence(data, CODEPOINTS.enter, 0) ||
212
+ matchesKittySequence(data, CODEPOINTS.kpEnter, 0));
213
+ }
214
+ return (matchesKittySequence(data, CODEPOINTS.enter, modifier) ||
215
+ matchesKittySequence(data, CODEPOINTS.kpEnter, modifier));
216
+ case "backspace":
217
+ if (alt && !ctrl && !shift) {
218
+ return data === "\x1b\x7f" || matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt);
219
+ }
220
+ if (modifier === 0) {
221
+ return data === "\x7f" || data === "\x08" || matchesKittySequence(data, CODEPOINTS.backspace, 0);
222
+ }
223
+ return matchesKittySequence(data, CODEPOINTS.backspace, modifier);
224
+ case "delete":
225
+ if (modifier === 0) {
226
+ return data === "\x1b[3~" || matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.delete, 0);
227
+ }
228
+ return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.delete, modifier);
229
+ case "home":
230
+ if (modifier === 0) {
231
+ return (data === "\x1b[H" ||
232
+ data === "\x1b[1~" ||
233
+ data === "\x1b[7~" ||
234
+ matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.home, 0));
235
+ }
236
+ return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.home, modifier);
237
+ case "end":
238
+ if (modifier === 0) {
239
+ return (data === "\x1b[F" ||
240
+ data === "\x1b[4~" ||
241
+ data === "\x1b[8~" ||
242
+ matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.end, 0));
243
+ }
244
+ return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.end, modifier);
245
+ case "up":
246
+ if (modifier === 0) {
247
+ return data === "\x1b[A" || matchesKittySequence(data, ARROW_CODEPOINTS.up, 0);
248
+ }
249
+ return matchesKittySequence(data, ARROW_CODEPOINTS.up, modifier);
250
+ case "down":
251
+ if (modifier === 0) {
252
+ return data === "\x1b[B" || matchesKittySequence(data, ARROW_CODEPOINTS.down, 0);
253
+ }
254
+ return matchesKittySequence(data, ARROW_CODEPOINTS.down, modifier);
255
+ case "left":
256
+ if (alt && !ctrl && !shift) {
257
+ return (data === "\x1b[1;3D" ||
258
+ data === "\x1bb" ||
259
+ matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.alt));
260
+ }
261
+ if (ctrl && !alt && !shift) {
262
+ return data === "\x1b[1;5D" || matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.ctrl);
263
+ }
264
+ if (modifier === 0) {
265
+ return data === "\x1b[D" || matchesKittySequence(data, ARROW_CODEPOINTS.left, 0);
266
+ }
267
+ return matchesKittySequence(data, ARROW_CODEPOINTS.left, modifier);
268
+ case "right":
269
+ if (alt && !ctrl && !shift) {
270
+ return (data === "\x1b[1;3C" ||
271
+ data === "\x1bf" ||
272
+ matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.alt));
273
+ }
274
+ if (ctrl && !alt && !shift) {
275
+ return data === "\x1b[1;5C" || matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.ctrl);
276
+ }
277
+ if (modifier === 0) {
278
+ return data === "\x1b[C" || matchesKittySequence(data, ARROW_CODEPOINTS.right, 0);
279
+ }
280
+ return matchesKittySequence(data, ARROW_CODEPOINTS.right, modifier);
281
+ }
282
+ // Handle single letter keys (a-z)
283
+ if (key.length === 1 && key >= "a" && key <= "z") {
284
+ const codepoint = key.charCodeAt(0);
285
+ if (ctrl && !shift && !alt) {
286
+ const raw = rawCtrlChar(key);
287
+ if (data === raw)
288
+ return true;
289
+ if (data.length > 0 && data.charCodeAt(0) === raw.charCodeAt(0))
290
+ return true;
291
+ return matchesKittySequence(data, codepoint, MODIFIERS.ctrl);
292
+ }
293
+ if (ctrl && shift && !alt) {
294
+ return matchesKittySequence(data, codepoint, MODIFIERS.shift + MODIFIERS.ctrl);
295
+ }
296
+ if (modifier !== 0) {
297
+ return matchesKittySequence(data, codepoint, modifier);
298
+ }
299
+ return data === key;
300
+ }
301
+ return false;
493
302
  }
494
303
  /**
495
- * Check if input matches Shift+Delete (Kitty protocol).
496
- * Returns true so caller can treat it as regular delete.
497
- * Ignores lock key bits.
498
- */
499
- export function isShiftDelete(data) {
500
- return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.delete, MODIFIERS.shift);
304
+ * Parse input data and return the key identifier if recognized.
305
+ *
306
+ * @param data - Raw input data from terminal
307
+ * @returns Key identifier string (e.g., "ctrl+c") or undefined
308
+ */
309
+ export function parseKey(data) {
310
+ const kitty = parseKittySequence(data);
311
+ if (kitty) {
312
+ const { codepoint, modifier } = kitty;
313
+ const mods = [];
314
+ const effectiveMod = modifier & ~LOCK_MASK;
315
+ if (effectiveMod & MODIFIERS.shift)
316
+ mods.push("shift");
317
+ if (effectiveMod & MODIFIERS.ctrl)
318
+ mods.push("ctrl");
319
+ if (effectiveMod & MODIFIERS.alt)
320
+ mods.push("alt");
321
+ let keyName;
322
+ if (codepoint === CODEPOINTS.escape)
323
+ keyName = "escape";
324
+ else if (codepoint === CODEPOINTS.tab)
325
+ keyName = "tab";
326
+ else if (codepoint === CODEPOINTS.enter || codepoint === CODEPOINTS.kpEnter)
327
+ keyName = "enter";
328
+ else if (codepoint === CODEPOINTS.space)
329
+ keyName = "space";
330
+ else if (codepoint === CODEPOINTS.backspace)
331
+ keyName = "backspace";
332
+ else if (codepoint === FUNCTIONAL_CODEPOINTS.delete)
333
+ keyName = "delete";
334
+ else if (codepoint === FUNCTIONAL_CODEPOINTS.home)
335
+ keyName = "home";
336
+ else if (codepoint === FUNCTIONAL_CODEPOINTS.end)
337
+ keyName = "end";
338
+ else if (codepoint === ARROW_CODEPOINTS.up)
339
+ keyName = "up";
340
+ else if (codepoint === ARROW_CODEPOINTS.down)
341
+ keyName = "down";
342
+ else if (codepoint === ARROW_CODEPOINTS.left)
343
+ keyName = "left";
344
+ else if (codepoint === ARROW_CODEPOINTS.right)
345
+ keyName = "right";
346
+ else if (codepoint >= 97 && codepoint <= 122)
347
+ keyName = String.fromCharCode(codepoint);
348
+ if (keyName) {
349
+ return mods.length > 0 ? `${mods.join("+")}+${keyName}` : keyName;
350
+ }
351
+ }
352
+ // Legacy sequences
353
+ if (data === "\x1b")
354
+ return "escape";
355
+ if (data === "\t")
356
+ return "tab";
357
+ if (data === "\r" || data === "\x1bOM")
358
+ return "enter";
359
+ if (data === " ")
360
+ return "space";
361
+ if (data === "\x7f" || data === "\x08")
362
+ return "backspace";
363
+ if (data === "\x1b[Z")
364
+ return "shift+tab";
365
+ if (data === "\x1b\r")
366
+ return "alt+enter";
367
+ if (data === "\x1b\x7f")
368
+ return "alt+backspace";
369
+ if (data === "\x1b[A")
370
+ return "up";
371
+ if (data === "\x1b[B")
372
+ return "down";
373
+ if (data === "\x1b[C")
374
+ return "right";
375
+ if (data === "\x1b[D")
376
+ return "left";
377
+ if (data === "\x1b[H")
378
+ return "home";
379
+ if (data === "\x1b[F")
380
+ return "end";
381
+ if (data === "\x1b[3~")
382
+ return "delete";
383
+ // Raw Ctrl+letter
384
+ if (data.length === 1) {
385
+ const code = data.charCodeAt(0);
386
+ if (code >= 1 && code <= 26) {
387
+ return `ctrl+${String.fromCharCode(code + 96)}`;
388
+ }
389
+ if (code >= 32 && code <= 126) {
390
+ return data;
391
+ }
392
+ }
393
+ return undefined;
501
394
  }
502
395
  //# sourceMappingURL=keys.js.map