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
#[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
| Variant | Semantics | Typical use |
|---|---|---|
None | No-op; elided by the effect executor. | Returned when update has no side effect. |
Quit | Run 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. |
SaveState | Ask the StateRegistry to flush dirty entries. | After a meaningful state change. |
RestoreState | Reload 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
fn update(&mut self, msg: Msg) -> Cmd<Msg> {
match msg {
Msg::Ignored => Cmd::none(),
// ...
}
}Cmd::quit
Msg::Key(k) if k.is_char('q') => Cmd::quit(),Cmd::quit triggers on_shutdown, so that’s where you flush state.
Cmd::batch
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.
Msg::Submit => Cmd::sequence(vec![
Cmd::log("validating..."),
Cmd::task(|| Msg::Validated(validate_payload())),
]),Cmd::msg
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.
Msg::StartDebounce => Cmd::tick(Duration::from_millis(150)),Cmd::log
Msg::Click(hit) => Cmd::log(format!("hit id={}", hit.id())),Goes through TerminalWriter; safe in inline mode (emitted above the
UI region).
Cmd::task
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
Msg::PreferencesChanged => Cmd::save_state(),
Msg::RevertPreferences => Cmd::restore_state(),No-ops if no StateRegistry is installed.
Cmd::set_mouse_capture
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
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/Sequenceflatten with short-circuit onQuit.Taskroutes through the selectedTaskExecutorBackend(Spawned,EffectQueue, or — with theasupersync-executorfeature —Asupersync).Msgis pushed to the front of the update queue to preserve causal order.Logacquires theTerminalWritermutex and appends.SaveState/RestoreStatedelegate to theStateRegistry.
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.