Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
inbox: Add selection help
Merged did:key:z6MkgFq6...nBGz opened 2 years ago
2 files changed +310 -6 1e21a105 1f2b15ad
modified bin/commands/inbox/flux/select.rs
@@ -42,6 +42,7 @@ pub struct App {
pub struct UIState {
    page_size: usize,
    show_search: bool,
+
    show_help: bool,
}

impl Default for UIState {
@@ -49,6 +50,7 @@ impl Default for UIState {
        Self {
            page_size: 1,
            show_search: false,
+
            show_help: false,
        }
    }
}
@@ -171,6 +173,8 @@ pub enum Action {
    UpdateSearch { value: String },
    ApplySearch,
    CloseSearch,
+
    OpenHelp,
+
    CloseHelp,
}

impl store::State<Action, Selection> for State {
@@ -205,6 +209,14 @@ impl store::State<Action, Selection> for State {
                self.ui.show_search = false;
                None
            }
+
            Action::OpenHelp => {
+
                self.ui.show_help = true;
+
                None
+
            }
+
            Action::CloseHelp => {
+
                self.ui.show_help = false;
+
                None
+
            }
        }
    }
}
modified bin/commands/inbox/flux/select/ui.rs
@@ -8,7 +8,7 @@ use termion::event::Key;
use ratatui::backend::Backend;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::Stylize;
-
use ratatui::text::Line;
+
use ratatui::text::{Line, Span, Text};

use radicle::identity::Project;

@@ -18,6 +18,7 @@ use tui::flux::ui::items::{NotificationItem, NotificationItemFilter, Notificatio
use tui::flux::ui::span;
use tui::flux::ui::widget::container::{Footer, FooterProps, Header, HeaderProps};
use tui::flux::ui::widget::input::{TextField, TextFieldProps};
+
use tui::flux::ui::widget::text::{Paragraph, ParagraphProps};
use tui::flux::ui::widget::{
    Render, Shortcut, Shortcuts, ShortcutsProps, Table, TableProps, Widget,
};
@@ -31,6 +32,7 @@ pub struct ListPageProps {
    selected: Option<NotificationItem>,
    mode: Mode,
    show_search: bool,
+
    show_help: bool,
}

impl From<&State> for ListPageProps {
@@ -39,11 +41,12 @@ impl From<&State> for ListPageProps {
            selected: state.selected.clone(),
            mode: state.mode.clone(),
            show_search: state.ui.show_search,
+
            show_help: state.ui.show_help,
        }
    }
}

-
pub struct ListPage {
+
pub struct ListPage<'a> {
    /// Action sender
    pub action_tx: UnboundedSender<Action>,
    /// State mapped props
@@ -52,11 +55,13 @@ pub struct ListPage {
    notifications: Notifications,
    /// Search widget
    search: Search,
+
    /// Help widget
+
    help: Help<'a>,
    /// Shortcut widget
    shortcuts: Shortcuts<Action>,
}

-
impl Widget<State, Action> for ListPage {
+
impl<'a> Widget<State, Action> for ListPage<'a> {
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
    where
        Self: Sized,
@@ -66,6 +71,7 @@ impl Widget<State, Action> for ListPage {
            props: ListPageProps::from(state),
            notifications: Notifications::new(state, action_tx.clone()),
            search: Search::new(state, action_tx.clone()),
+
            help: Help::new(state, action_tx.clone()),
            shortcuts: Shortcuts::new(state, action_tx.clone()),
        }
        .move_with_state(state)
@@ -78,6 +84,7 @@ impl Widget<State, Action> for ListPage {
        ListPage {
            notifications: self.notifications.move_with_state(state),
            shortcuts: self.shortcuts.move_with_state(state),
+
            help: self.help.move_with_state(state),
            props: ListPageProps::from(state),
            ..self
        }
@@ -90,6 +97,8 @@ impl Widget<State, Action> for ListPage {
    fn handle_key_event(&mut self, key: termion::event::Key) {
        if self.props.show_search {
            <Search as Widget<State, Action>>::handle_key_event(&mut self.search, key)
+
        } else if self.props.show_help {
+
            <Help as Widget<State, Action>>::handle_key_event(&mut self.help, key)
        } else {
            match key {
                Key::Esc | Key::Ctrl('c') => {
@@ -122,6 +131,9 @@ impl Widget<State, Action> for ListPage {
                Key::Char('/') => {
                    let _ = self.action_tx.send(Action::OpenSearch);
                }
+
                Key::Char('?') => {
+
                    let _ = self.action_tx.send(Action::OpenHelp);
+
                }
                _ => {
                    <Notifications as Widget<State, Action>>::handle_key_event(
                        &mut self.notifications,
@@ -133,16 +145,18 @@ impl Widget<State, Action> for ListPage {
    }
}

-
impl Render<()> for ListPage {
+
impl<'a> Render<()> for ListPage<'a> {
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, _area: Rect, _props: ()) {
        let area = frame.size();
        let layout = tui::flux::ui::layout::default_page(area, 0u16, 1u16);

        let shortcuts = if self.props.show_search {
            vec![
-
                Shortcut::new("esc", "back"),
-
                Shortcut::new("enter", "search"),
+
                Shortcut::new("esc", "cancel"),
+
                Shortcut::new("enter", "apply"),
            ]
+
        } else if self.props.show_help {
+
            vec![Shortcut::new("?", "close")]
        } else {
            match self.props.mode.selection() {
                SelectionMode::Id => vec![
@@ -153,6 +167,7 @@ impl Render<()> for ListPage {
                    Shortcut::new("enter", "show"),
                    Shortcut::new("c", "clear"),
                    Shortcut::new("/", "search"),
+
                    Shortcut::new("?", "help"),
                ],
            }
        };
@@ -165,6 +180,8 @@ impl Render<()> for ListPage {
                .render::<B>(frame, component_layout[0], ());
            self.search
                .render::<B>(frame, component_layout[1], SearchProps {});
+
        } else if self.props.show_help {
+
            self.help.render::<B>(frame, layout.component, ());
        } else {
            self.notifications.render::<B>(frame, layout.component, ());
        }
@@ -580,3 +597,278 @@ impl Render<SearchProps> for Search {
        );
    }
}
+

+
pub struct HelpProps<'a> {
+
    content: Text<'a>,
+
    focus: bool,
+
    page_size: usize,
+
}
+

+
impl<'a> From<&State> for HelpProps<'a> {
+
    fn from(state: &State) -> Self {
+
        let content = 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 notification (if --mode id)").gray().dim(),
+
                    ]
+
                    .to_vec(),
+
                ),
+
                Line::from(
+
                    [
+
                        Span::raw(format!("{key:>10}", key = "enter")).gray(),
+
                        Span::raw(" "),
+
                        Span::raw("Show notification").gray().dim(),
+
                    ]
+
                    .to_vec(),
+
                ),
+
                Line::from(
+
                    [
+
                        Span::raw(format!("{key:>10}", key = "c")).gray(),
+
                        Span::raw(" "),
+
                        Span::raw("Clear notifications").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:patch | is:issue | <search>")
+
                            .gray()
+
                            .dim(),
+
                    ]
+
                    .to_vec(),
+
                ),
+
                Line::from(
+
                    [
+
                        Span::raw(format!("{key:>10}", key = "Example")).gray(),
+
                        Span::raw(" "),
+
                        Span::raw("is:unseen is:patch Print").gray().dim(),
+
                    ]
+
                    .to_vec(),
+
                ),
+
            ]
+
            .to_vec(),
+
        );
+

+
        Self {
+
            content,
+
            focus: false,
+
            page_size: state.ui.page_size,
+
        }
+
    }
+
}
+

+
pub struct Help<'a> {
+
    /// Send messages
+
    pub action_tx: UnboundedSender<Action>,
+
    /// This widget's render properties
+
    pub props: HelpProps<'a>,
+
    /// Container header
+
    header: Header<Action>,
+
    /// Content widget
+
    content: Paragraph<Action>,
+
    /// Container footer
+
    footer: Footer<Action>,
+
}
+

+
impl<'a> Widget<State, Action> for Help<'a> {
+
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
+
    where
+
        Self: Sized,
+
    {
+
        Self {
+
            action_tx: action_tx.clone(),
+
            props: HelpProps::from(state),
+
            header: Header::new(state, action_tx.clone()),
+
            content: Paragraph::new(state, action_tx.clone()),
+
            footer: Footer::new(state, action_tx),
+
        }
+
        .move_with_state(state)
+
    }
+

+
    fn move_with_state(self, state: &State) -> Self
+
    where
+
        Self: Sized,
+
    {
+
        Self {
+
            props: HelpProps::from(state),
+
            header: self.header.move_with_state(state),
+
            content: self.content.move_with_state(state),
+
            footer: self.footer.move_with_state(state),
+
            ..self
+
        }
+
    }
+

+
    fn name(&self) -> &str {
+
        "help"
+
    }
+

+
    fn handle_key_event(&mut self, key: termion::event::Key) {
+
        let len = self.props.content.lines.len() + 1;
+
        let page_size = self.props.page_size;
+
        match key {
+
            Key::Esc => {
+
                let _ = self.action_tx.send(Action::Exit { selection: None });
+
            }
+
            Key::Char('?') => {
+
                let _ = self.action_tx.send(Action::CloseHelp);
+
            }
+
            Key::Up | Key::Char('k') => {
+
                self.content.prev(len, page_size);
+
            }
+
            Key::Down | Key::Char('j') => {
+
                self.content.next(len, page_size);
+
            }
+
            Key::PageUp => {
+
                self.content.prev_page(len, page_size);
+
            }
+
            Key::PageDown => {
+
                self.content.next_page(len, page_size);
+
            }
+
            Key::Home => {
+
                self.content.begin(len, page_size);
+
            }
+
            Key::End => {
+
                self.content.end(len, page_size);
+
            }
+
            _ => {}
+
        }
+
    }
+
}
+

+
impl<'a> Render<()> for Help<'a> {
+
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
        let [header_area, content_area, footer_area] = Layout::vertical([
+
            Constraint::Length(3),
+
            Constraint::Min(1),
+
            Constraint::Length(3),
+
        ])
+
        .areas(area);
+

+
        self.header.render::<B>(
+
            frame,
+
            header_area,
+
            HeaderProps {
+
                cells: [String::from(" Help ").into()],
+
                widths: [Constraint::Fill(1)],
+
                focus: self.props.focus,
+
                cutoff: usize::MIN,
+
                cutoff_after: usize::MAX,
+
            },
+
        );
+

+
        self.content.render::<B>(
+
            frame,
+
            content_area,
+
            ParagraphProps {
+
                content: self.props.content.clone(),
+
                focus: self.props.focus,
+
                has_footer: true,
+
                has_header: true,
+
            },
+
        );
+

+
        let progress = span::default(format!("{}%", self.content.progress())).dim();
+

+
        self.footer.render::<B>(
+
            frame,
+
            footer_area,
+
            FooterProps {
+
                cells: [String::new().into(), progress.clone().into()],
+
                widths: [Constraint::Fill(1), Constraint::Min(4)],
+
                focus: self.props.focus,
+
                cutoff: usize::MAX,
+
                cutoff_after: usize::MAX,
+
            },
+
        );
+

+
        let page_size = content_area.height as usize;
+
        if page_size != self.props.page_size {
+
            let _ = self.action_tx.send(Action::PageSize(page_size));
+
        }
+
    }
+
}