use crate::playset::steam::find_steam_root_path;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub const CONFIG_DIR_ENV: &str = "FOCH_CONFIG_DIR";
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct Config {
#[serde(default)]
pub steam_root_path: Option<PathBuf>,
#[serde(default)]
pub paradox_data_path: Option<PathBuf>,
#[serde(default)]
pub game_path: HashMap<String, PathBuf>,
#[serde(default)]
pub extra_ignore_patterns: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub enum ValidationStatus {
Ok,
Warning,
Error,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ValidationItem {
pub key: String,
pub status: ValidationStatus,
pub message: String,
}
impl TryFrom<&Path> for Config {
type Error = std::io::Error;
fn try_from(p: &Path) -> Result<Self, Self::Error> {
let content = std::fs::read_to_string(p)?;
toml::from_str(&content).map_err(std::io::Error::other)
}
}
pub fn get_config_dir_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
if let Ok(path) = std::env::var(CONFIG_DIR_ENV) {
return Ok(PathBuf::from(path));
}
let home = dirs::home_dir().ok_or("failed to get $HOME directory")?;
Ok(home.join(".config").join("foch"))
}
impl Config {
pub fn load_config(path: &Path) -> Result<Self, std::io::Error> {
let content = std::fs::read_to_string(path)?;
if content.trim().is_empty() {
return Ok(Self::default());
}
toml::from_str(&content).map_err(std::io::Error::other)
}
pub fn save_config(&self, path: &Path) -> Result<(), std::io::Error> {
let content = toml::to_string_pretty(self).map_err(std::io::Error::other)?;
std::fs::write(path, content)
}
pub fn validate(&self) -> Vec<ValidationItem> {
let mut items = Vec::new();
match &self.steam_root_path {
Some(path) if path.exists() => items.push(ValidationItem {
key: "steam_root_path".to_string(),
status: ValidationStatus::Ok,
message: format!("Steam path is available: {}", path.display()),
}),
Some(path) => items.push(ValidationItem {
key: "steam_root_path".to_string(),
status: ValidationStatus::Error,
message: format!("Steam path does not exist: {}", path.display()),
}),
None => items.push(ValidationItem {
key: "steam_root_path".to_string(),
status: ValidationStatus::Warning,
message: "Steam path is not set; automatic Workshop mod discovery may fail"
.to_string(),
}),
}
match &self.paradox_data_path {
Some(path) if path.exists() => items.push(ValidationItem {
key: "paradox_data_path".to_string(),
status: ValidationStatus::Ok,
message: format!("Paradox data directory is available: {}", path.display()),
}),
Some(path) => items.push(ValidationItem {
key: "paradox_data_path".to_string(),
status: ValidationStatus::Error,
message: format!("Paradox data directory does not exist: {}", path.display()),
}),
None => items.push(ValidationItem {
key: "paradox_data_path".to_string(),
status: ValidationStatus::Warning,
message: "Paradox data directory is not set; local mod resolution may fail"
.to_string(),
}),
}
if self.game_path.is_empty() {
items.push(ValidationItem {
key: "game_path".to_string(),
status: ValidationStatus::Warning,
message: "no game install paths are configured".to_string(),
});
} else {
for (game, path) in &self.game_path {
let status = if path.exists() {
ValidationStatus::Ok
} else {
ValidationStatus::Error
};
let message = if path.exists() {
format!("game {game} path is available: {}", path.display())
} else {
format!("game {game} path does not exist: {}", path.display())
};
items.push(ValidationItem {
key: format!("game_path.{game}"),
status,
message,
});
}
}
items
}
}
pub fn load_or_init_config() -> Result<(Config, PathBuf), Box<dyn std::error::Error>> {
let config_dir = get_config_dir_path()?;
if !config_dir.exists() {
std::fs::create_dir_all(&config_dir)?;
}
let config_file = config_dir.join("config.toml");
if !config_file.exists() {
std::fs::File::create(&config_file)?;
}
let mut config = Config::load_config(&config_file)?;
if config.steam_root_path.is_none()
&& let Some(path) = find_steam_root_path()
{
config.steam_root_path = Some(path);
config.save_config(&config_file)?;
}
Ok((config, config_file))
}
pub fn load_config_read_only() -> Result<Config, Box<dyn std::error::Error>> {
let config_file = get_config_dir_path()?.join("config.toml");
let mut config = if config_file.is_file() {
Config::load_config(&config_file)?
} else {
Config::default()
};
if config.steam_root_path.is_none() {
config.steam_root_path = find_steam_root_path();
}
Ok(config)
}
#[cfg(test)]
mod tests {
use super::{Config, ValidationStatus};
#[test]
fn validate_reports_missing_paths() {
let config = Config::default();
let statuses: Vec<ValidationStatus> =
config.validate().into_iter().map(|x| x.status).collect();
assert!(statuses.contains(&ValidationStatus::Warning));
}
}