diff --git a/.github/scripts/ubuntu/Dockerfile b/.github/scripts/ubuntu/Dockerfile index f974dd6..50379af 100644 --- a/.github/scripts/ubuntu/Dockerfile +++ b/.github/scripts/ubuntu/Dockerfile @@ -9,7 +9,7 @@ RUN apt-get update \ ENV RUSTUP_HOME=/usr/local/rustup \ CARGO_HOME=/usr/local/cargo \ PATH=/usr/local/cargo/bin:$PATH \ - RUST_VERSION=1.55.0 + RUST_VERSION=1.57.0 RUN set -eux; \ dpkgArch="$(dpkg --print-architecture)"; \ diff --git a/Cargo.lock b/Cargo.lock index a9e3bdf..1dd561d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,9 +187,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.66" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[package]] name = "cfg-if" @@ -212,6 +212,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "pure-rust-locales", "time", "winapi 0.3.9", ] @@ -399,6 +400,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "cstr_core" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644828c273c063ab0d39486ba42a5d1f3a499d35529c759e763a9c6cb8a0fb08" +dependencies = [ + "cty", + "memchr", +] + [[package]] name = "ctor" version = "0.1.20" @@ -409,6 +420,12 @@ dependencies = [ "syn 1.0.67", ] +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + [[package]] name = "dbus" version = "0.9.1" @@ -572,7 +589,7 @@ dependencies = [ [[package]] name = "espanso" -version = "2.1.3-alpha" +version = "2.1.4-beta" dependencies = [ "anyhow", "caps", @@ -851,6 +868,7 @@ dependencies = [ "log", "rand 0.8.3", "regex", + "sys-locale", "thiserror", ] @@ -2040,6 +2058,12 @@ dependencies = [ "unicode-xid 0.2.1", ] +[[package]] +name = "pure-rust-locales" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45c49fc4f91f35bae654f85ebb3a44d60ac64f11b3166ffa609def390c732d8" + [[package]] name = "quote" version = "0.3.15" @@ -2577,6 +2601,19 @@ dependencies = [ "unicode-xid 0.0.4", ] +[[package]] +name = "sys-locale" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f89ebb59fa30d4f65fafc2d68e94f6975256fd87e812dd99cb6e020c8563df" +dependencies = [ + "cc", + "cstr_core", + "libc", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "tempdir" version = "0.3.7" diff --git a/espanso-clipboard/Cargo.toml b/espanso-clipboard/Cargo.toml index b389613..943588f 100644 --- a/espanso-clipboard/Cargo.toml +++ b/espanso-clipboard/Cargo.toml @@ -28,4 +28,4 @@ widestring = "0.4.3" wait-timeout = { version = "0.2.0", optional = true } [build-dependencies] -cc = "1.0.66" \ No newline at end of file +cc = "1.0.73" \ No newline at end of file diff --git a/espanso-clipboard/src/cocoa/native.mm b/espanso-clipboard/src/cocoa/native.mm index 91ed16a..a5bbf38 100644 --- a/espanso-clipboard/src/cocoa/native.mm +++ b/espanso-clipboard/src/cocoa/native.mm @@ -30,8 +30,6 @@ int32_t clipboard_get_text(char * buffer, int32_t buffer_size) { const char * text = [string UTF8String]; strncpy(buffer, text, buffer_size); - [string release]; - return 1; } } diff --git a/espanso-config/src/config/default.rs b/espanso-config/src/config/default.rs index 09decba..7966ba7 100644 --- a/espanso-config/src/config/default.rs +++ b/espanso-config/src/config/default.rs @@ -21,3 +21,5 @@ pub(crate) const DEFAULT_CLIPBOARD_THRESHOLD: usize = 100; pub(crate) const DEFAULT_PRE_PASTE_DELAY: usize = 100; pub(crate) const DEFAULT_SHORTCUT_EVENT_DELAY: usize = 10; pub(crate) const DEFAULT_RESTORE_CLIPBOARD_DELAY: usize = 300; +pub(crate) const DEFAULT_POST_FORM_DELAY: usize = 200; +pub(crate) const DEFAULT_POST_SEARCH_DELAY: usize = 200; diff --git a/espanso-config/src/config/mod.rs b/espanso-config/src/config/mod.rs index 0992372..945841e 100644 --- a/espanso-config/src/config/mod.rs +++ b/espanso-config/src/config/mod.rs @@ -150,6 +150,18 @@ pub trait Config: Send + Sync { // If false, avoid showing the SecureInput notification on macOS fn secure_input_notification(&self) -> bool; + // The number of milliseconds to wait after a form has been closed. + // This is useful to let the target application regain focus + // after a form has been closed, otherwise the injection might + // not be targeted to the right application. + fn post_form_delay(&self) -> usize; + + // The number of milliseconds to wait after the search bar has been closed. + // This is useful to let the target application regain focus + // after the search bar has been closed, otherwise the injection might + // not be targeted to the right application. + fn post_search_delay(&self) -> usize; + // If true, use the `xclip` command to implement the clipboard instead of // the built-in native module on X11. fn x11_use_xclip_backend(&self) -> bool; @@ -188,6 +200,8 @@ pub trait Config: Send + Sync { toggle_key: {:?} auto_restart: {:?} restore_clipboard_delay: {:?} + post_form_delay: {:?} + post_search_delay: {:?} backspace_limit: {} search_trigger: {:?} search_shortcut: {:?} @@ -220,6 +234,8 @@ pub trait Config: Send + Sync { self.toggle_key(), self.auto_restart(), self.restore_clipboard_delay(), + self.post_form_delay(), + self.post_search_delay(), self.backspace_limit(), self.search_trigger(), self.search_shortcut(), diff --git a/espanso-config/src/config/parse/mod.rs b/espanso-config/src/config/parse/mod.rs index 94f174d..644b068 100644 --- a/espanso-config/src/config/parse/mod.rs +++ b/espanso-config/src/config/parse/mod.rs @@ -44,6 +44,8 @@ pub(crate) struct ParsedConfig { pub show_notifications: Option, pub show_icon: Option, pub secure_input_notification: Option, + pub post_form_delay: Option, + pub post_search_delay: Option, pub win32_exclude_orphan_events: Option, pub win32_keyboard_layout_cache_interval: Option, pub x11_use_xclip_backend: Option, diff --git a/espanso-config/src/config/parse/yaml.rs b/espanso-config/src/config/parse/yaml.rs index d666662..64558db 100644 --- a/espanso-config/src/config/parse/yaml.rs +++ b/espanso-config/src/config/parse/yaml.rs @@ -103,6 +103,12 @@ pub(crate) struct YAMLConfig { #[serde(default)] pub show_icon: Option, + #[serde(default)] + pub post_form_delay: Option, + + #[serde(default)] + pub post_search_delay: Option, + #[serde(default)] pub secure_input_notification: Option, @@ -201,6 +207,8 @@ impl TryFrom for ParsedConfig { pre_paste_delay: yaml_config.pre_paste_delay, restore_clipboard_delay: yaml_config.restore_clipboard_delay, paste_shortcut_event_delay: yaml_config.paste_shortcut_event_delay, + post_form_delay: yaml_config.post_form_delay, + post_search_delay: yaml_config.post_search_delay, win32_exclude_orphan_events: yaml_config.win32_exclude_orphan_events, win32_keyboard_layout_cache_interval: yaml_config.win32_keyboard_layout_cache_interval, @@ -260,6 +268,8 @@ mod tests { show_icon: false show_notifications: false secure_input_notification: false + post_form_delay: 300 + post_search_delay: 400 win32_exclude_orphan_events: false win32_keyboard_layout_cache_interval: 300 x11_use_xclip_backend: true @@ -314,6 +324,8 @@ mod tests { show_icon: Some(false), show_notifications: Some(false), secure_input_notification: Some(false), + post_form_delay: Some(300), + post_search_delay: Some(400), win32_exclude_orphan_events: Some(false), win32_keyboard_layout_cache_interval: Some(300), x11_use_xclip_backend: Some(true), diff --git a/espanso-config/src/config/resolve.rs b/espanso-config/src/config/resolve.rs index 518c1ff..7b7dcc3 100644 --- a/espanso-config/src/config/resolve.rs +++ b/espanso-config/src/config/resolve.rs @@ -19,8 +19,8 @@ use super::{ default::{ - DEFAULT_CLIPBOARD_THRESHOLD, DEFAULT_PRE_PASTE_DELAY, DEFAULT_RESTORE_CLIPBOARD_DELAY, - DEFAULT_SHORTCUT_EVENT_DELAY, + DEFAULT_CLIPBOARD_THRESHOLD, DEFAULT_POST_FORM_DELAY, DEFAULT_POST_SEARCH_DELAY, + DEFAULT_PRE_PASTE_DELAY, DEFAULT_RESTORE_CLIPBOARD_DELAY, DEFAULT_SHORTCUT_EVENT_DELAY, }, parse::ParsedConfig, path::calculate_paths, @@ -299,6 +299,20 @@ impl Config for ResolvedConfig { self.parsed.secure_input_notification.unwrap_or(true) } + fn post_form_delay(&self) -> usize { + self + .parsed + .post_form_delay + .unwrap_or(DEFAULT_POST_FORM_DELAY) + } + + fn post_search_delay(&self) -> usize { + self + .parsed + .post_search_delay + .unwrap_or(DEFAULT_POST_SEARCH_DELAY) + } + fn win32_exclude_orphan_events(&self) -> bool { self.parsed.win32_exclude_orphan_events.unwrap_or(true) } @@ -398,6 +412,8 @@ impl ResolvedConfig { show_icon, show_notifications, secure_input_notification, + post_form_delay, + post_search_delay, win32_exclude_orphan_events, win32_keyboard_layout_cache_interval, x11_use_xclip_backend, diff --git a/espanso-config/src/legacy/mod.rs b/espanso-config/src/legacy/mod.rs index b67e0f7..24a94aa 100644 --- a/espanso-config/src/legacy/mod.rs +++ b/espanso-config/src/legacy/mod.rs @@ -387,6 +387,14 @@ impl Config for LegacyInteropConfig { self.config.enable_active } + fn post_form_delay(&self) -> usize { + crate::config::default::DEFAULT_POST_FORM_DELAY + } + + fn post_search_delay(&self) -> usize { + crate::config::default::DEFAULT_POST_SEARCH_DELAY + } + fn win32_exclude_orphan_events(&self) -> bool { true } diff --git a/espanso-detect/Cargo.toml b/espanso-detect/Cargo.toml index ae760fd..b97c457 100644 --- a/espanso-detect/Cargo.toml +++ b/espanso-detect/Cargo.toml @@ -27,7 +27,7 @@ scopeguard = "1.1.0" sctk = { package = "smithay-client-toolkit", version = "0.14.0", optional = true } [build-dependencies] -cc = "1.0.66" +cc = "1.0.73" [dev-dependencies] enum-as-inner = "0.3.3" \ No newline at end of file diff --git a/espanso-detect/src/layout/mod.rs b/espanso-detect/src/layout/mod.rs index 0215529..83480df 100644 --- a/espanso-detect/src/layout/mod.rs +++ b/espanso-detect/src/layout/mod.rs @@ -37,7 +37,6 @@ pub fn get_active_layout() -> Option { if gnome::is_gnome() { gnome::get_active_layout() } else { - log::warn!("unable to determine the currently active layout, you might need to explicitly specify the layout in the config for espanso to work correctly."); None } } diff --git a/espanso-engine/src/process/middleware/image_resolve.rs b/espanso-engine/src/process/middleware/image_resolve.rs index 8a9569f..7fb1151 100644 --- a/espanso-engine/src/process/middleware/image_resolve.rs +++ b/espanso-engine/src/process/middleware/image_resolve.rs @@ -47,7 +47,7 @@ impl<'a> Middleware for ImageResolverMiddleware<'a> { if let EventType::ImageRequested(m_event) = &event.etype { // On Windows, we have to replace the forward / with the backslash \ in the path let path = if cfg!(target_os = "windows") { - m_event.image_path.replace("/", "\\") + m_event.image_path.replace('/', "\\") } else { m_event.image_path.to_owned() }; diff --git a/espanso-info/Cargo.toml b/espanso-info/Cargo.toml index 56aa979..2a19659 100644 --- a/espanso-info/Cargo.toml +++ b/espanso-info/Cargo.toml @@ -20,4 +20,4 @@ lazy_static = "1.4.0" widestring = "0.4.3" [build-dependencies] -cc = "1.0.66" \ No newline at end of file +cc = "1.0.73" \ No newline at end of file diff --git a/espanso-info/src/cocoa/native.mm b/espanso-info/src/cocoa/native.mm index 86d9403..b437952 100644 --- a/espanso-info/src/cocoa/native.mm +++ b/espanso-info/src/cocoa/native.mm @@ -23,29 +23,27 @@ int32_t info_get_title(char *buffer, int32_t buffer_size) { - @autoreleasepool { - CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListExcludeDesktopElements | kCGWindowListOptionOnScreenOnly, kCGNullWindowID); - int32_t result = 0; + CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListExcludeDesktopElements | kCGWindowListOptionOnScreenOnly, kCGNullWindowID); + int32_t result = 0; - if (windows) { - for (NSDictionary *window in (NSArray *)windows) { - NSNumber *ownerPid = window[(id) kCGWindowOwnerPID]; + if (windows) { + for (NSDictionary *window in (NSArray *)windows) { + NSNumber *ownerPid = window[(id) kCGWindowOwnerPID]; - NSRunningApplication *currentApp = [NSRunningApplication runningApplicationWithProcessIdentifier: [ownerPid intValue]]; + NSRunningApplication *currentApp = [NSRunningApplication runningApplicationWithProcessIdentifier: [ownerPid intValue]]; - if ([currentApp isActive]) { - NSString *name = window[(id) kCGWindowName]; - if (name.length > 0) { - const char * title = [name UTF8String]; - snprintf(buffer, buffer_size, "%s", title); - result = 1; - } - break; + if ([currentApp isActive]) { + NSString *name = window[(id) kCGWindowName]; + if (name.length > 0) { + const char * title = [name UTF8String]; + snprintf(buffer, buffer_size, "%s", title); + result = 1; } + break; } - - CFRelease(windows); } + + CFRelease(windows); } return 0; @@ -54,70 +52,64 @@ int32_t info_get_title(char *buffer, int32_t buffer_size) // Partially taken from: https://stackoverflow.com/questions/480866/get-the-title-of-the-current-active-window-document-in-mac-os-x/23451568#23451568 int32_t info_get_title_fallback(char *buffer, int32_t buffer_size) { - @autoreleasepool { - // Get the process ID of the frontmost application. - NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication]; - pid_t pid = [app processIdentifier]; + // Get the process ID of the frontmost application. + NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication]; + pid_t pid = [app processIdentifier]; - AXUIElementRef appElem = AXUIElementCreateApplication(pid); - if (!appElem) { - return -1; - } + AXUIElementRef appElem = AXUIElementCreateApplication(pid); + if (!appElem) { + return -1; + } - // Get the accessibility element corresponding to the frontmost window - // of the frontmost application. - AXUIElementRef window = NULL; - if (AXUIElementCopyAttributeValue(appElem, - kAXFocusedWindowAttribute, (CFTypeRef*)&window) != kAXErrorSuccess) { - CFRelease(appElem); - return -2; - } - - // Finally, get the title of the frontmost window. - CFStringRef title = NULL; - AXError result = AXUIElementCopyAttributeValue(window, kAXTitleAttribute, - (CFTypeRef*)&title); - - // At this point, we don't need window and appElem anymore. - CFRelease(window); + // Get the accessibility element corresponding to the frontmost window + // of the frontmost application. + AXUIElementRef window = NULL; + if (AXUIElementCopyAttributeValue(appElem, + kAXFocusedWindowAttribute, (CFTypeRef*)&window) != kAXErrorSuccess) { CFRelease(appElem); + return -2; + } - if (result != kAXErrorSuccess) { - // Failed to get the window title. - return -3; - } + // Finally, get the title of the frontmost window. + CFStringRef title = NULL; + AXError result = AXUIElementCopyAttributeValue(window, kAXTitleAttribute, + (CFTypeRef*)&title); - if (CFStringGetCString(title, buffer, buffer_size, kCFStringEncodingUTF8)) { - CFRelease(title); - return 1; - } else { - return -4; - } + // At this point, we don't need window and appElem anymore. + CFRelease(window); + CFRelease(appElem); + + if (result != kAXErrorSuccess) { + // Failed to get the window title. + return -3; + } + + if (CFStringGetCString(title, buffer, buffer_size, kCFStringEncodingUTF8)) { + CFRelease(title); + return 1; + } else { + return -4; } } int32_t info_get_exec(char *buffer, int32_t buffer_size) { - @autoreleasepool { - NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; - NSString *bundlePath = [frontApp bundleURL].path; - const char * path = [bundlePath UTF8String]; + NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; + NSString *bundlePath = [frontApp bundleURL].path; + const char * path = [bundlePath UTF8String]; - snprintf(buffer, buffer_size, "%s", path); - } + snprintf(buffer, buffer_size, "%s", path); return 1; } int32_t info_get_class(char *buffer, int32_t buffer_size) { - @autoreleasepool { - NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; - NSString *bundleId = frontApp.bundleIdentifier; - const char * bundle = [bundleId UTF8String]; + NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; + NSString *bundleId = frontApp.bundleIdentifier; + const char * bundle = [bundleId UTF8String]; - snprintf(buffer, buffer_size, "%s", bundle); - } + snprintf(buffer, buffer_size, "%s", bundle); return 1; } \ No newline at end of file diff --git a/espanso-inject/Cargo.toml b/espanso-inject/Cargo.toml index 5a0c9a0..c7724be 100644 --- a/espanso-inject/Cargo.toml +++ b/espanso-inject/Cargo.toml @@ -27,7 +27,7 @@ scopeguard = "1.1.0" itertools = "0.10.0" [build-dependencies] -cc = "1.0.66" +cc = "1.0.73" [dev-dependencies] enum-as-inner = "0.3.3" \ No newline at end of file diff --git a/espanso-mac-utils/Cargo.toml b/espanso-mac-utils/Cargo.toml index 6172e9c..cd47a4b 100644 --- a/espanso-mac-utils/Cargo.toml +++ b/espanso-mac-utils/Cargo.toml @@ -14,4 +14,4 @@ lazy_static = "1.4.0" regex = "1.4.3" [build-dependencies] -cc = "1.0.66" \ No newline at end of file +cc = "1.0.73" \ No newline at end of file diff --git a/espanso-modulo/Cargo.toml b/espanso-modulo/Cargo.toml index a6f3ea5..332c043 100644 --- a/espanso-modulo/Cargo.toml +++ b/espanso-modulo/Cargo.toml @@ -15,7 +15,7 @@ lazy_static = "1.4.0" regex = "1.4.3" [build-dependencies] -cc = "1.0.66" +cc = "1.0.73" regex = "1.4.3" zip = "0.5.12" winres = "0.1.11" diff --git a/espanso-modulo/build.rs b/espanso-modulo/build.rs index f3f1a21..5c5570d 100644 --- a/espanso-modulo/build.rs +++ b/espanso-modulo/build.rs @@ -49,7 +49,7 @@ fn build_native() { .expect("unable to extract wxWidgets source dir"); // Compile wxWidgets - let tool = cc::windows_registry::find_tool("msvc", "msbuild") + let tool = cc::windows_registry::find_tool("msvc", "devenv") .expect("unable to locate MSVC compiler, did you install Visual Studio?"); let mut vcvars_path = None; let mut current_root = tool.path(); diff --git a/espanso-modulo/src/sys/search/search.cpp b/espanso-modulo/src/sys/search/search.cpp index a06f796..6d4ac54 100644 --- a/espanso-modulo/src/sys/search/search.cpp +++ b/espanso-modulo/src/sys/search/search.cpp @@ -41,7 +41,7 @@ const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxFRAME_TOOL_WINDOW | wxRESIZE_BORDER #endif #ifdef __LINUX__ const int SEARCH_BAR_FONT_SIZE = 20; -const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxFRAME_TOOL_WINDOW | wxBORDER_NONE; +const long DEFAULT_STYLE = wxSTAY_ON_TOP; #endif const int HELP_TEXT_FONT_SIZE = 10; diff --git a/espanso-render/Cargo.toml b/espanso-render/Cargo.toml index 3940d0d..2244e8a 100644 --- a/espanso-render/Cargo.toml +++ b/espanso-render/Cargo.toml @@ -10,6 +10,7 @@ anyhow = "1.0.38" thiserror = "1.0.23" regex = "1.4.3" lazy_static = "1.4.0" -chrono = "0.4.19" +chrono = {version = "0.4.19", features=["unstable-locales"]} enum-as-inner = "0.3.3" -rand = "0.8.3" \ No newline at end of file +rand = "0.8.3" +sys-locale = "0.1.0" \ No newline at end of file diff --git a/espanso-render/src/extension/date.rs b/espanso-render/src/extension/date.rs index 5cb0812..12f74c2 100644 --- a/espanso-render/src/extension/date.rs +++ b/espanso-render/src/extension/date.rs @@ -17,22 +17,30 @@ * along with espanso. If not, see . */ -use chrono::{DateTime, Duration, Local}; +use chrono::{DateTime, Duration, Local, Locale}; use crate::{Extension, ExtensionOutput, ExtensionResult, Number, Params, Value}; -pub struct DateExtension { +pub trait LocaleProvider { + fn get_system_locale(&self) -> String; +} + +pub struct DateExtension<'a> { fixed_date: Option>, + locale_provider: &'a dyn LocaleProvider, } #[allow(clippy::new_without_default)] -impl DateExtension { - pub fn new() -> Self { - Self { fixed_date: None } +impl<'a> DateExtension<'a> { + pub fn new(locale_provider: &'a dyn LocaleProvider) -> Self { + Self { + fixed_date: None, + locale_provider, + } } } -impl Extension for DateExtension { +impl<'a> Extension for DateExtension<'a> { fn name(&self) -> &str { "date" } @@ -53,9 +61,14 @@ impl Extension for DateExtension { } let format = params.get("format"); + let locale = params + .get("locale") + .and_then(|val| val.as_string()) + .map(String::from) + .unwrap_or_else(|| self.locale_provider.get_system_locale()); let date = if let Some(Value::String(format)) = format { - now.format(format).to_string() + DateExtension::format_date_with_locale_string(now, format, &locale) } else { now.to_rfc2822() }; @@ -64,7 +77,7 @@ impl Extension for DateExtension { } } -impl DateExtension { +impl<'a> DateExtension<'a> { fn get_date(&self) -> DateTime { if let Some(fixed_date) = self.fixed_date { fixed_date @@ -72,6 +85,340 @@ impl DateExtension { Local::now() } } + + fn format_date_with_locale(date: DateTime, format: &str, locale: Locale) -> String { + date.format_localized(format, locale).to_string() + } + + fn format_date_with_locale_string( + date: DateTime, + format: &str, + locale_str: &str, + ) -> String { + let locale = convert_locale_string_to_locale(locale_str).unwrap_or(Locale::en_US); + Self::format_date_with_locale(date, format, locale) + } +} + +fn convert_locale_string_to_locale(locale_str: &str) -> Option { + match locale_str { + "aa-DJ" => Some(Locale::aa_DJ), + "aa-ER" => Some(Locale::aa_ER), + "aa-ET" => Some(Locale::aa_ET), + "af-ZA" => Some(Locale::af_ZA), + "agr-PE" => Some(Locale::agr_PE), + "ak-GH" => Some(Locale::ak_GH), + "am-ET" => Some(Locale::am_ET), + "an-ES" => Some(Locale::an_ES), + "anp-IN" => Some(Locale::anp_IN), + "ar-AE" => Some(Locale::ar_AE), + "ar-BH" => Some(Locale::ar_BH), + "ar-DZ" => Some(Locale::ar_DZ), + "ar-EG" => Some(Locale::ar_EG), + "ar-IN" => Some(Locale::ar_IN), + "ar-IQ" => Some(Locale::ar_IQ), + "ar-JO" => Some(Locale::ar_JO), + "ar-KW" => Some(Locale::ar_KW), + "ar-LB" => Some(Locale::ar_LB), + "ar-LY" => Some(Locale::ar_LY), + "ar-MA" => Some(Locale::ar_MA), + "ar-OM" => Some(Locale::ar_OM), + "ar-QA" => Some(Locale::ar_QA), + "ar-SA" => Some(Locale::ar_SA), + "ar-SD" => Some(Locale::ar_SD), + "ar-SS" => Some(Locale::ar_SS), + "ar-SY" => Some(Locale::ar_SY), + "ar-TN" => Some(Locale::ar_TN), + "ar-YE" => Some(Locale::ar_YE), + "as-IN" => Some(Locale::as_IN), + "ast-ES" => Some(Locale::ast_ES), + "ayc-PE" => Some(Locale::ayc_PE), + "az-AZ" => Some(Locale::az_AZ), + "az-IR" => Some(Locale::az_IR), + "be-BY" => Some(Locale::be_BY), + "bem-ZM" => Some(Locale::bem_ZM), + "ber-DZ" => Some(Locale::ber_DZ), + "ber-MA" => Some(Locale::ber_MA), + "bg-BG" => Some(Locale::bg_BG), + "bhb-IN" => Some(Locale::bhb_IN), + "bho-IN" => Some(Locale::bho_IN), + "bho-NP" => Some(Locale::bho_NP), + "bi-VU" => Some(Locale::bi_VU), + "bn-BD" => Some(Locale::bn_BD), + "bn-IN" => Some(Locale::bn_IN), + "bo-CN" => Some(Locale::bo_CN), + "bo-IN" => Some(Locale::bo_IN), + "br-FR" => Some(Locale::br_FR), + "brx-IN" => Some(Locale::brx_IN), + "bs-BA" => Some(Locale::bs_BA), + "byn-ER" => Some(Locale::byn_ER), + "ca-AD" => Some(Locale::ca_AD), + "ca-ES" => Some(Locale::ca_ES), + "ca-FR" => Some(Locale::ca_FR), + "ca-IT" => Some(Locale::ca_IT), + "ce-RU" => Some(Locale::ce_RU), + "chr-US" => Some(Locale::chr_US), + "cmn-TW" => Some(Locale::cmn_TW), + "crh-UA" => Some(Locale::crh_UA), + "cs-CZ" => Some(Locale::cs_CZ), + "csb-PL" => Some(Locale::csb_PL), + "cv-RU" => Some(Locale::cv_RU), + "cy-GB" => Some(Locale::cy_GB), + "da-DK" => Some(Locale::da_DK), + "de-AT" => Some(Locale::de_AT), + "de-BE" => Some(Locale::de_BE), + "de-CH" => Some(Locale::de_CH), + "de-DE" => Some(Locale::de_DE), + "de-IT" => Some(Locale::de_IT), + "de-LI" => Some(Locale::de_LI), + "de-LU" => Some(Locale::de_LU), + "doi-IN" => Some(Locale::doi_IN), + "dsb-DE" => Some(Locale::dsb_DE), + "dv-MV" => Some(Locale::dv_MV), + "dz-BT" => Some(Locale::dz_BT), + "el-CY" => Some(Locale::el_CY), + "el-GR" => Some(Locale::el_GR), + "en-AG" => Some(Locale::en_AG), + "en-AU" => Some(Locale::en_AU), + "en-BW" => Some(Locale::en_BW), + "en-CA" => Some(Locale::en_CA), + "en-DK" => Some(Locale::en_DK), + "en-GB" => Some(Locale::en_GB), + "en-HK" => Some(Locale::en_HK), + "en-IE" => Some(Locale::en_IE), + "en-IL" => Some(Locale::en_IL), + "en-IN" => Some(Locale::en_IN), + "en-NG" => Some(Locale::en_NG), + "en-NZ" => Some(Locale::en_NZ), + "en-PH" => Some(Locale::en_PH), + "en-SC" => Some(Locale::en_SC), + "en-SG" => Some(Locale::en_SG), + "en-US" => Some(Locale::en_US), + "en-ZA" => Some(Locale::en_ZA), + "en-ZM" => Some(Locale::en_ZM), + "en-ZW" => Some(Locale::en_ZW), + "eo" => Some(Locale::eo), + "es-AR" => Some(Locale::es_AR), + "es-BO" => Some(Locale::es_BO), + "es-CL" => Some(Locale::es_CL), + "es-CO" => Some(Locale::es_CO), + "es-CR" => Some(Locale::es_CR), + "es-CU" => Some(Locale::es_CU), + "es-DO" => Some(Locale::es_DO), + "es-EC" => Some(Locale::es_EC), + "es-ES" => Some(Locale::es_ES), + "es-GT" => Some(Locale::es_GT), + "es-HN" => Some(Locale::es_HN), + "es-MX" => Some(Locale::es_MX), + "es-NI" => Some(Locale::es_NI), + "es-PA" => Some(Locale::es_PA), + "es-PE" => Some(Locale::es_PE), + "es-PR" => Some(Locale::es_PR), + "es-PY" => Some(Locale::es_PY), + "es-SV" => Some(Locale::es_SV), + "es-US" => Some(Locale::es_US), + "es-UY" => Some(Locale::es_UY), + "es-VE" => Some(Locale::es_VE), + "et-EE" => Some(Locale::et_EE), + "eu-ES" => Some(Locale::eu_ES), + "fa-IR" => Some(Locale::fa_IR), + "ff-SN" => Some(Locale::ff_SN), + "fi-FI" => Some(Locale::fi_FI), + "fil-PH" => Some(Locale::fil_PH), + "fo-FO" => Some(Locale::fo_FO), + "fr-BE" => Some(Locale::fr_BE), + "fr-CA" => Some(Locale::fr_CA), + "fr-CH" => Some(Locale::fr_CH), + "fr-FR" => Some(Locale::fr_FR), + "fr-LU" => Some(Locale::fr_LU), + "fur-IT" => Some(Locale::fur_IT), + "fy-DE" => Some(Locale::fy_DE), + "fy-NL" => Some(Locale::fy_NL), + "ga-IE" => Some(Locale::ga_IE), + "gd-GB" => Some(Locale::gd_GB), + "gez-ER" => Some(Locale::gez_ER), + "gez-ET" => Some(Locale::gez_ET), + "gl-ES" => Some(Locale::gl_ES), + "gu-IN" => Some(Locale::gu_IN), + "gv-GB" => Some(Locale::gv_GB), + "ha-NG" => Some(Locale::ha_NG), + "hak-TW" => Some(Locale::hak_TW), + "he-IL" => Some(Locale::he_IL), + "hi-IN" => Some(Locale::hi_IN), + "hif-FJ" => Some(Locale::hif_FJ), + "hne-IN" => Some(Locale::hne_IN), + "hr-HR" => Some(Locale::hr_HR), + "hsb-DE" => Some(Locale::hsb_DE), + "ht-HT" => Some(Locale::ht_HT), + "hu-HU" => Some(Locale::hu_HU), + "hy-AM" => Some(Locale::hy_AM), + "ia-FR" => Some(Locale::ia_FR), + "id-ID" => Some(Locale::id_ID), + "ig-NG" => Some(Locale::ig_NG), + "ik-CA" => Some(Locale::ik_CA), + "is-IS" => Some(Locale::is_IS), + "it-CH" => Some(Locale::it_CH), + "it-IT" => Some(Locale::it_IT), + "iu-CA" => Some(Locale::iu_CA), + "ja-JP" => Some(Locale::ja_JP), + "ka-GE" => Some(Locale::ka_GE), + "kab-DZ" => Some(Locale::kab_DZ), + "kk-KZ" => Some(Locale::kk_KZ), + "kl-GL" => Some(Locale::kl_GL), + "km-KH" => Some(Locale::km_KH), + "kn-IN" => Some(Locale::kn_IN), + "ko-KR" => Some(Locale::ko_KR), + "kok-IN" => Some(Locale::kok_IN), + "ks-IN" => Some(Locale::ks_IN), + "ku-TR" => Some(Locale::ku_TR), + "kw-GB" => Some(Locale::kw_GB), + "ky-KG" => Some(Locale::ky_KG), + "lb-LU" => Some(Locale::lb_LU), + "lg-UG" => Some(Locale::lg_UG), + "li-BE" => Some(Locale::li_BE), + "li-NL" => Some(Locale::li_NL), + "lij-IT" => Some(Locale::lij_IT), + "ln-CD" => Some(Locale::ln_CD), + "lo-LA" => Some(Locale::lo_LA), + "lt-LT" => Some(Locale::lt_LT), + "lv-LV" => Some(Locale::lv_LV), + "lzh-TW" => Some(Locale::lzh_TW), + "mag-IN" => Some(Locale::mag_IN), + "mai-IN" => Some(Locale::mai_IN), + "mai-NP" => Some(Locale::mai_NP), + "mfe-MU" => Some(Locale::mfe_MU), + "mg-MG" => Some(Locale::mg_MG), + "mhr-RU" => Some(Locale::mhr_RU), + "mi-NZ" => Some(Locale::mi_NZ), + "miq-NI" => Some(Locale::miq_NI), + "mjw-IN" => Some(Locale::mjw_IN), + "mk-MK" => Some(Locale::mk_MK), + "ml-IN" => Some(Locale::ml_IN), + "mn-MN" => Some(Locale::mn_MN), + "mni-IN" => Some(Locale::mni_IN), + "mnw-MM" => Some(Locale::mnw_MM), + "mr-IN" => Some(Locale::mr_IN), + "ms-MY" => Some(Locale::ms_MY), + "mt-MT" => Some(Locale::mt_MT), + "my-MM" => Some(Locale::my_MM), + "nan-TW" => Some(Locale::nan_TW), + "nb-NO" => Some(Locale::nb_NO), + "nds-DE" => Some(Locale::nds_DE), + "nds-NL" => Some(Locale::nds_NL), + "ne-NP" => Some(Locale::ne_NP), + "nhn-MX" => Some(Locale::nhn_MX), + "niu-NU" => Some(Locale::niu_NU), + "niu-NZ" => Some(Locale::niu_NZ), + "nl-AW" => Some(Locale::nl_AW), + "nl-BE" => Some(Locale::nl_BE), + "nl-NL" => Some(Locale::nl_NL), + "nn-NO" => Some(Locale::nn_NO), + "nr-ZA" => Some(Locale::nr_ZA), + "nso-ZA" => Some(Locale::nso_ZA), + "oc-FR" => Some(Locale::oc_FR), + "om-ET" => Some(Locale::om_ET), + "om-KE" => Some(Locale::om_KE), + "or-IN" => Some(Locale::or_IN), + "os-RU" => Some(Locale::os_RU), + "pa-IN" => Some(Locale::pa_IN), + "pa-PK" => Some(Locale::pa_PK), + "pap-AW" => Some(Locale::pap_AW), + "pap-CW" => Some(Locale::pap_CW), + "pl-PL" => Some(Locale::pl_PL), + "ps-AF" => Some(Locale::ps_AF), + "pt-BR" => Some(Locale::pt_BR), + "pt-PT" => Some(Locale::pt_PT), + "quz-PE" => Some(Locale::quz_PE), + "raj-IN" => Some(Locale::raj_IN), + "ro-RO" => Some(Locale::ro_RO), + "ru-RU" => Some(Locale::ru_RU), + "ru-UA" => Some(Locale::ru_UA), + "rw-RW" => Some(Locale::rw_RW), + "sa-IN" => Some(Locale::sa_IN), + "sah-RU" => Some(Locale::sah_RU), + "sat-IN" => Some(Locale::sat_IN), + "sc-IT" => Some(Locale::sc_IT), + "sd-IN" => Some(Locale::sd_IN), + "se-NO" => Some(Locale::se_NO), + "sgs-LT" => Some(Locale::sgs_LT), + "shn-MM" => Some(Locale::shn_MM), + "shs-CA" => Some(Locale::shs_CA), + "si-LK" => Some(Locale::si_LK), + "sid-ET" => Some(Locale::sid_ET), + "sk-SK" => Some(Locale::sk_SK), + "sl-SI" => Some(Locale::sl_SI), + "sm-WS" => Some(Locale::sm_WS), + "so-DJ" => Some(Locale::so_DJ), + "so-ET" => Some(Locale::so_ET), + "so-KE" => Some(Locale::so_KE), + "so-SO" => Some(Locale::so_SO), + "sq-AL" => Some(Locale::sq_AL), + "sq-MK" => Some(Locale::sq_MK), + "sr-ME" => Some(Locale::sr_ME), + "sr-RS" => Some(Locale::sr_RS), + "ss-ZA" => Some(Locale::ss_ZA), + "st-ZA" => Some(Locale::st_ZA), + "sv-FI" => Some(Locale::sv_FI), + "sv-SE" => Some(Locale::sv_SE), + "sw-KE" => Some(Locale::sw_KE), + "sw-TZ" => Some(Locale::sw_TZ), + "szl-PL" => Some(Locale::szl_PL), + "ta-IN" => Some(Locale::ta_IN), + "ta-LK" => Some(Locale::ta_LK), + "tcy-IN" => Some(Locale::tcy_IN), + "te-IN" => Some(Locale::te_IN), + "tg-TJ" => Some(Locale::tg_TJ), + "th-TH" => Some(Locale::th_TH), + "the-NP" => Some(Locale::the_NP), + "ti-ER" => Some(Locale::ti_ER), + "ti-ET" => Some(Locale::ti_ET), + "tig-ER" => Some(Locale::tig_ER), + "tk-TM" => Some(Locale::tk_TM), + "tl-PH" => Some(Locale::tl_PH), + "tn-ZA" => Some(Locale::tn_ZA), + "to-TO" => Some(Locale::to_TO), + "tpi-PG" => Some(Locale::tpi_PG), + "tr-CY" => Some(Locale::tr_CY), + "tr-TR" => Some(Locale::tr_TR), + "ts-ZA" => Some(Locale::ts_ZA), + "tt-RU" => Some(Locale::tt_RU), + "ug-CN" => Some(Locale::ug_CN), + "uk-UA" => Some(Locale::uk_UA), + "unm-US" => Some(Locale::unm_US), + "ur-IN" => Some(Locale::ur_IN), + "ur-PK" => Some(Locale::ur_PK), + "uz-UZ" => Some(Locale::uz_UZ), + "ve-ZA" => Some(Locale::ve_ZA), + "vi-VN" => Some(Locale::vi_VN), + "wa-BE" => Some(Locale::wa_BE), + "wae-CH" => Some(Locale::wae_CH), + "wal-ET" => Some(Locale::wal_ET), + "wo-SN" => Some(Locale::wo_SN), + "xh-ZA" => Some(Locale::xh_ZA), + "yi-US" => Some(Locale::yi_US), + "yo-NG" => Some(Locale::yo_NG), + "yue-HK" => Some(Locale::yue_HK), + "yuw-PG" => Some(Locale::yuw_PG), + "zh-CN" => Some(Locale::zh_CN), + "zh-HK" => Some(Locale::zh_HK), + "zh-SG" => Some(Locale::zh_SG), + "zh-TW" => Some(Locale::zh_TW), + "zu-ZA" => Some(Locale::zu_ZA), + _ => None, + } +} + +pub struct DefaultLocaleProvider {} +impl LocaleProvider for DefaultLocaleProvider { + fn get_system_locale(&self) -> String { + sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")) + } +} +#[allow(clippy::new_without_default)] +impl DefaultLocaleProvider { + pub fn new() -> Self { + Self {} + } } #[cfg(test)] @@ -79,9 +426,30 @@ mod tests { use super::*; use chrono::offset::TimeZone; + struct MockLocaleProvider { + locale: String, + } + impl LocaleProvider for MockLocaleProvider { + fn get_system_locale(&self) -> String { + self.locale.clone() + } + } + impl MockLocaleProvider { + pub fn new() -> Self { + Self { + locale: "en-US".to_string(), + } + } + + pub fn new_with_locale(locale: String) -> Self { + Self { locale } + } + } + #[test] fn date_formatted_correctly() { - let mut extension = DateExtension::new(); + let locale_provider = MockLocaleProvider::new(); + let mut extension = DateExtension::new(&locale_provider); extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11)); let param = vec![("format".to_string(), Value::String("%H:%M:%S".to_string()))] @@ -98,7 +466,8 @@ mod tests { #[test] fn offset_works_correctly() { - let mut extension = DateExtension::new(); + let locale_provider = MockLocaleProvider::new(); + let mut extension = DateExtension::new(&locale_provider); extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11)); let param = vec![ @@ -115,4 +484,61 @@ mod tests { ExtensionOutput::Single("10:10:11".to_string()) ); } + + #[test] + fn default_locale_works_correctly() { + let locale_provider = MockLocaleProvider::new_with_locale("it-IT".to_string()); + let mut extension = DateExtension::new(&locale_provider); + extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11)); + + let param = vec![("format".to_string(), Value::String("%A".to_string()))] + .into_iter() + .collect::(); + assert_eq!( + extension + .calculate(&Default::default(), &Default::default(), ¶m) + .into_success() + .unwrap(), + ExtensionOutput::Single("martedì".to_string()) + ); + } + + #[test] + fn invalid_locale_should_default_to_en_us() { + let locale_provider = MockLocaleProvider::new_with_locale("invalid".to_string()); + let mut extension = DateExtension::new(&locale_provider); + extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11)); + + let param = vec![("format".to_string(), Value::String("%A".to_string()))] + .into_iter() + .collect::(); + assert_eq!( + extension + .calculate(&Default::default(), &Default::default(), ¶m) + .into_success() + .unwrap(), + ExtensionOutput::Single("Tuesday".to_string()) + ); + } + + #[test] + fn override_locale() { + let locale_provider = MockLocaleProvider::new(); + let mut extension = DateExtension::new(&locale_provider); + extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11)); + + let param = vec![ + ("format".to_string(), Value::String("%A".to_string())), + ("locale".to_string(), Value::String("it-IT".to_string())), + ] + .into_iter() + .collect::(); + assert_eq!( + extension + .calculate(&Default::default(), &Default::default(), ¶m) + .into_success() + .unwrap(), + ExtensionOutput::Single("martedì".to_string()) + ); + } } diff --git a/espanso-render/src/extension/exec_util.rs b/espanso-render/src/extension/exec_util.rs new file mode 100644 index 0000000..e602e72 --- /dev/null +++ b/espanso-render/src/extension/exec_util.rs @@ -0,0 +1,99 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2022 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +pub enum MacShell { + Bash, + Sh, + Zsh, +} + +// Determine the PATH env variable value available inside a regular terminal session +#[cfg(target_os = "macos")] +pub fn determine_path_env_variable_override(explicit_shell: Option) -> Option { + let shell: MacShell = explicit_shell.or_else(determine_default_macos_shell)?; + + match shell { + MacShell::Bash => { + launch_command_and_get_output("bash", &["--login", "-c", "source ~/.bashrc; echo $PATH"]) + } + MacShell::Sh => launch_command_and_get_output("sh", &["--login", "-c", "echo $PATH"]), + MacShell::Zsh => { + launch_command_and_get_output("zsh", &["--login", "-c", "source ~/.zshrc; echo $PATH"]) + } + } +} + +#[cfg(not(target_os = "macos"))] +pub fn determine_path_env_variable_override(_: Option) -> Option { + None +} + +#[cfg(target_os = "macos")] +pub fn determine_default_macos_shell() -> Option { + use regex::Regex; + use std::process::Command; + + let output = Command::new("sh") + .args(&["--login", "-c", "dscl . -read ~/ UserShell"]) + .output() + .ok()?; + + lazy_static! { + static ref EXTRACT_SHELL_REGEX: Regex = + Regex::new(r"UserShell:\s(.*)$").expect("unable to generate regex to extract default shell"); + } + + if !output.status.success() { + return None; + } + + let output_str = String::from_utf8_lossy(&output.stdout); + let captures = EXTRACT_SHELL_REGEX.captures(output_str.trim())?; + + let shell = captures.get(1)?.as_str().trim(); + + if shell.ends_with("/bash") { + Some(MacShell::Bash) + } else if shell.ends_with("/zsh") { + Some(MacShell::Zsh) + } else if shell.ends_with("/sh") { + Some(MacShell::Sh) + } else { + None + } +} + +#[cfg(not(target_os = "macos"))] +pub fn determine_default_macos_shell() -> Option { + None +} + +#[cfg(target_os = "macos")] +fn launch_command_and_get_output(command: &str, args: &[&str]) -> Option { + use std::process::Command; + + let output = Command::new(command).args(args).output().ok()?; + + if !output.status.success() { + return None; + } + + let output_str = String::from_utf8_lossy(&output.stdout); + Some(output_str.to_string()) +} diff --git a/espanso-render/src/extension/mod.rs b/espanso-render/src/extension/mod.rs index 4cc7356..1f620ef 100644 --- a/espanso-render/src/extension/mod.rs +++ b/espanso-render/src/extension/mod.rs @@ -1,7 +1,7 @@ /* * This file is part of espanso. * - * Copyright (C) 2019-2021 Federico Terzi + * Copyright (C) 2019-2022 Federico Terzi * * espanso is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,6 +21,7 @@ pub mod choice; pub mod clipboard; pub mod date; pub mod echo; +mod exec_util; pub mod form; pub mod random; pub mod script; diff --git a/espanso-render/src/extension/shell.rs b/espanso-render/src/extension/shell.rs index 4fe4250..a060b7f 100644 --- a/espanso-render/src/extension/shell.rs +++ b/espanso-render/src/extension/shell.rs @@ -1,7 +1,7 @@ /* * This file is part of espanso. * - * Copyright (C) 2019-2021 Federico Terzi + * Copyright (C) 2019-2022 Federico Terzi * * espanso is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,7 +24,7 @@ use std::{ }; use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value}; -use log::{error, info}; +use log::{debug, error, info}; use thiserror::Error; #[allow(clippy::upper_case_acronyms)] @@ -35,10 +35,16 @@ pub enum Shell { WSL2, Bash, Sh, + Zsh, } impl Shell { - fn execute_cmd(&self, cmd: &str, vars: &HashMap) -> std::io::Result { + fn execute_cmd( + &self, + cmd: &str, + vars: &HashMap, + override_path_on_macos: bool, + ) -> std::io::Result { let mut is_wsl = false; let mut command = match self { @@ -74,6 +80,11 @@ impl Shell { command.args(&["-c", cmd]); command } + Shell::Zsh => { + let mut command = Command::new("zsh"); + command.args(&["-c", cmd]); + command + } }; // Set the OS-specific flags @@ -84,6 +95,27 @@ impl Shell { command.env(key, value); } + // If Espanso is executed as an app bundle on macOS, it doesn't inherit the PATH + // environment variables that are available inside a terminal, and this can be confusing for users. + // For example, one might use "jq" inside the terminal but then it throws an error with "command not found" + // if launched through the Espanso shell extension. + // For this reason, Espanso tries to obtain the same PATH value by spawning a login shell and extracting + // the PATH after the processing. + if cfg!(target_os = "macos") && override_path_on_macos { + let supported_mac_shell = match self { + Shell::Bash => Some(super::exec_util::MacShell::Bash), + Shell::Sh => Some(super::exec_util::MacShell::Sh), + Shell::Zsh => Some(super::exec_util::MacShell::Zsh), + _ => None, + }; + if let Some(path_env_override) = + super::exec_util::determine_path_env_variable_override(supported_mac_shell) + { + debug!("overriding PATH env variable with: {}", path_env_override); + command.env("PATH", path_env_override); + } + } + // In WSL environment, we have to specify which ENV variables // should be passed to linux. // For more information: https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/ @@ -110,6 +142,7 @@ impl Shell { "wsl2" => Some(Shell::WSL2), "bash" => Some(Shell::Bash), "sh" => Some(Shell::Sh), + "zsh" => Some(Shell::Zsh), _ => None, } } @@ -120,7 +153,17 @@ impl Default for Shell { if cfg!(target_os = "windows") { Shell::Powershell } else if cfg!(target_os = "macos") { - Shell::Sh + lazy_static! { + static ref DEFAULT_MACOS_SHELL: Option = + super::exec_util::determine_default_macos_shell(); + } + + match *DEFAULT_MACOS_SHELL { + Some(super::exec_util::MacShell::Bash) => Shell::Bash, + Some(super::exec_util::MacShell::Sh) => Shell::Sh, + Some(super::exec_util::MacShell::Zsh) => Shell::Zsh, + None => Shell::Sh, + } } else if cfg!(target_os = "linux") { Shell::Bash } else { @@ -172,7 +215,13 @@ impl Extension for ShellExtension { self.config_path.to_string_lossy().to_string(), ); - match shell.execute_cmd(cmd, &env_variables) { + let macos_override_path = params + .get("macos_override_path") + .and_then(|v| v.as_bool()) + .copied() + .unwrap_or(true); + + match shell.execute_cmd(cmd, &env_variables, macos_override_path) { Ok(output) => { let output_str = String::from_utf8_lossy(&output.stdout); let error_str = String::from_utf8_lossy(&output.stderr); diff --git a/espanso-ui/Cargo.toml b/espanso-ui/Cargo.toml index 1194758..e1d5b3d 100644 --- a/espanso-ui/Cargo.toml +++ b/espanso-ui/Cargo.toml @@ -31,4 +31,4 @@ notify-rust = "4.2.2" crossbeam = "0.8.0" [build-dependencies] -cc = "1.0.66" \ No newline at end of file +cc = "1.0.73" \ No newline at end of file diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml index 5a5a745..c5c573b 100644 --- a/espanso/Cargo.toml +++ b/espanso/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "2.1.3-alpha" +version = "2.1.4-beta" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" @@ -81,7 +81,7 @@ section = "utility" license-file = ["../LICENSE", "1"] [package.metadata.deb.variants.wayland] -depends = "$auto, systemd, libnotify-bin, libxkbcommon0, libwxgtk3.0-gtk3-0v5" +depends = "$auto, systemd, libnotify-bin, libxkbcommon0, libwxgtk3.0-gtk3-0v5, wl-clipboard" # TODO: once this issue [1] is fixed, we should create a variant for # wayland to automatically run the setcap script. # [1]: https://github.com/mmstick/cargo-deb/issues/151 diff --git a/espanso/src/cli/worker/config.rs b/espanso/src/cli/worker/config.rs index dd69176..7371ffc 100644 --- a/espanso/src/cli/worker/config.rs +++ b/espanso/src/cli/worker/config.rs @@ -197,3 +197,15 @@ impl<'a> espanso_engine::process::EnabledStatusProvider for ConfigManager<'a> { self.active().enable() } } + +impl<'a> crate::gui::modulo::form::ModuloFormUIOptionProvider for ConfigManager<'a> { + fn get_post_form_delay(&self) -> usize { + self.active().post_form_delay() + } +} + +impl<'a> crate::gui::modulo::search::ModuloSearchUIOptionProvider for ConfigManager<'a> { + fn get_post_search_delay(&self) -> usize { + self.active().post_search_delay() + } +} diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index 8581a62..d48269d 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -107,8 +107,10 @@ pub fn initialize_and_spawn( let default_config = &*config_manager.default(); let modulo_manager = crate::gui::modulo::manager::ModuloManager::new(); - let modulo_form_ui = crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager); - let modulo_search_ui = crate::gui::modulo::search::ModuloSearchUI::new(&modulo_manager); + let modulo_form_ui = + crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager, &config_manager); + let modulo_search_ui = + crate::gui::modulo::search::ModuloSearchUI::new(&modulo_manager, &config_manager); let modulo_text_ui = crate::gui::modulo::textview::ModuloTextUI::new(&modulo_manager); let context: Box = Box::new(super::context::DefaultContext::new( @@ -186,7 +188,8 @@ pub fn initialize_and_spawn( let clipboard_adapter = ClipboardAdapter::new(&*clipboard, &config_manager); let clipboard_extension = espanso_render::extension::clipboard::ClipboardExtension::new(&clipboard_adapter); - let date_extension = espanso_render::extension::date::DateExtension::new(); + let locale_provider = espanso_render::extension::date::DefaultLocaleProvider::new(); + let date_extension = espanso_render::extension::date::DateExtension::new(&locale_provider); let echo_extension = espanso_render::extension::echo::EchoExtension::new(); // For backwards compatiblity purposes, the echo extension can also be called with "dummy" type let dummy_extension = espanso_render::extension::echo::EchoExtension::new_with_alias("dummy"); diff --git a/espanso/src/gui/modulo/form.rs b/espanso/src/gui/modulo/form.rs index 6c0f778..c483a13 100644 --- a/espanso/src/gui/modulo/form.rs +++ b/espanso/src/gui/modulo/form.rs @@ -20,18 +20,30 @@ use serde::Serialize; use serde_json::{json, Map, Value}; use std::collections::HashMap; +use std::convert::TryInto; use crate::gui::{FormField, FormUI}; use super::manager::ModuloManager; +pub trait ModuloFormUIOptionProvider { + fn get_post_form_delay(&self) -> usize; +} + pub struct ModuloFormUI<'a> { manager: &'a ModuloManager, + option_provider: &'a dyn ModuloFormUIOptionProvider, } impl<'a> ModuloFormUI<'a> { - pub fn new(manager: &'a ModuloManager) -> Self { - Self { manager } + pub fn new( + manager: &'a ModuloManager, + option_provider: &'a dyn ModuloFormUIOptionProvider, + ) -> Self { + Self { + manager, + option_provider, + } } } @@ -52,7 +64,7 @@ impl<'a> FormUI for ModuloFormUI<'a> { .manager .invoke(&["form", "-j", "-i", "-"], &json_config)?; let json: Result, _> = serde_json::from_str(&output); - match json { + let result = match json { Ok(json) => { if json.is_empty() { Ok(None) @@ -61,7 +73,16 @@ impl<'a> FormUI for ModuloFormUI<'a> { } } Err(error) => Err(error.into()), + }; + + let post_form_delay = self.option_provider.get_post_form_delay(); + if post_form_delay > 0 { + std::thread::sleep(std::time::Duration::from_millis( + post_form_delay.try_into().unwrap(), + )); } + + result } } diff --git a/espanso/src/gui/modulo/manager.rs b/espanso/src/gui/modulo/manager.rs index 9f7ed51..a860e5d 100644 --- a/espanso/src/gui/modulo/manager.rs +++ b/espanso/src/gui/modulo/manager.rs @@ -105,6 +105,13 @@ impl ModuloManager { error!("modulo reported an error: {}", error); } + if !child_output.status.success() { + error!( + "modulo exited with non-zero status code: {:?}", + child_output.status.code() + ) + } + if !output.trim().is_empty() { Ok(output.to_string()) } else { diff --git a/espanso/src/gui/modulo/search.rs b/espanso/src/gui/modulo/search.rs index a55d525..d8d0bac 100644 --- a/espanso/src/gui/modulo/search.rs +++ b/espanso/src/gui/modulo/search.rs @@ -19,19 +19,30 @@ use serde::Serialize; use serde_json::Value; -use std::collections::HashMap; +use std::{collections::HashMap, convert::TryInto}; use crate::gui::{SearchItem, SearchUI}; use super::manager::ModuloManager; +pub trait ModuloSearchUIOptionProvider { + fn get_post_search_delay(&self) -> usize; +} + pub struct ModuloSearchUI<'a> { manager: &'a ModuloManager, + option_provider: &'a dyn ModuloSearchUIOptionProvider, } impl<'a> ModuloSearchUI<'a> { - pub fn new(manager: &'a ModuloManager) -> Self { - Self { manager } + pub fn new( + manager: &'a ModuloManager, + option_provider: &'a dyn ModuloSearchUIOptionProvider, + ) -> Self { + Self { + manager, + option_provider, + } } } @@ -48,7 +59,7 @@ impl<'a> SearchUI for ModuloSearchUI<'a> { .manager .invoke(&["search", "-j", "-i", "-"], &json_config)?; let json: Result, _> = serde_json::from_str(&output); - match json { + let result = match json { Ok(json) => { if let Some(Value::String(selected_id)) = json.get("selected") { Ok(Some(selected_id.clone())) @@ -57,7 +68,16 @@ impl<'a> SearchUI for ModuloSearchUI<'a> { } } Err(error) => Err(error.into()), + }; + + let post_search_delay = self.option_provider.get_post_search_delay(); + if post_search_delay > 0 { + std::thread::sleep(std::time::Duration::from_millis( + post_search_delay.try_into().unwrap(), + )); } + + result } } diff --git a/espanso/src/patch/patches/mod.rs b/espanso/src/patch/patches/mod.rs index e45567d..cd0a492 100644 --- a/espanso/src/patch/patches/mod.rs +++ b/espanso/src/patch/patches/mod.rs @@ -48,6 +48,8 @@ generate_patchable_config!( backspace_limit -> usize, apply_patch -> bool, undo_backspace -> bool, + post_form_delay -> usize, + post_search_delay -> usize, win32_exclude_orphan_events -> bool, win32_keyboard_layout_cache_interval -> i64, x11_use_xclip_backend -> bool, diff --git a/scripts/build_windows_resources.rs b/scripts/build_windows_resources.rs index 0c11750..6357b6e 100644 --- a/scripts/build_windows_resources.rs +++ b/scripts/build_windows_resources.rs @@ -1,6 +1,6 @@ //! ```cargo //! [dependencies] -//! cc = "1.0.66" +//! cc = "1.0.73" //! glob = "0.3.0" //! envmnt = "*" //! ``` @@ -26,7 +26,7 @@ fn main() { // First, we try to find the directory containing the various versions: // C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Redist\MSVC\ - let tool = cc::windows_registry::find_tool("msvc", "msbuild") + let tool = cc::windows_registry::find_tool("msvc", "devenv") .expect("unable to locate MSVC compiler, did you install Visual Studio?"); let mut versions_dir = None; let mut current_root = tool.path(); @@ -54,14 +54,21 @@ fn main() { .next() .expect("unable to find vcruntime140_1.dll file") .expect("unable to extract path of vcruntime140_1.dll file"); - + // Copy the DLLs in the target directory - let parent_dir = target_file.parent().expect("unable to obtain directory containing DLLs"); - for entry in glob::glob(&format!(r"{}\*.dll", parent_dir.to_string_lossy().to_string())).expect("unable to glob over DLLs") { + let parent_dir = target_file + .parent() + .expect("unable to obtain directory containing DLLs"); + for entry in glob::glob(&format!( + r"{}\*.dll", + parent_dir.to_string_lossy().to_string() + )) + .expect("unable to glob over DLLs") + { let entry = entry.expect("unable to unwrap DLL entry"); let filename = entry.file_name().expect("unable to obtain filename"); std::fs::copy(&entry, target_dir.join(filename)).expect("unable to copy DLL"); - } + } // Copy the executable let exec_path = envmnt::get_or_panic("EXEC_PATH"); diff --git a/snapcraft.yaml b/snapcraft.yaml index 82b7b3e..a57ec80 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: espanso -version: 2.1.3-alpha +version: 2.1.4-beta summary: A Cross-platform Text Expander written in Rust description: | espanso is a Cross-platform, Text Expander written in Rust.