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.
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 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.