From 382f708a02bd96d4b5cdc587d58918fcf09339a4 Mon Sep 17 00:00:00 2001
From: Federico Terzi
Date: Sat, 30 Oct 2021 15:39:10 +0200
Subject: [PATCH 01/29] chore(misc): version bump
---
Cargo.lock | 2 +-
espanso/Cargo.toml | 2 +-
snapcraft.yaml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 716e52c..9572b17 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -572,7 +572,7 @@ dependencies = [
[[package]]
name = "espanso"
-version = "2.0.4-alpha"
+version = "2.0.5-alpha"
dependencies = [
"anyhow",
"caps",
diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml
index a31cae3..a80bffe 100644
--- a/espanso/Cargo.toml
+++ b/espanso/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "espanso"
-version = "2.0.4-alpha"
+version = "2.0.5-alpha"
authors = ["Federico Terzi "]
license = "GPL-3.0"
description = "Cross-platform Text Expander written in Rust"
diff --git a/snapcraft.yaml b/snapcraft.yaml
index 92a5f11..4141d80 100644
--- a/snapcraft.yaml
+++ b/snapcraft.yaml
@@ -1,5 +1,5 @@
name: espanso
-version: 2.0.4-alpha
+version: 2.0.5-alpha
summary: A Cross-platform Text Expander written in Rust
description: |
espanso is a Cross-platform, Text Expander written in Rust.
From 119d537fb7772c9e8d358fb6789152981fdbf7ca Mon Sep 17 00:00:00 2001
From: Federico Terzi
Date: Sat, 30 Oct 2021 20:46:25 +0200
Subject: [PATCH 02/29] feat(core): implement 'match list' command. Fix #786
---
espanso/src/cli/match_cli/list.rs | 102 ++++++++++++++++++++++++++++++
espanso/src/cli/match_cli/mod.rs | 51 +++++++++++++++
espanso/src/cli/mod.rs | 1 +
espanso/src/main.rs | 85 +++++++++++++++----------
4 files changed, 206 insertions(+), 33 deletions(-)
create mode 100644 espanso/src/cli/match_cli/list.rs
create mode 100644 espanso/src/cli/match_cli/mod.rs
diff --git a/espanso/src/cli/match_cli/list.rs b/espanso/src/cli/match_cli/list.rs
new file mode 100644
index 0000000..53614de
--- /dev/null
+++ b/espanso/src/cli/match_cli/list.rs
@@ -0,0 +1,102 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+use anyhow::Result;
+use clap::ArgMatches;
+use espanso_config::{
+ config::{AppProperties, ConfigStore},
+ matches::{store::MatchStore, Match, MatchCause},
+};
+use serde::Serialize;
+
+pub fn list_main(
+ cli_args: &ArgMatches,
+ config_store: Box,
+ match_store: Box,
+) -> Result<()> {
+ let only_triggers = cli_args.is_present("onlytriggers");
+ let preserve_newlines = cli_args.is_present("preservenewlines");
+
+ let class = cli_args.value_of("class");
+ let title = cli_args.value_of("title");
+ let exec = cli_args.value_of("exec");
+
+ let config = config_store.active(&AppProperties { title, class, exec });
+ let match_set = match_store.query(config.match_paths());
+
+ if cli_args.is_present("json") {
+ print_matches_as_json(&match_set.matches)?;
+ } else {
+ print_matches_as_plain(&match_set.matches, only_triggers, preserve_newlines)
+ }
+
+ Ok(())
+}
+
+pub fn print_matches_as_plain(match_list: &[&Match], only_triggers: bool, preserve_newlines: bool) {
+ for m in match_list {
+ let triggers = match &m.cause {
+ MatchCause::None => vec!["(none)".to_string()],
+ MatchCause::Trigger(trigger_cause) => trigger_cause.triggers.clone(),
+ MatchCause::Regex(regex_cause) => vec![regex_cause.regex.clone()],
+ };
+
+ for trigger in triggers {
+ if only_triggers {
+ println!("{}", trigger);
+ } else {
+ let description = m.description();
+
+ if preserve_newlines {
+ println!("{} - {}", trigger, description)
+ } else {
+ println!("{} - {}", trigger, description.replace('\n', " "))
+ }
+ }
+ }
+ }
+}
+
+#[derive(Debug, Serialize)]
+struct JsonMatchEntry {
+ triggers: Vec,
+ replace: String,
+}
+
+pub fn print_matches_as_json(match_list: &[&Match]) -> Result<()> {
+ let mut entries = Vec::new();
+ for m in match_list {
+ let triggers = match &m.cause {
+ MatchCause::None => vec!["(none)".to_string()],
+ MatchCause::Trigger(trigger_cause) => trigger_cause.triggers.clone(),
+ MatchCause::Regex(regex_cause) => vec![regex_cause.regex.clone()],
+ };
+
+ entries.push(JsonMatchEntry {
+ triggers,
+ replace: m.description().to_string(),
+ })
+ }
+
+ let json = serde_json::to_string_pretty(&entries)?;
+
+ println!("{}", json);
+
+ Ok(())
+}
diff --git a/espanso/src/cli/match_cli/mod.rs b/espanso/src/cli/match_cli/mod.rs
new file mode 100644
index 0000000..aa458b8
--- /dev/null
+++ b/espanso/src/cli/match_cli/mod.rs
@@ -0,0 +1,51 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+use super::{CliModule, CliModuleArgs};
+
+mod list;
+
+pub fn new() -> CliModule {
+ CliModule {
+ 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");
+
+ 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 {
+ eprintln!("Invalid use, please run 'espanso match --help' to get more information.");
+ return 1;
+ }
+
+ 0
+}
diff --git a/espanso/src/cli/mod.rs b/espanso/src/cli/mod.rs
index 2cdba94..6c3fb99 100644
--- a/espanso/src/cli/mod.rs
+++ b/espanso/src/cli/mod.rs
@@ -28,6 +28,7 @@ pub mod edit;
pub mod env_path;
pub mod launcher;
pub mod log;
+pub mod match_cli;
pub mod migrate;
pub mod modulo;
pub mod package;
diff --git a/espanso/src/main.rs b/espanso/src/main.rs
index d0897ed..f5c9f0b 100644
--- a/espanso/src/main.rs
+++ b/espanso/src/main.rs
@@ -71,6 +71,7 @@ lazy_static! {
cli::service::new(),
cli::workaround::new(),
cli::package::new(),
+ cli::match_cli::new(),
];
static ref ALIASES: Vec = vec![
CliAlias {
@@ -353,39 +354,57 @@ For example, specifying 'email' is equivalent to 'match/email.yml'."#))
.subcommand(restart_subcommand)
.subcommand(stop_subcommand)
.subcommand(status_subcommand)
- // .subcommand(SubCommand::with_name("match")
- // .about("List and execute matches from the CLI")
- // .subcommand(SubCommand::with_name("list")
- // .about("Print all matches to standard output")
- // .arg(Arg::with_name("json")
- // .short("j")
- // .long("json")
- // .help("Return the matches as json")
- // .required(false)
- // .takes_value(false)
- // )
- // .arg(Arg::with_name("onlytriggers")
- // .short("t")
- // .long("onlytriggers")
- // .help("Print only triggers without replacement")
- // .required(false)
- // .takes_value(false)
- // )
- // .arg(Arg::with_name("preservenewlines")
- // .short("n")
- // .long("preservenewlines")
- // .help("Preserve newlines when printing replacements")
- // .required(false)
- // .takes_value(false)
- // )
- // )
- // .subcommand(SubCommand::with_name("exec")
- // .about("Triggers the expansion of the given match")
- // .arg(Arg::with_name("trigger")
- // .help("The trigger of the match to be expanded")
- // )
- // )
- // )
+ .subcommand(SubCommand::with_name("match")
+ .about("List and execute matches from the CLI")
+ .subcommand(SubCommand::with_name("list")
+ .about("Print matches to standard output")
+ .arg(Arg::with_name("json")
+ .short("j")
+ .long("json")
+ .help("Output matches to the JSON format")
+ .required(false)
+ .takes_value(false)
+ )
+ .arg(Arg::with_name("onlytriggers")
+ .short("t")
+ .long("only-triggers")
+ .help("Print only triggers without replacement")
+ .required(false)
+ .takes_value(false)
+ )
+ .arg(Arg::with_name("preservenewlines")
+ .short("n")
+ .long("preserve-newlines")
+ .help("Preserve newlines when printing replacements. Does nothing when using JSON format.")
+ .required(false)
+ .takes_value(false)
+ )
+ .arg(Arg::with_name("class")
+ .long("class")
+ .help("Only return matches that would be active with the given class. This is relevant if you want to list matches only active inside an app-specific config.")
+ .required(false)
+ .takes_value(true)
+ )
+ .arg(Arg::with_name("title")
+ .long("title")
+ .help("Only return matches that would be active with the given title. This is relevant if you want to list matches only active inside an app-specific config.")
+ .required(false)
+ .takes_value(true)
+ )
+ .arg(Arg::with_name("exec")
+ .long("exec")
+ .help("Only return matches that would be active with the given exec. This is relevant if you want to list matches only active inside an app-specific config.")
+ .required(false)
+ .takes_value(true)
+ )
+ )
+ // .subcommand(SubCommand::with_name("exec")
+ // .about("Triggers the expansion of the given match")
+ // .arg(Arg::with_name("trigger")
+ // .help("The trigger of the match to be expanded")
+ // )
+ // )
+ )
.subcommand(
SubCommand::with_name("package")
.about("package-management commands")
From b38623376e84627250f1ac940ea011a2c49168f1 Mon Sep 17 00:00:00 2001
From: Federico Terzi
Date: Sun, 31 Oct 2021 15:43:50 +0100
Subject: [PATCH 03/29] feat(engine): add components to support ipc match exec
---
espanso-engine/src/event/external.rs | 26 ++++++
espanso-engine/src/event/mod.rs | 4 +
espanso-engine/src/process/default.rs | 10 ++-
.../src/process/middleware/match_exec.rs | 80 +++++++++++++++++++
espanso-engine/src/process/middleware/mod.rs | 1 +
espanso-engine/src/process/mod.rs | 3 +
6 files changed, 120 insertions(+), 4 deletions(-)
create mode 100644 espanso-engine/src/event/external.rs
create mode 100644 espanso-engine/src/process/middleware/match_exec.rs
diff --git a/espanso-engine/src/event/external.rs b/espanso-engine/src/event/external.rs
new file mode 100644
index 0000000..91ae9d1
--- /dev/null
+++ b/espanso-engine/src/event/external.rs
@@ -0,0 +1,26 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct MatchExecRequestEvent {
+ pub trigger: Option,
+ pub args: HashMap,
+}
diff --git a/espanso-engine/src/event/mod.rs b/espanso-engine/src/event/mod.rs
index 8769e0e..43397e7 100644
--- a/espanso-engine/src/event/mod.rs
+++ b/espanso-engine/src/event/mod.rs
@@ -18,6 +18,7 @@
*/
pub mod effect;
+pub mod external;
pub mod input;
pub mod internal;
pub mod ui;
@@ -60,6 +61,9 @@ pub enum EventType {
TrayIconClicked,
ContextMenuClicked(input::ContextMenuClickedEvent),
+ // External requests
+ MatchExecRequest(external::MatchExecRequestEvent),
+
// Internal
MatchesDetected(internal::MatchesDetectedEvent),
MatchSelected(internal::MatchSelectedEvent),
diff --git a/espanso-engine/src/process/default.rs b/espanso-engine/src/process/default.rs
index b0f85b8..0fc652f 100644
--- a/espanso-engine/src/process/default.rs
+++ b/espanso-engine/src/process/default.rs
@@ -33,16 +33,16 @@ use super::{
render::RenderMiddleware,
},
DisableOptions, EnabledStatusProvider, MatchFilter, MatchInfoProvider, MatchProvider,
- MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware, ModifierStateProvider,
- Multiplexer, PathProvider, Processor, Renderer, UndoEnabledProvider,
+ MatchResolver, MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware,
+ ModifierStateProvider, Multiplexer, PathProvider, Processor, Renderer, UndoEnabledProvider,
};
use crate::{
event::{Event, EventType},
process::middleware::{
context_menu::ContextMenuMiddleware, disable::DisableMiddleware, exit::ExitMiddleware,
hotkey::HotKeyMiddleware, icon_status::IconStatusMiddleware,
- image_resolve::ImageResolverMiddleware, search::SearchMiddleware, suppress::SuppressMiddleware,
- undo::UndoMiddleware,
+ image_resolve::ImageResolverMiddleware, match_exec::MatchExecRequestMiddleware,
+ search::SearchMiddleware, suppress::SuppressMiddleware, undo::UndoMiddleware,
},
};
use std::collections::VecDeque;
@@ -70,6 +70,7 @@ impl<'a> DefaultProcessor<'a> {
undo_enabled_provider: &'a dyn UndoEnabledProvider,
enabled_status_provider: &'a dyn EnabledStatusProvider,
modifier_state_provider: &'a dyn ModifierStateProvider,
+ match_resolver: &'a dyn MatchResolver,
) -> DefaultProcessor<'a> {
Self {
event_queue: VecDeque::new(),
@@ -82,6 +83,7 @@ impl<'a> DefaultProcessor<'a> {
matcher_options_provider,
modifier_state_provider,
)),
+ Box::new(MatchExecRequestMiddleware::new(match_resolver)),
Box::new(SuppressMiddleware::new(enabled_status_provider)),
Box::new(ContextMenuMiddleware::new()),
Box::new(HotKeyMiddleware::new()),
diff --git a/espanso-engine/src/process/middleware/match_exec.rs b/espanso-engine/src/process/middleware/match_exec.rs
new file mode 100644
index 0000000..5b8a669
--- /dev/null
+++ b/espanso-engine/src/process/middleware/match_exec.rs
@@ -0,0 +1,80 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+use log::warn;
+
+use super::super::Middleware;
+use crate::event::{
+ internal::{DetectedMatch, MatchesDetectedEvent},
+ Event, EventType,
+};
+
+pub trait MatchResolver {
+ fn find_matches_from_trigger(&self, trigger: &str) -> Vec;
+}
+
+pub struct MatchExecRequestMiddleware<'a> {
+ match_resolver: &'a dyn MatchResolver,
+}
+
+impl<'a> MatchExecRequestMiddleware<'a> {
+ pub fn new(match_resolver: &'a dyn MatchResolver) -> Self {
+ Self { match_resolver }
+ }
+}
+
+impl<'a> Middleware for MatchExecRequestMiddleware<'a> {
+ fn name(&self) -> &'static str {
+ "match_exec_request"
+ }
+
+ fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
+ if let EventType::MatchExecRequest(m_event) = &event.etype {
+ let mut matches = if let Some(trigger) = &m_event.trigger {
+ self.match_resolver.find_matches_from_trigger(trigger)
+ } else {
+ Vec::new()
+ };
+
+ // Inject the request args into the detected matches
+ matches.iter_mut().for_each(|m| {
+ for (key, value) in &m_event.args {
+ m.args.insert(key.to_string(), value.to_string());
+ }
+ });
+
+ if matches.is_empty() {
+ warn!("received match exec request, but no matches have been found for the given query.");
+ return Event::caused_by(event.source_id, EventType::NOOP);
+ }
+
+ return Event::caused_by(
+ event.source_id,
+ EventType::MatchesDetected(MatchesDetectedEvent {
+ matches,
+ is_search: false,
+ }),
+ );
+ }
+
+ event
+ }
+}
+
+// TODO: test
diff --git a/espanso-engine/src/process/middleware/mod.rs b/espanso-engine/src/process/middleware/mod.rs
index 9ba98b3..5c72d6e 100644
--- a/espanso-engine/src/process/middleware/mod.rs
+++ b/espanso-engine/src/process/middleware/mod.rs
@@ -29,6 +29,7 @@ pub mod hotkey;
pub mod icon_status;
pub mod image_resolve;
pub mod markdown;
+pub mod match_exec;
pub mod match_select;
pub mod matcher;
pub mod multiplex;
diff --git a/espanso-engine/src/process/mod.rs b/espanso-engine/src/process/mod.rs
index 1e295f9..eb42768 100644
--- a/espanso-engine/src/process/mod.rs
+++ b/espanso-engine/src/process/mod.rs
@@ -37,6 +37,7 @@ pub use middleware::action::{EventSequenceProvider, MatchInfoProvider};
pub use middleware::delay_modifiers::ModifierStatusProvider;
pub use middleware::disable::DisableOptions;
pub use middleware::image_resolve::PathProvider;
+pub use middleware::match_exec::MatchResolver;
pub use middleware::match_select::{MatchFilter, MatchSelector};
pub use middleware::matcher::{
MatchResult, Matcher, MatcherEvent, MatcherMiddlewareConfigProvider, ModifierState,
@@ -65,6 +66,7 @@ pub fn default<'a, MatcherState>(
undo_enabled_provider: &'a dyn UndoEnabledProvider,
enabled_status_provider: &'a dyn EnabledStatusProvider,
modifier_state_provider: &'a dyn ModifierStateProvider,
+ match_resolver: &'a dyn MatchResolver,
) -> impl Processor + 'a {
default::DefaultProcessor::new(
matchers,
@@ -82,5 +84,6 @@ pub fn default<'a, MatcherState>(
undo_enabled_provider,
enabled_status_provider,
modifier_state_provider,
+ match_resolver,
)
}
From 8edf998e6038c041b0e92ddf2ec13f7e72b95c3c Mon Sep 17 00:00:00 2001
From: Federico Terzi
Date: Sun, 31 Oct 2021 15:44:25 +0100
Subject: [PATCH 04/29] feat(core): implement 'match exec' subcommand. Fix
#883, Fix #780
---
espanso/src/cli/match_cli/exec.rs | 71 ++++++++++++++++++
espanso/src/cli/match_cli/mod.rs | 10 ++-
espanso/src/cli/worker/engine/funnel/ipc.rs | 72 +++++++++++++++++++
espanso/src/cli/worker/engine/funnel/mod.rs | 1 +
.../cli/worker/engine/funnel/secure_input.rs | 37 ++++------
espanso/src/cli/worker/engine/mod.rs | 17 +++--
espanso/src/cli/worker/ipc.rs | 23 +++++-
espanso/src/cli/worker/match_cache.rs | 50 ++++++++++++-
espanso/src/cli/worker/mod.rs | 4 +-
espanso/src/ipc.rs | 10 ++-
espanso/src/main.rs | 24 +++++--
11 files changed, 275 insertions(+), 44 deletions(-)
create mode 100644 espanso/src/cli/match_cli/exec.rs
create mode 100644 espanso/src/cli/worker/engine/funnel/ipc.rs
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")
From 4d3b1a5a59aafd683e15bff24c5c0a5311e8aff6 Mon Sep 17 00:00:00 2001
From: Federico Terzi
Date: Sun, 31 Oct 2021 16:06:44 +0100
Subject: [PATCH 05/29] fix(engine): avoid stripping away paragraph tags with
markdown when multiple are present. Fix #811
---
.../src/process/middleware/markdown.rs | 51 +++++++++++++++----
1 file changed, 41 insertions(+), 10 deletions(-)
diff --git a/espanso-engine/src/process/middleware/markdown.rs b/espanso-engine/src/process/middleware/markdown.rs
index 7deec85..ff1bea2 100644
--- a/espanso-engine/src/process/middleware/markdown.rs
+++ b/espanso-engine/src/process/middleware/markdown.rs
@@ -45,15 +45,8 @@ impl Middleware for MarkdownMiddleware {
// See also: https://github.com/federico-terzi/espanso/issues/759
let html = std::panic::catch_unwind(|| markdown::to_html(&m_event.markdown));
if let Ok(html) = html {
- let mut html = html.trim();
-
- // Remove the surrounding paragraph
- if html.starts_with("
") {
- html = html.trim_start_matches("
");
- }
- if html.ends_with("
") {
- html = html.trim_end_matches("
");
- }
+ let html = html.trim();
+ let html = remove_paragraph_tag_if_single_occurrence(html);
return Event::caused_by(
event.source_id,
@@ -72,4 +65,42 @@ impl Middleware for MarkdownMiddleware {
}
}
-// TODO: test
+// If the match is composed of a single paragraph, we remove the tag to avoid
+// a forced "newline" on some editors. In other words, we assume that if the snippet
+// is composed of a single paragraph, then it should be inlined.
+// On the other hand, if the snippet is composed of multiple paragraphs, then we
+// avoid removing the paragraph to prevent HTML corruption.
+// See: https://github.com/federico-terzi/espanso/issues/811
+fn remove_paragraph_tag_if_single_occurrence(html: &str) -> &str {
+ let paragraph_count = html.matches("
").count();
+ if paragraph_count <= 1 {
+ let mut new_html = html;
+ if new_html.starts_with("