Merge pull request #851 from federico-terzi/dev

v2.0.5-alpha release
This commit is contained in:
Federico Terzi 2021-11-06 19:49:01 +01:00 committed by GitHub
commit 3f5b0b04f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1132 additions and 186 deletions

2
Cargo.lock generated
View File

@ -572,7 +572,7 @@ dependencies = [
[[package]] [[package]]
name = "espanso" name = "espanso"
version = "2.0.4-alpha" version = "2.0.5-alpha"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"caps", "caps",

View File

@ -44,7 +44,11 @@ int32_t clipboard_set_text(char * text) {
[pasteboard declareTypes:array owner:nil]; [pasteboard declareTypes:array owner:nil];
NSString *nsText = [NSString stringWithUTF8String:text]; 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) { int32_t clipboard_set_image(char * image_path) {

View File

@ -26,7 +26,7 @@ use std::{
use crate::{Clipboard, ClipboardOptions}; use crate::{Clipboard, ClipboardOptions};
use anyhow::Result; use anyhow::Result;
use log::error; use log::{error, warn};
use std::process::Command; use std::process::Command;
use thiserror::Error; use thiserror::Error;
use wait_timeout::ChildExt; use wait_timeout::ChildExt;
@ -49,7 +49,15 @@ impl WaylandFallbackClipboard {
// Try to connect to the wayland display // Try to connect to the wayland display
let wayland_socket = if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { 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 { } else {
error!("environment variable XDG_RUNTIME_DIR is missing, can't initialize the clipboard"); error!("environment variable XDG_RUNTIME_DIR is missing, can't initialize the clipboard");
return Err(WaylandFallbackClipboardError::MissingEnvVariable().into()); return Err(WaylandFallbackClipboardError::MissingEnvVariable().into());

View File

@ -2,7 +2,7 @@
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c // https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c
use anyhow::Result; 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 log::trace;
use scopeguard::ScopeGuard; use scopeguard::ScopeGuard;
use std::collections::HashMap; use std::collections::HashMap;
@ -127,7 +127,11 @@ impl Device {
} }
if len < 0 && unsafe { *errno_ptr } != EWOULDBLOCK { 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) Ok(events)
@ -322,6 +326,9 @@ pub enum DeviceError {
#[error("no devices found")] #[error("no devices found")]
NoDevicesFound(), NoDevicesFound(),
#[error("read operation can't block device")] #[error("read operation failed with code: `{0}`")]
BlockingReadOperation(), FailedRead(i32),
#[error("read operation failed: ENODEV No such device")]
FailedReadNoSuchDevice,
} }

View File

@ -38,8 +38,9 @@ use keymap::Keymap;
use lazycell::LazyCell; use lazycell::LazyCell;
use libc::{ use libc::{
__errno_location, close, epoll_ctl, epoll_event, epoll_wait, EINTR, EPOLLIN, EPOLL_CTL_ADD, __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 thiserror::Error;
use crate::event::{InputEvent, Key, KeyboardEvent, Variant}; 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]; let device = &self.devices[ev.u64 as usize];
match device.read() { match device.read() {
Ok(events) if !events.is_empty() => { Ok(events) if !events.is_empty() => {
@ -226,7 +229,30 @@ impl Source for EVDEVSource {
}); });
} }
Ok(_) => { /* SKIP EMPTY */ } 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::<DeviceError>() {
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)
}
}
} }
} }
} }

View File

@ -30,7 +30,7 @@ pub struct CursorHintCompensationEvent {
pub cursor_hint_back_count: usize, pub cursor_hint_back_count: usize,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct TextInjectRequest { pub struct TextInjectRequest {
pub text: String, pub text: String,
pub force_mode: Option<TextInjectMode>, pub force_mode: Option<TextInjectMode>,

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct MatchExecRequestEvent {
pub trigger: Option<String>,
pub args: HashMap<String, String>,
}

View File

@ -18,6 +18,7 @@
*/ */
pub mod effect; pub mod effect;
pub mod external;
pub mod input; pub mod input;
pub mod internal; pub mod internal;
pub mod ui; pub mod ui;
@ -48,7 +49,6 @@ impl Event {
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
pub enum EventType { pub enum EventType {
NOOP, NOOP,
ProcessingError(String),
ExitRequested(ExitMode), ExitRequested(ExitMode),
Exit(ExitMode), Exit(ExitMode),
Heartbeat, Heartbeat,
@ -60,6 +60,9 @@ pub enum EventType {
TrayIconClicked, TrayIconClicked,
ContextMenuClicked(input::ContextMenuClickedEvent), ContextMenuClicked(input::ContextMenuClickedEvent),
// External requests
MatchExecRequest(external::MatchExecRequestEvent),
// Internal // Internal
MatchesDetected(internal::MatchesDetectedEvent), MatchesDetected(internal::MatchesDetectedEvent),
MatchSelected(internal::MatchSelectedEvent), MatchSelected(internal::MatchSelectedEvent),
@ -73,11 +76,13 @@ pub enum EventType {
DiscardPrevious(internal::DiscardPreviousEvent), DiscardPrevious(internal::DiscardPreviousEvent),
DiscardBetween(internal::DiscardBetweenEvent), DiscardBetween(internal::DiscardBetweenEvent),
Undo(internal::UndoEvent), Undo(internal::UndoEvent),
RenderingError,
Disabled, Disabled,
Enabled, Enabled,
DisableRequest, DisableRequest,
EnableRequest, EnableRequest,
ToggleRequest,
SecureInputEnabled(internal::SecureInputEnabledEvent), SecureInputEnabled(internal::SecureInputEnabledEvent),
SecureInputDisabled, SecureInputDisabled,

View File

@ -33,15 +33,17 @@ use super::{
render::RenderMiddleware, render::RenderMiddleware,
}, },
DisableOptions, EnabledStatusProvider, MatchFilter, MatchInfoProvider, MatchProvider, DisableOptions, EnabledStatusProvider, MatchFilter, MatchInfoProvider, MatchProvider,
MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware, ModifierStateProvider, MatchResolver, MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware,
Multiplexer, PathProvider, Processor, Renderer, UndoEnabledProvider, ModifierStateProvider, Multiplexer, NotificationManager, PathProvider, Processor, Renderer,
UndoEnabledProvider,
}; };
use crate::{ use crate::{
event::{Event, EventType}, event::{Event, EventType},
process::middleware::{ process::middleware::{
context_menu::ContextMenuMiddleware, disable::DisableMiddleware, exit::ExitMiddleware, context_menu::ContextMenuMiddleware, disable::DisableMiddleware, exit::ExitMiddleware,
hotkey::HotKeyMiddleware, icon_status::IconStatusMiddleware, 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, undo::UndoMiddleware,
}, },
}; };
@ -70,6 +72,8 @@ impl<'a> DefaultProcessor<'a> {
undo_enabled_provider: &'a dyn UndoEnabledProvider, undo_enabled_provider: &'a dyn UndoEnabledProvider,
enabled_status_provider: &'a dyn EnabledStatusProvider, enabled_status_provider: &'a dyn EnabledStatusProvider,
modifier_state_provider: &'a dyn ModifierStateProvider, modifier_state_provider: &'a dyn ModifierStateProvider,
match_resolver: &'a dyn MatchResolver,
notification_manager: &'a dyn NotificationManager,
) -> DefaultProcessor<'a> { ) -> DefaultProcessor<'a> {
Self { Self {
event_queue: VecDeque::new(), event_queue: VecDeque::new(),
@ -82,6 +86,7 @@ impl<'a> DefaultProcessor<'a> {
matcher_options_provider, matcher_options_provider,
modifier_state_provider, modifier_state_provider,
)), )),
Box::new(MatchExecRequestMiddleware::new(match_resolver)),
Box::new(SuppressMiddleware::new(enabled_status_provider)), Box::new(SuppressMiddleware::new(enabled_status_provider)),
Box::new(ContextMenuMiddleware::new()), Box::new(ContextMenuMiddleware::new()),
Box::new(HotKeyMiddleware::new()), Box::new(HotKeyMiddleware::new()),
@ -103,6 +108,7 @@ impl<'a> DefaultProcessor<'a> {
)), )),
Box::new(SearchMiddleware::new(match_provider)), Box::new(SearchMiddleware::new(match_provider)),
Box::new(MarkdownMiddleware::new()), Box::new(MarkdownMiddleware::new()),
Box::new(NotificationMiddleware::new(notification_manager)),
Box::new(DelayForModifierReleaseMiddleware::new( Box::new(DelayForModifierReleaseMiddleware::new(
modifier_status_provider, modifier_status_provider,
)), )),

View File

@ -93,6 +93,10 @@ impl Middleware for DisableMiddleware {
*enabled = false; *enabled = false;
has_status_changed = true; has_status_changed = true;
} }
EventType::ToggleRequest => {
*enabled = !*enabled;
has_status_changed = true;
}
_ => {} _ => {}
} }

View File

@ -45,15 +45,8 @@ impl Middleware for MarkdownMiddleware {
// See also: https://github.com/federico-terzi/espanso/issues/759 // See also: https://github.com/federico-terzi/espanso/issues/759
let html = std::panic::catch_unwind(|| markdown::to_html(&m_event.markdown)); let html = std::panic::catch_unwind(|| markdown::to_html(&m_event.markdown));
if let Ok(html) = html { if let Ok(html) = html {
let mut html = html.trim(); let html = html.trim();
let html = remove_paragraph_tag_if_single_occurrence(html);
// Remove the surrounding paragraph
if html.starts_with("<p>") {
html = html.trim_start_matches("<p>");
}
if html.ends_with("</p>") {
html = html.trim_end_matches("</p>");
}
return Event::caused_by( return Event::caused_by(
event.source_id, 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("<p>").count();
if paragraph_count <= 1 {
let mut new_html = html;
if new_html.starts_with("<p>") {
new_html = new_html.trim_start_matches("<p>");
}
if new_html.ends_with("</p>") {
new_html = new_html.trim_end_matches("</p>");
}
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("<p>single occurrence</p>"),
"single occurrence"
);
assert_eq!(
remove_paragraph_tag_if_single_occurrence("<p>multi</p> <p>occurrence</p>"),
"<p>multi</p> <p>occurrence</p>"
);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<DetectedMatch>;
}
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

View File

@ -29,9 +29,11 @@ pub mod hotkey;
pub mod icon_status; pub mod icon_status;
pub mod image_resolve; pub mod image_resolve;
pub mod markdown; pub mod markdown;
pub mod match_exec;
pub mod match_select; pub mod match_select;
pub mod matcher; pub mod matcher;
pub mod multiplex; pub mod multiplex;
pub mod notification;
pub mod render; pub mod render;
pub mod search; pub mod search;
pub mod suppress; pub mod suppress;

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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

View File

@ -22,7 +22,7 @@ use std::collections::HashMap;
use log::error; use log::error;
use super::super::Middleware; use super::super::Middleware;
use crate::event::{internal::RenderedEvent, Event, EventType}; use crate::event::{effect::TextInjectRequest, internal::RenderedEvent, Event, EventType};
use anyhow::Result; use anyhow::Result;
use thiserror::Error; use thiserror::Error;
@ -62,7 +62,7 @@ impl<'a> Middleware for RenderMiddleware<'a> {
"render" "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 { if let EventType::RenderingRequested(m_event) = event.etype {
match self.renderer.render( match self.renderer.render(
m_event.match_id, m_event.match_id,
@ -91,7 +91,16 @@ impl<'a> Middleware for RenderMiddleware<'a> {
} }
_ => { _ => {
error!("error during rendering: {:?}", err); 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);
} }
}, },
} }

View File

@ -89,12 +89,11 @@ impl<'a> Middleware for UndoMiddleware<'a> {
} }
*record = None; *record = None;
} }
} else if let EventType::Mouse(_) = &event.etype { } else if let EventType::Mouse(_) | EventType::CursorHintCompensation(_) = &event.etype {
// Any mouse event invalidates the undo feature, as it could // Explanation:
// * Any mouse event invalidates the undo feature, as it could
// represent a change in application // represent a change in application
*record = None; // * Cursor hints invalidate the undo feature, as it would be pretty
} 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. // complex to determine which delete operations should be performed.
// This might change in the future. // This might change in the future.
*record = None; *record = None;

View File

@ -37,12 +37,14 @@ pub use middleware::action::{EventSequenceProvider, MatchInfoProvider};
pub use middleware::delay_modifiers::ModifierStatusProvider; pub use middleware::delay_modifiers::ModifierStatusProvider;
pub use middleware::disable::DisableOptions; pub use middleware::disable::DisableOptions;
pub use middleware::image_resolve::PathProvider; pub use middleware::image_resolve::PathProvider;
pub use middleware::match_exec::MatchResolver;
pub use middleware::match_select::{MatchFilter, MatchSelector}; pub use middleware::match_select::{MatchFilter, MatchSelector};
pub use middleware::matcher::{ pub use middleware::matcher::{
MatchResult, Matcher, MatcherEvent, MatcherMiddlewareConfigProvider, ModifierState, MatchResult, Matcher, MatcherEvent, MatcherMiddlewareConfigProvider, ModifierState,
ModifierStateProvider, ModifierStateProvider,
}; };
pub use middleware::multiplex::Multiplexer; pub use middleware::multiplex::Multiplexer;
pub use middleware::notification::NotificationManager;
pub use middleware::render::{Renderer, RendererError}; pub use middleware::render::{Renderer, RendererError};
pub use middleware::search::MatchProvider; pub use middleware::search::MatchProvider;
pub use middleware::suppress::EnabledStatusProvider; pub use middleware::suppress::EnabledStatusProvider;
@ -65,6 +67,8 @@ pub fn default<'a, MatcherState>(
undo_enabled_provider: &'a dyn UndoEnabledProvider, undo_enabled_provider: &'a dyn UndoEnabledProvider,
enabled_status_provider: &'a dyn EnabledStatusProvider, enabled_status_provider: &'a dyn EnabledStatusProvider,
modifier_state_provider: &'a dyn ModifierStateProvider, modifier_state_provider: &'a dyn ModifierStateProvider,
match_resolver: &'a dyn MatchResolver,
notification_manager: &'a dyn NotificationManager,
) -> impl Processor + 'a { ) -> impl Processor + 'a {
default::DefaultProcessor::new( default::DefaultProcessor::new(
matchers, matchers,
@ -82,5 +86,7 @@ pub fn default<'a, MatcherState>(
undo_enabled_provider, undo_enabled_provider,
enabled_status_provider, enabled_status_provider,
modifier_state_provider, modifier_state_provider,
match_resolver,
notification_manager,
) )
} }

View File

@ -1,7 +1,7 @@
// This code is a port of the libxkbcommon "interactive-evdev.c" example // This code is a port of the libxkbcommon "interactive-evdev.c" example
// https://github.com/xkbcommon/libxkbcommon/blob/master/tools/interactive-evdev.c // 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; use scopeguard::ScopeGuard;
@ -48,17 +48,17 @@ impl State {
} }
pub fn get_string(&self, code: u32) -> Option<String> { pub fn get_string(&self, code: u32) -> Option<String> {
let mut buffer: [u8; 16] = [0; 16]; let mut buffer: [c_char; 16] = [0; 16];
let len = unsafe { let len = unsafe {
xkb_state_key_get_utf8( xkb_state_key_get_utf8(
self.state, self.state,
code, code,
buffer.as_mut_ptr() as *mut i8, buffer.as_mut_ptr(),
std::mem::size_of_val(&buffer), std::mem::size_of_val(&buffer),
) )
}; };
if len > 0 { 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(); let string = content_raw.to_string_lossy().to_string();
if string.is_empty() { if string.is_empty() {
None None

View File

@ -23,6 +23,7 @@ use libc::{c_uint, close, ioctl, open, O_NONBLOCK, O_WRONLY};
use scopeguard::ScopeGuard; use scopeguard::ScopeGuard;
use anyhow::Result; use anyhow::Result;
use log::error;
use thiserror::Error; use thiserror::Error;
use super::ffi::{ use super::ffi::{
@ -39,6 +40,9 @@ impl UInputDevice {
let uinput_path = CString::new("/dev/uinput").expect("unable to generate /dev/uinput path"); 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) }; let raw_fd = unsafe { open(uinput_path.as_ptr(), O_WRONLY | O_NONBLOCK) };
if raw_fd < 0 { 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()); return Err(UInputDeviceError::Open().into());
} }
let fd = scopeguard::guard(raw_fd, |raw_fd| unsafe { let fd = scopeguard::guard(raw_fd, |raw_fd| unsafe {

View File

@ -420,15 +420,26 @@ fn macos_link_search_path() -> Option<String> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn build_native() { fn build_native() {
// Make sure wxWidgets is installed // 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") .arg("--version")
.output() .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 cpp_flags = get_cpp_flags(&config_path);
let mut build = cc::Build::new(); let mut build = cc::Build::new();

View File

@ -89,11 +89,16 @@ public:
std::vector<void *> fields; std::vector<void *> fields;
std::unordered_map<const char *, std::unique_ptr<FieldWrapper>> idMap; std::unordered_map<const char *, std::unique_ptr<FieldWrapper>> idMap;
wxButton *submit; wxButton *submit;
wxStaticText *helpText;
bool hasFocusedMultilineControl;
private: private:
void AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata meta); void AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata meta);
void Submit(); void Submit();
void OnSubmitBtn(wxCommandEvent& event); void OnSubmitBtn(wxCommandEvent& event);
void OnEscape(wxKeyEvent& event); void OnCharHook(wxKeyEvent& event);
void UpdateHelpText();
void HandleNormalFocus(wxFocusEvent& event);
void HandleMultilineFocus(wxFocusEvent& event);
}; };
enum enum
{ {
@ -113,6 +118,8 @@ bool FormApp::OnInit()
FormFrame::FormFrame(const wxString& title, const wxPoint& pos, const wxSize& size) FormFrame::FormFrame(const wxString& title, const wxPoint& pos, const wxSize& size)
: wxFrame(NULL, wxID_ANY, title, pos, size, DEFAULT_STYLE) : wxFrame(NULL, wxID_ANY, title, pos, size, DEFAULT_STYLE)
{ {
hasFocusedMultilineControl = false;
panel = new wxPanel(this, wxID_ANY); panel = new wxPanel(this, wxID_ANY);
wxBoxSizer *vbox = new wxBoxSizer(wxVERTICAL); wxBoxSizer *vbox = new wxBoxSizer(wxVERTICAL);
panel->SetSizer(vbox); 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"); submit = new wxButton(panel, ID_Submit, "Submit");
vbox->Add(submit, 1, wxEXPAND | wxALL, PADDING); 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_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 // TODO: register ESC click handler: https://forums.wxwidgets.org/viewtopic.php?t=41926
this->SetClientSize(panel->GetBestSize()); this->SetClientSize(panel->GetBestSize());
@ -157,6 +171,9 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m
if (textMeta->multiline) { if (textMeta->multiline) {
textControl->SetMinSize(wxSize(MULTILINE_MIN_WIDTH, MULTILINE_MIN_HEIGHT)); 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 // Create the field wrapper
@ -188,6 +205,8 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m
((wxChoice*)choice)->SetSelection(selectedItem); ((wxChoice*)choice)->SetSelection(selectedItem);
} }
((wxChoice*)choice)->Bind(wxEVT_SET_FOCUS, &FormFrame::HandleNormalFocus, this, wxID_ANY);
// Create the field wrapper // Create the field wrapper
std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ChoiceFieldWrapper((wxChoice*) choice)); std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ChoiceFieldWrapper((wxChoice*) choice));
idMap[meta.id] = std::move(field); idMap[meta.id] = std::move(field);
@ -198,13 +217,13 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m
((wxListBox*)choice)->SetSelection(selectedItem); ((wxListBox*)choice)->SetSelection(selectedItem);
} }
((wxListBox*)choice)->Bind(wxEVT_SET_FOCUS, &FormFrame::HandleNormalFocus, this, wxID_ANY);
// Create the field wrapper // Create the field wrapper
std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ListFieldWrapper((wxListBox*) choice)); std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ListFieldWrapper((wxListBox*) choice));
idMap[meta.id] = std::move(field); idMap[meta.id] = std::move(field);
} }
control = choice; control = choice;
fields.push_back(choice); fields.push_back(choice);
break; break;
@ -253,18 +272,43 @@ void FormFrame::Submit() {
Close(true); 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) { void FormFrame::OnSubmitBtn(wxCommandEvent &event) {
Submit(); Submit();
} }
void FormFrame::OnEscape(wxKeyEvent& event) { void FormFrame::OnCharHook(wxKeyEvent& event) {
if (event.GetKeyCode() == WXK_ESCAPE) { if (event.GetKeyCode() == WXK_ESCAPE) {
Close(true); Close(true);
}else if(event.GetKeyCode() == WXK_RETURN && wxGetKeyState(WXK_RAW_CONTROL)) { }else if(event.GetKeyCode() == WXK_RETURN) {
if (!hasFocusedMultilineControl || wxGetKeyState(WXK_RAW_CONTROL)) {
Submit(); Submit();
} else { } else {
event.Skip(); event.Skip();
} }
}else{
event.Skip();
}
} }
extern "C" void interop_show_form(FormMetadata * _metadata, void (*callback)(ValuePair *values, int size, void *data), void *data) { extern "C" void interop_show_form(FormMetadata * _metadata, void (*callback)(ValuePair *values, int size, void *data), void *data) {

View File

@ -236,6 +236,7 @@ SearchFrame::SearchFrame(const wxString &title, const wxPoint &pos, const wxSize
vbox->Add(resultBox, 5, wxEXPAND | wxALL, 0); vbox->Add(resultBox, 5, wxEXPAND | wxALL, 0);
Bind(wxEVT_CHAR_HOOK, &SearchFrame::OnCharEvent, this, wxID_ANY); 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_TEXT, &SearchFrame::OnQueryChange, this, textId);
Bind(wxEVT_LISTBOX_DCLICK, &SearchFrame::OnItemClickEvent, this, resultId); Bind(wxEVT_LISTBOX_DCLICK, &SearchFrame::OnItemClickEvent, this, resultId);
Bind(wxEVT_ACTIVATE, &SearchFrame::OnActivate, this, wxID_ANY); Bind(wxEVT_ACTIVATE, &SearchFrame::OnActivate, this, wxID_ANY);
@ -276,9 +277,9 @@ void SearchFrame::OnCharEvent(wxKeyEvent &event)
SelectNext(); SelectNext();
} }
} }
else if (event.GetKeyCode() >= 49 && event.GetKeyCode() <= 56) else if (event.GetUnicodeKey() >= '1' && event.GetUnicodeKey() <= '9')
{ // Alt + num shortcut { // Alt + num shortcut
int index = event.GetKeyCode() - 49; int index = event.GetUnicodeKey() - '1';
if (wxGetKeyState(WXK_ALT)) if (wxGetKeyState(WXK_ALT))
{ {
if (resultBox->GetItemCount() > index) if (resultBox->GetItemCount() > index)

View File

@ -115,6 +115,7 @@ protected:
void add_path_continue_clicked( wxCommandEvent& event ); void add_path_continue_clicked( wxCommandEvent& event );
void accessibility_enable_clicked( wxCommandEvent& event ); void accessibility_enable_clicked( wxCommandEvent& event );
void quit_espanso_clicked( wxCommandEvent& event ); void quit_espanso_clicked( wxCommandEvent& event );
void move_bundle_quit_clicked( wxCommandEvent& event );
void navigate_to_next_page_or_close(); void navigate_to_next_page_or_close();
void change_default_button(int target_page); 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() bool WizardApp::OnInit()
{ {
wxInitAllImageHandlers(); wxInitAllImageHandlers();

View File

@ -708,7 +708,7 @@
<property name="window_extra_style"></property> <property name="window_extra_style"></property>
<property name="window_name"></property> <property name="window_name"></property>
<property name="window_style"></property> <property name="window_style"></property>
<property name="wrap">-1</property> <property name="wrap">500</property>
</object> </object>
</object> </object>
<object class="sizeritem" expanded="1"> <object class="sizeritem" expanded="1">
@ -758,7 +758,7 @@
<property name="gripper">0</property> <property name="gripper">0</property>
<property name="hidden">0</property> <property name="hidden">0</property>
<property name="id">wxID_ANY</property> <property name="id">wxID_ANY</property>
<property name="label">Start</property> <property name="label">Quit</property>
<property name="margins"></property> <property name="margins"></property>
<property name="markup">0</property> <property name="markup">0</property>
<property name="max_size"></property> <property name="max_size"></property>

View File

@ -80,13 +80,13 @@ WizardFrame::WizardFrame( wxWindow* parent, wxWindowID id, const wxString& title
bSizer22->Add( 0, 20, 0, 0, 5 ); 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 = 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( move_bundle_description, 0, wxALL, 10 );
bSizer22->Add( 0, 20, 1, wxEXPAND, 5 ); 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(); move_bundle_quit_button->SetDefault();
bSizer22->Add( move_bundle_quit_button, 0, wxALIGN_RIGHT|wxALL, 10 ); bSizer22->Add( move_bundle_quit_button, 0, wxALIGN_RIGHT|wxALL, 10 );

View File

@ -1,6 +1,6 @@
[package] [package]
name = "espanso" name = "espanso"
version = "2.0.4-alpha" version = "2.0.5-alpha"
authors = ["Federico Terzi <federicoterzi96@gmail.com>"] authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
license = "GPL-3.0" license = "GPL-3.0"
description = "Cross-platform Text Expander written in Rust" description = "Cross-platform Text Expander written in Rust"

72
espanso/src/cli/cmd.rs Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}

View File

@ -28,7 +28,7 @@ use espanso_path::Paths;
use log::{error, info, warn}; use log::{error, info, warn};
use crate::{ use crate::{
cli::util::CommandExt, cli::util::{prevent_running_as_root_on_macos, CommandExt},
common_flags::*, common_flags::*,
exit_code::{ exit_code::{
DAEMON_ALREADY_RUNNING, DAEMON_FATAL_CONFIG_ERROR, DAEMON_GENERAL_ERROR, DAEMON_ALREADY_RUNNING, DAEMON_FATAL_CONFIG_ERROR, DAEMON_GENERAL_ERROR,
@ -60,6 +60,8 @@ pub fn new() -> CliModule {
} }
fn daemon_main(args: CliModuleArgs) -> i32 { fn daemon_main(args: CliModuleArgs) -> i32 {
prevent_running_as_root_on_macos();
let paths = args.paths.expect("missing paths in daemon main"); let paths = args.paths.expect("missing paths in daemon main");
let paths_overrides = args let paths_overrides = args
.paths_overrides .paths_overrides

View File

@ -72,7 +72,7 @@ fn launcher_main(args: CliModuleArgs) -> i32 {
let is_welcome_page_enabled = !preferences.has_completed_wizard(); 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 is_legacy_version_page_enabled = util::is_legacy_version_running(&paths.runtime);
let runtime_dir_clone = paths.runtime.clone(); let runtime_dir_clone = paths.runtime.clone();

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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(())
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<dyn ConfigStore>,
match_store: Box<dyn MatchStore>,
) -> 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<String>,
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(())
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

View File

@ -23,11 +23,13 @@ use clap::ArgMatches;
use espanso_config::{config::ConfigStore, error::NonFatalErrorSet, matches::store::MatchStore}; use espanso_config::{config::ConfigStore, error::NonFatalErrorSet, matches::store::MatchStore};
use espanso_path::Paths; use espanso_path::Paths;
pub mod cmd;
pub mod daemon; pub mod daemon;
pub mod edit; pub mod edit;
pub mod env_path; pub mod env_path;
pub mod launcher; pub mod launcher;
pub mod log; pub mod log;
pub mod match_cli;
pub mod migrate; pub mod migrate;
pub mod modulo; pub mod modulo;
pub mod package; pub mod package;

View File

@ -17,18 +17,31 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use anyhow::Result; use anyhow::{bail, Result};
use log::{info, warn}; use log::{error, info, warn};
use std::process::Command; use std::process::Command;
use std::{fs::create_dir_all, process::ExitStatus}; use std::{fs::create_dir_all, process::ExitStatus};
use thiserror::Error; use thiserror::Error;
use crate::cli::util::prevent_running_as_root_on_macos;
use crate::error_eprintln;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
const SERVICE_PLIST_CONTENT: &str = include_str!("../../res/macos/com.federicoterzi.espanso.plist"); const SERVICE_PLIST_CONTENT: &str = include_str!("../../res/macos/com.federicoterzi.espanso.plist");
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
const SERVICE_PLIST_FILE_NAME: &str = "com.federicoterzi.espanso.plist"; const SERVICE_PLIST_FILE_NAME: &str = "com.federicoterzi.espanso.plist";
pub fn register() -> Result<()> { 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 home_dir = dirs::home_dir().expect("could not get user home directory");
let library_dir = home_dir.join("Library"); let library_dir = home_dir.join("Library");
let agents_dir = library_dir.join("LaunchAgents"); let agents_dir = library_dir.join("LaunchAgents");
@ -94,6 +107,8 @@ pub enum RegisterError {
} }
pub fn unregister() -> Result<()> { pub fn unregister() -> Result<()> {
prevent_running_as_root_on_macos();
let home_dir = dirs::home_dir().expect("could not get user home directory"); let home_dir = dirs::home_dir().expect("could not get user home directory");
let library_dir = home_dir.join("Library"); let library_dir = home_dir.join("Library");
let agents_dir = library_dir.join("LaunchAgents"); let agents_dir = library_dir.join("LaunchAgents");

View File

@ -17,12 +17,14 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::time::Instant;
use super::{CliModule, CliModuleArgs, PathsOverrides}; use super::{CliModule, CliModuleArgs, PathsOverrides};
use crate::{ use crate::{
error_eprintln, error_eprintln,
exit_code::{ exit_code::{
SERVICE_ALREADY_RUNNING, SERVICE_FAILURE, SERVICE_NOT_REGISTERED, SERVICE_NOT_RUNNING, SERVICE_ALREADY_RUNNING, SERVICE_FAILURE, SERVICE_NOT_REGISTERED, SERVICE_NOT_RUNNING,
SERVICE_SUCCESS, SERVICE_SUCCESS, SERVICE_TIMED_OUT,
}, },
info_println, info_println,
lock::acquire_worker_lock, lock::acquire_worker_lock,
@ -99,6 +101,7 @@ fn service_main(args: CliModuleArgs) -> i32 {
return status_main(&paths); return status_main(&paths);
} else if let Some(sub_args) = cli_args.subcommand_matches("restart") { } else if let Some(sub_args) = cli_args.subcommand_matches("restart") {
stop_main(&paths); stop_main(&paths);
std::thread::sleep(std::time::Duration::from_millis(300));
return start_main(&paths, &paths_overrides, sub_args); 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() { if let Err(err) = start_service() {
error_eprintln!("unable to start service: {}", err); error_eprintln!("unable to start service: {}", err);
return SERVICE_FAILURE; 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 { fn stop_main(paths: &Paths) -> i32 {

View File

@ -53,3 +53,33 @@ impl CommandExt for Command {
self 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/")
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<EventType>,
pub sequencer: &'a Sequencer,
}
impl<'a> IpcEventSource<'a> {
pub fn new(ipc_event_receiver: Receiver<EventType>, 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<Event> {
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
)
}

View File

@ -35,6 +35,7 @@ use self::{
pub mod detect; pub mod detect;
pub mod exit; pub mod exit;
pub mod ipc;
pub mod key_state; pub mod key_state;
pub mod modifier; pub mod modifier;
pub mod secure_input; pub mod secure_input;

View File

@ -43,15 +43,10 @@ impl<'a> SecureInputSource<'a> {
impl<'a> funnel::Source<'a> for SecureInputSource<'a> { impl<'a> funnel::Source<'a> for SecureInputSource<'a> {
fn register(&'a self, select: &mut Select<'a>) -> usize { fn register(&'a self, select: &mut Select<'a>) -> usize {
if cfg!(target_os = "macos") {
select.recv(&self.receiver) select.recv(&self.receiver)
} else {
999999
}
} }
fn receive(&self, op: SelectedOperation) -> Option<Event> { fn receive(&self, op: SelectedOperation) -> Option<Event> {
if cfg!(target_os = "macos") {
let si_event = op let si_event = op
.recv(&self.receiver) .recv(&self.receiver)
.expect("unable to select data from SecureInputSource receiver"); .expect("unable to select data from SecureInputSource receiver");
@ -65,11 +60,5 @@ impl<'a> funnel::Source<'a> for SecureInputSource<'a> {
} }
}, },
}) })
} else {
Some(Event {
source_id: self.sequencer.next_id(),
etype: EventType::NOOP,
})
}
} }
} }

View File

@ -23,7 +23,7 @@ use anyhow::Result;
use crossbeam::channel::Receiver; use crossbeam::channel::Receiver;
use espanso_config::{config::ConfigStore, matches::store::MatchStore}; use espanso_config::{config::ConfigStore, matches::store::MatchStore};
use espanso_detect::SourceCreationOptions; use espanso_detect::SourceCreationOptions;
use espanso_engine::event::ExitMode; use espanso_engine::event::{EventType, ExitMode};
use espanso_inject::{InjectorCreationOptions, KeyboardStateProvider}; use espanso_inject::{InjectorCreationOptions, KeyboardStateProvider};
use espanso_path::Paths; use espanso_path::Paths;
use espanso_ui::{event::UIEvent, UIRemote}; use espanso_ui::{event::UIEvent, UIRemote};
@ -82,6 +82,7 @@ pub fn initialize_and_spawn(
secure_input_receiver: Receiver<SecureInputEvent>, secure_input_receiver: Receiver<SecureInputEvent>,
use_evdev_backend: bool, use_evdev_backend: bool,
start_reason: Option<String>, start_reason: Option<String>,
ipc_event_receiver: Receiver<EventType>,
) -> Result<JoinHandle<ExitMode>> { ) -> Result<JoinHandle<ExitMode>> {
let handle = std::thread::Builder::new() let handle = std::thread::Builder::new()
.name("engine thread".to_string()) .name("engine thread".to_string())
@ -131,17 +132,18 @@ pub fn initialize_and_spawn(
}) })
.expect("failed to initialize detector module"); .expect("failed to initialize detector module");
let exit_source = super::engine::funnel::exit::ExitSource::new(exit_signal, &sequencer); 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 ui_source = super::engine::funnel::ui::UISource::new(ui_event_receiver, &sequencer);
let secure_input_source = super::engine::funnel::secure_input::SecureInputSource::new( let secure_input_source = super::engine::funnel::secure_input::SecureInputSource::new(
secure_input_receiver, secure_input_receiver,
&sequencer, &sequencer,
); );
let sources: Vec<&dyn espanso_engine::funnel::Source> = vec![ let mut sources: Vec<&dyn espanso_engine::funnel::Source> =
&detect_source, vec![&detect_source, &exit_source, &ui_source, &ipc_event_source];
&exit_source, if cfg!(target_os = "macos") {
&ui_source, sources.push(&secure_input_source);
&secure_input_source, }
];
let funnel = espanso_engine::funnel::default(&sources); let funnel = espanso_engine::funnel::default(&sources);
let rolling_matcher = RollingMatcherAdapter::new( let rolling_matcher = RollingMatcherAdapter::new(
@ -210,6 +212,8 @@ pub fn initialize_and_spawn(
let disable_options = let disable_options =
process::middleware::disable::extract_disable_options(&*config_manager.default()); 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( let mut processor = espanso_engine::process::default(
&matchers, &matchers,
&config_manager, &config_manager,
@ -226,6 +230,8 @@ pub fn initialize_and_spawn(
&config_manager, &config_manager,
&config_manager, &config_manager,
&modifier_state_store, &modifier_state_store,
&combined_match_cache,
&notification_manager,
); );
let event_injector = EventInjectorAdapter::new(&*injector, &config_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() { match start_reason.as_deref() {
Some(flag) if flag == WORKER_START_REASON_CONFIG_CHANGED => { Some(flag) if flag == WORKER_START_REASON_CONFIG_CHANGED => {
notification_manager.notify_config_reloaded(false); notification_manager.notify_config_reloaded(false);

View File

@ -21,13 +21,17 @@ use std::path::Path;
use anyhow::Result; use anyhow::Result;
use crossbeam::channel::Sender; use crossbeam::channel::Sender;
use espanso_engine::event::ExitMode; use espanso_engine::event::{external::MatchExecRequestEvent, EventType, ExitMode};
use espanso_ipc::{EventHandlerResponse, IPCServer}; use espanso_ipc::{EventHandlerResponse, IPCServer};
use log::{error, warn}; use log::{error, warn};
use crate::ipc::IPCEvent; use crate::ipc::IPCEvent;
pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<ExitMode>) -> Result<()> { pub fn initialize_and_spawn(
runtime_dir: &Path,
exit_notify: Sender<ExitMode>,
event_notify: Sender<EventType>,
) -> Result<()> {
let server = crate::ipc::create_worker_ipc_server(runtime_dir)?; let server = crate::ipc::create_worker_ipc_server(runtime_dir)?;
std::thread::Builder::new() std::thread::Builder::new()
@ -55,6 +59,17 @@ pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<ExitMode>) -
EventHandlerResponse::NoResponse 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)] #[allow(unreachable_patterns)]
unexpected_event => { unexpected_event => {
warn!( warn!(
@ -70,3 +85,17 @@ pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<ExitMode>) -
Ok(()) Ok(())
} }
fn send_event(
event_notify: &Sender<EventType>,
event: EventType,
) -> EventHandlerResponse<IPCEvent> {
if let Err(err) = event_notify.send(event) {
error!(
"experienced error while sending event signal from worker ipc handler: {}",
err
);
}
EventHandlerResponse::NoResponse
}

View File

@ -21,8 +21,9 @@ use std::collections::HashMap;
use espanso_config::{ use espanso_config::{
config::ConfigStore, 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}; use super::{builtin::BuiltInMatch, engine::process::middleware::match_select::MatchSummary};
@ -164,3 +165,50 @@ impl<'a> espanso_engine::process::MatchProvider for CombinedMatchCache<'a> {
ids ids
} }
} }
impl<'a> espanso_engine::process::MatchResolver for CombinedMatchCache<'a> {
fn find_matches_from_trigger(&self, trigger: &str) -> Vec<DetectedMatch> {
let user_matches: Vec<DetectedMatch> = 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<DetectedMatch> = 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
}
}

View File

@ -22,6 +22,7 @@ use espanso_engine::event::ExitMode;
use log::{debug, error, info}; use log::{debug, error, info};
use crate::{ use crate::{
cli::util::prevent_running_as_root_on_macos,
exit_code::{ exit_code::{
WORKER_ALREADY_RUNNING, WORKER_EXIT_ALL_PROCESSES, WORKER_GENERAL_ERROR, WORKER_ALREADY_RUNNING, WORKER_EXIT_ALL_PROCESSES, WORKER_GENERAL_ERROR,
WORKER_LEGACY_ALREADY_RUNNING, WORKER_RESTART, WORKER_SUCCESS, WORKER_LEGACY_ALREADY_RUNNING, WORKER_RESTART, WORKER_SUCCESS,
@ -58,6 +59,8 @@ pub fn new() -> CliModule {
} }
fn worker_main(args: CliModuleArgs) -> i32 { fn worker_main(args: CliModuleArgs) -> i32 {
prevent_running_as_root_on_macos();
let paths = args.paths.expect("missing paths in worker main"); let paths = args.paths.expect("missing paths in worker main");
let cli_args = args.cli_args.expect("missing cli_args 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"); .expect("unable to initialize UI module");
let (engine_exit_notify, engine_exit_receiver) = unbounded(); 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_ui_event_sender, engine_ui_event_receiver) = unbounded();
let (engine_secure_input_sender, engine_secure_input_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, engine_secure_input_receiver,
use_evdev_backend, use_evdev_backend,
start_reason, start_reason,
ipc_event_receiver,
) )
.expect("unable to initialize engine"); .expect("unable to initialize engine");
// Setup the IPC server // 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"); .expect("unable to initialize IPC server");
// If specified, automatically monitor the daemon status and // If specified, automatically monitor the daemon status and

View File

@ -54,3 +54,23 @@ impl<'a> NotificationManager<'a> {
self.notify("Updated keyboard layout!"); 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.");
}
}

View File

@ -51,6 +51,7 @@ pub const SERVICE_FAILURE: i32 = 1;
pub const SERVICE_NOT_REGISTERED: i32 = 2; pub const SERVICE_NOT_REGISTERED: i32 = 2;
pub const SERVICE_ALREADY_RUNNING: i32 = 3; pub const SERVICE_ALREADY_RUNNING: i32 = 3;
pub const SERVICE_NOT_RUNNING: i32 = 4; pub const SERVICE_NOT_RUNNING: i32 = 4;
pub const SERVICE_TIMED_OUT: i32 = 5;
pub const WORKAROUND_SUCCESS: i32 = 0; pub const WORKAROUND_SUCCESS: i32 = 0;
#[allow(dead_code)] #[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_FAILED: i32 = 5;
pub const PACKAGE_UPDATE_PARTIAL_FAILURE: i32 = 6; pub const PACKAGE_UPDATE_PARTIAL_FAILURE: i32 = 6;
#[allow(dead_code)]
pub const UNEXPECTED_RUN_AS_ROOT: i32 = 42;
use std::sync::Mutex; use std::sync::Mutex;
use crate::error_eprintln; use crate::error_eprintln;

View File

@ -20,12 +20,25 @@
use anyhow::Result; use anyhow::Result;
use espanso_ipc::{IPCClient, IPCServer}; use espanso_ipc::{IPCClient, IPCServer};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path; use std::{collections::HashMap, path::Path};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum IPCEvent { pub enum IPCEvent {
Exit, Exit,
ExitAllProcesses, ExitAllProcesses,
EnableRequest,
DisableRequest,
ToggleRequest,
OpenSearchBar,
RequestMatchExpansion(RequestMatchExpansionPayload),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RequestMatchExpansionPayload {
pub trigger: Option<String>,
pub args: HashMap<String, String>,
} }
pub fn create_daemon_ipc_server(runtime_dir: &Path) -> Result<impl IPCServer<IPCEvent>> { pub fn create_daemon_ipc_server(runtime_dir: &Path) -> Result<impl IPCServer<IPCEvent>> {

View File

@ -71,6 +71,8 @@ lazy_static! {
cli::service::new(), cli::service::new(),
cli::workaround::new(), cli::workaround::new(),
cli::package::new(), cli::package::new(),
cli::match_cli::new(),
cli::cmd::new(),
]; ];
static ref ALIASES: Vec<CliAlias> = vec![ static ref ALIASES: Vec<CliAlias> = vec![
CliAlias { CliAlias {
@ -219,17 +221,17 @@ fn main() {
.subcommand(SubCommand::with_name("unregister").about("Remove 'espanso' command from PATH")) .subcommand(SubCommand::with_name("unregister").about("Remove 'espanso' command from PATH"))
.about("Add or remove the 'espanso' command from the PATH"), .about("Add or remove the 'espanso' command from the PATH"),
) )
// .subcommand(SubCommand::with_name("cmd") .subcommand(SubCommand::with_name("cmd")
// .about("Send a command to the espanso daemon.") .about("Send a command to the espanso daemon.")
// .subcommand(SubCommand::with_name("exit") .subcommand(SubCommand::with_name("enable")
// .about("Terminate the daemon.")) .about("Enable expansions."))
// .subcommand(SubCommand::with_name("enable") .subcommand(SubCommand::with_name("disable")
// .about("Enable the espanso replacement engine.")) .about("Disable expansions."))
// .subcommand(SubCommand::with_name("disable") .subcommand(SubCommand::with_name("toggle")
// .about("Disable the espanso replacement engine.")) .about("Enable/Disable expansions."))
// .subcommand(SubCommand::with_name("toggle") .subcommand(SubCommand::with_name("search")
// .about("Toggle the status of the espanso replacement engine.")) .about("Open the Espanso's search bar."))
// ) )
.subcommand(SubCommand::with_name("edit") .subcommand(SubCommand::with_name("edit")
.about("Shortcut to open the default text editor to edit config files") .about("Shortcut to open the default text editor to edit config files")
.arg(Arg::with_name("target_file") .arg(Arg::with_name("target_file")
@ -251,10 +253,6 @@ For example, specifying 'email' is equivalent to 'match/email.yml'."#))
.setting(AppSettings::Hidden) .setting(AppSettings::Hidden)
.about("Start the daemon without spawning a new process."), .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("launcher").setting(AppSettings::Hidden))
.subcommand(SubCommand::with_name("log").about("Print the daemon logs.")) .subcommand(SubCommand::with_name("log").about("Print the daemon logs."))
.subcommand( .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(
SubCommand::with_name("path") SubCommand::with_name("path")
.about("Prints all the espanso directory paths to easily locate configuration and matches.") .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(restart_subcommand)
.subcommand(stop_subcommand) .subcommand(stop_subcommand)
.subcommand(status_subcommand) .subcommand(status_subcommand)
// .subcommand(SubCommand::with_name("match") .subcommand(SubCommand::with_name("match")
// .about("List and execute matches from the CLI") .about("List and execute matches from the CLI")
// .subcommand(SubCommand::with_name("list") .subcommand(SubCommand::with_name("list")
// .about("Print all matches to standard output") .about("Print matches to standard output")
// .arg(Arg::with_name("json") .arg(Arg::with_name("json")
// .short("j") .short("j")
// .long("json") .long("json")
// .help("Return the matches as json") .help("Output matches to the JSON format")
// .required(false) .required(false)
// .takes_value(false) .takes_value(false)
// ) )
// .arg(Arg::with_name("onlytriggers") .arg(Arg::with_name("onlytriggers")
// .short("t") .short("t")
// .long("onlytriggers") .long("only-triggers")
// .help("Print only triggers without replacement") .help("Print only triggers without replacement")
// .required(false) .required(false)
// .takes_value(false) .takes_value(false)
// ) )
// .arg(Arg::with_name("preservenewlines") .arg(Arg::with_name("preservenewlines")
// .short("n") .short("n")
// .long("preservenewlines") .long("preserve-newlines")
// .help("Preserve newlines when printing replacements") .help("Preserve newlines when printing replacements. Does nothing when using JSON format.")
// .required(false) .required(false)
// .takes_value(false) .takes_value(false)
// ) )
// ) .arg(Arg::with_name("class")
// .subcommand(SubCommand::with_name("exec") .long("class")
// .about("Triggers the expansion of the given match") .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.")
// .arg(Arg::with_name("trigger") .required(false)
// .help("The trigger of the match to be expanded") .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(
SubCommand::with_name("package") SubCommand::with_name("package")
.about("package-management commands") .about("package-management commands")

View File

@ -17,11 +17,11 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::io::ErrorKind; use std::path::{Path, PathBuf};
use std::path::PathBuf; use std::{fs::create_dir_all, io::ErrorKind};
use thiserror::Error; use thiserror::Error;
use anyhow::Result; use anyhow::{bail, Context, Result};
use log::{error, warn}; use log::{error, warn};
pub fn is_espanso_in_path() -> bool { pub fn is_espanso_in_path() -> bool {
@ -29,21 +29,59 @@ pub fn is_espanso_in_path() -> bool {
} }
pub fn add_espanso_to_path(prompt_when_necessary: bool) -> Result<()> { 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 target_link_dir = PathBuf::from("/usr/local/bin");
let exec_path = std::env::current_exe()?; 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"); 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) { if let Err(error) = std::os::unix::fs::symlink(&exec_path, &target_link_path) {
match error.kind() { match error.kind() {
ErrorKind::PermissionDenied => { ErrorKind::PermissionDenied if prompt_when_necessary => {
if prompt_when_necessary {
warn!("target link file can't be accessed with current permissions, requesting elevated ones through AppleScript."); 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());
}
}
}
Ok(())
}
fn create_dir_and_link_with_applescript(exec_path: &Path, target_link_path: &Path) -> Result<()> {
let params = format!( let params = format!(
r##"do shell script "mkdir -p /usr/local/bin && ln -sf '{}' '{}'" with administrator privileges"##, r##"do shell script "mkdir -p /usr/local/bin && ln -sf '{}' '{}'" with administrator privileges"##,
exec_path.to_string_lossy(), exec_path.to_string_lossy(),
@ -55,18 +93,10 @@ pub fn add_espanso_to_path(prompt_when_necessary: bool) -> Result<()> {
.spawn()?; .spawn()?;
let result = child.wait()?; let result = child.wait()?;
if !result.success() { if !result.success() {
return Err(PathError::ElevationRequestFailure.into()); return Err(PathError::ElevationRequestFailure.into());
} }
} else {
return Err(PathError::SymlinkError(error).into());
}
}
_other_error => {
return Err(PathError::SymlinkError(error).into());
}
}
}
Ok(()) Ok(())
} }
@ -112,9 +142,6 @@ pub fn remove_espanso_from_path(prompt_when_necessary: bool) -> Result<()> {
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum PathError { pub enum PathError {
#[error("/usr/local/bin directory doesn't exist")]
UsrLocalBinDirDoesNotExist,
#[error("symlink error: `{0}`")] #[error("symlink error: `{0}`")]
SymlinkError(std::io::Error), SymlinkError(std::io::Error),

View File

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