@aurigax-ai/ghostty-web 0.1.0 → 0.3.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/lib/ghostty.ts CHANGED
@@ -6,7 +6,14 @@
6
6
  * grid references and key encoder through `Abi`.
7
7
  */
8
8
 
9
- import { Abi, GHOSTTY_SUCCESS, type VtExports } from './abi';
9
+ import {
10
+ Abi,
11
+ GHOSTTY_NO_VALUE,
12
+ GHOSTTY_OUT_OF_SPACE,
13
+ GHOSTTY_SUCCESS,
14
+ GhosttyCallError,
15
+ type VtExports,
16
+ } from './abi';
10
17
  import { EventEmitter } from './event-emitter';
11
18
  import type { IEvent } from './interfaces';
12
19
  import {
@@ -232,8 +239,136 @@ function rgbAt(view: DataView, ptr: number): RGB {
232
239
  return { r: view.getUint8(ptr), g: view.getUint8(ptr + 1), b: view.getUint8(ptr + 2) };
233
240
  }
234
241
 
235
- function bitsOf(raw: bigint, field: { lsb: number; width: number }): number {
236
- return Number((raw >> BigInt(field.lsb)) & ((1n << BigInt(field.width)) - 1n));
242
+ interface CellLayout {
243
+ styleSize: number;
244
+ fgColor: number;
245
+ bgColor: number;
246
+ bold: number;
247
+ italic: number;
248
+ faint: number;
249
+ blink: number;
250
+ inverse: number;
251
+ invisible: number;
252
+ strikethrough: number;
253
+ underline: number;
254
+ colorTag: number;
255
+ colorValue: number;
256
+ colorPalette: number;
257
+ colorRgb: number;
258
+ tagCodepoint: number;
259
+ tagGrapheme: number;
260
+ tagBgPalette: number;
261
+ tagBgRgb: number;
262
+ wideWide: number;
263
+ wideSpacerTail: number;
264
+ rowIteratorData: number;
265
+ rowDirtyData: number;
266
+ rowCellsData: number;
267
+ rowIteratorNext: (iterator: number) => number;
268
+ rowGet: (iterator: number, key: number, out: number) => number;
269
+ rowCellsNext: (cells: number) => number;
270
+ rowCellsGetMulti: (
271
+ cells: number,
272
+ count: number,
273
+ keys: number,
274
+ values: number,
275
+ written: number
276
+ ) => number;
277
+ }
278
+
279
+ const EXPECTED_CELL_BITS: Record<string, [number, number]> = {
280
+ content_tag: [0, 2],
281
+ content: [2, 24],
282
+ style_id: [26, 16],
283
+ wide: [42, 2],
284
+ hyperlink: [45, 1],
285
+ };
286
+
287
+ const cellLayouts = new WeakMap<Abi, CellLayout>();
288
+
289
+ function cellLayout(abi: Abi): CellLayout {
290
+ const cached = cellLayouts.get(abi);
291
+ if (cached) return cached;
292
+ for (const [field, [lsb, width]] of Object.entries(EXPECTED_CELL_BITS)) {
293
+ const bits = abi.bits('GhosttyCell', field);
294
+ if (bits.lsb !== lsb || bits.width !== width) {
295
+ throw new Error(`libghostty-vt GhosttyCell.${field} moved to ${bits.lsb}+${bits.width}`);
296
+ }
297
+ }
298
+ const style = (f: string) => abi.offset('GhosttyStyle', f);
299
+ const tags = 'GhosttyCellContentTag';
300
+ const fn = <T>(name: string) => abi.fn(name) as unknown as T;
301
+ const layout: CellLayout = {
302
+ styleSize: abi.sizeOf('GhosttyStyle'),
303
+ fgColor: style('fg_color'),
304
+ bgColor: style('bg_color'),
305
+ bold: style('bold'),
306
+ italic: style('italic'),
307
+ faint: style('faint'),
308
+ blink: style('blink'),
309
+ inverse: style('inverse'),
310
+ invisible: style('invisible'),
311
+ strikethrough: style('strikethrough'),
312
+ underline: style('underline'),
313
+ colorTag: abi.offset('GhosttyStyleColor', 'tag'),
314
+ colorValue: abi.offset('GhosttyStyleColor', 'value'),
315
+ colorPalette: abi.enumValue('GhosttyStyleColorTag', 'PALETTE'),
316
+ colorRgb: abi.enumValue('GhosttyStyleColorTag', 'RGB'),
317
+ tagCodepoint: abi.enumValue(tags, 'CODEPOINT'),
318
+ tagGrapheme: abi.enumValue(tags, 'CODEPOINT_GRAPHEME'),
319
+ tagBgPalette: abi.enumValue(tags, 'BG_COLOR_PALETTE'),
320
+ tagBgRgb: abi.enumValue(tags, 'BG_COLOR_RGB'),
321
+ wideWide: abi.enumValue('GhosttyCellWide', 'WIDE'),
322
+ wideSpacerTail: abi.enumValue('GhosttyCellWide', 'SPACER_TAIL'),
323
+ rowIteratorData: abi.enumValue('GhosttyRenderStateData', 'ROW_ITERATOR'),
324
+ rowDirtyData: abi.enumValue('GhosttyRenderStateRowData', 'DIRTY'),
325
+ rowCellsData: abi.enumValue('GhosttyRenderStateRowData', 'CELLS'),
326
+ rowIteratorNext: fn('ghostty_render_state_row_iterator_next'),
327
+ rowGet: fn('ghostty_render_state_row_get'),
328
+ rowCellsNext: fn('ghostty_render_state_row_cells_next'),
329
+ rowCellsGetMulti: fn('ghostty_render_state_row_cells_get_multi'),
330
+ };
331
+ cellLayouts.set(abi, layout);
332
+ return layout;
333
+ }
334
+
335
+ interface ExtractScratch {
336
+ base: number;
337
+ size: number;
338
+ slot: number;
339
+ flag: number;
340
+ keys: number;
341
+ values: number;
342
+ raw: number;
343
+ len: number;
344
+ style: number;
345
+ written: number;
346
+ }
347
+
348
+ function allocScratch(abi: Abi, layout: CellLayout): ExtractScratch {
349
+ const size = 8 + 8 + 12 + 12 + 8 + 8 + layout.styleSize + 8;
350
+ const base = abi.alloc(size);
351
+ const scratch: ExtractScratch = {
352
+ base,
353
+ size,
354
+ slot: base,
355
+ flag: base + 8,
356
+ keys: base + 16,
357
+ values: base + 28,
358
+ raw: base + 40,
359
+ len: base + 48,
360
+ style: base + 56,
361
+ written: base + 56 + layout.styleSize,
362
+ };
363
+ const view = abi.view();
364
+ const cells = 'GhosttyRenderStateRowCellsData';
365
+ view.setInt32(scratch.keys, abi.enumValue(cells, 'RAW'), true);
366
+ view.setInt32(scratch.keys + 4, abi.enumValue(cells, 'GRAPHEMES_LEN'), true);
367
+ view.setInt32(scratch.keys + 8, abi.enumValue(cells, 'STYLE'), true);
368
+ view.setUint32(scratch.values, scratch.raw, true);
369
+ view.setUint32(scratch.values + 4, scratch.len, true);
370
+ view.setUint32(scratch.values + 8, scratch.style, true);
371
+ return scratch;
237
372
  }
238
373
 
239
374
  /**
@@ -244,6 +379,176 @@ function bitsOf(raw: bigint, field: { lsb: number; width: number }): number {
244
379
  * Scrollback, graphemes, hyperlinks and wrap flags are read through grid
245
380
  * references on demand.
246
381
  */
382
+ /** A match in screen coordinates: columns, and rows counted from the top of the scrollback. */
383
+ export interface SearchMatch {
384
+ startX: number;
385
+ startY: number;
386
+ endX: number;
387
+ endY: number;
388
+ }
389
+
390
+ /**
391
+ * A search of one terminal's screen and scrollback with libghostty-vt's
392
+ * search: case-insensitive for ASCII letters, kept in sync with the terminal
393
+ * by `run()` after writes. It never scrolls; the caller shows the selected
394
+ * match. "Next" moves toward older output (up), as in Ghostty.
395
+ */
396
+ export class TerminalSearch {
397
+ private handle: number;
398
+
399
+ constructor(
400
+ private readonly abi: Abi,
401
+ private readonly terminal: number
402
+ ) {
403
+ this.handle = abi.newHandle('ghostty_search_new', (slot) =>
404
+ abi.call('ghostty_search_new', 0, slot, terminal)
405
+ );
406
+ abi.with(4, (ptr) => {
407
+ abi.view().setInt32(ptr, abi.enumValue('GhosttySearchScroll', 'NONE'), true);
408
+ abi.check('ghostty_search_set', this.handle, this.option('SELECT_SCROLL'), ptr);
409
+ });
410
+ }
411
+
412
+ private option(name: string): number {
413
+ return this.abi.enumValue('GhosttySearchOption', name);
414
+ }
415
+
416
+ private data(name: string): number {
417
+ return this.abi.enumValue('GhosttySearchData', name);
418
+ }
419
+
420
+ /** Starts a search for `text`, or stops searching when it is empty. */
421
+ setNeedle(text: string): void {
422
+ const abi = this.abi;
423
+ if (!text) {
424
+ abi.check('ghostty_search_set', this.handle, this.option('NEEDLE'), 0);
425
+ return;
426
+ }
427
+ const bytes = new TextEncoder().encode(text);
428
+ abi.withBytes(bytes, (ptr, len) =>
429
+ abi.with(abi.sizeOf('GhosttyString'), (str) => {
430
+ const view = abi.view();
431
+ view.setUint32(str + abi.offset('GhosttyString', 'ptr'), ptr, true);
432
+ view.setUint32(str + abi.offset('GhosttyString', 'len'), len, true);
433
+ abi.check('ghostty_search_set', this.handle, this.option('NEEDLE'), str);
434
+ })
435
+ );
436
+ }
437
+
438
+ /** Catches up with everything written to the terminal since the last run. */
439
+ run(): void {
440
+ this.abi.check('ghostty_search_run', this.handle);
441
+ }
442
+
443
+ /** Selects the next older (`older`) or newer match, wrapping; false when there are none. */
444
+ select(older: boolean): boolean {
445
+ const result = this.abi.call(
446
+ 'ghostty_search_set',
447
+ this.handle,
448
+ this.option(older ? 'SELECT_NEXT' : 'SELECT_PREV'),
449
+ 0
450
+ );
451
+ if (result === GHOSTTY_NO_VALUE) return false;
452
+ if (result !== GHOSTTY_SUCCESS) throw new GhosttyCallError('ghostty_search_set', result);
453
+ return true;
454
+ }
455
+
456
+ total(): number {
457
+ return this.readU32('TOTAL_MATCHES') ?? 0;
458
+ }
459
+
460
+ /** Index of the selected match, newest first (0 is the newest), or null. */
461
+ selectedIndex(): number | null {
462
+ return this.readU32('SELECTED_INDEX');
463
+ }
464
+
465
+ private readU32(name: string): number | null {
466
+ const abi = this.abi;
467
+ return abi.with(8, (ptr) => {
468
+ const result = abi.call('ghostty_search_get', this.handle, this.data(name), ptr);
469
+ if (result === GHOSTTY_NO_VALUE) return null;
470
+ if (result !== GHOSTTY_SUCCESS) throw new GhosttyCallError('ghostty_search_get', result);
471
+ return abi.view().getUint32(ptr, true);
472
+ });
473
+ }
474
+
475
+ selected(): SearchMatch | null {
476
+ const abi = this.abi;
477
+ return abi.withSized('GhosttySelection', (sel) => {
478
+ const result = abi.call('ghostty_search_get', this.handle, this.data('SELECTED_MATCH'), sel);
479
+ if (result === GHOSTTY_NO_VALUE) return null;
480
+ if (result !== GHOSTTY_SUCCESS) throw new GhosttyCallError('ghostty_search_get', result);
481
+ return this.toMatch(sel);
482
+ });
483
+ }
484
+
485
+ /** Every match, newest first, read right after `run()` and before the next write. */
486
+ matches(): SearchMatch[] {
487
+ const abi = this.abi;
488
+ const selSize = abi.sizeOf('GhosttySelection');
489
+ const bufSize = abi.sizeOf('GhosttySelectionBuffer');
490
+ const at = (buf: number, field: string) => buf + abi.offset('GhosttySelectionBuffer', field);
491
+ return abi.with(bufSize, (buf) => {
492
+ const query = abi.call('ghostty_search_get', this.handle, this.data('MATCHES'), buf);
493
+ if (query === GHOSTTY_SUCCESS) return [];
494
+ if (query !== GHOSTTY_OUT_OF_SPACE) {
495
+ if (query === GHOSTTY_NO_VALUE) return [];
496
+ throw new GhosttyCallError('ghostty_search_get', query);
497
+ }
498
+ const count = abi.view().getUint32(at(buf, 'len'), true);
499
+ return abi.with(count * selSize, (items) => {
500
+ for (let i = 0; i < count; i++) {
501
+ abi.view().setUint32(items + i * selSize, selSize, true);
502
+ }
503
+ const view = abi.view();
504
+ view.setUint32(at(buf, 'ptr'), items, true);
505
+ view.setUint32(at(buf, 'cap'), count, true);
506
+ abi.check('ghostty_search_get', this.handle, this.data('MATCHES'), buf);
507
+ const len = abi.view().getUint32(at(buf, 'len'), true);
508
+ const out: SearchMatch[] = [];
509
+ for (let i = 0; i < len; i++) {
510
+ const match = this.toMatch(items + i * selSize);
511
+ if (match) out.push(match);
512
+ }
513
+ return out;
514
+ });
515
+ });
516
+ }
517
+
518
+ private toMatch(sel: number): SearchMatch | null {
519
+ const abi = this.abi;
520
+ const start = this.screenPoint(sel + abi.offset('GhosttySelection', 'start'));
521
+ const end = this.screenPoint(sel + abi.offset('GhosttySelection', 'end'));
522
+ if (!start || !end) return null;
523
+ return { startX: start.x, startY: start.y, endX: end.x, endY: end.y };
524
+ }
525
+
526
+ private screenPoint(ref: number): { x: number; y: number } | null {
527
+ const abi = this.abi;
528
+ return abi.with(abi.sizeOf('GhosttyPointCoordinate'), (out) => {
529
+ const result = abi.call(
530
+ 'ghostty_terminal_point_from_grid_ref',
531
+ this.terminal,
532
+ ref,
533
+ abi.enumValue('GhosttyPointTag', 'SCREEN'),
534
+ out
535
+ );
536
+ if (result !== GHOSTTY_SUCCESS) return null;
537
+ const view = abi.view();
538
+ return {
539
+ x: view.getUint16(out + abi.offset('GhosttyPointCoordinate', 'x'), true),
540
+ y: view.getUint32(out + abi.offset('GhosttyPointCoordinate', 'y'), true),
541
+ };
542
+ });
543
+ }
544
+
545
+ free(): void {
546
+ if (!this.handle) return;
547
+ this.abi.call('ghostty_search_free', this.handle);
548
+ this.handle = 0;
549
+ }
550
+ }
551
+
247
552
  export class GhosttyTerminal {
248
553
  private handle: number;
249
554
  private renderState: number;
@@ -252,6 +557,8 @@ export class GhosttyTerminal {
252
557
  private _cols: number;
253
558
  private _rows: number;
254
559
 
560
+ private readonly layout: CellLayout;
561
+ private readonly scratch: ExtractScratch;
255
562
  private cellPool: GhosttyCell[] = [];
256
563
  private rowDirty: boolean[] = [];
257
564
  private dirty: DirtyState = DirtyState.FULL;
@@ -298,6 +605,8 @@ export class GhosttyTerminal {
298
605
  ) {
299
606
  this._cols = cols;
300
607
  this._rows = rows;
608
+ this.layout = cellLayout(abi);
609
+ this.scratch = allocScratch(abi, this.layout);
301
610
  this.handle = abi.newHandle('ghostty_terminal_new', (slot) =>
302
611
  abi.call('ghostty_terminal_new', 0, slot, cols, rows)
303
612
  );
@@ -317,7 +626,7 @@ export class GhosttyTerminal {
317
626
  this.setOption('WRITE_PTY', writePty);
318
627
  this.installHooks();
319
628
  this.setModeDefault(GRAPHEME_CLUSTER_MODE, true);
320
- if (config) this.applyConfig(config);
629
+ if (config) this.configure(config);
321
630
  this.initCellPool();
322
631
  }
323
632
 
@@ -328,6 +637,11 @@ export class GhosttyTerminal {
328
637
  return this._rows;
329
638
  }
330
639
 
640
+ /** A new search of this terminal; free it before or after the terminal. */
641
+ createSearch(): TerminalSearch {
642
+ return new TerminalSearch(this.abi, this.handle);
643
+ }
644
+
331
645
  /** The raw libghostty-vt terminal handle, for APIs this class does not wrap yet. */
332
646
  get rawHandle(): number {
333
647
  return this.handle;
@@ -366,6 +680,7 @@ export class GhosttyTerminal {
366
680
  abi.call('ghostty_render_state_row_cells_free', this.rowCells);
367
681
  abi.call('ghostty_render_state_row_iterator_free', this.rowIterator);
368
682
  abi.call('ghostty_render_state_free', this.renderState);
683
+ abi.free(this.scratch.base, this.scratch.size);
369
684
  abi.call('ghostty_terminal_free', this.handle);
370
685
  for (const index of this.callbacks) abi.removeCallback(index);
371
686
  this.callbacks = [];
@@ -678,7 +993,11 @@ export class GhosttyTerminal {
678
993
  // Private helpers
679
994
  // ==========================================================================
680
995
 
681
- private applyConfig(config: GhosttyTerminalConfig): void {
996
+ /**
997
+ * Applies scrollback and color settings. Colors take effect on the whole
998
+ * screen at the next update; programs can still change them (OSC 4/10/11).
999
+ */
1000
+ configure(config: GhosttyTerminalConfig): void {
682
1001
  const abi = this.abi;
683
1002
  if (config.scrollbackLimit !== undefined) {
684
1003
  abi.with(4, (ptr) => {
@@ -687,7 +1006,7 @@ export class GhosttyTerminal {
687
1006
  });
688
1007
  }
689
1008
  const setRgb = (option: string, color: number | undefined) => {
690
- if (!color) return;
1009
+ if (color === undefined) return;
691
1010
  abi.with(4, (ptr) => {
692
1011
  const view = abi.view();
693
1012
  view.setUint8(ptr, (color >> 16) & 0xff);
@@ -699,7 +1018,7 @@ export class GhosttyTerminal {
699
1018
  setRgb('COLOR_FOREGROUND', config.fgColor);
700
1019
  setRgb('COLOR_BACKGROUND', config.bgColor);
701
1020
  setRgb('COLOR_CURSOR', config.cursorColor);
702
- if (config.palette?.some((c) => c)) {
1021
+ if (config.palette?.some((c) => c !== undefined)) {
703
1022
  const size = 256 * 3;
704
1023
  abi.with(size, (ptr) => {
705
1024
  abi.check(
@@ -710,7 +1029,7 @@ export class GhosttyTerminal {
710
1029
  );
711
1030
  const view = abi.view();
712
1031
  config.palette!.slice(0, 16).forEach((color, i) => {
713
- if (!color) return;
1032
+ if (color === undefined) return;
714
1033
  view.setUint8(ptr + i * 3, (color >> 16) & 0xff);
715
1034
  view.setUint8(ptr + i * 3 + 1, (color >> 8) & 0xff);
716
1035
  view.setUint8(ptr + i * 3 + 2, color & 0xff);
@@ -718,6 +1037,8 @@ export class GhosttyTerminal {
718
1037
  this.setOption('COLOR_PALETTE', ptr);
719
1038
  });
720
1039
  }
1040
+ this.changed = true;
1041
+ this.dirty = DirtyState.FULL;
721
1042
  }
722
1043
 
723
1044
  private installHooks(): void {
@@ -895,117 +1216,150 @@ export class GhosttyTerminal {
895
1216
  palette,
896
1217
  };
897
1218
  });
1219
+ this.readTerminalColors();
898
1220
  }
899
1221
 
900
- private extractRows(all: boolean): void {
1222
+ /**
1223
+ * The render state does not pick up default colors set with
1224
+ * ghostty_terminal_set, so the effective colors (a program's OSC override,
1225
+ * else the default) are read from the terminal itself.
1226
+ */
1227
+ private readTerminalColors(): void {
901
1228
  const abi = this.abi;
902
- const rowData = 'GhosttyRenderStateRowData';
903
- abi.with(8, (slot) => {
904
- const view = abi.view();
905
- view.setUint32(slot, this.rowIterator, true);
906
- abi.check(
907
- 'ghostty_render_state_get',
908
- this.renderState,
909
- abi.enumValue('GhosttyRenderStateData', 'ROW_ITERATOR'),
910
- slot
1229
+ const data = (name: string) => abi.enumValue('GhosttyTerminalData', name);
1230
+ const rgb = (name: string): RGB | null =>
1231
+ abi.with(4, (ptr) =>
1232
+ abi.call('ghostty_terminal_get', this.handle, data(name), ptr) === GHOSTTY_SUCCESS
1233
+ ? rgbAt(abi.view(), ptr)
1234
+ : null
911
1235
  );
912
- abi.view().setUint32(slot, this.rowCells, true);
913
- let y = 0;
914
- while (
915
- y < this._rows &&
916
- abi.call('ghostty_render_state_row_iterator_next', this.rowIterator)
1236
+ this.colors.background = rgb('COLOR_BACKGROUND') ?? this.colors.background;
1237
+ this.colors.foreground = rgb('COLOR_FOREGROUND') ?? this.colors.foreground;
1238
+ this.colors.cursor = rgb('COLOR_CURSOR') ?? this.colors.cursor;
1239
+ abi.with(256 * 3, (ptr) => {
1240
+ if (
1241
+ abi.call('ghostty_terminal_get', this.handle, data('COLOR_PALETTE'), ptr) !==
1242
+ GHOSTTY_SUCCESS
917
1243
  ) {
918
- const rowDirty = abi.with(4, (out) => {
919
- abi.check(
920
- 'ghostty_render_state_row_get',
921
- this.rowIterator,
922
- abi.enumValue(rowData, 'DIRTY'),
923
- out
924
- );
925
- return abi.view().getUint8(out) !== 0;
926
- });
927
- if (all || rowDirty) {
928
- this.rowDirty[y] = true;
929
- abi.check(
930
- 'ghostty_render_state_row_get',
931
- this.rowIterator,
932
- abi.enumValue(rowData, 'CELLS'),
933
- slot
934
- );
935
- this.extractCells(y);
936
- }
937
- y++;
1244
+ return;
938
1245
  }
1246
+ const view = abi.view();
1247
+ for (let i = 0; i < 256; i++) this.colors.palette[i] = rgbAt(view, ptr + i * 3);
939
1248
  });
940
1249
  }
941
1250
 
1251
+ private extractRows(all: boolean): void {
1252
+ const abi = this.abi;
1253
+ const layout = this.layout;
1254
+ const rowGet = layout.rowGet;
1255
+ const iteratorNext = layout.rowIteratorNext;
1256
+ const scratch = this.scratch;
1257
+ const view = abi.view();
1258
+ view.setUint32(scratch.slot, this.rowIterator, true);
1259
+ abi.check('ghostty_render_state_get', this.renderState, layout.rowIteratorData, scratch.slot);
1260
+ let y = 0;
1261
+ while (y < this._rows && iteratorNext(this.rowIterator)) {
1262
+ const v = abi.view();
1263
+ v.setUint32(scratch.slot, this.rowCells, true);
1264
+ let rowDirty = all;
1265
+ if (!rowDirty) {
1266
+ rowGet(this.rowIterator, layout.rowDirtyData, scratch.flag);
1267
+ rowDirty = abi.view().getUint8(scratch.flag) !== 0;
1268
+ }
1269
+ if (rowDirty) {
1270
+ this.rowDirty[y] = true;
1271
+ const result = rowGet(this.rowIterator, layout.rowCellsData, scratch.slot);
1272
+ if (result !== GHOSTTY_SUCCESS)
1273
+ throw new GhosttyCallError('ghostty_render_state_row_get', result);
1274
+ this.extractCells(y);
1275
+ }
1276
+ y++;
1277
+ }
1278
+ }
1279
+
942
1280
  private extractCells(y: number): void {
943
1281
  const abi = this.abi;
944
- const cellsData = 'GhosttyRenderStateRowCellsData';
945
- const styleSize = abi.sizeOf('GhosttyStyle');
946
- const keyRaw = abi.enumValue(cellsData, 'RAW');
947
- const keyLen = abi.enumValue(cellsData, 'GRAPHEMES_LEN');
948
- const keyStyle = abi.enumValue(cellsData, 'STYLE');
949
- const block = 3 * 4 + 3 * 4 + 8 + 8 + styleSize + 8;
950
- abi.with(block, (base) => {
951
- const keys = base;
952
- const values = base + 12;
953
- const raw = base + 24;
954
- const len = base + 32;
955
- const style = base + 40;
956
- const written = style + styleSize;
957
- const view = abi.view();
958
- view.setInt32(keys, keyRaw, true);
959
- view.setInt32(keys + 4, keyLen, true);
960
- view.setInt32(keys + 8, keyStyle, true);
961
- view.setUint32(values, raw, true);
962
- view.setUint32(values + 4, len, true);
963
- view.setUint32(values + 8, style, true);
964
- let x = 0;
965
- let hyperlinkRun = 0;
966
- let previousLinked = false;
967
- while (x < this._cols && abi.call('ghostty_render_state_row_cells_next', this.rowCells)) {
968
- abi.view().setUint32(style, styleSize, true);
969
- abi.check(
970
- 'ghostty_render_state_row_cells_get_multi',
971
- this.rowCells,
972
- 3,
973
- keys,
974
- values,
975
- written
976
- );
977
- const v = abi.view();
978
- const cell = this.cellPool[y * this._cols + x];
979
- const graphemeLen = v.getUint32(len, true);
980
- this.fillCell(cell, v.getBigUint64(raw, true), style, graphemeLen);
981
- if (cell.hyperlink_id) {
982
- if (!previousLinked) hyperlinkRun++;
983
- cell.hyperlink_id = hyperlinkRun;
984
- }
985
- previousLinked = cell.hyperlink_id !== 0;
986
- x++;
1282
+ const layout = this.layout;
1283
+ const scratch = this.scratch;
1284
+ const cellsNext = layout.rowCellsNext;
1285
+ const getMulti = layout.rowCellsGetMulti;
1286
+ const pool = this.cellPool;
1287
+ const cols = this._cols;
1288
+ const rowStart = y * cols;
1289
+ let view = abi.view();
1290
+ let x = 0;
1291
+ let hyperlinkRun = 0;
1292
+ let previousLinked = false;
1293
+ while (x < cols && cellsNext(this.rowCells)) {
1294
+ if (view.buffer !== abi.exports.memory.buffer) view = abi.view();
1295
+ view.setUint32(scratch.style, layout.styleSize, true);
1296
+ const result = getMulti(this.rowCells, 3, scratch.keys, scratch.values, scratch.written);
1297
+ if (result !== GHOSTTY_SUCCESS) {
1298
+ throw new GhosttyCallError('ghostty_render_state_row_cells_get_multi', result);
987
1299
  }
988
- });
1300
+ if (view.buffer !== abi.exports.memory.buffer) view = abi.view();
1301
+ const cell = pool[rowStart + x];
1302
+ this.fillCellFrom(
1303
+ cell,
1304
+ view,
1305
+ view.getUint32(scratch.raw, true),
1306
+ view.getUint32(scratch.raw + 4, true),
1307
+ scratch.style,
1308
+ view.getUint32(scratch.len, true)
1309
+ );
1310
+ if (cell.hyperlink_id) {
1311
+ if (!previousLinked) hyperlinkRun++;
1312
+ cell.hyperlink_id = hyperlinkRun;
1313
+ }
1314
+ previousLinked = cell.hyperlink_id !== 0;
1315
+ x++;
1316
+ }
989
1317
  }
990
1318
 
991
1319
  private fillCell(cell: GhosttyCell, raw: bigint, stylePtr: number, graphemeLen: number): void {
992
- const abi = this.abi;
993
- const view = abi.view();
994
- const tag = bitsOf(raw, abi.bits('GhosttyCell', 'content_tag'));
995
- const content = abi.bits('GhosttyCell', 'content');
996
- const contentBits = bitsOf(raw, content);
997
- const tags = 'GhosttyCellContentTag';
998
- const isText =
999
- tag === abi.enumValue(tags, 'CODEPOINT') || tag === abi.enumValue(tags, 'CODEPOINT_GRAPHEME');
1000
- cell.codepoint = isText ? contentBits & 0x1fffff : 0;
1001
- cell.grapheme_len = Math.max(0, graphemeLen - 1);
1002
-
1003
- const at = (f: string) => stylePtr + abi.offset('GhosttyStyle', f);
1004
- const fg = this.styleColor(view, at('fg_color')) ?? this.colors.foreground;
1005
- let bg = this.styleColor(view, at('bg_color')) ?? this.colors.background;
1006
- if (tag === abi.enumValue(tags, 'BG_COLOR_PALETTE')) {
1320
+ this.fillCellFrom(
1321
+ cell,
1322
+ this.abi.view(),
1323
+ Number(raw & 0xffffffffn),
1324
+ Number(raw >> 32n),
1325
+ stylePtr,
1326
+ graphemeLen
1327
+ );
1328
+ }
1329
+
1330
+ private fillCellFrom(
1331
+ cell: GhosttyCell,
1332
+ view: DataView,
1333
+ lo: number,
1334
+ hi: number,
1335
+ stylePtr: number,
1336
+ graphemeLen: number
1337
+ ): void {
1338
+ const L = this.layout;
1339
+ const tag = lo & 0x3;
1340
+ const contentBits = (lo >>> 2) & 0xffffff;
1341
+ cell.codepoint = tag === L.tagCodepoint || tag === L.tagGrapheme ? contentBits & 0x1fffff : 0;
1342
+ cell.grapheme_len = graphemeLen > 1 ? graphemeLen - 1 : 0;
1343
+
1344
+ const styleId = ((lo >>> 26) | (hi << 6)) & 0xffff;
1345
+ let fg = this.colors.foreground;
1346
+ let bg = this.colors.background;
1347
+ let flags = 0;
1348
+ if (styleId !== 0) {
1349
+ fg = this.styleColor(view, stylePtr + L.fgColor) ?? fg;
1350
+ bg = this.styleColor(view, stylePtr + L.bgColor) ?? bg;
1351
+ if (view.getUint8(stylePtr + L.bold)) flags |= CellFlags.BOLD;
1352
+ if (view.getUint8(stylePtr + L.italic)) flags |= CellFlags.ITALIC;
1353
+ if (view.getInt32(stylePtr + L.underline, true) !== 0) flags |= CellFlags.UNDERLINE;
1354
+ if (view.getUint8(stylePtr + L.strikethrough)) flags |= CellFlags.STRIKETHROUGH;
1355
+ if (view.getUint8(stylePtr + L.inverse)) flags |= CellFlags.INVERSE;
1356
+ if (view.getUint8(stylePtr + L.invisible)) flags |= CellFlags.INVISIBLE;
1357
+ if (view.getUint8(stylePtr + L.blink)) flags |= CellFlags.BLINK;
1358
+ if (view.getUint8(stylePtr + L.faint)) flags |= CellFlags.FAINT;
1359
+ }
1360
+ if (tag === L.tagBgPalette) {
1007
1361
  bg = this.colors.palette[contentBits & 0xff] ?? bg;
1008
- } else if (tag === abi.enumValue(tags, 'BG_COLOR_RGB')) {
1362
+ } else if (tag === L.tagBgRgb) {
1009
1363
  bg = { r: contentBits & 0xff, g: (contentBits >> 8) & 0xff, b: (contentBits >> 16) & 0xff };
1010
1364
  }
1011
1365
  cell.fg_r = fg.r;
@@ -1014,37 +1368,19 @@ export class GhosttyTerminal {
1014
1368
  cell.bg_r = bg.r;
1015
1369
  cell.bg_g = bg.g;
1016
1370
  cell.bg_b = bg.b;
1017
-
1018
- let flags = 0;
1019
- if (view.getUint8(at('bold'))) flags |= CellFlags.BOLD;
1020
- if (view.getUint8(at('italic'))) flags |= CellFlags.ITALIC;
1021
- if (view.getInt32(at('underline'), true) !== 0) flags |= CellFlags.UNDERLINE;
1022
- if (view.getUint8(at('strikethrough'))) flags |= CellFlags.STRIKETHROUGH;
1023
- if (view.getUint8(at('inverse'))) flags |= CellFlags.INVERSE;
1024
- if (view.getUint8(at('invisible'))) flags |= CellFlags.INVISIBLE;
1025
- if (view.getUint8(at('blink'))) flags |= CellFlags.BLINK;
1026
- if (view.getUint8(at('faint'))) flags |= CellFlags.FAINT;
1027
1371
  cell.flags = flags;
1028
1372
 
1029
- const wide = bitsOf(raw, abi.bits('GhosttyCell', 'wide'));
1030
- const wides = 'GhosttyCellWide';
1031
- cell.width =
1032
- wide === abi.enumValue(wides, 'WIDE')
1033
- ? 2
1034
- : wide === abi.enumValue(wides, 'SPACER_TAIL')
1035
- ? 0
1036
- : 1;
1037
- cell.hyperlink_id = bitsOf(raw, abi.bits('GhosttyCell', 'hyperlink'));
1373
+ const wide = (hi >>> 10) & 0x3;
1374
+ cell.width = wide === L.wideWide ? 2 : wide === L.wideSpacerTail ? 0 : 1;
1375
+ cell.hyperlink_id = (hi >>> 13) & 0x1;
1038
1376
  }
1039
1377
 
1040
1378
  private styleColor(view: DataView, ptr: number): RGB | null {
1041
- const abi = this.abi;
1042
- const tag = view.getInt32(ptr + abi.offset('GhosttyStyleColor', 'tag'), true);
1043
- const value = ptr + abi.offset('GhosttyStyleColor', 'value');
1044
- if (tag === abi.enumValue('GhosttyStyleColorTag', 'PALETTE')) {
1045
- return this.colors.palette[view.getUint8(value)] ?? null;
1046
- }
1047
- if (tag === abi.enumValue('GhosttyStyleColorTag', 'RGB')) return rgbAt(view, value);
1379
+ const L = this.layout;
1380
+ const tag = view.getInt32(ptr + L.colorTag, true);
1381
+ if (tag === L.colorPalette)
1382
+ return this.colors.palette[view.getUint8(ptr + L.colorValue)] ?? null;
1383
+ if (tag === L.colorRgb) return rgbAt(view, ptr + L.colorValue);
1048
1384
  return null;
1049
1385
  }
1050
1386