@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
|
@@ -0,0 +1,1810 @@
|
|
|
1
|
+
// Byte-level stdin parser that turns raw terminal input into typed StdinEvents.
|
|
2
|
+
//
|
|
3
|
+
// This replaces a two-phase token -> decode pipeline with a single state machine
|
|
4
|
+
// that produces fully typed events (key, mouse, paste, response) directly from
|
|
5
|
+
// bytes. The parser owns all byte framing and protocol recognition. It does NOT
|
|
6
|
+
// own event dispatch — that belongs to KeyHandler and the renderer.
|
|
7
|
+
|
|
8
|
+
import { Buffer } from "node:buffer"
|
|
9
|
+
import { SystemClock, type Clock, type TimerHandle } from "./clock.js"
|
|
10
|
+
import { parseKeypress, type ParsedKey } from "./parse.keypress.js"
|
|
11
|
+
import { MouseParser, type RawMouseEvent } from "./parse.mouse.js"
|
|
12
|
+
import type { PasteMetadata } from "./paste.js"
|
|
13
|
+
|
|
14
|
+
export { SystemClock, type Clock, type TimerHandle } from "./clock.js"
|
|
15
|
+
|
|
16
|
+
export type StdinResponseProtocol = "csi" | "osc" | "dcs" | "apc" | "unknown"
|
|
17
|
+
|
|
18
|
+
// The four event types the parser produces. Everything stdin sends becomes
|
|
19
|
+
// exactly one of these.
|
|
20
|
+
export type StdinEvent =
|
|
21
|
+
| {
|
|
22
|
+
type: "key"
|
|
23
|
+
raw: string
|
|
24
|
+
key: ParsedKey
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
type: "mouse"
|
|
28
|
+
raw: string
|
|
29
|
+
encoding: "sgr" | "x10"
|
|
30
|
+
event: RawMouseEvent
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
type: "paste"
|
|
34
|
+
bytes: Uint8Array
|
|
35
|
+
metadata?: PasteMetadata
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
type: "response"
|
|
39
|
+
protocol: StdinResponseProtocol
|
|
40
|
+
sequence: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface StdinParserProtocolContext {
|
|
44
|
+
kittyKeyboardEnabled: boolean
|
|
45
|
+
privateCapabilityRepliesActive: boolean
|
|
46
|
+
pixelResolutionQueryActive: boolean
|
|
47
|
+
explicitWidthCprActive: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface StdinParserOptions {
|
|
51
|
+
timeoutMs?: number
|
|
52
|
+
maxPendingBytes?: number
|
|
53
|
+
armTimeouts?: boolean
|
|
54
|
+
onTimeoutFlush?: () => void
|
|
55
|
+
useKittyKeyboard?: boolean
|
|
56
|
+
protocolContext?: Partial<StdinParserProtocolContext>
|
|
57
|
+
clock?: Clock
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// State machine tags for the byte scanner. Each tag represents which protocol
|
|
61
|
+
// framing mode the parser is currently inside. The sawEsc flag in osc/dcs/apc
|
|
62
|
+
// tracks whether the previous byte was ESC, since the two-byte ST terminator
|
|
63
|
+
// (ESC \) can split across push() calls.
|
|
64
|
+
type ParserState =
|
|
65
|
+
| { tag: "ground" }
|
|
66
|
+
| { tag: "utf8"; expected: number; seen: number }
|
|
67
|
+
| { tag: "esc" }
|
|
68
|
+
| { tag: "ss3" }
|
|
69
|
+
| { tag: "csi" }
|
|
70
|
+
| { tag: "csi_sgr_mouse"; part: number; hasDigit: boolean }
|
|
71
|
+
| { tag: "csi_sgr_mouse_deferred"; part: number; hasDigit: boolean }
|
|
72
|
+
| { tag: "csi_parametric"; semicolons: number; segments: number; hasDigit: boolean; firstParamValue: number | null }
|
|
73
|
+
| {
|
|
74
|
+
tag: "csi_parametric_deferred"
|
|
75
|
+
semicolons: number
|
|
76
|
+
segments: number
|
|
77
|
+
hasDigit: boolean
|
|
78
|
+
firstParamValue: number | null
|
|
79
|
+
}
|
|
80
|
+
| { tag: "csi_private_reply"; semicolons: number; hasDigit: boolean; sawDollar: boolean }
|
|
81
|
+
| { tag: "csi_private_reply_deferred"; semicolons: number; hasDigit: boolean; sawDollar: boolean }
|
|
82
|
+
| { tag: "osc"; sawEsc: boolean }
|
|
83
|
+
| { tag: "dcs"; sawEsc: boolean }
|
|
84
|
+
| { tag: "apc"; sawEsc: boolean }
|
|
85
|
+
| { tag: "esc_recovery" }
|
|
86
|
+
| { tag: "esc_less_mouse" }
|
|
87
|
+
| { tag: "esc_less_x10_mouse" }
|
|
88
|
+
|
|
89
|
+
// Collects paste body incrementally, bypassing the main ByteQueue so large
|
|
90
|
+
// pastes don't grow the parser buffer. Keeps only a small tail for end-marker
|
|
91
|
+
// detection across chunk boundaries.
|
|
92
|
+
interface PasteCollector {
|
|
93
|
+
tail: Uint8Array
|
|
94
|
+
parts: Uint8Array[]
|
|
95
|
+
totalLength: number
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 20ms is to distinguish a lone ESC keypress from the start of an
|
|
99
|
+
// escape sequence. Gemini/Claude uses 50ms, Codex uses 20ms, trying
|
|
100
|
+
// this as a balanced default for now.
|
|
101
|
+
const DEFAULT_TIMEOUT_MS = 20
|
|
102
|
+
const DEFAULT_MAX_PENDING_BYTES = 64 * 1024
|
|
103
|
+
const INITIAL_PENDING_CAPACITY = 256
|
|
104
|
+
const ESC = 0x1b
|
|
105
|
+
const BEL = 0x07
|
|
106
|
+
const BRACKETED_PASTE_START = Buffer.from("\x1b[200~")
|
|
107
|
+
const BRACKETED_PASTE_END = Buffer.from("\x1b[201~")
|
|
108
|
+
const EMPTY_BYTES = new Uint8Array(0)
|
|
109
|
+
const KEY_DECODER = new TextDecoder()
|
|
110
|
+
const DEFAULT_PROTOCOL_CONTEXT: StdinParserProtocolContext = {
|
|
111
|
+
kittyKeyboardEnabled: false,
|
|
112
|
+
privateCapabilityRepliesActive: false,
|
|
113
|
+
pixelResolutionQueryActive: false,
|
|
114
|
+
explicitWidthCprActive: false,
|
|
115
|
+
}
|
|
116
|
+
// rxvt uses $-terminated CSI sequences for shifted function keys (e.g. ESC[2$).
|
|
117
|
+
// Standard CSI treats $ as an intermediate byte, not a final, so we match these
|
|
118
|
+
// explicitly to avoid waiting for a "real" final byte that never arrives.
|
|
119
|
+
const RXVT_DOLLAR_CSI_RE = /^\x1b\[\d+\$$/
|
|
120
|
+
|
|
121
|
+
const SYSTEM_CLOCK = new SystemClock()
|
|
122
|
+
|
|
123
|
+
// Byte buffer for pending input. Uses start/end offsets so consume() just
|
|
124
|
+
// advances the start pointer without copying. Compacts (via copyWithin) only
|
|
125
|
+
// when the consumed prefix exceeds half the buffer, keeping amortized cost low.
|
|
126
|
+
class ByteQueue {
|
|
127
|
+
private buf: Uint8Array
|
|
128
|
+
private start = 0
|
|
129
|
+
private end = 0
|
|
130
|
+
|
|
131
|
+
constructor(capacity = INITIAL_PENDING_CAPACITY) {
|
|
132
|
+
this.buf = new Uint8Array(capacity)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
get length(): number {
|
|
136
|
+
return this.end - this.start
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get capacity(): number {
|
|
140
|
+
return this.buf.length
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
view(): Uint8Array {
|
|
144
|
+
return this.buf.subarray(this.start, this.end)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Returns a view of the contents and resets the queue. The view shares
|
|
148
|
+
// the underlying buffer, so it becomes invalid on the next append().
|
|
149
|
+
take(): Uint8Array {
|
|
150
|
+
const chunk = this.view()
|
|
151
|
+
this.start = 0
|
|
152
|
+
this.end = 0
|
|
153
|
+
return chunk
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
append(chunk: Uint8Array): void {
|
|
157
|
+
if (chunk.length === 0) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.ensureCapacity(this.length + chunk.length)
|
|
162
|
+
this.buf.set(chunk, this.end)
|
|
163
|
+
this.end += chunk.length
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Drops the first `count` bytes. Compacts when the consumed prefix
|
|
167
|
+
// exceeds half the buffer to reclaim wasted space at the front.
|
|
168
|
+
consume(count: number): void {
|
|
169
|
+
if (count <= 0) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (count >= this.length) {
|
|
174
|
+
this.start = 0
|
|
175
|
+
this.end = 0
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.start += count
|
|
180
|
+
if (this.start >= this.buf.length / 2) {
|
|
181
|
+
this.buf.copyWithin(0, this.start, this.end)
|
|
182
|
+
this.end -= this.start
|
|
183
|
+
this.start = 0
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
clear(): void {
|
|
188
|
+
this.start = 0
|
|
189
|
+
this.end = 0
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
reset(capacity = INITIAL_PENDING_CAPACITY): void {
|
|
193
|
+
this.buf = new Uint8Array(capacity)
|
|
194
|
+
this.start = 0
|
|
195
|
+
this.end = 0
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Tries reclaiming space by compacting data to the front first.
|
|
199
|
+
// Doubles the allocation if that still isn't enough.
|
|
200
|
+
private ensureCapacity(requiredLength: number): void {
|
|
201
|
+
const currentLength = this.length
|
|
202
|
+
if (requiredLength <= this.buf.length) {
|
|
203
|
+
const availableAtEnd = this.buf.length - this.end
|
|
204
|
+
if (availableAtEnd >= requiredLength - currentLength) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.buf.copyWithin(0, this.start, this.end)
|
|
209
|
+
this.end = currentLength
|
|
210
|
+
this.start = 0
|
|
211
|
+
if (requiredLength <= this.buf.length) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let nextCapacity = this.buf.length
|
|
217
|
+
while (nextCapacity < requiredLength) {
|
|
218
|
+
nextCapacity *= 2
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const next = new Uint8Array(nextCapacity)
|
|
222
|
+
next.set(this.view(), 0)
|
|
223
|
+
this.buf = next
|
|
224
|
+
this.start = 0
|
|
225
|
+
this.end = currentLength
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function normalizePositiveOption(value: number | undefined, fallback: number): number {
|
|
230
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
231
|
+
return fallback
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return Math.floor(value)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Returns the expected byte count for a UTF-8 sequence given its lead byte,
|
|
238
|
+
// or 0 for bytes that aren't valid UTF-8 leads. Returning 0 tells the parser
|
|
239
|
+
// this is a legacy high-byte character (0x80–0xBF, 0xC0–0xC1, 0xF5+) that
|
|
240
|
+
// goes through the parseKeypress() meta-key path instead.
|
|
241
|
+
function utf8SequenceLength(first: number): number {
|
|
242
|
+
if (first < 0x80) return 1
|
|
243
|
+
if (first >= 0xc2 && first <= 0xdf) return 2
|
|
244
|
+
if (first >= 0xe0 && first <= 0xef) return 3
|
|
245
|
+
if (first >= 0xf0 && first <= 0xf4) return 4
|
|
246
|
+
return 0
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function bytesEqual(left: Uint8Array, right: Uint8Array): boolean {
|
|
250
|
+
if (left.length !== right.length) {
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
255
|
+
if (left[index] !== right[index]) {
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return true
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Checks whether a byte sequence is a complete SGR mouse report:
|
|
264
|
+
// ESC [ < Ps ; Ps ; Ps M/m (three semicolon-separated digit groups).
|
|
265
|
+
function isMouseSgrSequence(sequence: Uint8Array): boolean {
|
|
266
|
+
if (sequence.length < 7) {
|
|
267
|
+
return false
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (sequence[0] !== ESC || sequence[1] !== 0x5b || sequence[2] !== 0x3c) {
|
|
271
|
+
return false
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const final = sequence[sequence.length - 1]
|
|
275
|
+
if (final !== 0x4d && final !== 0x6d) {
|
|
276
|
+
return false
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let part = 0
|
|
280
|
+
let hasDigit = false
|
|
281
|
+
for (let index = 3; index < sequence.length - 1; index += 1) {
|
|
282
|
+
const byte = sequence[index]!
|
|
283
|
+
if (byte >= 0x30 && byte <= 0x39) {
|
|
284
|
+
hasDigit = true
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (byte === 0x3b && hasDigit && part < 2) {
|
|
289
|
+
part += 1
|
|
290
|
+
hasDigit = false
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return part === 2 && hasDigit
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function isAsciiDigit(byte: number): boolean {
|
|
301
|
+
return byte >= 0x30 && byte <= 0x39
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
interface ParametricCsiLike {
|
|
305
|
+
semicolons: number
|
|
306
|
+
segments: number
|
|
307
|
+
hasDigit: boolean
|
|
308
|
+
firstParamValue: number | null
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
interface PrivateReplyCsiLike {
|
|
312
|
+
semicolons: number
|
|
313
|
+
hasDigit: boolean
|
|
314
|
+
sawDollar: boolean
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function parsePositiveDecimalPrefix(sequence: Uint8Array, start: number, endExclusive: number): number | null {
|
|
318
|
+
if (start >= endExclusive) return null
|
|
319
|
+
|
|
320
|
+
let value = 0
|
|
321
|
+
let sawDigit = false
|
|
322
|
+
for (let index = start; index < endExclusive; index += 1) {
|
|
323
|
+
const byte = sequence[index]!
|
|
324
|
+
if (!isAsciiDigit(byte)) return null
|
|
325
|
+
sawDigit = true
|
|
326
|
+
value = value * 10 + (byte - 0x30)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return sawDigit ? value : null
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Returns the leading kitty codepoint from field 1, like `97` in `97:65`.
|
|
333
|
+
// The CSI scanner uses this at `;` boundaries to recognize alternate-key
|
|
334
|
+
// forms (`codepoint[:shifted[:base]]`). That keeps split kitty sequences
|
|
335
|
+
// pending, instead of flushing them as unknown on timeout.
|
|
336
|
+
function parseKittyFirstFieldCodepoint(sequence: Uint8Array, start: number, endExclusive: number): number | null {
|
|
337
|
+
if (start >= endExclusive) return null
|
|
338
|
+
|
|
339
|
+
let firstColon = -1
|
|
340
|
+
for (let index = start; index < endExclusive; index += 1) {
|
|
341
|
+
if (sequence[index] === 0x3a) {
|
|
342
|
+
firstColon = index
|
|
343
|
+
break
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (firstColon === -1) return null
|
|
348
|
+
|
|
349
|
+
const codepoint = parsePositiveDecimalPrefix(sequence, start, firstColon)
|
|
350
|
+
if (codepoint === null) return null
|
|
351
|
+
|
|
352
|
+
// Remaining bytes in field 1 must stay kitty-compatible: digits or colons.
|
|
353
|
+
for (let index = firstColon + 1; index < endExclusive; index += 1) {
|
|
354
|
+
const byte = sequence[index]!
|
|
355
|
+
if (byte !== 0x3a && !isAsciiDigit(byte)) return null
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return codepoint
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function canStillBeKittyU(state: ParametricCsiLike): boolean {
|
|
362
|
+
return state.semicolons >= 1
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function canStillBeKittySpecial(state: ParametricCsiLike): boolean {
|
|
366
|
+
return state.semicolons === 1 && state.segments > 1
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function canStillBeExplicitWidthCpr(state: ParametricCsiLike): boolean {
|
|
370
|
+
return state.firstParamValue === 1 && state.semicolons === 1
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function canStillBePixelResolution(state: ParametricCsiLike): boolean {
|
|
374
|
+
return state.firstParamValue === 4 && state.semicolons === 2
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function canDeferParametricCsi(state: ParametricCsiLike, context: StdinParserProtocolContext): boolean {
|
|
378
|
+
return (
|
|
379
|
+
(context.kittyKeyboardEnabled && (canStillBeKittyU(state) || canStillBeKittySpecial(state))) ||
|
|
380
|
+
(context.explicitWidthCprActive && canStillBeExplicitWidthCpr(state)) ||
|
|
381
|
+
(context.pixelResolutionQueryActive && canStillBePixelResolution(state))
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function canCompleteDeferredParametricCsi(
|
|
386
|
+
state: ParametricCsiLike,
|
|
387
|
+
byte: number,
|
|
388
|
+
context: StdinParserProtocolContext,
|
|
389
|
+
): boolean {
|
|
390
|
+
if (context.kittyKeyboardEnabled) {
|
|
391
|
+
if (state.hasDigit && byte === 0x75) return true
|
|
392
|
+
if (
|
|
393
|
+
state.hasDigit &&
|
|
394
|
+
state.semicolons === 1 &&
|
|
395
|
+
state.segments > 1 &&
|
|
396
|
+
(byte === 0x7e || (byte >= 0x41 && byte <= 0x5a))
|
|
397
|
+
) {
|
|
398
|
+
return true
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (
|
|
403
|
+
context.explicitWidthCprActive &&
|
|
404
|
+
state.hasDigit &&
|
|
405
|
+
state.firstParamValue === 1 &&
|
|
406
|
+
state.semicolons === 1 &&
|
|
407
|
+
byte === 0x52
|
|
408
|
+
) {
|
|
409
|
+
return true
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (
|
|
413
|
+
context.pixelResolutionQueryActive &&
|
|
414
|
+
state.hasDigit &&
|
|
415
|
+
state.firstParamValue === 4 &&
|
|
416
|
+
state.semicolons === 2 &&
|
|
417
|
+
byte === 0x74
|
|
418
|
+
) {
|
|
419
|
+
return true
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return false
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function canDeferPrivateReplyCsi(context: StdinParserProtocolContext): boolean {
|
|
426
|
+
return context.privateCapabilityRepliesActive
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function canCompleteDeferredPrivateReplyCsi(
|
|
430
|
+
state: PrivateReplyCsiLike,
|
|
431
|
+
byte: number,
|
|
432
|
+
context: StdinParserProtocolContext,
|
|
433
|
+
): boolean {
|
|
434
|
+
if (!context.privateCapabilityRepliesActive) return false
|
|
435
|
+
if (state.sawDollar) return state.hasDigit && byte === 0x79
|
|
436
|
+
if (byte === 0x63) return state.hasDigit || state.semicolons > 0
|
|
437
|
+
return state.hasDigit && byte === 0x75
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function concatBytes(left: Uint8Array, right: Uint8Array): Uint8Array {
|
|
441
|
+
if (left.length === 0) {
|
|
442
|
+
return right
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (right.length === 0) {
|
|
446
|
+
return left
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const combined = new Uint8Array(left.length + right.length)
|
|
450
|
+
combined.set(left, 0)
|
|
451
|
+
combined.set(right, left.length)
|
|
452
|
+
return combined
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function indexOfBytes(haystack: Uint8Array, needle: Uint8Array): number {
|
|
456
|
+
if (needle.length === 0) {
|
|
457
|
+
return 0
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const limit = haystack.length - needle.length
|
|
461
|
+
for (let offset = 0; offset <= limit; offset += 1) {
|
|
462
|
+
let matched = true
|
|
463
|
+
for (let index = 0; index < needle.length; index += 1) {
|
|
464
|
+
if (haystack[offset + index] !== needle[index]) {
|
|
465
|
+
matched = false
|
|
466
|
+
break
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (matched) {
|
|
471
|
+
return offset
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return -1
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Decodes raw protocol bytes as latin1. Used for mouse and response events
|
|
479
|
+
// where the wire bytes may not be valid UTF-8 but need a lossless string
|
|
480
|
+
// form for downstream sequence handlers.
|
|
481
|
+
function decodeLatin1(bytes: Uint8Array): string {
|
|
482
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("latin1")
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function decodeUtf8(bytes: Uint8Array): string {
|
|
486
|
+
return KEY_DECODER.decode(bytes)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function createPasteCollector(): PasteCollector {
|
|
490
|
+
return {
|
|
491
|
+
tail: EMPTY_BYTES,
|
|
492
|
+
parts: [],
|
|
493
|
+
totalLength: 0,
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function joinPasteBytes(parts: Uint8Array[], totalLength: number): Uint8Array {
|
|
498
|
+
if (totalLength === 0) {
|
|
499
|
+
return EMPTY_BYTES
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (parts.length === 1) {
|
|
503
|
+
return parts[0]!
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const bytes = new Uint8Array(totalLength)
|
|
507
|
+
let offset = 0
|
|
508
|
+
for (const part of parts) {
|
|
509
|
+
bytes.set(part, offset)
|
|
510
|
+
offset += part.length
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return bytes
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Push-driven stdin parser. Callers feed raw bytes via push(), then read
|
|
517
|
+
// typed events via read() or drain(). At most one incomplete protocol unit
|
|
518
|
+
// is buffered at a time; everything else is immediately converted to events.
|
|
519
|
+
//
|
|
520
|
+
// The parser guarantees chunk-shape invariance: the same bytes always produce
|
|
521
|
+
// the same events, regardless of chunk boundaries. A lone ESC resolves via
|
|
522
|
+
// timeout, split UTF-8 codepoints reassemble correctly, and bracketed paste
|
|
523
|
+
// markers may split across any chunk boundary.
|
|
524
|
+
export class StdinParser {
|
|
525
|
+
private readonly pending = new ByteQueue(INITIAL_PENDING_CAPACITY)
|
|
526
|
+
private readonly events: StdinEvent[] = []
|
|
527
|
+
private readonly timeoutMs: number
|
|
528
|
+
private readonly maxPendingBytes: number
|
|
529
|
+
private readonly armTimeouts: boolean
|
|
530
|
+
private readonly onTimeoutFlush: (() => void) | null
|
|
531
|
+
private readonly useKittyKeyboard: boolean
|
|
532
|
+
private readonly mouseParser = new MouseParser()
|
|
533
|
+
private readonly clock: Clock
|
|
534
|
+
private protocolContext: StdinParserProtocolContext
|
|
535
|
+
private timeoutId: TimerHandle | null = null
|
|
536
|
+
private destroyed = false
|
|
537
|
+
// When the current incomplete unit first appeared. Null when nothing is pending.
|
|
538
|
+
private pendingSinceMs: number | null = null
|
|
539
|
+
// When true, the state machine treats the current incomplete prefix as
|
|
540
|
+
// final and emits it as one atomic event (e.g. a lone ESC becomes an
|
|
541
|
+
// Escape key). Set by the timeout, consumed by the next read() or drain().
|
|
542
|
+
private forceFlush = false
|
|
543
|
+
// True only immediately after a timeout flush emits a lone ESC key. The next
|
|
544
|
+
// `[` may begin a delayed `[<...M/m` mouse continuation recovery path.
|
|
545
|
+
private justFlushedEsc = false
|
|
546
|
+
private state: ParserState = { tag: "ground" }
|
|
547
|
+
// Scan position within pending.view() during scanPending().
|
|
548
|
+
private cursor = 0
|
|
549
|
+
// Start of the protocol unit currently being parsed. The bytes from
|
|
550
|
+
// unitStart through cursor all belong to one atomic unit.
|
|
551
|
+
private unitStart = 0
|
|
552
|
+
// When non-null, the parser is inside a bracketed paste. All incoming
|
|
553
|
+
// bytes flow through consumePasteBytes() instead of the normal state machine.
|
|
554
|
+
private paste: PasteCollector | null = null
|
|
555
|
+
|
|
556
|
+
constructor(options: StdinParserOptions = {}) {
|
|
557
|
+
this.timeoutMs = normalizePositiveOption(options.timeoutMs, DEFAULT_TIMEOUT_MS)
|
|
558
|
+
this.maxPendingBytes = normalizePositiveOption(options.maxPendingBytes, DEFAULT_MAX_PENDING_BYTES)
|
|
559
|
+
this.armTimeouts = options.armTimeouts ?? true
|
|
560
|
+
this.onTimeoutFlush = options.onTimeoutFlush ?? null
|
|
561
|
+
this.useKittyKeyboard = options.useKittyKeyboard ?? true
|
|
562
|
+
this.clock = options.clock ?? SYSTEM_CLOCK
|
|
563
|
+
this.protocolContext = {
|
|
564
|
+
...DEFAULT_PROTOCOL_CONTEXT,
|
|
565
|
+
kittyKeyboardEnabled: options.protocolContext?.kittyKeyboardEnabled ?? false,
|
|
566
|
+
privateCapabilityRepliesActive: options.protocolContext?.privateCapabilityRepliesActive ?? false,
|
|
567
|
+
pixelResolutionQueryActive: options.protocolContext?.pixelResolutionQueryActive ?? false,
|
|
568
|
+
explicitWidthCprActive: options.protocolContext?.explicitWidthCprActive ?? false,
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
public get bufferCapacity(): number {
|
|
573
|
+
return this.pending.capacity
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
public updateProtocolContext(patch: Partial<StdinParserProtocolContext>): void {
|
|
577
|
+
this.ensureAlive()
|
|
578
|
+
this.protocolContext = { ...this.protocolContext, ...patch }
|
|
579
|
+
this.reconcileDeferredStateWithProtocolContext()
|
|
580
|
+
this.reconcileTimeoutState()
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Feeds raw stdin bytes into the parser. Converts as much as possible into
|
|
584
|
+
// queued events and leaves at most one incomplete unit behind in pending.
|
|
585
|
+
//
|
|
586
|
+
// When a chunk contains a paste start marker, bytes before the marker go
|
|
587
|
+
// through normal parsing, then paste mode takes over for the rest. This
|
|
588
|
+
// prevents large pastes from growing the main buffer.
|
|
589
|
+
public push(data: Uint8Array): void {
|
|
590
|
+
this.ensureAlive()
|
|
591
|
+
if (data.length === 0) {
|
|
592
|
+
// Preserve the existing empty-chunk -> empty-keypress behavior.
|
|
593
|
+
this.emitKeyOrResponse("unknown", "")
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let remainder = data
|
|
598
|
+
while (remainder.length > 0) {
|
|
599
|
+
if (this.paste) {
|
|
600
|
+
remainder = this.consumePasteBytes(remainder)
|
|
601
|
+
continue
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// If we're in ground state with nothing pending, scan the incoming
|
|
605
|
+
// chunk for a paste start marker. Only append through the marker so
|
|
606
|
+
// scanPending() enters paste mode without buffering the full paste.
|
|
607
|
+
const immediatePasteStartIndex =
|
|
608
|
+
this.state.tag === "ground" && this.pending.length === 0 ? indexOfBytes(remainder, BRACKETED_PASTE_START) : -1
|
|
609
|
+
const appendEnd =
|
|
610
|
+
immediatePasteStartIndex === -1 ? remainder.length : immediatePasteStartIndex + BRACKETED_PASTE_START.length
|
|
611
|
+
|
|
612
|
+
this.pending.append(remainder.subarray(0, appendEnd))
|
|
613
|
+
remainder = remainder.subarray(appendEnd)
|
|
614
|
+
this.scanPending()
|
|
615
|
+
|
|
616
|
+
if (this.paste && this.pending.length > 0) {
|
|
617
|
+
remainder = this.consumePasteBytes(this.takePendingBytes())
|
|
618
|
+
continue
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!this.paste && this.pending.length > this.maxPendingBytes) {
|
|
622
|
+
this.flushPendingOverflow()
|
|
623
|
+
this.scanPending()
|
|
624
|
+
|
|
625
|
+
if (this.paste && this.pending.length > 0) {
|
|
626
|
+
remainder = this.consumePasteBytes(this.takePendingBytes())
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
this.reconcileTimeoutState()
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Pops one event from the queue. If the queue is empty and a timeout has
|
|
635
|
+
// set forceFlush, re-scans pending to convert the timed-out incomplete
|
|
636
|
+
// unit into one final event before returning it.
|
|
637
|
+
public read(): StdinEvent | null {
|
|
638
|
+
this.ensureAlive()
|
|
639
|
+
|
|
640
|
+
if (this.events.length === 0 && this.forceFlush) {
|
|
641
|
+
this.scanPending()
|
|
642
|
+
this.reconcileTimeoutState()
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return this.events.shift() ?? null
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Delivers all queued events. Stops early if the parser is destroyed
|
|
649
|
+
// during a callback (e.g. an event handler triggers teardown).
|
|
650
|
+
public drain(onEvent: (event: StdinEvent) => void): void {
|
|
651
|
+
this.ensureAlive()
|
|
652
|
+
|
|
653
|
+
while (true) {
|
|
654
|
+
if (this.destroyed) {
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const event = this.read()
|
|
659
|
+
if (!event) {
|
|
660
|
+
return
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
onEvent(event)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Marks the parser for forced flush if enough time has passed since
|
|
668
|
+
// incomplete data arrived. Does not immediately emit events — the next
|
|
669
|
+
// read() or drain() does the actual flush. This separation keeps the
|
|
670
|
+
// timer callback from emitting events mid-flight in user code.
|
|
671
|
+
public flushTimeout(nowMsValue: number = this.clock.now()): void {
|
|
672
|
+
this.ensureAlive()
|
|
673
|
+
|
|
674
|
+
if (
|
|
675
|
+
this.pendingSinceMs !== null &&
|
|
676
|
+
(nowMsValue < this.pendingSinceMs || nowMsValue - this.pendingSinceMs < this.timeoutMs)
|
|
677
|
+
) {
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
this.tryForceFlush()
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Sets forceFlush when there are pending bytes outside of a paste.
|
|
685
|
+
// Extracted so the setTimeout callback in reconcileTimeoutState() can
|
|
686
|
+
// bypass flushTimeout()'s elapsed-time comparison. Timer scheduling and
|
|
687
|
+
// clock.now() sampling can disagree by a small amount; re-checking elapsed
|
|
688
|
+
// time in the callback can skip a flush and leave pending bytes stuck.
|
|
689
|
+
private tryForceFlush(): void {
|
|
690
|
+
if (this.paste || this.pendingSinceMs === null || this.pending.length === 0) {
|
|
691
|
+
return
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
this.forceFlush = true
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
public reset(): void {
|
|
698
|
+
if (this.destroyed) {
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
this.clearTimeout()
|
|
703
|
+
this.resetState()
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
public resetMouseState(): void {
|
|
707
|
+
this.ensureAlive()
|
|
708
|
+
this.mouseParser.reset()
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
public destroy(): void {
|
|
712
|
+
if (this.destroyed) {
|
|
713
|
+
return
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
this.clearTimeout()
|
|
717
|
+
this.destroyed = true
|
|
718
|
+
this.resetState()
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private ensureAlive(): void {
|
|
722
|
+
if (this.destroyed) {
|
|
723
|
+
throw new Error("StdinParser has been destroyed")
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Scans the pending byte buffer one byte at a time, dispatching on the
|
|
728
|
+
// current parser state. All protocol framing lives in this single switch
|
|
729
|
+
// — intentionally not split into per-mode scan helpers.
|
|
730
|
+
//
|
|
731
|
+
// Exits when: all bytes consumed (ground), more bytes needed (incomplete
|
|
732
|
+
// unit), or paste mode entered (body handled by consumePasteBytes).
|
|
733
|
+
private scanPending(): void {
|
|
734
|
+
while (!this.paste) {
|
|
735
|
+
const bytes = this.pending.view()
|
|
736
|
+
if (this.state.tag === "ground" && this.cursor >= bytes.length) {
|
|
737
|
+
this.pending.clear()
|
|
738
|
+
this.cursor = 0
|
|
739
|
+
this.unitStart = 0
|
|
740
|
+
this.pendingSinceMs = null
|
|
741
|
+
this.forceFlush = false
|
|
742
|
+
return
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const byte = this.cursor < bytes.length ? bytes[this.cursor]! : -1
|
|
746
|
+
switch (this.state.tag) {
|
|
747
|
+
case "ground": {
|
|
748
|
+
this.unitStart = this.cursor
|
|
749
|
+
|
|
750
|
+
// After a timeout-flushed lone ESC, a following `[` may be the start
|
|
751
|
+
// of a delayed `[<...M/m` mouse continuation. Recover only this narrow
|
|
752
|
+
// case; otherwise clear the recovery flag and parse bytes normally.
|
|
753
|
+
if (this.justFlushedEsc) {
|
|
754
|
+
if (byte === 0x5b) {
|
|
755
|
+
this.justFlushedEsc = false
|
|
756
|
+
this.cursor += 1
|
|
757
|
+
this.state = { tag: "esc_recovery" }
|
|
758
|
+
continue
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
this.justFlushedEsc = false
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (byte === ESC) {
|
|
765
|
+
this.cursor += 1
|
|
766
|
+
this.state = { tag: "esc" }
|
|
767
|
+
continue
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (byte < 0x80) {
|
|
771
|
+
this.emitKeyOrResponse("unknown", decodeUtf8(bytes.subarray(this.cursor, this.cursor + 1)))
|
|
772
|
+
this.consumePrefix(this.cursor + 1)
|
|
773
|
+
continue
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Invalid UTF-8 lead byte. Could be a legacy high-byte from an
|
|
777
|
+
// older terminal. If it's the last byte in the buffer, wait for
|
|
778
|
+
// more data or a timeout before committing. On timeout, emit
|
|
779
|
+
// through parseKeypress() which handles meta-key behavior.
|
|
780
|
+
const expected = utf8SequenceLength(byte)
|
|
781
|
+
if (expected === 0) {
|
|
782
|
+
if (!this.forceFlush && this.cursor + 1 === bytes.length) {
|
|
783
|
+
this.markPending()
|
|
784
|
+
return
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
this.emitLegacyHighByte(byte)
|
|
788
|
+
this.consumePrefix(this.cursor + 1)
|
|
789
|
+
continue
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
this.cursor += 1
|
|
793
|
+
this.state = { tag: "utf8", expected, seen: 1 }
|
|
794
|
+
continue
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
case "utf8": {
|
|
798
|
+
if (this.cursor >= bytes.length) {
|
|
799
|
+
if (!this.forceFlush) {
|
|
800
|
+
this.markPending()
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
this.emitLegacyHighByte(bytes[this.unitStart]!)
|
|
805
|
+
this.state = { tag: "ground" }
|
|
806
|
+
this.consumePrefix(this.unitStart + 1)
|
|
807
|
+
continue
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Not a valid continuation byte. Treat the lead byte as a legacy
|
|
811
|
+
// high-byte character and restart parsing from this position.
|
|
812
|
+
if ((byte & 0xc0) !== 0x80) {
|
|
813
|
+
this.emitLegacyHighByte(bytes[this.unitStart]!)
|
|
814
|
+
this.state = { tag: "ground" }
|
|
815
|
+
this.consumePrefix(this.unitStart + 1)
|
|
816
|
+
continue
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const nextSeen = this.state.seen + 1
|
|
820
|
+
this.cursor += 1
|
|
821
|
+
if (nextSeen < this.state.expected) {
|
|
822
|
+
this.state = { tag: "utf8", expected: this.state.expected, seen: nextSeen }
|
|
823
|
+
continue
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
this.emitKeyOrResponse("unknown", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))
|
|
827
|
+
this.state = { tag: "ground" }
|
|
828
|
+
this.consumePrefix(this.cursor)
|
|
829
|
+
continue
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
case "esc": {
|
|
833
|
+
if (this.cursor >= bytes.length) {
|
|
834
|
+
if (!this.forceFlush) {
|
|
835
|
+
this.markPending()
|
|
836
|
+
return
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const flushedLoneEsc = this.cursor === this.unitStart + 1 && bytes[this.unitStart] === ESC
|
|
840
|
+
this.emitKeyOrResponse("unknown", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))
|
|
841
|
+
this.justFlushedEsc = flushedLoneEsc
|
|
842
|
+
this.state = { tag: "ground" }
|
|
843
|
+
this.consumePrefix(this.cursor)
|
|
844
|
+
continue
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// The byte after ESC determines the sub-protocol:
|
|
848
|
+
// [ -> CSI, O -> SS3, ] -> OSC, P -> DCS, _ -> APC.
|
|
849
|
+
switch (byte) {
|
|
850
|
+
case 0x5b:
|
|
851
|
+
this.cursor += 1
|
|
852
|
+
this.state = { tag: "csi" }
|
|
853
|
+
continue
|
|
854
|
+
case 0x4f:
|
|
855
|
+
this.cursor += 1
|
|
856
|
+
this.state = { tag: "ss3" }
|
|
857
|
+
continue
|
|
858
|
+
case 0x5d:
|
|
859
|
+
this.cursor += 1
|
|
860
|
+
this.state = { tag: "osc", sawEsc: false }
|
|
861
|
+
continue
|
|
862
|
+
case 0x50:
|
|
863
|
+
this.cursor += 1
|
|
864
|
+
this.state = { tag: "dcs", sawEsc: false }
|
|
865
|
+
continue
|
|
866
|
+
case 0x5f:
|
|
867
|
+
this.cursor += 1
|
|
868
|
+
this.state = { tag: "apc", sawEsc: false }
|
|
869
|
+
continue
|
|
870
|
+
// ESC ESC: stay in esc state. Terminals encode Alt+ESC and
|
|
871
|
+
// similar sequences as ESC ESC [...], so we keep scanning.
|
|
872
|
+
case ESC:
|
|
873
|
+
this.cursor += 1
|
|
874
|
+
continue
|
|
875
|
+
default:
|
|
876
|
+
this.cursor += 1
|
|
877
|
+
this.emitKeyOrResponse("unknown", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))
|
|
878
|
+
this.state = { tag: "ground" }
|
|
879
|
+
this.consumePrefix(this.cursor)
|
|
880
|
+
continue
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
case "ss3": {
|
|
885
|
+
if (this.cursor >= bytes.length) {
|
|
886
|
+
if (!this.forceFlush) {
|
|
887
|
+
this.markPending()
|
|
888
|
+
return
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
892
|
+
this.state = { tag: "ground" }
|
|
893
|
+
this.consumePrefix(this.cursor)
|
|
894
|
+
continue
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (byte === ESC) {
|
|
898
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
899
|
+
this.state = { tag: "ground" }
|
|
900
|
+
this.consumePrefix(this.cursor)
|
|
901
|
+
continue
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
this.cursor += 1
|
|
905
|
+
this.emitKeyOrResponse("unknown", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))
|
|
906
|
+
this.state = { tag: "ground" }
|
|
907
|
+
this.consumePrefix(this.cursor)
|
|
908
|
+
continue
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Narrow recovery path for delayed mouse continuations after a
|
|
912
|
+
// timeout-flushed lone ESC. Wait for either `<` (SGR) or `M` (X10); if
|
|
913
|
+
// neither arrives, flush `[` as a normal key.
|
|
914
|
+
case "esc_recovery": {
|
|
915
|
+
if (this.cursor >= bytes.length) {
|
|
916
|
+
if (!this.forceFlush) {
|
|
917
|
+
this.markPending()
|
|
918
|
+
return
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
this.emitKeyOrResponse("unknown", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))
|
|
922
|
+
this.state = { tag: "ground" }
|
|
923
|
+
this.consumePrefix(this.cursor)
|
|
924
|
+
continue
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (byte === 0x3c) {
|
|
928
|
+
this.cursor += 1
|
|
929
|
+
this.state = { tag: "esc_less_mouse" }
|
|
930
|
+
continue
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (byte === 0x4d) {
|
|
934
|
+
this.cursor += 1
|
|
935
|
+
this.state = { tag: "esc_less_x10_mouse" }
|
|
936
|
+
continue
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
this.emitKeyOrResponse("unknown", decodeUtf8(bytes.subarray(this.unitStart, this.unitStart + 1)))
|
|
940
|
+
this.state = { tag: "ground" }
|
|
941
|
+
this.consumePrefix(this.unitStart + 1)
|
|
942
|
+
continue
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
case "csi": {
|
|
946
|
+
if (this.cursor >= bytes.length) {
|
|
947
|
+
if (!this.forceFlush) {
|
|
948
|
+
this.markPending()
|
|
949
|
+
return
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
953
|
+
this.state = { tag: "ground" }
|
|
954
|
+
this.consumePrefix(this.cursor)
|
|
955
|
+
continue
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// A new ESC inside an incomplete CSI means the previous sequence
|
|
959
|
+
// was interrupted. Flush everything before the new ESC as one
|
|
960
|
+
// opaque response, then restart parsing at the new ESC.
|
|
961
|
+
if (byte === ESC) {
|
|
962
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
963
|
+
this.state = { tag: "ground" }
|
|
964
|
+
this.consumePrefix(this.cursor)
|
|
965
|
+
continue
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// X10 mouse: ESC [ M plus 3 raw payload bytes (button, x, y).
|
|
969
|
+
// cursor === unitStart + 2 confirms M comes right after ESC[,
|
|
970
|
+
// not as a later final byte in a different CSI sequence.
|
|
971
|
+
if (byte === 0x4d && this.cursor === this.unitStart + 2) {
|
|
972
|
+
const end = this.cursor + 4
|
|
973
|
+
if (bytes.length < end) {
|
|
974
|
+
if (!this.forceFlush) {
|
|
975
|
+
this.markPending()
|
|
976
|
+
return
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, bytes.length))
|
|
980
|
+
this.state = { tag: "ground" }
|
|
981
|
+
this.consumePrefix(bytes.length)
|
|
982
|
+
continue
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
this.emitMouse(bytes.subarray(this.unitStart, end), "x10")
|
|
986
|
+
this.state = { tag: "ground" }
|
|
987
|
+
this.consumePrefix(end)
|
|
988
|
+
continue
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (byte === 0x24) {
|
|
992
|
+
const candidateEnd = this.cursor + 1
|
|
993
|
+
const candidate = decodeUtf8(bytes.subarray(this.unitStart, candidateEnd))
|
|
994
|
+
if (RXVT_DOLLAR_CSI_RE.test(candidate)) {
|
|
995
|
+
this.emitKeyOrResponse("csi", candidate)
|
|
996
|
+
this.state = { tag: "ground" }
|
|
997
|
+
this.consumePrefix(candidateEnd)
|
|
998
|
+
continue
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (!this.forceFlush && candidateEnd >= bytes.length) {
|
|
1002
|
+
this.markPending()
|
|
1003
|
+
return
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (byte === 0x3c && this.cursor === this.unitStart + 2) {
|
|
1008
|
+
this.cursor += 1
|
|
1009
|
+
this.state = { tag: "csi_sgr_mouse", part: 0, hasDigit: false }
|
|
1010
|
+
continue
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Some terminals use ESC [[A..E / ESC [[5~ / ESC [[6~ variants.
|
|
1014
|
+
// Treat the second `[` immediately after ESC[ as part of the CSI
|
|
1015
|
+
// payload instead of as a final byte so parseKeypress() can match
|
|
1016
|
+
// `[[A`, `[[B`, `[[5~`, etc.
|
|
1017
|
+
if (byte === 0x5b && this.cursor === this.unitStart + 2) {
|
|
1018
|
+
this.cursor += 1
|
|
1019
|
+
continue
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (byte === 0x3f && this.cursor === this.unitStart + 2) {
|
|
1023
|
+
this.cursor += 1
|
|
1024
|
+
this.state = { tag: "csi_private_reply", semicolons: 0, hasDigit: false, sawDollar: false }
|
|
1025
|
+
continue
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (byte === 0x3b) {
|
|
1029
|
+
const firstParamStart = this.unitStart + 2
|
|
1030
|
+
const firstParamEnd = this.cursor
|
|
1031
|
+
let firstParamValue = parsePositiveDecimalPrefix(bytes, firstParamStart, firstParamEnd)
|
|
1032
|
+
|
|
1033
|
+
if (firstParamValue === null && this.protocolContext.kittyKeyboardEnabled) {
|
|
1034
|
+
firstParamValue = parseKittyFirstFieldCodepoint(bytes, firstParamStart, firstParamEnd)
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (firstParamValue !== null) {
|
|
1038
|
+
this.cursor += 1
|
|
1039
|
+
this.state = {
|
|
1040
|
+
tag: "csi_parametric",
|
|
1041
|
+
semicolons: 1,
|
|
1042
|
+
segments: 1,
|
|
1043
|
+
hasDigit: false,
|
|
1044
|
+
firstParamValue,
|
|
1045
|
+
}
|
|
1046
|
+
continue
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Standard CSI final byte (0x40–0x7E). Check for bracketed paste
|
|
1051
|
+
// start, SGR mouse, or a regular CSI key/response.
|
|
1052
|
+
if (byte >= 0x40 && byte <= 0x7e) {
|
|
1053
|
+
const end = this.cursor + 1
|
|
1054
|
+
const rawBytes = bytes.subarray(this.unitStart, end)
|
|
1055
|
+
|
|
1056
|
+
if (bytesEqual(rawBytes, BRACKETED_PASTE_START)) {
|
|
1057
|
+
this.state = { tag: "ground" }
|
|
1058
|
+
this.consumePrefix(end)
|
|
1059
|
+
this.paste = createPasteCollector()
|
|
1060
|
+
continue
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (isMouseSgrSequence(rawBytes)) {
|
|
1064
|
+
this.emitMouse(rawBytes, "sgr")
|
|
1065
|
+
this.state = { tag: "ground" }
|
|
1066
|
+
this.consumePrefix(end)
|
|
1067
|
+
continue
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
this.emitKeyOrResponse("csi", decodeUtf8(rawBytes))
|
|
1071
|
+
this.state = { tag: "ground" }
|
|
1072
|
+
this.consumePrefix(end)
|
|
1073
|
+
continue
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
this.cursor += 1
|
|
1077
|
+
continue
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
case "csi_sgr_mouse": {
|
|
1081
|
+
if (this.cursor >= bytes.length) {
|
|
1082
|
+
if (!this.forceFlush) {
|
|
1083
|
+
this.markPending()
|
|
1084
|
+
return
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
this.state = { tag: "csi_sgr_mouse_deferred", part: this.state.part, hasDigit: this.state.hasDigit }
|
|
1088
|
+
this.pendingSinceMs = null
|
|
1089
|
+
this.forceFlush = false
|
|
1090
|
+
return
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (byte === ESC) {
|
|
1094
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1095
|
+
this.state = { tag: "ground" }
|
|
1096
|
+
this.consumePrefix(this.cursor)
|
|
1097
|
+
continue
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (isAsciiDigit(byte)) {
|
|
1101
|
+
this.cursor += 1
|
|
1102
|
+
this.state = { tag: "csi_sgr_mouse", part: this.state.part, hasDigit: true }
|
|
1103
|
+
continue
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (byte === 0x3b && this.state.hasDigit && this.state.part < 2) {
|
|
1107
|
+
this.cursor += 1
|
|
1108
|
+
this.state = { tag: "csi_sgr_mouse", part: this.state.part + 1, hasDigit: false }
|
|
1109
|
+
continue
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (byte >= 0x40 && byte <= 0x7e) {
|
|
1113
|
+
const end = this.cursor + 1
|
|
1114
|
+
const rawBytes = bytes.subarray(this.unitStart, end)
|
|
1115
|
+
if (isMouseSgrSequence(rawBytes)) {
|
|
1116
|
+
this.emitMouse(rawBytes, "sgr")
|
|
1117
|
+
} else {
|
|
1118
|
+
this.emitKeyOrResponse("csi", decodeUtf8(rawBytes))
|
|
1119
|
+
}
|
|
1120
|
+
this.state = { tag: "ground" }
|
|
1121
|
+
this.consumePrefix(end)
|
|
1122
|
+
continue
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
this.state = { tag: "csi" }
|
|
1126
|
+
continue
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
case "csi_sgr_mouse_deferred": {
|
|
1130
|
+
if (this.cursor >= bytes.length) {
|
|
1131
|
+
this.pendingSinceMs = null
|
|
1132
|
+
this.forceFlush = false
|
|
1133
|
+
return
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (byte === ESC) {
|
|
1137
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1138
|
+
this.state = { tag: "ground" }
|
|
1139
|
+
this.consumePrefix(this.cursor)
|
|
1140
|
+
continue
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (isAsciiDigit(byte) || byte === 0x3b || byte === 0x4d || byte === 0x6d) {
|
|
1144
|
+
this.state = { tag: "csi_sgr_mouse", part: this.state.part, hasDigit: this.state.hasDigit }
|
|
1145
|
+
continue
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1149
|
+
this.state = { tag: "ground" }
|
|
1150
|
+
this.consumePrefix(this.cursor)
|
|
1151
|
+
continue
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
case "csi_parametric": {
|
|
1155
|
+
if (this.cursor >= bytes.length) {
|
|
1156
|
+
if (!this.forceFlush) {
|
|
1157
|
+
this.markPending()
|
|
1158
|
+
return
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (canDeferParametricCsi(this.state, this.protocolContext)) {
|
|
1162
|
+
this.state = {
|
|
1163
|
+
tag: "csi_parametric_deferred",
|
|
1164
|
+
semicolons: this.state.semicolons,
|
|
1165
|
+
segments: this.state.segments,
|
|
1166
|
+
hasDigit: this.state.hasDigit,
|
|
1167
|
+
firstParamValue: this.state.firstParamValue,
|
|
1168
|
+
}
|
|
1169
|
+
this.pendingSinceMs = null
|
|
1170
|
+
this.forceFlush = false
|
|
1171
|
+
return
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1175
|
+
this.state = { tag: "ground" }
|
|
1176
|
+
this.consumePrefix(this.cursor)
|
|
1177
|
+
continue
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (byte === ESC) {
|
|
1181
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1182
|
+
this.state = { tag: "ground" }
|
|
1183
|
+
this.consumePrefix(this.cursor)
|
|
1184
|
+
continue
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (isAsciiDigit(byte)) {
|
|
1188
|
+
this.cursor += 1
|
|
1189
|
+
this.state = {
|
|
1190
|
+
tag: "csi_parametric",
|
|
1191
|
+
semicolons: this.state.semicolons,
|
|
1192
|
+
segments: this.state.segments,
|
|
1193
|
+
hasDigit: true,
|
|
1194
|
+
firstParamValue: this.state.firstParamValue,
|
|
1195
|
+
}
|
|
1196
|
+
continue
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
if (byte === 0x3a && this.state.hasDigit && this.state.segments < 3) {
|
|
1200
|
+
this.cursor += 1
|
|
1201
|
+
this.state = {
|
|
1202
|
+
tag: "csi_parametric",
|
|
1203
|
+
semicolons: this.state.semicolons,
|
|
1204
|
+
segments: this.state.segments + 1,
|
|
1205
|
+
hasDigit: false,
|
|
1206
|
+
firstParamValue: this.state.firstParamValue,
|
|
1207
|
+
}
|
|
1208
|
+
continue
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (byte === 0x3b && this.state.semicolons < 2) {
|
|
1212
|
+
this.cursor += 1
|
|
1213
|
+
this.state = {
|
|
1214
|
+
tag: "csi_parametric",
|
|
1215
|
+
semicolons: this.state.semicolons + 1,
|
|
1216
|
+
segments: 1,
|
|
1217
|
+
hasDigit: false,
|
|
1218
|
+
firstParamValue: this.state.firstParamValue,
|
|
1219
|
+
}
|
|
1220
|
+
continue
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (byte >= 0x40 && byte <= 0x7e) {
|
|
1224
|
+
const end = this.cursor + 1
|
|
1225
|
+
this.emitKeyOrResponse("csi", decodeUtf8(bytes.subarray(this.unitStart, end)))
|
|
1226
|
+
this.state = { tag: "ground" }
|
|
1227
|
+
this.consumePrefix(end)
|
|
1228
|
+
continue
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
this.state = { tag: "csi" }
|
|
1232
|
+
continue
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
case "csi_parametric_deferred": {
|
|
1236
|
+
if (this.cursor >= bytes.length) {
|
|
1237
|
+
this.pendingSinceMs = null
|
|
1238
|
+
this.forceFlush = false
|
|
1239
|
+
return
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
if (byte === ESC) {
|
|
1243
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1244
|
+
this.state = { tag: "ground" }
|
|
1245
|
+
this.consumePrefix(this.cursor)
|
|
1246
|
+
continue
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (isAsciiDigit(byte) || byte === 0x3a || byte === 0x3b) {
|
|
1250
|
+
this.state = {
|
|
1251
|
+
tag: "csi_parametric",
|
|
1252
|
+
semicolons: this.state.semicolons,
|
|
1253
|
+
segments: this.state.segments,
|
|
1254
|
+
hasDigit: this.state.hasDigit,
|
|
1255
|
+
firstParamValue: this.state.firstParamValue,
|
|
1256
|
+
}
|
|
1257
|
+
continue
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (canCompleteDeferredParametricCsi(this.state, byte, this.protocolContext)) {
|
|
1261
|
+
this.state = {
|
|
1262
|
+
tag: "csi_parametric",
|
|
1263
|
+
semicolons: this.state.semicolons,
|
|
1264
|
+
segments: this.state.segments,
|
|
1265
|
+
hasDigit: this.state.hasDigit,
|
|
1266
|
+
firstParamValue: this.state.firstParamValue,
|
|
1267
|
+
}
|
|
1268
|
+
continue
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1272
|
+
this.state = { tag: "ground" }
|
|
1273
|
+
this.consumePrefix(this.cursor)
|
|
1274
|
+
continue
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
case "csi_private_reply": {
|
|
1278
|
+
if (this.cursor >= bytes.length) {
|
|
1279
|
+
if (!this.forceFlush) {
|
|
1280
|
+
this.markPending()
|
|
1281
|
+
return
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
if (canDeferPrivateReplyCsi(this.protocolContext)) {
|
|
1285
|
+
this.state = {
|
|
1286
|
+
tag: "csi_private_reply_deferred",
|
|
1287
|
+
semicolons: this.state.semicolons,
|
|
1288
|
+
hasDigit: this.state.hasDigit,
|
|
1289
|
+
sawDollar: this.state.sawDollar,
|
|
1290
|
+
}
|
|
1291
|
+
this.pendingSinceMs = null
|
|
1292
|
+
this.forceFlush = false
|
|
1293
|
+
return
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1297
|
+
this.state = { tag: "ground" }
|
|
1298
|
+
this.consumePrefix(this.cursor)
|
|
1299
|
+
continue
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (byte === ESC) {
|
|
1303
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1304
|
+
this.state = { tag: "ground" }
|
|
1305
|
+
this.consumePrefix(this.cursor)
|
|
1306
|
+
continue
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if (isAsciiDigit(byte)) {
|
|
1310
|
+
this.cursor += 1
|
|
1311
|
+
this.state = {
|
|
1312
|
+
tag: "csi_private_reply",
|
|
1313
|
+
semicolons: this.state.semicolons,
|
|
1314
|
+
hasDigit: true,
|
|
1315
|
+
sawDollar: this.state.sawDollar,
|
|
1316
|
+
}
|
|
1317
|
+
continue
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (byte === 0x3b) {
|
|
1321
|
+
this.cursor += 1
|
|
1322
|
+
this.state = {
|
|
1323
|
+
tag: "csi_private_reply",
|
|
1324
|
+
semicolons: this.state.semicolons + 1,
|
|
1325
|
+
hasDigit: false,
|
|
1326
|
+
sawDollar: false,
|
|
1327
|
+
}
|
|
1328
|
+
continue
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (byte === 0x24 && this.state.hasDigit && !this.state.sawDollar) {
|
|
1332
|
+
this.cursor += 1
|
|
1333
|
+
this.state = {
|
|
1334
|
+
tag: "csi_private_reply",
|
|
1335
|
+
semicolons: this.state.semicolons,
|
|
1336
|
+
hasDigit: true,
|
|
1337
|
+
sawDollar: true,
|
|
1338
|
+
}
|
|
1339
|
+
continue
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (byte >= 0x40 && byte <= 0x7e) {
|
|
1343
|
+
const end = this.cursor + 1
|
|
1344
|
+
this.emitKeyOrResponse("csi", decodeUtf8(bytes.subarray(this.unitStart, end)))
|
|
1345
|
+
this.state = { tag: "ground" }
|
|
1346
|
+
this.consumePrefix(end)
|
|
1347
|
+
continue
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
this.state = { tag: "csi" }
|
|
1351
|
+
continue
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
case "csi_private_reply_deferred": {
|
|
1355
|
+
if (this.cursor >= bytes.length) {
|
|
1356
|
+
this.pendingSinceMs = null
|
|
1357
|
+
this.forceFlush = false
|
|
1358
|
+
return
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (byte === ESC) {
|
|
1362
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1363
|
+
this.state = { tag: "ground" }
|
|
1364
|
+
this.consumePrefix(this.cursor)
|
|
1365
|
+
continue
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (isAsciiDigit(byte) || byte === 0x3b || byte === 0x24) {
|
|
1369
|
+
this.state = {
|
|
1370
|
+
tag: "csi_private_reply",
|
|
1371
|
+
semicolons: this.state.semicolons,
|
|
1372
|
+
hasDigit: this.state.hasDigit,
|
|
1373
|
+
sawDollar: this.state.sawDollar,
|
|
1374
|
+
}
|
|
1375
|
+
continue
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (canCompleteDeferredPrivateReplyCsi(this.state, byte, this.protocolContext)) {
|
|
1379
|
+
this.state = {
|
|
1380
|
+
tag: "csi_private_reply",
|
|
1381
|
+
semicolons: this.state.semicolons,
|
|
1382
|
+
hasDigit: this.state.hasDigit,
|
|
1383
|
+
sawDollar: this.state.sawDollar,
|
|
1384
|
+
}
|
|
1385
|
+
continue
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1389
|
+
this.state = { tag: "ground" }
|
|
1390
|
+
this.consumePrefix(this.cursor)
|
|
1391
|
+
continue
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// OSC sequences end at BEL or ESC \. DCS and APC end at ESC \
|
|
1395
|
+
// only. The sawEsc flag tracks whether the previous byte was ESC,
|
|
1396
|
+
// since the two-byte ESC \ can split across push() calls.
|
|
1397
|
+
case "osc": {
|
|
1398
|
+
if (this.cursor >= bytes.length) {
|
|
1399
|
+
if (!this.forceFlush) {
|
|
1400
|
+
this.markPending()
|
|
1401
|
+
return
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1405
|
+
this.state = { tag: "ground" }
|
|
1406
|
+
this.consumePrefix(this.cursor)
|
|
1407
|
+
continue
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
if (this.state.sawEsc) {
|
|
1411
|
+
if (byte === 0x5c) {
|
|
1412
|
+
const end = this.cursor + 1
|
|
1413
|
+
this.emitOpaqueResponse("osc", bytes.subarray(this.unitStart, end))
|
|
1414
|
+
this.state = { tag: "ground" }
|
|
1415
|
+
this.consumePrefix(end)
|
|
1416
|
+
continue
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
this.state = { tag: "osc", sawEsc: false }
|
|
1420
|
+
continue
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if (byte === BEL) {
|
|
1424
|
+
const end = this.cursor + 1
|
|
1425
|
+
this.emitOpaqueResponse("osc", bytes.subarray(this.unitStart, end))
|
|
1426
|
+
this.state = { tag: "ground" }
|
|
1427
|
+
this.consumePrefix(end)
|
|
1428
|
+
continue
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
if (byte === ESC) {
|
|
1432
|
+
this.cursor += 1
|
|
1433
|
+
this.state = { tag: "osc", sawEsc: true }
|
|
1434
|
+
continue
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
this.cursor += 1
|
|
1438
|
+
continue
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
case "dcs": {
|
|
1442
|
+
if (this.cursor >= bytes.length) {
|
|
1443
|
+
if (!this.forceFlush) {
|
|
1444
|
+
this.markPending()
|
|
1445
|
+
return
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1449
|
+
this.state = { tag: "ground" }
|
|
1450
|
+
this.consumePrefix(this.cursor)
|
|
1451
|
+
continue
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (this.state.sawEsc) {
|
|
1455
|
+
if (byte === 0x5c) {
|
|
1456
|
+
const end = this.cursor + 1
|
|
1457
|
+
this.emitOpaqueResponse("dcs", bytes.subarray(this.unitStart, end))
|
|
1458
|
+
this.state = { tag: "ground" }
|
|
1459
|
+
this.consumePrefix(end)
|
|
1460
|
+
continue
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
this.state = { tag: "dcs", sawEsc: false }
|
|
1464
|
+
continue
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (byte === ESC) {
|
|
1468
|
+
this.cursor += 1
|
|
1469
|
+
this.state = { tag: "dcs", sawEsc: true }
|
|
1470
|
+
continue
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
this.cursor += 1
|
|
1474
|
+
continue
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
case "apc": {
|
|
1478
|
+
if (this.cursor >= bytes.length) {
|
|
1479
|
+
if (!this.forceFlush) {
|
|
1480
|
+
this.markPending()
|
|
1481
|
+
return
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1485
|
+
this.state = { tag: "ground" }
|
|
1486
|
+
this.consumePrefix(this.cursor)
|
|
1487
|
+
continue
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (this.state.sawEsc) {
|
|
1491
|
+
if (byte === 0x5c) {
|
|
1492
|
+
const end = this.cursor + 1
|
|
1493
|
+
this.emitOpaqueResponse("apc", bytes.subarray(this.unitStart, end))
|
|
1494
|
+
this.state = { tag: "ground" }
|
|
1495
|
+
this.consumePrefix(end)
|
|
1496
|
+
continue
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
this.state = { tag: "apc", sawEsc: false }
|
|
1500
|
+
continue
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
if (byte === ESC) {
|
|
1504
|
+
this.cursor += 1
|
|
1505
|
+
this.state = { tag: "apc", sawEsc: true }
|
|
1506
|
+
continue
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
this.cursor += 1
|
|
1510
|
+
continue
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// Delayed SGR mouse continuation after `esc_recovery` has consumed the
|
|
1514
|
+
// leading `[`. Consume the rest of `<digits;digits;digitsM/m` as one
|
|
1515
|
+
// opaque response so split mouse bytes never leak into text.
|
|
1516
|
+
case "esc_less_mouse": {
|
|
1517
|
+
if (this.cursor >= bytes.length) {
|
|
1518
|
+
if (!this.forceFlush) {
|
|
1519
|
+
this.markPending()
|
|
1520
|
+
return
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1524
|
+
this.state = { tag: "ground" }
|
|
1525
|
+
this.consumePrefix(this.cursor)
|
|
1526
|
+
continue
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if ((byte >= 0x30 && byte <= 0x39) || byte === 0x3b) {
|
|
1530
|
+
this.cursor += 1
|
|
1531
|
+
continue
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
if (byte === 0x4d || byte === 0x6d) {
|
|
1535
|
+
const end = this.cursor + 1
|
|
1536
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, end))
|
|
1537
|
+
this.state = { tag: "ground" }
|
|
1538
|
+
this.consumePrefix(end)
|
|
1539
|
+
continue
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
|
|
1543
|
+
this.state = { tag: "ground" }
|
|
1544
|
+
this.consumePrefix(this.cursor)
|
|
1545
|
+
continue
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Delayed X10 mouse continuation after `esc_recovery` has consumed the
|
|
1549
|
+
// leading `[`. Consume `[M` plus its three raw payload bytes as one
|
|
1550
|
+
// opaque response so split mouse bytes never leak into text.
|
|
1551
|
+
case "esc_less_x10_mouse": {
|
|
1552
|
+
const end = this.unitStart + 5
|
|
1553
|
+
|
|
1554
|
+
if (bytes.length < end) {
|
|
1555
|
+
if (!this.forceFlush) {
|
|
1556
|
+
this.markPending()
|
|
1557
|
+
return
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, bytes.length))
|
|
1561
|
+
this.state = { tag: "ground" }
|
|
1562
|
+
this.consumePrefix(bytes.length)
|
|
1563
|
+
continue
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, end))
|
|
1567
|
+
this.state = { tag: "ground" }
|
|
1568
|
+
this.consumePrefix(end)
|
|
1569
|
+
continue
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Tries to parse the raw string as a key via parseKeypress(). If it
|
|
1576
|
+
// recognizes the sequence (printable char, arrow, function key, etc.),
|
|
1577
|
+
// emits a key event. Otherwise emits a response event — this is how
|
|
1578
|
+
// capability responses, focus sequences, and other non-key CSI traffic
|
|
1579
|
+
// avoids becoming text.
|
|
1580
|
+
private emitKeyOrResponse(protocol: StdinResponseProtocol, raw: string): void {
|
|
1581
|
+
const parsed = parseKeypress(raw, { useKittyKeyboard: this.useKittyKeyboard })
|
|
1582
|
+
if (parsed) {
|
|
1583
|
+
this.events.push({
|
|
1584
|
+
type: "key",
|
|
1585
|
+
raw: parsed.raw,
|
|
1586
|
+
key: parsed,
|
|
1587
|
+
})
|
|
1588
|
+
return
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
this.events.push({
|
|
1592
|
+
type: "response",
|
|
1593
|
+
protocol,
|
|
1594
|
+
sequence: raw,
|
|
1595
|
+
})
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
private emitMouse(rawBytes: Uint8Array, encoding: "sgr" | "x10"): void {
|
|
1599
|
+
const event = this.mouseParser.parseMouseEvent(rawBytes)
|
|
1600
|
+
if (!event) {
|
|
1601
|
+
this.emitOpaqueResponse("unknown", rawBytes)
|
|
1602
|
+
return
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
this.events.push({
|
|
1606
|
+
type: "mouse",
|
|
1607
|
+
raw: decodeLatin1(rawBytes),
|
|
1608
|
+
encoding,
|
|
1609
|
+
event,
|
|
1610
|
+
})
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// Handles single bytes in the 0x80–0xFF range that aren't valid UTF-8
|
|
1614
|
+
// leads. Passes them through parseKeypress() which maps them to the
|
|
1615
|
+
// existing meta-key behavior (e.g. Alt+letter in terminals that send
|
|
1616
|
+
// high bytes instead of ESC-prefixed sequences).
|
|
1617
|
+
private emitLegacyHighByte(byte: number): void {
|
|
1618
|
+
const parsed = parseKeypress(Buffer.from([byte]), { useKittyKeyboard: this.useKittyKeyboard })
|
|
1619
|
+
if (parsed) {
|
|
1620
|
+
this.events.push({
|
|
1621
|
+
type: "key",
|
|
1622
|
+
raw: parsed.raw,
|
|
1623
|
+
key: parsed,
|
|
1624
|
+
})
|
|
1625
|
+
return
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
this.events.push({
|
|
1629
|
+
type: "response",
|
|
1630
|
+
protocol: "unknown",
|
|
1631
|
+
sequence: String.fromCharCode(byte),
|
|
1632
|
+
})
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
private emitOpaqueResponse(protocol: StdinResponseProtocol, rawBytes: Uint8Array): void {
|
|
1636
|
+
this.events.push({
|
|
1637
|
+
type: "response",
|
|
1638
|
+
protocol,
|
|
1639
|
+
sequence: decodeLatin1(rawBytes),
|
|
1640
|
+
})
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// Advances past a completed protocol unit. Resets cursor, unitStart,
|
|
1644
|
+
// and timeout state so the next scan iteration starts clean.
|
|
1645
|
+
private consumePrefix(endExclusive: number): void {
|
|
1646
|
+
this.pending.consume(endExclusive)
|
|
1647
|
+
this.cursor = 0
|
|
1648
|
+
this.unitStart = 0
|
|
1649
|
+
this.pendingSinceMs = null
|
|
1650
|
+
this.forceFlush = false
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Removes all bytes from the pending queue and returns them. Used when
|
|
1654
|
+
// entering paste mode — leftover bytes after the paste start marker
|
|
1655
|
+
// need to flow through consumePasteBytes() instead.
|
|
1656
|
+
private takePendingBytes(): Uint8Array {
|
|
1657
|
+
const buffered = this.pending.take()
|
|
1658
|
+
this.cursor = 0
|
|
1659
|
+
this.unitStart = 0
|
|
1660
|
+
this.pendingSinceMs = null
|
|
1661
|
+
this.forceFlush = false
|
|
1662
|
+
return buffered
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// Emits all pending bytes as one opaque response and clears the buffer.
|
|
1666
|
+
// This keeps the parser buffer bounded at maxPendingBytes without
|
|
1667
|
+
// dropping data or splitting it into per-character events.
|
|
1668
|
+
private flushPendingOverflow(): void {
|
|
1669
|
+
if (this.pending.length === 0) {
|
|
1670
|
+
return
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
this.emitOpaqueResponse("unknown", this.pending.view())
|
|
1674
|
+
this.pending.clear()
|
|
1675
|
+
this.cursor = 0
|
|
1676
|
+
this.unitStart = 0
|
|
1677
|
+
this.pendingSinceMs = null
|
|
1678
|
+
this.forceFlush = false
|
|
1679
|
+
this.state = { tag: "ground" }
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Records when incomplete data first appeared so flushTimeout() can
|
|
1683
|
+
// decide whether enough time has elapsed to force-flush it.
|
|
1684
|
+
private markPending(): void {
|
|
1685
|
+
this.pendingSinceMs = this.clock.now()
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// Processes bytes during an active bracketed paste. Searches for the end
|
|
1689
|
+
// marker (ESC[201~) using a sliding tail window so the marker can split
|
|
1690
|
+
// across chunk boundaries. Bytes that can't be part of the end marker are
|
|
1691
|
+
// appended to the paste collector without decoding.
|
|
1692
|
+
//
|
|
1693
|
+
// Returns any bytes that follow the end marker — those go back through
|
|
1694
|
+
// normal parsing in the push() loop.
|
|
1695
|
+
private consumePasteBytes(chunk: Uint8Array): Uint8Array {
|
|
1696
|
+
const paste = this.paste!
|
|
1697
|
+
const combined = concatBytes(paste.tail, chunk)
|
|
1698
|
+
const endIndex = indexOfBytes(combined, BRACKETED_PASTE_END)
|
|
1699
|
+
|
|
1700
|
+
if (endIndex !== -1) {
|
|
1701
|
+
this.pushPasteBytes(combined.subarray(0, endIndex))
|
|
1702
|
+
|
|
1703
|
+
this.events.push({
|
|
1704
|
+
type: "paste",
|
|
1705
|
+
bytes: joinPasteBytes(paste.parts, paste.totalLength),
|
|
1706
|
+
})
|
|
1707
|
+
|
|
1708
|
+
this.paste = null
|
|
1709
|
+
return combined.subarray(endIndex + BRACKETED_PASTE_END.length)
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// Keep enough trailing bytes to detect an end marker split across chunks.
|
|
1713
|
+
// Everything before that point is safe to retain immediately.
|
|
1714
|
+
const keep = Math.min(BRACKETED_PASTE_END.length - 1, combined.length)
|
|
1715
|
+
const stableLength = combined.length - keep
|
|
1716
|
+
if (stableLength > 0) {
|
|
1717
|
+
this.pushPasteBytes(combined.subarray(0, stableLength))
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
paste.tail = Uint8Array.from(combined.subarray(stableLength))
|
|
1721
|
+
return EMPTY_BYTES
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
private pushPasteBytes(bytes: Uint8Array): void {
|
|
1725
|
+
if (bytes.length === 0) {
|
|
1726
|
+
return
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Copy here because subarray() inputs may alias the caller's chunk or the
|
|
1730
|
+
// parser's pending buffer across pushes. The emitted paste event must keep
|
|
1731
|
+
// the original bytes even if those backing buffers are later reused.
|
|
1732
|
+
this.paste!.parts.push(Uint8Array.from(bytes))
|
|
1733
|
+
this.paste!.totalLength += bytes.length
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
private reconcileDeferredStateWithProtocolContext(): void {
|
|
1737
|
+
switch (this.state.tag) {
|
|
1738
|
+
case "csi_parametric_deferred":
|
|
1739
|
+
if (!canDeferParametricCsi(this.state, this.protocolContext)) {
|
|
1740
|
+
this.emitOpaqueResponse("unknown", this.pending.view().subarray(this.unitStart, this.cursor))
|
|
1741
|
+
this.state = { tag: "ground" }
|
|
1742
|
+
this.consumePrefix(this.cursor)
|
|
1743
|
+
}
|
|
1744
|
+
return
|
|
1745
|
+
|
|
1746
|
+
case "csi_private_reply_deferred":
|
|
1747
|
+
if (!canDeferPrivateReplyCsi(this.protocolContext)) {
|
|
1748
|
+
this.emitOpaqueResponse("unknown", this.pending.view().subarray(this.unitStart, this.cursor))
|
|
1749
|
+
this.state = { tag: "ground" }
|
|
1750
|
+
this.consumePrefix(this.cursor)
|
|
1751
|
+
}
|
|
1752
|
+
return
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// Arms or disarms the timeout after every push(). If there's an incomplete
|
|
1757
|
+
// unit in the buffer, starts a timer. When the timer fires, it sets
|
|
1758
|
+
// forceFlush so the next read() converts the incomplete unit into one
|
|
1759
|
+
// atomic event (e.g. a lone ESC becoming an Escape key).
|
|
1760
|
+
private reconcileTimeoutState(): void {
|
|
1761
|
+
if (!this.armTimeouts) {
|
|
1762
|
+
return
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
if (this.paste || this.pendingSinceMs === null || this.pending.length === 0) {
|
|
1766
|
+
this.clearTimeout()
|
|
1767
|
+
return
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
this.clearTimeout()
|
|
1771
|
+
this.timeoutId = this.clock.setTimeout(() => {
|
|
1772
|
+
this.timeoutId = null
|
|
1773
|
+
if (this.destroyed) {
|
|
1774
|
+
return
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
try {
|
|
1778
|
+
this.tryForceFlush()
|
|
1779
|
+
this.onTimeoutFlush?.()
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
console.error("stdin parser timeout flush failed", error)
|
|
1782
|
+
}
|
|
1783
|
+
}, this.timeoutMs)
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
private clearTimeout(): void {
|
|
1787
|
+
if (!this.timeoutId) {
|
|
1788
|
+
return
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
this.clock.clearTimeout(this.timeoutId)
|
|
1792
|
+
this.timeoutId = null
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// Clears all parser state: pending bytes, queued events, timeout tracking,
|
|
1796
|
+
// and any active paste collector. Called by both reset() (suspend/resume)
|
|
1797
|
+
// and destroy() to ensure no stale state survives.
|
|
1798
|
+
private resetState(): void {
|
|
1799
|
+
this.pending.reset(INITIAL_PENDING_CAPACITY)
|
|
1800
|
+
this.events.length = 0
|
|
1801
|
+
this.pendingSinceMs = null
|
|
1802
|
+
this.forceFlush = false
|
|
1803
|
+
this.justFlushedEsc = false
|
|
1804
|
+
this.state = { tag: "ground" }
|
|
1805
|
+
this.cursor = 0
|
|
1806
|
+
this.unitStart = 0
|
|
1807
|
+
this.paste = null
|
|
1808
|
+
this.mouseParser.reset()
|
|
1809
|
+
}
|
|
1810
|
+
}
|