miette/handlers/
theme.rs

1use std::{env, io::IsTerminal};
2
3use owo_colors::Style;
4
5/**
6Theme used by [`GraphicalReportHandler`](crate::GraphicalReportHandler) to
7render fancy [`Diagnostic`](crate::Diagnostic) reports.
8
9A theme consists of two things: the set of characters to be used for drawing,
10and the
11[`owo_colors::Style`](https://docs.rs/owo-colors/latest/owo_colors/struct.Style.html)s to be used to paint various items.
12
13You can create your own custom graphical theme using this type, or you can use
14one of the predefined ones using the methods below.
15*/
16#[derive(Debug, Clone)]
17pub struct GraphicalTheme {
18    /// Characters to be used for drawing.
19    pub characters: ThemeCharacters,
20    /// Styles to be used for painting.
21    pub styles: ThemeStyles,
22}
23
24fn force_color() -> bool {
25    // Assume CI can always print colors.
26    env::var("CI").is_ok() || env::var("FORCE_COLOR").is_ok_and(|env| env != "0")
27}
28
29impl Default for GraphicalTheme {
30    fn default() -> Self {
31        if force_color() {
32            return Self::unicode();
33        }
34        match std::env::var("NO_COLOR") {
35            _ if !std::io::stdout().is_terminal() || !std::io::stderr().is_terminal() => {
36                Self::none()
37            }
38            Ok(string) if string != "0" => Self::unicode_nocolor(),
39            _ => Self::unicode(),
40        }
41    }
42}
43
44impl GraphicalTheme {
45    pub fn new(is_terminal: bool) -> Self {
46        if force_color() {
47            return Self::unicode();
48        }
49        match std::env::var("NO_COLOR") {
50            _ if !is_terminal => Self::none(),
51            Ok(string) if string != "0" => Self::unicode_nocolor(),
52            _ => Self::unicode(),
53        }
54    }
55
56    /// ASCII-art-based graphical drawing, with ANSI styling.
57    pub fn ascii() -> Self {
58        Self { characters: ThemeCharacters::ascii(), styles: ThemeStyles::ansi() }
59    }
60
61    /// Graphical theme that draws using both ansi colors and unicode
62    /// characters.
63    ///
64    /// Note that full rgb colors aren't enabled by default because they're
65    /// an accessibility hazard, especially in the context of terminal themes
66    /// that can change the background color and make hardcoded colors illegible.
67    /// Such themes typically remap ansi codes properly, treating them more
68    /// like CSS classes than specific colors.
69    pub fn unicode() -> Self {
70        Self { characters: ThemeCharacters::unicode(), styles: ThemeStyles::rgb() }
71    }
72
73    /// Graphical theme that draws in monochrome, while still using unicode
74    /// characters.
75    pub fn unicode_nocolor() -> Self {
76        Self { characters: ThemeCharacters::unicode(), styles: ThemeStyles::none() }
77    }
78
79    /// A "basic" graphical theme that skips colors and unicode characters and
80    /// just does monochrome ascii art. If you want a completely non-graphical
81    /// rendering of your [`Diagnostic`](crate::Diagnostic)s, check out
82    /// [`NarratableReportHandler`](crate::NarratableReportHandler), or write
83    /// your own [`ReportHandler`](crate::ReportHandler)
84    pub fn none() -> Self {
85        Self { characters: ThemeCharacters::ascii(), styles: ThemeStyles::none() }
86    }
87}
88
89/**
90Styles for various parts of graphical rendering for the
91[`GraphicalReportHandler`](crate::GraphicalReportHandler).
92*/
93#[derive(Debug, Clone)]
94pub struct ThemeStyles {
95    /// Style to apply to things highlighted as "error".
96    pub error: Style,
97    /// Style to apply to things highlighted as "warning".
98    pub warning: Style,
99    /// Style to apply to things highlighted as "advice".
100    pub advice: Style,
101    /// Style to apply to the help text.
102    pub help: Style,
103    /// Style to apply to the note text.
104    pub note: Style,
105    /// Style to apply to filenames/links/URLs.
106    pub link: Style,
107    /// Style to apply to line numbers.
108    pub linum: Style,
109    /// Styles to cycle through (using `.iter().cycle()`), to render the lines
110    /// and text for diagnostic highlights.
111    pub highlights: Vec<Style>,
112}
113
114fn style() -> Style {
115    Style::new()
116}
117
118impl ThemeStyles {
119    /// Nice RGB colors.
120    /// [Credit](http://terminal.sexy/#FRUV0NDQFRUVrEFCkKlZ9L91ap-1qnWfdbWq0NDQUFBQrEFCkKlZ9L91ap-1qnWfdbWq9fX1).
121    pub fn rgb() -> Self {
122        Self {
123            error: style().fg_rgb::<225, 80, 80>().bold(), // CHANGED: <255, 30, 30>
124            warning: style().fg_rgb::<244, 191, 117>().bold(),
125            advice: style().fg_rgb::<106, 159, 181>(),
126            help: style().fg_rgb::<106, 159, 181>(),
127            note: style().fg_rgb::<106, 159, 181>(),
128            link: style().fg_rgb::<92, 157, 255>().bold(),
129            linum: style().dimmed(),
130            highlights: vec![
131                style().fg_rgb::<246, 87, 248>(),
132                style().fg_rgb::<30, 201, 212>(),
133                style().fg_rgb::<145, 246, 111>(),
134            ],
135        }
136    }
137
138    /// ANSI color-based styles.
139    pub fn ansi() -> Self {
140        Self {
141            error: style().red(),
142            warning: style().yellow(),
143            advice: style().cyan(),
144            help: style().cyan(),
145            note: style().cyan(),
146            link: style().cyan().underline().bold(),
147            linum: style().dimmed(),
148            highlights: vec![
149                style().magenta().bold(),
150                style().yellow().bold(),
151                style().green().bold(),
152            ],
153        }
154    }
155
156    /// No styling. Just regular ol' monochrome.
157    pub fn none() -> Self {
158        Self {
159            error: style(),
160            warning: style(),
161            advice: style(),
162            help: style(),
163            note: style(),
164            link: style(),
165            linum: style(),
166            highlights: vec![style()],
167        }
168    }
169}
170
171// ----------------------------------------
172// Most of these characters were taken from
173// https://github.com/zesterer/ariadne/blob/e3cb394cb56ecda116a0a1caecd385a49e7f6662/src/draw.rs
174
175/// Characters to be used when drawing when using
176/// [`GraphicalReportHandler`](crate::GraphicalReportHandler).
177#[allow(missing_docs)]
178#[derive(Debug, Clone, Eq, PartialEq)]
179pub struct ThemeCharacters {
180    pub hbar: char,
181    pub vbar: char,
182    pub xbar: char,
183    pub vbar_break: char,
184
185    pub uarrow: char,
186    pub rarrow: char,
187
188    pub ltop: char,
189    pub mtop: char,
190    pub rtop: char,
191    pub lbot: char,
192    pub rbot: char,
193    pub mbot: char,
194
195    pub lbox: char,
196    pub rbox: char,
197
198    pub lcross: char,
199    pub rcross: char,
200
201    pub underbar: char,
202    pub underline: char,
203
204    pub error: String,
205    pub warning: String,
206    pub advice: String,
207}
208
209impl ThemeCharacters {
210    /// Fancy unicode-based graphical elements.
211    pub fn unicode() -> Self {
212        Self {
213            hbar: '─',
214            vbar: '│',
215            xbar: '┼',
216            vbar_break: '·',
217            uarrow: '▲',
218            rarrow: '▶',
219            ltop: '╭',
220            mtop: '┬',
221            rtop: '╮',
222            lbot: '╰',
223            mbot: '┴',
224            rbot: '╯',
225            lbox: '[',
226            rbox: ']',
227            lcross: '├',
228            rcross: '┤',
229            underbar: '┬',
230            underline: '─',
231            error: "×".into(),
232            warning: "⚠".into(),
233            advice: "☞".into(),
234        }
235    }
236
237    /// Emoji-heavy unicode characters.
238    pub fn emoji() -> Self {
239        Self {
240            hbar: '─',
241            vbar: '│',
242            xbar: '┼',
243            vbar_break: '·',
244            uarrow: '▲',
245            rarrow: '▶',
246            ltop: '╭',
247            mtop: '┬',
248            rtop: '╮',
249            lbot: '╰',
250            mbot: '┴',
251            rbot: '╯',
252            lbox: '[',
253            rbox: ']',
254            lcross: '├',
255            rcross: '┤',
256            underbar: '┬',
257            underline: '─',
258            error: "💥".into(),
259            warning: "⚠️".into(),
260            advice: "💡".into(),
261        }
262    }
263
264    /// ASCII-art-based graphical elements. Works well on older terminals.
265    pub fn ascii() -> Self {
266        Self {
267            hbar: '-',
268            vbar: '|',
269            xbar: '+',
270            vbar_break: ':',
271            uarrow: '^',
272            rarrow: '>',
273            ltop: ',',
274            mtop: 'v',
275            rtop: '.',
276            lbot: '`',
277            mbot: '^',
278            rbot: '\'',
279            lbox: '[',
280            rbox: ']',
281            lcross: '|',
282            rcross: '|',
283            underbar: '|',
284            underline: '^',
285            error: "x".into(),
286            warning: "!".into(),
287            advice: ">".into(),
288        }
289    }
290}