@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/ghostty.ts
CHANGED
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
* grid references and key encoder through `Abi`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
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
|
-
|
|
236
|
-
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
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
|
|
945
|
-
const
|
|
946
|
-
const
|
|
947
|
-
const
|
|
948
|
-
const
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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 ===
|
|
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 =
|
|
1030
|
-
|
|
1031
|
-
cell.
|
|
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
|
|
1042
|
-
const tag = view.getInt32(ptr +
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
|