tui: Implement table item patches
This commit is contained in:
parent
5d7bf9f663
commit
de6bb8fdb0
|
|
@ -2180,6 +2180,7 @@ dependencies = [
|
||||||
"lexopt",
|
"lexopt",
|
||||||
"radicle",
|
"radicle",
|
||||||
"radicle-cli",
|
"radicle-cli",
|
||||||
|
"radicle-surf",
|
||||||
"radicle-term",
|
"radicle-term",
|
||||||
"timeago 0.4.1",
|
"timeago 0.4.1",
|
||||||
"tui-realm-stdlib",
|
"tui-realm-stdlib",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ path = "src/main.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { version = "1" }
|
anyhow = { version = "1" }
|
||||||
lexopt = { version = "0.2" }
|
lexopt = { version = "0.2" }
|
||||||
|
radicle-surf = { version = "0.11.0" }
|
||||||
timeago = { version = "0.4.1" }
|
timeago = { version = "0.4.1" }
|
||||||
tuirealm = { version = "1.8.0", default-features = false, features = [ "with-termion" ] }
|
tuirealm = { version = "1.8.0", default-features = false, features = [ "with-termion" ] }
|
||||||
tui-realm-stdlib = { version = "1.2.0", default-features = false, features = [ "with-termion" ] }
|
tui-realm-stdlib = { version = "1.2.0", default-features = false, features = [ "with-termion" ] }
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -19,8 +19,12 @@ pub struct Colors {
|
||||||
pub shortcut_short_fg: Color,
|
pub shortcut_short_fg: Color,
|
||||||
pub shortcut_long_fg: Color,
|
pub shortcut_long_fg: Color,
|
||||||
pub shortcutbar_divider_fg: Color,
|
pub shortcutbar_divider_fg: Color,
|
||||||
|
pub browser_patch_list_id: Color,
|
||||||
pub browser_patch_list_title: Color,
|
pub browser_patch_list_title: Color,
|
||||||
pub browser_patch_list_author: 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_tags: Color,
|
||||||
pub browser_patch_list_comments: Color,
|
pub browser_patch_list_comments: Color,
|
||||||
pub browser_patch_list_timestamp: Color,
|
pub browser_patch_list_timestamp: Color,
|
||||||
|
|
@ -82,8 +86,12 @@ pub fn default_dark() -> Theme {
|
||||||
shortcut_short_fg: COLOR_DEFAULT_DARK,
|
shortcut_short_fg: COLOR_DEFAULT_DARK,
|
||||||
shortcut_long_fg: COLOR_DEFAULT_DARKER,
|
shortcut_long_fg: COLOR_DEFAULT_DARKER,
|
||||||
shortcutbar_divider_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_title: COLOR_DEFAULT_FG,
|
||||||
browser_patch_list_author: Color::Gray,
|
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_tags: Color::Yellow,
|
||||||
browser_patch_list_comments: COLOR_DEFAULT_DARK_FG,
|
browser_patch_list_comments: COLOR_DEFAULT_DARK_FG,
|
||||||
browser_patch_list_timestamp: COLOR_DEFAULT_DARK,
|
browser_patch_list_timestamp: COLOR_DEFAULT_DARK,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use radicle::cob::patch::{Patch, PatchId};
|
use radicle::cob::patch::{Patch, PatchId};
|
||||||
use radicle::Profile;
|
use radicle::Profile;
|
||||||
|
|
||||||
|
use radicle_cli::terminal::format;
|
||||||
|
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
use tuirealm::command::{Cmd, CmdResult};
|
||||||
use tuirealm::props::Color;
|
use tuirealm::props::Color;
|
||||||
use tuirealm::tui::layout::Rect;
|
use tuirealm::tui::layout::Rect;
|
||||||
|
|
@ -13,9 +15,8 @@ use super::common::container::Tabs;
|
||||||
use super::common::context::{ContextBar, Shortcuts};
|
use super::common::context::{ContextBar, Shortcuts};
|
||||||
use super::common::label::Label;
|
use super::common::label::Label;
|
||||||
|
|
||||||
use crate::ui::cob::patch;
|
|
||||||
use crate::ui::layout;
|
|
||||||
use crate::ui::theme::Theme;
|
use crate::ui::theme::Theme;
|
||||||
|
use crate::ui::{cob, layout};
|
||||||
|
|
||||||
pub struct Activity {
|
pub struct Activity {
|
||||||
label: Widget<Label>,
|
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> {
|
pub fn context(theme: &Theme, patch: (PatchId, &Patch), profile: &Profile) -> Widget<ContextBar> {
|
||||||
let (id, patch) = patch;
|
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 title = patch.title();
|
||||||
let author = patch::format_author(patch, profile);
|
let author = cob::format_author(patch, profile);
|
||||||
let comments = patch::format_comments(patch);
|
let comments = rev.discussion().comments().count();
|
||||||
|
|
||||||
let context = common::label(" patch ").background(theme.colors.context_badge_bg);
|
let context = common::label(" patch ").background(theme.colors.context_badge_bg);
|
||||||
let id = common::label(&format!(" {id} "))
|
let id = common::label(&format!(" {id} "))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue