buninu 0.5.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 +47 -0
- package/apps/jsgotty/LICENSES/MIT-jsmdcui.txt +22 -0
- package/apps/jsgotty/NOTICE +5 -0
- package/apps/jsgotty/README.en.md +21 -0
- package/apps/jsgotty/README.md +21 -0
- package/apps/jsgotty/client-modes.mjs +117 -0
- package/apps/jsgotty/client.mjs +100 -10
- package/apps/jsgotty/clipboard-powershell.js +18 -0
- package/apps/jsgotty/clipboard.js +148 -0
- package/apps/jsgotty/gotty.js +13 -0
- package/apps/jsgotty/package-lock.json +2 -2
- package/apps/jsgotty/package.json +1 -1
- package/apps/jsgotty/static/css/index.css +14 -1
- package/apps/jsgotty/static/js/gotty.js +107 -8
- package/apps/jsgotty/static/js/kitty.js +115 -7
- package/apps/jsgotty/terminal-modes.js +332 -0
- package/apps/jsgotty/tests/client-exit.test.mjs +45 -0
- package/apps/jsgotty/tests/client-modes.test.mjs +105 -0
- package/apps/jsgotty/tests/clipboard-browser.test.js +81 -0
- package/apps/jsgotty/tests/clipboard.test.js +75 -0
- package/apps/jsgotty/tests/manual/zoom-collector.cjs +55 -0
- package/apps/jsgotty/tests/manual/zoom-debug.js +39 -0
- package/apps/jsgotty/tests/manual-touch-diagnostics.js +33 -0
- package/apps/jsgotty/tests/manual-touch-diagnostics.mjs +22 -0
- package/apps/jsgotty/tests/nested-cli-pty.test.mjs +116 -0
- package/apps/jsgotty/tests/reconnect-modes-browser.test.js +144 -0
- package/apps/jsgotty/tests/reconnect-modes.md +44 -0
- package/apps/jsgotty/tests/reconnect-modes.test.js +166 -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,52 @@
|
|
|
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
|
+
|
|
29
|
+
## 0.6.0 - 2026-10-25
|
|
30
|
+
|
|
31
|
+
- Sync jsgotty to 1.3.0:
|
|
32
|
+
* Add OSC 52 clipboard reads and writes for browser terminals using the
|
|
33
|
+
server host's clipboard. Try xclip first on every platform, then available
|
|
34
|
+
platform tools, with an in-memory fallback when all tools fail. Only the
|
|
35
|
+
session writer performs clipboard operations; CLI clients pass OSC 52
|
|
36
|
+
through to their terminal emulator.
|
|
37
|
+
* Restore mouse reporting, mouse encoding, Kitty keyboard state and other
|
|
38
|
+
terminal modes when reconnecting to an existing session, including from
|
|
39
|
+
a fresh terminal and through nested CLI clients. Preserve keyboard mode
|
|
40
|
+
stacks without unnecessarily switching screens.
|
|
41
|
+
* Probe and restore the CLI client's original terminal modes on exit;
|
|
42
|
+
handle unsupported mode replies and independently enabled mouse modes
|
|
43
|
+
without leaving stale mouse reporting behind.
|
|
44
|
+
* Fix read-only CLI viewer exit shortcuts (Ctrl-C, Ctrl-D and Ctrl-Q) when
|
|
45
|
+
the enclosing terminal uses Kitty keyboard encoding.
|
|
46
|
+
* Fix Android WebView double-tap handling at non-default viewport zoom so
|
|
47
|
+
terminal clicks reach applications instead of triggering browser zoom,
|
|
48
|
+
while retaining two-finger pinch zoom.
|
|
49
|
+
|
|
3
50
|
## 0.5.0 - 2026-10-21
|
|
4
51
|
|
|
5
52
|
- Sync jsgotty to 1.2.0:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016-2020: Zachary Yedidia, et al.
|
|
4
|
+
Copyright (c) 2026 Dr. John (醫者小智)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/apps/jsgotty/NOTICE
CHANGED
|
@@ -119,3 +119,8 @@ such an executable must also follow Bun's license terms, including the LGPL
|
|
|
119
119
|
relinking provisions described at
|
|
120
120
|
https://github.com/oven-sh/bun/blob/main/LICENSE.md. The executables carry
|
|
121
121
|
LICENSE, NOTICE and LICENSES/ as embedded assets.
|
|
122
|
+
|
|
123
|
+
Clipboard backend helpers reference jsmdcui (MIT).
|
|
124
|
+
clipboard-powershell.js retains its powershellClipboard function verbatim from
|
|
125
|
+
jsmdcui/src/platform/clipboard.js, Copyright (c) 2026 Dr. John (醫者小智).
|
|
126
|
+
Full source license: LICENSES/MIT-jsmdcui.txt.
|
|
@@ -332,3 +332,24 @@ bun sz.js file.txt
|
|
|
332
332
|
- Bun is the primary runtime.
|
|
333
333
|
- On Linux / macOS, PTY support will not work unless the program is launched with Bun.
|
|
334
334
|
- GoTTY config file loading is not implemented yet.
|
|
335
|
+
|
|
336
|
+
## OSC 52 clipboard
|
|
337
|
+
|
|
338
|
+
The browser frontend supports OSC 52 writes and `?` queries against the **server host's clipboard**, not the browser device's clipboard. Only the session writer can request access; viewers do not duplicate operations. Query replies are sent directly back to the originating PTY as OSC 52 sequences.
|
|
339
|
+
|
|
340
|
+
Each operation tries `xclip` first, including on Windows and Android. Missing or failing commands fall through to applicable `termux-clipboard-*`, `wl-copy`/`wl-paste`, `xsel`, macOS `pbcopy`/`pbpaste`, then Windows PowerShell. The PowerShell function is copied verbatim from jsmdcui. If all tools fail, the server uses its in-memory cache. A successful empty read remains empty, rather than returning stale cached text. There is no polling; external commands run in a worker.
|
|
341
|
+
|
|
342
|
+
Supported selections: `c` (clipboard), `p` (primary), `q` (secondary); `s` maps to clipboard, and `0`–`7` are in-memory buffers. An empty selector uses `s0`. Tools without primary/secondary support do not substitute the clipboard. Requests are limited to 1 MiB.
|
|
343
|
+
|
|
344
|
+
```sh
|
|
345
|
+
# Write to the server clipboard
|
|
346
|
+
printf '\033]52;c;%s\007' "$(printf 'Hello' | base64 | tr -d '\n')"
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Applications can send `ESC ] 52 ; c ; ? ST` to read and receive a base64-encoded OSC 52 reply on stdin.
|
|
350
|
+
|
|
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
|
@@ -334,3 +334,24 @@ bun sz.js file.txt
|
|
|
334
334
|
- 以 Bun 為主要執行方式。
|
|
335
335
|
- Linux / macOS 如果不是用 Bun 執行,PTY 會無法使用。
|
|
336
336
|
- 目前未實作 Go 版的 config 檔載入
|
|
337
|
+
|
|
338
|
+
## OSC 52 剪貼板
|
|
339
|
+
|
|
340
|
+
瀏覽器版 jsgotty 支援 OSC 52 寫入與 `?` 查詢。操作的是 **server 所在主機**的剪貼板,不是瀏覽器裝置的剪貼板。只有 session 的 writer 可以執行;viewer 不會重複操作。查詢結果以 OSC 52 回覆直接送回原本的 PTY。
|
|
341
|
+
|
|
342
|
+
每次操作都先嘗試 `xclip`(包括 Windows/Android),不存在或執行失敗才依序嘗試適用的 `termux-clipboard-*`、`wl-copy`/`wl-paste`、`xsel`、macOS 的 `pbcopy`/`pbpaste`、Windows 的 PowerShell。PowerShell 函式原封不動取自 jsmdcui。所有工具都失敗時使用 server 記憶體快取;成功讀到空字串就是空剪貼板,不會被舊快取取代。沒有輪詢,外部工具在 worker 中執行。
|
|
343
|
+
|
|
344
|
+
支援 `c`(clipboard)、`p`(primary)、`q`(secondary);`s` 對應 clipboard,`0`~`7` 為記憶體暫存。空選擇使用 `s0`。不提供 primary/secondary 的工具不會拿 clipboard 冒充。單次 OSC 52 請求上限 1 MiB。
|
|
345
|
+
|
|
346
|
+
```sh
|
|
347
|
+
# 寫入 server 剪貼板
|
|
348
|
+
printf '\033]52;c;%s\007' "$(printf 'Hello' | base64 | tr -d '\n')"
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
程式可送出 `ESC ] 52 ; c ; ? ST` 讀取,並從 stdin 接收 base64 編碼的 OSC 52 回覆。
|
|
352
|
+
|
|
353
|
+
`jsgotty --client` **不攔截 OSC 52**,讀寫控制碼都原樣轉送,交給最外層真正的終端模擬器決定。
|
|
354
|
+
|
|
355
|
+
### 觸控拖曳與右鍵
|
|
356
|
+
|
|
357
|
+
在程式啟用滑鼠回報時,快速點兩下並按住第二下可拖曳左鍵,放開即釋放;單指滑動仍是捲動。既有圖片長按選單的 `Remove image` 下方新增 `Right click`,會在叫出選單的原始位置送出右鍵點擊。雙指縮放與原本的移除圖片入口不變。若要停用雙擊拖曳,可在網頁 JavaScript 設定 `window.touchDoubleTapDrag = false`。
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Read the terminal before attaching. No reset and no guessed default modes.
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import modes from './terminal-modes.js';
|
|
4
|
+
const {TerminalModeTracker, validateSnapshot, TRACKED_DEC_FLAGS, MOUSE_PROTOCOLS, MOUSE_ENCODINGS} = modes;
|
|
5
|
+
const ESC = '\x1b';
|
|
6
|
+
export class ModeProbe {
|
|
7
|
+
constructor(nonce = randomBytes(16).toString('hex')) {
|
|
8
|
+
this.nonce = nonce;
|
|
9
|
+
this.pending = Buffer.alloc(0);
|
|
10
|
+
this.complete = false;
|
|
11
|
+
this.kittyRestore = false;
|
|
12
|
+
this.state = new TerminalModeTracker().snapshot();
|
|
13
|
+
for (const key of Object.keys(this.state.dec)) this.state.dec[key] = null;
|
|
14
|
+
for (const key of ['alternate','cursorHidden','mouse','encoding']) this.state[key] = null;
|
|
15
|
+
this.state.kitty.normal.flags = this.state.kitty.alternate.flags = null;
|
|
16
|
+
this.state.mouseModeBits = Object.fromEntries(Object.keys(MOUSE_PROTOCOLS).map(n=>[n,null]));
|
|
17
|
+
this.state.mouseEncodingBits = Object.fromEntries(Object.keys(MOUSE_ENCODINGS).map(n=>[n,null]));
|
|
18
|
+
this.replies = new Map();
|
|
19
|
+
this.outstanding = new Set();
|
|
20
|
+
this.kittyOutstanding = false;
|
|
21
|
+
this.privatePrefix = `${ESC}]777;jsgotty-modes;${nonce};`;
|
|
22
|
+
}
|
|
23
|
+
privateQuery() { return `${ESC}]777;jsgotty-modes;${this.nonce}\x07`; }
|
|
24
|
+
standardQuery() {
|
|
25
|
+
this.outstanding = new Set([...Object.keys(TRACKED_DEC_FLAGS),25,1049,...Object.keys(MOUSE_PROTOCOLS),...Object.keys(MOUSE_ENCODINGS)].map(Number));
|
|
26
|
+
this.kittyOutstanding = true;
|
|
27
|
+
this.replyDeadline = Date.now() + 1500;
|
|
28
|
+
return [...this.outstanding]
|
|
29
|
+
.map(n => `${ESC}[?${n}$p`).join('') + `${ESC}[?u`;
|
|
30
|
+
}
|
|
31
|
+
reply(mode, value) {
|
|
32
|
+
if (this.complete) return;
|
|
33
|
+
const yes = value === 1 || value === 3;
|
|
34
|
+
// An unsupported mode cannot be active, but unlike silence it is a reply.
|
|
35
|
+
// Still aggregate below when 0 is the last answer in a mouse-mode group.
|
|
36
|
+
this.replies.set(mode, yes);
|
|
37
|
+
if (MOUSE_PROTOCOLS[mode]) this.state.mouseModeBits[mode] = yes;
|
|
38
|
+
if (MOUSE_ENCODINGS[mode]) this.state.mouseEncodingBits[mode] = yes;
|
|
39
|
+
if (value !== 0) {
|
|
40
|
+
if (TRACKED_DEC_FLAGS[mode]) this.state.dec[TRACKED_DEC_FLAGS[mode]] = yes;
|
|
41
|
+
else if (mode === 25) this.state.cursorHidden = !yes;
|
|
42
|
+
else if (mode === 1049) this.state.alternate = yes;
|
|
43
|
+
}
|
|
44
|
+
for (const [name, table, fallback, priority] of [
|
|
45
|
+
['mouse',MOUSE_PROTOCOLS,'NONE',[1003,1002,1000,9]],
|
|
46
|
+
['encoding',MOUSE_ENCODINGS,'DEFAULT',[1016,1006]],
|
|
47
|
+
]) {
|
|
48
|
+
const active = priority.find(key => this.replies.get(key) === true);
|
|
49
|
+
if (active) this.state[name] = table[active];
|
|
50
|
+
else if (Object.keys(table).every(key => this.replies.has(+key))) this.state[name] = fallback;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
consume(chunk) {
|
|
54
|
+
if ((this.complete && !this.outstanding.size && !this.kittyOutstanding) ||
|
|
55
|
+
(this.replyDeadline && Date.now() > this.replyDeadline)) {
|
|
56
|
+
this.outstanding.clear(); this.kittyOutstanding = false;
|
|
57
|
+
return Buffer.concat([this.flush(), Buffer.from(chunk)]);
|
|
58
|
+
}
|
|
59
|
+
this.pending = Buffer.concat([this.pending, Buffer.from(chunk)]);
|
|
60
|
+
const out = [];
|
|
61
|
+
while (this.pending.length) {
|
|
62
|
+
const text = this.pending.toString('latin1');
|
|
63
|
+
if (text.startsWith(this.privatePrefix)) {
|
|
64
|
+
const end = text.indexOf('\x07', this.privatePrefix.length);
|
|
65
|
+
if (end < 0 && text.length < 16384) break;
|
|
66
|
+
if (end >= 0) {
|
|
67
|
+
try {
|
|
68
|
+
const reply = JSON.parse(Buffer.from(text.slice(this.privatePrefix.length,end),'base64').toString());
|
|
69
|
+
this.state = validateSnapshot(reply);
|
|
70
|
+
this.kittyRestore = reply.kittyRestore === 1;
|
|
71
|
+
this.complete = true;
|
|
72
|
+
} catch {}
|
|
73
|
+
this.pending = this.pending.subarray(end+1); continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const dec = /^\x1b\[\?(\d+);([0-4])\$y/.exec(text);
|
|
77
|
+
const kitty = /^\x1b\[\?(\d+)u/.exec(text);
|
|
78
|
+
if ((dec && this.outstanding.delete(+dec[1])) || (kitty && this.kittyOutstanding)) {
|
|
79
|
+
if (dec) this.reply(+dec[1],+dec[2]);
|
|
80
|
+
else {
|
|
81
|
+
this.kittyOutstanding = false;
|
|
82
|
+
if (!this.complete && this.state.alternate != null)
|
|
83
|
+
this.state.kitty[this.state.alternate?'alternate':'normal'].flags = +kitty[1] & 31;
|
|
84
|
+
}
|
|
85
|
+
this.pending = this.pending.subarray((dec||kitty)[0].length); continue;
|
|
86
|
+
}
|
|
87
|
+
// Hold only possible query replies; normal keystrokes and mouse packets pass unchanged.
|
|
88
|
+
if (this.privatePrefix.startsWith(text) || (text.length < 64 && /^\x1b\[\?[0-9;\$]*$/.test(text)) || text === ESC+'[') break;
|
|
89
|
+
// Forward ordinary input in spans; large pastes must not become quadratic.
|
|
90
|
+
const next = this.pending.indexOf(0x1b, 1);
|
|
91
|
+
const count = next < 0 ? this.pending.length : next;
|
|
92
|
+
out.push(this.pending.subarray(0,count));this.pending=this.pending.subarray(count);
|
|
93
|
+
}
|
|
94
|
+
return Buffer.concat(out);
|
|
95
|
+
}
|
|
96
|
+
flush() { const data=this.pending;this.pending=Buffer.alloc(0);return data; }
|
|
97
|
+
}
|
|
98
|
+
export async function captureTerminalModes(input, output, {timeout=1000, probe=new ModeProbe()}={}) {
|
|
99
|
+
const buffered=[];
|
|
100
|
+
if (!input.isTTY || !output.isTTY) {
|
|
101
|
+
probe.replyDeadline = 1;
|
|
102
|
+
return {state:probe.state, probe, buffered:Buffer.alloc(0)};
|
|
103
|
+
}
|
|
104
|
+
const raw=!!input.isRaw;
|
|
105
|
+
input.setRawMode(true);
|
|
106
|
+
await new Promise(resolve=>{
|
|
107
|
+
let finished=false;
|
|
108
|
+
const finish=()=>{if(finished)return;finished=true;clearTimeout(fallback);clearTimeout(end);input.off('data',data);resolve();};
|
|
109
|
+
const data=chunk=>{const rest=probe.consume(chunk);if(rest.length)buffered.push(rest);if(probe.complete)finish();};
|
|
110
|
+
const fallback=setTimeout(()=>output.write(probe.standardQuery()),Math.min(200,timeout/2));
|
|
111
|
+
const end=setTimeout(finish,timeout);
|
|
112
|
+
input.on('data',data);input.resume();output.write(probe.privateQuery());
|
|
113
|
+
});
|
|
114
|
+
input.pause();input.setRawMode(raw);
|
|
115
|
+
buffered.push(probe.flush());
|
|
116
|
+
return {state:validateSnapshot(probe.state),probe,buffered:Buffer.concat(buffered)};
|
|
117
|
+
}
|
package/apps/jsgotty/client.mjs
CHANGED
|
@@ -4,6 +4,10 @@ import process from "node:process";
|
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
5
|
import WebSocket from "ws";
|
|
6
6
|
import { encodeKittyGraphics, syntheticKittyCursorMotion } from "./client-kitty.mjs";
|
|
7
|
+
import terminalModes from "./terminal-modes.js";
|
|
8
|
+
|
|
9
|
+
const { TerminalModeTracker, modeTransition, validateSnapshot } = terminalModes;
|
|
10
|
+
import { captureTerminalModes } from "./client-modes.mjs";
|
|
7
11
|
|
|
8
12
|
const MSG_INPUT = "1";
|
|
9
13
|
const MSG_PING = "2";
|
|
@@ -19,6 +23,7 @@ const MSG_SET_BUFFER_SIZE = "6";
|
|
|
19
23
|
const MSG_KITTY_GRAPHICS = "7";
|
|
20
24
|
const MSG_SET_ROLE = "a";
|
|
21
25
|
const MSG_CONTROL_RESULT = "b";
|
|
26
|
+
const MSG_RESTORE_MODES = "m";
|
|
22
27
|
|
|
23
28
|
const DEFAULT_URL = "ws://127.0.0.1:8080/ws";
|
|
24
29
|
|
|
@@ -409,6 +414,40 @@ async function main() {
|
|
|
409
414
|
return;
|
|
410
415
|
}
|
|
411
416
|
const args = buildArguments(options, initArguments);
|
|
417
|
+
const originalRawMode = !!process.stdin.isRaw;
|
|
418
|
+
const localModes = new TerminalModeTracker();
|
|
419
|
+
// A closed terminal surfaces as an async EIO/EPIPE, not a throw. Nobody is
|
|
420
|
+
// left to read anything, and the PTY keeps its modes, so just exit.
|
|
421
|
+
for (const stream of [process.stdin, process.stdout, process.stderr]) {
|
|
422
|
+
stream.on("error", (error) => {
|
|
423
|
+
if (error.code !== "EIO" && error.code !== "EPIPE") {
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
process.exit(process.exitCode ?? 1);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
const writeTerminal = (data) => {
|
|
430
|
+
process.stdout.write(data);
|
|
431
|
+
localModes.consume(Buffer.from(data));
|
|
432
|
+
};
|
|
433
|
+
const captured = await captureTerminalModes(process.stdin, process.stdout);
|
|
434
|
+
const originalModes = captured.state;
|
|
435
|
+
Object.assign(localModes, validateSnapshot(originalModes));
|
|
436
|
+
let bufferedInput = captured.buffered;
|
|
437
|
+
let probeFlushTimer, replayingProbe = false;
|
|
438
|
+
const terminalInput = chunk => {
|
|
439
|
+
if (replayingProbe) return Buffer.from(chunk);
|
|
440
|
+
const input = captured.probe.consume(chunk);
|
|
441
|
+
clearTimeout(probeFlushTimer);
|
|
442
|
+
// A lone Escape is user input too. Never hold it indefinitely while
|
|
443
|
+
// filtering a split or late response to our startup queries.
|
|
444
|
+
if (captured.probe.pending.length) probeFlushTimer = setTimeout(() => {
|
|
445
|
+
const pending = captured.probe.flush();
|
|
446
|
+
replayingProbe = true;
|
|
447
|
+
try { process.stdin.emit("data", pending); } finally { replayingProbe = false; }
|
|
448
|
+
}, 30);
|
|
449
|
+
return input;
|
|
450
|
+
};
|
|
412
451
|
writeStatus(`[gotty] connecting to\n ${wsUrl}`);
|
|
413
452
|
const ws = new WebSocket(wsUrl, "webtty");
|
|
414
453
|
let pingTimer = null;
|
|
@@ -432,7 +471,8 @@ async function main() {
|
|
|
432
471
|
if (!writable || ws.readyState !== WebSocket.OPEN) {
|
|
433
472
|
return;
|
|
434
473
|
}
|
|
435
|
-
|
|
474
|
+
const input = terminalInput(chunk);
|
|
475
|
+
if (input.length) ws.send(MSG_INPUT + input.toString("base64"));
|
|
436
476
|
};
|
|
437
477
|
|
|
438
478
|
const handleResize = () => {
|
|
@@ -441,21 +481,52 @@ async function main() {
|
|
|
441
481
|
}
|
|
442
482
|
};
|
|
443
483
|
|
|
484
|
+
let exiting = false;
|
|
444
485
|
const exitClient = (code = 0) => {
|
|
486
|
+
if (exiting) return;
|
|
487
|
+
exiting = true;
|
|
445
488
|
cleanup();
|
|
446
489
|
reportDisconnected();
|
|
447
490
|
ws.close();
|
|
448
|
-
process.exit(code);
|
|
491
|
+
process.stdout.write("", () => process.exit(code));
|
|
449
492
|
};
|
|
450
493
|
|
|
494
|
+
// Restoring the viewed session also restores Kitty keyboard modes. Exit
|
|
495
|
+
// keys can therefore arrive as CSI u rather than legacy control bytes.
|
|
496
|
+
// Keep CSI packets across chunks; never treat a key-release as a press.
|
|
497
|
+
let viewerSequence = "";
|
|
451
498
|
const handleViewerInput = (chunk) => {
|
|
452
|
-
for (const byte of
|
|
453
|
-
if (
|
|
454
|
-
|
|
499
|
+
for (const byte of terminalInput(chunk)) {
|
|
500
|
+
if (viewerSequence) {
|
|
501
|
+
viewerSequence += String.fromCharCode(byte);
|
|
502
|
+
if (viewerSequence === "\x1b[") continue;
|
|
503
|
+
if (viewerSequence.length === 2 && byte !== 0x5b) {
|
|
504
|
+
viewerSequence = "";
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (viewerSequence.length > 2 && byte >= 0x40 && byte <= 0x7e) {
|
|
508
|
+
const sequence = viewerSequence;
|
|
509
|
+
viewerSequence = "";
|
|
510
|
+
const kitty = /^\x1b\[(\d+)(?::[0-9:]*)?(?:;(\d+)(?::([123]))?)?(?:;[0-9:]+)?u$/.exec(sequence);
|
|
511
|
+
const modified = /^\x1b\[27;(\d+);(\d+)~$/.exec(sequence);
|
|
512
|
+
const key = kitty ? +kitty[1] : modified ? +modified[2] : 0;
|
|
513
|
+
const modifiers = (kitty ? +(kitty[2] || 1) : modified ? +modified[1] : 1) - 1;
|
|
514
|
+
// Ignore Caps/Num Lock, but require Ctrl without Alt/Shift/Super.
|
|
515
|
+
if ((modifiers & ~192) === 4 && (!kitty || kitty[3] !== "3")) {
|
|
516
|
+
if (key === 99) return exitClient(130);
|
|
517
|
+
if (key === 100 || key === 113) return exitClient(0);
|
|
518
|
+
}
|
|
519
|
+
} else if (viewerSequence.length > 128) {
|
|
520
|
+
viewerSequence = "";
|
|
521
|
+
}
|
|
522
|
+
continue;
|
|
455
523
|
}
|
|
456
|
-
if (byte ===
|
|
457
|
-
|
|
524
|
+
if (byte === 0x1b) {
|
|
525
|
+
viewerSequence = "\x1b";
|
|
526
|
+
continue;
|
|
458
527
|
}
|
|
528
|
+
if (byte === 0x03) return exitClient(130);
|
|
529
|
+
if (byte === 0x04 || byte === 0x11) return exitClient(0);
|
|
459
530
|
}
|
|
460
531
|
};
|
|
461
532
|
|
|
@@ -498,7 +569,8 @@ async function main() {
|
|
|
498
569
|
return;
|
|
499
570
|
}
|
|
500
571
|
|
|
501
|
-
const input =
|
|
572
|
+
const input = terminalInput(chunk);
|
|
573
|
+
if (!input.length) return;
|
|
502
574
|
fallbackProbe = normalizeEcho(input.toString("utf8"));
|
|
503
575
|
ws.send(MSG_INPUT + input.toString("base64"));
|
|
504
576
|
stopFallbackInput();
|
|
@@ -532,6 +604,10 @@ async function main() {
|
|
|
532
604
|
setRawMode(true);
|
|
533
605
|
rawEnabled = process.stdin.isTTY;
|
|
534
606
|
sendResize(ws, options);
|
|
607
|
+
if (bufferedInput.length) {
|
|
608
|
+
ws.send(MSG_INPUT + bufferedInput.toString("base64"));
|
|
609
|
+
bufferedInput = Buffer.alloc(0);
|
|
610
|
+
}
|
|
535
611
|
return;
|
|
536
612
|
}
|
|
537
613
|
|
|
@@ -558,6 +634,7 @@ pid:
|
|
|
558
634
|
return;
|
|
559
635
|
}
|
|
560
636
|
closed = true;
|
|
637
|
+
clearTimeout(probeFlushTimer);
|
|
561
638
|
if (pingTimer) {
|
|
562
639
|
clearInterval(pingTimer);
|
|
563
640
|
pingTimer = null;
|
|
@@ -565,8 +642,13 @@ pid:
|
|
|
565
642
|
setWritable(false);
|
|
566
643
|
stopFallbackInput();
|
|
567
644
|
stopViewerInput();
|
|
645
|
+
if (process.stdout.isTTY) {
|
|
646
|
+
try {
|
|
647
|
+
writeTerminal(modeTransition(localModes.snapshot(), originalModes, { normalizeMouse: true, kittyRestore: captured.probe.kittyRestore }));
|
|
648
|
+
} catch {}
|
|
649
|
+
}
|
|
568
650
|
if (rawEnabled) {
|
|
569
|
-
setRawMode(
|
|
651
|
+
setRawMode(originalRawMode);
|
|
570
652
|
rawEnabled = false;
|
|
571
653
|
}
|
|
572
654
|
process.stdin.pause();
|
|
@@ -611,7 +693,7 @@ pid:
|
|
|
611
693
|
break;
|
|
612
694
|
}
|
|
613
695
|
pendingKittyCursorMotion = "";
|
|
614
|
-
|
|
696
|
+
writeTerminal(output);
|
|
615
697
|
if (fallbackProbe && accessRole === "") {
|
|
616
698
|
fallbackOutputTail = normalizeEcho(
|
|
617
699
|
`${fallbackOutputTail}${output.toString("utf8")}`
|
|
@@ -641,6 +723,14 @@ pid:
|
|
|
641
723
|
if (encoded) process.stdout.write(encoded);
|
|
642
724
|
} catch {}
|
|
643
725
|
break;
|
|
726
|
+
case MSG_RESTORE_MODES:
|
|
727
|
+
// Only local terminal state; the PTY keeps its modes for the next resume.
|
|
728
|
+
if (process.stdout.isTTY) {
|
|
729
|
+
try {
|
|
730
|
+
writeTerminal(modeTransition(localModes.snapshot(), validateSnapshot(JSON.parse(payload)), { normalizeMouse: true, kittyRestore: captured.probe.kittyRestore }));
|
|
731
|
+
} catch {}
|
|
732
|
+
}
|
|
733
|
+
break;
|
|
644
734
|
case MSG_SET_RECONNECT: {
|
|
645
735
|
try {
|
|
646
736
|
const reconnect = JSON.parse(payload);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copied verbatim from jsmdcui/src/platform/clipboard.js (MIT).
|
|
3
|
+
// Keep the function below unchanged; inject its existing command helpers.
|
|
4
|
+
module.exports = function createPowerShellClipboard(shell, runSync, outputOrThrow) {
|
|
5
|
+
function powershellClipboard(shell) {
|
|
6
|
+
return {
|
|
7
|
+
name: "powershell",
|
|
8
|
+
// Get-Clipboard -Raw appends \r\n to stdout; strip exactly one trailing line ending.
|
|
9
|
+
read: () => outputOrThrow(runSync([shell, "-NoProfile", "-Command", "Get-Clipboard -Raw"], {})).replace(/\r?\n$/, ""),
|
|
10
|
+
write: (text) => {
|
|
11
|
+
text=(text+'').replaceAll("'","''") ;
|
|
12
|
+
return runSync([shell, "-NoProfile", "-Command", `Set-Clipboard '${text}'`], { stdout: "ignore" }).ok ;
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return powershellClipboard(shell);
|
|
18
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const createPowerShellClipboard = require('./clipboard-powershell.js');
|
|
3
|
+
// Self-contained so the worker remains valid after bundling/minification.
|
|
4
|
+
function clipboardRuntime(createPowerShellClipboard) {
|
|
5
|
+
const {spawnSync} = require('node:child_process');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const MAX_OSC52 = 1024 * 1024;
|
|
9
|
+
function which(name) {
|
|
10
|
+
if (typeof Bun !== 'undefined') return Bun.which(name);
|
|
11
|
+
const names=process.platform==='win32' && !path.extname(name)
|
|
12
|
+
? [name,...(process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';').map(ext=>name+ext)] : [name];
|
|
13
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
14
|
+
for(const candidate of names) {
|
|
15
|
+
const file=path.join(dir,candidate);
|
|
16
|
+
try {fs.accessSync(file,fs.constants.X_OK);if(fs.statSync(file).isFile())return file;} catch {}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
function runSync(command, options={}) {
|
|
22
|
+
try {
|
|
23
|
+
const r=spawnSync(command[0],command.slice(1),{
|
|
24
|
+
input:options.stdin, encoding:'utf8', timeout:options.timeout ?? 2000,
|
|
25
|
+
maxBuffer:MAX_OSC52, stdio:['pipe',options.stdout ?? 'pipe','pipe'],windowsHide:true,
|
|
26
|
+
});
|
|
27
|
+
return {ok:!r.error && r.status===0,stdout:r.stdout || '',stderr:r.stderr || String(r.error || '')};
|
|
28
|
+
} catch(error) {return {ok:false,stdout:'',stderr:String(error)};}
|
|
29
|
+
}
|
|
30
|
+
function outputOrThrow(result) {
|
|
31
|
+
if (!result.ok) throw new Error(result.stderr || result.stdout || 'clipboard command failed');
|
|
32
|
+
return result.stdout;
|
|
33
|
+
}
|
|
34
|
+
function createClipboard({find=which,run=runSync,platform=process.platform}={}) {
|
|
35
|
+
const internal=new Map();
|
|
36
|
+
function candidates(register,write,text) {
|
|
37
|
+
const result=[];
|
|
38
|
+
const command=(name,args)=>{const executable=find(name);if(executable)result.push(()=>run([executable,...args],write?{stdin:text,stdout:'ignore'}:{}));};
|
|
39
|
+
// Always first, including Windows and Android. Failure does not pin a backend.
|
|
40
|
+
command('xclip',['-selection',register,...(write?[]:['-o'])]);
|
|
41
|
+
if(register==='clipboard') {
|
|
42
|
+
command(write?'termux-clipboard-set':'termux-clipboard-get',[]);
|
|
43
|
+
}
|
|
44
|
+
if(register!=='secondary')command(write?'wl-copy':'wl-paste',[...(write?[]:['--no-newline']),...(register==='primary'?['--primary']:[])]);
|
|
45
|
+
command('xsel',[`--${register}`,write?'--input':'--output']);
|
|
46
|
+
if(register==='clipboard') {
|
|
47
|
+
if(platform==='darwin')command(write?'pbcopy':'pbpaste',[]);
|
|
48
|
+
if(platform==='win32')for(const name of ['pwsh.exe','powershell.exe','pwsh','powershell']){
|
|
49
|
+
const shell=find(name);if(!shell)continue;
|
|
50
|
+
const backend=createPowerShellClipboard(shell,run,outputOrThrow);
|
|
51
|
+
result.push(()=>write?{ok:backend.write(text)}:{ok:true,stdout:backend.read()});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
read(register='clipboard') {
|
|
58
|
+
if(/^cut[0-7]$/.test(register))return internal.get(register);
|
|
59
|
+
for(const attempt of candidates(register,false))try {
|
|
60
|
+
const r=attempt();if(r.ok){internal.set(register,r.stdout);return r.stdout;}
|
|
61
|
+
}catch{}
|
|
62
|
+
return internal.get(register);
|
|
63
|
+
},
|
|
64
|
+
write(text,register='clipboard') {
|
|
65
|
+
internal.set(register,text);
|
|
66
|
+
if(/^cut[0-7]$/.test(register))return;
|
|
67
|
+
for(const attempt of candidates(register,true,text))try {if(attempt().ok)return;}catch{}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function parseOSC52(data) {
|
|
72
|
+
if(typeof data!=='string'||data.length>MAX_OSC52)return null;
|
|
73
|
+
const i=data.indexOf(';');if(i<0)return null;
|
|
74
|
+
const selection=data.slice(0,i),payload=data.slice(i+1);
|
|
75
|
+
if(!/^[cpqs0-7]*$/.test(selection)||selection.length>16)return null;
|
|
76
|
+
const registers=[...new Set([...(selection || 's0')].map(c=>({c:'clipboard',p:'primary',q:'secondary',s:'clipboard'}[c]||`cut${c}`)))];
|
|
77
|
+
if(payload==='?')return {selection,registers,read:true};
|
|
78
|
+
// OSC 52 specifies clearing the selection for non-base64 data.
|
|
79
|
+
const valid=/^[A-Za-z0-9+/]*={0,2}$/.test(payload) && payload.replace(/=+$/,'').length % 4 !== 1;
|
|
80
|
+
return {selection,registers,read:false,text:valid?Buffer.from(payload,'base64').toString('utf8'):''};
|
|
81
|
+
}
|
|
82
|
+
function executeOSC52(clipboard,request) {
|
|
83
|
+
if(request.read) {
|
|
84
|
+
let text;
|
|
85
|
+
for(const register of request.registers){text=clipboard.read(register);if(text!==undefined)break;}
|
|
86
|
+
const data=Buffer.from(text ?? '', 'utf8');
|
|
87
|
+
if(data.length>MAX_OSC52)return null;
|
|
88
|
+
return `\x1b]52;${request.selection};${data.toString('base64')}\x1b\\`;
|
|
89
|
+
}
|
|
90
|
+
for(const register of request.registers)clipboard.write(request.text,register);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return {createClipboard,parseOSC52,executeOSC52,MAX_OSC52};
|
|
94
|
+
}
|
|
95
|
+
const {createClipboard,parseOSC52,executeOSC52,MAX_OSC52}=clipboardRuntime(createPowerShellClipboard);
|
|
96
|
+
module.exports={createClipboard,parseOSC52,executeOSC52,MAX_OSC52};
|
|
97
|
+
|
|
98
|
+
// Run the synchronous jsmdcui-compatible backends off the server event loop.
|
|
99
|
+
// Inline the helpers so compiled single-file builds need no external worker file.
|
|
100
|
+
class ClipboardHost {
|
|
101
|
+
constructor(){this.pending=new Map();this.nextId=0;this.worker=null;}
|
|
102
|
+
execute(request){
|
|
103
|
+
if(this.pending.size>=32)return Promise.reject(Error('clipboard queue full'));
|
|
104
|
+
if(!this.worker){
|
|
105
|
+
const {Worker}=require('node:worker_threads');
|
|
106
|
+
const source=`const {parentPort}=require('node:worker_threads');
|
|
107
|
+
const runtime=(${clipboardRuntime.toString()})(${createPowerShellClipboard.toString()});
|
|
108
|
+
const clipboard=runtime.createClipboard();parentPort.on('message',({id,request})=>{
|
|
109
|
+
try{parentPort.postMessage({id,result:runtime.executeOSC52(clipboard,request)});}
|
|
110
|
+
catch(error){parentPort.postMessage({id,error:String(error)});}
|
|
111
|
+
});`;
|
|
112
|
+
const worker=this.worker=new Worker(source,{eval:true});
|
|
113
|
+
const failed=error=>{
|
|
114
|
+
if(this.worker!==worker)return;
|
|
115
|
+
this.worker=null;
|
|
116
|
+
for(const job of this.pending.values())job.reject(error);
|
|
117
|
+
this.pending.clear();worker.terminate();
|
|
118
|
+
};
|
|
119
|
+
worker.on('error',failed);
|
|
120
|
+
worker.on('exit',()=>failed(Error('clipboard worker exited')));
|
|
121
|
+
worker.on('message',({id,result,error})=>{
|
|
122
|
+
const job=this.pending.get(id);if(!job)return;
|
|
123
|
+
this.pending.delete(id);error?job.reject(Error(error)):job.resolve(result);
|
|
124
|
+
if(!this.pending.size)worker.unref();
|
|
125
|
+
});
|
|
126
|
+
worker.unref();
|
|
127
|
+
}
|
|
128
|
+
return new Promise((resolve,reject)=>{
|
|
129
|
+
const id=++this.nextId;this.pending.set(id,{resolve,reject});this.worker.ref();
|
|
130
|
+
this.worker.postMessage({id,request});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async close(){
|
|
134
|
+
const worker=this.worker;this.worker=null;
|
|
135
|
+
for(const job of this.pending.values())job.reject(Error('clipboard closed'));
|
|
136
|
+
this.pending.clear();if(worker)await worker.terminate();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Authorization is checked both before host access and before returning data.
|
|
140
|
+
function handleOSC52(session,ws,payload,host){
|
|
141
|
+
if(!session.isWriter(ws))return;
|
|
142
|
+
const request=parseOSC52(payload);if(!request)return;
|
|
143
|
+
host.execute(request).then(reply=>{
|
|
144
|
+
if(reply && !session.closed && session.isWriter(ws))session.backend.write(Buffer.from(reply));
|
|
145
|
+
}).catch(()=>{});
|
|
146
|
+
}
|
|
147
|
+
module.exports.ClipboardHost=ClipboardHost;
|
|
148
|
+
module.exports.handleOSC52=handleOSC52;
|