1use std::fmt;
6use std::io;
7
8pub type Result<T> = std::result::Result<T, Error>;
10
11#[derive(Debug)]
13pub enum Error {
14 Io(io::Error),
16
17 Terminal(String),
19
20 Event(String),
22
23 Command(String),
25
26 Component(String),
28
29 Model(String),
31
32 Config(String),
34
35 Custom(Box<dyn std::error::Error + Send + Sync>),
37}
38
39impl fmt::Display for Error {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Error::Io(err) => write!(f, "I/O error: {err}"),
43 Error::Terminal(msg) => write!(f, "Terminal error: {msg}"),
44 Error::Event(msg) => write!(f, "Event error: {msg}"),
45 Error::Command(msg) => write!(f, "Command error: {msg}"),
46 Error::Component(msg) => write!(f, "Component error: {msg}"),
47 Error::Model(msg) => write!(f, "Model error: {msg}"),
48 Error::Config(msg) => write!(f, "Configuration error: {msg}"),
49 Error::Custom(err) => write!(f, "Custom error: {err}"),
50 }
51 }
52}
53
54impl std::error::Error for Error {
55 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
56 match self {
57 Error::Io(err) => Some(err),
58 Error::Custom(err) => Some(err.as_ref()),
59 _ => None,
60 }
61 }
62}
63
64impl From<io::Error> for Error {
65 fn from(err: io::Error) -> Self {
66 Error::Io(err)
67 }
68}
69
70impl From<std::sync::mpsc::RecvError> for Error {
71 fn from(err: std::sync::mpsc::RecvError) -> Self {
72 Error::Event(format!("Channel receive error: {err}"))
73 }
74}
75
76impl<T> From<std::sync::mpsc::SendError<T>> for Error {
77 fn from(err: std::sync::mpsc::SendError<T>) -> Self {
78 Error::Event(format!("Channel send error: {err}"))
79 }
80}
81
82pub trait ErrorContext<T> {
84 fn context(self, msg: &str) -> Result<T>;
86
87 fn with_context<F>(self, f: F) -> Result<T>
89 where
90 F: FnOnce() -> String;
91}
92
93impl<T, E> ErrorContext<T> for std::result::Result<T, E>
94where
95 E: Into<Error>,
96{
97 fn context(self, msg: &str) -> Result<T> {
98 self.map_err(|err| {
99 let base_error = err.into();
100 Error::Custom(Box::new(ContextError {
101 context: msg.to_string(),
102 source: base_error,
103 }))
104 })
105 }
106
107 fn with_context<F>(self, f: F) -> Result<T>
108 where
109 F: FnOnce() -> String,
110 {
111 self.map_err(|err| {
112 let base_error = err.into();
113 Error::Custom(Box::new(ContextError {
114 context: f(),
115 source: base_error,
116 }))
117 })
118 }
119}
120
121#[derive(Debug)]
123struct ContextError {
124 context: String,
125 source: Error,
126}
127
128impl fmt::Display for ContextError {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 write!(f, "{}: {}", self.context, self.source)
131 }
132}
133
134impl std::error::Error for ContextError {
135 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
136 Some(&self.source)
137 }
138}
139
140pub trait ErrorHandler {
142 fn handle_error(&mut self, error: Error) -> bool;
144}
145
146pub struct DefaultErrorHandler;
148
149impl ErrorHandler for DefaultErrorHandler {
150 fn handle_error(&mut self, error: Error) -> bool {
151 eprintln!("Error: {error}");
152
153 let mut current_error: &dyn std::error::Error = &error;
155 while let Some(source) = current_error.source() {
156 eprintln!(" Caused by: {source}");
157 current_error = source;
158 }
159
160 false }
162}
163
164pub fn set_panic_handler() {
166 std::panic::set_hook(Box::new(|panic_info| {
167 let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
168 s.to_string()
169 } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
170 s.clone()
171 } else {
172 "Unknown panic".to_string()
173 };
174
175 let location = if let Some(location) = panic_info.location() {
176 format!(
177 " at {}:{}:{}",
178 location.file(),
179 location.line(),
180 location.column()
181 )
182 } else {
183 String::new()
184 };
185
186 eprintln!("Panic occurred: {msg}{location}");
187 }));
188}
189
190#[macro_export]
192macro_rules! bail {
193 ($msg:literal $(,)?) => {
194 return Err($crate::error::Error::Custom(
195 format!($msg).into()
196 ))
197 };
198 ($err:expr $(,)?) => {
199 return Err($crate::error::Error::Custom(
200 format!("{}", $err).into()
201 ))
202 };
203 ($fmt:expr, $($arg:tt)*) => {
204 return Err($crate::error::Error::Custom(
205 format!($fmt, $($arg)*).into()
206 ))
207 };
208}
209
210#[macro_export]
212macro_rules! ensure {
213 ($cond:expr, $msg:literal $(,)?) => {
214 if !$cond {
215 $crate::bail!($msg);
216 }
217 };
218 ($cond:expr, $err:expr $(,)?) => {
219 if !$cond {
220 $crate::bail!($err);
221 }
222 };
223 ($cond:expr, $fmt:expr, $($arg:tt)*) => {
224 if !$cond {
225 $crate::bail!($fmt, $($arg)*);
226 }
227 };
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use std::error::Error as StdError;
234
235 #[test]
236 fn test_error_display() {
237 let err = Error::Terminal("Failed to initialize".to_string());
238 assert_eq!(err.to_string(), "Terminal error: Failed to initialize");
239
240 let err = Error::Io(io::Error::new(io::ErrorKind::NotFound, "File not found"));
241 assert_eq!(err.to_string(), "I/O error: File not found");
242 }
243
244 #[test]
245 fn test_error_context() {
246 let result: Result<()> = Err(Error::Terminal("Base error".to_string()));
247 let with_context = result.context("While initializing terminal");
248
249 assert!(with_context.is_err());
250 let err_str = with_context.unwrap_err().to_string();
251 assert!(err_str.contains("While initializing terminal"));
252 assert!(err_str.contains("Base error"));
253 }
254
255 #[test]
256 fn test_error_from_io() {
257 let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "Access denied");
258 let err: Error = io_err.into();
259
260 match err {
261 Error::Io(_) => (),
262 _ => panic!("Expected Io error variant"),
263 }
264 }
265
266 #[test]
267 fn test_bail_macro() {
268 fn test_fn() -> Result<()> {
269 bail!("Test error");
270 }
271
272 assert!(test_fn().is_err());
273 assert_eq!(
274 test_fn().unwrap_err().to_string(),
275 "Custom error: Test error"
276 );
277 }
278
279 #[test]
280 fn test_ensure_macro() {
281 fn test_fn(value: i32) -> Result<i32> {
282 ensure!(value > 0, "Value must be positive");
283 Ok(value)
284 }
285
286 assert!(test_fn(5).is_ok());
287 assert!(test_fn(-1).is_err());
288 }
289
290 #[test]
291 fn test_all_error_variants() {
292 let errors = vec![
293 Error::Terminal("terminal error".to_string()),
294 Error::Event("event error".to_string()),
295 Error::Command("command error".to_string()),
296 Error::Component("component error".to_string()),
297 Error::Model("model error".to_string()),
298 Error::Config("config error".to_string()),
299 ];
300
301 for error in errors {
302 let display_str = error.to_string();
303 assert!(!display_str.is_empty());
304
305 assert!(error.source().is_none());
307 }
308 }
309
310 #[test]
311 fn test_error_source_chain() {
312 let io_err = io::Error::new(io::ErrorKind::NotFound, "File not found");
313 let err = Error::Io(io_err);
314
315 assert!(StdError::source(&err).is_some());
317
318 let source = err.source().unwrap();
319 assert_eq!(source.to_string(), "File not found");
320 }
321
322 #[test]
323 fn test_custom_error() {
324 let custom_err = Box::new(io::Error::other("custom"));
325 let err = Error::Custom(custom_err);
326
327 assert!(StdError::source(&err).is_some());
328 assert!(err.to_string().contains("Custom error"));
329 }
330
331 #[test]
332 fn test_channel_error_conversions() {
333 let recv_err = std::sync::mpsc::RecvError;
334 let err: Error = recv_err.into();
335
336 match err {
337 Error::Event(_) => (),
338 _ => panic!("Expected Event error variant"),
339 }
340
341 let (tx, _rx) = std::sync::mpsc::channel::<i32>();
342 drop(_rx); let send_result = tx.send(42);
345 if let Err(send_err) = send_result {
346 let err: Error = send_err.into();
347 match err {
348 Error::Event(_) => (),
349 _ => panic!("Expected Event error variant"),
350 }
351 }
352 }
353
354 #[test]
355 fn test_with_context() {
356 let result: Result<()> = Err(Error::Terminal("Base error".to_string()));
357 let with_context = result.with_context(|| "Dynamic context".to_string());
358
359 assert!(with_context.is_err());
360 let err_str = with_context.unwrap_err().to_string();
361 assert!(err_str.contains("Dynamic context"));
362 }
363
364 #[test]
365 fn test_default_error_handler() {
366 let mut handler = DefaultErrorHandler;
367 let error = Error::Terminal("test error".to_string());
368
369 assert!(!handler.handle_error(error));
371 }
372
373 #[test]
374 fn test_context_error() {
375 let base_error = Error::Terminal("base".to_string());
376 let context_error = ContextError {
377 context: "context".to_string(),
378 source: base_error,
379 };
380
381 let display_str = context_error.to_string();
382 assert!(display_str.contains("context"));
383 assert!(display_str.contains("base"));
384
385 assert!(StdError::source(&context_error).is_some());
386 }
387
388 #[test]
389 fn test_bail_macro_with_format() {
390 fn test_fn(value: i32) -> Result<()> {
391 bail!("Value {} is invalid", value);
392 }
393
394 let err = test_fn(42).unwrap_err();
395 assert!(err.to_string().contains("Value 42 is invalid"));
396 }
397
398 #[test]
399 fn test_ensure_macro_with_format() {
400 fn test_fn(value: i32, min: i32) -> Result<i32> {
401 ensure!(value >= min, "Value {} must be >= {}", value, min);
402 Ok(value)
403 }
404
405 assert!(test_fn(10, 5).is_ok());
406
407 let err = test_fn(3, 5).unwrap_err();
408 assert!(err.to_string().contains("Value 3 must be >= 5"));
409 }
410
411 #[test]
412 fn test_panic_handler() {
413 set_panic_handler();
415
416 let _ = std::panic::take_hook();
418 }
419}