Getting Started (Library Consumers)
September 16, 2026 ยท View on GitHub
FrankenTUI is early-stage and APIs evolve quickly, but the core loop is stable:
Event -> Model::update -> Model::view -> BufferDiff -> ANSI.
If you just want to see what it can do, run the showcase:
cargo run -p ftui-demo-showcase
If you want to embed FrankenTUI in your own Rust app, this page is the shortest path to a working inline (scrollback-preserving) UI.
Stability Notes
- Expect breaking API changes: this is pre-1.0 and moving fast.
- These examples target 0.7.0. Use the version dependency below, or a local checkout when developing against repository source.
- The facade's default features compile a terminal backend (native
ftui-ttyon Unix, Crossterm elsewhere) soApp::run()works without configuration.
Crate Map (Core vs Optional)
Core stack (what most applications use):
ftui(facade): recommended entry point; re-exports core APIs.ftui-core: terminal lifecycle, capabilities, and input events.ftui-render: buffer/frame, diff computation, and ANSI presenter.ftui-runtime: Elm-style program loop, subscriptions, and terminal writer.ftui-widgets: core widget library.ftui-layout,ftui-style,ftui-text,ftui-i18n: supporting crates.
Optional / higher-churn:
ftui-extras: feature-gated add-ons (markdown, syntax highlighting, mermaid, text effects).ftui-harness: snapshot + PTY helpers and runnable examples (used heavily in this guide).ftui-pty: PTY utilities for tests.ftui-demo-showcase: reference app + visual snapshots.ftui-simd: internal perf experiments.
Prereqs
- Rust nightly (required by
rust-toolchain.toml) - A terminal with basic ANSI support (tmux/zellij are supported)
Embedding In frankentui_website (Next.js + bun)
This section is for the web stack and is explicitly xterm.js-free.
Current repo reality:
- This checkout contains the in-tree web foundations:
ftui-webandftui-showcase-wasm. - This checkout does not currently vendor a local
crates/frankenterm-webpackage. - The build helper retrieves
FrankenTermWebfrom this repository's pinned historical source, before that crate was removed from the workspace.
1. Build artifacts from this repo
Run these commands inside a native DSR job using the channel in
rust-toolchain.toml. Keep GitHub Actions disabled.
# One-time target install
rustup target add wasm32-unknown-unknown
# Verify ftui-web compiles for wasm32 (backend crate used by the web stack)
cargo check -p ftui-web --target wasm32-unknown-unknown
# Verify the in-tree WASM showcase target also compiles
cargo check -p ftui-showcase-wasm --target wasm32-unknown-unknown
# Optional: emit ftui-web release artifacts into target/wasm32-unknown-unknown/release/deps/
cargo build -p ftui-web --target wasm32-unknown-unknown --release
# Build both real browser packages and a self-contained site into a NEW directory
bash build-wasm.sh /absolute/new-browser-build
# Serve the completed site (not the Rust checkout)
python3 -m http.server --directory /absolute/new-browser-build/site 8080
The helper requires Cargo, Python 3.11+, curl, tar and wasm-bindgen matching
both checked-in locks (currently 0.2.127). Install the CLI through Cargo on the
DSR host when needed. It builds the current showcase and renderer source at
88b402b8be9c70a4405895d4172e449940cab2fe, verifies the source archive SHA-256,
and applies crates/ftui-showcase-wasm/renderer.lock to that isolated source
tree. Both builds use the current repository's exact toolchain pin and
--locked. Cargo profile configuration supplies the WASM size override;
the helper never rewrites source manifests or removes previous outputs.
The output retains source, licenses, dependency locks, tool versions and
source-inputs.json. Its site/ directory contains the host, font, text assets,
and both JS/WASM packages together. The host verifies package bytes against
pkg/manifest.json before execution and checks the renderer API contract.
Deploy the complete site/ directory together. Integrity checks detect mixed or
corrupt packages; they do not authenticate a manifest or prevent a coherent
rollback of an entire site. The local host forwards input immediately into the
bounded runner instead of accumulating it in the historical renderer's
drop-oldest queue. It bounds each producer input object's combined text to
768 KiB of UTF-8 before encoding, cancels oversized compositions, and reports rejected input without
printing its contents. Accepted input remaining after quit is downloadable;
later rejections leave that recovery link intact. The standalone renderer's
queue policy, grapheme transport and full browser/device matrix remain open.
For a real browser smoke test, launch Chrome with a retained profile and CDP endpoint on the DSR host, then use Node 22+ directly (no automation package):
node scripts/browser_showcase_smoke.mjs SITE NEW_EVIDENCE_DIR CDP_URL
The test exercises actual rendering startup/resize, quit-tail recovery and a keyboard-activated browser download, plus missing/corrupt/wrong-revision/ABI package failures. Input checks cover a 4,098-event burst, partial composition admission, exact UTF-8 byte boundaries, capacity drain/retry frames, oversized IME cancellation and visible rejection with logging off. It asserts visible compositor pixels and retains screenshots and browser observations. Synthetic DOM inputs do not establish physical keyboard/IME/mobile behavior; inspect the recorded renderer backend before making GPU claims.
2. Initialize in a Next.js client component
The snippet below assumes your website repo already contains a FrankenTermWeb
bundle at @/wasm/frankenterm-web/FrankenTerm from the pinned build above.
"use client";
import { useEffect, useRef } from "react";
import init, { FrankenTermWeb } from "@/wasm/frankenterm-web/FrankenTerm";
export function TerminalCanvas() {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
let term: FrankenTermWeb | null = null;
let disposed = false;
(async () => {
await init(); // loads FrankenTerm_bg.wasm
if (disposed || !canvasRef.current) return;
term = new FrankenTermWeb();
await term.init(canvasRef.current, undefined);
// Contract guardrail: pin to the stable FrankenTermJS API line.
const contract = term.apiContract();
if (contract.apiLine !== "frankenterm-js" || !String(contract.apiVersion).startsWith("1.")) {
throw new Error(`Unsupported FrankenTerm API: ${contract.apiLine}@${contract.apiVersion}`);
}
term.resize(120, 40);
term.render();
})();
return () => {
disposed = true;
term?.destroy();
};
}, []);
return <canvas ref={canvasRef} className="h-full w-full" />;
}
3. Feed input and patches
- Forward normalized DOM input with
term.input(...). - For ftui-driven rendering, call
term.applyPatch(patch)followed byterm.render(). - For ANSI-stream mode, call
term.feed(data)followed byterm.render(). - Clipboard integration (browser-safe flow):
- Copy: call
term.copySelection()and write the returned text vianavigator.clipboard.writeText(...)inside a trusted user gesture. - Paste: read text from a DOM
pasteevent (event.clipboardData) and callterm.pasteText(text)(orterm.input({ kind: "paste", data: text })). - Chrome/Safari/Firefox all gate clipboard APIs behind user gesture/permission rules, so keep clipboard read/write in host JS.
- Copy: call
This host integration is separate from native terminal clipboard commands.
Cmd::set_clipboard(text) and Cmd::get_clipboard() use OSC 52 through the
native runtime's TerminalWriter; replies arrive as Event::Clipboard when
the terminal permits a read. Multiplexer sessions disable this output by
default. FTUI_OSC52_CLIPBOARD=1 opts in while retaining tmux/screen wrapping;
the multiplexer must also allow passthrough. FTUI_OSC52_CLIPBOARD=0 disables
output. Neither command bypasses browser clipboard permissions.
Minimal keyboard event forwarding example:
term.input({
kind: "key",
phase: "down",
key: event.key,
code: event.code,
repeat: event.repeat,
mods: {
shift: event.shiftKey,
ctrl: event.ctrlKey,
alt: event.altKey,
meta: event.metaKey,
},
});
4. Current ftui-web status (important)
ftui-web currently provides the WASM-friendly backend core in Rust, but does
not yet expose a public wasm-bindgen JS wrapper by itself. The browser-facing
FrankenTermWeb wrapper remains adjacent/out-of-tree from this checkout.
Do not embed xterm.js as a fallback for this integration path.
Add The Dependency
The examples in this guide target 0.7.0 through the ftui facade:
[dependencies]
ftui = "=0.7.0"
To use a local checkout instead, replace that dependency with a path. Adjust the path to match your directory layout:
[dependencies]
ftui = { path = "../frankentui/crates/ftui" }
The default features (runtime, extras, backend) are what the examples in
this guide assume. App::run() selects native ftui-tty on Unix and Crossterm
elsewhere when backend is enabled; ftui::DEFAULT_BACKEND names the selection.
Headless, WASM, or custom-backend consumers
should use default-features = false, features = ["runtime"], in which case
App::run() returns an Unsupported error naming the feature to enable.
If you only want a small slice, you can depend on individual crates directly:
[dependencies]
ftui-core = "=0.7.0"
ftui-runtime = "=0.7.0"
ftui-render = "=0.7.0"
ftui-widgets = "=0.7.0"
For local development, replace each version with a path to its directory under
../frankentui/crates/. Direct ftui-runtime dependencies have no default
terminal backend; enable native-backend on Unix or crossterm-compat for
Crossterm if you need App::run().
Minimal Inline App (Copy/Paste)
This is adapted from crates/ftui-harness/examples/minimal.rs but written
against the ftui facade so you can depend on a single crate. The block below
is byte-for-byte crates/ftui/examples/getting_started.rs, which the facade's
tests compile (cargo run -p ftui --example getting_started runs it from the
repository).
use std::time::Duration;
use ftui::core::event::{Event, KeyCode, KeyEventKind, Modifiers};
use ftui::core::geometry::Rect;
use ftui::render::frame::Frame;
use ftui::runtime::{Every, Subscription};
use ftui::widgets::StatefulWidget;
use ftui::widgets::log_viewer::{LogViewer, LogViewerState};
use ftui::{App, Cmd, Model, ScreenMode};
struct Harness {
log: LogViewer,
state: LogViewerState,
}
enum Msg {
Key(ftui::KeyEvent),
Tick,
}
impl From<Event> for Msg {
fn from(e: Event) -> Self {
match e {
Event::Key(k) => Msg::Key(k),
_ => Msg::Tick,
}
}
}
impl Model for Harness {
type Message = Msg;
fn init(&mut self) -> Cmd<Self::Message> {
Cmd::none()
}
fn update(&mut self, msg: Msg) -> Cmd<Self::Message> {
match msg {
Msg::Key(k) if k.kind == KeyEventKind::Press => {
if k.modifiers.contains(Modifiers::CTRL) && k.code == KeyCode::Char('c') {
return Cmd::quit();
}
self.log.push(format!("Key: {:?}", k.code));
}
Msg::Tick => self.log.push("Tick..."),
_ => {}
}
Cmd::none()
}
fn view(&self, frame: &mut Frame) {
let area = Rect::from_size(frame.buffer.width(), frame.buffer.height());
let mut state = self.state.clone();
self.log.render(area, frame, &mut state);
}
fn subscriptions(&self) -> Vec<Box<dyn Subscription<Self::Message>>> {
vec![Box::new(Every::new(Duration::from_secs(1), || Msg::Tick))]
}
}
fn main() -> ftui::Result<()> {
let mut log = LogViewer::new(1000);
log.push("Started. Press Ctrl+C to quit.");
App::new(Harness {
log,
state: LogViewerState::default(),
})
.screen_mode(ScreenMode::Inline { ui_height: 5 })
.run()?;
Ok(())
}
Run it:
cargo run
Common Patterns
Inline UI + Scrolling Logs
- Inline mode keeps normal terminal scrollback intact.
- To write to scrollback from your model, use
Cmd::log("..."). - To render a scrolling log panel inside the UI region, use
LogViewer.
Streaming Output
See crates/ftui-harness/examples/streaming.rs for a reference pattern:
cargo run -p ftui-harness --example streaming
Interactive Input
The runtime delivers Event::Key, Event::Mouse, and friends via your message
type (impl From<Event> for Msg). A typical input flow is:
- Track input state in your
Model(cursor/selection/history). - Handle key events in
update(). - Render an input widget in
view().
Troubleshooting
Terminal Looks Corrupted After A Crash
FrankenTUI uses RAII teardown (TerminalSession) to restore state, but if you
force-kill the process your terminal may need a reset:
reset
Nightly Is Required
If you see -Z is only accepted on the nightly compiler, install nightly:
rustup toolchain install nightly
One-Writer Rule
Only one component should own terminal output. If you need to emit logs,
prefer Cmd::log so the runtime can keep inline mode correct.
See one-writer-rule.md.
Examples Index
All of these are runnable and kept aligned with the repo's current APIs:
crates/ftui-harness/examples/minimal.rs(hello world)crates/ftui-harness/examples/streaming.rs(streaming output + inline UI)crates/ftui-harness/examples/counter.rs(state updates)crates/ftui-harness/examples/layout.rs(layout composition)crates/ftui-harness/examples/modal.rs(modal patterns)
Tutorial: