@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.
- package/dist/backend/iTerminalBackend.d.ts +41 -0
- package/dist/backend/iTerminalBackend.js +1 -0
- package/dist/common/colorUtils.d.ts +19 -0
- package/dist/common/colorUtils.js +30 -0
- package/dist/common/displayLine.d.ts +72 -0
- package/dist/common/displayLine.js +190 -0
- package/dist/common/disposable.d.ts +9 -0
- package/dist/common/disposable.js +18 -0
- package/dist/common/geometryPromitives.d.ts +47 -0
- package/dist/common/geometryPromitives.js +115 -0
- package/dist/common/iTerminalSurface.d.ts +68 -0
- package/dist/common/iTerminalSurface.js +8 -0
- package/dist/common/measureTextWidth.d.ts +21 -0
- package/dist/common/measureTextWidth.js +43 -0
- package/dist/common/styleFlags.d.ts +17 -0
- package/dist/common/styleFlags.js +16 -0
- package/dist/common/textTruncation.d.ts +30 -0
- package/dist/common/textTruncation.js +124 -0
- package/dist/common/typingUtils.d.ts +1 -0
- package/dist/common/typingUtils.js +3 -0
- package/dist/common/unicodeWidth.d.ts +18 -0
- package/dist/common/unicodeWidth.js +339 -0
- package/dist/dom/borderStyle.d.ts +31 -0
- package/dist/dom/borderStyle.js +40 -0
- package/dist/dom/compositeElement.d.ts +20 -0
- package/dist/dom/compositeElement.js +44 -0
- package/dist/dom/events/contextMenuEventSource.d.ts +17 -0
- package/dist/dom/events/contextMenuEventSource.js +44 -0
- package/dist/dom/events/focusManager.d.ts +13 -0
- package/dist/dom/events/focusManager.js +56 -0
- package/dist/dom/events/mouseEventDispatcher.d.ts +24 -0
- package/dist/dom/events/mouseEventDispatcher.js +177 -0
- package/dist/dom/events/tuiEventBase.d.ts +25 -0
- package/dist/dom/events/tuiEventBase.js +39 -0
- package/dist/dom/events/tuiFocusEvent.d.ts +6 -0
- package/dist/dom/events/tuiFocusEvent.js +8 -0
- package/dist/dom/events/tuiKeyboardEvent.d.ts +21 -0
- package/dist/dom/events/tuiKeyboardEvent.js +20 -0
- package/dist/dom/events/tuiMouseEvent.d.ts +40 -0
- package/dist/dom/events/tuiMouseEvent.js +32 -0
- package/dist/dom/events/tuiPasteEvent.d.ts +10 -0
- package/dist/dom/events/tuiPasteEvent.js +13 -0
- package/dist/dom/overlayLayer.d.ts +92 -0
- package/dist/dom/overlayLayer.js +343 -0
- package/dist/dom/styles/index.d.ts +4 -0
- package/dist/dom/styles/index.js +2 -0
- package/dist/dom/styles/styleTokens.d.ts +114 -0
- package/dist/dom/styles/styleTokens.js +122 -0
- package/dist/dom/styles/tuiStyle.d.ts +75 -0
- package/dist/dom/styles/tuiStyle.js +121 -0
- package/dist/dom/tuiApplication.d.ts +57 -0
- package/dist/dom/tuiApplication.js +231 -0
- package/dist/dom/tuiElement.d.ts +528 -0
- package/dist/dom/tuiElement.js +1168 -0
- package/dist/dom/tuiSelector.d.ts +9 -0
- package/dist/dom/tuiSelector.js +82 -0
- package/dist/dom/validateTree.d.ts +42 -0
- package/dist/dom/validateTree.js +125 -0
- package/dist/input/convertToken.d.ts +3 -0
- package/dist/input/convertToken.js +114 -0
- package/dist/input/keyEvent.d.ts +46 -0
- package/dist/input/keyEvent.js +26 -0
- package/dist/input/keyInputParser.d.ts +72 -0
- package/dist/input/keyInputParser.js +249 -0
- package/dist/input/mouseTracking.d.ts +18 -0
- package/dist/input/mouseTracking.js +18 -0
- package/dist/input/parseInput.d.ts +13 -0
- package/dist/input/parseInput.js +17 -0
- package/dist/input/rawTerminalToken.d.ts +142 -0
- package/dist/input/rawTerminalToken.js +2 -0
- package/dist/input/serializeKey.d.ts +14 -0
- package/dist/input/serializeKey.js +179 -0
- package/dist/input/serializeMouse.d.ts +26 -0
- package/dist/input/serializeMouse.js +38 -0
- package/dist/input/tokenize.d.ts +59 -0
- package/dist/input/tokenize.js +681 -0
- package/dist/rendering/cell.d.ts +24 -0
- package/dist/rendering/cell.js +46 -0
- package/dist/rendering/damage.d.ts +35 -0
- package/dist/rendering/damage.js +99 -0
- package/dist/rendering/grid.d.ts +49 -0
- package/dist/rendering/grid.js +216 -0
- package/dist/rendering/gridSnapshot.d.ts +33 -0
- package/dist/rendering/gridSnapshot.js +32 -0
- package/dist/rendering/gridToSvg.d.ts +29 -0
- package/dist/rendering/gridToSvg.js +145 -0
- package/dist/rendering/terminalRenderer.d.ts +28 -0
- package/dist/rendering/terminalRenderer.js +161 -0
- package/dist/rendering/terminalScreen.d.ts +25 -0
- package/dist/rendering/terminalScreen.js +50 -0
- 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
|
+
}
|