Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
tui: Implement table item patches
Erik Kundt committed 2 years ago
commit 0b615459bcbfd32cfea8ecbe47fb08dda34f7eef
parent 20f4bbed3b26abd1cae56d744e40a5c9cbea4656
5 files changed +161 -87
modified radicle-tui/Cargo.toml
@@ -13,6 +13,7 @@ path = "src/main.rs"
[dependencies]
anyhow = { version = "1" }
lexopt = { version = "0.2" }
+
radicle-surf = { version = "0.11.0" }
timeago = { version = "0.4.1" }
tuirealm = { version = "1.8.0", default-features = false, features = [ "with-termion" ] }
tui-realm-stdlib = { version = "1.2.0", default-features = false, features = [ "with-termion" ] }
modified radicle-tui/src/ui/cob.rs
@@ -1 +1,143 @@
-
pub mod patch;
+
use radicle_surf;
+

+
use cli::terminal::format;
+
use radicle_cli as cli;
+

+
use radicle::prelude::Did;
+
use radicle::storage::git::Repository;
+
use radicle::storage::{Oid, ReadRepository};
+
use radicle::Profile;
+

+
use radicle::cob::patch::{Patch, PatchId, State};
+
use radicle::cob::Timestamp;
+

+
use tuirealm::props::{Color, Style};
+
use tuirealm::tui::widgets::Cell;
+

+
use crate::ui::theme::Theme;
+
use crate::ui::widget::common::list::TableItem;
+

+
/// An author item that can be used in tables, list or trees.
+
///
+
/// Breaks up dependencies to [`Profile`] and [`Repository`] that
+
/// would be needed if [`Author`] would be used directly.
+
#[derive(Clone)]
+
pub struct AuthorItem {
+
    /// The author's DID.
+
    did: Did,
+
    /// True if the author is the current user.
+
    is_you: bool,
+
}
+

+
/// A patch item that can be used in tables, list or trees.
+
///
+
/// Breaks up dependencies to [`Profile`] and [`Repository`] that
+
/// would be needed if [`Patch`] would be used directly.
+
#[derive(Clone)]
+
pub struct PatchItem {
+
    /// Patch OID.
+
    id: PatchId,
+
    /// Patch state.
+
    state: State,
+
    /// Patch title.
+
    title: String,
+
    /// Author of the latest revision.
+
    author: AuthorItem,
+
    /// Head of the latest revision.
+
    head: Oid,
+
    /// Lines added by the latest revision.
+
    added: u16,
+
    /// Lines removed by the latest revision.
+
    removed: u16,
+
    /// Time when patch was opened.
+
    timestamp: Timestamp,
+
}
+

+
impl PatchItem {
+
    pub fn id(&self) -> PatchId {
+
        self.id
+
    }
+
}
+

+
impl TryFrom<(&Profile, &Repository, PatchId, Patch)> for PatchItem {
+
    type Error = anyhow::Error;
+

+
    fn try_from(value: (&Profile, &Repository, PatchId, Patch)) -> Result<Self, Self::Error> {
+
        let (profile, repo, id, patch) = value;
+
        let (_, rev) = patch.latest();
+
        let repo = radicle_surf::Repository::open(repo.path())?;
+
        let base = repo.commit(rev.base())?;
+
        let head = repo.commit(rev.head())?;
+
        let diff = repo.diff(base.id, head.id)?;
+

+
        Ok(PatchItem {
+
            id,
+
            state: patch.state().clone(),
+
            title: patch.title().into(),
+
            author: AuthorItem {
+
                did: patch.author().id,
+
                is_you: *patch.author().id == *profile.did(),
+
            },
+
            head: rev.head(),
+
            added: diff.stats().insertions as u16,
+
            removed: diff.stats().deletions as u16,
+
            timestamp: rev.timestamp(),
+
        })
+
    }
+
}
+

+
impl TableItem<8> for PatchItem {
+
    fn row(&self, theme: &Theme) -> [Cell; 8] {
+
        let (icon, color) = format_state(&self.state);
+
        let state = Cell::from(icon).style(Style::default().fg(color));
+

+
        let id = Cell::from(format::cob(&self.id))
+
            .style(Style::default().fg(theme.colors.browser_patch_list_id));
+

+
        let title = Cell::from(self.title.clone())
+
            .style(Style::default().fg(theme.colors.browser_patch_list_title));
+

+
        let author = Cell::from(format_author(&self.author.did, self.author.is_you))
+
            .style(Style::default().fg(theme.colors.browser_patch_list_author));
+

+
        let head = Cell::from(format::oid(self.head))
+
            .style(Style::default().fg(theme.colors.browser_patch_list_head));
+

+
        let added = Cell::from(format!("{}", self.added))
+
            .style(Style::default().fg(theme.colors.browser_patch_list_added));
+

+
        let removed = Cell::from(format!("{}", self.removed))
+
            .style(Style::default().fg(theme.colors.browser_patch_list_removed));
+

+
        let updated = Cell::from(format::timestamp(&self.timestamp).to_string())
+
            .style(Style::default().fg(theme.colors.browser_patch_list_timestamp));
+

+
        [state, id, title, author, head, added, removed, updated]
+
    }
+
}
+

+
impl TableItem<1> for () {
+
    fn row(&self, _theme: &Theme) -> [Cell; 1] {
+
        [Cell::default()]
+
    }
+
}
+

+
pub fn format_state(state: &State) -> (String, Color) {
+
    match state {
+
        State::Open { conflicts: _ } => (" ● ".into(), Color::Green),
+
        State::Archived => (" ● ".into(), Color::Yellow),
+
        State::Draft => (" ● ".into(), Color::Gray),
+
        State::Merged {
+
            revision: _,
+
            commit: _,
+
        } => (" ✔ ".into(), Color::Blue),
+
    }
+
}
+

+
pub fn format_author(did: &Did, is_you: bool) -> String {
+
    if is_you {
+
        format!("{} (you)", format::did(did))
+
    } else {
+
        format!("{}", format::did(did))
+
    }
+
}
deleted radicle-tui/src/ui/cob/patch.rs
@@ -1,81 +0,0 @@
-
use std::time::{Duration, SystemTime, UNIX_EPOCH};
-

-
use radicle::Profile;
-
use timeago;
-

-
use radicle::cob::patch::{Patch, PatchId};
-

-
use tuirealm::props::{Color, TextSpan};
-

-
use crate::ui::theme::Theme;
-
use crate::ui::widget::common::list::List;
-

-
pub fn format_status(_patch: &Patch) -> String {
-
    String::from(" ⏺ ")
-
}
-

-
pub fn format_id(id: PatchId) -> String {
-
    id.to_string()[0..10].to_string()
-
}
-

-
pub fn format_title(patch: &Patch) -> String {
-
    patch.title().to_string()
-
}
-

-
pub fn format_author(patch: &Patch, profile: &Profile) -> String {
-
    let author_did = patch.author().id();
-
    let start = &author_did.to_human()[0..4];
-
    let end = &author_did.to_human()[43..47];
-

-
    if *author_did == profile.did() {
-
        format!("did:key:{start}...{end} (you)")
-
    } else {
-
        format!("did:key:{start}...{end}")
-
    }
-
}
-

-
pub fn format_tags(patch: &Patch) -> String {
-
    format!("{:?}", patch.tags().collect::<Vec<_>>())
-
}
-

-
pub fn format_timestamp(patch: &Patch) -> String {
-
    let fmt = timeago::Formatter::new();
-
    let now = SystemTime::now()
-
        .duration_since(UNIX_EPOCH)
-
        .unwrap()
-
        .as_secs();
-

-
    fmt.convert(Duration::from_secs(now - patch.timestamp().as_secs()))
-
}
-

-
pub fn format_comments(patch: &Patch) -> String {
-
    let count: usize = patch.revisions().map(|(_, r)| r.discussion().len()).sum();
-

-
    format!("{count}")
-
}
-

-
impl List for (PatchId, Patch) {
-
    fn row(&self, theme: &Theme, profile: &Profile) -> Vec<TextSpan> {
-
        let (_, patch) = self;
-

-
        let status = format_status(patch);
-
        let status = TextSpan::from(status).fg(Color::Green);
-

-
        let title = format_title(patch);
-
        let title = TextSpan::from(title).fg(theme.colors.browser_patch_list_title);
-

-
        let author = format_author(patch, profile);
-
        let author = TextSpan::from(author).fg(theme.colors.browser_patch_list_author);
-

-
        let timestamp = format_timestamp(patch);
-
        let timestamp = TextSpan::from(timestamp).fg(theme.colors.browser_patch_list_timestamp);
-

-
        let comments = format_comments(patch);
-
        let comments = TextSpan::from(comments).fg(theme.colors.browser_patch_list_comments);
-

-
        let tags = format_tags(patch);
-
        let tags = TextSpan::from(tags).fg(theme.colors.browser_patch_list_tags);
-

-
        vec![status, title, author, timestamp, comments, tags]
-
    }
-
}
modified radicle-tui/src/ui/theme.rs
@@ -19,8 +19,12 @@ pub struct Colors {
    pub shortcut_short_fg: Color,
    pub shortcut_long_fg: Color,
    pub shortcutbar_divider_fg: Color,
+
    pub browser_patch_list_id: Color,
    pub browser_patch_list_title: Color,
    pub browser_patch_list_author: Color,
+
    pub browser_patch_list_head: Color,
+
    pub browser_patch_list_added: Color,
+
    pub browser_patch_list_removed: Color,
    pub browser_patch_list_tags: Color,
    pub browser_patch_list_comments: Color,
    pub browser_patch_list_timestamp: Color,
@@ -82,8 +86,12 @@ pub fn default_dark() -> Theme {
            shortcut_short_fg: COLOR_DEFAULT_DARK,
            shortcut_long_fg: COLOR_DEFAULT_DARKER,
            shortcutbar_divider_fg: COLOR_DEFAULT_DARKER,
+
            browser_patch_list_id: Color::Cyan,
            browser_patch_list_title: COLOR_DEFAULT_FG,
            browser_patch_list_author: Color::Gray,
+
            browser_patch_list_head: Color::LightBlue,
+
            browser_patch_list_added: Color::Green,
+
            browser_patch_list_removed: Color::Red,
            browser_patch_list_tags: Color::Yellow,
            browser_patch_list_comments: COLOR_DEFAULT_DARK_FG,
            browser_patch_list_timestamp: COLOR_DEFAULT_DARK,
modified radicle-tui/src/ui/widget/patch.rs
@@ -1,6 +1,8 @@
use radicle::cob::patch::{Patch, PatchId};
use radicle::Profile;

+
use radicle_cli::terminal::format;
+

use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::Color;
use tuirealm::tui::layout::Rect;
@@ -13,9 +15,8 @@ use super::common::container::Tabs;
use super::common::context::{ContextBar, Shortcuts};
use super::common::label::Label;

-
use crate::ui::cob::patch;
-
use crate::ui::layout;
use crate::ui::theme::Theme;
+
use crate::ui::{cob, layout};

pub struct Activity {
    label: Widget<Label>,
@@ -173,10 +174,13 @@ pub fn files(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Widg

pub fn context(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Widget<ContextBar> {
    let (id, patch) = patch;
-
    let id = patch::format_id(id);
+
    let (_, rev) = patch.latest();
+
    let is_you = *patch.author().id() == profile.did();
+

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

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