synd_term/config/
categories.rs1use 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 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}