@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.
- package/README.md +141 -0
- package/index.ts +45 -0
- package/package.json +59 -0
- package/src/api/index.ts +7 -0
- package/src/api/mount.ts +230 -0
- package/src/engine/arrays/core.ts +60 -0
- package/src/engine/arrays/dimensions.ts +68 -0
- package/src/engine/arrays/index.ts +166 -0
- package/src/engine/arrays/interaction.ts +112 -0
- package/src/engine/arrays/layout.ts +175 -0
- package/src/engine/arrays/spacing.ts +100 -0
- package/src/engine/arrays/text.ts +55 -0
- package/src/engine/arrays/visual.ts +140 -0
- package/src/engine/index.ts +25 -0
- package/src/engine/inheritance.ts +138 -0
- package/src/engine/registry.ts +180 -0
- package/src/pipeline/frameBuffer.ts +473 -0
- package/src/pipeline/layout/index.ts +105 -0
- package/src/pipeline/layout/titan-engine.ts +798 -0
- package/src/pipeline/layout/types.ts +194 -0
- package/src/pipeline/layout/utils/hierarchy.ts +202 -0
- package/src/pipeline/layout/utils/math.ts +134 -0
- package/src/pipeline/layout/utils/text-measure.ts +160 -0
- package/src/pipeline/layout.ts +30 -0
- package/src/primitives/box.ts +312 -0
- package/src/primitives/index.ts +12 -0
- package/src/primitives/text.ts +199 -0
- package/src/primitives/types.ts +222 -0
- package/src/primitives/utils.ts +37 -0
- package/src/renderer/ansi.ts +625 -0
- package/src/renderer/buffer.ts +667 -0
- package/src/renderer/index.ts +40 -0
- package/src/renderer/input.ts +518 -0
- package/src/renderer/output.ts +451 -0
- package/src/state/cursor.ts +176 -0
- package/src/state/focus.ts +241 -0
- package/src/state/index.ts +43 -0
- package/src/state/keyboard.ts +771 -0
- package/src/state/mouse.ts +524 -0
- package/src/state/scroll.ts +341 -0
- package/src/state/theme.ts +687 -0
- package/src/types/color.ts +401 -0
- package/src/types/index.ts +316 -0
- package/src/utils/text.ts +471 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - Focus State Module
|
|
3
|
+
*
|
|
4
|
+
* Manages focus state and navigation:
|
|
5
|
+
* - focusedIndex signal (currently focused component)
|
|
6
|
+
* - Focus cycling (Tab/Shift+Tab)
|
|
7
|
+
* - Focus trapping for modals
|
|
8
|
+
* - Focus history for restoration
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { signal, derived, unwrap } from '@rlabs-inc/signals'
|
|
12
|
+
import { focusable, tabIndex, focusedIndex as _focusedIndex } from '../engine/arrays/interaction'
|
|
13
|
+
import { visible } from '../engine/arrays/core'
|
|
14
|
+
import { getAllocatedIndices } from '../engine/registry'
|
|
15
|
+
|
|
16
|
+
// Re-export the focusedIndex from interaction arrays
|
|
17
|
+
export const focusedIndex = _focusedIndex
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// FOCUS TRAP (for modals/dialogs)
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
const focusTrapStack: number[] = []
|
|
24
|
+
|
|
25
|
+
/** Push a focus trap - focus will be contained within this component's children */
|
|
26
|
+
export function pushFocusTrap(containerIndex: number): void {
|
|
27
|
+
focusTrapStack.push(containerIndex)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Pop the current focus trap */
|
|
31
|
+
export function popFocusTrap(): number | undefined {
|
|
32
|
+
return focusTrapStack.pop()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Check if focus is currently trapped */
|
|
36
|
+
export function isFocusTrapped(): boolean {
|
|
37
|
+
return focusTrapStack.length > 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Get the current focus trap container */
|
|
41
|
+
export function getFocusTrapContainer(): number | undefined {
|
|
42
|
+
return focusTrapStack[focusTrapStack.length - 1]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// FOCUS HISTORY (for restoration)
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
const focusHistory: number[] = []
|
|
50
|
+
const MAX_HISTORY = 10
|
|
51
|
+
|
|
52
|
+
/** Save current focus to history */
|
|
53
|
+
export function saveFocusToHistory(): void {
|
|
54
|
+
const current = focusedIndex.value
|
|
55
|
+
if (current >= 0) {
|
|
56
|
+
focusHistory.push(current)
|
|
57
|
+
if (focusHistory.length > MAX_HISTORY) {
|
|
58
|
+
focusHistory.shift()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Restore focus from history */
|
|
64
|
+
export function restoreFocusFromHistory(): boolean {
|
|
65
|
+
while (focusHistory.length > 0) {
|
|
66
|
+
const index = focusHistory.pop()!
|
|
67
|
+
// Check if component is still valid and focusable
|
|
68
|
+
if (unwrap(focusable[index]) && unwrap(visible[index])) {
|
|
69
|
+
focusedIndex.value = index
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// FOCUSABLE QUERIES
|
|
78
|
+
// =============================================================================
|
|
79
|
+
|
|
80
|
+
/** Get all focusable component indices, sorted by tabIndex */
|
|
81
|
+
export function getFocusableIndices(): number[] {
|
|
82
|
+
const indices = getAllocatedIndices()
|
|
83
|
+
const result: number[] = []
|
|
84
|
+
|
|
85
|
+
for (const i of indices) {
|
|
86
|
+
if (unwrap(focusable[i]) && unwrap(visible[i])) {
|
|
87
|
+
result.push(i)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Sort by tabIndex (components with same tabIndex keep allocation order)
|
|
92
|
+
result.sort((a, b) => {
|
|
93
|
+
const tabA = unwrap(tabIndex[a]) ?? 0
|
|
94
|
+
const tabB = unwrap(tabIndex[b]) ?? 0
|
|
95
|
+
if (tabA !== tabB) return tabA - tabB
|
|
96
|
+
return a - b // Stable sort by index
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return result
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Derived: all focusable indices */
|
|
103
|
+
export const focusableIndices = derived(getFocusableIndices)
|
|
104
|
+
|
|
105
|
+
/** Derived: is any component focused */
|
|
106
|
+
export const hasFocus = derived(() => focusedIndex.value >= 0)
|
|
107
|
+
|
|
108
|
+
/** Derived: is specific component focused */
|
|
109
|
+
export function isFocused(index: number): boolean {
|
|
110
|
+
return focusedIndex.value === index
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// =============================================================================
|
|
114
|
+
// FOCUS NAVIGATION
|
|
115
|
+
// =============================================================================
|
|
116
|
+
|
|
117
|
+
/** Find next focusable component */
|
|
118
|
+
function findNextFocusable(fromIndex: number, direction: 1 | -1): number {
|
|
119
|
+
let focusables = getFocusableIndices()
|
|
120
|
+
|
|
121
|
+
// Apply focus trap if active
|
|
122
|
+
if (isFocusTrapped()) {
|
|
123
|
+
const trapContainer = getFocusTrapContainer()
|
|
124
|
+
// In a real implementation, we'd filter to children of trapContainer
|
|
125
|
+
// For now, this is a placeholder
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (focusables.length === 0) return -1
|
|
129
|
+
|
|
130
|
+
const currentPos = focusables.indexOf(fromIndex)
|
|
131
|
+
|
|
132
|
+
if (currentPos === -1) {
|
|
133
|
+
// Not currently focused on a focusable
|
|
134
|
+
return direction === 1 ? focusables[0]! : focusables[focusables.length - 1]!
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Move in direction with wrap
|
|
138
|
+
const nextPos = (currentPos + direction + focusables.length) % focusables.length
|
|
139
|
+
return focusables[nextPos]!
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Move focus to next focusable component */
|
|
143
|
+
export function focusNext(): boolean {
|
|
144
|
+
const next = findNextFocusable(focusedIndex.value, 1)
|
|
145
|
+
if (next !== -1 && next !== focusedIndex.value) {
|
|
146
|
+
saveFocusToHistory()
|
|
147
|
+
focusedIndex.value = next
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Move focus to previous focusable component */
|
|
154
|
+
export function focusPrevious(): boolean {
|
|
155
|
+
const prev = findNextFocusable(focusedIndex.value, -1)
|
|
156
|
+
if (prev !== -1 && prev !== focusedIndex.value) {
|
|
157
|
+
saveFocusToHistory()
|
|
158
|
+
focusedIndex.value = prev
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Focus a specific component by index */
|
|
165
|
+
export function focus(index: number): boolean {
|
|
166
|
+
if (unwrap(focusable[index]) && unwrap(visible[index])) {
|
|
167
|
+
if (focusedIndex.value !== index) {
|
|
168
|
+
saveFocusToHistory()
|
|
169
|
+
focusedIndex.value = index
|
|
170
|
+
}
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Clear focus (no component focused) */
|
|
177
|
+
export function blur(): void {
|
|
178
|
+
if (focusedIndex.value >= 0) {
|
|
179
|
+
saveFocusToHistory()
|
|
180
|
+
focusedIndex.value = -1
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Focus the first focusable component */
|
|
185
|
+
export function focusFirst(): boolean {
|
|
186
|
+
const focusables = getFocusableIndices()
|
|
187
|
+
if (focusables.length > 0) {
|
|
188
|
+
return focus(focusables[0]!)
|
|
189
|
+
}
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Focus the last focusable component */
|
|
194
|
+
export function focusLast(): boolean {
|
|
195
|
+
const focusables = getFocusableIndices()
|
|
196
|
+
if (focusables.length > 0) {
|
|
197
|
+
return focus(focusables[focusables.length - 1]!)
|
|
198
|
+
}
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// =============================================================================
|
|
203
|
+
// RESET (for testing)
|
|
204
|
+
// =============================================================================
|
|
205
|
+
|
|
206
|
+
/** Reset all focus state (for testing) */
|
|
207
|
+
export function resetFocusState(): void {
|
|
208
|
+
focusedIndex.value = -1
|
|
209
|
+
focusTrapStack.length = 0
|
|
210
|
+
focusHistory.length = 0
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// =============================================================================
|
|
214
|
+
// EXPORT
|
|
215
|
+
// =============================================================================
|
|
216
|
+
|
|
217
|
+
export const focusManager = {
|
|
218
|
+
// State
|
|
219
|
+
get focusedIndex() { return focusedIndex.value },
|
|
220
|
+
get hasFocus() { return hasFocus.value },
|
|
221
|
+
get focusableIndices() { return focusableIndices.value },
|
|
222
|
+
isFocused,
|
|
223
|
+
|
|
224
|
+
// Navigation
|
|
225
|
+
focusNext,
|
|
226
|
+
focusPrevious,
|
|
227
|
+
focus,
|
|
228
|
+
blur,
|
|
229
|
+
focusFirst,
|
|
230
|
+
focusLast,
|
|
231
|
+
|
|
232
|
+
// Focus trap
|
|
233
|
+
pushFocusTrap,
|
|
234
|
+
popFocusTrap,
|
|
235
|
+
isFocusTrapped,
|
|
236
|
+
getFocusTrapContainer,
|
|
237
|
+
|
|
238
|
+
// History
|
|
239
|
+
saveFocusToHistory,
|
|
240
|
+
restoreFocusFromHistory,
|
|
241
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - State Modules
|
|
3
|
+
*
|
|
4
|
+
* Input handling and interaction state management.
|
|
5
|
+
*
|
|
6
|
+
* Modules:
|
|
7
|
+
* - keyboard: Key events, focus navigation, Kitty protocol
|
|
8
|
+
* - mouse: Mouse events, HitGrid, SGR protocol
|
|
9
|
+
* - focus: Focus management, trapping, history
|
|
10
|
+
* - scroll: Scroll state, wheel/keyboard handling
|
|
11
|
+
* - cursor: Cursor visibility, shape, position
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Keyboard is primary for focus navigation
|
|
15
|
+
export * from './keyboard'
|
|
16
|
+
export * from './mouse'
|
|
17
|
+
// Focus exports - exclude duplicates that are in keyboard
|
|
18
|
+
export {
|
|
19
|
+
focusedIndex,
|
|
20
|
+
pushFocusTrap,
|
|
21
|
+
popFocusTrap,
|
|
22
|
+
isFocusTrapped,
|
|
23
|
+
getFocusTrapContainer,
|
|
24
|
+
saveFocusToHistory,
|
|
25
|
+
restoreFocusFromHistory,
|
|
26
|
+
getFocusableIndices,
|
|
27
|
+
focusableIndices,
|
|
28
|
+
hasFocus,
|
|
29
|
+
isFocused,
|
|
30
|
+
focus,
|
|
31
|
+
blur,
|
|
32
|
+
focusFirst,
|
|
33
|
+
focusLast,
|
|
34
|
+
focusManager,
|
|
35
|
+
} from './focus'
|
|
36
|
+
export * from './scroll'
|
|
37
|
+
export * from './cursor'
|
|
38
|
+
|
|
39
|
+
// Convenient namespace exports
|
|
40
|
+
export { keyboard } from './keyboard'
|
|
41
|
+
export { mouse } from './mouse'
|
|
42
|
+
export { scroll } from './scroll'
|
|
43
|
+
export { cursor } from './cursor'
|