commit
c9a33bf356
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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)",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
/*
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
/*
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
19
src/main.rs
19
src/main.rs
|
@ -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)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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)]
|
||||
|
|
|
@ -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
BIN
src/res/mac/AppIcon.icns
Normal file
Binary file not shown.
BIN
src/res/mac/icondisabled.png
Normal file
BIN
src/res/mac/icondisabled.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
30
src/res/mac/modulo.plist
Normal file
30
src/res/mac/modulo.plist
Normal 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
64
src/ui/modulo/mac.rs
Normal 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)
|
||||
}
|
|
@ -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 }
|
||||
|
|
Loading…
Reference in New Issue
Block a user