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]
|
||||
name = "espanso"
|
||||
version = "0.1.2"
|
||||
version = "0.2.0"
|
||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||
license = "GPL-3.0"
|
||||
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"
|
||||
chrono = "0.4.9"
|
||||
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]
|
||||
libc = "0.2.62"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.1.0"
|
||||
|
||||
[build-dependencies]
|
||||
cmake = "0.1.31"
|
|
@ -26,20 +26,24 @@ use std::fs::{File, create_dir_all};
|
|||
use std::io::Read;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::event::KeyModifier;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashSet, HashMap};
|
||||
use log::{error};
|
||||
use std::fmt;
|
||||
use std::error::Error;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub(crate) mod runtime;
|
||||
|
||||
// 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
|
||||
fn default_name() -> String{ "default".to_owned() }
|
||||
fn default_parent() -> String{ "self".to_owned() }
|
||||
fn default_filter_title() -> String{ "".to_owned() }
|
||||
fn default_filter_class() -> 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_toggle_interval() -> u32 { 230 }
|
||||
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() }
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
|
@ -58,6 +62,9 @@ pub struct Configs {
|
|||
#[serde(default = "default_name")]
|
||||
pub name: String,
|
||||
|
||||
#[serde(default = "default_parent")]
|
||||
pub parent: String,
|
||||
|
||||
#[serde(default = "default_filter_title")]
|
||||
pub filter_title: String,
|
||||
|
||||
|
@ -94,8 +101,8 @@ pub struct Configs {
|
|||
#[serde(default)]
|
||||
pub backend: BackendType,
|
||||
|
||||
#[serde(default = "default_exclude_parent_matches")]
|
||||
pub exclude_parent_matches: bool,
|
||||
#[serde(default = "default_exclude_default_matches")]
|
||||
pub exclude_default_matches: bool,
|
||||
|
||||
#[serde(default = "default_matches")]
|
||||
pub matches: Vec<Match>
|
||||
|
@ -110,7 +117,7 @@ macro_rules! validate_field {
|
|||
if field_name.starts_with("self.") {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
@ -119,10 +126,10 @@ macro_rules! validate_field {
|
|||
impl Configs {
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
fn validate_specific_config(&self) -> bool {
|
||||
fn validate_user_defined_config(&self) -> bool {
|
||||
let mut result = true;
|
||||
|
||||
validate_field!(result, self.config_caching_interval, default_config_caching_interval());
|
||||
|
@ -171,6 +178,32 @@ impl Configs {
|
|||
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)]
|
||||
|
@ -185,70 +218,86 @@ impl ConfigSet {
|
|||
return Err(ConfigLoadError::InvalidConfigDirectory)
|
||||
}
|
||||
|
||||
// Load default configuration
|
||||
let default_file = dir_path.join(DEFAULT_CONFIG_FILE_NAME);
|
||||
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 name_set = HashSet::new();
|
||||
let mut target_files = Vec::new();
|
||||
|
||||
let dir_entry = fs::read_dir(dir_path);
|
||||
if dir_entry.is_err() {
|
||||
return Err(ConfigLoadError::UnableToReadFile)
|
||||
let specific_dir = dir_path.join(USER_CONFIGS_FOLDER_NAME);
|
||||
if specific_dir.exists() {
|
||||
let dir_entry = WalkDir::new(specific_dir);
|
||||
target_files.extend(dir_entry);
|
||||
}
|
||||
let dir_entry = dir_entry.unwrap();
|
||||
|
||||
for entry in dir_entry {
|
||||
let entry = entry;
|
||||
let package_dir = dir_path.join(PACKAGES_FOLDER_NAME);
|
||||
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 {
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
// No name specified, defaulting to the path name
|
||||
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) {
|
||||
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();
|
||||
name_set.insert(config.name.clone());
|
||||
|
||||
merged_matches.extend(parent_matches);
|
||||
config.matches = merged_matches;
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check if it contains at least a filter, and warn the user about the problem
|
||||
// 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);
|
||||
}
|
||||
|
||||
name_set.insert(config.name.clone());
|
||||
specific.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,10 +307,21 @@ impl ConfigSet {
|
|||
})
|
||||
}
|
||||
|
||||
fn reduce_configs(target: Configs, children_map: &HashMap<String, Vec<Configs>>) -> Configs {
|
||||
if children_map.contains_key(&target.name) {
|
||||
let mut target = target;
|
||||
for children in children_map.get(&target.name).unwrap() {
|
||||
let children = Self::reduce_configs(children.clone(), children_map);
|
||||
target.merge_config(children);
|
||||
}
|
||||
target
|
||||
}else{
|
||||
target
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_default() -> Result<ConfigSet, ConfigLoadError> {
|
||||
let res = dirs::home_dir();
|
||||
if let Some(home_dir) = res {
|
||||
let espanso_dir = home_dir.join(".espanso");
|
||||
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());
|
||||
|
@ -277,14 +337,42 @@ impl ConfigSet {
|
|||
}
|
||||
}
|
||||
|
||||
return ConfigSet::load(espanso_dir.as_path())
|
||||
// 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)
|
||||
}
|
||||
|
||||
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> {
|
||||
fn active_config(&'a self) -> &'a Configs;
|
||||
fn default_config(&'a self) -> &'a Configs;
|
||||
|
@ -299,7 +387,6 @@ pub enum ConfigLoadError {
|
|||
InvalidYAML(PathBuf, String),
|
||||
InvalidConfigDirectory,
|
||||
InvalidParameter(PathBuf),
|
||||
MissingName(PathBuf),
|
||||
NameDuplicate(PathBuf),
|
||||
UnableToCreateDefaultConfig,
|
||||
}
|
||||
|
@ -311,8 +398,7 @@ impl fmt::Display for ConfigLoadError {
|
|||
ConfigLoadError::UnableToReadFile => write!(f, "Unable to read config file"),
|
||||
ConfigLoadError::InvalidYAML(path, e) => write!(f, "Error parsing YAML file '{}', invalid syntax: {}", path.to_str().unwrap_or_default(), e),
|
||||
ConfigLoadError::InvalidConfigDirectory => write!(f, "Invalid config directory"),
|
||||
ConfigLoadError::InvalidParameter(path) => write!(f, "Invalid parameter in '{}', use of reserved parameters in app-specific configs is not permitted", path.to_str().unwrap_or_default()),
|
||||
ConfigLoadError::MissingName(path) => write!(f, "The 'name' field is required in app-specific configurations, but it's missing in '{}'", path.to_str().unwrap_or_default()),
|
||||
ConfigLoadError::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::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"),
|
||||
}
|
||||
|
@ -326,8 +412,7 @@ impl Error for ConfigLoadError {
|
|||
ConfigLoadError::UnableToReadFile => "Unable to read config file",
|
||||
ConfigLoadError::InvalidYAML(_, _) => "Error parsing YAML file, invalid syntax",
|
||||
ConfigLoadError::InvalidConfigDirectory => "Invalid config directory",
|
||||
ConfigLoadError::InvalidParameter(_) => "Invalid parameter, use of reserved parameters in app-specific configs is not permitted",
|
||||
ConfigLoadError::MissingName(_) => "The 'name' field is required in app-specific configurations, but it's missing",
|
||||
ConfigLoadError::InvalidParameter(_) => "Invalid parameter, use of reserved parameters in user defined configs is not permitted",
|
||||
ConfigLoadError::NameDuplicate(_) => "Found duplicate 'name' in some configurations, please use different names",
|
||||
ConfigLoadError::UnableToCreateDefaultConfig => "Could not generate default config file",
|
||||
}
|
||||
|
@ -343,8 +428,8 @@ mod tests {
|
|||
use tempfile::{NamedTempFile, TempDir};
|
||||
use std::any::Any;
|
||||
|
||||
const TEST_WORKING_CONFIG_FILE : &str = include_str!("../res/test/working_config.yaml");
|
||||
const TEST_CONFIG_FILE_WITH_BAD_YAML : &str = include_str!("../res/test/config_with_bad_yaml.yaml");
|
||||
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.yml");
|
||||
|
||||
// Test Configs
|
||||
|
||||
|
@ -397,18 +482,18 @@ mod tests {
|
|||
}
|
||||
|
||||
#[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###"
|
||||
|
||||
backend: Clipboard
|
||||
|
||||
"###);
|
||||
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]
|
||||
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###"
|
||||
|
||||
# This should not happen in an app-specific config
|
||||
|
@ -416,11 +501,11 @@ mod tests {
|
|||
|
||||
"###);
|
||||
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]
|
||||
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###"
|
||||
|
||||
# This should not happen in an app-specific config
|
||||
|
@ -428,11 +513,11 @@ mod tests {
|
|||
|
||||
"###);
|
||||
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]
|
||||
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###"
|
||||
|
||||
# This should not happen in an app-specific config
|
||||
|
@ -440,11 +525,11 @@ mod tests {
|
|||
|
||||
"###);
|
||||
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]
|
||||
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###"
|
||||
|
||||
# This should not happen in an app-specific config
|
||||
|
@ -452,7 +537,7 @@ mod tests {
|
|||
|
||||
"###);
|
||||
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]
|
||||
|
@ -516,59 +601,80 @@ mod tests {
|
|||
let default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
|
||||
fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT);
|
||||
|
||||
let specific_path = tmp_dir.path().join("specific.yaml");
|
||||
let specific_path_copy = specific_path.clone();
|
||||
fs::write(specific_path, r###"
|
||||
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###"
|
||||
config_caching_interval: 10000
|
||||
"###);
|
||||
let user_defined_path_copy = user_defined_path.clone();
|
||||
|
||||
let config_set = ConfigSet::load(tmp_dir.path());
|
||||
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]
|
||||
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 default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
|
||||
fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT);
|
||||
|
||||
let specific_path = tmp_dir.path().join("specific.yaml");
|
||||
let specific_path_copy = specific_path.clone();
|
||||
fs::write(specific_path, r###"
|
||||
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###"
|
||||
backend: Clipboard
|
||||
"###);
|
||||
let user_defined_path_copy = user_defined_path.clone();
|
||||
|
||||
let config_set = ConfigSet::load(tmp_dir.path());
|
||||
assert!(config_set.is_err());
|
||||
assert_eq!(config_set.unwrap_err(), ConfigLoadError::MissingName(specific_path_copy))
|
||||
assert!(config_set.is_ok());
|
||||
assert_eq!(config_set.unwrap().specific[0].name, user_defined_path_copy.to_str().unwrap_or_default())
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
pub fn create_temp_file_in_dir(tmp_dir: &TempDir, name: &str, content: &str) -> PathBuf {
|
||||
let specific_path = tmp_dir.path().join(name);
|
||||
let specific_path_copy = specific_path.clone();
|
||||
fs::write(specific_path, content);
|
||||
pub fn create_temp_file_in_dir(tmp_dir: &PathBuf, name: &str, content: &str) -> PathBuf {
|
||||
let user_defined_path = tmp_dir.join(name);
|
||||
let user_defined_path_copy = user_defined_path.clone();
|
||||
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]
|
||||
fn test_config_set_specific_file_duplicate_name() {
|
||||
let tmp_dir = create_temp_espanso_directory();
|
||||
|
||||
let specific_path = create_temp_file_in_dir(&tmp_dir, "specific.yaml", r###"
|
||||
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###"
|
||||
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
|
||||
"###);
|
||||
|
||||
|
@ -578,7 +684,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[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 default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
|
||||
fs::write(default_path, r###"
|
||||
|
@ -589,9 +695,7 @@ mod tests {
|
|||
replace: "Bob"
|
||||
"###);
|
||||
|
||||
let specific_path = tmp_dir.path().join("specific.yaml");
|
||||
let specific_path_copy = specific_path.clone();
|
||||
fs::write(specific_path, r###"
|
||||
let user_defined_path = create_user_config_file(tmp_dir.path(), "specific1.yml", r###"
|
||||
name: specific1
|
||||
|
||||
matches:
|
||||
|
@ -609,7 +713,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[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 default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
|
||||
fs::write(default_path, r###"
|
||||
|
@ -620,9 +724,7 @@ mod tests {
|
|||
replace: "Bob"
|
||||
"###);
|
||||
|
||||
let specific_path = tmp_dir.path().join("specific.yaml");
|
||||
let specific_path_copy = specific_path.clone();
|
||||
fs::write(specific_path, r###"
|
||||
let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific2.yml", r###"
|
||||
name: specific1
|
||||
|
||||
matches:
|
||||
|
@ -639,7 +741,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[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 default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME);
|
||||
fs::write(default_path, r###"
|
||||
|
@ -650,12 +752,10 @@ mod tests {
|
|||
replace: "Bob"
|
||||
"###);
|
||||
|
||||
let specific_path = tmp_dir.path().join("specific.yaml");
|
||||
let specific_path_copy = specific_path.clone();
|
||||
fs::write(specific_path, r###"
|
||||
let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific2.yml", r###"
|
||||
name: specific1
|
||||
|
||||
exclude_parent_matches: true
|
||||
exclude_default_matches: true
|
||||
|
||||
matches:
|
||||
- trigger: "hello"
|
||||
|
@ -681,12 +781,10 @@ mod tests {
|
|||
replace: "Bob"
|
||||
"###);
|
||||
|
||||
let specific_path = tmp_dir.path().join("specific.zzz");
|
||||
let specific_path_copy = specific_path.clone();
|
||||
fs::write(specific_path, r###"
|
||||
let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific.zzz", r###"
|
||||
name: specific1
|
||||
|
||||
exclude_parent_matches: true
|
||||
exclude_default_matches: true
|
||||
|
||||
matches:
|
||||
- trigger: "hello"
|
||||
|
@ -696,4 +794,196 @@ mod tests {
|
|||
let config_set = ConfigSet::load(tmp_dir.path()).unwrap();
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::{NamedTempFile, TempDir};
|
||||
use crate::config::{DEFAULT_CONFIG_FILE_NAME, DEFAULT_CONFIG_FILE_CONTENT};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
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 {
|
||||
title: RefCell<String>,
|
||||
|
@ -252,18 +251,18 @@ mod tests {
|
|||
fn test_runtime_constructor_regex_load_correctly() {
|
||||
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
|
||||
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
|
||||
filter_title: "Yeah"
|
||||
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
|
||||
filter_title: "Nice"
|
||||
"###);
|
||||
|
@ -303,18 +302,18 @@ mod tests {
|
|||
fn test_runtime_constructor_malformed_regexes_are_ignored() {
|
||||
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
|
||||
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
|
||||
filter_title: "[`-_]"
|
||||
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
|
||||
filter_title: "Nice"
|
||||
"###);
|
||||
|
@ -354,7 +353,7 @@ mod tests {
|
|||
fn test_runtime_calculate_active_config_specific_title_match() {
|
||||
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
|
||||
filter_title: "Chrome"
|
||||
"###);
|
||||
|
@ -372,7 +371,7 @@ mod tests {
|
|||
fn test_runtime_calculate_active_config_specific_class_match() {
|
||||
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
|
||||
filter_class: "Chrome"
|
||||
"###);
|
||||
|
@ -390,7 +389,7 @@ mod tests {
|
|||
fn test_runtime_calculate_active_config_specific_exec_match() {
|
||||
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
|
||||
filter_exec: "chrome.exe"
|
||||
"###);
|
||||
|
@ -408,7 +407,7 @@ mod tests {
|
|||
fn test_runtime_calculate_active_config_specific_multi_filter_match() {
|
||||
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
|
||||
filter_class: Browser
|
||||
filter_exec: "firefox.exe"
|
||||
|
@ -428,7 +427,7 @@ mod tests {
|
|||
fn test_runtime_calculate_active_config_no_match() {
|
||||
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
|
||||
filter_title: "Firefox"
|
||||
"###);
|
||||
|
@ -447,7 +446,7 @@ mod tests {
|
|||
fn test_runtime_active_config_cache() {
|
||||
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
|
||||
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::protocol::*;
|
||||
use std::io::{BufReader, BufRead};
|
||||
use crate::package::default::DefaultPackageManager;
|
||||
use crate::package::{PackageManager, InstallResult, UpdateResult, RemoveResult};
|
||||
|
||||
mod ui;
|
||||
mod event;
|
||||
mod check;
|
||||
mod utils;
|
||||
mod bridge;
|
||||
mod engine;
|
||||
mod config;
|
||||
mod system;
|
||||
mod sysdaemon;
|
||||
mod context;
|
||||
mod matcher;
|
||||
mod package;
|
||||
mod keyboard;
|
||||
mod protocol;
|
||||
mod clipboard;
|
||||
mod extension;
|
||||
mod sysdaemon;
|
||||
|
||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
const LOG_FILE: &str = "espanso.log";
|
||||
|
||||
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)
|
||||
.author("Federico Terzi")
|
||||
.about("Cross-platform Text Expander written in Rust")
|
||||
|
@ -71,7 +80,7 @@ fn main() {
|
|||
.short("c")
|
||||
.long("config")
|
||||
.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))
|
||||
.arg(Arg::with_name("v")
|
||||
.short("v")
|
||||
|
@ -108,7 +117,26 @@ fn main() {
|
|||
.about("Restart the espanso daemon."))
|
||||
.subcommand(SubCommand::with_name("status")
|
||||
.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;
|
||||
|
||||
|
@ -190,8 +218,32 @@ fn main() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Defaults to start subcommand
|
||||
start_main(config_set);
|
||||
if let Some(matches) = matches.subcommand_matches("install") {
|
||||
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
|
||||
|
@ -547,6 +599,142 @@ fn unregister_main(config_set: ConfigSet) {
|
|||
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> {
|
||||
let espanso_dir = context::get_data_dir();
|
||||
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
|
||||
# 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
|
||||
# it gets replaced by the "replace" string.
|
||||
|
@ -27,34 +27,4 @@ matches:
|
|||
- name: output
|
||||
type: shell
|
||||
params:
|
||||
cmd: "echo Hello from you 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: "É"
|
||||
cmd: "echo Hello from your shell"
|
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