Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Add doc comments
Merged did:key:z6MkgFq6...nBGz opened 2 years ago
5 files changed +49 -7 cf32da9a 73f29c94
modified src/store.rs
@@ -11,15 +11,22 @@ use super::task::{Interrupted, Terminator};

const STORE_TICK_RATE: Duration = Duration::from_millis(1000);

+
/// The `State` known to the application store. It handles user-defined
+
/// application messages as well as ticks.
pub trait State<A, P>
where
    P: Clone + Debug + Send + Sync,
{
-
    fn tick(&self);
-

+
    /// Handle a user-defined application message and return an `Exit` object
+
    /// in case the received message requested the application to also quit.
    fn handle_action(&mut self, action: A) -> Option<Exit<P>>;
+

+
    /// Handle recurring tick.
+
    fn tick(&self);
}

+
/// The `Store` updates the applications' state concurrently. It handles
+
/// messages coming from the frontend and updates the state accordingly.
pub struct Store<A, S, P>
where
    S: State<A, P> + Clone + Send + Sync,
@@ -52,6 +59,10 @@ where
    S: State<A, P> + Clone + Debug + Send + Sync + 'static,
    P: Clone + Debug + Send + Sync + 'static,
{
+
    /// By calling `main_loop`, the store will wait for new messages coming
+
    /// from the frontend and update the applications' state accordingly. It will
+
    /// also tick with the defined `STORE_TICK_RATE`.
+
    /// Updated states are then being send to the state message channel.
    pub async fn main_loop(
        self,
        mut state: S,
@@ -59,14 +70,14 @@ where
        mut action_rx: UnboundedReceiver<A>,
        mut interrupt_rx: broadcast::Receiver<Interrupted<P>>,
    ) -> anyhow::Result<Interrupted<P>> {
-
        // the initial state once
+
        // Send the initial state once
        self.state_tx.send(state.clone())?;

        let mut ticker = tokio::time::interval(STORE_TICK_RATE);

        let result = loop {
            tokio::select! {
-
                // Handle the actions coming from the UI
+
                // Handle the actions coming from the frontend
                // and process them to do async operations
                Some(action) = action_rx.recv() => {
                    if let Some(exit) = state.handle_action(action) {
modified src/task.rs
@@ -4,6 +4,8 @@ use std::fmt::Debug;
use tokio::signal::unix::signal;
use tokio::sync::broadcast;

+
/// An `Interrupt` message that is produced by either an OS signal (e.g. kill)
+
/// or the user by requesting the application to close.
#[derive(Debug, Clone)]
pub enum Interrupted<P>
where
@@ -13,6 +15,7 @@ where
    User { payload: Option<P> },
}

+
/// The `Terminator` wraps a broadcast channel and can send an interrupt messages.
#[derive(Debug, Clone)]
pub struct Terminator<P>
where
@@ -25,10 +28,12 @@ impl<P> Terminator<P>
where
    P: Clone + Send + Sync + Debug + 'static,
{
+
    /// Create a `Terminator` that stores the sending end of a broadcast channel.
    pub fn new(interrupt_tx: broadcast::Sender<Interrupted<P>>) -> Self {
        Self { interrupt_tx }
    }

+
    /// Send interrupt message to the broadcast channel.
    pub fn terminate(&mut self, interrupted: Interrupted<P>) -> anyhow::Result<()> {
        self.interrupt_tx.send(interrupted)?;

@@ -36,6 +41,7 @@ where
    }
}

+
/// Receive `SIGINT` and call terminator which sends the interrupt message to its broadcast channel.
#[cfg(unix)]
async fn terminate_by_unix_signal<P>(mut terminator: Terminator<P>)
where
@@ -51,7 +57,7 @@ where
        .expect("failed to send interrupt signal");
}

-
// create a broadcast channel for retrieving the application kill signal
+
/// Create a broadcast channel and spawn a task for retrieving the applications' kill signal.
pub fn create_termination<P>() -> (Terminator<P>, broadcast::Receiver<Interrupted<P>>)
where
    P: Clone + Send + Sync + Debug + 'static,
modified src/terminal.rs
@@ -91,6 +91,7 @@ impl<W: Write> ratatui::backend::Backend for TermionBackendExt<W> {
    }
}

+
/// Setup a `Terminal` with inline viewport using the `termion` backend.
pub fn setup(height: usize) -> anyhow::Result<Terminal<Backend>> {
    let stdout = io::stdout().into_raw_mode()?;
    let options = TerminalOptions {
@@ -103,11 +104,15 @@ pub fn setup(height: usize) -> anyhow::Result<Terminal<Backend>> {
    )?)
}

+
/// Restore the `Terminal` on quit.
pub fn restore(terminal: &mut Terminal<Backend>) -> anyhow::Result<()> {
    terminal.clear()?;
    Ok(())
}

+
/// Spawn one thread that polls `stdin` for new user input and another thread
+
/// that polls UNIX signals, e.g. `SIGWINCH` when the terminal window size is
+
/// being changed.
pub fn events() -> mpsc::UnboundedReceiver<Event> {
    let (tx, rx) = mpsc::unbounded_channel();
    let events_tx = tx.clone();
modified src/ui.rs
@@ -27,11 +27,17 @@ type Backend = TermionBackendExt<RawTerminal<io::Stdout>>;
const RENDERING_TICK_RATE: Duration = Duration::from_millis(250);
const INLINE_HEIGHT: usize = 20;

+
/// The `Frontend` runs an applications' view concurrently. It handles
+
/// terminal events as well as state updates and renders the view accordingly.
+
///
+
/// Once created and run with `main_loop`, the `Frontend` will wait for new messages
+
/// being sent on either the terminal event, the state or the interrupt message channel.
pub struct Frontend<A> {
    action_tx: mpsc::UnboundedSender<A>,
}

impl<A> Frontend<A> {
+
    /// Create a new `Frontend` storing the sending end of a message channel.
    pub fn new() -> (Self, UnboundedSender<A>, UnboundedReceiver<A>) {
        let (action_tx, action_rx) = mpsc::unbounded_channel();

@@ -44,6 +50,20 @@ impl<A> Frontend<A> {
        )
    }

+
    /// By calling `main_loop`, the `Frontend` will wait for new messages being sent
+
    /// on either the terminal event, the state or the interrupt message channel.
+
    /// After all, it will draw the (potentially) updated root widget.
+
    ///
+
    /// Terminal event messages are being sent by a thread polling `stdin` for new user input
+
    /// and another thread polling UNIX signals, e.g. `SIGWINCH` when the terminal
+
    /// window size is being changed. Terminal events are then passed to the root widget
+
    /// of the application.
+
    ///
+
    /// State messages are being sent by the applications' `Store`. Received state updates
+
    /// will be passed to the root widget as well.
+
    ///
+
    /// Interrupt messages are being sent to broadcast channel for retrieving the
+
    /// application kill signal.
    pub async fn main_loop<S, W, P>(
        self,
        root: Option<W>,
modified src/ui/widget.rs
@@ -63,9 +63,9 @@ pub trait View<S, A> {
    /// the type of `state`. These can use widgets from the library that do not know the
    /// type of `state`.
    ///
-
    /// If `on_update` is set, implementators of this function should call it to
+
    /// If `on_update` is set, implementations of this function should call it to
    /// construct and update the internal props. If it is not set, app widgets can construct
-
    /// prosp directly via their state converters, whereas library widgets can just fallback
+
    /// props directly via their state converters, whereas library widgets can just fallback
    /// to their current props.
    fn update(&mut self, state: &S);
}