mirror of
https://seed.radicle.dev/rad:z39mP9rQAaGmERfUMPULfPUi473tY.git
synced 2026-10-30 16:30:56 +08:00
issue: Remove legacy tui-realm commands
This commit is contained in:
+4
-39
@@ -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())
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#[path = "flux/select.rs"]
|
||||
pub mod select;
|
||||
@@ -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;
|
||||
@@ -1,2 +0,0 @@
|
||||
#[path = "common/ui.rs"]
|
||||
pub mod ui;
|
||||
@@ -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<IssueItem>,
|
||||
table: Widget<Table<IssueItem, 7>>,
|
||||
}
|
||||
|
||||
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<IssueItem> {
|
||||
&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<ContextBar> {
|
||||
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)
|
||||
}
|
||||
@@ -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<IssueId>;
|
||||
|
||||
#[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<Selection>),
|
||||
Batch(Vec<Message>),
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
context: Context,
|
||||
pages: PageStack<Cid, Message>,
|
||||
theme: Theme,
|
||||
quit: bool,
|
||||
mode: Mode,
|
||||
filter: Filter,
|
||||
output: Option<Selection>,
|
||||
}
|
||||
|
||||
/// 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<Cid, Message, NoUserEvent>,
|
||||
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<Cid, Message, NoUserEvent>,
|
||||
message: Message,
|
||||
) -> Result<Option<Message>> {
|
||||
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<Cid, Message, Selection> 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::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<Cid, Message, NoUserEvent>, frame: &mut Frame) {
|
||||
if let Ok(page) = self.pages.peek_mut() {
|
||||
page.view(app, frame);
|
||||
}
|
||||
}
|
||||
|
||||
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<Selection>> {
|
||||
if self.quit {
|
||||
return Some(Exit {
|
||||
value: self.output.clone(),
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -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<IssueId>;
|
||||
|
||||
/// 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<IdSelect> {
|
||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||
let mut submit = || -> Option<radicle::cob::patch::PatchId> {
|
||||
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<Message, NoUserEvent> for Widget<OperationSelect> {
|
||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||
let mut submit = || -> Option<radicle::cob::patch::PatchId> {
|
||||
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<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
|
||||
}
|
||||
}
|
||||
@@ -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<ListCid, Widget<Shortcuts>>,
|
||||
}
|
||||
|
||||
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<Cid, Message, NoUserEvent>,
|
||||
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, 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(
|
||||
&mut self,
|
||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||
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<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 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<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(())
|
||||
}
|
||||
}
|
||||
@@ -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<common::ui::IssueBrowser>,
|
||||
}
|
||||
|
||||
impl IdSelect {
|
||||
pub fn new(theme: Theme, browser: Widget<common::ui::IssueBrowser>) -> Self {
|
||||
Self { theme, browser }
|
||||
}
|
||||
|
||||
pub fn items(&self) -> &Vec<IssueItem> {
|
||||
self.browser.items()
|
||||
}
|
||||
|
||||
pub fn shortcuts(&self) -> HashMap<ListCid, Widget<Shortcuts>> {
|
||||
[(
|
||||
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<common::ui::IssueBrowser>,
|
||||
}
|
||||
|
||||
impl OperationSelect {
|
||||
pub fn new(theme: Theme, browser: Widget<common::ui::IssueBrowser>) -> Self {
|
||||
Self { theme, browser }
|
||||
}
|
||||
|
||||
pub fn items(&self) -> &Vec<IssueItem> {
|
||||
self.browser.items()
|
||||
}
|
||||
|
||||
pub fn shortcuts(&self) -> HashMap<ListCid, Widget<Shortcuts>> {
|
||||
[(
|
||||
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<Tabs> {
|
||||
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<IdSelect> {
|
||||
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<OperationSelect> {
|
||||
let browser = Widget::new(common::ui::IssueBrowser::new(
|
||||
theme, context, filter, selected,
|
||||
));
|
||||
|
||||
Widget::new(OperationSelect::new(theme.clone(), browser))
|
||||
}
|
||||
@@ -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<IssueId>),
|
||||
Changed(IssueId),
|
||||
Focus(IssueCid),
|
||||
Created(IssueId),
|
||||
Cob(IssueCobMessage),
|
||||
Reload(Option<IssueId>),
|
||||
OpenForm,
|
||||
HideForm,
|
||||
Leave(Option<IssueId>),
|
||||
}
|
||||
|
||||
#[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<Message>),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct App {
|
||||
context: Context,
|
||||
pages: PageStack<Cid, Message>,
|
||||
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<Cid, Message, NoUserEvent>,
|
||||
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<Cid, Message, NoUserEvent>,
|
||||
id: Option<IssueId>,
|
||||
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<Cid, Message, NoUserEvent>,
|
||||
message: Message,
|
||||
) -> Result<Option<Message>> {
|
||||
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<Cid, Message, NoUserEvent>,
|
||||
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<Cid, Message, NoUserEvent>,
|
||||
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<Cid, Message, NoUserEvent>,
|
||||
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<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||
app.blur()?;
|
||||
app.umount(&Cid::Popup)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_issue(
|
||||
&mut self,
|
||||
title: String,
|
||||
description: String,
|
||||
labels: String,
|
||||
assignees: String,
|
||||
) -> Result<IssueId> {
|
||||
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<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::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<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: None });
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -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<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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl tuirealm::Component<Message, NoUserEvent> for Widget<AppHeader> {
|
||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||
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<Message, NoUserEvent> for Widget<ui::LargeList> {
|
||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||
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<Message, NoUserEvent> for Widget<ui::IssueDetails> {
|
||||
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::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<Message, NoUserEvent> for Widget<Form> {
|
||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||
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<Message, NoUserEvent> for Widget<ui::IssueBrowser> {
|
||||
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
|
||||
let mut submit = || -> Option<IssueId> {
|
||||
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<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<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
|
||||
}
|
||||
}
|
||||
@@ -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<ListCid, Widget<Shortcuts>>,
|
||||
}
|
||||
|
||||
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, Message, NoUserEvent>,
|
||||
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, Widget<Shortcuts>> {
|
||||
[(
|
||||
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<Cid, Message, NoUserEvent>,
|
||||
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, 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 ListPage {
|
||||
fn mount(
|
||||
&mut self,
|
||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||
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<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>,
|
||||
context: &Context,
|
||||
theme: &Theme,
|
||||
message: Message,
|
||||
) -> Result<Option<Message>> {
|
||||
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<Cid, Message, NoUserEvent>, 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<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(())
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Issue detail page
|
||||
///
|
||||
pub struct IssuePage {
|
||||
issue: Option<(IssueId, Issue)>,
|
||||
active_component: IssueCid,
|
||||
shortcuts: HashMap<IssueCid, Widget<Shortcuts>>,
|
||||
}
|
||||
|
||||
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, Message, NoUserEvent>,
|
||||
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, Widget<Shortcuts>> {
|
||||
[
|
||||
(
|
||||
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<Cid, Message, NoUserEvent>,
|
||||
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, Message, NoUserEvent>,
|
||||
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<Cid, Message> for IssuePage {
|
||||
fn mount(
|
||||
&mut self,
|
||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||
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::<Vec<_>>();
|
||||
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<Cid, Message, NoUserEvent>) -> 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<Cid, Message, NoUserEvent>,
|
||||
context: &Context,
|
||||
theme: &Theme,
|
||||
message: Message,
|
||||
) -> Result<Option<Message>> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
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<Cid, Message, NoUserEvent>, 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<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unsubscribe(&self, _app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -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<IssueItem>,
|
||||
table: Widget<Table<IssueItem, 7>>,
|
||||
}
|
||||
|
||||
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<IssueItem> {
|
||||
&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<IssueItem>,
|
||||
list: Widget<Container>,
|
||||
}
|
||||
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<IssueItem> {
|
||||
&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<Container>,
|
||||
}
|
||||
|
||||
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::<Vec<_>>(),
|
||||
)),
|
||||
);
|
||||
|
||||
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<IssueHeader>,
|
||||
description: Widget<CommentBody>,
|
||||
}
|
||||
|
||||
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<Container>,
|
||||
}
|
||||
|
||||
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<Tabs> {
|
||||
tui::realm::ui::tabs(
|
||||
theme,
|
||||
vec![label::reversable("Issues").style(style::magenta())],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn list(
|
||||
context: &Context,
|
||||
theme: &Theme,
|
||||
issue: Option<(IssueId, Issue)>,
|
||||
) -> Widget<LargeList> {
|
||||
let list = LargeList::new(context, theme, issue);
|
||||
|
||||
Widget::new(list)
|
||||
}
|
||||
|
||||
pub fn header(context: &Context, theme: &Theme, issue: (IssueId, Issue)) -> Widget<IssueHeader> {
|
||||
let header = IssueHeader::new(context, theme, issue);
|
||||
Widget::new(header)
|
||||
}
|
||||
|
||||
pub fn description(
|
||||
context: &Context,
|
||||
theme: &Theme,
|
||||
comment: Option<(&CommentId, &Comment)>,
|
||||
) -> Widget<CommentBody> {
|
||||
let body = CommentBody::new(context, theme, comment);
|
||||
Widget::new(body)
|
||||
}
|
||||
|
||||
pub fn new_form(_context: &Context, theme: &Theme) -> Widget<Form> {
|
||||
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<Box<dyn MockComponent>> = 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<IssueDetails> {
|
||||
let discussion = IssueDetails::new(context, theme, issue, comment);
|
||||
Widget::new(discussion)
|
||||
}
|
||||
|
||||
pub fn browse_context(context: &Context, theme: &Theme, progress: Progress) -> Widget<ContextBar> {
|
||||
use radicle::cob::issue::State;
|
||||
|
||||
let issues = context.issues().as_ref().unwrap();
|
||||
let open = issues
|
||||
.iter()
|
||||
.filter(|issue| *issue.1.state() == State::Open)
|
||||
.collect::<Vec<_>>()
|
||||
.len();
|
||||
let closed = issues
|
||||
.iter()
|
||||
.filter(|issue| *issue.1.state() != State::Open)
|
||||
.collect::<Vec<_>>()
|
||||
.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<ContextBar> {
|
||||
tui::realm::ui::widget::context::bar(theme, "Show", "", "", "", &progress.to_string())
|
||||
}
|
||||
|
||||
pub fn form_context(_context: &Context, theme: &Theme, progress: Progress) -> Widget<ContextBar> {
|
||||
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<IssueBrowser> {
|
||||
Widget::new(IssueBrowser::new(context, theme, selected))
|
||||
}
|
||||
@@ -18,7 +18,7 @@ use tui::Exit;
|
||||
|
||||
use ui::ListPage;
|
||||
|
||||
use super::super::common::Mode;
|
||||
use super::common::Mode;
|
||||
|
||||
type Selection = tui::Selection<IssueId>;
|
||||
|
||||
Reference in New Issue
Block a user