error_stack/fmt/
color.rs

1use core::fmt::{self, Display, Formatter};
2
3use crate::{
4    fmt::r#override::{AtomicOverride, AtomicPreference},
5    Report,
6};
7
8/// The available modes of color support
9///
10/// Can be accessed through [`crate::fmt::HookContext::color_mode`], and set via
11/// [`Report::set_color_mode`]
12#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
13pub enum ColorMode {
14    /// User preference to disable all colors
15    None,
16
17    /// User preference to enable colors
18    Color,
19
20    /// User preference to enable styles, but discourage colors
21    ///
22    /// This is the same as [`ColorMode::Color`], but signals to the user that while colors are
23    /// supported, the user prefers instead the use of emphasis, like bold and italic text.
24    // The default is `ColorMode::Emphasis`, because colors are hard. ANSI colors are not
25    // standardized, and some colors may not show at all.
26    #[default]
27    Emphasis,
28}
29
30impl ColorMode {
31    pub(super) fn load() -> Self {
32        COLOR_OVERRIDE.load()
33    }
34}
35
36/// Value layout:
37/// `0x00`: `ColorMode::None`
38/// `0x01`: `ColorMode::Color`
39/// `0x02`: `ColorMode::Emphasis`
40///
41/// all others: [`Self::default`]
42impl AtomicPreference for ColorMode {
43    fn from_u8(value: u8) -> Self {
44        match value {
45            0x00 => Self::None,
46            0x01 => Self::Color,
47            0x02 => Self::Emphasis,
48            _ => Self::default(),
49        }
50    }
51
52    fn into_u8(self) -> u8 {
53        match self {
54            Self::None => 0x00,
55            Self::Color => 0x01,
56            Self::Emphasis => 0x02,
57        }
58    }
59}
60
61static COLOR_OVERRIDE: AtomicOverride<ColorMode> = AtomicOverride::new();
62
63impl Report<()> {
64    /// Set the color mode preference
65    ///
66    /// If no [`ColorMode`] is set, it defaults to [`ColorMode::Emphasis`].
67    ///
68    /// # Example
69    ///
70    /// ```rust
71    /// # // we only test the snapshot on nightly, therefore report is unused (so is render)
72    /// # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
73    /// use std::io::{Error, ErrorKind};
74    /// use owo_colors::OwoColorize;
75    ///
76    /// use error_stack::{report, Report};
77    /// use error_stack::fmt::ColorMode;
78    ///
79    /// struct Suggestion(&'static str);
80    ///
81    /// Report::install_debug_hook::<Suggestion>(|Suggestion(value), context| {
82    ///     let body = format!("suggestion: {value}");
83    ///     match context.color_mode() {
84    ///         ColorMode::Color => context.push_body(body.green().to_string()),
85    ///         ColorMode::Emphasis => context.push_body(body.italic().to_string()),
86    ///         ColorMode::None => context.push_body(body)
87    ///     };
88    /// });
89    ///
90    /// let report =
91    ///     report!(Error::from(ErrorKind::InvalidInput)).attach(Suggestion("oh no, try again"));
92    ///
93    /// # fn render(value: String) -> String {
94    /// #     let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?:  .*\n)*  .*").unwrap();
95    /// #     let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
96    /// #
97    /// #     let value = backtrace.replace_all(&value, "backtrace no. $1\n  [redacted]");
98    /// #     let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
99    /// #
100    /// #     ansi_to_html::convert(value.as_ref()).unwrap()
101    /// # }
102    /// #
103    /// Report::set_color_mode(ColorMode::None);
104    /// println!("{report:?}");
105    /// # #[cfg(nightly)]
106    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__preference_none.snap")].assert_eq(&render(format!("{report:?}")));
107    ///
108    /// Report::set_color_mode(ColorMode::Emphasis);
109    /// println!("{report:?}");
110    /// # #[cfg(nightly)]
111    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__preference_emphasis.snap")].assert_eq(&render(format!("{report:?}")));
112    ///
113    /// Report::set_color_mode(ColorMode::Color);
114    /// println!("{report:?}");
115    /// # #[cfg(nightly)]
116    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__preference_color.snap")].assert_eq(&render(format!("{report:?}")));
117    /// ```
118    ///
119    /// Which will result in something like:
120    ///
121    /// <pre>
122    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__preference_none.snap"))]
123    /// </pre>
124    ///
125    /// <pre>
126    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__preference_emphasis.snap"))]
127    /// </pre>
128    ///
129    /// <pre>
130    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__preference_color.snap"))]
131    /// </pre>
132    pub fn set_color_mode(mode: ColorMode) {
133        COLOR_OVERRIDE.store(mode);
134    }
135}
136
137#[derive(Copy, Clone, Debug, Eq, PartialEq)]
138pub(crate) enum Color {
139    Black,
140    Red,
141}
142
143impl Color {
144    const fn digit(self) -> u8 {
145        match self {
146            Self::Black => b'0',
147            Self::Red => b'1',
148        }
149    }
150}
151
152#[derive(Copy, Clone, Debug, Eq, PartialEq)]
153pub(crate) struct Foreground {
154    color: Color,
155    bright: bool,
156}
157
158impl Foreground {
159    fn start_ansi(self, sequence: &mut ControlSequence) -> fmt::Result {
160        let Self { color, bright } = self;
161
162        let buffer = &[if bright { b'9' } else { b'3' }, color.digit()];
163        // This should never fail because both are valid ASCII
164        let control = core::str::from_utf8(buffer).expect("should be valid utf-8 buffer");
165
166        sequence.push_control(control)
167    }
168
169    fn end_ansi(sequence: &mut ControlSequence) -> fmt::Result {
170        sequence.push_control("39")
171    }
172}
173
174struct ControlSequence<'a, 'b> {
175    fmt: &'a mut Formatter<'b>,
176    empty: bool,
177}
178
179impl<'a, 'b> ControlSequence<'a, 'b> {
180    fn new(fmt: &'a mut Formatter<'b>) -> Self {
181        Self { fmt, empty: true }
182    }
183
184    fn finish(self) -> Result<&'a mut Formatter<'b>, fmt::Error> {
185        if !self.empty {
186            // we wrote a specific formatting character, therefore we need to end
187            self.fmt.write_str("m")?;
188        }
189
190        Ok(self.fmt)
191    }
192}
193
194impl ControlSequence<'_, '_> {
195    fn push_control(&mut self, control: &str) -> fmt::Result {
196        if self.empty {
197            self.fmt.write_str("\u{1b}[")?;
198        } else {
199            self.fmt.write_str(";")?;
200        }
201
202        self.fmt.write_str(control)?;
203        self.empty = false;
204
205        Ok(())
206    }
207}
208
209#[derive(Copy, Clone, Debug, Eq, PartialEq)]
210pub(crate) struct DisplayStyle {
211    bold: bool,
212    italic: bool,
213}
214
215impl DisplayStyle {
216    pub(crate) const fn new() -> Self {
217        Self {
218            bold: false,
219            italic: false,
220        }
221    }
222
223    pub(crate) fn set_bold(&mut self, value: bool) {
224        self.bold = value;
225    }
226
227    pub(crate) fn with_bold(mut self, value: bool) -> Self {
228        self.set_bold(value);
229        self
230    }
231
232    pub(crate) fn set_italic(&mut self, value: bool) {
233        self.italic = value;
234    }
235
236    pub(crate) fn with_italic(mut self, value: bool) -> Self {
237        self.set_italic(value);
238        self
239    }
240}
241
242impl DisplayStyle {
243    fn start_ansi(self, sequence: &mut ControlSequence) -> fmt::Result {
244        if self.bold {
245            sequence.push_control("1")?;
246        }
247
248        if self.italic {
249            sequence.push_control("3")?;
250        }
251
252        Ok(())
253    }
254
255    fn end_ansi(self, sequence: &mut ControlSequence) -> fmt::Result {
256        if self.bold {
257            sequence.push_control("22")?;
258        }
259
260        if self.italic {
261            sequence.push_control("23")?;
262        }
263
264        Ok(())
265    }
266}
267
268#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
269pub(crate) struct Style {
270    display: Option<DisplayStyle>,
271    foreground: Option<Foreground>,
272}
273
274impl Style {
275    pub(crate) const fn new() -> Self {
276        Self {
277            display: None,
278            foreground: None,
279        }
280    }
281
282    pub(crate) const fn apply<T: Display>(self, value: &T) -> StyleDisplay<T> {
283        StyleDisplay { style: self, value }
284    }
285
286    pub(crate) fn set_foreground(&mut self, color: Color, bright: bool) {
287        self.foreground = Some(Foreground { color, bright });
288    }
289
290    pub(crate) fn set_display(&mut self, style: DisplayStyle) {
291        self.display = Some(style);
292    }
293}
294
295pub(crate) struct StyleDisplay<'a, T: Display> {
296    style: Style,
297    value: &'a T,
298}
299
300impl<'a, T: Display> Display for StyleDisplay<'a, T> {
301    fn fmt(&self, mut f: &mut Formatter<'_>) -> fmt::Result {
302        let mut sequence = ControlSequence::new(f);
303
304        if let Some(display) = self.style.display {
305            display.start_ansi(&mut sequence)?;
306        }
307
308        if let Some(foreground) = self.style.foreground {
309            foreground.start_ansi(&mut sequence)?;
310        }
311
312        f = sequence.finish()?;
313
314        Display::fmt(&self.value, f)?;
315
316        let mut sequence = ControlSequence::new(f);
317
318        if let Some(display) = self.style.display {
319            display.end_ansi(&mut sequence)?;
320        }
321
322        if self.style.foreground.is_some() {
323            Foreground::end_ansi(&mut sequence)?;
324        }
325
326        sequence.finish()?;
327
328        Ok(())
329    }
330}