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#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
15pub enum RgbColors {
16 Always,
18 Preferred,
20 #[default]
22 Never,
23}
24
25#[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 pub fn new() -> Self {
66 Default::default()
67 }
68
69 pub fn terminal_links(mut self, linkify: bool) -> Self {
73 self.linkify = Some(linkify);
74 self
75 }
76
77 pub fn graphical_theme(mut self, theme: GraphicalTheme) -> Self {
82 self.theme = Some(theme);
83 self
84 }
85
86 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 pub fn without_syntax_highlighting(mut self) -> Self {
121 self.highlighter = Some(MietteHighlighter::nocolor());
122 self
123 }
124
125 pub fn width(mut self, width: usize) -> Self {
127 self.width = Some(width);
128 self
129 }
130
131 pub fn wrap_lines(mut self, wrap_lines: bool) -> Self {
137 self.wrap_lines = Some(wrap_lines);
138 self
139 }
140
141 pub fn break_words(mut self, break_words: bool) -> Self {
147 self.break_words = Some(break_words);
148 self
149 }
150 pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
152 self.word_separator = Some(word_separator);
153 self
154 }
155
156 pub fn word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
158 self.word_splitter = Some(word_splitter);
159 self
160 }
161 pub fn with_cause_chain(mut self) -> Self {
163 self.with_cause_chain = Some(true);
164 self
165 }
166
167 pub fn without_cause_chain(mut self) -> Self {
169 self.with_cause_chain = Some(false);
170 self
171 }
172
173 pub fn show_related_errors_as_siblings(mut self) -> Self {
175 self.show_related_as_nested = Some(false);
176 self
177 }
178
179 pub fn show_related_errors_as_nested(mut self) -> Self {
181 self.show_related_as_nested = Some(true);
182 self
183 }
184
185 pub fn color(mut self, color: bool) -> Self {
195 self.color = Some(color);
196 self
197 }
198
199 pub fn rgb_colors(mut self, color: RgbColors) -> Self {
211 self.rgb_colors = color;
212 self
213 }
214
215 pub fn unicode(mut self, unicode: bool) -> Self {
218 self.unicode = Some(unicode);
219 self
220 }
221
222 pub fn force_graphical(mut self, force: bool) -> Self {
225 self.force_graphical = Some(force);
226 self
227 }
228
229 pub fn force_narrated(mut self, force: bool) -> Self {
231 self.force_narrated = Some(force);
232 self
233 }
234
235 pub fn footer(mut self, footer: String) -> Self {
237 self.footer = Some(footer);
238 self
239 }
240
241 pub fn context_lines(mut self, context_lines: usize) -> Self {
243 self.context_lines = Some(context_lines);
244 self
245 }
246
247 pub fn tab_width(mut self, width: usize) -> Self {
249 self.tab_width = Some(width);
250 self
251 }
252
253 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 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#[allow(missing_debug_implementations)]
387pub struct MietteHandler {
388 inner: Box<dyn ReportHandler + Send + Sync>,
389}
390
391impl MietteHandler {
392 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#[allow(clippy::derivable_impls)]
439impl Default for HighlighterOption {
440 fn default() -> Self {
441 cfg_if! {
442 if #[cfg(feature = "syntect-highlighter")] {
443 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 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 assert_highlighter_opt!(
571 opts = MietteHandlerOpts::new().color(false),
572 supports_color = true,
573 expected = HighlighterOption::Disable,
574 );
575
576 assert_highlighter_opt!(
578 opts = MietteHandlerOpts::new(),
579 supports_color = false,
580 expected = HighlighterOption::Disable,
581 );
582
583 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 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 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}