diff --git a/bin/commands/inbox.rs b/bin/commands/inbox.rs index a6f0d32c..ddec24e1 100644 --- a/bin/commands/inbox.rs +++ b/bin/commands/inbox.rs @@ -148,11 +148,7 @@ pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()> ); let output = Window::default().run(&mut app, 1000 / FPS)?; - let output = output - .map(|o| serde_json::to_string(&o).unwrap_or_default()) - .unwrap_or_default(); - - eprint!("{output}"); + eprint!("{:?}", output); } } diff --git a/bin/commands/inbox/select/flux.rs b/bin/commands/inbox/select/flux.rs new file mode 100644 index 00000000..0c5a16ae --- /dev/null +++ b/bin/commands/inbox/select/flux.rs @@ -0,0 +1,84 @@ +#[path = "flux/ui.rs"] +mod ui; + +use std::default; + +use anyhow::Result; + +use radicle::node::notifications::Notification; +use radicle_tui as tui; +use tui::cob::inbox::Filter; +use tui::context::Context; +use tui::flux::store::{State, Store}; +use tui::flux::termination::{self, Interrupted}; +use tui::flux::ui::Frontend; + +use crate::tui_inbox::select::flux::ui::ListPage; + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub enum Mode { + Id, + #[default] + Operation, +} + +pub struct App { + context: Context, + filter: Filter, +} + +#[derive(Clone)] +pub struct InboxState { + notifications: Vec, +} + +impl InboxState { + pub fn notifications(&self) -> &Vec { + &self.notifications + } +} + +pub enum Action { + Exit, +} + +impl State for InboxState { + fn tick(&self) {} + + fn handle_action(self, action: Action) -> anyhow::Result { + match action { + Action::Exit => Ok(true), + } + } +} + +impl App { + pub fn new(context: Context, filter: Filter) -> Self { + Self { context, filter } + } + + pub async fn run(&self) -> Result<()> { + let (terminator, mut interrupt_rx) = termination::create_termination(); + let (store, state_rx) = Store::::new(); + let (frontend, action_rx) = Frontend::::new(); + let state = InboxState { + notifications: vec![], + }; + + tokio::try_join!( + store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()), + frontend.main_loop::(state_rx, interrupt_rx.resubscribe()), + )?; + + if let Ok(reason) = interrupt_rx.recv().await { + match reason { + Interrupted::UserInt => {} + Interrupted::OsSigInt => println!("exited because of an os sig int"), + } + } else { + println!("exited because of an unexpected error"); + } + + Ok(()) + } +} diff --git a/bin/commands/inbox/select/flux/ui.rs b/bin/commands/inbox/select/flux/ui.rs new file mode 100644 index 00000000..85e660c2 --- /dev/null +++ b/bin/commands/inbox/select/flux/ui.rs @@ -0,0 +1,77 @@ +use tokio::sync::mpsc::UnboundedSender; + +use termion::event::Key; + +use ratatui::backend::Backend; +use ratatui::widgets::Paragraph; + +use radicle_tui as tui; +use tui::flux::ui::{Render, Widget}; + +use crate::tui_inbox::select::flux::{Action, InboxState}; + +pub struct Props { + notifications: Vec, +} + +impl From<&InboxState> for Props { + fn from(state: &InboxState) -> Self { + Props { + notifications: state.notifications().clone(), + } + } +} + +pub struct ListPage { + /// Action sender + pub action_tx: UnboundedSender, + // Mapped Props from State + props: Props, +} + +impl Widget for ListPage { + fn new(state: &InboxState, action_tx: UnboundedSender) -> Self + where + Self: Sized, + { + ListPage { + action_tx: action_tx.clone(), + props: Props::from(state), + } + .move_with_state(state) + } + + fn move_with_state(self, state: &InboxState) -> Self + where + Self: Sized, + { + ListPage { + props: Props::from(state), + ..self + } + } + + fn name(&self) -> &str { + "list-page" + } + + fn handle_key_event(&mut self, key: termion::event::Key) { + match key { + Key::Char('q') => { + let _ = self.action_tx.send(Action::Exit); + } + _ => {} + } + } +} + +impl Render<()> for ListPage { + fn render(&self, frame: &mut ratatui::Frame, _props: ()) { + let area = frame.size(); + let layout = tui::flux::ui::layout::default_page(area, 1u16, 1u16); + + let shortcuts = Paragraph::new(String::from("q quit")); + + frame.render_widget(shortcuts, layout.shortcuts); + } +} diff --git a/bin/commands/inbox/select/realm.rs b/bin/commands/inbox/select/realm.rs new file mode 100644 index 00000000..ec31e1ec --- /dev/null +++ b/bin/commands/inbox/select/realm.rs @@ -0,0 +1,225 @@ +#[path = "realm/event.rs"] +mod event; +#[path = "realm/page.rs"] +mod page; +#[path = "realm/ui.rs"] +mod ui; + +use std::fmt::Display; +use std::hash::Hash; + +use anyhow::Result; + +use serde::{Serialize, Serializer}; + +use tuirealm::application::PollStrategy; +use tuirealm::{Application, Frame, NoUserEvent, Sub, SubClause}; + +use radicle_tui as tui; + +use tui::cob::inbox::Filter; +use tui::context::Context; +use tui::ui::subscription; +use tui::ui::theme::Theme; +use tui::{Exit, PageStack, SelectionExit, Tui}; + +use page::ListView; + +/// Wrapper around radicle's `PatchId` that serializes +/// to a human-readable string. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PatchId(radicle::cob::patch::PatchId); + +impl From for PatchId { + fn from(value: radicle::cob::patch::PatchId) -> Self { + PatchId(value) + } +} + +impl Display for PatchId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Serialize for PatchId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{}", *self.0)) + } +} + +/// The application's subject. It tells the application +/// which widgets to render and which output to produce. +/// +/// Depends on CLI arguments given by the user. +#[derive(Clone, Default, Debug, Eq, PartialEq)] +pub enum Mode { + #[default] + Operation, + #[allow(dead_code)] + Id, +} + +/// The selected issue operation returned by the operation +/// selection widget. +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub enum InboxOperation { + Show, + Clear, +} + +impl Display for InboxOperation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InboxOperation::Show => { + write!(f, "show") + } + InboxOperation::Clear => { + write!(f, "clear") + } + } + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum ListCid { + NotificationBrowser, + Context, + Shortcuts, +} + +/// All component ids known to this application. +#[derive(Debug, Default, Eq, PartialEq, Clone, Hash)] +pub enum Cid { + List(ListCid), + #[default] + GlobalListener, +} + +#[derive(Clone, Default, Debug, Eq, PartialEq)] +pub enum Message { + #[default] + Tick, + Quit(Option), + Batch(Vec), +} + +pub struct App { + context: Context, + pages: PageStack, + theme: Theme, + quit: bool, + filter: Filter, + output: Option, +} + +/// Creates a new application using a tui-realm-application, mounts all +/// components and sets focus to a default one. +#[allow(dead_code)] +impl App { + pub fn new(context: Context, filter: Filter) -> Self { + Self { + context, + pages: PageStack::default(), + theme: Theme::default(), + quit: false, + filter, + output: None, + } + } + + fn view_list( + &mut self, + app: &mut Application, + theme: &Theme, + ) -> Result<()> { + let home = Box::new(ListView::new(self.filter.clone())); + self.pages.push(home, app, &self.context, theme)?; + + Ok(()) + } + + fn process( + &mut self, + app: &mut Application, + message: Message, + ) -> Result> { + let theme = Theme::default(); + match message { + Message::Batch(messages) => { + let mut results = vec![]; + for message in messages { + if let Some(result) = self.process(app, message)? { + results.push(result); + } + } + match results.len() { + 0 => Ok(None), + 1 => Ok(Some(results[0].to_owned())), + _ => Ok(Some(Message::Batch(results))), + } + } + Message::Quit(output) => { + self.quit = true; + self.output = output; + Ok(None) + } + _ => self + .pages + .peek_mut()? + .update(app, &self.context, &theme, message), + } + } +} + +impl Tui for App { + fn init(&mut self, app: &mut Application) -> Result<()> { + self.view_list(app, &self.theme.clone())?; + + // Add global key listener and subscribe to key events + let global = tui::ui::global_listener().to_boxed(); + app.mount( + Cid::GlobalListener, + global, + vec![Sub::new( + subscription::quit_clause(tuirealm::event::Key::Char('q')), + SubClause::Always, + )], + )?; + + Ok(()) + } + + fn view(&mut self, app: &mut Application, frame: &mut Frame) { + if let Ok(page) = self.pages.peek_mut() { + page.view(app, frame); + } + } + + fn update(&mut self, app: &mut Application) -> Result { + match app.tick(PollStrategy::Once) { + Ok(messages) if !messages.is_empty() => { + for message in messages { + let mut msg = Some(message); + while msg.is_some() { + msg = self.process(app, msg.unwrap())?; + } + } + Ok(true) + } + _ => Ok(false), + } + } + + fn exit(&self) -> Option> { + if self.quit { + return Some(Exit { + value: self.output.clone(), + }); + } + None + } +} diff --git a/bin/commands/inbox/select/event.rs b/bin/commands/inbox/select/realm/event.rs similarity index 100% rename from bin/commands/inbox/select/event.rs rename to bin/commands/inbox/select/realm/event.rs diff --git a/bin/commands/inbox/select/page.rs b/bin/commands/inbox/select/realm/page.rs similarity index 100% rename from bin/commands/inbox/select/page.rs rename to bin/commands/inbox/select/realm/page.rs diff --git a/bin/commands/inbox/select/ui.rs b/bin/commands/inbox/select/realm/ui.rs similarity index 99% rename from bin/commands/inbox/select/ui.rs rename to bin/commands/inbox/select/realm/ui.rs index 455f161c..0b0a51f0 100644 --- a/bin/commands/inbox/select/ui.rs +++ b/bin/commands/inbox/select/realm/ui.rs @@ -49,7 +49,7 @@ impl NotificationBrowser { ColumnWidth::Fixed(10), ColumnWidth::Fixed(18), ]; - + let mut items = vec![]; for notification in context.notifications() { if let Ok(item) =