@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
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# TUI Framework
|
|
2
|
+
|
|
3
|
+
**The Terminal UI Framework for TypeScript/Bun**
|
|
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
|
+
| Maximum components tested | 1,000,000 |
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun add tui @rlabs-inc/signals
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { mount, box, text } from 'tui'
|
|
25
|
+
import { signal } from '@rlabs-inc/signals'
|
|
26
|
+
|
|
27
|
+
// Create reactive state
|
|
28
|
+
const count = signal(0)
|
|
29
|
+
|
|
30
|
+
// Build your UI
|
|
31
|
+
box({ width: 40, height: 10, children: () => {
|
|
32
|
+
text({ content: `Count: ${count.value}` })
|
|
33
|
+
}})
|
|
34
|
+
|
|
35
|
+
// Mount to terminal
|
|
36
|
+
mount()
|
|
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
|
+
|
|
60
|
+
### Svelte-like .tui Files (Compiler)
|
|
61
|
+
```html
|
|
62
|
+
<script>
|
|
63
|
+
const name = signal('World')
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<box width={40} height={3}>
|
|
67
|
+
<text content={`Hello, ${name.value}!`} />
|
|
68
|
+
</box>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### State Modules
|
|
72
|
+
- **keyboard** - Key events, shortcuts, input buffering
|
|
73
|
+
- **mouse** - Click, hover, drag, wheel events
|
|
74
|
+
- **focus** - Tab navigation, focus trapping
|
|
75
|
+
- **scroll** - Scroll state and navigation
|
|
76
|
+
- **theme** - 14 color variants, theming
|
|
77
|
+
|
|
78
|
+
## Architecture
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
User Signals → bind() → Parallel Arrays → layoutDerived → frameBufferDerived → render
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Why So Fast?
|
|
85
|
+
|
|
86
|
+
1. **Parallel Arrays** - ECS-like data layout for cache efficiency
|
|
87
|
+
2. **No Reconciliation** - Fine-grained reactivity replaces diffing
|
|
88
|
+
3. **Single Effect** - One render effect for entire app
|
|
89
|
+
4. **TITAN Layout** - O(n) flexbox in pure TypeScript
|
|
90
|
+
5. **Zero CPU Idle** - Only updates when signals change
|
|
91
|
+
|
|
92
|
+
## Examples
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Run examples
|
|
96
|
+
bun run examples/hello.ts
|
|
97
|
+
bun run examples/showcase.ts
|
|
98
|
+
|
|
99
|
+
# Run tests
|
|
100
|
+
bun run examples/tests/01-box-basics.ts
|
|
101
|
+
bun run examples/tests/03-layout-flex.ts
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Documentation
|
|
105
|
+
|
|
106
|
+
- [CLAUDE.md](./CLAUDE.md) - Development guide and philosophy
|
|
107
|
+
- [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) - Deep dive into the architecture
|
|
108
|
+
- [docs/API.md](./docs/API.md) - Complete API reference
|
|
109
|
+
- [docs/BIND_PRIMITIVE.md](./docs/BIND_PRIMITIVE.md) - Understanding reactivity
|
|
110
|
+
|
|
111
|
+
## Primitives
|
|
112
|
+
|
|
113
|
+
| Primitive | Status | Description |
|
|
114
|
+
|-----------|--------|-------------|
|
|
115
|
+
| `box` | Complete | Container with flexbox layout |
|
|
116
|
+
| `text` | Complete | Text display with styling |
|
|
117
|
+
| `input` | Planned | Text input field |
|
|
118
|
+
| `select` | Planned | Dropdown selection |
|
|
119
|
+
| `progress` | Planned | Progress bar |
|
|
120
|
+
|
|
121
|
+
## Test Coverage
|
|
122
|
+
|
|
123
|
+
- **130 tests** passing
|
|
124
|
+
- TITAN layout engine: 48 tests
|
|
125
|
+
- Parallel arrays: 17 tests
|
|
126
|
+
- Focus manager: 29 tests
|
|
127
|
+
- Compiler: 36 tests (unit + integration)
|
|
128
|
+
|
|
129
|
+
## Requirements
|
|
130
|
+
|
|
131
|
+
- Bun 1.0+ (for runtime and build)
|
|
132
|
+
- Node.js 18+ (for compatibility)
|
|
133
|
+
- Terminal with ANSI support
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
Built with love by Rusty & Watson in São Paulo, Brazil.
|
package/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework
|
|
3
|
+
*
|
|
4
|
+
* The definitive TypeScript/Bun terminal UI framework.
|
|
5
|
+
* Fine-grained reactivity, parallel arrays, zero fixed-FPS rendering.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// API - User-facing
|
|
9
|
+
export { mount } from './src/api'
|
|
10
|
+
|
|
11
|
+
// Primitives - UI building blocks
|
|
12
|
+
export { box, text } from './src/primitives'
|
|
13
|
+
export type { BoxProps, TextProps, Cleanup } from './src/primitives'
|
|
14
|
+
|
|
15
|
+
// State modules - Input handling
|
|
16
|
+
export { keyboard } from './src/state/keyboard'
|
|
17
|
+
export { focusManager, focusedIndex } from './src/state/focus'
|
|
18
|
+
export { scroll } from './src/state/scroll'
|
|
19
|
+
export { mouse, hitGrid } from './src/state/mouse'
|
|
20
|
+
|
|
21
|
+
// Types
|
|
22
|
+
export * from './src/types'
|
|
23
|
+
export * from './src/types/color'
|
|
24
|
+
|
|
25
|
+
// Signals re-export for convenience
|
|
26
|
+
export { signal, state, derived, effect, bind, signals } from '@rlabs-inc/signals'
|
|
27
|
+
|
|
28
|
+
// Theme
|
|
29
|
+
export {
|
|
30
|
+
theme,
|
|
31
|
+
themes,
|
|
32
|
+
setTheme,
|
|
33
|
+
resolveColor,
|
|
34
|
+
resolvedTheme,
|
|
35
|
+
t,
|
|
36
|
+
getVariantStyle,
|
|
37
|
+
variantStyle,
|
|
38
|
+
} from './src/state/theme'
|
|
39
|
+
export type { Variant, VariantStyle, ThemeColor } from './src/state/theme'
|
|
40
|
+
|
|
41
|
+
// Renderer (advanced)
|
|
42
|
+
export * from './src/renderer'
|
|
43
|
+
|
|
44
|
+
// Engine (advanced)
|
|
45
|
+
export * from './src/engine'
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rlabs-inc/tui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Terminal UI Framework for TypeScript/Bun - Blazing-fast, fine-grained reactive terminal UI with complete flexbox layout",
|
|
5
|
+
"module": "index.ts",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.ts",
|
|
10
|
+
"./primitives": "./src/primitives/index.ts",
|
|
11
|
+
"./state": "./src/state/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.ts",
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "bun run examples/hello.ts",
|
|
19
|
+
"test": "bun test",
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"tui",
|
|
24
|
+
"terminal",
|
|
25
|
+
"ui",
|
|
26
|
+
"framework",
|
|
27
|
+
"reactive",
|
|
28
|
+
"signals",
|
|
29
|
+
"flexbox",
|
|
30
|
+
"bun",
|
|
31
|
+
"typescript",
|
|
32
|
+
"cli"
|
|
33
|
+
],
|
|
34
|
+
"author": "Rusty & Watson",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/rlabs-inc/tui.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/rlabs-inc/tui",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/rlabs-inc/tui/issues"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/bun": "latest"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"typescript": "^5.0.0"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@rlabs-inc/signals": "^1.6.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/api/index.ts
ADDED
package/src/api/mount.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - Mount API
|
|
3
|
+
*
|
|
4
|
+
* The entry point for TUI applications.
|
|
5
|
+
* Sets up terminal, creates the render effect, handles cleanup.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```ts
|
|
9
|
+
* const cleanup = await mount(() => {
|
|
10
|
+
* box({
|
|
11
|
+
* width: 40,
|
|
12
|
+
* height: 10,
|
|
13
|
+
* border: 1,
|
|
14
|
+
* children: () => {
|
|
15
|
+
* text({ content: 'Hello, TUI!' })
|
|
16
|
+
* }
|
|
17
|
+
* })
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* // Later...
|
|
21
|
+
* await cleanup()
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { effect } from '@rlabs-inc/signals'
|
|
26
|
+
import type { MountOptions, ResizeEvent } from '../types'
|
|
27
|
+
import {
|
|
28
|
+
DiffRenderer,
|
|
29
|
+
InlineRenderer,
|
|
30
|
+
} from '../renderer/output'
|
|
31
|
+
import * as ansi from '../renderer/ansi'
|
|
32
|
+
import { frameBufferDerived } from '../pipeline/frameBuffer'
|
|
33
|
+
import { layoutDerived, terminalWidth, terminalHeight, updateTerminalSize, renderMode } from '../pipeline/layout'
|
|
34
|
+
import { resetRegistry } from '../engine/registry'
|
|
35
|
+
import { hitGrid, clearHitGrid } from '../state/mouse'
|
|
36
|
+
import { keyboard } from '../state/keyboard'
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// MOUNT
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Mount a TUI application.
|
|
44
|
+
*
|
|
45
|
+
* @param root - Function that creates the component tree
|
|
46
|
+
* @param options - Mount options (mode, mouse, keyboard)
|
|
47
|
+
* @returns Cleanup function to unmount
|
|
48
|
+
*/
|
|
49
|
+
export async function mount(
|
|
50
|
+
root: () => void,
|
|
51
|
+
options: MountOptions = {}
|
|
52
|
+
): Promise<() => Promise<void>> {
|
|
53
|
+
const {
|
|
54
|
+
mode = 'fullscreen',
|
|
55
|
+
mouse = true,
|
|
56
|
+
kittyKeyboard = true,
|
|
57
|
+
} = options
|
|
58
|
+
|
|
59
|
+
// Set render mode signal BEFORE creating components
|
|
60
|
+
// This affects how layout and frameBuffer compute dimensions
|
|
61
|
+
renderMode.value = mode
|
|
62
|
+
|
|
63
|
+
// Create renderer based on mode
|
|
64
|
+
// Fullscreen uses DiffRenderer (absolute positioning)
|
|
65
|
+
// Inline uses InlineRenderer (eraseLines + sequential write)
|
|
66
|
+
// Append uses DiffRenderer for now (TODO: may need AppendRenderer)
|
|
67
|
+
const diffRenderer = new DiffRenderer()
|
|
68
|
+
const inlineRenderer = new InlineRenderer()
|
|
69
|
+
|
|
70
|
+
// Mode-specific state
|
|
71
|
+
let previousHeight = 0 // For append mode: track last rendered height
|
|
72
|
+
let isFirstRender = true
|
|
73
|
+
|
|
74
|
+
// Resize handlers (keyboard module handles key/mouse)
|
|
75
|
+
const resizeHandlers: Set<(event: ResizeEvent) => void> = new Set()
|
|
76
|
+
|
|
77
|
+
// Setup terminal
|
|
78
|
+
const setupSequence: string[] = []
|
|
79
|
+
|
|
80
|
+
if (mode === 'fullscreen') {
|
|
81
|
+
setupSequence.push(ansi.enterAlternativeScreen)
|
|
82
|
+
setupSequence.push(ansi.clearScreen)
|
|
83
|
+
setupSequence.push(ansi.cursorTo(1, 1)) // Cursor home
|
|
84
|
+
}
|
|
85
|
+
// Inline/Append: no special setup needed
|
|
86
|
+
// InlineRenderer handles erasing previous content
|
|
87
|
+
|
|
88
|
+
setupSequence.push(ansi.hideCursor)
|
|
89
|
+
|
|
90
|
+
if (mouse) {
|
|
91
|
+
setupSequence.push(ansi.enableMouse)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (kittyKeyboard) {
|
|
95
|
+
setupSequence.push(ansi.enableKittyKeyboard)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setupSequence.push(ansi.enableBracketedPaste)
|
|
99
|
+
setupSequence.push(ansi.enableFocusReporting)
|
|
100
|
+
|
|
101
|
+
// Write setup sequence
|
|
102
|
+
process.stdout.write(setupSequence.join(''))
|
|
103
|
+
|
|
104
|
+
// Initialize keyboard module (handles stdin, raw mode, input parsing)
|
|
105
|
+
// Pass cleanup callback for proper shutdown on Ctrl+C
|
|
106
|
+
keyboard.initialize()
|
|
107
|
+
|
|
108
|
+
// Handle resize
|
|
109
|
+
const handleResize = () => {
|
|
110
|
+
updateTerminalSize()
|
|
111
|
+
|
|
112
|
+
// For fullscreen, invalidate diff renderer to force full redraw
|
|
113
|
+
if (mode === 'fullscreen') {
|
|
114
|
+
diffRenderer.invalidate()
|
|
115
|
+
}
|
|
116
|
+
// For inline/append, InlineRenderer always redraws (eraseLines approach)
|
|
117
|
+
|
|
118
|
+
for (const handler of resizeHandlers) {
|
|
119
|
+
handler({ width: terminalWidth.value, height: terminalHeight.value })
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
process.stdout.on('resize', handleResize)
|
|
123
|
+
|
|
124
|
+
// Initialize terminal size
|
|
125
|
+
updateTerminalSize()
|
|
126
|
+
|
|
127
|
+
// Create the component tree
|
|
128
|
+
root()
|
|
129
|
+
|
|
130
|
+
// THE ONE RENDER EFFECT
|
|
131
|
+
// This is where the magic happens - reactive rendering!
|
|
132
|
+
// Side effects (HitGrid) are applied HERE, not in the derived.
|
|
133
|
+
let stopEffect: (() => void) | null = null
|
|
134
|
+
|
|
135
|
+
stopEffect = effect(() => {
|
|
136
|
+
const start = Bun.nanoseconds()
|
|
137
|
+
|
|
138
|
+
// Time layout separately (reading .value triggers computation if needed)
|
|
139
|
+
// const layoutStart = Bun.nanoseconds()
|
|
140
|
+
// const _layout = layoutDerived.value
|
|
141
|
+
// const layoutNs = Bun.nanoseconds() - layoutStart
|
|
142
|
+
|
|
143
|
+
// Time buffer computation
|
|
144
|
+
const bufferStart = Bun.nanoseconds()
|
|
145
|
+
const { buffer, hitRegions, terminalSize } = frameBufferDerived.value
|
|
146
|
+
const bufferNs = Bun.nanoseconds() - bufferStart
|
|
147
|
+
|
|
148
|
+
// Apply hit regions to HitGrid (side effect happens in effect, not derived!)
|
|
149
|
+
if (hitGrid.width !== terminalSize.width || hitGrid.height !== terminalSize.height) {
|
|
150
|
+
hitGrid.resize(terminalSize.width, terminalSize.height)
|
|
151
|
+
} else {
|
|
152
|
+
clearHitGrid()
|
|
153
|
+
}
|
|
154
|
+
for (const region of hitRegions) {
|
|
155
|
+
hitGrid.fillRect(region.x, region.y, region.width, region.height, region.componentIndex)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Time render to terminal
|
|
159
|
+
const renderStart = Bun.nanoseconds()
|
|
160
|
+
if (mode === 'fullscreen') {
|
|
161
|
+
diffRenderer.render(buffer)
|
|
162
|
+
} else if (mode === 'inline') {
|
|
163
|
+
inlineRenderer.render(buffer)
|
|
164
|
+
} else {
|
|
165
|
+
inlineRenderer.render(buffer)
|
|
166
|
+
}
|
|
167
|
+
const renderNs = Bun.nanoseconds() - renderStart
|
|
168
|
+
|
|
169
|
+
const totalNs = Bun.nanoseconds() - start
|
|
170
|
+
|
|
171
|
+
// Show all timing stats in window title
|
|
172
|
+
// const layoutMs = layoutNs / 1_000_000
|
|
173
|
+
const bufferMs = bufferNs / 1_000_000
|
|
174
|
+
const renderMs = renderNs / 1_000_000
|
|
175
|
+
const totalMs = totalNs / 1_000_000
|
|
176
|
+
// process.stdout.write(`\x1b]0;TUI | ${mode} | layout: ${layoutMs.toFixed(3)}ms | buffer: ${bufferMs.toFixed(3)}ms | render: ${renderMs.toFixed(3)}ms | total: ${totalMs.toFixed(3)}ms\x07`)
|
|
177
|
+
process.stdout.write(`\x1b]0;TUI | ${mode} | buffer: ${bufferMs.toFixed(3)}ms | render: ${renderMs.toFixed(3)}ms | total: ${totalMs.toFixed(3)}ms\x07`)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// Cleanup function
|
|
181
|
+
return async () => {
|
|
182
|
+
// Stop the render effect
|
|
183
|
+
if (stopEffect) {
|
|
184
|
+
stopEffect()
|
|
185
|
+
stopEffect = null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Cleanup keyboard module (removes stdin listener, restores terminal)
|
|
189
|
+
keyboard.cleanup()
|
|
190
|
+
|
|
191
|
+
// Remove resize listener
|
|
192
|
+
process.stdout.removeListener('resize', handleResize)
|
|
193
|
+
|
|
194
|
+
// Restore terminal
|
|
195
|
+
const restoreSequence: string[] = []
|
|
196
|
+
|
|
197
|
+
restoreSequence.push(ansi.disableFocusReporting)
|
|
198
|
+
restoreSequence.push(ansi.disableBracketedPaste)
|
|
199
|
+
|
|
200
|
+
if (kittyKeyboard) {
|
|
201
|
+
restoreSequence.push(ansi.disableKittyKeyboard)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (mouse) {
|
|
205
|
+
restoreSequence.push(ansi.disableMouse)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
restoreSequence.push(ansi.showCursor)
|
|
209
|
+
restoreSequence.push(ansi.reset)
|
|
210
|
+
|
|
211
|
+
if (mode === 'fullscreen') {
|
|
212
|
+
restoreSequence.push(ansi.exitAlternativeScreen)
|
|
213
|
+
} else if (mode === 'inline' || mode === 'append') {
|
|
214
|
+
// For inline/append: content is already on screen, just reset and add newline
|
|
215
|
+
// InlineRenderer leaves cursor at end of content
|
|
216
|
+
restoreSequence.push('\n')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
process.stdout.write(restoreSequence.join(''))
|
|
220
|
+
|
|
221
|
+
// Disable raw mode
|
|
222
|
+
if (process.stdin.isTTY) {
|
|
223
|
+
process.stdin.setRawMode(false)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Reset registry for clean slate
|
|
227
|
+
resetRegistry()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - Core Arrays
|
|
3
|
+
*
|
|
4
|
+
* The most fundamental component arrays:
|
|
5
|
+
* - componentType: What kind of component (box, text, etc.)
|
|
6
|
+
* - parentIndex: Parent in hierarchy
|
|
7
|
+
* - visible: Is component rendered
|
|
8
|
+
*
|
|
9
|
+
* NOTE: Focus state (focusable, tabIndex) is in interaction.ts
|
|
10
|
+
*
|
|
11
|
+
* CRITICAL: Arrays storing Bindings must be regular arrays (NOT state!)
|
|
12
|
+
* state() proxies snapshot getter values, breaking reactivity.
|
|
13
|
+
* componentType is the exception - it stores values directly, not bindings.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { bind, disconnectBinding, type Binding } from '@rlabs-inc/signals'
|
|
17
|
+
import { ComponentType } from '../../types'
|
|
18
|
+
import type { ComponentTypeValue } from '../../types'
|
|
19
|
+
|
|
20
|
+
/** Component type (box, text, input, etc.) - stores values directly */
|
|
21
|
+
export const componentType: ComponentTypeValue[] = []
|
|
22
|
+
|
|
23
|
+
/** Parent component index (-1 for root) */
|
|
24
|
+
export const parentIndex: Binding<number>[] = []
|
|
25
|
+
|
|
26
|
+
/** Is component visible (0/false = hidden, 1/true = visible) */
|
|
27
|
+
export const visible: Binding<number | boolean>[] = []
|
|
28
|
+
|
|
29
|
+
/** Component ID (for debugging and lookups) */
|
|
30
|
+
export const componentId: Binding<string>[] = []
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ensure array has capacity for the given index.
|
|
34
|
+
* Called by registry when allocating.
|
|
35
|
+
*
|
|
36
|
+
* LAZY BINDING: We push undefined here, not bindings.
|
|
37
|
+
* Primitives create bindings only for props they actually use.
|
|
38
|
+
* This reduces memory from ~70 bindings/component to ~5-10.
|
|
39
|
+
*/
|
|
40
|
+
export function ensureCapacity(index: number): void {
|
|
41
|
+
while (componentType.length <= index) {
|
|
42
|
+
componentType.push(ComponentType.NONE)
|
|
43
|
+
parentIndex.push(undefined as any)
|
|
44
|
+
visible.push(undefined as any)
|
|
45
|
+
componentId.push(undefined as any)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Clear values at index (called when releasing) */
|
|
50
|
+
export function clearAtIndex(index: number): void {
|
|
51
|
+
if (index < componentType.length) {
|
|
52
|
+
componentType[index] = ComponentType.NONE
|
|
53
|
+
disconnectBinding(parentIndex[index])
|
|
54
|
+
disconnectBinding(visible[index])
|
|
55
|
+
disconnectBinding(componentId[index])
|
|
56
|
+
parentIndex[index] = undefined as any
|
|
57
|
+
visible[index] = undefined as any
|
|
58
|
+
componentId[index] = undefined as any
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - Dimension Arrays
|
|
3
|
+
*
|
|
4
|
+
* Width, height, min/max constraints.
|
|
5
|
+
* These are INPUT values that components write.
|
|
6
|
+
* The layout derived READS these and RETURNS computed positions.
|
|
7
|
+
*
|
|
8
|
+
* CRITICAL: Use regular arrays (NOT state!) to preserve binding getters.
|
|
9
|
+
* state() proxies snapshot getter values, breaking reactivity.
|
|
10
|
+
*
|
|
11
|
+
* Supports both absolute and percentage dimensions:
|
|
12
|
+
* - number: Absolute value in cells (e.g., 50)
|
|
13
|
+
* - string: Percentage of parent (e.g., '50%', '100%')
|
|
14
|
+
*
|
|
15
|
+
* TITAN resolves percentages against parent computed sizes at layout time.
|
|
16
|
+
*
|
|
17
|
+
* Note: 0 means "auto" for width/height, "no constraint" for min/max.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { bind, disconnectBinding, type Binding } from '@rlabs-inc/signals'
|
|
21
|
+
import type { Dimension } from '../../types'
|
|
22
|
+
|
|
23
|
+
/** Requested width (0 = auto, '100%' = full parent width) */
|
|
24
|
+
export const width: Binding<Dimension>[] = []
|
|
25
|
+
|
|
26
|
+
/** Requested height (0 = auto, '100%' = full parent height) */
|
|
27
|
+
export const height: Binding<Dimension>[] = []
|
|
28
|
+
|
|
29
|
+
/** Minimum width constraint */
|
|
30
|
+
export const minWidth: Binding<Dimension>[] = []
|
|
31
|
+
|
|
32
|
+
/** Minimum height constraint */
|
|
33
|
+
export const minHeight: Binding<Dimension>[] = []
|
|
34
|
+
|
|
35
|
+
/** Maximum width constraint (0 = no max) */
|
|
36
|
+
export const maxWidth: Binding<Dimension>[] = []
|
|
37
|
+
|
|
38
|
+
/** Maximum height constraint (0 = no max) */
|
|
39
|
+
export const maxHeight: Binding<Dimension>[] = []
|
|
40
|
+
|
|
41
|
+
/** LAZY BINDING: Push undefined, primitives create bindings for used props only */
|
|
42
|
+
export function ensureCapacity(index: number): void {
|
|
43
|
+
while (width.length <= index) {
|
|
44
|
+
width.push(undefined as any)
|
|
45
|
+
height.push(undefined as any)
|
|
46
|
+
minWidth.push(undefined as any)
|
|
47
|
+
minHeight.push(undefined as any)
|
|
48
|
+
maxWidth.push(undefined as any)
|
|
49
|
+
maxHeight.push(undefined as any)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clearAtIndex(index: number): void {
|
|
54
|
+
if (index < width.length) {
|
|
55
|
+
disconnectBinding(width[index])
|
|
56
|
+
disconnectBinding(height[index])
|
|
57
|
+
disconnectBinding(minWidth[index])
|
|
58
|
+
disconnectBinding(minHeight[index])
|
|
59
|
+
disconnectBinding(maxWidth[index])
|
|
60
|
+
disconnectBinding(maxHeight[index])
|
|
61
|
+
width[index] = undefined as any
|
|
62
|
+
height[index] = undefined as any
|
|
63
|
+
minWidth[index] = undefined as any
|
|
64
|
+
minHeight[index] = undefined as any
|
|
65
|
+
maxWidth[index] = undefined as any
|
|
66
|
+
maxHeight[index] = undefined as any
|
|
67
|
+
}
|
|
68
|
+
}
|