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::{process_raw_event, spawn_event_poller, RawEvent};
16use crate::effect::{DispatchResult, EffectStore, EffectStoreWithMiddleware};
17use crate::event::EventKind;
18use crate::store::{Middleware, Reducer, Store, StoreWithMiddleware};
19use crate::Action;
20
21#[cfg(feature = "subscriptions")]
22use crate::subscriptions::Subscriptions;
23#[cfg(feature = "tasks")]
24use crate::tasks::TaskManager;
25
26#[derive(Debug, Clone, Copy)]
28pub struct PollerConfig {
29 pub poll_timeout: Duration,
31 pub loop_sleep: Duration,
33}
34
35impl Default for PollerConfig {
36 fn default() -> Self {
37 Self {
38 poll_timeout: Duration::from_millis(10),
39 loop_sleep: Duration::from_millis(16),
40 }
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct EventOutcome<A> {
47 pub actions: Vec<A>,
49 pub needs_render: bool,
51}
52
53#[derive(Debug, Clone, Copy, Default)]
55pub struct RenderContext {
56 pub debug_enabled: bool,
58}
59
60impl RenderContext {
61 pub fn is_focused(self) -> bool {
63 !self.debug_enabled
64 }
65}
66
67impl<A> EventOutcome<A> {
68 pub fn ignored() -> Self {
70 Self {
71 actions: Vec::new(),
72 needs_render: false,
73 }
74 }
75
76 pub fn needs_render() -> Self {
78 Self {
79 actions: Vec::new(),
80 needs_render: true,
81 }
82 }
83
84 pub fn action(action: A) -> Self {
86 Self {
87 actions: vec![action],
88 needs_render: false,
89 }
90 }
91
92 pub fn actions<I>(actions: I) -> Self
94 where
95 I: IntoIterator<Item = A>,
96 {
97 Self {
98 actions: actions.into_iter().collect(),
99 needs_render: false,
100 }
101 }
102
103 pub fn with_render(mut self) -> Self {
105 self.needs_render = true;
106 self
107 }
108}
109
110impl<A> Default for EventOutcome<A> {
111 fn default() -> Self {
112 Self::ignored()
113 }
114}
115
116impl<A> From<A> for EventOutcome<A> {
117 fn from(action: A) -> Self {
118 Self::action(action)
119 }
120}
121
122impl<A> From<Vec<A>> for EventOutcome<A> {
123 fn from(actions: Vec<A>) -> Self {
124 Self {
125 actions,
126 needs_render: false,
127 }
128 }
129}
130
131impl<A> From<Option<A>> for EventOutcome<A> {
132 fn from(action: Option<A>) -> Self {
133 match action {
134 Some(action) => Self::action(action),
135 None => Self::ignored(),
136 }
137 }
138}
139
140impl<A> EventOutcome<A> {
141 pub fn from_actions(iter: impl IntoIterator<Item = A>) -> Self {
146 Self {
147 actions: iter.into_iter().collect(),
148 needs_render: false,
149 }
150 }
151}
152
153#[cfg(feature = "debug")]
154pub trait DebugAdapter<S, A>: 'static {
155 fn render(
156 &mut self,
157 frame: &mut Frame,
158 state: &S,
159 render_ctx: RenderContext,
160 render_fn: &mut dyn FnMut(&mut Frame, Rect, &S, RenderContext),
161 );
162
163 fn handle_event(
164 &mut self,
165 event: &EventKind,
166 state: &S,
167 action_tx: &mpsc::UnboundedSender<A>,
168 ) -> Option<bool>;
169
170 fn log_action(&mut self, action: &A);
171 fn is_enabled(&self) -> bool;
172}
173
174#[cfg(feature = "debug")]
175pub trait DebugHooks<A>: Sized {
176 #[cfg(feature = "tasks")]
177 fn with_task_manager(self, _tasks: &TaskManager<A>) -> Self {
178 self
179 }
180
181 #[cfg(feature = "subscriptions")]
182 fn with_subscriptions(self, _subscriptions: &Subscriptions<A>) -> Self {
183 self
184 }
185}
186
187pub trait DispatchStore<S, A: Action> {
189 fn dispatch(&mut self, action: A) -> bool;
191 fn state(&self) -> &S;
193}
194
195impl<S, A: Action> DispatchStore<S, A> for Store<S, A> {
196 fn dispatch(&mut self, action: A) -> bool {
197 Store::dispatch(self, action)
198 }
199
200 fn state(&self) -> &S {
201 Store::state(self)
202 }
203}
204
205impl<S, A: Action, M: Middleware<A>> DispatchStore<S, A> for StoreWithMiddleware<S, A, M> {
206 fn dispatch(&mut self, action: A) -> bool {
207 StoreWithMiddleware::dispatch(self, action)
208 }
209
210 fn state(&self) -> &S {
211 StoreWithMiddleware::state(self)
212 }
213}
214
215pub trait EffectStoreLike<S, A: Action, E> {
217 fn dispatch(&mut self, action: A) -> DispatchResult<E>;
219 fn state(&self) -> &S;
221}
222
223impl<S, A: Action, E> EffectStoreLike<S, A, E> for EffectStore<S, A, E> {
224 fn dispatch(&mut self, action: A) -> DispatchResult<E> {
225 EffectStore::dispatch(self, action)
226 }
227
228 fn state(&self) -> &S {
229 EffectStore::state(self)
230 }
231}
232
233impl<S, A: Action, E, M: Middleware<A>> EffectStoreLike<S, A, E>
234 for EffectStoreWithMiddleware<S, A, E, M>
235{
236 fn dispatch(&mut self, action: A) -> DispatchResult<E> {
237 EffectStoreWithMiddleware::dispatch(self, action)
238 }
239
240 fn state(&self) -> &S {
241 EffectStoreWithMiddleware::state(self)
242 }
243}
244
245pub struct DispatchRuntime<S, A: Action, St: DispatchStore<S, A> = Store<S, A>> {
247 store: St,
248 action_tx: mpsc::UnboundedSender<A>,
249 action_rx: mpsc::UnboundedReceiver<A>,
250 poller_config: PollerConfig,
251 #[cfg(feature = "debug")]
252 debug: Option<Box<dyn DebugAdapter<S, A>>>,
253 should_render: bool,
254 _state: std::marker::PhantomData<S>,
255}
256
257impl<S: 'static, A: Action> DispatchRuntime<S, A, Store<S, A>> {
258 pub fn new(state: S, reducer: Reducer<S, A>) -> Self {
260 Self::from_store(Store::new(state, reducer))
261 }
262}
263
264impl<S: 'static, A: Action, St: DispatchStore<S, A>> DispatchRuntime<S, A, St> {
265 pub fn from_store(store: St) -> Self {
267 let (action_tx, action_rx) = mpsc::unbounded_channel();
268 Self {
269 store,
270 action_tx,
271 action_rx,
272 poller_config: PollerConfig::default(),
273 #[cfg(feature = "debug")]
274 debug: None,
275 should_render: true,
276 _state: std::marker::PhantomData,
277 }
278 }
279
280 #[cfg(feature = "debug")]
282 pub fn with_debug<D>(mut self, debug: D) -> Self
283 where
284 D: DebugAdapter<S, A>,
285 {
286 self.debug = Some(Box::new(debug));
287 self
288 }
289
290 pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
292 self.poller_config = config;
293 self
294 }
295
296 pub fn enqueue(&self, action: A) {
298 let _ = self.action_tx.send(action);
299 }
300
301 pub fn action_tx(&self) -> mpsc::UnboundedSender<A> {
303 self.action_tx.clone()
304 }
305
306 pub fn state(&self) -> &S {
308 self.store.state()
309 }
310
311 pub async fn run<B, FRender, FEvent, FQuit, R>(
313 &mut self,
314 terminal: &mut Terminal<B>,
315 mut render: FRender,
316 mut map_event: FEvent,
317 mut should_quit: FQuit,
318 ) -> io::Result<()>
319 where
320 B: Backend,
321 FRender: FnMut(&mut Frame, Rect, &S, RenderContext),
322 FEvent: FnMut(&EventKind, &S) -> R,
323 R: Into<EventOutcome<A>>,
324 FQuit: FnMut(&A) -> bool,
325 {
326 let (event_tx, mut event_rx) = mpsc::unbounded_channel::<RawEvent>();
327 let cancel_token = CancellationToken::new();
328 let _handle = spawn_event_poller(
329 event_tx,
330 self.poller_config.poll_timeout,
331 self.poller_config.loop_sleep,
332 cancel_token.clone(),
333 );
334
335 loop {
336 if self.should_render {
337 let state = self.store.state();
338 let render_ctx = RenderContext {
339 debug_enabled: {
340 #[cfg(feature = "debug")]
341 {
342 self.debug
343 .as_ref()
344 .map(|debug| debug.is_enabled())
345 .unwrap_or(false)
346 }
347 #[cfg(not(feature = "debug"))]
348 {
349 false
350 }
351 },
352 };
353 terminal.draw(|frame| {
354 #[cfg(feature = "debug")]
355 if let Some(debug) = self.debug.as_mut() {
356 let mut render_fn =
357 |f: &mut Frame, area: Rect, state: &S, ctx: RenderContext| {
358 render(f, area, state, ctx);
359 };
360 debug.render(frame, state, render_ctx, &mut render_fn);
361 } else {
362 render(frame, frame.area(), state, render_ctx);
363 }
364
365 #[cfg(not(feature = "debug"))]
366 {
367 render(frame, frame.area(), state, render_ctx);
368 }
369 })?;
370 self.should_render = false;
371 }
372
373 tokio::select! {
374 Some(raw_event) = event_rx.recv() => {
375 let event = process_raw_event(raw_event);
376
377 #[cfg(feature = "debug")]
378 if let Some(debug) = self.debug.as_mut() {
379 if let Some(needs_render) =
380 debug.handle_event(&event, self.store.state(), &self.action_tx)
381 {
382 self.should_render = needs_render;
383 continue;
384 }
385 }
386
387 let outcome: EventOutcome<A> = map_event(&event, self.store.state()).into();
388 if outcome.needs_render {
389 self.should_render = true;
390 }
391 for action in outcome.actions {
392 let _ = self.action_tx.send(action);
393 }
394 }
395
396 Some(action) = self.action_rx.recv() => {
397 if should_quit(&action) {
398 break;
399 }
400
401 #[cfg(feature = "debug")]
402 if let Some(debug) = self.debug.as_mut() {
403 debug.log_action(&action);
404 }
405
406 self.should_render = self.store.dispatch(action);
407 }
408
409 else => {
410 break;
411 }
412 }
413 }
414
415 cancel_token.cancel();
416 Ok(())
417 }
418}
419
420pub struct EffectContext<'a, A: Action> {
422 action_tx: &'a mpsc::UnboundedSender<A>,
423 #[cfg(feature = "tasks")]
424 tasks: &'a mut TaskManager<A>,
425 #[cfg(feature = "subscriptions")]
426 subscriptions: &'a mut Subscriptions<A>,
427}
428
429impl<'a, A: Action> EffectContext<'a, A> {
430 pub fn emit(&self, action: A) {
432 let _ = self.action_tx.send(action);
433 }
434
435 pub fn action_tx(&self) -> &mpsc::UnboundedSender<A> {
437 self.action_tx
438 }
439
440 #[cfg(feature = "tasks")]
442 pub fn tasks(&mut self) -> &mut TaskManager<A> {
443 self.tasks
444 }
445
446 #[cfg(feature = "subscriptions")]
448 pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
449 self.subscriptions
450 }
451}
452
453pub struct EffectRuntime<S, A: Action, E, St: EffectStoreLike<S, A, E> = EffectStore<S, A, E>> {
455 store: St,
456 action_tx: mpsc::UnboundedSender<A>,
457 action_rx: mpsc::UnboundedReceiver<A>,
458 poller_config: PollerConfig,
459 #[cfg(feature = "debug")]
460 debug: Option<Box<dyn DebugAdapter<S, A>>>,
461 should_render: bool,
462 #[cfg(feature = "tasks")]
463 tasks: TaskManager<A>,
464 #[cfg(feature = "subscriptions")]
465 subscriptions: Subscriptions<A>,
466 action_broadcast: tokio::sync::broadcast::Sender<String>,
468 _state: std::marker::PhantomData<S>,
469 _effect: std::marker::PhantomData<E>,
470}
471
472impl<S: 'static, A: Action, E> EffectRuntime<S, A, E, EffectStore<S, A, E>> {
473 pub fn new(state: S, reducer: crate::effect::EffectReducer<S, A, E>) -> Self {
475 Self::from_store(EffectStore::new(state, reducer))
476 }
477}
478
479impl<S: 'static, A: Action, E, St: EffectStoreLike<S, A, E>> EffectRuntime<S, A, E, St> {
480 pub fn from_store(store: St) -> Self {
482 let (action_tx, action_rx) = mpsc::unbounded_channel();
483 let (action_broadcast, _) = tokio::sync::broadcast::channel(64);
484
485 #[cfg(feature = "tasks")]
486 let tasks = TaskManager::new(action_tx.clone());
487 #[cfg(feature = "subscriptions")]
488 let subscriptions = Subscriptions::new(action_tx.clone());
489
490 Self {
491 store,
492 action_tx,
493 action_rx,
494 poller_config: PollerConfig::default(),
495 #[cfg(feature = "debug")]
496 debug: None,
497 should_render: true,
498 #[cfg(feature = "tasks")]
499 tasks,
500 #[cfg(feature = "subscriptions")]
501 subscriptions,
502 action_broadcast,
503 _state: std::marker::PhantomData,
504 _effect: std::marker::PhantomData,
505 }
506 }
507
508 #[cfg(feature = "debug")]
510 pub fn with_debug<D>(mut self, debug: D) -> Self
511 where
512 D: DebugAdapter<S, A> + DebugHooks<A>,
513 {
514 let debug = {
515 let debug = debug;
516 #[cfg(feature = "tasks")]
517 let debug = debug.with_task_manager(&self.tasks);
518 #[cfg(feature = "subscriptions")]
519 let debug = debug.with_subscriptions(&self.subscriptions);
520 debug
521 };
522 self.debug = Some(Box::new(debug));
523 self
524 }
525
526 pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
528 self.poller_config = config;
529 self
530 }
531
532 pub fn subscribe_actions(&self) -> tokio::sync::broadcast::Receiver<String> {
537 self.action_broadcast.subscribe()
538 }
539
540 pub fn enqueue(&self, action: A) {
542 let _ = self.action_tx.send(action);
543 }
544
545 pub fn action_tx(&self) -> mpsc::UnboundedSender<A> {
547 self.action_tx.clone()
548 }
549
550 pub fn state(&self) -> &S {
552 self.store.state()
553 }
554
555 #[cfg(feature = "tasks")]
557 pub fn tasks(&mut self) -> &mut TaskManager<A> {
558 &mut self.tasks
559 }
560
561 #[cfg(feature = "subscriptions")]
563 pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
564 &mut self.subscriptions
565 }
566
567 #[cfg(all(feature = "tasks", feature = "subscriptions"))]
568 fn effect_context(&mut self) -> EffectContext<'_, A> {
569 EffectContext {
570 action_tx: &self.action_tx,
571 tasks: &mut self.tasks,
572 subscriptions: &mut self.subscriptions,
573 }
574 }
575
576 #[cfg(all(feature = "tasks", not(feature = "subscriptions")))]
577 fn effect_context(&mut self) -> EffectContext<'_, A> {
578 EffectContext {
579 action_tx: &self.action_tx,
580 tasks: &mut self.tasks,
581 }
582 }
583
584 #[cfg(all(not(feature = "tasks"), feature = "subscriptions"))]
585 fn effect_context(&mut self) -> EffectContext<'_, A> {
586 EffectContext {
587 action_tx: &self.action_tx,
588 subscriptions: &mut self.subscriptions,
589 }
590 }
591
592 #[cfg(all(not(feature = "tasks"), not(feature = "subscriptions")))]
593 fn effect_context(&mut self) -> EffectContext<'_, A> {
594 EffectContext {
595 action_tx: &self.action_tx,
596 }
597 }
598
599 pub async fn run<B, FRender, FEvent, FQuit, FEffect, R>(
601 &mut self,
602 terminal: &mut Terminal<B>,
603 mut render: FRender,
604 mut map_event: FEvent,
605 mut should_quit: FQuit,
606 mut handle_effect: FEffect,
607 ) -> io::Result<()>
608 where
609 B: Backend,
610 FRender: FnMut(&mut Frame, Rect, &S, RenderContext),
611 FEvent: FnMut(&EventKind, &S) -> R,
612 R: Into<EventOutcome<A>>,
613 FQuit: FnMut(&A) -> bool,
614 FEffect: FnMut(E, &mut EffectContext<A>),
615 {
616 let (event_tx, mut event_rx) = mpsc::unbounded_channel::<RawEvent>();
617 let cancel_token = CancellationToken::new();
618 let _handle = spawn_event_poller(
619 event_tx,
620 self.poller_config.poll_timeout,
621 self.poller_config.loop_sleep,
622 cancel_token.clone(),
623 );
624
625 loop {
626 if self.should_render {
627 let state = self.store.state();
628 let render_ctx = RenderContext {
629 debug_enabled: {
630 #[cfg(feature = "debug")]
631 {
632 self.debug
633 .as_ref()
634 .map(|debug| debug.is_enabled())
635 .unwrap_or(false)
636 }
637 #[cfg(not(feature = "debug"))]
638 {
639 false
640 }
641 },
642 };
643 terminal.draw(|frame| {
644 #[cfg(feature = "debug")]
645 if let Some(debug) = self.debug.as_mut() {
646 let mut render_fn =
647 |f: &mut Frame, area: Rect, state: &S, ctx: RenderContext| {
648 render(f, area, state, ctx);
649 };
650 debug.render(frame, state, render_ctx, &mut render_fn);
651 } else {
652 render(frame, frame.area(), state, render_ctx);
653 }
654
655 #[cfg(not(feature = "debug"))]
656 {
657 render(frame, frame.area(), state, render_ctx);
658 }
659 })?;
660 self.should_render = false;
661 }
662
663 tokio::select! {
664 Some(raw_event) = event_rx.recv() => {
665 let event = process_raw_event(raw_event);
666
667 #[cfg(feature = "debug")]
668 if let Some(debug) = self.debug.as_mut() {
669 if let Some(needs_render) =
670 debug.handle_event(&event, self.store.state(), &self.action_tx)
671 {
672 self.should_render = needs_render;
673 continue;
674 }
675 }
676
677 let outcome: EventOutcome<A> = map_event(&event, self.store.state()).into();
678 if outcome.needs_render {
679 self.should_render = true;
680 }
681 for action in outcome.actions {
682 let _ = self.action_tx.send(action);
683 }
684 }
685
686 Some(action) = self.action_rx.recv() => {
687 if should_quit(&action) {
688 break;
689 }
690
691 #[cfg(feature = "debug")]
692 if let Some(debug) = self.debug.as_mut() {
693 debug.log_action(&action);
694 }
695
696 let _ = self.action_broadcast.send(action.name().to_string());
698
699 let result = self.store.dispatch(action);
700 if result.has_effects() {
701 let mut ctx = self.effect_context();
702 for effect in result.effects {
703 handle_effect(effect, &mut ctx);
704 }
705 }
706 self.should_render = result.changed;
707 }
708
709 else => {
710 break;
711 }
712 }
713 }
714
715 cancel_token.cancel();
716 #[cfg(feature = "subscriptions")]
717 self.subscriptions.cancel_all();
718 #[cfg(feature = "tasks")]
719 self.tasks.cancel_all();
720
721 Ok(())
722 }
723}