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::{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#[cfg(feature = "debug")]
62pub trait DebugAdapter<S, A>: 'static {
63    fn render(
64        &mut self,
65        frame: &mut Frame,
66        state: &S,
67        render_ctx: RenderContext,
68        render_fn: &mut dyn FnMut(&mut Frame, Rect, &S, RenderContext),
69    );
70
71    fn handle_event(
72        &mut self,
73        event: &EventKind,
74        state: &S,
75        action_tx: &mpsc::UnboundedSender<A>,
76    ) -> Option<bool>;
77
78    fn log_action(&mut self, action: &A);
79    fn is_enabled(&self) -> bool;
80}
81
82#[cfg(feature = "debug")]
83pub trait DebugHooks<A>: Sized {
84    #[cfg(feature = "tasks")]
85    fn with_task_manager(self, _tasks: &TaskManager<A>) -> Self {
86        self
87    }
88
89    #[cfg(feature = "subscriptions")]
90    fn with_subscriptions(self, _subscriptions: &Subscriptions<A>) -> Self {
91        self
92    }
93}
94
95// ---------------------------------------------------------------------------
96// Shared draw macro — renders a frame with optional debug overlay.
97//
98// `$render_call` must be callable as `$render_call(frame, area, state, ctx)`.
99// For bus variants, wrap the user's render closure to include EventContext.
100// ---------------------------------------------------------------------------
101macro_rules! draw_frame {
102    ($self:expr, $terminal:expr, $render_call:expr) => {{
103        let render_ctx = $self.render_ctx();
104        let state = $self.store.state();
105        $terminal.draw(|frame| {
106            #[cfg(feature = "debug")]
107            if let Some(debug) = $self.debug.as_mut() {
108                let mut rf = |f: &mut Frame, area: Rect, s: &S, ctx: RenderContext| {
109                    ($render_call)(f, area, s, ctx)
110                };
111                debug.render(frame, state, render_ctx, &mut rf);
112            } else {
113                ($render_call)(frame, frame.area(), state, render_ctx);
114            }
115            #[cfg(not(feature = "debug"))]
116            {
117                ($render_call)(frame, frame.area(), state, render_ctx);
118            }
119        })?;
120        $self.should_render = false;
121    }};
122}
123
124/// Store interface used by `DispatchRuntime`.
125pub trait DispatchStore<S, A: Action> {
126    /// Dispatch an action and return whether the state changed.
127    fn dispatch(&mut self, action: A) -> bool;
128    /// Get the current state.
129    fn state(&self) -> &S;
130}
131
132impl<S, A: Action> DispatchStore<S, A> for Store<S, A> {
133    fn dispatch(&mut self, action: A) -> bool {
134        Store::dispatch(self, action)
135    }
136
137    fn state(&self) -> &S {
138        Store::state(self)
139    }
140}
141
142impl<S, A: Action, M: Middleware<S, A>> DispatchStore<S, A> for StoreWithMiddleware<S, A, M> {
143    fn dispatch(&mut self, action: A) -> bool {
144        StoreWithMiddleware::dispatch(self, action)
145    }
146
147    fn state(&self) -> &S {
148        StoreWithMiddleware::state(self)
149    }
150}
151
152/// Effect store interface used by `EffectRuntime`.
153pub trait EffectStoreLike<S, A: Action, E> {
154    /// Dispatch an action and return state changes plus effects.
155    fn dispatch(&mut self, action: A) -> ReducerResult<E>;
156    /// Get the current state.
157    fn state(&self) -> &S;
158}
159
160impl<S, A: Action, E> EffectStoreLike<S, A, E> for EffectStore<S, A, E> {
161    fn dispatch(&mut self, action: A) -> ReducerResult<E> {
162        EffectStore::dispatch(self, action)
163    }
164
165    fn state(&self) -> &S {
166        EffectStore::state(self)
167    }
168}
169
170impl<S, A: Action, E, M: Middleware<S, A>> EffectStoreLike<S, A, E>
171    for EffectStoreWithMiddleware<S, A, E, M>
172{
173    fn dispatch(&mut self, action: A) -> ReducerResult<E> {
174        EffectStoreWithMiddleware::dispatch(self, action)
175    }
176
177    fn state(&self) -> &S {
178        EffectStoreWithMiddleware::state(self)
179    }
180}
181
182/// Runtime helper for simple stores (no effects).
183pub struct DispatchRuntime<S, A: Action, St: DispatchStore<S, A> = Store<S, A>> {
184    store: St,
185    action_tx: mpsc::UnboundedSender<A>,
186    action_rx: mpsc::UnboundedReceiver<A>,
187    poller_config: PollerConfig,
188    #[cfg(feature = "debug")]
189    debug: Option<Box<dyn DebugAdapter<S, A>>>,
190    should_render: bool,
191    _state: std::marker::PhantomData<S>,
192}
193
194impl<S: 'static, A: Action> DispatchRuntime<S, A, Store<S, A>> {
195    /// Create a runtime from state + reducer.
196    pub fn new(state: S, reducer: Reducer<S, A>) -> Self {
197        Self::from_store(Store::new(state, reducer))
198    }
199}
200
201impl<S: 'static, A: Action, St: DispatchStore<S, A>> DispatchRuntime<S, A, St> {
202    /// Create a runtime from an existing store.
203    pub fn from_store(store: St) -> Self {
204        let (action_tx, action_rx) = mpsc::unbounded_channel();
205        Self {
206            store,
207            action_tx,
208            action_rx,
209            poller_config: PollerConfig::default(),
210            #[cfg(feature = "debug")]
211            debug: None,
212            should_render: true,
213            _state: std::marker::PhantomData,
214        }
215    }
216
217    /// Attach a debug layer.
218    #[cfg(feature = "debug")]
219    pub fn with_debug<D>(mut self, debug: D) -> Self
220    where
221        D: DebugAdapter<S, A>,
222    {
223        self.debug = Some(Box::new(debug));
224        self
225    }
226
227    /// Configure event polling behavior.
228    pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
229        self.poller_config = config;
230        self
231    }
232
233    /// Send an action into the runtime queue.
234    pub fn enqueue(&self, action: A) {
235        let _ = self.action_tx.send(action);
236    }
237
238    /// Clone the action sender.
239    pub fn action_tx(&self) -> mpsc::UnboundedSender<A> {
240        self.action_tx.clone()
241    }
242
243    /// Access the current state.
244    pub fn state(&self) -> &S {
245        self.store.state()
246    }
247
248    fn render_ctx(&self) -> RenderContext {
249        RenderContext {
250            debug_enabled: {
251                #[cfg(feature = "debug")]
252                {
253                    self.debug.as_ref().is_some_and(|d| d.is_enabled())
254                }
255                #[cfg(not(feature = "debug"))]
256                {
257                    false
258                }
259            },
260        }
261    }
262
263    /// Returns `Some(needs_render)` if the debug layer intercepted the event.
264    #[allow(unused_variables)]
265    fn debug_intercept_event(&mut self, event: &EventKind) -> Option<bool> {
266        #[cfg(feature = "debug")]
267        if let Some(debug) = self.debug.as_mut() {
268            return debug.handle_event(event, self.store.state(), &self.action_tx);
269        }
270        None
271    }
272
273    #[allow(unused_variables)]
274    fn debug_log_action(&mut self, action: &A) {
275        #[cfg(feature = "debug")]
276        if let Some(debug) = self.debug.as_mut() {
277            debug.log_action(action);
278        }
279    }
280
281    fn enqueue_outcome(&mut self, outcome: EventOutcome<A>) {
282        if outcome.needs_render {
283            self.should_render = true;
284        }
285        for action in outcome.actions {
286            let _ = self.action_tx.send(action);
287        }
288    }
289
290    fn spawn_poller(&self) -> (mpsc::UnboundedReceiver<RawEvent>, CancellationToken) {
291        let (event_tx, event_rx) = mpsc::unbounded_channel::<RawEvent>();
292        let cancel_token = CancellationToken::new();
293        let _handle = spawn_event_poller(
294            event_tx,
295            self.poller_config.poll_timeout,
296            self.poller_config.loop_sleep,
297            cancel_token.clone(),
298        );
299        (event_rx, cancel_token)
300    }
301
302    /// Run the event/action loop until quit.
303    pub async fn run<B, FRender, FEvent, FQuit, R>(
304        &mut self,
305        terminal: &mut Terminal<B>,
306        mut render: FRender,
307        mut map_event: FEvent,
308        mut should_quit: FQuit,
309    ) -> io::Result<()>
310    where
311        B: Backend,
312        FRender: FnMut(&mut Frame, Rect, &S, RenderContext),
313        FEvent: FnMut(&EventKind, &S) -> R,
314        R: Into<EventOutcome<A>>,
315        FQuit: FnMut(&A) -> bool,
316    {
317        let (mut event_rx, cancel_token) = self.spawn_poller();
318
319        loop {
320            if self.should_render {
321                draw_frame!(self, terminal, render);
322            }
323
324            tokio::select! {
325                Some(raw_event) = event_rx.recv() => {
326                    let event = process_raw_event(raw_event);
327                    if let Some(needs_render) = self.debug_intercept_event(&event) {
328                        self.should_render = needs_render;
329                        continue;
330                    }
331                    let outcome: EventOutcome<A> = map_event(&event, self.store.state()).into();
332                    self.enqueue_outcome(outcome);
333                }
334
335                Some(action) = self.action_rx.recv() => {
336                    if should_quit(&action) {
337                        break;
338                    }
339                    self.debug_log_action(&action);
340                    self.should_render = self.store.dispatch(action);
341                }
342
343                else => { break; }
344            }
345        }
346
347        cancel_token.cancel();
348        Ok(())
349    }
350
351    /// Run the event/action loop using an EventBus for routing.
352    pub async fn run_with_bus<B, FRender, FQuit, Id, Ctx>(
353        &mut self,
354        terminal: &mut Terminal<B>,
355        bus: &mut EventBus<S, A, Id, Ctx>,
356        keybindings: &Keybindings<Ctx>,
357        mut render: FRender,
358        mut should_quit: FQuit,
359    ) -> io::Result<()>
360    where
361        B: Backend,
362        Id: ComponentId + 'static,
363        Ctx: BindingContext + 'static,
364        S: EventRoutingState<Id, Ctx>,
365        FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
366        FQuit: FnMut(&A) -> bool,
367    {
368        let (mut event_rx, cancel_token) = self.spawn_poller();
369
370        loop {
371            if self.should_render {
372                draw_frame!(self, terminal, |f, area, s, ctx| render(
373                    f,
374                    area,
375                    s,
376                    ctx,
377                    bus.context_mut()
378                ));
379            }
380
381            tokio::select! {
382                Some(raw_event) = event_rx.recv() => {
383                    let event = process_raw_event(raw_event);
384                    if let Some(needs_render) = self.debug_intercept_event(&event) {
385                        self.should_render = needs_render;
386                        continue;
387                    }
388                    let outcome = bus.handle_event(&event, self.store.state(), keybindings);
389                    self.enqueue_outcome(outcome);
390                }
391
392                Some(action) = self.action_rx.recv() => {
393                    if should_quit(&action) {
394                        break;
395                    }
396                    self.debug_log_action(&action);
397                    self.should_render = self.store.dispatch(action);
398                }
399
400                else => { break; }
401            }
402        }
403
404        cancel_token.cancel();
405        Ok(())
406    }
407}
408
409/// Context passed to effect handlers.
410pub struct EffectContext<'a, A: Action> {
411    action_tx: &'a mpsc::UnboundedSender<A>,
412    #[cfg(feature = "tasks")]
413    tasks: &'a mut TaskManager<A>,
414    #[cfg(feature = "subscriptions")]
415    subscriptions: &'a mut Subscriptions<A>,
416}
417
418impl<'a, A: Action> EffectContext<'a, A> {
419    /// Send an action directly.
420    pub fn emit(&self, action: A) {
421        let _ = self.action_tx.send(action);
422    }
423
424    /// Access the action sender.
425    pub fn action_tx(&self) -> &mpsc::UnboundedSender<A> {
426        self.action_tx
427    }
428
429    /// Access the task manager.
430    #[cfg(feature = "tasks")]
431    pub fn tasks(&mut self) -> &mut TaskManager<A> {
432        self.tasks
433    }
434
435    /// Access subscriptions.
436    #[cfg(feature = "subscriptions")]
437    pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
438        self.subscriptions
439    }
440}
441
442/// Runtime helper for effect-based stores.
443pub struct EffectRuntime<S, A: Action, E, St: EffectStoreLike<S, A, E> = EffectStore<S, A, E>> {
444    store: St,
445    action_tx: mpsc::UnboundedSender<A>,
446    action_rx: mpsc::UnboundedReceiver<A>,
447    poller_config: PollerConfig,
448    #[cfg(feature = "debug")]
449    debug: Option<Box<dyn DebugAdapter<S, A>>>,
450    should_render: bool,
451    #[cfg(feature = "tasks")]
452    tasks: TaskManager<A>,
453    #[cfg(feature = "subscriptions")]
454    subscriptions: Subscriptions<A>,
455    /// Broadcasts action names when dispatched (for replay await functionality).
456    action_broadcast: tokio::sync::broadcast::Sender<String>,
457    _state: std::marker::PhantomData<S>,
458    _effect: std::marker::PhantomData<E>,
459}
460
461impl<S: 'static, A: Action, E> EffectRuntime<S, A, E, EffectStore<S, A, E>> {
462    /// Create a runtime from state + effect reducer.
463    pub fn new(state: S, reducer: crate::effect::EffectReducer<S, A, E>) -> Self {
464        Self::from_store(EffectStore::new(state, reducer))
465    }
466}
467
468impl<S: 'static, A: Action, E, St: EffectStoreLike<S, A, E>> EffectRuntime<S, A, E, St> {
469    /// Create a runtime from an existing effect store.
470    pub fn from_store(store: St) -> Self {
471        let (action_tx, action_rx) = mpsc::unbounded_channel();
472        let (action_broadcast, _) = tokio::sync::broadcast::channel(64);
473
474        #[cfg(feature = "tasks")]
475        let tasks = TaskManager::new(action_tx.clone());
476        #[cfg(feature = "subscriptions")]
477        let subscriptions = Subscriptions::new(action_tx.clone());
478
479        Self {
480            store,
481            action_tx,
482            action_rx,
483            poller_config: PollerConfig::default(),
484            #[cfg(feature = "debug")]
485            debug: None,
486            should_render: true,
487            #[cfg(feature = "tasks")]
488            tasks,
489            #[cfg(feature = "subscriptions")]
490            subscriptions,
491            action_broadcast,
492            _state: std::marker::PhantomData,
493            _effect: std::marker::PhantomData,
494        }
495    }
496
497    /// Attach a debug layer (auto-wires tasks/subscriptions when available).
498    #[cfg(feature = "debug")]
499    pub fn with_debug<D>(mut self, debug: D) -> Self
500    where
501        D: DebugAdapter<S, A> + DebugHooks<A>,
502    {
503        let debug = {
504            let debug = debug;
505            #[cfg(feature = "tasks")]
506            let debug = debug.with_task_manager(&self.tasks);
507            #[cfg(feature = "subscriptions")]
508            let debug = debug.with_subscriptions(&self.subscriptions);
509            debug
510        };
511        self.debug = Some(Box::new(debug));
512        self
513    }
514
515    /// Configure event polling behavior.
516    pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
517        self.poller_config = config;
518        self
519    }
520
521    /// Subscribe to action name broadcasts.
522    ///
523    /// Returns a receiver that will receive action names (from `action.name()`)
524    /// whenever an action is dispatched. Useful for replay await functionality.
525    pub fn subscribe_actions(&self) -> tokio::sync::broadcast::Receiver<String> {
526        self.action_broadcast.subscribe()
527    }
528
529    /// Send an action into the runtime queue.
530    pub fn enqueue(&self, action: A) {
531        let _ = self.action_tx.send(action);
532    }
533
534    /// Clone the action sender.
535    pub fn action_tx(&self) -> mpsc::UnboundedSender<A> {
536        self.action_tx.clone()
537    }
538
539    /// Access the current state.
540    pub fn state(&self) -> &S {
541        self.store.state()
542    }
543
544    /// Access the task manager.
545    #[cfg(feature = "tasks")]
546    pub fn tasks(&mut self) -> &mut TaskManager<A> {
547        &mut self.tasks
548    }
549
550    /// Access subscriptions.
551    #[cfg(feature = "subscriptions")]
552    pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
553        &mut self.subscriptions
554    }
555
556    #[cfg(all(feature = "tasks", feature = "subscriptions"))]
557    fn effect_context(&mut self) -> EffectContext<'_, A> {
558        EffectContext {
559            action_tx: &self.action_tx,
560            tasks: &mut self.tasks,
561            subscriptions: &mut self.subscriptions,
562        }
563    }
564
565    #[cfg(all(feature = "tasks", not(feature = "subscriptions")))]
566    fn effect_context(&mut self) -> EffectContext<'_, A> {
567        EffectContext {
568            action_tx: &self.action_tx,
569            tasks: &mut self.tasks,
570        }
571    }
572
573    #[cfg(all(not(feature = "tasks"), feature = "subscriptions"))]
574    fn effect_context(&mut self) -> EffectContext<'_, A> {
575        EffectContext {
576            action_tx: &self.action_tx,
577            subscriptions: &mut self.subscriptions,
578        }
579    }
580
581    #[cfg(all(not(feature = "tasks"), not(feature = "subscriptions")))]
582    fn effect_context(&mut self) -> EffectContext<'_, A> {
583        EffectContext {
584            action_tx: &self.action_tx,
585        }
586    }
587
588    fn render_ctx(&self) -> RenderContext {
589        RenderContext {
590            debug_enabled: {
591                #[cfg(feature = "debug")]
592                {
593                    self.debug.as_ref().is_some_and(|d| d.is_enabled())
594                }
595                #[cfg(not(feature = "debug"))]
596                {
597                    false
598                }
599            },
600        }
601    }
602
603    #[allow(unused_variables)]
604    fn debug_intercept_event(&mut self, event: &EventKind) -> Option<bool> {
605        #[cfg(feature = "debug")]
606        if let Some(debug) = self.debug.as_mut() {
607            return debug.handle_event(event, self.store.state(), &self.action_tx);
608        }
609        None
610    }
611
612    #[allow(unused_variables)]
613    fn debug_log_action(&mut self, action: &A) {
614        #[cfg(feature = "debug")]
615        if let Some(debug) = self.debug.as_mut() {
616            debug.log_action(action);
617        }
618    }
619
620    fn enqueue_outcome(&mut self, outcome: EventOutcome<A>) {
621        if outcome.needs_render {
622            self.should_render = true;
623        }
624        for action in outcome.actions {
625            let _ = self.action_tx.send(action);
626        }
627    }
628
629    fn broadcast_action(&self, action: &A) {
630        if self.action_broadcast.receiver_count() > 0 {
631            let _ = self.action_broadcast.send(action.name().to_string());
632        }
633    }
634
635    fn spawn_poller(&self) -> (mpsc::UnboundedReceiver<RawEvent>, CancellationToken) {
636        let (event_tx, event_rx) = mpsc::unbounded_channel::<RawEvent>();
637        let cancel_token = CancellationToken::new();
638        let _handle = spawn_event_poller(
639            event_tx,
640            self.poller_config.poll_timeout,
641            self.poller_config.loop_sleep,
642            cancel_token.clone(),
643        );
644        (event_rx, cancel_token)
645    }
646
647    fn cleanup(&mut self, cancel_token: CancellationToken) {
648        cancel_token.cancel();
649        #[cfg(feature = "subscriptions")]
650        self.subscriptions.cancel_all();
651        #[cfg(feature = "tasks")]
652        self.tasks.cancel_all();
653    }
654
655    fn dispatch_and_handle_effects(
656        &mut self,
657        action: A,
658        handle_effect: &mut impl FnMut(E, &mut EffectContext<A>),
659    ) {
660        self.broadcast_action(&action);
661        let result = self.store.dispatch(action);
662        if result.has_effects() {
663            let mut ctx = self.effect_context();
664            for effect in result.effects {
665                handle_effect(effect, &mut ctx);
666            }
667        }
668        self.should_render = result.changed;
669    }
670
671    /// Run the event/action loop until quit.
672    pub async fn run<B, FRender, FEvent, FQuit, FEffect, R>(
673        &mut self,
674        terminal: &mut Terminal<B>,
675        mut render: FRender,
676        mut map_event: FEvent,
677        mut should_quit: FQuit,
678        mut handle_effect: FEffect,
679    ) -> io::Result<()>
680    where
681        B: Backend,
682        FRender: FnMut(&mut Frame, Rect, &S, RenderContext),
683        FEvent: FnMut(&EventKind, &S) -> R,
684        R: Into<EventOutcome<A>>,
685        FQuit: FnMut(&A) -> bool,
686        FEffect: FnMut(E, &mut EffectContext<A>),
687    {
688        let (mut event_rx, cancel_token) = self.spawn_poller();
689
690        loop {
691            if self.should_render {
692                draw_frame!(self, terminal, render);
693            }
694
695            tokio::select! {
696                Some(raw_event) = event_rx.recv() => {
697                    let event = process_raw_event(raw_event);
698                    if let Some(needs_render) = self.debug_intercept_event(&event) {
699                        self.should_render = needs_render;
700                        continue;
701                    }
702                    let outcome: EventOutcome<A> = map_event(&event, self.store.state()).into();
703                    self.enqueue_outcome(outcome);
704                }
705
706                Some(action) = self.action_rx.recv() => {
707                    if should_quit(&action) {
708                        break;
709                    }
710                    self.debug_log_action(&action);
711                    self.dispatch_and_handle_effects(action, &mut handle_effect);
712                }
713
714                else => { break; }
715            }
716        }
717
718        self.cleanup(cancel_token);
719        Ok(())
720    }
721
722    /// Run the event/action loop using an EventBus for routing.
723    pub async fn run_with_bus<B, FRender, FQuit, FEffect, Id, Ctx>(
724        &mut self,
725        terminal: &mut Terminal<B>,
726        bus: &mut EventBus<S, A, Id, Ctx>,
727        keybindings: &Keybindings<Ctx>,
728        mut render: FRender,
729        mut should_quit: FQuit,
730        mut handle_effect: FEffect,
731    ) -> io::Result<()>
732    where
733        B: Backend,
734        Id: ComponentId + 'static,
735        Ctx: BindingContext + 'static,
736        S: EventRoutingState<Id, Ctx>,
737        FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
738        FQuit: FnMut(&A) -> bool,
739        FEffect: FnMut(E, &mut EffectContext<A>),
740    {
741        let (mut event_rx, cancel_token) = self.spawn_poller();
742
743        loop {
744            if self.should_render {
745                draw_frame!(self, terminal, |f, area, s, ctx| render(
746                    f,
747                    area,
748                    s,
749                    ctx,
750                    bus.context_mut()
751                ));
752            }
753
754            tokio::select! {
755                Some(raw_event) = event_rx.recv() => {
756                    let event = process_raw_event(raw_event);
757                    if let Some(needs_render) = self.debug_intercept_event(&event) {
758                        self.should_render = needs_render;
759                        continue;
760                    }
761                    let outcome = bus.handle_event(&event, self.store.state(), keybindings);
762                    self.enqueue_outcome(outcome);
763                }
764
765                Some(action) = self.action_rx.recv() => {
766                    if should_quit(&action) {
767                        break;
768                    }
769                    self.debug_log_action(&action);
770                    self.dispatch_and_handle_effects(action, &mut handle_effect);
771                }
772
773                else => { break; }
774            }
775        }
776
777        self.cleanup(cancel_token);
778        Ok(())
779    }
780}