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::{process_raw_event, spawn_event_poller, EventBus, EventRoutingState, RawEvent};
16use crate::effect::{DispatchResult, EffectStore, EffectStoreWithMiddleware};
17use crate::event::{ComponentId, EventContext, EventKind};
18use crate::keybindings::Keybindings;
19use crate::store::{Middleware, Reducer, Store, StoreWithMiddleware};
20use crate::{Action, BindingContext};
21
22#[cfg(feature = "subscriptions")]
23use crate::subscriptions::Subscriptions;
24#[cfg(feature = "tasks")]
25use crate::tasks::TaskManager;
26
27/// Configuration for the event poller.
28#[derive(Debug, Clone, Copy)]
29pub struct PollerConfig {
30    /// Timeout passed to each `crossterm::event::poll` call.
31    pub poll_timeout: Duration,
32    /// Sleep between poll cycles.
33    pub loop_sleep: Duration,
34}
35
36impl Default for PollerConfig {
37    fn default() -> Self {
38        Self {
39            poll_timeout: Duration::from_millis(10),
40            loop_sleep: Duration::from_millis(16),
41        }
42    }
43}
44
45/// Result of mapping an event into actions plus an optional render hint.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct EventOutcome<A> {
48    /// Actions to enqueue.
49    pub actions: Vec<A>,
50    /// Whether to force a re-render.
51    pub needs_render: bool,
52}
53
54/// Context passed to render closures.
55#[derive(Debug, Clone, Copy, Default)]
56pub struct RenderContext {
57    /// Whether the debug overlay is currently active.
58    pub debug_enabled: bool,
59}
60
61impl RenderContext {
62    /// Whether the app should treat input focus as active.
63    pub fn is_focused(self) -> bool {
64        !self.debug_enabled
65    }
66}
67
68impl<A> EventOutcome<A> {
69    /// No actions and no render.
70    pub fn ignored() -> Self {
71        Self {
72            actions: Vec::new(),
73            needs_render: false,
74        }
75    }
76
77    /// No actions, but request a render.
78    pub fn needs_render() -> Self {
79        Self {
80            actions: Vec::new(),
81            needs_render: true,
82        }
83    }
84
85    /// Wrap a single action.
86    pub fn action(action: A) -> Self {
87        Self {
88            actions: vec![action],
89            needs_render: false,
90        }
91    }
92
93    /// Wrap multiple actions.
94    pub fn actions<I>(actions: I) -> Self
95    where
96        I: IntoIterator<Item = A>,
97    {
98        Self {
99            actions: actions.into_iter().collect(),
100            needs_render: false,
101        }
102    }
103
104    /// Mark that a render is needed.
105    pub fn with_render(mut self) -> Self {
106        self.needs_render = true;
107        self
108    }
109}
110
111impl<A> Default for EventOutcome<A> {
112    fn default() -> Self {
113        Self::ignored()
114    }
115}
116
117impl<A> From<A> for EventOutcome<A> {
118    fn from(action: A) -> Self {
119        Self::action(action)
120    }
121}
122
123impl<A> From<Vec<A>> for EventOutcome<A> {
124    fn from(actions: Vec<A>) -> Self {
125        Self {
126            actions,
127            needs_render: false,
128        }
129    }
130}
131
132impl<A> From<Option<A>> for EventOutcome<A> {
133    fn from(action: Option<A>) -> Self {
134        match action {
135            Some(action) => Self::action(action),
136            None => Self::ignored(),
137        }
138    }
139}
140
141impl<A> EventOutcome<A> {
142    /// Create from any iterator of actions
143    ///
144    /// Useful for converting `Component::handle_event` results which return
145    /// `impl IntoIterator<Item = A>`.
146    pub fn from_actions(iter: impl IntoIterator<Item = A>) -> Self {
147        Self {
148            actions: iter.into_iter().collect(),
149            needs_render: false,
150        }
151    }
152}
153
154#[cfg(feature = "debug")]
155pub trait DebugAdapter<S, A>: 'static {
156    fn render(
157        &mut self,
158        frame: &mut Frame,
159        state: &S,
160        render_ctx: RenderContext,
161        render_fn: &mut dyn FnMut(&mut Frame, Rect, &S, RenderContext),
162    );
163
164    fn handle_event(
165        &mut self,
166        event: &EventKind,
167        state: &S,
168        action_tx: &mpsc::UnboundedSender<A>,
169    ) -> Option<bool>;
170
171    fn log_action(&mut self, action: &A);
172    fn is_enabled(&self) -> bool;
173}
174
175#[cfg(feature = "debug")]
176pub trait DebugHooks<A>: Sized {
177    #[cfg(feature = "tasks")]
178    fn with_task_manager(self, _tasks: &TaskManager<A>) -> Self {
179        self
180    }
181
182    #[cfg(feature = "subscriptions")]
183    fn with_subscriptions(self, _subscriptions: &Subscriptions<A>) -> Self {
184        self
185    }
186}
187
188/// Store interface used by `DispatchRuntime`.
189pub trait DispatchStore<S, A: Action> {
190    /// Dispatch an action and return whether the state changed.
191    fn dispatch(&mut self, action: A) -> bool;
192    /// Get the current state.
193    fn state(&self) -> &S;
194}
195
196impl<S, A: Action> DispatchStore<S, A> for Store<S, A> {
197    fn dispatch(&mut self, action: A) -> bool {
198        Store::dispatch(self, action)
199    }
200
201    fn state(&self) -> &S {
202        Store::state(self)
203    }
204}
205
206impl<S, A: Action, M: Middleware<A>> DispatchStore<S, A> for StoreWithMiddleware<S, A, M> {
207    fn dispatch(&mut self, action: A) -> bool {
208        StoreWithMiddleware::dispatch(self, action)
209    }
210
211    fn state(&self) -> &S {
212        StoreWithMiddleware::state(self)
213    }
214}
215
216/// Effect store interface used by `EffectRuntime`.
217pub trait EffectStoreLike<S, A: Action, E> {
218    /// Dispatch an action and return state changes plus effects.
219    fn dispatch(&mut self, action: A) -> DispatchResult<E>;
220    /// Get the current state.
221    fn state(&self) -> &S;
222}
223
224impl<S, A: Action, E> EffectStoreLike<S, A, E> for EffectStore<S, A, E> {
225    fn dispatch(&mut self, action: A) -> DispatchResult<E> {
226        EffectStore::dispatch(self, action)
227    }
228
229    fn state(&self) -> &S {
230        EffectStore::state(self)
231    }
232}
233
234impl<S, A: Action, E, M: Middleware<A>> EffectStoreLike<S, A, E>
235    for EffectStoreWithMiddleware<S, A, E, M>
236{
237    fn dispatch(&mut self, action: A) -> DispatchResult<E> {
238        EffectStoreWithMiddleware::dispatch(self, action)
239    }
240
241    fn state(&self) -> &S {
242        EffectStoreWithMiddleware::state(self)
243    }
244}
245
246/// Runtime helper for simple stores (no effects).
247pub struct DispatchRuntime<S, A: Action, St: DispatchStore<S, A> = Store<S, A>> {
248    store: St,
249    action_tx: mpsc::UnboundedSender<A>,
250    action_rx: mpsc::UnboundedReceiver<A>,
251    poller_config: PollerConfig,
252    #[cfg(feature = "debug")]
253    debug: Option<Box<dyn DebugAdapter<S, A>>>,
254    should_render: bool,
255    _state: std::marker::PhantomData<S>,
256}
257
258impl<S: 'static, A: Action> DispatchRuntime<S, A, Store<S, A>> {
259    /// Create a runtime from state + reducer.
260    pub fn new(state: S, reducer: Reducer<S, A>) -> Self {
261        Self::from_store(Store::new(state, reducer))
262    }
263}
264
265impl<S: 'static, A: Action, St: DispatchStore<S, A>> DispatchRuntime<S, A, St> {
266    /// Create a runtime from an existing store.
267    pub fn from_store(store: St) -> Self {
268        let (action_tx, action_rx) = mpsc::unbounded_channel();
269        Self {
270            store,
271            action_tx,
272            action_rx,
273            poller_config: PollerConfig::default(),
274            #[cfg(feature = "debug")]
275            debug: None,
276            should_render: true,
277            _state: std::marker::PhantomData,
278        }
279    }
280
281    /// Attach a debug layer.
282    #[cfg(feature = "debug")]
283    pub fn with_debug<D>(mut self, debug: D) -> Self
284    where
285        D: DebugAdapter<S, A>,
286    {
287        self.debug = Some(Box::new(debug));
288        self
289    }
290
291    /// Configure event polling behavior.
292    pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
293        self.poller_config = config;
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    /// Run the event/action loop until quit.
313    pub async fn run<B, FRender, FEvent, FQuit, R>(
314        &mut self,
315        terminal: &mut Terminal<B>,
316        mut render: FRender,
317        mut map_event: FEvent,
318        mut should_quit: FQuit,
319    ) -> io::Result<()>
320    where
321        B: Backend,
322        FRender: FnMut(&mut Frame, Rect, &S, RenderContext),
323        FEvent: FnMut(&EventKind, &S) -> R,
324        R: Into<EventOutcome<A>>,
325        FQuit: FnMut(&A) -> bool,
326    {
327        let (event_tx, mut event_rx) = mpsc::unbounded_channel::<RawEvent>();
328        let cancel_token = CancellationToken::new();
329        let _handle = spawn_event_poller(
330            event_tx,
331            self.poller_config.poll_timeout,
332            self.poller_config.loop_sleep,
333            cancel_token.clone(),
334        );
335
336        loop {
337            if self.should_render {
338                let state = self.store.state();
339                let render_ctx = RenderContext {
340                    debug_enabled: {
341                        #[cfg(feature = "debug")]
342                        {
343                            self.debug
344                                .as_ref()
345                                .map(|debug| debug.is_enabled())
346                                .unwrap_or(false)
347                        }
348                        #[cfg(not(feature = "debug"))]
349                        {
350                            false
351                        }
352                    },
353                };
354                terminal.draw(|frame| {
355                    #[cfg(feature = "debug")]
356                    if let Some(debug) = self.debug.as_mut() {
357                        let mut render_fn =
358                            |f: &mut Frame, area: Rect, state: &S, ctx: RenderContext| {
359                                render(f, area, state, ctx);
360                            };
361                        debug.render(frame, state, render_ctx, &mut render_fn);
362                    } else {
363                        render(frame, frame.area(), state, render_ctx);
364                    }
365
366                    #[cfg(not(feature = "debug"))]
367                    {
368                        render(frame, frame.area(), state, render_ctx);
369                    }
370                })?;
371                self.should_render = false;
372            }
373
374            tokio::select! {
375                Some(raw_event) = event_rx.recv() => {
376                    let event = process_raw_event(raw_event);
377
378                    #[cfg(feature = "debug")]
379                    if let Some(debug) = self.debug.as_mut() {
380                        if let Some(needs_render) =
381                            debug.handle_event(&event, self.store.state(), &self.action_tx)
382                        {
383                            self.should_render = needs_render;
384                            continue;
385                        }
386                    }
387
388                    let outcome: EventOutcome<A> = map_event(&event, self.store.state()).into();
389                    if outcome.needs_render {
390                        self.should_render = true;
391                    }
392                    for action in outcome.actions {
393                        let _ = self.action_tx.send(action);
394                    }
395                }
396
397                Some(action) = self.action_rx.recv() => {
398                    if should_quit(&action) {
399                        break;
400                    }
401
402                    #[cfg(feature = "debug")]
403                    if let Some(debug) = self.debug.as_mut() {
404                        debug.log_action(&action);
405                    }
406
407                    self.should_render = self.store.dispatch(action);
408                }
409
410                else => {
411                    break;
412                }
413            }
414        }
415
416        cancel_token.cancel();
417        Ok(())
418    }
419
420    /// Run the event/action loop using an EventBus for routing.
421    pub async fn run_with_bus<B, FRender, FQuit, Id, Ctx>(
422        &mut self,
423        terminal: &mut Terminal<B>,
424        bus: &mut EventBus<S, A, Id, Ctx>,
425        keybindings: &Keybindings<Ctx>,
426        mut render: FRender,
427        mut should_quit: FQuit,
428    ) -> io::Result<()>
429    where
430        B: Backend,
431        Id: ComponentId + 'static,
432        Ctx: BindingContext + 'static,
433        S: EventRoutingState<Id, Ctx>,
434        FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
435        FQuit: FnMut(&A) -> bool,
436    {
437        let (event_tx, mut event_rx) = mpsc::unbounded_channel::<RawEvent>();
438        let cancel_token = CancellationToken::new();
439        let _handle = spawn_event_poller(
440            event_tx,
441            self.poller_config.poll_timeout,
442            self.poller_config.loop_sleep,
443            cancel_token.clone(),
444        );
445
446        loop {
447            if self.should_render {
448                let state = self.store.state();
449                let render_ctx = RenderContext {
450                    debug_enabled: {
451                        #[cfg(feature = "debug")]
452                        {
453                            self.debug
454                                .as_ref()
455                                .map(|debug| debug.is_enabled())
456                                .unwrap_or(false)
457                        }
458                        #[cfg(not(feature = "debug"))]
459                        {
460                            false
461                        }
462                    },
463                };
464                terminal.draw(|frame| {
465                    #[cfg(feature = "debug")]
466                    if let Some(debug) = self.debug.as_mut() {
467                        let mut render_fn =
468                            |f: &mut Frame, area: Rect, state: &S, ctx: RenderContext| {
469                                render(f, area, state, ctx, bus.context_mut());
470                            };
471                        debug.render(frame, state, render_ctx, &mut render_fn);
472                    } else {
473                        render(frame, frame.area(), state, render_ctx, bus.context_mut());
474                    }
475
476                    #[cfg(not(feature = "debug"))]
477                    {
478                        render(frame, frame.area(), state, render_ctx, bus.context_mut());
479                    }
480                })?;
481                self.should_render = false;
482            }
483
484            tokio::select! {
485                Some(raw_event) = event_rx.recv() => {
486                    let event = process_raw_event(raw_event);
487
488                    #[cfg(feature = "debug")]
489                    if let Some(debug) = self.debug.as_mut() {
490                        if let Some(needs_render) =
491                            debug.handle_event(&event, self.store.state(), &self.action_tx)
492                        {
493                            self.should_render = needs_render;
494                            continue;
495                        }
496                    }
497
498                    let outcome = bus.handle_event(&event, self.store.state(), keybindings);
499                    if outcome.needs_render {
500                        self.should_render = true;
501                    }
502                    for action in outcome.actions {
503                        let _ = self.action_tx.send(action);
504                    }
505                }
506
507                Some(action) = self.action_rx.recv() => {
508                    if should_quit(&action) {
509                        break;
510                    }
511
512                    #[cfg(feature = "debug")]
513                    if let Some(debug) = self.debug.as_mut() {
514                        debug.log_action(&action);
515                    }
516
517                    self.should_render = self.store.dispatch(action);
518                }
519
520                else => {
521                    break;
522                }
523            }
524        }
525
526        cancel_token.cancel();
527        Ok(())
528    }
529}
530
531/// Context passed to effect handlers.
532pub struct EffectContext<'a, A: Action> {
533    action_tx: &'a mpsc::UnboundedSender<A>,
534    #[cfg(feature = "tasks")]
535    tasks: &'a mut TaskManager<A>,
536    #[cfg(feature = "subscriptions")]
537    subscriptions: &'a mut Subscriptions<A>,
538}
539
540impl<'a, A: Action> EffectContext<'a, A> {
541    /// Send an action directly.
542    pub fn emit(&self, action: A) {
543        let _ = self.action_tx.send(action);
544    }
545
546    /// Access the action sender.
547    pub fn action_tx(&self) -> &mpsc::UnboundedSender<A> {
548        self.action_tx
549    }
550
551    /// Access the task manager.
552    #[cfg(feature = "tasks")]
553    pub fn tasks(&mut self) -> &mut TaskManager<A> {
554        self.tasks
555    }
556
557    /// Access subscriptions.
558    #[cfg(feature = "subscriptions")]
559    pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
560        self.subscriptions
561    }
562}
563
564/// Runtime helper for effect-based stores.
565pub struct EffectRuntime<S, A: Action, E, St: EffectStoreLike<S, A, E> = EffectStore<S, A, E>> {
566    store: St,
567    action_tx: mpsc::UnboundedSender<A>,
568    action_rx: mpsc::UnboundedReceiver<A>,
569    poller_config: PollerConfig,
570    #[cfg(feature = "debug")]
571    debug: Option<Box<dyn DebugAdapter<S, A>>>,
572    should_render: bool,
573    #[cfg(feature = "tasks")]
574    tasks: TaskManager<A>,
575    #[cfg(feature = "subscriptions")]
576    subscriptions: Subscriptions<A>,
577    /// Broadcasts action names when dispatched (for replay await functionality).
578    action_broadcast: tokio::sync::broadcast::Sender<String>,
579    _state: std::marker::PhantomData<S>,
580    _effect: std::marker::PhantomData<E>,
581}
582
583impl<S: 'static, A: Action, E> EffectRuntime<S, A, E, EffectStore<S, A, E>> {
584    /// Create a runtime from state + effect reducer.
585    pub fn new(state: S, reducer: crate::effect::EffectReducer<S, A, E>) -> Self {
586        Self::from_store(EffectStore::new(state, reducer))
587    }
588}
589
590impl<S: 'static, A: Action, E, St: EffectStoreLike<S, A, E>> EffectRuntime<S, A, E, St> {
591    /// Create a runtime from an existing effect store.
592    pub fn from_store(store: St) -> Self {
593        let (action_tx, action_rx) = mpsc::unbounded_channel();
594        let (action_broadcast, _) = tokio::sync::broadcast::channel(64);
595
596        #[cfg(feature = "tasks")]
597        let tasks = TaskManager::new(action_tx.clone());
598        #[cfg(feature = "subscriptions")]
599        let subscriptions = Subscriptions::new(action_tx.clone());
600
601        Self {
602            store,
603            action_tx,
604            action_rx,
605            poller_config: PollerConfig::default(),
606            #[cfg(feature = "debug")]
607            debug: None,
608            should_render: true,
609            #[cfg(feature = "tasks")]
610            tasks,
611            #[cfg(feature = "subscriptions")]
612            subscriptions,
613            action_broadcast,
614            _state: std::marker::PhantomData,
615            _effect: std::marker::PhantomData,
616        }
617    }
618
619    /// Attach a debug layer (auto-wires tasks/subscriptions when available).
620    #[cfg(feature = "debug")]
621    pub fn with_debug<D>(mut self, debug: D) -> Self
622    where
623        D: DebugAdapter<S, A> + DebugHooks<A>,
624    {
625        let debug = {
626            let debug = debug;
627            #[cfg(feature = "tasks")]
628            let debug = debug.with_task_manager(&self.tasks);
629            #[cfg(feature = "subscriptions")]
630            let debug = debug.with_subscriptions(&self.subscriptions);
631            debug
632        };
633        self.debug = Some(Box::new(debug));
634        self
635    }
636
637    /// Configure event polling behavior.
638    pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
639        self.poller_config = config;
640        self
641    }
642
643    /// Subscribe to action name broadcasts.
644    ///
645    /// Returns a receiver that will receive action names (from `action.name()`)
646    /// whenever an action is dispatched. Useful for replay await functionality.
647    pub fn subscribe_actions(&self) -> tokio::sync::broadcast::Receiver<String> {
648        self.action_broadcast.subscribe()
649    }
650
651    /// Send an action into the runtime queue.
652    pub fn enqueue(&self, action: A) {
653        let _ = self.action_tx.send(action);
654    }
655
656    /// Clone the action sender.
657    pub fn action_tx(&self) -> mpsc::UnboundedSender<A> {
658        self.action_tx.clone()
659    }
660
661    /// Access the current state.
662    pub fn state(&self) -> &S {
663        self.store.state()
664    }
665
666    /// Access the task manager.
667    #[cfg(feature = "tasks")]
668    pub fn tasks(&mut self) -> &mut TaskManager<A> {
669        &mut self.tasks
670    }
671
672    /// Access subscriptions.
673    #[cfg(feature = "subscriptions")]
674    pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
675        &mut self.subscriptions
676    }
677
678    #[cfg(all(feature = "tasks", feature = "subscriptions"))]
679    fn effect_context(&mut self) -> EffectContext<'_, A> {
680        EffectContext {
681            action_tx: &self.action_tx,
682            tasks: &mut self.tasks,
683            subscriptions: &mut self.subscriptions,
684        }
685    }
686
687    #[cfg(all(feature = "tasks", not(feature = "subscriptions")))]
688    fn effect_context(&mut self) -> EffectContext<'_, A> {
689        EffectContext {
690            action_tx: &self.action_tx,
691            tasks: &mut self.tasks,
692        }
693    }
694
695    #[cfg(all(not(feature = "tasks"), feature = "subscriptions"))]
696    fn effect_context(&mut self) -> EffectContext<'_, A> {
697        EffectContext {
698            action_tx: &self.action_tx,
699            subscriptions: &mut self.subscriptions,
700        }
701    }
702
703    #[cfg(all(not(feature = "tasks"), not(feature = "subscriptions")))]
704    fn effect_context(&mut self) -> EffectContext<'_, A> {
705        EffectContext {
706            action_tx: &self.action_tx,
707        }
708    }
709
710    /// Run the event/action loop until quit.
711    pub async fn run<B, FRender, FEvent, FQuit, FEffect, R>(
712        &mut self,
713        terminal: &mut Terminal<B>,
714        mut render: FRender,
715        mut map_event: FEvent,
716        mut should_quit: FQuit,
717        mut handle_effect: FEffect,
718    ) -> io::Result<()>
719    where
720        B: Backend,
721        FRender: FnMut(&mut Frame, Rect, &S, RenderContext),
722        FEvent: FnMut(&EventKind, &S) -> R,
723        R: Into<EventOutcome<A>>,
724        FQuit: FnMut(&A) -> bool,
725        FEffect: FnMut(E, &mut EffectContext<A>),
726    {
727        let (event_tx, mut event_rx) = mpsc::unbounded_channel::<RawEvent>();
728        let cancel_token = CancellationToken::new();
729        let _handle = spawn_event_poller(
730            event_tx,
731            self.poller_config.poll_timeout,
732            self.poller_config.loop_sleep,
733            cancel_token.clone(),
734        );
735
736        loop {
737            if self.should_render {
738                let state = self.store.state();
739                let render_ctx = RenderContext {
740                    debug_enabled: {
741                        #[cfg(feature = "debug")]
742                        {
743                            self.debug
744                                .as_ref()
745                                .map(|debug| debug.is_enabled())
746                                .unwrap_or(false)
747                        }
748                        #[cfg(not(feature = "debug"))]
749                        {
750                            false
751                        }
752                    },
753                };
754                terminal.draw(|frame| {
755                    #[cfg(feature = "debug")]
756                    if let Some(debug) = self.debug.as_mut() {
757                        let mut render_fn =
758                            |f: &mut Frame, area: Rect, state: &S, ctx: RenderContext| {
759                                render(f, area, state, ctx);
760                            };
761                        debug.render(frame, state, render_ctx, &mut render_fn);
762                    } else {
763                        render(frame, frame.area(), state, render_ctx);
764                    }
765
766                    #[cfg(not(feature = "debug"))]
767                    {
768                        render(frame, frame.area(), state, render_ctx);
769                    }
770                })?;
771                self.should_render = false;
772            }
773
774            tokio::select! {
775                Some(raw_event) = event_rx.recv() => {
776                    let event = process_raw_event(raw_event);
777
778                    #[cfg(feature = "debug")]
779                    if let Some(debug) = self.debug.as_mut() {
780                        if let Some(needs_render) =
781                            debug.handle_event(&event, self.store.state(), &self.action_tx)
782                        {
783                            self.should_render = needs_render;
784                            continue;
785                        }
786                    }
787
788                    let outcome: EventOutcome<A> = map_event(&event, self.store.state()).into();
789                    if outcome.needs_render {
790                        self.should_render = true;
791                    }
792                    for action in outcome.actions {
793                        let _ = self.action_tx.send(action);
794                    }
795                }
796
797                Some(action) = self.action_rx.recv() => {
798                    if should_quit(&action) {
799                        break;
800                    }
801
802                    #[cfg(feature = "debug")]
803                    if let Some(debug) = self.debug.as_mut() {
804                        debug.log_action(&action);
805                    }
806
807                    // Broadcast action name for replay await functionality
808                    let _ = self.action_broadcast.send(action.name().to_string());
809
810                    let result = self.store.dispatch(action);
811                    if result.has_effects() {
812                        let mut ctx = self.effect_context();
813                        for effect in result.effects {
814                            handle_effect(effect, &mut ctx);
815                        }
816                    }
817                    self.should_render = result.changed;
818                }
819
820                else => {
821                    break;
822                }
823            }
824        }
825
826        cancel_token.cancel();
827        #[cfg(feature = "subscriptions")]
828        self.subscriptions.cancel_all();
829        #[cfg(feature = "tasks")]
830        self.tasks.cancel_all();
831
832        Ok(())
833    }
834
835    /// Run the event/action loop using an EventBus for routing.
836    pub async fn run_with_bus<B, FRender, FQuit, FEffect, Id, Ctx>(
837        &mut self,
838        terminal: &mut Terminal<B>,
839        bus: &mut EventBus<S, A, Id, Ctx>,
840        keybindings: &Keybindings<Ctx>,
841        mut render: FRender,
842        mut should_quit: FQuit,
843        mut handle_effect: FEffect,
844    ) -> io::Result<()>
845    where
846        B: Backend,
847        Id: ComponentId + 'static,
848        Ctx: BindingContext + 'static,
849        S: EventRoutingState<Id, Ctx>,
850        FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
851        FQuit: FnMut(&A) -> bool,
852        FEffect: FnMut(E, &mut EffectContext<A>),
853    {
854        let (event_tx, mut event_rx) = mpsc::unbounded_channel::<RawEvent>();
855        let cancel_token = CancellationToken::new();
856        let _handle = spawn_event_poller(
857            event_tx,
858            self.poller_config.poll_timeout,
859            self.poller_config.loop_sleep,
860            cancel_token.clone(),
861        );
862
863        loop {
864            if self.should_render {
865                let state = self.store.state();
866                let render_ctx = RenderContext {
867                    debug_enabled: {
868                        #[cfg(feature = "debug")]
869                        {
870                            self.debug
871                                .as_ref()
872                                .map(|debug| debug.is_enabled())
873                                .unwrap_or(false)
874                        }
875                        #[cfg(not(feature = "debug"))]
876                        {
877                            false
878                        }
879                    },
880                };
881                terminal.draw(|frame| {
882                    #[cfg(feature = "debug")]
883                    if let Some(debug) = self.debug.as_mut() {
884                        let mut render_fn =
885                            |f: &mut Frame, area: Rect, state: &S, ctx: RenderContext| {
886                                render(f, area, state, ctx, bus.context_mut());
887                            };
888                        debug.render(frame, state, render_ctx, &mut render_fn);
889                    } else {
890                        render(frame, frame.area(), state, render_ctx, bus.context_mut());
891                    }
892
893                    #[cfg(not(feature = "debug"))]
894                    {
895                        render(frame, frame.area(), state, render_ctx, bus.context_mut());
896                    }
897                })?;
898                self.should_render = false;
899            }
900
901            tokio::select! {
902                Some(raw_event) = event_rx.recv() => {
903                    let event = process_raw_event(raw_event);
904
905                    #[cfg(feature = "debug")]
906                    if let Some(debug) = self.debug.as_mut() {
907                        if let Some(needs_render) =
908                            debug.handle_event(&event, self.store.state(), &self.action_tx)
909                        {
910                            self.should_render = needs_render;
911                            continue;
912                        }
913                    }
914
915                    let outcome = bus.handle_event(&event, self.store.state(), keybindings);
916                    if outcome.needs_render {
917                        self.should_render = true;
918                    }
919                    for action in outcome.actions {
920                        let _ = self.action_tx.send(action);
921                    }
922                }
923
924                Some(action) = self.action_rx.recv() => {
925                    if should_quit(&action) {
926                        break;
927                    }
928
929                    #[cfg(feature = "debug")]
930                    if let Some(debug) = self.debug.as_mut() {
931                        debug.log_action(&action);
932                    }
933
934                    // Broadcast action name for replay await functionality
935                    let _ = self.action_broadcast.send(action.name().to_string());
936
937                    let result = self.store.dispatch(action);
938                    if result.has_effects() {
939                        let mut ctx = self.effect_context();
940                        for effect in result.effects {
941                            handle_effect(effect, &mut ctx);
942                        }
943                    }
944                    self.should_render = result.changed;
945                }
946
947                else => {
948                    break;
949                }
950            }
951        }
952
953        cancel_token.cancel();
954        #[cfg(feature = "subscriptions")]
955        self.subscriptions.cancel_all();
956        #[cfg(feature = "tasks")]
957        self.tasks.cancel_all();
958
959        Ok(())
960    }
961}