hex_patch/app/settings/
settings.rs

1#![allow(clippy::module_inception)]
2use std::{
3    collections::HashMap,
4    io,
5    path::{Path, PathBuf},
6};
7
8use ratatui::style::Style;
9use serde::de::Visitor;
10use termbg::Theme;
11
12use super::{
13    app_settings::AppSettings, color_settings::ColorSettings, key_settings::KeySettings,
14    settings_value::SettingsValue,
15};
16
17#[derive(Debug, Clone, PartialEq, serde::Serialize)]
18pub struct Settings {
19    pub color: ColorSettings,
20    pub key: KeySettings,
21    pub app: AppSettings,
22    pub custom: HashMap<String, SettingsValue>,
23}
24
25impl Settings {
26    pub fn load(path: Option<&Path>, terminal_theme: Theme) -> Result<Settings, io::Error> {
27        let path = match path {
28            Some(path) => path.to_path_buf(),
29            None => Self::get_default_settings_path().ok_or(io::Error::new(
30                io::ErrorKind::Other,
31                "Could not get default settings path",
32            ))?,
33        };
34
35        if !path.exists() {
36            return Err(io::Error::new(
37                io::ErrorKind::NotFound,
38                "Settings file not found",
39            ));
40        }
41
42        let settings = std::fs::read_to_string(&path)?;
43
44        let mut deserializer = serde_json::Deserializer::from_str(&settings);
45
46        Ok(
47            match Settings::custom_deserialize(&mut deserializer, terminal_theme) {
48                Ok(settings) => settings,
49                Err(e) => {
50                    return Err(io::Error::new(
51                        io::ErrorKind::InvalidData,
52                        format!("Could not parse settings file: {}", e),
53                    ))
54                }
55            },
56        )
57    }
58
59    fn get_default_settings_path() -> Option<PathBuf> {
60        let config = dirs::config_dir()?;
61        Some(config.join("HexPatch").join("settings.json"))
62    }
63
64    pub fn load_or_create(path: Option<&Path>, terminal_theme: Theme) -> Result<Settings, String> {
65        match Self::load(path, terminal_theme) {
66            Ok(settings) => Ok(settings),
67            Err(e) => {
68                if e.kind() != io::ErrorKind::NotFound {
69                    Err(format!("Could not load settings: {}", e))
70                } else {
71                    let settings = Settings::empty(terminal_theme);
72                    if path.is_some() {
73                        settings
74                            .save(path)
75                            .ok_or(format!("Could not save default settings: {}", e))?;
76                    }
77                    Ok(settings)
78                }
79            }
80        }
81    }
82
83    pub fn save(&self, path: Option<&Path>) -> Option<()> {
84        let path = match path {
85            Some(path) => path.to_path_buf(),
86            None => Self::get_default_settings_path()?,
87        };
88
89        let settings = serde_json::to_string_pretty(self).ok()?;
90        std::fs::create_dir_all(path.parent()?).ok()?;
91        std::fs::write(&path, settings).ok()?;
92        Some(())
93    }
94
95    pub fn empty(terminal_theme: Theme) -> Self {
96        Self {
97            color: ColorSettings::get_default_theme(terminal_theme),
98            key: KeySettings::default(),
99            app: AppSettings::default(),
100            custom: HashMap::new(),
101        }
102    }
103}
104
105struct SettingsVisitor {
106    theme: Theme,
107}
108
109impl<'de> Visitor<'de> for SettingsVisitor {
110    type Value = Settings;
111
112    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
113        formatter.write_str("a valid Settings struct")
114    }
115
116    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
117    where
118        A: serde::de::MapAccess<'de>,
119    {
120        let mut color_settings: Option<HashMap<String, Style>> = None;
121        let mut key_settings: Option<KeySettings> = None;
122        let mut app_settings: Option<AppSettings> = None;
123        let mut custom_settings: Option<HashMap<String, SettingsValue>> = None;
124
125        while let Some(key) = map.next_key()? {
126            match key {
127                "color" => {
128                    if color_settings.is_some() {
129                        return Err(serde::de::Error::duplicate_field("color"));
130                    }
131                    color_settings = Some(map.next_value()?);
132                }
133                "key" => {
134                    if key_settings.is_some() {
135                        return Err(serde::de::Error::duplicate_field("key"));
136                    }
137                    key_settings = Some(map.next_value()?);
138                }
139                "app" => {
140                    if app_settings.is_some() {
141                        return Err(serde::de::Error::duplicate_field("app"));
142                    }
143                    app_settings = Some(map.next_value()?);
144                }
145                "custom" => {
146                    if custom_settings.is_some() {
147                        return Err(serde::de::Error::duplicate_field("custom"));
148                    }
149                    custom_settings = Some(map.next_value()?);
150                }
151                _ => {
152                    return Err(serde::de::Error::unknown_field(
153                        key,
154                        &["color", "key", "app", "custom"],
155                    ));
156                }
157            }
158        }
159        let key_settings = key_settings.unwrap_or_default();
160        let app_settings = app_settings.unwrap_or_default();
161        let custom_settings = custom_settings.unwrap_or_default();
162        let color_settings = ColorSettings::from_map(
163            &color_settings.unwrap_or_default(),
164            &app_settings,
165            self.theme,
166        )
167        .map_err(serde::de::Error::custom)?;
168
169        Ok(Self::Value {
170            color: color_settings,
171            key: key_settings,
172            app: app_settings,
173            custom: custom_settings,
174        })
175    }
176}
177
178impl Settings {
179    fn custom_deserialize<'de, D>(deserializer: D, theme: Theme) -> Result<Self, D::Error>
180    where
181        D: serde::Deserializer<'de>,
182    {
183        deserializer.deserialize_map(SettingsVisitor { theme })
184    }
185}
186
187impl Default for Settings {
188    fn default() -> Self {
189        Self {
190            color: ColorSettings::get_default_theme(Theme::Dark),
191            key: KeySettings::default(),
192            app: AppSettings::default(),
193            custom: HashMap::new(),
194        }
195    }
196}
197
198#[cfg(test)]
199mod test {
200    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
201    use ratatui::style::{Color, Style};
202
203    use super::*;
204
205    #[test]
206    fn test_settings_load() {
207        let settings = Settings::load(Some(Path::new("test/default_settings.json")), Theme::Dark);
208        if let Err(e) = settings {
209            panic!("Could not load settings: {}", e);
210        }
211        assert_eq!(settings.unwrap(), Settings::default());
212    }
213
214    #[test]
215    fn test_settings_partial_load() {
216        let settings = Settings::load(
217            Some(Path::new("test/partial_default_settings.json")),
218            Theme::Dark,
219        );
220        if let Err(e) = settings {
221            panic!("Could not load settings: {}", e);
222        }
223        assert_eq!(settings.unwrap(), Settings::default());
224    }
225
226    #[test]
227    fn test_settings_load_custom() {
228        let settings = Settings::load(Some(Path::new("test/custom_settings.json")), Theme::Dark);
229        if let Err(e) = settings {
230            panic!("Could not load settings: {}", e);
231        }
232        let mut expected = Settings::default();
233        expected
234            .custom
235            .insert("plugin1.value1".to_string(), SettingsValue::from("value1"));
236        expected
237            .custom
238            .insert("plugin1.value2".to_string(), SettingsValue::from(2));
239        expected
240            .custom
241            .insert("plugin2.value1".to_string(), SettingsValue::from(3.0));
242        expected
243            .custom
244            .insert("plugin2.value2".to_string(), SettingsValue::from(true));
245        expected.custom.insert(
246            "plugin3.value1".to_string(),
247            SettingsValue::from(Style::default().fg(Color::Red)),
248        );
249        expected.custom.insert(
250            "plugin3.value2".to_string(),
251            SettingsValue::from(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
252        );
253    }
254}