Skip to main content

tui_dispatch_core/
runtime.rs

1//! Runtime helpers for tui-dispatch apps.
2//!
3//! These helpers wrap the common event/action/render loop while keeping
4//! the same behavior as the manual wiring shown in the examples.
5
6use std::io;
7use std::time::Duration;
8
9use ratatui::backend::Backend;
10use ratatui::layout::Rect;
11use ratatui::{Frame, Terminal};
12use tokio::sync::mpsc;
13use tokio_util::sync::CancellationToken;
14
15use crate::bus::{
16    process_raw_event, spawn_event_poller, EventBus, EventOutcome, EventRoutingState, RawEvent,
17};
18use crate::effect::{EffectStore, EffectStoreWithMiddleware, ReducerResult};
19use crate::event::{ComponentId, EventContext, EventKind};
20use crate::keybindings::Keybindings;
21use crate::store::{DispatchError, Middleware, Reducer, Store, StoreWithMiddleware};
22use crate::{Action, BindingContext};
23
24#[cfg(feature = "subscriptions")]
25use crate::subscriptions::Subscriptions;
26#[cfg(feature = "tasks")]
27use crate::tasks::TaskManager;
28
29/// Configuration for the event poller.
30#[derive(Debug, Clone, Copy)]
31pub struct PollerConfig {
32    /// Timeout passed to each `crossterm::event::poll` call.
33    pub poll_timeout: Duration,
34    /// Sleep between poll cycles.
35    pub loop_sleep: Duration,
36}
37
38impl Default for PollerConfig {
39    fn default() -> Self {
40        Self {
41            poll_timeout: Duration::from_millis(10),
42            loop_sleep: Duration::from_millis(16),
43        }
44    }
45}
46
47/// Context passed to render closures.
48#[derive(Debug, Clone, Copy, Default)]
49pub struct RenderContext {
50    /// Whether the debug overlay is currently active.
51    pub debug_enabled: bool,
52}
53
54impl RenderContext {
55    /// Whether the app should treat input focus as active.
56    pub fn is_focused(self) -> bool {
57        !self.debug_enabled
58    }
59}
60
61/// Policy applied by runtimes when `try_dispatch` returns a [`DispatchError`].
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum DispatchErrorPolicy {
64    /// Keep running without forcing a render.
65    Continue,
66    /// Keep running and force a render pass.
67    ///
68    /// The runtime does not persist or log the error automatically;
69    /// capture visibility in your error handler closure if needed.
70    Render,
71    /// Stop the runtime loop gracefully.
72    Stop,
73}
74
75fn apply_dispatch_error_policy(
76    handler: &mut dyn FnMut(&DispatchError) -> DispatchErrorPolicy,
77    error: DispatchError,
78    should_render: &mut bool,
79) -> bool {
80    match handler(&error) {
81        DispatchErrorPolicy::Continue => false,
82        DispatchErrorPolicy::Render => {
83            *should_render = true;
84            false
85        }
86        DispatchErrorPolicy::Stop => true,
87    }
88}
89
90#[cfg(feature = "debug")]
91pub trait DebugAdapter<S, A>: 'static {
92    fn render(
93        &mut self,
94        frame: &mut Frame,
95        state: &S,
96        render_ctx: RenderContext,
97        render_fn: &mut dyn FnMut(&mut Frame, Rect, &S, RenderContext),
98    );
99
100    fn handle_event(
101        &mut self,
102        event: &EventKind,
103        state: &S,
104        action_tx: &mpsc::UnboundedSender<A>,
105    ) -> Option<bool>;
106
107    fn log_action(&mut self, action: &A);
108    fn is_enabled(&self) -> bool;
109}
110
111#[cfg(feature = "debug")]
112pub trait DebugHooks<A>: Sized {
113    #[cfg(feature = "tasks")]
114    fn with_task_manager(self, _tasks: &TaskManager<A>) -> Self {
115        self
116    }
117
118    #[cfg(feature = "subscriptions")]
119    fn with_subscriptions(self, _subscriptions: &Subscriptions<A>) -> Self {
120        self
121    }
122}
123
124// ---------------------------------------------------------------------------
125// Shared draw macro — renders a frame with optional debug overlay.
126//
127// `$render_call` must be callable as `$render_call(frame, area, state, ctx)`.
128// For bus variants, wrap the user's render closure to include EventContext.
129// ---------------------------------------------------------------------------
130macro_rules! draw_frame {
131    ($self:expr, $terminal:expr, $render_call:expr) => {{
132        let render_ctx = $self.render_ctx();
133        let state = $self.store.state();
134        $terminal.draw(|frame| {
135            #[cfg(feature = "debug")]
136            if let Some(debug) = $self.debug.as_mut() {
137                let mut rf = |f: &mut Frame, area: Rect, s: &S, ctx: RenderContext| {
138                    ($render_call)(f, area, s, ctx)
139                };
140                debug.render(frame, state, render_ctx, &mut rf);
141            } else {
142                ($render_call)(frame, frame.area(), state, render_ctx);
143            }
144            #[cfg(not(feature = "debug"))]
145            {
146                ($render_call)(frame, frame.area(), state, render_ctx);
147            }
148        })?;
149        $self.should_render = false;
150    }};
151}
152
153/// Store interface used by `DispatchRuntime`.
154pub trait DispatchStore<S, A: Action> {
155    /// Dispatch an action and return whether the state changed.
156    fn dispatch(&mut self, action: A) -> bool;
157    /// Dispatch an action and return whether the state changed.
158    ///
159    /// Default behavior wraps [`Self::dispatch`] in `Ok(...)`.
160    fn try_dispatch(&mut self, action: A) -> Result<bool, DispatchError> {
161        Ok(self.dispatch(action))
162    }
163    /// Get the current state.
164    fn state(&self) -> &S;
165}
166
167impl<S, A: Action> DispatchStore<S, A> for Store<S, A> {
168    fn dispatch(&mut self, action: A) -> bool {
169        Store::dispatch(self, action)
170    }
171
172    fn state(&self) -> &S {
173        Store::state(self)
174    }
175}
176
177impl<S, A: Action, M: Middleware<S, A>> DispatchStore<S, A> for StoreWithMiddleware<S, A, M> {
178    fn dispatch(&mut self, action: A) -> bool {
179        StoreWithMiddleware::dispatch(self, action)
180    }
181
182    fn try_dispatch(&mut self, action: A) -> Result<bool, DispatchError> {
183        StoreWithMiddleware::try_dispatch(self, action)
184    }
185
186    fn state(&self) -> &S {
187        StoreWithMiddleware::state(self)
188    }
189}
190
191/// Effect store interface used by `EffectRuntime`.
192pub trait EffectStoreLike<S, A: Action, E> {
193    /// Dispatch an action and return state changes plus effects.
194    fn dispatch(&mut self, action: A) -> ReducerResult<E>;
195    /// Dispatch an action and return state changes plus effects.
196    ///
197    /// Default behavior wraps [`Self::dispatch`] in `Ok(...)`.
198    fn try_dispatch(&mut self, action: A) -> Result<ReducerResult<E>, DispatchError> {
199        Ok(self.dispatch(action))
200    }
201    /// Get the current state.
202    fn state(&self) -> &S;
203}
204
205impl<S, A: Action, E> EffectStoreLike<S, A, E> for EffectStore<S, A, E> {
206    fn dispatch(&mut self, action: A) -> ReducerResult<E> {
207        EffectStore::dispatch(self, action)
208    }
209
210    fn state(&self) -> &S {
211        EffectStore::state(self)
212    }
213}
214
215impl<S, A: Action, E, M: Middleware<S, A>> EffectStoreLike<S, A, E>
216    for EffectStoreWithMiddleware<S, A, E, M>
217{
218    fn dispatch(&mut self, action: A) -> ReducerResult<E> {
219        EffectStoreWithMiddleware::dispatch(self, action)
220    }
221
222    fn try_dispatch(&mut self, action: A) -> Result<ReducerResult<E>, DispatchError> {
223        EffectStoreWithMiddleware::try_dispatch(self, action)
224    }
225
226    fn state(&self) -> &S {
227        EffectStoreWithMiddleware::state(self)
228    }
229}
230
231/// Runtime helper for simple stores (no effects).
232pub struct DispatchRuntime<S, A: Action, St: DispatchStore<S, A> = Store<S, A>> {
233    store: St,
234    action_tx: mpsc::UnboundedSender<A>,
235    action_rx: mpsc::UnboundedReceiver<A>,
236    poller_config: PollerConfig,
237    #[cfg(feature = "debug")]
238    debug: Option<Box<dyn DebugAdapter<S, A>>>,
239    dispatch_error_handler: Box<dyn FnMut(&DispatchError) -> DispatchErrorPolicy>,
240    should_render: bool,
241    _state: std::marker::PhantomData<S>,
242}
243
244impl<S: 'static, A: Action> DispatchRuntime<S, A, Store<S, A>> {
245    /// Create a runtime from state + reducer.
246    pub fn new(state: S, reducer: Reducer<S, A>) -> Self {
247        Self::from_store(Store::new(state, reducer))
248    }
249}
250
251impl<S: 'static, A: Action, St: DispatchStore<S, A>> DispatchRuntime<S, A, St> {
252    /// Create a runtime from an existing store.
253    pub fn from_store(store: St) -> Self {
254        let (action_tx, action_rx) = mpsc::unbounded_channel();
255        Self {
256            store,
257            action_tx,
258            action_rx,
259            poller_config: PollerConfig::default(),
260            #[cfg(feature = "debug")]
261            debug: None,
262            dispatch_error_handler: Box::new(|_| DispatchErrorPolicy::Stop),
263            should_render: true,
264            _state: std::marker::PhantomData,
265        }
266    }
267
268    /// Attach a debug layer.
269    #[cfg(feature = "debug")]
270    pub fn with_debug<D>(mut self, debug: D) -> Self
271    where
272        D: DebugAdapter<S, A>,
273    {
274        self.debug = Some(Box::new(debug));
275        self
276    }
277
278    /// Configure event polling behavior.
279    pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
280        self.poller_config = config;
281        self
282    }
283
284    /// Configure handling for recoverable dispatch errors.
285    ///
286    /// The handler receives each [`DispatchError`] and selects a
287    /// [`DispatchErrorPolicy`]. Runtimes do not log or store errors by default;
288    /// do that inside this closure when needed.
289    pub fn with_dispatch_error_handler<F>(mut self, handler: F) -> Self
290    where
291        F: FnMut(&DispatchError) -> DispatchErrorPolicy + 'static,
292    {
293        self.dispatch_error_handler = Box::new(handler);
294        self
295    }
296
297    /// Send an action into the runtime queue.
298    pub fn enqueue(&self, action: A) {
299        let _ = self.action_tx.send(action);
300    }
301
302    /// Clone the action sender.
303    pub fn action_tx(&self) -> mpsc::UnboundedSender<A> {
304        self.action_tx.clone()
305    }
306
307    /// Access the current state.
308    pub fn state(&self) -> &S {
309        self.store.state()
310    }
311
312    fn render_ctx(&self) -> RenderContext {
313        RenderContext {
314            debug_enabled: {
315                #[cfg(feature = "debug")]
316                {
317                    self.debug.as_ref().is_some_and(|d| d.is_enabled())
318                }
319                #[cfg(not(feature = "debug"))]
320                {
321                    false
322                }
323            },
324        }
325    }
326
327    /// Returns `Some(needs_render)` if the debug layer intercepted the event.
328    #[allow(unused_variables)]
329    fn debug_intercept_event(&mut self, event: &EventKind) -> Option<bool> {
330        #[cfg(feature = "debug")]
331        if let Some(debug) = self.debug.as_mut() {
332            return debug.handle_event(event, self.store.state(), &self.action_tx);
333        }
334        None
335    }
336
337    #[allow(unused_variables)]
338    fn debug_log_action(&mut self, action: &A) {
339        #[cfg(feature = "debug")]
340        if let Some(debug) = self.debug.as_mut() {
341            debug.log_action(action);
342        }
343    }
344
345    fn enqueue_outcome(&mut self, outcome: EventOutcome<A>) {
346        if outcome.needs_render {
347            self.should_render = true;
348        }
349        for action in outcome.actions {
350            let _ = self.action_tx.send(action);
351        }
352    }
353
354    fn dispatch_action(&mut self, action: A) -> bool {
355        match self.store.try_dispatch(action) {
356            Ok(changed) => {
357                self.should_render = changed;
358                false
359            }
360            Err(error) => apply_dispatch_error_policy(
361                self.dispatch_error_handler.as_mut(),
362                error,
363                &mut self.should_render,
364            ),
365        }
366    }
367
368    fn spawn_poller(&self) -> (mpsc::UnboundedReceiver<RawEvent>, CancellationToken) {
369        let (event_tx, event_rx) = mpsc::unbounded_channel::<RawEvent>();
370        let cancel_token = CancellationToken::new();
371        let _handle = spawn_event_poller(
372            event_tx,
373            self.poller_config.poll_timeout,
374            self.poller_config.loop_sleep,
375            cancel_token.clone(),
376        );
377        (event_rx, cancel_token)
378    }
379
380    /// Run the event/action loop until quit.
381    pub async fn run<B, FRender, FEvent, FQuit, R>(
382        &mut self,
383        terminal: &mut Terminal<B>,
384        mut render: FRender,
385        mut map_event: FEvent,
386        mut should_quit: FQuit,
387    ) -> io::Result<()>
388    where
389        B: Backend,
390        FRender: FnMut(&mut Frame, Rect, &S, RenderContext),
391        FEvent: FnMut(&EventKind, &S) -> R,
392        R: Into<EventOutcome<A>>,
393        FQuit: FnMut(&A) -> bool,
394    {
395        let (mut event_rx, cancel_token) = self.spawn_poller();
396
397        loop {
398            if self.should_render {
399                draw_frame!(self, terminal, render);
400            }
401
402            tokio::select! {
403                Some(raw_event) = event_rx.recv() => {
404                    let event = process_raw_event(raw_event);
405                    if let Some(needs_render) = self.debug_intercept_event(&event) {
406                        self.should_render = needs_render;
407                        continue;
408                    }
409                    let outcome: EventOutcome<A> = map_event(&event, self.store.state()).into();
410                    self.enqueue_outcome(outcome);
411                }
412
413                Some(action) = self.action_rx.recv() => {
414                    if should_quit(&action) {
415                        break;
416                    }
417                    self.debug_log_action(&action);
418                    if self.dispatch_action(action) {
419                        break;
420                    }
421                }
422
423                else => { break; }
424            }
425        }
426
427        cancel_token.cancel();
428        Ok(())
429    }
430
431    /// Run the event/action loop using an EventBus for routing.
432    pub async fn run_with_bus<B, FRender, FQuit, Id, Ctx>(
433        &mut self,
434        terminal: &mut Terminal<B>,
435        bus: &mut EventBus<S, A, Id, Ctx>,
436        keybindings: &Keybindings<Ctx>,
437        mut render: FRender,
438        mut should_quit: FQuit,
439    ) -> io::Result<()>
440    where
441        B: Backend,
442        Id: ComponentId + 'static,
443        Ctx: BindingContext + 'static,
444        S: EventRoutingState<Id, Ctx>,
445        FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
446        FQuit: FnMut(&A) -> bool,
447    {
448        let (mut event_rx, cancel_token) = self.spawn_poller();
449
450        loop {
451            if self.should_render {
452                draw_frame!(self, terminal, |f, area, s, ctx| render(
453                    f,
454                    area,
455                    s,
456                    ctx,
457                    bus.context_mut()
458                ));
459            }
460
461            tokio::select! {
462                Some(raw_event) = event_rx.recv() => {
463                    let event = process_raw_event(raw_event);
464                    if let Some(needs_render) = self.debug_intercept_event(&event) {
465                        self.should_render = needs_render;
466                        continue;
467                    }
468                    let outcome = bus.handle_event(&event, self.store.state(), keybindings);
469                    self.enqueue_outcome(outcome);
470                }
471
472                Some(action) = self.action_rx.recv() => {
473                    if should_quit(&action) {
474                        break;
475                    }
476                    self.debug_log_action(&action);
477                    if self.dispatch_action(action) {
478                        break;
479                    }
480                }
481
482                else => { break; }
483            }
484        }
485
486        cancel_token.cancel();
487        Ok(())
488    }
489}
490
491/// Context passed to effect handlers.
492pub struct EffectContext<'a, A: Action> {
493    action_tx: &'a mpsc::UnboundedSender<A>,
494    #[cfg(feature = "tasks")]
495    tasks: &'a mut TaskManager<A>,
496    #[cfg(feature = "subscriptions")]
497    subscriptions: &'a mut Subscriptions<A>,
498}
499
500impl<'a, A: Action> EffectContext<'a, A> {
501    /// Send an action directly.
502    pub fn emit(&self, action: A) {
503        let _ = self.action_tx.send(action);
504    }
505
506    /// Access the action sender.
507    pub fn action_tx(&self) -> &mpsc::UnboundedSender<A> {
508        self.action_tx
509    }
510
511    /// Access the task manager.
512    #[cfg(feature = "tasks")]
513    pub fn tasks(&mut self) -> &mut TaskManager<A> {
514        self.tasks
515    }
516
517    /// Access subscriptions.
518    #[cfg(feature = "subscriptions")]
519    pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
520        self.subscriptions
521    }
522}
523
524/// Runtime helper for effect-based stores.
525pub struct EffectRuntime<S, A: Action, E, St: EffectStoreLike<S, A, E> = EffectStore<S, A, E>> {
526    store: St,
527    action_tx: mpsc::UnboundedSender<A>,
528    action_rx: mpsc::UnboundedReceiver<A>,
529    poller_config: PollerConfig,
530    #[cfg(feature = "debug")]
531    debug: Option<Box<dyn DebugAdapter<S, A>>>,
532    dispatch_error_handler: Box<dyn FnMut(&DispatchError) -> DispatchErrorPolicy>,
533    should_render: bool,
534    #[cfg(feature = "tasks")]
535    tasks: TaskManager<A>,
536    #[cfg(feature = "subscriptions")]
537    subscriptions: Subscriptions<A>,
538    /// Broadcasts action names when dispatched (for replay await functionality).
539    action_broadcast: tokio::sync::broadcast::Sender<String>,
540    _state: std::marker::PhantomData<S>,
541    _effect: std::marker::PhantomData<E>,
542}
543
544impl<S: 'static, A: Action, E> EffectRuntime<S, A, E, EffectStore<S, A, E>> {
545    /// Create a runtime from state + effect reducer.
546    pub fn new(state: S, reducer: crate::effect::EffectReducer<S, A, E>) -> Self {
547        Self::from_store(EffectStore::new(state, reducer))
548    }
549}
550
551impl<S: 'static, A: Action, E, St: EffectStoreLike<S, A, E>> EffectRuntime<S, A, E, St> {
552    /// Create a runtime from an existing effect store.
553    pub fn from_store(store: St) -> Self {
554        let (action_tx, action_rx) = mpsc::unbounded_channel();
555        let (action_broadcast, _) = tokio::sync::broadcast::channel(64);
556
557        #[cfg(feature = "tasks")]
558        let tasks = TaskManager::new(action_tx.clone());
559        #[cfg(feature = "subscriptions")]
560        let subscriptions = Subscriptions::new(action_tx.clone());
561
562        Self {
563            store,
564            action_tx,
565            action_rx,
566            poller_config: PollerConfig::default(),
567            #[cfg(feature = "debug")]
568            debug: None,
569            dispatch_error_handler: Box::new(|_| DispatchErrorPolicy::Stop),
570            should_render: true,
571            #[cfg(feature = "tasks")]
572            tasks,
573            #[cfg(feature = "subscriptions")]
574            subscriptions,
575            action_broadcast,
576            _state: std::marker::PhantomData,
577            _effect: std::marker::PhantomData,
578        }
579    }
580
581    /// Attach a debug layer (auto-wires tasks/subscriptions when available).
582    #[cfg(feature = "debug")]
583    pub fn with_debug<D>(mut self, debug: D) -> Self
584    where
585        D: DebugAdapter<S, A> + DebugHooks<A>,
586    {
587        let debug = {
588            let debug = debug;
589            #[cfg(feature = "tasks")]
590            let debug = debug.with_task_manager(&self.tasks);
591            #[cfg(feature = "subscriptions")]
592            let debug = debug.with_subscriptions(&self.subscriptions);
593            debug
594        };
595        self.debug = Some(Box::new(debug));
596        self
597    }
598
599    /// Configure event polling behavior.
600    pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
601        self.poller_config = config;
602        self
603    }
604
605    /// Configure handling for recoverable dispatch errors.
606    ///
607    /// The handler receives each [`DispatchError`] and selects a
608    /// [`DispatchErrorPolicy`]. Runtimes do not log or store errors by default;
609    /// do that inside this closure when needed.
610    pub fn with_dispatch_error_handler<F>(mut self, handler: F) -> Self
611    where
612        F: FnMut(&DispatchError) -> DispatchErrorPolicy + 'static,
613    {
614        self.dispatch_error_handler = Box::new(handler);
615        self
616    }
617
618    /// Subscribe to action name broadcasts.
619    ///
620    /// Returns a receiver that will receive action names (from `action.name()`)
621    /// whenever an action is dispatched. Useful for replay await functionality.
622    pub fn subscribe_actions(&self) -> tokio::sync::broadcast::Receiver<String> {
623        self.action_broadcast.subscribe()
624    }
625
626    /// Send an action into the runtime queue.
627    pub fn enqueue(&self, action: A) {
628        let _ = self.action_tx.send(action);
629    }
630
631    /// Clone the action sender.
632    pub fn action_tx(&self) -> mpsc::UnboundedSender<A> {
633        self.action_tx.clone()
634    }
635
636    /// Access the current state.
637    pub fn state(&self) -> &S {
638        self.store.state()
639    }
640
641    /// Access the task manager.
642    #[cfg(feature = "tasks")]
643    pub fn tasks(&mut self) -> &mut TaskManager<A> {
644        &mut self.tasks
645    }
646
647    /// Access subscriptions.
648    #[cfg(feature = "subscriptions")]
649    pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
650        &mut self.subscriptions
651    }
652
653    #[cfg(all(feature = "tasks", feature = "subscriptions"))]
654    fn effect_context(&mut self) -> EffectContext<'_, A> {
655        EffectContext {
656            action_tx: &self.action_tx,
657            tasks: &mut self.tasks,
658            subscriptions: &mut self.subscriptions,
659        }
660    }
661
662    #[cfg(all(feature = "tasks", not(feature = "subscriptions")))]
663    fn effect_context(&mut self) -> EffectContext<'_, A> {
664        EffectContext {
665            action_tx: &self.action_tx,
666            tasks: &mut self.tasks,
667        }
668    }
669
670    #[cfg(all(not(feature = "tasks"), feature = "subscriptions"))]
671    fn effect_context(&mut self) -> EffectContext<'_, A> {
672        EffectContext {
673            action_tx: &self.action_tx,
674            subscriptions: &mut self.subscriptions,
675        }
676    }
677
678    #[cfg(all(not(feature = "tasks"), not(feature = "subscriptions")))]
679    fn effect_context(&mut self) -> EffectContext<'_, A> {
680        EffectContext {
681            action_tx: &self.action_tx,
682        }
683    }
684
685    fn render_ctx(&self) -> RenderContext {
686        RenderContext {
687            debug_enabled: {
688                #[cfg(feature = "debug")]
689                {
690                    self.debug.as_ref().is_some_and(|d| d.is_enabled())
691                }
692                #[cfg(not(feature = "debug"))]
693                {
694                    false
695                }
696            },
697        }
698    }
699
700    #[allow(unused_variables)]
701    fn debug_intercept_event(&mut self, event: &EventKind) -> Option<bool> {
702        #[cfg(feature = "debug")]
703        if let Some(debug) = self.debug.as_mut() {
704            return debug.handle_event(event, self.store.state(), &self.action_tx);
705        }
706        None
707    }
708
709    #[allow(unused_variables)]
710    fn debug_log_action(&mut self, action: &A) {
711        #[cfg(feature = "debug")]
712        if let Some(debug) = self.debug.as_mut() {
713            debug.log_action(action);
714        }
715    }
716
717    fn enqueue_outcome(&mut self, outcome: EventOutcome<A>) {
718        if outcome.needs_render {
719            self.should_render = true;
720        }
721        for action in outcome.actions {
722            let _ = self.action_tx.send(action);
723        }
724    }
725
726    fn broadcast_action(&self, action: &A) {
727        if self.action_broadcast.receiver_count() > 0 {
728            let _ = self.action_broadcast.send(action.name().to_string());
729        }
730    }
731
732    fn spawn_poller(&self) -> (mpsc::UnboundedReceiver<RawEvent>, CancellationToken) {
733        let (event_tx, event_rx) = mpsc::unbounded_channel::<RawEvent>();
734        let cancel_token = CancellationToken::new();
735        let _handle = spawn_event_poller(
736            event_tx,
737            self.poller_config.poll_timeout,
738            self.poller_config.loop_sleep,
739            cancel_token.clone(),
740        );
741        (event_rx, cancel_token)
742    }
743
744    fn cleanup(&mut self, cancel_token: CancellationToken) {
745        cancel_token.cancel();
746        #[cfg(feature = "subscriptions")]
747        self.subscriptions.cancel_all();
748        #[cfg(feature = "tasks")]
749        self.tasks.cancel_all();
750    }
751
752    fn dispatch_and_handle_effects(
753        &mut self,
754        action: A,
755        handle_effect: &mut impl FnMut(E, &mut EffectContext<A>),
756    ) -> bool {
757        self.broadcast_action(&action);
758        match self.store.try_dispatch(action) {
759            Ok(result) => {
760                if result.has_effects() {
761                    let mut ctx = self.effect_context();
762                    for effect in result.effects {
763                        handle_effect(effect, &mut ctx);
764                    }
765                }
766                self.should_render = result.changed;
767                false
768            }
769            Err(error) => apply_dispatch_error_policy(
770                self.dispatch_error_handler.as_mut(),
771                error,
772                &mut self.should_render,
773            ),
774        }
775    }
776
777    /// Run the event/action loop until quit.
778    pub async fn run<B, FRender, FEvent, FQuit, FEffect, R>(
779        &mut self,
780        terminal: &mut Terminal<B>,
781        mut render: FRender,
782        mut map_event: FEvent,
783        mut should_quit: FQuit,
784        mut handle_effect: FEffect,
785    ) -> io::Result<()>
786    where
787        B: Backend,
788        FRender: FnMut(&mut Frame, Rect, &S, RenderContext),
789        FEvent: FnMut(&EventKind, &S) -> R,
790        R: Into<EventOutcome<A>>,
791        FQuit: FnMut(&A) -> bool,
792        FEffect: FnMut(E, &mut EffectContext<A>),
793    {
794        let (mut event_rx, cancel_token) = self.spawn_poller();
795
796        loop {
797            if self.should_render {
798                draw_frame!(self, terminal, render);
799            }
800
801            tokio::select! {
802                Some(raw_event) = event_rx.recv() => {
803                    let event = process_raw_event(raw_event);
804                    if let Some(needs_render) = self.debug_intercept_event(&event) {
805                        self.should_render = needs_render;
806                        continue;
807                    }
808                    let outcome: EventOutcome<A> = map_event(&event, self.store.state()).into();
809                    self.enqueue_outcome(outcome);
810                }
811
812                Some(action) = self.action_rx.recv() => {
813                    if should_quit(&action) {
814                        break;
815                    }
816                    self.debug_log_action(&action);
817                    if self.dispatch_and_handle_effects(action, &mut handle_effect) {
818                        break;
819                    }
820                }
821
822                else => { break; }
823            }
824        }
825
826        self.cleanup(cancel_token);
827        Ok(())
828    }
829
830    /// Run the event/action loop using an EventBus for routing.
831    pub async fn run_with_bus<B, FRender, FQuit, FEffect, Id, Ctx>(
832        &mut self,
833        terminal: &mut Terminal<B>,
834        bus: &mut EventBus<S, A, Id, Ctx>,
835        keybindings: &Keybindings<Ctx>,
836        mut render: FRender,
837        mut should_quit: FQuit,
838        mut handle_effect: FEffect,
839    ) -> io::Result<()>
840    where
841        B: Backend,
842        Id: ComponentId + 'static,
843        Ctx: BindingContext + 'static,
844        S: EventRoutingState<Id, Ctx>,
845        FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
846        FQuit: FnMut(&A) -> bool,
847        FEffect: FnMut(E, &mut EffectContext<A>),
848    {
849        let (mut event_rx, cancel_token) = self.spawn_poller();
850
851        loop {
852            if self.should_render {
853                draw_frame!(self, terminal, |f, area, s, ctx| render(
854                    f,
855                    area,
856                    s,
857                    ctx,
858                    bus.context_mut()
859                ));
860            }
861
862            tokio::select! {
863                Some(raw_event) = event_rx.recv() => {
864                    let event = process_raw_event(raw_event);
865                    if let Some(needs_render) = self.debug_intercept_event(&event) {
866                        self.should_render = needs_render;
867                        continue;
868                    }
869                    let outcome = bus.handle_event(&event, self.store.state(), keybindings);
870                    self.enqueue_outcome(outcome);
871                }
872
873                Some(action) = self.action_rx.recv() => {
874                    if should_quit(&action) {
875                        break;
876                    }
877                    self.debug_log_action(&action);
878                    if self.dispatch_and_handle_effects(action, &mut handle_effect) {
879                        break;
880                    }
881                }
882
883                else => { break; }
884            }
885        }
886
887        self.cleanup(cancel_token);
888        Ok(())
889    }
890}
891
892#[cfg(test)]
893mod tests {
894    use super::*;
895    use crate::store::DispatchLimits;
896    use std::collections::VecDeque;
897
898    #[derive(Clone, Debug)]
899    enum TestAction {
900        Increment,
901    }
902
903    impl Action for TestAction {
904        fn name(&self) -> &'static str {
905            match self {
906                TestAction::Increment => "Increment",
907            }
908        }
909    }
910
911    #[derive(Default)]
912    struct TestState {
913        count: usize,
914    }
915
916    fn reducer(state: &mut TestState, _action: TestAction) -> bool {
917        state.count += 1;
918        true
919    }
920
921    fn effect_reducer(state: &mut TestState, _action: TestAction) -> ReducerResult<()> {
922        state.count += 1;
923        ReducerResult::changed()
924    }
925
926    struct LoopMiddleware;
927
928    impl Middleware<TestState, TestAction> for LoopMiddleware {
929        fn before(&mut self, _action: &TestAction, _state: &TestState) -> bool {
930            true
931        }
932
933        fn after(
934            &mut self,
935            _action: &TestAction,
936            _state_changed: bool,
937            _state: &TestState,
938        ) -> Vec<TestAction> {
939            vec![TestAction::Increment]
940        }
941    }
942
943    struct MockDispatchStore {
944        state: TestState,
945        queued_results: VecDeque<Result<bool, DispatchError>>,
946    }
947
948    impl MockDispatchStore {
949        fn from_results(results: impl IntoIterator<Item = Result<bool, DispatchError>>) -> Self {
950            Self {
951                state: TestState::default(),
952                queued_results: results.into_iter().collect(),
953            }
954        }
955    }
956
957    impl DispatchStore<TestState, TestAction> for MockDispatchStore {
958        fn dispatch(&mut self, _action: TestAction) -> bool {
959            true
960        }
961
962        fn try_dispatch(&mut self, _action: TestAction) -> Result<bool, DispatchError> {
963            self.queued_results
964                .pop_front()
965                .expect("test configured with at least one dispatch result")
966        }
967
968        fn state(&self) -> &TestState {
969            &self.state
970        }
971    }
972
973    struct MockEffectStore {
974        state: TestState,
975        queued_results: VecDeque<Result<ReducerResult<()>, DispatchError>>,
976    }
977
978    impl MockEffectStore {
979        fn from_results(
980            results: impl IntoIterator<Item = Result<ReducerResult<()>, DispatchError>>,
981        ) -> Self {
982            Self {
983                state: TestState::default(),
984                queued_results: results.into_iter().collect(),
985            }
986        }
987    }
988
989    impl EffectStoreLike<TestState, TestAction, ()> for MockEffectStore {
990        fn dispatch(&mut self, _action: TestAction) -> ReducerResult<()> {
991            ReducerResult::changed()
992        }
993
994        fn try_dispatch(
995            &mut self,
996            _action: TestAction,
997        ) -> Result<ReducerResult<()>, DispatchError> {
998            self.queued_results
999                .pop_front()
1000                .expect("test configured with at least one dispatch result")
1001        }
1002
1003        fn state(&self) -> &TestState {
1004            &self.state
1005        }
1006    }
1007
1008    fn test_error() -> DispatchError {
1009        DispatchError::DepthExceeded {
1010            max_depth: 1,
1011            action: "Increment",
1012        }
1013    }
1014
1015    #[test]
1016    fn dispatch_runtime_continue_policy_keeps_running_without_render() {
1017        let store = MockDispatchStore::from_results([Err(test_error())]);
1018        let mut runtime = DispatchRuntime::from_store(store)
1019            .with_dispatch_error_handler(|_| DispatchErrorPolicy::Continue);
1020        runtime.should_render = false;
1021
1022        let should_stop = runtime.dispatch_action(TestAction::Increment);
1023
1024        assert!(!should_stop);
1025        assert!(!runtime.should_render);
1026    }
1027
1028    #[test]
1029    fn dispatch_runtime_render_policy_forces_render() {
1030        let store = MockDispatchStore::from_results([Err(test_error())]);
1031        let mut runtime = DispatchRuntime::from_store(store)
1032            .with_dispatch_error_handler(|_| DispatchErrorPolicy::Render);
1033        runtime.should_render = false;
1034
1035        let should_stop = runtime.dispatch_action(TestAction::Increment);
1036
1037        assert!(!should_stop);
1038        assert!(runtime.should_render);
1039    }
1040
1041    #[test]
1042    fn dispatch_runtime_stop_policy_breaks_loop() {
1043        let store = MockDispatchStore::from_results([Err(test_error())]);
1044        let mut runtime = DispatchRuntime::from_store(store)
1045            .with_dispatch_error_handler(|_| DispatchErrorPolicy::Stop);
1046        runtime.should_render = false;
1047
1048        let should_stop = runtime.dispatch_action(TestAction::Increment);
1049
1050        assert!(should_stop);
1051        assert!(!runtime.should_render);
1052    }
1053
1054    #[test]
1055    fn effect_runtime_continue_policy_keeps_running_without_render() {
1056        let store = MockEffectStore::from_results([Err(test_error())]);
1057        let mut runtime = EffectRuntime::from_store(store)
1058            .with_dispatch_error_handler(|_| DispatchErrorPolicy::Continue);
1059        runtime.should_render = false;
1060
1061        let should_stop =
1062            runtime.dispatch_and_handle_effects(TestAction::Increment, &mut |_effect, _ctx| {});
1063
1064        assert!(!should_stop);
1065        assert!(!runtime.should_render);
1066    }
1067
1068    #[test]
1069    fn effect_runtime_render_policy_forces_render() {
1070        let store = MockEffectStore::from_results([Err(test_error())]);
1071        let mut runtime = EffectRuntime::from_store(store)
1072            .with_dispatch_error_handler(|_| DispatchErrorPolicy::Render);
1073        runtime.should_render = false;
1074
1075        let should_stop =
1076            runtime.dispatch_and_handle_effects(TestAction::Increment, &mut |_effect, _ctx| {});
1077
1078        assert!(!should_stop);
1079        assert!(runtime.should_render);
1080    }
1081
1082    #[test]
1083    fn effect_runtime_stop_policy_breaks_loop() {
1084        let store = MockEffectStore::from_results([Err(test_error())]);
1085        let mut runtime = EffectRuntime::from_store(store)
1086            .with_dispatch_error_handler(|_| DispatchErrorPolicy::Stop);
1087        runtime.should_render = false;
1088
1089        let should_stop =
1090            runtime.dispatch_and_handle_effects(TestAction::Increment, &mut |_effect, _ctx| {});
1091
1092        assert!(should_stop);
1093        assert!(!runtime.should_render);
1094    }
1095
1096    #[test]
1097    fn dispatch_runtime_uses_try_dispatch_for_middleware_overflow() {
1098        let store = StoreWithMiddleware::new(TestState::default(), reducer, LoopMiddleware)
1099            .with_dispatch_limits(DispatchLimits {
1100                max_depth: 1,
1101                max_actions: 100,
1102            });
1103        let mut runtime = DispatchRuntime::from_store(store)
1104            .with_dispatch_error_handler(|_| DispatchErrorPolicy::Stop);
1105        runtime.should_render = false;
1106
1107        let should_stop = runtime.dispatch_action(TestAction::Increment);
1108
1109        assert!(should_stop);
1110        assert_eq!(runtime.state().count, 1);
1111    }
1112
1113    #[test]
1114    fn effect_runtime_uses_try_dispatch_for_middleware_overflow() {
1115        let store =
1116            EffectStoreWithMiddleware::new(TestState::default(), effect_reducer, LoopMiddleware)
1117                .with_dispatch_limits(DispatchLimits {
1118                    max_depth: 1,
1119                    max_actions: 100,
1120                });
1121        let mut runtime = EffectRuntime::from_store(store)
1122            .with_dispatch_error_handler(|_| DispatchErrorPolicy::Stop);
1123        runtime.should_render = false;
1124
1125        let should_stop =
1126            runtime.dispatch_and_handle_effects(TestAction::Increment, &mut |_effect, _ctx| {});
1127
1128        assert!(should_stop);
1129        assert_eq!(runtime.state().count, 1);
1130    }
1131}