buninu-linux 0.3.1 → 0.5.1

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 (53) hide show
  1. package/LICENSES/Apache-2.0.txt +202 -0
  2. package/LICENSES/BSD-3-Clause-Skia.txt +29 -0
  3. package/LICENSES/Bitstream-Vera.txt +44 -0
  4. package/LICENSES/MIT-xterm.js.txt +21 -0
  5. package/LICENSES/OFL-1.1.txt +91 -0
  6. package/NOTICE.md +18 -1
  7. package/README.md +101 -9
  8. package/fetch-alpine.sh +6 -0
  9. package/graphics.md +218 -0
  10. package/initramfs/bin/bunterm +70 -0
  11. package/initramfs/bin/chroot +213 -0
  12. package/initramfs/bin/stripansi +27 -0
  13. package/initramfs/bin/tar +189 -14
  14. package/initramfs/buninu/CHANGELOG.md +40 -0
  15. package/initramfs/buninu/apps/jsgotty/gotty.js +4 -3
  16. package/initramfs/buninu/apps/jsgotty/package.json +1 -1
  17. package/initramfs/buninu/package.json +1 -1
  18. package/initramfs/init.js +80 -1
  19. package/initramfs/lib/bunterm/glyphs.js +446 -0
  20. package/initramfs/lib/bunterm/images.js +225 -0
  21. package/initramfs/lib/bunterm/input.js +140 -0
  22. package/initramfs/lib/bunterm/mouse.js +222 -0
  23. package/initramfs/lib/bunterm/renderer.js +469 -0
  24. package/initramfs/lib/bunterm/terminal.js +324 -0
  25. package/initramfs/lib/canvas.js +181 -0
  26. package/initramfs/lib/canvaskit/LICENSE +29 -0
  27. package/initramfs/lib/canvaskit/canvaskit.js +264 -0
  28. package/initramfs/lib/canvaskit/canvaskit.wasm +0 -0
  29. package/initramfs/lib/dlopen.js +18 -2
  30. package/initramfs/lib/document.js +5 -1
  31. package/initramfs/lib/fbdev.js +581 -0
  32. package/initramfs/lib/xterm/LICENSE +21 -0
  33. package/initramfs/lib/xterm/addon-unicode-graphemes.mjs +44 -0
  34. package/initramfs/lib/xterm/xterm-headless.mjs +46 -0
  35. package/initramfs/usr/share/doc/buninu-linux/bunterm.md +112 -0
  36. package/initramfs/usr/share/doc/buninu-linux/canvas.md +91 -0
  37. package/initramfs/usr/share/doc/buninu-linux/chroot.md +105 -0
  38. package/initramfs/usr/share/doc/buninu-linux/fbdev.md +113 -0
  39. package/initramfs/usr/share/doc/buninu-linux/tar.md +30 -6
  40. package/initramfs/usr/share/fonts/DejaVuSansMono-Bold.ttf +0 -0
  41. package/initramfs/usr/share/fonts/DejaVuSansMono.ttf +0 -0
  42. package/initramfs/usr/share/fonts/LICENSE-Apache-2.0.txt +202 -0
  43. package/initramfs/usr/share/fonts/LICENSE-DejaVu.txt +44 -0
  44. package/initramfs/usr/share/fonts/LICENSE-OFL-1.1.txt +91 -0
  45. package/initramfs/usr/share/fonts/NotoColorEmoji.ttf +0 -0
  46. package/initramfs/usr/share/fonts/NotoColorEmojiFlags.ttf +0 -0
  47. package/initramfs/usr/share/fonts/NotoSansCJK-Regular.ttc +0 -0
  48. package/initramfs/usr/share/fonts/NotoSansSymbols-Regular-Subsetted.ttf +0 -0
  49. package/initramfs/usr/share/fonts/NotoSansSymbols-Regular-Subsetted2.ttf +0 -0
  50. package/initramfs/usr/share/fonts/Roboto-Regular.ttf +0 -0
  51. package/initramfs/usr/share/fonts/fonts.json +28 -0
  52. package/package.json +2 -2
  53. package/run-qemu.sh +1 -1
@@ -0,0 +1,469 @@
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
+ this.lastPointer = null;
109
+ }
110
+
111
+ fontFor(typeface, size, { bold = false, italic = false } = {}) {
112
+ let id = this.typefaceIds.get(typeface);
113
+ if (id === undefined) {
114
+ id = this.typefaceIds.size;
115
+ this.typefaceIds.set(typeface, id);
116
+ }
117
+ const key = `${id}:${size}:${bold ? "b" : ""}${italic ? "i" : ""}`;
118
+ let font = this.fontCache.get(key);
119
+ if (!font) {
120
+ font = new this.CanvasKit.Font(typeface, size);
121
+ font.setSubpixel(true);
122
+ font.setEdging(this.CanvasKit.FontEdging.AntiAlias);
123
+ if (bold) font.setEmbolden(true);
124
+ if (italic) font.setSkewX(-0.2);
125
+ this.fontCache.set(key, font);
126
+ }
127
+ return font;
128
+ }
129
+
130
+ metricsFor(font) {
131
+ let metrics = this.metricsCache.get(font);
132
+ if (!metrics) {
133
+ metrics = font.getMetrics();
134
+ this.metricsCache.set(font, metrics);
135
+ }
136
+ return metrics;
137
+ }
138
+
139
+ paintFor(color, alpha = 255) {
140
+ const key = packColor(color) * 256 + alpha;
141
+ let paint = this.paintCache.get(key);
142
+ if (!paint) {
143
+ paint = new this.CanvasKit.Paint();
144
+ paint.setColor(this.CanvasKit.Color(color[0], color[1], color[2], alpha / 255));
145
+ paint.setAntiAlias(true);
146
+ this.paintCache.set(key, paint);
147
+ }
148
+ return paint;
149
+ }
150
+
151
+ // Finds the typeface holding `text` (a single code point) and its glyph.
152
+ resolveGlyph(text, emoji) {
153
+ const key = (emoji ? "e" : "m") + text;
154
+ let entry = this.glyphCache.get(key);
155
+ if (entry === undefined) {
156
+ entry = null;
157
+ const order = emoji
158
+ ? [this.fonts.regular, ...this.colorFallback, ...this.monoFallback]
159
+ : [this.fonts.regular, ...this.monoFallback, ...this.colorFallback];
160
+ for (const typeface of order) {
161
+ const id = typeface.getGlyphIDs(text, 1)[0];
162
+ if (id) {
163
+ entry = { typeface, glyph: id };
164
+ break;
165
+ }
166
+ }
167
+ this.glyphCache.set(key, entry);
168
+ }
169
+ return entry;
170
+ }
171
+
172
+ colorOf(cell, foreground) {
173
+ const rgb = foreground ? cell.isFgRGB() : cell.isBgRGB();
174
+ const palette = foreground ? cell.isFgPalette() : cell.isBgPalette();
175
+ const value = foreground ? cell.getFgColor() : cell.getBgColor();
176
+ if (rgb) return unpackColor(value);
177
+ if (palette) {
178
+ // Bold text in the eight base colours uses the bright variants.
179
+ const index = foreground && cell.isBold() && value < 8 ? value + 8 : value;
180
+ return this.palette[index] ?? this.foreground;
181
+ }
182
+ return foreground ? this.foreground : this.background;
183
+ }
184
+
185
+ rowHash(line, columns) {
186
+ let hash = 0x811c9dc5 | 0;
187
+ const mix = (value) => { hash = Math.imul(hash ^ value, 0x01000193); };
188
+ for (let x = 0; x < columns; x++) {
189
+ const cell = line.getCell(x);
190
+ if (!cell) break;
191
+ mix(cell.getCode());
192
+ mix(cell.getChars().length);
193
+ mix(cell.getWidth());
194
+ mix(cell.getFgColor() ^ (cell.getFgColorMode() >>> 16));
195
+ mix(cell.getBgColor() ^ (cell.getBgColorMode() >>> 16));
196
+ mix(cell.isBold() | cell.isItalic() | cell.isDim() | cell.isUnderline()
197
+ | cell.isInverse() | cell.isInvisible() | cell.isStrikethrough() | cell.isBlink());
198
+ }
199
+ return hash;
200
+ }
201
+
202
+ invalidate() {
203
+ this.dirtyAll = true;
204
+ }
205
+
206
+ // Marks rows [start, end) for repainting even if their text is unchanged,
207
+ // for image placements appearing or disappearing over them.
208
+ invalidateRows(start, end) {
209
+ for (let row = Math.max(0, start); row < Math.min(this.rows, end); row++) this.forcedRows.add(row);
210
+ }
211
+
212
+ cellRect(x, y, width = 1, height = 1) {
213
+ return this.CanvasKit.XYWHRect(this.offsetX + x * this.cellWidth, this.offsetY + y * this.cellHeight,
214
+ width * this.cellWidth, height * this.cellHeight);
215
+ }
216
+
217
+ drawText(text, x, y, cells, color, { bold, italic, emoji }) {
218
+ const { CanvasKit, canvas } = this;
219
+ const available = cells * this.cellWidth;
220
+ const paint = this.paintFor(color);
221
+ if (isSingleCodePoint(text)) {
222
+ const entry = this.resolveGlyph(text, emoji);
223
+ if (!entry) return;
224
+ const primary = entry.typeface === this.fonts.regular;
225
+ const typeface = primary && bold && this.fonts.bold ? this.fonts.bold : entry.typeface;
226
+ const glyph = primary && bold && this.fonts.bold ? this.fonts.bold.getGlyphIDs(text, 1)[0] : entry.glyph;
227
+ let font = this.fontFor(typeface, this.fontSize, { bold: bold && !primary, italic });
228
+ let advance = font.getGlyphWidths([glyph])[0];
229
+ // A fallback glyph wider than its cells (emoji, some symbols) is
230
+ // scaled down to fit rather than overlapping its neighbour.
231
+ if (advance > available + 0.5) {
232
+ const size = Math.floor(this.fontSize * available / advance);
233
+ font = this.fontFor(typeface, size, { bold: bold && !primary, italic });
234
+ advance = font.getGlyphWidths([glyph])[0];
235
+ }
236
+ const metrics = this.metricsFor(font);
237
+ 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);
239
+ return;
240
+ }
241
+ // Grapheme clusters are shaped by the Paragraph API and cached.
242
+ const key = `${text}|${packColor(color)}|${bold ? 1 : 0}${italic ? 1 : 0}`;
243
+ let paragraph = this.paragraphCache.get(key);
244
+ if (!paragraph) {
245
+ if (this.paragraphCache.size > 512) {
246
+ for (const [oldKey, old] of this.paragraphCache) {
247
+ old.delete();
248
+ this.paragraphCache.delete(oldKey);
249
+ if (this.paragraphCache.size <= 256) break;
250
+ }
251
+ }
252
+ const style = new CanvasKit.ParagraphStyle({
253
+ textStyle: {
254
+ color: CanvasKit.Color(color[0], color[1], color[2]),
255
+ fontFamilies: emoji ? this.emojiFamilies : this.fonts.families,
256
+ fontSize: this.fontSize,
257
+ fontStyle: {
258
+ weight: bold ? CanvasKit.FontWeight.Bold : CanvasKit.FontWeight.Normal,
259
+ slant: italic ? CanvasKit.FontSlant.Italic : CanvasKit.FontSlant.Upright,
260
+ },
261
+ },
262
+ maxLines: 1,
263
+ });
264
+ const builder = CanvasKit.ParagraphBuilder.Make(style, this.fonts.fontMgr);
265
+ builder.addText(text);
266
+ paragraph = builder.build();
267
+ builder.delete();
268
+ paragraph.layout(available * 2 + this.fontSize * 2);
269
+ this.paragraphCache.set(key, paragraph);
270
+ }
271
+ const width = paragraph.getLongestLine();
272
+ const height = paragraph.getHeight();
273
+ canvas.save();
274
+ canvas.clipRect(CanvasKit.XYWHRect(x, y, available, this.cellHeight), CanvasKit.ClipOp.Intersect, false);
275
+ if (width > available) {
276
+ const scale = available / width;
277
+ canvas.translate(x, y + (this.cellHeight - height * scale) / 2);
278
+ canvas.scale(scale, scale);
279
+ canvas.drawParagraph(paragraph, 0, 0);
280
+ } else {
281
+ canvas.drawParagraph(paragraph, x + (available - width) / 2, y + (this.cellHeight - height) / 2);
282
+ }
283
+ canvas.restore();
284
+ }
285
+
286
+ drawCell(cell, column, row) {
287
+ const width = cell.getWidth();
288
+ if (width === 0) return;
289
+ const { CanvasKit, canvas } = this;
290
+ let foreground = this.colorOf(cell, true);
291
+ let background = this.colorOf(cell, false);
292
+ if (cell.isInverse()) [foreground, background] = [background, foreground];
293
+ const x = this.offsetX + column * this.cellWidth;
294
+ const y = this.offsetY + row * this.cellHeight;
295
+ if (packColor(background) !== packColor(this.background)) {
296
+ canvas.drawRect(CanvasKit.XYWHRect(x, y, width * this.cellWidth, this.cellHeight), this.paintFor(background));
297
+ }
298
+ if (cell.isInvisible()) return;
299
+ if (cell.isDim()) {
300
+ foreground = foreground.map((channel, index) => Math.round((channel + background[index]) / 2));
301
+ }
302
+ const text = cell.getChars();
303
+ const bold = Boolean(cell.isBold());
304
+ const italic = Boolean(cell.isItalic());
305
+ if (text && text !== " ") {
306
+ if (hasCustomGlyph(text)) {
307
+ drawCustomGlyph(CanvasKit, canvas, text, x, y, width * this.cellWidth, this.cellHeight,
308
+ this.fontSize, this.paintFor(foreground), foreground);
309
+ } else {
310
+ this.drawText(text, x, y, width, foreground, { bold, italic, emoji: isEmojiText(text) });
311
+ }
312
+ }
313
+ if (cell.isUnderline() || cell.isStrikethrough()) {
314
+ const paint = this.paintFor(foreground);
315
+ const thickness = Math.max(1, Math.floor(this.fontSize / 14));
316
+ if (cell.isUnderline()) {
317
+ canvas.drawRect(CanvasKit.XYWHRect(x, y + this.cellHeight - thickness - 1, width * this.cellWidth, thickness), paint);
318
+ }
319
+ if (cell.isStrikethrough()) {
320
+ canvas.drawRect(CanvasKit.XYWHRect(x, y + Math.round(this.cellHeight * 0.55), width * this.cellWidth, thickness), paint);
321
+ }
322
+ }
323
+ }
324
+
325
+ drawRow(line, row) {
326
+ const { canvas } = this;
327
+ canvas.drawRect(this.cellRect(0, row, this.columns, 1), this.paintFor(this.background));
328
+ if (!line) return;
329
+ for (let column = 0; column < this.columns; column++) {
330
+ const cell = line.getCell(column);
331
+ if (!cell) break;
332
+ this.drawCell(cell, column, row);
333
+ }
334
+ }
335
+
336
+ drawCursor(line, x, y) {
337
+ const { CanvasKit, canvas } = this;
338
+ const cell = line?.getCell(x);
339
+ const width = cell?.getWidth() || 1;
340
+ const rect = this.cellRect(x, y, width, 1);
341
+ canvas.drawRect(rect, this.paintFor(this.cursorColor));
342
+ if (cell) {
343
+ // Repaint the character in the background colour over the block.
344
+ const text = cell.getChars();
345
+ const left = this.offsetX + x * this.cellWidth;
346
+ const top = this.offsetY + y * this.cellHeight;
347
+ if (text && text !== " ") {
348
+ if (hasCustomGlyph(text)) {
349
+ drawCustomGlyph(CanvasKit, canvas, text, left, top, width * this.cellWidth, this.cellHeight,
350
+ this.fontSize, this.paintFor(this.background), this.background);
351
+ } else {
352
+ this.drawText(text, left, top, width, this.background,
353
+ { bold: Boolean(cell.isBold()), italic: Boolean(cell.isItalic()), emoji: isEmojiText(text) });
354
+ }
355
+ }
356
+ }
357
+ }
358
+
359
+ // Draws the mouse pointer at (x, y) in pixels: an arrow outlined in the
360
+ // background colour so it stays visible over any cell.
361
+ drawPointer({ x, y }) {
362
+ const { CanvasKit, canvas } = this;
363
+ const size = Math.max(10, Math.round(this.fontSize * 0.9));
364
+ const builder = new CanvasKit.PathBuilder();
365
+ builder.moveTo(x, y);
366
+ builder.lineTo(x, y + size);
367
+ builder.lineTo(x + size * 0.28, y + size * 0.74);
368
+ builder.lineTo(x + size * 0.46, y + size * 1.08);
369
+ builder.lineTo(x + size * 0.63, y + size * 1.0);
370
+ builder.lineTo(x + size * 0.45, y + size * 0.66);
371
+ builder.lineTo(x + size * 0.72, y + size * 0.66);
372
+ builder.close();
373
+ const path = builder.detach();
374
+ builder.delete();
375
+ const outline = this.paintFor(this.background).copy();
376
+ outline.setStyle(CanvasKit.PaintStyle.Stroke);
377
+ outline.setStrokeWidth(Math.max(1, size / 8));
378
+ canvas.drawPath(path, outline);
379
+ canvas.drawPath(path, this.paintFor(this.cursorColor));
380
+ outline.delete();
381
+ path.delete();
382
+ }
383
+
384
+ // The rows an arrow at (x, y) covers, so they are repainted when it moves.
385
+ pointerRows({ x, y }) {
386
+ void x;
387
+ const size = Math.max(10, Math.round(this.fontSize * 0.9));
388
+ const first = Math.floor((y - this.offsetY) / this.cellHeight);
389
+ const last = Math.ceil((y + size * 1.1 - this.offsetY) / this.cellHeight);
390
+ return [first, last];
391
+ }
392
+
393
+ // Repaints what changed and flushes to the framebuffer. `cursor` is
394
+ // { x, y, visible }; `images` is a list of { image, rect } placements in
395
+ // screen pixels, from images.js, drawn over the text; `pointer` is the
396
+ // mouse position in pixels, drawn last. Returns true when anything was
397
+ // painted.
398
+ render(term, { cursor, images = [], pointer = null } = {}) {
399
+ const buffer = term.buffer.active;
400
+ const top = buffer.viewportY;
401
+ const dirty = [];
402
+ for (let row = 0; row < this.rows; row++) {
403
+ 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)) {
406
+ dirty.push(row);
407
+ this.rowHashes[row] = hash;
408
+ }
409
+ }
410
+ this.forcedRows.clear();
411
+ const cursorRows = new Set();
412
+ if (this.lastCursor.visible) cursorRows.add(this.lastCursor.y);
413
+ if (cursor?.visible) cursorRows.add(cursor.y);
414
+ // Wherever the pointer was and is has to be repainted: the text under it
415
+ // has not changed, so no row hash would say so.
416
+ for (const position of [this.lastPointer, pointer]) {
417
+ if (!position) continue;
418
+ const [first, last] = this.pointerRows(position);
419
+ for (let row = Math.max(0, first); row < Math.min(this.rows, last); row++) cursorRows.add(row);
420
+ }
421
+ for (const row of cursorRows) {
422
+ if (row >= 0 && row < this.rows && !dirty.includes(row)) dirty.push(row);
423
+ }
424
+ if (this.dirtyAll) {
425
+ this.canvas.clear(this.CanvasKit.Color(this.background[0], this.background[1], this.background[2]));
426
+ }
427
+ this.dirtyAll = false;
428
+ if (!dirty.length) return false;
429
+ dirty.sort((a, b) => a - b);
430
+ for (const row of dirty) this.drawRow(buffer.getLine(top + row), row);
431
+ if (cursor?.visible && cursor.y >= 0 && cursor.y < this.rows) {
432
+ this.drawCursor(buffer.getLine(top + cursor.y), cursor.x, cursor.y);
433
+ }
434
+ this.lastCursor = { x: cursor?.x ?? -1, y: cursor?.y ?? -1, visible: Boolean(cursor?.visible) };
435
+ if (images.length) this.drawImages(images, dirty);
436
+ if (pointer) this.drawPointer(pointer);
437
+ this.lastPointer = pointer ? { x: pointer.x, y: pointer.y } : null;
438
+ this.screen.flush();
439
+ return true;
440
+ }
441
+
442
+ // Redraws the parts of image placements that intersect the repainted rows.
443
+ drawImages(images, dirtyRows) {
444
+ const { CanvasKit, canvas } = this;
445
+ for (const { image, rect, source } of images) {
446
+ const [left, top, right, bottom] = rect;
447
+ const sourceRect = source ? CanvasKit.LTRBRect(...source) : CanvasKit.LTRBRect(0, 0, image.width(), image.height());
448
+ for (const row of dirtyRows) {
449
+ const rowTop = this.offsetY + row * this.cellHeight;
450
+ const rowBottom = rowTop + this.cellHeight;
451
+ if (bottom <= rowTop || top >= rowBottom) continue;
452
+ canvas.save();
453
+ 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),
455
+ CanvasKit.FilterMode.Linear, CanvasKit.MipmapMode.Linear, null);
456
+ canvas.restore();
457
+ }
458
+ }
459
+ }
460
+
461
+ delete() {
462
+ for (const font of this.fontCache.values()) font.delete();
463
+ for (const paint of this.paintCache.values()) paint.delete();
464
+ for (const paragraph of this.paragraphCache.values()) paragraph.delete();
465
+ this.fontCache.clear();
466
+ this.paintCache.clear();
467
+ this.paragraphCache.clear();
468
+ }
469
+ }