@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,874 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // These scenarios isolate text-buffer/editor rendering from the rest of the UI
4
+ // so native text drawing and TextRenderable lifecycle work can be measured on
5
+ // wrapped, styled, and selected content with minimal unrelated noise.
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 {
13
+ bold,
14
+ BoxRenderable,
15
+ dim,
16
+ EditBuffer,
17
+ EditorView,
18
+ fg,
19
+ link,
20
+ OptimizedBuffer,
21
+ RGBA,
22
+ StyledText,
23
+ TextBuffer,
24
+ TextBufferView,
25
+ TextNodeRenderable,
26
+ TextRenderable,
27
+ TextareaRenderable,
28
+ underline,
29
+ } from "../index.js"
30
+ import { createTestRenderer, type TestRenderer } from "../testing.js"
31
+
32
+ type ScenarioKind = "direct-buffer" | "render-tree"
33
+
34
+ type ScenarioRuntime = {
35
+ kind: ScenarioKind
36
+ drawCallsPerIteration: number
37
+ renderablesPerIteration: number
38
+ runIteration: (iteration: number) => Promise<void> | void
39
+ teardown?: () => Promise<void> | void
40
+ }
41
+
42
+ type ScenarioDefinition = {
43
+ name: string
44
+ description: string
45
+ setup: (ctx: BenchmarkContext) => Promise<ScenarioRuntime> | ScenarioRuntime
46
+ }
47
+
48
+ type ScenarioResult = {
49
+ name: string
50
+ description: string
51
+ kind: ScenarioKind
52
+ iterations: number
53
+ warmupIterations: number
54
+ elapsedMs: number
55
+ drawCallsPerIteration: number
56
+ renderablesPerIteration: number
57
+ avgMs: number
58
+ medianMs: number
59
+ p95Ms: number
60
+ minMs: number
61
+ maxMs: number
62
+ stdDevMs: number
63
+ avgUsPerDrawCall: number
64
+ }
65
+
66
+ type BenchmarkContext = {
67
+ renderer: TestRenderer
68
+ renderOnce: () => Promise<void>
69
+ width: number
70
+ height: number
71
+ }
72
+
73
+ type TimingStats = {
74
+ avgMs: number
75
+ medianMs: number
76
+ p95Ms: number
77
+ minMs: number
78
+ maxMs: number
79
+ stdDevMs: number
80
+ }
81
+
82
+ type TextFeedTreeState = {
83
+ root: BoxRenderable
84
+ textNodes: TextRenderable[]
85
+ drawCallsPerIteration: number
86
+ renderablesPerIteration: number
87
+ }
88
+
89
+ type TextNodeFeedTreeState = {
90
+ root: BoxRenderable
91
+ texts: TextRenderable[]
92
+ mutableLeaves: TextNodeRenderable[]
93
+ drawCallsPerIteration: number
94
+ renderablesPerIteration: number
95
+ }
96
+
97
+ const SUITES = {
98
+ quick: { iterations: 300, warmupIterations: 40 },
99
+ default: { iterations: 1800, warmupIterations: 120 },
100
+ long: { iterations: 5000, warmupIterations: 250 },
101
+ } as const
102
+
103
+ const COLORS = {
104
+ transparent: RGBA.fromInts(0, 0, 0, 0),
105
+ panel: RGBA.fromInts(28, 32, 38),
106
+ element: RGBA.fromInts(40, 46, 56),
107
+ accent: RGBA.fromInts(84, 171, 224),
108
+ success: RGBA.fromInts(143, 188, 143),
109
+ warning: RGBA.fromInts(219, 186, 96),
110
+ selectionBg: RGBA.fromInts(73, 84, 107),
111
+ selectionFg: RGBA.fromInts(236, 239, 244),
112
+ } as const
113
+
114
+ const program = new Command()
115
+ program
116
+ .name("text-buffer-render-benchmark")
117
+ .description("Benchmark bufferDrawTextBufferView and related render paths")
118
+ .option("-s, --suite <name>", "benchmark suite: quick, default, long", "default")
119
+ .option("-i, --iterations <count>", "iterations per scenario")
120
+ .option("--warmup-iterations <count>", "warmup iterations per scenario")
121
+ .option("--width <count>", "test renderer width", "140")
122
+ .option("--height <count>", "test renderer height", "44")
123
+ .option("--scenario <name>", "run only one scenario")
124
+ .option("--list-scenarios", "list scenario names and exit")
125
+ .option("--json [path]", "write benchmark results to JSON")
126
+ .option("--no-output", "suppress stdout output")
127
+ .parse(process.argv)
128
+
129
+ const options = program.opts()
130
+ const suiteName = String(options.suite)
131
+ const suiteDefaults = SUITES[suiteName as keyof typeof SUITES]
132
+
133
+ if (!suiteDefaults) {
134
+ console.error(`Unknown suite: ${suiteName}. Valid suites: ${Object.keys(SUITES).join(", ")}`)
135
+ process.exit(1)
136
+ }
137
+
138
+ const iterations = Math.max(1, Math.floor(toNumber(options.iterations, suiteDefaults.iterations)))
139
+ const warmupIterations = Math.max(0, Math.floor(toNumber(options.warmupIterations, suiteDefaults.warmupIterations)))
140
+ const width = Math.max(40, Math.floor(toNumber(options.width, 140)))
141
+ const height = Math.max(20, Math.floor(toNumber(options.height, 44)))
142
+ const scenarioFilter = options.scenario ? String(options.scenario) : null
143
+ const outputEnabled = options.output !== false
144
+
145
+ const jsonArg = options.json
146
+ const jsonPath =
147
+ typeof jsonArg === "string"
148
+ ? path.resolve(process.cwd(), jsonArg)
149
+ : jsonArg
150
+ ? path.resolve(process.cwd(), "latest-text-buffer-render-bench-run.json")
151
+ : null
152
+
153
+ if (jsonPath) {
154
+ const dir = path.dirname(jsonPath)
155
+ if (!existsSync(dir)) {
156
+ await mkdir(dir, { recursive: true })
157
+ }
158
+
159
+ if (existsSync(jsonPath)) {
160
+ console.error(`Error: output file already exists: ${jsonPath}`)
161
+ process.exit(1)
162
+ }
163
+ }
164
+
165
+ const scenarios = createScenarios()
166
+
167
+ if (options.listScenarios) {
168
+ for (const scenario of scenarios) {
169
+ console.log(`${scenario.name}`)
170
+ }
171
+ process.exit(0)
172
+ }
173
+
174
+ const selectedScenarios = scenarioFilter ? scenarios.filter((scenario) => scenario.name === scenarioFilter) : scenarios
175
+ if (selectedScenarios.length === 0) {
176
+ console.error(`Unknown scenario: ${scenarioFilter}`)
177
+ process.exit(1)
178
+ }
179
+
180
+ if (outputEnabled) {
181
+ console.log(`text buffer render benchmark (${suiteName})`)
182
+ console.log(`- renderer: ${width}x${height}`)
183
+ console.log(`- scenarios: ${selectedScenarios.length}`)
184
+ console.log(`- iterations: ${iterations} (+${warmupIterations} warmup)`)
185
+ }
186
+
187
+ const { renderer, renderOnce } = await createTestRenderer({
188
+ width,
189
+ height,
190
+ targetFps: 60,
191
+ maxFps: 60,
192
+ screenMode: "main-screen",
193
+ externalOutputMode: "passthrough",
194
+ consoleMode: "disabled",
195
+ useMouse: false,
196
+ })
197
+
198
+ const ctx: BenchmarkContext = { renderer, renderOnce, width, height }
199
+ const results: ScenarioResult[] = []
200
+
201
+ try {
202
+ for (const scenario of selectedScenarios) {
203
+ writeLine(outputEnabled, `Running ${scenario.name}...`)
204
+ const result = await runScenario(scenario, ctx, iterations, warmupIterations)
205
+ results.push(result)
206
+ writeLine(outputEnabled, ` avg=${result.avgMs.toFixed(4)}ms p95=${result.p95Ms.toFixed(4)}ms`)
207
+ }
208
+ } finally {
209
+ renderer.destroy()
210
+ }
211
+
212
+ if (outputEnabled) {
213
+ console.table(
214
+ results.map((result) => ({
215
+ scenario: result.name,
216
+ kind: result.kind,
217
+ drawCalls: result.drawCallsPerIteration,
218
+ renderables: result.renderablesPerIteration,
219
+ avgMs: result.avgMs,
220
+ p95Ms: result.p95Ms,
221
+ usPerDraw: result.avgUsPerDrawCall,
222
+ })),
223
+ )
224
+ }
225
+
226
+ if (jsonPath) {
227
+ await Bun.write(
228
+ jsonPath,
229
+ JSON.stringify(
230
+ {
231
+ metadata: {
232
+ suite: suiteName,
233
+ width,
234
+ height,
235
+ iterations,
236
+ warmupIterations,
237
+ scenarioFilter,
238
+ timestamp: new Date().toISOString(),
239
+ },
240
+ scenarios: results,
241
+ },
242
+ null,
243
+ 2,
244
+ ),
245
+ )
246
+ writeLine(outputEnabled, `Wrote benchmark JSON: ${jsonPath}`)
247
+ }
248
+
249
+ function createScenarios(): ScenarioDefinition[] {
250
+ return [
251
+ {
252
+ name: "direct_plain_wrapped_view",
253
+ description: "Direct TextBufferView draw with plain wrapped transcript",
254
+ setup: (ctx) => {
255
+ clearRoot(ctx.renderer)
256
+ resetBuffers(ctx.renderer)
257
+
258
+ const buffer = ctx.renderer.currentRenderBuffer
259
+ const textBuffer = TextBuffer.create("unicode")
260
+ textBuffer.setText(createPlainTranscript(140))
261
+
262
+ const view = TextBufferView.create(textBuffer)
263
+ view.setWrapMode("word")
264
+ view.setViewport(0, 0, ctx.width - 2, ctx.height - 2)
265
+
266
+ return {
267
+ kind: "direct-buffer",
268
+ drawCallsPerIteration: 1,
269
+ renderablesPerIteration: 0,
270
+ runIteration: () => {
271
+ buffer.drawTextBuffer(view, 1, 1)
272
+ },
273
+ teardown: () => {
274
+ view.destroy()
275
+ textBuffer.destroy()
276
+ },
277
+ }
278
+ },
279
+ },
280
+ {
281
+ name: "direct_styled_wrapped_view",
282
+ description: "Direct styled TextBufferView draw with many spans and links",
283
+ setup: (ctx) => {
284
+ clearRoot(ctx.renderer)
285
+ resetBuffers(ctx.renderer)
286
+
287
+ const buffer = ctx.renderer.currentRenderBuffer
288
+ const textBuffer = TextBuffer.create("unicode")
289
+ textBuffer.setStyledText(createStyledTranscript(120))
290
+
291
+ const view = TextBufferView.create(textBuffer)
292
+ view.setWrapMode("word")
293
+ view.setViewport(0, 0, ctx.width - 2, ctx.height - 2)
294
+
295
+ return {
296
+ kind: "direct-buffer",
297
+ drawCallsPerIteration: 1,
298
+ renderablesPerIteration: 0,
299
+ runIteration: () => {
300
+ buffer.drawTextBuffer(view, 1, 1)
301
+ },
302
+ teardown: () => {
303
+ view.destroy()
304
+ textBuffer.destroy()
305
+ },
306
+ }
307
+ },
308
+ },
309
+ {
310
+ name: "direct_selected_styled_view",
311
+ description: "Direct styled TextBufferView draw with active selection",
312
+ setup: (ctx) => {
313
+ clearRoot(ctx.renderer)
314
+ resetBuffers(ctx.renderer)
315
+
316
+ const buffer = ctx.renderer.currentRenderBuffer
317
+ const textBuffer = TextBuffer.create("unicode")
318
+ const styled = createStyledTranscript(120)
319
+ textBuffer.setStyledText(styled)
320
+
321
+ const view = TextBufferView.create(textBuffer)
322
+ view.setWrapMode("word")
323
+ view.setViewport(0, 0, ctx.width - 2, ctx.height - 2)
324
+ view.setSelection(80, 520, COLORS.selectionBg, COLORS.selectionFg)
325
+
326
+ return {
327
+ kind: "direct-buffer",
328
+ drawCallsPerIteration: 1,
329
+ renderablesPerIteration: 0,
330
+ runIteration: () => {
331
+ buffer.drawTextBuffer(view, 1, 1)
332
+ },
333
+ teardown: () => {
334
+ view.destroy()
335
+ textBuffer.destroy()
336
+ },
337
+ }
338
+ },
339
+ },
340
+ {
341
+ name: "direct_editor_view_selection",
342
+ description: "Direct EditorView draw with wrapped content and selection",
343
+ setup: (ctx) => {
344
+ clearRoot(ctx.renderer)
345
+ resetBuffers(ctx.renderer)
346
+
347
+ const buffer = ctx.renderer.currentRenderBuffer
348
+ const editBuffer = EditBuffer.create("unicode")
349
+ editBuffer.setText(createEditorDocument(220))
350
+ const view = EditorView.create(editBuffer, ctx.width - 2, ctx.height - 2)
351
+ view.setWrapMode("word")
352
+ view.setViewport(0, 8, ctx.width - 2, ctx.height - 2, false)
353
+ view.setSelection(120, 720, COLORS.selectionBg, COLORS.selectionFg)
354
+
355
+ return {
356
+ kind: "direct-buffer",
357
+ drawCallsPerIteration: 1,
358
+ renderablesPerIteration: 0,
359
+ runIteration: () => {
360
+ buffer.drawEditorView(view, 1, 1)
361
+ },
362
+ teardown: () => {
363
+ view.destroy()
364
+ editBuffer.destroy()
365
+ },
366
+ }
367
+ },
368
+ },
369
+ {
370
+ name: "render_text_feed_wrapped",
371
+ description: "Render-tree feed of wrapped TextRenderables in layout wrappers",
372
+ setup: async (ctx) => {
373
+ const state = await buildTextFeedTree(ctx)
374
+ return {
375
+ kind: "render-tree",
376
+ drawCallsPerIteration: state.textNodes.length,
377
+ renderablesPerIteration: state.renderablesPerIteration,
378
+ runIteration: async () => {
379
+ await ctx.renderOnce()
380
+ },
381
+ teardown: () => {
382
+ state.root.destroyRecursively()
383
+ },
384
+ }
385
+ },
386
+ },
387
+ {
388
+ name: "render_text_nodes_idle",
389
+ description: "Render-tree TextRenderables backed by text nodes with no per-frame changes",
390
+ setup: async (ctx) => {
391
+ const state = await buildTextNodeFeedTree(ctx)
392
+ return {
393
+ kind: "render-tree",
394
+ drawCallsPerIteration: state.drawCallsPerIteration,
395
+ renderablesPerIteration: state.renderablesPerIteration,
396
+ runIteration: async () => {
397
+ await ctx.renderOnce()
398
+ },
399
+ teardown: () => {
400
+ state.root.destroyRecursively()
401
+ },
402
+ }
403
+ },
404
+ },
405
+ {
406
+ name: "render_text_nodes_idle_dense",
407
+ description: "Dense tree of idle text-node-backed TextRenderables",
408
+ setup: async (ctx) => {
409
+ const state = await buildDenseTextNodeTree(ctx)
410
+ return {
411
+ kind: "render-tree",
412
+ drawCallsPerIteration: state.drawCallsPerIteration,
413
+ renderablesPerIteration: state.renderablesPerIteration,
414
+ runIteration: async () => {
415
+ await ctx.renderOnce()
416
+ },
417
+ teardown: () => {
418
+ state.root.destroyRecursively()
419
+ },
420
+ }
421
+ },
422
+ },
423
+ {
424
+ name: "render_text_nodes_mutating",
425
+ description: "Render-tree TextRenderables backed by text nodes with per-frame text updates",
426
+ setup: async (ctx) => {
427
+ const state = await buildTextNodeFeedTree(ctx)
428
+ return {
429
+ kind: "render-tree",
430
+ drawCallsPerIteration: state.drawCallsPerIteration,
431
+ renderablesPerIteration: state.renderablesPerIteration,
432
+ runIteration: async (iteration) => {
433
+ const leaf = state.mutableLeaves[iteration % state.mutableLeaves.length]
434
+ leaf.replace(`status-${iteration % 10}`.padEnd(12, " "), 0)
435
+ await ctx.renderOnce()
436
+ },
437
+ teardown: () => {
438
+ state.root.destroyRecursively()
439
+ },
440
+ }
441
+ },
442
+ },
443
+ {
444
+ name: "render_textarea_editor",
445
+ description: "Render-tree Textarea editor view with selection and wrapping",
446
+ setup: async (ctx) => {
447
+ clearRoot(ctx.renderer)
448
+ resetBuffers(ctx.renderer)
449
+
450
+ const root = new BoxRenderable(ctx.renderer, {
451
+ id: "bench-textarea-root",
452
+ width: "100%",
453
+ height: "100%",
454
+ border: false,
455
+ backgroundColor: COLORS.transparent,
456
+ paddingLeft: 1,
457
+ paddingRight: 1,
458
+ paddingTop: 1,
459
+ })
460
+ ctx.renderer.root.add(root)
461
+
462
+ const textarea = new TextareaRenderable(ctx.renderer, {
463
+ id: "bench-textarea",
464
+ width: "100%",
465
+ height: Math.max(10, ctx.height - 2),
466
+ wrapMode: "word",
467
+ initialValue: createEditorDocument(220),
468
+ selectionBg: COLORS.selectionBg,
469
+ selectionFg: COLORS.selectionFg,
470
+ showCursor: false,
471
+ backgroundColor: COLORS.transparent,
472
+ textColor: COLORS.selectionFg,
473
+ })
474
+ textarea.editorView.setSelection(120, 720, COLORS.selectionBg, COLORS.selectionFg)
475
+ root.add(textarea)
476
+
477
+ await ctx.renderOnce()
478
+
479
+ return {
480
+ kind: "render-tree",
481
+ drawCallsPerIteration: 1,
482
+ renderablesPerIteration: 2,
483
+ runIteration: async () => {
484
+ await ctx.renderOnce()
485
+ },
486
+ teardown: () => {
487
+ root.destroyRecursively()
488
+ },
489
+ }
490
+ },
491
+ },
492
+ ]
493
+ }
494
+
495
+ async function buildTextNodeFeedTree(ctx: BenchmarkContext): Promise<TextNodeFeedTreeState> {
496
+ clearRoot(ctx.renderer)
497
+ resetBuffers(ctx.renderer)
498
+
499
+ let renderablesPerIteration = 0
500
+ const texts: TextRenderable[] = []
501
+ const mutableLeaves: TextNodeRenderable[] = []
502
+
503
+ const root = new BoxRenderable(ctx.renderer, {
504
+ id: "bench-text-node-root",
505
+ width: "100%",
506
+ height: "100%",
507
+ border: false,
508
+ backgroundColor: COLORS.transparent,
509
+ flexDirection: "column",
510
+ paddingLeft: 1,
511
+ paddingRight: 1,
512
+ paddingTop: 1,
513
+ gap: 1,
514
+ })
515
+ renderablesPerIteration += 1
516
+ ctx.renderer.root.add(root)
517
+
518
+ const messageCount = Math.max(12, Math.floor((ctx.height - 2) / 2))
519
+ for (let i = 0; i < messageCount; i += 1) {
520
+ const row = new BoxRenderable(ctx.renderer, {
521
+ id: `bench-text-node-row-${i}`,
522
+ width: "100%",
523
+ border: false,
524
+ backgroundColor: COLORS.transparent,
525
+ flexDirection: "row",
526
+ gap: 1,
527
+ })
528
+ renderablesPerIteration += 1
529
+
530
+ const rail = new BoxRenderable(ctx.renderer, {
531
+ id: `bench-text-node-rail-${i}`,
532
+ width: 2,
533
+ minWidth: 2,
534
+ maxWidth: 2,
535
+ border: false,
536
+ backgroundColor: COLORS.transparent,
537
+ })
538
+ renderablesPerIteration += 1
539
+
540
+ const text = new TextRenderable(ctx.renderer, {
541
+ id: `bench-text-node-${i}`,
542
+ width: "100%",
543
+ wrapMode: "word",
544
+ bg: COLORS.transparent,
545
+ })
546
+ renderablesPerIteration += 1
547
+ texts.push(text)
548
+
549
+ const prefix = new TextNodeRenderable({ fg: COLORS.warning, attributes: 0 })
550
+ prefix.add(`[${String(i).padStart(2, "0")}] `)
551
+
552
+ const body = new TextNodeRenderable({ fg: COLORS.selectionFg, attributes: 0 })
553
+ body.add("The renderer should update this text node tree only when it changes, not on every frame. ")
554
+
555
+ const linkNode = new TextNodeRenderable({
556
+ fg: COLORS.accent,
557
+ link: { url: `https://example.test/${i}` },
558
+ attributes: 0,
559
+ })
560
+ linkNode.add("documentation")
561
+
562
+ const mutable = new TextNodeRenderable({ fg: COLORS.success, attributes: 0 })
563
+ mutable.add(`status-${i % 10}`.padEnd(12, " "))
564
+ mutableLeaves.push(mutable)
565
+
566
+ body.add(linkNode)
567
+ body.add(" current state=")
568
+ body.add(mutable)
569
+ body.add(" and wrapped notes about viewport offsets and selection handling.")
570
+
571
+ text.add(prefix)
572
+ text.add(body)
573
+
574
+ row.add(rail)
575
+ row.add(text)
576
+ root.add(row)
577
+ }
578
+
579
+ await ctx.renderOnce()
580
+
581
+ return {
582
+ root,
583
+ texts,
584
+ mutableLeaves,
585
+ drawCallsPerIteration: texts.length,
586
+ renderablesPerIteration,
587
+ }
588
+ }
589
+
590
+ async function buildDenseTextNodeTree(ctx: BenchmarkContext): Promise<TextNodeFeedTreeState> {
591
+ clearRoot(ctx.renderer)
592
+ resetBuffers(ctx.renderer)
593
+
594
+ let renderablesPerIteration = 0
595
+ const texts: TextRenderable[] = []
596
+ const mutableLeaves: TextNodeRenderable[] = []
597
+
598
+ const root = new BoxRenderable(ctx.renderer, {
599
+ id: "bench-dense-text-node-root",
600
+ width: "100%",
601
+ height: "100%",
602
+ border: false,
603
+ backgroundColor: COLORS.transparent,
604
+ flexDirection: "column",
605
+ })
606
+ renderablesPerIteration += 1
607
+ ctx.renderer.root.add(root)
608
+
609
+ const textCount = Math.max(140, ctx.height * 6)
610
+ for (let i = 0; i < textCount; i += 1) {
611
+ const text = new TextRenderable(ctx.renderer, {
612
+ id: `bench-dense-text-${i}`,
613
+ width: "100%",
614
+ wrapMode: "word",
615
+ bg: COLORS.transparent,
616
+ })
617
+ renderablesPerIteration += 1
618
+ texts.push(text)
619
+
620
+ const prefix = new TextNodeRenderable({ fg: COLORS.warning, attributes: 0 })
621
+ prefix.add(`${String(i).padStart(3, "0")}: `)
622
+
623
+ const body = new TextNodeRenderable({ fg: COLORS.selectionFg, attributes: 0 })
624
+ body.add("text-node lifecycle bookkeeping should stay idle when nothing changes ")
625
+
626
+ const mutable = new TextNodeRenderable({ fg: COLORS.success, attributes: 0 })
627
+ mutable.add(`idle-${i % 10}`)
628
+ mutableLeaves.push(mutable)
629
+
630
+ body.add(mutable)
631
+ text.add(prefix)
632
+ text.add(body)
633
+ root.add(text)
634
+ }
635
+
636
+ await ctx.renderOnce()
637
+
638
+ return {
639
+ root,
640
+ texts,
641
+ mutableLeaves,
642
+ drawCallsPerIteration: texts.length,
643
+ renderablesPerIteration,
644
+ }
645
+ }
646
+
647
+ async function buildTextFeedTree(ctx: BenchmarkContext): Promise<TextFeedTreeState> {
648
+ clearRoot(ctx.renderer)
649
+ resetBuffers(ctx.renderer)
650
+
651
+ let renderablesPerIteration = 0
652
+ const textNodes: TextRenderable[] = []
653
+
654
+ const root = new BoxRenderable(ctx.renderer, {
655
+ id: "bench-text-feed-root",
656
+ width: "100%",
657
+ height: "100%",
658
+ border: false,
659
+ backgroundColor: COLORS.transparent,
660
+ flexDirection: "column",
661
+ paddingLeft: 1,
662
+ paddingRight: 1,
663
+ paddingTop: 1,
664
+ gap: 1,
665
+ })
666
+ renderablesPerIteration += 1
667
+ ctx.renderer.root.add(root)
668
+
669
+ const messageCount = Math.max(12, Math.floor((ctx.height - 2) / 2))
670
+ for (let i = 0; i < messageCount; i += 1) {
671
+ const row = new BoxRenderable(ctx.renderer, {
672
+ id: `bench-text-row-${i}`,
673
+ width: "100%",
674
+ border: false,
675
+ backgroundColor: COLORS.transparent,
676
+ flexDirection: "row",
677
+ gap: 1,
678
+ })
679
+ renderablesPerIteration += 1
680
+
681
+ const rail = new BoxRenderable(ctx.renderer, {
682
+ id: `bench-text-rail-${i}`,
683
+ width: 2,
684
+ minWidth: 2,
685
+ maxWidth: 2,
686
+ border: false,
687
+ backgroundColor: COLORS.transparent,
688
+ })
689
+ renderablesPerIteration += 1
690
+
691
+ const text = new TextRenderable(ctx.renderer, {
692
+ id: `bench-text-${i}`,
693
+ width: "100%",
694
+ wrapMode: "word",
695
+ content: i % 3 === 0 ? createStyledTranscript(4) : createStyledTranscript(3),
696
+ bg: COLORS.transparent,
697
+ })
698
+ renderablesPerIteration += 1
699
+ textNodes.push(text)
700
+
701
+ row.add(rail)
702
+ row.add(text)
703
+ root.add(row)
704
+ }
705
+
706
+ await ctx.renderOnce()
707
+
708
+ return {
709
+ root,
710
+ textNodes,
711
+ drawCallsPerIteration: textNodes.length,
712
+ renderablesPerIteration,
713
+ }
714
+ }
715
+
716
+ async function runScenario(
717
+ scenario: ScenarioDefinition,
718
+ ctx: BenchmarkContext,
719
+ iterations: number,
720
+ warmupIterations: number,
721
+ ): Promise<ScenarioResult> {
722
+ const runtime = await scenario.setup(ctx)
723
+
724
+ try {
725
+ for (let i = 0; i < warmupIterations; i += 1) {
726
+ await runtime.runIteration(i)
727
+ }
728
+
729
+ const samples = new Array<number>(iterations)
730
+ const elapsedStart = performance.now()
731
+
732
+ for (let i = 0; i < iterations; i += 1) {
733
+ const start = performance.now()
734
+ await runtime.runIteration(i)
735
+ samples[i] = performance.now() - start
736
+ }
737
+
738
+ const elapsedMs = performance.now() - elapsedStart
739
+ const stats = calculateStats(samples)
740
+
741
+ return {
742
+ name: scenario.name,
743
+ description: scenario.description,
744
+ kind: runtime.kind,
745
+ iterations,
746
+ warmupIterations,
747
+ elapsedMs: round(elapsedMs, 4),
748
+ drawCallsPerIteration: runtime.drawCallsPerIteration,
749
+ renderablesPerIteration: runtime.renderablesPerIteration,
750
+ avgMs: round(stats.avgMs, 4),
751
+ medianMs: round(stats.medianMs, 4),
752
+ p95Ms: round(stats.p95Ms, 4),
753
+ minMs: round(stats.minMs, 4),
754
+ maxMs: round(stats.maxMs, 4),
755
+ stdDevMs: round(stats.stdDevMs, 4),
756
+ avgUsPerDrawCall:
757
+ runtime.drawCallsPerIteration > 0 ? round((stats.avgMs * 1000) / runtime.drawCallsPerIteration, 3) : 0,
758
+ }
759
+ } finally {
760
+ await runtime.teardown?.()
761
+ clearRoot(ctx.renderer)
762
+ resetBuffers(ctx.renderer)
763
+ }
764
+ }
765
+
766
+ function createPlainTranscript(lines: number): string {
767
+ const parts: string[] = []
768
+ for (let i = 0; i < lines; i += 1) {
769
+ parts.push(
770
+ `[${String(i).padStart(3, "0")}] The renderer keeps a wrapped transcript of tool output, inline notes, and status lines with occasional\ttabs and emoji like 📦.`,
771
+ )
772
+ }
773
+ return parts.join("\n")
774
+ }
775
+
776
+ function createStyledTranscript(lines: number): StyledText {
777
+ const chunks = []
778
+
779
+ const plain = (text: string) => ({ __isChunk: true as const, text, attributes: 0 })
780
+
781
+ for (let i = 0; i < lines; i += 1) {
782
+ chunks.push(dim(`[12:${String(i % 60).padStart(2, "0")}] `))
783
+ chunks.push(bold(fg("#88c0d0")(`agent-${i % 4}`)))
784
+ chunks.push(plain(" opened "))
785
+ chunks.push(underline(link(`https://example.test/item/${i}`)(fg("#81a1c1")("documentation"))))
786
+ chunks.push(plain(" and returned "))
787
+ chunks.push(fg(i % 2 === 0 ? "#a3be8c" : "#ebcb8b")("status=ok"))
788
+ chunks.push(plain(" with inline code "))
789
+ chunks.push(bold(fg("#d08770")("buffer.drawTextBuffer(view, x, y)")))
790
+ chunks.push(plain(" plus wrapped notes about viewport offsets, spans, tabs\tand Unicode like café.\n"))
791
+ }
792
+
793
+ return new StyledText(chunks)
794
+ }
795
+
796
+ function createEditorDocument(lines: number): string {
797
+ const out: string[] = []
798
+ for (let i = 0; i < lines; i += 1) {
799
+ out.push(
800
+ `function render_line_${i}(view, buffer) { const status = "line-${i}"; return status + " => " + view.getVirtualLineCount(); }`,
801
+ )
802
+ }
803
+ return out.join("\n")
804
+ }
805
+
806
+ function clearRoot(renderer: TestRenderer): void {
807
+ for (const child of renderer.root.getChildren()) {
808
+ child.destroyRecursively()
809
+ }
810
+ }
811
+
812
+ function resetBuffers(renderer: TestRenderer): void {
813
+ const buffers: OptimizedBuffer[] = [renderer.currentRenderBuffer, renderer.nextRenderBuffer]
814
+ for (const buffer of buffers) {
815
+ buffer.clearScissorRects()
816
+ buffer.clearOpacity()
817
+ buffer.clear(COLORS.transparent)
818
+ }
819
+ }
820
+
821
+ function calculateStats(samples: number[]): TimingStats {
822
+ if (samples.length === 0) {
823
+ return {
824
+ avgMs: 0,
825
+ medianMs: 0,
826
+ p95Ms: 0,
827
+ minMs: 0,
828
+ maxMs: 0,
829
+ stdDevMs: 0,
830
+ }
831
+ }
832
+
833
+ const sorted = [...samples].sort((a, b) => a - b)
834
+ const total = samples.reduce((sum, value) => sum + value, 0)
835
+ const avgMs = total / samples.length
836
+ const mid = Math.floor(sorted.length / 2)
837
+ const medianMs = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]
838
+ const p95Index = Math.floor((sorted.length - 1) * 0.95)
839
+ const p95Ms = sorted[p95Index]
840
+ const minMs = sorted[0]
841
+ const maxMs = sorted[sorted.length - 1]
842
+
843
+ let variance = 0
844
+ for (const value of samples) {
845
+ const diff = value - avgMs
846
+ variance += diff * diff
847
+ }
848
+ variance /= samples.length
849
+
850
+ return {
851
+ avgMs,
852
+ medianMs,
853
+ p95Ms,
854
+ minMs,
855
+ maxMs,
856
+ stdDevMs: Math.sqrt(variance),
857
+ }
858
+ }
859
+
860
+ function round(value: number, places: number): number {
861
+ return Number(value.toFixed(places))
862
+ }
863
+
864
+ function toNumber(value: unknown, fallback: number): number {
865
+ if (value === undefined || value === null || value === "") return fallback
866
+ const parsed = Number(value)
867
+ return Number.isFinite(parsed) ? parsed : fallback
868
+ }
869
+
870
+ function writeLine(enabled: boolean, line: string): void {
871
+ if (enabled) {
872
+ console.log(line)
873
+ }
874
+ }