1use serde::{Deserialize, Serialize};
4use serde_json::{from_str, Value};
5use std::fs::{create_dir_all, read_to_string, remove_dir, remove_file, File};
6use std::io::{Read, Write};
7use std::path::PathBuf;
8
9#[cfg(test)]
10mod tests;
11
12const DEFAULT_FILENAME: &str = "settings.json";
14
15#[derive(Serialize, Deserialize)]
17pub struct JsonSettings {
18 filepath: PathBuf,
19}
20
21#[derive(Serialize, Deserialize)]
23pub struct KeyValue {
24 key: Option<String>,
25 pub value: Option<Value>,
26 file_exists: bool,
27 key_exists: bool,
28}
29
30impl JsonSettings {
31 pub fn new(filepath: &PathBuf) -> JsonSettings {
33 JsonSettings {
34 filepath: filepath.to_owned(),
35 }
36 }
37
38 pub fn exe_dir() -> JsonSettings {
40 let filepath = exe_dir_filepath(DEFAULT_FILENAME);
41 JsonSettings::new(&filepath)
42 }
43
44 pub fn exe_dir_with_filename(filename: &str) -> JsonSettings {
46 let filepath = exe_dir_filepath(filename);
47 JsonSettings::new(&filepath)
48 }
49
50 pub fn config_dir() -> JsonSettings {
52 let filepath = config_dir_filepath(DEFAULT_FILENAME);
53 JsonSettings::new(&filepath)
54 }
55
56 pub fn config_dir_with_filename(filename: &str) -> JsonSettings {
58 let filepath = config_dir_filepath(filename);
59 JsonSettings::new(&filepath)
60 }
61
62 pub fn read_by_key(&self, key: &str) -> Result<KeyValue, Box<dyn std::error::Error>> {
64 let filepath = &self.filepath;
65
66 if !filepath.exists() {
67 return Ok(KeyValue {
68 key: None,
69 value: None,
70 file_exists: false,
71 key_exists: false,
72 });
73 }
74
75 let json = json_load(&filepath)?;
76 if let Some(value) = json.get(key) {
77 Ok(KeyValue {
78 key: Some(key.to_owned()),
79 value: Some(value.to_owned()),
80 file_exists: true,
81 key_exists: true,
82 })
83 } else {
84 return Ok(KeyValue {
85 key: Some(key.to_owned()),
86 value: None,
87 file_exists: true,
88 key_exists: false,
89 });
90 }
91 }
92
93 pub fn write_by_key(&self, key: &str, value: &Value) -> Result<(), std::io::Error> {
95 let filepath = &self.filepath;
96
97 let mut current_settings = if filepath.exists() {
98 let file_text = read_to_string(&filepath)?;
99 serde_json::from_str(&file_text).unwrap_or_default()
100 } else {
101 Value::Object(serde_json::Map::new())
102 };
103
104 let map = current_settings.as_object_mut().unwrap();
105 map.insert(key.to_owned(), value.to_owned());
106
107 let updated_settings = serde_json::to_string_pretty(¤t_settings)?;
108
109 let mut file = File::create(&filepath)?;
110 file.write_all(updated_settings.as_bytes())?;
111
112 Ok(())
113 }
114
115 pub fn remove(&self, remove_dir_if_empty: bool) {
117 remove_file(&self.filepath).expect("Failed to remove settings file");
118 if !remove_dir_if_empty {
119 match remove_dir(self.filepath.parent().unwrap()) {
120 Ok(_) => (),
121 Err(_) => (), }
123 }
124 }
125}
126
127pub fn config_dir() -> PathBuf {
129 let current_exe = std::env::current_exe().unwrap();
130 let filename = current_exe.file_name().unwrap().to_str().unwrap();
131 config_root_dir().join(filename)
132}
133
134fn exe_dir_filepath(filename: &str) -> PathBuf {
136 let exec_filepath = std::env::current_exe().expect("Failed to get exec path");
137 let dirpath = exec_filepath
138 .parent()
139 .expect("Failed to get exec parent dir path");
140 dirpath.join(filename)
141}
142
143fn config_dir_filepath(filename: &str) -> PathBuf {
145 let dirpath = config_dir();
146 if !dirpath.exists() {
147 create_dir_all(&dirpath).expect("Failed to create app dir in user config");
148 }
149 dirpath.join(filename)
150}
151
152fn json_load(filepath: &PathBuf) -> Result<Value, Box<dyn std::error::Error>> {
154 let mut file =
155 File::open(&filepath).map_err(|e| format!("Failed to open settings file: {}", e))?;
156 let mut contents = String::new();
157 file.read_to_string(&mut contents)
158 .map_err(|e| format!("Failed to read settings file: {}", e))?;
159 let json: Value =
160 from_str(&contents).map_err(|e| format!("Failed to deserialize settings: {}", e))?;
161 Ok(json)
162}
163
164#[cfg(target_os = "linux")]
165fn config_root_dir() -> PathBuf {
166 std::env::var("XDG_CONFIG_HOME")
167 .map(PathBuf::from)
168 .unwrap_or_else(|_| {
169 let mut home_dir = std::env::var("HOME").expect("HOME not set");
170 home_dir.push_str("/.config");
171 PathBuf::from(home_dir)
172 })
173}
174
175#[cfg(target_os = "windows")]
176fn config_root_dir() -> PathBuf {
177 std::env::var("APPDATA")
178 .map(PathBuf::from)
179 .expect("APPDATA not set")
180}
181
182#[cfg(target_os = "macos")]
183fn config_root_dir() -> PathBuf {
184 let mut home_dir = std::env::var("HOME").expect("HOME not set");
185 home_dir.push_str("/Library/Application Support");
186 PathBuf::from(home_dir)
187}
188
189#[cfg(target_os = "android")]
190fn config_root_dir() -> PathBuf {
191 let internal_storage =
192 std::env::var("ANDROID_INTERNAL_STORAGE").expect("ANDROID_INTERNAL_STORAGE not set");
193 PathBuf::from(internal_storage).join("config")
194}
195
196#[cfg(target_os = "ios")]
197fn config_root_dir() -> PathBuf {
198 let home_dir = std::env::var("HOME").expect("HOME not set");
199 PathBuf::from(home_dir).join("Documents").join("config")
200}