1import {
2 type GhosttyKeyboardLayoutMap,
3 ghosttyConsumedMods,
4 ghosttyKeyForEvent,
5 ghosttyUnshiftedCodepoint,
6 loadGhosttyKeyboardLayoutMap,
7} from "./keyCodes";
8import { GhosttyRuntime, loadGhosttyRuntime } from "./runtime";
9
10const GHOSTTY_SUCCESS = 0;
11const GHOSTTY_OUT_OF_SPACE = -3;
12// The pinned ABI measures max_scrollback in bytes despite its header saying lines.
13// Match Ghostty's default scrollback-limit.
14const MAX_SCROLLBACK_BYTES = 10_000_000;
15// wasm32 GhosttyTerminalSelectionFormatOptions layout at the pinned libghostty-vt revision.
16const SELECTION_FORMAT_OPTIONS_SIZE = 16;
17const PALETTE_BYTES = 256 * 3;
18
19const RENDER_DATA = {
20 cols: 1,
21 rows: 2,
22 dirty: 3,
23 rowIterator: 4,
24 background: 5,
25 foreground: 6,
26 cursor: 7,
27 cursorHasValue: 8,
28 cursorStyle: 10,
29 cursorVisible: 11,
30 cursorBlinking: 12,
31 cursorInViewport: 14,
32 cursorX: 15,
33 cursorY: 16,
34} as const;
35
36const ROW_DATA = {
37 dirty: 1,
38 raw: 2,
39 cells: 3,
40} as const;
41
42const CELL_DATA = {
43 raw: 1,
44 style: 2,
45 graphemesLength: 3,
46 graphemes: 4,
47 background: 5,
48 foreground: 6,
49 selected: 7,
50} as const;
51
52// Optional row_cells_get_multi colors must come last; an unset color stops the query.
53const CELL_QUERY = {
54 keys: 0,
55 values: 20,
56 graphemesLength: 40,
57 selected: 44,
58 background: 48,
59 foreground: 52,
60 written: 56,
61 size: 60,
62} as const;
63
64const RAW_CELL_DATA = {
65 wide: 3,
66} as const;
67
68export const GHOSTTY_CELL_WIDE = {
69 narrow: 0,
70 wide: 1,
71 spacerTail: 2,
72 spacerHead: 3,
73} as const;
74
75export interface GhosttyColor {
76 readonly r: number;
77 readonly g: number;
78 readonly b: number;
79}
80
81export interface GhosttyTheme {
82 readonly foreground: GhosttyColor;
83 readonly background: GhosttyColor;
84 readonly cursor: GhosttyColor;
85 readonly palette?: readonly GhosttyColor[];
86 readonly selectionBackground?: string;
87}
88
89export interface GhosttyCell {
90 readonly text: string;
91 readonly wide: number;
92 readonly foreground: GhosttyColor;
93 readonly background: GhosttyColor;
94 readonly bold: boolean;
95 readonly italic: boolean;
96 readonly invisible: boolean;
97 readonly strikethrough: boolean;
98 readonly overline: boolean;
99 readonly underline: boolean;
100 readonly selected: boolean;
101}
102
103export interface GhosttyRow {
104 readonly cells: readonly GhosttyCell[];
105 readonly text: string;
106 readonly isWrapContinuation: boolean;
107 readonly wrapsToNext: boolean;
108}
109
110export interface GhosttySnapshot {
111 readonly cols: number;
112 readonly rows: number;
113 readonly foreground: GhosttyColor;
114 readonly background: GhosttyColor;
115 readonly cursor: GhosttyColor;
116 readonly cursorX: number;
117 readonly cursorY: number;
118 readonly cursorVisible: boolean;
119 readonly cursorBlinking: boolean;
120 readonly cursorStyle: number;
121 readonly dirtyRows: ReadonlySet<number>;
122 readonly rowData: readonly GhosttyRow[];
123}
124
125export interface GhosttySelectionRange {
126 readonly viewport: {
127 readonly start: { readonly x: number; readonly y: number };
128 readonly end: { readonly x: number; readonly y: number };
129 };
130 readonly screen: {
131 readonly start: { readonly x: number; readonly y: number };
132 readonly end: { readonly x: number; readonly y: number };
133 };
134}
135
136export interface GhosttyScrollbar {
137 readonly total: number;
138 readonly offset: number;
139 readonly len: number;
140}
141
142export interface GhosttyPointInput {
143 readonly x: number;
144 readonly y: number;
145 readonly tag?: 1 | 2;
146}
147
148export interface GhosttyMouseInput {
149 readonly action: "press" | "release" | "motion";
150 readonly button: number | null;
151 readonly mods: number;
152 readonly x: number;
153 readonly y: number;
154 readonly screenWidth: number;
155 readonly screenHeight: number;
156 readonly cellWidth: number;
157 readonly cellHeight: number;
158 readonly paddingLeft: number;
159 readonly paddingRight: number;
160 readonly paddingTop: number;
161 readonly paddingBottom: number;
162 readonly anyButtonPressed: boolean;
163}
164
165const decoder = new TextDecoder();
166const encoder = new TextEncoder();
167
168function blend(foreground: GhosttyColor, background: GhosttyColor): GhosttyColor {
169 const channel = (front: number, back: number) => Math.floor((front * 155 + back * 100) / 255);
170 return {
171 r: channel(foreground.r, background.r),
172 g: channel(foreground.g, background.g),
173 b: channel(foreground.b, background.b),
174 };
175}
176
177function sameColor(left: GhosttyColor, right: GhosttyColor): boolean {
178 return left.r === right.r && left.g === right.g && left.b === right.b;
179}
180
181/** Bounds String.fromCodePoint arguments for cells with enormous combining-mark sequences. */
182export function ghosttyCellText(
183 codepointView: DataView,
184 graphemeLength: number,
185 byteOffset = 0,
186): string {
187 if (graphemeLength === 1) return String.fromCodePoint(codepointView.getUint32(byteOffset, true));
188 const CHUNK_SIZE = 4_096;
189 let text = "";
190 for (let start = 0; start < graphemeLength; start += CHUNK_SIZE) {
191 const count = Math.min(CHUNK_SIZE, graphemeLength - start);
192 const codes = new Array<number>(count);
193 for (let index = 0; index < count; index += 1) {
194 codes[index] = codepointView.getUint32(byteOffset + (start + index) * 4, true);
195 }
196 text += String.fromCodePoint(...codes);
197 }
198 return text;
199}
200
201export class GhosttyTerminalCore {
202 private readonly runtime: GhosttyRuntime;
203 private terminalSlot = 0;
204 private terminal = 0;
205 private renderStateSlot = 0;
206 private renderState = 0;
207 private rowIteratorSlot = 0;
208 private rowCellsSlot = 0;
209 private keyEncoderSlot = 0;
210 private keyEncoder = 0;
211 private keyEventSlot = 0;
212 private keyEvent = 0;
213 private mouseEncoderSlot = 0;
214 private mouseEncoder = 0;
215 private mouseEventSlot = 0;
216 private mouseEvent = 0;
217 private ptyWriterId = 0;
218 private ptyWriter: ((data: string) => void) | null = null;
219 private scratch = 0;
220 private graphemes = 0;
221 private graphemeCapacity = 0;
222 private style = 0;
223 private cellQuery = 0;
224 private scrollbar = 0;
225 private rows: GhosttyRow[] = [];
226 private disposed = false;
227 private keyboardLayoutMap: GhosttyKeyboardLayoutMap | undefined;
228
229 private constructor(runtime: GhosttyRuntime) {
230 this.runtime = runtime;
231 void loadGhosttyKeyboardLayoutMap().then((layoutMap) => {
232 if (!this.disposed) this.keyboardLayoutMap = layoutMap;
233 });
234 }
235
236 static async create(
237 cols: number,
238 rows: number,
239 cellWidth: number,
240 cellHeight: number,
241 theme: GhosttyTheme,
242 onPtyData: (data: string) => void,
243 ): Promise<GhosttyTerminalCore> {
244 const core = new GhosttyTerminalCore(await loadGhosttyRuntime());
245 try {
246 core.initialize(cols, rows, cellWidth, cellHeight, theme, onPtyData);
247 return core;
248 } catch (error) {
249 core.dispose();
250 throw error;
251 }
252 }
253
254 private initialize(
255 cols: number,
256 rows: number,
257 cellWidth: number,
258 cellHeight: number,
259 theme: GhosttyTheme,
260 onPtyData: (data: string) => void,
261 ): void {
262 const optionsSize = this.runtime.layout("GhosttyTerminalOptions").size;
263 const options = this.runtime.alloc(optionsSize);
264 this.runtime.setField(options, "GhosttyTerminalOptions", "cols", cols);
265 this.runtime.setField(options, "GhosttyTerminalOptions", "rows", rows);
266 this.runtime.setField(
267 options,
268 "GhosttyTerminalOptions",
269 "max_scrollback",
270 MAX_SCROLLBACK_BYTES,
271 );
272 this.terminalSlot = this.runtime.allocOpaque();
273 const terminalResult = this.runtime.call("ghostty_terminal_new", 0, this.terminalSlot, options);
274 this.runtime.free(options, optionsSize);
275 this.assertSuccess("ghostty_terminal_new", terminalResult);
276 this.terminal = this.runtime.readPointer(this.terminalSlot);
277 this.applyDefaultCursorBlink();
278 this.ptyWriter = onPtyData;
279 this.ptyWriterId = this.runtime.attachPtyWriter(this.terminal, onPtyData);
280
281 this.renderStateSlot = this.runtime.allocOpaque();
282 this.assertSuccess(
283 "ghostty_render_state_new",
284 this.runtime.call("ghostty_render_state_new", 0, this.renderStateSlot),
285 );
286 this.renderState = this.runtime.readPointer(this.renderStateSlot);
287
288 this.rowIteratorSlot = this.runtime.allocOpaque();
289 this.assertSuccess(
290 "ghostty_render_state_row_iterator_new",
291 this.runtime.call("ghostty_render_state_row_iterator_new", 0, this.rowIteratorSlot),
292 );
293 this.rowCellsSlot = this.runtime.allocOpaque();
294 this.assertSuccess(
295 "ghostty_render_state_row_cells_new",
296 this.runtime.call("ghostty_render_state_row_cells_new", 0, this.rowCellsSlot),
297 );
298
299 this.keyEncoderSlot = this.runtime.allocOpaque();
300 this.assertSuccess(
301 "ghostty_key_encoder_new",
302 this.runtime.call("ghostty_key_encoder_new", 0, this.keyEncoderSlot),
303 );
304 this.keyEncoder = this.runtime.readPointer(this.keyEncoderSlot);
305 this.keyEventSlot = this.runtime.allocOpaque();
306 this.assertSuccess(
307 "ghostty_key_event_new",
308 this.runtime.call("ghostty_key_event_new", 0, this.keyEventSlot),
309 );
310 this.keyEvent = this.runtime.readPointer(this.keyEventSlot);
311
312 this.mouseEncoderSlot = this.runtime.allocOpaque();
313 this.assertSuccess(
314 "ghostty_mouse_encoder_new",
315 this.runtime.call("ghostty_mouse_encoder_new", 0, this.mouseEncoderSlot),
316 );
317 this.mouseEncoder = this.runtime.readPointer(this.mouseEncoderSlot);
318 this.mouseEventSlot = this.runtime.allocOpaque();
319 this.assertSuccess(
320 "ghostty_mouse_event_new",
321 this.runtime.call("ghostty_mouse_event_new", 0, this.mouseEventSlot),
322 );
323 this.mouseEvent = this.runtime.readPointer(this.mouseEventSlot);
324
325 this.scratch = this.runtime.alloc(16);
326 const styleSize = this.runtime.layout("GhosttyStyle").size;
327 this.style = this.runtime.alloc(styleSize);
328 this.runtime.setField(this.style, "GhosttyStyle", "size", styleSize);
329 this.cellQuery = this.runtime.alloc(CELL_QUERY.size);
330 const queryView = this.runtime.memoryDataView();
331 const cellFields = [
332 [CELL_DATA.style, this.style],
333 [CELL_DATA.graphemesLength, this.cellQuery + CELL_QUERY.graphemesLength],
334 [CELL_DATA.selected, this.cellQuery + CELL_QUERY.selected],
335 [CELL_DATA.background, this.cellQuery + CELL_QUERY.background],
336 [CELL_DATA.foreground, this.cellQuery + CELL_QUERY.foreground],
337 ] as const;
338 for (const [index, [key, pointer]] of cellFields.entries()) {
339 queryView.setUint32(this.cellQuery + CELL_QUERY.keys + index * 4, key, true);
340 queryView.setUint32(this.cellQuery + CELL_QUERY.values + index * 4, pointer, true);
341 }
342 this.scrollbar = this.runtime.alloc(this.runtime.layout("GhosttyTerminalScrollbar").size);
343 this.setTheme(theme);
344 this.resize(cols, rows, cellWidth, cellHeight);
345 }
346
347 write(data: string | Uint8Array, onSynchronizedOutputEnd?: () => void): void {
348 this.ensureActive();
349 const bytes = typeof data === "string" ? encoder.encode(data) : data;
350 if (bytes.length === 0) return;
351 const pointer = this.runtime.alloc(bytes.length);
352 this.runtime.bytes(pointer, bytes.length).set(bytes);
353 // An existing render deadline belongs to one group. Observe Ghostty before
354 // the next escape can reopen it; Ghostty still parses OSC/DCS and split VT
355 // sequences itself. Once that group ends, the remaining bytes can be batched.
356 let synchronized = onSynchronizedOutputEnd !== undefined && this.isSynchronizedOutput();
357 try {
358 for (let start = 0; start < bytes.length;) {
359 const nextEscape = synchronized ? bytes.indexOf(0x1b, start + 1) : -1;
360 const end = nextEscape === -1 ? bytes.length : nextEscape;
361 this.runtime.call("ghostty_terminal_vt_write", this.terminal, pointer + start, end - start);
362 start = end;
363 if (synchronized && !this.isSynchronizedOutput()) {
364 synchronized = false;
365 onSynchronizedOutputEnd?.();
366 }
367 }
368 } finally {
369 this.runtime.free(pointer, bytes.length);
370 }
371 }
372
373 resetAndWrite(data: string): void {
374 this.ensureActive();
375 this.runtime.call("ghostty_terminal_reset", this.terminal);
376 // RIS restores the built-in steady cursor; reapply the embedder default before replay.
377 this.applyDefaultCursorBlink();
378 this.rows = [];
379 if (data.length === 0) return;
380 const writer = this.ptyWriter;
381 if (this.ptyWriterId !== 0) {
382 this.runtime.detachPtyWriter(this.terminal, this.ptyWriterId);
383 this.ptyWriterId = 0;
384 }
385 try {
386 this.write(data);
387 } finally {
388 if (writer !== null && !this.disposed) {
389 this.ptyWriterId = this.runtime.attachPtyWriter(this.terminal, writer);
390 }
391 }
392 }
393
394 resize(cols: number, rows: number, cellWidth: number, cellHeight: number): void {
395 this.ensureActive();
396 this.assertSuccess(
397 "ghostty_terminal_resize",
398 this.runtime.call(
399 "ghostty_terminal_resize",
400 this.terminal,
401 Math.max(1, Math.min(65_535, cols)),
402 Math.max(1, Math.min(65_535, rows)),
403 Math.max(1, Math.round(cellWidth)),
404 Math.max(1, Math.round(cellHeight)),
405 ),
406 );
407 }
408
409 // Option 23 sets blink on startup and DECSCUSR reset; explicit DECSCUSR or DEC mode 12 still wins.
410 private applyDefaultCursorBlink(): void {
411 const blink = this.runtime.alloc(1);
412 this.runtime.bytes(blink, 1)[0] = 1;
413 this.runtime.call("ghostty_terminal_set", this.terminal, 23, blink);
414 this.runtime.free(blink, 1);
415 }
416
417 setTheme(theme: GhosttyTheme): void {
418 this.ensureActive();
419 const color = this.runtime.alloc(3);
420 for (const [option, value] of [
421 [11, theme.foreground],
422 [12, theme.background],
423 [13, theme.cursor],
424 ] as const) {
425 this.runtime.bytes(color, 3).set([value.r, value.g, value.b]);
426 this.runtime.call("ghostty_terminal_set", this.terminal, option, color);
427 }
428 this.runtime.free(color, 3);
429 if (theme.palette) this.setPalette(theme.palette);
430 }
431
432 private setPalette(colors: readonly GhosttyColor[]): void {
433 const palette = this.runtime.alloc(PALETTE_BYTES);
434 this.runtime.call("ghostty_terminal_get", this.terminal, 25, palette);
435 const bytes = this.runtime.bytes(palette, PALETTE_BYTES);
436 colors.slice(0, 16).forEach((color, index) => {
437 bytes.set([color.r, color.g, color.b], index * 3);
438 });
439 this.runtime.call("ghostty_terminal_set", this.terminal, 14, palette);
440 this.runtime.free(palette, PALETTE_BYTES);
441 }
442
443 scroll(deltaRows: number): void {
444 this.ensureActive();
445 const layout = this.runtime.layout("GhosttyTerminalScrollViewport");
446 const scroll = this.runtime.alloc(layout.size);
447 this.runtime.setField(scroll, "GhosttyTerminalScrollViewport", "tag", 2);
448 const value = layout.fields.value!;
449 this.runtime.view(scroll + value.offset, value.size).setInt32(0, deltaRows, true);
450 this.runtime.call("ghostty_terminal_scroll_viewport", this.terminal, scroll);
451 this.runtime.free(scroll, layout.size);
452 }
453
454 scrollToBottom(): void {
455 this.ensureActive();
456 const layout = this.runtime.layout("GhosttyTerminalScrollViewport");
457 const scroll = this.runtime.alloc(layout.size);
458 this.runtime.setField(scroll, "GhosttyTerminalScrollViewport", "tag", 1);
459 this.runtime.call("ghostty_terminal_scroll_viewport", this.terminal, scroll);
460 this.runtime.free(scroll, layout.size);
461 }
462
463 isViewportActive(): boolean {
464 this.ensureActive();
465 this.runtime.bytes(this.scratch, 1)[0] = 0;
466 return (
467 this.runtime.call("ghostty_terminal_get", this.terminal, 32, this.scratch) ===
468 GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0
469 );
470 }
471
472 scrollbarState(): GhosttyScrollbar | null {
473 this.ensureActive();
474 const layout = this.runtime.layout("GhosttyTerminalScrollbar");
475 this.runtime.bytes(this.scrollbar, layout.size).fill(0);
476 if (
477 this.runtime.call("ghostty_terminal_get", this.terminal, 9, this.scrollbar) !==
478 GHOSTTY_SUCCESS
479 ) {
480 return null;
481 }
482 return {
483 total: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "total"),
484 offset: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "offset"),
485 len: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "len"),
486 };
487 }
488
489 isMouseTracking(): boolean {
490 this.ensureActive();
491 this.runtime.bytes(this.scratch, 1)[0] = 0;
492 return (
493 this.runtime.call("ghostty_terminal_get", this.terminal, 11, this.scratch) ===
494 GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0
495 );
496 }
497
498 isMouseAnyEventTracking(): boolean {
499 this.ensureActive();
500 this.runtime.bytes(this.scratch, 1)[0] = 0;
501 return (
502 this.runtime.call("ghostty_terminal_mode_get", this.terminal, 1003, this.scratch) ===
503 GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0
504 );
505 }
506
507 isSynchronizedOutput(): boolean {
508 this.ensureActive();
509 this.runtime.bytes(this.scratch, 1)[0] = 0;
510 return (
511 this.runtime.call("ghostty_terminal_mode_get", this.terminal, 2026, this.scratch) ===
512 GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0
513 );
514 }
515
516 endSynchronizedOutput(): void {
517 this.ensureActive();
518 this.assertSuccess(
519 "ghostty_terminal_mode_set",
520 this.runtime.call("ghostty_terminal_mode_set", this.terminal, 2026, 0),
521 );
522 }
523
524 isAlternateScreen(): boolean {
525 this.ensureActive();
526 this.runtime.bytes(this.scratch, 4).fill(0);
527 return (
528 this.runtime.call("ghostty_terminal_get", this.terminal, 6, this.scratch) ===
529 GHOSTTY_SUCCESS && this.runtime.view(this.scratch, 4).getUint32(0, true) === 1
530 );
531 }
532
533 isApplicationCursorKeys(): boolean {
534 this.ensureActive();
535 this.runtime.bytes(this.scratch, 1)[0] = 0;
536 return (
537 this.runtime.call("ghostty_terminal_mode_get", this.terminal, 1, this.scratch) ===
538 GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0
539 );
540 }
541
542 encodeKey(event: KeyboardEvent, action: "press" | "release" = "press"): string {
543 this.ensureActive();
544 this.runtime.call("ghostty_key_encoder_setopt_from_terminal", this.keyEncoder, this.terminal);
545 this.runtime.call(
546 "ghostty_key_event_set_action",
547 this.keyEvent,
548 action === "release" ? 0 : event.repeat ? 2 : 1,
549 );
550 this.runtime.call("ghostty_key_event_set_key", this.keyEvent, ghosttyKeyForEvent(event));
551 const mods =
552 (event.shiftKey ? 1 : 0) |
553 (event.ctrlKey ? 1 << 1 : 0) |
554 (event.altKey ? 1 << 2 : 0) |
555 (event.metaKey ? 1 << 3 : 0) |
556 (event.getModifierState("CapsLock") ? 1 << 4 : 0) |
557 (event.getModifierState("NumLock") ? 1 << 5 : 0);
558 this.runtime.call("ghostty_key_event_set_mods", this.keyEvent, mods);
559 this.runtime.call(
560 "ghostty_key_event_set_consumed_mods",
561 this.keyEvent,
562 ghosttyConsumedMods(event),
563 );
564 this.runtime.call("ghostty_key_event_set_composing", this.keyEvent, event.isComposing ? 1 : 0);
565 this.runtime.call(
566 "ghostty_key_event_set_unshifted_codepoint",
567 this.keyEvent,
568 ghosttyUnshiftedCodepoint(event, this.keyboardLayoutMap),
569 );
570
571 const text = event.key.length === 1 ? event.key : "";
572 const textBytes = encoder.encode(text);
573 const textPointer = textBytes.length === 0 ? 0 : this.runtime.alloc(textBytes.length);
574 if (textPointer !== 0) this.runtime.bytes(textPointer, textBytes.length).set(textBytes);
575 this.runtime.call("ghostty_key_event_set_utf8", this.keyEvent, textPointer, textBytes.length);
576
577 const written = this.runtime.call("ghostty_wasm_alloc_usize");
578 const encoded = this.encodeOutput(written, (output, outputSize) =>
579 this.runtime.call(
580 "ghostty_key_encoder_encode",
581 this.keyEncoder,
582 this.keyEvent,
583 output,
584 outputSize,
585 written,
586 ),
587 );
588 this.runtime.call("ghostty_wasm_free_usize", written);
589 if (textPointer !== 0) this.runtime.free(textPointer, textBytes.length);
590 return encoded;
591 }
592
593 encodePaste(data: string): string {
594 this.ensureActive();
595 const input = encoder.encode(data);
596 if (input.length === 0) return "";
597 const inputPointer = this.runtime.alloc(input.length);
598 this.runtime.bytes(inputPointer, input.length).set(input);
599 this.runtime.bytes(this.scratch, 1)[0] = 0;
600 const bracketed =
601 this.runtime.call("ghostty_terminal_mode_get", this.terminal, 2004, this.scratch) ===
602 GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0;
603 const written = this.runtime.call("ghostty_wasm_alloc_usize");
604 let encoded = "";
605 const sizeResult = this.runtime.call(
606 "ghostty_paste_encode",
607 inputPointer,
608 input.length,
609 bracketed ? 1 : 0,
610 0,
611 0,
612 written,
613 );
614 const outputSize = this.runtime.view(written, 4).getUint32(0, true);
615 if (sizeResult === GHOSTTY_OUT_OF_SPACE && outputSize > 0) {
616 const output = this.runtime.alloc(outputSize);
617 const result = this.runtime.call(
618 "ghostty_paste_encode",
619 inputPointer,
620 input.length,
621 bracketed ? 1 : 0,
622 output,
623 outputSize,
624 written,
625 );
626 const outputLength = this.runtime.view(written, 4).getUint32(0, true);
627 encoded =
628 result === GHOSTTY_SUCCESS ? decoder.decode(this.runtime.bytes(output, outputLength)) : "";
629 this.runtime.free(output, outputSize);
630 }
631 this.runtime.call("ghostty_wasm_free_usize", written);
632 this.runtime.free(inputPointer, input.length);
633 return encoded;
634 }
635
636 encodeMouse(input: GhosttyMouseInput): string {
637 this.ensureActive();
638 this.runtime.call(
639 "ghostty_mouse_encoder_setopt_from_terminal",
640 this.mouseEncoder,
641 this.terminal,
642 );
643
644 const sizeLayout = this.runtime.layout("GhosttyMouseEncoderSize");
645 const size = this.runtime.alloc(sizeLayout.size);
646 for (const [field, value] of [
647 ["size", sizeLayout.size],
648 ["screen_width", input.screenWidth],
649 ["screen_height", input.screenHeight],
650 ["cell_width", input.cellWidth],
651 ["cell_height", input.cellHeight],
652 ["padding_top", input.paddingTop],
653 ["padding_bottom", input.paddingBottom],
654 ["padding_right", input.paddingRight],
655 ["padding_left", input.paddingLeft],
656 ] as const) {
657 this.runtime.setField(size, "GhosttyMouseEncoderSize", field, Math.max(0, Math.round(value)));
658 }
659 this.runtime.call("ghostty_mouse_encoder_setopt", this.mouseEncoder, 2, size);
660 this.runtime.free(size, sizeLayout.size);
661
662 this.runtime.bytes(this.scratch, 1)[0] = input.anyButtonPressed ? 1 : 0;
663 this.runtime.call("ghostty_mouse_encoder_setopt", this.mouseEncoder, 3, this.scratch);
664 this.runtime.bytes(this.scratch, 1)[0] = 1;
665 this.runtime.call("ghostty_mouse_encoder_setopt", this.mouseEncoder, 4, this.scratch);
666
667 this.runtime.call(
668 "ghostty_mouse_event_set_action",
669 this.mouseEvent,
670 input.action === "press" ? 0 : input.action === "release" ? 1 : 2,
671 );
672 if (input.button === null) {
673 this.runtime.call("ghostty_mouse_event_clear_button", this.mouseEvent);
674 } else {
675 this.runtime.call("ghostty_mouse_event_set_button", this.mouseEvent, input.button);
676 }
677 this.runtime.call("ghostty_mouse_event_set_mods", this.mouseEvent, input.mods);
678 const positionLayout = this.runtime.layout("GhosttyMousePosition");
679 const position = this.runtime.alloc(positionLayout.size);
680 const positionView = this.runtime.view(position, positionLayout.size);
681 positionView.setFloat32(positionLayout.fields.x!.offset, input.x, true);
682 positionView.setFloat32(positionLayout.fields.y!.offset, input.y, true);
683 this.runtime.call("ghostty_mouse_event_set_position", this.mouseEvent, position);
684 this.runtime.free(position, positionLayout.size);
685
686 const written = this.runtime.call("ghostty_wasm_alloc_usize");
687 const encoded = this.encodeOutput(written, (output, outputSize) =>
688 this.runtime.call(
689 "ghostty_mouse_encoder_encode",
690 this.mouseEncoder,
691 this.mouseEvent,
692 output,
693 outputSize,
694 written,
695 ),
696 );
697 this.runtime.call("ghostty_wasm_free_usize", written);
698 return encoded;
699 }
700
701 setSelection(anchor: GhosttyPointInput, end: GhosttyPointInput): void {
702 this.ensureActive();
703 const selectionLayout = this.runtime.layout("GhosttySelection");
704 const gridRefSize = this.runtime.layout("GhosttyGridRef").size;
705 const selection = this.runtime.alloc(selectionLayout.size);
706 let start = 0;
707 let endRef = 0;
708 try {
709 this.runtime.setField(selection, "GhosttySelection", "size", selectionLayout.size);
710 start = this.gridRef(anchor.x, anchor.y, anchor.tag ?? 1);
711 endRef = this.gridRef(end.x, end.y, end.tag ?? 1);
712 const startField = selectionLayout.fields.start!;
713 const endField = selectionLayout.fields.end!;
714 this.runtime
715 .bytes(selection + startField.offset, startField.size)
716 .set(this.runtime.bytes(start, startField.size));
717 this.runtime
718 .bytes(selection + endField.offset, endField.size)
719 .set(this.runtime.bytes(endRef, endField.size));
720 this.runtime.call("ghostty_terminal_set", this.terminal, 21, selection);
721 } finally {
722 this.runtime.free(start, gridRefSize);
723 this.runtime.free(endRef, gridRefSize);
724 this.runtime.free(selection, selectionLayout.size);
725 }
726 }
727
728 selectAll(): GhosttySelectionRange["screen"] | null {
729 this.ensureActive();
730 const layout = this.runtime.layout("GhosttySelection");
731 const selection = this.runtime.alloc(layout.size);
732 try {
733 this.runtime.setField(selection, "GhosttySelection", "size", layout.size);
734 if (
735 this.runtime.call("ghostty_terminal_select_all", this.terminal, selection) !==
736 GHOSTTY_SUCCESS
737 ) {
738 return null;
739 }
740 const start = this.pointFromGridRef(selection + layout.fields.start!.offset, 2);
741 const end = this.pointFromGridRef(selection + layout.fields.end!.offset, 2);
742 if (!start || !end) return null;
743 this.runtime.call("ghostty_terminal_set", this.terminal, 21, selection);
744 return { start, end };
745 } finally {
746 this.runtime.free(selection, layout.size);
747 }
748 }
749
750 selectionRange(): GhosttySelectionRange["screen"] | null {
751 this.ensureActive();
752 const layout = this.runtime.layout("GhosttySelection");
753 const selection = this.runtime.alloc(layout.size);
754 try {
755 this.runtime.setField(selection, "GhosttySelection", "size", layout.size);
756 if (
757 this.runtime.call("ghostty_terminal_get", this.terminal, 31, selection) !== GHOSTTY_SUCCESS
758 ) {
759 return null;
760 }
761 const start = this.pointFromGridRef(selection + layout.fields.start!.offset, 2);
762 const end = this.pointFromGridRef(selection + layout.fields.end!.offset, 2);
763 return start && end ? { start, end } : null;
764 } finally {
765 this.runtime.free(selection, layout.size);
766 }
767 }
768
769 selectWord(col: number, row: number): GhosttySelectionRange | null {
770 return this.selectAt(
771 "GhosttyTerminalSelectWordOptions",
772 "ghostty_terminal_select_word",
773 col,
774 row,
775 );
776 }
777
778 selectLine(col: number, row: number): GhosttySelectionRange | null {
779 return this.selectAt(
780 "GhosttyTerminalSelectLineOptions",
781 "ghostty_terminal_select_line",
782 col,
783 row,
784 );
785 }
786
787 hyperlinkAt(col: number, row: number): string | null {
788 this.ensureActive();
789 const ref = this.gridRef(col, row);
790 const written = this.runtime.call("ghostty_wasm_alloc_usize");
791 const sizeResult = this.runtime.call("ghostty_grid_ref_hyperlink_uri", ref, 0, 0, written);
792 const outputSize = this.runtime.view(written, 4).getUint32(0, true);
793 let hyperlink: string | null = null;
794 if (sizeResult === GHOSTTY_OUT_OF_SPACE && outputSize > 0) {
795 const output = this.runtime.alloc(outputSize);
796 const result = this.runtime.call(
797 "ghostty_grid_ref_hyperlink_uri",
798 ref,
799 output,
800 outputSize,
801 written,
802 );
803 const outputLength = this.runtime.view(written, 4).getUint32(0, true);
804 if (result === GHOSTTY_SUCCESS && outputLength > 0) {
805 hyperlink = decoder.decode(this.runtime.bytes(output, outputLength));
806 }
807 this.runtime.free(output, outputSize);
808 }
809 this.runtime.call("ghostty_wasm_free_usize", written);
810 this.runtime.free(ref, this.runtime.layout("GhosttyGridRef").size);
811 return hyperlink;
812 }
813
814 clearSelection(): void {
815 this.ensureActive();
816 this.runtime.call("ghostty_terminal_set", this.terminal, 21, 0);
817 }
818
819 snapshot(): GhosttySnapshot {
820 this.ensureActive();
821 this.assertSuccess(
822 "ghostty_render_state_update",
823 this.runtime.call("ghostty_render_state_update", this.renderState, this.terminal),
824 );
825 const cols = this.getU16(RENDER_DATA.cols);
826 const rowCount = this.getU16(RENDER_DATA.rows);
827 const dirty = this.getU32(RENDER_DATA.dirty);
828 const foreground = this.getColor(RENDER_DATA.foreground, { r: 229, g: 231, b: 235 });
829 const background = this.getColor(RENDER_DATA.background, { r: 0, g: 0, b: 0 });
830 const cursorHasValue = this.getBool(RENDER_DATA.cursorHasValue);
831 const cursor = cursorHasValue ? this.getColor(RENDER_DATA.cursor, foreground) : foreground;
832 const cursorInViewport = this.getBool(RENDER_DATA.cursorInViewport);
833 const cursorVisible = this.getBool(RENDER_DATA.cursorVisible) && cursorInViewport;
834 const cursorX = cursorInViewport ? this.getU16(RENDER_DATA.cursorX) : -1;
835 const cursorY = cursorInViewport ? this.getU16(RENDER_DATA.cursorY) : -1;
836
837 if (this.rows.length !== rowCount || this.rows.some((row) => row.cells.length !== cols)) {
838 this.rows = Array.from({ length: rowCount }, () => ({
839 cells: Array.from({ length: cols }, () => this.emptyCell(foreground, background)),
840 text: "",
841 isWrapContinuation: false,
842 wrapsToNext: false,
843 }));
844 }
845
846 const dirtyRows = new Set<number>();
847 if (dirty !== 0) {
848 this.assertSuccess(
849 "ghostty_render_state_get(row iterator)",
850 this.runtime.call(
851 "ghostty_render_state_get",
852 this.renderState,
853 RENDER_DATA.rowIterator,
854 this.rowIteratorSlot,
855 ),
856 );
857 const iterator = this.runtime.readPointer(this.rowIteratorSlot);
858 let rowIndex = 0;
859 while (
860 rowIndex < rowCount &&
861 this.runtime.call("ghostty_render_state_row_iterator_next", iterator) !== 0
862 ) {
863 const rowDirty = dirty === 2 || this.getRowBool(iterator, ROW_DATA.dirty);
864 if (rowDirty) {
865 this.rows[rowIndex] = this.readRow(iterator, cols, foreground, background);
866 dirtyRows.add(rowIndex);
867 this.runtime.memoryBytes()[this.scratch] = 0;
868 this.runtime.call("ghostty_render_state_row_set", iterator, 0, this.scratch);
869 }
870 rowIndex += 1;
871 }
872 this.runtime.memoryDataView().setUint32(this.scratch, 0, true);
873 this.runtime.call("ghostty_render_state_set", this.renderState, 0, this.scratch);
874 }
875
876 return {
877 cols,
878 rows: rowCount,
879 foreground,
880 background,
881 cursor,
882 cursorX,
883 cursorY,
884 cursorVisible,
885 cursorBlinking: this.getBool(RENDER_DATA.cursorBlinking),
886 cursorStyle: this.getU32(RENDER_DATA.cursorStyle),
887 dirtyRows,
888 rowData: this.rows,
889 };
890 }
891
892 selectionText(): string {
893 this.ensureActive();
894 const options = this.runtime.alloc(SELECTION_FORMAT_OPTIONS_SIZE);
895 const optionsView = this.runtime.view(options, SELECTION_FORMAT_OPTIONS_SIZE);
896 optionsView.setUint32(0, SELECTION_FORMAT_OPTIONS_SIZE, true);
897 optionsView.setUint32(4, 0, true);
898 optionsView.setUint8(8, 1);
899 optionsView.setUint8(9, 1);
900 optionsView.setUint32(12, 0, true);
901 const written = this.runtime.call("ghostty_wasm_alloc_usize");
902 const sizeResult = this.runtime.call(
903 "ghostty_terminal_selection_format_buf",
904 this.terminal,
905 options,
906 0,
907 0,
908 written,
909 );
910 const outputSize = this.runtime.view(written, 4).getUint32(0, true);
911 let text = "";
912 if (sizeResult === GHOSTTY_OUT_OF_SPACE && outputSize > 0) {
913 const output = this.runtime.alloc(outputSize);
914 const result = this.runtime.call(
915 "ghostty_terminal_selection_format_buf",
916 this.terminal,
917 options,
918 output,
919 outputSize,
920 written,
921 );
922 const outputLength = this.runtime.view(written, 4).getUint32(0, true);
923 if (result === GHOSTTY_SUCCESS) {
924 text = decoder.decode(this.runtime.bytes(output, outputLength));
925 }
926 this.runtime.free(output, outputSize);
927 }
928 this.runtime.call("ghostty_wasm_free_usize", written);
929 this.runtime.free(options, SELECTION_FORMAT_OPTIONS_SIZE);
930 return text;
931 }
932
933 viewportPointToScreen(col: number, row: number): { x: number; y: number } | null {
934 return this.convertPoint(col, row, 1, 2);
935 }
936
937 screenPointToViewport(col: number, row: number): { x: number; y: number } | null {
938 return this.convertPoint(col, row, 2, 1);
939 }
940
941 private convertPoint(
942 col: number,
943 row: number,
944 fromTag: 1 | 2,
945 toTag: 1 | 2,
946 ): { x: number; y: number } | null {
947 this.ensureActive();
948 const ref = this.gridRef(col, row, fromTag);
949 const point = this.pointFromGridRef(ref, toTag);
950 this.runtime.free(ref, this.runtime.layout("GhosttyGridRef").size);
951 return point;
952 }
953
954 dispose(): void {
955 if (this.disposed) return;
956 this.disposed = true;
957 if (this.mouseEvent) this.runtime.call("ghostty_mouse_event_free", this.mouseEvent);
958 if (this.mouseEncoder) this.runtime.call("ghostty_mouse_encoder_free", this.mouseEncoder);
959 if (this.keyEvent) this.runtime.call("ghostty_key_event_free", this.keyEvent);
960 if (this.keyEncoder) this.runtime.call("ghostty_key_encoder_free", this.keyEncoder);
961 if (this.rowCellsSlot) {
962 const cells = this.runtime.readPointer(this.rowCellsSlot);
963 if (cells) this.runtime.call("ghostty_render_state_row_cells_free", cells);
964 }
965 if (this.rowIteratorSlot) {
966 const iterator = this.runtime.readPointer(this.rowIteratorSlot);
967 if (iterator) this.runtime.call("ghostty_render_state_row_iterator_free", iterator);
968 }
969 if (this.renderState) this.runtime.call("ghostty_render_state_free", this.renderState);
970 if (this.terminal) {
971 if (this.ptyWriterId) this.runtime.detachPtyWriter(this.terminal, this.ptyWriterId);
972 this.runtime.call("ghostty_terminal_free", this.terminal);
973 }
974 if (this.style) this.runtime.free(this.style, this.runtime.layout("GhosttyStyle").size);
975 if (this.cellQuery) this.runtime.free(this.cellQuery, CELL_QUERY.size);
976 if (this.scrollbar) {
977 this.runtime.free(this.scrollbar, this.runtime.layout("GhosttyTerminalScrollbar").size);
978 }
979 if (this.scratch) this.runtime.free(this.scratch, 16);
980 if (this.graphemes) this.runtime.free(this.graphemes, this.graphemeCapacity);
981 for (const slot of [
982 this.mouseEventSlot,
983 this.mouseEncoderSlot,
984 this.keyEventSlot,
985 this.keyEncoderSlot,
986 this.rowCellsSlot,
987 this.rowIteratorSlot,
988 this.renderStateSlot,
989 this.terminalSlot,
990 ]) {
991 this.runtime.freeOpaque(slot);
992 }
993 }
994
995 private encodeOutput(
996 written: number,
997 encode: (output: number, outputSize: number) => number,
998 ): string {
999 const sizeResult = encode(0, 0);
1000 const outputSize = this.runtime.view(written, 4).getUint32(0, true);
1001 if (sizeResult === GHOSTTY_SUCCESS && outputSize === 0) return "";
1002 if (sizeResult !== GHOSTTY_OUT_OF_SPACE || outputSize === 0) return "";
1003
1004 const output = this.runtime.alloc(outputSize);
1005 const result = encode(output, outputSize);
1006 const outputLength = this.runtime.view(written, 4).getUint32(0, true);
1007 const encoded =
1008 result === GHOSTTY_SUCCESS ? decoder.decode(this.runtime.bytes(output, outputLength)) : "";
1009 this.runtime.free(output, outputSize);
1010 return encoded;
1011 }
1012
1013 private readRow(
1014 iterator: number,
1015 cols: number,
1016 defaultForeground: GhosttyColor,
1017 defaultBackground: GhosttyColor,
1018 ): GhosttyRow {
1019 this.assertSuccess(
1020 "ghostty_render_state_row_get(raw)",
1021 this.runtime.call("ghostty_render_state_row_get", iterator, ROW_DATA.raw, this.scratch),
1022 );
1023 const rawRow = this.runtime.memoryDataView().getBigUint64(this.scratch, true);
1024 this.runtime.memoryBytes()[this.scratch + 8] = 0;
1025 this.assertSuccess(
1026 "ghostty_row_get(wrap continuation)",
1027 this.runtime.call("ghostty_row_get", rawRow, 2, this.scratch + 8),
1028 );
1029 const isWrapContinuation = this.runtime.memoryBytes()[this.scratch + 8] !== 0;
1030 this.runtime.memoryBytes()[this.scratch + 8] = 0;
1031 this.assertSuccess(
1032 "ghostty_row_get(wrap)",
1033 this.runtime.call("ghostty_row_get", rawRow, 1, this.scratch + 8),
1034 );
1035 const wrapsToNext = this.runtime.memoryBytes()[this.scratch + 8] !== 0;
1036
1037 this.assertSuccess(
1038 "ghostty_render_state_row_get(cells)",
1039 this.runtime.call(
1040 "ghostty_render_state_row_get",
1041 iterator,
1042 ROW_DATA.cells,
1043 this.rowCellsSlot,
1044 ),
1045 );
1046 const cellsIterator = this.runtime.readPointer(this.rowCellsSlot);
1047 const { size: styleSize, fields: styleFields } = this.runtime.layout("GhosttyStyle");
1048 const cellsNext = this.runtime.fn("ghostty_render_state_row_cells_next");
1049 const cellsGetMulti = this.runtime.fn("ghostty_render_state_row_cells_get_multi");
1050 const cells: GhosttyCell[] = [];
1051 while (cells.length < cols && cellsNext(cellsIterator) !== 0) {
1052 this.runtime.memoryBytes().fill(0, this.style, this.style + styleSize);
1053 this.runtime.setField(this.style, "GhosttyStyle", "size", styleSize);
1054 const result = cellsGetMulti(
1055 cellsIterator,
1056 5,
1057 this.cellQuery + CELL_QUERY.keys,
1058 this.cellQuery + CELL_QUERY.values,
1059 this.cellQuery + CELL_QUERY.written,
1060 );
1061 const queryView = this.runtime.memoryDataView();
1062 const written = queryView.getUint32(this.cellQuery + CELL_QUERY.written, true);
1063 if (written < 3) this.assertSuccess("ghostty_render_state_row_cells_get_multi", result);
1064 const graphemeLength = queryView.getUint32(this.cellQuery + CELL_QUERY.graphemesLength, true);
1065 const selected = queryView.getUint8(this.cellQuery + CELL_QUERY.selected) !== 0;
1066 let background =
1067 written >= 4 ? this.readColor(this.cellQuery + CELL_QUERY.background) : defaultBackground;
1068 let foreground =
1069 written === 5
1070 ? this.readColor(this.cellQuery + CELL_QUERY.foreground)
1071 : written === 3
1072 ? this.getCellColor(cellsIterator, CELL_DATA.foreground, defaultForeground)
1073 : defaultForeground;
1074 let text = "";
1075 if (graphemeLength > 0) {
1076 const bufferSize = graphemeLength * 4;
1077 if (bufferSize > this.graphemeCapacity) {
1078 const capacity = Math.max(bufferSize, this.graphemeCapacity * 2);
1079 const buffer = this.runtime.alloc(capacity);
1080 this.runtime.free(this.graphemes, this.graphemeCapacity);
1081 this.graphemes = buffer;
1082 this.graphemeCapacity = capacity;
1083 }
1084 if (
1085 this.runtime.call(
1086 "ghostty_render_state_row_cells_get",
1087 cellsIterator,
1088 CELL_DATA.graphemes,
1089 this.graphemes,
1090 ) === GHOSTTY_SUCCESS
1091 ) {
1092 // Byte allocations need not be 4-byte aligned; use DataView instead of Uint32Array.
1093 text = ghosttyCellText(this.runtime.memoryDataView(), graphemeLength, this.graphemes);
1094 }
1095 }
1096 let wide = 0;
1097 // A wrap spacer in the final column can follow a spacer tail.
1098 if (text.length === 0 && (cells.at(-1)?.text.length || cells.length === cols - 1)) {
1099 this.assertSuccess(
1100 "ghostty_render_state_row_cells_get(raw)",
1101 this.runtime.call(
1102 "ghostty_render_state_row_cells_get",
1103 cellsIterator,
1104 CELL_DATA.raw,
1105 this.scratch,
1106 ),
1107 );
1108 const rawCell = this.runtime.memoryDataView().getBigUint64(this.scratch, true);
1109 this.runtime.memoryDataView().setUint32(this.scratch + 8, 0, true);
1110 this.assertSuccess(
1111 "ghostty_cell_get(wide)",
1112 this.runtime.call("ghostty_cell_get", rawCell, RAW_CELL_DATA.wide, this.scratch + 8),
1113 );
1114 wide = this.runtime.memoryDataView().getUint32(this.scratch + 8, true);
1115 }
1116 // Read the style after allocation and ABI calls, which can grow WASM memory.
1117 const styleView = this.runtime.memoryDataView();
1118 if (styleView.getUint8(this.style + styleFields.inverse!.offset) !== 0) {
1119 [foreground, background] = [background, foreground];
1120 }
1121 if (styleView.getUint8(this.style + styleFields.faint!.offset) !== 0) {
1122 foreground = blend(foreground, background);
1123 }
1124 cells.push({
1125 text,
1126 wide,
1127 foreground,
1128 background,
1129 bold: styleView.getUint8(this.style + styleFields.bold!.offset) !== 0,
1130 italic: styleView.getUint8(this.style + styleFields.italic!.offset) !== 0,
1131 invisible: styleView.getUint8(this.style + styleFields.invisible!.offset) !== 0,
1132 strikethrough: styleView.getUint8(this.style + styleFields.strikethrough!.offset) !== 0,
1133 overline: styleView.getUint8(this.style + styleFields.overline!.offset) !== 0,
1134 underline: styleView.getInt32(this.style + styleFields.underline!.offset, true) !== 0,
1135 selected,
1136 });
1137 }
1138 while (cells.length < cols) cells.push(this.emptyCell(defaultForeground, defaultBackground));
1139 return {
1140 cells,
1141 text: cells
1142 .map((cell) => cell.text || " ")
1143 .join("")
1144 .trimEnd(),
1145 isWrapContinuation,
1146 wrapsToNext,
1147 };
1148 }
1149
1150 private gridRef(col: number, row: number, tag: 1 | 2 = 1): number {
1151 const pointLayout = this.runtime.layout("GhosttyPoint");
1152 const point = this.runtime.alloc(pointLayout.size);
1153 this.runtime.setField(point, "GhosttyPoint", "tag", tag);
1154 const pointValue = pointLayout.fields.value!;
1155 const valueOffset = pointValue.offset;
1156 const view = this.runtime.view(point + valueOffset, pointValue.size);
1157 view.setUint16(0, Math.max(0, col), true);
1158 view.setUint32(4, Math.max(0, row), true);
1159 const gridRefSize = this.runtime.layout("GhosttyGridRef").size;
1160 const gridRef = this.runtime.alloc(gridRefSize);
1161 this.runtime.setField(gridRef, "GhosttyGridRef", "size", gridRefSize);
1162 const result = this.runtime.call("ghostty_terminal_grid_ref", this.terminal, point, gridRef);
1163 this.runtime.free(point, pointLayout.size);
1164 if (result !== GHOSTTY_SUCCESS) {
1165 this.runtime.free(gridRef, gridRefSize);
1166 this.assertSuccess("ghostty_terminal_grid_ref", result);
1167 }
1168 return gridRef;
1169 }
1170
1171 private selectAt(
1172 optionsName: "GhosttyTerminalSelectWordOptions" | "GhosttyTerminalSelectLineOptions",
1173 operation: "ghostty_terminal_select_word" | "ghostty_terminal_select_line",
1174 col: number,
1175 row: number,
1176 ): GhosttySelectionRange | null {
1177 this.ensureActive();
1178 const optionsLayout = this.runtime.layout(optionsName);
1179 const selectionLayout = this.runtime.layout("GhosttySelection");
1180 const options = this.runtime.alloc(optionsLayout.size);
1181 let ref = 0;
1182 let selection = 0;
1183 let range: GhosttySelectionRange | null = null;
1184 try {
1185 this.runtime.setField(options, optionsName, "size", optionsLayout.size);
1186 ref = this.gridRef(col, row);
1187 const refField = optionsLayout.fields.ref!;
1188 this.runtime
1189 .bytes(options + refField.offset, refField.size)
1190 .set(this.runtime.bytes(ref, refField.size));
1191 selection = this.runtime.alloc(selectionLayout.size);
1192 this.runtime.setField(selection, "GhosttySelection", "size", selectionLayout.size);
1193 const result = this.runtime.call(operation, this.terminal, options, selection);
1194 if (result === GHOSTTY_SUCCESS) {
1195 const start = selection + selectionLayout.fields.start!.offset;
1196 const end = selection + selectionLayout.fields.end!.offset;
1197 const viewportStart = this.pointFromGridRef(start, 1);
1198 const viewportEnd = this.pointFromGridRef(end, 1);
1199 const screenStart = this.pointFromGridRef(start, 2);
1200 const screenEnd = this.pointFromGridRef(end, 2);
1201 if (viewportStart && viewportEnd && screenStart && screenEnd) {
1202 range = {
1203 viewport: { start: viewportStart, end: viewportEnd },
1204 screen: { start: screenStart, end: screenEnd },
1205 };
1206 }
1207 this.runtime.call("ghostty_terminal_set", this.terminal, 21, selection);
1208 }
1209 } finally {
1210 this.runtime.free(selection, selectionLayout.size);
1211 this.runtime.free(ref, this.runtime.layout("GhosttyGridRef").size);
1212 this.runtime.free(options, optionsLayout.size);
1213 }
1214 return range;
1215 }
1216
1217 private pointFromGridRef(ref: number, tag: 1 | 2): { x: number; y: number } | null {
1218 const coordinateLayout = this.runtime.layout("GhosttyPointCoordinate");
1219 const coordinate = this.runtime.alloc(coordinateLayout.size);
1220 const result = this.runtime.call(
1221 "ghostty_terminal_point_from_grid_ref",
1222 this.terminal,
1223 ref,
1224 tag,
1225 coordinate,
1226 );
1227 const point =
1228 result === GHOSTTY_SUCCESS
1229 ? {
1230 x: this.runtime.readField(coordinate, "GhosttyPointCoordinate", "x"),
1231 y: this.runtime.readField(coordinate, "GhosttyPointCoordinate", "y"),
1232 }
1233 : null;
1234 this.runtime.free(coordinate, coordinateLayout.size);
1235 return point;
1236 }
1237
1238 private getU16(data: number): number {
1239 this.runtime.memoryDataView().setUint16(this.scratch, 0, true);
1240 this.assertSuccess(
1241 "ghostty_render_state_get",
1242 this.runtime.call("ghostty_render_state_get", this.renderState, data, this.scratch),
1243 );
1244 return this.runtime.memoryDataView().getUint16(this.scratch, true);
1245 }
1246
1247 private getU32(data: number): number {
1248 this.runtime.memoryDataView().setUint32(this.scratch, 0, true);
1249 this.assertSuccess(
1250 "ghostty_render_state_get",
1251 this.runtime.call("ghostty_render_state_get", this.renderState, data, this.scratch),
1252 );
1253 return this.runtime.memoryDataView().getUint32(this.scratch, true);
1254 }
1255
1256 private getBool(data: number): boolean {
1257 this.runtime.memoryBytes()[this.scratch] = 0;
1258 this.assertSuccess(
1259 "ghostty_render_state_get",
1260 this.runtime.call("ghostty_render_state_get", this.renderState, data, this.scratch),
1261 );
1262 return this.runtime.memoryBytes()[this.scratch] !== 0;
1263 }
1264
1265 private getColor(data: number, fallback: GhosttyColor): GhosttyColor {
1266 this.runtime.memoryBytes().fill(0, this.scratch, this.scratch + 3);
1267 const result = this.runtime.call(
1268 "ghostty_render_state_get",
1269 this.renderState,
1270 data,
1271 this.scratch,
1272 );
1273 return result === GHOSTTY_SUCCESS ? this.readColor(this.scratch) : fallback;
1274 }
1275
1276 private getRowBool(iterator: number, data: number): boolean {
1277 this.runtime.memoryBytes()[this.scratch] = 0;
1278 return (
1279 this.runtime.call("ghostty_render_state_row_get", iterator, data, this.scratch) ===
1280 GHOSTTY_SUCCESS && this.runtime.memoryBytes()[this.scratch] !== 0
1281 );
1282 }
1283
1284 private getCellColor(iterator: number, data: number, fallback: GhosttyColor): GhosttyColor {
1285 this.runtime.memoryBytes().fill(0, this.scratch, this.scratch + 3);
1286 const result = this.runtime.call(
1287 "ghostty_render_state_row_cells_get",
1288 iterator,
1289 data,
1290 this.scratch,
1291 );
1292 return result === GHOSTTY_SUCCESS ? this.readColor(this.scratch) : fallback;
1293 }
1294
1295 private readColor(pointer: number): GhosttyColor {
1296 const view = this.runtime.memoryDataView();
1297 return {
1298 r: view.getUint8(pointer),
1299 g: view.getUint8(pointer + 1),
1300 b: view.getUint8(pointer + 2),
1301 };
1302 }
1303
1304 private emptyCell(foreground: GhosttyColor, background: GhosttyColor): GhosttyCell {
1305 return {
1306 text: "",
1307 wide: 0,
1308 foreground,
1309 background,
1310 bold: false,
1311 italic: false,
1312 invisible: false,
1313 strikethrough: false,
1314 overline: false,
1315 underline: false,
1316 selected: false,
1317 };
1318 }
1319
1320 private assertSuccess(operation: string, result: number): void {
1321 if (result !== GHOSTTY_SUCCESS) throw new Error(`${operation} failed with result ${result}`);
1322 }
1323
1324 private ensureActive(): void {
1325 if (this.disposed) throw new Error("libghostty-vt terminal has been disposed");
1326 }
1327}
1328
1329export function ghosttyColorsEqual(left: GhosttyColor, right: GhosttyColor): boolean {
1330 return sameColor(left, right);
1331}