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