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,446 @@
|
|
|
1
|
+
// /lib/bunterm/glyphs.js — box drawing, block, shade, Powerline and legacy
|
|
2
|
+
// computing characters drawn as vectors instead of font glyphs, so they meet
|
|
3
|
+
// their neighbours exactly at every cell size.
|
|
4
|
+
//
|
|
5
|
+
// The shape tables are ported from xterm.js's addon-webgl CustomGlyphs.ts
|
|
6
|
+
// (Copyright (c) 2021 The xterm.js authors, MIT License; see /lib/xterm/
|
|
7
|
+
// LICENSE). The SVG-style path strings are kept as they are there; only the
|
|
8
|
+
// drawing side is rewritten for Skia.
|
|
9
|
+
|
|
10
|
+
// Block elements: rectangles on an 8x8 grid of the cell.
|
|
11
|
+
const blockElementDefinitions = {
|
|
12
|
+
"▀": [{ x: 0, y: 0, w: 8, h: 4 }],
|
|
13
|
+
"▁": [{ x: 0, y: 7, w: 8, h: 1 }],
|
|
14
|
+
"▂": [{ x: 0, y: 6, w: 8, h: 2 }],
|
|
15
|
+
"▃": [{ x: 0, y: 5, w: 8, h: 3 }],
|
|
16
|
+
"▄": [{ x: 0, y: 4, w: 8, h: 4 }],
|
|
17
|
+
"▅": [{ x: 0, y: 3, w: 8, h: 5 }],
|
|
18
|
+
"▆": [{ x: 0, y: 2, w: 8, h: 6 }],
|
|
19
|
+
"▇": [{ x: 0, y: 1, w: 8, h: 7 }],
|
|
20
|
+
"█": [{ x: 0, y: 0, w: 8, h: 8 }],
|
|
21
|
+
"▉": [{ x: 0, y: 0, w: 7, h: 8 }],
|
|
22
|
+
"▊": [{ x: 0, y: 0, w: 6, h: 8 }],
|
|
23
|
+
"▋": [{ x: 0, y: 0, w: 5, h: 8 }],
|
|
24
|
+
"▌": [{ x: 0, y: 0, w: 4, h: 8 }],
|
|
25
|
+
"▍": [{ x: 0, y: 0, w: 3, h: 8 }],
|
|
26
|
+
"▎": [{ x: 0, y: 0, w: 2, h: 8 }],
|
|
27
|
+
"▏": [{ x: 0, y: 0, w: 1, h: 8 }],
|
|
28
|
+
"▐": [{ x: 4, y: 0, w: 4, h: 8 }],
|
|
29
|
+
"▔": [{ x: 0, y: 0, w: 8, h: 1 }],
|
|
30
|
+
"▕": [{ x: 7, y: 0, w: 1, h: 8 }],
|
|
31
|
+
"▖": [{ x: 0, y: 4, w: 4, h: 4 }],
|
|
32
|
+
"▗": [{ x: 4, y: 4, w: 4, h: 4 }],
|
|
33
|
+
"▘": [{ x: 0, y: 0, w: 4, h: 4 }],
|
|
34
|
+
"▙": [{ x: 0, y: 0, w: 4, h: 8 }, { x: 0, y: 4, w: 8, h: 4 }],
|
|
35
|
+
"▚": [{ x: 0, y: 0, w: 4, h: 4 }, { x: 4, y: 4, w: 4, h: 4 }],
|
|
36
|
+
"▛": [{ x: 0, y: 0, w: 4, h: 8 }, { x: 4, y: 0, w: 4, h: 4 }],
|
|
37
|
+
"▜": [{ x: 0, y: 0, w: 8, h: 4 }, { x: 4, y: 0, w: 4, h: 8 }],
|
|
38
|
+
"▝": [{ x: 4, y: 0, w: 4, h: 4 }],
|
|
39
|
+
"▞": [{ x: 4, y: 0, w: 4, h: 4 }, { x: 0, y: 4, w: 4, h: 4 }],
|
|
40
|
+
"▟": [{ x: 4, y: 0, w: 4, h: 8 }, { x: 0, y: 4, w: 8, h: 4 }],
|
|
41
|
+
// Legacy computing: vertical and horizontal one eighth blocks
|
|
42
|
+
"\u{1FB70}": [{ x: 1, y: 0, w: 1, h: 8 }],
|
|
43
|
+
"\u{1FB71}": [{ x: 2, y: 0, w: 1, h: 8 }],
|
|
44
|
+
"\u{1FB72}": [{ x: 3, y: 0, w: 1, h: 8 }],
|
|
45
|
+
"\u{1FB73}": [{ x: 4, y: 0, w: 1, h: 8 }],
|
|
46
|
+
"\u{1FB74}": [{ x: 5, y: 0, w: 1, h: 8 }],
|
|
47
|
+
"\u{1FB75}": [{ x: 6, y: 0, w: 1, h: 8 }],
|
|
48
|
+
"\u{1FB76}": [{ x: 0, y: 1, w: 8, h: 1 }],
|
|
49
|
+
"\u{1FB77}": [{ x: 0, y: 2, w: 8, h: 1 }],
|
|
50
|
+
"\u{1FB78}": [{ x: 0, y: 3, w: 8, h: 1 }],
|
|
51
|
+
"\u{1FB79}": [{ x: 0, y: 4, w: 8, h: 1 }],
|
|
52
|
+
"\u{1FB7A}": [{ x: 0, y: 5, w: 8, h: 1 }],
|
|
53
|
+
"\u{1FB7B}": [{ x: 0, y: 6, w: 8, h: 1 }],
|
|
54
|
+
"\u{1FB7C}": [{ x: 0, y: 0, w: 1, h: 8 }, { x: 0, y: 7, w: 8, h: 1 }],
|
|
55
|
+
"\u{1FB7D}": [{ x: 0, y: 0, w: 1, h: 8 }, { x: 0, y: 0, w: 8, h: 1 }],
|
|
56
|
+
"\u{1FB7E}": [{ x: 7, y: 0, w: 1, h: 8 }, { x: 0, y: 0, w: 8, h: 1 }],
|
|
57
|
+
"\u{1FB7F}": [{ x: 7, y: 0, w: 1, h: 8 }, { x: 0, y: 7, w: 8, h: 1 }],
|
|
58
|
+
"\u{1FB80}": [{ x: 0, y: 0, w: 8, h: 1 }, { x: 0, y: 7, w: 8, h: 1 }],
|
|
59
|
+
"\u{1FB81}": [{ x: 0, y: 0, w: 8, h: 1 }, { x: 0, y: 2, w: 8, h: 1 }, { x: 0, y: 4, w: 8, h: 1 }, { x: 0, y: 7, w: 8, h: 1 }],
|
|
60
|
+
"\u{1FB82}": [{ x: 0, y: 0, w: 8, h: 2 }],
|
|
61
|
+
"\u{1FB83}": [{ x: 0, y: 0, w: 8, h: 3 }],
|
|
62
|
+
"\u{1FB84}": [{ x: 0, y: 0, w: 8, h: 5 }],
|
|
63
|
+
"\u{1FB85}": [{ x: 0, y: 0, w: 8, h: 6 }],
|
|
64
|
+
"\u{1FB86}": [{ x: 0, y: 0, w: 8, h: 7 }],
|
|
65
|
+
"\u{1FB87}": [{ x: 6, y: 0, w: 2, h: 8 }],
|
|
66
|
+
"\u{1FB88}": [{ x: 5, y: 0, w: 3, h: 8 }],
|
|
67
|
+
"\u{1FB89}": [{ x: 3, y: 0, w: 5, h: 8 }],
|
|
68
|
+
"\u{1FB8A}": [{ x: 2, y: 0, w: 6, h: 8 }],
|
|
69
|
+
"\u{1FB8B}": [{ x: 1, y: 0, w: 7, h: 8 }],
|
|
70
|
+
"\u{1FB95}": [
|
|
71
|
+
{ x: 0, y: 0, w: 2, h: 2 }, { x: 4, y: 0, w: 2, h: 2 },
|
|
72
|
+
{ x: 2, y: 2, w: 2, h: 2 }, { x: 6, y: 2, w: 2, h: 2 },
|
|
73
|
+
{ x: 0, y: 4, w: 2, h: 2 }, { x: 4, y: 4, w: 2, h: 2 },
|
|
74
|
+
{ x: 2, y: 6, w: 2, h: 2 }, { x: 6, y: 6, w: 2, h: 2 },
|
|
75
|
+
],
|
|
76
|
+
"\u{1FB96}": [
|
|
77
|
+
{ x: 2, y: 0, w: 2, h: 2 }, { x: 6, y: 0, w: 2, h: 2 },
|
|
78
|
+
{ x: 0, y: 2, w: 2, h: 2 }, { x: 4, y: 2, w: 2, h: 2 },
|
|
79
|
+
{ x: 2, y: 4, w: 2, h: 2 }, { x: 6, y: 4, w: 2, h: 2 },
|
|
80
|
+
{ x: 0, y: 6, w: 2, h: 2 }, { x: 4, y: 6, w: 2, h: 2 },
|
|
81
|
+
],
|
|
82
|
+
"\u{1FB97}": [{ x: 0, y: 2, w: 8, h: 2 }, { x: 0, y: 6, w: 8, h: 2 }],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Shade characters: a repeating pixel pattern (1 = filled).
|
|
86
|
+
const patternDefinitions = {
|
|
87
|
+
"░": [[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]],
|
|
88
|
+
"▒": [[1, 0], [0, 0], [0, 1], [0, 0]],
|
|
89
|
+
"▓": [[0, 1], [1, 1], [1, 0], [1, 1]],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const Shapes = {
|
|
93
|
+
TOP_TO_BOTTOM: "M.5,0 L.5,1",
|
|
94
|
+
LEFT_TO_RIGHT: "M0,.5 L1,.5",
|
|
95
|
+
TOP_TO_RIGHT: "M.5,0 L.5,.5 L1,.5",
|
|
96
|
+
TOP_TO_LEFT: "M.5,0 L.5,.5 L0,.5",
|
|
97
|
+
LEFT_TO_BOTTOM: "M0,.5 L.5,.5 L.5,1",
|
|
98
|
+
RIGHT_TO_BOTTOM: "M0.5,1 L.5,.5 L1,.5",
|
|
99
|
+
MIDDLE_TO_TOP: "M.5,.5 L.5,0",
|
|
100
|
+
MIDDLE_TO_LEFT: "M.5,.5 L0,.5",
|
|
101
|
+
MIDDLE_TO_RIGHT: "M.5,.5 L1,.5",
|
|
102
|
+
MIDDLE_TO_BOTTOM: "M.5,.5 L.5,1",
|
|
103
|
+
T_TOP: "M0,.5 L1,.5 M.5,.5 L.5,0",
|
|
104
|
+
T_LEFT: "M.5,0 L.5,1 M.5,.5 L0,.5",
|
|
105
|
+
T_RIGHT: "M.5,0 L.5,1 M.5,.5 L1,.5",
|
|
106
|
+
T_BOTTOM: "M0,.5 L1,.5 M.5,.5 L.5,1",
|
|
107
|
+
CROSS: "M0,.5 L1,.5 M.5,0 L.5,1",
|
|
108
|
+
TWO_DASHES_HORIZONTAL: "M.1,.5 L.4,.5 M.6,.5 L.9,.5",
|
|
109
|
+
THREE_DASHES_HORIZONTAL: "M.0667,.5 L.2667,.5 M.4,.5 L.6,.5 M.7333,.5 L.9333,.5",
|
|
110
|
+
FOUR_DASHES_HORIZONTAL: "M.05,.5 L.2,.5 M.3,.5 L.45,.5 M.55,.5 L.7,.5 M.8,.5 L.95,.5",
|
|
111
|
+
TWO_DASHES_VERTICAL: "M.5,.1 L.5,.4 M.5,.6 L.5,.9",
|
|
112
|
+
THREE_DASHES_VERTICAL: "M.5,.0667 L.5,.2667 M.5,.4 L.5,.6 M.5,.7333 L.5,.9333",
|
|
113
|
+
FOUR_DASHES_VERTICAL: "M.5,.05 L.5,.2 M.5,.3 L.5,.45 L.5,.55 M.5,.7 L.5,.95",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const NORMAL = 1;
|
|
117
|
+
const BOLD = 3;
|
|
118
|
+
|
|
119
|
+
// Box drawing: per line weight, a path string or a function of the double
|
|
120
|
+
// line spacing (xp, yp) that returns one.
|
|
121
|
+
const S = Shapes;
|
|
122
|
+
const boxDrawingDefinitions = {
|
|
123
|
+
"─": { [NORMAL]: S.LEFT_TO_RIGHT },
|
|
124
|
+
"━": { [BOLD]: S.LEFT_TO_RIGHT },
|
|
125
|
+
"│": { [NORMAL]: S.TOP_TO_BOTTOM },
|
|
126
|
+
"┃": { [BOLD]: S.TOP_TO_BOTTOM },
|
|
127
|
+
"┌": { [NORMAL]: S.RIGHT_TO_BOTTOM },
|
|
128
|
+
"┏": { [BOLD]: S.RIGHT_TO_BOTTOM },
|
|
129
|
+
"┐": { [NORMAL]: S.LEFT_TO_BOTTOM },
|
|
130
|
+
"┓": { [BOLD]: S.LEFT_TO_BOTTOM },
|
|
131
|
+
"└": { [NORMAL]: S.TOP_TO_RIGHT },
|
|
132
|
+
"┗": { [BOLD]: S.TOP_TO_RIGHT },
|
|
133
|
+
"┘": { [NORMAL]: S.TOP_TO_LEFT },
|
|
134
|
+
"┛": { [BOLD]: S.TOP_TO_LEFT },
|
|
135
|
+
"├": { [NORMAL]: S.T_RIGHT },
|
|
136
|
+
"┣": { [BOLD]: S.T_RIGHT },
|
|
137
|
+
"┤": { [NORMAL]: S.T_LEFT },
|
|
138
|
+
"┫": { [BOLD]: S.T_LEFT },
|
|
139
|
+
"┬": { [NORMAL]: S.T_BOTTOM },
|
|
140
|
+
"┳": { [BOLD]: S.T_BOTTOM },
|
|
141
|
+
"┴": { [NORMAL]: S.T_TOP },
|
|
142
|
+
"┻": { [BOLD]: S.T_TOP },
|
|
143
|
+
"┼": { [NORMAL]: S.CROSS },
|
|
144
|
+
"╋": { [BOLD]: S.CROSS },
|
|
145
|
+
"╴": { [NORMAL]: S.MIDDLE_TO_LEFT },
|
|
146
|
+
"╸": { [BOLD]: S.MIDDLE_TO_LEFT },
|
|
147
|
+
"╵": { [NORMAL]: S.MIDDLE_TO_TOP },
|
|
148
|
+
"╹": { [BOLD]: S.MIDDLE_TO_TOP },
|
|
149
|
+
"╶": { [NORMAL]: S.MIDDLE_TO_RIGHT },
|
|
150
|
+
"╺": { [BOLD]: S.MIDDLE_TO_RIGHT },
|
|
151
|
+
"╷": { [NORMAL]: S.MIDDLE_TO_BOTTOM },
|
|
152
|
+
"╻": { [BOLD]: S.MIDDLE_TO_BOTTOM },
|
|
153
|
+
// Double
|
|
154
|
+
"═": { [NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` },
|
|
155
|
+
"║": { [NORMAL]: (xp) => `M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` },
|
|
156
|
+
"╒": { [NORMAL]: (xp, yp) => `M.5,1 L.5,${.5 - yp} L1,${.5 - yp} M.5,${.5 + yp} L1,${.5 + yp}` },
|
|
157
|
+
"╓": { [NORMAL]: (xp) => `M${.5 - xp},1 L${.5 - xp},.5 L1,.5 M${.5 + xp},.5 L${.5 + xp},1` },
|
|
158
|
+
"╔": { [NORMAL]: (xp, yp) => `M1,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1` },
|
|
159
|
+
"╕": { [NORMAL]: (xp, yp) => `M0,${.5 - yp} L.5,${.5 - yp} L.5,1 M0,${.5 + yp} L.5,${.5 + yp}` },
|
|
160
|
+
"╖": { [NORMAL]: (xp) => `M${.5 + xp},1 L${.5 + xp},.5 L0,.5 M${.5 - xp},.5 L${.5 - xp},1` },
|
|
161
|
+
"╗": { [NORMAL]: (xp, yp) => `M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M0,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},1` },
|
|
162
|
+
"╘": { [NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 + yp} L1,${.5 + yp} M.5,${.5 - yp} L1,${.5 - yp}` },
|
|
163
|
+
"╙": { [NORMAL]: (xp) => `M1,.5 L${.5 - xp},.5 L${.5 - xp},0 M${.5 + xp},.5 L${.5 + xp},0` },
|
|
164
|
+
"╚": { [NORMAL]: (xp, yp) => `M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0 M1,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},0` },
|
|
165
|
+
"╛": { [NORMAL]: (xp, yp) => `M0,${.5 + yp} L.5,${.5 + yp} L.5,0 M0,${.5 - yp} L.5,${.5 - yp}` },
|
|
166
|
+
"╜": { [NORMAL]: (xp) => `M0,.5 L${.5 + xp},.5 L${.5 + xp},0 M${.5 - xp},.5 L${.5 - xp},0` },
|
|
167
|
+
"╝": { [NORMAL]: (xp, yp) => `M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M0,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},0` },
|
|
168
|
+
"╞": { [NORMAL]: (xp, yp) => `${S.TOP_TO_BOTTOM} M.5,${.5 - yp} L1,${.5 - yp} M.5,${.5 + yp} L1,${.5 + yp}` },
|
|
169
|
+
"╟": { [NORMAL]: (xp) => `M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1 M${.5 + xp},.5 L1,.5` },
|
|
170
|
+
"╠": { [NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` },
|
|
171
|
+
"╡": { [NORMAL]: (xp, yp) => `${S.TOP_TO_BOTTOM} M0,${.5 - yp} L.5,${.5 - yp} M0,${.5 + yp} L.5,${.5 + yp}` },
|
|
172
|
+
"╢": { [NORMAL]: (xp) => `M0,.5 L${.5 - xp},.5 M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` },
|
|
173
|
+
"╣": { [NORMAL]: (xp, yp) => `M${.5 + xp},0 L${.5 + xp},1 M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0` },
|
|
174
|
+
"╤": { [NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp} M.5,${.5 + yp} L.5,1` },
|
|
175
|
+
"╥": { [NORMAL]: (xp) => `${S.LEFT_TO_RIGHT} M${.5 - xp},.5 L${.5 - xp},1 M${.5 + xp},.5 L${.5 + xp},1` },
|
|
176
|
+
"╦": { [NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1` },
|
|
177
|
+
"╧": { [NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 - yp} M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` },
|
|
178
|
+
"╨": { [NORMAL]: (xp) => `${S.LEFT_TO_RIGHT} M${.5 - xp},.5 L${.5 - xp},0 M${.5 + xp},.5 L${.5 + xp},0` },
|
|
179
|
+
"╩": { [NORMAL]: (xp, yp) => `M0,${.5 + yp} L1,${.5 + yp} M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` },
|
|
180
|
+
"╪": { [NORMAL]: (xp, yp) => `${S.TOP_TO_BOTTOM} M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` },
|
|
181
|
+
"╫": { [NORMAL]: (xp) => `${S.LEFT_TO_RIGHT} M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` },
|
|
182
|
+
"╬": { [NORMAL]: (xp, yp) => `M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1 M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` },
|
|
183
|
+
// Diagonal
|
|
184
|
+
"╱": { [NORMAL]: "M1,0 L0,1" },
|
|
185
|
+
"╲": { [NORMAL]: "M0,0 L1,1" },
|
|
186
|
+
"╳": { [NORMAL]: "M1,0 L0,1 M0,0 L1,1" },
|
|
187
|
+
// Mixed weight
|
|
188
|
+
"╼": { [NORMAL]: S.MIDDLE_TO_LEFT, [BOLD]: S.MIDDLE_TO_RIGHT },
|
|
189
|
+
"╽": { [NORMAL]: S.MIDDLE_TO_TOP, [BOLD]: S.MIDDLE_TO_BOTTOM },
|
|
190
|
+
"╾": { [NORMAL]: S.MIDDLE_TO_RIGHT, [BOLD]: S.MIDDLE_TO_LEFT },
|
|
191
|
+
"╿": { [NORMAL]: S.MIDDLE_TO_BOTTOM, [BOLD]: S.MIDDLE_TO_TOP },
|
|
192
|
+
"┍": { [NORMAL]: S.MIDDLE_TO_BOTTOM, [BOLD]: S.MIDDLE_TO_RIGHT },
|
|
193
|
+
"┎": { [NORMAL]: S.MIDDLE_TO_RIGHT, [BOLD]: S.MIDDLE_TO_BOTTOM },
|
|
194
|
+
"┑": { [NORMAL]: S.MIDDLE_TO_BOTTOM, [BOLD]: S.MIDDLE_TO_LEFT },
|
|
195
|
+
"┒": { [NORMAL]: S.MIDDLE_TO_LEFT, [BOLD]: S.MIDDLE_TO_BOTTOM },
|
|
196
|
+
"┕": { [NORMAL]: S.MIDDLE_TO_TOP, [BOLD]: S.MIDDLE_TO_RIGHT },
|
|
197
|
+
"┖": { [NORMAL]: S.MIDDLE_TO_RIGHT, [BOLD]: S.MIDDLE_TO_TOP },
|
|
198
|
+
"┙": { [NORMAL]: S.MIDDLE_TO_TOP, [BOLD]: S.MIDDLE_TO_LEFT },
|
|
199
|
+
"┚": { [NORMAL]: S.MIDDLE_TO_LEFT, [BOLD]: S.MIDDLE_TO_TOP },
|
|
200
|
+
"┝": { [NORMAL]: S.TOP_TO_BOTTOM, [BOLD]: S.MIDDLE_TO_RIGHT },
|
|
201
|
+
"┞": { [NORMAL]: S.RIGHT_TO_BOTTOM, [BOLD]: S.MIDDLE_TO_TOP },
|
|
202
|
+
"┟": { [NORMAL]: S.TOP_TO_RIGHT, [BOLD]: S.MIDDLE_TO_BOTTOM },
|
|
203
|
+
"┠": { [NORMAL]: S.MIDDLE_TO_RIGHT, [BOLD]: S.TOP_TO_BOTTOM },
|
|
204
|
+
"┡": { [NORMAL]: S.MIDDLE_TO_BOTTOM, [BOLD]: S.TOP_TO_RIGHT },
|
|
205
|
+
"┢": { [NORMAL]: S.MIDDLE_TO_TOP, [BOLD]: S.RIGHT_TO_BOTTOM },
|
|
206
|
+
"┥": { [NORMAL]: S.TOP_TO_BOTTOM, [BOLD]: S.MIDDLE_TO_LEFT },
|
|
207
|
+
"┦": { [NORMAL]: S.LEFT_TO_BOTTOM, [BOLD]: S.MIDDLE_TO_TOP },
|
|
208
|
+
"┧": { [NORMAL]: S.TOP_TO_LEFT, [BOLD]: S.MIDDLE_TO_BOTTOM },
|
|
209
|
+
"┨": { [NORMAL]: S.MIDDLE_TO_LEFT, [BOLD]: S.TOP_TO_BOTTOM },
|
|
210
|
+
"┩": { [NORMAL]: S.MIDDLE_TO_BOTTOM, [BOLD]: S.TOP_TO_LEFT },
|
|
211
|
+
"┪": { [NORMAL]: S.MIDDLE_TO_TOP, [BOLD]: S.LEFT_TO_BOTTOM },
|
|
212
|
+
"┭": { [NORMAL]: S.RIGHT_TO_BOTTOM, [BOLD]: S.MIDDLE_TO_LEFT },
|
|
213
|
+
"┮": { [NORMAL]: S.LEFT_TO_BOTTOM, [BOLD]: S.MIDDLE_TO_RIGHT },
|
|
214
|
+
"┯": { [NORMAL]: S.MIDDLE_TO_BOTTOM, [BOLD]: S.LEFT_TO_RIGHT },
|
|
215
|
+
"┰": { [NORMAL]: S.LEFT_TO_RIGHT, [BOLD]: S.MIDDLE_TO_BOTTOM },
|
|
216
|
+
"┱": { [NORMAL]: S.MIDDLE_TO_RIGHT, [BOLD]: S.LEFT_TO_BOTTOM },
|
|
217
|
+
"┲": { [NORMAL]: S.MIDDLE_TO_LEFT, [BOLD]: S.RIGHT_TO_BOTTOM },
|
|
218
|
+
"┵": { [NORMAL]: S.TOP_TO_RIGHT, [BOLD]: S.MIDDLE_TO_LEFT },
|
|
219
|
+
"┶": { [NORMAL]: S.TOP_TO_LEFT, [BOLD]: S.MIDDLE_TO_RIGHT },
|
|
220
|
+
"┷": { [NORMAL]: S.MIDDLE_TO_TOP, [BOLD]: S.LEFT_TO_RIGHT },
|
|
221
|
+
"┸": { [NORMAL]: S.LEFT_TO_RIGHT, [BOLD]: S.MIDDLE_TO_TOP },
|
|
222
|
+
"┹": { [NORMAL]: S.MIDDLE_TO_RIGHT, [BOLD]: S.TOP_TO_LEFT },
|
|
223
|
+
"┺": { [NORMAL]: S.MIDDLE_TO_LEFT, [BOLD]: S.TOP_TO_RIGHT },
|
|
224
|
+
"┽": { [NORMAL]: `${S.TOP_TO_BOTTOM} ${S.MIDDLE_TO_RIGHT}`, [BOLD]: S.MIDDLE_TO_LEFT },
|
|
225
|
+
"┾": { [NORMAL]: `${S.TOP_TO_BOTTOM} ${S.MIDDLE_TO_LEFT}`, [BOLD]: S.MIDDLE_TO_RIGHT },
|
|
226
|
+
"┿": { [NORMAL]: S.TOP_TO_BOTTOM, [BOLD]: S.LEFT_TO_RIGHT },
|
|
227
|
+
"╀": { [NORMAL]: `${S.LEFT_TO_RIGHT} ${S.MIDDLE_TO_BOTTOM}`, [BOLD]: S.MIDDLE_TO_TOP },
|
|
228
|
+
"╁": { [NORMAL]: `${S.MIDDLE_TO_TOP} ${S.LEFT_TO_RIGHT}`, [BOLD]: S.MIDDLE_TO_BOTTOM },
|
|
229
|
+
"╂": { [NORMAL]: S.LEFT_TO_RIGHT, [BOLD]: S.TOP_TO_BOTTOM },
|
|
230
|
+
"╃": { [NORMAL]: S.RIGHT_TO_BOTTOM, [BOLD]: S.TOP_TO_LEFT },
|
|
231
|
+
"╄": { [NORMAL]: S.LEFT_TO_BOTTOM, [BOLD]: S.TOP_TO_RIGHT },
|
|
232
|
+
"╅": { [NORMAL]: S.TOP_TO_RIGHT, [BOLD]: S.LEFT_TO_BOTTOM },
|
|
233
|
+
"╆": { [NORMAL]: S.TOP_TO_LEFT, [BOLD]: S.RIGHT_TO_BOTTOM },
|
|
234
|
+
"╇": { [NORMAL]: S.MIDDLE_TO_BOTTOM, [BOLD]: `${S.MIDDLE_TO_TOP} ${S.LEFT_TO_RIGHT}` },
|
|
235
|
+
"╈": { [NORMAL]: S.MIDDLE_TO_TOP, [BOLD]: `${S.LEFT_TO_RIGHT} ${S.MIDDLE_TO_BOTTOM}` },
|
|
236
|
+
"╉": { [NORMAL]: S.MIDDLE_TO_RIGHT, [BOLD]: `${S.TOP_TO_BOTTOM} ${S.MIDDLE_TO_LEFT}` },
|
|
237
|
+
"╊": { [NORMAL]: S.MIDDLE_TO_LEFT, [BOLD]: `${S.TOP_TO_BOTTOM} ${S.MIDDLE_TO_RIGHT}` },
|
|
238
|
+
// Dashed
|
|
239
|
+
"╌": { [NORMAL]: S.TWO_DASHES_HORIZONTAL },
|
|
240
|
+
"╍": { [BOLD]: S.TWO_DASHES_HORIZONTAL },
|
|
241
|
+
"┄": { [NORMAL]: S.THREE_DASHES_HORIZONTAL },
|
|
242
|
+
"┅": { [BOLD]: S.THREE_DASHES_HORIZONTAL },
|
|
243
|
+
"┈": { [NORMAL]: S.FOUR_DASHES_HORIZONTAL },
|
|
244
|
+
"┉": { [BOLD]: S.FOUR_DASHES_HORIZONTAL },
|
|
245
|
+
"╎": { [NORMAL]: S.TWO_DASHES_VERTICAL },
|
|
246
|
+
"╏": { [BOLD]: S.TWO_DASHES_VERTICAL },
|
|
247
|
+
"┆": { [NORMAL]: S.THREE_DASHES_VERTICAL },
|
|
248
|
+
"┇": { [BOLD]: S.THREE_DASHES_VERTICAL },
|
|
249
|
+
"┊": { [NORMAL]: S.FOUR_DASHES_VERTICAL },
|
|
250
|
+
"┋": { [BOLD]: S.FOUR_DASHES_VERTICAL },
|
|
251
|
+
// Curved
|
|
252
|
+
"╭": { [NORMAL]: (xp, yp) => `M.5,1 L.5,${.5 + (yp / .15 * .5)} C.5,${.5 + (yp / .15 * .5)},.5,.5,1,.5` },
|
|
253
|
+
"╮": { [NORMAL]: (xp, yp) => `M.5,1 L.5,${.5 + (yp / .15 * .5)} C.5,${.5 + (yp / .15 * .5)},.5,.5,0,.5` },
|
|
254
|
+
"╯": { [NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 - (yp / .15 * .5)} C.5,${.5 - (yp / .15 * .5)},.5,.5,0,.5` },
|
|
255
|
+
"╰": { [NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 - (yp / .15 * .5)} C.5,${.5 - (yp / .15 * .5)},.5,.5,1,.5` },
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const FILL = 0;
|
|
259
|
+
const STROKE = 1;
|
|
260
|
+
|
|
261
|
+
// Powerline and private-use separators (https://github.com/powerline/fontpatcher).
|
|
262
|
+
// Line variants extend past the cell and are clipped so the line ends stay hidden.
|
|
263
|
+
const powerlineDefinitions = {
|
|
264
|
+
"\u{E0A0}": { d: "M.3,1 L.03,1 L.03,.88 C.03,.82,.06,.78,.11,.73 C.15,.7,.2,.68,.28,.65 L.43,.6 C.49,.58,.53,.56,.56,.53 C.59,.5,.6,.47,.6,.43 L.6,.27 L.4,.27 L.69,.1 L.98,.27 L.78,.27 L.78,.46 C.78,.52,.76,.56,.72,.61 C.68,.66,.63,.67,.56,.7 L.48,.72 C.42,.74,.38,.76,.35,.78 C.32,.8,.31,.84,.31,.88 L.31,1 M.3,.5 L.03,.59 L.03,.09 L.3,.09 L.3,.655", type: FILL },
|
|
265
|
+
"\u{E0A1}": { d: "M.7,.4 L.7,.47 L.2,.47 L.2,.03 L.355,.03 L.355,.4 L.705,.4 M.7,.5 L.86,.5 L.86,.95 L.69,.95 L.44,.66 L.46,.86 L.46,.95 L.3,.95 L.3,.49 L.46,.49 L.71,.78 L.69,.565 L.69,.5", type: FILL },
|
|
266
|
+
"\u{E0A2}": { d: "M.25,.94 C.16,.94,.11,.92,.11,.87 L.11,.53 C.11,.48,.15,.455,.23,.45 L.23,.3 C.23,.25,.26,.22,.31,.19 C.36,.16,.43,.15,.51,.15 C.59,.15,.66,.16,.71,.19 C.77,.22,.79,.26,.79,.3 L.79,.45 C.87,.45,.91,.48,.91,.53 L.91,.87 C.91,.92,.86,.94,.77,.94 L.24,.94 M.53,.2 C.49,.2,.45,.21,.42,.23 C.39,.25,.38,.27,.38,.3 L.38,.45 L.68,.45 L.68,.3 C.68,.27,.67,.25,.64,.23 C.61,.21,.58,.2,.53,.2 M.58,.82 L.58,.66 C.63,.65,.65,.63,.65,.6 C.65,.58,.64,.57,.61,.56 C.58,.55,.56,.54,.52,.54 C.48,.54,.46,.55,.43,.56 C.4,.57,.39,.59,.39,.6 C.39,.63,.41,.64,.46,.66 L.46,.82 L.57,.82", type: FILL },
|
|
267
|
+
"\u{E0B0}": { d: "M0,0 L1,.5 L0,1", type: FILL, rightPadding: 2 },
|
|
268
|
+
"\u{E0B1}": { d: "M-1,-.5 L1,.5 L-1,1.5", type: STROKE, leftPadding: 1, rightPadding: 1 },
|
|
269
|
+
"\u{E0B2}": { d: "M1,0 L0,.5 L1,1", type: FILL, leftPadding: 2 },
|
|
270
|
+
"\u{E0B3}": { d: "M2,-.5 L0,.5 L2,1.5", type: STROKE, leftPadding: 1, rightPadding: 1 },
|
|
271
|
+
"\u{E0B4}": { d: "M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0", type: FILL, rightPadding: 1 },
|
|
272
|
+
"\u{E0B5}": { d: "M.2,1 C.422,1,.8,.826,.78,.5 C.8,.174,0.422,0,.2,0", type: STROKE, rightPadding: 1 },
|
|
273
|
+
"\u{E0B6}": { d: "M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0", type: FILL, leftPadding: 1 },
|
|
274
|
+
"\u{E0B7}": { d: "M.8,1 C0.578,1,0.2,.826,.22,.5 C0.2,0.174,0.578,0,0.8,0", type: STROKE, leftPadding: 1 },
|
|
275
|
+
"\u{E0B8}": { d: "M-.5,-.5 L1.5,1.5 L-.5,1.5", type: FILL },
|
|
276
|
+
"\u{E0B9}": { d: "M-.5,-.5 L1.5,1.5", type: STROKE, leftPadding: 1, rightPadding: 1 },
|
|
277
|
+
"\u{E0BA}": { d: "M1.5,-.5 L-.5,1.5 L1.5,1.5", type: FILL },
|
|
278
|
+
"\u{E0BC}": { d: "M1.5,-.5 L-.5,1.5 L-.5,-.5", type: FILL },
|
|
279
|
+
"\u{E0BD}": { d: "M1.5,-.5 L-.5,1.5", type: STROKE, leftPadding: 1, rightPadding: 1 },
|
|
280
|
+
"\u{E0BE}": { d: "M-.5,-.5 L1.5,1.5 L1.5,-.5", type: FILL },
|
|
281
|
+
};
|
|
282
|
+
powerlineDefinitions["\u{E0BB}"] = powerlineDefinitions["\u{E0BD}"];
|
|
283
|
+
powerlineDefinitions["\u{E0BF}"] = powerlineDefinitions["\u{E0B9}"];
|
|
284
|
+
|
|
285
|
+
export const hasCustomGlyph = (character) =>
|
|
286
|
+
character in blockElementDefinitions
|
|
287
|
+
|| character in patternDefinitions
|
|
288
|
+
|| character in boxDrawingDefinitions
|
|
289
|
+
|| character in powerlineDefinitions;
|
|
290
|
+
|
|
291
|
+
// "M.5,0 L.5,1 C..." → [["M", .5, 0], ["L", .5, 1], ["C", ...]]
|
|
292
|
+
const parsePath = (instructions) => instructions.split(" ").map((instruction) => {
|
|
293
|
+
const numbers = instruction.slice(1).split(",").map(Number);
|
|
294
|
+
return [instruction[0], ...numbers];
|
|
295
|
+
}).filter((op) => op.length >= 3 && "MLC".includes(op[0]));
|
|
296
|
+
|
|
297
|
+
const parsedPaths = new Map();
|
|
298
|
+
|
|
299
|
+
const pathFor = (instructions) => {
|
|
300
|
+
let ops = parsedPaths.get(instructions);
|
|
301
|
+
if (!ops) {
|
|
302
|
+
ops = parsePath(instructions);
|
|
303
|
+
parsedPaths.set(instructions, ops);
|
|
304
|
+
}
|
|
305
|
+
return ops;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const clamp = (value, maximum) => Math.max(Math.min(value, maximum), 0);
|
|
309
|
+
|
|
310
|
+
// Scales 0–1 path coordinates to the cell. Box drawing coordinates snap to
|
|
311
|
+
// pixel centres (odd line widths) or pixel edges (even) so lines are crisp
|
|
312
|
+
// and meet the neighbouring cell without a seam.
|
|
313
|
+
const buildPath = (CanvasKit, ops, x, y, width, height, {
|
|
314
|
+
snap = false, lineWidth = 1, leftPadding = 0, rightPadding = 0,
|
|
315
|
+
} = {}) => {
|
|
316
|
+
const builder = new CanvasKit.PathBuilder();
|
|
317
|
+
const drawWidth = width - leftPadding - rightPadding;
|
|
318
|
+
const half = lineWidth % 2 ? 0.5 : 0;
|
|
319
|
+
const translateX = (value) => {
|
|
320
|
+
let result = value * drawWidth;
|
|
321
|
+
if (snap && result !== 0) result = clamp(Math.round(result + half) - half, width);
|
|
322
|
+
return result + x + leftPadding;
|
|
323
|
+
};
|
|
324
|
+
const translateY = (value) => {
|
|
325
|
+
let result = value * height;
|
|
326
|
+
if (snap && result !== 0) result = clamp(Math.round(result + half) - half, height);
|
|
327
|
+
return result + y;
|
|
328
|
+
};
|
|
329
|
+
for (const op of ops) {
|
|
330
|
+
if (op[0] === "M") builder.moveTo(translateX(op[1]), translateY(op[2]));
|
|
331
|
+
else if (op[0] === "L") builder.lineTo(translateX(op[1]), translateY(op[2]));
|
|
332
|
+
else if (op[0] === "C") {
|
|
333
|
+
builder.cubicTo(translateX(op[1]), translateY(op[2]), translateX(op[3]), translateY(op[4]),
|
|
334
|
+
translateX(op[5]), translateY(op[6]));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const path = builder.detach();
|
|
338
|
+
builder.delete();
|
|
339
|
+
return path;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// Shade characters are a repeating image shader, one per (pattern, color).
|
|
343
|
+
const shaderCache = new Map();
|
|
344
|
+
|
|
345
|
+
const patternShader = (CanvasKit, pattern, color) => {
|
|
346
|
+
const key = pattern.length + ":" + color.join(",");
|
|
347
|
+
let entry = shaderCache.get(key);
|
|
348
|
+
if (!entry) {
|
|
349
|
+
const height = pattern.length;
|
|
350
|
+
const width = pattern[0].length;
|
|
351
|
+
const pixels = new Uint8Array(width * height * 4);
|
|
352
|
+
for (let row = 0; row < height; row++) {
|
|
353
|
+
for (let column = 0; column < width; column++) {
|
|
354
|
+
const offset = (row * width + column) * 4;
|
|
355
|
+
pixels[offset] = color[0];
|
|
356
|
+
pixels[offset + 1] = color[1];
|
|
357
|
+
pixels[offset + 2] = color[2];
|
|
358
|
+
pixels[offset + 3] = pattern[row][column] ? 255 : 0;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const image = CanvasKit.MakeImage({
|
|
362
|
+
width, height,
|
|
363
|
+
colorType: CanvasKit.ColorType.RGBA_8888,
|
|
364
|
+
alphaType: CanvasKit.AlphaType.Unpremul,
|
|
365
|
+
colorSpace: CanvasKit.ColorSpace.SRGB,
|
|
366
|
+
}, pixels, width * 4);
|
|
367
|
+
const shader = image.makeShaderOptions(CanvasKit.TileMode.Repeat, CanvasKit.TileMode.Repeat,
|
|
368
|
+
CanvasKit.FilterMode.Nearest, CanvasKit.MipmapMode.None);
|
|
369
|
+
entry = { image, shader };
|
|
370
|
+
shaderCache.set(key, entry);
|
|
371
|
+
}
|
|
372
|
+
return entry.shader;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Draws `character` into the cell at (x, y) if it has a vector definition.
|
|
376
|
+
// `paint` is a fill paint carrying the foreground colour; `color` is the
|
|
377
|
+
// same colour as [r, g, b] for the shade shader cache. `fontSize` scales the
|
|
378
|
+
// line widths: 1 px up to 23 px, then one more pixel per 12 px, the way
|
|
379
|
+
// xterm.js's Powerline glyphs scale. Returns false when the character is not
|
|
380
|
+
// a custom glyph and should come from a font.
|
|
381
|
+
export const drawCustomGlyph = (CanvasKit, canvas, character, x, y, width, height, fontSize, paint, color) => {
|
|
382
|
+
const block = blockElementDefinitions[character];
|
|
383
|
+
if (block) {
|
|
384
|
+
const eighthX = width / 8;
|
|
385
|
+
const eighthY = height / 8;
|
|
386
|
+
for (const box of block) {
|
|
387
|
+
canvas.drawRect(CanvasKit.XYWHRect(x + box.x * eighthX, y + box.y * eighthY,
|
|
388
|
+
box.w * eighthX, box.h * eighthY), paint);
|
|
389
|
+
}
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const pattern = patternDefinitions[character];
|
|
394
|
+
if (pattern) {
|
|
395
|
+
const shaded = new CanvasKit.Paint();
|
|
396
|
+
shaded.setShader(patternShader(CanvasKit, pattern, color));
|
|
397
|
+
canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), shaded);
|
|
398
|
+
shaded.delete();
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const unit = Math.max(1, Math.floor(fontSize / 12));
|
|
403
|
+
const box = boxDrawingDefinitions[character];
|
|
404
|
+
if (box) {
|
|
405
|
+
const stroke = paint.copy();
|
|
406
|
+
stroke.setStyle(CanvasKit.PaintStyle.Stroke);
|
|
407
|
+
stroke.setAntiAlias(false);
|
|
408
|
+
for (const [weight, instructions] of Object.entries(box)) {
|
|
409
|
+
const lineWidth = unit * Number(weight);
|
|
410
|
+
stroke.setStrokeWidth(lineWidth);
|
|
411
|
+
const resolved = typeof instructions === "function"
|
|
412
|
+
? instructions(0.15, 0.15 / height * width)
|
|
413
|
+
: instructions;
|
|
414
|
+
const path = buildPath(CanvasKit, pathFor(resolved), x, y, width, height, { snap: true, lineWidth });
|
|
415
|
+
canvas.drawPath(path, stroke);
|
|
416
|
+
path.delete();
|
|
417
|
+
}
|
|
418
|
+
stroke.delete();
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const powerline = powerlineDefinitions[character];
|
|
423
|
+
if (powerline) {
|
|
424
|
+
const lineWidth = fontSize / 12;
|
|
425
|
+
const path = buildPath(CanvasKit, pathFor(powerline.d), x, y, width, height, {
|
|
426
|
+
leftPadding: (powerline.leftPadding ?? 0) * lineWidth / 2,
|
|
427
|
+
rightPadding: (powerline.rightPadding ?? 0) * lineWidth / 2,
|
|
428
|
+
});
|
|
429
|
+
canvas.save();
|
|
430
|
+
canvas.clipRect(CanvasKit.XYWHRect(x, y, width, height), CanvasKit.ClipOp.Intersect, false);
|
|
431
|
+
if (powerline.type === STROKE) {
|
|
432
|
+
const stroke = paint.copy();
|
|
433
|
+
stroke.setStyle(CanvasKit.PaintStyle.Stroke);
|
|
434
|
+
stroke.setStrokeWidth(lineWidth);
|
|
435
|
+
canvas.drawPath(path, stroke);
|
|
436
|
+
stroke.delete();
|
|
437
|
+
} else {
|
|
438
|
+
canvas.drawPath(path, paint);
|
|
439
|
+
}
|
|
440
|
+
canvas.restore();
|
|
441
|
+
path.delete();
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return false;
|
|
446
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// /lib/bunterm/images.js — kitty graphics protocol placements on the cell
|
|
2
|
+
// grid.
|
|
3
|
+
//
|
|
4
|
+
// jsgotty's KittyGraphicsParser has already split the APC packets out of the
|
|
5
|
+
// PTY stream and assembled chunked transfers; this module turns its
|
|
6
|
+
// "placement" / "delete" events into Skia images anchored to terminal lines.
|
|
7
|
+
// A placement is tied to an xterm.js marker, so it scrolls with the text and
|
|
8
|
+
// disappears when its line leaves the scrollback.
|
|
9
|
+
|
|
10
|
+
const number = (value, fallback = 0) => {
|
|
11
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
12
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Decodes a transferred image into a Skia Image. Format 100 is PNG (or any
|
|
16
|
+
// encoded format Skia knows); 24 and 32 are raw RGB / RGBA. Bun.Image, when
|
|
17
|
+
// present, transcodes encodings Skia cannot read on its own.
|
|
18
|
+
export const decodeImage = async (CanvasKit, image) => {
|
|
19
|
+
const bytes = Buffer.from(image.data, "base64");
|
|
20
|
+
if (image.format === 24 || image.format === 32) {
|
|
21
|
+
const { width, height } = image;
|
|
22
|
+
if (!width || !height) throw new Error("raw image without dimensions");
|
|
23
|
+
let pixels = bytes;
|
|
24
|
+
if (image.format === 24) {
|
|
25
|
+
pixels = Buffer.alloc(width * height * 4, 255);
|
|
26
|
+
for (let i = 0, o = 0; i < bytes.length; i += 3, o += 4) {
|
|
27
|
+
pixels[o] = bytes[i];
|
|
28
|
+
pixels[o + 1] = bytes[i + 1];
|
|
29
|
+
pixels[o + 2] = bytes[i + 2];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const decoded = CanvasKit.MakeImage({
|
|
33
|
+
width, height,
|
|
34
|
+
colorType: CanvasKit.ColorType.RGBA_8888,
|
|
35
|
+
alphaType: CanvasKit.AlphaType.Unpremul,
|
|
36
|
+
colorSpace: CanvasKit.ColorSpace.SRGB,
|
|
37
|
+
}, pixels, width * 4);
|
|
38
|
+
if (!decoded) throw new Error("raw image rejected");
|
|
39
|
+
return decoded;
|
|
40
|
+
}
|
|
41
|
+
let decoded = CanvasKit.MakeImageFromEncoded(bytes);
|
|
42
|
+
if (!decoded && typeof Bun !== "undefined" && Bun.Image) {
|
|
43
|
+
const png = await new Bun.Image(bytes).png().toBuffer();
|
|
44
|
+
decoded = CanvasKit.MakeImageFromEncoded(png);
|
|
45
|
+
}
|
|
46
|
+
if (!decoded) throw new Error("undecodable image data");
|
|
47
|
+
return decoded;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export class ImageStore {
|
|
51
|
+
constructor({ CanvasKit, term, renderer }) {
|
|
52
|
+
this.CanvasKit = CanvasKit;
|
|
53
|
+
this.term = term;
|
|
54
|
+
this.renderer = renderer;
|
|
55
|
+
this.images = new Map(); // image id → Skia Image
|
|
56
|
+
this.placements = new Map(); // placement key → placement
|
|
57
|
+
this.counter = 0;
|
|
58
|
+
this.coveredKey = "";
|
|
59
|
+
this.coveredRows = new Set();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handles one parsed graphic. Returns the control sequence that moves the
|
|
63
|
+
// cursor past a placement (to be written to the terminal), or "".
|
|
64
|
+
async handle(graphic) {
|
|
65
|
+
if (!graphic) return "";
|
|
66
|
+
if (graphic.kind === "delete") {
|
|
67
|
+
this.remove(graphic.delete);
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
if (graphic.kind !== "placement") return "";
|
|
71
|
+
const { control, image } = graphic;
|
|
72
|
+
let decoded = this.images.get(image.id);
|
|
73
|
+
if (!decoded) {
|
|
74
|
+
try {
|
|
75
|
+
decoded = await decodeImage(this.CanvasKit, image);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
graphic.responseMessage = `EBADPNG:${error.message}`;
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
this.images.set(image.id, decoded);
|
|
81
|
+
}
|
|
82
|
+
const { cellWidth, cellHeight } = this.renderer;
|
|
83
|
+
const buffer = this.term.buffer.active;
|
|
84
|
+
const sourceX = number(control.x);
|
|
85
|
+
const sourceY = number(control.y);
|
|
86
|
+
const sourceWidth = number(control.w) || decoded.width() - sourceX;
|
|
87
|
+
const sourceHeight = number(control.h) || decoded.height() - sourceY;
|
|
88
|
+
const columns = number(control.c) || Math.max(1, Math.ceil(sourceWidth / cellWidth));
|
|
89
|
+
const rows = number(control.r) || Math.max(1, Math.ceil(sourceHeight / cellHeight));
|
|
90
|
+
// xterm.js only issues markers in the normal buffer. On the alternate
|
|
91
|
+
// screen (full-screen programs) there is no scrollback, so the placement
|
|
92
|
+
// is anchored to a fixed row instead and dropped when that screen ends.
|
|
93
|
+
const marker = buffer.type === "normal" ? this.term.registerMarker(0) : null;
|
|
94
|
+
const placementId = control.p ?? null;
|
|
95
|
+
const key = `${image.id}:${placementId ?? `auto${++this.counter}`}`;
|
|
96
|
+
const existing = this.placements.get(key);
|
|
97
|
+
if (existing) this.dispose(existing);
|
|
98
|
+
const placement = {
|
|
99
|
+
key,
|
|
100
|
+
imageId: image.id,
|
|
101
|
+
placementId,
|
|
102
|
+
marker,
|
|
103
|
+
bufferType: buffer.type,
|
|
104
|
+
fixedLine: buffer.baseY + buffer.cursorY,
|
|
105
|
+
get line() { return this.marker ? this.marker.line : this.fixedLine; },
|
|
106
|
+
get isDisposed() {
|
|
107
|
+
return this.marker ? this.marker.isDisposed : this.bufferType !== this.term.buffer.active.type;
|
|
108
|
+
},
|
|
109
|
+
term: this.term,
|
|
110
|
+
column: buffer.cursorX,
|
|
111
|
+
columns,
|
|
112
|
+
rows,
|
|
113
|
+
offsetX: number(control.X),
|
|
114
|
+
offsetY: number(control.Y),
|
|
115
|
+
source: [sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight],
|
|
116
|
+
zIndex: number(control.z),
|
|
117
|
+
};
|
|
118
|
+
this.placements.set(key, placement);
|
|
119
|
+
this.invalidate(placement);
|
|
120
|
+
if (control.C === "1") return "";
|
|
121
|
+
// Move the cursor below the image, the way jsgotty does for its browser
|
|
122
|
+
// terminal: one line feed per image row (so the screen scrolls when the
|
|
123
|
+
// image runs past the bottom, and the placement's marker scrolls with
|
|
124
|
+
// it), then to the column after the image.
|
|
125
|
+
return `${"\r\n".repeat(rows)}\u001b[${buffer.cursorX + columns + 1}G`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
invalidate(placement) {
|
|
129
|
+
const top = placement.line - this.term.buffer.active.viewportY;
|
|
130
|
+
this.renderer.invalidateRows(top, top + placement.rows);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
dispose(placement) {
|
|
134
|
+
this.invalidate(placement);
|
|
135
|
+
placement.marker?.dispose();
|
|
136
|
+
this.placements.delete(placement.key);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
remove({ scope, imageId, placementId, cursor }) {
|
|
140
|
+
const wipe = scope === scope.toUpperCase();
|
|
141
|
+
const targets = [];
|
|
142
|
+
for (const placement of this.placements.values()) {
|
|
143
|
+
switch (scope.toLowerCase()) {
|
|
144
|
+
case "a":
|
|
145
|
+
targets.push(placement);
|
|
146
|
+
break;
|
|
147
|
+
case "i":
|
|
148
|
+
if (placement.imageId === imageId && (!placementId || placement.placementId === placementId)) {
|
|
149
|
+
targets.push(placement);
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
case "c": {
|
|
153
|
+
const row = placement.line - this.term.buffer.active.baseY + 1;
|
|
154
|
+
const column = placement.column + 1;
|
|
155
|
+
if (cursor && cursor.row >= row && cursor.row < row + placement.rows
|
|
156
|
+
&& cursor.col >= column && cursor.col < column + placement.columns) {
|
|
157
|
+
targets.push(placement);
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
default:
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const placement of targets) this.dispose(placement);
|
|
166
|
+
if (wipe) {
|
|
167
|
+
const used = new Set([...this.placements.values()].map((placement) => placement.imageId));
|
|
168
|
+
for (const [id, image] of this.images) {
|
|
169
|
+
if (!used.has(id)) {
|
|
170
|
+
image.delete();
|
|
171
|
+
this.images.delete(id);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Placements currently on screen, as { image, source, rect } in pixels.
|
|
178
|
+
visible() {
|
|
179
|
+
const { renderer } = this;
|
|
180
|
+
const { cellWidth, cellHeight, offsetX, offsetY } = renderer;
|
|
181
|
+
const viewportY = this.term.buffer.active.viewportY;
|
|
182
|
+
const result = [];
|
|
183
|
+
for (const placement of [...this.placements.values()]) {
|
|
184
|
+
if (placement.isDisposed) {
|
|
185
|
+
this.placements.delete(placement.key);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const top = placement.line - viewportY;
|
|
189
|
+
if (top + placement.rows <= 0 || top >= renderer.rows) continue;
|
|
190
|
+
const image = this.images.get(placement.imageId);
|
|
191
|
+
if (!image) continue;
|
|
192
|
+
const left = offsetX + placement.column * cellWidth + placement.offsetX;
|
|
193
|
+
const y = offsetY + top * cellHeight + placement.offsetY;
|
|
194
|
+
result.push({
|
|
195
|
+
image,
|
|
196
|
+
source: placement.source,
|
|
197
|
+
rect: [left, y, left + placement.columns * cellWidth, y + placement.rows * cellHeight],
|
|
198
|
+
zIndex: placement.zIndex,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// The renderer's row hashes only see text, so when placements move or
|
|
202
|
+
// vanish every row they covered before or cover now is repainted.
|
|
203
|
+
const covered = new Set();
|
|
204
|
+
for (const { rect } of result) {
|
|
205
|
+
const first = Math.floor((rect[1] - offsetY) / cellHeight);
|
|
206
|
+
const last = Math.ceil((rect[3] - offsetY) / cellHeight);
|
|
207
|
+
for (let row = first; row < last; row++) covered.add(row);
|
|
208
|
+
}
|
|
209
|
+
const key = [...covered].sort((a, b) => a - b).join(",");
|
|
210
|
+
if (key !== this.coveredKey) {
|
|
211
|
+
for (const row of this.coveredRows ?? []) renderer.invalidateRows(row, row + 1);
|
|
212
|
+
for (const row of covered) renderer.invalidateRows(row, row + 1);
|
|
213
|
+
this.coveredKey = key;
|
|
214
|
+
this.coveredRows = covered;
|
|
215
|
+
}
|
|
216
|
+
return result.sort((a, b) => a.zIndex - b.zIndex);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
delete() {
|
|
220
|
+
for (const placement of [...this.placements.values()]) placement.marker?.dispose();
|
|
221
|
+
this.placements.clear();
|
|
222
|
+
for (const image of this.images.values()) image.delete();
|
|
223
|
+
this.images.clear();
|
|
224
|
+
}
|
|
225
|
+
}
|