@effect-tui/react 0.1.3 → 0.1.5

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