diff --git a/espanso-render/src/extension/choice.rs b/espanso-render/src/extension/choice.rs new file mode 100644 index 0000000..b043680 --- /dev/null +++ b/espanso-render/src/extension/choice.rs @@ -0,0 +1,132 @@ +/* + * 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 anyhow::Result; +use log::error; +use thiserror::Error; + +use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value}; + +pub trait ChoiceSelector { + fn show(&self, choices: &[Choice]) -> ChoiceSelectorResult; +} + +#[derive(Debug, Clone)] +pub struct Choice<'a> { + pub label: &'a str, + pub id: &'a str, +} + +pub enum ChoiceSelectorResult { + Success(String), + Aborted, + Error(anyhow::Error), +} + +pub struct ChoiceExtension<'a> { + selector: &'a dyn ChoiceSelector, +} + +#[allow(clippy::new_without_default)] +impl<'a> ChoiceExtension<'a> { + pub fn new(selector: &'a dyn ChoiceSelector) -> Self { + Self { selector } + } +} + +impl<'a> Extension for ChoiceExtension<'a> { + fn name(&self) -> &str { + "choice" + } + + fn calculate( + &self, + _: &crate::Context, + _: &crate::Scope, + params: &Params, + ) -> crate::ExtensionResult { + let choices: Vec = if let Some(Value::String(values)) = params.get("values") { + values + .lines() + .filter_map(|line| { + let trimmed_line = line.trim(); + if !trimmed_line.is_empty() { + Some(trimmed_line) + } else { + None + } + }) + .map(|line| Choice { + label: line, + id: line, + }) + .collect() + } else if let Some(Value::Array(values)) = params.get("values") { + let choices: Result> = values + .iter() + .map(|value| match value { + Value::String(string) => Ok(Choice { + id: string, + label: string, + }), + Value::Object(fields) => Ok(Choice { + id: fields + .get("id") + .and_then(|val| val.as_string()) + .ok_or(ChoiceError::InvalidObjectValue)?, + label: fields + .get("label") + .and_then(|val| val.as_string()) + .ok_or(ChoiceError::InvalidObjectValue)?, + }), + _ => Err(ChoiceError::InvalidValueType.into()), + }) + .collect(); + + match choices { + Ok(choices) => choices, + Err(err) => { + return crate::ExtensionResult::Error(err); + } + } + } else { + return crate::ExtensionResult::Error(ChoiceError::MissingValues.into()); + }; + + match self.selector.show(&choices) { + ChoiceSelectorResult::Success(choice_id) => { + ExtensionResult::Success(ExtensionOutput::Single(choice_id)) + } + ChoiceSelectorResult::Aborted => ExtensionResult::Aborted, + ChoiceSelectorResult::Error(error) => ExtensionResult::Error(error), + } + } +} + +#[derive(Error, Debug)] +pub enum ChoiceError { + #[error("missing values parameter")] + MissingValues, + + #[error("values contain object items, but they are missing either the 'id' or 'label' fields")] + InvalidObjectValue, + + #[error("values contain an invalid item type. items can only be strings or objects")] + InvalidValueType, +} diff --git a/espanso-render/src/extension/mod.rs b/espanso-render/src/extension/mod.rs index 35bae77..4cc7356 100644 --- a/espanso-render/src/extension/mod.rs +++ b/espanso-render/src/extension/mod.rs @@ -17,6 +17,7 @@ * along with espanso. If not, see . */ +pub mod choice; pub mod clipboard; pub mod date; pub mod echo;