dtmt/crates/dtmm/src/util/config.rs
Lucas Schwiderski 9428b076f0
feat(dtmm): Delay initial load
Delays the loading of the configuration file and mod data, so that
any error can be shown in the UI.

Closes #72.
2023-03-16 14:31:53 +01:00

166 lines
4.9 KiB
Rust

use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use color_eyre::{eyre::Context, Result};
use serde::{Deserialize, Serialize};
use tokio::fs;
use crate::state::{ActionState, ModInfo};
#[derive(Clone, Debug, Serialize)]
pub(crate) struct LoadOrderEntrySerialize<'a> {
pub id: &'a String,
pub enabled: bool,
}
impl<'a> From<&'a ModInfo> for LoadOrderEntrySerialize<'a> {
fn from(info: &'a ModInfo) -> Self {
Self {
id: &info.id,
enabled: info.enabled,
}
}
}
#[derive(Debug, Serialize)]
pub(crate) struct ConfigSerialize<'a> {
game_dir: &'a Path,
data_dir: &'a Path,
nexus_api_key: &'a String,
mod_order: Vec<LoadOrderEntrySerialize<'a>>,
}
impl<'a> From<&'a ActionState> for ConfigSerialize<'a> {
fn from(state: &'a ActionState) -> Self {
Self {
game_dir: &state.game_dir,
data_dir: &state.data_dir,
nexus_api_key: &state.nexus_api_key,
mod_order: state
.mods
.iter()
.map(Arc::as_ref)
.map(LoadOrderEntrySerialize::from)
.collect(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct LoadOrderEntry {
pub id: String,
pub enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct Config {
#[serde(skip)]
pub path: PathBuf,
#[serde(default = "get_default_data_dir")]
pub data_dir: PathBuf,
pub game_dir: Option<PathBuf>,
pub nexus_api_key: Option<String>,
#[serde(default)]
pub mod_order: Vec<LoadOrderEntry>,
}
#[cfg(not(target_os = "windows"))]
pub fn get_default_config_path() -> PathBuf {
let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| {
let user = std::env::var("USER").expect("user env variable not set");
format!("/home/{user}")
});
format!("{home}/.config")
});
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
}
#[cfg(target_os = "windows")]
pub fn get_default_config_path() -> PathBuf {
let config_dir = std::env::var("APPDATA").expect("appdata env var not set");
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
}
#[cfg(not(target_os = "windows"))]
pub fn get_default_data_dir() -> PathBuf {
let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| {
let user = std::env::var("USER").expect("user env variable not set");
format!("/home/{user}")
});
format!("{home}/.local/share")
});
PathBuf::from(data_dir).join("dtmm")
}
#[cfg(target_os = "windows")]
pub fn get_default_data_dir() -> PathBuf {
let data_dir = std::env::var("LOCALAPPDATA").expect("appdata env var not set");
PathBuf::from(data_dir).join("dtmm")
}
#[tracing::instrument]
pub(crate) async fn read_config<P>(path: P, is_default: bool) -> Result<Config>
where
P: Into<PathBuf> + std::fmt::Debug,
{
let path = path.into();
let default_path = get_default_config_path();
match fs::read(&path).await {
Ok(data) => {
let data = String::from_utf8(data).wrap_err_with(|| {
format!("Config file '{}' contains invalid UTF-8", path.display())
})?;
let mut cfg: Config = serde_sjson::from_str(&data)
.wrap_err_with(|| format!("Invalid config file {}", path.display()))?;
cfg.path = path;
Ok(cfg)
}
Err(err) if err.kind() == ErrorKind::NotFound => {
if !is_default {
return Err(err)
.wrap_err_with(|| format!("Failed to read config file {}", path.display()))?;
}
{
let parent = default_path
.parent()
.expect("a file path always has a parent directory");
fs::create_dir_all(parent).await.wrap_err_with(|| {
format!("Failed to create directories {}", parent.display())
})?;
}
let config = Config {
path: default_path,
data_dir: get_default_data_dir(),
game_dir: None,
nexus_api_key: None,
mod_order: Vec::new(),
};
{
let data = serde_sjson::to_string(&config)
.wrap_err("Failed to serialize default config value")?;
fs::write(&config.path, data).await.wrap_err_with(|| {
format!(
"failed to write default config to {}",
config.path.display()
)
})?;
}
Ok(config)
}
Err(err) => {
Err(err).wrap_err_with(|| format!("Failed to read config file {}", path.display()))
}
}
}