tui-dispatch-core 0.1.1

Core traits and types for tui-dispatch
Documentation

Core traits and types for tui-dispatch

This crate provides the foundational abstractions for building TUI applications with centralized state management, following a Redux/Elm-inspired architecture.

Core Concepts

  • Action: Events that describe state changes
  • Store: Centralized state container with reducer pattern
  • Component: Pure UI elements that render based on props
  • EventBus: Pub/sub system for event routing
  • Keybindings: Context-aware key mapping

Basic Example

use tui_dispatch_core::prelude::*;

#[derive(Action, Clone, Debug)]
enum MyAction {
    Increment,
    Decrement,
}

#[derive(Default)]
struct AppState {
    counter: i32,
}

fn reducer(state: &mut AppState, action: MyAction) -> bool {
    match action {
        MyAction::Increment => { state.counter += 1; true }
        MyAction::Decrement => { state.counter -= 1; true }
    }
}

let mut store = Store::new(AppState::default(), reducer);
store.dispatch(MyAction::Increment);

Async Handler Pattern

For applications with async operations (API calls, file I/O, etc.), use a two-phase action pattern:

  1. Intent actions trigger async work (e.g., FetchData)
  2. Result actions carry the outcome back (e.g., DidFetchData, DidFetchError)
use tokio::sync::mpsc;

#[derive(Action, Clone, Debug)]
#[action(infer_categories)]
enum Action {
    // Intent: triggers async fetch
    DataFetch { id: String },
    // Result: async operation completed
    DataDidLoad { id: String, payload: Vec<u8> },
    DataDidError { id: String, error: String },
}

// Async handler spawns a task and sends result back via channel
fn handle_async(action: &Action, tx: mpsc::UnboundedSender<Action>) {
    match action {
        Action::DataFetch { id } => {
            let id = id.clone();
            let tx = tx.clone();
            tokio::spawn(async move {
                match fetch_from_api(&id).await {
                    Ok(payload) => tx.send(Action::DataDidLoad { id, payload }),
                    Err(e) => tx.send(Action::DataDidError { id, error: e.to_string() }),
                }
            });
        }
        _ => {}
    }
}

// Main loop receives actions from both events and async completions
loop {
    tokio::select! {
        Some(action) = action_rx.recv() => {
            handle_async(&action, action_tx.clone());
            store.dispatch(action);
        }
        // ... event handling
    }
}

The Did* naming convention clearly identifies result actions. With #[action(infer_categories)], these are automatically grouped (e.g., DataFetch and DataDidLoad both get category "data").