@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,375 @@
1
+ import {
2
+ CliRenderer,
3
+ CliRenderEvents,
4
+ type CliRendererConfig,
5
+ type CliRendererExternalOutputEvent,
6
+ type CliRendererFrameEvent,
7
+ } from "../renderer.js"
8
+ import type { NativeSpanFeed } from "../NativeSpanFeed.js"
9
+ import type { NativeRenderStats } from "../zig.js"
10
+ import { createMockKeys } from "./mock-keys.js"
11
+ import { createMockMouse } from "./mock-mouse.js"
12
+ import { createTestStdin, createTestStdout } from "./test-streams.js"
13
+ import type { CapturedFrame } from "../types.js"
14
+
15
+ export interface TestRendererOptions extends CliRendererConfig {
16
+ width?: number
17
+ height?: number
18
+ kittyKeyboard?: boolean
19
+ otherModifiersMode?: boolean
20
+ }
21
+ export type TestRenderer = CliRenderer
22
+ export type MockInput = ReturnType<typeof createMockKeys>
23
+ export type MockMouse = ReturnType<typeof createMockMouse>
24
+
25
+ type RendererFeedAccess = {
26
+ _feed?: NativeSpanFeed | null
27
+ }
28
+
29
+ export interface TestFlushOptions {
30
+ maxPasses?: number
31
+ }
32
+
33
+ export interface TestVisualIdleOptions {
34
+ quietFrames?: number
35
+ maxFrames?: number
36
+ }
37
+
38
+ export interface TestWaitForOptions {
39
+ maxPasses?: number
40
+ }
41
+
42
+ export interface TestExternalOutputCommit {
43
+ text: string
44
+ rows: string[]
45
+ width: number
46
+ height: number
47
+ rowColumns: number
48
+ startOnNewLine: boolean
49
+ trailingNewline: boolean
50
+ }
51
+
52
+ export interface TestExternalOutput {
53
+ take(): TestExternalOutputCommit[]
54
+ takeText(): string
55
+ clear(): void
56
+ }
57
+
58
+ export interface TestRendererSetup {
59
+ renderer: TestRenderer
60
+ mockInput: MockInput
61
+ mockMouse: MockMouse
62
+ renderOnce: () => Promise<void>
63
+ flush: (options?: TestFlushOptions) => Promise<void>
64
+ waitFor: (predicate: () => boolean | Promise<boolean>, options?: TestWaitForOptions) => Promise<void>
65
+ waitForFrame: (
66
+ predicate: (frame: string) => boolean | Promise<boolean>,
67
+ options?: TestWaitForOptions,
68
+ ) => Promise<string>
69
+ waitForVisualIdle: (options?: TestVisualIdleOptions) => Promise<void>
70
+ externalOutput: TestExternalOutput
71
+ getNativeStats: () => NativeRenderStats
72
+ captureCharFrame: () => string
73
+ captureSpans: () => CapturedFrame
74
+ resize: (width: number, height: number) => void
75
+ }
76
+
77
+ const decoder = new TextDecoder()
78
+ const DEFAULT_MAX_PASSES = 20
79
+ const DEFAULT_MAX_VISUAL_IDLE_FRAMES = 20
80
+ const DEFAULT_QUIET_FRAMES = 1
81
+
82
+ async function drainImmediateWork(): Promise<void> {
83
+ await Promise.resolve()
84
+ await new Promise<void>((resolve) => process.nextTick(resolve))
85
+ await Promise.resolve()
86
+ }
87
+
88
+ function normalizePositiveInteger(value: number | undefined, fallback: number): number {
89
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
90
+ return fallback
91
+ }
92
+
93
+ return Math.floor(value)
94
+ }
95
+
96
+ function createWaitError(renderer: TestRenderer, message: string, frame?: string): Error {
97
+ const stats = renderer.getStats()
98
+ const scheduler = renderer.getSchedulerState()
99
+ const details = [
100
+ message,
101
+ `frameId: ${renderer.frameId}`,
102
+ `nativeFrameCount: ${stats.nativeFrameCount}`,
103
+ `cellsUpdated: ${stats.cellsUpdated}`,
104
+ `isRunning: ${scheduler.isRunning}`,
105
+ `isRendering: ${scheduler.isRendering}`,
106
+ `hasScheduledRender: ${scheduler.hasScheduledRender}`,
107
+ ]
108
+
109
+ if (frame !== undefined) {
110
+ details.push(`lastFrame:\n${frame}`)
111
+ }
112
+
113
+ return new Error(details.join("\n"))
114
+ }
115
+
116
+ class TestExternalOutputRecorder implements TestExternalOutput {
117
+ private commits: TestExternalOutputCommit[] = []
118
+
119
+ constructor(renderer: TestRenderer) {
120
+ renderer.on(CliRenderEvents.EXTERNAL_OUTPUT, this.record)
121
+ renderer.once(CliRenderEvents.DESTROY, () => {
122
+ renderer.off(CliRenderEvents.EXTERNAL_OUTPUT, this.record)
123
+ })
124
+ }
125
+
126
+ private record = (event: CliRendererExternalOutputEvent): void => {
127
+ const raw = decoder.decode(event.snapshot.getRealCharBytes(false))
128
+ const rows = Array.from({ length: event.snapshot.height }, (_, index) =>
129
+ raw.slice(index * event.snapshot.width, (index + 1) * event.snapshot.width).trimEnd(),
130
+ )
131
+
132
+ this.commits.push({
133
+ text: rows.join("\n"),
134
+ rows,
135
+ width: event.snapshot.width,
136
+ height: event.snapshot.height,
137
+ rowColumns: event.rowColumns,
138
+ startOnNewLine: event.startOnNewLine,
139
+ trailingNewline: event.trailingNewline,
140
+ })
141
+ }
142
+
143
+ public take(): TestExternalOutputCommit[] {
144
+ const commits = this.commits
145
+ this.commits = []
146
+ return commits
147
+ }
148
+
149
+ public takeText(): string {
150
+ return this.take()
151
+ .flatMap((commit) => commit.rows)
152
+ .join("\n")
153
+ }
154
+
155
+ public clear(): void {
156
+ this.commits = []
157
+ }
158
+ }
159
+
160
+ function waitForNextFrameOrIdle(renderer: TestRenderer): Promise<CliRendererFrameEvent | null> {
161
+ const scheduler = renderer.getSchedulerState()
162
+ if (!scheduler.isRunning && !scheduler.isRendering && !scheduler.hasScheduledRender) {
163
+ return Promise.resolve(null)
164
+ }
165
+
166
+ return new Promise((resolve) => {
167
+ let settled = false
168
+
169
+ const cleanup = () => {
170
+ renderer.off(CliRenderEvents.FRAME, onFrame)
171
+ renderer.off(CliRenderEvents.DESTROY, onDestroy)
172
+ }
173
+
174
+ const finish = (event: CliRendererFrameEvent | null) => {
175
+ if (settled) return
176
+ settled = true
177
+ cleanup()
178
+ resolve(event)
179
+ }
180
+
181
+ const onFrame = (event: CliRendererFrameEvent) => {
182
+ finish(event)
183
+ }
184
+
185
+ const onDestroy = () => {
186
+ finish(null)
187
+ }
188
+
189
+ renderer.on(CliRenderEvents.FRAME, onFrame)
190
+ renderer.once(CliRenderEvents.DESTROY, onDestroy)
191
+
192
+ if (!scheduler.isRunning) {
193
+ renderer.idle().then(() => finish(null))
194
+ }
195
+ })
196
+ }
197
+
198
+ export async function createTestRenderer(options: TestRendererOptions): Promise<TestRendererSetup> {
199
+ // Convert legacy kittyKeyboard boolean to new format
200
+ const useKittyKeyboard = options.kittyKeyboard ? { events: true } : options.useKittyKeyboard
201
+
202
+ const renderer = await setupTestRenderer({
203
+ ...options,
204
+ useKittyKeyboard,
205
+ screenMode: options.screenMode ?? "main-screen",
206
+ footerHeight: options.footerHeight ?? 12,
207
+ consoleMode: options.consoleMode ?? "disabled",
208
+ externalOutputMode: options.externalOutputMode ?? "passthrough",
209
+ })
210
+ const externalOutput = new TestExternalOutputRecorder(renderer)
211
+
212
+ const mockInput = createMockKeys(renderer, {
213
+ kittyKeyboard: options.kittyKeyboard,
214
+ otherModifiersMode: options.otherModifiersMode,
215
+ })
216
+ const mockMouse = createMockMouse(renderer)
217
+
218
+ const renderOnce = async () => {
219
+ const feed = (renderer as unknown as RendererFeedAccess)._feed
220
+ if (feed?.isBackpressured()) {
221
+ await feed.idle()
222
+ }
223
+ //@ts-expect-error - this is a test renderer
224
+ await renderer.loop()
225
+ }
226
+
227
+ const captureCharFrame = () => {
228
+ const currentBuffer = renderer.currentRenderBuffer
229
+ const frameBytes = currentBuffer.getRealCharBytes(true)
230
+ return decoder.decode(frameBytes)
231
+ }
232
+
233
+ const waitForVisualIdle = async (waitOptions: TestVisualIdleOptions = {}): Promise<void> => {
234
+ const maxFrames = normalizePositiveInteger(waitOptions.maxFrames, DEFAULT_MAX_VISUAL_IDLE_FRAMES)
235
+ const quietFrames = normalizePositiveInteger(waitOptions.quietFrames, DEFAULT_QUIET_FRAMES)
236
+ let consecutiveQuietFrames = 0
237
+
238
+ for (let frame = 0; frame < maxFrames; frame++) {
239
+ await drainImmediateWork()
240
+
241
+ const scheduler = renderer.getSchedulerState()
242
+ if (!scheduler.isRunning && !scheduler.isRendering && !scheduler.hasScheduledRender) {
243
+ return
244
+ }
245
+
246
+ const event = await waitForNextFrameOrIdle(renderer)
247
+ if (!event) {
248
+ return
249
+ }
250
+
251
+ if (renderer.getNativeStats().cellsUpdated === 0) {
252
+ consecutiveQuietFrames++
253
+ if (consecutiveQuietFrames >= quietFrames) {
254
+ return
255
+ }
256
+ } else {
257
+ consecutiveQuietFrames = 0
258
+ }
259
+ }
260
+
261
+ await drainImmediateWork()
262
+ const scheduler = renderer.getSchedulerState()
263
+ if (!scheduler.isRunning && !scheduler.isRendering && !scheduler.hasScheduledRender) {
264
+ return
265
+ }
266
+
267
+ throw createWaitError(renderer, `Timed out waiting for visual idle after ${maxFrames} frames`)
268
+ }
269
+
270
+ const flush = async (flushOptions: TestFlushOptions = {}): Promise<void> => {
271
+ await waitForVisualIdle({ maxFrames: normalizePositiveInteger(flushOptions.maxPasses, DEFAULT_MAX_PASSES) })
272
+ }
273
+
274
+ const waitFor = async (
275
+ predicate: () => boolean | Promise<boolean>,
276
+ waitOptions: TestWaitForOptions = {},
277
+ ): Promise<void> => {
278
+ const maxPasses = normalizePositiveInteger(waitOptions.maxPasses, DEFAULT_MAX_PASSES)
279
+
280
+ for (let pass = 0; pass <= maxPasses; pass++) {
281
+ await drainImmediateWork()
282
+ if (await predicate()) {
283
+ return
284
+ }
285
+
286
+ if (pass === maxPasses) {
287
+ break
288
+ }
289
+
290
+ const scheduler = renderer.getSchedulerState()
291
+ if (!scheduler.isRunning && !scheduler.isRendering && !scheduler.hasScheduledRender) {
292
+ break
293
+ }
294
+
295
+ await waitForNextFrameOrIdle(renderer)
296
+ }
297
+
298
+ throw createWaitError(renderer, `Timed out waiting for predicate after ${maxPasses} passes`)
299
+ }
300
+
301
+ const waitForFrame = async (
302
+ predicate: (frame: string) => boolean | Promise<boolean>,
303
+ waitOptions: TestWaitForOptions = {},
304
+ ): Promise<string> => {
305
+ const maxPasses = normalizePositiveInteger(waitOptions.maxPasses, DEFAULT_MAX_PASSES)
306
+ let frame = captureCharFrame()
307
+
308
+ for (let pass = 0; pass <= maxPasses; pass++) {
309
+ await drainImmediateWork()
310
+ frame = captureCharFrame()
311
+ if (await predicate(frame)) {
312
+ return frame
313
+ }
314
+
315
+ if (pass === maxPasses) {
316
+ break
317
+ }
318
+
319
+ const scheduler = renderer.getSchedulerState()
320
+ if (!scheduler.isRunning && !scheduler.isRendering && !scheduler.hasScheduledRender) {
321
+ break
322
+ }
323
+
324
+ await waitForNextFrameOrIdle(renderer)
325
+ }
326
+
327
+ frame = captureCharFrame()
328
+ throw createWaitError(renderer, `Timed out waiting for frame predicate after ${maxPasses} passes`, frame)
329
+ }
330
+
331
+ return {
332
+ renderer,
333
+ mockInput,
334
+ mockMouse,
335
+ renderOnce,
336
+ flush,
337
+ waitFor,
338
+ waitForFrame,
339
+ waitForVisualIdle,
340
+ externalOutput,
341
+ getNativeStats: () => renderer.getNativeStats(),
342
+ captureCharFrame,
343
+ captureSpans: () => {
344
+ const currentBuffer = renderer.currentRenderBuffer
345
+ const lines = currentBuffer.getSpanLines()
346
+ const cursorState = renderer.getCursorState()
347
+ return {
348
+ cols: currentBuffer.width,
349
+ rows: currentBuffer.height,
350
+ cursor: [cursorState.x, cursorState.y] as [number, number],
351
+ lines,
352
+ }
353
+ },
354
+ resize: (width: number, height: number) => {
355
+ //@ts-expect-error - this is a test renderer
356
+ renderer.processResize(width, height)
357
+ },
358
+ }
359
+ }
360
+
361
+ async function setupTestRenderer(config: TestRendererOptions) {
362
+ const stdin = config.stdin || createTestStdin()
363
+ const width = config.width || config.stdout?.columns || process.stdout.columns || 80
364
+ const height = config.height || config.stdout?.rows || process.stdout.rows || 24
365
+ const stdout = config.stdout || createTestStdout(width, height)
366
+
367
+ // Direct construction skips setupTerminal(); native bytes are routed to an
368
+ // explicit memory destination so tests do not depend on process stdout or feed
369
+ // backpressure behavior. CliRenderer still owns native renderer creation and
370
+ // applies the same useThread defaults as production construction.
371
+ return new CliRenderer(stdin, stdout, width, height, {
372
+ ...config,
373
+ bufferedOutput: config.bufferedOutput ?? "memory",
374
+ })
375
+ }
@@ -0,0 +1,278 @@
1
+ import { afterEach, expect, spyOn, test } from "bun:test"
2
+ import { CliRenderEvents } from "../renderer.js"
3
+ import { TextRenderable } from "../renderables/Text.js"
4
+ import { ManualClock } from "./manual-clock.js"
5
+ import { createTestRenderer, type TestRendererSetup } from "./test-renderer.js"
6
+
7
+ let setup: TestRendererSetup | null = null
8
+
9
+ async function drainImmediateWork(): Promise<void> {
10
+ await Promise.resolve()
11
+ await new Promise<void>((resolve) => process.nextTick(resolve))
12
+ await Promise.resolve()
13
+ }
14
+
15
+ afterEach(() => {
16
+ setup?.renderer.destroy()
17
+ setup = null
18
+ })
19
+
20
+ test("flush waits for scheduled render work without forcing an extra frame", async () => {
21
+ setup = await createTestRenderer({ width: 10, height: 4, useThread: false, maxFps: Number.POSITIVE_INFINITY })
22
+
23
+ const text = new TextRenderable(setup.renderer, {
24
+ content: "abc",
25
+ width: 3,
26
+ height: 1,
27
+ })
28
+ setup.renderer.root.add(text)
29
+
30
+ await setup.flush({ maxPasses: 1 })
31
+
32
+ const renderedStats = setup.getNativeStats()
33
+ expect(renderedStats.nativeFrameCount).toBe(1)
34
+ expect(renderedStats.cellsUpdated).toBeGreaterThanOrEqual(3)
35
+
36
+ await setup.flush()
37
+
38
+ expect(setup.getNativeStats().nativeFrameCount).toBe(1)
39
+ })
40
+
41
+ test("waitForFrame observes text from a scheduled render", async () => {
42
+ setup = await createTestRenderer({ width: 10, height: 4, useThread: false, maxFps: Number.POSITIVE_INFINITY })
43
+
44
+ const text = new TextRenderable(setup.renderer, {
45
+ content: "hello",
46
+ width: 5,
47
+ height: 1,
48
+ })
49
+ setup.renderer.root.add(text)
50
+
51
+ const frame = await setup.waitForFrame((value) => value.includes("hello"), { maxPasses: 1 })
52
+
53
+ expect(frame).toContain("hello")
54
+ expect(setup.getNativeStats().nativeFrameCount).toBe(1)
55
+ })
56
+
57
+ test("waitFor observes predicate changes after scheduled work", async () => {
58
+ setup = await createTestRenderer({ width: 10, height: 4, useThread: false, maxFps: Number.POSITIVE_INFINITY })
59
+
60
+ const text = new TextRenderable(setup.renderer, {
61
+ content: "ready",
62
+ width: 5,
63
+ height: 1,
64
+ })
65
+ setup.renderer.root.add(text)
66
+
67
+ await setup.waitFor(() => setup!.getNativeStats().nativeFrameCount > 0, { maxPasses: 1 })
68
+
69
+ expect(setup.getNativeStats().nativeFrameCount).toBe(1)
70
+ })
71
+
72
+ test("renderer does not build frame event stats when no frame listener is registered", async () => {
73
+ setup = await createTestRenderer({ width: 10, height: 4, useThread: false })
74
+
75
+ const getStats = spyOn(setup.renderer, "getStats")
76
+
77
+ const text = new TextRenderable(setup.renderer, {
78
+ content: "quiet",
79
+ width: 5,
80
+ height: 1,
81
+ })
82
+ setup.renderer.root.add(text)
83
+
84
+ await setup.renderOnce()
85
+
86
+ expect(getStats).not.toHaveBeenCalled()
87
+ getStats.mockRestore()
88
+ })
89
+
90
+ test("renderer emits frame event without building stats when a frame listener is registered", async () => {
91
+ setup = await createTestRenderer({ width: 10, height: 4, useThread: false })
92
+
93
+ const getStats = spyOn(setup.renderer, "getStats")
94
+ let frameEventCount = 0
95
+ let frameEvent: unknown
96
+ setup.renderer.on(CliRenderEvents.FRAME, (event) => {
97
+ frameEventCount++
98
+ frameEvent = event
99
+ })
100
+
101
+ const text = new TextRenderable(setup.renderer, {
102
+ content: "event",
103
+ width: 5,
104
+ height: 1,
105
+ })
106
+ setup.renderer.root.add(text)
107
+
108
+ await setup.renderOnce()
109
+
110
+ expect(frameEventCount).toBe(1)
111
+ expect(frameEvent).toEqual({ frameId: setup.renderer.frameId })
112
+ expect(getStats).not.toHaveBeenCalled()
113
+ getStats.mockRestore()
114
+ })
115
+
116
+ test("waitForFrame fails instead of rendering when no work is pending", async () => {
117
+ setup = await createTestRenderer({ width: 10, height: 4, useThread: false, maxFps: Number.POSITIVE_INFINITY })
118
+
119
+ await expect(setup.waitForFrame((frame) => frame.includes("missing"), { maxPasses: 2 })).rejects.toThrow(
120
+ "hasScheduledRender: false",
121
+ )
122
+
123
+ expect(setup.getNativeStats().nativeFrameCount).toBe(0)
124
+ })
125
+
126
+ test("waitForVisualIdle observes a naturally emitted zero-cell live frame", async () => {
127
+ const clock = new ManualClock()
128
+ setup = await createTestRenderer({
129
+ width: 10,
130
+ height: 4,
131
+ useThread: false,
132
+ clock,
133
+ maxFps: Number.POSITIVE_INFINITY,
134
+ targetFps: Number.POSITIVE_INFINITY,
135
+ })
136
+
137
+ const text = new TextRenderable(setup.renderer, {
138
+ content: "live",
139
+ width: 4,
140
+ height: 1,
141
+ })
142
+ setup.renderer.root.add(text)
143
+ setup.renderer.start()
144
+
145
+ await drainImmediateWork()
146
+ expect(setup.getNativeStats().nativeFrameCount).toBe(1)
147
+
148
+ const idle = setup.waitForVisualIdle({ maxFrames: 2 })
149
+ await drainImmediateWork()
150
+ clock.advance(1)
151
+ await idle
152
+
153
+ const stats = setup.getNativeStats()
154
+ expect(stats.nativeFrameCount).toBe(2)
155
+ expect(stats.cellsUpdated).toBe(0)
156
+
157
+ setup.renderer.stop()
158
+ })
159
+
160
+ test("externalOutput records writeToScrollback commits without consuming native queue", async () => {
161
+ setup = await createTestRenderer({
162
+ width: 10,
163
+ height: 6,
164
+ screenMode: "split-footer",
165
+ footerHeight: 3,
166
+ externalOutputMode: "capture-stdout",
167
+ consoleMode: "disabled",
168
+ useThread: false,
169
+ maxFps: Number.POSITIVE_INFINITY,
170
+ })
171
+
172
+ setup.renderer.writeToScrollback((ctx) => {
173
+ const root = new TextRenderable(ctx.renderContext, {
174
+ content: "hello\nworld",
175
+ width: 5,
176
+ height: 2,
177
+ })
178
+
179
+ return {
180
+ root,
181
+ width: 5,
182
+ height: 2,
183
+ trailingNewline: false,
184
+ }
185
+ })
186
+
187
+ const commits = setup.externalOutput.take()
188
+
189
+ expect(commits).toHaveLength(1)
190
+ expect(commits[0]).toMatchObject({
191
+ text: "hello\nworld",
192
+ rows: ["hello", "world"],
193
+ width: 5,
194
+ height: 2,
195
+ rowColumns: 5,
196
+ startOnNewLine: true,
197
+ trailingNewline: false,
198
+ })
199
+ expect((setup.renderer as any).externalOutputQueue.size).toBe(1)
200
+
201
+ await setup.renderOnce()
202
+
203
+ expect((setup.renderer as any).externalOutputQueue.size).toBe(0)
204
+ expect(setup.externalOutput.take()).toEqual([])
205
+ })
206
+
207
+ test("externalOutput records scrollback surface commits", async () => {
208
+ setup = await createTestRenderer({
209
+ width: 10,
210
+ height: 6,
211
+ screenMode: "split-footer",
212
+ footerHeight: 3,
213
+ externalOutputMode: "capture-stdout",
214
+ consoleMode: "disabled",
215
+ useThread: false,
216
+ })
217
+
218
+ const surface = setup.renderer.createScrollbackSurface()
219
+ const text = new TextRenderable(surface.renderContext, {
220
+ content: "surface",
221
+ width: 7,
222
+ height: 1,
223
+ })
224
+
225
+ surface.root.add(text)
226
+ surface.render()
227
+ surface.commitRows(0, 1, { trailingNewline: false })
228
+
229
+ const commits = setup.externalOutput.take()
230
+
231
+ expect(commits).toHaveLength(1)
232
+ expect(commits[0]).toMatchObject({
233
+ text: "surface",
234
+ rows: ["surface"],
235
+ width: 10,
236
+ height: 1,
237
+ rowColumns: 10,
238
+ startOnNewLine: true,
239
+ trailingNewline: false,
240
+ })
241
+ })
242
+
243
+ test("externalOutput records captured stdout in FIFO order", async () => {
244
+ setup = await createTestRenderer({
245
+ width: 10,
246
+ height: 6,
247
+ screenMode: "split-footer",
248
+ footerHeight: 3,
249
+ externalOutputMode: "capture-stdout",
250
+ consoleMode: "disabled",
251
+ useThread: false,
252
+ })
253
+
254
+ setup.renderer.writeToScrollback((ctx) => {
255
+ const root = new TextRenderable(ctx.renderContext, {
256
+ content: "api",
257
+ width: 3,
258
+ height: 1,
259
+ })
260
+
261
+ return {
262
+ root,
263
+ width: 3,
264
+ height: 1,
265
+ }
266
+ })
267
+ ;(setup.renderer as any).stdout.write("out-1\n\nout-2")
268
+
269
+ const commits = setup.externalOutput.take()
270
+
271
+ expect(commits.map((commit) => commit.text)).toEqual(["api", "out-1", "", "out-2"])
272
+ expect(commits.map((commit) => commit.startOnNewLine)).toEqual([true, false, false, false])
273
+ expect(commits.map((commit) => commit.trailingNewline)).toEqual([true, true, true, false])
274
+
275
+ ;(setup.renderer as any).stdout.write("again")
276
+ expect(setup.externalOutput.takeText()).toBe("again")
277
+ expect(setup.externalOutput.take()).toEqual([])
278
+ })
@@ -0,0 +1,31 @@
1
+ import { Readable, Writable } from "stream"
2
+
3
+ export class TestWriteStream extends Writable {
4
+ public readonly isTTY = true
5
+ public readonly columns: number
6
+ public readonly rows: number
7
+
8
+ constructor(columns = 80, rows = 24) {
9
+ super()
10
+ this.columns = columns
11
+ this.rows = rows
12
+ }
13
+
14
+ override _write(_chunk: any, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
15
+ callback()
16
+ }
17
+
18
+ getColorDepth(): number {
19
+ return 24
20
+ }
21
+ }
22
+
23
+ export type TestStdout = TestWriteStream & NodeJS.WriteStream
24
+
25
+ export function createTestStdin(): NodeJS.ReadStream {
26
+ return new Readable({ read() {} }) as NodeJS.ReadStream
27
+ }
28
+
29
+ export function createTestStdout(columns = 80, rows = 24): NodeJS.WriteStream {
30
+ return new TestWriteStream(columns, rows) as TestStdout
31
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,9 @@
1
+ // Testing utilities module exports
2
+ export * from "./testing/test-renderer.js"
3
+ export * from "./testing/mock-keys.js"
4
+ export * from "./testing/mock-mouse.js"
5
+ export * from "./testing/mock-tree-sitter-client.js"
6
+ export * from "./testing/terminal-capabilities.js"
7
+ export * from "./testing/spy.js"
8
+ export { ManualClock } from "./testing/manual-clock.js"
9
+ export { TestRecorder, type RecordedFrame } from "./testing/test-recorder.js"