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