issue: Adjust new API

This commit is contained in:
Erik Kundt
2024-06-04 17:45:52 +02:00
parent f24fc7574f
commit fd30b1ed80
2 changed files with 338 additions and 467 deletions
+306 -14
View File
@@ -5,6 +5,12 @@ use std::str::FromStr;
use anyhow::Result;
use termion::event::Key;
use ratatui::layout::Constraint;
use ratatui::style::Stylize;
use ratatui::text::{Line, Span, Text};
use radicle::issue::IssueId;
use radicle::storage::git::Repository;
use radicle::Profile;
@@ -15,12 +21,15 @@ use tui::cob::issue;
use tui::store;
use tui::store::StateValue;
use tui::ui::items::{Filter, IssueItem, IssueItemFilter};
use tui::ui::widget::window::{Window, WindowProps};
use tui::ui::widget::ToWidget;
use tui::ui::span;
use tui::ui::widget::container::{Column, Container, Footer, FooterProps, Header, HeaderProps};
use tui::ui::widget::text::{Paragraph, ParagraphProps};
use tui::ui::widget::window::{Page, PageProps, Shortcuts, ShortcutsProps, Window, WindowProps};
use tui::ui::widget::{ToWidget, Widget};
use tui::{BoxedAny, Channel, Exit, PageStack};
use self::ui::{BrowserPage, HelpPage};
use self::ui::{Browser, BrowserProps};
use super::common::Mode;
@@ -38,8 +47,8 @@ pub struct App {
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum Page {
Browse,
pub enum AppPage {
Browser,
Help,
}
@@ -72,7 +81,7 @@ pub struct HelpState {
#[derive(Clone, Debug)]
pub struct State {
mode: Mode,
pages: PageStack<Page>,
pages: PageStack<AppPage>,
browser: BrowserState,
help: HelpState,
}
@@ -96,7 +105,7 @@ impl TryFrom<&Context> for State {
Ok(Self {
mode: context.mode.clone(),
pages: PageStack::new(vec![Page::Browse]),
pages: PageStack::new(vec![AppPage::Browser]),
browser: BrowserState {
items,
selected: Some(0),
@@ -176,7 +185,7 @@ impl store::State<Selection> for State {
None
}
Message::OpenHelp => {
self.pages.push(Page::Help);
self.pages.push(AppPage::Help);
None
}
Message::LeavePage => {
@@ -204,15 +213,12 @@ impl App {
let tx = channel.tx.clone();
let window = Window::default()
.page(
Page::Browse,
BrowserPage::new(tx.clone()).to_widget(tx.clone()),
)
.page(Page::Help, HelpPage::new(tx.clone()).to_widget(tx.clone()))
.page(AppPage::Browser, browser_page(&state, &channel))
.page(AppPage::Help, help_page(&state, &channel))
.to_widget(tx.clone())
.on_update(|state| {
WindowProps::default()
.current_page(state.pages.peek().unwrap_or(&Page::Browse).clone())
.current_page(state.pages.peek().unwrap_or(&AppPage::Browser).clone())
.to_boxed_any()
.into()
});
@@ -220,3 +226,289 @@ impl App {
tui::run(channel, state, window).await
}
}
fn browser_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Message> {
let tx = channel.tx.clone();
let content = Browser::new(tx.clone())
.to_widget(tx.clone())
.on_update(|state| BrowserProps::from(state).to_boxed_any().into());
let shortcuts = Shortcuts::default()
.to_widget(tx.clone())
.on_update(|state: &State| {
let shortcuts = if state.browser.show_search {
vec![("esc", "cancel"), ("enter", "apply")]
} else {
match state.mode {
Mode::Id => vec![("enter", "select"), ("/", "search")],
Mode::Operation => vec![
("enter", "show"),
("e", "edit"),
("/", "search"),
("?", "help"),
],
}
};
ShortcutsProps::default()
.shortcuts(&shortcuts)
.to_boxed_any()
.into()
});
Page::default()
.content(content)
.shortcuts(shortcuts)
.to_widget(tx.clone())
.on_event(|key, _, props| {
let default = PageProps::default();
let props = props
.and_then(|props| props.inner_ref::<PageProps>())
.unwrap_or(&default);
if props.handle_keys {
match key {
Key::Esc | Key::Ctrl('c') => Some(Message::Exit { selection: None }),
Key::Char('?') => Some(Message::OpenHelp),
_ => None,
}
} else {
None
}
})
.on_update(|state: &State| {
PageProps::default()
.page_size(state.browser.page_size)
.handle_keys(!state.browser.show_search)
.to_boxed_any()
.into()
})
.on_render(|props, render| {
let default = PageProps::default();
let props = props
.and_then(|props| props.inner_ref::<PageProps>())
.unwrap_or(&default);
let page_size = render.area.height.saturating_sub(6) as usize;
if page_size != props.page_size {
return Some(Message::BrowserPageSize(page_size));
}
None
})
}
fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Message> {
let tx = channel.tx.clone();
let content = Container::default()
.header(Header::default().to_widget(tx.clone()).on_update(|_| {
HeaderProps::default()
.columns([Column::new(" Help ", Constraint::Fill(1))].to_vec())
.to_boxed_any()
.into()
}))
.content(
Paragraph::default()
.to_widget(tx.clone())
.on_event(|_, s, _| {
Some(Message::ScrollHelp {
progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
})
})
.on_update(|state: &State| {
ParagraphProps::default()
.text(&help_text())
.page_size(state.help.page_size)
.to_boxed_any()
.into()
}),
)
.footer(
Footer::default()
.to_widget(tx.clone())
.on_update(|state: &State| {
FooterProps::default()
.columns(
[
Column::new(Text::raw(""), Constraint::Fill(1)),
Column::new(
span::default(&format!("{}%", state.help.progress)).dim(),
Constraint::Min(4),
),
]
.to_vec(),
)
.to_boxed_any()
.into()
}),
)
.to_widget(tx.clone());
let shortcuts = Shortcuts::default().to_widget(tx.clone()).on_update(|_| {
ShortcutsProps::default()
.shortcuts(&[("?", "close")])
.to_boxed_any()
.into()
});
Page::default()
.content(content)
.shortcuts(shortcuts)
.to_widget(tx.clone())
.on_event(|key, _, _| match key {
Key::Esc | Key::Ctrl('c') => Some(Message::Exit { selection: None }),
Key::Char('?') => Some(Message::LeavePage),
_ => None,
})
.on_update(|state: &State| {
PageProps::default()
.page_size(state.help.page_size)
.handle_keys(true)
.to_boxed_any()
.into()
})
.on_render(|props, render| {
let default = PageProps::default();
let props = props
.and_then(|props| props.inner_ref::<PageProps>())
.unwrap_or(&default);
let page_size = render.area.height.saturating_sub(6) as usize;
if page_size != props.page_size {
return Some(Message::HelpPageSize(page_size));
}
None
})
}
fn help_text() -> Text<'static> {
Text::from(
[
Line::from(Span::raw("Generic keybindings").cyan()),
Line::raw(""),
Line::from(
[
Span::raw(format!("{key:>10}", key = "↑,k")).gray(),
Span::raw(" "),
Span::raw("move cursor one line up").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "↓,j")).gray(),
Span::raw(" "),
Span::raw("move cursor one line down").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "PageUp")).gray(),
Span::raw(" "),
Span::raw("move cursor one page up").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "PageDown")).gray(),
Span::raw(" "),
Span::raw("move cursor one page down").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "Home")).gray(),
Span::raw(" "),
Span::raw("move cursor to the first line").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "End")).gray(),
Span::raw(" "),
Span::raw("move cursor to the last line").gray().dim(),
]
.to_vec(),
),
Line::raw(""),
Line::from(Span::raw("Specific keybindings").cyan()),
Line::raw(""),
Line::from(
[
Span::raw(format!("{key:>10}", key = "enter")).gray(),
Span::raw(" "),
Span::raw("Select issue (if --mode id)").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "enter")).gray(),
Span::raw(" "),
Span::raw("Show issue").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "e")).gray(),
Span::raw(" "),
Span::raw("Edit patch").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "/")).gray(),
Span::raw(" "),
Span::raw("Search").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "?")).gray(),
Span::raw(" "),
Span::raw("Show help").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "Esc")).gray(),
Span::raw(" "),
Span::raw("Quit / cancel").gray().dim(),
]
.to_vec(),
),
Line::raw(""),
Line::from(Span::raw("Searching").cyan()),
Line::raw(""),
Line::from(
[
Span::raw(format!("{key:>10}", key = "Pattern")).gray(),
Span::raw(" "),
Span::raw("is:<state> | is:authored | is:assigned | authors:[<did>, ...] | assignees:[<did>, ...] | <search>")
.gray()
.dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "Example")).gray(),
Span::raw(" "),
Span::raw("is:solved is:authored alias").gray().dim(),
]
.to_vec(),
),
Line::raw(""),
Line::raw(""),
]
.to_vec())
}
+32 -453
View File
@@ -10,21 +10,19 @@ use termion::event::Key;
use ratatui::layout::{Constraint, Layout};
use ratatui::style::Stylize;
use ratatui::text::{Line, Span, Text};
use ratatui::text::{Line, Text};
use radicle_tui as tui;
use tui::ui::items::{IssueItem, IssueItemFilter};
use tui::ui::span;
use tui::ui::widget;
use tui::ui::widget::container::{
Column, Container, ContainerProps, Footer, FooterProps, Header, HeaderProps, SectionGroup,
SectionGroupProps,
Column, Container, ContainerProps, Footer, FooterProps, Header, HeaderProps,
};
use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps, TableUtils};
use tui::ui::widget::text::{Paragraph, ParagraphProps};
use tui::ui::widget::window::{Shortcuts, ShortcutsProps};
use tui::ui::widget::{self, ViewProps};
use tui::ui::widget::ViewProps;
use tui::ui::widget::{RenderProps, ToWidget, View};
use tui::{BoxedAny, Selection};
@@ -37,7 +35,7 @@ use super::{Message, State};
type Widget = widget::Widget<State, Message>;
#[derive(Clone, Default)]
struct BrowserProps<'a> {
pub struct BrowserProps<'a> {
/// Application mode: openation and id or id only.
mode: Mode,
/// Filtered issues.
@@ -137,15 +135,18 @@ pub struct Browser {
}
impl Browser {
fn new(tx: UnboundedSender<Message>) -> Self {
pub fn new(tx: UnboundedSender<Message>) -> Self {
Self {
issues: Container::default()
.header(
Header::default()
// .columns(props.header.clone())
// .cutoff(props.cutoff, props.cutoff_after)
.to_widget(tx.clone()),
)
.header(Header::default().to_widget(tx.clone()).on_update(|state| {
// TODO: remove and use state directly
let props = BrowserProps::from(state);
HeaderProps::default()
.columns(props.header.clone())
.cutoff(props.cutoff, props.cutoff_after)
.to_boxed_any()
.into()
}))
.content(
Table::<State, Message, IssueItem, 8>::default()
.to_widget(tx.clone())
@@ -183,7 +184,23 @@ impl Browser {
.to_boxed_any()
.into()
}),
search: Search::new(tx.clone()).to_widget(tx.clone()),
search: TextField::default()
.to_widget(tx.clone())
.on_event(|key, s, _| match key {
Key::Esc => Some(Message::CloseSearch),
Key::Char('\n') => Some(Message::ApplySearch),
_ => Some(Message::UpdateSearch {
value: s.and_then(|i| i.unwrap_string()).unwrap_or_default(),
}),
})
.on_update(|state: &State| {
TextFieldProps::default()
.text(&state.browser.search.read().to_string())
.title("Search")
.inline(true)
.to_boxed_any()
.into()
}),
}
}
}
@@ -263,313 +280,6 @@ impl View for Browser {
}
}
#[derive(Clone, Default)]
struct BrowserPageProps<'a> {
/// Current page size (height of table content).
page_size: usize,
/// If this pages' keys should be handled (`false` if search is shown).
handle_keys: bool,
/// This pages' shortcuts.
shortcuts: Vec<(&'a str, &'a str)>,
}
impl<'a> From<&State> for BrowserPageProps<'a> {
fn from(state: &State) -> Self {
Self {
page_size: state.browser.page_size,
handle_keys: !state.browser.show_search,
shortcuts: if state.browser.show_search {
vec![("esc", "cancel"), ("enter", "apply")]
} else {
match state.mode {
Mode::Id => vec![("enter", "select"), ("/", "search")],
Mode::Operation => vec![
("enter", "show"),
("e", "edit"),
("/", "search"),
("?", "help"),
],
}
},
}
}
}
pub struct BrowserPage<'a> {
/// Internal props
props: BrowserPageProps<'a>,
/// Sections widget
sections: Widget,
/// Shortcut widget
shortcuts: Widget,
}
impl<'a: 'static> BrowserPage<'a> {
pub fn new(tx: UnboundedSender<Message>) -> Self {
Self {
props: BrowserPageProps::default(),
sections: SectionGroup::default()
.section(Browser::new(tx.clone()).to_widget(tx.clone()))
.to_widget(tx.clone())
.on_update(|state| {
let props = BrowserPageProps::from(state);
SectionGroupProps::default()
.handle_keys(props.handle_keys)
.to_boxed_any()
.into()
}),
shortcuts: Shortcuts::default()
.to_widget(tx.clone())
.on_update(|state| {
ShortcutsProps::default()
.shortcuts(&BrowserPageProps::from(state).shortcuts)
.to_boxed_any()
.into()
}),
}
}
}
impl<'a: 'static> View for BrowserPage<'a> {
type Message = Message;
type State = State;
fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
self.sections.handle_event(key);
if self.props.handle_keys {
return match key {
Key::Esc | Key::Ctrl('c') => Some(Message::Exit { selection: None }),
Key::Char('?') => Some(Message::OpenHelp),
_ => None,
};
}
None
}
fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
self.sections.update(state);
self.shortcuts.update(state);
}
fn render(&self, _props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
let page_size = render.area.height.saturating_sub(6) as usize;
let [content_area, shortcuts_area] =
Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(render.area);
self.sections.render(
RenderProps::from(content_area)
.layout(Layout::horizontal([Constraint::Min(1)]))
.focus(true),
frame,
);
self.shortcuts
.render(RenderProps::from(shortcuts_area), frame);
// TODO: Find better solution
if page_size != self.props.page_size {
self.sections.send(Message::BrowserPageSize(page_size));
}
}
}
#[derive(Clone)]
pub struct SearchProps {}
pub struct Search {
/// Internal props
props: SearchProps,
/// Search input field
input: Widget,
}
impl Search {
fn new(tx: UnboundedSender<Message>) -> Self
where
Self: Sized,
{
Self {
props: SearchProps {},
input: TextField::default()
.to_widget(tx.clone())
.on_event(|_, s, _| {
Some(Message::UpdateSearch {
value: s.and_then(|i| i.unwrap_string()).unwrap_or_default(),
})
})
.on_update(|state: &State| {
TextFieldProps::default()
.text(&state.browser.search.read().to_string())
.title("Search")
.inline(true)
.to_boxed_any()
.into()
}),
}
}
}
impl View for Search {
type Message = Message;
type State = State;
fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
match key {
Key::Esc => Some(Message::CloseSearch),
Key::Char('\n') => Some(Message::ApplySearch),
_ => {
self.input.handle_event(key);
None
}
}
}
fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
self.input.update(state);
}
fn render(&self, _props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
let layout = Layout::horizontal(Constraint::from_mins([0]))
.horizontal_margin(1)
.split(render.area);
self.input.render(RenderProps::from(layout[0]), frame);
}
}
#[derive(Clone, Default)]
struct HelpPageProps<'a> {
/// Current page size (height of table content).
page_size: usize,
/// Scroll progress of help paragraph.
help_progress: usize,
/// This pages' shortcuts.
shortcuts: Vec<(&'a str, &'a str)>,
}
impl<'a> From<&State> for HelpPageProps<'a> {
fn from(state: &State) -> Self {
Self {
page_size: state.help.page_size,
help_progress: state.help.progress,
shortcuts: vec![("?", "close")],
}
}
}
pub struct HelpPage {
/// Content widget
content: Widget,
/// Shortcut widget
shortcuts: Widget,
}
impl HelpPage {
pub fn new(tx: UnboundedSender<Message>) -> Self
where
Self: Sized,
{
Self {
content: Container::default()
.header(Header::default().to_widget(tx.clone()).on_update(|_| {
HeaderProps::default()
.columns([Column::new(" Help ", Constraint::Fill(1))].to_vec())
.to_boxed_any()
.into()
}))
.content(
Paragraph::default()
.to_widget(tx.clone())
.on_event(|_, s, _| {
Some(Message::ScrollHelp {
progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
})
})
.on_update(|state| {
let props = HelpPageProps::from(state);
ParagraphProps::default()
.text(&help_text())
.page_size(props.page_size)
.to_boxed_any()
.into()
}),
)
.footer(Footer::default().to_widget(tx.clone()).on_update(|state| {
let props = HelpPageProps::from(state);
FooterProps::default()
.columns(
[
Column::new(Text::raw(""), Constraint::Fill(1)),
Column::new(
span::default(&format!("{}%", props.help_progress)).dim(),
Constraint::Min(4),
),
]
.to_vec(),
)
.to_boxed_any()
.into()
}))
.to_widget(tx.clone()),
shortcuts: Shortcuts::default()
.to_widget(tx.clone())
.on_update(|state| {
ShortcutsProps::default()
.shortcuts(&HelpPageProps::from(state).shortcuts)
.to_boxed_any()
.into()
}),
}
}
}
impl View for HelpPage {
type Message = Message;
type State = State;
fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
match key {
Key::Esc | Key::Ctrl('c') => Some(Message::Exit { selection: None }),
Key::Char('?') => Some(Message::LeavePage),
_ => {
self.content.handle_event(key);
None
}
}
}
fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
self.content.update(state);
self.shortcuts.update(state);
}
fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
let default = HelpPageProps::default();
let props = props
.and_then(|props| props.inner_ref::<HelpPageProps>())
.unwrap_or(&default);
let page_size = render.area.height.saturating_sub(6) as usize;
let [content_area, shortcuts_area] =
Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(render.area);
self.content
.render(RenderProps::from(content_area).focus(true), frame);
self.shortcuts
.render(RenderProps::from(content_area).focus(true), frame);
// TODO: Find better solution
if page_size != props.page_size {
self.content.send(Message::HelpPageSize(page_size));
}
}
}
fn browse_footer<'a>(props: &BrowserProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
let search = Line::from(vec![
span::default(" Search ").cyan().dim().reversed(),
@@ -644,134 +354,3 @@ fn browse_footer<'a>(props: &BrowserProps<'a>, selected: Option<usize>) -> Vec<C
.to_vec(),
}
}
fn help_text() -> Text<'static> {
Text::from(
[
Line::from(Span::raw("Generic keybindings").cyan()),
Line::raw(""),
Line::from(
[
Span::raw(format!("{key:>10}", key = "↑,k")).gray(),
Span::raw(" "),
Span::raw("move cursor one line up").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "↓,j")).gray(),
Span::raw(" "),
Span::raw("move cursor one line down").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "PageUp")).gray(),
Span::raw(" "),
Span::raw("move cursor one page up").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "PageDown")).gray(),
Span::raw(" "),
Span::raw("move cursor one page down").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "Home")).gray(),
Span::raw(" "),
Span::raw("move cursor to the first line").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "End")).gray(),
Span::raw(" "),
Span::raw("move cursor to the last line").gray().dim(),
]
.to_vec(),
),
Line::raw(""),
Line::from(Span::raw("Specific keybindings").cyan()),
Line::raw(""),
Line::from(
[
Span::raw(format!("{key:>10}", key = "enter")).gray(),
Span::raw(" "),
Span::raw("Select issue (if --mode id)").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "enter")).gray(),
Span::raw(" "),
Span::raw("Show issue").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "e")).gray(),
Span::raw(" "),
Span::raw("Edit patch").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "/")).gray(),
Span::raw(" "),
Span::raw("Search").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "?")).gray(),
Span::raw(" "),
Span::raw("Show help").gray().dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "Esc")).gray(),
Span::raw(" "),
Span::raw("Quit / cancel").gray().dim(),
]
.to_vec(),
),
Line::raw(""),
Line::from(Span::raw("Searching").cyan()),
Line::raw(""),
Line::from(
[
Span::raw(format!("{key:>10}", key = "Pattern")).gray(),
Span::raw(" "),
Span::raw("is:<state> | is:authored | is:assigned | authors:[<did>, ...] | assignees:[<did>, ...] | <search>")
.gray()
.dim(),
]
.to_vec(),
),
Line::from(
[
Span::raw(format!("{key:>10}", key = "Example")).gray(),
Span::raw(" "),
Span::raw("is:solved is:authored alias").gray().dim(),
]
.to_vec(),
),
Line::raw(""),
Line::raw(""),
]
.to_vec())
}