buninu-linux 0.15.58 → 0.16.59

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 (228) hide show
  1. package/NOTICE.md +46 -46
  2. package/README.md +22 -15
  3. package/README.zh.md +22 -15
  4. package/initramfs/buninu/CHANGELOG.md +47 -0
  5. package/initramfs/buninu/apps/jsgotty/LICENSES/MIT-jsmdcui.txt +22 -0
  6. package/initramfs/buninu/apps/jsgotty/NOTICE +5 -0
  7. package/initramfs/buninu/apps/jsgotty/README.en.md +21 -0
  8. package/initramfs/buninu/apps/jsgotty/README.md +21 -0
  9. package/initramfs/buninu/apps/jsgotty/client-modes.mjs +117 -0
  10. package/initramfs/buninu/apps/jsgotty/client.mjs +100 -10
  11. package/initramfs/buninu/apps/jsgotty/clipboard-powershell.js +18 -0
  12. package/initramfs/buninu/apps/jsgotty/clipboard.js +148 -0
  13. package/initramfs/buninu/apps/jsgotty/gotty.js +13 -0
  14. package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
  15. package/initramfs/buninu/apps/jsgotty/package.json +1 -1
  16. package/initramfs/buninu/apps/jsgotty/static/css/index.css +14 -1
  17. package/initramfs/buninu/apps/jsgotty/static/js/gotty.js +107 -8
  18. package/initramfs/buninu/apps/jsgotty/static/js/kitty.js +115 -7
  19. package/initramfs/buninu/apps/jsgotty/terminal-modes.js +332 -0
  20. package/initramfs/buninu/apps/jsgotty/tests/client-exit.test.mjs +45 -0
  21. package/initramfs/buninu/apps/jsgotty/tests/client-modes.test.mjs +105 -0
  22. package/initramfs/buninu/apps/jsgotty/tests/clipboard-browser.test.js +81 -0
  23. package/initramfs/buninu/apps/jsgotty/tests/clipboard.test.js +75 -0
  24. package/initramfs/buninu/apps/jsgotty/tests/manual/zoom-collector.cjs +55 -0
  25. package/initramfs/buninu/apps/jsgotty/tests/manual/zoom-debug.js +39 -0
  26. package/initramfs/buninu/apps/jsgotty/tests/manual-touch-diagnostics.js +33 -0
  27. package/initramfs/buninu/apps/jsgotty/tests/manual-touch-diagnostics.mjs +22 -0
  28. package/initramfs/buninu/apps/jsgotty/tests/nested-cli-pty.test.mjs +116 -0
  29. package/initramfs/buninu/apps/jsgotty/tests/reconnect-modes-browser.test.js +144 -0
  30. package/initramfs/buninu/apps/jsgotty/tests/reconnect-modes.md +44 -0
  31. package/initramfs/buninu/apps/jsgotty/tests/reconnect-modes.test.js +166 -0
  32. package/initramfs/buninu/apps/jsgotty/tests/touch-drag-browser.test.js +106 -0
  33. package/initramfs/buninu/apps/jsgotty/tests/wheel-browser.test.js +60 -0
  34. package/initramfs/buninu/apps/jsmdcui/CHANGELOG.md +44 -0
  35. package/initramfs/buninu/apps/jsmdcui/package.json +1 -1
  36. package/initramfs/buninu/apps/jsmdcui/src/highlight/go-regexp.js +83 -0
  37. package/initramfs/buninu/apps/jsmdcui/src/highlight/highlighter.js +49 -62
  38. package/initramfs/buninu/apps/jsmdcui/src/highlight/parser.js +8 -5
  39. package/initramfs/buninu/apps/jsmdcui/tests/go-regexp.test.js +98 -0
  40. package/initramfs/buninu/apps/jsmdcui/tests/heading-ids.test.js +21 -16
  41. package/initramfs/buninu/apps/jsmdcui/tests/highlight-regions.test.js +73 -0
  42. package/initramfs/buninu/apps/jsmdcui/tests/id-collision.test.js +32 -9
  43. package/initramfs/buninu/apps/jsmdcui/tests/markdown-behavior.mjs +9 -0
  44. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/NORMAL-HTML.md +46 -0
  45. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/README.md +85 -0
  46. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/REPORT.md +328 -0
  47. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/compare.mjs +75 -0
  48. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/go.mod +5 -0
  49. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/normal-html-report.json +619 -0
  50. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/normal-html.html +9 -0
  51. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/oracle.go +49 -0
  52. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/report.json +2174 -0
  53. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/PowerShell.txt +39 -0
  54. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ada.txt +25 -0
  55. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/apacheconf.txt +49 -0
  56. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/arduino.txt +38 -0
  57. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/asciidoc.txt +33 -0
  58. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/asm.txt +77 -0
  59. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ats.txt +48 -0
  60. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/awk.txt +28 -0
  61. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/b.txt +27 -0
  62. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/bat.txt +39 -0
  63. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/c.txt +34 -0
  64. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/caddyfile.txt +14 -0
  65. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cake.txt +7 -0
  66. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/clojure.txt +18 -0
  67. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cmake.txt +20 -0
  68. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/coffeescript.txt +32 -0
  69. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/colortest.txt +19 -0
  70. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/conky.txt +12 -0
  71. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cpp.txt +46 -0
  72. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/crontab.txt +16 -0
  73. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/crystal.txt +28 -0
  74. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/csharp.txt +28 -0
  75. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/css.txt +19 -0
  76. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/csx.txt +7 -0
  77. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cuda.txt +37 -0
  78. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cython.txt +22 -0
  79. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/d.txt +52 -0
  80. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/dart.txt +27 -0
  81. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/default.txt +8 -0
  82. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/dockerfile.txt +10 -0
  83. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/dot.txt +15 -0
  84. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/elixir.txt +23 -0
  85. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/elm.txt +20 -0
  86. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/erb.txt +32 -0
  87. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/erlang.txt +23 -0
  88. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/fish.txt +20 -0
  89. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/forth.txt +17 -0
  90. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/fortran.txt +40 -0
  91. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/freebsd-kernel.txt +10 -0
  92. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/fsharp.txt +27 -0
  93. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gdscript.txt +26 -0
  94. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gemini.txt +11 -0
  95. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gentoo-ebuild.txt +28 -0
  96. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gentoo-etc-portage.txt +14 -0
  97. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/git-commit.txt +19 -0
  98. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/git-config.txt +11 -0
  99. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/git-rebase-todo.txt +10 -0
  100. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gleam.txt +44 -0
  101. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/glsl.txt +18 -0
  102. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gnuplot.txt +11 -0
  103. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/go.txt +33 -0
  104. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/godoc.txt +9 -0
  105. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/golo.txt +50 -0
  106. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gomod.txt +16 -0
  107. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/graphql.txt +20 -0
  108. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/groff.txt +18 -0
  109. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/groovy.txt +42 -0
  110. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/haml.txt +16 -0
  111. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/hare.txt +31 -0
  112. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/haskell.txt +26 -0
  113. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/hc.txt +28 -0
  114. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/html.txt +26 -0
  115. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/html4.txt +20 -0
  116. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/html5.txt +20 -0
  117. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ini.txt +16 -0
  118. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/inputrc.txt +14 -0
  119. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/java.txt +18 -0
  120. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/javascript.txt +41 -0
  121. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/jinja2.txt +13 -0
  122. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/json.txt +21 -0
  123. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/jsonnet.txt +28 -0
  124. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/julia.txt +24 -0
  125. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/justfile.txt +26 -0
  126. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/keymap.txt +15 -0
  127. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/kickstart.txt +16 -0
  128. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/kotlin.txt +30 -0
  129. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/kvlang.txt +37 -0
  130. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ledger.txt +13 -0
  131. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/lfe.txt +17 -0
  132. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/lilypond.txt +15 -0
  133. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/lisp.txt +17 -0
  134. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/log.txt +45 -0
  135. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/lua.txt +48 -0
  136. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/mail.txt +17 -0
  137. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/makefile.txt +25 -0
  138. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/man.txt +12 -0
  139. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/markdown.txt +21 -0
  140. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/mc.txt +11 -0
  141. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/meson.txt +29 -0
  142. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/micro.txt +25 -0
  143. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/mpdconf.txt +13 -0
  144. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/msbuild.txt +7 -0
  145. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nanorc.txt +16 -0
  146. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nftables.txt +22 -0
  147. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nginx.txt +17 -0
  148. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nim.txt +23 -0
  149. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nix.txt +15 -0
  150. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nu.txt +92 -0
  151. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/objc.txt +36 -0
  152. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ocaml.txt +25 -0
  153. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/octave.txt +35 -0
  154. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/odin.txt +35 -0
  155. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/pascal.txt +22 -0
  156. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/patch.txt +13 -0
  157. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/peg.txt +16 -0
  158. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/perl.txt +27 -0
  159. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/php.txt +44 -0
  160. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/pkg-config.txt +12 -0
  161. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/po.txt +12 -0
  162. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/pony.txt +29 -0
  163. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/pov.txt +17 -0
  164. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/privoxy-action.txt +14 -0
  165. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/privoxy-config.txt +10 -0
  166. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/privoxy-filter.txt +12 -0
  167. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/proto.txt +22 -0
  168. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/prql.txt +42 -0
  169. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/puppet.txt +22 -0
  170. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/python2.txt +24 -0
  171. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/python3.txt +28 -0
  172. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/r.txt +16 -0
  173. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/raku.txt +19 -0
  174. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/reST.txt +18 -0
  175. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/renpy.txt +11 -0
  176. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/rpmspec.txt +31 -0
  177. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ruby.txt +34 -0
  178. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/rust.txt +27 -0
  179. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sage.txt +24 -0
  180. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/scad.txt +23 -0
  181. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/scala.txt +16 -0
  182. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sed.txt +12 -0
  183. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sh.txt +20 -0
  184. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sls.txt +15 -0
  185. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/smalltalk.txt +28 -0
  186. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/solidity.txt +26 -0
  187. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sql.txt +35 -0
  188. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/stata.txt +32 -0
  189. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/svelte.txt +10 -0
  190. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/swift.txt +53 -0
  191. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/systemd.txt +15 -0
  192. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/tcl.txt +17 -0
  193. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/terraform.txt +28 -0
  194. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/tex.txt +15 -0
  195. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/toml.txt +31 -0
  196. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/twig.txt +29 -0
  197. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/typescript.txt +32 -0
  198. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/v.txt +47 -0
  199. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/vala.txt +22 -0
  200. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/verilog.txt +37 -0
  201. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/vhdl.txt +37 -0
  202. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/vi.txt +17 -0
  203. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/vue.txt +29 -0
  204. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/xml.txt +14 -0
  205. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/xresources.txt +14 -0
  206. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/yaml.txt +19 -0
  207. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/yum.txt +12 -0
  208. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/zig.txt +24 -0
  209. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/zscript.txt +31 -0
  210. package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/zsh.txt +21 -0
  211. package/initramfs/buninu/package.json +1 -1
  212. package/initramfs/lib/bunterm/images.js +5 -1
  213. package/initramfs/lib/bunterm/raw-protocol.js +82 -4
  214. package/initramfs/lib/bunterm/renderer.js +35 -16
  215. package/initramfs/lib/bunterm/terminal.js +11 -1
  216. package/initramfs/lib/fbdev.js +11 -5
  217. package/initramfs/lib/mapped-frame.js +28 -0
  218. package/initramfs/lib/raw-image.js +29 -5
  219. package/package.json +1 -1
  220. package/scripts/arch.js +5 -5
  221. package/scripts/arch.sh +5 -5
  222. package/scripts/npm-packages.md +11 -10
  223. package/test/bunterm-image-bytes.test.js +15 -0
  224. package/test/bunterm-mapped-lifetime.test.js +147 -0
  225. package/test/bunterm-raw-protocol.test.js +20 -0
  226. package/test/bunterm-renderer.test.js +9 -0
  227. package/test/fbdev-sync.test.js +9 -0
  228. package/test/fetch-npm.test.js +2 -2
@@ -561,6 +561,26 @@
561
561
  state.renderBound = state.renderDisposables.length > 0;
562
562
  }
563
563
 
564
+ // Touch gestures report mouse input without DOM mousedown: xterm's DOM
565
+ // handler focuses its hidden textarea, which can zoom Android WebView.
566
+ function reportTouchMouse(type, point, button = 0) {
567
+ const core = terminal?.__gottyXterm?._core;
568
+ if (!core?.coreMouseService?.areMouseEventsActive) return;
569
+ const screen = core.screenElement;
570
+ if (!screen) return;
571
+ const rect = screen.getBoundingClientRect();
572
+ const coords = core._mouseService.getMouseReportCoords({
573
+ clientX: Math.max(rect.left, Math.min(rect.right - 1, point.clientX)),
574
+ clientY: Math.max(rect.top, Math.min(rect.bottom - 1, point.clientY)),
575
+ }, screen);
576
+ if (!coords) return;
577
+ core.coreMouseService.triggerMouseEvent({
578
+ ...coords, button,
579
+ action: type === "mousedown" ? 1 : type === "mousemove" ? 32 : 0,
580
+ ctrl: false, alt: false, shift: false,
581
+ });
582
+ }
583
+
564
584
  function dispatchSyntheticWheel(target, touch, deltaX, deltaY) {
565
585
  if (!target) {
566
586
  return;
@@ -597,22 +617,69 @@
597
617
  return screen || viewport || xterm || terminal;
598
618
  };
599
619
 
620
+ let lastTap = null;
621
+ let drag = null;
622
+ const mouse = (type, point) => reportTouchMouse(type, point);
623
+ const releaseDrag = () => {
624
+ if (drag) {
625
+ const point = drag;
626
+ drag = null;
627
+ mouse("mouseup", point);
628
+ }
629
+ };
630
+ const cancelGesture = () => {
631
+ releaseDrag();
632
+ lastTap = null;
633
+ state.touchScroll = null;
634
+ };
635
+ state.cancelTouchGesture = cancelGesture;
636
+ bindTarget.addEventListener("contextmenu", (event) => {
637
+ if (drag) { event.preventDefault(); event.stopImmediatePropagation(); }
638
+ }, true);
639
+ window.addEventListener("blur", cancelGesture);
640
+ document.addEventListener("visibilitychange", () => {
641
+ if (document.hidden) cancelGesture();
642
+ });
643
+
600
644
  bindTarget.addEventListener("touchstart", (event) => {
601
645
  if (event.touches.length !== 1) {
602
- state.touchScroll = null;
646
+ cancelGesture();
603
647
  return;
604
648
  }
605
649
  const touch = event.touches[0];
650
+ const reporting = terminal.__gottyXterm?._core?.coreMouseService?.areMouseEventsActive;
651
+ if (reporting && window.touchDoubleTapDrag !== false && lastTap &&
652
+ performance.now() - lastTap.time < 350 &&
653
+ Math.hypot(touch.clientX - lastTap.x, touch.clientY - lastTap.y) < 24) {
654
+ lastTap = null;
655
+ state.touchScroll = null;
656
+ drag = { identifier: touch.identifier, clientX: touch.clientX, clientY: touch.clientY };
657
+ // Cancel compatibility mouse events: this is the second press, not a third click.
658
+ event.preventDefault();
659
+ mouse("mousedown", drag);
660
+ return;
661
+ }
662
+ lastTap = null;
606
663
  state.touchScroll = {
664
+ startX: touch.clientX, startY: touch.clientY, startTime: performance.now(), moved: false,
607
665
  identifier: touch.identifier,
608
666
  lastX: touch.clientX,
609
667
  lastY: touch.clientY,
610
668
  residualX: 0,
611
669
  residualY: 0
612
670
  };
613
- }, { passive: true });
671
+ }, { passive: false });
614
672
 
615
673
  bindTarget.addEventListener("touchmove", (event) => {
674
+ if (drag) {
675
+ const touch = Array.from(event.touches).find(t => t.identifier === drag.identifier);
676
+ if (!touch || event.touches.length !== 1) { cancelGesture(); return; }
677
+ event.preventDefault();
678
+ drag.clientX = touch.clientX;
679
+ drag.clientY = touch.clientY;
680
+ mouse("mousemove", drag);
681
+ return;
682
+ }
616
683
  if (!state.touchScroll) {
617
684
  return;
618
685
  }
@@ -621,6 +688,8 @@
621
688
  return;
622
689
  }
623
690
 
691
+ if (Math.hypot(touch.clientX - state.touchScroll.startX,
692
+ touch.clientY - state.touchScroll.startY) > 8) state.touchScroll.moved = true;
624
693
  const deltaX = state.touchScroll.lastX - touch.clientX;
625
694
  const deltaY = state.touchScroll.lastY - touch.clientY;
626
695
  state.touchScroll.lastX = touch.clientX;
@@ -653,11 +722,20 @@
653
722
  state.touchScroll.residualY -= ticksY * threshold * stepY;
654
723
  }, { passive: false });
655
724
 
656
- const clearTouchScroll = () => {
725
+ bindTarget.addEventListener("touchend", (event) => {
726
+ if (drag) {
727
+ event.preventDefault();
728
+ releaseDrag();
729
+ } else {
730
+ const scroll = state.touchScroll;
731
+ if (scroll && !scroll.moved && event.touches.length === 0 &&
732
+ performance.now() - scroll.startTime < 350) {
733
+ lastTap = { x: scroll.lastX, y: scroll.lastY, time: performance.now() };
734
+ }
735
+ }
657
736
  state.touchScroll = null;
658
- };
659
- bindTarget.addEventListener("touchend", clearTouchScroll, { passive: true });
660
- bindTarget.addEventListener("touchcancel", clearTouchScroll, { passive: true });
737
+ }, { passive: false });
738
+ bindTarget.addEventListener("touchcancel", cancelGesture, { passive: true });
661
739
  state.touchBound = true;
662
740
  }
663
741
 
@@ -676,9 +754,25 @@
676
754
  removeButton.textContent = "Remove image";
677
755
  menu.appendChild(removeButton);
678
756
 
757
+ const separator = document.createElement("hr");
758
+ const rightButton = document.createElement("button");
759
+ rightButton.type = "button";
760
+ rightButton.className = "kitty-context-menu__item";
761
+ rightButton.textContent = "Right click";
762
+ menu.append(separator, rightButton);
763
+ rightButton.addEventListener("click", (event) => {
764
+ event.preventDefault();
765
+ event.stopPropagation();
766
+ const point = state.contextMenu?.point;
767
+ hideContextMenu();
768
+ if (!point || !terminal.__gottyXterm?._core?.coreMouseService?.areMouseEventsActive) return;
769
+ reportTouchMouse("mousedown", { clientX: point.x, clientY: point.y }, 2);
770
+ reportTouchMouse("mouseup", { clientX: point.x, clientY: point.y }, 2);
771
+ });
772
+
679
773
  state.contextMenu = {
680
774
  root: menu,
681
- removeButton,
775
+ removeButton, rightButton, separator,
682
776
  targetKey: null
683
777
  };
684
778
 
@@ -722,6 +816,9 @@
722
816
  function showContextMenu(x, y, key) {
723
817
  const menu = ensureContextMenu();
724
818
  menu.targetKey = key;
819
+ menu.point = { x, y };
820
+ const reporting = !!terminal.__gottyXterm?._core?.coreMouseService?.areMouseEventsActive;
821
+ menu.rightButton.hidden = menu.separator.hidden = !reporting;
725
822
  menu.root.hidden = false;
726
823
 
727
824
  const margin = 10;
@@ -1004,6 +1101,15 @@
1004
1101
  return false;
1005
1102
  }
1006
1103
 
1104
+ const watchedSockets = new WeakSet();
1105
+ function watchTouchSocket(socket) {
1106
+ if (watchedSockets.has(socket)) return;
1107
+ watchedSockets.add(socket);
1108
+ socket.addEventListener("close", () => {
1109
+ if (state.socket === socket) state.cancelTouchGesture?.();
1110
+ });
1111
+ }
1112
+
1007
1113
  const nativeSend = WebSocket.prototype.send;
1008
1114
  const nativeOnOpen = WebSocket.prototype.addEventListener;
1009
1115
  WebSocket.prototype.addEventListener = function (type, listener, options) {
@@ -1012,6 +1118,7 @@
1012
1118
  }
1013
1119
  const wrapped = function (event) {
1014
1120
  state.socket = this;
1121
+ watchTouchSocket(this);
1015
1122
  inspectTerminalOutput(event.data);
1016
1123
  if (typeof event.data === "string" && event.data[0] === MSG_KITTY) {
1017
1124
  if (handleKittyMessage(event.data.slice(1))) {
@@ -1058,6 +1165,7 @@
1058
1165
  }
1059
1166
  const wrapped = function (event) {
1060
1167
  state.socket = this;
1168
+ watchTouchSocket(this);
1061
1169
  inspectTerminalOutput(event.data);
1062
1170
  if (typeof event.data === "string" && event.data[0] === MSG_KITTY) {
1063
1171
  if (handleKittyMessage(event.data.slice(1))) {
@@ -0,0 +1,332 @@
1
+ "use strict";
2
+
3
+ // All flags defined by the kitty keyboard protocol. Kept unmasked so a CLI
4
+ // client on a real kitty terminal can restore them; the browser masks its own.
5
+ const KITTY_KEYBOARD_FLAGS = 0x1f;
6
+ const TRACKED_DEC_FLAGS = {
7
+ 1: "applicationCursorKeys",
8
+ 6: "origin",
9
+ 7: "wraparound",
10
+ 45: "reverseWraparound",
11
+ 66: "applicationKeypad",
12
+ 1004: "sendFocus",
13
+ 2004: "bracketedPasteMode",
14
+ };
15
+ const MOUSE_PROTOCOLS = { 9: "X10", 1000: "VT200", 1002: "DRAG", 1003: "ANY" };
16
+ const MOUSE_ENCODINGS = { 1006: "SGR", 1016: "SGR_PIXELS" };
17
+
18
+ // Mirrors the PTY's input/protocol modes so a resumed client can be brought
19
+ // back in sync. Mode semantics follow xterm.js, which renders the output.
20
+ class TerminalModeTracker {
21
+ constructor() {
22
+ this.reset();
23
+ this._state = "text";
24
+ this._seq = "";
25
+ }
26
+
27
+ reset() {
28
+ this.dec = {
29
+ applicationCursorKeys: false,
30
+ applicationKeypad: false,
31
+ bracketedPasteMode: false,
32
+ origin: false,
33
+ reverseWraparound: false,
34
+ sendFocus: false,
35
+ wraparound: true,
36
+ };
37
+ this.cursorHidden = false;
38
+ this.mouse = "NONE";
39
+ this.encoding = "DEFAULT";
40
+ this.alternate = false;
41
+ this.kitty = { normal: { flags: 0, stack: [] }, alternate: { flags: 0, stack: [] } };
42
+ }
43
+
44
+ snapshot() {
45
+ return {
46
+ dec: { ...this.dec },
47
+ cursorHidden: this.cursorHidden,
48
+ mouse: this.mouse,
49
+ encoding: this.encoding,
50
+ alternate: this.alternate,
51
+ kitty: {
52
+ normal: { flags: this.kitty.normal.flags, stack: [...this.kitty.normal.stack] },
53
+ alternate: { flags: this.kitty.alternate.flags, stack: [...this.kitty.alternate.stack] },
54
+ },
55
+ };
56
+ }
57
+
58
+ consume(buffer) {
59
+ // Control sequences are ASCII, so bytes can be scanned without UTF-8 decoding.
60
+ for (let i = 0; i < buffer.length; i += 1) {
61
+ const byte = buffer[i];
62
+ switch (this._state) {
63
+ case "text":
64
+ if (byte === 0x1b) {
65
+ this._state = "esc";
66
+ }
67
+ break;
68
+ case "esc":
69
+ if (byte === 0x5b) {
70
+ this._state = "csi";
71
+ this._seq = "";
72
+ } else if (byte === 0x5d || byte === 0x50 || byte === 0x5f || byte === 0x5e || byte === 0x58) {
73
+ this._state = "string";
74
+ this._osc = byte === 0x5d ? "" : null;
75
+ } else if (byte === 0x1b) {
76
+ this._state = "esc";
77
+ } else if (byte >= 0x20 && byte <= 0x2f) {
78
+ this._state = "esc-intermediate";
79
+ } else {
80
+ this._state = "text";
81
+ this.handleEsc(byte);
82
+ }
83
+ break;
84
+ case "esc-intermediate":
85
+ if (byte < 0x20 || byte > 0x2f) {
86
+ this._state = byte === 0x1b ? "esc" : "text";
87
+ }
88
+ break;
89
+ case "csi":
90
+ if (byte >= 0x40 && byte <= 0x7e) {
91
+ this._state = "text";
92
+ this.handleCsi(this._seq, String.fromCharCode(byte));
93
+ } else if (byte === 0x1b) {
94
+ this._state = "esc";
95
+ } else if (byte === 0x18 || byte === 0x1a || this._seq.length >= 64) {
96
+ this._state = "text";
97
+ } else if (byte >= 0x20) {
98
+ this._seq += String.fromCharCode(byte);
99
+ }
100
+ break;
101
+ case "string":
102
+ if (byte === 0x07 || byte === 0x18 || byte === 0x1a) {
103
+ if (byte === 0x07) this.handleOsc();
104
+ this._state = "text";
105
+ } else if (byte === 0x1b) {
106
+ this._state = "string-esc";
107
+ } else if (this._osc != null) {
108
+ this._osc = this._osc.length < 256 ? this._osc + String.fromCharCode(byte) : null;
109
+ }
110
+ break;
111
+ case "string-esc":
112
+ if (byte === 0x5c) this.handleOsc();
113
+ else this._osc = null;
114
+ this._state = byte === 0x1b ? "string-esc" : byte === 0x5c ? "text" : "string";
115
+ break;
116
+ }
117
+ }
118
+ }
119
+
120
+ handleOsc() {
121
+ const match = /^777;jsgotty-kitty-v1;(normal|alternate);(\d{1,2});(\d{1,2}(?:,\d{1,2})*)?$/.exec(this._osc || '');
122
+ if (!match) return;
123
+ const flags = +match[2], stack = match[3] ? match[3].split(',').map(Number) : [];
124
+ if (flags > 31 || stack.length > 32 || stack.some(n=>n > 31)) return;
125
+ this.kitty[match[1]] = {flags, stack};
126
+ }
127
+
128
+ handleEsc(byte) {
129
+ if (byte === 0x3d) {
130
+ this.dec.applicationKeypad = true;
131
+ } else if (byte === 0x3e) {
132
+ this.dec.applicationKeypad = false;
133
+ } else if (byte === 0x63) {
134
+ this.reset();
135
+ }
136
+ }
137
+
138
+ handleCsi(seq, final) {
139
+ const prefix = /^[<=>?]/.test(seq) ? seq[0] : "";
140
+ const body = prefix ? seq.slice(1) : seq;
141
+ if (!/^[0-9;:]*$/.test(body)) {
142
+ if (body === "!" && final === "p") {
143
+ this.softReset();
144
+ }
145
+ return;
146
+ }
147
+ const params = body === "" ? [] : body.split(";").map((part) => {
148
+ const value = Number.parseInt(part, 10);
149
+ return Number.isFinite(value) ? value : 0;
150
+ });
151
+ if (prefix === "?" && (final === "h" || final === "l")) {
152
+ for (const mode of params) {
153
+ this.setPrivateMode(mode, final === "h");
154
+ }
155
+ } else if (final === "u" && prefix !== "?") {
156
+ this.handleKittyKeyboard(prefix, params);
157
+ }
158
+ }
159
+
160
+ softReset() {
161
+ // DECSTR keeps the active screen and mouse reporting, like xterm.js.
162
+ const { alternate, mouse, encoding } = this;
163
+ this.reset();
164
+ Object.assign(this, { alternate, mouse, encoding });
165
+ }
166
+
167
+ setPrivateMode(mode, enabled) {
168
+ if (TRACKED_DEC_FLAGS[mode]) {
169
+ this.dec[TRACKED_DEC_FLAGS[mode]] = enabled;
170
+ } else if (MOUSE_PROTOCOLS[mode]) {
171
+ this.mouse = enabled ? MOUSE_PROTOCOLS[mode] : "NONE";
172
+ } else if (MOUSE_ENCODINGS[mode]) {
173
+ this.encoding = enabled ? MOUSE_ENCODINGS[mode] : "DEFAULT";
174
+ } else if (mode === 25) {
175
+ this.cursorHidden = !enabled;
176
+ } else if (mode === 47 || mode === 1047 || mode === 1049) {
177
+ this.alternate = enabled;
178
+ }
179
+ }
180
+
181
+ handleKittyKeyboard(prefix, params) {
182
+ const state = this.kitty[this.alternate ? "alternate" : "normal"];
183
+ const value = (index, fallback = 0) => (Number.isInteger(params[index]) ? params[index] : fallback);
184
+ if (prefix === ">") {
185
+ state.stack.push(state.flags);
186
+ if (state.stack.length > 32) {
187
+ state.stack.shift();
188
+ }
189
+ state.flags = value(0) & KITTY_KEYBOARD_FLAGS;
190
+ } else if (prefix === "<") {
191
+ let count = Math.min(value(0) || 1, 33);
192
+ while (count--) {
193
+ state.flags = state.stack.pop() ?? 0;
194
+ }
195
+ } else if (prefix === "=") {
196
+ const flags = value(0) & KITTY_KEYBOARD_FLAGS;
197
+ const mode = value(1) || 1;
198
+ if (mode === 1) {
199
+ state.flags = flags;
200
+ } else if (mode === 2) {
201
+ state.flags |= flags;
202
+ } else if (mode === 3) {
203
+ state.flags &= ~flags;
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ const DEFAULT_MODES = new TerminalModeTracker().snapshot();
210
+ const MOUSE_PROTOCOL_MODES = { X10: 9, VT200: 1000, DRAG: 1002, ANY: 1003 };
211
+ const MOUSE_ENCODING_MODES = { SGR: 1006, SGR_PIXELS: 1016 };
212
+
213
+ function sameKitty(a, b) {
214
+ return a.flags === b.flags && a.stack.length === b.stack.length &&
215
+ a.stack.every((flags, index) => flags === b.stack[index]);
216
+ }
217
+
218
+ // Work only on the observed stack; never pop an arbitrary number of entries.
219
+ // With a terminal that cannot report its stack, [] denotes the unseen baseline.
220
+ function kittyTransition(from, to) {
221
+ if (to.flags == null) return "";
222
+ let common = 0;
223
+ while (common < from.stack.length && common < to.stack.length &&
224
+ from.stack[common] === to.stack[common]) common++;
225
+ let sequence = "";
226
+ let flags = from.flags;
227
+ const pops = from.stack.length - common;
228
+ if (pops) {
229
+ sequence += `\x1b[<${pops}u`;
230
+ flags = from.stack[common];
231
+ }
232
+ const first = to.stack[common] ?? to.flags;
233
+ if (flags !== first) sequence += `\x1b[=${first};1u`;
234
+ for (let i = common; i < to.stack.length; i++)
235
+ sequence += `\x1b[>${to.stack[i + 1] ?? to.flags}u`;
236
+ return sequence;
237
+ }
238
+
239
+ // Escape sequence that moves a terminal known to be in `from` to `to`, emitting
240
+ // only differences: DECOM homes the cursor and 1049 overwrites the saved cursor.
241
+ function modeTransition(from, to = DEFAULT_MODES, { normalizeMouse = false, kittyRestore = false } = {}) {
242
+ let sequence = "";
243
+ const fromScreen = from.alternate ? "alternate" : "normal";
244
+ const toScreen = to.alternate ? "alternate" : "normal";
245
+ // Kitty keyboard stacks are per screen, so each is fixed while it is active.
246
+ if (from.alternate != null && !sameKitty(from.kitty[fromScreen], to.kitty[fromScreen])) {
247
+ sequence += kittyTransition(from.kitty[fromScreen], to.kitty[fromScreen]);
248
+ }
249
+ if (to.alternate != null && from.alternate !== to.alternate) {
250
+ sequence += `\x1b[?1049${to.alternate ? "h" : "l"}`;
251
+ if (!sameKitty(from.kitty[toScreen], to.kitty[toScreen])) {
252
+ sequence += kittyTransition(from.kitty[toScreen], to.kitty[toScreen]);
253
+ }
254
+ }
255
+ const inactive = toScreen === "normal" ? "alternate" : "normal";
256
+ // Switching screens just to edit a hidden stack destroys alternate contents.
257
+ // Only emit this extension after the actual terminal advertised support.
258
+ const hidden = to.kitty[inactive];
259
+ if (kittyRestore && from.alternate != null && to.alternate != null && fromScreen === toScreen &&
260
+ !sameKitty(from.kitty[inactive], hidden) && hidden.flags != null && hidden.stack.every(Number.isInteger)) {
261
+ sequence += `\x1b]777;jsgotty-kitty-v1;${inactive};${hidden.flags};${hidden.stack.join(',')}\x07`;
262
+ }
263
+ for (const [mode, name] of Object.entries(TRACKED_DEC_FLAGS)) {
264
+ if (to.dec[name] == null || from.dec[name] === to.dec[name]) {
265
+ continue;
266
+ }
267
+ if (name === "applicationKeypad") {
268
+ sequence += to.dec[name] ? "\x1b=" : "\x1b>"; // more widely supported than ?66
269
+ } else {
270
+ sequence += `\x1b[?${mode}${to.dec[name] ? "h" : "l"}`;
271
+ }
272
+ }
273
+ if (to.cursorHidden != null && from.cursorHidden !== to.cursorHidden) {
274
+ sequence += `\x1b[?25${to.cursorHidden ? "l" : "h"}`;
275
+ }
276
+ // Raw query results preserve independent switches on native terminals. Reset
277
+ // first, then enable in priority order so exclusive terminals also end in the
278
+ // intended effective mode. Unanswered switches are never guessed.
279
+ for (const [name, bitsName, table] of [
280
+ ['mouse', 'mouseModeBits', MOUSE_PROTOCOL_MODES],
281
+ ['encoding', 'mouseEncodingBits', MOUSE_ENCODING_MODES],
282
+ ]) {
283
+ const bits = to[bitsName];
284
+ if (bits) {
285
+ for (const mode of Object.values(table))
286
+ if (bits[mode] === false) sequence += `\x1b[?${mode}l`;
287
+ for (const mode of Object.values(table))
288
+ if (bits[mode] === true) sequence += `\x1b[?${mode}h`;
289
+ } else if (to[name] != null && (normalizeMouse || from[name] !== to[name])) {
290
+ for (const mode of Object.values(table)) sequence += `\x1b[?${mode}l`;
291
+ if (table[to[name]]) sequence += `\x1b[?${table[to[name]]}h`;
292
+ }
293
+ }
294
+ return sequence;
295
+ }
296
+
297
+ function validateSnapshot(value) {
298
+ if (!value || !value.dec || !value.kitty) throw Error("Invalid terminal modes");
299
+ const result = new TerminalModeTracker().snapshot();
300
+ for (const key of Object.values(TRACKED_DEC_FLAGS)) {
301
+ if (typeof value.dec[key] !== 'boolean' && value.dec[key] !== null) throw Error("Invalid DEC mode");
302
+ result.dec[key] = value.dec[key];
303
+ }
304
+ for (const key of ['alternate', 'cursorHidden']) {
305
+ if (typeof value[key] !== 'boolean' && value[key] !== null) throw Error("Invalid screen mode");
306
+ result[key] = value[key];
307
+ }
308
+ for (const [key, choices] of [['mouse', ['NONE', ...Object.values(MOUSE_PROTOCOLS)]], ['encoding', ['DEFAULT', ...Object.values(MOUSE_ENCODINGS)]]]) {
309
+ if (value[key] !== null && !choices.includes(value[key])) throw Error("Invalid mouse mode");
310
+ result[key] = value[key];
311
+ }
312
+ for (const [key, table] of [['mouseModeBits', MOUSE_PROTOCOLS], ['mouseEncodingBits', MOUSE_ENCODINGS]]) {
313
+ if (value[key] === undefined) continue;
314
+ if (!value[key] || typeof value[key] !== 'object') throw Error('Invalid mouse bits');
315
+ result[key] = {};
316
+ for (const mode of Object.keys(table)) {
317
+ const bit = value[key][mode];
318
+ if (bit !== null && typeof bit !== 'boolean') throw Error('Invalid mouse bit');
319
+ result[key][mode] = bit;
320
+ }
321
+ }
322
+ for (const key of ['normal', 'alternate']) {
323
+ const state = value.kitty[key];
324
+ const flag = n => Number.isInteger(n) && n >= 0 && n <= KITTY_KEYBOARD_FLAGS;
325
+ if (!state || (state.flags !== null && !flag(state.flags)) || !Array.isArray(state.stack) ||
326
+ state.stack.length > 32 || !state.stack.every(n => n === null || flag(n))) throw Error("Invalid Kitty state");
327
+ result.kitty[key] = {flags: state.flags, stack: [...state.stack]};
328
+ }
329
+ return result;
330
+ }
331
+
332
+ module.exports = { TerminalModeTracker, modeTransition, validateSnapshot, TRACKED_DEC_FLAGS, MOUSE_PROTOCOLS, MOUSE_ENCODINGS };
@@ -0,0 +1,45 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import vm from 'node:vm';
5
+ const source=fs.readFileSync(new URL('../client.mjs',import.meta.url),'utf8');
6
+ const methods=source.slice(source.indexOf(' let exiting = false;'),source.indexOf(' const stopViewerInput ='));
7
+ function fixture(){
8
+ const calls=[],callbacks=[];
9
+ const handlers=vm.runInNewContext(`(()=>{${methods};return {exitClient,handleViewerInput};})()`,{
10
+ cleanup:()=>calls.push('cleanup'),reportDisconnected:()=>calls.push('report'),
11
+ ws:{close:()=>calls.push('close')},terminalInput:b=>b,
12
+ process:{stdout:{write:(s,done)=>callbacks.push(done)},exit:code=>calls.push(code)},
13
+ });
14
+ return {calls,callbacks,...handlers};
15
+ }
16
+ for(const [input,code] of [[[3,4],130],[[4,3],0],[[17,3],0]]){
17
+ test(`viewer exit handles first key only: ${input}`,()=>{
18
+ const f=fixture();f.handleViewerInput(Buffer.from(input));
19
+ assert.deepEqual(f.calls,['cleanup','report','close']);assert.equal(f.callbacks.length,1);
20
+ assert.ok(!f.calls.includes(code),'does not exit before output flush');
21
+ f.callbacks[0]();assert.equal(f.calls.at(-1),code);
22
+ });
23
+ }
24
+ test('concurrent exit requests queue only one flush and retain first exit code',()=>{
25
+ const f=fixture();f.exitClient(130);f.exitClient(143);f.exitClient(0);
26
+ assert.equal(f.callbacks.length,1);assert.deepEqual(f.calls,['cleanup','report','close']);
27
+ f.callbacks[0]();assert.equal(f.calls.at(-1),130);
28
+ });
29
+ for (const [key, code] of [[99,130],[100,0],[113,0]]) {
30
+ for (const sequence of ['\x1b['+key+';5u', '\x1b['+key+';5:1u', '\x1b['+key+';197u', '\x1b[27;5;'+key+'~']) {
31
+ test('viewer encoded Ctrl key '+JSON.stringify(sequence)+' split',()=>{
32
+ const f=fixture();
33
+ for(const byte of Buffer.from(sequence)) f.handleViewerInput(Buffer.from([byte]));
34
+ assert.equal(f.callbacks.length,1);f.callbacks[0]();assert.equal(f.calls.at(-1),code);
35
+ });
36
+ }
37
+ }
38
+ test('viewer ignores release and other modifiers before exit press',()=>{
39
+ const f=fixture();
40
+ for(const sequence of ['\x1b[99;5:3u','\x1b[100;1u','\x1b[113;7u','\x1b[<0;99;100M'])
41
+ f.handleViewerInput(Buffer.from(sequence));
42
+ assert.equal(f.callbacks.length,0);
43
+ f.handleViewerInput(Buffer.from('\x1b[113;5:2u'));
44
+ assert.equal(f.callbacks.length,1);
45
+ });
@@ -0,0 +1,105 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {ModeProbe,captureTerminalModes} from '../client-modes.mjs';
4
+ import modes from '../terminal-modes.js';
5
+ import {PassThrough} from 'node:stream';
6
+ const {TerminalModeTracker}=modes;
7
+ test('private reply split at every byte preserves typing and both Kitty stacks',()=>{
8
+ const p=new ModeProbe('a'.repeat(32));const t=new TerminalModeTracker();
9
+ t.consume(Buffer.from('\x1b[>1u\x1b[?1049h\x1b[>11u'));
10
+ const reply=p.privatePrefix+Buffer.from(JSON.stringify(t.snapshot())).toString('base64')+'\x07';
11
+ const out=[];for(const c of Buffer.from('before'+reply+'after'))out.push(p.consume(Buffer.from([c])));
12
+ assert.equal(Buffer.concat(out).toString(),'beforeafter');assert.equal(p.complete,true);assert.deepEqual(p.state,t.snapshot());
13
+ assert.equal(p.consume('\x1b').toString(),'\x1b','Escape is immediate after successful negotiation');
14
+ });
15
+ test('standard replies are consumed once; later application queries pass unchanged',()=>{
16
+ const p=new ModeProbe();p.standardQuery();
17
+ const replies='\x1b[?1049;2$y\x1b[?1002;1$y\x1b[?7;0$y\x1b[?11u';
18
+ assert.equal(p.consume(replies).length,0);assert.equal(p.state.mouse,'DRAG');assert.equal(p.state.alternate,false);
19
+ assert.equal(p.state.kitty.normal.flags,11);assert.equal(p.state.dec.wraparound,null);
20
+ assert.equal(p.consume(replies).toString(),replies);
21
+ });
22
+ test('no reply does not mean defaults, nor intercept application replies forever',()=>{
23
+ const p=new ModeProbe();p.standardQuery();p.replyDeadline=1;
24
+ const reply='\x1b[?1049;1$y\x1b[?11u';assert.equal(p.consume(reply).toString(),reply);
25
+ assert.equal(p.state.alternate,null);assert.equal(p.state.kitty.normal.flags,null);
26
+ });
27
+ test('foreign nonce, keys, mouse and a large paste are transparent',()=>{
28
+ const p=new ModeProbe();const data=Buffer.from('\x1b]777;jsgotty-modes;other;ignored\x07\x1b[<64;3;4M'+ '漢字'.repeat(200000));
29
+ assert.deepEqual(p.consume(data),data);
30
+ });
31
+ test('capture falls back after silence and restores stdin raw state',async()=>{
32
+ const input=new PassThrough();input.isTTY=true;input.isRaw=false;input.setRawMode=v=>{input.isRaw=v;};
33
+ const output=new PassThrough();output.isTTY=true;const sent=[];output.on('data',b=>sent.push(b.toString()));
34
+ const result=await captureTerminalModes(input,output,{timeout:30});
35
+ assert.equal(input.isRaw,false);assert.equal(result.state.mouse,null);assert.equal(sent.length,2);
36
+ assert.match(sent[0],/jsgotty-modes/);assert.match(sent[1],/\?1049\$p/);
37
+ });
38
+
39
+ for (const unsupportedFirst of [false,true]) {
40
+ test(`unsupported mouse modes count as inactive replies (${unsupportedFirst?'first':'last'})`,()=>{
41
+ const p=new ModeProbe();p.standardQuery();
42
+ const known='\x1b[?1000;2$y\x1b[?1002;2$y\x1b[?1003;2$y\x1b[?1006;2$y';
43
+ const unsupported='\x1b[?9;0$y\x1b[?1016;0$y';
44
+ p.consume(unsupportedFirst?unsupported+known:known+unsupported);
45
+ assert.equal(p.state.mouse,'NONE');assert.equal(p.state.encoding,'DEFAULT');
46
+ p.consume('\x1b[?7;0$y\x1b[?25;0$y\x1b[?1049;0$y');
47
+ assert.equal(p.state.dec.wraparound,null);assert.equal(p.state.cursorHidden,null);assert.equal(p.state.alternate,null);
48
+ const local=new TerminalModeTracker();Object.assign(local,structuredClone(p.state));
49
+ local.consume(Buffer.from('\x1b[?1003h\x1b[?1006h'));
50
+ const cleanup=modes.modeTransition(local.snapshot(),p.state);
51
+ assert.ok(cleanup.includes('\x1b[?1003l'));assert.ok(cleanup.includes('\x1b[?1006l'));
52
+ local.consume(Buffer.from(cleanup));assert.equal(local.mouse,'NONE');assert.equal(local.encoding,'DEFAULT');
53
+ });
54
+ }
55
+ test('unsupported replies do not override active modes or fill unanswered fields',()=>{
56
+ const p=new ModeProbe();p.standardQuery();
57
+ p.consume('\x1b[?9;0$y\x1b[?1016;0$y');
58
+ assert.equal(p.state.mouse,null);assert.equal(p.state.encoding,null);
59
+ p.consume('\x1b[?1002;1$y\x1b[?1006;1$y');
60
+ assert.equal(p.state.mouse,'DRAG');assert.equal(p.state.encoding,'SGR');
61
+ p.consume('\x1b[?1000;0$y\x1b[?1003;0$y');
62
+ assert.equal(p.state.mouse,'DRAG');
63
+ });
64
+
65
+ test('multiple active replies preserve individual switches and select explicit priorities',()=>{
66
+ const p=new ModeProbe();p.standardQuery();
67
+ for(const n of [1003,1002,1000,1016,1006])p.consume(`\x1b[?${n};1$y`);
68
+ p.consume('\x1b[?9;2$y');
69
+ assert.equal(p.state.mouse,'ANY');assert.equal(p.state.encoding,'SGR_PIXELS');
70
+ assert.deepEqual(p.state.mouseModeBits,{9:false,1000:true,1002:true,1003:true});
71
+ assert.deepEqual(p.state.mouseEncodingBits,{1006:true,1016:true});
72
+ assert.deepEqual(modes.validateSnapshot(p.state),p.state);
73
+ const bits={9:true,1000:false,1002:false,1003:false,1006:false,1016:false};
74
+ const cleanup=modes.modeTransition(new TerminalModeTracker().snapshot(),p.state);
75
+ for(const [,n,on] of cleanup.matchAll(/\x1b\[\?(\d+)([hl])/g))bits[n]=on==='h';
76
+ assert.deepEqual(bits,{...p.state.mouseModeBits,...p.state.mouseEncodingBits});
77
+ // The same sequence on an exclusive xterm-style terminal ends at the
78
+ // effective mode, without trying to change its parser semantics.
79
+ const exclusive=new TerminalModeTracker();exclusive.consume(Buffer.from(cleanup));
80
+ assert.equal(exclusive.mouse,'ANY');assert.equal(exclusive.encoding,'SGR_PIXELS');
81
+ });
82
+
83
+ test('partial query restoration leaves unreported switches untouched',()=>{
84
+ const p=new ModeProbe();p.standardQuery();p.consume('\x1b[?1000;2$y\x1b[?1006;1$y');
85
+ const cleanup=modes.modeTransition(new TerminalModeTracker().snapshot(),p.state,{normalizeMouse:true});
86
+ assert.equal(cleanup,'\x1b[?1000l\x1b[?1006h');
87
+ });
88
+
89
+ test('normalization clears every conflicting switch, even if aggregate appears unchanged',()=>{
90
+ for(const target of [{mouse:'NONE',encoding:'DEFAULT'},{mouse:'VT200',encoding:'SGR'}]){
91
+ const state={...new TerminalModeTracker().snapshot(),...target};
92
+ const bits={9:true,1000:true,1002:true,1003:true,1006:true,1016:true};
93
+ const seq=modes.modeTransition(state,state,{normalizeMouse:true});
94
+ for(const [,n,on] of seq.matchAll(/\x1b\[\?(\d+)([hl])/g))bits[n]=on==='h';
95
+ assert.deepEqual(bits,{9:false,1000:target.mouse==='VT200',1002:false,1003:false,1006:target.encoding==='SGR',1016:false});
96
+ }
97
+ });
98
+
99
+ test('hidden Kitty restore requires an explicit capability in a valid private reply',()=>{
100
+ for(const advertised of [undefined,0,true,1]){
101
+ const p=new ModeProbe(),reply={...new TerminalModeTracker().snapshot(),kittyRestore:advertised};
102
+ p.consume(p.privatePrefix+Buffer.from(JSON.stringify(reply)).toString('base64')+'\x07');
103
+ assert.equal(p.complete,true);assert.equal(p.kittyRestore,advertised===1);
104
+ }
105
+ });