1pub mod event;
2pub mod store;
3pub mod task;
4pub mod terminal;
5pub mod ui;
6
7use std::any::Any;
8use std::fmt::Debug;
9
10use ratatui::Viewport;
11use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
12
13use serde::ser::{Serialize, SerializeStruct, Serializer};
14
15use anyhow::Result;
16
17use store::Update;
18use task::Interrupted;
19use ui::im;
20use ui::im::Show;
21use ui::rm;
22
23#[derive(Clone, Debug)]
25pub struct Exit<T> {
26 pub value: Option<T>,
27}
28
29#[derive(Clone, Default, Debug, Eq, PartialEq)]
31pub struct Selection<I>
32where
33 I: ToString,
34{
35 pub operation: Option<String>,
36 pub ids: Vec<I>,
37 pub args: Vec<String>,
38}
39
40impl<I> Selection<I>
41where
42 I: ToString,
43{
44 pub fn with_operation(mut self, operation: String) -> Self {
45 self.operation = Some(operation);
46 self
47 }
48
49 pub fn with_id(mut self, id: I) -> Self {
50 self.ids.push(id);
51 self
52 }
53
54 pub fn with_args(mut self, arg: String) -> Self {
55 self.args.push(arg);
56 self
57 }
58}
59
60impl<I> Serialize for Selection<I>
61where
62 I: ToString,
63{
64 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
65 where
66 S: Serializer,
67 {
68 let mut state = serializer.serialize_struct("", 3)?;
69 state.serialize_field("operation", &self.operation)?;
70 state.serialize_field(
71 "ids",
72 &self.ids.iter().map(|id| id.to_string()).collect::<Vec<_>>(),
73 )?;
74 state.serialize_field("args", &self.args)?;
75 state.end()
76 }
77}
78
79pub trait BoxedAny {
81 fn from_boxed_any(any: Box<dyn Any>) -> Option<Self>
82 where
83 Self: Sized + Clone + 'static;
84
85 fn to_boxed_any(self) -> Box<dyn Any>
86 where
87 Self: Sized + Clone + 'static;
88}
89
90impl<T> BoxedAny for T
91where
92 T: Sized + Clone + 'static,
93{
94 fn from_boxed_any(any: Box<dyn Any>) -> Option<Self>
95 where
96 Self: Sized + Clone + 'static,
97 {
98 any.downcast::<Self>().ok().map(|b| *b)
99 }
100
101 fn to_boxed_any(self) -> Box<dyn Any>
102 where
103 Self: Sized + Clone + 'static,
104 {
105 Box::new(self)
106 }
107}
108
109#[derive(Clone, Default, Debug)]
112pub struct PageStack<T> {
113 pages: Vec<T>,
114}
115
116impl<T> PageStack<T> {
117 pub fn new(pages: Vec<T>) -> Self {
118 Self { pages }
119 }
120
121 pub fn push(&mut self, page: T) {
122 self.pages.push(page);
123 }
124
125 pub fn pop(&mut self) -> Option<T> {
126 self.pages.pop()
127 }
128
129 pub fn peek(&self) -> Result<&T> {
130 match self.pages.last() {
131 Some(page) => Ok(page),
132 None => Err(anyhow::anyhow!(
133 "Could not peek active page. Page stack is empty."
134 )),
135 }
136 }
137}
138
139pub struct Channel<M> {
141 pub tx: UnboundedSender<M>,
142 pub rx: UnboundedReceiver<M>,
143}
144
145impl<A> Default for Channel<A> {
146 fn default() -> Self {
147 let (tx, rx) = mpsc::unbounded_channel();
148 Self { tx: tx.clone(), rx }
149 }
150}
151
152pub async fn rm<S, M, P>(
156 state: S,
157 root: rm::widget::Widget<S, M>,
158 viewport: Viewport,
159 channel: Channel<M>,
160) -> Result<Option<P>>
161where
162 S: Update<M, Return = P> + Clone + Debug + Send + Sync + 'static,
163 M: Debug + Send + Sync + 'static,
164 P: Clone + Debug + Send + Sync + 'static,
165{
166 let (terminator, mut interrupt_rx) = task::create_termination();
167
168 let (store, state_rx) = store::Store::<S, M, P>::new();
169 let frontend = rm::Frontend::default();
170
171 tokio::try_join!(
172 store.run(state, terminator, channel.rx, interrupt_rx.resubscribe()),
173 frontend.run(root, state_rx, interrupt_rx.resubscribe(), viewport),
174 )?;
175
176 if let Ok(reason) = interrupt_rx.recv().await {
177 match reason {
178 Interrupted::User { payload } => Ok(payload),
179 Interrupted::OsSignal => anyhow::bail!("exited because of an os sig int"),
180 }
181 } else {
182 anyhow::bail!("exited because of an unexpected error");
183 }
184}
185
186pub async fn im<S, M, P>(state: S, viewport: Viewport, channel: Channel<M>) -> Result<Option<P>>
190where
191 S: Update<M, Return = P> + Show<M> + Clone + Send + Sync + 'static,
192 M: Clone + Debug + Send + Sync + 'static,
193 P: Clone + Debug + Send + Sync + 'static,
194{
195 let (terminator, mut interrupt_rx) = task::create_termination();
196
197 let state_tx = channel.tx.clone();
198 let (store, state_rx) = store::Store::<S, M, P>::new();
199 let frontend = im::Frontend::default();
200
201 tokio::try_join!(
202 store.run(state, terminator, channel.rx, interrupt_rx.resubscribe()),
203 frontend.run(state_tx, state_rx, interrupt_rx.resubscribe(), viewport),
204 )?;
205
206 if let Ok(reason) = interrupt_rx.recv().await {
207 match reason {
208 Interrupted::User { payload } => Ok(payload),
209 Interrupted::OsSignal => anyhow::bail!("exited because of an os sig int"),
210 }
211 } else {
212 anyhow::bail!("exited because of an unexpected error");
213 }
214}