@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/README.md +6 -1
- package/lib/addons/search.ts +190 -0
- package/lib/box-glyphs.ts +278 -0
- package/lib/ghostty.ts +465 -129
- package/lib/glyph-ink.ts +42 -0
- package/lib/index.ts +8 -1
- package/lib/input-handler.ts +55 -2
- package/lib/interfaces.ts +29 -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 +781 -686
- package/lib/selection-manager.ts +8 -3
- package/lib/terminal.ts +205 -85
- package/lib/types.ts +2 -1
- package/lib/webgl-instances.ts +69 -0
- package/lib/webgl-renderer.ts +943 -0
- package/package.json +2 -2
package/lib/selection-manager.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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:
|
|
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 {
|
|
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?:
|
|
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
|
-
|
|
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
|
|
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) ?
|
|
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
|
-
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
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
|
|
482
|
-
height: renderer
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|