patch-list: Add implementation

This commit is contained in:
Erik Kundt
2024-01-02 18:28:23 +01:00
parent b0fc9fab28
commit 9d676ce50e
12 changed files with 750 additions and 43 deletions
+4
View File
@@ -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" ] }
+6 -4
View File
@@ -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<Cid, Message> for App {
}
}
fn quit(&self) -> bool {
self.quit
fn exit(&self) -> Option<Exit> {
if self.quit {
return Some(Exit { value: None });
}
None
}
}
+33 -11
View File
@@ -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<OperationName> = None;
let mut op: Option<OperationName> = 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(())
}
+238
View File
@@ -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<PatchId>),
Batch(Vec<Message>),
}
#[allow(dead_code)]
pub struct App {
context: Context,
pages: PageStack<Cid, Message>,
theme: Theme,
quit: bool,
patch_id: Option<PatchId>,
}
/// 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<Cid, Message, NoUserEvent>,
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<Cid, Message, NoUserEvent>,
message: Message,
) -> Result<Option<Message>> {
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<Cid, Message, NoUserEvent>,
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<Cid, Message, NoUserEvent>,
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<Cid, Message, NoUserEvent>,
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<Cid, Message, NoUserEvent>) -> Result<()> {
app.blur()?;
app.umount(&Cid::Popup)?;
Ok(())
}
}
impl Tui<Cid, Message> for App {
fn init(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>, 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<Cid, Message, NoUserEvent>) -> Result<bool> {
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<Exit> {
if self.quit {
return Some(Exit {
value: self.patch_id.map(|id| format!("{id}")),
});
}
None
}
}
+107
View File
@@ -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<Message, NoUserEvent> for Widget<GlobalListener> {
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
match event {
Event::Keyboard(KeyEvent {
code: Key::Char('q'),
..
}) => Some(Message::Quit(None)),
_ => None,
}
}
}
impl tuirealm::Component<Message, NoUserEvent> for Widget<ui::PatchBrowser> {
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
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<Message, NoUserEvent> for Widget<Popup> {
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
match event {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Message::Popup(PopupMessage::Hide))
}
_ => None,
}
}
}
impl tuirealm::Component<Message, NoUserEvent> for Widget<AppHeader> {
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
None
}
}
impl tuirealm::Component<Message, NoUserEvent> for Widget<LabeledContainer> {
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
None
}
}
impl tuirealm::Component<Message, NoUserEvent> for Widget<PropertyList> {
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
None
}
}
impl tuirealm::Component<Message, NoUserEvent> for Widget<ContextBar> {
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
None
}
}
impl tuirealm::Component<Message, NoUserEvent> for Widget<Shortcuts> {
fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
None
}
}
+163
View File
@@ -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<ListCid, Widget<Shortcuts>>,
}
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, Widget<Shortcuts>> {
[(
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<Cid, Message, NoUserEvent>,
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, Message, NoUserEvent>,
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<Cid, Message> for ListView {
fn mount(
&self,
app: &mut Application<Cid, Message, NoUserEvent>,
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<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>,
context: &Context,
theme: &Theme,
_message: Message,
) -> Result<Option<Message>> {
self.update_context(app, context, theme)?;
Ok(None)
}
fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, 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<Cid, Message, NoUserEvent>) -> Result<()> {
app.subscribe(
&Cid::List(ListCid::Header),
Sub::new(subscription::navigation_clause(), SubClause::Always),
)?;
Ok(())
}
fn unsubscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
app.unsubscribe(
&Cid::List(ListCid::Header),
subscription::navigation_clause(),
)?;
Ok(())
}
}
+140
View File
@@ -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<PatchItem>,
table: Widget<Table<PatchItem, 8>>,
}
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<PatchItem> {
&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<Tabs> {
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<PatchBrowser> {
Widget::new(PatchBrowser::new(context, theme, selected))
}
pub fn browse_context(context: &Context, theme: &Theme, progress: Progress) -> Widget<ContextBar> {
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(),
)
}
+10 -8
View File
@@ -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<Cid, Message> for App {
}
}
fn quit(&self) -> bool {
self.quit
fn exit(&self) -> Option<Exit> {
if self.quit {
return Some(Exit { value: None });
}
None
}
}
+13 -4
View File
@@ -95,8 +95,17 @@ fn run(command: Command) -> Result<(), Option<anyhow::Error>> {
Ok(())
}
fn run_other(exe: &str, _args: &[OsString]) -> Result<(), Option<anyhow::Error>> {
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<anyhow::Error>> {
match exe {
"patch" => {
terminal::run_command_args::<tui_patch::Options, _>(
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.",
))),
}
}
-1
View File
@@ -46,7 +46,6 @@ where
}
}
#[allow(dead_code)]
pub fn run_command_args<A, C>(help: Help, cmd: C, args: Vec<OsString>) -> !
where
A: Args,
+23 -7
View File
@@ -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<String> 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,
+13 -8
View File
@@ -37,8 +37,13 @@ where
/// Should draw the application to a frame.
fn view(&mut self, app: &mut Application<Id, Message, NoUserEvent>, 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<Exit>;
}
/// An optional return value.
pub struct Exit {
pub value: Option<String>,
}
/// 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<T, Id, Message>(&mut self, tui: &mut T, interval: u64) -> Result<()>
pub fn run<T, Id, Message>(&mut self, tui: &mut T, interval: u64) -> Result<Option<String>>
where
T: Tui<Id, Message>,
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)
}
}