1use 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#[derive(Debug, Clone, Copy)]
31pub struct PollerConfig {
32 pub poll_timeout: Duration,
34 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#[derive(Debug, Clone, Copy, Default)]
49pub struct RenderContext {
50 pub debug_enabled: bool,
52}
53
54impl RenderContext {
55 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
95macro_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
124pub trait DispatchStore<S, A: Action> {
126 fn dispatch(&mut self, action: A) -> bool;
128 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
152pub trait EffectStoreLike<S, A: Action, E> {
154 fn dispatch(&mut self, action: A) -> ReducerResult<E>;
156 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
182pub 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 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 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 #[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 pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
229 self.poller_config = config;
230 self
231 }
232
233 pub fn enqueue(&self, action: A) {
235 let _ = self.action_tx.send(action);
236 }
237
238 pub fn action_tx(&self) -> mpsc::UnboundedSender<A> {
240 self.action_tx.clone()
241 }
242
243 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 #[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 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 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
409pub 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 pub fn emit(&self, action: A) {
421 let _ = self.action_tx.send(action);
422 }
423
424 pub fn action_tx(&self) -> &mpsc::UnboundedSender<A> {
426 self.action_tx
427 }
428
429 #[cfg(feature = "tasks")]
431 pub fn tasks(&mut self) -> &mut TaskManager<A> {
432 self.tasks
433 }
434
435 #[cfg(feature = "subscriptions")]
437 pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
438 self.subscriptions
439 }
440}
441
442pub 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 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 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 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 #[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 pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
517 self.poller_config = config;
518 self
519 }
520
521 pub fn subscribe_actions(&self) -> tokio::sync::broadcast::Receiver<String> {
526 self.action_broadcast.subscribe()
527 }
528
529 pub fn enqueue(&self, action: A) {
531 let _ = self.action_tx.send(action);
532 }
533
534 pub fn action_tx(&self) -> mpsc::UnboundedSender<A> {
536 self.action_tx.clone()
537 }
538
539 pub fn state(&self) -> &S {
541 self.store.state()
542 }
543
544 #[cfg(feature = "tasks")]
546 pub fn tasks(&mut self) -> &mut TaskManager<A> {
547 &mut self.tasks
548 }
549
550 #[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 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 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}