Skip to Content
ftui-runtimeCommands

Commands (Cmd<M>)

Commands are the runtime’s effect language. When update returns a Cmd<Msg>, the runtime executes it — scheduling tasks, writing logs, reconfiguring the tick strategy — and feeds any produced messages back into update. Commands are the only legitimate way to cause a side effect from a model.

File: crates/ftui-runtime/src/program.rs:310 (variants) and 379 (constructors).

Shape

crates/ftui-runtime/src/program.rs
#[derive(Default)] pub enum Cmd<M> { #[default] None, Quit, Batch(Vec<Cmd<M>>), Sequence(Vec<Cmd<M>>), Msg(M), Tick(Duration), Log(String), Task(TaskSpec, Box<dyn FnOnce() -> M + Send>), SaveState, RestoreState, SetMouseCapture(bool), SetTickStrategy(Box<dyn TickStrategy>), }

The type parameter M is always your Model::Message. Each constructor has a short alias (Cmd::none(), Cmd::quit(), …) and a few have longer forms (Cmd::task_with_spec, Cmd::task_weighted) for fine-grained scheduler hints.

Variant table

VariantSemanticsTypical use
NoneNo-op; elided by the effect executor.Returned when update has no side effect.
QuitRun on_shutdown, drain pending commands, exit the loop.Closing the app.
Batch(cmds)Run each command once, in order. An empty batch collapses to None.Reporting + follow-up action in one branch.
Sequence(cmds)Explicit sequential execution (same order guarantee as Batch, exposed for clarity).Pipelines that read as narrative.
Msg(m)Re-enter update with m as the next message.Synthesizing events (e.g. simulated key).
Tick(dur)Schedule a single delayed tick that ultimately arrives as an Event::Tick.Debounced polling, animation frames.
Log(s)Write s through the one-writer TerminalWriter.Progress notes, diagnostic prints.
Task(spec, f)Execute f on a background worker; its return value becomes a message.Network, disk, crypto, CPU work.
SaveStateAsk the StateRegistry to flush dirty entries.After a meaningful state change.
RestoreStateReload state from the storage backend.After a destructive operation you want to undo.
SetMouseCapture(b)Toggle terminal mouse capture at runtime.Context-aware mouse handling.
SetTickStrategy(boxed)Replace the active TickStrategy.Switching from Uniform to Predictive after warmup.

Naming convention: the enum variant is Cmd::Batch(Vec<Cmd<M>>), the constructor helper (the form you’ll actually write) is Cmd::batch(vec![...]) — lowercase. This mirrors how Cmd::none(), Cmd::quit(), Cmd::perform() relate to their None, Quit, Perform(...) variants.

Worked examples

Cmd::none

update.rs
fn update(&mut self, msg: Msg) -> Cmd<Msg> { match msg { Msg::Ignored => Cmd::none(), // ... } }

Cmd::quit

update.rs
Msg::Key(k) if k.is_char('q') => Cmd::quit(),

Cmd::quit triggers on_shutdown, so that’s where you flush state.

Cmd::batch

update.rs
Msg::Saved => Cmd::batch(vec![ Cmd::log("saved"), Cmd::save_state(), ]),

Empty batches collapse to Cmd::None (program.rs:408). A single- element batch is unwrapped.

Cmd::sequence

Exactly the same runtime semantics as Cmd::batch today; the name signals intent for readers of a multi-step pipeline.

update.rs
Msg::Submit => Cmd::sequence(vec![ Cmd::log("validating..."), Cmd::task(|| Msg::Validated(validate_payload())), ]),

Cmd::msg

update.rs
Msg::SyntheticEnter => Cmd::msg(Msg::Key(KeyEvent::enter())),

Useful in keyboard macros and tests.

Cmd::tick

Delayed ticks come back as Event::Tick, which your From<Event> impl then turns into a model message.

update.rs
Msg::StartDebounce => Cmd::tick(Duration::from_millis(150)),

Cmd::log

update.rs
Msg::Click(hit) => Cmd::log(format!("hit id={}", hit.id())),

Goes through TerminalWriter; safe in inline mode (emitted above the UI region).

Cmd::task

update.rs
Msg::Fetch(url) => Cmd::task(move || { let body = std::fs::read_to_string("config.toml") .unwrap_or_default(); Msg::Fetched(body) }),

The closure is FnOnce() -> M + Send + 'static. When the task finishes, its return value is routed back into update. Scheduling depends on the configured runtime lane — Legacy spawns a thread per task, Structured enqueues on the effect queue.

For weighted scheduling you can reach for the longer form:

Cmd::task_weighted(/* weight */ 1.0, /* est_ms */ 50.0, move || { Msg::Heavy(do_work()) })

Cmd::save_state / Cmd::restore_state

update.rs
Msg::PreferencesChanged => Cmd::save_state(), Msg::RevertPreferences => Cmd::restore_state(),

No-ops if no StateRegistry is installed.

Cmd::set_mouse_capture

update.rs
Msg::EnterEditor => Cmd::set_mouse_capture(true), Msg::LeaveEditor => Cmd::set_mouse_capture(false),

Overrides the static ProgramConfig::with_mouse_capture_policy for the remainder of the session (or until the next SetMouseCapture).

Cmd::set_tick_strategy

update.rs
use ftui_runtime::tick_strategy::{Predictive, PredictiveConfig}; Msg::HotPathDetected => { Cmd::set_tick_strategy(Predictive::new(PredictiveConfig::default())) }

Swaps the live tick strategy. The new strategy is installed after the current frame completes so the transition cannot corrupt the current render.

Effect executor, briefly

execute_cmd in program.rs walks the command tree:

  • Batch / Sequence flatten with short-circuit on Quit.
  • Task routes through the selected TaskExecutorBackend (Spawned, EffectQueue, or — with the asupersync-executor feature — Asupersync).
  • Msg is pushed to the front of the update queue to preserve causal order.
  • Log acquires the TerminalWriter mutex and appends.
  • SaveState / RestoreState delegate to the StateRegistry.

All commands are instrumented: each atomic command emits an effect.command span on the ftui.effect target (crates/ftui-runtime/src/telemetry_schema.rs).

Pitfalls

A long-running closure inside Cmd::task still competes for the effect-queue worker. Budget the work you put on the queue and tune EffectQueueConfig; if you must run arbitrary user code, prefer the Spawned backend for that session.

Cmd::msg plus a self-triggering branch is a tight loop. If your update always re-emits a message matching its own arm, you will starve subscriptions and renders. Add Cmd::tick with a small delay to yield.

Cross-references