1use crate::console::RenderContext;
6use crate::panel::{BorderStyle, Panel};
7use crate::renderable::{Renderable, Segment};
8use crate::style::{Color, Style};
9use crate::text::Text;
10use std::panic::{self, PanicHookInfo};
11use std::sync::Once;
12
13#[derive(Debug, Clone)]
15pub struct TracebackConfig {
16 pub show_source: bool,
18 pub context_lines: usize,
20 pub show_locals: bool,
22 pub border_style: BorderStyle,
24 pub error_style: Style,
26}
27
28impl Default for TracebackConfig {
29 fn default() -> Self {
30 TracebackConfig {
31 show_source: true,
32 context_lines: 3,
33 show_locals: false,
34 border_style: BorderStyle::Heavy,
35 error_style: Style::new().foreground(Color::Red).bold(),
36 }
37 }
38}
39
40pub struct Traceback {
42 message: String,
44 location: Option<String>,
46 config: TracebackConfig,
48}
49
50impl Traceback {
51 pub fn from_panic(info: &PanicHookInfo<'_>) -> Self {
53 let message = match info.payload().downcast_ref::<&str>() {
54 Some(s) => s.to_string(),
55 None => match info.payload().downcast_ref::<String>() {
56 Some(s) => s.clone(),
57 None => "Unknown panic".to_string(),
58 },
59 };
60
61 let location = info
62 .location()
63 .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()));
64
65 Traceback {
66 message,
67 location,
68 config: TracebackConfig::default(),
69 }
70 }
71
72 pub fn from_error(message: &str) -> Self {
74 Traceback {
75 message: message.to_string(),
76 location: None,
77 config: TracebackConfig::default(),
78 }
79 }
80
81 pub fn with_config(mut self, config: TracebackConfig) -> Self {
83 self.config = config;
84 self
85 }
86
87 fn build_content(&self) -> Text {
88 let mut text = Text::new();
89
90 text.push_styled("Error: ", Style::new().foreground(Color::Red).bold());
92 text.push_styled(
93 format!("{}\n", self.message),
94 Style::new().foreground(Color::White),
95 );
96
97 if let Some(ref loc) = self.location {
99 text.push_styled("\nLocation: ", Style::new().foreground(Color::Cyan));
100 text.push_styled(format!("{}\n", loc), Style::new().foreground(Color::Yellow));
101 }
102
103 if self.config.show_source {
105 if let Some(ref loc) = self.location {
106 if let Some(source_context) = self.get_source_context(loc) {
107 text.push_styled("\nSource:\n", Style::new().foreground(Color::Cyan));
108 text.push(source_context);
109 }
110 }
111 }
112
113 text
114 }
115
116 fn get_source_context(&self, location: &str) -> Option<String> {
117 let parts: Vec<&str> = location.split(':').collect();
119 if parts.len() < 2 {
120 return None;
121 }
122
123 let file_path = parts[0];
124 let line_num: usize = parts[1].parse().ok()?;
125
126 let content = std::fs::read_to_string(file_path).ok()?;
128 let lines: Vec<&str> = content.lines().collect();
129
130 if line_num == 0 || line_num > lines.len() {
131 return None;
132 }
133
134 let context = self.config.context_lines;
135 let start = line_num.saturating_sub(context + 1);
136 let end = (line_num + context).min(lines.len());
137
138 let mut result = String::new();
139 for (i, line) in lines.iter().enumerate().take(end).skip(start) {
140 let line_number = i + 1;
141 let prefix = if line_number == line_num {
142 "→ "
143 } else {
144 " "
145 };
146 result.push_str(&format!("{}{:4} │ {}\n", prefix, line_number, line));
147 }
148
149 Some(result)
150 }
151}
152
153impl Renderable for Traceback {
154 fn render(&self, context: &RenderContext) -> Vec<Segment> {
155 let content = self.build_content();
156
157 let panel = Panel::new(content)
158 .title("Traceback")
159 .border_style(self.config.border_style)
160 .style(Style::new().foreground(Color::Red));
161
162 panel.render(context)
163 }
164}
165
166static PANIC_HOOK_INSTALLED: Once = Once::new();
167
168pub fn install_panic_hook() {
173 PANIC_HOOK_INSTALLED.call_once(|| {
174 let _default_hook = panic::take_hook();
175
176 panic::set_hook(Box::new(move |info| {
177 let traceback = Traceback::from_panic(info);
179 let console = crate::Console::new();
180
181 console.newline();
183 console.print_renderable(&traceback);
184 console.newline();
185
186 }));
190 });
191}
192
193pub fn format_error<E: std::error::Error>(error: &E) -> Traceback {
195 let mut message = error.to_string();
196
197 let mut source = error.source();
199 while let Some(s) = source {
200 message.push_str(&format!("\n Caused by: {}", s));
201 source = s.source();
202 }
203
204 Traceback::from_error(&message)
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_traceback_from_error() {
213 let tb = Traceback::from_error("Something went wrong");
214 assert_eq!(tb.message, "Something went wrong");
215 assert!(tb.location.is_none());
216 }
217
218 #[test]
219 fn test_traceback_render() {
220 let tb = Traceback::from_error("Test error");
221 let context = RenderContext {
222 width: 60,
223 height: None,
224 };
225 let segments = tb.render(&context);
226
227 assert!(!segments.is_empty());
229
230 let text: String = segments.iter().map(|s| s.plain_text()).collect();
232 assert!(text.contains("Test error"));
233 }
234
235 #[test]
236 fn test_traceback_config() {
237 let config = TracebackConfig {
238 show_source: false,
239 context_lines: 5,
240 ..Default::default()
241 };
242
243 let tb = Traceback::from_error("Test").with_config(config);
244 assert!(!tb.config.show_source);
245 assert_eq!(tb.config.context_lines, 5);
246 }
247}