diff --git a/espanso/src/cli/match_cli/exec.rs b/espanso/src/cli/match_cli/exec.rs
new file mode 100644
index 0000000..c2d859e
--- /dev/null
+++ b/espanso/src/cli/match_cli/exec.rs
@@ -0,0 +1,71 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+use std::collections::HashMap;
+
+use anyhow::{bail, Context, Result};
+use clap::ArgMatches;
+use espanso_ipc::IPCClient;
+use espanso_path::Paths;
+
+use crate::{
+ ipc::{create_ipc_client_to_worker, IPCEvent, RequestMatchExpansionPayload},
+ lock::acquire_worker_lock,
+};
+
+pub fn exec_main(cli_args: &ArgMatches, paths: &Paths) -> Result<()> {
+ let trigger = cli_args.value_of("trigger");
+ let args = cli_args.values_of("arg");
+
+ if trigger.is_none() || trigger.map(str::is_empty).unwrap_or(false) {
+ bail!("You need to specify the --trigger 'trigger' option. Run `espanso match exec --help` for more information.");
+ }
+
+ if acquire_worker_lock(&paths.runtime).is_some() {
+ bail!("Worker process is not running, please start Espanso first.")
+ }
+
+ let mut client = create_ipc_client_to_worker(&paths.runtime)?;
+
+ let mut match_args = HashMap::new();
+ if let Some(args) = args {
+ args.for_each(|arg| {
+ let tokens = arg.split_once('=');
+ if let Some((key, value)) = tokens {
+ match_args.insert(key.to_string(), value.to_string());
+ } else {
+ eprintln!(
+ "invalid format for argument '{}', you should follow the 'name=value' format",
+ arg
+ );
+ }
+ })
+ }
+
+ client
+ .send_async(IPCEvent::RequestMatchExpansion(
+ RequestMatchExpansionPayload {
+ trigger: trigger.map(String::from),
+ args: match_args,
+ },
+ ))
+ .context("unable to send payload to worker process")?;
+
+ Ok(())
+}
diff --git a/espanso/src/cli/match_cli/mod.rs b/espanso/src/cli/match_cli/mod.rs
index aa458b8..781bc32 100644
--- a/espanso/src/cli/match_cli/mod.rs
+++ b/espanso/src/cli/match_cli/mod.rs
@@ -19,10 +19,12 @@
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,
@@ -34,14 +36,18 @@ 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") {
- todo!();
+ } 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;
diff --git a/espanso/src/cli/worker/engine/funnel/ipc.rs b/espanso/src/cli/worker/engine/funnel/ipc.rs
new file mode 100644
index 0000000..3efc97f
--- /dev/null
+++ b/espanso/src/cli/worker/engine/funnel/ipc.rs
@@ -0,0 +1,72 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+use crossbeam::channel::{Receiver, Select, SelectedOperation};
+
+use espanso_engine::{
+ event::{Event, EventType},
+ funnel,
+};
+use log::warn;
+
+use super::sequencer::Sequencer;
+
+pub struct IpcEventSource<'a> {
+ pub ipc_event_receiver: Receiver,
+ pub sequencer: &'a Sequencer,
+}
+
+impl<'a> IpcEventSource<'a> {
+ pub fn new(ipc_event_receiver: Receiver, sequencer: &'a Sequencer) -> Self {
+ IpcEventSource {
+ ipc_event_receiver,
+ sequencer,
+ }
+ }
+}
+
+impl<'a> funnel::Source<'a> for IpcEventSource<'a> {
+ fn register(&'a self, select: &mut Select<'a>) -> usize {
+ select.recv(&self.ipc_event_receiver)
+ }
+
+ fn receive(&self, op: SelectedOperation) -> Option {
+ let ipc_event = op
+ .recv(&self.ipc_event_receiver)
+ .expect("unable to select data from IpcEventSource receiver");
+
+ // Execute only events that have been whitelisted
+ if !is_event_type_allowed(&ipc_event) {
+ warn!(
+ "received black-listed event from IPC stream, blocking it: {:?}",
+ ipc_event
+ );
+ return None;
+ }
+
+ Some(Event {
+ source_id: self.sequencer.next_id(),
+ etype: ipc_event,
+ })
+ }
+}
+
+fn is_event_type_allowed(event: &EventType) -> bool {
+ matches!(event, EventType::MatchExecRequest(_))
+}
diff --git a/espanso/src/cli/worker/engine/funnel/mod.rs b/espanso/src/cli/worker/engine/funnel/mod.rs
index f1d699b..c0706fc 100644
--- a/espanso/src/cli/worker/engine/funnel/mod.rs
+++ b/espanso/src/cli/worker/engine/funnel/mod.rs
@@ -35,6 +35,7 @@ use self::{
pub mod detect;
pub mod exit;
+pub mod ipc;
pub mod key_state;
pub mod modifier;
pub mod secure_input;
diff --git a/espanso/src/cli/worker/engine/funnel/secure_input.rs b/espanso/src/cli/worker/engine/funnel/secure_input.rs
index 3e4da03..ad9437d 100644
--- a/espanso/src/cli/worker/engine/funnel/secure_input.rs
+++ b/espanso/src/cli/worker/engine/funnel/secure_input.rs
@@ -43,33 +43,22 @@ impl<'a> SecureInputSource<'a> {
impl<'a> funnel::Source<'a> for SecureInputSource<'a> {
fn register(&'a self, select: &mut Select<'a>) -> usize {
- if cfg!(target_os = "macos") {
- select.recv(&self.receiver)
- } else {
- 999999
- }
+ select.recv(&self.receiver)
}
fn receive(&self, op: SelectedOperation) -> Option {
- if cfg!(target_os = "macos") {
- let si_event = op
- .recv(&self.receiver)
- .expect("unable to select data from SecureInputSource receiver");
+ let si_event = op
+ .recv(&self.receiver)
+ .expect("unable to select data from SecureInputSource receiver");
- Some(Event {
- source_id: self.sequencer.next_id(),
- etype: match si_event {
- SecureInputEvent::Disabled => EventType::SecureInputDisabled,
- SecureInputEvent::Enabled { app_name, app_path } => {
- EventType::SecureInputEnabled(SecureInputEnabledEvent { app_name, app_path })
- }
- },
- })
- } else {
- Some(Event {
- source_id: self.sequencer.next_id(),
- etype: EventType::NOOP,
- })
- }
+ Some(Event {
+ source_id: self.sequencer.next_id(),
+ etype: match si_event {
+ SecureInputEvent::Disabled => EventType::SecureInputDisabled,
+ SecureInputEvent::Enabled { app_name, app_path } => {
+ EventType::SecureInputEnabled(SecureInputEnabledEvent { app_name, app_path })
+ }
+ },
+ })
}
}
diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs
index f68309a..6dd1fd3 100644
--- a/espanso/src/cli/worker/engine/mod.rs
+++ b/espanso/src/cli/worker/engine/mod.rs
@@ -23,7 +23,7 @@ use anyhow::Result;
use crossbeam::channel::Receiver;
use espanso_config::{config::ConfigStore, matches::store::MatchStore};
use espanso_detect::SourceCreationOptions;
-use espanso_engine::event::ExitMode;
+use espanso_engine::event::{EventType, ExitMode};
use espanso_inject::{InjectorCreationOptions, KeyboardStateProvider};
use espanso_path::Paths;
use espanso_ui::{event::UIEvent, UIRemote};
@@ -82,6 +82,7 @@ pub fn initialize_and_spawn(
secure_input_receiver: Receiver,
use_evdev_backend: bool,
start_reason: Option,
+ ipc_event_receiver: Receiver,
) -> Result> {
let handle = std::thread::Builder::new()
.name("engine thread".to_string())
@@ -131,17 +132,18 @@ pub fn initialize_and_spawn(
})
.expect("failed to initialize detector module");
let exit_source = super::engine::funnel::exit::ExitSource::new(exit_signal, &sequencer);
+ let ipc_event_source =
+ super::engine::funnel::ipc::IpcEventSource::new(ipc_event_receiver, &sequencer);
let ui_source = super::engine::funnel::ui::UISource::new(ui_event_receiver, &sequencer);
let secure_input_source = super::engine::funnel::secure_input::SecureInputSource::new(
secure_input_receiver,
&sequencer,
);
- let sources: Vec<&dyn espanso_engine::funnel::Source> = vec![
- &detect_source,
- &exit_source,
- &ui_source,
- &secure_input_source,
- ];
+ let mut sources: Vec<&dyn espanso_engine::funnel::Source> =
+ vec![&detect_source, &exit_source, &ui_source, &ipc_event_source];
+ if cfg!(target_os = "macos") {
+ sources.push(&secure_input_source);
+ }
let funnel = espanso_engine::funnel::default(&sources);
let rolling_matcher = RollingMatcherAdapter::new(
@@ -226,6 +228,7 @@ pub fn initialize_and_spawn(
&config_manager,
&config_manager,
&modifier_state_store,
+ &combined_match_cache,
);
let event_injector = EventInjectorAdapter::new(&*injector, &config_manager);
diff --git a/espanso/src/cli/worker/ipc.rs b/espanso/src/cli/worker/ipc.rs
index 07f6177..9c06c03 100644
--- a/espanso/src/cli/worker/ipc.rs
+++ b/espanso/src/cli/worker/ipc.rs
@@ -21,13 +21,17 @@ use std::path::Path;
use anyhow::Result;
use crossbeam::channel::Sender;
-use espanso_engine::event::ExitMode;
+use espanso_engine::event::{external::MatchExecRequestEvent, EventType, ExitMode};
use espanso_ipc::{EventHandlerResponse, IPCServer};
use log::{error, warn};
use crate::ipc::IPCEvent;
-pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender) -> Result<()> {
+pub fn initialize_and_spawn(
+ runtime_dir: &Path,
+ exit_notify: Sender,
+ event_notify: Sender,
+) -> Result<()> {
let server = crate::ipc::create_worker_ipc_server(runtime_dir)?;
std::thread::Builder::new()
@@ -55,6 +59,21 @@ pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender) -
EventHandlerResponse::NoResponse
}
+ IPCEvent::RequestMatchExpansion(payload) => {
+ if let Err(err) =
+ event_notify.send(EventType::MatchExecRequest(MatchExecRequestEvent {
+ trigger: payload.trigger,
+ args: payload.args,
+ }))
+ {
+ error!(
+ "experienced error while sending event signal from worker ipc handler: {}",
+ err
+ );
+ }
+
+ EventHandlerResponse::NoResponse
+ }
#[allow(unreachable_patterns)]
unexpected_event => {
warn!(
diff --git a/espanso/src/cli/worker/match_cache.rs b/espanso/src/cli/worker/match_cache.rs
index fe23b7d..6c2423f 100644
--- a/espanso/src/cli/worker/match_cache.rs
+++ b/espanso/src/cli/worker/match_cache.rs
@@ -21,8 +21,9 @@ use std::collections::HashMap;
use espanso_config::{
config::ConfigStore,
- matches::{store::MatchStore, Match, MatchEffect},
+ matches::{store::MatchStore, Match, MatchCause, MatchEffect},
};
+use espanso_engine::event::internal::DetectedMatch;
use super::{builtin::BuiltInMatch, engine::process::middleware::match_select::MatchSummary};
@@ -164,3 +165,50 @@ impl<'a> espanso_engine::process::MatchProvider for CombinedMatchCache<'a> {
ids
}
}
+
+impl<'a> espanso_engine::process::MatchResolver for CombinedMatchCache<'a> {
+ fn find_matches_from_trigger(&self, trigger: &str) -> Vec {
+ let user_matches: Vec = self
+ .user_match_cache
+ .cache
+ .values()
+ .filter_map(|m| {
+ if let MatchCause::Trigger(trigger_cause) = &m.cause {
+ if trigger_cause.triggers.iter().any(|t| t == trigger) {
+ Some(DetectedMatch {
+ id: m.id,
+ trigger: Some(trigger.to_string()),
+ ..Default::default()
+ })
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ let builtin_matches: Vec = self
+ .builtin_match_cache
+ .values()
+ .filter_map(|m| {
+ if m.triggers.iter().any(|t| t == trigger) {
+ Some(DetectedMatch {
+ id: m.id,
+ trigger: Some(trigger.to_string()),
+ ..Default::default()
+ })
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ let mut matches = Vec::with_capacity(user_matches.len() + builtin_matches.len());
+ matches.extend(user_matches);
+ matches.extend(builtin_matches);
+
+ matches
+ }
+}
diff --git a/espanso/src/cli/worker/mod.rs b/espanso/src/cli/worker/mod.rs
index 473b5f8..f5dd997 100644
--- a/espanso/src/cli/worker/mod.rs
+++ b/espanso/src/cli/worker/mod.rs
@@ -112,6 +112,7 @@ fn worker_main(args: CliModuleArgs) -> i32 {
.expect("unable to initialize UI module");
let (engine_exit_notify, engine_exit_receiver) = unbounded();
+ let (ipc_event_notify, ipc_event_receiver) = unbounded();
let (engine_ui_event_sender, engine_ui_event_receiver) = unbounded();
let (engine_secure_input_sender, engine_secure_input_receiver) = unbounded();
@@ -126,11 +127,12 @@ fn worker_main(args: CliModuleArgs) -> i32 {
engine_secure_input_receiver,
use_evdev_backend,
start_reason,
+ ipc_event_receiver,
)
.expect("unable to initialize engine");
// Setup the IPC server
- ipc::initialize_and_spawn(&paths.runtime, engine_exit_notify.clone())
+ ipc::initialize_and_spawn(&paths.runtime, engine_exit_notify.clone(), ipc_event_notify)
.expect("unable to initialize IPC server");
// If specified, automatically monitor the daemon status and
diff --git a/espanso/src/ipc.rs b/espanso/src/ipc.rs
index 4d157d4..1e1f6b1 100644
--- a/espanso/src/ipc.rs
+++ b/espanso/src/ipc.rs
@@ -20,12 +20,20 @@
use anyhow::Result;
use espanso_ipc::{IPCClient, IPCServer};
use serde::{Deserialize, Serialize};
-use std::path::Path;
+use std::{collections::HashMap, path::Path};
#[derive(Debug, Serialize, Deserialize)]
pub enum IPCEvent {
Exit,
ExitAllProcesses,
+
+ RequestMatchExpansion(RequestMatchExpansionPayload),
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct RequestMatchExpansionPayload {
+ pub trigger: Option,
+ pub args: HashMap,
}
pub fn create_daemon_ipc_server(runtime_dir: &Path) -> Result> {
diff --git a/espanso/src/main.rs b/espanso/src/main.rs
index f5c9f0b..d066ab6 100644
--- a/espanso/src/main.rs
+++ b/espanso/src/main.rs
@@ -398,12 +398,24 @@ For example, specifying 'email' is equivalent to 'match/email.yml'."#))
.takes_value(true)
)
)
- // .subcommand(SubCommand::with_name("exec")
- // .about("Triggers the expansion of the given match")
- // .arg(Arg::with_name("trigger")
- // .help("The trigger of the match to be expanded")
- // )
- // )
+ .subcommand(SubCommand::with_name("exec")
+ .about("Triggers the expansion of a match")
+ .arg(Arg::with_name("trigger")
+ .short("t")
+ .long("trigger")
+ .help("The trigger of the match to be expanded")
+ .required(false)
+ .takes_value(true)
+ )
+ .arg(Arg::with_name("arg")
+ .long("arg")
+ .help("Specify also an argument for the expansion, following the --arg 'name=value' format. You can specify multiple ones.")
+ .required(false)
+ .takes_value(true)
+ .multiple(true)
+ .number_of_values(1)
+ )
+ )
)
.subcommand(
SubCommand::with_name("package")