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
|
@@ -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
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {skiaImage} from '../raw-image.js';
|
|
1
2
|
// /lib/bunterm/renderer.js — paints an xterm.js headless buffer onto a Skia
|
|
2
3
|
// canvas as a grid of cells.
|
|
3
4
|
//
|
|
@@ -64,6 +65,7 @@ export class Renderer {
|
|
|
64
65
|
this.canvas = screen.canvas;
|
|
65
66
|
this.fonts = fonts;
|
|
66
67
|
this.fontSize = fontSize;
|
|
68
|
+
this.lineHeight = lineHeight;
|
|
67
69
|
this.theme = theme;
|
|
68
70
|
this.palette = buildPalette(theme);
|
|
69
71
|
this.foreground = parseColor(theme.foreground);
|
|
@@ -127,6 +129,30 @@ export class Renderer {
|
|
|
127
129
|
return font;
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
setFontSize(size) {
|
|
133
|
+
if (!Number.isInteger(size) || size < 6 || size > 96) throw new Error("font size must be an integer from 6 to 96");
|
|
134
|
+
if (size === this.fontSize) return;
|
|
135
|
+
for (const font of this.fontCache.values()) font.delete();
|
|
136
|
+
for (const paragraph of this.paragraphCache.values()) paragraph.delete();
|
|
137
|
+
this.fontCache.clear();
|
|
138
|
+
this.metricsCache.clear();
|
|
139
|
+
this.paragraphCache.clear();
|
|
140
|
+
this.fontSize = size;
|
|
141
|
+
const font = this.fontFor(this.fonts.regular, size), metrics = font.getMetrics();
|
|
142
|
+
this.cellWidth = Math.max(1, Math.round(font.getGlyphWidths(this.fonts.regular.getGlyphIDs("M"))[0]));
|
|
143
|
+
this.cellHeight = Math.max(1, Math.round((metrics.descent-metrics.ascent+(metrics.leading||0))*this.lineHeight));
|
|
144
|
+
this.columns = Math.max(1, Math.floor(this.screen.width/this.cellWidth));
|
|
145
|
+
this.rows = Math.max(1, Math.floor(this.screen.height/this.cellHeight));
|
|
146
|
+
this.offsetX = Math.floor((this.screen.width-this.columns*this.cellWidth)/2);
|
|
147
|
+
this.offsetY = Math.floor((this.screen.height-this.rows*this.cellHeight)/2);
|
|
148
|
+
this.rowHashes = new Int32Array(this.rows);
|
|
149
|
+
this.rawCoveredRows = new Set();
|
|
150
|
+
this.forcedRows.clear();
|
|
151
|
+
this.lastCursor = {x:-1,y:-1,visible:false};
|
|
152
|
+
this.lastPointer = null;
|
|
153
|
+
this.invalidate();
|
|
154
|
+
}
|
|
155
|
+
|
|
130
156
|
metricsFor(font) {
|
|
131
157
|
let metrics = this.metricsCache.get(font);
|
|
132
158
|
if (!metrics) {
|
|
@@ -214,7 +240,7 @@ export class Renderer {
|
|
|
214
240
|
width * this.cellWidth, height * this.cellHeight);
|
|
215
241
|
}
|
|
216
242
|
|
|
217
|
-
drawText(text, x, y, cells, color, { bold, italic, emoji }) {
|
|
243
|
+
drawText(text, x, y, cells, color, { bold, italic, emoji, align = "center" }) {
|
|
218
244
|
const { CanvasKit, canvas } = this;
|
|
219
245
|
const available = cells * this.cellWidth;
|
|
220
246
|
const paint = this.paintFor(color);
|
|
@@ -235,7 +261,7 @@ export class Renderer {
|
|
|
235
261
|
}
|
|
236
262
|
const metrics = this.metricsFor(font);
|
|
237
263
|
const baseline = y + (this.cellHeight - (metrics.descent - metrics.ascent)) / 2 - metrics.ascent;
|
|
238
|
-
canvas.drawGlyphs([glyph], [x + (available - advance) / 2, baseline], 0, 0, font, paint);
|
|
264
|
+
canvas.drawGlyphs([glyph], [align === "left" ? x : x + (available - advance) / 2, baseline], 0, 0, font, paint);
|
|
239
265
|
return;
|
|
240
266
|
}
|
|
241
267
|
// Grapheme clusters are shaped by the Paragraph API and cached.
|
|
@@ -272,13 +298,13 @@ export class Renderer {
|
|
|
272
298
|
const height = paragraph.getHeight();
|
|
273
299
|
canvas.save();
|
|
274
300
|
canvas.clipRect(CanvasKit.XYWHRect(x, y, available, this.cellHeight), CanvasKit.ClipOp.Intersect, false);
|
|
275
|
-
if (width > available) {
|
|
301
|
+
if (width > available && align !== "left") {
|
|
276
302
|
const scale = available / width;
|
|
277
303
|
canvas.translate(x, y + (this.cellHeight - height * scale) / 2);
|
|
278
304
|
canvas.scale(scale, scale);
|
|
279
305
|
canvas.drawParagraph(paragraph, 0, 0);
|
|
280
306
|
} else {
|
|
281
|
-
canvas.drawParagraph(paragraph, x + (available - width) / 2, y + (this.cellHeight - height) / 2);
|
|
307
|
+
canvas.drawParagraph(paragraph, align === "left" ? x : x + (available - width) / 2, y + (this.cellHeight - height) / 2);
|
|
282
308
|
}
|
|
283
309
|
canvas.restore();
|
|
284
310
|
}
|
|
@@ -395,25 +421,44 @@ export class Renderer {
|
|
|
395
421
|
// screen pixels, from images.js, drawn over the text; `pointer` is the
|
|
396
422
|
// mouse position in pixels, drawn last. Returns true when anything was
|
|
397
423
|
// painted.
|
|
398
|
-
render(term, { cursor, images = [], pointer = null } = {}) {
|
|
424
|
+
render(term, { cursor, images = [], pointer = null, overlay = null } = {}) {
|
|
399
425
|
const buffer = term.buffer.active;
|
|
400
426
|
const top = buffer.viewportY;
|
|
401
427
|
const dirty = [];
|
|
428
|
+
// Only proven opaque, exact raw copies may hide text work. Partial rows,
|
|
429
|
+
// scaling, transparency and unsupported framebuffer layouts remain unchanged.
|
|
430
|
+
const coveredRows = new Set();
|
|
431
|
+
const previouslyCovered = this.rawCoveredRows ?? new Set();
|
|
432
|
+
for (const {image, rect, source} of images) {
|
|
433
|
+
const bounds = source || [0, 0, image.width(), image.height()];
|
|
434
|
+
if (rect[0] > this.offsetX || rect[2] < this.offsetX + this.columns * this.cellWidth
|
|
435
|
+
|| !this.screen.canDrawRawImage?.(image, rect, bounds)) continue;
|
|
436
|
+
const first = Math.max(0, Math.ceil((rect[1] - this.offsetY) / this.cellHeight));
|
|
437
|
+
const end = Math.min(this.rows, Math.floor((rect[3] - this.offsetY) / this.cellHeight));
|
|
438
|
+
for (let row = first; row < end; row++) coveredRows.add(row);
|
|
439
|
+
}
|
|
402
440
|
for (let row = 0; row < this.rows; row++) {
|
|
403
441
|
const line = buffer.getLine(top + row);
|
|
404
|
-
const hash = line ? this.rowHash(line, this.columns) : 0;
|
|
405
|
-
if (this.dirtyAll || hash !== this.rowHashes[row] || this.forcedRows.has(row)
|
|
442
|
+
const hash = coveredRows.has(row) ? this.rowHashes[row] : line ? this.rowHash(line, this.columns) : 0;
|
|
443
|
+
if (this.dirtyAll || hash !== this.rowHashes[row] || this.forcedRows.has(row)
|
|
444
|
+
|| coveredRows.has(row) !== previouslyCovered.has(row)) {
|
|
406
445
|
dirty.push(row);
|
|
407
446
|
this.rowHashes[row] = hash;
|
|
408
447
|
}
|
|
409
448
|
}
|
|
449
|
+
this.rawCoveredRows = coveredRows;
|
|
410
450
|
this.forcedRows.clear();
|
|
451
|
+
const localPointer = Boolean(this.screen.saveRegion && this.screen.restoreRegion);
|
|
452
|
+
const pointerChanged = this.lastPointer?.x !== pointer?.x || this.lastPointer?.y !== pointer?.y;
|
|
453
|
+
const cursorChanged = this.lastCursor.x !== (cursor?.x ?? -1)
|
|
454
|
+
|| this.lastCursor.y !== (cursor?.y ?? -1)
|
|
455
|
+
|| this.lastCursor.visible !== Boolean(cursor?.visible);
|
|
411
456
|
const cursorRows = new Set();
|
|
412
|
-
if (this.lastCursor.visible) cursorRows.add(this.lastCursor.y);
|
|
413
|
-
if (cursor?.visible) cursorRows.add(cursor.y);
|
|
457
|
+
if ((!localPointer || cursorChanged) && this.lastCursor.visible) cursorRows.add(this.lastCursor.y);
|
|
458
|
+
if ((!localPointer || cursorChanged) && cursor?.visible) cursorRows.add(cursor.y);
|
|
414
459
|
// Wherever the pointer was and is has to be repainted: the text under it
|
|
415
460
|
// has not changed, so no row hash would say so.
|
|
416
|
-
for (const position of [this.lastPointer, pointer]) {
|
|
461
|
+
for (const position of localPointer ? [] : [this.lastPointer, pointer]) {
|
|
417
462
|
if (!position) continue;
|
|
418
463
|
const [first, last] = this.pointerRows(position);
|
|
419
464
|
for (let row = Math.max(0, first); row < Math.min(this.rows, last); row++) cursorRows.add(row);
|
|
@@ -421,19 +466,35 @@ export class Renderer {
|
|
|
421
466
|
for (const row of cursorRows) {
|
|
422
467
|
if (row >= 0 && row < this.rows && !dirty.includes(row)) dirty.push(row);
|
|
423
468
|
}
|
|
469
|
+
if (!dirty.length && !this.dirtyAll && (!localPointer || !pointerChanged)) return false;
|
|
470
|
+
if (this.pointerBackground) {
|
|
471
|
+
this.screen.restoreRegion(this.pointerBackground);
|
|
472
|
+
this.pointerBackground = null;
|
|
473
|
+
}
|
|
424
474
|
if (this.dirtyAll) {
|
|
425
475
|
this.canvas.clear(this.CanvasKit.Color(this.background[0], this.background[1], this.background[2]));
|
|
426
476
|
}
|
|
427
477
|
this.dirtyAll = false;
|
|
428
|
-
if (!dirty.length) return false;
|
|
429
478
|
dirty.sort((a, b) => a - b);
|
|
430
|
-
for (const row of dirty)
|
|
431
|
-
|
|
479
|
+
for (const row of dirty) {
|
|
480
|
+
if (!coveredRows.has(row)) this.drawRow(buffer.getLine(top + row), row);
|
|
481
|
+
}
|
|
482
|
+
if (cursor?.visible && !coveredRows.has(cursor.y) && dirty.includes(cursor.y)) {
|
|
432
483
|
this.drawCursor(buffer.getLine(top + cursor.y), cursor.x, cursor.y);
|
|
433
484
|
}
|
|
434
485
|
this.lastCursor = { x: cursor?.x ?? -1, y: cursor?.y ?? -1, visible: Boolean(cursor?.visible) };
|
|
435
|
-
if (images.length) this.drawImages(images, dirty);
|
|
436
|
-
if (
|
|
486
|
+
if (images.length && dirty.length) this.drawImages(images, dirty);
|
|
487
|
+
if (overlay && dirty.length) overlay();
|
|
488
|
+
if (pointer) {
|
|
489
|
+
if (localPointer) {
|
|
490
|
+
const size = Math.max(10, Math.round(this.fontSize * 0.9));
|
|
491
|
+
// Include the mitered outline and anti-aliasing outside the arrow path.
|
|
492
|
+
const padding = Math.ceil(Math.max(1, size / 8) * 2) + 2;
|
|
493
|
+
this.pointerBackground = this.screen.saveRegion(pointer.x - padding, pointer.y - padding,
|
|
494
|
+
pointer.x + size * 0.72 + padding, pointer.y + size * 1.08 + padding);
|
|
495
|
+
}
|
|
496
|
+
this.drawPointer(pointer);
|
|
497
|
+
}
|
|
437
498
|
this.lastPointer = pointer ? { x: pointer.x, y: pointer.y } : null;
|
|
438
499
|
this.screen.flush();
|
|
439
500
|
return true;
|
|
@@ -442,16 +503,24 @@ export class Renderer {
|
|
|
442
503
|
// Redraws the parts of image placements that intersect the repainted rows.
|
|
443
504
|
drawImages(images, dirtyRows) {
|
|
444
505
|
const { CanvasKit, canvas } = this;
|
|
506
|
+
const bands = [];
|
|
507
|
+
for (const row of dirtyRows) {
|
|
508
|
+
const top = this.offsetY + row * this.cellHeight, bottom = top + this.cellHeight;
|
|
509
|
+
const last = bands.at(-1);
|
|
510
|
+
if (last && last[1] === top) last[1] = bottom;
|
|
511
|
+
else bands.push([top, bottom]);
|
|
512
|
+
}
|
|
445
513
|
for (const { image, rect, source } of images) {
|
|
446
514
|
const [left, top, right, bottom] = rect;
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
515
|
+
if (!bands.some(([y0, y1]) => bottom > y0 && top < y1)) continue;
|
|
516
|
+
const bounds = source || [0, 0, image.width(), image.height()];
|
|
517
|
+
if (this.screen.drawOpaqueImage?.(image, rect, bounds, bands)) continue;
|
|
518
|
+
const sourceRect = CanvasKit.LTRBRect(...bounds);
|
|
519
|
+
for (const [rowTop, rowBottom] of bands) {
|
|
451
520
|
if (bottom <= rowTop || top >= rowBottom) continue;
|
|
452
521
|
canvas.save();
|
|
453
522
|
canvas.clipRect(CanvasKit.LTRBRect(0, rowTop, this.screen.width, rowBottom), CanvasKit.ClipOp.Intersect, false);
|
|
454
|
-
canvas.drawImageRectOptions(image, sourceRect, CanvasKit.LTRBRect(left, top, right, bottom),
|
|
523
|
+
canvas.drawImageRectOptions(skiaImage(image), sourceRect, CanvasKit.LTRBRect(left, top, right, bottom),
|
|
455
524
|
CanvasKit.FilterMode.Linear, CanvasKit.MipmapMode.Linear, null);
|
|
456
525
|
canvas.restore();
|
|
457
526
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Coordinates are buffer cells, not UTF-16 indices. Endpoints include a whole
|
|
2
|
+
// grapheme, even when the mouse hits the continuation cell of a wide character.
|
|
3
|
+
export function createSelection(term, renderer, changed) {
|
|
4
|
+
let anchor = null, end = null, dragging = false;
|
|
5
|
+
const clear = () => { if (anchor) { anchor = end = null; dragging = false; changed(); } };
|
|
6
|
+
const point = report => {
|
|
7
|
+
const x = Math.max(0, Math.min(term.cols - 1, Math.floor((report.x - renderer.offsetX) / renderer.cellWidth)));
|
|
8
|
+
const y = term.buffer.active.viewportY + Math.max(0, Math.min(term.rows - 1,
|
|
9
|
+
Math.floor((report.y - renderer.offsetY) / renderer.cellHeight)));
|
|
10
|
+
return {x,y};
|
|
11
|
+
};
|
|
12
|
+
const bounds = () => {
|
|
13
|
+
if (!anchor) return null;
|
|
14
|
+
let [a,b] = anchor.y < end.y || anchor.y === end.y && anchor.x <= end.x ? [anchor,end] : [end,anchor];
|
|
15
|
+
const first = term.buffer.active.getLine(a.y), last = term.buffer.active.getLine(b.y);
|
|
16
|
+
let start = a.x, stop = b.x;
|
|
17
|
+
if (first?.getCell(start)?.getWidth() === 0) start = Math.max(0,start-1);
|
|
18
|
+
if (last?.getCell(stop)?.getWidth() === 0) stop = Math.max(0,stop-1);
|
|
19
|
+
stop += Math.max(1,last?.getCell(stop)?.getWidth() || 1);
|
|
20
|
+
return {a:{x:start,y:a.y},b:{x:stop,y:b.y}};
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
clear,
|
|
24
|
+
stop() { dragging = false; },
|
|
25
|
+
get active() { return !!anchor; },
|
|
26
|
+
pointer(report) {
|
|
27
|
+
if (term._core?.coreMouseService?.areMouseEventsActive) { clear(); return false; }
|
|
28
|
+
if (report.pressed.includes("left")) { anchor = end = point(report); dragging = true; changed(); }
|
|
29
|
+
if (dragging && (report.moved || report.released.includes("left"))) { end = point(report); changed(); }
|
|
30
|
+
if (report.released.includes("left")) dragging = false;
|
|
31
|
+
return true;
|
|
32
|
+
},
|
|
33
|
+
text() {
|
|
34
|
+
const range = bounds();
|
|
35
|
+
if (!range) return "";
|
|
36
|
+
const {a,b} = range;
|
|
37
|
+
let text = "";
|
|
38
|
+
for (let y = a.y; y <= b.y; y++) {
|
|
39
|
+
const line = term.buffer.active.getLine(y);
|
|
40
|
+
if (!line) continue;
|
|
41
|
+
const start = y === a.y ? a.x : 0, stop = y === b.y ? b.x : term.cols;
|
|
42
|
+
if (y > a.y && !line.isWrapped) text += "\n";
|
|
43
|
+
// Wrapped rows join without a newline or lost meaningful spaces.
|
|
44
|
+
const nextWrapped = y < b.y && term.buffer.active.getLine(y + 1)?.isWrapped;
|
|
45
|
+
text += line.translateToString(!nextWrapped, start, stop);
|
|
46
|
+
}
|
|
47
|
+
return text;
|
|
48
|
+
},
|
|
49
|
+
draw() {
|
|
50
|
+
const range = bounds();
|
|
51
|
+
if (!range) return;
|
|
52
|
+
const {a,b} = range, top = term.buffer.active.viewportY;
|
|
53
|
+
for (let y = Math.max(a.y,top); y <= Math.min(b.y,top+term.rows-1); y++) {
|
|
54
|
+
const start = y === a.y ? a.x : 0, stop = y === b.y ? b.x : term.cols;
|
|
55
|
+
renderer.canvas.drawRect(renderer.cellRect(start,y-top,stop-start),
|
|
56
|
+
renderer.paintFor([70,150,220],95));
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|