From 1816b06a2efcd5705dfe91147ab5651fe0b517d6 Mon Sep 17 00:00:00 2001 From: Benjamin Lee Date: Fri, 24 Jun 2022 08:30:36 -0700 Subject: [PATCH] feat(theme): restructure automatic color selection (#177) Fixes: https://github.com/zkat/miette/issues/176 Change the default ansi color theme to use colors that are more similar to the colors from the default RGB theme. In particular, don't use red for any of the span labels, since that color is also used for errors. BREAKING CHANGES: * the default theme now prefers ANSI colors, even if RGB is supported * `MietteHandlerOpts::ansi_colors` is removed * `MietteHandlerOpts::rgb_color` now takes an enum that controls the color format used when color support is enabled, and has no effect otherwise. --- Cargo.toml | 2 + src/handler.rs | 69 ++++++++++++++------- src/handlers/theme.rs | 4 +- tests/color_format.rs | 137 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 188 insertions(+), 24 deletions(-) create mode 100644 tests/color_format.rs diff --git a/Cargo.toml b/Cargo.toml index 70b0994..b423f83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,8 @@ indenter = "0.3.0" rustversion = "1.0" trybuild = { version = "1.0.19", features = ["diff"] } syn = { version = "1.0", features = ["full"] } +regex = "1.5" +lazy_static = "1.4" [features] default = [] diff --git a/src/handler.rs b/src/handler.rs index 74de008..a6e20da 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -10,6 +10,23 @@ use crate::ReportHandler; use crate::ThemeCharacters; use crate::ThemeStyles; +/// Settings to control the color format used for graphical rendering. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum RgbColors { + /// Use RGB colors even if the terminal does not support them + Always, + /// Use RGB colors instead of ANSI if the terminal supports RGB + Preferred, + /// Always use ANSI, regardless of terminal support for RGB + Never, +} + +impl Default for RgbColors { + fn default() -> RgbColors { + RgbColors::Never + } +} + /** Create a custom [`MietteHandler`] from options. @@ -33,8 +50,7 @@ pub struct MietteHandlerOpts { 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) rgb_colors: RgbColors, pub(crate) color: Option, pub(crate) unicode: Option, pub(crate) footer: Option, @@ -71,16 +87,31 @@ impl MietteHandlerOpts { self } - /// If true, colors will be used during graphical rendering. Actual color - /// format will be auto-detected. + /// If true, colors will be used during graphical rendering, regardless + /// of whether or not the terminal supports them. + /// + /// If false, colors will never be used. + /// + /// If unspecified, colors will be used only if the terminal supports them. + /// + /// The actual format depends on the value of + /// [`MietteHandlerOpts::rgb_colors`]. 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); + /// Controls which color format to use if colors are used in graphical + /// rendering. + /// + /// The default is `Never`. + /// + /// This value does not control whether or not colors are being used in the + /// first place. That is handled by the [`MietteHandlerOpts::color`] + /// setting. If colors are not being used, the value of `rgb_colors` has + /// no effect. + pub fn rgb_colors(mut self, color: RgbColors) -> Self { + self.rgb_colors = color; self } @@ -91,11 +122,6 @@ impl MietteHandlerOpts { self } - /// If true, ANSI colors will be used during graphical rendering. - pub fn ansi_colors(mut self, color: bool) -> Self { - self.ansi_colors = Some(color); - self - } /// If true, graphical rendering will be used regardless of terminal /// detection. pub fn force_graphical(mut self, force: bool) -> Self { @@ -152,18 +178,17 @@ impl MietteHandlerOpts { }; let styles = if self.color == Some(false) { ThemeStyles::none() - } else 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 let Some(color) = supports_color::on(Stream::Stderr) { + match self.rgb_colors { + RgbColors::Always => ThemeStyles::rgb(), + RgbColors::Preferred if color.has_16m => ThemeStyles::rgb(), + _ => ThemeStyles::ansi(), } } else if self.color == Some(true) { - ThemeStyles::ansi() + match self.rgb_colors { + RgbColors::Always => ThemeStyles::rgb(), + _ => ThemeStyles::ansi(), + } } else { ThemeStyles::none() }; diff --git a/src/handlers/theme.rs b/src/handlers/theme.rs index 8d1d1c5..e76a44d 100644 --- a/src/handlers/theme.rs +++ b/src/handlers/theme.rs @@ -125,9 +125,9 @@ impl ThemeStyles { link: style().cyan().underline().bold(), linum: style().dimmed(), highlights: vec![ - style().red().bold(), + style().magenta().bold(), style().yellow().bold(), - style().cyan().bold(), + style().green().bold(), ], } } diff --git a/tests/color_format.rs b/tests/color_format.rs new file mode 100644 index 0000000..bb90708 --- /dev/null +++ b/tests/color_format.rs @@ -0,0 +1,137 @@ +#![cfg(feature = "fancy-no-backtrace")] + +use lazy_static::lazy_static; +use miette::{Diagnostic, MietteHandler, MietteHandlerOpts, ReportHandler, RgbColors}; +use regex::Regex; +use std::fmt::{self, Debug}; +use std::sync::Mutex; +use thiserror::Error; + +#[derive(Eq, PartialEq, Debug)] +enum ColorFormat { + NoColor, + Ansi, + Rgb, +} + +#[derive(Debug, Diagnostic, Error)] +#[error("oops!")] +#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))] +struct MyBad; + +struct FormatTester(MietteHandler); + +impl Debug for FormatTester { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.debug(&MyBad, f) + } +} + +/// Check the color format used by a handler. +fn color_format(handler: MietteHandler) -> ColorFormat { + let out = format!("{:?}", FormatTester(handler)); + + let rgb_colors = Regex::new(r"\u{1b}\[[34]8;2;").unwrap(); + let ansi_colors = Regex::new(r"\u{1b}\[(3|4|9|10)[0-7][m;]").unwrap(); + if rgb_colors.is_match(&out) { + ColorFormat::Rgb + } else if ansi_colors.is_match(&out) { + ColorFormat::Ansi + } else { + ColorFormat::NoColor + } +} + +/// Runs a function with an environment variable set to a specific value, then +/// sets it back to it's original value once completed. +fn with_env_var(var: &str, value: &str, body: F) { + let old_value = std::env::var_os(var); + std::env::set_var(var, value); + body(); + if let Some(old_value) = old_value { + std::env::set_var(var, old_value); + } else { + std::env::remove_var(var); + } +} + +lazy_static! { + static ref COLOR_ENV_VARS: Mutex<()> = Mutex::new(()); +} + +/// Assert the color format used by a handler with different levels of terminal +/// support. +fn check_colors MietteHandlerOpts>( + make_handler: F, + no_support: ColorFormat, + ansi_support: ColorFormat, + rgb_support: ColorFormat, +) { + // To simulate different levels of terminal support we're using specific + // environment variables that are handled by the supports_color crate. + // + // Since environment variables are shared for the entire process, we need + // to ensure that only one test that modifies these env vars runs at a time. + let guard = COLOR_ENV_VARS.lock().unwrap(); + + with_env_var("NO_COLOR", "1", || { + let handler = make_handler(MietteHandlerOpts::new()).build(); + assert_eq!(color_format(handler), no_support); + }); + with_env_var("FORCE_COLOR", "1", || { + let handler = make_handler(MietteHandlerOpts::new()).build(); + assert_eq!(color_format(handler), ansi_support); + }); + with_env_var("FORCE_COLOR", "3", || { + let handler = make_handler(MietteHandlerOpts::new()).build(); + assert_eq!(color_format(handler), rgb_support); + }); + + drop(guard); +} + +#[test] +fn no_color_preference() { + use ColorFormat::*; + check_colors(|opts| opts, NoColor, Ansi, Ansi); +} + +#[test] +fn color_never() { + use ColorFormat::*; + check_colors(|opts| opts.color(false), NoColor, NoColor, NoColor); +} + +#[test] +fn color_always() { + use ColorFormat::*; + check_colors(|opts| opts.color(true), Ansi, Ansi, Ansi); +} + +#[test] +fn rgb_preferred() { + use ColorFormat::*; + check_colors( + |opts| opts.rgb_colors(RgbColors::Preferred), + NoColor, + Ansi, + Rgb, + ); +} + +#[test] +fn rgb_always() { + use ColorFormat::*; + check_colors(|opts| opts.rgb_colors(RgbColors::Always), NoColor, Rgb, Rgb); +} + +#[test] +fn color_always_rgb_always() { + use ColorFormat::*; + check_colors( + |opts| opts.color(true).rgb_colors(RgbColors::Always), + Rgb, + Rgb, + Rgb, + ); +}