buninu 0.4.21 → 0.6.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 +40 -0
- package/apps/bunmsh/CHANGELOG.md +4 -0
- package/apps/bunmsh/package.json +1 -1
- package/apps/bunmsh/src/shell.js +1 -1
- package/apps/jsgotty/LICENSES/MIT-jsmdcui.txt +22 -0
- package/apps/jsgotty/NOTICE +5 -0
- package/apps/jsgotty/README.en.md +57 -0
- package/apps/jsgotty/README.md +57 -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 +17 -2
- package/apps/jsgotty/package-lock.json +2 -2
- package/apps/jsgotty/package.json +1 -1
- package/apps/jsgotty/static/css/index.css +7 -1
- package/apps/jsgotty/static/index.html +1 -0
- package/apps/jsgotty/static/js/gotty.js +175 -19
- package/apps/jsgotty/static/js/kitty-keyboard.js +193 -0
- package/apps/jsgotty/terminal-modes.js +332 -0
- package/apps/jsgotty/tests/android-alt.test.js +115 -0
- package/apps/jsgotty/tests/arrow-modifiers.test.js +80 -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/kitty-keyboard-browser.test.js +53 -0
- package/apps/jsgotty/tests/kitty-keyboard.test.js +102 -0
- package/apps/jsgotty/tests/manual/README.md +20 -0
- package/apps/jsgotty/tests/manual/keyboard-collector.cjs +55 -0
- package/apps/jsgotty/tests/manual/keyboard-debug.js +74 -0
- package/apps/jsgotty/tests/manual/kitty-keyboard.js +80 -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/package.json +1 -1
|
@@ -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;
|
package/apps/jsgotty/gotty.js
CHANGED
|
@@ -12,7 +12,10 @@ const crypto = require("crypto");
|
|
|
12
12
|
const zlib = require("zlib");
|
|
13
13
|
const { constants: fsConstants } = require("fs");
|
|
14
14
|
const WebSocket = require("ws");
|
|
15
|
+
const { TerminalModeTracker } = require("./terminal-modes.js");
|
|
15
16
|
const { WebSocketServer } = WebSocket;
|
|
17
|
+
const { ClipboardHost, handleOSC52 } = require("./clipboard.js");
|
|
18
|
+
const clipboardHost = new ClipboardHost();
|
|
16
19
|
|
|
17
20
|
const MSG_INPUT = "1";
|
|
18
21
|
const MSG_PING = "2";
|
|
@@ -34,6 +37,7 @@ const MSG_ZMODEM_MODE = "z";
|
|
|
34
37
|
const MSG_ZMODEM_OUTPUT = "y";
|
|
35
38
|
const MSG_ZMODEM_ACK = "Y";
|
|
36
39
|
const MSG_KITTY_DEBUG = "K";
|
|
40
|
+
const MSG_RESTORE_MODES = "m";
|
|
37
41
|
const KITTY_TRACE = /^(1|true|yes|on)$/i.test(String(process.env.JSGOTTY_KITTY_DEBUG || ""));
|
|
38
42
|
const WINDOWS_BRIDGE_TRACE = process.env.WINDOWS_BRIDGE_TRACE === "1";
|
|
39
43
|
const WINDOWS_BRIDGE_PREFIX = Buffer.from("@@GOTTYCTL:", "ascii");
|
|
@@ -1001,7 +1005,9 @@ class CursorStateTracker {
|
|
|
1001
1005
|
}
|
|
1002
1006
|
|
|
1003
1007
|
class KittyGraphicsParser {
|
|
1004
|
-
constructor() {
|
|
1008
|
+
constructor({ binaryData = false } = {}) {
|
|
1009
|
+
// Local renderers can consume bytes directly; browser clients retain base64.
|
|
1010
|
+
this.binaryData = binaryData;
|
|
1005
1011
|
this.pending = Buffer.alloc(0);
|
|
1006
1012
|
this.transfers = new Map();
|
|
1007
1013
|
this.images = new Map();
|
|
@@ -1220,7 +1226,7 @@ class KittyGraphicsParser {
|
|
|
1220
1226
|
width: Number.parseInt(existing.control.s || "0", 10),
|
|
1221
1227
|
height: Number.parseInt(existing.control.v || "0", 10),
|
|
1222
1228
|
mime: existing.control.U || existing.control.mime || null,
|
|
1223
|
-
data: binary.toString("base64")
|
|
1229
|
+
data: this.binaryData ? binary : binary.toString("base64")
|
|
1224
1230
|
};
|
|
1225
1231
|
|
|
1226
1232
|
this.images.set(imageId, image);
|
|
@@ -1678,6 +1684,7 @@ class PtySession {
|
|
|
1678
1684
|
this.supportsWindowsBridge = process.platform === "win32";
|
|
1679
1685
|
this.cursorTracker = new CursorStateTracker();
|
|
1680
1686
|
this.cursorTracker.setDimensions(this.columns || 80);
|
|
1687
|
+
this.modeTracker = new TerminalModeTracker();
|
|
1681
1688
|
this.kittyParser = new KittyGraphicsParser();
|
|
1682
1689
|
this.windowsBridgeParser = this.supportsWindowsBridge ? new WindowsBridgeParser("incoming") : null;
|
|
1683
1690
|
this.outgoingWindowsBridgeParser = this.supportsWindowsBridge ? new WindowsBridgeParser("outgoing") : null;
|
|
@@ -1757,6 +1764,7 @@ class PtySession {
|
|
|
1757
1764
|
if (!this.supportsWindowsBridge) {
|
|
1758
1765
|
const cursorBefore = this.cursorTracker.snapshot();
|
|
1759
1766
|
this.cursorTracker.consume(filteredChunk);
|
|
1767
|
+
this.modeTracker.consume(filteredChunk);
|
|
1760
1768
|
const cursorAfter = this.cursorTracker.snapshot();
|
|
1761
1769
|
const controls = cursorControls(filteredChunk);
|
|
1762
1770
|
if (controls.length > 0) {
|
|
@@ -1774,6 +1782,7 @@ class PtySession {
|
|
|
1774
1782
|
if (bridged.plain.length > 0) {
|
|
1775
1783
|
const cursorBefore = this.cursorTracker.snapshot();
|
|
1776
1784
|
this.cursorTracker.consume(bridged.plain);
|
|
1785
|
+
this.modeTracker.consume(bridged.plain);
|
|
1777
1786
|
const cursorAfter = this.cursorTracker.snapshot();
|
|
1778
1787
|
const controls = cursorControls(bridged.plain);
|
|
1779
1788
|
if (controls.length > 0) {
|
|
@@ -1853,6 +1862,9 @@ class PtySession {
|
|
|
1853
1862
|
this.writerWs = ws;
|
|
1854
1863
|
}
|
|
1855
1864
|
this.sendSessionMetadata(ws, remoteAddr);
|
|
1865
|
+
// Queued ahead of live output; the client applies only modes that differ,
|
|
1866
|
+
// which also clears modes a program turned off while the socket was down.
|
|
1867
|
+
this.send(MSG_RESTORE_MODES, JSON.stringify(this.modeTracker.snapshot()), ws);
|
|
1856
1868
|
this.startWebSocketHandlers(ws);
|
|
1857
1869
|
this.notifyRoles();
|
|
1858
1870
|
return true;
|
|
@@ -2440,6 +2452,9 @@ class PtySession {
|
|
|
2440
2452
|
}
|
|
2441
2453
|
|
|
2442
2454
|
switch (type) {
|
|
2455
|
+
case "C":
|
|
2456
|
+
handleOSC52(this, ws, payload, clipboardHost);
|
|
2457
|
+
break;
|
|
2443
2458
|
case MSG_INPUT:
|
|
2444
2459
|
if (!this.isWriter(ws) || payload.length === 0) {
|
|
2445
2460
|
return;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsgotty",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "jsgotty",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.3.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"node-addon-api": "^7.1.1",
|
|
12
12
|
"ws": "^8.18.3"
|
|
@@ -10,4 +10,10 @@ html, body, #terminal {
|
|
|
10
10
|
transition: unset;
|
|
11
11
|
transition-duration: 0.1s;
|
|
12
12
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
13
|
-
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Keep touch taps available to xterm even with a widened WebView viewport.
|
|
16
|
+
Include the viewport (a scroll container); preserve panning and pinch zoom. */
|
|
17
|
+
#terminal, #terminal .xterm, #terminal .xterm-viewport, #terminal .xterm-screen {
|
|
18
|
+
touch-action: manipulation;
|
|
19
|
+
}
|
|
@@ -12168,7 +12168,8 @@
|
|
|
12168
12168
|
(t.msgSetWindowTitle = "3"),
|
|
12169
12169
|
(t.msgSetPreferences = "4"),
|
|
12170
12170
|
(t.msgSetReconnect = "5"),
|
|
12171
|
-
(t.msgSetBufferSize = "6")
|
|
12171
|
+
(t.msgSetBufferSize = "6"),
|
|
12172
|
+
(t.msgRestoreModes = "m"));
|
|
12172
12173
|
t.WebTTY = class {
|
|
12173
12174
|
term;
|
|
12174
12175
|
connectionFactory;
|
|
@@ -12193,6 +12194,9 @@
|
|
|
12193
12194
|
this.reconnectDeadline = 0;
|
|
12194
12195
|
this.reconnectAttempts = 0;
|
|
12195
12196
|
this.zmodemExpectedSeq = 0;
|
|
12197
|
+
this.term.onClipboard?.(data => {
|
|
12198
|
+
if (!this.disconnected) this.connection.send("C" + data);
|
|
12199
|
+
});
|
|
12196
12200
|
}
|
|
12197
12201
|
open() {
|
|
12198
12202
|
let e;
|
|
@@ -12330,6 +12334,9 @@
|
|
|
12330
12334
|
const e = JSON.parse(i);
|
|
12331
12335
|
this.term.setPreferences(e);
|
|
12332
12336
|
break;
|
|
12337
|
+
case t.msgRestoreModes:
|
|
12338
|
+
this.term.applySessionModes(JSON.parse(i));
|
|
12339
|
+
break;
|
|
12333
12340
|
case t.msgSetReconnect:
|
|
12334
12341
|
const r = JSON.parse(i);
|
|
12335
12342
|
"number" == typeof r
|
|
@@ -12540,9 +12547,33 @@
|
|
|
12540
12547
|
})),
|
|
12541
12548
|
(this.fitAddOn = new s.FitAddon()),
|
|
12542
12549
|
(this.zmodemAddon = new a.ZModemAddon({
|
|
12543
|
-
toTerminal: (e) => this.
|
|
12550
|
+
toTerminal: (e) => this.writeTerminal(e),
|
|
12544
12551
|
toServer: (e) => this.sendInput(e),
|
|
12545
12552
|
})),
|
|
12553
|
+
(this.kittyKeyboard = new globalThis.GoTTYKittyKeyboard.KittyKeyboard(this.term, {
|
|
12554
|
+
android: /Android/i.test(navigator.userAgent),
|
|
12555
|
+
})),
|
|
12556
|
+
this.term.parser.registerOscHandler(52, data => {
|
|
12557
|
+
if (!this.disconnected && data.length <= 1048576) this.clipboardHandler?.(data);
|
|
12558
|
+
return true;
|
|
12559
|
+
}),
|
|
12560
|
+
this.term.parser.registerOscHandler(777, data => {
|
|
12561
|
+
const match = /^jsgotty-modes;([a-f0-9]{32})$/.exec(data);
|
|
12562
|
+
if (!match) {
|
|
12563
|
+
const restore = /^jsgotty-kitty-v1;(normal|alternate);(\d{1,2});(\d{1,2}(?:,\d{1,2})*)?$/.exec(data);
|
|
12564
|
+
if (!restore) return false;
|
|
12565
|
+
const flags = +restore[2], stack = restore[3] ? restore[3].split(',').map(Number) : [];
|
|
12566
|
+
if (flags > 31 || stack.length > 32 || stack.some(n => n > 31)) return true;
|
|
12567
|
+
this.kittyKeyboard.states[restore[1]] = {flags: flags & 11, stack: stack.map(n => n & 11)};
|
|
12568
|
+
return true;
|
|
12569
|
+
}
|
|
12570
|
+
this.term.input("\x1b]777;jsgotty-modes;" + match[1] + ";" +
|
|
12571
|
+
btoa(JSON.stringify({...this.sessionModes(), kittyRestore: 1})) + "\x07", false);
|
|
12572
|
+
return true;
|
|
12573
|
+
}),
|
|
12574
|
+
this.term.attachCustomKeyEventHandler((event) =>
|
|
12575
|
+
this.kittyKeyboard.handle(event) && this.handleAndroidAltKey(event),
|
|
12576
|
+
),
|
|
12546
12577
|
this.setPreferences(t),
|
|
12547
12578
|
(() => {
|
|
12548
12579
|
try {
|
|
@@ -12594,12 +12625,73 @@
|
|
|
12594
12625
|
));
|
|
12595
12626
|
}),
|
|
12596
12627
|
this.term.open(e),
|
|
12628
|
+
this.kittyKeyboard.attach(this.term.textarea),
|
|
12629
|
+
this.term.textarea?.addEventListener("blur", () => {
|
|
12630
|
+
this.androidAltDownKeys?.clear();
|
|
12631
|
+
}),
|
|
12597
12632
|
this.term.focus(),
|
|
12598
12633
|
this.resizeListener(),
|
|
12599
12634
|
window.addEventListener("resize", () => {
|
|
12600
12635
|
this.resizeListener();
|
|
12601
12636
|
}));
|
|
12602
12637
|
}
|
|
12638
|
+
// Android can translate physical Alt shortcuts into accent dead keys.
|
|
12639
|
+
// Resolve printable physical keys before xterm's composition handling.
|
|
12640
|
+
handleAndroidAltKey(event) {
|
|
12641
|
+
if (!/Android/i.test(navigator.userAgent)) return true;
|
|
12642
|
+
let code = event.code;
|
|
12643
|
+
// Some Android IMEs omit code and swallow the dead-key keydown,
|
|
12644
|
+
// but still deliver keyup with a usable legacy keyCode.
|
|
12645
|
+
if (!code) {
|
|
12646
|
+
if (event.keyCode >= 65 && event.keyCode <= 90)
|
|
12647
|
+
code = "Key" + String.fromCharCode(event.keyCode);
|
|
12648
|
+
else if (event.keyCode >= 48 && event.keyCode <= 57)
|
|
12649
|
+
code = "Digit" + String.fromCharCode(event.keyCode);
|
|
12650
|
+
else code = {
|
|
12651
|
+
32: "Space", 186: "Semicolon", 187: "Equal", 188: "Comma",
|
|
12652
|
+
189: "Minus", 190: "Period", 191: "Slash", 192: "Backquote",
|
|
12653
|
+
219: "BracketLeft", 220: "Backslash", 221: "BracketRight", 222: "Quote",
|
|
12654
|
+
}[event.keyCode];
|
|
12655
|
+
}
|
|
12656
|
+
this.androidAltDownKeys ??= new Set();
|
|
12657
|
+
const alreadySent = event.type === "keyup" && this.androidAltDownKeys.delete(code);
|
|
12658
|
+
if (
|
|
12659
|
+
!["keydown", "keypress", "keyup"].includes(event.type) ||
|
|
12660
|
+
!event.altKey || event.ctrlKey || event.metaKey ||
|
|
12661
|
+
event.isComposing || event.getModifierState?.("AltGraph") ||
|
|
12662
|
+
(event.type === "keyup" && event.key !== "Dead")
|
|
12663
|
+
) return true;
|
|
12664
|
+
|
|
12665
|
+
let character;
|
|
12666
|
+
if (/^Key[A-Z]$/.test(code)) {
|
|
12667
|
+
const letter = code.slice(3);
|
|
12668
|
+
character = event.shiftKey ? letter : letter.toLowerCase();
|
|
12669
|
+
} else if (/^Digit[0-9]$/.test(code)) {
|
|
12670
|
+
const digit = code.slice(5);
|
|
12671
|
+
character = event.shiftKey ? ")!@#$%^&*("[Number(digit)] : digit;
|
|
12672
|
+
} else {
|
|
12673
|
+
const punctuation = {
|
|
12674
|
+
Backquote: ["`", "~"], Minus: ["-", "_"], Equal: ["=", "+"],
|
|
12675
|
+
BracketLeft: ["[", "{"], BracketRight: ["]", "}"],
|
|
12676
|
+
Backslash: ["\\", "|"], Semicolon: [";", ":"], Quote: ["'", '"'],
|
|
12677
|
+
Comma: [",", "<"], Period: [".", ">"], Slash: ["/", "?"],
|
|
12678
|
+
Space: [" ", " "],
|
|
12679
|
+
};
|
|
12680
|
+
character = Object.hasOwn(punctuation, code)
|
|
12681
|
+
? punctuation[code][event.shiftKey ? 1 : 0]
|
|
12682
|
+
: undefined;
|
|
12683
|
+
}
|
|
12684
|
+
if (character === undefined) return true;
|
|
12685
|
+
event.preventDefault();
|
|
12686
|
+
event.stopPropagation();
|
|
12687
|
+
if (event.type === "keydown") {
|
|
12688
|
+
this.androidAltDownKeys.add(code);
|
|
12689
|
+
this.term.input("\x1b" + character, true);
|
|
12690
|
+
} else if (event.type === "keyup" && !alreadySent) {
|
|
12691
|
+
this.term.input("\x1b" + character, true);
|
|
12692
|
+
}
|
|
12693
|
+
return false;
|
|
12694
|
+
}
|
|
12603
12695
|
info() {
|
|
12604
12696
|
return { columns: this.term.cols, rows: this.term.rows };
|
|
12605
12697
|
}
|
|
@@ -12609,7 +12701,73 @@
|
|
|
12609
12701
|
getMessage() {
|
|
12610
12702
|
return this.message;
|
|
12611
12703
|
}
|
|
12704
|
+
enqueueTerminal(task) {
|
|
12705
|
+
this.terminalTasks ||= [];
|
|
12706
|
+
this.terminalTasks.push(task);
|
|
12707
|
+
if (this.terminalBusy) return;
|
|
12708
|
+
this.terminalBusy = true;
|
|
12709
|
+
const next = () => {
|
|
12710
|
+
const task = this.terminalTasks.shift();
|
|
12711
|
+
if (!task) { this.terminalBusy = false; return; }
|
|
12712
|
+
task(next);
|
|
12713
|
+
};
|
|
12714
|
+
next();
|
|
12715
|
+
}
|
|
12716
|
+
writeTerminal(data) {
|
|
12717
|
+
this.enqueueTerminal(done => this.term.write(data, done));
|
|
12718
|
+
}
|
|
12719
|
+
onClipboard(handler) {
|
|
12720
|
+
this.clipboardHandler = handler;
|
|
12721
|
+
}
|
|
12722
|
+
sessionModes() {
|
|
12723
|
+
const core = this.term._core;
|
|
12724
|
+
return {dec: {...core.coreService.decPrivateModes},
|
|
12725
|
+
mouse: core.coreMouseService.activeProtocol,
|
|
12726
|
+
encoding: core.coreMouseService.activeEncoding,
|
|
12727
|
+
cursorHidden: !!core.coreService.isCursorHidden,
|
|
12728
|
+
alternate: this.term.buffer.active.type === "alternate",
|
|
12729
|
+
kitty: JSON.parse(JSON.stringify(this.kittyKeyboard.states))};
|
|
12730
|
+
}
|
|
12731
|
+
applySessionModes(state) {
|
|
12732
|
+
this.enqueueTerminal(done => this.term.write("", () => this.restoreSessionModes(state, done)));
|
|
12733
|
+
}
|
|
12734
|
+
restoreSessionModes(state, done) {
|
|
12735
|
+
if (!state || !state.dec) { done(); return; }
|
|
12736
|
+
const core = this.term._core;
|
|
12737
|
+
const dec = core.coreService.decPrivateModes;
|
|
12738
|
+
const mouse = core.coreMouseService;
|
|
12739
|
+
// A socket-only reconnect keeps xterm's modes, so send only differences:
|
|
12740
|
+
// DECOM homes the cursor and 1049 overwrites the saved cursor.
|
|
12741
|
+
let sequence = "";
|
|
12742
|
+
const alternate = this.term.buffer.active.type === "alternate";
|
|
12743
|
+
if (!!state.alternate !== alternate) sequence += `\x1b[?1049${state.alternate ? "h" : "l"}`;
|
|
12744
|
+
const flags = {1:"applicationCursorKeys", 6:"origin", 7:"wraparound", 45:"reverseWraparound",
|
|
12745
|
+
66:"applicationKeypad", 1004:"sendFocus", 2004:"bracketedPasteMode"};
|
|
12746
|
+
for (const [mode, name] of Object.entries(flags))
|
|
12747
|
+
if (!!state.dec[name] !== !!dec[name]) sequence += `\x1b[?${mode}${state.dec[name] ? "h" : "l"}`;
|
|
12748
|
+
if (!!state.cursorHidden !== !!core.coreService.isCursorHidden)
|
|
12749
|
+
sequence += `\x1b[?25${state.cursorHidden ? "l" : "h"}`;
|
|
12750
|
+
if (state.mouse !== mouse.activeProtocol) {
|
|
12751
|
+
const modes = {X10:9, VT200:1000, DRAG:1002, ANY:1003};
|
|
12752
|
+
sequence += modes[state.mouse] ? `\x1b[?${modes[state.mouse]}h` : "\x1b[?1000l";
|
|
12753
|
+
}
|
|
12754
|
+
if (state.encoding !== mouse.activeEncoding) {
|
|
12755
|
+
const modes = {SGR:1006, SGR_PIXELS:1016};
|
|
12756
|
+
sequence += modes[state.encoding] ? `\x1b[?${modes[state.encoding]}h` : "\x1b[?1006l";
|
|
12757
|
+
}
|
|
12758
|
+
const complete = () => {
|
|
12759
|
+
// Assigned rather than replayed so the push stack never doubles; the server
|
|
12760
|
+
// keeps every flag, so mask to the ones kitty-keyboard.js implements.
|
|
12761
|
+
const supported = (s) => ({flags: s.flags & 11, stack: s.stack.map((f) => f & 11)});
|
|
12762
|
+
if (state.kitty?.normal && state.kitty?.alternate)
|
|
12763
|
+
this.kittyKeyboard.states = {normal: supported(state.kitty.normal), alternate: supported(state.kitty.alternate)};
|
|
12764
|
+
done();
|
|
12765
|
+
};
|
|
12766
|
+
if (sequence) this.term.write(sequence, complete);
|
|
12767
|
+
else complete();
|
|
12768
|
+
}
|
|
12612
12769
|
setDisconnected(e) {
|
|
12770
|
+
if (e) this.kittyKeyboard.clearHeld(false);
|
|
12613
12771
|
this.disconnected = !!e;
|
|
12614
12772
|
}
|
|
12615
12773
|
setReconnectAction(e) {
|
|
@@ -12721,9 +12879,12 @@
|
|
|
12721
12879
|
this.term.blur();
|
|
12722
12880
|
}
|
|
12723
12881
|
reset() {
|
|
12882
|
+
// Reconnecting the transport does not reset the running PTY's modes.
|
|
12883
|
+
this.kittyKeyboard.clearHeld(false);
|
|
12724
12884
|
(this.removeMessage(), this.term.clear());
|
|
12725
12885
|
}
|
|
12726
12886
|
close() {
|
|
12887
|
+
this.kittyKeyboard.dispose();
|
|
12727
12888
|
(window.removeEventListener("resize", this.resizeListener),
|
|
12728
12889
|
this.term.dispose());
|
|
12729
12890
|
}
|
|
@@ -26746,39 +26907,32 @@
|
|
|
26746
26907
|
case 37:
|
|
26747
26908
|
if (e.metaKey) break;
|
|
26748
26909
|
a
|
|
26749
|
-
? (
|
|
26750
|
-
o.key === r.C0.ESC + "[1;3D" &&
|
|
26751
|
-
(o.key = r.C0.ESC + (i ? "b" : "[1;5D")))
|
|
26910
|
+
? (o.key = r.C0.ESC + "[1;" + (a + 1) + "D")
|
|
26752
26911
|
: (o.key = t ? r.C0.ESC + "OD" : r.C0.ESC + "[D");
|
|
26753
26912
|
break;
|
|
26754
26913
|
case 39:
|
|
26755
26914
|
if (e.metaKey) break;
|
|
26756
26915
|
a
|
|
26757
|
-
? (
|
|
26758
|
-
o.key === r.C0.ESC + "[1;3C" &&
|
|
26759
|
-
(o.key = r.C0.ESC + (i ? "f" : "[1;5C")))
|
|
26916
|
+
? (o.key = r.C0.ESC + "[1;" + (a + 1) + "C")
|
|
26760
26917
|
: (o.key = t ? r.C0.ESC + "OC" : r.C0.ESC + "[C");
|
|
26761
26918
|
break;
|
|
26762
26919
|
case 38:
|
|
26763
26920
|
if (e.metaKey) break;
|
|
26764
26921
|
a
|
|
26765
|
-
? (
|
|
26766
|
-
i ||
|
|
26767
|
-
o.key !== r.C0.ESC + "[1;3A" ||
|
|
26768
|
-
(o.key = r.C0.ESC + "[1;5A"))
|
|
26922
|
+
? (o.key = r.C0.ESC + "[1;" + (a + 1) + "A")
|
|
26769
26923
|
: (o.key = t ? r.C0.ESC + "OA" : r.C0.ESC + "[A");
|
|
26770
26924
|
break;
|
|
26771
26925
|
case 40:
|
|
26772
26926
|
if (e.metaKey) break;
|
|
26773
26927
|
a
|
|
26774
|
-
? (
|
|
26775
|
-
i ||
|
|
26776
|
-
o.key !== r.C0.ESC + "[1;3B" ||
|
|
26777
|
-
(o.key = r.C0.ESC + "[1;5B"))
|
|
26928
|
+
? (o.key = r.C0.ESC + "[1;" + (a + 1) + "B")
|
|
26778
26929
|
: (o.key = t ? r.C0.ESC + "OB" : r.C0.ESC + "[B");
|
|
26779
26930
|
break;
|
|
26780
26931
|
case 45:
|
|
26781
|
-
e.shiftKey || e.ctrlKey ||
|
|
26932
|
+
e.shiftKey || e.ctrlKey ||
|
|
26933
|
+
(o.key = e.altKey
|
|
26934
|
+
? r.C0.ESC + "[2;" + (a + 1) + "~"
|
|
26935
|
+
: r.C0.ESC + "[2~");
|
|
26782
26936
|
break;
|
|
26783
26937
|
case 46:
|
|
26784
26938
|
o.key = a
|
|
@@ -26802,14 +26956,14 @@
|
|
|
26802
26956
|
case 33:
|
|
26803
26957
|
e.shiftKey
|
|
26804
26958
|
? (o.type = 2)
|
|
26805
|
-
: e.ctrlKey
|
|
26959
|
+
: e.ctrlKey || e.altKey
|
|
26806
26960
|
? (o.key = r.C0.ESC + "[5;" + (a + 1) + "~")
|
|
26807
26961
|
: (o.key = r.C0.ESC + "[5~");
|
|
26808
26962
|
break;
|
|
26809
26963
|
case 34:
|
|
26810
26964
|
e.shiftKey
|
|
26811
26965
|
? (o.type = 3)
|
|
26812
|
-
: e.ctrlKey
|
|
26966
|
+
: e.ctrlKey || e.altKey
|
|
26813
26967
|
? (o.key = r.C0.ESC + "[6;" + (a + 1) + "~")
|
|
26814
26968
|
: (o.key = r.C0.ESC + "[6~");
|
|
26815
26969
|
break;
|
|
@@ -26927,6 +27081,8 @@
|
|
|
26927
27081
|
))
|
|
26928
27082
|
: 56 === e.keyCode
|
|
26929
27083
|
? (o.key = r.C0.DEL)
|
|
27084
|
+
: "/" === e.key
|
|
27085
|
+
? (o.key = r.C0.US)
|
|
26930
27086
|
: 219 === e.keyCode
|
|
26931
27087
|
? (o.key = r.C0.ESC)
|
|
26932
27088
|
: 220 === e.keyCode
|