@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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Custom Effect logger that feeds logs into SpanTreeState for TUI visualization
|
|
2
|
+
|
|
3
|
+
import { Logger, FiberRef, FiberRefs, Cause, Context, Tracer } from "effect"
|
|
4
|
+
import type { LogLevel, SourceLocation, SpanTreeState } from "./span-state.js"
|
|
5
|
+
import { captureAppLocation, parseStackLine, pickBestLocation } from "./location.js"
|
|
6
|
+
|
|
7
|
+
function captureCallerLocation(): SourceLocation | undefined {
|
|
8
|
+
const err = new Error()
|
|
9
|
+
const stack = err.stack
|
|
10
|
+
if (!stack) return
|
|
11
|
+
|
|
12
|
+
const lines = stack.split("\n").slice(1)
|
|
13
|
+
const parsed: SourceLocation[] = []
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
if (line.includes("tui-logger") || line.includes("makeTuiLogger")) continue
|
|
16
|
+
const loc = parseStackLine(line)
|
|
17
|
+
if (loc) parsed.push(loc)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const best = pickBestLocation(parsed)
|
|
21
|
+
if (best) return best
|
|
22
|
+
return captureAppLocation([/tui-logger/, /makeTuiLogger/])
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a Logger that feeds log entries into SpanTreeState.
|
|
27
|
+
* Logs are associated with the current span and displayed in the TUI.
|
|
28
|
+
*/
|
|
29
|
+
export function makeTuiLogger(state: SpanTreeState, onUpdate: () => void): Logger.Logger<unknown, void> {
|
|
30
|
+
return Logger.make(({ logLevel, message, cause, context }) => {
|
|
31
|
+
const debugStacks = process.env.EFFECT_TUI_DEBUG_STACKS === "1"
|
|
32
|
+
// Get current context from fiber refs
|
|
33
|
+
const ctx = FiberRefs.getOrDefault(context, FiberRef.currentContext)
|
|
34
|
+
|
|
35
|
+
// Get current span from context
|
|
36
|
+
const spanOption = Context.getOption(ctx, Tracer.ParentSpan)
|
|
37
|
+
if (spanOption._tag !== "Some") return
|
|
38
|
+
|
|
39
|
+
const span = spanOption.value
|
|
40
|
+
if (!span || typeof span !== "object") return
|
|
41
|
+
|
|
42
|
+
// Get span ID - Effect spans have spanContext() method
|
|
43
|
+
const anySpan = span as any
|
|
44
|
+
const spanId = typeof anySpan.spanContext === "function" ? anySpan.spanContext().spanId : anySpan.spanId
|
|
45
|
+
if (!spanId) return
|
|
46
|
+
|
|
47
|
+
// Map Effect log level to our level
|
|
48
|
+
let level: LogLevel = "INFO"
|
|
49
|
+
const levelLabel = logLevel.label.toUpperCase()
|
|
50
|
+
if (levelLabel === "ERROR" || levelLabel === "FATAL") level = "ERROR"
|
|
51
|
+
else if (levelLabel === "WARNING" || levelLabel === "WARN") level = "WARN"
|
|
52
|
+
else if (levelLabel === "DEBUG") level = "DEBUG"
|
|
53
|
+
else if (levelLabel === "TRACE") level = "TRACE"
|
|
54
|
+
|
|
55
|
+
// Format message
|
|
56
|
+
let text = String(message)
|
|
57
|
+
|
|
58
|
+
// Add cause if present
|
|
59
|
+
if (cause && !Cause.isEmpty(cause)) {
|
|
60
|
+
text += ` ${Cause.pretty(cause)}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const location = captureCallerLocation()
|
|
64
|
+
|
|
65
|
+
state.addLog(spanId, level, text, location)
|
|
66
|
+
if (!location && debugStacks) {
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.error("[effect-tui] no location for log; stack:", new Error().stack)
|
|
69
|
+
}
|
|
70
|
+
onUpdate()
|
|
71
|
+
})
|
|
72
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Border utilities shared between box and canvas hosts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CellBuffer } from "@effect-tui/core"
|
|
6
|
+
|
|
7
|
+
export type BorderKind = "none" | "rounded" | "square" | "ascii"
|
|
8
|
+
|
|
9
|
+
export interface BorderChars {
|
|
10
|
+
tl: string // top-left
|
|
11
|
+
tr: string // top-right
|
|
12
|
+
bl: string // bottom-left
|
|
13
|
+
br: string // bottom-right
|
|
14
|
+
h: string // horizontal
|
|
15
|
+
v: string // vertical
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get border characters for a given border style.
|
|
20
|
+
* Returns empty strings for "none" (drawing code should skip when border is "none").
|
|
21
|
+
*/
|
|
22
|
+
export function borderChars(kind: BorderKind): BorderChars {
|
|
23
|
+
switch (kind) {
|
|
24
|
+
case "rounded":
|
|
25
|
+
return { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" }
|
|
26
|
+
case "square":
|
|
27
|
+
return { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" }
|
|
28
|
+
case "ascii":
|
|
29
|
+
return { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" }
|
|
30
|
+
default:
|
|
31
|
+
return { tl: "", tr: "", bl: "", br: "", h: "", v: "" }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ClipRect {
|
|
36
|
+
ox: number
|
|
37
|
+
oy: number
|
|
38
|
+
w: number
|
|
39
|
+
h: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Draw a border around a rectangle.
|
|
44
|
+
* If clip is provided, only draws within the clip bounds (for canvas).
|
|
45
|
+
* If clip is not provided, draws the full border (for box).
|
|
46
|
+
*/
|
|
47
|
+
export function drawBorder(
|
|
48
|
+
buffer: CellBuffer,
|
|
49
|
+
x: number,
|
|
50
|
+
y: number,
|
|
51
|
+
w: number,
|
|
52
|
+
h: number,
|
|
53
|
+
chars: BorderChars,
|
|
54
|
+
style: number,
|
|
55
|
+
clip?: ClipRect,
|
|
56
|
+
): void {
|
|
57
|
+
if (w < 2 || h < 2) return
|
|
58
|
+
|
|
59
|
+
const tlcp = chars.tl.codePointAt(0)!
|
|
60
|
+
const trcp = chars.tr.codePointAt(0)!
|
|
61
|
+
const blcp = chars.bl.codePointAt(0)!
|
|
62
|
+
const brcp = chars.br.codePointAt(0)!
|
|
63
|
+
const hcp = chars.h.codePointAt(0)!
|
|
64
|
+
const vcp = chars.v.codePointAt(0)!
|
|
65
|
+
|
|
66
|
+
if (clip) {
|
|
67
|
+
// Clipped drawing (for canvas)
|
|
68
|
+
const { ox, oy, w: cw, h: ch } = clip
|
|
69
|
+
const inBounds = (px: number, py: number) => px >= ox && px < ox + cw && py >= oy && py < oy + ch
|
|
70
|
+
|
|
71
|
+
// Corners
|
|
72
|
+
if (inBounds(x, y)) buffer.drawCP(x, y, tlcp, style)
|
|
73
|
+
if (inBounds(x + w - 1, y)) buffer.drawCP(x + w - 1, y, trcp, style)
|
|
74
|
+
if (inBounds(x, y + h - 1)) buffer.drawCP(x, y + h - 1, blcp, style)
|
|
75
|
+
if (inBounds(x + w - 1, y + h - 1)) buffer.drawCP(x + w - 1, y + h - 1, brcp, style)
|
|
76
|
+
|
|
77
|
+
// Horizontal lines
|
|
78
|
+
for (let col = 1; col < w - 1; col++) {
|
|
79
|
+
const xx = x + col
|
|
80
|
+
if (inBounds(xx, y)) buffer.drawCP(xx, y, hcp, style)
|
|
81
|
+
if (inBounds(xx, y + h - 1)) buffer.drawCP(xx, y + h - 1, hcp, style)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Vertical lines
|
|
85
|
+
for (let row = 1; row < h - 1; row++) {
|
|
86
|
+
const yy = y + row
|
|
87
|
+
if (inBounds(x, yy)) buffer.drawCP(x, yy, vcp, style)
|
|
88
|
+
if (inBounds(x + w - 1, yy)) buffer.drawCP(x + w - 1, yy, vcp, style)
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
// Full drawing (for box)
|
|
92
|
+
// Corners
|
|
93
|
+
buffer.drawCP(x, y, tlcp, style)
|
|
94
|
+
buffer.drawCP(x + w - 1, y, trcp, style)
|
|
95
|
+
buffer.drawCP(x, y + h - 1, blcp, style)
|
|
96
|
+
buffer.drawCP(x + w - 1, y + h - 1, brcp, style)
|
|
97
|
+
|
|
98
|
+
// Horizontal lines
|
|
99
|
+
buffer.fillRect(x + 1, y, w - 2, 1, hcp, style)
|
|
100
|
+
buffer.fillRect(x + 1, y + h - 1, w - 2, 1, hcp, style)
|
|
101
|
+
|
|
102
|
+
// Vertical lines
|
|
103
|
+
for (let yy = y + 1; yy < y + h - 1; yy++) {
|
|
104
|
+
buffer.drawCP(x, yy, vcp, style)
|
|
105
|
+
buffer.drawCP(x + w - 1, yy, vcp, style)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic flex layout algorithm for VStack and HStack.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { HostInstance, Rect, Size } from "../reconciler/types.js"
|
|
6
|
+
import type { BaseHost } from "../hosts/base.js"
|
|
7
|
+
|
|
8
|
+
export type FlexAxis = "vertical" | "horizontal"
|
|
9
|
+
export type FlexAlignment = "start" | "center" | "end"
|
|
10
|
+
|
|
11
|
+
export interface FlexMeasureResult {
|
|
12
|
+
sizes: Size[]
|
|
13
|
+
totalSize: Size
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Measure children along a flex axis.
|
|
18
|
+
* Returns cached sizes and total size.
|
|
19
|
+
*/
|
|
20
|
+
export function measureFlex(
|
|
21
|
+
axis: FlexAxis,
|
|
22
|
+
children: HostInstance[],
|
|
23
|
+
spacing: number,
|
|
24
|
+
maxMain: number,
|
|
25
|
+
maxCross: number,
|
|
26
|
+
): FlexMeasureResult {
|
|
27
|
+
const sizes: Size[] = []
|
|
28
|
+
let totalMain = 0
|
|
29
|
+
let maxChildCross = 0
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < children.length; i++) {
|
|
32
|
+
const child = children[i]
|
|
33
|
+
const remainingMain = Math.max(0, maxMain - totalMain)
|
|
34
|
+
|
|
35
|
+
// For vertical: maxW=maxCross, maxH=remainingMain
|
|
36
|
+
// For horizontal: maxW=remainingMain, maxH=maxCross
|
|
37
|
+
const [childMaxW, childMaxH] = axis === "vertical" ? [maxCross, remainingMain] : [remainingMain, maxCross]
|
|
38
|
+
|
|
39
|
+
const size = child.measure(childMaxW, childMaxH)
|
|
40
|
+
sizes.push(size)
|
|
41
|
+
|
|
42
|
+
const childMain = axis === "vertical" ? size.h : size.w
|
|
43
|
+
const childCross = axis === "vertical" ? size.w : size.h
|
|
44
|
+
|
|
45
|
+
totalMain += childMain
|
|
46
|
+
if (i < children.length - 1) totalMain += spacing
|
|
47
|
+
maxChildCross = Math.max(maxChildCross, childCross)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const totalSize: Size = axis === "vertical" ? { w: maxChildCross, h: totalMain } : { w: totalMain, h: maxChildCross }
|
|
51
|
+
|
|
52
|
+
return { sizes, totalSize }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Layout children along a flex axis using cached sizes.
|
|
57
|
+
*/
|
|
58
|
+
export function layoutFlex(
|
|
59
|
+
axis: FlexAxis,
|
|
60
|
+
children: HostInstance[],
|
|
61
|
+
cachedSizes: Size[],
|
|
62
|
+
rect: Rect,
|
|
63
|
+
spacing: number,
|
|
64
|
+
alignment: FlexAlignment,
|
|
65
|
+
stretchCross: boolean,
|
|
66
|
+
): void {
|
|
67
|
+
// Calculate totals
|
|
68
|
+
let totalNaturalMain = 0
|
|
69
|
+
let totalFlexGrow = 0
|
|
70
|
+
const totalSpacing = Math.max(0, (children.length - 1) * spacing)
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < children.length; i++) {
|
|
73
|
+
const child = children[i]
|
|
74
|
+
const size = cachedSizes[i] ?? child.measure(rect.w, rect.h)
|
|
75
|
+
const childMain = axis === "vertical" ? size.h : size.w
|
|
76
|
+
totalNaturalMain += childMain
|
|
77
|
+
totalFlexGrow += (child as BaseHost).flexGrow ?? 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Calculate extra space to distribute
|
|
81
|
+
const availableMain = axis === "vertical" ? rect.h : rect.w
|
|
82
|
+
const extraSpace = Math.max(0, availableMain - totalNaturalMain - totalSpacing)
|
|
83
|
+
|
|
84
|
+
// Layout children
|
|
85
|
+
let mainPos = axis === "vertical" ? rect.y : rect.x
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < children.length; i++) {
|
|
88
|
+
const child = children[i]
|
|
89
|
+
const size = cachedSizes[i] ?? { w: 0, h: 0 }
|
|
90
|
+
const flexGrow = (child as BaseHost).flexGrow ?? 0
|
|
91
|
+
const flexExtra = totalFlexGrow > 0 ? (extraSpace * flexGrow) / totalFlexGrow : 0
|
|
92
|
+
|
|
93
|
+
const childMain = (axis === "vertical" ? size.h : size.w) + flexExtra
|
|
94
|
+
const childCross = axis === "vertical" ? size.w : size.h
|
|
95
|
+
const crossSize = axis === "vertical" ? rect.w : rect.h
|
|
96
|
+
const crossStart = axis === "vertical" ? rect.x : rect.y
|
|
97
|
+
|
|
98
|
+
// Calculate cross position based on alignment
|
|
99
|
+
let crossPos = crossStart
|
|
100
|
+
if (alignment === "center") {
|
|
101
|
+
crossPos = crossStart + Math.floor((crossSize - childCross) / 2)
|
|
102
|
+
} else if (alignment === "end") {
|
|
103
|
+
crossPos = crossStart + crossSize - childCross
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Build child rect
|
|
107
|
+
const childRect: Rect =
|
|
108
|
+
axis === "vertical"
|
|
109
|
+
? {
|
|
110
|
+
x: crossPos,
|
|
111
|
+
y: mainPos,
|
|
112
|
+
w: stretchCross ? crossSize : childCross,
|
|
113
|
+
h: childMain,
|
|
114
|
+
}
|
|
115
|
+
: {
|
|
116
|
+
x: mainPos,
|
|
117
|
+
y: crossPos,
|
|
118
|
+
w: childMain,
|
|
119
|
+
h: stretchCross ? crossSize : childCross,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
child.layout(childRect)
|
|
123
|
+
mainPos += childMain + spacing
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { type BorderKind, type BorderChars, borderChars, type ClipRect, drawBorder } from "./border.js"
|
|
2
|
+
export { type Padding, type PaddingInput, resolvePadding } from "./padding.js"
|
|
3
|
+
export { type StyleOptions } from "./styles.js"
|
|
4
|
+
export { type FlexAxis, type FlexAlignment, type FlexMeasureResult, measureFlex, layoutFlex } from "./flex-layout.js"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Padding utilities shared between box, canvas, and codeblock hosts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface Padding {
|
|
6
|
+
top: number
|
|
7
|
+
right: number
|
|
8
|
+
bottom: number
|
|
9
|
+
left: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type PaddingInput =
|
|
13
|
+
| number
|
|
14
|
+
| { x?: number; y?: number; top?: number; right?: number; bottom?: number; left?: number }
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve padding input to a normalized Padding object.
|
|
18
|
+
* Supports:
|
|
19
|
+
* - number: uniform padding on all sides
|
|
20
|
+
* - { x, y }: horizontal and vertical padding
|
|
21
|
+
* - { top, right, bottom, left }: individual sides
|
|
22
|
+
*/
|
|
23
|
+
export function resolvePadding(p?: PaddingInput): Padding {
|
|
24
|
+
if (p === undefined) {
|
|
25
|
+
return { top: 0, right: 0, bottom: 0, left: 0 }
|
|
26
|
+
}
|
|
27
|
+
if (typeof p === "number") {
|
|
28
|
+
return { top: p, right: p, bottom: p, left: p }
|
|
29
|
+
}
|
|
30
|
+
// x/y shorthand takes precedence if specified
|
|
31
|
+
if (p.x !== undefined || p.y !== undefined) {
|
|
32
|
+
return {
|
|
33
|
+
top: p.y ?? 0,
|
|
34
|
+
bottom: p.y ?? 0,
|
|
35
|
+
left: p.x ?? 0,
|
|
36
|
+
right: p.x ?? 0,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
top: p.top ?? 0,
|
|
41
|
+
right: p.right ?? 0,
|
|
42
|
+
bottom: p.bottom ?? 0,
|
|
43
|
+
left: p.left ?? 0,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style utilities for building palette-compatible style objects.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ColorValue } from "@effect-tui/core"
|
|
6
|
+
|
|
7
|
+
export interface StyleOptions {
|
|
8
|
+
fg?: ColorValue
|
|
9
|
+
bg?: ColorValue
|
|
10
|
+
bold?: boolean
|
|
11
|
+
italic?: boolean
|
|
12
|
+
underline?: boolean
|
|
13
|
+
inverse?: boolean
|
|
14
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// Effect visualization wrapper using React
|
|
2
|
+
// Uses Effect Layer for proper terminal state management across multiple calls
|
|
3
|
+
|
|
4
|
+
import { useState, useEffect } from "react"
|
|
5
|
+
import { Cause, Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect"
|
|
6
|
+
import { Colors } from "@effect-tui/core"
|
|
7
|
+
import { createRenderer, createRoot } from "../renderer.js"
|
|
8
|
+
import { ANSI } from "../terminal.js"
|
|
9
|
+
|
|
10
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠸", "⠴", "⠦", "⠇"] as const
|
|
11
|
+
const SPIN_INTERVAL_MS = 80
|
|
12
|
+
const ELAPSED_INTERVAL_MS = 16
|
|
13
|
+
const COMPLETION_DELAY_MS = 360
|
|
14
|
+
|
|
15
|
+
type VisualizerStatus = "pending" | "success" | "failure"
|
|
16
|
+
|
|
17
|
+
interface VisualizeOptions {
|
|
18
|
+
/** Show the result value below completion status */
|
|
19
|
+
showResult?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Bridges Effect execution with React rendering
|
|
23
|
+
class VisualizerState {
|
|
24
|
+
status: VisualizerStatus = "pending"
|
|
25
|
+
spinnerIndex = 0
|
|
26
|
+
elapsedMs = 0
|
|
27
|
+
errorSummary?: string
|
|
28
|
+
result?: unknown
|
|
29
|
+
showResult = false
|
|
30
|
+
private listeners = new Set<() => void>()
|
|
31
|
+
|
|
32
|
+
update(
|
|
33
|
+
patch: Partial<
|
|
34
|
+
Pick<VisualizerState, "status" | "spinnerIndex" | "elapsedMs" | "errorSummary" | "result" | "showResult">
|
|
35
|
+
>,
|
|
36
|
+
) {
|
|
37
|
+
Object.assign(this, patch)
|
|
38
|
+
this.listeners.forEach((l) => l())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
subscribe(listener: () => void): () => void {
|
|
42
|
+
this.listeners.add(listener)
|
|
43
|
+
return () => this.listeners.delete(listener)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const formatDuration = (ms: number): string => Duration.format(Duration.millis(Math.max(0, Math.round(ms))))
|
|
48
|
+
|
|
49
|
+
const formatResult = (value: unknown): string => {
|
|
50
|
+
if (value === undefined) return "undefined"
|
|
51
|
+
if (value === null) return "null"
|
|
52
|
+
if (typeof value === "string") return JSON.stringify(value)
|
|
53
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value)
|
|
54
|
+
try {
|
|
55
|
+
return JSON.stringify(value)
|
|
56
|
+
} catch {
|
|
57
|
+
return String(value)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const summarizeCause = (cause: Cause.Cause<unknown>): string => {
|
|
62
|
+
const failure = Cause.failureOption(cause)
|
|
63
|
+
if (Option.isSome(failure)) {
|
|
64
|
+
const error = failure.value
|
|
65
|
+
if (error && typeof error === "object" && "_tag" in error && "message" in error) {
|
|
66
|
+
const message = String((error as any).message).trim()
|
|
67
|
+
return message ? `${(error as any)._tag}: ${message}` : String((error as any)._tag)
|
|
68
|
+
}
|
|
69
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
70
|
+
return String((error as any).message)
|
|
71
|
+
}
|
|
72
|
+
return String(error)
|
|
73
|
+
}
|
|
74
|
+
const pretty = Cause.pretty(cause, { renderErrorCause: false })
|
|
75
|
+
const firstLine = pretty.split("\n").find((line: string) => line.trim().length > 0)
|
|
76
|
+
return firstLine ?? "Unknown failure"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function Visualizer({ state, label }: { state: VisualizerState; label: string }) {
|
|
80
|
+
const [, forceUpdate] = useState(0)
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
return state.subscribe(() => forceUpdate((n) => n + 1))
|
|
84
|
+
}, [state])
|
|
85
|
+
|
|
86
|
+
const elapsed = formatDuration(state.elapsedMs)
|
|
87
|
+
|
|
88
|
+
if (state.status === "pending") {
|
|
89
|
+
return (
|
|
90
|
+
<vstack>
|
|
91
|
+
<hstack spacing={1}>
|
|
92
|
+
<text fg={Colors.brightCyan}>{SPINNER_FRAMES[state.spinnerIndex]}</text>
|
|
93
|
+
<text bold fg={Colors.brightWhite}>
|
|
94
|
+
{label}
|
|
95
|
+
</text>
|
|
96
|
+
</hstack>
|
|
97
|
+
<text fg={Colors.gray(10)}>└ Running… {elapsed}</text>
|
|
98
|
+
</vstack>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (state.status === "success") {
|
|
103
|
+
const showingResult = state.showResult && state.result !== undefined
|
|
104
|
+
return (
|
|
105
|
+
<vstack>
|
|
106
|
+
<hstack spacing={1}>
|
|
107
|
+
<text bold fg={Colors.brightGreen}>
|
|
108
|
+
✓
|
|
109
|
+
</text>
|
|
110
|
+
<text bold fg={Colors.brightGreen}>
|
|
111
|
+
{label}
|
|
112
|
+
</text>
|
|
113
|
+
</hstack>
|
|
114
|
+
{showingResult && <text fg={Colors.gray(20)}>├ {formatResult(state.result)}</text>}
|
|
115
|
+
<text fg={Colors.green}>
|
|
116
|
+
{showingResult ? "└" : "└"} Completed in {elapsed}
|
|
117
|
+
</text>
|
|
118
|
+
</vstack>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// failure
|
|
123
|
+
return (
|
|
124
|
+
<vstack>
|
|
125
|
+
<hstack spacing={1}>
|
|
126
|
+
<text bold fg={Colors.brightRed}>
|
|
127
|
+
✗
|
|
128
|
+
</text>
|
|
129
|
+
<text bold fg={Colors.brightRed}>
|
|
130
|
+
{label}
|
|
131
|
+
</text>
|
|
132
|
+
</hstack>
|
|
133
|
+
<text fg={Colors.red}>├ {state.errorSummary ?? "Failed"}</text>
|
|
134
|
+
<text fg={Colors.red}>└ Failed after {elapsed}</text>
|
|
135
|
+
</vstack>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// InlineRenderer Service - manages terminal state across multiple visualize()
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
interface InlineRendererService {
|
|
144
|
+
readonly stdout: NodeJS.WriteStream
|
|
145
|
+
readonly stdin: NodeJS.ReadStream
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Service that manages terminal state for inline rendering.
|
|
150
|
+
* Hides cursor on acquire, shows cursor on release.
|
|
151
|
+
*/
|
|
152
|
+
export class InlineRenderer extends Context.Tag("@effect-tui/react/InlineRenderer")<
|
|
153
|
+
InlineRenderer,
|
|
154
|
+
InlineRendererService
|
|
155
|
+
>() {}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Layer that provides InlineRenderer service.
|
|
159
|
+
* Manages terminal state: hides cursor on start, restores on finalize.
|
|
160
|
+
*/
|
|
161
|
+
export const InlineRendererLive: Layer.Layer<InlineRenderer> = Layer.scoped(
|
|
162
|
+
InlineRenderer,
|
|
163
|
+
Effect.gen(function* () {
|
|
164
|
+
const stdout = process.stdout
|
|
165
|
+
const stdin = process.stdin
|
|
166
|
+
|
|
167
|
+
// Setup: hide cursor, enable raw mode
|
|
168
|
+
stdout.write(ANSI.cursor.hide)
|
|
169
|
+
if (stdin.isTTY && stdin.setRawMode) {
|
|
170
|
+
stdin.setRawMode(true)
|
|
171
|
+
stdin.resume()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Teardown on scope close: show cursor, disable raw mode, pause stdin
|
|
175
|
+
yield* Effect.addFinalizer(() =>
|
|
176
|
+
Effect.sync(() => {
|
|
177
|
+
stdout.write(ANSI.cursor.show)
|
|
178
|
+
if (stdin.isTTY && stdin.setRawMode) {
|
|
179
|
+
stdin.setRawMode(false)
|
|
180
|
+
}
|
|
181
|
+
// Pause stdin so Node can exit
|
|
182
|
+
stdin.pause()
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return { stdout, stdin }
|
|
187
|
+
}),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// visualize() - uses InlineRenderer service
|
|
192
|
+
// ============================================================================
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Wrap an Effect with inline visualization.
|
|
196
|
+
* Shows a spinner while running, then success/failure status.
|
|
197
|
+
* Returns the effect's result or propagates its error.
|
|
198
|
+
*
|
|
199
|
+
* Requires InlineRenderer service (use InlineRendererLive layer).
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```ts
|
|
203
|
+
* const program = Effect.gen(function* () {
|
|
204
|
+
* const result = yield* visualize("Fetching data", fetchData)
|
|
205
|
+
* yield* visualize("Processing", processData(result))
|
|
206
|
+
* }).pipe(Effect.provide(InlineRendererLive))
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export const visualize = <A, E, R>(
|
|
210
|
+
label: string,
|
|
211
|
+
effect: Effect.Effect<A, E, R>,
|
|
212
|
+
options?: VisualizeOptions,
|
|
213
|
+
): Effect.Effect<A, E, R | InlineRenderer> =>
|
|
214
|
+
Effect.gen(function* () {
|
|
215
|
+
const { stdout, stdin } = yield* InlineRenderer
|
|
216
|
+
|
|
217
|
+
// Create renderer with skipTerminalSetup (Layer manages terminal state)
|
|
218
|
+
const renderer = createRenderer({
|
|
219
|
+
mode: "inline",
|
|
220
|
+
stdout,
|
|
221
|
+
stdin,
|
|
222
|
+
skipTerminalSetup: true,
|
|
223
|
+
})
|
|
224
|
+
const root = createRoot(renderer)
|
|
225
|
+
|
|
226
|
+
const state = new VisualizerState()
|
|
227
|
+
state.showResult = options?.showResult ?? false
|
|
228
|
+
|
|
229
|
+
// Mount React component
|
|
230
|
+
root.render(<Visualizer state={state} label={label} />)
|
|
231
|
+
|
|
232
|
+
// Spinner tick fiber
|
|
233
|
+
const startTime = Date.now()
|
|
234
|
+
const spinnerFiber = yield* Effect.fork(
|
|
235
|
+
Effect.forever(
|
|
236
|
+
Effect.gen(function* () {
|
|
237
|
+
yield* Effect.sleep(SPIN_INTERVAL_MS)
|
|
238
|
+
state.update({
|
|
239
|
+
spinnerIndex: (state.spinnerIndex + 1) % SPINNER_FRAMES.length,
|
|
240
|
+
})
|
|
241
|
+
renderer.requestRender()
|
|
242
|
+
}),
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
// High-frequency elapsed timer so duration display stays smooth
|
|
247
|
+
const elapsedFiber = yield* Effect.fork(
|
|
248
|
+
Effect.forever(
|
|
249
|
+
Effect.gen(function* () {
|
|
250
|
+
yield* Effect.sleep(ELAPSED_INTERVAL_MS)
|
|
251
|
+
state.update({ elapsedMs: Date.now() - startTime })
|
|
252
|
+
renderer.requestRender()
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
// Run the actual effect
|
|
258
|
+
const exit = yield* Effect.exit(effect)
|
|
259
|
+
|
|
260
|
+
// Stop spinner, update state to final
|
|
261
|
+
yield* Fiber.interrupt(spinnerFiber)
|
|
262
|
+
yield* Fiber.interrupt(elapsedFiber)
|
|
263
|
+
const isSuccess = Exit.isSuccess(exit)
|
|
264
|
+
state.update(
|
|
265
|
+
isSuccess
|
|
266
|
+
? { status: "success", elapsedMs: Date.now() - startTime, result: exit.value }
|
|
267
|
+
: { status: "failure", elapsedMs: Date.now() - startTime, errorSummary: summarizeCause(exit.cause) },
|
|
268
|
+
)
|
|
269
|
+
renderer.requestRender()
|
|
270
|
+
renderer.flush() // Ensure final state is rendered
|
|
271
|
+
|
|
272
|
+
// Wait for completion animation
|
|
273
|
+
yield* Effect.sleep(COMPLETION_DELAY_MS)
|
|
274
|
+
|
|
275
|
+
// Stop renderer (doesn't touch terminal state since skipTerminalSetup)
|
|
276
|
+
renderer.stop()
|
|
277
|
+
|
|
278
|
+
// Write newline to advance cursor past the output
|
|
279
|
+
stdout.write("\n")
|
|
280
|
+
|
|
281
|
+
// Return result or propagate failure
|
|
282
|
+
if (Exit.isSuccess(exit)) return exit.value
|
|
283
|
+
return yield* Effect.failCause(exit.cause)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// Convenience: self-contained visualize that provides its own layer
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Self-contained visualize that manages its own terminal state.
|
|
292
|
+
* Use this for single visualize() calls. For multiple sequential calls,
|
|
293
|
+
* use visualize() with InlineRendererLive layer for better cursor handling.
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```ts
|
|
297
|
+
* const result = yield* visualizeSingle("Fetching data", fetchData)
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
export const visualizeSingle = <A, E, R>(
|
|
301
|
+
label: string,
|
|
302
|
+
effect: Effect.Effect<A, E, R>,
|
|
303
|
+
options?: VisualizeOptions,
|
|
304
|
+
): Effect.Effect<A, E, R> =>
|
|
305
|
+
Effect.scoped(visualize(label, effect, options).pipe(Effect.provide(InlineRendererLive))) as Effect.Effect<A, E, R>
|