@effect-tui/react 0.15.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. package/README.md +11 -2
  2. package/dist/src/codeblock.d.ts +1 -1
  3. package/dist/src/codeblock.d.ts.map +1 -1
  4. package/dist/src/codeblock.js +2 -2
  5. package/dist/src/codeblock.js.map +1 -1
  6. package/dist/src/components/ListView.d.ts +4 -4
  7. package/dist/src/components/ListView.d.ts.map +1 -1
  8. package/dist/src/components/ListView.js +16 -17
  9. package/dist/src/components/ListView.js.map +1 -1
  10. package/dist/src/components/Markdown.js +3 -3
  11. package/dist/src/components/Markdown.js.map +1 -1
  12. package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
  13. package/dist/src/components/MultilineTextInput.js +133 -305
  14. package/dist/src/components/MultilineTextInput.js.map +1 -1
  15. package/dist/src/components/TextInput.d.ts.map +1 -1
  16. package/dist/src/components/TextInput.js +51 -98
  17. package/dist/src/components/TextInput.js.map +1 -1
  18. package/dist/src/components/text-editing.d.ts +61 -0
  19. package/dist/src/components/text-editing.d.ts.map +1 -1
  20. package/dist/src/components/text-editing.js +131 -0
  21. package/dist/src/components/text-editing.js.map +1 -1
  22. package/dist/src/console/ConsolePopover.d.ts +7 -1
  23. package/dist/src/console/ConsolePopover.d.ts.map +1 -1
  24. package/dist/src/console/ConsolePopover.js +55 -74
  25. package/dist/src/console/ConsolePopover.js.map +1 -1
  26. package/dist/src/debug/DebugOverlay.d.ts.map +1 -1
  27. package/dist/src/debug/DebugOverlay.js +3 -57
  28. package/dist/src/debug/DebugOverlay.js.map +1 -1
  29. package/dist/src/debug/DiagnosticsPanel.js +1 -1
  30. package/dist/src/debug/DiagnosticsPanel.js.map +1 -1
  31. package/dist/src/dev.d.ts +5 -117
  32. package/dist/src/dev.d.ts.map +1 -1
  33. package/dist/src/dev.js +3 -333
  34. package/dist/src/dev.js.map +1 -1
  35. package/dist/src/hooks/use-scroll.d.ts +31 -35
  36. package/dist/src/hooks/use-scroll.d.ts.map +1 -1
  37. package/dist/src/hooks/use-scroll.js +51 -90
  38. package/dist/src/hooks/use-scroll.js.map +1 -1
  39. package/dist/src/hosts/base.d.ts +13 -2
  40. package/dist/src/hosts/base.d.ts.map +1 -1
  41. package/dist/src/hosts/base.js +74 -2
  42. package/dist/src/hosts/base.js.map +1 -1
  43. package/dist/src/hosts/box.d.ts +2 -2
  44. package/dist/src/hosts/box.d.ts.map +1 -1
  45. package/dist/src/hosts/box.js +29 -2
  46. package/dist/src/hosts/box.js.map +1 -1
  47. package/dist/src/hosts/canvas.d.ts +24 -4
  48. package/dist/src/hosts/canvas.d.ts.map +1 -1
  49. package/dist/src/hosts/canvas.js +107 -41
  50. package/dist/src/hosts/canvas.js.map +1 -1
  51. package/dist/src/hosts/codeblock.d.ts +10 -12
  52. package/dist/src/hosts/codeblock.d.ts.map +1 -1
  53. package/dist/src/hosts/codeblock.js +38 -35
  54. package/dist/src/hosts/codeblock.js.map +1 -1
  55. package/dist/src/hosts/flex-container.d.ts +3 -3
  56. package/dist/src/hosts/flex-container.d.ts.map +1 -1
  57. package/dist/src/hosts/flex-container.js +20 -5
  58. package/dist/src/hosts/flex-container.js.map +1 -1
  59. package/dist/src/hosts/index.d.ts +3 -2
  60. package/dist/src/hosts/index.d.ts.map +1 -1
  61. package/dist/src/hosts/index.js +2 -1
  62. package/dist/src/hosts/index.js.map +1 -1
  63. package/dist/src/hosts/layout-helpers.d.ts +10 -0
  64. package/dist/src/hosts/layout-helpers.d.ts.map +1 -0
  65. package/dist/src/hosts/layout-helpers.js +10 -0
  66. package/dist/src/hosts/layout-helpers.js.map +1 -0
  67. package/dist/src/hosts/leaf.d.ts +14 -0
  68. package/dist/src/hosts/leaf.d.ts.map +1 -0
  69. package/dist/src/hosts/leaf.js +31 -0
  70. package/dist/src/hosts/leaf.js.map +1 -0
  71. package/dist/src/hosts/overlay-item.d.ts +2 -2
  72. package/dist/src/hosts/overlay-item.d.ts.map +1 -1
  73. package/dist/src/hosts/overlay-item.js +7 -2
  74. package/dist/src/hosts/overlay-item.js.map +1 -1
  75. package/dist/src/hosts/overlay.d.ts +2 -2
  76. package/dist/src/hosts/overlay.d.ts.map +1 -1
  77. package/dist/src/hosts/overlay.js +6 -9
  78. package/dist/src/hosts/overlay.js.map +1 -1
  79. package/dist/src/hosts/scroll.d.ts +54 -26
  80. package/dist/src/hosts/scroll.d.ts.map +1 -1
  81. package/dist/src/hosts/scroll.js +185 -87
  82. package/dist/src/hosts/scroll.js.map +1 -1
  83. package/dist/src/hosts/single-child.d.ts.map +1 -1
  84. package/dist/src/hosts/single-child.js +2 -0
  85. package/dist/src/hosts/single-child.js.map +1 -1
  86. package/dist/src/hosts/spacer.d.ts +3 -3
  87. package/dist/src/hosts/spacer.d.ts.map +1 -1
  88. package/dist/src/hosts/spacer.js +8 -3
  89. package/dist/src/hosts/spacer.js.map +1 -1
  90. package/dist/src/hosts/text.d.ts +22 -18
  91. package/dist/src/hosts/text.d.ts.map +1 -1
  92. package/dist/src/hosts/text.js +108 -131
  93. package/dist/src/hosts/text.js.map +1 -1
  94. package/dist/src/hosts/vstack.js +1 -1
  95. package/dist/src/hosts/vstack.js.map +1 -1
  96. package/dist/src/hosts/zstack.d.ts +3 -3
  97. package/dist/src/hosts/zstack.d.ts.map +1 -1
  98. package/dist/src/hosts/zstack.js +13 -8
  99. package/dist/src/hosts/zstack.js.map +1 -1
  100. package/dist/src/index.d.ts +2 -2
  101. package/dist/src/index.d.ts.map +1 -1
  102. package/dist/src/internal/dev/hmr.d.ts +20 -0
  103. package/dist/src/internal/dev/hmr.d.ts.map +1 -0
  104. package/dist/src/internal/dev/hmr.js +93 -0
  105. package/dist/src/internal/dev/hmr.js.map +1 -0
  106. package/dist/src/internal/dev/runtime.d.ts +24 -0
  107. package/dist/src/internal/dev/runtime.d.ts.map +1 -0
  108. package/dist/src/internal/dev/runtime.js +135 -0
  109. package/dist/src/internal/dev/runtime.js.map +1 -0
  110. package/dist/src/internal/dev/ui.d.ts +13 -0
  111. package/dist/src/internal/dev/ui.d.ts.map +1 -0
  112. package/dist/src/internal/dev/ui.js +51 -0
  113. package/dist/src/internal/dev/ui.js.map +1 -0
  114. package/dist/src/internal/renderer/context.d.ts +9 -0
  115. package/dist/src/internal/renderer/context.d.ts.map +1 -0
  116. package/dist/src/internal/renderer/context.js +22 -0
  117. package/dist/src/internal/renderer/context.js.map +1 -0
  118. package/dist/src/internal/renderer/core/FrameBuilder.d.ts +18 -0
  119. package/dist/src/internal/renderer/core/FrameBuilder.d.ts.map +1 -0
  120. package/dist/src/internal/renderer/core/FrameBuilder.js +40 -0
  121. package/dist/src/internal/renderer/core/FrameBuilder.js.map +1 -0
  122. package/dist/src/internal/renderer/core/RendererState.d.ts +41 -0
  123. package/dist/src/internal/renderer/core/RendererState.d.ts.map +1 -0
  124. package/dist/src/internal/renderer/core/RendererState.js +70 -0
  125. package/dist/src/internal/renderer/core/RendererState.js.map +1 -0
  126. package/dist/src/internal/renderer/core/index.d.ts +3 -0
  127. package/dist/src/internal/renderer/core/index.d.ts.map +1 -0
  128. package/dist/src/internal/renderer/core/index.js +3 -0
  129. package/dist/src/internal/renderer/core/index.js.map +1 -0
  130. package/dist/src/internal/renderer/index.d.ts +40 -0
  131. package/dist/src/internal/renderer/index.d.ts.map +1 -0
  132. package/dist/src/internal/renderer/index.js +543 -0
  133. package/dist/src/internal/renderer/index.js.map +1 -0
  134. package/dist/src/internal/renderer/input/InputProcessor.d.ts +30 -0
  135. package/dist/src/internal/renderer/input/InputProcessor.d.ts.map +1 -0
  136. package/dist/src/internal/renderer/input/InputProcessor.js +122 -0
  137. package/dist/src/internal/renderer/input/InputProcessor.js.map +1 -0
  138. package/dist/src/internal/renderer/input/index.d.ts +2 -0
  139. package/dist/src/internal/renderer/input/index.d.ts.map +1 -0
  140. package/dist/src/internal/renderer/input/index.js +2 -0
  141. package/dist/src/internal/renderer/input/index.js.map +1 -0
  142. package/dist/src/internal/renderer/lifecycle/EventBus.d.ts +42 -0
  143. package/dist/src/internal/renderer/lifecycle/EventBus.d.ts.map +1 -0
  144. package/dist/src/internal/renderer/lifecycle/EventBus.js +97 -0
  145. package/dist/src/internal/renderer/lifecycle/EventBus.js.map +1 -0
  146. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts +13 -0
  147. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts.map +1 -0
  148. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js +111 -0
  149. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js.map +1 -0
  150. package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts +3 -0
  151. package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts.map +1 -0
  152. package/dist/src/internal/renderer/lifecycle/RenderCache.js +9 -0
  153. package/dist/src/internal/renderer/lifecycle/RenderCache.js.map +1 -0
  154. package/dist/src/internal/renderer/lifecycle/index.d.ts +4 -0
  155. package/dist/src/internal/renderer/lifecycle/index.d.ts.map +1 -0
  156. package/dist/src/internal/renderer/lifecycle/index.js +4 -0
  157. package/dist/src/internal/renderer/lifecycle/index.js.map +1 -0
  158. package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts +12 -0
  159. package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts.map +1 -0
  160. package/dist/src/internal/renderer/modes/FullscreenRenderer.js +54 -0
  161. package/dist/src/internal/renderer/modes/FullscreenRenderer.js.map +1 -0
  162. package/dist/src/internal/renderer/modes/InlineRenderer.d.ts +25 -0
  163. package/dist/src/internal/renderer/modes/InlineRenderer.d.ts.map +1 -0
  164. package/dist/src/internal/renderer/modes/InlineRenderer.js +166 -0
  165. package/dist/src/internal/renderer/modes/InlineRenderer.js.map +1 -0
  166. package/dist/src/internal/renderer/modes/RendererMode.d.ts +42 -0
  167. package/dist/src/internal/renderer/modes/RendererMode.d.ts.map +1 -0
  168. package/dist/src/internal/renderer/modes/RendererMode.js +2 -0
  169. package/dist/src/internal/renderer/modes/RendererMode.js.map +1 -0
  170. package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts +25 -0
  171. package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts.map +1 -0
  172. package/dist/src/internal/renderer/modes/StaticContentRenderer.js +49 -0
  173. package/dist/src/internal/renderer/modes/StaticContentRenderer.js.map +1 -0
  174. package/dist/src/internal/renderer/modes/index.d.ts +5 -0
  175. package/dist/src/internal/renderer/modes/index.d.ts.map +1 -0
  176. package/dist/src/internal/renderer/modes/index.js +4 -0
  177. package/dist/src/internal/renderer/modes/index.js.map +1 -0
  178. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts +13 -0
  179. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts.map +1 -0
  180. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js +75 -0
  181. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js.map +1 -0
  182. package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts +29 -0
  183. package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts.map +1 -0
  184. package/dist/src/internal/renderer/terminal/TerminalSetup.js +82 -0
  185. package/dist/src/internal/renderer/terminal/TerminalSetup.js.map +1 -0
  186. package/dist/src/internal/renderer/terminal/index.d.ts +3 -0
  187. package/dist/src/internal/renderer/terminal/index.d.ts.map +1 -0
  188. package/dist/src/internal/renderer/terminal/index.js +3 -0
  189. package/dist/src/internal/renderer/terminal/index.js.map +1 -0
  190. package/dist/src/internal/renderer/types.d.ts +122 -0
  191. package/dist/src/internal/renderer/types.d.ts.map +1 -0
  192. package/dist/src/internal/renderer/types.js +2 -0
  193. package/dist/src/internal/renderer/types.js.map +1 -0
  194. package/dist/src/motion/hooks.d.ts +1 -1
  195. package/dist/src/motion/hooks.js +1 -1
  196. package/dist/src/reconciler/host-config.js +2 -2
  197. package/dist/src/reconciler/host-config.js.map +1 -1
  198. package/dist/src/reconciler/types.d.ts +5 -1
  199. package/dist/src/reconciler/types.d.ts.map +1 -1
  200. package/dist/src/renderer-context.d.ts +1 -8
  201. package/dist/src/renderer-context.d.ts.map +1 -1
  202. package/dist/src/renderer-context.js +1 -21
  203. package/dist/src/renderer-context.js.map +1 -1
  204. package/dist/src/renderer-types.d.ts +1 -115
  205. package/dist/src/renderer-types.d.ts.map +1 -1
  206. package/dist/src/renderer.d.ts +1 -31
  207. package/dist/src/renderer.d.ts.map +1 -1
  208. package/dist/src/renderer.js +1 -495
  209. package/dist/src/renderer.js.map +1 -1
  210. package/dist/src/test/render-tui.d.ts +3 -3
  211. package/dist/src/test/render-tui.d.ts.map +1 -1
  212. package/dist/src/test/render-tui.js +16 -9
  213. package/dist/src/test/render-tui.js.map +1 -1
  214. package/dist/src/utils/alignment.d.ts +1 -1
  215. package/dist/src/utils/alignment.d.ts.map +1 -1
  216. package/dist/src/utils/alignment.js +0 -2
  217. package/dist/src/utils/alignment.js.map +1 -1
  218. package/dist/src/utils/border.d.ts +1 -1
  219. package/dist/src/utils/border.d.ts.map +1 -1
  220. package/dist/src/utils/border.js +2 -0
  221. package/dist/src/utils/border.js.map +1 -1
  222. package/dist/src/utils/console-helpers.d.ts +19 -0
  223. package/dist/src/utils/console-helpers.d.ts.map +1 -0
  224. package/dist/src/utils/console-helpers.js +61 -0
  225. package/dist/src/utils/console-helpers.js.map +1 -0
  226. package/dist/src/utils/index.d.ts +2 -1
  227. package/dist/src/utils/index.d.ts.map +1 -1
  228. package/dist/src/utils/index.js +2 -1
  229. package/dist/src/utils/index.js.map +1 -1
  230. package/dist/src/utils/styles.d.ts +8 -1
  231. package/dist/src/utils/styles.d.ts.map +1 -1
  232. package/dist/src/utils/styles.js +10 -8
  233. package/dist/src/utils/styles.js.map +1 -1
  234. package/dist/src/utils/text-layout.d.ts +22 -0
  235. package/dist/src/utils/text-layout.d.ts.map +1 -0
  236. package/dist/src/utils/text-layout.js +37 -0
  237. package/dist/src/utils/text-layout.js.map +1 -0
  238. package/dist/src/utils/text-wrap.d.ts +31 -1
  239. package/dist/src/utils/text-wrap.d.ts.map +1 -1
  240. package/dist/src/utils/text-wrap.js +205 -48
  241. package/dist/src/utils/text-wrap.js.map +1 -1
  242. package/dist/src/visualize/index.js +1 -1
  243. package/dist/src/visualize/index.js.map +1 -1
  244. package/dist/tsconfig.tsbuildinfo +1 -1
  245. package/package.json +2 -2
  246. package/src/codeblock.tsx +2 -2
  247. package/src/components/ListView.tsx +21 -23
  248. package/src/components/Markdown.tsx +3 -3
  249. package/src/components/MultilineTextInput.tsx +138 -344
  250. package/src/components/TextInput.tsx +54 -99
  251. package/src/components/text-editing.ts +180 -0
  252. package/src/console/ConsolePopover.tsx +124 -107
  253. package/src/debug/DebugOverlay.ts +15 -74
  254. package/src/debug/DiagnosticsPanel.tsx +1 -1
  255. package/src/dev.tsx +5 -458
  256. package/src/hooks/use-scroll.ts +85 -145
  257. package/src/hosts/base.ts +86 -3
  258. package/src/hosts/box.ts +37 -2
  259. package/src/hosts/canvas.ts +128 -42
  260. package/src/hosts/codeblock.ts +48 -35
  261. package/src/hosts/flex-container.ts +25 -6
  262. package/src/hosts/index.ts +11 -2
  263. package/src/hosts/layout-helpers.ts +20 -0
  264. package/src/hosts/leaf.ts +36 -0
  265. package/src/hosts/overlay-item.ts +8 -2
  266. package/src/hosts/overlay.ts +13 -11
  267. package/src/hosts/scroll.ts +228 -106
  268. package/src/hosts/single-child.ts +2 -0
  269. package/src/hosts/spacer.ts +8 -3
  270. package/src/hosts/text.ts +126 -132
  271. package/src/hosts/vstack.ts +1 -1
  272. package/src/hosts/zstack.ts +14 -9
  273. package/src/index.ts +2 -2
  274. package/src/internal/dev/hmr.ts +101 -0
  275. package/src/internal/dev/runtime.ts +170 -0
  276. package/src/internal/dev/ui.tsx +87 -0
  277. package/src/internal/renderer/context.ts +27 -0
  278. package/src/{renderer → internal/renderer}/core/FrameBuilder.ts +2 -2
  279. package/src/internal/renderer/index.ts +689 -0
  280. package/src/{renderer → internal/renderer}/input/InputProcessor.ts +10 -1
  281. package/src/{renderer → internal/renderer}/lifecycle/EventBus.ts +9 -1
  282. package/src/internal/renderer/lifecycle/ProcessLifecycle.ts +125 -0
  283. package/src/internal/renderer/lifecycle/index.ts +3 -0
  284. package/src/{renderer → internal/renderer}/modes/InlineRenderer.ts +5 -2
  285. package/src/{renderer → internal/renderer}/modes/RendererMode.ts +1 -1
  286. package/src/{renderer → internal/renderer}/modes/StaticContentRenderer.ts +5 -2
  287. package/src/internal/renderer/terminal/KeyboardCapabilityProbe.ts +91 -0
  288. package/src/{renderer/lifecycle → internal/renderer/terminal}/TerminalSetup.ts +4 -22
  289. package/src/internal/renderer/terminal/index.ts +2 -0
  290. package/src/internal/renderer/types.ts +129 -0
  291. package/src/motion/hooks.ts +1 -1
  292. package/src/reconciler/host-config.ts +2 -2
  293. package/src/reconciler/types.ts +7 -1
  294. package/src/renderer-context.ts +1 -27
  295. package/src/renderer-types.ts +10 -123
  296. package/src/renderer.ts +1 -619
  297. package/src/test/render-tui.ts +16 -10
  298. package/src/utils/alignment.ts +1 -3
  299. package/src/utils/border.ts +11 -1
  300. package/src/utils/console-helpers.ts +86 -0
  301. package/src/utils/index.ts +15 -1
  302. package/src/utils/styles.ts +16 -4
  303. package/src/utils/text-layout.ts +65 -0
  304. package/src/utils/text-wrap.ts +261 -48
  305. package/src/visualize/index.tsx +1 -1
  306. package/src/renderer/lifecycle/ResizeManager.ts +0 -65
  307. package/src/renderer/lifecycle/index.ts +0 -4
  308. /package/src/{renderer → internal/renderer}/core/RendererState.ts +0 -0
  309. /package/src/{renderer → internal/renderer}/core/index.ts +0 -0
  310. /package/src/{renderer → internal/renderer}/input/index.ts +0 -0
  311. /package/src/{renderer → internal/renderer}/lifecycle/RenderCache.ts +0 -0
  312. /package/src/{renderer → internal/renderer}/modes/FullscreenRenderer.ts +0 -0
  313. /package/src/{renderer → internal/renderer}/modes/index.ts +0 -0
package/src/dev.tsx CHANGED
@@ -1,463 +1,10 @@
1
1
  /**
2
2
  * Development utilities for hot module replacement (HMR).
3
- *
4
3
  * Use render(..., { dev: true, importMeta: import.meta }) to enable HMR.
5
- *
6
- * Enables hot reload during development:
7
- * - File changes trigger re-render without process restart
8
- * - Terminal stays stable (no screen flash)
9
- * - State preserved when using Effect's globalValue with effect-atom
10
- * - Debug console panel (` to toggle, ~ for screenshot)
11
- * - Remote control when EFFECT_TUI_REMOTE=1
12
4
  */
13
5
 
14
- import { readFileSync } from "node:fs"
15
- import { stat } from "node:fs/promises"
16
- import { dirname } from "node:path"
17
- import { Colors } from "@effect-tui/core"
18
- import * as watcher from "@parcel/watcher"
19
- import { globalValue } from "effect/GlobalValue"
20
- import React from "react"
21
- import { Overlay } from "./components/Overlay.js"
22
- import { ConsolePopover } from "./console/ConsolePopover.js"
23
- import { copyToClipboard, copyToClipboardSync } from "./console/clipboard.js"
24
- import { useConsole } from "./console/useConsole.js"
25
- import { DiagnosticsPanel } from "./debug/DiagnosticsPanel.js"
26
- import { ToastContainer, ToastProvider, useToast } from "./dev/Toast.js"
27
- import { useKeyboard } from "./hooks/use-keyboard.js"
28
- import { enableRemote } from "./remote/index.js"
29
- import { useRenderer, useTerminalSize } from "./renderer-context.js"
30
- import type { RendererOptions, TuiRenderer } from "./renderer-types.js"
31
-
32
- /**
33
- * Pipeable combinator that makes an atom persist across hot reloads.
34
- *
35
- * This is the cleanest way to define HMR-persistent atoms - just pipe it!
36
- * Combines globalValue (for persistence) + keepAlive (prevents registry cleanup).
37
- *
38
- * @example
39
- * ```tsx
40
- * import { Atom } from "@effect-atom/atom-react"
41
- * import { hmr, render } from "@effect-tui/react"
42
- *
43
- * // Just pipe hmr() - that's it!
44
- * const countAtom = Atom.make(0).pipe(hmr("count"))
45
- * const userAtom = Atom.make<User | null>(null).pipe(hmr("user"))
46
- *
47
- * export default function App() {
48
- * const count = useAtomValue(countAtom)
49
- * // Edit this file - count persists!
50
- * }
51
- *
52
- * render(<App />, { dev: true, importMeta: import.meta })
53
- * ```
54
- */
55
- export function hmr(key: string): <A,>(self: A) => A {
56
- return <A,>(self: A): A => {
57
- return globalValue(Symbol.for(`hmr/${key}`), () => {
58
- // Apply keepAlive if it's an atom (just sets keepAlive: true)
59
- if (typeof self === "object" && self !== null && "keepAlive" in self) {
60
- return Object.assign(Object.create(Object.getPrototypeOf(self)), {
61
- ...self,
62
- keepAlive: true,
63
- })
64
- }
65
- return self
66
- }) as A
67
- }
68
- }
69
-
70
- /**
71
- * Wrap any value creation in HMR persistence.
72
- *
73
- * @example
74
- * ```tsx
75
- * const countAtom = hmrState("count", () => Atom.make(0).pipe(Atom.keepAlive))
76
- * ```
77
- */
78
- export function hmrState<T>(key: string, create: () => T): T {
79
- return globalValue(Symbol.for(`hmr/${key}`), create)
80
- }
81
-
82
- // Cache for runtime key extraction
83
- const keyCache = new Map<string, string>()
84
-
85
- /**
86
- * Parse stack trace to get file path and line number.
87
- * Returns null if parsing fails.
88
- */
89
- function parseStack(stack: string): { file: string; line: number; col: number } | null {
90
- // Bun stack format: " at functionName (file:line:col)" or " at file:line:col"
91
- const lines = stack.split("\n")
92
- // Skip first line (Error message) and find caller (skip autoHmr itself)
93
- for (let i = 2; i < lines.length; i++) {
94
- const match = lines[i].match(/\((.+?):(\d+):(\d+)\)/) || lines[i].match(/at\s+(.+?):(\d+):(\d+)/)
95
- if (match) {
96
- return { file: match[1], line: parseInt(match[2], 10), col: parseInt(match[3], 10) }
97
- }
98
- }
99
- return null
100
- }
101
-
102
- /**
103
- * Extract variable name from source line using regex.
104
- * Returns null if extraction fails.
105
- */
106
- function extractVarName(source: string, line: number): string | null {
107
- const lines = source.split("\n")
108
- if (line < 1 || line > lines.length) return null
109
-
110
- const sourceLine = lines[line - 1]
111
- const match = sourceLine.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=/)
112
- return match?.[1] ?? null
113
- }
114
-
115
- /**
116
- * Auto-keyed HMR persistence for atoms.
117
- *
118
- * When used with the HMR plugin (recommended), keys are injected at load time
119
- * based on the variable name. Without the plugin, falls back to runtime
120
- * stack trace parsing.
121
- *
122
- * @example
123
- * ```tsx
124
- * import { Atom } from "@effect-atom/atom-react"
125
- * import { autoHmr, render } from "@effect-tui/react"
126
- *
127
- * // No manual key needed - derived from variable name!
128
- * const countAtom = Atom.make(0).pipe(autoHmr)
129
- * const userAtom = Atom.make<User | null>(null).pipe(autoHmr)
130
- *
131
- * // To enable compile-time transform (faster, more reliable):
132
- * // bun --preload @effect-tui/react/hmr-plugin run src/app.tsx
133
- * ```
134
- */
135
- export function autoHmr<A>(self: A): A {
136
- // Get stack trace for caller location
137
- const stack = new Error().stack ?? ""
138
- const location = parseStack(stack)
139
-
140
- if (!location) {
141
- // Fallback: use a hash of the stack trace
142
- const fallbackKey = `unknown:${stack.slice(0, 100)}`
143
- return hmr(fallbackKey)(self)
144
- }
145
-
146
- const cacheKey = `${location.file}:${location.line}:${location.col}`
147
-
148
- // Check cache first
149
- let key = keyCache.get(cacheKey)
150
- if (!key) {
151
- try {
152
- // Read source file and extract variable name
153
- const source = readFileSync(location.file, "utf-8")
154
- const varName = extractVarName(source, location.line)
155
- key = `${location.file}:${varName ?? `line${location.line}`}`
156
- } catch {
157
- // File read failed, use line-based key
158
- key = `${location.file}:line${location.line}`
159
- }
160
- keyCache.set(cacheKey, key)
161
- }
162
-
163
- return hmr(key)(self)
164
- }
165
-
166
- export interface DevOptions {
167
- /** Directories to watch (defaults to entry file's directory) */
168
- watchDirs?: string[]
169
- /** File extensions to watch (defaults to [".ts", ".tsx"]) */
170
- watchExtensions?: string[]
171
- /** Debounce delay in ms (defaults to 150) */
172
- debounce?: number
173
- /** Wait for file writes to stabilize (defaults to 50ms) */
174
- awaitWriteFinish?: number
175
- /** Called before each hot reload */
176
- onReload?: () => void
177
- /** Called on errors */
178
- onError?: (error: Error) => void
179
- /** Show renderer diagnostics overlay. */
180
- showStats?: boolean
181
- /** Sampling window for renderer stats (ms). */
182
- statsSampleMs?: number
183
- /** Override diagnostics panel title. */
184
- statsTitle?: string
185
- }
186
-
187
- export interface DevRuntime {
188
- /** Promise that resolves after the first render + watcher setup. */
189
- ready: Promise<void>
190
- /** Manually trigger a reload. */
191
- reload: () => Promise<void>
192
- /** Stop watching and cleanup. */
193
- stop: () => Promise<void>
194
- }
195
-
196
- /**
197
- * Clear require.cache for project files to ensure fresh imports.
198
- * Bun uses require.cache internally even for ESM modules.
199
- * This is necessary because query-string cache-busting only affects
200
- * the entry point, not transitive dependencies.
201
- */
202
- function clearProjectCache(projectRoot: string): void {
203
- for (const file of Object.keys(require.cache)) {
204
- if (file.startsWith(projectRoot) && !file.includes("node_modules")) {
205
- delete require.cache[file]
206
- }
207
- }
208
- }
209
-
210
- /**
211
- * Wait for a file to stabilize (no size changes) before proceeding.
212
- * This avoids importing partially-written files.
213
- */
214
- async function awaitWriteFinish(path: string, stabilityMs: number): Promise<void> {
215
- let lastSize = -1
216
- let stableCount = 0
217
- const checkInterval = 10
218
-
219
- while (stableCount < stabilityMs / checkInterval) {
220
- try {
221
- const stats = await stat(path)
222
- if (stats.size === lastSize) {
223
- stableCount++
224
- } else {
225
- lastSize = stats.size
226
- stableCount = 0
227
- }
228
- } catch {
229
- // File might be temporarily unavailable during write
230
- stableCount = 0
231
- }
232
- await new Promise((r) => setTimeout(r, checkInterval))
233
- }
234
- }
235
-
236
- /**
237
- * Internal component that handles screenshot with toast notification
238
- */
239
- function ScreenshotHandler() {
240
- const renderer = useRenderer()
241
- const { show } = useToast()
242
-
243
- useKeyboard((key) => {
244
- // ~ (tilde) - screenshot
245
- if (key.name === "char" && key.text === "~") {
246
- const ansiOutput = renderer.getScreenshot()
247
- if (!ansiOutput) return
248
-
249
- // Copy actual content to clipboard
250
- const copied = copyToClipboardSync(ansiOutput)
251
- if (!copied) {
252
- copyToClipboard(ansiOutput)
253
- }
254
-
255
- // Also save to file as backup
256
- const tmpPath = `/tmp/tui-screenshot-${Date.now()}.txt`
257
- Bun.write(tmpPath, ansiOutput)
258
-
259
- show("Screenshot copied!", "screenshot", 2500)
260
- key.preventDefault?.()
261
- }
262
- })
263
-
264
- return null
265
- }
266
-
267
- /**
268
- * Overlay for renderer diagnostics stats.
269
- */
270
- function StatsOverlay({ sampleMs, title }: { sampleMs?: number; title?: string }) {
271
- const { width, height } = useTerminalSize()
272
-
273
- return (
274
- <vstack width={width} height={height}>
275
- <hstack width={width}>
276
- <spacer />
277
- <box padding={1} border="rounded" borderColor={Colors.ansi.gray(8)} bg={Colors.ansi.gray(2)}>
278
- <DiagnosticsPanel sampleMs={sampleMs} title={title ?? "Renderer Stats"} />
279
- </box>
280
- </hstack>
281
- <spacer />
282
- </vstack>
283
- )
284
- }
285
-
286
- export interface DevWrapperProps {
287
- children?: React.ReactNode
288
- showStats?: boolean
289
- statsSampleMs?: number
290
- statsTitle?: string
291
- mode?: "fullscreen" | "inline"
292
- }
293
-
294
- /**
295
- * Wrapper component that provides dev mode features:
296
- * - Debug console panel (` backtick to toggle)
297
- * - Screenshot support (~ tilde) with toast notification
298
- * - Auto-show console on errors
299
- * - Optional renderer stats overlay (showStats)
300
- *
301
- * Use this to wrap your app when you need dev UI features without the dev runtime
302
- * (e.g., when you need to pass props or load the app manually).
303
- */
304
- export function DevWrapper({
305
- children,
306
- showStats,
307
- statsSampleMs,
308
- statsTitle,
309
- mode,
310
- }: DevWrapperProps) {
311
- const { visible } = useConsole({ autoShowOnError: true, initiallyVisible: false })
312
-
313
- // Inline mode: content flows naturally in vstack
314
- if (mode === "inline") {
315
- return (
316
- <ToastProvider>
317
- <vstack>
318
- <ScreenshotHandler />
319
- <ToastContainer />
320
- {children}
321
- {visible && <ConsolePopover fixedHeight={5} mode="inline" />}
322
- </vstack>
323
- </ToastProvider>
324
- )
325
- }
326
-
327
- // Fullscreen mode: Overlay with toast/console positioned by alignment
328
- return (
329
- <ToastProvider>
330
- <Overlay>
331
- {/* Base - determines container size */}
332
-
333
- <ScreenshotHandler />
334
- {children}
335
-
336
- {/* Overlays */}
337
- {visible && (
338
- <Overlay.Item alignment={{ v: "bottom" }}>
339
- <ConsolePopover fixedHeight={12} />
340
- </Overlay.Item>
341
- )}
342
- {showStats && (
343
- <Overlay.Item alignment={{ v: "top", h: "right" }}>
344
- <StatsOverlay sampleMs={statsSampleMs} title={statsTitle} />
345
- </Overlay.Item>
346
- )}
347
- <Overlay.Item alignment={{ v: "top" }}>
348
- <ToastContainer />
349
- </Overlay.Item>
350
- </Overlay>
351
- </ToastProvider>
352
- )
353
- }
354
-
355
- type DevRoot = {
356
- render(element: React.ReactNode, sync?: boolean): void
357
- }
358
-
359
- export function startDevRuntime(
360
- entryPath: string,
361
- renderer: TuiRenderer,
362
- root: DevRoot,
363
- options?: DevOptions & { mode?: RendererOptions["mode"] },
364
- ): DevRuntime {
365
- const watchExtensions = options?.watchExtensions ?? [".ts", ".tsx"]
366
- const debounceMs = options?.debounce ?? 150
367
- const stabilityMs = options?.awaitWriteFinish ?? 50
368
-
369
- // Dev mode always enables remote control with entry path for identification
370
- const stopRemote = enableRemote(renderer, { entryPath })
371
-
372
- let debounceTimer: ReturnType<typeof setTimeout> | null = null
373
- let version = 0
374
- let subscription: watcher.AsyncSubscription | null = null
375
-
376
- // Determine project root for cache clearing
377
- const projectRoot = options?.watchDirs?.[0] ?? dirname(entryPath)
378
-
379
- const reload = async () => {
380
- const thisVersion = ++version
381
-
382
- try {
383
- // Clear module cache for project files before re-importing
384
- clearProjectCache(projectRoot)
385
-
386
- // Cache-bust by adding query string with version
387
- const mod = await import(`${entryPath}?v=${thisVersion}`)
388
-
389
- // Skip if a newer render was triggered
390
- if (thisVersion !== version) return
391
-
392
- const App = mod.default
393
-
394
- if (!App) {
395
- const err = new Error(`No default export found in ${entryPath}`)
396
- options?.onError?.(err)
397
- console.error("[effect-tui] Dev render failed:", err.message)
398
- return
399
- }
400
-
401
- // Render the component wrapped in DevWrapper for console panel
402
- const appElement = typeof App === "function" ? React.createElement(App) : App
403
- const wrapped = React.createElement(
404
- DevWrapper,
405
- {
406
- showStats: options?.showStats,
407
- statsSampleMs: options?.statsSampleMs,
408
- statsTitle: options?.statsTitle,
409
- mode: options?.mode,
410
- },
411
- appElement,
412
- )
413
- root.render(wrapped)
414
- } catch (err) {
415
- const error = err instanceof Error ? err : new Error(String(err))
416
- options?.onError?.(error)
417
- console.error("[effect-tui] Dev render error:", error.message)
418
- }
419
- }
420
-
421
- const ready = (async () => {
422
- await reload()
423
-
424
- try {
425
- subscription = await watcher.subscribe(projectRoot, async (err, events) => {
426
- if (err) {
427
- options?.onError?.(err)
428
- console.error("[effect-tui] Dev watcher error:", err)
429
- return
430
- }
431
-
432
- // Filter to relevant file extensions
433
- const relevantEvents = events.filter((event) => watchExtensions.some((ext) => event.path.endsWith(ext)))
434
-
435
- if (relevantEvents.length === 0) return
436
-
437
- // Debounce rapid changes
438
- if (debounceTimer) clearTimeout(debounceTimer)
439
- debounceTimer = setTimeout(async () => {
440
- // Wait for file writes to stabilize
441
- const changedPath = relevantEvents[0].path
442
- await awaitWriteFinish(changedPath, stabilityMs)
443
-
444
- options?.onReload?.()
445
- await reload()
446
- }, debounceMs)
447
- })
448
- } catch (err) {
449
- console.error(`[effect-tui] Failed to watch ${projectRoot}:`, err)
450
- }
451
- })()
452
-
453
- const stop = async () => {
454
- if (debounceTimer) clearTimeout(debounceTimer)
455
- if (subscription) {
456
- await subscription.unsubscribe()
457
- }
458
- stopRemote()
459
- renderer.stop()
460
- }
461
-
462
- return { ready, reload, stop }
463
- }
6
+ export { hmr, hmrState, autoHmr } from "./internal/dev/hmr.js"
7
+ export { DevWrapper } from "./internal/dev/ui.js"
8
+ export type { DevWrapperProps } from "./internal/dev/ui.js"
9
+ export { startDevRuntime } from "./internal/dev/runtime.js"
10
+ export type { DevOptions, DevRuntime } from "./internal/dev/runtime.js"