lib: Add generic exit payload to flux app

This commit is contained in:
Erik Kundt
2024-02-21 15:01:16 +01:00
parent c864888d48
commit ff1c09202c
5 changed files with 86 additions and 89 deletions
+24 -17
View File
@@ -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
+28 -11
View File
@@ -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);
+13 -5
View File
@@ -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(())
}
+3 -19
View File
@@ -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)
+18 -37
View File
@@ -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()