timewall 1.3.0

All-in-one tool for Apple dynamic HEIF wallpapers on GNU/Linux
use std::{
    env, fs,
    path::{Path, PathBuf},
};

use anyhow::{anyhow, bail, Context, Ok, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};

use crate::constants::{APP_NAME, APP_QUALIFIER};
use crate::geo::Coords;

const CONFIG_FILE_NAME: &str = "config.toml";

const DEFAULT_CONFIG_FILE_CONTENT: &str = "\
# Configuration file for timewall

# Set your geographical location coordinates here
# [location]
# lat = 51.11
# lon = 17.02

# Uncomment and adjust the following section to use a custom wallpaper setter command.
# The example uses `swww`: https://github.com/LGFae/swww.
# [setter]
# command = ['swww', 'img', '%f']

# Change how often the wallpaper is updated in daemon mode
# [daemon]
# update_interval_seconds = 300
";

#[derive(Deserialize, Serialize, Debug)]
pub struct Setter {
    pub command: Vec<String>,
}

#[derive(Deserialize, Serialize, Clone, Copy, Debug)]
pub struct Daemon {
    pub update_interval_seconds: u64,
}

impl Default for Daemon {
    fn default() -> Self {
        Self {
            update_interval_seconds: 5 * 60,
        }
    }
}

#[derive(Deserialize, Serialize, Debug)]
pub struct Config {
    pub daemon: Option<Daemon>,
    pub location: Option<Coords>,
    pub setter: Option<Setter>,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            daemon: Some(Daemon::default()),
            location: None,
            setter: None,
        }
    }
}

impl Config {
    pub fn find() -> Result<Self> {
        Self::load_or_create(Self::find_path()?)
    }

    pub fn find_path() -> Result<PathBuf> {
        let config_dir = if let Result::Ok(path_str) = env::var("TIMEWALL_CONFIG_DIR") {
            PathBuf::from(path_str)
        } else {
            match ProjectDirs::from(APP_QUALIFIER, "", APP_NAME) {
                Some(app_dirs) => app_dirs.config_dir().to_path_buf(),
                None => bail!("couldn't determine user's home directory"),
            }
        };
        Ok(config_dir.join(CONFIG_FILE_NAME))
    }

    fn load_or_create<P: AsRef<Path>>(path: P) -> Result<Self> {
        let path = path.as_ref();
        if !path.exists() {
            Self::create_default(path)?;
        }
        Self::load(path)
    }

    fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
        let path = path.as_ref();
        let config_str =
            fs::read_to_string(path).with_context(|| "couldn't read the configuration file")?;
        let config: Self =
            toml::from_str(&config_str).with_context(|| "couldn't parse the configuation file")?;
        Ok(config)
    }

    fn create_default<P: AsRef<Path>>(path: P) -> Result<()> {
        let path = path.as_ref();
        let config_dir = path.parent().unwrap();
        if !config_dir.exists() {
            fs::create_dir_all(config_dir).context("couldn't create config directory")?;
        }

        fs::write(path, DEFAULT_CONFIG_FILE_CONTENT).with_context(|| {
            format!("couldn't write default configuration to {}", path.display())
        })?;

        eprintln!("Default config written to {}.", path.display());
        eprintln!("You should probably adjust it to your needs!");
        Ok(())
    }

    pub fn setter_command(&self) -> Option<&Vec<String>> {
        self.setter.as_ref().map(|s| &s.command)
    }

    pub fn update_interval_seconds(&self) -> u64 {
        self.daemon.unwrap_or_default().update_interval_seconds
    }

    pub fn try_get_location(&self) -> Result<&Coords> {
        self.location
            .as_ref()
            .ok_or_else(|| anyhow!("location not set in the configuration"))
    }

    pub fn validate_for_solar(&self) -> Result<()> {
        if self.location.is_none() {
            let config_path = Self::find_path()?;
            bail!(
                "using wallpapers with solar schedule requires setting your location in the configuration file at {}",
                config_path.display()
            );
        }
        Ok(())
    }
}