Skip to Content
PlatformsOverview

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:

TraitResponsibilityNative implBrowser impl
BackendClockMonotonic timestd::time::Instant wrapperDeterministicClock (host-driven)
BackendEventSourceInput events + terminal sizecrossterm polling loopWebEventSource queue fed by JSON
BackendPresenterCommit a frame to the hostANSI emission via one writerPatch 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. Direct Instant usage in portable code will not compile to wasm32.
  • 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 returns Ok(None) and drops it.

See also