Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
tui: Use table model in patch browser
Erik Kundt committed 2 years ago
commit b3581de617fdcd64bbd12620c0218546dae3bb27
parent 4245406b89ccee9da9887b089cc6be2725788201
5 files changed +142 -168
modified radicle-tui/src/app.rs
@@ -4,14 +4,14 @@ pub mod subscription;

use anyhow::Result;

+
use radicle::cob::patch::{PatchId, Patches};
use radicle::identity::{Id, Project};
use radicle::profile::Profile;
+
use radicle::storage::ReadStorage;

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

-
use radicle_tui::cob::patch::{self};
-

use radicle_tui::ui;
use radicle_tui::ui::theme::{self, Theme};
use radicle_tui::Tui;
@@ -45,13 +45,11 @@ pub enum Cid {

/// Messages handled by this application.
#[derive(Debug, Eq, PartialEq)]
-
pub enum HomeMessage {
-
    PatchChanged(usize),
-
}
+
pub enum HomeMessage {}

#[derive(Debug, Eq, PartialEq)]
pub enum PatchMessage {
-
    Show,
+
    Show(PatchId),
    Leave,
}

@@ -99,8 +97,7 @@ impl App {
        app: &mut Application<Cid, Message, NoUserEvent>,
        theme: &Theme,
    ) -> Result<()> {
-
        let patches = patch::load_all(&self.context.profile, self.context.id);
-
        let home = Box::new(HomeView::new(patches));
+
        let home = Box::<HomeView>::default();
        self.pages.push(home, app, &self.context, theme)?;

        Ok(())
@@ -109,29 +106,26 @@ impl App {
    fn view_patch(
        &mut self,
        app: &mut Application<Cid, Message, NoUserEvent>,
+
        id: PatchId,
        theme: &Theme,
    ) -> Result<()> {
-
        let page = self.pages.peek_mut()?;
-
        let state = page.state().unwrap_map();
-
        let patches = state
-
            .and_then(|mut values| values.remove("patches"))
-
            .and_then(|value| value.unwrap_patches());
-

-
        match patches {
-
            Some((patches, selection)) => match patches.get(selection) {
-
                Some((id, patch)) => {
-
                    let view = Box::new(PatchView::new((*id, patch.clone())));
-
                    self.pages.push(view, app, &self.context, theme)?;
-

-
                    Ok(())
-
                }
-
                None => Err(anyhow::anyhow!(
-
                    "Could not mount 'page::PatchView'. Patch not found."
-
                )),
-
            },
-
            None => Err(anyhow::anyhow!(
-
                "Could not mount 'page::PatchView'. No state value for 'patches' found."
-
            )),
+
        let repo = self
+
            .context
+
            .profile
+
            .storage
+
            .repository(self.context.id)
+
            .unwrap();
+
        let patches = Patches::open(&repo).unwrap();
+

+
        if let Some(patch) = patches.get(&id)? {
+
            let view = Box::new(PatchView::new((id, patch)));
+
            self.pages.push(view, app, &self.context, theme)?;
+

+
            Ok(())
+
        } else {
+
            Err(anyhow::anyhow!(
+
                "Could not mount 'page::PatchView'. Patch not found."
+
            ))
        }
    }
}
@@ -159,8 +153,8 @@ impl Tui<Cid, Message> for App {
                let theme = theme::default_dark();
                for message in messages {
                    match message {
-
                        Message::Patch(PatchMessage::Show) => {
-
                            self.view_patch(app, &theme)?;
+
                        Message::Patch(PatchMessage::Show(id)) => {
+
                            self.view_patch(app, id, &theme)?;
                        }
                        Message::Patch(PatchMessage::Leave) => {
                            self.pages.pop(app)?;
modified radicle-tui/src/app/event.rs
@@ -1,21 +1,16 @@
-
use radicle::cob::patch::{Patch, PatchId};
-

use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection};
use tuirealm::event::{Event, Key, KeyEvent};
use tuirealm::{MockComponent, NoUserEvent, State, StateValue};

use radicle_tui::ui::widget::common::container::{GlobalListener, LabeledContainer, Tabs};
use radicle_tui::ui::widget::common::context::{ContextBar, Shortcuts};
-

use radicle_tui::ui::widget::common::list::PropertyList;
-

-
use radicle_tui::ui::widget::common::Browser;
-
use radicle_tui::ui::widget::home::{Dashboard, IssueBrowser};
+
use radicle_tui::ui::widget::home::{Dashboard, IssueBrowser, PatchBrowser};
use radicle_tui::ui::widget::patch;

use radicle_tui::ui::widget::Widget;

-
use super::{HomeMessage, Message, PatchMessage};
+
use super::{Message, PatchMessage};

/// 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
@@ -50,28 +45,24 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<Tabs> {
    }
}

-
impl tuirealm::Component<Message, NoUserEvent> for Widget<Browser<(PatchId, Patch)>> {
+
impl tuirealm::Component<Message, NoUserEvent> for Widget<PatchBrowser> {
    fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
        match event {
            Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
-
                match self.perform(Cmd::Move(MoveDirection::Up)) {
-
                    CmdResult::Changed(State::One(StateValue::Usize(index))) => {
-
                        Some(Message::Home(HomeMessage::PatchChanged(index)))
-
                    }
-
                    _ => Some(Message::Tick),
-
                }
+
                self.perform(Cmd::Move(MoveDirection::Up));
+
                Some(Message::Tick)
            }
            Event::Keyboard(KeyEvent {
                code: Key::Down, ..
-
            }) => match self.perform(Cmd::Move(MoveDirection::Down)) {
-
                CmdResult::Changed(State::One(StateValue::Usize(index))) => {
-
                    Some(Message::Home(HomeMessage::PatchChanged(index)))
-
                }
-
                _ => Some(Message::Tick),
-
            },
+
            }) => {
+
                self.perform(Cmd::Move(MoveDirection::Down));
+
                Some(Message::Tick)
+
            }
            Event::Keyboard(KeyEvent {
                code: Key::Enter, ..
-
            }) => Some(Message::Patch(PatchMessage::Show)),
+
            }) => self
+
                .selected_item()
+
                .map(|item| Message::Patch(PatchMessage::Show(item.id()))),
            _ => None,
        }
    }
modified radicle-tui/src/ui/widget/common.rs
@@ -3,62 +3,19 @@ pub mod context;
pub mod label;
pub mod list;

-
use std::marker::PhantomData;
-

-
use radicle::Profile;
-

-
use tuirealm::command::{Cmd, CmdResult};
-
use tuirealm::props::{AttrValue, Attribute, PropPayload, PropValue, TextSpan};
-
use tuirealm::tui::layout::Rect;
-
use tuirealm::{Frame, MockComponent, Props, State};
+
use tuirealm::props::{AttrValue, Attribute};
+
use tuirealm::MockComponent;

use container::{GlobalListener, Header, LabeledContainer, Tabs};
use context::{Shortcut, Shortcuts};
use label::Label;
-
use list::{List, Property, PropertyList, Table};
-

-
use super::{Widget, WidgetComponent};
-

-
use crate::ui::layout;
-
use crate::ui::theme::Theme;
+
use list::{Property, PropertyList};

-
pub struct Browser<T> {
-
    list: Widget<Table>,
-
    shortcuts: Widget<Shortcuts>,
-
    phantom: PhantomData<T>,
-
}
+
use self::list::{ColumnWidth, TableModel};

-
impl<T: List> Browser<T> {
-
    pub fn new(list: Widget<Table>, shortcuts: Widget<Shortcuts>) -> Self {
-
        Self {
-
            list,
-
            shortcuts,
-
            phantom: PhantomData,
-
        }
-
    }
-
}
+
use super::Widget;

-
impl<T: List> WidgetComponent for Browser<T> {
-
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
-
        let shortcuts_h = self
-
            .shortcuts
-
            .query(Attribute::Height)
-
            .unwrap_or(AttrValue::Size(0))
-
            .unwrap_size();
-
        let layout = layout::root_component(area, shortcuts_h);
-

-
        self.list.view(frame, layout[0]);
-
        self.shortcuts.view(frame, layout[1]);
-
    }
-

-
    fn state(&self) -> State {
-
        State::None
-
    }
-

-
    fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
-
        self.list.perform(cmd)
-
    }
-
}
+
use crate::ui::theme::Theme;

pub fn global_listener() -> Widget<GlobalListener> {
    Widget::new(GlobalListener::default())
@@ -76,34 +33,14 @@ pub fn label(content: &str) -> Widget<Label> {

pub fn reversable_label(content: &str) -> Widget<Label> {
    let content = &format!(" {content} ");
+

    label(content)
}

-
pub fn container_header(theme: &Theme, label: &str) -> Widget<Header> {
-
    let content = AttrValue::Payload(PropPayload::Vec(vec![PropValue::TextSpan(
-
        TextSpan::from(&format!(" {label} ")).fg(theme.colors.default_fg),
-
    )]));
-
    let widths = AttrValue::Payload(PropPayload::Vec(vec![PropValue::U16(100)]));
-

-
    Widget::new(Header::default())
-
        .content(content)
-
        .custom("widths", widths)
-
}
+
pub fn container_header(theme: &Theme, label: Widget<Label>) -> Widget<Header<(), 1>> {
+
    let model = TableModel::new([label], [ColumnWidth::Fixed(100)]);

-
pub fn table_header(theme: &Theme, labels: &[&str], widths: &[u16]) -> Widget<Header> {
-
    let content = labels
-
        .iter()
-
        .map(|label| {
-
            PropValue::TextSpan(TextSpan::from(label.to_string()).fg(theme.colors.default_fg))
-
        })
-
        .collect::<Vec<_>>();
-
    let widths = AttrValue::Payload(PropPayload::Vec(
-
        widths.iter().map(|w| PropValue::U16(*w)).collect(),
-
    ));
-

-
    Widget::new(Header::default())
-
        .content(AttrValue::Payload(PropPayload::Vec(content)))
-
        .custom("widths", widths)
+
    Widget::new(Header::new(model, theme.clone()))
}

pub fn labeled_container(
@@ -111,7 +48,10 @@ pub fn labeled_container(
    title: &str,
    component: Box<dyn MockComponent>,
) -> Widget<LabeledContainer> {
-
    let header = container_header(theme, title);
+
    let header = container_header(
+
        theme,
+
        label(&format!(" {title} ")).foreground(theme.colors.default_fg),
+
    );
    let container = LabeledContainer::new(header, component);

    Widget::new(container)
@@ -170,26 +110,3 @@ pub fn tabs(theme: &Theme, tabs: Vec<Widget<Label>>) -> Widget<Tabs> {

    Widget::new(tabs).height(2)
}
-

-
pub fn table(
-
    theme: &Theme,
-
    labels: &[&str],
-
    widths: &[u16],
-
    items: &[impl List],
-
    profile: &Profile,
-
) -> Widget<Table> {
-
    let items = items.iter().map(|item| item.row(theme, profile)).collect();
-

-
    let header = table_header(theme, labels, widths);
-
    let table = Table::new(header);
-

-
    let widths = AttrValue::Payload(PropPayload::Vec(
-
        widths.iter().map(|w| PropValue::U16(*w)).collect(),
-
    ));
-

-
    Widget::new(table)
-
        .content(AttrValue::Table(items))
-
        .custom("widths", widths)
-
        .background(theme.colors.labeled_container_bg)
-
        .highlight(theme.colors.item_list_highlighted_bg)
-
}
modified radicle-tui/src/ui/widget/home.rs
@@ -1,18 +1,22 @@
-
use radicle::cob::patch::{Patch, PatchId};
+
use radicle::prelude::{Id, Project};
+
use radicle::storage::ReadStorage;
use radicle::Profile;

-
use radicle::prelude::{Id, Project};
+
use radicle::cob::patch::Patches;
+

use tuirealm::command::{Cmd, CmdResult};
use tuirealm::tui::layout::Rect;
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State};

-
use super::{Widget, WidgetComponent};
-

+
use super::common;
use super::common::container::{LabeledContainer, Tabs};
use super::common::context::Shortcuts;
use super::common::label::Label;
-
use super::common::{self, Browser};
+
use super::common::list::{ColumnWidth, Table};

+
use super::{Widget, WidgetComponent};
+

+
use crate::ui::cob::PatchItem;
use crate::ui::layout;
use crate::ui::theme::Theme;

@@ -88,6 +92,84 @@ impl WidgetComponent for IssueBrowser {
    }
}

+
pub struct PatchBrowser {
+
    table: Widget<Table<PatchItem, 8>>,
+
    shortcuts: Widget<Shortcuts>,
+
}
+

+
impl PatchBrowser {
+
    pub fn new(profile: &Profile, id: &Id, shortcuts: Widget<Shortcuts>, theme: Theme) -> Self {
+
        let repo = profile.storage.repository(*id).unwrap();
+
        let patches = Patches::open(&repo)
+
            .and_then(|patches| patches.all().map(|iter| iter.flatten().collect::<Vec<_>>()));
+

+
        let header = [
+
            common::label(" ● "),
+
            common::label("ID"),
+
            common::label("Title"),
+
            common::label("Author"),
+
            common::label("Head"),
+
            common::label("+"),
+
            common::label("-"),
+
            common::label("Updated"),
+
        ];
+

+
        let widths = [
+
            ColumnWidth::Fixed(3),
+
            ColumnWidth::Fixed(7),
+
            ColumnWidth::Grow,
+
            ColumnWidth::Fixed(21),
+
            ColumnWidth::Fixed(7),
+
            ColumnWidth::Fixed(4),
+
            ColumnWidth::Fixed(4),
+
            ColumnWidth::Fixed(18),
+
        ];
+

+
        let mut items = vec![];
+
        if let Ok(mut patches) = patches {
+
            patches.sort_by(|(_, a, _), (_, b, _)| b.timestamp().cmp(&a.timestamp()));
+
            patches.sort_by(|(_, a, _), (_, b, _)| a.state().cmp(b.state()));
+

+
            for (id, patch, _) in patches {
+
                if let Ok(item) = PatchItem::try_from((profile, &repo, id, patch)) {
+
                    items.push(item);
+
                }
+
            }
+
        }
+

+
        let table = Widget::new(Table::new(&items, header, widths, theme.clone()))
+
            .highlight(theme.colors.item_list_highlighted_bg);
+

+
        Self { table, shortcuts }
+
    }
+

+
    pub fn selected_item(&self) -> Option<&PatchItem> {
+
        self.table.selection()
+
    }
+
}
+

+
impl WidgetComponent for PatchBrowser {
+
    fn view(&mut self, _properties: &Props, frame: &mut Frame, area: Rect) {
+
        let shortcuts_h = self
+
            .shortcuts
+
            .query(Attribute::Height)
+
            .unwrap_or(AttrValue::Size(0))
+
            .unwrap_size();
+
        let layout = layout::root_component(area, shortcuts_h);
+

+
        self.table.view(frame, layout[0]);
+
        self.shortcuts.view(frame, layout[1]);
+
    }
+

+
    fn state(&self) -> State {
+
        self.table.state()
+
    }
+

+
    fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
+
        self.table.perform(cmd)
+
    }
+
}
+

pub fn navigation(theme: &Theme) -> Widget<Tabs> {
    common::tabs(
        theme,
@@ -125,11 +207,7 @@ pub fn dashboard(theme: &Theme, id: &Id, project: &Project) -> Widget<Dashboard>
    Widget::new(dashboard)
}

-
pub fn patches(
-
    theme: &Theme,
-
    items: &[(PatchId, Patch)],
-
    profile: &Profile,
-
) -> Widget<Browser<(PatchId, Patch)>> {
+
pub fn patches(theme: &Theme, id: &Id, profile: &Profile) -> Widget<PatchBrowser> {
    let shortcuts = common::shortcuts(
        theme,
        vec![
@@ -140,13 +218,7 @@ pub fn patches(
        ],
    );

-
    let labels = vec!["", "title", "author", "time", "comments", "tags"];
-
    let widths = vec![3u16, 42, 15, 10, 5, 25];
-

-
    let table = common::table(theme, &labels, &widths, items, profile);
-
    let browser: Browser<(PatchId, Patch)> = Browser::new(table, shortcuts);
-

-
    Widget::new(browser)
+
    Widget::new(PatchBrowser::new(profile, id, shortcuts, theme.clone()))
}

pub fn issues(theme: &Theme) -> Widget<IssueBrowser> {
modified radicle-tui/src/ui/widget/patch.rs
@@ -179,8 +179,8 @@ pub fn context(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Wi

    let id = format::cob(&id);
    let title = patch.title();
-
    let author = cob::format_author(patch, profile);
-
    let comments = rev.discussion().comments().count();
+
    let author = cob::format_author(patch.author().id(), is_you);
+
    let comments = rev.discussion().len();

    let context = common::label(" patch ").background(theme.colors.context_badge_bg);
    let id = common::label(&format!(" {id} "))