@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,303 @@
1
+ import { afterEach, expect, test } from "bun:test"
2
+ import { Audio } from "../audio.js"
3
+ import { resolveRenderLib } from "../zig.js"
4
+
5
+ const SAMPLE_RATE = 48_000
6
+
7
+ function buildPcm16Wav(samples: number[], channels: number): Uint8Array {
8
+ if (channels <= 0 || samples.length % channels !== 0) {
9
+ throw new Error(`Invalid PCM payload for channel count ${channels}`)
10
+ }
11
+
12
+ const bitsPerSample = 16
13
+ const bytesPerSample = bitsPerSample / 8
14
+ const frameCount = samples.length / channels
15
+ const dataSize = frameCount * channels * bytesPerSample
16
+ const byteRate = SAMPLE_RATE * channels * bytesPerSample
17
+ const blockAlign = channels * bytesPerSample
18
+ const totalSize = 44 + dataSize
19
+ const out = new Uint8Array(totalSize)
20
+ const view = new DataView(out.buffer)
21
+
22
+ out.set([0x52, 0x49, 0x46, 0x46], 0) // RIFF
23
+ view.setUint32(4, totalSize - 8, true)
24
+ out.set([0x57, 0x41, 0x56, 0x45], 8) // WAVE
25
+ out.set([0x66, 0x6d, 0x74, 0x20], 12) // fmt
26
+ view.setUint32(16, 16, true)
27
+ view.setUint16(20, 1, true) // PCM
28
+ view.setUint16(22, channels, true)
29
+ view.setUint32(24, SAMPLE_RATE, true)
30
+ view.setUint32(28, byteRate, true)
31
+ view.setUint16(32, blockAlign, true)
32
+ view.setUint16(34, bitsPerSample, true)
33
+ out.set([0x64, 0x61, 0x74, 0x61], 36) // data
34
+ view.setUint32(40, dataSize, true)
35
+
36
+ for (let i = 0; i < samples.length; i += 1) {
37
+ const clamped = Math.max(-1, Math.min(1, samples[i]))
38
+ view.setInt16(44 + i * 2, Math.round(clamped * 32767), true)
39
+ }
40
+
41
+ return out
42
+ }
43
+
44
+ function buildMonoPcm16Wav(samples: number[]): Uint8Array {
45
+ return buildPcm16Wav(samples, 1)
46
+ }
47
+
48
+ const instances: Audio[] = []
49
+
50
+ afterEach(() => {
51
+ for (const instance of instances.splice(0)) {
52
+ instance.dispose()
53
+ }
54
+ })
55
+
56
+ test("Audio loads wav and mixes frames", () => {
57
+ const audio = Audio.create({ autoStart: false })
58
+ instances.push(audio)
59
+
60
+ const wav = buildMonoPcm16Wav([0, 0.25, -0.25, 0.5, -0.5, 0])
61
+ const sound = audio.loadSound(wav)
62
+ const sfx = audio.group("sfx")
63
+
64
+ expect(sound).not.toBeNull()
65
+ expect(sfx).not.toBeNull()
66
+ if (sound == null || sfx == null) return
67
+
68
+ expect(audio.startMixer()).toBe(true)
69
+ const voice = audio.play(sound, { groupId: sfx, volume: 1, pan: 0, loop: false })
70
+ expect(voice).not.toBeNull()
71
+ const mixed = audio.mixFrames(6, 2)
72
+
73
+ expect(mixed).not.toBeNull()
74
+ if (!mixed) return
75
+ expect(mixed.length).toBe(12)
76
+ expect(mixed.some((sample) => Math.abs(sample) > 0.001)).toBe(true)
77
+ expect(audio.getStats()?.soundsLoaded).toBe(1)
78
+ })
79
+
80
+ test("Audio does not auto-start by default", () => {
81
+ const audio = Audio.create()
82
+ instances.push(audio)
83
+
84
+ expect(audio.isStarted()).toBe(false)
85
+ expect(audio.isMixerStarted()).toBe(false)
86
+ })
87
+
88
+ test("Audio start reports playback availability only", () => {
89
+ const audio = Audio.create({ autoStart: false })
90
+ audio.on("error", () => {})
91
+ instances.push(audio)
92
+
93
+ expect(audio.isStarted()).toBe(false)
94
+ expect(audio.isMixerStarted()).toBe(false)
95
+
96
+ const started = audio.start()
97
+ expect(audio.isStarted()).toBe(started)
98
+ expect(audio.isMixerStarted()).toBe(started)
99
+ })
100
+
101
+ test("Audio startMixer enables headless mixing without playback", () => {
102
+ const audio = Audio.create({ autoStart: false })
103
+ instances.push(audio)
104
+
105
+ const wav = buildMonoPcm16Wav([0, 0.25, -0.25, 0.5, -0.5, 0])
106
+ const sound = audio.loadSound(wav)
107
+ expect(sound).not.toBeNull()
108
+ if (sound == null) return
109
+
110
+ expect(audio.startMixer()).toBe(true)
111
+ expect(audio.isStarted()).toBe(false)
112
+ expect(audio.isMixerStarted()).toBe(true)
113
+
114
+ const voice = audio.play(sound, { volume: 1, loop: true })
115
+ expect(voice).not.toBeNull()
116
+
117
+ const mixed = audio.mixFrames(6, 2)
118
+ expect(mixed).not.toBeNull()
119
+ expect(mixed?.some((sample) => Math.abs(sample) > 0.001)).toBe(true)
120
+
121
+ expect(audio.stop()).toBe(true)
122
+ expect(audio.isStarted()).toBe(false)
123
+ expect(audio.isMixerStarted()).toBe(false)
124
+ })
125
+
126
+ test("Audio unloads sounds and invalidates old handles", () => {
127
+ const audio = Audio.create({ autoStart: false })
128
+ audio.on("error", () => {})
129
+ instances.push(audio)
130
+
131
+ const first = audio.loadSound(buildMonoPcm16Wav([0, 0.25, -0.25, 0.5, -0.5, 0]))
132
+ expect(first).not.toBeNull()
133
+ if (first == null) return
134
+
135
+ expect(audio.startMixer()).toBe(true)
136
+ const firstVoice = audio.play(first, { volume: 1, pan: 0, loop: true })
137
+ expect(firstVoice).not.toBeNull()
138
+ expect(audio.getStats()?.voicesActive).toBeGreaterThan(0)
139
+
140
+ expect(audio.unloadSound(first)).toBe(true)
141
+ expect(audio.getStats()?.soundsLoaded).toBe(0)
142
+ expect(audio.getStats()?.voicesActive).toBe(0)
143
+ expect(audio.play(first, { volume: 1 })).toBeNull()
144
+ expect(audio.unloadSound(first)).toBe(false)
145
+
146
+ const second = audio.loadSound(buildMonoPcm16Wav([0.6, -0.2, 0.4, -0.4, 0.3, -0.1]))
147
+ expect(second).not.toBeNull()
148
+ if (second == null) return
149
+ expect(second).not.toBe(first)
150
+
151
+ const secondVoice = audio.play(second, { volume: 1, pan: 0, loop: false })
152
+ expect(secondVoice).not.toBeNull()
153
+ })
154
+
155
+ test("Audio mixes into mono and multichannel output buffers", () => {
156
+ const audio = Audio.create({ autoStart: false })
157
+ instances.push(audio)
158
+
159
+ const wav = buildMonoPcm16Wav([0.6, -0.2, 0.4, -0.4, 0.3, -0.1])
160
+ const sound = audio.loadSound(wav)
161
+ expect(sound).not.toBeNull()
162
+ if (sound == null) return
163
+
164
+ expect(audio.startMixer()).toBe(true)
165
+ const voice = audio.play(sound, { volume: 1, pan: 0, loop: true })
166
+ expect(voice).not.toBeNull()
167
+
168
+ const mono = audio.mixFrames(6, 1)
169
+ expect(mono).not.toBeNull()
170
+ if (!mono) return
171
+ expect(mono.length).toBe(6)
172
+ expect(mono.some((sample) => Math.abs(sample) > 0.001)).toBe(true)
173
+
174
+ const quad = audio.mixFrames(6, 4)
175
+ expect(quad).not.toBeNull()
176
+ if (!quad) return
177
+ expect(quad.length).toBe(24)
178
+ expect(quad.some((sample, index) => index % 4 < 2 && Math.abs(sample) > 0.001)).toBe(true)
179
+ for (let frame = 0; frame < 6; frame += 1) {
180
+ expect(quad[frame * 4 + 2]).toBe(0)
181
+ expect(quad[frame * 4 + 3]).toBe(0)
182
+ }
183
+ })
184
+
185
+ test("Audio updates mix stats", () => {
186
+ const audio = Audio.create({ autoStart: false })
187
+ instances.push(audio)
188
+
189
+ const wave = Array.from({ length: 2048 }, (_, index) => Math.sin((Math.PI * 2 * index) / 32) * 0.8)
190
+ const wav = buildMonoPcm16Wav(wave)
191
+ const sound = audio.loadSound(wav)
192
+ expect(sound).not.toBeNull()
193
+ if (sound == null) return
194
+
195
+ expect(audio.startMixer()).toBe(true)
196
+ const voice = audio.play(sound, { volume: 1, pan: 0, loop: true })
197
+ expect(voice).not.toBeNull()
198
+
199
+ const initialStats = audio.getStats()
200
+ expect(initialStats).not.toBeNull()
201
+ const initialFrames = initialStats?.framesMixed ?? 0n
202
+
203
+ const mixed = audio.mixFrames(512, 2)
204
+ expect(mixed).not.toBeNull()
205
+
206
+ const finalStats = audio.getStats()
207
+ expect(finalStats).not.toBeNull()
208
+ expect(finalStats?.framesMixed ?? 0n).toBeGreaterThan(initialFrames)
209
+ expect(finalStats?.voicesActive ?? 0).toBeGreaterThan(0)
210
+ })
211
+
212
+ test("Audio tap mirrors mixed frames without consuming stream", () => {
213
+ const audio = Audio.create({ autoStart: false })
214
+ instances.push(audio)
215
+
216
+ const wav = buildMonoPcm16Wav([0, 0.5, -0.5, 0.25, -0.25, 0])
217
+ const sound = audio.loadSound(wav)
218
+ expect(sound).not.toBeNull()
219
+ if (sound == null) return
220
+
221
+ expect(audio.startMixer()).toBe(true)
222
+ expect(audio.enableTap(2048)).toBe(true)
223
+ const voice = audio.play(sound, { volume: 1, pan: 0, loop: true })
224
+ expect(voice).not.toBeNull()
225
+
226
+ const mixed = audio.mixFrames(256, 2)
227
+ expect(mixed).not.toBeNull()
228
+
229
+ const tap = audio.readTapFrames(128, 2)
230
+ expect(tap).not.toBeNull()
231
+ if (!tap) return
232
+
233
+ expect(tap.framesRead).toBeGreaterThan(0)
234
+ expect(tap.frames.length).toBe(256)
235
+ expect(tap.frames.some((sample) => Math.abs(sample) > 0.001)).toBe(true)
236
+
237
+ expect(audio.disableTap()).toBe(true)
238
+ })
239
+
240
+ test("Audio supports immutable custom sample rate", () => {
241
+ const audio = Audio.create({ autoStart: false, sampleRate: 44_100, playbackChannels: 1 })
242
+ instances.push(audio)
243
+
244
+ const wav = buildMonoPcm16Wav([0.3, -0.3, 0.2, -0.2, 0.1, -0.1])
245
+ const sound = audio.loadSound(wav)
246
+ expect(sound).not.toBeNull()
247
+ if (sound == null) return
248
+
249
+ expect(audio.startMixer()).toBe(true)
250
+ const voice = audio.play(sound, { volume: 1, pan: 0, loop: true })
251
+ expect(voice).not.toBeNull()
252
+
253
+ const mixed = audio.mixFrames(128, 2)
254
+ expect(mixed).not.toBeNull()
255
+ if (!mixed) return
256
+ expect(mixed.some((sample) => Math.abs(sample) > 0.001)).toBe(true)
257
+ })
258
+
259
+ test("audioLoad rejects oversized payload lengths before truncating to u32", () => {
260
+ const lib = resolveRenderLib()
261
+ const engine = lib.createAudioEngine()
262
+ expect(engine).not.toBeNull()
263
+ if (engine == null) return
264
+
265
+ const oversized = {
266
+ buffer: new ArrayBuffer(1),
267
+ byteOffset: 0,
268
+ byteLength: 0x1_0000_0000,
269
+ length: 0x1_0000_0000,
270
+ } as unknown as Uint8Array
271
+
272
+ try {
273
+ expect(() => lib.audioLoad(engine, oversized)).toThrow("Audio data length exceeds native u32 length limit")
274
+ } finally {
275
+ lib.destroyAudioEngine(engine)
276
+ }
277
+ })
278
+
279
+ test("audioCreateGroup rejects oversized encoded name lengths before truncating to u32", () => {
280
+ const lib = resolveRenderLib()
281
+ const engine = lib.createAudioEngine()
282
+ expect(engine).not.toBeNull()
283
+ if (engine == null) return
284
+
285
+ const originalEncode = lib.encoder.encode
286
+ const oversized = {
287
+ buffer: new ArrayBuffer(1),
288
+ byteOffset: 0,
289
+ byteLength: 0x1_0000_0000,
290
+ length: 0x1_0000_0000,
291
+ } as unknown as Uint8Array
292
+
293
+ ;(lib.encoder as { encode: (input: string) => Uint8Array }).encode = () => oversized
294
+
295
+ try {
296
+ expect(() => lib.audioCreateGroup(engine, "oversized")).toThrow(
297
+ "Audio group name length exceeds native u32 length limit",
298
+ )
299
+ } finally {
300
+ ;(lib.encoder as { encode: (input: string) => Uint8Array }).encode = originalEncode
301
+ lib.destroyAudioEngine(engine)
302
+ }
303
+ })
@@ -0,0 +1,200 @@
1
+ import { test, expect, beforeEach, afterEach, describe } from "bun:test"
2
+ import { Renderable, type RenderableOptions } from "../Renderable.js"
3
+ import { createTestRenderer, type TestRenderer } from "../testing/test-renderer.js"
4
+ import type { RenderContext } from "../types.js"
5
+ import type { OptimizedBuffer } from "../buffer.js"
6
+
7
+ class TestRenderable extends Renderable {
8
+ public renderSelfCalled = false
9
+ public customOnUpdate?: () => void
10
+
11
+ constructor(ctx: RenderContext, options: RenderableOptions) {
12
+ super(ctx, options)
13
+ }
14
+
15
+ public onUpdate(deltaTime: number): void {
16
+ if (this.customOnUpdate) {
17
+ this.customOnUpdate()
18
+ }
19
+ }
20
+
21
+ protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {
22
+ this.renderSelfCalled = true
23
+ }
24
+ }
25
+
26
+ let testRenderer: TestRenderer
27
+ let renderOnce: () => Promise<void>
28
+
29
+ beforeEach(async () => {
30
+ ;({ renderer: testRenderer, renderOnce } = await createTestRenderer({}))
31
+ })
32
+
33
+ afterEach(() => {
34
+ testRenderer.destroy()
35
+ })
36
+
37
+ describe("Destroy During Render - Actual Bugs", () => {
38
+ test("BUG: destroying self in onUpdate still calls renderSelf", async () => {
39
+ const renderable = new TestRenderable(testRenderer, {
40
+ id: "test",
41
+ width: 100,
42
+ height: 100,
43
+ })
44
+
45
+ renderable.customOnUpdate = () => {
46
+ renderable.destroy()
47
+ }
48
+
49
+ testRenderer.root.add(renderable)
50
+ await renderOnce()
51
+
52
+ expect(renderable.isDestroyed).toBe(true)
53
+ // BUG: renderSelf should NOT be called after destroy in onUpdate
54
+ expect(renderable.renderSelfCalled).toBe(false)
55
+ })
56
+
57
+ test("BUG: destroying child in parent's onUpdate, child still renders", async () => {
58
+ const parent = new TestRenderable(testRenderer, {
59
+ id: "parent",
60
+ width: 100,
61
+ height: 100,
62
+ })
63
+ const child = new TestRenderable(testRenderer, {
64
+ id: "child",
65
+ width: 50,
66
+ height: 50,
67
+ })
68
+
69
+ parent.add(child)
70
+ testRenderer.root.add(parent)
71
+
72
+ parent.customOnUpdate = () => {
73
+ child.destroy()
74
+ }
75
+
76
+ await renderOnce()
77
+
78
+ expect(child.isDestroyed).toBe(true)
79
+ // BUG: Child should not render if destroyed in parent's onUpdate
80
+ expect(child.renderSelfCalled).toBe(false)
81
+ })
82
+
83
+ test("BUG: destroying sibling in onUpdate, sibling still renders", async () => {
84
+ const parent = new TestRenderable(testRenderer, { id: "parent" })
85
+ const child1 = new TestRenderable(testRenderer, {
86
+ id: "child1",
87
+ width: 50,
88
+ height: 50,
89
+ })
90
+ const child2 = new TestRenderable(testRenderer, {
91
+ id: "child2",
92
+ width: 50,
93
+ height: 50,
94
+ })
95
+
96
+ parent.add(child1)
97
+ parent.add(child2)
98
+ testRenderer.root.add(parent)
99
+
100
+ child1.customOnUpdate = () => {
101
+ child2.destroy()
102
+ }
103
+
104
+ await renderOnce()
105
+
106
+ expect(child2.isDestroyed).toBe(true)
107
+ // BUG: child2 should not render if destroyed by sibling's onUpdate
108
+ expect(child2.renderSelfCalled).toBe(false)
109
+ })
110
+
111
+ test("BUG: destroying sibling in renderBefore, sibling (later in render list) still renders", async () => {
112
+ const parent = new TestRenderable(testRenderer, { id: "parent" })
113
+ const child2 = new TestRenderable(testRenderer, {
114
+ id: "child2",
115
+ width: 50,
116
+ height: 50,
117
+ zIndex: 2,
118
+ })
119
+
120
+ const child1 = new TestRenderable(testRenderer, {
121
+ id: "child1",
122
+ width: 50,
123
+ height: 50,
124
+ zIndex: 1,
125
+ renderBefore: function () {
126
+ child2.destroy()
127
+ },
128
+ })
129
+
130
+ parent.add(child1)
131
+ parent.add(child2)
132
+ testRenderer.root.add(parent)
133
+
134
+ await renderOnce()
135
+
136
+ expect(child2.isDestroyed).toBe(true)
137
+ // BUG: child2 should not render since it was destroyed before its turn
138
+ expect(child2.renderSelfCalled).toBe(false)
139
+ })
140
+
141
+ test("BUG: onLifecyclePass not called (registration issue)", async () => {
142
+ let lifecyclePassCalled = false
143
+
144
+ const renderable = new TestRenderable(testRenderer, { id: "test" })
145
+ renderable.onLifecyclePass = () => {
146
+ lifecyclePassCalled = true
147
+ }
148
+
149
+ testRenderer.root.add(renderable)
150
+ await renderOnce()
151
+
152
+ // BUG: Lifecycle pass should be called but isn't
153
+ expect(lifecyclePassCalled).toBe(true)
154
+ })
155
+ })
156
+
157
+ describe("Destroy During Render - Working Cases (for documentation)", () => {
158
+ test("WORKS: destroying self in renderAfter", async () => {
159
+ const renderable = new TestRenderable(testRenderer, {
160
+ id: "test",
161
+ width: 100,
162
+ height: 100,
163
+ renderAfter: function () {
164
+ this.destroy()
165
+ },
166
+ })
167
+
168
+ testRenderer.root.add(renderable)
169
+ await renderOnce()
170
+
171
+ expect(renderable.isDestroyed).toBe(true)
172
+ // renderSelf was already called by this point, which is fine
173
+ expect(renderable.renderSelfCalled).toBe(true)
174
+ })
175
+
176
+ test("WORKS: destroying child in renderAfter", async () => {
177
+ const child = new TestRenderable(testRenderer, {
178
+ id: "child",
179
+ width: 50,
180
+ height: 50,
181
+ })
182
+
183
+ const parent = new TestRenderable(testRenderer, {
184
+ id: "parent",
185
+ width: 100,
186
+ height: 100,
187
+ renderAfter: function () {
188
+ child.destroy()
189
+ },
190
+ })
191
+
192
+ parent.add(child)
193
+ testRenderer.root.add(parent)
194
+
195
+ await renderOnce()
196
+
197
+ expect(child.isDestroyed).toBe(true)
198
+ // Child already rendered before parent's renderAfter, which is expected
199
+ })
200
+ })
@@ -0,0 +1,36 @@
1
+ import { Readable } from "node:stream"
2
+ import { createTestRenderer } from "../testing.js"
3
+
4
+ const code = parseInt(process.argv[2] ?? "0", 10)
5
+ const mode = process.argv[3] ?? "idle"
6
+
7
+ const stdin = new Readable({ read() {} }) as NodeJS.ReadStream & {
8
+ setRawMode: (enabled: boolean) => NodeJS.ReadStream
9
+ }
10
+ stdin.setRawMode = (enabled) => {
11
+ if (!enabled) {
12
+ console.log("raw mode disabled")
13
+ }
14
+ return stdin
15
+ }
16
+
17
+ const { renderer } = await createTestRenderer({ width: 20, height: 10, stdin })
18
+ const lib = (renderer as any).lib
19
+ const originalSuspendRenderer = lib.suspendRenderer.bind(lib)
20
+ lib.suspendRenderer = (rendererPtr: unknown) => {
21
+ console.log("renderer suspended")
22
+ originalSuspendRenderer(rendererPtr)
23
+ }
24
+
25
+ process.on("exit", () => {
26
+ renderer.destroy()
27
+ })
28
+
29
+ if (mode === "during-render") {
30
+ renderer.setFrameCallback(async () => {
31
+ process.exit(code)
32
+ })
33
+ renderer.start()
34
+ } else {
35
+ process.exit(code)
36
+ }
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { spawnSync } from "node:child_process"
3
+ import { dirname, extname, join, resolve, sep } from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ const testFilePath = fileURLToPath(import.meta.url)
7
+ const testDir = dirname(testFilePath)
8
+ const fixturePath = join(testDir, `destroy-on-exit.fixture${extname(testFilePath)}`)
9
+ const packageRoot = testFilePath.includes(`${sep}.node-test${sep}`)
10
+ ? resolve(testDir, "..", "..", "..")
11
+ : resolve(testDir, "..", "..")
12
+ const workspaceRoot = resolve(packageRoot, "..", "..")
13
+
14
+ const runFixture = (code: number, mode: "idle" | "during-render" = "idle") => {
15
+ const result = spawnSync(process.execPath, [...getFixtureRuntimeArgs(), fixturePath, code.toString(), mode], {
16
+ cwd: packageRoot,
17
+ env: process.env,
18
+ })
19
+
20
+ const stdout = result.stdout?.toString() ?? ""
21
+
22
+ return { result, stdout }
23
+ }
24
+
25
+ function getFixtureRuntimeArgs(): string[] {
26
+ if (process.versions.bun) {
27
+ return []
28
+ }
29
+
30
+ return [
31
+ "--permission",
32
+ `--allow-fs-read=${workspaceRoot}`,
33
+ "--allow-child-process",
34
+ "--allow-worker",
35
+ "--allow-ffi",
36
+ "--experimental-ffi",
37
+ ]
38
+ }
39
+
40
+ describe("destroy on process exit", () => {
41
+ it("it should let applications restore terminal state in an exit handler", () => {
42
+ const { result, stdout } = runFixture(0)
43
+
44
+ expect(result.status).toBe(0)
45
+ expect(stdout).toContain("raw mode disabled")
46
+ })
47
+
48
+ it("it should restore terminal state for non-zero exit codes", () => {
49
+ const { result, stdout } = runFixture(1)
50
+
51
+ expect(result.status).toBe(1)
52
+ expect(stdout).toContain("raw mode disabled")
53
+ })
54
+
55
+ it("it should suspend the renderer when destroy happens during an active frame in an exit handler", () => {
56
+ const { result, stdout } = runFixture(0, "during-render")
57
+
58
+ expect(result.status).toBe(0)
59
+ expect(stdout).toContain("raw mode disabled")
60
+ expect(stdout).toContain("renderer suspended")
61
+ })
62
+ })