@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 CHANGED
@@ -1,279 +1,43 @@
1
- # TUI Framework
1
+ # TUI
2
2
 
3
- **The Terminal UI Framework for TypeScript/Bun**
3
+ A reactive terminal UI framework for TypeScript and Bun.
4
4
 
5
- A blazing-fast, fine-grained reactive terminal UI framework with complete flexbox layout and zero CPU when idle.
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
- ```typescript
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 { mount, show, box, text, signal } from '@rlabs-inc/tui'
14
+ import { signal, box, text, mount, keyboard } from '@rlabs-inc/tui'
183
15
 
184
- const isLoggedIn = signal(false)
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
- when(
223
- () => fetchUser(userId.value), // Promise getter (re-runs on userId change)
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
- // Change userId - triggers new fetch, shows loading, then result
243
- userId.value = '456'
28
+ keyboard.onKey('+', () => { count.value++ })
29
+ keyboard.onKey('q', () => cleanup())
244
30
  ```
245
31
 
246
- ### How Template Primitives Work
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
- ## License
34
+ ## Core Concepts
274
35
 
275
- MIT
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
- Built with love by Rusty & Watson in São Paulo, Brazil.
43
+ - [Quick Start Guide](./getting-started/quickstart.md) - Build your first app step by step
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rlabs-inc/tui",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "The Terminal UI Framework for TypeScript/Bun - Blazing-fast, fine-grained reactive terminal UI with complete flexbox layout",
5
5
  "module": "index.ts",
6
6
  "main": "index.ts",
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
- process.stdout.write(`\x1b]0;TUI | h:${computedLayout.contentHeight} | layout: ${layoutMs.toFixed(3)}ms | buffer: ${bufferMs.toFixed(3)}ms | render: ${renderMs.toFixed(3)}ms | total: ${totalMs.toFixed(3)}ms\x07`)
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
  })