color_eyre/
config.rs

1//! Configuration options for customizing the behavior of the provided panic
2//! and error reporting hooks
3use crate::{
4    section::PanicMessage,
5    writers::{EnvSection, WriterExt},
6};
7use fmt::Display;
8use indenter::{indented, Format};
9use owo_colors::{style, OwoColorize, Style};
10use std::env;
11use std::fmt::Write as _;
12use std::{fmt, path::PathBuf, sync::Arc};
13
14/// A struct that represents a theme that is used by `color_eyre`
15#[derive(Debug, Copy, Clone, Default)]
16pub struct Theme {
17    pub(crate) file: Style,
18    pub(crate) line_number: Style,
19    pub(crate) spantrace_target: Style,
20    pub(crate) spantrace_fields: Style,
21    pub(crate) active_line: Style,
22    pub(crate) error: Style,
23    pub(crate) help_info_note: Style,
24    pub(crate) help_info_warning: Style,
25    pub(crate) help_info_suggestion: Style,
26    pub(crate) help_info_error: Style,
27    pub(crate) dependency_code: Style,
28    pub(crate) crate_code: Style,
29    pub(crate) code_hash: Style,
30    pub(crate) panic_header: Style,
31    pub(crate) panic_message: Style,
32    pub(crate) panic_file: Style,
33    pub(crate) panic_line_number: Style,
34    pub(crate) hidden_frames: Style,
35}
36
37macro_rules! theme_setters {
38    ($(#[$meta:meta] $name:ident),* $(,)?) => {
39        $(
40            #[$meta]
41            pub fn $name(mut self, style: Style) -> Self {
42                self.$name = style;
43                self
44            }
45        )*
46    };
47}
48
49impl Theme {
50    /// Creates a blank theme
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Returns a theme for dark backgrounds. This is the default
56    pub fn dark() -> Self {
57        Self {
58            file: style().purple(),
59            line_number: style().purple(),
60            active_line: style().white().bold(),
61            error: style().bright_red(),
62            help_info_note: style().bright_cyan(),
63            help_info_warning: style().bright_yellow(),
64            help_info_suggestion: style().bright_cyan(),
65            help_info_error: style().bright_red(),
66            dependency_code: style().green(),
67            crate_code: style().bright_red(),
68            code_hash: style().bright_black(),
69            panic_header: style().red(),
70            panic_message: style().cyan(),
71            panic_file: style().purple(),
72            panic_line_number: style().purple(),
73            hidden_frames: style().bright_cyan(),
74            spantrace_target: style().bright_red(),
75            spantrace_fields: style().bright_cyan(),
76        }
77    }
78
79    // XXX it would be great, if someone with more style optimizes the light theme below. I just fixed the biggest problems, but ideally there would be darker colors (however, the standard ANSI colors don't seem to have many dark enough colors. Maybe xterm colors or RGB colors would be better (however, again, see my comment regarding xterm colors in `color_spantrace`))
80
81    /// Returns a theme for light backgrounds
82    pub fn light() -> Self {
83        Self {
84            file: style().purple(),
85            line_number: style().purple(),
86            spantrace_target: style().red(),
87            spantrace_fields: style().blue(),
88            active_line: style().bold(),
89            error: style().red(),
90            help_info_note: style().blue(),
91            help_info_warning: style().bright_red(),
92            help_info_suggestion: style().blue(),
93            help_info_error: style().red(),
94            dependency_code: style().green(),
95            crate_code: style().red(),
96            code_hash: style().bright_black(),
97            panic_header: style().red(),
98            panic_message: style().blue(),
99            panic_file: style().purple(),
100            panic_line_number: style().purple(),
101            hidden_frames: style().blue(),
102        }
103    }
104
105    theme_setters! {
106        /// Styles printed paths
107        file,
108        /// Styles the line number of a file
109        line_number,
110        /// Styles the `color_spantrace` target (i.e. the module and function name, and so on)
111        spantrace_target,
112        /// Styles fields associated with a the `tracing::Span`.
113        spantrace_fields,
114        /// Styles the selected line of displayed code
115        active_line,
116        // XXX not sure how to describe this better (or if this is even completely correct)
117        /// Styles errors printed by `EyreHandler`
118        error,
119        /// Styles the "note" section header
120        help_info_note,
121        /// Styles the "warning" section header
122        help_info_warning,
123        /// Styles the "suggestion" section header
124        help_info_suggestion,
125        /// Styles the "error" section header
126        help_info_error,
127        /// Styles code that is not part of your crate
128        dependency_code,
129        /// Styles code that's in your crate
130        crate_code,
131        /// Styles the hash after `dependency_code` and `crate_code`
132        code_hash,
133        /// Styles the header of a panic
134        panic_header,
135        /// Styles the message of a panic
136        panic_message,
137        /// Styles paths of a panic
138        panic_file,
139        /// Styles the line numbers of a panic
140        panic_line_number,
141        /// Styles the "N frames hidden" message
142        hidden_frames,
143    }
144}
145
146/// A representation of a Frame from a Backtrace or a SpanTrace
147#[derive(Debug)]
148#[non_exhaustive]
149pub struct Frame {
150    /// Frame index
151    pub n: usize,
152    /// frame symbol name
153    pub name: Option<String>,
154    /// source line number
155    pub lineno: Option<u32>,
156    /// source file path
157    pub filename: Option<PathBuf>,
158}
159
160#[derive(Debug)]
161struct StyledFrame<'a>(&'a Frame, Theme);
162
163impl fmt::Display for StyledFrame<'_> {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        let Self(frame, theme) = self;
166
167        let is_dependency_code = frame.is_dependency_code();
168
169        // Print frame index.
170        write!(f, "{:>2}: ", frame.n)?;
171
172        // Does the function have a hash suffix?
173        // (dodging a dep on the regex crate here)
174        let name = frame.name.as_deref().unwrap_or("<unknown>");
175        let has_hash_suffix = name.len() > 19
176            && &name[name.len() - 19..name.len() - 16] == "::h"
177            && name[name.len() - 16..]
178                .chars()
179                .all(|x| x.is_ascii_hexdigit());
180
181        let hash_suffix = if has_hash_suffix {
182            &name[name.len() - 19..]
183        } else {
184            "<unknown>"
185        };
186
187        // Print function name.
188        let name = if has_hash_suffix {
189            &name[..name.len() - 19]
190        } else {
191            name
192        };
193
194        if is_dependency_code {
195            write!(f, "{}", (name).style(theme.dependency_code))?;
196        } else {
197            write!(f, "{}", (name).style(theme.crate_code))?;
198        }
199
200        write!(f, "{}", (hash_suffix).style(theme.code_hash))?;
201
202        let mut separated = f.header("\n");
203
204        // Print source location, if known.
205        let file = frame.filename.as_ref().map(|path| path.display());
206        let file: &dyn fmt::Display = if let Some(ref filename) = file {
207            filename
208        } else {
209            &"<unknown source file>"
210        };
211        let lineno = frame
212            .lineno
213            .map_or("<unknown line>".to_owned(), |x| x.to_string());
214        write!(
215            &mut separated.ready(),
216            "    at {}:{}",
217            file.style(theme.file),
218            lineno.style(theme.line_number),
219        )?;
220
221        let v = if std::thread::panicking() {
222            panic_verbosity()
223        } else {
224            lib_verbosity()
225        };
226
227        // Maybe print source.
228        if v >= Verbosity::Full {
229            write!(&mut separated.ready(), "{}", SourceSection(frame, *theme))?;
230        }
231
232        Ok(())
233    }
234}
235
236struct SourceSection<'a>(&'a Frame, Theme);
237
238impl fmt::Display for SourceSection<'_> {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        let Self(frame, theme) = self;
241
242        let (lineno, filename) = match (frame.lineno, frame.filename.as_ref()) {
243            (Some(a), Some(b)) => (a, b),
244            // Without a line number and file name, we can't sensibly proceed.
245            _ => return Ok(()),
246        };
247
248        let file = match std::fs::File::open(filename) {
249            Ok(file) => file,
250            Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
251            e @ Err(_) => e.unwrap(),
252        };
253
254        use std::fmt::Write;
255        use std::io::BufRead;
256
257        // Extract relevant lines.
258        let reader = std::io::BufReader::new(file);
259        let start_line = lineno - 2.min(lineno - 1);
260        let surrounding_src = reader.lines().skip(start_line as usize - 1).take(5);
261        let mut separated = f.header("\n");
262        let mut f = separated.in_progress();
263        for (line, cur_line_no) in surrounding_src.zip(start_line..) {
264            let line = line.unwrap();
265            if cur_line_no == lineno {
266                write!(
267                    &mut f,
268                    "{:>8} {} {}",
269                    cur_line_no.style(theme.active_line),
270                    ">".style(theme.active_line),
271                    line.style(theme.active_line),
272                )?;
273            } else {
274                write!(&mut f, "{:>8} │ {}", cur_line_no, line)?;
275            }
276            f = separated.ready();
277        }
278
279        Ok(())
280    }
281}
282
283impl Frame {
284    fn is_dependency_code(&self) -> bool {
285        const SYM_PREFIXES: &[&str] = &[
286            "std::",
287            "core::",
288            "backtrace::backtrace::",
289            "_rust_begin_unwind",
290            "color_traceback::",
291            "__rust_",
292            "___rust_",
293            "__pthread",
294            "_main",
295            "main",
296            "__scrt_common_main_seh",
297            "BaseThreadInitThunk",
298            "_start",
299            "__libc_start_main",
300            "start_thread",
301        ];
302
303        // Inspect name.
304        if let Some(ref name) = self.name {
305            if SYM_PREFIXES.iter().any(|x| name.starts_with(x)) {
306                return true;
307            }
308        }
309
310        const FILE_PREFIXES: &[&str] = &[
311            "/rustc/",
312            "src/libstd/",
313            "src/libpanic_unwind/",
314            "src/libtest/",
315        ];
316
317        // Inspect filename.
318        if let Some(ref filename) = self.filename {
319            let filename = filename.to_string_lossy();
320            if FILE_PREFIXES.iter().any(|x| filename.starts_with(x))
321                || filename.contains("/.cargo/registry/src/")
322            {
323                return true;
324            }
325        }
326
327        false
328    }
329
330    /// Heuristically determine whether a frame is likely to be a post panic
331    /// frame.
332    ///
333    /// Post panic frames are frames of a functions called after the actual panic
334    /// is already in progress and don't contain any useful information for a
335    /// reader of the backtrace.
336    fn is_post_panic_code(&self) -> bool {
337        const SYM_PREFIXES: &[&str] = &[
338            "_rust_begin_unwind",
339            "rust_begin_unwind",
340            "core::result::unwrap_failed",
341            "core::option::expect_none_failed",
342            "core::panicking::panic_fmt",
343            "color_backtrace::create_panic_handler",
344            "std::panicking::begin_panic",
345            "begin_panic_fmt",
346            "failure::backtrace::Backtrace::new",
347            "backtrace::capture",
348            "failure::error_message::err_msg",
349            "<failure::error::Error as core::convert::From<F>>::from",
350        ];
351
352        match self.name.as_ref() {
353            Some(name) => SYM_PREFIXES.iter().any(|x| name.starts_with(x)),
354            None => false,
355        }
356    }
357
358    /// Heuristically determine whether a frame is likely to be part of language
359    /// runtime.
360    fn is_runtime_init_code(&self) -> bool {
361        const SYM_PREFIXES: &[&str] = &[
362            "std::rt::lang_start::",
363            "test::run_test::run_test_inner::",
364            "std::sys_common::backtrace::__rust_begin_short_backtrace",
365        ];
366
367        let (name, file) = match (self.name.as_ref(), self.filename.as_ref()) {
368            (Some(name), Some(filename)) => (name, filename.to_string_lossy()),
369            _ => return false,
370        };
371
372        if SYM_PREFIXES.iter().any(|x| name.starts_with(x)) {
373            return true;
374        }
375
376        // For Linux, this is the best rule for skipping test init I found.
377        if name == "{{closure}}" && file == "src/libtest/lib.rs" {
378            return true;
379        }
380
381        false
382    }
383}
384
385/// Builder for customizing the behavior of the global panic and error report hooks
386pub struct HookBuilder {
387    filters: Vec<Box<FilterCallback>>,
388    capture_span_trace_by_default: bool,
389    display_env_section: bool,
390    #[cfg(feature = "track-caller")]
391    display_location_section: bool,
392    panic_section: Option<Box<dyn Display + Send + Sync + 'static>>,
393    panic_message: Option<Box<dyn PanicMessage>>,
394    theme: Theme,
395    #[cfg(feature = "issue-url")]
396    issue_url: Option<String>,
397    #[cfg(feature = "issue-url")]
398    issue_metadata: Vec<(String, Box<dyn Display + Send + Sync + 'static>)>,
399    #[cfg(feature = "issue-url")]
400    issue_filter: Arc<IssueFilterCallback>,
401}
402
403impl HookBuilder {
404    /// Construct a HookBuilder
405    ///
406    /// # Details
407    ///
408    /// By default this function calls `add_default_filters()` and
409    /// `capture_span_trace_by_default(true)`. To get a `HookBuilder` with all
410    /// features disabled by default call `HookBuilder::blank()`.
411    ///
412    /// # Example
413    ///
414    /// ```rust
415    /// use color_eyre::config::HookBuilder;
416    ///
417    /// HookBuilder::new()
418    ///     .install()
419    ///     .unwrap();
420    /// ```
421    pub fn new() -> Self {
422        Self::blank()
423            .add_default_filters()
424            .capture_span_trace_by_default(true)
425    }
426
427    /// Construct a HookBuilder with minimal features enabled
428    pub fn blank() -> Self {
429        HookBuilder {
430            filters: vec![],
431            capture_span_trace_by_default: false,
432            display_env_section: true,
433            #[cfg(feature = "track-caller")]
434            display_location_section: true,
435            panic_section: None,
436            panic_message: None,
437            theme: Theme::dark(),
438            #[cfg(feature = "issue-url")]
439            issue_url: None,
440            #[cfg(feature = "issue-url")]
441            issue_metadata: vec![],
442            #[cfg(feature = "issue-url")]
443            issue_filter: Arc::new(|_| true),
444        }
445    }
446
447    /// Set the global styles that `color_eyre` should use.
448    ///
449    /// **Tip:** You can test new styles by editing `examples/theme.rs` in the `color-eyre` repository.
450    pub fn theme(mut self, theme: Theme) -> Self {
451        self.theme = theme;
452        self
453    }
454
455    /// Add a custom section to the panic hook that will be printed
456    /// in the panic message.
457    ///
458    /// # Examples
459    ///
460    /// ```rust
461    /// color_eyre::config::HookBuilder::default()
462    ///     .panic_section("consider reporting the bug at https://github.com/eyre-rs/eyre/issues")
463    ///     .install()
464    ///     .unwrap()
465    /// ```
466    pub fn panic_section<S: Display + Send + Sync + 'static>(mut self, section: S) -> Self {
467        self.panic_section = Some(Box::new(section));
468        self
469    }
470
471    /// Overrides the main error message printing section at the start of panic
472    /// reports
473    ///
474    /// # Examples
475    ///
476    /// ```rust
477    /// use std::{panic::Location, fmt};
478    /// use color_eyre::section::PanicMessage;
479    /// use owo_colors::OwoColorize;
480    ///
481    /// struct MyPanicMessage;
482    ///
483    /// color_eyre::config::HookBuilder::default()
484    ///     .panic_message(MyPanicMessage)
485    ///     .install()
486    ///     .unwrap();
487    ///
488    /// impl PanicMessage for MyPanicMessage {
489    ///     fn display(&self, pi: &std::panic::PanicInfo<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490    ///         writeln!(f, "{}", "The application panicked (crashed).".red())?;
491    ///
492    ///         // Print panic message.
493    ///         let payload = pi
494    ///             .payload()
495    ///             .downcast_ref::<String>()
496    ///             .map(String::as_str)
497    ///             .or_else(|| pi.payload().downcast_ref::<&str>().cloned())
498    ///             .unwrap_or("<non string panic payload>");
499    ///
500    ///         write!(f, "Message:  ")?;
501    ///         writeln!(f, "{}", payload.cyan())?;
502    ///
503    ///         // If known, print panic location.
504    ///         write!(f, "Location: ")?;
505    ///         if let Some(loc) = pi.location() {
506    ///             write!(f, "{}", loc.file().purple())?;
507    ///             write!(f, ":")?;
508    ///             write!(f, "{}", loc.line().purple())?;
509    ///
510    ///             write!(f, "\n\nConsider reporting the bug at {}", custom_url(loc, payload))?;
511    ///         } else {
512    ///             write!(f, "<unknown>")?;
513    ///         }
514    ///
515    ///         Ok(())
516    ///     }
517    /// }
518    ///
519    /// fn custom_url(location: &Location<'_>, message: &str) -> impl fmt::Display {
520    ///     "todo"
521    /// }
522    /// ```
523    pub fn panic_message<S: PanicMessage>(mut self, section: S) -> Self {
524        self.panic_message = Some(Box::new(section));
525        self
526    }
527
528    /// Set an upstream github repo and enable issue reporting url generation
529    ///
530    /// # Details
531    ///
532    /// Once enabled, color-eyre will generate urls that will create customized
533    /// issues pre-populated with information about the associated error report.
534    ///
535    /// Additional information can be added to the metadata table in the
536    /// generated urls by calling `add_issue_metadata` when configuring the
537    /// HookBuilder.
538    ///
539    /// # Examples
540    ///
541    /// ```rust
542    /// color_eyre::config::HookBuilder::default()
543    ///     .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
544    ///     .install()
545    ///     .unwrap();
546    /// ```
547    #[cfg(feature = "issue-url")]
548    #[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
549    pub fn issue_url<S: ToString>(mut self, url: S) -> Self {
550        self.issue_url = Some(url.to_string());
551        self
552    }
553
554    /// Add a new entry to the metadata table in generated github issue urls
555    ///
556    /// **Note**: this metadata will be ignored if no `issue_url` is set.
557    ///
558    /// # Examples
559    ///
560    /// ```rust
561    /// color_eyre::config::HookBuilder::default()
562    ///     .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
563    ///     .add_issue_metadata("version", env!("CARGO_PKG_VERSION"))
564    ///     .install()
565    ///     .unwrap();
566    /// ```
567    #[cfg(feature = "issue-url")]
568    #[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
569    pub fn add_issue_metadata<K, V>(mut self, key: K, value: V) -> Self
570    where
571        K: Display,
572        V: Display + Send + Sync + 'static,
573    {
574        let pair = (key.to_string(), Box::new(value) as _);
575        self.issue_metadata.push(pair);
576        self
577    }
578
579    /// Configures a filter for disabling issue url generation for certain kinds of errors
580    ///
581    /// If the closure returns `true`, then the issue url will be generated.
582    ///
583    /// # Examples
584    ///
585    /// ```rust
586    /// color_eyre::config::HookBuilder::default()
587    ///     .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
588    ///     .issue_filter(|kind| match kind {
589    ///         color_eyre::ErrorKind::NonRecoverable(payload) => {
590    ///             let payload = payload
591    ///                 .downcast_ref::<String>()
592    ///                 .map(String::as_str)
593    ///                 .or_else(|| payload.downcast_ref::<&str>().cloned())
594    ///                 .unwrap_or("<non string panic payload>");
595    ///
596    ///             !payload.contains("my irrelevant error message")
597    ///         },
598    ///         color_eyre::ErrorKind::Recoverable(error) => !error.is::<std::fmt::Error>(),
599    ///     })
600    ///     .install()
601    ///     .unwrap();
602    ///
603    #[cfg(feature = "issue-url")]
604    #[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
605    pub fn issue_filter<F>(mut self, predicate: F) -> Self
606    where
607        F: Fn(crate::ErrorKind<'_>) -> bool + Send + Sync + 'static,
608    {
609        self.issue_filter = Arc::new(predicate);
610        self
611    }
612
613    /// Configures the default capture mode for `SpanTraces` in error reports and panics
614    pub fn capture_span_trace_by_default(mut self, cond: bool) -> Self {
615        self.capture_span_trace_by_default = cond;
616        self
617    }
618
619    /// Configures the enviroment varible info section and whether or not it is displayed
620    pub fn display_env_section(mut self, cond: bool) -> Self {
621        self.display_env_section = cond;
622        self
623    }
624
625    /// Configures the location info section and whether or not it is displayed.
626    ///
627    /// # Notes
628    ///
629    /// This will not disable the location section in a panic message.
630    #[cfg(feature = "track-caller")]
631    #[cfg_attr(docsrs, doc(cfg(feature = "track-caller")))]
632    pub fn display_location_section(mut self, cond: bool) -> Self {
633        self.display_location_section = cond;
634        self
635    }
636
637    /// Add a custom filter to the set of frame filters
638    ///
639    /// # Examples
640    ///
641    /// ```rust
642    /// color_eyre::config::HookBuilder::default()
643    ///     .add_frame_filter(Box::new(|frames| {
644    ///         let filters = &[
645    ///             "uninteresting_function",
646    ///         ];
647    ///
648    ///         frames.retain(|frame| {
649    ///             !filters.iter().any(|f| {
650    ///                 let name = if let Some(name) = frame.name.as_ref() {
651    ///                     name.as_str()
652    ///                 } else {
653    ///                     return true;
654    ///                 };
655    ///
656    ///                 name.starts_with(f)
657    ///             })
658    ///         });
659    ///     }))
660    ///     .install()
661    ///     .unwrap();
662    /// ```
663    pub fn add_frame_filter(mut self, filter: Box<FilterCallback>) -> Self {
664        self.filters.push(filter);
665        self
666    }
667
668    /// Install the given Hook as the global error report hook
669    pub fn install(self) -> Result<(), crate::eyre::Report> {
670        let (panic_hook, eyre_hook) = self.try_into_hooks()?;
671        eyre_hook.install()?;
672        panic_hook.install();
673        Ok(())
674    }
675
676    /// Add the default set of filters to this `HookBuilder`'s configuration
677    pub fn add_default_filters(self) -> Self {
678        self.add_frame_filter(Box::new(default_frame_filter))
679            .add_frame_filter(Box::new(eyre_frame_filters))
680    }
681
682    /// Create a `PanicHook` and `EyreHook` from this `HookBuilder`.
683    /// This can be used if you want to combine these handlers with other handlers.
684    pub fn into_hooks(self) -> (PanicHook, EyreHook) {
685        self.try_into_hooks().expect("into_hooks should only be called when no `color_spantrace` themes have previously been set")
686    }
687
688    /// Create a `PanicHook` and `EyreHook` from this `HookBuilder`.
689    /// This can be used if you want to combine these handlers with other handlers.
690    pub fn try_into_hooks(self) -> Result<(PanicHook, EyreHook), crate::eyre::Report> {
691        let theme = self.theme;
692        #[cfg(feature = "issue-url")]
693        let metadata = Arc::new(self.issue_metadata);
694        let panic_hook = PanicHook {
695            filters: self.filters.into(),
696            section: self.panic_section,
697            #[cfg(feature = "capture-spantrace")]
698            capture_span_trace_by_default: self.capture_span_trace_by_default,
699            display_env_section: self.display_env_section,
700            panic_message: self
701                .panic_message
702                .unwrap_or_else(|| Box::new(DefaultPanicMessage(theme))),
703            theme,
704            #[cfg(feature = "issue-url")]
705            issue_url: self.issue_url.clone(),
706            #[cfg(feature = "issue-url")]
707            issue_metadata: metadata.clone(),
708            #[cfg(feature = "issue-url")]
709            issue_filter: self.issue_filter.clone(),
710        };
711
712        let eyre_hook = EyreHook {
713            filters: panic_hook.filters.clone(),
714            #[cfg(feature = "capture-spantrace")]
715            capture_span_trace_by_default: self.capture_span_trace_by_default,
716            display_env_section: self.display_env_section,
717            #[cfg(feature = "track-caller")]
718            display_location_section: self.display_location_section,
719            theme,
720            #[cfg(feature = "issue-url")]
721            issue_url: self.issue_url,
722            #[cfg(feature = "issue-url")]
723            issue_metadata: metadata,
724            #[cfg(feature = "issue-url")]
725            issue_filter: self.issue_filter,
726        };
727
728        #[cfg(feature = "capture-spantrace")]
729        eyre::WrapErr::wrap_err(color_spantrace::set_theme(self.theme.into()), "could not set the provided `Theme` via `color_spantrace::set_theme` globally as another was already set")?;
730
731        Ok((panic_hook, eyre_hook))
732    }
733}
734
735#[cfg(feature = "capture-spantrace")]
736impl From<Theme> for color_spantrace::Theme {
737    fn from(src: Theme) -> color_spantrace::Theme {
738        color_spantrace::Theme::new()
739            .file(src.file)
740            .line_number(src.line_number)
741            .target(src.spantrace_target)
742            .fields(src.spantrace_fields)
743            .active_line(src.active_line)
744    }
745}
746
747#[allow(missing_docs)]
748impl Default for HookBuilder {
749    fn default() -> Self {
750        Self::new()
751    }
752}
753
754fn default_frame_filter(frames: &mut Vec<&Frame>) {
755    let top_cutoff = frames
756        .iter()
757        .rposition(|x| x.is_post_panic_code())
758        .map(|x| x + 2) // indices are 1 based
759        .unwrap_or(0);
760
761    let bottom_cutoff = frames
762        .iter()
763        .position(|x| x.is_runtime_init_code())
764        .unwrap_or(frames.len());
765
766    let rng = top_cutoff..=bottom_cutoff;
767    frames.retain(|x| rng.contains(&x.n))
768}
769
770fn eyre_frame_filters(frames: &mut Vec<&Frame>) {
771    let filters = &[
772        "<color_eyre::Handler as eyre::EyreHandler>::default",
773        "eyre::",
774        "color_eyre::",
775    ];
776
777    frames.retain(|frame| {
778        !filters.iter().any(|f| {
779            let name = if let Some(name) = frame.name.as_ref() {
780                name.as_str()
781            } else {
782                return true;
783            };
784
785            name.starts_with(f)
786        })
787    });
788}
789
790struct DefaultPanicMessage(Theme);
791
792impl PanicMessage for DefaultPanicMessage {
793    fn display(&self, pi: &std::panic::PanicInfo<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
794        // XXX is my assumption correct that this function is guaranteed to only run after `color_eyre` was setup successfully (including setting `THEME`), and that therefore the following line will never panic? Otherwise, we could return `fmt::Error`, but if the above is true, I like `unwrap` + a comment why this never fails better
795        let theme = &self.0;
796
797        writeln!(
798            f,
799            "{}",
800            "The application panicked (crashed).".style(theme.panic_header)
801        )?;
802
803        // Print panic message.
804        let payload = pi
805            .payload()
806            .downcast_ref::<String>()
807            .map(String::as_str)
808            .or_else(|| pi.payload().downcast_ref::<&str>().cloned())
809            .unwrap_or("<non string panic payload>");
810
811        write!(f, "Message:  ")?;
812        writeln!(f, "{}", payload.style(theme.panic_message))?;
813
814        // If known, print panic location.
815        write!(f, "Location: ")?;
816        write!(f, "{}", crate::fmt::LocationSection(pi.location(), *theme))?;
817
818        Ok(())
819    }
820}
821
822/// A type representing an error report for a panic.
823pub struct PanicReport<'a> {
824    hook: &'a PanicHook,
825    panic_info: &'a std::panic::PanicInfo<'a>,
826    backtrace: Option<backtrace::Backtrace>,
827    #[cfg(feature = "capture-spantrace")]
828    span_trace: Option<tracing_error::SpanTrace>,
829}
830
831fn print_panic_info(report: &PanicReport<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
832    report.hook.panic_message.display(report.panic_info, f)?;
833
834    let v = panic_verbosity();
835    let capture_bt = v != Verbosity::Minimal;
836
837    let mut separated = f.header("\n\n");
838
839    if let Some(ref section) = report.hook.section {
840        write!(&mut separated.ready(), "{}", section)?;
841    }
842
843    #[cfg(feature = "capture-spantrace")]
844    {
845        if let Some(span_trace) = report.span_trace.as_ref() {
846            write!(
847                &mut separated.ready(),
848                "{}",
849                crate::writers::FormattedSpanTrace(span_trace)
850            )?;
851        }
852    }
853
854    if let Some(bt) = report.backtrace.as_ref() {
855        let fmted_bt = report.hook.format_backtrace(bt);
856        write!(
857            indented(&mut separated.ready()).with_format(Format::Uniform { indentation: "  " }),
858            "{}",
859            fmted_bt
860        )?;
861    }
862
863    if report.hook.display_env_section {
864        let env_section = EnvSection {
865            bt_captured: &capture_bt,
866            #[cfg(feature = "capture-spantrace")]
867            span_trace: report.span_trace.as_ref(),
868        };
869
870        write!(&mut separated.ready(), "{}", env_section)?;
871    }
872
873    #[cfg(feature = "issue-url")]
874    {
875        let payload = report.panic_info.payload();
876
877        if report.hook.issue_url.is_some()
878            && (*report.hook.issue_filter)(crate::ErrorKind::NonRecoverable(payload))
879        {
880            let url = report.hook.issue_url.as_ref().unwrap();
881            let payload = payload
882                .downcast_ref::<String>()
883                .map(String::as_str)
884                .or_else(|| payload.downcast_ref::<&str>().cloned())
885                .unwrap_or("<non string panic payload>");
886
887            let issue_section = crate::section::github::IssueSection::new(url, payload)
888                .with_backtrace(report.backtrace.as_ref())
889                .with_location(report.panic_info.location())
890                .with_metadata(&report.hook.issue_metadata);
891
892            #[cfg(feature = "capture-spantrace")]
893            let issue_section = issue_section.with_span_trace(report.span_trace.as_ref());
894
895            write!(&mut separated.ready(), "{}", issue_section)?;
896        }
897    }
898
899    Ok(())
900}
901
902impl fmt::Display for PanicReport<'_> {
903    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
904        print_panic_info(self, f)
905    }
906}
907
908/// A panic reporting hook
909pub struct PanicHook {
910    filters: Arc<[Box<FilterCallback>]>,
911    section: Option<Box<dyn Display + Send + Sync + 'static>>,
912    panic_message: Box<dyn PanicMessage>,
913    theme: Theme,
914    #[cfg(feature = "capture-spantrace")]
915    capture_span_trace_by_default: bool,
916    display_env_section: bool,
917    #[cfg(feature = "issue-url")]
918    issue_url: Option<String>,
919    #[cfg(feature = "issue-url")]
920    issue_metadata: Arc<Vec<(String, Box<dyn Display + Send + Sync + 'static>)>>,
921    #[cfg(feature = "issue-url")]
922    issue_filter: Arc<IssueFilterCallback>,
923}
924
925impl PanicHook {
926    pub(crate) fn format_backtrace<'a>(
927        &'a self,
928        trace: &'a backtrace::Backtrace,
929    ) -> BacktraceFormatter<'a> {
930        BacktraceFormatter {
931            filters: &self.filters,
932            inner: trace,
933            theme: self.theme,
934        }
935    }
936
937    #[cfg(feature = "capture-spantrace")]
938    fn spantrace_capture_enabled(&self) -> bool {
939        std::env::var("RUST_SPANTRACE")
940            .map(|val| val != "0")
941            .unwrap_or(self.capture_span_trace_by_default)
942    }
943
944    /// Install self as a global panic hook via `std::panic::set_hook`.
945    pub fn install(self) {
946        std::panic::set_hook(self.into_panic_hook());
947    }
948
949    /// Convert self into the type expected by `std::panic::set_hook`.
950    pub fn into_panic_hook(
951        self,
952    ) -> Box<dyn Fn(&std::panic::PanicInfo<'_>) + Send + Sync + 'static> {
953        Box::new(move |panic_info| {
954            eprintln!("{}", self.panic_report(panic_info));
955        })
956    }
957
958    /// Construct a panic reporter which prints it's panic report via the
959    /// `Display` trait.
960    pub fn panic_report<'a>(
961        &'a self,
962        panic_info: &'a std::panic::PanicInfo<'_>,
963    ) -> PanicReport<'a> {
964        let v = panic_verbosity();
965        let capture_bt = v != Verbosity::Minimal;
966
967        #[cfg(feature = "capture-spantrace")]
968        let span_trace = if self.spantrace_capture_enabled() {
969            Some(tracing_error::SpanTrace::capture())
970        } else {
971            None
972        };
973
974        let backtrace = if capture_bt {
975            Some(backtrace::Backtrace::new())
976        } else {
977            None
978        };
979
980        PanicReport {
981            panic_info,
982            #[cfg(feature = "capture-spantrace")]
983            span_trace,
984            backtrace,
985            hook: self,
986        }
987    }
988}
989
990/// An eyre reporting hook used to construct `EyreHandler`s
991pub struct EyreHook {
992    filters: Arc<[Box<FilterCallback>]>,
993    #[cfg(feature = "capture-spantrace")]
994    capture_span_trace_by_default: bool,
995    display_env_section: bool,
996    #[cfg(feature = "track-caller")]
997    display_location_section: bool,
998    theme: Theme,
999    #[cfg(feature = "issue-url")]
1000    issue_url: Option<String>,
1001    #[cfg(feature = "issue-url")]
1002    issue_metadata: Arc<Vec<(String, Box<dyn Display + Send + Sync + 'static>)>>,
1003    #[cfg(feature = "issue-url")]
1004    issue_filter: Arc<IssueFilterCallback>,
1005}
1006
1007type HookFunc = Box<
1008    dyn Fn(&(dyn std::error::Error + 'static)) -> Box<dyn eyre::EyreHandler>
1009        + Send
1010        + Sync
1011        + 'static,
1012>;
1013
1014impl EyreHook {
1015    #[allow(unused_variables)]
1016    pub(crate) fn default(&self, error: &(dyn std::error::Error + 'static)) -> crate::Handler {
1017        let backtrace = if lib_verbosity() != Verbosity::Minimal {
1018            Some(backtrace::Backtrace::new())
1019        } else {
1020            None
1021        };
1022
1023        #[cfg(feature = "capture-spantrace")]
1024        let span_trace = if self.spantrace_capture_enabled()
1025            && crate::handler::get_deepest_spantrace(error).is_none()
1026        {
1027            Some(tracing_error::SpanTrace::capture())
1028        } else {
1029            None
1030        };
1031
1032        crate::Handler {
1033            filters: self.filters.clone(),
1034            backtrace,
1035            suppress_backtrace: false,
1036            #[cfg(feature = "capture-spantrace")]
1037            span_trace,
1038            sections: Vec::new(),
1039            display_env_section: self.display_env_section,
1040            #[cfg(feature = "track-caller")]
1041            display_location_section: self.display_location_section,
1042            #[cfg(feature = "issue-url")]
1043            issue_url: self.issue_url.clone(),
1044            #[cfg(feature = "issue-url")]
1045            issue_metadata: self.issue_metadata.clone(),
1046            #[cfg(feature = "issue-url")]
1047            issue_filter: self.issue_filter.clone(),
1048            theme: self.theme,
1049            #[cfg(feature = "track-caller")]
1050            location: None,
1051        }
1052    }
1053
1054    #[cfg(feature = "capture-spantrace")]
1055    fn spantrace_capture_enabled(&self) -> bool {
1056        std::env::var("RUST_SPANTRACE")
1057            .map(|val| val != "0")
1058            .unwrap_or(self.capture_span_trace_by_default)
1059    }
1060
1061    /// Installs self as the global eyre handling hook via `eyre::set_hook`
1062    pub fn install(self) -> Result<(), crate::eyre::InstallError> {
1063        crate::eyre::set_hook(self.into_eyre_hook())
1064    }
1065
1066    /// Convert the self into the boxed type expected by `eyre::set_hook`.
1067    pub fn into_eyre_hook(self) -> HookFunc {
1068        Box::new(move |e| Box::new(self.default(e)))
1069    }
1070}
1071
1072pub(crate) struct BacktraceFormatter<'a> {
1073    pub(crate) filters: &'a [Box<FilterCallback>],
1074    pub(crate) inner: &'a backtrace::Backtrace,
1075    pub(crate) theme: Theme,
1076}
1077
1078impl fmt::Display for BacktraceFormatter<'_> {
1079    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1080        write!(f, "{:━^80}", " BACKTRACE ")?;
1081
1082        // Collect frame info.
1083        let frames: Vec<_> = self
1084            .inner
1085            .frames()
1086            .iter()
1087            .flat_map(|frame| frame.symbols())
1088            .zip(1usize..)
1089            .map(|(sym, n)| Frame {
1090                name: sym.name().map(|x| x.to_string()),
1091                lineno: sym.lineno(),
1092                filename: sym.filename().map(|x| x.into()),
1093                n,
1094            })
1095            .collect();
1096
1097        let mut filtered_frames = frames.iter().collect();
1098        match env::var("COLORBT_SHOW_HIDDEN").ok().as_deref() {
1099            Some("1") | Some("on") | Some("y") => (),
1100            _ => {
1101                for filter in self.filters {
1102                    filter(&mut filtered_frames);
1103                }
1104            }
1105        }
1106
1107        if filtered_frames.is_empty() {
1108            // TODO: Would probably look better centered.
1109            return write!(f, "\n<empty backtrace>");
1110        }
1111
1112        let mut separated = f.header("\n");
1113
1114        // Don't let filters mess with the order.
1115        filtered_frames.sort_by_key(|x| x.n);
1116
1117        let mut buf = String::new();
1118
1119        macro_rules! print_hidden {
1120            ($n:expr) => {
1121                let n = $n;
1122                buf.clear();
1123                write!(
1124                    &mut buf,
1125                    "{decorator} {n} frame{plural} hidden {decorator}",
1126                    n = n,
1127                    plural = if n == 1 { "" } else { "s" },
1128                    decorator = "⋮",
1129                )
1130                .expect("writing to strings doesn't panic");
1131                write!(
1132                    &mut separated.ready(),
1133                    "{:^80}",
1134                    buf.style(self.theme.hidden_frames)
1135                )?;
1136            };
1137        }
1138
1139        let mut last_n = 0;
1140        for frame in &filtered_frames {
1141            let frame_delta = frame.n - last_n - 1;
1142            if frame_delta != 0 {
1143                print_hidden!(frame_delta);
1144            }
1145            write!(&mut separated.ready(), "{}", StyledFrame(frame, self.theme))?;
1146            last_n = frame.n;
1147        }
1148
1149        let last_filtered_n = filtered_frames.last().unwrap().n;
1150        let last_unfiltered_n = frames.last().unwrap().n;
1151        if last_filtered_n < last_unfiltered_n {
1152            print_hidden!(last_unfiltered_n - last_filtered_n);
1153        }
1154
1155        Ok(())
1156    }
1157}
1158
1159#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
1160pub(crate) enum Verbosity {
1161    Minimal,
1162    Medium,
1163    Full,
1164}
1165
1166pub(crate) fn panic_verbosity() -> Verbosity {
1167    match env::var("RUST_BACKTRACE") {
1168        Ok(s) if s == "full" => Verbosity::Full,
1169        Ok(s) if s != "0" => Verbosity::Medium,
1170        _ => Verbosity::Minimal,
1171    }
1172}
1173
1174pub(crate) fn lib_verbosity() -> Verbosity {
1175    match env::var("RUST_LIB_BACKTRACE").or_else(|_| env::var("RUST_BACKTRACE")) {
1176        Ok(s) if s == "full" => Verbosity::Full,
1177        Ok(s) if s != "0" => Verbosity::Medium,
1178        _ => Verbosity::Minimal,
1179    }
1180}
1181
1182/// Callback for filtering a vector of `Frame`s
1183pub type FilterCallback = dyn Fn(&mut Vec<&Frame>) + Send + Sync + 'static;
1184
1185/// Callback for filtering issue url generation in error reports
1186#[cfg(feature = "issue-url")]
1187#[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
1188pub type IssueFilterCallback = dyn Fn(crate::ErrorKind<'_>) -> bool + Send + Sync + 'static;