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