@fairyhunter13/opentui-core 0.1.90 → 0.1.92

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 (571) 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 +34042 -0
  17. package/3d.js.map +155 -0
  18. package/LICENSE +21 -0
  19. package/NativeSpanFeed.d.ts +41 -0
  20. package/README.md +2 -2
  21. package/Renderable.d.ts +334 -0
  22. package/animation/Timeline.d.ts +126 -0
  23. package/ansi.d.ts +13 -0
  24. package/buffer.d.ts +107 -0
  25. package/console.d.ts +143 -0
  26. package/edit-buffer.d.ts +98 -0
  27. package/editor-view.d.ts +73 -0
  28. package/index-e6ec7apq.js +18415 -0
  29. package/index-e6ec7apq.js.map +64 -0
  30. package/index-h066zmrb.js +12619 -0
  31. package/index-h066zmrb.js.map +43 -0
  32. package/index-ynzawt3n.js +113 -0
  33. package/index-ynzawt3n.js.map +10 -0
  34. package/index.d.ts +21 -0
  35. package/index.js +430 -0
  36. package/index.js.map +9 -0
  37. package/lib/KeyHandler.d.ts +61 -0
  38. package/lib/RGBA.d.ts +25 -0
  39. package/lib/ascii.font.d.ts +508 -0
  40. package/lib/border.d.ts +49 -0
  41. package/lib/bunfs.d.ts +7 -0
  42. package/lib/clipboard.d.ts +17 -0
  43. package/lib/clock.d.ts +15 -0
  44. package/lib/data-paths.d.ts +26 -0
  45. package/lib/debounce.d.ts +42 -0
  46. package/lib/detect-links.d.ts +6 -0
  47. package/lib/env.d.ts +42 -0
  48. package/lib/extmarks-history.d.ts +17 -0
  49. package/lib/extmarks.d.ts +89 -0
  50. package/lib/hast-styled-text.d.ts +17 -0
  51. package/lib/index.d.ts +21 -0
  52. package/lib/keymapping.d.ts +25 -0
  53. package/lib/objects-in-viewport.d.ts +24 -0
  54. package/lib/output.capture.d.ts +24 -0
  55. package/lib/parse.keypress-kitty.d.ts +2 -0
  56. package/lib/parse.keypress.d.ts +26 -0
  57. package/lib/parse.mouse.d.ts +30 -0
  58. package/lib/paste.d.ts +7 -0
  59. package/lib/queue.d.ts +15 -0
  60. package/lib/renderable.validations.d.ts +12 -0
  61. package/lib/scroll-acceleration.d.ts +43 -0
  62. package/lib/selection.d.ts +63 -0
  63. package/lib/singleton.d.ts +7 -0
  64. package/lib/stdin-parser.d.ts +76 -0
  65. package/lib/styled-text.d.ts +63 -0
  66. package/lib/terminal-capability-detection.d.ts +30 -0
  67. package/lib/terminal-palette.d.ts +50 -0
  68. package/lib/tree-sitter/assets/update.d.ts +11 -0
  69. package/lib/tree-sitter/client.d.ts +47 -0
  70. package/lib/tree-sitter/default-parsers.d.ts +2 -0
  71. package/lib/tree-sitter/download-utils.d.ts +21 -0
  72. package/lib/tree-sitter/index.d.ts +8 -0
  73. package/lib/tree-sitter/parser.worker.d.ts +1 -0
  74. package/lib/tree-sitter/parsers-config.d.ts +38 -0
  75. package/lib/tree-sitter/resolve-ft.d.ts +2 -0
  76. package/lib/tree-sitter/types.d.ts +81 -0
  77. package/lib/tree-sitter-styled-text.d.ts +14 -0
  78. package/lib/validate-dir-name.d.ts +1 -0
  79. package/lib/yoga.options.d.ts +32 -0
  80. package/package.json +51 -63
  81. package/parser.worker.js +869 -0
  82. package/parser.worker.js.map +12 -0
  83. package/plugins/core-slot.d.ts +72 -0
  84. package/plugins/registry.d.ts +38 -0
  85. package/plugins/types.d.ts +34 -0
  86. package/post/filters.d.ts +105 -0
  87. package/renderables/ASCIIFont.d.ts +52 -0
  88. package/renderables/Box.d.ts +72 -0
  89. package/renderables/Code.d.ts +78 -0
  90. package/renderables/Diff.d.ts +142 -0
  91. package/renderables/EditBufferRenderable.d.ts +162 -0
  92. package/renderables/FrameBuffer.d.ts +16 -0
  93. package/renderables/Input.d.ts +67 -0
  94. package/renderables/LineNumberRenderable.d.ts +74 -0
  95. package/renderables/Markdown.d.ts +173 -0
  96. package/renderables/ScrollBar.d.ts +77 -0
  97. package/renderables/ScrollBox.d.ts +124 -0
  98. package/renderables/Select.d.ts +115 -0
  99. package/renderables/Slider.d.ts +44 -0
  100. package/renderables/TabSelect.d.ts +96 -0
  101. package/renderables/Text.d.ts +36 -0
  102. package/renderables/TextBufferRenderable.d.ts +105 -0
  103. package/renderables/TextNode.d.ts +91 -0
  104. package/renderables/TextTable.d.ts +140 -0
  105. package/renderables/Textarea.d.ts +114 -0
  106. package/renderables/TimeToFirstDraw.d.ts +24 -0
  107. package/renderables/__tests__/renderable-test-utils.d.ts +12 -0
  108. package/renderables/composition/VRenderable.d.ts +16 -0
  109. package/renderables/composition/constructs.d.ts +35 -0
  110. package/renderables/composition/vnode.d.ts +46 -0
  111. package/renderables/index.d.ts +22 -0
  112. package/renderables/markdown-parser.d.ts +10 -0
  113. package/renderer.d.ts +388 -0
  114. package/runtime-plugin-support.d.ts +3 -0
  115. package/runtime-plugin-support.js +29 -0
  116. package/runtime-plugin-support.js.map +10 -0
  117. package/runtime-plugin.d.ts +11 -0
  118. package/runtime-plugin.js +16 -0
  119. package/runtime-plugin.js.map +9 -0
  120. package/syntax-style.d.ts +54 -0
  121. package/testing/manual-clock.d.ts +16 -0
  122. package/testing/mock-keys.d.ts +81 -0
  123. package/testing/mock-mouse.d.ts +38 -0
  124. package/testing/mock-tree-sitter-client.d.ts +23 -0
  125. package/testing/spy.d.ts +7 -0
  126. package/testing/test-recorder.d.ts +61 -0
  127. package/testing/test-renderer.d.ts +23 -0
  128. package/testing.d.ts +6 -0
  129. package/testing.js +675 -0
  130. package/testing.js.map +15 -0
  131. package/text-buffer-view.d.ts +42 -0
  132. package/text-buffer.d.ts +67 -0
  133. package/types.d.ts +131 -0
  134. package/utils.d.ts +14 -0
  135. package/zig-structs.d.ts +155 -0
  136. package/zig.d.ts +351 -0
  137. package/dev/keypress-debug-renderer.ts +0 -148
  138. package/dev/keypress-debug.ts +0 -43
  139. package/dev/print-env-vars.ts +0 -32
  140. package/dev/test-tmux-graphics-334.sh +0 -68
  141. package/dev/thai-debug-test.ts +0 -68
  142. package/docs/development.md +0 -141
  143. package/docs/env-vars.md +0 -140
  144. package/docs/getting-started.md +0 -353
  145. package/docs/renderables-vs-constructs.md +0 -159
  146. package/docs/tree-sitter.md +0 -311
  147. package/scripts/build.ts +0 -400
  148. package/scripts/publish.ts +0 -60
  149. package/src/3d/SpriteResourceManager.ts +0 -286
  150. package/src/3d/SpriteUtils.ts +0 -71
  151. package/src/3d/TextureUtils.ts +0 -196
  152. package/src/3d/ThreeRenderable.ts +0 -197
  153. package/src/3d/WGPURenderer.ts +0 -294
  154. package/src/3d/animation/ExplodingSpriteEffect.ts +0 -513
  155. package/src/3d/animation/PhysicsExplodingSpriteEffect.ts +0 -429
  156. package/src/3d/animation/SpriteAnimator.ts +0 -633
  157. package/src/3d/animation/SpriteParticleGenerator.ts +0 -435
  158. package/src/3d/canvas.ts +0 -464
  159. package/src/3d/index.ts +0 -12
  160. package/src/3d/physics/PlanckPhysicsAdapter.ts +0 -72
  161. package/src/3d/physics/RapierPhysicsAdapter.ts +0 -66
  162. package/src/3d/physics/physics-interface.ts +0 -31
  163. package/src/3d/shaders/supersampling.wgsl +0 -201
  164. package/src/3d.ts +0 -3
  165. package/src/NativeSpanFeed.ts +0 -300
  166. package/src/Renderable.ts +0 -1698
  167. package/src/__snapshots__/buffer.test.ts.snap +0 -28
  168. package/src/animation/Timeline.test.ts +0 -2709
  169. package/src/animation/Timeline.ts +0 -598
  170. package/src/ansi.ts +0 -18
  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 -1804
  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 -947
  183. package/src/buffer.test.ts +0 -291
  184. package/src/buffer.ts +0 -519
  185. package/src/console.test.ts +0 -612
  186. package/src/console.ts +0 -1255
  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 -584
  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 -699
  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 -204
  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 -181
  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 -925
  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 -620
  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 -507
  240. package/src/examples/physx-rapier-2d-demo.ts +0 -526
  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 -772
  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 -445
  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 -241
  266. package/src/examples/vnode-composition-demo.ts +0 -404
  267. package/src/index.ts +0 -22
  268. package/src/lib/KeyHandler.integration.test.ts +0 -292
  269. package/src/lib/KeyHandler.stopPropagation.test.ts +0 -289
  270. package/src/lib/KeyHandler.test.ts +0 -662
  271. package/src/lib/KeyHandler.ts +0 -222
  272. package/src/lib/RGBA.test.ts +0 -984
  273. package/src/lib/RGBA.ts +0 -204
  274. package/src/lib/ascii.font.ts +0 -330
  275. package/src/lib/border.test.ts +0 -83
  276. package/src/lib/border.ts +0 -168
  277. package/src/lib/bunfs.test.ts +0 -27
  278. package/src/lib/bunfs.ts +0 -18
  279. package/src/lib/clipboard.test.ts +0 -41
  280. package/src/lib/clipboard.ts +0 -47
  281. package/src/lib/clock.ts +0 -31
  282. package/src/lib/data-paths.test.ts +0 -133
  283. package/src/lib/data-paths.ts +0 -109
  284. package/src/lib/debounce.ts +0 -106
  285. package/src/lib/detect-links.test.ts +0 -98
  286. package/src/lib/detect-links.ts +0 -56
  287. package/src/lib/env.test.ts +0 -228
  288. package/src/lib/env.ts +0 -209
  289. package/src/lib/extmarks-history.ts +0 -51
  290. package/src/lib/extmarks-multiwidth.test.ts +0 -322
  291. package/src/lib/extmarks.test.ts +0 -3457
  292. package/src/lib/extmarks.ts +0 -843
  293. package/src/lib/fonts/block.json +0 -405
  294. package/src/lib/fonts/grid.json +0 -265
  295. package/src/lib/fonts/huge.json +0 -741
  296. package/src/lib/fonts/pallet.json +0 -314
  297. package/src/lib/fonts/shade.json +0 -591
  298. package/src/lib/fonts/slick.json +0 -321
  299. package/src/lib/fonts/tiny.json +0 -69
  300. package/src/lib/hast-styled-text.ts +0 -59
  301. package/src/lib/index.ts +0 -21
  302. package/src/lib/keymapping.test.ts +0 -280
  303. package/src/lib/keymapping.ts +0 -87
  304. package/src/lib/objects-in-viewport.test.ts +0 -787
  305. package/src/lib/objects-in-viewport.ts +0 -153
  306. package/src/lib/output.capture.ts +0 -58
  307. package/src/lib/parse.keypress-kitty.protocol.test.ts +0 -340
  308. package/src/lib/parse.keypress-kitty.test.ts +0 -663
  309. package/src/lib/parse.keypress-kitty.ts +0 -439
  310. package/src/lib/parse.keypress.test.ts +0 -1849
  311. package/src/lib/parse.keypress.ts +0 -397
  312. package/src/lib/parse.mouse.test.ts +0 -552
  313. package/src/lib/parse.mouse.ts +0 -232
  314. package/src/lib/paste.ts +0 -16
  315. package/src/lib/queue.ts +0 -65
  316. package/src/lib/renderable.validations.test.ts +0 -87
  317. package/src/lib/renderable.validations.ts +0 -83
  318. package/src/lib/scroll-acceleration.ts +0 -98
  319. package/src/lib/selection.ts +0 -240
  320. package/src/lib/singleton.ts +0 -28
  321. package/src/lib/stdin-parser.test.ts +0 -1676
  322. package/src/lib/stdin-parser.ts +0 -1248
  323. package/src/lib/styled-text.ts +0 -178
  324. package/src/lib/terminal-capability-detection.test.ts +0 -202
  325. package/src/lib/terminal-capability-detection.ts +0 -79
  326. package/src/lib/terminal-palette.test.ts +0 -878
  327. package/src/lib/terminal-palette.ts +0 -383
  328. package/src/lib/tree-sitter/assets/README.md +0 -118
  329. package/src/lib/tree-sitter/assets/update.ts +0 -331
  330. package/src/lib/tree-sitter/assets.d.ts +0 -9
  331. package/src/lib/tree-sitter/cache.test.ts +0 -270
  332. package/src/lib/tree-sitter/client.test.ts +0 -1061
  333. package/src/lib/tree-sitter/client.ts +0 -615
  334. package/src/lib/tree-sitter/default-parsers.ts +0 -80
  335. package/src/lib/tree-sitter/download-utils.ts +0 -148
  336. package/src/lib/tree-sitter/index.ts +0 -28
  337. package/src/lib/tree-sitter/parser.worker.ts +0 -1001
  338. package/src/lib/tree-sitter/parsers-config.ts +0 -75
  339. package/src/lib/tree-sitter/resolve-ft.ts +0 -62
  340. package/src/lib/tree-sitter/types.ts +0 -81
  341. package/src/lib/tree-sitter-styled-text.test.ts +0 -1253
  342. package/src/lib/tree-sitter-styled-text.ts +0 -306
  343. package/src/lib/validate-dir-name.ts +0 -55
  344. package/src/lib/yoga.options.test.ts +0 -628
  345. package/src/lib/yoga.options.ts +0 -346
  346. package/src/plugins/core-slot.ts +0 -579
  347. package/src/plugins/registry.ts +0 -377
  348. package/src/plugins/types.ts +0 -46
  349. package/src/post/filters.ts +0 -888
  350. package/src/renderables/ASCIIFont.ts +0 -219
  351. package/src/renderables/Box.test.ts +0 -160
  352. package/src/renderables/Box.ts +0 -295
  353. package/src/renderables/Code.test.ts +0 -2062
  354. package/src/renderables/Code.ts +0 -357
  355. package/src/renderables/Diff.regression.test.ts +0 -226
  356. package/src/renderables/Diff.test.ts +0 -3027
  357. package/src/renderables/Diff.ts +0 -1209
  358. package/src/renderables/EditBufferRenderable.ts +0 -764
  359. package/src/renderables/FrameBuffer.ts +0 -47
  360. package/src/renderables/Input.test.ts +0 -1228
  361. package/src/renderables/Input.ts +0 -245
  362. package/src/renderables/LineNumberRenderable.ts +0 -675
  363. package/src/renderables/Markdown.ts +0 -1106
  364. package/src/renderables/ScrollBar.ts +0 -422
  365. package/src/renderables/ScrollBox.ts +0 -883
  366. package/src/renderables/Select.test.ts +0 -1010
  367. package/src/renderables/Select.ts +0 -523
  368. package/src/renderables/Slider.test.ts +0 -456
  369. package/src/renderables/Slider.ts +0 -347
  370. package/src/renderables/TabSelect.test.ts +0 -197
  371. package/src/renderables/TabSelect.ts +0 -455
  372. package/src/renderables/Text.selection-buffer.test.ts +0 -123
  373. package/src/renderables/Text.test.ts +0 -2660
  374. package/src/renderables/Text.ts +0 -147
  375. package/src/renderables/TextBufferRenderable.ts +0 -518
  376. package/src/renderables/TextNode.test.ts +0 -1058
  377. package/src/renderables/TextNode.ts +0 -325
  378. package/src/renderables/TextTable.test.ts +0 -1421
  379. package/src/renderables/TextTable.ts +0 -1344
  380. package/src/renderables/Textarea.ts +0 -732
  381. package/src/renderables/TimeToFirstDraw.ts +0 -89
  382. package/src/renderables/__snapshots__/Code.test.ts.snap +0 -13
  383. package/src/renderables/__snapshots__/Diff.test.ts.snap +0 -785
  384. package/src/renderables/__snapshots__/Text.test.ts.snap +0 -421
  385. package/src/renderables/__snapshots__/TextTable.test.ts.snap +0 -215
  386. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +0 -144
  387. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +0 -816
  388. package/src/renderables/__tests__/LineNumberRenderable.test.ts +0 -1787
  389. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +0 -85
  390. package/src/renderables/__tests__/Markdown.test.ts +0 -2287
  391. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +0 -87
  392. package/src/renderables/__tests__/Textarea.buffer.test.ts +0 -682
  393. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +0 -675
  394. package/src/renderables/__tests__/Textarea.editing.test.ts +0 -2041
  395. package/src/renderables/__tests__/Textarea.error-handling.test.ts +0 -35
  396. package/src/renderables/__tests__/Textarea.events.test.ts +0 -738
  397. package/src/renderables/__tests__/Textarea.highlights.test.ts +0 -590
  398. package/src/renderables/__tests__/Textarea.keybinding.test.ts +0 -3149
  399. package/src/renderables/__tests__/Textarea.paste.test.ts +0 -357
  400. package/src/renderables/__tests__/Textarea.rendering.test.ts +0 -1864
  401. package/src/renderables/__tests__/Textarea.scroll.test.ts +0 -733
  402. package/src/renderables/__tests__/Textarea.selection.test.ts +0 -1590
  403. package/src/renderables/__tests__/Textarea.stress.test.ts +0 -670
  404. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +0 -383
  405. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +0 -310
  406. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +0 -221
  407. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +0 -89
  408. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +0 -457
  409. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +0 -158
  410. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +0 -387
  411. package/src/renderables/__tests__/markdown-parser.test.ts +0 -217
  412. package/src/renderables/__tests__/renderable-test-utils.ts +0 -60
  413. package/src/renderables/composition/README.md +0 -8
  414. package/src/renderables/composition/VRenderable.ts +0 -32
  415. package/src/renderables/composition/constructs.ts +0 -127
  416. package/src/renderables/composition/vnode.ts +0 -289
  417. package/src/renderables/index.ts +0 -22
  418. package/src/renderables/markdown-parser.ts +0 -66
  419. package/src/renderer.ts +0 -2363
  420. package/src/runtime-plugin-support.ts +0 -39
  421. package/src/runtime-plugin.ts +0 -144
  422. package/src/syntax-style.test.ts +0 -841
  423. package/src/syntax-style.ts +0 -264
  424. package/src/testing/README.md +0 -210
  425. package/src/testing/capture-spans.test.ts +0 -194
  426. package/src/testing/integration.test.ts +0 -276
  427. package/src/testing/manual-clock.ts +0 -106
  428. package/src/testing/mock-keys.test.ts +0 -1356
  429. package/src/testing/mock-keys.ts +0 -449
  430. package/src/testing/mock-mouse.test.ts +0 -218
  431. package/src/testing/mock-mouse.ts +0 -247
  432. package/src/testing/mock-tree-sitter-client.ts +0 -73
  433. package/src/testing/spy.ts +0 -13
  434. package/src/testing/test-recorder.test.ts +0 -415
  435. package/src/testing/test-recorder.ts +0 -145
  436. package/src/testing/test-renderer.ts +0 -116
  437. package/src/testing.ts +0 -7
  438. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +0 -481
  439. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +0 -19
  440. package/src/tests/__snapshots__/scrollbox.test.ts.snap +0 -29
  441. package/src/tests/absolute-positioning.snapshot.test.ts +0 -638
  442. package/src/tests/allocator-stats.test.ts +0 -38
  443. package/src/tests/destroy-during-render.test.ts +0 -200
  444. package/src/tests/hover-cursor.test.ts +0 -98
  445. package/src/tests/native-span-feed-async.test.ts +0 -173
  446. package/src/tests/native-span-feed-close.test.ts +0 -120
  447. package/src/tests/native-span-feed-coverage.test.ts +0 -227
  448. package/src/tests/native-span-feed-edge-cases.test.ts +0 -352
  449. package/src/tests/native-span-feed-use-after-free.test.ts +0 -45
  450. package/src/tests/opacity.test.ts +0 -123
  451. package/src/tests/renderable.snapshot.test.ts +0 -524
  452. package/src/tests/renderable.test.ts +0 -1281
  453. package/src/tests/renderer.console-startup.test.ts +0 -65
  454. package/src/tests/renderer.control.test.ts +0 -364
  455. package/src/tests/renderer.core-slot-binding.test.ts +0 -952
  456. package/src/tests/renderer.cursor.test.ts +0 -26
  457. package/src/tests/renderer.destroy-during-render.test.ts +0 -110
  458. package/src/tests/renderer.focus-restore.test.ts +0 -228
  459. package/src/tests/renderer.focus.test.ts +0 -251
  460. package/src/tests/renderer.idle.test.ts +0 -219
  461. package/src/tests/renderer.input.test.ts +0 -2145
  462. package/src/tests/renderer.kitty-flags.test.ts +0 -195
  463. package/src/tests/renderer.mouse.test.ts +0 -1269
  464. package/src/tests/renderer.palette.test.ts +0 -629
  465. package/src/tests/renderer.selection.test.ts +0 -49
  466. package/src/tests/renderer.slot-registry.test.ts +0 -649
  467. package/src/tests/renderer.useMouse.test.ts +0 -50
  468. package/src/tests/runtime-plugin-support.fixture.ts +0 -11
  469. package/src/tests/runtime-plugin-support.test.ts +0 -28
  470. package/src/tests/runtime-plugin.fixture.ts +0 -40
  471. package/src/tests/runtime-plugin.test.ts +0 -190
  472. package/src/tests/scrollbox-culling-bug.test.ts +0 -114
  473. package/src/tests/scrollbox-hitgrid-resize.test.ts +0 -136
  474. package/src/tests/scrollbox-hitgrid.test.ts +0 -909
  475. package/src/tests/scrollbox.test.ts +0 -1530
  476. package/src/tests/wrap-resize-perf.test.ts +0 -229
  477. package/src/tests/yoga-setters.test.ts +0 -921
  478. package/src/text-buffer-view.test.ts +0 -705
  479. package/src/text-buffer-view.ts +0 -189
  480. package/src/text-buffer.test.ts +0 -347
  481. package/src/text-buffer.ts +0 -250
  482. package/src/types.ts +0 -152
  483. package/src/utils.ts +0 -88
  484. package/src/zig/ansi.zig +0 -268
  485. package/src/zig/bench/README.md +0 -50
  486. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +0 -887
  487. package/src/zig/bench/edit-buffer_bench.zig +0 -476
  488. package/src/zig/bench/native-span-feed_bench.zig +0 -100
  489. package/src/zig/bench/rope-markers_bench.zig +0 -713
  490. package/src/zig/bench/rope_bench.zig +0 -514
  491. package/src/zig/bench/styled-text_bench.zig +0 -470
  492. package/src/zig/bench/text-buffer-coords_bench.zig +0 -362
  493. package/src/zig/bench/text-buffer-view_bench.zig +0 -459
  494. package/src/zig/bench/text-chunk-graphemes_bench.zig +0 -273
  495. package/src/zig/bench/utf8_bench.zig +0 -799
  496. package/src/zig/bench-utils.zig +0 -431
  497. package/src/zig/bench.zig +0 -217
  498. package/src/zig/buffer.zig +0 -2223
  499. package/src/zig/build.zig +0 -289
  500. package/src/zig/build.zig.zon +0 -16
  501. package/src/zig/edit-buffer.zig +0 -825
  502. package/src/zig/editor-view.zig +0 -802
  503. package/src/zig/event-bus.zig +0 -13
  504. package/src/zig/event-emitter.zig +0 -65
  505. package/src/zig/file-logger.zig +0 -92
  506. package/src/zig/grapheme.zig +0 -599
  507. package/src/zig/lib.zig +0 -1834
  508. package/src/zig/link.zig +0 -333
  509. package/src/zig/logger.zig +0 -43
  510. package/src/zig/mem-registry.zig +0 -125
  511. package/src/zig/native-span-feed-bench-lib.zig +0 -7
  512. package/src/zig/native-span-feed.zig +0 -708
  513. package/src/zig/renderer.zig +0 -1386
  514. package/src/zig/rope.zig +0 -1220
  515. package/src/zig/syntax-style.zig +0 -161
  516. package/src/zig/terminal.zig +0 -975
  517. package/src/zig/test.zig +0 -70
  518. package/src/zig/tests/README.md +0 -18
  519. package/src/zig/tests/buffer_test.zig +0 -2526
  520. package/src/zig/tests/edit-buffer-history_test.zig +0 -271
  521. package/src/zig/tests/edit-buffer_test.zig +0 -1689
  522. package/src/zig/tests/editor-view_test.zig +0 -3299
  523. package/src/zig/tests/event-emitter_test.zig +0 -249
  524. package/src/zig/tests/grapheme_test.zig +0 -1304
  525. package/src/zig/tests/link_test.zig +0 -190
  526. package/src/zig/tests/mem-registry_test.zig +0 -473
  527. package/src/zig/tests/memory_leak_regression_test.zig +0 -159
  528. package/src/zig/tests/native-span-feed_test.zig +0 -1264
  529. package/src/zig/tests/renderer_test.zig +0 -1010
  530. package/src/zig/tests/rope-nested_test.zig +0 -712
  531. package/src/zig/tests/rope_fuzz_test.zig +0 -238
  532. package/src/zig/tests/rope_test.zig +0 -2362
  533. package/src/zig/tests/segment-merge.test.zig +0 -148
  534. package/src/zig/tests/syntax-style_test.zig +0 -557
  535. package/src/zig/tests/terminal_test.zig +0 -719
  536. package/src/zig/tests/text-buffer-drawing_test.zig +0 -3237
  537. package/src/zig/tests/text-buffer-highlights_test.zig +0 -666
  538. package/src/zig/tests/text-buffer-iterators_test.zig +0 -776
  539. package/src/zig/tests/text-buffer-segment_test.zig +0 -320
  540. package/src/zig/tests/text-buffer-selection_test.zig +0 -1035
  541. package/src/zig/tests/text-buffer-selection_viewport_test.zig +0 -358
  542. package/src/zig/tests/text-buffer-view_test.zig +0 -3649
  543. package/src/zig/tests/text-buffer_test.zig +0 -2191
  544. package/src/zig/tests/unicode-width-map.zon +0 -3909
  545. package/src/zig/tests/utf8_no_zwj_test.zig +0 -260
  546. package/src/zig/tests/utf8_test.zig +0 -4057
  547. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +0 -267
  548. package/src/zig/tests/utf8_wcwidth_test.zig +0 -357
  549. package/src/zig/tests/word-wrap-editing_test.zig +0 -498
  550. package/src/zig/tests/wrap-cache-perf_test.zig +0 -113
  551. package/src/zig/text-buffer-iterators.zig +0 -499
  552. package/src/zig/text-buffer-segment.zig +0 -404
  553. package/src/zig/text-buffer-view.zig +0 -1371
  554. package/src/zig/text-buffer.zig +0 -1180
  555. package/src/zig/utf8.zig +0 -1948
  556. package/src/zig/utils.zig +0 -9
  557. package/src/zig-structs.ts +0 -261
  558. package/src/zig.ts +0 -3843
  559. package/tsconfig.build.json +0 -22
  560. package/tsconfig.json +0 -28
  561. /package/{src/lib/tree-sitter/assets → assets}/javascript/highlights.scm +0 -0
  562. /package/{src/lib/tree-sitter/assets → assets}/javascript/tree-sitter-javascript.wasm +0 -0
  563. /package/{src/lib/tree-sitter/assets → assets}/markdown/highlights.scm +0 -0
  564. /package/{src/lib/tree-sitter/assets → assets}/markdown/injections.scm +0 -0
  565. /package/{src/lib/tree-sitter/assets → assets}/markdown/tree-sitter-markdown.wasm +0 -0
  566. /package/{src/lib/tree-sitter/assets → assets}/markdown_inline/highlights.scm +0 -0
  567. /package/{src/lib/tree-sitter/assets → assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  568. /package/{src/lib/tree-sitter/assets → assets}/typescript/highlights.scm +0 -0
  569. /package/{src/lib/tree-sitter/assets → assets}/typescript/tree-sitter-typescript.wasm +0 -0
  570. /package/{src/lib/tree-sitter/assets → assets}/zig/highlights.scm +0 -0
  571. /package/{src/lib/tree-sitter/assets → assets}/zig/tree-sitter-zig.wasm +0 -0
@@ -1,1676 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { Buffer } from "node:buffer"
3
- import { ManualClock } from "../testing/manual-clock"
4
- import type { ScrollInfo } from "./parse.mouse"
5
- import { StdinParser, type StdinEvent, type StdinParserOptions } from "./stdin-parser"
6
-
7
- type KeySnap = {
8
- type: "key"
9
- raw: string
10
- name: string
11
- ctrl: boolean
12
- meta: boolean
13
- shift: boolean
14
- eventType: string
15
- }
16
- type MouseSnap = { type: "mouse"; raw: string; encoding: "sgr" | "x10"; event: Record<string, unknown> }
17
- type PasteSnap = { type: "paste"; bytes: Uint8Array }
18
- type RespSnap = { type: "response"; protocol: string; sequence: string }
19
- type Snap = KeySnap | MouseSnap | PasteSnap | RespSnap
20
-
21
- const K_DEFAULTS = { ctrl: false, meta: false, shift: false, eventType: "press" }
22
- type KOpts = { raw?: string; ctrl?: boolean; meta?: boolean; shift?: boolean; eventType?: string }
23
-
24
- function k(name: string, opts: KOpts = {}): KeySnap {
25
- return { type: "key", raw: opts.raw ?? name, name, ...K_DEFAULTS, ...opts }
26
- }
27
-
28
- function resp(protocol: string, sequence: string): RespSnap {
29
- return { type: "response", protocol, sequence }
30
- }
31
-
32
- function paste(text: string): PasteSnap {
33
- return { type: "paste", bytes: Uint8Array.from(Buffer.from(text)) }
34
- }
35
-
36
- const NO_MODS = { shift: false, alt: false, ctrl: false }
37
-
38
- function sgr(
39
- raw: string,
40
- evType: string,
41
- x: number,
42
- y: number,
43
- opts: { button?: number; mods?: Partial<typeof NO_MODS>; scroll?: ScrollInfo } = {},
44
- ): MouseSnap {
45
- const event: Record<string, unknown> = {
46
- type: evType,
47
- button: opts.button ?? 0,
48
- x,
49
- y,
50
- modifiers: { ...NO_MODS, ...opts.mods },
51
- }
52
- if (opts.scroll) event.scroll = opts.scroll
53
- return { type: "mouse", raw, encoding: "sgr", event }
54
- }
55
-
56
- function x10m(
57
- raw: string,
58
- evType: string,
59
- x: number,
60
- y: number,
61
- opts: { button?: number; mods?: Partial<typeof NO_MODS>; scroll?: ScrollInfo } = {},
62
- ): MouseSnap {
63
- const event: Record<string, unknown> = {
64
- type: evType,
65
- button: opts.button ?? 0,
66
- x,
67
- y,
68
- modifiers: { ...NO_MODS, ...opts.mods },
69
- }
70
- if (opts.scroll) event.scroll = opts.scroll
71
- return { type: "mouse", raw, encoding: "x10", event }
72
- }
73
-
74
- function createParser(options: StdinParserOptions = {}): StdinParser {
75
- return new StdinParser({ armTimeouts: false, clock: new ManualClock(), ...options })
76
- }
77
-
78
- function createTimedParser(options: StdinParserOptions = {}): { parser: StdinParser; clock: ManualClock } {
79
- const clock = new ManualClock()
80
- return { parser: new StdinParser({ armTimeouts: true, clock, ...options }), clock }
81
- }
82
-
83
- function snapshotEvent(event: StdinEvent): Snap {
84
- switch (event.type) {
85
- case "key":
86
- return {
87
- type: "key",
88
- raw: event.raw,
89
- name: event.key.name,
90
- ctrl: event.key.ctrl,
91
- meta: event.key.meta,
92
- shift: event.key.shift,
93
- eventType: event.key.eventType,
94
- }
95
- case "mouse": {
96
- const ev: Record<string, unknown> = { ...event.event }
97
- if (!ev.scroll) delete ev.scroll
98
- return { type: "mouse", raw: event.raw, encoding: event.encoding, event: ev }
99
- }
100
- case "paste":
101
- return { type: "paste", bytes: event.bytes }
102
- case "response":
103
- return { type: "response", protocol: event.protocol, sequence: event.sequence }
104
- }
105
- }
106
-
107
- function snap(parser: StdinParser): Snap[] {
108
- const events: StdinEvent[] = []
109
- parser.drain((e) => events.push(e))
110
- return events.map(snapshotEvent)
111
- }
112
-
113
- type ChunkInput = string | number[] | Uint8Array
114
-
115
- function buf(input: ChunkInput): Uint8Array {
116
- if (typeof input === "string") return Buffer.from(input)
117
- return input instanceof Uint8Array ? input : Uint8Array.from(input)
118
- }
119
-
120
- function latin1(input: number[] | Uint8Array): string {
121
- return Buffer.from(buf(input)).toString("latin1")
122
- }
123
-
124
- function snapChunks(chunks: ChunkInput[], opts?: StdinParserOptions): Snap[] {
125
- const p = createParser(opts)
126
- try {
127
- for (const chunk of chunks) p.push(buf(chunk))
128
- return snap(p)
129
- } finally {
130
- p.destroy()
131
- }
132
- }
133
-
134
- function concatChunks(chunks: ChunkInput[]): Uint8Array {
135
- return Buffer.concat(chunks.map((chunk) => Buffer.from(buf(chunk))))
136
- }
137
-
138
- function x10bytes(rawButton: number, x: number, y: number): number[] {
139
- return [0x1b, 0x5b, 0x4d, rawButton + 32, x + 33, y + 33]
140
- }
141
-
142
- type Case = [label: string, input: ChunkInput, expected: Snap[]]
143
-
144
- function table(cases: Case[], opts?: StdinParserOptions) {
145
- for (const [label, input, expected] of cases) {
146
- test(label, () => {
147
- const p = createParser(opts)
148
- try {
149
- p.push(buf(input))
150
- expect(snap(p)).toEqual(expected)
151
- } finally {
152
- p.destroy()
153
- }
154
- })
155
- }
156
- }
157
-
158
- /** push each byte individually, assert same result as whole-chunk push */
159
- function assertChunkInvariant(input: Uint8Array, opts?: StdinParserOptions) {
160
- const whole = createParser(opts)
161
- const split = createParser(opts)
162
- try {
163
- whole.push(input)
164
- const expected = snap(whole)
165
- for (let i = 0; i < input.length; i++) split.push(input.subarray(i, i + 1))
166
- expect(snap(split)).toEqual(expected)
167
- } finally {
168
- whole.destroy()
169
- split.destroy()
170
- }
171
- }
172
-
173
- describe("StdinParser", () => {
174
- describe("printable ASCII", () => {
175
- test("lowercase a-z", () => {
176
- const p = createParser()
177
- try {
178
- p.push(Buffer.from("abcdefghijklmnopqrstuvwxyz"))
179
- expect(snap(p)).toEqual("abcdefghijklmnopqrstuvwxyz".split("").map((c) => k(c)))
180
- } finally {
181
- p.destroy()
182
- }
183
- })
184
-
185
- test("uppercase A-Z produce shifted keys", () => {
186
- const p = createParser()
187
- try {
188
- p.push(Buffer.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
189
- expect(snap(p)).toEqual(
190
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((c) => k(c.toLowerCase(), { raw: c, shift: true })),
191
- )
192
- } finally {
193
- p.destroy()
194
- }
195
- })
196
-
197
- test("digits 0-9", () => {
198
- const p = createParser()
199
- try {
200
- p.push(Buffer.from("0123456789"))
201
- expect(snap(p)).toEqual("0123456789".split("").map((c) => k(c)))
202
- } finally {
203
- p.destroy()
204
- }
205
- })
206
-
207
- test("common symbols", () => {
208
- const p = createParser()
209
- try {
210
- const syms = "!@#$%^&*()-_=+[]{}|;':,./<>?`~"
211
- p.push(Buffer.from(syms))
212
- expect(snap(p)).toEqual(syms.split("").map((c) => k(c)))
213
- } finally {
214
- p.destroy()
215
- }
216
- })
217
-
218
- test("space produces key named space", () => {
219
- const p = createParser()
220
- try {
221
- p.push(Buffer.from(" "))
222
- expect(snap(p)).toEqual([k("space", { raw: " " })])
223
- } finally {
224
- p.destroy()
225
- }
226
- })
227
- })
228
-
229
- describe("control characters", () => {
230
- // Map of special control bytes that get their own key name instead of ctrl+letter
231
- const special: Record<number, [string, KOpts]> = {
232
- 0x00: ["space", { ctrl: true }],
233
- 0x08: ["backspace", {}],
234
- 0x09: ["tab", {}],
235
- 0x0a: ["linefeed", {}],
236
- 0x0d: ["return", {}],
237
- }
238
-
239
- const cases: Case[] = []
240
- for (let byte = 0; byte <= 0x1a; byte++) {
241
- if (byte === 0x1b) continue // ESC tested separately
242
- const raw = String.fromCharCode(byte)
243
- const sp = special[byte]
244
- if (sp) {
245
- cases.push([`0x${byte.toString(16).padStart(2, "0")} → ${sp[0]}`, [byte], [k(sp[0], { raw, ...sp[1] })]])
246
- } else {
247
- const letter = String.fromCharCode(byte + 96)
248
- cases.push([
249
- `ctrl+${letter} (0x${byte.toString(16).padStart(2, "0")})`,
250
- [byte],
251
- [k(letter, { raw, ctrl: true })],
252
- ])
253
- }
254
- }
255
- cases.push(["0x7f → backspace", [0x7f], [k("backspace", { raw: "\x7f" })]])
256
-
257
- table(cases)
258
- })
259
-
260
- describe("special keys", () => {
261
- table([
262
- ["return", "\r", [k("return", { raw: "\r" })]],
263
- ["linefeed", "\n", [k("linefeed", { raw: "\n" })]],
264
- ["tab", "\t", [k("tab", { raw: "\t" })]],
265
- ["backspace (0x08)", "\b", [k("backspace", { raw: "\b" })]],
266
- ["backspace (0x7f)", "\x7f", [k("backspace", { raw: "\x7f" })]],
267
- ["escape (lone, no timeout)", "\x1b", []], // stays pending without timeout
268
- ["shift-tab", "\x1b[Z", [k("tab", { raw: "\x1b[Z", shift: true })]],
269
- ["ctrl+space", "\x00", [k("space", { raw: "\x00", ctrl: true })]],
270
- ])
271
-
272
- test("lone ESC with timeout produces escape key", () => {
273
- const { parser, clock } = createTimedParser()
274
- try {
275
- parser.push(Buffer.from("\x1b"))
276
- expect(snap(parser)).toEqual([])
277
- clock.advance(10)
278
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
279
- } finally {
280
- parser.destroy()
281
- }
282
- })
283
- })
284
-
285
- describe("arrows and navigation", () => {
286
- table([
287
- // CSI arrows
288
- ["up", "\x1b[A", [k("up", { raw: "\x1b[A" })]],
289
- ["down", "\x1b[B", [k("down", { raw: "\x1b[B" })]],
290
- ["right", "\x1b[C", [k("right", { raw: "\x1b[C" })]],
291
- ["left", "\x1b[D", [k("left", { raw: "\x1b[D" })]],
292
- ["home", "\x1b[H", [k("home", { raw: "\x1b[H" })]],
293
- ["end", "\x1b[F", [k("end", { raw: "\x1b[F" })]],
294
- ["clear", "\x1b[E", [k("clear", { raw: "\x1b[E" })]],
295
- // tilde navigation
296
- ["home ~", "\x1b[1~", [k("home", { raw: "\x1b[1~" })]],
297
- ["insert ~", "\x1b[2~", [k("insert", { raw: "\x1b[2~" })]],
298
- ["delete ~", "\x1b[3~", [k("delete", { raw: "\x1b[3~" })]],
299
- ["end ~", "\x1b[4~", [k("end", { raw: "\x1b[4~" })]],
300
- ["pgup ~", "\x1b[5~", [k("pageup", { raw: "\x1b[5~" })]],
301
- ["pgdn ~", "\x1b[6~", [k("pagedown", { raw: "\x1b[6~" })]],
302
- // rxvt
303
- ["home rxvt", "\x1b[7~", [k("home", { raw: "\x1b[7~" })]],
304
- ["end rxvt", "\x1b[8~", [k("end", { raw: "\x1b[8~" })]],
305
- ])
306
- })
307
-
308
- describe("function keys", () => {
309
- // ESC [ n ~ form
310
- const tildeF: [string, string][] = [
311
- ["f1", "11"],
312
- ["f2", "12"],
313
- ["f3", "13"],
314
- ["f4", "14"],
315
- ["f5", "15"],
316
- ["f6", "17"],
317
- ["f7", "18"],
318
- ["f8", "19"],
319
- ["f9", "20"],
320
- ["f10", "21"],
321
- ["f11", "23"],
322
- ["f12", "24"],
323
- ]
324
- table(tildeF.map(([name, num]) => [`${name} (CSI ${num}~)`, `\x1b[${num}~`, [k(name, { raw: `\x1b[${num}~` })]]))
325
-
326
- // ESC O letter (SS3) form — F1-F4
327
- table([
328
- ["f1 (SS3)", "\x1bOP", [k("f1", { raw: "\x1bOP" })]],
329
- ["f2 (SS3)", "\x1bOQ", [k("f2", { raw: "\x1bOQ" })]],
330
- ["f3 (SS3)", "\x1bOR", [k("f3", { raw: "\x1bOR" })]],
331
- ["f4 (SS3)", "\x1bOS", [k("f4", { raw: "\x1bOS" })]],
332
- ])
333
- })
334
-
335
- describe("double-bracket CSI variants", () => {
336
- table([
337
- ["f1 ([[A)", "\x1b[[A", [k("f1", { raw: "\x1b[[A" })]],
338
- ["f2 ([[B)", "\x1b[[B", [k("f2", { raw: "\x1b[[B" })]],
339
- ["f3 ([[C)", "\x1b[[C", [k("f3", { raw: "\x1b[[C" })]],
340
- ["f4 ([[D)", "\x1b[[D", [k("f4", { raw: "\x1b[[D" })]],
341
- ["f5 ([[E)", "\x1b[[E", [k("f5", { raw: "\x1b[[E" })]],
342
- ["pageup ([[5~)", "\x1b[[5~", [k("pageup", { raw: "\x1b[[5~" })]],
343
- ["pagedown ([[6~)", "\x1b[[6~", [k("pagedown", { raw: "\x1b[[6~" })]],
344
- ])
345
- })
346
-
347
- describe("SS3 sequences", () => {
348
- table([
349
- ["up", "\x1bOA", [k("up", { raw: "\x1bOA" })]],
350
- ["down", "\x1bOB", [k("down", { raw: "\x1bOB" })]],
351
- ["right", "\x1bOC", [k("right", { raw: "\x1bOC" })]],
352
- ["left", "\x1bOD", [k("left", { raw: "\x1bOD" })]],
353
- ["home", "\x1bOH", [k("home", { raw: "\x1bOH" })]],
354
- ["end", "\x1bOF", [k("end", { raw: "\x1bOF" })]],
355
- ["clear", "\x1bOE", [k("clear", { raw: "\x1bOE" })]],
356
- ])
357
-
358
- test("SS3 interrupted by embedded ESC flushes partial then restarts", () => {
359
- const p = createParser()
360
- try {
361
- p.push(Buffer.from("\x1bO\x1bOA"))
362
- const s = snap(p)
363
- expect(s).toHaveLength(2)
364
- expect(s[0]).toEqual(resp("unknown", "\x1bO"))
365
- expect(s[1]).toEqual(k("up", { raw: "\x1bOA" }))
366
- } finally {
367
- p.destroy()
368
- }
369
- })
370
-
371
- test("SS3 timeout-flushed as unknown response", () => {
372
- const { parser, clock } = createTimedParser()
373
- try {
374
- parser.push(Buffer.from("\x1bO"))
375
- expect(snap(parser)).toEqual([])
376
- clock.advance(10)
377
- expect(snap(parser)).toEqual([resp("unknown", "\x1bO")])
378
- } finally {
379
- parser.destroy()
380
- }
381
- })
382
- })
383
-
384
- describe("modifier combinations", () => {
385
- // CSI 1;modifier letter format
386
- const modTable: [string, number, KOpts][] = [
387
- ["shift", 2, { shift: true }],
388
- ["alt", 3, { meta: true }],
389
- ["shift+alt", 4, { shift: true, meta: true }],
390
- ["ctrl", 5, { ctrl: true }],
391
- ["shift+ctrl", 6, { shift: true, ctrl: true }],
392
- ["alt+ctrl", 7, { meta: true, ctrl: true }],
393
- ]
394
-
395
- const arrows: [string, string][] = [
396
- ["up", "A"],
397
- ["down", "B"],
398
- ["right", "C"],
399
- ["left", "D"],
400
- ]
401
-
402
- const cases: Case[] = []
403
- for (const [modName, modNum, modOpts] of modTable) {
404
- for (const [keyName, letter] of arrows) {
405
- const seq = `\x1b[1;${modNum}${letter}`
406
- cases.push([`${modName}+${keyName}`, seq, [k(keyName, { raw: seq, ...modOpts })]])
407
- }
408
- }
409
- table(cases)
410
-
411
- // rxvt shift variants
412
- table([
413
- ["shift+up (rxvt)", "\x1b[a", [k("up", { raw: "\x1b[a", shift: true })]],
414
- ["shift+down (rxvt)", "\x1b[b", [k("down", { raw: "\x1b[b", shift: true })]],
415
- ["shift+right (rxvt)", "\x1b[c", [k("right", { raw: "\x1b[c", shift: true })]],
416
- ["shift+left (rxvt)", "\x1b[d", [k("left", { raw: "\x1b[d", shift: true })]],
417
- ])
418
-
419
- // rxvt ctrl variants
420
- table([
421
- ["ctrl+up (rxvt)", "\x1bOa", [k("up", { raw: "\x1bOa", ctrl: true })]],
422
- ["ctrl+down (rxvt)", "\x1bOb", [k("down", { raw: "\x1bOb", ctrl: true })]],
423
- ["ctrl+right (rxvt)", "\x1bOc", [k("right", { raw: "\x1bOc", ctrl: true })]],
424
- ["ctrl+left (rxvt)", "\x1bOd", [k("left", { raw: "\x1bOd", ctrl: true })]],
425
- ])
426
-
427
- // rxvt $ (shift) and ^ (ctrl) on tilde keys
428
- table([
429
- ["shift+insert (rxvt $)", "\x1b[2$", [k("insert", { raw: "\x1b[2$", shift: true })]],
430
- ["shift+delete (rxvt $)", "\x1b[3$", [k("delete", { raw: "\x1b[3$", shift: true })]],
431
- ["shift+pgup (rxvt $)", "\x1b[5$", [k("pageup", { raw: "\x1b[5$", shift: true })]],
432
- ["shift+pgdn (rxvt $)", "\x1b[6$", [k("pagedown", { raw: "\x1b[6$", shift: true })]],
433
- ["ctrl+insert (rxvt ^)", "\x1b[2^", [k("insert", { raw: "\x1b[2^", ctrl: true })]],
434
- ["ctrl+delete (rxvt ^)", "\x1b[3^", [k("delete", { raw: "\x1b[3^", ctrl: true })]],
435
- ["ctrl+pgup (rxvt ^)", "\x1b[5^", [k("pageup", { raw: "\x1b[5^", ctrl: true })]],
436
- ["ctrl+pgdn (rxvt ^)", "\x1b[6^", [k("pagedown", { raw: "\x1b[6^", ctrl: true })]],
437
- ])
438
- })
439
-
440
- describe("meta key combinations", () => {
441
- test("meta+lowercase letters", () => {
442
- const p = createParser()
443
- try {
444
- // Push all ESC+letter pairs at once — each should produce meta+key
445
- for (const ch of "acdeghijklmoqrstuvwxyz".split("")) {
446
- p.push(Buffer.from(`\x1b${ch}`))
447
- }
448
- const s = snap(p)
449
- for (let i = 0; i < s.length; i++) {
450
- const ch = "acdeghijklmoqrstuvwxyz"[i]!
451
- expect(s[i]).toEqual(k(ch, { raw: `\x1b${ch}`, meta: true }))
452
- }
453
- } finally {
454
- p.destroy()
455
- }
456
- })
457
-
458
- // Lowercase ESC+b / ESC+f stay literal meta chords, while uppercase ESC+B / ESC+F
459
- // preserve the old-style meta+arrow behavior from `main`.
460
- table([
461
- ["meta+b (literal chord)", "\x1bb", [k("b", { raw: "\x1bb", meta: true })]],
462
- ["meta+f (literal chord)", "\x1bf", [k("f", { raw: "\x1bf", meta: true })]],
463
- ["meta+B (old-style left)", "\x1bB", [k("left", { raw: "\x1bB", meta: true })]],
464
- ["meta+F (old-style right)", "\x1bF", [k("right", { raw: "\x1bF", meta: true })]],
465
- ["meta+n (plain letter)", "\x1bn", [k("n", { raw: "\x1bn", meta: true })]],
466
- ["meta+p (plain letter)", "\x1bp", [k("p", { raw: "\x1bp", meta: true })]],
467
- ])
468
-
469
- table([
470
- ["meta+return", "\x1b\r", [k("return", { raw: "\x1b\r", meta: true })]],
471
- ["meta+linefeed", "\x1b\n", [k("linefeed", { raw: "\x1b\n", meta: true })]],
472
- ["meta+backspace", "\x1b\x7f", [k("backspace", { raw: "\x1b\x7f", meta: true })]],
473
- ["meta+backspace (0x08)", "\x1b\b", [k("backspace", { raw: "\x1b\b", meta: true })]],
474
- ["meta+space", "\x1b ", [k("space", { raw: "\x1b ", meta: true })]],
475
- ])
476
-
477
- test("meta+escape (requires timeout for \\x1b\\x1b)", () => {
478
- const { parser, clock } = createTimedParser()
479
- try {
480
- parser.push(Buffer.from("\x1b\x1b"))
481
- expect(snap(parser)).toEqual([])
482
- clock.advance(10)
483
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b\x1b", meta: true })])
484
- } finally {
485
- parser.destroy()
486
- }
487
- })
488
-
489
- table([["double-ESC + [A → meta+up", "\x1b\x1b[A", [k("up", { raw: "\x1b\x1b[A", meta: true })]]])
490
-
491
- test("meta+uppercase sets shift", () => {
492
- const p = createParser()
493
- try {
494
- // ESC + uppercase letter → meta + shift + name (uppercase preserved in parseKeypress)
495
- // Excluding B and F which map to arrow keys
496
- p.push(Buffer.from("\x1bA"))
497
- const s = snap(p)
498
- expect(s).toEqual([k("A", { raw: "\x1bA", meta: true, shift: true })])
499
- } finally {
500
- p.destroy()
501
- }
502
- })
503
-
504
- test("meta+ctrl+letter", () => {
505
- const p = createParser()
506
- try {
507
- // ESC + ctrl char (e.g. ESC + 0x01 = meta+ctrl+a)
508
- p.push(Uint8Array.from([0x1b, 0x01]))
509
- expect(snap(p)).toEqual([k("a", { raw: "\x1b\x01", meta: true, ctrl: true })])
510
- } finally {
511
- p.destroy()
512
- }
513
- })
514
-
515
- test("meta+digit", () => {
516
- const p = createParser()
517
- try {
518
- p.push(Buffer.from("\x1b5"))
519
- expect(snap(p)).toEqual([k("5", { raw: "\x1b5", meta: true })])
520
- } finally {
521
- p.destroy()
522
- }
523
- })
524
- })
525
-
526
- describe("kitty keyboard protocol", () => {
527
- // CSI codepoint u format
528
- table([
529
- ["a key", "\x1b[97u", [k("a", { raw: "\x1b[97u" })]],
530
- ["shift+a", "\x1b[97;2u", [k("a", { raw: "\x1b[97;2u", shift: true })]],
531
- ["ctrl+a", "\x1b[97;5u", [k("a", { raw: "\x1b[97;5u", ctrl: true })]],
532
- ["alt+a", "\x1b[97;3u", [k("a", { raw: "\x1b[97;3u", meta: true })]],
533
- ["ctrl+shift+a", "\x1b[97;6u", [k("a", { raw: "\x1b[97;6u", ctrl: true, shift: true })]],
534
- ["a release", "\x1b[97;1:3u", [k("a", { raw: "\x1b[97;1:3u", eventType: "release" })]],
535
- ["escape", "\x1b[27u", [k("escape", { raw: "\x1b[27u" })]],
536
- ["return", "\x1b[13u", [k("return", { raw: "\x1b[13u" })]],
537
- ["tab", "\x1b[9u", [k("tab", { raw: "\x1b[9u" })]],
538
- ["backspace", "\x1b[127u", [k("backspace", { raw: "\x1b[127u" })]],
539
- ["delete", "\x1b[57349u", [k("delete", { raw: "\x1b[57349u" })]],
540
- ["insert", "\x1b[57348u", [k("insert", { raw: "\x1b[57348u" })]],
541
- ["f1", "\x1b[57364u", [k("f1", { raw: "\x1b[57364u" })]],
542
- ["f12", "\x1b[57375u", [k("f12", { raw: "\x1b[57375u" })]],
543
- ])
544
-
545
- // CSI 1;modifier:event letter format (kitty functional keys)
546
- table([
547
- ["up press", "\x1b[1;1:1A", [k("up", { raw: "\x1b[1;1:1A" })]],
548
- ["up release", "\x1b[1;1:3A", [k("up", { raw: "\x1b[1;1:3A", eventType: "release" })]],
549
- ["ctrl+right", "\x1b[1;5:1C", [k("right", { raw: "\x1b[1;5:1C", ctrl: true })]],
550
- ["shift+left", "\x1b[1;2:1D", [k("left", { raw: "\x1b[1;2:1D", shift: true })]],
551
- ["home", "\x1b[1;1:1H", [k("home", { raw: "\x1b[1;1:1H" })]],
552
- ["end release", "\x1b[1;1:3F", [k("end", { raw: "\x1b[1;1:3F", eventType: "release" })]],
553
- ["f1 press", "\x1b[1;1:1P", [k("f1", { raw: "\x1b[1;1:1P" })]],
554
- ])
555
-
556
- // CSI number;modifier:event ~ format (kitty tilde keys)
557
- table([
558
- ["pageup press", "\x1b[5;1:1~", [k("pageup", { raw: "\x1b[5;1:1~" })]],
559
- ["ctrl+delete", "\x1b[3;5:1~", [k("delete", { raw: "\x1b[3;5:1~", ctrl: true })]],
560
- ["insert release", "\x1b[2;1:3~", [k("insert", { raw: "\x1b[2;1:3~", eventType: "release" })]],
561
- ])
562
- })
563
-
564
- describe("modifyOtherKeys", () => {
565
- table([
566
- ["shift+return", "\x1b[27;2;13~", [k("return", { raw: "\x1b[27;2;13~", shift: true })]],
567
- ["ctrl+return", "\x1b[27;5;13~", [k("return", { raw: "\x1b[27;5;13~", ctrl: true })]],
568
- ["ctrl+escape", "\x1b[27;5;27~", [k("escape", { raw: "\x1b[27;5;27~", ctrl: true })]],
569
- ["alt+tab", "\x1b[27;3;9~", [k("tab", { raw: "\x1b[27;3;9~", meta: true })]],
570
- ["shift+space", "\x1b[27;2;32~", [k("space", { raw: "\x1b[27;2;32~", shift: true })]],
571
- ["ctrl+backspace", "\x1b[27;5;127~", [k("backspace", { raw: "\x1b[27;5;127~", ctrl: true })]],
572
- ["shift+digit 5", "\x1b[27;2;53~", [k("5", { raw: "\x1b[27;2;53~", shift: true })]],
573
- ])
574
- })
575
-
576
- describe("mouse: SGR protocol", () => {
577
- table([
578
- // Button press/release
579
- ["left down", "\x1b[<0;1;1M", [sgr("\x1b[<0;1;1M", "down", 0, 0)]],
580
- ["left up", "\x1b[<0;1;1m", [sgr("\x1b[<0;1;1m", "up", 0, 0)]],
581
- ["middle down", "\x1b[<1;1;1M", [sgr("\x1b[<1;1;1M", "down", 0, 0, { button: 1 })]],
582
- ["middle up", "\x1b[<1;1;1m", [sgr("\x1b[<1;1;1m", "up", 0, 0, { button: 1 })]],
583
- ["right down", "\x1b[<2;1;1M", [sgr("\x1b[<2;1;1M", "down", 0, 0, { button: 2 })]],
584
- ["right up", "\x1b[<2;1;1m", [sgr("\x1b[<2;1;1m", "up", 0, 0, { button: 2 })]],
585
- // Scroll
586
- [
587
- "scroll up",
588
- "\x1b[<64;10;5M",
589
- [sgr("\x1b[<64;10;5M", "scroll", 9, 4, { scroll: { direction: "up", delta: 1 } })],
590
- ],
591
- [
592
- "scroll down",
593
- "\x1b[<65;10;5M",
594
- [sgr("\x1b[<65;10;5M", "scroll", 9, 4, { button: 1, scroll: { direction: "down", delta: 1 } })],
595
- ],
596
- [
597
- "scroll left",
598
- "\x1b[<66;10;5M",
599
- [sgr("\x1b[<66;10;5M", "scroll", 9, 4, { button: 2, scroll: { direction: "left", delta: 1 } })],
600
- ],
601
- [
602
- "scroll right",
603
- "\x1b[<67;10;5M",
604
- [sgr("\x1b[<67;10;5M", "scroll", 9, 4, { button: 0, scroll: { direction: "right", delta: 1 } })],
605
- ],
606
- // Motion (no button)
607
- ["move", "\x1b[<35;20;10M", [sgr("\x1b[<35;20;10M", "move", 19, 9)]],
608
- // Large coordinates
609
- ["large coords", "\x1b[<0;300;200M", [sgr("\x1b[<0;300;200M", "down", 299, 199)]],
610
- // Modifiers
611
- ["shift+left down", "\x1b[<4;1;1M", [sgr("\x1b[<4;1;1M", "down", 0, 0, { mods: { shift: true } })]],
612
- ["alt+left down", "\x1b[<8;1;1M", [sgr("\x1b[<8;1;1M", "down", 0, 0, { mods: { alt: true } })]],
613
- ["ctrl+left down", "\x1b[<16;1;1M", [sgr("\x1b[<16;1;1M", "down", 0, 0, { mods: { ctrl: true } })]],
614
- ])
615
-
616
- test("drag detection after button down", () => {
617
- const p = createParser()
618
- try {
619
- // Button 0 down, then motion with button 0 flag
620
- p.push(Buffer.from("\x1b[<0;5;5M\x1b[<32;6;5M"))
621
- const s = snap(p)
622
- expect(s).toHaveLength(2)
623
- expect(s[0]).toEqual(sgr("\x1b[<0;5;5M", "down", 4, 4))
624
- expect(s[1]).toEqual(sgr("\x1b[<32;6;5M", "drag", 5, 4))
625
- } finally {
626
- p.destroy()
627
- }
628
- })
629
-
630
- test("split SGR across two pushes", () => {
631
- const p = createParser()
632
- try {
633
- p.push(Buffer.from("\x1b[<64;10;"))
634
- expect(snap(p)).toEqual([])
635
- p.push(Buffer.from("5M"))
636
- expect(snap(p)).toEqual([sgr("\x1b[<64;10;5M", "scroll", 9, 4, { scroll: { direction: "up", delta: 1 } })])
637
- } finally {
638
- p.destroy()
639
- }
640
- })
641
-
642
- test("multiple mouse events in one push", () => {
643
- const p = createParser()
644
- try {
645
- p.push(Buffer.from("\x1b[<0;1;1M\x1b[<0;2;1M\x1b[<0;2;1m"))
646
- const s = snap(p)
647
- expect(s).toHaveLength(3)
648
- expect(s[0]).toEqual(sgr("\x1b[<0;1;1M", "down", 0, 0))
649
- expect(s[1]).toEqual(sgr("\x1b[<0;2;1M", "down", 1, 0))
650
- expect(s[2]).toEqual(sgr("\x1b[<0;2;1m", "up", 1, 0))
651
- } finally {
652
- p.destroy()
653
- }
654
- })
655
- })
656
-
657
- describe("mouse: X10 protocol", () => {
658
- // X10: ESC [ M <button+32> <x+33> <y+33>
659
- const leftDown = x10bytes(0, 0, 0)
660
- const middleDown = x10bytes(1, 0, 0)
661
- const rightDown = x10bytes(2, 0, 0)
662
- const release = x10bytes(3, 0, 0)
663
- const at1020 = x10bytes(0, 10, 20)
664
- const move = x10bytes(35, 4, 5)
665
- const scrollUp = x10bytes(64, 2, 3)
666
- const shiftLeftDown = x10bytes(4, 0, 0)
667
- const ctrlScrollUp = x10bytes(80, 7, 8)
668
-
669
- table([
670
- ["left down (0,0)", leftDown, [x10m(latin1(leftDown), "down", 0, 0)]],
671
- ["middle down", middleDown, [x10m(latin1(middleDown), "down", 0, 0, { button: 1 })]],
672
- ["right down", rightDown, [x10m(latin1(rightDown), "down", 0, 0, { button: 2 })]],
673
- ["release", release, [x10m(latin1(release), "up", 0, 0)]],
674
- ["at position 10,20", at1020, [x10m(latin1(at1020), "down", 10, 20)]],
675
- ["move with no button", move, [x10m(latin1(move), "move", 4, 5, { button: -1 })]],
676
- ["scroll up", scrollUp, [x10m(latin1(scrollUp), "scroll", 2, 3, { scroll: { direction: "up", delta: 1 } })]],
677
- ["shift+left down", shiftLeftDown, [x10m(latin1(shiftLeftDown), "down", 0, 0, { mods: { shift: true } })]],
678
- [
679
- "ctrl+scroll up",
680
- ctrlScrollUp,
681
- [x10m(latin1(ctrlScrollUp), "scroll", 7, 8, { mods: { ctrl: true }, scroll: { direction: "up", delta: 1 } })],
682
- ],
683
- ])
684
-
685
- test("X10 mouse followed by key", () => {
686
- const p = createParser()
687
- try {
688
- p.push(Buffer.from("\x1b[M !!x"))
689
- const s = snap(p)
690
- expect(s).toHaveLength(2)
691
- expect(s[0]).toEqual(x10m("\x1b[M !!", "down", 0, 0))
692
- expect(s[1]).toEqual(k("x"))
693
- } finally {
694
- p.destroy()
695
- }
696
- })
697
-
698
- test("split X10 across pushes waits for payload", () => {
699
- const p = createParser()
700
- try {
701
- p.push(Buffer.from("\x1b[M"))
702
- expect(snap(p)).toEqual([])
703
- p.push(Buffer.from(" !!"))
704
- expect(snap(p)).toEqual([x10m("\x1b[M !!", "down", 0, 0)])
705
- } finally {
706
- p.destroy()
707
- }
708
- })
709
-
710
- test("delayed X10 continuation after timed-out escape stays opaque", () => {
711
- const { parser, clock } = createTimedParser()
712
- try {
713
- parser.push(Buffer.from("\x1b"))
714
- expect(snap(parser)).toEqual([])
715
- clock.advance(10)
716
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
717
-
718
- parser.push(Buffer.from("[M"))
719
- expect(snap(parser)).toEqual([])
720
- parser.push(Buffer.from(" !!"))
721
- expect(snap(parser)).toEqual([resp("unknown", "[M !!")])
722
- } finally {
723
- parser.destroy()
724
- }
725
- })
726
- })
727
-
728
- describe("UTF-8 handling", () => {
729
- table([
730
- ["2-byte (é)", "\u00e9", [k("\u00e9")]],
731
- ["3-byte (中)", "\u4e2d", [k("\u4e2d")]],
732
- ["4-byte (👍)", "👍", [k("👍")]],
733
- ["multiple utf-8 chars", "日本語", [k("日"), k("本"), k("語")]],
734
- ])
735
-
736
- test("2-byte split at byte boundary", () => {
737
- const bytes = Buffer.from("é")
738
- expect(bytes.length).toBe(2)
739
- const p = createParser()
740
- try {
741
- p.push(bytes.subarray(0, 1))
742
- expect(snap(p)).toEqual([])
743
- p.push(bytes.subarray(1))
744
- expect(snap(p)).toEqual([k("é")])
745
- } finally {
746
- p.destroy()
747
- }
748
- })
749
-
750
- test("3-byte split at every boundary", () => {
751
- const bytes = Buffer.from("中")
752
- expect(bytes.length).toBe(3)
753
- for (let split = 1; split < bytes.length; split++) {
754
- const p = createParser()
755
- try {
756
- p.push(bytes.subarray(0, split))
757
- expect(snap(p)).toEqual([])
758
- p.push(bytes.subarray(split))
759
- expect(snap(p)).toEqual([k("中")])
760
- } finally {
761
- p.destroy()
762
- }
763
- }
764
- })
765
-
766
- test("4-byte split at every boundary", () => {
767
- const bytes = Buffer.from("👍")
768
- expect(bytes.length).toBe(4)
769
- for (let split = 1; split < bytes.length; split++) {
770
- const p = createParser()
771
- try {
772
- p.push(bytes.subarray(0, split))
773
- expect(snap(p)).toEqual([])
774
- p.push(bytes.subarray(split))
775
- expect(snap(p)).toEqual([k("👍")])
776
- } finally {
777
- p.destroy()
778
- }
779
- }
780
- })
781
-
782
- test("invalid UTF-8 lead (0xC0) followed by ASCII falls back to legacy high-byte", () => {
783
- const p = createParser()
784
- try {
785
- p.push(Uint8Array.from([0xc0, 0x41]))
786
- const s = snap(p)
787
- expect(s).toHaveLength(2)
788
- // 0xC0 - 128 = 0x40 = '@', treated as ESC + '@' → legacy path
789
- expect(s[0]!.type).toBe("key")
790
- expect(s[1]).toEqual(k("a", { raw: "A", shift: true }))
791
- } finally {
792
- p.destroy()
793
- }
794
- })
795
-
796
- test("invalid continuation byte after valid lead falls back to legacy", () => {
797
- const p = createParser()
798
- try {
799
- p.push(Uint8Array.from([0xe9])) // valid 3-byte lead
800
- expect(snap(p)).toEqual([]) // waits for continuation
801
- p.push(Buffer.from("x")) // not a continuation byte
802
- const s = snap(p)
803
- expect(s).toEqual([
804
- k("i", { raw: "\x1bi", meta: true }), // 0xe9 → legacy: 0xe9-128=0x69='i', ESC prefix
805
- k("x"),
806
- ])
807
- } finally {
808
- p.destroy()
809
- }
810
- })
811
-
812
- test("legacy single high-byte on timeout", () => {
813
- const { parser, clock } = createTimedParser()
814
- try {
815
- parser.push(Uint8Array.from([0xe9]))
816
- expect(snap(parser)).toEqual([])
817
- clock.advance(10)
818
- expect(snap(parser)).toEqual([k("i", { raw: "\x1bi", meta: true })])
819
- } finally {
820
- parser.destroy()
821
- }
822
- })
823
-
824
- test("high byte 0xFF on timeout → meta+backspace", () => {
825
- const { parser, clock } = createTimedParser()
826
- try {
827
- parser.push(Uint8Array.from([0xff]))
828
- expect(snap(parser)).toEqual([])
829
- clock.advance(10)
830
- // 0xFF - 128 = 0x7F = DEL, so ESC + DEL = meta+backspace
831
- expect(snap(parser)).toEqual([k("backspace", { raw: "\x1b\x7f", meta: true })])
832
- } finally {
833
- parser.destroy()
834
- }
835
- })
836
- })
837
-
838
- describe("protocol responses", () => {
839
- table([
840
- // OSC (BEL-terminated)
841
- ["OSC (BEL)", "\x1b]4;0;#ffffff\x07", [resp("osc", "\x1b]4;0;#ffffff\x07")]],
842
- // OSC (ESC \\ terminated)
843
- ["OSC (ST)", "\x1b]4;0;rgb:ff/ff/ff\x1b\\", [resp("osc", "\x1b]4;0;rgb:ff/ff/ff\x1b\\")]],
844
- // DCS
845
- ["DCS", "\x1bP>|kitty(0.40.1)\x1b\\", [resp("dcs", "\x1bP>|kitty(0.40.1)\x1b\\")]],
846
- // APC
847
- ["APC", "\x1b_Gi=1;OK\x1b\\", [resp("apc", "\x1b_Gi=1;OK\x1b\\")]],
848
- // Focus
849
- ["focus in", "\x1b[I", [resp("csi", "\x1b[I")]],
850
- ["focus out", "\x1b[O", [resp("csi", "\x1b[O")]],
851
- // DA (Device Attributes)
852
- ["DA1", "\x1b[?62;1;2;6;7;8;9;15;22c", [resp("csi", "\x1b[?62;1;2;6;7;8;9;15;22c")]],
853
- // CPR (Cursor Position Report)
854
- ["CPR", "\x1b[24;80R", [resp("csi", "\x1b[24;80R")]],
855
- // Window/cell size
856
- ["window size", "\x1b[4;600;800t", [resp("csi", "\x1b[4;600;800t")]],
857
- // Mode report
858
- ["mode report", "\x1b[?2004;1$y", [resp("csi", "\x1b[?2004;1$y")]],
859
- ])
860
-
861
- test("all three protocol responses in one push", () => {
862
- const p = createParser()
863
- try {
864
- p.push(Buffer.from("\x1b]4;0;#fff\x07\x1bP>|test\x1b\\\x1b_OK\x1b\\"))
865
- expect(snap(p)).toEqual([
866
- resp("osc", "\x1b]4;0;#fff\x07"),
867
- resp("dcs", "\x1bP>|test\x1b\\"),
868
- resp("apc", "\x1b_OK\x1b\\"),
869
- ])
870
- } finally {
871
- p.destroy()
872
- }
873
- })
874
-
875
- test("split OSC across pushes", () => {
876
- const p = createParser()
877
- try {
878
- p.push(Buffer.from("\x1b]4;0;"))
879
- expect(snap(p)).toEqual([])
880
- p.push(Buffer.from("#ffffff\x07"))
881
- expect(snap(p)).toEqual([resp("osc", "\x1b]4;0;#ffffff\x07")])
882
- } finally {
883
- p.destroy()
884
- }
885
- })
886
-
887
- test("split DCS terminator ESC \\ across pushes", () => {
888
- const p = createParser()
889
- try {
890
- p.push(Buffer.from("\x1bPtest\x1b"))
891
- expect(snap(p)).toEqual([])
892
- p.push(Buffer.from("\\"))
893
- expect(snap(p)).toEqual([resp("dcs", "\x1bPtest\x1b\\")])
894
- } finally {
895
- p.destroy()
896
- }
897
- })
898
-
899
- test("focus events interleaved with keys", () => {
900
- const p = createParser()
901
- try {
902
- p.push(Buffer.from("a\x1b[Ib\x1b[Oc"))
903
- expect(snap(p)).toEqual([k("a"), resp("csi", "\x1b[I"), k("b"), resp("csi", "\x1b[O"), k("c")])
904
- } finally {
905
- p.destroy()
906
- }
907
- })
908
-
909
- test("timeout flushes partial OSC as unknown", () => {
910
- const { parser, clock } = createTimedParser()
911
- try {
912
- parser.push(Buffer.from("\x1b]incomplete"))
913
- expect(snap(parser)).toEqual([])
914
- clock.advance(10)
915
- expect(snap(parser)).toEqual([resp("unknown", "\x1b]incomplete")])
916
- } finally {
917
- parser.destroy()
918
- }
919
- })
920
-
921
- test("timeout flushes partial DCS as unknown", () => {
922
- const { parser, clock } = createTimedParser()
923
- try {
924
- parser.push(Buffer.from("\x1bPpartial"))
925
- expect(snap(parser)).toEqual([])
926
- clock.advance(10)
927
- expect(snap(parser)).toEqual([resp("unknown", "\x1bPpartial")])
928
- } finally {
929
- parser.destroy()
930
- }
931
- })
932
-
933
- test("timeout flushes partial APC as unknown", () => {
934
- const { parser, clock } = createTimedParser()
935
- try {
936
- parser.push(Buffer.from("\x1b_partial"))
937
- expect(snap(parser)).toEqual([])
938
- clock.advance(10)
939
- expect(snap(parser)).toEqual([resp("unknown", "\x1b_partial")])
940
- } finally {
941
- parser.destroy()
942
- }
943
- })
944
-
945
- test("timeout flushes partial CSI as unknown", () => {
946
- const { parser, clock } = createTimedParser()
947
- try {
948
- parser.push(Buffer.from("\x1b[123"))
949
- expect(snap(parser)).toEqual([])
950
- clock.advance(10)
951
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[123")])
952
- } finally {
953
- parser.destroy()
954
- }
955
- })
956
- })
957
-
958
- describe("bracketed paste", () => {
959
- table([
960
- ["simple paste", "\x1b[200~hello\x1b[201~", [paste("hello")]],
961
- ["empty paste", "\x1b[200~\x1b[201~", [paste("")]],
962
- ["paste with newlines", "\x1b[200~line1\nline2\x1b[201~", [paste("line1\nline2")]],
963
- ["paste with tabs", "\x1b[200~a\tb\x1b[201~", [paste("a\tb")]],
964
- ["paste with ESC in body", "\x1b[200~abc\x1bdef\x1b[201~", [paste("abc\x1bdef")]],
965
- ])
966
-
967
- test("split paste start marker across pushes", () => {
968
- const start = "\x1b[200~"
969
- for (let split = 1; split < start.length; split++) {
970
- const p = createParser()
971
- try {
972
- p.push(Buffer.from(start.slice(0, split)))
973
- p.push(Buffer.from(start.slice(split) + "hi\x1b[201~"))
974
- expect(snap(p)).toEqual([paste("hi")])
975
- } finally {
976
- p.destroy()
977
- }
978
- }
979
- })
980
-
981
- test("split paste end marker at every boundary", () => {
982
- const end = "\x1b[201~"
983
- for (let split = 1; split < end.length; split++) {
984
- const p = createParser()
985
- try {
986
- p.push(Buffer.from("\x1b[200~hello"))
987
- p.push(Buffer.from(end.slice(0, split)))
988
- expect(snap(p)).toEqual([])
989
- p.push(Buffer.from(end.slice(split)))
990
- expect(snap(p)).toEqual([paste("hello")])
991
- } finally {
992
- p.destroy()
993
- }
994
- }
995
- })
996
-
997
- test("paste body bytes do not alias caller buffers across pushes", () => {
998
- const p = createParser()
999
- try {
1000
- p.push(Buffer.from("\x1b[200~"))
1001
-
1002
- const chunk = Buffer.from("hello")
1003
- p.push(chunk)
1004
- chunk.fill(0x78)
1005
-
1006
- p.push(Buffer.from("\x1b[201~"))
1007
- expect(snap(p)).toEqual([paste("hello")])
1008
- } finally {
1009
- p.destroy()
1010
- }
1011
- })
1012
-
1013
- test("near-match end markers are part of paste body", () => {
1014
- const p = createParser()
1015
- try {
1016
- p.push(Buffer.from("\x1b[200~abc\x1b[202~def\x1b[201~"))
1017
- expect(snap(p)).toEqual([paste("abc\x1b[202~def")])
1018
- } finally {
1019
- p.destroy()
1020
- }
1021
- })
1022
-
1023
- test("doubled ESC before paste end marker", () => {
1024
- const p = createParser()
1025
- try {
1026
- p.push(Buffer.from("\x1b[200~abc\x1b"))
1027
- expect(snap(p)).toEqual([])
1028
- p.push(Buffer.from("\x1b[201~"))
1029
- expect(snap(p)).toEqual([paste("abc\x1b")])
1030
- } finally {
1031
- p.destroy()
1032
- }
1033
- })
1034
-
1035
- test("large paste does not grow parser buffer", () => {
1036
- const p = createParser({ maxPendingBytes: 32 })
1037
- const payload = "x".repeat(100_000)
1038
- try {
1039
- p.push(Buffer.from(`\x1b[200~${payload}\x1b[201~z`))
1040
- expect(snap(p)).toEqual([paste(payload), k("z")])
1041
- expect(p.bufferCapacity).toBeLessThanOrEqual(512)
1042
- } finally {
1043
- p.destroy()
1044
- }
1045
- })
1046
-
1047
- test("large paste across many small chunks", () => {
1048
- const p = createParser({ maxPendingBytes: 32 })
1049
- try {
1050
- p.push(Buffer.from("\x1b[200~"))
1051
- for (let i = 0; i < 1000; i++) p.push(Buffer.from("chunk "))
1052
- p.push(Buffer.from("\x1b[201~"))
1053
- const s = snap(p)
1054
- expect(s).toHaveLength(1)
1055
- expect(s[0]!.type).toBe("paste")
1056
- expect((s[0] as PasteSnap).bytes).toHaveLength(6000)
1057
- expect(p.bufferCapacity).toBeLessThanOrEqual(512)
1058
- } finally {
1059
- p.destroy()
1060
- }
1061
- })
1062
-
1063
- test("trailing bytes after paste end are parsed normally", () => {
1064
- const p = createParser()
1065
- try {
1066
- p.push(Buffer.from("\x1b[200~hello\x1b[201~\x1b[A"))
1067
- expect(snap(p)).toEqual([paste("hello"), k("up", { raw: "\x1b[A" })])
1068
- } finally {
1069
- p.destroy()
1070
- }
1071
- })
1072
-
1073
- test("back-to-back pastes", () => {
1074
- const p = createParser()
1075
- try {
1076
- p.push(Buffer.from("\x1b[200~first\x1b[201~\x1b[200~second\x1b[201~"))
1077
- expect(snap(p)).toEqual([paste("first"), paste("second")])
1078
- } finally {
1079
- p.destroy()
1080
- }
1081
- })
1082
-
1083
- test("paste with UTF-8 content", () => {
1084
- const p = createParser()
1085
- try {
1086
- p.push(Buffer.from("\x1b[200~日本語👍\x1b[201~"))
1087
- expect(snap(p)).toEqual([paste("日本語👍")])
1088
- } finally {
1089
- p.destroy()
1090
- }
1091
- })
1092
-
1093
- test("paste with UTF-8 split across chunks", () => {
1094
- const p = createParser()
1095
- const emoji = Buffer.from("👍")
1096
- try {
1097
- p.push(Buffer.from("\x1b[200~"))
1098
- p.push(emoji.subarray(0, 2))
1099
- p.push(emoji.subarray(2))
1100
- p.push(Buffer.from("\x1b[201~"))
1101
- expect(snap(p)).toEqual([paste("👍")])
1102
- } finally {
1103
- p.destroy()
1104
- }
1105
- })
1106
- })
1107
-
1108
- describe("ESC-less SGR continuation recovery", () => {
1109
- test("after timed-out ESC, continuation is not split into text", () => {
1110
- const { parser, clock } = createTimedParser()
1111
- try {
1112
- parser.push(Buffer.from("\x1b"))
1113
- clock.advance(10)
1114
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1115
-
1116
- parser.push(Buffer.from("[<35;20;5m"))
1117
- expect(snap(parser)).toEqual([resp("unknown", "[<35;20;5m")])
1118
- } finally {
1119
- parser.destroy()
1120
- }
1121
- })
1122
-
1123
- test("after timed-out ESC, split continuation across pushes is not split into text", () => {
1124
- const { parser, clock } = createTimedParser()
1125
- try {
1126
- parser.push(Buffer.from("\x1b"))
1127
- clock.advance(10)
1128
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1129
-
1130
- parser.push(Buffer.from("["))
1131
- expect(snap(parser)).toEqual([])
1132
-
1133
- parser.push(Buffer.from("<35;20;5m"))
1134
- expect(snap(parser)).toEqual([resp("unknown", "[<35;20;5m")])
1135
- } finally {
1136
- parser.destroy()
1137
- }
1138
- })
1139
-
1140
- test("after timed-out ESC, partial [< waits, then timeout flushes as one response", () => {
1141
- const { parser, clock } = createTimedParser()
1142
- try {
1143
- parser.push(Buffer.from("\x1b"))
1144
- clock.advance(10)
1145
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1146
-
1147
- parser.push(Buffer.from("[<35;20"))
1148
- expect(snap(parser)).toEqual([])
1149
- clock.advance(10)
1150
- expect(snap(parser)).toEqual([resp("unknown", "[<35;20")])
1151
- } finally {
1152
- parser.destroy()
1153
- }
1154
- })
1155
-
1156
- test("after timed-out ESC, [< followed by non-digit aborts immediately", () => {
1157
- const { parser, clock } = createTimedParser()
1158
- try {
1159
- parser.push(Buffer.from("\x1b"))
1160
- clock.advance(10)
1161
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1162
-
1163
- parser.push(Buffer.from("[<x"))
1164
- const s = snap(parser)
1165
- expect(s).toHaveLength(2)
1166
- expect(s[0]).toEqual(resp("unknown", "[<"))
1167
- expect(s[1]).toEqual(k("x"))
1168
- } finally {
1169
- parser.destroy()
1170
- }
1171
- })
1172
-
1173
- test("without prior flushed ESC, [< stays literal text", () => {
1174
- const p = createParser()
1175
- try {
1176
- p.push(Buffer.from("[<35;20;5m"))
1177
- expect(snap(p)).toEqual("[<35;20;5m".split("").map((char) => k(char)))
1178
- } finally {
1179
- p.destroy()
1180
- }
1181
- })
1182
-
1183
- test("without prior flushed ESC, standalone [ then < stay as individual keys", () => {
1184
- const p = createParser()
1185
- try {
1186
- p.push(Buffer.from("["))
1187
- expect(snap(p)).toEqual([k("[")])
1188
- p.push(Buffer.from("<"))
1189
- expect(snap(p)).toEqual([k("<")])
1190
- } finally {
1191
- p.destroy()
1192
- }
1193
- })
1194
-
1195
- test("after timed-out ESC, bare [ waits for more and then flushes as text", () => {
1196
- const { parser, clock } = createTimedParser()
1197
- try {
1198
- parser.push(Buffer.from("\x1b"))
1199
- clock.advance(10)
1200
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1201
-
1202
- parser.push(Buffer.from("["))
1203
- expect(snap(parser)).toEqual([])
1204
- clock.advance(10)
1205
- expect(snap(parser)).toEqual([k("[")])
1206
- } finally {
1207
- parser.destroy()
1208
- }
1209
- })
1210
- })
1211
-
1212
- describe("timeout behavior", () => {
1213
- test("timeout at exact boundary (9ms no fire, 10ms fires)", () => {
1214
- const { parser, clock } = createTimedParser()
1215
- try {
1216
- parser.push(Buffer.from("\x1b"))
1217
- clock.advance(9)
1218
- expect(snap(parser)).toEqual([])
1219
- clock.advance(1)
1220
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1221
- } finally {
1222
- parser.destroy()
1223
- }
1224
- })
1225
-
1226
- test("timeout resets when more bytes arrive", () => {
1227
- const { parser, clock } = createTimedParser()
1228
- try {
1229
- parser.push(Buffer.from("\x1b[<35;20;"))
1230
- clock.advance(9) // almost timeout
1231
- parser.push(Buffer.from("5")) // new byte resets timer
1232
- expect(snap(parser)).toEqual([])
1233
- clock.advance(9) // almost timeout again
1234
- expect(snap(parser)).toEqual([])
1235
- parser.push(Buffer.from("m")) // complete
1236
- expect(snap(parser)).toEqual([sgr("\x1b[<35;20;5m", "move", 19, 4)])
1237
- } finally {
1238
- parser.destroy()
1239
- }
1240
- })
1241
-
1242
- test("timeout does not fire during paste mode", () => {
1243
- const { parser, clock } = createTimedParser()
1244
- try {
1245
- parser.push(Buffer.from("\x1b[200~partial"))
1246
- clock.advance(100) // way past timeout
1247
- expect(snap(parser)).toEqual([]) // still collecting paste
1248
- parser.push(Buffer.from("\x1b[201~"))
1249
- expect(snap(parser)).toEqual([paste("partial")])
1250
- } finally {
1251
- parser.destroy()
1252
- }
1253
- })
1254
-
1255
- test("multiple sequential timeouts", () => {
1256
- const { parser, clock } = createTimedParser()
1257
- try {
1258
- parser.push(Buffer.from("\x1b"))
1259
- clock.advance(10)
1260
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1261
-
1262
- parser.push(Buffer.from("\x1b"))
1263
- clock.advance(10)
1264
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1265
-
1266
- parser.push(Buffer.from("\x1b"))
1267
- clock.advance(10)
1268
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1269
- } finally {
1270
- parser.destroy()
1271
- }
1272
- })
1273
-
1274
- test("custom timeout delay", () => {
1275
- const { parser, clock } = createTimedParser({ timeoutMs: 50 })
1276
- try {
1277
- parser.push(Buffer.from("\x1b"))
1278
- clock.advance(49)
1279
- expect(snap(parser)).toEqual([])
1280
- clock.advance(1)
1281
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1282
- } finally {
1283
- parser.destroy()
1284
- }
1285
- })
1286
-
1287
- test("data completing sequence before timeout cancels flush", () => {
1288
- const { parser, clock } = createTimedParser()
1289
- try {
1290
- parser.push(Buffer.from("\x1b"))
1291
- clock.advance(5) // halfway to timeout
1292
- parser.push(Buffer.from("[A")) // completes arrow sequence
1293
- expect(snap(parser)).toEqual([k("up", { raw: "\x1b[A" })])
1294
- clock.advance(100) // timeout would have fired, but sequence is done
1295
- expect(snap(parser)).toEqual([])
1296
- } finally {
1297
- parser.destroy()
1298
- }
1299
- })
1300
- })
1301
-
1302
- describe("embedded ESC abort", () => {
1303
- test("ESC inside partial CSI flushes as unknown, restarts", () => {
1304
- const p = createParser()
1305
- try {
1306
- p.push(Buffer.from("\x1b[<35;\x1b[<35;20;5m"))
1307
- expect(snap(p)).toEqual([resp("unknown", "\x1b[<35;"), sgr("\x1b[<35;20;5m", "move", 19, 4)])
1308
- } finally {
1309
- p.destroy()
1310
- }
1311
- })
1312
-
1313
- test("ESC inside partial CSI with no following data", () => {
1314
- const { parser, clock } = createTimedParser()
1315
- try {
1316
- parser.push(Buffer.from("\x1b[123\x1b"))
1317
- const s = snap(parser)
1318
- // first part flushed as unknown response, ESC starts new escape
1319
- expect(s).toEqual([resp("unknown", "\x1b[123")])
1320
- // the trailing ESC is pending
1321
- clock.advance(10)
1322
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1323
- } finally {
1324
- parser.destroy()
1325
- }
1326
- })
1327
-
1328
- test("ESC inside OSC restarts parsing", () => {
1329
- const p = createParser()
1330
- try {
1331
- // ESC ] ... ESC ESC [ A — the first ESC after OSC body starts ST check,
1332
- // but second ESC byte is not \, so sawEsc resets. Then ESC starts escape.
1333
- // Actually: \x1b]foo has sawEsc=false. Then \x1b sets sawEsc=true.
1334
- // Then [ is not \, so sawEsc resets to false and [ is consumed as content.
1335
- // Then \x1b sets sawEsc=true. Then \ (0x5c = \\) terminates OSC.
1336
- p.push(Buffer.from("\x1b]foo\x1b\\"))
1337
- expect(snap(p)).toEqual([resp("osc", "\x1b]foo\x1b\\")])
1338
- } finally {
1339
- p.destroy()
1340
- }
1341
- })
1342
-
1343
- test("ESC in SS3 flushes partial as unknown", () => {
1344
- const p = createParser()
1345
- try {
1346
- p.push(Buffer.from("\x1bO\x1b[A"))
1347
- expect(snap(p)).toEqual([resp("unknown", "\x1bO"), k("up", { raw: "\x1b[A" })])
1348
- } finally {
1349
- p.destroy()
1350
- }
1351
- })
1352
- })
1353
-
1354
- describe("chunk-shape invariance", () => {
1355
- const sequences = [
1356
- "abc", // multiple ASCII
1357
- "\x1b[A", // arrow
1358
- "\x1bOP", // SS3 F1
1359
- "\x1b[[A", // Cygwin F1
1360
- "\x1b[[5~", // putty pageup
1361
- "\x1b[<0;10;20M", // SGR mouse
1362
- "\x1b[M !!", // X10 mouse
1363
- "\x1b]4;0;#ffffff\x07", // OSC
1364
- "\x1bP>|test\x1b\\", // DCS
1365
- "\x1b_OK\x1b\\", // APC
1366
- "\x1b[200~hello\x1b[201~", // paste
1367
- "\x1b[I", // focus in
1368
- "\x1b[1;5A", // ctrl+up
1369
- "\x1b[97u", // kitty key
1370
- "\x1b[27;2;13~", // modifyOtherKeys
1371
- ]
1372
-
1373
- for (const seq of sequences) {
1374
- test(`byte-at-a-time: ${JSON.stringify(seq).slice(1, -1).slice(0, 30)}`, () => {
1375
- assertChunkInvariant(Buffer.from(seq))
1376
- })
1377
- }
1378
-
1379
- test("mixed stream byte-at-a-time", () => {
1380
- const stream = Buffer.concat([
1381
- Buffer.from("x"),
1382
- Buffer.from("\x1b[<64;10;5M"),
1383
- Buffer.from("\x1b[I"),
1384
- Buffer.from("\x1b]4;0;#fff\x07"),
1385
- Buffer.from("\x1b[200~paste\x1b[201~"),
1386
- Buffer.from("👍"),
1387
- ])
1388
- assertChunkInvariant(stream)
1389
- })
1390
-
1391
- test("random two-chunk splits", () => {
1392
- const stream = Buffer.from("x\x1b[<64;10;5M\x1b[I\x1b]4;0;#fff\x07\x1b[200~p\x1b[201~y")
1393
- const whole = createParser()
1394
- try {
1395
- whole.push(stream)
1396
- const expected = snap(whole)
1397
- // Try splitting at every possible position
1398
- for (let split = 1; split < stream.length - 1; split++) {
1399
- const p = createParser()
1400
- try {
1401
- p.push(stream.subarray(0, split))
1402
- p.push(stream.subarray(split))
1403
- expect(snap(p)).toEqual(expected)
1404
- } finally {
1405
- p.destroy()
1406
- }
1407
- }
1408
- } finally {
1409
- whole.destroy()
1410
- }
1411
- })
1412
-
1413
- const comboAtoms: Array<[label: string, input: ChunkInput]> = [
1414
- ["ascii", "xy"],
1415
- ["utf8", "👍"],
1416
- ["arrow", "\x1b[A"],
1417
- ["sgr", "\x1b[<64;10;5M"],
1418
- ["x10", x10bytes(0, 0, 0)],
1419
- ["osc", "\x1b]4;0;#fff\x07"],
1420
- ["paste", "\x1b[200~p\x1b[201~"],
1421
- ["kitty", "\x1b[97u"],
1422
- ]
1423
-
1424
- for (const [firstLabel, first] of comboAtoms) {
1425
- for (const [secondLabel, second] of comboAtoms) {
1426
- test(`${firstLabel} + ${secondLabel} across every two-chunk split`, () => {
1427
- const stream = concatChunks([first, second])
1428
- const expected = snapChunks([stream])
1429
-
1430
- expect(snapChunks([first, second])).toEqual(expected)
1431
- for (let split = 1; split < stream.length; split++) {
1432
- expect(snapChunks([stream.subarray(0, split), stream.subarray(split)])).toEqual(expected)
1433
- }
1434
- })
1435
- }
1436
- }
1437
- })
1438
-
1439
- describe("state management", () => {
1440
- test("reset clears pending bytes and releases capacity", () => {
1441
- const p = createParser()
1442
- try {
1443
- p.push(Buffer.from("\x1b["))
1444
- expect(snap(p)).toEqual([])
1445
- p.push(Buffer.alloc(4096, 0x78)) // 'x' bytes to grow buffer
1446
- p.reset()
1447
- expect(snap(p)).toEqual([])
1448
- expect(p.bufferCapacity).toBeLessThanOrEqual(256)
1449
- // parser works normally after reset
1450
- p.push(Buffer.from("a"))
1451
- expect(snap(p)).toEqual([k("a")])
1452
- } finally {
1453
- p.destroy()
1454
- }
1455
- })
1456
-
1457
- test("reset during paste mode clears paste state", () => {
1458
- const p = createParser()
1459
- try {
1460
- p.push(Buffer.from("\x1b[200~partial paste"))
1461
- expect(snap(p)).toEqual([])
1462
- p.reset()
1463
- expect(snap(p)).toEqual([])
1464
- // parser works normally after reset
1465
- p.push(Buffer.from("a"))
1466
- expect(snap(p)).toEqual([k("a")])
1467
- } finally {
1468
- p.destroy()
1469
- }
1470
- })
1471
-
1472
- test("reset during escape sequence clears state", () => {
1473
- const p = createParser()
1474
- try {
1475
- p.push(Buffer.from("\x1b["))
1476
- expect(snap(p)).toEqual([])
1477
- p.reset()
1478
- // After reset, the partial CSI is gone; new input starts fresh
1479
- p.push(Buffer.from("A"))
1480
- expect(snap(p)).toEqual([k("a", { raw: "A", shift: true })]) // 'A' = shift+a
1481
- } finally {
1482
- p.destroy()
1483
- }
1484
- })
1485
-
1486
- test("double reset is safe", () => {
1487
- const p = createParser()
1488
- try {
1489
- p.push(Buffer.from("\x1b["))
1490
- p.reset()
1491
- p.reset()
1492
- p.push(Buffer.from("x"))
1493
- expect(snap(p)).toEqual([k("x")])
1494
- } finally {
1495
- p.destroy()
1496
- }
1497
- })
1498
-
1499
- test("double destroy is safe", () => {
1500
- const p = createParser()
1501
- p.destroy()
1502
- expect(() => p.destroy()).not.toThrow()
1503
- })
1504
-
1505
- test("push after destroy throws", () => {
1506
- const p = createParser()
1507
- p.destroy()
1508
- expect(() => p.push(Buffer.from("a"))).toThrow("destroyed")
1509
- })
1510
-
1511
- test("read after destroy throws", () => {
1512
- const p = createParser()
1513
- p.destroy()
1514
- expect(() => p.read()).toThrow("destroyed")
1515
- })
1516
-
1517
- test("drain after destroy throws", () => {
1518
- const p = createParser()
1519
- p.destroy()
1520
- expect(() => p.drain(() => {})).toThrow("destroyed")
1521
- })
1522
-
1523
- test("destroy during drain stops iteration", () => {
1524
- const p = createParser()
1525
- p.push(Buffer.from("abc"))
1526
- let count = 0
1527
- expect(() => {
1528
- p.drain(() => {
1529
- count++
1530
- if (count === 1) p.destroy()
1531
- })
1532
- }).not.toThrow()
1533
- expect(count).toBe(1)
1534
- })
1535
-
1536
- test("read returns null when queue is empty", () => {
1537
- const p = createParser()
1538
- try {
1539
- expect(p.read()).toBeNull()
1540
- } finally {
1541
- p.destroy()
1542
- }
1543
- })
1544
-
1545
- test("read pops events one at a time", () => {
1546
- const p = createParser()
1547
- try {
1548
- p.push(Buffer.from("abc"))
1549
- const e1 = p.read()
1550
- const e2 = p.read()
1551
- const e3 = p.read()
1552
- const e4 = p.read()
1553
- expect(e1).not.toBeNull()
1554
- expect(e2).not.toBeNull()
1555
- expect(e3).not.toBeNull()
1556
- expect(e4).toBeNull()
1557
- expect(snapshotEvent(e1!)).toEqual(k("a"))
1558
- expect(snapshotEvent(e2!)).toEqual(k("b"))
1559
- expect(snapshotEvent(e3!)).toEqual(k("c"))
1560
- } finally {
1561
- p.destroy()
1562
- }
1563
- })
1564
-
1565
- test("overflow flushes incomplete protocols as one unknown response and recovers", () => {
1566
- const longDigits = "1".repeat(40)
1567
- const longOsc = "a".repeat(40)
1568
- const longDcs = "x".repeat(40)
1569
- const cases: Array<[label: string, chunks: ChunkInput[], expected: Snap[]]> = [
1570
- ["CSI", [`\x1b[${longDigits}`], [resp("unknown", `\x1b[${longDigits}`)]],
1571
- ["OSC", [`\x1b]${longOsc}`], [resp("unknown", `\x1b]${longOsc}`)]],
1572
- ["DCS + recovery", [`\x1bP${longDcs}`, "z"], [resp("unknown", `\x1bP${longDcs}`), k("z")]],
1573
- ]
1574
-
1575
- for (const [label, chunks, expected] of cases) {
1576
- expect(snapChunks(chunks, { maxPendingBytes: 16 })).toEqual(expected)
1577
- }
1578
- })
1579
- })
1580
-
1581
- describe("multi-event interleaving", () => {
1582
- table([
1583
- [
1584
- "key + mouse",
1585
- "x\x1b[<64;10;5M",
1586
- [k("x"), sgr("\x1b[<64;10;5M", "scroll", 9, 4, { scroll: { direction: "up", delta: 1 } })],
1587
- ],
1588
- [
1589
- "mouse + key",
1590
- "\x1b[<64;10;5Mx",
1591
- [sgr("\x1b[<64;10;5M", "scroll", 9, 4, { scroll: { direction: "up", delta: 1 } }), k("x")],
1592
- ],
1593
- ["key + focus + key", "a\x1b[Ib", [k("a"), resp("csi", "\x1b[I"), k("b")]],
1594
- ["paste + key", "\x1b[200~hi\x1b[201~z", [paste("hi"), k("z")]],
1595
- ["multiple keys", "abc", [k("a"), k("b"), k("c")]],
1596
- [
1597
- "arrow + text + mouse",
1598
- "\x1b[Ax\x1b[<0;1;1M",
1599
- [k("up", { raw: "\x1b[A" }), k("x"), sgr("\x1b[<0;1;1M", "down", 0, 0)],
1600
- ],
1601
- ])
1602
-
1603
- test("OSC + key + mouse + paste in one push", () => {
1604
- const p = createParser()
1605
- try {
1606
- const input = "\x1b]4;0;#fff\x07a\x1b[<0;1;1M\x1b[200~p\x1b[201~"
1607
- p.push(Buffer.from(input))
1608
- expect(snap(p)).toEqual([
1609
- resp("osc", "\x1b]4;0;#fff\x07"),
1610
- k("a"),
1611
- sgr("\x1b[<0;1;1M", "down", 0, 0),
1612
- paste("p"),
1613
- ])
1614
- } finally {
1615
- p.destroy()
1616
- }
1617
- })
1618
- })
1619
-
1620
- describe("negative and edge cases", () => {
1621
- test("push with empty buffer emits an empty key event", () => {
1622
- const p = createParser()
1623
- try {
1624
- p.push(new Uint8Array(0))
1625
- expect(snap(p)).toEqual([k("")])
1626
- } finally {
1627
- p.destroy()
1628
- }
1629
- })
1630
-
1631
- test("drain with no events does not call callback", () => {
1632
- const p = createParser()
1633
- try {
1634
- let called = false
1635
- p.drain(() => {
1636
- called = true
1637
- })
1638
- expect(called).toBe(false)
1639
- } finally {
1640
- p.destroy()
1641
- }
1642
- })
1643
-
1644
- table([
1645
- ["CSI with unknown final byte produces empty-name key", "\x1b[h", [k("", { raw: "\x1b[h" })]],
1646
- ["ESC followed by punctuation stays one empty-name key", "\x1b!", [k("", { raw: "\x1b!" })]],
1647
- ["ESC followed by N becomes meta+shift+N", "\x1bN", [k("N", { raw: "\x1bN", meta: true, shift: true })]],
1648
- ["malformed SGR mouse falls through as empty-name CSI key", "\x1b[<0M", [k("", { raw: "\x1b[<0M" })]],
1649
- ["bracketed paste end outside paste mode is a CSI response", "\x1b[201~", [resp("csi", "\x1b[201~")]],
1650
- ])
1651
-
1652
- test("partial X10 times out as one unknown response", () => {
1653
- const { parser, clock } = createTimedParser()
1654
- try {
1655
- parser.push(Buffer.from("\x1b[M !"))
1656
- expect(snap(parser)).toEqual([])
1657
- clock.advance(10)
1658
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[M !")])
1659
- } finally {
1660
- parser.destroy()
1661
- }
1662
- })
1663
-
1664
- test("very long paste with partial end marker in every chunk", () => {
1665
- const p = createParser()
1666
- try {
1667
- p.push(Buffer.from("\x1b[200~"))
1668
- for (let i = 0; i < 100; i++) p.push(Buffer.from("\x1b[20"))
1669
- p.push(Buffer.from("\x1b[201~"))
1670
- expect(snap(p)).toEqual([paste("\x1b[20".repeat(100))])
1671
- } finally {
1672
- p.destroy()
1673
- }
1674
- })
1675
- })
1676
- })