buninu-linux 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/Apache-2.0.txt +202 -0
- package/LICENSES/BSD-3-Clause-Skia.txt +29 -0
- package/LICENSES/Bitstream-Vera.txt +44 -0
- package/LICENSES/MIT-xterm.js.txt +21 -0
- package/LICENSES/OFL-1.1.txt +91 -0
- package/NOTICE.md +18 -1
- package/README.md +48 -1
- package/graphics.md +213 -0
- package/initramfs/bin/bunterm +66 -0
- package/initramfs/buninu/CHANGELOG.md +40 -0
- package/initramfs/buninu/apps/jsgotty/gotty.js +4 -3
- package/initramfs/buninu/apps/jsgotty/package.json +1 -1
- package/initramfs/buninu/package.json +1 -1
- package/initramfs/init.js +6 -0
- package/initramfs/lib/bunterm/glyphs.js +446 -0
- package/initramfs/lib/bunterm/images.js +225 -0
- package/initramfs/lib/bunterm/input.js +140 -0
- package/initramfs/lib/bunterm/renderer.js +424 -0
- package/initramfs/lib/bunterm/terminal.js +203 -0
- package/initramfs/lib/canvas.js +181 -0
- package/initramfs/lib/canvaskit/LICENSE +29 -0
- package/initramfs/lib/canvaskit/canvaskit.js +264 -0
- package/initramfs/lib/canvaskit/canvaskit.wasm +0 -0
- package/initramfs/lib/dlopen.js +14 -2
- package/initramfs/lib/fbdev.js +581 -0
- package/initramfs/lib/xterm/LICENSE +21 -0
- package/initramfs/lib/xterm/addon-unicode-graphemes.mjs +44 -0
- package/initramfs/lib/xterm/xterm-headless.mjs +46 -0
- package/initramfs/usr/share/doc/buninu-linux/bunterm.md +83 -0
- package/initramfs/usr/share/doc/buninu-linux/canvas.md +91 -0
- package/initramfs/usr/share/doc/buninu-linux/fbdev.md +113 -0
- package/initramfs/usr/share/fonts/DejaVuSansMono-Bold.ttf +0 -0
- package/initramfs/usr/share/fonts/DejaVuSansMono.ttf +0 -0
- package/initramfs/usr/share/fonts/LICENSE-Apache-2.0.txt +202 -0
- package/initramfs/usr/share/fonts/LICENSE-DejaVu.txt +44 -0
- package/initramfs/usr/share/fonts/LICENSE-OFL-1.1.txt +91 -0
- package/initramfs/usr/share/fonts/NotoColorEmoji.ttf +0 -0
- package/initramfs/usr/share/fonts/NotoColorEmojiFlags.ttf +0 -0
- package/initramfs/usr/share/fonts/NotoSansCJK-Regular.ttc +0 -0
- package/initramfs/usr/share/fonts/NotoSansSymbols-Regular-Subsetted.ttf +0 -0
- package/initramfs/usr/share/fonts/NotoSansSymbols-Regular-Subsetted2.ttf +0 -0
- package/initramfs/usr/share/fonts/Roboto-Regular.ttf +0 -0
- package/initramfs/usr/share/fonts/fonts.json +28 -0
- package/package.json +2 -2
- package/run-qemu.sh +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// /lib/bunterm/input.js — keyboard bytes from a Linux virtual console.
|
|
2
|
+
//
|
|
3
|
+
// The console is read in raw mode so every key reaches the terminal
|
|
4
|
+
// emulator unchanged (Ctrl-C included; the program in the PTY decides what
|
|
5
|
+
// it means). The kernel keeps translating keys to bytes (K_XLATE), and its
|
|
6
|
+
// "linux" console sequences for a few keys are rewritten into the xterm
|
|
7
|
+
// forms that programs seeing TERM=xterm-256color expect.
|
|
8
|
+
|
|
9
|
+
import { closeSync, openSync, read } from "node:fs";
|
|
10
|
+
import { libc, ptr, errno, strerror } from "../dlopen.js";
|
|
11
|
+
|
|
12
|
+
const TCGETS = 0x5401;
|
|
13
|
+
const TCSETS = 0x5402;
|
|
14
|
+
// struct termios (kernel ABI): 4 x u32 flags, c_line, c_cc[19].
|
|
15
|
+
const TERMIOS_LENGTH = 36;
|
|
16
|
+
const IFLAG = 0;
|
|
17
|
+
const OFLAG = 4;
|
|
18
|
+
const CFLAG = 8;
|
|
19
|
+
const LFLAG = 12;
|
|
20
|
+
const CC = 17;
|
|
21
|
+
const VTIME = 5;
|
|
22
|
+
const VMIN = 6;
|
|
23
|
+
|
|
24
|
+
const IGNBRK = 0o1, BRKINT = 0o2, PARMRK = 0o10, ISTRIP = 0o40, INLCR = 0o100,
|
|
25
|
+
IGNCR = 0o200, ICRNL = 0o400, IXON = 0o2000;
|
|
26
|
+
const OPOST = 0o1;
|
|
27
|
+
const CSIZE = 0o60, CS8 = 0o60, PARENB = 0o400;
|
|
28
|
+
const ISIG = 0o1, ICANON = 0o2, ECHO = 0o10, ECHONL = 0o100, IEXTEN = 0o100000;
|
|
29
|
+
|
|
30
|
+
const fail = (operation) => {
|
|
31
|
+
const code = errno();
|
|
32
|
+
throw new Error(`${operation}: ${strerror(code)}`);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getTermios = (fd) => {
|
|
36
|
+
const buffer = new Uint8Array(TERMIOS_LENGTH);
|
|
37
|
+
if (libc.symbols.ioctl(fd, TCGETS, ptr(buffer)) === -1) fail("TCGETS");
|
|
38
|
+
return buffer;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const setTermios = (fd, buffer) => {
|
|
42
|
+
if (libc.symbols.ioctl(fd, TCSETS, ptr(buffer)) === -1) fail("TCSETS");
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// cfmakeraw() on a copy of the current settings.
|
|
46
|
+
const rawTermios = (current) => {
|
|
47
|
+
const raw = new Uint8Array(current);
|
|
48
|
+
const view = new DataView(raw.buffer);
|
|
49
|
+
const clear = (offset, mask) => view.setUint32(offset, view.getUint32(offset, true) & ~mask, true);
|
|
50
|
+
clear(IFLAG, IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
|
|
51
|
+
clear(OFLAG, OPOST);
|
|
52
|
+
clear(LFLAG, ECHO | ECHONL | ICANON | ISIG | IEXTEN);
|
|
53
|
+
clear(CFLAG, CSIZE | PARENB);
|
|
54
|
+
view.setUint32(CFLAG, view.getUint32(CFLAG, true) | CS8, true);
|
|
55
|
+
raw[CC + VMIN] = 1;
|
|
56
|
+
raw[CC + VTIME] = 0;
|
|
57
|
+
return raw;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Linux console sequences → xterm sequences (keys programs commonly bind).
|
|
61
|
+
const consoleToXterm = new Map([
|
|
62
|
+
["\u001b[[A", "\u001bOP"], ["\u001b[[B", "\u001bOQ"], ["\u001b[[C", "\u001bOR"],
|
|
63
|
+
["\u001b[[D", "\u001bOS"], ["\u001b[[E", "\u001b[15~"],
|
|
64
|
+
["\u001b[1~", "\u001b[H"], ["\u001b[4~", "\u001b[F"],
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const cursorKeys = new Map([
|
|
68
|
+
["\u001b[A", "\u001bOA"], ["\u001b[B", "\u001bOB"], ["\u001b[C", "\u001bOC"], ["\u001b[D", "\u001bOD"],
|
|
69
|
+
["\u001b[H", "\u001bOH"], ["\u001b[F", "\u001bOF"],
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// Rewrites console key sequences in `text`. With `applicationCursorKeys`
|
|
73
|
+
// (DECCKM set by the program) the arrow keys use the SS3 forms.
|
|
74
|
+
export const translateKeys = (text, { applicationCursorKeys = false } = {}) => {
|
|
75
|
+
let output = "";
|
|
76
|
+
let index = 0;
|
|
77
|
+
while (index < text.length) {
|
|
78
|
+
if (text[index] !== "\u001b") {
|
|
79
|
+
output += text[index++];
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
let matched = false;
|
|
83
|
+
for (const [from, to] of consoleToXterm) {
|
|
84
|
+
if (text.startsWith(from, index)) {
|
|
85
|
+
output += applicationCursorKeys ? (cursorKeys.get(to) ?? to) : to;
|
|
86
|
+
index += from.length;
|
|
87
|
+
matched = true;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (matched) continue;
|
|
92
|
+
if (applicationCursorKeys) {
|
|
93
|
+
for (const [from, to] of cursorKeys) {
|
|
94
|
+
if (text.startsWith(from, index)) {
|
|
95
|
+
output += to;
|
|
96
|
+
index += from.length;
|
|
97
|
+
matched = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (matched) continue;
|
|
102
|
+
}
|
|
103
|
+
output += text[index++];
|
|
104
|
+
}
|
|
105
|
+
return output;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Opens the keyboard: `device` names a console such as /dev/tty1; without
|
|
109
|
+
// it the process's own stdin is used (it must be a terminal). `onData`
|
|
110
|
+
// 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));
|
|
115
|
+
let open = true;
|
|
116
|
+
const chunk = Buffer.alloc(4096);
|
|
117
|
+
const pump = () => {
|
|
118
|
+
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;
|
|
125
|
+
}
|
|
126
|
+
if (count > 0) onData(Buffer.from(chunk.subarray(0, count)));
|
|
127
|
+
pump();
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
pump();
|
|
131
|
+
return {
|
|
132
|
+
fd,
|
|
133
|
+
close() {
|
|
134
|
+
if (!open) return;
|
|
135
|
+
open = false;
|
|
136
|
+
try { setTermios(fd, saved); } catch {}
|
|
137
|
+
if (device) closeSync(fd);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
};
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// /lib/bunterm/renderer.js — paints an xterm.js headless buffer onto a Skia
|
|
2
|
+
// canvas as a grid of cells.
|
|
3
|
+
//
|
|
4
|
+
// Every cell is placed by the renderer itself, never by a text layout: the
|
|
5
|
+
// primary monospace font sets the cell size, each character is looked up in
|
|
6
|
+
// the primary font and then the fallback fonts, and the glyph is centred in
|
|
7
|
+
// the one or two cells the terminal assigned it. Box drawing and block
|
|
8
|
+
// characters come from glyphs.js so they join seamlessly. Only rows whose
|
|
9
|
+
// content changed since the last frame are repainted.
|
|
10
|
+
|
|
11
|
+
import { drawCustomGlyph, hasCustomGlyph } from "./glyphs.js";
|
|
12
|
+
|
|
13
|
+
// xterm.js's default theme.
|
|
14
|
+
export const defaultTheme = {
|
|
15
|
+
foreground: "#ffffff",
|
|
16
|
+
background: "#000000",
|
|
17
|
+
cursor: "#ffffff",
|
|
18
|
+
ansi: [
|
|
19
|
+
"#2e3436", "#cc0000", "#4e9a06", "#c4a000", "#3465a4", "#75507b", "#06989a", "#d3d7cf",
|
|
20
|
+
"#555753", "#ef2929", "#8ae234", "#fce94f", "#729fcf", "#ad7fa8", "#34e2e2", "#eeeeec",
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const parseColor = (hex) => [
|
|
25
|
+
parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// The 256-colour palette: 16 theme colours, a 6x6x6 cube and 24 greys.
|
|
29
|
+
export const buildPalette = (theme) => {
|
|
30
|
+
const palette = theme.ansi.map(parseColor);
|
|
31
|
+
const steps = [0, 95, 135, 175, 215, 255];
|
|
32
|
+
for (let r = 0; r < 6; r++) {
|
|
33
|
+
for (let g = 0; g < 6; g++) {
|
|
34
|
+
for (let b = 0; b < 6; b++) palette.push([steps[r], steps[g], steps[b]]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (let i = 0; i < 24; i++) {
|
|
38
|
+
const level = 8 + i * 10;
|
|
39
|
+
palette.push([level, level, level]);
|
|
40
|
+
}
|
|
41
|
+
return palette;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const packColor = ([r, g, b]) => (r << 16) | (g << 8) | b;
|
|
45
|
+
const unpackColor = (packed) => [(packed >> 16) & 255, (packed >> 8) & 255, packed & 255];
|
|
46
|
+
|
|
47
|
+
// Code points that default to emoji presentation get the colour emoji
|
|
48
|
+
// fonts before the monochrome symbol fonts; everything else the reverse.
|
|
49
|
+
const emojiPresentation = /^\p{Emoji_Presentation}/u;
|
|
50
|
+
const isEmojiText = (text) => emojiPresentation.test(text) || text.includes("\uFE0F");
|
|
51
|
+
|
|
52
|
+
// A cell may hold a multi-code-point grapheme (a flag, a ZWJ sequence, a
|
|
53
|
+
// base with combining marks). Those need shaping, so they go through the
|
|
54
|
+
// Paragraph API; single code points are drawn as one glyph.
|
|
55
|
+
const isSingleCodePoint = (text) => {
|
|
56
|
+
const first = text.codePointAt(0);
|
|
57
|
+
return text.length === (first > 0xffff ? 2 : 1);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export class Renderer {
|
|
61
|
+
constructor({ CanvasKit, screen, fonts, fontSize = 16, lineHeight = 1, theme = defaultTheme }) {
|
|
62
|
+
this.CanvasKit = CanvasKit;
|
|
63
|
+
this.screen = screen;
|
|
64
|
+
this.canvas = screen.canvas;
|
|
65
|
+
this.fonts = fonts;
|
|
66
|
+
this.fontSize = fontSize;
|
|
67
|
+
this.theme = theme;
|
|
68
|
+
this.palette = buildPalette(theme);
|
|
69
|
+
this.foreground = parseColor(theme.foreground);
|
|
70
|
+
this.background = parseColor(theme.background);
|
|
71
|
+
this.cursorColor = parseColor(theme.cursor);
|
|
72
|
+
|
|
73
|
+
this.typefaceIds = new Map();
|
|
74
|
+
this.fontCache = new Map();
|
|
75
|
+
this.metricsCache = new Map();
|
|
76
|
+
this.glyphCache = new Map();
|
|
77
|
+
this.paragraphCache = new Map();
|
|
78
|
+
this.paintCache = new Map();
|
|
79
|
+
|
|
80
|
+
const regular = this.fontFor(fonts.regular, fontSize);
|
|
81
|
+
const metrics = regular.getMetrics();
|
|
82
|
+
const advance = regular.getGlyphWidths(fonts.regular.getGlyphIDs("M"))[0];
|
|
83
|
+
this.cellWidth = Math.max(1, Math.round(advance));
|
|
84
|
+
this.cellHeight = Math.max(1, Math.round((metrics.descent - metrics.ascent + (metrics.leading || 0)) * lineHeight));
|
|
85
|
+
this.columns = Math.max(1, Math.floor(screen.width / this.cellWidth));
|
|
86
|
+
this.rows = Math.max(1, Math.floor(screen.height / this.cellHeight));
|
|
87
|
+
// The grid is centred; the margins keep the background colour.
|
|
88
|
+
this.offsetX = Math.floor((screen.width - this.columns * this.cellWidth) / 2);
|
|
89
|
+
this.offsetY = Math.floor((screen.height - this.rows * this.cellHeight) / 2);
|
|
90
|
+
|
|
91
|
+
// Fallback typefaces split by kind so lookup order can depend on the text.
|
|
92
|
+
this.monoFallback = [];
|
|
93
|
+
this.colorFallback = [];
|
|
94
|
+
fonts.names.slice(fonts.bold ? 2 : 1).forEach((name, index) => {
|
|
95
|
+
const typeface = fonts.fallback[index];
|
|
96
|
+
(name.includes("Emoji") ? this.colorFallback : this.monoFallback).push(typeface);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.emojiFamilies = [
|
|
100
|
+
...fonts.families.filter((name) => name.includes("Emoji")),
|
|
101
|
+
...fonts.families.filter((name) => !name.includes("Emoji")),
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
this.rowHashes = new Int32Array(this.rows).fill(0);
|
|
105
|
+
this.forcedRows = new Set();
|
|
106
|
+
this.dirtyAll = true;
|
|
107
|
+
this.lastCursor = { x: -1, y: -1, visible: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fontFor(typeface, size, { bold = false, italic = false } = {}) {
|
|
111
|
+
let id = this.typefaceIds.get(typeface);
|
|
112
|
+
if (id === undefined) {
|
|
113
|
+
id = this.typefaceIds.size;
|
|
114
|
+
this.typefaceIds.set(typeface, id);
|
|
115
|
+
}
|
|
116
|
+
const key = `${id}:${size}:${bold ? "b" : ""}${italic ? "i" : ""}`;
|
|
117
|
+
let font = this.fontCache.get(key);
|
|
118
|
+
if (!font) {
|
|
119
|
+
font = new this.CanvasKit.Font(typeface, size);
|
|
120
|
+
font.setSubpixel(true);
|
|
121
|
+
font.setEdging(this.CanvasKit.FontEdging.AntiAlias);
|
|
122
|
+
if (bold) font.setEmbolden(true);
|
|
123
|
+
if (italic) font.setSkewX(-0.2);
|
|
124
|
+
this.fontCache.set(key, font);
|
|
125
|
+
}
|
|
126
|
+
return font;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
metricsFor(font) {
|
|
130
|
+
let metrics = this.metricsCache.get(font);
|
|
131
|
+
if (!metrics) {
|
|
132
|
+
metrics = font.getMetrics();
|
|
133
|
+
this.metricsCache.set(font, metrics);
|
|
134
|
+
}
|
|
135
|
+
return metrics;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
paintFor(color, alpha = 255) {
|
|
139
|
+
const key = packColor(color) * 256 + alpha;
|
|
140
|
+
let paint = this.paintCache.get(key);
|
|
141
|
+
if (!paint) {
|
|
142
|
+
paint = new this.CanvasKit.Paint();
|
|
143
|
+
paint.setColor(this.CanvasKit.Color(color[0], color[1], color[2], alpha / 255));
|
|
144
|
+
paint.setAntiAlias(true);
|
|
145
|
+
this.paintCache.set(key, paint);
|
|
146
|
+
}
|
|
147
|
+
return paint;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Finds the typeface holding `text` (a single code point) and its glyph.
|
|
151
|
+
resolveGlyph(text, emoji) {
|
|
152
|
+
const key = (emoji ? "e" : "m") + text;
|
|
153
|
+
let entry = this.glyphCache.get(key);
|
|
154
|
+
if (entry === undefined) {
|
|
155
|
+
entry = null;
|
|
156
|
+
const order = emoji
|
|
157
|
+
? [this.fonts.regular, ...this.colorFallback, ...this.monoFallback]
|
|
158
|
+
: [this.fonts.regular, ...this.monoFallback, ...this.colorFallback];
|
|
159
|
+
for (const typeface of order) {
|
|
160
|
+
const id = typeface.getGlyphIDs(text, 1)[0];
|
|
161
|
+
if (id) {
|
|
162
|
+
entry = { typeface, glyph: id };
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
this.glyphCache.set(key, entry);
|
|
167
|
+
}
|
|
168
|
+
return entry;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
colorOf(cell, foreground) {
|
|
172
|
+
const rgb = foreground ? cell.isFgRGB() : cell.isBgRGB();
|
|
173
|
+
const palette = foreground ? cell.isFgPalette() : cell.isBgPalette();
|
|
174
|
+
const value = foreground ? cell.getFgColor() : cell.getBgColor();
|
|
175
|
+
if (rgb) return unpackColor(value);
|
|
176
|
+
if (palette) {
|
|
177
|
+
// Bold text in the eight base colours uses the bright variants.
|
|
178
|
+
const index = foreground && cell.isBold() && value < 8 ? value + 8 : value;
|
|
179
|
+
return this.palette[index] ?? this.foreground;
|
|
180
|
+
}
|
|
181
|
+
return foreground ? this.foreground : this.background;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
rowHash(line, columns) {
|
|
185
|
+
let hash = 0x811c9dc5 | 0;
|
|
186
|
+
const mix = (value) => { hash = Math.imul(hash ^ value, 0x01000193); };
|
|
187
|
+
for (let x = 0; x < columns; x++) {
|
|
188
|
+
const cell = line.getCell(x);
|
|
189
|
+
if (!cell) break;
|
|
190
|
+
mix(cell.getCode());
|
|
191
|
+
mix(cell.getChars().length);
|
|
192
|
+
mix(cell.getWidth());
|
|
193
|
+
mix(cell.getFgColor() ^ (cell.getFgColorMode() >>> 16));
|
|
194
|
+
mix(cell.getBgColor() ^ (cell.getBgColorMode() >>> 16));
|
|
195
|
+
mix(cell.isBold() | cell.isItalic() | cell.isDim() | cell.isUnderline()
|
|
196
|
+
| cell.isInverse() | cell.isInvisible() | cell.isStrikethrough() | cell.isBlink());
|
|
197
|
+
}
|
|
198
|
+
return hash;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
invalidate() {
|
|
202
|
+
this.dirtyAll = true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Marks rows [start, end) for repainting even if their text is unchanged,
|
|
206
|
+
// for image placements appearing or disappearing over them.
|
|
207
|
+
invalidateRows(start, end) {
|
|
208
|
+
for (let row = Math.max(0, start); row < Math.min(this.rows, end); row++) this.forcedRows.add(row);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
cellRect(x, y, width = 1, height = 1) {
|
|
212
|
+
return this.CanvasKit.XYWHRect(this.offsetX + x * this.cellWidth, this.offsetY + y * this.cellHeight,
|
|
213
|
+
width * this.cellWidth, height * this.cellHeight);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
drawText(text, x, y, cells, color, { bold, italic, emoji }) {
|
|
217
|
+
const { CanvasKit, canvas } = this;
|
|
218
|
+
const available = cells * this.cellWidth;
|
|
219
|
+
const paint = this.paintFor(color);
|
|
220
|
+
if (isSingleCodePoint(text)) {
|
|
221
|
+
const entry = this.resolveGlyph(text, emoji);
|
|
222
|
+
if (!entry) return;
|
|
223
|
+
const primary = entry.typeface === this.fonts.regular;
|
|
224
|
+
const typeface = primary && bold && this.fonts.bold ? this.fonts.bold : entry.typeface;
|
|
225
|
+
const glyph = primary && bold && this.fonts.bold ? this.fonts.bold.getGlyphIDs(text, 1)[0] : entry.glyph;
|
|
226
|
+
let font = this.fontFor(typeface, this.fontSize, { bold: bold && !primary, italic });
|
|
227
|
+
let advance = font.getGlyphWidths([glyph])[0];
|
|
228
|
+
// A fallback glyph wider than its cells (emoji, some symbols) is
|
|
229
|
+
// scaled down to fit rather than overlapping its neighbour.
|
|
230
|
+
if (advance > available + 0.5) {
|
|
231
|
+
const size = Math.floor(this.fontSize * available / advance);
|
|
232
|
+
font = this.fontFor(typeface, size, { bold: bold && !primary, italic });
|
|
233
|
+
advance = font.getGlyphWidths([glyph])[0];
|
|
234
|
+
}
|
|
235
|
+
const metrics = this.metricsFor(font);
|
|
236
|
+
const baseline = y + (this.cellHeight - (metrics.descent - metrics.ascent)) / 2 - metrics.ascent;
|
|
237
|
+
canvas.drawGlyphs([glyph], [x + (available - advance) / 2, baseline], 0, 0, font, paint);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// Grapheme clusters are shaped by the Paragraph API and cached.
|
|
241
|
+
const key = `${text}|${packColor(color)}|${bold ? 1 : 0}${italic ? 1 : 0}`;
|
|
242
|
+
let paragraph = this.paragraphCache.get(key);
|
|
243
|
+
if (!paragraph) {
|
|
244
|
+
if (this.paragraphCache.size > 512) {
|
|
245
|
+
for (const [oldKey, old] of this.paragraphCache) {
|
|
246
|
+
old.delete();
|
|
247
|
+
this.paragraphCache.delete(oldKey);
|
|
248
|
+
if (this.paragraphCache.size <= 256) break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const style = new CanvasKit.ParagraphStyle({
|
|
252
|
+
textStyle: {
|
|
253
|
+
color: CanvasKit.Color(color[0], color[1], color[2]),
|
|
254
|
+
fontFamilies: emoji ? this.emojiFamilies : this.fonts.families,
|
|
255
|
+
fontSize: this.fontSize,
|
|
256
|
+
fontStyle: {
|
|
257
|
+
weight: bold ? CanvasKit.FontWeight.Bold : CanvasKit.FontWeight.Normal,
|
|
258
|
+
slant: italic ? CanvasKit.FontSlant.Italic : CanvasKit.FontSlant.Upright,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
maxLines: 1,
|
|
262
|
+
});
|
|
263
|
+
const builder = CanvasKit.ParagraphBuilder.Make(style, this.fonts.fontMgr);
|
|
264
|
+
builder.addText(text);
|
|
265
|
+
paragraph = builder.build();
|
|
266
|
+
builder.delete();
|
|
267
|
+
paragraph.layout(available * 2 + this.fontSize * 2);
|
|
268
|
+
this.paragraphCache.set(key, paragraph);
|
|
269
|
+
}
|
|
270
|
+
const width = paragraph.getLongestLine();
|
|
271
|
+
const height = paragraph.getHeight();
|
|
272
|
+
canvas.save();
|
|
273
|
+
canvas.clipRect(CanvasKit.XYWHRect(x, y, available, this.cellHeight), CanvasKit.ClipOp.Intersect, false);
|
|
274
|
+
if (width > available) {
|
|
275
|
+
const scale = available / width;
|
|
276
|
+
canvas.translate(x, y + (this.cellHeight - height * scale) / 2);
|
|
277
|
+
canvas.scale(scale, scale);
|
|
278
|
+
canvas.drawParagraph(paragraph, 0, 0);
|
|
279
|
+
} else {
|
|
280
|
+
canvas.drawParagraph(paragraph, x + (available - width) / 2, y + (this.cellHeight - height) / 2);
|
|
281
|
+
}
|
|
282
|
+
canvas.restore();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
drawCell(cell, column, row) {
|
|
286
|
+
const width = cell.getWidth();
|
|
287
|
+
if (width === 0) return;
|
|
288
|
+
const { CanvasKit, canvas } = this;
|
|
289
|
+
let foreground = this.colorOf(cell, true);
|
|
290
|
+
let background = this.colorOf(cell, false);
|
|
291
|
+
if (cell.isInverse()) [foreground, background] = [background, foreground];
|
|
292
|
+
const x = this.offsetX + column * this.cellWidth;
|
|
293
|
+
const y = this.offsetY + row * this.cellHeight;
|
|
294
|
+
if (packColor(background) !== packColor(this.background)) {
|
|
295
|
+
canvas.drawRect(CanvasKit.XYWHRect(x, y, width * this.cellWidth, this.cellHeight), this.paintFor(background));
|
|
296
|
+
}
|
|
297
|
+
if (cell.isInvisible()) return;
|
|
298
|
+
if (cell.isDim()) {
|
|
299
|
+
foreground = foreground.map((channel, index) => Math.round((channel + background[index]) / 2));
|
|
300
|
+
}
|
|
301
|
+
const text = cell.getChars();
|
|
302
|
+
const bold = Boolean(cell.isBold());
|
|
303
|
+
const italic = Boolean(cell.isItalic());
|
|
304
|
+
if (text && text !== " ") {
|
|
305
|
+
if (hasCustomGlyph(text)) {
|
|
306
|
+
drawCustomGlyph(CanvasKit, canvas, text, x, y, width * this.cellWidth, this.cellHeight,
|
|
307
|
+
this.fontSize, this.paintFor(foreground), foreground);
|
|
308
|
+
} else {
|
|
309
|
+
this.drawText(text, x, y, width, foreground, { bold, italic, emoji: isEmojiText(text) });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (cell.isUnderline() || cell.isStrikethrough()) {
|
|
313
|
+
const paint = this.paintFor(foreground);
|
|
314
|
+
const thickness = Math.max(1, Math.floor(this.fontSize / 14));
|
|
315
|
+
if (cell.isUnderline()) {
|
|
316
|
+
canvas.drawRect(CanvasKit.XYWHRect(x, y + this.cellHeight - thickness - 1, width * this.cellWidth, thickness), paint);
|
|
317
|
+
}
|
|
318
|
+
if (cell.isStrikethrough()) {
|
|
319
|
+
canvas.drawRect(CanvasKit.XYWHRect(x, y + Math.round(this.cellHeight * 0.55), width * this.cellWidth, thickness), paint);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
drawRow(line, row) {
|
|
325
|
+
const { canvas } = this;
|
|
326
|
+
canvas.drawRect(this.cellRect(0, row, this.columns, 1), this.paintFor(this.background));
|
|
327
|
+
if (!line) return;
|
|
328
|
+
for (let column = 0; column < this.columns; column++) {
|
|
329
|
+
const cell = line.getCell(column);
|
|
330
|
+
if (!cell) break;
|
|
331
|
+
this.drawCell(cell, column, row);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
drawCursor(line, x, y) {
|
|
336
|
+
const { CanvasKit, canvas } = this;
|
|
337
|
+
const cell = line?.getCell(x);
|
|
338
|
+
const width = cell?.getWidth() || 1;
|
|
339
|
+
const rect = this.cellRect(x, y, width, 1);
|
|
340
|
+
canvas.drawRect(rect, this.paintFor(this.cursorColor));
|
|
341
|
+
if (cell) {
|
|
342
|
+
// Repaint the character in the background colour over the block.
|
|
343
|
+
const text = cell.getChars();
|
|
344
|
+
const left = this.offsetX + x * this.cellWidth;
|
|
345
|
+
const top = this.offsetY + y * this.cellHeight;
|
|
346
|
+
if (text && text !== " ") {
|
|
347
|
+
if (hasCustomGlyph(text)) {
|
|
348
|
+
drawCustomGlyph(CanvasKit, canvas, text, left, top, width * this.cellWidth, this.cellHeight,
|
|
349
|
+
this.fontSize, this.paintFor(this.background), this.background);
|
|
350
|
+
} else {
|
|
351
|
+
this.drawText(text, left, top, width, this.background,
|
|
352
|
+
{ bold: Boolean(cell.isBold()), italic: Boolean(cell.isItalic()), emoji: isEmojiText(text) });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Repaints what changed and flushes to the framebuffer. `cursor` is
|
|
359
|
+
// { x, y, visible }; `images` is a list of { image, rect } placements in
|
|
360
|
+
// screen pixels, from images.js, drawn over the text. Returns true when
|
|
361
|
+
// anything was painted.
|
|
362
|
+
render(term, { cursor, images = [] } = {}) {
|
|
363
|
+
const buffer = term.buffer.active;
|
|
364
|
+
const top = buffer.viewportY;
|
|
365
|
+
const dirty = [];
|
|
366
|
+
for (let row = 0; row < this.rows; row++) {
|
|
367
|
+
const line = buffer.getLine(top + row);
|
|
368
|
+
const hash = line ? this.rowHash(line, this.columns) : 0;
|
|
369
|
+
if (this.dirtyAll || hash !== this.rowHashes[row] || this.forcedRows.has(row)) {
|
|
370
|
+
dirty.push(row);
|
|
371
|
+
this.rowHashes[row] = hash;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
this.forcedRows.clear();
|
|
375
|
+
const cursorRows = new Set();
|
|
376
|
+
if (this.lastCursor.visible) cursorRows.add(this.lastCursor.y);
|
|
377
|
+
if (cursor?.visible) cursorRows.add(cursor.y);
|
|
378
|
+
for (const row of cursorRows) {
|
|
379
|
+
if (row >= 0 && row < this.rows && !dirty.includes(row)) dirty.push(row);
|
|
380
|
+
}
|
|
381
|
+
if (this.dirtyAll) {
|
|
382
|
+
this.canvas.clear(this.CanvasKit.Color(this.background[0], this.background[1], this.background[2]));
|
|
383
|
+
}
|
|
384
|
+
this.dirtyAll = false;
|
|
385
|
+
if (!dirty.length) return false;
|
|
386
|
+
dirty.sort((a, b) => a - b);
|
|
387
|
+
for (const row of dirty) this.drawRow(buffer.getLine(top + row), row);
|
|
388
|
+
if (cursor?.visible && cursor.y >= 0 && cursor.y < this.rows) {
|
|
389
|
+
this.drawCursor(buffer.getLine(top + cursor.y), cursor.x, cursor.y);
|
|
390
|
+
}
|
|
391
|
+
this.lastCursor = { x: cursor?.x ?? -1, y: cursor?.y ?? -1, visible: Boolean(cursor?.visible) };
|
|
392
|
+
if (images.length) this.drawImages(images, dirty);
|
|
393
|
+
this.screen.flush();
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Redraws the parts of image placements that intersect the repainted rows.
|
|
398
|
+
drawImages(images, dirtyRows) {
|
|
399
|
+
const { CanvasKit, canvas } = this;
|
|
400
|
+
for (const { image, rect, source } of images) {
|
|
401
|
+
const [left, top, right, bottom] = rect;
|
|
402
|
+
const sourceRect = source ? CanvasKit.LTRBRect(...source) : CanvasKit.LTRBRect(0, 0, image.width(), image.height());
|
|
403
|
+
for (const row of dirtyRows) {
|
|
404
|
+
const rowTop = this.offsetY + row * this.cellHeight;
|
|
405
|
+
const rowBottom = rowTop + this.cellHeight;
|
|
406
|
+
if (bottom <= rowTop || top >= rowBottom) continue;
|
|
407
|
+
canvas.save();
|
|
408
|
+
canvas.clipRect(CanvasKit.LTRBRect(0, rowTop, this.screen.width, rowBottom), CanvasKit.ClipOp.Intersect, false);
|
|
409
|
+
canvas.drawImageRectOptions(image, sourceRect, CanvasKit.LTRBRect(left, top, right, bottom),
|
|
410
|
+
CanvasKit.FilterMode.Linear, CanvasKit.MipmapMode.Linear, null);
|
|
411
|
+
canvas.restore();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
delete() {
|
|
417
|
+
for (const font of this.fontCache.values()) font.delete();
|
|
418
|
+
for (const paint of this.paintCache.values()) paint.delete();
|
|
419
|
+
for (const paragraph of this.paragraphCache.values()) paragraph.delete();
|
|
420
|
+
this.fontCache.clear();
|
|
421
|
+
this.paintCache.clear();
|
|
422
|
+
this.paragraphCache.clear();
|
|
423
|
+
}
|
|
424
|
+
}
|