use crate::console::RenderContext;
use crate::panel::{BorderStyle, Panel};
use crate::renderable::{Renderable, Segment};
use crate::style::{Color, Style};
use crate::text::Text;
use std::panic::{self, PanicHookInfo};
use std::sync::Once;
#[derive(Debug, Clone)]
pub struct TracebackConfig {
pub show_source: bool,
pub context_lines: usize,
pub show_locals: bool,
pub border_style: BorderStyle,
pub error_style: Style,
}
impl Default for TracebackConfig {
fn default() -> Self {
TracebackConfig {
show_source: true,
context_lines: 3,
show_locals: false,
border_style: BorderStyle::Heavy,
error_style: Style::new().foreground(Color::Red).bold(),
}
}
}
pub struct Traceback {
message: String,
location: Option<String>,
config: TracebackConfig,
}
impl Traceback {
pub fn from_panic(info: &PanicHookInfo<'_>) -> Self {
let message = match info.payload().downcast_ref::<&str>() {
Some(s) => s.to_string(),
None => match info.payload().downcast_ref::<String>() {
Some(s) => s.clone(),
None => "Unknown panic".to_string(),
},
};
let location = info
.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()));
Traceback {
message,
location,
config: TracebackConfig::default(),
}
}
pub fn from_error(message: &str) -> Self {
Traceback {
message: message.to_string(),
location: None,
config: TracebackConfig::default(),
}
}
pub fn with_config(mut self, config: TracebackConfig) -> Self {
self.config = config;
self
}
fn build_content(&self) -> Text {
let mut text = Text::new();
text.push_styled("Error: ", Style::new().foreground(Color::Red).bold());
text.push_styled(
format!("{}\n", self.message),
Style::new().foreground(Color::White),
);
if let Some(ref loc) = self.location {
text.push_styled("\nLocation: ", Style::new().foreground(Color::Cyan));
text.push_styled(format!("{}\n", loc), Style::new().foreground(Color::Yellow));
}
if self.config.show_source {
if let Some(ref loc) = self.location {
if let Some(source_context) = self.get_source_context(loc) {
text.push_styled("\nSource:\n", Style::new().foreground(Color::Cyan));
text.push(source_context);
}
}
}
text
}
fn get_source_context(&self, location: &str) -> Option<String> {
let parts: Vec<&str> = location.split(':').collect();
if parts.len() < 2 {
return None;
}
let file_path = parts[0];
let line_num: usize = parts[1].parse().ok()?;
let content = std::fs::read_to_string(file_path).ok()?;
let lines: Vec<&str> = content.lines().collect();
if line_num == 0 || line_num > lines.len() {
return None;
}
let context = self.config.context_lines;
let start = line_num.saturating_sub(context + 1);
let end = (line_num + context).min(lines.len());
let mut result = String::new();
for (i, line) in lines.iter().enumerate().take(end).skip(start) {
let line_number = i + 1;
let prefix = if line_number == line_num {
"→ "
} else {
" "
};
result.push_str(&format!("{}{:4} │ {}\n", prefix, line_number, line));
}
Some(result)
}
}
impl Renderable for Traceback {
fn render(&self, context: &RenderContext) -> Vec<Segment> {
let content = self.build_content();
let panel = Panel::new(content)
.title("Traceback")
.border_style(self.config.border_style)
.style(Style::new().foreground(Color::Red));
panel.render(context)
}
}
static PANIC_HOOK_INSTALLED: Once = Once::new();
pub fn install_panic_hook() {
PANIC_HOOK_INSTALLED.call_once(|| {
let _default_hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
let traceback = Traceback::from_panic(info);
let console = crate::Console::new();
console.newline();
console.print_renderable(&traceback);
console.newline();
}));
});
}
pub fn format_error<E: std::error::Error>(error: &E) -> Traceback {
let mut message = error.to_string();
let mut source = error.source();
while let Some(s) = source {
message.push_str(&format!("\n Caused by: {}", s));
source = s.source();
}
Traceback::from_error(&message)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_traceback_from_error() {
let tb = Traceback::from_error("Something went wrong");
assert_eq!(tb.message, "Something went wrong");
assert!(tb.location.is_none());
}
#[test]
fn test_traceback_render() {
let tb = Traceback::from_error("Test error");
let context = RenderContext {
width: 60,
height: None,
};
let segments = tb.render(&context);
assert!(!segments.is_empty());
let text: String = segments.iter().map(|s| s.plain_text()).collect();
assert!(text.contains("Test error"));
}
#[test]
fn test_traceback_config() {
let config = TracebackConfig {
show_source: false,
context_lines: 5,
..Default::default()
};
let tb = Traceback::from_error("Test").with_config(config);
assert!(!tb.config.show_source);
assert_eq!(tb.config.context_lines, 5);
}
}