@tuidom/core 0.1.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.
Files changed (91) hide show
  1. package/dist/backend/iTerminalBackend.d.ts +41 -0
  2. package/dist/backend/iTerminalBackend.js +1 -0
  3. package/dist/common/colorUtils.d.ts +19 -0
  4. package/dist/common/colorUtils.js +30 -0
  5. package/dist/common/displayLine.d.ts +72 -0
  6. package/dist/common/displayLine.js +190 -0
  7. package/dist/common/disposable.d.ts +9 -0
  8. package/dist/common/disposable.js +18 -0
  9. package/dist/common/geometryPromitives.d.ts +47 -0
  10. package/dist/common/geometryPromitives.js +115 -0
  11. package/dist/common/iTerminalSurface.d.ts +68 -0
  12. package/dist/common/iTerminalSurface.js +8 -0
  13. package/dist/common/measureTextWidth.d.ts +21 -0
  14. package/dist/common/measureTextWidth.js +43 -0
  15. package/dist/common/styleFlags.d.ts +17 -0
  16. package/dist/common/styleFlags.js +16 -0
  17. package/dist/common/textTruncation.d.ts +30 -0
  18. package/dist/common/textTruncation.js +124 -0
  19. package/dist/common/typingUtils.d.ts +1 -0
  20. package/dist/common/typingUtils.js +3 -0
  21. package/dist/common/unicodeWidth.d.ts +18 -0
  22. package/dist/common/unicodeWidth.js +339 -0
  23. package/dist/dom/borderStyle.d.ts +31 -0
  24. package/dist/dom/borderStyle.js +40 -0
  25. package/dist/dom/compositeElement.d.ts +20 -0
  26. package/dist/dom/compositeElement.js +44 -0
  27. package/dist/dom/events/contextMenuEventSource.d.ts +17 -0
  28. package/dist/dom/events/contextMenuEventSource.js +44 -0
  29. package/dist/dom/events/focusManager.d.ts +13 -0
  30. package/dist/dom/events/focusManager.js +56 -0
  31. package/dist/dom/events/mouseEventDispatcher.d.ts +24 -0
  32. package/dist/dom/events/mouseEventDispatcher.js +177 -0
  33. package/dist/dom/events/tuiEventBase.d.ts +25 -0
  34. package/dist/dom/events/tuiEventBase.js +39 -0
  35. package/dist/dom/events/tuiFocusEvent.d.ts +6 -0
  36. package/dist/dom/events/tuiFocusEvent.js +8 -0
  37. package/dist/dom/events/tuiKeyboardEvent.d.ts +21 -0
  38. package/dist/dom/events/tuiKeyboardEvent.js +20 -0
  39. package/dist/dom/events/tuiMouseEvent.d.ts +40 -0
  40. package/dist/dom/events/tuiMouseEvent.js +32 -0
  41. package/dist/dom/events/tuiPasteEvent.d.ts +10 -0
  42. package/dist/dom/events/tuiPasteEvent.js +13 -0
  43. package/dist/dom/overlayLayer.d.ts +92 -0
  44. package/dist/dom/overlayLayer.js +343 -0
  45. package/dist/dom/styles/index.d.ts +4 -0
  46. package/dist/dom/styles/index.js +2 -0
  47. package/dist/dom/styles/styleTokens.d.ts +114 -0
  48. package/dist/dom/styles/styleTokens.js +122 -0
  49. package/dist/dom/styles/tuiStyle.d.ts +75 -0
  50. package/dist/dom/styles/tuiStyle.js +121 -0
  51. package/dist/dom/tuiApplication.d.ts +57 -0
  52. package/dist/dom/tuiApplication.js +231 -0
  53. package/dist/dom/tuiElement.d.ts +528 -0
  54. package/dist/dom/tuiElement.js +1168 -0
  55. package/dist/dom/tuiSelector.d.ts +9 -0
  56. package/dist/dom/tuiSelector.js +82 -0
  57. package/dist/dom/validateTree.d.ts +42 -0
  58. package/dist/dom/validateTree.js +125 -0
  59. package/dist/input/convertToken.d.ts +3 -0
  60. package/dist/input/convertToken.js +114 -0
  61. package/dist/input/keyEvent.d.ts +46 -0
  62. package/dist/input/keyEvent.js +26 -0
  63. package/dist/input/keyInputParser.d.ts +72 -0
  64. package/dist/input/keyInputParser.js +249 -0
  65. package/dist/input/mouseTracking.d.ts +18 -0
  66. package/dist/input/mouseTracking.js +18 -0
  67. package/dist/input/parseInput.d.ts +13 -0
  68. package/dist/input/parseInput.js +17 -0
  69. package/dist/input/rawTerminalToken.d.ts +142 -0
  70. package/dist/input/rawTerminalToken.js +2 -0
  71. package/dist/input/serializeKey.d.ts +14 -0
  72. package/dist/input/serializeKey.js +179 -0
  73. package/dist/input/serializeMouse.d.ts +26 -0
  74. package/dist/input/serializeMouse.js +38 -0
  75. package/dist/input/tokenize.d.ts +59 -0
  76. package/dist/input/tokenize.js +681 -0
  77. package/dist/rendering/cell.d.ts +24 -0
  78. package/dist/rendering/cell.js +46 -0
  79. package/dist/rendering/damage.d.ts +35 -0
  80. package/dist/rendering/damage.js +99 -0
  81. package/dist/rendering/grid.d.ts +49 -0
  82. package/dist/rendering/grid.js +216 -0
  83. package/dist/rendering/gridSnapshot.d.ts +33 -0
  84. package/dist/rendering/gridSnapshot.js +32 -0
  85. package/dist/rendering/gridToSvg.d.ts +29 -0
  86. package/dist/rendering/gridToSvg.js +145 -0
  87. package/dist/rendering/terminalRenderer.d.ts +28 -0
  88. package/dist/rendering/terminalRenderer.js +161 -0
  89. package/dist/rendering/terminalScreen.d.ts +25 -0
  90. package/dist/rendering/terminalScreen.js +50 -0
  91. package/package.json +29 -0
@@ -0,0 +1,681 @@
1
+ /**
2
+ * Tokenize raw terminal input into protocol-specific RawTerminalToken[].
3
+ *
4
+ * Pure function, no side effects.
5
+ * Returns a discriminated union of token types preserving the wire format semantics:
6
+ * - Kitty CSI u, PUA characters
7
+ * - Standard CSI letter/tilde, SS3
8
+ * - Legacy ESC-prefixed, control chars, printable chars
9
+ */
10
+ /**
11
+ * Control-коды 0x1c–0x1f и клавиши, которые их порождают. Терминал шлёт их для
12
+ * Ctrl+\, Ctrl+], Ctrl+6 (традиционное Ctrl+^) и Ctrl+/ (традиционное Ctrl+_) —
13
+ * это ровно те комбинации вне Ctrl+A..Z, что доступны даже на legacy-терминале,
14
+ * где Ctrl+Shift+* и Ctrl+Tab неотличимы от своих простых версий.
15
+ */
16
+ const ctrlSymbolByByte = {
17
+ 0x1c: "\\",
18
+ 0x1d: "]",
19
+ 0x1e: "6",
20
+ 0x1f: "/",
21
+ };
22
+ export function tokenize(data) {
23
+ const tokens = [];
24
+ let i = 0;
25
+ while (i < data.length) {
26
+ const code = data.charCodeAt(i);
27
+ const ctrlSymbol = ctrlSymbolByByte[code];
28
+ if (code === 0x1b) {
29
+ // Escape: could be standalone, CSI, SS3, or Alt+key
30
+ if (i + 1 >= data.length) {
31
+ tokens.push({ kind: "standalone-esc", raw: data[i] });
32
+ i++;
33
+ continue;
34
+ }
35
+ const next = data.charCodeAt(i + 1);
36
+ if (next === 0x5b) {
37
+ // CSI sequence: \x1b[ ...
38
+ const csiResult = parseCSI(data, i);
39
+ if (csiResult) {
40
+ tokens.push(csiResult.token);
41
+ i = csiResult.nextIndex;
42
+ continue;
43
+ }
44
+ // Failed to parse CSI — emit standalone Escape, let '[' be handled next iteration
45
+ tokens.push({ kind: "standalone-esc", raw: data[i] });
46
+ i++;
47
+ }
48
+ else if (next === 0x5d) {
49
+ // OSC sequence: \x1b] ...
50
+ const oscResult = parseOSC(data, i);
51
+ if (oscResult) {
52
+ tokens.push(oscResult.token);
53
+ i = oscResult.nextIndex;
54
+ continue;
55
+ }
56
+ // Incomplete OSC — emit standalone Escape
57
+ tokens.push({ kind: "standalone-esc", raw: data[i] });
58
+ i++;
59
+ }
60
+ else if (next === 0x4f) {
61
+ // SS3 sequence: \x1bO<letter>
62
+ if (i + 2 < data.length) {
63
+ const letter = data[i + 2];
64
+ const keyName = ss3KeyMap[letter];
65
+ if (keyName) {
66
+ tokens.push({
67
+ kind: "ss3",
68
+ finalByte: letter,
69
+ key: keyName,
70
+ raw: data.slice(i, i + 3),
71
+ });
72
+ i += 3;
73
+ continue;
74
+ }
75
+ }
76
+ // Unknown SS3 — emit standalone Escape
77
+ tokens.push({ kind: "standalone-esc", raw: data[i] });
78
+ i++;
79
+ }
80
+ else if (next === 0x0d) {
81
+ // ESC + Enter
82
+ tokens.push({ kind: "esc-special", key: "Enter", raw: data.slice(i, i + 2) });
83
+ i += 2;
84
+ }
85
+ else if (next === 0x7f) {
86
+ // ESC + Backspace
87
+ tokens.push({ kind: "esc-special", key: "Backspace", raw: data.slice(i, i + 2) });
88
+ i += 2;
89
+ }
90
+ else if (next >= 0x01 && next <= 0x1a) {
91
+ // ESC + control char (Alt+Ctrl+letter)
92
+ const letter = String.fromCharCode(next + 0x60);
93
+ tokens.push({ kind: "esc-control", letter, raw: data.slice(i, i + 2) });
94
+ i += 2;
95
+ }
96
+ else if (next >= 0x20) {
97
+ // ESC + printable character, or Kitty PUA functional key
98
+ const kittyKey = kittyCodepointMap[next];
99
+ if (kittyKey) {
100
+ tokens.push({
101
+ kind: "pua",
102
+ codepoint: next,
103
+ key: kittyKey.key,
104
+ code: kittyKey.code ?? kittyKey.key,
105
+ raw: data.slice(i, i + 2),
106
+ });
107
+ }
108
+ else {
109
+ tokens.push({
110
+ kind: "esc-char",
111
+ char: data[i + 1],
112
+ charCode: next,
113
+ raw: data.slice(i, i + 2),
114
+ });
115
+ }
116
+ i += 2;
117
+ }
118
+ else {
119
+ // Escape followed by unknown byte — emit standalone Escape
120
+ tokens.push({ kind: "standalone-esc", raw: data[i] });
121
+ i++;
122
+ }
123
+ }
124
+ else if (code === 0x00) {
125
+ // Ctrl+Space (NUL)
126
+ tokens.push({ kind: "ctrl-char", letter: " ", raw: data[i] });
127
+ i++;
128
+ }
129
+ else if (code === 0x0d) {
130
+ tokens.push({ kind: "special-key", key: "Enter", raw: data[i] });
131
+ i++;
132
+ }
133
+ else if (code === 0x09) {
134
+ tokens.push({ kind: "special-key", key: "Tab", raw: data[i] });
135
+ i++;
136
+ }
137
+ else if (code === 0x7f) {
138
+ tokens.push({ kind: "special-key", key: "Backspace", raw: data[i] });
139
+ i++;
140
+ }
141
+ else if (code >= 0x01 && code <= 0x1a) {
142
+ // Ctrl+A through Ctrl+Z
143
+ const letter = String.fromCharCode(code + 0x60);
144
+ tokens.push({ kind: "ctrl-char", letter, raw: data[i] });
145
+ i++;
146
+ }
147
+ else if (ctrlSymbol !== undefined) {
148
+ // Ctrl+\ ] 6 / — единственные control-коды за пределами Ctrl+A..Z,
149
+ // которые терминал шлёт с обычной клавиатуры (0x1c–0x1f). Без них
150
+ // Ctrl+6 и Ctrl+/ уходили в unknown-byte и не доходили ни до одного бинда.
151
+ tokens.push({ kind: "ctrl-char", letter: ctrlSymbol, raw: data[i] });
152
+ i++;
153
+ }
154
+ else {
155
+ // Printable character or Kitty PUA functional key. Ветки выше разбирают
156
+ // весь диапазон < 0x20 (0x00, 0x01–0x1a, ESC, 0x1c–0x1f), поэтому сюда
157
+ // попадает только `code >= 0x20`.
158
+ const kittyKey = kittyCodepointMap[code];
159
+ if (kittyKey) {
160
+ tokens.push({
161
+ kind: "pua",
162
+ codepoint: code,
163
+ key: kittyKey.key,
164
+ code: kittyKey.code ?? kittyKey.key,
165
+ raw: data[i],
166
+ });
167
+ }
168
+ else {
169
+ tokens.push({ kind: "char", char: data[i], codepoint: code, raw: data[i] });
170
+ }
171
+ i++;
172
+ }
173
+ }
174
+ return tokens;
175
+ }
176
+ // ─── Kitty functional key codepoints ───
177
+ /**
178
+ * Map Kitty Keyboard Protocol codepoints (Private Use Area U+E000+) to DOM-style key/code.
179
+ * Also includes standard codepoints used in CSI u for common keys (Enter, Tab, etc.).
180
+ *
181
+ * Reference: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions
182
+ */
183
+ export const kittyCodepointMap = {
184
+ // Standard keys with CSI u encoding
185
+ // 8 (BS) — как Ctrl+Backspace шлют xterm-совместимые в modifyOtherKeys: без него
186
+ // ключ приезжал сырым «\b» и не совпадал ни с одним биндом. 127 (DEL) — форма kitty.
187
+ 8: { key: "Backspace", code: "Backspace" },
188
+ 9: { key: "Tab" },
189
+ 13: { key: "Enter" },
190
+ 27: { key: "Escape" },
191
+ 127: { key: "Backspace" },
192
+ // F1–F4 (Kitty PUA; legacy terminals use SS3 \x1bOP–\x1bOS)
193
+ 57344: { key: "F1", code: "F1" },
194
+ 57345: { key: "F2", code: "F2" },
195
+ 57346: { key: "F3", code: "F3" },
196
+ 57347: { key: "F4", code: "F4" },
197
+ // Navigation keys (Kitty sends these as CSI u with PUA codepoints when flags include 8)
198
+ 57348: { key: "Insert" },
199
+ 57349: { key: "Delete" },
200
+ 57350: { key: "ArrowLeft" },
201
+ 57351: { key: "ArrowRight" },
202
+ 57352: { key: "ArrowUp" },
203
+ 57353: { key: "ArrowDown" },
204
+ 57354: { key: "PageUp" },
205
+ 57355: { key: "PageDown" },
206
+ 57356: { key: "Home" },
207
+ 57357: { key: "End" },
208
+ // Lock keys
209
+ 57358: { key: "CapsLock" },
210
+ 57359: { key: "ScrollLock" },
211
+ 57360: { key: "NumLock" },
212
+ // Misc functional
213
+ 57361: { key: "PrintScreen" },
214
+ 57362: { key: "Pause" },
215
+ 57363: { key: "ContextMenu" },
216
+ // F13–F24
217
+ 57376: { key: "F13" },
218
+ 57377: { key: "F14" },
219
+ 57378: { key: "F15" },
220
+ 57379: { key: "F16" },
221
+ 57380: { key: "F17" },
222
+ 57381: { key: "F18" },
223
+ 57382: { key: "F19" },
224
+ 57383: { key: "F20" },
225
+ 57384: { key: "F21" },
226
+ 57385: { key: "F22" },
227
+ 57386: { key: "F23" },
228
+ 57387: { key: "F24" },
229
+ // F25–F35
230
+ 57388: { key: "F25" },
231
+ 57389: { key: "F26" },
232
+ 57390: { key: "F27" },
233
+ 57391: { key: "F28" },
234
+ 57392: { key: "F29" },
235
+ 57393: { key: "F30" },
236
+ 57394: { key: "F31" },
237
+ 57395: { key: "F32" },
238
+ 57396: { key: "F33" },
239
+ 57397: { key: "F34" },
240
+ 57398: { key: "F35" },
241
+ // Keypad
242
+ 57399: { key: "0", code: "Numpad0" },
243
+ 57400: { key: "1", code: "Numpad1" },
244
+ 57401: { key: "2", code: "Numpad2" },
245
+ 57402: { key: "3", code: "Numpad3" },
246
+ 57403: { key: "4", code: "Numpad4" },
247
+ 57404: { key: "5", code: "Numpad5" },
248
+ 57405: { key: "6", code: "Numpad6" },
249
+ 57406: { key: "7", code: "Numpad7" },
250
+ 57407: { key: "8", code: "Numpad8" },
251
+ 57408: { key: "9", code: "Numpad9" },
252
+ 57409: { key: ".", code: "NumpadDecimal" },
253
+ 57410: { key: "/", code: "NumpadDivide" },
254
+ 57411: { key: "*", code: "NumpadMultiply" },
255
+ 57412: { key: "-", code: "NumpadSubtract" },
256
+ 57413: { key: "+", code: "NumpadAdd" },
257
+ 57414: { key: "Enter", code: "NumpadEnter" },
258
+ 57415: { key: "=", code: "NumpadEqual" },
259
+ 57416: { key: ",", code: "NumpadComma" },
260
+ 57417: { key: "ArrowLeft", code: "Numpad4" },
261
+ 57418: { key: "ArrowRight", code: "Numpad6" },
262
+ 57419: { key: "ArrowUp", code: "Numpad8" },
263
+ 57420: { key: "ArrowDown", code: "Numpad2" },
264
+ 57421: { key: "PageUp", code: "Numpad9" },
265
+ 57422: { key: "PageDown", code: "Numpad3" },
266
+ 57423: { key: "Home", code: "Numpad7" },
267
+ 57424: { key: "End", code: "Numpad1" },
268
+ 57425: { key: "Insert", code: "Numpad0" },
269
+ 57426: { key: "Delete", code: "NumpadDecimal" },
270
+ 57427: { key: "Clear", code: "NumpadClear" },
271
+ // Media keys
272
+ 57428: { key: "MediaPlay" },
273
+ 57429: { key: "MediaPause" },
274
+ 57430: { key: "MediaPlayPause" },
275
+ 57431: { key: "MediaReverse" },
276
+ 57432: { key: "MediaStop" },
277
+ 57433: { key: "MediaFastForward" },
278
+ 57434: { key: "MediaRewind" },
279
+ 57435: { key: "MediaTrackNext" },
280
+ 57436: { key: "MediaTrackPrevious" },
281
+ 57437: { key: "MediaRecord" },
282
+ 57438: { key: "AudioVolumeDown" },
283
+ 57439: { key: "AudioVolumeUp" },
284
+ 57440: { key: "AudioVolumeMute" },
285
+ // Modifier keys
286
+ 57441: { key: "Shift", code: "ShiftLeft" },
287
+ 57442: { key: "Control", code: "ControlLeft" },
288
+ 57443: { key: "Alt", code: "AltLeft" },
289
+ 57444: { key: "Meta", code: "MetaLeft" },
290
+ 57445: { key: "Hyper", code: "HyperLeft" },
291
+ 57446: { key: "Super", code: "SuperLeft" },
292
+ 57447: { key: "Shift", code: "ShiftRight" },
293
+ 57448: { key: "Control", code: "ControlRight" },
294
+ 57449: { key: "Alt", code: "AltRight" },
295
+ 57450: { key: "Meta", code: "MetaRight" },
296
+ 57451: { key: "Hyper", code: "HyperRight" },
297
+ 57452: { key: "Super", code: "SuperRight" },
298
+ 57453: { key: "AltGraph" },
299
+ 57454: { key: "ISO_Level5_Shift" },
300
+ };
301
+ // ─── CSI key maps ───
302
+ /** CSI final-byte → key name (cursor keys, F1–F4) */
303
+ export const csiKeyMap = {
304
+ A: "ArrowUp",
305
+ B: "ArrowDown",
306
+ C: "ArrowRight",
307
+ D: "ArrowLeft",
308
+ H: "Home",
309
+ I: "Tab",
310
+ F: "End",
311
+ P: "F1",
312
+ Q: "F2",
313
+ R: "F3",
314
+ S: "F4",
315
+ Z: "Tab", // CSI Z = Shift+Tab (backtab)
316
+ };
317
+ /** CSI number~ → key name (navigation, F5–F12) */
318
+ export const tildeKeyMap = {
319
+ 1: "Home",
320
+ 2: "Insert",
321
+ 3: "Delete",
322
+ 4: "End",
323
+ 5: "PageUp",
324
+ 6: "PageDown",
325
+ 7: "Home", // rxvt
326
+ 8: "End", // rxvt
327
+ 11: "F1",
328
+ 12: "F2",
329
+ 13: "F3",
330
+ 14: "F4",
331
+ 15: "F5",
332
+ 17: "F6",
333
+ 18: "F7",
334
+ 19: "F8",
335
+ 20: "F9",
336
+ 21: "F10",
337
+ 23: "F11",
338
+ 24: "F12",
339
+ };
340
+ /** SS3 (ESC O) final-byte → key name */
341
+ export const ss3KeyMap = {
342
+ A: "ArrowUp",
343
+ B: "ArrowDown",
344
+ C: "ArrowRight",
345
+ D: "ArrowLeft",
346
+ H: "Home",
347
+ F: "End",
348
+ P: "F1",
349
+ Q: "F2",
350
+ R: "F3",
351
+ S: "F4",
352
+ };
353
+ // ─── Modifier decoding ───
354
+ /**
355
+ * Decode xterm/kitty modifier parameter into individual boolean flags.
356
+ *
357
+ * Modifier encoding: value = 1 + bitmask
358
+ * Bitmask bits: 0=Shift, 1=Alt, 2=Ctrl, 3=Super(Meta), 4=Hyper, 5=Meta, 6=CapsLock, 7=NumLock
359
+ */
360
+ export function decodeModifiers(mod) {
361
+ const bits = mod - 1;
362
+ return {
363
+ shiftKey: (bits & 1) !== 0,
364
+ altKey: (bits & 2) !== 0,
365
+ ctrlKey: (bits & 4) !== 0,
366
+ metaKey: (bits & 8) !== 0,
367
+ };
368
+ }
369
+ /**
370
+ * Parse a Kitty modifier parameter string that may contain `:eventtype` suffix.
371
+ *
372
+ * Format: "modifier" or "modifier:eventtype"
373
+ * Event types: 1=press, 2=repeat, 3=release
374
+ *
375
+ * Returns modifier value (default 1 = no modifiers) and event type (0 = not specified).
376
+ */
377
+ function parseModifierParam(paramStr) {
378
+ const colonIdx = paramStr.indexOf(":");
379
+ if (colonIdx >= 0) {
380
+ return {
381
+ mod: parseInt(paramStr.substring(0, colonIdx), 10) || 1,
382
+ eventType: parseInt(paramStr.substring(colonIdx + 1), 10) || 1,
383
+ };
384
+ }
385
+ return { mod: paramStr === "" ? 1 : parseInt(paramStr, 10) || 1, eventType: 0 };
386
+ }
387
+ /**
388
+ * Parse a Kitty codepoint parameter string that may contain `:shifted:base` sub-fields.
389
+ * Returns { codepoint, shiftedKey, baseLayoutKey }.
390
+ */
391
+ function parseCodepointParam(paramStr) {
392
+ const parts = paramStr.split(":");
393
+ const codepoint = parts.length === 0 || parts[0] === "" ? 0 : parseInt(parts[0], 10) || 0;
394
+ const shiftedKey = parts.length > 1 && parts[1] !== "" ? parseInt(parts[1], 10) || undefined : undefined;
395
+ const baseLayoutKey = parts.length > 2 && parts[2] !== "" ? parseInt(parts[2], 10) || undefined : undefined;
396
+ return { codepoint, shiftedKey, baseLayoutKey };
397
+ }
398
+ // ─── Code inference ───
399
+ /**
400
+ * Infer a DOM-style `code` from a key value.
401
+ * Best-effort for traditional terminal mode (no physical key info available).
402
+ */
403
+ export function inferCode(key) {
404
+ if (key.length === 1) {
405
+ const upper = key.toUpperCase();
406
+ if (upper >= "A" && upper <= "Z")
407
+ return `Key${upper}`;
408
+ if (key >= "0" && key <= "9")
409
+ return `Digit${key}`;
410
+ if (key === " ")
411
+ return "Space";
412
+ const punctuation = {
413
+ "-": "Minus",
414
+ "=": "Equal",
415
+ "[": "BracketLeft",
416
+ "]": "BracketRight",
417
+ "\\": "Backslash",
418
+ ";": "Semicolon",
419
+ "'": "Quote",
420
+ ",": "Comma",
421
+ ".": "Period",
422
+ "/": "Slash",
423
+ "`": "Backquote",
424
+ };
425
+ return punctuation[key] ?? key;
426
+ }
427
+ // Named keys: code matches key ("Enter", "ArrowUp", "F1", etc.)
428
+ return key;
429
+ }
430
+ /**
431
+ * Parse a CSI sequence starting at data[start] = ESC, data[start+1] = '['.
432
+ * Returns the parsed token and next index, or null if the sequence is not recognized.
433
+ *
434
+ * A `null` return also signals an *incomplete* sequence (e.g. the chunk was cut
435
+ * mid-sequence by stdin packetization) — KeyInputParser relies on this to buffer
436
+ * the tail until the rest arrives. See KeyInputParser.incompleteTailStart.
437
+ */
438
+ export function parseCSI(data, start) {
439
+ let i = start + 2; // skip ESC [
440
+ // Collect parameter bytes (0x30–0x3f: digits, semicolons, colons, <, =, >, ?)
441
+ let params = "";
442
+ while (i < data.length && data.charCodeAt(i) >= 0x30 && data.charCodeAt(i) <= 0x3f) {
443
+ params += data[i];
444
+ i++;
445
+ }
446
+ // Collect intermediate bytes (0x20–0x2f)
447
+ while (i < data.length && data.charCodeAt(i) >= 0x20 && data.charCodeAt(i) <= 0x2f) {
448
+ i++;
449
+ }
450
+ // Final byte (0x40–0x7e)
451
+ if (i >= data.length)
452
+ return null;
453
+ const finalCode = data.charCodeAt(i);
454
+ if (finalCode < 0x40 || finalCode > 0x7e)
455
+ return null;
456
+ const finalByte = data[i];
457
+ const raw = data.slice(start, i + 1);
458
+ const nextIndex = i + 1;
459
+ // Split semicolon-separated parameters, keeping raw strings for sub-parameter parsing
460
+ const paramStrings = params ? params.split(";") : [];
461
+ // ── Device reports (responses to our capability probes) ──
462
+ // CSI ? <flags> u → Kitty keyboard-protocol flags; CSI ? <attrs> c → Primary Device Attributes.
463
+ // The private marker '?' never appears in a real key event, so it's a safe discriminator —
464
+ // without this, `CSI ?15 u` would be misparsed as a phantom keypress and `CSI ?…c` would
465
+ // corrupt the stream. Surface them for the terminal-environment detector instead.
466
+ if (params.startsWith("?") && (finalByte === "u" || finalByte === "c")) {
467
+ const token = {
468
+ kind: "device-report",
469
+ report: finalByte === "u" ? "kitty-flags" : "da1",
470
+ params,
471
+ raw,
472
+ };
473
+ return { token, nextIndex };
474
+ }
475
+ // ── Kitty Keyboard Protocol: CSI <codepoint[:shifted[:base]]> ; <mod[:eventtype]> [; <text>] u ──
476
+ if (finalByte === "u") {
477
+ const { codepoint, shiftedKey, baseLayoutKey } = parseCodepointParam(paramStrings[0] ?? "0");
478
+ const { mod, eventType } = parseModifierParam(paramStrings[1] ?? "");
479
+ const mods = decodeModifiers(mod);
480
+ const kittyKey = kittyCodepointMap[codepoint];
481
+ const key = kittyKey ? kittyKey.key : String.fromCodePoint(codepoint);
482
+ // If baseLayoutKey is present (Kitty flag 4 / alternate keys), use it to compute the physical
483
+ // key code so that shortcuts match regardless of the active keyboard layout (e.g. Russian).
484
+ const baseKeyStr = baseLayoutKey != null && baseLayoutKey > 0 ? String.fromCodePoint(baseLayoutKey) : undefined;
485
+ const code = kittyKey ? (kittyKey.code ?? kittyKey.key) : inferCode(baseKeyStr ?? key);
486
+ const token = {
487
+ kind: "csi-u",
488
+ codepoint,
489
+ shiftedKey,
490
+ baseLayoutKey,
491
+ key,
492
+ code,
493
+ ...mods,
494
+ eventType,
495
+ raw,
496
+ };
497
+ return { token, nextIndex };
498
+ }
499
+ // ── Tilde sequences: CSI <number> ; <modifier[:eventtype]>? ~ ──
500
+ if (finalByte === "~") {
501
+ const { codepoint: num } = parseCodepointParam(paramStrings[0] ?? "0");
502
+ const { mod, eventType } = parseModifierParam(paramStrings[1] ?? "");
503
+ let keyName = tildeKeyMap[num];
504
+ // xterm modifyOtherKeys compatibility:
505
+ // CSI 27;<mod>;<codepoint>~ (e.g. Ctrl+Tab => CSI 27;5;9~)
506
+ if (!keyName && num === 27 && paramStrings.length >= 3) {
507
+ /* v8 ignore start -- length >= 3 is guaranteed above, so the ?? "0" fallback is unreachable */
508
+ const { codepoint: encodedCodepoint } = parseCodepointParam(paramStrings[2] ?? "0");
509
+ /* v8 ignore stop */
510
+ const kittyKey = kittyCodepointMap[encodedCodepoint];
511
+ keyName = kittyKey ? kittyKey.key : undefined;
512
+ }
513
+ // Complete tilde sequence (final byte present) but no known key — drop it, don't
514
+ // re-emit its bytes as literal text.
515
+ if (!keyName)
516
+ return { token: { kind: "unknown-csi", raw }, nextIndex };
517
+ const mods = decodeModifiers(mod);
518
+ const token = {
519
+ kind: "csi-tilde",
520
+ number: num,
521
+ key: keyName,
522
+ ...mods,
523
+ eventType,
524
+ raw,
525
+ };
526
+ return { token, nextIndex };
527
+ }
528
+ // ── SGR mouse: CSI < Cb ; Cx ; Cy M/m ──
529
+ if (params.startsWith("<") && (finalByte === "M" || finalByte === "m")) {
530
+ const sgrParams = params.substring(1); // strip leading '<'
531
+ const parts = sgrParams.split(";");
532
+ if (parts.length === 3) {
533
+ const cb = parseInt(parts[0], 10) || 0;
534
+ const cx = parseInt(parts[1], 10) || 1;
535
+ const cy = parseInt(parts[2], 10) || 1;
536
+ const isRelease = finalByte === "m";
537
+ const mouseResult = decodeMouseButton(cb, isRelease);
538
+ const token = {
539
+ kind: "mouse",
540
+ button: mouseResult.button,
541
+ action: mouseResult.action,
542
+ x: cx,
543
+ y: cy,
544
+ shiftKey: (cb & 4) !== 0,
545
+ altKey: (cb & 8) !== 0,
546
+ ctrlKey: (cb & 16) !== 0,
547
+ raw,
548
+ };
549
+ return { token, nextIndex };
550
+ }
551
+ }
552
+ // ── Legacy mouse: CSI M <3 raw bytes> ──
553
+ if (finalByte === "M" && params === "") {
554
+ // Need 3 more bytes after the final 'M'
555
+ if (nextIndex + 2 < data.length) {
556
+ const cb = data.charCodeAt(nextIndex) - 32;
557
+ const cx = data.charCodeAt(nextIndex + 1) - 32;
558
+ const cy = data.charCodeAt(nextIndex + 2) - 32;
559
+ const legacyNextIndex = nextIndex + 3;
560
+ const legacyRaw = data.slice(start, legacyNextIndex);
561
+ const isRelease = (cb & 3) === 3;
562
+ const mouseResult = decodeMouseButton(cb, isRelease);
563
+ const token = {
564
+ kind: "mouse",
565
+ button: mouseResult.button,
566
+ action: mouseResult.action,
567
+ x: cx,
568
+ y: cy,
569
+ shiftKey: (cb & 4) !== 0,
570
+ altKey: (cb & 8) !== 0,
571
+ ctrlKey: (cb & 16) !== 0,
572
+ raw: legacyRaw,
573
+ };
574
+ return { token, nextIndex: legacyNextIndex };
575
+ }
576
+ // Final 'M' present but the 3 coordinate bytes haven't arrived yet — treat as
577
+ // *incomplete* (null) so KeyInputParser buffers it until the rest of the read lands,
578
+ // rather than dropping it as an unknown CSI below.
579
+ return null;
580
+ }
581
+ // ── Cursor / navigation / F1–F4: CSI <1;mod[:eventtype]>? <letter> ──
582
+ const keyName = csiKeyMap[finalByte];
583
+ if (keyName) {
584
+ /* v8 ignore start -- length >= 2 is checked here, so the inner ?? "" fallback is unreachable */
585
+ const { mod, eventType } = parseModifierParam(paramStrings.length >= 2 ? (paramStrings[1] ?? "") : "");
586
+ /* v8 ignore stop */
587
+ const mods = decodeModifiers(mod);
588
+ // CSI Z (backtab) implies shiftKey when no explicit modifier
589
+ if (finalByte === "Z" && !mods.shiftKey) {
590
+ mods.shiftKey = true;
591
+ }
592
+ const token = {
593
+ kind: "csi-letter",
594
+ finalByte,
595
+ key: keyName,
596
+ ...mods,
597
+ eventType,
598
+ raw,
599
+ };
600
+ return { token, nextIndex };
601
+ }
602
+ // Unknown but COMPLETE CSI sequence (valid final byte, no handler matched). Consume and
603
+ // drop it as a single token so its bytes don't leak into the buffer as literal characters.
604
+ // (An *incomplete* sequence returns null earlier, so KeyInputParser can buffer the tail.)
605
+ return { token: { kind: "unknown-csi", raw }, nextIndex };
606
+ }
607
+ // ─── Mouse button decoding ───
608
+ /**
609
+ * Decode xterm mouse button byte into button name and action.
610
+ *
611
+ * Button byte encoding:
612
+ * - Bits 0-1: button number (0=left, 1=middle, 2=right, 3=release in legacy mode)
613
+ * - Bit 2: Shift
614
+ * - Bit 3: Alt (Meta)
615
+ * - Bit 4: Ctrl
616
+ * - Bit 5: Motion flag (drag / mouse move)
617
+ * - Bits 6-7: 0=normal buttons, 1=scroll (64), 2=extra buttons (128)
618
+ *
619
+ * Scroll: 64+0=up, 64+1=down, 64+2=left, 64+3=right
620
+ * Extra buttons: 128+0=button 8 (back), 128+1=button 9 (forward), etc.
621
+ */
622
+ function decodeMouseButton(cb, isRelease) {
623
+ const lowBits = cb & 3;
624
+ const isMotion = (cb & 32) !== 0;
625
+ const isScroll = (cb & 64) !== 0;
626
+ if (isScroll) {
627
+ const scrollActions = ["scroll-up", "scroll-down", "scroll-left", "scroll-right"];
628
+ /* v8 ignore start -- the ?? fallback is unreachable: lowBits is cb&3 ∈ {0..3} and scrollActions has exactly 4 entries */
629
+ return { button: "none", action: scrollActions[lowBits] ?? "scroll-up" };
630
+ /* v8 ignore stop */
631
+ }
632
+ const buttons = ["left", "middle", "right"];
633
+ const button = buttons[lowBits] ?? "none";
634
+ if (isRelease) {
635
+ return { button: lowBits === 3 ? "none" : button, action: "release" };
636
+ }
637
+ if (isMotion) {
638
+ return { button: lowBits === 3 ? "none" : button, action: "move" };
639
+ }
640
+ return { button, action: "press" };
641
+ }
642
+ /**
643
+ * Parse an OSC sequence starting at data[start] = ESC, data[start+1] = ']'.
644
+ * OSC is terminated by BEL (\x07) or ST (\x1b\\).
645
+ * Returns null if the sequence is not complete.
646
+ */
647
+ export function parseOSC(data, start) {
648
+ let i = start + 2; // skip ESC ]
649
+ // Collect OSC body until BEL or ST
650
+ let body = "";
651
+ while (i < data.length) {
652
+ const c = data.charCodeAt(i);
653
+ if (c === 0x07) {
654
+ // BEL terminator
655
+ const raw = data.slice(start, i + 1);
656
+ i++;
657
+ return buildOscResult(body, raw, i);
658
+ }
659
+ else if (c === 0x1b && i + 1 < data.length && data.charCodeAt(i + 1) === 0x5c) {
660
+ // ST terminator: ESC \
661
+ const raw = data.slice(start, i + 2);
662
+ i += 2;
663
+ return buildOscResult(body, raw, i);
664
+ }
665
+ body += data[i];
666
+ i++;
667
+ }
668
+ // Incomplete sequence
669
+ return null;
670
+ }
671
+ function buildOscResult(body, raw, nextIndex) {
672
+ // OSC body format: "<code>;<data>"
673
+ const semicolonIdx = body.indexOf(";");
674
+ const codeStr = semicolonIdx >= 0 ? body.substring(0, semicolonIdx) : body;
675
+ const data = semicolonIdx >= 0 ? body.substring(semicolonIdx + 1) : "";
676
+ const code = parseInt(codeStr, 10);
677
+ return {
678
+ token: { kind: "osc", code: isNaN(code) ? -1 : code, data, raw },
679
+ nextIndex,
680
+ };
681
+ }