Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c3115f2
chore: ignore local agent workflows, commands, task notes, worktrees
diegosouzapw May 23, 2026
d007ffb
docs(types): correct scrollbackLimit field documentation (#1)
diegosouzapw May 23, 2026
be25fd3
fix: handle URLs with balanced parentheses in URL detection (#3)
diegosouzapw May 23, 2026
4216c7f
fix: forward wheel events with coordinates when mouse tracking is act…
diegosouzapw May 23, 2026
453309b
fix(demo): close RCE via unauthenticated cross-origin WebSocket + pat…
diegosouzapw May 23, 2026
2f64784
fix(renderer): align font metrics to device pixel boundaries to preve…
diegosouzapw May 23, 2026
b410b1a
feat: add focusOnOpen option to Terminal (#2)
diegosouzapw May 23, 2026
84a64fb
feat: add preserveScrollOnWrite option to lock viewport on new output…
diegosouzapw May 23, 2026
332d257
fix(selection): skip wide-character continuation cells when copying (#9)
diegosouzapw May 23, 2026
21483bd
feat: add ImagePasteAddon for clipboard image handling (#8)
diegosouzapw May 23, 2026
4b2b590
feat: add emitTerminalResponses option to suppress parser-generated r…
diegosouzapw May 23, 2026
31ed228
fix: ghost cursor at (0,0) (#122) and ESC k title leak (#153) (#16)
diegosouzapw May 23, 2026
74aa7f3
fix(wasm): zero-initialize WASM page buffers to prevent memory corrup…
diegosouzapw May 23, 2026
7abc3df
fix: cursor shape (DECSCUSR), Ctrl+V forwarding, and mouse scroll in …
diegosouzapw May 23, 2026
ebcc6eb
fix(input): route IME composition events to the hidden textarea (#11)
diegosouzapw May 23, 2026
8664e68
test: update focusOnOpen assertion to accept textarea focus from IME …
diegosouzapw May 23, 2026
5fb8df6
refactor(input): route every keydown through the Ghostty encoder (#10)
diegosouzapw May 23, 2026
6f88b04
feat: add dynamic theme changes via Terminal.setTheme() and options.t…
diegosouzapw May 23, 2026
740452a
perf: synchronous render after user input to reduce echo latency (#17)
diegosouzapw May 23, 2026
828627c
fix(patch): correct hunk line counts in ghostty-wasm-api.patch after …
diegosouzapw May 23, 2026
2c4725e
fix(input): enable Altβ†’ESC prefix and fix macOS Alt-transformed keys …
diegosouzapw May 23, 2026
7050803
fix: viewport corruption and stale cell data from page memory reuse (…
diegosouzapw May 24, 2026
72d5993
fix(deps): upgrade happy-dom to v20 + pin rollup/postcss CVEs (#20)
diegosouzapw May 24, 2026
ad81727
feat(terminal): render blank bootstrap state until first output (#21)
diegosouzapw May 24, 2026
7ce804c
feat(renderer): powerline + block char pixel-perfect rendering (#22)
diegosouzapw May 24, 2026
4f32102
feat(wasm): upgrade Ghostty 1.2 β†’ 1.3 with new C API and kitty graphics
diegosouzapw May 24, 2026
fc15959
feat(wasm): upgrade Ghostty 1.2 β†’ 1.3 with new C API and kitty graphics
diegosouzapw May 24, 2026
240a519
feat(terminal): add headless mode via TerminalCore base class
diegosouzapw May 24, 2026
83658aa
feat(terminal): add headless mode via TerminalCore base class
diegosouzapw May 24, 2026
588dac8
fix(terminal): sync textarea position to cursor for correct IME place…
diegosouzapw May 24, 2026
9904fbb
Merge pull request #25 from diegosouzapw/fix/ime-textarea-position
diegosouzapw May 24, 2026
3a08bc4
feat(terminal): add focus events (mode 1004) and synchronized output …
diegosouzapw May 24, 2026
7b5d60e
Merge pull request #26 from diegosouzapw/feat/focus-events-and-sync-o…
diegosouzapw May 24, 2026
74187cf
feat(terminal): add OSC 133 shell integration events and OSC 22 curso…
diegosouzapw May 24, 2026
00c4d3e
chore: bump version to 0.4.0
diegosouzapw May 24, 2026
99af62e
feat: comprehensive E2E suite, CHANGELOG, headless mode, and upstream…
diegosouzapw May 24, 2026
faf6fbd
chore: bump version to 0.4.1
diegosouzapw May 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,20 @@ dist/

# WASM build output (built locally and in CI)
ghostty-vt.wasm

# Local-only agent workflows, commands, task notes and worktrees
.agents/
.claude/
.worktrees/
_tasks/

# Local dev container config (not part of the project)
.devcontainer/

# npm pack artifacts
*.tgz

# Playwright generated output (reports, traces, screenshots, videos)
tests/e2e/report/
tests/e2e/results/
test-results/
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ coverage/
.vite/
bun.lock
ghostty/
tests/e2e/report/
tests/e2e/results/
326 changes: 326 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

229 changes: 228 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ xterm.js is everywhereβ€”VS Code, Hyper, countless web terminals. But it has fun

xterm.js reimplements terminal emulation in JavaScript. Every escape sequence, every edge case, every Unicode quirkβ€”all hand-coded. Ghostty's emulator is the same battle-tested code that runs the native Ghostty app.

### Keyboard encoding

Keyboard input is encoded by Ghostty's key encoder. Byte sequences largely match xterm.js's defaults β€” Home/End honor DECCKM, Shift+nav and Shift+F-keys preserve the Shift modifier in the emitted CSI sequence, non-BMP characters pass through, Arrow keys honor cursor-application mode. Two deliberate differences:

- **Shift+Enter is distinguishable from Enter** (emitted as `\x1b[27;2;13~` rather than bare `\r`, following fixterms), so modern line editors and REPLs can treat Shift+Enter as a newline-without-submit.
- **Kitty keyboard protocol and xterm modifyOtherKeys state 2 are supported** when an app enables them. xterm.js implements only the traditional escape sequences.

If you need byte-for-byte xterm.js behavior for a specific key (e.g. Shift+Enter mapped to `\r` for tools that don't understand the fixterms sequence), intercept it in `attachCustomKeyEventHandler` and emit the bytes you want via `term.input(bytes, true)`:

```ts
term.attachCustomKeyEventHandler((e) => {
if (e.key === 'Enter' && e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
term.input('\r', true); // fires onData with '\r'
return true; // suppress the default encoder path
}
return false;
});
```

## Installation

```bash
Expand Down Expand Up @@ -63,7 +82,191 @@ term.onData((data) => websocket.send(data));
websocket.onmessage = (e) => term.write(e.data);
```

For a comprehensive client <-> server example, refer to the [demo](./demo/index.html#L141).
For a comprehensive client ↔ server example, refer to the [demo](./demo/index.html).

## Headless Mode

`TerminalCore` provides a headless terminal (no DOM, no canvas) for server-side rendering,
testing, or non-browser environments:

```typescript
import { init, TerminalCore } from 'ghostty-web';

await init();

const term = new TerminalCore({ cols: 80, rows: 24 });
term.write('Hello World\r\n');

const line = term.buffer.active.getLine(0);
// inspect line cells...
```

`Terminal` extends `TerminalCore` with all browser rendering, input handling, and addon support.

## Shell Integration (OSC 133)

ghostty-web understands [OSC 133](https://iterm2.com/documentation-escape-codes.html) shell
integration sequences, letting you hook into shell prompt and command lifecycle events:

```typescript
term.onPromptStart(() => {
console.log('Shell prompt started');
});

term.onPromptEnd(() => {
console.log('Shell prompt ended β€” user can now type');
});

term.onCommandStart(() => {
console.log('Command execution began');
});

term.onCommandEnd((e) => {
console.log('Command finished, exit code:', e.exitCode);
});
```

Shells that support OSC 133 (fish, bash with the integration script, zsh with the plugin) emit
these sequences automatically.

## Cursor Shape (OSC 22)

Applications can request cursor shape changes via `OSC 22`:

```typescript
term.onMouseCursorChange((cursor) => {
// cursor is a CSS cursor string: 'default', 'text', 'pointer', etc.
document.body.style.cursor = cursor;
});
```

## Focus Events (DEC mode 1004)

When an application enables focus tracking (`\x1b[?1004h`), ghostty-web fires focus/blur
sequences to the PTY and emits events:

```typescript
term.onFocus(() => console.log('terminal focused'));
term.onBlur(() => console.log('terminal blurred'));
```

## Synchronized Output (DEC mode 2026)

ghostty-web respects the synchronized output mode (`\x1b[?2026h` / `\x1b[?2026l`),
deferring rendering until the application signals it is ready. A timeout guard prevents
indefinite hangs.

## Dynamic Theming

Themes can be set at construction time or updated at runtime:

```typescript
// At construction
const term = new Terminal({ theme: { background: '#000', foreground: '#fff' } });

// At runtime (triggers a re-render)
term.options.theme = {
background: '#1e1e2e',
foreground: '#cdd6f4',
cursor: '#f5e0dc',
black: '#45475a',
red: '#f38ba8',
// ...all 16 ANSI colors supported
};
```

## Selection API

```typescript
// Programmatic selection
term.select(col, row, length); // select N characters starting at col/row
term.selectAll(); // select all visible content
term.clearSelection(); // clear selection
term.hasSelection(); // boolean
term.getSelectionPosition(); // { start: {x, y}, end: {x, y} } | null

// Event
term.onSelectionChange(() => {
console.log('Selection changed');
});
```

Mouse selection (click-drag), `selectAll`, `clearSelection`, and `getSelectionPosition`
all work out of the box.

## Scrolling API

```typescript
term.scrollToTop();
term.scrollToBottom();
term.scrollLines(n); // positive = down, negative = up
term.scrollPages(n); // scroll by viewport height

term.onScroll((viewportY) => {
console.log('Scrolled to viewport offset', viewportY);
});

// Keep viewport pinned when new output arrives
term.options.preserveScrollOnWrite = true;
```

## FitAddon

```typescript
import { init, Terminal } from 'ghostty-web';
import { FitAddon } from 'ghostty-web/addons/fit';

await init();
const term = new Terminal();
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('terminal'));

fitAddon.fit(); // resize terminal to fill container
const dims = fitAddon.proposeDimensions(); // { cols, rows }

window.addEventListener('resize', () => fitAddon.fit());
```

## Addon API

ghostty-web supports the xterm.js addon interface:

```typescript
const addon = {
activate(terminal) {
// receives the Terminal instance
},
dispose() {
// called when terminal is disposed
},
};

term.loadAddon(addon);
```

## Events Reference

| Event | Payload | Description |
| --------------------- | ----------------------- | --------------------------------------- |
| `onData` | `string` | Raw bytes from keyboard / `input()` |
| `onWrite` | `string \| Uint8Array` | Data written to the terminal |
| `onWriteParsed` | β€” | After all buffered writes are processed |
| `onRender` | `{ start, end }` | After a render frame (row range) |
| `onResize` | `{ cols, rows }` | Terminal resized |
| `onScroll` | `number` | Viewport Y offset changed |
| `onLineFeed` | β€” | Line feed received |
| `onCursorMove` | β€” | Cursor position changed |
| `onSelectionChange` | β€” | Selection changed |
| `onTitleChange` | `string` | OSC 0/2 title escape |
| `onBell` | β€” | BEL character received |
| `onFocus` | β€” | Terminal focused (mode 1004) |
| `onBlur` | β€” | Terminal blurred (mode 1004) |
| `onPromptStart` | β€” | OSC 133;A β€” prompt started |
| `onPromptEnd` | β€” | OSC 133;B β€” prompt ended |
| `onCommandStart` | β€” | OSC 133;C β€” command execution started |
| `onCommandEnd` | `{ exitCode?: number }` | OSC 133;D β€” command finished |
| `onMouseCursorChange` | `string` | OSC 22 CSS cursor string |

## Development

Expand All @@ -76,6 +279,30 @@ functionality.
bun run build
```

### Getting the WASM without Zig

If you don't have Zig installed, you can pull the pre-built WASM from the latest npm release:

```bash
npm pack ghostty-web@latest
tar xf ghostty-web-*.tgz
cp package/ghostty-vt.wasm .
```

### Running E2E Tests

```bash
bun run test:e2e
```

Tests use [Playwright](https://playwright.dev/) with Chromium. The dev server starts automatically.

```bash
bun run test:e2e:headed # watch tests run in a real browser
bun run test:e2e:ui # Playwright UI mode
bun run test:e2e:report # open HTML report
```

Mitchell Hashimoto (author of Ghostty) has [been working](https://mitchellh.com/writing/libghostty-is-coming) on `libghostty` which makes this all possible. The patches are very minimal thanks to the work the Ghostty team has done, and we expect them to get smaller.

This library will eventually consume a native Ghostty WASM distribution once available, and will continue to provide an xterm.js compatible API.
Expand Down
12 changes: 11 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@
}
},
"files": {
"ignore": ["node_modules", "dist", "coverage", "*.wasm", ".git", ".vite", "bun.lock"]
"ignore": [
"node_modules",
"dist",
"coverage",
"*.wasm",
".git",
".vite",
"bun.lock",
"tests/e2e/report",
"tests/e2e/results"
]
}
}
Loading