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")