Merge pull request #466 from federico-terzi/dev

Version 0.7.2
This commit is contained in:
Federico Terzi 2020-09-24 17:35:07 +02:00 committed by GitHub
commit c9a33bf356
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 312 additions and 74 deletions

2
Cargo.lock generated
View File

@ -371,7 +371,7 @@ dependencies = [
[[package]]
name = "espanso"
version = "0.7.1"
version = "0.7.2"
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)",

View File

@ -1,6 +1,6 @@
[package]
name = "espanso"
version = "0.7.1"
version = "0.7.2"
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
license = "GPL-3.0"
description = "Cross-platform Text Expander written in Rust"
@ -10,7 +10,7 @@ edition = "2018"
build="build.rs"
[modulo]
version = "0.1.0"
version = "0.1.1"
[dependencies]
widestring = "0.4.0"

View File

@ -29,5 +29,7 @@
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
- (IBAction) statusIconClick: (id) sender;
- (IBAction) contextMenuClick: (id) sender;
- (void) updateIcon: (char *)iconPath;
- (void) setIcon: (char *)iconPath;
@end

View File

@ -26,15 +26,8 @@
// Setup status icon
if (show_icon) {
myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
NSString *nsIconPath = [NSString stringWithUTF8String:icon_path];
NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath];
[statusImage setTemplate:YES];
[myStatusItem.button setImage:statusImage];
[myStatusItem setHighlightMode:YES];
[myStatusItem.button setAction:@selector(statusIconClick:)];
[myStatusItem.button setTarget:self];
[self setIcon: icon_path];
}
// Setup key listener
@ -66,6 +59,29 @@
}];
}
- (void) updateIcon: (char *)iconPath {
if (show_icon) {
[myStatusItem release];
[self setIcon: iconPath];
}
}
- (void) setIcon: (char *)iconPath {
if (show_icon) {
myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
NSString *nsIconPath = [NSString stringWithUTF8String:iconPath];
NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath];
[statusImage setTemplate:YES];
[myStatusItem.button setImage:statusImage];
[myStatusItem setHighlightMode:YES];
[myStatusItem.button setAction:@selector(statusIconClick:)];
[myStatusItem.button setTarget:self];
}
}
- (IBAction) statusIconClick: (id) sender {
icon_click_callback(context_instance);
}

View File

@ -26,12 +26,13 @@ extern "C" {
extern void * context_instance;
extern char * icon_path;
extern char * disabled_icon_path;
extern int32_t show_icon;
/*
* Initialize the AppDelegate and check for accessibility permissions
*/
int32_t initialize(void * context, const char * icon_path, int32_t show_icon);
int32_t initialize(void * context, const char * icon_path, const char * disabled_icon_path, int32_t show_icon);
/*
* Start the event loop indefinitely. Blocking call.
@ -117,6 +118,11 @@ typedef void (*ContextMenuClickCallback)(void * self, int32_t id);
extern ContextMenuClickCallback context_menu_click_callback;
extern "C" void register_context_menu_click_callback(ContextMenuClickCallback callback);
/*
* Update the tray icon status
*/
extern "C" void update_tray_icon(int32_t enabled);
// SYSTEM
/*

View File

@ -33,6 +33,7 @@ extern "C" {
void * context_instance;
char * icon_path;
char * disabled_icon_path;
int32_t show_icon;
AppDelegate * delegate_ptr;
@ -40,9 +41,10 @@ KeypressCallback keypress_callback;
IconClickCallback icon_click_callback;
ContextMenuClickCallback context_menu_click_callback;
int32_t initialize(void * context, const char * _icon_path, int32_t _show_icon) {
int32_t initialize(void * context, const char * _icon_path, const char * _disabled_icon_path, int32_t _show_icon) {
context_instance = context;
icon_path = strdup(_icon_path);
disabled_icon_path = strdup(_disabled_icon_path);
show_icon = _show_icon;
AppDelegate *delegate = [[AppDelegate alloc] init];
@ -74,6 +76,18 @@ int32_t headless_eventloop() {
return 0;
}
void update_tray_icon(int32_t enabled) {
dispatch_async(dispatch_get_main_queue(), ^(void) {
NSApplication * application = [NSApplication sharedApplication];
char * iconPath = icon_path;
if (!enabled) {
iconPath = disabled_icon_path;
}
[[application delegate] updateIcon: iconPath];
});
}
void send_string(const char * string) {
char * stringCopy = strdup(string);
dispatch_async(dispatch_get_main_queue(), ^(void) {

View File

@ -663,6 +663,25 @@ void trigger_copy() {
SendInput(vec.size(), vec.data(), sizeof(INPUT));
}
int32_t are_modifiers_pressed() {
short ctrl_pressed = GetAsyncKeyState(VK_CONTROL);
short enter_pressed = GetAsyncKeyState(VK_RETURN);
short alt_pressed = GetAsyncKeyState(VK_MENU);
short shift_pressed = GetAsyncKeyState(VK_SHIFT);
short meta_pressed = GetAsyncKeyState(VK_LWIN);
short rmeta_pressed = GetAsyncKeyState(VK_RWIN);
if (((ctrl_pressed & 0x8000) +
(enter_pressed & 0x8000) +
(alt_pressed & 0x8000) +
(shift_pressed & 0x8000) +
(meta_pressed & 0x8000) +
(rmeta_pressed & 0x8000)) != 0) {
return 1;
}
return 0;
}
// SYSTEM
@ -808,7 +827,7 @@ int32_t set_clipboard(wchar_t *text) {
}
int32_t get_clipboard(wchar_t *buffer, int32_t size) {
int32_t result = 0;
int32_t result = 1;
if (!OpenClipboard(NULL)) {
return -1;
}

View File

@ -97,6 +97,11 @@ extern "C" void trigger_shift_paste();
*/
extern "C" void trigger_copy();
/*
* Check whether keyboard modifiers (CTRL, CMD, SHIFT, ecc) are pressed
*/
extern "C" int32_t are_modifiers_pressed();
// Detect current application commands
/*

View File

@ -1,5 +1,5 @@
name: espanso
version: 0.7.1
version: 0.7.2
summary: A Cross-platform Text Expander written in Rust
description: |
espanso is a Cross-platform, Text Expander written in Rust.

View File

@ -29,7 +29,12 @@ pub struct MacMenuItem {
#[allow(improper_ctypes)]
#[link(name = "macbridge", kind = "static")]
extern "C" {
pub fn initialize(s: *const c_void, icon_path: *const c_char, show_icon: i32);
pub fn initialize(
s: *const c_void,
icon_path: *const c_char,
disabled_icon_path: *const c_char,
show_icon: i32,
);
pub fn eventloop();
pub fn headless_eventloop();
@ -51,6 +56,7 @@ extern "C" {
pub fn register_icon_click_callback(cb: extern "C" fn(_self: *mut c_void));
pub fn show_context_menu(items: *const MacMenuItem, count: i32) -> i32;
pub fn register_context_menu_click_callback(cb: extern "C" fn(_self: *mut c_void, id: i32));
pub fn update_tray_icon(enabled: i32);
// Keyboard
pub fn register_keypress_callback(

View File

@ -69,6 +69,7 @@ extern "C" {
pub fn trigger_paste();
pub fn trigger_shift_paste();
pub fn trigger_copy();
pub fn are_modifiers_pressed() -> i32;
// PROCESSES

View File

@ -34,6 +34,7 @@ use std::sync::Arc;
use std::{fs, thread};
const STATUS_ICON_BINARY: &[u8] = include_bytes!("../res/mac/icon.png");
const DISABLED_STATUS_ICON_BINARY: &[u8] = include_bytes!("../res/mac/icondisabled.png");
pub struct MacContext {
pub send_channel: Sender<Event>,
@ -72,6 +73,7 @@ impl MacContext {
// Initialize the status icon path
let espanso_dir = super::get_data_dir();
let status_icon_target = espanso_dir.join("icon.png");
let disabled_status_icon_target = espanso_dir.join("icondisabled.png");
if status_icon_target.exists() {
info!("Status icon already initialized, skipping.");
@ -84,6 +86,19 @@ impl MacContext {
});
}
if disabled_status_icon_target.exists() {
info!("Status icon (disabled) already initialized, skipping.");
} else {
fs::write(&disabled_status_icon_target, DISABLED_STATUS_ICON_BINARY).unwrap_or_else(
|e| {
error!(
"Error copying the Status Icon (disabled) to the espanso data directory: {}",
e
);
},
);
}
unsafe {
let context_ptr = &*context as *const MacContext as *const c_void;
@ -93,9 +108,17 @@ impl MacContext {
let status_icon_path =
CString::new(status_icon_target.to_str().unwrap_or_default()).unwrap_or_default();
let disabled_status_icon_path =
CString::new(disabled_status_icon_target.to_str().unwrap_or_default())
.unwrap_or_default();
let show_icon = if config.show_icon { 1 } else { 0 };
initialize(context_ptr, status_icon_path.as_ptr(), show_icon);
initialize(
context_ptr,
status_icon_path.as_ptr(),
disabled_status_icon_path.as_ptr(),
show_icon,
);
}
context
@ -146,6 +169,12 @@ impl MacContext {
}
}
pub fn update_icon(enabled: bool) {
unsafe {
crate::bridge::macos::update_tray_icon(if enabled { 1 } else { 0 });
}
}
impl super::Context for MacContext {
fn eventloop(&self) {
// Start the SecureInput watcher thread

View File

@ -50,7 +50,7 @@ pub fn new(
#[cfg(target_os = "macos")]
pub fn update_icon(enabled: bool) {
// TODO: add update icon on macOS
macos::update_icon(enabled);
}
#[cfg(target_os = "macos")]

View File

@ -498,9 +498,14 @@ impl<
if config.secure_input_notification && config.show_notifications {
self.ui_manager.notify_delay(&format!("{} has activated SecureInput. Espanso won't work until you disable it.", app_name), 5000);
}
crate::context::update_icon(false);
}
SystemEvent::SecureInputDisabled => {
info!("SecureInput has been disabled.");
let is_enabled = self.enabled.borrow();
crate::context::update_icon(*is_enabled);
}
SystemEvent::NotifyRequest(message) => {
let config = self.config_manager.default_config();

View File

@ -74,7 +74,7 @@ impl super::Extension for FormExtension {
.manager
.invoke(&["form", "-i", "-"], &serialized_config);
// On macOS, after the form closes we have to wait until the user releases the modifier keys
// On macOS and Windows, after the form closes we have to wait until the user releases the modifier keys
on_form_close();
if let Some(output) = output {
@ -95,9 +95,17 @@ impl super::Extension for FormExtension {
}
}
#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "linux")]
fn on_form_close() {
// NOOP on Windows and Linux
// NOOP on Linux
}
#[cfg(target_os = "windows")]
fn on_form_close() {
let released = crate::keyboard::windows::wait_for_modifiers_release();
if !released {
warn!("Wait for modifiers release timed out! Please after closing the form, release your modifiers keys (CTRL, CMD, ALT, SHIFT)");
}
}
#[cfg(target_os = "macos")]

View File

@ -21,7 +21,7 @@ use crate::config::Configs;
use serde::{Deserialize, Serialize};
#[cfg(target_os = "windows")]
mod windows;
pub mod windows;
#[cfg(target_os = "linux")]
mod linux;

View File

@ -78,3 +78,15 @@ impl super::KeyboardManager for WindowsKeyboardManager {
}
}
}
pub fn wait_for_modifiers_release() -> bool {
let start = std::time::SystemTime::now();
while start.elapsed().unwrap_or_default().as_millis() < 3000 {
let pressed = unsafe { crate::bridge::windows::are_modifiers_pressed() };
if pressed == 0 {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
false
}

View File

@ -97,6 +97,13 @@ fn main() {
.help("(Optional) Link to GitHub repository")
.required(false)
.default_value("hub"),
)
.arg(
Arg::with_name("proxy")
.help("Use a proxy, should be used as --proxy=https://proxy:1234")
.required(false)
.long("proxy")
.takes_value(true),
);
let uninstall_subcommand = SubCommand::with_name("uninstall")
@ -1096,6 +1103,14 @@ fn install_main(_config_set: ConfigSet, matches: &ArgMatches) {
repository = repository.trim_end_matches(".git")
}
let proxy = match matches.value_of("proxy") {
Some(proxy) => {
println!("Using proxy: {}", proxy);
Some(proxy.to_string())
}
None => None,
};
let package_resolver = Box::new(ZipPackageResolver::new());
let allow_external: bool = if matches.is_present("external") {
@ -1131,7 +1146,7 @@ fn install_main(_config_set: ConfigSet, matches: &ArgMatches) {
println!("Using cached package index, run 'espanso package refresh' to update it.")
}
package_manager.install_package(package_name, allow_external)
package_manager.install_package(package_name, allow_external, proxy)
} else {
// Make sure the repo is a valid github url
lazy_static! {
@ -1147,7 +1162,7 @@ fn install_main(_config_set: ConfigSet, matches: &ArgMatches) {
if !allow_external {
Ok(InstallResult::BlockedExternalPackage(repository.to_owned()))
} else {
package_manager.install_package_from_repo(package_name, repository)
package_manager.install_package_from_repo(package_name, repository, proxy)
}
};

View File

@ -264,12 +264,13 @@ impl super::PackageManager for DefaultPackageManager {
&self,
name: &str,
allow_external: bool,
proxy: Option<String>,
) -> Result<InstallResult, Box<dyn Error>> {
let package = self.get_package(name);
match package {
Some(package) => {
if package.is_core || allow_external {
self.install_package_from_repo(name, &package.repo)
self.install_package_from_repo(name, &package.repo, proxy)
} else {
Ok(BlockedExternalPackage(package.original_repo))
}
@ -282,6 +283,7 @@ impl super::PackageManager for DefaultPackageManager {
&self,
name: &str,
repo_url: &str,
proxy: Option<String>,
) -> Result<InstallResult, Box<dyn Error>> {
// Check if package is already installed
let packages = self.list_local_packages_names();
@ -294,7 +296,7 @@ impl super::PackageManager for DefaultPackageManager {
.package_resolver
.as_ref()
.unwrap()
.clone_repo_to_temp(repo_url)?;
.clone_repo_to_temp(repo_url, proxy)?;
let temp_package_dir = temp_dir.path().join(name);
if !temp_package_dir.exists() {
@ -532,7 +534,7 @@ mod tests {
assert_eq!(
temp.package_manager
.install_package("doesnotexist", false)
.install_package("doesnotexist", false, None)
.unwrap(),
NotFoundInIndex
);
@ -548,7 +550,7 @@ mod tests {
assert_eq!(
temp.package_manager
.install_package("italian-accents", false)
.install_package("italian-accents", false, None)
.unwrap(),
AlreadyInstalled
);
@ -563,7 +565,7 @@ mod tests {
assert_eq!(
temp.package_manager
.install_package("dummy-package", false)
.install_package("dummy-package", false, None)
.unwrap(),
Installed
);
@ -589,7 +591,7 @@ mod tests {
assert_eq!(
temp.package_manager
.install_package("not-existing", false)
.install_package("not-existing", false, None)
.unwrap(),
NotFoundInRepo
);
@ -604,7 +606,7 @@ mod tests {
assert_eq!(
temp.package_manager
.install_package("dummy-package2", false)
.install_package("dummy-package2", false, None)
.unwrap(),
MissingPackageVersion
);
@ -619,7 +621,7 @@ mod tests {
assert_eq!(
temp.package_manager
.install_package("dummy-package3", false)
.install_package("dummy-package3", false, None)
.unwrap(),
UnableToParsePackageInfo
);
@ -634,7 +636,7 @@ mod tests {
assert_eq!(
temp.package_manager
.install_package("dummy-package4", false)
.install_package("dummy-package4", false, None)
.unwrap(),
UnableToParsePackageInfo
);
@ -649,7 +651,7 @@ mod tests {
assert_eq!(
temp.package_manager
.install_package("dummy-package", false)
.install_package("dummy-package", false, None)
.unwrap(),
Installed
);

View File

@ -1,33 +0,0 @@
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<TempDir, Box<dyn Error>> {
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());
}
}

View File

@ -34,11 +34,13 @@ pub trait PackageManager {
&self,
name: &str,
allow_external: bool,
proxy: Option<String>,
) -> Result<InstallResult, Box<dyn Error>>;
fn install_package_from_repo(
&self,
name: &str,
repo_url: &str,
proxy: Option<String>,
) -> Result<InstallResult, Box<dyn Error>>;
fn remove_package(&self, name: &str) -> Result<RemoveResult, Box<dyn Error>>;
@ -47,7 +49,11 @@ pub trait PackageManager {
}
pub trait PackageResolver {
fn clone_repo_to_temp(&self, repo_url: &str) -> Result<TempDir, Box<dyn Error>>;
fn clone_repo_to_temp(
&self,
repo_url: &str,
proxy: Option<String>,
) -> Result<TempDir, Box<dyn Error>>;
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]

View File

@ -13,13 +13,26 @@ impl ZipPackageResolver {
}
impl super::PackageResolver for ZipPackageResolver {
fn clone_repo_to_temp(&self, repo_url: &str) -> Result<TempDir, Box<dyn Error>> {
fn clone_repo_to_temp(
&self,
repo_url: &str,
proxy: Option<String>,
) -> Result<TempDir, Box<dyn Error>> {
let temp_dir = TempDir::new()?;
let zip_url = repo_url.to_owned() + "/archive/master.zip";
let mut client = reqwest::Client::builder();
if let Some(proxy) = proxy {
let proxy = reqwest::Proxy::https(&proxy).expect("unable to setup https proxy");
client = client.proxy(proxy);
};
let client = client.build().expect("unable to create http client");
// Download the archive from GitHub
let mut response = reqwest::get(&zip_url)?;
let mut response = client.get(&zip_url).send()?;
// Extract zip file
let mut buffer = Vec::new();
@ -90,7 +103,7 @@ mod tests {
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")
.clone_repo_to_temp("https://github.com/federico-terzi/espanso-hub-core", None)
.unwrap();
assert!(cloned_dir.path().join("LICENSE").exists());
}

BIN
src/res/mac/AppIcon.icns Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

30
src/res/mac/modulo.plist Normal file
View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>{{{modulo_path}}}</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>>
<key>CFBundleIdentifier</key>
<string>com.federicoterzi.modulo</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Modulo</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

64
src/ui/modulo/mac.rs Normal file
View File

@ -0,0 +1,64 @@
use log::info;
use std::os::unix::fs::symlink;
use std::path::PathBuf;
const MODULO_APP_BUNDLE_NAME: &str = "Modulo.app";
const MODULO_APP_BUNDLE_PLIST_CONTENT: &'static str = include_str!("../../res/mac/modulo.plist");
const MODULO_APP_BUNDLE_ICON: &[u8] = include_bytes!("../../res/mac/AppIcon.icns");
pub fn generate_modulo_app_bundle(modulo_path: &str) -> Result<PathBuf, std::io::Error> {
let modulo_pathbuf = PathBuf::from(modulo_path);
let modulo_path: String = if !modulo_pathbuf.exists() {
// If modulo was taken from the PATH, we need to calculate the absolute path
// To do so, we use the `which` command
let output = std::process::Command::new("which")
.arg("modulo")
.output()
.expect("unable to call 'which' command to determine modulo's full path");
let path = String::from_utf8_lossy(output.stdout.as_slice());
let path = path.trim();
info!("Detected modulo's full path: {:?}", &path);
path.to_string()
} else {
modulo_path.to_owned()
};
let data_dir = crate::context::get_data_dir();
let modulo_app_dir = data_dir.join(MODULO_APP_BUNDLE_NAME);
// Remove previous bundle if present
if modulo_app_dir.exists() {
std::fs::remove_dir_all(&modulo_app_dir)?;
}
// Recreate the App bundle stub
std::fs::create_dir(&modulo_app_dir)?;
let contents_dir = modulo_app_dir.join("Contents");
std::fs::create_dir(&contents_dir)?;
let macos_dir = contents_dir.join("MacOS");
std::fs::create_dir(&macos_dir)?;
let resources_dir = contents_dir.join("Resources");
std::fs::create_dir(&resources_dir)?;
// Generate the Plist file
let plist_content = MODULO_APP_BUNDLE_PLIST_CONTENT.replace("{{{modulo_path}}}", &modulo_path);
let plist_file = contents_dir.join("Info.plist");
std::fs::write(plist_file, plist_content)?;
// Copy the icon file
let icon_file = resources_dir.join("AppIcon.icns");
std::fs::write(icon_file, MODULO_APP_BUNDLE_ICON)?;
// Generate the symbolic link to the modulo binary
let target_link = macos_dir.join("modulo");
symlink(modulo_path, &target_link)?;
info!("Created Modulo APP stub at: {:?}", &target_link);
Ok(target_link)
}

View File

@ -3,6 +3,9 @@ use log::{error, info};
use std::io::{Error, Write};
use std::process::{Child, Command, Output};
#[cfg(target_os = "macos")]
mod mac;
pub struct ModuloManager {
modulo_path: Option<String>,
}
@ -41,8 +44,23 @@ impl ModuloManager {
}
}
if let Some(ref modulo_path) = modulo_path {
info!("Using modulo at {:?}", modulo_path);
if let Some(ref path) = modulo_path {
info!("Using modulo at {:?}", path);
// MacOS specific remark
// In order to give modulo the focus when spawning a form, modulo has to be
// wrapped inside an application bundle. Therefore, we generate a bundle
// at startup.
// See issue: https://github.com/federico-terzi/espanso/issues/430
#[cfg(target_os = "macos")]
{
modulo_path = Some(
mac::generate_modulo_app_bundle(path)
.expect("unable to generate modulo app stub")
.to_string_lossy()
.to_string(),
);
}
}
Self { modulo_path }