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.
Files changed (109) hide show
  1. package/LICENSES/linux-firmware/LICENCE.nvidia +131 -0
  2. package/LICENSES/linux-firmware/LICENSE.amdgpu +51 -0
  3. package/LICENSES/linux-firmware/LICENSE.amdisp +31 -0
  4. package/LICENSES/linux-firmware/LICENSE.i915 +39 -0
  5. package/LICENSES/linux-firmware/LICENSE.radeon +51 -0
  6. package/LICENSES/linux-firmware/LICENSE.xe +39 -0
  7. package/LICENSES/linux-firmware/WHENCE +10280 -0
  8. package/NOTICE.md +64 -46
  9. package/README.md +410 -96
  10. package/README.zh.md +2034 -0
  11. package/build-image.sh +10 -1
  12. package/build-uki.sh +3 -1
  13. package/fetch-alpine.sh +15 -1
  14. package/graphics.md +31 -0
  15. package/index.js +28 -11
  16. package/initramfs/bin/buninu-browser +4 -1
  17. package/initramfs/bin/buninu-land +1 -1
  18. package/initramfs/bin/bunterm +3 -3
  19. package/initramfs/bin/chroot +13 -0
  20. package/initramfs/bin/chvt +11 -0
  21. package/initramfs/bin/dhclient +8 -0
  22. package/initramfs/bin/mount +4 -0
  23. package/initramfs/bin/ping +8 -0
  24. package/initramfs/bin/umount +6 -0
  25. package/initramfs/bin/xdg-open +33 -0
  26. package/initramfs/buninu/CHANGELOG.md +19 -0
  27. package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +4 -0
  28. package/initramfs/buninu/apps/bunmsh/package.json +1 -1
  29. package/initramfs/buninu/apps/bunmsh/src/main.js +1 -1
  30. package/initramfs/buninu/apps/jsgotty/README.en.md +40 -0
  31. package/initramfs/buninu/apps/jsgotty/README.md +40 -0
  32. package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
  33. package/initramfs/buninu/apps/jsgotty/package.json +1 -1
  34. package/initramfs/buninu/apps/jsgotty/static/index.html +1 -0
  35. package/initramfs/buninu/apps/jsgotty/static/js/gotty.js +82 -17
  36. package/initramfs/buninu/apps/jsgotty/static/js/kitty-keyboard.js +193 -0
  37. package/initramfs/buninu/apps/jsgotty/tests/android-alt.test.js +115 -0
  38. package/initramfs/buninu/apps/jsgotty/tests/arrow-modifiers.test.js +80 -0
  39. package/initramfs/buninu/apps/jsgotty/tests/kitty-keyboard-browser.test.js +53 -0
  40. package/initramfs/buninu/apps/jsgotty/tests/kitty-keyboard.test.js +102 -0
  41. package/initramfs/buninu/apps/jsgotty/tests/manual/README.md +20 -0
  42. package/initramfs/buninu/apps/jsgotty/tests/manual/keyboard-collector.cjs +55 -0
  43. package/initramfs/buninu/apps/jsgotty/tests/manual/keyboard-debug.js +74 -0
  44. package/initramfs/buninu/apps/jsgotty/tests/manual/kitty-keyboard.js +80 -0
  45. package/initramfs/buninu/package.json +1 -1
  46. package/initramfs/init.js +27 -10
  47. package/initramfs/lib/bunterm/clipboard.js +14 -0
  48. package/initramfs/lib/bunterm/commands.js +37 -0
  49. package/initramfs/lib/bunterm/fps.js +38 -0
  50. package/initramfs/lib/bunterm/images.js +9 -5
  51. package/initramfs/lib/bunterm/input.js +58 -15
  52. package/initramfs/lib/bunterm/keyboard.js +62 -0
  53. package/initramfs/lib/bunterm/keycodes.js +104 -0
  54. package/initramfs/lib/bunterm/kitty-keyboard.cjs +195 -0
  55. package/initramfs/lib/bunterm/raw-protocol.js +33 -0
  56. package/initramfs/lib/bunterm/renderer.js +90 -21
  57. package/initramfs/lib/bunterm/selection.js +60 -0
  58. package/initramfs/lib/bunterm/terminal.js +147 -12
  59. package/initramfs/lib/bunterm/vt-menu.js +181 -0
  60. package/initramfs/lib/bunterm/vt.js +27 -1
  61. package/initramfs/lib/canvas.js +99 -0
  62. package/initramfs/lib/chvt.js +31 -0
  63. package/initramfs/lib/dhclient.js +135 -0
  64. package/initramfs/lib/dhcp-start.js +85 -0
  65. package/initramfs/lib/dhcp-transport.js +44 -0
  66. package/initramfs/lib/dhcp.js +124 -0
  67. package/initramfs/lib/dlopen.js +58 -11
  68. package/initramfs/lib/fbdev.js +21 -3
  69. package/initramfs/lib/gpu.js +77 -0
  70. package/initramfs/lib/ping.js +110 -0
  71. package/initramfs/lib/raw-image.js +47 -0
  72. package/initramfs/lib/xdg-open.js +42 -0
  73. package/initramfs/usr/share/doc/buninu-linux/bunterm.md +86 -12
  74. package/initramfs/usr/share/doc/buninu-linux/chvt.md +34 -0
  75. package/initramfs/usr/share/doc/buninu-linux/dhclient.md +82 -0
  76. package/initramfs/usr/share/doc/buninu-linux/ping.md +91 -0
  77. package/initramfs/usr/share/doc/buninu-linux/xdg-open.md +43 -0
  78. package/package.json +2 -2
  79. package/scripts/arch.js +7 -5
  80. package/scripts/arch.sh +5 -5
  81. package/scripts/build.js +16 -3
  82. package/scripts/fetch-gpu.js +105 -0
  83. package/scripts/fetch.js +11 -1
  84. package/scripts/gpu-firmware.json +22 -0
  85. package/scripts/modules.js +5 -1
  86. package/scripts/npm-packages.md +17 -17
  87. package/test/bunterm-fps.test.js +17 -0
  88. package/test/bunterm-keyboard.test.js +116 -0
  89. package/test/bunterm-next-vt.test.js +9 -0
  90. package/test/bunterm-raw-protocol.test.js +19 -0
  91. package/test/bunterm-renderer.test.js +158 -0
  92. package/test/bunterm-selection.test.js +38 -0
  93. package/test/bunterm-vt-menu.test.js +178 -0
  94. package/test/chroot-bind.test.js +40 -2
  95. package/test/chvt.test.js +46 -0
  96. package/test/dhcp-start.test.js +67 -0
  97. package/test/dhcp.test.js +99 -0
  98. package/test/fbdev-sync.test.js +33 -0
  99. package/test/fetch-npm.test.js +1 -1
  100. package/test/gpu.test.js +155 -0
  101. package/test/image-size.test.js +17 -0
  102. package/test/libc.test.js +41 -0
  103. package/test/manual-bunterm-image-paths.js +31 -0
  104. package/test/mount-libc-lifetime.test.js +50 -0
  105. package/test/no-gpu.test.js +43 -0
  106. package/test/ping.test.js +80 -0
  107. package/test/readme-cli.test.js +29 -0
  108. package/test/stage-npm.test.js +11 -4
  109. 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, read } from "node:fs";
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
- const fd = device ? openSync(device, "r+") : 0;
113
- const saved = getTermios(fd);
114
- setTermios(fd, rawTermios(saved));
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
- read(fd, chunk, 0, chunk.length, null, (error, count) => {
120
- if (!open) return;
121
- if (error) {
122
- if (error.code === "EAGAIN") { setTimeout(pump, 10); return; }
123
- onError(error);
124
- return;
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
- if (count > 0) onData(Buffer.from(chunk.subarray(0, count)));
127
- pump();
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
- if (device) closeSync(fd);
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
+ }