feat(core): wire up match selection GUI

This commit is contained in:
Federico Terzi 2021-05-08 10:56:05 +02:00
parent e361bdb9c2
commit 5a66594532
8 changed files with 166 additions and 16 deletions

View File

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

View File

@ -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, &regex_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())

View File

@ -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

View File

@ -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()
})
}

View File

@ -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)]

View File

@ -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()),
}
}
}

View File

@ -19,5 +19,4 @@
pub mod form;
pub mod manager;
// TODO: implement FormUI and SearchUI traits using ModuloManager
pub mod search;

View 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()
}