synd_term/config/
categories.rs

1use std::{collections::HashMap, path::Path};
2
3use anyhow::Context;
4use ratatui::style::Color;
5use serde::{Deserialize, Serialize};
6use synd_feed::types::Category;
7
8#[derive(Clone, Deserialize, Debug)]
9pub struct Categories {
10    categories: HashMap<String, Entry>,
11    #[serde(skip)]
12    aliases: HashMap<String, String>,
13}
14
15impl Categories {
16    pub fn default_toml() -> Self {
17        let s = include_str!("../../categories.toml");
18        let mut c: Self = toml::from_str(s).unwrap();
19        c.update_aliases();
20        c
21    }
22
23    pub fn load(path: impl AsRef<Path>) -> anyhow::Result<Self> {
24        let path = path.as_ref();
25        let buf =
26            std::fs::read_to_string(path).with_context(|| format!("path: {}", path.display()))?;
27        let mut c: Self = toml::from_str(&buf)?;
28        c.update_aliases();
29        Ok(c)
30    }
31
32    pub fn icon(&self, category: &Category<'_>) -> Option<&Icon> {
33        self.categories
34            .get(category.as_str())
35            .map(|entry| &entry.icon)
36    }
37
38    pub fn normalize(&self, category: Category<'static>) -> Category<'static> {
39        match self.aliases.get(category.as_str()) {
40            Some(normalized) => Category::new(normalized.to_owned()).unwrap_or(category),
41            None => category,
42        }
43    }
44
45    fn update_aliases(&mut self) {
46        let new_map = self.categories.iter().fold(
47            HashMap::with_capacity(self.categories.len()),
48            |mut m, (category, entry)| {
49                entry.aliases.iter().for_each(|alias| {
50                    m.insert(alias.to_lowercase(), category.to_lowercase());
51                });
52                m
53            },
54        );
55
56        self.aliases = new_map;
57    }
58
59    pub(crate) fn lookup(&self, category: &str) -> Option<Category<'static>> {
60        let normalized = match self.aliases.get(category) {
61            Some(normalized) => normalized,
62            None => category,
63        };
64
65        if self.categories.contains_key(normalized) {
66            Category::new(normalized.to_owned()).ok()
67        } else {
68            None
69        }
70    }
71
72    pub(super) fn merge(&mut self, other: HashMap<String, Entry>) {
73        self.categories.extend(other);
74        self.update_aliases();
75    }
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize)]
79pub(super) struct Entry {
80    icon: Icon,
81    #[serde(default)]
82    aliases: Vec<String>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct Icon {
87    symbol: String,
88    color: Option<IconColor>,
89}
90
91impl Icon {
92    pub fn new(symbol: impl Into<String>) -> Self {
93        Self {
94            symbol: symbol.into(),
95            color: None,
96        }
97    }
98
99    #[must_use]
100    pub fn with_color(self, color: IconColor) -> Self {
101        Self {
102            color: Some(color),
103            ..self
104        }
105    }
106
107    pub fn symbol(&self) -> &str {
108        self.symbol.as_str()
109    }
110    pub fn color(&self) -> Option<Color> {
111        self.color.as_ref().and_then(IconColor::color)
112    }
113}
114
115#[derive(Clone, Debug, Serialize, Deserialize, Default)]
116pub struct IconColor {
117    rgb: Option<u32>,
118    // https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html#variant.Red
119    name: Option<String>,
120    #[serde(skip)]
121    color: Option<Color>,
122}
123
124impl IconColor {
125    pub fn new(color: Color) -> Self {
126        Self {
127            rgb: None,
128            name: None,
129            color: Some(color),
130        }
131    }
132}
133
134impl IconColor {
135    fn color(&self) -> Option<Color> {
136        self.color.or(self
137            .rgb
138            .as_ref()
139            .map(|rgb| Color::from_u32(*rgb))
140            .or(self.name.as_ref().and_then(|s| s.parse().ok())))
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn should_parse_default_toml() {
150        let c = Categories::default_toml();
151        let icon = c.icon(&Category::new("rust").unwrap()).unwrap();
152
153        assert_eq!(icon.symbol(), "");
154        assert_eq!(icon.color(), Some(Color::Rgb(247, 76, 0)));
155    }
156}