Skip to Content
ftui-runtimeOverview

ftui-runtime — Overview

ftui-runtime is the orchestrator. It consumes events produced by ftui-core, dispatches them into your Model::update, calls Model::view to build a Frame, and pushes the resulting Buffer through a BackendPresenter onto the terminal. Everything else in the workspace — widgets, layout, style, intelligence, testing — either produces data the runtime consumes or consumes data the runtime produces.

If a piece of code mentions a Cmd, a Subscription, a ProgramConfig, a tick strategy, or a rollout lane, it lives here.

Why a dedicated runtime crate?

The Elm architecture (adopted by Bubbletea in Go) has three parts — init / update / view — and a single loop that drives them. In Rust the loop needs more than just dispatch:

  1. Cancellation-safe subscriptions (timers, file watchers, child processes) whose lifecycles have to be reconciled each cycle.
  2. Deterministic effect execution (Cmd::Task, Cmd::Tick, Cmd::SaveState) with back-pressure and telemetry.
  3. Coordinated terminal output — the “one-writer rule” — so that inline logs never interleave with UI escape sequences.
  4. Migration safety — the runtime can run a shadow lane in parallel with the live lane to prove behaviour-preservation before a switch.
  5. Evidence — every non-trivial decision (resize, diff strategy, frame budget, rollout) emits a JSONL line a human can grep.

Doing all of that inside ftui-core would blur the input/output boundary. Doing it inside widgets would make every widget author re-invent it. ftui-runtime collects the responsibility in one crate, which is why it is also the crate you use ftui::prelude::* through.

Layer boundaries

The runtime is the only crate allowed to import from ftui-backend directly (via the optional native-backend feature). Widgets never see a backend; they see a Frame. The backend never sees your Model; it sees a Buffer and a BufferDiff.

What lives in this crate

    • program.rs Model + Cmd + Program::run loop
    • subscription.rs Subscription trait + Every built-in
    • process_subscription.rs ProcessSubscription
    • effect_system.rs queue telemetry + Cx-aware tasks
    • evidence_sink.rs JSONL sink, ftui-evidence-v1
    • state_persistence.rs StateRegistry + StorageBackend
    • lens.rs bidirectional binding primitives
    • terminal_writer.rs one-writer coordinator
    • telemetry.rs optional OTLP integration
    • telemetry_schema.rs canonical targets + events
    • tick_strategy/ Uniform / Predictive / Custom
    • resize_coalescer.rs BOCPD-backed resize policy
    • simulator.rs ProgramSimulator for tests

Note that ftui-runtime also hosts the intelligence primitives (BOCPD, conformal, VOI, e-processes, SOS barrier). That’s deliberate: every one of them is consumed by the runtime’s own decisions (resize coalescing, frame-budget risk gating, widget refresh selection). They are re-exported for application reuse but the runtime owns them.

Minimal example

This is the smallest end-to-end program. It uses Every to tick the model once a second and draws a counter. Later pages drill into every type you see here.

examples/hello_tick.rs
use ftui::prelude::*; use ftui_core::geometry::Rect; use ftui_runtime::subscription::Every; use ftui_widgets::paragraph::Paragraph; use std::time::Duration; #[derive(Clone, Debug)] enum Msg { Tick, Quit } impl From<Event> for Msg { fn from(e: Event) -> Self { match e { Event::Key(k) if k.is_char('q') => Msg::Quit, _ => Msg::Tick, } } } struct Counter { ticks: u64 } impl Model for Counter { type Message = Msg; fn update(&mut self, msg: Msg) -> Cmd<Msg> { match msg { Msg::Tick => { self.ticks += 1; Cmd::none() } Msg::Quit => Cmd::quit(), } } fn view(&self, frame: &mut Frame) { let text = format!("ticks: {} (q to quit)", self.ticks); let area = Rect::new(1, 1, frame.width().saturating_sub(1), 1); Paragraph::new(text.as_str()).render(area, frame); } fn subscriptions(&self) -> Vec<Box<dyn Subscription<Msg>>> { vec![Box::new(Every::new(Duration::from_secs(1), || Msg::Tick))] } } fn main() -> std::io::Result<()> { Program::with_config(Counter { ticks: 0 }, ProgramConfig::default())?.run() }

Every piece of this — Cmd, Subscription, ProgramConfig, the conversion from Event to your message type — is documented on its own page in this section.

Two entrypoints: App (builder) vs Program::with_config

FrankenTUI exposes two ways to spin up the runtime. They compile to the same underlying loop, but target different audiences:

  • App::new(model).screen_mode(...).run() — the builder façade. Used by the tutorials (see hello-tick). Short, fluent, great for simple programs and first-read code. App wraps a ProgramConfig internally and exposes only the knobs most apps need (screen mode, mouse capture, subscription overrides).

  • Program::with_config(model, ProgramConfig::default().with_*(...))?.run() — the advanced escape hatch. Used when you need fine-grained control: selecting a runtime lane, installing an evidence sink, wiring a StateRegistry for persistence, or tuning the effect queue. Everything the builder hides is a method on ProgramConfig.

Rule of thumb: start with App in tutorials and demos, drop to Program::with_config the first time you need a knob the builder does not expose. See the ProgramConfig reference for the full set.

The update cycle (one frame)

Phases are explained in full on the model-trait and subscriptions pages.

Pitfalls

Never write to stdout from inside update or view. A stray println! will race the presenter and corrupt the terminal state. Emit a Cmd::Log instead — it goes through the single TerminalWriter and respects inline-mode cursor discipline.

update must not block. Anything that could take longer than a frame (HTTP, disk I/O, cryptography) must be a Cmd::Task. Blocking inside update stalls input handling and subscriptions simultaneously.