@effect-tui/react 0.15.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/src/components/ListView.d.ts +4 -4
- package/dist/src/components/ListView.d.ts.map +1 -1
- package/dist/src/components/ListView.js +16 -17
- package/dist/src/components/ListView.js.map +1 -1
- package/dist/src/console/ConsolePopover.d.ts +7 -1
- package/dist/src/console/ConsolePopover.d.ts.map +1 -1
- package/dist/src/console/ConsolePopover.js +55 -74
- package/dist/src/console/ConsolePopover.js.map +1 -1
- package/dist/src/debug/DebugOverlay.d.ts.map +1 -1
- package/dist/src/debug/DebugOverlay.js +3 -57
- package/dist/src/debug/DebugOverlay.js.map +1 -1
- package/dist/src/debug/DiagnosticsPanel.js +1 -1
- package/dist/src/debug/DiagnosticsPanel.js.map +1 -1
- package/dist/src/dev.d.ts +5 -117
- package/dist/src/dev.d.ts.map +1 -1
- package/dist/src/dev.js +3 -333
- package/dist/src/dev.js.map +1 -1
- package/dist/src/hooks/use-scroll.d.ts +31 -35
- package/dist/src/hooks/use-scroll.d.ts.map +1 -1
- package/dist/src/hooks/use-scroll.js +51 -90
- package/dist/src/hooks/use-scroll.js.map +1 -1
- package/dist/src/hosts/canvas.d.ts +2 -2
- package/dist/src/hosts/canvas.d.ts.map +1 -1
- package/dist/src/hosts/canvas.js +8 -10
- package/dist/src/hosts/canvas.js.map +1 -1
- package/dist/src/hosts/codeblock.d.ts +2 -2
- package/dist/src/hosts/codeblock.js +2 -2
- package/dist/src/hosts/flex-container.d.ts +1 -1
- package/dist/src/hosts/flex-container.d.ts.map +1 -1
- package/dist/src/hosts/flex-container.js +3 -3
- package/dist/src/hosts/flex-container.js.map +1 -1
- package/dist/src/hosts/index.d.ts +2 -1
- package/dist/src/hosts/index.d.ts.map +1 -1
- package/dist/src/hosts/index.js +2 -1
- package/dist/src/hosts/index.js.map +1 -1
- package/dist/src/hosts/layout-helpers.d.ts +10 -0
- package/dist/src/hosts/layout-helpers.d.ts.map +1 -0
- package/dist/src/hosts/layout-helpers.js +10 -0
- package/dist/src/hosts/layout-helpers.js.map +1 -0
- package/dist/src/hosts/leaf.d.ts +14 -0
- package/dist/src/hosts/leaf.d.ts.map +1 -0
- package/dist/src/hosts/leaf.js +31 -0
- package/dist/src/hosts/leaf.js.map +1 -0
- package/dist/src/hosts/overlay.d.ts.map +1 -1
- package/dist/src/hosts/overlay.js +4 -7
- package/dist/src/hosts/overlay.js.map +1 -1
- package/dist/src/hosts/scroll.d.ts +47 -24
- package/dist/src/hosts/scroll.d.ts.map +1 -1
- package/dist/src/hosts/scroll.js +68 -51
- package/dist/src/hosts/scroll.js.map +1 -1
- package/dist/src/hosts/spacer.d.ts +2 -2
- package/dist/src/hosts/spacer.js +2 -2
- package/dist/src/hosts/text.d.ts +2 -3
- package/dist/src/hosts/text.d.ts.map +1 -1
- package/dist/src/hosts/text.js +5 -61
- package/dist/src/hosts/text.js.map +1 -1
- package/dist/src/hosts/vstack.js +1 -1
- package/dist/src/hosts/vstack.js.map +1 -1
- package/dist/src/hosts/zstack.d.ts +1 -1
- package/dist/src/hosts/zstack.d.ts.map +1 -1
- package/dist/src/hosts/zstack.js +6 -6
- package/dist/src/hosts/zstack.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/internal/dev/hmr.d.ts +20 -0
- package/dist/src/internal/dev/hmr.d.ts.map +1 -0
- package/dist/src/internal/dev/hmr.js +93 -0
- package/dist/src/internal/dev/hmr.js.map +1 -0
- package/dist/src/internal/dev/runtime.d.ts +24 -0
- package/dist/src/internal/dev/runtime.d.ts.map +1 -0
- package/dist/src/internal/dev/runtime.js +135 -0
- package/dist/src/internal/dev/runtime.js.map +1 -0
- package/dist/src/internal/dev/ui.d.ts +13 -0
- package/dist/src/internal/dev/ui.d.ts.map +1 -0
- package/dist/src/internal/dev/ui.js +51 -0
- package/dist/src/internal/dev/ui.js.map +1 -0
- package/dist/src/internal/renderer/context.d.ts +9 -0
- package/dist/src/internal/renderer/context.d.ts.map +1 -0
- package/dist/src/internal/renderer/context.js +22 -0
- package/dist/src/internal/renderer/context.js.map +1 -0
- package/dist/src/internal/renderer/core/FrameBuilder.d.ts +18 -0
- package/dist/src/internal/renderer/core/FrameBuilder.d.ts.map +1 -0
- package/dist/src/internal/renderer/core/FrameBuilder.js +40 -0
- package/dist/src/internal/renderer/core/FrameBuilder.js.map +1 -0
- package/dist/src/internal/renderer/core/RendererState.d.ts +41 -0
- package/dist/src/internal/renderer/core/RendererState.d.ts.map +1 -0
- package/dist/src/internal/renderer/core/RendererState.js +70 -0
- package/dist/src/internal/renderer/core/RendererState.js.map +1 -0
- package/dist/src/internal/renderer/core/index.d.ts +3 -0
- package/dist/src/internal/renderer/core/index.d.ts.map +1 -0
- package/dist/src/internal/renderer/core/index.js +3 -0
- package/dist/src/internal/renderer/core/index.js.map +1 -0
- package/dist/src/internal/renderer/index.d.ts +40 -0
- package/dist/src/internal/renderer/index.d.ts.map +1 -0
- package/dist/src/internal/renderer/index.js +518 -0
- package/dist/src/internal/renderer/index.js.map +1 -0
- package/dist/src/internal/renderer/input/InputProcessor.d.ts +30 -0
- package/dist/src/internal/renderer/input/InputProcessor.d.ts.map +1 -0
- package/dist/src/internal/renderer/input/InputProcessor.js +122 -0
- package/dist/src/internal/renderer/input/InputProcessor.js.map +1 -0
- package/dist/src/internal/renderer/input/index.d.ts +2 -0
- package/dist/src/internal/renderer/input/index.d.ts.map +1 -0
- package/dist/src/internal/renderer/input/index.js +2 -0
- package/dist/src/internal/renderer/input/index.js.map +1 -0
- package/dist/src/internal/renderer/lifecycle/EventBus.d.ts +42 -0
- package/dist/src/internal/renderer/lifecycle/EventBus.d.ts.map +1 -0
- package/dist/src/internal/renderer/lifecycle/EventBus.js +97 -0
- package/dist/src/internal/renderer/lifecycle/EventBus.js.map +1 -0
- package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts +13 -0
- package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts.map +1 -0
- package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js +111 -0
- package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js.map +1 -0
- package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts +3 -0
- package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts.map +1 -0
- package/dist/src/internal/renderer/lifecycle/RenderCache.js +9 -0
- package/dist/src/internal/renderer/lifecycle/RenderCache.js.map +1 -0
- package/dist/src/internal/renderer/lifecycle/index.d.ts +4 -0
- package/dist/src/internal/renderer/lifecycle/index.d.ts.map +1 -0
- package/dist/src/internal/renderer/lifecycle/index.js +4 -0
- package/dist/src/internal/renderer/lifecycle/index.js.map +1 -0
- package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts +12 -0
- package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts.map +1 -0
- package/dist/src/internal/renderer/modes/FullscreenRenderer.js +54 -0
- package/dist/src/internal/renderer/modes/FullscreenRenderer.js.map +1 -0
- package/dist/src/internal/renderer/modes/InlineRenderer.d.ts +25 -0
- package/dist/src/internal/renderer/modes/InlineRenderer.d.ts.map +1 -0
- package/dist/src/internal/renderer/modes/InlineRenderer.js +166 -0
- package/dist/src/internal/renderer/modes/InlineRenderer.js.map +1 -0
- package/dist/src/internal/renderer/modes/RendererMode.d.ts +42 -0
- package/dist/src/internal/renderer/modes/RendererMode.d.ts.map +1 -0
- package/dist/src/internal/renderer/modes/RendererMode.js +2 -0
- package/dist/src/internal/renderer/modes/RendererMode.js.map +1 -0
- package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts +25 -0
- package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts.map +1 -0
- package/dist/src/internal/renderer/modes/StaticContentRenderer.js +49 -0
- package/dist/src/internal/renderer/modes/StaticContentRenderer.js.map +1 -0
- package/dist/src/internal/renderer/modes/index.d.ts +5 -0
- package/dist/src/internal/renderer/modes/index.d.ts.map +1 -0
- package/dist/src/internal/renderer/modes/index.js +4 -0
- package/dist/src/internal/renderer/modes/index.js.map +1 -0
- package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts +13 -0
- package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts.map +1 -0
- package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js +75 -0
- package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js.map +1 -0
- package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts +29 -0
- package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts.map +1 -0
- package/dist/src/internal/renderer/terminal/TerminalSetup.js +82 -0
- package/dist/src/internal/renderer/terminal/TerminalSetup.js.map +1 -0
- package/dist/src/internal/renderer/terminal/index.d.ts +3 -0
- package/dist/src/internal/renderer/terminal/index.d.ts.map +1 -0
- package/dist/src/internal/renderer/terminal/index.js +3 -0
- package/dist/src/internal/renderer/terminal/index.js.map +1 -0
- package/dist/src/internal/renderer/types.d.ts +118 -0
- package/dist/src/internal/renderer/types.d.ts.map +1 -0
- package/dist/src/internal/renderer/types.js +2 -0
- package/dist/src/internal/renderer/types.js.map +1 -0
- package/dist/src/renderer-context.d.ts +1 -8
- package/dist/src/renderer-context.d.ts.map +1 -1
- package/dist/src/renderer-context.js +1 -21
- package/dist/src/renderer-context.js.map +1 -1
- package/dist/src/renderer-types.d.ts +1 -115
- package/dist/src/renderer-types.d.ts.map +1 -1
- package/dist/src/renderer.d.ts +1 -31
- package/dist/src/renderer.d.ts.map +1 -1
- package/dist/src/renderer.js +1 -495
- package/dist/src/renderer.js.map +1 -1
- package/dist/src/test/render-tui.d.ts +3 -3
- package/dist/src/test/render-tui.d.ts.map +1 -1
- package/dist/src/test/render-tui.js +16 -9
- package/dist/src/test/render-tui.js.map +1 -1
- package/dist/src/utils/alignment.d.ts +1 -1
- package/dist/src/utils/alignment.d.ts.map +1 -1
- package/dist/src/utils/alignment.js +0 -2
- package/dist/src/utils/alignment.js.map +1 -1
- package/dist/src/utils/console-helpers.d.ts +19 -0
- package/dist/src/utils/console-helpers.d.ts.map +1 -0
- package/dist/src/utils/console-helpers.js +61 -0
- package/dist/src/utils/console-helpers.js.map +1 -0
- package/dist/src/utils/index.d.ts +1 -1
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +1 -1
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/styles.d.ts +8 -1
- package/dist/src/utils/styles.d.ts.map +1 -1
- package/dist/src/utils/styles.js +10 -8
- package/dist/src/utils/styles.js.map +1 -1
- package/dist/src/utils/text-wrap.d.ts +5 -0
- package/dist/src/utils/text-wrap.d.ts.map +1 -1
- package/dist/src/utils/text-wrap.js +110 -48
- package/dist/src/utils/text-wrap.js.map +1 -1
- package/dist/src/visualize/index.js +1 -1
- package/dist/src/visualize/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/ListView.tsx +21 -23
- package/src/console/ConsolePopover.tsx +124 -107
- package/src/debug/DebugOverlay.ts +15 -74
- package/src/debug/DiagnosticsPanel.tsx +1 -1
- package/src/dev.tsx +5 -458
- package/src/hooks/use-scroll.ts +85 -145
- package/src/hosts/canvas.ts +8 -11
- package/src/hosts/codeblock.ts +2 -2
- package/src/hosts/flex-container.ts +4 -4
- package/src/hosts/index.ts +10 -1
- package/src/hosts/layout-helpers.ts +20 -0
- package/src/hosts/leaf.ts +36 -0
- package/src/hosts/overlay.ts +11 -9
- package/src/hosts/scroll.ts +94 -69
- package/src/hosts/spacer.ts +2 -2
- package/src/hosts/text.ts +5 -58
- package/src/hosts/vstack.ts +1 -1
- package/src/hosts/zstack.ts +7 -7
- package/src/index.ts +1 -1
- package/src/internal/dev/hmr.ts +101 -0
- package/src/internal/dev/runtime.ts +170 -0
- package/src/internal/dev/ui.tsx +87 -0
- package/src/internal/renderer/context.ts +27 -0
- package/src/{renderer → internal/renderer}/core/FrameBuilder.ts +2 -2
- package/src/internal/renderer/index.ts +656 -0
- package/src/{renderer → internal/renderer}/input/InputProcessor.ts +10 -1
- package/src/{renderer → internal/renderer}/lifecycle/EventBus.ts +9 -1
- package/src/internal/renderer/lifecycle/ProcessLifecycle.ts +125 -0
- package/src/internal/renderer/lifecycle/index.ts +3 -0
- package/src/{renderer → internal/renderer}/modes/InlineRenderer.ts +5 -2
- package/src/{renderer → internal/renderer}/modes/RendererMode.ts +1 -1
- package/src/{renderer → internal/renderer}/modes/StaticContentRenderer.ts +5 -2
- package/src/internal/renderer/terminal/KeyboardCapabilityProbe.ts +91 -0
- package/src/{renderer/lifecycle → internal/renderer/terminal}/TerminalSetup.ts +4 -22
- package/src/internal/renderer/terminal/index.ts +2 -0
- package/src/internal/renderer/types.ts +125 -0
- package/src/renderer-context.ts +1 -27
- package/src/renderer-types.ts +10 -123
- package/src/renderer.ts +1 -619
- package/src/test/render-tui.ts +16 -10
- package/src/utils/alignment.ts +1 -3
- package/src/utils/console-helpers.ts +86 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/styles.ts +16 -4
- package/src/utils/text-wrap.ts +139 -48
- package/src/visualize/index.tsx +1 -1
- package/src/renderer/lifecycle/ResizeManager.ts +0 -65
- package/src/renderer/lifecycle/index.ts +0 -4
- /package/src/{renderer → internal/renderer}/core/RendererState.ts +0 -0
- /package/src/{renderer → internal/renderer}/core/index.ts +0 -0
- /package/src/{renderer → internal/renderer}/input/index.ts +0 -0
- /package/src/{renderer → internal/renderer}/lifecycle/RenderCache.ts +0 -0
- /package/src/{renderer → internal/renderer}/modes/FullscreenRenderer.ts +0 -0
- /package/src/{renderer → internal/renderer}/modes/index.ts +0 -0
package/src/renderer.ts
CHANGED
|
@@ -1,619 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { fileURLToPath } from "node:url"
|
|
3
|
-
import { ANSI, bufferToString, type KeyMsg, type MouseMsg } from "@effect-tui/core"
|
|
4
|
-
import React, { type ReactNode } from "react"
|
|
5
|
-
import { createTerminalWriter, writeToTerminal } from "./console/ConsoleCapture.js"
|
|
6
|
-
import { DEFAULT_FPS } from "./constants.js"
|
|
7
|
-
import { requestExit } from "./exit.js"
|
|
8
|
-
import * as Prof from "./profiler.js"
|
|
9
|
-
import { flushSync, reconciler } from "./reconciler/host-config.js"
|
|
10
|
-
import type { HostContext } from "./reconciler/types.js"
|
|
11
|
-
// Extracted modules
|
|
12
|
-
import { FrameBuilder, RendererState } from "./renderer/core/index.js"
|
|
13
|
-
import { InputProcessor } from "./renderer/input/index.js"
|
|
14
|
-
import { EventBus, getRenderCache, TerminalSetup } from "./renderer/lifecycle/index.js"
|
|
15
|
-
import { FullscreenRenderer, InlineRenderer, StaticContentRenderer } from "./renderer/modes/index.js"
|
|
16
|
-
import { RendererContext } from "./renderer-context.js"
|
|
17
|
-
import { startDevRuntime, type DevOptions } from "./dev.js"
|
|
18
|
-
import type {
|
|
19
|
-
Container,
|
|
20
|
-
FrameStats,
|
|
21
|
-
PasteMsg,
|
|
22
|
-
RendererOptions,
|
|
23
|
-
TuiReadStream,
|
|
24
|
-
TuiRenderer,
|
|
25
|
-
TuiRendererInternal,
|
|
26
|
-
TuiWriteStream,
|
|
27
|
-
} from "./renderer-types.js"
|
|
28
|
-
|
|
29
|
-
export { RendererContext, useRenderer, useTerminalSize } from "./renderer-context.js"
|
|
30
|
-
// Re-export types and context for backwards compatibility
|
|
31
|
-
export type {
|
|
32
|
-
FrameStats,
|
|
33
|
-
PasteMsg,
|
|
34
|
-
RendererOptions,
|
|
35
|
-
TuiReadStream,
|
|
36
|
-
TuiRenderer,
|
|
37
|
-
TuiWriteStream,
|
|
38
|
-
} from "./renderer-types.js"
|
|
39
|
-
|
|
40
|
-
type HandledSignal = "SIGINT" | "SIGTERM"
|
|
41
|
-
|
|
42
|
-
export function createRenderer(options?: RendererOptions): TuiRenderer {
|
|
43
|
-
const fps = options?.fps ?? DEFAULT_FPS
|
|
44
|
-
// Use custom stdout if provided, otherwise use process.stdout with bypassed capture
|
|
45
|
-
let stdout: TuiWriteStream
|
|
46
|
-
if (options?.stdout) {
|
|
47
|
-
// Use custom stdout as-is (e.g., for testing with MockStdout)
|
|
48
|
-
stdout = options.stdout
|
|
49
|
-
} else {
|
|
50
|
-
// Create a proxy that bypasses console capture for writes
|
|
51
|
-
// This ensures Effect.log etc. don't corrupt the TUI
|
|
52
|
-
const terminalWrite = createTerminalWriter()
|
|
53
|
-
stdout = new Proxy(process.stdout as TuiWriteStream, {
|
|
54
|
-
get(target, prop) {
|
|
55
|
-
if (prop === "write") {
|
|
56
|
-
return terminalWrite
|
|
57
|
-
}
|
|
58
|
-
const value = (target as unknown as Record<string | symbol, unknown>)[prop]
|
|
59
|
-
// Bind methods to preserve `this` context (critical for .on(), .removeListener(), etc.)
|
|
60
|
-
if (typeof value === "function") {
|
|
61
|
-
return value.bind(target)
|
|
62
|
-
}
|
|
63
|
-
return value
|
|
64
|
-
},
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
const stdin: TuiReadStream = options?.stdin ?? process.stdin
|
|
68
|
-
const mode = options?.mode ?? "fullscreen"
|
|
69
|
-
const exitOnCtrlC = options?.exitOnCtrlC ?? true
|
|
70
|
-
const handleSignals = options?.handleSignals ?? true
|
|
71
|
-
const exitOnSignal = options?.exitOnSignal ?? true
|
|
72
|
-
const signalExitCodes: Record<HandledSignal, number> = {
|
|
73
|
-
SIGINT: 130,
|
|
74
|
-
SIGTERM: 143,
|
|
75
|
-
...options?.signalExitCodes,
|
|
76
|
-
}
|
|
77
|
-
const manualMode = options?.manualMode ?? false
|
|
78
|
-
const enableDiff = options?.diff ?? !manualMode
|
|
79
|
-
const skipTerminalSetup = options?.skipTerminalSetup ?? false
|
|
80
|
-
const enablePaste = options?.enablePaste ?? true
|
|
81
|
-
const enableMouse = options?.enableMouse ?? mode === "fullscreen"
|
|
82
|
-
const debugHook = options?.debug?.onFrame
|
|
83
|
-
|
|
84
|
-
// Initialize state
|
|
85
|
-
const state = new RendererState(stdout.columns || 80, stdout.rows || 24)
|
|
86
|
-
const events = new EventBus()
|
|
87
|
-
const frameBuilder = new FrameBuilder()
|
|
88
|
-
|
|
89
|
-
// Terminal setup/teardown
|
|
90
|
-
const terminal = new TerminalSetup(stdout, stdin, {
|
|
91
|
-
mode,
|
|
92
|
-
enablePaste,
|
|
93
|
-
enableMouse,
|
|
94
|
-
skipTerminalSetup,
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
// Render mode (fullscreen or inline)
|
|
98
|
-
const renderMode = mode === "fullscreen" ? new FullscreenRenderer() : new InlineRenderer()
|
|
99
|
-
|
|
100
|
-
// Static content renderer (inline mode only)
|
|
101
|
-
const staticRenderer = mode === "inline" ? new StaticContentRenderer(stdout, state.palette) : null
|
|
102
|
-
|
|
103
|
-
// Input processing
|
|
104
|
-
const inputProcessor = new InputProcessor({
|
|
105
|
-
exitOnCtrlC,
|
|
106
|
-
dispatchKey: (key) => {
|
|
107
|
-
events.dispatchKey(key)
|
|
108
|
-
return key.defaultPrevented ?? false
|
|
109
|
-
},
|
|
110
|
-
dispatchMouse: (mouse) => events.dispatchMouse(mouse),
|
|
111
|
-
dispatchPaste: (text) => events.dispatchPaste(text),
|
|
112
|
-
flushSync: (fn) => flushSync(fn) ?? (undefined as never),
|
|
113
|
-
onInputProcessed: () => {
|
|
114
|
-
if (!manualMode) renderFrame()
|
|
115
|
-
},
|
|
116
|
-
onQuit: () => {
|
|
117
|
-
// Clean up terminal state before exiting
|
|
118
|
-
renderer.stop()
|
|
119
|
-
requestExit(0)
|
|
120
|
-
},
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
const handleInlineFullRerender = () => {
|
|
124
|
-
if (mode !== "inline" || !staticRenderer) return
|
|
125
|
-
const inlineMode = renderMode as InlineRenderer
|
|
126
|
-
if (!inlineMode.needsFullRerender()) return
|
|
127
|
-
|
|
128
|
-
// Clear screen + scrollback + cursor home
|
|
129
|
-
stdout.write(ANSI.screen.clear + ANSI.screen.clearScrollback + ANSI.cursor.home)
|
|
130
|
-
// Replay all cached static content
|
|
131
|
-
const cachedStatic = staticRenderer.getCachedOutput()
|
|
132
|
-
if (cachedStatic) {
|
|
133
|
-
stdout.write(cachedStatic)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Reset state
|
|
137
|
-
inlineMode.clearFullRerenderFlag()
|
|
138
|
-
state.invalidateBuffers()
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const flushInlineStatic = (container: Container | null, frameWidth: number) => {
|
|
142
|
-
if (mode !== "inline" || !container?.staticDirty || !container.staticRoot || !staticRenderer) {
|
|
143
|
-
return ""
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const inlineMode = renderMode as InlineRenderer
|
|
147
|
-
const prevHeight = inlineMode.getPreviousHeight()
|
|
148
|
-
let output = ""
|
|
149
|
-
|
|
150
|
-
// Step 1: Clear the dynamic area (move up + clear to end of screen)
|
|
151
|
-
if (prevHeight > 0) {
|
|
152
|
-
output += ANSI.cursor.up(prevHeight) + ANSI.cursor.startOfLine + ANSI.screen.clearToEnd
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Step 2: Append static content (cursor ends at bottom of static)
|
|
156
|
-
output += staticRenderer.render(container.staticRoot, frameWidth)
|
|
157
|
-
|
|
158
|
-
// Step 3: Reset previousHeight to 0 (we cleared dynamic, starting fresh)
|
|
159
|
-
inlineMode.reset()
|
|
160
|
-
inlineMode.forceFullOutputOnce() // Force full output to resync cursor tracking after static
|
|
161
|
-
state.invalidateBuffers()
|
|
162
|
-
container.staticDirty = false
|
|
163
|
-
|
|
164
|
-
return output
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// The render frame logic
|
|
168
|
-
const renderFrame = () => {
|
|
169
|
-
const frameStart = Prof.startFrame()
|
|
170
|
-
const frameStartMs = performance.now()
|
|
171
|
-
// Flush any pending React updates before measuring/layout.
|
|
172
|
-
// Ensures resize-driven state (useTerminalSize) is reflected in the host tree.
|
|
173
|
-
flushSync(() => {})
|
|
174
|
-
const frameWidth = state.width
|
|
175
|
-
const frameHeight = state.height
|
|
176
|
-
let contentH = frameHeight
|
|
177
|
-
|
|
178
|
-
const container = (renderer as TuiRendererInternal)._container
|
|
179
|
-
const root = container?.root ?? null
|
|
180
|
-
|
|
181
|
-
// Must render if dirty OR if static content needs flushing
|
|
182
|
-
if ((!state.dirty && !container?.staticDirty) || !root) return
|
|
183
|
-
state.dirty = false
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
// Handle full rerender on resize (Ink-style: clear everything + replay static)
|
|
187
|
-
handleInlineFullRerender()
|
|
188
|
-
|
|
189
|
-
// Handle static content: clear dynamic area, append static, then fresh dynamic render
|
|
190
|
-
// Note: IL (insert lines) won't work here because inline mode uses relative positioning
|
|
191
|
-
// and IL would desync the screen state from our buffer tracking.
|
|
192
|
-
const staticOutput = flushInlineStatic(container ?? null, frameWidth)
|
|
193
|
-
|
|
194
|
-
// For inline mode, measure content unconstrained to handle overflow
|
|
195
|
-
let actualContentHeight = frameHeight
|
|
196
|
-
if (mode === "inline") {
|
|
197
|
-
const size = root.measure(frameWidth, Number.MAX_SAFE_INTEGER)
|
|
198
|
-
actualContentHeight = size.h
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Buffer height: content height for inline (to capture all content), terminal height for fullscreen
|
|
202
|
-
const bufferHeight = mode === "inline" ? Math.max(actualContentHeight, frameHeight) : frameHeight
|
|
203
|
-
|
|
204
|
-
// Ensure buffers exist
|
|
205
|
-
state.ensureBuffers(frameWidth, bufferHeight)
|
|
206
|
-
if (!state.nextBuffer) return
|
|
207
|
-
|
|
208
|
-
// Build frame (clear, layout, render)
|
|
209
|
-
const timings = frameBuilder.build(root, state.nextBuffer, state.palette, frameWidth, bufferHeight)
|
|
210
|
-
|
|
211
|
-
// Generate output
|
|
212
|
-
const t = Prof.startPhase()
|
|
213
|
-
const diffStartMs = performance.now()
|
|
214
|
-
|
|
215
|
-
const { output: modeOutput, contentHeight } = renderMode.generateOutput({
|
|
216
|
-
nextBuffer: state.nextBuffer,
|
|
217
|
-
prevBuffer: state.prevBuffer,
|
|
218
|
-
palette: state.palette,
|
|
219
|
-
frameWidth,
|
|
220
|
-
frameHeight,
|
|
221
|
-
contentHeight: actualContentHeight,
|
|
222
|
-
enableDiff,
|
|
223
|
-
stdout,
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
// Combine static + dynamic output for atomic write
|
|
227
|
-
// Wrap in synchronized output mode (DECSET 2026) to prevent tearing
|
|
228
|
-
// Terminals that don't support it will safely ignore these sequences
|
|
229
|
-
const output = ANSI.sync.begin + staticOutput + modeOutput + state.palette.sgr(0) + ANSI.sync.end
|
|
230
|
-
contentH = contentHeight
|
|
231
|
-
const diffAnsiMs = performance.now() - diffStartMs
|
|
232
|
-
Prof.endPhase("diff+ansi", t)
|
|
233
|
-
|
|
234
|
-
// Write output (single atomic write prevents visual glitches)
|
|
235
|
-
const writeT = Prof.startPhase()
|
|
236
|
-
const writeStart = performance.now()
|
|
237
|
-
stdout.write(output)
|
|
238
|
-
const writeMs = performance.now() - writeStart
|
|
239
|
-
Prof.endPhase("write", writeT)
|
|
240
|
-
|
|
241
|
-
Prof.endFrame(frameStart)
|
|
242
|
-
const frameMs = performance.now() - frameStartMs
|
|
243
|
-
|
|
244
|
-
// Swap buffers
|
|
245
|
-
state.swapBuffers()
|
|
246
|
-
|
|
247
|
-
// Build stats
|
|
248
|
-
const stats: FrameStats = {
|
|
249
|
-
mode,
|
|
250
|
-
width: state.width,
|
|
251
|
-
height: state.height,
|
|
252
|
-
contentHeight: contentH,
|
|
253
|
-
bytes: Buffer.byteLength(output, "utf8"),
|
|
254
|
-
frameMs,
|
|
255
|
-
phases: {
|
|
256
|
-
clear: timings.clear,
|
|
257
|
-
layout: timings.layout,
|
|
258
|
-
render: timings.render,
|
|
259
|
-
diffAnsi: diffAnsiMs,
|
|
260
|
-
write: writeMs,
|
|
261
|
-
},
|
|
262
|
-
timestamp: performance.now(),
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (debugHook) debugHook(stats)
|
|
266
|
-
if (events.hasFrameHandlers) events.dispatchFrame(stats)
|
|
267
|
-
} catch (err) {
|
|
268
|
-
console.error("[effect-tui] Render error:", err)
|
|
269
|
-
state.markDirty()
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
let onExit: (() => void) | null = null
|
|
274
|
-
let onSignal: ((signal: NodeJS.Signals) => void) | null = null
|
|
275
|
-
let onUncaughtException: ((err: Error) => void) | null = null
|
|
276
|
-
let onUnhandledRejection: ((reason: unknown) => void) | null = null
|
|
277
|
-
|
|
278
|
-
// Build renderer object
|
|
279
|
-
const renderer: TuiRenderer = {
|
|
280
|
-
get width() {
|
|
281
|
-
return state.width
|
|
282
|
-
},
|
|
283
|
-
get height() {
|
|
284
|
-
return state.height
|
|
285
|
-
},
|
|
286
|
-
requestRender() {
|
|
287
|
-
state.markDirty()
|
|
288
|
-
},
|
|
289
|
-
onKey: (handler: (key: KeyMsg) => void) => events.onKey(handler),
|
|
290
|
-
onMouse: (handler: (mouse: MouseMsg) => void) => events.onMouse(handler),
|
|
291
|
-
onPaste: (handler: (paste: PasteMsg) => void) => events.onPaste(handler),
|
|
292
|
-
onResize: (handler: (width: number, height: number) => void) => events.onResize(handler),
|
|
293
|
-
onFrameStats: (handler: (stats: FrameStats) => void) => events.onFrameStats(handler),
|
|
294
|
-
stop() {
|
|
295
|
-
state.running = false
|
|
296
|
-
if (onExit) {
|
|
297
|
-
process.off("exit", onExit)
|
|
298
|
-
onExit = null
|
|
299
|
-
}
|
|
300
|
-
if (onSignal) {
|
|
301
|
-
process.off("SIGINT", onSignal)
|
|
302
|
-
process.off("SIGTERM", onSignal)
|
|
303
|
-
onSignal = null
|
|
304
|
-
}
|
|
305
|
-
if (onUncaughtException) {
|
|
306
|
-
process.off("uncaughtException", onUncaughtException)
|
|
307
|
-
onUncaughtException = null
|
|
308
|
-
}
|
|
309
|
-
if (onUnhandledRejection) {
|
|
310
|
-
process.off("unhandledRejection", onUnhandledRejection)
|
|
311
|
-
onUnhandledRejection = null
|
|
312
|
-
}
|
|
313
|
-
if (state.loop) {
|
|
314
|
-
clearInterval(state.loop)
|
|
315
|
-
state.loop = null
|
|
316
|
-
}
|
|
317
|
-
if (state.inputHandler) {
|
|
318
|
-
stdin.removeListener("data", state.inputHandler)
|
|
319
|
-
state.inputHandler = null
|
|
320
|
-
}
|
|
321
|
-
if (state.resizeHandler) {
|
|
322
|
-
stdout.removeListener("resize", state.resizeHandler)
|
|
323
|
-
state.resizeHandler = null
|
|
324
|
-
}
|
|
325
|
-
terminal.teardown()
|
|
326
|
-
},
|
|
327
|
-
flush() {
|
|
328
|
-
renderFrame()
|
|
329
|
-
},
|
|
330
|
-
getScreenshot() {
|
|
331
|
-
// Return the previous buffer as ANSI string (it has the last rendered frame)
|
|
332
|
-
if (state.prevBuffer) {
|
|
333
|
-
return bufferToString(state.prevBuffer, state.palette, state.width, state.height)
|
|
334
|
-
}
|
|
335
|
-
return ""
|
|
336
|
-
},
|
|
337
|
-
dispatchKey(key: KeyMsg) {
|
|
338
|
-
events.dispatchKey(key)
|
|
339
|
-
if (!manualMode) renderFrame()
|
|
340
|
-
},
|
|
341
|
-
dispatchPaste(text: string) {
|
|
342
|
-
events.dispatchPaste(text)
|
|
343
|
-
if (!manualMode) renderFrame()
|
|
344
|
-
},
|
|
345
|
-
dispatchResize(width: number, height: number) {
|
|
346
|
-
renderMode.handleResize(width, height, state.lastWidth)
|
|
347
|
-
state.updateDimensions(width, height)
|
|
348
|
-
state.invalidateBuffers()
|
|
349
|
-
state.markDirty()
|
|
350
|
-
flushSync(() => {
|
|
351
|
-
events.dispatchResize(width, height)
|
|
352
|
-
})
|
|
353
|
-
if (!manualMode) renderFrame()
|
|
354
|
-
},
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Terminal setup
|
|
358
|
-
terminal.setup()
|
|
359
|
-
|
|
360
|
-
// Input handling
|
|
361
|
-
state.inputHandler = (data: Buffer) => inputProcessor.process(data)
|
|
362
|
-
stdin.on("data", state.inputHandler)
|
|
363
|
-
|
|
364
|
-
// Resize handling
|
|
365
|
-
state.resizeHandler = () => {
|
|
366
|
-
const newWidth = stdout.columns || 80
|
|
367
|
-
const newHeight = stdout.rows || 24
|
|
368
|
-
|
|
369
|
-
renderMode.handleResize(newWidth, newHeight, state.lastWidth)
|
|
370
|
-
|
|
371
|
-
state.updateDimensions(newWidth, newHeight)
|
|
372
|
-
state.invalidateBuffers()
|
|
373
|
-
state.markDirty()
|
|
374
|
-
|
|
375
|
-
flushSync(() => {
|
|
376
|
-
events.dispatchResize(newWidth, newHeight)
|
|
377
|
-
})
|
|
378
|
-
}
|
|
379
|
-
stdout.on("resize", state.resizeHandler)
|
|
380
|
-
|
|
381
|
-
// Render loop
|
|
382
|
-
if (!manualMode) {
|
|
383
|
-
const frameMs = 1000 / fps
|
|
384
|
-
state.loop = setInterval(() => {
|
|
385
|
-
if (!state.running) {
|
|
386
|
-
if (state.loop) clearInterval(state.loop)
|
|
387
|
-
terminal.teardown()
|
|
388
|
-
return
|
|
389
|
-
}
|
|
390
|
-
renderFrame()
|
|
391
|
-
}, frameMs)
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Process exit handlers - ensure terminal is restored on any exit
|
|
395
|
-
// These handlers are critical for proper cleanup when process.exit() is called
|
|
396
|
-
let cleanedUp = false
|
|
397
|
-
const cleanup = () => {
|
|
398
|
-
if (cleanedUp) return
|
|
399
|
-
cleanedUp = true
|
|
400
|
-
renderer.stop()
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (handleSignals) {
|
|
404
|
-
// Handle normal process exit (synchronous - runs before exit completes)
|
|
405
|
-
onExit = () => cleanup()
|
|
406
|
-
process.on("exit", onExit)
|
|
407
|
-
|
|
408
|
-
// Handle SIGINT (Ctrl+C from shell, not from TUI input) and SIGTERM
|
|
409
|
-
onSignal = (signal: NodeJS.Signals) => {
|
|
410
|
-
cleanup()
|
|
411
|
-
if (!exitOnSignal) return
|
|
412
|
-
const code = signalExitCodes[signal as HandledSignal] ?? 0
|
|
413
|
-
requestExit(code)
|
|
414
|
-
}
|
|
415
|
-
process.on("SIGINT", onSignal)
|
|
416
|
-
process.on("SIGTERM", onSignal)
|
|
417
|
-
|
|
418
|
-
// Handle uncaught exceptions - ensure error is visible before exit
|
|
419
|
-
onUncaughtException = (err: Error) => {
|
|
420
|
-
cleanup()
|
|
421
|
-
// Write directly to terminal, bypassing console capture
|
|
422
|
-
writeToTerminal(`\n[effect-tui] Uncaught exception:\n${err.stack || err.message}\n`)
|
|
423
|
-
process.exit(1)
|
|
424
|
-
}
|
|
425
|
-
process.on("uncaughtException", onUncaughtException)
|
|
426
|
-
|
|
427
|
-
// Handle unhandled promise rejections
|
|
428
|
-
onUnhandledRejection = (reason: unknown) => {
|
|
429
|
-
cleanup()
|
|
430
|
-
const message = reason instanceof Error ? reason.stack || reason.message : String(reason)
|
|
431
|
-
writeToTerminal(`\n[effect-tui] Unhandled rejection:\n${message}\n`)
|
|
432
|
-
process.exit(1)
|
|
433
|
-
}
|
|
434
|
-
process.on("unhandledRejection", onUnhandledRejection)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
;(renderer as TuiRendererInternal)._container = null
|
|
438
|
-
return renderer
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
export interface Root {
|
|
442
|
-
render(element: ReactNode, sync?: boolean): void
|
|
443
|
-
unmount(): void
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
export function createRoot(renderer: TuiRenderer): Root {
|
|
447
|
-
const hostContext: HostContext = {
|
|
448
|
-
requestRender: () => renderer.requestRender(),
|
|
449
|
-
requestImmediateRender: () => renderer.flush(),
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const container: Container = {
|
|
453
|
-
root: null,
|
|
454
|
-
ctx: hostContext,
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const fiberRoot = reconciler.createContainer(
|
|
458
|
-
container,
|
|
459
|
-
0,
|
|
460
|
-
null,
|
|
461
|
-
false,
|
|
462
|
-
null,
|
|
463
|
-
"",
|
|
464
|
-
(err: Error) => console.error(err),
|
|
465
|
-
(err: Error) => console.error(err),
|
|
466
|
-
(err: Error) => console.error(err),
|
|
467
|
-
() => {},
|
|
468
|
-
null,
|
|
469
|
-
)
|
|
470
|
-
|
|
471
|
-
;(renderer as TuiRendererInternal)._container = container
|
|
472
|
-
|
|
473
|
-
return {
|
|
474
|
-
render(element: ReactNode, sync = false) {
|
|
475
|
-
const wrapped = React.createElement(RendererContext.Provider, { value: renderer }, element)
|
|
476
|
-
if (sync) {
|
|
477
|
-
flushSync(() => {
|
|
478
|
-
reconciler.updateContainer(wrapped, fiberRoot, null, null)
|
|
479
|
-
})
|
|
480
|
-
renderer.requestRender()
|
|
481
|
-
} else {
|
|
482
|
-
reconciler.updateContainer(wrapped, fiberRoot, null, () => {
|
|
483
|
-
renderer.requestRender()
|
|
484
|
-
})
|
|
485
|
-
}
|
|
486
|
-
},
|
|
487
|
-
unmount() {
|
|
488
|
-
reconciler.updateContainer(null, fiberRoot, null, () => {
|
|
489
|
-
renderer.stop()
|
|
490
|
-
})
|
|
491
|
-
},
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// High-level convenience API (Ink-style)
|
|
496
|
-
export interface RenderInstance {
|
|
497
|
-
renderer: TuiRenderer
|
|
498
|
-
root: Root
|
|
499
|
-
rerender(element: ReactNode): void
|
|
500
|
-
unmount(): void
|
|
501
|
-
waitUntilExit(): Promise<void>
|
|
502
|
-
/** Cleanly exit the application, restoring terminal state before process.exit() */
|
|
503
|
-
quit(code?: number): void
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
export type ImportMetaLike = {
|
|
507
|
-
url: string
|
|
508
|
-
main?: boolean
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
export type RenderOptions = RendererOptions &
|
|
512
|
-
DevOptions & {
|
|
513
|
-
/** Enable dev runtime (HMR, console overlay, remote control). */
|
|
514
|
-
dev?: boolean
|
|
515
|
-
/** Required when passing options. */
|
|
516
|
-
importMeta: ImportMetaLike
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const stripQuery = (url: string): string => url.split("?")[0]
|
|
520
|
-
|
|
521
|
-
const createRenderInstance = (
|
|
522
|
-
renderer: TuiRenderer,
|
|
523
|
-
root: Root,
|
|
524
|
-
element: ReactNode,
|
|
525
|
-
stop: () => void,
|
|
526
|
-
skipInitialRender = false,
|
|
527
|
-
): RenderInstance => {
|
|
528
|
-
if (!skipInitialRender) {
|
|
529
|
-
root.render(element, true)
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
let resolved = false
|
|
533
|
-
let resolveExit: (() => void) | null = null
|
|
534
|
-
const exitPromise = new Promise<void>((resolve) => {
|
|
535
|
-
resolveExit = () => {
|
|
536
|
-
if (resolved) return
|
|
537
|
-
resolved = true
|
|
538
|
-
resolve()
|
|
539
|
-
}
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
// Resolve the exit promise on process exit
|
|
543
|
-
// Note: Terminal cleanup is handled by createRenderer's exit handlers
|
|
544
|
-
const onExit = () => {
|
|
545
|
-
resolveExit?.()
|
|
546
|
-
}
|
|
547
|
-
process.once("exit", onExit)
|
|
548
|
-
|
|
549
|
-
const unmount = () => {
|
|
550
|
-
process.off("exit", onExit)
|
|
551
|
-
if (!resolved) {
|
|
552
|
-
resolved = true
|
|
553
|
-
stop()
|
|
554
|
-
resolveExit?.()
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
const rerender = (next: ReactNode) => {
|
|
559
|
-
if (resolved) return
|
|
560
|
-
root.render(next)
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Clean quit function - stop() + process.exit()
|
|
564
|
-
// The createRenderer exit handler will also run, but it's idempotent
|
|
565
|
-
const quit = (code = 0) => {
|
|
566
|
-
stop()
|
|
567
|
-
requestExit(code)
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
return {
|
|
571
|
-
renderer,
|
|
572
|
-
root,
|
|
573
|
-
rerender,
|
|
574
|
-
unmount,
|
|
575
|
-
waitUntilExit: () => exitPromise,
|
|
576
|
-
quit,
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
export function render(element: ReactNode, options?: RenderOptions): RenderInstance {
|
|
581
|
-
if (options && !options.importMeta) {
|
|
582
|
-
throw new Error("[effect-tui] render(..., options) requires { importMeta: import.meta }")
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
if (!options?.dev) {
|
|
586
|
-
const renderer = createRenderer(options)
|
|
587
|
-
const root = createRoot(renderer)
|
|
588
|
-
return createRenderInstance(renderer, root, element, () => renderer.stop())
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const importMeta = options.importMeta
|
|
592
|
-
if (!importMeta) {
|
|
593
|
-
throw new Error("[effect-tui] render(..., { dev: true, importMeta }) is required in dev mode")
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const baseUrl = stripQuery(importMeta.url)
|
|
597
|
-
const renderCache = getRenderCache<RenderInstance>()
|
|
598
|
-
const cached = renderCache.get(baseUrl)
|
|
599
|
-
if (cached) return cached
|
|
600
|
-
|
|
601
|
-
const renderer = createRenderer(options)
|
|
602
|
-
const root = createRoot(renderer)
|
|
603
|
-
const entryPath = fileURLToPath(new URL(baseUrl))
|
|
604
|
-
|
|
605
|
-
const devRuntime = startDevRuntime(entryPath, renderer, root, options)
|
|
606
|
-
|
|
607
|
-
const instance = createRenderInstance(
|
|
608
|
-
renderer,
|
|
609
|
-
root,
|
|
610
|
-
element,
|
|
611
|
-
() => {
|
|
612
|
-
void devRuntime.stop()
|
|
613
|
-
},
|
|
614
|
-
true,
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
renderCache.set(baseUrl, instance)
|
|
618
|
-
return instance
|
|
619
|
-
}
|
|
1
|
+
export * from "./internal/renderer/index.js"
|
package/src/test/render-tui.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface RenderTUIResult {
|
|
|
21
21
|
/** Send a key event */
|
|
22
22
|
sendKey(key: KeyMsg): void
|
|
23
23
|
/** Trigger a render cycle and process React updates */
|
|
24
|
-
|
|
24
|
+
renderNow(): void
|
|
25
25
|
/** Unmount the component and stop the renderer */
|
|
26
26
|
unmount(): void
|
|
27
27
|
/** Access mock stdout for advanced assertions */
|
|
@@ -40,12 +40,12 @@ export interface RenderTUIResult {
|
|
|
40
40
|
*
|
|
41
41
|
* @example
|
|
42
42
|
* ```ts
|
|
43
|
-
* const { lastFrame, sendKey,
|
|
43
|
+
* const { lastFrame, sendKey, renderNow, unmount } = renderTUI(<Counter />)
|
|
44
44
|
*
|
|
45
45
|
* expect(lastFrame()).toContain("Count: 0")
|
|
46
46
|
*
|
|
47
47
|
* sendKey({ key: "up" })
|
|
48
|
-
*
|
|
48
|
+
* renderNow()
|
|
49
49
|
*
|
|
50
50
|
* expect(lastFrame()).toContain("Count: 1")
|
|
51
51
|
*
|
|
@@ -84,9 +84,9 @@ export function renderTUI(element: ReactElement, options?: RenderTUIOptions): Re
|
|
|
84
84
|
flushPassiveEffects()
|
|
85
85
|
|
|
86
86
|
// Initial render
|
|
87
|
-
renderer.
|
|
87
|
+
renderer.renderNow()
|
|
88
88
|
|
|
89
|
-
const
|
|
89
|
+
const renderNow = () => {
|
|
90
90
|
// Flush React updates synchronously
|
|
91
91
|
flushSync(() => {
|
|
92
92
|
bump?.()
|
|
@@ -95,7 +95,7 @@ export function renderTUI(element: ReactElement, options?: RenderTUIOptions): Re
|
|
|
95
95
|
// Clear buffer before re-render to get clean frame
|
|
96
96
|
stdout.clear()
|
|
97
97
|
renderer.requestRender()
|
|
98
|
-
renderer.
|
|
98
|
+
renderer.renderNow()
|
|
99
99
|
// Flush updates scheduled during layout (e.g., viewport/content size callbacks)
|
|
100
100
|
flushSync(() => {
|
|
101
101
|
bump?.()
|
|
@@ -103,7 +103,7 @@ export function renderTUI(element: ReactElement, options?: RenderTUIOptions): Re
|
|
|
103
103
|
flushPassiveEffects()
|
|
104
104
|
stdout.clear()
|
|
105
105
|
renderer.requestRender()
|
|
106
|
-
renderer.
|
|
106
|
+
renderer.renderNow()
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
return {
|
|
@@ -123,7 +123,7 @@ export function renderTUI(element: ReactElement, options?: RenderTUIOptions): Re
|
|
|
123
123
|
stdin.sendKey(key)
|
|
124
124
|
},
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
renderNow,
|
|
127
127
|
|
|
128
128
|
unmount() {
|
|
129
129
|
root.unmount()
|
|
@@ -134,8 +134,14 @@ export function renderTUI(element: ReactElement, options?: RenderTUIOptions): Re
|
|
|
134
134
|
renderer,
|
|
135
135
|
|
|
136
136
|
resize(w: number, h: number) {
|
|
137
|
-
stdout.
|
|
138
|
-
|
|
137
|
+
stdout.columns = w
|
|
138
|
+
stdout.rows = h
|
|
139
|
+
if (renderer.dispatchResize) {
|
|
140
|
+
renderer.dispatchResize(w, h)
|
|
141
|
+
} else {
|
|
142
|
+
stdout.resize(w, h)
|
|
143
|
+
}
|
|
144
|
+
renderNow()
|
|
139
145
|
},
|
|
140
146
|
}
|
|
141
147
|
}
|