@fairyhunter13/opentui-core 0.1.112 → 0.1.114
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/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/thai-debug-test.ts +68 -0
- package/docs/development.md +144 -0
- package/package.json +63 -51
- package/scripts/build.ts +400 -0
- package/scripts/publish.ts +60 -0
- package/src/3d/SpriteResourceManager.ts +286 -0
- package/src/3d/SpriteUtils.ts +70 -0
- package/src/3d/TextureUtils.ts +196 -0
- package/src/3d/ThreeRenderable.ts +197 -0
- package/src/3d/WGPURenderer.ts +294 -0
- package/src/3d/animation/ExplodingSpriteEffect.ts +513 -0
- package/src/3d/animation/PhysicsExplodingSpriteEffect.ts +429 -0
- package/src/3d/animation/SpriteAnimator.ts +633 -0
- package/src/3d/animation/SpriteParticleGenerator.ts +435 -0
- package/src/3d/canvas.ts +464 -0
- package/src/3d/index.ts +12 -0
- package/src/3d/physics/PlanckPhysicsAdapter.ts +72 -0
- package/src/3d/physics/RapierPhysicsAdapter.ts +66 -0
- package/src/3d/physics/physics-interface.ts +31 -0
- package/src/3d/shaders/supersampling.wgsl +201 -0
- package/src/3d.ts +3 -0
- package/src/NativeSpanFeed.ts +300 -0
- package/src/Renderable.ts +1704 -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 +18 -0
- package/src/benchmark/attenuation-benchmark.ts +81 -0
- package/src/benchmark/colormatrix-benchmark.ts +128 -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/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/renderer-benchmark.ts +754 -0
- package/src/benchmark/text-table-benchmark.ts +948 -0
- package/src/buffer.test.ts +291 -0
- package/src/buffer.ts +554 -0
- package/src/console.test.ts +612 -0
- package/src/console.ts +1254 -0
- package/src/edit-buffer.test.ts +1769 -0
- package/src/edit-buffer.ts +411 -0
- package/src/editor-view.test.ts +1032 -0
- package/src/editor-view.ts +284 -0
- package/src/examples/ascii-font-selection-demo.ts +245 -0
- package/src/examples/assets/Water_2_M_Normal.jpg +0 -0
- package/src/examples/assets/concrete.png +0 -0
- package/src/examples/assets/crate.png +0 -0
- package/src/examples/assets/crate_emissive.png +0 -0
- package/src/examples/assets/forrest_background.png +0 -0
- package/src/examples/assets/hast-example.json +1018 -0
- package/src/examples/assets/heart.png +0 -0
- package/src/examples/assets/main_char_heavy_attack.png +0 -0
- package/src/examples/assets/main_char_idle.png +0 -0
- package/src/examples/assets/main_char_jump_end.png +0 -0
- package/src/examples/assets/main_char_jump_landing.png +0 -0
- package/src/examples/assets/main_char_jump_start.png +0 -0
- package/src/examples/assets/main_char_run_loop.png +0 -0
- package/src/examples/assets/roughness_map.jpg +0 -0
- package/src/examples/build.ts +115 -0
- package/src/examples/code-demo.ts +924 -0
- package/src/examples/console-demo.ts +358 -0
- package/src/examples/core-plugin-slots-demo.ts +759 -0
- package/src/examples/diff-demo.ts +701 -0
- package/src/examples/draggable-three-demo.ts +259 -0
- package/src/examples/editor-demo.ts +322 -0
- package/src/examples/extmarks-demo.ts +196 -0
- package/src/examples/focus-restore-demo.ts +310 -0
- package/src/examples/fonts.ts +245 -0
- package/src/examples/fractal-shader-demo.ts +268 -0
- package/src/examples/framebuffer-demo.ts +674 -0
- package/src/examples/full-unicode-demo.ts +241 -0
- package/src/examples/golden-star-demo.ts +933 -0
- package/src/examples/grayscale-buffer-demo.ts +249 -0
- package/src/examples/hast-syntax-highlighting-demo.ts +129 -0
- package/src/examples/index.ts +926 -0
- package/src/examples/input-demo.ts +377 -0
- package/src/examples/input-select-layout-demo.ts +425 -0
- package/src/examples/install.sh +143 -0
- package/src/examples/keypress-debug-demo.ts +452 -0
- package/src/examples/lib/HexList.ts +122 -0
- package/src/examples/lib/PaletteGrid.ts +125 -0
- package/src/examples/lib/standalone-keys.ts +25 -0
- package/src/examples/lib/tab-controller.ts +243 -0
- package/src/examples/lights-phong-demo.ts +290 -0
- package/src/examples/link-demo.ts +220 -0
- package/src/examples/live-state-demo.ts +480 -0
- package/src/examples/markdown-demo.ts +725 -0
- package/src/examples/mouse-interaction-demo.ts +428 -0
- package/src/examples/nested-zindex-demo.ts +357 -0
- package/src/examples/opacity-example.ts +235 -0
- package/src/examples/opentui-demo.ts +1057 -0
- package/src/examples/physx-planck-2d-demo.ts +623 -0
- package/src/examples/physx-rapier-2d-demo.ts +655 -0
- package/src/examples/relative-positioning-demo.ts +323 -0
- package/src/examples/scroll-example.ts +214 -0
- package/src/examples/scrollbox-mouse-test.ts +112 -0
- package/src/examples/scrollbox-overlay-hit-test.ts +206 -0
- package/src/examples/select-demo.ts +237 -0
- package/src/examples/shader-cube-demo.ts +1015 -0
- package/src/examples/simple-layout-example.ts +591 -0
- package/src/examples/slider-demo.ts +617 -0
- package/src/examples/split-mode-demo.ts +453 -0
- package/src/examples/sprite-animation-demo.ts +443 -0
- package/src/examples/sprite-particle-generator-demo.ts +486 -0
- package/src/examples/static-sprite-demo.ts +193 -0
- package/src/examples/sticky-scroll-example.ts +308 -0
- package/src/examples/styled-text-demo.ts +282 -0
- package/src/examples/tab-select-demo.ts +219 -0
- package/src/examples/terminal-title.ts +29 -0
- package/src/examples/terminal.ts +305 -0
- package/src/examples/text-node-demo.ts +416 -0
- package/src/examples/text-selection-demo.ts +377 -0
- package/src/examples/text-table-demo.ts +503 -0
- package/src/examples/text-truncation-demo.ts +481 -0
- package/src/examples/text-wrap.ts +757 -0
- package/src/examples/texture-loading-demo.ts +259 -0
- package/src/examples/timeline-example.ts +670 -0
- package/src/examples/transparency-demo.ts +400 -0
- package/src/examples/vnode-composition-demo.ts +404 -0
- package/src/examples/wide-grapheme-overlay-demo.ts +280 -0
- package/src/index.ts +24 -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 +984 -0
- package/src/lib/RGBA.ts +204 -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 +41 -0
- package/src/lib/clipboard.ts +47 -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 +322 -0
- package/src/lib/extmarks.test.ts +3457 -0
- package/src/lib/extmarks.ts +843 -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 +21 -0
- package/src/lib/keymapping.test.ts +317 -0
- package/src/lib/keymapping.ts +115 -0
- package/src/lib/objects-in-viewport.test.ts +787 -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 +340 -0
- package/src/lib/parse.keypress-kitty.test.ts +663 -0
- package/src/lib/parse.keypress-kitty.ts +439 -0
- package/src/lib/parse.keypress.test.ts +1849 -0
- package/src/lib/parse.keypress.ts +397 -0
- package/src/lib/parse.mouse.test.ts +552 -0
- package/src/lib/parse.mouse.ts +232 -0
- package/src/lib/paste.ts +16 -0
- package/src/lib/queue.ts +65 -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 +240 -0
- package/src/lib/singleton.ts +28 -0
- package/src/lib/stdin-parser.test.ts +2290 -0
- package/src/lib/stdin-parser.ts +1810 -0
- package/src/lib/styled-text.ts +178 -0
- package/src/lib/terminal-capability-detection.test.ts +202 -0
- package/src/lib/terminal-capability-detection.ts +79 -0
- package/src/lib/terminal-palette.test.ts +878 -0
- package/src/lib/terminal-palette.ts +383 -0
- package/src/lib/tree-sitter/assets/README.md +118 -0
- package/src/lib/tree-sitter/assets/update.ts +334 -0
- package/src/lib/tree-sitter/assets.d.ts +9 -0
- package/src/lib/tree-sitter/cache.test.ts +273 -0
- package/src/lib/tree-sitter/client.test.ts +1165 -0
- package/src/lib/tree-sitter/client.ts +607 -0
- package/src/lib/tree-sitter/default-parsers.ts +86 -0
- package/src/lib/tree-sitter/download-utils.ts +148 -0
- package/src/lib/tree-sitter/index.ts +28 -0
- package/src/lib/tree-sitter/parser.worker.ts +1042 -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 +82 -0
- package/src/lib/tree-sitter-styled-text.test.ts +1253 -0
- package/src/lib/tree-sitter-styled-text.ts +306 -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/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 +930 -0
- package/src/post/filters.ts +489 -0
- package/src/post/matrices.ts +288 -0
- package/src/renderables/ASCIIFont.ts +219 -0
- package/src/renderables/Box.test.ts +205 -0
- package/src/renderables/Box.ts +326 -0
- package/src/renderables/Code.test.ts +2062 -0
- package/src/renderables/Code.ts +357 -0
- package/src/renderables/Diff.regression.test.ts +226 -0
- package/src/renderables/Diff.test.ts +3101 -0
- package/src/renderables/Diff.ts +1211 -0
- package/src/renderables/EditBufferRenderable.test.ts +288 -0
- package/src/renderables/EditBufferRenderable.ts +1166 -0
- package/src/renderables/FrameBuffer.ts +47 -0
- package/src/renderables/Input.test.ts +1228 -0
- package/src/renderables/Input.ts +247 -0
- package/src/renderables/LineNumberRenderable.ts +724 -0
- package/src/renderables/Markdown.ts +1393 -0
- package/src/renderables/ScrollBar.ts +422 -0
- package/src/renderables/ScrollBox.ts +883 -0
- package/src/renderables/Select.test.ts +1033 -0
- package/src/renderables/Select.ts +524 -0
- package/src/renderables/Slider.test.ts +456 -0
- package/src/renderables/Slider.ts +342 -0
- package/src/renderables/TabSelect.test.ts +197 -0
- package/src/renderables/TabSelect.ts +455 -0
- package/src/renderables/Text.selection-buffer.test.ts +123 -0
- package/src/renderables/Text.test.ts +2660 -0
- package/src/renderables/Text.ts +147 -0
- package/src/renderables/TextBufferRenderable.ts +518 -0
- package/src/renderables/TextNode.test.ts +1058 -0
- package/src/renderables/TextNode.ts +325 -0
- package/src/renderables/TextTable.test.ts +1421 -0
- package/src/renderables/TextTable.ts +1344 -0
- package/src/renderables/Textarea.ts +430 -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__/LineNumberRenderable.scrollbox-simple.test.ts +144 -0
- package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +816 -0
- package/src/renderables/__tests__/LineNumberRenderable.test.ts +1865 -0
- package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +85 -0
- package/src/renderables/__tests__/Markdown.code-colors.test.ts +242 -0
- package/src/renderables/__tests__/Markdown.test.ts +2518 -0
- package/src/renderables/__tests__/MultiRenderable.selection.test.ts +87 -0
- package/src/renderables/__tests__/Textarea.buffer.test.ts +682 -0
- package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +675 -0
- package/src/renderables/__tests__/Textarea.editing.test.ts +2041 -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 +3149 -0
- package/src/renderables/__tests__/Textarea.paste.test.ts +357 -0
- package/src/renderables/__tests__/Textarea.rendering.test.ts +1866 -0
- package/src/renderables/__tests__/Textarea.scroll.test.ts +733 -0
- package/src/renderables/__tests__/Textarea.selection.test.ts +1590 -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 +217 -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 +289 -0
- package/src/renderables/index.ts +23 -0
- package/src/renderables/markdown-parser.ts +66 -0
- package/src/renderer.ts +2681 -0
- package/src/runtime-plugin-support.ts +39 -0
- package/src/runtime-plugin.ts +615 -0
- package/src/syntax-style.test.ts +841 -0
- package/src/syntax-style.ts +257 -0
- package/src/testing/README.md +210 -0
- package/src/testing/capture-spans.test.ts +194 -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 +73 -0
- package/src/testing/spy.ts +13 -0
- package/src/testing/test-recorder.test.ts +415 -0
- package/src/testing/test-recorder.ts +145 -0
- package/src/testing/test-renderer.ts +132 -0
- package/src/testing.ts +7 -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 +38 -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 +41 -0
- package/src/tests/hover-cursor.test.ts +98 -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 +352 -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 +1281 -0
- package/src/tests/renderer.clock.test.ts +158 -0
- package/src/tests/renderer.console-startup.test.ts +185 -0
- package/src/tests/renderer.control.test.ts +425 -0
- package/src/tests/renderer.core-slot-binding.test.ts +952 -0
- package/src/tests/renderer.cursor.test.ts +26 -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 +294 -0
- package/src/tests/renderer.idle.test.ts +219 -0
- package/src/tests/renderer.input.test.ts +2237 -0
- package/src/tests/renderer.kitty-flags.test.ts +195 -0
- package/src/tests/renderer.mouse.test.ts +1274 -0
- package/src/tests/renderer.palette.test.ts +629 -0
- package/src/tests/renderer.selection.test.ts +49 -0
- package/src/tests/renderer.slot-registry.test.ts +684 -0
- package/src/tests/renderer.useMouse.test.ts +47 -0
- package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +76 -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.fixture.ts +11 -0
- package/src/tests/runtime-plugin-support.test.ts +19 -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 +354 -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 +1530 -0
- package/src/tests/wrap-resize-perf.test.ts +276 -0
- package/src/tests/yoga-setters.test.ts +921 -0
- package/src/text-buffer-view.test.ts +705 -0
- package/src/text-buffer-view.ts +189 -0
- package/src/text-buffer.test.ts +347 -0
- package/src/text-buffer.ts +250 -0
- package/src/types.ts +161 -0
- package/src/utils.ts +88 -0
- package/src/zig/ansi.zig +268 -0
- package/src/zig/bench/README.md +50 -0
- package/src/zig/bench/buffer-draw-text-buffer_bench.zig +887 -0
- package/src/zig/bench/edit-buffer_bench.zig +476 -0
- package/src/zig/bench/native-span-feed_bench.zig +100 -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 +470 -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 +273 -0
- package/src/zig/bench/utf8_bench.zig +799 -0
- package/src/zig/bench-utils.zig +431 -0
- package/src/zig/bench.zig +217 -0
- package/src/zig/buffer-methods.zig +211 -0
- package/src/zig/buffer.zig +2281 -0
- package/src/zig/build.zig +289 -0
- package/src/zig/build.zig.zon +16 -0
- package/src/zig/edit-buffer.zig +825 -0
- package/src/zig/editor-view.zig +802 -0
- package/src/zig/event-bus.zig +13 -0
- package/src/zig/event-emitter.zig +65 -0
- package/src/zig/file-logger.zig +92 -0
- package/src/zig/grapheme.zig +599 -0
- package/src/zig/lib.zig +1854 -0
- package/src/zig/link.zig +333 -0
- package/src/zig/logger.zig +43 -0
- package/src/zig/mem-registry.zig +125 -0
- package/src/zig/native-span-feed-bench-lib.zig +7 -0
- package/src/zig/native-span-feed.zig +708 -0
- package/src/zig/renderer.zig +1393 -0
- package/src/zig/rope.zig +1220 -0
- package/src/zig/syntax-style.zig +161 -0
- package/src/zig/terminal.zig +987 -0
- package/src/zig/test.zig +72 -0
- package/src/zig/tests/README.md +18 -0
- package/src/zig/tests/buffer-methods_test.zig +1109 -0
- package/src/zig/tests/buffer_test.zig +2557 -0
- package/src/zig/tests/edit-buffer-history_test.zig +271 -0
- package/src/zig/tests/edit-buffer_test.zig +1689 -0
- package/src/zig/tests/editor-view_test.zig +3299 -0
- package/src/zig/tests/event-emitter_test.zig +249 -0
- package/src/zig/tests/grapheme_test.zig +1304 -0
- package/src/zig/tests/link_test.zig +190 -0
- package/src/zig/tests/mem-registry_test.zig +473 -0
- package/src/zig/tests/memory_leak_regression_test.zig +159 -0
- package/src/zig/tests/native-span-feed_test.zig +1264 -0
- package/src/zig/tests/renderer_test.zig +1017 -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/syntax-style_test.zig +557 -0
- package/src/zig/tests/terminal_test.zig +754 -0
- package/src/zig/tests/text-buffer-drawing_test.zig +3237 -0
- package/src/zig/tests/text-buffer-highlights_test.zig +666 -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 +1035 -0
- package/src/zig/tests/text-buffer-selection_viewport_test.zig +358 -0
- package/src/zig/tests/text-buffer-view_test.zig +3649 -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 +4057 -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/text-buffer-iterators.zig +499 -0
- package/src/zig/text-buffer-segment.zig +404 -0
- package/src/zig/text-buffer-view.zig +1371 -0
- package/src/zig/text-buffer.zig +1180 -0
- package/src/zig/utf8.zig +1948 -0
- package/src/zig/utils.zig +9 -0
- package/src/zig-structs.ts +261 -0
- package/src/zig.ts +3884 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +27 -0
- package/3d/SpriteResourceManager.d.ts +0 -74
- package/3d/SpriteUtils.d.ts +0 -13
- package/3d/TextureUtils.d.ts +0 -24
- package/3d/ThreeRenderable.d.ts +0 -40
- package/3d/WGPURenderer.d.ts +0 -61
- package/3d/animation/ExplodingSpriteEffect.d.ts +0 -71
- package/3d/animation/PhysicsExplodingSpriteEffect.d.ts +0 -76
- package/3d/animation/SpriteAnimator.d.ts +0 -124
- package/3d/animation/SpriteParticleGenerator.d.ts +0 -62
- package/3d/canvas.d.ts +0 -44
- package/3d/index.d.ts +0 -12
- package/3d/physics/PlanckPhysicsAdapter.d.ts +0 -19
- package/3d/physics/RapierPhysicsAdapter.d.ts +0 -19
- package/3d/physics/physics-interface.d.ts +0 -27
- package/3d.d.ts +0 -2
- package/3d.js +0 -34041
- package/3d.js.map +0 -155
- package/LICENSE +0 -21
- package/NativeSpanFeed.d.ts +0 -41
- package/Renderable.d.ts +0 -334
- package/animation/Timeline.d.ts +0 -126
- package/ansi.d.ts +0 -13
- package/buffer.d.ts +0 -111
- package/console.d.ts +0 -144
- package/edit-buffer.d.ts +0 -98
- package/editor-view.d.ts +0 -73
- package/index-8fks7yv1.js +0 -411
- package/index-8fks7yv1.js.map +0 -10
- package/index-egy5e2rs.js +0 -12267
- package/index-egy5e2rs.js.map +0 -42
- package/index-tse8gzh0.js +0 -20614
- package/index-tse8gzh0.js.map +0 -67
- package/index.d.ts +0 -23
- package/index.js +0 -478
- package/index.js.map +0 -9
- package/lib/KeyHandler.d.ts +0 -61
- package/lib/RGBA.d.ts +0 -25
- package/lib/ascii.font.d.ts +0 -508
- package/lib/border.d.ts +0 -51
- package/lib/bunfs.d.ts +0 -7
- package/lib/clipboard.d.ts +0 -17
- package/lib/clock.d.ts +0 -15
- package/lib/data-paths.d.ts +0 -26
- package/lib/debounce.d.ts +0 -42
- package/lib/detect-links.d.ts +0 -6
- package/lib/env.d.ts +0 -42
- package/lib/extmarks-history.d.ts +0 -17
- package/lib/extmarks.d.ts +0 -89
- package/lib/hast-styled-text.d.ts +0 -17
- package/lib/index.d.ts +0 -21
- package/lib/keymapping.d.ts +0 -25
- package/lib/objects-in-viewport.d.ts +0 -24
- package/lib/output.capture.d.ts +0 -24
- package/lib/parse.keypress-kitty.d.ts +0 -2
- package/lib/parse.keypress.d.ts +0 -26
- package/lib/parse.mouse.d.ts +0 -30
- package/lib/paste.d.ts +0 -7
- package/lib/queue.d.ts +0 -15
- package/lib/renderable.validations.d.ts +0 -12
- package/lib/scroll-acceleration.d.ts +0 -43
- package/lib/selection.d.ts +0 -63
- package/lib/singleton.d.ts +0 -7
- package/lib/stdin-parser.d.ts +0 -87
- package/lib/styled-text.d.ts +0 -63
- package/lib/terminal-capability-detection.d.ts +0 -30
- package/lib/terminal-palette.d.ts +0 -50
- package/lib/tree-sitter/assets/update.d.ts +0 -11
- package/lib/tree-sitter/client.d.ts +0 -47
- package/lib/tree-sitter/default-parsers.d.ts +0 -2
- package/lib/tree-sitter/download-utils.d.ts +0 -21
- package/lib/tree-sitter/index.d.ts +0 -8
- package/lib/tree-sitter/parser.worker.d.ts +0 -1
- package/lib/tree-sitter/parsers-config.d.ts +0 -53
- package/lib/tree-sitter/resolve-ft.d.ts +0 -5
- package/lib/tree-sitter/types.d.ts +0 -82
- package/lib/tree-sitter-styled-text.d.ts +0 -14
- package/lib/validate-dir-name.d.ts +0 -1
- package/lib/yoga.options.d.ts +0 -32
- package/parser.worker.js +0 -899
- package/parser.worker.js.map +0 -12
- package/plugins/core-slot.d.ts +0 -72
- package/plugins/registry.d.ts +0 -42
- package/plugins/types.d.ts +0 -34
- package/post/effects.d.ts +0 -147
- package/post/filters.d.ts +0 -65
- package/post/matrices.d.ts +0 -20
- package/renderables/ASCIIFont.d.ts +0 -52
- package/renderables/Box.d.ts +0 -81
- package/renderables/Code.d.ts +0 -78
- package/renderables/Diff.d.ts +0 -142
- package/renderables/EditBufferRenderable.d.ts +0 -237
- package/renderables/FrameBuffer.d.ts +0 -16
- package/renderables/Input.d.ts +0 -67
- package/renderables/LineNumberRenderable.d.ts +0 -78
- package/renderables/Markdown.d.ts +0 -185
- package/renderables/ScrollBar.d.ts +0 -77
- package/renderables/ScrollBox.d.ts +0 -124
- package/renderables/Select.d.ts +0 -115
- package/renderables/Slider.d.ts +0 -47
- package/renderables/TabSelect.d.ts +0 -96
- package/renderables/Text.d.ts +0 -36
- package/renderables/TextBufferRenderable.d.ts +0 -105
- package/renderables/TextNode.d.ts +0 -91
- package/renderables/TextTable.d.ts +0 -140
- package/renderables/Textarea.d.ts +0 -63
- package/renderables/TimeToFirstDraw.d.ts +0 -24
- package/renderables/__tests__/renderable-test-utils.d.ts +0 -12
- package/renderables/composition/VRenderable.d.ts +0 -16
- package/renderables/composition/constructs.d.ts +0 -35
- package/renderables/composition/vnode.d.ts +0 -46
- package/renderables/index.d.ts +0 -23
- package/renderables/markdown-parser.d.ts +0 -10
- package/renderer.d.ts +0 -419
- package/runtime-plugin-support.d.ts +0 -3
- package/runtime-plugin-support.js +0 -29
- package/runtime-plugin-support.js.map +0 -10
- package/runtime-plugin.d.ts +0 -16
- package/runtime-plugin.js +0 -16
- package/runtime-plugin.js.map +0 -9
- package/syntax-style.d.ts +0 -54
- package/testing/manual-clock.d.ts +0 -17
- package/testing/mock-keys.d.ts +0 -81
- package/testing/mock-mouse.d.ts +0 -38
- package/testing/mock-tree-sitter-client.d.ts +0 -23
- package/testing/spy.d.ts +0 -7
- package/testing/test-recorder.d.ts +0 -61
- package/testing/test-renderer.d.ts +0 -23
- package/testing.d.ts +0 -6
- package/testing.js +0 -697
- package/testing.js.map +0 -15
- package/text-buffer-view.d.ts +0 -42
- package/text-buffer.d.ts +0 -67
- package/types.d.ts +0 -139
- package/utils.d.ts +0 -14
- package/zig-structs.d.ts +0 -155
- package/zig.d.ts +0 -353
- /package/{assets → src/lib/tree-sitter/assets}/javascript/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/javascript/tree-sitter-javascript.wasm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown/injections.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown/tree-sitter-markdown.wasm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/typescript/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/typescript/tree-sitter-typescript.wasm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/zig/highlights.scm +0 -0
- /package/{assets → src/lib/tree-sitter/assets}/zig/tree-sitter-zig.wasm +0 -0
|
@@ -0,0 +1,1421 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { OptimizedBuffer } from "../buffer.js"
|
|
3
|
+
import { RGBA } from "../lib/RGBA.js"
|
|
4
|
+
import { bold, green, red, yellow } from "../lib/styled-text.js"
|
|
5
|
+
import { createTestRenderer, type MockMouse, type TestRenderer } from "../testing/test-renderer.js"
|
|
6
|
+
import type { CapturedFrame } from "../types.js"
|
|
7
|
+
import { BoxRenderable } from "./Box.js"
|
|
8
|
+
import { ScrollBoxRenderable } from "./ScrollBox.js"
|
|
9
|
+
import { TextRenderable } from "./Text.js"
|
|
10
|
+
import { TextTableRenderable, type TextTableCellContent, type TextTableContent } from "./TextTable.js"
|
|
11
|
+
|
|
12
|
+
const VERTICAL_BORDER_CP = "│".codePointAt(0)!
|
|
13
|
+
const BORDER_CHAR_PATTERN = /[┌┐└┘├┤┬┴┼│─]/
|
|
14
|
+
|
|
15
|
+
let renderer: TestRenderer
|
|
16
|
+
let renderOnce: () => Promise<void>
|
|
17
|
+
let captureFrame: () => string
|
|
18
|
+
let captureSpans: () => CapturedFrame
|
|
19
|
+
let resizeRenderer: (width: number, height: number) => void
|
|
20
|
+
let mockMouse: MockMouse
|
|
21
|
+
|
|
22
|
+
function getCharAt(buffer: TestRenderer["currentRenderBuffer"], x: number, y: number): number {
|
|
23
|
+
return buffer.buffers.char[y * buffer.width + x] ?? 0
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getFgAt(buffer: TestRenderer["currentRenderBuffer"], x: number, y: number): RGBA {
|
|
27
|
+
const index = (y * buffer.width + x) * 4
|
|
28
|
+
return RGBA.fromValues(
|
|
29
|
+
buffer.buffers.fg[index] ?? 0,
|
|
30
|
+
buffer.buffers.fg[index + 1] ?? 0,
|
|
31
|
+
buffer.buffers.fg[index + 2] ?? 0,
|
|
32
|
+
buffer.buffers.fg[index + 3] ?? 0,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getBgAt(buffer: TestRenderer["currentRenderBuffer"], x: number, y: number): RGBA {
|
|
37
|
+
const index = (y * buffer.width + x) * 4
|
|
38
|
+
return RGBA.fromValues(
|
|
39
|
+
buffer.buffers.bg[index] ?? 0,
|
|
40
|
+
buffer.buffers.bg[index + 1] ?? 0,
|
|
41
|
+
buffer.buffers.bg[index + 2] ?? 0,
|
|
42
|
+
buffer.buffers.bg[index + 3] ?? 0,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findVerticalBorderXs(buffer: TestRenderer["currentRenderBuffer"], y: number): number[] {
|
|
47
|
+
const xs: number[] = []
|
|
48
|
+
|
|
49
|
+
for (let x = 0; x < buffer.width; x++) {
|
|
50
|
+
if (getCharAt(buffer, x, y) === VERTICAL_BORDER_CP) {
|
|
51
|
+
xs.push(x)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return xs
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function countChar(text: string, target: string): number {
|
|
59
|
+
return [...text].filter((char) => char === target).length
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeFrameBlock(lines: string[]): string {
|
|
63
|
+
const trimmed = lines.map((line) => line.trimEnd())
|
|
64
|
+
const nonEmpty = trimmed.filter((line) => line.length > 0)
|
|
65
|
+
const minIndent = nonEmpty.reduce((min, line) => {
|
|
66
|
+
const indent = line.match(/^ */)?.[0].length ?? 0
|
|
67
|
+
return Math.min(min, indent)
|
|
68
|
+
}, Number.POSITIVE_INFINITY)
|
|
69
|
+
const indent = Number.isFinite(minIndent) ? minIndent : 0
|
|
70
|
+
|
|
71
|
+
return trimmed.map((line) => line.slice(indent)).join("\n") + "\n"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractTableBlock(frame: string, headerMatcher: (line: string) => boolean): string {
|
|
75
|
+
const lines = frame.split("\n")
|
|
76
|
+
const headerY = lines.findIndex(headerMatcher)
|
|
77
|
+
if (headerY < 0) {
|
|
78
|
+
throw new Error("Unable to find table header line")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let topY = headerY
|
|
82
|
+
while (topY >= 0 && !lines[topY]?.includes("┌")) {
|
|
83
|
+
topY -= 1
|
|
84
|
+
}
|
|
85
|
+
if (topY < 0) {
|
|
86
|
+
throw new Error("Unable to find table top border")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let bottomY = headerY
|
|
90
|
+
while (bottomY < lines.length && !lines[bottomY]?.includes("└")) {
|
|
91
|
+
bottomY += 1
|
|
92
|
+
}
|
|
93
|
+
if (bottomY >= lines.length) {
|
|
94
|
+
throw new Error("Unable to find table bottom border")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return normalizeFrameBlock(lines.slice(topY, bottomY + 1))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function renderStandaloneTableBlock(
|
|
101
|
+
width: number,
|
|
102
|
+
content: TextTableContent,
|
|
103
|
+
headerMatcher: (line: string) => boolean,
|
|
104
|
+
): Promise<string> {
|
|
105
|
+
const testRenderer = await createTestRenderer({ width, height: 120 })
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const table = new TextTableRenderable(testRenderer.renderer, {
|
|
109
|
+
left: 0,
|
|
110
|
+
top: 0,
|
|
111
|
+
width,
|
|
112
|
+
wrapMode: "word",
|
|
113
|
+
content,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
testRenderer.renderer.root.add(table)
|
|
117
|
+
await testRenderer.renderOnce()
|
|
118
|
+
return extractTableBlock(testRenderer.captureCharFrame(), headerMatcher)
|
|
119
|
+
} finally {
|
|
120
|
+
testRenderer.renderer.destroy()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function findSelectablePoint(
|
|
125
|
+
table: TextTableRenderable,
|
|
126
|
+
direction: "top-left" | "bottom-right",
|
|
127
|
+
): { x: number; y: number } {
|
|
128
|
+
const points: Array<{ x: number; y: number }> = []
|
|
129
|
+
|
|
130
|
+
for (let y = table.y; y < table.y + table.height; y++) {
|
|
131
|
+
for (let x = table.x; x < table.x + table.width; x++) {
|
|
132
|
+
if (table.shouldStartSelection(x, y)) {
|
|
133
|
+
points.push({ x, y })
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
expect(points.length).toBeGreaterThan(0)
|
|
139
|
+
|
|
140
|
+
if (direction === "top-left") {
|
|
141
|
+
points.sort((a, b) => (a.y !== b.y ? a.y - b.y : a.x - b.x))
|
|
142
|
+
return points[0]!
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
points.sort((a, b) => (a.y !== b.y ? b.y - a.y : b.x - a.x))
|
|
146
|
+
return points[0]!
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function findTextPoint(frame: string, text: string): { x: number; y: number } {
|
|
150
|
+
const lines = frame.split("\n")
|
|
151
|
+
|
|
152
|
+
for (let y = 0; y < lines.length; y++) {
|
|
153
|
+
const x = lines[y]?.indexOf(text) ?? -1
|
|
154
|
+
if (x >= 0) {
|
|
155
|
+
return { x, y }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
throw new Error(`Unable to find '${text}' in frame`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function cell(text: string): TextTableCellContent {
|
|
163
|
+
return [
|
|
164
|
+
{
|
|
165
|
+
__isChunk: true,
|
|
166
|
+
text,
|
|
167
|
+
},
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getScrollContentBottom(scrollBox: ScrollBoxRenderable): number {
|
|
172
|
+
const children = scrollBox.content.getChildren()
|
|
173
|
+
const lastChild = children[children.length - 1]
|
|
174
|
+
|
|
175
|
+
if (!lastChild) {
|
|
176
|
+
return Math.max(0, Math.ceil(scrollBox.content.height))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const relativeBottom = lastChild.y - scrollBox.content.y + lastChild.height
|
|
180
|
+
return Math.max(0, Math.ceil(relativeBottom))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
beforeEach(async () => {
|
|
184
|
+
const testRenderer = await createTestRenderer({ width: 60, height: 16 })
|
|
185
|
+
renderer = testRenderer.renderer
|
|
186
|
+
renderOnce = testRenderer.renderOnce
|
|
187
|
+
captureFrame = testRenderer.captureCharFrame
|
|
188
|
+
captureSpans = testRenderer.captureSpans
|
|
189
|
+
resizeRenderer = testRenderer.resize
|
|
190
|
+
mockMouse = testRenderer.mockMouse
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
afterEach(() => {
|
|
194
|
+
renderer.destroy()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe("TextTableRenderable", () => {
|
|
198
|
+
test("renders a basic table with styled cell chunks", async () => {
|
|
199
|
+
const content: TextTableContent = [
|
|
200
|
+
[[bold("Name")], [bold("Status")], [bold("Notes")]],
|
|
201
|
+
[cell("Alpha"), [green("OK")], cell("All systems nominal")],
|
|
202
|
+
[cell("Bravo"), [red("WARN")], cell("Pending checks")],
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
const table = new TextTableRenderable(renderer, {
|
|
206
|
+
left: 1,
|
|
207
|
+
top: 1,
|
|
208
|
+
columnWidthMode: "content",
|
|
209
|
+
content,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
renderer.root.add(table)
|
|
213
|
+
await renderOnce()
|
|
214
|
+
|
|
215
|
+
const frame = captureFrame()
|
|
216
|
+
expect(frame).toMatchSnapshot("basic table")
|
|
217
|
+
expect(frame).toContain("Alpha")
|
|
218
|
+
expect(frame).toContain("WARN")
|
|
219
|
+
|
|
220
|
+
const spans = captureSpans().lines.flatMap((line) => line.spans)
|
|
221
|
+
const okSpan = spans.find((span) => span.text.includes("OK"))
|
|
222
|
+
|
|
223
|
+
expect(okSpan).toBeDefined()
|
|
224
|
+
expect(okSpan?.fg.equals(RGBA.fromHex("#008000"))).toBe(true)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test("wraps content and fits columns when width is constrained", async () => {
|
|
228
|
+
const content: TextTableContent = [
|
|
229
|
+
[[bold("ID")], [bold("Description")]],
|
|
230
|
+
[cell("1"), cell("This is a long sentence that should wrap across multiple visual lines")],
|
|
231
|
+
[cell("2"), cell("Short")],
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
const table = new TextTableRenderable(renderer, {
|
|
235
|
+
left: 0,
|
|
236
|
+
top: 0,
|
|
237
|
+
width: 34,
|
|
238
|
+
wrapMode: "word",
|
|
239
|
+
content,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
renderer.root.add(table)
|
|
243
|
+
await renderOnce()
|
|
244
|
+
|
|
245
|
+
const frame = captureFrame()
|
|
246
|
+
expect(frame).toMatchSnapshot("wrapped constrained width")
|
|
247
|
+
expect(frame).toContain("Description")
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test("keeps intrinsic width in content mode when extra space is available", async () => {
|
|
251
|
+
const table = new TextTableRenderable(renderer, {
|
|
252
|
+
left: 0,
|
|
253
|
+
top: 0,
|
|
254
|
+
width: 34,
|
|
255
|
+
wrapMode: "word",
|
|
256
|
+
columnWidthMode: "content",
|
|
257
|
+
content: [
|
|
258
|
+
[cell("A"), cell("B")],
|
|
259
|
+
[cell("1"), cell("2")],
|
|
260
|
+
],
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
renderer.root.add(table)
|
|
264
|
+
await renderOnce()
|
|
265
|
+
|
|
266
|
+
const lines = captureFrame().split("\n")
|
|
267
|
+
const headerY = lines.findIndex((line) => line.includes("A") && line.includes("B"))
|
|
268
|
+
expect(headerY).toBeGreaterThanOrEqual(0)
|
|
269
|
+
|
|
270
|
+
const buffer = renderer.currentRenderBuffer
|
|
271
|
+
const borderXs = findVerticalBorderXs(buffer, headerY)
|
|
272
|
+
|
|
273
|
+
expect(borderXs.length).toBe(3)
|
|
274
|
+
expect(borderXs[0]).toBe(0)
|
|
275
|
+
expect(borderXs[borderXs.length - 1]).toBeLessThan(33)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test("fills available width by default in full mode", async () => {
|
|
279
|
+
const table = new TextTableRenderable(renderer, {
|
|
280
|
+
left: 0,
|
|
281
|
+
top: 0,
|
|
282
|
+
width: 34,
|
|
283
|
+
wrapMode: "word",
|
|
284
|
+
content: [
|
|
285
|
+
[cell("A"), cell("B")],
|
|
286
|
+
[cell("1"), cell("2")],
|
|
287
|
+
],
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
renderer.root.add(table)
|
|
291
|
+
await renderOnce()
|
|
292
|
+
|
|
293
|
+
const lines = captureFrame().split("\n")
|
|
294
|
+
const headerY = lines.findIndex((line) => line.includes("A") && line.includes("B"))
|
|
295
|
+
expect(headerY).toBeGreaterThanOrEqual(0)
|
|
296
|
+
|
|
297
|
+
const buffer = renderer.currentRenderBuffer
|
|
298
|
+
const borderXs = findVerticalBorderXs(buffer, headerY)
|
|
299
|
+
|
|
300
|
+
expect(borderXs).toEqual([0, 17, 33])
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test("fills available width in no-wrap mode when columnWidthMode is full", async () => {
|
|
304
|
+
const table = new TextTableRenderable(renderer, {
|
|
305
|
+
left: 0,
|
|
306
|
+
top: 0,
|
|
307
|
+
width: 24,
|
|
308
|
+
wrapMode: "none",
|
|
309
|
+
columnWidthMode: "full",
|
|
310
|
+
content: [
|
|
311
|
+
[cell("Key"), cell("Value")],
|
|
312
|
+
[cell("A"), cell("B")],
|
|
313
|
+
],
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
renderer.root.add(table)
|
|
317
|
+
await renderOnce()
|
|
318
|
+
|
|
319
|
+
const lines = captureFrame().split("\n")
|
|
320
|
+
const headerY = lines.findIndex((line) => line.includes("Key") && line.includes("Value"))
|
|
321
|
+
expect(headerY).toBeGreaterThanOrEqual(0)
|
|
322
|
+
|
|
323
|
+
const buffer = renderer.currentRenderBuffer
|
|
324
|
+
const borderXs = findVerticalBorderXs(buffer, headerY)
|
|
325
|
+
|
|
326
|
+
expect(borderXs).toEqual([0, 11, 23])
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test("preserves bordered layout when border glyphs are hidden", async () => {
|
|
330
|
+
const table = new TextTableRenderable(renderer, {
|
|
331
|
+
left: 0,
|
|
332
|
+
top: 0,
|
|
333
|
+
border: true,
|
|
334
|
+
outerBorder: true,
|
|
335
|
+
showBorders: false,
|
|
336
|
+
columnWidthMode: "content",
|
|
337
|
+
content: [[cell("A"), cell("B")]],
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
renderer.root.add(table)
|
|
341
|
+
await renderOnce()
|
|
342
|
+
|
|
343
|
+
const frame = captureFrame()
|
|
344
|
+
expect(BORDER_CHAR_PATTERN.test(frame)).toBe(false)
|
|
345
|
+
|
|
346
|
+
const row = frame.split("\n").find((line) => line.includes("A") && line.includes("B"))
|
|
347
|
+
expect(row).toBeDefined()
|
|
348
|
+
expect(row?.indexOf("A")).toBe(1)
|
|
349
|
+
expect(row?.indexOf("B")).toBe(3)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
test("applies cell padding when provided", async () => {
|
|
353
|
+
const table = new TextTableRenderable(renderer, {
|
|
354
|
+
left: 0,
|
|
355
|
+
top: 0,
|
|
356
|
+
cellPadding: 1,
|
|
357
|
+
columnWidthMode: "content",
|
|
358
|
+
content: [
|
|
359
|
+
[cell("A"), cell("B")],
|
|
360
|
+
[cell("1"), cell("2")],
|
|
361
|
+
],
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
renderer.root.add(table)
|
|
365
|
+
await renderOnce()
|
|
366
|
+
|
|
367
|
+
const frame = captureFrame()
|
|
368
|
+
expect(frame).toContain("│ │ │")
|
|
369
|
+
expect(frame).toContain("│ A │ B │")
|
|
370
|
+
|
|
371
|
+
const lines = frame.split("\n")
|
|
372
|
+
const headerY = lines.findIndex((line) => line.includes(" A ") && line.includes(" B "))
|
|
373
|
+
expect(headerY).toBeGreaterThanOrEqual(0)
|
|
374
|
+
|
|
375
|
+
const borderXs = findVerticalBorderXs(renderer.currentRenderBuffer, headerY)
|
|
376
|
+
expect(borderXs).toEqual([0, 4, 8])
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test("reflows when columnWidthMode is changed after initial render", async () => {
|
|
380
|
+
const table = new TextTableRenderable(renderer, {
|
|
381
|
+
left: 0,
|
|
382
|
+
top: 0,
|
|
383
|
+
width: 34,
|
|
384
|
+
wrapMode: "word",
|
|
385
|
+
columnWidthMode: "content",
|
|
386
|
+
content: [
|
|
387
|
+
[cell("A"), cell("B")],
|
|
388
|
+
[cell("1"), cell("2")],
|
|
389
|
+
],
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
renderer.root.add(table)
|
|
393
|
+
await renderOnce()
|
|
394
|
+
|
|
395
|
+
let lines = captureFrame().split("\n")
|
|
396
|
+
let headerY = lines.findIndex((line) => line.includes("A") && line.includes("B"))
|
|
397
|
+
expect(headerY).toBeGreaterThanOrEqual(0)
|
|
398
|
+
|
|
399
|
+
let borderXs = findVerticalBorderXs(renderer.currentRenderBuffer, headerY)
|
|
400
|
+
expect(borderXs[borderXs.length - 1]).toBeLessThan(33)
|
|
401
|
+
|
|
402
|
+
table.columnWidthMode = "full"
|
|
403
|
+
await renderOnce()
|
|
404
|
+
|
|
405
|
+
lines = captureFrame().split("\n")
|
|
406
|
+
headerY = lines.findIndex((line) => line.includes("A") && line.includes("B"))
|
|
407
|
+
expect(headerY).toBeGreaterThanOrEqual(0)
|
|
408
|
+
|
|
409
|
+
borderXs = findVerticalBorderXs(renderer.currentRenderBuffer, headerY)
|
|
410
|
+
expect(borderXs).toEqual([0, 17, 33])
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test("accepts columnFitter in options and setter", () => {
|
|
414
|
+
const table = new TextTableRenderable(renderer, {
|
|
415
|
+
columnFitter: "balanced",
|
|
416
|
+
content: [[cell("A")]],
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
expect(table.columnFitter).toBe("balanced")
|
|
420
|
+
|
|
421
|
+
table.columnFitter = "proportional"
|
|
422
|
+
expect(table.columnFitter).toBe("proportional")
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
test("balanced fitter keeps constrained columns visually closer", async () => {
|
|
426
|
+
const table = new TextTableRenderable(renderer, {
|
|
427
|
+
left: 0,
|
|
428
|
+
top: 0,
|
|
429
|
+
width: 58,
|
|
430
|
+
wrapMode: "word",
|
|
431
|
+
columnWidthMode: "full",
|
|
432
|
+
columnFitter: "proportional",
|
|
433
|
+
content: [
|
|
434
|
+
[
|
|
435
|
+
cell("Provider"),
|
|
436
|
+
cell("Compute Services"),
|
|
437
|
+
cell("Storage Solutions"),
|
|
438
|
+
cell("Pricing Model"),
|
|
439
|
+
cell("Regions"),
|
|
440
|
+
cell("Use Cases"),
|
|
441
|
+
],
|
|
442
|
+
[
|
|
443
|
+
cell("Amazon Web Services"),
|
|
444
|
+
cell("EC2 instances with extensive options for general, memory, and accelerated workloads"),
|
|
445
|
+
cell("S3 tiers, EBS, EFS, and archive classes for long retention"),
|
|
446
|
+
cell("Pay as you go, reserved terms, and discounted spot capacity"),
|
|
447
|
+
cell("Global regions and many edge locations"),
|
|
448
|
+
cell("Enterprise migration, analytics, ML, and backend services"),
|
|
449
|
+
],
|
|
450
|
+
],
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
renderer.root.add(table)
|
|
454
|
+
await renderOnce()
|
|
455
|
+
|
|
456
|
+
const proportionalFrame = captureFrame()
|
|
457
|
+
expect(proportionalFrame).toMatchSnapshot("fitter proportional constrained")
|
|
458
|
+
|
|
459
|
+
const getRenderedWidths = (): number[] => {
|
|
460
|
+
const lines = captureFrame().split("\n")
|
|
461
|
+
const headerY = lines.findIndex((line) => line.includes("Compute") && line.includes("Pricing"))
|
|
462
|
+
expect(headerY).toBeGreaterThanOrEqual(0)
|
|
463
|
+
|
|
464
|
+
const borderXs = findVerticalBorderXs(renderer.currentRenderBuffer, headerY)
|
|
465
|
+
expect(borderXs.length).toBeGreaterThan(2)
|
|
466
|
+
|
|
467
|
+
return borderXs.slice(1).map((x, idx) => x - borderXs[idx] - 1)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const proportionalWidths = getRenderedWidths()
|
|
471
|
+
const proportionalSpread = Math.max(...proportionalWidths) - Math.min(...proportionalWidths)
|
|
472
|
+
|
|
473
|
+
table.columnFitter = "balanced"
|
|
474
|
+
await renderOnce()
|
|
475
|
+
|
|
476
|
+
const balancedFrame = captureFrame()
|
|
477
|
+
expect(balancedFrame).toMatchSnapshot("fitter balanced constrained")
|
|
478
|
+
|
|
479
|
+
const balancedWidths = getRenderedWidths()
|
|
480
|
+
const balancedSpread = Math.max(...balancedWidths) - Math.min(...balancedWidths)
|
|
481
|
+
|
|
482
|
+
expect(table.columnFitter).toBe("balanced")
|
|
483
|
+
expect(balancedFrame).not.toBe(proportionalFrame)
|
|
484
|
+
expect(balancedWidths[0]).toBeGreaterThan(proportionalWidths[0] ?? 0)
|
|
485
|
+
expect(balancedSpread).toBeLessThan(proportionalSpread)
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
test("uses native border draw for inner-only mode", async () => {
|
|
489
|
+
const originalDrawGrid = OptimizedBuffer.prototype.drawGrid
|
|
490
|
+
let nativeCalls = 0
|
|
491
|
+
|
|
492
|
+
OptimizedBuffer.prototype.drawGrid = function (...args: Parameters<OptimizedBuffer["drawGrid"]>) {
|
|
493
|
+
nativeCalls += 1
|
|
494
|
+
return originalDrawGrid.apply(this, args)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const table = new TextTableRenderable(renderer, {
|
|
499
|
+
left: 0,
|
|
500
|
+
top: 0,
|
|
501
|
+
border: true,
|
|
502
|
+
outerBorder: false,
|
|
503
|
+
columnWidthMode: "content",
|
|
504
|
+
content: [
|
|
505
|
+
[cell("A"), cell("B")],
|
|
506
|
+
[cell("1"), cell("2")],
|
|
507
|
+
],
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
renderer.root.add(table)
|
|
511
|
+
await renderOnce()
|
|
512
|
+
|
|
513
|
+
const frame = captureFrame()
|
|
514
|
+
expect(frame).not.toContain("┌")
|
|
515
|
+
expect(frame).not.toContain("┐")
|
|
516
|
+
expect(frame).not.toContain("└")
|
|
517
|
+
expect(frame).not.toContain("┘")
|
|
518
|
+
expect(frame).toContain("┼")
|
|
519
|
+
expect(nativeCalls).toBe(1)
|
|
520
|
+
|
|
521
|
+
const lines = frame.split("\n")
|
|
522
|
+
const rowY = lines.findIndex((line) => line.includes("A") && line.includes("B"))
|
|
523
|
+
expect(rowY).toBeGreaterThanOrEqual(0)
|
|
524
|
+
|
|
525
|
+
const borderXs = findVerticalBorderXs(renderer.currentRenderBuffer, rowY)
|
|
526
|
+
expect(borderXs).toEqual([1])
|
|
527
|
+
} finally {
|
|
528
|
+
OptimizedBuffer.prototype.drawGrid = originalDrawGrid
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
test("defaults outerBorder to false when border is false", async () => {
|
|
533
|
+
const originalDrawGrid = OptimizedBuffer.prototype.drawGrid
|
|
534
|
+
let nativeCalls = 0
|
|
535
|
+
|
|
536
|
+
OptimizedBuffer.prototype.drawGrid = function (...args: Parameters<OptimizedBuffer["drawGrid"]>) {
|
|
537
|
+
nativeCalls += 1
|
|
538
|
+
return originalDrawGrid.apply(this, args)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
const table = new TextTableRenderable(renderer, {
|
|
543
|
+
left: 0,
|
|
544
|
+
top: 0,
|
|
545
|
+
border: false,
|
|
546
|
+
columnWidthMode: "content",
|
|
547
|
+
content: [
|
|
548
|
+
[cell("A"), cell("B")],
|
|
549
|
+
[cell("1"), cell("2")],
|
|
550
|
+
],
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
renderer.root.add(table)
|
|
554
|
+
await renderOnce()
|
|
555
|
+
|
|
556
|
+
const frame = captureFrame()
|
|
557
|
+
expect(table.outerBorder).toBe(false)
|
|
558
|
+
expect(BORDER_CHAR_PATTERN.test(frame)).toBe(false)
|
|
559
|
+
expect(frame).toContain("AB")
|
|
560
|
+
expect(nativeCalls).toBe(0)
|
|
561
|
+
} finally {
|
|
562
|
+
OptimizedBuffer.prototype.drawGrid = originalDrawGrid
|
|
563
|
+
}
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
test("allows outer border even when inner border is off", async () => {
|
|
567
|
+
const originalDrawGrid = OptimizedBuffer.prototype.drawGrid
|
|
568
|
+
let nativeCalls = 0
|
|
569
|
+
|
|
570
|
+
OptimizedBuffer.prototype.drawGrid = function (...args: Parameters<OptimizedBuffer["drawGrid"]>) {
|
|
571
|
+
nativeCalls += 1
|
|
572
|
+
return originalDrawGrid.apply(this, args)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
const table = new TextTableRenderable(renderer, {
|
|
577
|
+
left: 0,
|
|
578
|
+
top: 0,
|
|
579
|
+
border: false,
|
|
580
|
+
outerBorder: true,
|
|
581
|
+
content: [
|
|
582
|
+
[cell("A"), cell("B")],
|
|
583
|
+
[cell("1"), cell("2")],
|
|
584
|
+
],
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
renderer.root.add(table)
|
|
588
|
+
await renderOnce()
|
|
589
|
+
|
|
590
|
+
const frame = captureFrame()
|
|
591
|
+
expect(frame).toContain("┌")
|
|
592
|
+
expect(frame).toContain("┐")
|
|
593
|
+
expect(frame).toContain("└")
|
|
594
|
+
expect(frame).toContain("┘")
|
|
595
|
+
expect(frame).not.toContain("┼")
|
|
596
|
+
expect(nativeCalls).toBe(1)
|
|
597
|
+
} finally {
|
|
598
|
+
OptimizedBuffer.prototype.drawGrid = originalDrawGrid
|
|
599
|
+
}
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
test("rebuilds table when content setter is used", async () => {
|
|
603
|
+
const table = new TextTableRenderable(renderer, {
|
|
604
|
+
left: 0,
|
|
605
|
+
top: 0,
|
|
606
|
+
columnWidthMode: "content",
|
|
607
|
+
content: [[cell("A"), cell("B")]],
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
renderer.root.add(table)
|
|
611
|
+
await renderOnce()
|
|
612
|
+
|
|
613
|
+
const before = captureFrame()
|
|
614
|
+
|
|
615
|
+
table.content = [
|
|
616
|
+
[[bold("Col 1")], [bold("Col 2")]],
|
|
617
|
+
[cell("row-1"), cell("updated")],
|
|
618
|
+
[cell("row-2"), [green("active")]],
|
|
619
|
+
]
|
|
620
|
+
|
|
621
|
+
await renderOnce()
|
|
622
|
+
|
|
623
|
+
const after = captureFrame()
|
|
624
|
+
expect(before).not.toBe(after)
|
|
625
|
+
expect(after).toMatchSnapshot("content setter update")
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
test("renders a final bottom border", async () => {
|
|
629
|
+
const table = new TextTableRenderable(renderer, {
|
|
630
|
+
left: 0,
|
|
631
|
+
top: 0,
|
|
632
|
+
content: [
|
|
633
|
+
[[bold("A")], [bold("B")]],
|
|
634
|
+
[cell("1"), cell("2")],
|
|
635
|
+
],
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
renderer.root.add(table)
|
|
639
|
+
await renderOnce()
|
|
640
|
+
|
|
641
|
+
const frame = captureFrame()
|
|
642
|
+
const lines = frame
|
|
643
|
+
.split("\n")
|
|
644
|
+
.map((line) => line.trimEnd())
|
|
645
|
+
.filter((line) => line.length > 0)
|
|
646
|
+
|
|
647
|
+
const lastLine = lines[lines.length - 1] ?? ""
|
|
648
|
+
|
|
649
|
+
expect(lastLine).toContain("└")
|
|
650
|
+
expect(lastLine).toContain("┴")
|
|
651
|
+
expect(lastLine).toContain("┘")
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
test("keeps borders aligned with CJK and emoji content", async () => {
|
|
655
|
+
const content: TextTableContent = [
|
|
656
|
+
[[bold("Locale")], [bold("Sample")]],
|
|
657
|
+
[cell("ja-JP"), cell("東京で寿司 🍣")],
|
|
658
|
+
[cell("zh-CN"), cell("你好世界 🚀")],
|
|
659
|
+
[cell("ko-KR"), cell("한글 테스트 😄")],
|
|
660
|
+
]
|
|
661
|
+
|
|
662
|
+
const table = new TextTableRenderable(renderer, {
|
|
663
|
+
left: 0,
|
|
664
|
+
top: 0,
|
|
665
|
+
width: 36,
|
|
666
|
+
wrapMode: "none",
|
|
667
|
+
columnWidthMode: "content",
|
|
668
|
+
content,
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
renderer.root.add(table)
|
|
672
|
+
await renderOnce()
|
|
673
|
+
|
|
674
|
+
const frame = captureFrame()
|
|
675
|
+
expect(frame).toMatchSnapshot("unicode border alignment")
|
|
676
|
+
expect(frame).toContain("東京で寿司")
|
|
677
|
+
expect(frame).toContain("🚀")
|
|
678
|
+
expect(frame).toContain("😄")
|
|
679
|
+
|
|
680
|
+
const lines = frame.split("\n")
|
|
681
|
+
const headerY = lines.findIndex((line) => line.includes("Locale"))
|
|
682
|
+
expect(headerY).toBeGreaterThanOrEqual(0)
|
|
683
|
+
|
|
684
|
+
const buffer = renderer.currentRenderBuffer
|
|
685
|
+
const borderXs = findVerticalBorderXs(buffer, headerY)
|
|
686
|
+
expect(borderXs.length).toBe(3)
|
|
687
|
+
|
|
688
|
+
const sampleRowYs = [
|
|
689
|
+
lines.findIndex((line) => line.includes("ja-JP")),
|
|
690
|
+
lines.findIndex((line) => line.includes("zh-CN")),
|
|
691
|
+
lines.findIndex((line) => line.includes("ko-KR")),
|
|
692
|
+
]
|
|
693
|
+
|
|
694
|
+
for (const y of sampleRowYs) {
|
|
695
|
+
expect(y).toBeGreaterThanOrEqual(0)
|
|
696
|
+
for (const x of borderXs) {
|
|
697
|
+
expect(getCharAt(buffer, x, y)).toBe(VERTICAL_BORDER_CP)
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
test("wraps CJK and emoji without grapheme duplication", async () => {
|
|
703
|
+
const content: TextTableContent = [
|
|
704
|
+
[[bold("Item")], [bold("Details")]],
|
|
705
|
+
[cell("mixed"), cell("東京界 🌍 emoji wrapping continues across lines for width checks")],
|
|
706
|
+
[cell("emoji"), cell("Faces 😀😃😄 should remain stable")],
|
|
707
|
+
]
|
|
708
|
+
|
|
709
|
+
const table = new TextTableRenderable(renderer, {
|
|
710
|
+
left: 0,
|
|
711
|
+
top: 0,
|
|
712
|
+
width: 30,
|
|
713
|
+
wrapMode: "word",
|
|
714
|
+
content,
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
renderer.root.add(table)
|
|
718
|
+
await renderOnce()
|
|
719
|
+
|
|
720
|
+
const frame = captureFrame()
|
|
721
|
+
expect(frame).toMatchSnapshot("unicode wrapping")
|
|
722
|
+
expect(frame).not.toContain("�")
|
|
723
|
+
expect(countChar(frame, "界")).toBe(1)
|
|
724
|
+
expect(countChar(frame, "🌍")).toBe(1)
|
|
725
|
+
|
|
726
|
+
const lines = frame.split("\n")
|
|
727
|
+
const wrappedRowStartY = lines.findIndex((line) => line.includes("mix") && line.includes("東京界"))
|
|
728
|
+
const wrappedRowEndBorderY = lines.findIndex((line, idx) => idx > wrappedRowStartY && line.includes("├"))
|
|
729
|
+
|
|
730
|
+
expect(wrappedRowStartY).toBeGreaterThanOrEqual(0)
|
|
731
|
+
expect(wrappedRowEndBorderY).toBeGreaterThan(wrappedRowStartY)
|
|
732
|
+
|
|
733
|
+
const wrappedRowYs: number[] = []
|
|
734
|
+
for (let y = wrappedRowStartY; y < wrappedRowEndBorderY; y++) {
|
|
735
|
+
wrappedRowYs.push(y)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
expect(wrappedRowYs.length).toBeGreaterThan(1)
|
|
739
|
+
|
|
740
|
+
const headerY = lines.findIndex((line) => line.includes("Ite") && line.includes("Details"))
|
|
741
|
+
expect(headerY).toBeGreaterThanOrEqual(0)
|
|
742
|
+
|
|
743
|
+
const buffer = renderer.currentRenderBuffer
|
|
744
|
+
const borderXs = findVerticalBorderXs(buffer, headerY)
|
|
745
|
+
expect(borderXs.length).toBe(3)
|
|
746
|
+
|
|
747
|
+
for (const y of wrappedRowYs) {
|
|
748
|
+
for (const x of borderXs) {
|
|
749
|
+
expect(getCharAt(buffer, x, y)).toBe(VERTICAL_BORDER_CP)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
test("starts selection only on table cell content", async () => {
|
|
755
|
+
const table = new TextTableRenderable(renderer, {
|
|
756
|
+
left: 0,
|
|
757
|
+
top: 0,
|
|
758
|
+
content: [
|
|
759
|
+
[[bold("A")], [bold("B")]],
|
|
760
|
+
[cell("1"), cell("2")],
|
|
761
|
+
],
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
renderer.root.add(table)
|
|
765
|
+
await renderOnce()
|
|
766
|
+
|
|
767
|
+
expect(table.shouldStartSelection(table.x, table.y)).toBe(false)
|
|
768
|
+
expect(table.shouldStartSelection(table.x + 1, table.y)).toBe(false)
|
|
769
|
+
expect(table.shouldStartSelection(table.x, table.y + 1)).toBe(false)
|
|
770
|
+
expect(table.shouldStartSelection(table.x + 1, table.y + 1)).toBe(true)
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
test("selection text excludes border glyphs", async () => {
|
|
774
|
+
const table = new TextTableRenderable(renderer, {
|
|
775
|
+
left: 0,
|
|
776
|
+
top: 0,
|
|
777
|
+
columnWidthMode: "content",
|
|
778
|
+
content: [
|
|
779
|
+
[[bold("c1")], [bold("c2")]],
|
|
780
|
+
[cell("aa"), cell("bb")],
|
|
781
|
+
[cell("cc"), cell("dd")],
|
|
782
|
+
],
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
renderer.root.add(table)
|
|
786
|
+
await renderOnce()
|
|
787
|
+
|
|
788
|
+
await mockMouse.drag(table.x + 1, table.y + 1, table.x + 5, table.y + 3)
|
|
789
|
+
await renderOnce()
|
|
790
|
+
|
|
791
|
+
expect(table.hasSelection()).toBe(true)
|
|
792
|
+
|
|
793
|
+
const selected = table.getSelectedText()
|
|
794
|
+
expect(selected).toContain("c1\tc2")
|
|
795
|
+
expect(selected).toContain("aa\tb")
|
|
796
|
+
expect(selected).not.toContain("│")
|
|
797
|
+
expect(selected).not.toContain("┌")
|
|
798
|
+
expect(selected).not.toContain("┼")
|
|
799
|
+
|
|
800
|
+
const rendererSelection = renderer.getSelection()
|
|
801
|
+
expect(rendererSelection).not.toBeNull()
|
|
802
|
+
expect(rendererSelection?.getSelectedText()).not.toContain("│")
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
test("keeps partial selection when focus stays in the anchor cell", async () => {
|
|
806
|
+
const table = new TextTableRenderable(renderer, {
|
|
807
|
+
left: 0,
|
|
808
|
+
top: 0,
|
|
809
|
+
content: [[cell("alphabet"), cell("status")]],
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
renderer.root.add(table)
|
|
813
|
+
await renderOnce()
|
|
814
|
+
|
|
815
|
+
const anchor = findTextPoint(captureFrame(), "alphabet")
|
|
816
|
+
|
|
817
|
+
await mockMouse.drag(anchor.x + 3, anchor.y, anchor.x + 5, anchor.y)
|
|
818
|
+
await renderOnce()
|
|
819
|
+
|
|
820
|
+
expect(table.getSelectedText()).toBe("ha")
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
test("selects the full anchor cell once focus leaves that cell", async () => {
|
|
824
|
+
const table = new TextTableRenderable(renderer, {
|
|
825
|
+
left: 0,
|
|
826
|
+
top: 0,
|
|
827
|
+
content: [[cell("alphabet"), cell("status")]],
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
renderer.root.add(table)
|
|
831
|
+
await renderOnce()
|
|
832
|
+
|
|
833
|
+
const frame = captureFrame()
|
|
834
|
+
const anchor = findTextPoint(frame, "alphabet")
|
|
835
|
+
const focus = findTextPoint(frame, "status")
|
|
836
|
+
|
|
837
|
+
await mockMouse.drag(anchor.x + 3, anchor.y, focus.x + 2, focus.y)
|
|
838
|
+
await renderOnce()
|
|
839
|
+
|
|
840
|
+
const [firstCell] = table.getSelectedText().split("\t")
|
|
841
|
+
expect(firstCell).toBe("alphabet")
|
|
842
|
+
})
|
|
843
|
+
|
|
844
|
+
test("locks vertical drag to the anchor column while focus stays in that column", async () => {
|
|
845
|
+
const table = new TextTableRenderable(renderer, {
|
|
846
|
+
left: 0,
|
|
847
|
+
top: 0,
|
|
848
|
+
content: [
|
|
849
|
+
[cell("colA"), cell("colB"), cell("colC")],
|
|
850
|
+
[cell("a1"), cell("b1"), cell("c1")],
|
|
851
|
+
[cell("a2"), cell("b2"), cell("c2")],
|
|
852
|
+
[cell("a3"), cell("b3"), cell("c3")],
|
|
853
|
+
],
|
|
854
|
+
})
|
|
855
|
+
|
|
856
|
+
renderer.root.add(table)
|
|
857
|
+
await renderOnce()
|
|
858
|
+
|
|
859
|
+
const anchor = findTextPoint(captureFrame(), "colB")
|
|
860
|
+
|
|
861
|
+
await mockMouse.drag(anchor.x, anchor.y, anchor.x, table.y + table.height + 2)
|
|
862
|
+
await renderOnce()
|
|
863
|
+
|
|
864
|
+
expect(table.getSelectedText()).toBe("colB\nb1\nb2\nb3")
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
test("returns to normal grid selection after focus leaves the anchor column", async () => {
|
|
868
|
+
const table = new TextTableRenderable(renderer, {
|
|
869
|
+
left: 0,
|
|
870
|
+
top: 0,
|
|
871
|
+
content: [
|
|
872
|
+
[cell("colA"), cell("colB"), cell("colC")],
|
|
873
|
+
[cell("a1"), cell("b1"), cell("c1")],
|
|
874
|
+
[cell("a2"), cell("b2"), cell("c2")],
|
|
875
|
+
[cell("a3"), cell("b3"), cell("c3")],
|
|
876
|
+
],
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
renderer.root.add(table)
|
|
880
|
+
await renderOnce()
|
|
881
|
+
|
|
882
|
+
const frame = captureFrame()
|
|
883
|
+
const anchor = findTextPoint(frame, "colB")
|
|
884
|
+
const focus = findTextPoint(frame, "colC")
|
|
885
|
+
|
|
886
|
+
await mockMouse.drag(anchor.x, anchor.y, focus.x, table.y + table.height + 2)
|
|
887
|
+
await renderOnce()
|
|
888
|
+
|
|
889
|
+
expect(table.getSelectedText()).toBe("colB\tcolC\na1\tb1\tc1\na2\tb2\tc2\na3\tb3\tc3")
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
test("selection colors reset when drag retracts back to the anchor", async () => {
|
|
893
|
+
const defaultFg = RGBA.fromHex("#111111")
|
|
894
|
+
const defaultBg = RGBA.fromValues(0, 0, 0, 0)
|
|
895
|
+
const selectionFg = RGBA.fromHex("#fefefe")
|
|
896
|
+
const selectionBg = RGBA.fromHex("#cc5500")
|
|
897
|
+
|
|
898
|
+
const table = new TextTableRenderable(renderer, {
|
|
899
|
+
left: 0,
|
|
900
|
+
top: 0,
|
|
901
|
+
fg: defaultFg,
|
|
902
|
+
bg: "transparent",
|
|
903
|
+
selectionFg,
|
|
904
|
+
selectionBg,
|
|
905
|
+
columnWidthMode: "content",
|
|
906
|
+
content: [
|
|
907
|
+
["A", "B"],
|
|
908
|
+
["C", "D"],
|
|
909
|
+
],
|
|
910
|
+
})
|
|
911
|
+
|
|
912
|
+
renderer.root.add(table)
|
|
913
|
+
await renderOnce()
|
|
914
|
+
|
|
915
|
+
const anchorX = table.x + 1
|
|
916
|
+
const anchorY = table.y + 1
|
|
917
|
+
const farX = table.x + 3
|
|
918
|
+
const farY = table.y + 3
|
|
919
|
+
|
|
920
|
+
await mockMouse.pressDown(anchorX, anchorY)
|
|
921
|
+
await mockMouse.moveTo(farX, farY)
|
|
922
|
+
await renderOnce()
|
|
923
|
+
|
|
924
|
+
expect(table.hasSelection()).toBe(true)
|
|
925
|
+
|
|
926
|
+
let buffer = renderer.currentRenderBuffer
|
|
927
|
+
const selectedCells: Array<{ x: number; y: number }> = []
|
|
928
|
+
|
|
929
|
+
for (let y = table.y; y < table.y + table.height; y++) {
|
|
930
|
+
for (let x = table.x; x < table.x + table.width; x++) {
|
|
931
|
+
if (getBgAt(buffer, x, y).equals(selectionBg)) {
|
|
932
|
+
selectedCells.push({ x, y })
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
expect(selectedCells.length).toBeGreaterThan(1)
|
|
938
|
+
|
|
939
|
+
await mockMouse.moveTo(anchorX, anchorY)
|
|
940
|
+
await renderOnce()
|
|
941
|
+
|
|
942
|
+
const assertDeselectedCellsRestored = (frameBuffer: TestRenderer["currentRenderBuffer"]): void => {
|
|
943
|
+
const mismatches: string[] = []
|
|
944
|
+
|
|
945
|
+
for (const { x, y } of selectedCells) {
|
|
946
|
+
if (x === anchorX && y === anchorY) continue
|
|
947
|
+
|
|
948
|
+
const cp = getCharAt(frameBuffer, x, y)
|
|
949
|
+
if (cp === 0 || cp === VERTICAL_BORDER_CP) continue
|
|
950
|
+
|
|
951
|
+
if (!getFgAt(frameBuffer, x, y).equals(defaultFg)) {
|
|
952
|
+
mismatches.push(`fg@${x},${y}`)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (!getBgAt(frameBuffer, x, y).equals(defaultBg)) {
|
|
956
|
+
mismatches.push(`bg@${x},${y}`)
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
expect(mismatches).toEqual([])
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
buffer = renderer.currentRenderBuffer
|
|
964
|
+
expect(table.getSelectedText()).toBe("")
|
|
965
|
+
assertDeselectedCellsRestored(buffer)
|
|
966
|
+
|
|
967
|
+
await mockMouse.release(anchorX, anchorY)
|
|
968
|
+
await renderOnce()
|
|
969
|
+
|
|
970
|
+
buffer = renderer.currentRenderBuffer
|
|
971
|
+
assertDeselectedCellsRestored(buffer)
|
|
972
|
+
expect(getCharAt(buffer, farX, farY)).toBe("D".codePointAt(0))
|
|
973
|
+
})
|
|
974
|
+
|
|
975
|
+
test("does not start selection when drag begins on border", async () => {
|
|
976
|
+
const table = new TextTableRenderable(renderer, {
|
|
977
|
+
left: 0,
|
|
978
|
+
top: 0,
|
|
979
|
+
content: [
|
|
980
|
+
[[bold("A")], [bold("B")]],
|
|
981
|
+
[cell("1"), cell("2")],
|
|
982
|
+
],
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
renderer.root.add(table)
|
|
986
|
+
await renderOnce()
|
|
987
|
+
|
|
988
|
+
await mockMouse.drag(table.x, table.y, table.x + 4, table.y + 1)
|
|
989
|
+
await renderOnce()
|
|
990
|
+
|
|
991
|
+
expect(table.hasSelection()).toBe(false)
|
|
992
|
+
expect(table.getSelectedText()).toBe("")
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
test("clears stale per-cell local selection state between drags", async () => {
|
|
996
|
+
const table = new TextTableRenderable(renderer, {
|
|
997
|
+
left: 1,
|
|
998
|
+
top: 8,
|
|
999
|
+
width: 44,
|
|
1000
|
+
content: [
|
|
1001
|
+
[[bold("Service")], [bold("Status")], [bold("Notes")]],
|
|
1002
|
+
[cell("api"), [green("OK")], cell("latency 28ms")],
|
|
1003
|
+
[cell("worker"), [yellow("DEGRADED")], cell("queue depth: 124")],
|
|
1004
|
+
[cell("billing"), [red("ERROR")], cell("retrying payment provider")],
|
|
1005
|
+
],
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
renderer.root.add(table)
|
|
1009
|
+
await renderOnce()
|
|
1010
|
+
|
|
1011
|
+
await mockMouse.drag(14, 9, 40, 18)
|
|
1012
|
+
await renderOnce()
|
|
1013
|
+
|
|
1014
|
+
await mockMouse.click(27, 13)
|
|
1015
|
+
await renderOnce()
|
|
1016
|
+
|
|
1017
|
+
await mockMouse.pressDown(13, 9)
|
|
1018
|
+
await renderOnce()
|
|
1019
|
+
|
|
1020
|
+
await mockMouse.moveTo(13, 10)
|
|
1021
|
+
await renderOnce()
|
|
1022
|
+
await mockMouse.moveTo(13, 11)
|
|
1023
|
+
await renderOnce()
|
|
1024
|
+
await mockMouse.moveTo(13, 13)
|
|
1025
|
+
await renderOnce()
|
|
1026
|
+
await mockMouse.moveTo(13, 16)
|
|
1027
|
+
await renderOnce()
|
|
1028
|
+
await mockMouse.moveTo(13, 20)
|
|
1029
|
+
await renderOnce()
|
|
1030
|
+
|
|
1031
|
+
await mockMouse.release(13, 20)
|
|
1032
|
+
await renderOnce()
|
|
1033
|
+
|
|
1034
|
+
expect(table.getSelectedText()).toBe("Status\nOK\nDEGRADED\nERROR")
|
|
1035
|
+
})
|
|
1036
|
+
|
|
1037
|
+
test("reverse drag across full table keeps left cells selected", async () => {
|
|
1038
|
+
const table = new TextTableRenderable(renderer, {
|
|
1039
|
+
left: 0,
|
|
1040
|
+
top: 0,
|
|
1041
|
+
content: [
|
|
1042
|
+
[[bold("H1")], [bold("H2")], [bold("H3")]],
|
|
1043
|
+
[cell("R1C1"), cell("R1C2"), cell("R1C3")],
|
|
1044
|
+
[cell("R2C1"), cell("R2C2"), cell("R2C3")],
|
|
1045
|
+
[cell("R3C1"), cell("R3C2"), cell("R3C3")],
|
|
1046
|
+
],
|
|
1047
|
+
})
|
|
1048
|
+
|
|
1049
|
+
renderer.root.add(table)
|
|
1050
|
+
await renderOnce()
|
|
1051
|
+
|
|
1052
|
+
const start = findSelectablePoint(table, "bottom-right")
|
|
1053
|
+
const end = findSelectablePoint(table, "top-left")
|
|
1054
|
+
|
|
1055
|
+
await mockMouse.drag(start.x, start.y, end.x, end.y)
|
|
1056
|
+
await renderOnce()
|
|
1057
|
+
|
|
1058
|
+
const selected = table.getSelectedText()
|
|
1059
|
+
|
|
1060
|
+
expect(selected).toBe("H1\tH2\tH3\nR1C1\tR1C2\tR1C3\nR2C1\tR2C2\tR2C3\nR3C1\tR3C2\tR3C3")
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
test("reverse drag ending on left border still includes first column", async () => {
|
|
1064
|
+
const table = new TextTableRenderable(renderer, {
|
|
1065
|
+
left: 0,
|
|
1066
|
+
top: 0,
|
|
1067
|
+
content: [
|
|
1068
|
+
[[bold("Name")], [bold("Status")]],
|
|
1069
|
+
[cell("Alice"), cell("Done")],
|
|
1070
|
+
[cell("Bob"), cell("In Progress")],
|
|
1071
|
+
],
|
|
1072
|
+
})
|
|
1073
|
+
|
|
1074
|
+
renderer.root.add(table)
|
|
1075
|
+
await renderOnce()
|
|
1076
|
+
|
|
1077
|
+
const start = findSelectablePoint(table, "bottom-right")
|
|
1078
|
+
const endX = table.x
|
|
1079
|
+
const endY = findSelectablePoint(table, "top-left").y
|
|
1080
|
+
|
|
1081
|
+
await mockMouse.drag(start.x, start.y, endX, endY)
|
|
1082
|
+
await renderOnce()
|
|
1083
|
+
|
|
1084
|
+
const selected = table.getSelectedText()
|
|
1085
|
+
|
|
1086
|
+
expect(selected).toContain("Name")
|
|
1087
|
+
expect(selected).toContain("Alice")
|
|
1088
|
+
expect(selected).toContain("Bob")
|
|
1089
|
+
})
|
|
1090
|
+
|
|
1091
|
+
test("keeps full wrapped table layouts after a wide-to-narrow demo-style resize", async () => {
|
|
1092
|
+
resizeRenderer(108, 38)
|
|
1093
|
+
await renderOnce()
|
|
1094
|
+
|
|
1095
|
+
const primaryContent: TextTableContent = [
|
|
1096
|
+
[[bold("Task")], [bold("Owner")], [bold("ETA")]],
|
|
1097
|
+
[
|
|
1098
|
+
cell(
|
|
1099
|
+
"Wrap regression in operational status dashboard with dynamic row heights and constrained layout validation",
|
|
1100
|
+
),
|
|
1101
|
+
cell("core platform and runtime reliability squad"),
|
|
1102
|
+
cell(
|
|
1103
|
+
"done after validating none, word, and char wrap modes across narrow, medium, wide, and ultra-wide terminal widths",
|
|
1104
|
+
),
|
|
1105
|
+
],
|
|
1106
|
+
[
|
|
1107
|
+
cell(
|
|
1108
|
+
"Unicode layout stabilization for mixed Latin, punctuation, symbols, and long identifiers in adjacent columns",
|
|
1109
|
+
),
|
|
1110
|
+
cell("render pipeline maintainers with fallback shaping support"),
|
|
1111
|
+
cell(
|
|
1112
|
+
"in review with follow-up checks for border style transitions, cell padding variants, and selection range consistency",
|
|
1113
|
+
),
|
|
1114
|
+
],
|
|
1115
|
+
[
|
|
1116
|
+
cell(
|
|
1117
|
+
"Snapshot pass for table rendering in content mode and full mode with heavy and double border combinations",
|
|
1118
|
+
),
|
|
1119
|
+
cell("qa automation and visual diff triage group"),
|
|
1120
|
+
cell(
|
|
1121
|
+
"today pending final baseline updates for oversized fixtures that intentionally stress wrapping behavior on high-resolution terminals",
|
|
1122
|
+
),
|
|
1123
|
+
],
|
|
1124
|
+
[
|
|
1125
|
+
cell(
|
|
1126
|
+
"Document edge cases where long tokens without spaces force char wrapping and reveal per-cell clipping regressions",
|
|
1127
|
+
),
|
|
1128
|
+
cell("developer experience and docs tooling"),
|
|
1129
|
+
cell(
|
|
1130
|
+
"planned for this sprint once final reproducible examples are captured and linked to regression tracking tickets",
|
|
1131
|
+
),
|
|
1132
|
+
],
|
|
1133
|
+
[
|
|
1134
|
+
cell(
|
|
1135
|
+
"Performance sweep of wrapping algorithm under large datasets to confirm stable frame times during rapid key toggling",
|
|
1136
|
+
),
|
|
1137
|
+
cell("runtime performance task force"),
|
|
1138
|
+
cell("scheduled after review, with benchmark runs on laptop and desktop terminals at 200-plus column widths"),
|
|
1139
|
+
],
|
|
1140
|
+
]
|
|
1141
|
+
|
|
1142
|
+
const unicodeContent: TextTableContent = [
|
|
1143
|
+
[[bold("Column")], [bold("Wrapped Text")]],
|
|
1144
|
+
[
|
|
1145
|
+
cell("mixed-languages"),
|
|
1146
|
+
cell(
|
|
1147
|
+
"CJK and emoji wrapping stress case: こんにちは世界 and 안녕하세요 세계 and 你好,世界 followed by long English prose that keeps flowing to test whether each cell wraps naturally even when the terminal is extremely wide and the row still needs multiple visual lines for readability 🌍🚀",
|
|
1148
|
+
),
|
|
1149
|
+
],
|
|
1150
|
+
[
|
|
1151
|
+
cell("emoji-and-symbols"),
|
|
1152
|
+
cell(
|
|
1153
|
+
"Faces 😀😃😄😁😆 plus symbols 🧪📦🛰️🔧📊 mixed with version tags like release-candidate-build-2026-02-very-long-token-without-breaks to ensure char wrapping remains stable and no glyph alignment issues appear at column boundaries",
|
|
1154
|
+
),
|
|
1155
|
+
],
|
|
1156
|
+
[
|
|
1157
|
+
cell("long-cjk-phrase"),
|
|
1158
|
+
cell(
|
|
1159
|
+
"長文の日本語テキストと中文段落和한국어문장을連続して配置し、その後に additional English context describing renderer behavior, border intersection handling, and selection extraction so that this single cell remains a reliable wrapping torture test.",
|
|
1160
|
+
),
|
|
1161
|
+
],
|
|
1162
|
+
[
|
|
1163
|
+
cell("mixed-punctuation"),
|
|
1164
|
+
cell(
|
|
1165
|
+
"Wrap behavior with punctuation-heavy content: [alpha]{beta}(gamma)<delta>|epsilon| then repeated fragments, commas, semicolons, and slashes to verify token boundaries do not break border drawing logic or spacing consistency in neighboring columns.",
|
|
1166
|
+
),
|
|
1167
|
+
],
|
|
1168
|
+
]
|
|
1169
|
+
|
|
1170
|
+
const container = new BoxRenderable(renderer, {
|
|
1171
|
+
width: "100%",
|
|
1172
|
+
height: "100%",
|
|
1173
|
+
flexDirection: "column",
|
|
1174
|
+
padding: 1,
|
|
1175
|
+
gap: 1,
|
|
1176
|
+
})
|
|
1177
|
+
|
|
1178
|
+
const tableAreaScrollBox = new ScrollBoxRenderable(renderer, {
|
|
1179
|
+
width: "100%",
|
|
1180
|
+
flexGrow: 1,
|
|
1181
|
+
flexShrink: 1,
|
|
1182
|
+
scrollY: true,
|
|
1183
|
+
scrollX: false,
|
|
1184
|
+
border: false,
|
|
1185
|
+
contentOptions: {
|
|
1186
|
+
flexDirection: "column",
|
|
1187
|
+
gap: 1,
|
|
1188
|
+
},
|
|
1189
|
+
})
|
|
1190
|
+
|
|
1191
|
+
const controlsText = new TextRenderable(renderer, {
|
|
1192
|
+
content: "TextTable Demo",
|
|
1193
|
+
wrapMode: "word",
|
|
1194
|
+
selectable: false,
|
|
1195
|
+
})
|
|
1196
|
+
|
|
1197
|
+
const primaryLabel = new TextRenderable(renderer, {
|
|
1198
|
+
content: "Operational Table",
|
|
1199
|
+
selectable: false,
|
|
1200
|
+
})
|
|
1201
|
+
|
|
1202
|
+
const primaryTable = new TextTableRenderable(renderer, {
|
|
1203
|
+
width: "100%",
|
|
1204
|
+
wrapMode: "word",
|
|
1205
|
+
content: primaryContent,
|
|
1206
|
+
})
|
|
1207
|
+
|
|
1208
|
+
const unicodeLabel = new TextRenderable(renderer, {
|
|
1209
|
+
content: "Unicode/CJK/Emoji Table",
|
|
1210
|
+
selectable: false,
|
|
1211
|
+
})
|
|
1212
|
+
|
|
1213
|
+
const unicodeTable = new TextTableRenderable(renderer, {
|
|
1214
|
+
width: "100%",
|
|
1215
|
+
wrapMode: "word",
|
|
1216
|
+
content: unicodeContent,
|
|
1217
|
+
})
|
|
1218
|
+
|
|
1219
|
+
const selectionBox = new BoxRenderable(renderer, {
|
|
1220
|
+
width: "100%",
|
|
1221
|
+
height: 10,
|
|
1222
|
+
flexGrow: 0,
|
|
1223
|
+
flexShrink: 0,
|
|
1224
|
+
border: true,
|
|
1225
|
+
title: "Selected Text",
|
|
1226
|
+
titleAlignment: "left",
|
|
1227
|
+
padding: 1,
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
tableAreaScrollBox.add(controlsText)
|
|
1231
|
+
tableAreaScrollBox.add(primaryLabel)
|
|
1232
|
+
tableAreaScrollBox.add(primaryTable)
|
|
1233
|
+
tableAreaScrollBox.add(unicodeLabel)
|
|
1234
|
+
tableAreaScrollBox.add(unicodeTable)
|
|
1235
|
+
|
|
1236
|
+
container.add(tableAreaScrollBox)
|
|
1237
|
+
container.add(selectionBox)
|
|
1238
|
+
renderer.root.add(container)
|
|
1239
|
+
|
|
1240
|
+
await renderOnce()
|
|
1241
|
+
|
|
1242
|
+
resizeRenderer(72, 38)
|
|
1243
|
+
await renderOnce()
|
|
1244
|
+
await renderOnce()
|
|
1245
|
+
|
|
1246
|
+
const expectedPrimaryFrame = await renderStandaloneTableBlock(primaryTable.width, primaryContent, (line) =>
|
|
1247
|
+
line.includes("Task"),
|
|
1248
|
+
)
|
|
1249
|
+
const expectedUnicodeFrame = await renderStandaloneTableBlock(unicodeTable.width, unicodeContent, (line) =>
|
|
1250
|
+
line.includes("Wrapped"),
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
expect(expectedPrimaryFrame).toMatchSnapshot("demo resize expected primary table")
|
|
1254
|
+
expect(expectedUnicodeFrame).toMatchSnapshot("demo resize expected unicode table")
|
|
1255
|
+
|
|
1256
|
+
const resizedFrame = captureFrame()
|
|
1257
|
+
expect(resizedFrame).toContain("Operational Table")
|
|
1258
|
+
expect(resizedFrame).toContain("Task")
|
|
1259
|
+
|
|
1260
|
+
const contentBottom = getScrollContentBottom(tableAreaScrollBox)
|
|
1261
|
+
expect(contentBottom).toBeGreaterThan(tableAreaScrollBox.viewport.height)
|
|
1262
|
+
expect(tableAreaScrollBox.scrollHeight).toBe(contentBottom)
|
|
1263
|
+
|
|
1264
|
+
const maxScrollTop = Math.max(0, tableAreaScrollBox.scrollHeight - tableAreaScrollBox.viewport.height)
|
|
1265
|
+
expect(maxScrollTop).toBeGreaterThan(0)
|
|
1266
|
+
|
|
1267
|
+
tableAreaScrollBox.scrollTop = maxScrollTop
|
|
1268
|
+
await renderOnce()
|
|
1269
|
+
|
|
1270
|
+
const scrolledToBottomFrame = captureFrame()
|
|
1271
|
+
expect(scrolledToBottomFrame).toContain("epsilon")
|
|
1272
|
+
})
|
|
1273
|
+
|
|
1274
|
+
test("keeps scroll height aligned with content bottom after word-wrap resize", async () => {
|
|
1275
|
+
resizeRenderer(104, 34)
|
|
1276
|
+
await renderOnce()
|
|
1277
|
+
|
|
1278
|
+
const tableContent: TextTableContent = [
|
|
1279
|
+
[[bold("Key")], [bold("Value")]],
|
|
1280
|
+
[
|
|
1281
|
+
cell("alpha"),
|
|
1282
|
+
cell(
|
|
1283
|
+
"word wrapping should preserve intrinsic table height even when parent measure passes provide a smaller at-most height",
|
|
1284
|
+
),
|
|
1285
|
+
],
|
|
1286
|
+
[
|
|
1287
|
+
cell("beta"),
|
|
1288
|
+
cell(
|
|
1289
|
+
"this row is intentionally verbose and pushes the wrapped table height so that scrolling must include all visual lines",
|
|
1290
|
+
),
|
|
1291
|
+
],
|
|
1292
|
+
[cell("marker"), cell("ENDWORD")],
|
|
1293
|
+
]
|
|
1294
|
+
|
|
1295
|
+
const root = new BoxRenderable(renderer, {
|
|
1296
|
+
width: "100%",
|
|
1297
|
+
height: "100%",
|
|
1298
|
+
flexDirection: "column",
|
|
1299
|
+
padding: 1,
|
|
1300
|
+
gap: 1,
|
|
1301
|
+
})
|
|
1302
|
+
|
|
1303
|
+
const scrollBox = new ScrollBoxRenderable(renderer, {
|
|
1304
|
+
width: "100%",
|
|
1305
|
+
flexGrow: 1,
|
|
1306
|
+
flexShrink: 1,
|
|
1307
|
+
scrollY: true,
|
|
1308
|
+
scrollX: false,
|
|
1309
|
+
border: false,
|
|
1310
|
+
contentOptions: {
|
|
1311
|
+
flexDirection: "column",
|
|
1312
|
+
gap: 1,
|
|
1313
|
+
},
|
|
1314
|
+
})
|
|
1315
|
+
|
|
1316
|
+
const table = new TextTableRenderable(renderer, {
|
|
1317
|
+
width: "100%",
|
|
1318
|
+
wrapMode: "word",
|
|
1319
|
+
content: tableContent,
|
|
1320
|
+
})
|
|
1321
|
+
|
|
1322
|
+
root.add(scrollBox)
|
|
1323
|
+
root.add(
|
|
1324
|
+
new BoxRenderable(renderer, {
|
|
1325
|
+
width: "100%",
|
|
1326
|
+
height: 16,
|
|
1327
|
+
flexGrow: 0,
|
|
1328
|
+
flexShrink: 0,
|
|
1329
|
+
}),
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
scrollBox.add(new TextRenderable(renderer, { content: "Word Wrap Table", selectable: false }))
|
|
1333
|
+
scrollBox.add(table)
|
|
1334
|
+
renderer.root.add(root)
|
|
1335
|
+
|
|
1336
|
+
await renderOnce()
|
|
1337
|
+
|
|
1338
|
+
resizeRenderer(66, 34)
|
|
1339
|
+
await renderOnce()
|
|
1340
|
+
await renderOnce()
|
|
1341
|
+
|
|
1342
|
+
const contentBottom = getScrollContentBottom(scrollBox)
|
|
1343
|
+
expect(contentBottom).toBeGreaterThan(scrollBox.viewport.height)
|
|
1344
|
+
expect(scrollBox.scrollHeight).toBe(contentBottom)
|
|
1345
|
+
|
|
1346
|
+
scrollBox.scrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
|
|
1347
|
+
await renderOnce()
|
|
1348
|
+
|
|
1349
|
+
expect(captureFrame()).toContain("ENDWORD")
|
|
1350
|
+
})
|
|
1351
|
+
|
|
1352
|
+
test("keeps scroll height aligned with content bottom in char-wrap full mode", async () => {
|
|
1353
|
+
resizeRenderer(104, 34)
|
|
1354
|
+
await renderOnce()
|
|
1355
|
+
|
|
1356
|
+
const tableContent: TextTableContent = [
|
|
1357
|
+
[[bold("Name")], [bold("Payload")]],
|
|
1358
|
+
[cell("row-1"), cell("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")],
|
|
1359
|
+
[cell("row-2"), cell("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")],
|
|
1360
|
+
[cell("row-3"), cell("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC")],
|
|
1361
|
+
[cell("marker"), cell("ENDCHAR")],
|
|
1362
|
+
]
|
|
1363
|
+
|
|
1364
|
+
const root = new BoxRenderable(renderer, {
|
|
1365
|
+
width: "100%",
|
|
1366
|
+
height: "100%",
|
|
1367
|
+
flexDirection: "column",
|
|
1368
|
+
padding: 1,
|
|
1369
|
+
gap: 1,
|
|
1370
|
+
})
|
|
1371
|
+
|
|
1372
|
+
const scrollBox = new ScrollBoxRenderable(renderer, {
|
|
1373
|
+
width: "100%",
|
|
1374
|
+
flexGrow: 1,
|
|
1375
|
+
flexShrink: 1,
|
|
1376
|
+
scrollY: true,
|
|
1377
|
+
scrollX: false,
|
|
1378
|
+
border: false,
|
|
1379
|
+
contentOptions: {
|
|
1380
|
+
flexDirection: "column",
|
|
1381
|
+
gap: 1,
|
|
1382
|
+
},
|
|
1383
|
+
})
|
|
1384
|
+
|
|
1385
|
+
const table = new TextTableRenderable(renderer, {
|
|
1386
|
+
width: "100%",
|
|
1387
|
+
wrapMode: "char",
|
|
1388
|
+
columnWidthMode: "full",
|
|
1389
|
+
content: tableContent,
|
|
1390
|
+
})
|
|
1391
|
+
|
|
1392
|
+
root.add(scrollBox)
|
|
1393
|
+
root.add(
|
|
1394
|
+
new BoxRenderable(renderer, {
|
|
1395
|
+
width: "100%",
|
|
1396
|
+
height: 16,
|
|
1397
|
+
flexGrow: 0,
|
|
1398
|
+
flexShrink: 0,
|
|
1399
|
+
}),
|
|
1400
|
+
)
|
|
1401
|
+
|
|
1402
|
+
scrollBox.add(new TextRenderable(renderer, { content: "Char Wrap Fill Table", selectable: false }))
|
|
1403
|
+
scrollBox.add(table)
|
|
1404
|
+
renderer.root.add(root)
|
|
1405
|
+
|
|
1406
|
+
await renderOnce()
|
|
1407
|
+
|
|
1408
|
+
resizeRenderer(58, 34)
|
|
1409
|
+
await renderOnce()
|
|
1410
|
+
await renderOnce()
|
|
1411
|
+
|
|
1412
|
+
const contentBottom = getScrollContentBottom(scrollBox)
|
|
1413
|
+
expect(contentBottom).toBeGreaterThan(scrollBox.viewport.height)
|
|
1414
|
+
expect(scrollBox.scrollHeight).toBe(contentBottom)
|
|
1415
|
+
|
|
1416
|
+
scrollBox.scrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
|
|
1417
|
+
await renderOnce()
|
|
1418
|
+
|
|
1419
|
+
expect(captureFrame()).toContain("ENDCHAR")
|
|
1420
|
+
})
|
|
1421
|
+
})
|