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.
tuios-web has no login
Anyone who can reach the port gets a shell as the user running tuios-web. There is no password or token. Keep it on localhost, or put a reverse proxy with authentication in front of it. See Security.
Install
brew tap Gaurav-Gosain/tap
brew install tuios-webyay -S tuios-web-bincurl -fsSL https://raw.githubusercontent.com/Gaurav-Gosain/tuios/main/install-web.sh | bashgo install github.com/Gaurav-Gosain/tuios/cmd/tuios-web@latestQuick start
Start the server
tuios-webIt 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
Binding anything other than a loopback address needs TLS, because otherwise every keystroke crosses the network in clear text. tuios-web refuses to start without one of these:
# HTTPS from a self-signed certificate tuios-web generates and keeps
tuios-web --host 0.0.0.0 --auto-tls
# HTTPS from a certificate you already have
tuios-web --host 0.0.0.0 --cert cert.pem --key key.pem
# Plain HTTP, only on a network you trust
tuios-web --host 0.0.0.0 --insecureWith --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@serverThen 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
| Flag | Default | Description |
|---|---|---|
--host | localhost | Address to bind |
--port | 7681 | HTTP port. WebTransport uses the next port up. |
--auto-tls | off | Serve HTTPS from a self-signed certificate that tuios-web generates and keeps |
--cert, --key | Serve HTTPS from your own certificate and key, in PEM form | |
--cert-days | 365 | Days an --auto-tls certificate is valid. Under 14 also keeps WebTransport working in Chrome. |
--cert-dir | sip's directory in your user config dir | Where --auto-tls keeps its certificate |
--cert-host | Extra DNS name or IP for the --auto-tls certificate. Repeatable. | |
--insecure | off | Allow plain HTTP on a non-loopback address |
--read-only | off | Ignore all input from browsers |
--max-connections | 0 | Maximum concurrent connections. 0 means no limit. |
--default-session | web | Session every browser attaches to |
--ephemeral | off | Give each connection its own standalone session, with no daemon. Nothing persists. |
--touch | auto | Touch mode: auto, on or off |
--debug | off | Log 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-keystuios-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.
| Setting | Options |
|---|---|
| Transport | Auto (prefers WebTransport), WebTransport, WebSocket |
| Renderer | Auto (prefers WebGL), WebGL, Canvas, DOM, vtgl (experimental) |
| Font size | 10 to 28 px, default 14 |
| Toggles | Copy 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.
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 images work too.
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
There is no authentication
tuios-web has no login of any kind. Whoever reaches the port gets a full shell with your privileges.
Browser connections from other web sites are refused: a WebSocket or WebTransport connection must come from a page served by tuios-web itself. That stops a page you happen to visit from opening a shell on localhost:7681, but it is not authentication. Programs that are not browsers send no origin and are accepted.
If anyone but you can reach the port:
- Put authentication in a reverse proxy.
tuios-webhas none of its own. - Bind to localhost with
--host 127.0.0.1so that only the proxy can reach it. - Use TLS end to end, or at least between the browser and the proxy.
- Limit connections with
--max-connections. - Use
--read-onlyfor demos, so viewers cannot type.
WebTransport and certificates
WebTransport always runs over TLS.
- On localhost without TLS,
tuios-webgenerates 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
--certor--auto-tls, WebTransport uses the same certificate as HTTPS. For a self-signed--auto-tlscertificate, Chrome only accepts WebTransport if the certificate is valid for under 14 days, so set--cert-days 13if you want it. - With
--insecurethere 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 and authentication. Most proxies carry only the WebSocket connection, so browsers use WebSocket rather than WebTransport.
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.
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.comConfigure 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:404Run both
tuios-web --host 127.0.0.1
cloudflared tunnel run tuiosTraefik
# 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.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 the only access control here. Without it, this publishes a shell to the internet.
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: terminate TLS and authenticate at a proxy in front of it.
ENTRYPOINT ["tuios-web", "--host", "0.0.0.0", "--insecure"]systemd
# /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.targetsudo systemctl enable --now tuios-webHow it works
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:
| Type | Byte | Direction | Content |
|---|---|---|---|
| Input | '0' | browser to server | Keyboard and mouse input |
| Output | '1' | server to browser | Terminal output |
| Resize | '2' | browser to server | New size in cells and pixels |
| Ping | '3' | browser to server | Keep-alive |
| Pong | '4' | server to browser | Keep-alive reply |
| Title | '5' | server to browser | Window title |
| Options | '6' | server to browser | Session settings |
| Close | '7' | server to browser | The session ended |
| Kitty keyboard | '8' | browser to server | Keyboard protocol flags |
Troubleshooting
It refuses to start on 0.0.0.0
That is the TLS requirement. Add --auto-tls, --cert and --key, or --insecure. The error message lists the exact commands.
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 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.