@dkkoval/tui-preview 0.1.0 → 0.2.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/README.md +24 -16
- package/dist/TuiPreview.js +64 -145
- package/dist/core/index.d.ts +2 -3
- package/dist/core/index.js +2 -3
- package/dist/core/libghostty.d.ts +86 -0
- package/dist/core/libghostty.js +678 -0
- package/dist/core/normalize.d.ts +0 -1
- package/dist/core/normalize.js +20 -66
- package/dist/core/wasi.d.ts +1 -1
- package/dist/core/wasi.js +26 -6
- package/dist/ghostty-vt.wasm +0 -0
- package/dist/index.cjs +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +645 -411
- package/dist/types.d.ts +8 -26
- package/package.json +14 -7
- package/dist/__vite-browser-external-2447137e-BcPniuRQ.cjs +0 -1
- package/dist/__vite-browser-external-2447137e-DYxpcVy9.js +0 -4
- package/dist/core/ansi.d.ts +0 -15
- package/dist/core/ansi.js +0 -181
- package/dist/core/ghostty.d.ts +0 -2
- package/dist/core/ghostty.js +0 -11
- package/dist/ghostty-web-BfBVpf8G.js +0 -2962
- package/dist/ghostty-web-DkOZu5AZ.cjs +0 -13
- package/dist/wasi.d.ts +0 -1
- package/dist/wasi.js +0 -2
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
const EXPECTED_CELL_SIZE = 16;
|
|
2
|
+
const EXPECTED_TERMINAL_CONFIG_SIZE = 80;
|
|
3
|
+
const DEFAULT_WASM_URL = "/ghostty-vt.wasm";
|
|
4
|
+
const FLAG_BOLD = 1 << 0;
|
|
5
|
+
const FLAG_ITALIC = 1 << 1;
|
|
6
|
+
const FLAG_UNDERLINE = 1 << 2;
|
|
7
|
+
const FLAG_FG_EXPLICIT = 1 << 3;
|
|
8
|
+
const FLAG_INVERSE = 1 << 4;
|
|
9
|
+
const FLAG_INVISIBLE = 1 << 5;
|
|
10
|
+
const FLAG_BG_EXPLICIT = 1 << 6;
|
|
11
|
+
const FLAG_FAINT = 1 << 7;
|
|
12
|
+
const ANSI_THEME_KEYS = [
|
|
13
|
+
"black",
|
|
14
|
+
"red",
|
|
15
|
+
"green",
|
|
16
|
+
"yellow",
|
|
17
|
+
"blue",
|
|
18
|
+
"magenta",
|
|
19
|
+
"cyan",
|
|
20
|
+
"white",
|
|
21
|
+
"brightBlack",
|
|
22
|
+
"brightRed",
|
|
23
|
+
"brightGreen",
|
|
24
|
+
"brightYellow",
|
|
25
|
+
"brightBlue",
|
|
26
|
+
"brightMagenta",
|
|
27
|
+
"brightCyan",
|
|
28
|
+
"brightWhite",
|
|
29
|
+
];
|
|
30
|
+
const DEFAULT_THEME = {
|
|
31
|
+
background: "#1a1b26",
|
|
32
|
+
foreground: "#a9b1d6",
|
|
33
|
+
cursor: "#c0caf5",
|
|
34
|
+
selectionBackground: "#33467c",
|
|
35
|
+
selectionForeground: "#c0caf5",
|
|
36
|
+
black: "#15161e",
|
|
37
|
+
red: "#f7768e",
|
|
38
|
+
green: "#9ece6a",
|
|
39
|
+
yellow: "#e0af68",
|
|
40
|
+
blue: "#7aa2f7",
|
|
41
|
+
magenta: "#bb9af7",
|
|
42
|
+
cyan: "#7dcfff",
|
|
43
|
+
white: "#a9b1d6",
|
|
44
|
+
brightBlack: "#414868",
|
|
45
|
+
brightRed: "#f7768e",
|
|
46
|
+
brightGreen: "#9ece6a",
|
|
47
|
+
brightYellow: "#e0af68",
|
|
48
|
+
brightBlue: "#7aa2f7",
|
|
49
|
+
brightMagenta: "#bb9af7",
|
|
50
|
+
brightCyan: "#7dcfff",
|
|
51
|
+
brightWhite: "#c0caf5",
|
|
52
|
+
};
|
|
53
|
+
class LibGhosttyRuntime {
|
|
54
|
+
wasm;
|
|
55
|
+
abi;
|
|
56
|
+
constructor(wasm, abi) {
|
|
57
|
+
this.wasm = wasm;
|
|
58
|
+
this.abi = abi;
|
|
59
|
+
}
|
|
60
|
+
createTerminal(cols, rows, theme) {
|
|
61
|
+
const configPtr = this.wasm.ghostty_wasm_alloc_u8_array(this.abi.terminalConfigSize);
|
|
62
|
+
if (!configPtr) {
|
|
63
|
+
throw new Error("Failed to allocate terminal config.");
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const view = new DataView(this.wasm.memory.buffer);
|
|
67
|
+
let offset = configPtr;
|
|
68
|
+
view.setUint32(offset, 10_000, true);
|
|
69
|
+
offset += 4;
|
|
70
|
+
view.setUint32(offset, parseColorToHex(theme.foreground), true);
|
|
71
|
+
offset += 4;
|
|
72
|
+
view.setUint32(offset, parseColorToHex(theme.background), true);
|
|
73
|
+
offset += 4;
|
|
74
|
+
view.setUint32(offset, parseColorToHex(theme.cursor), true);
|
|
75
|
+
offset += 4;
|
|
76
|
+
for (const key of ANSI_THEME_KEYS) {
|
|
77
|
+
view.setUint32(offset, parseColorToHex(theme[key]), true);
|
|
78
|
+
offset += 4;
|
|
79
|
+
}
|
|
80
|
+
const handle = this.wasm.ghostty_terminal_new_with_config(cols, rows, configPtr);
|
|
81
|
+
if (!handle) {
|
|
82
|
+
throw new Error("Failed to create libghostty terminal.");
|
|
83
|
+
}
|
|
84
|
+
return new LibGhosttyTerminal(this.wasm, handle, cols, rows, this.abi.cellSize);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
this.wasm.ghostty_wasm_free_u8_array(configPtr, this.abi.terminalConfigSize);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
class LibGhosttyTerminal {
|
|
92
|
+
wasm;
|
|
93
|
+
handle;
|
|
94
|
+
cols;
|
|
95
|
+
rows;
|
|
96
|
+
cellSize;
|
|
97
|
+
viewportPtr = 0;
|
|
98
|
+
viewportLen = 0;
|
|
99
|
+
constructor(wasm, handle, cols, rows, cellSize) {
|
|
100
|
+
this.wasm = wasm;
|
|
101
|
+
this.handle = handle;
|
|
102
|
+
this.cols = cols;
|
|
103
|
+
this.rows = rows;
|
|
104
|
+
this.cellSize = cellSize;
|
|
105
|
+
}
|
|
106
|
+
write(textOrData) {
|
|
107
|
+
const data = typeof textOrData === "string" ? new TextEncoder().encode(textOrData) : textOrData;
|
|
108
|
+
if (data.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
const ptr = this.wasm.ghostty_wasm_alloc_u8_array(data.length);
|
|
111
|
+
if (!ptr) {
|
|
112
|
+
throw new Error("Failed to allocate libghostty write buffer.");
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
new Uint8Array(this.wasm.memory.buffer).set(data, ptr);
|
|
116
|
+
this.wasm.ghostty_terminal_write(this.handle, ptr, data.length);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
this.wasm.ghostty_wasm_free_u8_array(ptr, data.length);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
resize(cols, rows) {
|
|
123
|
+
if (cols === this.cols && rows === this.rows)
|
|
124
|
+
return;
|
|
125
|
+
this.cols = cols;
|
|
126
|
+
this.rows = rows;
|
|
127
|
+
this.wasm.ghostty_terminal_resize(this.handle, cols, rows);
|
|
128
|
+
this.releaseViewport();
|
|
129
|
+
}
|
|
130
|
+
hasResponse() {
|
|
131
|
+
return this.wasm.ghostty_terminal_has_response(this.handle);
|
|
132
|
+
}
|
|
133
|
+
isDirty() {
|
|
134
|
+
return this.wasm.ghostty_render_state_update(this.handle) !== 0;
|
|
135
|
+
}
|
|
136
|
+
readResponse(maxBytes = 4096) {
|
|
137
|
+
const ptr = this.wasm.ghostty_wasm_alloc_u8_array(maxBytes);
|
|
138
|
+
if (!ptr) {
|
|
139
|
+
throw new Error("Failed to allocate libghostty response buffer.");
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const written = this.wasm.ghostty_terminal_read_response(this.handle, ptr, maxBytes);
|
|
143
|
+
if (written <= 0)
|
|
144
|
+
return null;
|
|
145
|
+
const bytes = new Uint8Array(this.wasm.memory.buffer, ptr, written);
|
|
146
|
+
return new TextDecoder().decode(bytes);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
this.wasm.ghostty_wasm_free_u8_array(ptr, maxBytes);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
getViewportData() {
|
|
153
|
+
const cols = this.wasm.ghostty_render_state_get_cols(this.handle);
|
|
154
|
+
const rows = this.wasm.ghostty_render_state_get_rows(this.handle);
|
|
155
|
+
this.cols = cols;
|
|
156
|
+
this.rows = rows;
|
|
157
|
+
const required = Math.max(1, cols * rows * this.cellSize);
|
|
158
|
+
if (required > this.viewportLen || this.viewportPtr === 0) {
|
|
159
|
+
this.releaseViewport();
|
|
160
|
+
this.viewportPtr = this.wasm.ghostty_wasm_alloc_u8_array(required);
|
|
161
|
+
this.viewportLen = required;
|
|
162
|
+
if (!this.viewportPtr) {
|
|
163
|
+
throw new Error("Failed to allocate libghostty viewport buffer.");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const written = this.wasm.ghostty_render_state_get_viewport(this.handle, this.viewportPtr, this.viewportLen);
|
|
167
|
+
const snapshot = new Uint8Array(written);
|
|
168
|
+
if (written > 0) {
|
|
169
|
+
const source = new Uint8Array(this.wasm.memory.buffer, this.viewportPtr, written);
|
|
170
|
+
snapshot.set(source);
|
|
171
|
+
}
|
|
172
|
+
this.wasm.ghostty_render_state_mark_clean(this.handle);
|
|
173
|
+
return {
|
|
174
|
+
cols,
|
|
175
|
+
rows,
|
|
176
|
+
buffer: snapshot,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
dispose() {
|
|
180
|
+
this.releaseViewport();
|
|
181
|
+
this.wasm.ghostty_terminal_free(this.handle);
|
|
182
|
+
}
|
|
183
|
+
releaseViewport() {
|
|
184
|
+
if (this.viewportPtr === 0)
|
|
185
|
+
return;
|
|
186
|
+
this.wasm.ghostty_wasm_free_u8_array(this.viewportPtr, this.viewportLen);
|
|
187
|
+
this.viewportPtr = 0;
|
|
188
|
+
this.viewportLen = 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
class MiniRenderer {
|
|
192
|
+
canvas;
|
|
193
|
+
cols;
|
|
194
|
+
rows;
|
|
195
|
+
fontSize;
|
|
196
|
+
fontFamily;
|
|
197
|
+
theme;
|
|
198
|
+
ctx;
|
|
199
|
+
dpr;
|
|
200
|
+
metrics;
|
|
201
|
+
constructor(canvas, cols, rows, fontSize, fontFamily, theme) {
|
|
202
|
+
this.canvas = canvas;
|
|
203
|
+
this.cols = cols;
|
|
204
|
+
this.rows = rows;
|
|
205
|
+
this.fontSize = fontSize;
|
|
206
|
+
this.fontFamily = fontFamily;
|
|
207
|
+
this.theme = theme;
|
|
208
|
+
const ctx = canvas.getContext("2d");
|
|
209
|
+
if (!ctx) {
|
|
210
|
+
throw new Error("Failed to create 2D canvas context.");
|
|
211
|
+
}
|
|
212
|
+
this.ctx = ctx;
|
|
213
|
+
this.dpr = window.devicePixelRatio || 1;
|
|
214
|
+
this.metrics = this.measureFont();
|
|
215
|
+
this.resizeCanvas(cols, rows);
|
|
216
|
+
}
|
|
217
|
+
get cellSize() {
|
|
218
|
+
return { w: this.metrics.width, h: this.metrics.height };
|
|
219
|
+
}
|
|
220
|
+
render(viewport) {
|
|
221
|
+
if (viewport.cols !== this.cols || viewport.rows !== this.rows) {
|
|
222
|
+
this.cols = viewport.cols;
|
|
223
|
+
this.rows = viewport.rows;
|
|
224
|
+
this.resizeCanvas(viewport.cols, viewport.rows);
|
|
225
|
+
}
|
|
226
|
+
const { cols, rows, buffer } = viewport;
|
|
227
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
228
|
+
const ctx = this.ctx;
|
|
229
|
+
const charW = this.metrics.width;
|
|
230
|
+
const charH = this.metrics.height;
|
|
231
|
+
ctx.fillStyle = this.theme.background;
|
|
232
|
+
ctx.fillRect(0, 0, cols * charW, rows * charH);
|
|
233
|
+
for (let y = 0; y < rows; y++) {
|
|
234
|
+
for (let x = 0; x < cols; x++) {
|
|
235
|
+
const base = (y * cols + x) * EXPECTED_CELL_SIZE;
|
|
236
|
+
const width = view.getUint8(base + 11);
|
|
237
|
+
if (width === 0)
|
|
238
|
+
continue;
|
|
239
|
+
const flags = view.getUint8(base + 10);
|
|
240
|
+
const fg = readRgb(view, base + 4);
|
|
241
|
+
const bg = readRgb(view, base + 7);
|
|
242
|
+
const inverse = hasFlag(flags, FLAG_INVERSE);
|
|
243
|
+
const fgColor = hasFlag(flags, FLAG_FG_EXPLICIT) ? rgbToCss(fg) : this.theme.foreground;
|
|
244
|
+
const bgColor = hasFlag(flags, FLAG_BG_EXPLICIT) ? rgbToCss(bg) : this.theme.background;
|
|
245
|
+
if (inverse || hasFlag(flags, FLAG_BG_EXPLICIT)) {
|
|
246
|
+
ctx.fillStyle = inverse ? fgColor : bgColor;
|
|
247
|
+
ctx.fillRect(x * charW, y * charH, width * charW, charH);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (let y = 0; y < rows; y++) {
|
|
252
|
+
for (let x = 0; x < cols; x++) {
|
|
253
|
+
const base = (y * cols + x) * EXPECTED_CELL_SIZE;
|
|
254
|
+
const width = view.getUint8(base + 11);
|
|
255
|
+
if (width === 0)
|
|
256
|
+
continue;
|
|
257
|
+
const flags = view.getUint8(base + 10);
|
|
258
|
+
if (hasFlag(flags, FLAG_INVISIBLE))
|
|
259
|
+
continue;
|
|
260
|
+
const codepoint = view.getUint32(base, true);
|
|
261
|
+
if (codepoint === 0)
|
|
262
|
+
continue;
|
|
263
|
+
const inverse = hasFlag(flags, FLAG_INVERSE);
|
|
264
|
+
const fg = readRgb(view, base + 4);
|
|
265
|
+
const bg = readRgb(view, base + 7);
|
|
266
|
+
const fgColor = hasFlag(flags, FLAG_FG_EXPLICIT) ? rgbToCss(fg) : this.theme.foreground;
|
|
267
|
+
const bgColor = hasFlag(flags, FLAG_BG_EXPLICIT) ? rgbToCss(bg) : this.theme.background;
|
|
268
|
+
ctx.fillStyle = inverse ? bgColor : fgColor;
|
|
269
|
+
let style = "";
|
|
270
|
+
if (hasFlag(flags, FLAG_ITALIC))
|
|
271
|
+
style += "italic ";
|
|
272
|
+
if (hasFlag(flags, FLAG_BOLD))
|
|
273
|
+
style += "bold ";
|
|
274
|
+
ctx.font = `${style}${this.fontSize}px ${this.fontFamily}`;
|
|
275
|
+
if (hasFlag(flags, FLAG_FAINT)) {
|
|
276
|
+
ctx.globalAlpha = 0.5;
|
|
277
|
+
}
|
|
278
|
+
const text = safeCodepoint(codepoint);
|
|
279
|
+
ctx.fillText(text, x * charW, y * charH + this.metrics.baseline);
|
|
280
|
+
if (hasFlag(flags, FLAG_FAINT)) {
|
|
281
|
+
ctx.globalAlpha = 1;
|
|
282
|
+
}
|
|
283
|
+
if (hasFlag(flags, FLAG_UNDERLINE)) {
|
|
284
|
+
const underlineY = y * charH + this.metrics.baseline + 2;
|
|
285
|
+
ctx.strokeStyle = ctx.fillStyle;
|
|
286
|
+
ctx.lineWidth = 1;
|
|
287
|
+
ctx.beginPath();
|
|
288
|
+
ctx.moveTo(x * charW, underlineY);
|
|
289
|
+
ctx.lineTo(x * charW + width * charW, underlineY);
|
|
290
|
+
ctx.stroke();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
dispose() {
|
|
296
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
297
|
+
}
|
|
298
|
+
measureFont() {
|
|
299
|
+
this.ctx.font = `${this.fontSize}px ${this.fontFamily}`;
|
|
300
|
+
const metrics = this.ctx.measureText("M");
|
|
301
|
+
const width = Math.ceil(metrics.width);
|
|
302
|
+
const ascent = metrics.actualBoundingBoxAscent || this.fontSize * 0.8;
|
|
303
|
+
const descent = metrics.actualBoundingBoxDescent || this.fontSize * 0.2;
|
|
304
|
+
return {
|
|
305
|
+
width,
|
|
306
|
+
height: Math.ceil(ascent + descent) + 2,
|
|
307
|
+
baseline: Math.ceil(ascent) + 1,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
resizeCanvas(cols, rows) {
|
|
311
|
+
const width = cols * this.metrics.width;
|
|
312
|
+
const height = rows * this.metrics.height;
|
|
313
|
+
this.canvas.width = Math.max(1, Math.floor(width * this.dpr));
|
|
314
|
+
this.canvas.height = Math.max(1, Math.floor(height * this.dpr));
|
|
315
|
+
this.canvas.style.width = `${width}px`;
|
|
316
|
+
this.canvas.style.height = `${height}px`;
|
|
317
|
+
this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
|
|
318
|
+
this.ctx.textBaseline = "alphabetic";
|
|
319
|
+
this.ctx.textAlign = "left";
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** Measure monospace cell size for a given font. Cheap and synchronous. */
|
|
323
|
+
export function measureCellSize(fontSize, fontFamily) {
|
|
324
|
+
const canvas = document.createElement("canvas");
|
|
325
|
+
const ctx = canvas.getContext("2d");
|
|
326
|
+
if (!ctx) {
|
|
327
|
+
return {
|
|
328
|
+
w: Math.ceil(fontSize * 0.6),
|
|
329
|
+
h: Math.ceil(fontSize * 1.2),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
333
|
+
const m = ctx.measureText("M");
|
|
334
|
+
const w = Math.ceil(m.width);
|
|
335
|
+
const ascent = m.actualBoundingBoxAscent || fontSize * 0.8;
|
|
336
|
+
const descent = m.actualBoundingBoxDescent || fontSize * 0.2;
|
|
337
|
+
return { w, h: Math.ceil(ascent + descent) + 2 };
|
|
338
|
+
}
|
|
339
|
+
const runtimeCache = new Map();
|
|
340
|
+
export function loadLibGhostty(wasmUrl = DEFAULT_WASM_URL) {
|
|
341
|
+
const key = wasmUrl.toString();
|
|
342
|
+
const cached = runtimeCache.get(key);
|
|
343
|
+
if (cached) {
|
|
344
|
+
return cached;
|
|
345
|
+
}
|
|
346
|
+
const runtimePromise = (async () => {
|
|
347
|
+
const response = await fetch(wasmUrl);
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
throw new Error(`Failed to load libghostty wasm: ${response.status} ${response.statusText}`);
|
|
350
|
+
}
|
|
351
|
+
const bytes = await response.arrayBuffer();
|
|
352
|
+
if (bytes.byteLength === 0) {
|
|
353
|
+
throw new Error("libghostty wasm is empty.");
|
|
354
|
+
}
|
|
355
|
+
const module = await WebAssembly.compile(bytes);
|
|
356
|
+
const instance = await WebAssembly.instantiate(module, {
|
|
357
|
+
env: {
|
|
358
|
+
log: () => { },
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
const wasm = instance.exports;
|
|
362
|
+
if (!wasm.memory ||
|
|
363
|
+
!wasm.ghostty_terminal_new ||
|
|
364
|
+
!wasm.ghostty_render_state_get_viewport) {
|
|
365
|
+
throw new Error("Invalid libghostty wasm exports.");
|
|
366
|
+
}
|
|
367
|
+
assertAbiCompatibility(wasm);
|
|
368
|
+
return new LibGhosttyRuntime(wasm, {
|
|
369
|
+
cellSize: EXPECTED_CELL_SIZE,
|
|
370
|
+
terminalConfigSize: EXPECTED_TERMINAL_CONFIG_SIZE,
|
|
371
|
+
});
|
|
372
|
+
})();
|
|
373
|
+
runtimeCache.set(key, runtimePromise);
|
|
374
|
+
return runtimePromise;
|
|
375
|
+
}
|
|
376
|
+
export async function createMiniTerminalSurface(options) {
|
|
377
|
+
const runtime = await loadLibGhostty(options.wasmUrl ?? DEFAULT_WASM_URL);
|
|
378
|
+
const theme = { ...DEFAULT_THEME, ...options.theme };
|
|
379
|
+
let cols;
|
|
380
|
+
let rows;
|
|
381
|
+
const cell = measureCellSize(options.fontSize, options.fontFamily);
|
|
382
|
+
if (options.widthPx != null && options.heightPx != null) {
|
|
383
|
+
cols = Math.max(1, Math.floor(options.widthPx / cell.w));
|
|
384
|
+
rows = Math.max(1, Math.floor(options.heightPx / cell.h));
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
cols = options.cols ?? 80;
|
|
388
|
+
rows = options.rows ?? 24;
|
|
389
|
+
}
|
|
390
|
+
options.container.innerHTML = "";
|
|
391
|
+
const canvas = document.createElement("canvas");
|
|
392
|
+
canvas.style.display = "block";
|
|
393
|
+
canvas.style.outline = "none";
|
|
394
|
+
canvas.tabIndex = options.interactive ? 0 : -1;
|
|
395
|
+
options.container.appendChild(canvas);
|
|
396
|
+
const terminal = runtime.createTerminal(cols, rows, theme);
|
|
397
|
+
const renderer = new MiniRenderer(canvas, cols, rows, options.fontSize, options.fontFamily, theme);
|
|
398
|
+
if (!options.showCursor) {
|
|
399
|
+
terminal.write("\x1b[?25l");
|
|
400
|
+
}
|
|
401
|
+
renderer.render(terminal.getViewportData());
|
|
402
|
+
const requestFrame = window.requestAnimationFrame?.bind(window) ?? ((cb) => window.setTimeout(cb, 16));
|
|
403
|
+
const cancelFrame = window.cancelAnimationFrame?.bind(window) ?? window.clearTimeout.bind(window);
|
|
404
|
+
let frameId = null;
|
|
405
|
+
let disposed = false;
|
|
406
|
+
const renderFrame = () => {
|
|
407
|
+
frameId = null;
|
|
408
|
+
if (disposed || !terminal.isDirty())
|
|
409
|
+
return;
|
|
410
|
+
renderer.render(terminal.getViewportData());
|
|
411
|
+
};
|
|
412
|
+
const scheduleRender = () => {
|
|
413
|
+
if (frameId !== null || disposed)
|
|
414
|
+
return;
|
|
415
|
+
frameId = requestFrame(renderFrame);
|
|
416
|
+
};
|
|
417
|
+
const detachInput = options.interactive
|
|
418
|
+
? attachBasicInput(canvas, (data) => options.onInput?.(data))
|
|
419
|
+
: () => { };
|
|
420
|
+
return {
|
|
421
|
+
cols,
|
|
422
|
+
rows,
|
|
423
|
+
cellSize: renderer.cellSize,
|
|
424
|
+
write(text) {
|
|
425
|
+
if (disposed)
|
|
426
|
+
return;
|
|
427
|
+
const normalized = options.convertEol ? normalizeEol(text) : text;
|
|
428
|
+
terminal.write(normalized);
|
|
429
|
+
scheduleRender();
|
|
430
|
+
},
|
|
431
|
+
drainResponses() {
|
|
432
|
+
if (disposed)
|
|
433
|
+
return [];
|
|
434
|
+
const responses = [];
|
|
435
|
+
while (terminal.hasResponse()) {
|
|
436
|
+
const response = terminal.readResponse();
|
|
437
|
+
if (!response)
|
|
438
|
+
break;
|
|
439
|
+
responses.push(response);
|
|
440
|
+
}
|
|
441
|
+
return responses;
|
|
442
|
+
},
|
|
443
|
+
dispose() {
|
|
444
|
+
disposed = true;
|
|
445
|
+
if (frameId !== null) {
|
|
446
|
+
cancelFrame(frameId);
|
|
447
|
+
frameId = null;
|
|
448
|
+
}
|
|
449
|
+
detachInput();
|
|
450
|
+
renderer.dispose();
|
|
451
|
+
terminal.dispose();
|
|
452
|
+
if (canvas.parentElement === options.container) {
|
|
453
|
+
options.container.removeChild(canvas);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function assertAbiCompatibility(wasm) {
|
|
459
|
+
const configPtr = wasm.ghostty_wasm_alloc_u8_array(EXPECTED_TERMINAL_CONFIG_SIZE);
|
|
460
|
+
if (!configPtr) {
|
|
461
|
+
throw new Error("Failed to allocate ABI probe config buffer.");
|
|
462
|
+
}
|
|
463
|
+
let handle = 0;
|
|
464
|
+
let writePtr = 0;
|
|
465
|
+
let writeLen = 0;
|
|
466
|
+
let viewportPtr = 0;
|
|
467
|
+
const probeFg = 0x112233;
|
|
468
|
+
const probeBg = 0x445566;
|
|
469
|
+
const [probeFgR, probeFgG, probeFgB] = unpackHexColor(probeFg);
|
|
470
|
+
const [probeBgR, probeBgG, probeBgB] = unpackHexColor(probeBg);
|
|
471
|
+
try {
|
|
472
|
+
new Uint8Array(wasm.memory.buffer, configPtr, EXPECTED_TERMINAL_CONFIG_SIZE).fill(0);
|
|
473
|
+
const cfg = new DataView(wasm.memory.buffer, configPtr, EXPECTED_TERMINAL_CONFIG_SIZE);
|
|
474
|
+
cfg.setUint32(0, 16, true);
|
|
475
|
+
cfg.setUint32(4, probeFg, true);
|
|
476
|
+
cfg.setUint32(8, probeBg, true);
|
|
477
|
+
cfg.setUint32(12, 0x778899, true);
|
|
478
|
+
handle = wasm.ghostty_terminal_new_with_config(2, 1, configPtr);
|
|
479
|
+
if (!handle) {
|
|
480
|
+
throw new Error("Failed to create ABI probe terminal.");
|
|
481
|
+
}
|
|
482
|
+
const probeWrite = new TextEncoder().encode("\x1b[7mX");
|
|
483
|
+
writeLen = probeWrite.length;
|
|
484
|
+
writePtr = wasm.ghostty_wasm_alloc_u8_array(writeLen);
|
|
485
|
+
if (!writePtr) {
|
|
486
|
+
throw new Error("Failed to allocate ABI probe write buffer.");
|
|
487
|
+
}
|
|
488
|
+
new Uint8Array(wasm.memory.buffer, writePtr, writeLen).set(probeWrite);
|
|
489
|
+
wasm.ghostty_terminal_write(handle, writePtr, writeLen);
|
|
490
|
+
viewportPtr = wasm.ghostty_wasm_alloc_u8_array(EXPECTED_CELL_SIZE);
|
|
491
|
+
if (!viewportPtr) {
|
|
492
|
+
throw new Error("Failed to allocate ABI probe viewport buffer.");
|
|
493
|
+
}
|
|
494
|
+
const written = wasm.ghostty_render_state_get_viewport(handle, viewportPtr, EXPECTED_CELL_SIZE);
|
|
495
|
+
if (written !== EXPECTED_CELL_SIZE) {
|
|
496
|
+
throw new Error(`Incompatible libghostty ABI: expected cell size ${EXPECTED_CELL_SIZE}, got ${written}.`);
|
|
497
|
+
}
|
|
498
|
+
const view = new DataView(wasm.memory.buffer, viewportPtr, EXPECTED_CELL_SIZE);
|
|
499
|
+
const flags = view.getUint8(10);
|
|
500
|
+
const hasFg = hasFlag(flags, FLAG_FG_EXPLICIT);
|
|
501
|
+
const hasBg = hasFlag(flags, FLAG_BG_EXPLICIT);
|
|
502
|
+
const fgMatches = view.getUint8(4) === probeFgR &&
|
|
503
|
+
view.getUint8(5) === probeFgG &&
|
|
504
|
+
view.getUint8(6) === probeFgB;
|
|
505
|
+
const bgMatches = view.getUint8(7) === probeBgR &&
|
|
506
|
+
view.getUint8(8) === probeBgG &&
|
|
507
|
+
view.getUint8(9) === probeBgB;
|
|
508
|
+
if (!hasFg || !hasBg || !fgMatches || !bgMatches) {
|
|
509
|
+
throw new Error("Incompatible libghostty ABI: terminal config layout mismatch.");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
if (viewportPtr) {
|
|
514
|
+
wasm.ghostty_wasm_free_u8_array(viewportPtr, EXPECTED_CELL_SIZE);
|
|
515
|
+
}
|
|
516
|
+
if (writePtr && writeLen > 0) {
|
|
517
|
+
wasm.ghostty_wasm_free_u8_array(writePtr, writeLen);
|
|
518
|
+
}
|
|
519
|
+
if (handle) {
|
|
520
|
+
wasm.ghostty_terminal_free(handle);
|
|
521
|
+
}
|
|
522
|
+
wasm.ghostty_wasm_free_u8_array(configPtr, EXPECTED_TERMINAL_CONFIG_SIZE);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function normalizeEol(text) {
|
|
526
|
+
return text.replace(/\r?\n/g, "\r\n");
|
|
527
|
+
}
|
|
528
|
+
function parseColorToHex(color) {
|
|
529
|
+
if (color.startsWith("#")) {
|
|
530
|
+
let hex = color.slice(1);
|
|
531
|
+
if (hex.length === 3) {
|
|
532
|
+
hex = `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}`;
|
|
533
|
+
}
|
|
534
|
+
const parsed = Number.parseInt(hex, 16);
|
|
535
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
536
|
+
}
|
|
537
|
+
const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
538
|
+
if (!rgbMatch)
|
|
539
|
+
return 0;
|
|
540
|
+
const r = Number.parseInt(rgbMatch[1], 10);
|
|
541
|
+
const g = Number.parseInt(rgbMatch[2], 10);
|
|
542
|
+
const b = Number.parseInt(rgbMatch[3], 10);
|
|
543
|
+
return (r << 16) | (g << 8) | b;
|
|
544
|
+
}
|
|
545
|
+
function unpackHexColor(hex) {
|
|
546
|
+
return [(hex >> 16) & 0xff, (hex >> 8) & 0xff, hex & 0xff];
|
|
547
|
+
}
|
|
548
|
+
function readRgb(view, offset) {
|
|
549
|
+
return {
|
|
550
|
+
r: view.getUint8(offset),
|
|
551
|
+
g: view.getUint8(offset + 1),
|
|
552
|
+
b: view.getUint8(offset + 2),
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function hasFlag(flags, bit) {
|
|
556
|
+
return (flags & bit) !== 0;
|
|
557
|
+
}
|
|
558
|
+
function rgbToCss(rgb) {
|
|
559
|
+
return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
|
|
560
|
+
}
|
|
561
|
+
function safeCodepoint(codepoint) {
|
|
562
|
+
try {
|
|
563
|
+
return String.fromCodePoint(codepoint);
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return " ";
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function attachBasicInput(target, onInput) {
|
|
570
|
+
const onMouseDown = () => target.focus();
|
|
571
|
+
const onPaste = (event) => {
|
|
572
|
+
const text = event.clipboardData?.getData("text");
|
|
573
|
+
if (!text)
|
|
574
|
+
return;
|
|
575
|
+
event.preventDefault();
|
|
576
|
+
onInput(text);
|
|
577
|
+
};
|
|
578
|
+
const onKeyDown = (event) => {
|
|
579
|
+
const encoded = encodeKeyboardEvent(event);
|
|
580
|
+
if (!encoded)
|
|
581
|
+
return;
|
|
582
|
+
event.preventDefault();
|
|
583
|
+
onInput(encoded);
|
|
584
|
+
};
|
|
585
|
+
target.addEventListener("mousedown", onMouseDown);
|
|
586
|
+
target.addEventListener("paste", onPaste);
|
|
587
|
+
target.addEventListener("keydown", onKeyDown);
|
|
588
|
+
return () => {
|
|
589
|
+
target.removeEventListener("mousedown", onMouseDown);
|
|
590
|
+
target.removeEventListener("paste", onPaste);
|
|
591
|
+
target.removeEventListener("keydown", onKeyDown);
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function encodeKeyboardEvent(event) {
|
|
595
|
+
if (event.isComposing || event.metaKey)
|
|
596
|
+
return null;
|
|
597
|
+
let value = null;
|
|
598
|
+
switch (event.key) {
|
|
599
|
+
case "Enter":
|
|
600
|
+
value = "\r";
|
|
601
|
+
break;
|
|
602
|
+
case "Backspace":
|
|
603
|
+
value = "\x7f";
|
|
604
|
+
break;
|
|
605
|
+
case "Tab":
|
|
606
|
+
value = event.shiftKey ? "\x1b[Z" : "\t";
|
|
607
|
+
break;
|
|
608
|
+
case "Escape":
|
|
609
|
+
value = "\x1b";
|
|
610
|
+
break;
|
|
611
|
+
case "ArrowUp":
|
|
612
|
+
value = "\x1b[A";
|
|
613
|
+
break;
|
|
614
|
+
case "ArrowDown":
|
|
615
|
+
value = "\x1b[B";
|
|
616
|
+
break;
|
|
617
|
+
case "ArrowRight":
|
|
618
|
+
value = "\x1b[C";
|
|
619
|
+
break;
|
|
620
|
+
case "ArrowLeft":
|
|
621
|
+
value = "\x1b[D";
|
|
622
|
+
break;
|
|
623
|
+
case "Home":
|
|
624
|
+
value = "\x1b[H";
|
|
625
|
+
break;
|
|
626
|
+
case "End":
|
|
627
|
+
value = "\x1b[F";
|
|
628
|
+
break;
|
|
629
|
+
case "Delete":
|
|
630
|
+
value = "\x1b[3~";
|
|
631
|
+
break;
|
|
632
|
+
case "PageUp":
|
|
633
|
+
value = "\x1b[5~";
|
|
634
|
+
break;
|
|
635
|
+
case "PageDown":
|
|
636
|
+
value = "\x1b[6~";
|
|
637
|
+
break;
|
|
638
|
+
default:
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
if (!value && event.ctrlKey) {
|
|
642
|
+
value = encodeCtrlKey(event.key);
|
|
643
|
+
}
|
|
644
|
+
if (!value && event.key.length === 1 && !event.ctrlKey) {
|
|
645
|
+
value = event.key;
|
|
646
|
+
}
|
|
647
|
+
if (!value)
|
|
648
|
+
return null;
|
|
649
|
+
if (event.altKey && !value.startsWith("\x1b")) {
|
|
650
|
+
return `\x1b${value}`;
|
|
651
|
+
}
|
|
652
|
+
return value;
|
|
653
|
+
}
|
|
654
|
+
function encodeCtrlKey(key) {
|
|
655
|
+
if (key.length !== 1)
|
|
656
|
+
return null;
|
|
657
|
+
const upper = key.toUpperCase();
|
|
658
|
+
if (upper >= "A" && upper <= "Z") {
|
|
659
|
+
return String.fromCharCode(upper.charCodeAt(0) - 64);
|
|
660
|
+
}
|
|
661
|
+
switch (key) {
|
|
662
|
+
case "@":
|
|
663
|
+
case " ":
|
|
664
|
+
return "\x00";
|
|
665
|
+
case "[":
|
|
666
|
+
return "\x1b";
|
|
667
|
+
case "\\":
|
|
668
|
+
return "\x1c";
|
|
669
|
+
case "]":
|
|
670
|
+
return "\x1d";
|
|
671
|
+
case "^":
|
|
672
|
+
return "\x1e";
|
|
673
|
+
case "_":
|
|
674
|
+
return "\x1f";
|
|
675
|
+
default:
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
}
|
package/dist/core/normalize.d.ts
CHANGED