From b0744462adbbfbb6d845f382db36be883c7f3c45 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 4 Jan 2024 12:44:52 -0600 Subject: [PATCH 1/5] feat(graphical): Add `wrap_lines: bool` option allowing wrapping be disabled entirely (#328) --- src/handler.rs | 15 ++++++++++- src/handlers/graphical.rs | 54 +++++++++++++++++++++++++++++++++++---- tests/graphical.rs | 44 +++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index e32f3ef1..38c9fc84 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -56,6 +56,7 @@ pub struct MietteHandlerOpts { pub(crate) tab_width: Option, pub(crate) with_cause_chain: Option, pub(crate) break_words: Option, + pub(crate) wrap_lines: Option, pub(crate) word_separator: Option, pub(crate) word_splitter: Option, } @@ -89,6 +90,16 @@ impl MietteHandlerOpts { self } + /// If true, long lines can be wrapped. + /// + /// If false, long lines will not be broken when they exceed the width. + /// + /// Defaults to true. + pub fn wrap_lines(mut self, wrap_lines: bool) -> Self { + self.wrap_lines = Some(wrap_lines); + self + } + /// If true, long words can be broken when wrapping. /// /// If false, long words will not be broken when they exceed the width. @@ -98,7 +109,6 @@ impl MietteHandlerOpts { self.break_words = Some(break_words); self } - /// Sets the `textwrap::WordSeparator` to use when determining wrap points. pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self { self.word_separator = Some(word_separator); @@ -260,6 +270,9 @@ impl MietteHandlerOpts { if let Some(b) = self.break_words { handler = handler.with_break_words(b) } + if let Some(b) = self.wrap_lines { + handler = handler.with_wrap_lines(b) + } if let Some(s) = self.word_separator { handler = handler.with_word_separator(s) } diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 31934724..951303ab 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -30,6 +30,7 @@ pub struct GraphicalReportHandler { pub(crate) context_lines: usize, pub(crate) tab_width: usize, pub(crate) with_cause_chain: bool, + pub(crate) wrap_lines: bool, pub(crate) break_words: bool, pub(crate) word_separator: Option, pub(crate) word_splitter: Option, @@ -54,6 +55,7 @@ impl GraphicalReportHandler { context_lines: 1, tab_width: 4, with_cause_chain: true, + wrap_lines: true, break_words: true, word_separator: None, word_splitter: None, @@ -69,6 +71,7 @@ impl GraphicalReportHandler { footer: None, context_lines: 1, tab_width: 4, + wrap_lines: true, with_cause_chain: true, break_words: true, word_separator: None, @@ -131,6 +134,12 @@ impl GraphicalReportHandler { self } + /// Enables or disables wrapping of lines to fit the width. + pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self { + self.wrap_lines = wrap_lines; + self + } + /// Enables or disables breaking of words during wrapping. pub fn with_break_words(mut self, break_words: bool) -> Self { self.break_words = break_words; @@ -197,7 +206,7 @@ impl GraphicalReportHandler { opts = opts.word_splitter(word_splitter); } - writeln!(f, "{}", textwrap::fill(footer, opts))?; + writeln!(f, "{}", self.wrap(footer, opts))?; } Ok(()) } @@ -258,7 +267,7 @@ impl GraphicalReportHandler { opts = opts.word_splitter(word_splitter); } - writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?; + writeln!(f, "{}", self.wrap(&diagnostic.to_string(), opts))?; if !self.with_cause_chain { return Ok(()); @@ -314,10 +323,10 @@ impl GraphicalReportHandler { inner_renderer.with_cause_chain = false; inner_renderer.render_report(&mut inner, diag)?; - writeln!(f, "{}", textwrap::fill(&inner, opts))?; + writeln!(f, "{}", self.wrap(&inner, opts))?; } ErrorKind::StdError(err) => { - writeln!(f, "{}", textwrap::fill(&err.to_string(), opts))?; + writeln!(f, "{}", self.wrap(&err.to_string(), opts))?; } } } @@ -341,7 +350,7 @@ impl GraphicalReportHandler { opts = opts.word_splitter(word_splitter); } - writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?; + writeln!(f, "{}", self.wrap(&help.to_string(), opts))?; } Ok(()) } @@ -810,6 +819,41 @@ impl GraphicalReportHandler { Ok(()) } + fn wrap(&self, text: &str, opts: textwrap::Options<'_>) -> String { + if self.wrap_lines { + textwrap::fill(text, opts) + } else { + // Format without wrapping, but retain the indentation options + // Implementation based on `textwrap::indent` + let mut result = String::with_capacity(2 * text.len()); + let trimmed_indent = opts.subsequent_indent.trim_end(); + for (idx, line) in text.split_terminator('\n').enumerate() { + if idx > 0 { + result.push('\n'); + } + if idx == 0 { + if line.trim().is_empty() { + result.push_str(opts.initial_indent.trim_end()); + } else { + result.push_str(opts.initial_indent); + } + } else { + if line.trim().is_empty() { + result.push_str(trimmed_indent); + } else { + result.push_str(opts.subsequent_indent); + } + } + result.push_str(line); + } + if text.ends_with('\n') { + // split_terminator will have eaten the final '\n'. + result.push('\n'); + } + result + } + } + fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result { write!( f, diff --git a/tests/graphical.rs b/tests/graphical.rs index aabf167f..21185f52 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -218,6 +218,50 @@ fn word_wrap_options() -> Result<(), MietteError> { Ok(()) } +#[test] +fn wrap_option() -> Result<(), MietteError> { + // A line should break on the width + let out = fmt_report_with_settings( + Report::msg("abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz"), + |handler| handler.with_width(15), + ); + let expected = r#" × abc def + │ ghi jkl + │ mno pqr + │ stu vwx + │ yz abc + │ def ghi + │ jkl mno + │ pqr stu + │ vwx yz +"# + .to_string(); + assert_eq!(expected, out); + + // Unless, wrapping is disabled + let out = fmt_report_with_settings( + Report::msg("abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz"), + |handler| handler.with_width(15).with_wrap_lines(false), + ); + let expected = + " × abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz\n".to_string(); + assert_eq!(expected, out); + + // Then, user-defined new lines should be preserved wrapping is disabled + let out = fmt_report_with_settings( + Report::msg("abc def ghi jkl mno pqr stu vwx yz\nabc def ghi jkl mno pqr stu vwx yz\nabc def ghi jkl mno pqr stu vwx yz"), + |handler| handler.with_width(15).with_wrap_lines(false), + ); + let expected = r#" × abc def ghi jkl mno pqr stu vwx yz + │ abc def ghi jkl mno pqr stu vwx yz + │ abc def ghi jkl mno pqr stu vwx yz +"# + .to_string(); + assert_eq!(expected, out); + + Ok(()) +} + #[test] fn empty_source() -> Result<(), MietteError> { #[derive(Debug, Diagnostic, Error)] From 19c22143cb544616046784e35c5e78cc5b881289 Mon Sep 17 00:00:00 2001 From: Hytak Date: Thu, 11 Jan 2024 19:42:32 +0100 Subject: [PATCH 2/5] feat(graphical): render disjoint snippets separately for cleaner output (#324) --- src/handlers/graphical.rs | 120 +++++++++++++++++++------------------- tests/graphical.rs | 92 +++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 61 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 951303ab..021ba34c 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -6,7 +6,7 @@ use unicode_width::UnicodeWidthChar; use crate::diagnostic_chain::{DiagnosticChain, ErrorKind}; use crate::handlers::theme::*; use crate::protocol::{Diagnostic, Severity}; -use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents}; +use crate::{LabeledSpan, ReportHandler, SourceCode, SourceSpan, SpanContents}; /** A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a @@ -386,66 +386,58 @@ impl GraphicalReportHandler { diagnostic: &(dyn Diagnostic), opt_source: Option<&dyn SourceCode>, ) -> fmt::Result { - if let Some(source) = opt_source { - if let Some(labels) = diagnostic.labels() { - let mut labels = labels.collect::>(); - labels.sort_unstable_by_key(|l| l.inner().offset()); - if !labels.is_empty() { - let contents = labels - .iter() - .map(|label| { - source.read_span(label.inner(), self.context_lines, self.context_lines) - }) - .collect::>>, MietteError>>() - .map_err(|_| fmt::Error)?; - let mut contexts = Vec::with_capacity(contents.len()); - for (right, right_conts) in labels.iter().cloned().zip(contents.iter()) { - if contexts.is_empty() { - contexts.push((right, right_conts)); - } else { - let (left, left_conts) = contexts.last().unwrap().clone(); - let left_end = left.offset() + left.len(); - let right_end = right.offset() + right.len(); - if left_conts.line() + left_conts.line_count() >= right_conts.line() { - // The snippets will overlap, so we create one Big Chunky Boi - let new_span = LabeledSpan::new( - left.label().map(String::from), - left.offset(), - if right_end >= left_end { - // Right end goes past left end - right_end - left.offset() - } else { - // right is contained inside left - left.len() - }, - ); - if source - .read_span( - new_span.inner(), - self.context_lines, - self.context_lines, - ) - .is_ok() - { - contexts.pop(); - contexts.push(( - // We'll throw this away later - new_span, left_conts, - )); - } else { - contexts.push((right, right_conts)); - } - } else { - contexts.push((right, right_conts)); - } - } - } - for (ctx, _) in contexts { - self.render_context(f, source, &ctx, &labels[..])?; - } + let source = match opt_source { + Some(source) => source, + None => return Ok(()), + }; + let labels = match diagnostic.labels() { + Some(labels) => labels, + None => return Ok(()), + }; + + let mut labels = labels.collect::>(); + labels.sort_unstable_by_key(|l| l.inner().offset()); + + let mut contexts = Vec::with_capacity(labels.len()); + for right in labels.iter().cloned() { + let right_conts = source + .read_span(right.inner(), self.context_lines, self.context_lines) + .map_err(|_| fmt::Error)?; + + if contexts.is_empty() { + contexts.push((right, right_conts)); + continue; + } + + let (left, left_conts) = contexts.last().unwrap(); + if left_conts.line() + left_conts.line_count() >= right_conts.line() { + // The snippets will overlap, so we create one Big Chunky Boi + let left_end = left.offset() + left.len(); + let right_end = right.offset() + right.len(); + let new_end = std::cmp::max(left_end, right_end); + + let new_span = LabeledSpan::new( + left.label().map(String::from), + left.offset(), + new_end - left.offset(), + ); + // Check that the two contexts can be combined + if let Ok(new_conts) = + source.read_span(new_span.inner(), self.context_lines, self.context_lines) + { + contexts.pop(); + // We'll throw the contents away later + contexts.push((new_span, new_conts)); + continue; } } + + contexts.push((right, right_conts)); + } + for (ctx, _) in contexts { + self.render_context(f, source, &ctx, &labels[..])?; } + Ok(()) } @@ -458,10 +450,16 @@ impl GraphicalReportHandler { ) -> fmt::Result { let (contents, lines) = self.get_lines(source, context.inner())?; - let primary_label = labels - .iter() + // only consider labels from the context as primary label + let ctx_labels = labels.iter().filter(|l| { + context.inner().offset() <= l.inner().offset() + && l.inner().offset() + l.inner().len() + <= context.inner().offset() + context.inner().len() + }); + let primary_label = ctx_labels + .clone() .find(|label| label.primary()) - .or_else(|| labels.first()); + .or_else(|| ctx_labels.clone().next()); // sorting is your friend let labels = labels diff --git a/tests/graphical.rs b/tests/graphical.rs index 21185f52..4e8b7375 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -1757,3 +1757,95 @@ fn single_line_with_wide_char_unaligned_span_empty() -> Result<(), MietteError> assert_eq!(expected, out); Ok(()) } + +#[test] +fn triple_adjacent_highlight() -> Result<(), MietteError> { + #[derive(Debug, Diagnostic, Error)] + #[error("oops!")] + #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))] + struct MyBad { + #[source_code] + src: NamedSource, + #[label = "this bit here"] + highlight1: SourceSpan, + #[label = "also this bit"] + highlight2: SourceSpan, + #[label = "finally we got"] + highlight3: SourceSpan, + } + + let src = "source\n\n\n text\n\n\n here".to_string(); + let err = MyBad { + src: NamedSource::new("bad_file.rs", src), + highlight1: (0, 6).into(), + highlight2: (11, 4).into(), + highlight3: (22, 4).into(), + }; + let out = fmt_report(err.into()); + println!("Error: {}", out); + let expected = "oops::my::bad + + × oops! + ╭─[bad_file.rs:1:1] + 1 │ source + · ───┬── + · ╰── this bit here + 2 │ + 3 │ + 4 │ text + · ──┬─ + · ╰── also this bit + 5 │ + 6 │ + 7 │ here + · ──┬─ + · ╰── finally we got + ╰──── + help: try doing it better next time? +"; + assert_eq!(expected, &out); + Ok(()) +} + +#[test] +fn non_adjacent_highlight() -> Result<(), MietteError> { + #[derive(Debug, Diagnostic, Error)] + #[error("oops!")] + #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))] + struct MyBad { + #[source_code] + src: NamedSource, + #[label = "this bit here"] + highlight1: SourceSpan, + #[label = "also this bit"] + highlight2: SourceSpan, + } + + let src = "source\n\n\n\n text here".to_string(); + let err = MyBad { + src: NamedSource::new("bad_file.rs", src), + highlight1: (0, 6).into(), + highlight2: (12, 4).into(), + }; + let out = fmt_report(err.into()); + println!("Error: {}", out); + let expected = "oops::my::bad + + × oops! + ╭─[bad_file.rs:1:1] + 1 │ source + · ───┬── + · ╰── this bit here + 2 │ + ╰──── + ╭─[bad_file.rs:5:3] + 4 │ + 5 │ text here + · ──┬─ + · ╰── also this bit + ╰──── + help: try doing it better next time? +"; + assert_eq!(expected, &out); + Ok(()) +} From 55bfc4201638e412dd2732d394389e22a7398822 Mon Sep 17 00:00:00 2001 From: Benjamin Lee Date: Thu, 11 Jan 2024 14:13:00 -0800 Subject: [PATCH 3/5] tests: improve robustness of color_format tests (#329) Previously, these tests would have spurious failures when NO_COLOR or FORCE_COLOR was set in the user's environment, since we weren't clearing one variable before testing a value for the other one. The previous version of the code also did not restore environment variable values on panic, which could cause spurious failures in other tests after one test fails. --- tests/color_format.rs | 77 ++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/tests/color_format.rs b/tests/color_format.rs index bb907084..95d40e0a 100644 --- a/tests/color_format.rs +++ b/tests/color_format.rs @@ -3,6 +3,7 @@ use lazy_static::lazy_static; use miette::{Diagnostic, MietteHandler, MietteHandlerOpts, ReportHandler, RgbColors}; use regex::Regex; +use std::ffi::OsString; use std::fmt::{self, Debug}; use std::sync::Mutex; use thiserror::Error; @@ -42,16 +43,29 @@ fn color_format(handler: MietteHandler) -> ColorFormat { } } -/// 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); +/// Store the current value of an environment variable on construction, and then +/// restore that value when the guard is dropped. +struct EnvVarGuard<'a> { + var: &'a str, + old_value: Option, +} + +impl EnvVarGuard<'_> { + fn new(var: &str) -> EnvVarGuard<'_> { + EnvVarGuard { + var, + old_value: std::env::var_os(var), + } + } +} + +impl Drop for EnvVarGuard<'_> { + fn drop(&mut self) { + if let Some(old_value) = &self.old_value { + std::env::set_var(self.var, old_value); + } else { + std::env::remove_var(self.var); + } } } @@ -72,22 +86,33 @@ fn check_colors MietteHandlerOpts>( // // 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); + let lock = COLOR_ENV_VARS.lock().unwrap(); + + let guards = ( + EnvVarGuard::new("NO_COLOR"), + EnvVarGuard::new("FORCE_COLOR"), + ); + // Clear color environment variables that may be set outside of 'cargo test' + std::env::remove_var("NO_COLOR"); + std::env::remove_var("FORCE_COLOR"); + + std::env::set_var("NO_COLOR", "1"); + let handler = make_handler(MietteHandlerOpts::new()).build(); + assert_eq!(color_format(handler), no_support); + std::env::remove_var("NO_COLOR"); + + std::env::set_var("FORCE_COLOR", "1"); + let handler = make_handler(MietteHandlerOpts::new()).build(); + assert_eq!(color_format(handler), ansi_support); + std::env::remove_var("FORCE_COLOR"); + + std::env::set_var("FORCE_COLOR", "3"); + let handler = make_handler(MietteHandlerOpts::new()).build(); + assert_eq!(color_format(handler), rgb_support); + std::env::remove_var("FORCE_COLOR"); + + drop(guards); + drop(lock); } #[test] From cb2ae2e18b446a5e90885faf8a30b5672c307df8 Mon Sep 17 00:00:00 2001 From: Brooks Rady Date: Wed, 31 Jan 2024 00:03:01 +0000 Subject: [PATCH 4/5] fix(graphical): render cause chains for inner errors (#330) The default `GraphicalReportHandler` disables the printing of cause chains for any inner errors (errors `related()` to a source diagnostic) when it disables nested footer printing. This results in lost cause chain information when printing with the default report handler. --- src/handlers/graphical.rs | 16 ++++-- tests/test_diagnostic_source_macro.rs | 82 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 021ba34c..78129258 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -317,9 +317,10 @@ impl GraphicalReportHandler { ErrorKind::Diagnostic(diag) => { let mut inner = String::new(); - // Don't print footer for inner errors let mut inner_renderer = self.clone(); + // Don't print footer for inner errors inner_renderer.footer = None; + // Cause chains are already flattened, so don't double-print the nested error inner_renderer.with_cause_chain = false; inner_renderer.render_report(&mut inner, diag)?; @@ -362,6 +363,9 @@ impl GraphicalReportHandler { parent_src: Option<&dyn SourceCode>, ) -> fmt::Result { if let Some(related) = diagnostic.related() { + let mut inner_renderer = self.clone(); + // Re-enable the printing of nested cause chains for related errors + inner_renderer.with_cause_chain = true; writeln!(f)?; for rel in related { match rel.severity() { @@ -369,12 +373,12 @@ impl GraphicalReportHandler { Some(Severity::Warning) => write!(f, "Warning: ")?, Some(Severity::Advice) => write!(f, "Advice: ")?, }; - self.render_header(f, rel)?; - self.render_causes(f, rel)?; + inner_renderer.render_header(f, rel)?; + inner_renderer.render_causes(f, rel)?; let src = rel.source_code().or(parent_src); - self.render_snippets(f, rel, src)?; - self.render_footer(f, rel)?; - self.render_related(f, rel, src)?; + inner_renderer.render_snippets(f, rel, src)?; + inner_renderer.render_footer(f, rel)?; + inner_renderer.render_related(f, rel, src)?; } } Ok(()) diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs index 334ca349..e5305acd 100644 --- a/tests/test_diagnostic_source_macro.rs +++ b/tests/test_diagnostic_source_macro.rs @@ -194,3 +194,85 @@ fn test_nested_diagnostic_source_is_output() { assert_eq!(expected, out); } + +#[derive(Debug, miette::Diagnostic, thiserror::Error)] +#[error("A multi-error happened")] +struct MultiError { + #[related] + related_errs: Vec>, +} + +#[cfg(feature = "fancy-no-backtrace")] +#[test] +fn test_nested_cause_chains_for_related_errors_are_output() { + let inner_error = TestStructError { + asdf_inner_foo: SourceError { + code: String::from("This is another error"), + help: String::from("You should fix this"), + label: (3, 4), + }, + }; + let first_error = NestedError { + code: String::from("right here"), + label: (6, 4), + the_other_err: Box::new(inner_error), + }; + let second_error = SourceError { + code: String::from("You're actually a mess"), + help: String::from("Get a grip..."), + label: (3, 4), + }; + let multi_error = MultiError { + related_errs: vec![Box::new(first_error), Box::new(second_error)], + }; + let diag = NestedError { + code: String::from("the outside world"), + label: (6, 4), + the_other_err: Box::new(multi_error), + }; + let mut out = String::new(); + miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode_nocolor()) + .with_width(80) + .with_footer("Yooo, a footer".to_string()) + .render_report(&mut out, &diag) + .unwrap(); + println!("{}", out); + + let expected = r#" × A nested error happened + ╰─▶ × A multi-error happened + + Error: × A nested error happened + ├─▶ × TestError + │ + ╰─▶ × A complex error happened + ╭──── + 1 │ This is another error + · ──┬─ + · ╰── here + ╰──── + help: You should fix this + + ╭──── + 1 │ right here + · ──┬─ + · ╰── here + ╰──── + Error: × A complex error happened + ╭──── + 1 │ You're actually a mess + · ──┬─ + · ╰── here + ╰──── + help: Get a grip... + + ╭──── + 1 │ the outside world + · ──┬─ + · ╰── here + ╰──── + + Yooo, a footer +"#; + + assert_eq!(expected, out); +} From f1dc89c07640445d224b61ef96c6b25fcdf62dee Mon Sep 17 00:00:00 2001 From: Boshen Date: Sun, 4 Feb 2024 11:12:49 +0800 Subject: [PATCH 5/5] fix(handler): remove the two extra `is_terminal` sys call from `MietteHandlerOpts::build` (#325) `GraphicalReportHandler::new()` calls `GraphicalTheme::default()` https://github.com/zkat/miette/blob/7ff4f874d693a665af4df40f4e94505013e3e262/src/handlers/graphical.rs#L52 which calls `std::io::stdout().is_terminal()` and `std::io::stderr().is_terminal()` https://github.com/zkat/miette/blob/7ff4f874d693a665af4df40f4e94505013e3e262/src/handlers/theme.rs#L72 but this default theme is overridden by `with_theme(theme)`. --- src/handler.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 38c9fc84..3c093730 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -247,10 +247,9 @@ impl MietteHandlerOpts { ThemeStyles::none() }; let theme = self.theme.unwrap_or(GraphicalTheme { characters, styles }); - let mut handler = GraphicalReportHandler::new() + let mut handler = GraphicalReportHandler::new_themed(theme) .with_width(width) - .with_links(linkify) - .with_theme(theme); + .with_links(linkify); if let Some(with_cause_chain) = self.with_cause_chain { if with_cause_chain { handler = handler.with_cause_chain();