Web Terminal

Use TUIOS from a browser with tuios-web, a separate server binary.

tuios-web serves TUIOS to a web browser. The browser draws the terminal with WebGL and connects over WebTransport when it can, or WebSocket when it cannot.

It is a separate binary from tuios, so the main binary contains no web server. It is built on sip, a library for serving any Bubble Tea program in a browser.

Every browser gets a shell

A browser that opens tuios-web gets a shell as the user who runs it. Set a password to control who connects. See Who can connect.

Upgrading from v0.8.0

In v0.8.1, a bind to an address other than localhost needs a password. See Upgrade from v0.8.0.

Install

brew tap Gaurav-Gosain/tap
brew install tuios-web
yay -S tuios-web-bin
curl -fsSL https://raw.githubusercontent.com/Gaurav-Gosain/tuios/main/install-web.sh | bash
go install github.com/Gaurav-Gosain/tuios/cmd/tuios-web@latest

Quick start

Start the server

tuios-web

It listens on http://localhost:7681, and for WebTransport on UDP port 7682 of the same host.

Open it

Go to http://localhost:7681. The page picks the best transport on its own.

By default every browser attaches to one daemon session called web, created the first time it is needed. Close the tab and the session keeps running, like any daemon session. Open a second tab and both show the same session.

Reaching it from another device

A bind to an address other than loopback needs TLS and a password. TLS encrypts the keystrokes. The password controls who connects. tuios-web does not start without both, and it prints the commands to use.

A password from a file or from TUIOS_WEB_PASSWORD gets a warning at start when it is shorter than 12 characters. tuios-web does not slow down wrong guesses. Use --random-password, or a longer password.

# HTTPS from a self-signed certificate that tuios-web makes and keeps,
# and a new password at each start. tuios-web prints the password.
tuios-web --host 0.0.0.0 --auto-tls --random-password

# HTTPS from a certificate you already have, and a password in a file
tuios-web --host 0.0.0.0 --cert cert.pem --key key.pem \
  --password-file ~/.config/tuios/web-password

# Plain HTTP and no password, only on a network you trust.
# --no-auth needs the address of one interface.
tuios-web --host 192.168.1.20 --insecure --no-auth

tuios-web refuses --no-auth on 0.0.0.0 and ::. These addresses also listen on this machine. tuios-web checks the Host header only on a loopback address, so a web page in your browser could open a shell there (DNS rebinding). Give --no-auth the address of one interface, or set a password.

An empty --host listens on localhost. tuios-web still asks for a password or --no-auth, and for TLS or --insecure.

tuios-web finds the address of a --host name once and binds that address. The rules on this page apply to the address, not to the name. A name that has both loopback and other addresses is refused.

With --auto-tls, each browser shows a certificate warning on its first visit, because the certificate is self-signed. tuios-web cert info explains what the warning looks like and how to make it go away. The other cert subcommands are new (replace the certificate), path (print its path) and rm (delete it and the key).

An SSH tunnel is another option, and needs no certificate:

ssh -L 7681:localhost:7681 you@server

Then open http://localhost:7681 on your own machine.

On phones and tablets, tuios-web adds a key bar with the prefix key and buttons for common commands (new, close, next, zoom, the command palette and more). It detects touch devices from the browser's request. Use --touch on or --touch off when it guesses wrong.

Flags

FlagDefaultDescription
--hostlocalhostAddress to bind
--port7681HTTP port. WebTransport uses the next port up.
--auto-tlsoffServe HTTPS from a self-signed certificate that tuios-web generates and keeps
--cert, --keyServe HTTPS from your own certificate and key, in PEM form
--cert-days365Days an --auto-tls certificate is valid. Under 14 also keeps WebTransport working in Chrome.
--cert-dirsip's directory in your user config dirWhere --auto-tls keeps its certificate
--cert-hostExtra DNS name or IP for the --auto-tls certificate. Repeatable.
--insecureoffAllow plain HTTP on a non-loopback address
--random-passwordoffMake a new password at start and print it
--password-fileRead the password from the first line of this file. The file must be yours, with mode 600 or 400. On Windows, only you, SYSTEM and Administrators may read it.
--usertuiosUser name that the browser must give with the password
--no-authoffServe a non-loopback address with no password. Not with 0.0.0.0 or ::
--allow-hostOn a loopback --host, also accept this host name in the Host header. Give the name with no port. Repeatable.
--read-onlyoffIgnore all input from browsers
--max-connections0Maximum concurrent connections. 0 means no limit.
--default-sessionwebSession every browser attaches to
--ephemeraloffGive each connection its own standalone session, with no daemon. Nothing persists.
--touchautoTouch mode: auto, on or off
--ctrl-v-to-program Added in v0.9.3offSend Ctrl+V to the program in the pane. See Paste
--debugoffLog connections, sessions and errors

All the appearance flags of tuios also work here and apply to every browser session: --theme, --border-style, --dockbar-position, --show-keys, --shared-borders, --ascii-only, --no-animations and the rest. tuios-web --help lists them. They are layered over your config file the same way tuios layers them.

tuios-web --theme nord --show-keys

tuios-web starts the daemon if it is not running. If the daemon cannot start, it logs a warning and falls back to ephemeral sessions, which do not persist.

Windows opened from the browser get TERM=xterm-256color and COLORTERM=truecolor.

Browser settings

The gear button in the corner opens the settings panel. Settings are saved in the browser's local storage.

SettingOptions
TransportAuto (prefers WebTransport), WebTransport, WebSocket
RendererAuto (prefers WebGL), WebGL, Canvas, DOM, vtgl (experimental), Worker engine (experimental). See The worker engine
Font size10 to 28 px, default 14
TogglesCopy on select, blinking cursor, the browser's right-click menu, and capturing Ctrl+W and similar keys (fullscreen only)

The page ships with JetBrains Mono Nerd Font, so icons render without anything installed on the client.

The worker engine

The worker engine is an experimental renderer from sip v0.10.0. It runs the terminal and the connection in Web Workers, off the page's main thread. A flood of output then does not freeze the page. The default renderer does not change.

To turn it on, do one of these:

  • Open the gear, and set Renderer to Worker engine (experimental).
  • Add ?renderer=engine to the page address, for example http://localhost:7681/?renderer=engine.

If the browser cannot run the engine, the page uses the xterm.js client and keeps working. The browser console says why.

With 50 MiB of cat output in a TUIOS pane:

xterm.js clientWorker engine
Time on the page's main thread2,767 ms18 ms
Longest gap between framesabout 200 ms16.8 ms

16.8 ms is one frame at 60 Hz.

Scroll the strip with a finger

With the worker engine renderer, a finger dragged sideways scrolls the strip of the scrolling layout. A tap, a vertical swipe and a long press keep their meaning. appearance.niri_scroll_snap = true lines the strip up on the nearest column when the finger lifts. A drag that starts on the rail, or a drag in another layout, does nothing.

Paste

The browser pastes the text. The page needs no clipboard permission.

  • Press Ctrl+V, Ctrl+Shift+V or Shift+Insert. On macOS, press Cmd+V.
  • Hold Shift, right-click, and select Paste. A right-click without Shift goes to the program in the pane.
  • On Linux, click the middle mouse button to paste the primary selection.

These keys do not go to the program in the pane. A paste key of the program, such as Ctrl+V in fish, reads the clipboard of the server, not your clipboard.

Send Ctrl+V to the program Added in v0.9.3

Start tuios-web with --ctrl-v-to-program to send Ctrl+V to the program. Ctrl+Shift+V and Shift+Insert still paste.

Use this option for vim, where Ctrl+V starts a block selection. Without the option, Ctrl+Q also starts a block selection in vim.

On macOS, Ctrl+V always goes to the program, and Cmd+V pastes.

Cursor Added in v0.9.3

The browser draws the cursor shape and blink that the program in the pane sets, in every renderer. When the page loses focus, the cursor is a hollow block. A cursor colour that the focused pane sets with OSC 12 shows in the browser. OSC 112 resets it. See Cursor colour.

Graphics in the browser

Inline images work, with a smaller part of the Kitty graphics protocol than a native terminal supports.

Works: direct base64 transmission (t=d), placing and deleting images, RGB, RGBA and PNG data, zlib compression, chunked transmission, source-region clipping, and images moving with scrolling.

Sixel: the browser client always takes sixel. It does not go through the xterm.js check that turns sixel off for other xterm.js terminals. A sixel picture shows, but it does not behave like the rest of the pane yet: it leaves a grey trail when the pane scrolls, it does not scroll away with the text, and it stays after clear. Kitty graphics are correct. See issue #587.

Does not work: file transmission (t=f), temporary files (t=t), shared memory (t=s), animation frame edits (a=f), and Unicode placeholders (U=1).

TUIOS tells programs what the browser can do. A program that asks whether it can send a file path is told no, and falls back to sending the bytes. A program that insists on shared memory gets no image: mpv --vo=kitty is the common case. See Graphics for how TUIOS handles graphics in a native terminal.

Security

Who can connect

Every browser that opens tuios-web gets a shell on this machine. The session switcher reaches every session. Set a password to control who connects. The browser asks for a user name and the password. The user name is tuios. Use --user to change it.

tuios-web reads the password from one of these:

  • --random-password: tuios-web makes a new password at each start. It prints the password and a URL that holds it.
  • --password-file <path>: tuios-web reads the first line of the file. The file must belong to you, with mode 600 or 400. On Windows, only you, SYSTEM and Administrators may read or change it.
  • TUIOS_WEB_PASSWORD: tuios-web reads this environment variable. It does not pass the variable to panes. Your own processes can still read it from /proc, so use --password-file if you can.

No flag takes the password itself, because other users can see command line arguments in ps. Give only one password source. A password and --no-auth do not go together.

# Make a password file that only you can read
mkdir -p ~/.config/tuios
(umask 077; head -c 18 /dev/urandom | base64 > ~/.config/tuios/web-password)
tuios-web --host 0.0.0.0 --auto-tls --password-file ~/.config/tuios/web-password

What tuios-web needs depends on the address:

--hostPasswordWhat happens
localhost or another loopback addressOptionalWith no password, other users on this machine can connect. tuios-web prints one line at start to say so.
0.0.0.0 or ::Requiredtuios-web does not start without a password. --no-auth is refused.
Any other address, or an empty --hostRequiredtuios-web does not start without a password or --no-auth.

With a password, sessions use WebSocket. A WebTransport connection carries no password, so tuios-web refuses it and the browser falls back to WebSocket.

The Host check

On a loopback address, tuios-web accepts a session only when the Host header names this machine, such as localhost or 127.0.0.1. This stops a web page that points its own name at 127.0.0.1 (DNS rebinding). A refused session gets the message "tuios-web does not answer to this host name".

Browser connections from other web sites are also refused. A WebSocket or WebTransport connection must come from a page that tuios-web served.

To accept the host name of a reverse proxy, use --allow-host. See Behind a reverse proxy.

If other people can reach the port

  1. Set a password with --random-password or --password-file.
  2. Use TLS with --auto-tls or --cert, or put a proxy with TLS in front.
  3. Bind to localhost with --host 127.0.0.1 when a proxy or an SSH tunnel carries the traffic.
  4. Limit connections with --max-connections.
  5. Use --read-only for demos, so viewers cannot type.

WebTransport and certificates

WebTransport always runs over TLS.

  • On localhost without TLS, tuios-web generates a short-lived self-signed certificate for WebTransport alone. It is valid for 10 days, Chrome's limit for certificates pinned by hash, and the page fetches its hash from /cert-hash, so no warning appears.
  • With --cert or --auto-tls, WebTransport uses the same certificate as HTTPS. For a self-signed --auto-tls certificate, Chrome only accepts WebTransport if the certificate is valid for under 14 days, so set --cert-days 13 if you want it.
  • With --insecure there is no certificate, so there is no WebTransport. Browsers use WebSocket.

Behind a reverse proxy

Run tuios-web on 127.0.0.1 and let the proxy handle TLS. Most proxies carry only the WebSocket connection, so browsers use WebSocket.

The proxy must pass the original Host header through. tuios-web compares it with the page's origin and refuses the WebSocket connection if they differ.

The proxy sends its own host name, such as tuios.example.com. Add that name with --allow-host. Give the name with no port. --allow-host works only with a loopback --host.

--allow-host needs a password or --no-auth, because the proxy lets the network in. Pick one of these:

# tuios-web asks for the password. The proxy adds no login of its own.
tuios-web --host 127.0.0.1 --allow-host tuios.example.com \
  --password-file ~/.config/tuios/web-password

# The proxy asks for the login, as in the examples below.
tuios-web --host 127.0.0.1 --allow-host tuios.example.com --no-auth

tuios-web uses HTTP basic auth. Do not put a second basic auth login in the proxy in front of a tuios-web password. The browser sends only one login.

Nginx with basic auth

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 / {
        auth_basic           "TUIOS";
        auth_basic_user_file /etc/nginx/.htpasswd;

        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;
}

Create the password file with htpasswd -c /etc/nginx/.htpasswd yourname.

Caddy with basic auth

Caddy gets a certificate for you:

tuios.example.com {
    basic_auth {
        yourname <hash from "caddy hash-password">
    }
    reverse_proxy localhost:7681
}

Cloudflare Tunnel

A tunnel reaches the server without opening a port. Put Cloudflare Access in front of the hostname, or anyone who finds it gets a shell.

Create the tunnel

cloudflared tunnel login
cloudflared tunnel create tuios
cloudflared tunnel route dns tuios tuios.example.com

Configure it

~/.cloudflared/config.yml:

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

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

Run both

tuios-web --host 127.0.0.1 --allow-host tuios.example.com \
  --password-file ~/.config/tuios/web-password
cloudflared tunnel run tuios

With Cloudflare Access in front, you can use --no-auth in place of the password.

Traefik

# docker-compose.yml
services:
  tuios:
    # Build this image yourself (see Docker below). No published image contains tuios-web.
    image: tuios-web:local
    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.services.tuios.loadbalancer.server.port=7681"

Traefik adds TLS. tuios-web in the container asks for the password (see Docker). Do not add a Traefik basicauth middleware as well. The browser sends only one login.

Running as a service

Docker

The published ghcr.io/gaurav-gosain/tuios image contains only tuios. Build your own image for tuios-web from a checkout of the repository:

FROM golang:1.26 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
# Plain HTTP inside the container: a proxy in front of it must add TLS.
# tuios-web makes a new password at each start and prints it to the log.
ENTRYPOINT ["tuios-web", "--host", "0.0.0.0", "--insecure", "--random-password"]

A published port needs --host 0.0.0.0, and tuios-web refuses --no-auth there. So the container always needs a password. Read the password with docker logs.

For a fixed password, use --password-file with a file that you mount into the container. The file must belong to the user that the container runs as, with mode 600 or 400.

systemd

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

[Service]
Type=simple
User=tuios
# The file must belong to the user tuios, with mode 600 or 400.
ExecStart=/usr/local/bin/tuios-web --host 127.0.0.1 --port 7681 --password-file /home/tuios/.config/tuios/web-password
Restart=always
RestartSec=5

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

How it works

tuios-webBrowserWebSocketQUICUnix socketxterm.js (WebGL)HTTP / WebSocket :7681WebTransport :7682TUIOS clienttuios daemon
tuios-webBrowserWebSocketQUICUnix socketxterm.js (WebGL)HTTP / WebSocket :7681WebTransport :7682TUIOS clienttuios daemon
                    ┌─ Browser ─────────────┐
                    │                       │
                    │ ┌──────────────────┐  │
                    │ │ xterm.js (WebGL) │  │
                    │ └──────────────────┘  │
                    │          ▲ ▲          │
                    └──────────┼─┼──────────┘
                 ┌─────────────┘ │
                 │               │
             WebSocket         QUIC
                 │               └──────────────┐
 ┌─ tuios-web ───┼──────────────────────────────┼────────────┐
 │               │                              │            │
 │               ▼                              ▼            │
 │  ┌─────────────────────────┐      ┌─────────────────────┐ │
 │  │ HTTP / WebSocket :7681  │      │ WebTransport :7682  │ │
 │  └─────────────────────────┘      └─────────────────────┘ │
 │               │                              │            │
 │               └──────────────┬───────────────┘            │
 │                              ▼                            │
 │                      ┌──────────────┐                     │
 │                      │ TUIOS client │                     │
 │                      └──────────────┘                     │
 │                              ▲                            │
 └──────────────────────────────┼────────────────────────────┘
                                │
                           Unix socket
                                │
                                ▼
                        ┌───────────────┐
                        │ tuios daemon  │
                        └───────────────┘

62 columns, 32 rows, drawn for 80 columns.

flowchart LR
    subgraph Browser
        xterm["xterm.js (WebGL)"]
    end

    subgraph Server["tuios-web"]
        http["HTTP / WebSocket :7681"]
        wt["WebTransport :7682"]
        tui["TUIOS client"]
    end

    daemon["tuios daemon"]

    xterm <-->|WebSocket| http
    xterm <-->|QUIC| wt
    http --> tui
    wt --> tui
    tui <-->|Unix socket| daemon

Each browser connection runs a TUIOS client inside tuios-web, which attaches to the daemon like any other client. The first byte of every message says what it is. It is an ASCII digit, not a raw byte value:

TypeByteDirectionContent
Input'0'browser to serverKeyboard and mouse input
Output'1'server to browserTerminal output
Resize'2'browser to serverNew size in cells and pixels
Ping'3'browser to serverKeep-alive
Pong'4'server to browserKeep-alive reply
Title'5'server to browserWindow title
Options'6'server to browserSession settings
Close'7'server to browserThe session ended
Kitty keyboard'8'browser to serverKeyboard protocol flags

Open the Inbox from a notification

A push notification links to its Inbox item when notify.web_url is the address of tuios-web. The link is web_url/inbox?item=ID. tuios-web answers it with a cookie that names the item and sends the browser to the page. When the page connects, TUIOS opens the Inbox with the cursor on that item. Answer the item there as at any other client.

  • The link needs the same password as the page.
  • The cookie holds only the item number. It expires after 60 seconds.
  • If the item closed before you open the link, the cursor goes to the first item.
  • Behind a reverse proxy, set web_url to the address with the proxy's path. The link sends the browser back to that path.
  • With --no-auth and TLS, a browser can connect over WebTransport. Some browsers do not send the cookie there. Then the Inbox does not open by itself.

Troubleshooting

It refuses to start on 0.0.0.0

A non-loopback address needs TLS and a password. Add --auto-tls, or --cert and --key, or --insecure. Add --random-password, --password-file or --no-auth. The error message lists the commands.

The session is refused with "does not answer to this host name"

The Host header does not name this machine. This happens behind a reverse proxy. Start tuios-web with --allow-host and the proxy's host name.

It refuses the password file

The file must belong to you, with mode 600 or 400. Run chmod 600 on the file.

WebTransport does not connect

The page falls back to WebSocket, so this only costs latency. Check that UDP port 7682 (or your --port plus one) is reachable, that you are not using --insecure, and that your browser supports WebTransport. The browser console shows certificate errors. You can force WebSocket in the settings panel.

The WebSocket connection is refused behind a proxy

The proxy is probably rewriting the Host header. Pass it through unchanged, for example proxy_set_header Host $host; in nginx.

Blank terminal

Check the browser console for errors, try another renderer in the settings panel, and run tuios-web --debug to see the server's log.

The session ended

When the session stops, the browser shows a last message before the connection closes. The message says why:

  • The session stopped. Another client or a command removed it. Connect again to use a different session.
  • TUIOS lost the connection to the daemon. The daemon stopped or failed. Connect again after the daemon starts.
  • The link to a remote host closed. TUIOS tried to connect again and stopped. Connect again when the link is back.

This needs a tuios-web newer than v0.8.0. Older builds, which use sip before v0.8.2, can close the tab before the message arrives.

The browser asks for a login

tuios-web has a password. Give the user name tuios, or the name from --user, and the password. With --random-password, tuios-web prints both at start.

The tab stays open after quitting

In a daemon session, Ctrl+B q opens the quit menu, and its default is to detach. Detaching or killing the session from that menu ends the browser connection. If the page does not close, reload it.

Upgrade from v0.8.0

v0.8.1 adds passwords to tuios-web. Some commands that worked in v0.8.0 are refused now.

  • A bind to an address other than localhost needs a password. --auto-tls or --cert alone is not enough. Add --random-password or --password-file. On a network you trust, add --no-auth.
  • An empty --host listens on localhost, but the same rule applies to it.
  • A reverse proxy on this machine sends its own host name. Add that name with --allow-host, and add a password or --no-auth.
  • On localhost with no password, tuios-web still starts. It prints one line to say that other users on this machine can connect.
  • The password file must belong to you, with mode 600 or 400.

tuios lives only at tuios.dev and github.com/Gaurav-Gosain/tuios. tuios has no X or Twitter account. An account that offers a tuios token or presale is not this project.

On this page