commit
						5e4ac5a4c2
					
				
							
								
								
									
										2
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							|  | @ -329,7 +329,7 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "espanso" | ||||
| version = "0.2.4" | ||||
| version = "0.3.0" | ||||
| dependencies = [ | ||||
|  "backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| [package] | ||||
| name = "espanso" | ||||
| version = "0.2.4" | ||||
| version = "0.3.0" | ||||
| authors = ["Federico Terzi <federicoterzi96@gmail.com>"] | ||||
| license = "GPL-3.0" | ||||
| description = "Cross-platform Text Expander written in Rust" | ||||
|  |  | |||
|  | @ -48,6 +48,14 @@ please consider making a small donation, it really helps :) | |||
| 
 | ||||
| [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FHNLR5DRS267E&source=url) | ||||
| 
 | ||||
| ## Contributors | ||||
| 
 | ||||
| Many people helped the project along the way, thanks to all of you. In particular, I want to thank:  | ||||
| 
 | ||||
| * [Scrumplex](https://scrumplex.net/) - Official AUR repo mantainer and Linux Guru | ||||
| * [Luca Antognetti](https://github.com/luca-ant) - Linux and Windows Tester | ||||
| * [Matteo Pellegrino](https://www.matteopellegrino.me/) - MacOS Tester | ||||
| 
 | ||||
| ## Remarks | ||||
| 
 | ||||
| * Special thanks to the [ModifyPath](https://www.legroom.net/software/modpath) | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ steps: | |||
|       set -e | ||||
|       python packager.py build | ||||
|       cp target/packager/mac/espanso-*.gz . | ||||
|       cp target/packager/mac/espanso-*.txt . | ||||
|       cp target/packager/mac/espanso.rb . | ||||
|       ls -la | ||||
|     displayName: "Cargo build and packaging for MacOS" | ||||
|  | @ -12,6 +12,7 @@ steps: | |||
|   - script: | | ||||
|       python packager.py build | ||||
|       copy "target\\packager\\win\\espanso-win-installer.exe" "espanso-win-installer.exe" | ||||
|       copy "target\\packager\\win\\espanso-win-installer-sha256.txt" "espanso-win-installer-sha256.txt" | ||||
|       dir | ||||
|     displayName: "Build and packaging for Windows" | ||||
| 
 | ||||
|  |  | |||
|  | @ -84,6 +84,16 @@ void register_keypress_callback(KeypressCallback callback) { | |||
|     keypress_callback = callback; | ||||
| } | ||||
| 
 | ||||
| int32_t check_x11() { | ||||
|     Display *check_disp = XOpenDisplay(NULL); | ||||
| 
 | ||||
|     if (!check_disp) { | ||||
|         return -1; | ||||
|     } | ||||
| 
 | ||||
|     XCloseDisplay(check_disp); | ||||
|     return 1; | ||||
| } | ||||
| 
 | ||||
| int32_t initialize(void * _context_instance) { | ||||
|     setlocale(LC_ALL, ""); | ||||
|  | @ -139,14 +149,53 @@ int32_t initialize(void * _context_instance) { | |||
|         return -5; | ||||
|     } | ||||
| 
 | ||||
|     if (!XRecordEnableContextAsync(data_disp, context, event_callback, NULL)) { | ||||
|         return -6; | ||||
|     } | ||||
| 
 | ||||
|     xdo_context = xdo_new(NULL); | ||||
| 
 | ||||
|     /**
 | ||||
|      * Note: We might never get a MappingNotify event if the | ||||
|      * modifier and keymap information was never cached in Xlib. | ||||
|      * The next line makes sure that this happens initially. | ||||
|      */ | ||||
|     XKeysymToKeycode(ctrl_disp, XK_F1); | ||||
| 
 | ||||
|     return 1; | ||||
| } | ||||
| 
 | ||||
| int32_t eventloop() { | ||||
|     if (!XRecordEnableContext (data_disp, context, event_callback, NULL)) { | ||||
|         return -1; | ||||
|     bool running = true; | ||||
| 
 | ||||
|     int ctrl_fd = XConnectionNumber(ctrl_disp); | ||||
|     int data_fd = XConnectionNumber(data_disp); | ||||
| 
 | ||||
|     while (running) | ||||
|     { | ||||
|         fd_set fds; | ||||
|         FD_ZERO(&fds); | ||||
|         FD_SET(ctrl_fd, &fds); | ||||
|         FD_SET(data_fd, &fds); | ||||
|         timeval timeout; | ||||
|         timeout.tv_sec = 2; | ||||
|         timeout.tv_usec = 0; | ||||
|         int retval = select(max(ctrl_fd, data_fd) + 1, | ||||
|                             &fds, NULL, NULL, &timeout); | ||||
| 
 | ||||
|         if (FD_ISSET(data_fd, &fds)) { | ||||
|             XRecordProcessReplies(data_disp); | ||||
|         } | ||||
|         if (FD_ISSET(ctrl_fd, &fds)) { | ||||
|             XEvent event; | ||||
|             XNextEvent(ctrl_disp, &event); | ||||
|             if (event.type == MappingNotify) { | ||||
|                 XMappingEvent *e = (XMappingEvent *) &event; | ||||
|                 if (e->request == MappingKeyboard) { | ||||
|                     XRefreshKeyboardMapping(e); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return 1; | ||||
|  | @ -395,4 +444,4 @@ int32_t is_current_window_terminal() { | |||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| } | ||||
|  | @ -24,6 +24,11 @@ | |||
| 
 | ||||
| extern void * context_instance; | ||||
| 
 | ||||
| /*
 | ||||
|  * Check if the X11 context is available | ||||
|  */ | ||||
| extern "C" int32_t check_x11(); | ||||
| 
 | ||||
| /*
 | ||||
|  * Initialize the X11 context and parameters | ||||
|  */ | ||||
|  |  | |||
|  | @ -250,7 +250,7 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR | |||
|                     // We need to call the callback in two different ways based on the type of key
 | ||||
|                     // The only modifier we use that has a result > 0 is the BACKSPACE, so we have to consider it.
 | ||||
|                     if (result >= 1 && raw->data.keyboard.VKey != VK_BACK) { | ||||
|                         keypress_callback(manager_instance, reinterpret_cast<int32_t*>(buffer.data()), buffer.size(), 0, raw->data.keyboard.VKey, is_key_down); | ||||
|                         keypress_callback(manager_instance, reinterpret_cast<uint16_t*>(buffer.data()), buffer.size(), 0, raw->data.keyboard.VKey, is_key_down); | ||||
|                     }else{ | ||||
|                         keypress_callback(manager_instance, nullptr, 0, 1, raw->data.keyboard.VKey, is_key_down); | ||||
|                     } | ||||
|  |  | |||
|  | @ -39,7 +39,7 @@ extern "C" int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_pat | |||
|  * Called when a new keypress is made, the first argument is an int array, | ||||
|  * while the second is the size of the array. | ||||
|  */ | ||||
| typedef void (*KeypressCallback)(void * self, int32_t *buffer, int32_t len, int32_t is_modifier, int32_t key_code, int32_t is_key_down); | ||||
| typedef void (*KeypressCallback)(void * self, uint16_t *buffer, int32_t len, int32_t is_modifier, int32_t key_code, int32_t is_key_down); | ||||
| extern KeypressCallback keypress_callback; | ||||
| 
 | ||||
| /*
 | ||||
|  |  | |||
							
								
								
									
										23
									
								
								packager.py
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								packager.py
									
									
									
									
									
								
							|  | @ -6,6 +6,7 @@ import hashlib | |||
| import click | ||||
| import shutil | ||||
| import toml | ||||
| import hashlib | ||||
| import urllib.request | ||||
| from dataclasses import dataclass | ||||
| 
 | ||||
|  | @ -106,6 +107,17 @@ def build_windows(package_info): | |||
|     print("Compiling installer with Inno setup") | ||||
|     subprocess.run(["iscc", os.path.abspath(os.path.join(TARGET_DIR, "setupscript.iss"))]) | ||||
| 
 | ||||
|     print("Calculating the SHA256") | ||||
|     sha256_hash = hashlib.sha256() | ||||
|     with open(os.path.abspath(os.path.join(TARGET_DIR, INSTALLER_NAME+".exe")),"rb") as f: | ||||
|         # Read and update hash string value in blocks of 4K | ||||
|         for byte_block in iter(lambda: f.read(4096),b""): | ||||
|             sha256_hash.update(byte_block) | ||||
| 
 | ||||
|         hash_file = os.path.abspath(os.path.join(TARGET_DIR, "espanso-win-installer-sha256.txt")) | ||||
|         with open(hash_file, "w") as hf: | ||||
|             hf.write(sha256_hash.hexdigest()) | ||||
| 
 | ||||
| 
 | ||||
| def build_mac(package_info): | ||||
|     print("Starting packaging process for MacOS...") | ||||
|  | @ -131,6 +143,17 @@ def build_mac(package_info): | |||
|                     ]) | ||||
|     print(f"Created archive: {archive_target}") | ||||
| 
 | ||||
|     print("Calculating the SHA256") | ||||
|     sha256_hash = hashlib.sha256() | ||||
|     with open(archive_target,"rb") as f: | ||||
|         # Read and update hash string value in blocks of 4K | ||||
|         for byte_block in iter(lambda: f.read(4096),b""): | ||||
|             sha256_hash.update(byte_block) | ||||
| 
 | ||||
|         hash_file = os.path.abspath(os.path.join(TARGET_DIR, "espanso-mac-sha256.txt")) | ||||
|         with open(hash_file, "w") as hf: | ||||
|             hf.write(sha256_hash.hexdigest()) | ||||
| 
 | ||||
|     print("Processing Homebrew formula template") | ||||
|     with open("packager/mac/espanso.rb", "r") as formula_template: | ||||
|         content = formula_template.read() | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ use std::os::raw::{c_void, c_char}; | |||
| #[allow(improper_ctypes)] | ||||
| #[link(name="linuxbridge", kind="static")] | ||||
| extern { | ||||
|     pub fn check_x11() -> i32; | ||||
|     pub fn initialize(s: *const c_void) -> i32; | ||||
|     pub fn eventloop(); | ||||
|     pub fn cleanup(); | ||||
|  |  | |||
|  | @ -49,7 +49,7 @@ extern { | |||
|     pub fn set_clipboard(payload: *const u16) -> i32; | ||||
| 
 | ||||
|     // KEYBOARD
 | ||||
|     pub fn register_keypress_callback(cb: extern fn(_self: *mut c_void, *const i32, | ||||
|     pub fn register_keypress_callback(cb: extern fn(_self: *mut c_void, *const u16, | ||||
|                                                 i32, i32, i32, i32)); | ||||
| 
 | ||||
|     pub fn eventloop(); | ||||
|  |  | |||
|  | @ -54,6 +54,12 @@ impl super::ClipboardManager for LinuxClipboardManager { | |||
|                 if let Err(e) = res { | ||||
|                     error!("Could not set clipboard: {}", e); | ||||
|                 } | ||||
| 
 | ||||
|                 let res = child.wait(); | ||||
| 
 | ||||
|                 if let Err(e) = res { | ||||
|                     error!("Could not set clipboard: {}", e); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -38,7 +38,6 @@ const DEFAULT_CONFIG_FILE_CONTENT : &str = include_str!("../res/config.yml"); | |||
| 
 | ||||
| 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() } | ||||
|  | @ -149,9 +148,22 @@ pub enum BackendType { | |||
|     Clipboard | ||||
| } | ||||
| impl Default for BackendType { | ||||
|     // The default backend varies based on the operating system.
 | ||||
|     // On Windows and macOS, the Inject backend is working great and should
 | ||||
|     // be preferred as it doesn't override the clipboard.
 | ||||
|     // On the other hand, on linux it has many problems due to the bugs
 | ||||
|     // of the libxdo used. For this reason, Clipboard will be the default
 | ||||
|     // backend on Linux from version v0.3.0
 | ||||
| 
 | ||||
|     #[cfg(not(target_os = "linux"))] | ||||
|     fn default() -> Self { | ||||
|         BackendType::Inject | ||||
|     } | ||||
| 
 | ||||
|     #[cfg(target_os = "linux")] | ||||
|     fn default() -> Self { | ||||
|         BackendType::Clipboard | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Configs { | ||||
|  | @ -212,26 +224,25 @@ pub struct ConfigSet { | |||
| } | ||||
| 
 | ||||
| impl ConfigSet { | ||||
|     pub fn load(dir_path: &Path) -> Result<ConfigSet, ConfigLoadError> { | ||||
|         if !dir_path.is_dir() { | ||||
|     pub fn load(config_dir: &Path, package_dir: &Path) -> Result<ConfigSet, ConfigLoadError> { | ||||
|         if !config_dir.is_dir() { | ||||
|             return Err(ConfigLoadError::InvalidConfigDirectory) | ||||
|         } | ||||
| 
 | ||||
|         // Load default configuration
 | ||||
|         let default_file = dir_path.join(DEFAULT_CONFIG_FILE_NAME); | ||||
|         let default_file = config_dir.join(DEFAULT_CONFIG_FILE_NAME); | ||||
|         let default = Configs::load_config(default_file.as_path())?; | ||||
| 
 | ||||
|         // Analyze which config files has to be loaded
 | ||||
| 
 | ||||
|         let mut target_files = Vec::new(); | ||||
| 
 | ||||
|         let specific_dir = dir_path.join(USER_CONFIGS_FOLDER_NAME); | ||||
|         let specific_dir = config_dir.join(USER_CONFIGS_FOLDER_NAME); | ||||
|         if specific_dir.exists() { | ||||
|             let dir_entry = WalkDir::new(specific_dir); | ||||
|             target_files.extend(dir_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); | ||||
|  | @ -320,54 +331,40 @@ impl ConfigSet { | |||
|     } | ||||
| 
 | ||||
|     pub fn load_default() -> Result<ConfigSet, ConfigLoadError> { | ||||
|         let espanso_dir = ConfigSet::get_default_config_dir(); | ||||
|         // Configuration related
 | ||||
| 
 | ||||
|         // Create the espanso dir if id doesn't exist
 | ||||
|         let res = create_dir_all(espanso_dir.as_path()); | ||||
|         let config_dir = crate::context::get_config_dir(); | ||||
| 
 | ||||
|         if res.is_ok() { | ||||
|             let default_file = espanso_dir.join(DEFAULT_CONFIG_FILE_NAME); | ||||
|         let default_file = config_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) | ||||
|                 } | ||||
|         // 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()) | ||||
|         } | ||||
| 
 | ||||
|         Err(ConfigLoadError::UnableToCreateDefaultConfig) | ||||
|     } | ||||
|         // Create auxiliary directories
 | ||||
| 
 | ||||
|     pub fn get_default_config_dir() -> PathBuf { | ||||
|         let home_dir = dirs::home_dir().expect("Unable to get home directory"); | ||||
|         home_dir.join(".espanso") | ||||
|     } | ||||
|         let user_config_dir = config_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) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     pub fn get_default_packages_dir() -> PathBuf { | ||||
|         let espanso_dir = ConfigSet::get_default_config_dir(); | ||||
|         espanso_dir.join(PACKAGES_FOLDER_NAME) | ||||
| 
 | ||||
|         // Packages
 | ||||
| 
 | ||||
|         let package_dir = crate::context::get_package_dir(); | ||||
|         let res = create_dir_all(package_dir.as_path()); | ||||
|         if res.is_err() { | ||||
|             return Err(ConfigLoadError::UnableToCreateDefaultConfig)  // TODO: change error type
 | ||||
|         } | ||||
| 
 | ||||
|         return ConfigSet::load(config_dir.as_path(), package_dir.as_path()); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -547,94 +544,18 @@ mod tests { | |||
| 
 | ||||
|     // Test ConfigSet
 | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_default_content_should_work_correctly() { | ||||
|         let tmp_dir = TempDir::new().expect("unable to create temp directory"); | ||||
|         let default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME); | ||||
|         fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT); | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()); | ||||
|         assert!(config_set.is_ok()); | ||||
|     pub fn create_temp_espanso_directories() -> (TempDir, TempDir) { | ||||
|         create_temp_espanso_directories_with_default_content(DEFAULT_CONFIG_FILE_CONTENT) | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_load_fail_bad_directory() { | ||||
|         let config_set = ConfigSet::load(Path::new("invalid/path")); | ||||
|         assert_eq!(config_set.is_err(), true); | ||||
|         assert_eq!(config_set.unwrap_err(), ConfigLoadError::InvalidConfigDirectory); | ||||
|     } | ||||
|     pub fn create_temp_espanso_directories_with_default_content(default_content: &str) -> (TempDir, TempDir) { | ||||
|         let data_dir = TempDir::new().expect("unable to create data directory"); | ||||
|         let package_dir = TempDir::new().expect("unable to create package directory"); | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_missing_default_file() { | ||||
|         let tmp_dir = TempDir::new().expect("unable to create temp directory"); | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()); | ||||
|         assert_eq!(config_set.is_err(), true); | ||||
|         assert_eq!(config_set.unwrap_err(), ConfigLoadError::FileNotFound); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_invalid_yaml_syntax() { | ||||
|         let tmp_dir = TempDir::new().expect("unable to create temp directory"); | ||||
|         let default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME); | ||||
|         let default_path_copy = default_path.clone(); | ||||
|         fs::write(default_path, TEST_CONFIG_FILE_WITH_BAD_YAML); | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()); | ||||
|         match config_set { | ||||
|             Ok(_) => {assert!(false)}, | ||||
|             Err(e) => { | ||||
|                 match e { | ||||
|                     ConfigLoadError::InvalidYAML(p, _) => assert_eq!(p, default_path_copy), | ||||
|                     _ => assert!(false), | ||||
|                 } | ||||
|                 assert!(true); | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_specific_file_with_reserved_fields() { | ||||
|         let tmp_dir = TempDir::new().expect("unable to create temp directory"); | ||||
|         let default_path = tmp_dir.path().join(DEFAULT_CONFIG_FILE_NAME); | ||||
|         fs::write(default_path, DEFAULT_CONFIG_FILE_CONTENT); | ||||
| 
 | ||||
|         let 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(user_defined_path_copy)) | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     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 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_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); | ||||
|         let default_path = data_dir.path().join(DEFAULT_CONFIG_FILE_NAME); | ||||
|         fs::write(default_path, default_content); | ||||
| 
 | ||||
|         tmp_dir | ||||
|         (data_dir, package_dir) | ||||
|     } | ||||
| 
 | ||||
|     pub fn create_temp_file_in_dir(tmp_dir: &PathBuf, name: &str, content: &str) -> PathBuf { | ||||
|  | @ -654,9 +575,8 @@ mod tests { | |||
|         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); | ||||
|     pub fn create_package_file(package_data_dir: &Path, package_name: &str, filename: &str, content: &str) -> PathBuf { | ||||
|         let package_dir = package_data_dir.join(package_name); | ||||
|         if !package_dir.exists() { | ||||
|             create_dir_all(&package_dir); | ||||
|         } | ||||
|  | @ -664,28 +584,99 @@ mod tests { | |||
|         create_temp_file_in_dir(&package_dir, filename, content) | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_default_content_should_work_correctly() { | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert!(config_set.is_ok()); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_load_fail_bad_directory() { | ||||
|         let config_set = ConfigSet::load(Path::new("invalid/path"), Path::new("invalid/path")); | ||||
|         assert_eq!(config_set.is_err(), true); | ||||
|         assert_eq!(config_set.unwrap_err(), ConfigLoadError::InvalidConfigDirectory); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_missing_default_file() { | ||||
|         let data_dir = TempDir::new().expect("unable to create temp directory"); | ||||
|         let package_dir = TempDir::new().expect("unable to create package directory"); | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert_eq!(config_set.is_err(), true); | ||||
|         assert_eq!(config_set.unwrap_err(), ConfigLoadError::FileNotFound); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_invalid_yaml_syntax() { | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( | ||||
|             TEST_CONFIG_FILE_WITH_BAD_YAML | ||||
|         ); | ||||
|         let default_path = data_dir.path().join(DEFAULT_CONFIG_FILE_NAME); | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         match config_set { | ||||
|             Ok(_) => {assert!(false)}, | ||||
|             Err(e) => { | ||||
|                 match e { | ||||
|                     ConfigLoadError::InvalidYAML(p, _) => assert_eq!(p, default_path), | ||||
|                     _ => assert!(false), | ||||
|                 } | ||||
|                 assert!(true); | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_specific_file_with_reserved_fields() { | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###" | ||||
|         config_caching_interval: 10000 | ||||
|         "###);
 | ||||
|         let user_defined_path_copy = user_defined_path.clone(); | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert!(config_set.is_err()); | ||||
|         assert_eq!(config_set.unwrap_err(), ConfigLoadError::InvalidParameter(user_defined_path_copy)) | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_specific_file_missing_name_auto_generated() { | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###" | ||||
|         backend: Clipboard | ||||
|         "###);
 | ||||
|         let user_defined_path_copy = user_defined_path.clone(); | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert!(config_set.is_ok()); | ||||
|         assert_eq!(config_set.unwrap().specific[0].name, user_defined_path_copy.to_str().unwrap_or_default()) | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_specific_file_duplicate_name() { | ||||
|         let tmp_dir = create_temp_espanso_directory(); | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###" | ||||
|         let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###" | ||||
|         name: specific1 | ||||
|         "###);
 | ||||
| 
 | ||||
|         let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific2.yml", r###" | ||||
|         let user_defined_path2 = create_user_config_file(data_dir.path(), "specific2.yml", r###" | ||||
|         name: specific1 | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert!(config_set.is_err()); | ||||
|         assert!(variant_eq(&config_set.unwrap_err(), &ConfigLoadError::NameDuplicate(PathBuf::new()))) | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     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###" | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###" | ||||
|         matches: | ||||
|             - trigger: ":lol" | ||||
|               replace: "LOL" | ||||
|  | @ -693,7 +684,7 @@ mod tests { | |||
|               replace: "Bob" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let user_defined_path = create_user_config_file(tmp_dir.path(), "specific1.yml", r###" | ||||
|         let user_defined_path = create_user_config_file(data_dir.path(), "specific1.yml", r###" | ||||
|         name: specific1 | ||||
| 
 | ||||
|         matches: | ||||
|  | @ -701,7 +692,7 @@ mod tests { | |||
|               replace: "newstring" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()).unwrap(); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); | ||||
|         assert_eq!(config_set.default.matches.len(), 2); | ||||
|         assert_eq!(config_set.specific[0].matches.len(), 3); | ||||
| 
 | ||||
|  | @ -712,9 +703,7 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     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###" | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###" | ||||
|         matches: | ||||
|             - trigger: ":lol" | ||||
|               replace: "LOL" | ||||
|  | @ -722,7 +711,7 @@ mod tests { | |||
|               replace: "Bob" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific2.yml", r###" | ||||
|         let user_defined_path2 = create_user_config_file(data_dir.path(), "specific2.yml", r###" | ||||
|         name: specific1 | ||||
| 
 | ||||
|         matches: | ||||
|  | @ -730,7 +719,7 @@ mod tests { | |||
|               replace: "newstring" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()).unwrap(); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); | ||||
|         assert_eq!(config_set.default.matches.len(), 2); | ||||
|         assert_eq!(config_set.specific[0].matches.len(), 2); | ||||
| 
 | ||||
|  | @ -740,9 +729,7 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     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###" | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###" | ||||
|         matches: | ||||
|             - trigger: ":lol" | ||||
|               replace: "LOL" | ||||
|  | @ -750,7 +737,7 @@ mod tests { | |||
|               replace: "Bob" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific2.yml", r###" | ||||
|         let user_defined_path2 = create_user_config_file(data_dir.path(), "specific2.yml", r###" | ||||
|         name: specific1 | ||||
| 
 | ||||
|         exclude_default_matches: true | ||||
|  | @ -760,7 +747,7 @@ mod tests { | |||
|               replace: "newstring" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()).unwrap(); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); | ||||
|         assert_eq!(config_set.default.matches.len(), 2); | ||||
|         assert_eq!(config_set.specific[0].matches.len(), 1); | ||||
| 
 | ||||
|  | @ -769,17 +756,17 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_only_yaml_files_are_loaded_from_config() { | ||||
|         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###" | ||||
|         matches: | ||||
|             - trigger: ":lol" | ||||
|               replace: "LOL" | ||||
|             - trigger: ":yess" | ||||
|               replace: "Bob" | ||||
|         "###);
 | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( | ||||
|             r###" | ||||
|             matches: | ||||
|                 - trigger: ":lol" | ||||
|                   replace: "LOL" | ||||
|                 - trigger: ":yess" | ||||
|                   replace: "Bob" | ||||
|             "###
 | ||||
|         ); | ||||
| 
 | ||||
|         let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific.zzz", r###" | ||||
|         let user_defined_path2 = create_user_config_file(data_dir.path(), "specific.zzz", r###" | ||||
|         name: specific1 | ||||
| 
 | ||||
|         exclude_default_matches: true | ||||
|  | @ -789,35 +776,35 @@ mod tests { | |||
|               replace: "newstring" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()).unwrap(); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_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 (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let user_defined_path = create_user_config_file(tmp_dir.path(), "specific.yml", r###" | ||||
|         let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###" | ||||
|         name: specific1 | ||||
|         "###);
 | ||||
| 
 | ||||
|         let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific2.yml", r###" | ||||
|         let user_defined_path2 = create_user_config_file(data_dir.path(), "specific2.yml", r###" | ||||
|         name: specific2 | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()).unwrap(); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_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###" | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories_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###" | ||||
|         let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###" | ||||
|         parent: default | ||||
| 
 | ||||
|         matches: | ||||
|  | @ -825,7 +812,7 @@ mod tests { | |||
|               replace: "world" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()).unwrap(); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_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")); | ||||
|  | @ -834,19 +821,19 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_no_parent_should_not_merge() { | ||||
|         let tmp_dir = create_temp_espanso_directory_with_default_content(r###" | ||||
|         let (data_dir, package_dir)= create_temp_espanso_directories_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###" | ||||
|         let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###" | ||||
|         matches: | ||||
|             - trigger: "hello" | ||||
|               replace: "world" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()).unwrap(); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_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")); | ||||
|  | @ -856,13 +843,13 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_default_nested_parent_works_correctly() { | ||||
|         let tmp_dir = create_temp_espanso_directory_with_default_content(r###" | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories_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###" | ||||
|         let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###" | ||||
|         name: custom1 | ||||
|         parent: default | ||||
| 
 | ||||
|  | @ -871,7 +858,7 @@ mod tests { | |||
|               replace: "world" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let user_defined_path2 = create_user_config_file(tmp_dir.path(), "specific2.yml", r###" | ||||
|         let user_defined_path2 = create_user_config_file(data_dir.path(), "specific2.yml", r###" | ||||
|         parent: custom1 | ||||
| 
 | ||||
|         matches: | ||||
|  | @ -879,7 +866,7 @@ mod tests { | |||
|               replace: "mario" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()).unwrap(); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_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")); | ||||
|  | @ -889,13 +876,13 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_parent_merge_children_priority_should_be_higher() { | ||||
|         let tmp_dir = create_temp_espanso_directory_with_default_content(r###" | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories_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###" | ||||
|         let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###" | ||||
|         parent: default | ||||
| 
 | ||||
|         matches: | ||||
|  | @ -903,7 +890,7 @@ mod tests { | |||
|               replace: "world" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()).unwrap(); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_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")); | ||||
|  | @ -911,13 +898,13 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_package_configs_default_merge() { | ||||
|         let tmp_dir = create_temp_espanso_directory_with_default_content(r###" | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###" | ||||
|         matches: | ||||
|             - trigger: hasta | ||||
|               replace: Hasta la vista | ||||
|         "###);
 | ||||
| 
 | ||||
|         let package_path = create_package_file(tmp_dir.path(), "package1", "package.yml", r###" | ||||
|         let package_path = create_package_file(package_dir.path(), "package1", "package.yml", r###" | ||||
|         parent: default | ||||
| 
 | ||||
|         matches: | ||||
|  | @ -925,7 +912,7 @@ mod tests { | |||
|               replace: "potter" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()).unwrap(); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_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")); | ||||
|  | @ -934,19 +921,19 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_package_configs_without_merge() { | ||||
|         let tmp_dir = create_temp_espanso_directory_with_default_content(r###" | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###" | ||||
|         matches: | ||||
|             - trigger: hasta | ||||
|               replace: Hasta la vista | ||||
|         "###);
 | ||||
| 
 | ||||
|         let package_path = create_package_file(tmp_dir.path(), "package1", "package.yml", r###" | ||||
|         let package_path = create_package_file(package_dir.path(), "package1", "package.yml", r###" | ||||
|         matches: | ||||
|             - trigger: "harry" | ||||
|               replace: "potter" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()).unwrap(); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_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")); | ||||
|  | @ -955,13 +942,13 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_config_set_package_configs_multiple_files() { | ||||
|         let tmp_dir = create_temp_espanso_directory_with_default_content(r###" | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###" | ||||
|         matches: | ||||
|             - trigger: hasta | ||||
|               replace: Hasta la vista | ||||
|         "###);
 | ||||
| 
 | ||||
|         let package_path = create_package_file(tmp_dir.path(), "package1", "package.yml", r###" | ||||
|         let package_path = create_package_file(package_dir.path(), "package1", "package.yml", r###" | ||||
|         name: package1 | ||||
| 
 | ||||
|         matches: | ||||
|  | @ -969,7 +956,7 @@ mod tests { | |||
|               replace: "potter" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let package_path2 = create_package_file(tmp_dir.path(), "package1", "addon.yml", r###" | ||||
|         let package_path2 = create_package_file(package_dir.path(), "package1", "addon.yml", r###" | ||||
|         parent: package1 | ||||
| 
 | ||||
|         matches: | ||||
|  | @ -977,7 +964,7 @@ mod tests { | |||
|               replace: "weasley" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()).unwrap(); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_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")); | ||||
|  |  | |||
|  | @ -209,7 +209,7 @@ mod tests { | |||
|     use std::fs; | ||||
|     use std::path::PathBuf; | ||||
|     use crate::config::ConfigManager; | ||||
|     use crate::config::tests::{create_temp_espanso_directory, create_temp_file_in_dir, create_user_config_file}; | ||||
|     use crate::config::tests::{create_temp_espanso_directories, create_temp_file_in_dir, create_user_config_file}; | ||||
| 
 | ||||
|     struct DummySystemManager { | ||||
|         title: RefCell<String>, | ||||
|  | @ -249,25 +249,25 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_runtime_constructor_regex_load_correctly() { | ||||
|         let tmp_dir = create_temp_espanso_directory(); | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###" | ||||
|         let specific_path = create_user_config_file(&data_dir.path(), "specific.yml", r###" | ||||
|         name: myname1 | ||||
|         filter_exec: "Title" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let specific_path2 = create_user_config_file(&tmp_dir.path(), "specific2.yml", r###" | ||||
|         let specific_path2 = create_user_config_file(&data_dir.path(), "specific2.yml", r###" | ||||
|         name: myname2 | ||||
|         filter_title: "Yeah" | ||||
|         filter_class: "Car" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let specific_path3 = create_user_config_file(&tmp_dir.path(), "specific3.yml", r###" | ||||
|         let specific_path3 = create_user_config_file(&data_dir.path(), "specific3.yml", r###" | ||||
|         name: myname3 | ||||
|         filter_title: "Nice" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert!(config_set.is_ok()); | ||||
| 
 | ||||
|         let dummy_system_manager = DummySystemManager::new(); | ||||
|  | @ -300,25 +300,25 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_runtime_constructor_malformed_regexes_are_ignored() { | ||||
|         let tmp_dir = create_temp_espanso_directory(); | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###" | ||||
|         let specific_path = create_user_config_file(&data_dir.path(), "specific.yml", r###" | ||||
|         name: myname1 | ||||
|         filter_exec: "[`-_]" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let specific_path2 = create_user_config_file(&tmp_dir.path(), "specific2.yml", r###" | ||||
|         let specific_path2 = create_user_config_file(&data_dir.path(), "specific2.yml", r###" | ||||
|         name: myname2 | ||||
|         filter_title: "[`-_]" | ||||
|         filter_class: "Car" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let specific_path3 = create_user_config_file(&tmp_dir.path(), "specific3.yml", r###" | ||||
|         let specific_path3 = create_user_config_file(&data_dir.path(), "specific3.yml", r###" | ||||
|         name: myname3 | ||||
|         filter_title: "Nice" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert!(config_set.is_ok()); | ||||
| 
 | ||||
|         let dummy_system_manager = DummySystemManager::new(); | ||||
|  | @ -351,14 +351,14 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_runtime_calculate_active_config_specific_title_match() { | ||||
|         let tmp_dir = create_temp_espanso_directory(); | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###" | ||||
|         let specific_path = create_user_config_file(&data_dir.path(), "specific.yml", r###" | ||||
|         name: chrome | ||||
|         filter_title: "Chrome" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert!(config_set.is_ok()); | ||||
| 
 | ||||
|         let dummy_system_manager = DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); | ||||
|  | @ -369,14 +369,14 @@ mod tests { | |||
|     } | ||||
| 
 | ||||
|     fn test_runtime_calculate_active_config_specific_class_match() { | ||||
|         let tmp_dir = create_temp_espanso_directory(); | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###" | ||||
|         let specific_path = create_user_config_file(&data_dir.path(), "specific.yml", r###" | ||||
|         name: chrome | ||||
|         filter_class: "Chrome" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert!(config_set.is_ok()); | ||||
| 
 | ||||
|         let dummy_system_manager = DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); | ||||
|  | @ -387,14 +387,14 @@ mod tests { | |||
|     } | ||||
| 
 | ||||
|     fn test_runtime_calculate_active_config_specific_exec_match() { | ||||
|         let tmp_dir = create_temp_espanso_directory(); | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###" | ||||
|         let specific_path = create_user_config_file(&data_dir.path(), "specific.yml", r###" | ||||
|         name: chrome | ||||
|         filter_exec: "chrome.exe" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert!(config_set.is_ok()); | ||||
| 
 | ||||
|         let dummy_system_manager = DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); | ||||
|  | @ -405,15 +405,15 @@ mod tests { | |||
|     } | ||||
| 
 | ||||
|     fn test_runtime_calculate_active_config_specific_multi_filter_match() { | ||||
|         let tmp_dir = create_temp_espanso_directory(); | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###" | ||||
|         let specific_path = create_user_config_file(&data_dir.path(), "specific.yml", r###" | ||||
|         name: chrome | ||||
|         filter_class: Browser | ||||
|         filter_exec: "firefox.exe" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert!(config_set.is_ok()); | ||||
| 
 | ||||
|         let dummy_system_manager = DummySystemManager::new_custom("Google Chrome", "Browser", "C:\\Path\\chrome.exe"); | ||||
|  | @ -425,14 +425,14 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_runtime_calculate_active_config_no_match() { | ||||
|         let tmp_dir = create_temp_espanso_directory(); | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###" | ||||
|         let specific_path = create_user_config_file(&data_dir.path(), "specific.yml", r###" | ||||
|         name: firefox | ||||
|         filter_title: "Firefox" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert!(config_set.is_ok()); | ||||
| 
 | ||||
|         let dummy_system_manager = DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); | ||||
|  | @ -444,14 +444,14 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_runtime_active_config_cache() { | ||||
|         let tmp_dir = create_temp_espanso_directory(); | ||||
|         let (data_dir, package_dir) = create_temp_espanso_directories(); | ||||
| 
 | ||||
|         let specific_path = create_user_config_file(&tmp_dir.path(), "specific.yml", r###" | ||||
|         let specific_path = create_user_config_file(&data_dir.path(), "specific.yml", r###" | ||||
|         name: firefox | ||||
|         filter_title: "Firefox" | ||||
|         "###);
 | ||||
| 
 | ||||
|         let config_set = ConfigSet::load(tmp_dir.path()); | ||||
|         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()); | ||||
|         assert!(config_set.is_ok()); | ||||
| 
 | ||||
|         let dummy_system_manager = DummySystemManager::new_custom("Google Chrome", "Chrome", "C:\\Path\\chrome.exe"); | ||||
|  |  | |||
|  | @ -18,12 +18,14 @@ | |||
|  */ | ||||
| 
 | ||||
| use std::sync::mpsc::Sender; | ||||
| use std::os::raw::c_void; | ||||
| use std::os::raw::{c_void, c_char}; | ||||
| use crate::event::*; | ||||
| use crate::event::KeyModifier::*; | ||||
| use crate::bridge::linux::*; | ||||
| use std::process::exit; | ||||
| use log::error; | ||||
| use log::{error, info}; | ||||
| use std::ffi::CStr; | ||||
| use std::{thread, time}; | ||||
| 
 | ||||
| #[repr(C)] | ||||
| pub struct LinuxContext { | ||||
|  | @ -32,6 +34,16 @@ pub struct LinuxContext { | |||
| 
 | ||||
| impl LinuxContext { | ||||
|     pub fn new(send_channel: Sender<Event>) -> Box<LinuxContext> { | ||||
|         // Check if the X11 context is available
 | ||||
|         let x11_available = unsafe { | ||||
|             check_x11() | ||||
|         }; | ||||
| 
 | ||||
|         if x11_available < 0 { | ||||
|             error!("Error, can't connect to X11 context"); | ||||
|             std::process::exit(100); | ||||
|         } | ||||
| 
 | ||||
|         let context = Box::new(LinuxContext { | ||||
|             send_channel, | ||||
|         }); | ||||
|  | @ -74,14 +86,19 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, | |||
|         let _self = _self as *mut LinuxContext; | ||||
| 
 | ||||
|         if is_modifier == 0 {  // Char event
 | ||||
|             // Convert the received buffer to a character
 | ||||
|             let buffer = std::slice::from_raw_parts(raw_buffer, len as usize); | ||||
|             let r = String::from_utf8_lossy(buffer).chars().nth(0); | ||||
|             // Convert the received buffer to a string
 | ||||
|             let c_str = CStr::from_ptr(raw_buffer as (*const c_char)); | ||||
|             let char_str = c_str.to_str(); | ||||
| 
 | ||||
|             // Send the char through the channel
 | ||||
|             if let Some(c) = r { | ||||
|                 let event = Event::Key(KeyEvent::Char(c)); | ||||
|                 (*_self).send_channel.send(event).unwrap(); | ||||
|             match char_str { | ||||
|                 Ok(char_str) => { | ||||
|                     let event = Event::Key(KeyEvent::Char(char_str.to_owned())); | ||||
|                     (*_self).send_channel.send(event).unwrap(); | ||||
|                 }, | ||||
|                 Err(e) => { | ||||
|                     error!("Unable to receive char: {}",e); | ||||
|                 }, | ||||
|             } | ||||
|         }else{  // Modifier event
 | ||||
|             let modifier: Option<KeyModifier> = match key_code { | ||||
|  |  | |||
|  | @ -18,11 +18,11 @@ | |||
|  */ | ||||
| 
 | ||||
| use std::sync::mpsc::Sender; | ||||
| use std::os::raw::c_void; | ||||
| use std::os::raw::{c_void, c_char}; | ||||
| use crate::bridge::macos::*; | ||||
| use crate::event::{Event, KeyEvent, KeyModifier, ActionType}; | ||||
| use crate::event::KeyModifier::*; | ||||
| use std::ffi::CString; | ||||
| use std::ffi::{CString, CStr}; | ||||
| use std::fs; | ||||
| use log::{info, error}; | ||||
| use std::process::exit; | ||||
|  | @ -94,14 +94,19 @@ extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, | |||
|         let _self = _self as *mut MacContext; | ||||
| 
 | ||||
|         if is_modifier == 0 {  // Char event
 | ||||
|             // Convert the received buffer to a character
 | ||||
|             let buffer = std::slice::from_raw_parts(raw_buffer, len as usize); | ||||
|             let r = String::from_utf8_lossy(buffer).chars().nth(0); | ||||
|             // Convert the received buffer to a string
 | ||||
|             let c_str = CStr::from_ptr(raw_buffer as (*const c_char)); | ||||
|             let char_str = c_str.to_str(); | ||||
| 
 | ||||
|             // Send the char through the channel
 | ||||
|             if let Some(c) = r { | ||||
|                 let event = Event::Key(KeyEvent::Char(c)); | ||||
|                 (*_self).send_channel.send(event).unwrap(); | ||||
|             match char_str { | ||||
|                 Ok(char_str) => { | ||||
|                     let event = Event::Key(KeyEvent::Char(char_str.to_owned())); | ||||
|                     (*_self).send_channel.send(event).unwrap(); | ||||
|                 }, | ||||
|                 Err(e) => { | ||||
|                     error!("Unable to receive char: {}",e); | ||||
|                 }, | ||||
|             } | ||||
|         }else{  // Modifier event
 | ||||
|             let modifier: Option<KeyModifier> = match key_code { | ||||
|  |  | |||
|  | @ -35,13 +35,6 @@ pub trait Context { | |||
|     fn eventloop(&self); | ||||
| } | ||||
| 
 | ||||
| pub fn get_data_dir() -> PathBuf { | ||||
|     let data_dir = dirs::data_dir().expect("Can't obtain data_dir(), terminating."); | ||||
|     let espanso_dir = data_dir.join("espanso"); | ||||
|     create_dir_all(&espanso_dir).expect("Error creating espanso data directory"); | ||||
|     espanso_dir | ||||
| } | ||||
| 
 | ||||
| // MAC IMPLEMENTATION
 | ||||
| #[cfg(target_os = "macos")] | ||||
| pub fn new(send_channel: Sender<Event>) -> Box<dyn Context> { | ||||
|  | @ -58,4 +51,62 @@ pub fn new(send_channel: Sender<Event>) -> Box<dyn Context> { | |||
| #[cfg(target_os = "windows")] | ||||
| pub fn new(send_channel: Sender<Event>) -> Box<dyn Context> { | ||||
|     windows::WindowsContext::new(send_channel) | ||||
| } | ||||
| 
 | ||||
| // espanso directories
 | ||||
| 
 | ||||
| pub fn get_data_dir() -> PathBuf { | ||||
|     let data_dir = dirs::data_local_dir().expect("Can't obtain data_local_dir(), terminating."); | ||||
|     let espanso_dir = data_dir.join("espanso"); | ||||
|     create_dir_all(&espanso_dir).expect("Error creating espanso data directory"); | ||||
|     espanso_dir | ||||
| } | ||||
| 
 | ||||
| pub fn get_config_dir() -> PathBuf { | ||||
|     // Portable mode check
 | ||||
|     // Get the espanso executable path
 | ||||
|     let espanso_exe_path = std::env::current_exe().expect("Could not get espanso executable path"); | ||||
|     let exe_dir = espanso_exe_path.parent(); | ||||
|     if let Some(parent) = exe_dir { | ||||
|         let config_dir = parent.join(".espanso"); | ||||
|         if config_dir.exists() { | ||||
|             println!("PORTABLE MODE, using config folder: '{}'", config_dir.to_string_lossy()); | ||||
|             return config_dir; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // For compatibility purposes, check if the $HOME/.espanso directory is available
 | ||||
|     let home_dir = dirs::home_dir().expect("Can't obtain the user home directory, terminating."); | ||||
|     let legacy_espanso_dir = home_dir.join(".espanso"); | ||||
|     if legacy_espanso_dir.exists() { | ||||
|         eprintln!("WARNING: using legacy espanso config location in $HOME/.espanso is DEPRECATED"); | ||||
|         eprintln!("Starting from espanso v0.3.0, espanso config location is changed."); | ||||
|         eprintln!("Please check out the documentation to find out more: https://espanso.org/docs/configuration/"); | ||||
| 
 | ||||
|         return legacy_espanso_dir; | ||||
|     } | ||||
| 
 | ||||
|     // New config location, from version v0.3.0
 | ||||
|     // Refer to issue #73 for more information: https://github.com/federico-terzi/espanso/issues/73
 | ||||
|     let config_dir = dirs::config_dir().expect("Can't obtain config_dir(), terminating."); | ||||
|     let espanso_dir = config_dir.join("espanso"); | ||||
|     create_dir_all(&espanso_dir).expect("Error creating espanso config directory"); | ||||
|     espanso_dir | ||||
| } | ||||
| 
 | ||||
| const PACKAGES_FOLDER_NAME : &str = "packages"; | ||||
| 
 | ||||
| pub fn get_package_dir() -> PathBuf { | ||||
|     // Deprecated $HOME/.espanso/packages directory compatibility check
 | ||||
|     let config_dir = get_config_dir(); | ||||
|     let legacy_package_dir = config_dir.join(PACKAGES_FOLDER_NAME); | ||||
|     if legacy_package_dir.exists() { | ||||
|         return legacy_package_dir; | ||||
|     } | ||||
| 
 | ||||
|     // New package location, starting from version v0.3.0
 | ||||
|     let data_dir = get_data_dir(); | ||||
|     let package_dir = data_dir.join(PACKAGES_FOLDER_NAME); | ||||
|     create_dir_all(&package_dir).expect("Error creating espanso packages directory"); | ||||
|     package_dir | ||||
| } | ||||
|  | @ -23,8 +23,8 @@ use crate::event::{Event, KeyEvent, KeyModifier, ActionType}; | |||
| use crate::event::KeyModifier::*; | ||||
| use std::ffi::c_void; | ||||
| use std::{fs}; | ||||
| use widestring::U16CString; | ||||
| use log::{info}; | ||||
| use widestring::{U16CString, U16CStr}; | ||||
| use log::{info, error}; | ||||
| 
 | ||||
| const BMP_BINARY : &[u8] = include_bytes!("../res/win/espanso.bmp"); | ||||
| const ICO_BINARY : &[u8] = include_bytes!("../res/win/espanso.ico"); | ||||
|  | @ -102,20 +102,31 @@ impl super::Context for WindowsContext { | |||
| 
 | ||||
| // Native bridge code
 | ||||
| 
 | ||||
| extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const i32, len: i32, | ||||
| extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u16, len: i32, | ||||
|                             is_modifier: i32, key_code: i32, is_key_down: i32) { | ||||
|     unsafe { | ||||
|         let _self = _self as *mut WindowsContext; | ||||
|         if is_key_down != 0 {  // KEY DOWN EVENT
 | ||||
|             if is_modifier == 0 {  // Char event
 | ||||
|                 // Convert the received buffer to a character
 | ||||
|                 // Convert the received buffer to a string
 | ||||
|                 let buffer = std::slice::from_raw_parts(raw_buffer, len as usize); | ||||
|                 let r = std::char::from_u32(buffer[0] as u32); | ||||
|                 let c_string = U16CStr::from_slice_with_nul(buffer); | ||||
| 
 | ||||
|                 // Send the char through the channel
 | ||||
|                 if let Some(c) = r { | ||||
|                     let event = Event::Key(KeyEvent::Char(c)); | ||||
|                     (*_self).send_channel.send(event).unwrap(); | ||||
|                 if let Ok(c_string) = c_string { | ||||
|                     let string = c_string.to_string(); | ||||
| 
 | ||||
|                     // Send the char through the channel
 | ||||
|                     match string { | ||||
|                         Ok(string) => { | ||||
|                             let event = Event::Key(KeyEvent::Char(string)); | ||||
|                             (*_self).send_channel.send(event).unwrap(); | ||||
|                         }, | ||||
|                         Err(e) => { | ||||
|                             error!("Unable to receive char: {}",e); | ||||
|                         }, | ||||
|                     } | ||||
|                 }else{ | ||||
|                     error!("unable to decode widechar"); | ||||
|                 } | ||||
|             } | ||||
|         }else{  // KEY UP event
 | ||||
|  |  | |||
|  | @ -110,7 +110,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa | |||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         self.keyboard_manager.delete_string(m.trigger.len() as i32); | ||||
|         self.keyboard_manager.delete_string(m.trigger.chars().count() as i32); | ||||
| 
 | ||||
|         let target_string = if m._has_vars { | ||||
|             let mut output_map = HashMap::new(); | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ impl From<i32> for ActionType { | |||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub enum KeyEvent { | ||||
|     Char(char), | ||||
|     Char(String), | ||||
|     Modifier(KeyModifier) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										90
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										90
									
								
								src/main.rs
									
									
									
									
									
								
							|  | @ -22,7 +22,6 @@ extern crate lazy_static; | |||
| 
 | ||||
| use std::thread; | ||||
| use std::fs::{File, OpenOptions}; | ||||
| use std::path::Path; | ||||
| use std::process::exit; | ||||
| use std::sync::mpsc; | ||||
| use std::sync::mpsc::Receiver; | ||||
|  | @ -81,12 +80,6 @@ fn main() { | |||
|         .version(VERSION) | ||||
|         .author("Federico Terzi") | ||||
|         .about("Cross-platform Text Expander written in Rust") | ||||
|         .arg(Arg::with_name("config") | ||||
|             .short("c") | ||||
|             .long("config") | ||||
|             .value_name("FILE") | ||||
|             .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") | ||||
|             .multiple(true) | ||||
|  | @ -109,9 +102,9 @@ fn main() { | |||
|         .subcommand(SubCommand::with_name("daemon") | ||||
|             .about("Start the daemon without spawning a new process.")) | ||||
|         .subcommand(SubCommand::with_name("register") | ||||
|             .about("MacOS only. Register espanso in the system daemon manager.")) | ||||
|             .about("MacOS and Linux only. Register espanso in the system daemon manager.")) | ||||
|         .subcommand(SubCommand::with_name("unregister") | ||||
|             .about("MacOS only. Unregister espanso from the system daemon manager.")) | ||||
|             .about("MacOS and Linux only. Unregister espanso from the system daemon manager.")) | ||||
|         .subcommand(SubCommand::with_name("log") | ||||
|             .about("Print the latest daemon logs.")) | ||||
|         .subcommand(SubCommand::with_name("start") | ||||
|  | @ -122,6 +115,8 @@ fn main() { | |||
|             .about("Restart the espanso daemon.")) | ||||
|         .subcommand(SubCommand::with_name("status") | ||||
|             .about("Check if the espanso daemon is running or not.")) | ||||
|         .subcommand(SubCommand::with_name("path") | ||||
|             .about("Prints all the current espanso directory paths, to easily locate configuration and data paths.")) | ||||
| 
 | ||||
|         // Package manager
 | ||||
|         .subcommand(SubCommand::with_name("package") | ||||
|  | @ -145,20 +140,7 @@ fn main() { | |||
|     let log_level = matches.occurrences_of("v") as i32; | ||||
| 
 | ||||
|     // Load the configuration
 | ||||
|     let mut config_set = match matches.value_of("config") { | ||||
|         None => { | ||||
|             if log_level > 1 { | ||||
|                 println!("loading configuration from default location..."); | ||||
|             } | ||||
|             ConfigSet::load_default() | ||||
|         }, | ||||
|         Some(path) => { | ||||
|             if log_level > 1 { | ||||
|                 println!("loading configuration from custom location: {}", path); | ||||
|             } | ||||
|             ConfigSet::load(Path::new(path)) | ||||
|         }, | ||||
|     }.unwrap_or_else(|e| { | ||||
|     let mut config_set = ConfigSet::load_default().unwrap_or_else(|e| { | ||||
|         println!("{}", e); | ||||
|         exit(1); | ||||
|     }); | ||||
|  | @ -232,6 +214,11 @@ fn main() { | |||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if matches.subcommand_matches("path").is_some() { | ||||
|         path_main(config_set); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if let Some(matches) = matches.subcommand_matches("package") { | ||||
|         if let Some(matches) = matches.subcommand_matches("install") { | ||||
|             install_main(config_set, matches); | ||||
|  | @ -304,6 +291,8 @@ fn daemon_main(config_set: ConfigSet) { | |||
|     log_panics::init(); | ||||
| 
 | ||||
|     info!("espanso version {}", VERSION); | ||||
|     info!("using config path: {}", context::get_config_dir().to_string_lossy()); | ||||
|     info!("using package path: {}", context::get_package_dir().to_string_lossy()); | ||||
|     info!("starting daemon..."); | ||||
| 
 | ||||
|     let (send_channel, receive_channel) = mpsc::channel(); | ||||
|  | @ -393,10 +382,10 @@ fn start_daemon(config_set: ConfigSet) { | |||
|             if status.success() { | ||||
|                 println!("Daemon started correctly!") | ||||
|             }else{ | ||||
|                 println!("Error starting launchd daemon with status: {}", status); | ||||
|                 eprintln!("Error starting launchd daemon with status: {}", status); | ||||
|             } | ||||
|         }else{ | ||||
|             println!("Error starting launchd daemon: {}", res.unwrap_err()); | ||||
|             eprintln!("Error starting launchd daemon: {}", res.unwrap_err()); | ||||
|         } | ||||
|     }else{ | ||||
|         fork_daemon(config_set); | ||||
|  | @ -405,7 +394,50 @@ fn start_daemon(config_set: ConfigSet) { | |||
| 
 | ||||
| #[cfg(target_os = "linux")] | ||||
| fn start_daemon(config_set: ConfigSet) { | ||||
|     fork_daemon(config_set); | ||||
|     if config_set.default.use_system_agent { | ||||
|         use std::process::Command; | ||||
| 
 | ||||
|         // Make sure espanso is currently registered in systemd
 | ||||
|         let res = Command::new("systemctl") | ||||
|             .args(&["--user", "is-enabled", "espanso.service"]) | ||||
|             .status(); | ||||
|         if !res.unwrap().success() { | ||||
|             use std::io::{self, BufRead}; | ||||
|             eprintln!("espanso must be registered to systemd (user level) first."); | ||||
|             eprint!("Do you want to proceed? [Y/n]: "); | ||||
| 
 | ||||
|             let mut line = String::new(); | ||||
|             let stdin = io::stdin(); | ||||
|             stdin.lock().read_line(&mut line).unwrap(); | ||||
|             let answer = line.trim().to_lowercase(); | ||||
|             if answer != "n" { | ||||
|                 register_main(config_set); | ||||
|             }else{ | ||||
|                 eprintln!("Please register espanso to systemd with this command:"); | ||||
|                 eprintln!("   espanso register"); | ||||
|                 // TODO: enable flag to use non-managed daemon mode
 | ||||
| 
 | ||||
|                 std::process::exit(4); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Start the espanso service
 | ||||
|         let res = Command::new("systemctl") | ||||
|             .args(&["--user", "start", "espanso.service"]) | ||||
|             .status(); | ||||
| 
 | ||||
|         if let Ok(status) = res { | ||||
|             if status.success() { | ||||
|                 println!("Daemon started correctly!") | ||||
|             }else{ | ||||
|                 eprintln!("Error starting systemd daemon with status: {}", status); | ||||
|             } | ||||
|         }else{ | ||||
|             eprintln!("Error starting systemd daemon: {}", res.unwrap_err()); | ||||
|         } | ||||
|     }else{ | ||||
|         fork_daemon(config_set); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(not(target_os = "windows"))] | ||||
|  | @ -744,6 +776,12 @@ fn list_package_main(_config_set: ConfigSet, matches: &ArgMatches) { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| fn path_main(_config_set: ConfigSet) { | ||||
|     println!("Config: {}", crate::context::get_config_dir().to_string_lossy()); | ||||
|     println!("Packages: {}", crate::context::get_package_dir().to_string_lossy()); | ||||
|     println!("Data: {}", crate::context::get_config_dir().to_string_lossy()); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| fn acquire_lock() -> Option<File> { | ||||
|     let espanso_dir = context::get_data_dir(); | ||||
|  |  | |||
|  | @ -90,7 +90,7 @@ pub trait MatchReceiver { | |||
| } | ||||
| 
 | ||||
| pub trait Matcher : KeyEventReceiver { | ||||
|     fn handle_char(&self, c: char); | ||||
|     fn handle_char(&self, c: &str); | ||||
|     fn handle_modifier(&self, m: KeyModifier); | ||||
| } | ||||
| 
 | ||||
|  | @ -98,7 +98,7 @@ impl <M: Matcher> KeyEventReceiver for M { | |||
|     fn on_key_event(&self, e: KeyEvent) { | ||||
|         match e { | ||||
|             KeyEvent::Char(c) => { | ||||
|                 self.handle_char(c); | ||||
|                 self.handle_char(&c); | ||||
|             }, | ||||
|             KeyEvent::Modifier(m) => { | ||||
|                 self.handle_modifier(m); | ||||
|  |  | |||
|  | @ -35,6 +35,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> { | |||
| 
 | ||||
| struct MatchEntry<'a> { | ||||
|     start: usize, | ||||
|     count: usize, | ||||
|     _match: &'a Match | ||||
| } | ||||
| 
 | ||||
|  | @ -68,7 +69,7 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> { | |||
| } | ||||
| 
 | ||||
| impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMatcher<'a, R, M> { | ||||
|     fn handle_char(&self, c: char) { | ||||
|     fn handle_char(&self, c: &str) { | ||||
|         // if not enabled, avoid any processing
 | ||||
|         if !*(self.is_enabled.borrow()) { | ||||
|             return; | ||||
|  | @ -77,8 +78,12 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa | |||
|         let mut current_set_queue = self.current_set_queue.borrow_mut(); | ||||
| 
 | ||||
|         let new_matches: Vec<MatchEntry> = self.config_manager.matches().iter() | ||||
|             .filter(|&x| x.trigger.chars().nth(0).unwrap() == c) | ||||
|             .map(|x | MatchEntry{start: 1, _match: &x}) | ||||
|             .filter(|&x| x.trigger.starts_with(c)) | ||||
|             .map(|x | MatchEntry{ | ||||
|                 start: 1, | ||||
|                 count: x.trigger.chars().count(), | ||||
|                 _match: &x | ||||
|             }) | ||||
|             .collect(); | ||||
|         // TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup.
 | ||||
| 
 | ||||
|  | @ -86,9 +91,18 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa | |||
|             Some(last_matches) => { | ||||
|                 let mut updated: Vec<MatchEntry> = last_matches.iter() | ||||
|                     .filter(|&x| { | ||||
|                         x._match.trigger[x.start..].chars().nth(0).unwrap() == c | ||||
|                         let nchar = x._match.trigger.chars().nth(x.start); | ||||
|                         if let Some(nchar) = nchar { | ||||
|                             c.starts_with(nchar) | ||||
|                         }else{ | ||||
|                             false | ||||
|                         } | ||||
|                     }) | ||||
|                     .map(|x | MatchEntry{ | ||||
|                         start: x.start+1, | ||||
|                         count: x.count, | ||||
|                         _match: &x._match | ||||
|                     }) | ||||
|                     .map(|x | MatchEntry{start: x.start+1, _match: &x._match}) | ||||
|                     .collect(); | ||||
| 
 | ||||
|                 updated.extend(new_matches); | ||||
|  | @ -100,7 +114,7 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa | |||
|         let mut found_match = None; | ||||
| 
 | ||||
|         for entry in combined_matches.iter() { | ||||
|             if entry.start == entry._match.trigger.len() { | ||||
|             if entry.start == entry.count { | ||||
|                 found_match = Some(entry._match); | ||||
|                 break; | ||||
|             } | ||||
|  |  | |||
|  | @ -54,7 +54,7 @@ impl DefaultPackageManager { | |||
| 
 | ||||
|     pub fn new_default() -> DefaultPackageManager { | ||||
|         DefaultPackageManager::new( | ||||
|             crate::config::ConfigSet::get_default_packages_dir(), | ||||
|             crate::context::get_package_dir(), | ||||
|             crate::context::get_data_dir() | ||||
|         ) | ||||
|     } | ||||
|  |  | |||
							
								
								
									
										11
									
								
								src/res/linux/systemd.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/res/linux/systemd.service
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| [Unit] | ||||
| Description=espanso daemon | ||||
| 
 | ||||
| [Service] | ||||
| ExecStart={{{espanso_path}}} daemon | ||||
| Restart=on-failure | ||||
| RestartSec=3 | ||||
| 
 | ||||
| [Install] | ||||
| WantedBy=default.target | ||||
| 
 | ||||
|  | @ -102,13 +102,101 @@ pub fn unregister(_config_set: ConfigSet) { | |||
| // LINUX
 | ||||
| 
 | ||||
| #[cfg(target_os = "linux")] | ||||
| pub fn register(_config_set: ConfigSet) { | ||||
|     println!("Linux does not support automatic system daemon integration."); | ||||
| const LINUX_SERVICE_CONTENT : &str = include_str!("res/linux/systemd.service"); | ||||
| #[cfg(target_os = "linux")] | ||||
| const LINUX_SERVICE_FILENAME : &str = "espanso.service"; | ||||
| 
 | ||||
| #[cfg(target_os = "linux")] | ||||
| pub fn register(config_set: ConfigSet) { | ||||
|     use std::fs::create_dir_all; | ||||
|     use std::process::{Command, ExitStatus}; | ||||
| 
 | ||||
|     // Check if espanso service is already registered
 | ||||
|     let res = Command::new("systemctl") | ||||
|         .args(&["--user", "is-enabled", "espanso"]) | ||||
|         .output(); | ||||
|     if let Ok(res) = res { | ||||
|         let output = String::from_utf8_lossy(res.stdout.as_slice()); | ||||
|         let output = output.trim(); | ||||
|         if res.status.success() && output == "enabled" { | ||||
|             eprintln!("espanso service is already registered to systemd"); | ||||
|             eprintln!("If you want to register it again, please uninstall it first with:"); | ||||
|             eprintln!("    espanso unregister"); | ||||
|             std::process::exit(5); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // User level systemd services should be placed in this directory:
 | ||||
|     // $XDG_CONFIG_HOME/systemd/user/, usually: ~/.config/systemd/user/
 | ||||
|     let config_dir = dirs::config_dir().expect("Could not get configuration directory"); | ||||
|     let systemd_dir = config_dir.join("systemd"); | ||||
|     let user_dir = systemd_dir.join("user"); | ||||
| 
 | ||||
|     // Make sure the directory exists
 | ||||
|     if !user_dir.exists() { | ||||
|         create_dir_all(user_dir.clone()).expect("Could not create systemd user directory"); | ||||
|     } | ||||
| 
 | ||||
|     let service_file = user_dir.join(LINUX_SERVICE_FILENAME); | ||||
|     if !service_file.exists() { | ||||
|         println!("Creating service entry: {}", service_file.to_str().unwrap_or_default()); | ||||
| 
 | ||||
|         let espanso_path = std::env::current_exe().expect("Could not get espanso executable path"); | ||||
|         println!("Entry will point to: {}", espanso_path.to_str().unwrap_or_default()); | ||||
| 
 | ||||
|         let service_content = String::from(LINUX_SERVICE_CONTENT) | ||||
|             .replace("{{{espanso_path}}}", espanso_path.to_str().unwrap_or_default()); | ||||
| 
 | ||||
|         std::fs::write(service_file.clone(), service_content).expect("Unable to write service file"); | ||||
| 
 | ||||
|         println!("Service file created correctly!") | ||||
|     } | ||||
| 
 | ||||
|     println!("Enabling espanso for systemd..."); | ||||
| 
 | ||||
|     let res = Command::new("systemctl") | ||||
|         .args(&["--user", "enable", "espanso"]) | ||||
|         .status(); | ||||
| 
 | ||||
|     if let Ok(status) = res { | ||||
|         if status.success() { | ||||
|             println!("Service registered correctly!") | ||||
|         } | ||||
|     }else{ | ||||
|         println!("Error loading espanso service"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(target_os = "linux")] | ||||
| pub fn unregister(_config_set: ConfigSet) { | ||||
|     println!("Linux does not support automatic system daemon integration."); | ||||
| pub fn unregister(config_set: ConfigSet) { | ||||
|     use std::process::{Command, ExitStatus}; | ||||
| 
 | ||||
|     // Disable the service first
 | ||||
|     let res = Command::new("systemctl") | ||||
|         .args(&["--user", "disable", "espanso"]) | ||||
|         .status(); | ||||
| 
 | ||||
|     // Then delete the espanso.service entry
 | ||||
|     let config_dir = dirs::config_dir().expect("Could not get configuration directory"); | ||||
|     let systemd_dir = config_dir.join("systemd"); | ||||
|     let user_dir = systemd_dir.join("user"); | ||||
|     let service_file = user_dir.join(LINUX_SERVICE_FILENAME); | ||||
| 
 | ||||
|     if service_file.exists() { | ||||
|         let res = std::fs::remove_file(&service_file); | ||||
|         match res { | ||||
|             Ok(_) => { | ||||
|                 println!("Deleted entry at {}", service_file.to_string_lossy()); | ||||
|                 println!("Service unregistered successfully!"); | ||||
|             }, | ||||
|             Err(e) => { | ||||
|                 println!("Error, could not delete service entry at {} with error {}", | ||||
|                          service_file.to_string_lossy(), e); | ||||
|             }, | ||||
|         } | ||||
|     }else{ | ||||
|         eprintln!("Error, could not find espanso service file"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // WINDOWS
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user