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 } 69 70 /// Create an empty portable.txt file in the emulator directory. 71 /// 72 /// # Errors 73 /// 74 /// 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 } 86 87 /// Delete the portable.txt file in the emulator directory. 88 /// 89 /// # Errors 90 /// 91 /// 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} 102 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 { 127 /// Save the current config. 128 /// 129 /// # Errors 130 /// 131 /// 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} 139 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} 212 213// Typically: 214// `$HOME/.config/jgenesis/` on Linux 215// `%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}