Skip to main content

inspect_format/
style.rs

1//! Style representation and terminal styling primitives.
2
3#[cfg(feature = "color")]
4pub use anstyle::{Ansi256Color, AnsiColor, Color, Effects, Reset, RgbColor};
5
6/// Standard ANSI color representation for zero-dependency / no-color builds.
7#[cfg(not(feature = "color"))]
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum AnsiColor {
10    Black,
11    Red,
12    Green,
13    Yellow,
14    Blue,
15    Magenta,
16    Cyan,
17    White,
18    BrightBlack,
19    BrightRed,
20    BrightGreen,
21    BrightYellow,
22    BrightBlue,
23    BrightMagenta,
24    BrightCyan,
25    BrightWhite,
26}
27
28/// A terminal style defining text effects and foreground/background colors.
29///
30/// When the `color` feature is enabled, this is backed by `anstyle::Style`
31/// which renders zero-allocation ANSI escape codes. When disabled, this is a
32/// zero-cost no-op type that emits nothing.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub struct Style {
35    #[cfg(feature = "color")]
36    inner: anstyle::Style,
37}
38
39impl Style {
40    /// Create a new, unstyled style.
41    #[inline]
42    pub const fn new() -> Self {
43        #[cfg(feature = "color")]
44        {
45            Self { inner: anstyle::Style::new() }
46        }
47        #[cfg(not(feature = "color"))]
48        {
49            Self {}
50        }
51    }
52
53    /// Set bold effect.
54    #[inline]
55    #[allow(unused_mut)]
56    pub const fn bold(mut self) -> Self {
57        #[cfg(feature = "color")]
58        {
59            self.inner = self.inner.bold();
60        }
61        self
62    }
63
64    /// Set dimmed/faint effect.
65    #[inline]
66    #[allow(unused_mut)]
67    pub const fn dimmed(mut self) -> Self {
68        #[cfg(feature = "color")]
69        {
70            self.inner = self.inner.dimmed();
71        }
72        self
73    }
74
75    /// Set italic effect.
76    #[inline]
77    #[allow(unused_mut)]
78    pub const fn italic(mut self) -> Self {
79        #[cfg(feature = "color")]
80        {
81            self.inner = self.inner.italic();
82        }
83        self
84    }
85
86    /// Set underline effect.
87    #[inline]
88    #[allow(unused_mut)]
89    pub const fn underline(mut self) -> Self {
90        #[cfg(feature = "color")]
91        {
92            self.inner = self.inner.underline();
93        }
94        self
95    }
96
97    /// Set 24-bit Truecolor (RGB) foreground color.
98    #[inline]
99    #[allow(unused_mut)]
100    pub const fn fg_rgb(mut self, r: u8, g: u8, b: u8) -> Self {
101        #[cfg(feature = "color")]
102        {
103            self.inner = self.inner.fg_color(Some(anstyle::Color::Rgb(anstyle::RgbColor(r, g, b))));
104        }
105        let _ = (r, g, b);
106        self
107    }
108
109    /// Set 8-bit ANSI 256-color palette foreground color.
110    #[inline]
111    #[allow(unused_mut)]
112    pub const fn fg_ansi256(mut self, code: u8) -> Self {
113        #[cfg(feature = "color")]
114        {
115            self.inner =
116                self.inner.fg_color(Some(anstyle::Color::Ansi256(anstyle::Ansi256Color(code))));
117        }
118        let _ = code;
119        self
120    }
121
122    /// Set 4-bit standard ANSI 16-color foreground color.
123    #[inline]
124    #[allow(unused_mut)]
125    pub const fn fg_ansi(mut self, color: AnsiColor) -> Self {
126        #[cfg(feature = "color")]
127        {
128            self.inner = self.inner.fg_color(Some(anstyle::Color::Ansi(color)));
129        }
130        let _ = color;
131        self
132    }
133
134    /// True if no styling effects or colors are configured.
135    #[inline]
136    pub fn is_plain(&self) -> bool {
137        #[cfg(feature = "color")]
138        {
139            self.inner == anstyle::Style::new()
140        }
141        #[cfg(not(feature = "color"))]
142        {
143            true
144        }
145    }
146
147    /// Render the ANSI sequence to activate this style.
148    #[inline]
149    pub fn render(&self) -> impl core::fmt::Display {
150        #[cfg(feature = "color")]
151        {
152            self.inner.render()
153        }
154        #[cfg(not(feature = "color"))]
155        {
156            ""
157        }
158    }
159
160    /// Render the ANSI sequence to reset styles.
161    #[inline]
162    pub fn render_reset(&self) -> impl core::fmt::Display {
163        #[cfg(feature = "color")]
164        {
165            self.inner.render_reset()
166        }
167        #[cfg(not(feature = "color"))]
168        {
169            ""
170        }
171    }
172
173    #[cfg(feature = "color")]
174    /// Get the underlying anstyle::Style.
175    pub const fn inner(&self) -> anstyle::Style {
176        self.inner
177    }
178
179    /// Convert this style's colors to ANSI 256 colors.
180    pub fn to_ansi256(self) -> Self {
181        #[cfg(feature = "color")]
182        {
183            let mut styled = self.inner;
184            if let Some(color) = styled.get_fg_color() {
185                let converted = match color {
186                    anstyle::Color::Rgb(rgb) => anstyle::Color::Ansi256(anstyle::Ansi256Color(
187                        rgb_to_ansi256(rgb.0, rgb.1, rgb.2),
188                    )),
189                    other => other,
190                };
191                styled = styled.fg_color(Some(converted));
192            }
193            Self { inner: styled }
194        }
195        #[cfg(not(feature = "color"))]
196        {
197            self
198        }
199    }
200
201    /// Convert this style's colors to standard 16 ANSI colors.
202    pub fn to_ansi16(self) -> Self {
203        #[cfg(feature = "color")]
204        {
205            let mut styled = self.inner;
206            if let Some(color) = styled.get_fg_color() {
207                let converted = match color {
208                    anstyle::Color::Rgb(rgb) => {
209                        anstyle::Color::Ansi(rgb_to_ansi16(rgb.0, rgb.1, rgb.2))
210                    }
211                    anstyle::Color::Ansi256(code) => {
212                        let (r, g, b) = ansi256_to_rgb(code.0);
213                        anstyle::Color::Ansi(rgb_to_ansi16(r, g, b))
214                    }
215                    anstyle::Color::Ansi(ansi) => anstyle::Color::Ansi(ansi),
216                };
217                styled = styled.fg_color(Some(converted));
218            }
219            Self { inner: styled }
220        }
221        #[cfg(not(feature = "color"))]
222        {
223            self
224        }
225    }
226
227    /// Strip all foreground/background colors, preserving effects like Bold and Dimmed.
228    pub fn to_monochrome(self) -> Self {
229        #[cfg(feature = "color")]
230        {
231            let styled = self.inner.fg_color(None).bg_color(None);
232            Self { inner: styled }
233        }
234        #[cfg(not(feature = "color"))]
235        {
236            self
237        }
238    }
239}
240
241#[cfg(feature = "color")]
242impl From<anstyle::Style> for Style {
243    fn from(inner: anstyle::Style) -> Self {
244        Self { inner }
245    }
246}
247
248#[cfg(feature = "color")]
249impl From<Style> for anstyle::Style {
250    fn from(style: Style) -> Self {
251        style.inner
252    }
253}
254
255/// Convert an RGB color to the closest index in the standard xterm 256-color palette.
256pub fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
257    if r == g && g == b {
258        if r < 8 {
259            16
260        } else if r > 248 {
261            231
262        } else {
263            (((r as u16 - 8) * 24) / 240) as u8 + 232
264        }
265    } else {
266        let r_idx = ((r as u16 * 5) + 127) / 255;
267        let g_idx = ((g as u16 * 5) + 127) / 255;
268        let b_idx = ((b as u16 * 5) + 127) / 255;
269        (16 + 36 * r_idx + 6 * g_idx + b_idx) as u8
270    }
271}
272
273/// Convert an ANSI 256-color index to approximate (r, g, b) values.
274pub fn ansi256_to_rgb(code: u8) -> (u8, u8, u8) {
275    if code < 16 {
276        ANSI16_PALETTE[code as usize].1
277    } else if code < 232 {
278        let idx = code - 16;
279        let b = (idx % 6) * 51;
280        let g = ((idx / 6) % 6) * 51;
281        let r = (idx / 36) * 51;
282        (r, g, b)
283    } else {
284        let gray = (code - 232) * 10 + 8;
285        (gray, gray, gray)
286    }
287}
288
289static ANSI16_PALETTE: [(AnsiColor, (u8, u8, u8)); 16] = [
290    (AnsiColor::Black, (0, 0, 0)),
291    (AnsiColor::Red, (205, 49, 49)),
292    (AnsiColor::Green, (13, 188, 121)),
293    (AnsiColor::Yellow, (229, 229, 16)),
294    (AnsiColor::Blue, (36, 114, 200)),
295    (AnsiColor::Magenta, (188, 63, 188)),
296    (AnsiColor::Cyan, (17, 168, 205)),
297    (AnsiColor::White, (229, 229, 229)),
298    (AnsiColor::BrightBlack, (102, 102, 102)),
299    (AnsiColor::BrightRed, (241, 76, 76)),
300    (AnsiColor::BrightGreen, (35, 209, 139)),
301    (AnsiColor::BrightYellow, (245, 245, 67)),
302    (AnsiColor::BrightBlue, (59, 142, 234)),
303    (AnsiColor::BrightMagenta, (214, 112, 214)),
304    (AnsiColor::BrightCyan, (41, 184, 219)),
305    (AnsiColor::BrightWhite, (255, 255, 255)),
306];
307
308/// Convert an RGB color to the closest standard ANSI 16 color using Euclidean distance.
309pub fn rgb_to_ansi16(r: u8, g: u8, b: u8) -> AnsiColor {
310    let mut closest = AnsiColor::White;
311    let mut min_dist = u32::MAX;
312
313    for (color, (pr, pg, pb)) in ANSI16_PALETTE {
314        let dr = (r as i32) - (pr as i32);
315        let dg = (g as i32) - (pg as i32);
316        let db = (b as i32) - (pb as i32);
317        let dist = (dr * dr + dg * dg + db * db) as u32;
318        if dist < min_dist {
319            min_dist = dist;
320            closest = color;
321        }
322    }
323
324    closest
325}