@gajae-code/tui 0.1.1

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +818 -0
  2. package/README.md +704 -0
  3. package/dist/types/autocomplete.d.ts +76 -0
  4. package/dist/types/bracketed-paste.d.ts +26 -0
  5. package/dist/types/components/box.d.ts +15 -0
  6. package/dist/types/components/cancellable-loader.d.ts +21 -0
  7. package/dist/types/components/editor.d.ts +101 -0
  8. package/dist/types/components/image.d.ts +16 -0
  9. package/dist/types/components/input.d.ts +16 -0
  10. package/dist/types/components/loader.d.ts +13 -0
  11. package/dist/types/components/markdown.d.ts +61 -0
  12. package/dist/types/components/select-list.d.ts +46 -0
  13. package/dist/types/components/settings-list.d.ts +39 -0
  14. package/dist/types/components/spacer.d.ts +11 -0
  15. package/dist/types/components/tab-bar.d.ts +56 -0
  16. package/dist/types/components/text.d.ts +13 -0
  17. package/dist/types/components/truncated-text.d.ts +10 -0
  18. package/dist/types/editor-component.d.ts +36 -0
  19. package/dist/types/fuzzy.d.ts +15 -0
  20. package/dist/types/index.d.ts +25 -0
  21. package/dist/types/keybindings.d.ts +189 -0
  22. package/dist/types/keys.d.ts +208 -0
  23. package/dist/types/kill-ring.d.ts +27 -0
  24. package/dist/types/stdin-buffer.d.ts +43 -0
  25. package/dist/types/symbols.d.ts +23 -0
  26. package/dist/types/terminal-capabilities.d.ts +75 -0
  27. package/dist/types/terminal.d.ts +61 -0
  28. package/dist/types/ttyid.d.ts +9 -0
  29. package/dist/types/tui.d.ts +161 -0
  30. package/dist/types/utils.d.ts +74 -0
  31. package/package.json +73 -0
  32. package/src/autocomplete.ts +836 -0
  33. package/src/bracketed-paste.ts +47 -0
  34. package/src/components/box.ts +144 -0
  35. package/src/components/cancellable-loader.ts +40 -0
  36. package/src/components/editor.ts +2664 -0
  37. package/src/components/image.ts +90 -0
  38. package/src/components/input.ts +465 -0
  39. package/src/components/loader.ts +86 -0
  40. package/src/components/markdown.ts +1009 -0
  41. package/src/components/select-list.ts +249 -0
  42. package/src/components/settings-list.ts +211 -0
  43. package/src/components/spacer.ts +28 -0
  44. package/src/components/tab-bar.ts +175 -0
  45. package/src/components/text.ts +110 -0
  46. package/src/components/truncated-text.ts +61 -0
  47. package/src/editor-component.ts +71 -0
  48. package/src/fuzzy.ts +143 -0
  49. package/src/index.ts +39 -0
  50. package/src/keybindings.ts +279 -0
  51. package/src/keys.ts +537 -0
  52. package/src/kill-ring.ts +46 -0
  53. package/src/stdin-buffer.ts +410 -0
  54. package/src/symbols.ts +24 -0
  55. package/src/terminal-capabilities.ts +537 -0
  56. package/src/terminal.ts +716 -0
  57. package/src/ttyid.ts +66 -0
  58. package/src/tui.ts +1481 -0
  59. package/src/utils.ts +359 -0
package/src/keys.ts ADDED
@@ -0,0 +1,537 @@
1
+ /**
2
+ * Keyboard input handling for terminal applications.
3
+ *
4
+ * Supports both legacy terminal sequences and Kitty keyboard protocol.
5
+ * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
6
+ * Reference: https://github.com/sst/opentui/blob/7da92b4088aebfe27b9f691c04163a48821e49fd/packages/core/src/lib/parse.keypress.ts
7
+ *
8
+ * Symbol keys are also supported, however some ctrl+symbol combos
9
+ * overlap with ASCII codes, e.g. ctrl+[ = ESC.
10
+ * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-ctrl-mapping-of-ascii-keys
11
+ * Those can still be * used for ctrl+shift combos
12
+ *
13
+ * API:
14
+ * - matchesKey(data, keyId) - Check if input matches a key identifier
15
+ * - parseKey(data) - Parse input and return the key identifier
16
+ * - Key - Helper object for creating typed key identifiers
17
+ * - setKittyProtocolActive(active) - Set global Kitty protocol state
18
+ * - isKittyProtocolActive() - Query global Kitty protocol state
19
+ */
20
+
21
+ import type { KeyEventType } from "@gajae-code/natives";
22
+ import {
23
+ matchesKey as matchesKeyNative,
24
+ parseKey as parseKeyNative,
25
+ parseKittySequence as parseKittySequenceNative,
26
+ } from "@gajae-code/natives";
27
+
28
+ // =============================================================================
29
+ // Platform Detection
30
+ // =============================================================================
31
+
32
+ function isWindowsTerminalSession(): boolean {
33
+ return (
34
+ Boolean(process.env.WT_SESSION) && !process.env.SSH_CONNECTION && !process.env.SSH_CLIENT && !process.env.SSH_TTY
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Raw 0x08 (BS) is ambiguous in legacy terminals.
40
+ *
41
+ * - Windows Terminal uses it for Ctrl+Backspace.
42
+ * - Some legacy terminals and tmux setups send it for plain Backspace.
43
+ *
44
+ * Prefer explicit Kitty / CSI-u / modifyOtherKeys sequences whenever they are
45
+ * available. Fall back to a Windows Terminal heuristic only for raw BS bytes.
46
+ */
47
+ function matchesRawBackspace(data: string, expectedModifier: number): boolean {
48
+ if (data === "\x7f") return expectedModifier === 0;
49
+ if (data !== "\x08") return false;
50
+ // On Windows Terminal, 0x08 = Ctrl+Backspace. On others, it's plain Backspace.
51
+ return isWindowsTerminalSession() ? expectedModifier === 4 : expectedModifier === 0;
52
+ }
53
+
54
+ export { isWindowsTerminalSession, matchesRawBackspace };
55
+
56
+ // =============================================================================
57
+ // Global Kitty Protocol State
58
+ // =============================================================================
59
+
60
+ let kittyProtocolActive = false;
61
+
62
+ /**
63
+ * Set the global Kitty keyboard protocol state.
64
+ * Called by ProcessTerminal after detecting protocol support.
65
+ */
66
+ export function setKittyProtocolActive(active: boolean): void {
67
+ kittyProtocolActive = active;
68
+ }
69
+
70
+ /**
71
+ * Query whether Kitty keyboard protocol is currently active.
72
+ */
73
+ export function isKittyProtocolActive(): boolean {
74
+ return kittyProtocolActive;
75
+ }
76
+
77
+ // =============================================================================
78
+ // Type-Safe Key Identifiers
79
+ // =============================================================================
80
+
81
+ type Letter =
82
+ | "a"
83
+ | "b"
84
+ | "c"
85
+ | "d"
86
+ | "e"
87
+ | "f"
88
+ | "g"
89
+ | "h"
90
+ | "i"
91
+ | "j"
92
+ | "k"
93
+ | "l"
94
+ | "m"
95
+ | "n"
96
+ | "o"
97
+ | "p"
98
+ | "q"
99
+ | "r"
100
+ | "s"
101
+ | "t"
102
+ | "u"
103
+ | "v"
104
+ | "w"
105
+ | "x"
106
+ | "y"
107
+ | "z";
108
+
109
+ type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
110
+
111
+ type SymbolKey =
112
+ | "`"
113
+ | "-"
114
+ | "="
115
+ | "["
116
+ | "]"
117
+ | "\\"
118
+ | ";"
119
+ | "'"
120
+ | ","
121
+ | "."
122
+ | "/"
123
+ | "!"
124
+ | "@"
125
+ | "#"
126
+ | "$"
127
+ | "%"
128
+ | "^"
129
+ | "&"
130
+ | "*"
131
+ | "("
132
+ | ")"
133
+ | "_"
134
+ | "+"
135
+ | "|"
136
+ | "~"
137
+ | "{"
138
+ | "}"
139
+ | ":"
140
+ | "<"
141
+ | ">"
142
+ | "?";
143
+
144
+ type SpecialKey =
145
+ | "escape"
146
+ | "esc"
147
+ | "enter"
148
+ | "return"
149
+ | "tab"
150
+ | "space"
151
+ | "backspace"
152
+ | "delete"
153
+ | "insert"
154
+ | "clear"
155
+ | "home"
156
+ | "end"
157
+ | "pageUp"
158
+ | "pageDown"
159
+ | "up"
160
+ | "down"
161
+ | "left"
162
+ | "right"
163
+ | "f1"
164
+ | "f2"
165
+ | "f3"
166
+ | "f4"
167
+ | "f5"
168
+ | "f6"
169
+ | "f7"
170
+ | "f8"
171
+ | "f9"
172
+ | "f10"
173
+ | "f11"
174
+ | "f12";
175
+
176
+ type BaseKey = Letter | Digit | SymbolKey | SpecialKey;
177
+ type ModifierName = "ctrl" | "shift" | "alt" | "super";
178
+
179
+ type ModifiedKeyId<Key extends string, RemainingModifiers extends ModifierName = ModifierName> = {
180
+ [M in RemainingModifiers]: `${M}+${Key}` | `${M}+${ModifiedKeyId<Key, Exclude<RemainingModifiers, M>>}`;
181
+ }[RemainingModifiers];
182
+
183
+ /**
184
+ * Union type of all valid key identifiers.
185
+ * Provides autocomplete and catches typos at compile time.
186
+ */
187
+ export type KeyId = BaseKey | ModifiedKeyId<BaseKey>;
188
+
189
+ /**
190
+ * Typed helper for constructing key identifiers with autocomplete.
191
+ *
192
+ * The runtime values are just the canonical key-name strings (so `Key.enter`
193
+ * is literally `"enter"`); the value of `Key` over a bag of magic strings is
194
+ * that each property is typed to the exact `KeyId` literal it produces and the
195
+ * modifier methods return precisely-typed concatenations (e.g. `Key.ctrl("c")`
196
+ * is `"ctrl+c"`, not just `string`). This mirrors the upstream
197
+ * `@mariozechner/pi-tui` `Key` export verbatim so plugins built against any
198
+ * scope alias (`@mariozechner`, `@earendil-works`, `@gajae-code`) keep working
199
+ * once the specifier shim remaps them to this package.
200
+ */
201
+ export const Key = {
202
+ escape: "escape",
203
+ esc: "esc",
204
+ enter: "enter",
205
+ return: "return",
206
+ tab: "tab",
207
+ space: "space",
208
+ backspace: "backspace",
209
+ delete: "delete",
210
+ insert: "insert",
211
+ clear: "clear",
212
+ home: "home",
213
+ end: "end",
214
+ pageUp: "pageUp",
215
+ pageDown: "pageDown",
216
+ up: "up",
217
+ down: "down",
218
+ left: "left",
219
+ right: "right",
220
+ f1: "f1",
221
+ f2: "f2",
222
+ f3: "f3",
223
+ f4: "f4",
224
+ f5: "f5",
225
+ f6: "f6",
226
+ f7: "f7",
227
+ f8: "f8",
228
+ f9: "f9",
229
+ f10: "f10",
230
+ f11: "f11",
231
+ f12: "f12",
232
+ backtick: "`",
233
+ hyphen: "-",
234
+ equals: "=",
235
+ leftbracket: "[",
236
+ rightbracket: "]",
237
+ backslash: "\\",
238
+ semicolon: ";",
239
+ quote: "'",
240
+ comma: ",",
241
+ period: ".",
242
+ slash: "/",
243
+ exclamation: "!",
244
+ at: "@",
245
+ hash: "#",
246
+ dollar: "$",
247
+ percent: "%",
248
+ caret: "^",
249
+ ampersand: "&",
250
+ asterisk: "*",
251
+ leftparen: "(",
252
+ rightparen: ")",
253
+ underscore: "_",
254
+ plus: "+",
255
+ pipe: "|",
256
+ tilde: "~",
257
+ leftbrace: "{",
258
+ rightbrace: "}",
259
+ colon: ":",
260
+ lessthan: "<",
261
+ greaterthan: ">",
262
+ question: "?",
263
+ ctrl: <K extends BaseKey>(key: K) => `ctrl+${key}` as const,
264
+ shift: <K extends BaseKey>(key: K) => `shift+${key}` as const,
265
+ alt: <K extends BaseKey>(key: K) => `alt+${key}` as const,
266
+ super: <K extends BaseKey>(key: K) => `super+${key}` as const,
267
+ ctrlShift: <K extends BaseKey>(key: K) => `ctrl+shift+${key}` as const,
268
+ shiftCtrl: <K extends BaseKey>(key: K) => `shift+ctrl+${key}` as const,
269
+ ctrlAlt: <K extends BaseKey>(key: K) => `ctrl+alt+${key}` as const,
270
+ altCtrl: <K extends BaseKey>(key: K) => `alt+ctrl+${key}` as const,
271
+ shiftAlt: <K extends BaseKey>(key: K) => `shift+alt+${key}` as const,
272
+ altShift: <K extends BaseKey>(key: K) => `alt+shift+${key}` as const,
273
+ ctrlSuper: <K extends BaseKey>(key: K) => `ctrl+super+${key}` as const,
274
+ superCtrl: <K extends BaseKey>(key: K) => `super+ctrl+${key}` as const,
275
+ shiftSuper: <K extends BaseKey>(key: K) => `shift+super+${key}` as const,
276
+ superShift: <K extends BaseKey>(key: K) => `super+shift+${key}` as const,
277
+ altSuper: <K extends BaseKey>(key: K) => `alt+super+${key}` as const,
278
+ superAlt: <K extends BaseKey>(key: K) => `super+alt+${key}` as const,
279
+ ctrlShiftAlt: <K extends BaseKey>(key: K) => `ctrl+shift+alt+${key}` as const,
280
+ ctrlShiftSuper: <K extends BaseKey>(key: K) => `ctrl+shift+super+${key}` as const,
281
+ } as const;
282
+
283
+ // =============================================================================
284
+ // Kitty Protocol Parsing
285
+ // =============================================================================
286
+
287
+ interface ParsedKittySequence {
288
+ codepoint: number;
289
+ shiftedKey?: number; // Shifted version of the key (when shift is pressed)
290
+ baseLayoutKey?: number; // Key in standard PC-101 layout (for non-Latin layouts)
291
+ modifier: number;
292
+ eventType?: KeyEventType;
293
+ }
294
+
295
+ // Regex for Kitty protocol event type detection
296
+ // Matches CSI sequences with :2 (repeat) or :3 (release) event type
297
+ // Format: \x1b[...;modifier:event_type<terminator> where terminator is u, ~, or A-F/H
298
+ const KITTY_RELEASE_PATTERN = /^\x1b\[[\d:;]*:3[u~ABCDHF]$/;
299
+ const KITTY_REPEAT_PATTERN = /^\x1b\[[\d:;]*:2[u~ABCDHF]$/;
300
+ const KITTY_CSI_U_PATTERN = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?(?:;([\d:]*))?u$/;
301
+ const KITTY_MOD_SHIFT = 1;
302
+ const KITTY_MOD_ALT = 2;
303
+ const KITTY_MOD_CTRL = 4;
304
+ const KITTY_MOD_SUPER = 8;
305
+ const KITTY_MOD_NUM_LOCK = 128;
306
+ const KITTY_LOCK_MASK = 64 + 128; // Caps Lock + Num Lock
307
+ const MODIFY_OTHER_KEYS_PATTERN = /^\x1b\[27;(\d+);(\d+)~$/;
308
+ const KITTY_KEYPAD_OPERATOR_TEXT: Record<number, string> = {
309
+ 57410: "/",
310
+ 57411: "*",
311
+ 57412: "-",
312
+ 57413: "+",
313
+ 57415: "=",
314
+ };
315
+ const KITTY_NUMPAD_TEXT: Record<number, string> = {
316
+ 57399: "0",
317
+ 57400: "1",
318
+ 57401: "2",
319
+ 57402: "3",
320
+ 57403: "4",
321
+ 57404: "5",
322
+ 57405: "6",
323
+ 57406: "7",
324
+ 57407: "8",
325
+ 57408: "9",
326
+ 57409: ".",
327
+ };
328
+
329
+ /**
330
+ * Check if the input is a key release event.
331
+ * Only meaningful when Kitty keyboard protocol with flag 2 is active.
332
+ * Returns false if Kitty protocol is not active.
333
+ */
334
+ export function isKeyRelease(data: string): boolean {
335
+ // Only detect release events when Kitty protocol is active
336
+ if (!kittyProtocolActive) {
337
+ return false;
338
+ }
339
+
340
+ // Don't treat bracketed paste content as key release
341
+ if (data.includes("\x1b[200~")) {
342
+ return false;
343
+ }
344
+
345
+ // Match the full CSI sequence pattern for release events
346
+ return KITTY_RELEASE_PATTERN.test(data);
347
+ }
348
+
349
+ /**
350
+ * Check if the input is a key repeat event.
351
+ * Only meaningful when Kitty keyboard protocol with flag 2 is active.
352
+ * Returns false if Kitty protocol is not active.
353
+ */
354
+ export function isKeyRepeat(data: string): boolean {
355
+ // Only detect repeat events when Kitty protocol is active
356
+ if (!kittyProtocolActive) {
357
+ return false;
358
+ }
359
+
360
+ // Don't treat bracketed paste content as key repeat
361
+ if (data.includes("\x1b[200~")) {
362
+ return false;
363
+ }
364
+
365
+ // Match the full CSI sequence pattern for repeat events
366
+ return KITTY_REPEAT_PATTERN.test(data);
367
+ }
368
+
369
+ export function parseKittySequence(data: string): ParsedKittySequence | null {
370
+ const result = parseKittySequenceNative(data);
371
+ if (!result) return null;
372
+ return {
373
+ codepoint: result.codepoint,
374
+ shiftedKey: result.shiftedKey ?? undefined,
375
+ baseLayoutKey: result.baseLayoutKey ?? undefined,
376
+ modifier: result.modifier,
377
+ eventType: result.eventType,
378
+ };
379
+ }
380
+
381
+ function hasControlChars(data: string): boolean {
382
+ return [...data].some(ch => {
383
+ const code = ch.charCodeAt(0);
384
+ return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
385
+ });
386
+ }
387
+
388
+ function decodeKittyPrintable(data: string): string | undefined {
389
+ const match = data.match(KITTY_CSI_U_PATTERN);
390
+ if (!match) return undefined;
391
+
392
+ const codepoint = Number.parseInt(match[1] ?? "", 10);
393
+ if (!Number.isFinite(codepoint)) return undefined;
394
+
395
+ if (match[5] === "3") return undefined;
396
+
397
+ const shiftedKey = match[2] && match[2].length > 0 ? Number.parseInt(match[2], 10) : undefined;
398
+ const modValue = match[4] ? Number.parseInt(match[4], 10) : 1;
399
+ const modifier = Number.isFinite(modValue) ? modValue - 1 : 0;
400
+ const effectiveMod = modifier & ~KITTY_LOCK_MASK;
401
+ const supportedModifierMask = KITTY_MOD_SHIFT | KITTY_MOD_ALT | KITTY_MOD_CTRL | KITTY_MOD_SUPER;
402
+
403
+ if (effectiveMod & ~supportedModifierMask) return undefined;
404
+ if (effectiveMod & (KITTY_MOD_ALT | KITTY_MOD_CTRL | KITTY_MOD_SUPER)) return undefined;
405
+
406
+ const textField = match[6];
407
+ if (textField && textField.length > 0) {
408
+ const codepoints = textField
409
+ .split(":")
410
+ .filter(Boolean)
411
+ .map(value => Number.parseInt(value, 10))
412
+ .filter(value => Number.isFinite(value) && value >= 32);
413
+ if (codepoints.length > 0) {
414
+ try {
415
+ return String.fromCodePoint(...codepoints);
416
+ } catch {
417
+ return undefined;
418
+ }
419
+ }
420
+ }
421
+ const keypadOperatorText = KITTY_KEYPAD_OPERATOR_TEXT[codepoint];
422
+ if (keypadOperatorText) return keypadOperatorText;
423
+
424
+ if (effectiveMod === 0 && modifier & KITTY_MOD_NUM_LOCK) {
425
+ const numpadText = KITTY_NUMPAD_TEXT[codepoint];
426
+ if (numpadText) return numpadText;
427
+ }
428
+
429
+ let effectiveCodepoint = codepoint;
430
+ if (effectiveMod & KITTY_MOD_SHIFT && typeof shiftedKey === "number") {
431
+ effectiveCodepoint = shiftedKey;
432
+ }
433
+
434
+ if (effectiveCodepoint >= 0xe000 && effectiveCodepoint <= 0xf8ff) {
435
+ return undefined;
436
+ }
437
+
438
+ if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32) return undefined;
439
+
440
+ try {
441
+ return String.fromCodePoint(effectiveCodepoint);
442
+ } catch {
443
+ return undefined;
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Extract printable text from raw terminal input.
449
+ *
450
+ * Handles Kitty CSI-u text-producing keys so text-entry components can treat
451
+ * keypad digits, keypad operators, and shifted symbols the same as direct character input.
452
+ */
453
+ export function extractPrintableText(data: string): string | undefined {
454
+ const printable = decodePrintableKey(data);
455
+ if (printable !== undefined) return printable;
456
+ if (data.length === 0 || hasControlChars(data)) return undefined;
457
+ return data;
458
+ }
459
+
460
+ interface ParsedModifyOtherKeysSequence {
461
+ codepoint: number;
462
+ modifier: number;
463
+ }
464
+
465
+ /**
466
+ * Parse an xterm `modifyOtherKeys` format sequence: `CSI 27 ; modifiers ; keycode ~`.
467
+ * Modifier values are 1-indexed in the wire format; we normalize to a 0-based bitmask.
468
+ */
469
+ function parseModifyOtherKeysSequence(data: string): ParsedModifyOtherKeysSequence | null {
470
+ const match = data.match(MODIFY_OTHER_KEYS_PATTERN);
471
+ if (!match) return null;
472
+ const modValue = Number.parseInt(match[1] ?? "", 10);
473
+ const codepoint = Number.parseInt(match[2] ?? "", 10);
474
+ if (!Number.isFinite(modValue) || !Number.isFinite(codepoint)) return null;
475
+ return { codepoint, modifier: modValue - 1 };
476
+ }
477
+
478
+ /**
479
+ * Decode an xterm modifyOtherKeys sequence into the printable character it represents.
480
+ *
481
+ * Only sequences with no modifiers or Shift alone produce text; Ctrl/Alt/Super combos
482
+ * are treated as bindings, not text input.
483
+ */
484
+ function decodeModifyOtherKeysPrintable(data: string): string | undefined {
485
+ const parsed = parseModifyOtherKeysSequence(data);
486
+ if (!parsed) return undefined;
487
+ const modifier = parsed.modifier & ~KITTY_LOCK_MASK;
488
+ if ((modifier & ~KITTY_MOD_SHIFT) !== 0) return undefined;
489
+ if (!Number.isFinite(parsed.codepoint) || parsed.codepoint < 32) return undefined;
490
+ try {
491
+ return String.fromCodePoint(parsed.codepoint);
492
+ } catch {
493
+ return undefined;
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Decode terminal input into the printable character it represents.
499
+ *
500
+ * Tries Kitty CSI-u first, then falls back to xterm modifyOtherKeys. Returns
501
+ * undefined for control sequences and modifier-only events.
502
+ */
503
+ export function decodePrintableKey(data: string): string | undefined {
504
+ return decodeKittyPrintable(data) ?? decodeModifyOtherKeysPrintable(data);
505
+ }
506
+
507
+ /**
508
+ * Match input data against a key identifier string.
509
+ *
510
+ * Supported key identifiers:
511
+ * - Single keys: "escape", "tab", "enter", "backspace", "delete", "home", "end", "space"
512
+ * - Arrow keys: "up", "down", "left", "right"
513
+ * - Ctrl combinations: "ctrl+c", "ctrl+z", etc.
514
+ * - Shift combinations: "shift+tab", "shift+enter"
515
+ * - Alt combinations: "alt+enter", "alt+backspace"
516
+ * - Combined modifiers: "shift+ctrl+p", "ctrl+alt+x"
517
+ *
518
+ * Use the Key helper for autocomplete: Key.ctrl("c"), Key.escape, Key.ctrlShift("p")
519
+ *
520
+ * @param data - Raw input data from terminal
521
+ * @param keyId - Key identifier (e.g., "ctrl+c", "escape", Key.ctrl("c"))
522
+ */
523
+ export function matchesKey(data: string, keyId: KeyId): boolean {
524
+ return matchesKeyNative(data, keyId, kittyProtocolActive);
525
+ }
526
+
527
+ /**
528
+ * Parse terminal input and return a normalized key identifier.
529
+ *
530
+ * Returns key names like "escape", "ctrl+c", "shift+tab", "alt+enter".
531
+ * Returns undefined if the input is not a recognized key sequence.
532
+ *
533
+ * @param data - Raw input data from terminal
534
+ */
535
+ export function parseKey(data: string): string | undefined {
536
+ return parseKeyNative(data, kittyProtocolActive) ?? undefined;
537
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Ring buffer for Emacs-style kill/yank operations.
3
+ *
4
+ * Tracks killed (deleted) text entries. Consecutive kills can accumulate
5
+ * into a single entry. Supports yank (paste most recent) and yank-pop
6
+ * (cycle through older entries).
7
+ */
8
+ export class KillRing {
9
+ #ring: string[] = [];
10
+
11
+ /**
12
+ * Add text to the kill ring.
13
+ *
14
+ * @param text - The killed text to add
15
+ * @param opts - Push options
16
+ * @param opts.prepend - If accumulating, prepend (backward deletion) or append (forward deletion)
17
+ * @param opts.accumulate - Merge with the most recent entry instead of creating a new one
18
+ */
19
+ push(text: string, opts: { prepend: boolean; accumulate?: boolean }): void {
20
+ if (!text) return;
21
+
22
+ if (opts.accumulate && this.#ring.length > 0) {
23
+ const last = this.#ring.pop()!;
24
+ this.#ring.push(opts.prepend ? text + last : last + text);
25
+ } else {
26
+ this.#ring.push(text);
27
+ }
28
+ }
29
+
30
+ /** Get most recent entry without modifying the ring. */
31
+ peek(): string | undefined {
32
+ return this.#ring.length > 0 ? this.#ring[this.#ring.length - 1] : undefined;
33
+ }
34
+
35
+ /** Move last entry to front (for yank-pop cycling). */
36
+ rotate(): void {
37
+ if (this.#ring.length > 1) {
38
+ const last = this.#ring.pop()!;
39
+ this.#ring.unshift(last);
40
+ }
41
+ }
42
+
43
+ get length(): number {
44
+ return this.#ring.length;
45
+ }
46
+ }