@fairyhunter13/opentui-core 0.1.114 → 0.1.116

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