osteak 0.1.2

Elm for ratatui — you bring the loop
Documentation

osteak

Crates.io docs.rs CI License

Elm for ratatui — you bring the loop.

Why osteak?

Every ratatui app that grows past demo complexity hits the "bag of booleans" wall — state scattered across fields, implicit transitions, race conditions when async work outlives UI signals.

osteak ratatui-elm teatui tui-realm
ratatui 0.30 yes no (0.29) no (0.29) no (0.29)
You keep event loop yes no no no
&mut update (no clone) yes yes no n/a
Full Frame access yes yes no yes
Async task integration yes no own runtime n/a
Dirty tracking yes no no no

Quick Start

cargo add osteak
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> {
        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());
    }
}

With the built-in runner

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
}

Without the runner

You always have the option to write your own event loop — osteak never takes control away. See examples/counter_manual.rs.

Architecture

                    ┌──────────────────────┐
                    │    Your Event Loop    │
                    │  (or osteak::runner)  │
                    └──────┬───────────────┘
                           │ Msg
                    ┌──────▼───────────────┐
                    │   Tea::update(&mut)   │
                    │   → Cmd { action,     │
                    │         dirty }       │
                    └──────┬───────────────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
        Action::Task  Action::None  Action::Quit
        (you spawn)   (no-op)       (exit loop)
              │
              │ Msg (on completion)
              └──────────► back to update

Features

Feature Default Description
crossterm-backend yes Crossterm terminal backend + EventStream
tokio-runtime yes Tokio integration for the runner module

To use osteak without the runner (just the traits):

cargo add osteak --no-default-features

Examples

  • counter — minimal app with the built-in runner
  • counter_manual — same app, hand-written event loop
  • async_tasksCmd::task with tokio::spawn in a manual loop
  • multi_pane — model composition with Cmd::map
cargo run --example counter
cargo run --example counter_manual
cargo run --example multi_pane

MSRV

The minimum supported Rust version is 1.86.0 (same as ratatui 0.30).

License

MIT

Contributing

See CONTRIBUTING.md.