| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | | use console::{style, StyledObject, Term}; |
| 6 | | use indicatif::{ProgressBar, ProgressStyle}; |
| 7 | | use std::sync::atomic::{AtomicBool, Ordering}; |
| 8 | | use std::time::Duration; |
| 9 | |
|
| 10 | | static PLAIN: AtomicBool = AtomicBool::new(false); |
| 11 | | static UNICODE: AtomicBool = AtomicBool::new(false); |
| 12 | | static INIT: std::sync::Once = std::sync::Once::new(); |
| 13 | |
|
| 14 | |
|
| 15 | 209× | pub fn init(plain_flag: bool) { |
| 16 | 209× | INIT.call_once(|| { |
| 17 | 198× | let no_color = std::env::var_os("NO_COLOR").is_some(); |
| 18 | 198× | let tty = Term::stdout().features().is_attended(); |
| 19 | 198× | let plain = plain_flag || no_color || !tty; |
| 20 | 198× | PLAIN.store(plain, Ordering::Relaxed); |
| 21 | 198× | console::set_colors_enabled(!plain); |
| 22 | 198× | let utf8 = ["LC_ALL", "LC_CTYPE", "LANG"] |
| 23 | 198× | .iter() |
| 24 | 594× | .filter_map(|k| std::env::var(k).ok()) |
| 25 | 198× | .find(|v| !v.is_empty()) |
| 26 | 198× | .map(|v| { |
| 27 | 0× | v.to_ascii_uppercase().contains("UTF-8") || v.to_ascii_uppercase().contains("UTF8") |
| 28 | 0× | }) |
| 29 | 198× | .unwrap_or(false); |
| 30 | 198× | UNICODE.store(!plain && utf8, Ordering::Relaxed); |
| 31 | 198× | }); |
| 32 | 209× | } |
| 33 | |
|
| 34 | 79× | pub fn plain() -> bool { |
| 35 | 79× | PLAIN.load(Ordering::Relaxed) |
| 36 | 79× | } |
| 37 | |
|
| 38 | |
|
| 39 | 257× | pub fn unicode() -> bool { |
| 40 | 257× | UNICODE.load(Ordering::Relaxed) |
| 41 | 257× | } |
| 42 | |
|
| 43 | |
|
| 44 | 256× | pub fn glyph<'a>(fancy: &'a str, ascii: &'a str) -> &'a str { |
| 45 | 256× | if unicode() { |
| 46 | 0× | fancy |
| 47 | | } else { |
| 48 | 256× | ascii |
| 49 | | } |
| 50 | 256× | } |
| 51 | |
|
| 52 | |
|
| 53 | 116× | pub fn done<T: std::fmt::Display>(v: T) -> String { |
| 54 | 116× | format!("{} {}", ok(glyph("✔", "*")), ok(v)) |
| 55 | 116× | } |
| 56 | |
|
| 57 | |
|
| 58 | 4× | pub fn caution() -> String { |
| 59 | 4× | warn(glyph("⚠", "!")) |
| 60 | 4× | } |
| 61 | |
|
| 62 | |
|
| 63 | 24× | pub fn note() -> String { |
| 64 | 24× | dim(glyph("›", ">")) |
| 65 | 24× | } |
| 66 | |
|
| 67 | |
|
| 68 | 7× | pub fn section<T: std::fmt::Display>(v: T) -> String { |
| 69 | 7× | format!("{} {}", s(glyph("▍", "#")).cyan(), s(v).bold()) |
| 70 | 7× | } |
| 71 | |
|
| 72 | |
|
| 73 | 77× | pub fn kv<T: std::fmt::Display>(key: &str, value: T) -> String { |
| 74 | 77× | format!(" {} {value}", dim(format!("{key:<8}"))) |
| 75 | 77× | } |
| 76 | |
|
| 77 | |
|
| 78 | 36× | pub fn author<T: std::fmt::Display>(v: T) -> String { |
| 79 | 36× | s(v).magenta().to_string() |
| 80 | 36× | } |
| 81 | |
|
| 82 | |
|
| 83 | 101× | pub fn bookmark<T: std::fmt::Display>(v: T) -> String { |
| 84 | 101× | s(v).green().bold().to_string() |
| 85 | 101× | } |
| 86 | |
|
| 87 | |
|
| 88 | 38× | pub fn subject<T: std::fmt::Display>(v: T) -> String { |
| 89 | 38× | s(v).bold().to_string() |
| 90 | 38× | } |
| 91 | |
|
| 92 | |
|
| 93 | 30× | pub fn sep() -> String { |
| 94 | 30× | dim(format!(" {} ", glyph("·", "-"))) |
| 95 | 30× | } |
| 96 | |
|
| 97 | |
|
| 98 | |
|
| 99 | 15× | pub fn ago(ms: u64) -> String { |
| 100 | 15× | let now = std::time::SystemTime::now() |
| 101 | 15× | .duration_since(std::time::UNIX_EPOCH) |
| 102 | 15× | .map(|d| d.as_millis() as u64) |
| 103 | 15× | .unwrap_or(0); |
| 104 | 15× | let secs = now.saturating_sub(ms) / 1000; |
| 105 | 15× | let unit = |n: u64, w: &str| format!("{n} {w}{} ago", if n == 1 { "" } else { "s" }); |
| 106 | 15× | match secs { |
| 107 | 15× | 0..=44 => "just now".into(), |
| 108 | 6× | 45..=89 => "a minute ago".into(), |
| 109 | 5× | 90..=2_699 => unit(secs.div_ceil(60), "minute"), |
| 110 | 4× | 2_700..=5_399 => "an hour ago".into(), |
| 111 | 4× | 5_400..=79_199 => unit(secs.div_ceil(3_600), "hour"), |
| 112 | 3× | 79_200..=129_599 => "yesterday".into(), |
| 113 | 2× | 129_600..=2_591_999 => unit(secs.div_ceil(86_400), "day"), |
| 114 | 1× | _ => super::format::when(ms) |
| 115 | 1× | .split(' ') |
| 116 | 1× | .next() |
| 117 | 1× | .unwrap_or("") |
| 118 | 1× | .to_string(), |
| 119 | | } |
| 120 | 15× | } |
| 121 | |
|
| 122 | |
|
| 123 | 4× | pub fn log_bullet(working_copy: bool, head: bool, conflicted: bool) -> String { |
| 124 | 4× | if working_copy { |
| 125 | 1× | s(glyph("◉", "@")).cyan().bold().to_string() |
| 126 | 3× | } else if conflicted { |
| 127 | 0× | err(glyph("✱", "!")) |
| 128 | 3× | } else if head { |
| 129 | 2× | ok(glyph("●", "*")) |
| 130 | | } else { |
| 131 | 1× | dim(glyph("○", "o")) |
| 132 | | } |
| 133 | 4× | } |
| 134 | |
|
| 135 | |
|
| 136 | 3× | pub fn status_mark(tag: &str) -> String { |
| 137 | 3× | let (g, a, word) = match tag { |
| 138 | 3× | "A" => ("✚", "+", "added"), |
| 139 | 2× | "M" => ("✎", "~", "modified"), |
| 140 | 1× | "D" => ("✖", "-", "removed"), |
| 141 | 1× | "X" => ("⚙", "x", "mode"), |
| 142 | 0× | "C!" => ("‼", "!", "markers"), |
| 143 | 0× | "R" => ("➜", ">", "renamed"), |
| 144 | 0× | "V" => ("✔", "r", "resolved"), |
| 145 | 0× | _ => ("?", "?", "unknown"), |
| 146 | | }; |
| 147 | 3× | let text = format!("{} {word:<9}", glyph(g, a)); |
| 148 | 3× | self::tag_style(tag, &text) |
| 149 | 3× | } |
| 150 | |
|
| 151 | 3× | fn tag_style(t: &str, text: &str) -> String { |
| 152 | 3× | match t { |
| 153 | 3× | "A" | "V" => s(text).green().to_string(), |
| 154 | 2× | "R" => s(text).cyan().to_string(), |
| 155 | 2× | "M" | "X" => s(text).yellow().to_string(), |
| 156 | 0× | "D" | "C!" => s(text).red().to_string(), |
| 157 | 0× | _ => text.to_string(), |
| 158 | | } |
| 159 | 3× | } |
| 160 | |
|
| 161 | 1083× | fn s<T: std::fmt::Display>(v: T) -> StyledObject<T> { |
| 162 | 1083× | style(v) |
| 163 | 1083× | } |
| 164 | |
|
| 165 | |
|
| 166 | 139× | pub fn id<T: std::fmt::Display>(v: T) -> String { |
| 167 | 139× | s(v).cyan().bold().to_string() |
| 168 | 139× | } |
| 169 | |
|
| 170 | 53× | pub fn hash<T: std::fmt::Display>(v: T) -> String { |
| 171 | 53× | s(v).dim().to_string() |
| 172 | 53× | } |
| 173 | 48× | pub fn path<T: std::fmt::Display>(v: T) -> String { |
| 174 | 48× | s(v).bold().to_string() |
| 175 | 48× | } |
| 176 | 269× | pub fn ok<T: std::fmt::Display>(v: T) -> String { |
| 177 | 269× | s(v).green().to_string() |
| 178 | 269× | } |
| 179 | 10× | pub fn warn<T: std::fmt::Display>(v: T) -> String { |
| 180 | 10× | s(v).yellow().to_string() |
| 181 | 10× | } |
| 182 | 14× | pub fn err<T: std::fmt::Display>(v: T) -> String { |
| 183 | 14× | s(v).red().bold().to_string() |
| 184 | 14× | } |
| 185 | 357× | pub fn dim<T: std::fmt::Display>(v: T) -> String { |
| 186 | 357× | s(v).dim().to_string() |
| 187 | 357× | } |
| 188 | 0× | pub fn heading<T: std::fmt::Display>(v: T) -> String { |
| 189 | 0× | s(v).bold().underlined().to_string() |
| 190 | 0× | } |
| 191 | |
|
| 192 | |
|
| 193 | | pub struct Spinner { |
| 194 | | bar: Option<ProgressBar>, |
| 195 | | } |
| 196 | |
|
| 197 | 79× | pub fn spinner(msg: &str) -> Spinner { |
| 198 | 79× | if plain() { |
| 199 | 79× | return Spinner { bar: None }; |
| 200 | 0× | } |
| 201 | 0× | let bar = ProgressBar::new_spinner(); |
| 202 | 0× | bar.set_style( |
| 203 | 0× | ProgressStyle::with_template("{spinner:.cyan} {msg}") |
| 204 | 0× | .unwrap() |
| 205 | 0× | .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", " "]), |
| 206 | | ); |
| 207 | 0× | bar.set_message(msg.to_string()); |
| 208 | 0× | bar.enable_steady_tick(Duration::from_millis(80)); |
| 209 | 0× | Spinner { bar: Some(bar) } |
| 210 | 79× | } |
| 211 | |
|
| 212 | | impl Spinner { |
| 213 | | |
| 214 | | |
| 215 | 51× | pub fn done(self, msg: &str) { |
| 216 | 51× | if let Some(b) = &self.bar { |
| 217 | 0× | b.finish_and_clear(); |
| 218 | 51× | } |
| 219 | 51× | println!("{msg}"); |
| 220 | 51× | } |
| 221 | | |
| 222 | | |
| 223 | 5× | pub fn set(&self, msg: &str) { |
| 224 | 5× | match &self.bar { |
| 225 | 0× | Some(b) => b.set_message(msg.to_string()), |
| 226 | 5× | None => eprintln!("{} {msg}", dim(glyph("›", ">"))), |
| 227 | | } |
| 228 | 5× | } |
| 229 | | |
| 230 | 26× | pub fn clear(self) { |
| 231 | 26× | if let Some(b) = &self.bar { |
| 232 | 0× | b.finish_and_clear(); |
| 233 | 26× | } |
| 234 | 26× | } |
| 235 | | } |
| 236 | |
|
| 237 | | #[cfg(test)] |
| 238 | | mod tests { |
| 239 | | use super::*; |
| 240 | |
|
| 241 | | #[test] |
| 242 | 1× | fn relative_times() { |
| 243 | 1× | let now = std::time::SystemTime::now() |
| 244 | 1× | .duration_since(std::time::UNIX_EPOCH) |
| 245 | 1× | .unwrap() |
| 246 | 1× | .as_millis() as u64; |
| 247 | 1× | assert_eq!(ago(now), "just now"); |
| 248 | 1× | assert_eq!(ago(now - 60_000), "a minute ago"); |
| 249 | 1× | assert_eq!(ago(now - 5 * 60_000), "5 minutes ago"); |
| 250 | 1× | assert_eq!(ago(now - 3 * 3_600_000), "3 hours ago"); |
| 251 | 1× | assert_eq!(ago(now - 25 * 3_600_000), "yesterday"); |
| 252 | 1× | assert_eq!(ago(now - 4 * 86_400_000), "4 days ago"); |
| 253 | 1× | assert_eq!(ago(0), "1970-01-01"); |
| 254 | 1× | assert_eq!(glyph("✔", "*"), if unicode() { "✔" } else { "*" }); |
| 255 | 1× | } |
| 256 | | } |