The terminal on my homepage is real

July 7, 2026

There's a terminal on my homepage. It isn't a JavaScript imitation with a hardcoded list of fake commands — it's a genuine terminal session, running a real Python process on a real server, streamed to your browser byte-for-byte. This post is about how that works, because the plumbing turned out to be the most interesting part.

Your browserxterm.js canvasnginxTLS + ws upgradeFastAPIDocker, /ws/tuiTextual appinside a PTYthinks it's areal terminalwss://localhost:8000pty.fork()keystrokes travel left to right · raw ANSI bytes travel right to left
The whole pipeline: four hops between your keyboard and a Python process on a VPS.

The front: xterm.js

The thing you see is xterm.js — the same terminal emulator that powers VS Code's integrated terminal. It renders to canvas (WebGL when available), understands the full ANSI escape-code vocabulary, and fires a callback for every keystroke. Each keypress gets wrapped in a small JSON message and pushed down a secure websocket.

The middle: nginx and FastAPI

The websocket lands on a VPS where nginx terminates TLS and handles the HTTP → websocket upgrade handshake, then proxies to a FastAPI app in a Docker container that only listens on localhost. FastAPI's job is deliberately boring: accept the connection, forward input bytes one way, forward output bytes the other, enforce a rate limit, and clean up when you leave.

The clever bit: the pseudo-terminal

Here's the trick that makes it feel real: for every visitor, the server calls pty.fork(). That asks the kernel for a pseudo-terminal — the same mechanism ssh and tmux use — and starts a fresh copy of the TUI app inside it. The app checks "am I attached to a terminal?", the kernel says yes, and from that moment it behaves exactly as it would in your local shell: it emits cursor movements, 24-bit colours, and screen redraws as raw ANSI escape sequences. The server doesn't parse any of it — it just shovels the bytes into the websocket, and xterm.js renders them.

the life of one keystroke1. you pressh2. xterm.js sends{"type": "input", "data": "h"}3. FastAPI writesos.write(pty_fd, b"h")4. Textual redrawsInput widget now shows "h"5. PTY emits ANSI\x1b[9;4H\x1b[38;2;13;188;121mh…6. xterm.js paints itthe same escape codes a real terminal would render
Every keypress does this round trip. At ~10ms server polling it still feels instant.

The app itself: Textual

The program living inside the PTY is built with Textual, a Python framework that brings CSS-ish styling, widgets and an event loop to the terminal. The portfolio app is a couple of widgets — a scrolling output log and an input box — plus a command router. Type help and it lists what it knows; each command is just a Python method writing styled text back to the log.

A confession about Ctrl+C

For an embarrassingly long time, pressing Ctrl+C in the terminal killed it. Muscle memory from a real shell sent \x03 down the websocket, the TUI had that key bound to quit, the Python process exited, the pseudo-terminal closed, and the session died — a crash you could type. The fix was one line: rebind Ctrl+C to behave like a shell interrupt (echo ^C, clear the input line, carry on). The session now outlives your reflexes.

Each visitor gets their own isolated process that's destroyed on disconnect, the container runs as a non-root user, and input is rate-limited — so please do go and hammer on it. It's on the homepage. Type help.