@rlabs-inc/tui 0.8.1 → 0.8.2
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 +20 -256
- package/package.json +1 -1
- package/src/api/mount.ts +6 -2
- package/src/pipeline/frameBuffer.ts +20 -0
package/README.md
CHANGED
|
@@ -1,279 +1,43 @@
|
|
|
1
|
-
# TUI
|
|
1
|
+
# TUI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A reactive terminal UI framework for TypeScript and Bun.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Performance
|
|
8
|
-
|
|
9
|
-
| Metric | Value |
|
|
10
|
-
|--------|-------|
|
|
11
|
-
| Single update latency | 0.028ms |
|
|
12
|
-
| Updates per second | 41,000+ |
|
|
13
|
-
| Layout (10K components) | 0.66ms |
|
|
14
|
-
| Memory per component | ~500 bytes |
|
|
15
|
-
|
|
16
|
-
## Quick Start
|
|
5
|
+
## Install
|
|
17
6
|
|
|
18
7
|
```bash
|
|
19
8
|
bun add @rlabs-inc/tui
|
|
20
9
|
```
|
|
21
10
|
|
|
22
|
-
|
|
23
|
-
import { mount, box, text, signal, derived } from '@rlabs-inc/tui'
|
|
24
|
-
|
|
25
|
-
// Create reactive state
|
|
26
|
-
const count = signal(0)
|
|
27
|
-
const doubled = derived(() => count.value * 2)
|
|
28
|
-
|
|
29
|
-
// Mount to terminal - build UI inside mount()
|
|
30
|
-
mount(() => {
|
|
31
|
-
box({ width: 40, height: 10, children: () => {
|
|
32
|
-
text({ content: count }) // signal directly
|
|
33
|
-
text({ content: doubled }) // derived directly
|
|
34
|
-
text({ content: () => `Count: ${count.value}` }) // () => for formatting
|
|
35
|
-
}})
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
// Update anywhere - UI reacts automatically
|
|
39
|
-
setInterval(() => count.value++, 1000)
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Features
|
|
43
|
-
|
|
44
|
-
### Complete Flexbox Layout (TITAN Engine)
|
|
45
|
-
- Direction: row, column, row-reverse, column-reverse
|
|
46
|
-
- Wrap: nowrap, wrap, wrap-reverse
|
|
47
|
-
- Justify: flex-start, center, flex-end, space-between, space-around, space-evenly
|
|
48
|
-
- Align: stretch, flex-start, center, flex-end
|
|
49
|
-
- Grow, shrink, basis, gap
|
|
50
|
-
- Min/max constraints
|
|
51
|
-
- Percentage dimensions
|
|
52
|
-
|
|
53
|
-
### Fine-Grained Reactivity
|
|
54
|
-
- Signals for primitive values
|
|
55
|
-
- Derived for computed values
|
|
56
|
-
- Effects for side effects
|
|
57
|
-
- Bindings for prop connections
|
|
58
|
-
- Zero reconciliation - reactivity IS the update mechanism
|
|
59
|
-
- **Clean syntax**: Pass signals/deriveds directly, use `() =>` only for inline computations
|
|
60
|
-
|
|
61
|
-
### State Modules
|
|
62
|
-
- **keyboard** - Key events, shortcuts, input buffering
|
|
63
|
-
- **mouse** - Click, hover, drag, wheel events
|
|
64
|
-
- **focus** - Tab navigation, focus trapping
|
|
65
|
-
- **scroll** - Scroll state and navigation
|
|
66
|
-
- **theme** - 14 color variants, theming
|
|
67
|
-
|
|
68
|
-
## Architecture
|
|
69
|
-
|
|
70
|
-
```
|
|
71
|
-
User Signals → bind() → Parallel Arrays → layoutDerived → frameBufferDerived → render
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### Why So Fast?
|
|
75
|
-
|
|
76
|
-
1. **Parallel Arrays** - ECS-like data layout for cache efficiency
|
|
77
|
-
2. **No Reconciliation** - Fine-grained reactivity replaces diffing
|
|
78
|
-
3. **Single Effect** - One render effect for entire app
|
|
79
|
-
4. **TITAN Layout** - O(n) flexbox in pure TypeScript
|
|
80
|
-
5. **Zero CPU Idle** - Only updates when signals change
|
|
81
|
-
|
|
82
|
-
## Examples
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
# Run examples
|
|
86
|
-
bun run examples/showcase/01-hello-counter.ts
|
|
87
|
-
bun run examples/showcase/08-todo-app.ts
|
|
88
|
-
bun run examples/showcase/04-dashboard.ts
|
|
89
|
-
|
|
90
|
-
# Run visual tests
|
|
91
|
-
bun run examples/tests/03-layout-flex.ts
|
|
92
|
-
bun run examples/tests/05-flex-complete.ts
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Documentation
|
|
96
|
-
|
|
97
|
-
### Getting Started
|
|
98
|
-
- [Installation](./docs/getting-started/installation.md) - Requirements and setup
|
|
99
|
-
- [Quick Start](./docs/getting-started/quick-start.md) - Hello world in 5 minutes
|
|
100
|
-
- [Core Concepts](./docs/getting-started/concepts.md) - Understand the fundamentals
|
|
101
|
-
- [First App Tutorial](./docs/getting-started/first-app.md) - Build a complete app
|
|
102
|
-
|
|
103
|
-
### User Guides
|
|
104
|
-
- [Primitives](./docs/guides/primitives/box.md) - box, text, each, show, when
|
|
105
|
-
- [Layout](./docs/guides/layout/flexbox.md) - Flexbox layout system
|
|
106
|
-
- [Styling](./docs/guides/styling/colors.md) - Colors, themes, borders
|
|
107
|
-
- [Reactivity](./docs/guides/reactivity/signals.md) - Signals and reactivity
|
|
108
|
-
- [Patterns](./docs/guides/patterns/component-patterns.md) - Building components
|
|
109
|
-
|
|
110
|
-
### Reference
|
|
111
|
-
- [API Reference](./docs/api/README.md) - Complete API documentation
|
|
112
|
-
- [Architecture](./docs/contributing/architecture.md) - Deep dive into internals
|
|
113
|
-
- [Examples](./docs/examples/README.md) - 100+ working examples
|
|
114
|
-
|
|
115
|
-
### Contributing
|
|
116
|
-
- [Development Setup](./docs/contributing/development.md) - Get started contributing
|
|
117
|
-
- [Internals](./docs/contributing/internals/) - Engine deep-dives
|
|
118
|
-
|
|
119
|
-
## Primitives
|
|
120
|
-
|
|
121
|
-
### UI Primitives
|
|
122
|
-
|
|
123
|
-
| Primitive | Status | Description |
|
|
124
|
-
|-----------|--------|-------------|
|
|
125
|
-
| `box` | Complete | Container with flexbox layout |
|
|
126
|
-
| `text` | Complete | Text display with styling |
|
|
127
|
-
| `input` | Complete | Text input field |
|
|
128
|
-
| `select` | Planned | Dropdown selection |
|
|
129
|
-
| `progress` | Planned | Progress bar |
|
|
130
|
-
|
|
131
|
-
### Template Primitives
|
|
132
|
-
|
|
133
|
-
Reactive control flow for dynamic UIs - no manual effects needed!
|
|
134
|
-
|
|
135
|
-
| Primitive | Purpose | Description |
|
|
136
|
-
|-----------|---------|-------------|
|
|
137
|
-
| `each()` | Lists | Reactive list rendering with keyed reconciliation |
|
|
138
|
-
| `show()` | Conditionals | Show/hide components based on condition |
|
|
139
|
-
| `when()` | Async | Suspense-like pending/success/error states |
|
|
140
|
-
|
|
141
|
-
#### `each()` - Reactive Lists
|
|
142
|
-
|
|
143
|
-
Renders a list of components that automatically updates when the array changes.
|
|
144
|
-
|
|
145
|
-
```typescript
|
|
146
|
-
import { mount, signal, each, box, text } from '@rlabs-inc/tui'
|
|
147
|
-
|
|
148
|
-
const todos = signal([
|
|
149
|
-
{ id: '1', text: 'Learn TUI', done: false },
|
|
150
|
-
{ id: '2', text: 'Build app', done: false },
|
|
151
|
-
])
|
|
152
|
-
|
|
153
|
-
mount(() => {
|
|
154
|
-
box({
|
|
155
|
-
children: () => {
|
|
156
|
-
each(
|
|
157
|
-
() => todos.value, // Reactive array getter
|
|
158
|
-
(getItem, key) => box({ // Render function: getItem() + stable key
|
|
159
|
-
id: `todo-${key}`, // Use key for stable ID
|
|
160
|
-
children: () => {
|
|
161
|
-
text({ content: () => getItem().text }) // Call getItem() to access value
|
|
162
|
-
}
|
|
163
|
-
}),
|
|
164
|
-
{ key: (todo) => todo.id } // Key function for efficient updates
|
|
165
|
-
)
|
|
166
|
-
}
|
|
167
|
-
})
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
// Add item - UI updates automatically
|
|
171
|
-
todos.value = [...todos.value, { id: '3', text: 'Deploy', done: false }]
|
|
172
|
-
|
|
173
|
-
// Remove item - component is cleaned up automatically
|
|
174
|
-
todos.value = todos.value.filter(t => t.id !== '1')
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
#### `show()` - Conditional Rendering
|
|
178
|
-
|
|
179
|
-
Shows or hides components based on a reactive condition.
|
|
11
|
+
## Quick Example
|
|
180
12
|
|
|
181
13
|
```typescript
|
|
182
|
-
import {
|
|
14
|
+
import { signal, box, text, mount, keyboard } from '@rlabs-inc/tui'
|
|
183
15
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
mount(() => {
|
|
187
|
-
box({
|
|
188
|
-
children: () => {
|
|
189
|
-
show(
|
|
190
|
-
() => isLoggedIn.value, // Condition getter
|
|
191
|
-
() => box({ // Render when true
|
|
192
|
-
children: () => {
|
|
193
|
-
text({ content: 'Welcome back!' })
|
|
194
|
-
}
|
|
195
|
-
}),
|
|
196
|
-
() => text({ content: 'Please log in' }) // Optional: render when false
|
|
197
|
-
)
|
|
198
|
-
}
|
|
199
|
-
})
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
// Toggle - UI switches automatically
|
|
203
|
-
isLoggedIn.value = true
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
#### `when()` - Async/Suspense
|
|
207
|
-
|
|
208
|
-
Handles async operations with loading, success, and error states.
|
|
209
|
-
|
|
210
|
-
```typescript
|
|
211
|
-
import { mount, when, box, text, signal } from '@rlabs-inc/tui'
|
|
212
|
-
|
|
213
|
-
const userId = signal('123')
|
|
214
|
-
|
|
215
|
-
// Fetch function that returns a promise
|
|
216
|
-
const fetchUser = (id: string) =>
|
|
217
|
-
fetch(`/api/users/${id}`).then(r => r.json())
|
|
16
|
+
const count = signal(0)
|
|
218
17
|
|
|
219
|
-
mount(() => {
|
|
18
|
+
const cleanup = await mount(() => {
|
|
220
19
|
box({
|
|
20
|
+
padding: 1,
|
|
221
21
|
children: () => {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
{
|
|
225
|
-
pending: () => text({ content: 'Loading...' }),
|
|
226
|
-
then: (user) => box({
|
|
227
|
-
children: () => {
|
|
228
|
-
text({ content: `Name: ${user.name}` })
|
|
229
|
-
text({ content: `Email: ${user.email}` })
|
|
230
|
-
}
|
|
231
|
-
}),
|
|
232
|
-
catch: (error) => text({
|
|
233
|
-
content: `Error: ${error.message}`,
|
|
234
|
-
fg: 'red'
|
|
235
|
-
})
|
|
236
|
-
}
|
|
237
|
-
)
|
|
22
|
+
text({ content: () => `Count: ${count.value}` })
|
|
23
|
+
text({ content: 'Press + to increment, q to quit' })
|
|
238
24
|
}
|
|
239
25
|
})
|
|
240
26
|
})
|
|
241
27
|
|
|
242
|
-
|
|
243
|
-
|
|
28
|
+
keyboard.onKey('+', () => { count.value++ })
|
|
29
|
+
keyboard.onKey('q', () => cleanup())
|
|
244
30
|
```
|
|
245
31
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
All template primitives follow the same elegant pattern:
|
|
249
|
-
|
|
250
|
-
1. **Capture parent context** at creation time
|
|
251
|
-
2. **Initial render synchronously** (correct parent hierarchy)
|
|
252
|
-
3. **Internal effect** tracks reactive dependencies
|
|
253
|
-
4. **Reconcile on change** (create new, cleanup removed)
|
|
254
|
-
|
|
255
|
-
This means:
|
|
256
|
-
- User code stays clean - no manual effects
|
|
257
|
-
- Props inside templates are fully reactive
|
|
258
|
-
- Cleanup is automatic
|
|
259
|
-
- Performance is optimal (only affected components update)
|
|
260
|
-
|
|
261
|
-
## Test Coverage
|
|
262
|
-
|
|
263
|
-
- TITAN layout engine: 48 tests
|
|
264
|
-
- Parallel arrays: 17 tests
|
|
265
|
-
- Focus manager: 29 tests
|
|
266
|
-
|
|
267
|
-
## Requirements
|
|
268
|
-
|
|
269
|
-
- Bun 1.0+ (for runtime and build)
|
|
270
|
-
- Node.js 18+ (for compatibility)
|
|
271
|
-
- Terminal with ANSI support
|
|
32
|
+
Run with `bun run your-file.ts`.
|
|
272
33
|
|
|
273
|
-
##
|
|
34
|
+
## Core Concepts
|
|
274
35
|
|
|
275
|
-
|
|
36
|
+
- **Signals** - Reactive state: `signal(0)` creates a value that triggers updates when changed
|
|
37
|
+
- **Primitives** - UI building blocks: `box`, `text`, `input`
|
|
38
|
+
- **Reactivity** - Use `() =>` functions for dynamic values that update automatically
|
|
39
|
+
- **Keyboard** - `keyboard.onKey('Enter', handler)` for input handling
|
|
276
40
|
|
|
277
|
-
|
|
41
|
+
## Next Steps
|
|
278
42
|
|
|
279
|
-
|
|
43
|
+
- [Quick Start Guide](./getting-started/quickstart.md) - Build your first app step by step
|
package/package.json
CHANGED
package/src/api/mount.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
import { AppendRegionRenderer } from '../renderer/append-region'
|
|
32
32
|
import { HistoryWriter, createRenderToHistory } from './history'
|
|
33
33
|
import * as ansi from '../renderer/ansi'
|
|
34
|
-
import { frameBufferDerived } from '../pipeline/frameBuffer'
|
|
34
|
+
import { frameBufferDerived, _dbg_layout_us, _dbg_buf_us, _dbg_map_us, _dbg_render_us } from '../pipeline/frameBuffer'
|
|
35
35
|
import { layoutDerived, terminalWidth, terminalHeight, updateTerminalSize, renderMode } from '../pipeline/layout'
|
|
36
36
|
import { resetRegistry } from '../engine/registry'
|
|
37
37
|
import { hitGrid, clearHitGrid, mouse } from '../state/mouse'
|
|
@@ -219,7 +219,11 @@ export async function mount(
|
|
|
219
219
|
const bufferMs = bufferNs / 1_000_000
|
|
220
220
|
const renderMs = renderNs / 1_000_000
|
|
221
221
|
const totalMs = totalNs / 1_000_000
|
|
222
|
-
|
|
222
|
+
// DEBUG: Internal breakdown + mount.ts comparison
|
|
223
|
+
const buffer_us = bufferNs / 1000
|
|
224
|
+
const internal = _dbg_layout_us + _dbg_buf_us + _dbg_map_us + _dbg_render_us
|
|
225
|
+
const gap = buffer_us - internal
|
|
226
|
+
process.stdout.write(`\x1b]0;l:${_dbg_layout_us.toFixed(0)} b:${_dbg_buf_us.toFixed(0)} m:${_dbg_map_us.toFixed(0)} r:${_dbg_render_us.toFixed(0)} | buf:${buffer_us.toFixed(0)} int:${internal.toFixed(0)} gap:${gap.toFixed(0)} μs\x07`)
|
|
223
227
|
} catch (err) {
|
|
224
228
|
console.error('[TUI] Render effect error:', err)
|
|
225
229
|
}
|
|
@@ -87,8 +87,19 @@ export interface FrameBufferResult {
|
|
|
87
87
|
* - FrameBufferResult containing buffer, hitRegions, and terminal size
|
|
88
88
|
* - hitRegions should be applied to HitGrid by the render effect (no side effects here!)
|
|
89
89
|
*/
|
|
90
|
+
// DEBUG: Export timing for mount.ts to display
|
|
91
|
+
export let _dbg_layout_us = 0
|
|
92
|
+
export let _dbg_buf_us = 0
|
|
93
|
+
export let _dbg_map_us = 0
|
|
94
|
+
export let _dbg_render_us = 0
|
|
95
|
+
|
|
90
96
|
export const frameBufferDerived = derived((): FrameBufferResult => {
|
|
97
|
+
// DEBUG TIMING
|
|
98
|
+
const t0 = Bun.nanoseconds()
|
|
99
|
+
|
|
91
100
|
const computedLayout = layoutDerived.value
|
|
101
|
+
const t1 = Bun.nanoseconds()
|
|
102
|
+
|
|
92
103
|
const tw = terminalWidth.value
|
|
93
104
|
const th = terminalHeight.value
|
|
94
105
|
const mode = renderMode.value
|
|
@@ -106,6 +117,7 @@ export const frameBufferDerived = derived((): FrameBufferResult => {
|
|
|
106
117
|
|
|
107
118
|
// Create fresh buffer with terminal default background
|
|
108
119
|
const buffer = createBuffer(bufferWidth, bufferHeight, TERMINAL_DEFAULT)
|
|
120
|
+
const t2 = Bun.nanoseconds()
|
|
109
121
|
|
|
110
122
|
const indices = getAllocatedIndices()
|
|
111
123
|
if (indices.size === 0) {
|
|
@@ -141,6 +153,7 @@ export const frameBufferDerived = derived((): FrameBufferResult => {
|
|
|
141
153
|
for (const children of childMap.values()) {
|
|
142
154
|
children.sort((a, b) => (layout.zIndex[a] || 0) - (layout.zIndex[b] || 0))
|
|
143
155
|
}
|
|
156
|
+
const t3 = Bun.nanoseconds()
|
|
144
157
|
|
|
145
158
|
// Render tree recursively
|
|
146
159
|
for (const rootIdx of rootIndices) {
|
|
@@ -155,6 +168,13 @@ export const frameBufferDerived = derived((): FrameBufferResult => {
|
|
|
155
168
|
0 // No parent scroll X
|
|
156
169
|
)
|
|
157
170
|
}
|
|
171
|
+
const t4 = Bun.nanoseconds()
|
|
172
|
+
|
|
173
|
+
// Store timing in exported vars for mount.ts to display
|
|
174
|
+
_dbg_layout_us = (t1 - t0) / 1000
|
|
175
|
+
_dbg_buf_us = (t2 - t1) / 1000
|
|
176
|
+
_dbg_map_us = (t3 - t2) / 1000
|
|
177
|
+
_dbg_render_us = (t4 - t3) / 1000
|
|
158
178
|
|
|
159
179
|
return { buffer, hitRegions, terminalSize: { width: bufferWidth, height: bufferHeight } }
|
|
160
180
|
})
|