use std::fmt; use localtime::LocalTime; pub use radicle_term::format::*; pub use radicle_term::{style, Paint}; use radicle::cob::ObjectId; use radicle::identity::Visibility; use radicle::node::policy::Policy; use radicle::node::{Alias, AliasStore, NodeId}; use radicle::prelude::Did; use radicle::profile::{env, Profile}; use radicle::storage::RefUpdate; use radicle_term::element::Line; use crate::terminal as term; /// Format a node id to be more compact. #[must_use] pub fn node_id_human_compact(node: &NodeId) -> Paint { let node = node.to_human(); let start = node.chars().take(7).collect::(); let end = node.chars().skip(node.len() - 7).collect::(); Paint::new(format!("{start}…{end}")) } /// Format a node id. #[must_use] pub fn node_id_human(node: &NodeId) -> Paint { Paint::new(node.to_human()) } /// Format a git Oid. pub fn oid(oid: impl Into) -> Paint { Paint::new(format!("{:.7}", oid.into())) } /// Wrap parenthesis around styled input, eg. `"input"` -> `"(input)"`. pub fn parens(input: Paint) -> Paint { Paint::new(format!("({})", input.item)).with_style(input.style) } /// Wrap spaces around styled input, eg. `"input"` -> `" input "`. pub fn spaced(input: Paint) -> Paint { Paint::new(format!(" {} ", input.item)).with_style(input.style) } /// Format a command suggestion, eg. `rad init`. pub fn command(cmd: D) -> Paint { primary(format!("`{cmd}`")) } /// Format a COB id. #[must_use] pub fn cob(id: &ObjectId) -> Paint { Paint::new(format!("{:.7}", id.to_string())) } /// Format a DID. #[must_use] pub fn did(did: &Did) -> Paint { let nid = did.as_key().to_human(); Paint::new(format!("{}…{}", &nid[..7], &nid[nid.len() - 7..])) } /// Format a Visibility. #[must_use] pub fn visibility(v: &Visibility) -> Paint<&str> { match v { Visibility::Public => term::format::positive("public"), Visibility::Private { .. } => term::format::yellow("private"), } } /// Format a policy. #[must_use] pub fn policy(p: &Policy) -> Paint { match p { Policy::Allow => term::format::positive(p.to_string()), Policy::Block => term::format::negative(p.to_string()), } } /// Format a timestamp. pub fn timestamp(time: impl Into) -> Paint { let time = time.into(); let now = env::local_time(); let duration = now - time; let fmt = timeago::Formatter::new(); Paint::new(fmt.convert(duration.into())) } #[must_use] pub fn bytes(size: usize) -> Paint { const KB: usize = 1024; const MB: usize = 1024usize.pow(2); const GB: usize = 1024usize.pow(3); let size = if size < KB { format!("{size} B") } else if size < MB { format!("{} KiB", size / KB) } else if size < GB { format!("{} MiB", size / MB) } else { format!("{} GiB", size / GB) }; Paint::new(size) } /// Format a ref update. #[must_use] pub fn ref_update(update: &RefUpdate) -> Paint<&'static str> { match update { RefUpdate::Updated { .. } => term::format::tertiary("updated"), RefUpdate::Created { .. } => term::format::positive("created"), RefUpdate::Deleted { .. } => term::format::negative("deleted"), RefUpdate::Skipped { .. } => term::format::dim("skipped"), } } #[must_use] pub fn ref_update_verbose(update: &RefUpdate) -> Paint { match update { RefUpdate::Created { name, .. } => format!( "{: <17} {}", term::format::positive("* [new ref]"), term::format::secondary(name), ) .into(), RefUpdate::Updated { name, old, new } => format!( "{: <17} {}", format!("{}..{}", term::format::oid(*old), term::format::oid(*new)), term::format::secondary(name), ) .into(), RefUpdate::Deleted { name, .. } => format!( "{: <17} {}", term::format::negative("- [deleted]"), term::format::secondary(name), ) .into(), RefUpdate::Skipped { name, .. } => format!( "{: <17} {}", term::format::italic("* [skipped]"), term::format::secondary(name) ) .into(), } } /// Identity formatter that takes a profile and displays it as /// ` ()` depending on the configuration. pub struct Identity<'a> { profile: &'a Profile, /// If true, node id is printed in its compact form. short: bool, /// If true, node id and username are printed using the terminal's /// styled formatters. styled: bool, } impl<'a> Identity<'a> { #[must_use] pub fn new(profile: &'a Profile) -> Self { Self { profile, short: false, styled: false, } } #[must_use] pub fn short(mut self) -> Self { self.short = true; self } #[must_use] pub fn styled(mut self) -> Self { self.styled = true; self } } impl fmt::Display for Identity<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let nid = self.profile.id(); let alias = self.profile.aliases().alias(nid); let node_id = match self.short { true => self::node_id_human_compact(nid).to_string(), false => nid.to_human(), }; if self.styled { write!(f, "{}", term::format::highlight(node_id))?; if let Some(a) = alias { write!(f, " {}", term::format::parens(term::format::dim(a)))?; } } else { write!(f, "{node_id}")?; if let Some(a) = alias { write!(f, " ({a})")?; } } Ok(()) } } /// This enum renders (nid, alias) in terminal depending on user variant. pub struct Author<'a> { nid: &'a NodeId, alias: Option, you: bool, verbose: bool, } impl<'a> Author<'a> { #[must_use] pub fn new(nid: &'a NodeId, profile: &Profile, verbose: bool) -> Author<'a> { let alias = profile.alias(nid); Self { nid, alias, you: nid == profile.id(), verbose, } } #[must_use] pub fn alias(&self) -> Option { self.alias.as_ref().map(|a| a.to_string().into()) } #[must_use] pub fn you(&self) -> Option { if self.you { Some(term::format::primary("(you)").dim().italic().into()) } else { None } } /// Get the labels of the `Author`. The labels can take the following forms: /// /// * `(, (you))` -- the `Author` is the local peer and has an alias /// * `(, (you))` -- the `Author` is the local peer and has no alias /// * `(, )` -- the `Author` is another peer and has an alias /// * `(, )` -- the `Author` is another peer and has no alias #[must_use] pub fn labels(self) -> (term::Label, term::Label) { let node_id = if self.verbose { term::format::node_id_human(self.nid) } else { term::format::node_id_human_compact(self.nid) }; let alias = match self.alias.as_ref() { Some(alias) => term::format::primary(alias).into(), None if self.you => term::format::primary(node_id.clone()).dim().into(), None => term::Label::blank(), }; let author = self .you() .unwrap_or_else(move || term::format::primary(node_id).dim().into()); (alias, author) } #[must_use] pub fn line(self) -> Line { let (alias, author) = self.labels(); Line::spaced([alias, author]) } } /// HTML-related formatting. pub mod html { /// Comment a string with HTML comments. #[must_use] pub fn commented(s: &str) -> String { format!("") } /// Remove html style comments from a string. /// /// The HTML comments must start at the beginning of a line and stop at the end. #[must_use] pub fn strip_comments(s: &str) -> String { let ends_with_newline = s.ends_with('\n'); let mut is_comment = false; let mut w = String::new(); for line in s.lines() { if is_comment { if line.ends_with("-->") { is_comment = false; } continue; } else if line.starts_with(""; let exp = "\ commit 2\n\ "; let res = strip_comments(test); assert_eq!(exp, res); let test = "\ commit 2\n\ -->"; let exp = "\ commit 2\n\ -->"; let res = strip_comments(test); assert_eq!(exp, res); let test = "\ \n\ -->"; let exp = "\ commit 2\n\ \n\ -->"; let res = strip_comments(test); assert_eq!(exp, res); } #[test] fn test_bytes() { assert_eq!(bytes(1023).to_string(), "1023 B"); assert_eq!(bytes(1024).to_string(), "1 KiB"); assert_eq!(bytes(1024 * 9).to_string(), "9 KiB"); assert_eq!(bytes(1024usize.pow(2)).to_string(), "1 MiB"); assert_eq!(bytes(1024usize.pow(2) * 56).to_string(), "56 MiB"); assert_eq!(bytes(1024usize.pow(3) * 1024).to_string(), "1024 GiB"); } }