1use std::io::{IsTerminal, stdout};
7use std::sync::OnceLock;
8use std::time::{Duration, Instant};
9
10use colored::{ColoredString, Colorize};
11
12pub fn pewter(s: &str) -> ColoredString {
20 match background() {
21 Background::Light => paint(s, 0x55, 0x5c, 0x5f),
22 Background::Dark => paint(s, 0x7d, 0x85, 0x88),
23 }
24}
25pub fn emerald(s: &str) -> ColoredString {
26 match background() {
27 Background::Light => paint(s, 0x15, 0x80, 0x3d),
28 Background::Dark => paint(s, 0x22, 0xc5, 0x5e),
29 }
30}
31pub fn amber(s: &str) -> ColoredString {
32 match background() {
33 Background::Light => paint(s, 0x8a, 0x62, 0x00),
34 Background::Dark => paint(s, 0xe0, 0xa5, 0x2a),
35 }
36}
37pub fn danger(s: &str) -> ColoredString {
38 match background() {
39 Background::Light => paint(s, 0xc0, 0x1c, 0x1c),
40 Background::Dark => paint(s, 0xef, 0x54, 0x54),
41 }
42}
43pub fn info(s: &str) -> ColoredString {
44 match background() {
45 Background::Light => paint(s, 0x1d, 0x6c, 0xd4),
46 Background::Dark => paint(s, 0x5a, 0xa0, 0xf2),
47 }
48}
49
50pub fn cmd(s: &str) -> ColoredString {
54 info(s)
55}
56pub fn title(s: &str) -> ColoredString {
60 match color_support() {
61 ColorSupport::None => s.normal(),
62 _ => s.bold(),
63 }
64}
65pub fn ident(s: &str) -> ColoredString {
69 pewter(s).bold()
70}
71pub fn ok(s: &str) -> ColoredString {
73 emerald(s).bold()
74}
75pub fn warn(s: &str) -> ColoredString {
76 amber(s).bold()
77}
78pub fn err(s: &str) -> ColoredString {
79 danger(s).bold()
80}
81
82fn paint(s: &str, r: u8, g: u8, b: u8) -> ColoredString {
83 match color_support() {
84 ColorSupport::None => s.normal(),
85 _ => s.truecolor(r, g, b),
86 }
87}
88
89pub mod sym {
92 pub const OK: &str = "OK";
93 pub const ERR: &str = "X";
94 pub const WARN: &str = "!";
95 pub const TIP: &str = ">";
96 pub const BULLET: &str = "-";
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub enum ColorSupport {
103 TrueColor,
104 Ansi256,
105 None,
106}
107
108static COLOR_CACHE: OnceLock<ColorSupport> = OnceLock::new();
109
110pub fn detect_color_support() -> ColorSupport {
112 *COLOR_CACHE.get_or_init(detect_color_support_uncached)
113}
114
115fn color_support() -> ColorSupport {
116 *COLOR_CACHE.get_or_init(detect_color_support_uncached)
117}
118
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub enum Background {
128 Dark,
129 Light,
130}
131
132static BG_CACHE: OnceLock<Background> = OnceLock::new();
133
134fn background() -> Background {
135 *BG_CACHE.get_or_init(detect_background_uncached)
136}
137
138fn detect_background_uncached() -> Background {
139 use difflore_core::env;
140 if let Some(theme) = env::var(env::DIFFLORE_THEME) {
142 match theme.trim().to_ascii_lowercase().as_str() {
143 "light" => return Background::Light,
144 "dark" => return Background::Dark,
145 _ => {}
146 }
147 }
148 if let Some(fgbg) = env::var(env::COLORFGBG)
151 && let Some(bg) = fgbg.rsplit(';').next()
152 && let Ok(idx) = bg.trim().parse::<u8>()
153 {
154 return if matches!(idx, 0..=6 | 8) {
155 Background::Dark
156 } else {
157 Background::Light
158 };
159 }
160 Background::Dark
161}
162
163fn detect_color_support_uncached() -> ColorSupport {
164 use difflore_core::env;
165 if env::flag_set(env::NO_COLOR) {
166 return ColorSupport::None;
167 }
168 if !stdout().is_terminal() {
169 return ColorSupport::None;
170 }
171 match env::var(env::COLORTERM).as_deref() {
172 Some("truecolor" | "24bit") => ColorSupport::TrueColor,
173 _ => match env::var(env::TERM).as_deref() {
174 Some(t) if t.contains("256color") => ColorSupport::Ansi256,
175 _ => ColorSupport::TrueColor, },
177 }
178}
179
180pub struct Hint {
185 pub label: &'static str,
186 pub body: String,
187}
188
189impl Hint {
190 pub(crate) fn try_(body: impl Into<String>) -> Self {
191 Self {
192 label: "try",
193 body: body.into(),
194 }
195 }
196}
197
198pub fn report_error(summary: &str, context: &str, hints: &[Hint]) {
211 eprintln!(
212 "{} {} {} {}",
213 danger(sym::ERR),
214 err("error"),
215 pewter(sym::BULLET),
216 summary
217 );
218 eprintln!();
219 if !context.is_empty() {
220 for line in context.lines() {
221 eprintln!(" {line}");
222 }
223 eprintln!();
224 }
225 for h in hints {
226 eprintln!(" {} {} {}", emerald(sym::TIP), pewter(h.label), h.body);
227 }
228}
229
230const SPIN_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
233
234pub struct Spinner {
237 label: String,
238 frame: std::cell::Cell<usize>,
239 last_tick: std::cell::Cell<Instant>,
240 tick_interval: Duration,
241}
242
243impl Spinner {
244 pub(crate) fn new(label: &str) -> Self {
245 let s = Self {
246 label: label.to_owned(),
247 frame: std::cell::Cell::new(0),
248 last_tick: std::cell::Cell::new(Instant::now()),
249 tick_interval: Duration::from_millis(80),
250 };
251 s.draw();
254 s
255 }
256
257 pub(crate) fn tick(&self) {
258 let now = Instant::now();
259 if now.duration_since(self.last_tick.get()) < self.tick_interval {
260 return;
261 }
262 self.last_tick.set(now);
263 self.frame.set((self.frame.get() + 1) % SPIN_FRAMES.len());
264 self.draw();
265 }
266
267 fn draw(&self) {
268 if color_support() == ColorSupport::None {
269 return;
272 }
273 let glyph = SPIN_FRAMES[self.frame.get()];
274 eprint!("\r{} {} ", pewter(glyph), self.label);
278 let _ = std::io::Write::flush(&mut std::io::stderr());
279 }
280
281 pub(crate) fn set_message(&mut self, msg: &str) {
282 msg.clone_into(&mut self.label);
283 self.draw();
284 }
285
286 pub(crate) fn finish_ok(self, msg: &str) {
288 self.clear_line();
289 eprintln!("{} {}", emerald(sym::OK), msg);
290 }
291
292 pub(crate) fn finish_err(self, msg: &str) {
294 self.clear_line();
295 eprintln!("{} {}", danger(sym::ERR), msg);
296 }
297
298 fn clear_line(&self) {
299 if color_support() == ColorSupport::None {
300 return;
301 }
302 let pad = " ".repeat(self.label.chars().count() + 6);
305 eprint!("\r{pad}\r");
306 let _ = std::io::Write::flush(&mut std::io::stderr());
307 }
308}
309
310pub fn wordmark() -> String {
312 format!("{}{}", pewter("diff").bold(), emerald("lore").bold())
313}
314
315pub const DIVIDER: &str = "─────────────────────────────────────────────";
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn hint_helpers_use_locked_vocabulary() {
323 assert_eq!(Hint::try_("x").label, "try");
324 }
325
326 #[test]
327 fn symbols_match_contract() {
328 assert_eq!(sym::OK, "OK");
329 assert_eq!(sym::ERR, "X");
330 assert_eq!(sym::WARN, "!");
331 assert_eq!(sym::TIP, ">");
332 assert_eq!(sym::BULLET, "-");
333 }
334}