1use directories::ProjectDirs;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5
6use crate::{Error, Result};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Config {
10 pub install_dir: PathBuf,
11 pub log_level: String,
12 #[serde(default = "default_log_to_file")]
13 pub log_to_file: bool,
14 #[serde(default = "default_crash_reporting")]
15 pub enable_crash_reporting: bool,
16 #[serde(default = "default_privacy_mode")]
17 pub privacy_mode: bool,
18
19 #[serde(default = "default_auto_update")]
21 pub auto_update: bool,
22 #[serde(default = "default_minimize_to_tray")]
23 pub minimize_to_tray: bool,
24 #[serde(default = "default_close_to_tray")]
25 pub close_to_tray: bool,
26 #[serde(default = "default_language")]
27 pub language: String,
28 #[serde(default = "default_theme")]
29 pub theme: String,
30 #[serde(default = "default_max_concurrent_downloads")]
32 pub max_concurrent_downloads: u32,
33 #[serde(default = "default_download_threads")]
34 pub download_threads: u32,
35 #[serde(default)]
36 pub enable_bandwidth_limit: bool,
37 #[serde(default = "default_bandwidth_limit")]
38 pub bandwidth_limit_mbps: u32,
39 #[serde(default = "default_cdn_region")]
40 pub cdn_region: String,
41
42 #[serde(default = "default_wine_prefix")]
44 pub wine_prefix: String,
45 #[serde(default)]
46 pub enable_dxvk: bool,
47 #[serde(default)]
48 pub enable_esync: bool,
49 #[serde(default)]
50 pub enable_fsync: bool,
51
52 #[serde(default)]
54 pub disable_telemetry: bool,
55 #[serde(default)]
56 pub clear_cache_on_exit: bool,
57 #[serde(default = "default_telemetry_opt_in")]
58 pub telemetry_opt_in: bool,
59}
60
61fn default_auto_update() -> bool {
62 false
63}
64
65fn default_minimize_to_tray() -> bool {
66 false
67}
68
69fn default_close_to_tray() -> bool {
70 false
71}
72fn default_max_concurrent_downloads() -> u32 {
73 3
74}
75
76fn default_download_threads() -> u32 {
77 4
78}
79
80fn default_bandwidth_limit() -> u32 {
81 10
82}
83
84fn default_cdn_region() -> String {
85 "Auto".to_string()
86}
87
88fn default_wine_prefix() -> String {
89 "~/.wine".to_string()
90}
91
92fn default_language() -> String {
93 "en".to_string()
94}
95
96fn default_theme() -> String {
97 "Epic".to_string()
98}
99
100fn default_log_to_file() -> bool {
101 true
102}
103
104fn default_crash_reporting() -> bool {
105 true
106}
107
108fn default_privacy_mode() -> bool {
109 false
110}
111
112fn default_telemetry_opt_in() -> bool {
113 true
114}
115
116impl Default for Config {
117 fn default() -> Self {
118 let project_dirs =
119 ProjectDirs::from("", "", "epik").expect("Failed to determine project directories");
120
121 Self {
122 install_dir: project_dirs.data_dir().join("games"),
123 log_level: "info".to_string(),
124 log_to_file: default_log_to_file(),
125 enable_crash_reporting: default_crash_reporting(),
126 privacy_mode: default_privacy_mode(),
127 auto_update: default_auto_update(),
128 minimize_to_tray: default_minimize_to_tray(),
129 close_to_tray: default_close_to_tray(),
130 language: default_language(),
131 theme: default_theme(),
132 max_concurrent_downloads: default_max_concurrent_downloads(),
133 download_threads: default_download_threads(),
134 enable_bandwidth_limit: false,
135 bandwidth_limit_mbps: default_bandwidth_limit(),
136 cdn_region: default_cdn_region(),
137 wine_prefix: default_wine_prefix(),
138 enable_dxvk: false,
139 enable_esync: false,
140 enable_fsync: false,
141 disable_telemetry: false,
142 clear_cache_on_exit: false,
143 telemetry_opt_in: default_telemetry_opt_in(),
144 }
145 }
146}
147
148impl Config {
149 pub fn load() -> Result<Self> {
150 let config_path = Self::config_path()?;
154
155 if config_path.exists() {
156 let contents = fs::read_to_string(&config_path)?;
157 let config: Config = toml::from_str(&contents)?;
158 config.validate()?;
159 Ok(config)
160 } else {
161 let config = Self::default();
162 config.save()?;
163 Ok(config)
164 }
165 }
166
167 pub fn reload(&mut self) -> Result<()> {
169 let new_config = Self::load()?;
170 *self = new_config;
171 Ok(())
172 }
173
174 fn validate(&self) -> Result<()> {
176 let valid_log_levels = ["trace", "debug", "info", "warn", "error"];
178 let normalized_level = self.log_level.to_lowercase();
179 if !valid_log_levels.contains(&normalized_level.as_str()) {
180 return Err(Error::Config(format!(
181 "Invalid log level: '{}'. Must be one of: {}",
182 self.log_level,
183 valid_log_levels.join(", ")
184 )));
185 }
186
187 if let Some(parent) = self.install_dir.parent() {
189 if !parent.exists() {
190 return Err(Error::Config(format!(
191 "Install directory parent does not exist: {}",
192 parent.display()
193 )));
194 }
195 }
196
197 if self.disable_telemetry && self.telemetry_opt_in {
199 return Err(Error::Config(
200 "Telemetry settings inconsistent: disable_telemetry=true but telemetry_opt_in=true"
201 .to_string(),
202 ));
203 }
204
205 Ok(())
206 }
207
208 pub fn save(&self) -> Result<()> {
209 let config_path = Self::config_path()?;
210
211 if let Some(parent) = config_path.parent() {
212 fs::create_dir_all(parent)?;
213 }
214
215 let contents = toml::to_string_pretty(self).map_err(|e| Error::Config(e.to_string()))?;
216 fs::write(&config_path, contents)?;
217
218 Ok(())
219 }
220
221 pub fn config_path() -> Result<PathBuf> {
222 let project_dirs = ProjectDirs::from("", "", "epik")
223 .ok_or_else(|| Error::Config("Failed to determine project directories".to_string()))?;
224
225 Ok(project_dirs.config_dir().join("config.toml"))
226 }
227
228 pub fn data_dir() -> Result<PathBuf> {
229 let project_dirs = ProjectDirs::from("", "", "epik")
230 .ok_or_else(|| Error::Config("Failed to determine project directories".to_string()))?;
231
232 Ok(project_dirs.data_dir().to_path_buf())
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_config_default() {
242 let config = Config::default();
243 assert_eq!(config.log_level, "info");
244 assert!(config.install_dir.to_string_lossy().contains("games"));
245 }
246
247 #[test]
248 fn test_config_serialization() {
249 let config = Config::default();
250 let serialized = toml::to_string(&config).unwrap();
251 let deserialized: Config = toml::from_str(&serialized).unwrap();
252 assert_eq!(config.log_level, deserialized.log_level);
253 }
254}