use std::io::{IsTerminal, stdout};
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use colored::{ColoredString, Colorize};
pub fn pewter(s: &str) -> ColoredString {
match background() {
Background::Light => paint(s, 0x55, 0x5c, 0x5f),
Background::Dark => paint(s, 0x7d, 0x85, 0x88),
}
}
pub fn emerald(s: &str) -> ColoredString {
match background() {
Background::Light => paint(s, 0x15, 0x80, 0x3d),
Background::Dark => paint(s, 0x22, 0xc5, 0x5e),
}
}
pub fn amber(s: &str) -> ColoredString {
match background() {
Background::Light => paint(s, 0x8a, 0x62, 0x00),
Background::Dark => paint(s, 0xe0, 0xa5, 0x2a),
}
}
pub fn danger(s: &str) -> ColoredString {
match background() {
Background::Light => paint(s, 0xc0, 0x1c, 0x1c),
Background::Dark => paint(s, 0xef, 0x54, 0x54),
}
}
pub fn info(s: &str) -> ColoredString {
match background() {
Background::Light => paint(s, 0x1d, 0x6c, 0xd4),
Background::Dark => paint(s, 0x5a, 0xa0, 0xf2),
}
}
pub fn cmd(s: &str) -> ColoredString {
info(s)
}
pub fn title(s: &str) -> ColoredString {
match color_support() {
ColorSupport::None => s.normal(),
_ => s.bold(),
}
}
pub fn ident(s: &str) -> ColoredString {
pewter(s).bold()
}
pub fn ok(s: &str) -> ColoredString {
emerald(s).bold()
}
pub fn warn(s: &str) -> ColoredString {
amber(s).bold()
}
pub fn err(s: &str) -> ColoredString {
danger(s).bold()
}
fn paint(s: &str, r: u8, g: u8, b: u8) -> ColoredString {
match color_support() {
ColorSupport::None => s.normal(),
_ => s.truecolor(r, g, b),
}
}
pub mod sym {
pub const OK: &str = "OK";
pub const ERR: &str = "X";
pub const WARN: &str = "!";
pub const TIP: &str = ">";
pub const BULLET: &str = "-";
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ColorSupport {
TrueColor,
Ansi256,
None,
}
static COLOR_CACHE: OnceLock<ColorSupport> = OnceLock::new();
pub fn detect_color_support() -> ColorSupport {
*COLOR_CACHE.get_or_init(detect_color_support_uncached)
}
fn color_support() -> ColorSupport {
*COLOR_CACHE.get_or_init(detect_color_support_uncached)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Background {
Dark,
Light,
}
static BG_CACHE: OnceLock<Background> = OnceLock::new();
fn background() -> Background {
*BG_CACHE.get_or_init(detect_background_uncached)
}
fn detect_background_uncached() -> Background {
use difflore_core::env;
if let Some(theme) = env::var(env::DIFFLORE_THEME) {
match theme.trim().to_ascii_lowercase().as_str() {
"light" => return Background::Light,
"dark" => return Background::Dark,
_ => {}
}
}
if let Some(fgbg) = env::var(env::COLORFGBG)
&& let Some(bg) = fgbg.rsplit(';').next()
&& let Ok(idx) = bg.trim().parse::<u8>()
{
return if matches!(idx, 0..=6 | 8) {
Background::Dark
} else {
Background::Light
};
}
Background::Dark
}
fn detect_color_support_uncached() -> ColorSupport {
use difflore_core::env;
if env::flag_set(env::NO_COLOR) {
return ColorSupport::None;
}
if !stdout().is_terminal() {
return ColorSupport::None;
}
match env::var(env::COLORTERM).as_deref() {
Some("truecolor" | "24bit") => ColorSupport::TrueColor,
_ => match env::var(env::TERM).as_deref() {
Some(t) if t.contains("256color") => ColorSupport::Ansi256,
_ => ColorSupport::TrueColor, },
}
}
pub struct Hint {
pub label: &'static str,
pub body: String,
}
impl Hint {
pub(crate) fn try_(body: impl Into<String>) -> Self {
Self {
label: "try",
body: body.into(),
}
}
}
pub fn report_error(summary: &str, context: &str, hints: &[Hint]) {
eprintln!(
"{} {} {} {}",
danger(sym::ERR),
err("error"),
pewter(sym::BULLET),
summary
);
eprintln!();
if !context.is_empty() {
for line in context.lines() {
eprintln!(" {line}");
}
eprintln!();
}
for h in hints {
eprintln!(" {} {} {}", emerald(sym::TIP), pewter(h.label), h.body);
}
}
const SPIN_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
pub struct Spinner {
label: String,
frame: std::cell::Cell<usize>,
last_tick: std::cell::Cell<Instant>,
tick_interval: Duration,
}
impl Spinner {
pub(crate) fn new(label: &str) -> Self {
let s = Self {
label: label.to_owned(),
frame: std::cell::Cell::new(0),
last_tick: std::cell::Cell::new(Instant::now()),
tick_interval: Duration::from_millis(80),
};
s.draw();
s
}
pub(crate) fn tick(&self) {
let now = Instant::now();
if now.duration_since(self.last_tick.get()) < self.tick_interval {
return;
}
self.last_tick.set(now);
self.frame.set((self.frame.get() + 1) % SPIN_FRAMES.len());
self.draw();
}
fn draw(&self) {
if color_support() == ColorSupport::None {
return;
}
let glyph = SPIN_FRAMES[self.frame.get()];
eprint!("\r{} {} ", pewter(glyph), self.label);
let _ = std::io::Write::flush(&mut std::io::stderr());
}
pub(crate) fn set_message(&mut self, msg: &str) {
msg.clone_into(&mut self.label);
self.draw();
}
pub(crate) fn finish_ok(self, msg: &str) {
self.clear_line();
eprintln!("{} {}", emerald(sym::OK), msg);
}
pub(crate) fn finish_err(self, msg: &str) {
self.clear_line();
eprintln!("{} {}", danger(sym::ERR), msg);
}
fn clear_line(&self) {
if color_support() == ColorSupport::None {
return;
}
let pad = " ".repeat(self.label.chars().count() + 6);
eprint!("\r{pad}\r");
let _ = std::io::Write::flush(&mut std::io::stderr());
}
}
pub fn wordmark() -> String {
format!("{}{}", pewter("diff").bold(), emerald("lore").bold())
}
pub const DIVIDER: &str = "─────────────────────────────────────────────";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hint_helpers_use_locked_vocabulary() {
assert_eq!(Hint::try_("x").label, "try");
}
#[test]
fn symbols_match_contract() {
assert_eq!(sym::OK, "OK");
assert_eq!(sym::ERR, "X");
assert_eq!(sym::WARN, "!");
assert_eq!(sym::TIP, ">");
assert_eq!(sym::BULLET, "-");
}
}