Skip to content

Commit e3a5d78

Browse files
committed
feat: add dynamic font resize support
Implement runtime font size and family changes via options proxy, matching xterm.js API behavior. Changes: - Wire up fontSize option changes to renderer.setFontSize() - Wire up fontFamily option changes to renderer.setFontFamily() - Add handleFontChange() helper that: - Clears active selection (pixel positions changed) - Resizes canvas to match new font metrics - Forces full re-render - Add tests for font change behavior Usage: term.options.fontSize = 20; // Dynamic resize term.options.fontFamily = 'Courier New'; Note: Font changes keep cols/rows constant (canvas grows/shrinks). Use FitAddon.fit() after font change to recalculate grid dimensions.
1 parent 90c1178 commit e3a5d78

File tree

2 files changed

+126
-1
lines changed

2 files changed

+126
-1
lines changed

lib/terminal.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2462,6 +2462,98 @@ describe('Options Proxy handleOptionChange', () => {
24622462

24632463
expect(term.options.cursorStyle).toBe('underline');
24642464
});
2465+
2466+
test('changing fontSize updates renderer and resizes canvas', async () => {
2467+
if (!container) return;
2468+
2469+
const term = await createIsolatedTerminal({ fontSize: 15, cols: 80, rows: 24 });
2470+
term.open(container);
2471+
2472+
// Get initial metrics
2473+
// @ts-ignore - accessing private for test
2474+
const renderer = term.renderer;
2475+
const initialMetrics = renderer.getMetrics();
2476+
// @ts-ignore - accessing private for test
2477+
const canvas = term.canvas;
2478+
const initialWidth = canvas.style.width;
2479+
2480+
// Change font size
2481+
term.options.fontSize = 20;
2482+
2483+
// Verify option was updated
2484+
expect(term.options.fontSize).toBe(20);
2485+
2486+
// Verify renderer metrics changed
2487+
const newMetrics = renderer.getMetrics();
2488+
expect(newMetrics.width).toBeGreaterThan(initialMetrics.width);
2489+
expect(newMetrics.height).toBeGreaterThan(initialMetrics.height);
2490+
2491+
// Verify canvas size changed
2492+
expect(canvas.style.width).not.toBe(initialWidth);
2493+
2494+
term.dispose();
2495+
});
2496+
2497+
test('changing fontFamily updates renderer', async () => {
2498+
if (!container) return;
2499+
2500+
const term = await createIsolatedTerminal({ fontFamily: 'monospace', cols: 80, rows: 24 });
2501+
term.open(container);
2502+
2503+
// @ts-ignore - accessing private for test
2504+
const renderer = term.renderer;
2505+
2506+
// Change font family
2507+
term.options.fontFamily = 'Courier New, monospace';
2508+
2509+
// Verify option was updated
2510+
expect(term.options.fontFamily).toBe('Courier New, monospace');
2511+
2512+
// Verify renderer was updated
2513+
// @ts-ignore - accessing private for test
2514+
expect(renderer.fontFamily).toBe('Courier New, monospace');
2515+
2516+
term.dispose();
2517+
});
2518+
2519+
test('font change clears active selection', async () => {
2520+
if (!container) return;
2521+
2522+
const term = await createIsolatedTerminal({ fontSize: 15, cols: 80, rows: 24 });
2523+
term.open(container);
2524+
2525+
// Write some text and select it
2526+
term.write('Hello World');
2527+
term.select(0, 0, 5); // Select "Hello"
2528+
expect(term.hasSelection()).toBe(true);
2529+
2530+
// Change font size
2531+
term.options.fontSize = 20;
2532+
2533+
// Selection should be cleared (pixel positions changed)
2534+
expect(term.hasSelection()).toBe(false);
2535+
2536+
term.dispose();
2537+
});
2538+
2539+
test('font change maintains terminal dimensions (cols/rows)', async () => {
2540+
if (!container) return;
2541+
2542+
const term = await createIsolatedTerminal({ fontSize: 15, cols: 80, rows: 24 });
2543+
term.open(container);
2544+
2545+
const initialCols = term.cols;
2546+
const initialRows = term.rows;
2547+
2548+
// Change font size
2549+
term.options.fontSize = 20;
2550+
2551+
// Cols and rows should remain the same (canvas grows instead)
2552+
expect(term.cols).toBe(initialCols);
2553+
expect(term.rows).toBe(initialRows);
2554+
2555+
term.dispose();
2556+
});
24652557
});
24662558

24672559
// ==========================================================================

lib/terminal.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,16 @@ export class Terminal implements ITerminalCore {
206206
break;
207207

208208
case 'fontSize':
209+
if (this.renderer) {
210+
this.renderer.setFontSize(this.options.fontSize);
211+
this.handleFontChange();
212+
}
213+
break;
214+
209215
case 'fontFamily':
210216
if (this.renderer) {
211-
console.warn('ghostty-web: font changes after open() are not yet fully supported');
217+
this.renderer.setFontFamily(this.options.fontFamily);
218+
this.handleFontChange();
212219
}
213220
break;
214221

@@ -220,6 +227,32 @@ export class Terminal implements ITerminalCore {
220227
}
221228
}
222229

230+
/**
231+
* Handle font changes (fontSize or fontFamily)
232+
* Updates canvas size to match new font metrics and forces a full re-render
233+
*/
234+
private handleFontChange(): void {
235+
if (!this.renderer || !this.wasmTerm || !this.canvas) return;
236+
237+
// Clear any active selection since pixel positions have changed
238+
if (this.selectionManager) {
239+
this.selectionManager.clearSelection();
240+
}
241+
242+
// Resize canvas to match new font metrics
243+
this.renderer.resize(this.cols, this.rows);
244+
245+
// Update canvas element dimensions to match renderer
246+
const metrics = this.renderer.getMetrics();
247+
this.canvas.width = metrics.width * this.cols;
248+
this.canvas.height = metrics.height * this.rows;
249+
this.canvas.style.width = `${metrics.width * this.cols}px`;
250+
this.canvas.style.height = `${metrics.height * this.rows}px`;
251+
252+
// Force full re-render with new font
253+
this.renderer.render(this.wasmTerm, true, this.viewportY, this);
254+
}
255+
223256
/**
224257
* Parse a CSS color string to 0xRRGGBB format.
225258
* Returns 0 if the color is undefined or invalid.

0 commit comments

Comments
 (0)