paths.rsannotatedpaths.rssource219 lines · 7.2 KB · raw
1use crate::AppConfig;
2use std::fs::File;
3use std::path::{Path, PathBuf};
4use std::{fs, io};
5use thiserror::Error;
6
7pub const CONFIG_FILENAME: &str = "jgenesis-config.toml";
8pub const JGENESIS_SUBDIR: &str = "jgenesis";
9pub const PORTABLE_TXT_FILE_NAME: &str = "portable.txt";
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum ConfigDirType {
13    UserProfile,
14    EmulatorDirectory,
15    Override { file_path: PathBuf },
16}
17
18#[derive(Debug, Clone)]
19pub struct ConfigDirs {
20    pub user_profile_dir: Option<PathBuf>,
21    pub emulator_dir: Option<PathBuf>,
22}
23
24impl ConfigDirs {
25    #[must_use]
26    pub fn new() -> Self {
27        let user_profile_dir = determine_user_profile_dir();
28        let emulator_dir = jgenesis_common::determine_emulator_dir();
29
30        if user_profile_dir.is_none() {
31            log::error!("Unable to determine user profile directory");
32        }
33
34        if emulator_dir.is_none() {
35            log::error!("Unable to determine emulator directory");
36        }
37
38        Self { user_profile_dir, emulator_dir }
39    }
40
41    #[must_use]
42    pub fn get(&self, path_type: &ConfigDirType) -> Option<PathBuf> {
43        match path_type {
44            ConfigDirType::UserProfile => self.user_profile_dir.clone(),
45            ConfigDirType::EmulatorDirectory => self.emulator_dir.clone(),
46            ConfigDirType::Override { .. } => None,
47        }
48    }
49
50    #[must_use]
51    pub fn default_dir_type(&self, override_file_path: Option<PathBuf>) -> ConfigDirType {
52        if let Some(override_path) = override_file_path {
53            // Override path passed in via command line; takes priority
54            return ConfigDirType::Override { file_path: override_path };
55        }
56
57        if self.emulator_dir.as_ref().is_some_and(|emulator_dir| {
58            let portable_txt_path = emulator_dir.join(PORTABLE_TXT_FILE_NAME);
59            portable_txt_path.exists()
60                && portable_txt_path.metadata().is_ok_and(|metadata| metadata.is_file())
61        }) {
62            // portable.txt exists; default to storing settings in emulator directory
63            return ConfigDirType::EmulatorDirectory;
64        }
65
66        // Otherwise, default to storing settings in user profile directory
67        ConfigDirType::UserProfile
68    }

Create an empty portable.txt file in the emulator directory.

Errors

Propagates any I/O errors.

75    pub fn create_portable_txt(&self) -> io::Result<()> {
76        let Some(emulator_dir) = &self.emulator_dir else {
77            return Err(emulator_dir_not_found_error());
78        };
79
80        let portable_txt_path = emulator_dir.join(PORTABLE_TXT_FILE_NAME);
81        log::info!("Creating portable.txt file at '{}'", portable_txt_path.display());
82        File::create(portable_txt_path)?;
83
84        Ok(())
85    }

Delete the portable.txt file in the emulator directory.

Errors

Propagates any I/O errors, including if the file does not exist.

92    pub fn delete_portable_txt(&self) -> io::Result<()> {
93        let Some(emulator_dir) = &self.emulator_dir else {
94            return Err(emulator_dir_not_found_error());
95        };
96
97        let portable_txt_path = emulator_dir.join(PORTABLE_TXT_FILE_NAME);
98        log::info!("Deleting portable.txt file at '{}'", portable_txt_path.display());
99        fs::remove_file(portable_txt_path)
100    }
101}
103fn emulator_dir_not_found_error() -> io::Error {
104    io::Error::new(io::ErrorKind::NotFound, "unable to determine emulator directory".to_string())
105}
106
107impl Default for ConfigDirs {
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113#[derive(Debug, Error)]
114pub enum SaveConfigError {
115    #[error("I/O error: {0}")]
116    Io(#[from] io::Error),
117    #[error("TOML serialization error: {0}")]
118    Toml(#[from] toml::ser::Error),
119}
120
121pub struct ConfigWithPath {
122    pub config: AppConfig,
123    pub path: PathBuf,
124}
125
126impl ConfigWithPath {

Save the current config.

Errors

Propagates any errors encountered while serializing to TOML or writing to disk.

132    pub fn save_config(&self) -> Result<(), SaveConfigError> {
133        let config_str = toml::to_string_pretty(&self.config)?;
134        fs::write(&self.path, &config_str)?;
135
136        Ok(())
137    }
138}
140impl ConfigWithPath {
141    #[must_use]
142    pub fn new(config: AppConfig, path: PathBuf) -> Self {
143        Self { config, path }
144    }
145
146    #[must_use]
147    pub fn load_from_dir_or_default(
148        config_dirs: &ConfigDirs,
149        dir_type: &ConfigDirType,
150        default: impl FnOnce() -> AppConfig,
151    ) -> Self {
152        let config_path = match dir_type {
153            ConfigDirType::Override { file_path } => file_path.clone(),
154            _ => {
155                let Some(directory) = config_dirs.get(dir_type) else {
156                    log::error!(
157                        "Unable to determine filesystem path for directory type {dir_type:?}; config will probably not save"
158                    );
159                    return Self::new(default(), CONFIG_FILENAME.into());
160                };
161
162                if !directory.exists()
163                    && let Err(err) = fs::create_dir_all(&directory)
164                {
165                    log::error!(
166                        "Unable to create config directory '{}', app config will probably not save: {err}",
167                        directory.display()
168                    );
169                    return Self::new(default(), CONFIG_FILENAME.into());
170                }
171
172                directory.join(CONFIG_FILENAME)
173            }
174        };
175
176        if !config_path.exists() {
177            // Config file used to always be at $CWD/jgenesis-config.toml
178            // If this file exists but there's no file at the config path, bootstrap config path
179            // by copying from that location
180            if matches!(dir_type, ConfigDirType::UserProfile) && Path::new(CONFIG_FILENAME).exists()
181            {
182                log::info!(
183                    "Bootstrapping config at '{}' from config in current directory",
184                    config_path.display()
185                );
186
187                if let Err(err) = fs::copy(Path::new(CONFIG_FILENAME), &config_path) {
188                    log::error!("Error copying config to '{}': {err}", config_path.display());
189                }
190            } else {
191                // File does not exist and we're not going to bootstrap; use default/current config
192                return Self::new(default(), config_path);
193            }
194        }
195
196        log::info!("Loading initial config from '{}'", config_path.display());
197
198        let mut config_str = fs::read_to_string(&config_path).unwrap_or_default();
199
200        crate::migrate_config_str(&mut config_str);
201
202        let config = toml::from_str(&config_str).unwrap_or_else(|err| {
203            log::error!("Error deserializing app config at '{}': {err}", config_path.display());
204            default()
205        });
206
207        let migrated_config = crate::migrate_config(&config, &config_str).unwrap_or(config);
208
209        Self::new(migrated_config, config_path)
210    }
211}

Typically: $HOME/.config/jgenesis/ on Linux %userprofile\AppData\Local\jgenesis\ on Windows

216fn determine_user_profile_dir() -> Option<PathBuf> {
217    let base_dirs = directories::BaseDirs::new()?;
218    Some(base_dirs.config_local_dir().join(JGENESIS_SUBDIR))
219}