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!("{}\n", self.caption(&format!("⚠️ {}", err.caption()))));
174
175 result.push_str(&format!("{}\n", self.error(&err.to_string())));
177
178 if err.is_retryable() {
180 result.push_str(&format!("{}Retryable: {}{}\n",
181 self.dim,
182 self.success("Yes"),
183 self.reset
184 ));
185 } else {
186 result.push_str(&format!("{}Retryable: {}{}\n",
187 self.dim,
188 self.error("No"),
189 self.reset
190 ));
191 }
192
193 if let Some(source) = err.source() {
195 result.push_str(&format!("{}Caused by: {}{}\n",
196 self.dim,
197 self.error(&source.to_string()),
198 self.reset
199 ));
200 }
201
202 result
203 }
204}
205
206pub fn print_error<E: crate::error::ForgeError>(err: &E) {
208 let theme = ConsoleTheme::default();
209 eprintln!("{}", theme.format_error(err));
210}
211
212pub fn install_panic_hook() {
214 let theme = ConsoleTheme::default();
215 std::panic::set_hook(Box::new(move |panic_info| {
216 let message = match panic_info.payload().downcast_ref::<&str>() {
217 Some(s) => *s,
218 None => match panic_info.payload().downcast_ref::<String>() {
219 Some(s) => s.as_str(),
220 None => "Unknown panic",
221 },
222 };
223
224 let location = if let Some(location) = panic_info.location() {
225 format!("at {}:{}", location.file(), location.line())
226 } else {
227 "at unknown location".to_string()
228 };
229
230 eprintln!("{}", theme.caption("💥 PANIC"));
231 eprintln!("{}", theme.error(&format!("{} {}", message, theme.dim(&location))));
232 }));
233}