commit
c9a33bf356
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -371,7 +371,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
description = "Cross-platform Text Expander written in Rust"
|
description = "Cross-platform Text Expander written in Rust"
|
||||||
|
@ -10,7 +10,7 @@ edition = "2018"
|
||||||
build="build.rs"
|
build="build.rs"
|
||||||
|
|
||||||
[modulo]
|
[modulo]
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
widestring = "0.4.0"
|
widestring = "0.4.0"
|
||||||
|
|
|
@ -29,5 +29,7 @@
|
||||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
|
||||||
- (IBAction) statusIconClick: (id) sender;
|
- (IBAction) statusIconClick: (id) sender;
|
||||||
- (IBAction) contextMenuClick: (id) sender;
|
- (IBAction) contextMenuClick: (id) sender;
|
||||||
|
- (void) updateIcon: (char *)iconPath;
|
||||||
|
- (void) setIcon: (char *)iconPath;
|
||||||
|
|
||||||
@end
|
@end
|
|
@ -27,14 +27,7 @@
|
||||||
if (show_icon) {
|
if (show_icon) {
|
||||||
myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
|
myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
|
||||||
|
|
||||||
NSString *nsIconPath = [NSString stringWithUTF8String:icon_path];
|
[self setIcon: 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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup key listener
|
// 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 {
|
- (IBAction) statusIconClick: (id) sender {
|
||||||
icon_click_callback(context_instance);
|
icon_click_callback(context_instance);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,12 +26,13 @@ extern "C" {
|
||||||
|
|
||||||
extern void * context_instance;
|
extern void * context_instance;
|
||||||
extern char * icon_path;
|
extern char * icon_path;
|
||||||
|
extern char * disabled_icon_path;
|
||||||
extern int32_t show_icon;
|
extern int32_t show_icon;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Initialize the AppDelegate and check for accessibility permissions
|
* 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.
|
* 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 ContextMenuClickCallback context_menu_click_callback;
|
||||||
extern "C" void register_context_menu_click_callback(ContextMenuClickCallback 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
|
// SYSTEM
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -33,6 +33,7 @@ extern "C" {
|
||||||
|
|
||||||
void * context_instance;
|
void * context_instance;
|
||||||
char * icon_path;
|
char * icon_path;
|
||||||
|
char * disabled_icon_path;
|
||||||
int32_t show_icon;
|
int32_t show_icon;
|
||||||
AppDelegate * delegate_ptr;
|
AppDelegate * delegate_ptr;
|
||||||
|
|
||||||
|
@ -40,9 +41,10 @@ KeypressCallback keypress_callback;
|
||||||
IconClickCallback icon_click_callback;
|
IconClickCallback icon_click_callback;
|
||||||
ContextMenuClickCallback context_menu_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;
|
context_instance = context;
|
||||||
icon_path = strdup(_icon_path);
|
icon_path = strdup(_icon_path);
|
||||||
|
disabled_icon_path = strdup(_disabled_icon_path);
|
||||||
show_icon = _show_icon;
|
show_icon = _show_icon;
|
||||||
|
|
||||||
AppDelegate *delegate = [[AppDelegate alloc] init];
|
AppDelegate *delegate = [[AppDelegate alloc] init];
|
||||||
|
@ -74,6 +76,18 @@ int32_t headless_eventloop() {
|
||||||
return 0;
|
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) {
|
void send_string(const char * string) {
|
||||||
char * stringCopy = strdup(string);
|
char * stringCopy = strdup(string);
|
||||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
|
|
|
@ -663,6 +663,25 @@ void trigger_copy() {
|
||||||
SendInput(vec.size(), vec.data(), sizeof(INPUT));
|
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
|
// SYSTEM
|
||||||
|
|
||||||
|
@ -808,7 +827,7 @@ int32_t set_clipboard(wchar_t *text) {
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t get_clipboard(wchar_t *buffer, int32_t size) {
|
int32_t get_clipboard(wchar_t *buffer, int32_t size) {
|
||||||
int32_t result = 0;
|
int32_t result = 1;
|
||||||
if (!OpenClipboard(NULL)) {
|
if (!OpenClipboard(NULL)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,11 @@ extern "C" void trigger_shift_paste();
|
||||||
*/
|
*/
|
||||||
extern "C" void trigger_copy();
|
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
|
// Detect current application commands
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: espanso
|
name: espanso
|
||||||
version: 0.7.1
|
version: 0.7.2
|
||||||
summary: A Cross-platform Text Expander written in Rust
|
summary: A Cross-platform Text Expander written in Rust
|
||||||
description: |
|
description: |
|
||||||
espanso is a Cross-platform, Text Expander written in Rust.
|
espanso is a Cross-platform, Text Expander written in Rust.
|
||||||
|
|
|
@ -29,7 +29,12 @@ pub struct MacMenuItem {
|
||||||
#[allow(improper_ctypes)]
|
#[allow(improper_ctypes)]
|
||||||
#[link(name = "macbridge", kind = "static")]
|
#[link(name = "macbridge", kind = "static")]
|
||||||
extern "C" {
|
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 eventloop();
|
||||||
pub fn headless_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 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 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 register_context_menu_click_callback(cb: extern "C" fn(_self: *mut c_void, id: i32));
|
||||||
|
pub fn update_tray_icon(enabled: i32);
|
||||||
|
|
||||||
// Keyboard
|
// Keyboard
|
||||||
pub fn register_keypress_callback(
|
pub fn register_keypress_callback(
|
||||||
|
|
|
@ -69,6 +69,7 @@ extern "C" {
|
||||||
pub fn trigger_paste();
|
pub fn trigger_paste();
|
||||||
pub fn trigger_shift_paste();
|
pub fn trigger_shift_paste();
|
||||||
pub fn trigger_copy();
|
pub fn trigger_copy();
|
||||||
|
pub fn are_modifiers_pressed() -> i32;
|
||||||
|
|
||||||
// PROCESSES
|
// PROCESSES
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ use std::sync::Arc;
|
||||||
use std::{fs, thread};
|
use std::{fs, thread};
|
||||||
|
|
||||||
const STATUS_ICON_BINARY: &[u8] = include_bytes!("../res/mac/icon.png");
|
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 struct MacContext {
|
||||||
pub send_channel: Sender<Event>,
|
pub send_channel: Sender<Event>,
|
||||||
|
@ -72,6 +73,7 @@ impl MacContext {
|
||||||
// Initialize the status icon path
|
// Initialize the status icon path
|
||||||
let espanso_dir = super::get_data_dir();
|
let espanso_dir = super::get_data_dir();
|
||||||
let status_icon_target = espanso_dir.join("icon.png");
|
let status_icon_target = espanso_dir.join("icon.png");
|
||||||
|
let disabled_status_icon_target = espanso_dir.join("icondisabled.png");
|
||||||
|
|
||||||
if status_icon_target.exists() {
|
if status_icon_target.exists() {
|
||||||
info!("Status icon already initialized, skipping.");
|
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 {
|
unsafe {
|
||||||
let context_ptr = &*context as *const MacContext as *const c_void;
|
let context_ptr = &*context as *const MacContext as *const c_void;
|
||||||
|
|
||||||
|
@ -93,9 +108,17 @@ impl MacContext {
|
||||||
|
|
||||||
let status_icon_path =
|
let status_icon_path =
|
||||||
CString::new(status_icon_target.to_str().unwrap_or_default()).unwrap_or_default();
|
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 };
|
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
|
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 {
|
impl super::Context for MacContext {
|
||||||
fn eventloop(&self) {
|
fn eventloop(&self) {
|
||||||
// Start the SecureInput watcher thread
|
// Start the SecureInput watcher thread
|
||||||
|
|
|
@ -50,7 +50,7 @@ pub fn new(
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn update_icon(enabled: bool) {
|
pub fn update_icon(enabled: bool) {
|
||||||
// TODO: add update icon on macOS
|
macos::update_icon(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
|
|
@ -498,9 +498,14 @@ impl<
|
||||||
if config.secure_input_notification && config.show_notifications {
|
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);
|
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 => {
|
SystemEvent::SecureInputDisabled => {
|
||||||
info!("SecureInput has been disabled.");
|
info!("SecureInput has been disabled.");
|
||||||
|
|
||||||
|
let is_enabled = self.enabled.borrow();
|
||||||
|
crate::context::update_icon(*is_enabled);
|
||||||
}
|
}
|
||||||
SystemEvent::NotifyRequest(message) => {
|
SystemEvent::NotifyRequest(message) => {
|
||||||
let config = self.config_manager.default_config();
|
let config = self.config_manager.default_config();
|
||||||
|
|
|
@ -74,7 +74,7 @@ impl super::Extension for FormExtension {
|
||||||
.manager
|
.manager
|
||||||
.invoke(&["form", "-i", "-"], &serialized_config);
|
.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();
|
on_form_close();
|
||||||
|
|
||||||
if let Some(output) = output {
|
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() {
|
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")]
|
#[cfg(target_os = "macos")]
|
||||||
|
|
|
@ -21,7 +21,7 @@ use crate::config::Configs;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
mod windows;
|
pub mod windows;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
mod 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")
|
.help("(Optional) Link to GitHub repository")
|
||||||
.required(false)
|
.required(false)
|
||||||
.default_value("hub"),
|
.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")
|
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")
|
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 package_resolver = Box::new(ZipPackageResolver::new());
|
||||||
|
|
||||||
let allow_external: bool = if matches.is_present("external") {
|
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.")
|
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 {
|
} else {
|
||||||
// Make sure the repo is a valid github url
|
// Make sure the repo is a valid github url
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -1147,7 +1162,7 @@ fn install_main(_config_set: ConfigSet, matches: &ArgMatches) {
|
||||||
if !allow_external {
|
if !allow_external {
|
||||||
Ok(InstallResult::BlockedExternalPackage(repository.to_owned()))
|
Ok(InstallResult::BlockedExternalPackage(repository.to_owned()))
|
||||||
} else {
|
} 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,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
allow_external: bool,
|
allow_external: bool,
|
||||||
|
proxy: Option<String>,
|
||||||
) -> Result<InstallResult, Box<dyn Error>> {
|
) -> Result<InstallResult, Box<dyn Error>> {
|
||||||
let package = self.get_package(name);
|
let package = self.get_package(name);
|
||||||
match package {
|
match package {
|
||||||
Some(package) => {
|
Some(package) => {
|
||||||
if package.is_core || allow_external {
|
if package.is_core || allow_external {
|
||||||
self.install_package_from_repo(name, &package.repo)
|
self.install_package_from_repo(name, &package.repo, proxy)
|
||||||
} else {
|
} else {
|
||||||
Ok(BlockedExternalPackage(package.original_repo))
|
Ok(BlockedExternalPackage(package.original_repo))
|
||||||
}
|
}
|
||||||
|
@ -282,6 +283,7 @@ impl super::PackageManager for DefaultPackageManager {
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
repo_url: &str,
|
repo_url: &str,
|
||||||
|
proxy: Option<String>,
|
||||||
) -> Result<InstallResult, Box<dyn Error>> {
|
) -> Result<InstallResult, Box<dyn Error>> {
|
||||||
// Check if package is already installed
|
// Check if package is already installed
|
||||||
let packages = self.list_local_packages_names();
|
let packages = self.list_local_packages_names();
|
||||||
|
@ -294,7 +296,7 @@ impl super::PackageManager for DefaultPackageManager {
|
||||||
.package_resolver
|
.package_resolver
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone_repo_to_temp(repo_url)?;
|
.clone_repo_to_temp(repo_url, proxy)?;
|
||||||
|
|
||||||
let temp_package_dir = temp_dir.path().join(name);
|
let temp_package_dir = temp_dir.path().join(name);
|
||||||
if !temp_package_dir.exists() {
|
if !temp_package_dir.exists() {
|
||||||
|
@ -532,7 +534,7 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
temp.package_manager
|
temp.package_manager
|
||||||
.install_package("doesnotexist", false)
|
.install_package("doesnotexist", false, None)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
NotFoundInIndex
|
NotFoundInIndex
|
||||||
);
|
);
|
||||||
|
@ -548,7 +550,7 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
temp.package_manager
|
temp.package_manager
|
||||||
.install_package("italian-accents", false)
|
.install_package("italian-accents", false, None)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
AlreadyInstalled
|
AlreadyInstalled
|
||||||
);
|
);
|
||||||
|
@ -563,7 +565,7 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
temp.package_manager
|
temp.package_manager
|
||||||
.install_package("dummy-package", false)
|
.install_package("dummy-package", false, None)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
Installed
|
Installed
|
||||||
);
|
);
|
||||||
|
@ -589,7 +591,7 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
temp.package_manager
|
temp.package_manager
|
||||||
.install_package("not-existing", false)
|
.install_package("not-existing", false, None)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
NotFoundInRepo
|
NotFoundInRepo
|
||||||
);
|
);
|
||||||
|
@ -604,7 +606,7 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
temp.package_manager
|
temp.package_manager
|
||||||
.install_package("dummy-package2", false)
|
.install_package("dummy-package2", false, None)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
MissingPackageVersion
|
MissingPackageVersion
|
||||||
);
|
);
|
||||||
|
@ -619,7 +621,7 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
temp.package_manager
|
temp.package_manager
|
||||||
.install_package("dummy-package3", false)
|
.install_package("dummy-package3", false, None)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
UnableToParsePackageInfo
|
UnableToParsePackageInfo
|
||||||
);
|
);
|
||||||
|
@ -634,7 +636,7 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
temp.package_manager
|
temp.package_manager
|
||||||
.install_package("dummy-package4", false)
|
.install_package("dummy-package4", false, None)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
UnableToParsePackageInfo
|
UnableToParsePackageInfo
|
||||||
);
|
);
|
||||||
|
@ -649,7 +651,7 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
temp.package_manager
|
temp.package_manager
|
||||||
.install_package("dummy-package", false)
|
.install_package("dummy-package", false, None)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
Installed
|
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,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
allow_external: bool,
|
allow_external: bool,
|
||||||
|
proxy: Option<String>,
|
||||||
) -> Result<InstallResult, Box<dyn Error>>;
|
) -> Result<InstallResult, Box<dyn Error>>;
|
||||||
fn install_package_from_repo(
|
fn install_package_from_repo(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
repo_url: &str,
|
repo_url: &str,
|
||||||
|
proxy: Option<String>,
|
||||||
) -> Result<InstallResult, Box<dyn Error>>;
|
) -> Result<InstallResult, Box<dyn Error>>;
|
||||||
|
|
||||||
fn remove_package(&self, name: &str) -> Result<RemoveResult, Box<dyn Error>>;
|
fn remove_package(&self, name: &str) -> Result<RemoveResult, Box<dyn Error>>;
|
||||||
|
@ -47,7 +49,11 @@ pub trait PackageManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PackageResolver {
|
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)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
|
|
@ -13,13 +13,26 @@ impl ZipPackageResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::PackageResolver for 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 temp_dir = TempDir::new()?;
|
||||||
|
|
||||||
let zip_url = repo_url.to_owned() + "/archive/master.zip";
|
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
|
// Download the archive from GitHub
|
||||||
let mut response = reqwest::get(&zip_url)?;
|
let mut response = client.get(&zip_url).send()?;
|
||||||
|
|
||||||
// Extract zip file
|
// Extract zip file
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
|
@ -90,7 +103,7 @@ mod tests {
|
||||||
fn test_clone_temp_repository() {
|
fn test_clone_temp_repository() {
|
||||||
let resolver = ZipPackageResolver::new();
|
let resolver = ZipPackageResolver::new();
|
||||||
let cloned_dir = resolver
|
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();
|
.unwrap();
|
||||||
assert!(cloned_dir.path().join("LICENSE").exists());
|
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::io::{Error, Write};
|
||||||
use std::process::{Child, Command, Output};
|
use std::process::{Child, Command, Output};
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod mac;
|
||||||
|
|
||||||
pub struct ModuloManager {
|
pub struct ModuloManager {
|
||||||
modulo_path: Option<String>,
|
modulo_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -41,8 +44,23 @@ impl ModuloManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref modulo_path) = modulo_path {
|
if let Some(ref path) = modulo_path {
|
||||||
info!("Using modulo at {:?}", 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 }
|
Self { modulo_path }
|
||||||
|
|
Loading…
Reference in New Issue
Block a user