diff --git a/Cargo.lock b/Cargo.lock
index 716e52c..9572b17 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -572,7 +572,7 @@ dependencies = [
[[package]]
name = "espanso"
-version = "2.0.4-alpha"
+version = "2.0.5-alpha"
dependencies = [
"anyhow",
"caps",
diff --git a/espanso-clipboard/src/cocoa/native.mm b/espanso-clipboard/src/cocoa/native.mm
index b60f56d..91ed16a 100644
--- a/espanso-clipboard/src/cocoa/native.mm
+++ b/espanso-clipboard/src/cocoa/native.mm
@@ -44,7 +44,11 @@ int32_t clipboard_set_text(char * text) {
[pasteboard declareTypes:array owner:nil];
NSString *nsText = [NSString stringWithUTF8String:text];
- [pasteboard setString:nsText forType:NSPasteboardTypeString];
+ if (![pasteboard setString:nsText forType:NSPasteboardTypeString]) {
+ return 0;
+ }
+
+ return 1;
}
int32_t clipboard_set_image(char * image_path) {
diff --git a/espanso-clipboard/src/wayland/fallback/mod.rs b/espanso-clipboard/src/wayland/fallback/mod.rs
index 157bdd5..c916dac 100644
--- a/espanso-clipboard/src/wayland/fallback/mod.rs
+++ b/espanso-clipboard/src/wayland/fallback/mod.rs
@@ -26,7 +26,7 @@ use std::{
use crate::{Clipboard, ClipboardOptions};
use anyhow::Result;
-use log::error;
+use log::{error, warn};
use std::process::Command;
use thiserror::Error;
use wait_timeout::ChildExt;
@@ -49,7 +49,15 @@ impl WaylandFallbackClipboard {
// Try to connect to the wayland display
let wayland_socket = if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
- PathBuf::from(runtime_dir).join("wayland-0")
+ let wayland_display = if let Ok(display) = std::env::var("WAYLAND_DISPLAY") {
+ display
+ } else {
+ warn!("Could not determine wayland display from WAYLAND_DISPLAY env variable, falling back to 'wayland-0'");
+ warn!("Note that this might not work on some systems.");
+ "wayland-0".to_string()
+ };
+
+ PathBuf::from(runtime_dir).join(wayland_display)
} else {
error!("environment variable XDG_RUNTIME_DIR is missing, can't initialize the clipboard");
return Err(WaylandFallbackClipboardError::MissingEnvVariable().into());
diff --git a/espanso-detect/src/evdev/device.rs b/espanso-detect/src/evdev/device.rs
index 818578d..5723ccc 100644
--- a/espanso-detect/src/evdev/device.rs
+++ b/espanso-detect/src/evdev/device.rs
@@ -2,7 +2,7 @@
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
use anyhow::Result;
-use libc::{input_event, size_t, ssize_t, EWOULDBLOCK, O_CLOEXEC, O_NONBLOCK, O_RDONLY};
+use libc::{input_event, size_t, ssize_t, ENODEV, EWOULDBLOCK, O_CLOEXEC, O_NONBLOCK, O_RDONLY};
use log::trace;
use scopeguard::ScopeGuard;
use std::collections::HashMap;
@@ -127,7 +127,11 @@ impl Device {
}
if len < 0 && unsafe { *errno_ptr } != EWOULDBLOCK {
- return Err(DeviceError::BlockingReadOperation().into());
+ if unsafe { *errno_ptr } == ENODEV {
+ return Err(DeviceError::FailedReadNoSuchDevice.into());
+ }
+
+ return Err(DeviceError::FailedRead(unsafe { *errno_ptr }).into());
}
Ok(events)
@@ -322,6 +326,9 @@ pub enum DeviceError {
#[error("no devices found")]
NoDevicesFound(),
- #[error("read operation can't block device")]
- BlockingReadOperation(),
+ #[error("read operation failed with code: `{0}`")]
+ FailedRead(i32),
+
+ #[error("read operation failed: ENODEV No such device")]
+ FailedReadNoSuchDevice,
}
diff --git a/espanso-detect/src/evdev/mod.rs b/espanso-detect/src/evdev/mod.rs
index d7d6337..4f5f5c3 100644
--- a/espanso-detect/src/evdev/mod.rs
+++ b/espanso-detect/src/evdev/mod.rs
@@ -38,8 +38,9 @@ use keymap::Keymap;
use lazycell::LazyCell;
use libc::{
__errno_location, close, epoll_ctl, epoll_event, epoll_wait, EINTR, EPOLLIN, EPOLL_CTL_ADD,
+ EPOLL_CTL_DEL,
};
-use log::{debug, error, info, trace};
+use log::{debug, error, info, trace, warn};
use thiserror::Error;
use crate::event::{InputEvent, Key, KeyboardEvent, Variant};
@@ -204,7 +205,9 @@ impl Source for EVDEVSource {
}
}
- for ev in evs.iter() {
+ #[allow(clippy::needless_range_loop)]
+ for i in 0usize..(ret as usize) {
+ let ev = evs[i];
let device = &self.devices[ev.u64 as usize];
match device.read() {
Ok(events) if !events.is_empty() => {
@@ -226,7 +229,30 @@ impl Source for EVDEVSource {
});
}
Ok(_) => { /* SKIP EMPTY */ }
- Err(err) => error!("Can't read from device {}: {}", device.get_path(), err),
+ Err(err) => {
+ if let Some(DeviceError::FailedReadNoSuchDevice) = err.downcast_ref::() {
+ warn!("Can't read from device {}, this error usually means the device has been disconnected, removing from epoll.", device.get_path());
+
+ if unsafe {
+ epoll_ctl(
+ *epfd,
+ EPOLL_CTL_DEL,
+ device.get_raw_fd(),
+ std::ptr::null_mut(),
+ )
+ } != 0
+ {
+ error!(
+ "Could not remove {} from epoll, errno {}",
+ device.get_path(),
+ unsafe { *errno_ptr }
+ );
+ return Err(EVDEVSourceError::Internal().into());
+ }
+ } else {
+ error!("Can't read from device {}: {}", device.get_path(), err)
+ }
+ }
}
}
}
diff --git a/espanso-engine/src/event/effect.rs b/espanso-engine/src/event/effect.rs
index 505c043..48314c1 100644
--- a/espanso-engine/src/event/effect.rs
+++ b/espanso-engine/src/event/effect.rs
@@ -30,7 +30,7 @@ pub struct CursorHintCompensationEvent {
pub cursor_hint_back_count: usize,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Default)]
pub struct TextInjectRequest {
pub text: String,
pub force_mode: Option,
diff --git a/espanso-engine/src/event/external.rs b/espanso-engine/src/event/external.rs
new file mode 100644
index 0000000..91ae9d1
--- /dev/null
+++ b/espanso-engine/src/event/external.rs
@@ -0,0 +1,26 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 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 .
+ */
+
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct MatchExecRequestEvent {
+ pub trigger: Option,
+ pub args: HashMap,
+}
diff --git a/espanso-engine/src/event/mod.rs b/espanso-engine/src/event/mod.rs
index 8769e0e..2042588 100644
--- a/espanso-engine/src/event/mod.rs
+++ b/espanso-engine/src/event/mod.rs
@@ -18,6 +18,7 @@
*/
pub mod effect;
+pub mod external;
pub mod input;
pub mod internal;
pub mod ui;
@@ -48,7 +49,6 @@ impl Event {
#[allow(clippy::upper_case_acronyms)]
pub enum EventType {
NOOP,
- ProcessingError(String),
ExitRequested(ExitMode),
Exit(ExitMode),
Heartbeat,
@@ -60,6 +60,9 @@ pub enum EventType {
TrayIconClicked,
ContextMenuClicked(input::ContextMenuClickedEvent),
+ // External requests
+ MatchExecRequest(external::MatchExecRequestEvent),
+
// Internal
MatchesDetected(internal::MatchesDetectedEvent),
MatchSelected(internal::MatchSelectedEvent),
@@ -73,11 +76,13 @@ pub enum EventType {
DiscardPrevious(internal::DiscardPreviousEvent),
DiscardBetween(internal::DiscardBetweenEvent),
Undo(internal::UndoEvent),
+ RenderingError,
Disabled,
Enabled,
DisableRequest,
EnableRequest,
+ ToggleRequest,
SecureInputEnabled(internal::SecureInputEnabledEvent),
SecureInputDisabled,
diff --git a/espanso-engine/src/process/default.rs b/espanso-engine/src/process/default.rs
index b0f85b8..4bb8efd 100644
--- a/espanso-engine/src/process/default.rs
+++ b/espanso-engine/src/process/default.rs
@@ -33,15 +33,17 @@ use super::{
render::RenderMiddleware,
},
DisableOptions, EnabledStatusProvider, MatchFilter, MatchInfoProvider, MatchProvider,
- MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware, ModifierStateProvider,
- Multiplexer, PathProvider, Processor, Renderer, UndoEnabledProvider,
+ MatchResolver, MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware,
+ ModifierStateProvider, Multiplexer, NotificationManager, PathProvider, Processor, Renderer,
+ UndoEnabledProvider,
};
use crate::{
event::{Event, EventType},
process::middleware::{
context_menu::ContextMenuMiddleware, disable::DisableMiddleware, exit::ExitMiddleware,
hotkey::HotKeyMiddleware, icon_status::IconStatusMiddleware,
- image_resolve::ImageResolverMiddleware, search::SearchMiddleware, suppress::SuppressMiddleware,
+ image_resolve::ImageResolverMiddleware, match_exec::MatchExecRequestMiddleware,
+ notification::NotificationMiddleware, search::SearchMiddleware, suppress::SuppressMiddleware,
undo::UndoMiddleware,
},
};
@@ -70,6 +72,8 @@ impl<'a> DefaultProcessor<'a> {
undo_enabled_provider: &'a dyn UndoEnabledProvider,
enabled_status_provider: &'a dyn EnabledStatusProvider,
modifier_state_provider: &'a dyn ModifierStateProvider,
+ match_resolver: &'a dyn MatchResolver,
+ notification_manager: &'a dyn NotificationManager,
) -> DefaultProcessor<'a> {
Self {
event_queue: VecDeque::new(),
@@ -82,6 +86,7 @@ impl<'a> DefaultProcessor<'a> {
matcher_options_provider,
modifier_state_provider,
)),
+ Box::new(MatchExecRequestMiddleware::new(match_resolver)),
Box::new(SuppressMiddleware::new(enabled_status_provider)),
Box::new(ContextMenuMiddleware::new()),
Box::new(HotKeyMiddleware::new()),
@@ -103,6 +108,7 @@ impl<'a> DefaultProcessor<'a> {
)),
Box::new(SearchMiddleware::new(match_provider)),
Box::new(MarkdownMiddleware::new()),
+ Box::new(NotificationMiddleware::new(notification_manager)),
Box::new(DelayForModifierReleaseMiddleware::new(
modifier_status_provider,
)),
diff --git a/espanso-engine/src/process/middleware/disable.rs b/espanso-engine/src/process/middleware/disable.rs
index 5b7e633..41438cf 100644
--- a/espanso-engine/src/process/middleware/disable.rs
+++ b/espanso-engine/src/process/middleware/disable.rs
@@ -93,6 +93,10 @@ impl Middleware for DisableMiddleware {
*enabled = false;
has_status_changed = true;
}
+ EventType::ToggleRequest => {
+ *enabled = !*enabled;
+ has_status_changed = true;
+ }
_ => {}
}
diff --git a/espanso-engine/src/process/middleware/markdown.rs b/espanso-engine/src/process/middleware/markdown.rs
index 7deec85..ff1bea2 100644
--- a/espanso-engine/src/process/middleware/markdown.rs
+++ b/espanso-engine/src/process/middleware/markdown.rs
@@ -45,15 +45,8 @@ impl Middleware for MarkdownMiddleware {
// See also: https://github.com/federico-terzi/espanso/issues/759
let html = std::panic::catch_unwind(|| markdown::to_html(&m_event.markdown));
if let Ok(html) = html {
- let mut html = html.trim();
-
- // Remove the surrounding paragraph
- if html.starts_with("") {
- html = html.trim_start_matches("
");
- }
- if html.ends_with("
") {
- html = html.trim_end_matches("
");
- }
+ let html = html.trim();
+ let html = remove_paragraph_tag_if_single_occurrence(html);
return Event::caused_by(
event.source_id,
@@ -72,4 +65,42 @@ impl Middleware for MarkdownMiddleware {
}
}
-// TODO: test
+// If the match is composed of a single paragraph, we remove the tag to avoid
+// a forced "newline" on some editors. In other words, we assume that if the snippet
+// is composed of a single paragraph, then it should be inlined.
+// On the other hand, if the snippet is composed of multiple paragraphs, then we
+// avoid removing the paragraph to prevent HTML corruption.
+// See: https://github.com/federico-terzi/espanso/issues/811
+fn remove_paragraph_tag_if_single_occurrence(html: &str) -> &str {
+ let paragraph_count = html.matches("").count();
+ if paragraph_count <= 1 {
+ let mut new_html = html;
+ if new_html.starts_with("
") {
+ new_html = new_html.trim_start_matches("
");
+ }
+ if new_html.ends_with("
") {
+ new_html = new_html.trim_end_matches("");
+ }
+
+ new_html
+ } else {
+ html
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_remove_paragraph_tag_if_single_occurrence() {
+ assert_eq!(
+ remove_paragraph_tag_if_single_occurrence("single occurrence
"),
+ "single occurrence"
+ );
+ assert_eq!(
+ remove_paragraph_tag_if_single_occurrence("multi
occurrence
"),
+ "multi
occurrence
"
+ );
+ }
+}
diff --git a/espanso-engine/src/process/middleware/match_exec.rs b/espanso-engine/src/process/middleware/match_exec.rs
new file mode 100644
index 0000000..5b8a669
--- /dev/null
+++ b/espanso-engine/src/process/middleware/match_exec.rs
@@ -0,0 +1,80 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 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 .
+ */
+
+use log::warn;
+
+use super::super::Middleware;
+use crate::event::{
+ internal::{DetectedMatch, MatchesDetectedEvent},
+ Event, EventType,
+};
+
+pub trait MatchResolver {
+ fn find_matches_from_trigger(&self, trigger: &str) -> Vec;
+}
+
+pub struct MatchExecRequestMiddleware<'a> {
+ match_resolver: &'a dyn MatchResolver,
+}
+
+impl<'a> MatchExecRequestMiddleware<'a> {
+ pub fn new(match_resolver: &'a dyn MatchResolver) -> Self {
+ Self { match_resolver }
+ }
+}
+
+impl<'a> Middleware for MatchExecRequestMiddleware<'a> {
+ fn name(&self) -> &'static str {
+ "match_exec_request"
+ }
+
+ fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+ if let EventType::MatchExecRequest(m_event) = &event.etype {
+ let mut matches = if let Some(trigger) = &m_event.trigger {
+ self.match_resolver.find_matches_from_trigger(trigger)
+ } else {
+ Vec::new()
+ };
+
+ // Inject the request args into the detected matches
+ matches.iter_mut().for_each(|m| {
+ for (key, value) in &m_event.args {
+ m.args.insert(key.to_string(), value.to_string());
+ }
+ });
+
+ if matches.is_empty() {
+ warn!("received match exec request, but no matches have been found for the given query.");
+ return Event::caused_by(event.source_id, EventType::NOOP);
+ }
+
+ return Event::caused_by(
+ event.source_id,
+ EventType::MatchesDetected(MatchesDetectedEvent {
+ matches,
+ is_search: false,
+ }),
+ );
+ }
+
+ event
+ }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/mod.rs b/espanso-engine/src/process/middleware/mod.rs
index 9ba98b3..d42e5c5 100644
--- a/espanso-engine/src/process/middleware/mod.rs
+++ b/espanso-engine/src/process/middleware/mod.rs
@@ -29,9 +29,11 @@ pub mod hotkey;
pub mod icon_status;
pub mod image_resolve;
pub mod markdown;
+pub mod match_exec;
pub mod match_select;
pub mod matcher;
pub mod multiplex;
+pub mod notification;
pub mod render;
pub mod search;
pub mod suppress;
diff --git a/espanso-engine/src/process/middleware/notification.rs b/espanso-engine/src/process/middleware/notification.rs
new file mode 100644
index 0000000..e0e6aa9
--- /dev/null
+++ b/espanso-engine/src/process/middleware/notification.rs
@@ -0,0 +1,57 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 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 .
+ */
+
+use super::super::Middleware;
+use crate::event::{Event, EventType};
+
+pub trait NotificationManager {
+ fn notify_status_change(&self, enabled: bool);
+ fn notify_rendering_error(&self);
+}
+
+pub struct NotificationMiddleware<'a> {
+ notification_manager: &'a dyn NotificationManager,
+}
+
+impl<'a> NotificationMiddleware<'a> {
+ pub fn new(notification_manager: &'a dyn NotificationManager) -> Self {
+ Self {
+ notification_manager,
+ }
+ }
+}
+
+impl<'a> Middleware for NotificationMiddleware<'a> {
+ fn name(&self) -> &'static str {
+ "notification"
+ }
+
+ fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+ match &event.etype {
+ EventType::Enabled => self.notification_manager.notify_status_change(true),
+ EventType::Disabled => self.notification_manager.notify_status_change(false),
+ EventType::RenderingError => self.notification_manager.notify_rendering_error(),
+ _ => {}
+ }
+
+ event
+ }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/render.rs b/espanso-engine/src/process/middleware/render.rs
index c227c0c..d8175e0 100644
--- a/espanso-engine/src/process/middleware/render.rs
+++ b/espanso-engine/src/process/middleware/render.rs
@@ -22,7 +22,7 @@ use std::collections::HashMap;
use log::error;
use super::super::Middleware;
-use crate::event::{internal::RenderedEvent, Event, EventType};
+use crate::event::{effect::TextInjectRequest, internal::RenderedEvent, Event, EventType};
use anyhow::Result;
use thiserror::Error;
@@ -62,7 +62,7 @@ impl<'a> Middleware for RenderMiddleware<'a> {
"render"
}
- fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+ fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
if let EventType::RenderingRequested(m_event) = event.etype {
match self.renderer.render(
m_event.match_id,
@@ -91,7 +91,16 @@ impl<'a> Middleware for RenderMiddleware<'a> {
}
_ => {
error!("error during rendering: {:?}", err);
- return Event::caused_by(event.source_id, EventType::ProcessingError("An error has occurred during rendering, please examine the logs or contact support.".to_string()));
+
+ dispatch(Event::caused_by(
+ event.source_id,
+ EventType::TextInject(TextInjectRequest {
+ text: "[Espanso]: An error occurred during rendering, please examine the logs for more information.".to_string(),
+ ..Default::default()
+ }),
+ ));
+
+ return Event::caused_by(event.source_id, EventType::RenderingError);
}
},
}
diff --git a/espanso-engine/src/process/middleware/undo.rs b/espanso-engine/src/process/middleware/undo.rs
index 29421e2..75bf67d 100644
--- a/espanso-engine/src/process/middleware/undo.rs
+++ b/espanso-engine/src/process/middleware/undo.rs
@@ -89,14 +89,13 @@ impl<'a> Middleware for UndoMiddleware<'a> {
}
*record = None;
}
- } else if let EventType::Mouse(_) = &event.etype {
- // Any mouse event invalidates the undo feature, as it could
- // represent a change in application
- *record = None;
- } else if let EventType::CursorHintCompensation(_) = &event.etype {
- // Cursor hints invalidate the undo feature, as it would be pretty
- // complex to determine which delete operations should be performed.
- // This might change in the future.
+ } else if let EventType::Mouse(_) | EventType::CursorHintCompensation(_) = &event.etype {
+ // Explanation:
+ // * Any mouse event invalidates the undo feature, as it could
+ // represent a change in application
+ // * Cursor hints invalidate the undo feature, as it would be pretty
+ // complex to determine which delete operations should be performed.
+ // This might change in the future.
*record = None;
}
diff --git a/espanso-engine/src/process/mod.rs b/espanso-engine/src/process/mod.rs
index 1e295f9..f55e520 100644
--- a/espanso-engine/src/process/mod.rs
+++ b/espanso-engine/src/process/mod.rs
@@ -37,12 +37,14 @@ pub use middleware::action::{EventSequenceProvider, MatchInfoProvider};
pub use middleware::delay_modifiers::ModifierStatusProvider;
pub use middleware::disable::DisableOptions;
pub use middleware::image_resolve::PathProvider;
+pub use middleware::match_exec::MatchResolver;
pub use middleware::match_select::{MatchFilter, MatchSelector};
pub use middleware::matcher::{
MatchResult, Matcher, MatcherEvent, MatcherMiddlewareConfigProvider, ModifierState,
ModifierStateProvider,
};
pub use middleware::multiplex::Multiplexer;
+pub use middleware::notification::NotificationManager;
pub use middleware::render::{Renderer, RendererError};
pub use middleware::search::MatchProvider;
pub use middleware::suppress::EnabledStatusProvider;
@@ -65,6 +67,8 @@ pub fn default<'a, MatcherState>(
undo_enabled_provider: &'a dyn UndoEnabledProvider,
enabled_status_provider: &'a dyn EnabledStatusProvider,
modifier_state_provider: &'a dyn ModifierStateProvider,
+ match_resolver: &'a dyn MatchResolver,
+ notification_manager: &'a dyn NotificationManager,
) -> impl Processor + 'a {
default::DefaultProcessor::new(
matchers,
@@ -82,5 +86,7 @@ pub fn default<'a, MatcherState>(
undo_enabled_provider,
enabled_status_provider,
modifier_state_provider,
+ match_resolver,
+ notification_manager,
)
}
diff --git a/espanso-inject/src/evdev/state.rs b/espanso-inject/src/evdev/state.rs
index 5a962b1..381dd88 100644
--- a/espanso-inject/src/evdev/state.rs
+++ b/espanso-inject/src/evdev/state.rs
@@ -1,7 +1,7 @@
// This code is a port of the libxkbcommon "interactive-evdev.c" example
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
-use std::ffi::CStr;
+use std::{ffi::CStr, os::raw::c_char};
use scopeguard::ScopeGuard;
@@ -48,17 +48,17 @@ impl State {
}
pub fn get_string(&self, code: u32) -> Option {
- let mut buffer: [u8; 16] = [0; 16];
+ let mut buffer: [c_char; 16] = [0; 16];
let len = unsafe {
xkb_state_key_get_utf8(
self.state,
code,
- buffer.as_mut_ptr() as *mut i8,
+ buffer.as_mut_ptr(),
std::mem::size_of_val(&buffer),
)
};
if len > 0 {
- let content_raw = unsafe { CStr::from_ptr(buffer.as_ptr() as *mut i8) };
+ let content_raw = unsafe { CStr::from_ptr(buffer.as_ptr()) };
let string = content_raw.to_string_lossy().to_string();
if string.is_empty() {
None
diff --git a/espanso-inject/src/evdev/uinput.rs b/espanso-inject/src/evdev/uinput.rs
index ebacf48..4445b31 100644
--- a/espanso-inject/src/evdev/uinput.rs
+++ b/espanso-inject/src/evdev/uinput.rs
@@ -23,6 +23,7 @@ use libc::{c_uint, close, ioctl, open, O_NONBLOCK, O_WRONLY};
use scopeguard::ScopeGuard;
use anyhow::Result;
+use log::error;
use thiserror::Error;
use super::ffi::{
@@ -39,6 +40,9 @@ impl UInputDevice {
let uinput_path = CString::new("/dev/uinput").expect("unable to generate /dev/uinput path");
let raw_fd = unsafe { open(uinput_path.as_ptr(), O_WRONLY | O_NONBLOCK) };
if raw_fd < 0 {
+ error!("Error: could not open uinput device");
+ error!("This might be due to a recent kernel update, please restart your PC so that the uinput module can be loaded correctly.");
+
return Err(UInputDeviceError::Open().into());
}
let fd = scopeguard::guard(raw_fd, |raw_fd| unsafe {
diff --git a/espanso-modulo/build.rs b/espanso-modulo/build.rs
index 86bc21d..53a995b 100644
--- a/espanso-modulo/build.rs
+++ b/espanso-modulo/build.rs
@@ -420,15 +420,26 @@ fn macos_link_search_path() -> Option {
#[cfg(target_os = "linux")]
fn build_native() {
// Make sure wxWidgets is installed
- if std::process::Command::new("wx-config")
+ // Depending on the installation package, the 'wx-config' command might also be available as 'wx-config-gtk3',
+ // so we need to check for both.
+ // See also: https://github.com/federico-terzi/espanso/issues/840
+ let wx_config_command = if std::process::Command::new("wx-config")
.arg("--version")
.output()
- .is_err()
+ .is_ok()
{
- panic!("wxWidgets is not installed, as `wx-config` cannot be exectued")
- }
+ "wx-config"
+ } else if std::process::Command::new("wx-config-gtk3")
+ .arg("--version")
+ .output()
+ .is_ok()
+ {
+ "wx-config-gtk3"
+ } else {
+ panic!("wxWidgets is not installed, as `wx-config` cannot be executed")
+ };
- let config_path = PathBuf::from("wx-config");
+ let config_path = PathBuf::from(wx_config_command);
let cpp_flags = get_cpp_flags(&config_path);
let mut build = cc::Build::new();
diff --git a/espanso-modulo/src/sys/form/form.cpp b/espanso-modulo/src/sys/form/form.cpp
index 722cd6b..3481314 100644
--- a/espanso-modulo/src/sys/form/form.cpp
+++ b/espanso-modulo/src/sys/form/form.cpp
@@ -89,11 +89,16 @@ public:
std::vector fields;
std::unordered_map> idMap;
wxButton *submit;
+ wxStaticText *helpText;
+ bool hasFocusedMultilineControl;
private:
void AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata meta);
void Submit();
void OnSubmitBtn(wxCommandEvent& event);
- void OnEscape(wxKeyEvent& event);
+ void OnCharHook(wxKeyEvent& event);
+ void UpdateHelpText();
+ void HandleNormalFocus(wxFocusEvent& event);
+ void HandleMultilineFocus(wxFocusEvent& event);
};
enum
{
@@ -113,6 +118,8 @@ bool FormApp::OnInit()
FormFrame::FormFrame(const wxString& title, const wxPoint& pos, const wxSize& size)
: wxFrame(NULL, wxID_ANY, title, pos, size, DEFAULT_STYLE)
{
+ hasFocusedMultilineControl = false;
+
panel = new wxPanel(this, wxID_ANY);
wxBoxSizer *vbox = new wxBoxSizer(wxVERTICAL);
panel->SetSizer(vbox);
@@ -125,8 +132,15 @@ FormFrame::FormFrame(const wxString& title, const wxPoint& pos, const wxSize& si
submit = new wxButton(panel, ID_Submit, "Submit");
vbox->Add(submit, 1, wxEXPAND | wxALL, PADDING);
+ helpText = new wxStaticText(panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize);
+ wxFont helpFont = helpText->GetFont();
+ helpFont.SetPointSize(8);
+ helpText->SetFont(helpFont);
+ vbox->Add(helpText, 0, wxLEFT | wxRIGHT | wxBOTTOM, PADDING);
+ UpdateHelpText();
+
Bind(wxEVT_BUTTON, &FormFrame::OnSubmitBtn, this, ID_Submit);
- Bind(wxEVT_CHAR_HOOK, &FormFrame::OnEscape, this, wxID_ANY);
+ Bind(wxEVT_CHAR_HOOK, &FormFrame::OnCharHook, this, wxID_ANY);
// TODO: register ESC click handler: https://forums.wxwidgets.org/viewtopic.php?t=41926
this->SetClientSize(panel->GetBestSize());
@@ -157,6 +171,9 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m
if (textMeta->multiline) {
textControl->SetMinSize(wxSize(MULTILINE_MIN_WIDTH, MULTILINE_MIN_HEIGHT));
+ textControl->Bind(wxEVT_SET_FOCUS, &FormFrame::HandleMultilineFocus, this, wxID_ANY);
+ } else {
+ textControl->Bind(wxEVT_SET_FOCUS, &FormFrame::HandleNormalFocus, this, wxID_ANY);
}
// Create the field wrapper
@@ -188,6 +205,8 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m
((wxChoice*)choice)->SetSelection(selectedItem);
}
+ ((wxChoice*)choice)->Bind(wxEVT_SET_FOCUS, &FormFrame::HandleNormalFocus, this, wxID_ANY);
+
// Create the field wrapper
std::unique_ptr field((FieldWrapper*) new ChoiceFieldWrapper((wxChoice*) choice));
idMap[meta.id] = std::move(field);
@@ -197,14 +216,14 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m
if (selectedItem >= 0) {
((wxListBox*)choice)->SetSelection(selectedItem);
}
+
+ ((wxListBox*)choice)->Bind(wxEVT_SET_FOCUS, &FormFrame::HandleNormalFocus, this, wxID_ANY);
// Create the field wrapper
std::unique_ptr field((FieldWrapper*) new ListFieldWrapper((wxListBox*) choice));
idMap[meta.id] = std::move(field);
}
-
-
control = choice;
fields.push_back(choice);
break;
@@ -253,15 +272,40 @@ void FormFrame::Submit() {
Close(true);
}
+void FormFrame::HandleNormalFocus(wxFocusEvent& event) {
+ hasFocusedMultilineControl = false;
+ UpdateHelpText();
+ event.Skip();
+}
+
+void FormFrame::HandleMultilineFocus(wxFocusEvent& event) {
+ hasFocusedMultilineControl = true;
+ UpdateHelpText();
+ event.Skip();
+}
+
+void FormFrame::UpdateHelpText() {
+ if (hasFocusedMultilineControl) {
+ helpText->SetLabel("(or press CTRL+Enter to submit, ESC to cancel)");
+ } else {
+ helpText->SetLabel("(or press Enter to submit, ESC to cancel)");
+ }
+ this->SetClientSize(panel->GetBestSize());
+}
+
void FormFrame::OnSubmitBtn(wxCommandEvent &event) {
Submit();
}
-void FormFrame::OnEscape(wxKeyEvent& event) {
+void FormFrame::OnCharHook(wxKeyEvent& event) {
if (event.GetKeyCode() == WXK_ESCAPE) {
Close(true);
- }else if(event.GetKeyCode() == WXK_RETURN && wxGetKeyState(WXK_RAW_CONTROL)) {
- Submit();
+ }else if(event.GetKeyCode() == WXK_RETURN) {
+ if (!hasFocusedMultilineControl || wxGetKeyState(WXK_RAW_CONTROL)) {
+ Submit();
+ } else {
+ event.Skip();
+ }
}else{
event.Skip();
}
diff --git a/espanso-modulo/src/sys/search/search.cpp b/espanso-modulo/src/sys/search/search.cpp
index 0e16d13..a06f796 100644
--- a/espanso-modulo/src/sys/search/search.cpp
+++ b/espanso-modulo/src/sys/search/search.cpp
@@ -236,6 +236,7 @@ SearchFrame::SearchFrame(const wxString &title, const wxPoint &pos, const wxSize
vbox->Add(resultBox, 5, wxEXPAND | wxALL, 0);
Bind(wxEVT_CHAR_HOOK, &SearchFrame::OnCharEvent, this, wxID_ANY);
+ searchBar->Bind(wxEVT_CHAR, &SearchFrame::OnCharEvent, this, wxID_ANY);
Bind(wxEVT_TEXT, &SearchFrame::OnQueryChange, this, textId);
Bind(wxEVT_LISTBOX_DCLICK, &SearchFrame::OnItemClickEvent, this, resultId);
Bind(wxEVT_ACTIVATE, &SearchFrame::OnActivate, this, wxID_ANY);
@@ -276,9 +277,9 @@ void SearchFrame::OnCharEvent(wxKeyEvent &event)
SelectNext();
}
}
- else if (event.GetKeyCode() >= 49 && event.GetKeyCode() <= 56)
+ else if (event.GetUnicodeKey() >= '1' && event.GetUnicodeKey() <= '9')
{ // Alt + num shortcut
- int index = event.GetKeyCode() - 49;
+ int index = event.GetUnicodeKey() - '1';
if (wxGetKeyState(WXK_ALT))
{
if (resultBox->GetItemCount() > index)
diff --git a/espanso-modulo/src/sys/wizard/wizard.cpp b/espanso-modulo/src/sys/wizard/wizard.cpp
index a4b94cf..61b965d 100644
--- a/espanso-modulo/src/sys/wizard/wizard.cpp
+++ b/espanso-modulo/src/sys/wizard/wizard.cpp
@@ -115,6 +115,7 @@ protected:
void add_path_continue_clicked( wxCommandEvent& event );
void accessibility_enable_clicked( wxCommandEvent& event );
void quit_espanso_clicked( wxCommandEvent& event );
+ void move_bundle_quit_clicked( wxCommandEvent& event );
void navigate_to_next_page_or_close();
void change_default_button(int target_page);
@@ -373,6 +374,11 @@ void DerivedFrame::change_default_button(int target_page)
}
}
+void DerivedFrame::move_bundle_quit_clicked( wxCommandEvent& event )
+{
+ Close(true);
+}
+
bool WizardApp::OnInit()
{
wxInitAllImageHandlers();
diff --git a/espanso-modulo/src/sys/wizard/wizard.fbp b/espanso-modulo/src/sys/wizard/wizard.fbp
index 913ec19..30500df 100644
--- a/espanso-modulo/src/sys/wizard/wizard.fbp
+++ b/espanso-modulo/src/sys/wizard/wizard.fbp
@@ -708,7 +708,7 @@
- -1
+ 500
@@ -758,7 +758,7 @@
0
0
wxID_ANY
- Start
+ Quit
0
diff --git a/espanso-modulo/src/sys/wizard/wizard_gui.cpp b/espanso-modulo/src/sys/wizard/wizard_gui.cpp
index bb53755..b5bbe8e 100644
--- a/espanso-modulo/src/sys/wizard/wizard_gui.cpp
+++ b/espanso-modulo/src/sys/wizard/wizard_gui.cpp
@@ -80,13 +80,13 @@ WizardFrame::WizardFrame( wxWindow* parent, wxWindowID id, const wxString& title
bSizer22->Add( 0, 20, 0, 0, 5 );
move_bundle_description = new wxStaticText( move_bundle_panel, wxID_ANY, wxT("Espanso is being run from outside the Applications directory, which prevents it from working correctly.\n\nPlease move the Espanso.app bundle inside your Applications folder and start it again.\n"), wxDefaultPosition, wxDefaultSize, 0 );
- move_bundle_description->Wrap( -1 );
+ move_bundle_description->Wrap( 500 );
bSizer22->Add( move_bundle_description, 0, wxALL, 10 );
bSizer22->Add( 0, 20, 1, wxEXPAND, 5 );
- move_bundle_quit_button = new wxButton( move_bundle_panel, wxID_ANY, wxT("Start"), wxDefaultPosition, wxDefaultSize, 0 );
+ move_bundle_quit_button = new wxButton( move_bundle_panel, wxID_ANY, wxT("Quit"), wxDefaultPosition, wxDefaultSize, 0 );
move_bundle_quit_button->SetDefault();
bSizer22->Add( move_bundle_quit_button, 0, wxALIGN_RIGHT|wxALL, 10 );
diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml
index a31cae3..a80bffe 100644
--- a/espanso/Cargo.toml
+++ b/espanso/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "espanso"
-version = "2.0.4-alpha"
+version = "2.0.5-alpha"
authors = ["Federico Terzi "]
license = "GPL-3.0"
description = "Cross-platform Text Expander written in Rust"
diff --git a/espanso/src/cli/cmd.rs b/espanso/src/cli/cmd.rs
new file mode 100644
index 0000000..1dcd462
--- /dev/null
+++ b/espanso/src/cli/cmd.rs
@@ -0,0 +1,72 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 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 .
+ */
+
+use std::path::Path;
+
+use crate::{
+ ipc::{create_ipc_client_to_worker, IPCEvent},
+ lock::acquire_worker_lock,
+};
+
+use super::{CliModule, CliModuleArgs};
+use anyhow::{bail, Result};
+use espanso_ipc::IPCClient;
+
+pub fn new() -> CliModule {
+ CliModule {
+ requires_paths: true,
+ subcommand: "cmd".to_string(),
+ entry: cmd_main,
+ ..Default::default()
+ }
+}
+
+fn cmd_main(args: CliModuleArgs) -> i32 {
+ let cli_args = args.cli_args.expect("missing cli_args");
+ let paths = args.paths.expect("missing paths");
+
+ let event = if cli_args.subcommand_matches("enable").is_some() {
+ IPCEvent::EnableRequest
+ } else if cli_args.subcommand_matches("disable").is_some() {
+ IPCEvent::DisableRequest
+ } else if cli_args.subcommand_matches("toggle").is_some() {
+ IPCEvent::ToggleRequest
+ } else if cli_args.subcommand_matches("search").is_some() {
+ IPCEvent::OpenSearchBar
+ } else {
+ eprintln!("unknown command, please run `espanso cmd --help` to see a list of valid ones.");
+ return 1;
+ };
+
+ if let Err(error) = send_event_to_worker(&paths.runtime, event) {
+ eprintln!("unable to send command, error: {:?}", error);
+ return 2;
+ }
+
+ 0
+}
+
+fn send_event_to_worker(runtime_path: &Path, event: IPCEvent) -> Result<()> {
+ if acquire_worker_lock(runtime_path).is_some() {
+ bail!("Worker process is not running, please start Espanso first.")
+ }
+
+ let mut client = create_ipc_client_to_worker(runtime_path)?;
+ client.send_async(event)
+}
diff --git a/espanso/src/cli/daemon/mod.rs b/espanso/src/cli/daemon/mod.rs
index 1b1c2fe..206ad73 100644
--- a/espanso/src/cli/daemon/mod.rs
+++ b/espanso/src/cli/daemon/mod.rs
@@ -28,7 +28,7 @@ use espanso_path::Paths;
use log::{error, info, warn};
use crate::{
- cli::util::CommandExt,
+ cli::util::{prevent_running_as_root_on_macos, CommandExt},
common_flags::*,
exit_code::{
DAEMON_ALREADY_RUNNING, DAEMON_FATAL_CONFIG_ERROR, DAEMON_GENERAL_ERROR,
@@ -60,6 +60,8 @@ pub fn new() -> CliModule {
}
fn daemon_main(args: CliModuleArgs) -> i32 {
+ prevent_running_as_root_on_macos();
+
let paths = args.paths.expect("missing paths in daemon main");
let paths_overrides = args
.paths_overrides
diff --git a/espanso/src/cli/launcher/mod.rs b/espanso/src/cli/launcher/mod.rs
index aa77a19..4f40f1e 100644
--- a/espanso/src/cli/launcher/mod.rs
+++ b/espanso/src/cli/launcher/mod.rs
@@ -72,7 +72,7 @@ fn launcher_main(args: CliModuleArgs) -> i32 {
let is_welcome_page_enabled = !preferences.has_completed_wizard();
- let is_move_bundle_page_enabled = false; // TODO
+ let is_move_bundle_page_enabled = crate::cli::util::is_subject_to_app_translocation_on_macos();
let is_legacy_version_page_enabled = util::is_legacy_version_running(&paths.runtime);
let runtime_dir_clone = paths.runtime.clone();
diff --git a/espanso/src/cli/match_cli/exec.rs b/espanso/src/cli/match_cli/exec.rs
new file mode 100644
index 0000000..c2d859e
--- /dev/null
+++ b/espanso/src/cli/match_cli/exec.rs
@@ -0,0 +1,71 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 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 .
+ */
+
+use std::collections::HashMap;
+
+use anyhow::{bail, Context, Result};
+use clap::ArgMatches;
+use espanso_ipc::IPCClient;
+use espanso_path::Paths;
+
+use crate::{
+ ipc::{create_ipc_client_to_worker, IPCEvent, RequestMatchExpansionPayload},
+ lock::acquire_worker_lock,
+};
+
+pub fn exec_main(cli_args: &ArgMatches, paths: &Paths) -> Result<()> {
+ let trigger = cli_args.value_of("trigger");
+ let args = cli_args.values_of("arg");
+
+ if trigger.is_none() || trigger.map(str::is_empty).unwrap_or(false) {
+ bail!("You need to specify the --trigger 'trigger' option. Run `espanso match exec --help` for more information.");
+ }
+
+ if acquire_worker_lock(&paths.runtime).is_some() {
+ bail!("Worker process is not running, please start Espanso first.")
+ }
+
+ let mut client = create_ipc_client_to_worker(&paths.runtime)?;
+
+ let mut match_args = HashMap::new();
+ if let Some(args) = args {
+ args.for_each(|arg| {
+ let tokens = arg.split_once('=');
+ if let Some((key, value)) = tokens {
+ match_args.insert(key.to_string(), value.to_string());
+ } else {
+ eprintln!(
+ "invalid format for argument '{}', you should follow the 'name=value' format",
+ arg
+ );
+ }
+ })
+ }
+
+ client
+ .send_async(IPCEvent::RequestMatchExpansion(
+ RequestMatchExpansionPayload {
+ trigger: trigger.map(String::from),
+ args: match_args,
+ },
+ ))
+ .context("unable to send payload to worker process")?;
+
+ Ok(())
+}
diff --git a/espanso/src/cli/match_cli/list.rs b/espanso/src/cli/match_cli/list.rs
new file mode 100644
index 0000000..53614de
--- /dev/null
+++ b/espanso/src/cli/match_cli/list.rs
@@ -0,0 +1,102 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 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 .
+ */
+
+use anyhow::Result;
+use clap::ArgMatches;
+use espanso_config::{
+ config::{AppProperties, ConfigStore},
+ matches::{store::MatchStore, Match, MatchCause},
+};
+use serde::Serialize;
+
+pub fn list_main(
+ cli_args: &ArgMatches,
+ config_store: Box,
+ match_store: Box,
+) -> Result<()> {
+ let only_triggers = cli_args.is_present("onlytriggers");
+ let preserve_newlines = cli_args.is_present("preservenewlines");
+
+ let class = cli_args.value_of("class");
+ let title = cli_args.value_of("title");
+ let exec = cli_args.value_of("exec");
+
+ let config = config_store.active(&AppProperties { title, class, exec });
+ let match_set = match_store.query(config.match_paths());
+
+ if cli_args.is_present("json") {
+ print_matches_as_json(&match_set.matches)?;
+ } else {
+ print_matches_as_plain(&match_set.matches, only_triggers, preserve_newlines)
+ }
+
+ Ok(())
+}
+
+pub fn print_matches_as_plain(match_list: &[&Match], only_triggers: bool, preserve_newlines: bool) {
+ for m in match_list {
+ let triggers = match &m.cause {
+ MatchCause::None => vec!["(none)".to_string()],
+ MatchCause::Trigger(trigger_cause) => trigger_cause.triggers.clone(),
+ MatchCause::Regex(regex_cause) => vec![regex_cause.regex.clone()],
+ };
+
+ for trigger in triggers {
+ if only_triggers {
+ println!("{}", trigger);
+ } else {
+ let description = m.description();
+
+ if preserve_newlines {
+ println!("{} - {}", trigger, description)
+ } else {
+ println!("{} - {}", trigger, description.replace('\n', " "))
+ }
+ }
+ }
+ }
+}
+
+#[derive(Debug, Serialize)]
+struct JsonMatchEntry {
+ triggers: Vec,
+ replace: String,
+}
+
+pub fn print_matches_as_json(match_list: &[&Match]) -> Result<()> {
+ let mut entries = Vec::new();
+ for m in match_list {
+ let triggers = match &m.cause {
+ MatchCause::None => vec!["(none)".to_string()],
+ MatchCause::Trigger(trigger_cause) => trigger_cause.triggers.clone(),
+ MatchCause::Regex(regex_cause) => vec![regex_cause.regex.clone()],
+ };
+
+ entries.push(JsonMatchEntry {
+ triggers,
+ replace: m.description().to_string(),
+ })
+ }
+
+ let json = serde_json::to_string_pretty(&entries)?;
+
+ println!("{}", json);
+
+ Ok(())
+}
diff --git a/espanso/src/cli/match_cli/mod.rs b/espanso/src/cli/match_cli/mod.rs
new file mode 100644
index 0000000..781bc32
--- /dev/null
+++ b/espanso/src/cli/match_cli/mod.rs
@@ -0,0 +1,57 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 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 .
+ */
+
+use super::{CliModule, CliModuleArgs};
+
+mod exec;
+mod list;
+
+pub fn new() -> CliModule {
+ CliModule {
+ requires_paths: true,
+ requires_config: true,
+ subcommand: "match".to_string(),
+ entry: match_main,
+ ..Default::default()
+ }
+}
+
+fn match_main(args: CliModuleArgs) -> i32 {
+ let cli_args = args.cli_args.expect("missing cli_args");
+ let config_store = args.config_store.expect("missing config_store");
+ let match_store = args.match_store.expect("missing match_store");
+ let paths = args.paths.expect("missing paths");
+
+ if let Some(sub_args) = cli_args.subcommand_matches("list") {
+ if let Err(err) = list::list_main(sub_args, config_store, match_store) {
+ eprintln!("unable to list matches: {:?}", err);
+ return 1;
+ }
+ } else if let Some(sub_args) = cli_args.subcommand_matches("exec") {
+ if let Err(err) = exec::exec_main(sub_args, &paths) {
+ eprintln!("unable to exec match: {:?}", err);
+ return 1;
+ }
+ } else {
+ eprintln!("Invalid use, please run 'espanso match --help' to get more information.");
+ return 1;
+ }
+
+ 0
+}
diff --git a/espanso/src/cli/mod.rs b/espanso/src/cli/mod.rs
index 2cdba94..f7a3768 100644
--- a/espanso/src/cli/mod.rs
+++ b/espanso/src/cli/mod.rs
@@ -23,11 +23,13 @@ use clap::ArgMatches;
use espanso_config::{config::ConfigStore, error::NonFatalErrorSet, matches::store::MatchStore};
use espanso_path::Paths;
+pub mod cmd;
pub mod daemon;
pub mod edit;
pub mod env_path;
pub mod launcher;
pub mod log;
+pub mod match_cli;
pub mod migrate;
pub mod modulo;
pub mod package;
diff --git a/espanso/src/cli/service/macos.rs b/espanso/src/cli/service/macos.rs
index f6914f5..5529c15 100644
--- a/espanso/src/cli/service/macos.rs
+++ b/espanso/src/cli/service/macos.rs
@@ -17,18 +17,31 @@
* along with espanso. If not, see .
*/
-use anyhow::Result;
-use log::{info, warn};
+use anyhow::{bail, Result};
+use log::{error, info, warn};
use std::process::Command;
use std::{fs::create_dir_all, process::ExitStatus};
use thiserror::Error;
+use crate::cli::util::prevent_running_as_root_on_macos;
+use crate::error_eprintln;
+
#[cfg(target_os = "macos")]
const SERVICE_PLIST_CONTENT: &str = include_str!("../../res/macos/com.federicoterzi.espanso.plist");
#[cfg(target_os = "macos")]
const SERVICE_PLIST_FILE_NAME: &str = "com.federicoterzi.espanso.plist";
pub fn register() -> Result<()> {
+ prevent_running_as_root_on_macos();
+
+ if crate::cli::util::is_subject_to_app_translocation_on_macos() {
+ error_eprintln!("Unable to register Espanso as service, please move the Espanso.app bundle inside the /Applications directory to proceed.");
+ error_eprintln!(
+ "For more information, please see: https://github.com/federico-terzi/espanso/issues/844"
+ );
+ bail!("macOS activated app-translocation on Espanso");
+ }
+
let home_dir = dirs::home_dir().expect("could not get user home directory");
let library_dir = home_dir.join("Library");
let agents_dir = library_dir.join("LaunchAgents");
@@ -94,6 +107,8 @@ pub enum RegisterError {
}
pub fn unregister() -> Result<()> {
+ prevent_running_as_root_on_macos();
+
let home_dir = dirs::home_dir().expect("could not get user home directory");
let library_dir = home_dir.join("Library");
let agents_dir = library_dir.join("LaunchAgents");
diff --git a/espanso/src/cli/service/mod.rs b/espanso/src/cli/service/mod.rs
index c80e418..6cf18fa 100644
--- a/espanso/src/cli/service/mod.rs
+++ b/espanso/src/cli/service/mod.rs
@@ -17,12 +17,14 @@
* along with espanso. If not, see .
*/
+use std::time::Instant;
+
use super::{CliModule, CliModuleArgs, PathsOverrides};
use crate::{
error_eprintln,
exit_code::{
SERVICE_ALREADY_RUNNING, SERVICE_FAILURE, SERVICE_NOT_REGISTERED, SERVICE_NOT_RUNNING,
- SERVICE_SUCCESS,
+ SERVICE_SUCCESS, SERVICE_TIMED_OUT,
},
info_println,
lock::acquire_worker_lock,
@@ -99,6 +101,7 @@ fn service_main(args: CliModuleArgs) -> i32 {
return status_main(&paths);
} else if let Some(sub_args) = cli_args.subcommand_matches("restart") {
stop_main(&paths);
+ std::thread::sleep(std::time::Duration::from_millis(300));
return start_main(&paths, &paths_overrides, sub_args);
}
@@ -131,12 +134,23 @@ fn start_main(paths: &Paths, _paths_overrides: &PathsOverrides, args: &ArgMatche
if let Err(err) = start_service() {
error_eprintln!("unable to start service: {}", err);
return SERVICE_FAILURE;
- } else {
- info_println!("espanso started correctly!");
}
}
- SERVICE_SUCCESS
+ let now = Instant::now();
+ while now.elapsed() < std::time::Duration::from_secs(5) {
+ let lock_file = acquire_worker_lock(&paths.runtime);
+ if lock_file.is_none() {
+ info_println!("espanso started correctly!");
+ return SERVICE_SUCCESS;
+ }
+ drop(lock_file);
+
+ std::thread::sleep(std::time::Duration::from_millis(200));
+ }
+
+ error_eprintln!("unable to start service: timed out");
+ SERVICE_TIMED_OUT
}
fn stop_main(paths: &Paths) -> i32 {
diff --git a/espanso/src/cli/util.rs b/espanso/src/cli/util.rs
index e029a74..335009a 100644
--- a/espanso/src/cli/util.rs
+++ b/espanso/src/cli/util.rs
@@ -53,3 +53,33 @@ impl CommandExt for Command {
self
}
}
+
+// For context, see also this issue: https://github.com/federico-terzi/espanso/issues/648
+#[cfg(target_os = "macos")]
+pub fn prevent_running_as_root_on_macos() {
+ use crate::{error_eprintln, exit_code::UNEXPECTED_RUN_AS_ROOT};
+
+ if unsafe { libc::geteuid() } == 0 {
+ error_eprintln!("Espanso is being run as root, but this can create unwanted side-effects. Please run it as a normal user.");
+ std::process::exit(UNEXPECTED_RUN_AS_ROOT);
+ }
+}
+
+#[cfg(not(target_os = "macos"))]
+pub fn prevent_running_as_root_on_macos() {
+ // Do nothing on other platforms
+}
+
+// This is needed to make sure the app is NOT subject to "App Translocation" on
+// macOS, which would make Espanso misbehave on some circumstances.
+// For more information, see: https://github.com/federico-terzi/espanso/issues/844
+pub fn is_subject_to_app_translocation_on_macos() -> bool {
+ if !cfg!(target_os = "macos") {
+ return false;
+ }
+
+ let exec_path = std::env::current_exe().expect("unable to extract executable path");
+ let exec_path = exec_path.to_string_lossy();
+
+ exec_path.contains("/private/")
+}
diff --git a/espanso/src/cli/worker/engine/funnel/ipc.rs b/espanso/src/cli/worker/engine/funnel/ipc.rs
new file mode 100644
index 0000000..0708358
--- /dev/null
+++ b/espanso/src/cli/worker/engine/funnel/ipc.rs
@@ -0,0 +1,79 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 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 .
+ */
+
+use crossbeam::channel::{Receiver, Select, SelectedOperation};
+
+use espanso_engine::{
+ event::{Event, EventType},
+ funnel,
+};
+use log::warn;
+
+use super::sequencer::Sequencer;
+
+pub struct IpcEventSource<'a> {
+ pub ipc_event_receiver: Receiver,
+ pub sequencer: &'a Sequencer,
+}
+
+impl<'a> IpcEventSource<'a> {
+ pub fn new(ipc_event_receiver: Receiver, sequencer: &'a Sequencer) -> Self {
+ IpcEventSource {
+ ipc_event_receiver,
+ sequencer,
+ }
+ }
+}
+
+impl<'a> funnel::Source<'a> for IpcEventSource<'a> {
+ fn register(&'a self, select: &mut Select<'a>) -> usize {
+ select.recv(&self.ipc_event_receiver)
+ }
+
+ fn receive(&self, op: SelectedOperation) -> Option {
+ let ipc_event = op
+ .recv(&self.ipc_event_receiver)
+ .expect("unable to select data from IpcEventSource receiver");
+
+ // Execute only events that have been whitelisted
+ if !is_event_type_allowed(&ipc_event) {
+ warn!(
+ "received black-listed event from IPC stream, blocking it: {:?}",
+ ipc_event
+ );
+ return None;
+ }
+
+ Some(Event {
+ source_id: self.sequencer.next_id(),
+ etype: ipc_event,
+ })
+ }
+}
+
+fn is_event_type_allowed(event: &EventType) -> bool {
+ matches!(
+ event,
+ EventType::MatchExecRequest(_)
+ | EventType::ShowSearchBar
+ | EventType::DisableRequest
+ | EventType::EnableRequest
+ | EventType::ToggleRequest
+ )
+}
diff --git a/espanso/src/cli/worker/engine/funnel/mod.rs b/espanso/src/cli/worker/engine/funnel/mod.rs
index f1d699b..c0706fc 100644
--- a/espanso/src/cli/worker/engine/funnel/mod.rs
+++ b/espanso/src/cli/worker/engine/funnel/mod.rs
@@ -35,6 +35,7 @@ use self::{
pub mod detect;
pub mod exit;
+pub mod ipc;
pub mod key_state;
pub mod modifier;
pub mod secure_input;
diff --git a/espanso/src/cli/worker/engine/funnel/secure_input.rs b/espanso/src/cli/worker/engine/funnel/secure_input.rs
index 3e4da03..ad9437d 100644
--- a/espanso/src/cli/worker/engine/funnel/secure_input.rs
+++ b/espanso/src/cli/worker/engine/funnel/secure_input.rs
@@ -43,33 +43,22 @@ impl<'a> SecureInputSource<'a> {
impl<'a> funnel::Source<'a> for SecureInputSource<'a> {
fn register(&'a self, select: &mut Select<'a>) -> usize {
- if cfg!(target_os = "macos") {
- select.recv(&self.receiver)
- } else {
- 999999
- }
+ select.recv(&self.receiver)
}
fn receive(&self, op: SelectedOperation) -> Option {
- if cfg!(target_os = "macos") {
- let si_event = op
- .recv(&self.receiver)
- .expect("unable to select data from SecureInputSource receiver");
+ let si_event = op
+ .recv(&self.receiver)
+ .expect("unable to select data from SecureInputSource receiver");
- Some(Event {
- source_id: self.sequencer.next_id(),
- etype: match si_event {
- SecureInputEvent::Disabled => EventType::SecureInputDisabled,
- SecureInputEvent::Enabled { app_name, app_path } => {
- EventType::SecureInputEnabled(SecureInputEnabledEvent { app_name, app_path })
- }
- },
- })
- } else {
- Some(Event {
- source_id: self.sequencer.next_id(),
- etype: EventType::NOOP,
- })
- }
+ Some(Event {
+ source_id: self.sequencer.next_id(),
+ etype: match si_event {
+ SecureInputEvent::Disabled => EventType::SecureInputDisabled,
+ SecureInputEvent::Enabled { app_name, app_path } => {
+ EventType::SecureInputEnabled(SecureInputEnabledEvent { app_name, app_path })
+ }
+ },
+ })
}
}
diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs
index f68309a..07c653a 100644
--- a/espanso/src/cli/worker/engine/mod.rs
+++ b/espanso/src/cli/worker/engine/mod.rs
@@ -23,7 +23,7 @@ use anyhow::Result;
use crossbeam::channel::Receiver;
use espanso_config::{config::ConfigStore, matches::store::MatchStore};
use espanso_detect::SourceCreationOptions;
-use espanso_engine::event::ExitMode;
+use espanso_engine::event::{EventType, ExitMode};
use espanso_inject::{InjectorCreationOptions, KeyboardStateProvider};
use espanso_path::Paths;
use espanso_ui::{event::UIEvent, UIRemote};
@@ -82,6 +82,7 @@ pub fn initialize_and_spawn(
secure_input_receiver: Receiver,
use_evdev_backend: bool,
start_reason: Option,
+ ipc_event_receiver: Receiver,
) -> Result> {
let handle = std::thread::Builder::new()
.name("engine thread".to_string())
@@ -131,17 +132,18 @@ pub fn initialize_and_spawn(
})
.expect("failed to initialize detector module");
let exit_source = super::engine::funnel::exit::ExitSource::new(exit_signal, &sequencer);
+ let ipc_event_source =
+ super::engine::funnel::ipc::IpcEventSource::new(ipc_event_receiver, &sequencer);
let ui_source = super::engine::funnel::ui::UISource::new(ui_event_receiver, &sequencer);
let secure_input_source = super::engine::funnel::secure_input::SecureInputSource::new(
secure_input_receiver,
&sequencer,
);
- let sources: Vec<&dyn espanso_engine::funnel::Source> = vec![
- &detect_source,
- &exit_source,
- &ui_source,
- &secure_input_source,
- ];
+ let mut sources: Vec<&dyn espanso_engine::funnel::Source> =
+ vec![&detect_source, &exit_source, &ui_source, &ipc_event_source];
+ if cfg!(target_os = "macos") {
+ sources.push(&secure_input_source);
+ }
let funnel = espanso_engine::funnel::default(&sources);
let rolling_matcher = RollingMatcherAdapter::new(
@@ -210,6 +212,8 @@ pub fn initialize_and_spawn(
let disable_options =
process::middleware::disable::extract_disable_options(&*config_manager.default());
+ let notification_manager = NotificationManager::new(&*ui_remote, default_config);
+
let mut processor = espanso_engine::process::default(
&matchers,
&config_manager,
@@ -226,6 +230,8 @@ pub fn initialize_and_spawn(
&config_manager,
&config_manager,
&modifier_state_store,
+ &combined_match_cache,
+ ¬ification_manager,
);
let event_injector = EventInjectorAdapter::new(&*injector, &config_manager);
@@ -254,8 +260,6 @@ pub fn initialize_and_spawn(
}
}
- let notification_manager = NotificationManager::new(&*ui_remote, default_config);
-
match start_reason.as_deref() {
Some(flag) if flag == WORKER_START_REASON_CONFIG_CHANGED => {
notification_manager.notify_config_reloaded(false);
diff --git a/espanso/src/cli/worker/ipc.rs b/espanso/src/cli/worker/ipc.rs
index 07f6177..1ae545e 100644
--- a/espanso/src/cli/worker/ipc.rs
+++ b/espanso/src/cli/worker/ipc.rs
@@ -21,13 +21,17 @@ use std::path::Path;
use anyhow::Result;
use crossbeam::channel::Sender;
-use espanso_engine::event::ExitMode;
+use espanso_engine::event::{external::MatchExecRequestEvent, EventType, ExitMode};
use espanso_ipc::{EventHandlerResponse, IPCServer};
use log::{error, warn};
use crate::ipc::IPCEvent;
-pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender) -> Result<()> {
+pub fn initialize_and_spawn(
+ runtime_dir: &Path,
+ exit_notify: Sender,
+ event_notify: Sender,
+) -> Result<()> {
let server = crate::ipc::create_worker_ipc_server(runtime_dir)?;
std::thread::Builder::new()
@@ -55,6 +59,17 @@ pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender) -
EventHandlerResponse::NoResponse
}
+ IPCEvent::DisableRequest => send_event(&event_notify, EventType::DisableRequest),
+ IPCEvent::EnableRequest => send_event(&event_notify, EventType::EnableRequest),
+ IPCEvent::ToggleRequest => send_event(&event_notify, EventType::ToggleRequest),
+ IPCEvent::OpenSearchBar => send_event(&event_notify, EventType::ShowSearchBar),
+ IPCEvent::RequestMatchExpansion(payload) => send_event(
+ &event_notify,
+ EventType::MatchExecRequest(MatchExecRequestEvent {
+ trigger: payload.trigger,
+ args: payload.args,
+ }),
+ ),
#[allow(unreachable_patterns)]
unexpected_event => {
warn!(
@@ -70,3 +85,17 @@ pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender) -
Ok(())
}
+
+fn send_event(
+ event_notify: &Sender,
+ event: EventType,
+) -> EventHandlerResponse {
+ if let Err(err) = event_notify.send(event) {
+ error!(
+ "experienced error while sending event signal from worker ipc handler: {}",
+ err
+ );
+ }
+
+ EventHandlerResponse::NoResponse
+}
diff --git a/espanso/src/cli/worker/match_cache.rs b/espanso/src/cli/worker/match_cache.rs
index fe23b7d..6c2423f 100644
--- a/espanso/src/cli/worker/match_cache.rs
+++ b/espanso/src/cli/worker/match_cache.rs
@@ -21,8 +21,9 @@ use std::collections::HashMap;
use espanso_config::{
config::ConfigStore,
- matches::{store::MatchStore, Match, MatchEffect},
+ matches::{store::MatchStore, Match, MatchCause, MatchEffect},
};
+use espanso_engine::event::internal::DetectedMatch;
use super::{builtin::BuiltInMatch, engine::process::middleware::match_select::MatchSummary};
@@ -164,3 +165,50 @@ impl<'a> espanso_engine::process::MatchProvider for CombinedMatchCache<'a> {
ids
}
}
+
+impl<'a> espanso_engine::process::MatchResolver for CombinedMatchCache<'a> {
+ fn find_matches_from_trigger(&self, trigger: &str) -> Vec {
+ let user_matches: Vec = self
+ .user_match_cache
+ .cache
+ .values()
+ .filter_map(|m| {
+ if let MatchCause::Trigger(trigger_cause) = &m.cause {
+ if trigger_cause.triggers.iter().any(|t| t == trigger) {
+ Some(DetectedMatch {
+ id: m.id,
+ trigger: Some(trigger.to_string()),
+ ..Default::default()
+ })
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ let builtin_matches: Vec = self
+ .builtin_match_cache
+ .values()
+ .filter_map(|m| {
+ if m.triggers.iter().any(|t| t == trigger) {
+ Some(DetectedMatch {
+ id: m.id,
+ trigger: Some(trigger.to_string()),
+ ..Default::default()
+ })
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ let mut matches = Vec::with_capacity(user_matches.len() + builtin_matches.len());
+ matches.extend(user_matches);
+ matches.extend(builtin_matches);
+
+ matches
+ }
+}
diff --git a/espanso/src/cli/worker/mod.rs b/espanso/src/cli/worker/mod.rs
index 473b5f8..b2cf019 100644
--- a/espanso/src/cli/worker/mod.rs
+++ b/espanso/src/cli/worker/mod.rs
@@ -22,6 +22,7 @@ use espanso_engine::event::ExitMode;
use log::{debug, error, info};
use crate::{
+ cli::util::prevent_running_as_root_on_macos,
exit_code::{
WORKER_ALREADY_RUNNING, WORKER_EXIT_ALL_PROCESSES, WORKER_GENERAL_ERROR,
WORKER_LEGACY_ALREADY_RUNNING, WORKER_RESTART, WORKER_SUCCESS,
@@ -58,6 +59,8 @@ pub fn new() -> CliModule {
}
fn worker_main(args: CliModuleArgs) -> i32 {
+ prevent_running_as_root_on_macos();
+
let paths = args.paths.expect("missing paths in worker main");
let cli_args = args.cli_args.expect("missing cli_args in worker main");
@@ -112,6 +115,7 @@ fn worker_main(args: CliModuleArgs) -> i32 {
.expect("unable to initialize UI module");
let (engine_exit_notify, engine_exit_receiver) = unbounded();
+ let (ipc_event_notify, ipc_event_receiver) = unbounded();
let (engine_ui_event_sender, engine_ui_event_receiver) = unbounded();
let (engine_secure_input_sender, engine_secure_input_receiver) = unbounded();
@@ -126,11 +130,12 @@ fn worker_main(args: CliModuleArgs) -> i32 {
engine_secure_input_receiver,
use_evdev_backend,
start_reason,
+ ipc_event_receiver,
)
.expect("unable to initialize engine");
// Setup the IPC server
- ipc::initialize_and_spawn(&paths.runtime, engine_exit_notify.clone())
+ ipc::initialize_and_spawn(&paths.runtime, engine_exit_notify.clone(), ipc_event_notify)
.expect("unable to initialize IPC server");
// If specified, automatically monitor the daemon status and
diff --git a/espanso/src/cli/worker/ui/notification.rs b/espanso/src/cli/worker/ui/notification.rs
index c08f139..9d18479 100644
--- a/espanso/src/cli/worker/ui/notification.rs
+++ b/espanso/src/cli/worker/ui/notification.rs
@@ -54,3 +54,23 @@ impl<'a> NotificationManager<'a> {
self.notify("Updated keyboard layout!");
}
}
+
+impl<'a> espanso_engine::process::NotificationManager for NotificationManager<'a> {
+ fn notify_status_change(&self, enabled: bool) {
+ // Don't notify the status change outside Linux for now
+ if !cfg!(target_os = "linux") {
+ return;
+ }
+
+ if enabled {
+ self.notify("Espanso enabled!")
+ } else {
+ self.notify("Espanso disabled!")
+ }
+ }
+
+ fn notify_rendering_error(&self) {
+ self
+ .notify("An error occurred during rendering, please examine the logs for more information.");
+ }
+}
diff --git a/espanso/src/exit_code.rs b/espanso/src/exit_code.rs
index 01ade17..c3bd78a 100644
--- a/espanso/src/exit_code.rs
+++ b/espanso/src/exit_code.rs
@@ -51,6 +51,7 @@ pub const SERVICE_FAILURE: i32 = 1;
pub const SERVICE_NOT_REGISTERED: i32 = 2;
pub const SERVICE_ALREADY_RUNNING: i32 = 3;
pub const SERVICE_NOT_RUNNING: i32 = 4;
+pub const SERVICE_TIMED_OUT: i32 = 5;
pub const WORKAROUND_SUCCESS: i32 = 0;
#[allow(dead_code)]
@@ -68,6 +69,9 @@ pub const PACKAGE_LIST_FAILED: i32 = 4;
pub const PACKAGE_UPDATE_FAILED: i32 = 5;
pub const PACKAGE_UPDATE_PARTIAL_FAILURE: i32 = 6;
+#[allow(dead_code)]
+pub const UNEXPECTED_RUN_AS_ROOT: i32 = 42;
+
use std::sync::Mutex;
use crate::error_eprintln;
diff --git a/espanso/src/ipc.rs b/espanso/src/ipc.rs
index 4d157d4..5e8d3b4 100644
--- a/espanso/src/ipc.rs
+++ b/espanso/src/ipc.rs
@@ -20,12 +20,25 @@
use anyhow::Result;
use espanso_ipc::{IPCClient, IPCServer};
use serde::{Deserialize, Serialize};
-use std::path::Path;
+use std::{collections::HashMap, path::Path};
#[derive(Debug, Serialize, Deserialize)]
pub enum IPCEvent {
Exit,
ExitAllProcesses,
+
+ EnableRequest,
+ DisableRequest,
+ ToggleRequest,
+ OpenSearchBar,
+
+ RequestMatchExpansion(RequestMatchExpansionPayload),
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct RequestMatchExpansionPayload {
+ pub trigger: Option,
+ pub args: HashMap,
}
pub fn create_daemon_ipc_server(runtime_dir: &Path) -> Result> {
diff --git a/espanso/src/main.rs b/espanso/src/main.rs
index d0897ed..58cf84f 100644
--- a/espanso/src/main.rs
+++ b/espanso/src/main.rs
@@ -71,6 +71,8 @@ lazy_static! {
cli::service::new(),
cli::workaround::new(),
cli::package::new(),
+ cli::match_cli::new(),
+ cli::cmd::new(),
];
static ref ALIASES: Vec = vec![
CliAlias {
@@ -219,17 +221,17 @@ fn main() {
.subcommand(SubCommand::with_name("unregister").about("Remove 'espanso' command from PATH"))
.about("Add or remove the 'espanso' command from the PATH"),
)
- // .subcommand(SubCommand::with_name("cmd")
- // .about("Send a command to the espanso daemon.")
- // .subcommand(SubCommand::with_name("exit")
- // .about("Terminate the daemon."))
- // .subcommand(SubCommand::with_name("enable")
- // .about("Enable the espanso replacement engine."))
- // .subcommand(SubCommand::with_name("disable")
- // .about("Disable the espanso replacement engine."))
- // .subcommand(SubCommand::with_name("toggle")
- // .about("Toggle the status of the espanso replacement engine."))
- // )
+ .subcommand(SubCommand::with_name("cmd")
+ .about("Send a command to the espanso daemon.")
+ .subcommand(SubCommand::with_name("enable")
+ .about("Enable expansions."))
+ .subcommand(SubCommand::with_name("disable")
+ .about("Disable expansions."))
+ .subcommand(SubCommand::with_name("toggle")
+ .about("Enable/Disable expansions."))
+ .subcommand(SubCommand::with_name("search")
+ .about("Open the Espanso's search bar."))
+ )
.subcommand(SubCommand::with_name("edit")
.about("Shortcut to open the default text editor to edit config files")
.arg(Arg::with_name("target_file")
@@ -251,10 +253,6 @@ For example, specifying 'email' is equivalent to 'match/email.yml'."#))
.setting(AppSettings::Hidden)
.about("Start the daemon without spawning a new process."),
)
- // .subcommand(SubCommand::with_name("register")
- // .about("MacOS and Linux only. Register espanso in the system daemon manager."))
- // .subcommand(SubCommand::with_name("unregister")
- // .about("MacOS and Linux only. Unregister espanso from the system daemon manager."))
.subcommand(SubCommand::with_name("launcher").setting(AppSettings::Hidden))
.subcommand(SubCommand::with_name("log").about("Print the daemon logs."))
.subcommand(
@@ -305,8 +303,6 @@ For example, specifying 'email' is equivalent to 'match/email.yml'."#))
),
),
)
- // .subcommand(SubCommand::with_name("status")
- // .about("Check if the espanso daemon is running or not."))
.subcommand(
SubCommand::with_name("path")
.about("Prints all the espanso directory paths to easily locate configuration and matches.")
@@ -353,39 +349,69 @@ For example, specifying 'email' is equivalent to 'match/email.yml'."#))
.subcommand(restart_subcommand)
.subcommand(stop_subcommand)
.subcommand(status_subcommand)
- // .subcommand(SubCommand::with_name("match")
- // .about("List and execute matches from the CLI")
- // .subcommand(SubCommand::with_name("list")
- // .about("Print all matches to standard output")
- // .arg(Arg::with_name("json")
- // .short("j")
- // .long("json")
- // .help("Return the matches as json")
- // .required(false)
- // .takes_value(false)
- // )
- // .arg(Arg::with_name("onlytriggers")
- // .short("t")
- // .long("onlytriggers")
- // .help("Print only triggers without replacement")
- // .required(false)
- // .takes_value(false)
- // )
- // .arg(Arg::with_name("preservenewlines")
- // .short("n")
- // .long("preservenewlines")
- // .help("Preserve newlines when printing replacements")
- // .required(false)
- // .takes_value(false)
- // )
- // )
- // .subcommand(SubCommand::with_name("exec")
- // .about("Triggers the expansion of the given match")
- // .arg(Arg::with_name("trigger")
- // .help("The trigger of the match to be expanded")
- // )
- // )
- // )
+ .subcommand(SubCommand::with_name("match")
+ .about("List and execute matches from the CLI")
+ .subcommand(SubCommand::with_name("list")
+ .about("Print matches to standard output")
+ .arg(Arg::with_name("json")
+ .short("j")
+ .long("json")
+ .help("Output matches to the JSON format")
+ .required(false)
+ .takes_value(false)
+ )
+ .arg(Arg::with_name("onlytriggers")
+ .short("t")
+ .long("only-triggers")
+ .help("Print only triggers without replacement")
+ .required(false)
+ .takes_value(false)
+ )
+ .arg(Arg::with_name("preservenewlines")
+ .short("n")
+ .long("preserve-newlines")
+ .help("Preserve newlines when printing replacements. Does nothing when using JSON format.")
+ .required(false)
+ .takes_value(false)
+ )
+ .arg(Arg::with_name("class")
+ .long("class")
+ .help("Only return matches that would be active with the given class. This is relevant if you want to list matches only active inside an app-specific config.")
+ .required(false)
+ .takes_value(true)
+ )
+ .arg(Arg::with_name("title")
+ .long("title")
+ .help("Only return matches that would be active with the given title. This is relevant if you want to list matches only active inside an app-specific config.")
+ .required(false)
+ .takes_value(true)
+ )
+ .arg(Arg::with_name("exec")
+ .long("exec")
+ .help("Only return matches that would be active with the given exec. This is relevant if you want to list matches only active inside an app-specific config.")
+ .required(false)
+ .takes_value(true)
+ )
+ )
+ .subcommand(SubCommand::with_name("exec")
+ .about("Triggers the expansion of a match")
+ .arg(Arg::with_name("trigger")
+ .short("t")
+ .long("trigger")
+ .help("The trigger of the match to be expanded")
+ .required(false)
+ .takes_value(true)
+ )
+ .arg(Arg::with_name("arg")
+ .long("arg")
+ .help("Specify also an argument for the expansion, following the --arg 'name=value' format. You can specify multiple ones.")
+ .required(false)
+ .takes_value(true)
+ .multiple(true)
+ .number_of_values(1)
+ )
+ )
+ )
.subcommand(
SubCommand::with_name("package")
.about("package-management commands")
diff --git a/espanso/src/path/macos.rs b/espanso/src/path/macos.rs
index 58f020a..cd0f7c2 100644
--- a/espanso/src/path/macos.rs
+++ b/espanso/src/path/macos.rs
@@ -17,11 +17,11 @@
* along with espanso. If not, see .
*/
-use std::io::ErrorKind;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
+use std::{fs::create_dir_all, io::ErrorKind};
use thiserror::Error;
-use anyhow::Result;
+use anyhow::{bail, Context, Result};
use log::{error, warn};
pub fn is_espanso_in_path() -> bool {
@@ -29,38 +29,48 @@ pub fn is_espanso_in_path() -> bool {
}
pub fn add_espanso_to_path(prompt_when_necessary: bool) -> Result<()> {
+ if crate::cli::util::is_subject_to_app_translocation_on_macos() {
+ error_eprintln!("Unable to register Espanso to PATH, please move the Espanso.app bundle inside the /Applications directory to proceed.");
+ error_eprintln!(
+ "For more information, please see: https://github.com/federico-terzi/espanso/issues/844"
+ );
+ bail!("macOS activated app-translocation on Espanso");
+ }
+
let target_link_dir = PathBuf::from("/usr/local/bin");
let exec_path = std::env::current_exe()?;
- if !target_link_dir.is_dir() {
- return Err(PathError::UsrLocalBinDirDoesNotExist.into());
- }
-
let target_link_path = target_link_dir.join("espanso");
+ if !target_link_dir.is_dir() {
+ warn!("/usr/local/bin folder does not exist, attempting to create one...");
+
+ if let Err(error) = create_dir_all(&target_link_dir) {
+ match error.kind() {
+ ErrorKind::PermissionDenied if prompt_when_necessary => {
+ warn!("target link file can't be accessed with current permissions, requesting elevated ones through AppleScript.");
+
+ create_dir_and_link_with_applescript(&exec_path, &target_link_path)
+ .context("unable to create link with AppleScript")?;
+
+ return Ok(());
+ }
+ _other_error => {
+ return Err(PathError::SymlinkError(error).into());
+ }
+ }
+ }
+ }
+
if let Err(error) = std::os::unix::fs::symlink(&exec_path, &target_link_path) {
match error.kind() {
- ErrorKind::PermissionDenied => {
- if prompt_when_necessary {
- warn!("target link file can't be accessed with current permissions, requesting elevated ones through AppleScript.");
+ ErrorKind::PermissionDenied if prompt_when_necessary => {
+ warn!("target link file can't be accessed with current permissions, requesting elevated ones through AppleScript.");
- let params = format!(
- r##"do shell script "mkdir -p /usr/local/bin && ln -sf '{}' '{}'" with administrator privileges"##,
- exec_path.to_string_lossy(),
- target_link_path.to_string_lossy(),
- );
+ create_dir_and_link_with_applescript(&exec_path, &target_link_path)
+ .context("unable to create link with AppleScript")?;
- let mut child = std::process::Command::new("osascript")
- .args(&["-e", ¶ms])
- .spawn()?;
-
- let result = child.wait()?;
- if !result.success() {
- return Err(PathError::ElevationRequestFailure.into());
- }
- } else {
- return Err(PathError::SymlinkError(error).into());
- }
+ return Ok(());
}
_other_error => {
return Err(PathError::SymlinkError(error).into());
@@ -71,6 +81,26 @@ pub fn add_espanso_to_path(prompt_when_necessary: bool) -> Result<()> {
Ok(())
}
+fn create_dir_and_link_with_applescript(exec_path: &Path, target_link_path: &Path) -> Result<()> {
+ let params = format!(
+ r##"do shell script "mkdir -p /usr/local/bin && ln -sf '{}' '{}'" with administrator privileges"##,
+ exec_path.to_string_lossy(),
+ target_link_path.to_string_lossy(),
+ );
+
+ let mut child = std::process::Command::new("osascript")
+ .args(&["-e", ¶ms])
+ .spawn()?;
+
+ let result = child.wait()?;
+
+ if !result.success() {
+ return Err(PathError::ElevationRequestFailure.into());
+ }
+
+ Ok(())
+}
+
pub fn remove_espanso_from_path(prompt_when_necessary: bool) -> Result<()> {
let target_link_path = PathBuf::from("/usr/local/bin/espanso");
@@ -112,9 +142,6 @@ pub fn remove_espanso_from_path(prompt_when_necessary: bool) -> Result<()> {
#[derive(Error, Debug)]
pub enum PathError {
- #[error("/usr/local/bin directory doesn't exist")]
- UsrLocalBinDirDoesNotExist,
-
#[error("symlink error: `{0}`")]
SymlinkError(std::io::Error),
diff --git a/snapcraft.yaml b/snapcraft.yaml
index 92a5f11..4141d80 100644
--- a/snapcraft.yaml
+++ b/snapcraft.yaml
@@ -1,5 +1,5 @@
name: espanso
-version: 2.0.4-alpha
+version: 2.0.5-alpha
summary: A Cross-platform Text Expander written in Rust
description: |
espanso is a Cross-platform, Text Expander written in Rust.