Skip to content
UrushiDocumentation

Application runtime

The urushi-tui-app crate is a runnable TEA-style full-screen application framework. An application owns its domain choices; the runtime owns the live model, ordered message delivery, effects, subscriptions, frame scheduling, and the terminal session.

The application layer contains no Ratatui types. It returns the same Urushi View used by other presentation surfaces.

An application separates the program description from the model that changes while it runs.

use std::time::Duration;
use urushi::{TextStyle, View};
use urushi_tui_app::{Application, Effect, Subscription};
struct Counter;
enum Message {
Increment,
}
impl Application for Counter {
type Model = i32;
type Message = Message;
fn init(&self) -> (Self::Model, Effect<Self::Message>) {
(
0,
Effect::after(Duration::from_millis(500), |_| Message::Increment),
)
}
fn update(
&self,
model: &mut Self::Model,
message: Self::Message,
) -> Effect<Self::Message> {
match message {
Message::Increment => *model += 1,
}
Effect::shutdown()
}
fn view(&self, model: &Self::Model) -> View {
View::text(model.to_string(), TextStyle::new())
}
fn subscriptions(
&self,
_model: &Self::Model,
) -> Subscription<Self::Message> {
Subscription::none()
}
}

Because view returns the current model as text, the complete frame before and after an Increment message is:

Initial frame
0
After Increment
1

Only update receives the model mutably. Terminal input, timer ticks, effect results, and other external events all become messages before they reach it. This keeps application transitions directly testable without opening a terminal.

For a complete persistent application with keyboard input, application focus, surface changes, errors, and user-driven shutdown, follow the interactive application guide.

urushi_tui_app::run supplies the production executor, clock, terminal backend, full-screen presentation, and session defaults:

fn main() -> Result<(), urushi_tui_app::Error> {
let final_model = urushi_tui_app::run(Counter)?;
println!("final count: {final_model}");
Ok(())
}

The example renders 0, delivers the delayed Increment message, updates the model to 1, and shuts down. After restoring the terminal session, run returns that final model and the program prints:

Output after the full-screen session closes
final count: 1

The production session profile is explicit. Builder methods replace these choices before the session is entered:

Session choice Default Observable consequence
Raw mode On Keys reach the application immediately instead of line-buffered input
Alternate screen On The full-screen application does not overwrite the primary shell buffer
Bracketed paste On A paste can arrive as one Input::Paste value
Focus reporting On Input::Focus can be produced by a supporting terminal
Keyboard enhancement Disambiguate escape codes and report event types, when supported KeyKind::Press, Repeat, and Release can be distinguished
Mouse capture Off Terminal text selection remains available until .mouse(true) is selected
Cursor hidden On The cursor stays hidden unless a resolved View requests a position

An input subscription receives only events that the selected terminal and session modes can produce. In particular, handle key kind deliberately rather than assuming every terminal reports releases.

Return effects instead of performing work in update

Section titled “Return effects instead of performing work in update”

An Effect<Message> describes work for the runtime to interpret. It supports blocking work, futures, delays, latest-only keyed work, batches, and shutdown. The completed value returns as another message, so it cannot mutate the model out of order.

Use an effect for filesystem or network I/O and for expensive preparation that would make input handling or view slow. Keep update focused on state transitions and keep view cheap enough to evaluate for a frame.

This complete transition starts blocking work, then handles its result as an ordinary message:

use urushi_tui_app::Effect;
#[derive(Debug, PartialEq)]
enum Message {
Load,
Loaded(String),
}
fn update(status: &mut String, message: Message) -> Effect<Message> {
match message {
Message::Load => {
*status = "Loading".into();
Effect::perform(|| Message::Loaded("Ready".into()))
}
Message::Loaded(value) => {
*status = value;
Effect::none()
}
}
}
let mut status = "Idle".to_owned();
let _work = update(&mut status, Message::Load);
assert_eq!(status, "Loading");
// The runtime executes _work and later delivers its Loaded message.
let _ = update(&mut status, Message::Loaded("Ready".into()));
assert_eq!(status, "Ready");
Observable model states

Each delivered message moves the model to the state used by the next View.

  1. stateIdle
  2. Load
    stateLoading
  3. Loaded
    stateReadystatus = “Ready”

perform_latest and future_latest replace unfinished work under the same key. after_latest does the same for a delayed message, which makes it the debounce form. batch starts independent effects concurrently; completion order is deliberately unspecified. Return a later effect from the update that receives an earlier completion when ordering is required.

See Effects and subscriptions for every effect form, latest-only work, batch and shutdown behavior, and observable completion traces.

subscriptions(&Model) returns the sources that should be active for the current model. Public subscription constructors cover:

  • terminal input;
  • surface changes such as terminal dimensions;
  • intervals and process signals;
  • terminal draw errors; and
  • custom streams, asynchronous producers, and blocking producers.

The runtime reconciles this declaration after updates. A source starts when it appears and stops when it is no longer declared. Source admission policy is explicit; the runtime does not inspect application message variants to decide which events may be replaced or backpressured.

For example, this application listens for input at all times but runs the clock only while running is true:

use std::time::Duration;
use urushi_tui_app::{Input, Subscription};
enum Message {
Input(Input),
Tick,
}
fn subscriptions(running: bool) -> Subscription<Message> {
let input = Subscription::input(Message::Input);
let clock = if running {
Subscription::interval(
"refresh-clock",
Duration::from_secs(1),
|_| Message::Tick,
)
} else {
Subscription::none()
};
Subscription::batch([input, clock])
}
let _active = subscriptions(true);
let _paused = subscriptions(false);
Model state Reconciled sources Observable result
running = true input + refresh-clock keys and one Tick per second can reach update
running = false input only the keyed clock is stopped; no later ticks are accepted from it

Changing an interval’s key or period replaces its identity and restarts it. For custom producers, choose Admission::bounded(capacity) when every accepted value must wait in FIFO order, or Admission::latest() when a newer pending value may replace an older one.

See Effects and subscriptions for custom producers, source identity, reconciliation, Sender, SendError, and bounded-versus-latest backpressure.

The runtime architecture does not make Ratatui part of Application:

Full-screen rendering boundaryApplication view produces an Urushi View. The renderer resolves it and writes styled graphemes through a borrowed Frame. Screen owns Urushi cell buffers, diffing, and transactional terminal output.Applicationview(&Model)Viewrenderer-neutralRendererresolve + drawURUSHI-TUIScreen::drawborrowed FrameCommandWriterterminal outputScreen owns frame history and commits only after output succeeds.

The runtime resolves an application View and writes it through a borrowed urushi_tui::Frame. The concrete urushi_tui::Screen owns working and committed cell buffers, diffing, output commit, and failed-output recovery. Those presentation resources do not enter the Application model.

Ratatui also remains available as an explicit adapter for applications that own their own loop. See the Ratatui adapter.

Runtime::new(app) exposes the same blocking runtime as a builder. A run:

  1. create and own the live model;
  2. admit input, subscription events, and effect completions into one ordered delivery path;
  3. apply every accepted message through update;
  4. coalesce physical draws without skipping logical model transitions;
  5. resolve the latest View once for the selected terminal’s frame; and
  6. restore the terminal session on normal and error returns, and attempt restoration from the session guard during panic unwinding.

The builder makes the physical backend, executor, clock, and session options selectable. Presentation always uses the concrete urushi_tui::Screen; these runtime-owned dependencies are not fields added to the application model:

use urushi_tui_app::Runtime;
fn main() -> Result<(), urushi_tui_app::Error> {
let final_model = Runtime::new(Counter)
.mouse(true)
.bracketed_paste(false)
.run()?;
assert_eq!(final_model, 1);
Ok(())
}

Message delivery and drawing defines the ordering contract behind accepted messages, subscription reconciliation, effect starts, coalesced draws, surface changes, draw failures, and shutdown.

Enable the optional graphics feature when the runtime should present Kitty or Sixel images contained in the application’s View:

[dependencies]
urushi = "0.1.0"
urushi-graphics = "0.1.0"
urushi-tui-app = { version = "0.1.0", features = ["graphics"] }

The runtime builder selects automatic, required-protocol, or text-only behavior with Runtime::graphics(GraphicsPreference). It then owns retained image state, repaint, failure recovery, cleanup, and terminal restoration alongside cell frames. Image construction remains in urushi-graphics rather than the application crate.

On Unix, enabling graphics makes the default runtime open Urushi’s native bidirectional terminal connection so it can positively query Kitty and Sixel support before the input reader starts. On platforms without that native connection, the portable default backend reports no graphics protocol evidence: Auto keeps the text fallback, while an explicit Kitty or Sixel request requires a custom TerminalBackend that supplies confirmed capabilities.

Configure graphics rendering and lifecycle →

The public runtime surface includes:

  • Application, Effect, and Subscription;
  • input, surface, signal, admission, and sender values;
  • run(app) with the production defaults; and
  • Runtime, including backend, executor, clock, and session-option replacement.

The synchronous Screen and Frame live in urushi-tui. Caller-owned Ratatui integration lives in urushi-adapter-ratatui; it is not a runtime backend. See the TUI overview for the complete boundary. The TUI reference indexes every runtime, frame-engine, and Ratatui-adapter API.