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-tty on Unix, Crossterm elsewhere) so App::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-web and ftui-showcase-wasm.
  • This checkout does not currently vendor a local crates/frankenterm-web package.
  • The build helper retrieves FrankenTermWeb from 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 by term.render().
  • For ANSI-stream mode, call term.feed(data) followed by term.render().
  • Clipboard integration (browser-safe flow):
    • Copy: call term.copySelection() and write the returned text via navigator.clipboard.writeText(...) inside a trusted user gesture.
    • Paste: read text from a DOM paste event (event.clipboardData) and call term.pasteText(text) (or term.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.

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:

Tutorial: