buninu 0.6.0 → 0.7.0
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/CHANGELOG.md +26 -0
- package/apps/jsgotty/README.en.md +4 -0
- package/apps/jsgotty/README.md +4 -0
- package/apps/jsgotty/package-lock.json +2 -2
- package/apps/jsgotty/package.json +1 -1
- package/apps/jsgotty/static/css/index.css +7 -0
- package/apps/jsgotty/static/js/gotty.js +12 -4
- package/apps/jsgotty/static/js/kitty.js +115 -7
- package/apps/jsgotty/tests/manual/zoom-collector.cjs +55 -0
- package/apps/jsgotty/tests/manual/zoom-debug.js +39 -0
- package/apps/jsgotty/tests/touch-drag-browser.test.js +106 -0
- package/apps/jsgotty/tests/wheel-browser.test.js +60 -0
- package/apps/jsmdcui/CHANGELOG.md +44 -0
- package/apps/jsmdcui/package.json +1 -1
- package/apps/jsmdcui/src/highlight/go-regexp.js +83 -0
- package/apps/jsmdcui/src/highlight/highlighter.js +49 -62
- package/apps/jsmdcui/src/highlight/parser.js +8 -5
- package/apps/jsmdcui/tests/go-regexp.test.js +98 -0
- package/apps/jsmdcui/tests/heading-ids.test.js +21 -16
- package/apps/jsmdcui/tests/highlight-regions.test.js +73 -0
- package/apps/jsmdcui/tests/id-collision.test.js +32 -9
- package/apps/jsmdcui/tests/markdown-behavior.mjs +9 -0
- package/apps/jsmdcui/tests/syntax-parity/NORMAL-HTML.md +46 -0
- package/apps/jsmdcui/tests/syntax-parity/README.md +85 -0
- package/apps/jsmdcui/tests/syntax-parity/REPORT.md +328 -0
- package/apps/jsmdcui/tests/syntax-parity/compare.mjs +75 -0
- package/apps/jsmdcui/tests/syntax-parity/go.mod +5 -0
- package/apps/jsmdcui/tests/syntax-parity/normal-html-report.json +619 -0
- package/apps/jsmdcui/tests/syntax-parity/normal-html.html +9 -0
- package/apps/jsmdcui/tests/syntax-parity/oracle.go +49 -0
- package/apps/jsmdcui/tests/syntax-parity/report.json +2174 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/PowerShell.txt +39 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ada.txt +25 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/apacheconf.txt +49 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/arduino.txt +38 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/asciidoc.txt +33 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/asm.txt +77 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ats.txt +48 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/awk.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/b.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/bat.txt +39 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/c.txt +34 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/caddyfile.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cake.txt +7 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/clojure.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cmake.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/coffeescript.txt +32 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/colortest.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/conky.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cpp.txt +46 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/crontab.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/crystal.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/csharp.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/css.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/csx.txt +7 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cuda.txt +37 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/cython.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/d.txt +52 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/dart.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/default.txt +8 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/dockerfile.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/dot.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/elixir.txt +23 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/elm.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/erb.txt +32 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/erlang.txt +23 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/fish.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/forth.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/fortran.txt +40 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/freebsd-kernel.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/fsharp.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gdscript.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gemini.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gentoo-ebuild.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gentoo-etc-portage.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/git-commit.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/git-config.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/git-rebase-todo.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gleam.txt +44 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/glsl.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gnuplot.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/go.txt +33 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/godoc.txt +9 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/golo.txt +50 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/gomod.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/graphql.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/groff.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/groovy.txt +42 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/haml.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/hare.txt +31 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/haskell.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/hc.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/html.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/html4.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/html5.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ini.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/inputrc.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/java.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/javascript.txt +41 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/jinja2.txt +13 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/json.txt +21 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/jsonnet.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/julia.txt +24 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/justfile.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/keymap.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/kickstart.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/kotlin.txt +30 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/kvlang.txt +37 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ledger.txt +13 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/lfe.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/lilypond.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/lisp.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/log.txt +45 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/lua.txt +48 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/mail.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/makefile.txt +25 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/man.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/markdown.txt +21 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/mc.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/meson.txt +29 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/micro.txt +25 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/mpdconf.txt +13 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/msbuild.txt +7 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nanorc.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nftables.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nginx.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nim.txt +23 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nix.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/nu.txt +92 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/objc.txt +36 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ocaml.txt +25 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/octave.txt +35 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/odin.txt +35 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/pascal.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/patch.txt +13 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/peg.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/perl.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/php.txt +44 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/pkg-config.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/po.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/pony.txt +29 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/pov.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/privoxy-action.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/privoxy-config.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/privoxy-filter.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/proto.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/prql.txt +42 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/puppet.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/python2.txt +24 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/python3.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/r.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/raku.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/reST.txt +18 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/renpy.txt +11 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/rpmspec.txt +31 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/ruby.txt +34 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/rust.txt +27 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sage.txt +24 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/scad.txt +23 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/scala.txt +16 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sed.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sh.txt +20 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sls.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/smalltalk.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/solidity.txt +26 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/sql.txt +35 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/stata.txt +32 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/svelte.txt +10 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/swift.txt +53 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/systemd.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/tcl.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/terraform.txt +28 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/tex.txt +15 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/toml.txt +31 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/twig.txt +29 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/typescript.txt +32 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/v.txt +47 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/vala.txt +22 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/verilog.txt +37 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/vhdl.txt +37 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/vi.txt +17 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/vue.txt +29 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/xml.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/xresources.txt +14 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/yaml.txt +19 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/yum.txt +12 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/zig.txt +24 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/zscript.txt +31 -0
- package/apps/jsmdcui/tests/syntax-parity/samples/zsh.txt +21 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 - 2026-10-26
|
|
4
|
+
|
|
5
|
+
- Sync jsgotty to 1.4.0:
|
|
6
|
+
* Add horizontal and diagonal scrolling through terminal mouse reporting,
|
|
7
|
+
including Shift-wheel horizontal scrolling and preserved wheel modifiers.
|
|
8
|
+
* Double-tap and hold the second tap to drag with the left mouse button when
|
|
9
|
+
application mouse reporting is enabled. Release the button on touch
|
|
10
|
+
cancellation, backgrounding or disconnect; ordinary swipes still scroll.
|
|
11
|
+
* Add Right click to the existing image long-press menu, sending the click
|
|
12
|
+
at the position where the menu was opened without removing the image.
|
|
13
|
+
* Prevent Android WebView from automatically zooming the terminal's hidden
|
|
14
|
+
input after native long-press paste, including at reduced viewport zoom.
|
|
15
|
+
Keep native paste, IME input and pinch zoom on the visible terminal.
|
|
16
|
+
- Sync jsmdcui to 0.19.4:
|
|
17
|
+
* Restore Dockerfile highlighting and improve compatibility with Micro's
|
|
18
|
+
syntax definitions by translating Go regexp flags and POSIX character
|
|
19
|
+
classes while preserving scoped flags.
|
|
20
|
+
* Fix nested HTML/XML highlighting regions so quoted attributes return to
|
|
21
|
+
their enclosing tags and unfinished attributes retain state across lines.
|
|
22
|
+
* Use iterative region scanning to support long minified lines without
|
|
23
|
+
recursive stack exhaustion or truncating highlighting.
|
|
24
|
+
* Guard against zero-width region cycles, cyclic syntax/state trees and
|
|
25
|
+
excessively nested regexp conversion. Disable failing rules individually
|
|
26
|
+
instead of losing an entire syntax definition; retain support for long
|
|
27
|
+
lines. These guards do not impose timeouts on individual JavaScript regexps.
|
|
28
|
+
|
|
3
29
|
## 0.6.0 - 2026-10-25
|
|
4
30
|
|
|
5
31
|
- Sync jsgotty to 1.3.0:
|
|
@@ -349,3 +349,7 @@ printf '\033]52;c;%s\007' "$(printf 'Hello' | base64 | tr -d '\n')"
|
|
|
349
349
|
Applications can send `ESC ] 52 ; c ; ? ST` to read and receive a base64-encoded OSC 52 reply on stdin.
|
|
350
350
|
|
|
351
351
|
`jsgotty --client` **does not intercept OSC 52**: it forwards reads and writes unchanged for the outer terminal emulator to handle.
|
|
352
|
+
|
|
353
|
+
### Touch dragging and right click
|
|
354
|
+
|
|
355
|
+
When the application enables mouse reporting, double-tap and hold the second tap to drag with the left button; lifting your finger releases it. Single-finger swipes still scroll. The existing image long-press menu now includes `Right click` below `Remove image`; it sends a right click at the position where the menu was opened. Pinch zoom and the existing remove-image gesture remain unchanged. To disable double-tap dragging, set `window.touchDoubleTapDrag = false` in the page JavaScript.
|
package/apps/jsgotty/README.md
CHANGED
|
@@ -351,3 +351,7 @@ printf '\033]52;c;%s\007' "$(printf 'Hello' | base64 | tr -d '\n')"
|
|
|
351
351
|
程式可送出 `ESC ] 52 ; c ; ? ST` 讀取,並從 stdin 接收 base64 編碼的 OSC 52 回覆。
|
|
352
352
|
|
|
353
353
|
`jsgotty --client` **不攔截 OSC 52**,讀寫控制碼都原樣轉送,交給最外層真正的終端模擬器決定。
|
|
354
|
+
|
|
355
|
+
### 觸控拖曳與右鍵
|
|
356
|
+
|
|
357
|
+
在程式啟用滑鼠回報時,快速點兩下並按住第二下可拖曳左鍵,放開即釋放;單指滑動仍是捲動。既有圖片長按選單的 `Remove image` 下方新增 `Right click`,會在叫出選單的原始位置送出右鍵點擊。雙指縮放與原本的移除圖片入口不變。若要停用雙擊拖曳,可在網頁 JavaScript 設定 `window.touchDoubleTapDrag = false`。
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsgotty",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "jsgotty",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.4.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"node-addon-api": "^7.1.1",
|
|
12
12
|
"ws": "^8.18.3"
|
|
@@ -17,3 +17,10 @@ html, body, #terminal {
|
|
|
17
17
|
#terminal, #terminal .xterm, #terminal .xterm-viewport, #terminal .xterm-screen {
|
|
18
18
|
touch-action: manipulation;
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
/* Chromium skips focused-editable auto-zoom when that element disallows
|
|
22
|
+
pinch zoom. Restrict only the hidden input, not the terminal canvas:
|
|
23
|
+
native paste/IME remain available and the visible terminal can still pinch. */
|
|
24
|
+
#terminal .xterm-helper-textarea {
|
|
25
|
+
touch-action: none;
|
|
26
|
+
}
|
|
@@ -15997,8 +15997,15 @@
|
|
|
15997
15997
|
!1 === e._customWheelEventHandler(t)
|
|
15998
15998
|
)
|
|
15999
15999
|
return !1;
|
|
16000
|
-
|
|
16001
|
-
|
|
16000
|
+
// Preserve both axes of diagonal wheel/touch gestures.
|
|
16001
|
+
// CoreMouseService already encodes wheel actions 2/3 as 66/67.
|
|
16002
|
+
if (t.deltaX) e.coreMouseService.triggerMouseEvent({
|
|
16003
|
+
col: i.col, row: i.row, x: i.x, y: i.y,
|
|
16004
|
+
button: 4, action: t.deltaX < 0 ? 2 : 3,
|
|
16005
|
+
ctrl: t.ctrlKey, alt: t.altKey, shift: t.shiftKey,
|
|
16006
|
+
});
|
|
16007
|
+
if (0 === e.viewport.getLinesScrolled(t, true))
|
|
16008
|
+
return !!t.deltaX;
|
|
16002
16009
|
((s = t.deltaY < 0 ? 0 : 1), (r = 4));
|
|
16003
16010
|
break;
|
|
16004
16011
|
default:
|
|
@@ -16972,8 +16979,9 @@
|
|
|
16972
16979
|
}
|
|
16973
16980
|
return { bufferElements: s, cursorElement: i };
|
|
16974
16981
|
}
|
|
16975
|
-
getLinesScrolled(e) {
|
|
16976
|
-
|
|
16982
|
+
getLinesScrolled(e, mouseReporting = false) {
|
|
16983
|
+
// Shift belongs to the remote application while mouse reporting is active.
|
|
16984
|
+
if (0 === e.deltaY || (e.shiftKey && !mouseReporting)) return 0;
|
|
16977
16985
|
let t = this._applyScrollModifier(e.deltaY, e);
|
|
16978
16986
|
return (
|
|
16979
16987
|
e.deltaMode === WheelEvent.DOM_DELTA_PIXEL
|
|
@@ -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,55 @@
|
|
|
1
|
+
// Usage: node tests/manual/zoom-collector.cjs [port] [output.jsonl]
|
|
2
|
+
const http = require('node:http');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const { randomBytes } = require('node:crypto');
|
|
7
|
+
const port = Number(process.argv[2] || 18767);
|
|
8
|
+
const file = path.resolve(process.argv[3] || path.join(os.tmpdir(), 'jsgotty-zoom-debug.jsonl'));
|
|
9
|
+
const token = randomBytes(16).toString('hex');
|
|
10
|
+
const endpoint = `http://127.0.0.1:${port}/${token}`;
|
|
11
|
+
const client = fs.readFileSync(path.join(__dirname, 'zoom-debug.js'), 'utf8') +
|
|
12
|
+
`\nwindow.startGottyZoomDebug({endpoint:${JSON.stringify(endpoint)}});\n`;
|
|
13
|
+
let count = 0;
|
|
14
|
+
http.createServer((req, res) => {
|
|
15
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
16
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
17
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
18
|
+
res.setHeader('Access-Control-Allow-Private-Network', 'true');
|
|
19
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
20
|
+
if (req.url !== '/' + token && req.url !== '/' + token + '/client.js') {
|
|
21
|
+
res.writeHead(404); return res.end();
|
|
22
|
+
}
|
|
23
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); return res.end(); }
|
|
24
|
+
if (req.method === 'GET' && req.url.endsWith('/client.js')) {
|
|
25
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
26
|
+
return res.end(client);
|
|
27
|
+
}
|
|
28
|
+
if (req.method !== 'POST' || req.url !== '/' + token) {
|
|
29
|
+
res.writeHead(405); return res.end();
|
|
30
|
+
}
|
|
31
|
+
let body = '';
|
|
32
|
+
req.on('data', chunk => {
|
|
33
|
+
body += chunk;
|
|
34
|
+
if (body.length > 16384) req.destroy();
|
|
35
|
+
});
|
|
36
|
+
req.on('end', () => {
|
|
37
|
+
if (count >= 20000) { res.writeHead(429); return res.end(); }
|
|
38
|
+
let event;
|
|
39
|
+
try { event = JSON.parse(body); }
|
|
40
|
+
catch { res.writeHead(400); return res.end(); }
|
|
41
|
+
try {
|
|
42
|
+
fs.appendFileSync(file, JSON.stringify({receivedAt: new Date().toISOString(), event}) + '\n', {mode: 0o600});
|
|
43
|
+
count++;
|
|
44
|
+
res.writeHead(204); res.end();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(error.message);
|
|
47
|
+
res.writeHead(500); res.end();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}).listen(port, '127.0.0.1', () => {
|
|
51
|
+
console.log('Log (append): ' + file);
|
|
52
|
+
console.log('Run in the connected jsgotty page via DevTools or CDP:');
|
|
53
|
+
console.log(`fetch(${JSON.stringify(endpoint + '/client.js')}).then(r => r.text()).then(source => (0, eval)(source))`);
|
|
54
|
+
console.log('Stop browser capture: window.stopGottyZoomDebug()');
|
|
55
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Manual WebView zoom diagnostics. Does not change focus, selection or gestures.
|
|
2
|
+
window.startGottyZoomDebug = ({endpoint} = {}) => {
|
|
3
|
+
window.stopGottyZoomDebug?.();
|
|
4
|
+
window.__zoomDiag?.stop();
|
|
5
|
+
const logs = [], cleanup = [], started = Date.now();
|
|
6
|
+
const session = Math.random().toString(36).slice(2);
|
|
7
|
+
let stopped = false;
|
|
8
|
+
const label = el => el ? `${el.tagName || ''}#${el.id || ''}.${String(el.className || '')}` : null;
|
|
9
|
+
const record = event => {
|
|
10
|
+
if (stopped || logs.length >= 4000 || Date.now() - started > 600000) return;
|
|
11
|
+
const v = window.visualViewport, active = document.activeElement;
|
|
12
|
+
const row = {session, sequence:logs.length, time:Date.now(), type:event.type,
|
|
13
|
+
target:label(event.target), active:label(active), trusted:event.isTrusted,
|
|
14
|
+
touches:event.touches?.length, viewport:document.querySelector('meta[name=viewport]')?.content,
|
|
15
|
+
scale:v?.scale, offset:v ? [v.offsetLeft,v.offsetTop] : null,
|
|
16
|
+
size:v ? [v.width,v.height] : null, scroll:[scrollX,scrollY],
|
|
17
|
+
selection:document.getSelection()?.type,
|
|
18
|
+
fontSize:active ? getComputedStyle(active).fontSize : null};
|
|
19
|
+
logs.push(row);
|
|
20
|
+
if(endpoint) fetch(endpoint,{method:'POST',headers:{'Content-Type':'text/plain'},body:JSON.stringify(row)})
|
|
21
|
+
.catch(e=>{window.__gottyZoomDeliveryError=String(e)});
|
|
22
|
+
};
|
|
23
|
+
const listen=(target,type)=>{
|
|
24
|
+
target.addEventListener(type,record,{capture:true,passive:true});
|
|
25
|
+
cleanup.push(()=>target.removeEventListener(type,record,true));
|
|
26
|
+
};
|
|
27
|
+
for(const type of ['touchstart','touchend','touchcancel','mousedown','mouseup','click',
|
|
28
|
+
'contextmenu','focusin','focusout','selectionchange','visibilitychange'])listen(document,type);
|
|
29
|
+
if(window.visualViewport)for(const type of ['resize','scroll'])listen(visualViewport,type);
|
|
30
|
+
const observer=new MutationObserver(()=>record({type:'viewport-mutation'}));
|
|
31
|
+
const meta=document.querySelector('meta[name=viewport]');
|
|
32
|
+
if(meta)observer.observe(meta,{attributes:true});
|
|
33
|
+
cleanup.push(()=>observer.disconnect());
|
|
34
|
+
window.stopGottyZoomDebug=()=>{stopped=true;cleanup.forEach(fn=>fn())};
|
|
35
|
+
window.__gottyZoomRecords=logs;
|
|
36
|
+
window.markGottyZoomDebug=label=>record({type:'mark:'+label});
|
|
37
|
+
record({type:'ready'});
|
|
38
|
+
return 'Zoom diagnostics started';
|
|
39
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Opt-in real bundled-xterm integration; no browser/dependency downloads.
|
|
2
|
+
// CHROMIUM_HEADLESS=/path/to/chromium-headless-shell node --test tests/touch-drag-browser.test.js
|
|
3
|
+
const test=require('node:test');
|
|
4
|
+
const assert=require('node:assert/strict');
|
|
5
|
+
const fs=require('node:fs');
|
|
6
|
+
const path=require('node:path');
|
|
7
|
+
const http=require('node:http');
|
|
8
|
+
const {spawn}=require('node:child_process');
|
|
9
|
+
const {once}=require('node:events');
|
|
10
|
+
const WebSocket=require('ws');
|
|
11
|
+
const root=path.join(__dirname,'../static');
|
|
12
|
+
test('touch double-tap drag and image-menu right click use bundled xterm', {skip:!process.env.CHROMIUM_HEADLESS,timeout:60000},async()=>{
|
|
13
|
+
const bundle=fs.readFileSync(path.join(root,'js/gotty.js'),'utf8').replace('(i.nc = void 0));','(i.nc = void 0)); window.TestGoTTYXterm = i(634).GoTTYXterm;');
|
|
14
|
+
const server=http.createServer((req,res)=>{
|
|
15
|
+
if(req.url==='/index.css'){res.setHeader('Content-Type','text/css');res.end(fs.readFileSync(path.join(root,'css/index.css')));}
|
|
16
|
+
else if(req.url==='/bundle.js'){res.setHeader('Content-Type','text/javascript');res.end(bundle);}
|
|
17
|
+
else if(req.url==='/gestures.js'){
|
|
18
|
+
const source=fs.readFileSync(path.join(root,'js/kitty.js'),'utf8');
|
|
19
|
+
res.setHeader('Content-Type','text/javascript');
|
|
20
|
+
res.end(source.slice(source.indexOf(' function reportTouchMouse('),source.indexOf(' function ensureContextMenu('))+source.slice(source.indexOf(' function ensureContextMenu('),source.indexOf(' function bindImageContextMenu('))+source.slice(source.indexOf(' function bindImageContextMenu('),source.indexOf(' function bindImageContextMenu(')+source.slice(source.indexOf(' function bindImageContextMenu(')).indexOf('\n function ',5))+'\nbindTouchScrolling();bindImageContextMenu();');
|
|
21
|
+
}
|
|
22
|
+
else if(req.url==='/keyboard.js'){res.setHeader('Content-Type','text/javascript');res.end(fs.readFileSync(path.join(root,'js/kitty-keyboard.js')));}
|
|
23
|
+
else res.end('<!doctype html><div id="terminal"><div id="fixture" style="width:800px;height:600px"></div></div><link rel="stylesheet" href="/index.css"><script src="/keyboard.js"></script><script src="/bundle.js"></script><script>window.wrapper=new TestGoTTYXterm(document.getElementById("fixture"));window.sent=[];wrapper.onInput(b=>sent.push(new TextDecoder().decode(b)));wrapper.kittyKeyboard.android=true;</script>');
|
|
24
|
+
});
|
|
25
|
+
server.listen(0,'127.0.0.1');await once(server,'listening');
|
|
26
|
+
const child=spawn(process.env.CHROMIUM_HEADLESS,['--no-sandbox','--disable-gpu','--remote-debugging-port=0','http://127.0.0.1:'+server.address().port],{detached:true,stdio:['ignore','ignore','pipe']});
|
|
27
|
+
let ws;
|
|
28
|
+
try{
|
|
29
|
+
const endpoint=await new Promise((resolve,reject)=>{let log='';const timer=setTimeout(()=>reject(Error('Browser startup timeout: '+log)),15000);child.on('error',reject);child.on('exit',code=>{clearTimeout(timer);reject(Error('Browser exited '+code+': '+log));});child.stderr.on('data',b=>{log+=b;const m=log.match(/DevTools listening on (ws:\/\/[^\s]+)/);if(m){clearTimeout(timer);resolve(m[1]);}});});
|
|
30
|
+
const u=new URL(endpoint);let tab;
|
|
31
|
+
for(let i=0;i<50;i++){tab=(await (await fetch('http://'+u.host+'/json/list')).json()).find(x=>x.type==='page');if(tab)break;await new Promise(r=>setTimeout(r,100));}
|
|
32
|
+
ws=new WebSocket(tab.webSocketDebuggerUrl);await once(ws,'open');let id=0;const pending=new Map();ws.on('message',data=>{const m=JSON.parse(data);if(pending.has(m.id)){const {resolve,reject}=pending.get(m.id);pending.delete(m.id);m.error?reject(m.error):resolve(m.result);}});
|
|
33
|
+
const call=(method,params)=>new Promise((resolve,reject)=>{pending.set(++id,{resolve,reject});ws.send(JSON.stringify({id,method,params}));});
|
|
34
|
+
for(let i=0;i<50;i++){const r=await call('Runtime.evaluate',{expression:'Boolean(window.wrapper)',returnByValue:true});if(r.result.value)break;await new Promise(r=>setTimeout(r,100));}
|
|
35
|
+
const result=await call('Runtime.evaluate',{awaitPromise:true,returnByValue:true,expression:`(async()=>{
|
|
36
|
+
const term=wrapper.term;
|
|
37
|
+
const write=s=>new Promise(r=>term.write(s,r));
|
|
38
|
+
const equal=(a,b)=>{if(JSON.stringify(a)!==JSON.stringify(b))throw Error(JSON.stringify({actual:a,expected:b}));};
|
|
39
|
+
await write('\\x1b[?1002h\\x1b[?1006h');
|
|
40
|
+
const terminal=document.getElementById('fixture');terminal.__gottyXterm=term;
|
|
41
|
+
const screen=term.element.querySelector('.xterm-screen'), rect=screen.getBoundingClientRect();
|
|
42
|
+
const state={placements:new Map()};
|
|
43
|
+
const xtermElements=()=>({screen});
|
|
44
|
+
const DEFAULT_TOUCH_SCROLL_THRESHOLD=24;
|
|
45
|
+
const removePlacementNode=node=>node.remove();
|
|
46
|
+
eval(await (await fetch('/gestures.js')).text());
|
|
47
|
+
const touch=(type,x=30,y=30,count=1)=>{
|
|
48
|
+
const point=new Touch({identifier:1,target:screen,clientX:rect.left+x,clientY:rect.top+y});
|
|
49
|
+
const touches=type==='touchend'||type==='touchcancel'?[]:count===1?[point]:[point,new Touch({identifier:2,target:screen,clientX:rect.left+x+30,clientY:rect.top+y})];
|
|
50
|
+
const event=new TouchEvent(type,{bubbles:true,cancelable:true,touches,changedTouches:[point]});
|
|
51
|
+
screen.dispatchEvent(event);return event.defaultPrevented;
|
|
52
|
+
};
|
|
53
|
+
const codes=()=>sent.map(s=>{const m=s.match(/<([0-9]+);[0-9]+;[0-9]+([Mm])/);return m?m[1]+m[2]:s;});
|
|
54
|
+
let focusCalls=0;const originalFocus=term._core.focus;term._core.focus=function(...args){focusCalls++;return originalFocus.apply(this,args);};
|
|
55
|
+
const firstTap=()=>{touch('touchstart');touch('touchend');
|
|
56
|
+
// Browser compatibility mouse sequence after an ordinary tap.
|
|
57
|
+
for(const type of ['mousedown','mouseup'])screen.dispatchEvent(new MouseEvent(type,{bubbles:true,clientX:rect.left+30,clientY:rect.top+30,button:0,buttons:type==='mousedown'?1:0}));};
|
|
58
|
+
sent.length=0;firstTap();equal(codes(),['0M','0m']);
|
|
59
|
+
const focusBeforeDrag=focusCalls;
|
|
60
|
+
if(!touch('touchstart'))throw Error('Second press must suppress compatibility click');
|
|
61
|
+
equal(codes(),['0M','0m','0M']); // still held, no premature release
|
|
62
|
+
touch('touchmove',80,70);touch('touchend',80,70);
|
|
63
|
+
equal(codes(),['0M','0m','0M','32M','0m']);
|
|
64
|
+
equal(focusCalls,focusBeforeDrag);
|
|
65
|
+
sent.length=0;firstTap();touch('touchstart');touch('touchend');
|
|
66
|
+
equal(codes(),['0M','0m','0M','0m']); // ordinary double click
|
|
67
|
+
for(const cancel of [()=>touch('touchcancel'),()=>touch('touchstart',30,30,2),()=>window.dispatchEvent(new Event('blur')),()=>state.cancelTouchGesture()]){
|
|
68
|
+
sent.length=0;firstTap();touch('touchstart');cancel();touch('touchend');equal(codes(),['0M','0m','0M','0m']);
|
|
69
|
+
}
|
|
70
|
+
sent.length=0;firstTap();touch('touchstart');
|
|
71
|
+
touch('touchmove',-200,-200);touch('touchend',-200,-200);
|
|
72
|
+
equal(codes(),['0M','0m','0M','32M','0m']); // release outside grid still reaches application
|
|
73
|
+
sent.length=0;touch('touchstart');touch('touchmove',30,90);touch('touchend',30,90);
|
|
74
|
+
if(!codes().length||codes().some(x=>!x.startsWith('64')))throw Error('Single touch must remain scrolling: '+codes());
|
|
75
|
+
sent.length=0;touch('touchstart');touch('touchmove',30,90);touch('touchend',30,90);
|
|
76
|
+
if(codes().some(x=>x==='0M'))throw Error('Scroll must not arm double tap');
|
|
77
|
+
// Use precisely the existing image context-menu path, with its original location.
|
|
78
|
+
const node=document.createElement('div');node.style.zIndex='0';node.getBoundingClientRect=()=>({left:0,top:0,right:1000,bottom:1000});state.placements.set('image',node);
|
|
79
|
+
screen.dispatchEvent(new MouseEvent('contextmenu',{bubbles:true,cancelable:true,clientX:rect.left+30,clientY:rect.top+30,button:2}));
|
|
80
|
+
if(state.contextMenu.root.hidden)throw Error('Menu missing');
|
|
81
|
+
const focusBeforeRight=focusCalls;
|
|
82
|
+
sent.length=0;state.contextMenu.rightButton.click();equal(focusCalls,focusBeforeRight);equal(codes(),['2M','2m']);
|
|
83
|
+
if(!state.contextMenu.root.hidden||!state.placements.has('image'))throw Error('Right click must hide menu and preserve image');
|
|
84
|
+
await write('\\x1b[?1002l');state.cancelTouchGesture();sent.length=0;
|
|
85
|
+
firstTap();touch('touchstart');touch('touchmove',80,70);touch('touchend');
|
|
86
|
+
if(codes().some(x=>/^[032][Mm]$/.test(x)))throw Error('No application mouse reporting outside mouse mode');
|
|
87
|
+
state.cancelTouchGesture();state.placements.clear();
|
|
88
|
+
term.focus();const textarea=term.textarea;
|
|
89
|
+
screen.dispatchEvent(new MouseEvent('contextmenu',{bubbles:true,cancelable:true,clientX:rect.left+30,clientY:rect.top+30,button:2}));
|
|
90
|
+
if(document.activeElement!==textarea)throw Error('Context menu must keep textarea focused for native paste');
|
|
91
|
+
const data=new DataTransfer();data.setData('text/plain','paste-check');sent.length=0;
|
|
92
|
+
textarea.dispatchEvent(new ClipboardEvent('paste',{bubbles:true,cancelable:true,clipboardData:data}));
|
|
93
|
+
if(!sent.join('').includes('paste-check'))throw Error('Paste stopped working');
|
|
94
|
+
equal(getComputedStyle(textarea).touchAction,'none');
|
|
95
|
+
equal(getComputedStyle(screen).touchAction,'manipulation');
|
|
96
|
+
touch('touchstart');
|
|
97
|
+
if(document.activeElement!==textarea)throw Error('Touch must not blur the input');
|
|
98
|
+
touch('touchend');
|
|
99
|
+
wrapper.close();return 'ok';
|
|
100
|
+
})()`});
|
|
101
|
+
assert.equal(result.exceptionDetails,undefined,JSON.stringify(result.exceptionDetails));assert.equal(result.result.value,'ok');
|
|
102
|
+
}finally{
|
|
103
|
+
ws?.close();if(child.pid){try{process.kill(-child.pid,'SIGTERM');}catch{}}
|
|
104
|
+
server.closeAllConnections();await new Promise(r=>server.close(r));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Opt-in real bundled-xterm integration; no browser/dependency downloads.
|
|
2
|
+
// CHROMIUM_HEADLESS=/path/to/chromium-headless-shell node --test tests/kitty-keyboard-browser.test.js
|
|
3
|
+
const test=require('node:test');
|
|
4
|
+
const assert=require('node:assert/strict');
|
|
5
|
+
const fs=require('node:fs');
|
|
6
|
+
const path=require('node:path');
|
|
7
|
+
const http=require('node:http');
|
|
8
|
+
const {spawn}=require('node:child_process');
|
|
9
|
+
const {once}=require('node:events');
|
|
10
|
+
const WebSocket=require('ws');
|
|
11
|
+
const root=path.join(__dirname,'../static');
|
|
12
|
+
test('shipped xterm reports horizontal, vertical and diagonal wheel events', {skip:!process.env.CHROMIUM_HEADLESS,timeout:60000},async()=>{
|
|
13
|
+
const bundle=fs.readFileSync(path.join(root,'js/gotty.js'),'utf8').replace('(i.nc = void 0));','(i.nc = void 0)); window.TestGoTTYXterm = i(634).GoTTYXterm;');
|
|
14
|
+
const server=http.createServer((req,res)=>{
|
|
15
|
+
if(req.url==='/bundle.js'){res.setHeader('Content-Type','text/javascript');res.end(bundle);}
|
|
16
|
+
else if(req.url==='/keyboard.js'){res.setHeader('Content-Type','text/javascript');res.end(fs.readFileSync(path.join(root,'js/kitty-keyboard.js')));}
|
|
17
|
+
else res.end('<!doctype html><div id="fixture" style="width:800px;height:600px"></div><script src="/keyboard.js"></script><script src="/bundle.js"></script><script>window.wrapper=new TestGoTTYXterm(document.getElementById("fixture"));window.sent=[];wrapper.onInput(b=>sent.push(new TextDecoder().decode(b)));wrapper.kittyKeyboard.android=true;</script>');
|
|
18
|
+
});
|
|
19
|
+
server.listen(0,'127.0.0.1');await once(server,'listening');
|
|
20
|
+
const child=spawn(process.env.CHROMIUM_HEADLESS,['--no-sandbox','--disable-gpu','--remote-debugging-port=0','http://127.0.0.1:'+server.address().port],{detached:true,stdio:['ignore','ignore','pipe']});
|
|
21
|
+
let ws;
|
|
22
|
+
try{
|
|
23
|
+
const endpoint=await new Promise((resolve,reject)=>{let log='';const timer=setTimeout(()=>reject(Error('Browser startup timeout: '+log)),15000);child.on('error',reject);child.on('exit',code=>{clearTimeout(timer);reject(Error('Browser exited '+code+': '+log));});child.stderr.on('data',b=>{log+=b;const m=log.match(/DevTools listening on (ws:\/\/[^\s]+)/);if(m){clearTimeout(timer);resolve(m[1]);}});});
|
|
24
|
+
const u=new URL(endpoint);let tab;
|
|
25
|
+
for(let i=0;i<50;i++){tab=(await (await fetch('http://'+u.host+'/json/list')).json()).find(x=>x.type==='page');if(tab)break;await new Promise(r=>setTimeout(r,100));}
|
|
26
|
+
ws=new WebSocket(tab.webSocketDebuggerUrl);await once(ws,'open');let id=0;const pending=new Map();ws.on('message',data=>{const m=JSON.parse(data);if(pending.has(m.id)){const {resolve,reject}=pending.get(m.id);pending.delete(m.id);m.error?reject(m.error):resolve(m.result);}});
|
|
27
|
+
const call=(method,params)=>new Promise((resolve,reject)=>{pending.set(++id,{resolve,reject});ws.send(JSON.stringify({id,method,params}));});
|
|
28
|
+
for(let i=0;i<50;i++){const r=await call('Runtime.evaluate',{expression:'Boolean(window.wrapper)',returnByValue:true});if(r.result.value)break;await new Promise(r=>setTimeout(r,100));}
|
|
29
|
+
const result=await call('Runtime.evaluate',{awaitPromise:true,returnByValue:true,expression:`(async()=>{
|
|
30
|
+
const term=wrapper.term;
|
|
31
|
+
const write=s=>new Promise(r=>term.write(s,r));
|
|
32
|
+
const equal=(a,b)=>{if(JSON.stringify(a)!==JSON.stringify(b))throw Error(JSON.stringify({actual:a,expected:b}));};
|
|
33
|
+
await write('\\x1b[?1002h\\x1b[?1006h');
|
|
34
|
+
const screen=term.element.querySelector('.xterm-screen'), rect=screen.getBoundingClientRect();
|
|
35
|
+
const wheel=(deltaX,deltaY,mods={})=>{
|
|
36
|
+
sent.length=0;
|
|
37
|
+
const event=new WheelEvent('wheel',{clientX:rect.left+20,clientY:rect.top+20,deltaX,deltaY,deltaMode:1,bubbles:true,cancelable:true,...mods});
|
|
38
|
+
screen.dispatchEvent(event);
|
|
39
|
+
if(term._core.coreMouseService.areMouseEventsActive&&!event.defaultPrevented)throw Error('Wheel default was not prevented');
|
|
40
|
+
return sent.map(s=>Number(s.match(/<([0-9]+);/)[1]));
|
|
41
|
+
};
|
|
42
|
+
equal(wheel(-1,0),[66]);equal(wheel(1,0),[67]);
|
|
43
|
+
equal(wheel(0,-1,{shiftKey:true}),[68]);equal(wheel(0,1,{shiftKey:true}),[69]);
|
|
44
|
+
equal(wheel(0,-1),[64]);equal(wheel(0,1),[65]);
|
|
45
|
+
equal(wheel(1,-1),[67,64]);
|
|
46
|
+
equal(wheel(-1,0,{shiftKey:true,altKey:true,ctrlKey:true}),[94]);
|
|
47
|
+
for(let bits=0;bits<8;bits++) {
|
|
48
|
+
const mods={shiftKey:!!(bits&1),altKey:!!(bits&2),ctrlKey:!!(bits&4)};
|
|
49
|
+
for(const [dx,dy,code] of [[0,-1,64],[0,1,65],[-1,0,66],[1,0,67]])
|
|
50
|
+
equal(wheel(dx,dy,mods),[code+(bits<<2)]);
|
|
51
|
+
}
|
|
52
|
+
await write('\\x1b[?1002l');equal(wheel(1,0),[]);
|
|
53
|
+
wrapper.close();return 'ok';
|
|
54
|
+
})()`});
|
|
55
|
+
assert.equal(result.exceptionDetails,undefined,JSON.stringify(result.exceptionDetails));assert.equal(result.result.value,'ok');
|
|
56
|
+
}finally{
|
|
57
|
+
ws?.close();if(child.pid){try{process.kill(-child.pid,'SIGTERM');}catch{}}
|
|
58
|
+
server.closeAllConnections();await new Promise(r=>server.close(r));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-visible changes to jsmdcui are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.19.4] - 2026-10-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Catch failures during Go regexp conversion as well as compilation, and
|
|
10
|
+
bound the converter's nested-group recursion. An excessively nested rule
|
|
11
|
+
is disabled individually instead of throwing from `SyntaxDefinition` or
|
|
12
|
+
causing the loader to silently discard the entire syntax definition.
|
|
13
|
+
Other rules remain usable; this does not limit input line length.
|
|
14
|
+
|
|
15
|
+
## [0.19.3] - 2026-10-26
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Restore Dockerfile highlighting and improve compatibility with Micro's
|
|
20
|
+
syntax definitions by translating Go regular-expression flags and POSIX
|
|
21
|
+
character classes into JavaScript equivalents. Scoped flags retain their
|
|
22
|
+
scope rather than changing the entire expression.
|
|
23
|
+
- Fix nested highlighting regions, especially HTML and XML attributes:
|
|
24
|
+
closing a quoted attribute now returns to the surrounding tag, and closing
|
|
25
|
+
the tag returns to normal text. Unfinished attributes still carry their
|
|
26
|
+
highlighting state across lines.
|
|
27
|
+
- Replace recursive region scanning with an iterative scanner so long,
|
|
28
|
+
minified lines can be highlighted through their final token without stack
|
|
29
|
+
exhaustion or truncation.
|
|
30
|
+
- Prevent zero-width region cycles and cyclic syntax/state trees from looping
|
|
31
|
+
indefinitely. Zero-width matches advance safely over Unicode characters,
|
|
32
|
+
and NUL characters or isolated surrogate units do not leak region state.
|
|
33
|
+
These guards do not impose a timeout on an individual JavaScript RegExp.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Add syntax samples and a reproducible comparison against Micro's Go
|
|
38
|
+
highlighter, including JavaScript, Python and Markdown with upstream syntax
|
|
39
|
+
definitions. See [the comparison guide](tests/syntax-parity/README.md) for
|
|
40
|
+
generating the HTML comparison locally; generated HTML is not bundled.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- Markdown heading tests detect Bun's actual raw-HTML parsing behavior instead
|
|
45
|
+
of relying on version checks, accommodating the slash-tag behavior change
|
|
46
|
+
while preserving fail-closed coverage. The heading suites were verified on
|
|
47
|
+
Bun 1.4.0 through 1.4.4.
|
|
48
|
+
|
|
5
49
|
## [0.19.2] - 2026-09-21
|
|
6
50
|
|
|
7
51
|
### Added
|