fast_rich/
traceback.rs

1//! Pretty traceback/panic rendering.
2//!
3//! Provides beautiful panic/error display similar to Rich's traceback feature.
4
5use 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/// Configuration for traceback display.
14#[derive(Debug, Clone)]
15pub struct TracebackConfig {
16    /// Show source code context
17    pub show_source: bool,
18    /// Number of context lines before/after
19    pub context_lines: usize,
20    /// Show local variables (limited in Rust)
21    pub show_locals: bool,
22    /// Panel style
23    pub border_style: BorderStyle,
24    /// Error style
25    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
40/// A formatted traceback.
41pub struct Traceback {
42    /// Error message
43    message: String,
44    /// Location information
45    location: Option<String>,
46    /// Configuration
47    config: TracebackConfig,
48}
49
50impl Traceback {
51    /// Create a new traceback from a panic info.
52    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    /// Create from an error message.
73    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    /// Set configuration.
82    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        // Error header
91        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        // Location
98        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        // Attempt to read source code if available
104        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        // Parse location (file:line:column)
118        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        // Try to read the file
127        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
168/// Install a pretty panic hook.
169///
170/// This replaces the default panic hook with one that displays
171/// nicely formatted tracebacks using rich formatting.
172pub 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            // Create and render traceback
178            let traceback = Traceback::from_panic(info);
179            let console = crate::Console::new();
180
181            // Print a blank line first
182            console.newline();
183            console.print_renderable(&traceback);
184            console.newline();
185
186            // Note: We don't call the default hook here to avoid
187            // double-printing. If you want to preserve it:
188            // default_hook(info);
189        }));
190    });
191}
192
193/// Format an error for display.
194pub fn format_error<E: std::error::Error>(error: &E) -> Traceback {
195    let mut message = error.to_string();
196
197    // Include source chain if available
198    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        // Should produce output
228        assert!(!segments.is_empty());
229
230        // Should contain error message
231        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}