10 releases (4 breaking)
| 0.5.4 | Jan 25, 2026 |
|---|---|
| 0.5.3 | Jan 18, 2026 |
| 0.4.0 | Jan 10, 2026 |
| 0.3.3 | Jan 4, 2026 |
| 0.1.1 | Dec 28, 2025 |
#166 in #action
Used in tui-dispatch
37KB
750 lines
tui-dispatch
Centralized state management for Rust TUI apps (ratatui + crossterm). Redux/Elm patterns: actions describe events, reducers mutate state, UI renders from state.
Quick Start
[dependencies]
tui-dispatch = "0.5.3"
ratatui = "0.29"
crossterm = "0.28"
Minimal counter:
use std::io;
use crossterm::event::{self, Event, KeyCode};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::execute;
use ratatui::{backend::CrosstermBackend, widgets::Paragraph, Terminal};
use tui_dispatch::prelude::*;
#[derive(Default)]
struct State { count: i32 }
#[derive(Action, Clone, Debug)]
enum Action { Inc, Dec, Quit }
fn reducer(state: &mut State, action: Action) -> bool {
match action {
Action::Inc => { state.count += 1; true }
Action::Dec => { state.count -= 1; true }
Action::Quit => false,
}
}
fn main() -> io::Result<()> {
enable_raw_mode()?;
execute!(io::stdout(), EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
let mut store = Store::new(State::default(), reducer);
loop {
terminal.draw(|f| {
f.render_widget(
Paragraph::new(format!("count = {} (k/j, q)", store.state().count)),
f.area(),
);
})?;
if let Event::Key(key) = event::read()? {
let action = match key.code {
KeyCode::Char('k') | KeyCode::Up => Action::Inc,
KeyCode::Char('j') | KeyCode::Down => Action::Dec,
KeyCode::Char('q') | KeyCode::Esc => Action::Quit,
_ => continue,
};
if !store.dispatch(action) { break; }
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}
That's the core: State, Action, reducer, Store.
Add What You Need
tui-dispatch is layered. Start with the core, add extensions when needed:
| Extension | When to use |
|---|---|
| Effects | Async operations (HTTP, file I/O) |
| EventBus | Multiple focusable components |
| TaskManager | Task cancellation, debouncing |
| Subscriptions | Timers, streams |
| Debug overlay | State/action inspection (F12) |
Async and Side Effects
When you need async work (HTTP, file IO, timers), switch to the effect pattern:
- Reducer returns
DispatchResult<Effect>instead ofbool - Reducer emits
Effectvalues (data), and an effect handler executes them - Async completion sends a normal action back into the runtime (often named
Did*)
Enable helpers:
features = ["tasks"]for cancellation + debouncing viaTaskManagerfeatures = ["subscriptions"]for continuous sources (interval ticks, streams)
See docs/src/content/docs/patterns/async.md and the weather-example / github-lookup-example apps.
Examples (In This Repo)
cargo run -p counter
cargo run -p github-lookup-example
cargo run -p weather-example -- --city London --debug
cargo run -p markdown-preview -- README.md --debug
Documentation
- Docs (Starlight):
docs/(runmake docs-serve) - EventBus guide:
docs/src/content/docs/patterns/event-bus.md - API docs: https://docs.rs/tui-dispatch
Crates
tui-dispatch: re-exports + preludetui-dispatch-core: store/runtime/tasks/subscriptions/testing primitivestui-dispatch-macros: derives (Action,DebugState,FeatureFlags, ...)tui-dispatch-components: reusable components (SelectList, TextInput, TreeView, ...)tui-dispatch-debug: debug overlay + headless debug sessions
Real-World Usage
Used in production by memtui, a TUI for Redis/Memcached/etcd.
License
MIT
Dependencies
~0.5–1MB
~21K SLoC