Skip to content

Commit 2da65f2

Browse files
authored
feat: make wasmPath optional with smart auto-detection (#21)
- Make Ghostty.load() wasmPath parameter optional with intelligent fallback logic - Try multiple default paths: import.meta.url, relative, and root paths - Update Terminal class to remove hardcoded wasmPath default - Simplify test code to use auto-detection - Fix build-wasm.sh to reference coder/ghostty repository - Improve error messages with helpful guidance Follows TensorFlow.js pattern for better developer experience.
1 parent bf58ec6 commit 2da65f2

File tree

6 files changed

+67
-34
lines changed

6 files changed

+67
-34
lines changed

bun.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"lockfileVersion": 1,
3+
"configVersion": 0,
34
"workspaces": {
45
"": {
56
"name": "@cmux/ghostty-terminal",

lib/ghostty.ts

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -55,39 +55,69 @@ export class Ghostty {
5555

5656
/**
5757
* Load Ghostty WASM from URL or file path
58+
* If no path is provided, attempts to load from common default locations
5859
*/
59-
static async load(wasmPath: string): Promise<Ghostty> {
60-
let wasmBytes: ArrayBuffer;
60+
static async load(wasmPath?: string): Promise<Ghostty> {
61+
// Default WASM paths to try (in order)
62+
const defaultPaths = [
63+
// When running in Node/Bun (resolve to file path)
64+
new URL('../ghostty-vt.wasm', import.meta.url).href.replace('file://', ''),
65+
// When published as npm package (browser)
66+
new URL('../ghostty-vt.wasm', import.meta.url).href,
67+
// When used from CDN or local dev
68+
'./ghostty-vt.wasm',
69+
'/ghostty-vt.wasm',
70+
];
71+
72+
const pathsToTry = wasmPath ? [wasmPath] : defaultPaths;
73+
let lastError: Error | null = null;
74+
75+
for (const path of pathsToTry) {
76+
try {
77+
let wasmBytes: ArrayBuffer;
78+
79+
// Try loading as file first (for Node/Bun environments)
80+
try {
81+
const fs = await import('fs/promises');
82+
const buffer = await fs.readFile(path);
83+
wasmBytes = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
84+
} catch (e) {
85+
// Fall back to fetch (for browser environments)
86+
const response = await fetch(path);
87+
if (!response.ok) {
88+
throw new Error(`Failed to fetch WASM: ${response.status} ${response.statusText}`);
89+
}
90+
wasmBytes = await response.arrayBuffer();
91+
if (wasmBytes.byteLength === 0) {
92+
throw new Error(`WASM file is empty (0 bytes). Check path: ${path}`);
93+
}
94+
}
6195

62-
// Try loading as file first (for Node/Bun environments)
63-
try {
64-
const fs = await import('fs/promises');
65-
const buffer = await fs.readFile(wasmPath);
66-
wasmBytes = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
67-
} catch (e) {
68-
// Fall back to fetch (for browser environments)
69-
const response = await fetch(wasmPath);
70-
if (!response.ok) {
71-
throw new Error(`Failed to fetch WASM: ${response.status} ${response.statusText}`);
72-
}
73-
wasmBytes = await response.arrayBuffer();
74-
if (wasmBytes.byteLength === 0) {
75-
throw new Error(`WASM file is empty (0 bytes). Check path: ${wasmPath}`);
96+
// Successfully loaded, instantiate and return
97+
const wasmModule = await WebAssembly.instantiate(wasmBytes, {
98+
env: {
99+
log: (ptr: number, len: number) => {
100+
const instance = (wasmModule as any).instance;
101+
const bytes = new Uint8Array(instance.exports.memory.buffer, ptr, len);
102+
const text = new TextDecoder().decode(bytes);
103+
console.log('[ghostty-wasm]', text);
104+
},
105+
},
106+
});
107+
108+
return new Ghostty(wasmModule.instance);
109+
} catch (e) {
110+
lastError = e instanceof Error ? e : new Error(String(e));
111+
// Try next path
76112
}
77113
}
78114

79-
const wasmModule = await WebAssembly.instantiate(wasmBytes, {
80-
env: {
81-
log: (ptr: number, len: number) => {
82-
const instance = (wasmModule as any).instance;
83-
const bytes = new Uint8Array(instance.exports.memory.buffer, ptr, len);
84-
const text = new TextDecoder().decode(bytes);
85-
console.log('[ghostty-wasm]', text);
86-
},
87-
},
88-
});
89-
90-
return new Ghostty(wasmModule.instance);
115+
// All paths failed
116+
throw new Error(
117+
`Failed to load ghostty-vt.wasm. Tried paths: ${pathsToTry.join(', ')}. ` +
118+
`Last error: ${lastError?.message}. ` +
119+
`You can specify a custom path with: new Terminal({ wasmPath: './path/to/ghostty-vt.wasm' })`
120+
);
91121
}
92122
}
93123

lib/input-handler.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ describe('InputHandler', () => {
129129

130130
beforeAll(async () => {
131131
// Load WASM once for all tests (expensive operation)
132-
const wasmPath = new URL('../ghostty-vt.wasm', import.meta.url).href;
133-
ghostty = await Ghostty.load(wasmPath);
132+
// wasmPath is now optional - auto-detected
133+
ghostty = await Ghostty.load();
134134
});
135135

136136
beforeEach(() => {

lib/interfaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +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/)
15+
wasmPath?: string; // Optional: custom WASM path (auto-detected by default)
1616
}
1717

1818
export interface ITheme {

lib/terminal.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ export class Terminal implements ITerminalCore {
3737
public textarea?: HTMLTextAreaElement;
3838

3939
// Options
40-
private options: Required<ITerminalOptions>;
40+
private options: Required<Omit<ITerminalOptions, 'wasmPath'>> & {
41+
wasmPath?: string;
42+
};
4143

4244
// Components (created on open())
4345
private ghostty?: Ghostty;
@@ -79,7 +81,7 @@ export class Terminal implements ITerminalCore {
7981
fontSize: options.fontSize ?? 15,
8082
fontFamily: options.fontFamily ?? 'monospace',
8183
allowTransparency: options.allowTransparency ?? false,
82-
wasmPath: options.wasmPath ?? '../ghostty-vt.wasm',
84+
wasmPath: options.wasmPath, // Optional - Ghostty.load() handles defaults
8385
};
8486

8587
this.cols = this.options.cols;

scripts/build-wasm.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ echo "✓ Found Zig $ZIG_VERSION"
2121
GHOSTTY_DIR="/tmp/ghostty-for-wasm"
2222
if [ ! -d "$GHOSTTY_DIR" ]; then
2323
echo "📦 Cloning Ghostty..."
24-
git clone --depth=1 https://github.com/ghostty-org/ghostty.git "$GHOSTTY_DIR"
24+
git clone --depth=1 https://github.com/coder/ghostty.git "$GHOSTTY_DIR"
2525
else
2626
echo "📦 Updating Ghostty..."
2727
cd "$GHOSTTY_DIR"

0 commit comments

Comments
 (0)