blob: e983a557947934be3d58d4dbfde9b7cbc67efca9 [file] [log] [blame] [edit]
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)
}
}