@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,928 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // This benchmark targets render/layout bookkeeping in wrapper-heavy trees,
4
+ // scrollbox culling, and scrollbar-heavy paths that exercise Renderable
5
+ // traversal without depending on one specific widget.
6
+
7
+ import { performance } from "node:perf_hooks"
8
+ import { existsSync } from "node:fs"
9
+ import { mkdir } from "node:fs/promises"
10
+ import path from "node:path"
11
+ import { Command } from "commander"
12
+ import { BoxRenderable, RGBA, ScrollBarRenderable, ScrollBoxRenderable, TextRenderable } from "../index.js"
13
+ import { createTestRenderer, type TestRenderer } from "../testing.js"
14
+
15
+ type ScenarioRuntime = {
16
+ renderablesPerIteration: number
17
+ layoutOnlyBoxesPerIteration: number
18
+ runIteration: (iteration: number) => Promise<void>
19
+ teardown?: () => void | Promise<void>
20
+ }
21
+
22
+ type ScenarioDefinition = {
23
+ name: string
24
+ description: string
25
+ setup: (ctx: BenchmarkContext) => Promise<ScenarioRuntime> | ScenarioRuntime
26
+ }
27
+
28
+ type BenchmarkContext = {
29
+ renderer: TestRenderer
30
+ renderOnce: () => Promise<void>
31
+ width: number
32
+ height: number
33
+ }
34
+
35
+ type ScenarioResult = {
36
+ name: string
37
+ description: string
38
+ iterations: number
39
+ warmupIterations: number
40
+ elapsedMs: number
41
+ renderablesPerIteration: number
42
+ layoutOnlyBoxesPerIteration: number
43
+ avgMs: number
44
+ medianMs: number
45
+ p95Ms: number
46
+ minMs: number
47
+ maxMs: number
48
+ stdDevMs: number
49
+ rmePercent: number
50
+ approxUsPerRenderable: number
51
+ }
52
+
53
+ type TimingStats = {
54
+ avgMs: number
55
+ medianMs: number
56
+ p95Ms: number
57
+ minMs: number
58
+ maxMs: number
59
+ stdDevMs: number
60
+ rmePercent: number
61
+ }
62
+
63
+ type TreeStats = {
64
+ renderables: number
65
+ layoutOnlyBoxes: number
66
+ }
67
+
68
+ type LayoutTreeOptions = {
69
+ messageCount: number
70
+ includeVisibleBoxes: boolean
71
+ includeText: boolean
72
+ }
73
+
74
+ type LayoutTreeState = {
75
+ root: BoxRenderable
76
+ stats: TreeStats
77
+ }
78
+
79
+ const SUITES = {
80
+ quick: { iterations: 300, warmupIterations: 40 },
81
+ default: { iterations: 1800, warmupIterations: 120 },
82
+ long: { iterations: 5000, warmupIterations: 250 },
83
+ } as const
84
+
85
+ const COLORS = {
86
+ transparent: RGBA.fromInts(0, 0, 0, 0),
87
+ panel: RGBA.fromInts(28, 32, 38),
88
+ element: RGBA.fromInts(40, 46, 56),
89
+ menu: RGBA.fromInts(35, 40, 48),
90
+ accent: RGBA.fromInts(84, 171, 224),
91
+ warning: RGBA.fromInts(219, 186, 96),
92
+ } as const
93
+
94
+ const program = new Command()
95
+ program
96
+ .name("render-traversal-benchmark")
97
+ .description("Benchmark render-tree traversal with headless test renderer")
98
+ .option("-s, --suite <name>", "benchmark suite: quick, default, long", "default")
99
+ .option("-i, --iterations <count>", "iterations per scenario")
100
+ .option("--warmup-iterations <count>", "warmup iterations per scenario")
101
+ .option("--width <count>", "test renderer width", "140")
102
+ .option("--height <count>", "test renderer height", "44")
103
+ .option("--scenario <name>", "run only one scenario")
104
+ .option("--list-scenarios", "list scenario names and exit")
105
+ .option("--json [path]", "write benchmark results to JSON")
106
+ .option("--no-output", "suppress stdout output")
107
+ .parse(process.argv)
108
+
109
+ const options = program.opts()
110
+ const suiteName = String(options.suite)
111
+ const suiteDefaults = SUITES[suiteName as keyof typeof SUITES]
112
+
113
+ if (!suiteDefaults) {
114
+ console.error(`Unknown suite: ${suiteName}. Valid suites: ${Object.keys(SUITES).join(", ")}`)
115
+ process.exit(1)
116
+ }
117
+
118
+ const iterations = Math.max(1, Math.floor(toNumber(options.iterations, suiteDefaults.iterations)))
119
+ const warmupIterations = Math.max(0, Math.floor(toNumber(options.warmupIterations, suiteDefaults.warmupIterations)))
120
+ const width = Math.max(40, Math.floor(toNumber(options.width, 140)))
121
+ const height = Math.max(20, Math.floor(toNumber(options.height, 44)))
122
+ const scenarioFilter = options.scenario ? String(options.scenario) : null
123
+ const outputEnabled = options.output !== false
124
+
125
+ const jsonArg = options.json
126
+ const jsonPath =
127
+ typeof jsonArg === "string"
128
+ ? path.resolve(process.cwd(), jsonArg)
129
+ : jsonArg
130
+ ? path.resolve(process.cwd(), "latest-render-traversal-bench-run.json")
131
+ : null
132
+
133
+ if (jsonPath) {
134
+ const dir = path.dirname(jsonPath)
135
+ if (!existsSync(dir)) {
136
+ await mkdir(dir, { recursive: true })
137
+ }
138
+
139
+ if (existsSync(jsonPath)) {
140
+ console.error(`Error: output file already exists: ${jsonPath}`)
141
+ process.exit(1)
142
+ }
143
+ }
144
+
145
+ const scenarios = createScenarios()
146
+
147
+ if (options.listScenarios) {
148
+ for (const scenario of scenarios) {
149
+ console.log(scenario.name)
150
+ }
151
+ process.exit(0)
152
+ }
153
+
154
+ const selectedScenarios = scenarioFilter ? scenarios.filter((scenario) => scenario.name === scenarioFilter) : scenarios
155
+ if (selectedScenarios.length === 0) {
156
+ console.error(`Unknown scenario: ${scenarioFilter}`)
157
+ process.exit(1)
158
+ }
159
+
160
+ if (outputEnabled) {
161
+ console.log(`render traversal benchmark (${suiteName})`)
162
+ console.log(`- renderer: ${width}x${height}`)
163
+ console.log(`- scenarios: ${selectedScenarios.length}`)
164
+ console.log(`- iterations: ${iterations} (+${warmupIterations} warmup)`)
165
+ }
166
+
167
+ const { renderer, renderOnce } = await createTestRenderer({
168
+ width,
169
+ height,
170
+ targetFps: 60,
171
+ maxFps: 60,
172
+ screenMode: "main-screen",
173
+ externalOutputMode: "passthrough",
174
+ consoleMode: "disabled",
175
+ useMouse: false,
176
+ })
177
+
178
+ const ctx: BenchmarkContext = { renderer, renderOnce, width, height }
179
+ const results: ScenarioResult[] = []
180
+
181
+ try {
182
+ for (const scenario of selectedScenarios) {
183
+ writeLine(outputEnabled, `Running ${scenario.name}...`)
184
+ const result = await runScenario(scenario, ctx, iterations, warmupIterations)
185
+ results.push(result)
186
+ writeLine(
187
+ outputEnabled,
188
+ ` avg=${result.avgMs.toFixed(4)}ms p95=${result.p95Ms.toFixed(4)}ms rme=${result.rmePercent.toFixed(2)}%`,
189
+ )
190
+ }
191
+ } finally {
192
+ renderer.destroy()
193
+ }
194
+
195
+ if (outputEnabled) {
196
+ console.table(
197
+ results.map((result) => ({
198
+ scenario: result.name,
199
+ renderables: result.renderablesPerIteration,
200
+ layoutOnlyBoxes: result.layoutOnlyBoxesPerIteration,
201
+ avgMs: result.avgMs,
202
+ p95Ms: result.p95Ms,
203
+ "rme%": result.rmePercent,
204
+ usPerRenderable: result.approxUsPerRenderable,
205
+ })),
206
+ )
207
+ }
208
+
209
+ if (jsonPath) {
210
+ await Bun.write(
211
+ jsonPath,
212
+ JSON.stringify(
213
+ {
214
+ metadata: {
215
+ suite: suiteName,
216
+ width,
217
+ height,
218
+ iterations,
219
+ warmupIterations,
220
+ scenarioFilter,
221
+ timestamp: new Date().toISOString(),
222
+ },
223
+ scenarios: results,
224
+ },
225
+ null,
226
+ 2,
227
+ ),
228
+ )
229
+ writeLine(outputEnabled, `Wrote benchmark JSON: ${jsonPath}`)
230
+ }
231
+
232
+ function createScenarios(): ScenarioDefinition[] {
233
+ return [
234
+ {
235
+ name: "layout_only_opencode_wrappers",
236
+ description: "OpenCode-like nested layout boxes with no visible box output",
237
+ setup: async (ctx) => {
238
+ const state = await buildOpencodeLayoutTree(ctx, {
239
+ messageCount: Math.max(48, ctx.height + 12),
240
+ includeVisibleBoxes: false,
241
+ includeText: false,
242
+ })
243
+
244
+ return {
245
+ renderablesPerIteration: state.stats.renderables,
246
+ layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
247
+ runIteration: async () => {
248
+ await ctx.renderOnce()
249
+ },
250
+ teardown: () => {
251
+ state.root.destroyRecursively()
252
+ },
253
+ }
254
+ },
255
+ },
256
+ {
257
+ name: "mixed_opencode_wrappers",
258
+ description: "OpenCode-like layout tree with sparse visible panels and text leaves",
259
+ setup: async (ctx) => {
260
+ const state = await buildOpencodeLayoutTree(ctx, {
261
+ messageCount: Math.max(40, ctx.height + 8),
262
+ includeVisibleBoxes: true,
263
+ includeText: true,
264
+ })
265
+
266
+ return {
267
+ renderablesPerIteration: state.stats.renderables,
268
+ layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
269
+ runIteration: async () => {
270
+ await ctx.renderOnce()
271
+ },
272
+ teardown: () => {
273
+ state.root.destroyRecursively()
274
+ },
275
+ }
276
+ },
277
+ },
278
+ {
279
+ name: "scrollbox_viewport_culling",
280
+ description: "Viewport-culling content tree with many hidden children",
281
+ setup: async (ctx) => {
282
+ clearRoot(ctx.renderer)
283
+ resetBuffers(ctx.renderer)
284
+
285
+ let renderables = 0
286
+ let layoutOnlyBoxes = 0
287
+ const root = new BoxRenderable(ctx.renderer, {
288
+ id: "bench-scroll-root",
289
+ width: "100%",
290
+ height: "100%",
291
+ border: false,
292
+ backgroundColor: COLORS.transparent,
293
+ })
294
+ renderables += 1
295
+ layoutOnlyBoxes += 1
296
+ ctx.renderer.root.add(root)
297
+
298
+ const scrollBox = new ScrollBoxRenderable(ctx.renderer, {
299
+ id: "bench-scrollbox",
300
+ width: "100%",
301
+ height: "100%",
302
+ stickyScroll: true,
303
+ stickyStart: "bottom",
304
+ viewportCulling: true,
305
+ })
306
+ renderables += 1
307
+ layoutOnlyBoxes += 1
308
+ root.add(scrollBox)
309
+
310
+ const itemCount = Math.max(120, ctx.height * 8)
311
+ for (let i = 0; i < itemCount; i += 1) {
312
+ const item = new BoxRenderable(ctx.renderer, {
313
+ id: `bench-scroll-item-${i}`,
314
+ width: "100%",
315
+ height: i % 3 === 0 ? 3 : 2,
316
+ border: false,
317
+ backgroundColor: COLORS.transparent,
318
+ paddingLeft: 2,
319
+ paddingRight: 1,
320
+ flexDirection: "column",
321
+ })
322
+ renderables += 1
323
+ layoutOnlyBoxes += 1
324
+
325
+ const leaf = new BoxRenderable(ctx.renderer, {
326
+ id: `bench-scroll-leaf-${i}`,
327
+ width: "100%",
328
+ height: 1,
329
+ border: false,
330
+ backgroundColor: i % 2 === 0 ? COLORS.panel : COLORS.element,
331
+ })
332
+ renderables += 1
333
+
334
+ item.add(leaf)
335
+ scrollBox.add(item)
336
+ }
337
+
338
+ await ctx.renderOnce()
339
+
340
+ return {
341
+ renderablesPerIteration: renderables,
342
+ layoutOnlyBoxesPerIteration: layoutOnlyBoxes,
343
+ runIteration: async () => {
344
+ await ctx.renderOnce()
345
+ },
346
+ teardown: () => {
347
+ root.destroyRecursively()
348
+ },
349
+ }
350
+ },
351
+ },
352
+ createCullingScalingScenario(100),
353
+ createCullingScalingScenario(1000),
354
+ createCullingScalingScenario(5000),
355
+ createCullingScalingScenario(10000),
356
+ {
357
+ name: "scrollbar_stack",
358
+ description: "Visible scrollbars and slider tracks with arrows",
359
+ setup: async (ctx) => {
360
+ clearRoot(ctx.renderer)
361
+ resetBuffers(ctx.renderer)
362
+
363
+ let renderables = 0
364
+ let layoutOnlyBoxes = 0
365
+
366
+ const root = new BoxRenderable(ctx.renderer, {
367
+ id: "bench-scrollbar-root",
368
+ width: "100%",
369
+ height: "100%",
370
+ border: false,
371
+ backgroundColor: COLORS.transparent,
372
+ flexDirection: "column",
373
+ gap: 1,
374
+ paddingLeft: 1,
375
+ paddingRight: 1,
376
+ paddingTop: 1,
377
+ })
378
+ renderables += 1
379
+ layoutOnlyBoxes += 1
380
+ ctx.renderer.root.add(root)
381
+
382
+ const verticalRow = new BoxRenderable(ctx.renderer, {
383
+ id: "bench-scrollbar-vertical-row",
384
+ width: "100%",
385
+ height: 14,
386
+ border: false,
387
+ backgroundColor: COLORS.transparent,
388
+ flexDirection: "row",
389
+ gap: 1,
390
+ })
391
+ renderables += 1
392
+ layoutOnlyBoxes += 1
393
+ root.add(verticalRow)
394
+
395
+ for (let i = 0; i < 10; i += 1) {
396
+ const bar = new ScrollBarRenderable(ctx.renderer, {
397
+ id: `bench-vertical-scrollbar-${i}`,
398
+ orientation: "vertical",
399
+ showArrows: true,
400
+ width: 2,
401
+ height: 14,
402
+ trackOptions: {
403
+ backgroundColor: COLORS.panel,
404
+ foregroundColor: COLORS.accent,
405
+ },
406
+ arrowOptions: {
407
+ backgroundColor: COLORS.panel,
408
+ foregroundColor: COLORS.warning,
409
+ },
410
+ })
411
+ bar.scrollSize = 400 + i * 30
412
+ bar.viewportSize = 24 + (i % 3)
413
+ bar.scrollPosition = 10 + i * 7
414
+ renderables += 4
415
+ verticalRow.add(bar)
416
+ }
417
+
418
+ const horizontalColumn = new BoxRenderable(ctx.renderer, {
419
+ id: "bench-scrollbar-horizontal-column",
420
+ width: "100%",
421
+ flexGrow: 1,
422
+ border: false,
423
+ backgroundColor: COLORS.transparent,
424
+ flexDirection: "column",
425
+ gap: 1,
426
+ })
427
+ renderables += 1
428
+ layoutOnlyBoxes += 1
429
+ root.add(horizontalColumn)
430
+
431
+ for (let i = 0; i < 8; i += 1) {
432
+ const bar = new ScrollBarRenderable(ctx.renderer, {
433
+ id: `bench-horizontal-scrollbar-${i}`,
434
+ orientation: "horizontal",
435
+ showArrows: true,
436
+ width: 28,
437
+ height: 1,
438
+ trackOptions: {
439
+ backgroundColor: COLORS.element,
440
+ foregroundColor: COLORS.accent,
441
+ },
442
+ arrowOptions: {
443
+ backgroundColor: COLORS.element,
444
+ foregroundColor: COLORS.warning,
445
+ },
446
+ })
447
+ bar.scrollSize = 520 + i * 50
448
+ bar.viewportSize = 32 + (i % 4)
449
+ bar.scrollPosition = 15 + i * 11
450
+ renderables += 4
451
+ horizontalColumn.add(bar)
452
+ }
453
+
454
+ await ctx.renderOnce()
455
+
456
+ return {
457
+ renderablesPerIteration: renderables,
458
+ layoutOnlyBoxesPerIteration: layoutOnlyBoxes,
459
+ runIteration: async () => {
460
+ await ctx.renderOnce()
461
+ },
462
+ teardown: () => {
463
+ root.destroyRecursively()
464
+ },
465
+ }
466
+ },
467
+ },
468
+ ]
469
+ }
470
+
471
+ // Frame-time scaling with total child count under viewport culling, at a
472
+ // constant visible count (~viewport height). This is the per-frame
473
+ // O(total children) layout-refresh path in Renderable.updateLayout for
474
+ // _hasVisibleChildFilter parents: before culling can read screen positions,
475
+ // every child gets one updateFromLayout (FFI getComputedLayout) per frame,
476
+ // so steady-state frame time grows with hidden children. Watch
477
+ // approxUsPerRenderable across the scaling scenarios: roughly constant means
478
+ // the per-frame cost is linear in total children; if the refresh path ever
479
+ // becomes O(visible), it should drop as childCount grows.
480
+ function createCullingScalingScenario(childCount: number): ScenarioDefinition {
481
+ return {
482
+ name: `scrollbox_culling_scaling_${childCount}`,
483
+ description: `Scrolling viewport-culled scrollbox with ${childCount} rows, constant visible count`,
484
+ setup: async (ctx) => {
485
+ clearRoot(ctx.renderer)
486
+ resetBuffers(ctx.renderer)
487
+
488
+ let renderables = 0
489
+ let layoutOnlyBoxes = 0
490
+
491
+ const root = new BoxRenderable(ctx.renderer, {
492
+ id: `bench-culling-scaling-root-${childCount}`,
493
+ width: "100%",
494
+ height: "100%",
495
+ border: false,
496
+ backgroundColor: COLORS.transparent,
497
+ })
498
+ renderables += 1
499
+ layoutOnlyBoxes += 1
500
+ ctx.renderer.root.add(root)
501
+
502
+ const scrollBox = new ScrollBoxRenderable(ctx.renderer, {
503
+ id: `bench-culling-scaling-scrollbox-${childCount}`,
504
+ width: "100%",
505
+ height: "100%",
506
+ viewportCulling: true,
507
+ })
508
+ renderables += 1
509
+ layoutOnlyBoxes += 1
510
+ root.add(scrollBox)
511
+
512
+ for (let i = 0; i < childCount; i += 1) {
513
+ const row = new BoxRenderable(ctx.renderer, {
514
+ id: `bench-culling-scaling-row-${i}`,
515
+ width: "100%",
516
+ height: 1,
517
+ flexShrink: 0,
518
+ border: false,
519
+ backgroundColor: i % 2 === 0 ? COLORS.panel : COLORS.element,
520
+ })
521
+ renderables += 1
522
+ scrollBox.add(row)
523
+ }
524
+
525
+ await ctx.renderOnce()
526
+ scrollBox.scrollTo(Math.floor(childCount / 2))
527
+ await ctx.renderOnce()
528
+
529
+ return {
530
+ renderablesPerIteration: renderables,
531
+ layoutOnlyBoxesPerIteration: layoutOnlyBoxes,
532
+ runIteration: async (iteration) => {
533
+ // Alternate 1-row scrolls: every frame is a real translate change
534
+ // (the steady-state streaming/scrolling workload) while the visible
535
+ // count stays constant and the position returns home every 2 frames.
536
+ scrollBox.scrollBy(iteration % 2 === 0 ? 1 : -1)
537
+ await ctx.renderOnce()
538
+ },
539
+ teardown: () => {
540
+ root.destroyRecursively()
541
+ },
542
+ }
543
+ },
544
+ }
545
+ }
546
+
547
+ async function buildOpencodeLayoutTree(ctx: BenchmarkContext, options: LayoutTreeOptions): Promise<LayoutTreeState> {
548
+ clearRoot(ctx.renderer)
549
+ resetBuffers(ctx.renderer)
550
+
551
+ let renderables = 0
552
+ let layoutOnlyBoxes = 0
553
+
554
+ const trackLayoutBox = (box: BoxRenderable): BoxRenderable => {
555
+ renderables += 1
556
+ layoutOnlyBoxes += 1
557
+ return box
558
+ }
559
+
560
+ const trackVisualBox = (box: BoxRenderable): BoxRenderable => {
561
+ renderables += 1
562
+ return box
563
+ }
564
+
565
+ const trackText = (text: TextRenderable): TextRenderable => {
566
+ renderables += 1
567
+ return text
568
+ }
569
+
570
+ const root = trackLayoutBox(
571
+ new BoxRenderable(ctx.renderer, {
572
+ id: "bench-layout-root",
573
+ width: "100%",
574
+ height: "100%",
575
+ border: false,
576
+ backgroundColor: COLORS.transparent,
577
+ flexDirection: "column",
578
+ }),
579
+ )
580
+ ctx.renderer.root.add(root)
581
+
582
+ const header = trackLayoutBox(
583
+ new BoxRenderable(ctx.renderer, {
584
+ id: "bench-layout-header",
585
+ width: "100%",
586
+ height: 3,
587
+ flexDirection: "row",
588
+ paddingLeft: 1,
589
+ paddingRight: 1,
590
+ gap: 1,
591
+ }),
592
+ )
593
+ root.add(header)
594
+
595
+ for (let i = 0; i < 5; i += 1) {
596
+ header.add(
597
+ trackLayoutBox(
598
+ new BoxRenderable(ctx.renderer, {
599
+ id: `bench-layout-chip-${i}`,
600
+ flexShrink: 0,
601
+ paddingLeft: 1,
602
+ paddingRight: 1,
603
+ }),
604
+ ),
605
+ )
606
+ }
607
+
608
+ const body = trackLayoutBox(
609
+ new BoxRenderable(ctx.renderer, {
610
+ id: "bench-layout-body",
611
+ width: "100%",
612
+ flexGrow: 1,
613
+ flexDirection: "row",
614
+ gap: 1,
615
+ paddingLeft: 1,
616
+ paddingRight: 1,
617
+ }),
618
+ )
619
+ root.add(body)
620
+
621
+ const sidebar = trackLayoutBox(
622
+ new BoxRenderable(ctx.renderer, {
623
+ id: "bench-layout-sidebar",
624
+ width: 22,
625
+ minWidth: 22,
626
+ maxWidth: 22,
627
+ flexShrink: 0,
628
+ flexDirection: "column",
629
+ gap: 1,
630
+ }),
631
+ )
632
+ body.add(sidebar)
633
+
634
+ for (let i = 0; i < 12; i += 1) {
635
+ sidebar.add(
636
+ trackLayoutBox(
637
+ new BoxRenderable(ctx.renderer, {
638
+ id: `bench-layout-sidebar-row-${i}`,
639
+ height: 1,
640
+ paddingLeft: 1,
641
+ paddingRight: 1,
642
+ }),
643
+ ),
644
+ )
645
+ }
646
+
647
+ const main = trackLayoutBox(
648
+ new BoxRenderable(ctx.renderer, {
649
+ id: "bench-layout-main",
650
+ flexGrow: 1,
651
+ flexDirection: "column",
652
+ gap: 1,
653
+ }),
654
+ )
655
+ body.add(main)
656
+
657
+ for (let i = 0; i < options.messageCount; i += 1) {
658
+ const row = trackLayoutBox(
659
+ new BoxRenderable(ctx.renderer, {
660
+ id: `bench-layout-row-${i}`,
661
+ width: "100%",
662
+ flexDirection: "row",
663
+ }),
664
+ )
665
+
666
+ const rail = trackLayoutBox(
667
+ new BoxRenderable(ctx.renderer, {
668
+ id: `bench-layout-rail-${i}`,
669
+ width: 3,
670
+ minWidth: 3,
671
+ maxWidth: 3,
672
+ flexShrink: 0,
673
+ }),
674
+ )
675
+
676
+ const content = trackLayoutBox(
677
+ new BoxRenderable(ctx.renderer, {
678
+ id: `bench-layout-content-${i}`,
679
+ flexGrow: 1,
680
+ flexDirection: "column",
681
+ }),
682
+ )
683
+
684
+ const meta = trackLayoutBox(
685
+ new BoxRenderable(ctx.renderer, {
686
+ id: `bench-layout-meta-${i}`,
687
+ width: "100%",
688
+ height: 1,
689
+ flexDirection: "row",
690
+ justifyContent: "space-between",
691
+ }),
692
+ )
693
+
694
+ const badges = trackLayoutBox(
695
+ new BoxRenderable(ctx.renderer, {
696
+ id: `bench-layout-badges-${i}`,
697
+ flexDirection: "row",
698
+ gap: 1,
699
+ }),
700
+ )
701
+
702
+ const actions = trackLayoutBox(
703
+ new BoxRenderable(ctx.renderer, {
704
+ id: `bench-layout-actions-${i}`,
705
+ flexDirection: "row",
706
+ gap: 1,
707
+ flexShrink: 0,
708
+ }),
709
+ )
710
+
711
+ meta.add(badges)
712
+ meta.add(actions)
713
+ content.add(meta)
714
+
715
+ if (options.includeVisibleBoxes) {
716
+ content.add(
717
+ trackVisualBox(
718
+ new BoxRenderable(ctx.renderer, {
719
+ id: `bench-layout-leaf-${i}`,
720
+ width: "100%",
721
+ height: i % 5 === 0 ? 3 : i % 2 === 0 ? 2 : 1,
722
+ border: false,
723
+ backgroundColor: i % 3 === 0 ? COLORS.menu : i % 2 === 0 ? COLORS.panel : COLORS.element,
724
+ }),
725
+ ),
726
+ )
727
+ }
728
+
729
+ if (options.includeText && i % 4 === 0) {
730
+ content.add(
731
+ trackText(
732
+ new TextRenderable(ctx.renderer, {
733
+ id: `bench-layout-text-${i}`,
734
+ content: `message-${i}`,
735
+ }),
736
+ ),
737
+ )
738
+ }
739
+
740
+ row.add(rail)
741
+ row.add(content)
742
+ main.add(row)
743
+ }
744
+
745
+ const footer = trackLayoutBox(
746
+ new BoxRenderable(ctx.renderer, {
747
+ id: "bench-layout-footer",
748
+ width: "100%",
749
+ height: 4,
750
+ flexDirection: "row",
751
+ gap: 1,
752
+ paddingLeft: 1,
753
+ paddingRight: 1,
754
+ }),
755
+ )
756
+ root.add(footer)
757
+
758
+ for (let i = 0; i < 6; i += 1) {
759
+ footer.add(
760
+ trackLayoutBox(
761
+ new BoxRenderable(ctx.renderer, {
762
+ id: `bench-layout-footer-item-${i}`,
763
+ flexDirection: "row",
764
+ gap: 1,
765
+ }),
766
+ ),
767
+ )
768
+ }
769
+
770
+ await ctx.renderOnce()
771
+
772
+ return {
773
+ root,
774
+ stats: {
775
+ renderables,
776
+ layoutOnlyBoxes,
777
+ },
778
+ }
779
+ }
780
+
781
+ async function runScenario(
782
+ scenario: ScenarioDefinition,
783
+ ctx: BenchmarkContext,
784
+ iterations: number,
785
+ warmupIterations: number,
786
+ ): Promise<ScenarioResult> {
787
+ const runtime = await scenario.setup(ctx)
788
+
789
+ try {
790
+ for (let i = 0; i < warmupIterations; i += 1) {
791
+ await runtime.runIteration(i)
792
+ }
793
+
794
+ const samples = new Array<number>(iterations)
795
+ const elapsedStart = performance.now()
796
+
797
+ for (let i = 0; i < iterations; i += 1) {
798
+ const start = performance.now()
799
+ await runtime.runIteration(i)
800
+ samples[i] = performance.now() - start
801
+ }
802
+
803
+ const elapsedMs = performance.now() - elapsedStart
804
+ const stats = calculateStats(samples)
805
+
806
+ return {
807
+ name: scenario.name,
808
+ description: scenario.description,
809
+ iterations,
810
+ warmupIterations,
811
+ elapsedMs: round(elapsedMs, 4),
812
+ renderablesPerIteration: runtime.renderablesPerIteration,
813
+ layoutOnlyBoxesPerIteration: runtime.layoutOnlyBoxesPerIteration,
814
+ avgMs: round(stats.avgMs, 4),
815
+ medianMs: round(stats.medianMs, 4),
816
+ p95Ms: round(stats.p95Ms, 4),
817
+ minMs: round(stats.minMs, 4),
818
+ maxMs: round(stats.maxMs, 4),
819
+ stdDevMs: round(stats.stdDevMs, 4),
820
+ rmePercent: round(stats.rmePercent, 2),
821
+ approxUsPerRenderable:
822
+ runtime.renderablesPerIteration > 0 ? round((stats.avgMs * 1000) / runtime.renderablesPerIteration, 3) : 0,
823
+ }
824
+ } finally {
825
+ await runtime.teardown?.()
826
+ clearRoot(ctx.renderer)
827
+ resetBuffers(ctx.renderer)
828
+ }
829
+ }
830
+
831
+ function clearRoot(renderer: TestRenderer): void {
832
+ for (const child of renderer.root.getChildren()) {
833
+ child.destroyRecursively()
834
+ }
835
+ }
836
+
837
+ function resetBuffers(renderer: TestRenderer): void {
838
+ const buffers = [renderer.currentRenderBuffer, renderer.nextRenderBuffer]
839
+ for (const buffer of buffers) {
840
+ buffer.clearScissorRects()
841
+ buffer.clearOpacity()
842
+ buffer.clear(COLORS.transparent)
843
+ }
844
+ }
845
+
846
+ function calculateStats(samples: number[]): TimingStats {
847
+ if (samples.length === 0) {
848
+ return {
849
+ avgMs: 0,
850
+ medianMs: 0,
851
+ p95Ms: 0,
852
+ minMs: 0,
853
+ maxMs: 0,
854
+ stdDevMs: 0,
855
+ rmePercent: 0,
856
+ }
857
+ }
858
+
859
+ const sorted = [...samples].sort((a, b) => a - b)
860
+ const total = samples.reduce((sum, value) => sum + value, 0)
861
+ const avgMs = total / samples.length
862
+ const mid = Math.floor(sorted.length / 2)
863
+ const medianMs = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]
864
+ const p95Index = Math.floor((sorted.length - 1) * 0.95)
865
+ const p95Ms = sorted[p95Index]
866
+ const minMs = sorted[0]
867
+ const maxMs = sorted[sorted.length - 1]
868
+
869
+ let variance = 0
870
+ for (const value of samples) {
871
+ const diff = value - avgMs
872
+ variance += diff * diff
873
+ }
874
+ variance /= samples.length
875
+
876
+ return {
877
+ avgMs,
878
+ medianMs,
879
+ p95Ms,
880
+ minMs,
881
+ maxMs,
882
+ stdDevMs: Math.sqrt(variance),
883
+ rmePercent: relativeMarginOfError(samples, avgMs),
884
+ }
885
+ }
886
+
887
+ // 95% confidence relative margin of error via Student-t, matching the
888
+ // convention in layout-benchmark.ts.
889
+ function relativeMarginOfError(samples: readonly number[], average: number): number {
890
+ if (samples.length <= 1 || average === 0) {
891
+ return 0
892
+ }
893
+
894
+ let variance = 0
895
+ for (const value of samples) {
896
+ const diff = value - average
897
+ variance += diff * diff
898
+ }
899
+ variance /= samples.length - 1
900
+
901
+ const sem = Math.sqrt(variance) / Math.sqrt(samples.length)
902
+ return Math.abs((sem * tCritical95(samples.length - 1) * 100) / average)
903
+ }
904
+
905
+ function tCritical95(degreesOfFreedom: number): number {
906
+ const table = [12.706, 4.303, 3.182, 2.776, 2.571, 2.447, 2.365, 2.306, 2.262, 2.228]
907
+ if (degreesOfFreedom <= 0) {
908
+ return 0
909
+ }
910
+
911
+ return table[degreesOfFreedom - 1] ?? 1.96
912
+ }
913
+
914
+ function round(value: number, places: number): number {
915
+ return Number(value.toFixed(places))
916
+ }
917
+
918
+ function toNumber(value: unknown, fallback: number): number {
919
+ if (value === undefined || value === null || value === "") return fallback
920
+ const parsed = Number(value)
921
+ return Number.isFinite(parsed) ? parsed : fallback
922
+ }
923
+
924
+ function writeLine(enabled: boolean, line: string): void {
925
+ if (enabled) {
926
+ console.log(line)
927
+ }
928
+ }