@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/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.