commit
3f5b0b04f2
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
26
espanso-engine/src/event/external.rs
Normal file
26
espanso-engine/src/event/external.rs
Normal 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>,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)),
|
)),
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
80
espanso-engine/src/process/middleware/match_exec.rs
Normal file
80
espanso-engine/src/process/middleware/match_exec.rs
Normal 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
|
|
@ -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;
|
||||||
|
|
57
espanso-engine/src/process/middleware/notification.rs
Normal file
57
espanso-engine/src/process/middleware/notification.rs
Normal 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
|
|
@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,15 +272,40 @@ 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 {
|
||||||
|
event.Skip();
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
event.Skip();
|
event.Skip();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 );
|
||||||
|
|
|
@ -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
72
espanso/src/cli/cmd.rs
Normal 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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
71
espanso/src/cli/match_cli/exec.rs
Normal file
71
espanso/src/cli/match_cli/exec.rs
Normal 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(())
|
||||||
|
}
|
102
espanso/src/cli/match_cli/list.rs
Normal file
102
espanso/src/cli/match_cli/list.rs
Normal 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(())
|
||||||
|
}
|
57
espanso/src/cli/match_cli/mod.rs
Normal file
57
espanso/src/cli/match_cli/mod.rs
Normal 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
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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/")
|
||||||
|
}
|
||||||
|
|
79
espanso/src/cli/worker/engine/funnel/ipc.rs
Normal file
79
espanso/src/cli/worker/engine/funnel/ipc.rs
Normal 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
|
||||||
|
)
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
¬ification_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);
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>> {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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),
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user