1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
use std::fmt;

use crate::protocol::Diagnostic;
use crate::GraphicalReportHandler;
use crate::GraphicalTheme;
use crate::NarratableReportHandler;
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.

## Example

```no_run
miette::set_hook(Box::new(|_| {
    Box::new(miette::MietteHandlerOpts::new()
        .terminal_links(true)
        .unicode(false)
        .context_lines(3)
        .build())
}))
# .unwrap();
```
*/
#[derive(Default, Debug, Clone)]
pub struct MietteHandlerOpts {
    pub(crate) linkify: Option<bool>,
    pub(crate) width: Option<usize>,
    pub(crate) theme: Option<GraphicalTheme>,
    pub(crate) force_graphical: Option<bool>,
    pub(crate) force_narrated: Option<bool>,
    pub(crate) rgb_colors: RgbColors,
    pub(crate) color: Option<bool>,
    pub(crate) unicode: Option<bool>,
    pub(crate) footer: Option<String>,
    pub(crate) context_lines: Option<usize>,
    pub(crate) tab_width: Option<usize>,
    pub(crate) with_cause_chain: Option<bool>,
}

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 [`force_graphical()`](`MietteHandlerOpts::force_graphical) to force
    /// graphical mode. This option overrides
    /// [`color()`](`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 to 80.
    pub fn width(mut self, width: usize) -> Self {
        self.width = Some(width);
        self
    }

    /// Include the cause chain of the top-level error in the report.
    pub fn with_cause_chain(mut self) -> Self {
        self.with_cause_chain = Some(true);
        self
    }

    /// Do not include the cause chain of the top-level error in the report.
    pub fn without_cause_chain(mut self) -> Self {
        self.with_cause_chain = Some(false);
        self
    }

    /// 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
    }

    /// 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
    }

    /// 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, 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
    }

    /// Set a footer to be displayed at the bottom of the report.
    pub fn footer(mut self, footer: String) -> Self {
        self.footer = Some(footer);
        self
    }

    /// Sets the number of context lines before and after a span to display.
    pub fn context_lines(mut self, context_lines: usize) -> Self {
        self.context_lines = Some(context_lines);
        self
    }

    /// Set the displayed tab width in spaces.
    pub fn tab_width(mut self, width: usize) -> Self {
        self.tab_width = Some(width);
        self
    }

    /// Builds a [`MietteHandler`] from this builder.
    pub fn build(self) -> MietteHandler {
        let graphical = self.is_graphical();
        let width = self.get_width();
        if !graphical {
            let mut handler = NarratableReportHandler::new();
            if let Some(footer) = self.footer {
                handler = handler.with_footer(footer);
            }
            if let Some(context_lines) = self.context_lines {
                handler = handler.with_context_lines(context_lines);
            }
            if let Some(with_cause_chain) = self.with_cause_chain {
                if with_cause_chain {
                    handler = handler.with_cause_chain();
                } else {
                    handler = handler.without_cause_chain();
                }
            }
            MietteHandler {
                inner: Box::new(handler),
            }
        } else {
            let linkify = self.use_links();
            let characters = match self.unicode {
                Some(true) => ThemeCharacters::unicode(),
                Some(false) => ThemeCharacters::ascii(),
                None if supports_unicode::on(supports_unicode::Stream::Stderr) => {
                    ThemeCharacters::unicode()
                }
                None => ThemeCharacters::ascii(),
            };
            let styles = if self.color == Some(false) {
                ThemeStyles::none()
            } else if let Some(color) = supports_color::on(supports_color::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) {
                match self.rgb_colors {
                    RgbColors::Always => ThemeStyles::rgb(),
                    _ => ThemeStyles::ansi(),
                }
            } else {
                ThemeStyles::none()
            };
            let theme = self.theme.unwrap_or(GraphicalTheme { characters, styles });
            let mut handler = GraphicalReportHandler::new()
                .with_width(width)
                .with_links(linkify)
                .with_theme(theme);
            if let Some(with_cause_chain) = self.with_cause_chain {
                if with_cause_chain {
                    handler = handler.with_cause_chain();
                } else {
                    handler = handler.without_cause_chain();
                }
            }
            if let Some(footer) = self.footer {
                handler = handler.with_footer(footer);
            }
            if let Some(context_lines) = self.context_lines {
                handler = handler.with_context_lines(context_lines);
            }
            if let Some(w) = self.tab_width {
                handler = handler.tab_width(w);
            }
            MietteHandler {
                inner: Box::new(handler),
            }
        }
    }

    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 Ok(env) = std::env::var("NO_GRAPHICS") {
            env == "0"
        } else {
            true
        }
    }

    // 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(supports_hyperlinks::Stream::Stderr)
        }
    }

    #[cfg(not(miri))]
    pub(crate) fn get_width(&self) -> usize {
        self.width.unwrap_or_else(|| {
            terminal_size::terminal_size()
                .unwrap_or((terminal_size::Width(80), terminal_size::Height(0)))
                .0
                 .0 as usize
        })
    }

    #[cfg(miri)]
    // miri doesn't support a syscall (specifically ioctl)
    // performed by terminal_size, which causes test execution to fail
    // so when miri is running we'll just fallback to a constant
    pub(crate) fn get_width(&self) -> usize {
        self.width.unwrap_or(80)
    }
}

/**
A [`ReportHandler`] that displays a given [`Report`](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
[`GraphicalReportHandler::new_themed()`] and handing it a [`GraphicalTheme`] of
your own creation (or using one of its own defaults).

See [`set_hook`](crate::set_hook) for more details on customizing your global
printer.
*/
#[allow(missing_debug_implementations)]
pub struct MietteHandler {
    inner: Box<dyn ReportHandler + Send + Sync>,
}

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), f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if f.alternate() {
            return fmt::Debug::fmt(diagnostic, f);
        }

        self.inner.debug(diagnostic, f)
    }
}