@fairyhunter13/opentui-core 0.1.113 → 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 +62 -53
- 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-9vwc3fg6.js +0 -12260
- package/index-9vwc3fg6.js.map +0 -42
- package/index-dcj62y8t.js +0 -20614
- package/index-dcj62y8t.js.map +0 -67
- package/index-f7n39gpy.js +0 -411
- package/index-f7n39gpy.js.map +0 -10
- 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
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,2681 @@
|
|
|
1
|
+
import { ANSI } from "./ansi.js"
|
|
2
|
+
import { Renderable, RootRenderable } from "./Renderable.js"
|
|
3
|
+
import {
|
|
4
|
+
DebugOverlayCorner,
|
|
5
|
+
type CursorStyleOptions,
|
|
6
|
+
type MousePointerStyle,
|
|
7
|
+
type RenderContext,
|
|
8
|
+
type ThemeMode,
|
|
9
|
+
type ViewportBounds,
|
|
10
|
+
type WidthMethod,
|
|
11
|
+
} from "./types.js"
|
|
12
|
+
import { RGBA, parseColor, type ColorInput } from "./lib/RGBA.js"
|
|
13
|
+
import type { Pointer } from "bun:ffi"
|
|
14
|
+
import { OptimizedBuffer } from "./buffer.js"
|
|
15
|
+
import { resolveRenderLib, type RenderLib } from "./zig.js"
|
|
16
|
+
import { TerminalConsole, type ConsoleOptions, capture } from "./console.js"
|
|
17
|
+
import { type MouseEventType, type RawMouseEvent, type ScrollInfo } from "./lib/parse.mouse.js"
|
|
18
|
+
import { Selection } from "./lib/selection.js"
|
|
19
|
+
import { Clipboard, type ClipboardTarget } from "./lib/clipboard.js"
|
|
20
|
+
import { EventEmitter } from "events"
|
|
21
|
+
import { destroySingleton, hasSingleton, singleton } from "./lib/singleton.js"
|
|
22
|
+
import { getObjectsInViewport } from "./lib/objects-in-viewport.js"
|
|
23
|
+
import { KeyHandler, InternalKeyHandler } from "./lib/KeyHandler.js"
|
|
24
|
+
import { isEditBufferRenderable, type EditBufferRenderable } from "./renderables/EditBufferRenderable.js"
|
|
25
|
+
import { env, registerEnvVar } from "./lib/env.js"
|
|
26
|
+
import { getTreeSitterClient } from "./lib/tree-sitter/index.js"
|
|
27
|
+
import {
|
|
28
|
+
createTerminalPalette,
|
|
29
|
+
type TerminalPaletteDetector,
|
|
30
|
+
type TerminalColors,
|
|
31
|
+
type GetPaletteOptions,
|
|
32
|
+
} from "./lib/terminal-palette.js"
|
|
33
|
+
import {
|
|
34
|
+
isCapabilityResponse,
|
|
35
|
+
isPixelResolutionResponse,
|
|
36
|
+
parsePixelResolution,
|
|
37
|
+
} from "./lib/terminal-capability-detection.js"
|
|
38
|
+
import { type Clock, type TimerHandle, SystemClock } from "./lib/clock.js"
|
|
39
|
+
import { StdinParser, type StdinEvent, type StdinParserProtocolContext } from "./lib/stdin-parser.js"
|
|
40
|
+
|
|
41
|
+
registerEnvVar({
|
|
42
|
+
name: "OTUI_DUMP_CAPTURES",
|
|
43
|
+
description: "Dump captured stdout and console caches when the renderer exit handler runs.",
|
|
44
|
+
type: "boolean",
|
|
45
|
+
default: false,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
registerEnvVar({
|
|
49
|
+
name: "OTUI_NO_NATIVE_RENDER",
|
|
50
|
+
description:
|
|
51
|
+
"Skip the Zig/native frame renderer. Useful for debugging the render loop; split-footer stdout flushing may still write ANSI.",
|
|
52
|
+
type: "boolean",
|
|
53
|
+
default: false,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
registerEnvVar({
|
|
57
|
+
name: "OTUI_USE_ALTERNATE_SCREEN",
|
|
58
|
+
description: "When explicitly set, force screen mode selection: true=alternate-screen, false=main-screen.",
|
|
59
|
+
type: "boolean",
|
|
60
|
+
default: true,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
registerEnvVar({
|
|
64
|
+
name: "OTUI_OVERRIDE_STDOUT",
|
|
65
|
+
description: "When explicitly set, force stdout routing: false=passthrough, true=capture in split-footer mode.",
|
|
66
|
+
type: "boolean",
|
|
67
|
+
default: true,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
registerEnvVar({
|
|
71
|
+
name: "OTUI_DEBUG",
|
|
72
|
+
description: "Enable debug mode to capture all raw input for debugging purposes.",
|
|
73
|
+
type: "boolean",
|
|
74
|
+
default: false,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
registerEnvVar({
|
|
78
|
+
name: "OTUI_SHOW_STATS",
|
|
79
|
+
description: "Show the debug overlay at startup.",
|
|
80
|
+
type: "boolean",
|
|
81
|
+
default: false,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
export interface CliRendererConfig {
|
|
85
|
+
// Read input from this stream. Defaults to process.stdin.
|
|
86
|
+
stdin?: NodeJS.ReadStream
|
|
87
|
+
|
|
88
|
+
// Use a custom stdout stream for size detection and stdout interception.
|
|
89
|
+
// Native frame output still goes to the real TTY.
|
|
90
|
+
stdout?: NodeJS.WriteStream
|
|
91
|
+
|
|
92
|
+
// Tell the native renderer it is driving a remote terminal.
|
|
93
|
+
remote?: boolean
|
|
94
|
+
|
|
95
|
+
// Skip terminal setup. Useful in tests.
|
|
96
|
+
testing?: boolean
|
|
97
|
+
|
|
98
|
+
// Call renderer.destroy() when Ctrl+C is pressed. Defaults to true.
|
|
99
|
+
exitOnCtrlC?: boolean
|
|
100
|
+
|
|
101
|
+
// Clean up on these signals. Defaults to the common termination signals.
|
|
102
|
+
exitSignals?: NodeJS.Signals[]
|
|
103
|
+
|
|
104
|
+
// Forward these env var names to native terminal detection.
|
|
105
|
+
forwardEnvKeys?: string[]
|
|
106
|
+
|
|
107
|
+
// Wait this long before handling resize events. Defaults to 100 ms.
|
|
108
|
+
debounceDelay?: number
|
|
109
|
+
|
|
110
|
+
// Aim for this many frames per second in continuous mode. Defaults to 30.
|
|
111
|
+
targetFps?: number
|
|
112
|
+
|
|
113
|
+
// Cap immediate re-renders at this frame rate. Defaults to 60.
|
|
114
|
+
maxFps?: number
|
|
115
|
+
|
|
116
|
+
// Emit memory snapshots on this interval in ms. Set 0 to disable.
|
|
117
|
+
memorySnapshotInterval?: number
|
|
118
|
+
|
|
119
|
+
// Render from a separate thread when the platform supports it.
|
|
120
|
+
useThread?: boolean
|
|
121
|
+
|
|
122
|
+
// Collect frame timing stats for the debug overlay.
|
|
123
|
+
gatherStats?: boolean
|
|
124
|
+
|
|
125
|
+
// Keep this many timing samples. Defaults to 300.
|
|
126
|
+
maxStatSamples?: number
|
|
127
|
+
|
|
128
|
+
// Pass options to the built-in console overlay.
|
|
129
|
+
consoleOptions?: Omit<ConsoleOptions, "clock">
|
|
130
|
+
|
|
131
|
+
// Run these hooks after each render pass.
|
|
132
|
+
postProcessFns?: ((buffer: OptimizedBuffer, deltaTime: number) => void)[]
|
|
133
|
+
|
|
134
|
+
// Track mouse move events. Defaults to true.
|
|
135
|
+
enableMouseMovement?: boolean
|
|
136
|
+
|
|
137
|
+
// Enable mouse input. Defaults to true.
|
|
138
|
+
useMouse?: boolean
|
|
139
|
+
|
|
140
|
+
// Focus the nearest focusable renderable on left click. Defaults to true.
|
|
141
|
+
autoFocus?: boolean
|
|
142
|
+
|
|
143
|
+
// Choose where the renderer owns terminal space. Defaults to "alternate-screen".
|
|
144
|
+
screenMode?: ScreenMode
|
|
145
|
+
|
|
146
|
+
// Set the requested footer height for "split-footer". Defaults to 12.
|
|
147
|
+
footerHeight?: number
|
|
148
|
+
|
|
149
|
+
// Choose what happens to writes that go through `stdout.write`.
|
|
150
|
+
externalOutputMode?: ExternalOutputMode
|
|
151
|
+
|
|
152
|
+
// Choose what the built-in console overlay does.
|
|
153
|
+
consoleMode?: ConsoleMode
|
|
154
|
+
|
|
155
|
+
// Set Kitty keyboard protocol flags, or null to disable them.
|
|
156
|
+
useKittyKeyboard?: KittyKeyboardOptions | null
|
|
157
|
+
|
|
158
|
+
// Fill the render buffer with this background color. Default transparent.
|
|
159
|
+
backgroundColor?: ColorInput
|
|
160
|
+
|
|
161
|
+
// Open the console overlay on uncaught errors. Defaults to true in development.
|
|
162
|
+
openConsoleOnError?: boolean
|
|
163
|
+
|
|
164
|
+
// Run these input handlers before the built-in handlers.
|
|
165
|
+
prependInputHandlers?: ((sequence: string) => boolean)[]
|
|
166
|
+
|
|
167
|
+
// Cap the stdin parser buffer size in bytes. Defaults to 64 MB.
|
|
168
|
+
stdinParserMaxBufferBytes?: number
|
|
169
|
+
|
|
170
|
+
// Use a custom clock for timers and tests.
|
|
171
|
+
clock?: Clock
|
|
172
|
+
|
|
173
|
+
// Run after destroy() finishes cleanup.
|
|
174
|
+
onDestroy?: () => void
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Controls how the renderer uses terminal space:
|
|
178
|
+
//
|
|
179
|
+
// - "alternate-screen": Use the terminal's alternate screen buffer.
|
|
180
|
+
//
|
|
181
|
+
// - "main-screen": Render on the main screen.
|
|
182
|
+
//
|
|
183
|
+
// - "split-footer": Keep the renderer in a reserved footer on the main screen.
|
|
184
|
+
export type ScreenMode = "alternate-screen" | "main-screen" | "split-footer"
|
|
185
|
+
|
|
186
|
+
// Controls writes that go through the configured `stdout.write`.
|
|
187
|
+
//
|
|
188
|
+
// - "capture-stdout": Queue stdout and replay it above the split footer.
|
|
189
|
+
// Only valid with "split-footer".
|
|
190
|
+
//
|
|
191
|
+
// - "passthrough": Leave stdout alone.
|
|
192
|
+
export type ExternalOutputMode = "capture-stdout" | "passthrough"
|
|
193
|
+
|
|
194
|
+
// Controls the built-in console overlay:
|
|
195
|
+
//
|
|
196
|
+
// - "console-overlay": Capture `console.*` output and show the overlay.
|
|
197
|
+
//
|
|
198
|
+
// - "disabled": Hide the overlay. `OTUI_USE_CONSOLE` controls global console
|
|
199
|
+
// capture.
|
|
200
|
+
export type ConsoleMode = "console-overlay" | "disabled"
|
|
201
|
+
|
|
202
|
+
export type PixelResolution = {
|
|
203
|
+
width: number
|
|
204
|
+
height: number
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const DEFAULT_FOOTER_HEIGHT = 12
|
|
208
|
+
|
|
209
|
+
function normalizeFooterHeight(footerHeight: number | undefined): number {
|
|
210
|
+
if (footerHeight === undefined) {
|
|
211
|
+
return DEFAULT_FOOTER_HEIGHT
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!Number.isFinite(footerHeight)) {
|
|
215
|
+
throw new Error("footerHeight must be a finite number")
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const normalizedFooterHeight = Math.trunc(footerHeight)
|
|
219
|
+
if (normalizedFooterHeight <= 0) {
|
|
220
|
+
throw new Error("footerHeight must be greater than 0")
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return normalizedFooterHeight
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function resolveModes(config: CliRendererConfig): {
|
|
227
|
+
screenMode: ScreenMode
|
|
228
|
+
footerHeight: number
|
|
229
|
+
externalOutputMode: ExternalOutputMode
|
|
230
|
+
} {
|
|
231
|
+
let screenMode = config.screenMode ?? "alternate-screen"
|
|
232
|
+
if (process.env.OTUI_USE_ALTERNATE_SCREEN !== undefined) {
|
|
233
|
+
screenMode = env.OTUI_USE_ALTERNATE_SCREEN ? "alternate-screen" : "main-screen"
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const footerHeight =
|
|
237
|
+
screenMode === "split-footer" ? normalizeFooterHeight(config.footerHeight) : DEFAULT_FOOTER_HEIGHT
|
|
238
|
+
|
|
239
|
+
let externalOutputMode =
|
|
240
|
+
config.externalOutputMode ?? (screenMode === "split-footer" ? "capture-stdout" : "passthrough")
|
|
241
|
+
if (process.env.OTUI_OVERRIDE_STDOUT !== undefined) {
|
|
242
|
+
externalOutputMode = env.OTUI_OVERRIDE_STDOUT && screenMode === "split-footer" ? "capture-stdout" : "passthrough"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (externalOutputMode === "capture-stdout" && screenMode !== "split-footer") {
|
|
246
|
+
throw new Error('externalOutputMode "capture-stdout" requires screenMode "split-footer"')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
screenMode,
|
|
251
|
+
footerHeight,
|
|
252
|
+
externalOutputMode,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const DEFAULT_FORWARDED_ENV_KEYS = [
|
|
257
|
+
"TMUX",
|
|
258
|
+
"TERM",
|
|
259
|
+
"OPENTUI_GRAPHICS",
|
|
260
|
+
"TERM_PROGRAM",
|
|
261
|
+
"TERM_PROGRAM_VERSION",
|
|
262
|
+
"ALACRITTY_SOCKET",
|
|
263
|
+
"ALACRITTY_LOG",
|
|
264
|
+
"COLORTERM",
|
|
265
|
+
"TERMUX_VERSION",
|
|
266
|
+
"VHS_RECORD",
|
|
267
|
+
"OPENTUI_FORCE_WCWIDTH",
|
|
268
|
+
"OPENTUI_FORCE_UNICODE",
|
|
269
|
+
"OPENTUI_FORCE_NOZWJ",
|
|
270
|
+
"OPENTUI_FORCE_EXPLICIT_WIDTH",
|
|
271
|
+
"WT_SESSION",
|
|
272
|
+
"STY",
|
|
273
|
+
"WSL_DISTRO_NAME",
|
|
274
|
+
"WSL_INTEROP",
|
|
275
|
+
] as const
|
|
276
|
+
|
|
277
|
+
// Kitty keyboard protocol flags
|
|
278
|
+
// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
279
|
+
const KITTY_FLAG_DISAMBIGUATE = 0b1 // Report disambiguated escape codes
|
|
280
|
+
const KITTY_FLAG_EVENT_TYPES = 0b10 // Report event types (press/repeat/release)
|
|
281
|
+
const KITTY_FLAG_ALTERNATE_KEYS = 0b100 // Report alternate keys (e.g., numpad vs regular)
|
|
282
|
+
const KITTY_FLAG_ALL_KEYS_AS_ESCAPES = 0b1000 // Report all keys as escape codes
|
|
283
|
+
const KITTY_FLAG_REPORT_TEXT = 0b10000 // Report text associated with key events
|
|
284
|
+
|
|
285
|
+
const DEFAULT_STDIN_PARSER_MAX_BUFFER_BYTES = 64 * 1024 * 1024
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Kitty Keyboard Protocol configuration options
|
|
289
|
+
* See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
|
|
290
|
+
*/
|
|
291
|
+
export interface KittyKeyboardOptions {
|
|
292
|
+
/** Disambiguate escape codes (fixes ESC timing, alt+key ambiguity, ctrl+c as event). Default: true */
|
|
293
|
+
disambiguate?: boolean
|
|
294
|
+
/** Report alternate keys (numpad, shifted, base layout) for cross-keyboard shortcuts. Default: true */
|
|
295
|
+
alternateKeys?: boolean
|
|
296
|
+
/** Report event types (press/repeat/release). Default: false */
|
|
297
|
+
events?: boolean
|
|
298
|
+
/** Report all keys as escape codes. Default: false */
|
|
299
|
+
allKeysAsEscapes?: boolean
|
|
300
|
+
/** Report text associated with key events. Default: false */
|
|
301
|
+
reportText?: boolean
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Build kitty keyboard protocol flags based on configuration
|
|
306
|
+
* @param config Kitty keyboard configuration object (null/undefined = disabled)
|
|
307
|
+
* @returns The combined flags value (0 = disabled, >0 = enabled)
|
|
308
|
+
* @internal Exported for testing
|
|
309
|
+
*/
|
|
310
|
+
export function buildKittyKeyboardFlags(config: KittyKeyboardOptions | null | undefined): number {
|
|
311
|
+
if (!config) {
|
|
312
|
+
return 0
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let flags = 0
|
|
316
|
+
|
|
317
|
+
// Default: disambiguate + alternate keys (both default to true)
|
|
318
|
+
// - Disambiguate (0b1): Fixes ESC timing issues, alt+key ambiguity, makes ctrl+c a key event
|
|
319
|
+
// - Alternate keys (0b100): Reports shifted/base-layout keys for cross-keyboard shortcuts
|
|
320
|
+
|
|
321
|
+
// disambiguate defaults to true unless explicitly set to false
|
|
322
|
+
if (config.disambiguate !== false) {
|
|
323
|
+
flags |= KITTY_FLAG_DISAMBIGUATE
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// alternateKeys defaults to true unless explicitly set to false
|
|
327
|
+
if (config.alternateKeys !== false) {
|
|
328
|
+
flags |= KITTY_FLAG_ALTERNATE_KEYS
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Optional flags (default to false, only enabled when explicitly true)
|
|
332
|
+
if (config.events === true) {
|
|
333
|
+
flags |= KITTY_FLAG_EVENT_TYPES
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (config.allKeysAsEscapes === true) {
|
|
337
|
+
flags |= KITTY_FLAG_ALL_KEYS_AS_ESCAPES
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (config.reportText === true) {
|
|
341
|
+
flags |= KITTY_FLAG_REPORT_TEXT
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return flags
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export class MouseEvent {
|
|
348
|
+
public readonly type: MouseEventType
|
|
349
|
+
public readonly button: number
|
|
350
|
+
public readonly x: number
|
|
351
|
+
public readonly y: number
|
|
352
|
+
public readonly source?: Renderable
|
|
353
|
+
public readonly modifiers: {
|
|
354
|
+
shift: boolean
|
|
355
|
+
alt: boolean
|
|
356
|
+
ctrl: boolean
|
|
357
|
+
}
|
|
358
|
+
public readonly scroll?: ScrollInfo
|
|
359
|
+
public readonly target: Renderable | null
|
|
360
|
+
public readonly isDragging?: boolean
|
|
361
|
+
private _propagationStopped: boolean = false
|
|
362
|
+
private _defaultPrevented: boolean = false
|
|
363
|
+
|
|
364
|
+
public get propagationStopped(): boolean {
|
|
365
|
+
return this._propagationStopped
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
public get defaultPrevented(): boolean {
|
|
369
|
+
return this._defaultPrevented
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
constructor(target: Renderable | null, attributes: RawMouseEvent & { source?: Renderable; isDragging?: boolean }) {
|
|
373
|
+
this.target = target
|
|
374
|
+
this.type = attributes.type
|
|
375
|
+
this.button = attributes.button
|
|
376
|
+
this.x = attributes.x
|
|
377
|
+
this.y = attributes.y
|
|
378
|
+
this.modifiers = attributes.modifiers
|
|
379
|
+
this.scroll = attributes.scroll
|
|
380
|
+
this.source = attributes.source
|
|
381
|
+
this.isDragging = attributes.isDragging
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
public stopPropagation(): void {
|
|
385
|
+
this._propagationStopped = true
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
public preventDefault(): void {
|
|
389
|
+
this._defaultPrevented = true
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export enum MouseButton {
|
|
394
|
+
LEFT = 0,
|
|
395
|
+
MIDDLE = 1,
|
|
396
|
+
RIGHT = 2,
|
|
397
|
+
WHEEL_UP = 4,
|
|
398
|
+
WHEEL_DOWN = 5,
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const rendererTracker = singleton("RendererTracker", () => {
|
|
402
|
+
const renderers = new Set<CliRenderer>()
|
|
403
|
+
return {
|
|
404
|
+
addRenderer: (renderer: CliRenderer) => {
|
|
405
|
+
renderers.add(renderer)
|
|
406
|
+
},
|
|
407
|
+
removeRenderer: (renderer: CliRenderer) => {
|
|
408
|
+
renderers.delete(renderer)
|
|
409
|
+
if (renderers.size === 0) {
|
|
410
|
+
process.stdin.pause()
|
|
411
|
+
|
|
412
|
+
if (hasSingleton("tree-sitter-client")) {
|
|
413
|
+
getTreeSitterClient().destroy()
|
|
414
|
+
destroySingleton("tree-sitter-client")
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
export async function createCliRenderer(config: CliRendererConfig = {}): Promise<CliRenderer> {
|
|
422
|
+
if (process.argv.includes("--delay-start")) {
|
|
423
|
+
await new Promise((resolve) => setTimeout(resolve, 5000))
|
|
424
|
+
}
|
|
425
|
+
const stdin = config.stdin || process.stdin
|
|
426
|
+
const stdout = config.stdout || process.stdout
|
|
427
|
+
const { screenMode, footerHeight } = resolveModes(config)
|
|
428
|
+
|
|
429
|
+
const width = stdout.columns || 80
|
|
430
|
+
const height = stdout.rows || 24
|
|
431
|
+
const renderHeight = screenMode === "split-footer" ? footerHeight : height
|
|
432
|
+
|
|
433
|
+
const ziglib = resolveRenderLib()
|
|
434
|
+
const rendererPtr = ziglib.createRenderer(width, renderHeight, {
|
|
435
|
+
remote: config.remote ?? false,
|
|
436
|
+
testing: config.testing ?? false,
|
|
437
|
+
})
|
|
438
|
+
if (!rendererPtr) {
|
|
439
|
+
throw new Error("Failed to create renderer")
|
|
440
|
+
}
|
|
441
|
+
if (config.useThread === undefined) {
|
|
442
|
+
config.useThread = true
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Disable threading on linux because there currently is currently an issue
|
|
446
|
+
// might be just a missing dependency for the build or something, but threads crash on linux
|
|
447
|
+
if (process.platform === "linux") {
|
|
448
|
+
config.useThread = false
|
|
449
|
+
}
|
|
450
|
+
ziglib.setUseThread(rendererPtr, config.useThread)
|
|
451
|
+
|
|
452
|
+
const kittyConfig = config.useKittyKeyboard ?? {}
|
|
453
|
+
const kittyFlags = buildKittyKeyboardFlags(kittyConfig)
|
|
454
|
+
|
|
455
|
+
ziglib.setKittyKeyboardFlags(rendererPtr, kittyFlags)
|
|
456
|
+
|
|
457
|
+
const renderer = new CliRenderer(ziglib, rendererPtr, stdin, stdout, width, height, config)
|
|
458
|
+
if (!config.testing) {
|
|
459
|
+
await renderer.setupTerminal()
|
|
460
|
+
}
|
|
461
|
+
return renderer
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export enum CliRenderEvents {
|
|
465
|
+
RESIZE = "resize",
|
|
466
|
+
FOCUS = "focus",
|
|
467
|
+
BLUR = "blur",
|
|
468
|
+
FOCUSED_EDITOR = "focused_editor",
|
|
469
|
+
THEME_MODE = "theme_mode",
|
|
470
|
+
CAPABILITIES = "capabilities",
|
|
471
|
+
SELECTION = "selection",
|
|
472
|
+
DEBUG_OVERLAY_TOGGLE = "debugOverlay:toggle",
|
|
473
|
+
DESTROY = "destroy",
|
|
474
|
+
MEMORY_SNAPSHOT = "memory:snapshot",
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export enum RendererControlState {
|
|
478
|
+
IDLE = "idle",
|
|
479
|
+
AUTO_STARTED = "auto_started",
|
|
480
|
+
EXPLICIT_STARTED = "explicit_started",
|
|
481
|
+
EXPLICIT_PAUSED = "explicit_paused",
|
|
482
|
+
EXPLICIT_SUSPENDED = "explicit_suspended",
|
|
483
|
+
EXPLICIT_STOPPED = "explicit_stopped",
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export class CliRenderer extends EventEmitter implements RenderContext {
|
|
487
|
+
private static animationFrameId = 0
|
|
488
|
+
private lib: RenderLib
|
|
489
|
+
public rendererPtr: Pointer
|
|
490
|
+
public stdin: NodeJS.ReadStream
|
|
491
|
+
private stdout: NodeJS.WriteStream
|
|
492
|
+
private exitOnCtrlC: boolean
|
|
493
|
+
private exitSignals: NodeJS.Signals[]
|
|
494
|
+
private _exitListenersAdded: boolean = false
|
|
495
|
+
private _isDestroyed: boolean = false
|
|
496
|
+
private _destroyPending: boolean = false
|
|
497
|
+
private _destroyFinalized: boolean = false
|
|
498
|
+
private _destroyCleanupPrepared: boolean = false
|
|
499
|
+
public nextRenderBuffer: OptimizedBuffer
|
|
500
|
+
public currentRenderBuffer: OptimizedBuffer
|
|
501
|
+
private _isRunning: boolean = false
|
|
502
|
+
private _targetFps: number = 30
|
|
503
|
+
private _maxFps: number = 60
|
|
504
|
+
private automaticMemorySnapshot: boolean = false
|
|
505
|
+
private memorySnapshotInterval: number
|
|
506
|
+
private memorySnapshotTimer: TimerHandle | null = null
|
|
507
|
+
private lastMemorySnapshot: { heapUsed: number; heapTotal: number; arrayBuffers: number } = {
|
|
508
|
+
heapUsed: 0,
|
|
509
|
+
heapTotal: 0,
|
|
510
|
+
arrayBuffers: 0,
|
|
511
|
+
}
|
|
512
|
+
public readonly root: RootRenderable
|
|
513
|
+
public width: number
|
|
514
|
+
public height: number
|
|
515
|
+
private _useThread: boolean = false
|
|
516
|
+
private gatherStats: boolean = false
|
|
517
|
+
private frameTimes: number[] = []
|
|
518
|
+
private maxStatSamples: number = 300
|
|
519
|
+
private postProcessFns: ((buffer: OptimizedBuffer, deltaTime: number) => void)[] = []
|
|
520
|
+
private backgroundColor: RGBA = RGBA.fromInts(0, 0, 0, 0)
|
|
521
|
+
private waitingForPixelResolution: boolean = false
|
|
522
|
+
private readonly clock: Clock
|
|
523
|
+
|
|
524
|
+
private rendering: boolean = false
|
|
525
|
+
private renderingNative: boolean = false
|
|
526
|
+
private renderTimeout: TimerHandle | null = null
|
|
527
|
+
private lastTime: number = 0
|
|
528
|
+
private frameCount: number = 0
|
|
529
|
+
private lastFpsTime: number = 0
|
|
530
|
+
private currentFps: number = 0
|
|
531
|
+
private targetFrameTime: number = 1000 / this._targetFps
|
|
532
|
+
private minTargetFrameTime: number = 1000 / this._maxFps
|
|
533
|
+
private immediateRerenderRequested: boolean = false
|
|
534
|
+
private updateScheduled: boolean = false
|
|
535
|
+
|
|
536
|
+
private liveRequestCounter: number = 0
|
|
537
|
+
private _controlState: RendererControlState = RendererControlState.IDLE
|
|
538
|
+
|
|
539
|
+
private frameCallbacks: ((deltaTime: number) => Promise<void>)[] = []
|
|
540
|
+
private renderStats: {
|
|
541
|
+
frameCount: number
|
|
542
|
+
fps: number
|
|
543
|
+
renderTime?: number
|
|
544
|
+
frameCallbackTime: number
|
|
545
|
+
} = {
|
|
546
|
+
frameCount: 0,
|
|
547
|
+
fps: 0,
|
|
548
|
+
renderTime: 0,
|
|
549
|
+
frameCallbackTime: 0,
|
|
550
|
+
}
|
|
551
|
+
public debugOverlay = {
|
|
552
|
+
enabled: env.OTUI_SHOW_STATS,
|
|
553
|
+
corner: DebugOverlayCorner.bottomRight,
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private _console: TerminalConsole
|
|
557
|
+
private _resolution: PixelResolution | null = null
|
|
558
|
+
private _keyHandler: InternalKeyHandler
|
|
559
|
+
private stdinParser: StdinParser | null = null
|
|
560
|
+
private readonly oscSubscribers = new Set<(sequence: string) => void>()
|
|
561
|
+
private hasLoggedStdinParserError = false
|
|
562
|
+
|
|
563
|
+
private animationRequest: Map<number, FrameRequestCallback> = new Map()
|
|
564
|
+
|
|
565
|
+
private resizeTimeoutId: TimerHandle | null = null
|
|
566
|
+
private capabilityTimeoutId: TimerHandle | null = null
|
|
567
|
+
private resizeDebounceDelay: number = 100
|
|
568
|
+
|
|
569
|
+
private enableMouseMovement: boolean = false
|
|
570
|
+
private _useMouse: boolean = true
|
|
571
|
+
private autoFocus: boolean = true
|
|
572
|
+
private _screenMode: ScreenMode = "alternate-screen"
|
|
573
|
+
private _footerHeight: number = DEFAULT_FOOTER_HEIGHT
|
|
574
|
+
private _externalOutputMode: ExternalOutputMode = "passthrough"
|
|
575
|
+
private _suspendedMouseEnabled: boolean = false
|
|
576
|
+
private _previousControlState: RendererControlState = RendererControlState.IDLE
|
|
577
|
+
private capturedRenderable?: Renderable
|
|
578
|
+
private lastOverRenderableNum: number = 0
|
|
579
|
+
private lastOverRenderable?: Renderable
|
|
580
|
+
|
|
581
|
+
private currentSelection: Selection | null = null
|
|
582
|
+
private selectionContainers: Renderable[] = []
|
|
583
|
+
private clipboard: Clipboard
|
|
584
|
+
|
|
585
|
+
private _splitHeight: number = 0
|
|
586
|
+
private renderOffset: number = 0
|
|
587
|
+
|
|
588
|
+
private _terminalWidth: number = 0
|
|
589
|
+
private _terminalHeight: number = 0
|
|
590
|
+
private _terminalIsSetup: boolean = false
|
|
591
|
+
|
|
592
|
+
private realStdoutWrite: (chunk: any, encoding?: any, callback?: any) => boolean
|
|
593
|
+
private captureCallback: () => void = () => {
|
|
594
|
+
if (this._splitHeight > 0) {
|
|
595
|
+
this.requestRender()
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private _useConsole: boolean = true
|
|
600
|
+
private sigwinchHandler: () => void = (() => {
|
|
601
|
+
const width = this.stdout.columns || 80
|
|
602
|
+
const height = this.stdout.rows || 24
|
|
603
|
+
this.handleResize(width, height)
|
|
604
|
+
}).bind(this)
|
|
605
|
+
private _capabilities: any | null = null
|
|
606
|
+
private _latestPointer: { x: number; y: number } = { x: 0, y: 0 }
|
|
607
|
+
private _hasPointer: boolean = false
|
|
608
|
+
private _lastPointerModifiers: RawMouseEvent["modifiers"] = { shift: false, alt: false, ctrl: false }
|
|
609
|
+
private _currentMousePointerStyle: MousePointerStyle | undefined = undefined
|
|
610
|
+
|
|
611
|
+
private _currentFocusedRenderable: Renderable | null = null
|
|
612
|
+
private lifecyclePasses: Set<Renderable> = new Set()
|
|
613
|
+
private _openConsoleOnError: boolean = true
|
|
614
|
+
private _paletteDetector: TerminalPaletteDetector | null = null
|
|
615
|
+
private _cachedPalette: TerminalColors | null = null
|
|
616
|
+
private _paletteDetectionPromise: Promise<TerminalColors> | null = null
|
|
617
|
+
private _onDestroy?: () => void
|
|
618
|
+
private _themeMode: ThemeMode | null = null
|
|
619
|
+
private _terminalFocusState: boolean | null = null
|
|
620
|
+
|
|
621
|
+
private sequenceHandlers: ((sequence: string) => boolean)[] = []
|
|
622
|
+
private prependedInputHandlers: ((sequence: string) => boolean)[] = []
|
|
623
|
+
private shouldRestoreModesOnNextFocus: boolean = false
|
|
624
|
+
|
|
625
|
+
private idleResolvers: (() => void)[] = []
|
|
626
|
+
|
|
627
|
+
private _debugInputs: Array<{ timestamp: string; sequence: string }> = []
|
|
628
|
+
private _debugModeEnabled: boolean = env.OTUI_DEBUG
|
|
629
|
+
|
|
630
|
+
private handleError: (error: Error) => void = ((error: Error) => {
|
|
631
|
+
console.error(error)
|
|
632
|
+
|
|
633
|
+
if (this._openConsoleOnError) {
|
|
634
|
+
this.console.show()
|
|
635
|
+
}
|
|
636
|
+
}).bind(this)
|
|
637
|
+
|
|
638
|
+
private dumpOutputCache(optionalMessage: string = ""): void {
|
|
639
|
+
const cachedLogs = this.console.getCachedLogs()
|
|
640
|
+
const capturedOutput = capture.claimOutput()
|
|
641
|
+
|
|
642
|
+
if (capturedOutput.length > 0 || cachedLogs.length > 0) {
|
|
643
|
+
this.realStdoutWrite.call(this.stdout, optionalMessage)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (cachedLogs.length > 0) {
|
|
647
|
+
this.realStdoutWrite.call(this.stdout, "Console cache:\n")
|
|
648
|
+
this.realStdoutWrite.call(this.stdout, cachedLogs)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (capturedOutput.length > 0) {
|
|
652
|
+
this.realStdoutWrite.call(this.stdout, "\nCaptured output:\n")
|
|
653
|
+
this.realStdoutWrite.call(this.stdout, capturedOutput + "\n")
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
this.realStdoutWrite.call(this.stdout, ANSI.reset)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private exitHandler: () => void = (() => {
|
|
660
|
+
this.destroy()
|
|
661
|
+
if (env.OTUI_DUMP_CAPTURES) {
|
|
662
|
+
Bun.sleep(100).then(() => {
|
|
663
|
+
this.dumpOutputCache("=== CAPTURED OUTPUT ===\n")
|
|
664
|
+
})
|
|
665
|
+
}
|
|
666
|
+
}).bind(this)
|
|
667
|
+
|
|
668
|
+
private warningHandler: (warning: any) => void = ((warning: any) => {
|
|
669
|
+
console.warn(JSON.stringify(warning.message, null, 2))
|
|
670
|
+
}).bind(this)
|
|
671
|
+
|
|
672
|
+
public get controlState(): RendererControlState {
|
|
673
|
+
return this._controlState
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
constructor(
|
|
677
|
+
lib: RenderLib,
|
|
678
|
+
rendererPtr: Pointer,
|
|
679
|
+
stdin: NodeJS.ReadStream,
|
|
680
|
+
stdout: NodeJS.WriteStream,
|
|
681
|
+
width: number,
|
|
682
|
+
height: number,
|
|
683
|
+
config: CliRendererConfig = {},
|
|
684
|
+
) {
|
|
685
|
+
super()
|
|
686
|
+
|
|
687
|
+
rendererTracker.addRenderer(this)
|
|
688
|
+
|
|
689
|
+
this.stdin = stdin
|
|
690
|
+
this.stdout = stdout
|
|
691
|
+
this.realStdoutWrite = stdout.write
|
|
692
|
+
this.lib = lib
|
|
693
|
+
this._terminalWidth = stdout.columns ?? width
|
|
694
|
+
this._terminalHeight = stdout.rows ?? height
|
|
695
|
+
this.width = width
|
|
696
|
+
this.height = height
|
|
697
|
+
this._useThread = config.useThread === undefined ? false : config.useThread
|
|
698
|
+
const { screenMode, footerHeight, externalOutputMode } = resolveModes(config)
|
|
699
|
+
|
|
700
|
+
this._footerHeight = footerHeight
|
|
701
|
+
this._screenMode = screenMode
|
|
702
|
+
|
|
703
|
+
this.rendererPtr = rendererPtr
|
|
704
|
+
|
|
705
|
+
const forwardEnvKeys = config.forwardEnvKeys ?? [...DEFAULT_FORWARDED_ENV_KEYS]
|
|
706
|
+
for (const key of forwardEnvKeys) {
|
|
707
|
+
const value = process.env[key]
|
|
708
|
+
if (value === undefined) continue
|
|
709
|
+
this.lib.setTerminalEnvVar(this.rendererPtr, key, value)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
this.exitOnCtrlC = config.exitOnCtrlC === undefined ? true : config.exitOnCtrlC
|
|
713
|
+
this.exitSignals = config.exitSignals || [
|
|
714
|
+
"SIGINT", // Ctrl+C
|
|
715
|
+
"SIGTERM", // Termination signal
|
|
716
|
+
"SIGQUIT", // Ctrl+\
|
|
717
|
+
"SIGABRT", // Abort signal
|
|
718
|
+
"SIGHUP", // Hangup (terminal closed)
|
|
719
|
+
"SIGBREAK", // Ctrl+Break on Windows
|
|
720
|
+
"SIGPIPE", // Broken pipe
|
|
721
|
+
"SIGBUS", // Bus error
|
|
722
|
+
]
|
|
723
|
+
|
|
724
|
+
this.clipboard = new Clipboard(this.lib, this.rendererPtr)
|
|
725
|
+
this.resizeDebounceDelay = config.debounceDelay || 100
|
|
726
|
+
this.targetFps = config.targetFps || 30
|
|
727
|
+
this.maxFps = config.maxFps || 60
|
|
728
|
+
this.clock = config.clock ?? new SystemClock()
|
|
729
|
+
this.memorySnapshotInterval = config.memorySnapshotInterval ?? 0
|
|
730
|
+
this.gatherStats = config.gatherStats || false
|
|
731
|
+
this.maxStatSamples = config.maxStatSamples || 300
|
|
732
|
+
this.enableMouseMovement = config.enableMouseMovement ?? true
|
|
733
|
+
this._useMouse = config.useMouse ?? true
|
|
734
|
+
this.autoFocus = config.autoFocus ?? true
|
|
735
|
+
this.nextRenderBuffer = this.lib.getNextBuffer(this.rendererPtr)
|
|
736
|
+
this.currentRenderBuffer = this.lib.getCurrentBuffer(this.rendererPtr)
|
|
737
|
+
this.postProcessFns = config.postProcessFns || []
|
|
738
|
+
this.prependedInputHandlers = config.prependInputHandlers || []
|
|
739
|
+
|
|
740
|
+
this.root = new RootRenderable(this)
|
|
741
|
+
|
|
742
|
+
if (this.memorySnapshotInterval > 0) {
|
|
743
|
+
this.startMemorySnapshotTimer()
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Handle terminal resize
|
|
747
|
+
process.on("SIGWINCH", this.sigwinchHandler)
|
|
748
|
+
|
|
749
|
+
process.on("warning", this.warningHandler)
|
|
750
|
+
|
|
751
|
+
process.on("uncaughtException", this.handleError)
|
|
752
|
+
process.on("unhandledRejection", this.handleError)
|
|
753
|
+
process.on("beforeExit", this.exitHandler)
|
|
754
|
+
|
|
755
|
+
const kittyConfig = config.useKittyKeyboard ?? {}
|
|
756
|
+
const useKittyForParsing = kittyConfig !== null
|
|
757
|
+
this._keyHandler = new InternalKeyHandler()
|
|
758
|
+
this._keyHandler.on("keypress", (event) => {
|
|
759
|
+
if (this.exitOnCtrlC && event.name === "c" && event.ctrl) {
|
|
760
|
+
process.nextTick(() => {
|
|
761
|
+
this.destroy()
|
|
762
|
+
})
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
this.addExitListeners()
|
|
768
|
+
|
|
769
|
+
const stdinParserMaxBufferBytes = config.stdinParserMaxBufferBytes ?? DEFAULT_STDIN_PARSER_MAX_BUFFER_BYTES
|
|
770
|
+
this.stdinParser = new StdinParser({
|
|
771
|
+
timeoutMs: 20,
|
|
772
|
+
maxPendingBytes: stdinParserMaxBufferBytes,
|
|
773
|
+
armTimeouts: true,
|
|
774
|
+
onTimeoutFlush: () => {
|
|
775
|
+
this.drainStdinParser()
|
|
776
|
+
},
|
|
777
|
+
useKittyKeyboard: useKittyForParsing,
|
|
778
|
+
protocolContext: {
|
|
779
|
+
kittyKeyboardEnabled: useKittyForParsing,
|
|
780
|
+
privateCapabilityRepliesActive: false,
|
|
781
|
+
pixelResolutionQueryActive: false,
|
|
782
|
+
explicitWidthCprActive: false,
|
|
783
|
+
},
|
|
784
|
+
clock: this.clock,
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
this._console = new TerminalConsole(this, {
|
|
788
|
+
...(config.consoleOptions ?? {}),
|
|
789
|
+
clock: this.clock,
|
|
790
|
+
})
|
|
791
|
+
this.consoleMode = config.consoleMode ?? "console-overlay"
|
|
792
|
+
this.applyScreenMode(screenMode, false, false)
|
|
793
|
+
this.externalOutputMode = externalOutputMode
|
|
794
|
+
this._openConsoleOnError = config.openConsoleOnError ?? process.env.NODE_ENV !== "production"
|
|
795
|
+
this._onDestroy = config.onDestroy
|
|
796
|
+
|
|
797
|
+
global.requestAnimationFrame = (callback: FrameRequestCallback) => {
|
|
798
|
+
const id = CliRenderer.animationFrameId++
|
|
799
|
+
this.animationRequest.set(id, callback)
|
|
800
|
+
this.requestLive()
|
|
801
|
+
return id
|
|
802
|
+
}
|
|
803
|
+
global.cancelAnimationFrame = (handle: number) => {
|
|
804
|
+
this.animationRequest.delete(handle)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const window = global.window
|
|
808
|
+
if (!window) {
|
|
809
|
+
global.window = {} as Window & typeof globalThis
|
|
810
|
+
}
|
|
811
|
+
global.window.requestAnimationFrame = requestAnimationFrame
|
|
812
|
+
|
|
813
|
+
// Prevents output from being written to the terminal, useful for debugging
|
|
814
|
+
if (env.OTUI_NO_NATIVE_RENDER) {
|
|
815
|
+
this.renderNative = () => {
|
|
816
|
+
if (this._splitHeight > 0) {
|
|
817
|
+
this.flushStdoutCache(this._splitHeight)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
this.setupInput()
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private addExitListeners(): void {
|
|
826
|
+
if (this._exitListenersAdded || this.exitSignals.length === 0) return
|
|
827
|
+
|
|
828
|
+
this.exitSignals.forEach((signal) => {
|
|
829
|
+
process.addListener(signal, this.exitHandler)
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
this._exitListenersAdded = true
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
private removeExitListeners(): void {
|
|
836
|
+
if (!this._exitListenersAdded || this.exitSignals.length === 0) return
|
|
837
|
+
|
|
838
|
+
this.exitSignals.forEach((signal) => {
|
|
839
|
+
process.removeListener(signal, this.exitHandler)
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
this._exitListenersAdded = false
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
public get isDestroyed(): boolean {
|
|
846
|
+
return this._isDestroyed
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
public registerLifecyclePass(renderable: Renderable) {
|
|
850
|
+
this.lifecyclePasses.add(renderable)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
public unregisterLifecyclePass(renderable: Renderable) {
|
|
854
|
+
this.lifecyclePasses.delete(renderable)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
public getLifecyclePasses() {
|
|
858
|
+
return this.lifecyclePasses
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
public get currentFocusedRenderable(): Renderable | null {
|
|
862
|
+
return this._currentFocusedRenderable
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
public get currentFocusedEditor(): EditBufferRenderable | null {
|
|
866
|
+
if (!this._currentFocusedRenderable) return null
|
|
867
|
+
if (!isEditBufferRenderable(this._currentFocusedRenderable)) return null
|
|
868
|
+
return this._currentFocusedRenderable
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private normalizeClockTime(now: number, fallback: number): number {
|
|
872
|
+
if (Number.isFinite(now)) {
|
|
873
|
+
return now
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return Number.isFinite(fallback) ? fallback : 0
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
private getElapsedMs(now: number, then: number): number {
|
|
880
|
+
if (!Number.isFinite(now) || !Number.isFinite(then)) {
|
|
881
|
+
return 0
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return Math.max(now - then, 0)
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
public focusRenderable(renderable: Renderable) {
|
|
888
|
+
if (this._currentFocusedRenderable === renderable) return
|
|
889
|
+
|
|
890
|
+
const prev = this.currentFocusedEditor
|
|
891
|
+
|
|
892
|
+
if (this._currentFocusedRenderable) {
|
|
893
|
+
this._currentFocusedRenderable.blur()
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
this._currentFocusedRenderable = renderable
|
|
897
|
+
|
|
898
|
+
const next = this.currentFocusedEditor
|
|
899
|
+
if (prev !== next) {
|
|
900
|
+
this.emit(CliRenderEvents.FOCUSED_EDITOR, next, prev)
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
private setCapturedRenderable(renderable: Renderable | undefined): void {
|
|
905
|
+
if (this.capturedRenderable === renderable) {
|
|
906
|
+
return
|
|
907
|
+
}
|
|
908
|
+
this.capturedRenderable = renderable
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
public addToHitGrid(x: number, y: number, width: number, height: number, id: number) {
|
|
912
|
+
if (id !== this.capturedRenderable?.num) {
|
|
913
|
+
this.lib.addToHitGrid(this.rendererPtr, x, y, width, height, id)
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
public pushHitGridScissorRect(x: number, y: number, width: number, height: number): void {
|
|
918
|
+
this.lib.hitGridPushScissorRect(this.rendererPtr, x, y, width, height)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
public popHitGridScissorRect(): void {
|
|
922
|
+
this.lib.hitGridPopScissorRect(this.rendererPtr)
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
public clearHitGridScissorRects(): void {
|
|
926
|
+
this.lib.hitGridClearScissorRects(this.rendererPtr)
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
public get widthMethod(): WidthMethod {
|
|
930
|
+
const caps = this.capabilities
|
|
931
|
+
return caps?.unicode === "wcwidth" ? "wcwidth" : "unicode"
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
private writeOut(chunk: any, encoding?: any, callback?: any): boolean {
|
|
935
|
+
if (this.rendererPtr && this._useThread) {
|
|
936
|
+
const data = typeof chunk === "string" ? chunk : (chunk?.toString() ?? "")
|
|
937
|
+
this.lib.writeOut(this.rendererPtr, data)
|
|
938
|
+
if (typeof callback === "function") {
|
|
939
|
+
process.nextTick(callback)
|
|
940
|
+
}
|
|
941
|
+
return true
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return this.realStdoutWrite.call(this.stdout, chunk, encoding, callback)
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
public requestRender() {
|
|
948
|
+
if (this._controlState === RendererControlState.EXPLICIT_SUSPENDED) {
|
|
949
|
+
return
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (this._isRunning) {
|
|
953
|
+
return
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// NOTE: Using a frame callback that causes a re-render while already rendering
|
|
957
|
+
// leads to a continuous loop of renders.
|
|
958
|
+
if (this.rendering) {
|
|
959
|
+
this.immediateRerenderRequested = true
|
|
960
|
+
return
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (!this.updateScheduled && !this.renderTimeout) {
|
|
964
|
+
this.updateScheduled = true
|
|
965
|
+
const now = this.normalizeClockTime(this.clock.now(), this.lastTime)
|
|
966
|
+
const elapsed = this.getElapsedMs(now, this.lastTime)
|
|
967
|
+
const delay = Math.max(this.minTargetFrameTime - elapsed, 0)
|
|
968
|
+
|
|
969
|
+
if (delay === 0) {
|
|
970
|
+
process.nextTick(() => this.activateFrame())
|
|
971
|
+
return
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
this.clock.setTimeout(() => this.activateFrame(), delay)
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
private async activateFrame() {
|
|
979
|
+
if (!this.updateScheduled) {
|
|
980
|
+
this.resolveIdleIfNeeded()
|
|
981
|
+
return
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
try {
|
|
985
|
+
await this.loop()
|
|
986
|
+
} finally {
|
|
987
|
+
this.updateScheduled = false
|
|
988
|
+
this.resolveIdleIfNeeded()
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
public get consoleMode(): ConsoleMode {
|
|
993
|
+
return this._useConsole ? "console-overlay" : "disabled"
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
public set consoleMode(mode: ConsoleMode) {
|
|
997
|
+
this._useConsole = mode === "console-overlay"
|
|
998
|
+
if (this._useConsole) {
|
|
999
|
+
this.console.activate()
|
|
1000
|
+
} else {
|
|
1001
|
+
this.console.deactivate()
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
public get isRunning(): boolean {
|
|
1006
|
+
return this._isRunning
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
private isIdleNow(): boolean {
|
|
1010
|
+
return (
|
|
1011
|
+
!this._isRunning &&
|
|
1012
|
+
!this.rendering &&
|
|
1013
|
+
!this.renderTimeout &&
|
|
1014
|
+
!this.updateScheduled &&
|
|
1015
|
+
!this.immediateRerenderRequested
|
|
1016
|
+
)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
private resolveIdleIfNeeded(): void {
|
|
1020
|
+
if (!this.isIdleNow()) return
|
|
1021
|
+
const resolvers = this.idleResolvers.splice(0)
|
|
1022
|
+
for (const resolve of resolvers) {
|
|
1023
|
+
resolve()
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
public idle(): Promise<void> {
|
|
1028
|
+
if (this._isDestroyed) return Promise.resolve()
|
|
1029
|
+
if (this.isIdleNow()) return Promise.resolve()
|
|
1030
|
+
return new Promise<void>((resolve) => {
|
|
1031
|
+
this.idleResolvers.push(resolve)
|
|
1032
|
+
})
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
public get resolution(): PixelResolution | null {
|
|
1036
|
+
return this._resolution
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
public get console(): TerminalConsole {
|
|
1040
|
+
return this._console
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
public get keyInput(): KeyHandler {
|
|
1044
|
+
return this._keyHandler
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
public get _internalKeyInput(): InternalKeyHandler {
|
|
1048
|
+
return this._keyHandler
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
public get terminalWidth(): number {
|
|
1052
|
+
return this._terminalWidth
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
public get terminalHeight(): number {
|
|
1056
|
+
return this._terminalHeight
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
public get useThread(): boolean {
|
|
1060
|
+
return this._useThread
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
public get targetFps(): number {
|
|
1064
|
+
return this._targetFps
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
public set targetFps(targetFps: number) {
|
|
1068
|
+
this._targetFps = targetFps
|
|
1069
|
+
this.targetFrameTime = 1000 / this._targetFps
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
public get maxFps(): number {
|
|
1073
|
+
return this._maxFps
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
public set maxFps(maxFps: number) {
|
|
1077
|
+
this._maxFps = maxFps
|
|
1078
|
+
this.minTargetFrameTime = 1000 / this._maxFps
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
public get useMouse(): boolean {
|
|
1082
|
+
return this._useMouse
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
public set useMouse(useMouse: boolean) {
|
|
1086
|
+
if (this._useMouse === useMouse) return // No change needed
|
|
1087
|
+
|
|
1088
|
+
this._useMouse = useMouse
|
|
1089
|
+
|
|
1090
|
+
if (useMouse) {
|
|
1091
|
+
this.enableMouse()
|
|
1092
|
+
} else {
|
|
1093
|
+
this.disableMouse()
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
public get screenMode(): ScreenMode {
|
|
1098
|
+
return this._screenMode
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
public set screenMode(mode: ScreenMode) {
|
|
1102
|
+
if (this.externalOutputMode === "capture-stdout" && mode !== "split-footer") {
|
|
1103
|
+
throw new Error('externalOutputMode "capture-stdout" requires screenMode "split-footer"')
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
this.applyScreenMode(mode)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
public get footerHeight(): number {
|
|
1110
|
+
return this._footerHeight
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
public set footerHeight(footerHeight: number) {
|
|
1114
|
+
const normalizedFooterHeight = normalizeFooterHeight(footerHeight)
|
|
1115
|
+
if (normalizedFooterHeight === this._footerHeight) {
|
|
1116
|
+
return
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
this._footerHeight = normalizedFooterHeight
|
|
1120
|
+
if (this.screenMode === "split-footer") {
|
|
1121
|
+
this.applyScreenMode("split-footer")
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
public get externalOutputMode(): ExternalOutputMode {
|
|
1126
|
+
return this._externalOutputMode
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
public set externalOutputMode(mode: ExternalOutputMode) {
|
|
1130
|
+
if (mode === "capture-stdout" && this.screenMode !== "split-footer") {
|
|
1131
|
+
throw new Error('externalOutputMode "capture-stdout" requires screenMode "split-footer"')
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
this._externalOutputMode = mode
|
|
1135
|
+
this.stdout.write = mode === "capture-stdout" ? this.interceptStdoutWrite : this.realStdoutWrite
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
public get liveRequestCount(): number {
|
|
1139
|
+
return this.liveRequestCounter
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
public get currentControlState(): string {
|
|
1143
|
+
return this._controlState
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
public get capabilities(): any | null {
|
|
1147
|
+
return this._capabilities
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
public get themeMode(): ThemeMode | null {
|
|
1151
|
+
return this._themeMode
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
public getDebugInputs(): Array<{ timestamp: string; sequence: string }> {
|
|
1155
|
+
return [...this._debugInputs]
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
public get useKittyKeyboard(): boolean {
|
|
1159
|
+
return this.lib.getKittyKeyboardFlags(this.rendererPtr) > 0
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
public set useKittyKeyboard(use: boolean) {
|
|
1163
|
+
const flags = use ? KITTY_FLAG_DISAMBIGUATE | KITTY_FLAG_ALTERNATE_KEYS : 0
|
|
1164
|
+
this.lib.setKittyKeyboardFlags(this.rendererPtr, flags)
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
private interceptStdoutWrite = (chunk: any, encoding?: any, callback?: any): boolean => {
|
|
1168
|
+
const text = chunk.toString()
|
|
1169
|
+
|
|
1170
|
+
capture.write("stdout", text)
|
|
1171
|
+
if (this._splitHeight > 0) {
|
|
1172
|
+
this.requestRender()
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (typeof callback === "function") {
|
|
1176
|
+
process.nextTick(callback)
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return true
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
private applyScreenMode(screenMode: ScreenMode, emitResize: boolean = true, requestRender: boolean = true): void {
|
|
1183
|
+
const prevScreenMode = this._screenMode
|
|
1184
|
+
const prevSplitHeight = this._splitHeight
|
|
1185
|
+
const nextSplitHeight = screenMode === "split-footer" ? this._footerHeight : 0
|
|
1186
|
+
|
|
1187
|
+
if (prevScreenMode === screenMode && prevSplitHeight === nextSplitHeight) {
|
|
1188
|
+
return
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const prevUseAlternateScreen = prevScreenMode === "alternate-screen"
|
|
1192
|
+
const nextUseAlternateScreen = screenMode === "alternate-screen"
|
|
1193
|
+
const terminalScreenModeChanged = this._terminalIsSetup && prevUseAlternateScreen !== nextUseAlternateScreen
|
|
1194
|
+
const leavingSplitFooter = prevSplitHeight > 0 && nextSplitHeight === 0
|
|
1195
|
+
|
|
1196
|
+
if (this._terminalIsSetup && leavingSplitFooter) {
|
|
1197
|
+
this.flushStdoutCache(this._terminalHeight, true)
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (this._terminalIsSetup && !terminalScreenModeChanged) {
|
|
1201
|
+
if (prevSplitHeight === 0 && nextSplitHeight > 0) {
|
|
1202
|
+
const freedLines = this._terminalHeight - nextSplitHeight
|
|
1203
|
+
const scrollDown = ANSI.scrollDown(freedLines)
|
|
1204
|
+
this.writeOut(scrollDown)
|
|
1205
|
+
} else if (prevSplitHeight > nextSplitHeight && nextSplitHeight > 0) {
|
|
1206
|
+
const freedLines = prevSplitHeight - nextSplitHeight
|
|
1207
|
+
const scrollDown = ANSI.scrollDown(freedLines)
|
|
1208
|
+
this.writeOut(scrollDown)
|
|
1209
|
+
} else if (prevSplitHeight < nextSplitHeight && prevSplitHeight > 0) {
|
|
1210
|
+
const additionalLines = nextSplitHeight - prevSplitHeight
|
|
1211
|
+
const scrollUp = ANSI.scrollUp(additionalLines)
|
|
1212
|
+
this.writeOut(scrollUp)
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (prevSplitHeight === 0 && nextSplitHeight > 0) {
|
|
1217
|
+
capture.on("write", this.captureCallback)
|
|
1218
|
+
} else if (prevSplitHeight > 0 && nextSplitHeight === 0) {
|
|
1219
|
+
capture.off("write", this.captureCallback)
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
this._screenMode = screenMode
|
|
1223
|
+
this._splitHeight = nextSplitHeight
|
|
1224
|
+
this.renderOffset = nextSplitHeight > 0 ? this._terminalHeight - nextSplitHeight : 0
|
|
1225
|
+
this.width = this._terminalWidth
|
|
1226
|
+
this.height = nextSplitHeight > 0 ? nextSplitHeight : this._terminalHeight
|
|
1227
|
+
|
|
1228
|
+
this.lib.setRenderOffset(this.rendererPtr, this.renderOffset)
|
|
1229
|
+
this.lib.resizeRenderer(this.rendererPtr, this.width, this.height)
|
|
1230
|
+
this.nextRenderBuffer = this.lib.getNextBuffer(this.rendererPtr)
|
|
1231
|
+
this.currentRenderBuffer = this.lib.getCurrentBuffer(this.rendererPtr)
|
|
1232
|
+
|
|
1233
|
+
this._console.resize(this.width, this.height)
|
|
1234
|
+
this.root.resize(this.width, this.height)
|
|
1235
|
+
|
|
1236
|
+
if (terminalScreenModeChanged) {
|
|
1237
|
+
this.lib.suspendRenderer(this.rendererPtr)
|
|
1238
|
+
this.lib.setupTerminal(this.rendererPtr, nextUseAlternateScreen)
|
|
1239
|
+
|
|
1240
|
+
if (this._useMouse) {
|
|
1241
|
+
this.enableMouse()
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (emitResize) {
|
|
1246
|
+
this.emit(CliRenderEvents.RESIZE, this.width, this.height)
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (requestRender) {
|
|
1250
|
+
this.requestRender()
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// TODO: Move this to native
|
|
1255
|
+
private flushStdoutCache(space: number, force: boolean = false): boolean {
|
|
1256
|
+
if (capture.size === 0 && !force) return false
|
|
1257
|
+
|
|
1258
|
+
const output = capture.claimOutput()
|
|
1259
|
+
|
|
1260
|
+
const rendererStartLine = this._terminalHeight - this._splitHeight
|
|
1261
|
+
const flush = ANSI.moveCursorAndClear(rendererStartLine, 1)
|
|
1262
|
+
|
|
1263
|
+
const outputLine = this._terminalHeight - this._splitHeight
|
|
1264
|
+
const move = ANSI.moveCursor(outputLine, 1)
|
|
1265
|
+
|
|
1266
|
+
let clear = ""
|
|
1267
|
+
if (space > 0) {
|
|
1268
|
+
const backgroundColor = this.backgroundColor.toInts()
|
|
1269
|
+
const newlines = " ".repeat(this.width) + "\n".repeat(space)
|
|
1270
|
+
// Check if background is transparent (alpha = 0)
|
|
1271
|
+
if (backgroundColor[3] === 0) {
|
|
1272
|
+
clear = newlines
|
|
1273
|
+
} else {
|
|
1274
|
+
clear =
|
|
1275
|
+
ANSI.setRgbBackground(backgroundColor[0], backgroundColor[1], backgroundColor[2]) +
|
|
1276
|
+
newlines +
|
|
1277
|
+
ANSI.resetBackground
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
this.writeOut(flush + move + output + clear)
|
|
1282
|
+
|
|
1283
|
+
return true
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
private enableMouse(): void {
|
|
1287
|
+
this._useMouse = true
|
|
1288
|
+
this.lib.enableMouse(this.rendererPtr, this.enableMouseMovement)
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
private disableMouse(): void {
|
|
1292
|
+
this._useMouse = false
|
|
1293
|
+
this.setCapturedRenderable(undefined)
|
|
1294
|
+
this.stdinParser?.resetMouseState()
|
|
1295
|
+
this.lib.disableMouse(this.rendererPtr)
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
public enableKittyKeyboard(flags: number = 0b00011): void {
|
|
1299
|
+
this.lib.enableKittyKeyboard(this.rendererPtr, flags)
|
|
1300
|
+
this.updateStdinParserProtocolContext({ kittyKeyboardEnabled: true })
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
public disableKittyKeyboard(): void {
|
|
1304
|
+
this.lib.disableKittyKeyboard(this.rendererPtr)
|
|
1305
|
+
this.updateStdinParserProtocolContext({ kittyKeyboardEnabled: false }, true)
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
public set useThread(useThread: boolean) {
|
|
1309
|
+
this._useThread = useThread
|
|
1310
|
+
this.lib.setUseThread(this.rendererPtr, useThread)
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// TODO: All input management may move to native when zig finally has async io support again,
|
|
1314
|
+
// without rolling a full event loop
|
|
1315
|
+
public async setupTerminal(): Promise<void> {
|
|
1316
|
+
if (this._terminalIsSetup) return
|
|
1317
|
+
this._terminalIsSetup = true
|
|
1318
|
+
|
|
1319
|
+
this.updateStdinParserProtocolContext({
|
|
1320
|
+
privateCapabilityRepliesActive: true,
|
|
1321
|
+
explicitWidthCprActive: true,
|
|
1322
|
+
})
|
|
1323
|
+
this.lib.setupTerminal(this.rendererPtr, this._screenMode === "alternate-screen")
|
|
1324
|
+
this._capabilities = this.lib.getTerminalCapabilities(this.rendererPtr)
|
|
1325
|
+
|
|
1326
|
+
if (this.debugOverlay.enabled) {
|
|
1327
|
+
this.lib.setDebugOverlay(this.rendererPtr, true, this.debugOverlay.corner)
|
|
1328
|
+
if (!this.memorySnapshotInterval) {
|
|
1329
|
+
this.memorySnapshotInterval = 3000
|
|
1330
|
+
this.startMemorySnapshotTimer()
|
|
1331
|
+
this.automaticMemorySnapshot = true
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
this.capabilityTimeoutId = this.clock.setTimeout(() => {
|
|
1336
|
+
this.capabilityTimeoutId = null
|
|
1337
|
+
this.removeInputHandler(this.capabilityHandler)
|
|
1338
|
+
this.updateStdinParserProtocolContext(
|
|
1339
|
+
{
|
|
1340
|
+
privateCapabilityRepliesActive: false,
|
|
1341
|
+
explicitWidthCprActive: false,
|
|
1342
|
+
},
|
|
1343
|
+
true,
|
|
1344
|
+
)
|
|
1345
|
+
}, 5000)
|
|
1346
|
+
|
|
1347
|
+
if (this._useMouse) {
|
|
1348
|
+
this.enableMouse()
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
this.queryPixelResolution()
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private stdinListener: (chunk: Buffer | string) => void = ((chunk: Buffer | string) => {
|
|
1355
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
1356
|
+
if (!this.stdinParser) return
|
|
1357
|
+
|
|
1358
|
+
try {
|
|
1359
|
+
this.stdinParser.push(data)
|
|
1360
|
+
this.drainStdinParser()
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
this.handleStdinParserFailure(error)
|
|
1363
|
+
}
|
|
1364
|
+
}).bind(this)
|
|
1365
|
+
|
|
1366
|
+
public addInputHandler(handler: (sequence: string) => boolean): void {
|
|
1367
|
+
this.sequenceHandlers.push(handler)
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
public prependInputHandler(handler: (sequence: string) => boolean): void {
|
|
1371
|
+
this.sequenceHandlers.unshift(handler)
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
public removeInputHandler(handler: (sequence: string) => boolean): void {
|
|
1375
|
+
this.sequenceHandlers = this.sequenceHandlers.filter((candidate) => candidate !== handler)
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
private updateStdinParserProtocolContext(patch: Partial<StdinParserProtocolContext>, drain = false): void {
|
|
1379
|
+
if (!this.stdinParser) return
|
|
1380
|
+
this.stdinParser.updateProtocolContext(patch)
|
|
1381
|
+
if (drain) this.drainStdinParser()
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
public subscribeOsc(handler: (sequence: string) => void): () => void {
|
|
1385
|
+
this.oscSubscribers.add(handler)
|
|
1386
|
+
return () => {
|
|
1387
|
+
this.oscSubscribers.delete(handler)
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
private capabilityHandler: (sequence: string) => boolean = ((sequence: string) => {
|
|
1392
|
+
if (isCapabilityResponse(sequence)) {
|
|
1393
|
+
this.lib.processCapabilityResponse(this.rendererPtr, sequence)
|
|
1394
|
+
this._capabilities = this.lib.getTerminalCapabilities(this.rendererPtr)
|
|
1395
|
+
this.emit(CliRenderEvents.CAPABILITIES, this._capabilities)
|
|
1396
|
+
return true
|
|
1397
|
+
}
|
|
1398
|
+
return false
|
|
1399
|
+
}).bind(this)
|
|
1400
|
+
|
|
1401
|
+
private focusHandler: (sequence: string) => boolean = ((sequence: string) => {
|
|
1402
|
+
if (sequence === "\x1b[I") {
|
|
1403
|
+
// When the terminal regains focus, some terminal emulators (notably
|
|
1404
|
+
// Windows Terminal / ConPTY) may have stripped DEC private modes like
|
|
1405
|
+
// mouse tracking, bracketed paste, and focus tracking itself while the
|
|
1406
|
+
// window was unfocused.
|
|
1407
|
+
if (this.shouldRestoreModesOnNextFocus) {
|
|
1408
|
+
this.lib.restoreTerminalModes(this.rendererPtr)
|
|
1409
|
+
this.shouldRestoreModesOnNextFocus = false
|
|
1410
|
+
}
|
|
1411
|
+
if (this._terminalFocusState !== true) {
|
|
1412
|
+
this._terminalFocusState = true
|
|
1413
|
+
this.emit(CliRenderEvents.FOCUS)
|
|
1414
|
+
}
|
|
1415
|
+
return true
|
|
1416
|
+
}
|
|
1417
|
+
if (sequence === "\x1b[O") {
|
|
1418
|
+
this.shouldRestoreModesOnNextFocus = true
|
|
1419
|
+
if (this._terminalFocusState !== false) {
|
|
1420
|
+
this._terminalFocusState = false
|
|
1421
|
+
this.emit(CliRenderEvents.BLUR)
|
|
1422
|
+
}
|
|
1423
|
+
return true
|
|
1424
|
+
}
|
|
1425
|
+
return false
|
|
1426
|
+
}).bind(this)
|
|
1427
|
+
|
|
1428
|
+
private themeModeHandler: (sequence: string) => boolean = ((sequence: string) => {
|
|
1429
|
+
if (sequence === "\x1b[?997;1n") {
|
|
1430
|
+
if (this._themeMode !== "dark") {
|
|
1431
|
+
this._themeMode = "dark"
|
|
1432
|
+
this.emit(CliRenderEvents.THEME_MODE, "dark")
|
|
1433
|
+
}
|
|
1434
|
+
return true
|
|
1435
|
+
}
|
|
1436
|
+
if (sequence === "\x1b[?997;2n") {
|
|
1437
|
+
if (this._themeMode !== "light") {
|
|
1438
|
+
this._themeMode = "light"
|
|
1439
|
+
this.emit(CliRenderEvents.THEME_MODE, "light")
|
|
1440
|
+
}
|
|
1441
|
+
return true
|
|
1442
|
+
}
|
|
1443
|
+
return false
|
|
1444
|
+
}).bind(this)
|
|
1445
|
+
|
|
1446
|
+
private dispatchSequenceHandlers(sequence: string): boolean {
|
|
1447
|
+
if (this._debugModeEnabled) {
|
|
1448
|
+
this._debugInputs.push({
|
|
1449
|
+
timestamp: new Date().toISOString(),
|
|
1450
|
+
sequence,
|
|
1451
|
+
})
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
for (const handler of this.sequenceHandlers) {
|
|
1455
|
+
if (handler(sequence)) {
|
|
1456
|
+
return true
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
return false
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
private drainStdinParser(): void {
|
|
1464
|
+
if (!this.stdinParser) return
|
|
1465
|
+
|
|
1466
|
+
this.stdinParser.drain((event) => {
|
|
1467
|
+
this.handleStdinEvent(event)
|
|
1468
|
+
})
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
private handleStdinEvent(event: StdinEvent): void {
|
|
1472
|
+
switch (event.type) {
|
|
1473
|
+
case "key":
|
|
1474
|
+
if (this.dispatchSequenceHandlers(event.raw)) {
|
|
1475
|
+
return
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
this._keyHandler.processParsedKey(event.key)
|
|
1479
|
+
return
|
|
1480
|
+
case "mouse":
|
|
1481
|
+
if (this._useMouse && this.processSingleMouseEvent(event.event)) {
|
|
1482
|
+
return
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
this.dispatchSequenceHandlers(event.raw)
|
|
1486
|
+
return
|
|
1487
|
+
case "paste":
|
|
1488
|
+
this._keyHandler.processPaste(event.bytes, event.metadata)
|
|
1489
|
+
return
|
|
1490
|
+
case "response":
|
|
1491
|
+
if (event.protocol === "osc") {
|
|
1492
|
+
for (const subscriber of this.oscSubscribers) {
|
|
1493
|
+
subscriber(event.sequence)
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
this.dispatchSequenceHandlers(event.sequence)
|
|
1498
|
+
return
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
private handleStdinParserFailure(error: unknown): void {
|
|
1503
|
+
if (!this.hasLoggedStdinParserError) {
|
|
1504
|
+
this.hasLoggedStdinParserError = true
|
|
1505
|
+
if (process.env.NODE_ENV !== "test") {
|
|
1506
|
+
console.error("[stdin-parser-error] parser failure, resetting parser", error)
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
try {
|
|
1511
|
+
this.stdinParser?.reset()
|
|
1512
|
+
} catch (resetError) {
|
|
1513
|
+
console.error("stdin parser reset failed after parser error", resetError)
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
private setupInput(): void {
|
|
1518
|
+
for (const handler of this.prependedInputHandlers) {
|
|
1519
|
+
this.addInputHandler(handler)
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
this.addInputHandler((sequence: string) => {
|
|
1523
|
+
if (isPixelResolutionResponse(sequence) && this.waitingForPixelResolution) {
|
|
1524
|
+
const resolution = parsePixelResolution(sequence)
|
|
1525
|
+
if (resolution) {
|
|
1526
|
+
this._resolution = resolution
|
|
1527
|
+
}
|
|
1528
|
+
this.waitingForPixelResolution = false
|
|
1529
|
+
this.updateStdinParserProtocolContext({ pixelResolutionQueryActive: false }, true)
|
|
1530
|
+
return true
|
|
1531
|
+
}
|
|
1532
|
+
return false
|
|
1533
|
+
})
|
|
1534
|
+
this.addInputHandler(this.capabilityHandler)
|
|
1535
|
+
this.addInputHandler(this.focusHandler)
|
|
1536
|
+
this.addInputHandler(this.themeModeHandler)
|
|
1537
|
+
|
|
1538
|
+
if (this.stdin.setRawMode) {
|
|
1539
|
+
this.stdin.setRawMode(true)
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
this.stdin.on("data", this.stdinListener)
|
|
1543
|
+
this.stdin.resume()
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
private dispatchMouseEvent(
|
|
1547
|
+
target: Renderable,
|
|
1548
|
+
attributes: RawMouseEvent & { source?: Renderable; isDragging?: boolean },
|
|
1549
|
+
): MouseEvent {
|
|
1550
|
+
const event = new MouseEvent(target, attributes)
|
|
1551
|
+
target.processMouseEvent(event)
|
|
1552
|
+
|
|
1553
|
+
if (this.autoFocus && event.type === "down" && event.button === MouseButton.LEFT && !event.defaultPrevented) {
|
|
1554
|
+
let current: Renderable | null = target
|
|
1555
|
+
while (current) {
|
|
1556
|
+
if (current.focusable) {
|
|
1557
|
+
current.focus()
|
|
1558
|
+
break
|
|
1559
|
+
}
|
|
1560
|
+
current = current.parent
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
return event
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
private processSingleMouseEvent(mouseEvent: RawMouseEvent): boolean {
|
|
1568
|
+
if (this._splitHeight > 0) {
|
|
1569
|
+
if (mouseEvent.y < this.renderOffset) {
|
|
1570
|
+
return false
|
|
1571
|
+
}
|
|
1572
|
+
mouseEvent.y -= this.renderOffset
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
this._latestPointer.x = mouseEvent.x
|
|
1576
|
+
this._latestPointer.y = mouseEvent.y
|
|
1577
|
+
this._hasPointer = true
|
|
1578
|
+
this._lastPointerModifiers = mouseEvent.modifiers
|
|
1579
|
+
|
|
1580
|
+
if (this._console.visible) {
|
|
1581
|
+
const consoleBounds = this._console.bounds
|
|
1582
|
+
if (
|
|
1583
|
+
mouseEvent.x >= consoleBounds.x &&
|
|
1584
|
+
mouseEvent.x < consoleBounds.x + consoleBounds.width &&
|
|
1585
|
+
mouseEvent.y >= consoleBounds.y &&
|
|
1586
|
+
mouseEvent.y < consoleBounds.y + consoleBounds.height
|
|
1587
|
+
) {
|
|
1588
|
+
const event = new MouseEvent(null, mouseEvent)
|
|
1589
|
+
const handled = this._console.handleMouse(event)
|
|
1590
|
+
if (handled) return true
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
if (mouseEvent.type === "scroll") {
|
|
1595
|
+
const maybeRenderableId = this.hitTest(mouseEvent.x, mouseEvent.y)
|
|
1596
|
+
const maybeRenderable = Renderable.renderablesByNumber.get(maybeRenderableId)
|
|
1597
|
+
const fallbackTarget =
|
|
1598
|
+
this._currentFocusedRenderable &&
|
|
1599
|
+
!this._currentFocusedRenderable.isDestroyed &&
|
|
1600
|
+
this._currentFocusedRenderable.focused
|
|
1601
|
+
? this._currentFocusedRenderable
|
|
1602
|
+
: null
|
|
1603
|
+
const scrollTarget = maybeRenderable ?? fallbackTarget
|
|
1604
|
+
|
|
1605
|
+
if (scrollTarget) {
|
|
1606
|
+
const event = new MouseEvent(scrollTarget, mouseEvent)
|
|
1607
|
+
scrollTarget.processMouseEvent(event)
|
|
1608
|
+
}
|
|
1609
|
+
return true
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
const maybeRenderableId = this.hitTest(mouseEvent.x, mouseEvent.y)
|
|
1613
|
+
const sameElement = maybeRenderableId === this.lastOverRenderableNum
|
|
1614
|
+
this.lastOverRenderableNum = maybeRenderableId
|
|
1615
|
+
const maybeRenderable = Renderable.renderablesByNumber.get(maybeRenderableId)
|
|
1616
|
+
|
|
1617
|
+
if (
|
|
1618
|
+
mouseEvent.type === "down" &&
|
|
1619
|
+
mouseEvent.button === MouseButton.LEFT &&
|
|
1620
|
+
!this.currentSelection?.isDragging &&
|
|
1621
|
+
!mouseEvent.modifiers.ctrl
|
|
1622
|
+
) {
|
|
1623
|
+
const canStartSelection = Boolean(
|
|
1624
|
+
maybeRenderable &&
|
|
1625
|
+
maybeRenderable.selectable &&
|
|
1626
|
+
!maybeRenderable.isDestroyed &&
|
|
1627
|
+
maybeRenderable.shouldStartSelection(mouseEvent.x, mouseEvent.y),
|
|
1628
|
+
)
|
|
1629
|
+
|
|
1630
|
+
if (canStartSelection && maybeRenderable) {
|
|
1631
|
+
this.startSelection(maybeRenderable, mouseEvent.x, mouseEvent.y)
|
|
1632
|
+
this.dispatchMouseEvent(maybeRenderable, mouseEvent)
|
|
1633
|
+
return true
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
if (mouseEvent.type === "drag" && this.currentSelection?.isDragging) {
|
|
1638
|
+
this.updateSelection(maybeRenderable, mouseEvent.x, mouseEvent.y)
|
|
1639
|
+
|
|
1640
|
+
if (maybeRenderable) {
|
|
1641
|
+
const event = new MouseEvent(maybeRenderable, { ...mouseEvent, isDragging: true })
|
|
1642
|
+
maybeRenderable.processMouseEvent(event)
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
return true
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (mouseEvent.type === "up" && this.currentSelection?.isDragging) {
|
|
1649
|
+
if (maybeRenderable) {
|
|
1650
|
+
const event = new MouseEvent(maybeRenderable, { ...mouseEvent, isDragging: true })
|
|
1651
|
+
maybeRenderable.processMouseEvent(event)
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
this.finishSelection()
|
|
1655
|
+
return true
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (mouseEvent.type === "down" && mouseEvent.button === MouseButton.LEFT && this.currentSelection) {
|
|
1659
|
+
if (mouseEvent.modifiers.ctrl) {
|
|
1660
|
+
this.currentSelection.isDragging = true
|
|
1661
|
+
this.updateSelection(maybeRenderable, mouseEvent.x, mouseEvent.y)
|
|
1662
|
+
return true
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
if (!sameElement && (mouseEvent.type === "drag" || mouseEvent.type === "move")) {
|
|
1667
|
+
if (
|
|
1668
|
+
this.lastOverRenderable &&
|
|
1669
|
+
this.lastOverRenderable !== this.capturedRenderable &&
|
|
1670
|
+
!this.lastOverRenderable.isDestroyed
|
|
1671
|
+
) {
|
|
1672
|
+
const event = new MouseEvent(this.lastOverRenderable, { ...mouseEvent, type: "out" })
|
|
1673
|
+
this.lastOverRenderable.processMouseEvent(event)
|
|
1674
|
+
}
|
|
1675
|
+
this.lastOverRenderable = maybeRenderable
|
|
1676
|
+
if (maybeRenderable) {
|
|
1677
|
+
const event = new MouseEvent(maybeRenderable, {
|
|
1678
|
+
...mouseEvent,
|
|
1679
|
+
type: "over",
|
|
1680
|
+
source: this.capturedRenderable,
|
|
1681
|
+
})
|
|
1682
|
+
maybeRenderable.processMouseEvent(event)
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
if (this.capturedRenderable && mouseEvent.type !== "up") {
|
|
1687
|
+
const event = new MouseEvent(this.capturedRenderable, mouseEvent)
|
|
1688
|
+
this.capturedRenderable.processMouseEvent(event)
|
|
1689
|
+
return true
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
if (this.capturedRenderable && mouseEvent.type === "up") {
|
|
1693
|
+
const event = new MouseEvent(this.capturedRenderable, { ...mouseEvent, type: "drag-end" })
|
|
1694
|
+
this.capturedRenderable.processMouseEvent(event)
|
|
1695
|
+
this.capturedRenderable.processMouseEvent(new MouseEvent(this.capturedRenderable, mouseEvent))
|
|
1696
|
+
if (maybeRenderable) {
|
|
1697
|
+
const event = new MouseEvent(maybeRenderable, {
|
|
1698
|
+
...mouseEvent,
|
|
1699
|
+
type: "drop",
|
|
1700
|
+
source: this.capturedRenderable,
|
|
1701
|
+
})
|
|
1702
|
+
maybeRenderable.processMouseEvent(event)
|
|
1703
|
+
}
|
|
1704
|
+
this.lastOverRenderable = this.capturedRenderable
|
|
1705
|
+
this.lastOverRenderableNum = this.capturedRenderable.num
|
|
1706
|
+
this.setCapturedRenderable(undefined)
|
|
1707
|
+
// Dropping the renderable needs to push another frame when the renderer is not live
|
|
1708
|
+
// to update the hit grid, otherwise capturedRenderable won't be in the hit grid and will not receive mouse events
|
|
1709
|
+
this.requestRender()
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
let event: MouseEvent | undefined
|
|
1713
|
+
if (maybeRenderable) {
|
|
1714
|
+
if (mouseEvent.type === "drag" && mouseEvent.button === MouseButton.LEFT) {
|
|
1715
|
+
this.setCapturedRenderable(maybeRenderable)
|
|
1716
|
+
} else {
|
|
1717
|
+
this.setCapturedRenderable(undefined)
|
|
1718
|
+
}
|
|
1719
|
+
event = this.dispatchMouseEvent(maybeRenderable, mouseEvent)
|
|
1720
|
+
} else {
|
|
1721
|
+
this.setCapturedRenderable(undefined)
|
|
1722
|
+
this.lastOverRenderable = undefined
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (!event?.defaultPrevented && mouseEvent.type === "down" && this.currentSelection) {
|
|
1726
|
+
this.clearSelection()
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
return true
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* Recheck hover state after hit grid changes.
|
|
1734
|
+
* Called after render when native code detects the hit grid changed.
|
|
1735
|
+
* Fires out/over events if the element under the cursor changed.
|
|
1736
|
+
*/
|
|
1737
|
+
private recheckHoverState(): void {
|
|
1738
|
+
if (this._isDestroyed || !this._hasPointer) return
|
|
1739
|
+
if (this.capturedRenderable) return
|
|
1740
|
+
|
|
1741
|
+
const hitId = this.hitTest(this._latestPointer.x, this._latestPointer.y)
|
|
1742
|
+
const hitRenderable = Renderable.renderablesByNumber.get(hitId)
|
|
1743
|
+
const lastOver = this.lastOverRenderable
|
|
1744
|
+
|
|
1745
|
+
// No change
|
|
1746
|
+
if (lastOver?.num === hitId) {
|
|
1747
|
+
this.lastOverRenderableNum = hitId
|
|
1748
|
+
return
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const baseEvent: RawMouseEvent = {
|
|
1752
|
+
type: "move",
|
|
1753
|
+
button: 0,
|
|
1754
|
+
x: this._latestPointer.x,
|
|
1755
|
+
y: this._latestPointer.y,
|
|
1756
|
+
modifiers: this._lastPointerModifiers,
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// Fire out on old element
|
|
1760
|
+
if (lastOver && !lastOver.isDestroyed) {
|
|
1761
|
+
const event = new MouseEvent(lastOver, { ...baseEvent, type: "out" })
|
|
1762
|
+
lastOver.processMouseEvent(event)
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
this.lastOverRenderable = hitRenderable
|
|
1766
|
+
this.lastOverRenderableNum = hitId
|
|
1767
|
+
|
|
1768
|
+
// Fire over on new element
|
|
1769
|
+
if (hitRenderable) {
|
|
1770
|
+
const event = new MouseEvent(hitRenderable, {
|
|
1771
|
+
...baseEvent,
|
|
1772
|
+
type: "over",
|
|
1773
|
+
})
|
|
1774
|
+
hitRenderable.processMouseEvent(event)
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
public setMousePointer(style: MousePointerStyle): void {
|
|
1778
|
+
this._currentMousePointerStyle = style
|
|
1779
|
+
this.lib.setCursorStyleOptions(this.rendererPtr, { cursor: style })
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
public hitTest(x: number, y: number): number {
|
|
1783
|
+
return this.lib.checkHit(this.rendererPtr, x, y)
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
private takeMemorySnapshot(): void {
|
|
1787
|
+
if (this._isDestroyed) return
|
|
1788
|
+
|
|
1789
|
+
const memoryUsage = process.memoryUsage()
|
|
1790
|
+
this.lastMemorySnapshot = {
|
|
1791
|
+
heapUsed: memoryUsage.heapUsed,
|
|
1792
|
+
heapTotal: memoryUsage.heapTotal,
|
|
1793
|
+
arrayBuffers: memoryUsage.arrayBuffers,
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
this.lib.updateMemoryStats(
|
|
1797
|
+
this.rendererPtr,
|
|
1798
|
+
this.lastMemorySnapshot.heapUsed,
|
|
1799
|
+
this.lastMemorySnapshot.heapTotal,
|
|
1800
|
+
this.lastMemorySnapshot.arrayBuffers,
|
|
1801
|
+
)
|
|
1802
|
+
|
|
1803
|
+
this.emit(CliRenderEvents.MEMORY_SNAPSHOT, this.lastMemorySnapshot)
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
private startMemorySnapshotTimer(): void {
|
|
1807
|
+
this.stopMemorySnapshotTimer()
|
|
1808
|
+
|
|
1809
|
+
this.memorySnapshotTimer = this.clock.setInterval(() => {
|
|
1810
|
+
this.takeMemorySnapshot()
|
|
1811
|
+
}, this.memorySnapshotInterval)
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
private stopMemorySnapshotTimer(): void {
|
|
1815
|
+
if (this.memorySnapshotTimer) {
|
|
1816
|
+
this.clock.clearInterval(this.memorySnapshotTimer)
|
|
1817
|
+
this.memorySnapshotTimer = null
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
public setMemorySnapshotInterval(interval: number): void {
|
|
1822
|
+
this.memorySnapshotInterval = interval
|
|
1823
|
+
|
|
1824
|
+
if (this._isRunning && interval > 0) {
|
|
1825
|
+
this.startMemorySnapshotTimer()
|
|
1826
|
+
} else if (interval <= 0 && this.memorySnapshotTimer) {
|
|
1827
|
+
this.clock.clearInterval(this.memorySnapshotTimer)
|
|
1828
|
+
this.memorySnapshotTimer = null
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
private handleResize(width: number, height: number): void {
|
|
1833
|
+
if (this._isDestroyed) return
|
|
1834
|
+
if (this._splitHeight > 0) {
|
|
1835
|
+
this.processResize(width, height)
|
|
1836
|
+
return
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
if (this.resizeTimeoutId !== null) {
|
|
1840
|
+
this.clock.clearTimeout(this.resizeTimeoutId)
|
|
1841
|
+
this.resizeTimeoutId = null
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
this.resizeTimeoutId = this.clock.setTimeout(() => {
|
|
1845
|
+
this.resizeTimeoutId = null
|
|
1846
|
+
this.processResize(width, height)
|
|
1847
|
+
}, this.resizeDebounceDelay)
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
private queryPixelResolution() {
|
|
1851
|
+
this.waitingForPixelResolution = true
|
|
1852
|
+
this.updateStdinParserProtocolContext({ pixelResolutionQueryActive: true })
|
|
1853
|
+
this.lib.queryPixelResolution(this.rendererPtr)
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
private processResize(width: number, height: number): void {
|
|
1857
|
+
if (width === this._terminalWidth && height === this._terminalHeight) return
|
|
1858
|
+
|
|
1859
|
+
const prevWidth = this._terminalWidth
|
|
1860
|
+
|
|
1861
|
+
this._terminalWidth = width
|
|
1862
|
+
this._terminalHeight = height
|
|
1863
|
+
this.queryPixelResolution()
|
|
1864
|
+
|
|
1865
|
+
this.setCapturedRenderable(undefined)
|
|
1866
|
+
this.stdinParser?.resetMouseState()
|
|
1867
|
+
|
|
1868
|
+
if (this._splitHeight > 0) {
|
|
1869
|
+
// TODO: Handle resizing split mode properly
|
|
1870
|
+
if (width < prevWidth) {
|
|
1871
|
+
const start = this._terminalHeight - this._splitHeight * 2
|
|
1872
|
+
const flush = ANSI.moveCursorAndClear(start, 1)
|
|
1873
|
+
this.writeOut(flush)
|
|
1874
|
+
}
|
|
1875
|
+
this.renderOffset = height - this._splitHeight
|
|
1876
|
+
this.width = width
|
|
1877
|
+
this.height = this._splitHeight
|
|
1878
|
+
this.currentRenderBuffer.clear(this.backgroundColor)
|
|
1879
|
+
this.lib.setRenderOffset(this.rendererPtr, this.renderOffset)
|
|
1880
|
+
} else {
|
|
1881
|
+
this.width = width
|
|
1882
|
+
this.height = height
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
this.lib.resizeRenderer(this.rendererPtr, this.width, this.height)
|
|
1886
|
+
this.nextRenderBuffer = this.lib.getNextBuffer(this.rendererPtr)
|
|
1887
|
+
this.currentRenderBuffer = this.lib.getCurrentBuffer(this.rendererPtr)
|
|
1888
|
+
this._console.resize(this.width, this.height)
|
|
1889
|
+
this.root.resize(this.width, this.height)
|
|
1890
|
+
this.emit(CliRenderEvents.RESIZE, this.width, this.height)
|
|
1891
|
+
this.requestRender()
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
public setBackgroundColor(color: ColorInput): void {
|
|
1895
|
+
const parsedColor = parseColor(color)
|
|
1896
|
+
this.lib.setBackgroundColor(this.rendererPtr, parsedColor as RGBA)
|
|
1897
|
+
this.backgroundColor = parsedColor as RGBA
|
|
1898
|
+
this.nextRenderBuffer.clear(parsedColor as RGBA)
|
|
1899
|
+
this.requestRender()
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
public toggleDebugOverlay(): void {
|
|
1903
|
+
const willBeEnabled = !this.debugOverlay.enabled
|
|
1904
|
+
|
|
1905
|
+
if (willBeEnabled && !this.memorySnapshotInterval) {
|
|
1906
|
+
this.memorySnapshotInterval = 3000
|
|
1907
|
+
this.startMemorySnapshotTimer()
|
|
1908
|
+
this.automaticMemorySnapshot = true
|
|
1909
|
+
} else if (!willBeEnabled && this.automaticMemorySnapshot) {
|
|
1910
|
+
this.stopMemorySnapshotTimer()
|
|
1911
|
+
this.memorySnapshotInterval = 0
|
|
1912
|
+
this.automaticMemorySnapshot = false
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
this.debugOverlay.enabled = !this.debugOverlay.enabled
|
|
1916
|
+
this.lib.setDebugOverlay(this.rendererPtr, this.debugOverlay.enabled, this.debugOverlay.corner)
|
|
1917
|
+
this.emit(CliRenderEvents.DEBUG_OVERLAY_TOGGLE, this.debugOverlay.enabled)
|
|
1918
|
+
this.requestRender()
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
public configureDebugOverlay(options: { enabled?: boolean; corner?: DebugOverlayCorner }): void {
|
|
1922
|
+
this.debugOverlay.enabled = options.enabled ?? this.debugOverlay.enabled
|
|
1923
|
+
this.debugOverlay.corner = options.corner ?? this.debugOverlay.corner
|
|
1924
|
+
this.lib.setDebugOverlay(this.rendererPtr, this.debugOverlay.enabled, this.debugOverlay.corner)
|
|
1925
|
+
this.requestRender()
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
public setTerminalTitle(title: string): void {
|
|
1929
|
+
this.lib.setTerminalTitle(this.rendererPtr, title)
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
public copyToClipboardOSC52(text: string, target?: ClipboardTarget): boolean {
|
|
1933
|
+
return this.clipboard.copyToClipboardOSC52(text, target)
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
public clearClipboardOSC52(target?: ClipboardTarget): boolean {
|
|
1937
|
+
return this.clipboard.clearClipboardOSC52(target)
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
public isOsc52Supported(): boolean {
|
|
1941
|
+
return this._capabilities?.osc52 ?? this.clipboard.isOsc52Supported()
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
public dumpHitGrid(): void {
|
|
1945
|
+
this.lib.dumpHitGrid(this.rendererPtr)
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
public dumpBuffers(timestamp?: number): void {
|
|
1949
|
+
this.lib.dumpBuffers(this.rendererPtr, timestamp)
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
public dumpStdoutBuffer(timestamp?: number): void {
|
|
1953
|
+
this.lib.dumpStdoutBuffer(this.rendererPtr, timestamp)
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
public static setCursorPosition(renderer: CliRenderer, x: number, y: number, visible: boolean = true): void {
|
|
1957
|
+
const lib = resolveRenderLib()
|
|
1958
|
+
lib.setCursorPosition(renderer.rendererPtr, x, y, visible)
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
public static setCursorStyle(renderer: CliRenderer, options: CursorStyleOptions): void {
|
|
1962
|
+
const lib = resolveRenderLib()
|
|
1963
|
+
lib.setCursorStyleOptions(renderer.rendererPtr, options)
|
|
1964
|
+
if (options.cursor !== undefined) {
|
|
1965
|
+
renderer._currentMousePointerStyle = options.cursor
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
public static setCursorColor(renderer: CliRenderer, color: RGBA): void {
|
|
1970
|
+
const lib = resolveRenderLib()
|
|
1971
|
+
lib.setCursorColor(renderer.rendererPtr, color)
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
public setCursorPosition(x: number, y: number, visible: boolean = true): void {
|
|
1975
|
+
this.lib.setCursorPosition(this.rendererPtr, x, y, visible)
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
public setCursorStyle(options: CursorStyleOptions): void {
|
|
1979
|
+
this.lib.setCursorStyleOptions(this.rendererPtr, options)
|
|
1980
|
+
if (options.cursor !== undefined) {
|
|
1981
|
+
this._currentMousePointerStyle = options.cursor
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
public setCursorColor(color: RGBA): void {
|
|
1986
|
+
this.lib.setCursorColor(this.rendererPtr, color)
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
public getCursorState() {
|
|
1990
|
+
return this.lib.getCursorState(this.rendererPtr)
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
public addPostProcessFn(processFn: (buffer: OptimizedBuffer, deltaTime: number) => void): void {
|
|
1994
|
+
this.postProcessFns.push(processFn)
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
public removePostProcessFn(processFn: (buffer: OptimizedBuffer, deltaTime: number) => void): void {
|
|
1998
|
+
this.postProcessFns = this.postProcessFns.filter((fn) => fn !== processFn)
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
public clearPostProcessFns(): void {
|
|
2002
|
+
this.postProcessFns = []
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
public setFrameCallback(callback: (deltaTime: number) => Promise<void>): void {
|
|
2006
|
+
this.frameCallbacks.push(callback)
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
public removeFrameCallback(callback: (deltaTime: number) => Promise<void>): void {
|
|
2010
|
+
this.frameCallbacks = this.frameCallbacks.filter((cb) => cb !== callback)
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
public clearFrameCallbacks(): void {
|
|
2014
|
+
this.frameCallbacks = []
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
public requestLive(): void {
|
|
2018
|
+
this.liveRequestCounter++
|
|
2019
|
+
|
|
2020
|
+
if (this._controlState === RendererControlState.IDLE && this.liveRequestCounter > 0) {
|
|
2021
|
+
this._controlState = RendererControlState.AUTO_STARTED
|
|
2022
|
+
this.internalStart()
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
public dropLive(): void {
|
|
2027
|
+
this.liveRequestCounter = Math.max(0, this.liveRequestCounter - 1)
|
|
2028
|
+
|
|
2029
|
+
if (this._controlState === RendererControlState.AUTO_STARTED && this.liveRequestCounter === 0) {
|
|
2030
|
+
this._controlState = RendererControlState.IDLE
|
|
2031
|
+
this.internalPause()
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
public start(): void {
|
|
2036
|
+
this._controlState = RendererControlState.EXPLICIT_STARTED
|
|
2037
|
+
this.internalStart()
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
public auto(): void {
|
|
2041
|
+
this._controlState = this._isRunning ? RendererControlState.AUTO_STARTED : RendererControlState.IDLE
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
private internalStart(): void {
|
|
2045
|
+
if (!this._isRunning && !this._isDestroyed) {
|
|
2046
|
+
this._isRunning = true
|
|
2047
|
+
|
|
2048
|
+
// Invalidate any queued idle one-shot frame.
|
|
2049
|
+
// start()/live/resume transition to the continuous loop, so queued
|
|
2050
|
+
// activateFrame callbacks must no-op via !updateScheduled.
|
|
2051
|
+
this.updateScheduled = false
|
|
2052
|
+
|
|
2053
|
+
if (this.memorySnapshotInterval > 0) {
|
|
2054
|
+
this.startMemorySnapshotTimer()
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
this.startRenderLoop()
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
public pause(): void {
|
|
2062
|
+
this._controlState = RendererControlState.EXPLICIT_PAUSED
|
|
2063
|
+
this.internalPause()
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
public suspend(): void {
|
|
2067
|
+
this._previousControlState = this._controlState
|
|
2068
|
+
|
|
2069
|
+
this._controlState = RendererControlState.EXPLICIT_SUSPENDED
|
|
2070
|
+
this.internalPause()
|
|
2071
|
+
|
|
2072
|
+
this._suspendedMouseEnabled = this._useMouse
|
|
2073
|
+
|
|
2074
|
+
this.disableMouse()
|
|
2075
|
+
this.removeExitListeners()
|
|
2076
|
+
this.waitingForPixelResolution = false
|
|
2077
|
+
this.updateStdinParserProtocolContext({
|
|
2078
|
+
privateCapabilityRepliesActive: false,
|
|
2079
|
+
pixelResolutionQueryActive: false,
|
|
2080
|
+
explicitWidthCprActive: false,
|
|
2081
|
+
})
|
|
2082
|
+
this.stdinParser?.reset()
|
|
2083
|
+
this.stdin.removeListener("data", this.stdinListener)
|
|
2084
|
+
this.lib.suspendRenderer(this.rendererPtr)
|
|
2085
|
+
|
|
2086
|
+
if (this.stdin.setRawMode) {
|
|
2087
|
+
this.stdin.setRawMode(false)
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
this.stdin.pause()
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
public resume(): void {
|
|
2094
|
+
if (this.stdin.setRawMode) {
|
|
2095
|
+
this.stdin.setRawMode(true)
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// Drain any input buffered during suspension before registering the
|
|
2099
|
+
// listener. Adding a "data" listener can auto-resume a Readable, so the
|
|
2100
|
+
// drain must come first while the stream is still paused and read()
|
|
2101
|
+
// pulls from the internal buffer rather than being a flowing-mode no-op.
|
|
2102
|
+
while (this.stdin.read() !== null) {}
|
|
2103
|
+
this.stdin.on("data", this.stdinListener)
|
|
2104
|
+
this.stdin.resume()
|
|
2105
|
+
this.addExitListeners()
|
|
2106
|
+
|
|
2107
|
+
this.lib.resumeRenderer(this.rendererPtr)
|
|
2108
|
+
|
|
2109
|
+
if (this._suspendedMouseEnabled) {
|
|
2110
|
+
this.enableMouse()
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
this.currentRenderBuffer.clear(this.backgroundColor)
|
|
2114
|
+
this._controlState = this._previousControlState
|
|
2115
|
+
|
|
2116
|
+
if (
|
|
2117
|
+
this._previousControlState === RendererControlState.AUTO_STARTED ||
|
|
2118
|
+
this._previousControlState === RendererControlState.EXPLICIT_STARTED
|
|
2119
|
+
) {
|
|
2120
|
+
this.internalStart()
|
|
2121
|
+
} else {
|
|
2122
|
+
this.requestRender()
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
private internalPause(): void {
|
|
2127
|
+
this._isRunning = false
|
|
2128
|
+
|
|
2129
|
+
if (this.renderTimeout) {
|
|
2130
|
+
this.clock.clearTimeout(this.renderTimeout)
|
|
2131
|
+
this.renderTimeout = null
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
if (!this.rendering) {
|
|
2135
|
+
this.resolveIdleIfNeeded()
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
public stop(): void {
|
|
2140
|
+
this._controlState = RendererControlState.EXPLICIT_STOPPED
|
|
2141
|
+
this.internalStop()
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
private internalStop(): void {
|
|
2145
|
+
if (this.isRunning && !this._isDestroyed) {
|
|
2146
|
+
this._isRunning = false
|
|
2147
|
+
|
|
2148
|
+
if (this.memorySnapshotTimer) {
|
|
2149
|
+
this.clock.clearInterval(this.memorySnapshotTimer)
|
|
2150
|
+
this.memorySnapshotTimer = null
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
if (this.renderTimeout) {
|
|
2154
|
+
this.clock.clearTimeout(this.renderTimeout)
|
|
2155
|
+
this.renderTimeout = null
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// If we're currently rendering, the frame will resolve idle when it completes
|
|
2159
|
+
// Otherwise, resolve immediately
|
|
2160
|
+
if (!this.rendering) {
|
|
2161
|
+
this.resolveIdleIfNeeded()
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
public destroy(): void {
|
|
2167
|
+
if (this._isDestroyed) return
|
|
2168
|
+
this._isDestroyed = true
|
|
2169
|
+
this._destroyPending = true
|
|
2170
|
+
|
|
2171
|
+
if (this.rendering) {
|
|
2172
|
+
// Restore terminal/input state immediately, but defer full native teardown until the frame unwinds.
|
|
2173
|
+
this.prepareDestroyDuringRender()
|
|
2174
|
+
return
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
this.finalizeDestroy()
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
private cleanupBeforeDestroy(): void {
|
|
2181
|
+
if (this._destroyCleanupPrepared) return
|
|
2182
|
+
this._destroyCleanupPrepared = true
|
|
2183
|
+
|
|
2184
|
+
process.removeListener("SIGWINCH", this.sigwinchHandler)
|
|
2185
|
+
process.removeListener("uncaughtException", this.handleError)
|
|
2186
|
+
process.removeListener("unhandledRejection", this.handleError)
|
|
2187
|
+
process.removeListener("warning", this.warningHandler)
|
|
2188
|
+
process.removeListener("beforeExit", this.exitHandler)
|
|
2189
|
+
capture.removeListener("write", this.captureCallback)
|
|
2190
|
+
this.removeExitListeners()
|
|
2191
|
+
|
|
2192
|
+
if (this.resizeTimeoutId !== null) {
|
|
2193
|
+
this.clock.clearTimeout(this.resizeTimeoutId)
|
|
2194
|
+
this.resizeTimeoutId = null
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
if (this.capabilityTimeoutId !== null) {
|
|
2198
|
+
this.clock.clearTimeout(this.capabilityTimeoutId)
|
|
2199
|
+
this.capabilityTimeoutId = null
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
if (this.memorySnapshotTimer) {
|
|
2203
|
+
this.clock.clearInterval(this.memorySnapshotTimer)
|
|
2204
|
+
this.memorySnapshotTimer = null
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
if (this.renderTimeout) {
|
|
2208
|
+
this.clock.clearTimeout(this.renderTimeout)
|
|
2209
|
+
this.renderTimeout = null
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
this._isRunning = false
|
|
2213
|
+
this.waitingForPixelResolution = false
|
|
2214
|
+
this.updateStdinParserProtocolContext(
|
|
2215
|
+
{
|
|
2216
|
+
privateCapabilityRepliesActive: false,
|
|
2217
|
+
pixelResolutionQueryActive: false,
|
|
2218
|
+
explicitWidthCprActive: false,
|
|
2219
|
+
},
|
|
2220
|
+
true,
|
|
2221
|
+
)
|
|
2222
|
+
this.setCapturedRenderable(undefined)
|
|
2223
|
+
|
|
2224
|
+
this.stdin.removeListener("data", this.stdinListener)
|
|
2225
|
+
if (this.stdin.setRawMode) {
|
|
2226
|
+
this.stdin.setRawMode(false)
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
this.externalOutputMode = "passthrough"
|
|
2230
|
+
|
|
2231
|
+
if (this._splitHeight > 0) {
|
|
2232
|
+
this.flushStdoutCache(this._splitHeight, true)
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
private prepareDestroyDuringRender(): void {
|
|
2237
|
+
this.cleanupBeforeDestroy()
|
|
2238
|
+
this.lib.suspendRenderer(this.rendererPtr)
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
private finalizeDestroy(): void {
|
|
2242
|
+
if (this._destroyFinalized) return
|
|
2243
|
+
|
|
2244
|
+
this._destroyFinalized = true
|
|
2245
|
+
this._destroyPending = false
|
|
2246
|
+
|
|
2247
|
+
this.cleanupBeforeDestroy()
|
|
2248
|
+
|
|
2249
|
+
// Clean up palette detector
|
|
2250
|
+
if (this._paletteDetector) {
|
|
2251
|
+
this._paletteDetector.cleanup()
|
|
2252
|
+
this._paletteDetector = null
|
|
2253
|
+
}
|
|
2254
|
+
this._paletteDetectionPromise = null
|
|
2255
|
+
this._cachedPalette = null
|
|
2256
|
+
|
|
2257
|
+
this.emit(CliRenderEvents.DESTROY)
|
|
2258
|
+
|
|
2259
|
+
try {
|
|
2260
|
+
this.root.destroyRecursively()
|
|
2261
|
+
} catch (e) {
|
|
2262
|
+
console.error("Error destroying root renderable:", e instanceof Error ? e.stack : String(e))
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
this.stdinParser?.destroy()
|
|
2266
|
+
this.stdinParser = null
|
|
2267
|
+
this.oscSubscribers.clear()
|
|
2268
|
+
this._console.destroy()
|
|
2269
|
+
|
|
2270
|
+
this.lib.destroyRenderer(this.rendererPtr)
|
|
2271
|
+
rendererTracker.removeRenderer(this)
|
|
2272
|
+
|
|
2273
|
+
if (this._onDestroy) {
|
|
2274
|
+
try {
|
|
2275
|
+
this._onDestroy()
|
|
2276
|
+
} catch (e) {
|
|
2277
|
+
console.error("Error in onDestroy callback:", e instanceof Error ? e.stack : String(e))
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
// Resolve any pending idle() calls
|
|
2282
|
+
this.resolveIdleIfNeeded()
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
private startRenderLoop(): void {
|
|
2286
|
+
if (!this._isRunning) return
|
|
2287
|
+
|
|
2288
|
+
this.lastTime = this.normalizeClockTime(this.clock.now(), 0)
|
|
2289
|
+
this.frameCount = 0
|
|
2290
|
+
this.lastFpsTime = this.lastTime
|
|
2291
|
+
this.currentFps = 0
|
|
2292
|
+
|
|
2293
|
+
this.loop()
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
private async loop(): Promise<void> {
|
|
2297
|
+
if (this.rendering || this._isDestroyed) return
|
|
2298
|
+
this.renderTimeout = null
|
|
2299
|
+
|
|
2300
|
+
this.rendering = true
|
|
2301
|
+
if (this.renderTimeout) {
|
|
2302
|
+
this.clock.clearTimeout(this.renderTimeout)
|
|
2303
|
+
this.renderTimeout = null
|
|
2304
|
+
}
|
|
2305
|
+
try {
|
|
2306
|
+
const now = this.normalizeClockTime(this.clock.now(), this.lastTime)
|
|
2307
|
+
const elapsed = this.getElapsedMs(now, this.lastTime)
|
|
2308
|
+
|
|
2309
|
+
const deltaTime = elapsed
|
|
2310
|
+
this.lastTime = now
|
|
2311
|
+
|
|
2312
|
+
this.frameCount++
|
|
2313
|
+
if (this.getElapsedMs(now, this.lastFpsTime) >= 1000) {
|
|
2314
|
+
this.currentFps = this.frameCount
|
|
2315
|
+
this.frameCount = 0
|
|
2316
|
+
this.lastFpsTime = now
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
this.renderStats.frameCount++
|
|
2320
|
+
this.renderStats.fps = this.currentFps
|
|
2321
|
+
const overallStart = performance.now()
|
|
2322
|
+
|
|
2323
|
+
const frameRequests = Array.from(this.animationRequest.values())
|
|
2324
|
+
this.animationRequest.clear()
|
|
2325
|
+
const animationRequestStart = performance.now()
|
|
2326
|
+
for (const callback of frameRequests) {
|
|
2327
|
+
callback(deltaTime)
|
|
2328
|
+
this.dropLive()
|
|
2329
|
+
}
|
|
2330
|
+
const animationRequestEnd = performance.now()
|
|
2331
|
+
const animationRequestTime = animationRequestEnd - animationRequestStart
|
|
2332
|
+
|
|
2333
|
+
const start = performance.now()
|
|
2334
|
+
for (const frameCallback of this.frameCallbacks) {
|
|
2335
|
+
try {
|
|
2336
|
+
await frameCallback(deltaTime)
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
console.error("Error in frame callback:", error)
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
const end = performance.now()
|
|
2342
|
+
this.renderStats.frameCallbackTime = end - start
|
|
2343
|
+
|
|
2344
|
+
this.root.render(this.nextRenderBuffer, deltaTime)
|
|
2345
|
+
|
|
2346
|
+
for (const postProcessFn of this.postProcessFns) {
|
|
2347
|
+
postProcessFn(this.nextRenderBuffer, deltaTime)
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
this._console.renderToBuffer(this.nextRenderBuffer)
|
|
2351
|
+
|
|
2352
|
+
// If destroy() was requested during this frame, skip native work and scheduling.
|
|
2353
|
+
if (!this._isDestroyed) {
|
|
2354
|
+
this.renderNative()
|
|
2355
|
+
|
|
2356
|
+
// Check if hit grid changed and recheck hover state if needed
|
|
2357
|
+
if (this._useMouse && this.lib.getHitGridDirty(this.rendererPtr)) {
|
|
2358
|
+
this.recheckHoverState()
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
const overallFrameTime = performance.now() - overallStart
|
|
2362
|
+
|
|
2363
|
+
// TODO: Add animationRequestTime to stats
|
|
2364
|
+
this.lib.updateStats(
|
|
2365
|
+
this.rendererPtr,
|
|
2366
|
+
overallFrameTime,
|
|
2367
|
+
this.renderStats.fps,
|
|
2368
|
+
this.renderStats.frameCallbackTime,
|
|
2369
|
+
)
|
|
2370
|
+
|
|
2371
|
+
if (this.gatherStats) {
|
|
2372
|
+
this.collectStatSample(overallFrameTime)
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
if (this._isRunning || this.immediateRerenderRequested) {
|
|
2376
|
+
const targetFrameTime = this.immediateRerenderRequested ? this.minTargetFrameTime : this.targetFrameTime
|
|
2377
|
+
const delay = Math.max(1, targetFrameTime - Math.floor(overallFrameTime))
|
|
2378
|
+
this.immediateRerenderRequested = false
|
|
2379
|
+
this.renderTimeout = this.clock.setTimeout(() => {
|
|
2380
|
+
this.renderTimeout = null
|
|
2381
|
+
this.loop()
|
|
2382
|
+
}, delay)
|
|
2383
|
+
} else {
|
|
2384
|
+
this.clock.clearTimeout(this.renderTimeout!)
|
|
2385
|
+
this.renderTimeout = null
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
} finally {
|
|
2389
|
+
this.rendering = false
|
|
2390
|
+
if (this._destroyPending) {
|
|
2391
|
+
this.finalizeDestroy()
|
|
2392
|
+
}
|
|
2393
|
+
this.resolveIdleIfNeeded()
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
public intermediateRender(): void {
|
|
2398
|
+
this.immediateRerenderRequested = true
|
|
2399
|
+
this.loop()
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
private renderNative(): void {
|
|
2403
|
+
if (this.renderingNative) {
|
|
2404
|
+
console.error("Rendering called concurrently")
|
|
2405
|
+
throw new Error("Rendering called concurrently")
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
let force = false
|
|
2409
|
+
if (this._splitHeight > 0) {
|
|
2410
|
+
// TODO: Flickering could maybe be even more reduced by moving the flush to the native layer,
|
|
2411
|
+
// to output the flush with the buffered writer, after the render is done.
|
|
2412
|
+
force = this.flushStdoutCache(this._splitHeight)
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
this.renderingNative = true
|
|
2416
|
+
this.lib.render(this.rendererPtr, force)
|
|
2417
|
+
// this.dumpStdoutBuffer(Date.now())
|
|
2418
|
+
this.renderingNative = false
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
private collectStatSample(frameTime: number): void {
|
|
2422
|
+
this.frameTimes.push(frameTime)
|
|
2423
|
+
if (this.frameTimes.length > this.maxStatSamples) {
|
|
2424
|
+
this.frameTimes.shift()
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
public getStats(): {
|
|
2429
|
+
fps: number
|
|
2430
|
+
frameCount: number
|
|
2431
|
+
frameTimes: number[]
|
|
2432
|
+
averageFrameTime: number
|
|
2433
|
+
minFrameTime: number
|
|
2434
|
+
maxFrameTime: number
|
|
2435
|
+
} {
|
|
2436
|
+
const frameTimes = [...this.frameTimes]
|
|
2437
|
+
const sum = frameTimes.reduce((acc, time) => acc + time, 0)
|
|
2438
|
+
const avg = frameTimes.length ? sum / frameTimes.length : 0
|
|
2439
|
+
const min = frameTimes.length ? Math.min(...frameTimes) : 0
|
|
2440
|
+
const max = frameTimes.length ? Math.max(...frameTimes) : 0
|
|
2441
|
+
|
|
2442
|
+
return {
|
|
2443
|
+
fps: this.renderStats.fps,
|
|
2444
|
+
frameCount: this.renderStats.frameCount,
|
|
2445
|
+
frameTimes,
|
|
2446
|
+
averageFrameTime: avg,
|
|
2447
|
+
minFrameTime: min,
|
|
2448
|
+
maxFrameTime: max,
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
public resetStats(): void {
|
|
2453
|
+
this.frameTimes = []
|
|
2454
|
+
this.renderStats.frameCount = 0
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
public setGatherStats(enabled: boolean): void {
|
|
2458
|
+
this.gatherStats = enabled
|
|
2459
|
+
if (!enabled) {
|
|
2460
|
+
this.frameTimes = []
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
public getSelection(): Selection | null {
|
|
2465
|
+
return this.currentSelection
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
public get hasSelection(): boolean {
|
|
2469
|
+
return !!this.currentSelection
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
public getSelectionContainer(): Renderable | null {
|
|
2473
|
+
return this.selectionContainers.length > 0 ? this.selectionContainers[this.selectionContainers.length - 1] : null
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
public clearSelection(): void {
|
|
2477
|
+
if (this.currentSelection) {
|
|
2478
|
+
for (const renderable of this.currentSelection.touchedRenderables) {
|
|
2479
|
+
if (renderable.selectable && !renderable.isDestroyed) {
|
|
2480
|
+
renderable.onSelectionChanged(null)
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
this.currentSelection = null
|
|
2484
|
+
}
|
|
2485
|
+
this.selectionContainers = []
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* Start a new selection at the given coordinates.
|
|
2490
|
+
* Used by both mouse and keyboard selection.
|
|
2491
|
+
*/
|
|
2492
|
+
public startSelection(renderable: Renderable, x: number, y: number): void {
|
|
2493
|
+
if (!renderable.selectable) return
|
|
2494
|
+
|
|
2495
|
+
this.clearSelection()
|
|
2496
|
+
this.selectionContainers.push(renderable.parent || this.root)
|
|
2497
|
+
this.currentSelection = new Selection(renderable, { x, y }, { x, y })
|
|
2498
|
+
this.currentSelection.isStart = true
|
|
2499
|
+
|
|
2500
|
+
this.notifySelectablesOfSelectionChange()
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
public updateSelection(
|
|
2504
|
+
currentRenderable: Renderable | undefined,
|
|
2505
|
+
x: number,
|
|
2506
|
+
y: number,
|
|
2507
|
+
options?: { finishDragging?: boolean },
|
|
2508
|
+
): void {
|
|
2509
|
+
if (this.currentSelection) {
|
|
2510
|
+
this.currentSelection.isStart = false
|
|
2511
|
+
this.currentSelection.focus = { x, y }
|
|
2512
|
+
|
|
2513
|
+
if (options?.finishDragging) {
|
|
2514
|
+
this.currentSelection.isDragging = false
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
if (this.selectionContainers.length > 0) {
|
|
2518
|
+
const currentContainer = this.selectionContainers[this.selectionContainers.length - 1]
|
|
2519
|
+
|
|
2520
|
+
if (!currentRenderable || !this.isWithinContainer(currentRenderable, currentContainer)) {
|
|
2521
|
+
const parentContainer = currentContainer.parent || this.root
|
|
2522
|
+
this.selectionContainers.push(parentContainer)
|
|
2523
|
+
} else if (currentRenderable && this.selectionContainers.length > 1) {
|
|
2524
|
+
let containerIndex = this.selectionContainers.indexOf(currentRenderable)
|
|
2525
|
+
|
|
2526
|
+
if (containerIndex === -1) {
|
|
2527
|
+
const immediateParent = currentRenderable.parent || this.root
|
|
2528
|
+
containerIndex = this.selectionContainers.indexOf(immediateParent)
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
if (containerIndex !== -1 && containerIndex < this.selectionContainers.length - 1) {
|
|
2532
|
+
this.selectionContainers = this.selectionContainers.slice(0, containerIndex + 1)
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
this.notifySelectablesOfSelectionChange()
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
public requestSelectionUpdate(): void {
|
|
2542
|
+
if (this.currentSelection?.isDragging) {
|
|
2543
|
+
const pointer = this._latestPointer
|
|
2544
|
+
|
|
2545
|
+
const maybeRenderableId = this.hitTest(pointer.x, pointer.y)
|
|
2546
|
+
const maybeRenderable = Renderable.renderablesByNumber.get(maybeRenderableId)
|
|
2547
|
+
|
|
2548
|
+
this.updateSelection(maybeRenderable, pointer.x, pointer.y)
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
private isWithinContainer(renderable: Renderable, container: Renderable): boolean {
|
|
2553
|
+
let current: Renderable | null = renderable
|
|
2554
|
+
while (current) {
|
|
2555
|
+
if (current === container) return true
|
|
2556
|
+
current = current.parent
|
|
2557
|
+
}
|
|
2558
|
+
return false
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
private finishSelection(): void {
|
|
2562
|
+
if (this.currentSelection) {
|
|
2563
|
+
this.currentSelection.isDragging = false
|
|
2564
|
+
this.emit(CliRenderEvents.SELECTION, this.currentSelection)
|
|
2565
|
+
this.notifySelectablesOfSelectionChange()
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
private notifySelectablesOfSelectionChange(): void {
|
|
2570
|
+
const selectedRenderables: Renderable[] = []
|
|
2571
|
+
const touchedRenderables: Renderable[] = []
|
|
2572
|
+
const currentContainer =
|
|
2573
|
+
this.selectionContainers.length > 0 ? this.selectionContainers[this.selectionContainers.length - 1] : this.root
|
|
2574
|
+
|
|
2575
|
+
if (this.currentSelection) {
|
|
2576
|
+
this.walkSelectableRenderables(
|
|
2577
|
+
currentContainer,
|
|
2578
|
+
this.currentSelection.bounds,
|
|
2579
|
+
selectedRenderables,
|
|
2580
|
+
touchedRenderables,
|
|
2581
|
+
)
|
|
2582
|
+
|
|
2583
|
+
for (const renderable of this.currentSelection.touchedRenderables) {
|
|
2584
|
+
if (!touchedRenderables.includes(renderable) && !renderable.isDestroyed) {
|
|
2585
|
+
renderable.onSelectionChanged(null)
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
this.currentSelection.updateSelectedRenderables(selectedRenderables)
|
|
2590
|
+
this.currentSelection.updateTouchedRenderables(touchedRenderables)
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
private walkSelectableRenderables(
|
|
2595
|
+
container: Renderable,
|
|
2596
|
+
selectionBounds: ViewportBounds,
|
|
2597
|
+
selectedRenderables: Renderable[],
|
|
2598
|
+
touchedRenderables: Renderable[],
|
|
2599
|
+
): void {
|
|
2600
|
+
const children = getObjectsInViewport<Renderable>(
|
|
2601
|
+
selectionBounds,
|
|
2602
|
+
container.getChildrenSortedByPrimaryAxis(),
|
|
2603
|
+
container.primaryAxis,
|
|
2604
|
+
0, // padding
|
|
2605
|
+
0, // minTriggerSize - always perform overlap checks for selection
|
|
2606
|
+
)
|
|
2607
|
+
|
|
2608
|
+
for (const child of children) {
|
|
2609
|
+
if (child.selectable) {
|
|
2610
|
+
const hasSelection = child.onSelectionChanged(this.currentSelection)
|
|
2611
|
+
if (hasSelection) {
|
|
2612
|
+
selectedRenderables.push(child)
|
|
2613
|
+
}
|
|
2614
|
+
touchedRenderables.push(child)
|
|
2615
|
+
}
|
|
2616
|
+
if (child.getChildrenCount() > 0) {
|
|
2617
|
+
this.walkSelectableRenderables(child, selectionBounds, selectedRenderables, touchedRenderables)
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
public get paletteDetectionStatus(): "idle" | "detecting" | "cached" {
|
|
2623
|
+
if (this._cachedPalette) return "cached"
|
|
2624
|
+
if (this._paletteDetectionPromise) return "detecting"
|
|
2625
|
+
return "idle"
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
public clearPaletteCache(): void {
|
|
2629
|
+
this._cachedPalette = null
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
/**
|
|
2633
|
+
* Detects the terminal's color palette
|
|
2634
|
+
*
|
|
2635
|
+
* @returns Promise resolving to TerminalColors object containing palette and special colors
|
|
2636
|
+
* @throws Error if renderer is suspended
|
|
2637
|
+
*/
|
|
2638
|
+
public async getPalette(options?: GetPaletteOptions): Promise<TerminalColors> {
|
|
2639
|
+
if (this._controlState === RendererControlState.EXPLICIT_SUSPENDED) {
|
|
2640
|
+
throw new Error("Cannot detect palette while renderer is suspended")
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
const requestedSize = options?.size ?? 16
|
|
2644
|
+
|
|
2645
|
+
if (this._cachedPalette && this._cachedPalette.palette.length !== requestedSize) {
|
|
2646
|
+
this._cachedPalette = null
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
if (this._cachedPalette) {
|
|
2650
|
+
return this._cachedPalette
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
if (this._paletteDetectionPromise) {
|
|
2654
|
+
return this._paletteDetectionPromise
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
if (!this._paletteDetector) {
|
|
2658
|
+
const isLegacyTmux =
|
|
2659
|
+
this.capabilities?.terminal?.name?.toLowerCase()?.includes("tmux") &&
|
|
2660
|
+
this.capabilities?.terminal?.version?.localeCompare("3.6") < 0
|
|
2661
|
+
this._paletteDetector = createTerminalPalette(
|
|
2662
|
+
this.stdin,
|
|
2663
|
+
this.stdout,
|
|
2664
|
+
this.writeOut.bind(this),
|
|
2665
|
+
isLegacyTmux,
|
|
2666
|
+
{
|
|
2667
|
+
subscribeOsc: this.subscribeOsc.bind(this),
|
|
2668
|
+
},
|
|
2669
|
+
this.clock,
|
|
2670
|
+
)
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
this._paletteDetectionPromise = this._paletteDetector.detect(options).then((result) => {
|
|
2674
|
+
this._cachedPalette = result
|
|
2675
|
+
this._paletteDetectionPromise = null
|
|
2676
|
+
return result
|
|
2677
|
+
})
|
|
2678
|
+
|
|
2679
|
+
return this._paletteDetectionPromise
|
|
2680
|
+
}
|
|
2681
|
+
}
|