@rlabs-inc/tui 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 (44) hide show
  1. package/README.md +141 -0
  2. package/index.ts +45 -0
  3. package/package.json +59 -0
  4. package/src/api/index.ts +7 -0
  5. package/src/api/mount.ts +230 -0
  6. package/src/engine/arrays/core.ts +60 -0
  7. package/src/engine/arrays/dimensions.ts +68 -0
  8. package/src/engine/arrays/index.ts +166 -0
  9. package/src/engine/arrays/interaction.ts +112 -0
  10. package/src/engine/arrays/layout.ts +175 -0
  11. package/src/engine/arrays/spacing.ts +100 -0
  12. package/src/engine/arrays/text.ts +55 -0
  13. package/src/engine/arrays/visual.ts +140 -0
  14. package/src/engine/index.ts +25 -0
  15. package/src/engine/inheritance.ts +138 -0
  16. package/src/engine/registry.ts +180 -0
  17. package/src/pipeline/frameBuffer.ts +473 -0
  18. package/src/pipeline/layout/index.ts +105 -0
  19. package/src/pipeline/layout/titan-engine.ts +798 -0
  20. package/src/pipeline/layout/types.ts +194 -0
  21. package/src/pipeline/layout/utils/hierarchy.ts +202 -0
  22. package/src/pipeline/layout/utils/math.ts +134 -0
  23. package/src/pipeline/layout/utils/text-measure.ts +160 -0
  24. package/src/pipeline/layout.ts +30 -0
  25. package/src/primitives/box.ts +312 -0
  26. package/src/primitives/index.ts +12 -0
  27. package/src/primitives/text.ts +199 -0
  28. package/src/primitives/types.ts +222 -0
  29. package/src/primitives/utils.ts +37 -0
  30. package/src/renderer/ansi.ts +625 -0
  31. package/src/renderer/buffer.ts +667 -0
  32. package/src/renderer/index.ts +40 -0
  33. package/src/renderer/input.ts +518 -0
  34. package/src/renderer/output.ts +451 -0
  35. package/src/state/cursor.ts +176 -0
  36. package/src/state/focus.ts +241 -0
  37. package/src/state/index.ts +43 -0
  38. package/src/state/keyboard.ts +771 -0
  39. package/src/state/mouse.ts +524 -0
  40. package/src/state/scroll.ts +341 -0
  41. package/src/state/theme.ts +687 -0
  42. package/src/types/color.ts +401 -0
  43. package/src/types/index.ts +316 -0
  44. package/src/utils/text.ts +471 -0
@@ -0,0 +1,518 @@
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
+ }