Skip to main content

gix_config_value/
color.rs

1use std::{borrow::Cow, fmt::Display, str::FromStr};
2
3use bstr::{BStr, BString};
4
5use crate::{Color, Error};
6
7impl Display for Color {
8    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9        let mut write_space = None;
10        if let Some(fg) = self.foreground {
11            fg.fmt(f)?;
12            write_space = Some(());
13        }
14
15        if let Some(bg) = self.background {
16            if write_space.take().is_some() {
17                write!(f, " ")?;
18            }
19            bg.fmt(f)?;
20            write_space = Some(());
21        }
22
23        if !self.attributes.is_empty() {
24            if write_space.take().is_some() {
25                write!(f, " ")?;
26            }
27            self.attributes.fmt(f)?;
28        }
29        Ok(())
30    }
31}
32
33fn color_err(input: impl Into<BString>) -> Error {
34    Error::new(
35        "Colors are specific color values and their attributes, like 'brightred', or 'blue'",
36        input,
37    )
38}
39
40impl TryFrom<&BStr> for Color {
41    type Error = Error;
42
43    fn try_from(s: &BStr) -> Result<Self, Self::Error> {
44        let s = std::str::from_utf8(s).map_err(|err| color_err(s).with_err(err))?;
45        enum ColorItem {
46            Value(Name),
47            Attr(Attribute),
48        }
49
50        let items = s.split_whitespace().filter_map(|s| {
51            if s.is_empty() {
52                return None;
53            }
54
55            Some(
56                Name::from_str(s)
57                    .map(ColorItem::Value)
58                    .or_else(|_| Attribute::from_str(s).map(ColorItem::Attr)),
59            )
60        });
61
62        let mut foreground = None;
63        let mut background = None;
64        let mut attributes = Attribute::empty();
65        for item in items {
66            match item {
67                Ok(item) => match item {
68                    ColorItem::Value(v) => {
69                        if foreground.is_none() {
70                            foreground = Some(v);
71                        } else if background.is_none() {
72                            background = Some(v);
73                        } else {
74                            return Err(color_err(s));
75                        }
76                    }
77                    ColorItem::Attr(a) => attributes |= a,
78                },
79                Err(_) => return Err(color_err(s)),
80            }
81        }
82
83        Ok(Color {
84            foreground,
85            background,
86            attributes,
87        })
88    }
89}
90
91impl TryFrom<&str> for Color {
92    type Error = Error;
93
94    fn try_from(value: &str) -> Result<Self, Self::Error> {
95        Self::try_from(BStr::new(value))
96    }
97}
98
99impl TryFrom<Cow<'_, BStr>> for Color {
100    type Error = Error;
101
102    fn try_from(c: Cow<'_, BStr>) -> Result<Self, Self::Error> {
103        Self::try_from(c.as_ref())
104    }
105}
106
107impl TryFrom<BString> for Color {
108    type Error = Error;
109
110    fn try_from(value: BString) -> Result<Self, Self::Error> {
111        Self::try_from(BStr::new(&value))
112    }
113}
114
115/// Discriminating enum for names of [`Color`] values.
116///
117/// `git-config` supports the eight standard colors, their bright variants, an
118/// ANSI color code, or a 24-bit hex value prefixed with an octothorpe/hash.
119/// Color names and the `bright` prefix are matched case-insensitively, and
120/// `bright` may only precede one of the eight standard colors.
121#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
122pub enum Name {
123    /// The `normal` color name.
124    Normal,
125    /// The terminal's default color.
126    Default,
127    /// Black.
128    Black,
129    /// Bright black.
130    BrightBlack,
131    /// Red.
132    Red,
133    /// Bright red.
134    BrightRed,
135    /// Green.
136    Green,
137    /// Bright green.
138    BrightGreen,
139    /// Yellow.
140    Yellow,
141    /// Bright yellow.
142    BrightYellow,
143    /// Blue.
144    Blue,
145    /// Bright blue.
146    BrightBlue,
147    /// Magenta.
148    Magenta,
149    /// Bright magenta.
150    BrightMagenta,
151    /// Cyan.
152    Cyan,
153    /// Bright cyan.
154    BrightCyan,
155    /// White.
156    White,
157    /// Bright white.
158    BrightWhite,
159    /// A color from the ANSI 256-color palette.
160    Ansi(
161        /// The palette index.
162        u8,
163    ),
164    /// A 24-bit RGB color.
165    Rgb(
166        /// The red component.
167        u8,
168        /// The green component.
169        u8,
170        /// The blue component.
171        u8,
172    ),
173}
174
175impl Display for Name {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        match self {
178            Self::Normal => write!(f, "normal"),
179            Self::Default => write!(f, "default"),
180            Self::Black => write!(f, "black"),
181            Self::BrightBlack => write!(f, "brightblack"),
182            Self::Red => write!(f, "red"),
183            Self::BrightRed => write!(f, "brightred"),
184            Self::Green => write!(f, "green"),
185            Self::BrightGreen => write!(f, "brightgreen"),
186            Self::Yellow => write!(f, "yellow"),
187            Self::BrightYellow => write!(f, "brightyellow"),
188            Self::Blue => write!(f, "blue"),
189            Self::BrightBlue => write!(f, "brightblue"),
190            Self::Magenta => write!(f, "magenta"),
191            Self::BrightMagenta => write!(f, "brightmagenta"),
192            Self::Cyan => write!(f, "cyan"),
193            Self::BrightCyan => write!(f, "brightcyan"),
194            Self::White => write!(f, "white"),
195            Self::BrightWhite => write!(f, "brightwhite"),
196            Self::Ansi(num) => num.fmt(f),
197            Self::Rgb(r, g, b) => write!(f, "#{r:02x}{g:02x}{b:02x}"),
198        }
199    }
200}
201
202#[cfg(feature = "serde")]
203impl serde::Serialize for Name {
204    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
205    where
206        S: serde::Serializer,
207    {
208        serializer.serialize_str(&self.to_string())
209    }
210}
211
212impl FromStr for Name {
213    type Err = Error;
214
215    fn from_str(s: &str) -> Result<Self, Self::Err> {
216        const BASIC: &[(&str, Name, Name)] = &[
217            ("black", Name::Black, Name::BrightBlack),
218            ("red", Name::Red, Name::BrightRed),
219            ("green", Name::Green, Name::BrightGreen),
220            ("yellow", Name::Yellow, Name::BrightYellow),
221            ("blue", Name::Blue, Name::BrightBlue),
222            ("magenta", Name::Magenta, Name::BrightMagenta),
223            ("cyan", Name::Cyan, Name::BrightCyan),
224            ("white", Name::White, Name::BrightWhite),
225        ];
226
227        if s.eq_ignore_ascii_case("normal") {
228            return Ok(Self::Normal);
229        }
230
231        let (name, is_bright) = match s.split_at_checked("bright".len()) {
232            Some((prefix, rest)) if prefix.eq_ignore_ascii_case("bright") => (rest, true),
233            _ => (s, false),
234        };
235
236        for &(basic, plain, brightened) in BASIC {
237            if name.eq_ignore_ascii_case(basic) {
238                return Ok(if is_bright { brightened } else { plain });
239            }
240        }
241
242        if is_bright {
243            return Err(color_err(s));
244        }
245
246        if s.eq_ignore_ascii_case("normal") || s == "-1" {
247            return Ok(Self::Normal);
248        }
249
250        if s.eq_ignore_ascii_case("default") {
251            return Ok(Self::Default);
252        }
253
254        if let Ok(v) = u8::from_str(s) {
255            return Ok(Self::Ansi(v));
256        }
257
258        if let Some(s) = s.strip_prefix('#') {
259            if s.len() == 6 && s.is_char_boundary(2) && s.is_char_boundary(4) && s.is_char_boundary(6) {
260                let rgb = (
261                    u8::from_str_radix(&s[..2], 16),
262                    u8::from_str_radix(&s[2..4], 16),
263                    u8::from_str_radix(&s[4..], 16),
264                );
265
266                if let (Ok(r), Ok(g), Ok(b)) = rgb {
267                    return Ok(Self::Rgb(r, g, b));
268                }
269            }
270        }
271
272        Err(color_err(s))
273    }
274}
275
276impl TryFrom<&BStr> for Name {
277    type Error = Error;
278
279    fn try_from(s: &BStr) -> Result<Self, Self::Error> {
280        Self::from_str(std::str::from_utf8(s).map_err(|err| color_err(s).with_err(err))?)
281    }
282}
283
284bitflags::bitflags! {
285    /// Discriminating enum for [`Color`] attributes.
286    ///
287    /// `git-config` supports modifiers and their negators. The negating color
288    /// attributes are equivalent to having a `no` or `no-` prefix to the normal
289    /// variant.
290    #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
291    pub struct Attribute: u32 {
292        /// Use bold or increased-intensity text.
293        const BOLD = 1 << 1;
294        /// Use dim or decreased-intensity text.
295        const DIM = 1 << 2;
296        /// Use italic text.
297        const ITALIC = 1 << 3;
298        /// Underline text.
299        const UL = 1 << 4;
300        /// Blink text.
301        const BLINK = 1 << 5;
302        /// Reverse the foreground and background colors.
303        const REVERSE = 1 << 6;
304        /// Strike through text.
305        const STRIKE = 1 << 7;
306        /// Parse the `reset` attribute, which Git otherwise leaves without an effect here.
307        const RESET = 1 << 8;
308
309        /// Disable dim text.
310        const NO_DIM = 1 << 21;
311        /// Disable bold text.
312        const NO_BOLD = 1 << 22;
313        /// Disable italic text.
314        const NO_ITALIC = 1 << 23;
315        /// Disable underlining.
316        const NO_UL = 1 << 24;
317        /// Disable blinking.
318        const NO_BLINK = 1 << 25;
319        /// Disable reversed colors.
320        const NO_REVERSE = 1 << 26;
321        /// Disable strikethrough.
322        const NO_STRIKE = 1 << 27;
323    }
324}
325
326impl Display for Attribute {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        let mut write_space = None;
329        for bit in 1..std::mem::size_of::<Attribute>() * 8 {
330            let attr = match Attribute::from_bits(1 << bit) {
331                Some(attr) => attr,
332                None => continue,
333            };
334            if self.contains(attr) {
335                if write_space.take().is_some() {
336                    write!(f, " ")?;
337                }
338                match attr {
339                    Attribute::RESET => write!(f, "reset"),
340                    Attribute::BOLD => write!(f, "bold"),
341                    Attribute::NO_BOLD => write!(f, "nobold"),
342                    Attribute::DIM => write!(f, "dim"),
343                    Attribute::NO_DIM => write!(f, "nodim"),
344                    Attribute::UL => write!(f, "ul"),
345                    Attribute::NO_UL => write!(f, "noul"),
346                    Attribute::BLINK => write!(f, "blink"),
347                    Attribute::NO_BLINK => write!(f, "noblink"),
348                    Attribute::REVERSE => write!(f, "reverse"),
349                    Attribute::NO_REVERSE => write!(f, "noreverse"),
350                    Attribute::ITALIC => write!(f, "italic"),
351                    Attribute::NO_ITALIC => write!(f, "noitalic"),
352                    Attribute::STRIKE => write!(f, "strike"),
353                    Attribute::NO_STRIKE => write!(f, "nostrike"),
354                    _ => unreachable!("BUG: add new attribute flag"),
355                }?;
356                write_space = Some(());
357            }
358        }
359        Ok(())
360    }
361}
362
363#[cfg(feature = "serde")]
364impl serde::Serialize for Attribute {
365    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
366    where
367        S: serde::Serializer,
368    {
369        serializer.serialize_str(&self.to_string())
370    }
371}
372
373impl FromStr for Attribute {
374    type Err = Error;
375
376    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
377        let inverted = if let Some(rest) = s.strip_prefix("no-").or_else(|| s.strip_prefix("no")) {
378            s = rest;
379            true
380        } else {
381            false
382        };
383
384        if s.eq_ignore_ascii_case("reset") {
385            return if inverted {
386                Err(color_err(s))
387            } else {
388                Ok(Attribute::RESET)
389            };
390        }
391
392        match s {
393            "bold" if !inverted => Ok(Attribute::BOLD),
394            "bold" if inverted => Ok(Attribute::NO_BOLD),
395            "dim" if !inverted => Ok(Attribute::DIM),
396            "dim" if inverted => Ok(Attribute::NO_DIM),
397            "ul" if !inverted => Ok(Attribute::UL),
398            "ul" if inverted => Ok(Attribute::NO_UL),
399            "blink" if !inverted => Ok(Attribute::BLINK),
400            "blink" if inverted => Ok(Attribute::NO_BLINK),
401            "reverse" if !inverted => Ok(Attribute::REVERSE),
402            "reverse" if inverted => Ok(Attribute::NO_REVERSE),
403            "italic" if !inverted => Ok(Attribute::ITALIC),
404            "italic" if inverted => Ok(Attribute::NO_ITALIC),
405            "strike" if !inverted => Ok(Attribute::STRIKE),
406            "strike" if inverted => Ok(Attribute::NO_STRIKE),
407            _ => Err(color_err(s)),
408        }
409    }
410}
411
412impl TryFrom<&BStr> for Attribute {
413    type Error = Error;
414
415    fn try_from(s: &BStr) -> Result<Self, Self::Error> {
416        Self::from_str(std::str::from_utf8(s).map_err(|err| color_err(s).with_err(err))?)
417    }
418}