diff --git a/Cargo.toml b/Cargo.toml index 30ecd45a..713cd7a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,7 @@ thiserror = { version = "1" } tuirealm = { version = "1.9.0", default-features = false, features = [ "with-termion" ] } tui-realm-stdlib = { version = "1.2.0", default-features = false, features = [ "with-termion" ] } tui-realm-textarea = { git = "https://github.com/erak/tui-realm-textarea.git", default-features = false, features = [ "with-termion", "clipboard" ] } + +# tuirealm = { version = "1.8.0", default-features = false, features = [ "with-crossterm" ] } +# tui-realm-stdlib = { version = "1.2.0", default-features = false, features = [ "with-crossterm" ] } +# tui-realm-textarea = { git = "https://github.com/erak/tui-realm-textarea.git", default-features = false, features = [ "with-crossterm", "clipboard" ] } diff --git a/bin/commands/issue/suite.rs b/bin/commands/issue/suite.rs index 64a836b4..ae4e975e 100644 --- a/bin/commands/issue/suite.rs +++ b/bin/commands/issue/suite.rs @@ -18,8 +18,7 @@ use tui::cob; use tui::context::Context; use tui::ui::subscription; use tui::ui::theme::{self, Theme}; -use tui::PageStack; -use tui::Tui; +use tui::{Exit, PageStack, Tui}; use page::{IssuePage, ListPage}; @@ -341,7 +340,10 @@ impl Tui for App { } } - fn quit(&self) -> bool { - self.quit + fn exit(&self) -> Option { + if self.quit { + return Some(Exit { value: None }); + } + None } } diff --git a/bin/commands/patch.rs b/bin/commands/patch.rs index dc867000..bea7b98a 100644 --- a/bin/commands/patch.rs +++ b/bin/commands/patch.rs @@ -1,14 +1,18 @@ #[path = "patch/suite.rs"] mod suite; +#[path = "patch/list.rs"] +mod list; use std::ffi::OsString; use anyhow::anyhow; +use radicle_tui::{context, log, Window}; + use crate::terminal; use crate::terminal::args::{Args, Error, Help}; -#[allow(dead_code)] +pub const FPS: u64 = 60; pub const HELP: Help = Help { name: "patch", description: "Terminal interfaces for patches", @@ -24,19 +28,17 @@ General options "#, }; -#[allow(dead_code)] pub struct Options { op: Operation, } pub enum Operation { - Suite, + List, } -#[derive(Default, PartialEq, Eq)] +#[derive(PartialEq, Eq)] pub enum OperationName { - #[default] - Suite, + List, } impl Args for Options { @@ -44,7 +46,7 @@ impl Args for Options { use lexopt::prelude::*; let mut parser = lexopt::Parser::from_args(args); - let op: Option = None; + let mut op: Option = None; #[allow(clippy::never_loop)] while let Some(arg) = parser.next()? { @@ -52,18 +54,38 @@ impl Args for Options { Long("help") | Short('h') => { return Err(Error::Help.into()); } + Value(val) if op.is_none() => match val.to_string_lossy().as_ref() { + "list" => op = Some(OperationName::List), + unknown => anyhow::bail!("unknown operation '{}'", unknown), + }, _ => return Err(anyhow!(arg.unexpected())), } } - let op = match op.unwrap_or_default() { - OperationName::Suite => Operation::Suite, + let op = match op.ok_or_else(|| anyhow!("an operation must be provided"))? { + OperationName::List => Operation::List, }; Ok((Options { op }, vec![])) } } -#[allow(dead_code)] -pub fn run(_options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()> { +pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()> { + let (_, id) = radicle::rad::cwd() + .map_err(|_| anyhow!("this command must be run in the context of a project"))?; + + let context = context::Context::new(id)?; + + match options.op { + Operation::List => { + log::enable("patch", "list", context.profile())?; + + let patch_id = Window::default() + .run(&mut list::App::new(context), 1000 / FPS)? + .ok_or_else(|| anyhow!("expected patch id"))?; + + eprint!("{patch_id}"); + } + } + Ok(()) } diff --git a/bin/commands/patch/list.rs b/bin/commands/patch/list.rs new file mode 100644 index 00000000..b4dd1907 --- /dev/null +++ b/bin/commands/patch/list.rs @@ -0,0 +1,238 @@ +#[path = "list/event.rs"] +mod event; +#[path = "list/page.rs"] +mod page; +#[path = "list/ui.rs"] +mod ui; + +use std::hash::Hash; + +use anyhow::Result; + +use radicle::cob::patch::PatchId; + +use tui::Exit; +use tuirealm::application::PollStrategy; +use tuirealm::{Application, Frame, NoUserEvent, Sub, SubClause}; + +use radicle_tui as tui; + +use radicle_tui::context::Context; +use radicle_tui::ui::subscription; +use radicle_tui::ui::theme::{self, Theme}; +use radicle_tui::PageStack; +use radicle_tui::Tui; + +use page::ListView; + +#[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, + Popup, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PopupMessage { + Info(String), + Warning(String), + Error(String), + Hide, +} + +#[derive(Clone, Default, Debug, Eq, PartialEq)] +pub enum Message { + Popup(PopupMessage), + #[default] + Tick, + Quit(Option), + Batch(Vec), +} + +#[allow(dead_code)] +pub struct App { + context: Context, + pages: PageStack, + theme: Theme, + quit: bool, + patch_id: 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) -> Self { + Self { + context, + pages: PageStack::default(), + theme: theme::default_dark(), + quit: false, + patch_id: None, + } + } + + fn view_list( + &mut self, + app: &mut Application, + theme: &Theme, + ) -> Result<()> { + let home = Box::new(ListView::new(theme.clone())); + self.pages.push(home, app, &self.context, theme)?; + + Ok(()) + } + + fn process( + &mut self, + app: &mut Application, + message: Message, + ) -> Result> { + let theme = theme::default_dark(); + 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::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(id) => { + self.quit = true; + self.patch_id = id; + 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::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::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::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(()) + } +} + +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::global_clause(), 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: self.patch_id.map(|id| format!("{id}")), + }); + } + None + } +} diff --git a/bin/commands/patch/list/event.rs b/bin/commands/patch/list/event.rs new file mode 100644 index 00000000..2055277b --- /dev/null +++ b/bin/commands/patch/list/event.rs @@ -0,0 +1,107 @@ +use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection}; +use tuirealm::event::{Event, Key, KeyEvent}; +use tuirealm::{MockComponent, NoUserEvent, State, StateValue}; + +use radicle_tui::ui::widget::container::{AppHeader, GlobalListener, LabeledContainer, Popup}; +use radicle_tui::ui::widget::context::{ContextBar, Shortcuts}; +use radicle_tui::ui::widget::list::PropertyList; + +use radicle_tui::ui::widget::Widget; + +use super::{ui, 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)), + _ => 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::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, .. + }) => { + let result = self.perform(Cmd::Submit); + match result { + CmdResult::Submit(State::One(StateValue::Usize(selected))) => { + let item = self.items().get(selected)?; + Some(Message::Quit(Some(item.id().to_owned()))) + } + _ => 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 + } +} + +impl tuirealm::Component for Widget { + fn on(&mut self, _event: Event) -> Option { + None + } +} diff --git a/bin/commands/patch/list/page.rs b/bin/commands/patch/list/page.rs new file mode 100644 index 00000000..3db4eb55 --- /dev/null +++ b/bin/commands/patch/list/page.rs @@ -0,0 +1,163 @@ +use std::collections::HashMap; + +use anyhow::Result; + +use tuirealm::{Frame, NoUserEvent, State, StateValue, Sub, SubClause}; + +use radicle_tui as tui; + +use tui::context::Context; +use tui::ui::theme::Theme; +use tui::ui::widget::context::{Progress, Shortcuts}; +use tui::ui::widget::Widget; +use tui::ui::{layout, subscription}; +use tui::ViewPage; + +use super::{ui, Application, Cid, ListCid, Message}; + +/// +/// Home +/// +pub struct ListView { + active_component: ListCid, + shortcuts: HashMap>, +} + +impl ListView { + pub fn new(theme: Theme) -> Self { + let shortcuts = Self::build_shortcuts(&theme); + Self { + active_component: ListCid::PatchBrowser, + shortcuts, + } + } + + fn build_shortcuts(theme: &Theme) -> HashMap> { + [( + ListCid::PatchBrowser, + tui::ui::shortcuts( + theme, + vec![ + tui::ui::shortcut(theme, "tab", "section"), + tui::ui::shortcut(theme, "↑/↓", "navigate"), + tui::ui::shortcut(theme, "enter", "show"), + tui::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::PatchBrowser))?; + 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); + + 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( + &self, + app: &mut Application, + context: &Context, + theme: &Theme, + ) -> Result<()> { + let navigation = ui::list_navigation(theme); + let header = tui::ui::app_header(context, theme, Some(navigation)).to_boxed(); + let patch_browser = ui::patches(context, theme, None).to_boxed(); + + app.remount(Cid::List(ListCid::Header), header, vec![])?; + 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 shortcuts_h = 1u16; + let layout = layout::default_page(area, 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::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/patch/list/ui.rs b/bin/commands/patch/list/ui.rs new file mode 100644 index 00000000..f0625f19 --- /dev/null +++ b/bin/commands/patch/list/ui.rs @@ -0,0 +1,140 @@ +use radicle::cob::patch::{Patch, PatchId}; + +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::context::Context; +use tui::ui::cob::PatchItem; +use tui::ui::theme::Theme; +use tui::ui::widget::{Widget, WidgetComponent}; + +use tui::ui::widget::container::Tabs; +use tui::ui::widget::context::{ContextBar, Progress}; +use tui::ui::widget::list::{ColumnWidth, Table}; + +pub struct PatchBrowser { + items: Vec, + table: Widget>, +} + +impl PatchBrowser { + pub fn new(context: &Context, theme: &Theme, selected: Option<(PatchId, Patch)>) -> Self { + let header = [ + tui::ui::label(" ● "), + tui::ui::label("ID"), + tui::ui::label("Title"), + tui::ui::label("Author"), + tui::ui::label("Head"), + tui::ui::label("+"), + tui::ui::label("-"), + tui::ui::label("Updated"), + ]; + + let widths = [ + ColumnWidth::Fixed(3), + ColumnWidth::Fixed(7), + ColumnWidth::Grow, + ColumnWidth::Fixed(21), + ColumnWidth::Fixed(7), + ColumnWidth::Fixed(4), + ColumnWidth::Fixed(4), + ColumnWidth::Fixed(18), + ]; + + let repo = context.repository(); + let mut items = vec![]; + + for (id, patch) in context.patches() { + if let Ok(item) = PatchItem::try_from((context.profile(), repo, *id, patch.clone())) { + items.push(item); + } + } + + items.sort_by(|a, b| b.timestamp().cmp(a.timestamp())); + items.sort_by(|a, b| a.state().cmp(b.state())); + + let selected = match selected { + Some((id, patch)) => { + Some(PatchItem::try_from((context.profile(), repo, id, patch)).unwrap()) + } + _ => items.first().cloned(), + }; + + let table = Widget::new(Table::new(&items, selected, header, widths, theme.clone())) + .highlight(theme.colors.item_list_highlighted_bg); + + Self { items, table } + } + + pub fn items(&self) -> &Vec { + &self.items + } +} + +impl WidgetComponent for PatchBrowser { + 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 list_navigation(theme: &Theme) -> Widget { + tui::ui::tabs( + theme, + vec![tui::ui::reversable_label("Patches").foreground(theme.colors.tabs_highlighted_fg)], + ) +} + +pub fn patches( + context: &Context, + theme: &Theme, + selected: Option<(PatchId, Patch)>, +) -> Widget { + Widget::new(PatchBrowser::new(context, theme, selected)) +} + +pub fn browse_context(context: &Context, theme: &Theme, progress: Progress) -> Widget { + use radicle::cob::patch::State; + + let patches = context.patches(); + let mut draft = 0; + let mut open = 0; + let mut archived = 0; + let mut merged = 0; + + for (_, patch) in patches { + match patch.state() { + State::Draft => draft += 1, + State::Open { conflicts: _ } => open += 1, + State::Archived => archived += 1, + State::Merged { + commit: _, + revision: _, + } => merged += 1, + } + } + + tui::ui::widget::context::bar( + theme, + "Browse", + "", + "", + &format!("{draft} draft | {open} open | {archived} archived | {merged} merged"), + &progress.to_string(), + ) +} diff --git a/bin/commands/patch/suite.rs b/bin/commands/patch/suite.rs index 4c5f85fa..4b9773c0 100644 --- a/bin/commands/patch/suite.rs +++ b/bin/commands/patch/suite.rs @@ -16,12 +16,11 @@ use tuirealm::{Application, Frame, NoUserEvent, Sub, SubClause}; use radicle_tui as tui; -use radicle_tui::cob; -use radicle_tui::context::Context; -use radicle_tui::ui::subscription; -use radicle_tui::ui::theme::{self, Theme}; -use radicle_tui::PageStack; -use radicle_tui::Tui; +use tui::cob; +use tui::context::Context; +use tui::ui::subscription; +use tui::ui::theme::{self, Theme}; +use tui::{Exit, PageStack, Tui}; use page::{ListView, PatchView}; @@ -272,7 +271,10 @@ impl Tui for App { } } - fn quit(&self) -> bool { - self.quit + fn exit(&self) -> Option { + if self.quit { + return Some(Exit { value: None }); + } + None } } diff --git a/bin/main.rs b/bin/main.rs index 1c6627dc..9ca4fc3e 100644 --- a/bin/main.rs +++ b/bin/main.rs @@ -95,8 +95,17 @@ fn run(command: Command) -> Result<(), Option> { Ok(()) } -fn run_other(exe: &str, _args: &[OsString]) -> Result<(), Option> { - Err(Some(anyhow!( - "`{exe}` is not a command. See `rad-tui --help` for a list of commands.", - ))) +fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option> { + match exe { + "patch" => { + terminal::run_command_args::( + tui_patch::HELP, + tui_patch::run, + args.to_vec(), + ); + } + other => Err(Some(anyhow!( + "`{other}` is not a command. See `rad-tui --help` for a list of commands.", + ))), + } } diff --git a/bin/terminal.rs b/bin/terminal.rs index 828544bd..9b0b438b 100644 --- a/bin/terminal.rs +++ b/bin/terminal.rs @@ -46,7 +46,6 @@ where } } -#[allow(dead_code)] pub fn run_command_args(help: Help, cmd: C, args: Vec) -> ! where A: Args, diff --git a/src/context.rs b/src/context.rs index e17e024f..e99cdeb6 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use radicle_term as term; use radicle::cob::issue::{Issue, IssueId}; @@ -6,16 +8,31 @@ use radicle::crypto::ssh::keystore::{Keystore, MemorySigner}; use radicle::crypto::Signer; use radicle::prelude::{Id, Project}; use radicle::profile::env::RAD_PASSPHRASE; -use radicle::storage::ReadRepository; -use radicle::Profile; - use radicle::storage::git::Repository; -use radicle::storage::ReadStorage; +use radicle::storage::{ReadRepository, ReadStorage}; + +use radicle::Profile; use term::{passphrase, spinner, Passphrase}; use inquire::validator; +/// Git revision parameter. Supports extended SHA-1 syntax. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Rev(String); + +impl From for Rev { + fn from(value: String) -> Self { + Rev(value) + } +} + +impl Display for Rev { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + pub struct Context { profile: Profile, id: Id, @@ -32,12 +49,11 @@ impl Context { let signer = signer(&profile)?; let repository = profile.storage.repository(id).unwrap(); - let doc = repository.identity_doc()?; - let project = doc.project()?; - let issues = crate::cob::issue::all(&repository).unwrap_or_default(); let patches = crate::cob::patch::all(&repository).unwrap_or_default(); + let project = repository.identity_doc()?.project()?; + Ok(Self { id, profile, diff --git a/src/lib.rs b/src/lib.rs index 81c80168..4a762ea1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,8 +37,13 @@ where /// Should draw the application to a frame. fn view(&mut self, app: &mut Application, frame: &mut Frame); - /// Should return true if the application is requested to quit. - fn quit(&self) -> bool; + /// Should return `Some` if the application is requested to quit. + fn exit(&self) -> Option; +} + +/// An optional return value. +pub struct Exit { + pub value: Option, } /// A tui-window using the cross-platform Terminal helper provided @@ -59,9 +64,9 @@ impl Window { /// Creates a tui-window using the default cross-platform Terminal /// helper and panics if its creation fails. pub fn new() -> Self { - Self { - terminal: TerminalBridge::new().expect("Cannot create terminal bridge"), - } + let terminal = TerminalBridge::new().expect("Cannot create terminal bridge"); + + Self { terminal } } /// Runs this tui-window with the tui-application given and performs the @@ -72,7 +77,7 @@ impl Window { /// - update application state /// - redraw view /// 3. Leave alternative terminal screen - pub fn run(&mut self, tui: &mut T, interval: u64) -> Result<()> + pub fn run(&mut self, tui: &mut T, interval: u64) -> Result> where T: Tui, Id: Eq + PartialEq + Clone + Hash, @@ -86,7 +91,7 @@ impl Window { ); tui.init(&mut app)?; - while !tui.quit() { + while tui.exit().is_none() { if update || resize { self.terminal .raw_mut() @@ -98,7 +103,7 @@ impl Window { size = self.terminal.raw().size()?; } - Ok(()) + Ok(tui.exit().unwrap().value) } }