miette/
handler.rs

1use crate::highlighters::Highlighter;
2use crate::highlighters::MietteHighlighter;
3use crate::protocol::Diagnostic;
4use crate::GraphicalReportHandler;
5use crate::GraphicalTheme;
6use crate::NarratableReportHandler;
7use crate::ReportHandler;
8use crate::ThemeCharacters;
9use crate::ThemeStyles;
10use cfg_if::cfg_if;
11use std::fmt;
12
13/// Settings to control the color format used for graphical rendering.
14#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
15pub enum RgbColors {
16    /// Use RGB colors even if the terminal does not support them
17    Always,
18    /// Use RGB colors instead of ANSI if the terminal supports RGB
19    Preferred,
20    /// Always use ANSI, regardless of terminal support for RGB
21    #[default]
22    Never,
23}
24
25/**
26Create a custom [`MietteHandler`] from options.
27
28## Example
29
30```no_run
31miette::set_hook(Box::new(|_| {
32    Box::new(miette::MietteHandlerOpts::new()
33        .terminal_links(true)
34        .unicode(false)
35        .context_lines(3)
36        .build())
37}))
38# .unwrap();
39```
40*/
41#[derive(Default, Debug, Clone)]
42pub struct MietteHandlerOpts {
43    pub(crate) linkify: Option<bool>,
44    pub(crate) width: Option<usize>,
45    pub(crate) theme: Option<GraphicalTheme>,
46    pub(crate) force_graphical: Option<bool>,
47    pub(crate) force_narrated: Option<bool>,
48    pub(crate) rgb_colors: RgbColors,
49    pub(crate) color: Option<bool>,
50    pub(crate) unicode: Option<bool>,
51    pub(crate) footer: Option<String>,
52    pub(crate) context_lines: Option<usize>,
53    pub(crate) tab_width: Option<usize>,
54    pub(crate) with_cause_chain: Option<bool>,
55    pub(crate) break_words: Option<bool>,
56    pub(crate) wrap_lines: Option<bool>,
57    pub(crate) word_separator: Option<textwrap::WordSeparator>,
58    pub(crate) word_splitter: Option<textwrap::WordSplitter>,
59    pub(crate) highlighter: Option<MietteHighlighter>,
60    pub(crate) show_related_as_nested: Option<bool>,
61}
62
63impl MietteHandlerOpts {
64    /// Create a new `MietteHandlerOpts`.
65    pub fn new() -> Self {
66        Default::default()
67    }
68
69    /// If true, specify whether the graphical handler will make codes be
70    /// clickable links in supported terminals. Defaults to auto-detection
71    /// based on known supported terminals.
72    pub fn terminal_links(mut self, linkify: bool) -> Self {
73        self.linkify = Some(linkify);
74        self
75    }
76
77    /// Set a graphical theme for the handler when rendering in graphical mode.
78    /// Use [`force_graphical()`](`MietteHandlerOpts::force_graphical) to force
79    /// graphical mode. This option overrides
80    /// [`color()`](`MietteHandlerOpts::color).
81    pub fn graphical_theme(mut self, theme: GraphicalTheme) -> Self {
82        self.theme = Some(theme);
83        self
84    }
85
86    /// Set a syntax highlighter when rendering in graphical mode.
87    /// Use [`force_graphical()`](MietteHandlerOpts::force_graphical()) to
88    /// force graphical mode.
89    ///
90    /// Syntax highlighting is disabled by default unless the
91    /// `syntect-highlighter` feature is enabled. Call this method
92    /// to override the default and use a custom highlighter
93    /// implementation instead.
94    ///
95    /// Use
96    /// [`without_syntax_highlighting()`](MietteHandlerOpts::without_syntax_highlighting())
97    /// To disable highlighting completely.
98    ///
99    /// Setting this option will not force color output. In all cases, the
100    /// current color configuration via
101    /// [`color()`](MietteHandlerOpts::color()) takes precedence over
102    /// highlighter configuration. However, this option does take precedence over
103    /// [`rgb_colors()`](MietteHandlerOpts::rgb_colors()) (meaning syntax highlighting will be
104    /// enabled regardless of the value of [`MietteHandlerOpts::rgb_colors`]).
105    pub fn with_syntax_highlighting(
106        mut self,
107        highlighter: impl Highlighter + Send + Sync + 'static,
108    ) -> Self {
109        self.highlighter = Some(MietteHighlighter::from(highlighter));
110        self
111    }
112
113    /// Disables syntax highlighting when rendering in graphical mode.
114    /// Use [`force_graphical()`](MietteHandlerOpts::force_graphical()) to
115    /// force graphical mode.
116    ///
117    /// Syntax highlighting is disabled by default unless the
118    /// `syntect-highlighter` feature is enabled. Call this method if you want
119    /// to disable highlighting when building with this feature.
120    pub fn without_syntax_highlighting(mut self) -> Self {
121        self.highlighter = Some(MietteHighlighter::nocolor());
122        self
123    }
124
125    /// Sets the width to wrap the report at. Defaults to 80.
126    pub fn width(mut self, width: usize) -> Self {
127        self.width = Some(width);
128        self
129    }
130
131    /// If true, long lines can be wrapped.
132    ///
133    /// If false, long lines will not be broken when they exceed the width.
134    ///
135    /// Defaults to true.
136    pub fn wrap_lines(mut self, wrap_lines: bool) -> Self {
137        self.wrap_lines = Some(wrap_lines);
138        self
139    }
140
141    /// If true, long words can be broken when wrapping.
142    ///
143    /// If false, long words will not be broken when they exceed the width.
144    ///
145    /// Defaults to true.
146    pub fn break_words(mut self, break_words: bool) -> Self {
147        self.break_words = Some(break_words);
148        self
149    }
150    /// Sets the `textwrap::WordSeparator` to use when determining wrap points.
151    pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
152        self.word_separator = Some(word_separator);
153        self
154    }
155
156    /// Sets the `textwrap::WordSplitter` to use when determining wrap points.
157    pub fn word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
158        self.word_splitter = Some(word_splitter);
159        self
160    }
161    /// Include the cause chain of the top-level error in the report.
162    pub fn with_cause_chain(mut self) -> Self {
163        self.with_cause_chain = Some(true);
164        self
165    }
166
167    /// Do not include the cause chain of the top-level error in the report.
168    pub fn without_cause_chain(mut self) -> Self {
169        self.with_cause_chain = Some(false);
170        self
171    }
172
173    /// Show related errors as siblings.
174    pub fn show_related_errors_as_siblings(mut self) -> Self {
175        self.show_related_as_nested = Some(false);
176        self
177    }
178
179    /// Show related errors as nested errors.
180    pub fn show_related_errors_as_nested(mut self) -> Self {
181        self.show_related_as_nested = Some(true);
182        self
183    }
184
185    /// If true, colors will be used during graphical rendering, regardless
186    /// of whether or not the terminal supports them.
187    ///
188    /// If false, colors will never be used.
189    ///
190    /// If unspecified, colors will be used only if the terminal supports them.
191    ///
192    /// The actual format depends on the value of
193    /// [`MietteHandlerOpts::rgb_colors`].
194    pub fn color(mut self, color: bool) -> Self {
195        self.color = Some(color);
196        self
197    }
198
199    /// Controls which color format to use if colors are used in graphical
200    /// rendering.
201    ///
202    /// The default is `Never`.
203    ///
204    /// This value does not control whether or not colors are being used in the
205    /// first place. That is handled by the [`MietteHandlerOpts::color`]
206    /// setting. If colors are not being used, the value of `rgb_colors` has
207    /// no effect.
208    ///
209    /// It also does not control colors when a syntax highlighter is in use.
210    pub fn rgb_colors(mut self, color: RgbColors) -> Self {
211        self.rgb_colors = color;
212        self
213    }
214
215    /// If true, forces unicode display for graphical output. If set to false,
216    /// forces ASCII art display.
217    pub fn unicode(mut self, unicode: bool) -> Self {
218        self.unicode = Some(unicode);
219        self
220    }
221
222    /// If true, graphical rendering will be used regardless of terminal
223    /// detection.
224    pub fn force_graphical(mut self, force: bool) -> Self {
225        self.force_graphical = Some(force);
226        self
227    }
228
229    /// If true, forces use of the narrated renderer.
230    pub fn force_narrated(mut self, force: bool) -> Self {
231        self.force_narrated = Some(force);
232        self
233    }
234
235    /// Set a footer to be displayed at the bottom of the report.
236    pub fn footer(mut self, footer: String) -> Self {
237        self.footer = Some(footer);
238        self
239    }
240
241    /// Sets the number of context lines before and after a span to display.
242    pub fn context_lines(mut self, context_lines: usize) -> Self {
243        self.context_lines = Some(context_lines);
244        self
245    }
246
247    /// Set the displayed tab width in spaces.
248    pub fn tab_width(mut self, width: usize) -> Self {
249        self.tab_width = Some(width);
250        self
251    }
252
253    /// Builds a [`MietteHandler`] from this builder.
254    pub fn build(self) -> MietteHandler {
255        let graphical = self.is_graphical();
256        let width = self.get_width();
257        if !graphical {
258            let mut handler = NarratableReportHandler::new();
259            if let Some(footer) = self.footer {
260                handler = handler.with_footer(footer);
261            }
262            if let Some(context_lines) = self.context_lines {
263                handler = handler.with_context_lines(context_lines);
264            }
265            if let Some(with_cause_chain) = self.with_cause_chain {
266                if with_cause_chain {
267                    handler = handler.with_cause_chain();
268                } else {
269                    handler = handler.without_cause_chain();
270                }
271            }
272            MietteHandler {
273                inner: Box::new(handler),
274            }
275        } else {
276            let linkify = self.use_links();
277            let characters = match self.unicode {
278                Some(true) => ThemeCharacters::unicode(),
279                Some(false) => ThemeCharacters::ascii(),
280                None if syscall::supports_unicode() => ThemeCharacters::unicode(),
281                None => ThemeCharacters::ascii(),
282            };
283            let styles = if self.color == Some(false) {
284                ThemeStyles::none()
285            } else if let Some(color_has_16m) = syscall::supports_color_has_16m() {
286                match self.rgb_colors {
287                    RgbColors::Always => ThemeStyles::rgb(),
288                    RgbColors::Preferred if color_has_16m => ThemeStyles::rgb(),
289                    _ => ThemeStyles::ansi(),
290                }
291            } else if self.color == Some(true) {
292                match self.rgb_colors {
293                    RgbColors::Always => ThemeStyles::rgb(),
294                    _ => ThemeStyles::ansi(),
295                }
296            } else {
297                ThemeStyles::none()
298            };
299            let highlighter_opt =
300                HighlighterOption::select(self.color, self.highlighter, syscall::supports_color());
301            let theme = self.theme.unwrap_or(GraphicalTheme { characters, styles });
302            let mut handler = GraphicalReportHandler::new_themed(theme)
303                .with_width(width)
304                .with_links(linkify);
305            handler.highlighter = highlighter_opt.into();
306            if let Some(with_cause_chain) = self.with_cause_chain {
307                if with_cause_chain {
308                    handler = handler.with_cause_chain();
309                } else {
310                    handler = handler.without_cause_chain();
311                }
312            }
313            if let Some(footer) = self.footer {
314                handler = handler.with_footer(footer);
315            }
316            if let Some(context_lines) = self.context_lines {
317                handler = handler.with_context_lines(context_lines);
318            }
319            if let Some(w) = self.tab_width {
320                handler = handler.tab_width(w);
321            }
322            if let Some(b) = self.break_words {
323                handler = handler.with_break_words(b)
324            }
325            if let Some(b) = self.wrap_lines {
326                handler = handler.with_wrap_lines(b)
327            }
328            if let Some(s) = self.word_separator {
329                handler = handler.with_word_separator(s)
330            }
331            if let Some(s) = self.word_splitter {
332                handler = handler.with_word_splitter(s)
333            }
334            if let Some(b) = self.show_related_as_nested {
335                handler = handler.with_show_related_as_nested(b)
336            }
337
338            MietteHandler {
339                inner: Box::new(handler),
340            }
341        }
342    }
343
344    pub(crate) fn is_graphical(&self) -> bool {
345        if let Some(force_narrated) = self.force_narrated {
346            !force_narrated
347        } else if let Some(force_graphical) = self.force_graphical {
348            force_graphical
349        } else if let Ok(env) = std::env::var("NO_GRAPHICS") {
350            env == "0"
351        } else {
352            true
353        }
354    }
355
356    // Detects known terminal apps based on env variables and returns true if
357    // they support rendering links.
358    pub(crate) fn use_links(&self) -> bool {
359        if let Some(linkify) = self.linkify {
360            linkify
361        } else {
362            syscall::supports_hyperlinks()
363        }
364    }
365
366    pub(crate) fn get_width(&self) -> usize {
367        self.width
368            .unwrap_or_else(|| syscall::terminal_width().unwrap_or(80))
369    }
370}
371
372/**
373A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a
374quasi-graphical way, using terminal colors, unicode drawing characters, and
375other such things.
376
377This is the default reporter bundled with `miette`.
378
379This printer can be customized by using
380[`GraphicalReportHandler::new_themed()`] and handing it a [`GraphicalTheme`] of
381your own creation (or using one of its own defaults).
382
383See [`set_hook`](crate::set_hook) for more details on customizing your global
384printer.
385*/
386#[allow(missing_debug_implementations)]
387pub struct MietteHandler {
388    inner: Box<dyn ReportHandler + Send + Sync>,
389}
390
391impl MietteHandler {
392    /// Creates a new [`MietteHandler`] with default settings.
393    pub fn new() -> Self {
394        Default::default()
395    }
396}
397
398impl Default for MietteHandler {
399    fn default() -> Self {
400        MietteHandlerOpts::new().build()
401    }
402}
403
404impl ReportHandler for MietteHandler {
405    fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
406        if f.alternate() {
407            return fmt::Debug::fmt(diagnostic, f);
408        }
409
410        self.inner.debug(diagnostic, f)
411    }
412}
413
414enum HighlighterOption {
415    Disable,
416    EnableCustom(MietteHighlighter),
417    #[cfg(feature = "syntect-highlighter")]
418    EnableSyntect,
419}
420
421impl HighlighterOption {
422    fn select(
423        color: Option<bool>,
424        highlighter: Option<MietteHighlighter>,
425        supports_color: bool,
426    ) -> HighlighterOption {
427        if color == Some(false) || (color.is_none() && !supports_color) {
428            return HighlighterOption::Disable;
429        }
430        highlighter
431            .map(HighlighterOption::EnableCustom)
432            .unwrap_or_default()
433    }
434}
435
436// NOTE: This is manually implemented so that it's clearer what's going on with
437// the conditional compilation — clippy isn't picking up the `cfg` stuff here
438#[allow(clippy::derivable_impls)]
439impl Default for HighlighterOption {
440    fn default() -> Self {
441        cfg_if! {
442            if #[cfg(feature = "syntect-highlighter")] {
443                // Because the syntect highlighter currently only supports 24-bit truecolor,
444                // it supersedes and ignores the `rgb_colors` config.
445                // TODO: In the future, if we find a way to convert the RGB syntect theme
446                // into an ANSI color theme, we can take `rgb_colors` into account.
447                HighlighterOption::EnableSyntect
448            } else {
449                HighlighterOption::Disable
450            }
451        }
452    }
453}
454
455impl From<HighlighterOption> for MietteHighlighter {
456    fn from(opt: HighlighterOption) -> Self {
457        match opt {
458            HighlighterOption::Disable => MietteHighlighter::nocolor(),
459            HighlighterOption::EnableCustom(highlighter) => highlighter,
460            #[cfg(feature = "syntect-highlighter")]
461            HighlighterOption::EnableSyntect => MietteHighlighter::syntect_truecolor(),
462        }
463    }
464}
465
466mod syscall {
467    use cfg_if::cfg_if;
468
469    #[inline]
470    pub(super) fn terminal_width() -> Option<usize> {
471        cfg_if! {
472            if #[cfg(any(feature = "fancy-no-syscall", miri))] {
473                None
474            } else {
475                terminal_size::terminal_size().map(|size| size.0 .0 as usize)
476            }
477        }
478    }
479
480    #[inline]
481    pub(super) fn supports_hyperlinks() -> bool {
482        cfg_if! {
483            if #[cfg(feature = "fancy-no-syscall")] {
484                false
485            } else {
486                supports_hyperlinks::on(supports_hyperlinks::Stream::Stderr)
487            }
488        }
489    }
490
491    #[inline]
492    pub(super) fn supports_color() -> bool {
493        cfg_if! {
494            if #[cfg(feature = "fancy-no-syscall")] {
495                false
496            } else {
497                supports_color::on(supports_color::Stream::Stderr).is_some()
498            }
499        }
500    }
501
502    #[inline]
503    pub(super) fn supports_color_has_16m() -> Option<bool> {
504        cfg_if! {
505            if #[cfg(feature = "fancy-no-syscall")] {
506                None
507            } else {
508                supports_color::on(supports_color::Stream::Stderr).map(|color| color.has_16m)
509            }
510        }
511    }
512
513    #[inline]
514    pub(super) fn supports_unicode() -> bool {
515        cfg_if! {
516            if #[cfg(feature = "fancy-no-syscall")] {
517                false
518            } else {
519                supports_unicode::on(supports_unicode::Stream::Stderr)
520            }
521        }
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528    use crate::highlighters::BlankHighlighter;
529    use cfg_if::cfg_if;
530
531    #[test]
532    fn test_highlighter_option() {
533        // Syntax highlighting is enabled depending on several variables:
534        // - The `color` config
535        // - The `highlighter` config
536        // - Whether the `syntect-highlighter` feature is enabled
537        // - Whether the terminal supports color
538        //
539        // This test asserts the expected highlighter depending on combinations of those variables.
540
541        macro_rules! assert_highlighter_opt {
542            (opts = $opts:expr, supports_color = $sup_color:literal, expected = $expected:pat $(,)?) => {
543                assert_highlighter_opt!(
544                    opts = $opts,
545                    supports_color = $sup_color,
546                    expected_with_syntect = $expected,
547                    expected_without_syntect = $expected,
548                );
549            };
550
551            (
552                opts = $opts:expr,
553                supports_color = $sup_color:literal,
554                expected_with_syntect = $expected_with:pat,
555                expected_without_syntect = $expected_without:pat $(,)?
556            ) => {{
557                let highlighter_opt =
558                    HighlighterOption::select($opts.color, $opts.highlighter, $sup_color);
559                cfg_if! {
560                    if #[cfg(feature = "syntect-highlighter")] {
561                        assert!(matches!(highlighter_opt, $expected_with));
562                    } else {
563                        assert!(matches!(highlighter_opt, $expected_without));
564                    }
565                }
566            }};
567        }
568
569        // When color is explicitly disabled, highlighting is also always disabled.
570        assert_highlighter_opt!(
571            opts = MietteHandlerOpts::new().color(false),
572            supports_color = true,
573            expected = HighlighterOption::Disable,
574        );
575
576        // When color is unset and the terminal doesn't support color, highlighting is disabled.
577        assert_highlighter_opt!(
578            opts = MietteHandlerOpts::new(),
579            supports_color = false,
580            expected = HighlighterOption::Disable,
581        );
582
583        // With explicit or implicit color support, highlighting is automatically enabled when
584        // `syntect-highlighter` is enabled.
585        assert_highlighter_opt!(
586            opts = MietteHandlerOpts::new().color(true),
587            supports_color = false,
588            expected_with_syntect = HighlighterOption::EnableSyntect,
589            expected_without_syntect = HighlighterOption::Disable,
590        );
591        assert_highlighter_opt!(
592            opts = MietteHandlerOpts::new(),
593            supports_color = true,
594            expected_with_syntect = HighlighterOption::EnableSyntect,
595            expected_without_syntect = HighlighterOption::Disable,
596        );
597
598        // With explicit or implicit color support, if custom highlighting is set, it's enabled.
599        assert_highlighter_opt!(
600            opts = MietteHandlerOpts::new()
601                .color(true)
602                .with_syntax_highlighting(BlankHighlighter),
603            supports_color = false,
604            expected = HighlighterOption::EnableCustom(_),
605        );
606        assert_highlighter_opt!(
607            opts = MietteHandlerOpts::new().with_syntax_highlighting(BlankHighlighter),
608            supports_color = true,
609            expected = HighlighterOption::EnableCustom(_),
610        );
611
612        // Setting `RgbColors::Never` has no effect when syntax highlighting is enabled.
613        assert_highlighter_opt!(
614            opts = MietteHandlerOpts::new()
615                .color(true)
616                .rgb_colors(RgbColors::Never),
617            supports_color = false,
618            expected_with_syntect = HighlighterOption::EnableSyntect,
619            expected_without_syntect = HighlighterOption::Disable,
620        );
621        assert_highlighter_opt!(
622            opts = MietteHandlerOpts::new().rgb_colors(RgbColors::Never),
623            supports_color = true,
624            expected_with_syntect = HighlighterOption::EnableSyntect,
625            expected_without_syntect = HighlighterOption::Disable,
626        );
627    }
628}