@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.
@@ -13,7 +13,7 @@
13
13
  import { EventEmitter } from './event-emitter';
14
14
  import type { GhosttyTerminal } from './ghostty';
15
15
  import type { IEvent } from './interfaces';
16
- import type { CanvasRenderer } from './renderer';
16
+ import type { TerminalRenderer } from './renderer';
17
17
  import type { Terminal } from './terminal';
18
18
  import type { GhosttyCell } from './types';
19
19
 
@@ -34,7 +34,7 @@ export interface SelectionCoordinates {
34
34
 
35
35
  export class SelectionManager {
36
36
  private terminal: Terminal;
37
- private renderer: CanvasRenderer;
37
+ private renderer: TerminalRenderer;
38
38
  private wasmTerm: GhosttyTerminal;
39
39
  private textarea: HTMLTextAreaElement;
40
40
 
@@ -101,7 +101,7 @@ export class SelectionManager {
101
101
 
102
102
  constructor(
103
103
  terminal: Terminal,
104
- renderer: CanvasRenderer,
104
+ renderer: TerminalRenderer,
105
105
  wasmTerm: GhosttyTerminal,
106
106
  textarea: HTMLTextAreaElement
107
107
  ) {
@@ -114,6 +114,11 @@ export class SelectionManager {
114
114
  this.attachEventListeners();
115
115
  }
116
116
 
117
+ /** Points the selection at a new renderer drawing the same canvas. */
118
+ setRenderer(renderer: TerminalRenderer): void {
119
+ this.renderer = renderer;
120
+ }
121
+
117
122
  // ==========================================================================
118
123
  // Public API
119
124
  // ==========================================================================
package/lib/terminal.ts CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  import { getGhostty } from './index';
31
31
  import { InputHandler, type MouseTrackingConfig } from './input-handler';
32
32
  import type {
33
+ FontWeight,
33
34
  IBufferNamespace,
34
35
  IBufferRange,
35
36
  IDisposable,
@@ -44,9 +45,16 @@ import { LinkDetector } from './link-detector';
44
45
  import { type IMarker, Marker } from './marker';
45
46
  import { OSC8LinkProvider } from './providers/osc8-link-provider';
46
47
  import { UrlRegexProvider } from './providers/url-regex-provider';
47
- import { CanvasRenderer } from './renderer';
48
+ import {
49
+ CanvasRenderer,
50
+ type RendererOptions,
51
+ type SearchHighlight,
52
+ type SearchHighlightColors,
53
+ type TerminalRenderer,
54
+ } from './renderer';
48
55
  import { SelectionManager } from './selection-manager';
49
56
  import type { ILink, ILinkProvider } from './types';
57
+ import { WebglRenderer } from './webgl-renderer';
50
58
 
51
59
  // ============================================================================
52
60
  // Terminal Class
@@ -75,7 +83,7 @@ export class Terminal implements ITerminalCore {
75
83
  // Components (created on open())
76
84
  private ghostty?: Ghostty;
77
85
  public wasmTerm?: GhosttyTerminal; // Made public for link providers
78
- public renderer?: CanvasRenderer; // Made public for FitAddon
86
+ public renderer?: TerminalRenderer; // Made public for FitAddon
79
87
  private inputHandler?: InputHandler;
80
88
  private selectionManager?: SelectionManager;
81
89
  private canvas?: HTMLCanvasElement;
@@ -96,6 +104,7 @@ export class Terminal implements ITerminalCore {
96
104
  private scrollEmitter = new EventEmitter<number>();
97
105
  private renderEmitter = new EventEmitter<{ start: number; end: number }>();
98
106
  private cursorMoveEmitter = new EventEmitter<void>();
107
+ private writeParsedEmitter = new EventEmitter<void>();
99
108
  // Public event accessors (xterm.js compatibility)
100
109
  public readonly onData: IEvent<string> = this.dataEmitter.event;
101
110
  public readonly onResize: IEvent<{ cols: number; rows: number }> = this.resizeEmitter.event;
@@ -106,6 +115,8 @@ export class Terminal implements ITerminalCore {
106
115
  public readonly onScroll: IEvent<number> = this.scrollEmitter.event;
107
116
  public readonly onRender: IEvent<{ start: number; end: number }> = this.renderEmitter.event;
108
117
  public readonly onCursorMove: IEvent<void> = this.cursorMoveEmitter.event;
118
+ /** Fires after written data was parsed, as xterm.js's onWriteParsed. */
119
+ public readonly onWriteParsed: IEvent<void> = this.writeParsedEmitter.event;
109
120
 
110
121
  private pwdEmitter = new EventEmitter<string>();
111
122
  private semanticPromptEmitter = new EventEmitter<SemanticPromptEvent>();
@@ -187,9 +198,17 @@ export class Terminal implements ITerminalCore {
187
198
  fontSize: options.fontSize ?? 15,
188
199
  fontFamily: options.fontFamily ?? 'monospace',
189
200
  allowTransparency: options.allowTransparency ?? false,
201
+ linkHandler: options.linkHandler ?? null,
202
+ renderer: options.renderer ?? 'webgl',
190
203
  convertEol: options.convertEol ?? false,
191
204
  disableStdin: options.disableStdin ?? false,
192
205
  smoothScrollDuration: options.smoothScrollDuration ?? 100, // Default: 100ms smooth scroll
206
+ fontWeight: options.fontWeight ?? 'normal',
207
+ fontWeightBold: options.fontWeightBold ?? 'bold',
208
+ lineHeight: options.lineHeight ?? 1,
209
+ scrollSensitivity: options.scrollSensitivity ?? 1,
210
+ minimumContrastRatio: options.minimumContrastRatio ?? 1,
211
+ macOptionIsMeta: options.macOptionIsMeta ?? false,
193
212
  };
194
213
 
195
214
  // Wrap in Proxy to intercept runtime changes (xterm.js compatibility)
@@ -241,11 +260,37 @@ export class Terminal implements ITerminalCore {
241
260
  break;
242
261
 
243
262
  case 'theme':
263
+ this.wasmTerm?.configure(this.themeConfig());
264
+ if (this.renderer && this.wasmTerm) {
265
+ this.renderer.setTheme(this.options.theme);
266
+ this.renderer.render(this.wasmTerm, true, this.viewportY, this, this.scrollbarOpacity);
267
+ }
268
+ break;
269
+
270
+ case 'macOptionIsMeta':
271
+ if (this.inputHandler) this.inputHandler.macOptionIsMeta = this.options.macOptionIsMeta;
272
+ break;
273
+
274
+ case 'fontWeight':
275
+ case 'fontWeightBold':
276
+ case 'lineHeight':
244
277
  if (this.renderer) {
245
- console.warn('ghostty-web: theme changes after open() are not yet fully supported');
278
+ this.renderer.setFontOptions(this.fontOptions());
279
+ this.handleFontChange();
280
+ }
281
+ break;
282
+
283
+ case 'minimumContrastRatio':
284
+ if (this.renderer && this.wasmTerm) {
285
+ this.renderer.setMinimumContrastRatio(this.options.minimumContrastRatio);
286
+ this.renderer.render(this.wasmTerm, true, this.viewportY, this, this.scrollbarOpacity);
246
287
  }
247
288
  break;
248
289
 
290
+ case 'scrollback':
291
+ this.wasmTerm?.configure({ scrollbackLimit: this.options.scrollback });
292
+ break;
293
+
249
294
  case 'fontSize':
250
295
  if (this.renderer) {
251
296
  this.renderer.setFontSize(this.options.fontSize);
@@ -283,13 +328,6 @@ export class Terminal implements ITerminalCore {
283
328
  // Resize canvas to match new font metrics
284
329
  this.renderer.resize(this.cols, this.rows);
285
330
 
286
- // Update canvas element dimensions to match renderer
287
- const metrics = this.renderer.getMetrics();
288
- this.canvas.width = metrics.width * this.cols;
289
- this.canvas.height = metrics.height * this.rows;
290
- this.canvas.style.width = `${metrics.width * this.cols}px`;
291
- this.canvas.style.height = `${metrics.height * this.rows}px`;
292
-
293
331
  // Force full re-render with new font
294
332
  this.renderer.render(this.wasmTerm, true, this.viewportY, this);
295
333
  }
@@ -298,67 +336,48 @@ export class Terminal implements ITerminalCore {
298
336
  * Parse a CSS color string to 0xRRGGBB format.
299
337
  * Returns 0 if the color is undefined or invalid.
300
338
  */
301
- private parseColorToHex(color?: string): number {
302
- if (!color) return 0;
303
-
304
- // Handle hex colors (#RGB, #RRGGBB)
339
+ private parseColorToHex(color?: string): number | undefined {
340
+ if (!color) return undefined;
305
341
  if (color.startsWith('#')) {
306
342
  let hex = color.slice(1);
307
- if (hex.length === 3) {
343
+ if (hex.length === 3 || hex.length === 4) {
308
344
  hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
309
345
  }
310
- const value = Number.parseInt(hex, 16);
311
- return Number.isNaN(value) ? 0 : value;
346
+ const value = Number.parseInt(hex.slice(0, 6), 16);
347
+ return hex.length >= 6 && !Number.isNaN(value) ? value : undefined;
312
348
  }
313
-
314
- // Handle rgb(r, g, b) format
315
- const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
349
+ const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
316
350
  if (match) {
317
351
  const r = Number.parseInt(match[1], 10);
318
352
  const g = Number.parseInt(match[2], 10);
319
353
  const b = Number.parseInt(match[3], 10);
320
354
  return (r << 16) | (g << 8) | b;
321
355
  }
322
-
323
- return 0;
356
+ return undefined;
324
357
  }
325
358
 
326
- /**
327
- * Convert terminal options to WASM terminal config.
328
- */
329
- private buildWasmConfig(): GhosttyTerminalConfig | undefined {
359
+ /** Theme colors in the form libghostty-vt takes; a color the theme leaves out keeps Ghostty's default. */
360
+ private themeConfig(): GhosttyTerminalConfig {
330
361
  const theme = this.options.theme;
331
- const scrollback = this.options.scrollback;
332
-
333
- // If no theme and default scrollback, use defaults
334
- if (!theme && scrollback === 10000) {
335
- return undefined;
336
- }
337
-
338
- // Build palette array from theme colors
339
- // Order: black, red, green, yellow, blue, magenta, cyan, white,
340
- // brightBlack, brightRed, brightGreen, brightYellow, brightBlue, brightMagenta, brightCyan, brightWhite
341
- const palette: number[] = [
342
- this.parseColorToHex(theme?.black),
343
- this.parseColorToHex(theme?.red),
344
- this.parseColorToHex(theme?.green),
345
- this.parseColorToHex(theme?.yellow),
346
- this.parseColorToHex(theme?.blue),
347
- this.parseColorToHex(theme?.magenta),
348
- this.parseColorToHex(theme?.cyan),
349
- this.parseColorToHex(theme?.white),
350
- this.parseColorToHex(theme?.brightBlack),
351
- this.parseColorToHex(theme?.brightRed),
352
- this.parseColorToHex(theme?.brightGreen),
353
- this.parseColorToHex(theme?.brightYellow),
354
- this.parseColorToHex(theme?.brightBlue),
355
- this.parseColorToHex(theme?.brightMagenta),
356
- this.parseColorToHex(theme?.brightCyan),
357
- this.parseColorToHex(theme?.brightWhite),
358
- ];
359
-
362
+ const palette = [
363
+ theme?.black,
364
+ theme?.red,
365
+ theme?.green,
366
+ theme?.yellow,
367
+ theme?.blue,
368
+ theme?.magenta,
369
+ theme?.cyan,
370
+ theme?.white,
371
+ theme?.brightBlack,
372
+ theme?.brightRed,
373
+ theme?.brightGreen,
374
+ theme?.brightYellow,
375
+ theme?.brightBlue,
376
+ theme?.brightMagenta,
377
+ theme?.brightCyan,
378
+ theme?.brightWhite,
379
+ ].map((color) => this.parseColorToHex(color));
360
380
  return {
361
- scrollbackLimit: scrollback,
362
381
  fgColor: this.parseColorToHex(theme?.foreground),
363
382
  bgColor: this.parseColorToHex(theme?.background),
364
383
  cursorColor: this.parseColorToHex(theme?.cursor),
@@ -366,6 +385,10 @@ export class Terminal implements ITerminalCore {
366
385
  };
367
386
  }
368
387
 
388
+ private buildWasmConfig(): GhosttyTerminalConfig {
389
+ return { ...this.themeConfig(), scrollbackLimit: this.options.scrollback };
390
+ }
391
+
369
392
  // ==========================================================================
370
393
  // Lifecycle Methods
371
394
  // ==========================================================================
@@ -457,29 +480,18 @@ export class Terminal implements ITerminalCore {
457
480
  textarea.focus();
458
481
  });
459
482
 
460
- // Create renderer
461
- this.renderer = new CanvasRenderer(this.canvas, {
462
- fontSize: this.options.fontSize,
463
- fontFamily: this.options.fontFamily,
464
- cursorStyle: this.options.cursorStyle,
465
- cursorBlink: this.options.cursorBlink,
466
- theme: this.options.theme,
467
- });
468
- this.renderer.onNeedsFrame = () => this.wake();
469
-
470
- // Size canvas to terminal dimensions (use renderer.resize for proper DPI scaling)
483
+ this.renderer = this.createRenderer();
471
484
  this.renderer.resize(this.cols, this.rows);
472
485
 
473
486
  // Create mouse tracking configuration
474
487
  const canvas = this.canvas;
475
- const renderer = this.renderer;
476
488
  const wasmTerm = this.wasmTerm;
477
489
  const mouseConfig: MouseTrackingConfig = {
478
490
  hasMouseTracking: () => wasmTerm?.hasMouseTracking() ?? false,
479
491
  hasSgrMouseMode: () => wasmTerm?.getMode(1006, false) ?? true, // SGR extended mode
480
492
  getCellDimensions: () => ({
481
- width: renderer.charWidth,
482
- height: renderer.charHeight,
493
+ width: this.renderer!.charWidth,
494
+ height: this.renderer!.charHeight,
483
495
  }),
484
496
  getCanvasOffset: () => {
485
497
  const rect = canvas.getBoundingClientRect();
@@ -522,6 +534,7 @@ export class Terminal implements ITerminalCore {
522
534
  this.textarea,
523
535
  mouseConfig
524
536
  );
537
+ this.inputHandler.macOptionIsMeta = this.options.macOptionIsMeta;
525
538
 
526
539
  // Create selection manager (pass textarea for context menu positioning)
527
540
  this.selectionManager = new SelectionManager(
@@ -553,6 +566,8 @@ export class Terminal implements ITerminalCore {
553
566
  parent.addEventListener('mousedown', this.handleMouseDown, { capture: true });
554
567
  parent.addEventListener('mousemove', this.handleMouseMove);
555
568
  parent.addEventListener('mouseleave', this.handleMouseLeave);
569
+ parent.addEventListener('focusin', this.handleFocusIn);
570
+ parent.addEventListener('focusout', this.handleFocusOut);
556
571
  parent.addEventListener('click', this.handleClick);
557
572
 
558
573
  // Setup document-level mouseup for scrollbar drag (so drag works even outside canvas)
@@ -578,6 +593,74 @@ export class Terminal implements ITerminalCore {
578
593
  }
579
594
  }
580
595
 
596
+ /** Which renderer draws the terminal now: 'webgl', or 'canvas' when asked for or fallen back to. */
597
+ get rendererType(): 'webgl' | 'canvas' {
598
+ return this.renderer instanceof WebglRenderer ? 'webgl' : 'canvas';
599
+ }
600
+
601
+ private fontOptions(): {
602
+ fontWeight: FontWeight;
603
+ fontWeightBold: FontWeight;
604
+ lineHeight: number;
605
+ } {
606
+ return {
607
+ fontWeight: this.options.fontWeight,
608
+ fontWeightBold: this.options.fontWeightBold,
609
+ lineHeight: this.options.lineHeight,
610
+ };
611
+ }
612
+
613
+ private rendererOptions(): RendererOptions {
614
+ return {
615
+ ...this.fontOptions(),
616
+ minimumContrastRatio: this.options.minimumContrastRatio,
617
+ fontSize: this.options.fontSize,
618
+ fontFamily: this.options.fontFamily,
619
+ cursorStyle: this.options.cursorStyle,
620
+ cursorBlink: this.options.cursorBlink,
621
+ theme: this.options.theme,
622
+ };
623
+ }
624
+
625
+ private createRenderer(): TerminalRenderer {
626
+ let renderer: TerminalRenderer | null = null;
627
+ if (this.options.renderer === 'webgl') {
628
+ try {
629
+ const webgl = new WebglRenderer(this.canvas!, this.rendererOptions());
630
+ webgl.onContextLost = () => this.fallBackToCanvas();
631
+ renderer = webgl;
632
+ } catch {
633
+ renderer = null;
634
+ }
635
+ }
636
+ renderer ??= new CanvasRenderer(this.canvas!, this.rendererOptions());
637
+ renderer.onNeedsFrame = () => this.requestFrame();
638
+ renderer.setFocused(this.focused);
639
+ return renderer;
640
+ }
641
+
642
+ /** Replaces a WebGL renderer whose context was lost with a canvas renderer on the same canvas. */
643
+ private fallBackToCanvas(): void {
644
+ if (!this.renderer || !this.canvas || !(this.renderer instanceof WebglRenderer)) return;
645
+ const hoveredLink = this.renderer.getHoveredHyperlinkId();
646
+ this.renderer.dispose();
647
+ const renderer = new CanvasRenderer(this.canvas, this.rendererOptions());
648
+ renderer.onNeedsFrame = () => this.requestFrame();
649
+ renderer.setHoveredHyperlinkId(hoveredLink);
650
+ renderer.setFocused(this.focused);
651
+ renderer.setSearchHighlights(this.searchHighlights.list, this.searchHighlights.colors);
652
+ if (this.selectionManager) {
653
+ renderer.setSelectionManager(this.selectionManager);
654
+ this.selectionManager.setRenderer(renderer);
655
+ }
656
+ this.renderer = renderer;
657
+ renderer.resize(this.cols, this.rows);
658
+ if (this.wasmTerm) {
659
+ renderer.render(this.wasmTerm, true, this.viewportY, this, this.scrollbarOpacity);
660
+ }
661
+ this.wake();
662
+ }
663
+
581
664
  /**
582
665
  * Write data to terminal
583
666
  */
@@ -723,13 +806,6 @@ export class Terminal implements ITerminalCore {
723
806
  // Resize renderer
724
807
  this.renderer!.resize(cols, rows);
725
808
 
726
- // Update canvas dimensions
727
- const metrics = this.renderer!.getMetrics();
728
- this.canvas!.width = metrics.width * cols;
729
- this.canvas!.height = metrics.height * rows;
730
- this.canvas!.style.width = `${metrics.width * cols}px`;
731
- this.canvas!.style.height = `${metrics.height * rows}px`;
732
-
733
809
  // Fire resize event
734
810
  this.resizeEmitter.fire({ cols, rows });
735
811
 
@@ -1007,16 +1083,18 @@ export class Terminal implements ITerminalCore {
1007
1083
  }
1008
1084
  }
1009
1085
 
1010
- /**
1011
- * Scroll viewport to a specific line in the buffer
1012
- * @param line Line number (0 = top of scrollback, scrollbackLength = bottom)
1013
- */
1014
1086
  /** Scrolls so that absolute buffer line `line` (0 = oldest scrollback line) is at the top, like xterm.js. */
1015
1087
  public scrollToLine(line: number): void {
1016
- this.wake();
1017
1088
  const scrollbackLength = this.getScrollbackLength();
1018
1089
  const top = Math.max(0, Math.min(scrollbackLength, Math.round(line)));
1019
- const newViewportY = scrollbackLength - top;
1090
+ this.scrollToViewportY(scrollbackLength - top);
1091
+ }
1092
+
1093
+ /** Scrolls to `viewportY` lines above the bottom. */
1094
+ private scrollToViewportY(viewportY: number): void {
1095
+ this.wake();
1096
+ const scrollbackLength = this.getScrollbackLength();
1097
+ const newViewportY = Math.max(0, Math.min(scrollbackLength, Math.round(viewportY)));
1020
1098
 
1021
1099
  if (newViewportY !== this.viewportY) {
1022
1100
  this.viewportY = newViewportY;
@@ -1173,6 +1251,7 @@ export class Terminal implements ITerminalCore {
1173
1251
  this.scrollEmitter.dispose();
1174
1252
  this.renderEmitter.dispose();
1175
1253
  this.cursorMoveEmitter.dispose();
1254
+ this.writeParsedEmitter.dispose();
1176
1255
  }
1177
1256
 
1178
1257
  // ==========================================================================
@@ -1280,6 +1359,8 @@ export class Terminal implements ITerminalCore {
1280
1359
  this.element.removeEventListener('mousedown', this.handleMouseDown, { capture: true });
1281
1360
  this.element.removeEventListener('mousemove', this.handleMouseMove);
1282
1361
  this.element.removeEventListener('mouseleave', this.handleMouseLeave);
1362
+ this.element.removeEventListener('focusin', this.handleFocusIn);
1363
+ this.element.removeEventListener('focusout', this.handleFocusOut);
1283
1364
  this.element.removeEventListener('click', this.handleClick);
1284
1365
 
1285
1366
  // Remove contenteditable and accessibility attributes added in open()
@@ -1635,6 +1716,8 @@ export class Terminal implements ITerminalCore {
1635
1716
  deltaLines = e.deltaY / 33;
1636
1717
  }
1637
1718
 
1719
+ deltaLines *= this.options.scrollSensitivity;
1720
+
1638
1721
  // Use smooth scrolling for any amount (no rounding needed)
1639
1722
  if (deltaLines !== 0) {
1640
1723
  // Calculate target position
@@ -1700,7 +1783,7 @@ export class Terminal implements ITerminalCore {
1700
1783
  const relativeY = mouseY - scrollbarPadding;
1701
1784
  const scrollFraction = 1 - relativeY / scrollbarTrackHeight; // Inverted: top = 1, bottom = 0
1702
1785
  const targetViewportY = Math.round(scrollFraction * scrollbackLength);
1703
- this.scrollToLine(Math.max(0, Math.min(scrollbackLength, targetViewportY)));
1786
+ this.scrollToViewportY(targetViewportY);
1704
1787
  }
1705
1788
  }
1706
1789
  };
@@ -1758,7 +1841,7 @@ export class Terminal implements ITerminalCore {
1758
1841
  const viewportDelta = Math.round(scrollFraction * scrollbackLength);
1759
1842
 
1760
1843
  const newViewportY = this.scrollbarDragStartViewportY + viewportDelta;
1761
- this.scrollToLine(Math.max(0, Math.min(scrollbackLength, newViewportY)));
1844
+ this.scrollToViewportY(newViewportY);
1762
1845
  }
1763
1846
 
1764
1847
  /**
@@ -1937,6 +2020,30 @@ export class Terminal implements ITerminalCore {
1937
2020
  };
1938
2021
  }
1939
2022
 
2023
+ private focused = false;
2024
+
2025
+ private readonly handleFocusIn = (): void => this.setFocused(true);
2026
+
2027
+ private readonly handleFocusOut = (event: FocusEvent): void => {
2028
+ const next = event.relatedTarget;
2029
+ if (next instanceof Node && this.element?.contains(next)) return;
2030
+ this.setFocused(false);
2031
+ };
2032
+
2033
+ private setFocused(focused: boolean): void {
2034
+ if (focused === this.focused) return;
2035
+ this.focused = focused;
2036
+ this.renderer?.setFocused(focused);
2037
+ this.requestFrame();
2038
+ }
2039
+
2040
+ /** Draws one frame, for a change that needs no follow-up frames (a cursor blink). */
2041
+ private requestFrame(): void {
2042
+ if (this.animationFrameId) return;
2043
+ this.idleFrames = Terminal.IDLE_FRAMES_BEFORE_SLEEP;
2044
+ if (this.isOpen && !this.isDisposed) this.startRenderLoop();
2045
+ }
2046
+
1940
2047
  /** Keeps the render loop running for a while; called whenever something may need drawing. */
1941
2048
  public wake(): void {
1942
2049
  this.idleFrames = 0;
@@ -1971,6 +2078,19 @@ export class Terminal implements ITerminalCore {
1971
2078
  (this.buffer as BufferNamespace)._fireBufferChange(this.buffer.active);
1972
2079
  }
1973
2080
  for (const marker of [...this.markers]) marker.refresh();
2081
+ this.writeParsedEmitter.fire();
2082
+ this.wake();
2083
+ }
2084
+
2085
+ private searchHighlights: {
2086
+ list: SearchHighlight[] | null;
2087
+ colors?: SearchHighlightColors;
2088
+ } = { list: null };
2089
+
2090
+ /** Shows search matches on viewport rows (null clears them). Used by SearchAddon. */
2091
+ setSearchHighlights(list: SearchHighlight[] | null, colors?: SearchHighlightColors): void {
2092
+ this.searchHighlights = { list, colors: colors ?? this.searchHighlights.colors };
2093
+ this.renderer?.setSearchHighlights(list, colors);
1974
2094
  this.wake();
1975
2095
  }
1976
2096
 
package/lib/types.ts CHANGED
@@ -394,7 +394,8 @@ export interface GhosttyTerminalConfig {
394
394
  fgColor?: number;
395
395
  bgColor?: number;
396
396
  cursorColor?: number;
397
- palette?: number[];
397
+ /** The 16 ANSI colors; an undefined entry keeps Ghostty's default. */
398
+ palette?: (number | undefined)[];
398
399
  }
399
400
 
400
401
  /**
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Packs the renderer's per-cell instance slots into dense arrays so a frame
3
+ * draws only the instances that show something. Slots with a zero width are
4
+ * empty. Rects that continue the previous one on the same row with the same
5
+ * color are merged into it.
6
+ */
7
+
8
+ export const RECT_WORDS = 5;
9
+ export const GLYPH_WORDS = 8;
10
+
11
+ export interface InstanceArrays {
12
+ words: Float32Array;
13
+ bits: Uint32Array;
14
+ }
15
+
16
+ export function instanceArrays(instances: number, wordsPer: number): InstanceArrays {
17
+ const buffer = new ArrayBuffer(instances * wordsPer * 4);
18
+ return { words: new Float32Array(buffer), bits: new Uint32Array(buffer) };
19
+ }
20
+
21
+ export function compactRects(
22
+ src: InstanceArrays,
23
+ first: number,
24
+ count: number,
25
+ out: InstanceArrays
26
+ ): number {
27
+ const { words, bits } = src;
28
+ const outWords = out.words;
29
+ const outBits = out.bits;
30
+ let n = 0;
31
+ let last = -1;
32
+ for (let i = first; i < first + count; i++) {
33
+ const at = i * RECT_WORDS;
34
+ const width = words[at + 2];
35
+ if (width === 0) continue;
36
+ if (
37
+ last >= 0 &&
38
+ bits[at + 4] === outBits[last + 4] &&
39
+ words[at + 1] === outWords[last + 1] &&
40
+ words[at + 3] === outWords[last + 3] &&
41
+ outWords[last] + outWords[last + 2] === words[at]
42
+ ) {
43
+ outWords[last + 2] += width;
44
+ continue;
45
+ }
46
+ last = n * RECT_WORDS;
47
+ outBits[last] = bits[at];
48
+ outBits[last + 1] = bits[at + 1];
49
+ outBits[last + 2] = bits[at + 2];
50
+ outBits[last + 3] = bits[at + 3];
51
+ outBits[last + 4] = bits[at + 4];
52
+ n++;
53
+ }
54
+ return n;
55
+ }
56
+
57
+ export function compactGlyphs(src: InstanceArrays, count: number, out: InstanceArrays): number {
58
+ const { words, bits } = src;
59
+ const outBits = out.bits;
60
+ let n = 0;
61
+ for (let i = 0; i < count; i++) {
62
+ const at = i * GLYPH_WORDS;
63
+ if (words[at + 4] === 0) continue;
64
+ const to = n * GLYPH_WORDS;
65
+ for (let w = 0; w < GLYPH_WORDS; w++) outBits[to + w] = bits[at + w];
66
+ n++;
67
+ }
68
+ return n;
69
+ }