@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,249 @@
1
+ import { convertTokenToKeyPressEvent } from "./convertToken.js";
2
+ import { createKeyPressEvent } from "./keyEvent.js";
3
+ import { parseCSI, parseOSC, tokenize } from "./tokenize.js";
4
+ /**
5
+ * Index where an *incomplete* trailing escape sequence begins, or -1 if `data`
6
+ * ends on a clean token boundary.
7
+ *
8
+ * Over SSH/tmux (and any slow link) a single keypress can be split across two
9
+ * stdin reads — e.g. Ctrl+Shift+P (`\x1b[112;6u`) arriving as `\x1b[112;6` then
10
+ * `u`. Without this, the first chunk is mis-tokenized into a lone Escape plus
11
+ * literal characters and the keybinding silently fails; the user has to press
12
+ * again. We detect the dangling tail so KeyInputParser can hold it and prepend
13
+ * it to the next chunk.
14
+ *
15
+ * Only a sequence that starts at the *last* ESC and runs to the end can be
16
+ * incomplete — anything before it is already followed by more bytes.
17
+ */
18
+ function incompleteTailStart(data) {
19
+ const esc = data.lastIndexOf("\x1b");
20
+ if (esc === -1)
21
+ return -1;
22
+ const next = data.charCodeAt(esc + 1);
23
+ // Lone trailing ESC: ambiguous (Escape key vs. start of a split sequence) — buffer it.
24
+ if (Number.isNaN(next))
25
+ return esc;
26
+ // CSI / OSC reuse the real parsers, so completeness exactly matches tokenize().
27
+ if (next === 0x5b)
28
+ return parseCSI(data, esc) === null ? esc : -1; // ESC [
29
+ if (next === 0x5d)
30
+ return parseOSC(data, esc) === null ? esc : -1; // ESC ]
31
+ if (next === 0x4f)
32
+ return esc + 2 >= data.length ? esc : -1; // ESC O (SS3) needs its final letter
33
+ // ESC + printable / control / special is complete once the second byte is present (it is).
34
+ return -1;
35
+ }
36
+ /**
37
+ * Bracketed paste markers (DEC mode ?2004). The terminal wraps clipboard pastes
38
+ * as `ESC[200~ <literal text> ESC[201~`, letting us insert the whole block as one
39
+ * text edit instead of replaying it byte-by-byte through key parsing (which would
40
+ * turn newlines into Enter keypresses and run special characters as shortcuts).
41
+ */
42
+ const PASTE_START = "\x1b[200~";
43
+ const PASTE_END = "\x1b[201~";
44
+ /**
45
+ * If `s` ends with a non-empty proper prefix of `marker` (a marker split across
46
+ * stdin reads), return the index where that partial tail begins; otherwise `s.length`.
47
+ * Used while accumulating paste content so a split `ESC[201~` end marker is held back
48
+ * rather than swallowed into the pasted text.
49
+ */
50
+ function partialMarkerTailStart(s, marker) {
51
+ const max = Math.min(s.length, marker.length - 1);
52
+ for (let len = max; len > 0; len--) {
53
+ if (s.endsWith(marker.slice(0, len)))
54
+ return s.length - len;
55
+ }
56
+ return s.length;
57
+ }
58
+ /** Normalize pasted line endings: CRLF and lone CR both become LF. */
59
+ function normalizeNewlines(text) {
60
+ return text.replace(/\r\n?/g, "\n");
61
+ }
62
+ /**
63
+ * Set of key values representing modifier keys.
64
+ * These follow the browser model: keydown/keyup only, no keypress synthesized.
65
+ */
66
+ const modifierKeyValues = new Set([
67
+ "Shift",
68
+ "Control",
69
+ "Alt",
70
+ "Meta",
71
+ "Hyper",
72
+ "Super",
73
+ "AltGraph",
74
+ "ISO_Level5_Shift",
75
+ "CapsLock",
76
+ "NumLock",
77
+ "ScrollLock",
78
+ ]);
79
+ function emptyStreams() {
80
+ return { keys: [], mouse: [], osc: [], deviceReports: [], paste: [] };
81
+ }
82
+ export class KeyInputParser {
83
+ pressedKeys = new Set();
84
+ /** Tail of the previous chunk that was cut mid-escape-sequence; "" when none. */
85
+ pending = "";
86
+ /** True while accumulating bracketed-paste content (between ESC[200~ and ESC[201~). */
87
+ pasting = false;
88
+ /** Pasted text accumulated so far, awaiting the ESC[201~ end marker. */
89
+ pasteBuffer = "";
90
+ /**
91
+ * Parse a chunk of raw terminal input into browser-like keyboard events.
92
+ */
93
+ parse(data) {
94
+ return this.ingest(data).keys;
95
+ }
96
+ /**
97
+ * Parse raw terminal input, returning both keyboard events and mouse tokens.
98
+ */
99
+ parseWithMouse(data) {
100
+ return this.ingest(data);
101
+ }
102
+ /** True when a partial escape sequence is buffered, awaiting the rest of its bytes. */
103
+ hasPending() {
104
+ return this.pending !== "";
105
+ }
106
+ /**
107
+ * Force-process any buffered partial sequence as-is (a lone ESC becomes the
108
+ * Escape key). Callers use a short timeout so a real Escape keypress isn't
109
+ * held hostage waiting for a continuation that never comes.
110
+ *
111
+ * While a paste is in flight (ESC[200~ seen, ESC[201~ not yet), this is a no-op:
112
+ * the held tail is a split end marker, not a stuck escape — keep accumulating so
113
+ * the paste isn't corrupted or truncated.
114
+ */
115
+ flush() {
116
+ const streams = emptyStreams();
117
+ if (this.pasting)
118
+ return streams;
119
+ const tail = this.pending;
120
+ this.pending = "";
121
+ if (tail !== "")
122
+ this.tokenizeInto(tail, streams);
123
+ return streams;
124
+ }
125
+ /**
126
+ * Reassemble the stream across reads, splitting out bracketed-paste blocks so
127
+ * their literal text bypasses key tokenization entirely. Normal segments keep the
128
+ * existing behavior: prepend any buffered tail, then hold back a fresh incomplete
129
+ * tail so an escape sequence cut across stdin reads is reassembled next chunk.
130
+ */
131
+ ingest(data) {
132
+ let combined = this.pending + data;
133
+ this.pending = "";
134
+ const streams = emptyStreams();
135
+ while (combined !== "") {
136
+ if (this.pasting) {
137
+ const endIdx = combined.indexOf(PASTE_END);
138
+ if (endIdx === -1) {
139
+ // No end marker yet: accumulate, but hold back a possible split ESC[201~.
140
+ const tailStart = partialMarkerTailStart(combined, PASTE_END);
141
+ this.pasteBuffer += combined.slice(0, tailStart);
142
+ this.pending = combined.slice(tailStart);
143
+ return streams;
144
+ }
145
+ this.pasteBuffer += combined.slice(0, endIdx);
146
+ const text = normalizeNewlines(this.pasteBuffer);
147
+ if (text !== "")
148
+ streams.paste.push(text);
149
+ this.pasteBuffer = "";
150
+ this.pasting = false;
151
+ combined = combined.slice(endIdx + PASTE_END.length);
152
+ continue;
153
+ }
154
+ const startIdx = combined.indexOf(PASTE_START);
155
+ if (startIdx === -1) {
156
+ // No paste start in view. A trailing partial ESC[200~ is an incomplete CSI,
157
+ // so incompleteTailStart already holds it back like any split sequence.
158
+ const cut = incompleteTailStart(combined);
159
+ if (cut === -1) {
160
+ this.tokenizeInto(combined, streams);
161
+ }
162
+ else {
163
+ this.tokenizeInto(combined.slice(0, cut), streams);
164
+ this.pending = combined.slice(cut);
165
+ }
166
+ return streams;
167
+ }
168
+ // Bytes before the marker are complete (a full marker follows them).
169
+ this.tokenizeInto(combined.slice(0, startIdx), streams);
170
+ this.pasting = true;
171
+ combined = combined.slice(startIdx + PASTE_START.length);
172
+ }
173
+ return streams;
174
+ }
175
+ tokenizeInto(data, streams) {
176
+ if (data === "")
177
+ return;
178
+ const tokens = tokenize(data);
179
+ const keyEvents = [];
180
+ for (const token of tokens) {
181
+ if (token.kind === "mouse") {
182
+ streams.mouse.push(token);
183
+ }
184
+ else if (token.kind === "osc") {
185
+ streams.osc.push(token);
186
+ }
187
+ else if (token.kind === "device-report") {
188
+ streams.deviceReports.push(token);
189
+ }
190
+ else if (token.kind === "unknown-csi") {
191
+ // Complete but unrecognized CSI sequence — drop it (never type it as text).
192
+ }
193
+ else {
194
+ keyEvents.push(convertTokenToKeyPressEvent(token));
195
+ }
196
+ }
197
+ for (const event of this.processKeyEvents(keyEvents)) {
198
+ streams.keys.push(event);
199
+ }
200
+ }
201
+ processKeyEvents(rawEvents) {
202
+ const result = [];
203
+ for (const event of rawEvents) {
204
+ if (event.type === "keydown") {
205
+ this.pressedKeys.add(event.code);
206
+ result.push(event);
207
+ if (!modifierKeyValues.has(event.key)) {
208
+ result.push(createKeyPressEvent(event.key, event.raw, {
209
+ type: "keypress",
210
+ code: event.code,
211
+ ctrlKey: event.ctrlKey,
212
+ shiftKey: event.shiftKey,
213
+ altKey: event.altKey,
214
+ metaKey: event.metaKey,
215
+ }));
216
+ }
217
+ /* v8 ignore start -- unreachable: no token converter emits a synthetic "keypress" (processKeyEvents only ever sees keydown/keyup), so the keypress branch never runs and the keyup else-if's false path (the closed-union fall-through) is also dead */
218
+ }
219
+ else if (event.type === "keypress") {
220
+ this.pressedKeys.add(event.code);
221
+ result.push(event);
222
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
223
+ }
224
+ else if (event.type === "keyup") {
225
+ /* v8 ignore stop */
226
+ if (!modifierKeyValues.has(event.key) && !this.pressedKeys.has(event.code)) {
227
+ const synth = {
228
+ type: "keypress",
229
+ code: event.code,
230
+ ctrlKey: event.ctrlKey,
231
+ shiftKey: event.shiftKey,
232
+ altKey: event.altKey,
233
+ metaKey: event.metaKey,
234
+ };
235
+ result.push(createKeyPressEvent(event.key, event.raw, { ...synth, type: "keydown" }));
236
+ result.push(createKeyPressEvent(event.key, event.raw, synth));
237
+ }
238
+ this.pressedKeys.delete(event.code);
239
+ result.push(event);
240
+ }
241
+ else {
242
+ /* v8 ignore start -- unreachable: event.type is a closed union (keydown|keypress|keyup), all handled above */
243
+ result.push(event);
244
+ /* v8 ignore stop */
245
+ }
246
+ }
247
+ return result;
248
+ }
249
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Mouse tracking escape sequences for xterm-compatible terminals.
3
+ *
4
+ * Modes:
5
+ * - 1000: Normal tracking — reports button press and release
6
+ * - 1002: Button-event tracking — also reports motion while a button is held (drag)
7
+ * - 1003: Any-event tracking — reports all motion events (even without button held)
8
+ * - 1006: SGR extended mode — uses CSI < Cb;Cx;Cy M/m format (no coordinate limits)
9
+ *
10
+ * SGR mode (1006) should always be enabled alongside a tracking mode,
11
+ * because the legacy X10 format limits coordinates to 223 and doesn't distinguish release buttons.
12
+ *
13
+ * Reference: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
14
+ */
15
+ /** Enable any-event tracking (press + release + all motion) with SGR extended coordinates */
16
+ export declare const MOUSE_TRACKING_ALL_ENABLE = "\u001B[?1003h\u001B[?1006h";
17
+ /** Disable all mouse tracking modes */
18
+ export declare const MOUSE_TRACKING_DISABLE = "\u001B[?1000l\u001B[?1002l\u001B[?1003l\u001B[?1006l";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Mouse tracking escape sequences for xterm-compatible terminals.
3
+ *
4
+ * Modes:
5
+ * - 1000: Normal tracking — reports button press and release
6
+ * - 1002: Button-event tracking — also reports motion while a button is held (drag)
7
+ * - 1003: Any-event tracking — reports all motion events (even without button held)
8
+ * - 1006: SGR extended mode — uses CSI < Cb;Cx;Cy M/m format (no coordinate limits)
9
+ *
10
+ * SGR mode (1006) should always be enabled alongside a tracking mode,
11
+ * because the legacy X10 format limits coordinates to 223 and doesn't distinguish release buttons.
12
+ *
13
+ * Reference: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
14
+ */
15
+ /** Enable any-event tracking (press + release + all motion) with SGR extended coordinates */
16
+ export const MOUSE_TRACKING_ALL_ENABLE = "\x1b[?1003h\x1b[?1006h";
17
+ /** Disable all mouse tracking modes */
18
+ export const MOUSE_TRACKING_DISABLE = "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l";
@@ -0,0 +1,13 @@
1
+ import type { KeyPressEvent } from "./keyEvent.js";
2
+ /**
3
+ * Parse raw terminal input into KeyPressEvent[].
4
+ *
5
+ * Composes tokenize() (protocol-specific token extraction) with
6
+ * convertTokenToKeyPressEvent() (unified KeyPressEvent mapping).
7
+ *
8
+ * Mouse tokens are filtered out — they are handled separately
9
+ * at the tokenize layer (see MouseToken in RawTerminalToken).
10
+ *
11
+ * Pure function, no side effects.
12
+ */
13
+ export declare function parseInput(data: string): KeyPressEvent[];
@@ -0,0 +1,17 @@
1
+ import { convertTokenToKeyPressEvent } from "./convertToken.js";
2
+ import { tokenize } from "./tokenize.js";
3
+ /**
4
+ * Parse raw terminal input into KeyPressEvent[].
5
+ *
6
+ * Composes tokenize() (protocol-specific token extraction) with
7
+ * convertTokenToKeyPressEvent() (unified KeyPressEvent mapping).
8
+ *
9
+ * Mouse tokens are filtered out — they are handled separately
10
+ * at the tokenize layer (see MouseToken in RawTerminalToken).
11
+ *
12
+ * Pure function, no side effects.
13
+ */
14
+ export function parseInput(data) {
15
+ const keyTokens = tokenize(data).filter((t) => t.kind !== "mouse" && t.kind !== "osc" && t.kind !== "device-report" && t.kind !== "unknown-csi");
16
+ return keyTokens.map(convertTokenToKeyPressEvent);
17
+ }
@@ -0,0 +1,142 @@
1
+ export interface CsiUToken {
2
+ readonly kind: "csi-u";
3
+ readonly codepoint: number;
4
+ readonly shiftedKey: number | undefined;
5
+ readonly baseLayoutKey: number | undefined;
6
+ readonly key: string;
7
+ readonly code: string;
8
+ readonly shiftKey: boolean;
9
+ readonly altKey: boolean;
10
+ readonly ctrlKey: boolean;
11
+ readonly metaKey: boolean;
12
+ /** 0 = not specified, 1 = press, 2 = repeat, 3 = release */
13
+ readonly eventType: number;
14
+ readonly raw: string;
15
+ }
16
+ export interface PuaToken {
17
+ readonly kind: "pua";
18
+ readonly codepoint: number;
19
+ readonly key: string;
20
+ readonly code: string;
21
+ readonly raw: string;
22
+ }
23
+ export interface CsiLetterToken {
24
+ readonly kind: "csi-letter";
25
+ readonly finalByte: string;
26
+ readonly key: string;
27
+ readonly shiftKey: boolean;
28
+ readonly altKey: boolean;
29
+ readonly ctrlKey: boolean;
30
+ readonly metaKey: boolean;
31
+ /** 0 = not specified (legacy), 1 = press, 2 = repeat, 3 = release */
32
+ readonly eventType: number;
33
+ readonly raw: string;
34
+ }
35
+ export interface CsiTildeToken {
36
+ readonly kind: "csi-tilde";
37
+ readonly number: number;
38
+ readonly key: string;
39
+ readonly shiftKey: boolean;
40
+ readonly altKey: boolean;
41
+ readonly ctrlKey: boolean;
42
+ readonly metaKey: boolean;
43
+ /** 0 = not specified (legacy), 1 = press, 2 = repeat, 3 = release */
44
+ readonly eventType: number;
45
+ readonly raw: string;
46
+ }
47
+ export interface Ss3Token {
48
+ readonly kind: "ss3";
49
+ readonly finalByte: string;
50
+ readonly key: string;
51
+ readonly raw: string;
52
+ }
53
+ export interface EscCharToken {
54
+ readonly kind: "esc-char";
55
+ readonly char: string;
56
+ readonly charCode: number;
57
+ readonly raw: string;
58
+ }
59
+ export interface EscControlToken {
60
+ readonly kind: "esc-control";
61
+ readonly letter: string;
62
+ readonly raw: string;
63
+ }
64
+ export interface EscSpecialToken {
65
+ readonly kind: "esc-special";
66
+ readonly key: "Enter" | "Backspace";
67
+ readonly raw: string;
68
+ }
69
+ export interface StandaloneEscToken {
70
+ readonly kind: "standalone-esc";
71
+ readonly raw: string;
72
+ }
73
+ export interface CharToken {
74
+ readonly kind: "char";
75
+ readonly char: string;
76
+ readonly codepoint: number;
77
+ readonly raw: string;
78
+ }
79
+ export interface SpecialKeyToken {
80
+ readonly kind: "special-key";
81
+ readonly key: "Enter" | "Tab" | "Backspace";
82
+ readonly raw: string;
83
+ }
84
+ export interface CtrlCharToken {
85
+ readonly kind: "ctrl-char";
86
+ readonly letter: string;
87
+ readonly raw: string;
88
+ }
89
+ export interface UnknownByteToken {
90
+ readonly kind: "unknown-byte";
91
+ readonly byte: number;
92
+ readonly raw: string;
93
+ }
94
+ /**
95
+ * A *complete* but unrecognized CSI sequence (ESC `[` … final byte in 0x40–0x7e that
96
+ * matched no key/mouse/device-report handler). Carries the full raw bytes so the whole
97
+ * sequence is consumed and dropped rather than leaking its `[…u`-style bytes into the
98
+ * buffer as literal characters. Distinct from a `null` parse, which means *incomplete*
99
+ * (a sequence cut across stdin reads, which must be buffered until the rest arrives).
100
+ */
101
+ export interface UnknownCsiToken {
102
+ readonly kind: "unknown-csi";
103
+ readonly raw: string;
104
+ }
105
+ export interface OscToken {
106
+ readonly kind: "osc";
107
+ /** OSC command number, e.g. 52 for clipboard */
108
+ readonly code: number;
109
+ /** Everything after the first semicolon, up to the terminator */
110
+ readonly data: string;
111
+ readonly raw: string;
112
+ }
113
+ /**
114
+ * A private-mode CSI response to one of our capability probes — NOT a key event.
115
+ * - "kitty-flags": reply to the Kitty keyboard-protocol query `CSI ? u` → `CSI ? <flags> u`.
116
+ * - "da1": reply to Primary Device Attributes `CSI c` → `CSI ? <attrs> c`.
117
+ * These begin with the private marker `?`, which never appears in a real key event.
118
+ */
119
+ export interface DeviceReportToken {
120
+ readonly kind: "device-report";
121
+ readonly report: "kitty-flags" | "da1";
122
+ /** Raw parameter bytes including the leading `?` (e.g. "?15" or "?62;1;6"). */
123
+ readonly params: string;
124
+ readonly raw: string;
125
+ }
126
+ export type MouseButton = "left" | "middle" | "right" | "none";
127
+ export type MouseAction = "press" | "release" | "move" | "scroll-up" | "scroll-down" | "scroll-left" | "scroll-right";
128
+ export interface MouseToken {
129
+ readonly kind: "mouse";
130
+ readonly button: MouseButton;
131
+ readonly action: MouseAction;
132
+ /** 1-based column */
133
+ readonly x: number;
134
+ /** 1-based row */
135
+ readonly y: number;
136
+ readonly shiftKey: boolean;
137
+ readonly altKey: boolean;
138
+ readonly ctrlKey: boolean;
139
+ readonly raw: string;
140
+ }
141
+ export type RawTerminalToken = CsiUToken | PuaToken | CsiLetterToken | CsiTildeToken | Ss3Token | EscCharToken | EscControlToken | EscSpecialToken | StandaloneEscToken | CharToken | SpecialKeyToken | CtrlCharToken | UnknownByteToken | UnknownCsiToken | MouseToken | OscToken | DeviceReportToken;
142
+ export type RawKeyToken = CsiUToken | PuaToken | CsiLetterToken | CsiTildeToken | Ss3Token | EscCharToken | EscControlToken | EscSpecialToken | StandaloneEscToken | CharToken | SpecialKeyToken | CtrlCharToken | UnknownByteToken;
@@ -0,0 +1,2 @@
1
+ // ─── Kitty protocol tokens ───
2
+ export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Convert a human-readable key name (DSL) to the raw terminal escape sequence.
3
+ * Inverse of parseInput — used in tests for the DSL:
4
+ * serializeKey('a') → 'a'
5
+ * serializeKey('Ctrl+C') → '\x03'
6
+ * serializeKey('Enter') → '\r'
7
+ * serializeKey('ArrowUp') → '\x1b[A'
8
+ * serializeKey('Ctrl+ArrowUp') → '\x1b[1;5A'
9
+ * serializeKey('F5') → '\x1b[15~'
10
+ * serializeKey('Alt+a') → '\x1ba'
11
+ *
12
+ * Supports modifier prefixes: Ctrl+, Shift+, Alt+, Meta+ (combinable).
13
+ */
14
+ export declare function serializeKey(name: string): string;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Convert a human-readable key name (DSL) to the raw terminal escape sequence.
3
+ * Inverse of parseInput — used in tests for the DSL:
4
+ * serializeKey('a') → 'a'
5
+ * serializeKey('Ctrl+C') → '\x03'
6
+ * serializeKey('Enter') → '\r'
7
+ * serializeKey('ArrowUp') → '\x1b[A'
8
+ * serializeKey('Ctrl+ArrowUp') → '\x1b[1;5A'
9
+ * serializeKey('F5') → '\x1b[15~'
10
+ * serializeKey('Alt+a') → '\x1ba'
11
+ *
12
+ * Supports modifier prefixes: Ctrl+, Shift+, Alt+, Meta+ (combinable).
13
+ */
14
+ /** Simple special keys (no modifiers, no CSI) */
15
+ const simpleSpecialKeys = {
16
+ Enter: "\x0d",
17
+ Tab: "\x09",
18
+ Backspace: "\x7f",
19
+ Escape: "\x1b",
20
+ Space: " ",
21
+ };
22
+ /** Keys using CSI <letter> format (also used for Ctrl/Shift/Alt variants) */
23
+ const csiLetterKeys = {
24
+ ArrowUp: "A",
25
+ ArrowDown: "B",
26
+ ArrowRight: "C",
27
+ ArrowLeft: "D",
28
+ Home: "H",
29
+ End: "F",
30
+ F1: "P",
31
+ F2: "Q",
32
+ F3: "R",
33
+ F4: "S",
34
+ };
35
+ /** Keys using SS3 format (no-modifier variant of F1–F4 and cursor keys in app mode) */
36
+ const ss3Keys = {
37
+ F1: "P",
38
+ F2: "Q",
39
+ F3: "R",
40
+ F4: "S",
41
+ };
42
+ /** Keys using CSI <num>~ format */
43
+ const csiTildeKeys = {
44
+ Insert: 2,
45
+ Delete: 3,
46
+ PageUp: 5,
47
+ PageDown: 6,
48
+ F5: 15,
49
+ F6: 17,
50
+ F7: 18,
51
+ F8: 19,
52
+ F9: 20,
53
+ F10: 21,
54
+ F11: 23,
55
+ F12: 24,
56
+ };
57
+ /** Клавиши, чей Ctrl-вариант терминал шлёт кодами 0x1c–0x1f (зеркало `tokenize`). */
58
+ const ctrlSymbolCodes = {
59
+ "\\": 0x1c,
60
+ "]": 0x1d,
61
+ "6": 0x1e,
62
+ "/": 0x1f,
63
+ };
64
+ function encodeModifier(ctrl, shift, alt, meta) {
65
+ let mod = 1;
66
+ if (shift)
67
+ mod += 1;
68
+ if (alt)
69
+ mod += 2;
70
+ if (ctrl)
71
+ mod += 4;
72
+ if (meta)
73
+ mod += 8;
74
+ return mod;
75
+ }
76
+ export function serializeKey(name) {
77
+ // Parse modifier prefixes: "Ctrl+Shift+ArrowUp" → modifiers + "ArrowUp"
78
+ let ctrl = false;
79
+ let shift = false;
80
+ let alt = false;
81
+ let meta = false;
82
+ let remaining = name;
83
+ const modPattern = /^(Ctrl|Shift|Alt|Meta)\+/;
84
+ let match = modPattern.exec(remaining);
85
+ while (match) {
86
+ const mod = match[1];
87
+ if (mod === "Ctrl")
88
+ ctrl = true;
89
+ else if (mod === "Shift")
90
+ shift = true;
91
+ /* v8 ignore start -- the modifier regex only ever captures Ctrl|Shift|Alt|Meta, so the final "none matched" fall-through (no else clause) is unreachable; v8 attributes that phantom branch to these chained else-ifs */ else if (mod === "Alt")
92
+ alt = true;
93
+ else if (mod === "Meta")
94
+ meta = true;
95
+ /* v8 ignore stop */
96
+ remaining = remaining.slice(match[0].length);
97
+ match = modPattern.exec(remaining);
98
+ }
99
+ const hasModifiers = ctrl || shift || alt || meta;
100
+ // Simple special keys without modifiers
101
+ if (!hasModifiers && remaining in simpleSpecialKeys) {
102
+ return simpleSpecialKeys[remaining];
103
+ }
104
+ // Shift+Tab → CSI Z (reverse tab / backtab)
105
+ if (shift && !ctrl && !alt && !meta && remaining === "Tab") {
106
+ return "\x1b[Z";
107
+ }
108
+ // Tab with modifiers in Kitty CSI-u form: CSI 9;{mod}:1u
109
+ if (remaining === "Tab" && hasModifiers) {
110
+ const mod = encodeModifier(ctrl, shift, alt, meta);
111
+ return `\x1b[9;${mod.toString()}:1u`;
112
+ }
113
+ // Enter with modifiers in Kitty CSI-u form: CSI 13;{mod}u (Ctrl+Enter = commit).
114
+ if (remaining === "Enter" && hasModifiers) {
115
+ const mod = encodeModifier(ctrl, shift, alt, meta);
116
+ return `\x1b[13;${mod.toString()}u`;
117
+ }
118
+ // Ctrl+Space → NUL: именно так его шлёт терминал (см. tokenize.ts, 0x00).
119
+ if (ctrl && !shift && !alt && !meta && remaining === "Space") {
120
+ return "\x00";
121
+ }
122
+ // Backspace с модификаторами — CSI 127;{mod}u (форма kitty). Легаси-байт 0x08
123
+ // здесь не годится: он неотличим от Ctrl+H, и бинд `ctrl+backspace` по нему
124
+ // не сматчится — ровно поэтому Ctrl+Backspace и не работал.
125
+ if (remaining === "Backspace" && hasModifiers) {
126
+ const mod = encodeModifier(ctrl, shift, alt, meta);
127
+ return `\x1b[127;${mod.toString()}u`;
128
+ }
129
+ // Ctrl+letter → control character (0x01–0x1a)
130
+ if (ctrl && !shift && !alt && !meta && remaining.length === 1 && /[a-zA-Z]/.test(remaining)) {
131
+ const code = remaining.toUpperCase().charCodeAt(0) - 0x40;
132
+ return String.fromCharCode(code);
133
+ }
134
+ // Ctrl+\ ] 6 / → control-коды 0x1c–0x1f. Парно с разбором в `tokenize`:
135
+ // это единственные не-буквенные комбинации, которые терминал вообще умеет
136
+ // передать без расширенного протокола.
137
+ if (ctrl && !shift && !alt && !meta && remaining in ctrlSymbolCodes) {
138
+ return String.fromCharCode(ctrlSymbolCodes[remaining]);
139
+ }
140
+ // Alt+single character → ESC prefix
141
+ if (alt && !ctrl && !shift && !meta && remaining.length === 1) {
142
+ return `\x1b${remaining}`;
143
+ }
144
+ // CSI letter keys (cursor keys, F1–F4)
145
+ if (remaining in csiLetterKeys) {
146
+ const letter = csiLetterKeys[remaining];
147
+ if (!hasModifiers) {
148
+ // F1–F4 without modifiers use SS3 format
149
+ if (remaining in ss3Keys) {
150
+ return `\x1bO${ss3Keys[remaining]}`;
151
+ }
152
+ return `\x1b[${letter}`;
153
+ }
154
+ const mod = encodeModifier(ctrl, shift, alt, meta);
155
+ return `\x1b[1;${mod.toString()}${letter}`;
156
+ }
157
+ // Kitty functional keys without legacy escape form (CSI-u codepoint)
158
+ if (remaining === "ContextMenu") {
159
+ if (!hasModifiers) {
160
+ return "\x1b[57363u";
161
+ }
162
+ const mod = encodeModifier(ctrl, shift, alt, meta);
163
+ return `\x1b[57363;${mod.toString()}u`;
164
+ }
165
+ // CSI tilde keys (Insert, Delete, PageUp, PageDown, F5–F12)
166
+ if (remaining in csiTildeKeys) {
167
+ const num = csiTildeKeys[remaining];
168
+ if (!hasModifiers) {
169
+ return `\x1b[${num.toString()}~`;
170
+ }
171
+ const mod = encodeModifier(ctrl, shift, alt, meta);
172
+ return `\x1b[${num.toString()};${mod.toString()}~`;
173
+ }
174
+ // Single printable character (no modifiers)
175
+ if (!hasModifiers && remaining.length === 1) {
176
+ return remaining;
177
+ }
178
+ throw new Error(`serializeKey: unknown key name "${name}". Add it to the mapping.`);
179
+ }