pub struct ConsoleTheme {
error_color: String,
warning_color: String,
info_color: String,
success_color: String,
caption_color: String,
reset: String,
bold: String,
dim: String,
}
fn terminal_supports_ansi() -> bool {
#[cfg(windows)]
{
static WINDOWS_ANSI_SUPPORT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*WINDOWS_ANSI_SUPPORT.get_or_init(|| {
if !atty::is(atty::Stream::Stderr) {
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;
}
if std::env::var_os("WT_SESSION").is_some() {
return true;
}
true
})
}
#[cfg(not(windows))]
{
if !atty::is(atty::Stream::Stderr) {
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;
}
true
}
}
impl Default for ConsoleTheme {
fn default() -> Self {
if terminal_supports_ansi() {
Self {
error_color: "\x1b[31m".to_string(), warning_color: "\x1b[33m".to_string(), info_color: "\x1b[34m".to_string(), success_color: "\x1b[32m".to_string(), caption_color: "\x1b[36m".to_string(), reset: "\x1b[0m".to_string(),
bold: "\x1b[1m".to_string(),
dim: "\x1b[2m".to_string(),
}
} else {
Self::plain()
}
}
}
impl ConsoleTheme {
pub fn new() -> Self {
Self::default()
}
pub fn with_colors() -> Self {
Self {
error_color: "\x1b[31m".to_string(), warning_color: "\x1b[33m".to_string(), info_color: "\x1b[34m".to_string(), success_color: "\x1b[32m".to_string(), caption_color: "\x1b[36m".to_string(), reset: "\x1b[0m".to_string(),
bold: "\x1b[1m".to_string(),
dim: "\x1b[2m".to_string(),
}
}
pub fn plain() -> Self {
Self {
error_color: "".to_string(),
warning_color: "".to_string(),
info_color: "".to_string(),
success_color: "".to_string(),
caption_color: "".to_string(),
reset: "".to_string(),
bold: "".to_string(),
dim: "".to_string(),
}
}
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 {
let mut result = String::new();
result.push_str(&format!(
"{}\n",
self.caption(&format!("⚠️ {}", err.caption()))
));
result.push_str(&format!("{}\n", self.error(&err.to_string())));
if err.is_retryable() {
result.push_str(&format!(
"{}Retryable: {}{}\n",
self.dim,
self.success("Yes"),
self.reset
));
} else {
result.push_str(&format!(
"{}Retryable: {}{}\n",
self.dim,
self.error("No"),
self.reset
));
}
if let Some(source) = err.source() {
result.push_str(&format!(
"{}Caused by: {}{}\n",
self.dim,
self.error(&source.to_string()),
self.reset
));
}
result
}
}
pub fn print_error<E: crate::error::ForgeError>(err: &E) {
let theme = 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)))
);
}));
}