diff --git a/Cargo.lock b/Cargo.lock index 2e16150..979c3ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)", diff --git a/Cargo.toml b/Cargo.toml index caa07c1..58f45d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "0.2.4" +version = "0.3.0" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" diff --git a/README.md b/README.md index bc9da88..dc5a120 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,14 @@ please consider making a small donation, it really helps :) [![Donate with PayPal](images/donate.gif)](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) diff --git a/ci/build-macos.yml b/ci/build-macos.yml index ea7bee9..d4fd4d9 100644 --- a/ci/build-macos.yml +++ b/ci/build-macos.yml @@ -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" \ No newline at end of file diff --git a/ci/build-win.yml b/ci/build-win.yml index 726a210..cc2fc33 100644 --- a/ci/build-win.yml +++ b/ci/build-win.yml @@ -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" diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index 3f6369e..b32039f 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -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; -} +} \ No newline at end of file diff --git a/native/liblinuxbridge/bridge.h b/native/liblinuxbridge/bridge.h index 89d60aa..25c78e7 100644 --- a/native/liblinuxbridge/bridge.h +++ b/native/liblinuxbridge/bridge.h @@ -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 */ diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp index 3d977f6..dfe6108 100644 --- a/native/libwinbridge/bridge.cpp +++ b/native/libwinbridge/bridge.cpp @@ -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(buffer.data()), buffer.size(), 0, raw->data.keyboard.VKey, is_key_down); + keypress_callback(manager_instance, reinterpret_cast(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); } diff --git a/native/libwinbridge/bridge.h b/native/libwinbridge/bridge.h index e9ddbf8..fc0a80c 100644 --- a/native/libwinbridge/bridge.h +++ b/native/libwinbridge/bridge.h @@ -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; /* diff --git a/packager.py b/packager.py index 8a0f141..2cc55cd 100644 --- a/packager.py +++ b/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() diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs index a7f40e3..d85156b 100644 --- a/src/bridge/linux.rs +++ b/src/bridge/linux.rs @@ -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(); diff --git a/src/bridge/windows.rs b/src/bridge/windows.rs index 6866cbe..349230c 100644 --- a/src/bridge/windows.rs +++ b/src/bridge/windows.rs @@ -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(); diff --git a/src/clipboard/linux.rs b/src/clipboard/linux.rs index a20c2c6..7e14a07 100644 --- a/src/clipboard/linux.rs +++ b/src/clipboard/linux.rs @@ -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); + } } } } diff --git a/src/config/mod.rs b/src/config/mod.rs index ce1f192..a8bf6cc 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -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 { - if !dir_path.is_dir() { + pub fn load(config_dir: &Path, package_dir: &Path) -> Result { + 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 { - 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")); diff --git a/src/config/runtime.rs b/src/config/runtime.rs index 3bb0a8b..2e2aff0 100644 --- a/src/config/runtime.rs +++ b/src/config/runtime.rs @@ -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, @@ -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"); diff --git a/src/context/linux.rs b/src/context/linux.rs index 152ed0b..2595d1b 100644 --- a/src/context/linux.rs +++ b/src/context/linux.rs @@ -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) -> Box { + // 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 = match key_code { diff --git a/src/context/macos.rs b/src/context/macos.rs index c419618..1f46a50 100644 --- a/src/context/macos.rs +++ b/src/context/macos.rs @@ -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 = match key_code { diff --git a/src/context/mod.rs b/src/context/mod.rs index bbc1d9e..c338de7 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -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) -> Box { @@ -58,4 +51,62 @@ pub fn new(send_channel: Sender) -> Box { #[cfg(target_os = "windows")] pub fn new(send_channel: Sender) -> Box { 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 } \ No newline at end of file diff --git a/src/context/windows.rs b/src/context/windows.rs index 39d2a14..c6ecf57 100644 --- a/src/context/windows.rs +++ b/src/context/windows.rs @@ -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 diff --git a/src/engine.rs b/src/engine.rs index 84cb22b..d848b27 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -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(); diff --git a/src/event/mod.rs b/src/event/mod.rs index abcfdcc..aeb50da 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -52,7 +52,7 @@ impl From for ActionType { #[derive(Debug, Clone)] pub enum KeyEvent { - Char(char), + Char(String), Modifier(KeyModifier) } diff --git a/src/main.rs b/src/main.rs index 0620c70..328e638 100644 --- a/src/main.rs +++ b/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 { let espanso_dir = context::get_data_dir(); diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index ece4eac..59b9ca7 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -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 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); diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index 7df7894..3d919db 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -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 = 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 = 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; } diff --git a/src/package/default.rs b/src/package/default.rs index 8950d59..dfbff4a 100644 --- a/src/package/default.rs +++ b/src/package/default.rs @@ -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() ) } diff --git a/src/res/linux/systemd.service b/src/res/linux/systemd.service new file mode 100644 index 0000000..4fdd26e --- /dev/null +++ b/src/res/linux/systemd.service @@ -0,0 +1,11 @@ +[Unit] +Description=espanso daemon + +[Service] +ExecStart={{{espanso_path}}} daemon +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=default.target + diff --git a/src/sysdaemon.rs b/src/sysdaemon.rs index 23443fd..4f932d3 100644 --- a/src/sysdaemon.rs +++ b/src/sysdaemon.rs @@ -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