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.
- package/NOTICE.md +46 -46
- package/README.md +22 -15
- package/README.zh.md +22 -15
- package/initramfs/buninu/CHANGELOG.md +47 -0
- package/initramfs/buninu/apps/jsgotty/LICENSES/MIT-jsmdcui.txt +22 -0
- package/initramfs/buninu/apps/jsgotty/NOTICE +5 -0
- package/initramfs/buninu/apps/jsgotty/README.en.md +21 -0
- package/initramfs/buninu/apps/jsgotty/README.md +21 -0
- package/initramfs/buninu/apps/jsgotty/client-modes.mjs +117 -0
- package/initramfs/buninu/apps/jsgotty/client.mjs +100 -10
- package/initramfs/buninu/apps/jsgotty/clipboard-powershell.js +18 -0
- package/initramfs/buninu/apps/jsgotty/clipboard.js +148 -0
- package/initramfs/buninu/apps/jsgotty/gotty.js +13 -0
- package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
- package/initramfs/buninu/apps/jsgotty/package.json +1 -1
- package/initramfs/buninu/apps/jsgotty/static/css/index.css +14 -1
- package/initramfs/buninu/apps/jsgotty/static/js/gotty.js +107 -8
- package/initramfs/buninu/apps/jsgotty/static/js/kitty.js +115 -7
- package/initramfs/buninu/apps/jsgotty/terminal-modes.js +332 -0
- package/initramfs/buninu/apps/jsgotty/tests/client-exit.test.mjs +45 -0
- package/initramfs/buninu/apps/jsgotty/tests/client-modes.test.mjs +105 -0
- package/initramfs/buninu/apps/jsgotty/tests/clipboard-browser.test.js +81 -0
- package/initramfs/buninu/apps/jsgotty/tests/clipboard.test.js +75 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/zoom-collector.cjs +55 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/zoom-debug.js +39 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual-touch-diagnostics.js +33 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual-touch-diagnostics.mjs +22 -0
- package/initramfs/buninu/apps/jsgotty/tests/nested-cli-pty.test.mjs +116 -0
- package/initramfs/buninu/apps/jsgotty/tests/reconnect-modes-browser.test.js +144 -0
- package/initramfs/buninu/apps/jsgotty/tests/reconnect-modes.md +44 -0
- package/initramfs/buninu/apps/jsgotty/tests/reconnect-modes.test.js +166 -0
- package/initramfs/buninu/apps/jsgotty/tests/touch-drag-browser.test.js +106 -0
- package/initramfs/buninu/apps/jsgotty/tests/wheel-browser.test.js +60 -0
- package/initramfs/buninu/apps/jsmdcui/CHANGELOG.md +44 -0
- package/initramfs/buninu/apps/jsmdcui/package.json +1 -1
- package/initramfs/buninu/apps/jsmdcui/src/highlight/go-regexp.js +83 -0
- package/initramfs/buninu/apps/jsmdcui/src/highlight/highlighter.js +49 -62
- package/initramfs/buninu/apps/jsmdcui/src/highlight/parser.js +8 -5
- package/initramfs/buninu/apps/jsmdcui/tests/go-regexp.test.js +98 -0
- package/initramfs/buninu/apps/jsmdcui/tests/heading-ids.test.js +21 -16
- package/initramfs/buninu/apps/jsmdcui/tests/highlight-regions.test.js +73 -0
- package/initramfs/buninu/apps/jsmdcui/tests/id-collision.test.js +32 -9
- package/initramfs/buninu/apps/jsmdcui/tests/markdown-behavior.mjs +9 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/NORMAL-HTML.md +46 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/README.md +85 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/REPORT.md +328 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/compare.mjs +75 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/go.mod +5 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/normal-html-report.json +619 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/normal-html.html +9 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/oracle.go +49 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/report.json +2174 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/PowerShell.txt +39 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ada.txt +25 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/apacheconf.txt +49 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/arduino.txt +38 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/asciidoc.txt +33 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/asm.txt +77 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ats.txt +48 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/awk.txt +28 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/b.txt +27 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/bat.txt +39 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/c.txt +34 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/caddyfile.txt +14 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cake.txt +7 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/clojure.txt +18 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cmake.txt +20 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/coffeescript.txt +32 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/colortest.txt +19 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/conky.txt +12 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cpp.txt +46 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/crontab.txt +16 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/crystal.txt +28 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/csharp.txt +28 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/css.txt +19 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/csx.txt +7 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cuda.txt +37 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/cython.txt +22 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/d.txt +52 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/dart.txt +27 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/default.txt +8 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/dockerfile.txt +10 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/dot.txt +15 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/elixir.txt +23 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/elm.txt +20 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/erb.txt +32 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/erlang.txt +23 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/fish.txt +20 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/forth.txt +17 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/fortran.txt +40 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/freebsd-kernel.txt +10 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/fsharp.txt +27 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gdscript.txt +26 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gemini.txt +11 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gentoo-ebuild.txt +28 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gentoo-etc-portage.txt +14 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/git-commit.txt +19 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/git-config.txt +11 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/git-rebase-todo.txt +10 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gleam.txt +44 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/glsl.txt +18 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gnuplot.txt +11 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/go.txt +33 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/godoc.txt +9 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/golo.txt +50 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/gomod.txt +16 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/graphql.txt +20 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/groff.txt +18 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/groovy.txt +42 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/haml.txt +16 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/hare.txt +31 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/haskell.txt +26 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/hc.txt +28 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/html.txt +26 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/html4.txt +20 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/html5.txt +20 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ini.txt +16 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/inputrc.txt +14 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/java.txt +18 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/javascript.txt +41 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/jinja2.txt +13 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/json.txt +21 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/jsonnet.txt +28 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/julia.txt +24 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/justfile.txt +26 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/keymap.txt +15 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/kickstart.txt +16 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/kotlin.txt +30 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/kvlang.txt +37 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ledger.txt +13 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/lfe.txt +17 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/lilypond.txt +15 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/lisp.txt +17 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/log.txt +45 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/lua.txt +48 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/mail.txt +17 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/makefile.txt +25 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/man.txt +12 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/markdown.txt +21 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/mc.txt +11 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/meson.txt +29 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/micro.txt +25 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/mpdconf.txt +13 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/msbuild.txt +7 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nanorc.txt +16 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nftables.txt +22 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nginx.txt +17 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nim.txt +23 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nix.txt +15 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/nu.txt +92 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/objc.txt +36 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ocaml.txt +25 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/octave.txt +35 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/odin.txt +35 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/pascal.txt +22 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/patch.txt +13 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/peg.txt +16 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/perl.txt +27 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/php.txt +44 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/pkg-config.txt +12 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/po.txt +12 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/pony.txt +29 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/pov.txt +17 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/privoxy-action.txt +14 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/privoxy-config.txt +10 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/privoxy-filter.txt +12 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/proto.txt +22 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/prql.txt +42 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/puppet.txt +22 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/python2.txt +24 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/python3.txt +28 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/r.txt +16 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/raku.txt +19 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/reST.txt +18 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/renpy.txt +11 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/rpmspec.txt +31 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/ruby.txt +34 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/rust.txt +27 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sage.txt +24 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/scad.txt +23 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/scala.txt +16 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sed.txt +12 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sh.txt +20 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sls.txt +15 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/smalltalk.txt +28 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/solidity.txt +26 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/sql.txt +35 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/stata.txt +32 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/svelte.txt +10 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/swift.txt +53 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/systemd.txt +15 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/tcl.txt +17 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/terraform.txt +28 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/tex.txt +15 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/toml.txt +31 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/twig.txt +29 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/typescript.txt +32 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/v.txt +47 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/vala.txt +22 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/verilog.txt +37 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/vhdl.txt +37 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/vi.txt +17 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/vue.txt +29 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/xml.txt +14 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/xresources.txt +14 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/yaml.txt +19 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/yum.txt +12 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/zig.txt +24 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/zscript.txt +31 -0
- package/initramfs/buninu/apps/jsmdcui/tests/syntax-parity/samples/zsh.txt +21 -0
- package/initramfs/buninu/package.json +1 -1
- package/initramfs/lib/bunterm/images.js +5 -1
- package/initramfs/lib/bunterm/raw-protocol.js +82 -4
- package/initramfs/lib/bunterm/renderer.js +35 -16
- package/initramfs/lib/bunterm/terminal.js +11 -1
- package/initramfs/lib/fbdev.js +11 -5
- package/initramfs/lib/mapped-frame.js +28 -0
- package/initramfs/lib/raw-image.js +29 -5
- package/package.json +1 -1
- package/scripts/arch.js +5 -5
- package/scripts/arch.sh +5 -5
- package/scripts/npm-packages.md +11 -10
- package/test/bunterm-image-bytes.test.js +15 -0
- package/test/bunterm-mapped-lifetime.test.js +147 -0
- package/test/bunterm-raw-protocol.test.js +20 -0
- package/test/bunterm-renderer.test.js +9 -0
- package/test/fbdev-sync.test.js +9 -0
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
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("
|
|
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
|
+
});
|