@rlabs-inc/tui 0.1.0 → 0.2.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/README.md +126 -13
- package/index.ts +11 -5
- package/package.json +2 -2
- package/src/api/mount.ts +42 -27
- package/src/engine/arrays/core.ts +13 -21
- package/src/engine/arrays/dimensions.ts +22 -32
- package/src/engine/arrays/index.ts +88 -86
- package/src/engine/arrays/interaction.ts +34 -48
- package/src/engine/arrays/layout.ts +67 -92
- package/src/engine/arrays/spacing.ts +37 -52
- package/src/engine/arrays/text.ts +23 -31
- package/src/engine/arrays/visual.ts +56 -75
- package/src/engine/inheritance.ts +18 -18
- package/src/engine/registry.ts +15 -0
- package/src/pipeline/frameBuffer.ts +26 -26
- package/src/pipeline/layout/index.ts +2 -2
- package/src/pipeline/layout/titan-engine.ts +112 -84
- package/src/primitives/animation.ts +194 -0
- package/src/primitives/box.ts +74 -86
- package/src/primitives/each.ts +87 -0
- package/src/primitives/index.ts +7 -0
- package/src/primitives/scope.ts +215 -0
- package/src/primitives/show.ts +77 -0
- package/src/primitives/text.ts +63 -59
- package/src/primitives/types.ts +1 -1
- package/src/primitives/when.ts +102 -0
- package/src/renderer/append-region.ts +303 -0
- package/src/renderer/index.ts +4 -2
- package/src/renderer/output.ts +11 -34
- package/src/state/focus.ts +16 -5
- package/src/state/global-keys.ts +184 -0
- package/src/state/index.ts +44 -8
- package/src/state/input.ts +534 -0
- package/src/state/keyboard.ts +98 -674
- package/src/state/mouse.ts +163 -340
- package/src/state/scroll.ts +7 -9
- package/src/types/index.ts +6 -0
- package/src/renderer/input.ts +0 -518
package/src/state/scroll.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* - scrollable/maxScrollX/Y = computed by TITAN (read from layoutDerived)
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// SlotArray auto-unwraps, no unwrap() needed
|
|
22
22
|
import * as interaction from '../engine/arrays/interaction'
|
|
23
23
|
import { focusedIndex } from '../engine/arrays/interaction'
|
|
24
24
|
import { layoutDerived } from '../pipeline/layout'
|
|
@@ -50,8 +50,8 @@ export function isScrollable(index: number): boolean {
|
|
|
50
50
|
/** Get current scroll offset for a component (user state) */
|
|
51
51
|
export function getScrollOffset(index: number): { x: number; y: number } {
|
|
52
52
|
return {
|
|
53
|
-
x:
|
|
54
|
-
y:
|
|
53
|
+
x: interaction.scrollOffsetX[index] ?? 0,
|
|
54
|
+
y: interaction.scrollOffsetY[index] ?? 0,
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -78,12 +78,9 @@ export function setScrollOffset(index: number, x: number, y: number): void {
|
|
|
78
78
|
const clampedX = Math.max(0, Math.min(x, max.x))
|
|
79
79
|
const clampedY = Math.max(0, Math.min(y, max.y))
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (interaction.scrollOffsetY[index]) {
|
|
85
|
-
interaction.scrollOffsetY[index]!.value = clampedY
|
|
86
|
-
}
|
|
81
|
+
// SlotArray uses setValue() for direct value updates
|
|
82
|
+
interaction.scrollOffsetX.setValue(index, clampedX)
|
|
83
|
+
interaction.scrollOffsetY.setValue(index, clampedY)
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
/** Scroll by a delta amount */
|
|
@@ -186,6 +183,7 @@ export function getFocusedScrollable(): number {
|
|
|
186
183
|
export function handleArrowScroll(
|
|
187
184
|
direction: 'up' | 'down' | 'left' | 'right'
|
|
188
185
|
): boolean {
|
|
186
|
+
const focused = focusedIndex.value
|
|
189
187
|
const scrollable = getFocusedScrollable()
|
|
190
188
|
if (scrollable < 0) return false
|
|
191
189
|
|
package/src/types/index.ts
CHANGED
|
@@ -282,6 +282,12 @@ export interface MountOptions {
|
|
|
282
282
|
kittyKeyboard?: boolean
|
|
283
283
|
/** Initial cursor configuration */
|
|
284
284
|
cursor?: Partial<Cursor>
|
|
285
|
+
/**
|
|
286
|
+
* For append mode: Function to determine static region height.
|
|
287
|
+
* Called on each render to decide where to split static/reactive regions.
|
|
288
|
+
* Return number of lines to freeze into terminal history.
|
|
289
|
+
*/
|
|
290
|
+
getStaticHeight?: () => number
|
|
285
291
|
}
|
|
286
292
|
|
|
287
293
|
// =============================================================================
|
package/src/renderer/input.ts
DELETED
|
@@ -1,518 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TUI Framework - Input Parsing
|
|
3
|
-
*
|
|
4
|
-
* Parses terminal stdin into structured key, mouse, and other events.
|
|
5
|
-
* Handles escape sequence buffering and the various protocols:
|
|
6
|
-
* - CSI sequences (arrows, function keys)
|
|
7
|
-
* - SS3 sequences (F1-F4)
|
|
8
|
-
* - SGR mouse protocol
|
|
9
|
-
* - Kitty keyboard protocol
|
|
10
|
-
* - Bracketed paste
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { KeyEvent, MouseEvent, FocusEvent, Modifiers, KeyState, MouseButton, MouseAction } from '../types'
|
|
14
|
-
|
|
15
|
-
// =============================================================================
|
|
16
|
-
// Parsed Input Types
|
|
17
|
-
// =============================================================================
|
|
18
|
-
|
|
19
|
-
export type ParsedInput =
|
|
20
|
-
| { type: 'key'; event: KeyEvent }
|
|
21
|
-
| { type: 'mouse'; event: MouseEvent }
|
|
22
|
-
| { type: 'focus'; event: FocusEvent }
|
|
23
|
-
| { type: 'paste'; data: string }
|
|
24
|
-
|
|
25
|
-
// =============================================================================
|
|
26
|
-
// Input Buffer
|
|
27
|
-
// =============================================================================
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Buffer for accumulating partial escape sequences.
|
|
31
|
-
* Escape sequences can arrive split across multiple stdin reads.
|
|
32
|
-
*/
|
|
33
|
-
export class InputBuffer {
|
|
34
|
-
private buffer = ''
|
|
35
|
-
private timeout: Timer | null = null
|
|
36
|
-
private readonly TIMEOUT_MS = 50 // Max wait for complete sequence
|
|
37
|
-
private pendingEvents: ParsedInput[] = []
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Add data to buffer and extract complete events.
|
|
41
|
-
*/
|
|
42
|
-
parse(data: string | Uint8Array): ParsedInput[] {
|
|
43
|
-
const str = typeof data === 'string' ? data : new TextDecoder().decode(data)
|
|
44
|
-
this.buffer += str
|
|
45
|
-
|
|
46
|
-
// Clear any pending timeout
|
|
47
|
-
if (this.timeout) {
|
|
48
|
-
clearTimeout(this.timeout)
|
|
49
|
-
this.timeout = null
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const events: ParsedInput[] = []
|
|
53
|
-
let consumed = 0
|
|
54
|
-
|
|
55
|
-
while (consumed < this.buffer.length) {
|
|
56
|
-
const remaining = this.buffer.slice(consumed)
|
|
57
|
-
const result = this.parseOne(remaining)
|
|
58
|
-
|
|
59
|
-
if (result.event) {
|
|
60
|
-
events.push(result.event)
|
|
61
|
-
consumed += result.consumed
|
|
62
|
-
} else if (result.incomplete) {
|
|
63
|
-
// Partial sequence - wait for more data
|
|
64
|
-
break
|
|
65
|
-
} else {
|
|
66
|
-
// Unknown/invalid - skip one byte
|
|
67
|
-
consumed++
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Remove consumed data
|
|
72
|
-
this.buffer = this.buffer.slice(consumed)
|
|
73
|
-
|
|
74
|
-
// If buffer still has data, set timeout to flush as raw input
|
|
75
|
-
if (this.buffer.length > 0) {
|
|
76
|
-
this.timeout = setTimeout(() => {
|
|
77
|
-
for (const char of this.buffer) {
|
|
78
|
-
this.pendingEvents.push({
|
|
79
|
-
type: 'key',
|
|
80
|
-
event: this.simpleKey(char),
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
this.buffer = ''
|
|
84
|
-
}, this.TIMEOUT_MS)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Include any pending events from previous timeout
|
|
88
|
-
if (this.pendingEvents.length > 0) {
|
|
89
|
-
events.push(...this.pendingEvents)
|
|
90
|
-
this.pendingEvents = []
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return events
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Parse a single event from the start of the string.
|
|
98
|
-
*/
|
|
99
|
-
private parseOne(data: string): { event?: ParsedInput; consumed: number; incomplete?: boolean } {
|
|
100
|
-
if (data.length === 0) {
|
|
101
|
-
return { consumed: 0 }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Escape sequence
|
|
105
|
-
if (data[0] === '\x1b') {
|
|
106
|
-
return this.parseEscape(data)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Regular character
|
|
110
|
-
const char = data[0]!
|
|
111
|
-
const codepoint = char.codePointAt(0) ?? 0
|
|
112
|
-
|
|
113
|
-
// Control characters
|
|
114
|
-
if (codepoint < 32) {
|
|
115
|
-
return {
|
|
116
|
-
event: { type: 'key', event: this.controlKey(codepoint) },
|
|
117
|
-
consumed: 1,
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// DEL
|
|
122
|
-
if (codepoint === 127) {
|
|
123
|
-
return {
|
|
124
|
-
event: { type: 'key', event: this.simpleKey('Backspace') },
|
|
125
|
-
consumed: 1,
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Normal character (handles multi-byte Unicode)
|
|
130
|
-
return {
|
|
131
|
-
event: { type: 'key', event: this.simpleKey(char!) },
|
|
132
|
-
consumed: char!.length,
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Parse escape sequence.
|
|
138
|
-
*/
|
|
139
|
-
private parseEscape(data: string): { event?: ParsedInput; consumed: number; incomplete?: boolean } {
|
|
140
|
-
if (data.length === 1) {
|
|
141
|
-
// Just ESC, might be start of sequence
|
|
142
|
-
return { consumed: 0, incomplete: true }
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const second = data[1]!
|
|
146
|
-
|
|
147
|
-
// CSI sequence: ESC [
|
|
148
|
-
if (second === '[') {
|
|
149
|
-
return this.parseCSI(data)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// SS3 sequence: ESC O
|
|
153
|
-
if (second === 'O') {
|
|
154
|
-
return this.parseSS3(data)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Alt + key: ESC + char
|
|
158
|
-
if (second.length === 1 && second.codePointAt(0)! >= 32) {
|
|
159
|
-
return {
|
|
160
|
-
event: {
|
|
161
|
-
type: 'key',
|
|
162
|
-
event: {
|
|
163
|
-
key: second,
|
|
164
|
-
modifiers: { ctrl: false, alt: true, shift: false, meta: false },
|
|
165
|
-
state: 'press',
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
consumed: 2,
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Just ESC key
|
|
173
|
-
return {
|
|
174
|
-
event: { type: 'key', event: this.simpleKey('Escape') },
|
|
175
|
-
consumed: 1,
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Parse CSI sequence (ESC [ ...).
|
|
181
|
-
*/
|
|
182
|
-
private parseCSI(data: string): { event?: ParsedInput; consumed: number; incomplete?: boolean } {
|
|
183
|
-
// Find the terminator (letter or ~)
|
|
184
|
-
let i = 2
|
|
185
|
-
while (i < data.length) {
|
|
186
|
-
const c = data.charCodeAt(i)
|
|
187
|
-
if ((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 126) {
|
|
188
|
-
break
|
|
189
|
-
}
|
|
190
|
-
i++
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (i >= data.length) {
|
|
194
|
-
return { consumed: 0, incomplete: true }
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const sequence = data.slice(2, i)
|
|
198
|
-
const terminator = data[i]!
|
|
199
|
-
const consumed = i + 1
|
|
200
|
-
|
|
201
|
-
// Focus events
|
|
202
|
-
if (terminator === 'I') {
|
|
203
|
-
return { event: { type: 'focus', event: { focused: true } }, consumed }
|
|
204
|
-
}
|
|
205
|
-
if (terminator === 'O') {
|
|
206
|
-
return { event: { type: 'focus', event: { focused: false } }, consumed }
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Mouse SGR mode: ESC [ < params M/m
|
|
210
|
-
if (sequence.startsWith('<')) {
|
|
211
|
-
return this.parseMouseSGR(sequence, terminator, consumed)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Keyboard sequences
|
|
215
|
-
return this.parseCSIKey(sequence, terminator, consumed)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Parse SGR mouse event.
|
|
220
|
-
* Format: ESC [ < button ; x ; y M/m
|
|
221
|
-
*/
|
|
222
|
-
private parseMouseSGR(
|
|
223
|
-
sequence: string,
|
|
224
|
-
terminator: string,
|
|
225
|
-
consumed: number
|
|
226
|
-
): { event?: ParsedInput; consumed: number } {
|
|
227
|
-
const params = sequence.slice(1).split(';').map(Number)
|
|
228
|
-
if (params.length < 3) {
|
|
229
|
-
return { consumed }
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const buttonCode = params[0]!
|
|
233
|
-
const x = params[1]!
|
|
234
|
-
const y = params[2]!
|
|
235
|
-
|
|
236
|
-
// Decode button and action
|
|
237
|
-
const baseButton = buttonCode & 3
|
|
238
|
-
const isScroll = (buttonCode & 64) !== 0
|
|
239
|
-
const isMotion = (buttonCode & 32) !== 0
|
|
240
|
-
|
|
241
|
-
let button: MouseButton
|
|
242
|
-
let action: MouseAction
|
|
243
|
-
let scrollDelta: number | undefined
|
|
244
|
-
|
|
245
|
-
if (isScroll) {
|
|
246
|
-
button = 'none'
|
|
247
|
-
action = 'scroll'
|
|
248
|
-
scrollDelta = baseButton === 0 ? -1 : 1 // 0 = up, 1 = down
|
|
249
|
-
} else if (isMotion && baseButton === 3) {
|
|
250
|
-
button = 'none'
|
|
251
|
-
action = 'move'
|
|
252
|
-
} else {
|
|
253
|
-
button = (['left', 'middle', 'right', 'none'] as const)[baseButton] ?? 'none'
|
|
254
|
-
action = terminator === 'M' ? 'down' : 'up'
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Modifiers
|
|
258
|
-
const modifiers: Modifiers = {
|
|
259
|
-
shift: (buttonCode & 4) !== 0,
|
|
260
|
-
alt: (buttonCode & 8) !== 0,
|
|
261
|
-
ctrl: (buttonCode & 16) !== 0,
|
|
262
|
-
meta: false,
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return {
|
|
266
|
-
event: {
|
|
267
|
-
type: 'mouse',
|
|
268
|
-
event: {
|
|
269
|
-
x: x - 1, // Convert to 0-indexed
|
|
270
|
-
y: y - 1,
|
|
271
|
-
button,
|
|
272
|
-
action,
|
|
273
|
-
scrollDelta,
|
|
274
|
-
modifiers,
|
|
275
|
-
},
|
|
276
|
-
},
|
|
277
|
-
consumed,
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Parse CSI keyboard sequence.
|
|
283
|
-
*/
|
|
284
|
-
private parseCSIKey(
|
|
285
|
-
sequence: string,
|
|
286
|
-
terminator: string,
|
|
287
|
-
consumed: number
|
|
288
|
-
): { event?: ParsedInput; consumed: number } {
|
|
289
|
-
const modifiers = this.defaultModifiers()
|
|
290
|
-
|
|
291
|
-
// Parse modifiers from sequence (e.g., "1;5" means Ctrl)
|
|
292
|
-
const parts = sequence.split(';')
|
|
293
|
-
if (parts.length >= 2) {
|
|
294
|
-
const mod = parseInt(parts[1]!, 10) - 1
|
|
295
|
-
modifiers.shift = (mod & 1) !== 0
|
|
296
|
-
modifiers.alt = (mod & 2) !== 0
|
|
297
|
-
modifiers.ctrl = (mod & 4) !== 0
|
|
298
|
-
modifiers.meta = (mod & 8) !== 0
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
let key: string
|
|
302
|
-
|
|
303
|
-
// Arrow keys
|
|
304
|
-
if (terminator === 'A') key = 'ArrowUp'
|
|
305
|
-
else if (terminator === 'B') key = 'ArrowDown'
|
|
306
|
-
else if (terminator === 'C') key = 'ArrowRight'
|
|
307
|
-
else if (terminator === 'D') key = 'ArrowLeft'
|
|
308
|
-
else if (terminator === 'H') key = 'Home'
|
|
309
|
-
else if (terminator === 'F') key = 'End'
|
|
310
|
-
else if (terminator === 'Z') {
|
|
311
|
-
// Shift+Tab
|
|
312
|
-
key = 'Tab'
|
|
313
|
-
modifiers.shift = true
|
|
314
|
-
}
|
|
315
|
-
// Function keys (~ terminator)
|
|
316
|
-
else if (terminator === '~') {
|
|
317
|
-
const code = parseInt(parts[0]!, 10)
|
|
318
|
-
switch (code) {
|
|
319
|
-
case 1: key = 'Home'; break
|
|
320
|
-
case 2: key = 'Insert'; break
|
|
321
|
-
case 3: key = 'Delete'; break
|
|
322
|
-
case 4: key = 'End'; break
|
|
323
|
-
case 5: key = 'PageUp'; break
|
|
324
|
-
case 6: key = 'PageDown'; break
|
|
325
|
-
case 7: key = 'Home'; break
|
|
326
|
-
case 8: key = 'End'; break
|
|
327
|
-
case 11: key = 'F1'; break
|
|
328
|
-
case 12: key = 'F2'; break
|
|
329
|
-
case 13: key = 'F3'; break
|
|
330
|
-
case 14: key = 'F4'; break
|
|
331
|
-
case 15: key = 'F5'; break
|
|
332
|
-
case 17: key = 'F6'; break
|
|
333
|
-
case 18: key = 'F7'; break
|
|
334
|
-
case 19: key = 'F8'; break
|
|
335
|
-
case 20: key = 'F9'; break
|
|
336
|
-
case 21: key = 'F10'; break
|
|
337
|
-
case 23: key = 'F11'; break
|
|
338
|
-
case 24: key = 'F12'; break
|
|
339
|
-
default: key = `F${code}`; break
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
// Kitty keyboard protocol
|
|
343
|
-
else if (terminator === 'u') {
|
|
344
|
-
return this.parseKittyKey(sequence, consumed)
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
key = `CSI${sequence}${terminator}`
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return {
|
|
351
|
-
event: {
|
|
352
|
-
type: 'key',
|
|
353
|
-
event: { key, modifiers, state: 'press' },
|
|
354
|
-
},
|
|
355
|
-
consumed,
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Parse Kitty keyboard protocol sequence.
|
|
361
|
-
* Format: ESC [ codepoint ; modifiers [: event_type] u
|
|
362
|
-
*/
|
|
363
|
-
private parseKittyKey(sequence: string, consumed: number): { event?: ParsedInput; consumed: number } {
|
|
364
|
-
const parts = sequence.split(';')
|
|
365
|
-
const codepoint = parseInt(parts[0]!, 10)
|
|
366
|
-
const modifiers = this.defaultModifiers()
|
|
367
|
-
let state: KeyState = 'press'
|
|
368
|
-
|
|
369
|
-
if (parts.length >= 2) {
|
|
370
|
-
// Modifiers may include event type after colon
|
|
371
|
-
const modParts = parts[1]!.split(':')
|
|
372
|
-
const modBits = parseInt(modParts[0]!, 10) - 1
|
|
373
|
-
modifiers.shift = (modBits & 1) !== 0
|
|
374
|
-
modifiers.alt = (modBits & 2) !== 0
|
|
375
|
-
modifiers.ctrl = (modBits & 4) !== 0
|
|
376
|
-
modifiers.meta = (modBits & 8) !== 0
|
|
377
|
-
|
|
378
|
-
if (modParts.length >= 2) {
|
|
379
|
-
const eventType = parseInt(modParts[1]!, 10)
|
|
380
|
-
if (eventType === 1) state = 'press'
|
|
381
|
-
else if (eventType === 2) state = 'repeat'
|
|
382
|
-
else if (eventType === 3) state = 'release'
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Map codepoint to key name
|
|
387
|
-
let key: string
|
|
388
|
-
switch (codepoint) {
|
|
389
|
-
case 13: key = 'Enter'; break
|
|
390
|
-
case 9: key = 'Tab'; break
|
|
391
|
-
case 127: key = 'Backspace'; break
|
|
392
|
-
case 27: key = 'Escape'; break
|
|
393
|
-
case 32: key = 'Space'; break
|
|
394
|
-
default:
|
|
395
|
-
key = codepoint >= 32 && codepoint < 127
|
|
396
|
-
? String.fromCodePoint(codepoint)
|
|
397
|
-
: String.fromCodePoint(codepoint)
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
event: {
|
|
402
|
-
type: 'key',
|
|
403
|
-
event: { key, modifiers, state },
|
|
404
|
-
},
|
|
405
|
-
consumed,
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Parse SS3 sequence (ESC O ...).
|
|
411
|
-
*/
|
|
412
|
-
private parseSS3(data: string): { event?: ParsedInput; consumed: number; incomplete?: boolean } {
|
|
413
|
-
if (data.length < 3) {
|
|
414
|
-
return { consumed: 0, incomplete: true }
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const terminator = data[2]
|
|
418
|
-
let key: string
|
|
419
|
-
|
|
420
|
-
switch (terminator) {
|
|
421
|
-
case 'P': key = 'F1'; break
|
|
422
|
-
case 'Q': key = 'F2'; break
|
|
423
|
-
case 'R': key = 'F3'; break
|
|
424
|
-
case 'S': key = 'F4'; break
|
|
425
|
-
case 'H': key = 'Home'; break
|
|
426
|
-
case 'F': key = 'End'; break
|
|
427
|
-
case 'A': key = 'ArrowUp'; break
|
|
428
|
-
case 'B': key = 'ArrowDown'; break
|
|
429
|
-
case 'C': key = 'ArrowRight'; break
|
|
430
|
-
case 'D': key = 'ArrowLeft'; break
|
|
431
|
-
default: key = `SS3${terminator}`; break
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return {
|
|
435
|
-
event: {
|
|
436
|
-
type: 'key',
|
|
437
|
-
event: { key, modifiers: this.defaultModifiers(), state: 'press' },
|
|
438
|
-
},
|
|
439
|
-
consumed: 3,
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Create a simple key event.
|
|
445
|
-
*/
|
|
446
|
-
private simpleKey(key: string): KeyEvent {
|
|
447
|
-
return {
|
|
448
|
-
key,
|
|
449
|
-
modifiers: this.defaultModifiers(),
|
|
450
|
-
state: 'press',
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Create a control key event.
|
|
456
|
-
*/
|
|
457
|
-
private controlKey(code: number): KeyEvent {
|
|
458
|
-
let key: string
|
|
459
|
-
const modifiers = this.defaultModifiers()
|
|
460
|
-
|
|
461
|
-
switch (code) {
|
|
462
|
-
case 0: key = '@'; modifiers.ctrl = true; break
|
|
463
|
-
case 1: key = 'a'; modifiers.ctrl = true; break
|
|
464
|
-
case 2: key = 'b'; modifiers.ctrl = true; break
|
|
465
|
-
case 3: key = 'c'; modifiers.ctrl = true; break
|
|
466
|
-
case 4: key = 'd'; modifiers.ctrl = true; break
|
|
467
|
-
case 5: key = 'e'; modifiers.ctrl = true; break
|
|
468
|
-
case 6: key = 'f'; modifiers.ctrl = true; break
|
|
469
|
-
case 7: key = 'g'; modifiers.ctrl = true; break
|
|
470
|
-
case 8: key = 'Backspace'; break
|
|
471
|
-
case 9: key = 'Tab'; break
|
|
472
|
-
case 10: key = 'Enter'; break
|
|
473
|
-
case 11: key = 'k'; modifiers.ctrl = true; break
|
|
474
|
-
case 12: key = 'l'; modifiers.ctrl = true; break
|
|
475
|
-
case 13: key = 'Enter'; break
|
|
476
|
-
case 14: key = 'n'; modifiers.ctrl = true; break
|
|
477
|
-
case 15: key = 'o'; modifiers.ctrl = true; break
|
|
478
|
-
case 16: key = 'p'; modifiers.ctrl = true; break
|
|
479
|
-
case 17: key = 'q'; modifiers.ctrl = true; break
|
|
480
|
-
case 18: key = 'r'; modifiers.ctrl = true; break
|
|
481
|
-
case 19: key = 's'; modifiers.ctrl = true; break
|
|
482
|
-
case 20: key = 't'; modifiers.ctrl = true; break
|
|
483
|
-
case 21: key = 'u'; modifiers.ctrl = true; break
|
|
484
|
-
case 22: key = 'v'; modifiers.ctrl = true; break
|
|
485
|
-
case 23: key = 'w'; modifiers.ctrl = true; break
|
|
486
|
-
case 24: key = 'x'; modifiers.ctrl = true; break
|
|
487
|
-
case 25: key = 'y'; modifiers.ctrl = true; break
|
|
488
|
-
case 26: key = 'z'; modifiers.ctrl = true; break
|
|
489
|
-
case 27: key = 'Escape'; break
|
|
490
|
-
case 28: key = '\\'; modifiers.ctrl = true; break
|
|
491
|
-
case 29: key = ']'; modifiers.ctrl = true; break
|
|
492
|
-
case 30: key = '^'; modifiers.ctrl = true; break
|
|
493
|
-
case 31: key = '_'; modifiers.ctrl = true; break
|
|
494
|
-
default: key = `Ctrl+${code}`; break
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return { key, modifiers, state: 'press' }
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Default modifiers (all false).
|
|
502
|
-
*/
|
|
503
|
-
private defaultModifiers(): Modifiers {
|
|
504
|
-
return { ctrl: false, alt: false, shift: false, meta: false }
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Clear buffer.
|
|
509
|
-
*/
|
|
510
|
-
clear(): void {
|
|
511
|
-
this.buffer = ''
|
|
512
|
-
if (this.timeout) {
|
|
513
|
-
clearTimeout(this.timeout)
|
|
514
|
-
this.timeout = null
|
|
515
|
-
}
|
|
516
|
-
this.pendingEvents = []
|
|
517
|
-
}
|
|
518
|
-
}
|