use std::io::IsTerminal;
pub struct ConsoleTheme {
error_color: &'static str,
warning_color: &'static str,
info_color: &'static str,
success_color: &'static str,
caption_color: &'static str,
reset: &'static str,
bold: &'static str,
dim: &'static str,
}
fn terminal_supports_ansi() -> bool {
static SUPPORTS_ANSI: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*SUPPORTS_ANSI.get_or_init(|| {
if !std::io::stderr().is_terminal() {
return false;
}
if let Ok(term) = std::env::var("TERM") {
if term == "dumb" {
return false;
}
}
if std::env::var_os("NO_COLOR").is_some() {
return false;
}
#[cfg(windows)]
{
if std::env::var_os("WT_SESSION").is_some() {
return true;
}
}
true
})
}
impl Default for ConsoleTheme {
fn default() -> Self {
if terminal_supports_ansi() {
Self::with_colors()
} else {
Self::plain()
}
}
}
impl ConsoleTheme {
pub fn new() -> Self {
Self::default()
}
pub const fn with_colors() -> Self {
Self {
error_color: "\x1b[31m", warning_color: "\x1b[33m", info_color: "\x1b[34m", success_color: "\x1b[32m", caption_color: "\x1b[36m", reset: "\x1b[0m",
bold: "\x1b[1m",
dim: "\x1b[2m",
}
}
pub const fn plain() -> Self {
Self {
error_color: "",
warning_color: "",
info_color: "",
success_color: "",
caption_color: "",
reset: "",
bold: "",
dim: "",
}
}
pub fn error(&self, text: &str) -> String {
format!("{}{}{}", self.error_color, text, self.reset)
}
pub fn warning(&self, text: &str) -> String {
format!("{}{}{}", self.warning_color, text, self.reset)
}
pub fn info(&self, text: &str) -> String {
format!("{}{}{}", self.info_color, text, self.reset)
}
pub fn success(&self, text: &str) -> String {
format!("{}{}{}", self.success_color, text, self.reset)
}
pub fn caption(&self, text: &str) -> String {
format!("{}{}{}", self.caption_color, text, self.reset)
}
pub fn bold(&self, text: &str) -> String {
format!("{}{}{}", self.bold, text, self.reset)
}
pub fn dim(&self, text: &str) -> String {
format!("{}{}{}", self.dim, text, self.reset)
}
pub fn format_error<E: crate::error::ForgeError>(&self, err: &E) -> String {
use std::fmt::Write as _;
let mut buf = String::with_capacity(160);
let _ = writeln!(buf, "{}", self.caption(&format!("⚠️ {}", err.caption())));
let _ = writeln!(buf, "{}", self.error(&err.to_string()));
let marker = if err.is_retryable() {
self.success("Yes")
} else {
self.error("No")
};
let _ = writeln!(buf, "{}Retryable: {}{}", self.dim, marker, self.reset);
if let Some(source) = err.source() {
let _ = writeln!(
buf,
"{}Caused by: {}{}",
self.dim,
self.error(&source.to_string()),
self.reset
);
}
buf
}
}
pub fn print_error<E: crate::error::ForgeError>(err: &E) {
static DEFAULT_THEME: std::sync::OnceLock<ConsoleTheme> = std::sync::OnceLock::new();
let theme = DEFAULT_THEME.get_or_init(ConsoleTheme::default);
eprintln!("{}", theme.format_error(err));
}
pub fn install_panic_hook() {
let theme = ConsoleTheme::default();
std::panic::set_hook(Box::new(move |panic_info| {
let message = match panic_info.payload().downcast_ref::<&str>() {
Some(s) => *s,
None => match panic_info.payload().downcast_ref::<String>() {
Some(s) => s.as_str(),
None => "Unknown panic",
},
};
let location = if let Some(location) = panic_info.location() {
format!("at {}:{}", location.file(), location.line())
} else {
"at unknown location".to_string()
};
eprintln!("{}", theme.caption("💥 PANIC"));
eprintln!(
"{}",
theme.error(&format!("{} {}", message, theme.dim(&location)))
);
}));
}