1 use std::fmt;
2 
3 use crate::protocol::Diagnostic;
4 use crate::GraphicalReportHandler;
5 use crate::GraphicalTheme;
6 use crate::NarratableReportHandler;
7 use crate::ReportHandler;
8 use crate::ThemeCharacters;
9 use crate::ThemeStyles;
10 
11 /// Settings to control the color format used for graphical rendering.
12 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
13 pub enum RgbColors {
14     /// Use RGB colors even if the terminal does not support them
15     Always,
16     /// Use RGB colors instead of ANSI if the terminal supports RGB
17     Preferred,
18     /// Always use ANSI, regardless of terminal support for RGB
19     Never,
20 }
21 
22 impl Default for RgbColors {
default() -> RgbColors23     fn default() -> RgbColors {
24         RgbColors::Never
25     }
26 }
27 
28 /**
29 Create a custom [`MietteHandler`] from options.
30 
31 ## Example
32 
33 ```no_run
34 miette::set_hook(Box::new(|_| {
35     Box::new(miette::MietteHandlerOpts::new()
36         .terminal_links(true)
37         .unicode(false)
38         .context_lines(3)
39         .build())
40 }))
41 # .unwrap();
42 ```
43 */
44 #[derive(Default, Debug, Clone)]
45 pub struct MietteHandlerOpts {
46     pub(crate) linkify: Option<bool>,
47     pub(crate) width: Option<usize>,
48     pub(crate) theme: Option<GraphicalTheme>,
49     pub(crate) force_graphical: Option<bool>,
50     pub(crate) force_narrated: Option<bool>,
51     pub(crate) rgb_colors: RgbColors,
52     pub(crate) color: Option<bool>,
53     pub(crate) unicode: Option<bool>,
54     pub(crate) footer: Option<String>,
55     pub(crate) context_lines: Option<usize>,
56     pub(crate) tab_width: Option<usize>,
57     pub(crate) with_cause_chain: Option<bool>,
58 }
59 
60 impl MietteHandlerOpts {
61     /// Create a new `MietteHandlerOpts`.
new() -> Self62     pub fn new() -> Self {
63         Default::default()
64     }
65 
66     /// If true, specify whether the graphical handler will make codes be
67     /// clickable links in supported terminals. Defaults to auto-detection
68     /// based on known supported terminals.
terminal_links(mut self, linkify: bool) -> Self69     pub fn terminal_links(mut self, linkify: bool) -> Self {
70         self.linkify = Some(linkify);
71         self
72     }
73 
74     /// Set a graphical theme for the handler when rendering in graphical mode.
75     /// Use [`force_graphical()`](`MietteHandlerOpts::force_graphical) to force
76     /// graphical mode. This option overrides
77     /// [`color()`](`MietteHandlerOpts::color).
graphical_theme(mut self, theme: GraphicalTheme) -> Self78     pub fn graphical_theme(mut self, theme: GraphicalTheme) -> Self {
79         self.theme = Some(theme);
80         self
81     }
82 
83     /// Sets the width to wrap the report at. Defaults to 80.
width(mut self, width: usize) -> Self84     pub fn width(mut self, width: usize) -> Self {
85         self.width = Some(width);
86         self
87     }
88 
89     /// Include the cause chain of the top-level error in the report.
with_cause_chain(mut self) -> Self90     pub fn with_cause_chain(mut self) -> Self {
91         self.with_cause_chain = Some(true);
92         self
93     }
94 
95     /// Do not include the cause chain of the top-level error in the report.
without_cause_chain(mut self) -> Self96     pub fn without_cause_chain(mut self) -> Self {
97         self.with_cause_chain = Some(false);
98         self
99     }
100 
101     /// If true, colors will be used during graphical rendering, regardless
102     /// of whether or not the terminal supports them.
103     ///
104     /// If false, colors will never be used.
105     ///
106     /// If unspecified, colors will be used only if the terminal supports them.
107     ///
108     /// The actual format depends on the value of
109     /// [`MietteHandlerOpts::rgb_colors`].
color(mut self, color: bool) -> Self110     pub fn color(mut self, color: bool) -> Self {
111         self.color = Some(color);
112         self
113     }
114 
115     /// Controls which color format to use if colors are used in graphical
116     /// rendering.
117     ///
118     /// The default is `Never`.
119     ///
120     /// This value does not control whether or not colors are being used in the
121     /// first place. That is handled by the [`MietteHandlerOpts::color`]
122     /// setting. If colors are not being used, the value of `rgb_colors` has
123     /// no effect.
rgb_colors(mut self, color: RgbColors) -> Self124     pub fn rgb_colors(mut self, color: RgbColors) -> Self {
125         self.rgb_colors = color;
126         self
127     }
128 
129     /// If true, forces unicode display for graphical output. If set to false,
130     /// forces ASCII art display.
unicode(mut self, unicode: bool) -> Self131     pub fn unicode(mut self, unicode: bool) -> Self {
132         self.unicode = Some(unicode);
133         self
134     }
135 
136     /// If true, graphical rendering will be used regardless of terminal
137     /// detection.
force_graphical(mut self, force: bool) -> Self138     pub fn force_graphical(mut self, force: bool) -> Self {
139         self.force_graphical = Some(force);
140         self
141     }
142 
143     /// If true, forces use of the narrated renderer.
force_narrated(mut self, force: bool) -> Self144     pub fn force_narrated(mut self, force: bool) -> Self {
145         self.force_narrated = Some(force);
146         self
147     }
148 
149     /// Set a footer to be displayed at the bottom of the report.
footer(mut self, footer: String) -> Self150     pub fn footer(mut self, footer: String) -> Self {
151         self.footer = Some(footer);
152         self
153     }
154 
155     /// Sets the number of context lines before and after a span to display.
context_lines(mut self, context_lines: usize) -> Self156     pub fn context_lines(mut self, context_lines: usize) -> Self {
157         self.context_lines = Some(context_lines);
158         self
159     }
160 
161     /// Set the displayed tab width in spaces.
tab_width(mut self, width: usize) -> Self162     pub fn tab_width(mut self, width: usize) -> Self {
163         self.tab_width = Some(width);
164         self
165     }
166 
167     /// Builds a [`MietteHandler`] from this builder.
build(self) -> MietteHandler168     pub fn build(self) -> MietteHandler {
169         let graphical = self.is_graphical();
170         let width = self.get_width();
171         if !graphical {
172             let mut handler = NarratableReportHandler::new();
173             if let Some(footer) = self.footer {
174                 handler = handler.with_footer(footer);
175             }
176             if let Some(context_lines) = self.context_lines {
177                 handler = handler.with_context_lines(context_lines);
178             }
179             if let Some(with_cause_chain) = self.with_cause_chain {
180                 if with_cause_chain {
181                     handler = handler.with_cause_chain();
182                 } else {
183                     handler = handler.without_cause_chain();
184                 }
185             }
186             MietteHandler {
187                 inner: Box::new(handler),
188             }
189         } else {
190             let linkify = self.use_links();
191             let characters = match self.unicode {
192                 Some(true) => ThemeCharacters::unicode(),
193                 Some(false) => ThemeCharacters::ascii(),
194                 None if supports_unicode::on(supports_unicode::Stream::Stderr) => {
195                     ThemeCharacters::unicode()
196                 }
197                 None => ThemeCharacters::ascii(),
198             };
199             let styles = if self.color == Some(false) {
200                 ThemeStyles::none()
201             } else if let Some(color) = supports_color::on(supports_color::Stream::Stderr) {
202                 match self.rgb_colors {
203                     RgbColors::Always => ThemeStyles::rgb(),
204                     RgbColors::Preferred if color.has_16m => ThemeStyles::rgb(),
205                     _ => ThemeStyles::ansi(),
206                 }
207             } else if self.color == Some(true) {
208                 match self.rgb_colors {
209                     RgbColors::Always => ThemeStyles::rgb(),
210                     _ => ThemeStyles::ansi(),
211                 }
212             } else {
213                 ThemeStyles::none()
214             };
215             let theme = self.theme.unwrap_or(GraphicalTheme { characters, styles });
216             let mut handler = GraphicalReportHandler::new()
217                 .with_width(width)
218                 .with_links(linkify)
219                 .with_theme(theme);
220             if let Some(with_cause_chain) = self.with_cause_chain {
221                 if with_cause_chain {
222                     handler = handler.with_cause_chain();
223                 } else {
224                     handler = handler.without_cause_chain();
225                 }
226             }
227             if let Some(footer) = self.footer {
228                 handler = handler.with_footer(footer);
229             }
230             if let Some(context_lines) = self.context_lines {
231                 handler = handler.with_context_lines(context_lines);
232             }
233             if let Some(w) = self.tab_width {
234                 handler = handler.tab_width(w);
235             }
236             MietteHandler {
237                 inner: Box::new(handler),
238             }
239         }
240     }
241 
is_graphical(&self) -> bool242     pub(crate) fn is_graphical(&self) -> bool {
243         if let Some(force_narrated) = self.force_narrated {
244             !force_narrated
245         } else if let Some(force_graphical) = self.force_graphical {
246             force_graphical
247         } else if let Ok(env) = std::env::var("NO_GRAPHICS") {
248             env == "0"
249         } else {
250             true
251         }
252     }
253 
254     // Detects known terminal apps based on env variables and returns true if
255     // they support rendering links.
use_links(&self) -> bool256     pub(crate) fn use_links(&self) -> bool {
257         if let Some(linkify) = self.linkify {
258             linkify
259         } else {
260             supports_hyperlinks::on(supports_hyperlinks::Stream::Stderr)
261         }
262     }
263 
264     #[cfg(not(miri))]
get_width(&self) -> usize265     pub(crate) fn get_width(&self) -> usize {
266         self.width.unwrap_or_else(|| {
267             terminal_size::terminal_size()
268                 .unwrap_or((terminal_size::Width(80), terminal_size::Height(0)))
269                 .0
270                  .0 as usize
271         })
272     }
273 
274     #[cfg(miri)]
275     // miri doesn't support a syscall (specifically ioctl)
276     // performed by terminal_size, which causes test execution to fail
277     // so when miri is running we'll just fallback to a constant
get_width(&self) -> usize278     pub(crate) fn get_width(&self) -> usize {
279         self.width.unwrap_or(80)
280     }
281 }
282 
283 /**
284 A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a
285 quasi-graphical way, using terminal colors, unicode drawing characters, and
286 other such things.
287 
288 This is the default reporter bundled with `miette`.
289 
290 This printer can be customized by using
291 [`GraphicalReportHandler::new_themed()`] and handing it a [`GraphicalTheme`] of
292 your own creation (or using one of its own defaults).
293 
294 See [`set_hook`](crate::set_hook) for more details on customizing your global
295 printer.
296 */
297 #[allow(missing_debug_implementations)]
298 pub struct MietteHandler {
299     inner: Box<dyn ReportHandler + Send + Sync>,
300 }
301 
302 impl MietteHandler {
303     /// Creates a new [`MietteHandler`] with default settings.
new() -> Self304     pub fn new() -> Self {
305         Default::default()
306     }
307 }
308 
309 impl Default for MietteHandler {
default() -> Self310     fn default() -> Self {
311         MietteHandlerOpts::new().build()
312     }
313 }
314 
315 impl ReportHandler for MietteHandler {
debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result316     fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
317         if f.alternate() {
318             return fmt::Debug::fmt(diagnostic, f);
319         }
320 
321         self.inner.debug(diagnostic, f)
322     }
323 }
324