delta-gitu 0.1.16

An experimental fork of gitu (a git client inspired by Magit), published as a library for delta to embed
Documentation
use serde::{Deserialize, Deserializer, de};
use std::str::FromStr;

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Style {
    pub fg: Option<Color>,
    pub bg: Option<Color>,
    pub add_modifier: Modifier,
    pub sub_modifier: Modifier,
}

impl Style {
    pub const fn new() -> Self {
        Style {
            fg: None,
            bg: None,
            add_modifier: Modifier::empty(),
            sub_modifier: Modifier::empty(),
        }
    }

    /// Lays `other` over `self`, with what `other` leaves unset showing through.
    pub fn patch(mut self, other: Style) -> Style {
        self.fg = other.fg.or(self.fg);
        self.bg = other.bg.or(self.bg);

        self.add_modifier.remove(other.sub_modifier);
        self.add_modifier.insert(other.add_modifier);
        self.sub_modifier.remove(other.add_modifier);
        self.sub_modifier.insert(other.sub_modifier);

        self
    }
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Color {
    #[default]
    Reset,
    Black,
    Red,
    Green,
    Yellow,
    Blue,
    Magenta,
    Cyan,
    Gray,
    DarkGray,
    LightRed,
    LightGreen,
    LightYellow,
    LightBlue,
    LightMagenta,
    LightCyan,
    White,
    Rgb(u8, u8, u8),
    Indexed(u8),
}

impl From<Color> for crossterm::style::Color {
    fn from(color: Color) -> Self {
        match color {
            Color::Reset => Self::Reset,
            Color::Black => Self::Black,
            Color::Red => Self::DarkRed,
            Color::Green => Self::DarkGreen,
            Color::Yellow => Self::DarkYellow,
            Color::Blue => Self::DarkBlue,
            Color::Magenta => Self::DarkMagenta,
            Color::Cyan => Self::DarkCyan,
            Color::Gray => Self::Grey,
            Color::DarkGray => Self::DarkGrey,
            Color::LightRed => Self::Red,
            Color::LightGreen => Self::Green,
            Color::LightYellow => Self::Yellow,
            Color::LightBlue => Self::Blue,
            Color::LightMagenta => Self::Magenta,
            Color::LightCyan => Self::Cyan,
            Color::White => Self::White,
            Color::Rgb(r, g, b) => Self::Rgb { r, g, b },
            Color::Indexed(i) => Self::AnsiValue(i),
        }
    }
}

impl FromStr for Color {
    type Err = ParseColorError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // Colors are written by hand in the config, so accept the spellings
        // that are in the wild rather than one canonical form.
        let name = s
            .to_lowercase()
            .replace([' ', '-', '_'], "")
            .replace("bright", "light")
            .replace("grey", "gray")
            .replace("silver", "gray")
            .replace("lightblack", "darkgray")
            .replace("lightwhite", "white")
            .replace("lightgray", "white");

        Ok(match name.as_str() {
            "reset" => Self::Reset,
            "black" => Self::Black,
            "red" => Self::Red,
            "green" => Self::Green,
            "yellow" => Self::Yellow,
            "blue" => Self::Blue,
            "magenta" => Self::Magenta,
            "cyan" => Self::Cyan,
            "gray" => Self::Gray,
            "darkgray" => Self::DarkGray,
            "lightred" => Self::LightRed,
            "lightgreen" => Self::LightGreen,
            "lightyellow" => Self::LightYellow,
            "lightblue" => Self::LightBlue,
            "lightmagenta" => Self::LightMagenta,
            "lightcyan" => Self::LightCyan,
            "white" => Self::White,
            _ => {
                if let Ok(index) = s.parse::<u8>() {
                    Self::Indexed(index)
                } else if let Some((r, g, b)) = parse_hex_color(s) {
                    Self::Rgb(r, g, b)
                } else {
                    return Err(ParseColorError);
                }
            }
        })
    }
}

fn parse_hex_color(s: &str) -> Option<(u8, u8, u8)> {
    let hex = s.strip_prefix('#')?;
    if hex.len() != 6 {
        return None;
    }

    // Indexing would panic where a multi-byte char straddles the split.
    let channel = |range: std::ops::Range<usize>| u8::from_str_radix(hex.get(range)?, 16).ok();
    Some((channel(0..2)?, channel(2..4)?, channel(4..6)?))
}

#[derive(Debug)]
pub struct ParseColorError;

impl std::fmt::Display for ParseColorError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("invalid color")
    }
}

impl std::error::Error for ParseColorError {}

impl<'de> Deserialize<'de> for Color {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let s = String::deserialize(deserializer)?;
        Color::from_str(&s).map_err(|_| de::Error::custom(format!("unknown color: {s}")))
    }
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Modifier(pub u16);

impl Modifier {
    pub const BOLD: Self = Modifier(0b0000_0000_0001);
    pub const DIM: Self = Modifier(0b0000_0000_0010);
    pub const ITALIC: Self = Modifier(0b0000_0000_0100);
    pub const UNDERLINED: Self = Modifier(0b0000_0000_1000);
    pub const SLOW_BLINK: Self = Modifier(0b0000_0001_0000);
    pub const RAPID_BLINK: Self = Modifier(0b0000_0010_0000);
    pub const REVERSED: Self = Modifier(0b0000_0100_0000);
    pub const HIDDEN: Self = Modifier(0b0000_1000_0000);
    pub const CROSSED_OUT: Self = Modifier(0b0001_0000_0000);

    const NAMED: &'static [(&'static str, Self)] = &[
        ("BOLD", Self::BOLD),
        ("DIM", Self::DIM),
        ("ITALIC", Self::ITALIC),
        ("UNDERLINED", Self::UNDERLINED),
        ("SLOW_BLINK", Self::SLOW_BLINK),
        ("RAPID_BLINK", Self::RAPID_BLINK),
        ("REVERSED", Self::REVERSED),
        ("HIDDEN", Self::HIDDEN),
        ("CROSSED_OUT", Self::CROSSED_OUT),
    ];

    pub const fn empty() -> Self {
        Modifier(0)
    }

    pub fn contains(self, other: Self) -> bool {
        self.0 & other.0 == other.0
    }

    pub fn insert(&mut self, other: Self) {
        self.0 |= other.0;
    }

    pub fn remove(&mut self, other: Self) {
        self.0 &= !other.0;
    }
}

impl std::ops::BitOr for Modifier {
    type Output = Self;

    fn bitor(self, other: Self) -> Self {
        Modifier(self.0 | other.0)
    }
}

impl std::ops::BitOrAssign for Modifier {
    fn bitor_assign(&mut self, other: Self) {
        self.insert(other);
    }
}

impl FromStr for Modifier {
    type Err = ParseModifierError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut modifier = Modifier::empty();

        if s.trim().is_empty() {
            return Ok(modifier);
        }

        for name in s.split('|') {
            let name = name.trim();
            let (_, flag) = Modifier::NAMED
                .iter()
                .find(|(named, _)| named.eq_ignore_ascii_case(name))
                .ok_or(ParseModifierError)?;

            modifier.insert(*flag);
        }

        Ok(modifier)
    }
}

#[derive(Debug)]
pub struct ParseModifierError;

impl std::fmt::Display for ParseModifierError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("invalid modifier")
    }
}

impl std::error::Error for ParseModifierError {}

impl<'de> Deserialize<'de> for Modifier {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let s = String::deserialize(deserializer)?;
        Modifier::from_str(&s).map_err(|_| de::Error::custom(format!("unknown modifier: {s}")))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_modifiers() {
        assert_eq!(Modifier::from_str("").unwrap(), Modifier::empty());
        assert_eq!(Modifier::from_str("BOLD").unwrap(), Modifier::BOLD);

        let mut both = Modifier::BOLD;
        both.insert(Modifier::ITALIC);
        assert_eq!(Modifier::from_str("BOLD|ITALIC").unwrap(), both);
        assert_eq!(Modifier::from_str("BOLD | ITALIC").unwrap(), both);

        assert!(Modifier::from_str("NONSENSE").is_err());
    }

    #[test]
    fn parse_colors() {
        assert_eq!(Color::from_str("light blue").unwrap(), Color::LightBlue);
        assert_eq!(Color::from_str("LightBlue").unwrap(), Color::LightBlue);
        assert_eq!(Color::from_str("bright-blue").unwrap(), Color::LightBlue);
        assert_eq!(Color::from_str("grey").unwrap(), Color::Gray);
        assert_eq!(Color::from_str("reset").unwrap(), Color::Reset);
        assert_eq!(Color::from_str("255").unwrap(), Color::Indexed(255));
        assert_eq!(
            Color::from_str("#707070").unwrap(),
            Color::Rgb(112, 112, 112)
        );

        assert!(Color::from_str("#70707").is_err());
        assert!(Color::from_str("#€abc").is_err());
        assert!(Color::from_str("nonsense").is_err());
    }
}