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}