diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0d6919..9719f86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,9 +44,9 @@ jobs: features: - # default - --no-default-features - - --features track-caller + - --no-default-features --features track-caller + - --no-default-features --features auto-install - --features pyo3 - - --features auto-install - --all-features steps: - uses: actions/checkout@v1 @@ -67,8 +67,8 @@ jobs: features: - # default - --no-default-features - - --features track-caller - - --features auto-install + - --no-default-features --features track-caller + - --no-default-features --features auto-install # skip `--features pyo3` and `--all-features` because pyo3 doesn't support this msrv steps: - uses: actions/checkout@v1 @@ -136,6 +136,7 @@ jobs: with: command: clippy args: --all-targets --all-features -- -D warnings + miri: name: Miri runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 0515a7a..b58a509 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "color-eyre", "color-spantrace", "eyre" ] @@ -15,3 +16,9 @@ rust-version = "1.65.0" [workspace.dependencies] indenter = "0.3.0" once_cell = "1.18.0" +owo-colors = "3.2.0" + +[profile.dev.package.backtrace] +opt-level = 3 + + diff --git a/README.md b/README.md index d92d8fa..31e25a9 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ -eyre -==== +# eyre [![Build Status][actions-badge]][actions-url] -[![Latest Version](https://img.shields.io/crates/v/eyre.svg)](https://crates.io/crates/eyre) -[![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/eyre) +[![Latest Version][version-badge]][version-url] +[![Rust Documentation][docs-badge]][docs-url] [![Discord chat][discord-badge]][discord-url] [actions-badge]: https://github.com/eyre-rs/eyre/workflows/Continuous%20integration/badge.svg [actions-url]: https://github.com/eyre-rs/eyre/actions?query=workflow%3A%22Continuous+integration%22 +[version-badge]: https://img.shields.io/crates/v/eyre.svg +[version-url]: https://crates.io/crates/eyre +[docs-badge]: https://img.shields.io/badge/docs-latest-blue.svg +[docs-url]: https://docs.rs/eyre [discord-badge]: https://img.shields.io/discord/960645145018110012?label=eyre%20community%20discord [discord-url]: https://discord.gg/z94RqmUTKB @@ -255,10 +258,10 @@ implements `context` for options which you can import to make existing [`anyhow`]: https://github.com/dtolnay/anyhow [`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html [`stable-eyre`]: https://github.com/eyre-rs/stable-eyre -[`color-eyre`]: https://github.com/eyre-rs/color-eyre +[`color-eyre`]: https://github.com/eyre-rs/eyre/tree/master/color-eyre [`jane-eyre`]: https://github.com/yaahc/jane-eyre [`simple-eyre`]: https://github.com/eyre-rs/simple-eyre -[`color-spantrace`]: https://github.com/eyre-rs/color-spantrace +[`color-spantrace`]: https://github.com/eyre-rs/eyre/tree/master/color-spantrace [`color-backtrace`]: https://github.com/athre0z/color-backtrace [^1]: example and explanation of breakage https://github.com/eyre-rs/eyre/issues/30#issuecomment-647650361 diff --git a/color-eyre/.github/workflows/ci.yml b/color-eyre/.github/workflows/ci.yml new file mode 100644 index 0000000..23ad9a0 --- /dev/null +++ b/color-eyre/.github/workflows/ci.yml @@ -0,0 +1,135 @@ +on: + push: + branches: + - master + pull_request: {} + +name: Continuous integration + +jobs: + check: + name: Check + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + + test-features: + name: Test Features + runs-on: ubuntu-latest + strategy: + matrix: + features: + - + - --all-features + - --no-default-features + - --no-default-features --features issue-url + - --no-default-features --features capture-spantrace + - --no-default-features --features track-caller + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: test + args: ${{ matrix.features }} + + test-versions: + name: Test Versions + runs-on: ubuntu-latest + strategy: + matrix: + target: + - x86_64-unknown-linux-gnu + - wasm32-unknown-unknown + rust: + - stable + - beta + - nightly + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + target: ${{ matrix.target }} + toolchain: ${{ matrix.rust }} + override: true + - name: install test runner for wasm + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + if: ${{ matrix.target == 'wasm32-unknown-unknown' }} + - uses: actions-rs/cargo@v1 + with: + command: test + target: ${{ matrix.target }} + toolchain: ${{ matrix.rust }} + if: ${{ matrix.target != 'wasm32-unknown-unknown' }} + - name: run wasm tests + run: wasm-pack test --node + if: ${{ matrix.target == 'wasm32-unknown-unknown' }} + + test-os: + name: Test Operating Systems + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + - uses: actions-rs/cargo@v1 + with: + command: test + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + - run: rustup component add rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + - run: rustup component add clippy + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings diff --git a/color-eyre/CHANGELOG.md b/color-eyre/CHANGELOG.md new file mode 100644 index 0000000..1d859cb --- /dev/null +++ b/color-eyre/CHANGELOG.md @@ -0,0 +1,88 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + + +## [Unreleased] - ReleaseDate + +## [0.6.2] - 2022-07-11 +### Added +- Option to disable display of location section in error reports + +## [0.6.1] - 2022-02-24 +### Changed +- Collapsed backtrace help text into fewer lines + +## [0.6.0] - 2022-01-12 +### Changed +- Updated dependencies to match newest tracing versions + +## [0.5.11] - 2021-04-13 + +## [0.5.10] - 2020-12-02 +### Added +- Support custom themes + +## [0.5.9] - 2020-12-02 +### Fixed +- Bumped color-spantrace dependency version to fix a panic + +## [0.5.8] - 2020-11-23 +### Added +- Exposed internal interfaces for the panic handler so that it can be wrapped + by consumers to customize the behaviour of the panic hook. + +## [0.5.7] - 2020-11-05 +### Fixed +- Added missing `cfg`s that caused compiler errors when only enabling the + `issue-url` feature + +## [0.5.6] - 2020-10-02 +### Added +- Add support for track caller added in eyre 0.6.1 and print original + callsites of errors in all `eyre::Reports` by default + +## [0.5.5] - 2020-09-21 +### Added +- add `issue_filter` method to `HookBuilder` for disabling issue generation + based on the error encountered. + +## [0.5.4] - 2020-09-17 +### Added +- Add new "issue-url" feature for generating issue creation links in error + reports pre-populated with information about the error + +## [0.5.3] - 2020-09-14 +### Added +- add `panic_section` method to `HookBuilder` for overriding the printer for + the panic message at the start of panic reports + +## [0.5.2] - 2020-08-31 +### Added +- make it so all `Section` trait methods can be called on `Report` in + addition to the already supported usage on `Result>` +- panic_section to `HookBuilder` to add custom sections to panic reports +- display_env_section to `HookBuilder` to disable the output indicating what + environment variables can be set to manipulate the error reports +### Changed +- switched from ansi_term to owo-colors for colorizing output, allowing for + better compatibility with the Display trait + + +[Unreleased]: https://github.com/eyre-rs/color-eyre/compare/v0.6.2...HEAD +[0.6.2]: https://github.com/eyre-rs/color-eyre/compare/v0.6.1...v0.6.2 +[0.6.1]: https://github.com/eyre-rs/color-eyre/compare/v0.6.0...v0.6.1 +[0.6.0]: https://github.com/eyre-rs/color-eyre/compare/v0.5.11...v0.6.0 +[0.5.11]: https://github.com/eyre-rs/color-eyre/compare/v0.5.10...v0.5.11 +[0.5.10]: https://github.com/eyre-rs/color-eyre/compare/v0.5.9...v0.5.10 +[0.5.9]: https://github.com/eyre-rs/color-eyre/compare/v0.5.8...v0.5.9 +[0.5.8]: https://github.com/eyre-rs/color-eyre/compare/v0.5.7...v0.5.8 +[0.5.7]: https://github.com/eyre-rs/color-eyre/compare/v0.5.6...v0.5.7 +[0.5.6]: https://github.com/eyre-rs/color-eyre/compare/v0.5.5...v0.5.6 +[0.5.5]: https://github.com/eyre-rs/color-eyre/compare/v0.5.4...v0.5.5 +[0.5.4]: https://github.com/eyre-rs/color-eyre/compare/v0.5.3...v0.5.4 +[0.5.3]: https://github.com/eyre-rs/color-eyre/compare/v0.5.2...v0.5.3 +[0.5.2]: https://github.com/eyre-rs/color-eyre/releases/tag/v0.5.2 diff --git a/color-eyre/Cargo.toml b/color-eyre/Cargo.toml new file mode 100644 index 0000000..9f325ec --- /dev/null +++ b/color-eyre/Cargo.toml @@ -0,0 +1,83 @@ +[package] +name = "color-eyre" +version = "0.6.2" +description = "An error report handler for panics and eyre::Reports for colorful, consistent, and well formatted error reports for all kinds of errors." +documentation = "https://docs.rs/color-eyre" + +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +readme = { workspace = true } +rust-version = { workspace = true } + +[features] +default = ["track-caller", "capture-spantrace"] +capture-spantrace = ["tracing-error", "color-spantrace"] +issue-url = ["url"] +track-caller = [] + +[dependencies] +eyre = "0.6.1" +tracing-error = { version = "0.2.0", optional = true } +backtrace = { version = "0.3.48", features = ["gimli-symbolize"] } +indenter = { workspace = true } +owo-colors = { workspace = true } +color-spantrace = { version = "0.2", optional = true } +once_cell = { workspace = true } +url = { version = "2.1.1", optional = true } + +[dev-dependencies] +tracing-subscriber = { version = "0.3.0", features = ["env-filter"] } +tracing = "0.1.13" +pretty_assertions = "1.0.0" +thiserror = "1.0.19" +ansi-parser = "0.8.0" + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = "0.3.15" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[package.metadata.release] +dev-version = false + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "Unreleased" +replace="{{version}}" + +[[package.metadata.release.pre-release-replacements]] +file = "src/lib.rs" +search = "#!\\[doc\\(html_root_url.*" +replace = "#![doc(html_root_url = \"https://docs.rs/{{crate_name}}/{{version}}\")]" +exactly = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "\\.\\.\\.HEAD" +replace="...{{tag_name}}" +exactly = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "ReleaseDate" +replace="{{date}}" + +[[package.metadata.release.pre-release-replacements]] +file="CHANGELOG.md" +search="" +replace="\n\n## [Unreleased] - ReleaseDate" +exactly=1 + +[[package.metadata.release.pre-release-replacements]] +file="CHANGELOG.md" +search="" +replace="\n[Unreleased]: https://github.com/eyre-rs/{{crate_name}}/compare/{{tag_name}}...HEAD" +exactly=1 + +[[example]] +name = "color-eyre-usage" +path = "examples/usage.rs" diff --git a/color-eyre/LICENSE-APACHE b/color-eyre/LICENSE-APACHE new file mode 120000 index 0000000..965b606 --- /dev/null +++ b/color-eyre/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/color-eyre/LICENSE-MIT b/color-eyre/LICENSE-MIT new file mode 120000 index 0000000..76219eb --- /dev/null +++ b/color-eyre/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/color-eyre/README.md b/color-eyre/README.md new file mode 100644 index 0000000..e991641 --- /dev/null +++ b/color-eyre/README.md @@ -0,0 +1,231 @@ +# color-eyre + +[![Build Status][actions-badge]][actions-url] +[![Latest Version][version-badge]][version-url] +[![Rust Documentation][docs-badge]][docs-url] + +[actions-badge]: https://github.com/eyre-rs/eyre/workflows/Continuous%20integration/badge.svg +[actions-url]: https://github.com/eyre-rs/eyre/actions?query=workflow%3A%22Continuous+integration%22 +[version-badge]: https://img.shields.io/crates/v/color-eyre.svg +[version-url]: https://crates.io/crates/color-eyre +[docs-badge]: https://img.shields.io/badge/docs-latest-blue.svg +[docs-url]: https://docs.rs/color-eyre + +An error report handler for panics and the [`eyre`] crate for colorful, consistent, and well +formatted error reports for all kinds of errors. + +## TLDR + +`color_eyre` helps you build error reports that look like this: + +![custom section example](./pictures/custom_section.png) + +## Setup + +Add the following to your toml file: + +```toml +[dependencies] +color-eyre = "0.6" +``` + +And install the panic and error report handlers: + +```rust +use color_eyre::eyre::Result; + +fn main() -> Result<()> { + color_eyre::install()?; + + // ... + # Ok(()) +} +``` + +### Disabling tracing support + +If you don't plan on using `tracing_error` and `SpanTrace` you can disable the +tracing integration to cut down on unused dependencies: + +```toml +[dependencies] +color-eyre = { version = "0.6", default-features = false } +``` + +### Disabling SpanTrace capture by default + +color-eyre defaults to capturing span traces. This is because `SpanTrace` +capture is significantly cheaper than `Backtrace` capture. However, like +backtraces, span traces are most useful for debugging applications, and it's +not uncommon to want to disable span trace capture by default to keep noise out +developer. + +To disable span trace capture you must explicitly set one of the env variables +that regulate `SpanTrace` capture to `"0"`: + +```rust +if std::env::var("RUST_SPANTRACE").is_err() { + std::env::set_var("RUST_SPANTRACE", "0"); +} +``` + +### Improving perf on debug builds + +In debug mode `color-eyre` behaves noticably worse than `eyre`. This is caused +by the fact that `eyre` uses `std::backtrace::Backtrace` instead of +`backtrace::Backtrace`. The std version of backtrace is precompiled with +optimizations, this means that whether or not you're in debug mode doesn't +matter much for how expensive backtrace capture is, it will always be in the +10s of milliseconds to capture. A debug version of `backtrace::Backtrace` +however isn't so lucky, and can take an order of magnitude more time to capture +a backtrace compared to its std counterpart. + +Cargo [profile +overrides](https://doc.rust-lang.org/cargo/reference/profiles.html#overrides) +can be used to mitigate this problem. By configuring your project to always +build `backtrace` with optimizations you should get the same performance from +`color-eyre` that you're used to with `eyre`. To do so add the following to +your Cargo.toml: + +```toml +[profile.dev.package.backtrace] +opt-level = 3 +``` + +## Features + +### Multiple report format verbosity levels + +`color-eyre` provides 3 different report formats for how it formats the captured `SpanTrace` +and `Backtrace`, minimal, short, and full. Take the below snippets of the output produced by [`examples/usage.rs`]: + +--- + +Running `cargo run --example usage` without `RUST_LIB_BACKTRACE` set will produce a minimal +report like this: + +![minimal report format](./pictures/minimal.png) + +
+ +Running `RUST_LIB_BACKTRACE=1 cargo run --example usage` tells `color-eyre` to use the short +format, which additionally capture a [`backtrace::Backtrace`]: + +![short report format](./pictures/short.png) + +
+ +Finally, running `RUST_LIB_BACKTRACE=full cargo run --example usage` tells `color-eyre` to use +the full format, which in addition to the above will attempt to include source lines where the +error originated from, assuming it can find them on the disk. + +![full report format](./pictures/full.png) + +### Custom `Section`s for error reports via [`Section`] trait + +The `section` module provides helpers for adding extra sections to error +reports. Sections are disinct from error messages and are displayed +independently from the chain of errors. Take this example of adding sections +to contain `stderr` and `stdout` from a failed command, taken from +[`examples/custom_section.rs`]: + +```rust +use color_eyre::{eyre::eyre, SectionExt, Section, eyre::Report}; +use std::process::Command; +use tracing::instrument; + +trait Output { + fn output2(&mut self) -> Result; +} + +impl Output for Command { + #[instrument] + fn output2(&mut self) -> Result { + let output = self.output()?; + + let stdout = String::from_utf8_lossy(&output.stdout); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + Err(eyre!("cmd exited with non-zero status code")) + .with_section(move || stdout.trim().to_string().header("Stdout:")) + .with_section(move || stderr.trim().to_string().header("Stderr:")) + } else { + Ok(stdout.into()) + } + } +} +``` + +--- + +Here we have an function that, if the command exits unsuccessfully, creates a +report indicating the failure and attaches two sections, one for `stdout` and +one for `stderr`. + +Running `cargo run --example custom_section` shows us how these sections are +included in the output: + +![custom section example](./pictures/custom_section.png) + +Only the `Stderr:` section actually gets included. The `cat` command fails, +so stdout ends up being empty and is skipped in the final report. This gives +us a short and concise error report indicating exactly what was attempted and +how it failed. + +### Aggregating multiple errors into one report + +It's not uncommon for programs like batched task runners or parsers to want +to return an error with multiple sources. The current version of the error +trait does not support this use case very well, though there is [work being +done](https://github.com/rust-lang/rfcs/pull/2895) to improve this. + +For now however one way to work around this is to compose errors outside the +error trait. `color-eyre` supports such composition in its error reports via +the `Section` trait. + +For an example of how to aggregate errors check out [`examples/multiple_errors.rs`]. + +### Custom configuration for `color-backtrace` for setting custom filters and more + +The pretty printing for backtraces and span traces isn't actually provided by +`color-eyre`, but instead comes from its dependencies [`color-backtrace`] and +[`color-spantrace`]. `color-backtrace` in particular has many more features +than are exported by `color-eyre`, such as customized color schemes, panic +hooks, and custom frame filters. The custom frame filters are particularly +useful when combined with `color-eyre`, so to enable their usage we provide +the `install` fn for setting up a custom `BacktracePrinter` with custom +filters installed. + +For an example of how to setup custom filters, check out [`examples/custom_filter.rs`]. + +[`eyre`]: https://docs.rs/eyre +[`tracing-error`]: https://docs.rs/tracing-error +[`color-backtrace`]: https://docs.rs/color-backtrace +[`eyre::EyreHandler`]: https://docs.rs/eyre/*/eyre/trait.EyreHandler.html +[`backtrace::Backtrace`]: https://docs.rs/backtrace/*/backtrace/struct.Backtrace.html +[`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html +[`color-spantrace`]: https://github.com/eyre-rs/eyre/tree/master/color-spantrace +[`Section`]: https://docs.rs/color-eyre/*/color_eyre/section/trait.Section.html +[`eyre::Report`]: https://docs.rs/eyre/*/eyre/struct.Report.html +[`eyre::Result`]: https://docs.rs/eyre/*/eyre/type.Result.html +[`Handler`]: https://docs.rs/color-eyre/*/color_eyre/struct.Handler.html +[`examples/usage.rs`]: https://github.com/eyre-rs/color-eyre/blob/master/examples/usage.rs +[`examples/custom_filter.rs`]: https://github.com/eyre-rs/eyre/tree/master/color-eyre/blob/master/examples/custom_filter.rs +[`examples/custom_section.rs`]: https://github.com/eyre-rs/eyre/tree/master/color-eyre/blob/master/examples/custom_section.rs +[`examples/multiple_errors.rs`]: https://github.com/eyre-rs/eyre/tree/master/color-eyre/blob/master/examples/multiple_errors.rs + +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + diff --git a/color-eyre/examples/custom_filter.rs b/color-eyre/examples/custom_filter.rs new file mode 100644 index 0000000..5776d38 --- /dev/null +++ b/color-eyre/examples/custom_filter.rs @@ -0,0 +1,61 @@ +use color_eyre::{eyre::Report, eyre::WrapErr, Section}; +use tracing::{info, instrument}; + +#[instrument] +fn main() -> Result<(), Report> { + std::env::set_var("RUST_BACKTRACE", "1"); + #[cfg(feature = "capture-spantrace")] + install_tracing(); + + color_eyre::config::HookBuilder::default() + .add_frame_filter(Box::new(|frames| { + let filters = &["custom_filter::main"]; + + frames.retain(|frame| { + !filters.iter().any(|f| { + let name = if let Some(name) = frame.name.as_ref() { + name.as_str() + } else { + return true; + }; + + name.starts_with(f) + }) + }); + })) + .install() + .unwrap(); + + read_config() +} + +#[cfg(feature = "capture-spantrace")] +fn install_tracing() { + use tracing_error::ErrorLayer; + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; + + let fmt_layer = fmt::layer().with_target(false); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::default()) + .init(); +} + +#[instrument] +fn read_file(path: &str) -> Result<(), Report> { + info!("Reading file"); + Ok(std::fs::read_to_string(path).map(drop)?) +} + +#[instrument] +fn read_config() -> Result<(), Report> { + read_file("fake_file") + .wrap_err("Unable to read config") + .suggestion("try using a file that exists next time") +} diff --git a/color-eyre/examples/custom_section.rs b/color-eyre/examples/custom_section.rs new file mode 100644 index 0000000..352846d --- /dev/null +++ b/color-eyre/examples/custom_section.rs @@ -0,0 +1,68 @@ +use color_eyre::{ + eyre::Report, + eyre::{eyre, WrapErr}, + Section, SectionExt, +}; +use std::process::Command; +use tracing::instrument; + +trait Output { + fn output2(&mut self) -> Result; +} + +impl Output for Command { + #[instrument] + fn output2(&mut self) -> Result { + let output = self.output()?; + + let stdout = String::from_utf8_lossy(&output.stdout); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + Err(eyre!("cmd exited with non-zero status code")) + .with_section(move || stdout.trim().to_string().header("Stdout:")) + .with_section(move || stderr.trim().to_string().header("Stderr:")) + } else { + Ok(stdout.into()) + } + } +} + +#[instrument] +fn main() -> Result<(), Report> { + #[cfg(feature = "capture-spantrace")] + install_tracing(); + color_eyre::install()?; + + read_config().map(drop) +} + +#[cfg(feature = "capture-spantrace")] +fn install_tracing() { + use tracing_error::ErrorLayer; + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; + + let fmt_layer = fmt::layer().with_target(false); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::default()) + .init(); +} + +#[instrument] +fn read_file(path: &str) -> Result { + Command::new("cat").arg(path).output2() +} + +#[instrument] +fn read_config() -> Result { + read_file("fake_file") + .wrap_err("Unable to read config") + .suggestion("try using a file that exists next time") +} diff --git a/color-eyre/examples/debug_perf.rs b/color-eyre/examples/debug_perf.rs new file mode 100644 index 0000000..fbf03d6 --- /dev/null +++ b/color-eyre/examples/debug_perf.rs @@ -0,0 +1,56 @@ +//! example for manually testing the perf of color-eyre in debug vs release + +use color_eyre::{ + eyre::Report, + eyre::{eyre, WrapErr}, + Section, +}; +use tracing::instrument; + +fn main() -> Result<(), Report> { + #[cfg(feature = "capture-spantrace")] + install_tracing(); + color_eyre::install()?; + + time_report(); + + Ok(()) +} + +#[instrument] +fn time_report() { + time_report_inner() +} + +#[instrument] +fn time_report_inner() { + let start = std::time::Instant::now(); + let report = Err::<(), Report>(eyre!("fake error")) + .wrap_err("wrapped error") + .suggestion("try using a file that exists next time") + .unwrap_err(); + + println!("Error: {:?}", report); + drop(report); + let end = std::time::Instant::now(); + + dbg!(end - start); +} + +#[cfg(feature = "capture-spantrace")] +fn install_tracing() { + use tracing_error::ErrorLayer; + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; + + let fmt_layer = fmt::layer().with_target(false); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::default()) + .init(); +} diff --git a/color-eyre/examples/github_issue.rs b/color-eyre/examples/github_issue.rs new file mode 100644 index 0000000..a5e223b --- /dev/null +++ b/color-eyre/examples/github_issue.rs @@ -0,0 +1,74 @@ +#![allow(dead_code, unused_imports)] +use color_eyre::eyre; +use eyre::{Report, Result}; +use tracing::instrument; + +#[instrument] +#[cfg(feature = "issue-url")] +fn main() -> Result<(), Report> { + #[cfg(feature = "capture-spantrace")] + install_tracing(); + + color_eyre::config::HookBuilder::default() + .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new")) + .add_issue_metadata("version", env!("CARGO_PKG_VERSION")) + .issue_filter(|kind| match kind { + color_eyre::ErrorKind::NonRecoverable(_) => false, + color_eyre::ErrorKind::Recoverable(_) => true, + }) + .install()?; + + let report = read_config().unwrap_err(); + eprintln!("Error: {:?}", report); + + read_config2(); + + Ok(()) +} + +#[cfg(not(feature = "issue-url"))] +fn main() { + unimplemented!("this example requires the \"issue-url\" feature") +} + +#[cfg(feature = "capture-spantrace")] +fn install_tracing() { + use tracing_error::ErrorLayer; + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; + + let fmt_layer = fmt::layer().with_target(false); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::default()) + .init(); +} + +#[instrument] +fn read_file(path: &str) -> Result { + Ok(std::fs::read_to_string(path)?) +} + +#[instrument] +fn read_config() -> Result<()> { + read_file("fake_file")?; + + Ok(()) +} + +#[instrument] +fn read_file2(path: &str) { + if let Err(e) = std::fs::read_to_string(path) { + panic!("{}", e); + } +} + +#[instrument] +fn read_config2() { + read_file2("fake_file") +} diff --git a/color-eyre/examples/multiple_errors.rs b/color-eyre/examples/multiple_errors.rs new file mode 100644 index 0000000..f1e65a9 --- /dev/null +++ b/color-eyre/examples/multiple_errors.rs @@ -0,0 +1,55 @@ +use color_eyre::{eyre::eyre, eyre::Report, Section}; +use thiserror::Error; + +fn main() -> Result<(), Report> { + color_eyre::install()?; + let errors = get_errors(); + join_errors(errors) +} + +fn join_errors(results: Vec>) -> Result<(), Report> { + if results.iter().all(|r| r.is_ok()) { + return Ok(()); + } + + let err = results + .into_iter() + .filter(Result::is_err) + .map(Result::unwrap_err) + .fold(eyre!("encountered multiple errors"), |report, e| { + report.error(e) + }); + + Err(err) +} + +/// Helper function to generate errors +fn get_errors() -> Vec> { + vec![ + Err(SourceError { + source: StrError("The task you ran encountered an error"), + msg: "The task could not be completed", + }), + Err(SourceError { + source: StrError("The machine you're connecting to is actively on fire"), + msg: "The machine is unreachable", + }), + Err(SourceError { + source: StrError("The file you're parsing is literally written in c++ instead of rust, what the hell"), + msg: "The file could not be parsed", + }), + ] +} + +/// Arbitrary error type for demonstration purposes +#[derive(Debug, Error)] +#[error("{0}")] +struct StrError(&'static str); + +/// Arbitrary error type for demonstration purposes with a source error +#[derive(Debug, Error)] +#[error("{msg}")] +struct SourceError { + msg: &'static str, + source: StrError, +} diff --git a/color-eyre/examples/panic_compose.rs b/color-eyre/examples/panic_compose.rs new file mode 100644 index 0000000..55113c7 --- /dev/null +++ b/color-eyre/examples/panic_compose.rs @@ -0,0 +1,50 @@ +use color_eyre::eyre::Report; +use tracing::instrument; + +#[instrument] +fn main() -> Result<(), Report> { + #[cfg(feature = "capture-spantrace")] + install_tracing(); + + let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default().into_hooks(); + + eyre_hook.install()?; + + std::panic::set_hook(Box::new(move |pi| { + tracing::error!("{}", panic_hook.panic_report(pi)); + })); + + read_config(); + + Ok(()) +} + +#[cfg(feature = "capture-spantrace")] +fn install_tracing() { + use tracing_error::ErrorLayer; + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; + + let fmt_layer = fmt::layer().with_target(false); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::default()) + .init(); +} + +#[instrument] +fn read_file(path: &str) { + if let Err(e) = std::fs::read_to_string(path) { + panic!("{}", e); + } +} + +#[instrument] +fn read_config() { + read_file("fake_file") +} diff --git a/color-eyre/examples/panic_hook.rs b/color-eyre/examples/panic_hook.rs new file mode 100644 index 0000000..23233de --- /dev/null +++ b/color-eyre/examples/panic_hook.rs @@ -0,0 +1,46 @@ +use color_eyre::eyre::Report; +use tracing::instrument; + +#[instrument] +fn main() -> Result<(), Report> { + #[cfg(feature = "capture-spantrace")] + install_tracing(); + + color_eyre::config::HookBuilder::default() + .panic_section("consider reporting the bug on github") + .install()?; + + read_config(); + + Ok(()) +} + +#[cfg(feature = "capture-spantrace")] +fn install_tracing() { + use tracing_error::ErrorLayer; + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; + + let fmt_layer = fmt::layer().with_target(false); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::default()) + .init(); +} + +#[instrument] +fn read_file(path: &str) { + if let Err(e) = std::fs::read_to_string(path) { + panic!("{}", e); + } +} + +#[instrument] +fn read_config() { + read_file("fake_file") +} diff --git a/color-eyre/examples/theme.rs b/color-eyre/examples/theme.rs new file mode 100644 index 0000000..0d8b6ae --- /dev/null +++ b/color-eyre/examples/theme.rs @@ -0,0 +1,62 @@ +use color_eyre::{config::Theme, eyre::Report, owo_colors::style, Section}; + +/// To experiment with theme values, edit `theme()` below and execute `cargo run --example theme` +fn theme() -> Theme { + Theme::dark() + // ^ use `new` to derive from a blank theme, or `light` to derive from a light theme. + // Now configure your theme (see the docs for all options): + .line_number(style().blue()) + .help_info_suggestion(style().red()) +} + +#[derive(Debug, thiserror::Error)] +#[error("{0}")] +struct TestError(&'static str); + +#[tracing::instrument] +fn get_error(msg: &'static str) -> Report { + fn create_report(msg: &'static str) -> Report { + Report::msg(msg) + .note("note") + .warning("warning") + .suggestion("suggestion") + .error(TestError("error")) + } + + // Using `Option` to add dependency code. + // See https://github.com/eyre-rs/color-eyre/blob/4ddaeb2126ed8b14e4e6aa03d7eef49eb8561cf0/src/config.rs#L56 + None::> + .ok_or_else(|| create_report(msg)) + .unwrap_err() +} + +fn main() { + setup(); + println!("{:?}", get_error("test")); +} + +fn setup() { + std::env::set_var("RUST_LIB_BACKTRACE", "full"); + + #[cfg(feature = "capture-spantrace")] + { + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; + + let fmt_layer = fmt::layer().with_target(false); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(tracing_error::ErrorLayer::default()) + .init(); + } + + color_eyre::config::HookBuilder::new() + .theme(theme()) + .install() + .expect("Failed to install `color_eyre`"); +} diff --git a/color-eyre/examples/theme_test_helper.rs b/color-eyre/examples/theme_test_helper.rs new file mode 100644 index 0000000..395f4d0 --- /dev/null +++ b/color-eyre/examples/theme_test_helper.rs @@ -0,0 +1,62 @@ +//! Nothing interesting here. This is just a small helper used in a test. + +//! This needs to be an "example" until binaries can declare separate dependencies (see https://github.com/rust-lang/cargo/issues/1982) + +//! See "tests/theme.rs" for more information. + +use color_eyre::{eyre::Report, Section}; + +#[rustfmt::skip] +#[derive(Debug, thiserror::Error)] +#[error("{0}")] +struct TestError(&'static str); + +#[rustfmt::skip] +fn get_error(msg: &'static str) -> Report { + + #[rustfmt::skip] + #[inline(never)] + fn create_report(msg: &'static str) -> Report { + Report::msg(msg) + .note("note") + .warning("warning") + .suggestion("suggestion") + .error(TestError("error")) + } + + // Getting regular `Report`. Using `Option` to trigger `is_dependency_code`. + // See https://github.com/eyre-rs/color-eyre/blob/4ddaeb2126ed8b14e4e6aa03d7eef49eb8561cf0/src/config.rs#L56 + None::>.ok_or_else(|| create_report(msg)).unwrap_err() +} + +fn main() { + setup(); + let msg = "test"; + let span = tracing::info_span!("get_error", msg); + let _guard = span.enter(); + let error = get_error(msg); + std::panic::panic_any(error) +} + +fn setup() { + std::env::set_var("RUST_BACKTRACE", "1"); + + #[cfg(feature = "capture-spantrace")] + { + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; + + let fmt_layer = fmt::layer().with_target(false); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(tracing_error::ErrorLayer::default()) + .init(); + } + + color_eyre::install().expect("Failed to install `color_eyre`"); +} diff --git a/color-eyre/examples/usage.rs b/color-eyre/examples/usage.rs new file mode 100644 index 0000000..21db0fb --- /dev/null +++ b/color-eyre/examples/usage.rs @@ -0,0 +1,43 @@ +use color_eyre::{eyre::Report, eyre::WrapErr, Section}; +use tracing::{info, instrument}; + +#[instrument] +fn main() -> Result<(), Report> { + #[cfg(feature = "capture-spantrace")] + install_tracing(); + + color_eyre::install()?; + + read_config() +} + +#[cfg(feature = "capture-spantrace")] +fn install_tracing() { + use tracing_error::ErrorLayer; + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; + + let fmt_layer = fmt::layer().with_target(false); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::default()) + .init(); +} + +#[instrument] +fn read_file(path: &str) -> Result<(), Report> { + info!("Reading file"); + Ok(std::fs::read_to_string(path).map(drop)?) +} + +#[instrument] +fn read_config() -> Result<(), Report> { + read_file("fake_file") + .wrap_err("Unable to read config") + .suggestion("try using a file that exists next time") +} diff --git a/color-eyre/pictures/custom_section.png b/color-eyre/pictures/custom_section.png new file mode 100644 index 0000000..4a795f5 Binary files /dev/null and b/color-eyre/pictures/custom_section.png differ diff --git a/color-eyre/pictures/full.png b/color-eyre/pictures/full.png new file mode 100644 index 0000000..b4bc8c9 Binary files /dev/null and b/color-eyre/pictures/full.png differ diff --git a/color-eyre/pictures/minimal.png b/color-eyre/pictures/minimal.png new file mode 100644 index 0000000..02f50fa Binary files /dev/null and b/color-eyre/pictures/minimal.png differ diff --git a/color-eyre/pictures/short.png b/color-eyre/pictures/short.png new file mode 100644 index 0000000..b3a88e2 Binary files /dev/null and b/color-eyre/pictures/short.png differ diff --git a/color-eyre/scripts/default.nix b/color-eyre/scripts/default.nix new file mode 100644 index 0000000..3db90c4 --- /dev/null +++ b/color-eyre/scripts/default.nix @@ -0,0 +1,13 @@ +{ pkgs ? import { } }: +let + inherit (pkgs) stdenv lib python38; + + py = python38.withPackages (pypkgs: with pypkgs; [ beautifulsoup4 ]); + +in stdenv.mkDerivation { + pname = "color-eyre-scripts"; + version = "0.0.0"; + + src = ./.; + buildInputs = [ py ]; +} diff --git a/color-eyre/scripts/fix_html_examples.py b/color-eyre/scripts/fix_html_examples.py new file mode 100755 index 0000000..daf904e --- /dev/null +++ b/color-eyre/scripts/fix_html_examples.py @@ -0,0 +1,126 @@ +#! /usr/bin/env python3.8 + +from __future__ import annotations + +import argparse +from argparse import FileType, ArgumentParser +import enum +import sys + +from bs4 import BeautifulSoup, Tag + + +class LineType(enum.Enum): + OUTER_DOC = enum.auto() + INNER_DOC = enum.auto() + SOURCE = enum.auto() + + @classmethod + def from_line(cls, line: str) -> (LineType, str): + if line.startswith("//!"): + return (cls.OUTER_DOC, line[len("//!") :]) + elif line.startswith("///"): + return (cls.INNER_DOC, line[len("///") :]) + else: + return (cls.SOURCE, line) + + def prefix(self) -> str: + if self == LineType.OUTER_DOC: + return "//!" + elif self == LineType.INNER_DOC: + return "///" + else: + return "" + + +def fix_gnome_html(fh: file) -> str: + """Tweaks for fixing "Copy as HTML" output from gnome-terminal + + Reads source from a Rust file. + """ + + anything_changed = False + line_type = LineType.SOURCE + + # Lines of current HTML
 chunk
+    pre_chunk = []
+    # Lines of processed file
+    ret = []
+
+    for (line_type, stripped_line), line in map(
+        lambda line: (LineType.from_line(line), line), fh.readlines()
+    ):
+        if line_type == LineType.SOURCE:
+            ret.append(line)
+        elif stripped_line.lstrip().startswith(""):
+            pre_chunk.append(stripped_line)
+            if any(" str:
+    """Fixes an individual 
 tag from Gnome.
+
+    Optionally prepends a given prefix to each line in the returned output.
+    """
+    soup = BeautifulSoup(html, "html.parser")
+
+    for pre in soup.find_all("pre"):
+        for tag in pre.find_all("font"):
+            #  -> 
+            tag.name = "span"
+            color = tag.attrs.pop("color")
+            tag["style"] = f"color: {color}"
+
+    return "".join(prefix + line for line in str(soup).splitlines(keepends=True))
+
+
+def main():
+    parser = ArgumentParser(
+        description="""Convert HTML from Gnome terminal's 'Copy as HTML' feature
+        to use modern  tags and inline CSS.
+
+        This script is idempotent, i.e. multiple invocations will not change
+        the output past the first invocation."""
+    )
+    parser.add_argument(
+        "file",
+        nargs="+",
+        type=FileType("r+", encoding="utf-8"),
+        help="""Rust file to update 
 blocks in.""",
+    )
+    args = parser.parse_args()
+    for fh in args.file:
+        if not fh.name.endswith(".rs"):
+            print(
+                "This script only fixes Rust source files; you probably didn't mean to include",
+                fh.name,
+                "so I'll skip processing it.",
+            )
+        new_content = fix_gnome_html(fh)
+        if new_content is not None:
+            print("Updated example colored output in", fh.name)
+            fh.seek(0)
+            fh.write(new_content)
+        else:
+            print("Nothing to fix in", fh.name)
+        fh.close()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/color-eyre/src/config.rs b/color-eyre/src/config.rs
new file mode 100644
index 0000000..22b4e60
--- /dev/null
+++ b/color-eyre/src/config.rs
@@ -0,0 +1,1221 @@
+//! Configuration options for customizing the behavior of the provided panic
+//! and error reporting hooks
+use crate::{
+    section::PanicMessage,
+    writers::{EnvSection, WriterExt},
+};
+use fmt::Display;
+use indenter::{indented, Format};
+use owo_colors::{style, OwoColorize, Style};
+use std::env;
+use std::fmt::Write as _;
+use std::{fmt, path::PathBuf, sync::Arc};
+
+#[derive(Debug)]
+struct InstallError;
+
+impl fmt::Display for InstallError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str("could not install the BacktracePrinter as another was already installed")
+    }
+}
+
+impl std::error::Error for InstallError {}
+
+#[derive(Debug)]
+struct InstallThemeError;
+
+impl fmt::Display for InstallThemeError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str("could not set the provided `Theme` globally as another was already set")
+    }
+}
+
+impl std::error::Error for InstallThemeError {}
+
+#[derive(Debug)]
+struct InstallColorSpantraceThemeError;
+
+impl fmt::Display for InstallColorSpantraceThemeError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str("could not set the provided `Theme` via `color_spantrace::set_theme` globally as another was already set")
+    }
+}
+
+impl std::error::Error for InstallColorSpantraceThemeError {}
+
+/// A struct that represents a theme that is used by `color_eyre`
+#[derive(Debug, Copy, Clone, Default)]
+pub struct Theme {
+    pub(crate) file: Style,
+    pub(crate) line_number: Style,
+    pub(crate) spantrace_target: Style,
+    pub(crate) spantrace_fields: Style,
+    pub(crate) active_line: Style,
+    pub(crate) error: Style,
+    pub(crate) help_info_note: Style,
+    pub(crate) help_info_warning: Style,
+    pub(crate) help_info_suggestion: Style,
+    pub(crate) help_info_error: Style,
+    pub(crate) dependency_code: Style,
+    pub(crate) crate_code: Style,
+    pub(crate) code_hash: Style,
+    pub(crate) panic_header: Style,
+    pub(crate) panic_message: Style,
+    pub(crate) panic_file: Style,
+    pub(crate) panic_line_number: Style,
+    pub(crate) hidden_frames: Style,
+}
+
+macro_rules! theme_setters {
+    ($(#[$meta:meta] $name:ident),* $(,)?) => {
+        $(
+            #[$meta]
+            pub fn $name(mut self, style: Style) -> Self {
+                self.$name = style;
+                self
+            }
+        )*
+    };
+}
+
+impl Theme {
+    /// Creates a blank theme
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Returns a theme for dark backgrounds. This is the default
+    pub fn dark() -> Self {
+        Self {
+            file: style().purple(),
+            line_number: style().purple(),
+            active_line: style().white().bold(),
+            error: style().bright_red(),
+            help_info_note: style().bright_cyan(),
+            help_info_warning: style().bright_yellow(),
+            help_info_suggestion: style().bright_cyan(),
+            help_info_error: style().bright_red(),
+            dependency_code: style().green(),
+            crate_code: style().bright_red(),
+            code_hash: style().bright_black(),
+            panic_header: style().red(),
+            panic_message: style().cyan(),
+            panic_file: style().purple(),
+            panic_line_number: style().purple(),
+            hidden_frames: style().bright_cyan(),
+            spantrace_target: style().bright_red(),
+            spantrace_fields: style().bright_cyan(),
+        }
+    }
+
+    // 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`))
+
+    /// Returns a theme for light backgrounds
+    pub fn light() -> Self {
+        Self {
+            file: style().purple(),
+            line_number: style().purple(),
+            spantrace_target: style().red(),
+            spantrace_fields: style().blue(),
+            active_line: style().bold(),
+            error: style().red(),
+            help_info_note: style().blue(),
+            help_info_warning: style().bright_red(),
+            help_info_suggestion: style().blue(),
+            help_info_error: style().red(),
+            dependency_code: style().green(),
+            crate_code: style().red(),
+            code_hash: style().bright_black(),
+            panic_header: style().red(),
+            panic_message: style().blue(),
+            panic_file: style().purple(),
+            panic_line_number: style().purple(),
+            hidden_frames: style().blue(),
+        }
+    }
+
+    theme_setters! {
+        /// Styles printed paths
+        file,
+        /// Styles the line number of a file
+        line_number,
+        /// Styles the `color_spantrace` target (i.e. the module and function name, and so on)
+        spantrace_target,
+        /// Styles fields associated with a the `tracing::Span`.
+        spantrace_fields,
+        /// Styles the selected line of displayed code
+        active_line,
+        // XXX not sure how to describe this better (or if this is even completely correct)
+        /// Styles errors printed by `EyreHandler`
+        error,
+        /// Styles the "note" section header
+        help_info_note,
+        /// Styles the "warning" section header
+        help_info_warning,
+        /// Styles the "suggestion" section header
+        help_info_suggestion,
+        /// Styles the "error" section header
+        help_info_error,
+        /// Styles code that is not part of your crate
+        dependency_code,
+        /// Styles code that's in your crate
+        crate_code,
+        /// Styles the hash after `dependency_code` and `crate_code`
+        code_hash,
+        /// Styles the header of a panic
+        panic_header,
+        /// Styles the message of a panic
+        panic_message,
+        /// Styles paths of a panic
+        panic_file,
+        /// Styles the line numbers of a panic
+        panic_line_number,
+        /// Styles the "N frames hidden" message
+        hidden_frames,
+    }
+}
+
+/// A representation of a Frame from a Backtrace or a SpanTrace
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct Frame {
+    /// Frame index
+    pub n: usize,
+    /// frame symbol name
+    pub name: Option,
+    /// source line number
+    pub lineno: Option,
+    /// source file path
+    pub filename: Option,
+}
+
+#[derive(Debug)]
+struct StyledFrame<'a>(&'a Frame, Theme);
+
+impl<'a> fmt::Display for StyledFrame<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let Self(frame, theme) = self;
+
+        let is_dependency_code = frame.is_dependency_code();
+
+        // Print frame index.
+        write!(f, "{:>2}: ", frame.n)?;
+
+        // Does the function have a hash suffix?
+        // (dodging a dep on the regex crate here)
+        let name = frame.name.as_deref().unwrap_or("");
+        let has_hash_suffix = name.len() > 19
+            && &name[name.len() - 19..name.len() - 16] == "::h"
+            && name[name.len() - 16..]
+                .chars()
+                .all(|x| x.is_ascii_hexdigit());
+
+        let hash_suffix = if has_hash_suffix {
+            &name[name.len() - 19..]
+        } else {
+            ""
+        };
+
+        // Print function name.
+        let name = if has_hash_suffix {
+            &name[..name.len() - 19]
+        } else {
+            name
+        };
+
+        if is_dependency_code {
+            write!(f, "{}", (name).style(theme.dependency_code))?;
+        } else {
+            write!(f, "{}", (name).style(theme.crate_code))?;
+        }
+
+        write!(f, "{}", (hash_suffix).style(theme.code_hash))?;
+
+        let mut separated = f.header("\n");
+
+        // Print source location, if known.
+        let file = frame.filename.as_ref().map(|path| path.display());
+        let file: &dyn fmt::Display = if let Some(ref filename) = file {
+            filename
+        } else {
+            &""
+        };
+        let lineno = frame
+            .lineno
+            .map_or("".to_owned(), |x| x.to_string());
+        write!(
+            &mut separated.ready(),
+            "    at {}:{}",
+            file.style(theme.file),
+            lineno.style(theme.line_number),
+        )?;
+
+        let v = if std::thread::panicking() {
+            panic_verbosity()
+        } else {
+            lib_verbosity()
+        };
+
+        // Maybe print source.
+        if v >= Verbosity::Full {
+            write!(&mut separated.ready(), "{}", SourceSection(frame, *theme))?;
+        }
+
+        Ok(())
+    }
+}
+
+struct SourceSection<'a>(&'a Frame, Theme);
+
+impl fmt::Display for SourceSection<'_> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let Self(frame, theme) = self;
+
+        let (lineno, filename) = match (frame.lineno, frame.filename.as_ref()) {
+            (Some(a), Some(b)) => (a, b),
+            // Without a line number and file name, we can't sensibly proceed.
+            _ => return Ok(()),
+        };
+
+        let file = match std::fs::File::open(filename) {
+            Ok(file) => file,
+            Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
+            e @ Err(_) => e.unwrap(),
+        };
+
+        use std::fmt::Write;
+        use std::io::BufRead;
+
+        // Extract relevant lines.
+        let reader = std::io::BufReader::new(file);
+        let start_line = lineno - 2.min(lineno - 1);
+        let surrounding_src = reader.lines().skip(start_line as usize - 1).take(5);
+        let mut separated = f.header("\n");
+        let mut f = separated.in_progress();
+        for (line, cur_line_no) in surrounding_src.zip(start_line..) {
+            let line = line.unwrap();
+            if cur_line_no == lineno {
+                write!(
+                    &mut f,
+                    "{:>8} {} {}",
+                    cur_line_no.style(theme.active_line),
+                    ">".style(theme.active_line),
+                    line.style(theme.active_line),
+                )?;
+            } else {
+                write!(&mut f, "{:>8} │ {}", cur_line_no, line)?;
+            }
+            f = separated.ready();
+        }
+
+        Ok(())
+    }
+}
+
+impl Frame {
+    fn is_dependency_code(&self) -> bool {
+        const SYM_PREFIXES: &[&str] = &[
+            "std::",
+            "core::",
+            "backtrace::backtrace::",
+            "_rust_begin_unwind",
+            "color_traceback::",
+            "__rust_",
+            "___rust_",
+            "__pthread",
+            "_main",
+            "main",
+            "__scrt_common_main_seh",
+            "BaseThreadInitThunk",
+            "_start",
+            "__libc_start_main",
+            "start_thread",
+        ];
+
+        // Inspect name.
+        if let Some(ref name) = self.name {
+            if SYM_PREFIXES.iter().any(|x| name.starts_with(x)) {
+                return true;
+            }
+        }
+
+        const FILE_PREFIXES: &[&str] = &[
+            "/rustc/",
+            "src/libstd/",
+            "src/libpanic_unwind/",
+            "src/libtest/",
+        ];
+
+        // Inspect filename.
+        if let Some(ref filename) = self.filename {
+            let filename = filename.to_string_lossy();
+            if FILE_PREFIXES.iter().any(|x| filename.starts_with(x))
+                || filename.contains("/.cargo/registry/src/")
+            {
+                return true;
+            }
+        }
+
+        false
+    }
+
+    /// Heuristically determine whether a frame is likely to be a post panic
+    /// frame.
+    ///
+    /// Post panic frames are frames of a functions called after the actual panic
+    /// is already in progress and don't contain any useful information for a
+    /// reader of the backtrace.
+    fn is_post_panic_code(&self) -> bool {
+        const SYM_PREFIXES: &[&str] = &[
+            "_rust_begin_unwind",
+            "rust_begin_unwind",
+            "core::result::unwrap_failed",
+            "core::option::expect_none_failed",
+            "core::panicking::panic_fmt",
+            "color_backtrace::create_panic_handler",
+            "std::panicking::begin_panic",
+            "begin_panic_fmt",
+            "failure::backtrace::Backtrace::new",
+            "backtrace::capture",
+            "failure::error_message::err_msg",
+            ">::from",
+        ];
+
+        match self.name.as_ref() {
+            Some(name) => SYM_PREFIXES.iter().any(|x| name.starts_with(x)),
+            None => false,
+        }
+    }
+
+    /// Heuristically determine whether a frame is likely to be part of language
+    /// runtime.
+    fn is_runtime_init_code(&self) -> bool {
+        const SYM_PREFIXES: &[&str] = &[
+            "std::rt::lang_start::",
+            "test::run_test::run_test_inner::",
+            "std::sys_common::backtrace::__rust_begin_short_backtrace",
+        ];
+
+        let (name, file) = match (self.name.as_ref(), self.filename.as_ref()) {
+            (Some(name), Some(filename)) => (name, filename.to_string_lossy()),
+            _ => return false,
+        };
+
+        if SYM_PREFIXES.iter().any(|x| name.starts_with(x)) {
+            return true;
+        }
+
+        // For Linux, this is the best rule for skipping test init I found.
+        if name == "{{closure}}" && file == "src/libtest/lib.rs" {
+            return true;
+        }
+
+        false
+    }
+}
+
+/// Builder for customizing the behavior of the global panic and error report hooks
+pub struct HookBuilder {
+    filters: Vec>,
+    capture_span_trace_by_default: bool,
+    display_env_section: bool,
+    #[cfg(feature = "track-caller")]
+    display_location_section: bool,
+    panic_section: Option>,
+    panic_message: Option>,
+    theme: Theme,
+    #[cfg(feature = "issue-url")]
+    issue_url: Option,
+    #[cfg(feature = "issue-url")]
+    issue_metadata: Vec<(String, Box)>,
+    #[cfg(feature = "issue-url")]
+    issue_filter: Arc,
+}
+
+impl HookBuilder {
+    /// Construct a HookBuilder
+    ///
+    /// # Details
+    ///
+    /// By default this function calls `add_default_filters()` and
+    /// `capture_span_trace_by_default(true)`. To get a `HookBuilder` with all
+    /// features disabled by default call `HookBuilder::blank()`.
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// use color_eyre::config::HookBuilder;
+    ///
+    /// HookBuilder::new()
+    ///     .install()
+    ///     .unwrap();
+    /// ```
+    pub fn new() -> Self {
+        Self::blank()
+            .add_default_filters()
+            .capture_span_trace_by_default(true)
+    }
+
+    /// Construct a HookBuilder with minimal features enabled
+    pub fn blank() -> Self {
+        HookBuilder {
+            filters: vec![],
+            capture_span_trace_by_default: false,
+            display_env_section: true,
+            #[cfg(feature = "track-caller")]
+            display_location_section: true,
+            panic_section: None,
+            panic_message: None,
+            theme: Theme::dark(),
+            #[cfg(feature = "issue-url")]
+            issue_url: None,
+            #[cfg(feature = "issue-url")]
+            issue_metadata: vec![],
+            #[cfg(feature = "issue-url")]
+            issue_filter: Arc::new(|_| true),
+        }
+    }
+
+    /// Set the global styles that `color_eyre` should use.
+    ///
+    /// **Tip:** You can test new styles by editing `examples/theme.rs` in the `color-eyre` repository.
+    pub fn theme(mut self, theme: Theme) -> Self {
+        self.theme = theme;
+        self
+    }
+
+    /// Add a custom section to the panic hook that will be printed
+    /// in the panic message.
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// color_eyre::config::HookBuilder::default()
+    ///     .panic_section("consider reporting the bug at https://github.com/eyre-rs/eyre/issues")
+    ///     .install()
+    ///     .unwrap()
+    /// ```
+    pub fn panic_section(mut self, section: S) -> Self {
+        self.panic_section = Some(Box::new(section));
+        self
+    }
+
+    /// Overrides the main error message printing section at the start of panic
+    /// reports
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// use std::{panic::Location, fmt};
+    /// use color_eyre::section::PanicMessage;
+    /// use owo_colors::OwoColorize;
+    ///
+    /// struct MyPanicMessage;
+    ///
+    /// color_eyre::config::HookBuilder::default()
+    ///     .panic_message(MyPanicMessage)
+    ///     .install()
+    ///     .unwrap();
+    ///
+    /// impl PanicMessage for MyPanicMessage {
+    ///     fn display(&self, pi: &std::panic::PanicInfo<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    ///         writeln!(f, "{}", "The application panicked (crashed).".red())?;
+    ///
+    ///         // Print panic message.
+    ///         let payload = pi
+    ///             .payload()
+    ///             .downcast_ref::()
+    ///             .map(String::as_str)
+    ///             .or_else(|| pi.payload().downcast_ref::<&str>().cloned())
+    ///             .unwrap_or("");
+    ///
+    ///         write!(f, "Message:  ")?;
+    ///         writeln!(f, "{}", payload.cyan())?;
+    ///
+    ///         // If known, print panic location.
+    ///         write!(f, "Location: ")?;
+    ///         if let Some(loc) = pi.location() {
+    ///             write!(f, "{}", loc.file().purple())?;
+    ///             write!(f, ":")?;
+    ///             write!(f, "{}", loc.line().purple())?;
+    ///
+    ///             write!(f, "\n\nConsider reporting the bug at {}", custom_url(loc, payload))?;
+    ///         } else {
+    ///             write!(f, "")?;
+    ///         }
+    ///
+    ///         Ok(())
+    ///     }
+    /// }
+    ///
+    /// fn custom_url(location: &Location<'_>, message: &str) -> impl fmt::Display {
+    ///     "todo"
+    /// }
+    /// ```
+    pub fn panic_message(mut self, section: S) -> Self {
+        self.panic_message = Some(Box::new(section));
+        self
+    }
+
+    /// Set an upstream github repo and enable issue reporting url generation
+    ///
+    /// # Details
+    ///
+    /// Once enabled, color-eyre will generate urls that will create customized
+    /// issues pre-populated with information about the associated error report.
+    ///
+    /// Additional information can be added to the metadata table in the
+    /// generated urls by calling `add_issue_metadata` when configuring the
+    /// HookBuilder.
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// color_eyre::config::HookBuilder::default()
+    ///     .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
+    ///     .install()
+    ///     .unwrap();
+    /// ```
+    #[cfg(feature = "issue-url")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
+    pub fn issue_url(mut self, url: S) -> Self {
+        self.issue_url = Some(url.to_string());
+        self
+    }
+
+    /// Add a new entry to the metadata table in generated github issue urls
+    ///
+    /// **Note**: this metadata will be ignored if no `issue_url` is set.
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// color_eyre::config::HookBuilder::default()
+    ///     .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
+    ///     .add_issue_metadata("version", env!("CARGO_PKG_VERSION"))
+    ///     .install()
+    ///     .unwrap();
+    /// ```
+    #[cfg(feature = "issue-url")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
+    pub fn add_issue_metadata(mut self, key: K, value: V) -> Self
+    where
+        K: Display,
+        V: Display + Send + Sync + 'static,
+    {
+        let pair = (key.to_string(), Box::new(value) as _);
+        self.issue_metadata.push(pair);
+        self
+    }
+
+    /// Configures a filter for disabling issue url generation for certain kinds of errors
+    ///
+    /// If the closure returns `true`, then the issue url will be generated.
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// color_eyre::config::HookBuilder::default()
+    ///     .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
+    ///     .issue_filter(|kind| match kind {
+    ///         color_eyre::ErrorKind::NonRecoverable(payload) => {
+    ///             let payload = payload
+    ///                 .downcast_ref::()
+    ///                 .map(String::as_str)
+    ///                 .or_else(|| payload.downcast_ref::<&str>().cloned())
+    ///                 .unwrap_or("");
+    ///
+    ///             !payload.contains("my irrelevant error message")
+    ///         },
+    ///         color_eyre::ErrorKind::Recoverable(error) => !error.is::(),
+    ///     })
+    ///     .install()
+    ///     .unwrap();
+    ///
+    #[cfg(feature = "issue-url")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
+    pub fn issue_filter(mut self, predicate: F) -> Self
+    where
+        F: Fn(crate::ErrorKind<'_>) -> bool + Send + Sync + 'static,
+    {
+        self.issue_filter = Arc::new(predicate);
+        self
+    }
+
+    /// Configures the default capture mode for `SpanTraces` in error reports and panics
+    pub fn capture_span_trace_by_default(mut self, cond: bool) -> Self {
+        self.capture_span_trace_by_default = cond;
+        self
+    }
+
+    /// Configures the enviroment varible info section and whether or not it is displayed
+    pub fn display_env_section(mut self, cond: bool) -> Self {
+        self.display_env_section = cond;
+        self
+    }
+
+    /// Configures the location info section and whether or not it is displayed.
+    ///
+    /// # Notes
+    ///
+    /// This will not disable the location section in a panic message.
+    #[cfg(feature = "track-caller")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "track-caller")))]
+    pub fn display_location_section(mut self, cond: bool) -> Self {
+        self.display_location_section = cond;
+        self
+    }
+
+    /// Add a custom filter to the set of frame filters
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// color_eyre::config::HookBuilder::default()
+    ///     .add_frame_filter(Box::new(|frames| {
+    ///         let filters = &[
+    ///             "uninteresting_function",
+    ///         ];
+    ///
+    ///         frames.retain(|frame| {
+    ///             !filters.iter().any(|f| {
+    ///                 let name = if let Some(name) = frame.name.as_ref() {
+    ///                     name.as_str()
+    ///                 } else {
+    ///                     return true;
+    ///                 };
+    ///
+    ///                 name.starts_with(f)
+    ///             })
+    ///         });
+    ///     }))
+    ///     .install()
+    ///     .unwrap();
+    /// ```
+    pub fn add_frame_filter(mut self, filter: Box) -> Self {
+        self.filters.push(filter);
+        self
+    }
+
+    /// Install the given Hook as the global error report hook
+    pub fn install(self) -> Result<(), crate::eyre::Report> {
+        let (panic_hook, eyre_hook) = self.try_into_hooks()?;
+        eyre_hook.install()?;
+        panic_hook.install();
+        Ok(())
+    }
+
+    /// Add the default set of filters to this `HookBuilder`'s configuration
+    pub fn add_default_filters(self) -> Self {
+        self.add_frame_filter(Box::new(default_frame_filter))
+            .add_frame_filter(Box::new(eyre_frame_filters))
+    }
+
+    /// Create a `PanicHook` and `EyreHook` from this `HookBuilder`.
+    /// This can be used if you want to combine these handlers with other handlers.
+    pub fn into_hooks(self) -> (PanicHook, EyreHook) {
+        self.try_into_hooks().expect("into_hooks should only be called when no `color_spantrace` themes have previously been set")
+    }
+
+    /// Create a `PanicHook` and `EyreHook` from this `HookBuilder`.
+    /// This can be used if you want to combine these handlers with other handlers.
+    pub fn try_into_hooks(self) -> Result<(PanicHook, EyreHook), crate::eyre::Report> {
+        let theme = self.theme;
+        #[cfg(feature = "issue-url")]
+        let metadata = Arc::new(self.issue_metadata);
+        let panic_hook = PanicHook {
+            filters: self.filters.into(),
+            section: self.panic_section,
+            #[cfg(feature = "capture-spantrace")]
+            capture_span_trace_by_default: self.capture_span_trace_by_default,
+            display_env_section: self.display_env_section,
+            panic_message: self
+                .panic_message
+                .unwrap_or_else(|| Box::new(DefaultPanicMessage(theme))),
+            theme,
+            #[cfg(feature = "issue-url")]
+            issue_url: self.issue_url.clone(),
+            #[cfg(feature = "issue-url")]
+            issue_metadata: metadata.clone(),
+            #[cfg(feature = "issue-url")]
+            issue_filter: self.issue_filter.clone(),
+        };
+
+        let eyre_hook = EyreHook {
+            filters: panic_hook.filters.clone(),
+            #[cfg(feature = "capture-spantrace")]
+            capture_span_trace_by_default: self.capture_span_trace_by_default,
+            display_env_section: self.display_env_section,
+            #[cfg(feature = "track-caller")]
+            display_location_section: self.display_location_section,
+            theme,
+            #[cfg(feature = "issue-url")]
+            issue_url: self.issue_url,
+            #[cfg(feature = "issue-url")]
+            issue_metadata: metadata,
+            #[cfg(feature = "issue-url")]
+            issue_filter: self.issue_filter,
+        };
+
+        #[cfg(feature = "capture-spantrace")]
+        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")?;
+
+        Ok((panic_hook, eyre_hook))
+    }
+}
+
+#[cfg(feature = "capture-spantrace")]
+impl From for color_spantrace::Theme {
+    fn from(src: Theme) -> color_spantrace::Theme {
+        color_spantrace::Theme::new()
+            .file(src.file)
+            .line_number(src.line_number)
+            .target(src.spantrace_target)
+            .fields(src.spantrace_fields)
+            .active_line(src.active_line)
+    }
+}
+
+#[allow(missing_docs)]
+impl Default for HookBuilder {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+fn default_frame_filter(frames: &mut Vec<&Frame>) {
+    let top_cutoff = frames
+        .iter()
+        .rposition(|x| x.is_post_panic_code())
+        .map(|x| x + 2) // indices are 1 based
+        .unwrap_or(0);
+
+    let bottom_cutoff = frames
+        .iter()
+        .position(|x| x.is_runtime_init_code())
+        .unwrap_or(frames.len());
+
+    let rng = top_cutoff..=bottom_cutoff;
+    frames.retain(|x| rng.contains(&x.n))
+}
+
+fn eyre_frame_filters(frames: &mut Vec<&Frame>) {
+    let filters = &[
+        "::default",
+        "eyre::",
+        "color_eyre::",
+    ];
+
+    frames.retain(|frame| {
+        !filters.iter().any(|f| {
+            let name = if let Some(name) = frame.name.as_ref() {
+                name.as_str()
+            } else {
+                return true;
+            };
+
+            name.starts_with(f)
+        })
+    });
+}
+
+struct DefaultPanicMessage(Theme);
+
+impl PanicMessage for DefaultPanicMessage {
+    fn display(&self, pi: &std::panic::PanicInfo<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        // 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
+        let theme = &self.0;
+
+        writeln!(
+            f,
+            "{}",
+            "The application panicked (crashed).".style(theme.panic_header)
+        )?;
+
+        // Print panic message.
+        let payload = pi
+            .payload()
+            .downcast_ref::()
+            .map(String::as_str)
+            .or_else(|| pi.payload().downcast_ref::<&str>().cloned())
+            .unwrap_or("");
+
+        write!(f, "Message:  ")?;
+        writeln!(f, "{}", payload.style(theme.panic_message))?;
+
+        // If known, print panic location.
+        write!(f, "Location: ")?;
+        write!(f, "{}", crate::fmt::LocationSection(pi.location(), *theme))?;
+
+        Ok(())
+    }
+}
+
+/// A type representing an error report for a panic.
+pub struct PanicReport<'a> {
+    hook: &'a PanicHook,
+    panic_info: &'a std::panic::PanicInfo<'a>,
+    backtrace: Option,
+    #[cfg(feature = "capture-spantrace")]
+    span_trace: Option,
+}
+
+fn print_panic_info(report: &PanicReport<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    report.hook.panic_message.display(report.panic_info, f)?;
+
+    let v = panic_verbosity();
+    let capture_bt = v != Verbosity::Minimal;
+
+    let mut separated = f.header("\n\n");
+
+    if let Some(ref section) = report.hook.section {
+        write!(&mut separated.ready(), "{}", section)?;
+    }
+
+    #[cfg(feature = "capture-spantrace")]
+    {
+        if let Some(span_trace) = report.span_trace.as_ref() {
+            write!(
+                &mut separated.ready(),
+                "{}",
+                crate::writers::FormattedSpanTrace(span_trace)
+            )?;
+        }
+    }
+
+    if let Some(bt) = report.backtrace.as_ref() {
+        let fmted_bt = report.hook.format_backtrace(bt);
+        write!(
+            indented(&mut separated.ready()).with_format(Format::Uniform { indentation: "  " }),
+            "{}",
+            fmted_bt
+        )?;
+    }
+
+    if report.hook.display_env_section {
+        let env_section = EnvSection {
+            bt_captured: &capture_bt,
+            #[cfg(feature = "capture-spantrace")]
+            span_trace: report.span_trace.as_ref(),
+        };
+
+        write!(&mut separated.ready(), "{}", env_section)?;
+    }
+
+    #[cfg(feature = "issue-url")]
+    {
+        let payload = report.panic_info.payload();
+
+        if report.hook.issue_url.is_some()
+            && (*report.hook.issue_filter)(crate::ErrorKind::NonRecoverable(payload))
+        {
+            let url = report.hook.issue_url.as_ref().unwrap();
+            let payload = payload
+                .downcast_ref::()
+                .map(String::as_str)
+                .or_else(|| payload.downcast_ref::<&str>().cloned())
+                .unwrap_or("");
+
+            let issue_section = crate::section::github::IssueSection::new(url, payload)
+                .with_backtrace(report.backtrace.as_ref())
+                .with_location(report.panic_info.location())
+                .with_metadata(&report.hook.issue_metadata);
+
+            #[cfg(feature = "capture-spantrace")]
+            let issue_section = issue_section.with_span_trace(report.span_trace.as_ref());
+
+            write!(&mut separated.ready(), "{}", issue_section)?;
+        }
+    }
+
+    Ok(())
+}
+
+impl fmt::Display for PanicReport<'_> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        print_panic_info(self, f)
+    }
+}
+
+/// A panic reporting hook
+pub struct PanicHook {
+    filters: Arc<[Box]>,
+    section: Option>,
+    panic_message: Box,
+    theme: Theme,
+    #[cfg(feature = "capture-spantrace")]
+    capture_span_trace_by_default: bool,
+    display_env_section: bool,
+    #[cfg(feature = "issue-url")]
+    issue_url: Option,
+    #[cfg(feature = "issue-url")]
+    issue_metadata: Arc)>>,
+    #[cfg(feature = "issue-url")]
+    issue_filter: Arc,
+}
+
+impl PanicHook {
+    pub(crate) fn format_backtrace<'a>(
+        &'a self,
+        trace: &'a backtrace::Backtrace,
+    ) -> BacktraceFormatter<'a> {
+        BacktraceFormatter {
+            filters: &self.filters,
+            inner: trace,
+            theme: self.theme,
+        }
+    }
+
+    #[cfg(feature = "capture-spantrace")]
+    fn spantrace_capture_enabled(&self) -> bool {
+        std::env::var("RUST_SPANTRACE")
+            .map(|val| val != "0")
+            .unwrap_or(self.capture_span_trace_by_default)
+    }
+
+    /// Install self as a global panic hook via `std::panic::set_hook`.
+    pub fn install(self) {
+        std::panic::set_hook(self.into_panic_hook());
+    }
+
+    /// Convert self into the type expected by `std::panic::set_hook`.
+    pub fn into_panic_hook(
+        self,
+    ) -> Box) + Send + Sync + 'static> {
+        Box::new(move |panic_info| {
+            eprintln!("{}", self.panic_report(panic_info));
+        })
+    }
+
+    /// Construct a panic reporter which prints it's panic report via the
+    /// `Display` trait.
+    pub fn panic_report<'a>(
+        &'a self,
+        panic_info: &'a std::panic::PanicInfo<'_>,
+    ) -> PanicReport<'a> {
+        let v = panic_verbosity();
+        let capture_bt = v != Verbosity::Minimal;
+
+        #[cfg(feature = "capture-spantrace")]
+        let span_trace = if self.spantrace_capture_enabled() {
+            Some(tracing_error::SpanTrace::capture())
+        } else {
+            None
+        };
+
+        let backtrace = if capture_bt {
+            Some(backtrace::Backtrace::new())
+        } else {
+            None
+        };
+
+        PanicReport {
+            panic_info,
+            #[cfg(feature = "capture-spantrace")]
+            span_trace,
+            backtrace,
+            hook: self,
+        }
+    }
+}
+
+/// An eyre reporting hook used to construct `EyreHandler`s
+pub struct EyreHook {
+    filters: Arc<[Box]>,
+    #[cfg(feature = "capture-spantrace")]
+    capture_span_trace_by_default: bool,
+    display_env_section: bool,
+    #[cfg(feature = "track-caller")]
+    display_location_section: bool,
+    theme: Theme,
+    #[cfg(feature = "issue-url")]
+    issue_url: Option,
+    #[cfg(feature = "issue-url")]
+    issue_metadata: Arc)>>,
+    #[cfg(feature = "issue-url")]
+    issue_filter: Arc,
+}
+
+type HookFunc = Box<
+    dyn Fn(&(dyn std::error::Error + 'static)) -> Box
+        + Send
+        + Sync
+        + 'static,
+>;
+
+impl EyreHook {
+    #[allow(unused_variables)]
+    pub(crate) fn default(&self, error: &(dyn std::error::Error + 'static)) -> crate::Handler {
+        let backtrace = if lib_verbosity() != Verbosity::Minimal {
+            Some(backtrace::Backtrace::new())
+        } else {
+            None
+        };
+
+        #[cfg(feature = "capture-spantrace")]
+        let span_trace = if self.spantrace_capture_enabled()
+            && crate::handler::get_deepest_spantrace(error).is_none()
+        {
+            Some(tracing_error::SpanTrace::capture())
+        } else {
+            None
+        };
+
+        crate::Handler {
+            filters: self.filters.clone(),
+            backtrace,
+            suppress_backtrace: false,
+            #[cfg(feature = "capture-spantrace")]
+            span_trace,
+            sections: Vec::new(),
+            display_env_section: self.display_env_section,
+            #[cfg(feature = "track-caller")]
+            display_location_section: self.display_location_section,
+            #[cfg(feature = "issue-url")]
+            issue_url: self.issue_url.clone(),
+            #[cfg(feature = "issue-url")]
+            issue_metadata: self.issue_metadata.clone(),
+            #[cfg(feature = "issue-url")]
+            issue_filter: self.issue_filter.clone(),
+            theme: self.theme,
+            #[cfg(feature = "track-caller")]
+            location: None,
+        }
+    }
+
+    #[cfg(feature = "capture-spantrace")]
+    fn spantrace_capture_enabled(&self) -> bool {
+        std::env::var("RUST_SPANTRACE")
+            .map(|val| val != "0")
+            .unwrap_or(self.capture_span_trace_by_default)
+    }
+
+    /// Installs self as the global eyre handling hook via `eyre::set_hook`
+    pub fn install(self) -> Result<(), crate::eyre::InstallError> {
+        crate::eyre::set_hook(self.into_eyre_hook())
+    }
+
+    /// Convert the self into the boxed type expected by `eyre::set_hook`.
+    pub fn into_eyre_hook(self) -> HookFunc {
+        Box::new(move |e| Box::new(self.default(e)))
+    }
+}
+
+pub(crate) struct BacktraceFormatter<'a> {
+    pub(crate) filters: &'a [Box],
+    pub(crate) inner: &'a backtrace::Backtrace,
+    pub(crate) theme: Theme,
+}
+
+impl fmt::Display for BacktraceFormatter<'_> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{:━^80}", " BACKTRACE ")?;
+
+        // Collect frame info.
+        let frames: Vec<_> = self
+            .inner
+            .frames()
+            .iter()
+            .flat_map(|frame| frame.symbols())
+            .zip(1usize..)
+            .map(|(sym, n)| Frame {
+                name: sym.name().map(|x| x.to_string()),
+                lineno: sym.lineno(),
+                filename: sym.filename().map(|x| x.into()),
+                n,
+            })
+            .collect();
+
+        let mut filtered_frames = frames.iter().collect();
+        match env::var("COLORBT_SHOW_HIDDEN").ok().as_deref() {
+            Some("1") | Some("on") | Some("y") => (),
+            _ => {
+                for filter in self.filters {
+                    filter(&mut filtered_frames);
+                }
+            }
+        }
+
+        if filtered_frames.is_empty() {
+            // TODO: Would probably look better centered.
+            return write!(f, "\n");
+        }
+
+        let mut separated = f.header("\n");
+
+        // Don't let filters mess with the order.
+        filtered_frames.sort_by_key(|x| x.n);
+
+        let mut buf = String::new();
+
+        macro_rules! print_hidden {
+            ($n:expr) => {
+                let n = $n;
+                buf.clear();
+                write!(
+                    &mut buf,
+                    "{decorator} {n} frame{plural} hidden {decorator}",
+                    n = n,
+                    plural = if n == 1 { "" } else { "s" },
+                    decorator = "⋮",
+                )
+                .expect("writing to strings doesn't panic");
+                write!(
+                    &mut separated.ready(),
+                    "{:^80}",
+                    buf.style(self.theme.hidden_frames)
+                )?;
+            };
+        }
+
+        let mut last_n = 0;
+        for frame in &filtered_frames {
+            let frame_delta = frame.n - last_n - 1;
+            if frame_delta != 0 {
+                print_hidden!(frame_delta);
+            }
+            write!(&mut separated.ready(), "{}", StyledFrame(frame, self.theme))?;
+            last_n = frame.n;
+        }
+
+        let last_filtered_n = filtered_frames.last().unwrap().n;
+        let last_unfiltered_n = frames.last().unwrap().n;
+        if last_filtered_n < last_unfiltered_n {
+            print_hidden!(last_unfiltered_n - last_filtered_n);
+        }
+
+        Ok(())
+    }
+}
+
+#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
+pub(crate) enum Verbosity {
+    Minimal,
+    Medium,
+    Full,
+}
+
+pub(crate) fn panic_verbosity() -> Verbosity {
+    match env::var("RUST_BACKTRACE") {
+        Ok(s) if s == "full" => Verbosity::Full,
+        Ok(s) if s != "0" => Verbosity::Medium,
+        _ => Verbosity::Minimal,
+    }
+}
+
+pub(crate) fn lib_verbosity() -> Verbosity {
+    match env::var("RUST_LIB_BACKTRACE").or_else(|_| env::var("RUST_BACKTRACE")) {
+        Ok(s) if s == "full" => Verbosity::Full,
+        Ok(s) if s != "0" => Verbosity::Medium,
+        _ => Verbosity::Minimal,
+    }
+}
+
+/// Callback for filtering a vector of `Frame`s
+pub type FilterCallback = dyn Fn(&mut Vec<&Frame>) + Send + Sync + 'static;
+
+/// Callback for filtering issue url generation in error reports
+#[cfg(feature = "issue-url")]
+#[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
+pub type IssueFilterCallback = dyn Fn(crate::ErrorKind<'_>) -> bool + Send + Sync + 'static;
diff --git a/color-eyre/src/fmt.rs b/color-eyre/src/fmt.rs
new file mode 100644
index 0000000..42e6add
--- /dev/null
+++ b/color-eyre/src/fmt.rs
@@ -0,0 +1,25 @@
+//! Module for new types that isolate complext formatting
+use std::fmt;
+
+use owo_colors::OwoColorize;
+
+pub(crate) struct LocationSection<'a>(
+    pub(crate) Option<&'a std::panic::Location<'a>>,
+    pub(crate) crate::config::Theme,
+);
+
+impl fmt::Display for LocationSection<'_> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let theme = self.1;
+        // If known, print panic location.
+        if let Some(loc) = self.0 {
+            write!(f, "{}", loc.file().style(theme.panic_file))?;
+            write!(f, ":")?;
+            write!(f, "{}", loc.line().style(theme.panic_line_number))?;
+        } else {
+            write!(f, "")?;
+        }
+
+        Ok(())
+    }
+}
diff --git a/color-eyre/src/handler.rs b/color-eyre/src/handler.rs
new file mode 100644
index 0000000..80c1417
--- /dev/null
+++ b/color-eyre/src/handler.rs
@@ -0,0 +1,188 @@
+use crate::{
+    config::BacktraceFormatter,
+    section::help::HelpInfo,
+    writers::{EnvSection, WriterExt},
+    Handler,
+};
+use backtrace::Backtrace;
+use indenter::{indented, Format};
+use std::fmt::Write;
+#[cfg(feature = "capture-spantrace")]
+use tracing_error::{ExtractSpanTrace, SpanTrace};
+
+impl std::fmt::Debug for Handler {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str("redacted")
+    }
+}
+
+impl Handler {
+    /// Return a reference to the captured `Backtrace` type
+    pub fn backtrace(&self) -> Option<&Backtrace> {
+        self.backtrace.as_ref()
+    }
+
+    /// Return a reference to the captured `SpanTrace` type
+    #[cfg(feature = "capture-spantrace")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "capture-spantrace")))]
+    pub fn span_trace(&self) -> Option<&SpanTrace> {
+        self.span_trace.as_ref()
+    }
+
+    pub(crate) fn format_backtrace<'a>(
+        &'a self,
+        trace: &'a backtrace::Backtrace,
+    ) -> BacktraceFormatter<'a> {
+        BacktraceFormatter {
+            filters: &self.filters,
+            inner: trace,
+            theme: self.theme,
+        }
+    }
+}
+
+impl eyre::EyreHandler for Handler {
+    fn debug(
+        &self,
+        error: &(dyn std::error::Error + 'static),
+        f: &mut core::fmt::Formatter<'_>,
+    ) -> core::fmt::Result {
+        if f.alternate() {
+            return core::fmt::Debug::fmt(error, f);
+        }
+
+        #[cfg(feature = "capture-spantrace")]
+        let errors = || {
+            eyre::Chain::new(error)
+                .filter(|e| e.span_trace().is_none())
+                .enumerate()
+        };
+
+        #[cfg(not(feature = "capture-spantrace"))]
+        let errors = || eyre::Chain::new(error).enumerate();
+
+        for (n, error) in errors() {
+            writeln!(f)?;
+            write!(indented(f).ind(n), "{}", self.theme.error.style(error))?;
+        }
+
+        let mut separated = f.header("\n\n");
+
+        #[cfg(feature = "track-caller")]
+        if self.display_location_section {
+            write!(
+                separated.ready(),
+                "{}",
+                crate::SectionExt::header(
+                    crate::fmt::LocationSection(self.location, self.theme),
+                    "Location:"
+                )
+            )?;
+        }
+
+        for section in self
+            .sections
+            .iter()
+            .filter(|s| matches!(s, HelpInfo::Error(_, _)))
+        {
+            write!(separated.ready(), "{}", section)?;
+        }
+
+        for section in self
+            .sections
+            .iter()
+            .filter(|s| matches!(s, HelpInfo::Custom(_)))
+        {
+            write!(separated.ready(), "{}", section)?;
+        }
+
+        #[cfg(feature = "capture-spantrace")]
+        let span_trace = self
+            .span_trace
+            .as_ref()
+            .or_else(|| get_deepest_spantrace(error));
+
+        #[cfg(feature = "capture-spantrace")]
+        {
+            if let Some(span_trace) = span_trace {
+                write!(
+                    &mut separated.ready(),
+                    "{}",
+                    crate::writers::FormattedSpanTrace(span_trace)
+                )?;
+            }
+        }
+
+        if !self.suppress_backtrace {
+            if let Some(backtrace) = self.backtrace.as_ref() {
+                let fmted_bt = self.format_backtrace(backtrace);
+
+                write!(
+                    indented(&mut separated.ready())
+                        .with_format(Format::Uniform { indentation: "  " }),
+                    "{}",
+                    fmted_bt
+                )?;
+            }
+        }
+
+        let f = separated.ready();
+        let mut h = f.header("\n");
+        let mut f = h.in_progress();
+
+        for section in self
+            .sections
+            .iter()
+            .filter(|s| !matches!(s, HelpInfo::Custom(_) | HelpInfo::Error(_, _)))
+        {
+            write!(&mut f, "{}", section)?;
+            f = h.ready();
+        }
+
+        if self.display_env_section {
+            let env_section = EnvSection {
+                bt_captured: &self.backtrace.is_some(),
+                #[cfg(feature = "capture-spantrace")]
+                span_trace,
+            };
+
+            write!(&mut separated.ready(), "{}", env_section)?;
+        }
+
+        #[cfg(feature = "issue-url")]
+        if self.issue_url.is_some() && (*self.issue_filter)(crate::ErrorKind::Recoverable(error)) {
+            let url = self.issue_url.as_ref().unwrap();
+            let mut payload = String::from("Error: ");
+            for (n, error) in errors() {
+                writeln!(&mut payload)?;
+                write!(indented(&mut payload).ind(n), "{}", error)?;
+            }
+
+            let issue_section = crate::section::github::IssueSection::new(url, &payload)
+                .with_backtrace(self.backtrace.as_ref())
+                .with_metadata(&self.issue_metadata);
+
+            #[cfg(feature = "capture-spantrace")]
+            let issue_section = issue_section.with_span_trace(span_trace);
+
+            write!(&mut separated.ready(), "{}", issue_section)?;
+        }
+
+        Ok(())
+    }
+
+    #[cfg(feature = "track-caller")]
+    fn track_caller(&mut self, location: &'static std::panic::Location<'static>) {
+        self.location = Some(location);
+    }
+}
+
+#[cfg(feature = "capture-spantrace")]
+pub(crate) fn get_deepest_spantrace<'a>(
+    error: &'a (dyn std::error::Error + 'static),
+) -> Option<&'a SpanTrace> {
+    eyre::Chain::new(error)
+        .rev()
+        .flat_map(|error| error.span_trace())
+        .next()
+}
diff --git a/color-eyre/src/lib.rs b/color-eyre/src/lib.rs
new file mode 100644
index 0000000..2664cad
--- /dev/null
+++ b/color-eyre/src/lib.rs
@@ -0,0 +1,460 @@
+//! An error report handler for panics and the [`eyre`] crate for colorful, consistent, and well
+//! formatted error reports for all kinds of errors.
+//!
+//! ## TLDR
+//!
+//! `color_eyre` helps you build error reports that look like this:
+//!
+//! 
color-eyre on  hooked [$!] is 📦 v0.5.0 via 🦀 v1.44.0
+//!  cargo run --example custom_section
+//!     Finished dev [unoptimized + debuginfo] target(s) in 0.04s
+//!      Running `target/debug/examples/custom_section`
+//! Error:
+//!    0: Unable to read config
+//!    1: cmd exited with non-zero status code
+//!
+//! Stderr:
+//!    cat: fake_file: No such file or directory
+//!
+//!   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+//!
+//!    0: custom_section::output2 with self="cat" "fake_file"
+//!       at examples/custom_section.rs:14
+//!    1: custom_section::read_file with path="fake_file"
+//!       at examples/custom_section.rs:58
+//!    2: custom_section::read_config
+//!       at examples/custom_section.rs:63
+//!
+//! Suggestion: try using a file that exists next time
+//! +//! ## Setup +//! +//! Add the following to your toml file: +//! +//! ```toml +//! [dependencies] +//! color-eyre = "0.6" +//! ``` +//! +//! And install the panic and error report handlers: +//! +//! ```rust +//! use color_eyre::eyre::Result; +//! +//! fn main() -> Result<()> { +//! color_eyre::install()?; +//! +//! // ... +//! # Ok(()) +//! } +//! ``` +//! +//! ### Disabling tracing support +//! +//! If you don't plan on using `tracing_error` and `SpanTrace` you can disable the +//! tracing integration to cut down on unused dependencies: +//! +//! ```toml +//! [dependencies] +//! color-eyre = { version = "0.6", default-features = false } +//! ``` +//! +//! ### Disabling SpanTrace capture by default +//! +//! color-eyre defaults to capturing span traces. This is because `SpanTrace` +//! capture is significantly cheaper than `Backtrace` capture. However, like +//! backtraces, span traces are most useful for debugging applications, and it's +//! not uncommon to want to disable span trace capture by default to keep noise out +//! developer. +//! +//! To disable span trace capture you must explicitly set one of the env variables +//! that regulate `SpanTrace` capture to `"0"`: +//! +//! ```rust +//! if std::env::var("RUST_SPANTRACE").is_err() { +//! std::env::set_var("RUST_SPANTRACE", "0"); +//! } +//! ``` +//! +//! ### Improving perf on debug builds +//! +//! In debug mode `color-eyre` behaves noticably worse than `eyre`. This is caused +//! by the fact that `eyre` uses `std::backtrace::Backtrace` instead of +//! `backtrace::Backtrace`. The std version of backtrace is precompiled with +//! optimizations, this means that whether or not you're in debug mode doesn't +//! matter much for how expensive backtrace capture is, it will always be in the +//! 10s of milliseconds to capture. A debug version of `backtrace::Backtrace` +//! however isn't so lucky, and can take an order of magnitude more time to capture +//! a backtrace compared to its std counterpart. +//! +//! Cargo [profile +//! overrides](https://doc.rust-lang.org/cargo/reference/profiles.html#overrides) +//! can be used to mitigate this problem. By configuring your project to always +//! build `backtrace` with optimizations you should get the same performance from +//! `color-eyre` that you're used to with `eyre`. To do so add the following to +//! your Cargo.toml: +//! +//! ```toml +//! [profile.dev.package.backtrace] +//! opt-level = 3 +//! ``` +//! +//! ## Features +//! +//! ### Multiple report format verbosity levels +//! +//! `color-eyre` provides 3 different report formats for how it formats the captured `SpanTrace` +//! and `Backtrace`, minimal, short, and full. Take the below snippets of the output produced by [`examples/usage.rs`]: +//! +//! --- +//! +//! Running `cargo run --example usage` without `RUST_LIB_BACKTRACE` set will produce a minimal +//! report like this: +//! +//!
color-eyre on  hooked [$!] is 📦 v0.5.0 via 🦀 v1.44.0 took 2s
+//!  cargo run --example usage
+//!     Finished dev [unoptimized + debuginfo] target(s) in 0.04s
+//!      Running `target/debug/examples/usage`
+//! Jul 05 19:15:58.026  INFO read_config:read_file{path="fake_file"}: Reading file
+//! Error:
+//!    0: Unable to read config
+//!    1: No such file or directory (os error 2)
+//!
+//!   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+//!
+//!    0: usage::read_file with path="fake_file"
+//!       at examples/usage.rs:32
+//!    1: usage::read_config
+//!       at examples/usage.rs:38
+//!
+//! Suggestion: try using a file that exists next time
+//! +//!
+//! +//! Running `RUST_LIB_BACKTRACE=1 cargo run --example usage` tells `color-eyre` to use the short +//! format, which additionally capture a [`backtrace::Backtrace`]: +//! +//!
color-eyre on  hooked [$!] is 📦 v0.5.0 via 🦀 v1.44.0
+//!  RUST_LIB_BACKTRACE=1 cargo run --example usage
+//!     Finished dev [unoptimized + debuginfo] target(s) in 0.04s
+//!      Running `target/debug/examples/usage`
+//! Jul 05 19:16:02.853  INFO read_config:read_file{path="fake_file"}: Reading file
+//! Error:
+//!    0: Unable to read config
+//!    1: No such file or directory (os error 2)
+//!
+//!   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+//!
+//!    0: usage::read_file with path="fake_file"
+//!       at examples/usage.rs:32
+//!    1: usage::read_config
+//!       at examples/usage.rs:38
+//!
+//!   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+//!                                 ⋮ 5 frames hidden ⋮                               
+//!    6: usage::read_file::haee210cb22460af3
+//!       at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:35
+//!    7: usage::read_config::ha649ef4ec333524d
+//!       at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:40
+//!    8: usage::main::hbe443b50eac38236
+//!       at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:11
+//!                                 ⋮ 10 frames hidden ⋮                              
+//!
+//! Suggestion: try using a file that exists next time
+//! +//!
+//! +//! Finally, running `RUST_LIB_BACKTRACE=full cargo run --example usage` tells `color-eyre` to use +//! the full format, which in addition to the above will attempt to include source lines where the +//! error originated from, assuming it can find them on the disk. +//! +//!
color-eyre on  hooked [$!] is 📦 v0.5.0 via 🦀 v1.44.0
+//!  RUST_LIB_BACKTRACE=full cargo run --example usage
+//!     Finished dev [unoptimized + debuginfo] target(s) in 0.05s
+//!      Running `target/debug/examples/usage`
+//! Jul 05 19:16:06.335  INFO read_config:read_file{path="fake_file"}: Reading file
+//! Error:
+//!    0: Unable to read config
+//!    1: No such file or directory (os error 2)
+//!
+//!   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+//!
+//!    0: usage::read_file with path="fake_file"
+//!       at examples/usage.rs:32
+//!         30 │ }
+//!         31 │
+//!         32 > #[instrument]
+//!         33 │ fn read_file(path: &str) -> Result<(), Report> {
+//!         34 │     info!("Reading file");
+//!    1: usage::read_config
+//!       at examples/usage.rs:38
+//!         36 │ }
+//!         37 │
+//!         38 > #[instrument]
+//!         39 │ fn read_config() -> Result<(), Report> {
+//!         40 │     read_file("fake_file")
+//!
+//!   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+//!                                 ⋮ 5 frames hidden ⋮                               
+//!    6: usage::read_file::haee210cb22460af3
+//!       at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:35
+//!         33 │ fn read_file(path: &str) -> Result<(), Report> {
+//!         34 │     info!("Reading file");
+//!         35 >     Ok(std::fs::read_to_string(path).map(drop)?)
+//!         36 │ }
+//!         37 │
+//!    7: usage::read_config::ha649ef4ec333524d
+//!       at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:40
+//!         38 │ #[instrument]
+//!         39 │ fn read_config() -> Result<(), Report> {
+//!         40 >     read_file("fake_file")
+//!         41 │         .wrap_err("Unable to read config")
+//!         42 │         .suggestion("try using a file that exists next time")
+//!    8: usage::main::hbe443b50eac38236
+//!       at /home/jlusby/git/yaahc/color-eyre/examples/usage.rs:11
+//!          9 │     color_eyre::install()?;
+//!         10 │
+//!         11 >     Ok(read_config()?)
+//!         12 │ }
+//!         13 │
+//!                                 ⋮ 10 frames hidden ⋮                              
+//!
+//! Suggestion: try using a file that exists next time
+//! +//! ### Custom `Section`s for error reports via [`Section`] trait +//! +//! The `section` module provides helpers for adding extra sections to error +//! reports. Sections are disinct from error messages and are displayed +//! independently from the chain of errors. Take this example of adding sections +//! to contain `stderr` and `stdout` from a failed command, taken from +//! [`examples/custom_section.rs`]: +//! +//! ```rust +//! use color_eyre::{eyre::eyre, SectionExt, Section, eyre::Report}; +//! use std::process::Command; +//! use tracing::instrument; +//! +//! trait Output { +//! fn output2(&mut self) -> Result; +//! } +//! +//! impl Output for Command { +//! #[instrument] +//! fn output2(&mut self) -> Result { +//! let output = self.output()?; +//! +//! let stdout = String::from_utf8_lossy(&output.stdout); +//! +//! if !output.status.success() { +//! let stderr = String::from_utf8_lossy(&output.stderr); +//! Err(eyre!("cmd exited with non-zero status code")) +//! .with_section(move || stdout.trim().to_string().header("Stdout:")) +//! .with_section(move || stderr.trim().to_string().header("Stderr:")) +//! } else { +//! Ok(stdout.into()) +//! } +//! } +//! } +//! ``` +//! +//! --- +//! +//! Here we have an function that, if the command exits unsuccessfully, creates a +//! report indicating the failure and attaches two sections, one for `stdout` and +//! one for `stderr`. +//! +//! Running `cargo run --example custom_section` shows us how these sections are +//! included in the output: +//! +//!
color-eyre on  hooked [$!] is 📦 v0.5.0 via 🦀 v1.44.0 took 2s
+//!  cargo run --example custom_section
+//!     Finished dev [unoptimized + debuginfo] target(s) in 0.04s
+//!      Running `target/debug/examples/custom_section`
+//! Error:
+//!    0: Unable to read config
+//!    1: cmd exited with non-zero status code
+//!
+//! Stderr:
+//!    cat: fake_file: No such file or directory
+//!
+//!   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+//!
+//!    0: custom_section::output2 with self="cat" "fake_file"
+//!       at examples/custom_section.rs:14
+//!    1: custom_section::read_file with path="fake_file"
+//!       at examples/custom_section.rs:58
+//!    2: custom_section::read_config
+//!       at examples/custom_section.rs:63
+//!
+//! Suggestion: try using a file that exists next time
+//! +//! Only the `Stderr:` section actually gets included. The `cat` command fails, +//! so stdout ends up being empty and is skipped in the final report. This gives +//! us a short and concise error report indicating exactly what was attempted and +//! how it failed. +//! +//! ### Aggregating multiple errors into one report +//! +//! It's not uncommon for programs like batched task runners or parsers to want +//! to return an error with multiple sources. The current version of the error +//! trait does not support this use case very well, though there is [work being +//! done](https://github.com/rust-lang/rfcs/pull/2895) to improve this. +//! +//! For now however one way to work around this is to compose errors outside the +//! error trait. `color-eyre` supports such composition in its error reports via +//! the `Section` trait. +//! +//! For an example of how to aggregate errors check out [`examples/multiple_errors.rs`]. +//! +//! ### Custom configuration for `color-backtrace` for setting custom filters and more +//! +//! The pretty printing for backtraces and span traces isn't actually provided by +//! `color-eyre`, but instead comes from its dependencies [`color-backtrace`] and +//! [`color-spantrace`]. `color-backtrace` in particular has many more features +//! than are exported by `color-eyre`, such as customized color schemes, panic +//! hooks, and custom frame filters. The custom frame filters are particularly +//! useful when combined with `color-eyre`, so to enable their usage we provide +//! the `install` fn for setting up a custom `BacktracePrinter` with custom +//! filters installed. +//! +//! For an example of how to setup custom filters, check out [`examples/custom_filter.rs`]. +//! +//! [`eyre`]: https://docs.rs/eyre +//! [`tracing-error`]: https://docs.rs/tracing-error +//! [`color-backtrace`]: https://docs.rs/color-backtrace +//! [`eyre::EyreHandler`]: https://docs.rs/eyre/*/eyre/trait.EyreHandler.html +//! [`backtrace::Backtrace`]: https://docs.rs/backtrace/*/backtrace/struct.Backtrace.html +//! [`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html +//! [`color-spantrace`]: https://github.com/yaahc/color-spantrace +//! [`Section`]: https://docs.rs/color-eyre/*/color_eyre/trait.Section.html +//! [`eyre::Report`]: https://docs.rs/eyre/*/eyre/struct.Report.html +//! [`eyre::Result`]: https://docs.rs/eyre/*/eyre/type.Result.html +//! [`Handler`]: https://docs.rs/color-eyre/*/color_eyre/struct.Handler.html +//! [`examples/usage.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/usage.rs +//! [`examples/custom_filter.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_filter.rs +//! [`examples/custom_section.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_section.rs +//! [`examples/multiple_errors.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/multiple_errors.rs +#![doc(html_root_url = "https://docs.rs/color-eyre/0.6.2")] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![warn( + missing_docs, + rustdoc::missing_doc_code_examples, + rust_2018_idioms, + unreachable_pub, + bad_style, + dead_code, + improper_ctypes, + non_shorthand_field_patterns, + no_mangle_generic_items, + overflowing_literals, + path_statements, + patterns_in_fns_without_body, + unused, + unused_allocation, + unused_comparisons, + unused_parens, + while_true +)] +#![allow(clippy::try_err)] + +use std::sync::Arc; + +use backtrace::Backtrace; +pub use eyre; +#[doc(hidden)] +pub use eyre::Report; +#[doc(hidden)] +pub use eyre::Result; +pub use owo_colors; +use section::help::HelpInfo; +#[doc(hidden)] +pub use section::Section as Help; +pub use section::{IndentedSection, Section, SectionExt}; +#[cfg(feature = "capture-spantrace")] +use tracing_error::SpanTrace; +#[doc(hidden)] +pub use Handler as Context; + +pub mod config; +mod fmt; +mod handler; +pub(crate) mod private; +pub mod section; +mod writers; + +/// A custom handler type for [`eyre::Report`] which provides colorful error +/// reports and [`tracing-error`] support. +/// +/// # Details +/// +/// This type is not intended to be used directly, prefer using it via the +/// [`color_eyre::Report`] and [`color_eyre::Result`] type aliases. +/// +/// [`eyre::Report`]: https://docs.rs/eyre/*/eyre/struct.Report.html +/// [`tracing-error`]: https://docs.rs/tracing-error +/// [`color_eyre::Report`]: type.Report.html +/// [`color_eyre::Result`]: type.Result.html +pub struct Handler { + filters: Arc<[Box]>, + backtrace: Option, + suppress_backtrace: bool, + #[cfg(feature = "capture-spantrace")] + span_trace: Option, + sections: Vec, + display_env_section: bool, + #[cfg(feature = "track-caller")] + display_location_section: bool, + #[cfg(feature = "issue-url")] + issue_url: Option, + #[cfg(feature = "issue-url")] + issue_metadata: + std::sync::Arc)>>, + #[cfg(feature = "issue-url")] + issue_filter: std::sync::Arc, + theme: crate::config::Theme, + #[cfg(feature = "track-caller")] + location: Option<&'static std::panic::Location<'static>>, +} + +/// The kind of type erased error being reported +#[cfg(feature = "issue-url")] +#[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))] +pub enum ErrorKind<'a> { + /// A non recoverable error aka `panic!` + NonRecoverable(&'a dyn std::any::Any), + /// A recoverable error aka `impl std::error::Error` + Recoverable(&'a (dyn std::error::Error + 'static)), +} + +/// Install the default panic and error report hooks +/// +/// # Details +/// +/// This function must be called to enable the customization of `eyre::Report` +/// provided by `color-eyre`. This function should be called early, ideally +/// before any errors could be encountered. +/// +/// Only the first install will succeed. Calling this function after another +/// report handler has been installed will cause an error. **Note**: This +/// function _must_ be called before any `eyre::Report`s are constructed to +/// prevent the default handler from being installed. +/// +/// Installing a global theme in `color_spantrace` manually (by calling +/// `color_spantrace::set_theme` or `color_spantrace::colorize` before +/// `install` is called) will result in an error if this function is called. +/// +/// # Examples +/// +/// ```rust +/// use color_eyre::eyre::Result; +/// +/// fn main() -> Result<()> { +/// color_eyre::install()?; +/// +/// // ... +/// # Ok(()) +/// } +/// ``` +pub fn install() -> Result<(), crate::eyre::Report> { + config::HookBuilder::default().install() +} diff --git a/color-eyre/src/private.rs b/color-eyre/src/private.rs new file mode 100644 index 0000000..d2f22e6 --- /dev/null +++ b/color-eyre/src/private.rs @@ -0,0 +1,5 @@ +use crate::eyre::Report; +pub trait Sealed {} + +impl Sealed for std::result::Result where E: Into {} +impl Sealed for Report {} diff --git a/color-eyre/src/section/github.rs b/color-eyre/src/section/github.rs new file mode 100644 index 0000000..f0734e6 --- /dev/null +++ b/color-eyre/src/section/github.rs @@ -0,0 +1,190 @@ +use crate::writers::DisplayExt; +use backtrace::Backtrace; +use std::{fmt, panic::Location}; +#[cfg(feature = "capture-spantrace")] +use tracing_error::SpanTrace; +use url::Url; + +type Display<'a> = Box; + +pub(crate) struct IssueSection<'a> { + url: &'a str, + msg: &'a str, + location: Option<&'a Location<'a>>, + backtrace: Option<&'a Backtrace>, + #[cfg(feature = "capture-spantrace")] + span_trace: Option<&'a SpanTrace>, + metadata: &'a [(String, Display<'a>)], +} + +impl<'a> IssueSection<'a> { + pub(crate) fn new(url: &'a str, msg: &'a str) -> Self { + IssueSection { + url, + msg, + location: None, + backtrace: None, + #[cfg(feature = "capture-spantrace")] + span_trace: None, + metadata: &[], + } + } + + pub(crate) fn with_location(mut self, location: impl Into>>) -> Self { + self.location = location.into(); + self + } + + pub(crate) fn with_backtrace(mut self, backtrace: impl Into>) -> Self { + self.backtrace = backtrace.into(); + self + } + + #[cfg(feature = "capture-spantrace")] + pub(crate) fn with_span_trace(mut self, span_trace: impl Into>) -> Self { + self.span_trace = span_trace.into(); + self + } + + pub(crate) fn with_metadata(mut self, metadata: &'a [(String, Display<'a>)]) -> Self { + self.metadata = metadata; + self + } +} + +impl fmt::Display for IssueSection<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let location = self + .location + .map(|loc| ("location".to_string(), Box::new(loc) as _)); + let metadata = self.metadata.iter().chain(location.as_ref()); + let metadata = MetadataSection { metadata }.to_string(); + let mut body = Body::new(); + body.push_section("Error", ConsoleSection(self.msg))?; + + if !self.metadata.is_empty() { + body.push_section("Metadata", metadata)?; + } + + #[cfg(feature = "capture-spantrace")] + if let Some(st) = self.span_trace { + body.push_section( + "SpanTrace", + Collapsed(ConsoleSection(st.with_header("SpanTrace:\n"))), + )?; + } + + if let Some(bt) = self.backtrace { + body.push_section( + "Backtrace", + Collapsed(ConsoleSection( + DisplayFromDebug(bt).with_header("Backtrace:\n"), + )), + )?; + } + + let url_result = Url::parse_with_params( + self.url, + &[("title", ""), ("body", &body.body)], + ); + + let url: &dyn fmt::Display = match &url_result { + Ok(url_struct) => url_struct, + Err(_) => &self.url, + }; + + url.with_header("Consider reporting this error using this URL: ") + .fmt(f) + } +} + +struct Body { + body: String, +} + +impl Body { + fn new() -> Self { + Body { + body: String::new(), + } + } + fn push_section(&mut self, header: &'static str, section: T) -> fmt::Result + where + T: fmt::Display, + { + use std::fmt::Write; + + let separator = if self.body.is_empty() { "" } else { "\n\n" }; + let header = header + .with_header("## ") + .with_header(separator) + .with_footer("\n"); + + write!(&mut self.body, "{}", section.with_header(header)) + } +} + +struct MetadataSection { + metadata: T, +} + +impl<'a, T> MetadataSection +where + T: IntoIterator)>, +{ + // This is implemented as a free functions so it can consume the `metadata` + // iterator, rather than being forced to leave it unmodified if its behind a + // `&self` shared reference via the Display trait + #[allow(clippy::inherent_to_string, clippy::wrong_self_convention)] + fn to_string(self) -> String { + use std::fmt::Write; + + let mut out = String::new(); + let f = &mut out; + + writeln!(f, "|key|value|").expect("writing to a string doesn't panic"); + writeln!(f, "|--|--|").expect("writing to a string doesn't panic"); + + for (key, value) in self.metadata { + writeln!(f, "|**{}**|{}|", key, value).expect("writing to a string doesn't panic"); + } + + out + } +} + +struct ConsoleSection(T); + +impl fmt::Display for ConsoleSection +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (&self.0).with_header("```\n").with_footer("\n```").fmt(f) + } +} + +struct Collapsed(T); + +impl fmt::Display for Collapsed +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (&self.0) + .with_header("\n
\n\n") + .with_footer("\n
") + .fmt(f) + } +} + +struct DisplayFromDebug(T); + +impl fmt::Display for DisplayFromDebug +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} diff --git a/color-eyre/src/section/help.rs b/color-eyre/src/section/help.rs new file mode 100644 index 0000000..a03cf3e --- /dev/null +++ b/color-eyre/src/section/help.rs @@ -0,0 +1,320 @@ +//! Provides an extension trait for attaching `Section` to error reports. +use crate::{ + config::Theme, + eyre::{Report, Result}, + Section, +}; +use indenter::indented; +use owo_colors::OwoColorize; +use std::fmt::Write; +use std::fmt::{self, Display}; + +impl Section for Report { + type Return = Report; + + fn note(mut self, note: D) -> Self::Return + where + D: Display + Send + Sync + 'static, + { + if let Some(handler) = self.handler_mut().downcast_mut::() { + handler + .sections + .push(HelpInfo::Note(Box::new(note), handler.theme)); + } + + self + } + + fn with_note(mut self, note: F) -> Self::Return + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, + { + if let Some(handler) = self.handler_mut().downcast_mut::() { + handler + .sections + .push(HelpInfo::Note(Box::new(note()), handler.theme)); + } + + self + } + + fn warning(mut self, warning: D) -> Self::Return + where + D: Display + Send + Sync + 'static, + { + if let Some(handler) = self.handler_mut().downcast_mut::() { + handler + .sections + .push(HelpInfo::Warning(Box::new(warning), handler.theme)); + } + + self + } + + fn with_warning(mut self, warning: F) -> Self::Return + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, + { + if let Some(handler) = self.handler_mut().downcast_mut::() { + handler + .sections + .push(HelpInfo::Warning(Box::new(warning()), handler.theme)); + } + + self + } + + fn suggestion(mut self, suggestion: D) -> Self::Return + where + D: Display + Send + Sync + 'static, + { + if let Some(handler) = self.handler_mut().downcast_mut::() { + handler + .sections + .push(HelpInfo::Suggestion(Box::new(suggestion), handler.theme)); + } + + self + } + + fn with_suggestion(mut self, suggestion: F) -> Self::Return + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, + { + if let Some(handler) = self.handler_mut().downcast_mut::() { + handler + .sections + .push(HelpInfo::Suggestion(Box::new(suggestion()), handler.theme)); + } + + self + } + + fn with_section(mut self, section: F) -> Self::Return + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, + { + if let Some(handler) = self.handler_mut().downcast_mut::() { + let section = Box::new(section()); + handler.sections.push(HelpInfo::Custom(section)); + } + + self + } + + fn section(mut self, section: D) -> Self::Return + where + D: Display + Send + Sync + 'static, + { + if let Some(handler) = self.handler_mut().downcast_mut::() { + let section = Box::new(section); + handler.sections.push(HelpInfo::Custom(section)); + } + + self + } + + fn error(mut self, error: E2) -> Self::Return + where + E2: std::error::Error + Send + Sync + 'static, + { + if let Some(handler) = self.handler_mut().downcast_mut::() { + let error = error.into(); + handler.sections.push(HelpInfo::Error(error, handler.theme)); + } + + self + } + + fn with_error(mut self, error: F) -> Self::Return + where + F: FnOnce() -> E2, + E2: std::error::Error + Send + Sync + 'static, + { + if let Some(handler) = self.handler_mut().downcast_mut::() { + let error = error().into(); + handler.sections.push(HelpInfo::Error(error, handler.theme)); + } + + self + } + + fn suppress_backtrace(mut self, suppress: bool) -> Self::Return { + if let Some(handler) = self.handler_mut().downcast_mut::() { + handler.suppress_backtrace = suppress; + } + + self + } +} + +impl Section for Result +where + E: Into, +{ + type Return = Result; + + fn note(self, note: D) -> Self::Return + where + D: Display + Send + Sync + 'static, + { + self.map_err(|error| error.into()) + .map_err(|report| report.note(note)) + } + + fn with_note(self, note: F) -> Self::Return + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, + { + self.map_err(|error| error.into()) + .map_err(|report| report.note(note())) + } + + fn warning(self, warning: D) -> Self::Return + where + D: Display + Send + Sync + 'static, + { + self.map_err(|error| error.into()) + .map_err(|report| report.warning(warning)) + } + + fn with_warning(self, warning: F) -> Self::Return + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, + { + self.map_err(|error| error.into()) + .map_err(|report| report.warning(warning())) + } + + fn suggestion(self, suggestion: D) -> Self::Return + where + D: Display + Send + Sync + 'static, + { + self.map_err(|error| error.into()) + .map_err(|report| report.suggestion(suggestion)) + } + + fn with_suggestion(self, suggestion: F) -> Self::Return + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, + { + self.map_err(|error| error.into()) + .map_err(|report| report.suggestion(suggestion())) + } + + fn with_section(self, section: F) -> Self::Return + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D, + { + self.map_err(|error| error.into()) + .map_err(|report| report.section(section())) + } + + fn section(self, section: D) -> Self::Return + where + D: Display + Send + Sync + 'static, + { + self.map_err(|error| error.into()) + .map_err(|report| report.section(section)) + } + + fn error(self, error: E2) -> Self::Return + where + E2: std::error::Error + Send + Sync + 'static, + { + self.map_err(|error| error.into()) + .map_err(|report| report.error(error)) + } + + fn with_error(self, error: F) -> Self::Return + where + F: FnOnce() -> E2, + E2: std::error::Error + Send + Sync + 'static, + { + self.map_err(|error| error.into()) + .map_err(|report| report.error(error())) + } + + fn suppress_backtrace(self, suppress: bool) -> Self::Return { + self.map_err(|error| error.into()) + .map_err(|report| report.suppress_backtrace(suppress)) + } +} + +pub(crate) enum HelpInfo { + Error(Box, Theme), + Custom(Box), + Note(Box, Theme), + Warning(Box, Theme), + Suggestion(Box, Theme), +} + +impl Display for HelpInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HelpInfo::Note(note, theme) => { + write!(f, "{}: {}", "Note".style(theme.help_info_note), note) + } + HelpInfo::Warning(warning, theme) => write!( + f, + "{}: {}", + "Warning".style(theme.help_info_warning), + warning + ), + HelpInfo::Suggestion(suggestion, theme) => write!( + f, + "{}: {}", + "Suggestion".style(theme.help_info_suggestion), + suggestion + ), + HelpInfo::Custom(section) => write!(f, "{}", section), + HelpInfo::Error(error, theme) => { + // a lot here + let errors = std::iter::successors( + Some(error.as_ref() as &(dyn std::error::Error + 'static)), + |e| e.source(), + ); + + write!(f, "Error:")?; + for (n, error) in errors.enumerate() { + writeln!(f)?; + write!(indented(f).ind(n), "{}", error.style(theme.help_info_error))?; + } + + Ok(()) + } + } + } +} + +impl fmt::Debug for HelpInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HelpInfo::Note(note, ..) => f + .debug_tuple("Note") + .field(&format_args!("{}", note)) + .finish(), + HelpInfo::Warning(warning, ..) => f + .debug_tuple("Warning") + .field(&format_args!("{}", warning)) + .finish(), + HelpInfo::Suggestion(suggestion, ..) => f + .debug_tuple("Suggestion") + .field(&format_args!("{}", suggestion)) + .finish(), + HelpInfo::Custom(custom, ..) => f + .debug_tuple("CustomSection") + .field(&format_args!("{}", custom)) + .finish(), + HelpInfo::Error(error, ..) => f.debug_tuple("Error").field(error).finish(), + } + } +} diff --git a/color-eyre/src/section/mod.rs b/color-eyre/src/section/mod.rs new file mode 100644 index 0000000..f7b5b68 --- /dev/null +++ b/color-eyre/src/section/mod.rs @@ -0,0 +1,333 @@ +//! Helpers for adding custom sections to error reports +use crate::writers::WriterExt; +use std::fmt::{self, Display}; + +#[cfg(feature = "issue-url")] +pub(crate) mod github; +pub(crate) mod help; + +/// An indented section with a header for an error report +/// +/// # Details +/// +/// This helper provides two functions to help with constructing nicely formatted +/// error reports. First, it handles indentation of every line of the body for +/// you, and makes sure it is consistent with the rest of color-eyre's output. +/// Second, it omits outputting the header if the body itself is empty, +/// preventing unnecessary pollution of the report for sections with dynamic +/// content. +/// +/// # Examples +/// +/// ```rust +/// use color_eyre::{eyre::eyre, SectionExt, Section, eyre::Report}; +/// use std::process::Command; +/// use tracing::instrument; +/// +/// trait Output { +/// fn output2(&mut self) -> Result; +/// } +/// +/// impl Output for Command { +/// #[instrument] +/// fn output2(&mut self) -> Result { +/// let output = self.output()?; +/// +/// let stdout = String::from_utf8_lossy(&output.stdout); +/// +/// if !output.status.success() { +/// let stderr = String::from_utf8_lossy(&output.stderr); +/// Err(eyre!("cmd exited with non-zero status code")) +/// .with_section(move || stdout.trim().to_string().header("Stdout:")) +/// .with_section(move || stderr.trim().to_string().header("Stderr:")) +/// } else { +/// Ok(stdout.into()) +/// } +/// } +/// } +/// ``` +#[allow(missing_debug_implementations)] +pub struct IndentedSection { + header: H, + body: B, +} + +impl fmt::Display for IndentedSection +where + H: Display + Send + Sync + 'static, + B: Display + Send + Sync + 'static, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use std::fmt::Write; + let mut headered = f.header(&self.header); + let headered = headered.ready(); + let mut headered = headered.header("\n"); + + let mut headered = headered.ready(); + + let mut indented = indenter::indented(&mut headered) + .with_format(indenter::Format::Uniform { indentation: " " }); + + write!(&mut indented, "{}", self.body)?; + + Ok(()) + } +} + +/// Extension trait for constructing sections with commonly used formats +pub trait SectionExt: Sized { + /// Add a header to a `Section` and indent the body + /// + /// # Details + /// + /// Bodies are always indented to the same level as error messages and spans. + /// The header is not printed if the display impl of the body produces no + /// output. + /// + /// # Examples + /// + /// ```rust,no_run + /// use color_eyre::{eyre::eyre, Section, SectionExt, eyre::Report}; + /// + /// let all_in_header = "header\n body\n body"; + /// let report = Err::<(), Report>(eyre!("an error occurred")) + /// .section(all_in_header) + /// .unwrap_err(); + /// + /// let just_header = "header"; + /// let just_body = "body\nbody"; + /// let report2 = Err::<(), Report>(eyre!("an error occurred")) + /// .section(just_body.header(just_header)) + /// .unwrap_err(); + /// + /// assert_eq!(format!("{:?}", report), format!("{:?}", report2)) + /// ``` + fn header(self, header: C) -> IndentedSection + where + C: Display + Send + Sync + 'static; +} + +impl SectionExt for T +where + T: Display + Send + Sync + 'static, +{ + fn header(self, header: C) -> IndentedSection + where + C: Display + Send + Sync + 'static, + { + IndentedSection { body: self, header } + } +} + +/// A helper trait for attaching informational sections to error reports to be +/// displayed after the chain of errors +/// +/// # Details +/// +/// `color_eyre` provides two types of help text that can be attached to error reports: custom +/// sections and pre-configured sections. Custom sections are added via the `section` and +/// `with_section` methods, and give maximum control over formatting. +/// +/// The pre-configured sections are provided via `suggestion`, `warning`, and `note`. These +/// sections are displayed after all other sections with no extra newlines between subsequent Help +/// sections. They consist only of a header portion and are prepended with a colored string +/// indicating the kind of section, e.g. `Note: This might have failed due to ..." +pub trait Section: crate::private::Sealed { + /// The return type of each method after adding context + type Return; + + /// Add a section to an error report, to be displayed after the chain of errors. + /// + /// # Details + /// + /// Sections are displayed in the order they are added to the error report. They are displayed + /// immediately after the `Error:` section and before the `SpanTrace` and `Backtrace` sections. + /// They consist of a header and an optional body. The body of the section is indented by + /// default. + /// + /// # Examples + /// + /// ```rust,should_panic + /// use color_eyre::{eyre::eyre, eyre::Report, Section}; + /// + /// Err(eyre!("command failed")) + /// .section("Please report bugs to https://real.url/bugs")?; + /// # Ok::<_, Report>(()) + /// ``` + fn section(self, section: D) -> Self::Return + where + D: Display + Send + Sync + 'static; + + /// Add a Section to an error report, to be displayed after the chain of errors. The closure to + /// create the Section is lazily evaluated only in the case of an error. + /// + /// # Examples + /// + /// ```rust + /// use color_eyre::{eyre::eyre, eyre::Report, Section, SectionExt}; + /// + /// # #[cfg(not(miri))] + /// # { + /// let output = std::process::Command::new("ls") + /// .output()?; + /// + /// let output = if !output.status.success() { + /// let stderr = String::from_utf8_lossy(&output.stderr); + /// Err(eyre!("cmd exited with non-zero status code")) + /// .with_section(move || stderr.trim().to_string().header("Stderr:"))? + /// } else { + /// String::from_utf8_lossy(&output.stdout) + /// }; + /// + /// println!("{}", output); + /// # } + /// # Ok::<_, Report>(()) + /// ``` + fn with_section(self, section: F) -> Self::Return + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D; + + /// Add an error section to an error report, to be displayed after the primary error message + /// section. + /// + /// # Examples + /// + /// ```rust,should_panic + /// use color_eyre::{eyre::eyre, eyre::Report, Section}; + /// use thiserror::Error; + /// + /// #[derive(Debug, Error)] + /// #[error("{0}")] + /// struct StrError(&'static str); + /// + /// Err(eyre!("command failed")) + /// .error(StrError("got one error")) + /// .error(StrError("got a second error"))?; + /// # Ok::<_, Report>(()) + /// ``` + fn error(self, error: E) -> Self::Return + where + E: std::error::Error + Send + Sync + 'static; + + /// Add an error section to an error report, to be displayed after the primary error message + /// section. The closure to create the Section is lazily evaluated only in the case of an error. + /// + /// # Examples + /// + /// ```rust,should_panic + /// use color_eyre::{eyre::eyre, eyre::Report, Section}; + /// use thiserror::Error; + /// + /// #[derive(Debug, Error)] + /// #[error("{0}")] + /// struct StringError(String); + /// + /// Err(eyre!("command failed")) + /// .with_error(|| StringError("got one error".into())) + /// .with_error(|| StringError("got a second error".into()))?; + /// # Ok::<_, Report>(()) + /// ``` + fn with_error(self, error: F) -> Self::Return + where + F: FnOnce() -> E, + E: std::error::Error + Send + Sync + 'static; + + /// Add a Note to an error report, to be displayed after the chain of errors. + /// + /// # Examples + /// + /// ```rust + /// # use std::{error::Error, fmt::{self, Display}}; + /// # use color_eyre::eyre::Result; + /// # #[derive(Debug)] + /// # struct FakeErr; + /// # impl Display for FakeErr { + /// # fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + /// # write!(f, "FakeErr") + /// # } + /// # } + /// # impl std::error::Error for FakeErr {} + /// # fn main() -> Result<()> { + /// # fn fallible_fn() -> Result<(), FakeErr> { + /// # Ok(()) + /// # } + /// use color_eyre::Section as _; + /// + /// fallible_fn().note("This might have failed due to ...")?; + /// # Ok(()) + /// # } + /// ``` + fn note(self, note: D) -> Self::Return + where + D: Display + Send + Sync + 'static; + + /// Add a Note to an error report, to be displayed after the chain of errors. The closure to + /// create the Note is lazily evaluated only in the case of an error. + /// + /// # Examples + /// + /// ```rust + /// # use std::{error::Error, fmt::{self, Display}}; + /// # use color_eyre::eyre::Result; + /// # #[derive(Debug)] + /// # struct FakeErr; + /// # impl Display for FakeErr { + /// # fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + /// # write!(f, "FakeErr") + /// # } + /// # } + /// # impl std::error::Error for FakeErr {} + /// # fn main() -> Result<()> { + /// # fn fallible_fn() -> Result<(), FakeErr> { + /// # Ok(()) + /// # } + /// use color_eyre::Section as _; + /// + /// fallible_fn().with_note(|| { + /// format!("This might have failed due to ... It has failed {} times", 100) + /// })?; + /// # Ok(()) + /// # } + /// ``` + fn with_note(self, f: F) -> Self::Return + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D; + + /// Add a Warning to an error report, to be displayed after the chain of errors. + fn warning(self, warning: D) -> Self::Return + where + D: Display + Send + Sync + 'static; + + /// Add a Warning to an error report, to be displayed after the chain of errors. The closure to + /// create the Warning is lazily evaluated only in the case of an error. + fn with_warning(self, f: F) -> Self::Return + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D; + + /// Add a Suggestion to an error report, to be displayed after the chain of errors. + fn suggestion(self, suggestion: D) -> Self::Return + where + D: Display + Send + Sync + 'static; + + /// Add a Suggestion to an error report, to be displayed after the chain of errors. The closure + /// to create the Suggestion is lazily evaluated only in the case of an error. + fn with_suggestion(self, f: F) -> Self::Return + where + D: Display + Send + Sync + 'static, + F: FnOnce() -> D; + + /// Whether to suppress printing of collected backtrace (if any). + /// + /// Useful for reporting "unexceptional" errors for which a backtrace + /// isn't really necessary. + fn suppress_backtrace(self, suppress: bool) -> Self::Return; +} + +/// Trait for printing a panic error message for the given PanicInfo +pub trait PanicMessage: Send + Sync + 'static { + /// Display trait equivalent for implementing the display logic + fn display(&self, pi: &std::panic::PanicInfo<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result; +} diff --git a/color-eyre/src/writers.rs b/color-eyre/src/writers.rs new file mode 100644 index 0000000..b5bb344 --- /dev/null +++ b/color-eyre/src/writers.rs @@ -0,0 +1,261 @@ +use crate::config::{lib_verbosity, panic_verbosity, Verbosity}; +use fmt::Write; +use std::fmt::{self, Display}; +#[cfg(feature = "capture-spantrace")] +use tracing_error::{SpanTrace, SpanTraceStatus}; + +#[allow(explicit_outlives_requirements)] +pub(crate) struct HeaderWriter<'a, H, W> +where + H: ?Sized, +{ + inner: W, + header: &'a H, + started: bool, +} + +pub(crate) trait WriterExt: Sized { + fn header(self, header: &H) -> HeaderWriter<'_, H, Self>; +} + +impl WriterExt for W { + fn header(self, header: &H) -> HeaderWriter<'_, H, Self> { + HeaderWriter { + inner: self, + header, + started: false, + } + } +} + +pub(crate) trait DisplayExt: Sized + Display { + fn with_header(self, header: H) -> Header; + fn with_footer(self, footer: F) -> Footer; +} + +impl DisplayExt for T +where + T: Display, +{ + fn with_footer(self, footer: F) -> Footer { + Footer { body: self, footer } + } + + fn with_header(self, header: H) -> Header { + Header { + body: self, + h: header, + } + } +} + +pub(crate) struct ReadyHeaderWriter<'a, 'b, H: ?Sized, W>(&'b mut HeaderWriter<'a, H, W>); + +impl<'a, H: ?Sized, W> HeaderWriter<'a, H, W> { + pub(crate) fn ready(&mut self) -> ReadyHeaderWriter<'a, '_, H, W> { + self.started = false; + + ReadyHeaderWriter(self) + } + + pub(crate) fn in_progress(&mut self) -> ReadyHeaderWriter<'a, '_, H, W> { + self.started = true; + + ReadyHeaderWriter(self) + } +} + +impl<'a, H: ?Sized, W> fmt::Write for ReadyHeaderWriter<'a, '_, H, W> +where + H: Display, + W: fmt::Write, +{ + fn write_str(&mut self, s: &str) -> fmt::Result { + if !self.0.started && !s.is_empty() { + self.0.inner.write_fmt(format_args!("{}", self.0.header))?; + self.0.started = true; + } + + self.0.inner.write_str(s) + } +} + +pub(crate) struct FooterWriter { + inner: W, + had_output: bool, +} + +impl fmt::Write for FooterWriter +where + W: fmt::Write, +{ + fn write_str(&mut self, s: &str) -> fmt::Result { + if !self.had_output && !s.is_empty() { + self.had_output = true; + } + + self.inner.write_str(s) + } +} + +#[allow(explicit_outlives_requirements)] +pub(crate) struct Footer +where + B: Display, + H: Display, +{ + body: B, + footer: H, +} + +impl fmt::Display for Footer +where + B: Display, + H: Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut inner_f = FooterWriter { + inner: &mut *f, + had_output: false, + }; + + write!(&mut inner_f, "{}", self.body)?; + + if inner_f.had_output { + self.footer.fmt(f)?; + } + + Ok(()) + } +} + +#[allow(explicit_outlives_requirements)] +pub(crate) struct Header +where + B: Display, + H: Display, +{ + body: B, + h: H, +} + +impl fmt::Display for Header +where + B: Display, + H: Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f.header(&self.h).ready(), "{}", self.body)?; + + Ok(()) + } +} + +#[cfg(feature = "capture-spantrace")] +pub(crate) struct FormattedSpanTrace<'a>(pub(crate) &'a SpanTrace); + +#[cfg(feature = "capture-spantrace")] +impl fmt::Display for FormattedSpanTrace<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use indenter::indented; + use indenter::Format; + + if self.0.status() == SpanTraceStatus::CAPTURED { + write!( + indented(f).with_format(Format::Uniform { indentation: " " }), + "{}", + color_spantrace::colorize(self.0) + )?; + } + + Ok(()) + } +} + +pub(crate) struct EnvSection<'a> { + pub(crate) bt_captured: &'a bool, + #[cfg(feature = "capture-spantrace")] + pub(crate) span_trace: Option<&'a SpanTrace>, +} + +impl fmt::Display for EnvSection<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let v = if std::thread::panicking() { + panic_verbosity() + } else { + lib_verbosity() + }; + write!(f, "{}", BacktraceOmited(!self.bt_captured))?; + + let mut separated = HeaderWriter { + inner: &mut *f, + header: &"\n", + started: false, + }; + write!(&mut separated.ready(), "{}", SourceSnippets(v))?; + #[cfg(feature = "capture-spantrace")] + write!( + &mut separated.ready(), + "{}", + SpanTraceOmited(self.span_trace) + )?; + Ok(()) + } +} + +#[cfg(feature = "capture-spantrace")] +struct SpanTraceOmited<'a>(Option<&'a SpanTrace>); + +#[cfg(feature = "capture-spantrace")] +impl fmt::Display for SpanTraceOmited<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(span_trace) = self.0 { + if span_trace.status() == SpanTraceStatus::UNSUPPORTED { + writeln!(f, "Warning: SpanTrace capture is Unsupported.")?; + write!( + f, + "Ensure that you've setup a tracing-error ErrorLayer and the semver versions are compatible" + )?; + } + } + + Ok(()) + } +} + +struct BacktraceOmited(bool); + +impl fmt::Display for BacktraceOmited { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Print some info on how to increase verbosity. + if self.0 { + write!( + f, + "Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it." + )?; + } else { + // This text only makes sense if frames are displayed. + write!( + f, + "Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering." + )?; + } + + Ok(()) + } +} + +struct SourceSnippets(Verbosity); + +impl fmt::Display for SourceSnippets { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0 <= Verbosity::Medium { + write!( + f, + "Run with RUST_BACKTRACE=full to include source snippets." + )?; + } + + Ok(()) + } +} diff --git a/color-eyre/tests/bt_disabled.rs b/color-eyre/tests/bt_disabled.rs new file mode 100644 index 0000000..4e6f163 --- /dev/null +++ b/color-eyre/tests/bt_disabled.rs @@ -0,0 +1,15 @@ +use color_eyre::eyre; +use eyre::eyre; + +#[test] +fn disabled() { + color_eyre::config::HookBuilder::default() + .display_env_section(false) + .install() + .unwrap(); + + let report = eyre!("error occured"); + + let report = format!("{:?}", report); + assert!(!report.contains("RUST_BACKTRACE")); +} diff --git a/color-eyre/tests/bt_enabled.rs b/color-eyre/tests/bt_enabled.rs new file mode 100644 index 0000000..7bc6f03 --- /dev/null +++ b/color-eyre/tests/bt_enabled.rs @@ -0,0 +1,15 @@ +use color_eyre::eyre; +use eyre::eyre; + +#[test] +fn enabled() { + color_eyre::config::HookBuilder::default() + .display_env_section(true) + .install() + .unwrap(); + + let report = eyre!("error occured"); + + let report = format!("{:?}", report); + assert!(report.contains("RUST_BACKTRACE")); +} diff --git a/color-eyre/tests/data/theme_error_control.txt b/color-eyre/tests/data/theme_error_control.txt new file mode 100644 index 0000000..573fb79 --- /dev/null +++ b/color-eyre/tests/data/theme_error_control.txt @@ -0,0 +1,58 @@ + + 0: test + +Location: + tests/theme.rs:17 + +Error: + 0: error + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + 0: theme::get_error with msg="test" + at tests/theme.rs:11 + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +  ⋮ 5 frames hidden ⋮  + 6: theme::get_error::create_report::hdb41452bef3fc05d + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:17 + 7: theme::get_error::{{closure}}::h739c7fe800e2d03f + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:25 + 8: core::option::Option::ok_or_else::hd8e670bbca63e94a + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/option.rs:954 + 9: theme::get_error::h2f751f4927c6fecb + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:25 + 10: theme::test_error_backwards_compatibility::hfc4be9f22c32535c + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:43 + 11: theme::test_error_backwards_compatibility::{{closure}}::hb001a9a908f0f5a4 + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:41 + 12: core::ops::function::FnOnce::call_once::he26938a69d361bf6 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/ops/function.rs:227 + 13: core::ops::function::FnOnce::call_once::h83cc023b85256d97 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/ops/function.rs:227 + 14: test::__rust_begin_short_backtrace::h7330e4e8b0549e26 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/test/src/lib.rs:585 + 15:  as core::ops::function::FnOnce>::call_once::h6b77566b8f386abb + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/alloc/src/boxed.rs:1691 + 16:  as core::ops::function::FnOnce<()>>::call_once::h2ad5de64df41b71c + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/panic/unwind_safe.rs:271 + 17: std::panicking::try::do_call::he67b1e56b423a618 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panicking.rs:403 + 18: std::panicking::try::ha9224adcdd41a723 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panicking.rs:367 + 19: std::panic::catch_unwind::h9111b58ae0b27828 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panic.rs:133 + 20: test::run_test_in_process::h15b6b7d5919893aa + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/test/src/lib.rs:608 + 21: test::run_test::{{closure}}::h8ef02d13d4506b7f + at /rustc/7b4d9e155fec06583c763f176fc432dc779f1fc6/library/test/src/lib.rs:572 + 22: test::run_test::{{closure}}::hcd7b423365d0ff7e + at /rustc/7b4d9e155fec06583c763f176fc432dc779f1fc6/library/test/src/lib.rs:600 +  ⋮ 13 frames hidden ⋮  + +Note: note +Warning: warning +Suggestion: suggestion + +Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering. +Run with RUST_BACKTRACE=full to include source snippets. diff --git a/color-eyre/tests/data/theme_error_control_location.txt b/color-eyre/tests/data/theme_error_control_location.txt new file mode 100644 index 0000000..19fecfa --- /dev/null +++ b/color-eyre/tests/data/theme_error_control_location.txt @@ -0,0 +1,53 @@ + + 0: test + +Location: + tests/theme.rs:17 + +Error: + 0: error + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +  ⋮ 5 frames hidden ⋮  + 6: theme::get_error::create_report::hf800a973f2100b44 + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:17 + 7: theme::get_error::{{closure}}::ha65156cf9648d3e0 + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:25 + 8: core::option::Option::ok_or_else::h08df66cff4c7bff2 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/option.rs:954 + 9: theme::get_error::h7c1fce8fa3550ff9 + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:25 + 10: theme::test_error_backwards_compatibility::h732311d7da5d7160 + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:43 + 11: theme::test_error_backwards_compatibility::{{closure}}::h144cea82038adfc7 + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:41 + 12: core::ops::function::FnOnce::call_once::h8d0ee3b0b70ed418 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/ops/function.rs:227 + 13: core::ops::function::FnOnce::call_once::h83cc023b85256d97 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/ops/function.rs:227 + 14: test::__rust_begin_short_backtrace::h7330e4e8b0549e26 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/test/src/lib.rs:585 + 15:  as core::ops::function::FnOnce>::call_once::h6b77566b8f386abb + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/alloc/src/boxed.rs:1691 + 16:  as core::ops::function::FnOnce<()>>::call_once::h2ad5de64df41b71c + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/panic/unwind_safe.rs:271 + 17: std::panicking::try::do_call::he67b1e56b423a618 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panicking.rs:403 + 18: std::panicking::try::ha9224adcdd41a723 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panicking.rs:367 + 19: std::panic::catch_unwind::h9111b58ae0b27828 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panic.rs:133 + 20: test::run_test_in_process::h15b6b7d5919893aa + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/test/src/lib.rs:608 + 21: test::run_test::{{closure}}::h8ef02d13d4506b7f + at /rustc/7b4d9e155fec06583c763f176fc432dc779f1fc6/library/test/src/lib.rs:572 + 22: test::run_test::{{closure}}::hcd7b423365d0ff7e + at /rustc/7b4d9e155fec06583c763f176fc432dc779f1fc6/library/test/src/lib.rs:600 +  ⋮ 13 frames hidden ⋮  + +Note: note +Warning: warning +Suggestion: suggestion + +Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering. +Run with RUST_BACKTRACE=full to include source snippets. diff --git a/color-eyre/tests/data/theme_error_control_minimal.txt b/color-eyre/tests/data/theme_error_control_minimal.txt new file mode 100644 index 0000000..9a0cff8 --- /dev/null +++ b/color-eyre/tests/data/theme_error_control_minimal.txt @@ -0,0 +1,46 @@ + + 0: test + +Error: + 0: error + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +  ⋮ 5 frames hidden ⋮  + 6: theme::get_error::create_report::h43540daddae98383 + at /home/username/dev/rust/eyre/color-eyre/tests/theme.rs:17 + 7: theme::get_error::{{closure}}::h40bbef2f4cd93fab + at /home/username/dev/rust/eyre/color-eyre/tests/theme.rs:26 + 8: core::option::Option::ok_or_else::h8aa47839ff49cfbe + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/option.rs:1087 + 9: theme::get_error::h78b5b4d52bfbbad0 + at /home/username/dev/rust/eyre/color-eyre/tests/theme.rs:26 + 10: theme::test_error_backwards_compatibility::h9de398ce80defffa + at /home/username/dev/rust/eyre/color-eyre/tests/theme.rs:45 + 11: theme::test_error_backwards_compatibility::{{closure}}::hbe7b8ad2562c4dc4 + at /home/username/dev/rust/eyre/color-eyre/tests/theme.rs:43 + 12: core::ops::function::FnOnce::call_once::hfc715417a1b707c5 + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248 + 13: core::ops::function::FnOnce::call_once::h9ee1367930602049 + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248 + 14: test::__rust_begin_short_backtrace::h35061c5e0f5ad5d6 + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/test/src/lib.rs:572 + 15:  as core::ops::function::FnOnce>::call_once::h98fe3dd14bfe63ea + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/alloc/src/boxed.rs:1940 + 16:  as core::ops::function::FnOnce<()>>::call_once::h3ab012fb764e8d57 + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panic/unwind_safe.rs:271 + 17: std::panicking::try::do_call::h810a5ea64fd04126 + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:492 + 18: std::panicking::try::h0b213f9a8c1fe629 + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:456 + 19: std::panic::catch_unwind::h00f746771ade371f + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panic.rs:137 + 20: test::run_test_in_process::h5645647f0d0a3da3 + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/test/src/lib.rs:595 +  ⋮ 15 frames hidden ⋮  + +Note: note +Warning: warning +Suggestion: suggestion + +Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering. +Run with RUST_BACKTRACE=full to include source snippets. diff --git a/color-eyre/tests/data/theme_error_control_spantrace.txt b/color-eyre/tests/data/theme_error_control_spantrace.txt new file mode 100644 index 0000000..70635cc --- /dev/null +++ b/color-eyre/tests/data/theme_error_control_spantrace.txt @@ -0,0 +1,55 @@ + + 0: test + +Error: + 0: error + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + 0: theme::get_error with msg="test" + at tests/theme.rs:11 + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +  ⋮ 5 frames hidden ⋮  + 6: theme::get_error::create_report::h4bc625c000e4636e + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:17 + 7: theme::get_error::{{closure}}::h3dee499015f52230 + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:25 + 8: core::option::Option::ok_or_else::h32a80642d5f9cd65 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/option.rs:954 + 9: theme::get_error::hb3756d9f0d65527f + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:25 + 10: theme::test_error_backwards_compatibility::h69192dd92f3a8a2e + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:43 + 11: theme::test_error_backwards_compatibility::{{closure}}::hd9459c2e516ade18 + at /home/jlusby/git/yaahc/color-eyre/tests/theme.rs:41 + 12: core::ops::function::FnOnce::call_once::h540507413fe72275 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/ops/function.rs:227 + 13: core::ops::function::FnOnce::call_once::h83cc023b85256d97 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/ops/function.rs:227 + 14: test::__rust_begin_short_backtrace::h7330e4e8b0549e26 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/test/src/lib.rs:585 + 15:  as core::ops::function::FnOnce>::call_once::h6b77566b8f386abb + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/alloc/src/boxed.rs:1691 + 16:  as core::ops::function::FnOnce<()>>::call_once::h2ad5de64df41b71c + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/panic/unwind_safe.rs:271 + 17: std::panicking::try::do_call::he67b1e56b423a618 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panicking.rs:403 + 18: std::panicking::try::ha9224adcdd41a723 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panicking.rs:367 + 19: std::panic::catch_unwind::h9111b58ae0b27828 + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panic.rs:133 + 20: test::run_test_in_process::h15b6b7d5919893aa + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/test/src/lib.rs:608 + 21: test::run_test::{{closure}}::h8ef02d13d4506b7f + at /rustc/7b4d9e155fec06583c763f176fc432dc779f1fc6/library/test/src/lib.rs:572 + 22: test::run_test::{{closure}}::hcd7b423365d0ff7e + at /rustc/7b4d9e155fec06583c763f176fc432dc779f1fc6/library/test/src/lib.rs:600 +  ⋮ 13 frames hidden ⋮  + +Note: note +Warning: warning +Suggestion: suggestion + +Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering. +Run with RUST_BACKTRACE=full to include source snippets. diff --git a/color-eyre/tests/data/theme_panic_control.txt b/color-eyre/tests/data/theme_panic_control.txt new file mode 100644 index 0000000..fe925b4 --- /dev/null +++ b/color-eyre/tests/data/theme_panic_control.txt @@ -0,0 +1,23 @@ + Finished dev [unoptimized + debuginfo] target(s) in 0.03s + Running `target/debug/examples/theme_test_helper` +The application panicked (crashed). +Message:  +Location: examples/theme_test_helper.rs:37 + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + 0: theme_test_helper::get_error with msg="test" + at examples/theme_test_helper.rs:34 + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +  ⋮ 6 frames hidden ⋮  + 7: std::panic::panic_any::hd76a7f826307234c + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panic.rs:57 + 8: theme_test_helper::main::h767d3fd6c45048c8 + at /home/jlusby/git/yaahc/color-eyre/examples/theme_test_helper.rs:37 + 9: core::ops::function::FnOnce::call_once::hc5a1cd4127189dad + at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/ops/function.rs:227 +  ⋮ 15 frames hidden ⋮  + +Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering. +Run with RUST_BACKTRACE=full to include source snippets. diff --git a/color-eyre/tests/data/theme_panic_control_no_spantrace.txt b/color-eyre/tests/data/theme_panic_control_no_spantrace.txt new file mode 100644 index 0000000..48cef64 --- /dev/null +++ b/color-eyre/tests/data/theme_panic_control_no_spantrace.txt @@ -0,0 +1,18 @@ + Finished dev [unoptimized + debuginfo] target(s) in 0.07s + Running `/home/username/dev/rust/eyre/target/debug/examples/theme_test_helper` +The application panicked (crashed). +Message:  +Location: color-eyre/examples/theme_test_helper.rs:38 + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +  ⋮ 6 frames hidden ⋮  + 7: std::panic::panic_any::h4a05c03c4d0c389c + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panic.rs:61 + 8: theme_test_helper::main::hfc653b28cad3659d + at /home/username/dev/rust/eyre/color-eyre/examples/theme_test_helper.rs:38 + 9: core::ops::function::FnOnce::call_once::hb0110cdf4417a5ed + at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248 +  ⋮ 16 frames hidden ⋮  + +Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering. +Run with RUST_BACKTRACE=full to include source snippets. diff --git a/color-eyre/tests/install.rs b/color-eyre/tests/install.rs new file mode 100644 index 0000000..d5e85b2 --- /dev/null +++ b/color-eyre/tests/install.rs @@ -0,0 +1,7 @@ +use color_eyre::install; + +#[test] +fn double_install_should_not_panic() { + install().unwrap(); + assert!(install().is_err()); +} diff --git a/color-eyre/tests/location_disabled.rs b/color-eyre/tests/location_disabled.rs new file mode 100644 index 0000000..7eee57c --- /dev/null +++ b/color-eyre/tests/location_disabled.rs @@ -0,0 +1,16 @@ +#[cfg(feature = "track-caller")] +#[test] +fn disabled() { + use color_eyre::eyre; + use eyre::eyre; + + color_eyre::config::HookBuilder::default() + .display_location_section(false) + .install() + .unwrap(); + + let report = eyre!("error occured"); + + let report = format!("{:?}", report); + assert!(!report.contains("Location:")); +} diff --git a/color-eyre/tests/theme.rs b/color-eyre/tests/theme.rs new file mode 100644 index 0000000..939b02f --- /dev/null +++ b/color-eyre/tests/theme.rs @@ -0,0 +1,297 @@ +// Note: It's recommended, not to change anything above or below (see big comment below) + +use color_eyre::{eyre::Report, Section}; + +#[rustfmt::skip] +#[derive(Debug, thiserror::Error)] +#[error("{0}")] +struct TestError(&'static str); + +#[rustfmt::skip] +#[tracing::instrument] +fn get_error(msg: &'static str) -> Report { + + #[rustfmt::skip] + #[inline(never)] + fn create_report(msg: &'static str) -> Report { + Report::msg(msg) + .note("note") + .warning("warning") + .suggestion("suggestion") + .error(TestError("error")) + } + + // Using `Option` to trigger `is_dependency_code`. + // See https://github.com/eyre-rs/color-eyre/blob/4ddaeb2126ed8b14e4e6aa03d7eef49eb8561cf0/src/config.rs#L56 + None::>.ok_or_else(|| create_report(msg)).unwrap_err() +} + +#[cfg(all(not(feature = "track-caller"), not(feature = "capture-spantrace"),))] +static ERROR_FILE_NAME: &str = "theme_error_control_minimal.txt"; + +#[cfg(all(feature = "track-caller", not(feature = "capture-spantrace"),))] +static ERROR_FILE_NAME: &str = "theme_error_control_location.txt"; + +#[cfg(all(not(feature = "track-caller"), feature = "capture-spantrace",))] +static ERROR_FILE_NAME: &str = "theme_error_control_spantrace.txt"; + +#[cfg(all(feature = "capture-spantrace", feature = "track-caller",))] +static ERROR_FILE_NAME: &str = "theme_error_control.txt"; + +#[test] +#[cfg(not(miri))] +fn test_error_backwards_compatibility() { + setup(); + let error = get_error("test"); + + /* + Note: If you change anything above this comment, it could make the stored test data invalid (because the structure of the generated error might change). In most cases, small changes shouldn't be a problem, but keep this in mind if you change something and suddenly this test case fails. + + The empty lines at the beginning are needed because `color_eyre` sometimes seems to not be able to find the correct line of source and uses the first line of the module (plus the next four lines). + + If a change of the code above leads to incompatibility, you therefore have to backport this (changed) file to the version of `color_eyre` that you want to test against and execute it to generate new control test data. + + To do this, do the following: + + 1) Change this file, and if the test now fails do: + + 2) Checkout the `color_eyre` version from Git that you want to test against + + 3) Add this test file to '/tests' + + 4) If `error_file_path` or `panic_file_path` exist (see below), delete these files + + 5) If you now run this test, it will fail and generate test data files in the current working directory + + 6) copy these files to `error_file_path` and `panic_file_path` in the current version of `color_eyre` (see the instructions that are printed out in step 5) + + Now this test shouldn't fail anymore in the current version. + + Alternatively, you also could just regenerate the test data of the current repo (as described above, but without backporting), and use this test data from now on (this makes sense, if you only changed the above code, and nothing else that could lead to the test failing). + + + # How the tests in this file work: + + 1) generate a error (for example, with the code above) + + 2) convert this error to a string + + 3) load stored error data to compare to (stored in `error_file_path` and `panic_file_path`) + + 4) if `error_file_path` and/or `panic_file_path` doesn't exist, generate corresponding files in the current working directory and request the user to fix the issue (see below) + + 5) extract ANSI escaping sequences (of controls and current errors) + + 6) compare if the current error and the control contains the same ANSI escape sequences + + 7) If not, fail and show the full strings of the control and the current error + + Below you'll find instructions about how to debug failures of the tests in this file + */ + + let target = format!("{:?}", error); + test_backwards_compatibility(target, ERROR_FILE_NAME) +} + +#[cfg(not(feature = "capture-spantrace"))] +static PANIC_FILE_NAME: &str = "theme_panic_control_no_spantrace.txt"; + +#[cfg(feature = "capture-spantrace")] +static PANIC_FILE_NAME: &str = "theme_panic_control.txt"; + +// The following tests the installed panic handler +#[test] +#[allow(unused_mut)] +#[allow(clippy::vec_init_then_push)] +#[cfg(not(miri))] +fn test_panic_backwards_compatibility() { + let mut features: Vec<&str> = vec![]; + #[cfg(feature = "capture-spantrace")] + features.push("capture-spantrace"); + #[cfg(feature = "issue-url")] + features.push("issue-url"); + #[cfg(feature = "track-caller")] + features.push("track-caller"); + + let features = features.join(","); + let features = if !features.is_empty() { + vec!["--features", &features] + } else { + vec![] + }; + + let output = std::process::Command::new("cargo") + .args(["run", "--example", "theme_test_helper"]) + .arg("--no-default-features") + .args(&features) + .output() + .expect("failed to execute process"); + let target = String::from_utf8(output.stderr).expect("failed to convert output to `String`"); + println!("{}", target); + test_backwards_compatibility(target, PANIC_FILE_NAME) +} + +/// Helper for `test_error` and `test_panic` +fn test_backwards_compatibility(target: String, file_name: &str) { + use ansi_parser::{AnsiParser, AnsiSequence, Output}; + use owo_colors::OwoColorize; + use std::{fs, path::Path}; + + let file_path = ["tests/data/", file_name].concat(); + + // If `file_path` is missing, save corresponding file to current working directory, and panic with the request to move the file to `file_path`, and to commit it to Git. Being explicit (instead of saving directly to `file_path`) to make sure `file_path` is committed to Git. + + if !Path::new(&file_path).is_file() { + std::fs::write(file_name, &target) + .expect("\n\nError saving missing `control target` to a file"); + panic!("Required test data missing! Fix this, by moving '{}' to '{}', and commit it to Git.\n\nNote: '{0}' was just generated in the current working directory.\n\n", file_name, file_path); + } + + // `unwrap` should never fail with files generated by this function + let control = String::from_utf8(fs::read(file_path).unwrap()).unwrap(); + + fn split_ansi_output(input: &str) -> (Vec, Vec) { + let all: Vec<_> = input.ansi_parse().collect(); + let ansi: Vec<_> = input + .ansi_parse() + .filter_map(|x| { + if let Output::Escape(ansi) = x { + Some(ansi) + } else { + None + } + }) + .collect(); + (all, ansi) + } + + fn normalize_backtrace(input: &str) -> String { + input + .lines() + .take_while(|v| !v.contains("core::panic")) + .collect::>() + .join("\n") + } + + let control = normalize_backtrace(&control); + let target = normalize_backtrace(&target); + let (_control_tokens, control_ansi) = split_ansi_output(&control); + let (_target_tokens, target_ansi) = split_ansi_output(&target); + + fn section(title: &str, content: impl AsRef) -> String { + format!( + "{}\n{}", + format!("-------- {title} --------").red(), + content.as_ref() + ) + } + + // pretty_assertions::assert_eq!(target, control); + let msg = [ + // comment out / un-comment what you need or don't need for debugging (see below for more instructions): + + format!("{}", "\x1b[0m\n\nANSI escape sequences are not identical to control!".red()), + // ^ `\x1b[0m` clears previous ANSI escape sequences + + section("CONTROL STRING", &control), + // section("CONTROL DEBUG STRING", format!("{control:?}")), + // section("CONTROL ANSI PARSER OUTPUT", format!("{_control_tokens:?}")), + // section("CONTROL ANSI PARSER ANSI", format!("{control_ansi:?}")), + + section("CURRENT STRING", &target), + // section("CURRENT DEBUG STRING", format!("{target:?}")), + // section("CURRENT ANSI PARSER OUTPUT", format!("{_target_tokens:?}")), + // section("CURRENT ANSI PARSER ANSI", format!("{target_ansi:?}")), + + format!("{}", "See the src of this test for more information about the test and ways to include/exclude debugging information.\n\n".red()), + + ].join("\n\n"); + + pretty_assertions::assert_eq!(target_ansi, control_ansi, "{}", &msg); + + /* + # Tips for debugging test failures + + It's a bit a pain to find the reason for test failures. To make it as easy as possible, I recommend the following workflow: + + ## Compare the actual errors + + 1) Run the test in two terminals with "CONTROL STRING" and "CURRENT STRING" active + + 2) In on terminal have the output of "CONTROL STRING" visible, in the out that of "CURRENT STRING" + + 3) Make sure, both errors are at the same location of their terminal + + 4) Now switch between the two terminal rapidly and often. This way it's easy to see changes. + + Note that we only compare ANSI escape sequences – so if the text changes, that is not a problem. + + A problem would it be, if there is a new section of content (which might contain new ANSI escape sequences). This could happen, for example, if the current code produces warnings, etc. (especially, with the panic handler test). + + ## Compare `ansi_parser` tokens + + If you fixed all potential problems above, and the test still failes, compare the actual ANSI escape sequences: + + 1) Activate "CURRENT ANSI PARSER OUTPUT" and "CURRENT ANSI PARSER OUTPUT" above + + 2) Copy this output to a text editor and replace all "), " with ")," + a newline (this way every token is on its own line) + + 3) Compare this new output with a diff tool (https://meldmerge.org/ is a nice option with a GUI) + + With this approach, you should see what has changed. Just remember that we only compare the ANSI escape sequences, text is skipped. With "CURRENT ANSI PARSER OUTPUT" and "CURRENT ANSI PARSER OUTPUT", however, text tokens are shown as well (to make it easier to figure out the source of ANSI escape sequences.) + */ +} + +fn setup() { + std::env::set_var("RUST_LIB_BACKTRACE", "1"); + + #[cfg(feature = "capture-spantrace")] + { + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; + + let fmt_layer = fmt::layer().with_target(false); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(tracing_error::ErrorLayer::default()) + .init(); + } + + color_eyre::install().expect("Failed to install `color_eyre`"); + + /* + # Easy way to test styles + + 1) uncomment the last line + + 2) activate the following code + + 3) change the styles + + 4) run this test via `cargo test test_error_backwards_compatibility --test styles` + + 5) your new styled error will be below the output "CURRENT STRING =" + + 6) if there is not such output, search for "CURRENT STRING =" above, and activate the line + + 7) if you are interested in running this test for actual testing this crate, don't forget to uncomment the code below, activate the above line + */ + + /* + use owo_colors::style; + let styles = color_eyre::config::Styles::dark() + // ^ or, instead of `dark`, use `new` for blank styles or `light` if you what to derive from a light theme. Now configure your styles (see the docs for all options): + .line_number(style().blue()) + .help_info_suggestion(style().red()); + + color_eyre::config::HookBuilder::new() + .styles(styles) + .install() + .expect("Failed to install `color_eyre`"); + */ +} diff --git a/color-eyre/tests/wasm.rs b/color-eyre/tests/wasm.rs new file mode 100644 index 0000000..feed296 --- /dev/null +++ b/color-eyre/tests/wasm.rs @@ -0,0 +1,23 @@ +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test::wasm_bindgen_test] +pub fn color_eyre_simple() { + use color_eyre::eyre::WrapErr; + use color_eyre::*; + + install().expect("Failed to install color_eyre"); + let err_str = format!( + "{:?}", + Err::<(), Report>(eyre::eyre!("Base Error")) + .note("A note") + .suggestion("A suggestion") + .wrap_err("A wrapped error") + .unwrap_err() + ); + // Print it out so if people run with `-- --nocapture`, they + // can see the full message. + println!("Error String is:\n\n{}", err_str); + assert!(err_str.contains("A wrapped error")); + assert!(err_str.contains("A suggestion")); + assert!(err_str.contains("A note")); + assert!(err_str.contains("Base Error")); +} diff --git a/color-spantrace/README.md b/color-spantrace/README.md index 7f4594b..7205e61 100644 --- a/color-spantrace/README.md +++ b/color-spantrace/README.md @@ -1,12 +1,15 @@ -color-spantrace -=============== +# color-spantrace [![Build Status][actions-badge]][actions-url] -[![Latest Version](https://img.shields.io/crates/v/color-spantrace.svg)](https://crates.io/crates/color-spantrace) -[![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/color-spantrace) - -[actions-badge]: https://github.com/eyre-rs/color-spantrace/workflows/Continuous%20integration/badge.svg -[actions-url]: https://github.com/eyre-rs/color-spantrace/actions?query=workflow%3A%22Continuous+integration%22 +[![Latest Version][version-badge]][version-url] +[![Rust Documentation][docs-badge]][docs-url] + +[actions-badge]: https://github.com/eyre-rs/eyre/workflows/Continuous%20integration/badge.svg +[actions-url]: https://github.com/eyre-rs/eyre/actions?query=workflow%3A%22Continuous+integration%22 +[version-badge]: https://img.shields.io/crates/v/color-spantrace.svg +[version-url]: https://crates.io/crates/color-spantrace +[docs-badge]: https://img.shields.io/badge/docs-latest-blue.svg +[docs-url]: https://docs.rs/color-spantrace A rust library for colorizing [`tracing_error::SpanTrace`] objects in the style of [`color-backtrace`].