Platforms overview
FrankenTUI ships a single rendering kernel that runs unchanged against a native
terminal, a browser canvas, or an in-memory test harness. The seam that makes
this possible is a small trait contract in the ftui-backend crate; every
host (ftui-tty, ftui-web, and the PTY test fixtures in ftui-pty) is just
one more implementation of that contract.
Reading order: this page establishes the pattern. The host-specific pages (TTY, Web, WASM showcase, PTY, Windows) document each implementation in detail.
The portability claim
The runtime’s Elm-style loop, the render
pipeline, the widget library, text shaping, and the
intelligence layer all compile to wasm32-unknown-unknown with zero
conditional compilation. That is a load-bearing design constraint: any feature
that needs std::time::Instant, std::thread, or file I/O goes behind a
backend trait instead of into core.
The upshot is that the browser demo, the native binary, and every deterministic E2E test drive the exact same widget code. When a widget behaves one way in the terminal and a different way in the browser, that is a backend bug, not a widget bug, and it is fixable without touching UI code.
The ftui-backend seam
The ftui-backend crate defines three independent capabilities that every
host must supply:
| Trait | Responsibility | Native impl | Browser impl |
|---|---|---|---|
BackendClock | Monotonic time | std::time::Instant wrapper | DeterministicClock (host-driven) |
BackendEventSource | Input events + terminal size | crossterm polling loop | WebEventSource queue fed by JSON |
BackendPresenter | Commit a frame to the host | ANSI emission via one writer | Patch runs emitted as u32 arrays |
BackendFeatures advertises optional capabilities (synchronized output,
bracketed paste, OSC 8 hyperlinks, Kitty keyboard protocol). The runtime asks
the backend what it supports and downgrades gracefully instead of assuming a
least-common-denominator baseline.
The three hosts in this workspace
Mental model
┌─────────────────────────────────────────────────────────┐
│ widgets · runtime · render · text · style · a11y │ portable core
│ (compiles to wasm32, zero platform deps) │
└───────────┬──────────────┬───────────────┬──────────────┘
│ │ │
┌─────▼────┐ ┌─────▼────┐ ┌──────▼──────┐
│ ftui-tty │ │ ftui-web │ │ ftui-pty │ one trait impl each
│ Backend │ │ Backend │ │ (tests) │
└─────┬────┘ └─────┬────┘ └──────┬──────┘
│ │ │
┌────▼───┐ ┌─────▼─────┐ ┌─────▼─────┐
│ TTY │ │ Browser │ │ subprocess│
│ (Unix) │ │ (frankenterm-web)│ VT state│
└────────┘ └───────────┘ └───────────┘The diagram is deliberately boring: that is the point. All of the interesting engineering sits above the backend line, not below it.
Example: a single frame, two hosts
Runtime asks for events
while let Some(event) = backend.poll_event(Duration::from_millis(16))? {
model.update(event, &mut cmds);
}On ftui-tty, poll_event calls crossterm::event::poll. On ftui-web, it
pops from a VecDeque<Event> that JS populated via push_event. Same
signature, same event type, completely different plumbing.
Runtime renders a frame into a Buffer
Widgets write into the same Buffer regardless of
host. No host-specific branches in widget code.
Runtime hands the buffer to the presenter
ftui-tty diffs against the previous buffer and emits ANSI bytes. ftui-web
diffs against the previous buffer and emits a WebPatchRun array for JS to
render. The diff math is shared; only the emission side differs.
Clock advances
ftui-tty reads Instant::now(). ftui-web advances a DeterministicClock
that JS sets explicitly each animation frame. This is why web replays are
byte-identical across runs.
Why ftui-pty sits here
ftui-pty is a test-only crate, but it participates in the same platform
story. It spawns a real shell, runs a binary that uses ftui-tty, and scrapes
the output through a virtual terminal state machine to assert what the user
would actually see. It also carries the WebSocket bridge that integrates with
the external frankenterm-core streaming codec. Details on
pty-utilities.
Adjacent, not in-tree
frankenterm-core and frankenterm-web are not vendored in this
workspace. The README is explicit on this. They are developed separately
and pulled in as external crate dependencies. See the integration
note.
Pitfalls
- Do not sprinkle
#[cfg(target_arch = "wasm32")]into widgets. Push the platform concern down into the backend crate instead. Reviewers will reject widget PRs that leak host details. - Do not assume
Instant::now()is cheap in the browser. The backend gives you a monotonic clock; use it. DirectInstantusage in portable code will not compile towasm32. - Do not invent a custom event type per host. Everything normalizes to
ftui_core::event::Event. If a host sends something that does not map, the backend returnsOk(None)and drops it.
See also
- Runtime overview — the loop that sits above every backend
- Demo showcase overview — the same screens, both natively and in-browser
- Contributing — dev loop — building and testing across backends
- Web backend · WASM showcase · Windows