Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ wterm ("dub-term") renders to the DOM — native text selection, copy/paste, fin
- **24-bit color** — full RGB SGR support
- **Auto-resize** — `ResizeObserver`-based terminal resizing
- **WebSocket transport** — connect to a PTY backend with binary framing and reconnection
- **Inline images** — render PNGs via the [Kitty terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) as `<img>` overlays aligned to the cell grid

## Development

Expand Down
6 changes: 6 additions & 0 deletions apps/docs/src/app/api-reference/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ The React and Vue `<Terminal>` components and the vanilla `WTerm` constructor al
<td><code>false</code></td>
<td>Enable debug mode. Exposes a <code>DebugAdapter</code> on the <code>WTerm</code> instance (<code>wt.debug</code>) for inspecting escape sequences, cell data, render performance, and unhandled CSI sequences.</td>
</tr>
<tr>
<td><code>images</code></td>
<td><code>boolean</code></td>
<td><code>true</code></td>
<td>Enable inline image rendering via the <a href="https://sw.kovidgoyal.net/kitty/graphics-protocol/">Kitty terminal graphics protocol</a>. APC <code>{"\\x1b_G…\\x1b\\\\"}</code> sequences are intercepted and rendered as <code>&lt;img&gt;</code> overlays above the cell grid. Set to <code>false</code> to pass the bytes through to the core unchanged.</td>
</tr>
<tr>
<td><code>onData</code></td>
<td><code>(data: string) =&gt; void</code></td>
Expand Down
8 changes: 7 additions & 1 deletion apps/docs/src/app/configuration/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@

## Options

The React and Vue `Terminal` components and the vanilla `WTerm` constructor accept the same core options — `cols`, `rows`, `core`, `wasmUrl`, `autoResize`, `cursorBlink`, and `debug` — plus event callbacks `onData`, `onTitle`, and `onResize` (exposed as `@data`, `@title`, `@resize` events in Vue).
The React and Vue `Terminal` components and the vanilla `WTerm` constructor accept the same core options — `cols`, `rows`, `core`, `wasmUrl`, `autoResize`, `cursorBlink`, `debug`, and `images` — plus event callbacks `onData`, `onTitle`, and `onResize` (exposed as `@data`, `@title`, `@resize` events in Vue).

See the full [API Reference](/api-reference#terminal-options) for types, defaults, and descriptions.

### Pluggable cores

By default, wterm uses its built-in lightweight Zig WASM core (~12 KB). To use a different terminal emulation backend, pass a `TerminalCore` instance via the `core` option. See the [Ghostty Core](/ghostty) page for an example using libghostty.

### Inline images

wterm intercepts [Kitty terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) APC sequences and renders the transmitted PNGs as `<img>` overlays absolutely positioned over the cell grid. Enabled by default — set `images: false` to disable the filter and pass the bytes through to the core unchanged.

Supported actions: `t` (transmit), `T` (transmit + display), `p` (put placement), `d` (delete). Supported format: PNG (`f=100`) via direct base64 transport, including chunked transfers (`m=1`/`m=0`). Not yet supported: raw RGB/RGBA frames, file/shared-memory transports, virtual-placement via Unicode placeholders, animations, Sixel, and iTerm2 inline images.

### React-only

The React `Terminal` component adds `theme`, `onReady`, and `onError` on top of the shared options. It also spreads standard HTML attributes onto the root `div`, so you can pass `className`, `style`, ARIA attributes, and other DOM props directly.
Expand Down
29 changes: 29 additions & 0 deletions examples/kitty-images/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Kitty Graphics Protocol Example

A self-contained demo of inline image rendering in `wterm` via the [Kitty terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/).

Generates a PNG on the fly with `<canvas>` and transmits it through the protocol's chunked APC `\x1b_G...\x1b\\` transport. `@wterm/dom` intercepts the sequence and renders the image as an absolutely-positioned overlay aligned to the cell grid.

## Setup

From the monorepo root:

```bash
pnpm install
pnpm --filter kitty-images-example dev
```

Opens at `kitty-images.wterm.localhost` via [portless](https://github.com/vercel-labs/portless).

## How It Works

- `WTerm` enables image handling by default; APC `_G...` sequences are stripped from the byte stream before it reaches the VT core.
- The protocol payload is base64-encoded PNG bytes. Multi-chunk transfers (`m=1` … `m=0`) accumulate before decoding.
- Each placement is anchored to the cursor row at the moment the sequence is processed. Because new lines are inserted into scrollback above the grid, the image stays pixel-aligned with its content as the screen scrolls.

## Key Files

| File | Description |
| ------------- | ------------------------------------------------------------------- |
| `src/main.ts` | Generates a PNG with canvas and transmits it as a chunked Kitty APC |
| `index.html` | Minimal HTML with `<div id="terminal">` |
27 changes: 27 additions & 0 deletions examples/kitty-images/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>wterm — Kitty Graphics Protocol</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
background: #1a1a2e;
}
#terminal {
height: 100%;
}
</style>
</head>
<body>
<div id="terminal"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions examples/kitty-images/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "kitty-images-example",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
"dev": "portless kitty-images.wterm vite",
"build": "vite build",
"preview": "vite preview",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@wterm/dom": "workspace:*",
"@wterm/ghostty": "workspace:*"
},
"devDependencies": {
"typescript": "^6.0.2",
"vite": "^6.3.5"
}
}
78 changes: 78 additions & 0 deletions examples/kitty-images/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { WTerm } from "@wterm/dom";
import { GhosttyCore } from "@wterm/ghostty";
import "@wterm/dom/css";

const el = document.getElementById("terminal")!;

const core = await GhosttyCore.load();
const term = new WTerm(el, { core });
await term.init();

term.write(
"\x1b[1;36mwterm\x1b[0m — \x1b[1;35mKitty graphics protocol\x1b[0m demo\r\n\r\n",
);

term.write(
"PNG image transmitted via APC `\\x1b_G...` sequence and rendered\r\n" +
"as an absolutely-positioned <img> overlay above the cell grid.\r\n\r\n",
);

const pngBytes = await drawSamplePng(200, 100);
const b64 = base64FromBytes(pngBytes);

// Chunked transfer: real apps split the base64 payload across multiple
// `m=1` chunks with a final `m=0`. Demo it here with 4 KiB chunks.
const CHUNK = 4096;
let offset = 0;
let first = true;
while (offset < b64.length) {
const end = Math.min(offset + CHUNK, b64.length);
const isLast = end >= b64.length;
const slice = b64.slice(offset, end);
const control = first
? `a=T,f=100,i=1,c=25,r=6,m=${isLast ? 0 : 1}`
: `i=1,m=${isLast ? 0 : 1}`;
term.write(`\x1b_G${control};${slice}\x1b\\`);
offset = end;
first = false;
}

term.write(
"\r\n\x1b[2mYour PNG above. Press any key — input echoes.\x1b[0m\r\n",
);

async function drawSamplePng(w: number, h: number): Promise<Uint8Array> {
const canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d")!;

const grad = ctx.createLinearGradient(0, 0, w, h);
grad.addColorStop(0, "#ff7eb6");
grad.addColorStop(0.5, "#be95ff");
grad.addColorStop(1, "#33b1ff");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);

ctx.fillStyle = "rgba(255,255,255,0.95)";
ctx.font = "bold 28px system-ui, sans-serif";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillText("wterm 🚀", w / 2, h / 2);

const blob: Blob = await new Promise((resolve) =>
canvas.toBlob((b) => resolve(b!), "image/png"),
);
return new Uint8Array(await blob.arrayBuffer());
}

function base64FromBytes(bytes: Uint8Array): string {
let s = "";
const STEP = 0x8000;
for (let i = 0; i < bytes.length; i += STEP) {
s += String.fromCharCode(
...bytes.subarray(i, Math.min(i + STEP, bytes.length)),
);
}
return btoa(s);
}
13 changes: 13 additions & 0 deletions examples/kitty-images/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"isolatedModules": true,
"skipLibCheck": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"]
},
"include": ["src", "*.d.ts"]
}
7 changes: 7 additions & 0 deletions examples/kitty-images/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "vite";

export default defineConfig({
build: {
target: "esnext",
},
});
1 change: 1 addition & 0 deletions examples/kitty-images/wterm-dom.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "@wterm/dom/css";
15 changes: 15 additions & 0 deletions packages/@wterm/dom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ new WTerm(element: HTMLElement, options?: WTermOptions)
| `autoResize` | `boolean` | `true` | Auto-resize based on container dimensions |
| `cursorBlink` | `boolean` | `false` | Enable cursor blinking animation |
| `debug` | `boolean` | `false` | Enable debug mode. Exposes a `DebugAdapter` on the instance (`wt.debug`) for inspecting escape sequences, cell data, render performance, and unhandled CSI sequences. |
| `images` | `boolean` | `true` | Enable inline image rendering via the [Kitty terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/). When enabled, `\x1b_G…\x1b\\` APC sequences are intercepted and rendered as `<img>` overlays above the cell grid. Set to `false` to pass the bytes through to the core unchanged. |
| `onData` | `(data: string) => void` | — | Called when the terminal produces data (user input or host response). When omitted, input is echoed back automatically. |
| `onTitle` | `(title: string) => void` | — | Called when the terminal title changes |
| `onResize` | `(cols: number, rows: number) => void` | — | Called on resize |
Expand Down Expand Up @@ -79,6 +80,20 @@ ws.connect();
term.onData = (data) => ws.send(data);
```

## Inline images (Kitty graphics protocol)

When `images: true` (default), wterm intercepts Kitty graphics protocol APC sequences and renders the transmitted PNG as an absolutely-positioned `<img>` overlay aligned to the cell grid. Supports inline base64 transfers (`f=100`) and multi-chunk `m=1`/`m=0` payloads. Actions: `t` (transmit), `T` (transmit + display), `p` (put placement), `d` (delete).

```ts
const png = new Uint8Array(
await fetch("/icon.png").then((r) => r.arrayBuffer()),
);
const b64 = btoa(String.fromCharCode(...png));
term.write(`\x1b_Ga=T,f=100,i=1,c=20,r=5;${b64}\x1b\\`);
```

Not yet supported: raw RGB/RGBA frames (`f=24`/`f=32`), file/shared-memory transports, virtual-placement via Unicode placeholders, animations, Sixel, and iTerm2 inline images.

## Themes

Import the stylesheet and apply a theme class to the terminal element:
Expand Down
Loading