@fairyhunter13/opentui-core 0.1.112 → 0.1.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +63 -51
  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-8fks7yv1.js +0 -411
  472. package/index-8fks7yv1.js.map +0 -10
  473. package/index-egy5e2rs.js +0 -12267
  474. package/index-egy5e2rs.js.map +0 -42
  475. package/index-tse8gzh0.js +0 -20614
  476. package/index-tse8gzh0.js.map +0 -67
  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,2290 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { Buffer } from "node:buffer"
3
+ import { ManualClock } from "../testing/manual-clock.js"
4
+ import type { Clock, TimerHandle } from "./clock.js"
5
+ import type { ScrollInfo } from "./parse.mouse"
6
+ import { StdinParser, type StdinEvent, type StdinParserOptions } from "./stdin-parser.js"
7
+
8
+ type KeySnap = {
9
+ type: "key"
10
+ raw: string
11
+ name: string
12
+ ctrl: boolean
13
+ meta: boolean
14
+ shift: boolean
15
+ eventType: string
16
+ }
17
+ type MouseSnap = { type: "mouse"; raw: string; encoding: "sgr" | "x10"; event: Record<string, unknown> }
18
+ type PasteSnap = { type: "paste"; bytes: Uint8Array }
19
+ type RespSnap = { type: "response"; protocol: string; sequence: string }
20
+ type Snap = KeySnap | MouseSnap | PasteSnap | RespSnap
21
+
22
+ const K_DEFAULTS = { ctrl: false, meta: false, shift: false, eventType: "press" }
23
+ const TEST_TIMEOUT_MS = 10
24
+ type KOpts = { raw?: string; ctrl?: boolean; meta?: boolean; shift?: boolean; eventType?: string }
25
+
26
+ function k(name: string, opts: KOpts = {}): KeySnap {
27
+ return { type: "key", raw: opts.raw ?? name, name, ...K_DEFAULTS, ...opts }
28
+ }
29
+
30
+ function resp(protocol: string, sequence: string): RespSnap {
31
+ return { type: "response", protocol, sequence }
32
+ }
33
+
34
+ function paste(text: string): PasteSnap {
35
+ return { type: "paste", bytes: Uint8Array.from(Buffer.from(text)) }
36
+ }
37
+
38
+ const NO_MODS = { shift: false, alt: false, ctrl: false }
39
+
40
+ function sgr(
41
+ raw: string,
42
+ evType: string,
43
+ x: number,
44
+ y: number,
45
+ opts: { button?: number; mods?: Partial<typeof NO_MODS>; scroll?: ScrollInfo } = {},
46
+ ): MouseSnap {
47
+ const event: Record<string, unknown> = {
48
+ type: evType,
49
+ button: opts.button ?? 0,
50
+ x,
51
+ y,
52
+ modifiers: { ...NO_MODS, ...opts.mods },
53
+ }
54
+ if (opts.scroll) event.scroll = opts.scroll
55
+ return { type: "mouse", raw, encoding: "sgr", event }
56
+ }
57
+
58
+ function x10m(
59
+ raw: string,
60
+ evType: string,
61
+ x: number,
62
+ y: number,
63
+ opts: { button?: number; mods?: Partial<typeof NO_MODS>; scroll?: ScrollInfo } = {},
64
+ ): MouseSnap {
65
+ const event: Record<string, unknown> = {
66
+ type: evType,
67
+ button: opts.button ?? 0,
68
+ x,
69
+ y,
70
+ modifiers: { ...NO_MODS, ...opts.mods },
71
+ }
72
+ if (opts.scroll) event.scroll = opts.scroll
73
+ return { type: "mouse", raw, encoding: "x10", event }
74
+ }
75
+
76
+ function createParser(options: StdinParserOptions = {}): StdinParser {
77
+ return new StdinParser({ armTimeouts: false, clock: new ManualClock(), ...options })
78
+ }
79
+
80
+ function createTimedParser(options: StdinParserOptions = {}): { parser: StdinParser; clock: ManualClock } {
81
+ const clock = new ManualClock()
82
+ return { parser: new StdinParser({ armTimeouts: true, clock, timeoutMs: TEST_TIMEOUT_MS, ...options }), clock }
83
+ }
84
+
85
+ function snapshotEvent(event: StdinEvent): Snap {
86
+ switch (event.type) {
87
+ case "key":
88
+ return {
89
+ type: "key",
90
+ raw: event.raw,
91
+ name: event.key.name,
92
+ ctrl: event.key.ctrl,
93
+ meta: event.key.meta,
94
+ shift: event.key.shift,
95
+ eventType: event.key.eventType,
96
+ }
97
+ case "mouse": {
98
+ const ev: Record<string, unknown> = { ...event.event }
99
+ if (!ev.scroll) delete ev.scroll
100
+ return { type: "mouse", raw: event.raw, encoding: event.encoding, event: ev }
101
+ }
102
+ case "paste":
103
+ return { type: "paste", bytes: event.bytes }
104
+ case "response":
105
+ return { type: "response", protocol: event.protocol, sequence: event.sequence }
106
+ }
107
+ }
108
+
109
+ function snap(parser: StdinParser): Snap[] {
110
+ const events: StdinEvent[] = []
111
+ parser.drain((e) => events.push(e))
112
+ return events.map(snapshotEvent)
113
+ }
114
+
115
+ type ChunkInput = string | number[] | Uint8Array
116
+
117
+ function buf(input: ChunkInput): Uint8Array {
118
+ if (typeof input === "string") return Buffer.from(input)
119
+ return input instanceof Uint8Array ? input : Uint8Array.from(input)
120
+ }
121
+
122
+ function latin1(input: number[] | Uint8Array): string {
123
+ return Buffer.from(buf(input)).toString("latin1")
124
+ }
125
+
126
+ function snapChunks(chunks: ChunkInput[], opts?: StdinParserOptions): Snap[] {
127
+ const p = createParser(opts)
128
+ try {
129
+ for (const chunk of chunks) p.push(buf(chunk))
130
+ return snap(p)
131
+ } finally {
132
+ p.destroy()
133
+ }
134
+ }
135
+
136
+ function concatChunks(chunks: ChunkInput[]): Uint8Array {
137
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(buf(chunk))))
138
+ }
139
+
140
+ function x10bytes(rawButton: number, x: number, y: number): number[] {
141
+ return [0x1b, 0x5b, 0x4d, rawButton + 32, x + 33, y + 33]
142
+ }
143
+
144
+ type Case = [label: string, input: ChunkInput, expected: Snap[]]
145
+
146
+ function table(cases: Case[], opts?: StdinParserOptions) {
147
+ for (const [label, input, expected] of cases) {
148
+ test(label, () => {
149
+ const p = createParser(opts)
150
+ try {
151
+ p.push(buf(input))
152
+ expect(snap(p)).toEqual(expected)
153
+ } finally {
154
+ p.destroy()
155
+ }
156
+ })
157
+ }
158
+ }
159
+
160
+ /** push each byte individually, assert same result as whole-chunk push */
161
+ function assertChunkInvariant(input: Uint8Array, opts?: StdinParserOptions) {
162
+ const whole = createParser(opts)
163
+ const split = createParser(opts)
164
+ try {
165
+ whole.push(input)
166
+ const expected = snap(whole)
167
+ for (let i = 0; i < input.length; i++) split.push(input.subarray(i, i + 1))
168
+ expect(snap(split)).toEqual(expected)
169
+ } finally {
170
+ whole.destroy()
171
+ split.destroy()
172
+ }
173
+ }
174
+
175
+ describe("StdinParser", () => {
176
+ describe("printable ASCII", () => {
177
+ test("lowercase a-z", () => {
178
+ const p = createParser()
179
+ try {
180
+ p.push(Buffer.from("abcdefghijklmnopqrstuvwxyz"))
181
+ expect(snap(p)).toEqual("abcdefghijklmnopqrstuvwxyz".split("").map((c) => k(c)))
182
+ } finally {
183
+ p.destroy()
184
+ }
185
+ })
186
+
187
+ test("uppercase A-Z produce shifted keys", () => {
188
+ const p = createParser()
189
+ try {
190
+ p.push(Buffer.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
191
+ expect(snap(p)).toEqual(
192
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((c) => k(c.toLowerCase(), { raw: c, shift: true })),
193
+ )
194
+ } finally {
195
+ p.destroy()
196
+ }
197
+ })
198
+
199
+ test("digits 0-9", () => {
200
+ const p = createParser()
201
+ try {
202
+ p.push(Buffer.from("0123456789"))
203
+ expect(snap(p)).toEqual("0123456789".split("").map((c) => k(c)))
204
+ } finally {
205
+ p.destroy()
206
+ }
207
+ })
208
+
209
+ test("common symbols", () => {
210
+ const p = createParser()
211
+ try {
212
+ const syms = "!@#$%^&*()-_=+[]{}|;':,./<>?`~"
213
+ p.push(Buffer.from(syms))
214
+ expect(snap(p)).toEqual(syms.split("").map((c) => k(c)))
215
+ } finally {
216
+ p.destroy()
217
+ }
218
+ })
219
+
220
+ test("space produces key named space", () => {
221
+ const p = createParser()
222
+ try {
223
+ p.push(Buffer.from(" "))
224
+ expect(snap(p)).toEqual([k("space", { raw: " " })])
225
+ } finally {
226
+ p.destroy()
227
+ }
228
+ })
229
+ })
230
+
231
+ describe("control characters", () => {
232
+ // Map of special control bytes that get their own key name instead of ctrl+letter
233
+ const special: Record<number, [string, KOpts]> = {
234
+ 0x00: ["space", { ctrl: true }],
235
+ 0x08: ["backspace", {}],
236
+ 0x09: ["tab", {}],
237
+ 0x0a: ["linefeed", {}],
238
+ 0x0d: ["return", {}],
239
+ }
240
+
241
+ const cases: Case[] = []
242
+ for (let byte = 0; byte <= 0x1a; byte++) {
243
+ if (byte === 0x1b) continue // ESC tested separately
244
+ const raw = String.fromCharCode(byte)
245
+ const sp = special[byte]
246
+ if (sp) {
247
+ cases.push([`0x${byte.toString(16).padStart(2, "0")} → ${sp[0]}`, [byte], [k(sp[0], { raw, ...sp[1] })]])
248
+ } else {
249
+ const letter = String.fromCharCode(byte + 96)
250
+ cases.push([
251
+ `ctrl+${letter} (0x${byte.toString(16).padStart(2, "0")})`,
252
+ [byte],
253
+ [k(letter, { raw, ctrl: true })],
254
+ ])
255
+ }
256
+ }
257
+ cases.push(["0x7f → backspace", [0x7f], [k("backspace", { raw: "\x7f" })]])
258
+
259
+ table(cases)
260
+ })
261
+
262
+ describe("special keys", () => {
263
+ table([
264
+ ["return", "\r", [k("return", { raw: "\r" })]],
265
+ ["linefeed", "\n", [k("linefeed", { raw: "\n" })]],
266
+ ["tab", "\t", [k("tab", { raw: "\t" })]],
267
+ ["backspace (0x08)", "\b", [k("backspace", { raw: "\b" })]],
268
+ ["backspace (0x7f)", "\x7f", [k("backspace", { raw: "\x7f" })]],
269
+ ["escape (lone, no timeout)", "\x1b", []], // stays pending without timeout
270
+ ["shift-tab", "\x1b[Z", [k("tab", { raw: "\x1b[Z", shift: true })]],
271
+ ["ctrl+space", "\x00", [k("space", { raw: "\x00", ctrl: true })]],
272
+ ])
273
+
274
+ test("lone ESC with timeout produces escape key", () => {
275
+ const { parser, clock } = createTimedParser()
276
+ try {
277
+ parser.push(Buffer.from("\x1b"))
278
+ expect(snap(parser)).toEqual([])
279
+ clock.advance(10)
280
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
281
+ } finally {
282
+ parser.destroy()
283
+ }
284
+ })
285
+ })
286
+
287
+ describe("arrows and navigation", () => {
288
+ table([
289
+ // CSI arrows
290
+ ["up", "\x1b[A", [k("up", { raw: "\x1b[A" })]],
291
+ ["down", "\x1b[B", [k("down", { raw: "\x1b[B" })]],
292
+ ["right", "\x1b[C", [k("right", { raw: "\x1b[C" })]],
293
+ ["left", "\x1b[D", [k("left", { raw: "\x1b[D" })]],
294
+ ["home", "\x1b[H", [k("home", { raw: "\x1b[H" })]],
295
+ ["end", "\x1b[F", [k("end", { raw: "\x1b[F" })]],
296
+ ["clear", "\x1b[E", [k("clear", { raw: "\x1b[E" })]],
297
+ // tilde navigation
298
+ ["home ~", "\x1b[1~", [k("home", { raw: "\x1b[1~" })]],
299
+ ["insert ~", "\x1b[2~", [k("insert", { raw: "\x1b[2~" })]],
300
+ ["delete ~", "\x1b[3~", [k("delete", { raw: "\x1b[3~" })]],
301
+ ["end ~", "\x1b[4~", [k("end", { raw: "\x1b[4~" })]],
302
+ ["pgup ~", "\x1b[5~", [k("pageup", { raw: "\x1b[5~" })]],
303
+ ["pgdn ~", "\x1b[6~", [k("pagedown", { raw: "\x1b[6~" })]],
304
+ // rxvt
305
+ ["home rxvt", "\x1b[7~", [k("home", { raw: "\x1b[7~" })]],
306
+ ["end rxvt", "\x1b[8~", [k("end", { raw: "\x1b[8~" })]],
307
+ ])
308
+ })
309
+
310
+ describe("function keys", () => {
311
+ // ESC [ n ~ form
312
+ const tildeF: [string, string][] = [
313
+ ["f1", "11"],
314
+ ["f2", "12"],
315
+ ["f3", "13"],
316
+ ["f4", "14"],
317
+ ["f5", "15"],
318
+ ["f6", "17"],
319
+ ["f7", "18"],
320
+ ["f8", "19"],
321
+ ["f9", "20"],
322
+ ["f10", "21"],
323
+ ["f11", "23"],
324
+ ["f12", "24"],
325
+ ]
326
+ table(tildeF.map(([name, num]) => [`${name} (CSI ${num}~)`, `\x1b[${num}~`, [k(name, { raw: `\x1b[${num}~` })]]))
327
+
328
+ // ESC O letter (SS3) form — F1-F4
329
+ table([
330
+ ["f1 (SS3)", "\x1bOP", [k("f1", { raw: "\x1bOP" })]],
331
+ ["f2 (SS3)", "\x1bOQ", [k("f2", { raw: "\x1bOQ" })]],
332
+ ["f3 (SS3)", "\x1bOR", [k("f3", { raw: "\x1bOR" })]],
333
+ ["f4 (SS3)", "\x1bOS", [k("f4", { raw: "\x1bOS" })]],
334
+ ])
335
+ })
336
+
337
+ describe("double-bracket CSI variants", () => {
338
+ table([
339
+ ["f1 ([[A)", "\x1b[[A", [k("f1", { raw: "\x1b[[A" })]],
340
+ ["f2 ([[B)", "\x1b[[B", [k("f2", { raw: "\x1b[[B" })]],
341
+ ["f3 ([[C)", "\x1b[[C", [k("f3", { raw: "\x1b[[C" })]],
342
+ ["f4 ([[D)", "\x1b[[D", [k("f4", { raw: "\x1b[[D" })]],
343
+ ["f5 ([[E)", "\x1b[[E", [k("f5", { raw: "\x1b[[E" })]],
344
+ ["pageup ([[5~)", "\x1b[[5~", [k("pageup", { raw: "\x1b[[5~" })]],
345
+ ["pagedown ([[6~)", "\x1b[[6~", [k("pagedown", { raw: "\x1b[[6~" })]],
346
+ ])
347
+ })
348
+
349
+ describe("SS3 sequences", () => {
350
+ table([
351
+ ["up", "\x1bOA", [k("up", { raw: "\x1bOA" })]],
352
+ ["down", "\x1bOB", [k("down", { raw: "\x1bOB" })]],
353
+ ["right", "\x1bOC", [k("right", { raw: "\x1bOC" })]],
354
+ ["left", "\x1bOD", [k("left", { raw: "\x1bOD" })]],
355
+ ["home", "\x1bOH", [k("home", { raw: "\x1bOH" })]],
356
+ ["end", "\x1bOF", [k("end", { raw: "\x1bOF" })]],
357
+ ["clear", "\x1bOE", [k("clear", { raw: "\x1bOE" })]],
358
+ ])
359
+
360
+ test("SS3 interrupted by embedded ESC flushes partial then restarts", () => {
361
+ const p = createParser()
362
+ try {
363
+ p.push(Buffer.from("\x1bO\x1bOA"))
364
+ const s = snap(p)
365
+ expect(s).toHaveLength(2)
366
+ expect(s[0]).toEqual(resp("unknown", "\x1bO"))
367
+ expect(s[1]).toEqual(k("up", { raw: "\x1bOA" }))
368
+ } finally {
369
+ p.destroy()
370
+ }
371
+ })
372
+
373
+ test("SS3 timeout-flushed as unknown response", () => {
374
+ const { parser, clock } = createTimedParser()
375
+ try {
376
+ parser.push(Buffer.from("\x1bO"))
377
+ expect(snap(parser)).toEqual([])
378
+ clock.advance(10)
379
+ expect(snap(parser)).toEqual([resp("unknown", "\x1bO")])
380
+ } finally {
381
+ parser.destroy()
382
+ }
383
+ })
384
+
385
+ test("SS3 timeout flush does not swallow later text", () => {
386
+ const { parser, clock } = createTimedParser()
387
+ try {
388
+ parser.push(Buffer.from("\x1bO"))
389
+ expect(snap(parser)).toEqual([])
390
+ clock.advance(10)
391
+ expect(snap(parser)).toEqual([resp("unknown", "\x1bO")])
392
+
393
+ parser.push(Buffer.from("a"))
394
+ expect(snap(parser)).toEqual([k("a")])
395
+ } finally {
396
+ parser.destroy()
397
+ }
398
+ })
399
+ })
400
+
401
+ describe("modifier combinations", () => {
402
+ // CSI 1;modifier letter format
403
+ const modTable: [string, number, KOpts][] = [
404
+ ["shift", 2, { shift: true }],
405
+ ["alt", 3, { meta: true }],
406
+ ["shift+alt", 4, { shift: true, meta: true }],
407
+ ["ctrl", 5, { ctrl: true }],
408
+ ["shift+ctrl", 6, { shift: true, ctrl: true }],
409
+ ["alt+ctrl", 7, { meta: true, ctrl: true }],
410
+ ]
411
+
412
+ const arrows: [string, string][] = [
413
+ ["up", "A"],
414
+ ["down", "B"],
415
+ ["right", "C"],
416
+ ["left", "D"],
417
+ ]
418
+
419
+ const cases: Case[] = []
420
+ for (const [modName, modNum, modOpts] of modTable) {
421
+ for (const [keyName, letter] of arrows) {
422
+ const seq = `\x1b[1;${modNum}${letter}`
423
+ cases.push([`${modName}+${keyName}`, seq, [k(keyName, { raw: seq, ...modOpts })]])
424
+ }
425
+ }
426
+ table(cases)
427
+
428
+ // rxvt shift variants
429
+ table([
430
+ ["shift+up (rxvt)", "\x1b[a", [k("up", { raw: "\x1b[a", shift: true })]],
431
+ ["shift+down (rxvt)", "\x1b[b", [k("down", { raw: "\x1b[b", shift: true })]],
432
+ ["shift+right (rxvt)", "\x1b[c", [k("right", { raw: "\x1b[c", shift: true })]],
433
+ ["shift+left (rxvt)", "\x1b[d", [k("left", { raw: "\x1b[d", shift: true })]],
434
+ ])
435
+
436
+ // rxvt ctrl variants
437
+ table([
438
+ ["ctrl+up (rxvt)", "\x1bOa", [k("up", { raw: "\x1bOa", ctrl: true })]],
439
+ ["ctrl+down (rxvt)", "\x1bOb", [k("down", { raw: "\x1bOb", ctrl: true })]],
440
+ ["ctrl+right (rxvt)", "\x1bOc", [k("right", { raw: "\x1bOc", ctrl: true })]],
441
+ ["ctrl+left (rxvt)", "\x1bOd", [k("left", { raw: "\x1bOd", ctrl: true })]],
442
+ ])
443
+
444
+ // rxvt $ (shift) and ^ (ctrl) on tilde keys
445
+ table([
446
+ ["shift+insert (rxvt $)", "\x1b[2$", [k("insert", { raw: "\x1b[2$", shift: true })]],
447
+ ["shift+delete (rxvt $)", "\x1b[3$", [k("delete", { raw: "\x1b[3$", shift: true })]],
448
+ ["shift+pgup (rxvt $)", "\x1b[5$", [k("pageup", { raw: "\x1b[5$", shift: true })]],
449
+ ["shift+pgdn (rxvt $)", "\x1b[6$", [k("pagedown", { raw: "\x1b[6$", shift: true })]],
450
+ ["ctrl+insert (rxvt ^)", "\x1b[2^", [k("insert", { raw: "\x1b[2^", ctrl: true })]],
451
+ ["ctrl+delete (rxvt ^)", "\x1b[3^", [k("delete", { raw: "\x1b[3^", ctrl: true })]],
452
+ ["ctrl+pgup (rxvt ^)", "\x1b[5^", [k("pageup", { raw: "\x1b[5^", ctrl: true })]],
453
+ ["ctrl+pgdn (rxvt ^)", "\x1b[6^", [k("pagedown", { raw: "\x1b[6^", ctrl: true })]],
454
+ ])
455
+ })
456
+
457
+ describe("meta key combinations", () => {
458
+ test("meta+lowercase letters", () => {
459
+ const p = createParser()
460
+ try {
461
+ // Push all ESC+letter pairs at once — each should produce meta+key
462
+ for (const ch of "acdeghijklmoqrstuvwxyz".split("")) {
463
+ p.push(Buffer.from(`\x1b${ch}`))
464
+ }
465
+ const s = snap(p)
466
+ for (let i = 0; i < s.length; i++) {
467
+ const ch = "acdeghijklmoqrstuvwxyz"[i]!
468
+ expect(s[i]).toEqual(k(ch, { raw: `\x1b${ch}`, meta: true }))
469
+ }
470
+ } finally {
471
+ p.destroy()
472
+ }
473
+ })
474
+
475
+ // Lowercase ESC+b / ESC+f stay literal meta chords, while uppercase ESC+B / ESC+F
476
+ // preserve the old-style meta+arrow behavior from `main`.
477
+ table([
478
+ ["meta+b (literal chord)", "\x1bb", [k("b", { raw: "\x1bb", meta: true })]],
479
+ ["meta+f (literal chord)", "\x1bf", [k("f", { raw: "\x1bf", meta: true })]],
480
+ ["meta+B (old-style left)", "\x1bB", [k("left", { raw: "\x1bB", meta: true })]],
481
+ ["meta+F (old-style right)", "\x1bF", [k("right", { raw: "\x1bF", meta: true })]],
482
+ ["meta+n (plain letter)", "\x1bn", [k("n", { raw: "\x1bn", meta: true })]],
483
+ ["meta+p (plain letter)", "\x1bp", [k("p", { raw: "\x1bp", meta: true })]],
484
+ ])
485
+
486
+ table([
487
+ ["meta+return", "\x1b\r", [k("return", { raw: "\x1b\r", meta: true })]],
488
+ ["meta+linefeed", "\x1b\n", [k("linefeed", { raw: "\x1b\n", meta: true })]],
489
+ ["meta+backspace", "\x1b\x7f", [k("backspace", { raw: "\x1b\x7f", meta: true })]],
490
+ ["meta+backspace (0x08)", "\x1b\b", [k("backspace", { raw: "\x1b\b", meta: true })]],
491
+ ["meta+space", "\x1b ", [k("space", { raw: "\x1b ", meta: true })]],
492
+ ])
493
+
494
+ test("meta+escape (requires timeout for \\x1b\\x1b)", () => {
495
+ const { parser, clock } = createTimedParser()
496
+ try {
497
+ parser.push(Buffer.from("\x1b\x1b"))
498
+ expect(snap(parser)).toEqual([])
499
+ clock.advance(10)
500
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b\x1b", meta: true })])
501
+ } finally {
502
+ parser.destroy()
503
+ }
504
+ })
505
+
506
+ table([["double-ESC + [A → meta+up", "\x1b\x1b[A", [k("up", { raw: "\x1b\x1b[A", meta: true })]]])
507
+
508
+ test("meta+uppercase sets shift", () => {
509
+ const p = createParser()
510
+ try {
511
+ // ESC + uppercase letter → meta + shift + name (uppercase preserved in parseKeypress)
512
+ // Excluding B and F which map to arrow keys
513
+ p.push(Buffer.from("\x1bA"))
514
+ const s = snap(p)
515
+ expect(s).toEqual([k("A", { raw: "\x1bA", meta: true, shift: true })])
516
+ } finally {
517
+ p.destroy()
518
+ }
519
+ })
520
+
521
+ test("meta+ctrl+letter", () => {
522
+ const p = createParser()
523
+ try {
524
+ // ESC + ctrl char (e.g. ESC + 0x01 = meta+ctrl+a)
525
+ p.push(Uint8Array.from([0x1b, 0x01]))
526
+ expect(snap(p)).toEqual([k("a", { raw: "\x1b\x01", meta: true, ctrl: true })])
527
+ } finally {
528
+ p.destroy()
529
+ }
530
+ })
531
+
532
+ test("meta+digit", () => {
533
+ const p = createParser()
534
+ try {
535
+ p.push(Buffer.from("\x1b5"))
536
+ expect(snap(p)).toEqual([k("5", { raw: "\x1b5", meta: true })])
537
+ } finally {
538
+ p.destroy()
539
+ }
540
+ })
541
+ })
542
+
543
+ describe("kitty keyboard protocol", () => {
544
+ // CSI codepoint u format
545
+ table([
546
+ ["a key", "\x1b[97u", [k("a", { raw: "\x1b[97u" })]],
547
+ ["shift+a", "\x1b[97;2u", [k("a", { raw: "\x1b[97;2u", shift: true })]],
548
+ ["ctrl+a", "\x1b[97;5u", [k("a", { raw: "\x1b[97;5u", ctrl: true })]],
549
+ ["alt+a", "\x1b[97;3u", [k("a", { raw: "\x1b[97;3u", meta: true })]],
550
+ ["ctrl+shift+a", "\x1b[97;6u", [k("a", { raw: "\x1b[97;6u", ctrl: true, shift: true })]],
551
+ ["a release", "\x1b[97;1:3u", [k("a", { raw: "\x1b[97;1:3u", eventType: "release" })]],
552
+ ["escape", "\x1b[27u", [k("escape", { raw: "\x1b[27u" })]],
553
+ ["return", "\x1b[13u", [k("return", { raw: "\x1b[13u" })]],
554
+ ["tab", "\x1b[9u", [k("tab", { raw: "\x1b[9u" })]],
555
+ ["backspace", "\x1b[127u", [k("backspace", { raw: "\x1b[127u" })]],
556
+ ["delete", "\x1b[57349u", [k("delete", { raw: "\x1b[57349u" })]],
557
+ ["insert", "\x1b[57348u", [k("insert", { raw: "\x1b[57348u" })]],
558
+ ["f1", "\x1b[57364u", [k("f1", { raw: "\x1b[57364u" })]],
559
+ ["f12", "\x1b[57375u", [k("f12", { raw: "\x1b[57375u" })]],
560
+ ])
561
+
562
+ // CSI 1;modifier:event letter format (kitty functional keys)
563
+ table([
564
+ ["up press", "\x1b[1;1:1A", [k("up", { raw: "\x1b[1;1:1A" })]],
565
+ ["up release", "\x1b[1;1:3A", [k("up", { raw: "\x1b[1;1:3A", eventType: "release" })]],
566
+ ["ctrl+right", "\x1b[1;5:1C", [k("right", { raw: "\x1b[1;5:1C", ctrl: true })]],
567
+ ["shift+left", "\x1b[1;2:1D", [k("left", { raw: "\x1b[1;2:1D", shift: true })]],
568
+ ["home", "\x1b[1;1:1H", [k("home", { raw: "\x1b[1;1:1H" })]],
569
+ ["end release", "\x1b[1;1:3F", [k("end", { raw: "\x1b[1;1:3F", eventType: "release" })]],
570
+ ["f1 press", "\x1b[1;1:1P", [k("f1", { raw: "\x1b[1;1:1P" })]],
571
+ ])
572
+
573
+ // CSI number;modifier:event ~ format (kitty tilde keys)
574
+ table([
575
+ ["pageup press", "\x1b[5;1:1~", [k("pageup", { raw: "\x1b[5;1:1~" })]],
576
+ ["ctrl+delete", "\x1b[3;5:1~", [k("delete", { raw: "\x1b[3;5:1~", ctrl: true })]],
577
+ ["insert release", "\x1b[2;1:3~", [k("insert", { raw: "\x1b[2;1:3~", eventType: "release" })]],
578
+ ])
579
+ })
580
+
581
+ describe("modifyOtherKeys", () => {
582
+ table([
583
+ ["shift+return", "\x1b[27;2;13~", [k("return", { raw: "\x1b[27;2;13~", shift: true })]],
584
+ ["ctrl+return", "\x1b[27;5;13~", [k("return", { raw: "\x1b[27;5;13~", ctrl: true })]],
585
+ ["ctrl+escape", "\x1b[27;5;27~", [k("escape", { raw: "\x1b[27;5;27~", ctrl: true })]],
586
+ ["alt+tab", "\x1b[27;3;9~", [k("tab", { raw: "\x1b[27;3;9~", meta: true })]],
587
+ ["shift+space", "\x1b[27;2;32~", [k("space", { raw: "\x1b[27;2;32~", shift: true })]],
588
+ ["ctrl+backspace", "\x1b[27;5;127~", [k("backspace", { raw: "\x1b[27;5;127~", ctrl: true })]],
589
+ ["shift+digit 5", "\x1b[27;2;53~", [k("5", { raw: "\x1b[27;2;53~", shift: true })]],
590
+ ])
591
+ })
592
+
593
+ describe("mouse: SGR protocol", () => {
594
+ table([
595
+ // Button press/release
596
+ ["left down", "\x1b[<0;1;1M", [sgr("\x1b[<0;1;1M", "down", 0, 0)]],
597
+ ["left up", "\x1b[<0;1;1m", [sgr("\x1b[<0;1;1m", "up", 0, 0)]],
598
+ ["middle down", "\x1b[<1;1;1M", [sgr("\x1b[<1;1;1M", "down", 0, 0, { button: 1 })]],
599
+ ["middle up", "\x1b[<1;1;1m", [sgr("\x1b[<1;1;1m", "up", 0, 0, { button: 1 })]],
600
+ ["right down", "\x1b[<2;1;1M", [sgr("\x1b[<2;1;1M", "down", 0, 0, { button: 2 })]],
601
+ ["right up", "\x1b[<2;1;1m", [sgr("\x1b[<2;1;1m", "up", 0, 0, { button: 2 })]],
602
+ // Scroll
603
+ [
604
+ "scroll up",
605
+ "\x1b[<64;10;5M",
606
+ [sgr("\x1b[<64;10;5M", "scroll", 9, 4, { scroll: { direction: "up", delta: 1 } })],
607
+ ],
608
+ [
609
+ "scroll down",
610
+ "\x1b[<65;10;5M",
611
+ [sgr("\x1b[<65;10;5M", "scroll", 9, 4, { button: 1, scroll: { direction: "down", delta: 1 } })],
612
+ ],
613
+ [
614
+ "scroll left",
615
+ "\x1b[<66;10;5M",
616
+ [sgr("\x1b[<66;10;5M", "scroll", 9, 4, { button: 2, scroll: { direction: "left", delta: 1 } })],
617
+ ],
618
+ [
619
+ "scroll right",
620
+ "\x1b[<67;10;5M",
621
+ [sgr("\x1b[<67;10;5M", "scroll", 9, 4, { button: 0, scroll: { direction: "right", delta: 1 } })],
622
+ ],
623
+ // Motion (no button)
624
+ ["move", "\x1b[<35;20;10M", [sgr("\x1b[<35;20;10M", "move", 19, 9)]],
625
+ // Large coordinates
626
+ ["large coords", "\x1b[<0;300;200M", [sgr("\x1b[<0;300;200M", "down", 299, 199)]],
627
+ // Modifiers
628
+ ["shift+left down", "\x1b[<4;1;1M", [sgr("\x1b[<4;1;1M", "down", 0, 0, { mods: { shift: true } })]],
629
+ ["alt+left down", "\x1b[<8;1;1M", [sgr("\x1b[<8;1;1M", "down", 0, 0, { mods: { alt: true } })]],
630
+ ["ctrl+left down", "\x1b[<16;1;1M", [sgr("\x1b[<16;1;1M", "down", 0, 0, { mods: { ctrl: true } })]],
631
+ ])
632
+
633
+ test("drag detection after button down", () => {
634
+ const p = createParser()
635
+ try {
636
+ // Button 0 down, then motion with button 0 flag
637
+ p.push(Buffer.from("\x1b[<0;5;5M\x1b[<32;6;5M"))
638
+ const s = snap(p)
639
+ expect(s).toHaveLength(2)
640
+ expect(s[0]).toEqual(sgr("\x1b[<0;5;5M", "down", 4, 4))
641
+ expect(s[1]).toEqual(sgr("\x1b[<32;6;5M", "drag", 5, 4))
642
+ } finally {
643
+ p.destroy()
644
+ }
645
+ })
646
+
647
+ test("split SGR across two pushes", () => {
648
+ const p = createParser()
649
+ try {
650
+ p.push(Buffer.from("\x1b[<64;10;"))
651
+ expect(snap(p)).toEqual([])
652
+ p.push(Buffer.from("5M"))
653
+ expect(snap(p)).toEqual([sgr("\x1b[<64;10;5M", "scroll", 9, 4, { scroll: { direction: "up", delta: 1 } })])
654
+ } finally {
655
+ p.destroy()
656
+ }
657
+ })
658
+
659
+ test("multiple mouse events in one push", () => {
660
+ const p = createParser()
661
+ try {
662
+ p.push(Buffer.from("\x1b[<0;1;1M\x1b[<0;2;1M\x1b[<0;2;1m"))
663
+ const s = snap(p)
664
+ expect(s).toHaveLength(3)
665
+ expect(s[0]).toEqual(sgr("\x1b[<0;1;1M", "down", 0, 0))
666
+ expect(s[1]).toEqual(sgr("\x1b[<0;2;1M", "down", 1, 0))
667
+ expect(s[2]).toEqual(sgr("\x1b[<0;2;1m", "up", 1, 0))
668
+ } finally {
669
+ p.destroy()
670
+ }
671
+ })
672
+ })
673
+
674
+ describe("mouse: X10 protocol", () => {
675
+ // X10: ESC [ M <button+32> <x+33> <y+33>
676
+ const leftDown = x10bytes(0, 0, 0)
677
+ const middleDown = x10bytes(1, 0, 0)
678
+ const rightDown = x10bytes(2, 0, 0)
679
+ const release = x10bytes(3, 0, 0)
680
+ const at1020 = x10bytes(0, 10, 20)
681
+ const move = x10bytes(35, 4, 5)
682
+ const scrollUp = x10bytes(64, 2, 3)
683
+ const shiftLeftDown = x10bytes(4, 0, 0)
684
+ const ctrlScrollUp = x10bytes(80, 7, 8)
685
+
686
+ table([
687
+ ["left down (0,0)", leftDown, [x10m(latin1(leftDown), "down", 0, 0)]],
688
+ ["middle down", middleDown, [x10m(latin1(middleDown), "down", 0, 0, { button: 1 })]],
689
+ ["right down", rightDown, [x10m(latin1(rightDown), "down", 0, 0, { button: 2 })]],
690
+ ["release", release, [x10m(latin1(release), "up", 0, 0)]],
691
+ ["at position 10,20", at1020, [x10m(latin1(at1020), "down", 10, 20)]],
692
+ ["move with no button", move, [x10m(latin1(move), "move", 4, 5, { button: -1 })]],
693
+ ["scroll up", scrollUp, [x10m(latin1(scrollUp), "scroll", 2, 3, { scroll: { direction: "up", delta: 1 } })]],
694
+ ["shift+left down", shiftLeftDown, [x10m(latin1(shiftLeftDown), "down", 0, 0, { mods: { shift: true } })]],
695
+ [
696
+ "ctrl+scroll up",
697
+ ctrlScrollUp,
698
+ [x10m(latin1(ctrlScrollUp), "scroll", 7, 8, { mods: { ctrl: true }, scroll: { direction: "up", delta: 1 } })],
699
+ ],
700
+ ])
701
+
702
+ test("X10 mouse followed by key", () => {
703
+ const p = createParser()
704
+ try {
705
+ p.push(Buffer.from("\x1b[M !!x"))
706
+ const s = snap(p)
707
+ expect(s).toHaveLength(2)
708
+ expect(s[0]).toEqual(x10m("\x1b[M !!", "down", 0, 0))
709
+ expect(s[1]).toEqual(k("x"))
710
+ } finally {
711
+ p.destroy()
712
+ }
713
+ })
714
+
715
+ test("split X10 across pushes waits for payload", () => {
716
+ const p = createParser()
717
+ try {
718
+ p.push(Buffer.from("\x1b[M"))
719
+ expect(snap(p)).toEqual([])
720
+ p.push(Buffer.from(" !!"))
721
+ expect(snap(p)).toEqual([x10m("\x1b[M !!", "down", 0, 0)])
722
+ } finally {
723
+ p.destroy()
724
+ }
725
+ })
726
+
727
+ test("delayed X10 continuation after timed-out escape stays opaque", () => {
728
+ const { parser, clock } = createTimedParser()
729
+ try {
730
+ parser.push(Buffer.from("\x1b"))
731
+ expect(snap(parser)).toEqual([])
732
+ clock.advance(10)
733
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
734
+
735
+ parser.push(Buffer.from("[M"))
736
+ expect(snap(parser)).toEqual([])
737
+ parser.push(Buffer.from(" !!"))
738
+ expect(snap(parser)).toEqual([resp("unknown", "[M !!")])
739
+ } finally {
740
+ parser.destroy()
741
+ }
742
+ })
743
+ })
744
+
745
+ describe("UTF-8 handling", () => {
746
+ table([
747
+ ["2-byte (é)", "\u00e9", [k("\u00e9")]],
748
+ ["3-byte (中)", "\u4e2d", [k("\u4e2d")]],
749
+ ["4-byte (👍)", "👍", [k("👍")]],
750
+ ["multiple utf-8 chars", "日本語", [k("日"), k("本"), k("語")]],
751
+ ])
752
+
753
+ test("2-byte split at byte boundary", () => {
754
+ const bytes = Buffer.from("é")
755
+ expect(bytes.length).toBe(2)
756
+ const p = createParser()
757
+ try {
758
+ p.push(bytes.subarray(0, 1))
759
+ expect(snap(p)).toEqual([])
760
+ p.push(bytes.subarray(1))
761
+ expect(snap(p)).toEqual([k("é")])
762
+ } finally {
763
+ p.destroy()
764
+ }
765
+ })
766
+
767
+ test("3-byte split at every boundary", () => {
768
+ const bytes = Buffer.from("中")
769
+ expect(bytes.length).toBe(3)
770
+ for (let split = 1; split < bytes.length; split++) {
771
+ const p = createParser()
772
+ try {
773
+ p.push(bytes.subarray(0, split))
774
+ expect(snap(p)).toEqual([])
775
+ p.push(bytes.subarray(split))
776
+ expect(snap(p)).toEqual([k("中")])
777
+ } finally {
778
+ p.destroy()
779
+ }
780
+ }
781
+ })
782
+
783
+ test("4-byte split at every boundary", () => {
784
+ const bytes = Buffer.from("👍")
785
+ expect(bytes.length).toBe(4)
786
+ for (let split = 1; split < bytes.length; split++) {
787
+ const p = createParser()
788
+ try {
789
+ p.push(bytes.subarray(0, split))
790
+ expect(snap(p)).toEqual([])
791
+ p.push(bytes.subarray(split))
792
+ expect(snap(p)).toEqual([k("👍")])
793
+ } finally {
794
+ p.destroy()
795
+ }
796
+ }
797
+ })
798
+
799
+ test("invalid UTF-8 lead (0xC0) followed by ASCII falls back to legacy high-byte", () => {
800
+ const p = createParser()
801
+ try {
802
+ p.push(Uint8Array.from([0xc0, 0x41]))
803
+ const s = snap(p)
804
+ expect(s).toHaveLength(2)
805
+ // 0xC0 - 128 = 0x40 = '@', treated as ESC + '@' → legacy path
806
+ expect(s[0]!.type).toBe("key")
807
+ expect(s[1]).toEqual(k("a", { raw: "A", shift: true }))
808
+ } finally {
809
+ p.destroy()
810
+ }
811
+ })
812
+
813
+ test("invalid continuation byte after valid lead falls back to legacy", () => {
814
+ const p = createParser()
815
+ try {
816
+ p.push(Uint8Array.from([0xe9])) // valid 3-byte lead
817
+ expect(snap(p)).toEqual([]) // waits for continuation
818
+ p.push(Buffer.from("x")) // not a continuation byte
819
+ const s = snap(p)
820
+ expect(s).toEqual([
821
+ k("i", { raw: "\x1bi", meta: true }), // 0xe9 → legacy: 0xe9-128=0x69='i', ESC prefix
822
+ k("x"),
823
+ ])
824
+ } finally {
825
+ p.destroy()
826
+ }
827
+ })
828
+
829
+ test("legacy single high-byte on timeout", () => {
830
+ const { parser, clock } = createTimedParser()
831
+ try {
832
+ parser.push(Uint8Array.from([0xe9]))
833
+ expect(snap(parser)).toEqual([])
834
+ clock.advance(10)
835
+ expect(snap(parser)).toEqual([k("i", { raw: "\x1bi", meta: true })])
836
+ } finally {
837
+ parser.destroy()
838
+ }
839
+ })
840
+
841
+ test("high byte 0xFF on timeout → meta+backspace", () => {
842
+ const { parser, clock } = createTimedParser()
843
+ try {
844
+ parser.push(Uint8Array.from([0xff]))
845
+ expect(snap(parser)).toEqual([])
846
+ clock.advance(10)
847
+ // 0xFF - 128 = 0x7F = DEL, so ESC + DEL = meta+backspace
848
+ expect(snap(parser)).toEqual([k("backspace", { raw: "\x1b\x7f", meta: true })])
849
+ } finally {
850
+ parser.destroy()
851
+ }
852
+ })
853
+ })
854
+
855
+ describe("protocol responses", () => {
856
+ table([
857
+ // OSC (BEL-terminated)
858
+ ["OSC (BEL)", "\x1b]4;0;#ffffff\x07", [resp("osc", "\x1b]4;0;#ffffff\x07")]],
859
+ // OSC (ESC \\ terminated)
860
+ ["OSC (ST)", "\x1b]4;0;rgb:ff/ff/ff\x1b\\", [resp("osc", "\x1b]4;0;rgb:ff/ff/ff\x1b\\")]],
861
+ // DCS
862
+ ["DCS", "\x1bP>|kitty(0.40.1)\x1b\\", [resp("dcs", "\x1bP>|kitty(0.40.1)\x1b\\")]],
863
+ // APC
864
+ ["APC", "\x1b_Gi=1;OK\x1b\\", [resp("apc", "\x1b_Gi=1;OK\x1b\\")]],
865
+ // Focus
866
+ ["focus in", "\x1b[I", [resp("csi", "\x1b[I")]],
867
+ ["focus out", "\x1b[O", [resp("csi", "\x1b[O")]],
868
+ // DA (Device Attributes)
869
+ ["DA1", "\x1b[?62;1;2;6;7;8;9;15;22c", [resp("csi", "\x1b[?62;1;2;6;7;8;9;15;22c")]],
870
+ // CPR (Cursor Position Report)
871
+ ["CPR", "\x1b[24;80R", [resp("csi", "\x1b[24;80R")]],
872
+ // Window/cell size
873
+ ["window size", "\x1b[4;600;800t", [resp("csi", "\x1b[4;600;800t")]],
874
+ // Mode report
875
+ ["mode report", "\x1b[?2004;1$y", [resp("csi", "\x1b[?2004;1$y")]],
876
+ ])
877
+
878
+ test("all three protocol responses in one push", () => {
879
+ const p = createParser()
880
+ try {
881
+ p.push(Buffer.from("\x1b]4;0;#fff\x07\x1bP>|test\x1b\\\x1b_OK\x1b\\"))
882
+ expect(snap(p)).toEqual([
883
+ resp("osc", "\x1b]4;0;#fff\x07"),
884
+ resp("dcs", "\x1bP>|test\x1b\\"),
885
+ resp("apc", "\x1b_OK\x1b\\"),
886
+ ])
887
+ } finally {
888
+ p.destroy()
889
+ }
890
+ })
891
+
892
+ test("split OSC across pushes", () => {
893
+ const p = createParser()
894
+ try {
895
+ p.push(Buffer.from("\x1b]4;0;"))
896
+ expect(snap(p)).toEqual([])
897
+ p.push(Buffer.from("#ffffff\x07"))
898
+ expect(snap(p)).toEqual([resp("osc", "\x1b]4;0;#ffffff\x07")])
899
+ } finally {
900
+ p.destroy()
901
+ }
902
+ })
903
+
904
+ test("split DCS terminator ESC \\ across pushes", () => {
905
+ const p = createParser()
906
+ try {
907
+ p.push(Buffer.from("\x1bPtest\x1b"))
908
+ expect(snap(p)).toEqual([])
909
+ p.push(Buffer.from("\\"))
910
+ expect(snap(p)).toEqual([resp("dcs", "\x1bPtest\x1b\\")])
911
+ } finally {
912
+ p.destroy()
913
+ }
914
+ })
915
+
916
+ test("focus events interleaved with keys", () => {
917
+ const p = createParser()
918
+ try {
919
+ p.push(Buffer.from("a\x1b[Ib\x1b[Oc"))
920
+ expect(snap(p)).toEqual([k("a"), resp("csi", "\x1b[I"), k("b"), resp("csi", "\x1b[O"), k("c")])
921
+ } finally {
922
+ p.destroy()
923
+ }
924
+ })
925
+
926
+ test("partial OSC flushes on timeout as unknown", () => {
927
+ const { parser, clock } = createTimedParser()
928
+ try {
929
+ parser.push(Buffer.from("\x1b]incomplete"))
930
+ expect(snap(parser)).toEqual([])
931
+ clock.advance(10)
932
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b]incomplete")])
933
+ } finally {
934
+ parser.destroy()
935
+ }
936
+ })
937
+
938
+ test("partial DCS flushes on timeout as unknown", () => {
939
+ const { parser, clock } = createTimedParser()
940
+ try {
941
+ parser.push(Buffer.from("\x1bPpartial"))
942
+ expect(snap(parser)).toEqual([])
943
+ clock.advance(10)
944
+ expect(snap(parser)).toEqual([resp("unknown", "\x1bPpartial")])
945
+ } finally {
946
+ parser.destroy()
947
+ }
948
+ })
949
+
950
+ test("partial APC flushes on timeout as unknown", () => {
951
+ const { parser, clock } = createTimedParser()
952
+ try {
953
+ parser.push(Buffer.from("\x1b_partial"))
954
+ expect(snap(parser)).toEqual([])
955
+ clock.advance(10)
956
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b_partial")])
957
+ } finally {
958
+ parser.destroy()
959
+ }
960
+ })
961
+
962
+ test("partial generic CSI flushes on timeout as unknown", () => {
963
+ const { parser, clock } = createTimedParser()
964
+ try {
965
+ parser.push(Buffer.from("\x1b[123"))
966
+ expect(snap(parser)).toEqual([])
967
+ clock.advance(10)
968
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b[123")])
969
+ } finally {
970
+ parser.destroy()
971
+ }
972
+ })
973
+
974
+ test("partial kitty CSI stays pending after timeout", () => {
975
+ const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
976
+ try {
977
+ parser.push(Buffer.from("\x1b[118;5"))
978
+ expect(snap(parser)).toEqual([])
979
+ clock.advance(10)
980
+ expect(snap(parser)).toEqual([])
981
+ } finally {
982
+ parser.destroy()
983
+ }
984
+ })
985
+
986
+ test("partial kitty CSI stays pending after timeout when split after first semicolon", () => {
987
+ const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
988
+ try {
989
+ parser.push(Buffer.from("\x1b[97;"))
990
+ expect(snap(parser)).toEqual([])
991
+ clock.advance(10)
992
+ expect(snap(parser)).toEqual([])
993
+
994
+ parser.push(Buffer.from("2u"))
995
+ expect(snap(parser)).toEqual([k("a", { shift: true, raw: "\x1b[97;2u" })])
996
+ } finally {
997
+ parser.destroy()
998
+ }
999
+ })
1000
+
1001
+ test("partial kitty alternate-key CSI stays pending after timeout", () => {
1002
+ const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1003
+ try {
1004
+ parser.push(Buffer.from("\x1b[97:65;"))
1005
+ expect(snap(parser)).toEqual([])
1006
+ clock.advance(10)
1007
+ expect(snap(parser)).toEqual([])
1008
+
1009
+ parser.push(Buffer.from("6:1u"))
1010
+ expect(snap(parser)).toEqual([k("a", { raw: "\x1b[97:65;6:1u", ctrl: true, shift: true })])
1011
+ } finally {
1012
+ parser.destroy()
1013
+ }
1014
+ })
1015
+
1016
+ test("partial kitty CSI stays pending after timeout for higher modifier bits", () => {
1017
+ const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1018
+ try {
1019
+ parser.push(Buffer.from("\x1b[97;9"))
1020
+ expect(snap(parser)).toEqual([])
1021
+ clock.advance(10)
1022
+ expect(snap(parser)).toEqual([])
1023
+
1024
+ parser.push(Buffer.from("u"))
1025
+ const event = parser.read()
1026
+ expect(event?.type).toBe("key")
1027
+ if (!event || event.type !== "key") throw new Error("expected key event")
1028
+ expect(event.raw).toBe("\x1b[97;9u")
1029
+ expect(event.key.name).toBe("a")
1030
+ expect(event.key.super).toBe(true)
1031
+ expect(parser.read()).toBeNull()
1032
+ } finally {
1033
+ parser.destroy()
1034
+ }
1035
+ })
1036
+
1037
+ test("partial kitty special-key CSI stays pending after timeout", () => {
1038
+ const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1039
+ try {
1040
+ parser.push(Buffer.from("\x1b[1;1:"))
1041
+ expect(snap(parser)).toEqual([])
1042
+ clock.advance(10)
1043
+ expect(snap(parser)).toEqual([])
1044
+
1045
+ parser.push(Buffer.from("3A"))
1046
+ expect(snap(parser)).toEqual([k("up", { raw: "\x1b[1;1:3A", eventType: "release" })])
1047
+ } finally {
1048
+ parser.destroy()
1049
+ }
1050
+ })
1051
+
1052
+ test("partial SGR mouse CSI stays pending after timeout", () => {
1053
+ const { parser, clock } = createTimedParser()
1054
+ try {
1055
+ parser.push(Buffer.from("\x1b[<35;20"))
1056
+ expect(snap(parser)).toEqual([])
1057
+ clock.advance(10)
1058
+ expect(snap(parser)).toEqual([])
1059
+
1060
+ parser.push(Buffer.from(";5m"))
1061
+ expect(snap(parser)).toEqual([sgr("\x1b[<35;20;5m", "move", 19, 4)])
1062
+ } finally {
1063
+ parser.destroy()
1064
+ }
1065
+ })
1066
+
1067
+ test("split CSI across reads reassembles after timeout", () => {
1068
+ const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1069
+ try {
1070
+ // Kitty Ctrl+V release split across two reads
1071
+ parser.push(Buffer.from("\x1b[118;5"))
1072
+ expect(snap(parser)).toEqual([])
1073
+ clock.advance(10)
1074
+ // Stays pending — not flushed
1075
+ expect(snap(parser)).toEqual([])
1076
+ parser.push(Buffer.from(";3u"))
1077
+ expect(snap(parser)).toEqual([k("v", { ctrl: true, raw: "\x1b[118;5;3u" })])
1078
+ } finally {
1079
+ parser.destroy()
1080
+ }
1081
+ })
1082
+
1083
+ test("split kitty escape CSI across reads reassembles after timeout", () => {
1084
+ const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1085
+ try {
1086
+ parser.push(Buffer.from("\x1b[27;5"))
1087
+ expect(snap(parser)).toEqual([])
1088
+ clock.advance(10)
1089
+ expect(snap(parser)).toEqual([])
1090
+ parser.push(Buffer.from("u"))
1091
+ expect(snap(parser)).toEqual([k("escape", { ctrl: true, raw: "\x1b[27;5u" })])
1092
+ } finally {
1093
+ parser.destroy()
1094
+ }
1095
+ })
1096
+
1097
+ test("timed-out standard one-semicolon CSI key flushes before later text", () => {
1098
+ const { parser, clock } = createTimedParser()
1099
+ try {
1100
+ parser.push(Buffer.from("\x1b[1;5"))
1101
+ expect(snap(parser)).toEqual([])
1102
+ clock.advance(10)
1103
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b[1;5")])
1104
+
1105
+ parser.push(Buffer.from("A"))
1106
+ expect(snap(parser)).toEqual([k("a", { raw: "A", shift: true })])
1107
+ } finally {
1108
+ parser.destroy()
1109
+ }
1110
+ })
1111
+
1112
+ test("timed-out one-semicolon CSI response flushes before later text", () => {
1113
+ const { parser, clock } = createTimedParser()
1114
+ try {
1115
+ parser.push(Buffer.from("\x1b[24;80"))
1116
+ expect(snap(parser)).toEqual([])
1117
+ clock.advance(10)
1118
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b[24;80")])
1119
+
1120
+ parser.push(Buffer.from("R"))
1121
+ expect(snap(parser)).toEqual([k("r", { raw: "R", shift: true })])
1122
+ } finally {
1123
+ parser.destroy()
1124
+ }
1125
+ })
1126
+
1127
+ test("timed-out partial kitty CSI resyncs on a later ESC", () => {
1128
+ const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1129
+ try {
1130
+ parser.push(Buffer.from("\x1b[118;5"))
1131
+ expect(snap(parser)).toEqual([])
1132
+ clock.advance(10)
1133
+ expect(snap(parser)).toEqual([])
1134
+
1135
+ parser.push(Buffer.from("\x1b[A"))
1136
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b[118;5"), k("up", { raw: "\x1b[A" })])
1137
+ } finally {
1138
+ parser.destroy()
1139
+ }
1140
+ })
1141
+
1142
+ test("timed-out partial kitty CSI flushes before unrelated later text", () => {
1143
+ const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1144
+ try {
1145
+ parser.push(Buffer.from("\x1b[118;5"))
1146
+ expect(snap(parser)).toEqual([])
1147
+ clock.advance(10)
1148
+ expect(snap(parser)).toEqual([])
1149
+
1150
+ parser.push(Buffer.from("a"))
1151
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b[118;5"), k("a")])
1152
+ } finally {
1153
+ parser.destroy()
1154
+ }
1155
+ })
1156
+
1157
+ test("partial generic CSI timeout flush does not swallow later text", () => {
1158
+ const { parser, clock } = createTimedParser()
1159
+ try {
1160
+ parser.push(Buffer.from("\x1b[123"))
1161
+ expect(snap(parser)).toEqual([])
1162
+ clock.advance(10)
1163
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b[123")])
1164
+
1165
+ parser.push(Buffer.from("a"))
1166
+ expect(snap(parser)).toEqual([k("a")])
1167
+ } finally {
1168
+ parser.destroy()
1169
+ }
1170
+ })
1171
+
1172
+ test("partial large-parameter CSI flushes on timeout before later text", () => {
1173
+ const { parser, clock } = createTimedParser()
1174
+ try {
1175
+ parser.push(Buffer.from("\x1b[80;120"))
1176
+ expect(snap(parser)).toEqual([])
1177
+ clock.advance(10)
1178
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b[80;120")])
1179
+
1180
+ parser.push(Buffer.from("a"))
1181
+ expect(snap(parser)).toEqual([k("a")])
1182
+ } finally {
1183
+ parser.destroy()
1184
+ }
1185
+ })
1186
+
1187
+ test("partial OSC timeout flush does not swallow later text", () => {
1188
+ const { parser, clock } = createTimedParser()
1189
+ try {
1190
+ parser.push(Buffer.from("\x1b]52;c;"))
1191
+ expect(snap(parser)).toEqual([])
1192
+ clock.advance(10)
1193
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b]52;c;")])
1194
+
1195
+ parser.push(Buffer.from("abc"))
1196
+ expect(snap(parser)).toEqual([k("a"), k("b"), k("c")])
1197
+ } finally {
1198
+ parser.destroy()
1199
+ }
1200
+ })
1201
+
1202
+ test("partial OSC timeout flush does not swallow later escape sequences", () => {
1203
+ const { parser, clock } = createTimedParser()
1204
+ try {
1205
+ parser.push(Buffer.from("\x1b]52;c;"))
1206
+ expect(snap(parser)).toEqual([])
1207
+ clock.advance(10)
1208
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b]52;c;")])
1209
+
1210
+ parser.push(Buffer.from("\x1b[A"))
1211
+ expect(snap(parser)).toEqual([k("up", { raw: "\x1b[A" })])
1212
+ } finally {
1213
+ parser.destroy()
1214
+ }
1215
+ })
1216
+ })
1217
+
1218
+ describe("protocol context", () => {
1219
+ test("partial explicit-width CPR stays pending after timeout when probe is active", () => {
1220
+ const { parser, clock } = createTimedParser({
1221
+ protocolContext: { explicitWidthCprActive: true },
1222
+ })
1223
+
1224
+ try {
1225
+ parser.push(Buffer.from("\x1b[1;2"))
1226
+ expect(snap(parser)).toEqual([])
1227
+ clock.advance(10)
1228
+ expect(snap(parser)).toEqual([])
1229
+
1230
+ parser.push(Buffer.from("R"))
1231
+ expect(snap(parser)).toEqual([resp("csi", "\x1b[1;2R")])
1232
+ } finally {
1233
+ parser.destroy()
1234
+ }
1235
+ })
1236
+
1237
+ test("partial explicit-width CPR flushes before later text when probe is inactive", () => {
1238
+ const { parser, clock } = createTimedParser()
1239
+
1240
+ try {
1241
+ parser.push(Buffer.from("\x1b[1;2"))
1242
+ expect(snap(parser)).toEqual([])
1243
+ clock.advance(10)
1244
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b[1;2")])
1245
+
1246
+ parser.push(Buffer.from("R"))
1247
+ expect(snap(parser)).toEqual([k("r", { raw: "R", shift: true })])
1248
+ } finally {
1249
+ parser.destroy()
1250
+ }
1251
+ })
1252
+
1253
+ test("partial pixel resolution response stays pending after timeout while query is active", () => {
1254
+ const { parser, clock } = createTimedParser({
1255
+ protocolContext: { pixelResolutionQueryActive: true },
1256
+ })
1257
+
1258
+ try {
1259
+ parser.push(Buffer.from("\x1b[4;1080;192"))
1260
+ expect(snap(parser)).toEqual([])
1261
+ clock.advance(10)
1262
+ expect(snap(parser)).toEqual([])
1263
+
1264
+ parser.push(Buffer.from("0t"))
1265
+ expect(snap(parser)).toEqual([resp("csi", "\x1b[4;1080;1920t")])
1266
+ } finally {
1267
+ parser.destroy()
1268
+ }
1269
+ })
1270
+
1271
+ test("partial DECRPM stays pending after timeout while capability probe is active", () => {
1272
+ const { parser, clock } = createTimedParser({
1273
+ protocolContext: { privateCapabilityRepliesActive: true },
1274
+ })
1275
+
1276
+ try {
1277
+ parser.push(Buffer.from("\x1b[?1016;2$"))
1278
+ expect(snap(parser)).toEqual([])
1279
+ clock.advance(10)
1280
+ expect(snap(parser)).toEqual([])
1281
+
1282
+ parser.push(Buffer.from("y"))
1283
+ expect(snap(parser)).toEqual([resp("csi", "\x1b[?1016;2$y")])
1284
+ } finally {
1285
+ parser.destroy()
1286
+ }
1287
+ })
1288
+
1289
+ test("partial DA1 stays pending after timeout while capability probe is active", () => {
1290
+ const { parser, clock } = createTimedParser({
1291
+ protocolContext: { privateCapabilityRepliesActive: true },
1292
+ })
1293
+
1294
+ try {
1295
+ parser.push(Buffer.from("\x1b[?62;"))
1296
+ expect(snap(parser)).toEqual([])
1297
+ clock.advance(10)
1298
+ expect(snap(parser)).toEqual([])
1299
+
1300
+ parser.push(Buffer.from("c"))
1301
+ expect(snap(parser)).toEqual([resp("csi", "\x1b[?62;c")])
1302
+ } finally {
1303
+ parser.destroy()
1304
+ }
1305
+ })
1306
+
1307
+ test("timed-out modified CSI key still flushes before later final byte", () => {
1308
+ const { parser, clock } = createTimedParser({
1309
+ protocolContext: { explicitWidthCprActive: true },
1310
+ })
1311
+
1312
+ try {
1313
+ parser.push(Buffer.from("\x1b[1;5"))
1314
+ expect(snap(parser)).toEqual([])
1315
+ clock.advance(10)
1316
+ expect(snap(parser)).toEqual([])
1317
+
1318
+ parser.push(Buffer.from("A"))
1319
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b[1;5"), k("a", { raw: "A", shift: true })])
1320
+ } finally {
1321
+ parser.destroy()
1322
+ }
1323
+ })
1324
+
1325
+ test("generic row/col CPR does not reassemble during explicit-width probe window", () => {
1326
+ const { parser, clock } = createTimedParser({
1327
+ protocolContext: { explicitWidthCprActive: true },
1328
+ })
1329
+
1330
+ try {
1331
+ parser.push(Buffer.from("\x1b[24;80"))
1332
+ expect(snap(parser)).toEqual([])
1333
+ clock.advance(10)
1334
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b[24;80")])
1335
+
1336
+ parser.push(Buffer.from("R"))
1337
+ expect(snap(parser)).toEqual([k("r", { raw: "R", shift: true })])
1338
+ } finally {
1339
+ parser.destroy()
1340
+ }
1341
+ })
1342
+
1343
+ test("deferred explicit-width CPR flushes when probe context is cleared", () => {
1344
+ const { parser, clock } = createTimedParser({
1345
+ protocolContext: { explicitWidthCprActive: true },
1346
+ })
1347
+
1348
+ try {
1349
+ parser.push(Buffer.from("\x1b[1;2"))
1350
+ expect(snap(parser)).toEqual([])
1351
+ clock.advance(10)
1352
+ expect(snap(parser)).toEqual([])
1353
+
1354
+ parser.updateProtocolContext({ explicitWidthCprActive: false })
1355
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b[1;2")])
1356
+ } finally {
1357
+ parser.destroy()
1358
+ }
1359
+ })
1360
+
1361
+ test("timed-out pending explicit-width CPR does not rearm until more bytes arrive", () => {
1362
+ const clock = new ManualClock()
1363
+ let timeoutFlushes = 0
1364
+ let parser!: StdinParser
1365
+ parser = new StdinParser({
1366
+ armTimeouts: true,
1367
+ clock,
1368
+ timeoutMs: TEST_TIMEOUT_MS,
1369
+ protocolContext: { explicitWidthCprActive: true },
1370
+ onTimeoutFlush: () => {
1371
+ timeoutFlushes += 1
1372
+ parser.drain(() => {})
1373
+ },
1374
+ })
1375
+
1376
+ try {
1377
+ parser.push(Buffer.from("\x1b[1;2"))
1378
+ clock.advance(10)
1379
+ expect(timeoutFlushes).toBe(1)
1380
+
1381
+ clock.advance(50)
1382
+ expect(timeoutFlushes).toBe(1)
1383
+ expect(snap(parser)).toEqual([])
1384
+
1385
+ parser.push(Buffer.from(";"))
1386
+ clock.advance(10)
1387
+ expect(timeoutFlushes).toBe(2)
1388
+ } finally {
1389
+ parser.destroy()
1390
+ }
1391
+ })
1392
+
1393
+ test("timed-out pending private reply does not rearm until more bytes arrive", () => {
1394
+ const clock = new ManualClock()
1395
+ let timeoutFlushes = 0
1396
+ let parser!: StdinParser
1397
+ parser = new StdinParser({
1398
+ armTimeouts: true,
1399
+ clock,
1400
+ timeoutMs: TEST_TIMEOUT_MS,
1401
+ protocolContext: { privateCapabilityRepliesActive: true },
1402
+ onTimeoutFlush: () => {
1403
+ timeoutFlushes += 1
1404
+ parser.drain(() => {})
1405
+ },
1406
+ })
1407
+
1408
+ try {
1409
+ parser.push(Buffer.from("\x1b[?1016;2$"))
1410
+ clock.advance(10)
1411
+ expect(timeoutFlushes).toBe(1)
1412
+
1413
+ clock.advance(50)
1414
+ expect(timeoutFlushes).toBe(1)
1415
+ expect(snap(parser)).toEqual([])
1416
+
1417
+ parser.push(Buffer.from(";"))
1418
+ clock.advance(10)
1419
+ expect(timeoutFlushes).toBe(2)
1420
+ } finally {
1421
+ parser.destroy()
1422
+ }
1423
+ })
1424
+ })
1425
+
1426
+ describe("bracketed paste", () => {
1427
+ table([
1428
+ ["simple paste", "\x1b[200~hello\x1b[201~", [paste("hello")]],
1429
+ ["empty paste", "\x1b[200~\x1b[201~", [paste("")]],
1430
+ ["paste with newlines", "\x1b[200~line1\nline2\x1b[201~", [paste("line1\nline2")]],
1431
+ ["paste with tabs", "\x1b[200~a\tb\x1b[201~", [paste("a\tb")]],
1432
+ ["paste with ESC in body", "\x1b[200~abc\x1bdef\x1b[201~", [paste("abc\x1bdef")]],
1433
+ ])
1434
+
1435
+ test("split paste start marker across pushes", () => {
1436
+ const start = "\x1b[200~"
1437
+ for (let split = 1; split < start.length; split++) {
1438
+ const p = createParser()
1439
+ try {
1440
+ p.push(Buffer.from(start.slice(0, split)))
1441
+ p.push(Buffer.from(start.slice(split) + "hi\x1b[201~"))
1442
+ expect(snap(p)).toEqual([paste("hi")])
1443
+ } finally {
1444
+ p.destroy()
1445
+ }
1446
+ }
1447
+ })
1448
+
1449
+ test("split paste end marker at every boundary", () => {
1450
+ const end = "\x1b[201~"
1451
+ for (let split = 1; split < end.length; split++) {
1452
+ const p = createParser()
1453
+ try {
1454
+ p.push(Buffer.from("\x1b[200~hello"))
1455
+ p.push(Buffer.from(end.slice(0, split)))
1456
+ expect(snap(p)).toEqual([])
1457
+ p.push(Buffer.from(end.slice(split)))
1458
+ expect(snap(p)).toEqual([paste("hello")])
1459
+ } finally {
1460
+ p.destroy()
1461
+ }
1462
+ }
1463
+ })
1464
+
1465
+ test("paste body bytes do not alias caller buffers across pushes", () => {
1466
+ const p = createParser()
1467
+ try {
1468
+ p.push(Buffer.from("\x1b[200~"))
1469
+
1470
+ const chunk = Buffer.from("hello")
1471
+ p.push(chunk)
1472
+ chunk.fill(0x78)
1473
+
1474
+ p.push(Buffer.from("\x1b[201~"))
1475
+ expect(snap(p)).toEqual([paste("hello")])
1476
+ } finally {
1477
+ p.destroy()
1478
+ }
1479
+ })
1480
+
1481
+ test("near-match end markers are part of paste body", () => {
1482
+ const p = createParser()
1483
+ try {
1484
+ p.push(Buffer.from("\x1b[200~abc\x1b[202~def\x1b[201~"))
1485
+ expect(snap(p)).toEqual([paste("abc\x1b[202~def")])
1486
+ } finally {
1487
+ p.destroy()
1488
+ }
1489
+ })
1490
+
1491
+ test("doubled ESC before paste end marker", () => {
1492
+ const p = createParser()
1493
+ try {
1494
+ p.push(Buffer.from("\x1b[200~abc\x1b"))
1495
+ expect(snap(p)).toEqual([])
1496
+ p.push(Buffer.from("\x1b[201~"))
1497
+ expect(snap(p)).toEqual([paste("abc\x1b")])
1498
+ } finally {
1499
+ p.destroy()
1500
+ }
1501
+ })
1502
+
1503
+ test("large paste does not grow parser buffer", () => {
1504
+ const p = createParser({ maxPendingBytes: 32 })
1505
+ const payload = "x".repeat(100_000)
1506
+ try {
1507
+ p.push(Buffer.from(`\x1b[200~${payload}\x1b[201~z`))
1508
+ expect(snap(p)).toEqual([paste(payload), k("z")])
1509
+ expect(p.bufferCapacity).toBeLessThanOrEqual(512)
1510
+ } finally {
1511
+ p.destroy()
1512
+ }
1513
+ })
1514
+
1515
+ test("large paste across many small chunks", () => {
1516
+ const p = createParser({ maxPendingBytes: 32 })
1517
+ try {
1518
+ p.push(Buffer.from("\x1b[200~"))
1519
+ for (let i = 0; i < 1000; i++) p.push(Buffer.from("chunk "))
1520
+ p.push(Buffer.from("\x1b[201~"))
1521
+ const s = snap(p)
1522
+ expect(s).toHaveLength(1)
1523
+ expect(s[0]!.type).toBe("paste")
1524
+ expect((s[0] as PasteSnap).bytes).toHaveLength(6000)
1525
+ expect(p.bufferCapacity).toBeLessThanOrEqual(512)
1526
+ } finally {
1527
+ p.destroy()
1528
+ }
1529
+ })
1530
+
1531
+ test("trailing bytes after paste end are parsed normally", () => {
1532
+ const p = createParser()
1533
+ try {
1534
+ p.push(Buffer.from("\x1b[200~hello\x1b[201~\x1b[A"))
1535
+ expect(snap(p)).toEqual([paste("hello"), k("up", { raw: "\x1b[A" })])
1536
+ } finally {
1537
+ p.destroy()
1538
+ }
1539
+ })
1540
+
1541
+ test("back-to-back pastes", () => {
1542
+ const p = createParser()
1543
+ try {
1544
+ p.push(Buffer.from("\x1b[200~first\x1b[201~\x1b[200~second\x1b[201~"))
1545
+ expect(snap(p)).toEqual([paste("first"), paste("second")])
1546
+ } finally {
1547
+ p.destroy()
1548
+ }
1549
+ })
1550
+
1551
+ test("paste with UTF-8 content", () => {
1552
+ const p = createParser()
1553
+ try {
1554
+ p.push(Buffer.from("\x1b[200~日本語👍\x1b[201~"))
1555
+ expect(snap(p)).toEqual([paste("日本語👍")])
1556
+ } finally {
1557
+ p.destroy()
1558
+ }
1559
+ })
1560
+
1561
+ test("paste with UTF-8 split across chunks", () => {
1562
+ const p = createParser()
1563
+ const emoji = Buffer.from("👍")
1564
+ try {
1565
+ p.push(Buffer.from("\x1b[200~"))
1566
+ p.push(emoji.subarray(0, 2))
1567
+ p.push(emoji.subarray(2))
1568
+ p.push(Buffer.from("\x1b[201~"))
1569
+ expect(snap(p)).toEqual([paste("👍")])
1570
+ } finally {
1571
+ p.destroy()
1572
+ }
1573
+ })
1574
+ })
1575
+
1576
+ describe("ESC-less SGR continuation recovery", () => {
1577
+ test("after timed-out ESC, continuation is not split into text", () => {
1578
+ const { parser, clock } = createTimedParser()
1579
+ try {
1580
+ parser.push(Buffer.from("\x1b"))
1581
+ clock.advance(10)
1582
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1583
+
1584
+ parser.push(Buffer.from("[<35;20;5m"))
1585
+ expect(snap(parser)).toEqual([resp("unknown", "[<35;20;5m")])
1586
+ } finally {
1587
+ parser.destroy()
1588
+ }
1589
+ })
1590
+
1591
+ test("after timed-out ESC, split continuation across pushes is not split into text", () => {
1592
+ const { parser, clock } = createTimedParser()
1593
+ try {
1594
+ parser.push(Buffer.from("\x1b"))
1595
+ clock.advance(10)
1596
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1597
+
1598
+ parser.push(Buffer.from("["))
1599
+ expect(snap(parser)).toEqual([])
1600
+
1601
+ parser.push(Buffer.from("<35;20;5m"))
1602
+ expect(snap(parser)).toEqual([resp("unknown", "[<35;20;5m")])
1603
+ } finally {
1604
+ parser.destroy()
1605
+ }
1606
+ })
1607
+
1608
+ test("after timed-out ESC, partial [< waits, then timeout flushes as one response", () => {
1609
+ const { parser, clock } = createTimedParser()
1610
+ try {
1611
+ parser.push(Buffer.from("\x1b"))
1612
+ clock.advance(10)
1613
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1614
+
1615
+ parser.push(Buffer.from("[<35;20"))
1616
+ expect(snap(parser)).toEqual([])
1617
+ clock.advance(10)
1618
+ expect(snap(parser)).toEqual([resp("unknown", "[<35;20")])
1619
+ } finally {
1620
+ parser.destroy()
1621
+ }
1622
+ })
1623
+
1624
+ test("after timed-out ESC, [< followed by non-digit aborts immediately", () => {
1625
+ const { parser, clock } = createTimedParser()
1626
+ try {
1627
+ parser.push(Buffer.from("\x1b"))
1628
+ clock.advance(10)
1629
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1630
+
1631
+ parser.push(Buffer.from("[<x"))
1632
+ const s = snap(parser)
1633
+ expect(s).toHaveLength(2)
1634
+ expect(s[0]).toEqual(resp("unknown", "[<"))
1635
+ expect(s[1]).toEqual(k("x"))
1636
+ } finally {
1637
+ parser.destroy()
1638
+ }
1639
+ })
1640
+
1641
+ test("without prior flushed ESC, [< stays literal text", () => {
1642
+ const p = createParser()
1643
+ try {
1644
+ p.push(Buffer.from("[<35;20;5m"))
1645
+ expect(snap(p)).toEqual("[<35;20;5m".split("").map((char) => k(char)))
1646
+ } finally {
1647
+ p.destroy()
1648
+ }
1649
+ })
1650
+
1651
+ test("without prior flushed ESC, standalone [ then < stay as individual keys", () => {
1652
+ const p = createParser()
1653
+ try {
1654
+ p.push(Buffer.from("["))
1655
+ expect(snap(p)).toEqual([k("[")])
1656
+ p.push(Buffer.from("<"))
1657
+ expect(snap(p)).toEqual([k("<")])
1658
+ } finally {
1659
+ p.destroy()
1660
+ }
1661
+ })
1662
+
1663
+ test("after timed-out ESC, bare [ waits for more and then flushes as text", () => {
1664
+ const { parser, clock } = createTimedParser()
1665
+ try {
1666
+ parser.push(Buffer.from("\x1b"))
1667
+ clock.advance(10)
1668
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1669
+
1670
+ parser.push(Buffer.from("["))
1671
+ expect(snap(parser)).toEqual([])
1672
+ clock.advance(10)
1673
+ expect(snap(parser)).toEqual([k("[")])
1674
+ } finally {
1675
+ parser.destroy()
1676
+ }
1677
+ })
1678
+ })
1679
+
1680
+ describe("timeout behavior", () => {
1681
+ test("default timeout at exact boundary (19ms no fire, 20ms fires)", () => {
1682
+ const clock = new ManualClock()
1683
+ const parser = new StdinParser({ armTimeouts: true, clock })
1684
+ try {
1685
+ parser.push(Buffer.from("\x1b"))
1686
+ clock.advance(19)
1687
+ expect(snap(parser)).toEqual([])
1688
+ clock.advance(1)
1689
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1690
+ } finally {
1691
+ parser.destroy()
1692
+ }
1693
+ })
1694
+
1695
+ test("configured timeout at exact boundary (9ms no fire, 10ms fires)", () => {
1696
+ const { parser, clock } = createTimedParser()
1697
+ try {
1698
+ parser.push(Buffer.from("\x1b"))
1699
+ clock.advance(9)
1700
+ expect(snap(parser)).toEqual([])
1701
+ clock.advance(1)
1702
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1703
+ } finally {
1704
+ parser.destroy()
1705
+ }
1706
+ })
1707
+
1708
+ test("flushTimeout() only flushes when caller reports elapsed timeout", () => {
1709
+ const parser = createParser({ timeoutMs: TEST_TIMEOUT_MS })
1710
+ try {
1711
+ parser.push(Buffer.from("\x1b"))
1712
+
1713
+ parser.flushTimeout(TEST_TIMEOUT_MS - 1)
1714
+ expect(snap(parser)).toEqual([])
1715
+
1716
+ parser.flushTimeout(TEST_TIMEOUT_MS)
1717
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1718
+ } finally {
1719
+ parser.destroy()
1720
+ }
1721
+ })
1722
+
1723
+ test("timeout resets when more bytes arrive", () => {
1724
+ const { parser, clock } = createTimedParser()
1725
+ try {
1726
+ parser.push(Buffer.from("\x1b[<35;20;"))
1727
+ clock.advance(9) // almost timeout
1728
+ parser.push(Buffer.from("5")) // new byte resets timer
1729
+ expect(snap(parser)).toEqual([])
1730
+ clock.advance(9) // almost timeout again
1731
+ expect(snap(parser)).toEqual([])
1732
+ parser.push(Buffer.from("m")) // complete
1733
+ expect(snap(parser)).toEqual([sgr("\x1b[<35;20;5m", "move", 19, 4)])
1734
+ } finally {
1735
+ parser.destroy()
1736
+ }
1737
+ })
1738
+
1739
+ test("timed-out pending kitty CSI does not rearm until more bytes arrive", () => {
1740
+ const clock = new ManualClock()
1741
+ let timeoutFlushes = 0
1742
+ let parser!: StdinParser
1743
+ parser = new StdinParser({
1744
+ armTimeouts: true,
1745
+ clock,
1746
+ timeoutMs: TEST_TIMEOUT_MS,
1747
+ protocolContext: { kittyKeyboardEnabled: true },
1748
+ onTimeoutFlush: () => {
1749
+ timeoutFlushes += 1
1750
+ parser.drain(() => {})
1751
+ },
1752
+ })
1753
+
1754
+ try {
1755
+ parser.push(Buffer.from("\x1b[118;5"))
1756
+ clock.advance(10)
1757
+ expect(timeoutFlushes).toBe(1)
1758
+
1759
+ clock.advance(50)
1760
+ expect(timeoutFlushes).toBe(1)
1761
+ expect(snap(parser)).toEqual([])
1762
+
1763
+ parser.push(Buffer.from(";"))
1764
+ clock.advance(10)
1765
+ expect(timeoutFlushes).toBe(2)
1766
+ } finally {
1767
+ parser.destroy()
1768
+ }
1769
+ })
1770
+
1771
+ test("timed-out pending SGR mouse CSI does not rearm until more bytes arrive", () => {
1772
+ const clock = new ManualClock()
1773
+ let timeoutFlushes = 0
1774
+ let parser!: StdinParser
1775
+ parser = new StdinParser({
1776
+ armTimeouts: true,
1777
+ clock,
1778
+ timeoutMs: TEST_TIMEOUT_MS,
1779
+ onTimeoutFlush: () => {
1780
+ timeoutFlushes += 1
1781
+ parser.drain(() => {})
1782
+ },
1783
+ })
1784
+
1785
+ try {
1786
+ parser.push(Buffer.from("\x1b[<35;20"))
1787
+ clock.advance(10)
1788
+ expect(timeoutFlushes).toBe(1)
1789
+
1790
+ clock.advance(50)
1791
+ expect(timeoutFlushes).toBe(1)
1792
+ expect(snap(parser)).toEqual([])
1793
+
1794
+ parser.push(Buffer.from(";"))
1795
+ clock.advance(10)
1796
+ expect(timeoutFlushes).toBe(2)
1797
+ } finally {
1798
+ parser.destroy()
1799
+ }
1800
+ })
1801
+
1802
+ test("timeout does not fire during paste mode", () => {
1803
+ const { parser, clock } = createTimedParser()
1804
+ try {
1805
+ parser.push(Buffer.from("\x1b[200~partial"))
1806
+ clock.advance(100) // way past timeout
1807
+ expect(snap(parser)).toEqual([]) // still collecting paste
1808
+ parser.push(Buffer.from("\x1b[201~"))
1809
+ expect(snap(parser)).toEqual([paste("partial")])
1810
+ } finally {
1811
+ parser.destroy()
1812
+ }
1813
+ })
1814
+
1815
+ test("multiple sequential timeouts", () => {
1816
+ const { parser, clock } = createTimedParser()
1817
+ try {
1818
+ parser.push(Buffer.from("\x1b"))
1819
+ clock.advance(10)
1820
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1821
+
1822
+ parser.push(Buffer.from("\x1b"))
1823
+ clock.advance(10)
1824
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1825
+
1826
+ parser.push(Buffer.from("\x1b"))
1827
+ clock.advance(10)
1828
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1829
+ } finally {
1830
+ parser.destroy()
1831
+ }
1832
+ })
1833
+
1834
+ test("custom timeout delay", () => {
1835
+ const { parser, clock } = createTimedParser({ timeoutMs: 50 })
1836
+ try {
1837
+ parser.push(Buffer.from("\x1b"))
1838
+ clock.advance(49)
1839
+ expect(snap(parser)).toEqual([])
1840
+ clock.advance(1)
1841
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1842
+ } finally {
1843
+ parser.destroy()
1844
+ }
1845
+ })
1846
+
1847
+ test("data completing sequence before timeout cancels flush", () => {
1848
+ const { parser, clock } = createTimedParser()
1849
+ try {
1850
+ parser.push(Buffer.from("\x1b"))
1851
+ clock.advance(5) // halfway to timeout
1852
+ parser.push(Buffer.from("[A")) // completes arrow sequence
1853
+ expect(snap(parser)).toEqual([k("up", { raw: "\x1b[A" })])
1854
+ clock.advance(100) // timeout would have fired, but sequence is done
1855
+ expect(snap(parser)).toEqual([])
1856
+ } finally {
1857
+ parser.destroy()
1858
+ }
1859
+ })
1860
+ })
1861
+
1862
+ describe("embedded ESC abort", () => {
1863
+ test("ESC inside partial CSI flushes as unknown, restarts", () => {
1864
+ const p = createParser()
1865
+ try {
1866
+ p.push(Buffer.from("\x1b[<35;\x1b[<35;20;5m"))
1867
+ expect(snap(p)).toEqual([resp("unknown", "\x1b[<35;"), sgr("\x1b[<35;20;5m", "move", 19, 4)])
1868
+ } finally {
1869
+ p.destroy()
1870
+ }
1871
+ })
1872
+
1873
+ test("ESC inside partial CSI with no following data", () => {
1874
+ const { parser, clock } = createTimedParser()
1875
+ try {
1876
+ parser.push(Buffer.from("\x1b[123\x1b"))
1877
+ const s = snap(parser)
1878
+ // first part flushed as unknown response, ESC starts new escape
1879
+ expect(s).toEqual([resp("unknown", "\x1b[123")])
1880
+ // the trailing ESC is pending
1881
+ clock.advance(10)
1882
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1883
+ } finally {
1884
+ parser.destroy()
1885
+ }
1886
+ })
1887
+
1888
+ test("ESC inside OSC restarts parsing", () => {
1889
+ const p = createParser()
1890
+ try {
1891
+ // ESC ] ... ESC ESC [ A — the first ESC after OSC body starts ST check,
1892
+ // but second ESC byte is not \, so sawEsc resets. Then ESC starts escape.
1893
+ // Actually: \x1b]foo has sawEsc=false. Then \x1b sets sawEsc=true.
1894
+ // Then [ is not \, so sawEsc resets to false and [ is consumed as content.
1895
+ // Then \x1b sets sawEsc=true. Then \ (0x5c = \\) terminates OSC.
1896
+ p.push(Buffer.from("\x1b]foo\x1b\\"))
1897
+ expect(snap(p)).toEqual([resp("osc", "\x1b]foo\x1b\\")])
1898
+ } finally {
1899
+ p.destroy()
1900
+ }
1901
+ })
1902
+
1903
+ test("ESC in SS3 flushes partial as unknown", () => {
1904
+ const p = createParser()
1905
+ try {
1906
+ p.push(Buffer.from("\x1bO\x1b[A"))
1907
+ expect(snap(p)).toEqual([resp("unknown", "\x1bO"), k("up", { raw: "\x1b[A" })])
1908
+ } finally {
1909
+ p.destroy()
1910
+ }
1911
+ })
1912
+ })
1913
+
1914
+ describe("chunk-shape invariance", () => {
1915
+ const sequences = [
1916
+ "abc", // multiple ASCII
1917
+ "\x1b[A", // arrow
1918
+ "\x1bOP", // SS3 F1
1919
+ "\x1b[[A", // Cygwin F1
1920
+ "\x1b[[5~", // putty pageup
1921
+ "\x1b[<0;10;20M", // SGR mouse
1922
+ "\x1b[M !!", // X10 mouse
1923
+ "\x1b]4;0;#ffffff\x07", // OSC
1924
+ "\x1bP>|test\x1b\\", // DCS
1925
+ "\x1b_OK\x1b\\", // APC
1926
+ "\x1b[200~hello\x1b[201~", // paste
1927
+ "\x1b[I", // focus in
1928
+ "\x1b[1;5A", // ctrl+up
1929
+ "\x1b[97u", // kitty key
1930
+ "\x1b[27;2;13~", // modifyOtherKeys
1931
+ ]
1932
+
1933
+ for (const seq of sequences) {
1934
+ test(`byte-at-a-time: ${JSON.stringify(seq).slice(1, -1).slice(0, 30)}`, () => {
1935
+ assertChunkInvariant(Buffer.from(seq))
1936
+ })
1937
+ }
1938
+
1939
+ test("mixed stream byte-at-a-time", () => {
1940
+ const stream = Buffer.concat([
1941
+ Buffer.from("x"),
1942
+ Buffer.from("\x1b[<64;10;5M"),
1943
+ Buffer.from("\x1b[I"),
1944
+ Buffer.from("\x1b]4;0;#fff\x07"),
1945
+ Buffer.from("\x1b[200~paste\x1b[201~"),
1946
+ Buffer.from("👍"),
1947
+ ])
1948
+ assertChunkInvariant(stream)
1949
+ })
1950
+
1951
+ test("random two-chunk splits", () => {
1952
+ const stream = Buffer.from("x\x1b[<64;10;5M\x1b[I\x1b]4;0;#fff\x07\x1b[200~p\x1b[201~y")
1953
+ const whole = createParser()
1954
+ try {
1955
+ whole.push(stream)
1956
+ const expected = snap(whole)
1957
+ // Try splitting at every possible position
1958
+ for (let split = 1; split < stream.length - 1; split++) {
1959
+ const p = createParser()
1960
+ try {
1961
+ p.push(stream.subarray(0, split))
1962
+ p.push(stream.subarray(split))
1963
+ expect(snap(p)).toEqual(expected)
1964
+ } finally {
1965
+ p.destroy()
1966
+ }
1967
+ }
1968
+ } finally {
1969
+ whole.destroy()
1970
+ }
1971
+ })
1972
+
1973
+ const comboAtoms: Array<[label: string, input: ChunkInput]> = [
1974
+ ["ascii", "xy"],
1975
+ ["utf8", "👍"],
1976
+ ["arrow", "\x1b[A"],
1977
+ ["sgr", "\x1b[<64;10;5M"],
1978
+ ["x10", x10bytes(0, 0, 0)],
1979
+ ["osc", "\x1b]4;0;#fff\x07"],
1980
+ ["paste", "\x1b[200~p\x1b[201~"],
1981
+ ["kitty", "\x1b[97u"],
1982
+ ]
1983
+
1984
+ for (const [firstLabel, first] of comboAtoms) {
1985
+ for (const [secondLabel, second] of comboAtoms) {
1986
+ test(`${firstLabel} + ${secondLabel} across every two-chunk split`, () => {
1987
+ const stream = concatChunks([first, second])
1988
+ const expected = snapChunks([stream])
1989
+
1990
+ expect(snapChunks([first, second])).toEqual(expected)
1991
+ for (let split = 1; split < stream.length; split++) {
1992
+ expect(snapChunks([stream.subarray(0, split), stream.subarray(split)])).toEqual(expected)
1993
+ }
1994
+ })
1995
+ }
1996
+ }
1997
+ })
1998
+
1999
+ describe("state management", () => {
2000
+ test("reset clears pending bytes and releases capacity", () => {
2001
+ const p = createParser()
2002
+ try {
2003
+ p.push(Buffer.from("\x1b["))
2004
+ expect(snap(p)).toEqual([])
2005
+ p.push(Buffer.alloc(4096, 0x78)) // 'x' bytes to grow buffer
2006
+ p.reset()
2007
+ expect(snap(p)).toEqual([])
2008
+ expect(p.bufferCapacity).toBeLessThanOrEqual(256)
2009
+ // parser works normally after reset
2010
+ p.push(Buffer.from("a"))
2011
+ expect(snap(p)).toEqual([k("a")])
2012
+ } finally {
2013
+ p.destroy()
2014
+ }
2015
+ })
2016
+
2017
+ test("reset during paste mode clears paste state", () => {
2018
+ const p = createParser()
2019
+ try {
2020
+ p.push(Buffer.from("\x1b[200~partial paste"))
2021
+ expect(snap(p)).toEqual([])
2022
+ p.reset()
2023
+ expect(snap(p)).toEqual([])
2024
+ // parser works normally after reset
2025
+ p.push(Buffer.from("a"))
2026
+ expect(snap(p)).toEqual([k("a")])
2027
+ } finally {
2028
+ p.destroy()
2029
+ }
2030
+ })
2031
+
2032
+ test("reset during escape sequence clears state", () => {
2033
+ const p = createParser()
2034
+ try {
2035
+ p.push(Buffer.from("\x1b["))
2036
+ expect(snap(p)).toEqual([])
2037
+ p.reset()
2038
+ // After reset, the partial CSI is gone; new input starts fresh
2039
+ p.push(Buffer.from("A"))
2040
+ expect(snap(p)).toEqual([k("a", { raw: "A", shift: true })]) // 'A' = shift+a
2041
+ } finally {
2042
+ p.destroy()
2043
+ }
2044
+ })
2045
+
2046
+ test("double reset is safe", () => {
2047
+ const p = createParser()
2048
+ try {
2049
+ p.push(Buffer.from("\x1b["))
2050
+ p.reset()
2051
+ p.reset()
2052
+ p.push(Buffer.from("x"))
2053
+ expect(snap(p)).toEqual([k("x")])
2054
+ } finally {
2055
+ p.destroy()
2056
+ }
2057
+ })
2058
+
2059
+ test("double destroy is safe", () => {
2060
+ const p = createParser()
2061
+ p.destroy()
2062
+ expect(() => p.destroy()).not.toThrow()
2063
+ })
2064
+
2065
+ test("push after destroy throws", () => {
2066
+ const p = createParser()
2067
+ p.destroy()
2068
+ expect(() => p.push(Buffer.from("a"))).toThrow("destroyed")
2069
+ })
2070
+
2071
+ test("read after destroy throws", () => {
2072
+ const p = createParser()
2073
+ p.destroy()
2074
+ expect(() => p.read()).toThrow("destroyed")
2075
+ })
2076
+
2077
+ test("drain after destroy throws", () => {
2078
+ const p = createParser()
2079
+ p.destroy()
2080
+ expect(() => p.drain(() => {})).toThrow("destroyed")
2081
+ })
2082
+
2083
+ test("destroy during drain stops iteration", () => {
2084
+ const p = createParser()
2085
+ p.push(Buffer.from("abc"))
2086
+ let count = 0
2087
+ expect(() => {
2088
+ p.drain(() => {
2089
+ count++
2090
+ if (count === 1) p.destroy()
2091
+ })
2092
+ }).not.toThrow()
2093
+ expect(count).toBe(1)
2094
+ })
2095
+
2096
+ test("read returns null when queue is empty", () => {
2097
+ const p = createParser()
2098
+ try {
2099
+ expect(p.read()).toBeNull()
2100
+ } finally {
2101
+ p.destroy()
2102
+ }
2103
+ })
2104
+
2105
+ test("read pops events one at a time", () => {
2106
+ const p = createParser()
2107
+ try {
2108
+ p.push(Buffer.from("abc"))
2109
+ const e1 = p.read()
2110
+ const e2 = p.read()
2111
+ const e3 = p.read()
2112
+ const e4 = p.read()
2113
+ expect(e1).not.toBeNull()
2114
+ expect(e2).not.toBeNull()
2115
+ expect(e3).not.toBeNull()
2116
+ expect(e4).toBeNull()
2117
+ expect(snapshotEvent(e1!)).toEqual(k("a"))
2118
+ expect(snapshotEvent(e2!)).toEqual(k("b"))
2119
+ expect(snapshotEvent(e3!)).toEqual(k("c"))
2120
+ } finally {
2121
+ p.destroy()
2122
+ }
2123
+ })
2124
+
2125
+ test("overflow flushes incomplete protocols as one unknown response and recovers", () => {
2126
+ const longDigits = "1".repeat(40)
2127
+ const longOsc = "a".repeat(40)
2128
+ const longDcs = "x".repeat(40)
2129
+ const cases: Array<[label: string, chunks: ChunkInput[], expected: Snap[]]> = [
2130
+ ["CSI", [`\x1b[${longDigits}`], [resp("unknown", `\x1b[${longDigits}`)]],
2131
+ ["OSC", [`\x1b]${longOsc}`], [resp("unknown", `\x1b]${longOsc}`)]],
2132
+ ["DCS + recovery", [`\x1bP${longDcs}`, "z"], [resp("unknown", `\x1bP${longDcs}`), k("z")]],
2133
+ ]
2134
+
2135
+ for (const [label, chunks, expected] of cases) {
2136
+ expect(snapChunks(chunks, { maxPendingBytes: 16 })).toEqual(expected)
2137
+ }
2138
+ })
2139
+ })
2140
+
2141
+ describe("multi-event interleaving", () => {
2142
+ table([
2143
+ [
2144
+ "key + mouse",
2145
+ "x\x1b[<64;10;5M",
2146
+ [k("x"), sgr("\x1b[<64;10;5M", "scroll", 9, 4, { scroll: { direction: "up", delta: 1 } })],
2147
+ ],
2148
+ [
2149
+ "mouse + key",
2150
+ "\x1b[<64;10;5Mx",
2151
+ [sgr("\x1b[<64;10;5M", "scroll", 9, 4, { scroll: { direction: "up", delta: 1 } }), k("x")],
2152
+ ],
2153
+ ["key + focus + key", "a\x1b[Ib", [k("a"), resp("csi", "\x1b[I"), k("b")]],
2154
+ ["paste + key", "\x1b[200~hi\x1b[201~z", [paste("hi"), k("z")]],
2155
+ ["multiple keys", "abc", [k("a"), k("b"), k("c")]],
2156
+ [
2157
+ "arrow + text + mouse",
2158
+ "\x1b[Ax\x1b[<0;1;1M",
2159
+ [k("up", { raw: "\x1b[A" }), k("x"), sgr("\x1b[<0;1;1M", "down", 0, 0)],
2160
+ ],
2161
+ ])
2162
+
2163
+ test("OSC + key + mouse + paste in one push", () => {
2164
+ const p = createParser()
2165
+ try {
2166
+ const input = "\x1b]4;0;#fff\x07a\x1b[<0;1;1M\x1b[200~p\x1b[201~"
2167
+ p.push(Buffer.from(input))
2168
+ expect(snap(p)).toEqual([
2169
+ resp("osc", "\x1b]4;0;#fff\x07"),
2170
+ k("a"),
2171
+ sgr("\x1b[<0;1;1M", "down", 0, 0),
2172
+ paste("p"),
2173
+ ])
2174
+ } finally {
2175
+ p.destroy()
2176
+ }
2177
+ })
2178
+ })
2179
+
2180
+ describe("negative and edge cases", () => {
2181
+ test("push with empty buffer emits an empty key event", () => {
2182
+ const p = createParser()
2183
+ try {
2184
+ p.push(new Uint8Array(0))
2185
+ expect(snap(p)).toEqual([k("")])
2186
+ } finally {
2187
+ p.destroy()
2188
+ }
2189
+ })
2190
+
2191
+ test("drain with no events does not call callback", () => {
2192
+ const p = createParser()
2193
+ try {
2194
+ let called = false
2195
+ p.drain(() => {
2196
+ called = true
2197
+ })
2198
+ expect(called).toBe(false)
2199
+ } finally {
2200
+ p.destroy()
2201
+ }
2202
+ })
2203
+
2204
+ table([
2205
+ ["CSI with unknown final byte produces empty-name key", "\x1b[h", [k("", { raw: "\x1b[h" })]],
2206
+ ["ESC followed by punctuation stays one empty-name key", "\x1b!", [k("", { raw: "\x1b!" })]],
2207
+ ["ESC followed by N becomes meta+shift+N", "\x1bN", [k("N", { raw: "\x1bN", meta: true, shift: true })]],
2208
+ ["malformed SGR mouse falls through as empty-name CSI key", "\x1b[<0M", [k("", { raw: "\x1b[<0M" })]],
2209
+ ["bracketed paste end outside paste mode is a CSI response", "\x1b[201~", [resp("csi", "\x1b[201~")]],
2210
+ ])
2211
+
2212
+ test("partial X10 times out as one unknown response", () => {
2213
+ const { parser, clock } = createTimedParser()
2214
+ try {
2215
+ parser.push(Buffer.from("\x1b[M !"))
2216
+ expect(snap(parser)).toEqual([])
2217
+ clock.advance(10)
2218
+ expect(snap(parser)).toEqual([resp("unknown", "\x1b[M !")])
2219
+ } finally {
2220
+ parser.destroy()
2221
+ }
2222
+ })
2223
+
2224
+ test("very long paste with partial end marker in every chunk", () => {
2225
+ const p = createParser()
2226
+ try {
2227
+ p.push(Buffer.from("\x1b[200~"))
2228
+ for (let i = 0; i < 100; i++) p.push(Buffer.from("\x1b[20"))
2229
+ p.push(Buffer.from("\x1b[201~"))
2230
+ expect(snap(p)).toEqual([paste("\x1b[20".repeat(100))])
2231
+ } finally {
2232
+ p.destroy()
2233
+ }
2234
+ })
2235
+ })
2236
+
2237
+ describe("timer/clock disagreement race condition", () => {
2238
+ test("timeout callback flushes even when now() reports slightly less elapsed time than timeoutMs", () => {
2239
+ const inner = new ManualClock()
2240
+ let insideTimerCallback = false
2241
+
2242
+ // Wraps ManualClock so that now() returns pendingSinceMs + timeoutMs - 1
2243
+ // during the timeout callback, simulating runtime behavior where timer
2244
+ // scheduling and now() sampling disagree by a small amount.
2245
+ const disagreeingClock: Clock = {
2246
+ now(): number {
2247
+ if (insideTimerCallback) {
2248
+ // Report 1ms less than the timeout requires — this is the
2249
+ // race condition that kept bytes stuck before the fix.
2250
+ return TEST_TIMEOUT_MS - 1
2251
+ }
2252
+ return inner.now()
2253
+ },
2254
+ setTimeout(fn: () => void, delayMs: number): TimerHandle {
2255
+ return inner.setTimeout(() => {
2256
+ insideTimerCallback = true
2257
+ try {
2258
+ fn()
2259
+ } finally {
2260
+ insideTimerCallback = false
2261
+ }
2262
+ }, delayMs)
2263
+ },
2264
+ clearTimeout(handle: TimerHandle): void {
2265
+ inner.clearTimeout(handle)
2266
+ },
2267
+ setInterval(fn: () => void, delayMs: number): TimerHandle {
2268
+ return inner.setInterval(fn, delayMs)
2269
+ },
2270
+ clearInterval(handle: TimerHandle): void {
2271
+ inner.clearInterval(handle)
2272
+ },
2273
+ }
2274
+
2275
+ const parser = new StdinParser({ armTimeouts: true, clock: disagreeingClock, timeoutMs: TEST_TIMEOUT_MS })
2276
+ try {
2277
+ parser.push(Buffer.from("\x1b"))
2278
+ expect(snap(parser)).toEqual([])
2279
+
2280
+ // Fire the timer — now() will report timeoutMs - 1 elapsed, but the
2281
+ // timeout callback still force-flushes without re-checking elapsed time.
2282
+ inner.advance(TEST_TIMEOUT_MS)
2283
+
2284
+ expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
2285
+ } finally {
2286
+ parser.destroy()
2287
+ }
2288
+ })
2289
+ })
2290
+ })