development
rs 116 lines 3.32 KB
Raw
1 //! Terminal presentation: symbols, colour, and the plain/JSON output switch.
2
3 pub mod prompt;
4 pub mod render;
5
6 use {
7 chrono::{DateTime, Utc},
8 console::style,
9 serde::Serialize,
10 };
11
12 pub fn success_symbol() -> String {
13 style("✔").green().to_string()
14 }
15
16 /// Print a completed action.
17 pub fn success(message: impl AsRef<str>) {
18 println!("{} {}", success_symbol(), message.as_ref());
19 }
20
21 /// Print a non-fatal note. Goes to stderr so it never pollutes piped output.
22 pub fn note(message: impl AsRef<str>) {
23 eprintln!("{}", style(message.as_ref()).dim());
24 }
25
26 pub fn highlight(text: &str) -> String {
27 style(text).green().bold().to_string()
28 }
29
30 pub fn dim(text: &str) -> String {
31 style(text).dim().to_string()
32 }
33
34 pub fn bold(text: &str) -> String {
35 style(text).bold().to_string()
36 }
37
38 /// Colour an issue/PR state the way the web UI does.
39 pub fn state_badge(state: &str) -> String {
40 match state {
41 "open" => style("open").green().to_string(),
42 "merged" => style("merged").magenta().to_string(),
43 "closed" => style("closed").red().to_string(),
44 other => style(other).dim().to_string(),
45 }
46 }
47
48 /// Render a value as either pretty JSON or human output.
49 ///
50 /// Every read command routes its output through here, so `--json` is uniformly
51 /// available for scripting without each command reimplementing it.
52 pub fn emit<T: Serialize>(json: bool, value: &T, render: impl FnOnce(&T)) -> anyhow::Result<()> {
53 if json {
54 println!("{}", serde_json::to_string_pretty(value)?);
55 } else {
56 render(value);
57 }
58 Ok(())
59 }
60
61 /// A compact "how long ago" for list views, e.g. `3d`, `about 5h`, `just now`.
62 pub fn relative_time(timestamp: DateTime<Utc>) -> String {
63 let elapsed = Utc::now().signed_duration_since(timestamp);
64 let seconds = elapsed.num_seconds();
65
66 // A clock skew between client and server can put a timestamp slightly in
67 // the future; read those as "now" rather than printing a negative age.
68 if seconds < 60 {
69 return "just now".to_string();
70 }
71
72 let minutes = elapsed.num_minutes();
73 if minutes < 60 {
74 return format!("{minutes}m ago");
75 }
76 let hours = elapsed.num_hours();
77 if hours < 24 {
78 return format!("{hours}h ago");
79 }
80 let days = elapsed.num_days();
81 if days < 30 {
82 return format!("{days}d ago");
83 }
84 if days < 365 {
85 return format!("{}mo ago", days / 30);
86 }
87 format!("{}y ago", days / 365)
88 }
89
90 #[cfg(test)]
91 mod tests {
92 use super::*;
93 use chrono::Duration;
94
95 #[test]
96 fn recent_timestamps_read_as_just_now() {
97 assert_eq!(relative_time(Utc::now()), "just now");
98 }
99
100 #[test]
101 fn future_timestamps_do_not_produce_negative_ages() {
102 // Clock skew between the client and server is real; never print "-3m".
103 let future = Utc::now() + Duration::minutes(5);
104 assert_eq!(relative_time(future), "just now");
105 }
106
107 #[test]
108 fn ages_step_through_the_expected_units() {
109 let now = Utc::now();
110 assert_eq!(relative_time(now - Duration::minutes(5)), "5m ago");
111 assert_eq!(relative_time(now - Duration::hours(5)), "5h ago");
112 assert_eq!(relative_time(now - Duration::days(5)), "5d ago");
113 assert_eq!(relative_time(now - Duration::days(60)), "2mo ago");
114 assert_eq!(relative_time(now - Duration::days(800)), "2y ago");
115 }
116 }