Skip to content

Terminal

oldmangan edited this page Oct 6, 2026 · 2 revisions

An xterm.js terminal connected to a WebSocket shell relay. IguanaXterm is built on it.

from wapyt import Terminal, TerminalConfig, TerminalTheme

term = Terminal(TerminalConfig(
    ws_url=f"/ws/terminal/{session_id}",   # resolved against the page origin (ws/wss follows http/https)
    asset_base="/xterm",
    fit_debounce_ms=120,
    theme=TerminalTheme(background="#0b1220"),
), container=tabs.get_cell(f"term_{session_id}"))
term.on_disconnect(lambda p: show_status(p["code"]))

Serve xterm yourself

xterm is not bundled with wapyt: it is about 300 KB that only terminal apps need, and pyTincture's CSP (script-src 'self') rules out a CDN. Serve xterm.js, xterm.css, addon-fit.js and, for search, addon-search.js from your own origin under asset_base (default /xterm).

The relay protocol

JSON text frames over the WebSocket:

Direction Frame
browser → relay {"type": "input", "data": "<keystrokes>"}
browser → relay {"type": "resize", "cols": 120, "rows": 32}
relay → browser {"type": "output", "data": "<bytes for the screen>"}
relay → browser {"type": "connected", "data": "<host>"} → on_connect
relay → browser {"type": "disconnected", "data": "<reason>"}
relay → browser {"type": "error", "data": "<message>"} → on_error

A frame that is not JSON is written to the screen as is.

Terminal bytes never cross into Python. Keystrokes and output go straight between xterm and the socket in JavaScript. Pyodide runs on the main thread, and a Python hop per keypress and per output frame is what makes browser terminals feel laggy. The Python wrapper only manages the lifecycle.

Config

TerminalConfig
ws_url Relay endpoint; omit to connect later with connect()
font_family, font_size, scrollback, cursor_blink, theme Appearance (TerminalTheme(background, foreground, cursor, cursor_accent, selection_background))
search Search addon, Ctrl+F
clipboard Windows Terminal-style copy/paste and a right-click ContextMenu (Copy, disabled without a selection; Paste; Select all); costs sending a literal Ctrl+V
reconnect, reconnect_max_attempts, reconnect_base_ms Exponential backoff
fit_debounce_ms Set ~120 when the pane can be drag-resized, so a drag does not send a PTY resize per frame

Events

on_ready, on_connect ({host}), on_disconnect ({code, clean}), on_error, on_reconnecting ({attempt, delay}), on_reconnect_failed, on_title ({title}, OSC 0/2), on_copy ({chars}), on_clipboard_error ({action, message}).

Methods

connect, disconnect, reconnect, fit, focus, copy_selection, paste_clipboard, write (local notices), clear, show_search, hide_search, toggle_search, destroy.

Call fit() when a hidden tab becomes visible: a hidden pane has no size, and the widget skips fitting while hidden.

Clone this wiki locally