@effect-tui/react 0.1.0-alpha.1
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 +138 -0
- package/dist/jsx-dev-runtime.d.ts +3 -0
- package/dist/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/jsx-dev-runtime.js +3 -0
- package/dist/jsx-dev-runtime.js.map +1 -0
- package/dist/jsx-runtime.d.ts +47 -0
- package/dist/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx-runtime.js +6 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/src/codeblock.d.ts +9 -0
- package/dist/src/codeblock.d.ts.map +1 -0
- package/dist/src/codeblock.js +24 -0
- package/dist/src/codeblock.js.map +1 -0
- package/dist/src/constants.d.ts +3 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +3 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/debug/DiagnosticsPanel.d.ts +7 -0
- package/dist/src/debug/DiagnosticsPanel.d.ts.map +1 -0
- package/dist/src/debug/DiagnosticsPanel.js +13 -0
- package/dist/src/debug/DiagnosticsPanel.js.map +1 -0
- package/dist/src/highlight.d.ts +20 -0
- package/dist/src/highlight.d.ts.map +1 -0
- package/dist/src/highlight.js +51 -0
- package/dist/src/highlight.js.map +1 -0
- package/dist/src/hooks/index.d.ts +4 -0
- package/dist/src/hooks/index.d.ts.map +1 -0
- package/dist/src/hooks/index.js +3 -0
- package/dist/src/hooks/index.js.map +1 -0
- package/dist/src/hooks/use-keyboard.d.ts +18 -0
- package/dist/src/hooks/use-keyboard.d.ts.map +1 -0
- package/dist/src/hooks/use-keyboard.js +26 -0
- package/dist/src/hooks/use-keyboard.js.map +1 -0
- package/dist/src/hooks/use-paste.d.ts +5 -0
- package/dist/src/hooks/use-paste.d.ts.map +1 -0
- package/dist/src/hooks/use-paste.js +14 -0
- package/dist/src/hooks/use-paste.js.map +1 -0
- package/dist/src/hooks/useFrameStats.d.ts +7 -0
- package/dist/src/hooks/useFrameStats.d.ts.map +1 -0
- package/dist/src/hooks/useFrameStats.js +28 -0
- package/dist/src/hooks/useFrameStats.js.map +1 -0
- package/dist/src/hosts/base.d.ts +22 -0
- package/dist/src/hosts/base.d.ts.map +1 -0
- package/dist/src/hosts/base.js +53 -0
- package/dist/src/hosts/base.js.map +1 -0
- package/dist/src/hosts/box.d.ts +26 -0
- package/dist/src/hosts/box.d.ts.map +1 -0
- package/dist/src/hosts/box.js +84 -0
- package/dist/src/hosts/box.js.map +1 -0
- package/dist/src/hosts/canvas.d.ts +48 -0
- package/dist/src/hosts/canvas.d.ts.map +1 -0
- package/dist/src/hosts/canvas.js +109 -0
- package/dist/src/hosts/canvas.js.map +1 -0
- package/dist/src/hosts/codeblock.d.ts +32 -0
- package/dist/src/hosts/codeblock.d.ts.map +1 -0
- package/dist/src/hosts/codeblock.js +118 -0
- package/dist/src/hosts/codeblock.js.map +1 -0
- package/dist/src/hosts/hstack.d.ts +18 -0
- package/dist/src/hosts/hstack.d.ts.map +1 -0
- package/dist/src/hosts/hstack.js +45 -0
- package/dist/src/hosts/hstack.js.map +1 -0
- package/dist/src/hosts/index.d.ts +16 -0
- package/dist/src/hosts/index.d.ts.map +1 -0
- package/dist/src/hosts/index.js +40 -0
- package/dist/src/hosts/index.js.map +1 -0
- package/dist/src/hosts/spacer.d.ts +19 -0
- package/dist/src/hosts/spacer.d.ts.map +1 -0
- package/dist/src/hosts/spacer.js +28 -0
- package/dist/src/hosts/spacer.js.map +1 -0
- package/dist/src/hosts/text.d.ts +43 -0
- package/dist/src/hosts/text.d.ts.map +1 -0
- package/dist/src/hosts/text.js +148 -0
- package/dist/src/hosts/text.js.map +1 -0
- package/dist/src/hosts/vstack.d.ts +18 -0
- package/dist/src/hosts/vstack.d.ts.map +1 -0
- package/dist/src/hosts/vstack.js +45 -0
- package/dist/src/hosts/vstack.js.map +1 -0
- package/dist/src/hosts/zstack.d.ts +20 -0
- package/dist/src/hosts/zstack.d.ts.map +1 -0
- package/dist/src/hosts/zstack.js +65 -0
- package/dist/src/hosts/zstack.js.map +1 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +20 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/inline/index.d.ts +32 -0
- package/dist/src/inline/index.d.ts.map +1 -0
- package/dist/src/inline/index.js +111 -0
- package/dist/src/inline/index.js.map +1 -0
- package/dist/src/jsx.d.ts +2 -0
- package/dist/src/jsx.d.ts.map +1 -0
- package/dist/src/jsx.js +4 -0
- package/dist/src/jsx.js.map +1 -0
- package/dist/src/motion/color-motion-value.d.ts +32 -0
- package/dist/src/motion/color-motion-value.d.ts.map +1 -0
- package/dist/src/motion/color-motion-value.js +80 -0
- package/dist/src/motion/color-motion-value.js.map +1 -0
- package/dist/src/motion/color.d.ts +30 -0
- package/dist/src/motion/color.d.ts.map +1 -0
- package/dist/src/motion/color.js +172 -0
- package/dist/src/motion/color.js.map +1 -0
- package/dist/src/motion/color.test.d.ts +2 -0
- package/dist/src/motion/color.test.d.ts.map +1 -0
- package/dist/src/motion/color.test.js +97 -0
- package/dist/src/motion/color.test.js.map +1 -0
- package/dist/src/motion/event-emitter.d.ts +18 -0
- package/dist/src/motion/event-emitter.d.ts.map +1 -0
- package/dist/src/motion/event-emitter.js +30 -0
- package/dist/src/motion/event-emitter.js.map +1 -0
- package/dist/src/motion/frame.d.ts +9 -0
- package/dist/src/motion/frame.d.ts.map +1 -0
- package/dist/src/motion/frame.js +51 -0
- package/dist/src/motion/frame.js.map +1 -0
- package/dist/src/motion/hooks.d.ts +75 -0
- package/dist/src/motion/hooks.d.ts.map +1 -0
- package/dist/src/motion/hooks.js +190 -0
- package/dist/src/motion/hooks.js.map +1 -0
- package/dist/src/motion/index.d.ts +4 -0
- package/dist/src/motion/index.d.ts.map +1 -0
- package/dist/src/motion/index.js +7 -0
- package/dist/src/motion/index.js.map +1 -0
- package/dist/src/motion/motion-value.d.ts +40 -0
- package/dist/src/motion/motion-value.d.ts.map +1 -0
- package/dist/src/motion/motion-value.js +109 -0
- package/dist/src/motion/motion-value.js.map +1 -0
- package/dist/src/motion/motion-value.test.d.ts +2 -0
- package/dist/src/motion/motion-value.test.d.ts.map +1 -0
- package/dist/src/motion/motion-value.test.js +177 -0
- package/dist/src/motion/motion-value.test.js.map +1 -0
- package/dist/src/motion/spring-math.d.ts +28 -0
- package/dist/src/motion/spring-math.d.ts.map +1 -0
- package/dist/src/motion/spring-math.js +81 -0
- package/dist/src/motion/spring-math.js.map +1 -0
- package/dist/src/motion/types.d.ts +25 -0
- package/dist/src/motion/types.d.ts.map +1 -0
- package/dist/src/motion/types.js +13 -0
- package/dist/src/motion/types.js.map +1 -0
- package/dist/src/output.d.ts +47 -0
- package/dist/src/output.d.ts.map +1 -0
- package/dist/src/output.js +125 -0
- package/dist/src/output.js.map +1 -0
- package/dist/src/profiler.d.ts +6 -0
- package/dist/src/profiler.d.ts.map +1 -0
- package/dist/src/profiler.js +73 -0
- package/dist/src/profiler.js.map +1 -0
- package/dist/src/reconciler/host-config.d.ts +16 -0
- package/dist/src/reconciler/host-config.d.ts.map +1 -0
- package/dist/src/reconciler/host-config.js +174 -0
- package/dist/src/reconciler/host-config.js.map +1 -0
- package/dist/src/reconciler/types.d.ts +52 -0
- package/dist/src/reconciler/types.d.ts.map +1 -0
- package/dist/src/reconciler/types.js +2 -0
- package/dist/src/reconciler/types.js.map +1 -0
- package/dist/src/renderer.d.ts +101 -0
- package/dist/src/renderer.d.ts.map +1 -0
- package/dist/src/renderer.js +509 -0
- package/dist/src/renderer.js.map +1 -0
- package/dist/src/terminal.d.ts +37 -0
- package/dist/src/terminal.d.ts.map +1 -0
- package/dist/src/terminal.js +65 -0
- package/dist/src/terminal.js.map +1 -0
- package/dist/src/test/index.d.ts +3 -0
- package/dist/src/test/index.d.ts.map +1 -0
- package/dist/src/test/index.js +3 -0
- package/dist/src/test/index.js.map +1 -0
- package/dist/src/test/mock-streams.d.ts +44 -0
- package/dist/src/test/mock-streams.d.ts.map +1 -0
- package/dist/src/test/mock-streams.js +136 -0
- package/dist/src/test/mock-streams.js.map +1 -0
- package/dist/src/test/render-tui.d.ts +47 -0
- package/dist/src/test/render-tui.d.ts.map +1 -0
- package/dist/src/test/render-tui.js +76 -0
- package/dist/src/test/render-tui.js.map +1 -0
- package/dist/src/trace/SpanTree.d.ts +10 -0
- package/dist/src/trace/SpanTree.d.ts.map +1 -0
- package/dist/src/trace/SpanTree.js +104 -0
- package/dist/src/trace/SpanTree.js.map +1 -0
- package/dist/src/trace/index.d.ts +30 -0
- package/dist/src/trace/index.d.ts.map +1 -0
- package/dist/src/trace/index.js +142 -0
- package/dist/src/trace/index.js.map +1 -0
- package/dist/src/trace/location.d.ts +9 -0
- package/dist/src/trace/location.d.ts.map +1 -0
- package/dist/src/trace/location.js +88 -0
- package/dist/src/trace/location.js.map +1 -0
- package/dist/src/trace/span-processor.d.ts +16 -0
- package/dist/src/trace/span-processor.d.ts.map +1 -0
- package/dist/src/trace/span-processor.js +54 -0
- package/dist/src/trace/span-processor.js.map +1 -0
- package/dist/src/trace/span-state.d.ts +79 -0
- package/dist/src/trace/span-state.d.ts.map +1 -0
- package/dist/src/trace/span-state.js +229 -0
- package/dist/src/trace/span-state.js.map +1 -0
- package/dist/src/trace/tui-logger.d.ts +8 -0
- package/dist/src/trace/tui-logger.d.ts.map +1 -0
- package/dist/src/trace/tui-logger.js +70 -0
- package/dist/src/trace/tui-logger.js.map +1 -0
- package/dist/src/utils/border.d.ts +31 -0
- package/dist/src/utils/border.d.ts.map +1 -0
- package/dist/src/utils/border.js +81 -0
- package/dist/src/utils/border.js.map +1 -0
- package/dist/src/utils/flex-layout.d.ts +20 -0
- package/dist/src/utils/flex-layout.d.ts.map +1 -0
- package/dist/src/utils/flex-layout.js +85 -0
- package/dist/src/utils/flex-layout.js.map +1 -0
- package/dist/src/utils/index.d.ts +5 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +5 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/padding.d.ts +26 -0
- package/dist/src/utils/padding.d.ts.map +1 -0
- package/dist/src/utils/padding.js +34 -0
- package/dist/src/utils/padding.js.map +1 -0
- package/dist/src/utils/styles.d.ts +13 -0
- package/dist/src/utils/styles.d.ts.map +1 -0
- package/dist/src/utils/styles.js +5 -0
- package/dist/src/utils/styles.js.map +1 -0
- package/dist/src/visualize/index.d.ts +50 -0
- package/dist/src/visualize/index.d.ts.map +1 -0
- package/dist/src/visualize/index.js +194 -0
- package/dist/src/visualize/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +94 -0
- package/src/codeblock.tsx +47 -0
- package/src/constants.ts +2 -0
- package/src/debug/DiagnosticsPanel.tsx +38 -0
- package/src/highlight.ts +76 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-keyboard.ts +37 -0
- package/src/hooks/use-paste.ts +14 -0
- package/src/hooks/useFrameStats.ts +32 -0
- package/src/hosts/base.ts +65 -0
- package/src/hosts/box.ts +105 -0
- package/src/hosts/canvas.ts +155 -0
- package/src/hosts/codeblock.ts +145 -0
- package/src/hosts/hstack.ts +64 -0
- package/src/hosts/index.ts +45 -0
- package/src/hosts/spacer.ts +40 -0
- package/src/hosts/text.ts +175 -0
- package/src/hosts/vstack.ts +64 -0
- package/src/hosts/zstack.ts +77 -0
- package/src/index.ts +62 -0
- package/src/inline/index.tsx +181 -0
- package/src/jsx.ts +3 -0
- package/src/motion/color-motion-value.ts +90 -0
- package/src/motion/color.test.ts +115 -0
- package/src/motion/color.ts +191 -0
- package/src/motion/event-emitter.ts +35 -0
- package/src/motion/frame.ts +59 -0
- package/src/motion/hooks.ts +237 -0
- package/src/motion/index.ts +17 -0
- package/src/motion/motion-value.test.ts +222 -0
- package/src/motion/motion-value.ts +140 -0
- package/src/motion/spring-math.ts +114 -0
- package/src/motion/types.ts +34 -0
- package/src/output.ts +156 -0
- package/src/profiler.ts +88 -0
- package/src/reconciler/host-config.ts +277 -0
- package/src/reconciler/types.ts +66 -0
- package/src/renderer.ts +661 -0
- package/src/terminal.ts +67 -0
- package/src/test/index.ts +8 -0
- package/src/test/mock-streams.ts +149 -0
- package/src/test/render-tui.ts +118 -0
- package/src/trace/SpanTree.tsx +195 -0
- package/src/trace/index.tsx +205 -0
- package/src/trace/location.ts +90 -0
- package/src/trace/span-processor.ts +65 -0
- package/src/trace/span-state.ts +286 -0
- package/src/trace/tui-logger.ts +72 -0
- package/src/utils/border.ts +108 -0
- package/src/utils/flex-layout.ts +125 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/padding.ts +45 -0
- package/src/utils/styles.ts +14 -0
- package/src/visualize/index.tsx +305 -0
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect, type ReactNode } from "react"
|
|
2
|
+
import { performance } from "node:perf_hooks"
|
|
3
|
+
import { CellBuffer, Palette, decodeKeys, type KeyMsg } from "@effect-tui/core"
|
|
4
|
+
import { reconciler, type Container } from "./reconciler/host-config.js"
|
|
5
|
+
import type { HostContext } from "./reconciler/types.js"
|
|
6
|
+
import { ANSI, Terminal } from "./terminal.js"
|
|
7
|
+
import * as Prof from "./profiler.js"
|
|
8
|
+
import { DEFAULT_FPS } from "./constants.js"
|
|
9
|
+
import {
|
|
10
|
+
emitRowWithReset,
|
|
11
|
+
rowChanged,
|
|
12
|
+
rowContentWidth,
|
|
13
|
+
findChangeWindow,
|
|
14
|
+
contentHeight,
|
|
15
|
+
} from "./output.js"
|
|
16
|
+
|
|
17
|
+
/** Minimal write stream interface for renderer output */
|
|
18
|
+
export interface TuiWriteStream {
|
|
19
|
+
write(s: string): void
|
|
20
|
+
columns: number
|
|
21
|
+
rows: number
|
|
22
|
+
on(event: string, cb: () => void): void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Minimal read stream interface for renderer input */
|
|
26
|
+
export interface TuiReadStream {
|
|
27
|
+
isTTY?: boolean
|
|
28
|
+
setRawMode?(mode: boolean): void
|
|
29
|
+
resume?(): void
|
|
30
|
+
on(event: string, cb: (data: Buffer) => void): void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TuiRenderer {
|
|
34
|
+
/** Terminal width */
|
|
35
|
+
width: number
|
|
36
|
+
/** Terminal height */
|
|
37
|
+
height: number
|
|
38
|
+
/** Request a re-render */
|
|
39
|
+
requestRender(): void
|
|
40
|
+
/** Subscribe to per-frame stats (if enabled). */
|
|
41
|
+
onFrameStats?(handler: (stats: FrameStats) => void): () => void
|
|
42
|
+
/** Subscribe to keyboard events */
|
|
43
|
+
onKey(handler: (key: KeyMsg) => void): () => void
|
|
44
|
+
/** Subscribe to paste events (bracketed paste mode). */
|
|
45
|
+
onPaste?(handler: (text: string) => void): () => void
|
|
46
|
+
/** Subscribe to resize events */
|
|
47
|
+
onResize(handler: (width: number, height: number) => void): () => void
|
|
48
|
+
/** Stop the renderer */
|
|
49
|
+
stop(): void
|
|
50
|
+
/** Manually trigger one render frame (only in manualMode) */
|
|
51
|
+
flush(): void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Internal renderer type with container reference */
|
|
55
|
+
interface TuiRendererInternal extends TuiRenderer {
|
|
56
|
+
_container: Container | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Context for accessing renderer in components
|
|
60
|
+
export const RendererContext = createContext<TuiRenderer | null>(null)
|
|
61
|
+
|
|
62
|
+
export function useRenderer(): TuiRenderer {
|
|
63
|
+
const renderer = useContext(RendererContext)
|
|
64
|
+
if (!renderer) {
|
|
65
|
+
throw new Error("useRenderer must be used within a TUI renderer")
|
|
66
|
+
}
|
|
67
|
+
return renderer
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Hook that returns terminal size and re-renders on resize */
|
|
71
|
+
export function useTerminalSize(): { width: number; height: number } {
|
|
72
|
+
const renderer = useRenderer()
|
|
73
|
+
const [size, setSize] = useState({ width: renderer.width, height: renderer.height })
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
return renderer.onResize((width, height) => {
|
|
77
|
+
setSize({ width, height })
|
|
78
|
+
})
|
|
79
|
+
}, [renderer])
|
|
80
|
+
|
|
81
|
+
return size
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface RendererOptions {
|
|
85
|
+
fps?: number
|
|
86
|
+
stdout?: NodeJS.WriteStream | TuiWriteStream
|
|
87
|
+
stdin?: NodeJS.ReadStream | TuiReadStream
|
|
88
|
+
/** Render mode: "fullscreen" uses alternate buffer, "inline" renders in-place */
|
|
89
|
+
mode?: "fullscreen" | "inline"
|
|
90
|
+
/** Exit the process on Ctrl+C unless preventDefault was called. Defaults to true. */
|
|
91
|
+
exitOnCtrlC?: boolean
|
|
92
|
+
/** Enable diffed rendering (per-line). Defaults to true in runtime, false in manualMode (tests). */
|
|
93
|
+
diff?: boolean
|
|
94
|
+
/** Enable diffed rendering for inline mode (off by default; eraseLines baseline). */
|
|
95
|
+
diffInline?: boolean
|
|
96
|
+
/** Skip automatic render loop. Call flush() manually to render frames. */
|
|
97
|
+
manualMode?: boolean
|
|
98
|
+
/** Skip fullscreen/raw mode setup (for testing) */
|
|
99
|
+
skipTerminalSetup?: boolean
|
|
100
|
+
/** Enable bracketed paste (default true). */
|
|
101
|
+
enablePaste?: boolean
|
|
102
|
+
/** Optional per-frame diagnostics hook. Called after each frame is written. */
|
|
103
|
+
debug?: {
|
|
104
|
+
onFrame?: (stats: FrameStats) => void
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface FrameStats {
|
|
109
|
+
mode: "fullscreen" | "inline"
|
|
110
|
+
width: number
|
|
111
|
+
height: number
|
|
112
|
+
contentHeight: number
|
|
113
|
+
bytes: number
|
|
114
|
+
frameMs: number
|
|
115
|
+
phases: {
|
|
116
|
+
clear: number
|
|
117
|
+
layout: number
|
|
118
|
+
render: number
|
|
119
|
+
diffAnsi: number
|
|
120
|
+
write: number
|
|
121
|
+
}
|
|
122
|
+
timestamp: number
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
126
|
+
const fps = options?.fps ?? DEFAULT_FPS
|
|
127
|
+
const stdout: TuiWriteStream = options?.stdout ?? process.stdout
|
|
128
|
+
const stdin: TuiReadStream = options?.stdin ?? process.stdin
|
|
129
|
+
const mode = options?.mode ?? "fullscreen"
|
|
130
|
+
const exitOnCtrlC = options?.exitOnCtrlC ?? true
|
|
131
|
+
const manualMode = options?.manualMode ?? false
|
|
132
|
+
const enableDiff = options?.diff ?? !manualMode
|
|
133
|
+
const enableDiffInline = options?.diffInline ?? false
|
|
134
|
+
const skipTerminalSetup = options?.skipTerminalSetup ?? false
|
|
135
|
+
const enablePaste = options?.enablePaste ?? true
|
|
136
|
+
const debugHook = options?.debug?.onFrame
|
|
137
|
+
const PASTE_START = "\x1b[200~"
|
|
138
|
+
const PASTE_END = "\x1b[201~"
|
|
139
|
+
const PASTE_ENABLE = "\x1b[?2004h"
|
|
140
|
+
const PASTE_DISABLE = "\x1b[?2004l"
|
|
141
|
+
|
|
142
|
+
let width = stdout.columns || 80
|
|
143
|
+
let height = stdout.rows || 24
|
|
144
|
+
let lastWidth = width // Track for shrink detection (like Ink)
|
|
145
|
+
let dirty = true
|
|
146
|
+
let running = true
|
|
147
|
+
let previousHeight = 0 // For inline mode: track how many lines were rendered
|
|
148
|
+
const printedWidths = new Map<number, number>() // track rightmost printed col per row for inline diff
|
|
149
|
+
const keyHandlers = new Set<(key: KeyMsg) => void>()
|
|
150
|
+
const pasteHandlers = new Set<(text: string) => void>()
|
|
151
|
+
const resizeHandlers = new Set<(width: number, height: number) => void>()
|
|
152
|
+
const frameHandlers = new Set<(stats: FrameStats) => void>()
|
|
153
|
+
let pasteActive = false
|
|
154
|
+
let pasteBuffer = ""
|
|
155
|
+
|
|
156
|
+
const palette = new Palette()
|
|
157
|
+
let prevBuffer: CellBuffer | null = null
|
|
158
|
+
let nextBuffer: CellBuffer | null = null
|
|
159
|
+
let loop: ReturnType<typeof setInterval> | null = null
|
|
160
|
+
|
|
161
|
+
const teardown = () => {
|
|
162
|
+
if (skipTerminalSetup) return
|
|
163
|
+
if (mode === "fullscreen") {
|
|
164
|
+
stdout.write(Terminal.exitFullscreen)
|
|
165
|
+
} else {
|
|
166
|
+
stdout.write("\r\n")
|
|
167
|
+
}
|
|
168
|
+
if (enablePaste) {
|
|
169
|
+
stdout.write(PASTE_DISABLE)
|
|
170
|
+
}
|
|
171
|
+
stdout.write(Terminal.showCursor)
|
|
172
|
+
if (stdin.isTTY && stdin.setRawMode) {
|
|
173
|
+
stdin.setRawMode(false)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The actual render logic, extracted for manual flushing
|
|
178
|
+
const renderFrame = () => {
|
|
179
|
+
const frameStart = Prof.startFrame()
|
|
180
|
+
const frameStartMs = performance.now()
|
|
181
|
+
let clearMs = 0
|
|
182
|
+
let layoutMs = 0
|
|
183
|
+
let renderMs = 0
|
|
184
|
+
let diffAnsiMs = 0
|
|
185
|
+
let writeMs = 0
|
|
186
|
+
let contentH = height
|
|
187
|
+
const container = (renderer as TuiRendererInternal)._container
|
|
188
|
+
const root = container?.root ?? null
|
|
189
|
+
if (!dirty || !root) return
|
|
190
|
+
dirty = false
|
|
191
|
+
|
|
192
|
+
// Ensure buffers exist and are correct size
|
|
193
|
+
if (!prevBuffer || !nextBuffer || prevBuffer.w !== width || prevBuffer.h !== height) {
|
|
194
|
+
prevBuffer = new CellBuffer(width, height)
|
|
195
|
+
nextBuffer = new CellBuffer(width, height)
|
|
196
|
+
prevBuffer.clear(0)
|
|
197
|
+
nextBuffer.clear(0)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Clear next buffer
|
|
201
|
+
let t = Prof.startPhase()
|
|
202
|
+
{
|
|
203
|
+
const t0 = performance.now()
|
|
204
|
+
nextBuffer.clear(0)
|
|
205
|
+
clearMs = performance.now() - t0
|
|
206
|
+
}
|
|
207
|
+
Prof.endPhase("clear", t)
|
|
208
|
+
|
|
209
|
+
// Layout
|
|
210
|
+
t = Prof.startPhase()
|
|
211
|
+
{
|
|
212
|
+
const t0 = performance.now()
|
|
213
|
+
root.measure(width, height)
|
|
214
|
+
root.layout({ x: 0, y: 0, w: width, h: height })
|
|
215
|
+
layoutMs = performance.now() - t0
|
|
216
|
+
}
|
|
217
|
+
Prof.endPhase("layout", t)
|
|
218
|
+
|
|
219
|
+
// Render
|
|
220
|
+
t = Prof.startPhase()
|
|
221
|
+
{
|
|
222
|
+
const t0 = performance.now()
|
|
223
|
+
root.render(nextBuffer, palette)
|
|
224
|
+
renderMs = performance.now() - t0
|
|
225
|
+
}
|
|
226
|
+
Prof.endPhase("render", t)
|
|
227
|
+
|
|
228
|
+
// Output based on mode
|
|
229
|
+
t = Prof.startPhase()
|
|
230
|
+
const diffStartMs = performance.now()
|
|
231
|
+
let output = ""
|
|
232
|
+
|
|
233
|
+
if (mode === "fullscreen") {
|
|
234
|
+
// Fullscreen: optionally diff per line for minimal writes
|
|
235
|
+
if (enableDiff && prevBuffer) {
|
|
236
|
+
for (let y = 0; y < height; y++) {
|
|
237
|
+
if (!rowChanged(prevBuffer, nextBuffer, y, width)) continue
|
|
238
|
+
output += ANSI.cursor.to(1, y + 1)
|
|
239
|
+
output += ANSI.line.clear
|
|
240
|
+
output += emitRowWithReset(nextBuffer, palette, y, width)
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
// Full redraw (tests/manual mode)
|
|
244
|
+
stdout.write(ANSI.cursor.to(1, 1))
|
|
245
|
+
for (let y = 0; y < height; y++) {
|
|
246
|
+
output += emitRowWithReset(nextBuffer, palette, y, width)
|
|
247
|
+
if (y < height - 1) output += "\r\n"
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
// ============================================================
|
|
252
|
+
// INLINE MODE RENDERING
|
|
253
|
+
// ============================================================
|
|
254
|
+
// Renders in-place without alternate buffer. Key insights from Ink:
|
|
255
|
+
// - Track previousHeight to know how many lines to erase
|
|
256
|
+
// - Trim trailing spaces to prevent wrap on terminal shrink
|
|
257
|
+
// - Safety valve: clear terminal if content >= terminal height
|
|
258
|
+
// ============================================================
|
|
259
|
+
|
|
260
|
+
const newHeight = contentHeight(nextBuffer, width, height)
|
|
261
|
+
contentH = newHeight
|
|
262
|
+
|
|
263
|
+
// Safety valve: if previous content would overflow, clear terminal first
|
|
264
|
+
if (previousHeight >= height) {
|
|
265
|
+
stdout.write(ANSI.screen.clear + ANSI.cursor.to(1, 1))
|
|
266
|
+
previousHeight = 0
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Move cursor up to start of previous output (eraseLines pattern)
|
|
270
|
+
if (previousHeight > 0) {
|
|
271
|
+
output += ANSI.cursor.up(previousHeight)
|
|
272
|
+
output += ANSI.cursor.startOfLine
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (enableDiffInline && prevBuffer) {
|
|
276
|
+
// Diff-based inline rendering: only update changed regions
|
|
277
|
+
const rowsToProcess = Math.max(newHeight, previousHeight)
|
|
278
|
+
|
|
279
|
+
for (let y = 0; y < rowsToProcess; y++) {
|
|
280
|
+
// Rows beyond newHeight: clear if previously printed
|
|
281
|
+
if (y >= newHeight) {
|
|
282
|
+
if ((printedWidths.get(y) ?? 0) > 0) {
|
|
283
|
+
output += ANSI.cursor.to(y + 1, 1) + palette.sgr(0) + ANSI.line.clear
|
|
284
|
+
printedWidths.set(y, 0)
|
|
285
|
+
}
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const change = findChangeWindow(prevBuffer, nextBuffer, y, width)
|
|
290
|
+
const newW = rowContentWidth(nextBuffer, y, width)
|
|
291
|
+
const prevW = printedWidths.get(y) ?? 0
|
|
292
|
+
|
|
293
|
+
if (!change) {
|
|
294
|
+
// No change; maybe need to clear tail if content shrunk
|
|
295
|
+
if (prevW > newW) {
|
|
296
|
+
output += ANSI.cursor.to(y + 1, newW + 1) + palette.sgr(0) + ANSI.line.clearToEnd
|
|
297
|
+
printedWidths.set(y, newW)
|
|
298
|
+
}
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Emit changed region [left..right]
|
|
303
|
+
output += ANSI.cursor.to(y + 1, change.left + 1)
|
|
304
|
+
output += emitRowWithReset(nextBuffer, palette, y, width, change.left, change.right + 1)
|
|
305
|
+
|
|
306
|
+
// Clear tail if shrunk
|
|
307
|
+
const effectiveW = Math.max(newW, change.right + 1)
|
|
308
|
+
if (prevW > effectiveW) {
|
|
309
|
+
output += ANSI.cursor.to(y + 1, effectiveW + 1) + ANSI.line.clearToEnd
|
|
310
|
+
}
|
|
311
|
+
printedWidths.set(y, effectiveW)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
previousHeight = newHeight
|
|
315
|
+
} else {
|
|
316
|
+
// Full redraw inline: clear and emit each line, trimming trailing spaces
|
|
317
|
+
for (let y = 0; y < newHeight; y++) {
|
|
318
|
+
output += palette.sgr(0) + ANSI.line.clear
|
|
319
|
+
const trimmedWidth = rowContentWidth(nextBuffer, y, width)
|
|
320
|
+
output += emitRowWithReset(nextBuffer, palette, y, width, 0, trimmedWidth)
|
|
321
|
+
output += "\r\n"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Clear any extra lines if content shrank
|
|
325
|
+
for (let y = newHeight; y < previousHeight; y++) {
|
|
326
|
+
output += palette.sgr(0) + ANSI.line.clear + "\r\n"
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Move cursor back up to end of content
|
|
330
|
+
if (previousHeight > newHeight) {
|
|
331
|
+
output += ANSI.cursor.up(previousHeight - newHeight)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
previousHeight = newHeight
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
output += palette.sgr(0) // Reset style
|
|
339
|
+
diffAnsiMs = performance.now() - diffStartMs
|
|
340
|
+
Prof.endPhase("diff+ansi", t)
|
|
341
|
+
|
|
342
|
+
t = Prof.startPhase()
|
|
343
|
+
{
|
|
344
|
+
const t0 = performance.now()
|
|
345
|
+
stdout.write(output)
|
|
346
|
+
writeMs = performance.now() - t0
|
|
347
|
+
}
|
|
348
|
+
Prof.endPhase("write", t)
|
|
349
|
+
|
|
350
|
+
Prof.endFrame(frameStart)
|
|
351
|
+
const frameMs = performance.now() - frameStartMs
|
|
352
|
+
|
|
353
|
+
// Swap buffers
|
|
354
|
+
const tmp = prevBuffer
|
|
355
|
+
prevBuffer = nextBuffer
|
|
356
|
+
nextBuffer = tmp
|
|
357
|
+
|
|
358
|
+
const stats: FrameStats = {
|
|
359
|
+
mode,
|
|
360
|
+
width,
|
|
361
|
+
height,
|
|
362
|
+
contentHeight: contentH,
|
|
363
|
+
bytes: Buffer.byteLength(output, "utf8"),
|
|
364
|
+
frameMs,
|
|
365
|
+
phases: {
|
|
366
|
+
clear: clearMs,
|
|
367
|
+
layout: layoutMs,
|
|
368
|
+
render: renderMs,
|
|
369
|
+
diffAnsi: diffAnsiMs,
|
|
370
|
+
write: writeMs,
|
|
371
|
+
},
|
|
372
|
+
timestamp: performance.now(),
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (debugHook) debugHook(stats)
|
|
376
|
+
if (frameHandlers.size > 0) {
|
|
377
|
+
for (const handler of frameHandlers) handler(stats)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const renderer: TuiRenderer = {
|
|
382
|
+
get width() {
|
|
383
|
+
return width
|
|
384
|
+
},
|
|
385
|
+
get height() {
|
|
386
|
+
return height
|
|
387
|
+
},
|
|
388
|
+
requestRender() {
|
|
389
|
+
dirty = true
|
|
390
|
+
},
|
|
391
|
+
onKey(handler: (key: KeyMsg) => void) {
|
|
392
|
+
keyHandlers.add(handler)
|
|
393
|
+
return () => keyHandlers.delete(handler)
|
|
394
|
+
},
|
|
395
|
+
onPaste(handler: (text: string) => void) {
|
|
396
|
+
pasteHandlers.add(handler)
|
|
397
|
+
return () => pasteHandlers.delete(handler)
|
|
398
|
+
},
|
|
399
|
+
onResize(handler: (width: number, height: number) => void) {
|
|
400
|
+
resizeHandlers.add(handler)
|
|
401
|
+
return () => resizeHandlers.delete(handler)
|
|
402
|
+
},
|
|
403
|
+
onFrameStats(handler: (stats: FrameStats) => void) {
|
|
404
|
+
frameHandlers.add(handler)
|
|
405
|
+
return () => frameHandlers.delete(handler)
|
|
406
|
+
},
|
|
407
|
+
stop() {
|
|
408
|
+
running = false
|
|
409
|
+
if (loop) {
|
|
410
|
+
clearInterval(loop)
|
|
411
|
+
loop = null
|
|
412
|
+
}
|
|
413
|
+
teardown()
|
|
414
|
+
},
|
|
415
|
+
flush() {
|
|
416
|
+
renderFrame()
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Terminal setup (skip for testing)
|
|
421
|
+
if (!skipTerminalSetup) {
|
|
422
|
+
if (mode === "fullscreen") {
|
|
423
|
+
stdout.write(Terminal.enterFullscreen)
|
|
424
|
+
}
|
|
425
|
+
// Hide cursor during rendering (both modes)
|
|
426
|
+
stdout.write(Terminal.hideCursor)
|
|
427
|
+
if (enablePaste) stdout.write(PASTE_ENABLE)
|
|
428
|
+
|
|
429
|
+
if (stdin.isTTY && stdin.setRawMode) {
|
|
430
|
+
stdin.setRawMode(true)
|
|
431
|
+
stdin.resume?.()
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Handle keyboard input (and bracketed paste)
|
|
436
|
+
stdin.on("data", (data: Buffer) => {
|
|
437
|
+
let chunk = data.toString("utf8")
|
|
438
|
+
|
|
439
|
+
const emitKeys = (str: string) => {
|
|
440
|
+
if (!str) return
|
|
441
|
+
const keys = decodeKeys(Buffer.from(str, "utf8"))
|
|
442
|
+
for (const key of keys) {
|
|
443
|
+
const wrapped: KeyMsg = {
|
|
444
|
+
...key,
|
|
445
|
+
defaultPrevented: false,
|
|
446
|
+
preventDefault() {
|
|
447
|
+
wrapped.defaultPrevented = true
|
|
448
|
+
},
|
|
449
|
+
}
|
|
450
|
+
for (const handler of keyHandlers) {
|
|
451
|
+
if (wrapped.defaultPrevented) break
|
|
452
|
+
handler(wrapped)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Default Ctrl+C handling - exit unless user called preventDefault()
|
|
456
|
+
if (exitOnCtrlC && !wrapped.defaultPrevented && key.ctrl && key.text === "c") {
|
|
457
|
+
process.exit(0)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
while (chunk.length > 0) {
|
|
463
|
+
if (pasteActive) {
|
|
464
|
+
const endIdx = chunk.indexOf(PASTE_END)
|
|
465
|
+
if (endIdx >= 0) {
|
|
466
|
+
pasteBuffer += chunk.slice(0, endIdx)
|
|
467
|
+
pasteHandlers.forEach((h) => h(pasteBuffer))
|
|
468
|
+
pasteBuffer = ""
|
|
469
|
+
pasteActive = false
|
|
470
|
+
chunk = chunk.slice(endIdx + PASTE_END.length)
|
|
471
|
+
continue
|
|
472
|
+
} else {
|
|
473
|
+
pasteBuffer += chunk
|
|
474
|
+
chunk = ""
|
|
475
|
+
break
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const startIdx = chunk.indexOf(PASTE_START)
|
|
480
|
+
if (startIdx >= 0) {
|
|
481
|
+
// Emit any keys before the paste start
|
|
482
|
+
emitKeys(chunk.slice(0, startIdx))
|
|
483
|
+
pasteActive = true
|
|
484
|
+
pasteBuffer = ""
|
|
485
|
+
chunk = chunk.slice(startIdx + PASTE_START.length)
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// No paste markers; treat as normal keys
|
|
490
|
+
emitKeys(chunk)
|
|
491
|
+
chunk = ""
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
dirty = true
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
// Handle resize - render synchronously like Ink does
|
|
498
|
+
stdout.on("resize", () => {
|
|
499
|
+
const newWidth = stdout.columns || 80
|
|
500
|
+
const newHeight = stdout.rows || 24
|
|
501
|
+
|
|
502
|
+
// Fullscreen: clear entire screen on resize to prevent artifacts from terminal reflow
|
|
503
|
+
// The terminal may leave stale content that our diff-based rendering doesn't see
|
|
504
|
+
if (mode === "fullscreen") {
|
|
505
|
+
stdout.write(ANSI.screen.clear + ANSI.cursor.to(1, 1))
|
|
506
|
+
} else if (mode === "inline" && newWidth < lastWidth && previousHeight > 0) {
|
|
507
|
+
// Inline: on width shrink, previous content may have wrapped to MORE lines
|
|
508
|
+
// than previousHeight. Clear from content start to END OF SCREEN to catch all.
|
|
509
|
+
stdout.write(ANSI.cursor.up(previousHeight) + ANSI.cursor.startOfLine)
|
|
510
|
+
stdout.write(ANSI.screen.clearToEnd)
|
|
511
|
+
previousHeight = 0
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
width = newWidth
|
|
515
|
+
height = newHeight
|
|
516
|
+
lastWidth = newWidth
|
|
517
|
+
prevBuffer = null
|
|
518
|
+
nextBuffer = null
|
|
519
|
+
dirty = true
|
|
520
|
+
|
|
521
|
+
// Notify resize handlers
|
|
522
|
+
for (const handler of resizeHandlers) {
|
|
523
|
+
handler(newWidth, newHeight)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Render immediately on resize (like Ink) to prevent cursor corruption
|
|
527
|
+
renderFrame()
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
// Automatic render loop (skip in manual mode)
|
|
531
|
+
if (!manualMode) {
|
|
532
|
+
const frameMs = 1000 / fps
|
|
533
|
+
loop = setInterval(() => {
|
|
534
|
+
if (!running) {
|
|
535
|
+
if (loop) clearInterval(loop)
|
|
536
|
+
teardown()
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
renderFrame()
|
|
540
|
+
}, frameMs)
|
|
541
|
+
}
|
|
542
|
+
// Store container reference for direct root access
|
|
543
|
+
;(renderer as TuiRendererInternal)._container = null
|
|
544
|
+
|
|
545
|
+
return renderer
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export interface Root {
|
|
549
|
+
render(element: ReactNode, sync?: boolean): void
|
|
550
|
+
unmount(): void
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function createRoot(renderer: TuiRenderer): Root {
|
|
554
|
+
const hostContext: HostContext = {
|
|
555
|
+
requestRender: () => renderer.requestRender(),
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const container: Container = {
|
|
559
|
+
root: null,
|
|
560
|
+
ctx: hostContext,
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const fiberRoot = reconciler.createContainer(
|
|
564
|
+
container,
|
|
565
|
+
0, // LegacyRoot
|
|
566
|
+
null, // hydrationCallbacks
|
|
567
|
+
false, // isStrictMode
|
|
568
|
+
null, // concurrentUpdatesByDefaultOverride
|
|
569
|
+
"", // identifierPrefix
|
|
570
|
+
(err: Error) => console.error(err), // onUncaughtError
|
|
571
|
+
(err: Error) => console.error(err), // onCaughtError
|
|
572
|
+
(err: Error) => console.error(err), // onRecoverableError
|
|
573
|
+
() => {}, // onDefaultTransitionIndicator
|
|
574
|
+
null, // transitionCallbacks
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
// Give renderer direct access to container
|
|
578
|
+
;(renderer as TuiRendererInternal)._container = container
|
|
579
|
+
|
|
580
|
+
const reconcilerAny: any = reconciler
|
|
581
|
+
const runSync = reconcilerAny.flushSync?.bind(reconcilerAny) ?? ((fn: () => void) => fn())
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
render(element: ReactNode, sync = false) {
|
|
585
|
+
const wrapped = React.createElement(RendererContext.Provider, { value: renderer }, element)
|
|
586
|
+
if (sync) {
|
|
587
|
+
runSync(() => {
|
|
588
|
+
reconciler.updateContainer(wrapped, fiberRoot, null, null)
|
|
589
|
+
})
|
|
590
|
+
renderer.requestRender()
|
|
591
|
+
} else {
|
|
592
|
+
reconciler.updateContainer(wrapped, fiberRoot, null, () => {
|
|
593
|
+
renderer.requestRender()
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
unmount() {
|
|
598
|
+
reconciler.updateContainer(null, fiberRoot, null, () => {
|
|
599
|
+
renderer.stop()
|
|
600
|
+
})
|
|
601
|
+
},
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// High-level convenience API (Ink-style)
|
|
606
|
+
export interface RenderInstance {
|
|
607
|
+
renderer: TuiRenderer
|
|
608
|
+
root: Root
|
|
609
|
+
rerender(element: ReactNode): void
|
|
610
|
+
unmount(): void
|
|
611
|
+
waitUntilExit(): Promise<void>
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Render a React tree to the terminal in one call.
|
|
616
|
+
* Returns helpers similar to Ink: rerender, unmount, waitUntilExit.
|
|
617
|
+
*/
|
|
618
|
+
export function render(element: ReactNode, options?: RendererOptions): RenderInstance {
|
|
619
|
+
const renderer = createRenderer(options)
|
|
620
|
+
const root = createRoot(renderer)
|
|
621
|
+
|
|
622
|
+
// Initial render (sync to avoid flicker before the loop runs)
|
|
623
|
+
root.render(element, true)
|
|
624
|
+
|
|
625
|
+
let resolved = false
|
|
626
|
+
let resolveExit: (() => void) | null = null
|
|
627
|
+
const exitPromise = new Promise<void>((resolve) => {
|
|
628
|
+
resolveExit = () => {
|
|
629
|
+
if (resolved) return
|
|
630
|
+
resolved = true
|
|
631
|
+
resolve()
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
const onExit = () => {
|
|
636
|
+
if (!resolved) {
|
|
637
|
+
renderer.stop()
|
|
638
|
+
resolveExit?.()
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
process.once("exit", onExit)
|
|
642
|
+
|
|
643
|
+
const unmount = () => {
|
|
644
|
+
process.off("exit", onExit)
|
|
645
|
+
renderer.stop()
|
|
646
|
+
resolveExit?.()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const rerender = (next: ReactNode) => {
|
|
650
|
+
if (resolved) return
|
|
651
|
+
root.render(next)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
renderer,
|
|
656
|
+
root,
|
|
657
|
+
rerender,
|
|
658
|
+
unmount,
|
|
659
|
+
waitUntilExit: () => exitPromise,
|
|
660
|
+
}
|
|
661
|
+
}
|