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