mirror of
https://seed.radicle.dev/rad:z39mP9rQAaGmERfUMPULfPUi473tY.git
synced 2026-11-09 21:30:33 +08:00
inbox: Adjust new API
This commit is contained in:
+271
-17
@@ -5,26 +5,38 @@ use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use termion::event::Key;
|
||||
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
use ratatui::text::Text;
|
||||
|
||||
use radicle::identity::Project;
|
||||
use radicle::node::notifications::NotificationId;
|
||||
use radicle::storage::git::Repository;
|
||||
use radicle::storage::ReadRepository;
|
||||
use radicle::storage::ReadStorage;
|
||||
|
||||
use radicle::storage::git::Repository;
|
||||
use radicle::Profile;
|
||||
|
||||
use radicle_tui as tui;
|
||||
|
||||
use tui::cob::inbox;
|
||||
use tui::store;
|
||||
use tui::store::StateValue;
|
||||
use tui::ui::items::{Filter, NotificationItem, NotificationItemFilter};
|
||||
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;
|
||||
use self::ui::HelpPage;
|
||||
use self::ui::Browser;
|
||||
use self::ui::BrowserProps;
|
||||
|
||||
use super::common::SelectionMode;
|
||||
use super::common::{Mode, RepositoryMode};
|
||||
|
||||
type Selection = tui::Selection<NotificationId>;
|
||||
@@ -42,7 +54,7 @@ pub struct App {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum Page {
|
||||
pub enum AppPage {
|
||||
Browse,
|
||||
Help,
|
||||
}
|
||||
@@ -77,7 +89,7 @@ pub struct HelpState {
|
||||
pub struct State {
|
||||
mode: Mode,
|
||||
project: Project,
|
||||
pages: PageStack<Page>,
|
||||
pages: PageStack<AppPage>,
|
||||
browser: BrowserState,
|
||||
help: HelpState,
|
||||
}
|
||||
@@ -175,7 +187,7 @@ impl TryFrom<&Context> for State {
|
||||
Ok(Self {
|
||||
mode: context.mode.clone(),
|
||||
project,
|
||||
pages: PageStack::new(vec![Page::Browse]),
|
||||
pages: PageStack::new(vec![AppPage::Browse]),
|
||||
browser: BrowserState {
|
||||
items: notifications,
|
||||
selected: Some(0),
|
||||
@@ -257,7 +269,7 @@ impl store::State<Selection> for State {
|
||||
None
|
||||
}
|
||||
Message::OpenHelp => {
|
||||
self.pages.push(Page::Help);
|
||||
self.pages.push(AppPage::Help);
|
||||
None
|
||||
}
|
||||
Message::LeavePage => {
|
||||
@@ -283,15 +295,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::Browse, browser_page(&state, &channel))
|
||||
.page(AppPage::Help, help_page(&state, &channel))
|
||||
.to_widget(tx.clone())
|
||||
.on_update(|state| {
|
||||
.on_update(|state: &State| {
|
||||
WindowProps::default()
|
||||
.current_page(state.pages.peek().unwrap_or(&Page::Browse).clone())
|
||||
.current_page(state.pages.peek().unwrap_or(&AppPage::Browse).clone())
|
||||
.to_boxed_any()
|
||||
.into()
|
||||
});
|
||||
@@ -299,3 +308,248 @@ 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.selection() {
|
||||
SelectionMode::Id => vec![("enter", "select"), ("/", "search")],
|
||||
SelectionMode::Operation => vec![
|
||||
("enter", "show"),
|
||||
("c", "clear"),
|
||||
("/", "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(vec![
|
||||
Span::raw(format!("{key:>10}", key = "↑,k")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("move cursor one line up").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "↓,j")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("move cursor one line down").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "PageUp")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("move cursor one page up").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "PageDown")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("move cursor one page down").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "Home")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("move cursor to the first line").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "End")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("move cursor to the last line").gray().dim(),
|
||||
]),
|
||||
Line::raw(""),
|
||||
Line::from(Span::raw("Specific keybindings").cyan()),
|
||||
Line::raw(""),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "enter")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("Select notification (if --mode id)").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "enter")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("Show notification").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "c")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("Clear notifications").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "/")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("Search").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "?")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("Show help").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "Esc")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("Quit / cancel").gray().dim(),
|
||||
]),
|
||||
Line::raw(""),
|
||||
Line::from(Span::raw("Searching").cyan()),
|
||||
Line::raw(""),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "Pattern")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("is:<state> | is:patch | is:issue | <search>")
|
||||
.gray()
|
||||
.dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "Example")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("is:unseen is:patch Print").gray().dim(),
|
||||
]),
|
||||
Line::raw(""),
|
||||
Line::raw(""),
|
||||
]
|
||||
.to_vec(),
|
||||
)
|
||||
}
|
||||
|
||||
+35
-416
@@ -8,20 +8,17 @@ 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::{NotificationItem, NotificationItemFilter, NotificationState};
|
||||
use tui::ui::span;
|
||||
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::{RenderProps, ToWidget, View};
|
||||
|
||||
@@ -117,21 +114,24 @@ pub struct Browser {
|
||||
}
|
||||
|
||||
impl Browser {
|
||||
fn new(tx: UnboundedSender<Message>) -> Self {
|
||||
pub fn new(tx: UnboundedSender<Message>) -> Self {
|
||||
Self {
|
||||
notifications: Container::default()
|
||||
.header(
|
||||
Header::default()
|
||||
// .columns(
|
||||
// [
|
||||
// Column::new("", Constraint::Length(0)),
|
||||
// Column::new(Text::from(props.header), Constraint::Fill(1)),
|
||||
// ]
|
||||
// .to_vec(),
|
||||
// )
|
||||
// .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(
|
||||
[
|
||||
Column::new("", Constraint::Length(0)),
|
||||
Column::new(Text::from(props.header), Constraint::Fill(1)),
|
||||
]
|
||||
.to_vec(),
|
||||
)
|
||||
.cutoff(props.cutoff, props.cutoff_after)
|
||||
.to_boxed_any()
|
||||
.into()
|
||||
}))
|
||||
.content(
|
||||
Table::<State, Message, NotificationItem, 9>::default()
|
||||
.to_widget(tx.clone())
|
||||
@@ -169,7 +169,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()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,313 +264,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.selection() {
|
||||
SelectionMode::Id => vec![("enter", "select"), ("/", "search")],
|
||||
SelectionMode::Operation => vec![
|
||||
("enter", "show"),
|
||||
("c", "clear"),
|
||||
("/", "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>) -> Vec<Column<'a>> {
|
||||
let search = Line::from(vec![
|
||||
span::default(" Search ").cyan().dim().reversed(),
|
||||
@@ -614,93 +323,3 @@ fn browse_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
|
||||
.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
fn help_text() -> Text<'static> {
|
||||
Text::from(
|
||||
[
|
||||
Line::from(Span::raw("Generic keybindings").cyan()),
|
||||
Line::raw(""),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "↑,k")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("move cursor one line up").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "↓,j")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("move cursor one line down").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "PageUp")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("move cursor one page up").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "PageDown")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("move cursor one page down").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "Home")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("move cursor to the first line").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "End")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("move cursor to the last line").gray().dim(),
|
||||
]),
|
||||
Line::raw(""),
|
||||
Line::from(Span::raw("Specific keybindings").cyan()),
|
||||
Line::raw(""),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "enter")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("Select notification (if --mode id)").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "enter")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("Show notification").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "c")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("Clear notifications").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "/")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("Search").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "?")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("Show help").gray().dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "Esc")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("Quit / cancel").gray().dim(),
|
||||
]),
|
||||
Line::raw(""),
|
||||
Line::from(Span::raw("Searching").cyan()),
|
||||
Line::raw(""),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "Pattern")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("is:<state> | is:patch | is:issue | <search>")
|
||||
.gray()
|
||||
.dim(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{key:>10}", key = "Example")).gray(),
|
||||
Span::raw(" "),
|
||||
Span::raw("is:unseen is:patch Print").gray().dim(),
|
||||
]),
|
||||
Line::raw(""),
|
||||
Line::raw(""),
|
||||
]
|
||||
.to_vec(),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user