@floegence/floeterm-terminal-web 0.5.0 → 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.
Files changed (44) hide show
  1. package/dist/core/TerminalCore.d.ts +19 -0
  2. package/dist/core/TerminalCore.d.ts.map +1 -1
  3. package/dist/core/TerminalCore.js +414 -41
  4. package/dist/core/TerminalCore.js.map +1 -1
  5. package/dist/core/TerminalRenderScheduler.d.ts +4 -0
  6. package/dist/core/TerminalRenderScheduler.d.ts.map +1 -1
  7. package/dist/core/TerminalRenderScheduler.js +35 -2
  8. package/dist/core/TerminalRenderScheduler.js.map +1 -1
  9. package/dist/fabric/BeamtermFabricRenderer.d.ts +59 -0
  10. package/dist/fabric/BeamtermFabricRenderer.d.ts.map +1 -0
  11. package/dist/fabric/BeamtermFabricRenderer.js +448 -0
  12. package/dist/fabric/BeamtermFabricRenderer.js.map +1 -0
  13. package/dist/fabric/TerminalFabricCoordinator.d.ts +40 -0
  14. package/dist/fabric/TerminalFabricCoordinator.d.ts.map +1 -0
  15. package/dist/fabric/TerminalFabricCoordinator.js +140 -0
  16. package/dist/fabric/TerminalFabricCoordinator.js.map +1 -0
  17. package/dist/fabric/TerminalLiveFabric.d.ts +84 -0
  18. package/dist/fabric/TerminalLiveFabric.d.ts.map +1 -0
  19. package/dist/fabric/TerminalLiveFabric.js +211 -0
  20. package/dist/fabric/TerminalLiveFabric.js.map +1 -0
  21. package/dist/fabric/types.d.ts +112 -0
  22. package/dist/fabric/types.d.ts.map +1 -0
  23. package/dist/fabric/types.js +2 -0
  24. package/dist/fabric/types.js.map +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/internal/TerminalInitializationScheduler.d.ts +22 -0
  30. package/dist/internal/TerminalInitializationScheduler.d.ts.map +1 -0
  31. package/dist/internal/TerminalInitializationScheduler.js +79 -0
  32. package/dist/internal/TerminalInitializationScheduler.js.map +1 -0
  33. package/dist/internal/scheduleUiTurn.d.ts +5 -0
  34. package/dist/internal/scheduleUiTurn.d.ts.map +1 -0
  35. package/dist/internal/scheduleUiTurn.js +37 -0
  36. package/dist/internal/scheduleUiTurn.js.map +1 -0
  37. package/dist/manager/TerminalInstanceController.d.ts +1 -0
  38. package/dist/manager/TerminalInstanceController.d.ts.map +1 -1
  39. package/dist/manager/TerminalInstanceController.js +67 -30
  40. package/dist/manager/TerminalInstanceController.js.map +1 -1
  41. package/dist/types.d.ts +3 -1
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js.map +1 -1
  44. package/package.json +2 -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
- renderer.__floetermRowRenderCacheForceAll = Boolean(args[1]) || viewportChanged;
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
- return originalRender(...args);
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
- const overlay = document.createElement('canvas');
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
- const { cssWidth, cssHeight, dpr } = resolveCanvasLocalDisplaySize(termCanvas, this.container);
1761
- if (this.searchOverlay.canvas.width !== termCanvas.width ||
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
- overlay.ctx.clearRect(0, 0, overlay.cssWidth, overlay.cssHeight);
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.fitAddon?.fit();
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;