wterm-web-terminal

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

wterm 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

包列表

PackagePurpose
@wterm/core
Headless WASM bridge + WebSocket transport
@wterm/dom
DOM renderer + input handler (vanilla JS)
@wterm/react
React component +
useTerminal
hook
@wterm/just-bash
In-browser Bash shell
@wterm/markdown
Render Markdown in the terminal
包名用途
@wterm/core
无头WASM桥接 + WebSocket传输层
@wterm/dom
DOM渲染器 + 输入处理器(原生JS)
@wterm/react
React组件 +
useTerminal
钩子
@wterm/just-bash
浏览器内Bash Shell
@wterm/markdown
在终端中渲染Markdown

Installation

安装

bash
undefined
bash
undefined

React

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:
default
,
solarized-dark
,
monokai
,
light
tsx
// Via prop
<Terminal theme="monokai" wasmUrl="/wterm.wasm" wsUrl="..." />
内置主题:
default
solarized-dark
monokai
light
tsx
// 通过属性设置
<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
undefined
bash
undefined

Install

安装依赖

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

createTerminal
/
useTerminal
选项

OptionTypeDefaultDescription
wasmUrl
string
'/wterm.wasm'
Path to WASM binary
wsUrl
string
WebSocket PTY endpoint
transport
Transport
Custom transport (overrides wsUrl)
theme
string
'default'
Built-in theme name
scrollback
number
1000
Scrollback buffer rows
cols
number
autoInitial column count
rows
number
autoInitial row count
onData
(data: string) => void
Raw output callback
onConnect
() => void
Connection established
onDisconnect
() => void
Connection closed
onResize
(cols, rows) => void
Resize event callback
选项类型默认值描述
wasmUrl
string
'/wterm.wasm'
WASM二进制文件路径
wsUrl
string
WebSocket PTY端点
transport
Transport
自定义传输层(会覆盖wsUrl)
theme
string
'default'
内置主题名称
scrollback
number
1000
回滚缓冲区行数
cols
number
自动初始列数
rows
number
自动初始行数
onData
(data: string) => void
原始输出回调
onConnect
() => void
连接建立时触发
onDisconnect
() => void
连接关闭时触发
onResize
(cols, rows) => void
尺寸调整事件回调

Development Setup

开发环境搭建

bash
undefined
bash
undefined

Prerequisites: 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

undefined
undefined

Troubleshooting

故障排查

WASM file not found (404)
bash
undefined
WASM文件未找到(404)
bash
undefined

Ensure 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字符串)