//! Deserializes configuration options. use std::error; use std::fmt::{self, Display}; use std::fs::File; use std::io::{self, Read}; use std::path::{Path, PathBuf}; use super::keybinds::{Keybinds, KeybindsFromYaml}; use crate::cli::ConfigCli; use directories_next::ProjectDirs; use serde::Deserialize; type ConfigResult = Result; /// Intermediate deserialisation config struct #[derive(Debug, Deserialize)] pub struct ConfigFromYaml { pub keybinds: Option, } /// Main configuration. #[derive(Debug, Clone, PartialEq)] pub struct Config { pub keybinds: Keybinds, } #[derive(Debug)] pub enum ConfigError { // Deserialisation error Serde(serde_yaml::Error), // Io error Io(io::Error), // Io error with path context IoPath(io::Error, PathBuf), } impl Default for Config { fn default() -> Self { let keybinds = Keybinds::default(); Config { keybinds } } } impl Config { /// Uses defaults, but lets config override them. pub fn from_yaml(yaml_config: &str) -> ConfigResult { let config_from_yaml: ConfigFromYaml = serde_yaml::from_str(&yaml_config)?; let keybinds = Keybinds::get_default_keybinds_with_config(config_from_yaml.keybinds); Ok(Config { keybinds }) } /// Deserializes from given path. pub fn new(path: &Path) -> ConfigResult { match File::open(path) { Ok(mut file) => { let mut yaml_config = String::new(); file.read_to_string(&mut yaml_config) .map_err(|e| ConfigError::IoPath(e, path.to_path_buf()))?; Ok(Config::from_yaml(&yaml_config)?) } Err(e) => Err(ConfigError::IoPath(e, path.into())), } } /// Deserializes the config from a default platform specific path, /// merges the default configuration - options take precedence. fn from_default_path() -> ConfigResult { let project_dirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); let mut config_path: PathBuf = project_dirs.config_dir().to_owned(); config_path.push("config.yaml"); match Config::new(&config_path) { Ok(config) => Ok(config), Err(ConfigError::IoPath(_, _)) => Ok(Config::default()), Err(e) => Err(e), } } /// Entry point of the configuration #[cfg(not(test))] pub fn from_cli_config(cli_config: Option) -> ConfigResult { match cli_config { Some(ConfigCli::Config { clean, .. }) if clean => Ok(Config::default()), Some(ConfigCli::Config { path, .. }) if path.is_some() => { Ok(Config::new(&path.unwrap())?) } Some(_) | None => Ok(Config::from_default_path()?), } } /// In order not to mess up tests from changing configurations #[cfg(test)] pub fn from_cli_config(_: Option) -> ConfigResult { Ok(Config::default()) } } impl Display for ConfigError { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { match self { ConfigError::Io(ref err) => write!(formatter, "IoError: {}", err), ConfigError::IoPath(ref err, ref path) => { write!(formatter, "IoError: {}, File: {}", err, path.display(),) } ConfigError::Serde(ref err) => write!(formatter, "Deserialisation error: {}", err), } } } impl std::error::Error for ConfigError { fn cause(&self) -> Option<&dyn error::Error> { match *self { ConfigError::Io(ref err) => Some(err), ConfigError::IoPath(ref err, _) => Some(err), ConfigError::Serde(ref err) => Some(err), } } } impl From for ConfigError { fn from(err: io::Error) -> ConfigError { ConfigError::Io(err) } } impl From for ConfigError { fn from(err: serde_yaml::Error) -> ConfigError { ConfigError::Serde(err) } } // The unit test location. #[cfg(test)] mod config_test { use super::*; #[test] fn clean_option_equals_default_config() { let no_file = PathBuf::from(r"../fixtures/config/config.yamlll"); let cli_config = ConfigCli::Config { path: Some(no_file), clean: true, }; let config = Config::from_cli_config(Some(cli_config)).unwrap(); let default = Config::default(); assert_eq!(config, default); } #[test] fn no_config_option_file_equals_default_config() { let config = Config::from_cli_config(None).unwrap(); let default = Config::default(); assert_eq!(config, default); } }