summaryrefslogtreecommitdiffstats
path: root/src/skin/skin_entry.rs
blob: 8fab1fddc9acb7f226f568ed6d66379d1a2c3aeb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/// Manage conversion of a user provided string
/// defining foreground and background colors into
/// a string with TTY colors

use {
    crate::errors::InvalidSkinError,
    crossterm::style::{
        Attribute::{self, *},
        Attributes,
        Color::{self, *},
    },
    std::result::Result,
    super::*,
    termimad::CompoundStyle,
};

/// parsed content of a [skin] line of the conf.toml file
pub struct SkinEntry {
    focused: CompoundStyle,
    unfocused: Option<CompoundStyle>,
}

impl SkinEntry {
    pub fn new(focused: CompoundStyle, unfocused: Option<CompoundStyle>) -> Self {
        Self { focused, unfocused }
    }
    pub fn get_focused(&self) -> &CompoundStyle {
        &self.focused
    }
    pub fn get_unfocused(&self) -> &CompoundStyle {
        self.unfocused.as_ref().unwrap_or(&self.focused)
    }
    /// parse a string representation of a skin entry.
    ///
    /// The general form is either "<focused>" or "<focused> / <unfocused>":
    /// It may be just the focused compound_style, or both
    /// the focused and the unfocused ones, in which case there's
    /// a '/' as separator.
    ///
    /// Each part is "<foreground color> <background color> <attributes>"
    /// where the attributes list may be empty.
    pub fn parse(s: &str) -> Result<Self, InvalidSkinError> {
        let mut parts = s.split('/');
        let focused = parse_compound_style(parts.next().unwrap())?;
        let unfocused = parts.next()
            .map(|p| parse_compound_style(p))
            .transpose()?;
        Ok(Self { focused, unfocused })
    }
}


/// read a color from a string.
/// It may be either
/// - "none"
/// - one of the few known color name. Example: "darkred"
/// - grayscale with level in [0,24[. Example: "grey(5)"
/// - an Ansi code. Example "ansi(106)"
/// - RGB. Example: "rgb(25, 100, 0)"
/// This function needs a lowercase string (assuming lowercasing
/// has be done before, to ensure case-insensitive parsing)
fn parse_color(s: &str) -> Result<Option<Color>, InvalidSkinError> {
    if let Some(c) = regex!(r"^ansi\((?P<value>\d+)\)$").captures(&s) {
        let value: &str = c.name("value").unwrap().as_str();
        let value = value.parse();
        if let Ok(value) = value {
            return Ok(ansi(value)); // all ANSI values are ok
        } else {
            return Err(InvalidSkinError::InvalidColor { raw: s.to_owned() });
        }
    }

    if let Some(c) = regex!(r"^gr[ae]y(?:scale)?\((?P<level>\d+)\)$").captures(&s) {
        let level: &str = c.name("level").unwrap().as_str();
        let level = level.parse();
        if let Ok(level) = level {
            if level > 23 {
                return Err(InvalidSkinError::InvalidGreyLevel { level });
            }
            return Ok(gray(level));
        } else {
            return Err(InvalidSkinError::InvalidColor { raw: s.to_owned() });
        }
    }

    if let Some(c) = regex!(r"^rgb\((?P<r>\d+),\s*(?P<g>\d+),\s*(?P<b>\d+)\)$").captures(&s) {
        let r = c.name("r").unwrap().as_str().parse();
        let g = c.name("g").unwrap().as_str().parse();
        let b = c.name("b").unwrap().as_str().parse();
        if let (Ok(r), Ok(g), Ok(b)) = (r, g, b) {
            return Ok(rgb(r, g, b));
        } else {
            return Err(InvalidSkinError::InvalidColor { raw: s.to_owned() });
        }
    }

    match s {
        "black" => Ok(rgb(0, 0, 0)), // crossterm black isn't black
        "blue" => Ok(Some(Blue)),
        "cyan" => Ok(Some(Cyan)),
        "darkblue" => Ok(Some(DarkBlue)),
        "darkcyan" => Ok(Some(DarkCyan)),
        "darkgreen" => Ok(Some(DarkGreen)),
        "darkmagenta" => Ok(Some(DarkMagenta)),
        "darkred" => Ok(Some(DarkRed)),
        "green" => Ok(Some(Green)),
        "grey" => Ok(Some(Grey)),
        "magenta" => Ok(Some(Magenta)),
        "red" => Ok(Some(Red)),
        "yellow" => Ok(Some(Yellow)),
        "darkyellow" => Ok(Some(DarkYellow)),
        "white" => Ok(Some(White)),
        "none" => Ok(None),
        _ => Err(InvalidSkinError::InvalidColor { raw: s.to_owned() }),
    }
}

fn parse_attribute(s: &str) -> Result<Attribute, InvalidSkinError> {
    match s {
        "bold" => Ok(Bold),
        "crossedout" => Ok(CrossedOut),
        "dim" => Ok(Dim), // does it do anything ?
        "italic" => Ok(Italic),
        "reverse" => Ok(Reverse),
        "underlined" => Ok(Underlined),
        "overlined" => Ok(OverLined),
        // following ones aren't supported by crossterm yet
        // "defaultforegroundcolor" => Ok(DefaultForegroundColor),
        // "defaultbackgroundcolor" => Ok(DefaultBackgroundColor),
        "slowblink" => Ok(SlowBlink),
        "rapidblink" => Ok(RapidBlink),
        _ => Err(InvalidSkinError::InvalidAttribute { raw: s.to_owned() }),
    }
}

/// parse a sequence of space separated style attributes
fn parse_attributes(s: &str) -> Result<Vec<Attribute>, InvalidSkinError> {
    s.split_whitespace().map(|t| parse_attribute(t)).collect()
}

fn parse_compound_style(s: &str) -> Result<CompoundStyle, InvalidSkinError> {
    let s = s.to_ascii_lowercase();
    let parts_rex = regex!(
        r"(?x)
        ^
        \s*
        (?P<fg>\w+(\([\d,\s]+\))?)
        \s+
        (?P<bg>\w+(\([\d,\s]+\))?)
        (?P<attributes>.*)
        \s*
        $
        "
    );
    if let Some(c) = parts_rex.captures(&s) {
        let fg_color = parse_color(c.name("fg").unwrap().as_str())?;
        let bg_color = parse_color(c.name("bg").unwrap().as_str())?;
        let attrs = parse_attributes(c.name("attributes").unwrap().as_str())?;
        Ok(CompoundStyle::new(
            fg_color,
            bg_color,
            Attributes::from(attrs.as_slice()),
        ))
    } else {
        Err(InvalidSkinError::InvalidStyle {
            style: s.to_owned(),
        })
    }
}