@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.
Files changed (591) hide show
  1. package/dev/keypress-debug-renderer.ts +148 -0
  2. package/dev/keypress-debug.ts +43 -0
  3. package/dev/print-env-vars.ts +32 -0
  4. package/dev/test-tmux-graphics-334.sh +68 -0
  5. package/dev/thai-debug-test.ts +68 -0
  6. package/docs/development.md +144 -0
  7. package/package.json +62 -53
  8. package/scripts/build.ts +400 -0
  9. package/scripts/publish.ts +60 -0
  10. package/src/3d/SpriteResourceManager.ts +286 -0
  11. package/src/3d/SpriteUtils.ts +70 -0
  12. package/src/3d/TextureUtils.ts +196 -0
  13. package/src/3d/ThreeRenderable.ts +197 -0
  14. package/src/3d/WGPURenderer.ts +294 -0
  15. package/src/3d/animation/ExplodingSpriteEffect.ts +513 -0
  16. package/src/3d/animation/PhysicsExplodingSpriteEffect.ts +429 -0
  17. package/src/3d/animation/SpriteAnimator.ts +633 -0
  18. package/src/3d/animation/SpriteParticleGenerator.ts +435 -0
  19. package/src/3d/canvas.ts +464 -0
  20. package/src/3d/index.ts +12 -0
  21. package/src/3d/physics/PlanckPhysicsAdapter.ts +72 -0
  22. package/src/3d/physics/RapierPhysicsAdapter.ts +66 -0
  23. package/src/3d/physics/physics-interface.ts +31 -0
  24. package/src/3d/shaders/supersampling.wgsl +201 -0
  25. package/src/3d.ts +3 -0
  26. package/src/NativeSpanFeed.ts +300 -0
  27. package/src/Renderable.ts +1704 -0
  28. package/src/__snapshots__/buffer.test.ts.snap +28 -0
  29. package/src/animation/Timeline.test.ts +2709 -0
  30. package/src/animation/Timeline.ts +598 -0
  31. package/src/ansi.ts +18 -0
  32. package/src/benchmark/attenuation-benchmark.ts +81 -0
  33. package/src/benchmark/colormatrix-benchmark.ts +128 -0
  34. package/src/benchmark/gain-benchmark.ts +80 -0
  35. package/src/benchmark/latest-all-bench-run.json +707 -0
  36. package/src/benchmark/latest-async-bench-run.json +336 -0
  37. package/src/benchmark/latest-default-bench-run.json +657 -0
  38. package/src/benchmark/latest-large-bench-run.json +707 -0
  39. package/src/benchmark/latest-quick-bench-run.json +207 -0
  40. package/src/benchmark/markdown-benchmark.ts +1796 -0
  41. package/src/benchmark/native-span-feed-async-benchmark.ts +355 -0
  42. package/src/benchmark/native-span-feed-benchmark.md +56 -0
  43. package/src/benchmark/native-span-feed-benchmark.ts +596 -0
  44. package/src/benchmark/native-span-feed-compare.ts +280 -0
  45. package/src/benchmark/renderer-benchmark.ts +754 -0
  46. package/src/benchmark/text-table-benchmark.ts +948 -0
  47. package/src/buffer.test.ts +291 -0
  48. package/src/buffer.ts +554 -0
  49. package/src/console.test.ts +612 -0
  50. package/src/console.ts +1254 -0
  51. package/src/edit-buffer.test.ts +1769 -0
  52. package/src/edit-buffer.ts +411 -0
  53. package/src/editor-view.test.ts +1032 -0
  54. package/src/editor-view.ts +284 -0
  55. package/src/examples/ascii-font-selection-demo.ts +245 -0
  56. package/src/examples/assets/Water_2_M_Normal.jpg +0 -0
  57. package/src/examples/assets/concrete.png +0 -0
  58. package/src/examples/assets/crate.png +0 -0
  59. package/src/examples/assets/crate_emissive.png +0 -0
  60. package/src/examples/assets/forrest_background.png +0 -0
  61. package/src/examples/assets/hast-example.json +1018 -0
  62. package/src/examples/assets/heart.png +0 -0
  63. package/src/examples/assets/main_char_heavy_attack.png +0 -0
  64. package/src/examples/assets/main_char_idle.png +0 -0
  65. package/src/examples/assets/main_char_jump_end.png +0 -0
  66. package/src/examples/assets/main_char_jump_landing.png +0 -0
  67. package/src/examples/assets/main_char_jump_start.png +0 -0
  68. package/src/examples/assets/main_char_run_loop.png +0 -0
  69. package/src/examples/assets/roughness_map.jpg +0 -0
  70. package/src/examples/build.ts +115 -0
  71. package/src/examples/code-demo.ts +924 -0
  72. package/src/examples/console-demo.ts +358 -0
  73. package/src/examples/core-plugin-slots-demo.ts +759 -0
  74. package/src/examples/diff-demo.ts +701 -0
  75. package/src/examples/draggable-three-demo.ts +259 -0
  76. package/src/examples/editor-demo.ts +322 -0
  77. package/src/examples/extmarks-demo.ts +196 -0
  78. package/src/examples/focus-restore-demo.ts +310 -0
  79. package/src/examples/fonts.ts +245 -0
  80. package/src/examples/fractal-shader-demo.ts +268 -0
  81. package/src/examples/framebuffer-demo.ts +674 -0
  82. package/src/examples/full-unicode-demo.ts +241 -0
  83. package/src/examples/golden-star-demo.ts +933 -0
  84. package/src/examples/grayscale-buffer-demo.ts +249 -0
  85. package/src/examples/hast-syntax-highlighting-demo.ts +129 -0
  86. package/src/examples/index.ts +926 -0
  87. package/src/examples/input-demo.ts +377 -0
  88. package/src/examples/input-select-layout-demo.ts +425 -0
  89. package/src/examples/install.sh +143 -0
  90. package/src/examples/keypress-debug-demo.ts +452 -0
  91. package/src/examples/lib/HexList.ts +122 -0
  92. package/src/examples/lib/PaletteGrid.ts +125 -0
  93. package/src/examples/lib/standalone-keys.ts +25 -0
  94. package/src/examples/lib/tab-controller.ts +243 -0
  95. package/src/examples/lights-phong-demo.ts +290 -0
  96. package/src/examples/link-demo.ts +220 -0
  97. package/src/examples/live-state-demo.ts +480 -0
  98. package/src/examples/markdown-demo.ts +725 -0
  99. package/src/examples/mouse-interaction-demo.ts +428 -0
  100. package/src/examples/nested-zindex-demo.ts +357 -0
  101. package/src/examples/opacity-example.ts +235 -0
  102. package/src/examples/opentui-demo.ts +1057 -0
  103. package/src/examples/physx-planck-2d-demo.ts +623 -0
  104. package/src/examples/physx-rapier-2d-demo.ts +655 -0
  105. package/src/examples/relative-positioning-demo.ts +323 -0
  106. package/src/examples/scroll-example.ts +214 -0
  107. package/src/examples/scrollbox-mouse-test.ts +112 -0
  108. package/src/examples/scrollbox-overlay-hit-test.ts +206 -0
  109. package/src/examples/select-demo.ts +237 -0
  110. package/src/examples/shader-cube-demo.ts +1015 -0
  111. package/src/examples/simple-layout-example.ts +591 -0
  112. package/src/examples/slider-demo.ts +617 -0
  113. package/src/examples/split-mode-demo.ts +453 -0
  114. package/src/examples/sprite-animation-demo.ts +443 -0
  115. package/src/examples/sprite-particle-generator-demo.ts +486 -0
  116. package/src/examples/static-sprite-demo.ts +193 -0
  117. package/src/examples/sticky-scroll-example.ts +308 -0
  118. package/src/examples/styled-text-demo.ts +282 -0
  119. package/src/examples/tab-select-demo.ts +219 -0
  120. package/src/examples/terminal-title.ts +29 -0
  121. package/src/examples/terminal.ts +305 -0
  122. package/src/examples/text-node-demo.ts +416 -0
  123. package/src/examples/text-selection-demo.ts +377 -0
  124. package/src/examples/text-table-demo.ts +503 -0
  125. package/src/examples/text-truncation-demo.ts +481 -0
  126. package/src/examples/text-wrap.ts +757 -0
  127. package/src/examples/texture-loading-demo.ts +259 -0
  128. package/src/examples/timeline-example.ts +670 -0
  129. package/src/examples/transparency-demo.ts +400 -0
  130. package/src/examples/vnode-composition-demo.ts +404 -0
  131. package/src/examples/wide-grapheme-overlay-demo.ts +280 -0
  132. package/src/index.ts +24 -0
  133. package/src/lib/KeyHandler.integration.test.ts +292 -0
  134. package/src/lib/KeyHandler.stopPropagation.test.ts +289 -0
  135. package/src/lib/KeyHandler.test.ts +662 -0
  136. package/src/lib/KeyHandler.ts +222 -0
  137. package/src/lib/RGBA.test.ts +984 -0
  138. package/src/lib/RGBA.ts +204 -0
  139. package/src/lib/ascii.font.ts +330 -0
  140. package/src/lib/border.test.ts +83 -0
  141. package/src/lib/border.ts +170 -0
  142. package/src/lib/bunfs.test.ts +27 -0
  143. package/src/lib/bunfs.ts +18 -0
  144. package/src/lib/clipboard.test.ts +41 -0
  145. package/src/lib/clipboard.ts +47 -0
  146. package/src/lib/clock.ts +35 -0
  147. package/src/lib/data-paths.test.ts +133 -0
  148. package/src/lib/data-paths.ts +109 -0
  149. package/src/lib/debounce.ts +106 -0
  150. package/src/lib/detect-links.test.ts +98 -0
  151. package/src/lib/detect-links.ts +56 -0
  152. package/src/lib/env.test.ts +228 -0
  153. package/src/lib/env.ts +209 -0
  154. package/src/lib/extmarks-history.ts +51 -0
  155. package/src/lib/extmarks-multiwidth.test.ts +322 -0
  156. package/src/lib/extmarks.test.ts +3457 -0
  157. package/src/lib/extmarks.ts +843 -0
  158. package/src/lib/fonts/block.json +405 -0
  159. package/src/lib/fonts/grid.json +265 -0
  160. package/src/lib/fonts/huge.json +741 -0
  161. package/src/lib/fonts/pallet.json +314 -0
  162. package/src/lib/fonts/shade.json +591 -0
  163. package/src/lib/fonts/slick.json +321 -0
  164. package/src/lib/fonts/tiny.json +69 -0
  165. package/src/lib/hast-styled-text.ts +59 -0
  166. package/src/lib/index.ts +21 -0
  167. package/src/lib/keymapping.test.ts +317 -0
  168. package/src/lib/keymapping.ts +115 -0
  169. package/src/lib/objects-in-viewport.test.ts +787 -0
  170. package/src/lib/objects-in-viewport.ts +153 -0
  171. package/src/lib/output.capture.ts +58 -0
  172. package/src/lib/parse.keypress-kitty.protocol.test.ts +340 -0
  173. package/src/lib/parse.keypress-kitty.test.ts +663 -0
  174. package/src/lib/parse.keypress-kitty.ts +439 -0
  175. package/src/lib/parse.keypress.test.ts +1849 -0
  176. package/src/lib/parse.keypress.ts +397 -0
  177. package/src/lib/parse.mouse.test.ts +552 -0
  178. package/src/lib/parse.mouse.ts +232 -0
  179. package/src/lib/paste.ts +16 -0
  180. package/src/lib/queue.ts +65 -0
  181. package/src/lib/renderable.validations.test.ts +87 -0
  182. package/src/lib/renderable.validations.ts +83 -0
  183. package/src/lib/scroll-acceleration.ts +98 -0
  184. package/src/lib/selection.ts +240 -0
  185. package/src/lib/singleton.ts +28 -0
  186. package/src/lib/stdin-parser.test.ts +2290 -0
  187. package/src/lib/stdin-parser.ts +1810 -0
  188. package/src/lib/styled-text.ts +178 -0
  189. package/src/lib/terminal-capability-detection.test.ts +202 -0
  190. package/src/lib/terminal-capability-detection.ts +79 -0
  191. package/src/lib/terminal-palette.test.ts +878 -0
  192. package/src/lib/terminal-palette.ts +383 -0
  193. package/src/lib/tree-sitter/assets/README.md +118 -0
  194. package/src/lib/tree-sitter/assets/update.ts +334 -0
  195. package/src/lib/tree-sitter/assets.d.ts +9 -0
  196. package/src/lib/tree-sitter/cache.test.ts +273 -0
  197. package/src/lib/tree-sitter/client.test.ts +1165 -0
  198. package/src/lib/tree-sitter/client.ts +607 -0
  199. package/src/lib/tree-sitter/default-parsers.ts +86 -0
  200. package/src/lib/tree-sitter/download-utils.ts +148 -0
  201. package/src/lib/tree-sitter/index.ts +28 -0
  202. package/src/lib/tree-sitter/parser.worker.ts +1042 -0
  203. package/src/lib/tree-sitter/parsers-config.ts +81 -0
  204. package/src/lib/tree-sitter/resolve-ft.test.ts +55 -0
  205. package/src/lib/tree-sitter/resolve-ft.ts +189 -0
  206. package/src/lib/tree-sitter/types.ts +82 -0
  207. package/src/lib/tree-sitter-styled-text.test.ts +1253 -0
  208. package/src/lib/tree-sitter-styled-text.ts +306 -0
  209. package/src/lib/validate-dir-name.ts +55 -0
  210. package/src/lib/yoga.options.test.ts +628 -0
  211. package/src/lib/yoga.options.ts +346 -0
  212. package/src/plugins/core-slot.ts +579 -0
  213. package/src/plugins/registry.ts +402 -0
  214. package/src/plugins/types.ts +46 -0
  215. package/src/post/effects.ts +930 -0
  216. package/src/post/filters.ts +489 -0
  217. package/src/post/matrices.ts +288 -0
  218. package/src/renderables/ASCIIFont.ts +219 -0
  219. package/src/renderables/Box.test.ts +205 -0
  220. package/src/renderables/Box.ts +326 -0
  221. package/src/renderables/Code.test.ts +2062 -0
  222. package/src/renderables/Code.ts +357 -0
  223. package/src/renderables/Diff.regression.test.ts +226 -0
  224. package/src/renderables/Diff.test.ts +3101 -0
  225. package/src/renderables/Diff.ts +1211 -0
  226. package/src/renderables/EditBufferRenderable.test.ts +288 -0
  227. package/src/renderables/EditBufferRenderable.ts +1166 -0
  228. package/src/renderables/FrameBuffer.ts +47 -0
  229. package/src/renderables/Input.test.ts +1228 -0
  230. package/src/renderables/Input.ts +247 -0
  231. package/src/renderables/LineNumberRenderable.ts +724 -0
  232. package/src/renderables/Markdown.ts +1393 -0
  233. package/src/renderables/ScrollBar.ts +422 -0
  234. package/src/renderables/ScrollBox.ts +883 -0
  235. package/src/renderables/Select.test.ts +1033 -0
  236. package/src/renderables/Select.ts +524 -0
  237. package/src/renderables/Slider.test.ts +456 -0
  238. package/src/renderables/Slider.ts +342 -0
  239. package/src/renderables/TabSelect.test.ts +197 -0
  240. package/src/renderables/TabSelect.ts +455 -0
  241. package/src/renderables/Text.selection-buffer.test.ts +123 -0
  242. package/src/renderables/Text.test.ts +2660 -0
  243. package/src/renderables/Text.ts +147 -0
  244. package/src/renderables/TextBufferRenderable.ts +518 -0
  245. package/src/renderables/TextNode.test.ts +1058 -0
  246. package/src/renderables/TextNode.ts +325 -0
  247. package/src/renderables/TextTable.test.ts +1421 -0
  248. package/src/renderables/TextTable.ts +1344 -0
  249. package/src/renderables/Textarea.ts +430 -0
  250. package/src/renderables/TimeToFirstDraw.ts +89 -0
  251. package/src/renderables/__snapshots__/Code.test.ts.snap +13 -0
  252. package/src/renderables/__snapshots__/Diff.test.ts.snap +785 -0
  253. package/src/renderables/__snapshots__/Text.test.ts.snap +421 -0
  254. package/src/renderables/__snapshots__/TextTable.test.ts.snap +215 -0
  255. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +144 -0
  256. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +816 -0
  257. package/src/renderables/__tests__/LineNumberRenderable.test.ts +1865 -0
  258. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +85 -0
  259. package/src/renderables/__tests__/Markdown.code-colors.test.ts +242 -0
  260. package/src/renderables/__tests__/Markdown.test.ts +2518 -0
  261. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +87 -0
  262. package/src/renderables/__tests__/Textarea.buffer.test.ts +682 -0
  263. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +675 -0
  264. package/src/renderables/__tests__/Textarea.editing.test.ts +2041 -0
  265. package/src/renderables/__tests__/Textarea.error-handling.test.ts +35 -0
  266. package/src/renderables/__tests__/Textarea.events.test.ts +738 -0
  267. package/src/renderables/__tests__/Textarea.highlights.test.ts +590 -0
  268. package/src/renderables/__tests__/Textarea.keybinding.test.ts +3149 -0
  269. package/src/renderables/__tests__/Textarea.paste.test.ts +357 -0
  270. package/src/renderables/__tests__/Textarea.rendering.test.ts +1866 -0
  271. package/src/renderables/__tests__/Textarea.scroll.test.ts +733 -0
  272. package/src/renderables/__tests__/Textarea.selection.test.ts +1590 -0
  273. package/src/renderables/__tests__/Textarea.stress.test.ts +670 -0
  274. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +383 -0
  275. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +310 -0
  276. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +221 -0
  277. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +89 -0
  278. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +457 -0
  279. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +158 -0
  280. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +387 -0
  281. package/src/renderables/__tests__/markdown-parser.test.ts +217 -0
  282. package/src/renderables/__tests__/renderable-test-utils.ts +60 -0
  283. package/src/renderables/composition/README.md +8 -0
  284. package/src/renderables/composition/VRenderable.ts +32 -0
  285. package/src/renderables/composition/constructs.ts +127 -0
  286. package/src/renderables/composition/vnode.ts +289 -0
  287. package/src/renderables/index.ts +23 -0
  288. package/src/renderables/markdown-parser.ts +66 -0
  289. package/src/renderer.ts +2681 -0
  290. package/src/runtime-plugin-support.ts +39 -0
  291. package/src/runtime-plugin.ts +615 -0
  292. package/src/syntax-style.test.ts +841 -0
  293. package/src/syntax-style.ts +257 -0
  294. package/src/testing/README.md +210 -0
  295. package/src/testing/capture-spans.test.ts +194 -0
  296. package/src/testing/integration.test.ts +276 -0
  297. package/src/testing/manual-clock.ts +117 -0
  298. package/src/testing/mock-keys.test.ts +1378 -0
  299. package/src/testing/mock-keys.ts +457 -0
  300. package/src/testing/mock-mouse.test.ts +218 -0
  301. package/src/testing/mock-mouse.ts +247 -0
  302. package/src/testing/mock-tree-sitter-client.ts +73 -0
  303. package/src/testing/spy.ts +13 -0
  304. package/src/testing/test-recorder.test.ts +415 -0
  305. package/src/testing/test-recorder.ts +145 -0
  306. package/src/testing/test-renderer.ts +132 -0
  307. package/src/testing.ts +7 -0
  308. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +481 -0
  309. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +19 -0
  310. package/src/tests/__snapshots__/scrollbox.test.ts.snap +29 -0
  311. package/src/tests/absolute-positioning.snapshot.test.ts +638 -0
  312. package/src/tests/allocator-stats.test.ts +38 -0
  313. package/src/tests/destroy-during-render.test.ts +200 -0
  314. package/src/tests/destroy-on-exit.fixture.ts +36 -0
  315. package/src/tests/destroy-on-exit.test.ts +41 -0
  316. package/src/tests/hover-cursor.test.ts +98 -0
  317. package/src/tests/native-span-feed-async.test.ts +173 -0
  318. package/src/tests/native-span-feed-close.test.ts +120 -0
  319. package/src/tests/native-span-feed-coverage.test.ts +227 -0
  320. package/src/tests/native-span-feed-edge-cases.test.ts +352 -0
  321. package/src/tests/native-span-feed-use-after-free.test.ts +45 -0
  322. package/src/tests/opacity.test.ts +123 -0
  323. package/src/tests/renderable.snapshot.test.ts +524 -0
  324. package/src/tests/renderable.test.ts +1281 -0
  325. package/src/tests/renderer.clock.test.ts +158 -0
  326. package/src/tests/renderer.console-startup.test.ts +185 -0
  327. package/src/tests/renderer.control.test.ts +425 -0
  328. package/src/tests/renderer.core-slot-binding.test.ts +952 -0
  329. package/src/tests/renderer.cursor.test.ts +26 -0
  330. package/src/tests/renderer.destroy-during-render.test.ts +147 -0
  331. package/src/tests/renderer.focus-restore.test.ts +257 -0
  332. package/src/tests/renderer.focus.test.ts +294 -0
  333. package/src/tests/renderer.idle.test.ts +219 -0
  334. package/src/tests/renderer.input.test.ts +2237 -0
  335. package/src/tests/renderer.kitty-flags.test.ts +195 -0
  336. package/src/tests/renderer.mouse.test.ts +1274 -0
  337. package/src/tests/renderer.palette.test.ts +629 -0
  338. package/src/tests/renderer.selection.test.ts +49 -0
  339. package/src/tests/renderer.slot-registry.test.ts +684 -0
  340. package/src/tests/renderer.useMouse.test.ts +47 -0
  341. package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +76 -0
  342. package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +43 -0
  343. package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +67 -0
  344. package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +72 -0
  345. package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +44 -0
  346. package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +85 -0
  347. package/src/tests/runtime-plugin-path-alias.fixture.ts +43 -0
  348. package/src/tests/runtime-plugin-resolve-roots.fixture.ts +65 -0
  349. package/src/tests/runtime-plugin-support.fixture.ts +11 -0
  350. package/src/tests/runtime-plugin-support.test.ts +19 -0
  351. package/src/tests/runtime-plugin-windows-file-url.fixture.ts +30 -0
  352. package/src/tests/runtime-plugin.fixture.ts +40 -0
  353. package/src/tests/runtime-plugin.test.ts +354 -0
  354. package/src/tests/scrollbox-culling-bug.test.ts +114 -0
  355. package/src/tests/scrollbox-hitgrid-resize.test.ts +136 -0
  356. package/src/tests/scrollbox-hitgrid.test.ts +909 -0
  357. package/src/tests/scrollbox.test.ts +1530 -0
  358. package/src/tests/wrap-resize-perf.test.ts +276 -0
  359. package/src/tests/yoga-setters.test.ts +921 -0
  360. package/src/text-buffer-view.test.ts +705 -0
  361. package/src/text-buffer-view.ts +189 -0
  362. package/src/text-buffer.test.ts +347 -0
  363. package/src/text-buffer.ts +250 -0
  364. package/src/types.ts +161 -0
  365. package/src/utils.ts +88 -0
  366. package/src/zig/ansi.zig +268 -0
  367. package/src/zig/bench/README.md +50 -0
  368. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +887 -0
  369. package/src/zig/bench/edit-buffer_bench.zig +476 -0
  370. package/src/zig/bench/native-span-feed_bench.zig +100 -0
  371. package/src/zig/bench/rope-markers_bench.zig +713 -0
  372. package/src/zig/bench/rope_bench.zig +514 -0
  373. package/src/zig/bench/styled-text_bench.zig +470 -0
  374. package/src/zig/bench/text-buffer-coords_bench.zig +362 -0
  375. package/src/zig/bench/text-buffer-view_bench.zig +459 -0
  376. package/src/zig/bench/text-chunk-graphemes_bench.zig +273 -0
  377. package/src/zig/bench/utf8_bench.zig +799 -0
  378. package/src/zig/bench-utils.zig +431 -0
  379. package/src/zig/bench.zig +217 -0
  380. package/src/zig/buffer-methods.zig +211 -0
  381. package/src/zig/buffer.zig +2281 -0
  382. package/src/zig/build.zig +289 -0
  383. package/src/zig/build.zig.zon +16 -0
  384. package/src/zig/edit-buffer.zig +825 -0
  385. package/src/zig/editor-view.zig +802 -0
  386. package/src/zig/event-bus.zig +13 -0
  387. package/src/zig/event-emitter.zig +65 -0
  388. package/src/zig/file-logger.zig +92 -0
  389. package/src/zig/grapheme.zig +599 -0
  390. package/src/zig/lib.zig +1854 -0
  391. package/src/zig/link.zig +333 -0
  392. package/src/zig/logger.zig +43 -0
  393. package/src/zig/mem-registry.zig +125 -0
  394. package/src/zig/native-span-feed-bench-lib.zig +7 -0
  395. package/src/zig/native-span-feed.zig +708 -0
  396. package/src/zig/renderer.zig +1393 -0
  397. package/src/zig/rope.zig +1220 -0
  398. package/src/zig/syntax-style.zig +161 -0
  399. package/src/zig/terminal.zig +987 -0
  400. package/src/zig/test.zig +72 -0
  401. package/src/zig/tests/README.md +18 -0
  402. package/src/zig/tests/buffer-methods_test.zig +1109 -0
  403. package/src/zig/tests/buffer_test.zig +2557 -0
  404. package/src/zig/tests/edit-buffer-history_test.zig +271 -0
  405. package/src/zig/tests/edit-buffer_test.zig +1689 -0
  406. package/src/zig/tests/editor-view_test.zig +3299 -0
  407. package/src/zig/tests/event-emitter_test.zig +249 -0
  408. package/src/zig/tests/grapheme_test.zig +1304 -0
  409. package/src/zig/tests/link_test.zig +190 -0
  410. package/src/zig/tests/mem-registry_test.zig +473 -0
  411. package/src/zig/tests/memory_leak_regression_test.zig +159 -0
  412. package/src/zig/tests/native-span-feed_test.zig +1264 -0
  413. package/src/zig/tests/renderer_test.zig +1017 -0
  414. package/src/zig/tests/rope-nested_test.zig +712 -0
  415. package/src/zig/tests/rope_fuzz_test.zig +238 -0
  416. package/src/zig/tests/rope_test.zig +2362 -0
  417. package/src/zig/tests/segment-merge.test.zig +148 -0
  418. package/src/zig/tests/syntax-style_test.zig +557 -0
  419. package/src/zig/tests/terminal_test.zig +754 -0
  420. package/src/zig/tests/text-buffer-drawing_test.zig +3237 -0
  421. package/src/zig/tests/text-buffer-highlights_test.zig +666 -0
  422. package/src/zig/tests/text-buffer-iterators_test.zig +776 -0
  423. package/src/zig/tests/text-buffer-segment_test.zig +320 -0
  424. package/src/zig/tests/text-buffer-selection_test.zig +1035 -0
  425. package/src/zig/tests/text-buffer-selection_viewport_test.zig +358 -0
  426. package/src/zig/tests/text-buffer-view_test.zig +3649 -0
  427. package/src/zig/tests/text-buffer_test.zig +2191 -0
  428. package/src/zig/tests/unicode-width-map.zon +3909 -0
  429. package/src/zig/tests/utf8_no_zwj_test.zig +260 -0
  430. package/src/zig/tests/utf8_test.zig +4057 -0
  431. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +267 -0
  432. package/src/zig/tests/utf8_wcwidth_test.zig +357 -0
  433. package/src/zig/tests/word-wrap-editing_test.zig +498 -0
  434. package/src/zig/tests/wrap-cache-perf_test.zig +113 -0
  435. package/src/zig/text-buffer-iterators.zig +499 -0
  436. package/src/zig/text-buffer-segment.zig +404 -0
  437. package/src/zig/text-buffer-view.zig +1371 -0
  438. package/src/zig/text-buffer.zig +1180 -0
  439. package/src/zig/utf8.zig +1948 -0
  440. package/src/zig/utils.zig +9 -0
  441. package/src/zig-structs.ts +261 -0
  442. package/src/zig.ts +3884 -0
  443. package/tsconfig.build.json +24 -0
  444. package/tsconfig.json +27 -0
  445. package/3d/SpriteResourceManager.d.ts +0 -74
  446. package/3d/SpriteUtils.d.ts +0 -13
  447. package/3d/TextureUtils.d.ts +0 -24
  448. package/3d/ThreeRenderable.d.ts +0 -40
  449. package/3d/WGPURenderer.d.ts +0 -61
  450. package/3d/animation/ExplodingSpriteEffect.d.ts +0 -71
  451. package/3d/animation/PhysicsExplodingSpriteEffect.d.ts +0 -76
  452. package/3d/animation/SpriteAnimator.d.ts +0 -124
  453. package/3d/animation/SpriteParticleGenerator.d.ts +0 -62
  454. package/3d/canvas.d.ts +0 -44
  455. package/3d/index.d.ts +0 -12
  456. package/3d/physics/PlanckPhysicsAdapter.d.ts +0 -19
  457. package/3d/physics/RapierPhysicsAdapter.d.ts +0 -19
  458. package/3d/physics/physics-interface.d.ts +0 -27
  459. package/3d.d.ts +0 -2
  460. package/3d.js +0 -34041
  461. package/3d.js.map +0 -155
  462. package/LICENSE +0 -21
  463. package/NativeSpanFeed.d.ts +0 -41
  464. package/Renderable.d.ts +0 -334
  465. package/animation/Timeline.d.ts +0 -126
  466. package/ansi.d.ts +0 -13
  467. package/buffer.d.ts +0 -111
  468. package/console.d.ts +0 -144
  469. package/edit-buffer.d.ts +0 -98
  470. package/editor-view.d.ts +0 -73
  471. package/index-9vwc3fg6.js +0 -12260
  472. package/index-9vwc3fg6.js.map +0 -42
  473. package/index-dcj62y8t.js +0 -20614
  474. package/index-dcj62y8t.js.map +0 -67
  475. package/index-f7n39gpy.js +0 -411
  476. package/index-f7n39gpy.js.map +0 -10
  477. package/index.d.ts +0 -23
  478. package/index.js +0 -478
  479. package/index.js.map +0 -9
  480. package/lib/KeyHandler.d.ts +0 -61
  481. package/lib/RGBA.d.ts +0 -25
  482. package/lib/ascii.font.d.ts +0 -508
  483. package/lib/border.d.ts +0 -51
  484. package/lib/bunfs.d.ts +0 -7
  485. package/lib/clipboard.d.ts +0 -17
  486. package/lib/clock.d.ts +0 -15
  487. package/lib/data-paths.d.ts +0 -26
  488. package/lib/debounce.d.ts +0 -42
  489. package/lib/detect-links.d.ts +0 -6
  490. package/lib/env.d.ts +0 -42
  491. package/lib/extmarks-history.d.ts +0 -17
  492. package/lib/extmarks.d.ts +0 -89
  493. package/lib/hast-styled-text.d.ts +0 -17
  494. package/lib/index.d.ts +0 -21
  495. package/lib/keymapping.d.ts +0 -25
  496. package/lib/objects-in-viewport.d.ts +0 -24
  497. package/lib/output.capture.d.ts +0 -24
  498. package/lib/parse.keypress-kitty.d.ts +0 -2
  499. package/lib/parse.keypress.d.ts +0 -26
  500. package/lib/parse.mouse.d.ts +0 -30
  501. package/lib/paste.d.ts +0 -7
  502. package/lib/queue.d.ts +0 -15
  503. package/lib/renderable.validations.d.ts +0 -12
  504. package/lib/scroll-acceleration.d.ts +0 -43
  505. package/lib/selection.d.ts +0 -63
  506. package/lib/singleton.d.ts +0 -7
  507. package/lib/stdin-parser.d.ts +0 -87
  508. package/lib/styled-text.d.ts +0 -63
  509. package/lib/terminal-capability-detection.d.ts +0 -30
  510. package/lib/terminal-palette.d.ts +0 -50
  511. package/lib/tree-sitter/assets/update.d.ts +0 -11
  512. package/lib/tree-sitter/client.d.ts +0 -47
  513. package/lib/tree-sitter/default-parsers.d.ts +0 -2
  514. package/lib/tree-sitter/download-utils.d.ts +0 -21
  515. package/lib/tree-sitter/index.d.ts +0 -8
  516. package/lib/tree-sitter/parser.worker.d.ts +0 -1
  517. package/lib/tree-sitter/parsers-config.d.ts +0 -53
  518. package/lib/tree-sitter/resolve-ft.d.ts +0 -5
  519. package/lib/tree-sitter/types.d.ts +0 -82
  520. package/lib/tree-sitter-styled-text.d.ts +0 -14
  521. package/lib/validate-dir-name.d.ts +0 -1
  522. package/lib/yoga.options.d.ts +0 -32
  523. package/parser.worker.js +0 -899
  524. package/parser.worker.js.map +0 -12
  525. package/plugins/core-slot.d.ts +0 -72
  526. package/plugins/registry.d.ts +0 -42
  527. package/plugins/types.d.ts +0 -34
  528. package/post/effects.d.ts +0 -147
  529. package/post/filters.d.ts +0 -65
  530. package/post/matrices.d.ts +0 -20
  531. package/renderables/ASCIIFont.d.ts +0 -52
  532. package/renderables/Box.d.ts +0 -81
  533. package/renderables/Code.d.ts +0 -78
  534. package/renderables/Diff.d.ts +0 -142
  535. package/renderables/EditBufferRenderable.d.ts +0 -237
  536. package/renderables/FrameBuffer.d.ts +0 -16
  537. package/renderables/Input.d.ts +0 -67
  538. package/renderables/LineNumberRenderable.d.ts +0 -78
  539. package/renderables/Markdown.d.ts +0 -185
  540. package/renderables/ScrollBar.d.ts +0 -77
  541. package/renderables/ScrollBox.d.ts +0 -124
  542. package/renderables/Select.d.ts +0 -115
  543. package/renderables/Slider.d.ts +0 -47
  544. package/renderables/TabSelect.d.ts +0 -96
  545. package/renderables/Text.d.ts +0 -36
  546. package/renderables/TextBufferRenderable.d.ts +0 -105
  547. package/renderables/TextNode.d.ts +0 -91
  548. package/renderables/TextTable.d.ts +0 -140
  549. package/renderables/Textarea.d.ts +0 -63
  550. package/renderables/TimeToFirstDraw.d.ts +0 -24
  551. package/renderables/__tests__/renderable-test-utils.d.ts +0 -12
  552. package/renderables/composition/VRenderable.d.ts +0 -16
  553. package/renderables/composition/constructs.d.ts +0 -35
  554. package/renderables/composition/vnode.d.ts +0 -46
  555. package/renderables/index.d.ts +0 -23
  556. package/renderables/markdown-parser.d.ts +0 -10
  557. package/renderer.d.ts +0 -419
  558. package/runtime-plugin-support.d.ts +0 -3
  559. package/runtime-plugin-support.js +0 -29
  560. package/runtime-plugin-support.js.map +0 -10
  561. package/runtime-plugin.d.ts +0 -16
  562. package/runtime-plugin.js +0 -16
  563. package/runtime-plugin.js.map +0 -9
  564. package/syntax-style.d.ts +0 -54
  565. package/testing/manual-clock.d.ts +0 -17
  566. package/testing/mock-keys.d.ts +0 -81
  567. package/testing/mock-mouse.d.ts +0 -38
  568. package/testing/mock-tree-sitter-client.d.ts +0 -23
  569. package/testing/spy.d.ts +0 -7
  570. package/testing/test-recorder.d.ts +0 -61
  571. package/testing/test-renderer.d.ts +0 -23
  572. package/testing.d.ts +0 -6
  573. package/testing.js +0 -697
  574. package/testing.js.map +0 -15
  575. package/text-buffer-view.d.ts +0 -42
  576. package/text-buffer.d.ts +0 -67
  577. package/types.d.ts +0 -139
  578. package/utils.d.ts +0 -14
  579. package/zig-structs.d.ts +0 -155
  580. package/zig.d.ts +0 -353
  581. /package/{assets → src/lib/tree-sitter/assets}/javascript/highlights.scm +0 -0
  582. /package/{assets → src/lib/tree-sitter/assets}/javascript/tree-sitter-javascript.wasm +0 -0
  583. /package/{assets → src/lib/tree-sitter/assets}/markdown/highlights.scm +0 -0
  584. /package/{assets → src/lib/tree-sitter/assets}/markdown/injections.scm +0 -0
  585. /package/{assets → src/lib/tree-sitter/assets}/markdown/tree-sitter-markdown.wasm +0 -0
  586. /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/highlights.scm +0 -0
  587. /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  588. /package/{assets → src/lib/tree-sitter/assets}/typescript/highlights.scm +0 -0
  589. /package/{assets → src/lib/tree-sitter/assets}/typescript/tree-sitter-typescript.wasm +0 -0
  590. /package/{assets → src/lib/tree-sitter/assets}/zig/highlights.scm +0 -0
  591. /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
+ }