1
use std::{
2
    env,
3
    fs::{self, OpenOptions},
4
    path::Path,
5
};
6

            
7
use serde::{Deserialize, Serialize};
8

            
9
#[derive(Debug, Deserialize, Serialize)]
10
pub struct Config {
11
    #[serde(skip)]
12
    path: Option<String>,
13
    #[serde(default)]
14
    pub gamemode: bool,
15
    #[serde(default)]
16
    pub mangohud: bool,
17
}
18

            
19
18
fn get_path(root: &str, paths: Vec<&str>) -> String {
20
18
    let mut buf = Path::new(root).to_path_buf();
21

            
22
27
    for path in paths {
23
27
        buf = buf.join(path);
24
27
    }
25

            
26
18
    buf.to_str().expect("Failed to convert path.").to_string()
27
18
}
28

            
29
impl Config {
30
    /// The $HOME path for protontweaks.json
31
9
    pub fn home() -> Option<String> {
32
9
        let home = env::var("HOME").ok()?;
33

            
34
9
        Some(get_path(&home, vec![".config", "protontweaks.json"]))
35
9
    }
36

            
37
    /// The $XDG_CONFIG_HOME path for protontweaks.json
38
9
    pub fn xdg() -> Option<String> {
39
9
        let home = env::var("XDG_CONFIG_HOME").ok()?;
40

            
41
        Some(get_path(&home, vec!["protontweaks.json"]))
42
9
    }
43

            
44
    /// Returns a path to either the $XDG_CONFIG_HOME or $HOME protontweak paths if either variable is set.
45
    pub fn discover_valid_home() -> Result<String, String> {
46
        let paths = vec![Config::xdg(), Config::home()];
47

            
48
        for path in paths {
49
            if let Some(path) = path {
50
                return Ok(path);
51
            }
52
        }
53

            
54
        Err("Failed to detect a valid home directory, are you sure either $XDG_CONFIG_HOME or $HOME are set?".to_string())
55
    }
56

            
57
    /// The /etc path for protontweaks.json
58
9
    pub fn etc() -> String {
59
9
        get_path("/etc", vec!["protontweaks.json"])
60
9
    }
61

            
62
    /// Returns a list of all valid protontweak config paths
63
9
    pub fn all() -> Vec<String> {
64
9
        let mut paths: Vec<String> = vec![Config::xdg(), Config::home()]
65
9
            .iter()
66
18
            .filter(|x| x.is_some())
67
9
            .map(|x| x.clone().unwrap())
68
9
            .collect();
69

            
70
9
        paths.push(Config::etc());
71

            
72
9
        paths
73
9
    }
74

            
75
    /// Returns true if this config was loaded from the file system or persisted to the file system.
76
3
    pub fn persisted(&self) -> bool {
77
3
        self.path.is_some()
78
3
    }
79

            
80
    /// Searchs $XDG_CONFIG_HOME, $HOME/.config, and /etc for a protontweaks.json file.
81
    /// If one isn't discovered it returns the default config
82
6
    pub fn discover() -> Config {
83
12
        for path in Config::all() {
84
12
            if let Ok(config) = Config::load(&path) {
85
                return config;
86
12
            }
87
        }
88

            
89
6
        info!("No config found, loading default config!");
90
6
        Config::default()
91
6
    }
92

            
93
    /// Deletes all protontweak configs
94
3
    pub fn wipe() -> Result<(), String> {
95
6
        for path in Config::all() {
96
6
            if let Ok(mut config) = Config::load(&path) {
97
                config.delete()?;
98
6
            }
99
        }
100

            
101
3
        Ok(())
102
3
    }
103

            
104
    /// Loads a config at the given path
105
30
    pub fn load(path: &str) -> Result<Self, String> {
106
30
        info!("Checking {path} for config...");
107

            
108
30
        if fs::metadata(&path).is_ok() {
109
9
            let raw_config = fs::read_to_string(&path).map_err(|e| e.to_string())?;
110
9
            let mut config =
111
9
                serde_json::from_str::<Config>(&raw_config).map_err(|e| e.to_string())?;
112
9
            config.path = Some(path.to_string());
113
9
            Ok(config)
114
        } else {
115
21
            Err("File does not exist!".to_string())
116
        }
117
30
    }
118

            
119
    /// Deletes the current config if it is persisted
120
3
    pub fn delete(&mut self) -> Result<(), &'static str> {
121
3
        let path = self.path.as_ref().ok_or("Path not provided")?;
122

            
123
3
        if fs::metadata(&path).is_ok() && fs::remove_file(&path).is_ok() {
124
3
            info!("Deleted config located at '{path}'.");
125
3
            self.path = None;
126
3
            Ok(())
127
        } else {
128
            Err("File does not exist!")
129
        }
130
3
    }
131

            
132
    /// Saves the config if it was previously loaded or saved
133
3
    pub fn save(&mut self) -> Result<(), String> {
134
3
        let path = self
135
3
            .path
136
3
            .as_ref()
137
3
            .ok_or("Please run 'save_at' for the initial save.".to_string())?;
138

            
139
3
        self.save_at(&path.clone())
140
3
    }
141

            
142
    /// Saves the config in the home directory
143
    pub fn save_at_home(&mut self) -> Result<(), String> {
144
        let home = Config::home()
145
            .ok_or("Failed to save to the home directory as $HOME is not set!".to_string())?;
146

            
147
        self.save_at(&home)
148
    }
149

            
150
    /// Saves the config in the XDG_CONFIG_HOME directory
151
    pub fn save_at_xdg(&mut self) -> Result<(), String> {
152
        let home = Config::xdg().ok_or(
153
            "Failed to save to the xdg config home directory as $XDG_CONFIG_HOME is not set!"
154
                .to_string(),
155
        )?;
156

            
157
        self.save_at(&home)
158
    }
159

            
160
    /// Saves the config in the /etc directory
161
    pub fn save_at_etc(&mut self) -> Result<(), String> {
162
        self.save_at(&Config::etc())
163
    }
164

            
165
    /// Saves the config at the specified directory
166
12
    pub fn save_at(&mut self, path: &str) -> Result<(), String> {
167
12
        let file = OpenOptions::new()
168
12
            .create(true)
169
12
            .write(true)
170
12
            .open(&path)
171
12
            .map_err(|e| e.to_string())?;
172

            
173
12
        serde_json::to_writer_pretty(&file, &self).map_err(|e| e.to_string())?;
174

            
175
12
        self.path = Some(path.to_string());
176

            
177
12
        Ok(())
178
12
    }
179

            
180
    /// Returns a config with all the options off
181
12
    pub fn off() -> Self {
182
12
        Self {
183
12
            path: None,
184
12
            gamemode: false,
185
12
            mangohud: false,
186
12
        }
187
12
    }
188
}
189

            
190
impl PartialEq for Config {
191
12
    fn eq(&self, other: &Self) -> bool {
192
12
        self.path == other.path
193
12
            && self.gamemode == other.gamemode
194
12
            && self.mangohud == other.mangohud
195
12
    }
196

            
197
    fn ne(&self, other: &Self) -> bool {
198
        !self.eq(other)
199
    }
200
}
201

            
202
impl Default for Config {
203
21
    fn default() -> Self {
204
21
        Self {
205
21
            path: None,
206
21
            gamemode: true,
207
21
            mangohud: false,
208
21
        }
209
21
    }
210
}
211

            
212
#[cfg(test)]
213
pub mod tests {
214
    use super::*;
215

            
216
9
    pub fn get_test_file(name: &str) -> String {
217
9
        fs::create_dir_all("tests/.configs").unwrap();
218

            
219
9
        format!("tests/.configs/{}.json", name)
220
9
    }
221

            
222
    #[test]
223
3
    pub fn save_at() -> Result<(), String> {
224
3
        let file_name = get_test_file("save-at");
225
3
        let mut expected_config = Config::default();
226

            
227
3
        expected_config.save_at(&file_name)?;
228

            
229
3
        let actual_config = Config::load(&file_name)?;
230

            
231
3
        assert_eq!(expected_config, actual_config);
232

            
233
3
        Ok(())
234
3
    }
235

            
236
    #[test]
237
3
    pub fn save() -> Result<(), String> {
238
3
        let file_name = get_test_file("save");
239
3
        let mut expected_config = Config::default();
240

            
241
3
        expected_config.save_at(&file_name)?;
242

            
243
3
        expected_config.gamemode = false;
244

            
245
3
        expected_config.save()?;
246

            
247
3
        let actual_config = Config::load(&file_name)?;
248

            
249
3
        assert_eq!(expected_config, actual_config);
250

            
251
3
        Ok(())
252
3
    }
253

            
254
    #[test]
255
3
    pub fn delete() -> Result<(), String> {
256
3
        let file_name = get_test_file("delete");
257
3
        let mut expected_config = Config::default();
258

            
259
3
        expected_config.save_at(&file_name)?;
260

            
261
3
        Config::load(&file_name).expect("Config should exist");
262

            
263
3
        expected_config.delete()?;
264

            
265
3
        Config::load(&file_name).expect_err("Config should not exist");
266

            
267
3
        Ok(())
268
3
    }
269

            
270
    #[test]
271
3
    pub fn discover_default() -> Result<(), String> {
272
        // Not a fan of this, but not sure of a better way of testing this
273
3
        let mut previous_config = Config::discover();
274
3
        Config::wipe()?;
275

            
276
3
        let config = Config::discover();
277

            
278
3
        if previous_config.persisted() {
279
            previous_config.save()?;
280
3
        }
281

            
282
3
        assert_eq!(Config::default(), config);
283

            
284
3
        Ok(())
285
3
    }
286

            
287
    #[test]
288
3
    pub fn default() {
289
3
        assert_eq!(
290
3
            Config::default(),
291
            Config {
292
                path: None,
293
                gamemode: true,
294
                mangohud: false,
295
            }
296
        );
297
3
    }
298
}