@aurigax-ai/ghostty-web 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/lib/addons/search.ts +190 -0
- package/lib/buffer.ts +1 -3
- package/lib/ghostty.ts +236 -7
- package/lib/glyph-ink.ts +42 -0
- package/lib/index.ts +6 -0
- package/lib/input-handler.ts +45 -3
- package/lib/interfaces.ts +23 -0
- package/lib/providers/osc8-link-provider.ts +4 -10
- package/lib/providers/url-regex-provider.ts +10 -13
- package/lib/providers/web-link.ts +35 -0
- package/lib/renderer.ts +266 -33
- package/lib/selection-manager.ts +3 -16
- package/lib/terminal.ts +154 -73
- package/lib/types.ts +2 -1
- package/lib/webgl-instances.ts +69 -0
- package/lib/webgl-renderer.ts +176 -103
- package/package.json +1 -1
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,7 +45,13 @@ 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 {
|
|
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';
|
|
50
57
|
import { WebglRenderer } from './webgl-renderer';
|
|
@@ -97,6 +104,7 @@ export class Terminal implements ITerminalCore {
|
|
|
97
104
|
private scrollEmitter = new EventEmitter<number>();
|
|
98
105
|
private renderEmitter = new EventEmitter<{ start: number; end: number }>();
|
|
99
106
|
private cursorMoveEmitter = new EventEmitter<void>();
|
|
107
|
+
private writeParsedEmitter = new EventEmitter<void>();
|
|
100
108
|
// Public event accessors (xterm.js compatibility)
|
|
101
109
|
public readonly onData: IEvent<string> = this.dataEmitter.event;
|
|
102
110
|
public readonly onResize: IEvent<{ cols: number; rows: number }> = this.resizeEmitter.event;
|
|
@@ -107,6 +115,8 @@ export class Terminal implements ITerminalCore {
|
|
|
107
115
|
public readonly onScroll: IEvent<number> = this.scrollEmitter.event;
|
|
108
116
|
public readonly onRender: IEvent<{ start: number; end: number }> = this.renderEmitter.event;
|
|
109
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;
|
|
110
120
|
|
|
111
121
|
private pwdEmitter = new EventEmitter<string>();
|
|
112
122
|
private semanticPromptEmitter = new EventEmitter<SemanticPromptEvent>();
|
|
@@ -188,10 +198,17 @@ export class Terminal implements ITerminalCore {
|
|
|
188
198
|
fontSize: options.fontSize ?? 15,
|
|
189
199
|
fontFamily: options.fontFamily ?? 'monospace',
|
|
190
200
|
allowTransparency: options.allowTransparency ?? false,
|
|
201
|
+
linkHandler: options.linkHandler ?? null,
|
|
191
202
|
renderer: options.renderer ?? 'webgl',
|
|
192
203
|
convertEol: options.convertEol ?? false,
|
|
193
204
|
disableStdin: options.disableStdin ?? false,
|
|
194
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,
|
|
195
212
|
};
|
|
196
213
|
|
|
197
214
|
// Wrap in Proxy to intercept runtime changes (xterm.js compatibility)
|
|
@@ -243,11 +260,37 @@ export class Terminal implements ITerminalCore {
|
|
|
243
260
|
break;
|
|
244
261
|
|
|
245
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':
|
|
246
277
|
if (this.renderer) {
|
|
247
|
-
|
|
278
|
+
this.renderer.setFontOptions(this.fontOptions());
|
|
279
|
+
this.handleFontChange();
|
|
248
280
|
}
|
|
249
281
|
break;
|
|
250
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);
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
|
|
290
|
+
case 'scrollback':
|
|
291
|
+
this.wasmTerm?.configure({ scrollbackLimit: this.options.scrollback });
|
|
292
|
+
break;
|
|
293
|
+
|
|
251
294
|
case 'fontSize':
|
|
252
295
|
if (this.renderer) {
|
|
253
296
|
this.renderer.setFontSize(this.options.fontSize);
|
|
@@ -293,67 +336,48 @@ export class Terminal implements ITerminalCore {
|
|
|
293
336
|
* Parse a CSS color string to 0xRRGGBB format.
|
|
294
337
|
* Returns 0 if the color is undefined or invalid.
|
|
295
338
|
*/
|
|
296
|
-
private parseColorToHex(color?: string): number {
|
|
297
|
-
if (!color) return
|
|
298
|
-
|
|
299
|
-
// Handle hex colors (#RGB, #RRGGBB)
|
|
339
|
+
private parseColorToHex(color?: string): number | undefined {
|
|
340
|
+
if (!color) return undefined;
|
|
300
341
|
if (color.startsWith('#')) {
|
|
301
342
|
let hex = color.slice(1);
|
|
302
|
-
if (hex.length === 3) {
|
|
343
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
303
344
|
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
304
345
|
}
|
|
305
|
-
const value = Number.parseInt(hex, 16);
|
|
306
|
-
return Number.isNaN(value) ?
|
|
346
|
+
const value = Number.parseInt(hex.slice(0, 6), 16);
|
|
347
|
+
return hex.length >= 6 && !Number.isNaN(value) ? value : undefined;
|
|
307
348
|
}
|
|
308
|
-
|
|
309
|
-
// Handle rgb(r, g, b) format
|
|
310
|
-
const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
349
|
+
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
311
350
|
if (match) {
|
|
312
351
|
const r = Number.parseInt(match[1], 10);
|
|
313
352
|
const g = Number.parseInt(match[2], 10);
|
|
314
353
|
const b = Number.parseInt(match[3], 10);
|
|
315
354
|
return (r << 16) | (g << 8) | b;
|
|
316
355
|
}
|
|
317
|
-
|
|
318
|
-
return 0;
|
|
356
|
+
return undefined;
|
|
319
357
|
}
|
|
320
358
|
|
|
321
|
-
/**
|
|
322
|
-
|
|
323
|
-
*/
|
|
324
|
-
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 {
|
|
325
361
|
const theme = this.options.theme;
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
this.parseColorToHex(theme?.white),
|
|
345
|
-
this.parseColorToHex(theme?.brightBlack),
|
|
346
|
-
this.parseColorToHex(theme?.brightRed),
|
|
347
|
-
this.parseColorToHex(theme?.brightGreen),
|
|
348
|
-
this.parseColorToHex(theme?.brightYellow),
|
|
349
|
-
this.parseColorToHex(theme?.brightBlue),
|
|
350
|
-
this.parseColorToHex(theme?.brightMagenta),
|
|
351
|
-
this.parseColorToHex(theme?.brightCyan),
|
|
352
|
-
this.parseColorToHex(theme?.brightWhite),
|
|
353
|
-
];
|
|
354
|
-
|
|
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));
|
|
355
380
|
return {
|
|
356
|
-
scrollbackLimit: scrollback,
|
|
357
381
|
fgColor: this.parseColorToHex(theme?.foreground),
|
|
358
382
|
bgColor: this.parseColorToHex(theme?.background),
|
|
359
383
|
cursorColor: this.parseColorToHex(theme?.cursor),
|
|
@@ -361,6 +385,10 @@ export class Terminal implements ITerminalCore {
|
|
|
361
385
|
};
|
|
362
386
|
}
|
|
363
387
|
|
|
388
|
+
private buildWasmConfig(): GhosttyTerminalConfig {
|
|
389
|
+
return { ...this.themeConfig(), scrollbackLimit: this.options.scrollback };
|
|
390
|
+
}
|
|
391
|
+
|
|
364
392
|
// ==========================================================================
|
|
365
393
|
// Lifecycle Methods
|
|
366
394
|
// ==========================================================================
|
|
@@ -506,6 +534,7 @@ export class Terminal implements ITerminalCore {
|
|
|
506
534
|
this.textarea,
|
|
507
535
|
mouseConfig
|
|
508
536
|
);
|
|
537
|
+
this.inputHandler.macOptionIsMeta = this.options.macOptionIsMeta;
|
|
509
538
|
|
|
510
539
|
// Create selection manager (pass textarea for context menu positioning)
|
|
511
540
|
this.selectionManager = new SelectionManager(
|
|
@@ -537,6 +566,8 @@ export class Terminal implements ITerminalCore {
|
|
|
537
566
|
parent.addEventListener('mousedown', this.handleMouseDown, { capture: true });
|
|
538
567
|
parent.addEventListener('mousemove', this.handleMouseMove);
|
|
539
568
|
parent.addEventListener('mouseleave', this.handleMouseLeave);
|
|
569
|
+
parent.addEventListener('focusin', this.handleFocusIn);
|
|
570
|
+
parent.addEventListener('focusout', this.handleFocusOut);
|
|
540
571
|
parent.addEventListener('click', this.handleClick);
|
|
541
572
|
|
|
542
573
|
// Setup document-level mouseup for scrollbar drag (so drag works even outside canvas)
|
|
@@ -551,9 +582,6 @@ export class Terminal implements ITerminalCore {
|
|
|
551
582
|
|
|
552
583
|
// Start render loop
|
|
553
584
|
this.startRenderLoop();
|
|
554
|
-
|
|
555
|
-
// Focus input (auto-focus so user can start typing immediately)
|
|
556
|
-
this.focus();
|
|
557
585
|
} catch (error) {
|
|
558
586
|
// Clean up on error
|
|
559
587
|
this.isOpen = false;
|
|
@@ -567,8 +595,22 @@ export class Terminal implements ITerminalCore {
|
|
|
567
595
|
return this.renderer instanceof WebglRenderer ? 'webgl' : 'canvas';
|
|
568
596
|
}
|
|
569
597
|
|
|
598
|
+
private fontOptions(): {
|
|
599
|
+
fontWeight: FontWeight;
|
|
600
|
+
fontWeightBold: FontWeight;
|
|
601
|
+
lineHeight: number;
|
|
602
|
+
} {
|
|
603
|
+
return {
|
|
604
|
+
fontWeight: this.options.fontWeight,
|
|
605
|
+
fontWeightBold: this.options.fontWeightBold,
|
|
606
|
+
lineHeight: this.options.lineHeight,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
570
610
|
private rendererOptions(): RendererOptions {
|
|
571
611
|
return {
|
|
612
|
+
...this.fontOptions(),
|
|
613
|
+
minimumContrastRatio: this.options.minimumContrastRatio,
|
|
572
614
|
fontSize: this.options.fontSize,
|
|
573
615
|
fontFamily: this.options.fontFamily,
|
|
574
616
|
cursorStyle: this.options.cursorStyle,
|
|
@@ -589,7 +631,8 @@ export class Terminal implements ITerminalCore {
|
|
|
589
631
|
}
|
|
590
632
|
}
|
|
591
633
|
renderer ??= new CanvasRenderer(this.canvas!, this.rendererOptions());
|
|
592
|
-
renderer.onNeedsFrame = () => this.
|
|
634
|
+
renderer.onNeedsFrame = () => this.requestFrame();
|
|
635
|
+
renderer.setFocused(this.focused);
|
|
593
636
|
return renderer;
|
|
594
637
|
}
|
|
595
638
|
|
|
@@ -599,8 +642,10 @@ export class Terminal implements ITerminalCore {
|
|
|
599
642
|
const hoveredLink = this.renderer.getHoveredHyperlinkId();
|
|
600
643
|
this.renderer.dispose();
|
|
601
644
|
const renderer = new CanvasRenderer(this.canvas, this.rendererOptions());
|
|
602
|
-
renderer.onNeedsFrame = () => this.
|
|
645
|
+
renderer.onNeedsFrame = () => this.requestFrame();
|
|
603
646
|
renderer.setHoveredHyperlinkId(hoveredLink);
|
|
647
|
+
renderer.setFocused(this.focused);
|
|
648
|
+
renderer.setSearchHighlights(this.searchHighlights.list, this.searchHighlights.colors);
|
|
604
649
|
if (this.selectionManager) {
|
|
605
650
|
renderer.setSelectionManager(this.selectionManager);
|
|
606
651
|
this.selectionManager.setRenderer(renderer);
|
|
@@ -654,8 +699,9 @@ export class Terminal implements ITerminalCore {
|
|
|
654
699
|
|
|
655
700
|
// Call callback if provided
|
|
656
701
|
if (callback) {
|
|
657
|
-
//
|
|
658
|
-
|
|
702
|
+
// The data is parsed by now, so run the callback as soon as this task
|
|
703
|
+
// ends, as xterm.js does once a write is processed.
|
|
704
|
+
queueMicrotask(callback);
|
|
659
705
|
}
|
|
660
706
|
|
|
661
707
|
if (this.awaitingEcho) {
|
|
@@ -809,16 +855,7 @@ export class Terminal implements ITerminalCore {
|
|
|
809
855
|
*/
|
|
810
856
|
focus(): void {
|
|
811
857
|
this.wake();
|
|
812
|
-
if (this.isOpen && this.element)
|
|
813
|
-
// Focus immediately for immediate keyboard/wheel event handling
|
|
814
|
-
this.element.focus();
|
|
815
|
-
|
|
816
|
-
// Also schedule a delayed focus as backup to ensure it sticks
|
|
817
|
-
// (some browsers may need this if DOM isn't fully settled)
|
|
818
|
-
setTimeout(() => {
|
|
819
|
-
this.element?.focus();
|
|
820
|
-
}, 0);
|
|
821
|
-
}
|
|
858
|
+
if (this.isOpen && this.element) this.element.focus();
|
|
822
859
|
}
|
|
823
860
|
|
|
824
861
|
/**
|
|
@@ -1035,16 +1072,18 @@ export class Terminal implements ITerminalCore {
|
|
|
1035
1072
|
}
|
|
1036
1073
|
}
|
|
1037
1074
|
|
|
1038
|
-
/**
|
|
1039
|
-
* Scroll viewport to a specific line in the buffer
|
|
1040
|
-
* @param line Line number (0 = top of scrollback, scrollbackLength = bottom)
|
|
1041
|
-
*/
|
|
1042
1075
|
/** Scrolls so that absolute buffer line `line` (0 = oldest scrollback line) is at the top, like xterm.js. */
|
|
1043
1076
|
public scrollToLine(line: number): void {
|
|
1044
|
-
this.wake();
|
|
1045
1077
|
const scrollbackLength = this.getScrollbackLength();
|
|
1046
1078
|
const top = Math.max(0, Math.min(scrollbackLength, Math.round(line)));
|
|
1047
|
-
|
|
1079
|
+
this.scrollToViewportY(scrollbackLength - top);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** Scrolls to `viewportY` lines above the bottom. */
|
|
1083
|
+
private scrollToViewportY(viewportY: number): void {
|
|
1084
|
+
this.wake();
|
|
1085
|
+
const scrollbackLength = this.getScrollbackLength();
|
|
1086
|
+
const newViewportY = Math.max(0, Math.min(scrollbackLength, Math.round(viewportY)));
|
|
1048
1087
|
|
|
1049
1088
|
if (newViewportY !== this.viewportY) {
|
|
1050
1089
|
this.viewportY = newViewportY;
|
|
@@ -1201,6 +1240,7 @@ export class Terminal implements ITerminalCore {
|
|
|
1201
1240
|
this.scrollEmitter.dispose();
|
|
1202
1241
|
this.renderEmitter.dispose();
|
|
1203
1242
|
this.cursorMoveEmitter.dispose();
|
|
1243
|
+
this.writeParsedEmitter.dispose();
|
|
1204
1244
|
}
|
|
1205
1245
|
|
|
1206
1246
|
// ==========================================================================
|
|
@@ -1308,6 +1348,8 @@ export class Terminal implements ITerminalCore {
|
|
|
1308
1348
|
this.element.removeEventListener('mousedown', this.handleMouseDown, { capture: true });
|
|
1309
1349
|
this.element.removeEventListener('mousemove', this.handleMouseMove);
|
|
1310
1350
|
this.element.removeEventListener('mouseleave', this.handleMouseLeave);
|
|
1351
|
+
this.element.removeEventListener('focusin', this.handleFocusIn);
|
|
1352
|
+
this.element.removeEventListener('focusout', this.handleFocusOut);
|
|
1311
1353
|
this.element.removeEventListener('click', this.handleClick);
|
|
1312
1354
|
|
|
1313
1355
|
// Remove contenteditable and accessibility attributes added in open()
|
|
@@ -1663,6 +1705,8 @@ export class Terminal implements ITerminalCore {
|
|
|
1663
1705
|
deltaLines = e.deltaY / 33;
|
|
1664
1706
|
}
|
|
1665
1707
|
|
|
1708
|
+
deltaLines *= this.options.scrollSensitivity;
|
|
1709
|
+
|
|
1666
1710
|
// Use smooth scrolling for any amount (no rounding needed)
|
|
1667
1711
|
if (deltaLines !== 0) {
|
|
1668
1712
|
// Calculate target position
|
|
@@ -1728,7 +1772,7 @@ export class Terminal implements ITerminalCore {
|
|
|
1728
1772
|
const relativeY = mouseY - scrollbarPadding;
|
|
1729
1773
|
const scrollFraction = 1 - relativeY / scrollbarTrackHeight; // Inverted: top = 1, bottom = 0
|
|
1730
1774
|
const targetViewportY = Math.round(scrollFraction * scrollbackLength);
|
|
1731
|
-
this.
|
|
1775
|
+
this.scrollToViewportY(targetViewportY);
|
|
1732
1776
|
}
|
|
1733
1777
|
}
|
|
1734
1778
|
};
|
|
@@ -1786,7 +1830,7 @@ export class Terminal implements ITerminalCore {
|
|
|
1786
1830
|
const viewportDelta = Math.round(scrollFraction * scrollbackLength);
|
|
1787
1831
|
|
|
1788
1832
|
const newViewportY = this.scrollbarDragStartViewportY + viewportDelta;
|
|
1789
|
-
this.
|
|
1833
|
+
this.scrollToViewportY(newViewportY);
|
|
1790
1834
|
}
|
|
1791
1835
|
|
|
1792
1836
|
/**
|
|
@@ -1965,6 +2009,30 @@ export class Terminal implements ITerminalCore {
|
|
|
1965
2009
|
};
|
|
1966
2010
|
}
|
|
1967
2011
|
|
|
2012
|
+
private focused = false;
|
|
2013
|
+
|
|
2014
|
+
private readonly handleFocusIn = (): void => this.setFocused(true);
|
|
2015
|
+
|
|
2016
|
+
private readonly handleFocusOut = (event: FocusEvent): void => {
|
|
2017
|
+
const next = event.relatedTarget;
|
|
2018
|
+
if (next instanceof Node && this.element?.contains(next)) return;
|
|
2019
|
+
this.setFocused(false);
|
|
2020
|
+
};
|
|
2021
|
+
|
|
2022
|
+
private setFocused(focused: boolean): void {
|
|
2023
|
+
if (focused === this.focused) return;
|
|
2024
|
+
this.focused = focused;
|
|
2025
|
+
this.renderer?.setFocused(focused);
|
|
2026
|
+
this.requestFrame();
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
/** Draws one frame, for a change that needs no follow-up frames (a cursor blink). */
|
|
2030
|
+
private requestFrame(): void {
|
|
2031
|
+
if (this.animationFrameId) return;
|
|
2032
|
+
this.idleFrames = Terminal.IDLE_FRAMES_BEFORE_SLEEP;
|
|
2033
|
+
if (this.isOpen && !this.isDisposed) this.startRenderLoop();
|
|
2034
|
+
}
|
|
2035
|
+
|
|
1968
2036
|
/** Keeps the render loop running for a while; called whenever something may need drawing. */
|
|
1969
2037
|
public wake(): void {
|
|
1970
2038
|
this.idleFrames = 0;
|
|
@@ -1999,6 +2067,19 @@ export class Terminal implements ITerminalCore {
|
|
|
1999
2067
|
(this.buffer as BufferNamespace)._fireBufferChange(this.buffer.active);
|
|
2000
2068
|
}
|
|
2001
2069
|
for (const marker of [...this.markers]) marker.refresh();
|
|
2070
|
+
this.writeParsedEmitter.fire();
|
|
2071
|
+
this.wake();
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
private searchHighlights: {
|
|
2075
|
+
list: SearchHighlight[] | null;
|
|
2076
|
+
colors?: SearchHighlightColors;
|
|
2077
|
+
} = { list: null };
|
|
2078
|
+
|
|
2079
|
+
/** Shows search matches on viewport rows (null clears them). Used by SearchAddon. */
|
|
2080
|
+
setSearchHighlights(list: SearchHighlight[] | null, colors?: SearchHighlightColors): void {
|
|
2081
|
+
this.searchHighlights = { list, colors: colors ?? this.searchHighlights.colors };
|
|
2082
|
+
this.renderer?.setSearchHighlights(list, colors);
|
|
2002
2083
|
this.wake();
|
|
2003
2084
|
}
|
|
2004
2085
|
|
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
|
-
|
|
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
|
+
}
|