@floegence/floeterm-terminal-web 0.4.32 → 0.5.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 +86 -56
- package/dist/core/TerminalCore.d.ts +27 -1
- package/dist/core/TerminalCore.d.ts.map +1 -1
- package/dist/core/TerminalCore.js +490 -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 +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- 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 +71 -0
- package/dist/manager/TerminalInstanceController.d.ts.map +1 -0
- package/dist/manager/TerminalInstanceController.js +670 -0
- package/dist/manager/TerminalInstanceController.js.map +1 -0
- package/dist/types.d.ts +48 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -12
- package/dist/hooks/useTerminalInstance.d.ts +0 -3
- package/dist/hooks/useTerminalInstance.d.ts.map +0 -1
- package/dist/hooks/useTerminalInstance.js +0 -544
- package/dist/hooks/useTerminalInstance.js.map +0 -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 });
|
|
@@ -1254,6 +1590,82 @@ export class TerminalCore {
|
|
|
1254
1590
|
bufferLength: this.terminal.buffer.active.length
|
|
1255
1591
|
};
|
|
1256
1592
|
}
|
|
1593
|
+
readBufferLine(row, options = {}) {
|
|
1594
|
+
if (!this.terminal) {
|
|
1595
|
+
return '';
|
|
1596
|
+
}
|
|
1597
|
+
const normalizedRow = Math.floor(Number(row));
|
|
1598
|
+
if (!Number.isFinite(normalizedRow) || normalizedRow < 0) {
|
|
1599
|
+
return '';
|
|
1600
|
+
}
|
|
1601
|
+
try {
|
|
1602
|
+
const line = this.terminal.buffer?.active?.getLine?.(normalizedRow);
|
|
1603
|
+
const text = line?.translateToString?.(options.trimRight ?? false);
|
|
1604
|
+
return typeof text === 'string' ? text : '';
|
|
1605
|
+
}
|
|
1606
|
+
catch {
|
|
1607
|
+
return '';
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
readBufferLines(startRow, endRowInclusive, options = {}) {
|
|
1611
|
+
const start = Math.floor(Number(startRow));
|
|
1612
|
+
const end = Math.floor(Number(endRowInclusive));
|
|
1613
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) {
|
|
1614
|
+
return [];
|
|
1615
|
+
}
|
|
1616
|
+
const lines = [];
|
|
1617
|
+
for (let row = Math.max(0, start); row <= end; row += 1) {
|
|
1618
|
+
const text = this.readBufferLine(row, options);
|
|
1619
|
+
if (text.length > 0) {
|
|
1620
|
+
lines.push({ row, text });
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
return lines;
|
|
1624
|
+
}
|
|
1625
|
+
getTouchScrollRuntime() {
|
|
1626
|
+
const terminal = this.terminal;
|
|
1627
|
+
if (!terminal) {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
return {
|
|
1631
|
+
scrollLines: amount => {
|
|
1632
|
+
const normalizedAmount = Math.trunc(Number(amount));
|
|
1633
|
+
if (!Number.isFinite(normalizedAmount) || normalizedAmount === 0) {
|
|
1634
|
+
return false;
|
|
1635
|
+
}
|
|
1636
|
+
if (typeof terminal.scrollLines !== 'function') {
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
terminal.scrollLines(normalizedAmount);
|
|
1640
|
+
this.requestDemandRender(false);
|
|
1641
|
+
return true;
|
|
1642
|
+
},
|
|
1643
|
+
getScrollbackLength: () => {
|
|
1644
|
+
if (typeof terminal.getScrollbackLength !== 'function') {
|
|
1645
|
+
return 0;
|
|
1646
|
+
}
|
|
1647
|
+
const length = Number(terminal.getScrollbackLength());
|
|
1648
|
+
return Number.isFinite(length) && length > 0 ? length : 0;
|
|
1649
|
+
},
|
|
1650
|
+
isAlternateScreen: () => {
|
|
1651
|
+
if (typeof terminal.isAlternateScreen !== 'function') {
|
|
1652
|
+
return false;
|
|
1653
|
+
}
|
|
1654
|
+
return Boolean(terminal.isAlternateScreen());
|
|
1655
|
+
},
|
|
1656
|
+
sendAlternateScreenInput: data => {
|
|
1657
|
+
const text = String(data ?? '');
|
|
1658
|
+
if (!text) {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
if (typeof terminal.input === 'function') {
|
|
1662
|
+
terminal.input(text, true);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
this.eventHandlers.onData?.(text);
|
|
1666
|
+
},
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1257
1669
|
findNext(term, options) {
|
|
1258
1670
|
return this.findInternal(term, options, 1);
|
|
1259
1671
|
}
|
|
@@ -1534,6 +1946,7 @@ export class TerminalCore {
|
|
|
1534
1946
|
const restore = this.searchThemeRestore;
|
|
1535
1947
|
this.searchThemeRestore = null;
|
|
1536
1948
|
this.applyRendererTheme(terminalAny, restore);
|
|
1949
|
+
this.forceFullRender();
|
|
1537
1950
|
}
|
|
1538
1951
|
applySearchThemeOverride(theme) {
|
|
1539
1952
|
return {
|
|
@@ -1550,6 +1963,7 @@ export class TerminalCore {
|
|
|
1550
1963
|
this.terminal.options.theme = theme;
|
|
1551
1964
|
}
|
|
1552
1965
|
terminalAny.renderer?.setTheme?.(theme);
|
|
1966
|
+
this.fabricView?.renderer.setAppearance({ theme });
|
|
1553
1967
|
if (this.isVisualRenderSuspended()) {
|
|
1554
1968
|
this.markPendingDemandRender(true);
|
|
1555
1969
|
return;
|
|
@@ -1642,6 +2056,43 @@ export class TerminalCore {
|
|
|
1642
2056
|
}
|
|
1643
2057
|
this.searchOverlay = null;
|
|
1644
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
|
+
}
|
|
1645
2096
|
ensureSearchOverlay() {
|
|
1646
2097
|
const t = this.terminal;
|
|
1647
2098
|
if (!t)
|
|
@@ -1662,40 +2113,10 @@ export class TerminalCore {
|
|
|
1662
2113
|
this.disposeSearchOverlay();
|
|
1663
2114
|
}
|
|
1664
2115
|
if (!this.searchOverlay) {
|
|
1665
|
-
|
|
1666
|
-
overlay.style.position = 'absolute';
|
|
1667
|
-
overlay.style.top = '0';
|
|
1668
|
-
overlay.style.left = '0';
|
|
1669
|
-
overlay.style.pointerEvents = 'none';
|
|
1670
|
-
overlay.style.zIndex = '5';
|
|
1671
|
-
const ctx = overlay.getContext('2d');
|
|
1672
|
-
if (!ctx)
|
|
1673
|
-
return null;
|
|
1674
|
-
this.container.appendChild(overlay);
|
|
1675
|
-
this.searchOverlay = {
|
|
1676
|
-
canvas: overlay,
|
|
1677
|
-
ctx,
|
|
1678
|
-
dpr: 1,
|
|
1679
|
-
cssWidth: 0,
|
|
1680
|
-
cssHeight: 0,
|
|
1681
|
-
termCanvas
|
|
1682
|
-
};
|
|
2116
|
+
this.searchOverlay = this.createCanvasOverlay('5');
|
|
1683
2117
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
this.searchOverlay.canvas.height !== termCanvas.height ||
|
|
1687
|
-
this.searchOverlay.cssWidth !== cssWidth ||
|
|
1688
|
-
this.searchOverlay.cssHeight !== cssHeight ||
|
|
1689
|
-
this.searchOverlay.dpr !== dpr) {
|
|
1690
|
-
this.searchOverlay.dpr = dpr;
|
|
1691
|
-
this.searchOverlay.cssWidth = cssWidth;
|
|
1692
|
-
this.searchOverlay.cssHeight = cssHeight;
|
|
1693
|
-
this.searchOverlay.canvas.style.width = `${cssWidth}px`;
|
|
1694
|
-
this.searchOverlay.canvas.style.height = `${cssHeight}px`;
|
|
1695
|
-
this.searchOverlay.canvas.width = termCanvas.width;
|
|
1696
|
-
this.searchOverlay.canvas.height = termCanvas.height;
|
|
1697
|
-
this.searchOverlay.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1698
|
-
this.searchOverlay.ctx.scale(this.searchOverlay.dpr, this.searchOverlay.dpr);
|
|
2118
|
+
if (this.searchOverlay) {
|
|
2119
|
+
syncCanvasOverlaySize(this.searchOverlay, termCanvas, this.container);
|
|
1699
2120
|
}
|
|
1700
2121
|
return this.searchOverlay;
|
|
1701
2122
|
}
|
|
@@ -1712,16 +2133,16 @@ export class TerminalCore {
|
|
|
1712
2133
|
});
|
|
1713
2134
|
}
|
|
1714
2135
|
renderSearchOverlay() {
|
|
1715
|
-
const overlay = this.ensureSearchOverlay();
|
|
1716
|
-
if (!overlay)
|
|
1717
|
-
return;
|
|
1718
2136
|
const t = this.terminal;
|
|
1719
2137
|
const queryActive = this.searchQuery.trim().length > 0;
|
|
1720
2138
|
const rowIndex = this.searchRowIndex;
|
|
1721
2139
|
if (!queryActive || rowIndex.size === 0 || !t) {
|
|
1722
|
-
|
|
2140
|
+
this.searchOverlay?.ctx.clearRect(0, 0, this.searchOverlay.cssWidth, this.searchOverlay.cssHeight);
|
|
1723
2141
|
return;
|
|
1724
2142
|
}
|
|
2143
|
+
const overlay = this.ensureSearchOverlay();
|
|
2144
|
+
if (!overlay)
|
|
2145
|
+
return;
|
|
1725
2146
|
const renderer = t?.renderer;
|
|
1726
2147
|
const charW = typeof renderer?.charWidth === 'number' ? renderer.charWidth : 0;
|
|
1727
2148
|
const charH = typeof renderer?.charHeight === 'number' ? renderer.charHeight : 0;
|
|
@@ -1759,6 +2180,9 @@ export class TerminalCore {
|
|
|
1759
2180
|
}
|
|
1760
2181
|
setConnected(isConnected) {
|
|
1761
2182
|
this.setState(isConnected ? TerminalState.CONNECTED : TerminalState.READY);
|
|
2183
|
+
if (isConnected) {
|
|
2184
|
+
this.scheduleFabricRendererAttach();
|
|
2185
|
+
}
|
|
1762
2186
|
}
|
|
1763
2187
|
forceResize() {
|
|
1764
2188
|
this.flushPendingPresentationScale();
|
|
@@ -1819,6 +2243,7 @@ export class TerminalCore {
|
|
|
1819
2243
|
return;
|
|
1820
2244
|
}
|
|
1821
2245
|
this.terminal.options.fontSize = this.resolveEffectiveFontSize();
|
|
2246
|
+
this.fabricView?.renderer.setAppearance({ fontSize: this.resolveEffectiveFontSize() });
|
|
1822
2247
|
this.performResize('font');
|
|
1823
2248
|
void this.refreshFontMetricsAfterLoad('font_size');
|
|
1824
2249
|
}
|
|
@@ -1855,6 +2280,10 @@ export class TerminalCore {
|
|
|
1855
2280
|
return;
|
|
1856
2281
|
}
|
|
1857
2282
|
this.terminal.options.fontFamily = nextFamily;
|
|
2283
|
+
this.fabricView?.renderer.setAppearance({
|
|
2284
|
+
fontFamily: nextFamily,
|
|
2285
|
+
fontSize: this.resolveEffectiveFontSize(),
|
|
2286
|
+
});
|
|
1858
2287
|
this.performResize('font');
|
|
1859
2288
|
this.forceFullRender();
|
|
1860
2289
|
void this.refreshFontMetricsAfterLoad('font_family');
|
|
@@ -1866,6 +2295,9 @@ export class TerminalCore {
|
|
|
1866
2295
|
let disposed = false;
|
|
1867
2296
|
this.visualRenderState.suspendDepth += 1;
|
|
1868
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);
|
|
1869
2301
|
return {
|
|
1870
2302
|
id,
|
|
1871
2303
|
reason,
|
|
@@ -1925,6 +2357,11 @@ export class TerminalCore {
|
|
|
1925
2357
|
this.disposeInputBridge();
|
|
1926
2358
|
this.disposeTerminalEventListeners();
|
|
1927
2359
|
this.disposeSearchOverlay();
|
|
2360
|
+
this.fabricAttachSeq += 1;
|
|
2361
|
+
this.cancelFabricAttachSchedule?.();
|
|
2362
|
+
this.cancelFabricAttachSchedule = null;
|
|
2363
|
+
this.fabricView?.dispose();
|
|
2364
|
+
this.fabricView = null;
|
|
1928
2365
|
this.terminal?.dispose();
|
|
1929
2366
|
this.terminal = null;
|
|
1930
2367
|
this.viewportHost = null;
|
|
@@ -2098,16 +2535,28 @@ export class TerminalCore {
|
|
|
2098
2535
|
this.suppressResizeNotifications = false;
|
|
2099
2536
|
});
|
|
2100
2537
|
this.terminal.options.fontSize = this.resolveEffectiveFontSize(nextScale);
|
|
2538
|
+
this.fabricView?.renderer.setAppearance({ fontSize: this.resolveEffectiveFontSize(nextScale) });
|
|
2101
2539
|
if (this.isVisualRenderSuspended()) {
|
|
2102
2540
|
this.markPendingResize('force');
|
|
2103
2541
|
this.markPendingDemandRender(true);
|
|
2104
2542
|
this.scheduleRenderSearchOverlay();
|
|
2105
2543
|
return;
|
|
2106
2544
|
}
|
|
2107
|
-
this.
|
|
2545
|
+
this.performResize('force');
|
|
2108
2546
|
this.forceFullRender();
|
|
2109
2547
|
this.scheduleRenderSearchOverlay();
|
|
2110
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
|
+
}
|
|
2111
2560
|
forceFullRender() {
|
|
2112
2561
|
if (!this.terminal || !this.isReady()) {
|
|
2113
2562
|
return;
|