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