Skip to content

Commit 301acb7

Browse files
authored
add terminal integration (#9)
- Created Terminal class integrating all components (ScreenBuffer, VTParser, CanvasRenderer, InputHandler) - Added public API exports (lib/index.ts) - Added 36 integration tests (all passing) - Created full terminal demo (examples/terminal-demo.html) - Fixed WASM loading with configurable wasmPath option - Fixed keyboard input with auto-focus support - Fixed backspace/enter handling with proper local echo - Fixed character rendering with background clearing and font metrics padding - All 186 tests passing
1 parent 54384ba commit 301acb7

8 files changed

Lines changed: 1478 additions & 8 deletions

File tree

examples/terminal-demo.html

Lines changed: 573 additions & 0 deletions
Large diffs are not rendered by default.

lib/ghostty.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,13 @@ export class Ghostty {
6565
} catch (e) {
6666
// Fall back to fetch (for browser environments)
6767
const response = await fetch(wasmPath);
68+
if (!response.ok) {
69+
throw new Error(`Failed to fetch WASM: ${response.status} ${response.statusText}`);
70+
}
6871
wasmBytes = await response.arrayBuffer();
72+
if (wasmBytes.byteLength === 0) {
73+
throw new Error(`WASM file is empty (0 bytes). Check path: ${wasmPath}`);
74+
}
6975
}
7076

7177
const wasmModule = await WebAssembly.instantiate(wasmBytes, {

lib/index.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Public API for @cmux/ghostty-terminal
3+
*
4+
* Main entry point following xterm.js conventions
5+
*/
6+
7+
// Main Terminal class
8+
export { Terminal } from './terminal';
9+
10+
// xterm.js-compatible interfaces
11+
export type {
12+
ITerminalOptions,
13+
ITheme,
14+
ITerminalAddon,
15+
ITerminalCore,
16+
IDisposable,
17+
IEvent
18+
} from './interfaces';
19+
20+
// Ghostty WASM components (for advanced usage)
21+
export { Ghostty, SgrParser, KeyEncoder } from './ghostty';
22+
export type {
23+
SgrAttribute,
24+
SgrAttributeTag,
25+
KeyEvent,
26+
KeyAction,
27+
Key,
28+
Mods
29+
} from './types';
30+
31+
// Buffer types (for addon developers)
32+
export type { Cell, CellColor, Cursor } from './buffer';
33+
34+
// Low-level components (for custom integrations)
35+
export { ScreenBuffer } from './buffer';
36+
export { VTParser } from './vt-parser';
37+
export { CanvasRenderer } from './renderer';
38+
export type { RendererOptions, FontMetrics } from './renderer';
39+
export { InputHandler } from './input-handler';
40+
export { EventEmitter } from './event-emitter';

lib/input-handler.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,19 @@ export class InputHandler {
192192
* Attach keyboard event listeners to container
193193
*/
194194
private attach(): void {
195+
// Make container focusable so it can receive keyboard events (browser only)
196+
if (typeof this.container.hasAttribute === 'function' &&
197+
typeof this.container.setAttribute === 'function') {
198+
if (!this.container.hasAttribute('tabindex')) {
199+
this.container.setAttribute('tabindex', '0');
200+
}
201+
202+
// Add visual focus indication (only if style exists - for browser environments)
203+
if (this.container.style) {
204+
this.container.style.outline = 'none'; // Remove default outline
205+
}
206+
}
207+
195208
this.keydownListener = this.handleKeyDown.bind(this);
196209
this.container.addEventListener('keydown', this.keydownListener);
197210
}

lib/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface ITerminalOptions {
1212
fontSize?: number; // Default: 15
1313
fontFamily?: string; // Default: 'monospace'
1414
allowTransparency?: boolean;
15+
wasmPath?: string; // Default: '../ghostty-vt.wasm' (relative to examples/)
1516
}
1617

1718
export interface ITheme {

lib/renderer.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,14 @@ export class CanvasRenderer {
143143
const widthMetrics = ctx.measureText('M');
144144
const width = Math.ceil(widthMetrics.width);
145145

146-
// Measure height using ascent + descent
146+
// Measure height using ascent + descent with padding for glyph overflow
147147
const ascent = widthMetrics.actualBoundingBoxAscent || this.fontSize * 0.8;
148148
const descent = widthMetrics.actualBoundingBoxDescent || this.fontSize * 0.2;
149-
const height = Math.ceil(ascent + descent);
150-
const baseline = Math.ceil(ascent);
149+
150+
// Add 2px padding to height to account for glyphs that overflow (like 'f', 'd', 'g', 'p')
151+
// and anti-aliasing pixels
152+
const height = Math.ceil(ascent + descent) + 2;
153+
const baseline = Math.ceil(ascent) + 1; // Offset baseline by half the padding
151154

152155
return { width, height, baseline };
153156
}
@@ -204,6 +207,10 @@ export class CanvasRenderer {
204207
// Scale context to match DPI (setting canvas.width/height resets the context)
205208
this.ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
206209

210+
// Set text rendering properties for crisp text
211+
this.ctx.textBaseline = 'alphabetic';
212+
this.ctx.textAlign = 'left';
213+
207214
// Fill background after resize
208215
this.ctx.fillStyle = this.theme.background;
209216
this.ctx.fillRect(0, 0, cssWidth, cssHeight);
@@ -308,11 +315,10 @@ export class CanvasRenderer {
308315
[fg, bg] = [bg, fg];
309316
}
310317

311-
// Draw background
312-
if (bg.type !== 'default' || cell.inverse) {
313-
this.ctx.fillStyle = this.colorToCSS(bg, true);
314-
this.ctx.fillRect(cellX, cellY, cellWidth, this.metrics.height);
315-
}
318+
// Always draw background to clear previous character
319+
// This fixes the issue where overwriting characters leaves remnants
320+
this.ctx.fillStyle = this.colorToCSS(bg, true);
321+
this.ctx.fillRect(cellX, cellY, cellWidth, this.metrics.height);
316322

317323
// Skip rendering if invisible
318324
if (cell.invisible) {

0 commit comments

Comments
 (0)