diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..601b033
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,84 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2020 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 serde::Serialize;
+use crate::config::ConfigSet;
+use crate::matcher::{Match, MatchContentType};
+
+pub fn list_matches(config_set: ConfigSet, onlytriggers: bool) {
+ let matches = filter_matches(config_set);
+
+ for m in matches {
+ for trigger in m.triggers.iter() {
+ if onlytriggers {
+ println!("{}", trigger);
+ }else {
+ match m.content {
+ MatchContentType::Text(ref text) => {
+ println!("{} - {}", trigger, text.replace)
+ },
+ MatchContentType::Image(_) => {
+ // Skip image matches for now
+ },
+ }
+ }
+ }
+ }
+}
+
+#[derive(Debug, Serialize)]
+struct JsonMatchEntry {
+ triggers: Vec,
+ replace: String,
+}
+
+pub fn list_matches_as_json(config_set: ConfigSet) {
+ let matches = filter_matches(config_set);
+
+ let mut entries = Vec::new();
+
+ for m in matches {
+ match m.content {
+ MatchContentType::Text(ref text) => {
+ entries.push(JsonMatchEntry {
+ triggers: m.triggers,
+ replace: text.replace.clone(),
+ })
+ },
+ MatchContentType::Image(_) => {
+ // Skip image matches for now
+ },
+ }
+ }
+
+ let output = serde_json::to_string(&entries);
+
+ println!("{}", output.unwrap_or_default())
+}
+
+fn filter_matches(config_set: ConfigSet) -> Vec {
+ let mut output = Vec::new();
+ output.extend(config_set.default.matches);
+
+ // TODO: consider specific matches by class, title or exe path
+// for specific in config_set.specific {
+// output.extend(specific.matches)
+// }
+ output
+}
\ No newline at end of file
diff --git a/src/engine.rs b/src/engine.rs
index f349847..2a4d060 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -132,22 +132,20 @@ impl<
None
}
}
-}
-lazy_static! {
- static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P\\w+)\\s*\\}\\}").unwrap();
-}
+ fn find_match_by_trigger(&self, trigger: &str) -> Option {
+ let config = self.config_manager.active_config();
-impl<
- 'a,
- S: KeyboardManager,
- C: ClipboardManager,
- M: ConfigManager<'a>,
- U: UIManager,
- R: Renderer,
- > MatchReceiver for Engine<'a, S, C, M, U, R>
-{
- fn on_match(&self, m: &Match, trailing_separator: Option, trigger_offset: usize) {
+ if let Some(m) = config.matches.iter().find(|m|
+ m.triggers.iter().any(|t| t == trigger)
+ ) {
+ Some(m.clone())
+ }else{
+ None
+ }
+ }
+
+ fn inject_match(&self, m: &Match, trailing_separator: Option, trigger_offset: usize, skip_delete: bool) {
let config = self.config_manager.active_config();
if !config.enable_active {
@@ -163,7 +161,9 @@ impl<
m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator
};
- self.keyboard_manager.delete_string(&config, char_count);
+ if !skip_delete {
+ self.keyboard_manager.delete_string(&config, char_count);
+ }
let mut previous_clipboard_content: Option = None;
@@ -287,6 +287,24 @@ impl<
// Re-allow espanso to interpret actions
self.is_injecting.store(false, Release);
}
+}
+
+lazy_static! {
+ static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P\\w+)\\s*\\}\\}").unwrap();
+}
+
+impl<
+ 'a,
+ S: KeyboardManager,
+ C: ClipboardManager,
+ M: ConfigManager<'a>,
+ U: UIManager,
+ R: Renderer,
+ > MatchReceiver for Engine<'a, S, C, M, U, R>
+{
+ fn on_match(&self, m: &Match, trailing_separator: Option, trigger_offset: usize) {
+ self.inject_match(m, trailing_separator, trigger_offset, false);
+ }
fn on_enable_update(&self, status: bool) {
let message = if status {
@@ -432,6 +450,17 @@ impl<
self.ui_manager.notify(&message);
}
}
+ SystemEvent::Trigger(trigger) => {
+ let m = self.find_match_by_trigger(&trigger);
+ match m {
+ Some(m) => {
+ self.inject_match(&m, None, 0, true);
+ },
+ None => {
+ warn!("No match found with trigger: {}", trigger)
+ },
+ }
+ }
}
}
}
diff --git a/src/event/mod.rs b/src/event/mod.rs
index 600c5b6..a85f317 100644
--- a/src/event/mod.rs
+++ b/src/event/mod.rs
@@ -130,6 +130,9 @@ pub enum SystemEvent {
// Notification
NotifyRequest(String),
+
+ // Trigger an expansion from IPC
+ Trigger(String),
}
// Receivers
diff --git a/src/main.rs b/src/main.rs
index 90cdbcd..f7b3b18 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -69,6 +69,7 @@ mod render;
mod sysdaemon;
mod system;
mod ui;
+mod cli;
mod utils;
const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -158,6 +159,32 @@ fn main() {
.subcommand(SubCommand::with_name("default")
.about("Print the default configuration file path."))
)
+ .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)
+ )
+ )
+ .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")
+ )
+ )
+ )
// Package manager
.subcommand(SubCommand::with_name("package")
.about("Espanso package manager commands")
@@ -274,6 +301,11 @@ fn main() {
return;
}
+ if let Some(matches) = matches.subcommand_matches("match") {
+ match_main(config_set, matches);
+ return;
+ }
+
if let Some(matches) = matches.subcommand_matches("package") {
if let Some(matches) = matches.subcommand_matches("install") {
install_main(config_set, matches);
@@ -1229,6 +1261,31 @@ fn path_main(_config_set: ConfigSet, matches: &ArgMatches) {
}
}
+
+fn match_main(config_set: ConfigSet, matches: &ArgMatches) {
+ if let Some(matches) = matches.subcommand_matches("list") {
+ let json = matches.is_present("json");
+ let onlytriggers = matches.is_present("onlytriggers");
+
+ if !json {
+ crate::cli::list_matches(config_set, onlytriggers);
+ }else{
+ crate::cli::list_matches_as_json(config_set);
+ }
+ }else if let Some(matches) = matches.subcommand_matches("exec") {
+ let trigger = matches.value_of("trigger").unwrap_or_else(|| {
+ eprintln!("missing trigger");
+ exit(1);
+ });
+
+ send_command_or_warn(
+ Service::Worker,
+ config_set.default.clone(),
+ IPCCommand::trigger(trigger),
+ );
+ }
+}
+
fn edit_main(matches: &ArgMatches) {
// Determine which is the file to edit
let config = matches.value_of("config").unwrap_or("default");
diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs
index dae371b..d719dac 100644
--- a/src/protocol/mod.rs
+++ b/src/protocol/mod.rs
@@ -67,6 +67,9 @@ impl IPCCommand {
"notify" => Some(Event::System(SystemEvent::NotifyRequest(
self.payload.clone(),
))),
+ "trigger" => Some(Event::System(SystemEvent::Trigger(
+ self.payload.clone(),
+ ))),
_ => None,
}
}
@@ -101,6 +104,10 @@ impl IPCCommand {
id: "notify".to_owned(),
payload: message,
}),
+ Event::System(SystemEvent::Trigger(trigger)) => Some(IPCCommand {
+ id: "trigger".to_owned(),
+ payload: trigger,
+ }),
_ => None,
}
}
@@ -125,6 +132,13 @@ impl IPCCommand {
payload: "".to_owned(),
}
}
+
+ pub fn trigger(trigger: &str) -> IPCCommand {
+ Self {
+ id: "trigger".to_owned(),
+ payload: trigger.to_owned(),
+ }
+ }
}
fn process_event(event_channel: &Sender, stream: Result) {