Skip to main content

valheim_mod_manager/
config.rs

1use config::{Config, ConfigError, File};
2use serde::{Deserialize, Serialize};
3use std::io::Write;
4use std::{fs::OpenOptions, path::Path};
5
6use crate::error::{AppError, AppResult};
7
8/// Application configuration loaded from vmm_config.toml.
9///
10/// This structure defines all user-configurable settings for the Valheim Mod Manager,
11/// including which mods to manage, logging preferences, and file system paths.
12#[derive(Serialize, Deserialize)]
13pub struct AppConfig {
14  /// List of mods to install and manage, specified as "Owner-ModName" strings.
15  pub mod_list: Vec<String>,
16  /// Logging level (e.g., "error", "warn", "info", "debug", "trace").
17  pub log_level: String,
18  /// Optional directory path where mods should be installed.
19  pub install_dir: Option<String>,
20}
21
22impl Default for AppConfig {
23  fn default() -> Self {
24    Self {
25      mod_list: vec![],
26      log_level: "error".into(),
27      install_dir: None,
28    }
29  }
30}
31
32/// The XDG config directory for vmm (~/.config/vmm).
33///
34/// Used as the cache and data directory for downloaded manifests and mod files.
35#[cfg(not(tarpaulin_include))]
36pub static APP_CACHE_DIR: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
37  xdg::BaseDirectories::with_prefix("vmm")
38    .expect("Failed to initialize XDG base directories")
39    .get_config_home()
40    .to_string_lossy()
41    .into_owned()
42});
43
44/// Loads application configuration.
45///
46/// If `config_override` is provided, loads only from that path.
47/// Otherwise, looks for a local `vmm_config.toml` first, then falls back to
48/// the XDG config location (`~/.config/vmm/vmm_config.toml`). If neither
49/// exists, a default config is created at the XDG location.
50pub fn get_config(config_override: Option<&Path>) -> Result<AppConfig, ConfigError> {
51  let default_config_data = AppConfig::default();
52
53  let mut builder = Config::builder()
54    .set_default("mod_list", default_config_data.mod_list.clone())?
55    .set_default("log_level", default_config_data.log_level.clone())?
56    .set_default("install_dir", default_config_data.install_dir.clone())?;
57
58  if let Some(path) = config_override {
59    builder = builder.add_source(File::with_name(path.to_str().unwrap_or_default()));
60  } else {
61    let local_path = Path::new("vmm_config.toml");
62    let xdg_dirs =
63      xdg::BaseDirectories::with_prefix("vmm").expect("Failed to initialize XDG base directories");
64    let global_path = xdg_dirs.get_config_file("vmm_config.toml");
65
66    if !local_path.exists() && !global_path.exists() {
67      if let Ok(path) = xdg_dirs.place_config_file("vmm_config.toml") {
68        let _ = create_missing_config_file(&path, &default_config_data);
69      }
70    }
71
72    if global_path.exists() {
73      builder = builder.add_source(File::with_name(global_path.to_str().unwrap_or_default()));
74    }
75
76    if local_path.exists() {
77      builder = builder.add_source(File::with_name("vmm_config.toml"));
78    }
79  }
80
81  builder.build()?.try_deserialize()
82}
83
84/// Creates a new configuration file with default values.
85///
86/// # Parameters
87///
88/// * `config_path` - Path where the config file should be created
89/// * `default_config_data` - Default configuration values to serialize
90///
91/// # Returns
92///
93/// `Ok(())` on success, or an error if file creation or serialization fails.
94fn create_missing_config_file(
95  config_path: &Path,
96  default_config_data: &AppConfig,
97) -> AppResult<()> {
98  let serialized_config_data = toml::to_string(&default_config_data)
99    .map_err(|e| AppError::ConfigSerialization(format!("{}", e)))?;
100
101  let mut config_file = OpenOptions::new()
102    .write(true)
103    .create_new(true)
104    .open(config_path)?;
105
106  write!(config_file, "{}", serialized_config_data)?;
107
108  Ok(())
109}
110
111#[cfg(test)]
112mod tests {
113  use super::*;
114  use std::fs;
115  use tempfile::tempdir;
116
117  #[test]
118  fn test_default_config() {
119    let default_config = AppConfig::default();
120
121    assert!(default_config.mod_list.is_empty());
122    assert_eq!(default_config.log_level, "error");
123  }
124
125  #[test]
126  fn test_create_missing_config_file() {
127    let dir = tempdir().unwrap();
128    let config_path = dir.path().join("test_config.toml");
129    let default_config = AppConfig::default();
130
131    let result = create_missing_config_file(&config_path, &default_config);
132    assert!(result.is_ok());
133
134    assert!(config_path.exists());
135
136    let content = fs::read_to_string(&config_path).unwrap();
137    assert!(content.contains("mod_list"));
138    assert!(content.contains("log_level"));
139    assert!(content.contains("error"));
140
141    let parsed: AppConfig = toml::from_str(&content).unwrap();
142    assert_eq!(parsed.log_level, default_config.log_level);
143    assert_eq!(parsed.mod_list.len(), default_config.mod_list.len());
144  }
145
146  #[test]
147  fn test_custom_config_values() {
148    let custom_config = AppConfig {
149      mod_list: vec!["Owner1-ModA".to_string(), "Owner2-ModB".to_string()],
150      log_level: "debug".to_string(),
151      install_dir: Some("/path/to/install/directory".to_string()),
152    };
153
154    let dir = tempdir().unwrap();
155    let config_path = dir.path().join("custom_config.toml");
156
157    let result = create_missing_config_file(&config_path, &custom_config);
158    assert!(result.is_ok());
159
160    let content = fs::read_to_string(&config_path).unwrap();
161
162    assert!(content.contains("Owner1-ModA"));
163    assert!(content.contains("Owner2-ModB"));
164    assert!(content.contains("debug"));
165
166    let parsed: AppConfig = toml::from_str(&content).unwrap();
167    assert_eq!(parsed.log_level, "debug");
168    assert_eq!(parsed.mod_list.len(), 2);
169    assert_eq!(parsed.mod_list[0], "Owner1-ModA");
170    assert_eq!(parsed.mod_list[1], "Owner2-ModB");
171  }
172
173  #[test]
174  fn test_get_config_with_override() {
175    let dir = tempdir().unwrap();
176    let config_path = dir.path().join("override_config.toml");
177
178    let custom_config = AppConfig {
179      mod_list: vec!["Owner1-ModA".to_string()],
180      log_level: "debug".to_string(),
181      install_dir: None,
182    };
183
184    create_missing_config_file(&config_path, &custom_config).unwrap();
185
186    let loaded = get_config(Some(&config_path)).unwrap();
187    assert_eq!(loaded.log_level, "debug");
188    assert_eq!(loaded.mod_list, vec!["Owner1-ModA"]);
189  }
190}