1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#![allow(clippy::module_inception)]
use std::{
    collections::HashMap,
    io,
    path::{Path, PathBuf},
};

use super::{
    app_settings::AppSettings, color_settings::ColorSettings, key_settings::KeySettings,
    settings_value::SettingsValue,
};

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)]
#[serde(default)]
pub struct Settings {
    pub color: ColorSettings,
    pub key: KeySettings,
    pub app: AppSettings,
    pub custom: HashMap<String, SettingsValue>,
}

impl Settings {
    pub fn load(path: Option<&Path>) -> Result<Settings, io::Error> {
        let path = match path {
            Some(path) => path.to_path_buf(),
            None => Self::get_default_settings_path().ok_or(io::Error::new(
                io::ErrorKind::Other,
                "Could not get default settings path",
            ))?,
        };

        if !path.exists() {
            return Err(io::Error::new(
                io::ErrorKind::NotFound,
                "Settings file not found",
            ));
        }

        let settings = match std::fs::read_to_string(&path) {
            Ok(settings) => settings,
            Err(e) => return Err(e),
        };

        Ok(match serde_json::from_str(&settings) {
            Ok(settings) => settings,
            Err(e) => {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!("Could not parse settings file: {}", e),
                ))
            }
        })
    }

    fn get_default_settings_path() -> Option<PathBuf> {
        let config = dirs::config_dir()?;
        Some(config.join("HexPatch").join("settings.json"))
    }

    pub fn load_or_create(path: Option<&Path>) -> Result<Settings, String> {
        match Self::load(path) {
            Ok(settings) => Ok(settings),
            Err(e) => {
                if e.kind() != io::ErrorKind::NotFound {
                    Err(format!("Could not load settings: {}", e))
                } else {
                    let settings = Settings::default();
                    settings
                        .save(path)
                        .ok_or(format!("Could not save default settings: {}", e))?;
                    Ok(settings)
                }
            }
        }
    }

    pub fn save(&self, path: Option<&Path>) -> Option<()> {
        let path = match path {
            Some(path) => path.to_path_buf(),
            None => Self::get_default_settings_path()?,
        };

        let settings = serde_json::to_string_pretty(self).ok()?;
        std::fs::create_dir_all(path.parent()?).ok()?;
        std::fs::write(&path, settings).ok()?;
        Some(())
    }
}

#[cfg(test)]
mod test {
    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
    use ratatui::style::{Color, Style};

    use super::*;

    #[test]
    fn test_settings_load() {
        let settings = Settings::load(Some(Path::new("test/default_settings.json")));
        if let Err(e) = settings {
            panic!("Could not load settings: {}", e);
        }
        assert_eq!(settings.unwrap(), Settings::default());
    }

    #[test]
    fn test_settings_partial_load() {
        let settings = Settings::load(Some(Path::new("test/partial_default_settings.json")));
        if let Err(e) = settings {
            panic!("Could not load settings: {}", e);
        }
        assert_eq!(settings.unwrap(), Settings::default());
    }

    #[test]
    fn test_settings_load_custom() {
        let settings = Settings::load(Some(Path::new("test/custom_settings.json")));
        if let Err(e) = settings {
            panic!("Could not load settings: {}", e);
        }
        let mut expected = Settings::default();
        expected
            .custom
            .insert("plugin1.value1".to_string(), SettingsValue::from("value1"));
        expected
            .custom
            .insert("plugin1.value2".to_string(), SettingsValue::from(2));
        expected
            .custom
            .insert("plugin2.value1".to_string(), SettingsValue::from(3.0));
        expected
            .custom
            .insert("plugin2.value2".to_string(), SettingsValue::from(true));
        expected.custom.insert(
            "plugin3.value1".to_string(),
            SettingsValue::from(Style::default().fg(Color::Red)),
        );
        expected.custom.insert(
            "plugin3.value2".to_string(),
            SettingsValue::from(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
        );
    }
}