diff --git a/espanso/src/cli/worker/engine/match_cache.rs b/espanso/src/cli/worker/engine/match_cache.rs index d4f1346..c9a4007 100644 --- a/espanso/src/cli/worker/engine/match_cache.rs +++ b/espanso/src/cli/worker/engine/match_cache.rs @@ -63,6 +63,14 @@ impl<'a> super::render::MatchProvider<'a> for MatchCache<'a> { } } +impl<'a> super::ui::selector::MatchProvider<'a> for MatchCache<'a> { + fn get_matches(&self, ids: &[i32]) -> Vec<&'a Match> { + ids.iter().flat_map(|id| { + self.cache.get(&id).map(|m| *m) + }).collect() + } +} + impl<'a> MatchInfoProvider for MatchCache<'a> { fn get_force_mode(&self, match_id: i32) -> Option { let m = self.cache.get(&match_id)?; diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index aac0cc0..f88bd77 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -55,7 +55,8 @@ pub fn initialize_and_spawn( let match_cache = super::engine::match_cache::MatchCache::load(&*config_store, &*match_store); let modulo_manager = crate::gui::modulo::manager::ModuloManager::new(); - let modulo_form_ui = crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager, icon_paths.form_icon); + let modulo_form_ui = crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager, &icon_paths.form_icon); + let modulo_search_ui = crate::gui::modulo::search::ModuloSearchUI::new(&modulo_manager, &icon_paths.search_icon); let (detect_source, modifier_state_store, sequencer) = super::engine::source::init_and_spawn().expect("failed to initialize detector module"); @@ -74,7 +75,7 @@ pub fn initialize_and_spawn( let matchers: Vec< &dyn crate::engine::process::Matcher, > = vec![&rolling_matcher, ®ex_matcher]; - let selector = MatchSelectorAdapter::new(); + let selector = MatchSelectorAdapter::new(&modulo_search_ui, &match_cache); let multiplexer = super::engine::multiplex::MultiplexAdapter::new(&match_cache); let injector = espanso_inject::get_injector(Default::default()) diff --git a/espanso/src/cli/worker/engine/ui/selector.rs b/espanso/src/cli/worker/engine/ui/selector.rs index d68995d..515a670 100644 --- a/espanso/src/cli/worker/engine/ui/selector.rs +++ b/espanso/src/cli/worker/engine/ui/selector.rs @@ -17,21 +17,69 @@ * along with espanso. If not, see . */ -use crate::engine::process::MatchSelector; +use espanso_config::matches::{Match}; +use log::error; -pub struct MatchSelectorAdapter { - // TODO: pass Modulo search UI manager +use crate::{ + engine::process::MatchSelector, + gui::{SearchItem, SearchUI}, +}; + +const MAX_LABEL_LEN: usize = 100; + +pub trait MatchProvider<'a> { + fn get_matches(&self, ids: &[i32]) -> Vec<&'a Match>; } -impl MatchSelectorAdapter { - pub fn new() -> Self { - Self {} +pub struct MatchSelectorAdapter<'a> { + search_ui: &'a dyn SearchUI, + match_provider: &'a dyn MatchProvider<'a>, +} + +impl<'a> MatchSelectorAdapter<'a> { + pub fn new(search_ui: &'a dyn SearchUI, match_provider: &'a dyn MatchProvider<'a>) -> Self { + Self { + search_ui, + match_provider, + } } } -impl MatchSelector for MatchSelectorAdapter { +impl<'a> MatchSelector for MatchSelectorAdapter<'a> { fn select(&self, matches_ids: &[i32]) -> Option { - // TODO: replace with actual selection - Some(*matches_ids.first().unwrap()) + let matches = self.match_provider.get_matches(&matches_ids); + let search_items: Vec = matches + .iter() + .map(|m| { + let label = m.description(); + let clipped_label = &label[..std::cmp::min(label.len(), MAX_LABEL_LEN)]; + + SearchItem { + id: m.id.to_string(), + label: clipped_label.to_string(), + tag: m.cause_description().map(String::from), + } + }) + .collect(); + + match self.search_ui.show(&search_items) { + Ok(Some(selected_id)) => match selected_id.parse::() { + Ok(id) => Some(id), + Err(err) => { + error!( + "match selector received an invalid id from SearchUI: {}", + err + ); + None + } + }, + Ok(None) => None, + Err(err) => { + error!("SearchUI reported an error: {}", err); + None + } + } } } + +// TODO: test \ No newline at end of file diff --git a/espanso/src/cli/worker/ui/icon.rs b/espanso/src/cli/worker/ui/icon.rs index 0b7a3f1..5483935 100644 --- a/espanso/src/cli/worker/ui/icon.rs +++ b/espanso/src/cli/worker/ui/icon.rs @@ -33,6 +33,7 @@ const WINDOWS_RED_ICO_BINARY: &[u8] = include_bytes!("../../../res/windows/espan #[derive(Debug, Default)] pub struct IconPaths { pub form_icon: Option, + pub search_icon: Option, pub tray_icon_normal: Option, pub tray_icon_disabled: Option, @@ -45,6 +46,7 @@ pub struct IconPaths { pub fn load_icon_paths(runtime_dir: &Path) -> Result { Ok(IconPaths { form_icon: Some(extract_icon(WINDOWS_ICO_BINARY, &runtime_dir.join("form.ico"))?), + search_icon: Some(extract_icon(ICON_BINARY, &runtime_dir.join("search.png"))?), tray_icon_normal: Some(extract_icon(WINDOWS_ICO_BINARY, &runtime_dir.join("normal.ico"))?), tray_icon_disabled: Some(extract_icon(WINDOWS_RED_ICO_BINARY, &runtime_dir.join("disabled.ico"))?), logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("icon.png"))?), @@ -56,6 +58,7 @@ pub fn load_icon_paths(runtime_dir: &Path) -> Result { pub fn load_icon_paths(runtime_dir: &Path) -> Result { Ok(IconPaths { logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("icon.png"))?), + search_icon: Some(extract_icon(ICON_BINARY, &runtime_dir.join("search.png"))?), ..Default::default() }) } diff --git a/espanso/src/gui/mod.rs b/espanso/src/gui/mod.rs index 39a93b5..e108b78 100644 --- a/espanso/src/gui/mod.rs +++ b/espanso/src/gui/mod.rs @@ -24,7 +24,7 @@ use anyhow::Result; pub mod modulo; pub trait SearchUI { - fn show(&self, items: &SearchItem) -> Result>; + fn show(&self, items: &[SearchItem]) -> Result>; } #[derive(Debug)] diff --git a/espanso/src/gui/modulo/form.rs b/espanso/src/gui/modulo/form.rs index 91be259..5dc04a6 100644 --- a/espanso/src/gui/modulo/form.rs +++ b/espanso/src/gui/modulo/form.rs @@ -31,10 +31,10 @@ pub struct ModuloFormUI<'a> { } impl<'a> ModuloFormUI<'a> { - pub fn new(manager: &'a ModuloManager, icon_path: Option) -> Self { + pub fn new(manager: &'a ModuloManager, icon_path: &Option) -> Self { Self { manager, - icon_path: icon_path.map(|path| path.to_string_lossy().to_string()), + icon_path: icon_path.as_ref().map(|path| path.to_string_lossy().to_string()), } } } diff --git a/espanso/src/gui/modulo/mod.rs b/espanso/src/gui/modulo/mod.rs index 8c170f3..296e739 100644 --- a/espanso/src/gui/modulo/mod.rs +++ b/espanso/src/gui/modulo/mod.rs @@ -19,5 +19,4 @@ pub mod form; pub mod manager; - -// TODO: implement FormUI and SearchUI traits using ModuloManager \ No newline at end of file +pub mod search; \ No newline at end of file diff --git a/espanso/src/gui/modulo/search.rs b/espanso/src/gui/modulo/search.rs new file mode 100644 index 0000000..0859d32 --- /dev/null +++ b/espanso/src/gui/modulo/search.rs @@ -0,0 +1,91 @@ +/* + * 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 serde::Serialize; +use serde_json::Value; +use std::{collections::HashMap, path::PathBuf}; + +use crate::gui::{SearchItem, SearchUI}; + +use super::manager::ModuloManager; + +pub struct ModuloSearchUI<'a> { + manager: &'a ModuloManager, + icon_path: Option, +} + +impl<'a> ModuloSearchUI<'a> { + pub fn new(manager: &'a ModuloManager, icon_path: &Option) -> Self { + Self { + manager, + icon_path: icon_path.as_ref().map(|path| path.to_string_lossy().to_string()), + } + } +} + +impl<'a> SearchUI for ModuloSearchUI<'a> { + fn show(&self, items: &[SearchItem]) -> anyhow::Result> { + let modulo_config = ModuloSearchConfig { + icon: self.icon_path.as_deref(), + title: "espanso", + items: convert_items(&items), + }; + + let json_config = serde_json::to_string(&modulo_config)?; + let output = self + .manager + .invoke(&["search", "-j", "-i", "-"], &json_config)?; + let json: Result, _> = serde_json::from_str(&output); + match json { + Ok(json) => { + if let Some(Value::String(selected_id)) = json.get("selected") { + return Ok(Some(selected_id.clone())); + } else { + return Ok(None); + } + } + Err(error) => { + return Err(error.into()); + } + } + } +} + +#[derive(Debug, Serialize)] +struct ModuloSearchConfig<'a> { + icon: Option<&'a str>, + title: &'a str, + items: Vec>, +} + +#[derive(Debug, Serialize)] +struct ModuloSearchItemConfig<'a> { + id: &'a str, + label: &'a str, + trigger: Option<&'a str>, +} + +// TODO: test +fn convert_items<'a>(items: &'a [SearchItem]) -> Vec> { + items.iter().map(|item| ModuloSearchItemConfig { + id: &item.id, + label: &item.label, + trigger: item.tag.as_deref(), + }).collect() +}