miette/
handler.rs

1use std::fmt;
2
3use crate::highlighters::Highlighter;
4use crate::highlighters::MietteHighlighter;
5use crate::protocol::Diagnostic;
6use crate::GraphicalReportHandler;
7use crate::GraphicalTheme;
8use crate::NarratableReportHandler;
9use crate::ReportHandler;
10use crate::ThemeCharacters;
11use crate::ThemeStyles;
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}
61
62impl MietteHandlerOpts {
63    /// Create a new `MietteHandlerOpts`.
64    pub fn new() -> Self {
65        Default::default()
66    }
67
68    /// If true, specify whether the graphical handler will make codes be
69    /// clickable links in supported terminals. Defaults to auto-detection
70    /// based on known supported terminals.
71    pub fn terminal_links(mut self, linkify: bool) -> Self {
72        self.linkify = Some(linkify);
73        self
74    }
75
76    /// Set a graphical theme for the handler when rendering in graphical mode.
77    /// Use [`force_graphical()`](`MietteHandlerOpts::force_graphical) to force
78    /// graphical mode. This option overrides
79    /// [`color()`](`MietteHandlerOpts::color).
80    pub fn graphical_theme(mut self, theme: GraphicalTheme) -> Self {
81        self.theme = Some(theme);
82        self
83    }
84
85    /// Set a syntax highlighter when rendering in graphical mode.
86    /// Use [`force_graphical()`](MietteHandlerOpts::force_graphical()) to
87    /// force graphical mode.
88    ///
89    /// Syntax highlighting is disabled by default unless the
90    /// `syntect-highlighter` feature is enabled. Call this method
91    /// to override the default and use a custom highlighter
92    /// implementation instead.
93    ///
94    /// Use
95    /// [`without_syntax_highlighting()`](MietteHandlerOpts::without_syntax_highlighting())
96    /// To disable highlighting completely.
97    ///
98    /// Setting this option will not force color output. In all cases, the
99    /// current color configuration via
100    /// [`color()`](MietteHandlerOpts::color()) takes precedence over
101    /// highlighter configuration.
102    pub fn with_syntax_highlighting(
103        mut self,
104        highlighter: impl Highlighter + Send + Sync + 'static,
105    ) -> Self {
106        self.highlighter = Some(MietteHighlighter::from(highlighter));
107        self
108    }
109
110    /// Disables syntax highlighting when rendering in graphical mode.
111    /// Use [`force_graphical()`](MietteHandlerOpts::force_graphical()) to
112    /// force graphical mode.
113    ///
114    /// Syntax highlighting is disabled by default unless the
115    /// `syntect-highlighter` feature is enabled. Call this method if you want
116    /// to disable highlighting when building with this feature.
117    pub fn without_syntax_highlighting(mut self) -> Self {
118        self.highlighter = Some(MietteHighlighter::nocolor());
119        self
120    }
121
122    /// Sets the width to wrap the report at. Defaults to 80.
123    pub fn width(mut self, width: usize) -> Self {
124        self.width = Some(width);
125        self
126    }
127
128    /// If true, long lines can be wrapped.
129    ///
130    /// If false, long lines will not be broken when they exceed the width.
131    ///
132    /// Defaults to true.
133    pub fn wrap_lines(mut self, wrap_lines: bool) -> Self {
134        self.wrap_lines = Some(wrap_lines);
135        self
136    }
137
138    /// If true, long words can be broken when wrapping.
139    ///
140    /// If false, long words will not be broken when they exceed the width.
141    ///
142    /// Defaults to true.
143    pub fn break_words(mut self, break_words: bool) -> Self {
144        self.break_words = Some(break_words);
145        self
146    }
147    /// Sets the `textwrap::WordSeparator` to use when determining wrap points.
148    pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
149        self.word_separator = Some(word_separator);
150        self
151    }
152
153    /// Sets the `textwrap::WordSplitter` to use when determining wrap points.
154    pub fn word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
155        self.word_splitter = Some(word_splitter);
156        self
157    }
158    /// Include the cause chain of the top-level error in the report.
159    pub fn with_cause_chain(mut self) -> Self {
160        self.with_cause_chain = Some(true);
161        self
162    }
163
164    /// Do not include the cause chain of the top-level error in the report.
165    pub fn without_cause_chain(mut self) -> Self {
166        self.with_cause_chain = Some(false);
167        self
168    }
169
170    /// If true, colors will be used during graphical rendering, regardless
171    /// of whether or not the terminal supports them.
172    ///
173    /// If false, colors will never be used.
174    ///
175    /// If unspecified, colors will be used only if the terminal supports them.
176    ///
177    /// The actual format depends on the value of
178    /// [`MietteHandlerOpts::rgb_colors`].
179    pub fn color(mut self, color: bool) -> Self {
180        self.color = Some(color);
181        self
182    }
183
184    /// Controls which color format to use if colors are used in graphical
185    /// rendering.
186    ///
187    /// The default is `Never`.
188    ///
189    /// This value does not control whether or not colors are being used in the
190    /// first place. That is handled by the [`MietteHandlerOpts::color`]
191    /// setting. If colors are not being used, the value of `rgb_colors` has
192    /// no effect.
193    pub fn rgb_colors(mut self, color: RgbColors) -> Self {
194        self.rgb_colors = color;
195        self
196    }
197
198    /// If true, forces unicode display for graphical output. If set to false,
199    /// forces ASCII art display.
200    pub fn unicode(mut self, unicode: bool) -> Self {
201        self.unicode = Some(unicode);
202        self
203    }
204
205    /// If true, graphical rendering will be used regardless of terminal
206    /// detection.
207    pub fn force_graphical(mut self, force: bool) -> Self {
208        self.force_graphical = Some(force);
209        self
210    }
211
212    /// If true, forces use of the narrated renderer.
213    pub fn force_narrated(mut self, force: bool) -> Self {
214        self.force_narrated = Some(force);
215        self
216    }
217
218    /// Set a footer to be displayed at the bottom of the report.
219    pub fn footer(mut self, footer: String) -> Self {
220        self.footer = Some(footer);
221        self
222    }
223
224    /// Sets the number of context lines before and after a span to display.
225    pub fn context_lines(mut self, context_lines: usize) -> Self {
226        self.context_lines = Some(context_lines);
227        self
228    }
229
230    /// Set the displayed tab width in spaces.
231    pub fn tab_width(mut self, width: usize) -> Self {
232        self.tab_width = Some(width);
233        self
234    }
235
236    /// Builds a [`MietteHandler`] from this builder.
237    pub fn build(self) -> MietteHandler {
238        let graphical = self.is_graphical();
239        let width = self.get_width();
240        if !graphical {
241            let mut handler = NarratableReportHandler::new();
242            if let Some(footer) = self.footer {
243                handler = handler.with_footer(footer);
244            }
245            if let Some(context_lines) = self.context_lines {
246                handler = handler.with_context_lines(context_lines);
247            }
248            if let Some(with_cause_chain) = self.with_cause_chain {
249                if with_cause_chain {
250                    handler = handler.with_cause_chain();
251                } else {
252                    handler = handler.without_cause_chain();
253                }
254            }
255            MietteHandler {
256                inner: Box::new(handler),
257            }
258        } else {
259            let linkify = self.use_links();
260            let characters = match self.unicode {
261                Some(true) => ThemeCharacters::unicode(),
262                Some(false) => ThemeCharacters::ascii(),
263                None if syscall::supports_unicode() => ThemeCharacters::unicode(),
264                None => ThemeCharacters::ascii(),
265            };
266            let styles = if self.color == Some(false) {
267                ThemeStyles::none()
268            } else if let Some(color_has_16m) = syscall::supports_color_has_16m() {
269                match self.rgb_colors {
270                    RgbColors::Always => ThemeStyles::rgb(),
271                    RgbColors::Preferred if color_has_16m => ThemeStyles::rgb(),
272                    _ => ThemeStyles::ansi(),
273                }
274            } else if self.color == Some(true) {
275                match self.rgb_colors {
276                    RgbColors::Always => ThemeStyles::rgb(),
277                    _ => ThemeStyles::ansi(),
278                }
279            } else {
280                ThemeStyles::none()
281            };
282            #[cfg(not(feature = "syntect-highlighter"))]
283            let highlighter = self.highlighter.unwrap_or_else(MietteHighlighter::nocolor);
284            #[cfg(feature = "syntect-highlighter")]
285            let highlighter = if self.color == Some(false) {
286                MietteHighlighter::nocolor()
287            } else if self.color == Some(true) || syscall::supports_color() {
288                match self.highlighter {
289                    Some(highlighter) => highlighter,
290                    None => match self.rgb_colors {
291                        // Because the syntect highlighter currently only supports 24-bit truecolor,
292                        // respect RgbColor::Never by disabling the highlighter.
293                        // TODO: In the future, find a way to convert the RGB syntect theme
294                        // into an ANSI color theme.
295                        RgbColors::Never => MietteHighlighter::nocolor(),
296                        _ => MietteHighlighter::syntect_truecolor(),
297                    },
298                }
299            } else {
300                MietteHighlighter::nocolor()
301            };
302            let theme = self.theme.unwrap_or(GraphicalTheme { characters, styles });
303            let mut handler = GraphicalReportHandler::new_themed(theme)
304                .with_width(width)
305                .with_links(linkify);
306            handler.highlighter = highlighter;
307            if let Some(with_cause_chain) = self.with_cause_chain {
308                if with_cause_chain {
309                    handler = handler.with_cause_chain();
310                } else {
311                    handler = handler.without_cause_chain();
312                }
313            }
314            if let Some(footer) = self.footer {
315                handler = handler.with_footer(footer);
316            }
317            if let Some(context_lines) = self.context_lines {
318                handler = handler.with_context_lines(context_lines);
319            }
320            if let Some(w) = self.tab_width {
321                handler = handler.tab_width(w);
322            }
323            if let Some(b) = self.break_words {
324                handler = handler.with_break_words(b)
325            }
326            if let Some(b) = self.wrap_lines {
327                handler = handler.with_wrap_lines(b)
328            }
329            if let Some(s) = self.word_separator {
330                handler = handler.with_word_separator(s)
331            }
332            if let Some(s) = self.word_splitter {
333                handler = handler.with_word_splitter(s)
334            }
335
336            MietteHandler {
337                inner: Box::new(handler),
338            }
339        }
340    }
341
342    pub(crate) fn is_graphical(&self) -> bool {
343        if let Some(force_narrated) = self.force_narrated {
344            !force_narrated
345        } else if let Some(force_graphical) = self.force_graphical {
346            force_graphical
347        } else if let Ok(env) = std::env::var("NO_GRAPHICS") {
348            env == "0"
349        } else {
350            true
351        }
352    }
353
354    // Detects known terminal apps based on env variables and returns true if
355    // they support rendering links.
356    pub(crate) fn use_links(&self) -> bool {
357        if let Some(linkify) = self.linkify {
358            linkify
359        } else {
360            syscall::supports_hyperlinks()
361        }
362    }
363
364    pub(crate) fn get_width(&self) -> usize {
365        self.width
366            .unwrap_or_else(|| syscall::terminal_width().unwrap_or(80))
367    }
368}
369
370/**
371A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a
372quasi-graphical way, using terminal colors, unicode drawing characters, and
373other such things.
374
375This is the default reporter bundled with `miette`.
376
377This printer can be customized by using
378[`GraphicalReportHandler::new_themed()`] and handing it a [`GraphicalTheme`] of
379your own creation (or using one of its own defaults).
380
381See [`set_hook`](crate::set_hook) for more details on customizing your global
382printer.
383*/
384#[allow(missing_debug_implementations)]
385pub struct MietteHandler {
386    inner: Box<dyn ReportHandler + Send + Sync>,
387}
388
389impl MietteHandler {
390    /// Creates a new [`MietteHandler`] with default settings.
391    pub fn new() -> Self {
392        Default::default()
393    }
394}
395
396impl Default for MietteHandler {
397    fn default() -> Self {
398        MietteHandlerOpts::new().build()
399    }
400}
401
402impl ReportHandler for MietteHandler {
403    fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        if f.alternate() {
405            return fmt::Debug::fmt(diagnostic, f);
406        }
407
408        self.inner.debug(diagnostic, f)
409    }
410}
411
412mod syscall {
413    use cfg_if::cfg_if;
414
415    #[inline]
416    pub(super) fn terminal_width() -> Option<usize> {
417        cfg_if! {
418            if #[cfg(any(feature = "fancy-no-syscall", miri))] {
419                None
420            } else {
421                terminal_size::terminal_size().map(|size| size.0 .0 as usize)
422            }
423        }
424    }
425
426    #[inline]
427    pub(super) fn supports_hyperlinks() -> bool {
428        cfg_if! {
429            if #[cfg(feature = "fancy-no-syscall")] {
430                false
431            } else {
432                supports_hyperlinks::on(supports_hyperlinks::Stream::Stderr)
433            }
434        }
435    }
436
437    #[cfg(feature = "syntect-highlighter")]
438    #[inline]
439    pub(super) fn supports_color() -> bool {
440        cfg_if! {
441            if #[cfg(feature = "fancy-no-syscall")] {
442                false
443            } else {
444                supports_color::on(supports_color::Stream::Stderr).is_some()
445            }
446        }
447    }
448
449    #[inline]
450    pub(super) fn supports_color_has_16m() -> Option<bool> {
451        cfg_if! {
452            if #[cfg(feature = "fancy-no-syscall")] {
453                None
454            } else {
455                supports_color::on(supports_color::Stream::Stderr).map(|color| color.has_16m)
456            }
457        }
458    }
459
460    #[inline]
461    pub(super) fn supports_unicode() -> bool {
462        cfg_if! {
463            if #[cfg(feature = "fancy-no-syscall")] {
464                false
465            } else {
466                supports_unicode::on(supports_unicode::Stream::Stderr)
467            }
468        }
469    }
470}