Elm Architecture for ratatui. We built this because our TUI kept accumulating state bugs as it grew — booleans disagreeing, async tasks outliving the UI state that spawned them, the usual. TEA fixed it for us. Maybe it helps you too.
You bring the event loop. osteak gives you update, view, and
a Cmd type to describe side effects. There's an optional runner
if you don't have a loop yet.
cargo add osteakuse osteak::{Tea, Cmd};
use ratatui::Frame;
use ratatui::widgets::Paragraph;
struct Counter { count: i32 }
enum Msg { Increment, Decrement, Quit }
impl Tea for Counter {
type Msg = Msg;
fn update(&mut self, msg: Msg) -> Cmd<Msg> {
match msg {
Msg::Increment => { self.count += 1; Cmd::dirty() }
Msg::Decrement => { self.count -= 1; Cmd::dirty() }
Msg::Quit => Cmd::quit(),
}
}
fn view(&mut self, frame: &mut Frame) {
let text = format!("Count: {}", self.count);
frame.render_widget(Paragraph::new(text), frame.area());
}
}# use osteak::{Tea, Cmd};
# use ratatui::Frame;
# use ratatui::widgets::Paragraph;
# struct Counter { count: i32 }
# enum Msg { Increment, Decrement, Quit }
# impl Tea for Counter {
# type Msg = Msg;
# fn update(&mut self, msg: Msg) -> Cmd<Msg> { Cmd::none() }
# fn view(&mut self, frame: &mut Frame) {}
# }
use crossterm::event::{Event, KeyCode, KeyEventKind};
#[tokio::main]
async fn main() -> std::io::Result<()> {
osteak::runner::run(Counter { count: 0 }, |ev| {
let Event::Key(k) = ev else { return None };
if k.kind != KeyEventKind::Press { return None; }
match k.code {
KeyCode::Up => Some(Msg::Increment),
KeyCode::Down => Some(Msg::Decrement),
KeyCode::Char('q') => Some(Msg::Quit),
_ => None,
}
}).await
}Write your own loop — osteak doesn't take control.
See examples/counter_manual.rs.
┌──────────────────────┐
│ Your Event Loop │
│ (or osteak::runner) │
└──────┬───────────────┘
│ Msg
┌──────▼───────────────┐
│ Tea::update(&mut) │
│ → Cmd { action, │
│ dirty } │
└──────┬───────────────┘
│
┌────────┼────────┐
│ │ │
Task None Quit
(you spawn)
│
│ Msg (on completion)
└──────► back to update
| Feature | Default | Description |
|---|---|---|
crossterm-backend |
yes | Crossterm backend + EventStream |
tokio-runtime |
yes | Tokio integration for the runner |
Just the traits, no runtime:
cargo add osteak --no-default-featurescounter— with the runnercounter_manual— hand-written event loopasync_tasks—Cmd::task+tokio::spawnmulti_pane— composition withCmd::map
cargo run --example counter1.86.0 (same as ratatui 0.30).