@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.
Files changed (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -0
  3. package/dist/jsx-dev-runtime.d.ts +3 -0
  4. package/dist/jsx-dev-runtime.d.ts.map +1 -0
  5. package/dist/jsx-dev-runtime.js +3 -0
  6. package/dist/jsx-dev-runtime.js.map +1 -0
  7. package/dist/jsx-runtime.d.ts +47 -0
  8. package/dist/jsx-runtime.d.ts.map +1 -0
  9. package/dist/jsx-runtime.js +6 -0
  10. package/dist/jsx-runtime.js.map +1 -0
  11. package/dist/src/codeblock.d.ts +9 -0
  12. package/dist/src/codeblock.d.ts.map +1 -0
  13. package/dist/src/codeblock.js +24 -0
  14. package/dist/src/codeblock.js.map +1 -0
  15. package/dist/src/constants.d.ts +3 -0
  16. package/dist/src/constants.d.ts.map +1 -0
  17. package/dist/src/constants.js +3 -0
  18. package/dist/src/constants.js.map +1 -0
  19. package/dist/src/debug/DiagnosticsPanel.d.ts +7 -0
  20. package/dist/src/debug/DiagnosticsPanel.d.ts.map +1 -0
  21. package/dist/src/debug/DiagnosticsPanel.js +13 -0
  22. package/dist/src/debug/DiagnosticsPanel.js.map +1 -0
  23. package/dist/src/highlight.d.ts +20 -0
  24. package/dist/src/highlight.d.ts.map +1 -0
  25. package/dist/src/highlight.js +51 -0
  26. package/dist/src/highlight.js.map +1 -0
  27. package/dist/src/hooks/index.d.ts +4 -0
  28. package/dist/src/hooks/index.d.ts.map +1 -0
  29. package/dist/src/hooks/index.js +3 -0
  30. package/dist/src/hooks/index.js.map +1 -0
  31. package/dist/src/hooks/use-keyboard.d.ts +18 -0
  32. package/dist/src/hooks/use-keyboard.d.ts.map +1 -0
  33. package/dist/src/hooks/use-keyboard.js +26 -0
  34. package/dist/src/hooks/use-keyboard.js.map +1 -0
  35. package/dist/src/hooks/use-paste.d.ts +5 -0
  36. package/dist/src/hooks/use-paste.d.ts.map +1 -0
  37. package/dist/src/hooks/use-paste.js +14 -0
  38. package/dist/src/hooks/use-paste.js.map +1 -0
  39. package/dist/src/hooks/useFrameStats.d.ts +7 -0
  40. package/dist/src/hooks/useFrameStats.d.ts.map +1 -0
  41. package/dist/src/hooks/useFrameStats.js +28 -0
  42. package/dist/src/hooks/useFrameStats.js.map +1 -0
  43. package/dist/src/hosts/base.d.ts +22 -0
  44. package/dist/src/hosts/base.d.ts.map +1 -0
  45. package/dist/src/hosts/base.js +53 -0
  46. package/dist/src/hosts/base.js.map +1 -0
  47. package/dist/src/hosts/box.d.ts +26 -0
  48. package/dist/src/hosts/box.d.ts.map +1 -0
  49. package/dist/src/hosts/box.js +84 -0
  50. package/dist/src/hosts/box.js.map +1 -0
  51. package/dist/src/hosts/canvas.d.ts +48 -0
  52. package/dist/src/hosts/canvas.d.ts.map +1 -0
  53. package/dist/src/hosts/canvas.js +109 -0
  54. package/dist/src/hosts/canvas.js.map +1 -0
  55. package/dist/src/hosts/codeblock.d.ts +32 -0
  56. package/dist/src/hosts/codeblock.d.ts.map +1 -0
  57. package/dist/src/hosts/codeblock.js +118 -0
  58. package/dist/src/hosts/codeblock.js.map +1 -0
  59. package/dist/src/hosts/hstack.d.ts +18 -0
  60. package/dist/src/hosts/hstack.d.ts.map +1 -0
  61. package/dist/src/hosts/hstack.js +45 -0
  62. package/dist/src/hosts/hstack.js.map +1 -0
  63. package/dist/src/hosts/index.d.ts +16 -0
  64. package/dist/src/hosts/index.d.ts.map +1 -0
  65. package/dist/src/hosts/index.js +40 -0
  66. package/dist/src/hosts/index.js.map +1 -0
  67. package/dist/src/hosts/spacer.d.ts +19 -0
  68. package/dist/src/hosts/spacer.d.ts.map +1 -0
  69. package/dist/src/hosts/spacer.js +28 -0
  70. package/dist/src/hosts/spacer.js.map +1 -0
  71. package/dist/src/hosts/text.d.ts +43 -0
  72. package/dist/src/hosts/text.d.ts.map +1 -0
  73. package/dist/src/hosts/text.js +148 -0
  74. package/dist/src/hosts/text.js.map +1 -0
  75. package/dist/src/hosts/vstack.d.ts +18 -0
  76. package/dist/src/hosts/vstack.d.ts.map +1 -0
  77. package/dist/src/hosts/vstack.js +45 -0
  78. package/dist/src/hosts/vstack.js.map +1 -0
  79. package/dist/src/hosts/zstack.d.ts +20 -0
  80. package/dist/src/hosts/zstack.d.ts.map +1 -0
  81. package/dist/src/hosts/zstack.js +65 -0
  82. package/dist/src/hosts/zstack.js.map +1 -0
  83. package/dist/src/index.d.ts +20 -0
  84. package/dist/src/index.d.ts.map +1 -0
  85. package/dist/src/index.js +20 -0
  86. package/dist/src/index.js.map +1 -0
  87. package/dist/src/inline/index.d.ts +32 -0
  88. package/dist/src/inline/index.d.ts.map +1 -0
  89. package/dist/src/inline/index.js +111 -0
  90. package/dist/src/inline/index.js.map +1 -0
  91. package/dist/src/jsx.d.ts +2 -0
  92. package/dist/src/jsx.d.ts.map +1 -0
  93. package/dist/src/jsx.js +4 -0
  94. package/dist/src/jsx.js.map +1 -0
  95. package/dist/src/motion/color-motion-value.d.ts +32 -0
  96. package/dist/src/motion/color-motion-value.d.ts.map +1 -0
  97. package/dist/src/motion/color-motion-value.js +80 -0
  98. package/dist/src/motion/color-motion-value.js.map +1 -0
  99. package/dist/src/motion/color.d.ts +30 -0
  100. package/dist/src/motion/color.d.ts.map +1 -0
  101. package/dist/src/motion/color.js +172 -0
  102. package/dist/src/motion/color.js.map +1 -0
  103. package/dist/src/motion/color.test.d.ts +2 -0
  104. package/dist/src/motion/color.test.d.ts.map +1 -0
  105. package/dist/src/motion/color.test.js +97 -0
  106. package/dist/src/motion/color.test.js.map +1 -0
  107. package/dist/src/motion/event-emitter.d.ts +18 -0
  108. package/dist/src/motion/event-emitter.d.ts.map +1 -0
  109. package/dist/src/motion/event-emitter.js +30 -0
  110. package/dist/src/motion/event-emitter.js.map +1 -0
  111. package/dist/src/motion/frame.d.ts +9 -0
  112. package/dist/src/motion/frame.d.ts.map +1 -0
  113. package/dist/src/motion/frame.js +51 -0
  114. package/dist/src/motion/frame.js.map +1 -0
  115. package/dist/src/motion/hooks.d.ts +75 -0
  116. package/dist/src/motion/hooks.d.ts.map +1 -0
  117. package/dist/src/motion/hooks.js +190 -0
  118. package/dist/src/motion/hooks.js.map +1 -0
  119. package/dist/src/motion/index.d.ts +4 -0
  120. package/dist/src/motion/index.d.ts.map +1 -0
  121. package/dist/src/motion/index.js +7 -0
  122. package/dist/src/motion/index.js.map +1 -0
  123. package/dist/src/motion/motion-value.d.ts +40 -0
  124. package/dist/src/motion/motion-value.d.ts.map +1 -0
  125. package/dist/src/motion/motion-value.js +109 -0
  126. package/dist/src/motion/motion-value.js.map +1 -0
  127. package/dist/src/motion/motion-value.test.d.ts +2 -0
  128. package/dist/src/motion/motion-value.test.d.ts.map +1 -0
  129. package/dist/src/motion/motion-value.test.js +177 -0
  130. package/dist/src/motion/motion-value.test.js.map +1 -0
  131. package/dist/src/motion/spring-math.d.ts +28 -0
  132. package/dist/src/motion/spring-math.d.ts.map +1 -0
  133. package/dist/src/motion/spring-math.js +81 -0
  134. package/dist/src/motion/spring-math.js.map +1 -0
  135. package/dist/src/motion/types.d.ts +25 -0
  136. package/dist/src/motion/types.d.ts.map +1 -0
  137. package/dist/src/motion/types.js +13 -0
  138. package/dist/src/motion/types.js.map +1 -0
  139. package/dist/src/output.d.ts +47 -0
  140. package/dist/src/output.d.ts.map +1 -0
  141. package/dist/src/output.js +125 -0
  142. package/dist/src/output.js.map +1 -0
  143. package/dist/src/profiler.d.ts +6 -0
  144. package/dist/src/profiler.d.ts.map +1 -0
  145. package/dist/src/profiler.js +73 -0
  146. package/dist/src/profiler.js.map +1 -0
  147. package/dist/src/reconciler/host-config.d.ts +16 -0
  148. package/dist/src/reconciler/host-config.d.ts.map +1 -0
  149. package/dist/src/reconciler/host-config.js +174 -0
  150. package/dist/src/reconciler/host-config.js.map +1 -0
  151. package/dist/src/reconciler/types.d.ts +52 -0
  152. package/dist/src/reconciler/types.d.ts.map +1 -0
  153. package/dist/src/reconciler/types.js +2 -0
  154. package/dist/src/reconciler/types.js.map +1 -0
  155. package/dist/src/renderer.d.ts +101 -0
  156. package/dist/src/renderer.d.ts.map +1 -0
  157. package/dist/src/renderer.js +509 -0
  158. package/dist/src/renderer.js.map +1 -0
  159. package/dist/src/terminal.d.ts +37 -0
  160. package/dist/src/terminal.d.ts.map +1 -0
  161. package/dist/src/terminal.js +65 -0
  162. package/dist/src/terminal.js.map +1 -0
  163. package/dist/src/test/index.d.ts +3 -0
  164. package/dist/src/test/index.d.ts.map +1 -0
  165. package/dist/src/test/index.js +3 -0
  166. package/dist/src/test/index.js.map +1 -0
  167. package/dist/src/test/mock-streams.d.ts +44 -0
  168. package/dist/src/test/mock-streams.d.ts.map +1 -0
  169. package/dist/src/test/mock-streams.js +136 -0
  170. package/dist/src/test/mock-streams.js.map +1 -0
  171. package/dist/src/test/render-tui.d.ts +47 -0
  172. package/dist/src/test/render-tui.d.ts.map +1 -0
  173. package/dist/src/test/render-tui.js +76 -0
  174. package/dist/src/test/render-tui.js.map +1 -0
  175. package/dist/src/trace/SpanTree.d.ts +10 -0
  176. package/dist/src/trace/SpanTree.d.ts.map +1 -0
  177. package/dist/src/trace/SpanTree.js +104 -0
  178. package/dist/src/trace/SpanTree.js.map +1 -0
  179. package/dist/src/trace/index.d.ts +30 -0
  180. package/dist/src/trace/index.d.ts.map +1 -0
  181. package/dist/src/trace/index.js +142 -0
  182. package/dist/src/trace/index.js.map +1 -0
  183. package/dist/src/trace/location.d.ts +9 -0
  184. package/dist/src/trace/location.d.ts.map +1 -0
  185. package/dist/src/trace/location.js +88 -0
  186. package/dist/src/trace/location.js.map +1 -0
  187. package/dist/src/trace/span-processor.d.ts +16 -0
  188. package/dist/src/trace/span-processor.d.ts.map +1 -0
  189. package/dist/src/trace/span-processor.js +54 -0
  190. package/dist/src/trace/span-processor.js.map +1 -0
  191. package/dist/src/trace/span-state.d.ts +79 -0
  192. package/dist/src/trace/span-state.d.ts.map +1 -0
  193. package/dist/src/trace/span-state.js +229 -0
  194. package/dist/src/trace/span-state.js.map +1 -0
  195. package/dist/src/trace/tui-logger.d.ts +8 -0
  196. package/dist/src/trace/tui-logger.d.ts.map +1 -0
  197. package/dist/src/trace/tui-logger.js +70 -0
  198. package/dist/src/trace/tui-logger.js.map +1 -0
  199. package/dist/src/utils/border.d.ts +31 -0
  200. package/dist/src/utils/border.d.ts.map +1 -0
  201. package/dist/src/utils/border.js +81 -0
  202. package/dist/src/utils/border.js.map +1 -0
  203. package/dist/src/utils/flex-layout.d.ts +20 -0
  204. package/dist/src/utils/flex-layout.d.ts.map +1 -0
  205. package/dist/src/utils/flex-layout.js +85 -0
  206. package/dist/src/utils/flex-layout.js.map +1 -0
  207. package/dist/src/utils/index.d.ts +5 -0
  208. package/dist/src/utils/index.d.ts.map +1 -0
  209. package/dist/src/utils/index.js +5 -0
  210. package/dist/src/utils/index.js.map +1 -0
  211. package/dist/src/utils/padding.d.ts +26 -0
  212. package/dist/src/utils/padding.d.ts.map +1 -0
  213. package/dist/src/utils/padding.js +34 -0
  214. package/dist/src/utils/padding.js.map +1 -0
  215. package/dist/src/utils/styles.d.ts +13 -0
  216. package/dist/src/utils/styles.d.ts.map +1 -0
  217. package/dist/src/utils/styles.js +5 -0
  218. package/dist/src/utils/styles.js.map +1 -0
  219. package/dist/src/visualize/index.d.ts +50 -0
  220. package/dist/src/visualize/index.d.ts.map +1 -0
  221. package/dist/src/visualize/index.js +194 -0
  222. package/dist/src/visualize/index.js.map +1 -0
  223. package/dist/tsconfig.tsbuildinfo +1 -0
  224. package/package.json +94 -0
  225. package/src/codeblock.tsx +47 -0
  226. package/src/constants.ts +2 -0
  227. package/src/debug/DiagnosticsPanel.tsx +38 -0
  228. package/src/highlight.ts +76 -0
  229. package/src/hooks/index.ts +3 -0
  230. package/src/hooks/use-keyboard.ts +37 -0
  231. package/src/hooks/use-paste.ts +14 -0
  232. package/src/hooks/useFrameStats.ts +32 -0
  233. package/src/hosts/base.ts +65 -0
  234. package/src/hosts/box.ts +105 -0
  235. package/src/hosts/canvas.ts +155 -0
  236. package/src/hosts/codeblock.ts +145 -0
  237. package/src/hosts/hstack.ts +64 -0
  238. package/src/hosts/index.ts +45 -0
  239. package/src/hosts/spacer.ts +40 -0
  240. package/src/hosts/text.ts +175 -0
  241. package/src/hosts/vstack.ts +64 -0
  242. package/src/hosts/zstack.ts +77 -0
  243. package/src/index.ts +62 -0
  244. package/src/inline/index.tsx +181 -0
  245. package/src/jsx.ts +3 -0
  246. package/src/motion/color-motion-value.ts +90 -0
  247. package/src/motion/color.test.ts +115 -0
  248. package/src/motion/color.ts +191 -0
  249. package/src/motion/event-emitter.ts +35 -0
  250. package/src/motion/frame.ts +59 -0
  251. package/src/motion/hooks.ts +237 -0
  252. package/src/motion/index.ts +17 -0
  253. package/src/motion/motion-value.test.ts +222 -0
  254. package/src/motion/motion-value.ts +140 -0
  255. package/src/motion/spring-math.ts +114 -0
  256. package/src/motion/types.ts +34 -0
  257. package/src/output.ts +156 -0
  258. package/src/profiler.ts +88 -0
  259. package/src/reconciler/host-config.ts +277 -0
  260. package/src/reconciler/types.ts +66 -0
  261. package/src/renderer.ts +661 -0
  262. package/src/terminal.ts +67 -0
  263. package/src/test/index.ts +8 -0
  264. package/src/test/mock-streams.ts +149 -0
  265. package/src/test/render-tui.ts +118 -0
  266. package/src/trace/SpanTree.tsx +195 -0
  267. package/src/trace/index.tsx +205 -0
  268. package/src/trace/location.ts +90 -0
  269. package/src/trace/span-processor.ts +65 -0
  270. package/src/trace/span-state.ts +286 -0
  271. package/src/trace/tui-logger.ts +72 -0
  272. package/src/utils/border.ts +108 -0
  273. package/src/utils/flex-layout.ts +125 -0
  274. package/src/utils/index.ts +4 -0
  275. package/src/utils/padding.ts +45 -0
  276. package/src/utils/styles.ts +14 -0
  277. 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>