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