buninu-linux 0.13.56 → 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 +410 -96
- package/README.zh.md +2034 -0
- package/build-image.sh +10 -1
- package/build-uki.sh +3 -1
- package/fetch-alpine.sh +15 -1
- 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/dhclient +8 -0
- package/initramfs/bin/mount +4 -0
- package/initramfs/bin/ping +8 -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 +27 -10
- 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 +90 -21
- package/initramfs/lib/bunterm/selection.js +60 -0
- package/initramfs/lib/bunterm/terminal.js +147 -12
- 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/dhclient.js +135 -0
- package/initramfs/lib/dhcp-start.js +85 -0
- package/initramfs/lib/dhcp-transport.js +44 -0
- package/initramfs/lib/dhcp.js +124 -0
- package/initramfs/lib/dlopen.js +58 -11
- package/initramfs/lib/fbdev.js +21 -3
- package/initramfs/lib/gpu.js +77 -0
- package/initramfs/lib/ping.js +110 -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 +86 -12
- package/initramfs/usr/share/doc/buninu-linux/chvt.md +34 -0
- package/initramfs/usr/share/doc/buninu-linux/dhclient.md +82 -0
- package/initramfs/usr/share/doc/buninu-linux/ping.md +91 -0
- package/initramfs/usr/share/doc/buninu-linux/xdg-open.md +43 -0
- package/package.json +2 -2
- 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 +5 -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/dhcp-start.test.js +67 -0
- package/test/dhcp.test.js +99 -0
- package/test/fbdev-sync.test.js +33 -0
- package/test/fetch-npm.test.js +1 -1
- package/test/gpu.test.js +155 -0
- package/test/image-size.test.js +17 -0
- package/test/libc.test.js +41 -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/ping.test.js +80 -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
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
// "linux" console sequences for a few keys are rewritten into the xterm
|
|
7
7
|
// forms that programs seeing TERM=xterm-256color expect.
|
|
8
8
|
|
|
9
|
-
import { closeSync, openSync,
|
|
9
|
+
import { closeSync, openSync, readSync, constants } from "node:fs";
|
|
10
10
|
import { libc, ptr, errno, strerror } from "../dlopen.js";
|
|
11
|
+
import { ConsoleKeys, MediumRawDecoder } from "./keycodes.js";
|
|
11
12
|
|
|
12
13
|
const TCGETS = 0x5401;
|
|
13
14
|
const TCSETS = 0x5402;
|
|
@@ -108,33 +109,75 @@ export const translateKeys = (text, { applicationCursorKeys = false } = {}) => {
|
|
|
108
109
|
// Opens the keyboard: `device` names a console such as /dev/tty1; without
|
|
109
110
|
// it the process's own stdin is used (it must be a terminal). `onData`
|
|
110
111
|
// receives Buffers. close() restores the previous terminal settings.
|
|
111
|
-
export const openKeyboard = ({ device = null, onData, onError = () => {} }) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
export const openKeyboard = ({ device = null, onData, onKey = () => {}, onError = () => {} }) => {
|
|
113
|
+
// Own a nonblocking file description: no outstanding async read can deliver
|
|
114
|
+
// bytes from the previous keyboard mode after a protocol transition.
|
|
115
|
+
const fd = openSync(device || "/proc/self/fd/0", constants.O_RDWR | constants.O_NONBLOCK);
|
|
116
|
+
let saved;
|
|
117
|
+
try {
|
|
118
|
+
saved = getTermios(fd);
|
|
119
|
+
setTermios(fd, rawTermios(saved));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
closeSync(fd);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
const originalMode = new Int32Array(1);
|
|
125
|
+
const canReadMode = libc.symbols.ioctl(fd, 0x4b44, ptr(originalMode)) === 0;
|
|
126
|
+
let enhanced = false, decoder, keys;
|
|
127
|
+
const setEnhanced = (enabled) => {
|
|
128
|
+
if (enabled === enhanced) return;
|
|
129
|
+
if (!canReadMode) throw new Error("Kitty keyboard needs a Linux virtual console (KDGKBMODE)");
|
|
130
|
+
const maps = [];
|
|
131
|
+
if (enabled) {
|
|
132
|
+
// Unicode keysyms can only be queried while the console is still in
|
|
133
|
+
// K_UNICODE, before switching to K_MEDIUMRAW.
|
|
134
|
+
for (let table = 0; table < 4; table++) {
|
|
135
|
+
maps[table] = [];
|
|
136
|
+
for (let number = 0; number < 256; number++) {
|
|
137
|
+
const entry = new Uint8Array([table,number,0,0]);
|
|
138
|
+
maps[table][number] = libc.symbols.ioctl(fd,0x4b46,ptr(entry)) === 0
|
|
139
|
+
? new DataView(entry.buffer).getUint16(2,true) : null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Discard only console typeahead at the mode boundary, never PTY input.
|
|
144
|
+
if (libc.symbols.ioctl(fd, 0x540b, 0) === -1) fail("TCFLSH");
|
|
145
|
+
if (libc.symbols.ioctl(fd, 0x4b45, enabled ? 2 : originalMode[0]) === -1) fail("KDSKBMODE");
|
|
146
|
+
enhanced = enabled;
|
|
147
|
+
decoder = new MediumRawDecoder();
|
|
148
|
+
const leds = new Uint8Array(1);
|
|
149
|
+
libc.symbols.ioctl(fd, 0x4b64, ptr(leds));
|
|
150
|
+
keys = new ConsoleKeys({ leds: leds[0], lookup: (table, number) => maps[table]?.[number] });
|
|
151
|
+
};
|
|
115
152
|
let open = true;
|
|
153
|
+
let timer;
|
|
116
154
|
const chunk = Buffer.alloc(4096);
|
|
117
155
|
const pump = () => {
|
|
118
156
|
if (!open) return;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
157
|
+
try {
|
|
158
|
+
for (let batch = 0; batch < 16; batch++) {
|
|
159
|
+
const count = readSync(fd, chunk, 0, chunk.length, null);
|
|
160
|
+
if (!count) break;
|
|
161
|
+
if (enhanced) decoder.feed(chunk.subarray(0, count), (code, down) => onKey(keys.event(code, down)));
|
|
162
|
+
else onData(Buffer.from(chunk.subarray(0, count)));
|
|
125
163
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error.code !== "EAGAIN" && error.code !== "EWOULDBLOCK") { onError(error); return; }
|
|
166
|
+
}
|
|
167
|
+
timer = setTimeout(pump, 4);
|
|
129
168
|
};
|
|
130
169
|
pump();
|
|
131
170
|
return {
|
|
132
171
|
fd,
|
|
172
|
+
setEnhanced,
|
|
173
|
+
clearHeld() { keys?.held.clear(); },
|
|
133
174
|
close() {
|
|
134
175
|
if (!open) return;
|
|
135
176
|
open = false;
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
try { setEnhanced(false); } catch (error) { onError(error); }
|
|
136
179
|
try { setTermios(fd, saved); } catch {}
|
|
137
|
-
|
|
180
|
+
closeSync(fd);
|
|
138
181
|
},
|
|
139
182
|
};
|
|
140
183
|
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { legacyKey } from "./keycodes.js";
|
|
3
|
+
const { KittyKeyboard } = createRequire(import.meta.url)("./kitty-keyboard.cjs");
|
|
4
|
+
|
|
5
|
+
// The same protocol engine as jsgotty, with a console-mode notification.
|
|
6
|
+
// Wait for the console to exist before installing handlers/advertising support.
|
|
7
|
+
export function attachKeyboard({term, write, setEnhanced, localInput = () => false,
|
|
8
|
+
switchVT = () => {}, switchRelative = () => {}, onError = () => {}}) {
|
|
9
|
+
let keyboard, enhanced = false;
|
|
10
|
+
const sync = () => {
|
|
11
|
+
const next = Boolean(keyboard?.flags);
|
|
12
|
+
if (next === enhanced) return;
|
|
13
|
+
try {
|
|
14
|
+
setEnhanced(next);
|
|
15
|
+
enhanced = next;
|
|
16
|
+
keyboard.clearHeld(false);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
keyboard.reset();
|
|
19
|
+
onError(error);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const wrap = name => (id, fn) => term.parser[name](id, params => {
|
|
23
|
+
const result = fn(params);
|
|
24
|
+
sync();
|
|
25
|
+
return result;
|
|
26
|
+
});
|
|
27
|
+
keyboard = new KittyKeyboard({
|
|
28
|
+
parser: {registerCsiHandler:wrap("registerCsiHandler"),registerEscHandler:wrap("registerEscHandler")},
|
|
29
|
+
buffer:term.buffer,options:term.options,
|
|
30
|
+
input: text => write(text),
|
|
31
|
+
});
|
|
32
|
+
const bufferChange = term.buffer.onBufferChange(sync);
|
|
33
|
+
const reserved = new Set();
|
|
34
|
+
return {
|
|
35
|
+
get flags() { return keyboard.flags; },
|
|
36
|
+
event(event) {
|
|
37
|
+
// Mediumraw disables the kernel's console shortcuts. Keep Ctrl-Alt-Fn
|
|
38
|
+
// available even when a client has requested all keys.
|
|
39
|
+
if (event.type === "keydown" && event.ctrlKey && event.altKey && /^F([1-9]|1[0-2])$/.test(event.key)) {
|
|
40
|
+
reserved.add(event.code);
|
|
41
|
+
if (!event.repeat) switchVT(Number(event.key.slice(1)));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (event.type === "keydown" && event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey &&
|
|
45
|
+
["ArrowLeft","ArrowRight"].includes(event.key)) {
|
|
46
|
+
reserved.add(event.code);
|
|
47
|
+
if (!event.repeat) switchRelative(event.key === "ArrowLeft" ? -1 : 1);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (event.type === "keyup" && reserved.delete(event.code)) return;
|
|
51
|
+
const text = legacyKey(event, Boolean(term._core?.coreService?.decPrivateModes?.applicationCursorKeys));
|
|
52
|
+
if (localInput(event, text)) { keyboard.clearHeld(); return; }
|
|
53
|
+
if (keyboard.handle(event) && text) write(text);
|
|
54
|
+
},
|
|
55
|
+
blur() { keyboard.clearHeld(); reserved.clear(); },
|
|
56
|
+
dispose() {
|
|
57
|
+
bufferChange.dispose();
|
|
58
|
+
keyboard.dispose();
|
|
59
|
+
if (enhanced) setEnhanced(false);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Linux K_MEDIUMRAW -> browser-shaped key events for the shared Kitty encoder.
|
|
2
|
+
// The kernel keymap supplies text; these names identify physical/function keys.
|
|
3
|
+
const keys = {};
|
|
4
|
+
const row = (start, names) => names.split(" ").forEach((name, i) => { keys[start + i] = name; });
|
|
5
|
+
row(1, "Escape Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Backspace Tab");
|
|
6
|
+
row(16, "KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Enter ControlLeft");
|
|
7
|
+
row(30, "KeyA KeyS KeyD KeyF KeyG KeyH KeyJ KeyK KeyL Semicolon Quote Backquote ShiftLeft Backslash");
|
|
8
|
+
row(44, "KeyZ KeyX KeyC KeyV KeyB KeyN KeyM Comma Period Slash ShiftRight NumpadMultiply AltLeft Space CapsLock");
|
|
9
|
+
row(59, "F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 NumLock ScrollLock Numpad7 Numpad8 Numpad9 NumpadSubtract Numpad4 Numpad5 Numpad6 NumpadAdd Numpad1 Numpad2 Numpad3 Numpad0 NumpadDecimal");
|
|
10
|
+
row(86, "IntlBackslash F11 F12");
|
|
11
|
+
row(96, "NumpadEnter ControlRight NumpadDivide PrintScreen AltRight");
|
|
12
|
+
row(102, "Home ArrowUp PageUp ArrowLeft ArrowRight End ArrowDown PageDown Insert Delete");
|
|
13
|
+
Object.assign(keys, {113:"AudioVolumeMute",114:"AudioVolumeDown",115:"AudioVolumeUp",
|
|
14
|
+
117:"NumpadEqual",119:"Pause",121:"NumpadComma",125:"MetaLeft",126:"MetaRight",
|
|
15
|
+
127:"ContextMenu",163:"MediaTrackNext",164:"MediaPlayPause",165:"MediaTrackPrevious",
|
|
16
|
+
166:"MediaStop",167:"MediaRecord",168:"MediaRewind",208:"MediaFastForward"});
|
|
17
|
+
row(183, "F13 F14 F15 F16 F17 F18 F19 F20 F21 F22 F23 F24");
|
|
18
|
+
const special = {NumpadEnter:"Enter",NumpadDivide:"/",NumpadMultiply:"*",NumpadAdd:"+",
|
|
19
|
+
NumpadSubtract:"-",NumpadDecimal:".",NumpadEqual:"=",NumpadComma:","};
|
|
20
|
+
const navigation = ["Insert","End","ArrowDown","PageDown","ArrowLeft","Clear","ArrowRight","Home","ArrowUp","PageUp"];
|
|
21
|
+
|
|
22
|
+
// Mediumraw encodes >=128 as prefix (0 or 128), then two 7-bit groups
|
|
23
|
+
// with bit 7 set. Preserve incomplete triples across read() boundaries.
|
|
24
|
+
export class MediumRawDecoder {
|
|
25
|
+
pending = [];
|
|
26
|
+
feed(bytes, emit) {
|
|
27
|
+
for (const byte of bytes) {
|
|
28
|
+
this.pending.push(byte);
|
|
29
|
+
const first = this.pending[0];
|
|
30
|
+
if ((first & 127) !== 0) {
|
|
31
|
+
emit(first & 127, !(first & 128));
|
|
32
|
+
this.pending = [];
|
|
33
|
+
} else if (this.pending.length === 3) {
|
|
34
|
+
emit(((this.pending[1] & 127) << 7) | (this.pending[2] & 127), !(first & 128));
|
|
35
|
+
this.pending = [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class ConsoleKeys {
|
|
42
|
+
constructor({ lookup, leds = 0 } = {}) {
|
|
43
|
+
this.lookup = lookup;
|
|
44
|
+
this.held = new Set();
|
|
45
|
+
this.caps = Boolean(leds & 4);
|
|
46
|
+
this.num = Boolean(leds & 2);
|
|
47
|
+
}
|
|
48
|
+
event(number, down) {
|
|
49
|
+
const repeat = down && this.held.has(number);
|
|
50
|
+
if (down) this.held.add(number); else this.held.delete(number);
|
|
51
|
+
if (down && !repeat && number === 58) this.caps = !this.caps;
|
|
52
|
+
if (down && !repeat && number === 69) this.num = !this.num;
|
|
53
|
+
const shiftKey = this.held.has(42) || this.held.has(54);
|
|
54
|
+
const ctrlKey = this.held.has(29) || this.held.has(97);
|
|
55
|
+
const altKey = this.held.has(56);
|
|
56
|
+
const altgr = this.held.has(100);
|
|
57
|
+
const metaKey = this.held.has(125) || this.held.has(126);
|
|
58
|
+
const code = keys[number] || `LinuxKey${number}`;
|
|
59
|
+
let key = code.replace(/^(Shift|Control|Alt|Meta)(Left|Right)$/, "$1");
|
|
60
|
+
if (number === 100) key = "AltGraph";
|
|
61
|
+
else if (special[code]) key = special[code];
|
|
62
|
+
else if (/^Numpad\d$/.test(code)) key = this.num !== shiftKey ? code.slice(6) : navigation[Number(code.slice(6))];
|
|
63
|
+
else if (/^(Key|Digit|Intl)/.test(code) || ["Space","Minus","Equal","BracketLeft","BracketRight",
|
|
64
|
+
"Backslash","Semicolon","Quote","Backquote","Comma","Period","Slash"].includes(code)) {
|
|
65
|
+
// Exclude Ctrl/Alt from translation: the encoder needs the character,
|
|
66
|
+
// not an already-collapsed control byte or an ESC prefix.
|
|
67
|
+
const table = (shiftKey ? 1 : 0) | (altgr ? 2 : 0);
|
|
68
|
+
let symbol = this.lookup?.(table, number);
|
|
69
|
+
if ((symbol >> 8) === 11 && this.caps) symbol = this.lookup?.(table ^ 1, number);
|
|
70
|
+
const type = symbol >> 8;
|
|
71
|
+
key = symbol == null ? "Unidentified"
|
|
72
|
+
: type === 0 || type === 11 ? String.fromCodePoint(symbol & 255)
|
|
73
|
+
: type >= 16 ? String.fromCodePoint(symbol ^ 0xf000)
|
|
74
|
+
: type === 4 || type === 13 ? "Dead" : "Unidentified";
|
|
75
|
+
}
|
|
76
|
+
return {type:down?"keydown":"keyup",code,key,repeat,shiftKey,ctrlKey,altKey,metaKey,
|
|
77
|
+
location:/^Numpad/.test(code)?3:/Right$/.test(code)?2:/Left$/.test(code)?1:0,
|
|
78
|
+
getModifierState: name => name === "CapsLock" ? this.caps : name === "NumLock" ? this.num : name === "AltGraph" && altgr,
|
|
79
|
+
preventDefault() {}, stopPropagation() {}};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Recovery/plain-text path when the negotiated enhancements leave a key in
|
|
84
|
+
// legacy form. Used only in Kitty mode; normal console input stays unchanged.
|
|
85
|
+
export function legacyKey(event, applicationCursorKeys = false) {
|
|
86
|
+
if (event.type !== "keydown") return "";
|
|
87
|
+
const { key, ctrlKey, altKey, shiftKey } = event;
|
|
88
|
+
let text = ({Enter:"\r",Tab:shiftKey?"\x1b[Z":"\t",Backspace:"\x7f",Escape:"\x1b"})[key];
|
|
89
|
+
const cursor = {ArrowUp:"A",ArrowDown:"B",ArrowRight:"C",ArrowLeft:"D",Home:"H",End:"F"}[key];
|
|
90
|
+
if (cursor) text = `\x1b${applicationCursorKeys?"O":"["}${cursor}`;
|
|
91
|
+
const tilde = {Insert:2,Delete:3,PageUp:5,PageDown:6}[key];
|
|
92
|
+
if (tilde) text = `\x1b[${tilde}~`;
|
|
93
|
+
if (text === undefined && [...key].length === 1) {
|
|
94
|
+
text = key;
|
|
95
|
+
if (ctrlKey) {
|
|
96
|
+
const n = key.toUpperCase().codePointAt(0);
|
|
97
|
+
if (n >= 64 && n <= 95) text = String.fromCharCode(n & 31);
|
|
98
|
+
else if (key === " " || key === "2") text = "\0";
|
|
99
|
+
else if (key === "/" || key === "?") text = "\x1f";
|
|
100
|
+
else if (key === "8") text = "\x7f";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return text === undefined ? "" : (altKey ? "\x1b" : "") + text;
|
|
104
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Vendored from jsgotty 1.2.0, static/js/kitty-keyboard.js (MIT).
|
|
2
|
+
// Keep protocol behavior aligned with the browser terminal.
|
|
3
|
+
// Kitty keyboard transport for the shipped xterm wrapper (no build step).
|
|
4
|
+
// Spec: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
5
|
+
(function (root) {
|
|
6
|
+
'use strict';
|
|
7
|
+
// Supported progressive enhancements: disambiguation, event types, all keys.
|
|
8
|
+
// Do not advertise alternate-layout/text reporting until those are implemented.
|
|
9
|
+
const SUPPORTED = 1 | 2 | 8;
|
|
10
|
+
const functional = {
|
|
11
|
+
Escape:[27,'u'], Enter:[13,'u'], Tab:[9,'u'], Backspace:[127,'u'],
|
|
12
|
+
Insert:[2,'~'], Delete:[3,'~'], ArrowLeft:[1,'D'], ArrowRight:[1,'C'],
|
|
13
|
+
ArrowUp:[1,'A'], ArrowDown:[1,'B'], PageUp:[5,'~'], PageDown:[6,'~'],
|
|
14
|
+
Home:[1,'H'], End:[1,'F'], CapsLock:[57358,'u'], ScrollLock:[57359,'u'],
|
|
15
|
+
NumLock:[57360,'u'], PrintScreen:[57361,'u'], Pause:[57362,'u'], ContextMenu:[57363,'u'],
|
|
16
|
+
MediaPlay:[57428,'u'], MediaPause:[57429,'u'], MediaPlayPause:[57430,'u'],
|
|
17
|
+
MediaStop:[57432,'u'], MediaFastForward:[57433,'u'], MediaRewind:[57434,'u'],
|
|
18
|
+
MediaTrackNext:[57435,'u'], MediaTrackPrevious:[57436,'u'], MediaRecord:[57437,'u'],
|
|
19
|
+
AudioVolumeDown:[57438,'u'], AudioVolumeUp:[57439,'u'], AudioVolumeMute:[57440,'u'],
|
|
20
|
+
};
|
|
21
|
+
[11,12,13,14,15,17,18,19,20,21,23,24].forEach((n,i) => functional['F'+(i+1)] = [n,'~']);
|
|
22
|
+
for (let i=13;i<=35;i++) functional['F'+i] = [57376+i-13,'u'];
|
|
23
|
+
const punctuation = {
|
|
24
|
+
Backquote:['`','~'], Minus:['-','_'], Equal:['=','+'], BracketLeft:['[','{'],
|
|
25
|
+
BracketRight:[']','}'], Backslash:['\\','|'], Semicolon:[';',':'], Quote:["'",'"'],
|
|
26
|
+
Comma:[',','<'], Period:['.','>'], Slash:['/','?'], Space:[' ',' '],
|
|
27
|
+
};
|
|
28
|
+
const modifiers = {Shift:[1,57441], Control:[4,57442], Alt:[2,57443], Meta:[8,57444],
|
|
29
|
+
OS:[8,57444], Super:[8,57444], Hyper:[16,57445], AltGraph:[0,57453]};
|
|
30
|
+
const has = (object,key) => Object.prototype.hasOwnProperty.call(object,key);
|
|
31
|
+
const lock = (e,name) => Boolean(e.getModifierState?.(name));
|
|
32
|
+
function physicalCode(e) {
|
|
33
|
+
if (e.code && e.code !== 'Unidentified') return e.code;
|
|
34
|
+
if (e.keyCode>=65 && e.keyCode<=90) return 'Key'+String.fromCharCode(e.keyCode);
|
|
35
|
+
if (e.keyCode>=48 && e.keyCode<=57) return 'Digit'+String.fromCharCode(e.keyCode);
|
|
36
|
+
return ({32:'Space',186:'Semicolon',187:'Equal',188:'Comma',189:'Minus',190:'Period',
|
|
37
|
+
191:'Slash',192:'Backquote',219:'BracketLeft',220:'Backslash',221:'BracketRight',222:'Quote'})[e.keyCode] || '';
|
|
38
|
+
}
|
|
39
|
+
function baseCharacter(e, android) {
|
|
40
|
+
const code=physicalCode(e);
|
|
41
|
+
// Android's Alt accent conversion must not turn an E/I/N/U shortcut into a dead key.
|
|
42
|
+
const recover=android && e.altKey && !lock(e,'AltGraph');
|
|
43
|
+
if (recover || ([...e.key || ''].length===1 && e.key.codePointAt(0)<32)) {
|
|
44
|
+
if (/^Key[A-Z]$/.test(code)) return code.slice(3).toLowerCase();
|
|
45
|
+
if (/^Digit[0-9]$/.test(code)) return code.slice(5);
|
|
46
|
+
if (has(punctuation,code)) return punctuation[code][0];
|
|
47
|
+
}
|
|
48
|
+
if ([...e.key || ''].length!==1) return null;
|
|
49
|
+
if (e.shiftKey) {
|
|
50
|
+
if (has(punctuation,code) && punctuation[code][1]===e.key) return punctuation[code][0];
|
|
51
|
+
if (/^Digit[0-9]$/.test(code) && ')!@#$%^&*('[Number(code.slice(5))]===e.key) return code.slice(5);
|
|
52
|
+
}
|
|
53
|
+
const lower=e.key.toLowerCase();
|
|
54
|
+
return [...lower].length===1 ? lower : e.key;
|
|
55
|
+
}
|
|
56
|
+
function keyDefinition(e, flags, android) {
|
|
57
|
+
if (has(modifiers,e.key)) {
|
|
58
|
+
const [bit,left]=modifiers[e.key];
|
|
59
|
+
const right=e.location===2 || /Right$/.test(e.code || '');
|
|
60
|
+
return {number:left+(right && e.key!=='AltGraph'?6:0),suffix:'u',modifier:bit};
|
|
61
|
+
}
|
|
62
|
+
if ((flags & 9) && (e.location===3 || /^Numpad/.test(e.code || ''))) {
|
|
63
|
+
const numpad={NumpadDecimal:57409,NumpadDivide:57410,NumpadMultiply:57411,
|
|
64
|
+
NumpadSubtract:57412,NumpadAdd:57413,NumpadEnter:57414,NumpadEqual:57415,
|
|
65
|
+
NumpadComma:57416};
|
|
66
|
+
const nav={ArrowLeft:57417,ArrowRight:57418,ArrowUp:57419,ArrowDown:57420,
|
|
67
|
+
PageUp:57421,PageDown:57422,Home:57423,End:57424,Insert:57425,Delete:57426,Clear:57427};
|
|
68
|
+
const number=has(nav,e.key)?nav[e.key]:/^Numpad[0-9]$/.test(e.code || '')
|
|
69
|
+
?57399+Number(e.code.slice(6)):numpad[e.code];
|
|
70
|
+
if (number) return {number,suffix:'u'};
|
|
71
|
+
}
|
|
72
|
+
if (has(functional,e.key)) {
|
|
73
|
+
const [number,suffix]=functional[e.key];
|
|
74
|
+
return {number,suffix,recovery:['Enter','Tab','Backspace'].includes(e.key)};
|
|
75
|
+
}
|
|
76
|
+
const ch=baseCharacter(e,android);
|
|
77
|
+
return ch===null?null:{number:ch.codePointAt(0),suffix:'u',text:true};
|
|
78
|
+
}
|
|
79
|
+
class KittyKeyboard {
|
|
80
|
+
constructor(term, {android=false}={}) {
|
|
81
|
+
this.term=term; this.android=android; this.disposables=[];
|
|
82
|
+
this.metaKeys=new Set(); this.down=new Map(); this.suppressed=new Set(); this.reset();
|
|
83
|
+
const register=(prefix,fn) => this.disposables.push(term.parser.registerCsiHandler({prefix,final:'u'},p=>{fn(p);return true;}));
|
|
84
|
+
const value=(p,i,fallback=0) => Number.isInteger(p[i])?p[i]:fallback;
|
|
85
|
+
register('?',()=>term.input(`\x1b[?${this.flags}u`,false));
|
|
86
|
+
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;});
|
|
87
|
+
register('<',p=>{const s=this.state;let count=Math.min(value(p,0)||1,33);while(count--)s.flags=s.stack.pop()??0;});
|
|
88
|
+
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;});
|
|
89
|
+
this.disposables.push(term.parser.registerEscHandler({final:'c'},()=>{this.reset();return false;}));
|
|
90
|
+
this.disposables.push(term.parser.registerCsiHandler({intermediates:'!',final:'p'},()=>{this.reset();return false;}));
|
|
91
|
+
}
|
|
92
|
+
get state() { return this.states[this.term.buffer.active.type==='alternate'?'alternate':'normal']; }
|
|
93
|
+
get flags() { return this.state.flags; }
|
|
94
|
+
reset() {
|
|
95
|
+
this.states={normal:{flags:0,stack:[]},alternate:{flags:0,stack:[]}};
|
|
96
|
+
this.clearHeld(false);
|
|
97
|
+
}
|
|
98
|
+
clearHeld(report=true) {
|
|
99
|
+
// Release keys reported to the client before losing focus, avoiding stuck remote keys.
|
|
100
|
+
if (report && (this.flags & 2)) for (const key of this.down.values()) {
|
|
101
|
+
if(key.reported) this.term.input(this.sequence(key.def,0,3),true);
|
|
102
|
+
}
|
|
103
|
+
this.metaKeys.clear();this.down.clear();this.suppressed.clear();this.suppressKeypress=false;
|
|
104
|
+
}
|
|
105
|
+
attach(element) {
|
|
106
|
+
const doc=element.ownerDocument, win=doc.defaultView;
|
|
107
|
+
const blur=()=>this.clearHeld();
|
|
108
|
+
const visibility=()=>{if(doc.hidden)this.clearHeld();};
|
|
109
|
+
element.addEventListener('blur',blur);win.addEventListener('blur',blur);
|
|
110
|
+
doc.addEventListener('visibilitychange',visibility);
|
|
111
|
+
this.disposables.push({dispose:()=>{element.removeEventListener('blur',blur);win.removeEventListener('blur',blur);doc.removeEventListener('visibilitychange',visibility);}});
|
|
112
|
+
}
|
|
113
|
+
dispose() {this.clearHeld(false);for(const d of this.disposables)d.dispose();}
|
|
114
|
+
sequence(def,bits,type) {
|
|
115
|
+
const event=(this.flags&2) && type!==1?':'+type:'';
|
|
116
|
+
const mods=bits+1;
|
|
117
|
+
if(def.suffix!=='u' && mods===1 && !event)
|
|
118
|
+
return '\x1b['+(def.number===1?'':def.number)+def.suffix;
|
|
119
|
+
return `\x1b[${def.number}${mods!==1||event?';'+mods+event:''}${def.suffix}`;
|
|
120
|
+
}
|
|
121
|
+
handle(event) {
|
|
122
|
+
if (!['keydown','keyup','keypress'].includes(event.type)) return true;
|
|
123
|
+
const identity=physicalCode(event)||event.key;
|
|
124
|
+
const metaKey=['Meta','OS','Super'].includes(event.key);
|
|
125
|
+
if(this.android && metaKey) {
|
|
126
|
+
if(event.type==='keydown')this.metaKeys.add(event.code||String(event.location||0));
|
|
127
|
+
if(event.type==='keyup')this.metaKeys.delete(event.code||String(event.location||0));
|
|
128
|
+
}
|
|
129
|
+
// Patch only this terminal's Android event, preserving the native DOM event.
|
|
130
|
+
if(this.android && this.metaKeys.size && !event.metaKey) {
|
|
131
|
+
const original=event.getModifierState?.bind(event);
|
|
132
|
+
Object.defineProperties(event,{
|
|
133
|
+
metaKey:{configurable:true,value:true},
|
|
134
|
+
getModifierState:{configurable:true,value:name=>name==='Meta'||Boolean(original?.(name))},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if(!this.flags || this.term.options.disableStdin) return true;
|
|
138
|
+
const consume=()=>{event.preventDefault();event.stopPropagation();return false;};
|
|
139
|
+
if(event.type==='keypress')return this.suppressKeypress?consume():true;
|
|
140
|
+
let previous=this.down.get(identity);
|
|
141
|
+
if(event.type==='keyup') {
|
|
142
|
+
// Some Android IMEs emit only the release of an Alt dead key. Recover
|
|
143
|
+
// that chord here too, never falling back to legacy ESC text in Kitty mode.
|
|
144
|
+
if(!previous && this.android && event.altKey && event.key==='Dead' &&
|
|
145
|
+
!event.isComposing && !lock(event,'AltGraph') && (this.flags&9)) {
|
|
146
|
+
const def=keyDefinition(event,this.flags,this.android);
|
|
147
|
+
if(def) {
|
|
148
|
+
this.term.input(this.sequence(def,this.bits(event),1),true);
|
|
149
|
+
previous={def,reported:true};this.suppressed.add(identity);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
this.down.delete(identity);
|
|
153
|
+
const suppressed=this.suppressed.delete(identity);
|
|
154
|
+
this.suppressKeypress=false;
|
|
155
|
+
if(previous?.reported && (this.flags&2)) {
|
|
156
|
+
let bits=this.bits(event);
|
|
157
|
+
if(previous.def.modifier && ![...this.down.values()].some(k=>k.def.modifier===previous.def.modifier)) bits &= ~previous.def.modifier;
|
|
158
|
+
if(metaKey && this.metaKeys.size) bits|=8;
|
|
159
|
+
this.term.input(this.sequence(previous.def,bits,3),true);
|
|
160
|
+
}
|
|
161
|
+
return suppressed?consume():true;
|
|
162
|
+
}
|
|
163
|
+
this.suppressKeypress=false;
|
|
164
|
+
if(event.isComposing || event.keyCode===229 && event.key==='Unidentified')return true;
|
|
165
|
+
if(event.key==='Dead' && !(this.android && event.altKey && !lock(event,'AltGraph')))return true;
|
|
166
|
+
const def=keyDefinition(event,this.flags,this.android);
|
|
167
|
+
if(!def)return true;
|
|
168
|
+
let bits=this.bits(event);
|
|
169
|
+
if(def.modifier!==undefined) {
|
|
170
|
+
if(!(this.flags&8))return true;
|
|
171
|
+
bits|=def.modifier;
|
|
172
|
+
}
|
|
173
|
+
const all=Boolean(this.flags&8);
|
|
174
|
+
// Enter/Tab/Backspace remain recovery keys unless modified or report-all is requested.
|
|
175
|
+
if(!all && def.recovery && !(bits&~192))return true;
|
|
176
|
+
const shortcut=Boolean(bits&62);
|
|
177
|
+
if(def.text && !all && !(this.flags&1 && shortcut))return true;
|
|
178
|
+
// With only event reporting requested, leave legacy printable shortcuts untouched.
|
|
179
|
+
if(def.text && !all && !(this.flags&1))return true;
|
|
180
|
+
const type=event.repeat?2:1;
|
|
181
|
+
this.down.set(identity,{def,reported:!(def.recovery && !all)});this.suppressed.add(identity);this.suppressKeypress=true;
|
|
182
|
+
this.term.input(this.sequence(def,bits,type),true);
|
|
183
|
+
return consume();
|
|
184
|
+
}
|
|
185
|
+
bits(e) {
|
|
186
|
+
const altgr=lock(e,'AltGraph');
|
|
187
|
+
return (e.shiftKey?1:0)|(!altgr&&e.altKey?2:0)|(!altgr&&e.ctrlKey?4:0)|
|
|
188
|
+
(e.metaKey || this.android&&this.metaKeys.size?8:0)|
|
|
189
|
+
(lock(e,'Hyper')?16:0)|(lock(e,'CapsLock')?64:0)|(lock(e,'NumLock')?128:0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const api={KittyKeyboard,SUPPORTED};
|
|
193
|
+
if(typeof module==='object' && module.exports)module.exports=api;
|
|
194
|
+
else root.GoTTYKittyKeyboard=api;
|
|
195
|
+
})(globalThis);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Buninu private extension; not a registered Kitty pixel format.
|
|
2
|
+
export const BGRA_FORMAT = 0x42475241;
|
|
3
|
+
export function preferredPixelFormat(display, littleEndian = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1) {
|
|
4
|
+
if (!littleEndian || display?.bitsPerPixel !== 32) return 'png';
|
|
5
|
+
const f=display.fields;
|
|
6
|
+
const channel=(field,offset)=>field?.offset===offset&&field.length===8&&!field.msbRight;
|
|
7
|
+
const alpha=f?.transp;
|
|
8
|
+
if(alpha?.length && !channel(alpha,24))return 'png';
|
|
9
|
+
if(!channel(f?.green,8))return 'png';
|
|
10
|
+
if(channel(f?.red,16)&&channel(f?.blue,0))return 'bgra';
|
|
11
|
+
if(channel(f?.red,0)&&channel(f?.blue,16))return 'rgba';
|
|
12
|
+
return 'png';
|
|
13
|
+
}
|
|
14
|
+
// Only the native renderer advertises these formats; browser jsgotty is unchanged.
|
|
15
|
+
export function bgraParser(Base,display) {
|
|
16
|
+
return class extends Base {
|
|
17
|
+
_resolveImageData(control,payload) {
|
|
18
|
+
const bytes=super._resolveImageData(control,payload);
|
|
19
|
+
if(Number(control.f)===BGRA_FORMAT){
|
|
20
|
+
const w=Number(control.s),h=Number(control.v);
|
|
21
|
+
if(!Number.isInteger(w)||!Number.isInteger(h)||w<1||h<1||w*h>16777216||bytes.length!==w*h*4)
|
|
22
|
+
throw Error('Invalid BGRA dimensions or byte count');
|
|
23
|
+
}
|
|
24
|
+
return bytes;
|
|
25
|
+
}
|
|
26
|
+
_parsePacket(packet,cursor) {
|
|
27
|
+
const result=super._parsePacket(packet,cursor);
|
|
28
|
+
if(result?.kind==='query'&&Number(result.control.f)===BGRA_FORMAT&&result.responseMessage==='OK')
|
|
29
|
+
result.responseMessage='OK Buninu1 '+preferredPixelFormat(display);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|