@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.
- package/README.md +63 -0
- package/dev/keypress-debug-renderer.ts +148 -0
- package/dev/keypress-debug.ts +43 -0
- package/dev/print-env-vars.ts +32 -0
- package/dev/test-tmux-graphics-334.sh +68 -0
- package/dev/test-tmux-theme-queries.sh +151 -0
- package/dev/thai-debug-test.ts +68 -0
- package/dev/theme-mode.ts +290 -0
- package/docs/development.md +144 -0
- package/package.json +102 -0
- package/prebuilt/aarch64-android/libopentui.so +0 -0
- package/scripts/build-android.ts +226 -0
- package/scripts/build-native-termux.sh +525 -0
- package/scripts/build.ts +530 -0
- package/scripts/dist-test.ts +312 -0
- package/scripts/package-prebuilt.ts +110 -0
- package/scripts/publish.ts +60 -0
- package/scripts/test-node-hook.mjs +16 -0
- package/scripts/test-node.ts +241 -0
- package/scripts/vendor-deps.sh +73 -0
- package/src/NativeSpanFeed.ts +375 -0
- package/src/Renderable.ts +1878 -0
- package/src/__snapshots__/buffer.test.ts.snap +28 -0
- package/src/animation/Timeline.test.ts +2709 -0
- package/src/animation/Timeline.ts +598 -0
- package/src/ansi.ts +22 -0
- package/src/audio.ts +479 -0
- package/src/benchmark/attenuation-benchmark.ts +81 -0
- package/src/benchmark/audio-playback-benchmark.ts +344 -0
- package/src/benchmark/box-draw-benchmark.ts +1042 -0
- package/src/benchmark/colormatrix-benchmark.ts +123 -0
- package/src/benchmark/gain-benchmark.ts +80 -0
- package/src/benchmark/latest-all-bench-run.json +707 -0
- package/src/benchmark/latest-async-bench-run.json +336 -0
- package/src/benchmark/latest-default-bench-run.json +657 -0
- package/src/benchmark/latest-large-bench-run.json +707 -0
- package/src/benchmark/latest-quick-bench-run.json +207 -0
- package/src/benchmark/layout-benchmark.ts +2547 -0
- package/src/benchmark/markdown-benchmark.ts +1796 -0
- package/src/benchmark/native-span-feed-async-benchmark.ts +355 -0
- package/src/benchmark/native-span-feed-benchmark.md +56 -0
- package/src/benchmark/native-span-feed-benchmark.ts +596 -0
- package/src/benchmark/native-span-feed-compare.ts +280 -0
- package/src/benchmark/render-traversal-benchmark.ts +928 -0
- package/src/benchmark/text-buffer-render-benchmark.ts +874 -0
- package/src/benchmark/text-table-benchmark.ts +948 -0
- package/src/buffer.test.ts +292 -0
- package/src/buffer.ts +564 -0
- package/src/console.test.ts +629 -0
- package/src/console.ts +1253 -0
- package/src/edit-buffer.test.ts +1852 -0
- package/src/edit-buffer.ts +417 -0
- package/src/editor-view.test.ts +1056 -0
- package/src/editor-view.ts +290 -0
- package/src/index.ts +25 -0
- package/src/lib/KeyHandler.integration.test.ts +292 -0
- package/src/lib/KeyHandler.stopPropagation.test.ts +289 -0
- package/src/lib/KeyHandler.test.ts +662 -0
- package/src/lib/KeyHandler.ts +222 -0
- package/src/lib/RGBA.test.ts +60 -0
- package/src/lib/RGBA.ts +343 -0
- package/src/lib/ascii.font.ts +330 -0
- package/src/lib/border.test.ts +83 -0
- package/src/lib/border.ts +170 -0
- package/src/lib/bunfs.test.ts +27 -0
- package/src/lib/bunfs.ts +18 -0
- package/src/lib/clipboard.test.ts +63 -0
- package/src/lib/clipboard.ts +41 -0
- package/src/lib/clock.ts +35 -0
- package/src/lib/data-paths.test.ts +133 -0
- package/src/lib/data-paths.ts +109 -0
- package/src/lib/debounce.ts +106 -0
- package/src/lib/detect-links.test.ts +98 -0
- package/src/lib/detect-links.ts +56 -0
- package/src/lib/env.test.ts +228 -0
- package/src/lib/env.ts +209 -0
- package/src/lib/extmarks-history.ts +51 -0
- package/src/lib/extmarks-multiwidth.test.ts +324 -0
- package/src/lib/extmarks.test.ts +3547 -0
- package/src/lib/extmarks.ts +861 -0
- package/src/lib/fonts/block.json +405 -0
- package/src/lib/fonts/grid.json +265 -0
- package/src/lib/fonts/huge.json +741 -0
- package/src/lib/fonts/pallet.json +314 -0
- package/src/lib/fonts/shade.json +591 -0
- package/src/lib/fonts/slick.json +321 -0
- package/src/lib/fonts/tiny.json +69 -0
- package/src/lib/hast-styled-text.ts +59 -0
- package/src/lib/index.ts +22 -0
- package/src/lib/keybinding.internal.test.ts +342 -0
- package/src/lib/keybinding.internal.ts +182 -0
- package/src/lib/objects-in-viewport.test.ts +789 -0
- package/src/lib/objects-in-viewport.ts +153 -0
- package/src/lib/output.capture.ts +58 -0
- package/src/lib/parse.keypress-kitty.protocol.test.ts +347 -0
- package/src/lib/parse.keypress-kitty.test.ts +712 -0
- package/src/lib/parse.keypress-kitty.ts +470 -0
- package/src/lib/parse.keypress.test.ts +2020 -0
- package/src/lib/parse.keypress.ts +480 -0
- package/src/lib/parse.mouse.test.ts +552 -0
- package/src/lib/parse.mouse.ts +232 -0
- package/src/lib/paste.ts +18 -0
- package/src/lib/queue.ts +65 -0
- package/src/lib/render-geometry.ts +36 -0
- package/src/lib/renderable.validations.test.ts +87 -0
- package/src/lib/renderable.validations.ts +83 -0
- package/src/lib/scroll-acceleration.ts +98 -0
- package/src/lib/selection.ts +261 -0
- package/src/lib/singleton.ts +34 -0
- package/src/lib/stdin-parser.test.ts +2439 -0
- package/src/lib/stdin-parser.ts +2007 -0
- package/src/lib/styled-text.ts +178 -0
- package/src/lib/terminal-capability-detection.test.ts +226 -0
- package/src/lib/terminal-capability-detection.ts +98 -0
- package/src/lib/terminal-palette.test.ts +954 -0
- package/src/lib/terminal-palette.ts +441 -0
- package/src/lib/tree-sitter/assets/README.md +119 -0
- package/src/lib/tree-sitter/assets/javascript/highlights.scm +205 -0
- package/src/lib/tree-sitter/assets/javascript/tree-sitter-javascript.wasm +0 -0
- package/src/lib/tree-sitter/assets/markdown/highlights.scm +150 -0
- package/src/lib/tree-sitter/assets/markdown/injections.scm +27 -0
- package/src/lib/tree-sitter/assets/markdown/tree-sitter-markdown.wasm +0 -0
- package/src/lib/tree-sitter/assets/markdown_inline/highlights.scm +115 -0
- package/src/lib/tree-sitter/assets/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
- package/src/lib/tree-sitter/assets/typescript/highlights.scm +604 -0
- package/src/lib/tree-sitter/assets/typescript/tree-sitter-typescript.wasm +0 -0
- package/src/lib/tree-sitter/assets/update.ts +360 -0
- package/src/lib/tree-sitter/assets/zig/highlights.scm +284 -0
- package/src/lib/tree-sitter/assets/zig/tree-sitter-zig.wasm +0 -0
- package/src/lib/tree-sitter/assets.d.ts +9 -0
- package/src/lib/tree-sitter/cache.test.ts +291 -0
- package/src/lib/tree-sitter/client.test.ts +1434 -0
- package/src/lib/tree-sitter/client.ts +847 -0
- package/src/lib/tree-sitter/default-parsers.ts +133 -0
- package/src/lib/tree-sitter/download-utils.ts +148 -0
- package/src/lib/tree-sitter/index.ts +75 -0
- package/src/lib/tree-sitter/parser.worker.ts +1098 -0
- package/src/lib/tree-sitter/parsers-config.ts +81 -0
- package/src/lib/tree-sitter/resolve-ft.test.ts +55 -0
- package/src/lib/tree-sitter/resolve-ft.ts +189 -0
- package/src/lib/tree-sitter/types.ts +132 -0
- package/src/lib/tree-sitter/update-assets.ts +10 -0
- package/src/lib/tree-sitter-styled-text.test.ts +1253 -0
- package/src/lib/tree-sitter-styled-text.ts +314 -0
- package/src/lib/validate-dir-name.ts +55 -0
- package/src/lib/yoga.options.test.ts +628 -0
- package/src/lib/yoga.options.ts +346 -0
- package/src/native-event-worker-repro.test.ts +48 -0
- package/src/native-event-worker-repro.worker.ts +9 -0
- package/src/native-handle.test.ts +221 -0
- package/src/platform/ffi.test.ts +516 -0
- package/src/platform/ffi.ts +653 -0
- package/src/platform/runtime.test.ts +52 -0
- package/src/platform/runtime.ts +121 -0
- package/src/platform/test.ts +45 -0
- package/src/platform/worker-handler-cleanup.fixture.ts +12 -0
- package/src/platform/worker-onmessage-startup.fixture.ts +10 -0
- package/src/platform/worker-startup.fixture.ts +9 -0
- package/src/platform/worker.node-test.ts +153 -0
- package/src/platform/worker.ts +425 -0
- package/src/plugins/core-slot.ts +579 -0
- package/src/plugins/registry.ts +402 -0
- package/src/plugins/types.ts +46 -0
- package/src/post/effects.ts +948 -0
- package/src/post/filters.ts +512 -0
- package/src/post/matrices.ts +288 -0
- package/src/renderables/ASCIIFont.ts +219 -0
- package/src/renderables/Box.test.ts +474 -0
- package/src/renderables/Box.ts +353 -0
- package/src/renderables/Code.test.ts +2388 -0
- package/src/renderables/Code.ts +555 -0
- package/src/renderables/Diff.regression.test.ts +381 -0
- package/src/renderables/Diff.test.ts +3212 -0
- package/src/renderables/Diff.ts +1278 -0
- package/src/renderables/EditBufferRenderable.test.ts +288 -0
- package/src/renderables/EditBufferRenderable.ts +1152 -0
- package/src/renderables/FrameBuffer.ts +47 -0
- package/src/renderables/Input.test.ts +1310 -0
- package/src/renderables/Input.ts +273 -0
- package/src/renderables/LineNumberRenderable.ts +725 -0
- package/src/renderables/Markdown.ts +2144 -0
- package/src/renderables/ScrollBar.ts +423 -0
- package/src/renderables/ScrollBox.ts +903 -0
- package/src/renderables/Select.test.ts +1134 -0
- package/src/renderables/Select.ts +535 -0
- package/src/renderables/Slider.test.ts +456 -0
- package/src/renderables/Slider.ts +342 -0
- package/src/renderables/TabSelect.test.ts +238 -0
- package/src/renderables/TabSelect.ts +447 -0
- package/src/renderables/Text.selection-buffer.test.ts +118 -0
- package/src/renderables/Text.test.ts +2662 -0
- package/src/renderables/Text.ts +147 -0
- package/src/renderables/TextBufferRenderable.ts +504 -0
- package/src/renderables/TextNode.test.ts +1137 -0
- package/src/renderables/TextNode.ts +379 -0
- package/src/renderables/TextTable.test.ts +1411 -0
- package/src/renderables/TextTable.ts +1434 -0
- package/src/renderables/Textarea.ts +425 -0
- package/src/renderables/TimeToFirstDraw.ts +89 -0
- package/src/renderables/__snapshots__/Code.test.ts.snap +13 -0
- package/src/renderables/__snapshots__/Diff.test.ts.snap +785 -0
- package/src/renderables/__snapshots__/Text.test.ts.snap +421 -0
- package/src/renderables/__snapshots__/TextTable.test.ts.snap +215 -0
- package/src/renderables/__tests__/Code.test.ts +55 -0
- package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +144 -0
- package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +827 -0
- package/src/renderables/__tests__/LineNumberRenderable.test.ts +1906 -0
- package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +85 -0
- package/src/renderables/__tests__/Markdown.code-colors.test.ts +288 -0
- package/src/renderables/__tests__/Markdown.test.ts +4175 -0
- package/src/renderables/__tests__/MultiRenderable.selection.test.ts +87 -0
- package/src/renderables/__tests__/Textarea.buffer.test.ts +683 -0
- package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +675 -0
- package/src/renderables/__tests__/Textarea.editing.test.ts +2120 -0
- package/src/renderables/__tests__/Textarea.error-handling.test.ts +35 -0
- package/src/renderables/__tests__/Textarea.events.test.ts +738 -0
- package/src/renderables/__tests__/Textarea.highlights.test.ts +590 -0
- package/src/renderables/__tests__/Textarea.keybinding.test.ts +3275 -0
- package/src/renderables/__tests__/Textarea.paste.test.ts +357 -0
- package/src/renderables/__tests__/Textarea.rendering.test.ts +1927 -0
- package/src/renderables/__tests__/Textarea.scroll.test.ts +733 -0
- package/src/renderables/__tests__/Textarea.selection.test.ts +1589 -0
- package/src/renderables/__tests__/Textarea.stress.test.ts +670 -0
- package/src/renderables/__tests__/Textarea.undo-redo.test.ts +383 -0
- package/src/renderables/__tests__/Textarea.visual-lines.test.ts +310 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +221 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +89 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +457 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +158 -0
- package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +387 -0
- package/src/renderables/__tests__/markdown-parser.test.ts +241 -0
- package/src/renderables/__tests__/renderable-test-utils.ts +60 -0
- package/src/renderables/composition/README.md +8 -0
- package/src/renderables/composition/VRenderable.ts +32 -0
- package/src/renderables/composition/constructs.ts +127 -0
- package/src/renderables/composition/vnode.ts +277 -0
- package/src/renderables/index.ts +23 -0
- package/src/renderables/markdown-parser.ts +79 -0
- package/src/renderer-theme-mode.ts +199 -0
- package/src/renderer.ts +4975 -0
- package/src/runtime-plugin-support-configure.ts +63 -0
- package/src/runtime-plugin-support.ts +13 -0
- package/src/runtime-plugin.ts +626 -0
- package/src/specs/terminal-startup.md +64 -0
- package/src/syntax-style.test.ts +841 -0
- package/src/syntax-style.ts +274 -0
- package/src/testing/README.md +219 -0
- package/src/testing/bun-test-node.ts +880 -0
- package/src/testing/capture-spans.test.ts +216 -0
- package/src/testing/integration.test.ts +276 -0
- package/src/testing/manual-clock.ts +117 -0
- package/src/testing/mock-keys.test.ts +1378 -0
- package/src/testing/mock-keys.ts +457 -0
- package/src/testing/mock-mouse.test.ts +218 -0
- package/src/testing/mock-mouse.ts +247 -0
- package/src/testing/mock-tree-sitter-client.ts +81 -0
- package/src/testing/spy.ts +13 -0
- package/src/testing/terminal-capabilities.ts +47 -0
- package/src/testing/test-recorder.test.ts +410 -0
- package/src/testing/test-recorder.ts +133 -0
- package/src/testing/test-renderer.ts +375 -0
- package/src/testing/test-renderer.wait.test.ts +278 -0
- package/src/testing/test-streams.ts +31 -0
- package/src/testing.ts +9 -0
- package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +481 -0
- package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +19 -0
- package/src/tests/__snapshots__/scrollbox.test.ts.snap +29 -0
- package/src/tests/absolute-positioning.snapshot.test.ts +638 -0
- package/src/tests/allocator-stats.test.ts +53 -0
- package/src/tests/audio.test.ts +303 -0
- package/src/tests/destroy-during-render.test.ts +200 -0
- package/src/tests/destroy-on-exit.fixture.ts +36 -0
- package/src/tests/destroy-on-exit.test.ts +62 -0
- package/src/tests/ffi-borrowed-pointer-callsites.test.ts +153 -0
- package/src/tests/hover-cursor.test.ts +98 -0
- package/src/tests/native-backed-measurement-lifecycle.test.ts +133 -0
- package/src/tests/native-backed-measurement-parity.test.ts +322 -0
- package/src/tests/native-span-feed-async.test.ts +173 -0
- package/src/tests/native-span-feed-close.test.ts +120 -0
- package/src/tests/native-span-feed-coverage.test.ts +227 -0
- package/src/tests/native-span-feed-edge-cases.test.ts +384 -0
- package/src/tests/native-span-feed-use-after-free.test.ts +45 -0
- package/src/tests/opacity.test.ts +123 -0
- package/src/tests/renderable.snapshot.test.ts +524 -0
- package/src/tests/renderable.test.ts +1851 -0
- package/src/tests/renderer.clock.test.ts +270 -0
- package/src/tests/renderer.console-startup.test.ts +2151 -0
- package/src/tests/renderer.control.test.ts +458 -0
- package/src/tests/renderer.core-slot-binding.test.ts +962 -0
- package/src/tests/renderer.cursor.test.ts +26 -0
- package/src/tests/renderer.custom-stdout.test.ts +1112 -0
- package/src/tests/renderer.destroy-during-render.test.ts +147 -0
- package/src/tests/renderer.focus-restore.test.ts +257 -0
- package/src/tests/renderer.focus.test.ts +329 -0
- package/src/tests/renderer.idle.test.ts +219 -0
- package/src/tests/renderer.input.test.ts +2615 -0
- package/src/tests/renderer.kitty-flags.test.ts +195 -0
- package/src/tests/renderer.lifecycle.test.ts +34 -0
- package/src/tests/renderer.mouse.test.ts +1315 -0
- package/src/tests/renderer.notifications.test.ts +27 -0
- package/src/tests/renderer.palette.test.ts +1199 -0
- package/src/tests/renderer.remote.test.ts +247 -0
- package/src/tests/renderer.render-stats.test.ts +146 -0
- package/src/tests/renderer.scrollback-surface.test.ts +474 -0
- package/src/tests/renderer.selection.test.ts +138 -0
- package/src/tests/renderer.slot-registry.test.ts +684 -0
- package/src/tests/renderer.tracker.test.ts +202 -0
- package/src/tests/renderer.useMouse.test.ts +83 -0
- package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +76 -0
- package/src/tests/runtime-plugin-node-modules-import-like-string.fixture.ts +48 -0
- package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +43 -0
- package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +67 -0
- package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +72 -0
- package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +44 -0
- package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +85 -0
- package/src/tests/runtime-plugin-path-alias.fixture.ts +43 -0
- package/src/tests/runtime-plugin-resolve-roots.fixture.ts +65 -0
- package/src/tests/runtime-plugin-support-configure.fixture.ts +31 -0
- package/src/tests/runtime-plugin-support-late-addition.fixture.ts +17 -0
- package/src/tests/runtime-plugin-support.fixture.ts +11 -0
- package/src/tests/runtime-plugin-support.test.ts +54 -0
- package/src/tests/runtime-plugin-windows-file-url.fixture.ts +30 -0
- package/src/tests/runtime-plugin.fixture.ts +40 -0
- package/src/tests/runtime-plugin.test.ts +369 -0
- package/src/tests/scrollbox-culling-bug.test.ts +114 -0
- package/src/tests/scrollbox-hitgrid-resize.test.ts +136 -0
- package/src/tests/scrollbox-hitgrid.test.ts +909 -0
- package/src/tests/scrollbox.test.ts +1895 -0
- package/src/tests/wrap-resize-perf.test.ts +276 -0
- package/src/tests/yoga-api-coverage.test.ts +409 -0
- package/src/tests/yoga-callback-stress.test.ts +122 -0
- package/src/tests/yoga-ffi.test.ts +117 -0
- package/src/tests/yoga-setters.test.ts +921 -0
- package/src/tests/yoga-upstream/YGAlignBaselineTest.test.ts +124 -0
- package/src/tests/yoga-upstream/YGComputedBorderTest.test.ts +27 -0
- package/src/tests/yoga-upstream/YGComputedMarginTest.test.ts +27 -0
- package/src/tests/yoga-upstream/YGComputedPaddingTest.test.ts +27 -0
- package/src/tests/yoga-upstream/YGDirtiedTest.test.ts +157 -0
- package/src/tests/yoga-upstream/YGErrataTest.test.ts +40 -0
- package/src/tests/yoga-upstream/YGFlexBasisAuto.test.ts +23 -0
- package/src/tests/yoga-upstream/YGHasNewLayout.test.ts +81 -0
- package/src/tests/yoga-upstream/YGMeasureCacheTest.test.ts +31 -0
- package/src/tests/yoga-upstream/YGMeasureTest.test.ts +70 -0
- package/src/tests/yoga-upstream/generated/YGAbsolutePositionTest.test.ts +1864 -0
- package/src/tests/yoga-upstream/generated/YGAlignContentTest.test.ts +5829 -0
- package/src/tests/yoga-upstream/generated/YGAlignItemsTest.test.ts +2444 -0
- package/src/tests/yoga-upstream/generated/YGAlignSelfTest.test.ts +293 -0
- package/src/tests/yoga-upstream/generated/YGAndroidNewsFeed.test.ts +315 -0
- package/src/tests/yoga-upstream/generated/YGAspectRatioTest.test.ts +231 -0
- package/src/tests/yoga-upstream/generated/YGAutoTest.test.ts +347 -0
- package/src/tests/yoga-upstream/generated/YGBorderTest.test.ts +256 -0
- package/src/tests/yoga-upstream/generated/YGBoxSizingTest.test.ts +2747 -0
- package/src/tests/yoga-upstream/generated/YGDimensionTest.test.ts +129 -0
- package/src/tests/yoga-upstream/generated/YGDisplayContentsTest.test.ts +109 -0
- package/src/tests/yoga-upstream/generated/YGDisplayTest.test.ts +1097 -0
- package/src/tests/yoga-upstream/generated/YGFlexDirectionTest.test.ts +4562 -0
- package/src/tests/yoga-upstream/generated/YGFlexTest.test.ts +677 -0
- package/src/tests/yoga-upstream/generated/YGFlexWrapTest.test.ts +2071 -0
- package/src/tests/yoga-upstream/generated/YGGapTest.test.ts +3303 -0
- package/src/tests/yoga-upstream/generated/YGIntrinsicSizeTest.test.ts +582 -0
- package/src/tests/yoga-upstream/generated/YGJustifyContentTest.test.ts +2150 -0
- package/src/tests/yoga-upstream/generated/YGMarginTest.test.ts +1963 -0
- package/src/tests/yoga-upstream/generated/YGMinMaxDimensionTest.test.ts +1446 -0
- package/src/tests/yoga-upstream/generated/YGPaddingTest.test.ts +354 -0
- package/src/tests/yoga-upstream/generated/YGPercentageTest.test.ts +1760 -0
- package/src/tests/yoga-upstream/generated/YGRoundingTest.test.ts +1161 -0
- package/src/tests/yoga-upstream/generated/YGSizeOverflowTest.test.ts +207 -0
- package/src/tests/yoga-upstream/generated/YGStaticPositionTest.test.ts +6338 -0
- package/src/tests/yoga-upstream/tools/MeasureCounter.ts +54 -0
- package/src/tests/yoga-upstream/tools/utils.ts +81 -0
- package/src/text-buffer-view.test.ts +722 -0
- package/src/text-buffer-view.ts +193 -0
- package/src/text-buffer.test.ts +512 -0
- package/src/text-buffer.ts +250 -0
- package/src/types.ts +199 -0
- package/src/utils.ts +88 -0
- package/src/yoga.ts +1146 -0
- package/src/zig/ansi.zig +502 -0
- package/src/zig/audio.zig +1206 -0
- package/src/zig/bench/README.md +50 -0
- package/src/zig/bench/buffer-color-blending_bench.zig +350 -0
- package/src/zig/bench/buffer-draw-box_bench.zig +786 -0
- package/src/zig/bench/buffer-draw-text-buffer_bench.zig +894 -0
- package/src/zig/bench/edit-buffer_bench.zig +476 -0
- package/src/zig/bench/editor-view_bench.zig +169 -0
- package/src/zig/bench/native-span-feed_bench.zig +107 -0
- package/src/zig/bench/rope-markers_bench.zig +713 -0
- package/src/zig/bench/rope_bench.zig +514 -0
- package/src/zig/bench/styled-text_bench.zig +476 -0
- package/src/zig/bench/text-buffer-coords_bench.zig +362 -0
- package/src/zig/bench/text-buffer-view_bench.zig +459 -0
- package/src/zig/bench/text-chunk-graphemes_bench.zig +272 -0
- package/src/zig/bench/utf8_bench.zig +799 -0
- package/src/zig/bench-utils.zig +431 -0
- package/src/zig/bench.zig +223 -0
- package/src/zig/buffer-methods.zig +215 -0
- package/src/zig/buffer.zig +2515 -0
- package/src/zig/build.zig +700 -0
- package/src/zig/build.zig.zon +19 -0
- package/src/zig/edit-buffer.zig +900 -0
- package/src/zig/editor-view.zig +812 -0
- package/src/zig/event-bus.zig +28 -0
- package/src/zig/event-emitter.zig +65 -0
- package/src/zig/file-logger.zig +92 -0
- package/src/zig/grapheme.zig +838 -0
- package/src/zig/handles.zig +267 -0
- package/src/zig/lib.zig +2813 -0
- package/src/zig/link.zig +349 -0
- package/src/zig/logger.zig +43 -0
- package/src/zig/mem-registry.zig +126 -0
- package/src/zig/miniaudio_shim.c +21 -0
- package/src/zig/native-renderable.zig +112 -0
- package/src/zig/native-span-feed-bench-lib.zig +7 -0
- package/src/zig/native-span-feed.zig +747 -0
- package/src/zig/renderer-output.zig +677 -0
- package/src/zig/renderer.zig +2106 -0
- package/src/zig/rope.zig +1220 -0
- package/src/zig/split-scrollback.zig +88 -0
- package/src/zig/syntax-style.zig +174 -0
- package/src/zig/terminal.zig +1652 -0
- package/src/zig/test.zig +82 -0
- package/src/zig/tests/README.md +18 -0
- package/src/zig/tests/ansi_test.zig +18 -0
- package/src/zig/tests/audio_test.zig +439 -0
- package/src/zig/tests/buffer-methods_test.zig +1111 -0
- package/src/zig/tests/buffer_test.zig +2926 -0
- package/src/zig/tests/edit-buffer-history_test.zig +299 -0
- package/src/zig/tests/edit-buffer_test.zig +1688 -0
- package/src/zig/tests/editor-view_test.zig +3454 -0
- package/src/zig/tests/event-emitter_test.zig +249 -0
- package/src/zig/tests/grapheme_test.zig +1329 -0
- package/src/zig/tests/handles_test.zig +123 -0
- package/src/zig/tests/link_test.zig +220 -0
- package/src/zig/tests/mem-registry_test.zig +473 -0
- package/src/zig/tests/memory_leak_regression_test.zig +234 -0
- package/src/zig/tests/native-renderable_test.zig +16 -0
- package/src/zig/tests/native-span-feed_test.zig +1266 -0
- package/src/zig/tests/renderer_test.zig +2604 -0
- package/src/zig/tests/rope-nested_test.zig +712 -0
- package/src/zig/tests/rope_fuzz_test.zig +238 -0
- package/src/zig/tests/rope_test.zig +2362 -0
- package/src/zig/tests/segment-merge.test.zig +148 -0
- package/src/zig/tests/split-scrollback_test.zig +70 -0
- package/src/zig/tests/syntax-style_test.zig +558 -0
- package/src/zig/tests/terminal_test.zig +1489 -0
- package/src/zig/tests/test-renderer.zig +126 -0
- package/src/zig/tests/text-buffer-drawing_test.zig +3291 -0
- package/src/zig/tests/text-buffer-highlights_test.zig +706 -0
- package/src/zig/tests/text-buffer-iterators_test.zig +776 -0
- package/src/zig/tests/text-buffer-segment_test.zig +320 -0
- package/src/zig/tests/text-buffer-selection_test.zig +1056 -0
- package/src/zig/tests/text-buffer-selection_viewport_test.zig +358 -0
- package/src/zig/tests/text-buffer-view_test.zig +3672 -0
- package/src/zig/tests/text-buffer_test.zig +2191 -0
- package/src/zig/tests/unicode-width-map.zon +3909 -0
- package/src/zig/tests/utf8_no_zwj_test.zig +260 -0
- package/src/zig/tests/utf8_test.zig +4060 -0
- package/src/zig/tests/utf8_wcwidth_cursor_test.zig +267 -0
- package/src/zig/tests/utf8_wcwidth_test.zig +357 -0
- package/src/zig/tests/word-wrap-editing_test.zig +498 -0
- package/src/zig/tests/wrap-cache-perf_test.zig +113 -0
- package/src/zig/tests/yoga_test.zig +65 -0
- package/src/zig/text-buffer-iterators.zig +496 -0
- package/src/zig/text-buffer-segment.zig +403 -0
- package/src/zig/text-buffer-view.zig +1428 -0
- package/src/zig/text-buffer.zig +1272 -0
- package/src/zig/utf8.zig +1954 -0
- package/src/zig/utils.zig +12 -0
- package/src/zig/uucode-stub.zig +76 -0
- package/src/zig/vendor/miniaudio/miniaudio.h +93468 -0
- package/src/zig/yoga.zig +658 -0
- package/src/zig-structs.ts +381 -0
- package/src/zig.ts +5260 -0
- package/tsconfig.build.json +17 -0
- package/tsconfig.json +27 -0
- package/tsconfig.node-test.json +138 -0
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
import { test, expect, afterEach } from "bun:test"
|
|
2
|
+
import { Writable } from "stream"
|
|
3
|
+
import { createCliRenderer, CliRenderer, CliRenderEvents } from "../renderer.js"
|
|
4
|
+
import { ManualClock } from "../testing/manual-clock.js"
|
|
5
|
+
import { createTestStdin, TestWriteStream } from "../testing/test-streams.js"
|
|
6
|
+
|
|
7
|
+
// Collecting Writable used as a mock stdout. Because it is !== process.stdout,
|
|
8
|
+
// createCliRenderer allocates a NativeSpanFeed and pipes bytes through it.
|
|
9
|
+
class CollectingWriteStream extends TestWriteStream {
|
|
10
|
+
public readonly writes: Buffer[] = []
|
|
11
|
+
/** When > 0, delay the write callback by this many ms to simulate a slow consumer. */
|
|
12
|
+
public delayMs = 0
|
|
13
|
+
|
|
14
|
+
override _write(chunk: any, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
|
|
15
|
+
// Defensive copy: `Buffer.from(Uint8Array)` can alias the source's
|
|
16
|
+
// underlying ArrayBuffer. For feed-backed renderers the source is a view
|
|
17
|
+
// into Zig-owned chunk memory that is freed when the feed closes. Copy
|
|
18
|
+
// into a standalone Buffer so reads in assertions are safe after teardown.
|
|
19
|
+
const buf = Buffer.isBuffer(chunk) ? Buffer.from(chunk) : Buffer.from(chunk.slice())
|
|
20
|
+
this.writes.push(buf)
|
|
21
|
+
if (this.delayMs > 0) {
|
|
22
|
+
setTimeout(callback, this.delayMs)
|
|
23
|
+
} else {
|
|
24
|
+
callback()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getWrittenBytes(): Buffer {
|
|
29
|
+
return Buffer.concat(this.writes)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
clearWrites(): void {
|
|
33
|
+
this.writes.length = 0
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type CollectingStdout = CollectingWriteStream & NodeJS.WriteStream
|
|
38
|
+
|
|
39
|
+
function createCollectingStdout(columns = 80, rows = 24): CollectingStdout {
|
|
40
|
+
return new CollectingWriteStream(columns, rows) as CollectingStdout
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createPlainStdout(): NodeJS.WriteStream {
|
|
44
|
+
return new Writable({
|
|
45
|
+
write(_c, _e, cb) {
|
|
46
|
+
cb()
|
|
47
|
+
},
|
|
48
|
+
}) as NodeJS.WriteStream
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createRetryRenderer(feedBacked = false): { renderer: CliRenderer; clock: ManualClock } {
|
|
52
|
+
const clock = new ManualClock()
|
|
53
|
+
const renderer = new CliRenderer(
|
|
54
|
+
createTestStdin(),
|
|
55
|
+
feedBacked ? createCollectingStdout() : createPlainStdout(),
|
|
56
|
+
80,
|
|
57
|
+
24,
|
|
58
|
+
{
|
|
59
|
+
consoleMode: "disabled",
|
|
60
|
+
bufferedOutput: feedBacked ? undefined : "memory",
|
|
61
|
+
clock,
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
;(renderer as any).updateScheduled = false
|
|
65
|
+
clock.runAll()
|
|
66
|
+
destroyFns.push(() => renderer.destroy())
|
|
67
|
+
return { renderer, clock }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function mockNativeRender(renderer: CliRenderer, render: (...args: any[]) => number): void {
|
|
71
|
+
const rendererAny = renderer as any
|
|
72
|
+
const originalRender = rendererAny.lib.render
|
|
73
|
+
rendererAny.lib.render = render
|
|
74
|
+
destroyFns.unshift(() => {
|
|
75
|
+
rendererAny.lib.render = originalRender
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function deferFeedIdle(renderer: CliRenderer): { resolve: () => Promise<void>; calls: () => number } {
|
|
80
|
+
const feed = (renderer as any)._feed
|
|
81
|
+
const originalIdle = feed.idle.bind(feed)
|
|
82
|
+
let resolve = () => {}
|
|
83
|
+
let calls = 0
|
|
84
|
+
feed.idle = () => {
|
|
85
|
+
calls++
|
|
86
|
+
return new Promise<void>((done) => {
|
|
87
|
+
resolve = done
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
destroyFns.unshift(() => {
|
|
91
|
+
feed.idle = originalIdle
|
|
92
|
+
})
|
|
93
|
+
return {
|
|
94
|
+
resolve: async () => {
|
|
95
|
+
resolve()
|
|
96
|
+
await Promise.resolve()
|
|
97
|
+
await Promise.resolve()
|
|
98
|
+
},
|
|
99
|
+
calls: () => calls,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function forceNativeSplitSkip(renderer: CliRenderer): () => void {
|
|
104
|
+
const rendererAny = renderer as any
|
|
105
|
+
const originalCommit = rendererAny.lib.commitSplitFooterSnapshot.bind(rendererAny.lib)
|
|
106
|
+
|
|
107
|
+
rendererAny.lib.commitSplitFooterSnapshot = () => ({ renderOffset: rendererAny.renderOffset, status: 1 })
|
|
108
|
+
|
|
109
|
+
return () => {
|
|
110
|
+
rendererAny.lib.commitSplitFooterSnapshot = originalCommit
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let destroyFns: Array<() => void> = []
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
for (const fn of destroyFns) {
|
|
118
|
+
try {
|
|
119
|
+
fn()
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.error("cleanup error:", e)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
destroyFns = []
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// ---- Byte-routing behavior ----
|
|
128
|
+
|
|
129
|
+
test("non-process stdout: rendered bytes flow to the custom Writable", async () => {
|
|
130
|
+
const stdin = createTestStdin()
|
|
131
|
+
const stdout = createCollectingStdout(80, 24)
|
|
132
|
+
|
|
133
|
+
const renderer = await createCliRenderer({
|
|
134
|
+
stdin,
|
|
135
|
+
stdout,
|
|
136
|
+
})
|
|
137
|
+
destroyFns.push(() => renderer.destroy())
|
|
138
|
+
|
|
139
|
+
// Let setup writes settle.
|
|
140
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 30))
|
|
141
|
+
|
|
142
|
+
const received = stdout.getWrittenBytes()
|
|
143
|
+
expect(received.length).toBeGreaterThan(0)
|
|
144
|
+
// ANSI escape sequences contain ESC (0x1b).
|
|
145
|
+
expect(received.includes(0x1b)).toBe(true)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test("split-footer custom stdout: native feed bytes bypass stdout capture", async () => {
|
|
149
|
+
const stdin = createTestStdin()
|
|
150
|
+
const stdout = createCollectingStdout(80, 24)
|
|
151
|
+
|
|
152
|
+
// Construct directly so the test isolates the feed/write bridge without
|
|
153
|
+
// setupTerminal() adding unrelated startup ANSI.
|
|
154
|
+
const renderer = new CliRenderer(stdin, stdout, 80, 24, {
|
|
155
|
+
screenMode: "split-footer",
|
|
156
|
+
consoleMode: "disabled",
|
|
157
|
+
})
|
|
158
|
+
destroyFns.push(() => renderer.destroy())
|
|
159
|
+
|
|
160
|
+
stdout.clearWrites()
|
|
161
|
+
|
|
162
|
+
renderer.setTerminalTitle("split-footer custom stdout")
|
|
163
|
+
|
|
164
|
+
// Renderer-owned ANSI must go straight to the sink, not back through the
|
|
165
|
+
// split-footer stdout-capture queue.
|
|
166
|
+
expect((renderer as any).externalOutputQueue.size).toBe(0)
|
|
167
|
+
|
|
168
|
+
await new Promise<void>((resolve) => setImmediate(resolve))
|
|
169
|
+
|
|
170
|
+
expect(stdout.getWrittenBytes().toString("binary")).toContain("\x1b]0;split-footer custom stdout\x07")
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test("custom stdout resetTerminalBgColor routes through configured stdout", async () => {
|
|
174
|
+
const stdin = createTestStdin()
|
|
175
|
+
const stdout = createCollectingStdout(80, 24)
|
|
176
|
+
|
|
177
|
+
const renderer = new CliRenderer(stdin, stdout, 80, 24, {
|
|
178
|
+
consoleMode: "disabled",
|
|
179
|
+
})
|
|
180
|
+
destroyFns.push(() => renderer.destroy())
|
|
181
|
+
|
|
182
|
+
stdout.clearWrites()
|
|
183
|
+
renderer.resetTerminalBgColor()
|
|
184
|
+
|
|
185
|
+
await new Promise<void>((resolve) => setImmediate(resolve))
|
|
186
|
+
|
|
187
|
+
expect(stdout.getWrittenBytes().toString("binary")).toContain("\x1b]111\x07")
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test("process.stdout: no feed is allocated (stdout-direct path)", async () => {
|
|
191
|
+
const renderer = await createCliRenderer({
|
|
192
|
+
stdin: process.stdin,
|
|
193
|
+
stdout: process.stdout,
|
|
194
|
+
bufferedOutput: "memory",
|
|
195
|
+
})
|
|
196
|
+
// Direct private-field inspection: no feed should be allocated when output
|
|
197
|
+
// goes straight to process.stdout.
|
|
198
|
+
expect((renderer as any)._feed).toBeNull()
|
|
199
|
+
expect(() => renderer.destroy()).not.toThrow()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test("feed-backed renderer retries one skipped frame after feed idle", async () => {
|
|
203
|
+
const { renderer, clock } = createRetryRenderer(true)
|
|
204
|
+
const idle = deferFeedIdle(renderer)
|
|
205
|
+
let calls = 0
|
|
206
|
+
let frames = 0
|
|
207
|
+
mockNativeRender(renderer, () => (calls++ === 0 ? 1 : 0))
|
|
208
|
+
renderer.on(CliRenderEvents.FRAME, () => frames++)
|
|
209
|
+
|
|
210
|
+
await (renderer as any).loop()
|
|
211
|
+
expect(calls).toBe(1)
|
|
212
|
+
expect(frames).toBe(0)
|
|
213
|
+
expect(idle.calls()).toBe(1)
|
|
214
|
+
|
|
215
|
+
await idle.resolve()
|
|
216
|
+
expect(renderer.getSchedulerState().hasScheduledRender).toBe(true)
|
|
217
|
+
clock.advance(17)
|
|
218
|
+
|
|
219
|
+
expect(calls).toBe(2)
|
|
220
|
+
expect(frames).toBe(1)
|
|
221
|
+
expect(renderer.getSchedulerState().hasScheduledRender).toBe(false)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test("feed-backed renderer retries immediately when feed pressure outlasts the frame interval", async () => {
|
|
225
|
+
const { renderer, clock } = createRetryRenderer(true)
|
|
226
|
+
const idle = deferFeedIdle(renderer)
|
|
227
|
+
let calls = 0
|
|
228
|
+
mockNativeRender(renderer, () => (calls++ === 0 ? 1 : 0))
|
|
229
|
+
|
|
230
|
+
await (renderer as any).loop()
|
|
231
|
+
clock.advance(100)
|
|
232
|
+
await idle.resolve()
|
|
233
|
+
clock.advance(0)
|
|
234
|
+
|
|
235
|
+
expect(calls).toBe(2)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
test("feed-backed renderer coalesces requests while waiting for feed idle", async () => {
|
|
239
|
+
const { renderer, clock } = createRetryRenderer(true)
|
|
240
|
+
const idle = deferFeedIdle(renderer)
|
|
241
|
+
const observed: number[] = []
|
|
242
|
+
let state = 1
|
|
243
|
+
let calls = 0
|
|
244
|
+
renderer.setFrameCallback(async () => {
|
|
245
|
+
observed.push(state)
|
|
246
|
+
})
|
|
247
|
+
mockNativeRender(renderer, () => (calls++ === 0 ? 1 : 0))
|
|
248
|
+
|
|
249
|
+
await (renderer as any).loop()
|
|
250
|
+
state = 2
|
|
251
|
+
renderer.requestRender()
|
|
252
|
+
renderer.requestRender()
|
|
253
|
+
renderer.requestRender()
|
|
254
|
+
expect(calls).toBe(1)
|
|
255
|
+
|
|
256
|
+
await idle.resolve()
|
|
257
|
+
clock.advance(17)
|
|
258
|
+
await Promise.resolve()
|
|
259
|
+
|
|
260
|
+
expect(calls).toBe(2)
|
|
261
|
+
expect(observed).toEqual([1, 2])
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test("starting a feed-backed renderer waits for a skipped frame's feed idle", async () => {
|
|
265
|
+
const { renderer, clock } = createRetryRenderer(true)
|
|
266
|
+
const idle = deferFeedIdle(renderer)
|
|
267
|
+
let calls = 0
|
|
268
|
+
mockNativeRender(renderer, () => (calls++ === 0 ? 1 : 0))
|
|
269
|
+
|
|
270
|
+
await (renderer as any).loop()
|
|
271
|
+
renderer.start()
|
|
272
|
+
|
|
273
|
+
expect(renderer.isRunning).toBe(true)
|
|
274
|
+
clock.advance(100)
|
|
275
|
+
expect(calls).toBe(1)
|
|
276
|
+
|
|
277
|
+
await idle.resolve()
|
|
278
|
+
clock.advance(0)
|
|
279
|
+
|
|
280
|
+
expect(calls).toBe(2)
|
|
281
|
+
expect(renderer.isRunning).toBe(true)
|
|
282
|
+
renderer.pause()
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test("feed-backed renderer waits for each repeated skip", async () => {
|
|
286
|
+
const { renderer, clock } = createRetryRenderer(true)
|
|
287
|
+
const firstIdle = deferFeedIdle(renderer)
|
|
288
|
+
let calls = 0
|
|
289
|
+
mockNativeRender(renderer, () => (calls++ < 2 ? 1 : 0))
|
|
290
|
+
|
|
291
|
+
await (renderer as any).loop()
|
|
292
|
+
await firstIdle.resolve()
|
|
293
|
+
clock.advance(17)
|
|
294
|
+
expect(calls).toBe(2)
|
|
295
|
+
expect(firstIdle.calls()).toBe(2)
|
|
296
|
+
|
|
297
|
+
await firstIdle.resolve()
|
|
298
|
+
clock.advance(17)
|
|
299
|
+
expect(calls).toBe(3)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test("native failure does not retry and recovers on a later render request", async () => {
|
|
303
|
+
const { renderer, clock } = createRetryRenderer()
|
|
304
|
+
const originalError = console.error
|
|
305
|
+
const errors: unknown[][] = []
|
|
306
|
+
console.error = (...args: unknown[]) => errors.push(args)
|
|
307
|
+
destroyFns.unshift(() => {
|
|
308
|
+
console.error = originalError
|
|
309
|
+
})
|
|
310
|
+
let calls = 0
|
|
311
|
+
let frames = 0
|
|
312
|
+
mockNativeRender(renderer, () => (calls++ === 0 ? 2 : 0))
|
|
313
|
+
renderer.on(CliRenderEvents.FRAME, () => frames++)
|
|
314
|
+
|
|
315
|
+
await (renderer as any).loop()
|
|
316
|
+
clock.advance(1000)
|
|
317
|
+
expect(calls).toBe(1)
|
|
318
|
+
expect(frames).toBe(0)
|
|
319
|
+
expect(errors).toHaveLength(1)
|
|
320
|
+
expect(renderer.getSchedulerState().hasScheduledRender).toBe(false)
|
|
321
|
+
|
|
322
|
+
renderer.intermediateRender()
|
|
323
|
+
|
|
324
|
+
expect(calls).toBe(2)
|
|
325
|
+
expect(frames).toBe(1)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test("running renderer recovers from native failure on a later render request", async () => {
|
|
329
|
+
const { renderer, clock } = createRetryRenderer()
|
|
330
|
+
const originalError = console.error
|
|
331
|
+
console.error = () => {}
|
|
332
|
+
destroyFns.unshift(() => {
|
|
333
|
+
console.error = originalError
|
|
334
|
+
})
|
|
335
|
+
let calls = 0
|
|
336
|
+
mockNativeRender(renderer, () => (calls++ === 0 ? 2 : 0))
|
|
337
|
+
|
|
338
|
+
renderer.start()
|
|
339
|
+
expect(calls).toBe(1)
|
|
340
|
+
expect(renderer.isRunning).toBe(true)
|
|
341
|
+
|
|
342
|
+
renderer.requestRender()
|
|
343
|
+
clock.advance(17)
|
|
344
|
+
|
|
345
|
+
expect(calls).toBe(2)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
test("feed-backed native failure does not wait for feed idle or retry", async () => {
|
|
349
|
+
const { renderer, clock } = createRetryRenderer(true)
|
|
350
|
+
const idle = deferFeedIdle(renderer)
|
|
351
|
+
const originalError = console.error
|
|
352
|
+
console.error = () => {}
|
|
353
|
+
destroyFns.unshift(() => {
|
|
354
|
+
console.error = originalError
|
|
355
|
+
})
|
|
356
|
+
let calls = 0
|
|
357
|
+
mockNativeRender(renderer, () => {
|
|
358
|
+
calls++
|
|
359
|
+
return 2
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
await (renderer as any).loop()
|
|
363
|
+
clock.advance(1000)
|
|
364
|
+
|
|
365
|
+
expect(calls).toBe(1)
|
|
366
|
+
expect(idle.calls()).toBe(0)
|
|
367
|
+
expect(renderer.getSchedulerState().hasScheduledRender).toBe(false)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test("unexpected skip without a feed does not retry", async () => {
|
|
371
|
+
const { renderer, clock } = createRetryRenderer()
|
|
372
|
+
const originalError = console.error
|
|
373
|
+
const errors: unknown[][] = []
|
|
374
|
+
console.error = (...args: unknown[]) => errors.push(args)
|
|
375
|
+
destroyFns.unshift(() => {
|
|
376
|
+
console.error = originalError
|
|
377
|
+
})
|
|
378
|
+
let calls = 0
|
|
379
|
+
mockNativeRender(renderer, () => {
|
|
380
|
+
calls++
|
|
381
|
+
return 1
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
await (renderer as any).loop()
|
|
385
|
+
clock.advance(1000)
|
|
386
|
+
|
|
387
|
+
expect(calls).toBe(1)
|
|
388
|
+
expect(errors).toHaveLength(1)
|
|
389
|
+
expect(renderer.getSchedulerState().hasScheduledRender).toBe(false)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
for (const control of ["pause", "stop", "suspend", "destroy"] as const) {
|
|
393
|
+
test(`${control} cancels a feed-idle retry`, async () => {
|
|
394
|
+
const { renderer, clock } = createRetryRenderer(true)
|
|
395
|
+
const idle = deferFeedIdle(renderer)
|
|
396
|
+
let calls = 0
|
|
397
|
+
mockNativeRender(renderer, () => {
|
|
398
|
+
calls++
|
|
399
|
+
return 1
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
await (renderer as any).loop()
|
|
403
|
+
renderer[control]()
|
|
404
|
+
await idle.resolve()
|
|
405
|
+
clock.advance(17)
|
|
406
|
+
|
|
407
|
+
expect(calls).toBe(1)
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (const [control, state] of [
|
|
412
|
+
["pause", "paused"],
|
|
413
|
+
["stop", "stopped"],
|
|
414
|
+
] as const) {
|
|
415
|
+
test(`one-shot render requested while ${state} retries after feed idle`, async () => {
|
|
416
|
+
const { renderer, clock } = createRetryRenderer(true)
|
|
417
|
+
const idle = deferFeedIdle(renderer)
|
|
418
|
+
let calls = 0
|
|
419
|
+
mockNativeRender(renderer, () => (calls++ === 0 ? 1 : 0))
|
|
420
|
+
|
|
421
|
+
renderer[control]()
|
|
422
|
+
renderer.requestRender()
|
|
423
|
+
clock.advance(17)
|
|
424
|
+
expect(calls).toBe(1)
|
|
425
|
+
|
|
426
|
+
await idle.resolve()
|
|
427
|
+
clock.advance(17)
|
|
428
|
+
|
|
429
|
+
expect(calls).toBe(2)
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
test("cancelling a skipped frame with an immediate rerender request resolves idle", async () => {
|
|
434
|
+
const { renderer } = createRetryRenderer(true)
|
|
435
|
+
const idle = deferFeedIdle(renderer)
|
|
436
|
+
renderer.setFrameCallback(async () => {
|
|
437
|
+
renderer.requestRender()
|
|
438
|
+
})
|
|
439
|
+
mockNativeRender(renderer, () => 1)
|
|
440
|
+
|
|
441
|
+
await (renderer as any).loop()
|
|
442
|
+
renderer.pause()
|
|
443
|
+
const idlePromise = renderer.idle()
|
|
444
|
+
let idleResolved = false
|
|
445
|
+
void idlePromise.then(() => {
|
|
446
|
+
idleResolved = true
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
await idle.resolve()
|
|
450
|
+
await Promise.resolve()
|
|
451
|
+
|
|
452
|
+
expect(idleResolved).toBe(true)
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
test("running renderer resumes after feed idle", async () => {
|
|
456
|
+
const { renderer, clock } = createRetryRenderer(true)
|
|
457
|
+
const idle = deferFeedIdle(renderer)
|
|
458
|
+
let calls = 0
|
|
459
|
+
mockNativeRender(renderer, () => (calls++ === 0 ? 1 : 0))
|
|
460
|
+
|
|
461
|
+
renderer.start()
|
|
462
|
+
expect(calls).toBe(1)
|
|
463
|
+
await idle.resolve()
|
|
464
|
+
clock.advance(17)
|
|
465
|
+
|
|
466
|
+
expect(calls).toBe(2)
|
|
467
|
+
expect(renderer.isRunning).toBe(true)
|
|
468
|
+
renderer.pause()
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
test("omitting stdin/stdout uses process streams", async () => {
|
|
472
|
+
const renderer = await createCliRenderer({
|
|
473
|
+
bufferedOutput: "memory",
|
|
474
|
+
})
|
|
475
|
+
expect(renderer.stdin).toBe(process.stdin)
|
|
476
|
+
destroyFns.push(() => renderer.destroy())
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
test("custom stdout defaults to remote env behavior", async () => {
|
|
480
|
+
const previous = process.env.OPENTUI_FORCE_WCWIDTH
|
|
481
|
+
process.env.OPENTUI_FORCE_WCWIDTH = "1"
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const defaultRemoteRenderer = await createCliRenderer({
|
|
485
|
+
stdin: createTestStdin(),
|
|
486
|
+
stdout: createCollectingStdout(80, 24),
|
|
487
|
+
})
|
|
488
|
+
destroyFns.push(() => defaultRemoteRenderer.destroy())
|
|
489
|
+
|
|
490
|
+
expect(defaultRemoteRenderer.widthMethod).toBe("unicode")
|
|
491
|
+
|
|
492
|
+
const localRenderer = await createCliRenderer({
|
|
493
|
+
stdin: createTestStdin(),
|
|
494
|
+
stdout: createCollectingStdout(80, 24),
|
|
495
|
+
remote: false,
|
|
496
|
+
})
|
|
497
|
+
destroyFns.push(() => localRenderer.destroy())
|
|
498
|
+
|
|
499
|
+
expect(localRenderer.widthMethod).toBe("wcwidth")
|
|
500
|
+
} finally {
|
|
501
|
+
if (previous === undefined) {
|
|
502
|
+
delete process.env.OPENTUI_FORCE_WCWIDTH
|
|
503
|
+
} else {
|
|
504
|
+
process.env.OPENTUI_FORCE_WCWIDTH = previous
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
// ---- Shutdown bytes reach the remote Writable (F1 regression test) ----
|
|
510
|
+
|
|
511
|
+
test("destroy emits shutdown ANSI sequence through the custom Writable", async () => {
|
|
512
|
+
const stdin = createTestStdin()
|
|
513
|
+
const stdout = createCollectingStdout(80, 24)
|
|
514
|
+
|
|
515
|
+
const renderer = await createCliRenderer({
|
|
516
|
+
stdin,
|
|
517
|
+
stdout,
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
// Let setup output settle, then clear so we can isolate shutdown output.
|
|
521
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 30))
|
|
522
|
+
stdout.clearWrites()
|
|
523
|
+
|
|
524
|
+
renderer.destroy()
|
|
525
|
+
|
|
526
|
+
// Let final writes settle.
|
|
527
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 50))
|
|
528
|
+
|
|
529
|
+
const shutdownBytes = stdout.getWrittenBytes().toString("binary")
|
|
530
|
+
|
|
531
|
+
// The shutdown sequence must include at least:
|
|
532
|
+
// - showCursor (ANSI.showCursor = ESC[?25h) so the user isn't left with a hidden cursor
|
|
533
|
+
// - either the reset-cursor-color sequence or the default-cursor-style sequence
|
|
534
|
+
// This is the regression test for the teardown-order bug where the data
|
|
535
|
+
// handler was detached before destroyRenderer emitted shutdown, causing
|
|
536
|
+
// those bytes to be discarded.
|
|
537
|
+
expect(shutdownBytes.length).toBeGreaterThan(0)
|
|
538
|
+
expect(shutdownBytes).toContain("\x1b[?25h") // showCursor
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
// ---- Backpressure ----
|
|
542
|
+
|
|
543
|
+
test("slow Writable marks feed as backpressured until write callback settles", async () => {
|
|
544
|
+
const stdin = createTestStdin()
|
|
545
|
+
const stdout = createCollectingStdout(80, 24)
|
|
546
|
+
stdout.delayMs = 50
|
|
547
|
+
|
|
548
|
+
const renderer = await createCliRenderer({
|
|
549
|
+
stdin,
|
|
550
|
+
stdout,
|
|
551
|
+
})
|
|
552
|
+
destroyFns.push(() => {
|
|
553
|
+
stdout.delayMs = 0
|
|
554
|
+
renderer.destroy()
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
const feed = (renderer as any)._feed
|
|
558
|
+
expect(feed).not.toBeNull()
|
|
559
|
+
|
|
560
|
+
renderer.setTerminalTitle("slow-write")
|
|
561
|
+
await new Promise<void>((resolve) => setImmediate(resolve))
|
|
562
|
+
|
|
563
|
+
expect(feed.isBackpressured()).toBe(true)
|
|
564
|
+
|
|
565
|
+
stdout.delayMs = 0
|
|
566
|
+
await feed.idle()
|
|
567
|
+
|
|
568
|
+
expect(feed.isBackpressured()).toBe(false)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
test("split-footer custom stdout can flush captured commits while feed writes are in flight", async () => {
|
|
572
|
+
const stdin = createTestStdin()
|
|
573
|
+
const stdout = createCollectingStdout(80, 24)
|
|
574
|
+
stdout.delayMs = 100
|
|
575
|
+
|
|
576
|
+
const renderer = new CliRenderer(stdin, stdout, 80, 24, {
|
|
577
|
+
screenMode: "split-footer",
|
|
578
|
+
consoleMode: "disabled",
|
|
579
|
+
})
|
|
580
|
+
destroyFns.push(() => {
|
|
581
|
+
stdout.delayMs = 0
|
|
582
|
+
renderer.destroy()
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
const feed = (renderer as any)._feed
|
|
586
|
+
expect(feed).not.toBeNull()
|
|
587
|
+
|
|
588
|
+
renderer.setTerminalTitle("pin-feed")
|
|
589
|
+
await new Promise<void>((resolve) => setImmediate(resolve))
|
|
590
|
+
expect(feed.isBackpressured()).toBe(true)
|
|
591
|
+
|
|
592
|
+
stdout.write("captured\n")
|
|
593
|
+
await (renderer as any).loop()
|
|
594
|
+
|
|
595
|
+
expect((renderer as any).externalOutputQueue.size).toBe(0)
|
|
596
|
+
|
|
597
|
+
stdout.delayMs = 0
|
|
598
|
+
await feed.idle()
|
|
599
|
+
expect(stdout.getWrittenBytes().toString("binary")).toContain("captured")
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
test("split-footer custom stdout retains captured commits when native skips", async () => {
|
|
603
|
+
const stdin = createTestStdin()
|
|
604
|
+
const stdout = createCollectingStdout(80, 24)
|
|
605
|
+
|
|
606
|
+
const renderer = new CliRenderer(stdin, stdout, 80, 24, {
|
|
607
|
+
screenMode: "split-footer",
|
|
608
|
+
consoleMode: "disabled",
|
|
609
|
+
})
|
|
610
|
+
destroyFns.push(() => renderer.destroy())
|
|
611
|
+
|
|
612
|
+
const restoreNative = forceNativeSplitSkip(renderer)
|
|
613
|
+
|
|
614
|
+
stdout.write("captured-while-native-skipped\n")
|
|
615
|
+
const rendererAny = renderer as any
|
|
616
|
+
expect(rendererAny.externalOutputQueue.size).toBeGreaterThan(0)
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
await rendererAny.loop()
|
|
620
|
+
expect(rendererAny.externalOutputQueue.size).toBeGreaterThan(0)
|
|
621
|
+
} finally {
|
|
622
|
+
restoreNative()
|
|
623
|
+
}
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
test("split-footer coalesces render requests while waiting for feed idle", async () => {
|
|
627
|
+
const clock = new ManualClock()
|
|
628
|
+
const stdout = createCollectingStdout(80, 24)
|
|
629
|
+
const renderer = new CliRenderer(createTestStdin(), stdout, 80, 24, {
|
|
630
|
+
screenMode: "split-footer",
|
|
631
|
+
consoleMode: "disabled",
|
|
632
|
+
clock,
|
|
633
|
+
})
|
|
634
|
+
;(renderer as any).updateScheduled = false
|
|
635
|
+
clock.runAll()
|
|
636
|
+
destroyFns.push(() => renderer.destroy())
|
|
637
|
+
|
|
638
|
+
const idle = deferFeedIdle(renderer)
|
|
639
|
+
const rendererAny = renderer as any
|
|
640
|
+
const originalCommit = rendererAny.lib.commitSplitFooterSnapshot
|
|
641
|
+
let calls = 0
|
|
642
|
+
rendererAny.lib.commitSplitFooterSnapshot = () => {
|
|
643
|
+
calls++
|
|
644
|
+
return { renderOffset: rendererAny.renderOffset, status: calls === 1 ? 1 : 0 }
|
|
645
|
+
}
|
|
646
|
+
destroyFns.unshift(() => {
|
|
647
|
+
rendererAny.lib.commitSplitFooterSnapshot = originalCommit
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
stdout.write("first\n")
|
|
651
|
+
clock.advance(17)
|
|
652
|
+
await Promise.resolve()
|
|
653
|
+
expect(calls).toBe(1)
|
|
654
|
+
|
|
655
|
+
stdout.write("second\n")
|
|
656
|
+
renderer.requestRender()
|
|
657
|
+
clock.advance(100)
|
|
658
|
+
await Promise.resolve()
|
|
659
|
+
|
|
660
|
+
expect(calls).toBe(1)
|
|
661
|
+
expect(idle.calls()).toBe(1)
|
|
662
|
+
expect(rendererAny.externalOutputQueue.size).toBe(2)
|
|
663
|
+
|
|
664
|
+
await idle.resolve()
|
|
665
|
+
clock.advance(0)
|
|
666
|
+
await Promise.resolve()
|
|
667
|
+
|
|
668
|
+
expect(calls).toBe(3)
|
|
669
|
+
expect(rendererAny.externalOutputQueue.size).toBe(0)
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
test("split-footer custom stdout retains captured commits when native fails and retries", async () => {
|
|
673
|
+
const stdin = createTestStdin()
|
|
674
|
+
const stdout = createCollectingStdout(80, 24)
|
|
675
|
+
|
|
676
|
+
const renderer = new CliRenderer(stdin, stdout, 80, 24, {
|
|
677
|
+
screenMode: "split-footer",
|
|
678
|
+
consoleMode: "disabled",
|
|
679
|
+
})
|
|
680
|
+
destroyFns.push(() => renderer.destroy())
|
|
681
|
+
|
|
682
|
+
const rendererAny = renderer as any
|
|
683
|
+
const originalCommit = rendererAny.lib.commitSplitFooterSnapshot.bind(rendererAny.lib)
|
|
684
|
+
let calls = 0
|
|
685
|
+
rendererAny.lib.commitSplitFooterSnapshot = () => {
|
|
686
|
+
calls++
|
|
687
|
+
return { renderOffset: rendererAny.renderOffset, status: 2 }
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
stdout.write("captured-while-native-failed\n")
|
|
691
|
+
expect(rendererAny.externalOutputQueue.size).toBeGreaterThan(0)
|
|
692
|
+
|
|
693
|
+
try {
|
|
694
|
+
await rendererAny.loop()
|
|
695
|
+
expect(calls).toBeGreaterThan(0)
|
|
696
|
+
expect(rendererAny.externalOutputQueue.size).toBeGreaterThan(0)
|
|
697
|
+
} finally {
|
|
698
|
+
rendererAny.lib.commitSplitFooterSnapshot = originalCommit
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
await rendererAny.loop()
|
|
702
|
+
await (rendererAny._feed?.idle() ?? Promise.resolve())
|
|
703
|
+
|
|
704
|
+
expect(rendererAny.externalOutputQueue.size).toBe(0)
|
|
705
|
+
expect(stdout.getWrittenBytes().toString("binary")).toContain("captured-while-native-failed")
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
test("split-footer native failure without a feed does not schedule automatic retries", async () => {
|
|
709
|
+
const clock = new ManualClock()
|
|
710
|
+
const stdout = createPlainStdout()
|
|
711
|
+
const renderer = new CliRenderer(createTestStdin(), stdout, 80, 24, {
|
|
712
|
+
screenMode: "split-footer",
|
|
713
|
+
consoleMode: "disabled",
|
|
714
|
+
bufferedOutput: "memory",
|
|
715
|
+
clock,
|
|
716
|
+
})
|
|
717
|
+
;(renderer as any).updateScheduled = false
|
|
718
|
+
clock.runAll()
|
|
719
|
+
destroyFns.push(() => renderer.destroy())
|
|
720
|
+
|
|
721
|
+
const rendererAny = renderer as any
|
|
722
|
+
const originalCommit = rendererAny.lib.commitSplitFooterSnapshot
|
|
723
|
+
const originalError = console.error
|
|
724
|
+
let calls = 0
|
|
725
|
+
rendererAny.lib.commitSplitFooterSnapshot = () => {
|
|
726
|
+
calls++
|
|
727
|
+
return { renderOffset: rendererAny.renderOffset, status: 2 }
|
|
728
|
+
}
|
|
729
|
+
console.error = () => {}
|
|
730
|
+
destroyFns.unshift(() => {
|
|
731
|
+
rendererAny.lib.commitSplitFooterSnapshot = originalCommit
|
|
732
|
+
console.error = originalError
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
stdout.write("captured-while-native-failed\n")
|
|
736
|
+
rendererAny.updateScheduled = false
|
|
737
|
+
await rendererAny.loop()
|
|
738
|
+
expect(calls).toBe(1)
|
|
739
|
+
|
|
740
|
+
clock.advance(1000)
|
|
741
|
+
|
|
742
|
+
expect(calls).toBe(1)
|
|
743
|
+
expect(renderer.getSchedulerState().hasScheduledRender).toBe(false)
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
test("capture-to-passthrough flushes queued split-footer commits while feed is backpressured", async () => {
|
|
747
|
+
const stdin = createTestStdin()
|
|
748
|
+
const stdout = createCollectingStdout(80, 24)
|
|
749
|
+
stdout.delayMs = 30
|
|
750
|
+
|
|
751
|
+
const renderer = new CliRenderer(stdin, stdout, 80, 24, {
|
|
752
|
+
screenMode: "split-footer",
|
|
753
|
+
consoleMode: "disabled",
|
|
754
|
+
})
|
|
755
|
+
destroyFns.push(() => {
|
|
756
|
+
stdout.delayMs = 0
|
|
757
|
+
renderer.destroy()
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
const feed = (renderer as any)._feed
|
|
761
|
+
expect(feed).not.toBeNull()
|
|
762
|
+
|
|
763
|
+
renderer.setTerminalTitle("pin-feed-before-mode-switch")
|
|
764
|
+
await new Promise<void>((resolve) => setImmediate(resolve))
|
|
765
|
+
expect(feed.isBackpressured()).toBe(true)
|
|
766
|
+
|
|
767
|
+
stdout.write("captured-before-mode-switch\n")
|
|
768
|
+
expect((renderer as any).externalOutputQueue.size).toBeGreaterThan(0)
|
|
769
|
+
|
|
770
|
+
renderer.externalOutputMode = "passthrough"
|
|
771
|
+
stdout.delayMs = 0
|
|
772
|
+
|
|
773
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 80))
|
|
774
|
+
|
|
775
|
+
expect(stdout.getWrittenBytes().toString("binary")).toContain("captured-before-mode-switch")
|
|
776
|
+
expect((renderer as any).externalOutputQueue.size).toBe(0)
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
test("destroy resolves idle waiters when a feed-idle render was scheduled", async () => {
|
|
780
|
+
const stdin = createTestStdin()
|
|
781
|
+
const stdout = createCollectingStdout(80, 24)
|
|
782
|
+
stdout.delayMs = 30
|
|
783
|
+
|
|
784
|
+
const renderer = new CliRenderer(stdin, stdout, 80, 24, {
|
|
785
|
+
screenMode: "split-footer",
|
|
786
|
+
consoleMode: "disabled",
|
|
787
|
+
})
|
|
788
|
+
destroyFns.push(() => {
|
|
789
|
+
stdout.delayMs = 0
|
|
790
|
+
renderer.destroy()
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
const feed = (renderer as any)._feed
|
|
794
|
+
expect(feed).not.toBeNull()
|
|
795
|
+
|
|
796
|
+
renderer.setTerminalTitle("pin-feed-before-idle")
|
|
797
|
+
await new Promise<void>((resolve) => setImmediate(resolve))
|
|
798
|
+
expect(feed.isBackpressured()).toBe(true)
|
|
799
|
+
|
|
800
|
+
const restoreNative = forceNativeSplitSkip(renderer)
|
|
801
|
+
stdout.write("captured-before-idle-destroy\n")
|
|
802
|
+
try {
|
|
803
|
+
await (renderer as any).loop()
|
|
804
|
+
expect((renderer as any).feedIdleRenderScheduled).toBe(true)
|
|
805
|
+
} finally {
|
|
806
|
+
restoreNative()
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
let idleResolved = false
|
|
810
|
+
const idlePromise = renderer.idle().then(() => {
|
|
811
|
+
idleResolved = true
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
renderer.destroy()
|
|
815
|
+
stdout.delayMs = 0
|
|
816
|
+
await Promise.resolve()
|
|
817
|
+
|
|
818
|
+
expect(idleResolved).toBe(true)
|
|
819
|
+
await idlePromise
|
|
820
|
+
|
|
821
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 80))
|
|
822
|
+
expect(stdout.getWrittenBytes().toString("binary")).toContain("captured-before-idle-destroy")
|
|
823
|
+
expect((renderer as any).externalOutputQueue.size).toBe(0)
|
|
824
|
+
})
|
|
825
|
+
|
|
826
|
+
test("suspend resolves idle waiters when a feed-idle render was scheduled", async () => {
|
|
827
|
+
const stdin = createTestStdin()
|
|
828
|
+
const stdout = createCollectingStdout(80, 24)
|
|
829
|
+
stdout.delayMs = 30
|
|
830
|
+
|
|
831
|
+
const renderer = new CliRenderer(stdin, stdout, 80, 24, {
|
|
832
|
+
screenMode: "split-footer",
|
|
833
|
+
consoleMode: "disabled",
|
|
834
|
+
})
|
|
835
|
+
destroyFns.push(() => {
|
|
836
|
+
stdout.delayMs = 0
|
|
837
|
+
renderer.destroy()
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
const feed = (renderer as any)._feed
|
|
841
|
+
expect(feed).not.toBeNull()
|
|
842
|
+
|
|
843
|
+
renderer.setTerminalTitle("pin-feed-before-suspend")
|
|
844
|
+
await new Promise<void>((resolve) => setImmediate(resolve))
|
|
845
|
+
expect(feed.isBackpressured()).toBe(true)
|
|
846
|
+
|
|
847
|
+
const restoreNative = forceNativeSplitSkip(renderer)
|
|
848
|
+
stdout.write("captured-before-suspend\n")
|
|
849
|
+
try {
|
|
850
|
+
await (renderer as any).loop()
|
|
851
|
+
expect((renderer as any).feedIdleRenderScheduled).toBe(true)
|
|
852
|
+
} finally {
|
|
853
|
+
restoreNative()
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
let idleResolved = false
|
|
857
|
+
const idlePromise = renderer.idle().then(() => {
|
|
858
|
+
idleResolved = true
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
renderer.suspend()
|
|
862
|
+
stdout.delayMs = 0
|
|
863
|
+
await feed.idle()
|
|
864
|
+
await Promise.resolve()
|
|
865
|
+
|
|
866
|
+
expect(idleResolved).toBe(true)
|
|
867
|
+
await idlePromise
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
// ---- Dimension fallback ----
|
|
871
|
+
|
|
872
|
+
test("dimensions: stdout.columns wins over config.width", async () => {
|
|
873
|
+
const stdin = createTestStdin()
|
|
874
|
+
const stdout = createCollectingStdout(120, 30)
|
|
875
|
+
|
|
876
|
+
const renderer = await createCliRenderer({
|
|
877
|
+
stdin,
|
|
878
|
+
stdout,
|
|
879
|
+
width: 40,
|
|
880
|
+
height: 10,
|
|
881
|
+
bufferedOutput: "memory",
|
|
882
|
+
})
|
|
883
|
+
destroyFns.push(() => renderer.destroy())
|
|
884
|
+
|
|
885
|
+
expect(renderer.width).toBe(120)
|
|
886
|
+
expect(renderer.height).toBe(30)
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
test("dimensions: config.width used when stdout lacks columns", async () => {
|
|
890
|
+
const stdin = createTestStdin()
|
|
891
|
+
const stdout = createPlainStdout()
|
|
892
|
+
|
|
893
|
+
const renderer = await createCliRenderer({
|
|
894
|
+
stdin,
|
|
895
|
+
stdout,
|
|
896
|
+
width: 100,
|
|
897
|
+
height: 50,
|
|
898
|
+
bufferedOutput: "memory",
|
|
899
|
+
})
|
|
900
|
+
destroyFns.push(() => renderer.destroy())
|
|
901
|
+
|
|
902
|
+
expect(renderer.width).toBe(100)
|
|
903
|
+
expect(renderer.height).toBe(50)
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
test("dimensions: config.width used when stdout reports zero columns", async () => {
|
|
907
|
+
const stdin = createTestStdin()
|
|
908
|
+
const stdout = createCollectingStdout(0, 0)
|
|
909
|
+
|
|
910
|
+
const renderer = await createCliRenderer({
|
|
911
|
+
stdin,
|
|
912
|
+
stdout,
|
|
913
|
+
width: 100,
|
|
914
|
+
height: 50,
|
|
915
|
+
bufferedOutput: "memory",
|
|
916
|
+
})
|
|
917
|
+
destroyFns.push(() => renderer.destroy())
|
|
918
|
+
|
|
919
|
+
expect(renderer.width).toBe(100)
|
|
920
|
+
expect(renderer.height).toBe(50)
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
test("dimensions: defaults 80x24 when no stdout columns and no config", async () => {
|
|
924
|
+
const stdin = createTestStdin()
|
|
925
|
+
const stdout = createPlainStdout()
|
|
926
|
+
|
|
927
|
+
const renderer = await createCliRenderer({
|
|
928
|
+
stdin,
|
|
929
|
+
stdout,
|
|
930
|
+
bufferedOutput: "memory",
|
|
931
|
+
})
|
|
932
|
+
destroyFns.push(() => renderer.destroy())
|
|
933
|
+
|
|
934
|
+
expect(renderer.width).toBe(80)
|
|
935
|
+
expect(renderer.height).toBe(24)
|
|
936
|
+
})
|
|
937
|
+
|
|
938
|
+
test("dimensions: defaults 80x24 when stdout reports zero columns and no config", async () => {
|
|
939
|
+
const stdin = createTestStdin()
|
|
940
|
+
const stdout = createCollectingStdout(0, 0)
|
|
941
|
+
|
|
942
|
+
const renderer = await createCliRenderer({
|
|
943
|
+
stdin,
|
|
944
|
+
stdout,
|
|
945
|
+
bufferedOutput: "memory",
|
|
946
|
+
})
|
|
947
|
+
destroyFns.push(() => renderer.destroy())
|
|
948
|
+
|
|
949
|
+
expect(renderer.width).toBe(80)
|
|
950
|
+
expect(renderer.height).toBe(24)
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
// ---- Duck-typed stream capabilities ----
|
|
954
|
+
|
|
955
|
+
test("stdin without setRawMode: start/suspend/resume/destroy all succeed", async () => {
|
|
956
|
+
const stdin = createTestStdin() // Readable has no setRawMode
|
|
957
|
+
const stdout = createCollectingStdout(80, 24)
|
|
958
|
+
|
|
959
|
+
const renderer = await createCliRenderer({
|
|
960
|
+
stdin,
|
|
961
|
+
stdout,
|
|
962
|
+
bufferedOutput: "memory",
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
expect(() => renderer.suspend()).not.toThrow()
|
|
966
|
+
expect(() => renderer.resume()).not.toThrow()
|
|
967
|
+
expect(() => renderer.destroy()).not.toThrow()
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
// ---- Public resize API ----
|
|
971
|
+
|
|
972
|
+
test("resize(w, h) updates dimensions and fires RESIZE event", async () => {
|
|
973
|
+
const stdin = createTestStdin()
|
|
974
|
+
const stdout = createCollectingStdout(80, 24)
|
|
975
|
+
|
|
976
|
+
const renderer = await createCliRenderer({
|
|
977
|
+
stdin,
|
|
978
|
+
stdout,
|
|
979
|
+
bufferedOutput: "memory",
|
|
980
|
+
})
|
|
981
|
+
destroyFns.push(() => renderer.destroy())
|
|
982
|
+
|
|
983
|
+
let eventFired = false
|
|
984
|
+
let eventW = 0
|
|
985
|
+
let eventH = 0
|
|
986
|
+
renderer.on(CliRenderEvents.RESIZE, (w: number, h: number) => {
|
|
987
|
+
eventFired = true
|
|
988
|
+
eventW = w
|
|
989
|
+
eventH = h
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
renderer.resize(120, 40)
|
|
993
|
+
|
|
994
|
+
expect(eventFired).toBe(true)
|
|
995
|
+
expect(eventW).toBe(120)
|
|
996
|
+
expect(eventH).toBe(40)
|
|
997
|
+
expect(renderer.width).toBe(120)
|
|
998
|
+
expect(renderer.height).toBe(40)
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
test("resize() after destroy is a no-op", async () => {
|
|
1002
|
+
const stdin = createTestStdin()
|
|
1003
|
+
const stdout = createCollectingStdout(80, 24)
|
|
1004
|
+
|
|
1005
|
+
const renderer = await createCliRenderer({
|
|
1006
|
+
stdin,
|
|
1007
|
+
stdout,
|
|
1008
|
+
bufferedOutput: "memory",
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
renderer.destroy()
|
|
1012
|
+
expect(() => renderer.resize(100, 50)).not.toThrow()
|
|
1013
|
+
})
|
|
1014
|
+
|
|
1015
|
+
// ---- Full feed teardown path ----
|
|
1016
|
+
|
|
1017
|
+
test("full feed teardown after successful setup does not throw", async () => {
|
|
1018
|
+
const stdin = createTestStdin()
|
|
1019
|
+
const stdout = createCollectingStdout(80, 24)
|
|
1020
|
+
|
|
1021
|
+
const renderer = await createCliRenderer({
|
|
1022
|
+
stdin,
|
|
1023
|
+
stdout,
|
|
1024
|
+
})
|
|
1025
|
+
// Exercises the full drain → destroyRenderer → drain → detach → close path.
|
|
1026
|
+
expect(() => renderer.destroy()).not.toThrow()
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
// ---- Destroy resilience ----
|
|
1030
|
+
|
|
1031
|
+
test("constructor cleans up listeners when input setup fails", async () => {
|
|
1032
|
+
const stdin = createTestStdin()
|
|
1033
|
+
const stdout = createCollectingStdout(80, 24)
|
|
1034
|
+
const calls: boolean[] = []
|
|
1035
|
+
const processEvents = ["warning", "uncaughtException", "unhandledRejection", "beforeExit"] as const
|
|
1036
|
+
const listenerCounts = new Map(processEvents.map((event) => [event, process.listenerCount(event)]))
|
|
1037
|
+
|
|
1038
|
+
stdin.setRawMode = (enabled) => {
|
|
1039
|
+
calls.push(enabled)
|
|
1040
|
+
if (enabled) {
|
|
1041
|
+
throw new Error("raw mode setup failed")
|
|
1042
|
+
}
|
|
1043
|
+
return stdin
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
await expect(
|
|
1047
|
+
createCliRenderer({
|
|
1048
|
+
stdin,
|
|
1049
|
+
stdout,
|
|
1050
|
+
exitSignals: [],
|
|
1051
|
+
}),
|
|
1052
|
+
).rejects.toThrow("raw mode setup failed")
|
|
1053
|
+
|
|
1054
|
+
expect(calls).toEqual([true, false])
|
|
1055
|
+
expect(stdin.listenerCount("data")).toBe(0)
|
|
1056
|
+
for (const event of processEvents) {
|
|
1057
|
+
expect(process.listenerCount(event)).toBe(listenerCounts.get(event) ?? 0)
|
|
1058
|
+
}
|
|
1059
|
+
})
|
|
1060
|
+
|
|
1061
|
+
test("destroy tolerates drainAll throwing", async () => {
|
|
1062
|
+
const stdin = createTestStdin()
|
|
1063
|
+
const stdout = createCollectingStdout(80, 24)
|
|
1064
|
+
|
|
1065
|
+
const renderer = await createCliRenderer({
|
|
1066
|
+
stdin,
|
|
1067
|
+
stdout,
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
// Monkey-patch drainAll on the private feed handle to throw on the first
|
|
1071
|
+
// two calls (one before destroyRenderer, one after), then pass through.
|
|
1072
|
+
const feed = (renderer as any)._feed
|
|
1073
|
+
expect(feed).not.toBeNull()
|
|
1074
|
+
const originalDrainAll = feed.drainAll.bind(feed)
|
|
1075
|
+
let calls = 0
|
|
1076
|
+
feed.drainAll = () => {
|
|
1077
|
+
calls++
|
|
1078
|
+
if (calls <= 2) throw new Error("simulated drain failure")
|
|
1079
|
+
return originalDrainAll()
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// destroy must swallow the drainAll exceptions and still complete the
|
|
1083
|
+
// rest of the teardown path.
|
|
1084
|
+
expect(() => renderer.destroy()).not.toThrow()
|
|
1085
|
+
expect(calls).toBeGreaterThanOrEqual(2)
|
|
1086
|
+
})
|
|
1087
|
+
|
|
1088
|
+
// ---- onError handler wire-up ----
|
|
1089
|
+
|
|
1090
|
+
test("feed.onError handler registration and detach work", async () => {
|
|
1091
|
+
const stdin = createTestStdin()
|
|
1092
|
+
const stdout = createCollectingStdout(80, 24)
|
|
1093
|
+
|
|
1094
|
+
const renderer = await createCliRenderer({
|
|
1095
|
+
stdin,
|
|
1096
|
+
stdout,
|
|
1097
|
+
})
|
|
1098
|
+
destroyFns.push(() => renderer.destroy())
|
|
1099
|
+
|
|
1100
|
+
// The renderer-internal handler is already registered. We register a
|
|
1101
|
+
// secondary one and verify the detach function it returns.
|
|
1102
|
+
//
|
|
1103
|
+
// Note: this test only verifies the wire-up (subscribe + detach). There
|
|
1104
|
+
// is currently no supported API to synthetically trigger an EventId.Error
|
|
1105
|
+
// event on the feed, so end-to-end invocation is a coverage gap tracked
|
|
1106
|
+
// for a future NativeSpanFeed test-harness hook.
|
|
1107
|
+
const feed = (renderer as any)._feed
|
|
1108
|
+
expect(feed).not.toBeNull()
|
|
1109
|
+
const detach = feed.onError(() => {})
|
|
1110
|
+
expect(typeof detach).toBe("function")
|
|
1111
|
+
expect(() => detach()).not.toThrow()
|
|
1112
|
+
})
|