wterm-web-terminal
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesewterm Web Terminal Emulator
wterm Web终端模拟器
Skill by ara.so — Daily 2026 Skills collection.
wterm ("dub-term") is a web terminal emulator with a Zig/WASM core (~12 KB binary) for near-native VT100/VT220/xterm parsing. It renders to the DOM — giving you native text selection, copy/paste, browser find, and accessibility for free. Supports WebSocket PTY backends, alternate screen buffers, 24-bit color, scrollback, and themes.
由ara.so提供的技能 — 2026每日技能合集。
wterm(读作“dub-term”)是一款Web终端模拟器,采用Zig/WASM核心(二进制文件约12 KB),可实现接近原生的VT100/VT220/xterm解析。它渲染至DOM,让你免费获得原生文本选择、复制粘贴、浏览器查找及无障碍访问能力。支持WebSocket PTY后端、交替屏幕缓冲区、24位颜色、回滚功能及主题定制。
Packages
包列表
| Package | Purpose |
|---|---|
| Headless WASM bridge + WebSocket transport |
| DOM renderer + input handler (vanilla JS) |
| React component + |
| In-browser Bash shell |
| Render Markdown in the terminal |
| 包名 | 用途 |
|---|---|
| 无头WASM桥接 + WebSocket传输层 |
| DOM渲染器 + 输入处理器(原生JS) |
| React组件 + |
| 浏览器内Bash Shell |
| 在终端中渲染Markdown |
Installation
安装
bash
undefinedbash
undefinedReact
React环境
npm install @wterm/react @wterm/core
npm install @wterm/react @wterm/core
Vanilla JS
原生JS环境
npm install @wterm/dom @wterm/core
npm install @wterm/dom @wterm/core
In-browser bash (no backend needed)
浏览器内Bash(无需后端)
npm install @wterm/just-bash @wterm/core
Copy the WASM binary to your public directory:
```bash
cp node_modules/@wterm/core/wterm.wasm public/npm install @wterm/just-bash @wterm/core
将WASM二进制文件复制到你的公共目录:
```bash
cp node_modules/@wterm/core/wterm.wasm public/React Usage
React使用示例
Basic Terminal Component
基础终端组件
tsx
import { Terminal } from '@wterm/react';
export default function App() {
return (
<div style={{ width: '800px', height: '500px' }}>
<Terminal
wsUrl={`ws://${window.location.host}/pty`}
theme="default"
/>
</div>
);
}tsx
import { Terminal } from '@wterm/react';
export default function App() {
return (
<div style={{ width: '800px', height: '500px' }}>
<Terminal
wsUrl={`ws://${window.location.host}/pty`}
theme="default"
/>
</div>
);
}useTerminal Hook
useTerminal钩子
tsx
import { useTerminal } from '@wterm/react';
import { useEffect, useRef } from 'react';
export default function CustomTerminal() {
const containerRef = useRef<HTMLDivElement>(null);
const { terminal, connect, disconnect, write, resize } = useTerminal({
wsUrl: process.env.NEXT_PUBLIC_PTY_WS_URL,
wasmUrl: '/wterm.wasm',
theme: 'monokai',
scrollback: 1000,
onData: (data) => console.log('Terminal output:', data),
onConnect: () => console.log('Connected to PTY'),
onDisconnect: () => console.log('Disconnected'),
});
useEffect(() => {
if (containerRef.current && terminal) {
terminal.mount(containerRef.current);
connect();
}
return () => disconnect();
}, [terminal]);
return (
<div
ref={containerRef}
style={{ width: '100%', height: '400px', background: '#1e1e1e' }}
/>
);
}tsx
import { useTerminal } from '@wterm/react';
import { useEffect, useRef } from 'react';
export default function CustomTerminal() {
const containerRef = useRef<HTMLDivElement>(null);
const { terminal, connect, disconnect, write, resize } = useTerminal({
wsUrl: process.env.NEXT_PUBLIC_PTY_WS_URL,
wasmUrl: '/wterm.wasm',
theme: 'monokai',
scrollback: 1000,
onData: (data) => console.log('Terminal output:', data),
onConnect: () => console.log('Connected to PTY'),
onDisconnect: () => console.log('Disconnected'),
});
useEffect(() => {
if (containerRef.current && terminal) {
terminal.mount(containerRef.current);
connect();
}
return () => disconnect();
}, [terminal]);
return (
<div
ref={containerRef}
style={{ width: '100%', height: '400px', background: '#1e1e1e' }}
/>
);
}Programmatic Input/Output
程序化输入/输出
tsx
import { useTerminal } from '@wterm/react';
export default function ProgrammaticTerminal() {
const { terminal, write } = useTerminal({
wasmUrl: '/wterm.wasm',
});
const runCommand = () => {
// Write VT100 escape sequences or plain text
write('\x1b[32mHello, world!\x1b[0m\r\n');
write('\x1b[1mBold text\x1b[0m\r\n');
};
return (
<>
<div ref={(el) => el && terminal?.mount(el)} style={{ height: 300 }} />
<button onClick={runCommand}>Write to terminal</button>
</>
);
}tsx
import { useTerminal } from '@wterm/react';
export default function ProgrammaticTerminal() {
const { terminal, write } = useTerminal({
wasmUrl: '/wterm.wasm',
});
const runCommand = () => {
// 写入VT100转义序列或纯文本
write('\x1b[32mHello, world!\x1b[0m\r\n');
write('\x1b[1mBold text\x1b[0m\r\n');
};
return (
<>
<div ref={(el) => el && terminal?.mount(el)} style={{ height: 300 }} />
<button onClick={runCommand}>Write to terminal</button>
</>
);
}Vanilla JS Usage
原生JS使用示例
html
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="node_modules/@wterm/dom/dist/wterm.css" />
</head>
<body>
<div id="terminal" style="width:800px;height:500px"></div>
<script type="module">
import { createTerminal } from '@wterm/dom';
const term = await createTerminal({
container: document.getElementById('terminal'),
wasmUrl: '/wterm.wasm',
wsUrl: 'ws://localhost:3001/pty',
theme: 'solarized-dark',
scrollback: 2000,
});
term.connect();
// Write directly
term.write('\x1b[33mWelcome!\x1b[0m\r\n');
// Resize programmatically
term.resize(120, 40); // cols, rows
</script>
</body>
</html>html
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="node_modules/@wterm/dom/dist/wterm.css" />
</head>
<body>
<div id="terminal" style="width:800px;height:500px"></div>
<script type="module">
import { createTerminal } from '@wterm/dom';
const term = await createTerminal({
container: document.getElementById('terminal'),
wasmUrl: '/wterm.wasm',
wsUrl: 'ws://localhost:3001/pty',
theme: 'solarized-dark',
scrollback: 2000,
});
term.connect();
// 直接写入内容
term.write('\x1b[33mWelcome!\x1b[0m\r\n');
// 程序化调整尺寸
term.resize(120, 40); // 列数, 行数
</script>
</body>
</html>In-Browser Bash (No Backend)
浏览器内Bash(无需后端)
tsx
import { Terminal } from '@wterm/react';
import { JustBashTransport } from '@wterm/just-bash';
export default function BrowserShell() {
return (
<Terminal
transport={new JustBashTransport()}
wasmUrl="/wterm.wasm"
theme="default"
style={{ width: '100%', height: '500px' }}
/>
);
}tsx
import { Terminal } from '@wterm/react';
import { JustBashTransport } from '@wterm/just-bash';
export default function BrowserShell() {
return (
<Terminal
transport={new JustBashTransport()}
wasmUrl="/wterm.wasm"
theme="default"
style={{ width: '100%', height: '500px' }}
/>
);
}Themes
主题
Built-in themes: , , ,
defaultsolarized-darkmonokailighttsx
// Via prop
<Terminal theme="monokai" wasmUrl="/wterm.wasm" wsUrl="..." />内置主题:、、、
defaultsolarized-darkmonokailighttsx
// 通过属性设置
<Terminal theme="monokai" wasmUrl="/wterm.wasm" wsUrl="..." />Custom Theme via CSS Custom Properties
通过CSS自定义属性定制主题
css
#terminal {
--wterm-bg: #0d1117;
--wterm-fg: #c9d1d9;
--wterm-cursor: #58a6ff;
--wterm-selection-bg: rgba(88, 166, 255, 0.3);
/* ANSI colors */
--wterm-color-0: #161b22; /* black */
--wterm-color-1: #ff7b72; /* red */
--wterm-color-2: #3fb950; /* green */
--wterm-color-3: #d29922; /* yellow */
--wterm-color-4: #58a6ff; /* blue */
--wterm-color-5: #bc8cff; /* magenta */
--wterm-color-6: #39c5cf; /* cyan */
--wterm-color-7: #b1bac4; /* white */
/* bright variants: --wterm-color-8 through --wterm-color-15 */
}css
#terminal {
--wterm-bg: #0d1117;
--wterm-fg: #c9d1d9;
--wterm-cursor: #58a6ff;
--wterm-selection-bg: rgba(88, 166, 255, 0.3);
/* ANSI颜色 */
--wterm-color-0: #161b22; /* 黑色 */
--wterm-color-1: #ff7b72; /* 红色 */
--wterm-color-2: #3fb950; /* 绿色 */
--wterm-color-3: #d29922; /* 黄色 */
--wterm-color-4: #58a6ff; /* 蓝色 */
--wterm-color-5: #bc8cff; /* 品红 */
--wterm-color-6: #39c5cf; /* 青色 */
--wterm-color-7: #b1bac4; /* 白色 */
/* 亮色变体:--wterm-color-8 至 --wterm-color-15 */
}WebSocket PTY Backend (Node.js)
WebSocket PTY后端(Node.js)
ts
// server.ts — example PTY backend using node-pty
import { WebSocketServer } from 'ws';
import * as pty from 'node-pty';
const wss = new WebSocketServer({ port: 3001, path: '/pty' });
wss.on('connection', (ws) => {
const shell = pty.spawn(process.env.SHELL || 'bash', [], {
name: 'xterm-256color',
cols: 80,
rows: 24,
cwd: process.env.HOME,
env: process.env as Record<string, string>,
});
// PTY → client (binary framing)
shell.onData((data) => {
if (ws.readyState === ws.OPEN) {
ws.send(Buffer.from(data, 'binary'));
}
});
// Client → PTY
ws.on('message', (msg: Buffer) => {
const text = msg.toString('binary');
// wterm sends resize as JSON: {"type":"resize","cols":120,"rows":40}
try {
const parsed = JSON.parse(text);
if (parsed.type === 'resize') {
shell.resize(parsed.cols, parsed.rows);
return;
}
} catch {}
shell.write(text);
});
ws.on('close', () => shell.kill());
shell.onExit(() => ws.close());
});ts
// server.ts — 使用node-pty的示例PTY后端
import { WebSocketServer } from 'ws';
import * as pty from 'node-pty';
const wss = new WebSocketServer({ port: 3001, path: '/pty' });
wss.on('connection', (ws) => {
const shell = pty.spawn(process.env.SHELL || 'bash', [], {
name: 'xterm-256color',
cols: 80,
rows: 24,
cwd: process.env.HOME,
env: process.env as Record<string, string>,
});
// PTY → 客户端(二进制帧)
shell.onData((data) => {
if (ws.readyState === ws.OPEN) {
ws.send(Buffer.from(data, 'binary'));
}
});
// 客户端 → PTY
ws.on('message', (msg: Buffer) => {
const text = msg.toString('binary');
// wterm 以JSON格式发送调整尺寸指令:{"type":"resize","cols":120,"rows":40}
try {
const parsed = JSON.parse(text);
if (parsed.type === 'resize') {
shell.resize(parsed.cols, parsed.rows);
return;
}
} catch {}
shell.write(text);
});
ws.on('close', () => shell.kill());
shell.onExit(() => ws.close());
});Next.js Integration
Next.js集成
bash
undefinedbash
undefinedInstall
安装依赖
npm install @wterm/react @wterm/core
npm install @wterm/react @wterm/core
Copy WASM to public
复制WASM文件到公共目录
cp node_modules/@wterm/core/wterm.wasm public/
```tsx
// components/Terminal.tsx
'use client';
import dynamic from 'next/dynamic';
// Must be client-only — no SSR
const WTerminal = dynamic(
() => import('@wterm/react').then((m) => m.Terminal),
{ ssr: false }
);
export default function TerminalPage() {
return (
<WTerminal
wsUrl={process.env.NEXT_PUBLIC_PTY_WS_URL}
wasmUrl="/wterm.wasm"
theme="monokai"
style={{ width: '100%', height: '600px' }}
/>
);
}cp node_modules/@wterm/core/wterm.wasm public/
```tsx
// components/Terminal.tsx
'use client';
import dynamic from 'next/dynamic';
// 必须仅在客户端运行 — 不支持SSR
const WTerminal = dynamic(
() => import('@wterm/react').then((m) => m.Terminal),
{ ssr: false }
);
export default function TerminalPage() {
return (
<WTerminal
wsUrl={process.env.NEXT_PUBLIC_PTY_WS_URL}
wasmUrl="/wterm.wasm"
theme="monokai"
style={{ width: '100%', height: '600px' }}
/>
);
}Markdown in Terminal
终端中渲染Markdown
ts
import { renderMarkdown } from '@wterm/markdown';
import { createTerminal } from '@wterm/dom';
const term = await createTerminal({ container, wasmUrl: '/wterm.wasm' });
const md = `# Hello\n\n**Bold** and *italic* text.\n\n\`\`\`js\nconsole.log('hi');\n\`\`\``;
term.write(renderMarkdown(md));ts
import { renderMarkdown } from '@wterm/markdown';
import { createTerminal } from '@wterm/dom';
const term = await createTerminal({ container, wasmUrl: '/wterm.wasm' });
const md = `# Hello\n\n**Bold** and *italic* text.\n\n\`\`\`js\nconsole.log('hi');\n\`\`\``;
term.write(renderMarkdown(md));Configuration Reference
配置参考
createTerminal
/ useTerminal
Options
createTerminaluseTerminalcreateTerminal
/ useTerminal
选项
createTerminaluseTerminal| Option | Type | Default | Description |
|---|---|---|---|
| | | Path to WASM binary |
| | — | WebSocket PTY endpoint |
| | — | Custom transport (overrides wsUrl) |
| | | Built-in theme name |
| | | Scrollback buffer rows |
| | auto | Initial column count |
| | auto | Initial row count |
| | — | Raw output callback |
| | — | Connection established |
| | — | Connection closed |
| | — | Resize event callback |
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| | | WASM二进制文件路径 |
| | — | WebSocket PTY端点 |
| | — | 自定义传输层(会覆盖wsUrl) |
| | | 内置主题名称 |
| | | 回滚缓冲区行数 |
| | 自动 | 初始列数 |
| | 自动 | 初始行数 |
| | — | 原始输出回调 |
| | — | 连接建立时触发 |
| | — | 连接关闭时触发 |
| | — | 尺寸调整事件回调 |
Development Setup
开发环境搭建
bash
undefinedbash
undefinedPrerequisites: Zig 0.15.2+, Node.js 20+, pnpm 10+
前置依赖:Zig 0.15.2+、Node.js 20+、pnpm 10+
npm install -g portless
git clone https://github.com/vercel-labs/wterm
cd wterm
pnpm install
npm install -g portless
git clone https://github.com/vercel-labs/wterm
cd wterm
pnpm install
Build WASM core
构建WASM核心
zig build # debug
zig build -Doptimize=ReleaseSmall # ~12 KB release
zig build # 调试版本
zig build -Doptimize=ReleaseSmall # 约12 KB的发布版本
Build all packages
构建所有包
pnpm build
pnpm build
Run Zig tests
运行Zig测试
zig build test
zig build test
Serve vanilla demo
启动原生JS示例服务
cd web && python3 -m http.server 8000
cd web && python3 -m http.server 8000
Run Next.js example
运行Next.js示例
cp web/wterm.wasm examples/nextjs/public/
pnpm --filter nextjs dev
cp web/wterm.wasm examples/nextjs/public/
pnpm --filter nextjs dev
Available at: nextjs-example.wterm.localhost
访问地址:nextjs-example.wterm.localhost
undefinedundefinedTroubleshooting
故障排查
WASM file not found (404)
bash
undefinedWASM文件未找到(404)
bash
undefinedEnsure wterm.wasm is in your public directory
确保wterm.wasm在你的公共目录中
cp node_modules/@wterm/core/wterm.wasm public/
cp node_modules/@wterm/core/wterm.wasm public/
In Next.js, verify it's at public/wterm.wasm
在Next.js中,确认文件位于public/wterm.wasm
**Terminal not rendering / blank screen**
- Container must have explicit `width` and `height` (not `auto`)
- Wrap in `dynamic(..., { ssr: false })` in Next.js — WASM requires browser APIs
- Check browser console for WASM instantiation errors
**WebSocket connection refused**
- Verify PTY backend is running and `wsUrl` matches
- Check CORS headers if backend is on a different origin
- Use `wss://` for HTTPS-served apps
**Text selection / copy not working**
- wterm uses DOM rendering, so native browser selection should work
- Ensure the container does not have `user-select: none` in CSS
**Resize not working**
- wterm uses `ResizeObserver` automatically; ensure the container resizes with the page
- For manual resize: `terminal.resize(cols, rows)`
**Alternate screen apps (vim, htop) display incorrectly**
- Ensure your PTY backend sets `TERM=xterm-256color`
- Verify the WebSocket sends binary (not UTF-8 string) frames
**终端未渲染 / 空白屏幕**
- 容器必须设置明确的`width`和`height`(不能为`auto`)
- 在Next.js中需用`dynamic(..., { ssr: false })`包裹 — WASM依赖浏览器API
- 检查浏览器控制台是否有WASM实例化错误
**WebSocket连接被拒绝**
- 确认PTY后端已运行且`wsUrl`匹配
- 若后端在不同源,检查CORS头配置
- HTTPS部署的应用请使用`wss://`协议
**文本选择/复制无法工作**
- wterm使用DOM渲染,原生浏览器选择功能应正常工作
- 确保容器CSS未设置`user-select: none`
**尺寸调整无效**
- wterm自动使用`ResizeObserver`;确保容器随页面一起调整尺寸
- 手动调整尺寸:`terminal.resize(cols, rows)`
**交替屏幕应用(vim、htop)显示异常**
- 确保你的PTY后端设置`TERM=xterm-256color`
- 验证WebSocket发送的是二进制帧(而非UTF-8字符串)