@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
@@ -0,0 +1,749 @@
1
+ import { useState, useCallback, useEffect, useMemo, useRef } from "react"
2
+ import { Colors, type Color, displayWidth, graphemes } from "@effect-tui/core"
3
+ import { useKeyboard } from "../hooks/use-keyboard.js"
4
+ import type { DrawContext } from "../hosts/canvas.js"
5
+ import {
6
+ type MultilineState,
7
+ graphemeColToCharIdx,
8
+ deleteWordBackwardMultiline,
9
+ killToEndMultiline,
10
+ killToStartMultiline,
11
+ transposeCharsMultiline,
12
+ deleteCharForwardMultiline,
13
+ deleteCharBackwardMultiline,
14
+ insertTextMultiline,
15
+ matchPrevWord,
16
+ matchNextWord,
17
+ } from "./text-editing.js"
18
+
19
+ export interface MultilineTextInputProps {
20
+ /** Current input value (controlled) */
21
+ value: string
22
+ /** Called when value changes */
23
+ onChange: (value: string) => void
24
+ /** Placeholder text shown when value is empty */
25
+ placeholder?: string
26
+ /** Width of the input (if omitted, fills available width) */
27
+ width?: number
28
+ /** Height of the input (visible lines) */
29
+ height?: number
30
+ /** Foreground color for text */
31
+ fg?: Color
32
+ /** Background color */
33
+ bg?: Color
34
+ /** Cursor foreground color */
35
+ cursorFg?: Color
36
+ /** Cursor background color */
37
+ cursorBg?: Color
38
+ /** Placeholder foreground color */
39
+ placeholderFg?: Color
40
+ /** Line number foreground color */
41
+ lineNumberFg?: Color
42
+ /** Called when Escape is pressed */
43
+ onCancel?: () => void
44
+ /** Called when Ctrl+Enter or Cmd+Enter is pressed */
45
+ onSubmit?: (value: string) => void
46
+ /** Whether the input is focused and accepts input */
47
+ focused?: boolean
48
+ /** Show cursor even when not focused */
49
+ showCursor?: boolean
50
+ /** Show line numbers (for logical lines) */
51
+ showLineNumbers?: boolean
52
+ /** Enable word wrap (default: true) */
53
+ wordWrap?: boolean
54
+ }
55
+
56
+ /** Logical cursor position */
57
+ interface CursorPos {
58
+ row: number // logical line index
59
+ col: number // grapheme index within logical line
60
+ }
61
+
62
+ /** A visual line that maps back to a logical line */
63
+ interface VisualLine {
64
+ logicalRow: number
65
+ startCol: number // grapheme index in logical line (inclusive)
66
+ endCol: number // grapheme index in logical line (exclusive)
67
+ text: string // the actual text for this visual line
68
+ }
69
+
70
+ /** Layout for a single logical line */
71
+ interface LineLayout {
72
+ graphemeList: string[]
73
+ widths: number[]
74
+ prefixWidths: number[] // cumulative widths, length = graphemes.length + 1
75
+ visualLines: VisualLine[]
76
+ }
77
+
78
+ /** Complete layout for all text */
79
+ interface Layout {
80
+ lines: LineLayout[]
81
+ allVisualLines: VisualLine[]
82
+ }
83
+
84
+ /**
85
+ * Wrap a single logical line into visual lines.
86
+ * Uses word boundaries with fallback to character wrap.
87
+ */
88
+ function wrapLogicalLine(line: string, logicalRow: number, maxWidth: number): LineLayout {
89
+ const graphemeList = graphemes(line)
90
+ const widths = graphemeList.map((g) => displayWidth(g))
91
+
92
+ // Build prefix widths for O(1) range width queries
93
+ const prefixWidths: number[] = [0]
94
+ for (let i = 0; i < widths.length; i++) {
95
+ prefixWidths.push(prefixWidths[i] + widths[i])
96
+ }
97
+
98
+ // Find break opportunities (after spaces)
99
+ const isBreakable = graphemeList.map((g) => /\s/.test(g))
100
+
101
+ const visualLines: VisualLine[] = []
102
+ let start = 0
103
+ let currentWidth = 0
104
+ let lastBreak = -1
105
+
106
+ for (let i = 0; i < graphemeList.length; i++) {
107
+ const w = widths[i]
108
+
109
+ if (currentWidth + w > maxWidth && start < i) {
110
+ // Need to wrap - backtrack to last break if possible
111
+ const breakAt = lastBreak >= start ? lastBreak + 1 : i
112
+
113
+ visualLines.push({
114
+ logicalRow,
115
+ startCol: start,
116
+ endCol: breakAt,
117
+ text: graphemeList.slice(start, breakAt).join(""),
118
+ })
119
+
120
+ start = breakAt
121
+ currentWidth = prefixWidths[i + 1] - prefixWidths[start]
122
+ lastBreak = -1
123
+ } else {
124
+ currentWidth += w
125
+ }
126
+
127
+ if (isBreakable[i]) {
128
+ lastBreak = i
129
+ }
130
+ }
131
+
132
+ // Final segment
133
+ visualLines.push({
134
+ logicalRow,
135
+ startCol: start,
136
+ endCol: graphemeList.length,
137
+ text: graphemeList.slice(start).join(""),
138
+ })
139
+
140
+ return { graphemeList, widths, prefixWidths, visualLines }
141
+ }
142
+
143
+ /**
144
+ * Build complete layout for text with word wrap.
145
+ */
146
+ function buildLayout(text: string, maxWidth: number): Layout {
147
+ const logicalLines = text.split("\n")
148
+ const lines: LineLayout[] = []
149
+ const allVisualLines: VisualLine[] = []
150
+
151
+ for (let row = 0; row < logicalLines.length; row++) {
152
+ const lineLayout = wrapLogicalLine(logicalLines[row], row, maxWidth)
153
+ lines.push(lineLayout)
154
+ allVisualLines.push(...lineLayout.visualLines)
155
+ }
156
+
157
+ return { lines, allVisualLines }
158
+ }
159
+
160
+ /**
161
+ * Convert logical cursor to visual row/col.
162
+ */
163
+ function logicalToVisual(layout: Layout, cursor: CursorPos): { visualRow: number; visualCol: number } {
164
+ const lineLayout = layout.lines[cursor.row]
165
+ if (!lineLayout) {
166
+ return { visualRow: 0, visualCol: 0 }
167
+ }
168
+
169
+ // Find which visual line contains this column
170
+ let visualRowOffset = 0
171
+ for (let r = 0; r < cursor.row; r++) {
172
+ visualRowOffset += layout.lines[r].visualLines.length
173
+ }
174
+
175
+ for (let i = 0; i < lineLayout.visualLines.length; i++) {
176
+ const vl = lineLayout.visualLines[i]
177
+ if (cursor.col >= vl.startCol && cursor.col < vl.endCol) {
178
+ // Cursor is in this visual line
179
+ const visualCol = lineLayout.prefixWidths[cursor.col] - lineLayout.prefixWidths[vl.startCol]
180
+ return { visualRow: visualRowOffset + i, visualCol }
181
+ }
182
+ }
183
+
184
+ // Cursor at end - use last visual line
185
+ const lastVl = lineLayout.visualLines[lineLayout.visualLines.length - 1]
186
+ const visualCol = lineLayout.prefixWidths[cursor.col] - lineLayout.prefixWidths[lastVl.startCol]
187
+ return {
188
+ visualRow: visualRowOffset + lineLayout.visualLines.length - 1,
189
+ visualCol,
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Convert visual row to logical cursor (for up/down navigation).
195
+ * Tries to preserve the visual X position.
196
+ */
197
+ function visualToLogical(layout: Layout, visualRow: number, targetVisualX: number): CursorPos {
198
+ if (visualRow < 0) {
199
+ return { row: 0, col: 0 }
200
+ }
201
+ if (visualRow >= layout.allVisualLines.length) {
202
+ const lastLine = layout.lines[layout.lines.length - 1]
203
+ return {
204
+ row: layout.lines.length - 1,
205
+ col: lastLine?.graphemeList.length ?? 0,
206
+ }
207
+ }
208
+
209
+ const vl = layout.allVisualLines[visualRow]
210
+ const lineLayout = layout.lines[vl.logicalRow]
211
+
212
+ // Binary search for the grapheme at targetVisualX
213
+ let col = vl.startCol
214
+ let currentX = 0
215
+ for (let i = vl.startCol; i < vl.endCol; i++) {
216
+ const w = lineLayout.widths[i]
217
+ if (currentX + w > targetVisualX) {
218
+ // If we're past halfway into this char, go to next
219
+ if (currentX + w / 2 <= targetVisualX) {
220
+ col = i + 1
221
+ } else {
222
+ col = i
223
+ }
224
+ break
225
+ }
226
+ currentX += w
227
+ col = i + 1
228
+ }
229
+
230
+ return { row: vl.logicalRow, col: Math.min(col, vl.endCol) }
231
+ }
232
+
233
+ /**
234
+ * A controlled multiline text input component for terminal UIs.
235
+ *
236
+ * Features:
237
+ * - Word wrap (fills available width)
238
+ * - Arrow key navigation between lines (respects visual line breaks)
239
+ * - Word-by-word navigation with Option/Alt+Arrow
240
+ * - Emacs keybindings: Ctrl+A/E (start/end of line), Ctrl+B/F (back/forward char),
241
+ * Ctrl+H/D (delete back/forward char), Ctrl+K/U (kill to end/start of line),
242
+ * Ctrl+W (delete word back), Ctrl+T (transpose), Ctrl+Y (yank),
243
+ * Ctrl+N/P (next/previous line)
244
+ * - Enter inserts newline, Ctrl+Enter/Cmd+Enter submits
245
+ * - Kill ring for Ctrl+Y (yank last killed text)
246
+ * - Scrolling when content exceeds visible height
247
+ * - Optional line numbers
248
+ *
249
+ * @example
250
+ * ```tsx
251
+ * const [text, setText] = useState("")
252
+ * <MultilineTextInput
253
+ * value={text}
254
+ * onChange={setText}
255
+ * height={10}
256
+ * />
257
+ * ```
258
+ */
259
+ export function MultilineTextInput({
260
+ value,
261
+ onChange,
262
+ placeholder = "",
263
+ width,
264
+ height = 5,
265
+ fg = Colors.brightWhite,
266
+ bg,
267
+ cursorFg = Colors.black,
268
+ cursorBg = Colors.brightWhite,
269
+ placeholderFg = Colors.gray(10),
270
+ lineNumberFg = Colors.gray(12),
271
+ onCancel,
272
+ onSubmit,
273
+ focused = true,
274
+ showCursor = true,
275
+ showLineNumbers = false,
276
+ wordWrap = true,
277
+ }: MultilineTextInputProps) {
278
+ const logicalLines = useMemo(() => value.split("\n"), [value])
279
+ const [cursor, setCursor] = useState<CursorPos>({ row: 0, col: 0 })
280
+ const [scrollY, setScrollY] = useState(0)
281
+ const [killRing, setKillRing] = useState("")
282
+ const [preferredVisualX, setPreferredVisualX] = useState(0)
283
+ // Ref to track whether preferredVisualX should be updated after cursor change
284
+ // Set to false only during vertical movement, true otherwise
285
+ const shouldUpdatePreferredXRef = useRef(true)
286
+
287
+ // We need to know content width for layout - use a ref updated in draw
288
+ const [contentWidth, setContentWidth] = useState(80)
289
+
290
+ // Calculate line number gutter width
291
+ const lineNumGutterWidth = showLineNumbers ? String(logicalLines.length).length + 1 : 0
292
+
293
+ // Build layout with word wrap
294
+ const layout = useMemo(() => {
295
+ if (!wordWrap) {
296
+ // No wrap - each logical line is one visual line
297
+ const lines: LineLayout[] = logicalLines.map((line, row) => {
298
+ const graphemeList = graphemes(line)
299
+ const widths = graphemeList.map((g) => displayWidth(g))
300
+ const prefixWidths = [0]
301
+ for (let i = 0; i < widths.length; i++) {
302
+ prefixWidths.push(prefixWidths[i] + widths[i])
303
+ }
304
+ return {
305
+ graphemeList,
306
+ widths,
307
+ prefixWidths,
308
+ visualLines: [
309
+ {
310
+ logicalRow: row,
311
+ startCol: 0,
312
+ endCol: graphemeList.length,
313
+ text: line,
314
+ },
315
+ ],
316
+ }
317
+ })
318
+ return {
319
+ lines,
320
+ allVisualLines: lines.flatMap((l) => l.visualLines),
321
+ }
322
+ }
323
+
324
+ const effectiveWidth = Math.max(10, contentWidth - lineNumGutterWidth)
325
+ return buildLayout(value, effectiveWidth)
326
+ }, [value, contentWidth, wordWrap, lineNumGutterWidth, logicalLines])
327
+
328
+ // Keep cursor in bounds when value changes externally
329
+ useEffect(() => {
330
+ const maxRow = Math.max(0, logicalLines.length - 1)
331
+ const newRow = Math.min(cursor.row, maxRow)
332
+ const lineGraphemes = layout.lines[newRow]?.graphemeList ?? []
333
+ const maxCol = lineGraphemes.length
334
+ const newCol = Math.min(cursor.col, maxCol)
335
+ if (newRow !== cursor.row || newCol !== cursor.col) {
336
+ setCursor({ row: newRow, col: newCol })
337
+ }
338
+ }, [value, logicalLines, layout.lines, cursor.row, cursor.col])
339
+
340
+ // Keep scroll in bounds (visual lines)
341
+ useEffect(() => {
342
+ const { visualRow } = logicalToVisual(layout, cursor)
343
+ if (visualRow < scrollY) {
344
+ setScrollY(visualRow)
345
+ } else if (visualRow >= scrollY + height) {
346
+ setScrollY(visualRow - height + 1)
347
+ }
348
+ }, [cursor, layout, scrollY, height])
349
+
350
+ // Update preferredVisualX after cursor/layout changes (except during vertical movement)
351
+ useEffect(() => {
352
+ if (!shouldUpdatePreferredXRef.current) {
353
+ // Reset ref for next action
354
+ shouldUpdatePreferredXRef.current = true
355
+ return
356
+ }
357
+ const { visualCol } = logicalToVisual(layout, cursor)
358
+ setPreferredVisualX(visualCol)
359
+ }, [cursor, layout])
360
+
361
+ const currentLine = logicalLines[cursor.row] ?? ""
362
+ const currentLineLayout = layout.lines[cursor.row]
363
+
364
+ const handleKey = useCallback(
365
+ (key: { name: string; text?: string; ctrl?: boolean; meta?: boolean; shift?: boolean }) => {
366
+ if (!focused) return
367
+
368
+ const moveCursor = (row: number, col: number) => {
369
+ const newRow = Math.max(0, Math.min(logicalLines.length - 1, row))
370
+ const lineLayout = layout.lines[newRow]
371
+ const lineLen = lineLayout?.graphemeList.length ?? 0
372
+ const newCol = Math.max(0, Math.min(lineLen, col))
373
+ setCursor({ row: newRow, col: newCol })
374
+ // preferredVisualX will be updated by the effect
375
+ }
376
+
377
+ const moveVisual = (deltaRows: number) => {
378
+ // Mark that we shouldn't update preferredVisualX after this move
379
+ shouldUpdatePreferredXRef.current = false
380
+ const { visualRow } = logicalToVisual(layout, cursor)
381
+ const newVisualRow = visualRow + deltaRows
382
+ const newCursor = visualToLogical(layout, newVisualRow, preferredVisualX)
383
+ setCursor(newCursor)
384
+ }
385
+
386
+ switch (key.name) {
387
+ case "up":
388
+ if (key.meta) {
389
+ // Option+Up: Move to start of document
390
+ moveCursor(0, 0)
391
+ } else {
392
+ moveVisual(-1)
393
+ }
394
+ break
395
+
396
+ case "down":
397
+ if (key.meta) {
398
+ // Option+Down: Move to end of document
399
+ const lastRow = logicalLines.length - 1
400
+ const lastLineLen = layout.lines[lastRow]?.graphemeList.length ?? 0
401
+ moveCursor(lastRow, lastLineLen)
402
+ } else {
403
+ moveVisual(1)
404
+ }
405
+ break
406
+
407
+ case "left":
408
+ if (key.meta) {
409
+ // Option+Left: Move to previous word boundary
410
+ const lineLayout = layout.lines[cursor.row]
411
+ if (lineLayout) {
412
+ const beforeCursor = lineLayout.graphemeList.slice(0, cursor.col).join("")
413
+ const match = matchPrevWord(beforeCursor)
414
+ if (match) {
415
+ // Count graphemes in match
416
+ const matchGraphemes = graphemes(match).length
417
+ moveCursor(cursor.row, cursor.col - matchGraphemes)
418
+ } else if (cursor.col > 0) {
419
+ moveCursor(cursor.row, 0)
420
+ } else if (cursor.row > 0) {
421
+ const prevLineLen = layout.lines[cursor.row - 1]?.graphemeList.length ?? 0
422
+ moveCursor(cursor.row - 1, prevLineLen)
423
+ }
424
+ }
425
+ } else {
426
+ if (cursor.col > 0) {
427
+ moveCursor(cursor.row, cursor.col - 1)
428
+ } else if (cursor.row > 0) {
429
+ const prevLineLen = layout.lines[cursor.row - 1]?.graphemeList.length ?? 0
430
+ moveCursor(cursor.row - 1, prevLineLen)
431
+ }
432
+ }
433
+ break
434
+
435
+ case "right":
436
+ if (key.meta) {
437
+ // Option+Right: Move to next word boundary
438
+ const lineLayout = layout.lines[cursor.row]
439
+ if (lineLayout) {
440
+ const afterCursor = lineLayout.graphemeList.slice(cursor.col).join("")
441
+ const match = matchNextWord(afterCursor)
442
+ if (match) {
443
+ const matchGraphemes = graphemes(match).length
444
+ moveCursor(cursor.row, cursor.col + matchGraphemes)
445
+ } else if (cursor.col < lineLayout.graphemeList.length) {
446
+ moveCursor(cursor.row, lineLayout.graphemeList.length)
447
+ } else if (cursor.row < logicalLines.length - 1) {
448
+ moveCursor(cursor.row + 1, 0)
449
+ }
450
+ }
451
+ } else {
452
+ const lineLen = currentLineLayout?.graphemeList.length ?? 0
453
+ if (cursor.col < lineLen) {
454
+ moveCursor(cursor.row, cursor.col + 1)
455
+ } else if (cursor.row < logicalLines.length - 1) {
456
+ moveCursor(cursor.row + 1, 0)
457
+ }
458
+ }
459
+ break
460
+
461
+ case "home":
462
+ moveCursor(cursor.row, 0)
463
+ break
464
+
465
+ case "end":
466
+ moveCursor(cursor.row, currentLineLayout?.graphemeList.length ?? 0)
467
+ break
468
+
469
+ case "backspace":
470
+ if (key.meta && cursor.col > 0) {
471
+ // Option+Backspace: Delete previous word
472
+ const charIdx = graphemeColToCharIdx(currentLine, cursor.col)
473
+ const beforeCursor = currentLine.slice(0, charIdx)
474
+ const match = matchPrevWord(beforeCursor)
475
+ if (match) {
476
+ const newCharIdx = beforeCursor.length - match.length
477
+ const newCol = graphemes(currentLine.slice(0, newCharIdx)).length
478
+ const newLine = currentLine.slice(0, newCharIdx) + currentLine.slice(charIdx)
479
+ const newLines = [...logicalLines]
480
+ newLines[cursor.row] = newLine
481
+ onChange(newLines.join("\n"))
482
+ setCursor({ row: cursor.row, col: newCol })
483
+ }
484
+ } else {
485
+ // Delete character before cursor (or join with previous line)
486
+ const state: MultilineState = { lines: logicalLines, cursor, killRing }
487
+ const result = deleteCharBackwardMultiline(state)
488
+ if (result.changed) {
489
+ onChange(result.state.lines.join("\n"))
490
+ setCursor(result.state.cursor)
491
+ }
492
+ }
493
+ break
494
+
495
+ case "delete": {
496
+ const state: MultilineState = { lines: logicalLines, cursor, killRing }
497
+ const result = deleteCharForwardMultiline(state)
498
+ if (result.changed) {
499
+ onChange(result.state.lines.join("\n"))
500
+ }
501
+ break
502
+ }
503
+
504
+ case "enter":
505
+ if (key.ctrl || key.meta) {
506
+ onSubmit?.(value)
507
+ } else {
508
+ // Insert newline
509
+ const state: MultilineState = { lines: logicalLines, cursor, killRing }
510
+ const result = insertTextMultiline(state, "\n")
511
+ if (result.changed) {
512
+ onChange(result.state.lines.join("\n"))
513
+ setCursor(result.state.cursor)
514
+ }
515
+ }
516
+ break
517
+
518
+ case "escape":
519
+ onCancel?.()
520
+ break
521
+
522
+ case "char":
523
+ case "space":
524
+ if (key.ctrl && key.text) {
525
+ // Emacs-style keybindings
526
+ switch (key.text) {
527
+ case "a":
528
+ moveCursor(cursor.row, 0)
529
+ break
530
+ case "e":
531
+ moveCursor(cursor.row, currentLineLayout?.graphemeList.length ?? 0)
532
+ break
533
+ case "b":
534
+ if (cursor.col > 0) {
535
+ moveCursor(cursor.row, cursor.col - 1)
536
+ } else if (cursor.row > 0) {
537
+ const prevLen = layout.lines[cursor.row - 1]?.graphemeList.length ?? 0
538
+ moveCursor(cursor.row - 1, prevLen)
539
+ }
540
+ break
541
+ case "f":
542
+ if (cursor.col < (currentLineLayout?.graphemeList.length ?? 0)) {
543
+ moveCursor(cursor.row, cursor.col + 1)
544
+ } else if (cursor.row < logicalLines.length - 1) {
545
+ moveCursor(cursor.row + 1, 0)
546
+ }
547
+ break
548
+ case "n":
549
+ moveVisual(1)
550
+ break
551
+ case "p":
552
+ moveVisual(-1)
553
+ break
554
+ case "d": {
555
+ // Delete character at cursor
556
+ const state: MultilineState = { lines: logicalLines, cursor, killRing }
557
+ const result = deleteCharForwardMultiline(state)
558
+ if (result.changed) {
559
+ onChange(result.state.lines.join("\n"))
560
+ }
561
+ break
562
+ }
563
+ case "h": {
564
+ // Backspace
565
+ const state: MultilineState = { lines: logicalLines, cursor, killRing }
566
+ const result = deleteCharBackwardMultiline(state)
567
+ if (result.changed) {
568
+ onChange(result.state.lines.join("\n"))
569
+ setCursor(result.state.cursor)
570
+ }
571
+ break
572
+ }
573
+ case "k": {
574
+ // Kill to end of line
575
+ const state: MultilineState = { lines: logicalLines, cursor, killRing }
576
+ const result = killToEndMultiline(state)
577
+ if (result.changed) {
578
+ onChange(result.state.lines.join("\n"))
579
+ setCursor(result.state.cursor)
580
+ setKillRing(result.state.killRing)
581
+ }
582
+ break
583
+ }
584
+ case "u": {
585
+ // Kill to start of line
586
+ const state: MultilineState = { lines: logicalLines, cursor, killRing }
587
+ const result = killToStartMultiline(state)
588
+ if (result.changed) {
589
+ onChange(result.state.lines.join("\n"))
590
+ setCursor(result.state.cursor)
591
+ setKillRing(result.state.killRing)
592
+ }
593
+ break
594
+ }
595
+ case "w": {
596
+ // Delete word backward (crosses line boundaries)
597
+ const state: MultilineState = { lines: logicalLines, cursor, killRing }
598
+ const result = deleteWordBackwardMultiline(state)
599
+ if (result.changed) {
600
+ onChange(result.state.lines.join("\n"))
601
+ setCursor(result.state.cursor)
602
+ setKillRing(result.state.killRing)
603
+ }
604
+ break
605
+ }
606
+ case "t": {
607
+ // Transpose characters
608
+ const state: MultilineState = { lines: logicalLines, cursor, killRing }
609
+ const result = transposeCharsMultiline(state)
610
+ if (result.changed) {
611
+ onChange(result.state.lines.join("\n"))
612
+ setCursor(result.state.cursor)
613
+ }
614
+ break
615
+ }
616
+ case "y": {
617
+ // Yank from kill ring
618
+ const state: MultilineState = { lines: logicalLines, cursor, killRing }
619
+ const result = insertTextMultiline(state, killRing)
620
+ if (result.changed) {
621
+ onChange(result.state.lines.join("\n"))
622
+ setCursor(result.state.cursor)
623
+ }
624
+ break
625
+ }
626
+ }
627
+ } else if (key.text && !key.meta) {
628
+ // Insert character at cursor
629
+ const state: MultilineState = { lines: logicalLines, cursor, killRing }
630
+ const result = insertTextMultiline(state, key.text)
631
+ if (result.changed) {
632
+ onChange(result.state.lines.join("\n"))
633
+ setCursor(result.state.cursor)
634
+ }
635
+ }
636
+ break
637
+ }
638
+ },
639
+ [
640
+ value,
641
+ logicalLines,
642
+ layout,
643
+ cursor,
644
+ currentLine,
645
+ currentLineLayout,
646
+ focused,
647
+ onChange,
648
+ onSubmit,
649
+ onCancel,
650
+ killRing,
651
+ preferredVisualX,
652
+ ],
653
+ )
654
+
655
+ useKeyboard(handleKey, { phase: "any" })
656
+
657
+ const draw = useCallback(
658
+ (ctx: DrawContext) => {
659
+ // Update content width for layout calculations
660
+ const newContentWidth = width ?? ctx.width
661
+ if (newContentWidth !== contentWidth) {
662
+ setContentWidth(newContentWidth)
663
+ }
664
+
665
+ const lineNumWidth = showLineNumbers ? String(logicalLines.length).length + 1 : 0
666
+
667
+ const isPlaceholder = !value
668
+ const textColor = isPlaceholder ? placeholderFg : fg
669
+
670
+ // Clear with background
671
+ if (bg !== undefined) {
672
+ ctx.fill(0, 0, ctx.width, ctx.height, " ", { bg })
673
+ }
674
+
675
+ // Use placeholder layout if empty
676
+ const displayLayout = isPlaceholder ? buildLayout(placeholder, Math.max(10, ctx.width - lineNumWidth)) : layout
677
+
678
+ const { visualRow: cursorVisualRow } = isPlaceholder ? { visualRow: 0 } : logicalToVisual(layout, cursor)
679
+
680
+ // Draw visible visual lines
681
+ for (let y = 0; y < height && y + scrollY < displayLayout.allVisualLines.length; y++) {
682
+ const visualIdx = y + scrollY
683
+ const vl = displayLayout.allVisualLines[visualIdx]
684
+ const lineLayout = displayLayout.lines[vl.logicalRow]
685
+
686
+ let x = 0
687
+
688
+ // Draw line number (only on first visual line of logical line)
689
+ if (showLineNumbers) {
690
+ if (vl.startCol === 0) {
691
+ const lineNum = String(vl.logicalRow + 1).padStart(lineNumWidth - 1, " ")
692
+ ctx.text(0, y, lineNum, { fg: lineNumberFg, bg })
693
+ }
694
+ x = lineNumWidth
695
+ }
696
+
697
+ // Draw line content
698
+ for (let g = vl.startCol; g < vl.endCol; g++) {
699
+ const grapheme = lineLayout.graphemeList[g]
700
+ const charWidth = lineLayout.widths[g]
701
+
702
+ // Draw cursor if at this position
703
+ const isCursor = !isPlaceholder && vl.logicalRow === cursor.row && g === cursor.col && focused && showCursor
704
+
705
+ if (isCursor) {
706
+ ctx.text(x, y, grapheme, { fg: cursorFg, bg: cursorBg })
707
+ } else {
708
+ ctx.text(x, y, grapheme, { fg: textColor, bg })
709
+ }
710
+
711
+ x += charWidth
712
+ }
713
+
714
+ // Draw cursor at end of visual line if cursor is there
715
+ if (!isPlaceholder && visualIdx === cursorVisualRow && cursor.col >= vl.endCol && focused && showCursor) {
716
+ ctx.text(x, y, " ", { fg: cursorFg, bg: cursorBg })
717
+ }
718
+ }
719
+
720
+ // If placeholder and focused, show cursor at start
721
+ if (isPlaceholder && focused && showCursor) {
722
+ const firstChar = placeholder[0] || " "
723
+ ctx.text(lineNumWidth, 0, firstChar, { fg: cursorFg, bg: cursorBg })
724
+ }
725
+ },
726
+ [
727
+ value,
728
+ placeholder,
729
+ layout,
730
+ logicalLines,
731
+ cursor,
732
+ scrollY,
733
+ height,
734
+ focused,
735
+ showCursor,
736
+ showLineNumbers,
737
+ fg,
738
+ bg,
739
+ cursorFg,
740
+ cursorBg,
741
+ placeholderFg,
742
+ lineNumberFg,
743
+ width,
744
+ contentWidth,
745
+ ],
746
+ )
747
+
748
+ return <canvas draw={draw} width={width} height={height} />
749
+ }