use crate::components::constants::TEAM_IDS;
use crate::state::app_settings::{AppSettings, compute_timezone_abbreviation};
use anyhow::Context;
use chrono_tz::Tz;
use chrono_tz::Tz::US__Pacific;
use directories::ProjectDirs;
use log::{LevelFilter, error};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
static CONFIG_FILE_LOCATION: OnceLock<Option<PathBuf>> = OnceLock::new();
pub const DEFAULT_TIMEZONE: Tz = US__Pacific;
pub const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Off,
Trace,
Debug,
Info,
Warn,
Error,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ConfigFile {
pub favorite_team: Option<String>,
pub timezone: Option<Tz>,
pub log_level: Option<LogLevel>,
}
impl Default for ConfigFile {
fn default() -> Self {
Self {
favorite_team: None,
timezone: Some(DEFAULT_TIMEZONE),
log_level: Some(DEFAULT_LOG_LEVEL),
}
}
}
impl From<ConfigFile> for AppSettings {
fn from(file: ConfigFile) -> Self {
let favorite_team = file
.favorite_team
.as_deref()
.and_then(|name| TEAM_IDS.get(name).copied());
let timezone = file.timezone.unwrap_or(DEFAULT_TIMEZONE);
let timezone_abbreviation = compute_timezone_abbreviation(timezone);
let log_level = file.log_level.unwrap_or(DEFAULT_LOG_LEVEL);
Self {
favorite_team,
full_screen: false,
timezone,
timezone_abbreviation,
log_level,
}
}
}
impl From<&AppSettings> for ConfigFile {
fn from(s: &AppSettings) -> Self {
Self {
favorite_team: s.favorite_team.map(|t| t.name.to_string()),
timezone: Some(s.timezone),
log_level: Some(s.log_level),
}
}
}
pub struct TomlFileStore {
path: Option<PathBuf>,
}
impl Default for TomlFileStore {
fn default() -> Self {
Self {
path: Self::default_path(),
}
}
}
impl TomlFileStore {
const HEADER: &str = "# See https://github.com/mlb-rs/mlbt#config for options\n";
const CONFIG_FILE_NAME: &str = "mlbt.toml";
#[cfg(test)]
pub fn with_path(path: PathBuf) -> Self {
Self { path: Some(path) }
}
pub fn default_path() -> Option<PathBuf> {
CONFIG_FILE_LOCATION
.get_or_init(|| {
if let Some(proj_dirs) = ProjectDirs::from("", "", "mlbt") {
let dir = proj_dirs.config_dir();
if !dir.exists()
&& let Err(err) = std::fs::create_dir_all(dir)
{
error!("could not create config dir: {err:?}");
}
Some(dir.join(Self::CONFIG_FILE_NAME))
} else {
error!("could not get valid home directory for config file");
None
}
})
.clone()
}
pub fn load(&self) -> anyhow::Result<AppSettings> {
let path = self.path.as_ref().context("could not find config file")?;
if !path.exists() {
Self::write(path, &ConfigFile::default())?;
}
let contents = std::fs::read_to_string(path).context("could not read config file")?;
let file: ConfigFile =
toml::from_str(&contents).context("could not deserialize config file")?;
Ok(file.into())
}
pub fn save(&self, settings: &AppSettings) -> anyhow::Result<()> {
let path = self.path.as_ref().context("could not find config file")?;
Self::write(path, &ConfigFile::from(settings))
}
fn write(path: &Path, file: &ConfigFile) -> anyhow::Result<()> {
let body = toml::to_string(file).context("could not serialize config")?;
let contents = format!("{}{body}", Self::HEADER);
let tmp = path.with_extension("toml.tmp");
std::fs::write(&tmp, contents).context("could not write config file")?;
std::fs::rename(&tmp, path).context("could not finalize config file")?;
Ok(())
}
}
impl From<LogLevel> for LevelFilter {
fn from(level: LogLevel) -> Self {
match level {
LogLevel::Off => LevelFilter::Off,
LogLevel::Trace => LevelFilter::Trace,
LogLevel::Debug => LevelFilter::Debug,
LogLevel::Info => LevelFilter::Info,
LogLevel::Warn => LevelFilter::Warn,
LogLevel::Error => LevelFilter::Error,
}
}
}