2019-09-09 15:59:44 +00:00
extern crate dirs ;
2019-09-10 20:53:45 +00:00
use std ::path ::{ Path , PathBuf } ;
2019-09-13 09:55:42 +00:00
use std ::{ fs } ;
2019-09-09 15:59:44 +00:00
use crate ::matcher ::Match ;
use std ::fs ::{ File , create_dir_all } ;
use std ::io ::Read ;
use serde ::{ Serialize , Deserialize } ;
2019-09-12 20:14:41 +00:00
use crate ::event ::KeyModifier ;
2019-09-09 15:59:44 +00:00
use std ::collections ::HashSet ;
2019-09-14 19:38:47 +00:00
use log ::{ error } ;
2019-09-10 20:53:45 +00:00
use std ::fmt ;
use std ::error ::Error ;
2019-09-09 15:59:44 +00:00
pub ( crate ) mod runtime ;
// TODO: add documentation link
const DEFAULT_CONFIG_FILE_CONTENT : & str = include_str! ( " ../res/config.yaml " ) ;
const DEFAULT_CONFIG_FILE_NAME : & str = " default.yaml " ;
// Default values for primitives
fn default_name ( ) -> String { " default " . to_owned ( ) }
fn default_filter_title ( ) -> String { " " . to_owned ( ) }
fn default_filter_class ( ) -> String { " " . to_owned ( ) }
fn default_filter_exec ( ) -> String { " " . to_owned ( ) }
2019-09-14 08:30:51 +00:00
fn default_disabled ( ) -> bool { false }
fn default_log_level ( ) -> i32 { 0 }
2019-09-09 15:59:44 +00:00
fn default_config_caching_interval ( ) -> i32 { 800 }
fn default_toggle_interval ( ) -> u32 { 230 }
fn default_backspace_limit ( ) -> i32 { 3 }
fn default_matches ( ) -> Vec < Match > { Vec ::new ( ) }
#[ derive(Clone, Debug, Serialize, Deserialize) ]
pub struct Configs {
#[ serde(default = " default_name " ) ]
pub name : String ,
#[ serde(default = " default_filter_title " ) ]
pub filter_title : String ,
#[ serde(default = " default_filter_class " ) ]
pub filter_class : String ,
#[ serde(default = " default_filter_exec " ) ]
pub filter_exec : String ,
2019-09-14 08:30:51 +00:00
#[ serde(default = " default_disabled " ) ]
2019-09-09 15:59:44 +00:00
pub disabled : bool ,
2019-09-14 08:30:51 +00:00
#[ serde(default = " default_log_level " ) ]
pub log_level : i32 ,
2019-09-09 15:59:44 +00:00
#[ serde(default = " default_config_caching_interval " ) ]
pub config_caching_interval : i32 ,
#[ serde(default) ]
pub toggle_key : KeyModifier ,
#[ serde(default = " default_toggle_interval " ) ]
pub toggle_interval : u32 ,
#[ serde(default = " default_backspace_limit " ) ]
pub backspace_limit : i32 ,
#[ serde(default) ]
pub backend : BackendType ,
#[ serde(default = " default_matches " ) ]
pub matches : Vec < Match >
}
2019-09-09 16:56:55 +00:00
// Macro used to validate config fields
#[ macro_export ]
macro_rules ! validate_field {
( $result :expr , $field :expr , $def_value :expr ) = > {
if $field ! = $def_value {
2019-09-10 20:53:45 +00:00
let mut field_name = stringify! ( $field ) ;
if field_name . starts_with ( " self. " ) {
field_name = & field_name [ 5 .. ] ; // Remove the 'self.' prefix
}
2019-09-09 16:56:55 +00:00
error! ( " Validation error, parameter '{}' is reserved and can be only used in the default.yaml config file " , field_name ) ;
$result = false ;
}
} ;
}
impl Configs {
/*
* Validate the Config instance .
* It makes sure that app - specific config instances do not define
* attributes reserved to the default config .
* /
2019-09-10 20:53:45 +00:00
fn validate_specific_config ( & self ) -> bool {
2019-09-09 16:56:55 +00:00
let mut result = true ;
validate_field! ( result , self . config_caching_interval , default_config_caching_interval ( ) ) ;
2019-09-14 08:30:51 +00:00
validate_field! ( result , self . log_level , default_log_level ( ) ) ;
2019-09-09 16:56:55 +00:00
validate_field! ( result , self . toggle_key , KeyModifier ::default ( ) ) ;
validate_field! ( result , self . toggle_interval , default_toggle_interval ( ) ) ;
validate_field! ( result , self . backspace_limit , default_backspace_limit ( ) ) ;
result
}
}
2019-09-09 15:59:44 +00:00
#[ derive(Debug, Clone, PartialEq, Serialize, Deserialize) ]
pub enum BackendType {
Inject ,
Clipboard
}
impl Default for BackendType {
fn default ( ) -> Self {
BackendType ::Inject
}
}
impl Configs {
2019-09-10 20:53:45 +00:00
fn load_config ( path : & Path ) -> Result < Configs , ConfigLoadError > {
2019-09-09 15:59:44 +00:00
let file_res = File ::open ( path ) ;
if let Ok ( mut file ) = file_res {
let mut contents = String ::new ( ) ;
2019-09-10 20:53:45 +00:00
let res = file . read_to_string ( & mut contents ) ;
if let Err ( _ ) = res {
return Err ( ConfigLoadError ::UnableToReadFile )
}
2019-09-09 15:59:44 +00:00
2019-09-09 16:56:55 +00:00
let config_res = serde_yaml ::from_str ( & contents ) ;
match config_res {
2019-09-10 20:53:45 +00:00
Ok ( config ) = > Ok ( config ) ,
2019-09-09 16:56:55 +00:00
Err ( e ) = > {
2019-09-10 20:53:45 +00:00
Err ( ConfigLoadError ::InvalidYAML ( path . to_owned ( ) , e . to_string ( ) ) )
2019-09-09 16:56:55 +00:00
}
}
2019-09-09 15:59:44 +00:00
} else {
2019-09-10 20:53:45 +00:00
Err ( ConfigLoadError ::FileNotFound )
2019-09-09 15:59:44 +00:00
}
}
}
#[ derive(Clone, Debug, Serialize, Deserialize) ]
pub struct ConfigSet {
2019-09-14 08:30:51 +00:00
pub default : Configs ,
pub specific : Vec < Configs > ,
2019-09-09 15:59:44 +00:00
}
2019-09-10 20:53:45 +00:00
impl ConfigSet {
pub fn load ( dir_path : & Path ) -> Result < ConfigSet , ConfigLoadError > {
if ! dir_path . is_dir ( ) {
return Err ( ConfigLoadError ::InvalidConfigDirectory )
}
2019-09-09 15:59:44 +00:00
2019-09-10 20:53:45 +00:00
let default_file = dir_path . join ( DEFAULT_CONFIG_FILE_NAME ) ;
let default = Configs ::load_config ( default_file . as_path ( ) ) ? ;
2019-09-09 15:59:44 +00:00
2019-09-10 20:53:45 +00:00
let mut specific = Vec ::new ( ) ;
2019-09-09 15:59:44 +00:00
2019-09-10 20:53:45 +00:00
// Used to make sure no duplicates are present
let mut name_set = HashSet ::new ( ) ;
2019-09-09 15:59:44 +00:00
2019-09-10 20:53:45 +00:00
let dir_entry = fs ::read_dir ( dir_path ) ;
if dir_entry . is_err ( ) {
return Err ( ConfigLoadError ::UnableToReadFile )
}
let dir_entry = dir_entry . unwrap ( ) ;
2019-09-09 15:59:44 +00:00
2019-09-10 20:53:45 +00:00
for entry in dir_entry {
let entry = entry ;
if let Ok ( entry ) = entry {
let path = entry . path ( ) ;
2019-09-09 15:59:44 +00:00
2019-09-10 20:53:45 +00:00
// Skip the default one, already loaded
if path . file_name ( ) . unwrap_or ( " " . as_ref ( ) ) = = " default.yaml " {
continue ;
}
2019-09-09 15:59:44 +00:00
2019-09-10 20:53:45 +00:00
let config = Configs ::load_config ( path . as_path ( ) ) ? ;
2019-09-09 15:59:44 +00:00
2019-09-10 20:53:45 +00:00
if ! config . validate_specific_config ( ) {
return Err ( ConfigLoadError ::InvalidParameter ( path . to_owned ( ) ) )
}
2019-09-09 16:56:55 +00:00
2019-09-10 20:53:45 +00:00
if config . name = = " default " {
return Err ( ConfigLoadError ::MissingName ( path . to_owned ( ) ) ) ;
}
2019-09-09 15:59:44 +00:00
2019-09-10 20:53:45 +00:00
if name_set . contains ( & config . name ) {
return Err ( ConfigLoadError ::NameDuplicate ( path . to_owned ( ) ) ) ;
}
2019-09-09 15:59:44 +00:00
2019-09-10 21:32:43 +00:00
// TODO: check if it contains at least a filter, and warn the user about the problem
2019-09-10 20:53:45 +00:00
name_set . insert ( config . name . clone ( ) ) ;
specific . push ( config ) ;
}
2019-09-09 15:59:44 +00:00
}
2019-09-10 20:53:45 +00:00
Ok ( ConfigSet {
default ,
specific
} )
2019-09-09 15:59:44 +00:00
}
2019-09-10 20:53:45 +00:00
pub fn load_default ( ) -> Result < ConfigSet , ConfigLoadError > {
2019-09-09 15:59:44 +00:00
let res = dirs ::home_dir ( ) ;
if let Some ( home_dir ) = res {
let espanso_dir = home_dir . join ( " .espanso " ) ;
// Create the espanso dir if id doesn't exist
let res = create_dir_all ( espanso_dir . as_path ( ) ) ;
if let Ok ( _ ) = res {
let default_file = espanso_dir . join ( DEFAULT_CONFIG_FILE_NAME ) ;
// If config file does not exist, create one from template
if ! default_file . exists ( ) {
2019-09-10 20:53:45 +00:00
let result = fs ::write ( & default_file , DEFAULT_CONFIG_FILE_CONTENT ) ;
if result . is_err ( ) {
return Err ( ConfigLoadError ::UnableToCreateDefaultConfig )
}
2019-09-09 15:59:44 +00:00
}
return ConfigSet ::load ( espanso_dir . as_path ( ) )
}
}
2019-09-10 20:53:45 +00:00
return Err ( ConfigLoadError ::UnableToCreateDefaultConfig )
2019-09-09 15:59:44 +00:00
}
}
pub trait ConfigManager < ' a > {
fn active_config ( & ' a self ) -> & ' a Configs ;
fn default_config ( & ' a self ) -> & ' a Configs ;
fn matches ( & ' a self ) -> & ' a Vec < Match > ;
2019-09-10 20:53:45 +00:00
}
// Error handling
#[ derive(Debug, PartialEq) ]
pub enum ConfigLoadError {
FileNotFound ,
UnableToReadFile ,
InvalidYAML ( PathBuf , String ) ,
InvalidConfigDirectory ,
InvalidParameter ( PathBuf ) ,
MissingName ( PathBuf ) ,
NameDuplicate ( PathBuf ) ,
UnableToCreateDefaultConfig ,
}
impl fmt ::Display for ConfigLoadError {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> fmt ::Result {
match self {
ConfigLoadError ::FileNotFound = > write! ( f , " File not found " ) ,
ConfigLoadError ::UnableToReadFile = > write! ( f , " Unable to read config file " ) ,
ConfigLoadError ::InvalidYAML ( path , e ) = > write! ( f , " Error parsing YAML file '{}', invalid syntax: {} " , path . to_str ( ) . unwrap_or_default ( ) , e ) ,
ConfigLoadError ::InvalidConfigDirectory = > write! ( f , " Invalid config directory " ) ,
ConfigLoadError ::InvalidParameter ( path ) = > write! ( f , " Invalid parameter in '{}', use of reserved parameters in app-specific configs is not permitted " , path . to_str ( ) . unwrap_or_default ( ) ) ,
ConfigLoadError ::MissingName ( path ) = > write! ( f , " The 'name' field is required in app-specific configurations, but it's missing in '{}' " , path . to_str ( ) . unwrap_or_default ( ) ) ,
ConfigLoadError ::NameDuplicate ( path ) = > write! ( f , " Found duplicate 'name' in '{}', please use different names " , path . to_str ( ) . unwrap_or_default ( ) ) ,
ConfigLoadError ::UnableToCreateDefaultConfig = > write! ( f , " Could not generate default config file " ) ,
}
}
}
impl Error for ConfigLoadError {
fn description ( & self ) -> & str {
match self {
ConfigLoadError ::FileNotFound = > " File not found " ,
ConfigLoadError ::UnableToReadFile = > " Unable to read config file " ,
ConfigLoadError ::InvalidYAML ( _ , _ ) = > " Error parsing YAML file, invalid syntax " ,
ConfigLoadError ::InvalidConfigDirectory = > " Invalid config directory " ,
ConfigLoadError ::InvalidParameter ( _ ) = > " Invalid parameter, use of reserved parameters in app-specific configs is not permitted " ,
ConfigLoadError ::MissingName ( _ ) = > " The 'name' field is required in app-specific configurations, but it's missing " ,
ConfigLoadError ::NameDuplicate ( _ ) = > " Found duplicate 'name' in some configurations, please use different names " ,
ConfigLoadError ::UnableToCreateDefaultConfig = > " Could not generate default config file " ,
}
}
}
#[ cfg(test) ]
mod tests {
use super ::* ;
use std ::io ::Write ;
use tempfile ::{ NamedTempFile , TempDir } ;
2019-09-13 09:50:39 +00:00
use std ::any ::Any ;
2019-09-10 20:53:45 +00:00
const TEST_WORKING_CONFIG_FILE : & str = include_str! ( " ../res/test/working_config.yaml " ) ;
const TEST_CONFIG_FILE_WITH_BAD_YAML : & str = include_str! ( " ../res/test/config_with_bad_yaml.yaml " ) ;
// Test Configs
fn create_tmp_file ( string : & str ) -> NamedTempFile {
let file = NamedTempFile ::new ( ) . unwrap ( ) ;
file . as_file ( ) . write_all ( string . as_bytes ( ) ) ;
file
}
2019-09-13 09:50:39 +00:00
fn variant_eq < T > ( a : & T , b : & T ) -> bool {
std ::mem ::discriminant ( a ) = = std ::mem ::discriminant ( b )
}
2019-09-10 20:53:45 +00:00
#[ test ]
fn test_config_file_not_found ( ) {
let config = Configs ::load_config ( Path ::new ( " invalid/path " ) ) ;
assert_eq! ( config . is_err ( ) , true ) ;
assert_eq! ( config . unwrap_err ( ) , ConfigLoadError ::FileNotFound ) ;
}
#[ test ]
fn test_config_file_with_bad_yaml_syntax ( ) {
let broken_config_file = create_tmp_file ( TEST_CONFIG_FILE_WITH_BAD_YAML ) ;
let config = Configs ::load_config ( broken_config_file . path ( ) ) ;
match config {
Ok ( _ ) = > { assert! ( false ) } ,
Err ( e ) = > {
match e {
ConfigLoadError ::InvalidYAML ( p , _ ) = > assert_eq! ( p , broken_config_file . path ( ) . to_owned ( ) ) ,
_ = > assert! ( false ) ,
}
assert! ( true ) ;
} ,
}
}
#[ test ]
fn test_validate_field_macro ( ) {
let mut result = true ;
validate_field! ( result , 3 , 3 ) ;
assert_eq! ( result , true ) ;
validate_field! ( result , 10 , 3 ) ;
assert_eq! ( result , false ) ;
validate_field! ( result , 3 , 3 ) ;
assert_eq! ( result , false ) ;
}
#[ test ]
fn test_specific_config_does_not_have_reserved_fields ( ) {
let working_config_file = create_tmp_file ( r ###"
backend : Clipboard
" ###);
let config = Configs ::load_config ( working_config_file . path ( ) ) ;
assert_eq! ( config . unwrap ( ) . validate_specific_config ( ) , true ) ;
}
#[ test ]
fn test_specific_config_has_reserved_fields_config_caching_interval ( ) {
let working_config_file = create_tmp_file ( r ###"
# This should not happen in an app - specific config
config_caching_interval : 100
" ###);
let config = Configs ::load_config ( working_config_file . path ( ) ) ;
assert_eq! ( config . unwrap ( ) . validate_specific_config ( ) , false ) ;
}
#[ test ]
fn test_specific_config_has_reserved_fields_toggle_key ( ) {
let working_config_file = create_tmp_file ( r ###"
# This should not happen in an app - specific config
toggle_key : CTRL
" ###);
let config = Configs ::load_config ( working_config_file . path ( ) ) ;
assert_eq! ( config . unwrap ( ) . validate_specific_config ( ) , false ) ;
}
#[ test ]
fn test_specific_config_has_reserved_fields_toggle_interval ( ) {
let working_config_file = create_tmp_file ( r ###"
# This should not happen in an app - specific config
toggle_interval : 1000
" ###);
let config = Configs ::load_config ( working_config_file . path ( ) ) ;
assert_eq! ( config . unwrap ( ) . validate_specific_config ( ) , false ) ;
}
#[ test ]
fn test_specific_config_has_reserved_fields_backspace_limit ( ) {
let working_config_file = create_tmp_file ( r ###"
# This should not happen in an app - specific config
backspace_limit : 10
" ###);
let config = Configs ::load_config ( working_config_file . path ( ) ) ;
assert_eq! ( config . unwrap ( ) . validate_specific_config ( ) , false ) ;
}
#[ test ]
fn test_config_loaded_correctly ( ) {
let working_config_file = create_tmp_file ( TEST_WORKING_CONFIG_FILE ) ;
let config = Configs ::load_config ( working_config_file . path ( ) ) ;
assert_eq! ( config . is_ok ( ) , true ) ;
}
// Test ConfigSet
#[ test ]
fn test_config_set_default_content_should_work_correctly ( ) {
let tmp_dir = TempDir ::new ( ) . expect ( " unable to create temp directory " ) ;
let default_path = tmp_dir . path ( ) . join ( DEFAULT_CONFIG_FILE_NAME ) ;
fs ::write ( default_path , DEFAULT_CONFIG_FILE_CONTENT ) ;
let config_set = ConfigSet ::load ( tmp_dir . path ( ) ) ;
assert! ( config_set . is_ok ( ) ) ;
}
#[ test ]
fn test_config_set_load_fail_bad_directory ( ) {
let config_set = ConfigSet ::load ( Path ::new ( " invalid/path " ) ) ;
assert_eq! ( config_set . is_err ( ) , true ) ;
assert_eq! ( config_set . unwrap_err ( ) , ConfigLoadError ::InvalidConfigDirectory ) ;
}
#[ test ]
fn test_config_set_missing_default_file ( ) {
let tmp_dir = TempDir ::new ( ) . expect ( " unable to create temp directory " ) ;
let config_set = ConfigSet ::load ( tmp_dir . path ( ) ) ;
assert_eq! ( config_set . is_err ( ) , true ) ;
assert_eq! ( config_set . unwrap_err ( ) , ConfigLoadError ::FileNotFound ) ;
}
#[ test ]
fn test_config_set_invalid_yaml_syntax ( ) {
let tmp_dir = TempDir ::new ( ) . expect ( " unable to create temp directory " ) ;
let default_path = tmp_dir . path ( ) . join ( DEFAULT_CONFIG_FILE_NAME ) ;
let default_path_copy = default_path . clone ( ) ;
fs ::write ( default_path , TEST_CONFIG_FILE_WITH_BAD_YAML ) ;
let config_set = ConfigSet ::load ( tmp_dir . path ( ) ) ;
match config_set {
Ok ( _ ) = > { assert! ( false ) } ,
Err ( e ) = > {
match e {
ConfigLoadError ::InvalidYAML ( p , _ ) = > assert_eq! ( p , default_path_copy ) ,
_ = > assert! ( false ) ,
}
assert! ( true ) ;
} ,
}
}
#[ test ]
fn test_config_set_specific_file_with_reserved_fields ( ) {
let tmp_dir = TempDir ::new ( ) . expect ( " unable to create temp directory " ) ;
let default_path = tmp_dir . path ( ) . join ( DEFAULT_CONFIG_FILE_NAME ) ;
fs ::write ( default_path , DEFAULT_CONFIG_FILE_CONTENT ) ;
let specific_path = tmp_dir . path ( ) . join ( " specific.yaml " ) ;
let specific_path_copy = specific_path . clone ( ) ;
fs ::write ( specific_path , r ###"
config_caching_interval : 10000
" ###);
let config_set = ConfigSet ::load ( tmp_dir . path ( ) ) ;
assert! ( config_set . is_err ( ) ) ;
assert_eq! ( config_set . unwrap_err ( ) , ConfigLoadError ::InvalidParameter ( specific_path_copy ) )
}
#[ test ]
fn test_config_set_specific_file_missing_name ( ) {
let tmp_dir = TempDir ::new ( ) . expect ( " unable to create temp directory " ) ;
let default_path = tmp_dir . path ( ) . join ( DEFAULT_CONFIG_FILE_NAME ) ;
fs ::write ( default_path , DEFAULT_CONFIG_FILE_CONTENT ) ;
let specific_path = tmp_dir . path ( ) . join ( " specific.yaml " ) ;
let specific_path_copy = specific_path . clone ( ) ;
fs ::write ( specific_path , r ###"
backend : Clipboard
" ###);
let config_set = ConfigSet ::load ( tmp_dir . path ( ) ) ;
assert! ( config_set . is_err ( ) ) ;
assert_eq! ( config_set . unwrap_err ( ) , ConfigLoadError ::MissingName ( specific_path_copy ) )
}
2019-09-10 21:32:43 +00:00
pub fn create_temp_espanso_directory ( ) -> TempDir {
2019-09-10 20:53:45 +00:00
let tmp_dir = TempDir ::new ( ) . expect ( " unable to create temp directory " ) ;
let default_path = tmp_dir . path ( ) . join ( DEFAULT_CONFIG_FILE_NAME ) ;
fs ::write ( default_path , DEFAULT_CONFIG_FILE_CONTENT ) ;
2019-09-10 21:32:43 +00:00
tmp_dir
}
pub fn create_temp_file_in_dir ( tmp_dir : & TempDir , name : & str , content : & str ) -> PathBuf {
let specific_path = tmp_dir . path ( ) . join ( name ) ;
2019-09-10 20:53:45 +00:00
let specific_path_copy = specific_path . clone ( ) ;
2019-09-10 21:32:43 +00:00
fs ::write ( specific_path , content ) ;
specific_path_copy
}
#[ test ]
fn test_config_set_specific_file_duplicate_name ( ) {
let tmp_dir = create_temp_espanso_directory ( ) ;
let specific_path = create_temp_file_in_dir ( & tmp_dir , " specific.yaml " , r ###"
2019-09-10 20:53:45 +00:00
name : specific1
" ###);
2019-09-10 21:32:43 +00:00
let specific_path2 = create_temp_file_in_dir ( & tmp_dir , " specific2.yaml " , r ###"
2019-09-10 20:53:45 +00:00
name : specific1
" ###);
let config_set = ConfigSet ::load ( tmp_dir . path ( ) ) ;
assert! ( config_set . is_err ( ) ) ;
2019-09-13 09:50:39 +00:00
assert! ( variant_eq ( & config_set . unwrap_err ( ) , & ConfigLoadError ::NameDuplicate ( PathBuf ::new ( ) ) ) )
2019-09-10 20:53:45 +00:00
}
2019-09-09 15:59:44 +00:00
}