Skip to main content

gix_config_value/
color.rs

1use std::{borrow::Cow, fmt::Display, str::FromStr};
2
3use bstr::{BStr, BString};
4use gix_error::{ErrorExt, Message, ResultExt, validation};
5
6use crate::Color;
7
8impl Display for Color {
9    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
10        let mut write_space = None;
11        if let Some(fg) = self.foreground {
12            fg.fmt(f)?;
13            write_space = Some(());
14        }
15
16        if let Some(bg) = self.background {
17            if write_space.take().is_some() {
18                write!(f, " ")?;
19            }
20            bg.fmt(f)?;
21            write_space = Some(());
22        }
23
24        if !self.attributes.is_empty() {
25            if write_space.take().is_some() {
26                write!(f, " ")?;
27            }
28            self.attributes.fmt(f)?;
29        }
30        Ok(())
31    }
32}
33
34fn color_err(input: impl Into<BString>) -> Message {
35    validation("Colors are specific color values and their attributes, like 'brightred', or 'blue'")
36        .with("input", gix_error::MetadataValue::Bytes(input.into()))
37}
38
39impl TryFrom<&BStr> for Color {
40    type Error = gix_error::Exn<gix_error::Message>;
41
42    fn try_from(s: &BStr) -> Result<Self, Self::Error> {
43        let s = std::str::from_utf8(s).or_raise(|| color_err(s))?;
44        enum ColorItem {
45            Value(Name),
46            Attr(Attribute),
47        }
48
49        let items = s.split_whitespace().filter_map(|s| {
50            if s.is_empty() {
51                return None;
52            }
53
54            Some(
55                Name::from_str(s)
56                    .map(ColorItem::Value)
57                    .or_else(|_| Attribute::from_str(s).map(ColorItem::Attr)),
58            )
59        });
60
61        let mut foreground = None;
62        let mut background = None;
63        let mut attributes = Attribute::empty();
64        for item in items {
65            match item {
66                Ok(item) => match item {
67                    ColorItem::Value(v) => {
68                        if foreground.is_none() {
69                            foreground = Some(v);
70                        } else if background.is_none() {
71                            background = Some(v);
72                        } else {
73                            return Err(color_err(s).raise());
74                        }
75                    }
76                    ColorItem::Attr(a) => attributes |= a,
77                },
78                Err(_) => return Err(color_err(s).raise()),
79            }
80        }
81
82        Ok(Color {
83            foreground,
84            background,
85            attributes,
86        })
87    }
88}
89
90impl TryFrom<&str> for Color {
91    type Error = gix_error::Exn<gix_error::Message>;
92
93    fn try_from(value: &str) -> Result<Self, Self::Error> {
94        Self::try_from(BStr::new(value))
95    }
96}
97
98impl TryFrom<Cow<'_, BStr>> for Color {
99    type Error = gix_error::Exn<gix_error::Message>;
100
101    fn try_from(c: Cow<'_, BStr>) -> Result<Self, Self::Error> {
102        Self::try_from(c.as_ref())
103    }
104}
105
106impl TryFrom<BString> for Color {
107    type Error = gix_error::Exn<gix_error::Message>;
108
109    fn try_from(value: BString) -> Result<Self, Self::Error> {
110        Self::try_from(BStr::new(&value))
111    }
112}
113
114/// Discriminating enum for names of [`Color`] values.
115///
116/// `git-config` supports the eight standard colors, their bright variants, an
117/// ANSI color code, or a hex value prefixed with an octothorpe/hash. The hex value
118/// is either 24-bit, like `#ff11bb`, or the 12-bit shorthand `#f1b`, which stands
119/// for the same color. Color names and the `bright` prefix are matched
120/// case-insensitively, and `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
212/// Parse the digits behind a `#` the way `git` does, which is either a 24-bit value
213/// like `ff11bb`, or its 12-bit shorthand `f1b`, where each digit stands for a doubled
214/// pair. Any other length is rejected, as is a digit that isn't hexadecimal.
215fn parse_hex(hex: &[u8]) -> Option<(u8, u8, u8)> {
216    fn nibble(b: u8) -> Option<u8> {
217        char::from(b).to_digit(16).map(|d| d as u8)
218    }
219
220    match *hex {
221        [r, g, b] => Some((nibble(r)? * 0x11, nibble(g)? * 0x11, nibble(b)? * 0x11)),
222        [r1, r0, g1, g0, b1, b0] => Some((
223            nibble(r1)? << 4 | nibble(r0)?,
224            nibble(g1)? << 4 | nibble(g0)?,
225            nibble(b1)? << 4 | nibble(b0)?,
226        )),
227        _ => None,
228    }
229}
230
231impl FromStr for Name {
232    type Err = gix_error::Exn<gix_error::Message>;
233
234    fn from_str(s: &str) -> Result<Self, Self::Err> {
235        const BASIC: &[(&str, Name, Name)] = &[
236            ("black", Name::Black, Name::BrightBlack),
237            ("red", Name::Red, Name::BrightRed),
238            ("green", Name::Green, Name::BrightGreen),
239            ("yellow", Name::Yellow, Name::BrightYellow),
240            ("blue", Name::Blue, Name::BrightBlue),
241            ("magenta", Name::Magenta, Name::BrightMagenta),
242            ("cyan", Name::Cyan, Name::BrightCyan),
243            ("white", Name::White, Name::BrightWhite),
244        ];
245
246        if s.eq_ignore_ascii_case("normal") {
247            return Ok(Self::Normal);
248        }
249
250        let (name, is_bright) = match s.split_at_checked("bright".len()) {
251            Some((prefix, rest)) if prefix.eq_ignore_ascii_case("bright") => (rest, true),
252            _ => (s, false),
253        };
254
255        for &(basic, plain, brightened) in BASIC {
256            if name.eq_ignore_ascii_case(basic) {
257                return Ok(if is_bright { brightened } else { plain });
258            }
259        }
260
261        if is_bright {
262            return Err(color_err(s).raise());
263        }
264
265        if s.eq_ignore_ascii_case("normal") || s == "-1" {
266            return Ok(Self::Normal);
267        }
268
269        if s.eq_ignore_ascii_case("default") {
270            return Ok(Self::Default);
271        }
272
273        if let Ok(v) = u8::from_str(s) {
274            return Ok(Self::Ansi(v));
275        }
276
277        if let Some(hex) = s.strip_prefix('#')
278            && let Some((r, g, b)) = parse_hex(hex.as_bytes())
279        {
280            return Ok(Self::Rgb(r, g, b));
281        }
282
283        Err(color_err(s).raise())
284    }
285}
286
287impl TryFrom<&BStr> for Name {
288    type Error = gix_error::Exn<gix_error::Message>;
289
290    fn try_from(s: &BStr) -> Result<Self, Self::Error> {
291        Self::from_str(std::str::from_utf8(s).or_raise(|| color_err(s))?)
292    }
293}
294
295bitflags::bitflags! {
296    /// Discriminating enum for [`Color`] attributes.
297    ///
298    /// `git-config` supports modifiers and their negators. The negating color
299    /// attributes are equivalent to having a `no` or `no-` prefix to the normal
300    /// variant.
301    #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
302    pub struct Attribute: u32 {
303        /// Use bold or increased-intensity text.
304        const BOLD = 1 << 1;
305        /// Use dim or decreased-intensity text.
306        const DIM = 1 << 2;
307        /// Use italic text.
308        const ITALIC = 1 << 3;
309        /// Underline text.
310        const UL = 1 << 4;
311        /// Blink text.
312        const BLINK = 1 << 5;
313        /// Reverse the foreground and background colors.
314        const REVERSE = 1 << 6;
315        /// Strike through text.
316        const STRIKE = 1 << 7;
317        /// Parse the `reset` attribute, which Git otherwise leaves without an effect here.
318        const RESET = 1 << 8;
319
320        /// Disable dim text.
321        const NO_DIM = 1 << 21;
322        /// Disable bold text.
323        const NO_BOLD = 1 << 22;
324        /// Disable italic text.
325        const NO_ITALIC = 1 << 23;
326        /// Disable underlining.
327        const NO_UL = 1 << 24;
328        /// Disable blinking.
329        const NO_BLINK = 1 << 25;
330        /// Disable reversed colors.
331        const NO_REVERSE = 1 << 26;
332        /// Disable strikethrough.
333        const NO_STRIKE = 1 << 27;
334    }
335}
336
337impl Display for Attribute {
338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339        let mut write_space = None;
340        for bit in 1..std::mem::size_of::<Attribute>() * 8 {
341            let attr = match Attribute::from_bits(1 << bit) {
342                Some(attr) => attr,
343                None => continue,
344            };
345            if self.contains(attr) {
346                if write_space.take().is_some() {
347                    write!(f, " ")?;
348                }
349                match attr {
350                    Attribute::RESET => write!(f, "reset"),
351                    Attribute::BOLD => write!(f, "bold"),
352                    Attribute::NO_BOLD => write!(f, "nobold"),
353                    Attribute::DIM => write!(f, "dim"),
354                    Attribute::NO_DIM => write!(f, "nodim"),
355                    Attribute::UL => write!(f, "ul"),
356                    Attribute::NO_UL => write!(f, "noul"),
357                    Attribute::BLINK => write!(f, "blink"),
358                    Attribute::NO_BLINK => write!(f, "noblink"),
359                    Attribute::REVERSE => write!(f, "reverse"),
360                    Attribute::NO_REVERSE => write!(f, "noreverse"),
361                    Attribute::ITALIC => write!(f, "italic"),
362                    Attribute::NO_ITALIC => write!(f, "noitalic"),
363                    Attribute::STRIKE => write!(f, "strike"),
364                    Attribute::NO_STRIKE => write!(f, "nostrike"),
365                    _ => unreachable!("BUG: add new attribute flag"),
366                }?;
367                write_space = Some(());
368            }
369        }
370        Ok(())
371    }
372}
373
374#[cfg(feature = "serde")]
375impl serde::Serialize for Attribute {
376    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
377    where
378        S: serde::Serializer,
379    {
380        serializer.serialize_str(&self.to_string())
381    }
382}
383
384impl FromStr for Attribute {
385    type Err = gix_error::Exn<gix_error::Message>;
386
387    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
388        let inverted = if let Some(rest) = s.strip_prefix("no-").or_else(|| s.strip_prefix("no")) {
389            s = rest;
390            true
391        } else {
392            false
393        };
394
395        if s.eq_ignore_ascii_case("reset") {
396            return if inverted {
397                Err(color_err(s).raise())
398            } else {
399                Ok(Attribute::RESET)
400            };
401        }
402
403        match s {
404            "bold" if !inverted => Ok(Attribute::BOLD),
405            "bold" if inverted => Ok(Attribute::NO_BOLD),
406            "dim" if !inverted => Ok(Attribute::DIM),
407            "dim" if inverted => Ok(Attribute::NO_DIM),
408            "ul" if !inverted => Ok(Attribute::UL),
409            "ul" if inverted => Ok(Attribute::NO_UL),
410            "blink" if !inverted => Ok(Attribute::BLINK),
411            "blink" if inverted => Ok(Attribute::NO_BLINK),
412            "reverse" if !inverted => Ok(Attribute::REVERSE),
413            "reverse" if inverted => Ok(Attribute::NO_REVERSE),
414            "italic" if !inverted => Ok(Attribute::ITALIC),
415            "italic" if inverted => Ok(Attribute::NO_ITALIC),
416            "strike" if !inverted => Ok(Attribute::STRIKE),
417            "strike" if inverted => Ok(Attribute::NO_STRIKE),
418            _ => Err(color_err(s).raise()),
419        }
420    }
421}
422
423impl TryFrom<&BStr> for Attribute {
424    type Error = gix_error::Exn<gix_error::Message>;
425
426    fn try_from(s: &BStr) -> Result<Self, Self::Error> {
427        Self::from_str(std::str::from_utf8(s).or_raise(|| color_err(s))?)
428    }
429}