TUIOSTUIOS

Web Terminal

Access TUIOS from any browser with WebGL rendering (separate tuios-web binary)

Access TUIOS from any modern web browser with hardware-accelerated rendering and low-latency connections.

Powered by Sip

The web terminal is powered by sip, a standalone library for serving Bubble Tea apps through the browser. You can use sip to serve your own Bubble Tea applications on the web!

Separate Binary

The web terminal functionality is provided as a separate tuios-web binary for security isolation. This prevents the web server from being used as a potential backdoor in the main TUIOS binary.

Installation

brew tap Gaurav-Gosain/tap
brew install tuios-web
yay -S tuios-web-bin
# or
paru -S tuios-web-bin
go install github.com/Gaurav-Gosain/tuios/cmd/tuios-web@latest

Quick Start

Start the web server

tuios-web

This starts:

  • HTTP server on http://localhost:7681 (static files, WebSocket)
  • WebTransport server on https://127.0.0.1:7682 (QUIC/UDP)

Open in browser

open http://localhost:7681

The browser will automatically connect using the best available transport (WebTransport if supported, WebSocket otherwise).

Features

  • WebGL Rendering - GPU-accelerated terminal
  • Dual Protocols - WebTransport (QUIC) with WebSocket fallback
  • Bundled Fonts - JetBrains Mono Nerd Font included
  • Settings Panel - Configure transport, renderer, font size
  • Mouse Support - Full interaction with cell-based optimization
  • Auto-Reconnect - Automatic reconnection with backoff
  • Read-Only Mode - View-only sessions for demos

Command Reference

tuios-web [flags]

Flags

FlagDefaultDescription
--port7681HTTP server port
--hostlocalhostServer bind address
--read-onlyfalseDisable client input
--max-connections0Max concurrent sessions (0=unlimited)
--default-sessionwebSession name shared by all connections
--ephemeralfalseDisable daemon mode, so sessions do not persist

tuios-web starts the daemon automatically. If the daemon cannot be started it falls back to ephemeral mode silently, so sessions that you expected to persist will not.

Forwarded TUIOS Flags

Nine TUIOS flags are forwarded to the spawned instance: --debug, --ascii-only, --theme, --border-style, --dockbar-position, --hide-window-buttons, --scrollback-lines, --show-keys, and --no-animations.

This is not the full TUIOS flag set. --shared-borders, --show-clock, --show-cpu, --show-ram, --hide-scrollbar, --window-title-position, --zoom-max-width, --confirm-quit, and --pprof are not forwarded. Passing them to tuios-web is not an error, so they fail silently. Set them in your config file instead.

tuios-web also sets TERM=xterm-kitty and TERM_PROGRAM=tuios-web for every session it spawns.

# With theme
tuios-web --theme dracula

# With show-keys overlay
tuios-web --show-keys

# Disable animations for instant transitions
tuios-web --no-animations

# Multiple flags
tuios-web --theme nord --ascii-only --debug

Client Settings

Click the ⚙ button in the top-right corner to access settings:

Transport

OptionDescription
AutoPrefer WebTransport, fallback to WebSocket
WebTransportForce QUIC (lower latency)
WebSocketForce WebSocket (wider compatibility)

Renderer

OptionDescription
AutoPrefer WebGL, fallback to Canvas/DOM
WebGLGPU-accelerated (best performance)
Canvas2D canvas (good compatibility)
DOMStandard DOM (most compatible)

Font Size

Adjustable from 10px to 24px. Settings persist in localStorage.

Architecture

Loading diagram...

Message Protocol

TypeCodeDirectionDescription
Input0C→SKeyboard/mouse input
Output1S→CTerminal output data
Resize2C→STerminal size change
Ping3C→SKeep-alive ping
Pong4S→CKeep-alive response
Title5S→CWindow title update
Options6S→CSession configuration
Close7S→CSession ended

The code is the ASCII character '0' through '7' (bytes 0x30 to 0x37), not the raw byte values 0 to 7. It is the first byte of each frame.

Graphics in the Browser

Inline images work in the web client, but a narrower set of the Kitty graphics protocol is supported than in a native terminal.

Works: direct base64 transmission (t=d), the placement and deletion actions, RGB and RGBA and PNG formats, zlib compression, chunked transmission, source-region clipping, and repositioning on scroll. Sixel graphics also work.

Does not work: file transmission (t=f), temporary-file transmission (t=t), shared memory (t=s, including /dev/shm), the animation protocol (a=f, a=a, a=c), and Unicode placeholders (U=1).

The practical consequence is that a program which insists on passing a file path or a shared memory handle gets no image. mpv --vo=kitty is the common case: it prefers shared memory, which the browser client cannot read.

This is handled honestly rather than silently. TUIOS probes its host terminal at startup to find out which transmission media actually work, and answers guest capability queries with the real answer. A program that asks whether it can send a file path is told no, and falls back to streaming bytes, instead of sending a path that produces a blank image.

Earlier versions answered every capability query with a yes, which is why kitten icat and similar tools could silently render nothing. See Graphics Support for the host-side behaviour.

There is one conservative edge: the host probe shares a 300 ms budget, so a host that supports file transmission but answers slowly is treated as if it does not. That costs a server-side read and re-encode per frame rather than breaking anything, and there is no environment variable to force it back on.

Reverse Proxy Setup

For production deployments, put TUIOS behind a reverse proxy with proper TLS.

Important

WebTransport requires HTTP/3 (QUIC) support. Most reverse proxies only support WebSocket, so WebTransport connections will fall back to WebSocket when proxied.

Cloudflare Tunnel

Cloudflare Tunnels provide secure access without opening ports.

Install cloudflared

# macOS
brew install cloudflare/cloudflare/cloudflared

# Linux
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
chmod +x cloudflared
sudo mv cloudflared /usr/local/bin/

Authenticate

cloudflared tunnel login

Create tunnel

cloudflared tunnel create tuios

Configure tunnel

Create ~/.cloudflared/config.yml:

tunnel: <your-tunnel-id>
credentials-file: /path/to/credentials.json

ingress:
  - hostname: tuios.yourdomain.com
    service: http://localhost:7681
  - service: http_status:404

Start TUIOS and tunnel

# Terminal 1: Start TUIOS web server
tuios-web --host 127.0.0.1

# Terminal 2: Start tunnel
cloudflared tunnel run tuios

Add DNS record

cloudflared tunnel route dns tuios tuios.yourdomain.com

Access at https://tuios.yourdomain.com

Nginx

server {
    listen 80;
    server_name tuios.example.com;

    location / {
        proxy_pass http://127.0.0.1:7681;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 86400;
    }
}
server {
    listen 443 ssl http2;
    server_name tuios.example.com;

    ssl_certificate /etc/letsencrypt/live/tuios.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/tuios.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:7681;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 86400;
        proxy_send_timeout 86400;
    }
}

server {
    listen 80;
    server_name tuios.example.com;
    return 301 https://$server_name$request_uri;
}

Caddy

Caddy automatically handles TLS certificates:

tuios.example.com {
    reverse_proxy localhost:7681
}

Traefik

# docker-compose.yml
services:
  tuios:
    # Build this yourself; no published image ships tuios-web.
    image: tuios-web:local
    command: ["--host", "0.0.0.0"]
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.tuios.rule=Host(`tuios.example.com`)"
      - "traefik.http.routers.tuios.entrypoints=websecure"
      - "traefik.http.routers.tuios.tls.certresolver=letsencrypt"
      - "traefik.http.routers.tuios.middlewares=tuios-auth"
      - "traefik.http.middlewares.tuios-auth.basicauth.usersfile=/etc/traefik/htpasswd"
      - "traefik.http.services.tuios.loadbalancer.server.port=7681"

The basicauth middleware is doing the access control here. Without it this configuration publishes an unauthenticated shell on a public hostname.

Security Considerations

There is no authentication

tuios-web has no access control of any kind

tuios-web serves a full interactive shell to anyone who can reach the port. It has no login, no token, no password flag, and no TLS option. Anyone who connects gets your shell with your privileges.

It also accepts WebSocket connections from any origin. The origin check is a function that returns true unconditionally, and the allowed-origin list is left empty, which the underlying library treats as a wildcard. This means any web page you visit while tuios-web is running can open a connection to localhost:7681 and run commands as you. Binding to 127.0.0.1 does not protect against this, because the request originates from your own browser.

Treat a running tuios-web as an open shell on that port. Do not leave it running unattended, and do not expose it without putting authentication in front of it.

Putting authentication in front of it

Because the server has no authentication of its own, a reverse proxy is the only place to add it. This is a requirement for any use beyond a local session you are actively watching, not a hardening tip.

If you expose this at all

  1. Terminate TLS and authenticate at a reverse proxy. The server speaks plaintext HTTP and WebSocket and exposes no TLS flags.
  2. Bind to localhost: tuios-web --host 127.0.0.1, and let the proxy be the only thing that reaches it.
  3. Limit connections: tuios-web --max-connections 10
  4. Use read-only for demos: tuios-web --read-only
  5. Do not rely on origin checks. There are none to configure. The AllowOrigins setting exists in the underlying library but tuios-web exposes no flag for it.

WebTransport certificates

The server generates a self-signed certificate for WebTransport:

  • Valid for 10 days, which is Chrome's limit for serverCertificateHashes
  • The hash is served from the /cert-hash endpoint
  • No browser warning is shown for the WebTransport connection

The WebTransport listener binds to 127.0.0.1 regardless of what you pass to --host. With --host 0.0.0.0, remote clients cannot use WebTransport at all and always fall back to WebSocket. WebTransport is effectively a local-only optimisation today.

Basic Auth with Nginx

location / {
    auth_basic "TUIOS";
    auth_basic_user_file /etc/nginx/.htpasswd;
    
    proxy_pass http://127.0.0.1:7681;
    # ... rest of proxy config
}

Create password file:

htpasswd -c /etc/nginx/.htpasswd username

Performance

Server Optimizations

  • Buffer Pools - Reusable buffers reduce GC pressure
  • Atomic Counters - Lock-free connection counting
  • Direct Streaming - No intermediate buffering
  • Structured Logging - charmbracelet/log with configurable levels

Client Optimizations

  • requestAnimationFrame Batching - Writes batched per frame
  • Mouse Deduplication - Only sends on cell position change
  • Pre-allocated Buffers - Reusable send/receive buffers
  • Cached DOM Elements - No repeated queries

Measured Performance

There is no benchmark in the repository for the web path, so no latency, memory, or event-filtering figures are claimed here. The optimisations above are real code, but their effect has not been measured and published.

If you need numbers for your own deployment, measure them on your own hardware and network. Local latency is dominated by the transport in use, and WebTransport is only available when the browser reaches the server on 127.0.0.1.

Troubleshooting

WebTransport Not Connecting

  1. Check browser support - Chrome 97+, Edge 97+
  2. Verify UDP port - Port 7682 must be accessible
  3. Check console - Look for certificate hash errors
  4. Force WebSocket - Use settings panel to switch

Blank Terminal

  1. Check console - Look for JavaScript errors
  2. Verify fonts - Check if fonts loaded
  3. Try different renderer - Switch in settings
  4. Check server logs - Verify TUIOS process started

High Latency

  1. Check network - Run speed test
  2. Prefer WebTransport - Lower latency than WebSocket
  3. Use WebGL - Hardware acceleration
  4. Check server CPU - May be overloaded

Session Not Closing

When TUIOS quits (pressing q), the web session should close automatically. If not:

  1. Check browser console for errors
  2. Verify server logs show session cleanup
  3. Refresh browser to start new session

Debug Mode

tuios-web --debug

Server logs include:

  • Connection attempts
  • Session lifecycle
  • Bytes sent/received
  • Terminal resize events
  • Error details

Examples

Public Demo Server

# Read-only with connection limit
tuios-web \
  --host 0.0.0.0 \
  --port 7681 \
  --read-only \
  --max-connections 50 \
  --theme dracula

Development with Hot Reload

# With debug logging
tuios-web --debug --show-keys

Docker Deployment

There is no published image that contains tuios-web. The ghcr.io/gaurav-gosain/tuios image builds only cmd/tuios and its entrypoint is the tuios binary, which has no web subcommand. To run the web terminal in a container you have to build an image yourself.

FROM golang:1.25 AS build
WORKDIR /src
COPY . .
RUN go build -o /out/tuios-web ./cmd/tuios-web

FROM debian:bookworm-slim
COPY --from=build /out/tuios-web /usr/local/bin/tuios-web
EXPOSE 7681
ENTRYPOINT ["tuios-web", "--host", "0.0.0.0"]

Remember that this exposes an unauthenticated shell. See Security Considerations.

Systemd Service

# /etc/systemd/system/tuios-web.service
[Unit]
Description=TUIOS Web Terminal
After=network.target

[Service]
Type=simple
User=tuios
ExecStart=/usr/local/bin/tuios-web --host 127.0.0.1 --port 7681
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable tuios-web
sudo systemctl start tuios-web

On this page