@opentuah/react 0.1.77
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/LICENSE +21 -0
- package/README.md +1001 -0
- package/chunk-7627c4s7.js +569 -0
- package/chunk-eecw9x2f.js +4 -0
- package/chunk-pm1hna8x.js +28 -0
- package/index.js +117 -0
- package/jsx-dev-runtime.d.ts +2 -0
- package/jsx-dev-runtime.js +1 -0
- package/jsx-namespace.d.ts +62 -0
- package/jsx-runtime.d.ts +2 -0
- package/jsx-runtime.js +1 -0
- package/package.json +62 -0
- package/src/components/app.d.ts +8 -0
- package/src/components/error-boundary.d.ts +16 -0
- package/src/components/index.d.ts +42 -0
- package/src/components/text.d.ts +30 -0
- package/src/hooks/index.d.ts +5 -0
- package/src/hooks/use-event.d.ts +9 -0
- package/src/hooks/use-keyboard.d.ts +22 -0
- package/src/hooks/use-renderer.d.ts +1 -0
- package/src/hooks/use-resize.d.ts +1 -0
- package/src/hooks/use-terminal-dimensions.d.ts +4 -0
- package/src/hooks/use-timeline.d.ts +2 -0
- package/src/index.d.ts +6 -0
- package/src/reconciler/devtools-polyfill.d.ts +1 -0
- package/src/reconciler/devtools.d.ts +1 -0
- package/src/reconciler/host-config.d.ts +9 -0
- package/src/reconciler/reconciler.d.ts +5 -0
- package/src/reconciler/renderer.d.ts +23 -0
- package/src/test-utils.d.ts +11 -0
- package/src/types/components.d.ts +86 -0
- package/src/types/host.d.ts +11 -0
- package/src/utils/id.d.ts +2 -0
- package/src/utils/index.d.ts +3 -0
- package/test-utils.js +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
# @opentui/react
|
|
2
|
+
|
|
3
|
+
A React renderer for building terminal user interfaces using [OpenTUI core](https://github.com/anomalyco/opentui). Create rich, interactive console applications with familiar React patterns and components.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Quick start with [bun](https://bun.sh) and [create-tui](https://github.com/msmps/create-tui):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun create tui --template react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Manual installation:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun install @opentui/react @opentui/core react
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { createCliRenderer } from "@opentui/core"
|
|
23
|
+
import { createRoot } from "@opentui/react"
|
|
24
|
+
|
|
25
|
+
function App() {
|
|
26
|
+
return <text>Hello, world!</text>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const renderer = await createCliRenderer()
|
|
30
|
+
createRoot(renderer).render(<App />)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## TypeScript Configuration
|
|
34
|
+
|
|
35
|
+
For optimal TypeScript support, configure your `tsconfig.json`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"compilerOptions": {
|
|
40
|
+
"lib": ["ESNext", "DOM"],
|
|
41
|
+
"target": "ESNext",
|
|
42
|
+
"module": "ESNext",
|
|
43
|
+
"moduleResolution": "bundler",
|
|
44
|
+
"jsx": "react-jsx",
|
|
45
|
+
"jsxImportSource": "@opentui/react",
|
|
46
|
+
"strict": true,
|
|
47
|
+
"skipLibCheck": true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Table of Contents
|
|
53
|
+
|
|
54
|
+
- [Core Concepts](#core-concepts)
|
|
55
|
+
- [Components](#components)
|
|
56
|
+
- [Styling](#styling)
|
|
57
|
+
- [API Reference](#api-reference)
|
|
58
|
+
- [createRoot(renderer)](#createrootrenderer)
|
|
59
|
+
- [render(element, config?)](#renderelement-config-deprecated)
|
|
60
|
+
- [Hooks](#hooks)
|
|
61
|
+
- [useRenderer()](#userenderer)
|
|
62
|
+
- [useKeyboard(handler, options?)](#usekeyboardhandler-options)
|
|
63
|
+
- [useOnResize(callback)](#useonresizecallback)
|
|
64
|
+
- [useTerminalDimensions()](#useterminaldimensions)
|
|
65
|
+
- [useTimeline(options?)](#usetimelineoptions)
|
|
66
|
+
- [Components](#components-1)
|
|
67
|
+
- [Layout & Display Components](#layout--display-components)
|
|
68
|
+
- [Text Component](#text-component)
|
|
69
|
+
- [Box Component](#box-component)
|
|
70
|
+
- [Scrollbox Component](#scrollbox-component)
|
|
71
|
+
- [ASCII Font Component](#ascii-font-component)
|
|
72
|
+
- [Input Components](#input-components)
|
|
73
|
+
- [Input Component](#input-component)
|
|
74
|
+
- [Textarea Component](#textarea-component)
|
|
75
|
+
- [Select Component](#select-component)
|
|
76
|
+
- [Code & Diff Components](#code--diff-components)
|
|
77
|
+
- [Code Component](#code-component)
|
|
78
|
+
- [Line Number Component](#line-number-component)
|
|
79
|
+
- [Diff Component](#diff-component)
|
|
80
|
+
- [Examples](#examples)
|
|
81
|
+
- [Login Form](#login-form)
|
|
82
|
+
- [Counter with Timer](#counter-with-timer)
|
|
83
|
+
- [System Monitor Animation](#system-monitor-animation)
|
|
84
|
+
- [Styled Text Showcase](#styled-text-showcase)
|
|
85
|
+
- [Component Extension](#component-extension)
|
|
86
|
+
- [Using React DevTools](#using-react-devtools)
|
|
87
|
+
|
|
88
|
+
## Core Concepts
|
|
89
|
+
|
|
90
|
+
### Components
|
|
91
|
+
|
|
92
|
+
OpenTUI React provides several built-in components that map to OpenTUI core renderables:
|
|
93
|
+
|
|
94
|
+
**Layout & Display:**
|
|
95
|
+
|
|
96
|
+
- **`<text>`** - Display text with styling
|
|
97
|
+
- **`<box>`** - Container with borders and layout
|
|
98
|
+
- **`<scrollbox>`** - A scrollable box
|
|
99
|
+
- **`<ascii-font>`** - Display ASCII art text with different font styles
|
|
100
|
+
|
|
101
|
+
**Input Components:**
|
|
102
|
+
|
|
103
|
+
- **`<input>`** - Text input field
|
|
104
|
+
- **`<textarea>`** - Multi-line text input field
|
|
105
|
+
- **`<select>`** - Selection dropdown
|
|
106
|
+
- **`<tab-select>`** - Tab-based selection
|
|
107
|
+
|
|
108
|
+
**Code & Diff Components:**
|
|
109
|
+
|
|
110
|
+
- **`<code>`** - Code block with syntax highlighting
|
|
111
|
+
- **`<line-number>`** - Code display with line numbers, diff highlights, and diagnostics
|
|
112
|
+
- **`<diff>`** - Unified or split diff viewer with syntax highlighting
|
|
113
|
+
|
|
114
|
+
**Helpers:**
|
|
115
|
+
|
|
116
|
+
- **`<span>`, `<strong>`, `<em>`, `<u>`, `<b>`, `<i>`, `<br>`** - Text modifiers (_must be used inside of the text component_)
|
|
117
|
+
|
|
118
|
+
### Styling
|
|
119
|
+
|
|
120
|
+
Components can be styled using props or the `style` prop:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// Direct props
|
|
124
|
+
<box backgroundColor="blue" padding={2}>
|
|
125
|
+
<text>Hello, world!</text>
|
|
126
|
+
</box>
|
|
127
|
+
|
|
128
|
+
// Style prop
|
|
129
|
+
<box style={{ backgroundColor: "blue", padding: 2 }}>
|
|
130
|
+
<text>Hello, world!</text>
|
|
131
|
+
</box>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## API Reference
|
|
135
|
+
|
|
136
|
+
### `createRoot(renderer)`
|
|
137
|
+
|
|
138
|
+
Creates a root for rendering a React tree with the given CLI renderer.
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
import { createCliRenderer } from "@opentui/core"
|
|
142
|
+
import { createRoot } from "@opentui/react"
|
|
143
|
+
|
|
144
|
+
const renderer = await createCliRenderer({
|
|
145
|
+
// Optional renderer configuration
|
|
146
|
+
exitOnCtrlC: false,
|
|
147
|
+
})
|
|
148
|
+
createRoot(renderer).render(<App />)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Parameters:**
|
|
152
|
+
|
|
153
|
+
- `renderer`: A `CliRenderer` instance (typically created with `createCliRenderer()`)
|
|
154
|
+
|
|
155
|
+
**Returns:** An object with a `render` method that accepts a React element.
|
|
156
|
+
|
|
157
|
+
### `render(element, config?)` (Deprecated)
|
|
158
|
+
|
|
159
|
+
> **Deprecated:** Use `createRoot(renderer).render(node)` instead.
|
|
160
|
+
|
|
161
|
+
Renders a React element to the terminal. This function is deprecated in favor of `createRoot`.
|
|
162
|
+
|
|
163
|
+
### Hooks
|
|
164
|
+
|
|
165
|
+
#### `useRenderer()`
|
|
166
|
+
|
|
167
|
+
Access the OpenTUI renderer instance.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import { useRenderer } from "@opentui/react"
|
|
171
|
+
|
|
172
|
+
function App() {
|
|
173
|
+
const renderer = useRenderer()
|
|
174
|
+
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
renderer.console.show()
|
|
177
|
+
console.log("Hello, from the console!")
|
|
178
|
+
}, [])
|
|
179
|
+
|
|
180
|
+
return <box />
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### `useKeyboard(handler, options?)`
|
|
185
|
+
|
|
186
|
+
Handle keyboard events.
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
import { useKeyboard } from "@opentui/react"
|
|
190
|
+
|
|
191
|
+
function App() {
|
|
192
|
+
useKeyboard((key) => {
|
|
193
|
+
if (key.name === "escape") {
|
|
194
|
+
process.exit(0)
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
return <text>Press ESC to exit</text>
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Parameters:**
|
|
203
|
+
|
|
204
|
+
- `handler`: Callback function that receives a `KeyEvent` object
|
|
205
|
+
- `options?`: Optional configuration object:
|
|
206
|
+
- `release?`: Boolean to include key release events (default: `false`)
|
|
207
|
+
|
|
208
|
+
By default, only receives press events (including key repeats with `repeated: true`). Set `options.release` to `true` to also receive release events.
|
|
209
|
+
|
|
210
|
+
**Example with release events:**
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
import { useKeyboard } from "@opentui/react"
|
|
214
|
+
import { useState } from "react"
|
|
215
|
+
|
|
216
|
+
function App() {
|
|
217
|
+
const [pressedKeys, setPressedKeys] = useState<Set<string>>(new Set())
|
|
218
|
+
|
|
219
|
+
useKeyboard(
|
|
220
|
+
(event) => {
|
|
221
|
+
setPressedKeys((keys) => {
|
|
222
|
+
const newKeys = new Set(keys)
|
|
223
|
+
if (event.eventType === "release") {
|
|
224
|
+
newKeys.delete(event.name)
|
|
225
|
+
} else {
|
|
226
|
+
newKeys.add(event.name)
|
|
227
|
+
}
|
|
228
|
+
return newKeys
|
|
229
|
+
})
|
|
230
|
+
},
|
|
231
|
+
{ release: true },
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<box>
|
|
236
|
+
<text>Currently pressed: {Array.from(pressedKeys).join(", ") || "none"}</text>
|
|
237
|
+
</box>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### `useOnResize(callback)`
|
|
243
|
+
|
|
244
|
+
Handle terminal resize events.
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
import { useOnResize, useRenderer } from "@opentui/react"
|
|
248
|
+
import { useEffect } from "react"
|
|
249
|
+
|
|
250
|
+
function App() {
|
|
251
|
+
const renderer = useRenderer()
|
|
252
|
+
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
renderer.console.show()
|
|
255
|
+
}, [renderer])
|
|
256
|
+
|
|
257
|
+
useOnResize((width, height) => {
|
|
258
|
+
console.log(`Terminal resized to ${width}x${height}`)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
return <text>Resize-aware component</text>
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### `useTerminalDimensions()`
|
|
266
|
+
|
|
267
|
+
Get current terminal dimensions and automatically update when the terminal is resized.
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
import { useTerminalDimensions } from "@opentui/react"
|
|
271
|
+
|
|
272
|
+
function App() {
|
|
273
|
+
const { width, height } = useTerminalDimensions()
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<box>
|
|
277
|
+
<text>
|
|
278
|
+
Terminal dimensions: {width}x{height}
|
|
279
|
+
</text>
|
|
280
|
+
<box style={{ width: Math.floor(width / 2), height: Math.floor(height / 3) }}>
|
|
281
|
+
<text>Half-width, third-height box</text>
|
|
282
|
+
</box>
|
|
283
|
+
</box>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Returns:** An object with `width` and `height` properties representing the current terminal dimensions.
|
|
289
|
+
|
|
290
|
+
#### `useTimeline(options?)`
|
|
291
|
+
|
|
292
|
+
Create and manage animations using OpenTUI's timeline system. This hook automatically registers and unregisters the timeline with the animation engine.
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
import { useTimeline } from "@opentui/react"
|
|
296
|
+
import { useEffect, useState } from "react"
|
|
297
|
+
|
|
298
|
+
function App() {
|
|
299
|
+
const [width, setWidth] = useState(0)
|
|
300
|
+
|
|
301
|
+
const timeline = useTimeline({
|
|
302
|
+
duration: 2000,
|
|
303
|
+
loop: false,
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
timeline.add(
|
|
308
|
+
{
|
|
309
|
+
width,
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
width: 50,
|
|
313
|
+
duration: 2000,
|
|
314
|
+
ease: "linear",
|
|
315
|
+
onUpdate: (animation) => {
|
|
316
|
+
setWidth(animation.targets[0].width)
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
}, [])
|
|
321
|
+
|
|
322
|
+
return <box style={{ width, backgroundColor: "#6a5acd" }} />
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Parameters:**
|
|
327
|
+
|
|
328
|
+
- `options?`: Optional `TimelineOptions` object with properties:
|
|
329
|
+
- `duration?`: Animation duration in milliseconds (default: 1000)
|
|
330
|
+
- `loop?`: Whether the timeline should loop (default: false)
|
|
331
|
+
- `autoplay?`: Whether to automatically start the timeline (default: true)
|
|
332
|
+
- `onComplete?`: Callback when timeline completes
|
|
333
|
+
- `onPause?`: Callback when timeline is paused
|
|
334
|
+
|
|
335
|
+
**Returns:** A `Timeline` instance with methods:
|
|
336
|
+
|
|
337
|
+
- `add(target, properties, startTime)`: Add animation to timeline
|
|
338
|
+
- `play()`: Start the timeline
|
|
339
|
+
- `pause()`: Pause the timeline
|
|
340
|
+
- `restart()`: Restart the timeline from beginning
|
|
341
|
+
|
|
342
|
+
## Components
|
|
343
|
+
|
|
344
|
+
### Layout & Display Components
|
|
345
|
+
|
|
346
|
+
#### Text Component
|
|
347
|
+
|
|
348
|
+
Display text with rich formatting.
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
function App() {
|
|
352
|
+
return (
|
|
353
|
+
<box>
|
|
354
|
+
{/* Simple text */}
|
|
355
|
+
<text>Hello World</text>
|
|
356
|
+
|
|
357
|
+
{/* Rich text with children */}
|
|
358
|
+
<text>
|
|
359
|
+
<span fg="red">Red Text</span>
|
|
360
|
+
</text>
|
|
361
|
+
|
|
362
|
+
{/* Text modifiers */}
|
|
363
|
+
<text>
|
|
364
|
+
<strong>Bold</strong>, <em>Italic</em>, and <u>Underlined</u>
|
|
365
|
+
</text>
|
|
366
|
+
</box>
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
#### Box Component
|
|
372
|
+
|
|
373
|
+
Container with borders and layout capabilities.
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
function App() {
|
|
377
|
+
return (
|
|
378
|
+
<box flexDirection="column">
|
|
379
|
+
{/* Basic box */}
|
|
380
|
+
<box border>
|
|
381
|
+
<text>Simple box</text>
|
|
382
|
+
</box>
|
|
383
|
+
|
|
384
|
+
{/* Box with title and styling */}
|
|
385
|
+
<box title="Settings" border borderStyle="double" padding={2} backgroundColor="blue">
|
|
386
|
+
<text>Box content</text>
|
|
387
|
+
</box>
|
|
388
|
+
|
|
389
|
+
{/* Styled box */}
|
|
390
|
+
<box
|
|
391
|
+
style={{
|
|
392
|
+
border: true,
|
|
393
|
+
width: 40,
|
|
394
|
+
height: 10,
|
|
395
|
+
margin: 1,
|
|
396
|
+
alignItems: "center",
|
|
397
|
+
justifyContent: "center",
|
|
398
|
+
}}
|
|
399
|
+
>
|
|
400
|
+
<text>Centered content</text>
|
|
401
|
+
</box>
|
|
402
|
+
</box>
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### Scrollbox Component
|
|
408
|
+
|
|
409
|
+
A scrollable box.
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
function App() {
|
|
413
|
+
return (
|
|
414
|
+
<scrollbox
|
|
415
|
+
style={{
|
|
416
|
+
rootOptions: {
|
|
417
|
+
backgroundColor: "#24283b",
|
|
418
|
+
},
|
|
419
|
+
wrapperOptions: {
|
|
420
|
+
backgroundColor: "#1f2335",
|
|
421
|
+
},
|
|
422
|
+
viewportOptions: {
|
|
423
|
+
backgroundColor: "#1a1b26",
|
|
424
|
+
},
|
|
425
|
+
contentOptions: {
|
|
426
|
+
backgroundColor: "#16161e",
|
|
427
|
+
},
|
|
428
|
+
scrollbarOptions: {
|
|
429
|
+
showArrows: true,
|
|
430
|
+
trackOptions: {
|
|
431
|
+
foregroundColor: "#7aa2f7",
|
|
432
|
+
backgroundColor: "#414868",
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
}}
|
|
436
|
+
focused
|
|
437
|
+
>
|
|
438
|
+
{Array.from({ length: 1000 }).map((_, i) => (
|
|
439
|
+
<box
|
|
440
|
+
key={i}
|
|
441
|
+
style={{ width: "100%", padding: 1, marginBottom: 1, backgroundColor: i % 2 === 0 ? "#292e42" : "#2f3449" }}
|
|
442
|
+
>
|
|
443
|
+
<text content={`Box ${i}`} />
|
|
444
|
+
</box>
|
|
445
|
+
))}
|
|
446
|
+
</scrollbox>
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
#### ASCII Font Component
|
|
452
|
+
|
|
453
|
+
Display ASCII art text with different font styles.
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
import { useState } from "react"
|
|
457
|
+
|
|
458
|
+
function App() {
|
|
459
|
+
const text = "ASCII"
|
|
460
|
+
const [font, setFont] = useState<"block" | "shade" | "slick" | "tiny">("tiny")
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<box style={{ border: true, paddingLeft: 1, paddingRight: 1 }}>
|
|
464
|
+
<box
|
|
465
|
+
style={{
|
|
466
|
+
height: 8,
|
|
467
|
+
border: true,
|
|
468
|
+
marginBottom: 1,
|
|
469
|
+
}}
|
|
470
|
+
>
|
|
471
|
+
<select
|
|
472
|
+
focused
|
|
473
|
+
onChange={(_, option) => setFont(option?.value)}
|
|
474
|
+
showScrollIndicator
|
|
475
|
+
options={[
|
|
476
|
+
{
|
|
477
|
+
name: "Tiny",
|
|
478
|
+
description: "Tiny font",
|
|
479
|
+
value: "tiny",
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: "Block",
|
|
483
|
+
description: "Block font",
|
|
484
|
+
value: "block",
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: "Slick",
|
|
488
|
+
description: "Slick font",
|
|
489
|
+
value: "slick",
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: "Shade",
|
|
493
|
+
description: "Shade font",
|
|
494
|
+
value: "shade",
|
|
495
|
+
},
|
|
496
|
+
]}
|
|
497
|
+
style={{ flexGrow: 1 }}
|
|
498
|
+
/>
|
|
499
|
+
</box>
|
|
500
|
+
|
|
501
|
+
<ascii-font text={text} font={font} />
|
|
502
|
+
</box>
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Input Components
|
|
508
|
+
|
|
509
|
+
#### Input Component
|
|
510
|
+
|
|
511
|
+
Text input field with event handling.
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
import { useState } from "react"
|
|
515
|
+
|
|
516
|
+
function App() {
|
|
517
|
+
const [value, setValue] = useState("")
|
|
518
|
+
|
|
519
|
+
return (
|
|
520
|
+
<box title="Enter your name" style={{ border: true, height: 3 }}>
|
|
521
|
+
<input
|
|
522
|
+
placeholder="Type here..."
|
|
523
|
+
focused
|
|
524
|
+
onInput={setValue}
|
|
525
|
+
onSubmit={(value) => console.log("Submitted:", value)}
|
|
526
|
+
/>
|
|
527
|
+
</box>
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### Textarea Component
|
|
533
|
+
|
|
534
|
+
```tsx
|
|
535
|
+
import type { TextareaRenderable } from "@opentui/core"
|
|
536
|
+
import { useKeyboard, useRenderer } from "@opentui/react"
|
|
537
|
+
import { useEffect, useRef } from "react"
|
|
538
|
+
|
|
539
|
+
function App() {
|
|
540
|
+
const renderer = useRenderer()
|
|
541
|
+
const textareaRef = useRef<TextareaRenderable>(null)
|
|
542
|
+
|
|
543
|
+
useEffect(() => {
|
|
544
|
+
renderer.console.show()
|
|
545
|
+
}, [renderer])
|
|
546
|
+
|
|
547
|
+
useKeyboard((key) => {
|
|
548
|
+
if (key.name === "return") {
|
|
549
|
+
console.log(textareaRef.current?.plainText)
|
|
550
|
+
}
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
return (
|
|
554
|
+
<box title="Interactive Editor" style={{ border: true, flexGrow: 1 }}>
|
|
555
|
+
<textarea ref={textareaRef} placeholder="Type here..." focused />
|
|
556
|
+
</box>
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
#### Select Component
|
|
562
|
+
|
|
563
|
+
Dropdown selection component.
|
|
564
|
+
|
|
565
|
+
```tsx
|
|
566
|
+
import type { SelectOption } from "@opentui/core"
|
|
567
|
+
import { useState } from "react"
|
|
568
|
+
|
|
569
|
+
function App() {
|
|
570
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
571
|
+
|
|
572
|
+
const options: SelectOption[] = [
|
|
573
|
+
{ name: "Option 1", description: "Option 1 description", value: "opt1" },
|
|
574
|
+
{ name: "Option 2", description: "Option 2 description", value: "opt2" },
|
|
575
|
+
{ name: "Option 3", description: "Option 3 description", value: "opt3" },
|
|
576
|
+
]
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<box style={{ border: true, height: 24 }}>
|
|
580
|
+
<select
|
|
581
|
+
style={{ height: 22 }}
|
|
582
|
+
options={options}
|
|
583
|
+
focused={true}
|
|
584
|
+
onChange={(index, option) => {
|
|
585
|
+
setSelectedIndex(index)
|
|
586
|
+
console.log("Selected:", option)
|
|
587
|
+
}}
|
|
588
|
+
/>
|
|
589
|
+
</box>
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Code & Diff Components
|
|
595
|
+
|
|
596
|
+
#### Code Component
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
import { RGBA, SyntaxStyle } from "@opentui/core"
|
|
600
|
+
|
|
601
|
+
const syntaxStyle = SyntaxStyle.fromStyles({
|
|
602
|
+
keyword: { fg: RGBA.fromHex("#ff6b6b"), bold: true }, // red, bold
|
|
603
|
+
string: { fg: RGBA.fromHex("#51cf66") }, // green
|
|
604
|
+
comment: { fg: RGBA.fromHex("#868e96"), italic: true }, // gray, italic
|
|
605
|
+
number: { fg: RGBA.fromHex("#ffd43b") }, // yellow
|
|
606
|
+
default: { fg: RGBA.fromHex("#ffffff") }, // white
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
const codeExample = `function hello() {
|
|
610
|
+
// This is a comment
|
|
611
|
+
|
|
612
|
+
const message = "Hello, world!"
|
|
613
|
+
const count = 42
|
|
614
|
+
|
|
615
|
+
return message + " " + count
|
|
616
|
+
}`
|
|
617
|
+
|
|
618
|
+
function App() {
|
|
619
|
+
return (
|
|
620
|
+
<box style={{ border: true, flexGrow: 1 }}>
|
|
621
|
+
<code content={codeExample} filetype="javascript" syntaxStyle={syntaxStyle} />
|
|
622
|
+
</box>
|
|
623
|
+
)
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
#### Line Number Component
|
|
628
|
+
|
|
629
|
+
Display code with line numbers, and optionally add diff highlights or diagnostic indicators.
|
|
630
|
+
|
|
631
|
+
```tsx
|
|
632
|
+
import type { LineNumberRenderable } from "@opentui/core"
|
|
633
|
+
import { RGBA, SyntaxStyle } from "@opentui/core"
|
|
634
|
+
import { useEffect, useRef } from "react"
|
|
635
|
+
|
|
636
|
+
function App() {
|
|
637
|
+
const lineNumberRef = useRef<LineNumberRenderable>(null)
|
|
638
|
+
|
|
639
|
+
const syntaxStyle = SyntaxStyle.fromStyles({
|
|
640
|
+
keyword: { fg: RGBA.fromHex("#C792EA") },
|
|
641
|
+
string: { fg: RGBA.fromHex("#C3E88D") },
|
|
642
|
+
number: { fg: RGBA.fromHex("#F78C6C") },
|
|
643
|
+
default: { fg: RGBA.fromHex("#A6ACCD") },
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
const codeContent = `function fibonacci(n: number): number {
|
|
647
|
+
if (n <= 1) return n
|
|
648
|
+
return fibonacci(n - 1) + fibonacci(n - 2)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
console.log(fibonacci(10))`
|
|
652
|
+
|
|
653
|
+
useEffect(() => {
|
|
654
|
+
// Add diff highlight - line was added
|
|
655
|
+
lineNumberRef.current?.setLineColor(1, "#1a4d1a")
|
|
656
|
+
lineNumberRef.current?.setLineSign(1, { after: " +", afterColor: "#22c55e" })
|
|
657
|
+
|
|
658
|
+
// Add diagnostic indicator
|
|
659
|
+
lineNumberRef.current?.setLineSign(4, { before: "⚠️", beforeColor: "#f59e0b" })
|
|
660
|
+
}, [])
|
|
661
|
+
|
|
662
|
+
return (
|
|
663
|
+
<box style={{ border: true, flexGrow: 1 }}>
|
|
664
|
+
<line-number
|
|
665
|
+
ref={lineNumberRef}
|
|
666
|
+
fg="#6b7280"
|
|
667
|
+
bg="#161b22"
|
|
668
|
+
minWidth={3}
|
|
669
|
+
paddingRight={1}
|
|
670
|
+
showLineNumbers={true}
|
|
671
|
+
width="100%"
|
|
672
|
+
height="100%"
|
|
673
|
+
>
|
|
674
|
+
<code content={codeContent} filetype="typescript" syntaxStyle={syntaxStyle} width="100%" height="100%" />
|
|
675
|
+
</line-number>
|
|
676
|
+
</box>
|
|
677
|
+
)
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
For a more complete example with interactive diff highlights and diagnostics, see [`examples/line-number.tsx`](examples/line-number.tsx).
|
|
682
|
+
|
|
683
|
+
#### Diff Component
|
|
684
|
+
|
|
685
|
+
Display unified or split-view diffs with syntax highlighting, customizable themes, and line number support. Supports multiple view modes (unified/split), word wrapping, and theme customization.
|
|
686
|
+
|
|
687
|
+
For a complete interactive example with theme switching and keybindings, see [`examples/diff.tsx`](examples/diff.tsx).
|
|
688
|
+
|
|
689
|
+
## Examples
|
|
690
|
+
|
|
691
|
+
### Login Form
|
|
692
|
+
|
|
693
|
+
```tsx
|
|
694
|
+
import { createCliRenderer } from "@opentui/core"
|
|
695
|
+
import { createRoot, useKeyboard } from "@opentui/react"
|
|
696
|
+
import { useCallback, useState } from "react"
|
|
697
|
+
|
|
698
|
+
function App() {
|
|
699
|
+
const [username, setUsername] = useState("")
|
|
700
|
+
const [password, setPassword] = useState("")
|
|
701
|
+
const [focused, setFocused] = useState<"username" | "password">("username")
|
|
702
|
+
const [status, setStatus] = useState("idle")
|
|
703
|
+
|
|
704
|
+
useKeyboard((key) => {
|
|
705
|
+
if (key.name === "tab") {
|
|
706
|
+
setFocused((prev) => (prev === "username" ? "password" : "username"))
|
|
707
|
+
}
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
const handleSubmit = useCallback(() => {
|
|
711
|
+
if (username === "admin" && password === "secret") {
|
|
712
|
+
setStatus("success")
|
|
713
|
+
} else {
|
|
714
|
+
setStatus("error")
|
|
715
|
+
}
|
|
716
|
+
}, [username, password])
|
|
717
|
+
|
|
718
|
+
return (
|
|
719
|
+
<box style={{ border: true, padding: 2, flexDirection: "column", gap: 1 }}>
|
|
720
|
+
<text fg="#FFFF00">Login Form</text>
|
|
721
|
+
|
|
722
|
+
<box title="Username" style={{ border: true, width: 40, height: 3 }}>
|
|
723
|
+
<input
|
|
724
|
+
placeholder="Enter username..."
|
|
725
|
+
onInput={setUsername}
|
|
726
|
+
onSubmit={handleSubmit}
|
|
727
|
+
focused={focused === "username"}
|
|
728
|
+
/>
|
|
729
|
+
</box>
|
|
730
|
+
|
|
731
|
+
<box title="Password" style={{ border: true, width: 40, height: 3 }}>
|
|
732
|
+
<input
|
|
733
|
+
placeholder="Enter password..."
|
|
734
|
+
onInput={setPassword}
|
|
735
|
+
onSubmit={handleSubmit}
|
|
736
|
+
focused={focused === "password"}
|
|
737
|
+
/>
|
|
738
|
+
</box>
|
|
739
|
+
|
|
740
|
+
<text
|
|
741
|
+
style={{
|
|
742
|
+
fg: status === "success" ? "green" : status === "error" ? "red" : "#999",
|
|
743
|
+
}}
|
|
744
|
+
>
|
|
745
|
+
{status.toUpperCase()}
|
|
746
|
+
</text>
|
|
747
|
+
</box>
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const renderer = await createCliRenderer()
|
|
752
|
+
createRoot(renderer).render(<App />)
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### Counter with Timer
|
|
756
|
+
|
|
757
|
+
```tsx
|
|
758
|
+
import { createCliRenderer } from "@opentui/core"
|
|
759
|
+
import { createRoot } from "@opentui/react"
|
|
760
|
+
import { useEffect, useState } from "react"
|
|
761
|
+
|
|
762
|
+
function App() {
|
|
763
|
+
const [count, setCount] = useState(0)
|
|
764
|
+
|
|
765
|
+
useEffect(() => {
|
|
766
|
+
const interval = setInterval(() => {
|
|
767
|
+
setCount((prev) => prev + 1)
|
|
768
|
+
}, 1000)
|
|
769
|
+
|
|
770
|
+
return () => clearInterval(interval)
|
|
771
|
+
}, [])
|
|
772
|
+
|
|
773
|
+
return (
|
|
774
|
+
<box title="Counter" style={{ padding: 2 }}>
|
|
775
|
+
<text fg="#00FF00">{`Count: ${count}`}</text>
|
|
776
|
+
</box>
|
|
777
|
+
)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const renderer = await createCliRenderer()
|
|
781
|
+
createRoot(renderer).render(<App />)
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
### System Monitor Animation
|
|
785
|
+
|
|
786
|
+
```tsx
|
|
787
|
+
import { createCliRenderer, TextAttributes } from "@opentui/core"
|
|
788
|
+
import { createRoot, useTimeline } from "@opentui/react"
|
|
789
|
+
import { useEffect, useState } from "react"
|
|
790
|
+
|
|
791
|
+
type Stats = {
|
|
792
|
+
cpu: number
|
|
793
|
+
memory: number
|
|
794
|
+
network: number
|
|
795
|
+
disk: number
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
export const App = () => {
|
|
799
|
+
const [stats, setAnimatedStats] = useState<Stats>({
|
|
800
|
+
cpu: 0,
|
|
801
|
+
memory: 0,
|
|
802
|
+
network: 0,
|
|
803
|
+
disk: 0,
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
const timeline = useTimeline({
|
|
807
|
+
duration: 3000,
|
|
808
|
+
loop: false,
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
useEffect(() => {
|
|
812
|
+
timeline.add(
|
|
813
|
+
stats,
|
|
814
|
+
{
|
|
815
|
+
cpu: 85,
|
|
816
|
+
memory: 70,
|
|
817
|
+
network: 95,
|
|
818
|
+
disk: 60,
|
|
819
|
+
duration: 3000,
|
|
820
|
+
ease: "linear",
|
|
821
|
+
onUpdate: (values) => {
|
|
822
|
+
setAnimatedStats({ ...values.targets[0] })
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
0,
|
|
826
|
+
)
|
|
827
|
+
}, [])
|
|
828
|
+
|
|
829
|
+
const statsMap = [
|
|
830
|
+
{ name: "CPU", key: "cpu", color: "#6a5acd" },
|
|
831
|
+
{ name: "Memory", key: "memory", color: "#4682b4" },
|
|
832
|
+
{ name: "Network", key: "network", color: "#20b2aa" },
|
|
833
|
+
{ name: "Disk", key: "disk", color: "#daa520" },
|
|
834
|
+
]
|
|
835
|
+
|
|
836
|
+
return (
|
|
837
|
+
<box
|
|
838
|
+
title="System Monitor"
|
|
839
|
+
style={{
|
|
840
|
+
margin: 1,
|
|
841
|
+
padding: 1,
|
|
842
|
+
border: true,
|
|
843
|
+
marginLeft: 2,
|
|
844
|
+
marginRight: 2,
|
|
845
|
+
borderStyle: "single",
|
|
846
|
+
borderColor: "#4a4a4a",
|
|
847
|
+
}}
|
|
848
|
+
>
|
|
849
|
+
{statsMap.map((stat) => (
|
|
850
|
+
<box key={stat.key}>
|
|
851
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
852
|
+
<text>{stat.name}</text>
|
|
853
|
+
<text attributes={TextAttributes.DIM}>{Math.round(stats[stat.key as keyof Stats])}%</text>
|
|
854
|
+
</box>
|
|
855
|
+
<box style={{ backgroundColor: "#333333" }}>
|
|
856
|
+
<box style={{ width: `${stats[stat.key as keyof Stats]}%`, height: 1, backgroundColor: stat.color }} />
|
|
857
|
+
</box>
|
|
858
|
+
</box>
|
|
859
|
+
))}
|
|
860
|
+
</box>
|
|
861
|
+
)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const renderer = await createCliRenderer()
|
|
865
|
+
createRoot(renderer).render(<App />)
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### Styled Text Showcase
|
|
869
|
+
|
|
870
|
+
```tsx
|
|
871
|
+
import { createCliRenderer } from "@opentui/core"
|
|
872
|
+
import { createRoot } from "@opentui/react"
|
|
873
|
+
|
|
874
|
+
function App() {
|
|
875
|
+
return (
|
|
876
|
+
<>
|
|
877
|
+
<text>Simple text</text>
|
|
878
|
+
<text>
|
|
879
|
+
<strong>Bold text</strong>
|
|
880
|
+
</text>
|
|
881
|
+
<text>
|
|
882
|
+
<u>Underlined text</u>
|
|
883
|
+
</text>
|
|
884
|
+
<text>
|
|
885
|
+
<span fg="red">Red text</span>
|
|
886
|
+
</text>
|
|
887
|
+
<text>
|
|
888
|
+
<span fg="blue">Blue text</span>
|
|
889
|
+
</text>
|
|
890
|
+
<text>
|
|
891
|
+
<strong fg="red">Bold red text</strong>
|
|
892
|
+
</text>
|
|
893
|
+
<text>
|
|
894
|
+
<strong>Bold</strong> and <span fg="blue">blue</span> combined
|
|
895
|
+
</text>
|
|
896
|
+
</>
|
|
897
|
+
)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const renderer = await createCliRenderer()
|
|
901
|
+
createRoot(renderer).render(<App />)
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
## Component Extension
|
|
905
|
+
|
|
906
|
+
You can create custom components by extending OpenTUIs base renderables:
|
|
907
|
+
|
|
908
|
+
```tsx
|
|
909
|
+
import {
|
|
910
|
+
BoxRenderable,
|
|
911
|
+
createCliRenderer,
|
|
912
|
+
OptimizedBuffer,
|
|
913
|
+
RGBA,
|
|
914
|
+
type BoxOptions,
|
|
915
|
+
type RenderContext,
|
|
916
|
+
} from "@opentui/core"
|
|
917
|
+
import { createRoot, extend } from "@opentui/react"
|
|
918
|
+
|
|
919
|
+
// Create custom component class
|
|
920
|
+
class ButtonRenderable extends BoxRenderable {
|
|
921
|
+
private _label: string = "Button"
|
|
922
|
+
|
|
923
|
+
constructor(ctx: RenderContext, options: BoxOptions & { label?: string }) {
|
|
924
|
+
super(ctx, {
|
|
925
|
+
border: true,
|
|
926
|
+
borderStyle: "single",
|
|
927
|
+
minHeight: 3,
|
|
928
|
+
...options,
|
|
929
|
+
})
|
|
930
|
+
|
|
931
|
+
if (options.label) {
|
|
932
|
+
this._label = options.label
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
protected renderSelf(buffer: OptimizedBuffer): void {
|
|
937
|
+
super.renderSelf(buffer)
|
|
938
|
+
|
|
939
|
+
const centerX = this.x + Math.floor(this.width / 2 - this._label.length / 2)
|
|
940
|
+
const centerY = this.y + Math.floor(this.height / 2)
|
|
941
|
+
|
|
942
|
+
buffer.drawText(this._label, centerX, centerY, RGBA.fromInts(255, 255, 255, 255))
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
set label(value: string) {
|
|
946
|
+
this._label = value
|
|
947
|
+
this.requestRender()
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Add TypeScript support
|
|
952
|
+
declare module "@opentui/react" {
|
|
953
|
+
interface OpenTUIComponents {
|
|
954
|
+
consoleButton: typeof ButtonRenderable
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Register the component
|
|
959
|
+
extend({ consoleButton: ButtonRenderable })
|
|
960
|
+
|
|
961
|
+
// Use in JSX
|
|
962
|
+
function App() {
|
|
963
|
+
return (
|
|
964
|
+
<box>
|
|
965
|
+
<consoleButton label="Click me!" style={{ backgroundColor: "blue" }} />
|
|
966
|
+
<consoleButton label="Another button" style={{ backgroundColor: "green" }} />
|
|
967
|
+
</box>
|
|
968
|
+
)
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const renderer = await createCliRenderer()
|
|
972
|
+
createRoot(renderer).render(<App />)
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
## Using React DevTools
|
|
976
|
+
|
|
977
|
+
OpenTUI React supports [React DevTools](https://github.com/facebook/react/tree/master/packages/react-devtools) for debugging your terminal applications. To enable DevTools integration:
|
|
978
|
+
|
|
979
|
+
1. Install the optional peer dependency:
|
|
980
|
+
|
|
981
|
+
```bash
|
|
982
|
+
bun add --dev react-devtools-core@7
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
2. Start the standalone React DevTools:
|
|
986
|
+
|
|
987
|
+
```bash
|
|
988
|
+
npx react-devtools@7
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
3. Run your app with the `DEV` environment variable:
|
|
992
|
+
|
|
993
|
+
```bash
|
|
994
|
+
DEV=true bun run your-app.ts
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
After the app starts, you should see the component tree in React DevTools. You can inspect and modify props in real-time, and changes will be reflected immediately in your terminal UI.
|
|
998
|
+
|
|
999
|
+
### Process Exit with DevTools
|
|
1000
|
+
|
|
1001
|
+
When DevTools is connected, the WebSocket connection may prevent your process from exiting naturally.
|