diff --git a/Cargo.lock b/Cargo.lock index fb1e821..261229e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,7 +329,7 @@ dependencies = [ [[package]] name = "espanso" -version = "0.2.0" +version = "0.2.1" 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 297d585..bae6602 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "0.2.0" +version = "0.2.1" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" diff --git a/README.md b/README.md index f90cc17..bc9da88 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ ![example](images/example.gif) +Visit the [espanso website](https://espanso.org). + #### What is a Text Expander? A *text expander* is a program that detects when you type @@ -30,111 +32,13 @@ ___ * **Custom scripts** support * **Shell commands** support * **App-specific** configurations +* Expandable with **packages** +* Built-in **package manager** for [espanso hub](https://hub.espanso.org/) * File based configuration -## Table of contents +## Get Started -- [Installation](#installation) - - [Windows](#install-windows) - - [Linux](#install-linux) - - [macOS](#install-macos) -- [Usage](#usage) -- [FAQ](#faq) -- [Donations](#donations) -- [License](#license) - -## Installation - -### Windows - -The installation on Windows is pretty straightforward, navigate to the -[Release](https://github.com/federico-terzi/espanso/releases) page and -download the latest installer ( usually named like -`espanso-win-0.1.0.exe` ). - -Because espanso is not digitally signed, you may experience a warning from -Windows Smartscreen. In this case, just click on "More info" (1) and then -on "Run anyway" (2), as shown in the picture: - -![Windows Smartscreen](images/windows-smartscreen.png) - -If you completed the installation procedure, you should have espanso running. -A good way to find out is by going on any text field and typing `:espanso`. -You should see "Hi there!" appear. - -### Linux - -TODO - -### MacOS - -The easiest way to install espanso on macOS using the [Homebrew](https://brew.sh/) -package manager, but you can also do it manually. - -#### Using Homebrew - -The first thing to do is to add the official espanso *tap* to Homebrew with -the following command: - -``` -brew tap federico-terzi/espanso -``` - -Then you can install espanso with: - -``` -brew install espanso -``` - -To make sure that espanso was correctly installed, you can open a terminal and type: - -``` -espanso --version -``` - -At this point, you have to [Enable Accessibility](#enabling-accessibility) to use espanso. - -#### Enabling Accessibility - -Because espanso uses the macOS [Accessibility API](https://developer.apple.com/library/archive/documentation/Accessibility/Conceptual/AccessibilityMacOSX/) -to work, you need to authorize it using the following procedure: - -Open a terminal and type the command: - -``` -espanso install -``` - -A dialog should show up, click on "Open System Preferences", as shown here: - -![Accessibility Prompt](images/accessibility-prompt.png) - -Then, in the "Privacy" panel click on the Lock icon (1) to enable edits and -then check "espanso" (2), as shown in the picture: - -![Accessibility Settings](images/accessibility-macos-enable.png) - -Now open the terminal again and type: - -``` -espanso install -``` - -If everything goes well, you should see the espanso icon appear in the status bar: - -![macOS status bar icon](images/espanso-icon-macos-statusbar.png) - -If you now type `:espanso` in any text field, you should see "Hi there!" appear! - -## Usage - -TODO - -## FAQ - -#### How does espanso work? - -TODO +Visit the [official documentation](https://espanso.org/docs/). ## Donations @@ -144,6 +48,11 @@ 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) +## Remarks + +* Special thanks to the [ModifyPath](https://www.legroom.net/software/modpath) + script, used by espanso to improve the Windows installer. + ## License espanso was created by [Federico Terzi](http://federicoterzi.com) diff --git a/src/check.rs b/src/check.rs index 34b4f80..146b170 100644 --- a/src/check.rs +++ b/src/check.rs @@ -30,7 +30,7 @@ pub fn check_dependencies() -> bool { let status = Command::new("notify-send") .arg("-v") .output(); - if let Err(_) = status { + if status.is_err() { println!("Error: 'notify-send' command is needed for espanso to work correctly, please install it."); result = false; } @@ -39,7 +39,7 @@ pub fn check_dependencies() -> bool { let status = Command::new("xclip") .arg("-version") .output(); - if let Err(_) = status { + if status.is_err() { println!("Error: 'xclip' command is needed for espanso to work correctly, please install it."); result = false; } diff --git a/src/config/mod.rs b/src/config/mod.rs index 9ae30fd..ce1f192 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -34,7 +34,6 @@ use walkdir::WalkDir; pub(crate) mod runtime; -// TODO: add documentation link const DEFAULT_CONFIG_FILE_CONTENT : &str = include_str!("../res/config.yml"); const DEFAULT_CONFIG_FILE_NAME : &str = "default.yml"; @@ -162,7 +161,7 @@ impl Configs { let mut contents = String::new(); let res = file.read_to_string(&mut contents); - if let Err(_) = res { + if res.is_err() { return Err(ConfigLoadError::UnableToReadFile) } @@ -187,7 +186,7 @@ impl Configs { }); let parent_matches : Vec = self.matches.iter().filter(|&m| { !trigger_set.contains(&m.trigger) - }).map(|m| m.clone()).collect(); + }).cloned().collect(); merged_matches.extend(parent_matches); self.matches = merged_matches; @@ -200,7 +199,7 @@ impl Configs { }); let default_matches : Vec = default.matches.iter().filter(|&m| { !trigger_set.contains(&m.trigger) - }).map(|m| m.clone()).collect(); + }).cloned().collect(); self.matches.extend(default_matches); } @@ -326,7 +325,7 @@ impl ConfigSet { // Create the espanso dir if id doesn't exist let res = create_dir_all(espanso_dir.as_path()); - if let Ok(_) = res { + if res.is_ok() { let default_file = espanso_dir.join(DEFAULT_CONFIG_FILE_NAME); // If config file does not exist, create one from template @@ -358,13 +357,12 @@ impl ConfigSet { return ConfigSet::load(espanso_dir.as_path()) } - return Err(ConfigLoadError::UnableToCreateDefaultConfig) + Err(ConfigLoadError::UnableToCreateDefaultConfig) } pub fn get_default_config_dir() -> PathBuf { let home_dir = dirs::home_dir().expect("Unable to get home directory"); - let espanso_dir = home_dir.join(".espanso"); - espanso_dir + home_dir.join(".espanso") } pub fn get_default_packages_dir() -> PathBuf { diff --git a/src/context/macos.rs b/src/context/macos.rs index 3e6c0ea..c419618 100644 --- a/src/context/macos.rs +++ b/src/context/macos.rs @@ -27,7 +27,7 @@ use std::fs; use log::{info, error}; use std::process::exit; -const STATUS_ICON_BINARY : &'static [u8] = include_bytes!("../res/mac/icon.png"); +const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon.png"); pub struct MacContext { pub send_channel: Sender @@ -42,7 +42,7 @@ impl MacContext { if res == 0 { error!("Accessibility must be enabled to make espanso work on MacOS."); error!("Please allow espanso in the Security & Privacy panel, then restart espanso."); - error!("For more information: "); // TODO: add documentation link + error!("For more information: https://espanso.org/install/mac/"); exit(1); } } diff --git a/src/context/windows.rs b/src/context/windows.rs index d45715f..39d2a14 100644 --- a/src/context/windows.rs +++ b/src/context/windows.rs @@ -26,8 +26,8 @@ use std::{fs}; use widestring::U16CString; use log::{info}; -const BMP_BINARY : &'static [u8] = include_bytes!("../res/win/espanso.bmp"); -const ICO_BINARY : &'static [u8] = include_bytes!("../res/win/espanso.ico"); +const BMP_BINARY : &[u8] = include_bytes!("../res/win/espanso.bmp"); +const ICO_BINARY : &[u8] = include_bytes!("../res/win/espanso.ico"); pub struct WindowsContext { send_channel: Sender, diff --git a/src/event/manager.rs b/src/event/manager.rs index a119122..b6cc0b1 100644 --- a/src/event/manager.rs +++ b/src/event/manager.rs @@ -55,7 +55,7 @@ impl <'a> EventManager for DefaultEventManager<'a> { } } }, - Err(_) => panic!("Broken event channel"), + Err(e) => panic!("Broken event channel {}", e), } } } diff --git a/src/main.rs b/src/main.rs index 58a8c83..2cd9e96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,7 +63,7 @@ mod clipboard; mod extension; mod sysdaemon; -const VERSION: &'static str = env!("CARGO_PKG_VERSION"); +const VERSION: &str = env!("CARGO_PKG_VERSION"); const LOG_FILE: &str = "espanso.log"; fn main() { @@ -168,52 +168,52 @@ fn main() { return; } - if let Some(_) = matches.subcommand_matches("dump") { + if matches.subcommand_matches("dump").is_some() { println!("{:#?}", config_set); return; } - if let Some(_) = matches.subcommand_matches("detect") { + if matches.subcommand_matches("detect").is_some() { detect_main(); return; } - if let Some(_) = matches.subcommand_matches("daemon") { + if matches.subcommand_matches("daemon").is_some() { daemon_main(config_set); return; } - if let Some(_) = matches.subcommand_matches("register") { + if matches.subcommand_matches("register").is_some() { register_main(config_set); return; } - if let Some(_) = matches.subcommand_matches("unregister") { + if matches.subcommand_matches("unregister").is_some() { unregister_main(config_set); return; } - if let Some(_) = matches.subcommand_matches("log") { + if matches.subcommand_matches("log").is_some() { log_main(); return; } - if let Some(_) = matches.subcommand_matches("start") { + if matches.subcommand_matches("start").is_some() { start_main(config_set); return; } - if let Some(_) = matches.subcommand_matches("status") { + if matches.subcommand_matches("status").is_some() { status_main(); return; } - if let Some(_) = matches.subcommand_matches("stop") { + if matches.subcommand_matches("stop").is_some() { stop_main(config_set); return; } - if let Some(_) = matches.subcommand_matches("restart") { + if matches.subcommand_matches("restart").is_some() { restart_main(config_set); return; } @@ -236,7 +236,7 @@ fn main() { list_package_main(config_set, matches); return; } - if let Some(_) = matches.subcommand_matches("refresh") { + if matches.subcommand_matches("refresh").is_some() { update_index_main(config_set); return; } @@ -244,6 +244,7 @@ fn main() { // Defaults help print clap_instance.print_long_help().expect("Unable to print help"); + println!(); } /// Daemon subcommand, start the event loop and spawn a background thread worker @@ -524,22 +525,22 @@ fn detect_main() { /// Send the given command to the espanso daemon fn cmd_main(config_set: ConfigSet, matches: &ArgMatches) { - let command = if let Some(_) = matches.subcommand_matches("exit") { + let command = if matches.subcommand_matches("exit").is_some() { Some(IPCCommand { id: String::from("exit"), payload: String::from(""), }) - }else if let Some(_) = matches.subcommand_matches("toggle") { + }else if matches.subcommand_matches("toggle").is_some() { Some(IPCCommand { id: String::from("toggle"), payload: String::from(""), }) - }else if let Some(_) = matches.subcommand_matches("enable") { + }else if matches.subcommand_matches("enable").is_some() { Some(IPCCommand { id: String::from("enable"), payload: String::from(""), }) - }else if let Some(_) = matches.subcommand_matches("disable") { + }else if matches.subcommand_matches("disable").is_some() { Some(IPCCommand { id: String::from("disable"), payload: String::from(""), @@ -747,7 +748,7 @@ fn acquire_lock() -> Option { let res = file.try_lock_exclusive(); - if let Ok(_) = res { + if res.is_ok() { return Some(file) } diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index a14df4a..7df7894 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -126,7 +126,7 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa if m == config.toggle_key { let mut toggle_press_time = self.toggle_press_time.borrow_mut(); if let Ok(elapsed) = toggle_press_time.elapsed() { - if elapsed.as_millis() < config.toggle_interval as u128 { + if elapsed.as_millis() < u128::from(config.toggle_interval) { self.toggle(); let is_enabled = self.is_enabled.borrow(); diff --git a/src/package/default.rs b/src/package/default.rs index 96ce792..8950d59 100644 --- a/src/package/default.rs +++ b/src/package/default.rs @@ -117,16 +117,14 @@ impl DefaultPackageManager { }else{ started = true; } - }else{ - if started { - let caps = FIELD_REGEX.captures(&line); - if let Some(caps) = caps { - let property = caps.get(1); - let value = caps.get(2); - if property.is_some() && value.is_some() { - fields.insert(property.unwrap().as_str().to_owned(), - value.unwrap().as_str().to_owned()); - } + }else if started { + let caps = FIELD_REGEX.captures(&line); + if let Some(caps) = caps { + let property = caps.get(1); + let value = caps.get(2); + if property.is_some() && value.is_some() { + fields.insert(property.unwrap().as_str().to_owned(), + value.unwrap().as_str().to_owned()); } } } @@ -161,7 +159,7 @@ impl DefaultPackageManager { return local_index.last_update } - return 0; + 0 } fn list_local_packages_names(&self) -> Vec { @@ -260,14 +258,14 @@ impl super::PackageManager for DefaultPackageManager { let readme_path = temp_package_dir.join("README.md"); let package = Self::parse_package_from_readme(&readme_path); - if !package.is_some() { - return Ok(InstallResult::UnableToParsePackageInfo); // TODO: test + if package.is_none() { + return Ok(InstallResult::UnableToParsePackageInfo); } let package = package.unwrap(); let source_dir = temp_package_dir.join(package.version); if !source_dir.exists() { - return Ok(InstallResult::MissingPackageVersion); // TODO: test + return Ok(InstallResult::MissingPackageVersion); } let target_dir = &self.package_dir.join(name); @@ -317,7 +315,7 @@ mod tests { use std::path::Path; use crate::package::PackageManager; use std::fs::{create_dir, create_dir_all}; - use crate::package::InstallResult::{Installed, NotFoundInRepo}; + use crate::package::InstallResult::*; use std::io::Write; const OUTDATED_INDEX_CONTENT : &str = include_str!("../res/test/outdated_index.json"); @@ -496,6 +494,36 @@ mod tests { assert_eq!(temp.package_manager.install_package("not-existing").unwrap(), NotFoundInRepo); } + #[test] + fn test_install_package_missing_version() { + let mut temp = create_temp_package_manager(|_, data_dir| { + let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); + std::fs::write(index_file, INSTALL_PACKAGE_INDEX); + }); + + assert_eq!(temp.package_manager.install_package("dummy-package2").unwrap(), MissingPackageVersion); + } + + #[test] + fn test_install_package_missing_readme_unable_to_parse_package_info() { + let mut temp = create_temp_package_manager(|_, data_dir| { + let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); + std::fs::write(index_file, INSTALL_PACKAGE_INDEX); + }); + + assert_eq!(temp.package_manager.install_package("dummy-package3").unwrap(), UnableToParsePackageInfo); + } + + #[test] + fn test_install_package_bad_readme_unable_to_parse_package_info() { + let mut temp = create_temp_package_manager(|_, data_dir| { + let index_file = data_dir.join(DEFAULT_PACKAGE_INDEX_FILE); + std::fs::write(index_file, INSTALL_PACKAGE_INDEX); + }); + + assert_eq!(temp.package_manager.install_package("dummy-package4").unwrap(), UnableToParsePackageInfo); + } + #[test] fn test_list_local_packages() { let mut temp = create_temp_package_manager(|_, data_dir| { diff --git a/src/res/linux/icon.png b/src/res/linux/icon.png new file mode 100644 index 0000000..9bde0d2 Binary files /dev/null and b/src/res/linux/icon.png differ diff --git a/src/res/test/install_package_index.json b/src/res/test/install_package_index.json index df67503..c9b1e62 100644 --- a/src/res/test/install_package_index.json +++ b/src/res/test/install_package_index.json @@ -12,6 +12,36 @@ }, + { + "name": "dummy-package2", + "title": "Dummy Package", + "version": "9.9.9", + "repo": "https://github.com/federico-terzi/espanso-hub-core", + "desc": "Dummy package", + "author": "Federico Terzi" + + }, + + { + "name": "dummy-package3", + "title": "Dummy Package", + "version": "0.1.0", + "repo": "https://github.com/federico-terzi/espanso-hub-core", + "desc": "Dummy package", + "author": "Federico Terzi" + + }, + + { + "name": "dummy-package4", + "title": "Dummy Package", + "version": "0.1.0", + "repo": "https://github.com/federico-terzi/espanso-hub-core", + "desc": "Dummy package", + "author": "Federico Terzi" + + }, + { "name": "italian-accents", diff --git a/src/ui/linux.rs b/src/ui/linux.rs index de8ac81..f45d65a 100644 --- a/src/ui/linux.rs +++ b/src/ui/linux.rs @@ -19,14 +19,20 @@ use std::process::Command; use super::MenuItem; -use log::error; +use log::{error, info}; +use std::path::PathBuf; -pub struct LinuxUIManager {} +const LINUX_ICON_CONTENT : &[u8] = include_bytes!("../res/linux/icon.png"); + +pub struct LinuxUIManager { + icon_path: PathBuf, +} impl super::UIManager for LinuxUIManager { fn notify(&self, message: &str) { let res = Command::new("notify-send") - .args(&["-t", "2000", "espanso", message]) + .args(&["-i", self.icon_path.to_str().unwrap_or_default(), + "-t", "2000", "espanso", message]) .output(); if let Err(e) = res { @@ -45,6 +51,16 @@ impl super::UIManager for LinuxUIManager { impl LinuxUIManager { pub fn new() -> LinuxUIManager { - LinuxUIManager{} + // Initialize the icon if not present + let data_dir = crate::context::get_data_dir(); + let icon_path = data_dir.join("icon.png"); + if !icon_path.exists() { + info!("Creating espanso icon in '{}'", icon_path.to_str().unwrap_or_default()); + std::fs::write(&icon_path, LINUX_ICON_CONTENT).expect("Unable to copy espanso icon"); + } + + LinuxUIManager{ + icon_path + } } } \ No newline at end of file