Skip to main content

miette/handlers/
theme.rs

1use std::{
2    env,
3    io::{self, IsTerminal},
4};
5
6use owo_colors::Style;
7
8/// Theme used by [`GraphicalReportHandler`](crate::GraphicalReportHandler).
9///
10/// Use one of the predefined constructors below.
11#[derive(Debug, Clone)]
12pub struct GraphicalTheme {
13    pub(crate) characters: ThemeCharacters,
14    pub(crate) styles: ThemeStyles,
15}
16
17fn force_color() -> bool {
18    // Assume CI can always print colors.
19    env::var("CI").is_ok() || env::var("FORCE_COLOR").is_ok_and(|env| env != "0")
20}
21
22impl Default for GraphicalTheme {
23    fn default() -> Self {
24        Self::new(io::stdout().is_terminal() && io::stderr().is_terminal())
25    }
26}
27
28impl GraphicalTheme {
29    /// Chooses a graphical theme based on terminal and environment support.
30    #[must_use]
31    pub(crate) fn new(is_terminal: bool) -> Self {
32        if force_color() {
33            return Self::unicode();
34        }
35        match env::var("NO_COLOR") {
36            _ if !is_terminal => Self::none(),
37            Ok(string) if string != "0" => Self::unicode_nocolor(),
38            _ => Self::unicode(),
39        }
40    }
41
42    /// Graphical theme that draws using both ansi colors and unicode
43    /// characters.
44    ///
45    /// Note that full rgb colors aren't enabled by default because they're
46    /// an accessibility hazard, especially in the context of terminal themes
47    /// that can change the background color and make hardcoded colors illegible.
48    /// Such themes typically remap ansi codes properly, treating them more
49    /// like CSS classes than specific colors.
50    #[must_use]
51    pub fn unicode() -> Self {
52        Self { characters: ThemeCharacters::unicode(), styles: ThemeStyles::rgb() }
53    }
54
55    /// Graphical theme that draws in monochrome, while still using unicode
56    /// characters.
57    #[must_use]
58    pub fn unicode_nocolor() -> Self {
59        Self { characters: ThemeCharacters::unicode(), styles: ThemeStyles::none() }
60    }
61
62    /// A "basic" graphical theme that skips colors and unicode characters and
63    /// just does monochrome ASCII art.
64    #[must_use]
65    pub fn none() -> Self {
66        Self { characters: ThemeCharacters::ascii(), styles: ThemeStyles::none() }
67    }
68
69    /// Style used for warning text.
70    #[must_use]
71    pub const fn warning_style(&self) -> Style {
72        self.styles.warning
73    }
74}
75
76#[derive(Debug, Clone)]
77#[expect(clippy::redundant_pub_crate, reason = "prevents public glob re-export")]
78pub(crate) struct ThemeStyles {
79    pub(crate) error: Style,
80    pub(crate) warning: Style,
81    pub(crate) advice: Style,
82    pub(crate) help: Style,
83    pub(crate) note: Style,
84    pub(crate) link: Style,
85    pub(crate) linum: Style,
86    pub(crate) highlights: [Style; 3],
87}
88
89fn style() -> Style {
90    Style::new()
91}
92
93impl ThemeStyles {
94    fn rgb() -> Self {
95        Self {
96            error: style().fg_rgb::<225, 80, 80>().bold(), // CHANGED: <255, 30, 30>
97            warning: style().fg_rgb::<244, 191, 117>().bold(),
98            advice: style().fg_rgb::<106, 159, 181>(),
99            help: style().fg_rgb::<106, 159, 181>(),
100            note: style().fg_rgb::<106, 159, 181>(),
101            link: style().fg_rgb::<92, 157, 255>().bold(),
102            linum: style().dimmed(),
103            highlights: [
104                style().fg_rgb::<246, 87, 248>(),
105                style().fg_rgb::<30, 201, 212>(),
106                style().fg_rgb::<145, 246, 111>(),
107            ],
108        }
109    }
110
111    fn none() -> Self {
112        Self {
113            error: style(),
114            warning: style(),
115            advice: style(),
116            help: style(),
117            note: style(),
118            link: style(),
119            linum: style(),
120            highlights: [style(); 3],
121        }
122    }
123}
124
125// ----------------------------------------
126// Most of these characters were taken from
127// https://github.com/zesterer/ariadne/blob/e3cb394cb56ecda116a0a1caecd385a49e7f6662/src/draw.rs
128
129#[derive(Debug, Clone, Copy, Eq, PartialEq)]
130#[expect(clippy::redundant_pub_crate, reason = "prevents public glob re-export")]
131pub(crate) struct ThemeCharacters {
132    pub(crate) hbar: char,
133    pub(crate) vbar: char,
134    pub(crate) vbar_break: char,
135
136    pub(crate) uarrow: char,
137    pub(crate) rarrow: char,
138
139    pub(crate) ltop: char,
140    pub(crate) lbot: char,
141
142    pub(crate) lcross: char,
143    pub(crate) rcross: char,
144
145    pub(crate) underbar: char,
146    pub(crate) underline: char,
147
148    pub(crate) error: &'static str,
149    pub(crate) warning: &'static str,
150    pub(crate) advice: &'static str,
151}
152
153impl ThemeCharacters {
154    const fn unicode() -> Self {
155        Self {
156            hbar: '─',
157            vbar: '│',
158            vbar_break: '·',
159            uarrow: '▲',
160            rarrow: '▶',
161            ltop: '╭',
162            lbot: '╰',
163            lcross: '├',
164            rcross: '┤',
165            underbar: '┬',
166            underline: '─',
167            error: "×",
168            warning: "⚠",
169            advice: "☞",
170        }
171    }
172
173    const fn ascii() -> Self {
174        Self {
175            hbar: '-',
176            vbar: '|',
177            vbar_break: ':',
178            uarrow: '^',
179            rarrow: '>',
180            ltop: ',',
181            lbot: '`',
182            lcross: '|',
183            rcross: '|',
184            underbar: '|',
185            underline: '^',
186            error: "x",
187            warning: "!",
188            advice: ">",
189        }
190    }
191}