diff --git a/Cargo.lock b/Cargo.lock index e91938c..7c2fd3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,7 +370,7 @@ dependencies = [ [[package]] name = "espanso" -version = "0.5.2" +version = "0.5.3" 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 cdad161..2f2c89a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "0.5.2" +version = "0.5.3" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" @@ -30,13 +30,11 @@ git2 = {version = "0.10.1", features = ["https"]} tempfile = "3.1.0" dialoguer = "0.4.0" rand = "0.7.2" +zip = "0.5.3" [target.'cfg(unix)'.dependencies] libc = "0.2.62" -[target.'cfg(target_os = "macos")'.dependencies] -zip = "0.5.3" - [build-dependencies] cmake = "0.1.31" diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp index 3a771b9..dfb6d42 100644 --- a/native/libwinbridge/bridge.cpp +++ b/native/libwinbridge/bridge.cpp @@ -381,7 +381,7 @@ int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path) { Rid[1].usUsagePage = 0x01; Rid[1].usUsage = 0x02; - Rid[1].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK; // adds HID mouse and also ignores legacy mouse messages + Rid[1].dwFlags = RIDEV_INPUTSINK; // adds HID mouse and also ignores legacy mouse messages Rid[1].hwndTarget = window; if (RegisterRawInputDevices(Rid, 2, sizeof(Rid[0])) == FALSE) { // Something went wrong, error. diff --git a/packager.py b/packager.py index b558e47..358f1f9 100644 --- a/packager.py +++ b/packager.py @@ -82,10 +82,12 @@ def build_windows(package_info): msvc_dirs = glob.glob("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\*\\VC\\Redist\\MSVC\\*") print("Found Redists: ", msvc_dirs) - msvc_dir = msvc_dirs[0] - print("Using: ",msvc_dir) - if len(msvc_dir) == 0: + if len(msvc_dirs) == 0: raise Exception("Cannot find redistributable dlls") + + msvc_dir = msvc_dirs[-1] # Take the most recent version of the toolchain + print("Using: ",msvc_dir) + dll_files = glob.glob(msvc_dir + "\\x64\\*CRT\\*.dll") print("Found DLLs:") diff --git a/snapcraft.yaml b/snapcraft.yaml index 1ca478a..d767423 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: espanso -version: 0.5.2 +version: 0.5.3 summary: A Cross-platform Text Expander written in Rust description: | espanso is a Cross-platform, Text Expander written in Rust. diff --git a/src/config/mod.rs b/src/config/mod.rs index 8acf48d..9244868 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -47,7 +47,7 @@ fn default_filter_title() -> String{ "".to_owned() } fn default_filter_class() -> String{ "".to_owned() } fn default_filter_exec() -> String{ "".to_owned() } fn default_log_level() -> i32 { 0 } -fn default_conflict_check() -> bool{ true } +fn default_conflict_check() -> bool{ false } fn default_ipc_server_port() -> i32 { 34982 } fn default_use_system_agent() -> bool { true } fn default_config_caching_interval() -> i32 { 800 } diff --git a/src/main.rs b/src/main.rs index 0505a6f..6218e71 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,8 +43,10 @@ use crate::ui::UIManager; use crate::protocol::*; use std::io::{BufReader, BufRead}; use crate::package::default::DefaultPackageManager; -use crate::package::{PackageManager, InstallResult, UpdateResult, RemoveResult}; +use crate::package::{PackageManager, InstallResult, UpdateResult, RemoveResult, PackageResolver}; use std::sync::atomic::AtomicBool; +use crate::package::git::GitPackageResolver; +use crate::package::zip::ZipPackageResolver; mod ui; mod edit; @@ -71,6 +73,12 @@ const LOG_FILE: &str = "espanso.log"; fn main() { let install_subcommand = SubCommand::with_name("install") .about("Install a package. Equivalent to 'espanso package install'") + .arg(Arg::with_name("no-git") + .short("g") + .long("no-git") + .required(false) + .takes_value(false) + .help("Install packages avoiding the GIT package provider. Try this flag if the default mode is not working.")) .arg(Arg::with_name("package_name") .help("Package name")); @@ -426,6 +434,7 @@ fn start_daemon(config_set: ConfigSet) { #[cfg(target_os = "linux")] fn start_daemon(config_set: ConfigSet) { use std::process::{Command, Stdio}; + use crate::sysdaemon::{verify, VerifyResult}; // Check if Systemd is available in the system let status = Command::new("systemctl") @@ -444,25 +453,33 @@ fn start_daemon(config_set: ConfigSet) { if config_set.default.use_system_agent && !force_unmanaged { // Make sure espanso is currently registered in systemd - let res = Command::new("systemctl") - .args(&["--user", "is-enabled", "espanso.service"]) - .output(); - if !res.unwrap().status.success() { - use dialoguer::Confirmation; - if Confirmation::new() - .with_text("espanso must be registered to systemd (user level) first. Do you want to proceed?") - .default(true) - .show_default(true) - .interact().expect("Unable to read user answer") { - + let res = verify(); + match res { + VerifyResult::EnabledAndValid => { + // Do nothing, everything is ok! + }, + VerifyResult::EnabledButInvalidPath => { + eprintln!("Updating espanso service file with new path..."); + unregister_main(config_set.clone()); 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 + }, + VerifyResult::NotEnabled => { + use dialoguer::Confirmation; + if Confirmation::new() + .with_text("espanso must be registered to systemd (user level) first. Do you want to proceed?") + .default(true) + .show_default(true) + .interact().expect("Unable to read user answer") { - std::process::exit(4); - } + 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 @@ -742,7 +759,14 @@ fn install_main(_config_set: ConfigSet, matches: &ArgMatches) { exit(1); }); - let mut package_manager = DefaultPackageManager::new_default(); + let package_resolver: Box = if matches.is_present("no-git") { + println!("Using alternative package provider"); + Box::new(ZipPackageResolver::new()) + }else{ + Box::new(GitPackageResolver::new()) + }; + + let mut package_manager = DefaultPackageManager::new_default(Some(package_resolver)); if package_manager.is_index_outdated() { println!("Updating package index..."); @@ -808,7 +832,7 @@ fn remove_package_main(_config_set: ConfigSet, matches: &ArgMatches) { exit(1); }); - let package_manager = DefaultPackageManager::new_default(); + let package_manager = DefaultPackageManager::new_default(None); let res = package_manager.remove_package(package_name); @@ -833,7 +857,7 @@ fn remove_package_main(_config_set: ConfigSet, matches: &ArgMatches) { } fn update_index_main(_config_set: ConfigSet) { - let mut package_manager = DefaultPackageManager::new_default(); + let mut package_manager = DefaultPackageManager::new_default(None); let res = package_manager.update_index(true); @@ -856,7 +880,7 @@ fn update_index_main(_config_set: ConfigSet) { } fn list_package_main(_config_set: ConfigSet, matches: &ArgMatches) { - let package_manager = DefaultPackageManager::new_default(); + let package_manager = DefaultPackageManager::new_default(None); let list = package_manager.list_local_packages(); diff --git a/src/package/default.rs b/src/package/default.rs index dfbff4a..d3bbd7f 100644 --- a/src/package/default.rs +++ b/src/package/default.rs @@ -18,7 +18,7 @@ */ use std::path::{PathBuf, Path}; -use crate::package::{PackageIndex, UpdateResult, Package, InstallResult, RemoveResult}; +use crate::package::{PackageIndex, UpdateResult, Package, InstallResult, RemoveResult, PackageResolver}; use std::error::Error; use std::fs::{File, create_dir}; use std::io::{BufReader, BufRead}; @@ -31,6 +31,7 @@ use git2::Repository; use regex::Regex; use crate::package::RemoveResult::Removed; use std::collections::HashMap; +use super::git::GitPackageResolver; const DEFAULT_PACKAGE_INDEX_FILE : &str = "package_index.json"; @@ -38,24 +39,28 @@ pub struct DefaultPackageManager { package_dir: PathBuf, data_dir: PathBuf, + package_resolver: Option>, + local_index: Option, } impl DefaultPackageManager { - pub fn new(package_dir: PathBuf, data_dir: PathBuf) -> DefaultPackageManager { + pub fn new(package_dir: PathBuf, data_dir: PathBuf, package_resolver: Option>) -> DefaultPackageManager { let local_index = Self::load_local_index(&data_dir); DefaultPackageManager{ package_dir, data_dir, + package_resolver, local_index } } - pub fn new_default() -> DefaultPackageManager { + pub fn new_default(package_resolver: Option>) -> DefaultPackageManager { DefaultPackageManager::new( crate::context::get_package_dir(), - crate::context::get_data_dir() + crate::context::get_data_dir(), + package_resolver, ) } @@ -89,12 +94,6 @@ impl DefaultPackageManager { Ok(index) } - fn clone_repo_to_temp(repo_url: &str) -> Result> { - let temp_dir = TempDir::new()?; - let _repo = Repository::clone(repo_url, temp_dir.path())?; - Ok(temp_dir) - } - fn parse_package_from_readme(readme_path: &Path) -> Option { lazy_static! { static ref FIELD_REGEX: Regex = Regex::new(r###"^\s*(.*?)\s*:\s*"?(.*?)"?$"###).unwrap(); @@ -248,7 +247,7 @@ impl super::PackageManager for DefaultPackageManager { return Ok(AlreadyInstalled); } - let temp_dir = Self::clone_repo_to_temp(repo_url)?; + let temp_dir = self.package_resolver.as_ref().unwrap().clone_repo_to_temp(repo_url)?; let temp_package_dir = temp_dir.path().join(name); if !temp_package_dir.exists() { @@ -337,7 +336,8 @@ mod tests { let package_manager = DefaultPackageManager::new( package_dir.path().clone().to_path_buf(), - data_dir.path().clone().to_path_buf() + data_dir.path().clone().to_path_buf(), + Some(Box::new(GitPackageResolver::new())), ); TempPackageManager { @@ -465,12 +465,6 @@ mod tests { assert_eq!(temp.package_manager.install_package("italian-accents").unwrap(), AlreadyInstalled); } - #[test] - fn test_clone_temp_repository() { - let cloned_dir = DefaultPackageManager::clone_repo_to_temp("https://github.com/federico-terzi/espanso-hub-core").unwrap(); - assert!(cloned_dir.path().join("LICENSE").exists()); - } - #[test] fn test_install_package() { let mut temp = create_temp_package_manager(|_, data_dir| { diff --git a/src/package/git.rs b/src/package/git.rs new file mode 100644 index 0000000..f8d132b --- /dev/null +++ b/src/package/git.rs @@ -0,0 +1,33 @@ +use tempfile::TempDir; +use std::error::Error; +use git2::Repository; +use super::PackageResolver; + +pub struct GitPackageResolver; + +impl GitPackageResolver { + pub fn new() -> GitPackageResolver { + return GitPackageResolver{}; + } +} + +impl super::PackageResolver for GitPackageResolver { + fn clone_repo_to_temp(&self, repo_url: &str) -> Result> { + let temp_dir = TempDir::new()?; + let _repo = Repository::clone(repo_url, temp_dir.path())?; + Ok(temp_dir) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::{TempDir, NamedTempFile}; + + #[test] + fn test_clone_temp_repository() { + let resolver = GitPackageResolver::new(); + let cloned_dir = resolver.clone_repo_to_temp("https://github.com/federico-terzi/espanso-hub-core").unwrap(); + assert!(cloned_dir.path().join("LICENSE").exists()); + } +} \ No newline at end of file diff --git a/src/package/mod.rs b/src/package/mod.rs index 3ba505d..78ecaf1 100644 --- a/src/package/mod.rs +++ b/src/package/mod.rs @@ -17,9 +17,13 @@ * along with espanso. If not, see . */ +pub(crate) mod git; +pub(crate) mod zip; pub(crate) mod default; + use serde::{Serialize, Deserialize}; use std::error::Error; +use tempfile::TempDir; pub trait PackageManager { fn is_index_outdated(&self) -> bool; @@ -35,6 +39,10 @@ pub trait PackageManager { fn list_local_packages(&self) -> Vec; } +pub trait PackageResolver { + fn clone_repo_to_temp(&self, repo_url: &str) -> Result>; +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Package { pub name: String, diff --git a/src/package/zip.rs b/src/package/zip.rs new file mode 100644 index 0000000..169e78d --- /dev/null +++ b/src/package/zip.rs @@ -0,0 +1,88 @@ +use tempfile::TempDir; +use std::error::Error; +use super::PackageResolver; +use std::io::{Cursor, copy, Read}; +use std::{fs, io}; +use std::fs::File; +use log::debug; + +pub struct ZipPackageResolver; + +impl ZipPackageResolver { + pub fn new() -> ZipPackageResolver { + return ZipPackageResolver{}; + } +} + +impl super::PackageResolver for ZipPackageResolver { + fn clone_repo_to_temp(&self, repo_url: &str) -> Result> { + let temp_dir = TempDir::new()?; + + let zip_url = repo_url.to_owned() + "/archive/master.zip"; + + // Download the archive from GitHub + let mut response = reqwest::get(&zip_url)?; + + // Extract zip file + let mut buffer = Vec::new(); + copy(&mut response, &mut buffer)?; + + let reader = Cursor::new(buffer); + + let mut archive = zip::ZipArchive::new(reader).unwrap(); + + // Find the root folder name + let mut root_folder = { + let mut root_folder = archive.by_index(0).unwrap(); + let root_folder = root_folder.sanitized_name(); + root_folder.to_str().unwrap().to_owned() + }; + root_folder.push(std::path::MAIN_SEPARATOR); + + for i in 1..archive.len() { + let mut file = archive.by_index(i).unwrap(); + + let current_path = file.sanitized_name(); + let current_filename = current_path.to_str().unwrap(); + let trimmed_filename = current_filename.trim_start_matches(&root_folder); + + let outpath = temp_dir.path().join(trimmed_filename); + + { + let comment = file.comment(); + if !comment.is_empty() { + debug!("File {} comment: {}", i, comment); + } + } + + if (&*file.name()).ends_with('/') { + debug!("File {} extracted to \"{}\"", i, outpath.as_path().display()); + fs::create_dir_all(&outpath).unwrap(); + } else { + debug!("File {} extracted to \"{}\" ({} bytes)", i, outpath.as_path().display(), file.size()); + if let Some(p) = outpath.parent() { + if !p.exists() { + fs::create_dir_all(&p).unwrap(); + } + } + let mut outfile = fs::File::create(&outpath).unwrap(); + io::copy(&mut file, &mut outfile).unwrap(); + } + } + + Ok(temp_dir) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::{TempDir, NamedTempFile}; + + #[test] + fn test_clone_temp_repository() { + let resolver = ZipPackageResolver::new(); + let cloned_dir = resolver.clone_repo_to_temp("https://github.com/federico-terzi/espanso-hub-core").unwrap(); + assert!(cloned_dir.path().join("LICENSE").exists()); + } +} \ No newline at end of file diff --git a/src/sysdaemon.rs b/src/sysdaemon.rs index b13e0dc..4bbf06c 100644 --- a/src/sysdaemon.rs +++ b/src/sysdaemon.rs @@ -20,6 +20,7 @@ // This functions are used to register/unregister espanso from the system daemon manager. use crate::config::ConfigSet; +use crate::sysdaemon::VerifyResult::{EnabledAndValid, NotEnabled, EnabledButInvalidPath}; // INSTALLATION @@ -181,6 +182,54 @@ pub fn register(config_set: ConfigSet) { } } +pub enum VerifyResult { + EnabledAndValid, + EnabledButInvalidPath, + NotEnabled, +} + +#[cfg(target_os = "linux")] +pub fn verify() -> VerifyResult { + use regex::Regex; + 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" { + return NotEnabled + } + } + + lazy_static! { + static ref ExecPathRegex: Regex = Regex::new("ExecStart=(?P.*?)\\s").unwrap(); + } + + // Check if the currently registered path is valid + let res = Command::new("systemctl") + .args(&["--user", "cat", "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() { + let caps = ExecPathRegex.captures(output).unwrap(); + let path = caps.get(1).map_or("", |m| m.as_str()); + let espanso_path = std::env::current_exe().expect("Could not get espanso executable path"); + + if espanso_path.to_string_lossy() != path { + return EnabledButInvalidPath + } + } + } + + EnabledAndValid +} + #[cfg(target_os = "linux")] pub fn unregister(config_set: ConfigSet) { use std::process::{Command, ExitStatus};