Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
inbox: Various improvements to flux app
Erik Kundt committed 2 years ago
commit 937ae12bceafa7216e57e9fdb14ccdba414715d1
parent e22f6e3390d0cbd0ed4cf460220cfd72d24aeef1
9 files changed +447 -1173
modified bin/commands/inbox.rs
@@ -1,14 +1,18 @@
+
#[path = "inbox/common.rs"]
+
mod common;
+
#[cfg(feature = "flux")]
+
#[path = "inbox/flux.rs"]
+
mod flux;
#[cfg(feature = "realm")]
#[path = "inbox/realm.rs"]
mod realm;

-
#[path = "inbox/common.rs"]
-
mod common;
-

use std::ffi::OsString;

use anyhow::anyhow;

+
use radicle::storage::ReadStorage;
+

use radicle_tui as tui;

use tui::common::cob::inbox::{self};
@@ -94,7 +98,7 @@ impl Args for Options {

                    match terminal::args::string(&val).as_str() {
                        "timestamp" => field = Some("timestamp"),
-
                        "rowid" => field = Some("id"),
+
                        "id" => field = Some("id"),
                        other => anyhow::bail!("unknown sorting field '{other}'"),
                    }
                }
@@ -125,6 +129,7 @@ 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;
    use tui::common::context;
    use tui::common::log;
@@ -156,10 +161,32 @@ pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()>
}

#[cfg(feature = "flux")]
-
pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()> {
+
#[tokio::main]
+
pub async fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()> {
+
    let (_, rid) = radicle::rad::cwd()
+
        .map_err(|_| anyhow!("this command must be run in the context of a project"))?;
+

    match options.op {
-
        Operation::Select { opts: _ } => {
-
            anyhow::bail!("operation not yet implemented with flux")
+
        Operation::Select { opts } => {
+
            let profile = terminal::profile()?;
+
            let repository = profile.storage.repository(rid).unwrap();
+

+
            log::enable(&profile, "inbox", "select")?;
+

+
            let context = flux::select::Context {
+
                profile,
+
                repository,
+
                mode: opts.mode,
+
                filter: opts.filter.clone(),
+
                sort_by: opts.sort_by,
+
            };
+
            let output = flux::select::App::new(context).run().await?;
+

+
            let output = output
+
                .map(|o| serde_json::to_string(&o).unwrap_or_default())
+
                .unwrap_or_default();
+

+
            eprint!("{output}");
        }
    }
}
added bin/commands/inbox/flux/select.rs
@@ -0,0 +1,124 @@
+
#[path = "select/ui.rs"]
+
mod ui;
+

+
use anyhow::Result;
+

+
use radicle::node::notifications::NotificationId;
+

+
use radicle::storage::git::Repository;
+
use radicle::Profile;
+
use radicle_tui as tui;
+

+
use tui::common::cob::inbox::{self};
+
use tui::flux::store::{State, Store};
+
use tui::flux::termination::{self, Interrupted};
+
use tui::flux::ui::cob::NotificationItem;
+
use tui::flux::ui::Frontend;
+
use tui::Exit;
+

+
use ui::ListPage;
+

+
use super::super::common;
+

+
type Selection = tui::Selection<NotificationId>;
+

+
pub struct Context {
+
    pub profile: Profile,
+
    pub repository: Repository,
+
    pub mode: common::Mode,
+
    pub filter: inbox::Filter,
+
    pub sort_by: inbox::SortBy,
+
}
+

+
pub struct App {
+
    context: Context,
+
}
+

+
#[derive(Clone, Debug)]
+
pub struct InboxState {
+
    notifications: Vec<NotificationItem>,
+
    selected: Option<NotificationItem>,
+
    mode: common::Mode,
+
}
+

+
impl TryFrom<&Context> for InboxState {
+
    type Error = anyhow::Error;
+

+
    fn try_from(context: &Context) -> Result<Self, Self::Error> {
+
        let notifications = inbox::all(&context.repository, &context.profile)?;
+
        let mut items = vec![];
+

+
        // Convert into UI items
+
        for notif in &notifications {
+
            if let Ok(notif) =
+
                NotificationItem::try_from((&context.profile, &context.repository, notif))
+
            {
+
                items.push(notif);
+
            }
+
        }
+

+
        // Apply sorting
+
        match context.sort_by.field {
+
            "timestamp" => items.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)),
+
            "id" => items.sort_by(|a, b| a.id.cmp(&b.id)),
+
            _ => {}
+
        }
+
        if context.sort_by.reverse {
+
            items.reverse();
+
        }
+
        let selected = items.first().cloned();
+

+
        Ok(Self {
+
            notifications: items,
+
            selected,
+
            mode: context.mode.clone(),
+
        })
+
    }
+
}
+

+
pub enum Action {
+
    Exit { selection: Option<Selection> },
+
    Select { item: NotificationItem },
+
}
+

+
impl State<Action, Selection> for InboxState {
+
    fn tick(&self) {}
+

+
    fn handle_action(&mut self, action: Action) -> Option<Exit<Selection>> {
+
        match action {
+
            Action::Select { item } => {
+
                self.selected = Some(item);
+
                None
+
            }
+
            Action::Exit { selection } => Some(Exit { value: selection }),
+
        }
+
    }
+
}
+

+
impl App {
+
    pub fn new(context: Context) -> Self {
+
        Self { context }
+
    }
+

+
    pub async fn run(&self) -> Result<Option<Selection>> {
+
        let (terminator, mut interrupt_rx) = termination::create_termination();
+
        let (store, state_rx) = Store::<Action, InboxState, Selection>::new();
+
        let (frontend, action_rx) = Frontend::<Action>::new();
+
        let state = InboxState::try_from(&self.context)?;
+

+
        tokio::try_join!(
+
            store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()),
+
            frontend
+
                .main_loop::<InboxState, ListPage, Selection>(state_rx, interrupt_rx.resubscribe()),
+
        )?;
+

+
        if let Ok(reason) = interrupt_rx.recv().await {
+
            match reason {
+
                Interrupted::User { payload } => Ok(payload),
+
                Interrupted::OsSignal => anyhow::bail!("exited because of an os sig int"),
+
            }
+
        } else {
+
            anyhow::bail!("exited because of an unexpected error");
+
        }
+
    }
+
}
added bin/commands/inbox/flux/select/ui.rs
@@ -0,0 +1,289 @@
+
use std::vec;
+

+
use tokio::sync::mpsc::UnboundedSender;
+

+
use termion::event::Key;
+

+
use ratatui::backend::Backend;
+
use ratatui::layout::{Constraint, Rect};
+
use ratatui::widgets::Cell;
+

+
use radicle_tui as tui;
+

+
use tui::flux::ui::cob::NotificationItem;
+
use tui::flux::ui::span;
+
use tui::flux::ui::widget::{
+
    FooterProps, Render, Shortcut, Shortcuts, ShortcutsProps, Table, TableProps, Widget,
+
};
+
use tui::Selection;
+

+
use super::common::Mode;
+
use super::{Action, InboxState};
+

+
pub struct ListPageProps {
+
    selected: Option<NotificationItem>,
+
    mode: Mode,
+
}
+

+
impl From<&InboxState> for ListPageProps {
+
    fn from(state: &InboxState) -> Self {
+
        Self {
+
            selected: state.selected.clone(),
+
            mode: state.mode.clone(),
+
        }
+
    }
+
}
+

+
pub struct ListPage {
+
    /// Action sender
+
    pub action_tx: UnboundedSender<Action>,
+
    /// State mapped props
+
    props: ListPageProps,
+
    /// Notification widget
+
    notifications: Notifications,
+
    /// Shortcut widget
+
    shortcuts: Shortcuts<Action>,
+
}
+

+
impl Widget<InboxState, Action> for ListPage {
+
    fn new(state: &InboxState, action_tx: UnboundedSender<Action>) -> Self
+
    where
+
        Self: Sized,
+
    {
+
        Self {
+
            action_tx: action_tx.clone(),
+
            props: ListPageProps::from(state),
+
            notifications: Notifications::new(state, action_tx.clone()),
+
            shortcuts: Shortcuts::new(state, action_tx.clone()),
+
        }
+
        .move_with_state(state)
+
    }
+

+
    fn move_with_state(self, state: &InboxState) -> Self
+
    where
+
        Self: Sized,
+
    {
+
        ListPage {
+
            notifications: self.notifications.move_with_state(state),
+
            shortcuts: self.shortcuts.move_with_state(state),
+
            props: ListPageProps::from(state),
+
            ..self
+
        }
+
    }
+

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

+
    fn handle_key_event(&mut self, key: termion::event::Key) {
+
        match key {
+
            Key::Char('q') => {
+
                let _ = self.action_tx.send(Action::Exit { selection: None });
+
            }
+
            Key::Char('\n') => {
+
                if let Some(selected) = &self.props.selected {
+
                    let selection = match self.props.mode {
+
                        Mode::Operation => Selection::default()
+
                            .with_operation("show".to_string())
+
                            .with_id(selected.id),
+
                        Mode::Id => Selection::default().with_id(selected.id),
+
                    };
+
                    let _ = self.action_tx.send(Action::Exit {
+
                        selection: Some(selection),
+
                    });
+
                }
+
            }
+
            Key::Char('c') => {
+
                if let Some(selected) = &self.props.selected {
+
                    let _ = self.action_tx.send(Action::Exit {
+
                        selection: Some(
+
                            Selection::default()
+
                                .with_operation("clear".to_string())
+
                                .with_id(selected.id),
+
                        ),
+
                    });
+
                }
+
            }
+
            _ => {
+
                <Notifications as Widget<InboxState, Action>>::handle_key_event(
+
                    &mut self.notifications,
+
                    key,
+
                );
+
            }
+
        }
+
    }
+
}
+

+
impl Render<()> for ListPage {
+
    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 = match self.props.mode {
+
            Mode::Id => vec![Shortcut::new("enter", "select"), Shortcut::new("q", "quit")],
+
            Mode::Operation => vec![
+
                Shortcut::new("enter", "show"),
+
                Shortcut::new("c", "clear"),
+
                Shortcut::new("q", "quit"),
+
            ],
+
        };
+

+
        self.notifications.render::<B>(frame, layout.component, ());
+
        self.shortcuts.render::<B>(
+
            frame,
+
            layout.shortcuts,
+
            ShortcutsProps {
+
                shortcuts,
+
                divider: '∙',
+
            },
+
        );
+
    }
+
}
+

+
struct NotificationsProps {
+
    notifications: Vec<NotificationItem>,
+
}
+

+
impl From<&InboxState> for NotificationsProps {
+
    fn from(state: &InboxState) -> Self {
+
        Self {
+
            notifications: state.notifications.clone(),
+
        }
+
    }
+
}
+

+
struct Notifications {
+
    /// Action sender
+
    action_tx: UnboundedSender<Action>,
+
    /// State mapped props
+
    props: NotificationsProps,
+
    /// Notification table
+
    table: Table<Action>,
+
}
+

+
impl Widget<InboxState, Action> for Notifications {
+
    fn new(state: &InboxState, action_tx: UnboundedSender<Action>) -> Self {
+
        Self {
+
            action_tx: action_tx.clone(),
+
            props: NotificationsProps::from(state),
+
            table: Table::new(state, action_tx.clone()),
+
        }
+
    }
+

+
    fn move_with_state(self, state: &InboxState) -> Self
+
    where
+
        Self: Sized,
+
    {
+
        Self {
+
            props: NotificationsProps::from(state),
+
            table: self.table.move_with_state(state),
+
            ..self
+
        }
+
    }
+

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

+
    fn handle_key_event(&mut self, key: Key) {
+
        match key {
+
            Key::Up => {
+
                self.table.prev();
+

+
                let selected = self
+
                    .table
+
                    .selected()
+
                    .and_then(|selected| self.props.notifications.get(selected));
+

+
                // TODO: propagate error
+
                if let Some(notif) = selected {
+
                    let _ = self.action_tx.send(Action::Select {
+
                        item: notif.clone(),
+
                    });
+
                }
+
            }
+
            Key::Down => {
+
                self.table.next(self.props.notifications.len());
+

+
                let selected = self
+
                    .table
+
                    .selected()
+
                    .and_then(|selected| self.props.notifications.get(selected));
+

+
                // TODO: propagate error
+
                if let Some(notif) = selected {
+
                    let _ = self.action_tx.send(Action::Select {
+
                        item: notif.clone(),
+
                    });
+
                }
+
            }
+
            _ => {}
+
        }
+
    }
+
}
+

+
impl Render<()> for Notifications {
+
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
        let header: [Cell; 8] = [
+
            String::from("").into(),
+
            String::from(" ● ").into(),
+
            String::from("ID / Name").into(),
+
            String::from("Summary").into(),
+
            String::from("Type").into(),
+
            String::from("Status").into(),
+
            String::from("Author").into(),
+
            String::from("Updated").into(),
+
        ];
+

+
        let widths = [
+
            Constraint::Length(5),
+
            Constraint::Length(3),
+
            Constraint::Length(20),
+
            Constraint::Fill(1),
+
            Constraint::Length(8),
+
            Constraint::Length(10),
+
            Constraint::Length(15),
+
            Constraint::Length(18),
+
        ];
+

+
        let progress = {
+
            let step = self
+
                .table
+
                .selected()
+
                .map(|selected| selected.saturating_add(1).to_string())
+
                .unwrap_or("-".to_string());
+
            let length = self.props.notifications.len().to_string();
+

+
            span::badge(format!("{}/{}", step, length))
+
        };
+

+
        let footer = FooterProps {
+
            cells: [
+
                span::badge("/".to_string()),
+
                String::from("").into(),
+
                String::from("").into(),
+
                progress.clone(),
+
            ]
+
            .to_vec(),
+
            widths: [
+
                Constraint::Length(3),
+
                Constraint::Fill(1),
+
                Constraint::Fill(1),
+
                Constraint::Length(progress.width() as u16),
+
            ]
+
            .to_vec(),
+
        };
+

+
        self.table.render::<B>(
+
            frame,
+
            area,
+
            TableProps {
+
                items: self.props.notifications.to_vec(),
+
                focus: false,
+
                widths,
+
                header,
+
                footer: Some(footer),
+
            },
+
        );
+
    }
+
}
deleted bin/commands/inbox/select/flux.rs
@@ -1,109 +0,0 @@
-
#[path = "flux/ui.rs"]
-
mod ui;
-

-
use anyhow::Result;
-

-
use radicle::node::notifications::NotificationId;
-

-
use radicle_tui as tui;
-

-
use tui::cob::inbox::{self, Filter};
-
use tui::context::Context;
-
use tui::flux::store::{State, Store};
-
use tui::flux::termination::{self, Interrupted};
-
use tui::flux::ui::cob::NotificationItem;
-
use tui::flux::ui::Frontend;
-
use tui::Exit;
-

-
use crate::tui_inbox::select::flux::ui::ListPage;
-

-
#[derive(Debug, Default, Clone, PartialEq, Eq)]
-
pub enum Mode {
-
    Id,
-
    #[default]
-
    Operation,
-
}
-

-
pub struct App {
-
    context: Context,
-
    _filter: Filter,
-
}
-

-
#[derive(Clone, Debug)]
-
pub struct InboxState {
-
    notifications: Vec<NotificationItem>,
-
    selected: Option<NotificationId>,
-
}
-

-
impl TryFrom<&Context> for InboxState {
-
    type Error = anyhow::Error;
-

-
    fn try_from(context: &Context) -> Result<Self, Self::Error> {
-
        let notifications = inbox::all(context.repository(), context.profile())?;
-
        let mut items = vec![];
-

-
        for notif in &notifications {
-
            if let Ok(notif) = NotificationItem::try_from((context.repository(), notif)) {
-
                items.push(notif);
-
            }
-
        }
-

-
        Ok(Self {
-
            notifications: items,
-
            selected: None,
-
        })
-
    }
-
}
-

-
pub enum Action {
-
    Exit,
-
    Select(NotificationId),
-
}
-

-
impl State<Action> for InboxState {
-
    type Exit = Exit<String>;
-

-
    fn tick(&self) {}
-

-
    fn handle_action(&mut self, action: Action) -> Option<Exit<String>> {
-
        match action {
-
            Action::Select(id) => {
-
                self.selected = Some(id);
-
                None
-
            }
-
            Action::Exit => Some(Exit { value: None }),
-
        }
-
    }
-
}
-

-
impl App {
-
    pub fn new(context: Context, filter: Filter) -> Self {
-
        Self {
-
            context,
-
            _filter: filter,
-
        }
-
    }
-

-
    pub async fn run(&self) -> Result<()> {
-
        let (terminator, mut interrupt_rx) = termination::create_termination();
-
        let (store, state_rx) = Store::<Action, InboxState>::new();
-
        let (frontend, action_rx) = Frontend::<Action>::new();
-
        let state = InboxState::try_from(&self.context)?;
-

-
        tokio::try_join!(
-
            store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()),
-
            frontend.main_loop::<InboxState, ListPage>(state_rx, interrupt_rx.resubscribe()),
-
        )?;
-

-
        if let Ok(reason) = interrupt_rx.recv().await {
-
            match reason {
-
                Interrupted::UserInt => {}
-
                Interrupted::OsSigInt => println!("exited because of an os sig int"),
-
            }
-
        } else {
-
            println!("exited because of an unexpected error");
-
        }
-

-
        Ok(())
-
    }
-
}
deleted bin/commands/inbox/select/flux/ui.rs
@@ -1,228 +0,0 @@
-
use tokio::sync::mpsc::UnboundedSender;
-

-
use termion::event::Key;
-

-
use ratatui::backend::Backend;
-
use ratatui::layout::{Constraint, Rect};
-
use ratatui::widgets::Cell;
-

-
use radicle_tui as tui;
-

-
use tui::flux::ui::cob::NotificationItem;
-
use tui::flux::ui::span;
-
use tui::flux::ui::widget::{
-
    FooterProps, Render, Shortcut, Shortcuts, ShortcutsProps, Table, TableProps, Widget,
-
};
-

-
use crate::tui_inbox::select::flux::{Action, InboxState};
-

-
pub struct ListPage {
-
    /// Action sender
-
    pub action_tx: UnboundedSender<Action>,
-
    /// notification widget
-
    notifications: Notifications,
-
    /// Shortcut widget
-
    shortcuts: Shortcuts<Action>,
-
}
-

-
impl Widget<InboxState, Action> for ListPage {
-
    fn new(state: &InboxState, action_tx: UnboundedSender<Action>) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        Self {
-
            action_tx: action_tx.clone(),
-
            notifications: Notifications::new(state, action_tx.clone()),
-
            shortcuts: Shortcuts::new(state, action_tx.clone()),
-
        }
-
        .move_with_state(state)
-
    }
-

-
    fn move_with_state(self, state: &InboxState) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        ListPage {
-
            notifications: self.notifications.move_with_state(state),
-
            shortcuts: self.shortcuts.move_with_state(state),
-
            ..self
-
        }
-
    }
-

-
    fn name(&self) -> &str {
-
        "list-page"
-
    }
-

-
    fn handle_key_event(&mut self, key: termion::event::Key) {
-
        match key {
-
            Key::Char('q') => {
-
                let _ = self.action_tx.send(Action::Exit);
-
            }
-
            _ => {
-
                <Notifications as Widget<InboxState, Action>>::handle_key_event(
-
                    &mut self.notifications,
-
                    key,
-
                );
-
            }
-
        }
-
    }
-
}
-

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

-
        self.notifications.render::<B>(frame, layout.component, ());
-
        self.shortcuts.render::<B>(
-
            frame,
-
            layout.shortcuts,
-
            ShortcutsProps {
-
                shortcuts: vec![Shortcut::new("enter", "select"), Shortcut::new("q", "quit")],
-
                divider: '∙',
-
            },
-
        );
-
    }
-
}
-

-
struct NotificationsProps {
-
    notifications: Vec<NotificationItem>,
-
}
-

-
impl From<&InboxState> for NotificationsProps {
-
    fn from(state: &InboxState) -> Self {
-
        Self {
-
            notifications: state.notifications.clone(),
-
        }
-
    }
-
}
-

-
struct Notifications {
-
    /// Sending actions to the state store
-
    action_tx: UnboundedSender<Action>,
-
    /// State Mapped RoomList Props
-
    props: NotificationsProps,
-
    /// Notification table
-
    table: Table<Action>,
-
}
-

-
impl Widget<InboxState, Action> for Notifications {
-
    fn new(state: &InboxState, action_tx: UnboundedSender<Action>) -> Self {
-
        Self {
-
            action_tx: action_tx.clone(),
-
            props: NotificationsProps::from(state),
-
            table: Table::new(state, action_tx.clone()),
-
        }
-
    }
-

-
    fn move_with_state(self, state: &InboxState) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        Self {
-
            props: NotificationsProps::from(state),
-
            table: self.table.move_with_state(state),
-
            ..self
-
        }
-
    }
-

-
    fn name(&self) -> &str {
-
        "notification-list"
-
    }
-

-
    fn handle_key_event(&mut self, key: Key) {
-
        match key {
-
            Key::Up => {
-
                self.table.prev();
-

-
                let selected = self
-
                    .table
-
                    .selected()
-
                    .and_then(|selected| self.props.notifications.get(selected));
-

-
                // TODO: propagate error
-
                if let Some(notif) = selected {
-
                    let _ = self.action_tx.send(Action::Select(notif.id));
-
                }
-
            }
-
            Key::Down => {
-
                self.table.next(self.props.notifications.len());
-

-
                let selected = self
-
                    .table
-
                    .selected()
-
                    .and_then(|selected| self.props.notifications.get(selected));
-

-
                // TODO: propagate error
-
                if let Some(notif) = selected {
-
                    let _ = self.action_tx.send(Action::Select(notif.id));
-
                }
-
            }
-
            _ => {}
-
        }
-
    }
-
}
-

-
impl Render<()> for Notifications {
-
    fn render<B: Backend>(&mut self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
-
        let header: [Cell; 7] = [
-
            String::from("").into(),
-
            String::from(" ● ").into(),
-
            String::from("Type").into(),
-
            String::from("Summary").into(),
-
            String::from("ID").into(),
-
            String::from("Status").into(),
-
            String::from("Updated").into(),
-
        ];
-

-
        let widths = [
-
            Constraint::Length(5),
-
            Constraint::Length(3),
-
            Constraint::Length(6),
-
            Constraint::Fill(1),
-
            Constraint::Length(15),
-
            Constraint::Length(10),
-
            Constraint::Length(18),
-
        ];
-

-
        let progress = {
-
            let step = self
-
                .table
-
                .selected()
-
                .map(|selected| selected.saturating_add(1).to_string())
-
                .unwrap_or("-".to_string());
-
            let length = self.props.notifications.len().to_string();
-

-
            span::badge(format!("{}/{}", step, length))
-
        };
-

-
        let footer = FooterProps {
-
            cells: [
-
                span::badge("/".to_string()),
-
                String::from("").into(),
-
                String::from("").into(),
-
                progress.clone(),
-
            ]
-
            .to_vec(),
-
            widths: [
-
                Constraint::Length(3),
-
                Constraint::Fill(1),
-
                Constraint::Fill(1),
-
                Constraint::Length(progress.width() as u16),
-
            ]
-
            .to_vec(),
-
        };
-

-
        self.table.render::<B>(
-
            frame,
-
            area,
-
            TableProps {
-
                items: self.props.notifications.to_vec(),
-
                focus: false,
-
                widths,
-
                header,
-
                footer: Some(footer),
-
            },
-
        );
-
    }
-
}
deleted bin/commands/inbox/select/realm.rs
@@ -1,233 +0,0 @@
-
#[path = "realm/event.rs"]
-
mod event;
-
#[path = "realm/page.rs"]
-
mod page;
-
#[path = "realm/ui.rs"]
-
mod ui;
-

-
use std::fmt::Display;
-
use std::hash::Hash;
-

-
use anyhow::Result;
-

-
use serde::{Serialize, Serializer};
-

-
use tuirealm::application::PollStrategy;
-
use tuirealm::{Application, Frame, NoUserEvent, Sub, SubClause};
-

-
use radicle_tui as tui;
-

-
use tui::cob::inbox::{Filter, SortBy};
-
use tui::context::Context;
-
use tui::ui::subscription;
-
use tui::ui::theme::Theme;
-
use tui::{Exit, PageStack, SelectionExit, Tui};
-

-
use page::ListView;
-

-
/// Wrapper around radicle's `PatchId` that serializes
-
/// to a human-readable string.
-
#[derive(Clone, Debug, Eq, PartialEq)]
-
pub struct PatchId(radicle::cob::patch::PatchId);
-

-
impl From<radicle::cob::patch::PatchId> for PatchId {
-
    fn from(value: radicle::cob::patch::PatchId) -> Self {
-
        PatchId(value)
-
    }
-
}
-

-
impl Display for PatchId {
-
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-
        write!(f, "{}", self.0)
-
    }
-
}
-

-
impl Serialize for PatchId {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        serializer.serialize_str(&format!("{}", *self.0))
-
    }
-
}
-

-
/// The application's subject. It tells the application
-
/// which widgets to render and which output to produce.
-
///
-
/// Depends on CLI arguments given by the user.
-
#[derive(Clone, Default, Debug, Eq, PartialEq)]
-
pub enum Mode {
-
    #[default]
-
    Operation,
-
    #[allow(dead_code)]
-
    Id,
-
}
-

-
/// The selected issue operation returned by the operation
-
/// selection widget.
-
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
-
pub enum InboxOperation {
-
    Show,
-
    Clear,
-
}
-

-
impl Display for InboxOperation {
-
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-
        match self {
-
            InboxOperation::Show => {
-
                write!(f, "show")
-
            }
-
            InboxOperation::Clear => {
-
                write!(f, "clear")
-
            }
-
        }
-
    }
-
}
-

-
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
-
pub enum ListCid {
-
    NotificationBrowser,
-
    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<SelectionExit>),
-
    Batch(Vec<Message>),
-
}
-

-
pub struct App {
-
    context: Context,
-
    pages: PageStack<Cid, Message>,
-
    theme: Theme,
-
    mode: Mode,
-
    filter: Filter,
-
    sort_by: SortBy,
-
    quit: bool,
-
    output: Option<SelectionExit>,
-
}
-

-
/// 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, mode: Mode, filter: Filter, sort_by: SortBy) -> Self {
-
        Self {
-
            context,
-
            pages: PageStack::default(),
-
            theme: Theme::default(),
-
            mode,
-
            filter,
-
            sort_by,
-
            quit: false,
-
            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.sort_by,
-
        ));
-
        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, SelectionExit> for App {
-
    fn init(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
-
        self.view_list(app, &self.theme.clone())?;
-

-
        // Add global key listener and subscribe to key events
-
        let global = tui::ui::global_listener().to_boxed();
-
        app.mount(
-
            Cid::GlobalListener,
-
            global,
-
            vec![Sub::new(
-
                subscription::quit_clause(tuirealm::event::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<SelectionExit>> {
-
        if self.quit {
-
            return Some(Exit {
-
                value: self.output.clone(),
-
            });
-
        }
-
        None
-
    }
-
}
deleted bin/commands/inbox/select/realm/event.rs
@@ -1,161 +0,0 @@
-
use radicle::node::notifications::NotificationId;
-

-
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 tui::{Id, SelectionExit};
-

-
use super::ui::{IdSelect, OperationSelect};
-
use super::{InboxOperation, Message};
-

-
/// 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<NotificationId> {
-
            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 output = SelectionExit::default().with_id(Id::Notification(id));
-
                Message::Quit(Some(output))
-
            }),
-
            _ => None,
-
        }
-
    }
-
}
-

-
impl tuirealm::Component<Message, NoUserEvent> for Widget<OperationSelect> {
-
    fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
-
        let mut submit = || -> Option<NotificationId> {
-
            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 exit = SelectionExit::default()
-
                    .with_operation(InboxOperation::Show.to_string())
-
                    .with_id(Id::Notification(id));
-
                Message::Quit(Some(exit))
-
            }),
-
            Event::Keyboard(KeyEvent {
-
                code: Key::Char('c'),
-
                ..
-
            }) => submit().map(|id| {
-
                let exit = SelectionExit::default()
-
                    .with_operation(InboxOperation::Clear.to_string())
-
                    .with_id(Id::Notification(id));
-
                Message::Quit(Some(exit))
-
            }),
-
            _ => 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
-
    }
-
}
deleted bin/commands/inbox/select/realm/page.rs
@@ -1,168 +0,0 @@
-
use std::collections::HashMap;
-

-
use anyhow::Result;
-

-
use tuirealm::{AttrValue, Attribute, Frame, NoUserEvent};
-

-
use radicle_tui as tui;
-

-
use tui::common::cob::inbox::{Filter, SortBy};
-
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::{ui, Application, Cid, ListCid, Message, Mode};
-

-
///
-
/// Home
-
///
-
pub struct ListView {
-
    active_component: ListCid,
-
    mode: Mode,
-
    filter: Filter,
-
    sort_by: SortBy,
-
    shortcuts: HashMap<ListCid, Widget<Shortcuts>>,
-
}
-

-
impl ListView {
-
    pub fn new(mode: Mode, filter: Filter, sort_by: SortBy) -> Self {
-
        Self {
-
            active_component: ListCid::NotificationBrowser,
-
            mode,
-
            filter,
-
            sort_by,
-
            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::NotificationBrowser))?;
-
        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, 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 browser = ui::operation_select(theme, context, self.filter.clone(), self.sort_by, None)
-
            .to_boxed();
-
        self.shortcuts = browser.as_ref().shortcuts();
-

-
        match self.mode {
-
            Mode::Id => {
-
                let notif_browser =
-
                    ui::id_select(theme, context, self.filter.clone(), self.sort_by, None)
-
                        .to_boxed();
-
                self.shortcuts = notif_browser.as_ref().shortcuts();
-

-
                app.remount(Cid::List(ListCid::NotificationBrowser), browser, vec![])?;
-
            }
-
            Mode::Operation => {
-
                let notif_browser =
-
                    ui::operation_select(theme, context, self.filter.clone(), self.sort_by, None)
-
                        .to_boxed();
-
                self.shortcuts = notif_browser.as_ref().shortcuts();
-

-
                app.remount(Cid::List(ListCid::NotificationBrowser), 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::NotificationBrowser))?;
-
        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<()> {
-
        Ok(())
-
    }
-

-
    fn unsubscribe(&self, _app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
-
        Ok(())
-
    }
-
}
deleted bin/commands/inbox/select/realm/ui.rs
@@ -1,267 +0,0 @@
-
use std::collections::HashMap;
-

-
use radicle::node::notifications::Notification;
-

-
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::inbox::{Filter, SortBy};
-
use tui::common::context::Context;
-
use tui::realm::ui::cob::NotificationItem;
-
use tui::realm::ui::theme::{style, Theme};
-
use tui::realm::ui::widget::context::{ContextBar, Progress, Shortcuts};
-
use tui::realm::ui::widget::label::{self};
-
use tui::realm::ui::widget::list::{ColumnWidth, Table};
-
use tui::realm::ui::widget::{Widget, WidgetComponent};
-

-
use super::ListCid;
-

-
pub struct NotificationBrowser {
-
    items: Vec<NotificationItem>,
-
    table: Widget<Table<NotificationItem, 7>>,
-
}
-

-
impl NotificationBrowser {
-
    pub fn new(
-
        theme: &Theme,
-
        context: &Context,
-
        sort_by: SortBy,
-
        selected: Option<Notification>,
-
    ) -> Self {
-
        let header = [
-
            label::header(""),
-
            label::header(" ● "),
-
            label::header("Type"),
-
            label::header("Summary"),
-
            label::header("ID"),
-
            label::header("Status"),
-
            label::header("Updated"),
-
        ];
-
        let widths = [
-
            ColumnWidth::Fixed(5),
-
            ColumnWidth::Fixed(3),
-
            ColumnWidth::Fixed(6),
-
            ColumnWidth::Grow,
-
            ColumnWidth::Fixed(15),
-
            ColumnWidth::Fixed(10),
-
            ColumnWidth::Fixed(18),
-
        ];
-

-
        let mut items = vec![];
-
        for notification in context.notifications() {
-
            if let Ok(item) =
-
                NotificationItem::try_from((context.repository(), notification.clone()))
-
            {
-
                items.push(item);
-
            }
-
        }
-

-
        match sort_by.field {
-
            "timestamp" => items.sort_by(|a, b| b.timestamp().cmp(a.timestamp())),
-
            "id" => items.sort_by(|a, b| b.id().cmp(a.id())),
-
            _ => {}
-
        }
-
        if sort_by.reverse {
-
            items.reverse();
-
        }
-

-
        let selected = match selected {
-
            Some(notif) => {
-
                Some(NotificationItem::try_from((context.repository(), notif.clone())).unwrap())
-
            }
-
            _ => items.first().cloned(),
-
        };
-

-
        let table = Widget::new(Table::new(&items, selected, header, widths, theme.clone()));
-

-
        Self { items, table }
-
    }
-

-
    pub fn items(&self) -> &Vec<NotificationItem> {
-
        &self.items
-
    }
-
}
-

-
impl WidgetComponent for NotificationBrowser {
-
    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 IdSelect {
-
    theme: Theme,
-
    browser: Widget<NotificationBrowser>,
-
}
-

-
impl IdSelect {
-
    pub fn new(theme: Theme, browser: Widget<NotificationBrowser>) -> Self {
-
        Self { theme, browser }
-
    }
-

-
    pub fn items(&self) -> &Vec<NotificationItem> {
-
        self.browser.items()
-
    }
-

-
    pub fn shortcuts(&self) -> HashMap<ListCid, Widget<Shortcuts>> {
-
        [(
-
            ListCid::NotificationBrowser,
-
            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<NotificationBrowser>,
-
}
-

-
impl OperationSelect {
-
    pub fn new(theme: Theme, browser: Widget<NotificationBrowser>) -> Self {
-
        Self { theme, browser }
-
    }
-

-
    pub fn items(&self) -> &Vec<NotificationItem> {
-
        self.browser.items()
-
    }
-

-
    pub fn shortcuts(&self) -> HashMap<ListCid, Widget<Shortcuts>> {
-
        [(
-
            ListCid::NotificationBrowser,
-
            tui::realm::ui::shortcuts(
-
                &self.theme,
-
                vec![
-
                    tui::realm::ui::shortcut(&self.theme, "enter", "show"),
-
                    tui::realm::ui::shortcut(&self.theme, "c", "clear"),
-
                    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 id_select(
-
    theme: &Theme,
-
    context: &Context,
-
    _filter: Filter,
-
    sort_by: SortBy,
-
    selected: Option<Notification>,
-
) -> Widget<IdSelect> {
-
    let browser = Widget::new(NotificationBrowser::new(theme, context, sort_by, selected));
-

-
    Widget::new(IdSelect::new(theme.clone(), browser))
-
}
-

-
pub fn operation_select(
-
    theme: &Theme,
-
    context: &Context,
-
    _filter: Filter,
-
    sort_by: SortBy,
-
    selected: Option<Notification>,
-
) -> Widget<OperationSelect> {
-
    let browser = Widget::new(NotificationBrowser::new(theme, context, sort_by, selected));
-

-
    Widget::new(OperationSelect::new(theme.clone(), browser))
-
}
-

-
pub fn browse_context(
-
    _context: &Context,
-
    _theme: &Theme,
-
    _filter: Filter,
-
    progress: Progress,
-
) -> Widget<ContextBar> {
-
    let context = label::reversable("/").style(style::magenta_reversed());
-
    let filter = label::default("").style(style::magenta_dim());
-

-
    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.clone(),
-
            spacer.clone(),
-
            spacer.clone(),
-
            spacer.clone(),
-
            spacer.clone(),
-
            spacer.clone(),
-
        ]),
-
        label::group(&[progress]),
-
    );
-

-
    Widget::new(context_bar).height(1)
-
}