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}