@xincli/opentui-core 0.4.6 → 0.4.7

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 (619) hide show
  1. package/LICENSE +21 -0
  2. package/NativeSpanFeed.d.ts +52 -0
  3. package/README.md +0 -0
  4. package/Renderable.d.ts +346 -0
  5. package/animation/Timeline.d.ts +126 -0
  6. package/ansi.d.ts +17 -0
  7. package/audio.d.ts +89 -0
  8. package/buffer.d.ts +113 -0
  9. package/console.d.ts +146 -0
  10. package/edit-buffer.d.ts +97 -0
  11. package/editor-view.d.ts +72 -0
  12. package/index-m1y9e40w.js +10045 -0
  13. package/index-m1y9e40w.js.map +32 -0
  14. package/index-sqk4ajdf.js +16121 -0
  15. package/index-sqk4ajdf.js.map +58 -0
  16. package/index.d.ts +24 -0
  17. package/index.js +11709 -0
  18. package/index.js.map +35 -0
  19. package/lib/KeyHandler.d.ts +61 -0
  20. package/lib/RGBA.d.ts +42 -0
  21. package/lib/ascii.font.d.ts +508 -0
  22. package/lib/border.d.ts +51 -0
  23. package/lib/bunfs.d.ts +7 -0
  24. package/lib/clipboard.d.ts +15 -0
  25. package/lib/clock.d.ts +15 -0
  26. package/lib/data-paths.d.ts +26 -0
  27. package/lib/debounce.d.ts +42 -0
  28. package/lib/detect-links.d.ts +6 -0
  29. package/lib/env.d.ts +42 -0
  30. package/lib/extmarks-history.d.ts +17 -0
  31. package/lib/extmarks.d.ts +90 -0
  32. package/lib/hast-styled-text.d.ts +17 -0
  33. package/lib/index.d.ts +22 -0
  34. package/lib/keybinding.internal.d.ts +33 -0
  35. package/lib/objects-in-viewport.d.ts +24 -0
  36. package/lib/output.capture.d.ts +24 -0
  37. package/lib/parse.keypress-kitty.d.ts +3 -0
  38. package/lib/parse.keypress.d.ts +27 -0
  39. package/lib/parse.mouse.d.ts +30 -0
  40. package/lib/paste.d.ts +7 -0
  41. package/lib/queue.d.ts +15 -0
  42. package/lib/render-geometry.d.ts +8 -0
  43. package/lib/renderable.validations.d.ts +12 -0
  44. package/lib/scroll-acceleration.d.ts +43 -0
  45. package/lib/selection.d.ts +64 -0
  46. package/lib/singleton.d.ts +8 -0
  47. package/lib/stdin-parser.d.ts +90 -0
  48. package/lib/styled-text.d.ts +63 -0
  49. package/lib/terminal-capability-detection.d.ts +32 -0
  50. package/lib/terminal-palette.d.ts +65 -0
  51. package/lib/tree-sitter/assets/update.d.ts +12 -0
  52. package/lib/tree-sitter/client.d.ts +65 -0
  53. package/lib/tree-sitter/default-parsers.d.ts +2 -0
  54. package/lib/tree-sitter/download-utils.d.ts +21 -0
  55. package/lib/tree-sitter/index.d.ts +7 -0
  56. package/lib/tree-sitter/parser.worker.d.ts +1 -0
  57. package/lib/tree-sitter/parsers-config.d.ts +53 -0
  58. package/lib/tree-sitter/resolve-ft.d.ts +5 -0
  59. package/lib/tree-sitter/types.d.ts +185 -0
  60. package/lib/tree-sitter/update-assets.d.ts +3 -0
  61. package/lib/tree-sitter/update-assets.js +379 -0
  62. package/lib/tree-sitter/update-assets.js.map +12 -0
  63. package/lib/tree-sitter-styled-text.d.ts +16 -0
  64. package/lib/validate-dir-name.d.ts +1 -0
  65. package/lib/yoga.options.d.ts +32 -0
  66. package/native-event-worker-repro.worker.d.ts +1 -0
  67. package/package.json +52 -74
  68. package/parser.worker.js +1399 -0
  69. package/parser.worker.js.map +20 -0
  70. package/platform/ffi.d.ts +129 -0
  71. package/platform/runtime.d.ts +14 -0
  72. package/platform/test.d.ts +12 -0
  73. package/platform/worker.d.ts +26 -0
  74. package/platform/worker.node-test.d.ts +1 -0
  75. package/plugins/core-slot.d.ts +72 -0
  76. package/plugins/registry.d.ts +42 -0
  77. package/plugins/types.d.ts +34 -0
  78. package/post/effects.d.ts +147 -0
  79. package/post/filters.d.ts +65 -0
  80. package/post/matrices.d.ts +20 -0
  81. package/renderables/ASCIIFont.d.ts +52 -0
  82. package/renderables/Box.d.ts +85 -0
  83. package/renderables/Code.d.ts +97 -0
  84. package/renderables/Diff.d.ts +147 -0
  85. package/renderables/EditBufferRenderable.d.ts +238 -0
  86. package/renderables/FrameBuffer.d.ts +16 -0
  87. package/renderables/Input.d.ts +72 -0
  88. package/renderables/LineNumberRenderable.d.ts +78 -0
  89. package/renderables/Markdown.d.ts +255 -0
  90. package/renderables/ScrollBar.d.ts +77 -0
  91. package/renderables/ScrollBox.d.ts +128 -0
  92. package/renderables/Select.d.ts +122 -0
  93. package/renderables/Slider.d.ts +47 -0
  94. package/renderables/TabSelect.d.ts +98 -0
  95. package/renderables/Text.d.ts +36 -0
  96. package/renderables/TextBufferRenderable.d.ts +107 -0
  97. package/renderables/TextNode.d.ts +92 -0
  98. package/renderables/TextTable.d.ts +154 -0
  99. package/renderables/Textarea.d.ts +65 -0
  100. package/renderables/TimeToFirstDraw.d.ts +24 -0
  101. package/renderables/__tests__/renderable-test-utils.d.ts +12 -0
  102. package/renderables/composition/VRenderable.d.ts +16 -0
  103. package/renderables/composition/constructs.d.ts +35 -0
  104. package/renderables/composition/vnode.d.ts +46 -0
  105. package/renderables/index.d.ts +23 -0
  106. package/renderables/markdown-parser.d.ts +11 -0
  107. package/renderer-theme-mode.d.ts +29 -0
  108. package/renderer.d.ts +601 -0
  109. package/runtime-plugin-support-configure.d.ts +4 -0
  110. package/runtime-plugin-support-configure.js +36 -0
  111. package/runtime-plugin-support-configure.js.map +1 -0
  112. package/runtime-plugin-support-configure.node.js +15 -0
  113. package/runtime-plugin-support.d.ts +3 -0
  114. package/runtime-plugin-support.js +5 -0
  115. package/runtime-plugin-support.js.map +1 -0
  116. package/runtime-plugin-support.node.js +15 -0
  117. package/runtime-plugin.d.ts +16 -0
  118. package/runtime-plugin.js +473 -0
  119. package/runtime-plugin.js.map +1 -0
  120. package/runtime-plugin.node.js +15 -0
  121. package/syntax-style.d.ts +61 -0
  122. package/testing/bun-test-node.d.ts +87 -0
  123. package/testing/manual-clock.d.ts +17 -0
  124. package/testing/mock-keys.d.ts +81 -0
  125. package/testing/mock-mouse.d.ts +38 -0
  126. package/testing/mock-tree-sitter-client.d.ts +27 -0
  127. package/testing/spy.d.ts +7 -0
  128. package/testing/terminal-capabilities.d.ts +7 -0
  129. package/testing/test-recorder.d.ts +61 -0
  130. package/testing/test-renderer.d.ts +54 -0
  131. package/testing/test-streams.d.ts +12 -0
  132. package/testing.d.ts +8 -0
  133. package/testing.js +1004 -0
  134. package/testing.js.map +18 -0
  135. package/tests/yoga-upstream/tools/MeasureCounter.d.ts +16 -0
  136. package/tests/yoga-upstream/tools/utils.d.ts +11 -0
  137. package/text-buffer-view.d.ts +42 -0
  138. package/text-buffer.d.ts +66 -0
  139. package/types.d.ts +175 -0
  140. package/utils.d.ts +14 -0
  141. package/yoga.d.ts +437 -0
  142. package/yoga.js +193 -0
  143. package/yoga.js.map +9 -0
  144. package/zig-structs.d.ts +256 -0
  145. package/zig.d.ts +496 -0
  146. package/dev/keypress-debug-renderer.ts +0 -148
  147. package/dev/keypress-debug.ts +0 -43
  148. package/dev/print-env-vars.ts +0 -32
  149. package/dev/test-tmux-graphics-334.sh +0 -68
  150. package/dev/test-tmux-theme-queries.sh +0 -151
  151. package/dev/thai-debug-test.ts +0 -68
  152. package/dev/theme-mode.ts +0 -290
  153. package/docs/development.md +0 -144
  154. package/prebuilt/aarch64-android/libopentui.so +0 -0
  155. package/scripts/build-android.ts +0 -226
  156. package/scripts/build-native-termux.sh +0 -525
  157. package/scripts/build.ts +0 -530
  158. package/scripts/dist-test.ts +0 -312
  159. package/scripts/package-prebuilt.ts +0 -110
  160. package/scripts/publish.ts +0 -60
  161. package/scripts/test-node-hook.mjs +0 -16
  162. package/scripts/test-node.ts +0 -241
  163. package/scripts/vendor-deps.sh +0 -73
  164. package/src/NativeSpanFeed.ts +0 -375
  165. package/src/Renderable.ts +0 -1878
  166. package/src/__snapshots__/buffer.test.ts.snap +0 -28
  167. package/src/animation/Timeline.test.ts +0 -2709
  168. package/src/animation/Timeline.ts +0 -598
  169. package/src/ansi.ts +0 -22
  170. package/src/audio.ts +0 -479
  171. package/src/benchmark/attenuation-benchmark.ts +0 -81
  172. package/src/benchmark/audio-playback-benchmark.ts +0 -344
  173. package/src/benchmark/box-draw-benchmark.ts +0 -1042
  174. package/src/benchmark/colormatrix-benchmark.ts +0 -123
  175. package/src/benchmark/gain-benchmark.ts +0 -80
  176. package/src/benchmark/latest-all-bench-run.json +0 -707
  177. package/src/benchmark/latest-async-bench-run.json +0 -336
  178. package/src/benchmark/latest-default-bench-run.json +0 -657
  179. package/src/benchmark/latest-large-bench-run.json +0 -707
  180. package/src/benchmark/latest-quick-bench-run.json +0 -207
  181. package/src/benchmark/layout-benchmark.ts +0 -2547
  182. package/src/benchmark/markdown-benchmark.ts +0 -1796
  183. package/src/benchmark/native-span-feed-async-benchmark.ts +0 -355
  184. package/src/benchmark/native-span-feed-benchmark.md +0 -56
  185. package/src/benchmark/native-span-feed-benchmark.ts +0 -596
  186. package/src/benchmark/native-span-feed-compare.ts +0 -280
  187. package/src/benchmark/render-traversal-benchmark.ts +0 -928
  188. package/src/benchmark/text-buffer-render-benchmark.ts +0 -874
  189. package/src/benchmark/text-table-benchmark.ts +0 -948
  190. package/src/buffer.test.ts +0 -292
  191. package/src/buffer.ts +0 -564
  192. package/src/console.test.ts +0 -629
  193. package/src/console.ts +0 -1253
  194. package/src/edit-buffer.test.ts +0 -1852
  195. package/src/edit-buffer.ts +0 -417
  196. package/src/editor-view.test.ts +0 -1056
  197. package/src/editor-view.ts +0 -290
  198. package/src/index.ts +0 -25
  199. package/src/lib/KeyHandler.integration.test.ts +0 -292
  200. package/src/lib/KeyHandler.stopPropagation.test.ts +0 -289
  201. package/src/lib/KeyHandler.test.ts +0 -662
  202. package/src/lib/KeyHandler.ts +0 -222
  203. package/src/lib/RGBA.test.ts +0 -60
  204. package/src/lib/RGBA.ts +0 -343
  205. package/src/lib/ascii.font.ts +0 -330
  206. package/src/lib/border.test.ts +0 -83
  207. package/src/lib/border.ts +0 -170
  208. package/src/lib/bunfs.test.ts +0 -27
  209. package/src/lib/bunfs.ts +0 -18
  210. package/src/lib/clipboard.test.ts +0 -63
  211. package/src/lib/clipboard.ts +0 -41
  212. package/src/lib/clock.ts +0 -35
  213. package/src/lib/data-paths.test.ts +0 -133
  214. package/src/lib/data-paths.ts +0 -109
  215. package/src/lib/debounce.ts +0 -106
  216. package/src/lib/detect-links.test.ts +0 -98
  217. package/src/lib/detect-links.ts +0 -56
  218. package/src/lib/env.test.ts +0 -228
  219. package/src/lib/env.ts +0 -209
  220. package/src/lib/extmarks-history.ts +0 -51
  221. package/src/lib/extmarks-multiwidth.test.ts +0 -324
  222. package/src/lib/extmarks.test.ts +0 -3547
  223. package/src/lib/extmarks.ts +0 -861
  224. package/src/lib/fonts/block.json +0 -405
  225. package/src/lib/fonts/grid.json +0 -265
  226. package/src/lib/fonts/huge.json +0 -741
  227. package/src/lib/fonts/pallet.json +0 -314
  228. package/src/lib/fonts/shade.json +0 -591
  229. package/src/lib/fonts/slick.json +0 -321
  230. package/src/lib/fonts/tiny.json +0 -69
  231. package/src/lib/hast-styled-text.ts +0 -59
  232. package/src/lib/index.ts +0 -22
  233. package/src/lib/keybinding.internal.test.ts +0 -342
  234. package/src/lib/keybinding.internal.ts +0 -182
  235. package/src/lib/objects-in-viewport.test.ts +0 -789
  236. package/src/lib/objects-in-viewport.ts +0 -153
  237. package/src/lib/output.capture.ts +0 -58
  238. package/src/lib/parse.keypress-kitty.protocol.test.ts +0 -347
  239. package/src/lib/parse.keypress-kitty.test.ts +0 -712
  240. package/src/lib/parse.keypress-kitty.ts +0 -470
  241. package/src/lib/parse.keypress.test.ts +0 -2020
  242. package/src/lib/parse.keypress.ts +0 -480
  243. package/src/lib/parse.mouse.test.ts +0 -552
  244. package/src/lib/parse.mouse.ts +0 -232
  245. package/src/lib/paste.ts +0 -18
  246. package/src/lib/queue.ts +0 -65
  247. package/src/lib/render-geometry.ts +0 -36
  248. package/src/lib/renderable.validations.test.ts +0 -87
  249. package/src/lib/renderable.validations.ts +0 -83
  250. package/src/lib/scroll-acceleration.ts +0 -98
  251. package/src/lib/selection.ts +0 -261
  252. package/src/lib/singleton.ts +0 -34
  253. package/src/lib/stdin-parser.test.ts +0 -2439
  254. package/src/lib/stdin-parser.ts +0 -2007
  255. package/src/lib/styled-text.ts +0 -178
  256. package/src/lib/terminal-capability-detection.test.ts +0 -226
  257. package/src/lib/terminal-capability-detection.ts +0 -98
  258. package/src/lib/terminal-palette.test.ts +0 -954
  259. package/src/lib/terminal-palette.ts +0 -441
  260. package/src/lib/tree-sitter/assets/README.md +0 -119
  261. package/src/lib/tree-sitter/assets/update.ts +0 -360
  262. package/src/lib/tree-sitter/assets.d.ts +0 -9
  263. package/src/lib/tree-sitter/cache.test.ts +0 -291
  264. package/src/lib/tree-sitter/client.test.ts +0 -1434
  265. package/src/lib/tree-sitter/client.ts +0 -847
  266. package/src/lib/tree-sitter/default-parsers.ts +0 -133
  267. package/src/lib/tree-sitter/download-utils.ts +0 -148
  268. package/src/lib/tree-sitter/index.ts +0 -75
  269. package/src/lib/tree-sitter/parser.worker.ts +0 -1098
  270. package/src/lib/tree-sitter/parsers-config.ts +0 -81
  271. package/src/lib/tree-sitter/resolve-ft.test.ts +0 -55
  272. package/src/lib/tree-sitter/resolve-ft.ts +0 -189
  273. package/src/lib/tree-sitter/types.ts +0 -132
  274. package/src/lib/tree-sitter/update-assets.ts +0 -10
  275. package/src/lib/tree-sitter-styled-text.test.ts +0 -1253
  276. package/src/lib/tree-sitter-styled-text.ts +0 -314
  277. package/src/lib/validate-dir-name.ts +0 -55
  278. package/src/lib/yoga.options.test.ts +0 -628
  279. package/src/lib/yoga.options.ts +0 -346
  280. package/src/native-event-worker-repro.test.ts +0 -48
  281. package/src/native-event-worker-repro.worker.ts +0 -9
  282. package/src/native-handle.test.ts +0 -221
  283. package/src/platform/ffi.test.ts +0 -516
  284. package/src/platform/ffi.ts +0 -653
  285. package/src/platform/runtime.test.ts +0 -52
  286. package/src/platform/runtime.ts +0 -121
  287. package/src/platform/test.ts +0 -45
  288. package/src/platform/worker-handler-cleanup.fixture.ts +0 -12
  289. package/src/platform/worker-onmessage-startup.fixture.ts +0 -10
  290. package/src/platform/worker-startup.fixture.ts +0 -9
  291. package/src/platform/worker.node-test.ts +0 -153
  292. package/src/platform/worker.ts +0 -425
  293. package/src/plugins/core-slot.ts +0 -579
  294. package/src/plugins/registry.ts +0 -402
  295. package/src/plugins/types.ts +0 -46
  296. package/src/post/effects.ts +0 -948
  297. package/src/post/filters.ts +0 -512
  298. package/src/post/matrices.ts +0 -288
  299. package/src/renderables/ASCIIFont.ts +0 -219
  300. package/src/renderables/Box.test.ts +0 -474
  301. package/src/renderables/Box.ts +0 -353
  302. package/src/renderables/Code.test.ts +0 -2388
  303. package/src/renderables/Code.ts +0 -555
  304. package/src/renderables/Diff.regression.test.ts +0 -381
  305. package/src/renderables/Diff.test.ts +0 -3212
  306. package/src/renderables/Diff.ts +0 -1278
  307. package/src/renderables/EditBufferRenderable.test.ts +0 -288
  308. package/src/renderables/EditBufferRenderable.ts +0 -1152
  309. package/src/renderables/FrameBuffer.ts +0 -47
  310. package/src/renderables/Input.test.ts +0 -1310
  311. package/src/renderables/Input.ts +0 -273
  312. package/src/renderables/LineNumberRenderable.ts +0 -725
  313. package/src/renderables/Markdown.ts +0 -2144
  314. package/src/renderables/ScrollBar.ts +0 -423
  315. package/src/renderables/ScrollBox.ts +0 -903
  316. package/src/renderables/Select.test.ts +0 -1134
  317. package/src/renderables/Select.ts +0 -535
  318. package/src/renderables/Slider.test.ts +0 -456
  319. package/src/renderables/Slider.ts +0 -342
  320. package/src/renderables/TabSelect.test.ts +0 -238
  321. package/src/renderables/TabSelect.ts +0 -447
  322. package/src/renderables/Text.selection-buffer.test.ts +0 -118
  323. package/src/renderables/Text.test.ts +0 -2662
  324. package/src/renderables/Text.ts +0 -147
  325. package/src/renderables/TextBufferRenderable.ts +0 -504
  326. package/src/renderables/TextNode.test.ts +0 -1137
  327. package/src/renderables/TextNode.ts +0 -379
  328. package/src/renderables/TextTable.test.ts +0 -1411
  329. package/src/renderables/TextTable.ts +0 -1434
  330. package/src/renderables/Textarea.ts +0 -425
  331. package/src/renderables/TimeToFirstDraw.ts +0 -89
  332. package/src/renderables/__snapshots__/Code.test.ts.snap +0 -13
  333. package/src/renderables/__snapshots__/Diff.test.ts.snap +0 -785
  334. package/src/renderables/__snapshots__/Text.test.ts.snap +0 -421
  335. package/src/renderables/__snapshots__/TextTable.test.ts.snap +0 -215
  336. package/src/renderables/__tests__/Code.test.ts +0 -55
  337. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +0 -144
  338. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +0 -827
  339. package/src/renderables/__tests__/LineNumberRenderable.test.ts +0 -1906
  340. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +0 -85
  341. package/src/renderables/__tests__/Markdown.code-colors.test.ts +0 -288
  342. package/src/renderables/__tests__/Markdown.test.ts +0 -4175
  343. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +0 -87
  344. package/src/renderables/__tests__/Textarea.buffer.test.ts +0 -683
  345. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +0 -675
  346. package/src/renderables/__tests__/Textarea.editing.test.ts +0 -2120
  347. package/src/renderables/__tests__/Textarea.error-handling.test.ts +0 -35
  348. package/src/renderables/__tests__/Textarea.events.test.ts +0 -738
  349. package/src/renderables/__tests__/Textarea.highlights.test.ts +0 -590
  350. package/src/renderables/__tests__/Textarea.keybinding.test.ts +0 -3275
  351. package/src/renderables/__tests__/Textarea.paste.test.ts +0 -357
  352. package/src/renderables/__tests__/Textarea.rendering.test.ts +0 -1927
  353. package/src/renderables/__tests__/Textarea.scroll.test.ts +0 -733
  354. package/src/renderables/__tests__/Textarea.selection.test.ts +0 -1589
  355. package/src/renderables/__tests__/Textarea.stress.test.ts +0 -670
  356. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +0 -383
  357. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +0 -310
  358. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +0 -221
  359. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +0 -89
  360. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +0 -457
  361. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +0 -158
  362. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +0 -387
  363. package/src/renderables/__tests__/markdown-parser.test.ts +0 -241
  364. package/src/renderables/__tests__/renderable-test-utils.ts +0 -60
  365. package/src/renderables/composition/README.md +0 -8
  366. package/src/renderables/composition/VRenderable.ts +0 -32
  367. package/src/renderables/composition/constructs.ts +0 -127
  368. package/src/renderables/composition/vnode.ts +0 -277
  369. package/src/renderables/index.ts +0 -23
  370. package/src/renderables/markdown-parser.ts +0 -79
  371. package/src/renderer-theme-mode.ts +0 -199
  372. package/src/renderer.ts +0 -4975
  373. package/src/runtime-plugin-support-configure.ts +0 -63
  374. package/src/runtime-plugin-support.ts +0 -13
  375. package/src/runtime-plugin.ts +0 -626
  376. package/src/specs/terminal-startup.md +0 -64
  377. package/src/syntax-style.test.ts +0 -841
  378. package/src/syntax-style.ts +0 -274
  379. package/src/testing/README.md +0 -219
  380. package/src/testing/bun-test-node.ts +0 -880
  381. package/src/testing/capture-spans.test.ts +0 -216
  382. package/src/testing/integration.test.ts +0 -276
  383. package/src/testing/manual-clock.ts +0 -117
  384. package/src/testing/mock-keys.test.ts +0 -1378
  385. package/src/testing/mock-keys.ts +0 -457
  386. package/src/testing/mock-mouse.test.ts +0 -218
  387. package/src/testing/mock-mouse.ts +0 -247
  388. package/src/testing/mock-tree-sitter-client.ts +0 -81
  389. package/src/testing/spy.ts +0 -13
  390. package/src/testing/terminal-capabilities.ts +0 -47
  391. package/src/testing/test-recorder.test.ts +0 -410
  392. package/src/testing/test-recorder.ts +0 -133
  393. package/src/testing/test-renderer.ts +0 -375
  394. package/src/testing/test-renderer.wait.test.ts +0 -278
  395. package/src/testing/test-streams.ts +0 -31
  396. package/src/testing.ts +0 -9
  397. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +0 -481
  398. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +0 -19
  399. package/src/tests/__snapshots__/scrollbox.test.ts.snap +0 -29
  400. package/src/tests/absolute-positioning.snapshot.test.ts +0 -638
  401. package/src/tests/allocator-stats.test.ts +0 -53
  402. package/src/tests/audio.test.ts +0 -303
  403. package/src/tests/destroy-during-render.test.ts +0 -200
  404. package/src/tests/destroy-on-exit.fixture.ts +0 -36
  405. package/src/tests/destroy-on-exit.test.ts +0 -62
  406. package/src/tests/ffi-borrowed-pointer-callsites.test.ts +0 -153
  407. package/src/tests/hover-cursor.test.ts +0 -98
  408. package/src/tests/native-backed-measurement-lifecycle.test.ts +0 -133
  409. package/src/tests/native-backed-measurement-parity.test.ts +0 -322
  410. package/src/tests/native-span-feed-async.test.ts +0 -173
  411. package/src/tests/native-span-feed-close.test.ts +0 -120
  412. package/src/tests/native-span-feed-coverage.test.ts +0 -227
  413. package/src/tests/native-span-feed-edge-cases.test.ts +0 -384
  414. package/src/tests/native-span-feed-use-after-free.test.ts +0 -45
  415. package/src/tests/opacity.test.ts +0 -123
  416. package/src/tests/renderable.snapshot.test.ts +0 -524
  417. package/src/tests/renderable.test.ts +0 -1851
  418. package/src/tests/renderer.clock.test.ts +0 -270
  419. package/src/tests/renderer.console-startup.test.ts +0 -2151
  420. package/src/tests/renderer.control.test.ts +0 -458
  421. package/src/tests/renderer.core-slot-binding.test.ts +0 -962
  422. package/src/tests/renderer.cursor.test.ts +0 -26
  423. package/src/tests/renderer.custom-stdout.test.ts +0 -1112
  424. package/src/tests/renderer.destroy-during-render.test.ts +0 -147
  425. package/src/tests/renderer.focus-restore.test.ts +0 -257
  426. package/src/tests/renderer.focus.test.ts +0 -329
  427. package/src/tests/renderer.idle.test.ts +0 -219
  428. package/src/tests/renderer.input.test.ts +0 -2615
  429. package/src/tests/renderer.kitty-flags.test.ts +0 -195
  430. package/src/tests/renderer.lifecycle.test.ts +0 -34
  431. package/src/tests/renderer.mouse.test.ts +0 -1315
  432. package/src/tests/renderer.notifications.test.ts +0 -27
  433. package/src/tests/renderer.palette.test.ts +0 -1199
  434. package/src/tests/renderer.remote.test.ts +0 -247
  435. package/src/tests/renderer.render-stats.test.ts +0 -146
  436. package/src/tests/renderer.scrollback-surface.test.ts +0 -474
  437. package/src/tests/renderer.selection.test.ts +0 -138
  438. package/src/tests/renderer.slot-registry.test.ts +0 -684
  439. package/src/tests/renderer.tracker.test.ts +0 -202
  440. package/src/tests/renderer.useMouse.test.ts +0 -83
  441. package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +0 -76
  442. package/src/tests/runtime-plugin-node-modules-import-like-string.fixture.ts +0 -48
  443. package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +0 -43
  444. package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +0 -67
  445. package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +0 -72
  446. package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +0 -44
  447. package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +0 -85
  448. package/src/tests/runtime-plugin-path-alias.fixture.ts +0 -43
  449. package/src/tests/runtime-plugin-resolve-roots.fixture.ts +0 -65
  450. package/src/tests/runtime-plugin-support-configure.fixture.ts +0 -31
  451. package/src/tests/runtime-plugin-support-late-addition.fixture.ts +0 -17
  452. package/src/tests/runtime-plugin-support.fixture.ts +0 -11
  453. package/src/tests/runtime-plugin-support.test.ts +0 -54
  454. package/src/tests/runtime-plugin-windows-file-url.fixture.ts +0 -30
  455. package/src/tests/runtime-plugin.fixture.ts +0 -40
  456. package/src/tests/runtime-plugin.test.ts +0 -369
  457. package/src/tests/scrollbox-culling-bug.test.ts +0 -114
  458. package/src/tests/scrollbox-hitgrid-resize.test.ts +0 -136
  459. package/src/tests/scrollbox-hitgrid.test.ts +0 -909
  460. package/src/tests/scrollbox.test.ts +0 -1895
  461. package/src/tests/wrap-resize-perf.test.ts +0 -276
  462. package/src/tests/yoga-api-coverage.test.ts +0 -409
  463. package/src/tests/yoga-callback-stress.test.ts +0 -122
  464. package/src/tests/yoga-ffi.test.ts +0 -117
  465. package/src/tests/yoga-setters.test.ts +0 -921
  466. package/src/tests/yoga-upstream/YGAlignBaselineTest.test.ts +0 -124
  467. package/src/tests/yoga-upstream/YGComputedBorderTest.test.ts +0 -27
  468. package/src/tests/yoga-upstream/YGComputedMarginTest.test.ts +0 -27
  469. package/src/tests/yoga-upstream/YGComputedPaddingTest.test.ts +0 -27
  470. package/src/tests/yoga-upstream/YGDirtiedTest.test.ts +0 -157
  471. package/src/tests/yoga-upstream/YGErrataTest.test.ts +0 -40
  472. package/src/tests/yoga-upstream/YGFlexBasisAuto.test.ts +0 -23
  473. package/src/tests/yoga-upstream/YGHasNewLayout.test.ts +0 -81
  474. package/src/tests/yoga-upstream/YGMeasureCacheTest.test.ts +0 -31
  475. package/src/tests/yoga-upstream/YGMeasureTest.test.ts +0 -70
  476. package/src/tests/yoga-upstream/generated/YGAbsolutePositionTest.test.ts +0 -1864
  477. package/src/tests/yoga-upstream/generated/YGAlignContentTest.test.ts +0 -5829
  478. package/src/tests/yoga-upstream/generated/YGAlignItemsTest.test.ts +0 -2444
  479. package/src/tests/yoga-upstream/generated/YGAlignSelfTest.test.ts +0 -293
  480. package/src/tests/yoga-upstream/generated/YGAndroidNewsFeed.test.ts +0 -315
  481. package/src/tests/yoga-upstream/generated/YGAspectRatioTest.test.ts +0 -231
  482. package/src/tests/yoga-upstream/generated/YGAutoTest.test.ts +0 -347
  483. package/src/tests/yoga-upstream/generated/YGBorderTest.test.ts +0 -256
  484. package/src/tests/yoga-upstream/generated/YGBoxSizingTest.test.ts +0 -2747
  485. package/src/tests/yoga-upstream/generated/YGDimensionTest.test.ts +0 -129
  486. package/src/tests/yoga-upstream/generated/YGDisplayContentsTest.test.ts +0 -109
  487. package/src/tests/yoga-upstream/generated/YGDisplayTest.test.ts +0 -1097
  488. package/src/tests/yoga-upstream/generated/YGFlexDirectionTest.test.ts +0 -4562
  489. package/src/tests/yoga-upstream/generated/YGFlexTest.test.ts +0 -677
  490. package/src/tests/yoga-upstream/generated/YGFlexWrapTest.test.ts +0 -2071
  491. package/src/tests/yoga-upstream/generated/YGGapTest.test.ts +0 -3303
  492. package/src/tests/yoga-upstream/generated/YGIntrinsicSizeTest.test.ts +0 -582
  493. package/src/tests/yoga-upstream/generated/YGJustifyContentTest.test.ts +0 -2150
  494. package/src/tests/yoga-upstream/generated/YGMarginTest.test.ts +0 -1963
  495. package/src/tests/yoga-upstream/generated/YGMinMaxDimensionTest.test.ts +0 -1446
  496. package/src/tests/yoga-upstream/generated/YGPaddingTest.test.ts +0 -354
  497. package/src/tests/yoga-upstream/generated/YGPercentageTest.test.ts +0 -1760
  498. package/src/tests/yoga-upstream/generated/YGRoundingTest.test.ts +0 -1161
  499. package/src/tests/yoga-upstream/generated/YGSizeOverflowTest.test.ts +0 -207
  500. package/src/tests/yoga-upstream/generated/YGStaticPositionTest.test.ts +0 -6338
  501. package/src/tests/yoga-upstream/tools/MeasureCounter.ts +0 -54
  502. package/src/tests/yoga-upstream/tools/utils.ts +0 -81
  503. package/src/text-buffer-view.test.ts +0 -722
  504. package/src/text-buffer-view.ts +0 -193
  505. package/src/text-buffer.test.ts +0 -512
  506. package/src/text-buffer.ts +0 -250
  507. package/src/types.ts +0 -199
  508. package/src/utils.ts +0 -88
  509. package/src/yoga.ts +0 -1146
  510. package/src/zig/ansi.zig +0 -502
  511. package/src/zig/audio.zig +0 -1206
  512. package/src/zig/bench/README.md +0 -50
  513. package/src/zig/bench/buffer-color-blending_bench.zig +0 -350
  514. package/src/zig/bench/buffer-draw-box_bench.zig +0 -786
  515. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +0 -894
  516. package/src/zig/bench/edit-buffer_bench.zig +0 -476
  517. package/src/zig/bench/editor-view_bench.zig +0 -169
  518. package/src/zig/bench/native-span-feed_bench.zig +0 -107
  519. package/src/zig/bench/rope-markers_bench.zig +0 -713
  520. package/src/zig/bench/rope_bench.zig +0 -514
  521. package/src/zig/bench/styled-text_bench.zig +0 -476
  522. package/src/zig/bench/text-buffer-coords_bench.zig +0 -362
  523. package/src/zig/bench/text-buffer-view_bench.zig +0 -459
  524. package/src/zig/bench/text-chunk-graphemes_bench.zig +0 -272
  525. package/src/zig/bench/utf8_bench.zig +0 -799
  526. package/src/zig/bench-utils.zig +0 -431
  527. package/src/zig/bench.zig +0 -223
  528. package/src/zig/buffer-methods.zig +0 -215
  529. package/src/zig/buffer.zig +0 -2515
  530. package/src/zig/build.zig +0 -700
  531. package/src/zig/build.zig.zon +0 -19
  532. package/src/zig/edit-buffer.zig +0 -900
  533. package/src/zig/editor-view.zig +0 -812
  534. package/src/zig/event-bus.zig +0 -28
  535. package/src/zig/event-emitter.zig +0 -65
  536. package/src/zig/file-logger.zig +0 -92
  537. package/src/zig/grapheme.zig +0 -838
  538. package/src/zig/handles.zig +0 -267
  539. package/src/zig/lib.zig +0 -2813
  540. package/src/zig/link.zig +0 -349
  541. package/src/zig/logger.zig +0 -43
  542. package/src/zig/mem-registry.zig +0 -126
  543. package/src/zig/miniaudio_shim.c +0 -21
  544. package/src/zig/native-renderable.zig +0 -112
  545. package/src/zig/native-span-feed-bench-lib.zig +0 -7
  546. package/src/zig/native-span-feed.zig +0 -747
  547. package/src/zig/renderer-output.zig +0 -677
  548. package/src/zig/renderer.zig +0 -2106
  549. package/src/zig/rope.zig +0 -1220
  550. package/src/zig/split-scrollback.zig +0 -88
  551. package/src/zig/syntax-style.zig +0 -174
  552. package/src/zig/terminal.zig +0 -1652
  553. package/src/zig/test.zig +0 -82
  554. package/src/zig/tests/README.md +0 -18
  555. package/src/zig/tests/ansi_test.zig +0 -18
  556. package/src/zig/tests/audio_test.zig +0 -439
  557. package/src/zig/tests/buffer-methods_test.zig +0 -1111
  558. package/src/zig/tests/buffer_test.zig +0 -2926
  559. package/src/zig/tests/edit-buffer-history_test.zig +0 -299
  560. package/src/zig/tests/edit-buffer_test.zig +0 -1688
  561. package/src/zig/tests/editor-view_test.zig +0 -3454
  562. package/src/zig/tests/event-emitter_test.zig +0 -249
  563. package/src/zig/tests/grapheme_test.zig +0 -1329
  564. package/src/zig/tests/handles_test.zig +0 -123
  565. package/src/zig/tests/link_test.zig +0 -220
  566. package/src/zig/tests/mem-registry_test.zig +0 -473
  567. package/src/zig/tests/memory_leak_regression_test.zig +0 -234
  568. package/src/zig/tests/native-renderable_test.zig +0 -16
  569. package/src/zig/tests/native-span-feed_test.zig +0 -1266
  570. package/src/zig/tests/renderer_test.zig +0 -2604
  571. package/src/zig/tests/rope-nested_test.zig +0 -712
  572. package/src/zig/tests/rope_fuzz_test.zig +0 -238
  573. package/src/zig/tests/rope_test.zig +0 -2362
  574. package/src/zig/tests/segment-merge.test.zig +0 -148
  575. package/src/zig/tests/split-scrollback_test.zig +0 -70
  576. package/src/zig/tests/syntax-style_test.zig +0 -558
  577. package/src/zig/tests/terminal_test.zig +0 -1489
  578. package/src/zig/tests/test-renderer.zig +0 -126
  579. package/src/zig/tests/text-buffer-drawing_test.zig +0 -3291
  580. package/src/zig/tests/text-buffer-highlights_test.zig +0 -706
  581. package/src/zig/tests/text-buffer-iterators_test.zig +0 -776
  582. package/src/zig/tests/text-buffer-segment_test.zig +0 -320
  583. package/src/zig/tests/text-buffer-selection_test.zig +0 -1056
  584. package/src/zig/tests/text-buffer-selection_viewport_test.zig +0 -358
  585. package/src/zig/tests/text-buffer-view_test.zig +0 -3672
  586. package/src/zig/tests/text-buffer_test.zig +0 -2191
  587. package/src/zig/tests/unicode-width-map.zon +0 -3909
  588. package/src/zig/tests/utf8_no_zwj_test.zig +0 -260
  589. package/src/zig/tests/utf8_test.zig +0 -4060
  590. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +0 -267
  591. package/src/zig/tests/utf8_wcwidth_test.zig +0 -357
  592. package/src/zig/tests/word-wrap-editing_test.zig +0 -498
  593. package/src/zig/tests/wrap-cache-perf_test.zig +0 -113
  594. package/src/zig/tests/yoga_test.zig +0 -65
  595. package/src/zig/text-buffer-iterators.zig +0 -496
  596. package/src/zig/text-buffer-segment.zig +0 -403
  597. package/src/zig/text-buffer-view.zig +0 -1428
  598. package/src/zig/text-buffer.zig +0 -1272
  599. package/src/zig/utf8.zig +0 -1954
  600. package/src/zig/utils.zig +0 -12
  601. package/src/zig/uucode-stub.zig +0 -76
  602. package/src/zig/vendor/miniaudio/miniaudio.h +0 -93468
  603. package/src/zig/yoga.zig +0 -658
  604. package/src/zig-structs.ts +0 -381
  605. package/src/zig.ts +0 -5260
  606. package/tsconfig.build.json +0 -17
  607. package/tsconfig.json +0 -27
  608. package/tsconfig.node-test.json +0 -138
  609. /package/{src/lib/tree-sitter/assets → assets}/javascript/highlights.scm +0 -0
  610. /package/{src/lib/tree-sitter/assets → assets}/javascript/tree-sitter-javascript.wasm +0 -0
  611. /package/{src/lib/tree-sitter/assets → assets}/markdown/highlights.scm +0 -0
  612. /package/{src/lib/tree-sitter/assets → assets}/markdown/injections.scm +0 -0
  613. /package/{src/lib/tree-sitter/assets → assets}/markdown/tree-sitter-markdown.wasm +0 -0
  614. /package/{src/lib/tree-sitter/assets → assets}/markdown_inline/highlights.scm +0 -0
  615. /package/{src/lib/tree-sitter/assets → assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  616. /package/{src/lib/tree-sitter/assets → assets}/typescript/highlights.scm +0 -0
  617. /package/{src/lib/tree-sitter/assets → assets}/typescript/tree-sitter-typescript.wasm +0 -0
  618. /package/{src/lib/tree-sitter/assets → assets}/zig/highlights.scm +0 -0
  619. /package/{src/lib/tree-sitter/assets → assets}/zig/tree-sitter-zig.wasm +0 -0
@@ -1,2439 +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.js"
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
- ["space", "\x1b[32u", [k("space", { raw: "\x1b[32u" })]],
548
- ["shift+a", "\x1b[97;2u", [k("a", { raw: "\x1b[97;2u", shift: true })]],
549
- ["shift+space", "\x1b[32;2u", [k("space", { raw: "\x1b[32;2u", shift: true })]],
550
- ["ctrl+a", "\x1b[97;5u", [k("a", { raw: "\x1b[97;5u", ctrl: true })]],
551
- ["ctrl+space", "\x1b[32;5u", [k("space", { raw: "\x1b[32;5u", ctrl: true })]],
552
- ["alt+a", "\x1b[97;3u", [k("a", { raw: "\x1b[97;3u", meta: true })]],
553
- ["ctrl+shift+a", "\x1b[97;6u", [k("a", { raw: "\x1b[97;6u", ctrl: true, shift: true })]],
554
- ["a release", "\x1b[97;1:3u", [k("a", { raw: "\x1b[97;1:3u", eventType: "release" })]],
555
- ["escape", "\x1b[27u", [k("escape", { raw: "\x1b[27u" })]],
556
- ["return", "\x1b[13u", [k("return", { raw: "\x1b[13u" })]],
557
- ["tab", "\x1b[9u", [k("tab", { raw: "\x1b[9u" })]],
558
- ["backspace", "\x1b[127u", [k("backspace", { raw: "\x1b[127u" })]],
559
- ["delete", "\x1b[57349u", [k("delete", { raw: "\x1b[57349u" })]],
560
- ["insert", "\x1b[57348u", [k("insert", { raw: "\x1b[57348u" })]],
561
- ["f1", "\x1b[57364u", [k("f1", { raw: "\x1b[57364u" })]],
562
- ["f12", "\x1b[57375u", [k("f12", { raw: "\x1b[57375u" })]],
563
- ])
564
-
565
- // CSI 1;modifier:event letter format (kitty functional keys)
566
- table([
567
- ["up press", "\x1b[1;1:1A", [k("up", { raw: "\x1b[1;1:1A" })]],
568
- ["up release", "\x1b[1;1:3A", [k("up", { raw: "\x1b[1;1:3A", eventType: "release" })]],
569
- ["ctrl+right", "\x1b[1;5:1C", [k("right", { raw: "\x1b[1;5:1C", ctrl: true })]],
570
- ["shift+left", "\x1b[1;2:1D", [k("left", { raw: "\x1b[1;2:1D", shift: true })]],
571
- ["home", "\x1b[1;1:1H", [k("home", { raw: "\x1b[1;1:1H" })]],
572
- ["end release", "\x1b[1;1:3F", [k("end", { raw: "\x1b[1;1:3F", eventType: "release" })]],
573
- ["f1 press", "\x1b[1;1:1P", [k("f1", { raw: "\x1b[1;1:1P" })]],
574
- ])
575
-
576
- // CSI number;modifier:event ~ format (kitty tilde keys)
577
- table([
578
- ["pageup press", "\x1b[5;1:1~", [k("pageup", { raw: "\x1b[5;1:1~" })]],
579
- ["ctrl+delete", "\x1b[3;5:1~", [k("delete", { raw: "\x1b[3;5:1~", ctrl: true })]],
580
- ["insert release", "\x1b[2;1:3~", [k("insert", { raw: "\x1b[2;1:3~", eventType: "release" })]],
581
- ])
582
- })
583
-
584
- describe("modifyOtherKeys", () => {
585
- table([
586
- ["shift+return", "\x1b[27;2;13~", [k("return", { raw: "\x1b[27;2;13~", shift: true })]],
587
- ["ctrl+return", "\x1b[27;5;13~", [k("return", { raw: "\x1b[27;5;13~", ctrl: true })]],
588
- ["ctrl+escape", "\x1b[27;5;27~", [k("escape", { raw: "\x1b[27;5;27~", ctrl: true })]],
589
- ["alt+tab", "\x1b[27;3;9~", [k("tab", { raw: "\x1b[27;3;9~", meta: true })]],
590
- ["shift+space", "\x1b[27;2;32~", [k("space", { raw: "\x1b[27;2;32~", shift: true })]],
591
- ["ctrl+backspace", "\x1b[27;5;127~", [k("backspace", { raw: "\x1b[27;5;127~", ctrl: true })]],
592
- ["shift+digit 5", "\x1b[27;2;53~", [k("5", { raw: "\x1b[27;2;53~", shift: true })]],
593
- ])
594
- })
595
-
596
- describe("mouse: SGR protocol", () => {
597
- table([
598
- // Button press/release
599
- ["left down", "\x1b[<0;1;1M", [sgr("\x1b[<0;1;1M", "down", 0, 0)]],
600
- ["left up", "\x1b[<0;1;1m", [sgr("\x1b[<0;1;1m", "up", 0, 0)]],
601
- ["middle down", "\x1b[<1;1;1M", [sgr("\x1b[<1;1;1M", "down", 0, 0, { button: 1 })]],
602
- ["middle up", "\x1b[<1;1;1m", [sgr("\x1b[<1;1;1m", "up", 0, 0, { button: 1 })]],
603
- ["right down", "\x1b[<2;1;1M", [sgr("\x1b[<2;1;1M", "down", 0, 0, { button: 2 })]],
604
- ["right up", "\x1b[<2;1;1m", [sgr("\x1b[<2;1;1m", "up", 0, 0, { button: 2 })]],
605
- // Scroll
606
- [
607
- "scroll up",
608
- "\x1b[<64;10;5M",
609
- [sgr("\x1b[<64;10;5M", "scroll", 9, 4, { scroll: { direction: "up", delta: 1 } })],
610
- ],
611
- [
612
- "scroll down",
613
- "\x1b[<65;10;5M",
614
- [sgr("\x1b[<65;10;5M", "scroll", 9, 4, { button: 1, scroll: { direction: "down", delta: 1 } })],
615
- ],
616
- [
617
- "scroll left",
618
- "\x1b[<66;10;5M",
619
- [sgr("\x1b[<66;10;5M", "scroll", 9, 4, { button: 2, scroll: { direction: "left", delta: 1 } })],
620
- ],
621
- [
622
- "scroll right",
623
- "\x1b[<67;10;5M",
624
- [sgr("\x1b[<67;10;5M", "scroll", 9, 4, { button: 0, scroll: { direction: "right", delta: 1 } })],
625
- ],
626
- // Motion (no button)
627
- ["move", "\x1b[<35;20;10M", [sgr("\x1b[<35;20;10M", "move", 19, 9)]],
628
- // Large coordinates
629
- ["large coords", "\x1b[<0;300;200M", [sgr("\x1b[<0;300;200M", "down", 299, 199)]],
630
- // Modifiers
631
- ["shift+left down", "\x1b[<4;1;1M", [sgr("\x1b[<4;1;1M", "down", 0, 0, { mods: { shift: true } })]],
632
- ["alt+left down", "\x1b[<8;1;1M", [sgr("\x1b[<8;1;1M", "down", 0, 0, { mods: { alt: true } })]],
633
- ["ctrl+left down", "\x1b[<16;1;1M", [sgr("\x1b[<16;1;1M", "down", 0, 0, { mods: { ctrl: true } })]],
634
- ])
635
-
636
- test("drag detection after button down", () => {
637
- const p = createParser()
638
- try {
639
- // Button 0 down, then motion with button 0 flag
640
- p.push(Buffer.from("\x1b[<0;5;5M\x1b[<32;6;5M"))
641
- const s = snap(p)
642
- expect(s).toHaveLength(2)
643
- expect(s[0]).toEqual(sgr("\x1b[<0;5;5M", "down", 4, 4))
644
- expect(s[1]).toEqual(sgr("\x1b[<32;6;5M", "drag", 5, 4))
645
- } finally {
646
- p.destroy()
647
- }
648
- })
649
-
650
- test("split SGR across two pushes", () => {
651
- const p = createParser()
652
- try {
653
- p.push(Buffer.from("\x1b[<64;10;"))
654
- expect(snap(p)).toEqual([])
655
- p.push(Buffer.from("5M"))
656
- expect(snap(p)).toEqual([sgr("\x1b[<64;10;5M", "scroll", 9, 4, { scroll: { direction: "up", delta: 1 } })])
657
- } finally {
658
- p.destroy()
659
- }
660
- })
661
-
662
- test("multiple mouse events in one push", () => {
663
- const p = createParser()
664
- try {
665
- p.push(Buffer.from("\x1b[<0;1;1M\x1b[<0;2;1M\x1b[<0;2;1m"))
666
- const s = snap(p)
667
- expect(s).toHaveLength(3)
668
- expect(s[0]).toEqual(sgr("\x1b[<0;1;1M", "down", 0, 0))
669
- expect(s[1]).toEqual(sgr("\x1b[<0;2;1M", "down", 1, 0))
670
- expect(s[2]).toEqual(sgr("\x1b[<0;2;1m", "up", 1, 0))
671
- } finally {
672
- p.destroy()
673
- }
674
- })
675
- })
676
-
677
- describe("mouse: X10 protocol", () => {
678
- // X10: ESC [ M <button+32> <x+33> <y+33>
679
- const leftDown = x10bytes(0, 0, 0)
680
- const middleDown = x10bytes(1, 0, 0)
681
- const rightDown = x10bytes(2, 0, 0)
682
- const release = x10bytes(3, 0, 0)
683
- const at1020 = x10bytes(0, 10, 20)
684
- const move = x10bytes(35, 4, 5)
685
- const scrollUp = x10bytes(64, 2, 3)
686
- const shiftLeftDown = x10bytes(4, 0, 0)
687
- const ctrlScrollUp = x10bytes(80, 7, 8)
688
-
689
- table([
690
- ["left down (0,0)", leftDown, [x10m(latin1(leftDown), "down", 0, 0)]],
691
- ["middle down", middleDown, [x10m(latin1(middleDown), "down", 0, 0, { button: 1 })]],
692
- ["right down", rightDown, [x10m(latin1(rightDown), "down", 0, 0, { button: 2 })]],
693
- ["release", release, [x10m(latin1(release), "up", 0, 0)]],
694
- ["at position 10,20", at1020, [x10m(latin1(at1020), "down", 10, 20)]],
695
- ["move with no button", move, [x10m(latin1(move), "move", 4, 5, { button: -1 })]],
696
- ["scroll up", scrollUp, [x10m(latin1(scrollUp), "scroll", 2, 3, { scroll: { direction: "up", delta: 1 } })]],
697
- ["shift+left down", shiftLeftDown, [x10m(latin1(shiftLeftDown), "down", 0, 0, { mods: { shift: true } })]],
698
- [
699
- "ctrl+scroll up",
700
- ctrlScrollUp,
701
- [x10m(latin1(ctrlScrollUp), "scroll", 7, 8, { mods: { ctrl: true }, scroll: { direction: "up", delta: 1 } })],
702
- ],
703
- ])
704
-
705
- test("X10 mouse followed by key", () => {
706
- const p = createParser()
707
- try {
708
- p.push(Buffer.from("\x1b[M !!x"))
709
- const s = snap(p)
710
- expect(s).toHaveLength(2)
711
- expect(s[0]).toEqual(x10m("\x1b[M !!", "down", 0, 0))
712
- expect(s[1]).toEqual(k("x"))
713
- } finally {
714
- p.destroy()
715
- }
716
- })
717
-
718
- test("split X10 across pushes waits for payload", () => {
719
- const p = createParser()
720
- try {
721
- p.push(Buffer.from("\x1b[M"))
722
- expect(snap(p)).toEqual([])
723
- p.push(Buffer.from(" !!"))
724
- expect(snap(p)).toEqual([x10m("\x1b[M !!", "down", 0, 0)])
725
- } finally {
726
- p.destroy()
727
- }
728
- })
729
-
730
- test("delayed X10 continuation after timed-out escape becomes a mouse event", () => {
731
- const { parser, clock } = createTimedParser()
732
- try {
733
- parser.push(Buffer.from("\x1b"))
734
- expect(snap(parser)).toEqual([])
735
- clock.advance(10)
736
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
737
-
738
- parser.push(Buffer.from("[M"))
739
- expect(snap(parser)).toEqual([])
740
- parser.push(Buffer.from(" !!"))
741
- expect(snap(parser)).toEqual([x10m("\x1b[M !!", "down", 0, 0)])
742
- } finally {
743
- parser.destroy()
744
- }
745
- })
746
- })
747
-
748
- describe("UTF-8 handling", () => {
749
- table([
750
- ["2-byte (é)", "\u00e9", [k("\u00e9")]],
751
- ["3-byte (中)", "\u4e2d", [k("\u4e2d")]],
752
- ["4-byte (👍)", "👍", [k("👍")]],
753
- ["multiple utf-8 chars", "日本語", [k("日"), k("本"), k("語")]],
754
- ])
755
-
756
- test("2-byte split at byte boundary", () => {
757
- const bytes = Buffer.from("é")
758
- expect(bytes.length).toBe(2)
759
- const p = createParser()
760
- try {
761
- p.push(bytes.subarray(0, 1))
762
- expect(snap(p)).toEqual([])
763
- p.push(bytes.subarray(1))
764
- expect(snap(p)).toEqual([k("é")])
765
- } finally {
766
- p.destroy()
767
- }
768
- })
769
-
770
- test("3-byte split at every boundary", () => {
771
- const bytes = Buffer.from("中")
772
- expect(bytes.length).toBe(3)
773
- for (let split = 1; split < bytes.length; split++) {
774
- const p = createParser()
775
- try {
776
- p.push(bytes.subarray(0, split))
777
- expect(snap(p)).toEqual([])
778
- p.push(bytes.subarray(split))
779
- expect(snap(p)).toEqual([k("中")])
780
- } finally {
781
- p.destroy()
782
- }
783
- }
784
- })
785
-
786
- test("4-byte split at every boundary", () => {
787
- const bytes = Buffer.from("👍")
788
- expect(bytes.length).toBe(4)
789
- for (let split = 1; split < bytes.length; split++) {
790
- const p = createParser()
791
- try {
792
- p.push(bytes.subarray(0, split))
793
- expect(snap(p)).toEqual([])
794
- p.push(bytes.subarray(split))
795
- expect(snap(p)).toEqual([k("👍")])
796
- } finally {
797
- p.destroy()
798
- }
799
- }
800
- })
801
-
802
- test("invalid UTF-8 lead (0xC0) followed by ASCII falls back to legacy high-byte", () => {
803
- const p = createParser()
804
- try {
805
- p.push(Uint8Array.from([0xc0, 0x41]))
806
- const s = snap(p)
807
- expect(s).toHaveLength(2)
808
- // 0xC0 - 128 = 0x40 = '@', treated as ESC + '@' → legacy path
809
- expect(s[0]!.type).toBe("key")
810
- expect(s[1]).toEqual(k("a", { raw: "A", shift: true }))
811
- } finally {
812
- p.destroy()
813
- }
814
- })
815
-
816
- test("invalid continuation byte after valid lead falls back to legacy", () => {
817
- const p = createParser()
818
- try {
819
- p.push(Uint8Array.from([0xe9])) // valid 3-byte lead
820
- expect(snap(p)).toEqual([]) // waits for continuation
821
- p.push(Buffer.from("x")) // not a continuation byte
822
- const s = snap(p)
823
- expect(s).toEqual([
824
- k("i", { raw: "\x1bi", meta: true }), // 0xe9 → legacy: 0xe9-128=0x69='i', ESC prefix
825
- k("x"),
826
- ])
827
- } finally {
828
- p.destroy()
829
- }
830
- })
831
-
832
- test("legacy single high-byte on timeout", () => {
833
- const { parser, clock } = createTimedParser()
834
- try {
835
- parser.push(Uint8Array.from([0xe9]))
836
- expect(snap(parser)).toEqual([])
837
- clock.advance(10)
838
- expect(snap(parser)).toEqual([k("i", { raw: "\x1bi", meta: true })])
839
- } finally {
840
- parser.destroy()
841
- }
842
- })
843
-
844
- test("high byte 0xFF on timeout → meta+backspace", () => {
845
- const { parser, clock } = createTimedParser()
846
- try {
847
- parser.push(Uint8Array.from([0xff]))
848
- expect(snap(parser)).toEqual([])
849
- clock.advance(10)
850
- // 0xFF - 128 = 0x7F = DEL, so ESC + DEL = meta+backspace
851
- expect(snap(parser)).toEqual([k("backspace", { raw: "\x1b\x7f", meta: true })])
852
- } finally {
853
- parser.destroy()
854
- }
855
- })
856
- })
857
-
858
- describe("protocol responses", () => {
859
- table([
860
- // OSC (BEL-terminated)
861
- ["OSC (BEL)", "\x1b]4;0;#ffffff\x07", [resp("osc", "\x1b]4;0;#ffffff\x07")]],
862
- // OSC (ESC \\ terminated)
863
- ["OSC (ST)", "\x1b]4;0;rgb:ff/ff/ff\x1b\\", [resp("osc", "\x1b]4;0;rgb:ff/ff/ff\x1b\\")]],
864
- // DCS
865
- ["DCS", "\x1bP>|kitty(0.40.1)\x1b\\", [resp("dcs", "\x1bP>|kitty(0.40.1)\x1b\\")]],
866
- // APC
867
- ["APC", "\x1b_Gi=1;OK\x1b\\", [resp("apc", "\x1b_Gi=1;OK\x1b\\")]],
868
- // Focus
869
- ["focus in", "\x1b[I", [resp("csi", "\x1b[I")]],
870
- ["focus out", "\x1b[O", [resp("csi", "\x1b[O")]],
871
- // DA (Device Attributes)
872
- ["DA1", "\x1b[?62;1;2;6;7;8;9;15;22c", [resp("csi", "\x1b[?62;1;2;6;7;8;9;15;22c")]],
873
- // CPR (Cursor Position Report)
874
- ["CPR", "\x1b[24;80R", [resp("cpr", "\x1b[24;80R")]],
875
- // Window/cell size
876
- ["window size", "\x1b[4;600;800t", [resp("csi", "\x1b[4;600;800t")]],
877
- // Mode report
878
- ["mode report", "\x1b[?2004;1$y", [resp("csi", "\x1b[?2004;1$y")]],
879
- ])
880
-
881
- test("all three protocol responses in one push", () => {
882
- const p = createParser()
883
- try {
884
- p.push(Buffer.from("\x1b]4;0;#fff\x07\x1bP>|test\x1b\\\x1b_OK\x1b\\"))
885
- expect(snap(p)).toEqual([
886
- resp("osc", "\x1b]4;0;#fff\x07"),
887
- resp("dcs", "\x1bP>|test\x1b\\"),
888
- resp("apc", "\x1b_OK\x1b\\"),
889
- ])
890
- } finally {
891
- p.destroy()
892
- }
893
- })
894
-
895
- test("split OSC across pushes", () => {
896
- const p = createParser()
897
- try {
898
- p.push(Buffer.from("\x1b]4;0;"))
899
- expect(snap(p)).toEqual([])
900
- p.push(Buffer.from("#ffffff\x07"))
901
- expect(snap(p)).toEqual([resp("osc", "\x1b]4;0;#ffffff\x07")])
902
- } finally {
903
- p.destroy()
904
- }
905
- })
906
-
907
- test("split DCS terminator ESC \\ across pushes", () => {
908
- const p = createParser()
909
- try {
910
- p.push(Buffer.from("\x1bPtest\x1b"))
911
- expect(snap(p)).toEqual([])
912
- p.push(Buffer.from("\\"))
913
- expect(snap(p)).toEqual([resp("dcs", "\x1bPtest\x1b\\")])
914
- } finally {
915
- p.destroy()
916
- }
917
- })
918
-
919
- test("focus events interleaved with keys", () => {
920
- const p = createParser()
921
- try {
922
- p.push(Buffer.from("a\x1b[Ib\x1b[Oc"))
923
- expect(snap(p)).toEqual([k("a"), resp("csi", "\x1b[I"), k("b"), resp("csi", "\x1b[O"), k("c")])
924
- } finally {
925
- p.destroy()
926
- }
927
- })
928
-
929
- test("partial OSC flushes on timeout as unknown", () => {
930
- const { parser, clock } = createTimedParser()
931
- try {
932
- parser.push(Buffer.from("\x1b]incomplete"))
933
- expect(snap(parser)).toEqual([])
934
- clock.advance(10)
935
- expect(snap(parser)).toEqual([resp("unknown", "\x1b]incomplete")])
936
- } finally {
937
- parser.destroy()
938
- }
939
- })
940
-
941
- test("partial DCS flushes on timeout as unknown", () => {
942
- const { parser, clock } = createTimedParser()
943
- try {
944
- parser.push(Buffer.from("\x1bPpartial"))
945
- expect(snap(parser)).toEqual([])
946
- clock.advance(10)
947
- expect(snap(parser)).toEqual([resp("unknown", "\x1bPpartial")])
948
- } finally {
949
- parser.destroy()
950
- }
951
- })
952
-
953
- test("partial APC flushes on timeout as unknown", () => {
954
- const { parser, clock } = createTimedParser()
955
- try {
956
- parser.push(Buffer.from("\x1b_partial"))
957
- expect(snap(parser)).toEqual([])
958
- clock.advance(10)
959
- expect(snap(parser)).toEqual([resp("unknown", "\x1b_partial")])
960
- } finally {
961
- parser.destroy()
962
- }
963
- })
964
-
965
- test("partial generic CSI flushes on timeout as unknown", () => {
966
- const { parser, clock } = createTimedParser()
967
- try {
968
- parser.push(Buffer.from("\x1b[123"))
969
- expect(snap(parser)).toEqual([])
970
- clock.advance(10)
971
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[123")])
972
- } finally {
973
- parser.destroy()
974
- }
975
- })
976
-
977
- test("partial kitty CSI stays pending after timeout", () => {
978
- const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
979
- try {
980
- parser.push(Buffer.from("\x1b[118;5"))
981
- expect(snap(parser)).toEqual([])
982
- clock.advance(10)
983
- expect(snap(parser)).toEqual([])
984
- } finally {
985
- parser.destroy()
986
- }
987
- })
988
-
989
- test("partial kitty CSI stays pending after timeout when split after first semicolon", () => {
990
- const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
991
- try {
992
- parser.push(Buffer.from("\x1b[97;"))
993
- expect(snap(parser)).toEqual([])
994
- clock.advance(10)
995
- expect(snap(parser)).toEqual([])
996
-
997
- parser.push(Buffer.from("2u"))
998
- expect(snap(parser)).toEqual([k("a", { shift: true, raw: "\x1b[97;2u" })])
999
- } finally {
1000
- parser.destroy()
1001
- }
1002
- })
1003
-
1004
- test("partial kitty alternate-key CSI stays pending after timeout", () => {
1005
- const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1006
- try {
1007
- parser.push(Buffer.from("\x1b[97:65;"))
1008
- expect(snap(parser)).toEqual([])
1009
- clock.advance(10)
1010
- expect(snap(parser)).toEqual([])
1011
-
1012
- parser.push(Buffer.from("6:1u"))
1013
- expect(snap(parser)).toEqual([k("a", { raw: "\x1b[97:65;6:1u", ctrl: true, shift: true })])
1014
- } finally {
1015
- parser.destroy()
1016
- }
1017
- })
1018
-
1019
- test("partial kitty CSI stays pending after timeout for higher modifier bits", () => {
1020
- const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1021
- try {
1022
- parser.push(Buffer.from("\x1b[97;9"))
1023
- expect(snap(parser)).toEqual([])
1024
- clock.advance(10)
1025
- expect(snap(parser)).toEqual([])
1026
-
1027
- parser.push(Buffer.from("u"))
1028
- const event = parser.read()
1029
- expect(event?.type).toBe("key")
1030
- if (!event || event.type !== "key") throw new Error("expected key event")
1031
- expect(event.raw).toBe("\x1b[97;9u")
1032
- expect(event.key.name).toBe("a")
1033
- expect(event.key.super).toBe(true)
1034
- expect(parser.read()).toBeNull()
1035
- } finally {
1036
- parser.destroy()
1037
- }
1038
- })
1039
-
1040
- test("partial kitty special-key CSI stays pending after timeout", () => {
1041
- const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1042
- try {
1043
- parser.push(Buffer.from("\x1b[1;1:"))
1044
- expect(snap(parser)).toEqual([])
1045
- clock.advance(10)
1046
- expect(snap(parser)).toEqual([])
1047
-
1048
- parser.push(Buffer.from("3A"))
1049
- expect(snap(parser)).toEqual([k("up", { raw: "\x1b[1;1:3A", eventType: "release" })])
1050
- } finally {
1051
- parser.destroy()
1052
- }
1053
- })
1054
-
1055
- test("partial SGR mouse CSI stays pending after timeout", () => {
1056
- const { parser, clock } = createTimedParser()
1057
- try {
1058
- parser.push(Buffer.from("\x1b[<35;20"))
1059
- expect(snap(parser)).toEqual([])
1060
- clock.advance(10)
1061
- expect(snap(parser)).toEqual([])
1062
-
1063
- parser.push(Buffer.from(";5m"))
1064
- expect(snap(parser)).toEqual([sgr("\x1b[<35;20;5m", "move", 19, 4)])
1065
- } finally {
1066
- parser.destroy()
1067
- }
1068
- })
1069
-
1070
- test("split CSI across reads reassembles after timeout", () => {
1071
- const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1072
- try {
1073
- // Kitty Ctrl+V release split across two reads
1074
- parser.push(Buffer.from("\x1b[118;5"))
1075
- expect(snap(parser)).toEqual([])
1076
- clock.advance(10)
1077
- // Stays pending — not flushed
1078
- expect(snap(parser)).toEqual([])
1079
- parser.push(Buffer.from(";3u"))
1080
- expect(snap(parser)).toEqual([k("v", { ctrl: true, raw: "\x1b[118;5;3u" })])
1081
- } finally {
1082
- parser.destroy()
1083
- }
1084
- })
1085
-
1086
- test("split kitty escape CSI across reads reassembles after timeout", () => {
1087
- const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1088
- try {
1089
- parser.push(Buffer.from("\x1b[27;5"))
1090
- expect(snap(parser)).toEqual([])
1091
- clock.advance(10)
1092
- expect(snap(parser)).toEqual([])
1093
- parser.push(Buffer.from("u"))
1094
- expect(snap(parser)).toEqual([k("escape", { ctrl: true, raw: "\x1b[27;5u" })])
1095
- } finally {
1096
- parser.destroy()
1097
- }
1098
- })
1099
-
1100
- test("timed-out standard one-semicolon CSI key flushes before later text", () => {
1101
- const { parser, clock } = createTimedParser()
1102
- try {
1103
- parser.push(Buffer.from("\x1b[1;5"))
1104
- expect(snap(parser)).toEqual([])
1105
- clock.advance(10)
1106
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[1;5")])
1107
-
1108
- parser.push(Buffer.from("A"))
1109
- expect(snap(parser)).toEqual([k("a", { raw: "A", shift: true })])
1110
- } finally {
1111
- parser.destroy()
1112
- }
1113
- })
1114
-
1115
- test("timed-out one-semicolon CSI response flushes before later text", () => {
1116
- const { parser, clock } = createTimedParser()
1117
- try {
1118
- parser.push(Buffer.from("\x1b[24;80"))
1119
- expect(snap(parser)).toEqual([])
1120
- clock.advance(10)
1121
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[24;80")])
1122
-
1123
- parser.push(Buffer.from("R"))
1124
- expect(snap(parser)).toEqual([k("r", { raw: "R", shift: true })])
1125
- } finally {
1126
- parser.destroy()
1127
- }
1128
- })
1129
-
1130
- test("timed-out partial kitty CSI resyncs on a later ESC", () => {
1131
- const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1132
- try {
1133
- parser.push(Buffer.from("\x1b[118;5"))
1134
- expect(snap(parser)).toEqual([])
1135
- clock.advance(10)
1136
- expect(snap(parser)).toEqual([])
1137
-
1138
- parser.push(Buffer.from("\x1b[A"))
1139
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[118;5"), k("up", { raw: "\x1b[A" })])
1140
- } finally {
1141
- parser.destroy()
1142
- }
1143
- })
1144
-
1145
- test("timed-out partial kitty CSI flushes before unrelated later text", () => {
1146
- const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })
1147
- try {
1148
- parser.push(Buffer.from("\x1b[118;5"))
1149
- expect(snap(parser)).toEqual([])
1150
- clock.advance(10)
1151
- expect(snap(parser)).toEqual([])
1152
-
1153
- parser.push(Buffer.from("a"))
1154
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[118;5"), k("a")])
1155
- } finally {
1156
- parser.destroy()
1157
- }
1158
- })
1159
-
1160
- test("partial generic CSI timeout flush does not swallow later text", () => {
1161
- const { parser, clock } = createTimedParser()
1162
- try {
1163
- parser.push(Buffer.from("\x1b[123"))
1164
- expect(snap(parser)).toEqual([])
1165
- clock.advance(10)
1166
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[123")])
1167
-
1168
- parser.push(Buffer.from("a"))
1169
- expect(snap(parser)).toEqual([k("a")])
1170
- } finally {
1171
- parser.destroy()
1172
- }
1173
- })
1174
-
1175
- test("partial large-parameter CSI flushes on timeout before later text", () => {
1176
- const { parser, clock } = createTimedParser()
1177
- try {
1178
- parser.push(Buffer.from("\x1b[80;120"))
1179
- expect(snap(parser)).toEqual([])
1180
- clock.advance(10)
1181
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[80;120")])
1182
-
1183
- parser.push(Buffer.from("a"))
1184
- expect(snap(parser)).toEqual([k("a")])
1185
- } finally {
1186
- parser.destroy()
1187
- }
1188
- })
1189
-
1190
- test("partial OSC timeout flush does not swallow later text", () => {
1191
- const { parser, clock } = createTimedParser()
1192
- try {
1193
- parser.push(Buffer.from("\x1b]52;c;"))
1194
- expect(snap(parser)).toEqual([])
1195
- clock.advance(10)
1196
- expect(snap(parser)).toEqual([resp("unknown", "\x1b]52;c;")])
1197
-
1198
- parser.push(Buffer.from("abc"))
1199
- expect(snap(parser)).toEqual([k("a"), k("b"), k("c")])
1200
- } finally {
1201
- parser.destroy()
1202
- }
1203
- })
1204
-
1205
- test("partial OSC timeout flush does not swallow later escape sequences", () => {
1206
- const { parser, clock } = createTimedParser()
1207
- try {
1208
- parser.push(Buffer.from("\x1b]52;c;"))
1209
- expect(snap(parser)).toEqual([])
1210
- clock.advance(10)
1211
- expect(snap(parser)).toEqual([resp("unknown", "\x1b]52;c;")])
1212
-
1213
- parser.push(Buffer.from("\x1b[A"))
1214
- expect(snap(parser)).toEqual([k("up", { raw: "\x1b[A" })])
1215
- } finally {
1216
- parser.destroy()
1217
- }
1218
- })
1219
- })
1220
-
1221
- describe("protocol context", () => {
1222
- test("partial explicit-width CPR stays pending after timeout when probe is active", () => {
1223
- const { parser, clock } = createTimedParser({
1224
- protocolContext: { explicitWidthCprActive: true },
1225
- })
1226
-
1227
- try {
1228
- parser.push(Buffer.from("\x1b[1;2"))
1229
- expect(snap(parser)).toEqual([])
1230
- clock.advance(10)
1231
- expect(snap(parser)).toEqual([])
1232
-
1233
- parser.push(Buffer.from("R"))
1234
- expect(snap(parser)).toEqual([resp("cpr", "\x1b[1;2R")])
1235
- } finally {
1236
- parser.destroy()
1237
- }
1238
- })
1239
-
1240
- test("partial explicit-width CPR flushes before later text when probe is inactive", () => {
1241
- const { parser, clock } = createTimedParser()
1242
-
1243
- try {
1244
- parser.push(Buffer.from("\x1b[1;2"))
1245
- expect(snap(parser)).toEqual([])
1246
- clock.advance(10)
1247
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[1;2")])
1248
-
1249
- parser.push(Buffer.from("R"))
1250
- expect(snap(parser)).toEqual([k("r", { raw: "R", shift: true })])
1251
- } finally {
1252
- parser.destroy()
1253
- }
1254
- })
1255
-
1256
- test("partial pixel resolution response stays pending after timeout while query is active", () => {
1257
- const { parser, clock } = createTimedParser({
1258
- protocolContext: { pixelResolutionQueryActive: true },
1259
- })
1260
-
1261
- try {
1262
- parser.push(Buffer.from("\x1b[4;1080;192"))
1263
- expect(snap(parser)).toEqual([])
1264
- clock.advance(10)
1265
- expect(snap(parser)).toEqual([])
1266
-
1267
- parser.push(Buffer.from("0t"))
1268
- expect(snap(parser)).toEqual([resp("csi", "\x1b[4;1080;1920t")])
1269
- } finally {
1270
- parser.destroy()
1271
- }
1272
- })
1273
-
1274
- test("partial DECRPM stays pending after timeout while capability probe is active", () => {
1275
- const { parser, clock } = createTimedParser({
1276
- protocolContext: { privateCapabilityRepliesActive: true },
1277
- })
1278
-
1279
- try {
1280
- parser.push(Buffer.from("\x1b[?1016;2$"))
1281
- expect(snap(parser)).toEqual([])
1282
- clock.advance(10)
1283
- expect(snap(parser)).toEqual([])
1284
-
1285
- parser.push(Buffer.from("y"))
1286
- expect(snap(parser)).toEqual([resp("csi", "\x1b[?1016;2$y")])
1287
- } finally {
1288
- parser.destroy()
1289
- }
1290
- })
1291
-
1292
- test("partial DA1 stays pending after timeout while capability probe is active", () => {
1293
- const { parser, clock } = createTimedParser({
1294
- protocolContext: { privateCapabilityRepliesActive: true },
1295
- })
1296
-
1297
- try {
1298
- parser.push(Buffer.from("\x1b[?62;"))
1299
- expect(snap(parser)).toEqual([])
1300
- clock.advance(10)
1301
- expect(snap(parser)).toEqual([])
1302
-
1303
- parser.push(Buffer.from("c"))
1304
- expect(snap(parser)).toEqual([resp("csi", "\x1b[?62;c")])
1305
- } finally {
1306
- parser.destroy()
1307
- }
1308
- })
1309
-
1310
- test("theme mode replies are emitted as CSI responses", () => {
1311
- const parser = createParser({
1312
- protocolContext: { privateCapabilityRepliesActive: true },
1313
- })
1314
-
1315
- try {
1316
- parser.push(Buffer.from("\x1b[?997;1n"))
1317
- expect(snap(parser)).toEqual([resp("csi", "\x1b[?997;1n")])
1318
- } finally {
1319
- parser.destroy()
1320
- }
1321
- })
1322
-
1323
- test("partial theme mode reply stays pending after timeout while capability probe is active", () => {
1324
- const { parser, clock } = createTimedParser({
1325
- protocolContext: { privateCapabilityRepliesActive: true },
1326
- })
1327
-
1328
- try {
1329
- parser.push(Buffer.from("\x1b[?997;1"))
1330
- expect(snap(parser)).toEqual([])
1331
- clock.advance(10)
1332
- expect(snap(parser)).toEqual([])
1333
-
1334
- parser.push(Buffer.from("n"))
1335
- expect(snap(parser)).toEqual([resp("csi", "\x1b[?997;1n")])
1336
- } finally {
1337
- parser.destroy()
1338
- }
1339
- })
1340
-
1341
- test("timed-out modified CSI key still flushes before later final byte", () => {
1342
- const { parser, clock } = createTimedParser({
1343
- protocolContext: { explicitWidthCprActive: true },
1344
- })
1345
-
1346
- try {
1347
- parser.push(Buffer.from("\x1b[1;5"))
1348
- expect(snap(parser)).toEqual([])
1349
- clock.advance(10)
1350
- expect(snap(parser)).toEqual([])
1351
-
1352
- parser.push(Buffer.from("A"))
1353
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[1;5"), k("a", { raw: "A", shift: true })])
1354
- } finally {
1355
- parser.destroy()
1356
- }
1357
- })
1358
-
1359
- test("generic row/col CPR does not reassemble during explicit-width probe window", () => {
1360
- const { parser, clock } = createTimedParser({
1361
- protocolContext: { explicitWidthCprActive: true },
1362
- })
1363
-
1364
- try {
1365
- parser.push(Buffer.from("\x1b[24;80"))
1366
- expect(snap(parser)).toEqual([])
1367
- clock.advance(10)
1368
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[24;80")])
1369
-
1370
- parser.push(Buffer.from("R"))
1371
- expect(snap(parser)).toEqual([k("r", { raw: "R", shift: true })])
1372
- } finally {
1373
- parser.destroy()
1374
- }
1375
- })
1376
-
1377
- test("generic row/col CPR stays pending after timeout while startup cursor probe is active", () => {
1378
- const { parser, clock } = createTimedParser({
1379
- protocolContext: { startupCursorCprActive: true },
1380
- })
1381
-
1382
- try {
1383
- parser.push(Buffer.from("\x1b[24;80"))
1384
- expect(snap(parser)).toEqual([])
1385
- clock.advance(10)
1386
- expect(snap(parser)).toEqual([])
1387
-
1388
- parser.push(Buffer.from("R"))
1389
- expect(snap(parser)).toEqual([resp("cpr", "\x1b[24;80R")])
1390
- } finally {
1391
- parser.destroy()
1392
- }
1393
- })
1394
-
1395
- test("aborting a pending startup cursor CPR swallows a reply that finishes later", () => {
1396
- const parser = createParser({
1397
- protocolContext: { startupCursorCprActive: true },
1398
- })
1399
-
1400
- try {
1401
- parser.push(Buffer.from("\x1b[24;80"))
1402
- expect(snap(parser)).toEqual([])
1403
-
1404
- parser.abortPendingStartupCursorCpr()
1405
- parser.updateProtocolContext({ startupCursorCprActive: false })
1406
-
1407
- expect(snap(parser)).toEqual([])
1408
-
1409
- parser.push(Buffer.from("R"))
1410
- expect(snap(parser)).toEqual([])
1411
- } finally {
1412
- parser.destroy()
1413
- }
1414
- })
1415
-
1416
- test("aborting a pending startup cursor CPR swallows a reply split after the CSI introducer", () => {
1417
- const parser = createParser({
1418
- protocolContext: { startupCursorCprActive: true },
1419
- })
1420
-
1421
- try {
1422
- parser.push(Buffer.from("\x1b["))
1423
- expect(snap(parser)).toEqual([])
1424
-
1425
- parser.abortPendingStartupCursorCpr()
1426
- parser.updateProtocolContext({ startupCursorCprActive: false })
1427
-
1428
- parser.push(Buffer.from("24;80R"))
1429
- expect(snap(parser)).toEqual([])
1430
- } finally {
1431
- parser.destroy()
1432
- }
1433
- })
1434
-
1435
- test("aborting a pending startup cursor CPR preserves explicit-width CPR replies", () => {
1436
- const parser = createParser({
1437
- protocolContext: {
1438
- startupCursorCprActive: true,
1439
- explicitWidthCprActive: true,
1440
- },
1441
- })
1442
-
1443
- try {
1444
- parser.push(Buffer.from("\x1b["))
1445
- expect(snap(parser)).toEqual([])
1446
-
1447
- parser.abortPendingStartupCursorCpr()
1448
- parser.updateProtocolContext({ startupCursorCprActive: false })
1449
-
1450
- parser.push(Buffer.from("1;2R"))
1451
- expect(snap(parser)).toEqual([resp("cpr", "\x1b[1;2R")])
1452
- } finally {
1453
- parser.destroy()
1454
- }
1455
- })
1456
-
1457
- test("aborting a pending startup cursor CPR preserves later non-CPR input", () => {
1458
- const parser = createParser({
1459
- protocolContext: { startupCursorCprActive: true },
1460
- })
1461
-
1462
- try {
1463
- parser.push(Buffer.from("\x1b[24;80"))
1464
- expect(snap(parser)).toEqual([])
1465
-
1466
- parser.abortPendingStartupCursorCpr()
1467
- parser.updateProtocolContext({ startupCursorCprActive: false })
1468
-
1469
- parser.push(Buffer.from("a"))
1470
- expect(snap(parser)).toEqual([k("a")])
1471
- } finally {
1472
- parser.destroy()
1473
- }
1474
- })
1475
-
1476
- test("deferred explicit-width CPR flushes when probe context is cleared", () => {
1477
- const { parser, clock } = createTimedParser({
1478
- protocolContext: { explicitWidthCprActive: true },
1479
- })
1480
-
1481
- try {
1482
- parser.push(Buffer.from("\x1b[1;2"))
1483
- expect(snap(parser)).toEqual([])
1484
- clock.advance(10)
1485
- expect(snap(parser)).toEqual([])
1486
-
1487
- parser.updateProtocolContext({ explicitWidthCprActive: false })
1488
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[1;2")])
1489
- } finally {
1490
- parser.destroy()
1491
- }
1492
- })
1493
-
1494
- test("timed-out pending explicit-width CPR does not rearm until more bytes arrive", () => {
1495
- const clock = new ManualClock()
1496
- let timeoutFlushes = 0
1497
- let parser!: StdinParser
1498
- parser = new StdinParser({
1499
- armTimeouts: true,
1500
- clock,
1501
- timeoutMs: TEST_TIMEOUT_MS,
1502
- protocolContext: { explicitWidthCprActive: true },
1503
- onTimeoutFlush: () => {
1504
- timeoutFlushes += 1
1505
- parser.drain(() => {})
1506
- },
1507
- })
1508
-
1509
- try {
1510
- parser.push(Buffer.from("\x1b[1;2"))
1511
- clock.advance(10)
1512
- expect(timeoutFlushes).toBe(1)
1513
-
1514
- clock.advance(50)
1515
- expect(timeoutFlushes).toBe(1)
1516
- expect(snap(parser)).toEqual([])
1517
-
1518
- parser.push(Buffer.from(";"))
1519
- clock.advance(10)
1520
- expect(timeoutFlushes).toBe(2)
1521
- } finally {
1522
- parser.destroy()
1523
- }
1524
- })
1525
-
1526
- test("timed-out pending private reply does not rearm until more bytes arrive", () => {
1527
- const clock = new ManualClock()
1528
- let timeoutFlushes = 0
1529
- let parser!: StdinParser
1530
- parser = new StdinParser({
1531
- armTimeouts: true,
1532
- clock,
1533
- timeoutMs: TEST_TIMEOUT_MS,
1534
- protocolContext: { privateCapabilityRepliesActive: true },
1535
- onTimeoutFlush: () => {
1536
- timeoutFlushes += 1
1537
- parser.drain(() => {})
1538
- },
1539
- })
1540
-
1541
- try {
1542
- parser.push(Buffer.from("\x1b[?1016;2$"))
1543
- clock.advance(10)
1544
- expect(timeoutFlushes).toBe(1)
1545
-
1546
- clock.advance(50)
1547
- expect(timeoutFlushes).toBe(1)
1548
- expect(snap(parser)).toEqual([])
1549
-
1550
- parser.push(Buffer.from(";"))
1551
- clock.advance(10)
1552
- expect(timeoutFlushes).toBe(2)
1553
- } finally {
1554
- parser.destroy()
1555
- }
1556
- })
1557
- })
1558
-
1559
- describe("bracketed paste", () => {
1560
- table([
1561
- ["simple paste", "\x1b[200~hello\x1b[201~", [paste("hello")]],
1562
- ["empty paste", "\x1b[200~\x1b[201~", [paste("")]],
1563
- ["paste with newlines", "\x1b[200~line1\nline2\x1b[201~", [paste("line1\nline2")]],
1564
- ["paste with tabs", "\x1b[200~a\tb\x1b[201~", [paste("a\tb")]],
1565
- ["paste with ESC in body", "\x1b[200~abc\x1bdef\x1b[201~", [paste("abc\x1bdef")]],
1566
- ])
1567
-
1568
- test("split paste start marker across pushes", () => {
1569
- const start = "\x1b[200~"
1570
- for (let split = 1; split < start.length; split++) {
1571
- const p = createParser()
1572
- try {
1573
- p.push(Buffer.from(start.slice(0, split)))
1574
- p.push(Buffer.from(start.slice(split) + "hi\x1b[201~"))
1575
- expect(snap(p)).toEqual([paste("hi")])
1576
- } finally {
1577
- p.destroy()
1578
- }
1579
- }
1580
- })
1581
-
1582
- test("split paste end marker at every boundary", () => {
1583
- const end = "\x1b[201~"
1584
- for (let split = 1; split < end.length; split++) {
1585
- const p = createParser()
1586
- try {
1587
- p.push(Buffer.from("\x1b[200~hello"))
1588
- p.push(Buffer.from(end.slice(0, split)))
1589
- expect(snap(p)).toEqual([])
1590
- p.push(Buffer.from(end.slice(split)))
1591
- expect(snap(p)).toEqual([paste("hello")])
1592
- } finally {
1593
- p.destroy()
1594
- }
1595
- }
1596
- })
1597
-
1598
- test("paste body bytes do not alias caller buffers across pushes", () => {
1599
- const p = createParser()
1600
- try {
1601
- p.push(Buffer.from("\x1b[200~"))
1602
-
1603
- const chunk = Buffer.from("hello")
1604
- p.push(chunk)
1605
- chunk.fill(0x78)
1606
-
1607
- p.push(Buffer.from("\x1b[201~"))
1608
- expect(snap(p)).toEqual([paste("hello")])
1609
- } finally {
1610
- p.destroy()
1611
- }
1612
- })
1613
-
1614
- test("near-match end markers are part of paste body", () => {
1615
- const p = createParser()
1616
- try {
1617
- p.push(Buffer.from("\x1b[200~abc\x1b[202~def\x1b[201~"))
1618
- expect(snap(p)).toEqual([paste("abc\x1b[202~def")])
1619
- } finally {
1620
- p.destroy()
1621
- }
1622
- })
1623
-
1624
- test("doubled ESC before paste end marker", () => {
1625
- const p = createParser()
1626
- try {
1627
- p.push(Buffer.from("\x1b[200~abc\x1b"))
1628
- expect(snap(p)).toEqual([])
1629
- p.push(Buffer.from("\x1b[201~"))
1630
- expect(snap(p)).toEqual([paste("abc\x1b")])
1631
- } finally {
1632
- p.destroy()
1633
- }
1634
- })
1635
-
1636
- test("large paste does not grow parser buffer", () => {
1637
- const p = createParser({ maxPendingBytes: 32 })
1638
- const payload = "x".repeat(100_000)
1639
- try {
1640
- p.push(Buffer.from(`\x1b[200~${payload}\x1b[201~z`))
1641
- expect(snap(p)).toEqual([paste(payload), k("z")])
1642
- expect(p.bufferCapacity).toBeLessThanOrEqual(512)
1643
- } finally {
1644
- p.destroy()
1645
- }
1646
- })
1647
-
1648
- test("large paste across many small chunks", () => {
1649
- const p = createParser({ maxPendingBytes: 32 })
1650
- try {
1651
- p.push(Buffer.from("\x1b[200~"))
1652
- for (let i = 0; i < 1000; i++) p.push(Buffer.from("chunk "))
1653
- p.push(Buffer.from("\x1b[201~"))
1654
- const s = snap(p)
1655
- expect(s).toHaveLength(1)
1656
- expect(s[0]!.type).toBe("paste")
1657
- expect((s[0] as PasteSnap).bytes).toHaveLength(6000)
1658
- expect(p.bufferCapacity).toBeLessThanOrEqual(512)
1659
- } finally {
1660
- p.destroy()
1661
- }
1662
- })
1663
-
1664
- test("trailing bytes after paste end are parsed normally", () => {
1665
- const p = createParser()
1666
- try {
1667
- p.push(Buffer.from("\x1b[200~hello\x1b[201~\x1b[A"))
1668
- expect(snap(p)).toEqual([paste("hello"), k("up", { raw: "\x1b[A" })])
1669
- } finally {
1670
- p.destroy()
1671
- }
1672
- })
1673
-
1674
- test("back-to-back pastes", () => {
1675
- const p = createParser()
1676
- try {
1677
- p.push(Buffer.from("\x1b[200~first\x1b[201~\x1b[200~second\x1b[201~"))
1678
- expect(snap(p)).toEqual([paste("first"), paste("second")])
1679
- } finally {
1680
- p.destroy()
1681
- }
1682
- })
1683
-
1684
- test("paste with UTF-8 content", () => {
1685
- const p = createParser()
1686
- try {
1687
- p.push(Buffer.from("\x1b[200~日本語👍\x1b[201~"))
1688
- expect(snap(p)).toEqual([paste("日本語👍")])
1689
- } finally {
1690
- p.destroy()
1691
- }
1692
- })
1693
-
1694
- test("paste with UTF-8 split across chunks", () => {
1695
- const p = createParser()
1696
- const emoji = Buffer.from("👍")
1697
- try {
1698
- p.push(Buffer.from("\x1b[200~"))
1699
- p.push(emoji.subarray(0, 2))
1700
- p.push(emoji.subarray(2))
1701
- p.push(Buffer.from("\x1b[201~"))
1702
- expect(snap(p)).toEqual([paste("👍")])
1703
- } finally {
1704
- p.destroy()
1705
- }
1706
- })
1707
- })
1708
-
1709
- describe("ESC-less SGR continuation recovery", () => {
1710
- test("after timed-out ESC, scroll continuation still becomes a mouse event", () => {
1711
- const { parser, clock } = createTimedParser()
1712
- try {
1713
- parser.push(Buffer.from("\x1b"))
1714
- clock.advance(10)
1715
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1716
-
1717
- parser.push(Buffer.from("[<64;38;15M"))
1718
- expect(snap(parser)).toEqual([
1719
- sgr("\x1b[<64;38;15M", "scroll", 37, 14, { scroll: { direction: "up", delta: 1 } }),
1720
- ])
1721
- } finally {
1722
- parser.destroy()
1723
- }
1724
- })
1725
-
1726
- test("after timed-out ESC, continuation is recovered as mouse input", () => {
1727
- const { parser, clock } = createTimedParser()
1728
- try {
1729
- parser.push(Buffer.from("\x1b"))
1730
- clock.advance(10)
1731
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1732
-
1733
- parser.push(Buffer.from("[<35;20;5m"))
1734
- expect(snap(parser)).toEqual([sgr("\x1b[<35;20;5m", "move", 19, 4)])
1735
- } finally {
1736
- parser.destroy()
1737
- }
1738
- })
1739
-
1740
- test("after timed-out ESC, split continuation across pushes is recovered as mouse input", () => {
1741
- const { parser, clock } = createTimedParser()
1742
- try {
1743
- parser.push(Buffer.from("\x1b"))
1744
- clock.advance(10)
1745
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1746
-
1747
- parser.push(Buffer.from("["))
1748
- expect(snap(parser)).toEqual([])
1749
-
1750
- parser.push(Buffer.from("<35;20;5m"))
1751
- expect(snap(parser)).toEqual([sgr("\x1b[<35;20;5m", "move", 19, 4)])
1752
- } finally {
1753
- parser.destroy()
1754
- }
1755
- })
1756
-
1757
- test("after timed-out ESC, partial [< waits, then timeout flushes as one response", () => {
1758
- const { parser, clock } = createTimedParser()
1759
- try {
1760
- parser.push(Buffer.from("\x1b"))
1761
- clock.advance(10)
1762
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1763
-
1764
- parser.push(Buffer.from("[<35;20"))
1765
- expect(snap(parser)).toEqual([])
1766
- clock.advance(10)
1767
- expect(snap(parser)).toEqual([resp("unknown", "[<35;20")])
1768
- } finally {
1769
- parser.destroy()
1770
- }
1771
- })
1772
-
1773
- test("after timed-out ESC, [< followed by non-digit aborts immediately", () => {
1774
- const { parser, clock } = createTimedParser()
1775
- try {
1776
- parser.push(Buffer.from("\x1b"))
1777
- clock.advance(10)
1778
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1779
-
1780
- parser.push(Buffer.from("[<x"))
1781
- const s = snap(parser)
1782
- expect(s).toHaveLength(2)
1783
- expect(s[0]).toEqual(resp("unknown", "[<"))
1784
- expect(s[1]).toEqual(k("x"))
1785
- } finally {
1786
- parser.destroy()
1787
- }
1788
- })
1789
-
1790
- test("without prior flushed ESC, [< stays literal text", () => {
1791
- const p = createParser()
1792
- try {
1793
- p.push(Buffer.from("[<35;20;5m"))
1794
- expect(snap(p)).toEqual("[<35;20;5m".split("").map((char) => k(char)))
1795
- } finally {
1796
- p.destroy()
1797
- }
1798
- })
1799
-
1800
- test("without prior flushed ESC, standalone [ then < stay as individual keys", () => {
1801
- const p = createParser()
1802
- try {
1803
- p.push(Buffer.from("["))
1804
- expect(snap(p)).toEqual([k("[")])
1805
- p.push(Buffer.from("<"))
1806
- expect(snap(p)).toEqual([k("<")])
1807
- } finally {
1808
- p.destroy()
1809
- }
1810
- })
1811
-
1812
- test("after timed-out ESC, bare [ waits for more and then flushes as text", () => {
1813
- const { parser, clock } = createTimedParser()
1814
- try {
1815
- parser.push(Buffer.from("\x1b"))
1816
- clock.advance(10)
1817
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1818
-
1819
- parser.push(Buffer.from("["))
1820
- expect(snap(parser)).toEqual([])
1821
- clock.advance(10)
1822
- expect(snap(parser)).toEqual([k("[")])
1823
- } finally {
1824
- parser.destroy()
1825
- }
1826
- })
1827
- })
1828
-
1829
- describe("timeout behavior", () => {
1830
- test("default timeout at exact boundary (19ms no fire, 20ms fires)", () => {
1831
- const clock = new ManualClock()
1832
- const parser = new StdinParser({ armTimeouts: true, clock })
1833
- try {
1834
- parser.push(Buffer.from("\x1b"))
1835
- clock.advance(19)
1836
- expect(snap(parser)).toEqual([])
1837
- clock.advance(1)
1838
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1839
- } finally {
1840
- parser.destroy()
1841
- }
1842
- })
1843
-
1844
- test("configured timeout at exact boundary (9ms no fire, 10ms fires)", () => {
1845
- const { parser, clock } = createTimedParser()
1846
- try {
1847
- parser.push(Buffer.from("\x1b"))
1848
- clock.advance(9)
1849
- expect(snap(parser)).toEqual([])
1850
- clock.advance(1)
1851
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1852
- } finally {
1853
- parser.destroy()
1854
- }
1855
- })
1856
-
1857
- test("flushTimeout() only flushes when caller reports elapsed timeout", () => {
1858
- const parser = createParser({ timeoutMs: TEST_TIMEOUT_MS })
1859
- try {
1860
- parser.push(Buffer.from("\x1b"))
1861
-
1862
- parser.flushTimeout(TEST_TIMEOUT_MS - 1)
1863
- expect(snap(parser)).toEqual([])
1864
-
1865
- parser.flushTimeout(TEST_TIMEOUT_MS)
1866
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1867
- } finally {
1868
- parser.destroy()
1869
- }
1870
- })
1871
-
1872
- test("timeout resets when more bytes arrive", () => {
1873
- const { parser, clock } = createTimedParser()
1874
- try {
1875
- parser.push(Buffer.from("\x1b[<35;20;"))
1876
- clock.advance(9) // almost timeout
1877
- parser.push(Buffer.from("5")) // new byte resets timer
1878
- expect(snap(parser)).toEqual([])
1879
- clock.advance(9) // almost timeout again
1880
- expect(snap(parser)).toEqual([])
1881
- parser.push(Buffer.from("m")) // complete
1882
- expect(snap(parser)).toEqual([sgr("\x1b[<35;20;5m", "move", 19, 4)])
1883
- } finally {
1884
- parser.destroy()
1885
- }
1886
- })
1887
-
1888
- test("timed-out pending kitty CSI does not rearm until more bytes arrive", () => {
1889
- const clock = new ManualClock()
1890
- let timeoutFlushes = 0
1891
- let parser!: StdinParser
1892
- parser = new StdinParser({
1893
- armTimeouts: true,
1894
- clock,
1895
- timeoutMs: TEST_TIMEOUT_MS,
1896
- protocolContext: { kittyKeyboardEnabled: true },
1897
- onTimeoutFlush: () => {
1898
- timeoutFlushes += 1
1899
- parser.drain(() => {})
1900
- },
1901
- })
1902
-
1903
- try {
1904
- parser.push(Buffer.from("\x1b[118;5"))
1905
- clock.advance(10)
1906
- expect(timeoutFlushes).toBe(1)
1907
-
1908
- clock.advance(50)
1909
- expect(timeoutFlushes).toBe(1)
1910
- expect(snap(parser)).toEqual([])
1911
-
1912
- parser.push(Buffer.from(";"))
1913
- clock.advance(10)
1914
- expect(timeoutFlushes).toBe(2)
1915
- } finally {
1916
- parser.destroy()
1917
- }
1918
- })
1919
-
1920
- test("timed-out pending SGR mouse CSI does not rearm until more bytes arrive", () => {
1921
- const clock = new ManualClock()
1922
- let timeoutFlushes = 0
1923
- let parser!: StdinParser
1924
- parser = new StdinParser({
1925
- armTimeouts: true,
1926
- clock,
1927
- timeoutMs: TEST_TIMEOUT_MS,
1928
- onTimeoutFlush: () => {
1929
- timeoutFlushes += 1
1930
- parser.drain(() => {})
1931
- },
1932
- })
1933
-
1934
- try {
1935
- parser.push(Buffer.from("\x1b[<35;20"))
1936
- clock.advance(10)
1937
- expect(timeoutFlushes).toBe(1)
1938
-
1939
- clock.advance(50)
1940
- expect(timeoutFlushes).toBe(1)
1941
- expect(snap(parser)).toEqual([])
1942
-
1943
- parser.push(Buffer.from(";"))
1944
- clock.advance(10)
1945
- expect(timeoutFlushes).toBe(2)
1946
- } finally {
1947
- parser.destroy()
1948
- }
1949
- })
1950
-
1951
- test("timeout does not fire during paste mode", () => {
1952
- const { parser, clock } = createTimedParser()
1953
- try {
1954
- parser.push(Buffer.from("\x1b[200~partial"))
1955
- clock.advance(100) // way past timeout
1956
- expect(snap(parser)).toEqual([]) // still collecting paste
1957
- parser.push(Buffer.from("\x1b[201~"))
1958
- expect(snap(parser)).toEqual([paste("partial")])
1959
- } finally {
1960
- parser.destroy()
1961
- }
1962
- })
1963
-
1964
- test("multiple sequential timeouts", () => {
1965
- const { parser, clock } = createTimedParser()
1966
- try {
1967
- parser.push(Buffer.from("\x1b"))
1968
- clock.advance(10)
1969
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1970
-
1971
- parser.push(Buffer.from("\x1b"))
1972
- clock.advance(10)
1973
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1974
-
1975
- parser.push(Buffer.from("\x1b"))
1976
- clock.advance(10)
1977
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1978
- } finally {
1979
- parser.destroy()
1980
- }
1981
- })
1982
-
1983
- test("custom timeout delay", () => {
1984
- const { parser, clock } = createTimedParser({ timeoutMs: 50 })
1985
- try {
1986
- parser.push(Buffer.from("\x1b"))
1987
- clock.advance(49)
1988
- expect(snap(parser)).toEqual([])
1989
- clock.advance(1)
1990
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
1991
- } finally {
1992
- parser.destroy()
1993
- }
1994
- })
1995
-
1996
- test("data completing sequence before timeout cancels flush", () => {
1997
- const { parser, clock } = createTimedParser()
1998
- try {
1999
- parser.push(Buffer.from("\x1b"))
2000
- clock.advance(5) // halfway to timeout
2001
- parser.push(Buffer.from("[A")) // completes arrow sequence
2002
- expect(snap(parser)).toEqual([k("up", { raw: "\x1b[A" })])
2003
- clock.advance(100) // timeout would have fired, but sequence is done
2004
- expect(snap(parser)).toEqual([])
2005
- } finally {
2006
- parser.destroy()
2007
- }
2008
- })
2009
- })
2010
-
2011
- describe("embedded ESC abort", () => {
2012
- test("ESC inside partial CSI flushes as unknown, restarts", () => {
2013
- const p = createParser()
2014
- try {
2015
- p.push(Buffer.from("\x1b[<35;\x1b[<35;20;5m"))
2016
- expect(snap(p)).toEqual([resp("unknown", "\x1b[<35;"), sgr("\x1b[<35;20;5m", "move", 19, 4)])
2017
- } finally {
2018
- p.destroy()
2019
- }
2020
- })
2021
-
2022
- test("ESC inside partial CSI with no following data", () => {
2023
- const { parser, clock } = createTimedParser()
2024
- try {
2025
- parser.push(Buffer.from("\x1b[123\x1b"))
2026
- const s = snap(parser)
2027
- // first part flushed as unknown response, ESC starts new escape
2028
- expect(s).toEqual([resp("unknown", "\x1b[123")])
2029
- // the trailing ESC is pending
2030
- clock.advance(10)
2031
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
2032
- } finally {
2033
- parser.destroy()
2034
- }
2035
- })
2036
-
2037
- test("ESC inside OSC restarts parsing", () => {
2038
- const p = createParser()
2039
- try {
2040
- // ESC ] ... ESC ESC [ A — the first ESC after OSC body starts ST check,
2041
- // but second ESC byte is not \, so sawEsc resets. Then ESC starts escape.
2042
- // Actually: \x1b]foo has sawEsc=false. Then \x1b sets sawEsc=true.
2043
- // Then [ is not \, so sawEsc resets to false and [ is consumed as content.
2044
- // Then \x1b sets sawEsc=true. Then \ (0x5c = \\) terminates OSC.
2045
- p.push(Buffer.from("\x1b]foo\x1b\\"))
2046
- expect(snap(p)).toEqual([resp("osc", "\x1b]foo\x1b\\")])
2047
- } finally {
2048
- p.destroy()
2049
- }
2050
- })
2051
-
2052
- test("ESC in SS3 flushes partial as unknown", () => {
2053
- const p = createParser()
2054
- try {
2055
- p.push(Buffer.from("\x1bO\x1b[A"))
2056
- expect(snap(p)).toEqual([resp("unknown", "\x1bO"), k("up", { raw: "\x1b[A" })])
2057
- } finally {
2058
- p.destroy()
2059
- }
2060
- })
2061
- })
2062
-
2063
- describe("chunk-shape invariance", () => {
2064
- const sequences = [
2065
- "abc", // multiple ASCII
2066
- "\x1b[A", // arrow
2067
- "\x1bOP", // SS3 F1
2068
- "\x1b[[A", // Cygwin F1
2069
- "\x1b[[5~", // putty pageup
2070
- "\x1b[<0;10;20M", // SGR mouse
2071
- "\x1b[M !!", // X10 mouse
2072
- "\x1b]4;0;#ffffff\x07", // OSC
2073
- "\x1bP>|test\x1b\\", // DCS
2074
- "\x1b_OK\x1b\\", // APC
2075
- "\x1b[200~hello\x1b[201~", // paste
2076
- "\x1b[I", // focus in
2077
- "\x1b[1;5A", // ctrl+up
2078
- "\x1b[97u", // kitty key
2079
- "\x1b[27;2;13~", // modifyOtherKeys
2080
- ]
2081
-
2082
- for (const seq of sequences) {
2083
- test(`byte-at-a-time: ${JSON.stringify(seq).slice(1, -1).slice(0, 30)}`, () => {
2084
- assertChunkInvariant(Buffer.from(seq))
2085
- })
2086
- }
2087
-
2088
- test("mixed stream byte-at-a-time", () => {
2089
- const stream = Buffer.concat([
2090
- Buffer.from("x"),
2091
- Buffer.from("\x1b[<64;10;5M"),
2092
- Buffer.from("\x1b[I"),
2093
- Buffer.from("\x1b]4;0;#fff\x07"),
2094
- Buffer.from("\x1b[200~paste\x1b[201~"),
2095
- Buffer.from("👍"),
2096
- ])
2097
- assertChunkInvariant(stream)
2098
- })
2099
-
2100
- test("random two-chunk splits", () => {
2101
- const stream = Buffer.from("x\x1b[<64;10;5M\x1b[I\x1b]4;0;#fff\x07\x1b[200~p\x1b[201~y")
2102
- const whole = createParser()
2103
- try {
2104
- whole.push(stream)
2105
- const expected = snap(whole)
2106
- // Try splitting at every possible position
2107
- for (let split = 1; split < stream.length - 1; split++) {
2108
- const p = createParser()
2109
- try {
2110
- p.push(stream.subarray(0, split))
2111
- p.push(stream.subarray(split))
2112
- expect(snap(p)).toEqual(expected)
2113
- } finally {
2114
- p.destroy()
2115
- }
2116
- }
2117
- } finally {
2118
- whole.destroy()
2119
- }
2120
- })
2121
-
2122
- const comboAtoms: Array<[label: string, input: ChunkInput]> = [
2123
- ["ascii", "xy"],
2124
- ["utf8", "👍"],
2125
- ["arrow", "\x1b[A"],
2126
- ["sgr", "\x1b[<64;10;5M"],
2127
- ["x10", x10bytes(0, 0, 0)],
2128
- ["osc", "\x1b]4;0;#fff\x07"],
2129
- ["paste", "\x1b[200~p\x1b[201~"],
2130
- ["kitty", "\x1b[97u"],
2131
- ]
2132
-
2133
- for (const [firstLabel, first] of comboAtoms) {
2134
- for (const [secondLabel, second] of comboAtoms) {
2135
- test(`${firstLabel} + ${secondLabel} across every two-chunk split`, () => {
2136
- const stream = concatChunks([first, second])
2137
- const expected = snapChunks([stream])
2138
-
2139
- expect(snapChunks([first, second])).toEqual(expected)
2140
- for (let split = 1; split < stream.length; split++) {
2141
- expect(snapChunks([stream.subarray(0, split), stream.subarray(split)])).toEqual(expected)
2142
- }
2143
- })
2144
- }
2145
- }
2146
- })
2147
-
2148
- describe("state management", () => {
2149
- test("reset clears pending bytes and releases capacity", () => {
2150
- const p = createParser()
2151
- try {
2152
- p.push(Buffer.from("\x1b["))
2153
- expect(snap(p)).toEqual([])
2154
- p.push(Buffer.alloc(4096, 0x78)) // 'x' bytes to grow buffer
2155
- p.reset()
2156
- expect(snap(p)).toEqual([])
2157
- expect(p.bufferCapacity).toBeLessThanOrEqual(256)
2158
- // parser works normally after reset
2159
- p.push(Buffer.from("a"))
2160
- expect(snap(p)).toEqual([k("a")])
2161
- } finally {
2162
- p.destroy()
2163
- }
2164
- })
2165
-
2166
- test("reset during paste mode clears paste state", () => {
2167
- const p = createParser()
2168
- try {
2169
- p.push(Buffer.from("\x1b[200~partial paste"))
2170
- expect(snap(p)).toEqual([])
2171
- p.reset()
2172
- expect(snap(p)).toEqual([])
2173
- // parser works normally after reset
2174
- p.push(Buffer.from("a"))
2175
- expect(snap(p)).toEqual([k("a")])
2176
- } finally {
2177
- p.destroy()
2178
- }
2179
- })
2180
-
2181
- test("reset during escape sequence clears state", () => {
2182
- const p = createParser()
2183
- try {
2184
- p.push(Buffer.from("\x1b["))
2185
- expect(snap(p)).toEqual([])
2186
- p.reset()
2187
- // After reset, the partial CSI is gone; new input starts fresh
2188
- p.push(Buffer.from("A"))
2189
- expect(snap(p)).toEqual([k("a", { raw: "A", shift: true })]) // 'A' = shift+a
2190
- } finally {
2191
- p.destroy()
2192
- }
2193
- })
2194
-
2195
- test("double reset is safe", () => {
2196
- const p = createParser()
2197
- try {
2198
- p.push(Buffer.from("\x1b["))
2199
- p.reset()
2200
- p.reset()
2201
- p.push(Buffer.from("x"))
2202
- expect(snap(p)).toEqual([k("x")])
2203
- } finally {
2204
- p.destroy()
2205
- }
2206
- })
2207
-
2208
- test("double destroy is safe", () => {
2209
- const p = createParser()
2210
- p.destroy()
2211
- expect(() => p.destroy()).not.toThrow()
2212
- })
2213
-
2214
- test("push after destroy throws", () => {
2215
- const p = createParser()
2216
- p.destroy()
2217
- expect(() => p.push(Buffer.from("a"))).toThrow("destroyed")
2218
- })
2219
-
2220
- test("read after destroy throws", () => {
2221
- const p = createParser()
2222
- p.destroy()
2223
- expect(() => p.read()).toThrow("destroyed")
2224
- })
2225
-
2226
- test("drain after destroy throws", () => {
2227
- const p = createParser()
2228
- p.destroy()
2229
- expect(() => p.drain(() => {})).toThrow("destroyed")
2230
- })
2231
-
2232
- test("destroy during drain stops iteration", () => {
2233
- const p = createParser()
2234
- p.push(Buffer.from("abc"))
2235
- let count = 0
2236
- expect(() => {
2237
- p.drain(() => {
2238
- count++
2239
- if (count === 1) p.destroy()
2240
- })
2241
- }).not.toThrow()
2242
- expect(count).toBe(1)
2243
- })
2244
-
2245
- test("read returns null when queue is empty", () => {
2246
- const p = createParser()
2247
- try {
2248
- expect(p.read()).toBeNull()
2249
- } finally {
2250
- p.destroy()
2251
- }
2252
- })
2253
-
2254
- test("read pops events one at a time", () => {
2255
- const p = createParser()
2256
- try {
2257
- p.push(Buffer.from("abc"))
2258
- const e1 = p.read()
2259
- const e2 = p.read()
2260
- const e3 = p.read()
2261
- const e4 = p.read()
2262
- expect(e1).not.toBeNull()
2263
- expect(e2).not.toBeNull()
2264
- expect(e3).not.toBeNull()
2265
- expect(e4).toBeNull()
2266
- expect(snapshotEvent(e1!)).toEqual(k("a"))
2267
- expect(snapshotEvent(e2!)).toEqual(k("b"))
2268
- expect(snapshotEvent(e3!)).toEqual(k("c"))
2269
- } finally {
2270
- p.destroy()
2271
- }
2272
- })
2273
-
2274
- test("overflow flushes incomplete protocols as one unknown response and recovers", () => {
2275
- const longDigits = "1".repeat(40)
2276
- const longOsc = "a".repeat(40)
2277
- const longDcs = "x".repeat(40)
2278
- const cases: Array<[label: string, chunks: ChunkInput[], expected: Snap[]]> = [
2279
- ["CSI", [`\x1b[${longDigits}`], [resp("unknown", `\x1b[${longDigits}`)]],
2280
- ["OSC", [`\x1b]${longOsc}`], [resp("unknown", `\x1b]${longOsc}`)]],
2281
- ["DCS + recovery", [`\x1bP${longDcs}`, "z"], [resp("unknown", `\x1bP${longDcs}`), k("z")]],
2282
- ]
2283
-
2284
- for (const [label, chunks, expected] of cases) {
2285
- expect(snapChunks(chunks, { maxPendingBytes: 16 })).toEqual(expected)
2286
- }
2287
- })
2288
- })
2289
-
2290
- describe("multi-event interleaving", () => {
2291
- table([
2292
- [
2293
- "key + mouse",
2294
- "x\x1b[<64;10;5M",
2295
- [k("x"), sgr("\x1b[<64;10;5M", "scroll", 9, 4, { scroll: { direction: "up", delta: 1 } })],
2296
- ],
2297
- [
2298
- "mouse + key",
2299
- "\x1b[<64;10;5Mx",
2300
- [sgr("\x1b[<64;10;5M", "scroll", 9, 4, { scroll: { direction: "up", delta: 1 } }), k("x")],
2301
- ],
2302
- ["key + focus + key", "a\x1b[Ib", [k("a"), resp("csi", "\x1b[I"), k("b")]],
2303
- ["paste + key", "\x1b[200~hi\x1b[201~z", [paste("hi"), k("z")]],
2304
- ["multiple keys", "abc", [k("a"), k("b"), k("c")]],
2305
- [
2306
- "arrow + text + mouse",
2307
- "\x1b[Ax\x1b[<0;1;1M",
2308
- [k("up", { raw: "\x1b[A" }), k("x"), sgr("\x1b[<0;1;1M", "down", 0, 0)],
2309
- ],
2310
- ])
2311
-
2312
- test("OSC + key + mouse + paste in one push", () => {
2313
- const p = createParser()
2314
- try {
2315
- const input = "\x1b]4;0;#fff\x07a\x1b[<0;1;1M\x1b[200~p\x1b[201~"
2316
- p.push(Buffer.from(input))
2317
- expect(snap(p)).toEqual([
2318
- resp("osc", "\x1b]4;0;#fff\x07"),
2319
- k("a"),
2320
- sgr("\x1b[<0;1;1M", "down", 0, 0),
2321
- paste("p"),
2322
- ])
2323
- } finally {
2324
- p.destroy()
2325
- }
2326
- })
2327
- })
2328
-
2329
- describe("negative and edge cases", () => {
2330
- test("push with empty buffer emits an empty key event", () => {
2331
- const p = createParser()
2332
- try {
2333
- p.push(new Uint8Array(0))
2334
- expect(snap(p)).toEqual([k("")])
2335
- } finally {
2336
- p.destroy()
2337
- }
2338
- })
2339
-
2340
- test("drain with no events does not call callback", () => {
2341
- const p = createParser()
2342
- try {
2343
- let called = false
2344
- p.drain(() => {
2345
- called = true
2346
- })
2347
- expect(called).toBe(false)
2348
- } finally {
2349
- p.destroy()
2350
- }
2351
- })
2352
-
2353
- table([
2354
- ["CSI with unknown final byte produces empty-name key", "\x1b[h", [k("", { raw: "\x1b[h" })]],
2355
- ["ESC followed by punctuation stays one empty-name key", "\x1b!", [k("", { raw: "\x1b!" })]],
2356
- ["ESC followed by N becomes meta+shift+N", "\x1bN", [k("N", { raw: "\x1bN", meta: true, shift: true })]],
2357
- ["malformed SGR mouse falls through as empty-name CSI key", "\x1b[<0M", [k("", { raw: "\x1b[<0M" })]],
2358
- ["bracketed paste end outside paste mode is a CSI response", "\x1b[201~", [resp("csi", "\x1b[201~")]],
2359
- ])
2360
-
2361
- test("partial X10 times out as one unknown response", () => {
2362
- const { parser, clock } = createTimedParser()
2363
- try {
2364
- parser.push(Buffer.from("\x1b[M !"))
2365
- expect(snap(parser)).toEqual([])
2366
- clock.advance(10)
2367
- expect(snap(parser)).toEqual([resp("unknown", "\x1b[M !")])
2368
- } finally {
2369
- parser.destroy()
2370
- }
2371
- })
2372
-
2373
- test("very long paste with partial end marker in every chunk", () => {
2374
- const p = createParser()
2375
- try {
2376
- p.push(Buffer.from("\x1b[200~"))
2377
- for (let i = 0; i < 100; i++) p.push(Buffer.from("\x1b[20"))
2378
- p.push(Buffer.from("\x1b[201~"))
2379
- expect(snap(p)).toEqual([paste("\x1b[20".repeat(100))])
2380
- } finally {
2381
- p.destroy()
2382
- }
2383
- })
2384
- })
2385
-
2386
- describe("timer/clock disagreement race condition", () => {
2387
- test("timeout callback flushes even when now() reports slightly less elapsed time than timeoutMs", () => {
2388
- const inner = new ManualClock()
2389
- let insideTimerCallback = false
2390
-
2391
- // Wraps ManualClock so that now() returns pendingSinceMs + timeoutMs - 1
2392
- // during the timeout callback, simulating runtime behavior where timer
2393
- // scheduling and now() sampling disagree by a small amount.
2394
- const disagreeingClock: Clock = {
2395
- now(): number {
2396
- if (insideTimerCallback) {
2397
- // Report 1ms less than the timeout requires — this is the
2398
- // race condition that kept bytes stuck before the fix.
2399
- return TEST_TIMEOUT_MS - 1
2400
- }
2401
- return inner.now()
2402
- },
2403
- setTimeout(fn: () => void, delayMs: number): TimerHandle {
2404
- return inner.setTimeout(() => {
2405
- insideTimerCallback = true
2406
- try {
2407
- fn()
2408
- } finally {
2409
- insideTimerCallback = false
2410
- }
2411
- }, delayMs)
2412
- },
2413
- clearTimeout(handle: TimerHandle): void {
2414
- inner.clearTimeout(handle)
2415
- },
2416
- setInterval(fn: () => void, delayMs: number): TimerHandle {
2417
- return inner.setInterval(fn, delayMs)
2418
- },
2419
- clearInterval(handle: TimerHandle): void {
2420
- inner.clearInterval(handle)
2421
- },
2422
- }
2423
-
2424
- const parser = new StdinParser({ armTimeouts: true, clock: disagreeingClock, timeoutMs: TEST_TIMEOUT_MS })
2425
- try {
2426
- parser.push(Buffer.from("\x1b"))
2427
- expect(snap(parser)).toEqual([])
2428
-
2429
- // Fire the timer — now() will report timeoutMs - 1 elapsed, but the
2430
- // timeout callback still force-flushes without re-checking elapsed time.
2431
- inner.advance(TEST_TIMEOUT_MS)
2432
-
2433
- expect(snap(parser)).toEqual([k("escape", { raw: "\x1b" })])
2434
- } finally {
2435
- parser.destroy()
2436
- }
2437
- })
2438
- })
2439
- })