From 3d74a500c3193fb1dff26591191a67eaab079671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 11 Sep 2021 10:58:12 -0700 Subject: [PATCH] feat(report): make a single big MietteHandler that can switch modes BREAKING CHANGE: linkification option method on GraphicalReportHandler has been changed to .with_links(bool) --- Cargo.toml | 3 + src/eyreish/mod.rs | 17 +--- src/handler.rs | 206 ++++++++++++++++++++++++++++++++++++++ src/handlers/graphical.rs | 8 +- src/lib.rs | 2 + tests/printer.rs | 2 +- 6 files changed, 218 insertions(+), 20 deletions(-) create mode 100644 src/handler.rs diff --git a/Cargo.toml b/Cargo.toml index 225b670..2f178e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,9 @@ ci_info = "0.14.2" textwrap = "0.14.2" term_size = "0.3.2" unicode-width = "0.1.8" +supports-hyperlinks = "1.1.0" +supports-color = "1.0.2" +supports-unicode = "1.0.0" [dev-dependencies] semver = "1.0.4" diff --git a/src/eyreish/mod.rs b/src/eyreish/mod.rs index db1e563..aeedc0b 100644 --- a/src/eyreish/mod.rs +++ b/src/eyreish/mod.rs @@ -9,7 +9,6 @@ use core::mem::ManuallyDrop; use std::error::Error as StdError; -use atty::Stream; use once_cell::sync::OnceCell; #[allow(unreachable_pub)] @@ -27,7 +26,7 @@ pub use ReportHandler as EyreContext; #[allow(unreachable_pub)] pub use WrapErr as Context; -use crate::{Diagnostic, GraphicalReportHandler, NarratableReportHandler}; +use crate::{Diagnostic, MietteHandler}; use error::ErrorImpl; mod context; @@ -88,19 +87,7 @@ fn capture_handler(error: &(dyn Diagnostic + 'static)) -> Box } fn get_default_printer(_err: &(dyn Diagnostic + 'static)) -> Box { - let fancy = if let Ok(string) = std::env::var("NO_COLOR") { - string == "0" - } else if let Ok(string) = std::env::var("CLICOLOR") { - string != "0" || string == "1" - } else { - atty::is(Stream::Stdout) && atty::is(Stream::Stderr) && !ci_info::is_ci() - }; - let size = term_size::dimensions().unwrap_or((80, 0)).0; - if fancy { - Box::new(GraphicalReportHandler::new().with_width(size)) - } else { - Box::new(NarratableReportHandler) - } + Box::new(MietteHandler::new()) } impl dyn ReportHandler { diff --git a/src/handler.rs b/src/handler.rs new file mode 100644 index 0000000..8165bdb --- /dev/null +++ b/src/handler.rs @@ -0,0 +1,206 @@ +use std::fmt; + +use atty::Stream; + +use crate::protocol::Diagnostic; +use crate::GraphicalReportHandler; +use crate::GraphicalTheme; +use crate::NarratableReportHandler; +use crate::ReportHandler; +use crate::ThemeCharacters; +use crate::ThemeStyles; + +/** +Create a custom [MietteHandler] from options. +*/ +#[derive(Default, Debug, Clone)] +pub struct MietteHandlerOpts { + pub(crate) linkify: Option, + pub(crate) width: Option, + pub(crate) theme: Option, + pub(crate) force_graphical: Option, + pub(crate) force_narrated: Option, + pub(crate) ansi_colors: Option, + pub(crate) rgb_colors: Option, + pub(crate) color: Option, + pub(crate) unicode: Option, +} + +impl MietteHandlerOpts { + /// Create a new [MietteHandlerOpts]. + pub fn new() -> Self { + Default::default() + } + + /// If true, specify whether the graphical handler will make codes be + /// clickable links in supported terminals. Defaults to auto-detection + /// based on known supported terminals. + pub fn terminal_links(mut self, linkify: bool) -> Self { + self.linkify = Some(linkify); + self + } + + /// Set a graphical theme for the handler when rendering in graphical + /// mode. Use [MietteHandlerOpts::force_graphical] to force graphical + /// mode. This option overrides [MietteHandlerOpts::color]. + pub fn graphical_theme(mut self, theme: GraphicalTheme) -> Self { + self.theme = Some(theme); + self + } + + /// Sets the width to wrap the report at. Defaults + pub fn width(mut self, width: usize) -> Self { + self.width = Some(width); + self + } + + /// If true, colors will be used during graphical rendering. Actual color + /// format will be auto-detected. + pub fn color(mut self, color: bool) -> Self { + self.color = Some(color); + self + } + + /// If true, RGB colors will be used during graphical rendering. + pub fn rgb_colors(mut self, color: bool) -> Self { + self.rgb_colors = Some(color); + self + } + + /// If true, forces unicode display for graphical output. If set to false, + /// forces ASCII art display. + pub fn unicode(mut self, unicode: bool) -> Self { + self.unicode = Some(unicode); + self + } + + /// If true, ANSI colors will be used during graphical rendering. + pub fn ansi_colors(mut self, color: bool) -> Self { + self.rgb_colors = Some(color); + self + } + /// If true, graphical rendering will be used regardless of terminal + /// detection. + pub fn force_graphical(mut self, force: bool) -> Self { + self.force_graphical = Some(force); + self + } + + /// If true, forces use of the narrated renderer. + pub fn force_narrated(mut self, force: bool) -> Self { + self.force_narrated = Some(force); + self + } + + /// Builds a [MietteHandler] from this builder. + pub fn build(self) -> MietteHandler { + let graphical = self.is_graphical(); + let width = self.get_width(); + if !graphical { + MietteHandler { + inner: Box::new(NarratableReportHandler::new()), + } + } else { + let linkify = self.use_links(); + let characters = match self.unicode { + Some(true) => ThemeCharacters::unicode(), + Some(false) => ThemeCharacters::ascii(), + None if supports_unicode::on(Stream::Stderr) => ThemeCharacters::unicode(), + None => ThemeCharacters::ascii(), + }; + let styles = if self.rgb_colors == Some(true) { + ThemeStyles::rgb() + } else if self.ansi_colors == Some(true) { + ThemeStyles::ansi() + } else if let Some(colors) = supports_color::on(Stream::Stderr) { + if colors.has_16m { + ThemeStyles::rgb() + } else { + ThemeStyles::ansi() + } + } else if self.color == Some(true) { + ThemeStyles::ansi() + } else { + ThemeStyles::none() + }; + MietteHandler { + inner: Box::new( + GraphicalReportHandler::new() + .with_width(width) + .with_links(linkify) + .with_theme(GraphicalTheme { characters, styles }), + ), + } + } + } + + pub(crate) fn is_graphical(&self) -> bool { + if let Some(force_narrated) = self.force_narrated { + !force_narrated + } else if let Some(force_graphical) = self.force_graphical { + force_graphical + } else if let Some(info) = supports_color::on(Stream::Stderr) { + info.has_basic + } else { + false + } + } + + // Detects known terminal apps based on env variables and returns true if + // they support rendering links. + pub(crate) fn use_links(&self) -> bool { + if let Some(linkify) = self.linkify { + linkify + } else { + supports_hyperlinks::on(Stream::Stderr) + } + } + + pub(crate) fn get_width(&self) -> usize { + self.width + .unwrap_or_else(|| term_size::dimensions().unwrap_or((80, 0)).0) + } +} + +/** +A [ReportHandler] that displays a given [crate::Report] in a quasi-graphical +way, using terminal colors, unicode drawing characters, and other such things. + +This is the default reporter bundled with `miette`. + +This printer can be customized by using `new_themed()` and handing it a +[GraphicalTheme] of your own creation (or using one of its own defaults!) + +See [crate::set_hook] for more details on customizing your global printer. +*/ +#[allow(missing_debug_implementations)] +pub struct MietteHandler { + inner: Box, +} + +impl MietteHandler { + /// Creates a new [MietteHandler] with default settings. + pub fn new() -> Self { + Default::default() + } +} + +impl Default for MietteHandler { + fn default() -> Self { + MietteHandlerOpts::new().build() + } +} + +impl ReportHandler for MietteHandler { + fn debug( + &self, + diagnostic: &(dyn Diagnostic + 'static), + f: &mut fmt::Formatter<'_>, + ) -> fmt::Result { + if f.alternate() { + return fmt::Debug::fmt(diagnostic, f); + } + + self.inner.debug(diagnostic, f) + } +} diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 5c52c16..92bf89b 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -46,9 +46,9 @@ impl GraphicalReportHandler { } } - /// Disables error code linkification using [Diagnostic::url]. - pub fn without_code_linking(mut self) -> Self { - self.linkify_code = false; + /// Whether to enable error code linkification using [Diagnostic::url]. + pub fn with_links(mut self, links: bool) -> Self { + self.linkify_code = links; self } @@ -274,7 +274,7 @@ impl GraphicalReportHandler { } else { self.theme.characters.ltop }, - self.theme.characters.hbar.to_string().repeat(1), + self.theme.characters.hbar, )?; if let Some(source_name) = snippet.source.name() { let source_name = source_name.style(self.theme.styles.filename); diff --git a/src/lib.rs b/src/lib.rs index ae62ad0..46825dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub use miette_derive::*; pub use error::*; pub use eyreish::*; +pub use handler::*; pub use handlers::*; pub use named_source::*; pub use protocol::*; @@ -13,6 +14,7 @@ pub use protocol::*; mod chain; mod error; mod eyreish; +mod handler; mod handlers; mod named_source; mod protocol; diff --git a/tests/printer.rs b/tests/printer.rs index df3fcc2..0d88213 100644 --- a/tests/printer.rs +++ b/tests/printer.rs @@ -596,7 +596,7 @@ fn disable_url_links() -> Result<(), MietteError> { let err = MyBad; let mut out = String::new(); GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor()) - .without_code_linking() + .with_links(false) .render_report(&mut out, &err) .unwrap(); println!("{}", out);