use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Terminal initialization failed: {message}")]
InitializationError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Terminal not initialized. Make sure you're running in a terminal environment")]
TerminalNotInitialized,
#[error("Window operation failed: {0}")]
WindowError(String),
#[error("Invalid window dimensions: {width}x{height}. Width and height must be at least 1x1")]
InvalidDimensions {
width: u16,
height: u16,
},
#[error("Position ({x}, {y}) is outside window bounds of ({width}x{height})")]
OutOfBoundsError {
x: u16,
y: u16,
width: u16,
height: u16,
},
#[error("Box [{x1}:{x2}, {y1}:{y2}] extends outside window bounds of ({width}x{height})")]
BoxOutOfBoundsError {
x1: u16,
y1: u16,
x2: u16,
y2: u16,
width: u16,
height: u16,
},
#[error("Line {y} out of bounds for window height {height}")]
LineOutOfBoundsError {
y: u16,
height: u16,
},
#[error("Buffer operation failed: {0}")]
BufferError(String),
#[error("Write at ({x},{y}) exceeds buffer size of {width}x{height}")]
BufferSizeError {
x: u16,
y: u16,
width: u16,
height: u16,
},
#[error("Widget error: {0}")]
WidgetError(String),
#[error("Widget validation failed: {message}")]
WidgetValidationError {
message: String,
hint: Option<String>,
},
#[error(
"Widget has invalid dimensions: {width}x{height}. Both width and height must be greater than 0"
)]
InvalidWidgetDimensions {
width: u16,
height: u16,
hint: Option<String>,
},
#[error("Text rendering error: {message}")]
TextRenderError {
message: String,
position: Option<(u16, u16)>,
},
#[error("Input error: {0}")]
InputError(String),
#[error("Scroll offset {offset} is out of valid range [0, {max_offset}]")]
ScrollOffsetError {
offset: i16,
max_offset: i16,
},
#[error("Color error: {0}")]
ColorError(String),
#[error("Render error: {0}")]
RenderError(String),
#[error("IO error: {0}")]
IOError(#[from] std::io::Error),
#[error("Operation timed out after {timeout_ms}ms")]
TimeoutError {
timeout_ms: u64,
},
#[error("Invalid state: {message}")]
InvalidState {
message: String,
recovery: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorSeverity {
User,
Recoverable,
Fatal,
}
impl Error {
pub fn widget_validation(message: impl Into<String>) -> Self {
Self::WidgetValidationError {
message: message.into(),
hint: None,
}
}
pub fn widget_validation_with_hint(
message: impl Into<String>,
hint: impl Into<String>,
) -> Self {
Self::WidgetValidationError {
message: message.into(),
hint: Some(hint.into()),
}
}
pub fn render(message: impl Into<String>) -> Self {
Self::RenderError(message.into())
}
pub fn buffer(message: impl Into<String>) -> Self {
Self::BufferError(message.into())
}
pub fn out_of_bounds(x: u16, y: u16, width: u16, height: u16) -> Self {
Self::OutOfBoundsError {
x,
y,
width,
height,
}
}
pub fn box_out_of_bounds(x1: u16, y1: u16, x2: u16, y2: u16, width: u16, height: u16) -> Self {
Self::BoxOutOfBoundsError {
x1,
y1,
x2,
y2,
width,
height,
}
}
pub fn invalid_widget_dimensions(width: u16, height: u16) -> Self {
Self::InvalidWidgetDimensions {
width,
height,
hint: None,
}
}
pub fn invalid_widget_dimensions_with_hint(
width: u16,
height: u16,
hint: impl Into<String>,
) -> Self {
Self::InvalidWidgetDimensions {
width,
height,
hint: Some(hint.into()),
}
}
pub fn text_render_at(message: impl Into<String>, x: u16, y: u16) -> Self {
Self::TextRenderError {
message: message.into(),
position: Some((x, y)),
}
}
pub fn text_render(message: impl Into<String>) -> Self {
Self::TextRenderError {
message: message.into(),
position: None,
}
}
pub fn scroll_offset(offset: i16, max: i16) -> Self {
Self::ScrollOffsetError {
offset,
max_offset: max,
}
}
pub fn invalid_state(message: impl Into<String>) -> Self {
Self::InvalidState {
message: message.into(),
recovery: None,
}
}
pub fn invalid_state_with_recovery(
message: impl Into<String>,
recovery: impl Into<String>,
) -> Self {
Self::InvalidState {
message: message.into(),
recovery: Some(recovery.into()),
}
}
pub fn timeout(timeout_ms: u64) -> Self {
Self::TimeoutError { timeout_ms }
}
pub fn severity(&self) -> ErrorSeverity {
match self {
Error::InvalidDimensions { .. }
| Error::InvalidWidgetDimensions { .. }
| Error::OutOfBoundsError { .. }
| Error::BoxOutOfBoundsError { .. }
| Error::LineOutOfBoundsError { .. }
| Error::BufferSizeError { .. }
| Error::ScrollOffsetError { .. }
| Error::WidgetValidationError { .. } => ErrorSeverity::User,
Error::TimeoutError { .. } | Error::IOError(_) => ErrorSeverity::Recoverable,
Error::InitializationError { .. }
| Error::TerminalNotInitialized
| Error::WindowError(_)
| Error::BufferError(_)
| Error::WidgetError(_)
| Error::TextRenderError { .. }
| Error::InputError(_)
| Error::ColorError(_)
| Error::RenderError(_)
| Error::InvalidState { .. } => ErrorSeverity::Fatal,
}
}
pub fn is_recoverable(&self) -> bool {
matches!(self.severity(), ErrorSeverity::Recoverable)
}
pub fn is_user_error(&self) -> bool {
matches!(self.severity(), ErrorSeverity::User)
}
pub fn user_message(&self) -> String {
match self {
Error::WidgetValidationError { message, hint } => {
if let Some(h) = hint {
format!("{}\n💡 Try: {}", message, h)
} else {
message.clone()
}
}
Error::InvalidWidgetDimensions {
width,
height,
hint,
} => {
let base = format!(
"Widget dimensions {}x{} are invalid. Width and height must be > 0",
width, height
);
if let Some(h) = hint {
format!("{}\n💡 {}", base, h)
} else {
base
}
}
Error::InvalidState { message, recovery } => {
if let Some(r) = recovery {
format!("{}\n💡 Try: {}", message, r)
} else {
message.clone()
}
}
other => other.to_string(),
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_severities() {
let user_err = Error::invalid_widget_dimensions(0, 0);
assert_eq!(user_err.severity(), ErrorSeverity::User);
assert!(user_err.is_user_error());
let timeout_err = Error::timeout(5000);
assert_eq!(timeout_err.severity(), ErrorSeverity::Recoverable);
assert!(timeout_err.is_recoverable());
let fatal_err = Error::RenderError("test".to_string());
assert_eq!(fatal_err.severity(), ErrorSeverity::Fatal);
}
#[test]
fn test_error_messages_with_hints() {
let err = Error::widget_validation_with_hint(
"Invalid size",
"Set width and height to values > 0",
);
let msg = err.user_message();
assert!(msg.contains("Invalid size"));
assert!(msg.contains("Set width and height"));
}
#[test]
fn test_out_of_bounds_error() {
let err = Error::out_of_bounds(100, 50, 80, 40);
let msg = err.to_string();
assert!(msg.contains("100"));
assert!(msg.contains("50"));
assert!(msg.contains("80x40"));
}
#[test]
fn test_invalid_state_with_recovery() {
let err = Error::invalid_state_with_recovery(
"Widget not initialized",
"Call initialize() before drawing",
);
let msg = err.user_message();
assert!(msg.contains("not initialized"));
assert!(msg.contains("Call initialize"));
}
#[test]
fn test_scroll_offset_error() {
let err = Error::scroll_offset(-5, 100);
let msg = err.to_string();
assert!(msg.contains("-5"));
assert!(msg.contains("100"));
}
}