@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,140 @@
1
+ /**
2
+ * TUI Framework - Visual Arrays
3
+ *
4
+ * Colors, borders, and visual styling.
5
+ * Colors stored as RGBA objects for alpha blending support.
6
+ *
7
+ * CRITICAL: Use regular arrays (NOT state!) to preserve binding getters.
8
+ * state() proxies snapshot getter values, breaking reactivity.
9
+ *
10
+ * Border styles:
11
+ * 0 = none
12
+ * 1 = single (─ │ ┌ ┐ └ ┘)
13
+ * 2 = double (═ ║ ╔ ╗ ╚ ╝)
14
+ * 3 = rounded (─ │ ╭ ╮ ╰ ╯)
15
+ * 4 = heavy (━ ┃ ┏ ┓ ┗ ┛)
16
+ * 5 = dashed (╌ ╎ ┌ ┐ └ ┘)
17
+ * 6 = dotted (· · · · · ·)
18
+ * 7 = ascii (- | + + + +)
19
+ * 8 = block (█ █ █ █ █ █)
20
+ * 9 = mixedDoubleH (═ │ ╒ ╕ ╘ ╛)
21
+ * 10 = mixedDoubleV (─ ║ ╓ ╖ ╙ ╜)
22
+ *
23
+ * Per-side borders can have independent styles.
24
+ */
25
+
26
+ import { bind, disconnectBinding, type Binding } from '@rlabs-inc/signals'
27
+ import type { RGBA } from '../../types'
28
+
29
+ // =============================================================================
30
+ // COLORS - Regular arrays to preserve binding reactivity
31
+ // =============================================================================
32
+
33
+ /** Foreground color (text) - null means inherit from parent */
34
+ export const fgColor: Binding<RGBA | null>[] = []
35
+
36
+ /** Background color - null means transparent/inherit */
37
+ export const bgColor: Binding<RGBA | null>[] = []
38
+
39
+ /** Opacity 0-1 (1 = fully opaque) */
40
+ export const opacity: Binding<number>[] = []
41
+
42
+ // =============================================================================
43
+ // BORDERS - Per-side independent styles
44
+ // =============================================================================
45
+
46
+ /** Default border style for all sides (0-10) */
47
+ export const borderStyle: Binding<number>[] = []
48
+
49
+ /** Default border color for all sides - null means use foreground */
50
+ export const borderColor: Binding<RGBA | null>[] = []
51
+
52
+ /** Top border style (0=none or inherit from borderStyle, 1-10=specific style) */
53
+ export const borderTop: Binding<number>[] = []
54
+
55
+ /** Right border style */
56
+ export const borderRight: Binding<number>[] = []
57
+
58
+ /** Bottom border style */
59
+ export const borderBottom: Binding<number>[] = []
60
+
61
+ /** Left border style */
62
+ export const borderLeft: Binding<number>[] = []
63
+
64
+ /** Per-side border colors - null means use borderColor or foreground */
65
+ export const borderColorTop: Binding<RGBA | null>[] = []
66
+ export const borderColorRight: Binding<RGBA | null>[] = []
67
+ export const borderColorBottom: Binding<RGBA | null>[] = []
68
+ export const borderColorLeft: Binding<RGBA | null>[] = []
69
+
70
+ // =============================================================================
71
+ // FOCUS RING
72
+ // =============================================================================
73
+
74
+ /** Show focus ring when focused (1=yes, 0=no) */
75
+ export const showFocusRing: Binding<number>[] = []
76
+
77
+ /** Focus ring color */
78
+ export const focusRingColor: Binding<RGBA>[] = []
79
+
80
+ // =============================================================================
81
+ // DEFAULT VALUES
82
+ // =============================================================================
83
+
84
+ const DEFAULT_FOCUS_COLOR: RGBA = { r: 100, g: 149, b: 237, a: 255 } // cornflowerblue
85
+
86
+ /** LAZY BINDING: Push undefined, primitives create bindings for used props only */
87
+ export function ensureCapacity(index: number): void {
88
+ while (fgColor.length <= index) {
89
+ fgColor.push(undefined as any)
90
+ bgColor.push(undefined as any)
91
+ opacity.push(undefined as any)
92
+ borderStyle.push(undefined as any)
93
+ borderColor.push(undefined as any)
94
+ borderTop.push(undefined as any)
95
+ borderRight.push(undefined as any)
96
+ borderBottom.push(undefined as any)
97
+ borderLeft.push(undefined as any)
98
+ borderColorTop.push(undefined as any)
99
+ borderColorRight.push(undefined as any)
100
+ borderColorBottom.push(undefined as any)
101
+ borderColorLeft.push(undefined as any)
102
+ showFocusRing.push(undefined as any)
103
+ focusRingColor.push(undefined as any)
104
+ }
105
+ }
106
+
107
+ export function clearAtIndex(index: number): void {
108
+ if (index < fgColor.length) {
109
+ disconnectBinding(fgColor[index])
110
+ disconnectBinding(bgColor[index])
111
+ disconnectBinding(opacity[index])
112
+ disconnectBinding(borderStyle[index])
113
+ disconnectBinding(borderColor[index])
114
+ disconnectBinding(borderTop[index])
115
+ disconnectBinding(borderRight[index])
116
+ disconnectBinding(borderBottom[index])
117
+ disconnectBinding(borderLeft[index])
118
+ disconnectBinding(borderColorTop[index])
119
+ disconnectBinding(borderColorRight[index])
120
+ disconnectBinding(borderColorBottom[index])
121
+ disconnectBinding(borderColorLeft[index])
122
+ disconnectBinding(showFocusRing[index])
123
+ disconnectBinding(focusRingColor[index])
124
+ fgColor[index] = undefined as any
125
+ bgColor[index] = undefined as any
126
+ opacity[index] = undefined as any
127
+ borderStyle[index] = undefined as any
128
+ borderColor[index] = undefined as any
129
+ borderTop[index] = undefined as any
130
+ borderRight[index] = undefined as any
131
+ borderBottom[index] = undefined as any
132
+ borderLeft[index] = undefined as any
133
+ borderColorTop[index] = undefined as any
134
+ borderColorRight[index] = undefined as any
135
+ borderColorBottom[index] = undefined as any
136
+ borderColorLeft[index] = undefined as any
137
+ showFocusRing[index] = undefined as any
138
+ focusRingColor[index] = undefined as any
139
+ }
140
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * TUI Framework - Engine
3
+ *
4
+ * The component engine using parallel arrays pattern.
5
+ * Components allocate indices and write to arrays.
6
+ * Deriveds read arrays and RETURN computed values.
7
+ */
8
+
9
+ // Registry
10
+ export {
11
+ allocateIndex,
12
+ releaseIndex,
13
+ getIndex,
14
+ getId,
15
+ getAllocatedIndices,
16
+ isAllocated,
17
+ getAllocatedCount,
18
+ getCurrentParentIndex,
19
+ pushParentContext,
20
+ popParentContext,
21
+ resetRegistry,
22
+ } from './registry'
23
+
24
+ // Parallel arrays
25
+ export * as arrays from './arrays'
@@ -0,0 +1,138 @@
1
+ /**
2
+ * TUI Framework - Color and Style Inheritance
3
+ *
4
+ * Utilities for walking up the component tree to inherit colors and styles.
5
+ * Used when a component has null colors (meaning "inherit from parent").
6
+ */
7
+
8
+ import { unwrap } from '@rlabs-inc/signals'
9
+ import type { RGBA } from '../types'
10
+ import { Colors, TERMINAL_DEFAULT } from '../types/color'
11
+ import * as core from './arrays/core'
12
+ import * as visual from './arrays/visual'
13
+
14
+ /**
15
+ * Get inherited foreground color by walking up the parent tree.
16
+ * Returns TERMINAL_DEFAULT if no explicit color is found.
17
+ */
18
+ export function getInheritedFg(index: number): RGBA {
19
+ let current: number = index
20
+
21
+ while (current >= 0) {
22
+ const fg = unwrap(visual.fgColor[current])
23
+ if (fg !== null && fg !== undefined) return fg
24
+ const parent = unwrap(core.parentIndex[current])
25
+ if (parent === undefined || parent < 0) break
26
+ current = parent
27
+ }
28
+
29
+ return TERMINAL_DEFAULT
30
+ }
31
+
32
+ /**
33
+ * Get inherited background color by walking up the parent tree.
34
+ * Returns TERMINAL_DEFAULT if no explicit color is found.
35
+ */
36
+ export function getInheritedBg(index: number): RGBA {
37
+ let current: number = index
38
+
39
+ while (current >= 0) {
40
+ const bg = unwrap(visual.bgColor[current])
41
+ if (bg !== null && bg !== undefined) return bg
42
+ const parent = unwrap(core.parentIndex[current])
43
+ if (parent === undefined || parent < 0) break
44
+ current = parent
45
+ }
46
+
47
+ return TERMINAL_DEFAULT
48
+ }
49
+
50
+ /**
51
+ * Get inherited border color for a specific side.
52
+ * Falls back to unified border color, then foreground color.
53
+ */
54
+ export function getInheritedBorderColor(index: number, side: 'top' | 'right' | 'bottom' | 'left'): RGBA {
55
+ const colorArray = {
56
+ top: visual.borderColorTop,
57
+ right: visual.borderColorRight,
58
+ bottom: visual.borderColorBottom,
59
+ left: visual.borderColorLeft,
60
+ }[side]
61
+
62
+ const color = unwrap(colorArray[index])
63
+ if (color !== null && color !== undefined) return color
64
+
65
+ // Try unified border color
66
+ const unifiedColor = unwrap(visual.borderColor[index])
67
+ if (unifiedColor !== null && unifiedColor !== undefined) return unifiedColor
68
+
69
+ // Fall back to foreground color
70
+ return getInheritedFg(index)
71
+ }
72
+
73
+ /**
74
+ * Get all four border colors for a component.
75
+ */
76
+ export function getBorderColors(index: number): {
77
+ top: RGBA
78
+ right: RGBA
79
+ bottom: RGBA
80
+ left: RGBA
81
+ } {
82
+ const fg = getInheritedFg(index)
83
+ const unified = unwrap(visual.borderColor[index])
84
+ const fallback = unified ?? fg
85
+
86
+ return {
87
+ top: unwrap(visual.borderColorTop[index]) ?? fallback,
88
+ right: unwrap(visual.borderColorRight[index]) ?? fallback,
89
+ bottom: unwrap(visual.borderColorBottom[index]) ?? fallback,
90
+ left: unwrap(visual.borderColorLeft[index]) ?? fallback,
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get all four border styles for a component.
96
+ * Falls back to the unified borderStyle if per-side not set.
97
+ */
98
+ export function getBorderStyles(index: number): {
99
+ top: number
100
+ right: number
101
+ bottom: number
102
+ left: number
103
+ } {
104
+ const unified = unwrap(visual.borderStyle[index]) || 0
105
+
106
+ return {
107
+ top: unwrap(visual.borderTop[index]) || unified,
108
+ right: unwrap(visual.borderRight[index]) || unified,
109
+ bottom: unwrap(visual.borderBottom[index]) || unified,
110
+ left: unwrap(visual.borderLeft[index]) || unified,
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Check if a component has any border.
116
+ */
117
+ export function hasBorder(index: number): boolean {
118
+ const styles = getBorderStyles(index)
119
+ return styles.top > 0 || styles.right > 0 || styles.bottom > 0 || styles.left > 0
120
+ }
121
+
122
+ /**
123
+ * Get effective opacity by multiplying down the parent chain.
124
+ */
125
+ export function getEffectiveOpacity(index: number): number {
126
+ let opacity = 1
127
+ let current: number | undefined = index
128
+
129
+ while (current !== undefined && current >= 0) {
130
+ const nodeOpacity = unwrap(visual.opacity[current])
131
+ if (nodeOpacity !== undefined && nodeOpacity !== 1) {
132
+ opacity *= nodeOpacity
133
+ }
134
+ current = unwrap(core.parentIndex[current])
135
+ }
136
+
137
+ return opacity
138
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * TUI Framework - Component Registry
3
+ *
4
+ * Manages index allocation for the parallel arrays pattern.
5
+ * Each component gets a unique index, which is used across all arrays.
6
+ *
7
+ * Features:
8
+ * - ID ↔ Index bidirectional mapping
9
+ * - Free index pool for O(1) reuse
10
+ * - ReactiveSet for allocatedIndices (deriveds react to add/remove)
11
+ */
12
+
13
+ import { ReactiveSet } from '@rlabs-inc/signals'
14
+ import { ensureAllCapacity, clearAllAtIndex, resetAllArrays } from './arrays'
15
+ import { resetTitanArrays } from '../pipeline/layout/titan-engine'
16
+
17
+ // =============================================================================
18
+ // Registry State
19
+ // =============================================================================
20
+
21
+ /** Map component ID to array index */
22
+ const idToIndex = new Map<string, number>()
23
+
24
+ /** Map array index to component ID */
25
+ const indexToId = new Map<number, string>()
26
+
27
+ /**
28
+ * Set of currently allocated indices (for iteration).
29
+ *
30
+ * Using ReactiveSet so deriveds that iterate over this set
31
+ * automatically react when components are added or removed.
32
+ */
33
+ const allocatedIndices = new ReactiveSet<number>()
34
+
35
+ /** Pool of freed indices for reuse */
36
+ const freeIndices: number[] = []
37
+
38
+ /** Next index to allocate if pool is empty */
39
+ let nextIndex = 0
40
+
41
+ /** Counter for generating unique IDs */
42
+ let idCounter = 0
43
+
44
+ // =============================================================================
45
+ // Parent Context Stack
46
+ // =============================================================================
47
+
48
+ /** Stack of parent indices for nested component creation */
49
+ const parentStack: number[] = []
50
+
51
+ /** Get current parent index (-1 if at root) */
52
+ export function getCurrentParentIndex(): number {
53
+ return parentStack.length > 0 ? (parentStack[parentStack.length - 1] ?? -1) : -1
54
+ }
55
+
56
+ /** Push a parent index onto the stack */
57
+ export function pushParentContext(index: number): void {
58
+ parentStack.push(index)
59
+ }
60
+
61
+ /** Pop a parent index from the stack */
62
+ export function popParentContext(): void {
63
+ parentStack.pop()
64
+ }
65
+
66
+ // =============================================================================
67
+ // Index Allocation
68
+ // =============================================================================
69
+
70
+ /**
71
+ * Allocate an index for a new component.
72
+ *
73
+ * @param id - Optional component ID. If not provided, one is generated.
74
+ * @returns The allocated index.
75
+ */
76
+ export function allocateIndex(id?: string): number {
77
+ // Generate ID if not provided
78
+ const componentId = id ?? `c${idCounter++}`
79
+
80
+ // Check if already allocated
81
+ const existing = idToIndex.get(componentId)
82
+ if (existing !== undefined) {
83
+ return existing
84
+ }
85
+
86
+ // Reuse free index or allocate new
87
+ const index = freeIndices.length > 0
88
+ ? freeIndices.pop()!
89
+ : nextIndex++
90
+
91
+ // Register mappings
92
+ idToIndex.set(componentId, index)
93
+ indexToId.set(index, componentId)
94
+ allocatedIndices.add(index)
95
+
96
+ // Ensure arrays have capacity for this index
97
+ ensureAllCapacity(index)
98
+
99
+ return index
100
+ }
101
+
102
+ /**
103
+ * Release an index back to the pool.
104
+ *
105
+ * @param index - The index to release.
106
+ */
107
+ export function releaseIndex(index: number): void {
108
+ const id = indexToId.get(index)
109
+ if (id === undefined) return
110
+
111
+ // Clean up mappings
112
+ idToIndex.delete(id)
113
+ indexToId.delete(index)
114
+ allocatedIndices.delete(index)
115
+
116
+ // Clear all array values at this index
117
+ clearAllAtIndex(index)
118
+
119
+ // Return to pool for reuse
120
+ freeIndices.push(index)
121
+
122
+ // AUTO-CLEANUP: When all components destroyed, reset all arrays to free memory
123
+ if (allocatedIndices.size === 0) {
124
+ resetAllArrays()
125
+ resetTitanArrays()
126
+ freeIndices.length = 0
127
+ nextIndex = 0
128
+ // Note: GC is NOT forced here to avoid performance hit during rapid create/destroy cycles
129
+ // The FinalizationRegistry will clean up automatically when GC runs naturally
130
+ }
131
+ }
132
+
133
+ // =============================================================================
134
+ // Lookups
135
+ // =============================================================================
136
+
137
+ /** Get index for a component ID */
138
+ export function getIndex(id: string): number | undefined {
139
+ return idToIndex.get(id)
140
+ }
141
+
142
+ /** Get ID for an index */
143
+ export function getId(index: number): string | undefined {
144
+ return indexToId.get(index)
145
+ }
146
+
147
+ /** Get all currently allocated indices */
148
+ export function getAllocatedIndices(): Set<number> {
149
+ return allocatedIndices
150
+ }
151
+
152
+ /** Check if an index is currently allocated */
153
+ export function isAllocated(index: number): boolean {
154
+ return allocatedIndices.has(index)
155
+ }
156
+
157
+ /** Get the current capacity (highest index that would be allocated next) */
158
+ export function getCapacity(): number {
159
+ return nextIndex
160
+ }
161
+
162
+ /** Get the count of currently allocated components */
163
+ export function getAllocatedCount(): number {
164
+ return allocatedIndices.size
165
+ }
166
+
167
+ // =============================================================================
168
+ // Reset (for testing)
169
+ // =============================================================================
170
+
171
+ /** Reset all registry state (for testing) */
172
+ export function resetRegistry(): void {
173
+ idToIndex.clear()
174
+ indexToId.clear()
175
+ allocatedIndices.clear()
176
+ freeIndices.length = 0
177
+ nextIndex = 0
178
+ idCounter = 0
179
+ parentStack.length = 0
180
+ }