authorgravatar for 269035359+t3-code[bot]@users.noreply.github.comt3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> 2026-08-05 17:02:53-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-05 17:02:53-04:00
log30e471530b3e2ddf59caf309636bed940b8b3776
tree3d5332674a00429bde3ba3a272283ffbae27f9e0
parentde592a00e89776d2e0614f3be6e666012d90cd51
signature Signed by PGP key B5690EEEBB952194

fix(web): preserve terminal font size when splitting (#5444)

Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com>

2 files changed, 0 insertions(+), 74 deletions(-)

apps/web/src/terminal/ghostty/surface.test.ts-19
...@@ -23,7 +23,6 @@ import {...@@ -23,7 +23,6 @@ import {
23 terminalLinkAtPositionWithRange,23 terminalLinkAtPositionWithRange,
24 terminalContentOriginY,24 terminalContentOriginY,
25 terminalFontFamily,25 terminalFontFamily,
26 fittedTerminalFontSize,
27 terminalFontSize,26 terminalFontSize,
28 terminalWheelArrowData,27 terminalWheelArrowData,
29 terminalWheelDeltaRows,28 terminalWheelDeltaRows,
...@@ -343,24 +342,6 @@ describe("terminal font resolution", () => {...@@ -343,24 +342,6 @@ describe("terminal font resolution", () => {
343 expect(terminalFontFamily(" , ")).toBe(DEFAULT_TERMINAL_FONT_FAMILY);342 expect(terminalFontFamily(" , ")).toBe(DEFAULT_TERMINAL_FONT_FAMILY);
344 });343 });
345344
346 it("slides the rendered size down until a full-width grid fits the canvas", () => {
347 // SF Mono-like advance: 0.6em per cell.
348 const cellWidthAt = (size: number) => size * 0.6;
349 // A wide drawer keeps the preference untouched.
350 expect(fittedTerminalFontSize(cellWidthAt, 20, 1140)).toBe(20);
351 // A split pane at the same preference shrinks until 80 columns fit.
352 const fitted = fittedTerminalFontSize(cellWidthAt, 20, 570);
353 expect(fitted).toBeLessThan(20);
354 expect(Math.floor((570 - 8) / cellWidthAt(fitted))).toBeGreaterThanOrEqual(80);
355 // A tiny pane stops at the legibility floor instead of vanishing.
356 expect(fittedTerminalFontSize(cellWidthAt, 20, 220)).toBe(8);
357 // A preference below the floor is honored as-is.
358 expect(fittedTerminalFontSize(cellWidthAt, 6, 220)).toBe(6);
359 // Unmeasured layouts leave the preference alone.
360 expect(fittedTerminalFontSize(cellWidthAt, 14, 0)).toBe(14);
361 expect(fittedTerminalFontSize(() => 0, 14, 600)).toBe(14);
362 });
363
364 it("clamps requested font sizes to the supported range", () => {345 it("clamps requested font sizes to the supported range", () => {
365 expect(terminalFontSize()).toBe(DEFAULT_TERMINAL_FONT_SIZE);346 expect(terminalFontSize()).toBe(DEFAULT_TERMINAL_FONT_SIZE);
366 expect(terminalFontSize(Number.NaN)).toBe(DEFAULT_TERMINAL_FONT_SIZE);347 expect(terminalFontSize(Number.NaN)).toBe(DEFAULT_TERMINAL_FONT_SIZE);
apps/web/src/terminal/ghostty/surface.ts-55
...@@ -129,42 +129,6 @@ export async function loadTerminalFontFamily(...@@ -129,42 +129,6 @@ export async function loadTerminalFontFamily(
129 return (environment?.resolve ?? terminalFontFamily)(family);129 return (environment?.resolve ?? terminalFontFamily)(family);
130}130}
131131
132/**
133 * Grids narrower than a classic 80-column terminal wrap command output hard,
134 * so the rendered font size follows the canvas width: the preference is the
135 * ceiling, and the size slides down (to a legibility floor) until a full-width
136 * grid fits. A widening pane slides it back up toward the preference.
137 */
138const MIN_TERMINAL_FIT_COLUMNS = 80;
139const MIN_TERMINAL_FIT_FONT_SIZE = 8;
140
141export function fittedTerminalFontSize(
142 cellWidthAt: (size: number) => number,
143 requested: number,
144 mountWidth: number,
145): number {
146 const available = mountWidth - CONTENT_PADDING * 2;
147 if (available <= 0) return requested;
148 const floor = Math.min(requested, MIN_TERMINAL_FIT_FONT_SIZE);
149 const fits = (cellWidth: number) =>
150 cellWidth > 0 && Math.floor(available / cellWidth) >= MIN_TERMINAL_FIT_COLUMNS;
151 let cellWidth = cellWidthAt(requested);
152 if (cellWidth <= 0 || fits(cellWidth)) return requested;
153 // The advance scales linearly with size for monospace faces: jump close to
154 // the fitting size, then settle the remaining rounding one step at a time.
155 const targetCellWidth = available / MIN_TERMINAL_FIT_COLUMNS;
156 let size = Math.max(
157 floor,
158 Math.min(requested, Math.floor((requested * targetCellWidth) / cellWidth)),
159 );
160 while (size > floor) {
161 cellWidth = cellWidthAt(size);
162 if (cellWidth <= 0 || fits(cellWidth)) break;
163 size -= 1;
164 }
165 return size;
166}
167
168export function terminalFontSize(size?: number): number {132export function terminalFontSize(size?: number): number {
169 if (size === undefined || !Number.isFinite(size)) return DEFAULT_TERMINAL_FONT_SIZE;133 if (size === undefined || !Number.isFinite(size)) return DEFAULT_TERMINAL_FONT_SIZE;
170 return Math.max(MIN_TERMINAL_FONT_SIZE, Math.min(MAX_TERMINAL_FONT_SIZE, Math.round(size)));134 return Math.max(MIN_TERMINAL_FONT_SIZE, Math.min(MAX_TERMINAL_FONT_SIZE, Math.round(size)));
...@@ -519,7 +483,6 @@ export class GhosttyTerminalSurface {...@@ -519,7 +483,6 @@ export class GhosttyTerminalSurface {
519 private fontFamily: string;483 private fontFamily: string;
520 private requestedFontFamily: string | undefined;484 private requestedFontFamily: string | undefined;
521 private fontSize: number;485 private fontSize: number;
522 private requestedFontSize: number;
523 private fontEpoch = 0;486 private fontEpoch = 0;
524 private pendingFontEpoch: number | null = null;487 private pendingFontEpoch: number | null = null;
525 private readonly resizeObserver: ResizeObserver;488 private readonly resizeObserver: ResizeObserver;
...@@ -601,7 +564,6 @@ export class GhosttyTerminalSurface {...@@ -601,7 +564,6 @@ export class GhosttyTerminalSurface {
601 this.fontFamily = fontFamily;564 this.fontFamily = fontFamily;
602 this.requestedFontFamily = options.font?.family;565 this.requestedFontFamily = options.font?.family;
603 this.fontSize = terminalFontSize(options.font?.size);566 this.fontSize = terminalFontSize(options.font?.size);
604 this.requestedFontSize = this.fontSize;
605 this.resizeObserver = new ResizeObserver(() => this.fit());567 this.resizeObserver = new ResizeObserver(() => this.fit());
606 this.installEvents();568 this.installEvents();
607 this.watchDevicePixelRatio();569 this.watchDevicePixelRatio();
...@@ -719,7 +681,6 @@ export class GhosttyTerminalSurface {...@@ -719,7 +681,6 @@ export class GhosttyTerminalSurface {
719 this.pendingFontEpoch = null;681 this.pendingFontEpoch = null;
720 this.fontFamily = fontFamily;682 this.fontFamily = fontFamily;
721 this.requestedFontFamily = font.family;683 this.requestedFontFamily = font.family;
722 this.requestedFontSize = fontSize;
723 this.fontSize = fontSize;684 this.fontSize = fontSize;
724 this.applyFontMetrics();685 this.applyFontMetrics();
725 }686 }
...@@ -775,22 +736,6 @@ export class GhosttyTerminalSurface {...@@ -775,22 +736,6 @@ export class GhosttyTerminalSurface {
775 const width = this.mount.clientWidth;736 const width = this.mount.clientWidth;
776 const height = this.mount.clientHeight;737 const height = this.mount.clientHeight;
777 if (width <= 0 || height <= 0) return false;738 if (width <= 0 || height <= 0) return false;
778 const fitted = fittedTerminalFontSize(
779 (size) => measureGhosttyCell(this.context, size, this.fontFamily).width,
780 this.requestedFontSize,
781 width,
782 );
783 if (fitted !== this.fontSize) {
784 this.fontSize = fitted;
785 this.metrics = measureGhosttyCell(this.context, this.fontSize, this.fontFamily);
786 // The grid-change branch below resizes the core, but only when the
787 // column count moved; the cell geometry always did, so sync it here.
788 this.core.resize(this.cols, this.rows, this.metrics.width, this.metrics.height);
789 this.inputLeft = -1;
790 this.inputTop = -1;
791 this.forceFullRender = true;
792 this.scrollbarDirty = true;
793 }
794 const ratio = window.devicePixelRatio || 1;739 const ratio = window.devicePixelRatio || 1;
795 const pixelWidth = Math.max(1, Math.round(width * ratio));740 const pixelWidth = Math.max(1, Math.round(width * ratio));
796 const pixelHeight = Math.max(1, Math.round(height * ratio));741 const pixelHeight = Math.max(1, Math.round(height * ratio));