Merge pull request #51 from federico-terzi/package
Package manager functionality
This commit is contained in:
commit
d8f433e83c
1100
Cargo.lock
generated
1100
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.1.2"
|
version = "0.2.0"
|
||||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
description = "Cross-platform Text Expander written in Rust"
|
description = "Cross-platform Text Expander written in Rust"
|
||||||
|
@ -25,12 +25,13 @@ log-panics = {version = "2.0.0", features = ["with-backtrace"]}
|
||||||
backtrace = "0.3.37"
|
backtrace = "0.3.37"
|
||||||
chrono = "0.4.9"
|
chrono = "0.4.9"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
walkdir = "2.2.9"
|
||||||
|
reqwest = "0.9.20"
|
||||||
|
git2 = {version = "0.10.1", features = ["https"]}
|
||||||
|
tempfile = "3.1.0"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2.62"
|
libc = "0.2.62"
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3.1.0"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cmake = "0.1.31"
|
cmake = "0.1.31"
|
|
@ -26,20 +26,24 @@ use std::fs::{File, create_dir_all};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use crate::event::KeyModifier;
|
use crate::event::KeyModifier;
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashSet, HashMap};
|
||||||
use log::{error};
|
use log::{error};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
pub(crate) mod runtime;
|
pub(crate) mod runtime;
|
||||||
|
|
||||||
// TODO: add documentation link
|
// TODO: add documentation link
|
||||||
const DEFAULT_CONFIG_FILE_CONTENT : &str = include_str!("../res/config.yaml");
|
const DEFAULT_CONFIG_FILE_CONTENT : &str = include_str!("../res/config.yml");
|
||||||
|
|
||||||
const DEFAULT_CONFIG_FILE_NAME : &str = "default.yaml";
|
const DEFAULT_CONFIG_FILE_NAME : &str = "default.yml";
|
||||||
|
const USER_CONFIGS_FOLDER_NAME: &str = "user";
|
||||||
|
const PACKAGES_FOLDER_NAME : &str = "packages";
|
||||||
|
|
||||||
// Default values for primitives
|
// Default values for primitives
|
||||||
fn default_name() -> String{ "default".to_owned() }
|
fn default_name() -> String{ "default".to_owned() }
|
||||||
|
fn default_parent() -> String{ "self".to_owned() }
|
||||||
fn default_filter_title() -> String{ "".to_owned() }
|
fn default_filter_title() -> String{ "".to_owned() }
|
||||||
fn default_filter_class() -> String{ "".to_owned() }
|
fn default_filter_class() -> String{ "".to_owned() }
|
||||||
fn default_filter_exec() -> String{ "".to_owned() }
|
fn default_filter_exec() -> String{ "".to_owned() }
|
||||||
|
@ -50,7 +54,7 @@ fn default_use_system_agent() -> bool { true }
|
||||||
fn default_config_caching_interval() -> i32 { 800 }
|
fn default_config_caching_interval() -> i32 { 800 }
|
||||||
fn default_toggle_interval() -> u32 { 230 }
|
fn default_toggle_interval() -> u32 { 230 }
|
||||||
fn default_backspace_limit() -> i32 { 3 }
|
fn default_backspace_limit() -> i32 { 3 }
|
||||||
fn default_exclude_parent_matches() -> bool {false}
|
fn default_exclude_default_matches() -> bool {false}
|
||||||
fn default_matches() -> Vec<Match> { Vec::new() }
|
fn default_matches() -> Vec<Match> { Vec::new() }
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
@ -58,6 +62,9 @@ pub struct Configs {
|
||||||
#[serde(default = "default_name")]
|
#[serde(default = "default_name")]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde(default = "default_parent")]
|
||||||
|
pub parent: String,
|
||||||
|
|
||||||
#[serde(default = "default_filter_title")]
|
#[serde(default = "default_filter_title")]
|
||||||
pub filter_title: String,
|
pub filter_title: String,
|
||||||
|
|
||||||
|
@ -94,8 +101,8 @@ pub struct Configs {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub backend: BackendType,
|
pub backend: BackendType,
|
||||||
|
|
||||||
#[serde(default = "default_exclude_parent_matches")]
|
#[serde(default = "default_exclude_default_matches")]
|
||||||
pub exclude_parent_matches: bool,
|
pub exclude_default_matches: bool,
|
||||||
|
|
||||||
#[serde(default = "default_matches")]
|
#[serde(default = "default_matches")]
|
||||||
pub matches: Vec<Match>
|
pub matches: Vec<Match>
|
||||||
|
@ -110,7 +117,7 @@ macro_rules! validate_field {
|
||||||
if field_name.starts_with("self.") {
|
if field_name.starts_with("self.") {
|
||||||
field_name = &field_name[5..]; // Remove the 'self.' prefix
|
field_name = &field_name[5..]; // Remove the 'self.' prefix
|
||||||
}
|
}
|
||||||
error!("Validation error, parameter '{}' is reserved and can be only used in the default.yaml config file", field_name);
|
error!("Validation error, parameter '{}' is reserved and can be only used in the default.yml config file", field_name);
|
||||||
$result = false;
|
$result = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -119,10 +126,10 @@ macro_rules! validate_field {
|
||||||
impl Configs {
|
impl Configs {
|
||||||
/*
|
/*
|
||||||
* Validate the Config instance.
|
* Validate the Config instance.
|
||||||
* It makes sure that app-specific config instances do not define
|
* It makes sure that user defined config instances do not define
|
||||||
* attributes reserved to the default config.
|
* attributes reserved to the default config.
|
||||||
*/
|
*/
|
||||||
fn validate_specific_config(&self) -> bool {
|
fn validate_user_defined_config(&self) -> bool {
|
||||||
let mut result = true;
|
let mut result = true;
|
||||||
|
|
||||||
validate_field!(result, self.config_caching_interval, default_config_caching_interval());
|
validate_field!(result, self.config_caching_interval, default_config_caching_interval());
|
||||||
|
@ -171,6 +178,32 @@ impl Configs {
|
||||||
Err(ConfigLoadError::FileNotFound)
|
Err(ConfigLoadError::FileNotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn merge_config(&mut self, new_config: Configs) {
|
||||||
|
let mut merged_matches = new_config.matches;
|
||||||
|
let mut trigger_set = HashSet::new();
|
||||||
|
merged_matches.iter().for_each(|m| {
|
||||||
|
trigger_set.insert(m.trigger.clone());
|
||||||
|
});
|
||||||
|
let parent_matches : Vec<Match> = self.matches.iter().filter(|&m| {
|
||||||
|
!trigger_set.contains(&m.trigger)
|
||||||
|
}).map(|m| m.clone()).collect();
|
||||||
|
|
||||||
|
merged_matches.extend(parent_matches);
|
||||||
|
self.matches = merged_matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_default(&mut self, default: &Configs) {
|
||||||
|
let mut trigger_set = HashSet::new();
|
||||||
|
self.matches.iter().for_each(|m| {
|
||||||
|
trigger_set.insert(m.trigger.clone());
|
||||||
|
});
|
||||||
|
let default_matches : Vec<Match> = default.matches.iter().filter(|&m| {
|
||||||
|
!trigger_set.contains(&m.trigger)
|
||||||
|
}).map(|m| m.clone()).collect();
|
||||||
|
|
||||||
|
self.matches.extend(default_matches);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
@ -185,70 +218,86 @@ impl ConfigSet {
|
||||||
return Err(ConfigLoadError::InvalidConfigDirectory)
|
return Err(ConfigLoadError::InvalidConfigDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load default configuration
|
||||||
let default_file = dir_path.join(DEFAULT_CONFIG_FILE_NAME);
|
let default_file = dir_path.join(DEFAULT_CONFIG_FILE_NAME);
|
||||||
let default = Configs::load_config(default_file.as_path())?;
|
let default = Configs::load_config(default_file.as_path())?;
|
||||||
|
|
||||||
let mut specific = Vec::new();
|
// Analyze which config files has to be loaded
|
||||||
|
|
||||||
// Used to make sure no duplicates are present
|
let mut target_files = Vec::new();
|
||||||
let mut name_set = HashSet::new();
|
|
||||||
|
|
||||||
let dir_entry = fs::read_dir(dir_path);
|
let specific_dir = dir_path.join(USER_CONFIGS_FOLDER_NAME);
|
||||||
if dir_entry.is_err() {
|
if specific_dir.exists() {
|
||||||
return Err(ConfigLoadError::UnableToReadFile)
|
let dir_entry = WalkDir::new(specific_dir);
|
||||||
|
target_files.extend(dir_entry);
|
||||||
}
|
}
|
||||||
let dir_entry = dir_entry.unwrap();
|
|
||||||
|
|
||||||
for entry in dir_entry {
|
let package_dir = dir_path.join(PACKAGES_FOLDER_NAME);
|
||||||
let entry = entry;
|
if package_dir.exists() {
|
||||||
|
let dir_entry = WalkDir::new(package_dir);
|
||||||
|
target_files.extend(dir_entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the user defined config files
|
||||||
|
|
||||||
|
let mut name_set = HashSet::new();
|
||||||
|
let mut children_map: HashMap<String, Vec<Configs>> = HashMap::new();
|
||||||
|
let mut root_configs = Vec::new();
|
||||||
|
root_configs.push(default);
|
||||||
|
|
||||||
|
for entry in target_files {
|
||||||
if let Ok(entry) = entry {
|
if let Ok(entry) = entry {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
// Skip the default one, already loaded
|
|
||||||
if path.file_name().unwrap_or("".as_ref()) == "default.yaml" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip non-yaml config files
|
// Skip non-yaml config files
|
||||||
if path.extension().unwrap_or_default().to_str().unwrap_or_default() != "yaml" {
|
if path.extension().unwrap_or_default().to_str().unwrap_or_default() != "yml" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut config = Configs::load_config(path.as_path())?;
|
let mut config = Configs::load_config(&path)?;
|
||||||
|
|
||||||
if !config.validate_specific_config() {
|
// Make sure the config does not contain reserved fields
|
||||||
|
if !config.validate_user_defined_config() {
|
||||||
return Err(ConfigLoadError::InvalidParameter(path.to_owned()))
|
return Err(ConfigLoadError::InvalidParameter(path.to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No name specified, defaulting to the path name
|
||||||
if config.name == "default" {
|
if config.name == "default" {
|
||||||
return Err(ConfigLoadError::MissingName(path.to_owned()));
|
config.name = path.to_str().unwrap_or_default().to_owned();
|
||||||
}
|
}
|
||||||
|
|
||||||
if name_set.contains(&config.name) {
|
if name_set.contains(&config.name) {
|
||||||
return Err(ConfigLoadError::NameDuplicate(path.to_owned()));
|
return Err(ConfigLoadError::NameDuplicate(path.to_owned()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute new match set, merging the parent's matches.
|
|
||||||
// Note: if an app-specific redefines a trigger already present in the
|
|
||||||
// default config, the latter gets overwritten.
|
|
||||||
if !config.exclude_parent_matches {
|
|
||||||
let mut merged_matches = config.matches.clone();
|
|
||||||
let mut trigger_set = HashSet::new();
|
|
||||||
merged_matches.iter().for_each(|m| {
|
|
||||||
trigger_set.insert(m.trigger.clone());
|
|
||||||
});
|
|
||||||
let parent_matches : Vec<Match> = default.matches.iter().filter(|&m| {
|
|
||||||
!trigger_set.contains(&m.trigger)
|
|
||||||
}).map(|m| m.clone()).collect();
|
|
||||||
|
|
||||||
merged_matches.extend(parent_matches);
|
|
||||||
config.matches = merged_matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: check if it contains at least a filter, and warn the user about the problem
|
|
||||||
|
|
||||||
name_set.insert(config.name.clone());
|
name_set.insert(config.name.clone());
|
||||||
specific.push(config);
|
|
||||||
|
if config.parent == "self" { // No parent, root config
|
||||||
|
root_configs.push(config);
|
||||||
|
}else{ // Children config
|
||||||
|
let children_vec = children_map.entry(config.parent.clone()).or_default();
|
||||||
|
children_vec.push(config);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
eprintln!("Warning: Unable to read config file: {}", entry.unwrap_err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the children config files
|
||||||
|
let mut configs = Vec::new();
|
||||||
|
for root_config in root_configs {
|
||||||
|
let config = ConfigSet::reduce_configs(root_config, &children_map);
|
||||||
|
configs.push(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate default from specific
|
||||||
|
let default= configs.get(0).unwrap().clone();
|
||||||
|
let mut specific = (&configs[1..]).to_vec().clone();
|
||||||
|
|
||||||
|
// Add default matches to specific configs when needed
|
||||||
|
for config in specific.iter_mut() {
|
||||||
|
if !config.exclude_default_matches {
|
||||||
|
config.merge_default(&default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,31 +307,70 @@ impl ConfigSet {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_default() -> Result<ConfigSet, ConfigLoadError> {
|
fn reduce_configs(target: Configs, children_map: &HashMap<String, Vec<Configs>>) -> Configs {
|
||||||
let res = dirs::home_dir();
|
if children_map.contains_key(&target.name) {
|
||||||
if let Some(home_dir) = res {
|
let mut target = target;
|
||||||
let espanso_dir = home_dir.join(".espanso");
|
for children in children_map.get(&target.name).unwrap() {
|
||||||
|
let children = Self::reduce_configs(children.clone(), children_map);
|
||||||
// Create the espanso dir if id doesn't exist
|
target.merge_config(children);
|
||||||
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() {
|
|
||||||
let result = fs::write(&default_file, DEFAULT_CONFIG_FILE_CONTENT);
|
|
||||||
if result.is_err() {
|
|
||||||
return Err(ConfigLoadError::UnableToCreateDefaultConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ConfigSet::load(espanso_dir.as_path())
|
|
||||||
}
|
}
|
||||||
|
target
|
||||||
|
}else{
|
||||||
|
target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_default() -> Result<ConfigSet, ConfigLoadError> {
|
||||||
|
let espanso_dir = ConfigSet::get_default_config_dir();
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
let result = fs::write(&default_file, DEFAULT_CONFIG_FILE_CONTENT);
|
||||||
|
if result.is_err() {
|
||||||
|
return Err(ConfigLoadError::UnableToCreateDefaultConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create auxiliary directories
|
||||||
|
|
||||||
|
let user_config_dir = espanso_dir.join(USER_CONFIGS_FOLDER_NAME);
|
||||||
|
if !user_config_dir.exists() {
|
||||||
|
let res = create_dir_all(user_config_dir.as_path());
|
||||||
|
if res.is_err() {
|
||||||
|
return Err(ConfigLoadError::UnableToCreateDefaultConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let packages_dir = espanso_dir.join(PACKAGES_FOLDER_NAME);
|
||||||
|
if !packages_dir.exists() {
|
||||||
|
let res = create_dir_all(packages_dir.as_path());
|
||||||
|
if res.is_err() {
|
||||||
|
return Err(ConfigLoadError::UnableToCreateDefaultConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConfigSet::load(espanso_dir.as_path())
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(ConfigLoadError::UnableToCreateDefaultConfig)
|
return Err(ConfigLoadError::UnableToCreateDefaultConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_default_config_dir() -> PathBuf {
|
||||||
|
let home_dir = dirs::home_dir().expect("Unable to get home directory");
|
||||||
|
let espanso_dir = home_dir.join(".espanso");
|
||||||
|
espanso_dir
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_packages_dir() -> PathBuf {
|
||||||
|
let espanso_dir = ConfigSet::get_default_config_dir();
|
||||||
|
espanso_dir.join(PACKAGES_FOLDER_NAME)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ConfigManager<'a> {
|
pub trait ConfigManager<'a> {
|
||||||
|
@ -299,7 +387,6 @@ pub enum ConfigLoadError {
|
||||||
InvalidYAML(PathBuf, String),
|
InvalidYAML(PathBuf, String),
|
||||||
InvalidConfigDirectory,
|
InvalidConfigDirectory,
|
||||||
InvalidParameter(PathBuf),
|
InvalidParameter(PathBuf),
|
||||||
MissingName(PathBuf),
|
|
||||||
NameDuplicate(PathBuf),
|
NameDuplicate(PathBuf),
|
||||||
UnableToCreateDefaultConfig,
|
UnableToCreateDefaultConfig,
|
||||||
}
|
}
|
||||||
|
@ -311,8 +398,7 @@ impl fmt::Display for ConfigLoadError {
|
||||||
ConfigLoadError::UnableToReadFile => write!(f, "Unable to read config file"),
|
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::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::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::InvalidParameter(path) => write!(f, "Invalid parameter in '{}', use of reserved parameters in used defined 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::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"),
|
ConfigLoadError::UnableToCreateDefaultConfig => write!(f, "Could not generate default config file"),
|
||||||
}
|
}
|
||||||
|
@ -326,8 +412,7 @@ impl Error for ConfigLoadError {
|
||||||
ConfigLoadError::UnableToReadFile => "Unable to read config file",
|
ConfigLoadError::UnableToReadFile => "Unable to read config file",
|
||||||
ConfigLoadError::InvalidYAML(_, _) => "Error parsing YAML file, invalid syntax",
|
ConfigLoadError::InvalidYAML(_, _) => "Error parsing YAML file, invalid syntax",
|
||||||
ConfigLoadError::InvalidConfigDirectory => "Invalid config directory",
|
ConfigLoadError::InvalidConfigDirectory => "Invalid config directory",
|
||||||
ConfigLoadError::InvalidParameter(_) => "Invalid parameter, use of reserved parameters in app-specific configs is not permitted",
|
ConfigLoadError::InvalidParameter(_) => "Invalid parameter, use of reserved parameters in user defined 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::NameDuplicate(_) => "Found duplicate 'name' in some configurations, please use different names",
|
||||||
ConfigLoadError::UnableToCreateDefaultConfig => "Could not generate default config file",
|
ConfigLoadError::UnableToCreateDefaultConfig => "Could not generate default config file",
|
||||||
}
|
}
|
||||||
|
@ -343,8 +428,8 @@ mod tests {
|
||||||
use tempfile::{NamedTempFile, TempDir};
|
use tempfile::{NamedTempFile, TempDir};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
const TEST_WORKING_CONFIG_FILE : &str = include_str!("../res/test/working_config.yaml");
|
const TEST_WORKING_CONFIG_FILE : &str = include_str!("../res/test/working_config.yml");
|
||||||
const TEST_CONFIG_FILE_WITH_BAD_YAML : &str = include_str!("../res/test/config_with_bad_yaml.yaml");
|
const TEST_CONFIG_FILE_WITH_BAD_YAML : &str = include_str!("../res/test/config_with_bad_yaml.yml");
|
||||||
|
|
||||||
// Test Configs
|
// Test Configs
|
||||||
|
|
||||||
|
@ -397,18 +482,18 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_specific_config_does_not_have_reserved_fields() {
|
fn test_user_defined_config_does_not_have_reserved_fields() {
|
||||||
let working_config_file = create_tmp_file(r###"
|
let working_config_file = create_tmp_file(r###"
|
||||||
|
|
||||||
backend: Clipboard
|
backend: Clipboard
|
||||||
|
|
||||||
"###);
|
"###);
|
||||||
let config = Configs::load_config(working_config_file.path());
|
let config = Configs::load_config(working_config_file.path());
|
||||||
assert_eq!(config.unwrap().validate_specific_config(), true);
|
assert_eq!(config.unwrap().validate_user_defined_config(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_specific_config_has_reserved_fields_config_caching_interval() {
|
fn test_user_defined_config_has_reserved_fields_config_caching_interval() {
|
||||||
let working_config_file = create_tmp_file(r###"
|
let working_config_file = create_tmp_file(r###"
|
||||||
|
|
||||||
# This should not happen in an app-specific config
|
# This should not happen in an app-specific config
|
||||||
|
@ -416,11 +501,11 @@ mod tests {
|
||||||
|
|
||||||
"###);
|
"###);
|
||||||
let config = Configs::load_config(working_config_file.path());
|
let config = Configs::load_config(working_config_file.path());
|
||||||
assert_eq!(config.unwrap().validate_specific_config(), false);
|
assert_eq!(config.unwrap().validate_user_defined_config(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_specific_config_has_reserved_fields_toggle_key() {
|
fn test_user_defined_config_has_reserved_fields_toggle_key() {
|
||||||
let working_config_file = create_tmp_file(r###"
|
let working_config_file = create_tmp_file(r###"
|
||||||
|
|
||||||
# This should not happen in an app-specific config
|
# This should not happen in an app-specific config
|
||||||
|
@ -428,11 +513,11 @@ mod tests {
|
||||||
|
|
||||||
"###);
|
"###);
|
||||||
let config = Configs::load_config(working_config_file.path());
|
let config = Configs::load_config(working_config_file.path());
|
||||||
assert_eq!(config.unwrap().validate_specific_config(), false);
|
assert_eq!(config.unwrap().validate_user_defined_config(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_specific_config_has_reserved_fields_toggle_interval() {
|
fn test_user_defined_config_has_reserved_fields_toggle_interval() {
|
||||||
let working_config_file = create_tmp_file(r###"
|
let working_config_file = create_tmp_file(r###"
|
||||||
|
|
||||||
# This should not happen in an app-specific config
|
# This should not happen in an app-specific config
|
||||||
|
@ -440,11 +525,11 @@ mod tests {
|
||||||
|
|
||||||
"###);
|
"###);
|
||||||
let config = Configs::load_config(working_config_file.path());
|
let config = Configs::load_config(working_config_file.path());
|
||||||
assert_eq!(config.unwrap().validate_specific_config(), false);
|
assert_eq!(config.unwrap().validate_user_defined_config(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_specific_config_has_reserved_fields_backspace_limit() {
|
fn test_user_defined_config_has_reserved_fields_backspace_limit() {
|
||||||
let working_config_file = create_tmp_file(r###"
|
let working_config_file = create_tmp_file(r###"
|
||||||
|
|
||||||
# This should not happen in an app-specific config
|
# This should not happen in an app-specific config
|
||||||
|
@ -452,7 +537,7 @@ mod tests {
|
||||||
|
|
||||||
"###);
|
"###);
|
||||||
let config = Configs::load_config(working_config_file.path());
|
let config = Configs::load_config(working_config_file.path());
|
||||||
assert_eq!(config.unwrap().validate_specific_config(), false);
|
assert_eq!(config.unwrap().validate_user_defined_config(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -516,59 +601,80 @@ mod tests {
|
||||||
let default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
|
let default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
|
||||||
fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT);
|
fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT);
|
||||||
|
|
||||||
let specific_path = tmp_dir.path().join("specific.yaml");
|
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###"
|
||||||
let specific_path_copy = specific_path.clone();
|
|
||||||
fs::write(specific_path, r###"
|
|
||||||
config_caching_interval: 10000
|
config_caching_interval: 10000
|
||||||
"###);
|
"###);
|
||||||
|
let user_defined_path_copy = user_defined_path.clone();
|
||||||
|
|
||||||
let config_set = ConfigSet::load(tmp_dir.path());
|
let config_set = ConfigSet::load(tmp_dir.path());
|
||||||
assert!(config_set.is_err());
|
assert!(config_set.is_err());
|
||||||
assert_eq!(config_set.unwrap_err(), ConfigLoadError::InvalidParameter(specific_path_copy))
|
assert_eq!(config_set.unwrap_err(), ConfigLoadError::InvalidParameter(user_defined_path_copy))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_set_specific_file_missing_name() {
|
fn test_config_set_specific_file_missing_name_auto_generated() {
|
||||||
let tmp_dir = TempDir::new().expect("unable to create temp directory");
|
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 = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
|
||||||
fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT);
|
fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT);
|
||||||
|
|
||||||
let specific_path = tmp_dir.path().join("specific.yaml");
|
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###"
|
||||||
let specific_path_copy = specific_path.clone();
|
|
||||||
fs::write(specific_path, r###"
|
|
||||||
backend: Clipboard
|
backend: Clipboard
|
||||||
"###);
|
"###);
|
||||||
|
let user_defined_path_copy = user_defined_path.clone();
|
||||||
|
|
||||||
let config_set = ConfigSet::load(tmp_dir.path());
|
let config_set = ConfigSet::load(tmp_dir.path());
|
||||||
assert!(config_set.is_err());
|
assert!(config_set.is_ok());
|
||||||
assert_eq!(config_set.unwrap_err(), ConfigLoadError::MissingName(specific_path_copy))
|
assert_eq!(config_set.unwrap().specific[0].name, user_defined_path_copy.to_str().unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_temp_espanso_directory() -> TempDir {
|
pub fn create_temp_espanso_directory() -> TempDir {
|
||||||
|
create_temp_espanso_directory_with_default_content(DEFAULT_CONFIG_FILE_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_temp_espanso_directory_with_default_content(default_content: &str) -> TempDir {
|
||||||
let tmp_dir = TempDir::new().expect("unable to create temp directory");
|
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 = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
|
||||||
fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT);
|
fs::write(default_path, default_content);
|
||||||
|
|
||||||
tmp_dir
|
tmp_dir
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_temp_file_in_dir(tmp_dir: &TempDir, name: &str, content: &str) -> PathBuf {
|
pub fn create_temp_file_in_dir(tmp_dir: &PathBuf, name: &str, content: &str) -> PathBuf {
|
||||||
let specific_path = tmp_dir.path().join(name);
|
let user_defined_path = tmp_dir.join(name);
|
||||||
let specific_path_copy = specific_path.clone();
|
let user_defined_path_copy = user_defined_path.clone();
|
||||||
fs::write(specific_path, content);
|
fs::write(user_defined_path, content);
|
||||||
|
|
||||||
specific_path_copy
|
user_defined_path_copy
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_user_config_file(tmp_dir: &Path, name: &str, content: &str) -> PathBuf {
|
||||||
|
let user_config_dir = tmp_dir.join(USER_CONFIGS_FOLDER_NAME);
|
||||||
|
if !user_config_dir.exists() {
|
||||||
|
create_dir_all(&user_config_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
create_temp_file_in_dir(&user_config_dir, name, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_package_file(tmp_dir: &Path, package_name: &str, filename: &str, content: &str) -> PathBuf {
|
||||||
|
let package_config_dir = tmp_dir.join(PACKAGES_FOLDER_NAME);
|
||||||
|
let package_dir = package_config_dir.join(package_name);
|
||||||
|
if !package_dir.exists() {
|
||||||
|
create_dir_all(&package_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
create_temp_file_in_dir(&package_dir, filename, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_set_specific_file_duplicate_name() {
|
fn test_config_set_specific_file_duplicate_name() {
|
||||||
let tmp_dir = create_temp_espanso_directory();
|
let tmp_dir = create_temp_espanso_directory();
|
||||||
|
|
||||||
let specific_path = create_temp_file_in_dir(&tmp_dir, "specific.yaml", r###"
|
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###"
|
||||||
name: specific1
|
name: specific1
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
let specific_path2 = create_temp_file_in_dir(&tmp_dir, "specific2.yaml", r###"
|
let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific2.yml", r###"
|
||||||
name: specific1
|
name: specific1
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
|
@ -578,7 +684,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_specific_config_set_merge_with_parent_matches() {
|
fn test_user_defined_config_set_merge_with_parent_matches() {
|
||||||
let tmp_dir = TempDir::new().expect("unable to create temp directory");
|
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 = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
|
||||||
fs::write(default_path, r###"
|
fs::write(default_path, r###"
|
||||||
|
@ -589,9 +695,7 @@ mod tests {
|
||||||
replace: "Bob"
|
replace: "Bob"
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
let specific_path = tmp_dir.path().join("specific.yaml");
|
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific1.yml", r###"
|
||||||
let specific_path_copy = specific_path.clone();
|
|
||||||
fs::write(specific_path, r###"
|
|
||||||
name: specific1
|
name: specific1
|
||||||
|
|
||||||
matches:
|
matches:
|
||||||
|
@ -609,7 +713,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_specific_config_set_merge_with_parent_matches_child_priority() {
|
fn test_user_defined_config_set_merge_with_parent_matches_child_priority() {
|
||||||
let tmp_dir = TempDir::new().expect("unable to create temp directory");
|
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 = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
|
||||||
fs::write(default_path, r###"
|
fs::write(default_path, r###"
|
||||||
|
@ -620,9 +724,7 @@ mod tests {
|
||||||
replace: "Bob"
|
replace: "Bob"
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
let specific_path = tmp_dir.path().join("specific.yaml");
|
let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific2.yml", r###"
|
||||||
let specific_path_copy = specific_path.clone();
|
|
||||||
fs::write(specific_path, r###"
|
|
||||||
name: specific1
|
name: specific1
|
||||||
|
|
||||||
matches:
|
matches:
|
||||||
|
@ -639,7 +741,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_specific_config_set_exclude_merge_with_parent_matches() {
|
fn test_user_defined_config_set_exclude_merge_with_parent_matches() {
|
||||||
let tmp_dir = TempDir::new().expect("unable to create temp directory");
|
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 = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
|
||||||
fs::write(default_path, r###"
|
fs::write(default_path, r###"
|
||||||
|
@ -650,12 +752,10 @@ mod tests {
|
||||||
replace: "Bob"
|
replace: "Bob"
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
let specific_path = tmp_dir.path().join("specific.yaml");
|
let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific2.yml", r###"
|
||||||
let specific_path_copy = specific_path.clone();
|
|
||||||
fs::write(specific_path, r###"
|
|
||||||
name: specific1
|
name: specific1
|
||||||
|
|
||||||
exclude_parent_matches: true
|
exclude_default_matches: true
|
||||||
|
|
||||||
matches:
|
matches:
|
||||||
- trigger: "hello"
|
- trigger: "hello"
|
||||||
|
@ -681,12 +781,10 @@ mod tests {
|
||||||
replace: "Bob"
|
replace: "Bob"
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
let specific_path = tmp_dir.path().join("specific.zzz");
|
let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific.zzz", r###"
|
||||||
let specific_path_copy = specific_path.clone();
|
|
||||||
fs::write(specific_path, r###"
|
|
||||||
name: specific1
|
name: specific1
|
||||||
|
|
||||||
exclude_parent_matches: true
|
exclude_default_matches: true
|
||||||
|
|
||||||
matches:
|
matches:
|
||||||
- trigger: "hello"
|
- trigger: "hello"
|
||||||
|
@ -696,4 +794,196 @@ mod tests {
|
||||||
let config_set = ConfigSet::load(tmp_dir.path()).unwrap();
|
let config_set = ConfigSet::load(tmp_dir.path()).unwrap();
|
||||||
assert_eq!(config_set.specific.len(), 0);
|
assert_eq!(config_set.specific.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_set_no_parent_configs_works_correctly() {
|
||||||
|
let tmp_dir = create_temp_espanso_directory();
|
||||||
|
|
||||||
|
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###"
|
||||||
|
name: specific1
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific2.yml", r###"
|
||||||
|
name: specific2
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let config_set = ConfigSet::load(tmp_dir.path()).unwrap();
|
||||||
|
assert_eq!(config_set.specific.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_set_default_parent_works_correctly() {
|
||||||
|
let tmp_dir = create_temp_espanso_directory_with_default_content(r###"
|
||||||
|
matches:
|
||||||
|
- trigger: hasta
|
||||||
|
replace: Hasta la vista
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###"
|
||||||
|
parent: default
|
||||||
|
|
||||||
|
matches:
|
||||||
|
- trigger: "hello"
|
||||||
|
replace: "world"
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let config_set = ConfigSet::load(tmp_dir.path()).unwrap();
|
||||||
|
assert_eq!(config_set.specific.len(), 0);
|
||||||
|
assert_eq!(config_set.default.matches.len(), 2);
|
||||||
|
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
||||||
|
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_set_no_parent_should_not_merge() {
|
||||||
|
let tmp_dir = create_temp_espanso_directory_with_default_content(r###"
|
||||||
|
matches:
|
||||||
|
- trigger: hasta
|
||||||
|
replace: Hasta la vista
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###"
|
||||||
|
matches:
|
||||||
|
- trigger: "hello"
|
||||||
|
replace: "world"
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let config_set = ConfigSet::load(tmp_dir.path()).unwrap();
|
||||||
|
assert_eq!(config_set.specific.len(), 1);
|
||||||
|
assert_eq!(config_set.default.matches.len(), 1);
|
||||||
|
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
||||||
|
assert!(!config_set.default.matches.iter().any(|m| m.trigger == "hello"));
|
||||||
|
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "hello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_set_default_nested_parent_works_correctly() {
|
||||||
|
let tmp_dir = create_temp_espanso_directory_with_default_content(r###"
|
||||||
|
matches:
|
||||||
|
- trigger: hasta
|
||||||
|
replace: Hasta la vista
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###"
|
||||||
|
name: custom1
|
||||||
|
parent: default
|
||||||
|
|
||||||
|
matches:
|
||||||
|
- trigger: "hello"
|
||||||
|
replace: "world"
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific2.yml", r###"
|
||||||
|
parent: custom1
|
||||||
|
|
||||||
|
matches:
|
||||||
|
- trigger: "super"
|
||||||
|
replace: "mario"
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let config_set = ConfigSet::load(tmp_dir.path()).unwrap();
|
||||||
|
assert_eq!(config_set.specific.len(), 0);
|
||||||
|
assert_eq!(config_set.default.matches.len(), 3);
|
||||||
|
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
||||||
|
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hello"));
|
||||||
|
assert!(config_set.default.matches.iter().any(|m| m.trigger == "super"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_set_parent_merge_children_priority_should_be_higher() {
|
||||||
|
let tmp_dir = create_temp_espanso_directory_with_default_content(r###"
|
||||||
|
matches:
|
||||||
|
- trigger: hasta
|
||||||
|
replace: Hasta la vista
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###"
|
||||||
|
parent: default
|
||||||
|
|
||||||
|
matches:
|
||||||
|
- trigger: "hasta"
|
||||||
|
replace: "world"
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let config_set = ConfigSet::load(tmp_dir.path()).unwrap();
|
||||||
|
assert_eq!(config_set.specific.len(), 0);
|
||||||
|
assert_eq!(config_set.default.matches.len(), 1);
|
||||||
|
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta" && m.replace == "world"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_set_package_configs_default_merge() {
|
||||||
|
let tmp_dir = create_temp_espanso_directory_with_default_content(r###"
|
||||||
|
matches:
|
||||||
|
- trigger: hasta
|
||||||
|
replace: Hasta la vista
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let package_path = create_package_file(tmp_dir.path(), "package1", "package.yml", r###"
|
||||||
|
parent: default
|
||||||
|
|
||||||
|
matches:
|
||||||
|
- trigger: "harry"
|
||||||
|
replace: "potter"
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let config_set = ConfigSet::load(tmp_dir.path()).unwrap();
|
||||||
|
assert_eq!(config_set.specific.len(), 0);
|
||||||
|
assert_eq!(config_set.default.matches.len(), 2);
|
||||||
|
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
||||||
|
assert!(config_set.default.matches.iter().any(|m| m.trigger == "harry"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_set_package_configs_without_merge() {
|
||||||
|
let tmp_dir = create_temp_espanso_directory_with_default_content(r###"
|
||||||
|
matches:
|
||||||
|
- trigger: hasta
|
||||||
|
replace: Hasta la vista
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let package_path = create_package_file(tmp_dir.path(), "package1", "package.yml", r###"
|
||||||
|
matches:
|
||||||
|
- trigger: "harry"
|
||||||
|
replace: "potter"
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let config_set = ConfigSet::load(tmp_dir.path()).unwrap();
|
||||||
|
assert_eq!(config_set.specific.len(), 1);
|
||||||
|
assert_eq!(config_set.default.matches.len(), 1);
|
||||||
|
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
||||||
|
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "harry"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_set_package_configs_multiple_files() {
|
||||||
|
let tmp_dir = create_temp_espanso_directory_with_default_content(r###"
|
||||||
|
matches:
|
||||||
|
- trigger: hasta
|
||||||
|
replace: Hasta la vista
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let package_path = create_package_file(tmp_dir.path(), "package1", "package.yml", r###"
|
||||||
|
name: package1
|
||||||
|
|
||||||
|
matches:
|
||||||
|
- trigger: "harry"
|
||||||
|
replace: "potter"
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let package_path2 = create_package_file(tmp_dir.path(), "package1", "addon.yml", r###"
|
||||||
|
parent: package1
|
||||||
|
|
||||||
|
matches:
|
||||||
|
- trigger: "ron"
|
||||||
|
replace: "weasley"
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let config_set = ConfigSet::load(tmp_dir.path()).unwrap();
|
||||||
|
assert_eq!(config_set.specific.len(), 1);
|
||||||
|
assert_eq!(config_set.default.matches.len(), 1);
|
||||||
|
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
||||||
|
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "harry"));
|
||||||
|
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "ron"));
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -204,13 +204,12 @@ impl <'a, S: SystemManager> super::ConfigManager<'a> for RuntimeConfigManager<'a
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::io::Write;
|
|
||||||
use tempfile::{NamedTempFile, TempDir};
|
use tempfile::{NamedTempFile, TempDir};
|
||||||
use crate::config::{DEFAULT_CONFIG_FILE_NAME, DEFAULT_CONFIG_FILE_CONTENT};
|
use crate::config::{DEFAULT_CONFIG_FILE_NAME, DEFAULT_CONFIG_FILE_CONTENT};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use crate::config::ConfigManager;
|
use crate::config::ConfigManager;
|
||||||
use crate::config::tests::{create_temp_espanso_directory, create_temp_file_in_dir};
|
use crate::config::tests::{create_temp_espanso_directory, create_temp_file_in_dir, create_user_config_file};
|
||||||
|
|
||||||
struct DummySystemManager {
|
struct DummySystemManager {
|
||||||
title: RefCell<String>,
|
title: RefCell<String>,
|
||||||
|
@ -252,18 +251,18 @@ mod tests {
|
||||||
fn test_runtime_constructor_regex_load_correctly() {
|
fn test_runtime_constructor_regex_load_correctly() {
|
||||||
let tmp_dir = create_temp_espanso_directory();
|
let tmp_dir = create_temp_espanso_directory();
|
||||||
|
|
||||||
let specific_path = create_temp_file_in_dir(&tmp_dir, "specific.yaml", r###"
|
let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###"
|
||||||
name: myname1
|
name: myname1
|
||||||
filter_exec: "Title"
|
filter_exec: "Title"
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
let specific_path2 = create_temp_file_in_dir(&tmp_dir, "specific2.yaml", r###"
|
let specific_path2 = create_user_config_file(&tmp_dir.path(), "specific2.yml", r###"
|
||||||
name: myname2
|
name: myname2
|
||||||
filter_title: "Yeah"
|
filter_title: "Yeah"
|
||||||
filter_class: "Car"
|
filter_class: "Car"
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
let specific_path3 = create_temp_file_in_dir(&tmp_dir, "specific3.yaml", r###"
|
let specific_path3 = create_user_config_file(&tmp_dir.path(), "specific3.yml", r###"
|
||||||
name: myname3
|
name: myname3
|
||||||
filter_title: "Nice"
|
filter_title: "Nice"
|
||||||
"###);
|
"###);
|
||||||
|
@ -303,18 +302,18 @@ mod tests {
|
||||||
fn test_runtime_constructor_malformed_regexes_are_ignored() {
|
fn test_runtime_constructor_malformed_regexes_are_ignored() {
|
||||||
let tmp_dir = create_temp_espanso_directory();
|
let tmp_dir = create_temp_espanso_directory();
|
||||||
|
|
||||||
let specific_path = create_temp_file_in_dir(&tmp_dir, "specific.yaml", r###"
|
let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###"
|
||||||
name: myname1
|
name: myname1
|
||||||
filter_exec: "[`-_]"
|
filter_exec: "[`-_]"
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
let specific_path2 = create_temp_file_in_dir(&tmp_dir, "specific2.yaml", r###"
|
let specific_path2 = create_user_config_file(&tmp_dir.path(), "specific2.yml", r###"
|
||||||
name: myname2
|
name: myname2
|
||||||
filter_title: "[`-_]"
|
filter_title: "[`-_]"
|
||||||
filter_class: "Car"
|
filter_class: "Car"
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
let specific_path3 = create_temp_file_in_dir(&tmp_dir, "specific3.yaml", r###"
|
let specific_path3 = create_user_config_file(&tmp_dir.path(), "specific3.yml", r###"
|
||||||
name: myname3
|
name: myname3
|
||||||
filter_title: "Nice"
|
filter_title: "Nice"
|
||||||
"###);
|
"###);
|
||||||
|
@ -354,7 +353,7 @@ mod tests {
|
||||||
fn test_runtime_calculate_active_config_specific_title_match() {
|
fn test_runtime_calculate_active_config_specific_title_match() {
|
||||||
let tmp_dir = create_temp_espanso_directory();
|
let tmp_dir = create_temp_espanso_directory();
|
||||||
|
|
||||||
let specific_path = create_temp_file_in_dir(&tmp_dir, "specific.yaml", r###"
|
let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###"
|
||||||
name: chrome
|
name: chrome
|
||||||
filter_title: "Chrome"
|
filter_title: "Chrome"
|
||||||
"###);
|
"###);
|
||||||
|
@ -372,7 +371,7 @@ mod tests {
|
||||||
fn test_runtime_calculate_active_config_specific_class_match() {
|
fn test_runtime_calculate_active_config_specific_class_match() {
|
||||||
let tmp_dir = create_temp_espanso_directory();
|
let tmp_dir = create_temp_espanso_directory();
|
||||||
|
|
||||||
let specific_path = create_temp_file_in_dir(&tmp_dir, "specific.yaml", r###"
|
let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###"
|
||||||
name: chrome
|
name: chrome
|
||||||
filter_class: "Chrome"
|
filter_class: "Chrome"
|
||||||
"###);
|
"###);
|
||||||
|
@ -390,7 +389,7 @@ mod tests {
|
||||||
fn test_runtime_calculate_active_config_specific_exec_match() {
|
fn test_runtime_calculate_active_config_specific_exec_match() {
|
||||||
let tmp_dir = create_temp_espanso_directory();
|
let tmp_dir = create_temp_espanso_directory();
|
||||||
|
|
||||||
let specific_path = create_temp_file_in_dir(&tmp_dir, "specific.yaml", r###"
|
let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###"
|
||||||
name: chrome
|
name: chrome
|
||||||
filter_exec: "chrome.exe"
|
filter_exec: "chrome.exe"
|
||||||
"###);
|
"###);
|
||||||
|
@ -408,7 +407,7 @@ mod tests {
|
||||||
fn test_runtime_calculate_active_config_specific_multi_filter_match() {
|
fn test_runtime_calculate_active_config_specific_multi_filter_match() {
|
||||||
let tmp_dir = create_temp_espanso_directory();
|
let tmp_dir = create_temp_espanso_directory();
|
||||||
|
|
||||||
let specific_path = create_temp_file_in_dir(&tmp_dir, "specific.yaml", r###"
|
let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###"
|
||||||
name: chrome
|
name: chrome
|
||||||
filter_class: Browser
|
filter_class: Browser
|
||||||
filter_exec: "firefox.exe"
|
filter_exec: "firefox.exe"
|
||||||
|
@ -428,7 +427,7 @@ mod tests {
|
||||||
fn test_runtime_calculate_active_config_no_match() {
|
fn test_runtime_calculate_active_config_no_match() {
|
||||||
let tmp_dir = create_temp_espanso_directory();
|
let tmp_dir = create_temp_espanso_directory();
|
||||||
|
|
||||||
let specific_path = create_temp_file_in_dir(&tmp_dir, "specific.yaml", r###"
|
let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###"
|
||||||
name: firefox
|
name: firefox
|
||||||
filter_title: "Firefox"
|
filter_title: "Firefox"
|
||||||
"###);
|
"###);
|
||||||
|
@ -447,7 +446,7 @@ mod tests {
|
||||||
fn test_runtime_active_config_cache() {
|
fn test_runtime_active_config_cache() {
|
||||||
let tmp_dir = create_temp_espanso_directory();
|
let tmp_dir = create_temp_espanso_directory();
|
||||||
|
|
||||||
let specific_path = create_temp_file_in_dir(&tmp_dir, "specific.yaml", r###"
|
let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###"
|
||||||
name: firefox
|
name: firefox
|
||||||
filter_title: "Firefox"
|
filter_title: "Firefox"
|
||||||
"###);
|
"###);
|
||||||
|
|
200
src/main.rs
200
src/main.rs
|
@ -43,27 +43,36 @@ use crate::system::SystemManager;
|
||||||
use crate::ui::UIManager;
|
use crate::ui::UIManager;
|
||||||
use crate::protocol::*;
|
use crate::protocol::*;
|
||||||
use std::io::{BufReader, BufRead};
|
use std::io::{BufReader, BufRead};
|
||||||
|
use crate::package::default::DefaultPackageManager;
|
||||||
|
use crate::package::{PackageManager, InstallResult, UpdateResult, RemoveResult};
|
||||||
|
|
||||||
mod ui;
|
mod ui;
|
||||||
mod event;
|
mod event;
|
||||||
mod check;
|
mod check;
|
||||||
|
mod utils;
|
||||||
mod bridge;
|
mod bridge;
|
||||||
mod engine;
|
mod engine;
|
||||||
mod config;
|
mod config;
|
||||||
mod system;
|
mod system;
|
||||||
mod sysdaemon;
|
|
||||||
mod context;
|
mod context;
|
||||||
mod matcher;
|
mod matcher;
|
||||||
|
mod package;
|
||||||
mod keyboard;
|
mod keyboard;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
mod clipboard;
|
mod clipboard;
|
||||||
mod extension;
|
mod extension;
|
||||||
|
mod sysdaemon;
|
||||||
|
|
||||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||||
const LOG_FILE: &str = "espanso.log";
|
const LOG_FILE: &str = "espanso.log";
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let matches = App::new("espanso")
|
let install_subcommand = SubCommand::with_name("install")
|
||||||
|
.about("Install a package. Equivalent to 'espanso package install'")
|
||||||
|
.arg(Arg::with_name("package_name")
|
||||||
|
.help("Package name"));
|
||||||
|
|
||||||
|
let mut clap_instance = App::new("espanso")
|
||||||
.version(VERSION)
|
.version(VERSION)
|
||||||
.author("Federico Terzi")
|
.author("Federico Terzi")
|
||||||
.about("Cross-platform Text Expander written in Rust")
|
.about("Cross-platform Text Expander written in Rust")
|
||||||
|
@ -71,7 +80,7 @@ fn main() {
|
||||||
.short("c")
|
.short("c")
|
||||||
.long("config")
|
.long("config")
|
||||||
.value_name("FILE")
|
.value_name("FILE")
|
||||||
.help("Sets a custom config directory. If not specified, reads the default $HOME/.espanso/default.yaml file, creating it if not present.")
|
.help("Sets a custom config directory. If not specified, reads the default $HOME/.espanso/default.yml file, creating it if not present.")
|
||||||
.takes_value(true))
|
.takes_value(true))
|
||||||
.arg(Arg::with_name("v")
|
.arg(Arg::with_name("v")
|
||||||
.short("v")
|
.short("v")
|
||||||
|
@ -108,7 +117,26 @@ fn main() {
|
||||||
.about("Restart the espanso daemon."))
|
.about("Restart the espanso daemon."))
|
||||||
.subcommand(SubCommand::with_name("status")
|
.subcommand(SubCommand::with_name("status")
|
||||||
.about("Check if the espanso daemon is running or not."))
|
.about("Check if the espanso daemon is running or not."))
|
||||||
.get_matches();
|
|
||||||
|
// Package manager
|
||||||
|
.subcommand(SubCommand::with_name("package")
|
||||||
|
.about("Espanso package manager commands")
|
||||||
|
.subcommand(install_subcommand.clone())
|
||||||
|
.subcommand(SubCommand::with_name("list")
|
||||||
|
.about("List all installed packages")
|
||||||
|
.arg(Arg::with_name("full")
|
||||||
|
.help("Print all package info")
|
||||||
|
.long("full")))
|
||||||
|
.subcommand(SubCommand::with_name("remove")
|
||||||
|
.about("Remove an installed package")
|
||||||
|
.arg(Arg::with_name("package_name")
|
||||||
|
.help("Package name")))
|
||||||
|
.subcommand(SubCommand::with_name("refresh")
|
||||||
|
.about("Update espanso package index"))
|
||||||
|
)
|
||||||
|
.subcommand(install_subcommand);
|
||||||
|
|
||||||
|
let matches = clap_instance.clone().get_matches();
|
||||||
|
|
||||||
let log_level = matches.occurrences_of("v") as i32;
|
let log_level = matches.occurrences_of("v") as i32;
|
||||||
|
|
||||||
|
@ -190,8 +218,32 @@ fn main() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defaults to start subcommand
|
if let Some(matches) = matches.subcommand_matches("install") {
|
||||||
start_main(config_set);
|
install_main(config_set, matches);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(matches) = matches.subcommand_matches("package") {
|
||||||
|
if let Some(matches) = matches.subcommand_matches("install") {
|
||||||
|
install_main(config_set, matches);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(matches) = matches.subcommand_matches("remove") {
|
||||||
|
remove_package_main(config_set, matches);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(matches) = matches.subcommand_matches("list") {
|
||||||
|
list_package_main(config_set, matches);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(_) = matches.subcommand_matches("refresh") {
|
||||||
|
update_index_main(config_set);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults help print
|
||||||
|
clap_instance.print_long_help().expect("Unable to print help");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Daemon subcommand, start the event loop and spawn a background thread worker
|
/// Daemon subcommand, start the event loop and spawn a background thread worker
|
||||||
|
@ -547,6 +599,142 @@ fn unregister_main(config_set: ConfigSet) {
|
||||||
sysdaemon::unregister(config_set);
|
sysdaemon::unregister(config_set);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn install_main(_config_set: ConfigSet, matches: &ArgMatches) {
|
||||||
|
let package_name = matches.value_of("package_name").unwrap_or_else(|| {
|
||||||
|
eprintln!("Missing package name!");
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut package_manager = DefaultPackageManager::new_default();
|
||||||
|
|
||||||
|
if package_manager.is_index_outdated() {
|
||||||
|
println!("Updating package index...");
|
||||||
|
let res = package_manager.update_index(false);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(update_result) => {
|
||||||
|
match update_result {
|
||||||
|
UpdateResult::NotOutdated => {
|
||||||
|
eprintln!("Index was already up to date");
|
||||||
|
},
|
||||||
|
UpdateResult::Updated => {
|
||||||
|
println!("Index updated!");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
exit(2);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
println!("Using cached package index, run 'espanso package refresh' to update it.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = package_manager.install_package(package_name);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(install_result) => {
|
||||||
|
match install_result {
|
||||||
|
InstallResult::NotFoundInIndex => {
|
||||||
|
eprintln!("Package not found");
|
||||||
|
},
|
||||||
|
InstallResult::NotFoundInRepo => {
|
||||||
|
eprintln!("Package not found in repository, are you sure the folder exist in the repo?");
|
||||||
|
},
|
||||||
|
InstallResult::UnableToParsePackageInfo => {
|
||||||
|
eprintln!("Unable to parse Package info from README.md");
|
||||||
|
},
|
||||||
|
InstallResult::MissingPackageVersion => {
|
||||||
|
eprintln!("Missing package version");
|
||||||
|
},
|
||||||
|
InstallResult::AlreadyInstalled => {
|
||||||
|
eprintln!("{} already installed!", package_name);
|
||||||
|
},
|
||||||
|
InstallResult::Installed => {
|
||||||
|
println!("{} successfully installed!", package_name);
|
||||||
|
println!();
|
||||||
|
println!("You need to restart espanso for changes to take effect, using:");
|
||||||
|
println!(" espanso restart");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_package_main(_config_set: ConfigSet, matches: &ArgMatches) {
|
||||||
|
let package_name = matches.value_of("package_name").unwrap_or_else(|| {
|
||||||
|
eprintln!("Missing package name!");
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let package_manager = DefaultPackageManager::new_default();
|
||||||
|
|
||||||
|
let res = package_manager.remove_package(package_name);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(remove_result) => {
|
||||||
|
match remove_result {
|
||||||
|
RemoveResult::NotFound => {
|
||||||
|
eprintln!("{} package was not installed.", package_name);
|
||||||
|
},
|
||||||
|
RemoveResult::Removed => {
|
||||||
|
println!("{} successfully removed!", package_name);
|
||||||
|
println!();
|
||||||
|
println!("You need to restart espanso for changes to take effect, using:");
|
||||||
|
println!(" espanso restart");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_index_main(_config_set: ConfigSet) {
|
||||||
|
let mut package_manager = DefaultPackageManager::new_default();
|
||||||
|
|
||||||
|
let res = package_manager.update_index(true);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(update_result) => {
|
||||||
|
match update_result {
|
||||||
|
UpdateResult::NotOutdated => {
|
||||||
|
eprintln!("Index was already up to date");
|
||||||
|
},
|
||||||
|
UpdateResult::Updated => {
|
||||||
|
println!("Index updated!");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
exit(2);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_package_main(_config_set: ConfigSet, matches: &ArgMatches) {
|
||||||
|
let package_manager = DefaultPackageManager::new_default();
|
||||||
|
|
||||||
|
let list = package_manager.list_local_packages();
|
||||||
|
|
||||||
|
if matches.is_present("full") {
|
||||||
|
for package in list.iter() {
|
||||||
|
println!("{:?}", package);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
for package in list.iter() {
|
||||||
|
println!("{} - {}", package.name, package.version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fn acquire_lock() -> Option<File> {
|
fn acquire_lock() -> Option<File> {
|
||||||
let espanso_dir = context::get_data_dir();
|
let espanso_dir = context::get_data_dir();
|
||||||
let lock_file_path = espanso_dir.join("espanso.lock");
|
let lock_file_path = espanso_dir.join("espanso.lock");
|
||||||
|
|
597
src/package/default.rs
Normal file
597
src/package/default.rs
Normal file
|
@ -0,0 +1,597 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::path::{PathBuf, Path};
|
||||||
|
use crate::package::{PackageIndex, UpdateResult, Package, InstallResult, RemoveResult};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs::{File, create_dir};
|
||||||
|
use std::io::{BufReader, BufRead};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use crate::package::UpdateResult::{NotOutdated, Updated};
|
||||||
|
use crate::package::InstallResult::{NotFoundInIndex, AlreadyInstalled};
|
||||||
|
use std::fs;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use git2::Repository;
|
||||||
|
use regex::Regex;
|
||||||
|
use crate::package::RemoveResult::Removed;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
const DEFAULT_PACKAGE_INDEX_FILE : &str = "package_index.json";
|
||||||
|
|
||||||
|
pub struct DefaultPackageManager {
|
||||||
|
package_dir: PathBuf,
|
||||||
|
data_dir: PathBuf,
|
||||||
|
|
||||||
|
local_index: Option<PackageIndex>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultPackageManager {
|
||||||
|
pub fn new(package_dir: PathBuf, data_dir: PathBuf) -> DefaultPackageManager {
|
||||||
|
let local_index = Self::load_local_index(&data_dir);
|
||||||
|
|
||||||
|
DefaultPackageManager{
|
||||||
|
package_dir,
|
||||||
|
data_dir,
|
||||||
|
local_index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_default() -> DefaultPackageManager {
|
||||||
|
DefaultPackageManager::new(
|
||||||
|
crate::config::ConfigSet::get_default_packages_dir(),
|
||||||
|
crate::context::get_data_dir()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_package_index_path(data_dir: &Path) -> PathBuf {
|
||||||
|
data_dir.join(DEFAULT_PACKAGE_INDEX_FILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_local_index(data_dir: &Path) -> Option<super::PackageIndex> {
|
||||||
|
let local_index_file = File::open(Self::get_package_index_path(data_dir));
|
||||||
|
if let Ok(local_index_file) = local_index_file {
|
||||||
|
let reader = BufReader::new(local_index_file);
|
||||||
|
let local_index = serde_json::from_reader(reader);
|
||||||
|
|
||||||
|
if let Ok(local_index) = local_index {
|
||||||
|
return local_index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_index() -> Result<super::PackageIndex, Box<dyn Error>> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let request = client.get("https://hub.espanso.org/json/")
|
||||||
|
.header("User-Agent", format!("espanso/{}", crate::VERSION));
|
||||||
|
|
||||||
|
let mut res = request.send()?;
|
||||||
|
let body = res.text()?;
|
||||||
|
let index : PackageIndex = serde_json::from_str(&body)?;
|
||||||
|
|
||||||
|
Ok(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_repo_to_temp(repo_url: &str) -> Result<TempDir, Box<dyn Error>> {
|
||||||
|
let temp_dir = TempDir::new()?;
|
||||||
|
let _repo = Repository::clone(repo_url, temp_dir.path())?;
|
||||||
|
Ok(temp_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_package_from_readme(readme_path: &Path) -> Option<Package> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref FIELD_REGEX: Regex = Regex::new(r###"^\s*(.*?)\s*:\s*"?(.*?)"?$"###).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read readme line by line
|
||||||
|
let file = File::open(readme_path);
|
||||||
|
if let Ok(file) = file {
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
|
let mut fields :HashMap<String, String> = HashMap::new();
|
||||||
|
|
||||||
|
let mut started = false;
|
||||||
|
|
||||||
|
for (_index, line) in reader.lines().enumerate() {
|
||||||
|
let line = line.unwrap();
|
||||||
|
if line.contains("---") {
|
||||||
|
if started {
|
||||||
|
break
|
||||||
|
}else{
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
if started {
|
||||||
|
let caps = FIELD_REGEX.captures(&line);
|
||||||
|
if let Some(caps) = caps {
|
||||||
|
let property = caps.get(1);
|
||||||
|
let value = caps.get(2);
|
||||||
|
if property.is_some() && value.is_some() {
|
||||||
|
fields.insert(property.unwrap().as_str().to_owned(),
|
||||||
|
value.unwrap().as_str().to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fields.contains_key("package_name") ||
|
||||||
|
!fields.contains_key("package_title") ||
|
||||||
|
!fields.contains_key("package_version") ||
|
||||||
|
!fields.contains_key("package_repo") ||
|
||||||
|
!fields.contains_key("package_desc") ||
|
||||||
|
!fields.contains_key("package_author") {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
|
||||||
|
let package = Package {
|
||||||
|
name: fields.get("package_name").unwrap().clone(),
|
||||||
|
title: fields.get("package_title").unwrap().clone(),
|
||||||
|
version: fields.get("package_version").unwrap().clone(),
|
||||||
|
repo: fields.get("package_repo").unwrap().clone(),
|
||||||
|
desc: fields.get("package_desc").unwrap().clone(),
|
||||||
|
author: fields.get("package_author").unwrap().clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(package)
|
||||||
|
}else{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_index_timestamp(&self) -> u64 {
|
||||||
|
if let Some(local_index) = &self.local_index {
|
||||||
|
return local_index.last_update
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_local_packages_names(&self) -> Vec<String> {
|
||||||
|
let dir = fs::read_dir(&self.package_dir);
|
||||||
|
let mut output = Vec::new();
|
||||||
|
if let Ok(dir) = dir {
|
||||||
|
for entry in dir {
|
||||||
|
if let Ok(entry) = entry {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
let name = path.file_name();
|
||||||
|
if let Some(name) = name {
|
||||||
|
output.push(name.to_str().unwrap().to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cache_local_index(&self) {
|
||||||
|
if let Some(local_index) = &self.local_index {
|
||||||
|
let serialized = serde_json::to_string(local_index).expect("Unable to serialize local index");
|
||||||
|
let local_index_file = self.data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||||
|
std::fs::write(local_index_file, serialized).expect("Unable to cache local index");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::PackageManager for DefaultPackageManager {
|
||||||
|
fn is_index_outdated(&self) -> bool {
|
||||||
|
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards");
|
||||||
|
let current_timestamp = current_time.as_secs();
|
||||||
|
|
||||||
|
let local_index_timestamp = self.local_index_timestamp();
|
||||||
|
|
||||||
|
// Local index is outdated if older than a day
|
||||||
|
local_index_timestamp + 60*60*24 < current_timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_index(&mut self, force: bool) -> Result<UpdateResult, Box<dyn Error>> {
|
||||||
|
if force || self.is_index_outdated() {
|
||||||
|
let updated_index = DefaultPackageManager::request_index()?;
|
||||||
|
self.local_index = Some(updated_index);
|
||||||
|
|
||||||
|
// Save the index to file
|
||||||
|
self.cache_local_index();
|
||||||
|
|
||||||
|
Ok(Updated)
|
||||||
|
}else{
|
||||||
|
Ok(NotOutdated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_package(&self, name: &str) -> Option<Package> {
|
||||||
|
if let Some(local_index) = &self.local_index {
|
||||||
|
let result = local_index.packages.iter().find(|package| {
|
||||||
|
package.name == name
|
||||||
|
});
|
||||||
|
if let Some(package) = result {
|
||||||
|
return Some(package.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_package(&self, name: &str) -> Result<InstallResult, Box<dyn Error>> {
|
||||||
|
let package = self.get_package(name);
|
||||||
|
match package {
|
||||||
|
Some(package) => {
|
||||||
|
self.install_package_from_repo(name, &package.repo)
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
Ok(NotFoundInIndex)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_package_from_repo(&self, name: &str, repo_url: &str) -> Result<InstallResult, Box<dyn Error>> {
|
||||||
|
// Check if package is already installed
|
||||||
|
let packages = self.list_local_packages_names();
|
||||||
|
if packages.iter().any(|p| p == name) { // Package already installed
|
||||||
|
return Ok(AlreadyInstalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
let temp_dir = Self::clone_repo_to_temp(repo_url)?;
|
||||||
|
|
||||||
|
let temp_package_dir = temp_dir.path().join(name);
|
||||||
|
if !temp_package_dir.exists() {
|
||||||
|
return Ok(InstallResult::NotFoundInRepo);
|
||||||
|
}
|
||||||
|
|
||||||
|
let readme_path = temp_package_dir.join("README.md");
|
||||||
|
|
||||||
|
let package = Self::parse_package_from_readme(&readme_path);
|
||||||
|
if !package.is_some() {
|
||||||
|
return Ok(InstallResult::UnableToParsePackageInfo); // TODO: test
|
||||||
|
}
|
||||||
|
let package = package.unwrap();
|
||||||
|
|
||||||
|
let source_dir = temp_package_dir.join(package.version);
|
||||||
|
if !source_dir.exists() {
|
||||||
|
return Ok(InstallResult::MissingPackageVersion); // TODO: test
|
||||||
|
}
|
||||||
|
|
||||||
|
let target_dir = &self.package_dir.join(name);
|
||||||
|
create_dir(&target_dir)?;
|
||||||
|
|
||||||
|
crate::utils::copy_dir(&source_dir, target_dir)?;
|
||||||
|
|
||||||
|
let readme_dest = target_dir.join("README.md");
|
||||||
|
std::fs::copy(readme_path, readme_dest)?;
|
||||||
|
|
||||||
|
Ok(InstallResult::Installed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_package(&self, name: &str) -> Result<RemoveResult, Box<dyn Error>> {
|
||||||
|
let package_dir = self.package_dir.join(name);
|
||||||
|
if !package_dir.exists() {
|
||||||
|
return Ok(RemoveResult::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(package_dir)?;
|
||||||
|
|
||||||
|
Ok(Removed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_local_packages(&self) -> Vec<Package> {
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
let package_names = self.list_local_packages_names();
|
||||||
|
|
||||||
|
for name in package_names.iter() {
|
||||||
|
let package_dir = &self.package_dir.join(name);
|
||||||
|
let readme_file = package_dir.join("README.md");
|
||||||
|
let package = Self::parse_package_from_readme(&readme_file);
|
||||||
|
if let Some(package) = package {
|
||||||
|
output.push(package);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::{TempDir, NamedTempFile};
|
||||||
|
use std::path::Path;
|
||||||
|
use crate::package::PackageManager;
|
||||||
|
use std::fs::{create_dir, create_dir_all};
|
||||||
|
use crate::package::InstallResult::{Installed, NotFoundInRepo};
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
const OUTDATED_INDEX_CONTENT : &str = include_str!("../res/test/outdated_index.json");
|
||||||
|
const INDEX_CONTENT_WITHOUT_UPDATE: &str = include_str!("../res/test/index_without_update.json");
|
||||||
|
const GET_PACKAGE_INDEX: &str = include_str!("../res/test/get_package_index.json");
|
||||||
|
const INSTALL_PACKAGE_INDEX: &str = include_str!("../res/test/install_package_index.json");
|
||||||
|
|
||||||
|
struct TempPackageManager {
|
||||||
|
package_dir: TempDir,
|
||||||
|
data_dir: TempDir,
|
||||||
|
package_manager: DefaultPackageManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_temp_package_manager<F>(setup: F) -> TempPackageManager where F: Fn(&Path, &Path) -> (){
|
||||||
|
let package_dir = TempDir::new().expect("unable to create temp directory");
|
||||||
|
let data_dir = TempDir::new().expect("unable to create temp directory");
|
||||||
|
|
||||||
|
setup(package_dir.path(), data_dir.path());
|
||||||
|
|
||||||
|
let package_manager = DefaultPackageManager::new(
|
||||||
|
package_dir.path().clone().to_path_buf(),
|
||||||
|
data_dir.path().clone().to_path_buf()
|
||||||
|
);
|
||||||
|
|
||||||
|
TempPackageManager {
|
||||||
|
package_dir,
|
||||||
|
data_dir,
|
||||||
|
package_manager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_download_index() {
|
||||||
|
let temp = create_temp_package_manager(|_, _| {});
|
||||||
|
let index = DefaultPackageManager::request_index();
|
||||||
|
|
||||||
|
assert!(index.is_ok());
|
||||||
|
assert!(index.unwrap().packages.len() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_outdated_index() {
|
||||||
|
let temp = create_temp_package_manager(|_, data_dir| {
|
||||||
|
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||||
|
std::fs::write(index_file, OUTDATED_INDEX_CONTENT);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(temp.package_manager.is_index_outdated());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_up_to_date_index_should_not_be_updated() {
|
||||||
|
let mut temp = create_temp_package_manager(|_, data_dir| {
|
||||||
|
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||||
|
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards");
|
||||||
|
let current_timestamp = current_time.as_secs();
|
||||||
|
let new_contents = INDEX_CONTENT_WITHOUT_UPDATE.replace("XXXX", &format!("{}", current_timestamp));
|
||||||
|
std::fs::write(index_file, new_contents);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(temp.package_manager.update_index(false).unwrap(), UpdateResult::NotOutdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_up_to_date_index_with_force_should_be_updated() {
|
||||||
|
let mut temp = create_temp_package_manager(|_, data_dir| {
|
||||||
|
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||||
|
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards");
|
||||||
|
let current_timestamp = current_time.as_secs();
|
||||||
|
let new_contents = INDEX_CONTENT_WITHOUT_UPDATE.replace("XXXX", &format!("{}", current_timestamp));
|
||||||
|
std::fs::write(index_file, new_contents);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(temp.package_manager.update_index(true).unwrap(), UpdateResult::Updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_outdated_index_should_be_updated() {
|
||||||
|
let mut temp = create_temp_package_manager(|_, data_dir| {
|
||||||
|
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||||
|
std::fs::write(index_file, OUTDATED_INDEX_CONTENT);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(temp.package_manager.update_index(false).unwrap(), UpdateResult::Updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_index_should_create_file() {
|
||||||
|
let mut temp = create_temp_package_manager(|_, _| {});
|
||||||
|
|
||||||
|
assert_eq!(temp.package_manager.update_index(false).unwrap(), UpdateResult::Updated);
|
||||||
|
assert!(temp.data_dir.path().join(DEFAULT_PACKAGE_INDEX_FILE).exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_package_should_be_found() {
|
||||||
|
let mut temp = create_temp_package_manager(|_, data_dir| {
|
||||||
|
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||||
|
std::fs::write(index_file, GET_PACKAGE_INDEX);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(temp.package_manager.get_package("italian-accents").unwrap().title, "Italian Accents");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_package_should_not_be_found() {
|
||||||
|
let mut temp = create_temp_package_manager(|_, data_dir| {
|
||||||
|
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||||
|
std::fs::write(index_file, GET_PACKAGE_INDEX);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(temp.package_manager.get_package("not-existing").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_local_packages_names() {
|
||||||
|
let mut temp = create_temp_package_manager(|package_dir, _| {
|
||||||
|
create_dir(package_dir.join("package-1"));
|
||||||
|
create_dir(package_dir.join("package2"));
|
||||||
|
std::fs::write(package_dir.join("dummyfile.txt"), "test");
|
||||||
|
});
|
||||||
|
|
||||||
|
let packages = temp.package_manager.list_local_packages_names();
|
||||||
|
assert_eq!(packages.len(), 2);
|
||||||
|
assert!(packages.iter().any(|p| p == "package-1"));
|
||||||
|
assert!(packages.iter().any(|p| p == "package2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_install_package_not_found() {
|
||||||
|
let mut temp = create_temp_package_manager(|package_dir, data_dir| {
|
||||||
|
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||||
|
std::fs::write(index_file, INSTALL_PACKAGE_INDEX);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(temp.package_manager.install_package("doesnotexist").unwrap(), NotFoundInIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_install_package_already_installed() {
|
||||||
|
let mut temp = create_temp_package_manager(|package_dir, data_dir| {
|
||||||
|
create_dir(package_dir.join("italian-accents"));
|
||||||
|
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||||
|
std::fs::write(index_file, INSTALL_PACKAGE_INDEX);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(temp.package_manager.install_package("italian-accents").unwrap(), AlreadyInstalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clone_temp_repository() {
|
||||||
|
let cloned_dir = DefaultPackageManager::clone_repo_to_temp("https://github.com/federico-terzi/espanso-hub-core").unwrap();
|
||||||
|
assert!(cloned_dir.path().join("LICENSE").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_install_package() {
|
||||||
|
let mut temp = create_temp_package_manager(|_, data_dir| {
|
||||||
|
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||||
|
std::fs::write(index_file, INSTALL_PACKAGE_INDEX);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(temp.package_manager.install_package("dummy-package").unwrap(), Installed);
|
||||||
|
assert!(temp.package_dir.path().join("dummy-package").exists());
|
||||||
|
assert!(temp.package_dir.path().join("dummy-package/README.md").exists());
|
||||||
|
assert!(temp.package_dir.path().join("dummy-package/package.yml").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_install_package_does_not_exist_in_repo() {
|
||||||
|
let mut temp = create_temp_package_manager(|_, data_dir| {
|
||||||
|
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||||
|
std::fs::write(index_file, INSTALL_PACKAGE_INDEX);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(temp.package_manager.install_package("not-existing").unwrap(), NotFoundInRepo);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_local_packages() {
|
||||||
|
let mut temp = create_temp_package_manager(|_, data_dir| {
|
||||||
|
let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE);
|
||||||
|
std::fs::write(index_file, INSTALL_PACKAGE_INDEX);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(temp.package_manager.install_package("dummy-package").unwrap(), Installed);
|
||||||
|
assert!(temp.package_dir.path().join("dummy-package").exists());
|
||||||
|
assert!(temp.package_dir.path().join("dummy-package/README.md").exists());
|
||||||
|
assert!(temp.package_dir.path().join("dummy-package/package.yml").exists());
|
||||||
|
|
||||||
|
let list = temp.package_manager.list_local_packages();
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
assert_eq!(list[0].name, "dummy-package");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_remove_package() {
|
||||||
|
let mut temp = create_temp_package_manager(|package_dir, _| {
|
||||||
|
let dummy_package_dir = package_dir.join("dummy-package");
|
||||||
|
create_dir_all(&dummy_package_dir);
|
||||||
|
std::fs::write(dummy_package_dir.join("README.md"), "readme");
|
||||||
|
std::fs::write(dummy_package_dir.join("package.yml"), "name: package");
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(temp.package_dir.path().join("dummy-package").exists());
|
||||||
|
assert!(temp.package_dir.path().join("dummy-package/README.md").exists());
|
||||||
|
assert!(temp.package_dir.path().join("dummy-package/package.yml").exists());
|
||||||
|
assert_eq!(temp.package_manager.remove_package("dummy-package").unwrap(), RemoveResult::Removed);
|
||||||
|
assert!(!temp.package_dir.path().join("dummy-package").exists());
|
||||||
|
assert!(!temp.package_dir.path().join("dummy-package/README.md").exists());
|
||||||
|
assert!(!temp.package_dir.path().join("dummy-package/package.yml").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_remove_package_not_found() {
|
||||||
|
let mut temp = create_temp_package_manager(|_, _| {});
|
||||||
|
|
||||||
|
assert_eq!(temp.package_manager.remove_package("not-existing").unwrap(), RemoveResult::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_package_from_readme() {
|
||||||
|
let file = NamedTempFile::new().unwrap();
|
||||||
|
fs::write(file.path(), r###"
|
||||||
|
---
|
||||||
|
package_name: "italian-accents"
|
||||||
|
package_title: "Italian Accents"
|
||||||
|
package_desc: "Include Italian accents substitutions to espanso."
|
||||||
|
package_version: "0.1.0"
|
||||||
|
package_author: "Federico Terzi"
|
||||||
|
package_repo: "https://github.com/federico-terzi/espanso-hub-core"
|
||||||
|
---
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let package = DefaultPackageManager::parse_package_from_readme(file.path()).unwrap();
|
||||||
|
|
||||||
|
let target_package = Package {
|
||||||
|
name: "italian-accents".to_string(),
|
||||||
|
title: "Italian Accents".to_string(),
|
||||||
|
version: "0.1.0".to_string(),
|
||||||
|
repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(),
|
||||||
|
desc: "Include Italian accents substitutions to espanso.".to_string(),
|
||||||
|
author: "Federico Terzi".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(package, target_package);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_package_from_readme_with_bad_metadata() {
|
||||||
|
let file = NamedTempFile::new().unwrap();
|
||||||
|
fs::write(file.path(), r###"
|
||||||
|
---
|
||||||
|
package_name: italian-accents
|
||||||
|
package_title: "Italian Accents"
|
||||||
|
package_desc: "Include Italian accents substitutions to espanso."
|
||||||
|
package_version:"0.1.0"
|
||||||
|
package_author:Federico Terzi
|
||||||
|
package_repo: "https://github.com/federico-terzi/espanso-hub-core"
|
||||||
|
---
|
||||||
|
Readme text
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let package = DefaultPackageManager::parse_package_from_readme(file.path()).unwrap();
|
||||||
|
|
||||||
|
let target_package = Package {
|
||||||
|
name: "italian-accents".to_string(),
|
||||||
|
title: "Italian Accents".to_string(),
|
||||||
|
version: "0.1.0".to_string(),
|
||||||
|
repo: "https://github.com/federico-terzi/espanso-hub-core".to_string(),
|
||||||
|
desc: "Include Italian accents substitutions to espanso.".to_string(),
|
||||||
|
author: "Federico Terzi".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(package, target_package);
|
||||||
|
}
|
||||||
|
}
|
77
src/package/mod.rs
Normal file
77
src/package/mod.rs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub(crate) mod default;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
pub trait PackageManager {
|
||||||
|
fn is_index_outdated(&self) -> bool;
|
||||||
|
fn update_index(&mut self, force: bool) -> Result<UpdateResult, Box<dyn Error>>;
|
||||||
|
|
||||||
|
fn get_package(&self, name: &str) -> Option<Package>;
|
||||||
|
|
||||||
|
fn install_package(&self, name: &str) -> Result<InstallResult, Box<dyn Error>>;
|
||||||
|
fn install_package_from_repo(&self, name: &str, repo_url: &str) -> Result<InstallResult, Box<dyn Error>>;
|
||||||
|
|
||||||
|
fn remove_package(&self, name: &str) -> Result<RemoveResult, Box<dyn Error>>;
|
||||||
|
|
||||||
|
fn list_local_packages(&self) -> Vec<Package>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct Package {
|
||||||
|
pub name: String,
|
||||||
|
pub title: String,
|
||||||
|
pub version: String,
|
||||||
|
pub repo: String,
|
||||||
|
pub desc: String,
|
||||||
|
pub author: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct PackageIndex {
|
||||||
|
#[serde(rename = "lastUpdate")]
|
||||||
|
pub last_update: u64,
|
||||||
|
|
||||||
|
pub packages: Vec<Package>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum UpdateResult {
|
||||||
|
NotOutdated,
|
||||||
|
Updated,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum InstallResult {
|
||||||
|
NotFoundInIndex,
|
||||||
|
NotFoundInRepo,
|
||||||
|
UnableToParsePackageInfo,
|
||||||
|
MissingPackageVersion,
|
||||||
|
AlreadyInstalled,
|
||||||
|
Installed
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum RemoveResult {
|
||||||
|
NotFound,
|
||||||
|
Removed
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# This is the default configuration file, change it as you like it
|
# This is the default configuration file, change it as you like it
|
||||||
# You can refer to the official documentation:
|
# You can refer to the official documentation:
|
||||||
# https://github.com/federico-terzi/espanso
|
# https://espanso.org/docs/
|
||||||
|
|
||||||
# Matches are the substitution rules, when you type the "trigger" string
|
# Matches are the substitution rules, when you type the "trigger" string
|
||||||
# it gets replaced by the "replace" string.
|
# it gets replaced by the "replace" string.
|
||||||
|
@ -27,34 +27,4 @@ matches:
|
||||||
- name: output
|
- name: output
|
||||||
type: shell
|
type: shell
|
||||||
params:
|
params:
|
||||||
cmd: "echo Hello from you shell"
|
cmd: "echo Hello from your shell"
|
||||||
|
|
||||||
# Emojis
|
|
||||||
- trigger: ":lol"
|
|
||||||
replace: "😂"
|
|
||||||
- trigger: ":llol"
|
|
||||||
replace: "😂😂😂😂"
|
|
||||||
- trigger: ":sad"
|
|
||||||
replace: "☹"
|
|
||||||
- trigger: ":ssad"
|
|
||||||
replace: "☹☹☹☹"
|
|
||||||
|
|
||||||
# Accented letters
|
|
||||||
- trigger: "e''"
|
|
||||||
replace: "è"
|
|
||||||
- trigger: "e//"
|
|
||||||
replace: "é"
|
|
||||||
- trigger: "a''"
|
|
||||||
replace: "à"
|
|
||||||
- trigger: "i''"
|
|
||||||
replace: "ì"
|
|
||||||
- trigger: "o''"
|
|
||||||
replace: "ò"
|
|
||||||
- trigger: "u''"
|
|
||||||
replace: "ù"
|
|
||||||
|
|
||||||
# Capital accented letters
|
|
||||||
- trigger: "E''"
|
|
||||||
replace: "È"
|
|
||||||
- trigger: "E//"
|
|
||||||
replace: "É"
|
|
27
src/res/test/get_package_index.json
Normal file
27
src/res/test/get_package_index.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"lastUpdate": 1565437389,
|
||||||
|
"packages": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "basic-emojis",
|
||||||
|
"title": "Basic Emojis",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"repo": "https://github.com/federico-terzi/espanso-hub-core",
|
||||||
|
"desc": "A package to include some basic emojis in espanso.",
|
||||||
|
"author": "Federico Terzi"
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "italian-accents",
|
||||||
|
"title": "Italian Accents",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"repo": "https://github.com/federico-terzi/espanso-hub-core",
|
||||||
|
"desc": "Include Italian accents substitutions to espanso.",
|
||||||
|
"author": "Federico Terzi"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
]}
|
27
src/res/test/index_without_update.json
Normal file
27
src/res/test/index_without_update.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"lastUpdate": XXXX,
|
||||||
|
"packages": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "basic-emojis",
|
||||||
|
"title": "Basic Emojis",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"repo": "https://github.com/federico-terzi/espanso-hub-core",
|
||||||
|
"desc": "A package to include some basic emojis in espanso.",
|
||||||
|
"author": "Federico Terzi"
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "italian-accents",
|
||||||
|
"title": "Italian Accents",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"repo": "https://github.com/federico-terzi/espanso-hub-core",
|
||||||
|
"desc": "Include Italian accents substitutions to espanso.",
|
||||||
|
"author": "Federico Terzi"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
]}
|
35
src/res/test/install_package_index.json
Normal file
35
src/res/test/install_package_index.json
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"lastUpdate": 1565437389,
|
||||||
|
"packages": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "dummy-package",
|
||||||
|
"title": "Dummy Package",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"repo": "https://github.com/federico-terzi/espanso-hub-core",
|
||||||
|
"desc": "Dummy package",
|
||||||
|
"author": "Federico Terzi"
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "italian-accents",
|
||||||
|
"title": "Italian Accents",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"repo": "https://github.com/federico-terzi/espanso-hub-core",
|
||||||
|
"desc": "Include Italian accents substitutions to espanso.",
|
||||||
|
"author": "Federico Terzi"
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "not-existing",
|
||||||
|
"title": "Not Existing",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"repo": "https://github.com/federico-terzi/espanso-hub-core",
|
||||||
|
"desc": "Package that does not exist in the repo",
|
||||||
|
"author": "Federico Terzi"
|
||||||
|
|
||||||
|
}
|
||||||
|
]}
|
27
src/res/test/outdated_index.json
Normal file
27
src/res/test/outdated_index.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"lastUpdate": 1565437389,
|
||||||
|
"packages": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "basic-emojis",
|
||||||
|
"title": "Basic Emojis",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"repo": "https://github.com/federico-terzi/espanso-hub-core",
|
||||||
|
"desc": "A package to include some basic emojis in espanso.",
|
||||||
|
"author": "Federico Terzi"
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "italian-accents",
|
||||||
|
"title": "Italian Accents",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"repo": "https://github.com/federico-terzi/espanso-hub-core",
|
||||||
|
"desc": "Include Italian accents substitutions to espanso.",
|
||||||
|
"author": "Federico Terzi"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
]}
|
94
src/utils.rs
Normal file
94
src/utils.rs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs::create_dir;
|
||||||
|
|
||||||
|
pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box<dyn Error>> {
|
||||||
|
for entry in std::fs::read_dir(source_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let entry = entry.path();
|
||||||
|
if entry.is_dir() {
|
||||||
|
let name = entry.file_name().expect("Error obtaining the filename");
|
||||||
|
let target_dir = dest_dir.join(name);
|
||||||
|
create_dir(&target_dir)?;
|
||||||
|
copy_dir(&entry, &target_dir)?;
|
||||||
|
}else if entry.is_file() {
|
||||||
|
let target_entry = dest_dir.join(entry.file_name().expect("Error obtaining the filename"));
|
||||||
|
std::fs::copy(entry, target_entry)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use std::fs::create_dir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_dir_into() {
|
||||||
|
let source_tmp_dir = TempDir::new().expect("Error creating temp directory");
|
||||||
|
let dest_tmp_dir = TempDir::new().expect("Error creating temp directory");
|
||||||
|
|
||||||
|
let source_dir = source_tmp_dir.path().join("source");
|
||||||
|
create_dir(&source_dir);
|
||||||
|
std::fs::write(source_dir.join("file1.txt"), "file1");
|
||||||
|
std::fs::write(source_dir.join("file2.txt"), "file2");
|
||||||
|
|
||||||
|
let target_dir = dest_tmp_dir.path().join("source");
|
||||||
|
create_dir(&target_dir);
|
||||||
|
|
||||||
|
copy_dir(&source_dir, &target_dir);
|
||||||
|
|
||||||
|
assert!(dest_tmp_dir.path().join("source").exists());
|
||||||
|
assert!(dest_tmp_dir.path().join("source/file1.txt").exists());
|
||||||
|
assert!(dest_tmp_dir.path().join("source/file2.txt").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_dir_into_recursive() {
|
||||||
|
let source_tmp_dir = TempDir::new().expect("Error creating temp directory");
|
||||||
|
let dest_tmp_dir = TempDir::new().expect("Error creating temp directory");
|
||||||
|
|
||||||
|
let source_dir = source_tmp_dir.path().join("source");
|
||||||
|
create_dir(&source_dir);
|
||||||
|
std::fs::write(source_dir.join("file1.txt"), "file1");
|
||||||
|
std::fs::write(source_dir.join("file2.txt"), "file2");
|
||||||
|
let nested_dir = source_dir.join("nested");
|
||||||
|
create_dir(&nested_dir);
|
||||||
|
std::fs::write(nested_dir.join("nestedfile.txt"), "nestedfile1");
|
||||||
|
|
||||||
|
let target_dir = dest_tmp_dir.path().join("source");
|
||||||
|
create_dir(&target_dir);
|
||||||
|
|
||||||
|
copy_dir(&source_dir, &target_dir);
|
||||||
|
|
||||||
|
assert!(dest_tmp_dir.path().join("source").exists());
|
||||||
|
assert!(dest_tmp_dir.path().join("source/file1.txt").exists());
|
||||||
|
assert!(dest_tmp_dir.path().join("source/file2.txt").exists());
|
||||||
|
|
||||||
|
assert!(dest_tmp_dir.path().join("source/nested").exists());
|
||||||
|
assert!(dest_tmp_dir.path().join("source/nested/nestedfile.txt").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user