Add CLI option to list matches and exec a trigger. Fix #263

This commit is contained in:
Federico Terzi 2020-06-22 21:21:35 +02:00
parent 6766d91af3
commit 968ef578c1
5 changed files with 202 additions and 15 deletions

84
src/cli.rs Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String>,
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<Match> {
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
}

View File

@ -132,22 +132,20 @@ impl<
None
}
}
}
lazy_static! {
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
}
fn find_match_by_trigger(&self, trigger: &str) -> Option<Match> {
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<char>, 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<char>, 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
};
if !skip_delete {
self.keyboard_manager.delete_string(&config, char_count);
}
let mut previous_clipboard_content: Option<String> = 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<name>\\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<char>, 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)
},
}
}
}
}
}

View File

@ -130,6 +130,9 @@ pub enum SystemEvent {
// Notification
NotifyRequest(String),
// Trigger an expansion from IPC
Trigger(String),
}
// Receivers

View File

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

View File

@ -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<R: Read, E: Error>(event_channel: &Sender<Event>, stream: Result<R, E>) {