Skip to content

Web browser

The browser client is the same C++ core as every other client, compiled to WebAssembly. The page itself is thin: it owns the canvas, the frame loop and the window size. Colour, layout, attributes and glyph coverage arrive from Core already resolved, so the browser draws what macOS and Linux draw.

DOOM Fire drawn as coloured terminal cells in Ubiquitty Web, inside a Safari window connected to localhost.
DOOM Fire running on Ubiquitty Web

The page draws with WebGL2, using the same instance stream the Metal and Vulkan renderers bind, with the shaders ported to GLSL ES. If the browser refuses WebGL2, the page falls back to a 2D canvas and Core’s CPU raster. Images are composited on the CPU path. The build stages JetBrains Mono and Symbols Nerd Font Mono beside the page, so text uses the same faces as the desktop apps; without them the page falls back to the GNU Unifont bitmap face built into Core.

The CPU raster is the reference for the GPU path: Scripts/check-web-gpu-parity.sh draws the same frames both ways in a browser and compares them pixel by pixel; a pixel counts as different only if no pixel within one position of it matches, which tolerates rasterisers placing an edge a pixel apart but not a wrong glyph.

See Performance for the benchmark tools and measurement requirements.

The page is a full Sync client: the protocol’s client side is compiled into the WebAssembly module, so the browser speaks it directly. It needs a byte pipe to the host, and a page cannot open a Unix socket, so the host is reached over a WebSocket:

  1. ubiquitty-host runs and listens on its local socket.
  2. Scripts/web-proxy.py accepts a WebSocket on 127.0.0.1 and forwards bytes to and from the host’s socket without reading them.
  3. The page connects with a capability token the host issued.

The proxy is a local development bridge: the host does not accept WebSockets itself, and the proxy listens only on the loopback interface. The host checks the token and attachment consent; the proxy does not grant access. Do not expose this unauthenticated, unencrypted bridge to a network.

A page opened with ?attach=ws://127.0.0.1:<port>&token=<token> attaches on load; &workspace=<name> attaches to the session with that name or creates it. A token in a URL can appear in browser history and server logs. The existing-session example below keeps it out of the URL and selects a numeric session ID explicitly.

Named sessions persist when the tab closes. Whether unnamed sessions persist is a per-host setting on the page, Keep unnamed sessions, on by default. A page can also embed the client and call window.ubiquitty.connect({ url, token, sessionID, workspaceName }).

Without an attach URL the page is a local terminal fed from JavaScript, with nothing behind it.

  • Sessions on a host: attach, reattach after a reload, named sessions, the host’s session list.
  • Text, colour, sized and proportional text (OSC 66), DEC double size, inline images, the command palette.
  • A config.toml served beside the page configures it; the page passes the bytes to Core.
  • No QUIC: a browser has no UDP socket API for it.
  • No rich text blocks.
  • No consent prompt; the page’s permission policy allows local effects and refuses agent and remote ones.
  • The instructions below build and serve the client locally.

You need Emscripten (emcmake on your PATH) and the host’s build prerequisites.

Terminal window
./Scripts/run-web-demo.sh

The script builds the WebAssembly client and an optimised host, starts the host in a private home directory, issues a token, starts the WebSocket proxy, serves the page and opens it. Ctrl-C stops all of it. This is a separate demonstration host, not an attachment to your existing sessions.

Use a browser client that matches the running host’s source revision, not just the host binary currently on disk. These steps run on the machine that owns the session, under the account running its host.

Terminal window
./Scripts/build-web.sh --serve

Leave it running and open http://127.0.0.1:8777/ in a fresh private browsing window. Use a new private browsing session, not another tab sharing existing private storage: a saved host pairing can override the first explicit session selection. This command serves the page only; it does not start a host or attach to a session.

The --serve HTTP server listens on all interfaces. To serve the static page only on loopback, use these commands instead:

Terminal window
./Scripts/build-web.sh
python3 -m http.server 8777 --bind 127.0.0.1 \
--directory "$(python3 Scripts/build_root.py .)/web-dist"

In a shell inside the host session you want to attach to, run:

Terminal window
printf 'Socket: %s\nSession: %s\n' "$UBIQUITTY_SOCKET" "$UBIQUITTY_SESSION"

Keep the exact socket path and the number in session:1 (or whichever ID it prints). IDs belong to that running host; identify them again after a host restart. An empty value means this shell is not in a host-managed session. Do not select another socket just because its file is newer.

In another terminal, from the source checkout, replace the example path with the socket from step 2:

Terminal window
python3 Scripts/web-proxy.py \
--socket /absolute/path/to/host-example.sock \
--port 8778

Leave this running too. The HTTP page uses port 8777; the WebSocket bridge uses port 8778.

Use the executable belonging to that host. On macOS, the app bundles it at Ubiquitty.app/Contents/Helpers/ubiquitty-host; a standalone build places it under the build root’s host/ directory.

Terminal window
/absolute/path/to/ubiquitty-host token issue \
--scope observe,drive,control \
--expires 3600 \
--label web-browser

If the host uses a custom UBIQUITTY_HOME, set that same environment variable for this command. Copy the printed token privately. It permits viewing, input and control requests, including the session list; it does not grant session creation or file access.

Open the page’s developer console and run this, replacing 1 with the numeric session ID from step 2:

await window.ubiquitty.ready;
{
const token = window.prompt('Paste the host-issued token');
if (token) {
window.ubiquitty.connect({
url: 'ws://127.0.0.1:8778/ws',
token,
sessionID: 1,
});
}
}

Approve the attachment in an already attached native client when prompted. The browser cannot answer that consent prompt itself. For a headless host, an operator can explicitly add --allow-always when issuing the token to record standing attachment consent.

Check window.ubiquitty.connectionState for attached and window.ubiquitty.sessionID for the selected ID. The page’s Sessions panel lists other sessions on the same host. sessionID: 0 requests a new session and needs the new token scope; it is not an alias for the current session.

Use window.ubiquitty.detach() to disconnect without requesting session termination. Stop the two local servers with Ctrl-C when finished. The token expires after one hour; to revoke it sooner, find its ID with ubiquitty-host token list, then run ubiquitty-host token revoke <id>.

  • Connection refused: check that the bridge is running on port 8778 and its selected host socket is live.
  • Authentication or consent refused: check the token’s expiry, scopes and host control directory, then approve the attachment in a native client.
  • Version mismatch: rebuild the browser and host from the same revision. Replacing a host binary does not update an already running daemon; do not terminate sessions just to retry the connection.