1use std::fmt;
2
3use localtime::LocalTime;
4
5pub use radicle_term::format::*;
6pub use radicle_term::{style, Paint};
7
8use radicle::cob::ObjectId;
9use radicle::identity::Visibility;
10use radicle::node::policy::Policy;
11use radicle::node::{Address, Alias, AliasStore, HostName, NodeId};
12use radicle::prelude::Did;
13use radicle::profile::{env, Profile};
14use radicle::storage::RefUpdate;
15use radicle_term::element::Line;
16
17use crate::terminal as term;
18
19pub fn node_id_human_compact(node: &NodeId) -> Paint<String> {
21 let node = node.to_human();
22 let start = node.chars().take(7).collect::<String>();
23 let end = node.chars().skip(node.len() - 7).collect::<String>();
24
25 Paint::new(format!("{start}…{end}"))
26}
27
28pub fn node_id_human(node: &NodeId) -> Paint<String> {
30 Paint::new(node.to_human())
31}
32
33pub fn addr_compact(address: &Address) -> Paint<String> {
34 let host = match address.host() {
35 HostName::Ip(ip) => ip.to_string(),
36 HostName::Dns(dns) => dns.clone(),
37 HostName::Tor(onion) => {
38 let onion = onion.to_string();
39 let start = onion.chars().take(8).collect::<String>();
40 let end = onion
41 .chars()
42 .skip(onion.len() - 8 - ".onion".len())
43 .collect::<String>();
44 format!("{start}…{end}")
45 }
46 _ => unreachable!(),
47 };
48
49 let port = address.port().to_string();
50
51 Paint::new(format!("{host}:{port}"))
52}
53
54pub fn oid(oid: impl Into<radicle::git::Oid>) -> Paint<String> {
56 Paint::new(format!("{:.7}", oid.into()))
57}
58
59pub fn parens<D: fmt::Display>(input: Paint<D>) -> Paint<String> {
61 Paint::new(format!("({})", input.item)).with_style(input.style)
62}
63
64pub fn spaced<D: fmt::Display>(input: Paint<D>) -> Paint<String> {
66 Paint::new(format!(" {} ", input.item)).with_style(input.style)
67}
68
69pub fn command<D: fmt::Display>(cmd: D) -> Paint<String> {
71 primary(format!("`{cmd}`"))
72}
73
74pub fn cob(id: &ObjectId) -> Paint<String> {
76 Paint::new(format!("{:.7}", id.to_string()))
77}
78
79pub fn did(did: &Did) -> Paint<String> {
81 let nid = did.as_key().to_human();
82 Paint::new(format!("{}…{}", &nid[..7], &nid[nid.len() - 7..]))
83}
84
85pub fn visibility(v: &Visibility) -> Paint<&str> {
87 match v {
88 Visibility::Public => term::format::positive("public"),
89 Visibility::Private { .. } => term::format::yellow("private"),
90 }
91}
92
93pub fn policy(p: &Policy) -> Paint<String> {
95 match p {
96 Policy::Allow => term::format::positive(p.to_string()),
97 Policy::Block => term::format::negative(p.to_string()),
98 }
99}
100
101pub fn timestamp(time: impl Into<LocalTime>) -> Paint<String> {
103 let time = time.into();
104 let now = env::local_time();
105 let duration = now - time;
106 let fmt = timeago::Formatter::new();
107
108 Paint::new(fmt.convert(duration.into()))
109}
110
111pub fn bytes(size: usize) -> Paint<String> {
112 const KB: usize = 1024;
113 const MB: usize = 1024usize.pow(2);
114 const GB: usize = 1024usize.pow(3);
115 let size = if size < KB {
116 format!("{size} B")
117 } else if size < MB {
118 format!("{} KiB", size / KB)
119 } else if size < GB {
120 format!("{} MiB", size / MB)
121 } else {
122 format!("{} GiB", size / GB)
123 };
124 Paint::new(size)
125}
126
127pub fn ref_update(update: &RefUpdate) -> Paint<&'static str> {
129 match update {
130 RefUpdate::Updated { .. } => term::format::tertiary("updated"),
131 RefUpdate::Created { .. } => term::format::positive("created"),
132 RefUpdate::Deleted { .. } => term::format::negative("deleted"),
133 RefUpdate::Skipped { .. } => term::format::dim("skipped"),
134 }
135}
136
137pub fn ref_update_verbose(update: &RefUpdate) -> Paint<String> {
138 match update {
139 RefUpdate::Created { name, .. } => format!(
140 "{: <17} {}",
141 term::format::positive("* [new ref]"),
142 term::format::secondary(name),
143 )
144 .into(),
145 RefUpdate::Updated { name, old, new } => format!(
146 "{: <17} {}",
147 format!("{}..{}", term::format::oid(*old), term::format::oid(*new)),
148 term::format::secondary(name),
149 )
150 .into(),
151 RefUpdate::Deleted { name, .. } => format!(
152 "{: <17} {}",
153 term::format::negative("- [deleted]"),
154 term::format::secondary(name),
155 )
156 .into(),
157 RefUpdate::Skipped { name, .. } => format!(
158 "{: <17} {}",
159 term::format::italic("* [skipped]"),
160 term::format::secondary(name)
161 )
162 .into(),
163 }
164}
165
166pub struct Identity<'a> {
169 profile: &'a Profile,
170 short: bool,
172 styled: bool,
175}
176
177impl<'a> Identity<'a> {
178 pub fn new(profile: &'a Profile) -> Self {
179 Self {
180 profile,
181 short: false,
182 styled: false,
183 }
184 }
185
186 pub fn short(mut self) -> Self {
187 self.short = true;
188 self
189 }
190
191 pub fn styled(mut self) -> Self {
192 self.styled = true;
193 self
194 }
195}
196
197impl fmt::Display for Identity<'_> {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 let nid = self.profile.id();
200 let alias = self.profile.aliases().alias(nid);
201 let node_id = match self.short {
202 true => self::node_id_human_compact(nid).to_string(),
203 false => nid.to_human(),
204 };
205
206 if self.styled {
207 write!(f, "{}", term::format::highlight(node_id))?;
208 if let Some(a) = alias {
209 write!(f, " {}", term::format::parens(term::format::dim(a)))?;
210 }
211 } else {
212 write!(f, "{node_id}")?;
213 if let Some(a) = alias {
214 write!(f, " ({a})")?;
215 }
216 }
217 Ok(())
218 }
219}
220
221pub struct Author<'a> {
223 nid: &'a NodeId,
224 alias: Option<Alias>,
225 you: bool,
226 verbose: bool,
227}
228
229impl<'a> Author<'a> {
230 pub fn new(nid: &'a NodeId, profile: &Profile, verbose: bool) -> Author<'a> {
231 let alias = profile.alias(nid);
232
233 Self {
234 nid,
235 alias,
236 you: nid == profile.id(),
237 verbose,
238 }
239 }
240
241 pub fn alias(&self) -> Option<term::Label> {
242 self.alias.as_ref().map(|a| a.to_string().into())
243 }
244
245 pub fn you(&self) -> Option<term::Label> {
246 if self.you {
247 Some(term::format::primary("(you)").dim().italic().into())
248 } else {
249 None
250 }
251 }
252
253 pub fn labels(self) -> (term::Label, term::Label) {
260 let node_id = if self.verbose {
261 term::format::node_id_human(self.nid)
262 } else {
263 term::format::node_id_human_compact(self.nid)
264 };
265
266 let alias = match self.alias.as_ref() {
267 Some(alias) => term::format::primary(alias).into(),
268 None if self.you => term::format::primary(node_id.clone()).dim().into(),
269 None => term::Label::blank(),
270 };
271 let author = self
272 .you()
273 .unwrap_or_else(move || term::format::primary(node_id).dim().into());
274 (alias, author)
275 }
276
277 pub fn line(self) -> Line {
278 let (alias, author) = self.labels();
279 Line::spaced([alias, author])
280 }
281}
282
283pub mod html {
285 pub fn commented(s: &str) -> String {
287 format!("<!--\n{s}\n-->")
288 }
289
290 pub fn strip_comments(s: &str) -> String {
294 let ends_with_newline = s.ends_with('\n');
295 let mut is_comment = false;
296 let mut w = String::new();
297
298 for line in s.lines() {
299 if is_comment {
300 if line.ends_with("-->") {
301 is_comment = false;
302 }
303 continue;
304 } else if line.starts_with("<!--") {
305 is_comment = true;
306 continue;
307 }
308
309 w.push_str(line);
310 w.push('\n');
311 }
312 if !ends_with_newline {
313 w.pop();
314 }
315
316 w.to_string()
317 }
318}
319
320pub mod issue {
322 use super::*;
323 use radicle::issue::{CloseReason, State};
324
325 pub fn state(s: &State) -> term::Paint<String> {
327 match s {
328 State::Open => term::format::positive(s.to_string()),
329 State::Closed {
330 reason: CloseReason::Other,
331 } => term::format::negative(s.to_string()),
332 State::Closed {
333 reason: CloseReason::Solved,
334 } => term::format::secondary(s.to_string()),
335 }
336 }
337}
338
339pub mod patch {
341 use super::*;
342 use radicle::patch::{State, Verdict};
343
344 pub fn verdict(v: Option<Verdict>) -> term::Paint<String> {
345 match v {
346 Some(Verdict::Accept) => term::PREFIX_SUCCESS.into(),
347 Some(Verdict::Reject) => term::PREFIX_ERROR.into(),
348 None => term::format::dim("-".to_string()),
349 }
350 }
351
352 pub fn state(s: &State) -> term::Paint<String> {
354 match s {
355 State::Draft => term::format::dim(s.to_string()),
356 State::Open { .. } => term::format::positive(s.to_string()),
357 State::Archived => term::format::yellow(s.to_string()),
358 State::Merged { .. } => term::format::secondary(s.to_string()),
359 }
360 }
361}
362
363pub mod identity {
365 use super::*;
366 use radicle::cob::identity::State;
367
368 pub fn state(s: &State) -> term::Paint<String> {
370 match s {
371 State::Active => term::format::tertiary(s.to_string()),
372 State::Accepted => term::format::positive(s.to_string()),
373 State::Rejected => term::format::negative(s.to_string()),
374 State::Stale => term::format::dim(s.to_string()),
375 }
376 }
377}
378
379#[cfg(test)]
380mod test {
381 use super::*;
382 use html::strip_comments;
383
384 #[test]
385 fn test_strip_comments() {
386 let test = "\
387 commit 2\n\
388 \n\
389 <!--\n\
390 Please enter a comment for your patch update. Leaving this\n\
391 blank is also okay.\n\
392 -->";
393 let exp = "\
394 commit 2\n\
395 ";
396
397 let res = strip_comments(test);
398 assert_eq!(exp, res);
399
400 let test = "\
401 commit 2\n\
402 -->";
403 let exp = "\
404 commit 2\n\
405 -->";
406
407 let res = strip_comments(test);
408 assert_eq!(exp, res);
409
410 let test = "\
411 <!--\n\
412 commit 2\n\
413 ";
414 let exp = "";
415
416 let res = strip_comments(test);
417 assert_eq!(exp, res);
418
419 let test = "\
420 commit 2\n\
421 \n\
422 <!--\n\
423 <!--\n\
424 Please enter a comment for your patch update. Leaving this\n\
425 blank is also okay.\n\
426 -->\n\
427 -->";
428 let exp = "\
429 commit 2\n\
430 \n\
431 -->";
432
433 let res = strip_comments(test);
434 assert_eq!(exp, res);
435 }
436
437 #[test]
438 fn test_bytes() {
439 assert_eq!(bytes(1023).to_string(), "1023 B");
440 assert_eq!(bytes(1024).to_string(), "1 KiB");
441 assert_eq!(bytes(1024 * 9).to_string(), "9 KiB");
442 assert_eq!(bytes(1024usize.pow(2)).to_string(), "1 MiB");
443 assert_eq!(bytes(1024usize.pow(2) * 56).to_string(), "56 MiB");
444 assert_eq!(bytes(1024usize.pow(3) * 1024).to_string(), "1024 GiB");
445 }
446}