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