error_forge/
console_theme.rs1pub struct ConsoleTheme {
9 error_color: String,
10 warning_color: String,
11 info_color: String,
12 success_color: String,
13 caption_color: String,
14 reset: String,
15 bold: String,
16 dim: String,
17}
18
19fn terminal_supports_ansi() -> bool {
21 #[cfg(windows)]
22 {
23 static WINDOWS_ANSI_SUPPORT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
26
27 *WINDOWS_ANSI_SUPPORT.get_or_init(|| {
28 if !atty::is(atty::Stream::Stderr) {
30 return false;
31 }
32
33 if let Ok(term) = std::env::var("TERM") {
35 if term == "dumb" {
36 return false;
37 }
38 }
39
40 if std::env::var_os("NO_COLOR").is_some() {
42 return false;
43 }
44
45 if std::env::var_os("WT_SESSION").is_some() {
47 return true;
48 }
49
50 true
52 })
53 }
54
55 #[cfg(not(windows))]
56 {
57 if !atty::is(atty::Stream::Stderr) {
59 return false;
60 }
61
62 if let Ok(term) = std::env::var("TERM") {
64 if term == "dumb" {
65 return false;
66 }
67 }
68
69 if std::env::var_os("NO_COLOR").is_some() {
71 return false;
72 }
73
74 true
75 }
76}
77
78impl Default for ConsoleTheme {
79 fn default() -> Self {
80 if terminal_supports_ansi() {
81 Self {
82 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(),
88 bold: "\x1b[1m".to_string(),
89 dim: "\x1b[2m".to_string(),
90 }
91 } else {
92 Self::plain()
94 }
95 }
96}
97
98impl ConsoleTheme {
99 pub fn new() -> Self {
102 Self::default()
103 }
104
105 pub fn with_colors() -> Self {
107 Self {
108 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(),
114 bold: "\x1b[1m".to_string(),
115 dim: "\x1b[2m".to_string(),
116 }
117 }
118
119 pub fn plain() -> Self {
121 Self {
122 error_color: "".to_string(),
123 warning_color: "".to_string(),
124 info_color: "".to_string(),
125 success_color: "".to_string(),
126 caption_color: "".to_string(),
127 reset: "".to_string(),
128 bold: "".to_string(),
129 dim: "".to_string(),
130 }
131 }
132
133 pub fn error(&self, text: &str) -> String {
135 format!("{}{}{}", self.error_color, text, self.reset)
136 }
137
138 pub fn warning(&self, text: &str) -> String {
140 format!("{}{}{}", self.warning_color, text, self.reset)
141 }
142
143 pub fn info(&self, text: &str) -> String {
145 format!("{}{}{}", self.info_color, text, self.reset)
146 }
147
148 pub fn success(&self, text: &str) -> String {
150 format!("{}{}{}", self.success_color, text, self.reset)
151 }
152
153 pub fn caption(&self, text: &str) -> String {
155 format!("{}{}{}", self.caption_color, text, self.reset)
156 }
157
158 pub fn bold(&self, text: &str) -> String {
160 format!("{}{}{}", self.bold, text, self.reset)
161 }
162
163 pub fn dim(&self, text: &str) -> String {
165 format!("{}{}{}", self.dim, text, self.reset)
166 }
167
168 pub fn format_error<E: crate::error::ForgeError>(&self, err: &E) -> String {
170 let mut result = String::new();
171
172 result.push_str(&format!(
174 "{}\n",
175 self.caption(&format!("⚠️ {}", err.caption()))
176 ));
177
178 result.push_str(&format!("{}\n", self.error(&err.to_string())));
180
181 if err.is_retryable() {
183 result.push_str(&format!(
184 "{}Retryable: {}{}\n",
185 self.dim,
186 self.success("Yes"),
187 self.reset
188 ));
189 } else {
190 result.push_str(&format!(
191 "{}Retryable: {}{}\n",
192 self.dim,
193 self.error("No"),
194 self.reset
195 ));
196 }
197
198 if let Some(source) = err.source() {
200 result.push_str(&format!(
201 "{}Caused by: {}{}\n",
202 self.dim,
203 self.error(&source.to_string()),
204 self.reset
205 ));
206 }
207
208 result
209 }
210}
211
212pub fn print_error<E: crate::error::ForgeError>(err: &E) {
214 let theme = ConsoleTheme::default();
215 eprintln!("{}", theme.format_error(err));
216}
217
218pub fn install_panic_hook() {
220 let theme = ConsoleTheme::default();
221 std::panic::set_hook(Box::new(move |panic_info| {
222 let message = match panic_info.payload().downcast_ref::<&str>() {
223 Some(s) => *s,
224 None => match panic_info.payload().downcast_ref::<String>() {
225 Some(s) => s.as_str(),
226 None => "Unknown panic",
227 },
228 };
229
230 let location = if let Some(location) = panic_info.location() {
231 format!("at {}:{}", location.file(), location.line())
232 } else {
233 "at unknown location".to_string()
234 };
235
236 eprintln!("{}", theme.caption("💥 PANIC"));
237 eprintln!(
238 "{}",
239 theme.error(&format!("{} {}", message, theme.dim(&location)))
240 );
241 }));
242}