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(data:"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(data:"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(data:"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/yaahc/color-eyre")
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.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 let theme = self.theme;
719 #[cfg(feature = "issue-url")]
720 let metadata = Arc::new(self.issue_metadata);
721 let panic_hook = PanicHook {
722 filters: self.filters.into(),
723 section: self.panic_section,
724 #[cfg(feature = "capture-spantrace")]
725 capture_span_trace_by_default: self.capture_span_trace_by_default,
726 display_env_section: self.display_env_section,
727 panic_message: self
728 .panic_message
729 .unwrap_or_else(|| Box::new(DefaultPanicMessage(theme))),
730 theme,
731 #[cfg(feature = "issue-url")]
732 issue_url: self.issue_url.clone(),
733 #[cfg(feature = "issue-url")]
734 issue_metadata: metadata.clone(),
735 #[cfg(feature = "issue-url")]
736 issue_filter: self.issue_filter.clone(),
737 };
738
739 let eyre_hook = EyreHook {
740 filters: panic_hook.filters.clone(),
741 #[cfg(feature = "capture-spantrace")]
742 capture_span_trace_by_default: self.capture_span_trace_by_default,
743 display_env_section: self.display_env_section,
744 #[cfg(feature = "track-caller")]
745 display_location_section: self.display_location_section,
746 theme,
747 #[cfg(feature = "issue-url")]
748 issue_url: self.issue_url,
749 #[cfg(feature = "issue-url")]
750 issue_metadata: metadata,
751 #[cfg(feature = "issue-url")]
752 issue_filter: self.issue_filter,
753 };
754
755 #[cfg(feature = "capture-spantrace")]
756 color_spantrace::set_theme(self.theme.into()).expect("could not set the provided `Theme` via `color_spantrace::set_theme` globally as another was already set");
757
758 (panic_hook, eyre_hook)
759 }
760}
761
762#[cfg(feature = "capture-spantrace")]
763impl From<Theme> for color_spantrace::Theme {
764 fn from(src: Theme) -> color_spantrace::Theme {
765 color_spantrace::Theme::new()
766 .file(src.file)
767 .line_number(src.line_number)
768 .target(src.spantrace_target)
769 .fields(src.spantrace_fields)
770 .active_line(style:src.active_line)
771 }
772}
773
774#[allow(missing_docs)]
775impl Default for HookBuilder {
776 fn default() -> Self {
777 Self::new()
778 }
779}
780
781fn default_frame_filter(frames: &mut Vec<&Frame>) {
782 let top_cutoff: usize = frames
783 .iter()
784 .rposition(|x| x.is_post_panic_code())
785 .map(|x| x + 2) // indices are 1 based
786 .unwrap_or(default:0);
787
788 let bottom_cutoff: usize = frames
789 .iter()
790 .position(|x| x.is_runtime_init_code())
791 .unwrap_or(default:frames.len());
792
793 let rng: RangeInclusive = top_cutoff..=bottom_cutoff;
794 frames.retain(|x: &&Frame| rng.contains(&x.n))
795}
796
797fn eyre_frame_filters(frames: &mut Vec<&Frame>) {
798 let filters: &[&str; 3] = &[
799 "<color_eyre::Handler as eyre::EyreHandler>::default",
800 "eyre::",
801 "color_eyre::",
802 ];
803
804 frames.retain(|frame: &&Frame| {
805 !filters.iter().any(|f: &&str| {
806 let name: &str = if let Some(name: &String) = frame.name.as_ref() {
807 name.as_str()
808 } else {
809 return true;
810 };
811
812 name.starts_with(f)
813 })
814 });
815}
816
817struct DefaultPanicMessage(Theme);
818
819impl PanicMessage for DefaultPanicMessage {
820 fn display(&self, pi: &std::panic::PanicInfo<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
821 // 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
822 let theme = &self.0;
823
824 writeln!(
825 f,
826 "{}",
827 "The application panicked (crashed).".style(theme.panic_header)
828 )?;
829
830 // Print panic message.
831 let payload = pi
832 .payload()
833 .downcast_ref::<String>()
834 .map(String::as_str)
835 .or_else(|| pi.payload().downcast_ref::<&str>().cloned())
836 .unwrap_or("<non string panic payload>");
837
838 write!(f, "Message: ")?;
839 writeln!(f, "{}", payload.style(theme.panic_message))?;
840
841 // If known, print panic location.
842 write!(f, "Location: ")?;
843 write!(f, "{}", crate::fmt::LocationSection(pi.location(), *theme))?;
844
845 Ok(())
846 }
847}
848
849/// A type representing an error report for a panic.
850pub struct PanicReport<'a> {
851 hook: &'a PanicHook,
852 panic_info: &'a std::panic::PanicInfo<'a>,
853 backtrace: Option<backtrace::Backtrace>,
854 #[cfg(feature = "capture-spantrace")]
855 span_trace: Option<tracing_error::SpanTrace>,
856}
857
858fn print_panic_info(report: &PanicReport<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
859 report.hook.panic_message.display(report.panic_info, f)?;
860
861 let v = panic_verbosity();
862 let capture_bt = v != Verbosity::Minimal;
863
864 let mut separated = f.header("\n\n");
865
866 if let Some(ref section) = report.hook.section {
867 write!(&mut separated.ready(), "{}", section)?;
868 }
869
870 #[cfg(feature = "capture-spantrace")]
871 {
872 if let Some(span_trace) = report.span_trace.as_ref() {
873 write!(
874 &mut separated.ready(),
875 "{}",
876 crate::writers::FormattedSpanTrace(span_trace)
877 )?;
878 }
879 }
880
881 if let Some(bt) = report.backtrace.as_ref() {
882 let fmted_bt = report.hook.format_backtrace(bt);
883 write!(
884 indented(&mut separated.ready()).with_format(Format::Uniform { indentation: " " }),
885 "{}",
886 fmted_bt
887 )?;
888 }
889
890 if report.hook.display_env_section {
891 let env_section = EnvSection {
892 bt_captured: &capture_bt,
893 #[cfg(feature = "capture-spantrace")]
894 span_trace: report.span_trace.as_ref(),
895 };
896
897 write!(&mut separated.ready(), "{}", env_section)?;
898 }
899
900 #[cfg(feature = "issue-url")]
901 {
902 let payload = report.panic_info.payload();
903
904 if report.hook.issue_url.is_some()
905 && (*report.hook.issue_filter)(crate::ErrorKind::NonRecoverable(payload))
906 {
907 let url = report.hook.issue_url.as_ref().unwrap();
908 let payload = payload
909 .downcast_ref::<String>()
910 .map(String::as_str)
911 .or_else(|| payload.downcast_ref::<&str>().cloned())
912 .unwrap_or("<non string panic payload>");
913
914 let issue_section = crate::section::github::IssueSection::new(url, payload)
915 .with_backtrace(report.backtrace.as_ref())
916 .with_location(report.panic_info.location())
917 .with_metadata(&**report.hook.issue_metadata);
918
919 #[cfg(feature = "capture-spantrace")]
920 let issue_section = issue_section.with_span_trace(report.span_trace.as_ref());
921
922 write!(&mut separated.ready(), "{}", issue_section)?;
923 }
924 }
925
926 Ok(())
927}
928
929impl fmt::Display for PanicReport<'_> {
930 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
931 print_panic_info(self, f)
932 }
933}
934
935/// A panic reporting hook
936pub struct PanicHook {
937 filters: Arc<[Box<FilterCallback>]>,
938 section: Option<Box<dyn Display + Send + Sync + 'static>>,
939 panic_message: Box<dyn PanicMessage>,
940 theme: Theme,
941 #[cfg(feature = "capture-spantrace")]
942 capture_span_trace_by_default: bool,
943 display_env_section: bool,
944 #[cfg(feature = "issue-url")]
945 issue_url: Option<String>,
946 #[cfg(feature = "issue-url")]
947 issue_metadata: Arc<Vec<(String, Box<dyn Display + Send + Sync + 'static>)>>,
948 #[cfg(feature = "issue-url")]
949 issue_filter: Arc<IssueFilterCallback>,
950}
951
952impl PanicHook {
953 pub(crate) fn format_backtrace<'a>(
954 &'a self,
955 trace: &'a backtrace::Backtrace,
956 ) -> BacktraceFormatter<'a> {
957 BacktraceFormatter {
958 filters: &self.filters,
959 inner: trace,
960 theme: self.theme,
961 }
962 }
963
964 #[cfg(feature = "capture-spantrace")]
965 fn spantrace_capture_enabled(&self) -> bool {
966 std::env::var("RUST_SPANTRACE")
967 .map(|val| val != "0")
968 .unwrap_or(self.capture_span_trace_by_default)
969 }
970
971 /// Install self as a global panic hook via `std::panic::set_hook`.
972 pub fn install(self) {
973 std::panic::set_hook(self.into_panic_hook());
974 }
975
976 /// Convert self into the type expected by `std::panic::set_hook`.
977 pub fn into_panic_hook(
978 self,
979 ) -> Box<dyn Fn(&std::panic::PanicInfo<'_>) + Send + Sync + 'static> {
980 Box::new(move |panic_info| {
981 eprintln!("{}", self.panic_report(panic_info));
982 })
983 }
984
985 /// Construct a panic reporter which prints it's panic report via the
986 /// `Display` trait.
987 pub fn panic_report<'a>(
988 &'a self,
989 panic_info: &'a std::panic::PanicInfo<'_>,
990 ) -> PanicReport<'a> {
991 let v = panic_verbosity();
992 let capture_bt = v != Verbosity::Minimal;
993
994 #[cfg(feature = "capture-spantrace")]
995 let span_trace = if self.spantrace_capture_enabled() {
996 Some(tracing_error::SpanTrace::capture())
997 } else {
998 None
999 };
1000
1001 let backtrace = if capture_bt {
1002 Some(backtrace::Backtrace::new())
1003 } else {
1004 None
1005 };
1006
1007 PanicReport {
1008 panic_info,
1009 #[cfg(feature = "capture-spantrace")]
1010 span_trace,
1011 backtrace,
1012 hook: self,
1013 }
1014 }
1015}
1016
1017/// An eyre reporting hook used to construct `EyreHandler`s
1018pub struct EyreHook {
1019 filters: Arc<[Box<FilterCallback>]>,
1020 #[cfg(feature = "capture-spantrace")]
1021 capture_span_trace_by_default: bool,
1022 display_env_section: bool,
1023 #[cfg(feature = "track-caller")]
1024 display_location_section: bool,
1025 theme: Theme,
1026 #[cfg(feature = "issue-url")]
1027 issue_url: Option<String>,
1028 #[cfg(feature = "issue-url")]
1029 issue_metadata: Arc<Vec<(String, Box<dyn Display + Send + Sync + 'static>)>>,
1030 #[cfg(feature = "issue-url")]
1031 issue_filter: Arc<IssueFilterCallback>,
1032}
1033
1034impl EyreHook {
1035 #[allow(unused_variables)]
1036 pub(crate) fn default(&self, error: &(dyn std::error::Error + 'static)) -> crate::Handler {
1037 let backtrace = if lib_verbosity() != Verbosity::Minimal {
1038 Some(backtrace::Backtrace::new())
1039 } else {
1040 None
1041 };
1042
1043 #[cfg(feature = "capture-spantrace")]
1044 let span_trace = if self.spantrace_capture_enabled()
1045 && crate::handler::get_deepest_spantrace(error).is_none()
1046 {
1047 Some(tracing_error::SpanTrace::capture())
1048 } else {
1049 None
1050 };
1051
1052 crate::Handler {
1053 filters: self.filters.clone(),
1054 backtrace,
1055 #[cfg(feature = "capture-spantrace")]
1056 span_trace,
1057 sections: Vec::new(),
1058 display_env_section: self.display_env_section,
1059 #[cfg(feature = "track-caller")]
1060 display_location_section: self.display_location_section,
1061 #[cfg(feature = "issue-url")]
1062 issue_url: self.issue_url.clone(),
1063 #[cfg(feature = "issue-url")]
1064 issue_metadata: self.issue_metadata.clone(),
1065 #[cfg(feature = "issue-url")]
1066 issue_filter: self.issue_filter.clone(),
1067 theme: self.theme,
1068 #[cfg(feature = "track-caller")]
1069 location: None,
1070 }
1071 }
1072
1073 #[cfg(feature = "capture-spantrace")]
1074 fn spantrace_capture_enabled(&self) -> bool {
1075 std::env::var("RUST_SPANTRACE")
1076 .map(|val| val != "0")
1077 .unwrap_or(self.capture_span_trace_by_default)
1078 }
1079
1080 /// Installs self as the global eyre handling hook via `eyre::set_hook`
1081 pub fn install(self) -> Result<(), crate::eyre::InstallError> {
1082 crate::eyre::set_hook(self.into_eyre_hook())
1083 }
1084
1085 /// Convert the self into the boxed type expected by `eyre::set_hook`.
1086 pub fn into_eyre_hook(
1087 self,
1088 ) -> Box<
1089 dyn Fn(&(dyn std::error::Error + 'static)) -> Box<dyn eyre::EyreHandler>
1090 + Send
1091 + Sync
1092 + 'static,
1093 > {
1094 Box::new(move |e| Box::new(self.default(e)))
1095 }
1096}
1097
1098pub(crate) struct BacktraceFormatter<'a> {
1099 pub(crate) filters: &'a [Box<FilterCallback>],
1100 pub(crate) inner: &'a backtrace::Backtrace,
1101 pub(crate) theme: Theme,
1102}
1103
1104impl fmt::Display for BacktraceFormatter<'_> {
1105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1106 write!(f, "{:━^80}", " BACKTRACE ")?;
1107
1108 // Collect frame info.
1109 let frames: Vec<_> = self
1110 .inner
1111 .frames()
1112 .iter()
1113 .flat_map(|frame| frame.symbols())
1114 .zip(1usize..)
1115 .map(|(sym, n)| Frame {
1116 name: sym.name().map(|x| x.to_string()),
1117 lineno: sym.lineno(),
1118 filename: sym.filename().map(|x| x.into()),
1119 n,
1120 })
1121 .collect();
1122
1123 let mut filtered_frames = frames.iter().collect();
1124 match env::var("COLORBT_SHOW_HIDDEN").ok().as_deref() {
1125 Some("1") | Some("on") | Some("y") => (),
1126 _ => {
1127 for filter in self.filters {
1128 filter(&mut filtered_frames);
1129 }
1130 }
1131 }
1132
1133 if filtered_frames.is_empty() {
1134 // TODO: Would probably look better centered.
1135 return write!(f, "\n<empty backtrace>");
1136 }
1137
1138 let mut separated = f.header("\n");
1139
1140 // Don't let filters mess with the order.
1141 filtered_frames.sort_by_key(|x| x.n);
1142
1143 let mut buf = String::new();
1144
1145 macro_rules! print_hidden {
1146 ($n:expr) => {
1147 let n = $n;
1148 buf.clear();
1149 write!(
1150 &mut buf,
1151 "{decorator} {n} frame{plural} hidden {decorator}",
1152 n = n,
1153 plural = if n == 1 { "" } else { "s" },
1154 decorator = "⋮",
1155 )
1156 .expect("writing to strings doesn't panic");
1157 write!(
1158 &mut separated.ready(),
1159 "{:^80}",
1160 buf.style(self.theme.hidden_frames)
1161 )?;
1162 };
1163 }
1164
1165 let mut last_n = 0;
1166 for frame in &filtered_frames {
1167 let frame_delta = frame.n - last_n - 1;
1168 if frame_delta != 0 {
1169 print_hidden!(frame_delta);
1170 }
1171 write!(&mut separated.ready(), "{}", StyledFrame(frame, self.theme))?;
1172 last_n = frame.n;
1173 }
1174
1175 let last_filtered_n = filtered_frames.last().unwrap().n;
1176 let last_unfiltered_n = frames.last().unwrap().n;
1177 if last_filtered_n < last_unfiltered_n {
1178 print_hidden!(last_unfiltered_n - last_filtered_n);
1179 }
1180
1181 Ok(())
1182 }
1183}
1184
1185#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
1186pub(crate) enum Verbosity {
1187 Minimal,
1188 Medium,
1189 Full,
1190}
1191
1192pub(crate) fn panic_verbosity() -> Verbosity {
1193 match env::var(key:"RUST_BACKTRACE") {
1194 Ok(s: String) if s == "full" => Verbosity::Full,
1195 Ok(s: String) if s != "0" => Verbosity::Medium,
1196 _ => Verbosity::Minimal,
1197 }
1198}
1199
1200pub(crate) fn lib_verbosity() -> Verbosity {
1201 match env::var("RUST_LIB_BACKTRACE").or_else(|_| env::var(key:"RUST_BACKTRACE")) {
1202 Ok(s: String) if s == "full" => Verbosity::Full,
1203 Ok(s: String) if s != "0" => Verbosity::Medium,
1204 _ => Verbosity::Minimal,
1205 }
1206}
1207
1208/// Callback for filtering a vector of `Frame`s
1209pub type FilterCallback = dyn Fn(&mut Vec<&Frame>) + Send + Sync + 'static;
1210
1211/// Callback for filtering issue url generation in error reports
1212#[cfg(feature = "issue-url")]
1213#[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
1214pub type IssueFilterCallback = dyn Fn(crate::ErrorKind<'_>) -> bool + Send + Sync + 'static;
1215