Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Add generic exit payload to flux app
Erik Kundt committed 2 years ago
commit ff1c09202c82e2abd52140b8ee77d00f0fdabd19
parent c864888d48aa65865535e9693b261d5bbf1ccf24
5 files changed +86 -89
modified src/flux/store.rs
@@ -5,29 +5,34 @@ use std::time::Duration;
use tokio::sync::broadcast;
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};

+
use crate::Exit;
+

use super::termination::{Interrupted, Terminator};

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

-
pub trait State<A> {
-
    type Exit;
-

+
pub trait State<A, P>
+
where
+
    P: Clone + Debug + Send + Sync,
+
{
    fn tick(&self);

-
    fn handle_action(&mut self, action: A) -> Option<Self::Exit>;
+
    fn handle_action(&mut self, action: A) -> Option<Exit<P>>;
}

-
pub struct Store<A, S>
+
pub struct Store<A, S, P>
where
-
    S: State<A> + Clone + Send + Sync,
+
    S: State<A, P> + Clone + Send + Sync,
+
    P: Clone + Debug + Send + Sync,
{
    state_tx: UnboundedSender<S>,
-
    _phantom: PhantomData<A>,
+
    _phantom: PhantomData<(A, P)>,
}

-
impl<A, S> Store<A, S>
+
impl<A, S, P> Store<A, S, P>
where
-
    S: State<A> + Clone + Send + Sync,
+
    S: State<A, P> + Clone + Send + Sync,
+
    P: Clone + Debug + Send + Sync,
{
    pub fn new() -> (Self, UnboundedReceiver<S>) {
        let (state_tx, state_rx) = mpsc::unbounded_channel::<S>();
@@ -42,17 +47,18 @@ where
    }
}

-
impl<A, S> Store<A, S>
+
impl<A, S, P> Store<A, S, P>
where
-
    S: State<A> + Clone + Send + Sync + 'static + Debug,
+
    S: State<A, P> + Clone + Debug + Send + Sync + 'static,
+
    P: Clone + Debug + Send + Sync + 'static,
{
    pub async fn main_loop(
        self,
        mut state: S,
-
        mut terminator: Terminator,
+
        mut terminator: Terminator<P>,
        mut action_rx: UnboundedReceiver<A>,
-
        mut interrupt_rx: broadcast::Receiver<Interrupted>,
-
    ) -> anyhow::Result<Interrupted> {
+
        mut interrupt_rx: broadcast::Receiver<Interrupted<P>>,
+
    ) -> anyhow::Result<Interrupted<P>> {
        // the initial state once
        self.state_tx.send(state.clone())?;

@@ -63,10 +69,11 @@ where
                // Handle the actions coming from the UI
                // and process them to do async operations
                Some(action) = action_rx.recv() => {
-
                    if let Some(_exit) = state.handle_action(action) {
-
                        let _ = terminator.terminate(Interrupted::UserInt);
+
                    if let Some(exit) = state.handle_action(action) {
+
                        let interrupted = Interrupted::User { payload: exit.value };
+
                        let _ = terminator.terminate(interrupted.clone());

-
                        break Interrupted::UserInt;
+
                        break interrupted;
                    }
                },
                // Tick to terminate the select every N milliseconds
modified src/flux/termination.rs
@@ -1,24 +1,35 @@
+
use std::fmt::Debug;
+

#[cfg(unix)]
use tokio::signal::unix::signal;
use tokio::sync::broadcast;

#[derive(Debug, Clone)]
-
pub enum Interrupted {
-
    OsSigInt,
-
    UserInt,
+
pub enum Interrupted<P>
+
where
+
    P: Clone + Send + Sync + Debug,
+
{
+
    OsSignal,
+
    User { payload: Option<P> },
}

#[derive(Debug, Clone)]
-
pub struct Terminator {
-
    interrupt_tx: broadcast::Sender<Interrupted>,
+
pub struct Terminator<P>
+
where
+
    P: Clone + Send + Sync + Debug,
+
{
+
    interrupt_tx: broadcast::Sender<Interrupted<P>>,
}

-
impl Terminator {
-
    pub fn new(interrupt_tx: broadcast::Sender<Interrupted>) -> Self {
+
impl<P> Terminator<P>
+
where
+
    P: Clone + Send + Sync + Debug + 'static,
+
{
+
    pub fn new(interrupt_tx: broadcast::Sender<Interrupted<P>>) -> Self {
        Self { interrupt_tx }
    }

-
    pub fn terminate(&mut self, interrupted: Interrupted) -> anyhow::Result<()> {
+
    pub fn terminate(&mut self, interrupted: Interrupted<P>) -> anyhow::Result<()> {
        self.interrupt_tx.send(interrupted)?;

        Ok(())
@@ -26,19 +37,25 @@ impl Terminator {
}

#[cfg(unix)]
-
async fn terminate_by_unix_signal(mut terminator: Terminator) {
+
async fn terminate_by_unix_signal<P>(mut terminator: Terminator<P>)
+
where
+
    P: Clone + Send + Sync + Debug + 'static,
+
{
    let mut interrupt_signal = signal(tokio::signal::unix::SignalKind::interrupt())
        .expect("failed to create interrupt signal stream");

    interrupt_signal.recv().await;

    terminator
-
        .terminate(Interrupted::OsSigInt)
+
        .terminate(Interrupted::OsSignal)
        .expect("failed to send interrupt signal");
}

// create a broadcast channel for retrieving the application kill signal
-
pub fn create_termination() -> (Terminator, broadcast::Receiver<Interrupted>) {
+
pub fn create_termination<P>() -> (Terminator<P>, broadcast::Receiver<Interrupted<P>>)
+
where
+
    P: Clone + Send + Sync + Debug + 'static,
+
{
    let (tx, rx) = broadcast::channel(1);
    let terminator = Terminator::new(tx);

modified src/flux/ui.rs
@@ -6,6 +6,7 @@ pub mod span;
pub mod theme;
pub mod widget;

+
use std::fmt::Debug;
use std::io::{self};
use std::thread;
use std::time::Duration;
@@ -38,11 +39,16 @@ impl<A> Frontend<A> {
        (Self { action_tx }, action_rx)
    }

-
    pub async fn main_loop<S: State<A>, W: Widget<S, A> + Render<()>>(
+
    pub async fn main_loop<S, W, P>(
        self,
        mut state_rx: UnboundedReceiver<S>,
-
        mut interrupt_rx: broadcast::Receiver<Interrupted>,
-
    ) -> anyhow::Result<Interrupted> {
+
        mut interrupt_rx: broadcast::Receiver<Interrupted<P>>,
+
    ) -> anyhow::Result<Interrupted<P>>
+
    where
+
        S: State<A, P>,
+
        W: Widget<S, A> + Render<()>,
+
        P: Clone + Send + Sync + Debug,
+
    {
        let mut terminal = setup_terminal()?;
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
        let mut events_rx = events();
@@ -54,7 +60,7 @@ impl<A> Frontend<A> {
        };

        // let mut last_frame: Option<CompletedFrame> = None;
-
        let result: anyhow::Result<Interrupted> = loop {
+
        let result: anyhow::Result<Interrupted<P>> = loop {
            tokio::select! {
                // Tick to terminate the select every N milliseconds
                _ = ticker.tick() => (),
@@ -95,7 +101,9 @@ fn setup_terminal() -> anyhow::Result<Terminal<Backend>> {
    )?)
}

-
fn restore_terminal(_terminal: &mut Terminal<Backend>) -> anyhow::Result<()> {
+
fn restore_terminal(terminal: &mut Terminal<Backend>) -> anyhow::Result<()> {
+
    // let size = terminal.get_frame().size();
+
    terminal.clear()?;
    Ok(())
}

modified src/flux/ui/widget.rs
@@ -25,12 +25,9 @@ pub trait Widget<S, A> {
}

pub trait Render<P> {
-
    fn render<B: ratatui::backend::Backend>(&mut self, frame: &mut Frame, area: Rect, props: P);
+
    fn render<B: ratatui::backend::Backend>(&self, frame: &mut Frame, area: Rect, props: P);
}

-
///
-
///
-
///
pub struct Shortcut {
    pub short: String,
    pub long: String,
@@ -80,12 +77,7 @@ impl<S, A> Widget<S, A> for Shortcuts<A> {
}

impl<A> Render<ShortcutsProps> for Shortcuts<A> {
-
    fn render<B: Backend>(
-
        &mut self,
-
        frame: &mut ratatui::Frame,
-
        area: Rect,
-
        props: ShortcutsProps,
-
    ) {
+
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, props: ShortcutsProps) {
        use ratatui::widgets::Table;

        let mut shortcuts = props.shortcuts.iter().peekable();
@@ -123,9 +115,6 @@ impl<A> Render<ShortcutsProps> for Shortcuts<A> {
    }
}

-
///
-
///
-
///
pub trait ToRow<const W: usize> {
    fn to_row(&self) -> [Cell; W];
}
@@ -204,12 +193,7 @@ impl<'a, A, R, const W: usize> Render<TableProps<'a, R, W>> for Table<A>
where
    R: ToRow<W> + Debug,
{
-
    fn render<B: Backend>(
-
        &mut self,
-
        frame: &mut ratatui::Frame,
-
        area: Rect,
-
        props: TableProps<R, W>,
-
    ) {
+
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, props: TableProps<R, W>) {
        let layout = if props.footer.is_some() {
            Layout::default()
                .direction(Direction::Vertical)
modified src/lib.rs
@@ -1,12 +1,7 @@
-
use std::fmt::Display;
-

use anyhow::Result;

use serde::ser::{Serialize, SerializeStruct, Serializer};

-
use radicle::cob::ObjectId;
-
use radicle::node::notifications::NotificationId;
-

pub mod common;

#[cfg(feature = "realm")]
@@ -16,45 +11,32 @@ pub mod realm;
pub mod flux;

/// An optional return value.
+
#[derive(Clone, Debug)]
pub struct Exit<T> {
    pub value: Option<T>,
}

-
/// Returned ids can be of type `ObjectId` or `NotificationId`.
-
#[derive(Clone, Debug, Eq, PartialEq)]
-
pub enum Id {
-
    Object(ObjectId),
-
    Notification(NotificationId),
-
}
-

-
impl Display for Id {
-
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-
        match self {
-
            Id::Object(id) => {
-
                write!(f, "{id}")
-
            }
-
            Id::Notification(id) => {
-
                write!(f, "{id}")
-
            }
-
        }
-
    }
-
}
-

/// The output that is returned by all selection interfaces.
#[derive(Clone, Default, Debug, Eq, PartialEq)]
-
pub struct SelectionExit {
-
    operation: Option<String>,
-
    ids: Vec<Id>,
-
    args: Vec<String>,
+
pub struct Selection<I>
+
where
+
    I: ToString,
+
{
+
    pub operation: Option<String>,
+
    pub ids: Vec<I>,
+
    pub args: Vec<String>,
}

-
impl SelectionExit {
+
impl<I> Selection<I>
+
where
+
    I: ToString,
+
{
    pub fn with_operation(mut self, operation: String) -> Self {
        self.operation = Some(operation);
        self
    }

-
    pub fn with_id(mut self, id: Id) -> Self {
+
    pub fn with_id(mut self, id: I) -> Self {
        self.ids.push(id);
        self
    }
@@ -65,7 +47,10 @@ impl SelectionExit {
    }
}

-
impl Serialize for SelectionExit {
+
impl<I> Serialize for Selection<I>
+
where
+
    I: ToString,
+
{
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
@@ -74,11 +59,7 @@ impl Serialize for SelectionExit {
        state.serialize_field("operation", &self.operation)?;
        state.serialize_field(
            "ids",
-
            &self
-
                .ids
-
                .iter()
-
                .map(|id| format!("{}", id))
-
                .collect::<Vec<_>>(),
+
            &self.ids.iter().map(|id| id.to_string()).collect::<Vec<_>>(),
        )?;
        state.serialize_field("args", &self.args)?;
        state.end()