radicle_tui/
lib.rs

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/// An optional return value.
24#[derive(Clone, Debug)]
25pub struct Exit<T> {
26    pub value: Option<T>,
27}
28
29/// The output that is returned by all selection interfaces.
30#[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
79/// Provide implementations for conversions to and from `Box<dyn Any>`.
80pub 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/// A 'PageStack' for applications. Page identifier can be pushed to and
110/// popped from the stack.
111#[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
139/// A multi-producer, single-consumer message channel.
140pub 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
152/// Initialize a `Store` with the `State` given and a `Frontend` with the `Widget` given,
153/// and run their main loops concurrently. Connect them to the `Channel` and also to
154/// an interrupt broadcast channel also initialized in this function.
155pub 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
186/// Initialize a `Store` with the `State` given and a `Frontend` with the `App` given,
187/// and run their main loops concurrently. Connect them to the `Channel` and also to
188/// an interrupt broadcast channel also initialized in this function.
189pub 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}