mist-core 2.0.1

core functionality of mist
use super::{Colors, Font, KeybindsRaw, Panel};
use crate::error::Result;
use directories::BaseDirs;
use ron::{
    de::from_reader,
    extensions::Extensions,
    ser::{to_string_pretty, to_writer_pretty, PrettyConfig},
};
use serde::{Deserialize, Serialize};
use std::{fs::OpenOptions, io::Write, path::PathBuf};

#[derive(Serialize, Deserialize, Debug)]
/// Configuration of mist.
#[serde(default)]
pub struct Config {
    def_file: Option<PathBuf>,
    win_size: (u32, u32),
    #[cfg(feature = "bg")]
    img_file: Option<PathBuf>,
    #[cfg(feature = "bg")]
    img_scaled: bool,
    inline_splits: bool,
    colors: Colors,
    frame_rounding: Option<u128>,
    panels: Vec<Panel>,
    #[serde(default = "Font::timer_default")]
    t_font: Font,
    #[serde(default = "Font::splits_default")]
    s_font: Font,
    ms_ratio: f32,
    confirm_exit: bool,
    plugins: bool,
    binds: KeybindsRaw,
}

impl Config {
    /// Attempts to open and parse mist's default config.
    ///
    /// # Errors
    ///
    /// * If the file cannot be parsed.
    /// * If the file does not exist or cannot be read.
    pub fn open() -> Result<Self> {
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .open(config_path()?)?;
        Ok(from_reader(&file)?)
    }
    /// Get the split file from the Config. Returns None if no file set.
    pub fn file(&self) -> Option<&PathBuf> {
        self.def_file.as_ref()
    }
    #[cfg(feature = "bg")]
    /// Get the path to the image file to be used as a background for the timer.
    pub fn img(&self) -> Option<&PathBuf> {
        self.img_file.as_ref()
    }
    #[cfg(feature = "bg")]
    /// Determine whether the image should be scaled to fit the screen or cropped.
    pub fn img_scaled(&self) -> bool {
        self.img_scaled
    }
    /// Set the split file path to a new one.
    pub fn set_file(&mut self, file: impl Into<PathBuf>) {
        self.def_file = Some(file.into());
    }
    /// Get the [`Font`] used for the display timer.
    pub fn tfont(&self) -> &Font {
        &self.t_font
    }
    /// Get the [`Font`] used for the rows of splits.
    pub fn sfont(&self) -> &Font {
        &self.s_font
    }
    /// Get the list of colors to be used for the timer.
    pub fn colors(&self) -> Colors {
        self.colors
    }
    /// Write the config to the file.
    ///
    /// # Errors
    ///
    /// * If the serialization fails.
    /// * If the file cannot be written to or opened.
    pub fn save(&self) -> Result<()> {
        let mut file = OpenOptions::new()
            .write(true)
            .truncate(true)
            .open(config_path()?)?;
        let string = to_string_pretty(
            self,
            PrettyConfig::new().extensions(Extensions::IMPLICIT_SOME),
        )?;
        file.write_all(string.as_bytes())?;
        Ok(())
    }
    /// Get the keybinds in string form as names of keys.
    pub fn binds(&self) -> &KeybindsRaw {
        &self.binds
    }
    /// Get whether splits are in line with times or not.
    pub fn inline_splits(&self) -> bool {
        self.inline_splits
    }
    /// Get the list of timing display panels.
    pub fn panels(&self) -> &Vec<Panel> {
        &self.panels
    }
    /// Get the requested framerate to round times to.
    /// `None` representes no rounding.
    pub fn rounding(&self) -> Option<u128> {
        self.frame_rounding
    }
    /// Get the ratio of millisecond font size to timer font size.
    pub fn ms_ratio(&self) -> f32 {
        self.ms_ratio
    }
    /// Get the size of the window in pixels.
    pub fn win_size(&self) -> (u32, u32) {
        self.win_size
    }
    /// Set the window size.
    pub fn set_win_size(&mut self, new: (u32, u32)) {
        self.win_size = new;
    }
    /// Get whether mist should prompt the user to confirm exiting.
    pub fn confirm(&self) -> bool {
        self.confirm_exit
    }
    /// Get whether plugins are enabled.
    pub fn plugins(&self) -> bool {
        self.plugins
    }
}

impl Default for Config {
    fn default() -> Config {
        Config {
            def_file: None,
            win_size: (300, 500),
            #[cfg(feature = "bg")]
            img_file: None,
            #[cfg(feature = "bg")]
            img_scaled: false,
            frame_rounding: None,
            colors: Colors::default(),
            inline_splits: false,
            panels: vec![],
            t_font: Font::timer_default(),
            s_font: Font::splits_default(),
            ms_ratio: 1.0,
            confirm_exit: true,
            plugins: true,
            binds: KeybindsRaw::default(),
        }
    }
}

fn config_path() -> Result<PathBuf> {
    let cfg_path = get_config_path()?;
    if !cfg_path.exists() {
        let f = OpenOptions::new()
            .create(true)
            .write(true)
            .open(&cfg_path)?;
        to_writer_pretty(
            f,
            &Config::default(),
            PrettyConfig::new().extensions(Extensions::IMPLICIT_SOME),
        )?;
    }
    Ok(cfg_path)
}

fn get_config_path() -> Result<PathBuf> {
    #[cfg(feature = "portable")]
    if let Ok(mut p) = std::env::current_exe() {
        p.pop();
        p.push("mist.cfg");
        return Ok(p);
    }
    let dirs = BaseDirs::new().ok_or("Could not find your config directory!")?;
    let mut cfg_path = dirs.config_dir().to_path_buf();
    cfg_path.push("mist");
    if !cfg_path.exists() {
        std::fs::create_dir_all(&cfg_path)?;
    }
    cfg_path.push("mist.cfg");
    Ok(cfg_path)
}

/// Get the plugin directory.
///
/// This is platform-dependent and dependent on whether the `portable`
/// feature is enabled. It will be the directory `plugins` inside either
/// the platform-specific config directory or in the directory where the
/// executable is.
pub fn get_plugin_dir() -> Result<PathBuf> {
    let mut cfg_path = get_config_path()?;
    cfg_path.pop();
    cfg_path.push("plugins");
    Ok(cfg_path)
}