Event Bus
For apps with multiple focusable components, EventBus routes input based on focus state. You register handlers for components and the bus decides routing order (modal → hovered → focused → subscribers → global).
For simple single-component apps, you can handle events directly in your main loop without EventBus—see the minimal example.
Quick Start: SimpleEventBus
For apps that don’t need context-specific keybindings, use SimpleEventBus with DefaultBindingContext:
use tui_dispatch::{SimpleEventBus, DefaultBindingContext, EventRoutingState, Keybindings};
#[derive(tui_dispatch::ComponentId, Clone, Copy, PartialEq, Eq, Hash, Debug)]enum AppComponentId { Main,}
impl EventRoutingState<AppComponentId, DefaultBindingContext> for AppState { fn focused(&self) -> Option<AppComponentId> { Some(AppComponentId::Main) } fn modal(&self) -> Option<AppComponentId> { None } fn binding_context(&self, _id: AppComponentId) -> DefaultBindingContext { DefaultBindingContext } fn default_context(&self) -> DefaultBindingContext { DefaultBindingContext }}
let mut bus: SimpleEventBus<AppState, Action, AppComponentId> = SimpleEventBus::new();let keybindings: Keybindings<DefaultBindingContext> = Keybindings::new();This reduces boilerplate by ~10 lines compared to defining a custom context enum.
Full API: Custom Binding Contexts
For apps with multiple modes (e.g., normal/search/modal), define custom contexts:
1) Define routing types
#[derive(tui_dispatch::ComponentId, Clone, Copy, PartialEq, Eq, Hash, Debug)]enum AppComponentId { Main, Search,}
#[derive(tui_dispatch::BindingContext, Clone, Copy, PartialEq, Eq, Hash)]enum AppContext { Main, Search,}2) Implement EventRoutingState on your app state
impl EventRoutingState<AppComponentId, AppContext> for AppState { fn focused(&self) -> Option<AppComponentId> { if self.search_open { Some(AppComponentId::Search) } else { Some(AppComponentId::Main) } }
fn modal(&self) -> Option<AppComponentId> { if self.search_open { Some(AppComponentId::Search) } else { None } }
fn binding_context(&self, id: AppComponentId) -> AppContext { match id { AppComponentId::Main => AppContext::Main, AppComponentId::Search => AppContext::Search, } }
fn default_context(&self) -> AppContext { AppContext::Main }}3) Register handlers
let mut bus: EventBus<AppState, Action, AppComponentId, AppContext> = EventBus::new();let keybindings: Keybindings<AppContext> = Keybindings::new();
bus.register(AppComponentId::Main, |event, state| { let actions = handle_main_event(&event.kind, state); if actions.is_empty() { HandlerResponse::ignored() } else { HandlerResponse::actions(actions) }});
bus.register_global(|event, _state| match event.kind { EventKind::Resize(_, height) => { HandlerResponse::action(Action::UiTerminalResize(height)).with_render() } _ => HandlerResponse::ignored(),});HandlerResponse
HandlerResponse controls propagation:
HandlerResponse::ignored() // No action, event continues routingHandlerResponse::action(action) // One action, event consumedHandlerResponse::actions(vec) // Multiple actions, event consumedHandlerResponse::actions_passthrough(v) // Multiple actions, event continues routing .with_render() // Force render even if state unchanged .with_consumed(true) // Explicitly set consumed flag4) Use run_with_bus
runtime .run_with_bus( terminal, &mut bus, &keybindings, |frame, area, state, render_ctx, event_ctx| { event_ctx.set_component_area(AppComponentId::Main, area); render_app(frame, area, state, render_ctx); }, |action| matches!(action, Action::Quit), ) .await?;Configuring Global Keys
By default, Esc, Ctrl+C, and Ctrl+Q are treated as “global” events — they bypass modal blocking and reach global subscribers. This can be surprising for apps that use Esc to close modals.
Use GlobalKeyPolicy to customize this behavior:
use tui_dispatch::{EventBus, GlobalKeyPolicy};
// Default: Esc, Ctrl+C, Ctrl+Q are globallet bus = EventBus::new();
// Remove Esc from global keys (keep Ctrl+C, Ctrl+Q)let bus = EventBus::new() .with_global_key_policy(GlobalKeyPolicy::without_esc());
// Only Ctrl+C is globallet bus = EventBus::new() .with_global_key_policy(GlobalKeyPolicy::keys(vec![ (KeyCode::Char('c'), KeyModifiers::CONTROL), ]));
// No key events are global (only Resize remains global)let bus = EventBus::new() .with_global_key_policy(GlobalKeyPolicy::none());
// Custom predicatelet bus = EventBus::new() .with_global_key_policy(GlobalKeyPolicy::custom(|event| { matches!(event, EventKind::Key(k) if k.modifiers.contains(KeyModifiers::CONTROL)) }));Resize events are always global regardless of the policy.
Routing order
- Modal (if any)
- Hovered (mouse/scroll only)
- Focused
- Subscribers
- Global subscribers (global events only)
- Global handlers (always last)
Modal blocks non-broadcast events from reaching hovered/focused/subscribers.
Global subscribers only receive global events.
Global handlers run last and still run for global events even if a modal is active (they are skipped for non-global events when modal blocks).
Resize and Tick events broadcast to subscribers (Resize/Tick) and global handlers regardless of consumed.