7 unstable releases (3 breaking)

new 0.4.0 Jan 10, 2026
0.3.3 Jan 4, 2026
0.2.2 Jan 1, 2026
0.2.0 Dec 31, 2025
0.1.1 Dec 28, 2025

#328 in Command-line interface

MIT license

345KB
7K SLoC

tui-dispatch

Centralized state management for Rust TUI apps. Like Redux/Elm, but for terminals.

The Pitch

Components are pure: state → UI, events → actions. State mutations happen in reducers, making apps predictable and testable.

use tui_dispatch::prelude::*;

#[derive(Action, Clone, Debug)]
#[action(infer_categories)]
enum Action {
    NextItem,
    PrevItem,
    DidLoadData(Vec<String>),  // async result
}

fn reducer(state: &mut AppState, action: &Action) -> bool {
    match action {
        Action::NextItem => { state.selected += 1; true }
        Action::PrevItem => { state.selected -= 1; true }
        Action::DidLoadData(items) => { state.items = items.clone(); true }
    }
}

Derive Macros

Action

#[derive(Action, Clone, Debug)]
#[action(infer_categories)]
enum Action {
    SearchStart,           // category: "search"
    SearchClear,
    DidConnect(String),    // category: "async_result"
    Quit,                  // uncategorized
}

action.name()           // "SearchStart"
action.category()       // Some("search")
action.is_search()      // true

DebugState

#[derive(DebugState)]
struct AppState {
    #[debug(section = "Connection")]
    host: String,
    port: u16,

    #[debug(section = "Data")]
    items: Vec<String>,

    #[debug(skip)]
    internal_cache: HashMap<String, Value>,
}

FeatureFlags

#[derive(FeatureFlags, Default)]
struct Features {
    #[flag(default = true)]
    line_numbers: bool,
    wrap_lines: bool,
}

features.is_enabled("line_numbers")  // true
features.toggle("wrap_lines");

ComponentId & BindingContext

#[derive(ComponentId, Clone, Copy, PartialEq, Eq, Hash)]
enum ComponentId { KeyList, ValueViewer, Modal }

#[derive(BindingContext, Clone, Copy, PartialEq, Eq, Hash)]
enum Context { Default, Search, Modal }

Debug Layer

F12 to freeze UI and inspect state. One-line setup:

let mut debug = DebugLayer::<Action>::new(KeyCode::F(12));

// In event loop - handles F12 toggle, overlays, etc.
if debug.intercepts(&event) {
    continue;
}

// In render loop
debug.render(frame, |f, area| render_app(f, area, &state));

Debug mode keys: S state overlay, A action log, Y copy frame, I cell inspect.

Action Logging

let middleware = ActionLoggerMiddleware::with_default_log();
let mut store = StoreWithMiddleware::new(state, reducer, middleware);

// In debug mode, show action history
if let Some(log) = store.middleware().log() {
    debug.show_action_log(log);
}

Testing

#[test]
fn test_navigation() {
    let mut harness = TestHarness::new(AppState::default(), reducer);

    harness.send_keys("jjk");  // down, down, up
    harness.complete_actions();

    assert_eq!(harness.state().selected, 1);
    assert_emitted!(harness, Action::NextItem);
}

Architecture

Terminal → EventBus → Component::handle_event()Vec<Action>
                                                      │
              ┌───────────────────────────────────────┤
              ▼                                       ▼
        Sync Handler                           Async Handler
        (reducer)                              (spawn task)
              │                                       │
              ▼                                       │ Did* action
           State ◀────────────────────────────────────┘
              │
              ▼
        Component::render()

Crate Structure

tui-dispatch/           # Re-exports + prelude
tui-dispatch-core/      # Store, EventBus, Component, Debug, Testing
tui-dispatch-macros/    # #[derive(Action, DebugState, FeatureFlags, ...)]

Real-World Usage

Used in production by memtui, a TUI for Redis/Memcached/etcd.

License

MIT


lib.rs:

tui-dispatch: Centralized state management for Rust TUI apps

Like Redux/Elm, but for terminals. Components are pure functions of state, and all state mutations happen through dispatched actions.

Example

use tui_dispatch::prelude::*;

#[derive(Action, Clone, Debug)]
enum MyAction {
    NextItem,
    PrevItem,
}

#[derive(ComponentId, Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum MyComponentId {
    List,
    Detail,
}

Dependencies

~16–33MB
~386K SLoC