tui: Implement table item patches

This commit is contained in:
Erik Kundt 2023-05-17 11:36:20 +02:00
parent 5d7bf9f663
commit de6bb8fdb0
6 changed files with 162 additions and 87 deletions

1
Cargo.lock generated
View File

@ -2180,6 +2180,7 @@ dependencies = [
"lexopt",
"radicle",
"radicle-cli",
"radicle-surf",
"radicle-term",
"timeago 0.4.1",
"tui-realm-stdlib",

View File

@ -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" ] }

View File

@ -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))
}
}

View File

@ -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]
}
}

View File

@ -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,

View File

@ -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} "))