From 74b576ca7ced45ba85c36a21115efd682fb9dcf8 Mon Sep 17 00:00:00 2001 From: Erik Kundt Date: Thu, 4 Apr 2024 14:53:30 +0200 Subject: [PATCH] issue: Remove legacy tui-realm commands --- bin/commands/issue.rs | 43 +- bin/commands/issue/flux.rs | 2 - bin/commands/issue/realm.rs | 6 - bin/commands/issue/realm/common.rs | 2 - bin/commands/issue/realm/common/ui.rs | 162 ------ bin/commands/issue/realm/select.rs | 173 ------- bin/commands/issue/realm/select/event.rs | 172 ------- bin/commands/issue/realm/select/page.rs | 177 ------- bin/commands/issue/realm/select/ui.rs | 155 ------ bin/commands/issue/realm/suite.rs | 357 ------------- bin/commands/issue/realm/suite/event.rs | 322 ------------ bin/commands/issue/realm/suite/page.rs | 573 --------------------- bin/commands/issue/realm/suite/ui.rs | 435 ---------------- bin/commands/issue/{flux => }/select.rs | 2 +- bin/commands/issue/{flux => }/select/ui.rs | 0 15 files changed, 5 insertions(+), 2576 deletions(-) delete mode 100644 bin/commands/issue/flux.rs delete mode 100644 bin/commands/issue/realm.rs delete mode 100644 bin/commands/issue/realm/common.rs delete mode 100644 bin/commands/issue/realm/common/ui.rs delete mode 100644 bin/commands/issue/realm/select.rs delete mode 100644 bin/commands/issue/realm/select/event.rs delete mode 100644 bin/commands/issue/realm/select/page.rs delete mode 100644 bin/commands/issue/realm/select/ui.rs delete mode 100644 bin/commands/issue/realm/suite.rs delete mode 100644 bin/commands/issue/realm/suite/event.rs delete mode 100644 bin/commands/issue/realm/suite/page.rs delete mode 100644 bin/commands/issue/realm/suite/ui.rs rename bin/commands/issue/{flux => }/select.rs (99%) rename bin/commands/issue/{flux => }/select/ui.rs (100%) diff --git a/bin/commands/issue.rs b/bin/commands/issue.rs index 3eb6c401..8e99ac6d 100644 --- a/bin/commands/issue.rs +++ b/bin/commands/issue.rs @@ -1,11 +1,7 @@ #[path = "issue/common.rs"] mod common; -#[cfg(feature = "flux")] -#[path = "issue/flux.rs"] -mod flux; -#[cfg(feature = "realm")] -#[path = "issue/realm.rs"] -mod realm; +#[path = "issue/select.rs"] +mod select; use std::ffi::OsString; @@ -139,37 +135,6 @@ impl Args for Options { } } -#[cfg(feature = "realm")] -pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()> { - use tui::common::context; - use tui::realm::Window; - - pub const FPS: u64 = 60; - let (_, id) = radicle::rad::cwd() - .map_err(|_| anyhow!("this command must be run in the context of a project"))?; - - match options.op { - Operation::Select { ref opts } => { - let profile = terminal::profile()?; - let context = context::Context::new(profile, id)?.with_issues(); - - log::enable(context.profile(), "issue", "select")?; - - let mut app = realm::select::App::new(context, opts.mode.clone(), opts.filter.clone()); - 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}"); - } - } - - Ok(()) -} - -#[cfg(feature = "flux")] #[tokio::main] pub async fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()> { use radicle::storage::ReadStorage; @@ -185,13 +150,13 @@ pub async fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Resu log::enable(&profile, "issue", "select")?; - let context = flux::select::Context { + let context = select::Context { profile, repository, mode: opts.mode, filter: opts.filter.clone(), }; - let output = flux::select::App::new(context).run().await?; + let output = select::App::new(context).run().await?; let output = output .map(|o| serde_json::to_string(&o).unwrap_or_default()) diff --git a/bin/commands/issue/flux.rs b/bin/commands/issue/flux.rs deleted file mode 100644 index ffc8d2d0..00000000 --- a/bin/commands/issue/flux.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[path = "flux/select.rs"] -pub mod select; diff --git a/bin/commands/issue/realm.rs b/bin/commands/issue/realm.rs deleted file mode 100644 index 31f9ee56..00000000 --- a/bin/commands/issue/realm.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[path = "realm/common.rs"] -pub mod common; -#[path = "realm/select.rs"] -pub mod select; -#[path = "realm/suite.rs"] -pub mod suite; diff --git a/bin/commands/issue/realm/common.rs b/bin/commands/issue/realm/common.rs deleted file mode 100644 index 6c6eb76c..00000000 --- a/bin/commands/issue/realm/common.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[path = "common/ui.rs"] -pub mod ui; diff --git a/bin/commands/issue/realm/common/ui.rs b/bin/commands/issue/realm/common/ui.rs deleted file mode 100644 index c14490ca..00000000 --- a/bin/commands/issue/realm/common/ui.rs +++ /dev/null @@ -1,162 +0,0 @@ -use radicle::issue::{Issue, IssueId}; - -use tuirealm::command::{Cmd, CmdResult}; -use tuirealm::tui::layout::Rect; -use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State}; - -use radicle_tui as tui; - -use tui::common::cob::issue::Filter; -use tui::common::context::Context; -use tui::realm::ui::cob::IssueItem; -use tui::realm::ui::theme::{style, Theme}; -use tui::realm::ui::widget::context::{ContextBar, Progress}; -use tui::realm::ui::widget::label::{self}; -use tui::realm::ui::widget::list::{ColumnWidth, Table}; -use tui::realm::ui::widget::{Widget, WidgetComponent}; - -pub struct IssueBrowser { - items: Vec, - table: Widget>, -} - -impl IssueBrowser { - pub fn new( - theme: &Theme, - context: &Context, - filter: Filter, - selected: Option<(IssueId, Issue)>, - ) -> Self { - let header = [ - label::header(" ● "), - label::header("ID"), - label::header("Title"), - label::header("Author"), - label::header("Tags"), - label::header("Assignees"), - label::header("Opened"), - ]; - - let widths = [ - ColumnWidth::Fixed(3), - ColumnWidth::Fixed(7), - ColumnWidth::Grow, - ColumnWidth::Fixed(21), - ColumnWidth::Fixed(25), - ColumnWidth::Fixed(21), - ColumnWidth::Fixed(18), - ]; - - let repo = context.repository(); - let issues = context - .issues() - .as_ref() - .unwrap() - .iter() - .filter(|(_, issue)| filter.matches(context.profile(), issue)); - - let mut items = vec![]; - for (id, issue) in issues { - if let Ok(item) = IssueItem::try_from((context.profile(), repo, *id, issue.clone())) { - items.push(item); - } - } - - items.sort_by(|a, b| b.timestamp().cmp(a.timestamp())); - items.sort_by(|a, b| b.state().cmp(a.state())); - - let selected = match selected { - Some((id, issue)) => Some(IssueItem::from((context.profile(), repo, id, issue))), - _ => items.first().cloned(), - }; - - let table = Widget::new(Table::new(&items, selected, header, widths, theme.clone())); - - Self { items, table } - } - - pub fn items(&self) -> &Vec { - &self.items - } -} - -impl WidgetComponent for IssueBrowser { - fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) { - let focus = properties - .get_or(Attribute::Focus, AttrValue::Flag(false)) - .unwrap_flag(); - - self.table.attr(Attribute::Focus, AttrValue::Flag(focus)); - self.table.view(frame, area); - } - - fn state(&self) -> State { - self.table.state() - } - - fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult { - self.table.perform(cmd) - } -} - -pub fn browse_context( - context: &Context, - _theme: &Theme, - filter: Filter, - progress: Progress, -) -> Widget { - use radicle::issue::State; - - let mut open = 0; - let mut closed = 0; - - let issues = context - .issues() - .as_ref() - .unwrap() - .iter() - .filter(|(_, issue)| filter.matches(context.profile(), issue)); - - for (_, issue) in issues { - match issue.state() { - State::Open => open += 1, - State::Closed { reason: _ } => closed += 1, - } - } - - let context = label::reversable("/").style(style::magenta_reversed()); - let filter = label::default(&filter.to_string()).style(style::magenta_dim()); - - let open_n = label::default(&format!("{open}")).style(style::green()); - let open = label::default(" Open"); - - let closed_n = label::default(&format!("{closed}")).style(style::cyan()); - let closed = label::default(" Closed "); - - let progress = label::reversable(&progress.to_string()).style(style::magenta_reversed()); - - let spacer = label::default(""); - let divider = label::default(" | "); - - let context_bar = ContextBar::new( - label::group(&[context]), - label::group(&[filter]), - label::group(&[spacer.clone()]), - label::group(&[ - spacer.clone(), - spacer.clone(), - spacer.clone(), - spacer.clone(), - spacer.clone(), - spacer, - open_n, - open, - divider, - closed_n, - closed, - ]), - label::group(&[progress]), - ); - - Widget::new(context_bar).height(1) -} diff --git a/bin/commands/issue/realm/select.rs b/bin/commands/issue/realm/select.rs deleted file mode 100644 index a3e5daf2..00000000 --- a/bin/commands/issue/realm/select.rs +++ /dev/null @@ -1,173 +0,0 @@ -#[path = "select/event.rs"] -mod event; -#[path = "select/page.rs"] -mod page; -#[path = "select/ui.rs"] -mod ui; - -use std::hash::Hash; - -use anyhow::Result; -use radicle::issue::IssueId; - -use tuirealm::application::PollStrategy; -use tuirealm::event::Key; -use tuirealm::{Application, Frame, NoUserEvent, Sub, SubClause}; - -use radicle_tui as tui; - -use tui::common::cob::issue::Filter; -use tui::common::context::Context; - -use tui::realm::ui::subscription; -use tui::realm::ui::theme::Theme; -use tui::realm::{PageStack, Tui}; -use tui::Exit; - -use page::ListView; - -use super::super::common::Mode; - -type Selection = tui::Selection; - -#[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub enum ListCid { - Header, - PatchBrowser, - 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, - mode: Mode, - filter: Filter, - output: Option, -} - -/// Creates a new application using a tui-realm-application, mounts all -/// components and sets focus to a default one. -impl App { - pub fn new(context: Context, mode: Mode, filter: Filter) -> Self { - Self { - context, - pages: PageStack::default(), - theme: Theme::default(), - quit: false, - mode, - filter, - output: None, - } - } - - fn view_list( - &mut self, - app: &mut Application, - theme: &Theme, - ) -> Result<()> { - let home = Box::new(ListView::new(self.mode.clone(), 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::realm::ui::global_listener().to_boxed(); - app.mount( - Cid::GlobalListener, - global, - vec![Sub::new( - subscription::quit_clause(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/issue/realm/select/event.rs b/bin/commands/issue/realm/select/event.rs deleted file mode 100644 index d83b764b..00000000 --- a/bin/commands/issue/realm/select/event.rs +++ /dev/null @@ -1,172 +0,0 @@ -use radicle::issue::IssueId; - -use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection}; -use tuirealm::event::{Event, Key, KeyEvent}; -use tuirealm::{MockComponent, NoUserEvent}; - -use radicle_tui as tui; - -use tui::realm::ui::state::ItemState; -use tui::realm::ui::widget::container::{AppHeader, GlobalListener, LabeledContainer}; -use tui::realm::ui::widget::context::{ContextBar, Shortcuts}; -use tui::realm::ui::widget::list::PropertyList; -use tui::realm::ui::widget::Widget; - -use crate::tui_issue::common::IssueOperation; - -use super::ui::{IdSelect, OperationSelect}; -use super::Message; - -type Selection = tui::Selection; - -/// Since the framework does not know the type of messages that are being -/// passed around in the app, the following handlers need to be implemented for -/// each component used. -/// -/// TODO: should handle `Event::WindowResize`, which is not emitted by `termion`. -impl tuirealm::Component for Widget { - fn on(&mut self, event: Event) -> Option { - match event { - Event::Keyboard(KeyEvent { - code: Key::Char('q'), - .. - }) => Some(Message::Quit(None)), - _ => None, - } - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, event: Event) -> Option { - let mut submit = || -> Option { - match self.perform(Cmd::Submit) { - CmdResult::Submit(state) => { - let selected = ItemState::try_from(state).ok()?.selected()?; - let item = self.items().get(selected)?; - Some(item.id().to_owned()) - } - _ => None, - } - }; - - match event { - Event::Keyboard(KeyEvent { code: Key::Up, .. }) - | Event::Keyboard(KeyEvent { - code: Key::Char('k'), - .. - }) => { - self.perform(Cmd::Move(MoveDirection::Up)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) - | Event::Keyboard(KeyEvent { - code: Key::Char('j'), - .. - }) => { - self.perform(Cmd::Move(MoveDirection::Down)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => submit().map(|id| { - let selection = Selection { - operation: None, - ids: vec![id], - args: vec![], - }; - Message::Quit(Some(selection)) - }), - _ => None, - } - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, event: Event) -> Option { - let mut submit = || -> Option { - match self.perform(Cmd::Submit) { - CmdResult::Submit(state) => { - let selected = ItemState::try_from(state).ok()?.selected()?; - let item = self.items().get(selected)?; - Some(item.id().to_owned()) - } - _ => None, - } - }; - - match event { - Event::Keyboard(KeyEvent { code: Key::Up, .. }) - | Event::Keyboard(KeyEvent { - code: Key::Char('k'), - .. - }) => { - self.perform(Cmd::Move(MoveDirection::Up)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) - | Event::Keyboard(KeyEvent { - code: Key::Char('j'), - .. - }) => { - self.perform(Cmd::Move(MoveDirection::Down)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => submit().map(|id| { - let selection = Selection { - operation: Some(IssueOperation::Show.to_string()), - ids: vec![id], - args: vec![], - }; - Message::Quit(Some(selection)) - }), - Event::Keyboard(KeyEvent { - code: Key::Char('e'), - .. - }) => submit().map(|id| { - let selection = Selection { - operation: Some(IssueOperation::Edit.to_string()), - ids: vec![id], - args: vec![], - }; - Message::Quit(Some(selection)) - }), - _ => None, - } - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} diff --git a/bin/commands/issue/realm/select/page.rs b/bin/commands/issue/realm/select/page.rs deleted file mode 100644 index 310f22f4..00000000 --- a/bin/commands/issue/realm/select/page.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::collections::HashMap; - -use anyhow::Result; - -use tuirealm::{AttrValue, Attribute, Frame, NoUserEvent, Sub, SubClause}; - -use radicle_tui as tui; - -use tui::common::cob::issue::Filter; -use tui::common::context::Context; -use tui::realm::ui::state::ItemState; -use tui::realm::ui::theme::Theme; -use tui::realm::ui::widget::context::{Progress, Shortcuts}; -use tui::realm::ui::widget::Widget; -use tui::realm::ui::{layout, subscription}; -use tui::realm::ViewPage; - -use super::super::common; -use super::{ui, Application, Cid, ListCid, Message, Mode}; - -/// -/// Home -/// -pub struct ListView { - active_component: ListCid, - subject: Mode, - filter: Filter, - shortcuts: HashMap>, -} - -impl ListView { - pub fn new(subject: Mode, filter: Filter) -> Self { - Self { - active_component: ListCid::PatchBrowser, - subject, - filter, - shortcuts: HashMap::default(), - } - } - - fn update_context( - &self, - app: &mut Application, - context: &Context, - theme: &Theme, - ) -> Result<()> { - let state = app.state(&Cid::List(ListCid::PatchBrowser))?; - let progress = match ItemState::try_from(state) { - Ok(state) => Progress::Step( - state - .selected() - .map(|s| s.saturating_add(1)) - .unwrap_or_default(), - state.len(), - ), - Err(_) => Progress::None, - }; - - let context = common::ui::browse_context(context, theme, self.filter.clone(), progress); - - app.remount(Cid::List(ListCid::Context), context.to_boxed(), vec![])?; - - Ok(()) - } - - fn update_shortcuts( - &self, - app: &mut Application, - cid: ListCid, - ) -> Result<()> { - if let Some(shortcuts) = self.shortcuts.get(&cid) { - app.remount( - Cid::List(ListCid::Shortcuts), - shortcuts.clone().to_boxed(), - vec![], - )?; - } - - Ok(()) - } -} - -impl ViewPage for ListView { - fn mount( - &mut self, - app: &mut Application, - context: &Context, - theme: &Theme, - ) -> Result<()> { - let navigation = ui::list_navigation(theme); - let header = tui::realm::ui::app_header(context, theme, Some(navigation)).to_boxed(); - - app.remount(Cid::List(ListCid::Header), header, vec![])?; - - match self.subject { - Mode::Id => { - let patch_browser = - ui::id_select(theme, context, self.filter.clone(), None).to_boxed(); - self.shortcuts = patch_browser.as_ref().shortcuts(); - - app.remount(Cid::List(ListCid::PatchBrowser), patch_browser, vec![])?; - } - Mode::Operation => { - let patch_browser = - ui::operation_select(theme, context, self.filter.clone(), None).to_boxed(); - self.shortcuts = patch_browser.as_ref().shortcuts(); - - app.remount(Cid::List(ListCid::PatchBrowser), patch_browser, vec![])?; - } - }; - - app.active(&Cid::List(self.active_component.clone()))?; - self.update_shortcuts(app, self.active_component.clone())?; - self.update_context(app, context, theme)?; - - Ok(()) - } - - fn unmount(&self, app: &mut Application) -> Result<()> { - app.umount(&Cid::List(ListCid::Header))?; - app.umount(&Cid::List(ListCid::PatchBrowser))?; - app.umount(&Cid::List(ListCid::Context))?; - app.umount(&Cid::List(ListCid::Shortcuts))?; - Ok(()) - } - - fn update( - &mut self, - app: &mut Application, - context: &Context, - theme: &Theme, - _message: Message, - ) -> Result> { - self.update_context(app, context, theme)?; - - Ok(None) - } - - fn view(&mut self, app: &mut Application, frame: &mut Frame) { - let area = frame.size(); - let context_h = app - .query(&Cid::List(ListCid::Context), Attribute::Height) - .unwrap_or_default() - .unwrap_or(AttrValue::Size(0)) - .unwrap_size(); - let shortcuts_h = 1u16; - - let layout = layout::default_page(area, context_h, shortcuts_h); - - app.view( - &Cid::List(self.active_component.clone()), - frame, - layout.component, - ); - - app.view(&Cid::List(ListCid::Context), frame, layout.context); - app.view(&Cid::List(ListCid::Shortcuts), frame, layout.shortcuts); - } - - fn subscribe(&self, app: &mut Application) -> Result<()> { - app.subscribe( - &Cid::List(ListCid::Header), - Sub::new(subscription::navigation_clause(), SubClause::Always), - )?; - - Ok(()) - } - - fn unsubscribe(&self, app: &mut Application) -> Result<()> { - app.unsubscribe( - &Cid::List(ListCid::Header), - subscription::navigation_clause(), - )?; - - Ok(()) - } -} diff --git a/bin/commands/issue/realm/select/ui.rs b/bin/commands/issue/realm/select/ui.rs deleted file mode 100644 index bc4c9c2e..00000000 --- a/bin/commands/issue/realm/select/ui.rs +++ /dev/null @@ -1,155 +0,0 @@ -use std::collections::HashMap; - -use radicle::issue::{Issue, IssueId}; - -use tuirealm::command::{Cmd, CmdResult}; -use tuirealm::tui::layout::Rect; -use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State}; - -use radicle_tui as tui; - -use tui::common::cob::issue::Filter; -use tui::common::context::Context; -use tui::realm::ui::cob::IssueItem; -use tui::realm::ui::theme::{style, Theme}; -use tui::realm::ui::widget::container::Tabs; -use tui::realm::ui::widget::context::Shortcuts; -use tui::realm::ui::widget::label::{self}; -use tui::realm::ui::widget::{Widget, WidgetComponent}; - -use super::super::common; -use super::ListCid; - -pub struct IdSelect { - theme: Theme, - browser: Widget, -} - -impl IdSelect { - pub fn new(theme: Theme, browser: Widget) -> Self { - Self { theme, browser } - } - - pub fn items(&self) -> &Vec { - self.browser.items() - } - - pub fn shortcuts(&self) -> HashMap> { - [( - ListCid::PatchBrowser, - tui::realm::ui::shortcuts( - &self.theme, - vec![ - tui::realm::ui::shortcut(&self.theme, "enter", "select"), - tui::realm::ui::shortcut(&self.theme, "q", "quit"), - ], - ), - )] - .iter() - .cloned() - .collect() - } -} - -impl WidgetComponent for IdSelect { - fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) { - let focus = properties - .get_or(Attribute::Focus, AttrValue::Flag(false)) - .unwrap_flag(); - - self.browser.attr(Attribute::Focus, AttrValue::Flag(focus)); - self.browser.view(frame, area); - } - - fn state(&self) -> State { - self.browser.state() - } - - fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult { - self.browser.perform(cmd) - } -} - -pub struct OperationSelect { - theme: Theme, - browser: Widget, -} - -impl OperationSelect { - pub fn new(theme: Theme, browser: Widget) -> Self { - Self { theme, browser } - } - - pub fn items(&self) -> &Vec { - self.browser.items() - } - - pub fn shortcuts(&self) -> HashMap> { - [( - ListCid::PatchBrowser, - tui::realm::ui::shortcuts( - &self.theme, - vec![ - tui::realm::ui::shortcut(&self.theme, "enter", "show"), - tui::realm::ui::shortcut(&self.theme, "e", "edit"), - tui::realm::ui::shortcut(&self.theme, "q", "quit"), - ], - ), - )] - .iter() - .cloned() - .collect() - } -} - -impl WidgetComponent for OperationSelect { - fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) { - let focus = properties - .get_or(Attribute::Focus, AttrValue::Flag(false)) - .unwrap_flag(); - - self.browser.attr(Attribute::Focus, AttrValue::Flag(focus)); - self.browser.view(frame, area); - } - - fn state(&self) -> State { - self.browser.state() - } - - fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult { - self.browser.perform(cmd) - } -} - -pub fn list_navigation(theme: &Theme) -> Widget { - tui::realm::ui::tabs( - theme, - vec![label::reversable("Patches").style(style::cyan())], - ) -} - -pub fn id_select( - theme: &Theme, - context: &Context, - filter: Filter, - selected: Option<(IssueId, Issue)>, -) -> Widget { - let browser = Widget::new(common::ui::IssueBrowser::new( - theme, context, filter, selected, - )); - - Widget::new(IdSelect::new(theme.clone(), browser)) -} - -pub fn operation_select( - theme: &Theme, - context: &Context, - filter: Filter, - selected: Option<(IssueId, Issue)>, -) -> Widget { - let browser = Widget::new(common::ui::IssueBrowser::new( - theme, context, filter, selected, - )); - - Widget::new(OperationSelect::new(theme.clone(), browser)) -} diff --git a/bin/commands/issue/realm/suite.rs b/bin/commands/issue/realm/suite.rs deleted file mode 100644 index df90bb89..00000000 --- a/bin/commands/issue/realm/suite.rs +++ /dev/null @@ -1,357 +0,0 @@ -#[path = "suite/event.rs"] -mod event; -#[path = "suite/page.rs"] -mod page; -#[path = "suite/ui.rs"] -mod ui; - -use anyhow::Result; - -use radicle::cob::issue::IssueId; - -use tuirealm::application::PollStrategy; -use tuirealm::event::Key; -use tuirealm::{Application, Frame, NoUserEvent, Sub, SubClause}; - -use radicle_tui as tui; - -use tui::common::cob; -use tui::common::context::Context; -use tui::realm::ui::subscription; -use tui::realm::ui::theme::Theme; -use tui::realm::{PageStack, Tui}; -use tui::Exit; - -use page::{IssuePage, ListPage}; - -#[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub enum ListCid { - Header, - IssueBrowser, - Context, - Shortcuts, -} - -#[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub enum IssueCid { - Header, - List, - Details, - Context, - Form, - Shortcuts, -} - -/// All component ids known to this application. -#[derive(Debug, Default, Eq, PartialEq, Clone, Hash)] -pub enum Cid { - List(ListCid), - Issue(IssueCid), - #[default] - GlobalListener, - Popup, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum IssueCobMessage { - Create { - title: String, - tags: String, - assignees: String, - description: String, - }, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum IssueMessage { - Show(Option), - Changed(IssueId), - Focus(IssueCid), - Created(IssueId), - Cob(IssueCobMessage), - Reload(Option), - OpenForm, - HideForm, - Leave(Option), -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum PopupMessage { - Info(String), - Warning(String), - Error(String), - Hide, -} - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub enum Message { - Issue(IssueMessage), - NavigationChanged(u16), - FormSubmitted(String), - Popup(PopupMessage), - #[default] - Tick, - Quit, - Batch(Vec), -} - -#[allow(dead_code)] -pub struct App { - context: Context, - pages: PageStack, - theme: Theme, - quit: bool, -} - -/// 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) -> Self { - Self { - context, - pages: PageStack::default(), - theme: Theme::default(), - quit: false, - } - } - - fn view_list( - &mut self, - app: &mut Application, - theme: &Theme, - ) -> Result<()> { - let list = Box::new(ListPage::new(theme.clone())); - self.pages.push(list, app, &self.context, theme)?; - - Ok(()) - } - - fn view_issue( - &mut self, - app: &mut Application, - id: Option, - theme: &Theme, - ) -> Result<()> { - let repo = self.context.repository(); - match id { - Some(id) => { - let profile = self.context.profile(); - if let Some(issue) = cob::issue::find(profile, repo, &id)? { - let view = Box::new(IssuePage::new(&self.context, theme, Some((id, issue)))); - self.pages.push(view, app, &self.context, theme)?; - - Ok(()) - } else { - Err(anyhow::anyhow!( - "Could not mount 'page::IssueView'. Issue not found." - )) - } - } - None => { - let view = Box::new(IssuePage::new(&self.context, theme, None)); - self.pages.push(view, 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::Issue(IssueMessage::Cob(IssueCobMessage::Create { - title, - tags, - assignees, - description, - })) => match self.create_issue(title, description, tags, assignees) { - Ok(id) => { - self.context.reload_issues(); - - Ok(Some(Message::Batch(vec![ - Message::Issue(IssueMessage::HideForm), - Message::Issue(IssueMessage::Created(id)), - ]))) - } - Err(err) => { - let error = format!("{:?}", err); - self.show_error_popup(app, &theme, &error)?; - - Ok(None) - } - }, - Message::Issue(IssueMessage::Show(id)) => { - self.view_issue(app, id, &theme)?; - Ok(None) - } - Message::Issue(IssueMessage::Leave(id)) => { - self.pages.pop(app)?; - Ok(Some(Message::Issue(IssueMessage::Reload(id)))) - } - Message::Popup(PopupMessage::Info(info)) => { - self.show_info_popup(app, &theme, &info)?; - Ok(None) - } - Message::Popup(PopupMessage::Warning(warning)) => { - self.show_warning_popup(app, &theme, &warning)?; - Ok(None) - } - Message::Popup(PopupMessage::Error(error)) => { - self.show_error_popup(app, &theme, &error)?; - Ok(None) - } - Message::Popup(PopupMessage::Hide) => { - self.hide_popup(app)?; - Ok(None) - } - Message::Quit => { - self.quit = true; - Ok(None) - } - _ => self - .pages - .peek_mut()? - .update(app, &self.context, &theme, message), - } - } - - fn show_info_popup( - &mut self, - app: &mut Application, - theme: &Theme, - message: &str, - ) -> Result<()> { - let popup = tui::realm::ui::info(theme, message); - app.remount(Cid::Popup, popup.to_boxed(), vec![])?; - app.active(&Cid::Popup)?; - - Ok(()) - } - - fn show_warning_popup( - &mut self, - app: &mut Application, - theme: &Theme, - message: &str, - ) -> Result<()> { - let popup = tui::realm::ui::warning(theme, message); - app.remount(Cid::Popup, popup.to_boxed(), vec![])?; - app.active(&Cid::Popup)?; - - Ok(()) - } - - fn show_error_popup( - &mut self, - app: &mut Application, - theme: &Theme, - message: &str, - ) -> Result<()> { - let popup = tui::realm::ui::error(theme, message); - app.remount(Cid::Popup, popup.to_boxed(), vec![])?; - app.active(&Cid::Popup)?; - - Ok(()) - } - - fn hide_popup(&mut self, app: &mut Application) -> Result<()> { - app.blur()?; - app.umount(&Cid::Popup)?; - - Ok(()) - } - - fn create_issue( - &mut self, - title: String, - description: String, - labels: String, - assignees: String, - ) -> Result { - let profile = self.context.profile(); - let repository = self.context.repository(); - let signer = self.context.signer().as_ref().unwrap(); - - let labels = cob::parse_labels(labels)?; - let assignees = cob::parse_assignees(assignees)?; - - cob::issue::create( - profile, - repository, - signer, - title, - description, - labels.as_slice(), - assignees.as_slice(), - ) - } -} - -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::realm::ui::global_listener().to_boxed(); - app.mount( - Cid::GlobalListener, - global, - vec![Sub::new( - subscription::quit_clause(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); - } - - if app.mounted(&Cid::Popup) { - app.view(&Cid::Popup, frame, frame.size()); - } - } - - 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: None }); - } - None - } -} diff --git a/bin/commands/issue/realm/suite/event.rs b/bin/commands/issue/realm/suite/event.rs deleted file mode 100644 index d8d2e7ab..00000000 --- a/bin/commands/issue/realm/suite/event.rs +++ /dev/null @@ -1,322 +0,0 @@ -use radicle::cob::issue::IssueId; - -use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection, Position}; -use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers}; -use tuirealm::{MockComponent, NoUserEvent, State, StateValue}; - -use radicle_tui as tui; - -use tui::realm::ui::state::ItemState; -use tui::realm::ui::widget::container::{AppHeader, GlobalListener, LabeledContainer, Popup}; -use tui::realm::ui::widget::context::{ContextBar, Shortcuts}; -use tui::realm::ui::widget::form::Form; -use tui::realm::ui::widget::list::PropertyList; -use tui::realm::ui::widget::Widget; - -use super::ui; -use super::{IssueCid, IssueMessage, Message, PopupMessage}; - -/// Since the framework does not know the type of messages that are being -/// passed around in the app, the following handlers need to be implemented for -/// each component used. -/// -/// TODO: should handle `Event::WindowResize`, which is not emitted by `termion`. -impl tuirealm::Component for Widget { - fn on(&mut self, event: Event) -> Option { - match event { - Event::Keyboard(KeyEvent { - code: Key::Char('q'), - .. - }) => Some(Message::Quit), - _ => None, - } - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, event: Event) -> Option { - match event { - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - match self.perform(Cmd::Move(MoveDirection::Right)) { - CmdResult::Changed(State::One(StateValue::U16(index))) => { - Some(Message::NavigationChanged(index)) - } - _ => None, - } - } - _ => None, - } - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, event: Event) -> Option { - match event { - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - let selected = ItemState::try_from(self.state()).ok()?.selected()?; - let item = self.items().get(selected)?; - - Some(Message::Issue(IssueMessage::Leave(Some( - item.id().to_owned(), - )))) - } - Event::Keyboard(KeyEvent { code: Key::Up, .. }) - | Event::Keyboard(KeyEvent { - code: Key::Char('k'), - .. - }) => match self.perform(Cmd::Move(MoveDirection::Up)) { - CmdResult::Changed(state) => { - let selected = ItemState::try_from(state).ok()?.selected()?; - let item = self.items().get(selected)?; - - Some(Message::Issue(IssueMessage::Changed(item.id().to_owned()))) - } - _ => None, - }, - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) - | Event::Keyboard(KeyEvent { - code: Key::Char('j'), - .. - }) => match self.perform(Cmd::Move(MoveDirection::Down)) { - CmdResult::Changed(state) => { - let selected = ItemState::try_from(state).ok()?.selected()?; - let item = self.items().get(selected)?; - Some(Message::Issue(IssueMessage::Changed(item.id().to_owned()))) - } - _ => None, - }, - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => Some(Message::Issue(IssueMessage::Focus(IssueCid::Details))), - Event::Keyboard(KeyEvent { - code: Key::Char('o'), - .. - }) => Some(Message::Issue(IssueMessage::OpenForm)), - _ => None, - } - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, event: Event) -> Option { - match event { - Event::Keyboard(KeyEvent { code: Key::Up, .. }) - | Event::Keyboard(KeyEvent { - code: Key::Char('k'), - .. - }) => { - self.perform(Cmd::Scroll(MoveDirection::Up)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) - | Event::Keyboard(KeyEvent { - code: Key::Char('j'), - .. - }) => { - self.perform(Cmd::Scroll(MoveDirection::Down)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Message::Issue(IssueMessage::Focus(IssueCid::List))) - } - _ => None, - } - } -} - -impl tuirealm::Component for Widget
{ - fn on(&mut self, event: Event) -> Option { - match event { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(MoveDirection::Left)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(MoveDirection::Right)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { - self.perform(Cmd::Move(MoveDirection::Up)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => { - self.perform(Cmd::Move(MoveDirection::Down)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => { - self.perform(Cmd::Custom(Form::CMD_NEWLINE)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Char('s'), - modifiers: KeyModifiers::CONTROL, - }) => { - self.perform(Cmd::Submit); - self.query(tuirealm::Attribute::Custom(Form::PROP_ID)) - .map(|cid| Message::FormSubmitted(cid.unwrap_string())) - } - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Message::Issue(IssueMessage::HideForm)) - } - Event::Keyboard(KeyEvent { - code: Key::BackTab, .. - }) => { - self.perform(Cmd::Custom(Form::CMD_FOCUS_PREVIOUS)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - self.perform(Cmd::Custom(Form::CMD_FOCUS_NEXT)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - modifiers: KeyModifiers::SHIFT, - }) => { - self.perform(Cmd::Type(ch.to_ascii_uppercase())); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Char('v'), - modifiers: KeyModifiers::CONTROL, - }) => { - self.perform(Cmd::Custom(Form::CMD_PASTE)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - .. - }) => { - self.perform(Cmd::Type(ch)); - Some(Message::Tick) - } - _ => None, - } - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, event: Event) -> Option { - let mut submit = || -> Option { - match self.perform(Cmd::Submit) { - CmdResult::Submit(state) => { - let selected = ItemState::try_from(state).ok()?.selected()?; - let item = self.items().get(selected)?; - Some(item.id().to_owned()) - } - _ => None, - } - }; - - match event { - Event::Keyboard(KeyEvent { code: Key::Up, .. }) - | Event::Keyboard(KeyEvent { - code: Key::Char('k'), - .. - }) => { - self.perform(Cmd::Move(MoveDirection::Up)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) - | Event::Keyboard(KeyEvent { - code: Key::Char('j'), - .. - }) => { - self.perform(Cmd::Move(MoveDirection::Down)); - Some(Message::Tick) - } - Event::Keyboard(KeyEvent { - code: Key::Char('o'), - .. - }) => { - let id = submit(); - Some(Message::Batch(vec![ - Message::Issue(IssueMessage::Show(id)), - Message::Issue(IssueMessage::OpenForm), - ])) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => { - let id = submit(); - if id.is_some() { - Some(Message::Issue(IssueMessage::Show(id))) - } else { - None - } - } - _ => None, - } - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, event: Event) -> Option { - match event { - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Message::Popup(PopupMessage::Hide)) - } - _ => None, - } - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} - -impl tuirealm::Component for Widget { - fn on(&mut self, _event: Event) -> Option { - None - } -} diff --git a/bin/commands/issue/realm/suite/page.rs b/bin/commands/issue/realm/suite/page.rs deleted file mode 100644 index 0abdd84f..00000000 --- a/bin/commands/issue/realm/suite/page.rs +++ /dev/null @@ -1,573 +0,0 @@ -use std::collections::HashMap; - -use anyhow::Result; - -use radicle::cob::issue::{Issue, IssueId}; - -use tuirealm::event::Key; -use tuirealm::{AttrValue, Attribute, Frame, NoUserEvent, State, StateValue, Sub, SubClause}; - -use radicle_tui as tui; - -use tui::common::cob; -use tui::common::context::Context; -use tui::realm::ui::layout; -use tui::realm::ui::state::ItemState; -use tui::realm::ui::theme::Theme; -use tui::realm::ui::widget::context::{Progress, Shortcuts}; -use tui::realm::ui::widget::Widget; -use tui::realm::ViewPage; - -use super::{ - Application, Cid, IssueCid, IssueCobMessage, IssueMessage, ListCid, Message, PopupMessage, -}; - -use super::subscription; -use super::ui; - -/// -/// Home -/// -pub struct ListPage { - active_component: ListCid, - shortcuts: HashMap>, -} - -impl ListPage { - pub fn new(theme: Theme) -> Self { - let shortcuts = Self::build_shortcuts(&theme); - Self { - active_component: ListCid::IssueBrowser, - shortcuts, - } - } - - fn activate( - &mut self, - app: &mut Application, - cid: ListCid, - ) -> Result<()> { - self.active_component = cid; - let cid = Cid::List(self.active_component.clone()); - app.active(&cid)?; - app.attr(&cid, Attribute::Focus, AttrValue::Flag(true))?; - - Ok(()) - } - - fn build_shortcuts(theme: &Theme) -> HashMap> { - [( - ListCid::IssueBrowser, - tui::realm::ui::shortcuts( - theme, - vec![ - tui::realm::ui::shortcut(theme, "tab", "section"), - tui::realm::ui::shortcut(theme, "↑/↓", "navigate"), - tui::realm::ui::shortcut(theme, "enter", "show"), - tui::realm::ui::shortcut(theme, "o", "open"), - tui::realm::ui::shortcut(theme, "q", "quit"), - ], - ), - )] - .iter() - .cloned() - .collect() - } - - fn update_context( - &self, - app: &mut Application, - context: &Context, - theme: &Theme, - ) -> Result<()> { - let state = app.state(&Cid::List(ListCid::IssueBrowser))?; - let progress = match ItemState::try_from(state) { - Ok(state) => Progress::Step( - state - .selected() - .map(|s| s.saturating_add(1)) - .unwrap_or_default(), - state.len(), - ), - Err(_) => Progress::None, - }; - let context = ui::browse_context(context, theme, progress); - - app.remount(Cid::List(ListCid::Context), context.to_boxed(), vec![])?; - - Ok(()) - } - - fn update_shortcuts( - &self, - app: &mut Application, - cid: ListCid, - ) -> Result<()> { - if let Some(shortcuts) = self.shortcuts.get(&cid) { - app.remount( - Cid::List(ListCid::Shortcuts), - shortcuts.clone().to_boxed(), - vec![], - )?; - } - Ok(()) - } -} - -impl ViewPage for ListPage { - fn mount( - &mut self, - app: &mut Application, - context: &Context, - theme: &Theme, - ) -> Result<()> { - let navigation = ui::list_navigation(theme); - let header = tui::realm::ui::app_header(context, theme, Some(navigation)).to_boxed(); - let issue_browser = ui::issues(context, theme, None).to_boxed(); - - app.remount(Cid::List(ListCid::Header), header, vec![])?; - app.remount(Cid::List(ListCid::IssueBrowser), issue_browser, vec![])?; - - app.active(&Cid::List(self.active_component.clone()))?; - self.update_shortcuts(app, self.active_component.clone())?; - self.update_context(app, context, theme)?; - - Ok(()) - } - - fn unmount(&self, app: &mut Application) -> Result<()> { - app.umount(&Cid::List(ListCid::Header))?; - app.umount(&Cid::List(ListCid::IssueBrowser))?; - app.umount(&Cid::List(ListCid::Context))?; - app.umount(&Cid::List(ListCid::Shortcuts))?; - Ok(()) - } - - fn update( - &mut self, - app: &mut Application, - context: &Context, - theme: &Theme, - message: Message, - ) -> Result> { - if let Message::Issue(IssueMessage::Reload(id)) = message { - let selected = match id { - Some(id) => cob::issue::find(context.profile(), context.repository(), &id)? - .map(|issue| (id, issue)), - _ => None, - }; - - let issue_browser = ui::issues(context, theme, selected).to_boxed(); - app.remount(Cid::List(ListCid::IssueBrowser), issue_browser, vec![])?; - - self.activate(app, ListCid::IssueBrowser)?; - } - - self.update_context(app, context, theme)?; - - Ok(None) - } - - fn view(&mut self, app: &mut Application, frame: &mut Frame) { - let area = frame.size(); - let context_h = app - .query(&Cid::List(ListCid::Context), Attribute::Height) - .unwrap_or_default() - .unwrap_or(AttrValue::Size(0)) - .unwrap_size(); - let shortcuts_h = 1u16; - - let layout = layout::full_page(area, context_h, shortcuts_h); - - app.view(&Cid::List(ListCid::Header), frame, layout.navigation); - app.view( - &Cid::List(self.active_component.clone()), - frame, - layout.component, - ); - - app.view(&Cid::List(ListCid::Shortcuts), frame, layout.shortcuts); - } - - fn subscribe(&self, app: &mut Application) -> Result<()> { - app.subscribe( - &Cid::List(ListCid::Header), - Sub::new(subscription::navigation_clause(), SubClause::Always), - )?; - - Ok(()) - } - - fn unsubscribe(&self, app: &mut Application) -> Result<()> { - app.unsubscribe( - &Cid::List(ListCid::Header), - subscription::navigation_clause(), - )?; - - Ok(()) - } -} - -/// -/// Issue detail page -/// -pub struct IssuePage { - issue: Option<(IssueId, Issue)>, - active_component: IssueCid, - shortcuts: HashMap>, -} - -impl IssuePage { - pub fn new(_context: &Context, theme: &Theme, issue: Option<(IssueId, Issue)>) -> Self { - let shortcuts = Self::build_shortcuts(theme); - let active_component = IssueCid::List; - - Self { - issue, - active_component, - shortcuts, - } - } - - fn activate( - &mut self, - app: &mut Application, - cid: IssueCid, - ) -> Result<()> { - self.active_component = cid; - let cid = Cid::Issue(self.active_component.clone()); - app.active(&cid)?; - app.attr(&cid, Attribute::Focus, AttrValue::Flag(true))?; - - Ok(()) - } - - fn build_shortcuts(theme: &Theme) -> HashMap> { - [ - ( - IssueCid::List, - tui::realm::ui::shortcuts( - theme, - vec![ - tui::realm::ui::shortcut(theme, "esc", "back"), - tui::realm::ui::shortcut(theme, "↑/↓", "navigate"), - tui::realm::ui::shortcut(theme, "enter", "show"), - tui::realm::ui::shortcut(theme, "o", "open"), - tui::realm::ui::shortcut(theme, "q", "quit"), - ], - ), - ), - ( - IssueCid::Details, - tui::realm::ui::shortcuts( - theme, - vec![ - tui::realm::ui::shortcut(theme, "esc", "back"), - tui::realm::ui::shortcut(theme, "↑/↓", "scroll"), - tui::realm::ui::shortcut(theme, "q", "quit"), - ], - ), - ), - ( - IssueCid::Form, - tui::realm::ui::shortcuts( - theme, - vec![ - tui::realm::ui::shortcut(theme, "esc", "back"), - tui::realm::ui::shortcut(theme, "shift + tab / tab", "navigate"), - tui::realm::ui::shortcut(theme, "ctrl + s", "submit"), - ], - ), - ), - ] - .iter() - .cloned() - .collect() - } - - fn update_context( - &self, - app: &mut Application, - context: &Context, - theme: &Theme, - cid: IssueCid, - ) -> Result<()> { - let context = match cid { - IssueCid::List => { - let state = app.state(&Cid::Issue(IssueCid::List))?; - let progress = match state { - State::Tup2((StateValue::Usize(step), StateValue::Usize(total))) => { - Progress::Step(step.saturating_add(1), total) - } - _ => Progress::None, - }; - let context = ui::browse_context(context, theme, progress); - Some(context) - } - IssueCid::Details => { - let state = app.state(&Cid::Issue(IssueCid::Details))?; - let progress = match state { - State::One(StateValue::Usize(scroll)) => Progress::Percentage(scroll), - _ => Progress::None, - }; - let context = ui::description_context(context, theme, progress); - Some(context) - } - IssueCid::Form => { - let context = ui::form_context(context, theme, Progress::None); - Some(context) - } - _ => None, - }; - - if let Some(context) = context { - app.remount(Cid::Issue(IssueCid::Context), context.to_boxed(), vec![])?; - } - - Ok(()) - } - - fn update_shortcuts( - &self, - app: &mut Application, - cid: IssueCid, - ) -> Result<()> { - if let Some(shortcuts) = self.shortcuts.get(&cid) { - app.remount( - Cid::Issue(IssueCid::Shortcuts), - shortcuts.clone().to_boxed(), - vec![], - )?; - } - Ok(()) - } -} - -impl ViewPage for IssuePage { - fn mount( - &mut self, - app: &mut Application, - context: &Context, - theme: &Theme, - ) -> Result<()> { - let navigation = ui::list_navigation(theme); - let header = tui::realm::ui::app_header(context, theme, Some(navigation)).to_boxed(); - let list = ui::list(context, theme, self.issue.clone()).to_boxed(); - - app.remount(Cid::Issue(IssueCid::Header), header, vec![])?; - app.remount(Cid::Issue(IssueCid::List), list, vec![])?; - - if let Some((id, issue)) = &self.issue { - let comments = issue.comments().collect::>(); - let details = ui::details( - context, - theme, - (*id, issue.clone()), - comments.first().copied(), - ) - .to_boxed(); - app.remount(Cid::Issue(IssueCid::Details), details, vec![])?; - } - - app.active(&Cid::Issue(self.active_component.clone()))?; - - self.update_shortcuts(app, self.active_component.clone())?; - self.update_context(app, context, theme, self.active_component.clone())?; - - Ok(()) - } - - fn unmount(&self, app: &mut Application) -> Result<()> { - app.umount(&Cid::Issue(IssueCid::Header))?; - app.umount(&Cid::Issue(IssueCid::List))?; - app.umount(&Cid::Issue(IssueCid::Context))?; - app.umount(&Cid::Issue(IssueCid::Shortcuts))?; - - if app.mounted(&Cid::Issue(IssueCid::Details)) { - app.umount(&Cid::Issue(IssueCid::Details))?; - } - - if app.mounted(&Cid::Issue(IssueCid::Form)) { - app.umount(&Cid::Issue(IssueCid::Form))?; - } - - Ok(()) - } - - fn update( - &mut self, - app: &mut Application, - context: &Context, - theme: &Theme, - message: Message, - ) -> Result> { - match message { - Message::Issue(IssueMessage::Created(id)) => { - let profile = context.profile(); - let repo = context.repository(); - - if let Some(issue) = cob::issue::find(profile, repo, &id)? { - self.issue = Some((id, issue.clone())); - let list = ui::list(context, theme, self.issue.clone()).to_boxed(); - let comments = issue.comments().collect::>(); - - let details = ui::details( - context, - theme, - (id, issue.clone()), - comments.first().copied(), - ) - .to_boxed(); - - app.remount(Cid::Issue(IssueCid::List), list, vec![])?; - app.remount(Cid::Issue(IssueCid::Details), details, vec![])?; - } - } - Message::Issue(IssueMessage::Changed(id)) => { - let profile = context.profile(); - let repo = context.repository(); - - if let Some(issue) = cob::issue::find(profile, repo, &id)? { - self.issue = Some((id, issue.clone())); - let comments = issue.comments().collect::>(); - let details = ui::details( - context, - theme, - (id, issue.clone()), - comments.first().copied(), - ) - .to_boxed(); - app.remount(Cid::Issue(IssueCid::Details), details, vec![])?; - } - } - Message::Issue(IssueMessage::Focus(cid)) => { - self.activate(app, cid)?; - self.update_shortcuts(app, self.active_component.clone())?; - } - Message::Issue(IssueMessage::OpenForm) => { - let new_form = ui::new_form(context, theme).to_boxed(); - let list = ui::list(context, theme, None).to_boxed(); - - app.remount(Cid::Issue(IssueCid::List), list, vec![])?; - app.remount(Cid::Issue(IssueCid::Form), new_form, vec![])?; - app.active(&Cid::Issue(IssueCid::Form))?; - - app.unsubscribe( - &Cid::GlobalListener, - subscription::quit_clause(Key::Char('q')), - )?; - - return Ok(Some(Message::Issue(IssueMessage::Focus(IssueCid::Form)))); - } - Message::Issue(IssueMessage::HideForm) => { - app.umount(&Cid::Issue(IssueCid::Form))?; - - let list = ui::list(context, theme, self.issue.clone()).to_boxed(); - app.remount(Cid::Issue(IssueCid::List), list, vec![])?; - - app.subscribe( - &Cid::GlobalListener, - Sub::new(subscription::quit_clause(Key::Char('q')), SubClause::Always), - )?; - - if self.issue.is_none() { - return Ok(Some(Message::Issue(IssueMessage::Leave(None)))); - } - return Ok(Some(Message::Issue(IssueMessage::Focus(IssueCid::List)))); - } - Message::FormSubmitted(id) => { - if id == ui::FORM_ID_EDIT { - let state = app.state(&Cid::Issue(IssueCid::Form))?; - if let State::Linked(mut states) = state { - let mut missing_values = vec![]; - - let title = match states.front() { - Some(State::One(StateValue::String(title))) if !title.is_empty() => { - Some(title.clone()) - } - _ => None, - }; - states.pop_front(); - - let tags = match states.front() { - Some(State::One(StateValue::String(tags))) => Some(tags.clone()), - _ => Some(String::from("[]")), - }; - states.pop_front(); - - let assignees = match states.front() { - Some(State::One(StateValue::String(assignees))) => { - Some(assignees.clone()) - } - _ => Some(String::from("[]")), - }; - states.pop_front(); - - let description = match states.front() { - Some(State::One(StateValue::String(description))) - if !description.is_empty() => - { - Some(description.clone()) - } - _ => None, - }; - states.pop_front(); - - if title.is_none() { - missing_values.push("title"); - } - if description.is_none() { - missing_values.push("description"); - } - - // show error popup if missing. - if !missing_values.is_empty() { - let error = format!("Missing fields: {:?}", missing_values); - return Ok(Some(Message::Popup(PopupMessage::Error(error)))); - } else { - return Ok(Some(Message::Issue(IssueMessage::Cob( - IssueCobMessage::Create { - title: title.unwrap(), - tags: tags.unwrap(), - assignees: assignees.unwrap(), - description: description.unwrap(), - }, - )))); - } - } - } - } - _ => {} - } - - self.update_context(app, context, theme, self.active_component.clone())?; - - Ok(None) - } - - fn view(&mut self, app: &mut Application, frame: &mut Frame) { - let area = frame.size(); - let shortcuts_h = 1u16; - let layout = layout::issue_page(area, shortcuts_h); - - app.view(&Cid::Issue(IssueCid::Header), frame, layout.header); - app.view(&Cid::Issue(IssueCid::List), frame, layout.left); - - if app.mounted(&Cid::Issue(IssueCid::Form)) { - app.view(&Cid::Issue(IssueCid::Form), frame, layout.right); - } else if app.mounted(&Cid::Issue(IssueCid::Details)) { - app.view(&Cid::Issue(IssueCid::Details), frame, layout.right); - } - - app.view(&Cid::Issue(IssueCid::Context), frame, layout.context); - app.view(&Cid::Issue(IssueCid::Shortcuts), frame, layout.shortcuts); - } - - fn subscribe(&self, _app: &mut Application) -> Result<()> { - Ok(()) - } - - fn unsubscribe(&self, _app: &mut Application) -> Result<()> { - Ok(()) - } -} diff --git a/bin/commands/issue/realm/suite/ui.rs b/bin/commands/issue/realm/suite/ui.rs deleted file mode 100644 index 79d5c167..00000000 --- a/bin/commands/issue/realm/suite/ui.rs +++ /dev/null @@ -1,435 +0,0 @@ -use radicle::node::AliasStore; - -use radicle::cob::thread::Comment; -use radicle::cob::thread::CommentId; - -use radicle::cob::issue::Issue; -use radicle::cob::issue::IssueId; - -use tuirealm::command::{Cmd, CmdResult}; -use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect}; -use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State}; - -use radicle_tui as tui; - -use tui::common::context::Context; -use tui::realm::ui::cob; -use tui::realm::ui::cob::IssueItem; -use tui::realm::ui::theme::style; -use tui::realm::ui::theme::Theme; -use tui::realm::ui::widget::container::{Container, Tabs}; -use tui::realm::ui::widget::context::{ContextBar, Progress}; -use tui::realm::ui::widget::form::{Form, TextArea, TextField}; -use tui::realm::ui::widget::label::{self, Textarea}; -use tui::realm::ui::widget::list::{ColumnWidth, List, Property, Table}; -use tui::realm::ui::widget::{Widget, WidgetComponent}; - -pub const FORM_ID_EDIT: &str = "edit-form"; - -pub struct IssueBrowser { - items: Vec, - table: Widget>, -} - -impl IssueBrowser { - pub fn new(context: &Context, theme: &Theme, selected: Option<(IssueId, Issue)>) -> Self { - let header = [ - label::header(" ● "), - label::header("ID"), - label::header("Title"), - label::header("Author"), - label::header("Tags"), - label::header("Assignees"), - label::header("Opened"), - ]; - - let widths = [ - ColumnWidth::Fixed(3), - ColumnWidth::Fixed(7), - ColumnWidth::Grow, - ColumnWidth::Fixed(21), - ColumnWidth::Fixed(25), - ColumnWidth::Fixed(21), - ColumnWidth::Fixed(18), - ]; - - let repo = context.repository(); - let mut items = vec![]; - - let issues = context.issues().as_ref().unwrap(); - for (id, issue) in issues { - if let Ok(item) = IssueItem::try_from((context.profile(), repo, *id, issue.clone())) { - items.push(item); - } - } - - items.sort_by(|a, b| b.timestamp().cmp(a.timestamp())); - items.sort_by(|a, b| b.state().cmp(a.state())); - - let selected = match selected { - Some((id, issue)) => Some(IssueItem::from((context.profile(), repo, id, issue))), - _ => items.first().cloned(), - }; - - let table = Widget::new(Table::new(&items, selected, header, widths, theme.clone())); - - Self { items, table } - } - - pub fn items(&self) -> &Vec { - &self.items - } -} - -impl WidgetComponent for IssueBrowser { - fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) { - let focus = properties - .get_or(Attribute::Focus, AttrValue::Flag(false)) - .unwrap_flag(); - - self.table.attr(Attribute::Focus, AttrValue::Flag(focus)); - self.table.view(frame, area); - } - - fn state(&self) -> State { - self.table.state() - } - - fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult { - self.table.perform(cmd) - } -} - -pub struct LargeList { - items: Vec, - list: Widget, -} - -impl LargeList { - pub fn new(context: &Context, theme: &Theme, selected: Option<(IssueId, Issue)>) -> Self { - let repo = context.repository(); - - let issues = context.issues().as_ref().unwrap(); - let mut items = issues - .iter() - .map(|(id, issue)| IssueItem::from((context.profile(), repo, *id, issue.clone()))) - .collect::>(); - - items.sort_by(|a, b| b.timestamp().cmp(a.timestamp())); - items.sort_by(|a, b| b.state().cmp(a.state())); - - let selected = - selected.map(|(id, issue)| IssueItem::from((context.profile(), repo, id, issue))); - - let list = Widget::new(List::new(&items, selected, theme.clone())); - - let container = tui::realm::ui::container(theme, list.to_boxed()); - - Self { - items, - list: container, - } - } - - pub fn items(&self) -> &Vec { - &self.items - } -} - -impl WidgetComponent for LargeList { - fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) { - let focus = properties - .get_or(Attribute::Focus, AttrValue::Flag(false)) - .unwrap_flag(); - - self.list.attr(Attribute::Focus, AttrValue::Flag(focus)); - self.list.view(frame, area); - } - - fn state(&self) -> State { - self.list.state() - } - - fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult { - self.list.perform(cmd) - } -} - -pub struct IssueHeader { - container: Widget, -} - -impl IssueHeader { - pub fn new(context: &Context, theme: &Theme, issue: (IssueId, Issue)) -> Self { - let repo = context.repository(); - - let (id, issue) = issue; - let author = issue.author(); - let author = author.id(); - let alias = context.profile().aliases().alias(author); - let by_you = *author == context.profile().did(); - let item = IssueItem::from((context.profile(), repo, id, issue.clone())); - - let title = Property::new(label::property("Title"), label::default(item.title())); - - let author = match alias { - Some(_) => label::alias(&cob::format_author(issue.author().id(), &alias, by_you)), - None => label::did(&cob::format_author(issue.author().id(), &alias, by_you)), - }; - let author = Property::new(label::property("Author"), author); - - let issue_id = Property::new( - label::property("Issue"), - label::default(&id.to_string()).style(style::gray()), - ); - - let labels = Property::new( - label::property("Labels"), - label::labels(&cob::format_labels(item.labels())), - ); - - let assignees = Property::new( - label::property("Assignees"), - label::did(&cob::format_assignees( - &item - .assignees() - .iter() - .map(|item| (item.did(), item.alias(), item.is_you())) - .collect::>(), - )), - ); - - let state = Property::new( - label::property("Status"), - label::default(&item.state().to_string()), - ); - - let table = tui::realm::ui::property_table( - theme, - vec![ - Widget::new(title), - Widget::new(issue_id), - Widget::new(author), - Widget::new(labels), - Widget::new(assignees), - Widget::new(state), - ], - ); - let container = tui::realm::ui::container(theme, table.to_boxed()); - - Self { container } - } -} - -impl WidgetComponent for IssueHeader { - fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) { - self.container.view(frame, area); - } - - fn state(&self) -> State { - State::None - } - - fn perform(&mut self, _properties: &Props, _cmd: Cmd) -> CmdResult { - CmdResult::None - } -} - -pub struct IssueDetails { - header: Widget, - description: Widget, -} - -impl IssueDetails { - pub fn new( - context: &Context, - theme: &Theme, - issue: (IssueId, Issue), - description: Option<(&CommentId, &Comment)>, - ) -> Self { - Self { - header: header(context, theme, issue), - description: self::description(context, theme, description), - } - } -} - -impl WidgetComponent for IssueDetails { - fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) { - let focus = properties - .get_or(Attribute::Focus, AttrValue::Flag(false)) - .unwrap_flag(); - let layout = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(8), Constraint::Min(1)]) - .split(area); - - self.header.view(frame, layout[0]); - - self.description - .attr(Attribute::Focus, AttrValue::Flag(focus)); - self.description.view(frame, layout[1]); - } - - fn state(&self) -> State { - self.description.state() - } - - fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult { - self.description.perform(cmd) - } -} - -pub struct CommentBody { - textarea: Widget, -} - -impl CommentBody { - pub fn new(_context: &Context, theme: &Theme, comment: Option<(&CommentId, &Comment)>) -> Self { - let content = match comment { - Some((_, comment)) => comment.body().to_string(), - None => String::new(), - }; - let textarea = Widget::new(Textarea::default()) - .content(AttrValue::String(content)) - .style(style::reset()); - - let textarea = tui::realm::ui::container(theme, textarea.to_boxed()); - - Self { textarea } - } -} - -impl WidgetComponent for CommentBody { - fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) { - let focus = properties - .get_or(Attribute::Focus, AttrValue::Flag(false)) - .unwrap_flag(); - - self.textarea.attr(Attribute::Focus, AttrValue::Flag(focus)); - self.textarea.view(frame, area); - } - - fn state(&self) -> State { - self.textarea.state() - } - - fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult { - self.textarea.perform(cmd) - } -} - -pub fn list_navigation(theme: &Theme) -> Widget { - tui::realm::ui::tabs( - theme, - vec![label::reversable("Issues").style(style::magenta())], - ) -} - -pub fn list( - context: &Context, - theme: &Theme, - issue: Option<(IssueId, Issue)>, -) -> Widget { - let list = LargeList::new(context, theme, issue); - - Widget::new(list) -} - -pub fn header(context: &Context, theme: &Theme, issue: (IssueId, Issue)) -> Widget { - let header = IssueHeader::new(context, theme, issue); - Widget::new(header) -} - -pub fn description( - context: &Context, - theme: &Theme, - comment: Option<(&CommentId, &Comment)>, -) -> Widget { - let body = CommentBody::new(context, theme, comment); - Widget::new(body) -} - -pub fn new_form(_context: &Context, theme: &Theme) -> Widget { - use tuirealm::props::Layout; - - let title = Widget::new(TextField::new(theme.clone(), "Title")).to_boxed(); - let tags = Widget::new(TextField::new(theme.clone(), "Labels (bug, ...)")).to_boxed(); - let assignees = Widget::new(TextField::new( - theme.clone(), - "Assignees (z6MkvAdxCp1oLVVTsqYvev9YrhSN3gBQNUSM45hhy4pgkexk, ...)", - )) - .to_boxed(); - let description = Widget::new(TextArea::new(theme.clone(), "Description")).to_boxed(); - let inputs: Vec> = vec![title, tags, assignees, description]; - - let layout = Layout::default().constraints( - [ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Min(3), - ] - .as_ref(), - ); - - Widget::new(Form::new(theme.clone(), inputs)) - .custom(Form::PROP_ID, AttrValue::String(String::from(FORM_ID_EDIT))) - .layout(layout) -} - -pub fn details( - context: &Context, - theme: &Theme, - issue: (IssueId, Issue), - comment: Option<(&CommentId, &Comment)>, -) -> Widget { - let discussion = IssueDetails::new(context, theme, issue, comment); - Widget::new(discussion) -} - -pub fn browse_context(context: &Context, theme: &Theme, progress: Progress) -> Widget { - use radicle::cob::issue::State; - - let issues = context.issues().as_ref().unwrap(); - let open = issues - .iter() - .filter(|issue| *issue.1.state() == State::Open) - .collect::>() - .len(); - let closed = issues - .iter() - .filter(|issue| *issue.1.state() != State::Open) - .collect::>() - .len(); - - tui::realm::ui::widget::context::bar( - theme, - "Browse", - "", - "", - &format!("{open} open | {closed} closed"), - &progress.to_string(), - ) -} - -pub fn description_context( - _context: &Context, - theme: &Theme, - progress: Progress, -) -> Widget { - tui::realm::ui::widget::context::bar(theme, "Show", "", "", "", &progress.to_string()) -} - -pub fn form_context(_context: &Context, theme: &Theme, progress: Progress) -> Widget { - tui::realm::ui::widget::context::bar(theme, "Open", "", "", "", &progress.to_string()) - .custom(ContextBar::PROP_EDIT_MODE, AttrValue::Flag(true)) -} - -pub fn issues( - context: &Context, - theme: &Theme, - selected: Option<(IssueId, Issue)>, -) -> Widget { - Widget::new(IssueBrowser::new(context, theme, selected)) -} diff --git a/bin/commands/issue/flux/select.rs b/bin/commands/issue/select.rs similarity index 99% rename from bin/commands/issue/flux/select.rs rename to bin/commands/issue/select.rs index 8609b3c0..be1a7d64 100644 --- a/bin/commands/issue/flux/select.rs +++ b/bin/commands/issue/select.rs @@ -18,7 +18,7 @@ use tui::Exit; use ui::ListPage; -use super::super::common::Mode; +use super::common::Mode; type Selection = tui::Selection; diff --git a/bin/commands/issue/flux/select/ui.rs b/bin/commands/issue/select/ui.rs similarity index 100% rename from bin/commands/issue/flux/select/ui.rs rename to bin/commands/issue/select/ui.rs