@xincli/opentui-core 0.4.6

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 (476) hide show
  1. package/README.md +63 -0
  2. package/dev/keypress-debug-renderer.ts +148 -0
  3. package/dev/keypress-debug.ts +43 -0
  4. package/dev/print-env-vars.ts +32 -0
  5. package/dev/test-tmux-graphics-334.sh +68 -0
  6. package/dev/test-tmux-theme-queries.sh +151 -0
  7. package/dev/thai-debug-test.ts +68 -0
  8. package/dev/theme-mode.ts +290 -0
  9. package/docs/development.md +144 -0
  10. package/package.json +102 -0
  11. package/prebuilt/aarch64-android/libopentui.so +0 -0
  12. package/scripts/build-android.ts +226 -0
  13. package/scripts/build-native-termux.sh +525 -0
  14. package/scripts/build.ts +530 -0
  15. package/scripts/dist-test.ts +312 -0
  16. package/scripts/package-prebuilt.ts +110 -0
  17. package/scripts/publish.ts +60 -0
  18. package/scripts/test-node-hook.mjs +16 -0
  19. package/scripts/test-node.ts +241 -0
  20. package/scripts/vendor-deps.sh +73 -0
  21. package/src/NativeSpanFeed.ts +375 -0
  22. package/src/Renderable.ts +1878 -0
  23. package/src/__snapshots__/buffer.test.ts.snap +28 -0
  24. package/src/animation/Timeline.test.ts +2709 -0
  25. package/src/animation/Timeline.ts +598 -0
  26. package/src/ansi.ts +22 -0
  27. package/src/audio.ts +479 -0
  28. package/src/benchmark/attenuation-benchmark.ts +81 -0
  29. package/src/benchmark/audio-playback-benchmark.ts +344 -0
  30. package/src/benchmark/box-draw-benchmark.ts +1042 -0
  31. package/src/benchmark/colormatrix-benchmark.ts +123 -0
  32. package/src/benchmark/gain-benchmark.ts +80 -0
  33. package/src/benchmark/latest-all-bench-run.json +707 -0
  34. package/src/benchmark/latest-async-bench-run.json +336 -0
  35. package/src/benchmark/latest-default-bench-run.json +657 -0
  36. package/src/benchmark/latest-large-bench-run.json +707 -0
  37. package/src/benchmark/latest-quick-bench-run.json +207 -0
  38. package/src/benchmark/layout-benchmark.ts +2547 -0
  39. package/src/benchmark/markdown-benchmark.ts +1796 -0
  40. package/src/benchmark/native-span-feed-async-benchmark.ts +355 -0
  41. package/src/benchmark/native-span-feed-benchmark.md +56 -0
  42. package/src/benchmark/native-span-feed-benchmark.ts +596 -0
  43. package/src/benchmark/native-span-feed-compare.ts +280 -0
  44. package/src/benchmark/render-traversal-benchmark.ts +928 -0
  45. package/src/benchmark/text-buffer-render-benchmark.ts +874 -0
  46. package/src/benchmark/text-table-benchmark.ts +948 -0
  47. package/src/buffer.test.ts +292 -0
  48. package/src/buffer.ts +564 -0
  49. package/src/console.test.ts +629 -0
  50. package/src/console.ts +1253 -0
  51. package/src/edit-buffer.test.ts +1852 -0
  52. package/src/edit-buffer.ts +417 -0
  53. package/src/editor-view.test.ts +1056 -0
  54. package/src/editor-view.ts +290 -0
  55. package/src/index.ts +25 -0
  56. package/src/lib/KeyHandler.integration.test.ts +292 -0
  57. package/src/lib/KeyHandler.stopPropagation.test.ts +289 -0
  58. package/src/lib/KeyHandler.test.ts +662 -0
  59. package/src/lib/KeyHandler.ts +222 -0
  60. package/src/lib/RGBA.test.ts +60 -0
  61. package/src/lib/RGBA.ts +343 -0
  62. package/src/lib/ascii.font.ts +330 -0
  63. package/src/lib/border.test.ts +83 -0
  64. package/src/lib/border.ts +170 -0
  65. package/src/lib/bunfs.test.ts +27 -0
  66. package/src/lib/bunfs.ts +18 -0
  67. package/src/lib/clipboard.test.ts +63 -0
  68. package/src/lib/clipboard.ts +41 -0
  69. package/src/lib/clock.ts +35 -0
  70. package/src/lib/data-paths.test.ts +133 -0
  71. package/src/lib/data-paths.ts +109 -0
  72. package/src/lib/debounce.ts +106 -0
  73. package/src/lib/detect-links.test.ts +98 -0
  74. package/src/lib/detect-links.ts +56 -0
  75. package/src/lib/env.test.ts +228 -0
  76. package/src/lib/env.ts +209 -0
  77. package/src/lib/extmarks-history.ts +51 -0
  78. package/src/lib/extmarks-multiwidth.test.ts +324 -0
  79. package/src/lib/extmarks.test.ts +3547 -0
  80. package/src/lib/extmarks.ts +861 -0
  81. package/src/lib/fonts/block.json +405 -0
  82. package/src/lib/fonts/grid.json +265 -0
  83. package/src/lib/fonts/huge.json +741 -0
  84. package/src/lib/fonts/pallet.json +314 -0
  85. package/src/lib/fonts/shade.json +591 -0
  86. package/src/lib/fonts/slick.json +321 -0
  87. package/src/lib/fonts/tiny.json +69 -0
  88. package/src/lib/hast-styled-text.ts +59 -0
  89. package/src/lib/index.ts +22 -0
  90. package/src/lib/keybinding.internal.test.ts +342 -0
  91. package/src/lib/keybinding.internal.ts +182 -0
  92. package/src/lib/objects-in-viewport.test.ts +789 -0
  93. package/src/lib/objects-in-viewport.ts +153 -0
  94. package/src/lib/output.capture.ts +58 -0
  95. package/src/lib/parse.keypress-kitty.protocol.test.ts +347 -0
  96. package/src/lib/parse.keypress-kitty.test.ts +712 -0
  97. package/src/lib/parse.keypress-kitty.ts +470 -0
  98. package/src/lib/parse.keypress.test.ts +2020 -0
  99. package/src/lib/parse.keypress.ts +480 -0
  100. package/src/lib/parse.mouse.test.ts +552 -0
  101. package/src/lib/parse.mouse.ts +232 -0
  102. package/src/lib/paste.ts +18 -0
  103. package/src/lib/queue.ts +65 -0
  104. package/src/lib/render-geometry.ts +36 -0
  105. package/src/lib/renderable.validations.test.ts +87 -0
  106. package/src/lib/renderable.validations.ts +83 -0
  107. package/src/lib/scroll-acceleration.ts +98 -0
  108. package/src/lib/selection.ts +261 -0
  109. package/src/lib/singleton.ts +34 -0
  110. package/src/lib/stdin-parser.test.ts +2439 -0
  111. package/src/lib/stdin-parser.ts +2007 -0
  112. package/src/lib/styled-text.ts +178 -0
  113. package/src/lib/terminal-capability-detection.test.ts +226 -0
  114. package/src/lib/terminal-capability-detection.ts +98 -0
  115. package/src/lib/terminal-palette.test.ts +954 -0
  116. package/src/lib/terminal-palette.ts +441 -0
  117. package/src/lib/tree-sitter/assets/README.md +119 -0
  118. package/src/lib/tree-sitter/assets/javascript/highlights.scm +205 -0
  119. package/src/lib/tree-sitter/assets/javascript/tree-sitter-javascript.wasm +0 -0
  120. package/src/lib/tree-sitter/assets/markdown/highlights.scm +150 -0
  121. package/src/lib/tree-sitter/assets/markdown/injections.scm +27 -0
  122. package/src/lib/tree-sitter/assets/markdown/tree-sitter-markdown.wasm +0 -0
  123. package/src/lib/tree-sitter/assets/markdown_inline/highlights.scm +115 -0
  124. package/src/lib/tree-sitter/assets/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  125. package/src/lib/tree-sitter/assets/typescript/highlights.scm +604 -0
  126. package/src/lib/tree-sitter/assets/typescript/tree-sitter-typescript.wasm +0 -0
  127. package/src/lib/tree-sitter/assets/update.ts +360 -0
  128. package/src/lib/tree-sitter/assets/zig/highlights.scm +284 -0
  129. package/src/lib/tree-sitter/assets/zig/tree-sitter-zig.wasm +0 -0
  130. package/src/lib/tree-sitter/assets.d.ts +9 -0
  131. package/src/lib/tree-sitter/cache.test.ts +291 -0
  132. package/src/lib/tree-sitter/client.test.ts +1434 -0
  133. package/src/lib/tree-sitter/client.ts +847 -0
  134. package/src/lib/tree-sitter/default-parsers.ts +133 -0
  135. package/src/lib/tree-sitter/download-utils.ts +148 -0
  136. package/src/lib/tree-sitter/index.ts +75 -0
  137. package/src/lib/tree-sitter/parser.worker.ts +1098 -0
  138. package/src/lib/tree-sitter/parsers-config.ts +81 -0
  139. package/src/lib/tree-sitter/resolve-ft.test.ts +55 -0
  140. package/src/lib/tree-sitter/resolve-ft.ts +189 -0
  141. package/src/lib/tree-sitter/types.ts +132 -0
  142. package/src/lib/tree-sitter/update-assets.ts +10 -0
  143. package/src/lib/tree-sitter-styled-text.test.ts +1253 -0
  144. package/src/lib/tree-sitter-styled-text.ts +314 -0
  145. package/src/lib/validate-dir-name.ts +55 -0
  146. package/src/lib/yoga.options.test.ts +628 -0
  147. package/src/lib/yoga.options.ts +346 -0
  148. package/src/native-event-worker-repro.test.ts +48 -0
  149. package/src/native-event-worker-repro.worker.ts +9 -0
  150. package/src/native-handle.test.ts +221 -0
  151. package/src/platform/ffi.test.ts +516 -0
  152. package/src/platform/ffi.ts +653 -0
  153. package/src/platform/runtime.test.ts +52 -0
  154. package/src/platform/runtime.ts +121 -0
  155. package/src/platform/test.ts +45 -0
  156. package/src/platform/worker-handler-cleanup.fixture.ts +12 -0
  157. package/src/platform/worker-onmessage-startup.fixture.ts +10 -0
  158. package/src/platform/worker-startup.fixture.ts +9 -0
  159. package/src/platform/worker.node-test.ts +153 -0
  160. package/src/platform/worker.ts +425 -0
  161. package/src/plugins/core-slot.ts +579 -0
  162. package/src/plugins/registry.ts +402 -0
  163. package/src/plugins/types.ts +46 -0
  164. package/src/post/effects.ts +948 -0
  165. package/src/post/filters.ts +512 -0
  166. package/src/post/matrices.ts +288 -0
  167. package/src/renderables/ASCIIFont.ts +219 -0
  168. package/src/renderables/Box.test.ts +474 -0
  169. package/src/renderables/Box.ts +353 -0
  170. package/src/renderables/Code.test.ts +2388 -0
  171. package/src/renderables/Code.ts +555 -0
  172. package/src/renderables/Diff.regression.test.ts +381 -0
  173. package/src/renderables/Diff.test.ts +3212 -0
  174. package/src/renderables/Diff.ts +1278 -0
  175. package/src/renderables/EditBufferRenderable.test.ts +288 -0
  176. package/src/renderables/EditBufferRenderable.ts +1152 -0
  177. package/src/renderables/FrameBuffer.ts +47 -0
  178. package/src/renderables/Input.test.ts +1310 -0
  179. package/src/renderables/Input.ts +273 -0
  180. package/src/renderables/LineNumberRenderable.ts +725 -0
  181. package/src/renderables/Markdown.ts +2144 -0
  182. package/src/renderables/ScrollBar.ts +423 -0
  183. package/src/renderables/ScrollBox.ts +903 -0
  184. package/src/renderables/Select.test.ts +1134 -0
  185. package/src/renderables/Select.ts +535 -0
  186. package/src/renderables/Slider.test.ts +456 -0
  187. package/src/renderables/Slider.ts +342 -0
  188. package/src/renderables/TabSelect.test.ts +238 -0
  189. package/src/renderables/TabSelect.ts +447 -0
  190. package/src/renderables/Text.selection-buffer.test.ts +118 -0
  191. package/src/renderables/Text.test.ts +2662 -0
  192. package/src/renderables/Text.ts +147 -0
  193. package/src/renderables/TextBufferRenderable.ts +504 -0
  194. package/src/renderables/TextNode.test.ts +1137 -0
  195. package/src/renderables/TextNode.ts +379 -0
  196. package/src/renderables/TextTable.test.ts +1411 -0
  197. package/src/renderables/TextTable.ts +1434 -0
  198. package/src/renderables/Textarea.ts +425 -0
  199. package/src/renderables/TimeToFirstDraw.ts +89 -0
  200. package/src/renderables/__snapshots__/Code.test.ts.snap +13 -0
  201. package/src/renderables/__snapshots__/Diff.test.ts.snap +785 -0
  202. package/src/renderables/__snapshots__/Text.test.ts.snap +421 -0
  203. package/src/renderables/__snapshots__/TextTable.test.ts.snap +215 -0
  204. package/src/renderables/__tests__/Code.test.ts +55 -0
  205. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +144 -0
  206. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +827 -0
  207. package/src/renderables/__tests__/LineNumberRenderable.test.ts +1906 -0
  208. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +85 -0
  209. package/src/renderables/__tests__/Markdown.code-colors.test.ts +288 -0
  210. package/src/renderables/__tests__/Markdown.test.ts +4175 -0
  211. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +87 -0
  212. package/src/renderables/__tests__/Textarea.buffer.test.ts +683 -0
  213. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +675 -0
  214. package/src/renderables/__tests__/Textarea.editing.test.ts +2120 -0
  215. package/src/renderables/__tests__/Textarea.error-handling.test.ts +35 -0
  216. package/src/renderables/__tests__/Textarea.events.test.ts +738 -0
  217. package/src/renderables/__tests__/Textarea.highlights.test.ts +590 -0
  218. package/src/renderables/__tests__/Textarea.keybinding.test.ts +3275 -0
  219. package/src/renderables/__tests__/Textarea.paste.test.ts +357 -0
  220. package/src/renderables/__tests__/Textarea.rendering.test.ts +1927 -0
  221. package/src/renderables/__tests__/Textarea.scroll.test.ts +733 -0
  222. package/src/renderables/__tests__/Textarea.selection.test.ts +1589 -0
  223. package/src/renderables/__tests__/Textarea.stress.test.ts +670 -0
  224. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +383 -0
  225. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +310 -0
  226. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +221 -0
  227. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +89 -0
  228. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +457 -0
  229. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +158 -0
  230. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +387 -0
  231. package/src/renderables/__tests__/markdown-parser.test.ts +241 -0
  232. package/src/renderables/__tests__/renderable-test-utils.ts +60 -0
  233. package/src/renderables/composition/README.md +8 -0
  234. package/src/renderables/composition/VRenderable.ts +32 -0
  235. package/src/renderables/composition/constructs.ts +127 -0
  236. package/src/renderables/composition/vnode.ts +277 -0
  237. package/src/renderables/index.ts +23 -0
  238. package/src/renderables/markdown-parser.ts +79 -0
  239. package/src/renderer-theme-mode.ts +199 -0
  240. package/src/renderer.ts +4975 -0
  241. package/src/runtime-plugin-support-configure.ts +63 -0
  242. package/src/runtime-plugin-support.ts +13 -0
  243. package/src/runtime-plugin.ts +626 -0
  244. package/src/specs/terminal-startup.md +64 -0
  245. package/src/syntax-style.test.ts +841 -0
  246. package/src/syntax-style.ts +274 -0
  247. package/src/testing/README.md +219 -0
  248. package/src/testing/bun-test-node.ts +880 -0
  249. package/src/testing/capture-spans.test.ts +216 -0
  250. package/src/testing/integration.test.ts +276 -0
  251. package/src/testing/manual-clock.ts +117 -0
  252. package/src/testing/mock-keys.test.ts +1378 -0
  253. package/src/testing/mock-keys.ts +457 -0
  254. package/src/testing/mock-mouse.test.ts +218 -0
  255. package/src/testing/mock-mouse.ts +247 -0
  256. package/src/testing/mock-tree-sitter-client.ts +81 -0
  257. package/src/testing/spy.ts +13 -0
  258. package/src/testing/terminal-capabilities.ts +47 -0
  259. package/src/testing/test-recorder.test.ts +410 -0
  260. package/src/testing/test-recorder.ts +133 -0
  261. package/src/testing/test-renderer.ts +375 -0
  262. package/src/testing/test-renderer.wait.test.ts +278 -0
  263. package/src/testing/test-streams.ts +31 -0
  264. package/src/testing.ts +9 -0
  265. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +481 -0
  266. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +19 -0
  267. package/src/tests/__snapshots__/scrollbox.test.ts.snap +29 -0
  268. package/src/tests/absolute-positioning.snapshot.test.ts +638 -0
  269. package/src/tests/allocator-stats.test.ts +53 -0
  270. package/src/tests/audio.test.ts +303 -0
  271. package/src/tests/destroy-during-render.test.ts +200 -0
  272. package/src/tests/destroy-on-exit.fixture.ts +36 -0
  273. package/src/tests/destroy-on-exit.test.ts +62 -0
  274. package/src/tests/ffi-borrowed-pointer-callsites.test.ts +153 -0
  275. package/src/tests/hover-cursor.test.ts +98 -0
  276. package/src/tests/native-backed-measurement-lifecycle.test.ts +133 -0
  277. package/src/tests/native-backed-measurement-parity.test.ts +322 -0
  278. package/src/tests/native-span-feed-async.test.ts +173 -0
  279. package/src/tests/native-span-feed-close.test.ts +120 -0
  280. package/src/tests/native-span-feed-coverage.test.ts +227 -0
  281. package/src/tests/native-span-feed-edge-cases.test.ts +384 -0
  282. package/src/tests/native-span-feed-use-after-free.test.ts +45 -0
  283. package/src/tests/opacity.test.ts +123 -0
  284. package/src/tests/renderable.snapshot.test.ts +524 -0
  285. package/src/tests/renderable.test.ts +1851 -0
  286. package/src/tests/renderer.clock.test.ts +270 -0
  287. package/src/tests/renderer.console-startup.test.ts +2151 -0
  288. package/src/tests/renderer.control.test.ts +458 -0
  289. package/src/tests/renderer.core-slot-binding.test.ts +962 -0
  290. package/src/tests/renderer.cursor.test.ts +26 -0
  291. package/src/tests/renderer.custom-stdout.test.ts +1112 -0
  292. package/src/tests/renderer.destroy-during-render.test.ts +147 -0
  293. package/src/tests/renderer.focus-restore.test.ts +257 -0
  294. package/src/tests/renderer.focus.test.ts +329 -0
  295. package/src/tests/renderer.idle.test.ts +219 -0
  296. package/src/tests/renderer.input.test.ts +2615 -0
  297. package/src/tests/renderer.kitty-flags.test.ts +195 -0
  298. package/src/tests/renderer.lifecycle.test.ts +34 -0
  299. package/src/tests/renderer.mouse.test.ts +1315 -0
  300. package/src/tests/renderer.notifications.test.ts +27 -0
  301. package/src/tests/renderer.palette.test.ts +1199 -0
  302. package/src/tests/renderer.remote.test.ts +247 -0
  303. package/src/tests/renderer.render-stats.test.ts +146 -0
  304. package/src/tests/renderer.scrollback-surface.test.ts +474 -0
  305. package/src/tests/renderer.selection.test.ts +138 -0
  306. package/src/tests/renderer.slot-registry.test.ts +684 -0
  307. package/src/tests/renderer.tracker.test.ts +202 -0
  308. package/src/tests/renderer.useMouse.test.ts +83 -0
  309. package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +76 -0
  310. package/src/tests/runtime-plugin-node-modules-import-like-string.fixture.ts +48 -0
  311. package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +43 -0
  312. package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +67 -0
  313. package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +72 -0
  314. package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +44 -0
  315. package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +85 -0
  316. package/src/tests/runtime-plugin-path-alias.fixture.ts +43 -0
  317. package/src/tests/runtime-plugin-resolve-roots.fixture.ts +65 -0
  318. package/src/tests/runtime-plugin-support-configure.fixture.ts +31 -0
  319. package/src/tests/runtime-plugin-support-late-addition.fixture.ts +17 -0
  320. package/src/tests/runtime-plugin-support.fixture.ts +11 -0
  321. package/src/tests/runtime-plugin-support.test.ts +54 -0
  322. package/src/tests/runtime-plugin-windows-file-url.fixture.ts +30 -0
  323. package/src/tests/runtime-plugin.fixture.ts +40 -0
  324. package/src/tests/runtime-plugin.test.ts +369 -0
  325. package/src/tests/scrollbox-culling-bug.test.ts +114 -0
  326. package/src/tests/scrollbox-hitgrid-resize.test.ts +136 -0
  327. package/src/tests/scrollbox-hitgrid.test.ts +909 -0
  328. package/src/tests/scrollbox.test.ts +1895 -0
  329. package/src/tests/wrap-resize-perf.test.ts +276 -0
  330. package/src/tests/yoga-api-coverage.test.ts +409 -0
  331. package/src/tests/yoga-callback-stress.test.ts +122 -0
  332. package/src/tests/yoga-ffi.test.ts +117 -0
  333. package/src/tests/yoga-setters.test.ts +921 -0
  334. package/src/tests/yoga-upstream/YGAlignBaselineTest.test.ts +124 -0
  335. package/src/tests/yoga-upstream/YGComputedBorderTest.test.ts +27 -0
  336. package/src/tests/yoga-upstream/YGComputedMarginTest.test.ts +27 -0
  337. package/src/tests/yoga-upstream/YGComputedPaddingTest.test.ts +27 -0
  338. package/src/tests/yoga-upstream/YGDirtiedTest.test.ts +157 -0
  339. package/src/tests/yoga-upstream/YGErrataTest.test.ts +40 -0
  340. package/src/tests/yoga-upstream/YGFlexBasisAuto.test.ts +23 -0
  341. package/src/tests/yoga-upstream/YGHasNewLayout.test.ts +81 -0
  342. package/src/tests/yoga-upstream/YGMeasureCacheTest.test.ts +31 -0
  343. package/src/tests/yoga-upstream/YGMeasureTest.test.ts +70 -0
  344. package/src/tests/yoga-upstream/generated/YGAbsolutePositionTest.test.ts +1864 -0
  345. package/src/tests/yoga-upstream/generated/YGAlignContentTest.test.ts +5829 -0
  346. package/src/tests/yoga-upstream/generated/YGAlignItemsTest.test.ts +2444 -0
  347. package/src/tests/yoga-upstream/generated/YGAlignSelfTest.test.ts +293 -0
  348. package/src/tests/yoga-upstream/generated/YGAndroidNewsFeed.test.ts +315 -0
  349. package/src/tests/yoga-upstream/generated/YGAspectRatioTest.test.ts +231 -0
  350. package/src/tests/yoga-upstream/generated/YGAutoTest.test.ts +347 -0
  351. package/src/tests/yoga-upstream/generated/YGBorderTest.test.ts +256 -0
  352. package/src/tests/yoga-upstream/generated/YGBoxSizingTest.test.ts +2747 -0
  353. package/src/tests/yoga-upstream/generated/YGDimensionTest.test.ts +129 -0
  354. package/src/tests/yoga-upstream/generated/YGDisplayContentsTest.test.ts +109 -0
  355. package/src/tests/yoga-upstream/generated/YGDisplayTest.test.ts +1097 -0
  356. package/src/tests/yoga-upstream/generated/YGFlexDirectionTest.test.ts +4562 -0
  357. package/src/tests/yoga-upstream/generated/YGFlexTest.test.ts +677 -0
  358. package/src/tests/yoga-upstream/generated/YGFlexWrapTest.test.ts +2071 -0
  359. package/src/tests/yoga-upstream/generated/YGGapTest.test.ts +3303 -0
  360. package/src/tests/yoga-upstream/generated/YGIntrinsicSizeTest.test.ts +582 -0
  361. package/src/tests/yoga-upstream/generated/YGJustifyContentTest.test.ts +2150 -0
  362. package/src/tests/yoga-upstream/generated/YGMarginTest.test.ts +1963 -0
  363. package/src/tests/yoga-upstream/generated/YGMinMaxDimensionTest.test.ts +1446 -0
  364. package/src/tests/yoga-upstream/generated/YGPaddingTest.test.ts +354 -0
  365. package/src/tests/yoga-upstream/generated/YGPercentageTest.test.ts +1760 -0
  366. package/src/tests/yoga-upstream/generated/YGRoundingTest.test.ts +1161 -0
  367. package/src/tests/yoga-upstream/generated/YGSizeOverflowTest.test.ts +207 -0
  368. package/src/tests/yoga-upstream/generated/YGStaticPositionTest.test.ts +6338 -0
  369. package/src/tests/yoga-upstream/tools/MeasureCounter.ts +54 -0
  370. package/src/tests/yoga-upstream/tools/utils.ts +81 -0
  371. package/src/text-buffer-view.test.ts +722 -0
  372. package/src/text-buffer-view.ts +193 -0
  373. package/src/text-buffer.test.ts +512 -0
  374. package/src/text-buffer.ts +250 -0
  375. package/src/types.ts +199 -0
  376. package/src/utils.ts +88 -0
  377. package/src/yoga.ts +1146 -0
  378. package/src/zig/ansi.zig +502 -0
  379. package/src/zig/audio.zig +1206 -0
  380. package/src/zig/bench/README.md +50 -0
  381. package/src/zig/bench/buffer-color-blending_bench.zig +350 -0
  382. package/src/zig/bench/buffer-draw-box_bench.zig +786 -0
  383. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +894 -0
  384. package/src/zig/bench/edit-buffer_bench.zig +476 -0
  385. package/src/zig/bench/editor-view_bench.zig +169 -0
  386. package/src/zig/bench/native-span-feed_bench.zig +107 -0
  387. package/src/zig/bench/rope-markers_bench.zig +713 -0
  388. package/src/zig/bench/rope_bench.zig +514 -0
  389. package/src/zig/bench/styled-text_bench.zig +476 -0
  390. package/src/zig/bench/text-buffer-coords_bench.zig +362 -0
  391. package/src/zig/bench/text-buffer-view_bench.zig +459 -0
  392. package/src/zig/bench/text-chunk-graphemes_bench.zig +272 -0
  393. package/src/zig/bench/utf8_bench.zig +799 -0
  394. package/src/zig/bench-utils.zig +431 -0
  395. package/src/zig/bench.zig +223 -0
  396. package/src/zig/buffer-methods.zig +215 -0
  397. package/src/zig/buffer.zig +2515 -0
  398. package/src/zig/build.zig +700 -0
  399. package/src/zig/build.zig.zon +19 -0
  400. package/src/zig/edit-buffer.zig +900 -0
  401. package/src/zig/editor-view.zig +812 -0
  402. package/src/zig/event-bus.zig +28 -0
  403. package/src/zig/event-emitter.zig +65 -0
  404. package/src/zig/file-logger.zig +92 -0
  405. package/src/zig/grapheme.zig +838 -0
  406. package/src/zig/handles.zig +267 -0
  407. package/src/zig/lib.zig +2813 -0
  408. package/src/zig/link.zig +349 -0
  409. package/src/zig/logger.zig +43 -0
  410. package/src/zig/mem-registry.zig +126 -0
  411. package/src/zig/miniaudio_shim.c +21 -0
  412. package/src/zig/native-renderable.zig +112 -0
  413. package/src/zig/native-span-feed-bench-lib.zig +7 -0
  414. package/src/zig/native-span-feed.zig +747 -0
  415. package/src/zig/renderer-output.zig +677 -0
  416. package/src/zig/renderer.zig +2106 -0
  417. package/src/zig/rope.zig +1220 -0
  418. package/src/zig/split-scrollback.zig +88 -0
  419. package/src/zig/syntax-style.zig +174 -0
  420. package/src/zig/terminal.zig +1652 -0
  421. package/src/zig/test.zig +82 -0
  422. package/src/zig/tests/README.md +18 -0
  423. package/src/zig/tests/ansi_test.zig +18 -0
  424. package/src/zig/tests/audio_test.zig +439 -0
  425. package/src/zig/tests/buffer-methods_test.zig +1111 -0
  426. package/src/zig/tests/buffer_test.zig +2926 -0
  427. package/src/zig/tests/edit-buffer-history_test.zig +299 -0
  428. package/src/zig/tests/edit-buffer_test.zig +1688 -0
  429. package/src/zig/tests/editor-view_test.zig +3454 -0
  430. package/src/zig/tests/event-emitter_test.zig +249 -0
  431. package/src/zig/tests/grapheme_test.zig +1329 -0
  432. package/src/zig/tests/handles_test.zig +123 -0
  433. package/src/zig/tests/link_test.zig +220 -0
  434. package/src/zig/tests/mem-registry_test.zig +473 -0
  435. package/src/zig/tests/memory_leak_regression_test.zig +234 -0
  436. package/src/zig/tests/native-renderable_test.zig +16 -0
  437. package/src/zig/tests/native-span-feed_test.zig +1266 -0
  438. package/src/zig/tests/renderer_test.zig +2604 -0
  439. package/src/zig/tests/rope-nested_test.zig +712 -0
  440. package/src/zig/tests/rope_fuzz_test.zig +238 -0
  441. package/src/zig/tests/rope_test.zig +2362 -0
  442. package/src/zig/tests/segment-merge.test.zig +148 -0
  443. package/src/zig/tests/split-scrollback_test.zig +70 -0
  444. package/src/zig/tests/syntax-style_test.zig +558 -0
  445. package/src/zig/tests/terminal_test.zig +1489 -0
  446. package/src/zig/tests/test-renderer.zig +126 -0
  447. package/src/zig/tests/text-buffer-drawing_test.zig +3291 -0
  448. package/src/zig/tests/text-buffer-highlights_test.zig +706 -0
  449. package/src/zig/tests/text-buffer-iterators_test.zig +776 -0
  450. package/src/zig/tests/text-buffer-segment_test.zig +320 -0
  451. package/src/zig/tests/text-buffer-selection_test.zig +1056 -0
  452. package/src/zig/tests/text-buffer-selection_viewport_test.zig +358 -0
  453. package/src/zig/tests/text-buffer-view_test.zig +3672 -0
  454. package/src/zig/tests/text-buffer_test.zig +2191 -0
  455. package/src/zig/tests/unicode-width-map.zon +3909 -0
  456. package/src/zig/tests/utf8_no_zwj_test.zig +260 -0
  457. package/src/zig/tests/utf8_test.zig +4060 -0
  458. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +267 -0
  459. package/src/zig/tests/utf8_wcwidth_test.zig +357 -0
  460. package/src/zig/tests/word-wrap-editing_test.zig +498 -0
  461. package/src/zig/tests/wrap-cache-perf_test.zig +113 -0
  462. package/src/zig/tests/yoga_test.zig +65 -0
  463. package/src/zig/text-buffer-iterators.zig +496 -0
  464. package/src/zig/text-buffer-segment.zig +403 -0
  465. package/src/zig/text-buffer-view.zig +1428 -0
  466. package/src/zig/text-buffer.zig +1272 -0
  467. package/src/zig/utf8.zig +1954 -0
  468. package/src/zig/utils.zig +12 -0
  469. package/src/zig/uucode-stub.zig +76 -0
  470. package/src/zig/vendor/miniaudio/miniaudio.h +93468 -0
  471. package/src/zig/yoga.zig +658 -0
  472. package/src/zig-structs.ts +381 -0
  473. package/src/zig.ts +5260 -0
  474. package/tsconfig.build.json +17 -0
  475. package/tsconfig.json +27 -0
  476. package/tsconfig.node-test.json +138 -0
@@ -0,0 +1,153 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { resolveRenderLib } from "../zig.js"
3
+ import { StyledChunkStruct, CursorStyleOptionsStruct } from "../zig-structs.js"
4
+ import { RGBA } from "../lib/RGBA.js"
5
+ import { toArrayBuffer, type Pointer } from "../platform/ffi.js"
6
+
7
+ // Borrowed-pointer contract for styled text, styled placeholders, and cursor
8
+ // options: packed struct buffers must reach the FFI symbol as object values so
9
+ // the backend can borrow them for the synchronous call. Passing a pre-resolved
10
+ // address instead reintroduces the Node use-after-free from issue #1212.
11
+
12
+ const lib = resolveRenderLib()
13
+ const symbols = (lib as any).opentui.symbols as Record<string, (...args: any[]) => any>
14
+
15
+ function withStubbedSymbol(name: string, fn: (calls: any[][]) => void): void {
16
+ const calls: any[][] = []
17
+ const original = symbols[name]
18
+ symbols[name] = (...args: any[]) => {
19
+ calls.push(args)
20
+ }
21
+ try {
22
+ fn(calls)
23
+ } finally {
24
+ symbols[name] = original
25
+ }
26
+ }
27
+
28
+ async function forceGc(): Promise<void> {
29
+ if (typeof Bun !== "undefined") {
30
+ Bun.gc(true)
31
+ }
32
+ ;(globalThis as any).gc?.()
33
+ await new Promise((resolve) => setTimeout(resolve, 0))
34
+ }
35
+
36
+ function fieldOffset(struct: { layoutByName: Map<string, { offset: number }> }, name: string): number {
37
+ const field = struct.layoutByName.get(name)
38
+ if (!field) {
39
+ throw new Error(`Missing struct field: ${name}`)
40
+ }
41
+ return field.offset
42
+ }
43
+
44
+ function readPackedColor(packed: ArrayBuffer, offset: number): number[] {
45
+ // 64-bit StyledChunk/CursorStyleOptions layout; matches the supported
46
+ // x64/arm64 native targets.
47
+ const address = new DataView(packed).getBigUint64(offset, true)
48
+ expect(address).not.toBe(0n)
49
+ return [...new Uint16Array(toArrayBuffer(address as unknown as Pointer, 0, 8).slice(0))]
50
+ }
51
+
52
+ describe("borrowed pointer call sites", () => {
53
+ test("textBufferSetStyledText passes the packed chunk buffer as an object value", () => {
54
+ withStubbedSymbol("textBufferSetStyledText", (calls) => {
55
+ const chunks = [
56
+ { text: "hello", fg: RGBA.fromValues(1, 0, 0, 1) },
57
+ { text: "world", bg: RGBA.fromValues(0, 0, 1, 1) },
58
+ ]
59
+
60
+ lib.textBufferSetStyledText(0 as any, chunks)
61
+
62
+ expect(calls).toHaveLength(1)
63
+ expect(calls[0]![1]).toBeInstanceOf(ArrayBuffer)
64
+ expect((calls[0]![1] as ArrayBuffer).byteLength).toBe(StyledChunkStruct.size * chunks.length)
65
+ expect(calls[0]![2]).toBe(chunks.length)
66
+ })
67
+ })
68
+
69
+ test("editorViewSetPlaceholderStyledText passes the packed chunk buffer as an object value", () => {
70
+ withStubbedSymbol("editorViewSetPlaceholderStyledText", (calls) => {
71
+ lib.editorViewSetPlaceholderStyledText(0 as any, [{ text: "placeholder", fg: RGBA.fromValues(0, 1, 0, 1) }])
72
+ lib.editorViewSetPlaceholderStyledText(0 as any, [{ text: "" }])
73
+
74
+ expect(calls).toHaveLength(2)
75
+ expect(calls[0]![1]).toBeInstanceOf(ArrayBuffer)
76
+ expect((calls[0]![1] as ArrayBuffer).byteLength).toBe(StyledChunkStruct.size)
77
+ expect(calls[1]![1]).toBeNull()
78
+ expect(calls[1]![2]).toBe(0)
79
+ })
80
+ })
81
+
82
+ test("setCursorStyleOptions passes the packed options buffer as an object value", () => {
83
+ withStubbedSymbol("setCursorStyleOptions", (calls) => {
84
+ lib.setCursorStyleOptions(0 as any, { style: "block", blinking: true, color: RGBA.fromValues(1, 1, 0, 1) })
85
+
86
+ expect(calls).toHaveLength(1)
87
+ expect(calls[0]![1]).toBeInstanceOf(ArrayBuffer)
88
+ expect((calls[0]![1] as ArrayBuffer).byteLength).toBe(CursorStyleOptionsStruct.size)
89
+ })
90
+ })
91
+ })
92
+
93
+ describe("packed color owner retention", () => {
94
+ test("styled chunk fg and bg colors stay readable after GC of transient chunks", async () => {
95
+ const fgOffset = fieldOffset(StyledChunkStruct, "fg")
96
+ const bgOffset = fieldOffset(StyledChunkStruct, "bg")
97
+
98
+ const packTransientChunks = (count: number) => {
99
+ const chunks = []
100
+ const expected = []
101
+ for (let i = 0; i < count; i++) {
102
+ const fg = RGBA.fromValues((i % 16) / 15, 0, 1, 1)
103
+ const bg = RGBA.fromValues(0, (i % 16) / 15, 0, 1)
104
+ chunks.push({ text: `chunk-${i}`, fg, bg })
105
+ expected.push({ fg: [...fg.buffer], bg: [...bg.buffer] })
106
+ }
107
+ // The chunk objects and their RGBA instances are unreachable after this
108
+ // returns; only the packed buffer may keep the color memory alive.
109
+ return { packed: StyledChunkStruct.packList(chunks), expected }
110
+ }
111
+
112
+ const count = 16
113
+ const { packed, expected } = packTransientChunks(count)
114
+
115
+ for (let round = 0; round < 20; round++) {
116
+ const churn = []
117
+ for (let i = 0; i < 2048; i++) {
118
+ churn.push(new Uint16Array(4).fill(round))
119
+ }
120
+ await forceGc()
121
+
122
+ for (let i = 0; i < count; i++) {
123
+ const base = i * StyledChunkStruct.size
124
+ expect(readPackedColor(packed, base + fgOffset)).toEqual(expected[i]!.fg)
125
+ expect(readPackedColor(packed, base + bgOffset)).toEqual(expected[i]!.bg)
126
+ }
127
+ }
128
+ })
129
+
130
+ test("cursor style color stays readable after GC of the transient RGBA", async () => {
131
+ const colorOffset = fieldOffset(CursorStyleOptionsStruct, "color")
132
+
133
+ const packTransientColor = () => {
134
+ const color = RGBA.fromValues(0.5, 0.25, 0.75, 1)
135
+ return {
136
+ packed: CursorStyleOptionsStruct.pack({ style: 255, blinking: 255, color, cursor: 255 }),
137
+ expected: [...color.buffer],
138
+ }
139
+ }
140
+
141
+ const { packed, expected } = packTransientColor()
142
+
143
+ for (let round = 0; round < 20; round++) {
144
+ const churn = []
145
+ for (let i = 0; i < 2048; i++) {
146
+ churn.push(new Uint16Array(4).fill(round))
147
+ }
148
+ await forceGc()
149
+
150
+ expect(readPackedColor(packed, colorOffset)).toEqual(expected)
151
+ }
152
+ })
153
+ })
@@ -0,0 +1,98 @@
1
+ import { beforeEach, describe, expect, test, afterEach } from "bun:test"
2
+ import { createTestRenderer, MouseButtons, type MockMouse, type TestRenderer } from "../testing.js"
3
+ import { BoxRenderable } from "../renderables/index.js"
4
+ import type { MousePointerStyle } from "../types.js"
5
+
6
+ describe("mouse pointer style", () => {
7
+ let renderer: TestRenderer
8
+ let mockMouse: MockMouse
9
+ let renderOnce: () => Promise<void>
10
+
11
+ beforeEach(async () => {
12
+ ;({ renderer, mockMouse, renderOnce } = await createTestRenderer({ width: 40, height: 20 }))
13
+ })
14
+
15
+ afterEach(() => {
16
+ renderer.destroy()
17
+ })
18
+
19
+ test("setMousePointer sets style", async () => {
20
+ renderer.setMousePointer("pointer")
21
+ expect((renderer as any)._currentMousePointerStyle).toBe("pointer")
22
+ })
23
+
24
+ test("setMousePointer with 'default' clears style", async () => {
25
+ renderer.setMousePointer("pointer")
26
+ renderer.setMousePointer("default")
27
+ expect((renderer as any)._currentMousePointerStyle).toBe("default")
28
+ })
29
+
30
+ test("setMousePointer supports all style types", async () => {
31
+ const styles: MousePointerStyle[] = ["default", "pointer", "text", "crosshair", "move", "not-allowed"]
32
+ for (const style of styles) {
33
+ renderer.setMousePointer(style)
34
+ expect((renderer as any)._currentMousePointerStyle).toBe(style)
35
+ }
36
+ })
37
+
38
+ test("onMouseOver callback can set mouse pointer", async () => {
39
+ let pointerSet = false
40
+ const box = new BoxRenderable(renderer, {
41
+ position: "absolute",
42
+ left: 5,
43
+ top: 5,
44
+ width: 10,
45
+ height: 5,
46
+ onMouseOver() {
47
+ this.ctx.setMousePointer("pointer")
48
+ pointerSet = true
49
+ },
50
+ })
51
+ renderer.root.add(box)
52
+ await renderOnce()
53
+
54
+ await mockMouse.moveTo(10, 7)
55
+ await renderOnce()
56
+
57
+ expect(pointerSet).toBe(true)
58
+ expect((renderer as any)._currentMousePointerStyle).toBe("pointer")
59
+ })
60
+
61
+ test("onMouseOut callback can reset mouse pointer", async () => {
62
+ let pointerReset = false
63
+ const box = new BoxRenderable(renderer, {
64
+ position: "absolute",
65
+ left: 5,
66
+ top: 5,
67
+ width: 10,
68
+ height: 5,
69
+ onMouseOver() {
70
+ this.ctx.setMousePointer("pointer")
71
+ },
72
+ onMouseOut() {
73
+ this.ctx.setMousePointer("default")
74
+ pointerReset = true
75
+ },
76
+ })
77
+ renderer.root.add(box)
78
+ await renderOnce()
79
+
80
+ // Move into box
81
+ await mockMouse.moveTo(10, 7)
82
+ await renderOnce()
83
+ expect((renderer as any)._currentMousePointerStyle).toBe("pointer")
84
+
85
+ // Move out of box
86
+ await mockMouse.moveTo(1, 1)
87
+ await renderOnce()
88
+
89
+ expect(pointerReset).toBe(true)
90
+ expect((renderer as any)._currentMousePointerStyle).toBe("default")
91
+ })
92
+
93
+ test("pointer resets on renderer destroy", async () => {
94
+ renderer.setMousePointer("pointer")
95
+ renderer.destroy()
96
+ // After destroy, the reset is called internally - just verify no error
97
+ })
98
+ })
@@ -0,0 +1,133 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { BoxRenderable } from "../renderables/Box.js"
3
+ import { TextareaRenderable } from "../renderables/Textarea.js"
4
+ import { TextRenderable } from "../renderables/Text.js"
5
+ import { createTestRenderer, type TestRenderer } from "../testing/test-renderer.js"
6
+
7
+ // Native-backed measurement wires renderables to native state (measure targets,
8
+ // Yoga measure funcs, handles). These tests lock the lifecycle behavior:
9
+ // destroying renderables must detach cleanly while the rest of the tree keeps
10
+ // measuring, including under create/layout/destroy churn.
11
+
12
+ let renderer: TestRenderer
13
+ let renderOnce: () => Promise<void>
14
+
15
+ beforeEach(async () => {
16
+ ;({ renderer, renderOnce } = await createTestRenderer({ width: 80, height: 30 }))
17
+ })
18
+
19
+ afterEach(() => {
20
+ renderer.destroy()
21
+ })
22
+
23
+ function maybeCollectGarbage(): void {
24
+ const bun = (globalThis as { Bun?: { gc?: (force?: boolean) => void } }).Bun
25
+ bun?.gc?.(false)
26
+ }
27
+
28
+ function expectSize(
29
+ renderable: TextRenderable | TextareaRenderable,
30
+ expected: { width: number; height: number },
31
+ ): void {
32
+ expect(renderable.width).toBeCloseTo(expected.width, 5)
33
+ expect(renderable.height).toBeCloseTo(expected.height, 5)
34
+ }
35
+
36
+ describe("native-backed measurement lifecycle", () => {
37
+ test("destroying a text renderable keeps sibling measurement working", async () => {
38
+ const parent = new BoxRenderable(renderer, { width: 40, flexDirection: "column", alignItems: "flex-start" })
39
+ const first = new TextRenderable(renderer, { content: "AAAAA", wrapMode: "none", alignSelf: "flex-start" })
40
+ const second = new TextRenderable(renderer, { content: "BBBBBBBBBB", wrapMode: "none", alignSelf: "flex-start" })
41
+ parent.add(first)
42
+ parent.add(second)
43
+ renderer.root.add(parent)
44
+ await renderOnce()
45
+
46
+ expectSize(first, { width: 5, height: 1 })
47
+ expectSize(second, { width: 10, height: 1 })
48
+
49
+ first.destroy()
50
+ await renderOnce()
51
+ expectSize(second, { width: 10, height: 1 })
52
+
53
+ second.content = "CCC"
54
+ await renderOnce()
55
+ expectSize(second, { width: 3, height: 1 })
56
+ })
57
+
58
+ test("destroying a textarea keeps sibling measurement working", async () => {
59
+ const parent = new BoxRenderable(renderer, { width: 40, flexDirection: "column", alignItems: "flex-start" })
60
+ const first = new TextareaRenderable(renderer, { initialValue: "AAAAA", wrapMode: "none", alignSelf: "flex-start" })
61
+ const second = new TextareaRenderable(renderer, {
62
+ initialValue: "BBBBBBBBBB",
63
+ wrapMode: "none",
64
+ alignSelf: "flex-start",
65
+ })
66
+ parent.add(first)
67
+ parent.add(second)
68
+ renderer.root.add(parent)
69
+ await renderOnce()
70
+
71
+ expectSize(first, { width: 5, height: 1 })
72
+ expectSize(second, { width: 10, height: 1 })
73
+
74
+ first.destroy()
75
+ await renderOnce()
76
+ expectSize(second, { width: 10, height: 1 })
77
+
78
+ second.setText("CCC")
79
+ await renderOnce()
80
+ expectSize(second, { width: 3, height: 1 })
81
+ })
82
+
83
+ test("survives create/layout/destroy churn of native-backed renderables", async () => {
84
+ for (let round = 0; round < 20; round++) {
85
+ const container = new BoxRenderable(renderer, { flexDirection: "column", alignItems: "flex-start" })
86
+ renderer.root.add(container)
87
+
88
+ const texts: TextRenderable[] = []
89
+ const textareas: TextareaRenderable[] = []
90
+ for (let index = 0; index < 16; index++) {
91
+ const width = 1 + ((round + index) % 20)
92
+ const text = new TextRenderable(renderer, {
93
+ content: "X".repeat(width),
94
+ wrapMode: "none",
95
+ alignSelf: "flex-start",
96
+ })
97
+ texts.push(text)
98
+ container.add(text)
99
+ }
100
+ for (let index = 0; index < 6; index++) {
101
+ const width = 1 + ((round + index) % 15)
102
+ const textarea = new TextareaRenderable(renderer, {
103
+ initialValue: "Y".repeat(width),
104
+ wrapMode: "none",
105
+ alignSelf: "flex-start",
106
+ })
107
+ textareas.push(textarea)
108
+ container.add(textarea)
109
+ }
110
+
111
+ await renderOnce()
112
+
113
+ for (const [index, text] of texts.entries()) {
114
+ expectSize(text, { width: 1 + ((round + index) % 20), height: 1 })
115
+ }
116
+ for (const [index, textarea] of textareas.entries()) {
117
+ expectSize(textarea, { width: 1 + ((round + index) % 15), height: 1 })
118
+ }
119
+
120
+ // Alternate destroy orders: children-first and recursive subtree destroy.
121
+ if (round % 2 === 0) {
122
+ for (const text of texts) text.destroy()
123
+ for (const textarea of textareas) textarea.destroy()
124
+ container.destroy()
125
+ } else {
126
+ container.destroyRecursively()
127
+ }
128
+
129
+ if (round % 5 === 0) maybeCollectGarbage()
130
+ await renderOnce()
131
+ }
132
+ })
133
+ })
@@ -0,0 +1,322 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { BoxRenderable } from "../renderables/Box.js"
3
+ import { TextareaRenderable } from "../renderables/Textarea.js"
4
+ import { TextRenderable } from "../renderables/Text.js"
5
+ import { createTestRenderer, type TestRenderer } from "../testing/test-renderer.js"
6
+
7
+ // These tests lock the measurement behavior of the old TypeScript Yoga measure
8
+ // functions at the renderable/Yoga boundary, now that TextRenderable and
9
+ // TextareaRenderable use native-backed measurement. All expectations are
10
+ // hardcoded so the tests stay valid across implementation refactors.
11
+
12
+ const WORD_WRAP_CONTENT = "Hello wonderful world from OpenTUI"
13
+ const PLACEHOLDER_CONTENT = "Placeholder text that is longer than content"
14
+
15
+ let renderer: TestRenderer
16
+ let renderOnce: () => Promise<void>
17
+
18
+ beforeEach(async () => {
19
+ ;({ renderer, renderOnce } = await createTestRenderer({ width: 80, height: 30 }))
20
+ })
21
+
22
+ afterEach(() => {
23
+ renderer.destroy()
24
+ })
25
+
26
+ function expectCloseToLayout(actual: number, expected: number): void {
27
+ expect(actual).toBeCloseTo(expected, 5)
28
+ }
29
+
30
+ function expectLayout(
31
+ renderable: TextRenderable | TextareaRenderable,
32
+ expected: { width: number; height: number },
33
+ ): void {
34
+ expectCloseToLayout(renderable.width, expected.width)
35
+ expectCloseToLayout(renderable.height, expected.height)
36
+
37
+ const layout = renderable.getLayoutNode().getComputedLayout()
38
+ expectCloseToLayout(layout.width, expected.width)
39
+ expectCloseToLayout(layout.height, expected.height)
40
+ }
41
+
42
+ async function renderInConstrainedColumn(renderable: TextRenderable | TextareaRenderable): Promise<void> {
43
+ renderer.root.add(renderable)
44
+ await renderOnce()
45
+ }
46
+
47
+ async function renderInParent(
48
+ renderable: TextRenderable | TextareaRenderable,
49
+ options: { width: number; height?: number },
50
+ ): Promise<void> {
51
+ const parent = new BoxRenderable(renderer, {
52
+ width: options.width,
53
+ ...(options.height === undefined ? {} : { height: options.height }),
54
+ alignItems: "flex-start",
55
+ })
56
+ parent.add(renderable)
57
+ renderer.root.add(parent)
58
+ await renderOnce()
59
+ }
60
+
61
+ describe("native-backed measurement parity", () => {
62
+ describe("TextRenderable", () => {
63
+ test("matches native char-wrap measurement with relative AtMost clamping", async () => {
64
+ const text = new TextRenderable(renderer, {
65
+ content: "ABCDEFGHIJKLMNOPQRST",
66
+ wrapMode: "char",
67
+ alignSelf: "flex-start",
68
+ })
69
+
70
+ await renderInConstrainedColumn(text)
71
+
72
+ expectLayout(text, { width: 20, height: 1 })
73
+ })
74
+
75
+ test("matches native word-wrap measurement with relative AtMost clamping", async () => {
76
+ const text = new TextRenderable(renderer, {
77
+ content: WORD_WRAP_CONTENT,
78
+ wrapMode: "word",
79
+ alignSelf: "flex-start",
80
+ })
81
+
82
+ await renderInConstrainedColumn(text)
83
+
84
+ expectLayout(text, { width: WORD_WRAP_CONTENT.length, height: 1 })
85
+ })
86
+
87
+ test("matches native no-wrap measurement", async () => {
88
+ const text = new TextRenderable(renderer, {
89
+ content: "Short\nAVeryLongLineHere\nMedium",
90
+ wrapMode: "none",
91
+ alignSelf: "flex-start",
92
+ })
93
+
94
+ await renderInConstrainedColumn(text)
95
+
96
+ expectLayout(text, { width: "AVeryLongLineHere".length, height: 3 })
97
+ })
98
+
99
+ test("clamps relative AtMost measurement to a narrower parent", async () => {
100
+ const text = new TextRenderable(renderer, {
101
+ content: "ABCDEFGHIJKLMNOPQRST",
102
+ wrapMode: "none",
103
+ alignSelf: "flex-start",
104
+ })
105
+
106
+ await renderInParent(text, { width: 10 })
107
+
108
+ expectLayout(text, { width: 10, height: 1 })
109
+ })
110
+
111
+ test("stretches relative text to the root width by default", async () => {
112
+ const text = new TextRenderable(renderer, {
113
+ content: "Short",
114
+ wrapMode: "char",
115
+ })
116
+
117
+ await renderInConstrainedColumn(text)
118
+
119
+ expectLayout(text, { width: 80, height: 1 })
120
+ })
121
+
122
+ test("preserves minimum Yoga measurement size for empty content", async () => {
123
+ const text = new TextRenderable(renderer, {
124
+ content: "",
125
+ wrapMode: "char",
126
+ alignSelf: "flex-start",
127
+ })
128
+
129
+ await renderInConstrainedColumn(text)
130
+
131
+ expectLayout(text, { width: 1, height: 1 })
132
+ })
133
+
134
+ test("does not apply relative AtMost clamping for absolute-positioned text", async () => {
135
+ const text = new TextRenderable(renderer, {
136
+ content: "ABCDEFGHIJKLMNOPQRST",
137
+ wrapMode: "none",
138
+ position: "absolute",
139
+ left: 0,
140
+ top: 0,
141
+ })
142
+
143
+ await renderInConstrainedColumn(text)
144
+
145
+ expectLayout(text, { width: 20, height: 1 })
146
+ })
147
+
148
+ test("recomputes measurement after content changes", async () => {
149
+ const text = new TextRenderable(renderer, {
150
+ content: "Short",
151
+ wrapMode: "char",
152
+ alignSelf: "flex-start",
153
+ })
154
+
155
+ await renderInConstrainedColumn(text)
156
+ expectLayout(text, { width: 5, height: 1 })
157
+
158
+ text.content = "ABCDEFGHIJKLMNOPQRST"
159
+ await renderOnce()
160
+
161
+ expectLayout(text, { width: 20, height: 1 })
162
+ })
163
+
164
+ test("applies and removes AtMost clamping when position changes at runtime", async () => {
165
+ const text = new TextRenderable(renderer, {
166
+ content: "ABCDEFGHIJKLMNOPQRST",
167
+ wrapMode: "none",
168
+ alignSelf: "flex-start",
169
+ left: 0,
170
+ top: 0,
171
+ })
172
+
173
+ await renderInParent(text, { width: 10 })
174
+ expectLayout(text, { width: 10, height: 1 })
175
+
176
+ text.position = "absolute"
177
+ await renderOnce()
178
+ expectLayout(text, { width: 20, height: 1 })
179
+
180
+ text.position = "relative"
181
+ await renderOnce()
182
+ expectLayout(text, { width: 10, height: 1 })
183
+ })
184
+ })
185
+
186
+ describe("TextareaRenderable", () => {
187
+ test("matches native char-wrap editor measurement with relative AtMost clamping", async () => {
188
+ const textarea = new TextareaRenderable(renderer, {
189
+ initialValue: "ABCDEFGHIJKLMNOPQRST",
190
+ wrapMode: "char",
191
+ alignSelf: "flex-start",
192
+ })
193
+
194
+ await renderInConstrainedColumn(textarea)
195
+
196
+ expectLayout(textarea, { width: 20, height: 1 })
197
+ })
198
+
199
+ test("matches native word-wrap editor measurement with relative AtMost clamping", async () => {
200
+ const textarea = new TextareaRenderable(renderer, {
201
+ initialValue: WORD_WRAP_CONTENT,
202
+ wrapMode: "word",
203
+ alignSelf: "flex-start",
204
+ })
205
+
206
+ await renderInConstrainedColumn(textarea)
207
+
208
+ expectLayout(textarea, { width: WORD_WRAP_CONTENT.length, height: 1 })
209
+ })
210
+
211
+ test("matches native no-wrap editor measurement", async () => {
212
+ const textarea = new TextareaRenderable(renderer, {
213
+ initialValue: "Short\nAVeryLongLineHere\nMedium",
214
+ wrapMode: "none",
215
+ alignSelf: "flex-start",
216
+ })
217
+
218
+ await renderInConstrainedColumn(textarea)
219
+
220
+ expectLayout(textarea, { width: "AVeryLongLineHere".length, height: 3 })
221
+ })
222
+
223
+ test("clamps relative editor AtMost measurement to a narrower parent", async () => {
224
+ const textarea = new TextareaRenderable(renderer, {
225
+ initialValue: "ABCDEFGHIJKLMNOPQRST",
226
+ wrapMode: "none",
227
+ alignSelf: "flex-start",
228
+ })
229
+
230
+ await renderInParent(textarea, { width: 10 })
231
+
232
+ expectLayout(textarea, { width: 10, height: 1 })
233
+ })
234
+
235
+ test("stretches relative editors to the root width by default", async () => {
236
+ const textarea = new TextareaRenderable(renderer, {
237
+ initialValue: "Short",
238
+ wrapMode: "char",
239
+ })
240
+
241
+ await renderInConstrainedColumn(textarea)
242
+
243
+ expectLayout(textarea, { width: 80, height: 1 })
244
+ })
245
+
246
+ test("preserves minimum Yoga measurement size for an empty editor", async () => {
247
+ const textarea = new TextareaRenderable(renderer, {
248
+ initialValue: "",
249
+ wrapMode: "char",
250
+ alignSelf: "flex-start",
251
+ })
252
+
253
+ await renderInConstrainedColumn(textarea)
254
+
255
+ expectLayout(textarea, { width: 1, height: 1 })
256
+ })
257
+
258
+ test("does not apply relative AtMost clamping for absolute-positioned editors", async () => {
259
+ const textarea = new TextareaRenderable(renderer, {
260
+ initialValue: "ABCDEFGHIJKLMNOPQRST",
261
+ wrapMode: "none",
262
+ position: "absolute",
263
+ left: 0,
264
+ top: 0,
265
+ })
266
+
267
+ await renderInConstrainedColumn(textarea)
268
+
269
+ expectLayout(textarea, { width: 20, height: 1 })
270
+ })
271
+
272
+ test("recomputes editor measurement after text changes", async () => {
273
+ const textarea = new TextareaRenderable(renderer, {
274
+ initialValue: "Short",
275
+ wrapMode: "char",
276
+ alignSelf: "flex-start",
277
+ })
278
+
279
+ await renderInConstrainedColumn(textarea)
280
+ expectLayout(textarea, { width: 5, height: 1 })
281
+
282
+ textarea.setText("ABCDEFGHIJKLMNOPQRST")
283
+ await renderOnce()
284
+
285
+ expectLayout(textarea, { width: 20, height: 1 })
286
+ })
287
+
288
+ test("captures current placeholder measurement behavior", async () => {
289
+ const textarea = new TextareaRenderable(renderer, {
290
+ initialValue: "",
291
+ placeholder: PLACEHOLDER_CONTENT,
292
+ wrapMode: "char",
293
+ alignSelf: "flex-start",
294
+ })
295
+
296
+ await renderInConstrainedColumn(textarea)
297
+
298
+ expectLayout(textarea, { width: PLACEHOLDER_CONTENT.length, height: 1 })
299
+ })
300
+
301
+ test("applies and removes AtMost clamping when position changes at runtime", async () => {
302
+ const textarea = new TextareaRenderable(renderer, {
303
+ initialValue: "ABCDEFGHIJKLMNOPQRST",
304
+ wrapMode: "none",
305
+ alignSelf: "flex-start",
306
+ left: 0,
307
+ top: 0,
308
+ })
309
+
310
+ await renderInParent(textarea, { width: 10 })
311
+ expectLayout(textarea, { width: 10, height: 1 })
312
+
313
+ textarea.position = "absolute"
314
+ await renderOnce()
315
+ expectLayout(textarea, { width: 20, height: 1 })
316
+
317
+ textarea.position = "relative"
318
+ await renderOnce()
319
+ expectLayout(textarea, { width: 10, height: 1 })
320
+ })
321
+ })
322
+ })