wterm

repository·main·Indexed 25 days ago

https://github.com/vercel-labs/wterm

A high-performance web terminal emulator featuring a Zig-based WASM core that renders directly to the DOM for native text selection and accessibility. It includes a headless core (@wterm/core), a DOM renderer (@wterm/dom), and framework integrations for React and Vue. The library supports multiple backends, including a lightweight built-in core and a full-featured Ghostty core (@wterm/ghostty) for advanced VT emulation, as well as browser-based Bash shells via @wterm/just-bash.

Tokens
25.4K
Snippets
93
Records
180
Agent score
81%

What's inside wterm

  1. Overview of wterm packages

    main

    wterm is a terminal emulator for the web that renders to the DOM, providing native text selection, copy/paste, and accessibility. It uses a Zig-based WASM core for performance. Depending on your framework or requirements, you can use different packages:

    • @wterm/core: Headless WASM bridge, TerminalCore interface, and WebSocket transport.
    • @wterm/dom: Vanilla JS terminal with DOM renderer and input handler.
    • @wterm/react: React component and useTerminal hook (TypeScript).
    • @wterm/vue: Vue 3 component with template ref API.
    • @wterm/ghostty: Full-featured VT emulation core powered by libghostty.
    • @wterm/just-bash: In-browser Bash shell powered by just-bash.
    • @wterm/markdown: Markdown rendering within the terminal.
  2. Overview of wterm features

    main

    wterm (dub-term) is a terminal emulator for the web that renders directly to the DOM, providing native text selection, copy/paste, browser find, and accessibility support. The core is written in Zig and compiled to a ~12 KB WASM binary for high performance.

    Key capabilities include:

    • VT100/VT220/xterm support: High-performance escape sequence parsing via WASM.
    • Efficient Rendering: Uses dirty-row tracking to only re-render touched rows via requestAnimationFrame.
    • Full Terminal Features: Supports alternate screen buffers (for tools like vim, less, htop), configurable scrollback history ring buffers, and 24-bit RGB SGR color.
    • Responsive Design: Auto-resizing via ResizeObserver.
    • Theming: Built-in themes (Default, Solarized Dark, Monokai, and Light) using CSS custom properties.
    • Connectivity: Supports WebSocket transport for connecting to PTY backends with reconnection logic.
  3. Understand the Next.js Example Architecture

    main

    The Next.js example is built using the following core components:

    • Rendering: Uses @wterm/react to render the terminal via the <Terminal> component and the useTerminal hook.
    • Shell: Uses @wterm/just-bash to provide a Bash shell that runs entirely in the browser without a backend.
    • Theming: Includes a theme selector that supports Default, Solarized Dark, Monokai, and Light themes.
    • Filesystem: Preloads virtual files such as README.md, package.json, main.zig, and hello.sh into the shell environment.
  4. Understand the Vite example architecture

    main

    The Vite example is composed of the following key components:

    • @wterm/dom: Responsible for creating and managing the terminal instance inside a DOM element.
    • @wterm/just-bash: Provides the browser-based Bash shell execution.
    • src/main.ts: The entry point where the terminal is instantiated and the bash shell is attached.
    • index.html: Contains the minimal HTML structure, specifically a <div id="terminal"> where the terminal is mounted.
  5. Understand the SSH Client Architecture

    main

    The SSH client operates using a WebSocket-to-SSH bridge:

    1. Connection Initiation: The browser sends SSH connection parameters (host, port, username, and authentication details) as the initial WebSocket message.
    2. Server Bridge: A custom server (server.ts) manages an HTTP and WebSocket server alongside Next.js. It uses the ssh2 library to establish an SSH connection.
    3. Stream Piping: The server pipes the SSH shell stream to and from the WebSocket, allowing terminal interaction in the browser.
    4. Authentication: The implementation supports both password and private key authentication.
  6. Serve the WASM binary separately

    main

    By default, the ~12 KB WASM binary is embedded in the JS bundle. To serve it as a separate static file (e.g., for CDN or caching), copy the WASM file from the @wterm/core package to your public directory and provide the wasmUrl prop to the component.

    cp node_modules/@wterm/core/wasm/wterm.wasm public/wterm.wasm
    <Terminal wasmUrl="/wterm.wasm" />
  7. Run the Vue Example

    main

    To run the Vue 3 + Vite port of the wterm example, which features an in-browser terminal running just-bash with theme switching and a virtual filesystem, follow these steps from the monorepo root:

    1. Install dependencies using pnpm.
    2. Build the WASM binary using zig.
    3. Start the development server for the vue package.

    The application will be accessible at vue-example.wterm.localhost via portless.

    pnpm install
    zig build
    pnpm --filter vue dev
  8. Configure terminal themes

    main

    To use built-in themes, you must import the @wterm/react/css stylesheet. You can switch themes by passing the theme prop to the <Terminal> component.

    Built-in themes:

    • solarized-dark
    • monokai
    • light

    Custom themes can be defined using CSS custom properties.

    import "@wterm/react/css";
    
    // ...
    <Terminal theme="monokai" />
  9. Stream LLM Output with MarkdownRenderer

    main

    To render streaming LLM output in real-time:

    1. Create a MarkdownRenderer instance before the stream starts.
    2. As each chunk arrives, call md.push(chunk). This buffers incomplete lines and only returns output for complete lines.
    3. When the stream ends, call md.flush() to render any remaining buffered content and close open code blocks.
    4. Write each non-empty result to the terminal using your terminal's write() method.
    import { MarkdownRenderer } from "@wterm/markdown";
    
    const md = new MarkdownRenderer();
    
    async function streamChat(
      write: (data: string) => void,
      prompt: string,
    ) {
      const response = await fetch("/api/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ prompt }),
      });
    
      const reader = response.body!.getReader();
      const decoder = new TextDecoder();
    
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
    
        const chunk = decoder.decode(value, { stream: true });
        const rendered = md.push(chunk);
        if (rendered) write(rendered);
      }
    
      const remaining = md.flush();
      if (remaining) write(remaining);
    }