@floegence/floeterm-terminal-web 0.5.0 → 0.5.2
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/dist/core/TerminalCore.d.ts +19 -0
- package/dist/core/TerminalCore.d.ts.map +1 -1
- package/dist/core/TerminalCore.js +414 -41
- package/dist/core/TerminalCore.js.map +1 -1
- package/dist/core/TerminalRenderScheduler.d.ts +4 -0
- package/dist/core/TerminalRenderScheduler.d.ts.map +1 -1
- package/dist/core/TerminalRenderScheduler.js +35 -2
- package/dist/core/TerminalRenderScheduler.js.map +1 -1
- package/dist/fabric/BeamtermFabricRenderer.d.ts +59 -0
- package/dist/fabric/BeamtermFabricRenderer.d.ts.map +1 -0
- package/dist/fabric/BeamtermFabricRenderer.js +448 -0
- package/dist/fabric/BeamtermFabricRenderer.js.map +1 -0
- package/dist/fabric/TerminalFabricCoordinator.d.ts +40 -0
- package/dist/fabric/TerminalFabricCoordinator.d.ts.map +1 -0
- package/dist/fabric/TerminalFabricCoordinator.js +140 -0
- package/dist/fabric/TerminalFabricCoordinator.js.map +1 -0
- package/dist/fabric/TerminalLiveFabric.d.ts +84 -0
- package/dist/fabric/TerminalLiveFabric.d.ts.map +1 -0
- package/dist/fabric/TerminalLiveFabric.js +211 -0
- package/dist/fabric/TerminalLiveFabric.js.map +1 -0
- package/dist/fabric/types.d.ts +112 -0
- package/dist/fabric/types.d.ts.map +1 -0
- package/dist/fabric/types.js +2 -0
- package/dist/fabric/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/TerminalInitializationScheduler.d.ts +22 -0
- package/dist/internal/TerminalInitializationScheduler.d.ts.map +1 -0
- package/dist/internal/TerminalInitializationScheduler.js +79 -0
- package/dist/internal/TerminalInitializationScheduler.js.map +1 -0
- package/dist/internal/scheduleUiTurn.d.ts +5 -0
- package/dist/internal/scheduleUiTurn.d.ts.map +1 -0
- package/dist/internal/scheduleUiTurn.js +37 -0
- package/dist/internal/scheduleUiTurn.js.map +1 -0
- package/dist/manager/TerminalInstanceController.d.ts +1 -0
- package/dist/manager/TerminalInstanceController.d.ts.map +1 -1
- package/dist/manager/TerminalInstanceController.js +67 -30
- package/dist/manager/TerminalInstanceController.js.map +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +5 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { filterXtermAutoResponses } from '../utils/xtermAutoResponseFilter';
|
|
2
2
|
import { createConsoleLogger, noopLogger } from '../utils/logger';
|
|
3
|
+
import { cursorToFabricCursor, mapGhosttyRowToFabricCells, renderReasonFromForce, terminalLiveFabric, themeToFabricTheme, } from '../fabric/TerminalLiveFabric';
|
|
4
|
+
import { terminalFabricCoordinator } from '../fabric/TerminalFabricCoordinator';
|
|
3
5
|
import { TerminalState, } from '../types';
|
|
4
6
|
import { resolveTerminalInputElement, TerminalInputBridge } from './TerminalInputBridge';
|
|
5
7
|
import { terminalRenderScheduler } from './TerminalRenderScheduler';
|
|
8
|
+
import { scheduleUiTurn } from '../internal/scheduleUiTurn';
|
|
6
9
|
const TERMINAL_SEARCH_MAX_RESULTS = 5000;
|
|
7
10
|
const PRESENTATION_SCALE_EPSILON = 0.0001;
|
|
8
11
|
const GHOSTTY_DEFAULT_SCROLLBAR_RESERVE_PX = 15;
|
|
@@ -77,6 +80,25 @@ function resolveCanvasLocalDisplaySize(canvas, coordinateRoot) {
|
|
|
77
80
|
dpr: Number.isFinite(dpr) && dpr > 0 ? dpr : 1,
|
|
78
81
|
};
|
|
79
82
|
}
|
|
83
|
+
function syncCanvasOverlaySize(overlay, termCanvas, coordinateRoot) {
|
|
84
|
+
const { cssWidth, cssHeight, dpr } = resolveCanvasLocalDisplaySize(termCanvas, coordinateRoot);
|
|
85
|
+
if (overlay.canvas.width === termCanvas.width
|
|
86
|
+
&& overlay.canvas.height === termCanvas.height
|
|
87
|
+
&& overlay.cssWidth === cssWidth
|
|
88
|
+
&& overlay.cssHeight === cssHeight
|
|
89
|
+
&& overlay.dpr === dpr) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
overlay.dpr = dpr;
|
|
93
|
+
overlay.cssWidth = cssWidth;
|
|
94
|
+
overlay.cssHeight = cssHeight;
|
|
95
|
+
overlay.canvas.style.width = `${cssWidth}px`;
|
|
96
|
+
overlay.canvas.style.height = `${cssHeight}px`;
|
|
97
|
+
overlay.canvas.width = termCanvas.width;
|
|
98
|
+
overlay.canvas.height = termCanvas.height;
|
|
99
|
+
overlay.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
100
|
+
overlay.ctx.scale(dpr, dpr);
|
|
101
|
+
}
|
|
80
102
|
const getPerfProbe = () => {
|
|
81
103
|
if (typeof window === 'undefined') {
|
|
82
104
|
return undefined;
|
|
@@ -240,6 +262,11 @@ function canSkipCachedRowRender(renderer, row) {
|
|
|
240
262
|
}
|
|
241
263
|
return true;
|
|
242
264
|
}
|
|
265
|
+
function canSkipNativeCanvasRowRender(renderer, row, fabricActive) {
|
|
266
|
+
void renderer;
|
|
267
|
+
void row;
|
|
268
|
+
return fabricActive;
|
|
269
|
+
}
|
|
243
270
|
function samePresentationScale(left, right) {
|
|
244
271
|
return Math.abs(left - right) <= PRESENTATION_SCALE_EPSILON;
|
|
245
272
|
}
|
|
@@ -268,11 +295,61 @@ function normalizeNonNegativePixels(value, fallback) {
|
|
|
268
295
|
function hasUsableClientSize(element) {
|
|
269
296
|
return Boolean(element && element.clientWidth > 0 && element.clientHeight > 0);
|
|
270
297
|
}
|
|
298
|
+
function formatCssPixelValue(value) {
|
|
299
|
+
const normalized = Number.isFinite(value) ? Math.max(1, value) : 1;
|
|
300
|
+
return `${Math.round(normalized * 1000) / 1000}px`;
|
|
301
|
+
}
|
|
271
302
|
function hasTransientRendererState(renderer) {
|
|
272
303
|
return Boolean(renderer.currentSelectionCoords
|
|
273
304
|
|| (renderer.hoveredHyperlinkId ?? 0) > 0
|
|
274
305
|
|| renderer.hoveredLinkRange);
|
|
275
306
|
}
|
|
307
|
+
function forceTransientRowsOnExit(renderer) {
|
|
308
|
+
const active = hasTransientRendererState(renderer);
|
|
309
|
+
const previous = Boolean(renderer.__floetermRowRenderCacheHadTransientState);
|
|
310
|
+
renderer.__floetermRowRenderCacheHadTransientState = active;
|
|
311
|
+
return previous && !active;
|
|
312
|
+
}
|
|
313
|
+
function normalizeNumberField(record, key) {
|
|
314
|
+
if (!record || typeof record !== 'object') {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const value = Number(record[key]);
|
|
318
|
+
return Number.isFinite(value) ? value : null;
|
|
319
|
+
}
|
|
320
|
+
function normalizeSelectionRange(value) {
|
|
321
|
+
const startCol = normalizeNumberField(value, 'startCol');
|
|
322
|
+
const startRow = normalizeNumberField(value, 'startRow');
|
|
323
|
+
const endCol = normalizeNumberField(value, 'endCol');
|
|
324
|
+
const endRow = normalizeNumberField(value, 'endRow');
|
|
325
|
+
if (startCol === null || startRow === null || endCol === null || endRow === null) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
startCol: Math.max(0, Math.floor(startCol)),
|
|
330
|
+
startRow: Math.max(0, Math.floor(startRow)),
|
|
331
|
+
endCol: Math.max(0, Math.floor(endCol)),
|
|
332
|
+
endRow: Math.max(0, Math.floor(endRow)),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function normalizeHoverRange(value) {
|
|
336
|
+
const startX = normalizeNumberField(value, 'startX');
|
|
337
|
+
const startY = normalizeNumberField(value, 'startY');
|
|
338
|
+
const endX = normalizeNumberField(value, 'endX');
|
|
339
|
+
const endY = normalizeNumberField(value, 'endY');
|
|
340
|
+
if (startX === null || startY === null || endX === null || endY === null) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
startX: Math.max(0, Math.floor(startX)),
|
|
345
|
+
startY: Math.max(0, Math.floor(startY)),
|
|
346
|
+
endX: Math.max(0, Math.floor(endX)),
|
|
347
|
+
endY: Math.max(0, Math.floor(endY)),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function normalizeFabricIdentifier(value) {
|
|
351
|
+
return value.trim().replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'terminal';
|
|
352
|
+
}
|
|
276
353
|
// TerminalCore provides a focused wrapper around ghostty-web (xterm.js API-compatible) and its fit addon.
|
|
277
354
|
export class TerminalCore {
|
|
278
355
|
constructor(container, config = {}, eventHandlers = {}, logger = createConsoleLogger()) {
|
|
@@ -282,6 +359,9 @@ export class TerminalCore {
|
|
|
282
359
|
this.fitAddon = null;
|
|
283
360
|
this.needsFullRenderOnNextWrite = false;
|
|
284
361
|
this.demandRenderForceAll = false;
|
|
362
|
+
this.fabricView = null;
|
|
363
|
+
this.fabricAttachSeq = 0;
|
|
364
|
+
this.cancelFabricAttachSchedule = null;
|
|
285
365
|
this.viewportHost = null;
|
|
286
366
|
this.renderHost = null;
|
|
287
367
|
this.resizeObserver = null;
|
|
@@ -345,6 +425,7 @@ export class TerminalCore {
|
|
|
345
425
|
this.renderDemandFrame(forceAll);
|
|
346
426
|
},
|
|
347
427
|
};
|
|
428
|
+
this.fabricViewId = `floeterm-view-${this.renderTask.id}`;
|
|
348
429
|
TerminalCore.nextRenderTaskId += 1;
|
|
349
430
|
}
|
|
350
431
|
// initialize creates the ghostty-web terminal instance and binds addons.
|
|
@@ -459,6 +540,10 @@ export class TerminalCore {
|
|
|
459
540
|
fitAddon.proposeDimensions = () => this.proposeFitDimensions();
|
|
460
541
|
}
|
|
461
542
|
proposeFitDimensions() {
|
|
543
|
+
const fabricDimensions = this.proposeFabricFitDimensions();
|
|
544
|
+
if (fabricDimensions) {
|
|
545
|
+
return fabricDimensions;
|
|
546
|
+
}
|
|
462
547
|
const terminalAny = this.terminal;
|
|
463
548
|
const renderer = terminalAny?.renderer;
|
|
464
549
|
const metrics = typeof renderer?.getMetrics === 'function' ? renderer.getMetrics() : null;
|
|
@@ -488,12 +573,90 @@ export class TerminalCore {
|
|
|
488
573
|
rows: Math.max(MIN_TERMINAL_ROWS, Math.floor(availableHeight / cellHeight)),
|
|
489
574
|
};
|
|
490
575
|
}
|
|
576
|
+
proposeFabricFitDimensions() {
|
|
577
|
+
if (this.config.rendererType !== 'webgl') {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
const geometry = this.fabricView?.renderer.getGeometry();
|
|
581
|
+
if (!geometry) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
const cols = Math.floor(Number(geometry.cols));
|
|
585
|
+
const rows = Math.floor(Number(geometry.rows));
|
|
586
|
+
if (!Number.isFinite(cols) || !Number.isFinite(rows) || cols <= 0 || rows <= 0) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
cols: Math.max(MIN_TERMINAL_COLS, cols),
|
|
591
|
+
rows: Math.max(MIN_TERMINAL_ROWS, rows),
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
syncGhosttyInputGeometryToFabric() {
|
|
595
|
+
if (!this.terminal || !this.isFabricRendererActive()) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const geometry = this.fabricView?.renderer.getGeometry();
|
|
599
|
+
if (!geometry) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const terminalAny = this.terminal;
|
|
603
|
+
const renderer = terminalAny.renderer;
|
|
604
|
+
const canvas = renderer?.getCanvas?.();
|
|
605
|
+
const cols = Math.floor(Number(terminalAny.cols ?? geometry.cols));
|
|
606
|
+
const rows = Math.floor(Number(terminalAny.rows ?? geometry.rows));
|
|
607
|
+
const cellWidth = Number(geometry.cellWidth);
|
|
608
|
+
const cellHeight = Number(geometry.cellHeight);
|
|
609
|
+
if (!renderer
|
|
610
|
+
|| !canvas
|
|
611
|
+
|| !Number.isFinite(cols)
|
|
612
|
+
|| !Number.isFinite(rows)
|
|
613
|
+
|| !Number.isFinite(cellWidth)
|
|
614
|
+
|| !Number.isFinite(cellHeight)
|
|
615
|
+
|| cols <= 0
|
|
616
|
+
|| rows <= 0
|
|
617
|
+
|| cellWidth <= 0
|
|
618
|
+
|| cellHeight <= 0) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const currentMetrics = renderer.getMetrics?.() ?? renderer.metrics ?? {};
|
|
622
|
+
const baselineRatio = Number(currentMetrics.height) > 0
|
|
623
|
+
? Number(currentMetrics.baseline ?? cellHeight * 0.8) / Number(currentMetrics.height)
|
|
624
|
+
: 0.8;
|
|
625
|
+
const nextMetrics = {
|
|
626
|
+
...currentMetrics,
|
|
627
|
+
width: cellWidth,
|
|
628
|
+
height: cellHeight,
|
|
629
|
+
baseline: Math.max(1, Math.round(cellHeight * baselineRatio)),
|
|
630
|
+
};
|
|
631
|
+
renderer.metrics = nextMetrics;
|
|
632
|
+
const gridWidth = cols * cellWidth;
|
|
633
|
+
const gridHeight = rows * cellHeight;
|
|
634
|
+
canvas.style.width = formatCssPixelValue(gridWidth);
|
|
635
|
+
canvas.style.height = formatCssPixelValue(gridHeight);
|
|
636
|
+
canvas.style.maxWidth = 'none';
|
|
637
|
+
canvas.style.maxHeight = 'none';
|
|
638
|
+
const canvasDpr = canvas.width > 0 && gridWidth > 0
|
|
639
|
+
? canvas.width / gridWidth
|
|
640
|
+
: window.devicePixelRatio ?? 1;
|
|
641
|
+
const dpr = Number.isFinite(canvasDpr) && canvasDpr > 0 ? canvasDpr : 1;
|
|
642
|
+
const expectedWidth = Math.max(1, Math.round(gridWidth * dpr));
|
|
643
|
+
const expectedHeight = Math.max(1, Math.round(gridHeight * dpr));
|
|
644
|
+
if (canvas.width !== expectedWidth || canvas.height !== expectedHeight) {
|
|
645
|
+
canvas.width = expectedWidth;
|
|
646
|
+
canvas.height = expectedHeight;
|
|
647
|
+
}
|
|
648
|
+
canvas.style.position = 'absolute';
|
|
649
|
+
canvas.style.left = '0';
|
|
650
|
+
canvas.style.top = '0';
|
|
651
|
+
canvas.style.inset = '0 auto auto 0';
|
|
652
|
+
canvas.style.zIndex = '1';
|
|
653
|
+
}
|
|
491
654
|
resolveFitElement(terminalElement) {
|
|
492
655
|
const candidates = [
|
|
493
|
-
terminalElement,
|
|
494
656
|
this.renderHost,
|
|
495
657
|
this.viewportHost,
|
|
496
658
|
this.container,
|
|
659
|
+
terminalElement,
|
|
497
660
|
];
|
|
498
661
|
return candidates.find(hasUsableClientSize) ?? terminalElement ?? this.renderHost ?? this.container;
|
|
499
662
|
}
|
|
@@ -591,16 +754,26 @@ export class TerminalCore {
|
|
|
591
754
|
const viewportChanged = Number.isFinite(incomingViewportY)
|
|
592
755
|
&& Number.isFinite(currentViewportY)
|
|
593
756
|
&& incomingViewportY !== currentViewportY;
|
|
594
|
-
|
|
757
|
+
const forceTransientExit = forceTransientRowsOnExit(renderer);
|
|
758
|
+
const forceAll = Boolean(args[1]) || viewportChanged || forceTransientExit;
|
|
759
|
+
renderer.__floetermRowRenderCacheForceAll = forceAll;
|
|
760
|
+
const fabricFrame = this.beginFabricFrame(forceAll, args[0]);
|
|
595
761
|
try {
|
|
596
|
-
|
|
762
|
+
const renderArgs = [...args];
|
|
763
|
+
renderArgs[1] = forceAll;
|
|
764
|
+
return originalRender(...renderArgs);
|
|
597
765
|
}
|
|
598
766
|
finally {
|
|
767
|
+
this.finishFabricFrame(fabricFrame);
|
|
599
768
|
renderer.__floetermRowRenderCacheForceAll = false;
|
|
600
769
|
}
|
|
601
770
|
};
|
|
602
771
|
renderer.renderLine = (cells, row, cols) => {
|
|
603
772
|
const themedCells = this.translateCellsForTheme(cells);
|
|
773
|
+
const fabricActive = this.writeFabricRow(renderer, row, themedCells, cols);
|
|
774
|
+
if (canSkipNativeCanvasRowRender(renderer, row, fabricActive)) {
|
|
775
|
+
return undefined;
|
|
776
|
+
}
|
|
604
777
|
const signature = buildRowSignature(renderer, themedCells, row, cols);
|
|
605
778
|
if (canSkipCachedRowRender(renderer, row) && cache.get(row) === signature) {
|
|
606
779
|
return undefined;
|
|
@@ -614,6 +787,95 @@ export class TerminalCore {
|
|
|
614
787
|
return result;
|
|
615
788
|
};
|
|
616
789
|
}
|
|
790
|
+
beginFabricFrame(forceAll, buffer) {
|
|
791
|
+
const renderer = this.fabricView?.renderer;
|
|
792
|
+
if (!renderer?.isActive()) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
const dimensions = this.resolveFabricDimensions(buffer);
|
|
796
|
+
if (!dimensions) {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
const frame = terminalFabricCoordinator.beginFrame(renderReasonFromForce(forceAll), forceAll);
|
|
800
|
+
const theme = this.resolveFabricTheme();
|
|
801
|
+
renderer.startFrame(frame, {
|
|
802
|
+
cols: dimensions.cols,
|
|
803
|
+
rows: dimensions.rows,
|
|
804
|
+
theme: themeToFabricTheme(theme),
|
|
805
|
+
});
|
|
806
|
+
return frame;
|
|
807
|
+
}
|
|
808
|
+
resolveFabricDimensions(buffer) {
|
|
809
|
+
const raw = buffer;
|
|
810
|
+
const dimensions = raw?.getDimensions?.();
|
|
811
|
+
const cols = Math.floor(Number(dimensions?.cols ?? this.terminal?.cols ?? 0));
|
|
812
|
+
const rows = Math.floor(Number(dimensions?.rows ?? this.terminal?.rows ?? 0));
|
|
813
|
+
if (!Number.isFinite(cols) || !Number.isFinite(rows) || cols <= 0 || rows <= 0) {
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
return { cols, rows };
|
|
817
|
+
}
|
|
818
|
+
writeFabricRow(renderer, row, cells, cols) {
|
|
819
|
+
const fabricRenderer = this.fabricView?.renderer;
|
|
820
|
+
if (!fabricRenderer?.isActive()) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
fabricRenderer.writeRow(row, mapGhosttyRowToFabricCells(renderer, row, cells, cols, this.resolveFabricRowRenderHints(renderer)), cols);
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
resolveFabricRowRenderHints(renderer) {
|
|
827
|
+
return {
|
|
828
|
+
selection: this.resolveFabricSelectionHint(renderer),
|
|
829
|
+
hover: {
|
|
830
|
+
hyperlinkId: Number(renderer.hoveredHyperlinkId ?? 0),
|
|
831
|
+
range: normalizeHoverRange(renderer.hoveredLinkRange),
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
resolveFabricSelectionHint(renderer) {
|
|
836
|
+
const range = normalizeSelectionRange(renderer.currentSelectionCoords);
|
|
837
|
+
if (!range) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
const theme = this.resolveFabricTheme();
|
|
841
|
+
return {
|
|
842
|
+
...range,
|
|
843
|
+
background: parseThemeColor(theme.selectionBackground ?? TERMINAL_SELECTION_BACKGROUND)
|
|
844
|
+
?? parseThemeColor(TERMINAL_SELECTION_BACKGROUND),
|
|
845
|
+
foreground: parseThemeColor(theme.selectionForeground ?? TERMINAL_SELECTION_FOREGROUND)
|
|
846
|
+
?? parseThemeColor(TERMINAL_SELECTION_FOREGROUND),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
syncFabricSelectionState() {
|
|
850
|
+
const renderer = this.terminal?.renderer;
|
|
851
|
+
const selectionManager = this.terminal?.selectionManager;
|
|
852
|
+
if (!renderer || !selectionManager) {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
renderer.currentSelectionCoords = selectionManager.hasSelection?.()
|
|
856
|
+
? selectionManager.getSelectionCoords?.() ?? null
|
|
857
|
+
: null;
|
|
858
|
+
renderer.__floetermRowRenderCache?.clear();
|
|
859
|
+
this.demandRenderForceAll = true;
|
|
860
|
+
}
|
|
861
|
+
finishFabricFrame(frame) {
|
|
862
|
+
const fabricRenderer = this.fabricView?.renderer;
|
|
863
|
+
if (!frame || !fabricRenderer?.isActive()) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const terminalAny = this.terminal;
|
|
867
|
+
const result = fabricRenderer.finishFrame(cursorToFabricCursor(terminalAny?.wasmTerm?.getCursor?.()));
|
|
868
|
+
if (result.rendered) {
|
|
869
|
+
terminalFabricCoordinator.completeFrame(frame, result.renderedRows, result.dirtyCells);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
resolveFabricTheme() {
|
|
873
|
+
if (this.searchQuery.trim().length > 0 && this.searchThemeRestore) {
|
|
874
|
+
return this.applySearchThemeOverride(this.searchThemeRestore);
|
|
875
|
+
}
|
|
876
|
+
const configured = mapThemeToGhostty(this.config.theme);
|
|
877
|
+
return Object.keys(configured).length > 0 ? configured : this.terminalThemeSource;
|
|
878
|
+
}
|
|
617
879
|
translateCellsForTheme(cells) {
|
|
618
880
|
const translator = this.themeColorTranslator;
|
|
619
881
|
if (!translator) {
|
|
@@ -706,9 +968,72 @@ export class TerminalCore {
|
|
|
706
968
|
: null;
|
|
707
969
|
selectionManager.requestRender = () => {
|
|
708
970
|
originalRequestRender?.();
|
|
971
|
+
this.syncFabricSelectionState();
|
|
709
972
|
this.requestDemandRender(false);
|
|
710
973
|
};
|
|
711
974
|
}
|
|
975
|
+
async attachFabricRenderer() {
|
|
976
|
+
if (this.fabricView || this.config.rendererType !== 'webgl' || !this.terminal) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const terminalAny = this.terminal;
|
|
980
|
+
const theme = Object.keys(this.terminalThemeSource).length > 0
|
|
981
|
+
? this.terminalThemeSource
|
|
982
|
+
: mapThemeToGhostty(this.config.theme);
|
|
983
|
+
const fontFamily = typeof terminalAny.options?.fontFamily === 'string'
|
|
984
|
+
? terminalAny.options.fontFamily
|
|
985
|
+
: typeof this.config.fontFamily === 'string'
|
|
986
|
+
? this.config.fontFamily
|
|
987
|
+
: '"JetBrains Mono", monospace';
|
|
988
|
+
const fontSize = Number(terminalAny.options?.fontSize ?? this.resolveEffectiveFontSize());
|
|
989
|
+
const sessionId = normalizeFabricIdentifier(typeof this.config.sessionId === 'string' ? this.config.sessionId : this.fabricViewId);
|
|
990
|
+
this.fabricView = await terminalLiveFabric.attachView({
|
|
991
|
+
sessionId,
|
|
992
|
+
viewId: this.fabricViewId,
|
|
993
|
+
container: this.renderHost ?? this.container,
|
|
994
|
+
logger: this.logger,
|
|
995
|
+
fontFamily,
|
|
996
|
+
fontSize: Number.isFinite(fontSize) && fontSize > 0 ? fontSize : this.resolveEffectiveFontSize(),
|
|
997
|
+
theme,
|
|
998
|
+
getGhosttyCanvas: () => terminalAny.renderer?.getCanvas?.() ?? null,
|
|
999
|
+
focusInputSurface: () => this.inputBridge?.focus(),
|
|
1000
|
+
forwardWheel: event => {
|
|
1001
|
+
this.container.dispatchEvent(new WheelEvent(event.type, event));
|
|
1002
|
+
},
|
|
1003
|
+
});
|
|
1004
|
+
this.fabricView.renderer.setAppearance({ theme, fontFamily, fontSize });
|
|
1005
|
+
this.performResize('force');
|
|
1006
|
+
this.syncGhosttyInputGeometryToFabric();
|
|
1007
|
+
this.forceFullRender();
|
|
1008
|
+
}
|
|
1009
|
+
scheduleFabricRendererAttach() {
|
|
1010
|
+
if (this.config.rendererType !== 'webgl' || this.fabricView || !this.terminal) {
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (this.cancelFabricAttachSchedule !== null) {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
const seq = ++this.fabricAttachSeq;
|
|
1017
|
+
const attach = () => {
|
|
1018
|
+
this.cancelFabricAttachSchedule = null;
|
|
1019
|
+
if (this.isDisposed || seq !== this.fabricAttachSeq || this.fabricView) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
this.attachFabricRenderer().catch(error => {
|
|
1023
|
+
if (this.isDisposed || seq !== this.fabricAttachSeq) {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
this.logger.warn('[TerminalCore] Beamterm live fabric attach failed; keeping ghostty canvas live', { error });
|
|
1027
|
+
});
|
|
1028
|
+
};
|
|
1029
|
+
this.cancelFabricAttachSchedule = scheduleUiTurn(() => {
|
|
1030
|
+
if (this.isDisposed || seq !== this.fabricAttachSeq || this.fabricView) {
|
|
1031
|
+
this.cancelFabricAttachSchedule = null;
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
this.cancelFabricAttachSchedule = scheduleUiTurn(attach);
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
712
1037
|
setupInputBridge() {
|
|
713
1038
|
this.disposeInputBridge();
|
|
714
1039
|
const input = resolveTerminalInputElement(this.container);
|
|
@@ -853,6 +1178,7 @@ export class TerminalCore {
|
|
|
853
1178
|
}
|
|
854
1179
|
if (typeof this.terminal.onSelectionChange === 'function') {
|
|
855
1180
|
const disposable = this.terminal.onSelectionChange(() => {
|
|
1181
|
+
this.syncFabricSelectionState();
|
|
856
1182
|
this.requestDemandRender(false);
|
|
857
1183
|
});
|
|
858
1184
|
this.trackTerminalEventDisposable(disposable);
|
|
@@ -950,7 +1276,16 @@ export class TerminalCore {
|
|
|
950
1276
|
const startSeq = this.resizeNotifySeq;
|
|
951
1277
|
const before = this.lastNotifiedSize;
|
|
952
1278
|
try {
|
|
1279
|
+
if (this.isFabricRendererActive()) {
|
|
1280
|
+
this.resizeFabricToHost();
|
|
1281
|
+
}
|
|
953
1282
|
this.fitAddon.fit();
|
|
1283
|
+
if (this.isFabricRendererActive()) {
|
|
1284
|
+
this.syncGhosttyInputGeometryToFabric();
|
|
1285
|
+
}
|
|
1286
|
+
if (!this.isFabricRendererActive()) {
|
|
1287
|
+
this.resizeFabricToHost();
|
|
1288
|
+
}
|
|
954
1289
|
}
|
|
955
1290
|
catch (error) {
|
|
956
1291
|
this.logger.debug('[TerminalCore] Resize failed', { error });
|
|
@@ -979,6 +1314,7 @@ export class TerminalCore {
|
|
|
979
1314
|
if (changed) {
|
|
980
1315
|
try {
|
|
981
1316
|
this.terminal.resize(dims.cols, dims.rows);
|
|
1317
|
+
this.resizeFabricToHost();
|
|
982
1318
|
}
|
|
983
1319
|
catch (error) {
|
|
984
1320
|
this.logger.debug('[TerminalCore] Fixed-dimension resize failed', { error });
|
|
@@ -1610,6 +1946,7 @@ export class TerminalCore {
|
|
|
1610
1946
|
const restore = this.searchThemeRestore;
|
|
1611
1947
|
this.searchThemeRestore = null;
|
|
1612
1948
|
this.applyRendererTheme(terminalAny, restore);
|
|
1949
|
+
this.forceFullRender();
|
|
1613
1950
|
}
|
|
1614
1951
|
applySearchThemeOverride(theme) {
|
|
1615
1952
|
return {
|
|
@@ -1626,6 +1963,7 @@ export class TerminalCore {
|
|
|
1626
1963
|
this.terminal.options.theme = theme;
|
|
1627
1964
|
}
|
|
1628
1965
|
terminalAny.renderer?.setTheme?.(theme);
|
|
1966
|
+
this.fabricView?.renderer.setAppearance({ theme });
|
|
1629
1967
|
if (this.isVisualRenderSuspended()) {
|
|
1630
1968
|
this.markPendingDemandRender(true);
|
|
1631
1969
|
return;
|
|
@@ -1718,6 +2056,43 @@ export class TerminalCore {
|
|
|
1718
2056
|
}
|
|
1719
2057
|
this.searchOverlay = null;
|
|
1720
2058
|
}
|
|
2059
|
+
createCanvasOverlay(zIndex) {
|
|
2060
|
+
const t = this.terminal;
|
|
2061
|
+
if (!t)
|
|
2062
|
+
return null;
|
|
2063
|
+
const renderer = t?.renderer;
|
|
2064
|
+
const termCanvas = renderer?.getCanvas?.();
|
|
2065
|
+
if (!termCanvas)
|
|
2066
|
+
return null;
|
|
2067
|
+
try {
|
|
2068
|
+
const style = getComputedStyle(this.container);
|
|
2069
|
+
if (style.position === 'static')
|
|
2070
|
+
this.container.style.position = 'relative';
|
|
2071
|
+
}
|
|
2072
|
+
catch {
|
|
2073
|
+
}
|
|
2074
|
+
const overlay = document.createElement('canvas');
|
|
2075
|
+
overlay.style.position = 'absolute';
|
|
2076
|
+
overlay.style.top = '0';
|
|
2077
|
+
overlay.style.left = '0';
|
|
2078
|
+
overlay.style.pointerEvents = 'none';
|
|
2079
|
+
overlay.style.zIndex = zIndex;
|
|
2080
|
+
overlay.style.mixBlendMode = 'normal';
|
|
2081
|
+
const ctx = overlay.getContext('2d');
|
|
2082
|
+
if (!ctx)
|
|
2083
|
+
return null;
|
|
2084
|
+
this.container.appendChild(overlay);
|
|
2085
|
+
const state = {
|
|
2086
|
+
canvas: overlay,
|
|
2087
|
+
ctx,
|
|
2088
|
+
dpr: 1,
|
|
2089
|
+
cssWidth: 0,
|
|
2090
|
+
cssHeight: 0,
|
|
2091
|
+
termCanvas,
|
|
2092
|
+
};
|
|
2093
|
+
syncCanvasOverlaySize(state, termCanvas, this.container);
|
|
2094
|
+
return state;
|
|
2095
|
+
}
|
|
1721
2096
|
ensureSearchOverlay() {
|
|
1722
2097
|
const t = this.terminal;
|
|
1723
2098
|
if (!t)
|
|
@@ -1738,40 +2113,10 @@ export class TerminalCore {
|
|
|
1738
2113
|
this.disposeSearchOverlay();
|
|
1739
2114
|
}
|
|
1740
2115
|
if (!this.searchOverlay) {
|
|
1741
|
-
|
|
1742
|
-
overlay.style.position = 'absolute';
|
|
1743
|
-
overlay.style.top = '0';
|
|
1744
|
-
overlay.style.left = '0';
|
|
1745
|
-
overlay.style.pointerEvents = 'none';
|
|
1746
|
-
overlay.style.zIndex = '5';
|
|
1747
|
-
const ctx = overlay.getContext('2d');
|
|
1748
|
-
if (!ctx)
|
|
1749
|
-
return null;
|
|
1750
|
-
this.container.appendChild(overlay);
|
|
1751
|
-
this.searchOverlay = {
|
|
1752
|
-
canvas: overlay,
|
|
1753
|
-
ctx,
|
|
1754
|
-
dpr: 1,
|
|
1755
|
-
cssWidth: 0,
|
|
1756
|
-
cssHeight: 0,
|
|
1757
|
-
termCanvas
|
|
1758
|
-
};
|
|
2116
|
+
this.searchOverlay = this.createCanvasOverlay('5');
|
|
1759
2117
|
}
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
this.searchOverlay.canvas.height !== termCanvas.height ||
|
|
1763
|
-
this.searchOverlay.cssWidth !== cssWidth ||
|
|
1764
|
-
this.searchOverlay.cssHeight !== cssHeight ||
|
|
1765
|
-
this.searchOverlay.dpr !== dpr) {
|
|
1766
|
-
this.searchOverlay.dpr = dpr;
|
|
1767
|
-
this.searchOverlay.cssWidth = cssWidth;
|
|
1768
|
-
this.searchOverlay.cssHeight = cssHeight;
|
|
1769
|
-
this.searchOverlay.canvas.style.width = `${cssWidth}px`;
|
|
1770
|
-
this.searchOverlay.canvas.style.height = `${cssHeight}px`;
|
|
1771
|
-
this.searchOverlay.canvas.width = termCanvas.width;
|
|
1772
|
-
this.searchOverlay.canvas.height = termCanvas.height;
|
|
1773
|
-
this.searchOverlay.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1774
|
-
this.searchOverlay.ctx.scale(this.searchOverlay.dpr, this.searchOverlay.dpr);
|
|
2118
|
+
if (this.searchOverlay) {
|
|
2119
|
+
syncCanvasOverlaySize(this.searchOverlay, termCanvas, this.container);
|
|
1775
2120
|
}
|
|
1776
2121
|
return this.searchOverlay;
|
|
1777
2122
|
}
|
|
@@ -1788,16 +2133,16 @@ export class TerminalCore {
|
|
|
1788
2133
|
});
|
|
1789
2134
|
}
|
|
1790
2135
|
renderSearchOverlay() {
|
|
1791
|
-
const overlay = this.ensureSearchOverlay();
|
|
1792
|
-
if (!overlay)
|
|
1793
|
-
return;
|
|
1794
2136
|
const t = this.terminal;
|
|
1795
2137
|
const queryActive = this.searchQuery.trim().length > 0;
|
|
1796
2138
|
const rowIndex = this.searchRowIndex;
|
|
1797
2139
|
if (!queryActive || rowIndex.size === 0 || !t) {
|
|
1798
|
-
|
|
2140
|
+
this.searchOverlay?.ctx.clearRect(0, 0, this.searchOverlay.cssWidth, this.searchOverlay.cssHeight);
|
|
1799
2141
|
return;
|
|
1800
2142
|
}
|
|
2143
|
+
const overlay = this.ensureSearchOverlay();
|
|
2144
|
+
if (!overlay)
|
|
2145
|
+
return;
|
|
1801
2146
|
const renderer = t?.renderer;
|
|
1802
2147
|
const charW = typeof renderer?.charWidth === 'number' ? renderer.charWidth : 0;
|
|
1803
2148
|
const charH = typeof renderer?.charHeight === 'number' ? renderer.charHeight : 0;
|
|
@@ -1835,6 +2180,9 @@ export class TerminalCore {
|
|
|
1835
2180
|
}
|
|
1836
2181
|
setConnected(isConnected) {
|
|
1837
2182
|
this.setState(isConnected ? TerminalState.CONNECTED : TerminalState.READY);
|
|
2183
|
+
if (isConnected) {
|
|
2184
|
+
this.scheduleFabricRendererAttach();
|
|
2185
|
+
}
|
|
1838
2186
|
}
|
|
1839
2187
|
forceResize() {
|
|
1840
2188
|
this.flushPendingPresentationScale();
|
|
@@ -1895,6 +2243,7 @@ export class TerminalCore {
|
|
|
1895
2243
|
return;
|
|
1896
2244
|
}
|
|
1897
2245
|
this.terminal.options.fontSize = this.resolveEffectiveFontSize();
|
|
2246
|
+
this.fabricView?.renderer.setAppearance({ fontSize: this.resolveEffectiveFontSize() });
|
|
1898
2247
|
this.performResize('font');
|
|
1899
2248
|
void this.refreshFontMetricsAfterLoad('font_size');
|
|
1900
2249
|
}
|
|
@@ -1931,6 +2280,10 @@ export class TerminalCore {
|
|
|
1931
2280
|
return;
|
|
1932
2281
|
}
|
|
1933
2282
|
this.terminal.options.fontFamily = nextFamily;
|
|
2283
|
+
this.fabricView?.renderer.setAppearance({
|
|
2284
|
+
fontFamily: nextFamily,
|
|
2285
|
+
fontSize: this.resolveEffectiveFontSize(),
|
|
2286
|
+
});
|
|
1934
2287
|
this.performResize('font');
|
|
1935
2288
|
this.forceFullRender();
|
|
1936
2289
|
void this.refreshFontMetricsAfterLoad('font_family');
|
|
@@ -1942,6 +2295,9 @@ export class TerminalCore {
|
|
|
1942
2295
|
let disposed = false;
|
|
1943
2296
|
this.visualRenderState.suspendDepth += 1;
|
|
1944
2297
|
this.visualRenderState.activeReasons.set(id, reason);
|
|
2298
|
+
// Legacy compatibility: keep the handle lifecycle observable, but never
|
|
2299
|
+
// freeze visible terminal rendering in the live fabric path.
|
|
2300
|
+
this.visualRenderState.suspendDepth = Math.max(0, this.visualRenderState.suspendDepth - 1);
|
|
1945
2301
|
return {
|
|
1946
2302
|
id,
|
|
1947
2303
|
reason,
|
|
@@ -2001,6 +2357,11 @@ export class TerminalCore {
|
|
|
2001
2357
|
this.disposeInputBridge();
|
|
2002
2358
|
this.disposeTerminalEventListeners();
|
|
2003
2359
|
this.disposeSearchOverlay();
|
|
2360
|
+
this.fabricAttachSeq += 1;
|
|
2361
|
+
this.cancelFabricAttachSchedule?.();
|
|
2362
|
+
this.cancelFabricAttachSchedule = null;
|
|
2363
|
+
this.fabricView?.dispose();
|
|
2364
|
+
this.fabricView = null;
|
|
2004
2365
|
this.terminal?.dispose();
|
|
2005
2366
|
this.terminal = null;
|
|
2006
2367
|
this.viewportHost = null;
|
|
@@ -2174,16 +2535,28 @@ export class TerminalCore {
|
|
|
2174
2535
|
this.suppressResizeNotifications = false;
|
|
2175
2536
|
});
|
|
2176
2537
|
this.terminal.options.fontSize = this.resolveEffectiveFontSize(nextScale);
|
|
2538
|
+
this.fabricView?.renderer.setAppearance({ fontSize: this.resolveEffectiveFontSize(nextScale) });
|
|
2177
2539
|
if (this.isVisualRenderSuspended()) {
|
|
2178
2540
|
this.markPendingResize('force');
|
|
2179
2541
|
this.markPendingDemandRender(true);
|
|
2180
2542
|
this.scheduleRenderSearchOverlay();
|
|
2181
2543
|
return;
|
|
2182
2544
|
}
|
|
2183
|
-
this.
|
|
2545
|
+
this.performResize('force');
|
|
2184
2546
|
this.forceFullRender();
|
|
2185
2547
|
this.scheduleRenderSearchOverlay();
|
|
2186
2548
|
}
|
|
2549
|
+
resizeFabricToHost() {
|
|
2550
|
+
const host = this.renderHost ?? this.container;
|
|
2551
|
+
if (!host) {
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
this.fabricView?.renderer.resize(host.clientWidth, host.clientHeight);
|
|
2555
|
+
this.syncGhosttyInputGeometryToFabric();
|
|
2556
|
+
}
|
|
2557
|
+
isFabricRendererActive() {
|
|
2558
|
+
return Boolean(this.fabricView?.renderer.isActive());
|
|
2559
|
+
}
|
|
2187
2560
|
forceFullRender() {
|
|
2188
2561
|
if (!this.terminal || !this.isReady()) {
|
|
2189
2562
|
return;
|