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 @@ -13,6 +13,7 @@ wterm ("dub-term") renders to the DOM — native text selection, copy/paste, fin
| [`@wterm/react`](packages/@wterm/react) | React component + `useTerminal` hook (TypeScript) |
| [`@wterm/just-bash`](packages/@wterm/just-bash) | In-browser Bash shell powered by just-bash |
| [`@wterm/markdown`](packages/@wterm/markdown) | Render Markdown in the terminal |
| [`@wterm/serialize`](packages/@wterm/serialize) | Serialize and restore terminal state |

## Features

Expand Down
32 changes: 32 additions & 0 deletions apps/docs/src/app/api-reference/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -405,3 +405,35 @@ interface CursorState {
visible: boolean;
}
```

## @wterm/serialize

Serialize and restore terminal sessions for persistence across reloads.

### API

```ts
import { serialize, restore } from "@wterm/serialize";

interface TerminalSnapshot {
version: 1;
cols: number;
rows: number;
payload: string;
cursor: { row: number; col: number; visible: boolean };
modes: {
altScreen: boolean;
cursorKeysApp: boolean;
bracketedPaste: boolean;
};
}

function serialize(term: WTerm): TerminalSnapshot;
function restore(term: WTerm, snapshot: TerminalSnapshot): void;
```

### Notes

- `serialize()` throws if called before `term.init()`.
- `restore()` throws for unsupported snapshot versions.
- v1 snapshots do not capture partial parser state or terminal title state.
7 changes: 7 additions & 0 deletions apps/docs/src/app/serialize/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";

export const metadata = pageMetadata("serialize");

export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
118 changes: 118 additions & 0 deletions apps/docs/src/app/serialize/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Serialize

Persist and restore terminal sessions with `@wterm/serialize`. Capture a snapshot of terminal content and replay it on the next page load.

## Install

```bash
npm install @wterm/serialize
```

## Why Persistence

Session persistence is useful when:

- You want terminal output to survive page refreshes.
- You need to resume a session after navigation.
- You want fast restoration of UI state without replaying backend logs.

## Quick Start

### Vanilla JS

```js
import { WTerm } from "@wterm/dom";
import { serialize, restore } from "@wterm/serialize";
import "@wterm/dom/css";

const STORAGE_KEY = "wterm:snapshot";

const term = new WTerm(document.getElementById("terminal"));
await term.init();

const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
restore(term, JSON.parse(saved));
} catch {
localStorage.removeItem(STORAGE_KEY);
}
}

window.addEventListener("beforeunload", () => {
const snapshot = serialize(term);
localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
});
```

## API

### `TerminalSnapshot`

<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>version</code></td>
<td><code>1</code></td>
<td>Snapshot format version</td>
</tr>
<tr>
<td><code>cols</code></td>
<td><code>number</code></td>
<td>Terminal column count when serialized</td>
</tr>
<tr>
<td><code>rows</code></td>
<td><code>number</code></td>
<td>Terminal row count when serialized</td>
</tr>
<tr>
<td><code>payload</code></td>
<td><code>string</code></td>
<td>ANSI payload containing screen + scrollback content</td>
</tr>
<tr>
<td><code>cursor</code></td>
<td><code>&#123; row, col, visible &#125;</code></td>
<td>Cursor position and visibility</td>
</tr>
<tr>
<td><code>modes</code></td>
<td><code>&#123; altScreen, cursorKeysApp, bracketedPaste &#125;</code></td>
<td>Terminal mode flags restored after payload</td>
</tr>
</tbody>
</table>

### Functions

<table>
<thead>
<tr>
<th>Function</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>serialize(term: WTerm): TerminalSnapshot</code></td>
<td>Captures terminal content, cursor state, and mode flags.</td>
</tr>
<tr>
<td><code>restore(term: WTerm, snapshot: TerminalSnapshot): void</code></td>
<td>Replays payload and restores mode/cursor state.</td>
</tr>
</tbody>
</table>

## Limitations

- Partial parser state in the middle of an escape sequence is not captured.
- Terminal title state is not captured in v1.
1 change: 1 addition & 0 deletions apps/docs/src/lib/docs-navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const navGroups: NavGroup[] = [
items: [
{ name: "Just Bash", href: "/just-bash" },
{ name: "Markdown", href: "/markdown" },
{ name: "Serialize", href: "/serialize" },
{ name: "Core / Advanced", href: "/core" },
],
},
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/lib/page-titles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const PAGE_TITLES: Record<string, string> = {
vanilla: "Vanilla JS",
"just-bash": "Just Bash",
markdown: "Markdown",
serialize: "Serialize",
core: "Core / Advanced",
"api-reference": "API Reference",
};
Expand Down
1 change: 1 addition & 0 deletions packages/@wterm/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Headless terminal emulator core for [wterm](https://github.com/vercel-labs/wterm
| [`@wterm/react`](https://www.npmjs.com/package/@wterm/react) | React component + `useTerminal` hook |
| [`@wterm/just-bash`](https://www.npmjs.com/package/@wterm/just-bash) | In-browser Bash shell powered by just-bash |
| [`@wterm/markdown`](https://www.npmjs.com/package/@wterm/markdown) | Streaming Markdown-to-ANSI renderer for terminals |
| [`@wterm/serialize`](https://www.npmjs.com/package/@wterm/serialize) | Serialize and restore terminal state |

## Install

Expand Down
65 changes: 65 additions & 0 deletions packages/@wterm/serialize/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# @wterm/serialize

Serialize and restore terminal state for [wterm](https://github.com/vercel-labs/wterm). Capture a snapshot of the current grid + scrollback and restore it later for session persistence.

## Install

```bash
npm install @wterm/serialize
```

## Quick Start

```ts
import { WTerm } from "@wterm/dom";
import { serialize, restore } from "@wterm/serialize";
import "@wterm/dom/css";

const term = new WTerm(document.getElementById("terminal")!);
await term.init();

const saved = localStorage.getItem("terminal:snapshot");
if (saved) {
restore(term, JSON.parse(saved));
}

window.addEventListener("beforeunload", () => {
const snapshot = serialize(term);
localStorage.setItem("terminal:snapshot", JSON.stringify(snapshot));
});
```

## API

### `TerminalSnapshot`

```ts
interface TerminalSnapshot {
version: 1;
cols: number;
rows: number;
payload: string;
cursor: { row: number; col: number; visible: boolean };
modes: {
altScreen: boolean;
cursorKeysApp: boolean;
bracketedPaste: boolean;
};
}
```

### Functions

```ts
serialize(term: WTerm): TerminalSnapshot
restore(term: WTerm, snapshot: TerminalSnapshot): void
```

## Limitations

- Partial parser state in the middle of an escape sequence is not captured.
- Terminal title state is not captured in v1.

## License

Apache-2.0
46 changes: 46 additions & 0 deletions packages/@wterm/serialize/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@wterm/serialize",
"version": "0.1.8",
"description": "Serialize and restore wterm terminal state",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"prepublishOnly": "pnpm build",
"test": "vitest run",
"type-check": "tsc --noEmit"
},
"devDependencies": {
"@internal/ts": "workspace:*",
"typescript": "^6.0.2"
},
"peerDependencies": {
"@wterm/core": "workspace:*",
"@wterm/dom": "workspace:*"
},
"keywords": [
"terminal",
"wterm",
"serialize",
"persistence",
"ansi"
],
"license": "Apache-2.0",
"homepage": "https://wterm.dev",
"repository": {
"type": "git",
"url": "https://github.com/vercel-labs/wterm",
"directory": "packages/@wterm/serialize"
}
}
23 changes: 23 additions & 0 deletions packages/@wterm/serialize/src/__tests__/cursor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, it, expect } from "vitest";
import { restore, serialize } from "../index.js";
import { makeTerm } from "./helpers.js";

describe("cursor", () => {
it("preserves cursor position and visibility through roundtrip", async () => {
// Uses a minimal shim, not a real DOM WTerm, because serialize() only touches bridge + write().
const term = await makeTerm(20, 5);
term.write("abc\r\nxy");
term.write("\x1b[?25l");

const snapshot = serialize(term as any);
const restored = await makeTerm(20, 5);
restore(restored as any, snapshot);

const before = term.bridge!.getCursor();
const after = restored.bridge!.getCursor();

expect(after.row).toBe(before.row);
expect(after.col).toBe(before.col);
expect(after.visible).toBe(before.visible);
});
});
Loading