spdlog/
terminal_style.rs

1//! Provides stuff related to terminal style.
2//!
3//! They are referenced from [ANSI escape code].
4//!
5//! [ANSI escape code]: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
6
7use std::io;
8
9use crate::Level;
10
11/// Text color for terminal rendering.
12#[allow(missing_docs)]
13#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
14pub enum Color {
15    Black,
16    Red,
17    Green,
18    Yellow,
19    Blue,
20    Magenta,
21    Cyan,
22    White,
23}
24
25impl Color {
26    // Gets foreground color terminal escape code.
27    #[must_use]
28    pub(crate) fn fg_code(&self) -> &'static str {
29        match self {
30            Color::Black => "\x1b[30m",
31            Color::Red => "\x1b[31m",
32            Color::Green => "\x1b[32m",
33            Color::Yellow => "\x1b[33m",
34            Color::Blue => "\x1b[34m",
35            Color::Magenta => "\x1b[35m",
36            Color::Cyan => "\x1b[36m",
37            Color::White => "\x1b[37m",
38        }
39    }
40
41    // Gets background color terminal escape code.
42    #[must_use]
43    pub(crate) fn bg_code(&self) -> &'static str {
44        match self {
45            Color::Black => "\x1b[40m",
46            Color::Red => "\x1b[41m",
47            Color::Green => "\x1b[42m",
48            Color::Yellow => "\x1b[43m",
49            Color::Blue => "\x1b[44m",
50            Color::Magenta => "\x1b[45m",
51            Color::Cyan => "\x1b[46m",
52            Color::White => "\x1b[47m",
53        }
54    }
55}
56
57/// Expresses how to render a piece of text in the terminal.
58#[derive(Clone, Eq, PartialEq, Hash, Debug)]
59pub struct Style {
60    color: Option<Color>,
61    bg_color: Option<Color>,
62    bold: bool,
63    faint: bool,
64    italic: bool,
65    underline: bool,
66    slow_blink: bool,
67    rapid_blink: bool,
68    invert: bool,
69    conceal: bool,
70    strikethrough: bool,
71    reset: bool,
72}
73
74impl Style {
75    /// Constructs a `Style` with no styles.
76    #[allow(clippy::new_without_default)]
77    #[deprecated(
78        since = "0.3.0",
79        note = "it may be removed in the future, use `Style::builder()` instead"
80    )]
81    #[must_use]
82    pub fn new() -> Style {
83        Style::builder().build()
84    }
85
86    /// Gets a [`StyleBuilder`].
87    #[must_use]
88    pub fn builder() -> StyleBuilder {
89        StyleBuilder {
90            style: Style {
91                color: None,
92                bg_color: None,
93                bold: false,
94                faint: false,
95                italic: false,
96                underline: false,
97                slow_blink: false,
98                rapid_blink: false,
99                invert: false,
100                conceal: false,
101                strikethrough: false,
102                reset: false,
103            },
104        }
105    }
106
107    pub(crate) fn write_start(&self, dest: &mut impl io::Write) -> io::Result<()> {
108        if self.reset {
109            dest.write_all(Self::reset_code().as_bytes())?;
110            return Ok(());
111        }
112        if let Some(color) = self.color {
113            dest.write_all(color.fg_code().as_bytes())?;
114        }
115        if let Some(color) = self.bg_color {
116            dest.write_all(color.bg_code().as_bytes())?;
117        }
118        if self.bold {
119            dest.write_all("\x1b[1m".as_bytes())?;
120        }
121        if self.faint {
122            dest.write_all("\x1b[2m".as_bytes())?;
123        }
124        if self.italic {
125            dest.write_all("\x1b[3m".as_bytes())?;
126        }
127        if self.underline {
128            dest.write_all("\x1b[4m".as_bytes())?;
129        }
130        if self.slow_blink {
131            dest.write_all("\x1b[5m".as_bytes())?;
132        }
133        if self.rapid_blink {
134            dest.write_all("\x1b[6m".as_bytes())?;
135        }
136        if self.invert {
137            dest.write_all("\x1b[7m".as_bytes())?;
138        }
139        if self.conceal {
140            dest.write_all("\x1b[8m".as_bytes())?;
141        }
142        if self.strikethrough {
143            dest.write_all("\x1b[9m".as_bytes())?;
144        }
145        Ok(())
146    }
147
148    pub(crate) fn write_end(&self, dest: &mut impl io::Write) -> io::Result<()> {
149        dest.write_all(Self::reset_code().as_bytes())
150    }
151
152    #[must_use]
153    fn reset_code() -> &'static str {
154        "\x1b[m"
155    }
156}
157
158#[allow(missing_docs)]
159#[derive(Clone, Eq, PartialEq, Hash, Debug)]
160pub struct StyleBuilder {
161    style: Style,
162}
163
164pub(crate) mod macros {
165    macro_rules! impl_style_builder_setters {
166        ($builder_type:ident =>) => {};
167        ($builder_type:ident => $visibility:vis $field_name:ident: Option<$field_type:ty>, $($tail:tt)*) => {
168            #[allow(missing_docs)]
169            $visibility fn $field_name(&mut self, $field_name: $field_type) -> &mut $builder_type {
170                self.style.$field_name = Some($field_name);
171                self
172            }
173            macros::impl_style_builder_setters! { $builder_type => $($tail)* }
174        };
175        ($builder_type:ident => $visibility:vis $field_name:ident: bool, $($tail:tt)*) => {
176            #[allow(missing_docs)]
177            $visibility fn $field_name(&mut self) -> &mut $builder_type {
178                self.style.$field_name = true;
179                self
180            }
181            macros::impl_style_builder_setters! { $builder_type => $($tail)* }
182        };
183    }
184    pub(crate) use impl_style_builder_setters;
185}
186
187impl StyleBuilder {
188    /// Constructs a `StyleBuilder`.
189    #[allow(clippy::new_without_default)]
190    #[deprecated(
191        since = "0.3.0",
192        note = "it may be removed in the future, use `Style::builder()` instead"
193    )]
194    #[must_use]
195    pub fn new() -> StyleBuilder {
196        Style::builder()
197    }
198
199    macros::impl_style_builder_setters! {
200        StyleBuilder =>
201        pub reset: bool,
202        pub color: Option<Color>,
203        pub bg_color: Option<Color>,
204        pub bold: bool,
205        pub faint: bool,
206        pub italic: bool,
207        pub underline: bool,
208        pub slow_blink: bool,
209        pub rapid_blink: bool,
210        pub invert: bool,
211        pub conceal: bool,
212        pub strikethrough: bool,
213    }
214
215    /// Builds a [`Style`].
216    #[must_use]
217    pub fn build(&mut self) -> Style {
218        self.style.clone()
219    }
220}
221
222/// Represents terminal style enabling mode.
223#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
224pub enum StyleMode {
225    /// Always output style escape codes.
226    Always,
227    /// Output style escape codes only when the target is detected as a
228    /// terminal.
229    Auto,
230    /// Always do not output style escape codes.
231    Never,
232}
233
234#[derive(Clone, Eq, PartialEq, Hash, Debug)]
235pub(crate) struct LevelStyles([Style; Level::count()]);
236
237impl LevelStyles {
238    #[allow(dead_code)]
239    #[must_use]
240    pub(crate) fn style(&self, level: Level) -> &Style {
241        &self.0[level as usize]
242    }
243
244    #[allow(dead_code)]
245    pub(crate) fn set_style(&mut self, level: Level, style: Style) {
246        self.0[level as usize] = style;
247    }
248}
249
250impl Default for LevelStyles {
251    fn default() -> LevelStyles {
252        LevelStyles([
253            Style::builder().bg_color(Color::Red).bold().build(), // Critical
254            Style::builder().color(Color::Red).bold().build(),    // Error
255            Style::builder().color(Color::Yellow).bold().build(), // Warn
256            Style::builder().color(Color::Green).build(),         // Info
257            Style::builder().color(Color::Cyan).build(),          // Debug
258            Style::builder().color(Color::White).build(),         // Trace
259        ])
260    }
261}