error_forge/
console_theme.rs1use std::io::IsTerminal;
10
11pub struct ConsoleTheme {
17 error_color: &'static str,
18 warning_color: &'static str,
19 info_color: &'static str,
20 success_color: &'static str,
21 caption_color: &'static str,
22 reset: &'static str,
23 bold: &'static str,
24 dim: &'static str,
25}
26
27fn terminal_supports_ansi() -> bool {
29 static SUPPORTS_ANSI: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
34
35 *SUPPORTS_ANSI.get_or_init(|| {
36 if !std::io::stderr().is_terminal() {
38 return false;
39 }
40
41 if let Ok(term) = std::env::var("TERM") {
43 if term == "dumb" {
44 return false;
45 }
46 }
47
48 if std::env::var_os("NO_COLOR").is_some() {
50 return false;
51 }
52
53 #[cfg(windows)]
55 {
56 if std::env::var_os("WT_SESSION").is_some() {
57 return true;
58 }
59 }
60
61 true
64 })
65}
66
67impl Default for ConsoleTheme {
68 fn default() -> Self {
69 if terminal_supports_ansi() {
70 Self::with_colors()
71 } else {
72 Self::plain()
73 }
74 }
75}
76
77impl ConsoleTheme {
78 pub fn new() -> Self {
82 Self::default()
83 }
84
85 pub const fn with_colors() -> Self {
88 Self {
89 error_color: "\x1b[31m", warning_color: "\x1b[33m", info_color: "\x1b[34m", success_color: "\x1b[32m", caption_color: "\x1b[36m", reset: "\x1b[0m",
95 bold: "\x1b[1m",
96 dim: "\x1b[2m",
97 }
98 }
99
100 pub const fn plain() -> Self {
103 Self {
104 error_color: "",
105 warning_color: "",
106 info_color: "",
107 success_color: "",
108 caption_color: "",
109 reset: "",
110 bold: "",
111 dim: "",
112 }
113 }
114
115 pub fn error(&self, text: &str) -> String {
117 format!("{}{}{}", self.error_color, text, self.reset)
118 }
119
120 pub fn warning(&self, text: &str) -> String {
122 format!("{}{}{}", self.warning_color, text, self.reset)
123 }
124
125 pub fn info(&self, text: &str) -> String {
127 format!("{}{}{}", self.info_color, text, self.reset)
128 }
129
130 pub fn success(&self, text: &str) -> String {
132 format!("{}{}{}", self.success_color, text, self.reset)
133 }
134
135 pub fn caption(&self, text: &str) -> String {
137 format!("{}{}{}", self.caption_color, text, self.reset)
138 }
139
140 pub fn bold(&self, text: &str) -> String {
142 format!("{}{}{}", self.bold, text, self.reset)
143 }
144
145 pub fn dim(&self, text: &str) -> String {
147 format!("{}{}{}", self.dim, text, self.reset)
148 }
149
150 pub fn format_error<E: crate::error::ForgeError>(&self, err: &E) -> String {
156 use std::fmt::Write as _;
157 let mut buf = String::with_capacity(160);
158
159 let _ = writeln!(buf, "{}", self.caption(&format!("⚠️ {}", err.caption())));
162
163 let _ = writeln!(buf, "{}", self.error(&err.to_string()));
165
166 let marker = if err.is_retryable() {
168 self.success("Yes")
169 } else {
170 self.error("No")
171 };
172 let _ = writeln!(buf, "{}Retryable: {}{}", self.dim, marker, self.reset);
173
174 if let Some(source) = err.source() {
176 let _ = writeln!(
177 buf,
178 "{}Caused by: {}{}",
179 self.dim,
180 self.error(&source.to_string()),
181 self.reset
182 );
183 }
184
185 buf
186 }
187}
188
189pub fn print_error<E: crate::error::ForgeError>(err: &E) {
195 static DEFAULT_THEME: std::sync::OnceLock<ConsoleTheme> = std::sync::OnceLock::new();
196 let theme = DEFAULT_THEME.get_or_init(ConsoleTheme::default);
197 eprintln!("{}", theme.format_error(err));
198}
199
200pub fn install_panic_hook() {
202 let theme = ConsoleTheme::default();
203 std::panic::set_hook(Box::new(move |panic_info| {
204 let message = match panic_info.payload().downcast_ref::<&str>() {
205 Some(s) => *s,
206 None => match panic_info.payload().downcast_ref::<String>() {
207 Some(s) => s.as_str(),
208 None => "Unknown panic",
209 },
210 };
211
212 let location = if let Some(location) = panic_info.location() {
213 format!("at {}:{}", location.file(), location.line())
214 } else {
215 "at unknown location".to_string()
216 };
217
218 eprintln!("{}", theme.caption("💥 PANIC"));
219 eprintln!(
220 "{}",
221 theme.error(&format!("{} {}", message, theme.dim(&location)))
222 );
223 }));
224}