buninu-linux 0.14.57 → 0.15.58
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/LICENSES/linux-firmware/LICENCE.nvidia +131 -0
- package/LICENSES/linux-firmware/LICENSE.amdgpu +51 -0
- package/LICENSES/linux-firmware/LICENSE.amdisp +31 -0
- package/LICENSES/linux-firmware/LICENSE.i915 +39 -0
- package/LICENSES/linux-firmware/LICENSE.radeon +51 -0
- package/LICENSES/linux-firmware/LICENSE.xe +39 -0
- package/LICENSES/linux-firmware/WHENCE +10280 -0
- package/NOTICE.md +64 -46
- package/README.md +371 -87
- package/README.zh.md +2034 -0
- package/build-image.sh +10 -1
- package/build-uki.sh +3 -1
- package/fetch-alpine.sh +14 -0
- package/graphics.md +31 -0
- package/index.js +28 -11
- package/initramfs/bin/buninu-browser +4 -1
- package/initramfs/bin/buninu-land +1 -1
- package/initramfs/bin/bunterm +3 -3
- package/initramfs/bin/chroot +13 -0
- package/initramfs/bin/chvt +11 -0
- package/initramfs/bin/mount +4 -0
- package/initramfs/bin/umount +6 -0
- package/initramfs/bin/xdg-open +33 -0
- package/initramfs/buninu/CHANGELOG.md +19 -0
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +4 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/main.js +1 -1
- package/initramfs/buninu/apps/jsgotty/README.en.md +40 -0
- package/initramfs/buninu/apps/jsgotty/README.md +40 -0
- package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
- package/initramfs/buninu/apps/jsgotty/package.json +1 -1
- package/initramfs/buninu/apps/jsgotty/static/index.html +1 -0
- package/initramfs/buninu/apps/jsgotty/static/js/gotty.js +82 -17
- package/initramfs/buninu/apps/jsgotty/static/js/kitty-keyboard.js +193 -0
- package/initramfs/buninu/apps/jsgotty/tests/android-alt.test.js +115 -0
- package/initramfs/buninu/apps/jsgotty/tests/arrow-modifiers.test.js +80 -0
- package/initramfs/buninu/apps/jsgotty/tests/kitty-keyboard-browser.test.js +53 -0
- package/initramfs/buninu/apps/jsgotty/tests/kitty-keyboard.test.js +102 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/README.md +20 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/keyboard-collector.cjs +55 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/keyboard-debug.js +74 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/kitty-keyboard.js +80 -0
- package/initramfs/buninu/package.json +1 -1
- package/initramfs/init.js +19 -3
- package/initramfs/lib/bunterm/clipboard.js +14 -0
- package/initramfs/lib/bunterm/commands.js +37 -0
- package/initramfs/lib/bunterm/fps.js +38 -0
- package/initramfs/lib/bunterm/images.js +9 -5
- package/initramfs/lib/bunterm/input.js +58 -15
- package/initramfs/lib/bunterm/keyboard.js +62 -0
- package/initramfs/lib/bunterm/keycodes.js +104 -0
- package/initramfs/lib/bunterm/kitty-keyboard.cjs +195 -0
- package/initramfs/lib/bunterm/raw-protocol.js +33 -0
- package/initramfs/lib/bunterm/renderer.js +89 -20
- package/initramfs/lib/bunterm/selection.js +60 -0
- package/initramfs/lib/bunterm/terminal.js +146 -11
- package/initramfs/lib/bunterm/vt-menu.js +181 -0
- package/initramfs/lib/bunterm/vt.js +27 -1
- package/initramfs/lib/canvas.js +99 -0
- package/initramfs/lib/chvt.js +31 -0
- package/initramfs/lib/fbdev.js +21 -3
- package/initramfs/lib/gpu.js +77 -0
- package/initramfs/lib/raw-image.js +47 -0
- package/initramfs/lib/xdg-open.js +42 -0
- package/initramfs/usr/share/doc/buninu-linux/bunterm.md +85 -11
- package/initramfs/usr/share/doc/buninu-linux/chvt.md +34 -0
- package/initramfs/usr/share/doc/buninu-linux/xdg-open.md +43 -0
- package/package.json +1 -1
- package/scripts/arch.js +7 -5
- package/scripts/arch.sh +5 -5
- package/scripts/build.js +16 -3
- package/scripts/fetch-gpu.js +105 -0
- package/scripts/fetch.js +11 -1
- package/scripts/gpu-firmware.json +22 -0
- package/scripts/modules.js +4 -1
- package/scripts/npm-packages.md +17 -17
- package/test/bunterm-fps.test.js +17 -0
- package/test/bunterm-keyboard.test.js +116 -0
- package/test/bunterm-next-vt.test.js +9 -0
- package/test/bunterm-raw-protocol.test.js +19 -0
- package/test/bunterm-renderer.test.js +158 -0
- package/test/bunterm-selection.test.js +38 -0
- package/test/bunterm-vt-menu.test.js +178 -0
- package/test/chroot-bind.test.js +40 -2
- package/test/chvt.test.js +46 -0
- package/test/fbdev-sync.test.js +33 -0
- package/test/gpu.test.js +155 -0
- package/test/image-size.test.js +17 -0
- package/test/manual-bunterm-image-paths.js +31 -0
- package/test/mount-libc-lifetime.test.js +50 -0
- package/test/no-gpu.test.js +43 -0
- package/test/readme-cli.test.js +29 -0
- package/test/stage-npm.test.js +11 -4
- package/test/xdg-open.test.js +55 -0
|
@@ -12543,6 +12543,12 @@
|
|
|
12543
12543
|
toTerminal: (e) => this.term.write(e),
|
|
12544
12544
|
toServer: (e) => this.sendInput(e),
|
|
12545
12545
|
})),
|
|
12546
|
+
(this.kittyKeyboard = new globalThis.GoTTYKittyKeyboard.KittyKeyboard(this.term, {
|
|
12547
|
+
android: /Android/i.test(navigator.userAgent),
|
|
12548
|
+
})),
|
|
12549
|
+
this.term.attachCustomKeyEventHandler((event) =>
|
|
12550
|
+
this.kittyKeyboard.handle(event) && this.handleAndroidAltKey(event),
|
|
12551
|
+
),
|
|
12546
12552
|
this.setPreferences(t),
|
|
12547
12553
|
(() => {
|
|
12548
12554
|
try {
|
|
@@ -12594,12 +12600,73 @@
|
|
|
12594
12600
|
));
|
|
12595
12601
|
}),
|
|
12596
12602
|
this.term.open(e),
|
|
12603
|
+
this.kittyKeyboard.attach(this.term.textarea),
|
|
12604
|
+
this.term.textarea?.addEventListener("blur", () => {
|
|
12605
|
+
this.androidAltDownKeys?.clear();
|
|
12606
|
+
}),
|
|
12597
12607
|
this.term.focus(),
|
|
12598
12608
|
this.resizeListener(),
|
|
12599
12609
|
window.addEventListener("resize", () => {
|
|
12600
12610
|
this.resizeListener();
|
|
12601
12611
|
}));
|
|
12602
12612
|
}
|
|
12613
|
+
// Android can translate physical Alt shortcuts into accent dead keys.
|
|
12614
|
+
// Resolve printable physical keys before xterm's composition handling.
|
|
12615
|
+
handleAndroidAltKey(event) {
|
|
12616
|
+
if (!/Android/i.test(navigator.userAgent)) return true;
|
|
12617
|
+
let code = event.code;
|
|
12618
|
+
// Some Android IMEs omit code and swallow the dead-key keydown,
|
|
12619
|
+
// but still deliver keyup with a usable legacy keyCode.
|
|
12620
|
+
if (!code) {
|
|
12621
|
+
if (event.keyCode >= 65 && event.keyCode <= 90)
|
|
12622
|
+
code = "Key" + String.fromCharCode(event.keyCode);
|
|
12623
|
+
else if (event.keyCode >= 48 && event.keyCode <= 57)
|
|
12624
|
+
code = "Digit" + String.fromCharCode(event.keyCode);
|
|
12625
|
+
else code = {
|
|
12626
|
+
32: "Space", 186: "Semicolon", 187: "Equal", 188: "Comma",
|
|
12627
|
+
189: "Minus", 190: "Period", 191: "Slash", 192: "Backquote",
|
|
12628
|
+
219: "BracketLeft", 220: "Backslash", 221: "BracketRight", 222: "Quote",
|
|
12629
|
+
}[event.keyCode];
|
|
12630
|
+
}
|
|
12631
|
+
this.androidAltDownKeys ??= new Set();
|
|
12632
|
+
const alreadySent = event.type === "keyup" && this.androidAltDownKeys.delete(code);
|
|
12633
|
+
if (
|
|
12634
|
+
!["keydown", "keypress", "keyup"].includes(event.type) ||
|
|
12635
|
+
!event.altKey || event.ctrlKey || event.metaKey ||
|
|
12636
|
+
event.isComposing || event.getModifierState?.("AltGraph") ||
|
|
12637
|
+
(event.type === "keyup" && event.key !== "Dead")
|
|
12638
|
+
) return true;
|
|
12639
|
+
|
|
12640
|
+
let character;
|
|
12641
|
+
if (/^Key[A-Z]$/.test(code)) {
|
|
12642
|
+
const letter = code.slice(3);
|
|
12643
|
+
character = event.shiftKey ? letter : letter.toLowerCase();
|
|
12644
|
+
} else if (/^Digit[0-9]$/.test(code)) {
|
|
12645
|
+
const digit = code.slice(5);
|
|
12646
|
+
character = event.shiftKey ? ")!@#$%^&*("[Number(digit)] : digit;
|
|
12647
|
+
} else {
|
|
12648
|
+
const punctuation = {
|
|
12649
|
+
Backquote: ["`", "~"], Minus: ["-", "_"], Equal: ["=", "+"],
|
|
12650
|
+
BracketLeft: ["[", "{"], BracketRight: ["]", "}"],
|
|
12651
|
+
Backslash: ["\\", "|"], Semicolon: [";", ":"], Quote: ["'", '"'],
|
|
12652
|
+
Comma: [",", "<"], Period: [".", ">"], Slash: ["/", "?"],
|
|
12653
|
+
Space: [" ", " "],
|
|
12654
|
+
};
|
|
12655
|
+
character = Object.hasOwn(punctuation, code)
|
|
12656
|
+
? punctuation[code][event.shiftKey ? 1 : 0]
|
|
12657
|
+
: undefined;
|
|
12658
|
+
}
|
|
12659
|
+
if (character === undefined) return true;
|
|
12660
|
+
event.preventDefault();
|
|
12661
|
+
event.stopPropagation();
|
|
12662
|
+
if (event.type === "keydown") {
|
|
12663
|
+
this.androidAltDownKeys.add(code);
|
|
12664
|
+
this.term.input("\x1b" + character, true);
|
|
12665
|
+
} else if (event.type === "keyup" && !alreadySent) {
|
|
12666
|
+
this.term.input("\x1b" + character, true);
|
|
12667
|
+
}
|
|
12668
|
+
return false;
|
|
12669
|
+
}
|
|
12603
12670
|
info() {
|
|
12604
12671
|
return { columns: this.term.cols, rows: this.term.rows };
|
|
12605
12672
|
}
|
|
@@ -12610,6 +12677,7 @@
|
|
|
12610
12677
|
return this.message;
|
|
12611
12678
|
}
|
|
12612
12679
|
setDisconnected(e) {
|
|
12680
|
+
if (e) this.kittyKeyboard.reset();
|
|
12613
12681
|
this.disconnected = !!e;
|
|
12614
12682
|
}
|
|
12615
12683
|
setReconnectAction(e) {
|
|
@@ -12721,9 +12789,11 @@
|
|
|
12721
12789
|
this.term.blur();
|
|
12722
12790
|
}
|
|
12723
12791
|
reset() {
|
|
12792
|
+
this.kittyKeyboard.reset();
|
|
12724
12793
|
(this.removeMessage(), this.term.clear());
|
|
12725
12794
|
}
|
|
12726
12795
|
close() {
|
|
12796
|
+
this.kittyKeyboard.dispose();
|
|
12727
12797
|
(window.removeEventListener("resize", this.resizeListener),
|
|
12728
12798
|
this.term.dispose());
|
|
12729
12799
|
}
|
|
@@ -26746,39 +26816,32 @@
|
|
|
26746
26816
|
case 37:
|
|
26747
26817
|
if (e.metaKey) break;
|
|
26748
26818
|
a
|
|
26749
|
-
? (
|
|
26750
|
-
o.key === r.C0.ESC + "[1;3D" &&
|
|
26751
|
-
(o.key = r.C0.ESC + (i ? "b" : "[1;5D")))
|
|
26819
|
+
? (o.key = r.C0.ESC + "[1;" + (a + 1) + "D")
|
|
26752
26820
|
: (o.key = t ? r.C0.ESC + "OD" : r.C0.ESC + "[D");
|
|
26753
26821
|
break;
|
|
26754
26822
|
case 39:
|
|
26755
26823
|
if (e.metaKey) break;
|
|
26756
26824
|
a
|
|
26757
|
-
? (
|
|
26758
|
-
o.key === r.C0.ESC + "[1;3C" &&
|
|
26759
|
-
(o.key = r.C0.ESC + (i ? "f" : "[1;5C")))
|
|
26825
|
+
? (o.key = r.C0.ESC + "[1;" + (a + 1) + "C")
|
|
26760
26826
|
: (o.key = t ? r.C0.ESC + "OC" : r.C0.ESC + "[C");
|
|
26761
26827
|
break;
|
|
26762
26828
|
case 38:
|
|
26763
26829
|
if (e.metaKey) break;
|
|
26764
26830
|
a
|
|
26765
|
-
? (
|
|
26766
|
-
i ||
|
|
26767
|
-
o.key !== r.C0.ESC + "[1;3A" ||
|
|
26768
|
-
(o.key = r.C0.ESC + "[1;5A"))
|
|
26831
|
+
? (o.key = r.C0.ESC + "[1;" + (a + 1) + "A")
|
|
26769
26832
|
: (o.key = t ? r.C0.ESC + "OA" : r.C0.ESC + "[A");
|
|
26770
26833
|
break;
|
|
26771
26834
|
case 40:
|
|
26772
26835
|
if (e.metaKey) break;
|
|
26773
26836
|
a
|
|
26774
|
-
? (
|
|
26775
|
-
i ||
|
|
26776
|
-
o.key !== r.C0.ESC + "[1;3B" ||
|
|
26777
|
-
(o.key = r.C0.ESC + "[1;5B"))
|
|
26837
|
+
? (o.key = r.C0.ESC + "[1;" + (a + 1) + "B")
|
|
26778
26838
|
: (o.key = t ? r.C0.ESC + "OB" : r.C0.ESC + "[B");
|
|
26779
26839
|
break;
|
|
26780
26840
|
case 45:
|
|
26781
|
-
e.shiftKey || e.ctrlKey ||
|
|
26841
|
+
e.shiftKey || e.ctrlKey ||
|
|
26842
|
+
(o.key = e.altKey
|
|
26843
|
+
? r.C0.ESC + "[2;" + (a + 1) + "~"
|
|
26844
|
+
: r.C0.ESC + "[2~");
|
|
26782
26845
|
break;
|
|
26783
26846
|
case 46:
|
|
26784
26847
|
o.key = a
|
|
@@ -26802,14 +26865,14 @@
|
|
|
26802
26865
|
case 33:
|
|
26803
26866
|
e.shiftKey
|
|
26804
26867
|
? (o.type = 2)
|
|
26805
|
-
: e.ctrlKey
|
|
26868
|
+
: e.ctrlKey || e.altKey
|
|
26806
26869
|
? (o.key = r.C0.ESC + "[5;" + (a + 1) + "~")
|
|
26807
26870
|
: (o.key = r.C0.ESC + "[5~");
|
|
26808
26871
|
break;
|
|
26809
26872
|
case 34:
|
|
26810
26873
|
e.shiftKey
|
|
26811
26874
|
? (o.type = 3)
|
|
26812
|
-
: e.ctrlKey
|
|
26875
|
+
: e.ctrlKey || e.altKey
|
|
26813
26876
|
? (o.key = r.C0.ESC + "[6;" + (a + 1) + "~")
|
|
26814
26877
|
: (o.key = r.C0.ESC + "[6~");
|
|
26815
26878
|
break;
|
|
@@ -26927,6 +26990,8 @@
|
|
|
26927
26990
|
))
|
|
26928
26991
|
: 56 === e.keyCode
|
|
26929
26992
|
? (o.key = r.C0.DEL)
|
|
26993
|
+
: "/" === e.key
|
|
26994
|
+
? (o.key = r.C0.US)
|
|
26930
26995
|
: 219 === e.keyCode
|
|
26931
26996
|
? (o.key = r.C0.ESC)
|
|
26932
26997
|
: 220 === e.keyCode
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Kitty keyboard transport for the shipped xterm wrapper (no build step).
|
|
2
|
+
// Spec: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
3
|
+
(function (root) {
|
|
4
|
+
'use strict';
|
|
5
|
+
// Supported progressive enhancements: disambiguation, event types, all keys.
|
|
6
|
+
// Do not advertise alternate-layout/text reporting until those are implemented.
|
|
7
|
+
const SUPPORTED = 1 | 2 | 8;
|
|
8
|
+
const functional = {
|
|
9
|
+
Escape:[27,'u'], Enter:[13,'u'], Tab:[9,'u'], Backspace:[127,'u'],
|
|
10
|
+
Insert:[2,'~'], Delete:[3,'~'], ArrowLeft:[1,'D'], ArrowRight:[1,'C'],
|
|
11
|
+
ArrowUp:[1,'A'], ArrowDown:[1,'B'], PageUp:[5,'~'], PageDown:[6,'~'],
|
|
12
|
+
Home:[1,'H'], End:[1,'F'], CapsLock:[57358,'u'], ScrollLock:[57359,'u'],
|
|
13
|
+
NumLock:[57360,'u'], PrintScreen:[57361,'u'], Pause:[57362,'u'], ContextMenu:[57363,'u'],
|
|
14
|
+
MediaPlay:[57428,'u'], MediaPause:[57429,'u'], MediaPlayPause:[57430,'u'],
|
|
15
|
+
MediaStop:[57432,'u'], MediaFastForward:[57433,'u'], MediaRewind:[57434,'u'],
|
|
16
|
+
MediaTrackNext:[57435,'u'], MediaTrackPrevious:[57436,'u'], MediaRecord:[57437,'u'],
|
|
17
|
+
AudioVolumeDown:[57438,'u'], AudioVolumeUp:[57439,'u'], AudioVolumeMute:[57440,'u'],
|
|
18
|
+
};
|
|
19
|
+
[11,12,13,14,15,17,18,19,20,21,23,24].forEach((n,i) => functional['F'+(i+1)] = [n,'~']);
|
|
20
|
+
for (let i=13;i<=35;i++) functional['F'+i] = [57376+i-13,'u'];
|
|
21
|
+
const punctuation = {
|
|
22
|
+
Backquote:['`','~'], Minus:['-','_'], Equal:['=','+'], BracketLeft:['[','{'],
|
|
23
|
+
BracketRight:[']','}'], Backslash:['\\','|'], Semicolon:[';',':'], Quote:["'",'"'],
|
|
24
|
+
Comma:[',','<'], Period:['.','>'], Slash:['/','?'], Space:[' ',' '],
|
|
25
|
+
};
|
|
26
|
+
const modifiers = {Shift:[1,57441], Control:[4,57442], Alt:[2,57443], Meta:[8,57444],
|
|
27
|
+
OS:[8,57444], Super:[8,57444], Hyper:[16,57445], AltGraph:[0,57453]};
|
|
28
|
+
const has = (object,key) => Object.prototype.hasOwnProperty.call(object,key);
|
|
29
|
+
const lock = (e,name) => Boolean(e.getModifierState?.(name));
|
|
30
|
+
function physicalCode(e) {
|
|
31
|
+
if (e.code && e.code !== 'Unidentified') return e.code;
|
|
32
|
+
if (e.keyCode>=65 && e.keyCode<=90) return 'Key'+String.fromCharCode(e.keyCode);
|
|
33
|
+
if (e.keyCode>=48 && e.keyCode<=57) return 'Digit'+String.fromCharCode(e.keyCode);
|
|
34
|
+
return ({32:'Space',186:'Semicolon',187:'Equal',188:'Comma',189:'Minus',190:'Period',
|
|
35
|
+
191:'Slash',192:'Backquote',219:'BracketLeft',220:'Backslash',221:'BracketRight',222:'Quote'})[e.keyCode] || '';
|
|
36
|
+
}
|
|
37
|
+
function baseCharacter(e, android) {
|
|
38
|
+
const code=physicalCode(e);
|
|
39
|
+
// Android's Alt accent conversion must not turn an E/I/N/U shortcut into a dead key.
|
|
40
|
+
const recover=android && e.altKey && !lock(e,'AltGraph');
|
|
41
|
+
if (recover || ([...e.key || ''].length===1 && e.key.codePointAt(0)<32)) {
|
|
42
|
+
if (/^Key[A-Z]$/.test(code)) return code.slice(3).toLowerCase();
|
|
43
|
+
if (/^Digit[0-9]$/.test(code)) return code.slice(5);
|
|
44
|
+
if (has(punctuation,code)) return punctuation[code][0];
|
|
45
|
+
}
|
|
46
|
+
if ([...e.key || ''].length!==1) return null;
|
|
47
|
+
if (e.shiftKey) {
|
|
48
|
+
if (has(punctuation,code) && punctuation[code][1]===e.key) return punctuation[code][0];
|
|
49
|
+
if (/^Digit[0-9]$/.test(code) && ')!@#$%^&*('[Number(code.slice(5))]===e.key) return code.slice(5);
|
|
50
|
+
}
|
|
51
|
+
const lower=e.key.toLowerCase();
|
|
52
|
+
return [...lower].length===1 ? lower : e.key;
|
|
53
|
+
}
|
|
54
|
+
function keyDefinition(e, flags, android) {
|
|
55
|
+
if (has(modifiers,e.key)) {
|
|
56
|
+
const [bit,left]=modifiers[e.key];
|
|
57
|
+
const right=e.location===2 || /Right$/.test(e.code || '');
|
|
58
|
+
return {number:left+(right && e.key!=='AltGraph'?6:0),suffix:'u',modifier:bit};
|
|
59
|
+
}
|
|
60
|
+
if ((flags & 9) && (e.location===3 || /^Numpad/.test(e.code || ''))) {
|
|
61
|
+
const numpad={NumpadDecimal:57409,NumpadDivide:57410,NumpadMultiply:57411,
|
|
62
|
+
NumpadSubtract:57412,NumpadAdd:57413,NumpadEnter:57414,NumpadEqual:57415,
|
|
63
|
+
NumpadComma:57416};
|
|
64
|
+
const nav={ArrowLeft:57417,ArrowRight:57418,ArrowUp:57419,ArrowDown:57420,
|
|
65
|
+
PageUp:57421,PageDown:57422,Home:57423,End:57424,Insert:57425,Delete:57426,Clear:57427};
|
|
66
|
+
const number=has(nav,e.key)?nav[e.key]:/^Numpad[0-9]$/.test(e.code || '')
|
|
67
|
+
?57399+Number(e.code.slice(6)):numpad[e.code];
|
|
68
|
+
if (number) return {number,suffix:'u'};
|
|
69
|
+
}
|
|
70
|
+
if (has(functional,e.key)) {
|
|
71
|
+
const [number,suffix]=functional[e.key];
|
|
72
|
+
return {number,suffix,recovery:['Enter','Tab','Backspace'].includes(e.key)};
|
|
73
|
+
}
|
|
74
|
+
const ch=baseCharacter(e,android);
|
|
75
|
+
return ch===null?null:{number:ch.codePointAt(0),suffix:'u',text:true};
|
|
76
|
+
}
|
|
77
|
+
class KittyKeyboard {
|
|
78
|
+
constructor(term, {android=false}={}) {
|
|
79
|
+
this.term=term; this.android=android; this.disposables=[];
|
|
80
|
+
this.metaKeys=new Set(); this.down=new Map(); this.suppressed=new Set(); this.reset();
|
|
81
|
+
const register=(prefix,fn) => this.disposables.push(term.parser.registerCsiHandler({prefix,final:'u'},p=>{fn(p);return true;}));
|
|
82
|
+
const value=(p,i,fallback=0) => Number.isInteger(p[i])?p[i]:fallback;
|
|
83
|
+
register('?',()=>term.input(`\x1b[?${this.flags}u`,false));
|
|
84
|
+
register('>',p=>{const s=this.state;s.stack.push(s.flags);if(s.stack.length>32)s.stack.shift();s.flags=value(p,0)&SUPPORTED;});
|
|
85
|
+
register('<',p=>{const s=this.state;let count=Math.min(value(p,0)||1,33);while(count--)s.flags=s.stack.pop()??0;});
|
|
86
|
+
register('=',p=>{const s=this.state,f=value(p,0)&SUPPORTED,m=value(p,1)||1;if(m===1)s.flags=f;else if(m===2)s.flags|=f;else if(m===3)s.flags&=~f;});
|
|
87
|
+
this.disposables.push(term.parser.registerEscHandler({final:'c'},()=>{this.reset();return false;}));
|
|
88
|
+
this.disposables.push(term.parser.registerCsiHandler({intermediates:'!',final:'p'},()=>{this.reset();return false;}));
|
|
89
|
+
}
|
|
90
|
+
get state() { return this.states[this.term.buffer.active.type==='alternate'?'alternate':'normal']; }
|
|
91
|
+
get flags() { return this.state.flags; }
|
|
92
|
+
reset() {
|
|
93
|
+
this.states={normal:{flags:0,stack:[]},alternate:{flags:0,stack:[]}};
|
|
94
|
+
this.clearHeld(false);
|
|
95
|
+
}
|
|
96
|
+
clearHeld(report=true) {
|
|
97
|
+
// Release keys reported to the client before losing focus, avoiding stuck remote keys.
|
|
98
|
+
if (report && (this.flags & 2)) for (const key of this.down.values()) {
|
|
99
|
+
if(key.reported) this.term.input(this.sequence(key.def,0,3),true);
|
|
100
|
+
}
|
|
101
|
+
this.metaKeys.clear();this.down.clear();this.suppressed.clear();this.suppressKeypress=false;
|
|
102
|
+
}
|
|
103
|
+
attach(element) {
|
|
104
|
+
const doc=element.ownerDocument, win=doc.defaultView;
|
|
105
|
+
const blur=()=>this.clearHeld();
|
|
106
|
+
const visibility=()=>{if(doc.hidden)this.clearHeld();};
|
|
107
|
+
element.addEventListener('blur',blur);win.addEventListener('blur',blur);
|
|
108
|
+
doc.addEventListener('visibilitychange',visibility);
|
|
109
|
+
this.disposables.push({dispose:()=>{element.removeEventListener('blur',blur);win.removeEventListener('blur',blur);doc.removeEventListener('visibilitychange',visibility);}});
|
|
110
|
+
}
|
|
111
|
+
dispose() {this.clearHeld(false);for(const d of this.disposables)d.dispose();}
|
|
112
|
+
sequence(def,bits,type) {
|
|
113
|
+
const event=(this.flags&2) && type!==1?':'+type:'';
|
|
114
|
+
const mods=bits+1;
|
|
115
|
+
if(def.suffix!=='u' && mods===1 && !event)
|
|
116
|
+
return '\x1b['+(def.number===1?'':def.number)+def.suffix;
|
|
117
|
+
return `\x1b[${def.number}${mods!==1||event?';'+mods+event:''}${def.suffix}`;
|
|
118
|
+
}
|
|
119
|
+
handle(event) {
|
|
120
|
+
if (!['keydown','keyup','keypress'].includes(event.type)) return true;
|
|
121
|
+
const identity=physicalCode(event)||event.key;
|
|
122
|
+
const metaKey=['Meta','OS','Super'].includes(event.key);
|
|
123
|
+
if(this.android && metaKey) {
|
|
124
|
+
if(event.type==='keydown')this.metaKeys.add(event.code||String(event.location||0));
|
|
125
|
+
if(event.type==='keyup')this.metaKeys.delete(event.code||String(event.location||0));
|
|
126
|
+
}
|
|
127
|
+
// Patch only this terminal's Android event, preserving the native DOM event.
|
|
128
|
+
if(this.android && this.metaKeys.size && !event.metaKey) {
|
|
129
|
+
const original=event.getModifierState?.bind(event);
|
|
130
|
+
Object.defineProperties(event,{
|
|
131
|
+
metaKey:{configurable:true,value:true},
|
|
132
|
+
getModifierState:{configurable:true,value:name=>name==='Meta'||Boolean(original?.(name))},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if(!this.flags || this.term.options.disableStdin) return true;
|
|
136
|
+
const consume=()=>{event.preventDefault();event.stopPropagation();return false;};
|
|
137
|
+
if(event.type==='keypress')return this.suppressKeypress?consume():true;
|
|
138
|
+
let previous=this.down.get(identity);
|
|
139
|
+
if(event.type==='keyup') {
|
|
140
|
+
// Some Android IMEs emit only the release of an Alt dead key. Recover
|
|
141
|
+
// that chord here too, never falling back to legacy ESC text in Kitty mode.
|
|
142
|
+
if(!previous && this.android && event.altKey && event.key==='Dead' &&
|
|
143
|
+
!event.isComposing && !lock(event,'AltGraph') && (this.flags&9)) {
|
|
144
|
+
const def=keyDefinition(event,this.flags,this.android);
|
|
145
|
+
if(def) {
|
|
146
|
+
this.term.input(this.sequence(def,this.bits(event),1),true);
|
|
147
|
+
previous={def,reported:true};this.suppressed.add(identity);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
this.down.delete(identity);
|
|
151
|
+
const suppressed=this.suppressed.delete(identity);
|
|
152
|
+
this.suppressKeypress=false;
|
|
153
|
+
if(previous?.reported && (this.flags&2)) {
|
|
154
|
+
let bits=this.bits(event);
|
|
155
|
+
if(previous.def.modifier && ![...this.down.values()].some(k=>k.def.modifier===previous.def.modifier)) bits &= ~previous.def.modifier;
|
|
156
|
+
if(metaKey && this.metaKeys.size) bits|=8;
|
|
157
|
+
this.term.input(this.sequence(previous.def,bits,3),true);
|
|
158
|
+
}
|
|
159
|
+
return suppressed?consume():true;
|
|
160
|
+
}
|
|
161
|
+
this.suppressKeypress=false;
|
|
162
|
+
if(event.isComposing || event.keyCode===229 && event.key==='Unidentified')return true;
|
|
163
|
+
if(event.key==='Dead' && !(this.android && event.altKey && !lock(event,'AltGraph')))return true;
|
|
164
|
+
const def=keyDefinition(event,this.flags,this.android);
|
|
165
|
+
if(!def)return true;
|
|
166
|
+
let bits=this.bits(event);
|
|
167
|
+
if(def.modifier!==undefined) {
|
|
168
|
+
if(!(this.flags&8))return true;
|
|
169
|
+
bits|=def.modifier;
|
|
170
|
+
}
|
|
171
|
+
const all=Boolean(this.flags&8);
|
|
172
|
+
// Enter/Tab/Backspace remain recovery keys unless modified or report-all is requested.
|
|
173
|
+
if(!all && def.recovery && !(bits&~192))return true;
|
|
174
|
+
const shortcut=Boolean(bits&62);
|
|
175
|
+
if(def.text && !all && !(this.flags&1 && shortcut))return true;
|
|
176
|
+
// With only event reporting requested, leave legacy printable shortcuts untouched.
|
|
177
|
+
if(def.text && !all && !(this.flags&1))return true;
|
|
178
|
+
const type=event.repeat?2:1;
|
|
179
|
+
this.down.set(identity,{def,reported:!(def.recovery && !all)});this.suppressed.add(identity);this.suppressKeypress=true;
|
|
180
|
+
this.term.input(this.sequence(def,bits,type),true);
|
|
181
|
+
return consume();
|
|
182
|
+
}
|
|
183
|
+
bits(e) {
|
|
184
|
+
const altgr=lock(e,'AltGraph');
|
|
185
|
+
return (e.shiftKey?1:0)|(!altgr&&e.altKey?2:0)|(!altgr&&e.ctrlKey?4:0)|
|
|
186
|
+
(e.metaKey || this.android&&this.metaKeys.size?8:0)|
|
|
187
|
+
(lock(e,'Hyper')?16:0)|(lock(e,'CapsLock')?64:0)|(lock(e,'NumLock')?128:0);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const api={KittyKeyboard,SUPPORTED};
|
|
191
|
+
if(typeof module==='object' && module.exports)module.exports=api;
|
|
192
|
+
else root.GoTTYKittyKeyboard=api;
|
|
193
|
+
})(globalThis);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const vm = require('node:vm');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
// Exercise the shipped bundle's wrapper method, without a DOM or new dependencies.
|
|
8
|
+
const bundle = fs.readFileSync(path.join(__dirname, '../static/js/gotty.js'), 'utf8');
|
|
9
|
+
const start = bundle.indexOf(' handleAndroidAltKey(event) {');
|
|
10
|
+
assert.ok(start >= 0);
|
|
11
|
+
const end = bundle.indexOf(' info() {', start);
|
|
12
|
+
assert.ok(end > start);
|
|
13
|
+
const method = bundle.slice(start, end);
|
|
14
|
+
function setup(userAgent = 'Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 wv') {
|
|
15
|
+
const handler = vm.runInNewContext(`({${method}}).handleAndroidAltKey`, {
|
|
16
|
+
navigator: { userAgent },
|
|
17
|
+
});
|
|
18
|
+
const sent = [];
|
|
19
|
+
const target = { term: { input: (text, userInput) => sent.push([text, userInput]) } };
|
|
20
|
+
function dispatch(overrides = {}) {
|
|
21
|
+
const event = {
|
|
22
|
+
type: 'keydown', code: 'KeyE', key: 'Dead', keyCode: 69,
|
|
23
|
+
altKey: true, ctrlKey: false, metaKey: false, shiftKey: false,
|
|
24
|
+
isComposing: false, getModifierState: () => false,
|
|
25
|
+
prevented: false, stopped: false,
|
|
26
|
+
preventDefault() { this.prevented = true; },
|
|
27
|
+
stopPropagation() { this.stopped = true; },
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
return { result: handler.call(target, event), event };
|
|
31
|
+
}
|
|
32
|
+
return { sent, dispatch };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('Android accent shortcuts emit one ESC sequence; keypress/keyup do not duplicate it', () => {
|
|
36
|
+
for (const letter of ['E', 'I', 'N', 'U']) {
|
|
37
|
+
for (const keyCode of [letter.charCodeAt(0), 229]) {
|
|
38
|
+
const { sent, dispatch } = setup();
|
|
39
|
+
const { event, result } = dispatch({ code: `Key${letter}`, keyCode });
|
|
40
|
+
assert.equal(result, false);
|
|
41
|
+
assert.equal(event.prevented, true);
|
|
42
|
+
assert.equal(event.stopped, true);
|
|
43
|
+
dispatch({ type: 'keypress', code: `Key${letter}`, keyCode });
|
|
44
|
+
dispatch({ type: 'keyup', code: `Key${letter}`, keyCode });
|
|
45
|
+
assert.deepEqual(sent, [[`\x1b${letter.toLowerCase()}`, true]]);
|
|
46
|
+
// No dead-key state is retained in this handler for the next ordinary key.
|
|
47
|
+
assert.equal(dispatch({ altKey: false, key: 'e' }).result, true);
|
|
48
|
+
assert.equal(sent.length, 1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('all printable physical Alt keys bypass dead-key handling, including Shift and repeat', () => {
|
|
54
|
+
const { sent, dispatch } = setup();
|
|
55
|
+
for (const [code, shiftKey, expected] of [
|
|
56
|
+
['KeyA', false, 'a'], ['KeyE', true, 'E'], ['Digit0', false, '0'],
|
|
57
|
+
['Digit1', true, '!'], ['Digit0', true, ')'], ['Quote', false, "'"],
|
|
58
|
+
['Backquote', true, '~'], ['Backslash', false, '\\'], ['Space', false, ' '],
|
|
59
|
+
]) {
|
|
60
|
+
dispatch({ code, shiftKey, key: 'Dead' });
|
|
61
|
+
assert.deepEqual(sent.at(-1), [`\x1b${expected}`, true]);
|
|
62
|
+
}
|
|
63
|
+
dispatch({ code: 'KeyE', key: 'é', repeat: true });
|
|
64
|
+
assert.deepEqual(sent.at(-1), ['\x1be', true]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('composition, AltGr, other modifiers, and unmapped keys retain existing handling', () => {
|
|
68
|
+
for (const overrides of [
|
|
69
|
+
{ isComposing: true, keyCode: 229 }, { getModifierState: key => key === 'AltGraph' },
|
|
70
|
+
{ altKey: false }, { ctrlKey: true }, { metaKey: true },
|
|
71
|
+
{ code: '', keyCode: 229 }, { code: 'ArrowLeft' }, { code: 'Enter' },
|
|
72
|
+
{ code: 'Unidentified', keyCode: 229 }, { code: 'toString' },
|
|
73
|
+
{ type: 'keyup', key: 'e' },
|
|
74
|
+
]) {
|
|
75
|
+
const { sent, dispatch } = setup();
|
|
76
|
+
const { result, event } = dispatch(overrides);
|
|
77
|
+
assert.equal(result, true);
|
|
78
|
+
assert.equal(event.prevented, false);
|
|
79
|
+
assert.deepEqual(sent, []);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('non-Android browsers retain existing Alt/dead-key handling', () => {
|
|
84
|
+
for (const ua of ['Mozilla/5.0 (X11; Linux x86_64)', 'Mozilla/5.0 (Macintosh)', 'Windows']) {
|
|
85
|
+
const { sent, dispatch } = setup(ua);
|
|
86
|
+
assert.equal(dispatch().result, true);
|
|
87
|
+
assert.deepEqual(sent, []);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Replay the actual Android 16 WebView trace: no letter keydown and empty code.
|
|
92
|
+
test('Android keyup-only dead keys use legacy keyCode, and ordinary IME input stays untouched', () => {
|
|
93
|
+
const { sent, dispatch } = setup();
|
|
94
|
+
for (const keyCode of [69, 78, 85, 73]) {
|
|
95
|
+
dispatch({ type: 'keydown', code: '', key: 'Alt', keyCode: 18 });
|
|
96
|
+
assert.equal(dispatch({ type: 'keyup', code: '', key: 'Dead', keyCode }).result, false);
|
|
97
|
+
dispatch({ type: 'keyup', code: '', key: 'Alt', keyCode: 18 });
|
|
98
|
+
}
|
|
99
|
+
assert.deepEqual(sent, ['e', 'n', 'u', 'i'].map(c => ['\x1b' + c, true]));
|
|
100
|
+
assert.equal(dispatch({ code: '', key: 'Unidentified', keyCode: 229, altKey: false }).result, true);
|
|
101
|
+
assert.equal(sent.length, 4);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('legacy keydown plus dead keyup emits once, even with different code availability', () => {
|
|
105
|
+
for (const [downCode, upCode] of [['', ''], ['KeyE', ''], ['', 'KeyE']]) {
|
|
106
|
+
const { sent, dispatch } = setup();
|
|
107
|
+
dispatch({ code: downCode, keyCode: 69 });
|
|
108
|
+
dispatch({ type: 'keypress', code: downCode, keyCode: 69 });
|
|
109
|
+
dispatch({ type: 'keyup', code: upCode, keyCode: 69 });
|
|
110
|
+
assert.deepEqual(sent, [['\x1be', true]]);
|
|
111
|
+
// The next keyup-only shortcut must not be suppressed by the preceding press.
|
|
112
|
+
dispatch({ type: 'keyup', code: '', keyCode: 69 });
|
|
113
|
+
assert.equal(sent.length, 2);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const vm = require('node:vm');
|
|
6
|
+
|
|
7
|
+
// Exercise the keyboard encoder shipped in the frontend bundle.
|
|
8
|
+
const bundle = fs.readFileSync(path.join(__dirname, '../static/js/gotty.js'), 'utf8');
|
|
9
|
+
const marker = 't.evaluateKeyboardEvent = function (e, t, i, n) {';
|
|
10
|
+
const start = bundle.indexOf(marker);
|
|
11
|
+
assert.ok(start >= 0);
|
|
12
|
+
const end = bundle.indexOf('\n },\n 482:', start);
|
|
13
|
+
assert.ok(end > start);
|
|
14
|
+
const source = bundle.slice(start + 't.evaluateKeyboardEvent = '.length, end).trim().replace(/;$/, '');
|
|
15
|
+
const evaluate = vm.runInNewContext(`(${source})`, { r: { C0: { ESC: '\x1b', US: '\x1f' } }, s: {} });
|
|
16
|
+
const arrows = [[37, 'D'], [38, 'A'], [39, 'C'], [40, 'B']];
|
|
17
|
+
|
|
18
|
+
for (const isMac of [false, true]) {
|
|
19
|
+
test(`arrow modifiers stay distinct (isMac=${isMac})`, () => {
|
|
20
|
+
for (const [keyCode, suffix] of arrows) {
|
|
21
|
+
for (const applicationCursorMode of [false, true]) {
|
|
22
|
+
const sequences = new Set();
|
|
23
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
24
|
+
const result = evaluate({
|
|
25
|
+
keyCode, metaKey: false,
|
|
26
|
+
shiftKey: Boolean(mask & 1), altKey: Boolean(mask & 2), ctrlKey: Boolean(mask & 4),
|
|
27
|
+
}, applicationCursorMode, isMac, false);
|
|
28
|
+
const expected = mask
|
|
29
|
+
? `\x1b[1;${mask + 1}${suffix}`
|
|
30
|
+
: `\x1b${applicationCursorMode ? 'O' : '['}${suffix}`;
|
|
31
|
+
assert.equal(result.key, expected);
|
|
32
|
+
sequences.add(result.key);
|
|
33
|
+
}
|
|
34
|
+
assert.equal(sequences.size, 8);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test('Alt Insert/PageUp/PageDown keep Alt while preserving existing shortcuts', () => {
|
|
41
|
+
for (const isMac of [false, true]) {
|
|
42
|
+
const encode = (keyCode, modifiers = {}) => evaluate({
|
|
43
|
+
keyCode, shiftKey: false, altKey: false, ctrlKey: false, metaKey: false,
|
|
44
|
+
...modifiers,
|
|
45
|
+
}, false, isMac, false);
|
|
46
|
+
for (const [keyCode, parameter] of [[45, 2], [33, 5], [34, 6]]) {
|
|
47
|
+
assert.equal(encode(keyCode).key, `\x1b[${parameter}~`);
|
|
48
|
+
assert.equal(encode(keyCode, { altKey: true }).key, `\x1b[${parameter};3~`);
|
|
49
|
+
}
|
|
50
|
+
for (const [keyCode, parameter, scrollType] of [[33, 5, 2], [34, 6, 3]]) {
|
|
51
|
+
assert.equal(encode(keyCode, { ctrlKey: true }).key, `\x1b[${parameter};5~`);
|
|
52
|
+
assert.equal(encode(keyCode, { ctrlKey: true, altKey: true }).key, `\x1b[${parameter};7~`);
|
|
53
|
+
for (const ctrlKey of [false, true]) {
|
|
54
|
+
for (const altKey of [false, true]) {
|
|
55
|
+
const result = encode(keyCode, { shiftKey: true, ctrlKey, altKey });
|
|
56
|
+
assert.equal(result.type, scrollType);
|
|
57
|
+
assert.equal(result.key, undefined);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const modifiers of [
|
|
62
|
+
{ ctrlKey: true }, { shiftKey: true }, { ctrlKey: true, shiftKey: true },
|
|
63
|
+
{ ctrlKey: true, altKey: true }, { shiftKey: true, altKey: true },
|
|
64
|
+
]) assert.equal(encode(45, modifiers).key, undefined);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
test('Ctrl slash sends US like Ctrl underscore, including alternate layout keyCodes', () => {
|
|
70
|
+
for (const isMac of [false, true]) {
|
|
71
|
+
for (const keyCode of [191, 226]) {
|
|
72
|
+
const result = evaluate({ key: '/', keyCode, ctrlKey: true,
|
|
73
|
+
shiftKey: false, altKey: false, metaKey: false }, false, isMac, false);
|
|
74
|
+
assert.equal(result.key, '\x1f');
|
|
75
|
+
}
|
|
76
|
+
const plain = evaluate({ key: '/', keyCode: 191, ctrlKey: false,
|
|
77
|
+
shiftKey: false, altKey: false, metaKey: false }, false, isMac, false);
|
|
78
|
+
assert.equal(plain.key, '/');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
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 parses Kitty modes and delivers one encoded DOM event per key', {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, kb=wrapper.kittyKeyboard;
|
|
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
|
+
const key=(type,key,code,mods={})=>term.textarea.dispatchEvent(new KeyboardEvent(type,{key,code,bubbles:true,cancelable:true,...mods}));
|
|
34
|
+
await write('\\x1b[?u');equal(sent.splice(0),['\\x1b[?0u']);
|
|
35
|
+
// Split escape sequence across writes: only the parser may enable the mode.
|
|
36
|
+
await write('\\x1b[>');await write('11u');equal(kb.flags,11);
|
|
37
|
+
key('keydown','Meta','MetaLeft');key('keydown','x','KeyX',{ctrlKey:true,shiftKey:true});
|
|
38
|
+
key('keypress','x','KeyX',{ctrlKey:true,shiftKey:true});key('keyup','x','KeyX',{ctrlKey:true,shiftKey:true});key('keyup','Meta','MetaLeft');
|
|
39
|
+
equal(sent.splice(0),['\\x1b[57444;9u','\\x1b[120;14u','\\x1b[120;14:3u','\\x1b[57444;1:3u']);
|
|
40
|
+
await write('\\x1b[?1049h');equal(kb.flags,0);await write('\\x1b[>1u');equal(kb.flags,1);
|
|
41
|
+
await write('\\x1b[?1049l');equal(kb.flags,11);
|
|
42
|
+
await write('\\x1b[<u');equal(kb.flags,0);
|
|
43
|
+
key('keydown','c','KeyC',{ctrlKey:true,keyCode:67});key('keyup','c','KeyC',{ctrlKey:true,keyCode:67});equal(sent.splice(0),['\\x03']);
|
|
44
|
+
await write('\\x1b[>11u');await write('\\x1bc');equal(kb.flags,0);
|
|
45
|
+
await write('\\x1b[?u');equal(sent.splice(0),['\\x1b[?0u']);
|
|
46
|
+
wrapper.close();return 'ok';
|
|
47
|
+
})()`});
|
|
48
|
+
assert.equal(result.exceptionDetails,undefined,JSON.stringify(result.exceptionDetails));assert.equal(result.result.value,'ok');
|
|
49
|
+
}finally{
|
|
50
|
+
ws?.close();if(child.pid){try{process.kill(-child.pid,'SIGTERM');}catch{}}
|
|
51
|
+
server.closeAllConnections();await new Promise(r=>server.close(r));
|
|
52
|
+
}
|
|
53
|
+
});
|