diff --git a/Cargo.lock b/Cargo.lock index 8ed7e193..cdd6321d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2180,6 +2180,7 @@ dependencies = [ "lexopt", "radicle", "radicle-cli", + "radicle-surf", "radicle-term", "timeago 0.4.1", "tui-realm-stdlib", diff --git a/radicle-tui/Cargo.toml b/radicle-tui/Cargo.toml index d7e47811..5809ca07 100644 --- a/radicle-tui/Cargo.toml +++ b/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" ] } diff --git a/radicle-tui/src/ui/cob.rs b/radicle-tui/src/ui/cob.rs index e268c5ac..0351b3e0 100644 --- a/radicle-tui/src/ui/cob.rs +++ b/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 { + 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)) + } +} diff --git a/radicle-tui/src/ui/cob/patch.rs b/radicle-tui/src/ui/cob/patch.rs deleted file mode 100644 index 3b0d342c..00000000 --- a/radicle-tui/src/ui/cob/patch.rs +++ /dev/null @@ -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::>()) -} - -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 { - 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] - } -} diff --git a/radicle-tui/src/ui/theme.rs b/radicle-tui/src/ui/theme.rs index c7bb5324..aa30b711 100644 --- a/radicle-tui/src/ui/theme.rs +++ b/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, diff --git a/radicle-tui/src/ui/widget/patch.rs b/radicle-tui/src/ui/widget/patch.rs index f9377ee6..67527fa9 100644 --- a/radicle-tui/src/ui/widget/patch.rs +++ b/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