timewall 2.0.2

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

# Dynamic location service
# [geoclue]
# enable = true
# cache_fallback = true
# prefer = false
# timeout = 1000

# 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']
# overlap = 0
# quiet = true

# 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>,
    #[serde(default = "Setter::overlap_default_value")]
    pub overlap: u64,
    #[serde(default = "Setter::quiet_default_value")]
    pub quiet: bool,
}

impl Setter {
    const fn overlap_default_value() -> u64 {
        0
    }

    const fn quiet_default_value() -> bool {
        true
    }
}

#[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, Clone, Copy, Debug)]
pub struct Geoclue {
    #[serde(default = "Geoclue::enable_default_value")]
    pub enable: bool,
    #[serde(default = "Geoclue::cache_fallback_default_value")]
    pub cache_fallback: bool,
    #[serde(default = "Geoclue::prefer_default_value")]
    pub prefer: bool,
    #[serde(default = "Geoclue::timeout_default_value")]
    pub timeout: u64,
}

impl Geoclue {
    const fn enable_default_value() -> bool {
        true
    }

    const fn cache_fallback_default_value() -> bool {
        true
    }

    const fn prefer_default_value() -> bool {
        false
    }

    const fn timeout_default_value() -> u64 {
        1000
    }
}

impl Default for Geoclue {
    fn default() -> Self {
        Self {
            enable: Self::enable_default_value(),
            prefer: Self::prefer_default_value(),
            cache_fallback: Self::cache_fallback_default_value(),
            timeout: Self::timeout_default_value(),
        }
    }
}

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

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 try_get_location(&self) -> Result<Coords> {
        self.location
            .ok_or_else(|| anyhow!("location not set in the configuration"))
    }
}