feat(core): wire up match selection GUI
This commit is contained in:
		
							parent
							
								
									e361bdb9c2
								
							
						
					
					
						commit
						5a66594532
					
				|  | @ -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<crate::engine::event::effect::TextInjectMode> { | ||||
|     let m = self.cache.get(&match_id)?; | ||||
|  |  | |||
|  | @ -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<super::engine::matcher::MatcherState>, | ||||
|       > = 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()) | ||||
|  |  | |||
|  | @ -17,21 +17,69 @@ | |||
|  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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<i32> { | ||||
|     // TODO: replace with actual selection
 | ||||
|     Some(*matches_ids.first().unwrap()) | ||||
|     let matches = self.match_provider.get_matches(&matches_ids); | ||||
|     let search_items: Vec<SearchItem> = 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::<i32>() { | ||||
|         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
 | ||||
|  | @ -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<PathBuf>, | ||||
|   pub search_icon: Option<PathBuf>, | ||||
| 
 | ||||
|   pub tray_icon_normal: Option<PathBuf>, | ||||
|   pub tray_icon_disabled: Option<PathBuf>, | ||||
|  | @ -45,6 +46,7 @@ pub struct IconPaths { | |||
| pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> { | ||||
|   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<IconPaths> { | |||
| pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> { | ||||
|   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() | ||||
|   }) | ||||
| } | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ use anyhow::Result; | |||
| pub mod modulo; | ||||
| 
 | ||||
| pub trait SearchUI { | ||||
|   fn show(&self, items: &SearchItem) -> Result<Option<String>>; | ||||
|   fn show(&self, items: &[SearchItem]) -> Result<Option<String>>; | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
|  |  | |||
|  | @ -31,10 +31,10 @@ pub struct ModuloFormUI<'a> { | |||
| } | ||||
| 
 | ||||
| impl<'a> ModuloFormUI<'a> { | ||||
|   pub fn new(manager: &'a ModuloManager, icon_path: Option<PathBuf>) -> Self { | ||||
|   pub fn new(manager: &'a ModuloManager, icon_path: &Option<PathBuf>) -> 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()), | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -19,5 +19,4 @@ | |||
| 
 | ||||
| pub mod form; | ||||
| pub mod manager; | ||||
| 
 | ||||
| // TODO: implement FormUI and SearchUI traits using ModuloManager
 | ||||
| pub mod search; | ||||
							
								
								
									
										91
									
								
								espanso/src/gui/modulo/search.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								espanso/src/gui/modulo/search.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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<String>, | ||||
| } | ||||
| 
 | ||||
| impl<'a> ModuloSearchUI<'a> { | ||||
|   pub fn new(manager: &'a ModuloManager, icon_path: &Option<PathBuf>) -> 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<Option<String>> { | ||||
|     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<HashMap<String, Value>, _> = 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<ModuloSearchItemConfig<'a>>, | ||||
| } | ||||
| 
 | ||||
| #[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<ModuloSearchItemConfig<'a>> { | ||||
|   items.iter().map(|item| ModuloSearchItemConfig { | ||||
|     id: &item.id, | ||||
|     label: &item.label, 
 | ||||
|     trigger: item.tag.as_deref(), 
 | ||||
|   }).collect() | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user