❯_termy
DeveloperWeb terminal

Web terminal

Run Termy's terminal engine in the browser with the @termysh npm packages.

The @termysh packages are new and pre-1.0. The API can change between releases, so pin a version and test upgrades.

The @termysh packages bring Termy's terminal engine to the web. The parser, grid, scrollback, keyboard and mouse encoders, kitty protocols, box-drawing geometry and themes are the same Rust code the desktop app runs, compiled to WebAssembly. Use them to build a browser terminal or to replace xterm.js.

Choose a package

PackageWhat it isUse it when
@termysh/webBrowser terminal with renderer, input, selection, links and imagesYou are building something new
@termysh/xtermxterm.js-compatible API on top of @termysh/webYou are replacing @xterm/xterm
@termysh/coreWebAssembly engine with a typed headless APIYou render yourself, run in a worker or Node, or test terminal output
@termysh/ptyPseudo-terminals for Node.js and Bun, a drop-in replacement for node-ptyYour server runs the shell or program the terminal shows

Install

npm install @termysh/web
# or: bun add @termysh/web / pnpm add @termysh/web

@termysh/web depends on @termysh/core, which ships the termy.wasm module. There is no stylesheet to import.

Quick start

<div id="terminal" style="height: 400px"></div>
import { Terminal } from '@termysh/web'

const term = new Terminal({ theme: 'tokyo-night', fontSize: 13, cursorBlink: true })
term.open(document.getElementById('terminal')!)
term.write('Hello from \x1b[1;32mTermy\x1b[0m\r\n')
term.onData((data) => console.log('send to the host:', JSON.stringify(data)))

The terminal fills its parent element and, with autoFit (on by default), resizes its grid whenever the parent changes size. Give the parent a height.

To run a real shell, connect write and onData to a PTY on a server. The Connect to a shell guide has a complete server and client.

Essentials

These points cover most integration bugs:

  1. The parent needs a size. The terminal is width: 100%; height: 100% of the element you pass to open. A parent with no height renders nothing.
  2. The constructor is synchronous; the engine is not. The WebAssembly module loads in the background. write, resize, paste and other calls made before it is ready are queued. Await term.ready (or use await Terminal.create(options)) before you read state such as getLine, cursor or builtinThemeIds().
  3. Send everything from onData to the host. It carries keystrokes, pastes and the terminal's protocol replies (cursor position reports, device attributes, color queries). Programs like vim wait for those replies.
  4. Send every onResize to the host. Programs lay out for the PTY's size, not the terminal's. Forward the new cols and rows to the PTY.
  5. Call dispose() when you are done. It removes the element, stops timers and frees the engine's memory. In React, do it in the effect cleanup.
  6. Do not parse escape sequences yourself. Write raw bytes in and let the engine handle them. Use events such as onTitleChange and onCwdChange for the metadata programs send.

Load the WebAssembly module

By default termy.wasm is resolved next to @termysh/core with new URL('./termy.wasm', import.meta.url). Vite, Next.js and webpack emit the file automatically, with small caveats for some versions. See Frameworks and bundlers.

To serve the file yourself, copy node_modules/@termysh/core/dist/termy.wasm to your static assets and pass its URL:

new Terminal({ wasm: '/assets/termy.wasm' })
// or, once at startup, before creating terminals:
import { init } from '@termysh/web'
await init('/assets/termy.wasm')

Serve it with the application/wasm content type for the fastest startup.

API

const term = new Terminal(options?)  // or: await Terminal.create(options)
term.ready                           // Promise<void>, resolves when the engine is loaded
term.open(parent)                    // mount; throws if already open
term.write(data, callback?)          // string or Uint8Array; callback runs once parsed
term.writeln(data)
term.paste(text)                     // honors bracketed paste
term.input(text)                     // as if the user typed it (fires onData)
term.resize(cols, rows)
term.fit()                           // resize to the container now
term.proposeDimensions()             // { cols, rows } that would fit, without resizing
term.setOptions({ fontSize: 15 })    // any option, at runtime
term.setOption(key, value) / term.getOption(key)
term.focus() / term.blur() / term.hasFocus
term.clear()                         // clear scrollback, keep the prompt line
term.reset()                         // full reset (RIS)
term.processExited()                 // tell the terminal the host process ended
term.scrollLines(n) / term.scrollPages(n) / term.scrollToTop() / term.scrollToBottom() / term.scrollToLine(line)
term.select(col, line, length) / term.selectLines(start, end) / term.selectAll() / term.clearSelection()
term.getSelection() / term.hasSelection() / term.getSelectionPosition()
term.getLine(line) / term.isLineWrapped(line)
term.cols / term.rows / term.cursor / term.historySize / term.viewportY / term.modes / term.title
term.attachCustomKeyEventHandler((event) => true)  // return false to stop the terminal handling a key
term.element / term.textarea / term.cellSize
term.core                            // the underlying TermyCore, for advanced use
term.dispose()

Buffer lines are absolute: 0 is the oldest scrollback line and historySize is the first line of the live screen. The text on the cursor's line is term.getLine(term.historySize + term.cursor!.row) once term.ready has resolved.

Events

Every event is a function that takes a listener and returns a disposable:

const subscription = term.onTitleChange((title) => (document.title = title))
subscription.dispose()
EventPayloadFires when
onDatastringInput for the host: keys, paste, protocol replies
onBinarystringLegacy X10 mouse reports that are not valid UTF-8, as a binary string
onBytesUint8ArrayEvery byte sent to the host, whatever its encoding
onResize{ cols, rows }The grid size changes
onTitleChangestringThe application sets the title (OSC 0/2)
onBellBEL
onCwdChangestringOSC 7 working directory
onProgress{ state, value }OSC 9;4 progress
onProgramStatusProgramStatusRecord[]OSC 7501 program status changes. See Program status
onShellIntegrationstringOSC 133 prompt and command marks
onClipboard{ selection, text }OSC 52 clipboard write
onKey{ key, domEvent }A key produced input
onScrollnumberThe viewport moves
onRender{ start, end }Rows were repainted
onSelectionChange, onCursorMove, onWriteParsed, onFocus, onBlurAs named

onBytes is the simplest choice when your transport carries bytes: it includes everything onData and onBinary send.

Protocol support

The web terminal inherits the desktop engine's protocol support:

  • xterm and VT control sequences, 256-color and truecolor
  • Styled and colored underlines (curly, dotted, dashed, double)
  • OSC 8 hyperlinks, plus plain URL detection
  • OSC 4/10/11/12 color changes and queries, OSC 7, OSC 52, OSC 133, OSC 9;4, OSC 7501
  • Synchronized output (mode 2026), bracketed paste, focus events
  • Mouse tracking with default, UTF-8 and SGR encodings
  • The kitty keyboard protocol, including release events
  • The kitty graphics protocol: RGBA, RGB and PNG images, placements, z-index layering, animation and Unicode placeholders

Not yet supported: sixel and iTerm2 inline images, kitty OSC 5522 clipboard, and xterm.js decorations, custom link providers and parser hooks.

For AI coding agents

Every page in these docs is available as Markdown: add .md to the URL, for example /docs/developer/web/connect.md. /llms.txt lists all pages and /llms-full.txt contains the full text. The package READMEs on npm carry the same essentials, and the published type declarations document every option and method.

On this page