Skip to content

Commit e879eef

Browse files
authored
feat: support dynamic font resizing (#80)
1 parent 90c1178 commit e879eef

File tree

2 files changed

+127
-1
lines changed

2 files changed

+127
-1
lines changed

lib/terminal.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2462,6 +2462,99 @@ 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+
// @ts-ignore - accessing private for test
2473+
const renderer = term.renderer;
2474+
2475+
// Verify initial font size
2476+
// @ts-ignore - accessing private for test
2477+
expect(renderer.fontSize).toBe(15);
2478+
2479+
// Change font size
2480+
term.options.fontSize = 20;
2481+
2482+
// Verify option was updated
2483+
expect(term.options.fontSize).toBe(20);
2484+
2485+
// Verify renderer's internal fontSize was updated
2486+
// @ts-ignore - accessing private for test
2487+
expect(renderer.fontSize).toBe(20);
2488+
2489+
// Verify metrics were recalculated (getMetrics returns a copy)
2490+
const metrics = renderer.getMetrics();
2491+
expect(metrics).toBeDefined();
2492+
expect(metrics.width).toBeGreaterThan(0);
2493+
expect(metrics.height).toBeGreaterThan(0);
2494+
2495+
term.dispose();
2496+
});
2497+
2498+
test('changing fontFamily updates renderer', async () => {
2499+
if (!container) return;
2500+
2501+
const term = await createIsolatedTerminal({ fontFamily: 'monospace', cols: 80, rows: 24 });
2502+
term.open(container);
2503+
2504+
// @ts-ignore - accessing private for test
2505+
const renderer = term.renderer;
2506+
2507+
// Change font family
2508+
term.options.fontFamily = 'Courier New, monospace';
2509+
2510+
// Verify option was updated
2511+
expect(term.options.fontFamily).toBe('Courier New, monospace');
2512+
2513+
// Verify renderer was updated
2514+
// @ts-ignore - accessing private for test
2515+
expect(renderer.fontFamily).toBe('Courier New, monospace');
2516+
2517+
term.dispose();
2518+
});
2519+
2520+
test('font change clears active selection', async () => {
2521+
if (!container) return;
2522+
2523+
const term = await createIsolatedTerminal({ fontSize: 15, cols: 80, rows: 24 });
2524+
term.open(container);
2525+
2526+
// Write some text and select it
2527+
term.write('Hello World');
2528+
term.select(0, 0, 5); // Select "Hello"
2529+
expect(term.hasSelection()).toBe(true);
2530+
2531+
// Change font size
2532+
term.options.fontSize = 20;
2533+
2534+
// Selection should be cleared (pixel positions changed)
2535+
expect(term.hasSelection()).toBe(false);
2536+
2537+
term.dispose();
2538+
});
2539+
2540+
test('font change maintains terminal dimensions (cols/rows)', async () => {
2541+
if (!container) return;
2542+
2543+
const term = await createIsolatedTerminal({ fontSize: 15, cols: 80, rows: 24 });
2544+
term.open(container);
2545+
2546+
const initialCols = term.cols;
2547+
const initialRows = term.rows;
2548+
2549+
// Change font size
2550+
term.options.fontSize = 20;
2551+
2552+
// Cols and rows should remain the same (canvas grows instead)
2553+
expect(term.cols).toBe(initialCols);
2554+
expect(term.rows).toBe(initialRows);
2555+
2556+
term.dispose();
2557+
});
24652558
});
24662559

24672560
// ==========================================================================

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)