diff --git a/.github/scripts/ubuntu/Dockerfile b/.github/scripts/ubuntu/Dockerfile index b3c7eee..f974dd6 100644 --- a/.github/scripts/ubuntu/Dockerfile +++ b/.github/scripts/ubuntu/Dockerfile @@ -31,7 +31,7 @@ RUN set -eux; \ cargo --version; \ rustc --version; -RUN mkdir espanso && cargo install --force cargo-make --version 0.34.0 +RUN mkdir espanso && cargo install rust-script --version "0.7.0" && cargo install --force cargo-make --version 0.34.0 COPY . espanso diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b881035..f910d58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,8 +33,9 @@ jobs: cargo clippy -- -D warnings env: MACOSX_DEPLOYMENT_TARGET: "10.13" - - name: Install cargo-make + - name: Install rust-script and cargo-make run: | + cargo install rust-script --version "0.7.0" cargo install --force cargo-make --version 0.34.0 - name: Run test suite run: cargo make test-binary @@ -58,8 +59,9 @@ jobs: run: | rustup component add clippy cargo clippy -p espanso --features wayland -- -D warnings - - name: Install cargo-make + - name: Install rust-script and cargo-make run: | + cargo install rust-script --version "0.7.0" cargo install --force cargo-make --version 0.34.0 - name: Run test suite run: cargo make test-binary --env NO_X11=true @@ -72,8 +74,9 @@ jobs: - uses: actions/checkout@v2 - name: Install target run: rustup update && rustup target add aarch64-apple-darwin - - name: Install cargo-make + - name: Install rust-script and cargo-make run: | + cargo install rust-script --version "0.7.0" cargo install --force cargo-make --version 0.34.0 - name: Build run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9674a1f..974040b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,8 +59,9 @@ jobs: - name: Print target version run: | echo Using version ${{ needs.extract-version.outputs.espanso_version }} - - name: Install cargo-make + - name: Install rust-script and cargo-make run: | + cargo install rust-script --version "0.7.0" cargo install --force cargo-make --version 0.34.0 - name: Test run: cargo make test-binary --profile release @@ -126,8 +127,9 @@ jobs: - name: Print target version run: | echo Using version ${{ needs.extract-version.outputs.espanso_version }} - - name: Install cargo-make + - name: Install rust-script and cargo-make run: | + cargo install rust-script --version "0.7.0" cargo install --force cargo-make --version 0.34.0 - name: Test run: cargo make test-binary --profile release @@ -166,8 +168,9 @@ jobs: echo Using version ${{ needs.extract-version.outputs.espanso_version }} - name: Install rust target run: rustup update && rustup target add aarch64-apple-darwin - - name: Install cargo-make + - name: Install rust-script and cargo-make run: | + cargo install rust-script --version "0.7.0" cargo install --force cargo-make --version 0.34.0 - name: Build run: cargo make create-bundle --profile release --env BUILD_ARCH=aarch64-apple-darwin diff --git a/Cargo.lock b/Cargo.lock index 0ba00ea..193d7ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,7 +572,7 @@ dependencies = [ [[package]] name = "espanso" -version = "2.1.1-alpha" +version = "2.1.2-alpha" dependencies = [ "anyhow", "caps", diff --git a/espanso-clipboard/src/lib.rs b/espanso-clipboard/src/lib.rs index 7416ce4..68dfecf 100644 --- a/espanso-clipboard/src/lib.rs +++ b/espanso-clipboard/src/lib.rs @@ -49,18 +49,11 @@ pub trait Clipboard { } #[allow(dead_code)] +#[derive(Default)] pub struct ClipboardOperationOptions { pub use_xclip_backend: bool, } -impl Default for ClipboardOperationOptions { - fn default() -> Self { - Self { - use_xclip_backend: false, - } - } -} - #[allow(dead_code)] pub struct ClipboardOptions { // Wayland-only diff --git a/espanso-clipboard/src/wayland/fallback/mod.rs b/espanso-clipboard/src/wayland/fallback/mod.rs index f566a41..d6f27b6 100644 --- a/espanso-clipboard/src/wayland/fallback/mod.rs +++ b/espanso-clipboard/src/wayland/fallback/mod.rs @@ -135,7 +135,7 @@ impl Clipboard for WaylandFallbackClipboard { file.read_to_end(&mut data)?; self.invoke_command_with_timeout( - &mut Command::new("wl-copy").arg("--type").arg("image/png"), + Command::new("wl-copy").arg("--type").arg("image/png"), &data, "wl-copy", ) @@ -148,7 +148,7 @@ impl Clipboard for WaylandFallbackClipboard { _: &ClipboardOperationOptions, ) -> anyhow::Result<()> { self.invoke_command_with_timeout( - &mut Command::new("wl-copy").arg("--type").arg("text/html"), + Command::new("wl-copy").arg("--type").arg("text/html"), html.as_bytes(), "wl-copy", ) diff --git a/espanso-clipboard/src/x11/xclip/mod.rs b/espanso-clipboard/src/x11/xclip/mod.rs index 1136dff..f287386 100644 --- a/espanso-clipboard/src/x11/xclip/mod.rs +++ b/espanso-clipboard/src/x11/xclip/mod.rs @@ -30,7 +30,7 @@ pub struct XClipClipboard { impl XClipClipboard { pub fn new() -> Self { - let command = Command::new("xclipz").arg("-h").output(); + let command = Command::new("xclip").arg("-h").output(); let is_xclip_available = command .map(|output| output.status.success()) .unwrap_or(false); diff --git a/espanso-config/src/config/resolve.rs b/espanso-config/src/config/resolve.rs index 8738085..548c433 100644 --- a/espanso-config/src/config/resolve.rs +++ b/espanso-config/src/config/resolve.rs @@ -37,7 +37,7 @@ use thiserror::Error; const STANDARD_INCLUDES: &[&str] = &["../match/**/[!_]*.yml"]; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub(crate) struct ResolvedConfig { parsed: ParsedConfig, @@ -52,20 +52,6 @@ pub(crate) struct ResolvedConfig { filter_exec: Option, } -impl Default for ResolvedConfig { - fn default() -> Self { - Self { - parsed: Default::default(), - source_path: None, - id: 0, - match_paths: Vec::new(), - filter_title: None, - filter_class: None, - filter_exec: None, - } - } -} - impl Config for ResolvedConfig { fn id(&self) -> i32 { self.id @@ -202,13 +188,10 @@ impl Config for ResolvedConfig { Some("left_shift") => Some(ToggleKey::LeftShift), Some("left_meta") | Some("left_cmd") => Some(ToggleKey::LeftMeta), Some("off") => None, - None => Some(ToggleKey::Alt), + None => None, err => { - error!( - "invalid toggle_key specified {:?}, falling back to ALT", - err - ); - Some(ToggleKey::Alt) + error!("invalid toggle_key specified {:?}", err); + None } } } diff --git a/espanso-config/src/legacy/config.rs b/espanso-config/src/legacy/config.rs index 87ff50c..a873744 100644 --- a/espanso-config/src/legacy/config.rs +++ b/espanso-config/src/legacy/config.rs @@ -701,18 +701,15 @@ impl LegacyConfigSet { let mut sorted_triggers: Vec = default .matches .iter() - .flat_map(|t| triggers_for_match(t)) + .flat_map(triggers_for_match) .collect(); sorted_triggers.sort(); let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers); for s in specific.iter() { - let mut specific_triggers: Vec = s - .matches - .iter() - .flat_map(|t| triggers_for_match(t)) - .collect(); + let mut specific_triggers: Vec = + s.matches.iter().flat_map(triggers_for_match).collect(); specific_triggers.sort(); has_conflicts |= Self::list_has_conflicts(&specific_triggers); } diff --git a/espanso-config/src/matches/group/mod.rs b/espanso-config/src/matches/group/mod.rs index 3d61c23..895a183 100644 --- a/espanso-config/src/matches/group/mod.rs +++ b/espanso-config/src/matches/group/mod.rs @@ -27,23 +27,13 @@ use super::{Match, Variable}; pub(crate) mod loader; mod path; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Default)] pub(crate) struct MatchGroup { pub imports: Vec, pub global_vars: Vec, pub matches: Vec, } -impl Default for MatchGroup { - fn default() -> Self { - Self { - imports: Vec::new(), - global_vars: Vec::new(), - matches: Vec::new(), - } - } -} - impl MatchGroup { // TODO: test pub fn load(group_path: &Path) -> Result<(Self, Option)> { diff --git a/espanso-config/src/matches/mod.rs b/espanso-config/src/matches/mod.rs index 8d2a5bb..33fd8e4 100644 --- a/espanso-config/src/matches/mod.rs +++ b/espanso-config/src/matches/mod.rs @@ -132,19 +132,11 @@ pub enum UpperCasingStyle { CapitalizeWords, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub struct RegexCause { pub regex: String, } -impl Default for RegexCause { - fn default() -> Self { - Self { - regex: String::new(), - } - } -} - // Effects #[derive(Debug, Clone, PartialEq, Eq, Hash, EnumAsInner)] @@ -186,19 +178,11 @@ impl Default for TextEffect { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub struct ImageEffect { pub path: String, } -impl Default for ImageEffect { - fn default() -> Self { - Self { - path: String::new(), - } - } -} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Variable { pub id: StructId, diff --git a/espanso-inject/src/lib.rs b/espanso-inject/src/lib.rs index 6416216..ebc086e 100644 --- a/espanso-inject/src/lib.rs +++ b/espanso-inject/src/lib.rs @@ -89,6 +89,7 @@ impl Default for InjectionOptions { } #[allow(dead_code)] +#[derive(Default)] pub struct InjectorCreationOptions { // Only relevant in X11 Linux systems, use the EVDEV backend instead of X11. pub use_evdev: bool, @@ -128,18 +129,6 @@ pub trait KeyboardStateProvider { fn is_key_pressed(&self, code: u32) -> bool; } -impl Default for InjectorCreationOptions { - fn default() -> Self { - Self { - use_evdev: false, - evdev_modifiers: None, - evdev_max_modifier_combination_len: None, - evdev_keyboard_rmlvo: None, - keyboard_state_provider: None, - } - } -} - #[cfg(target_os = "windows")] pub fn get_injector(_options: InjectorCreationOptions) -> Result> { info!("using Win32Injector"); diff --git a/espanso-match/src/regex/mod.rs b/espanso-match/src/regex/mod.rs index 560f513..b05e5ca 100644 --- a/espanso-match/src/regex/mod.rs +++ b/espanso-match/src/regex/mod.rs @@ -40,19 +40,11 @@ impl RegexMatch { } } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct RegexMatcherState { buffer: String, } -impl Default for RegexMatcherState { - fn default() -> Self { - Self { - buffer: String::new(), - } - } -} - pub struct RegexMatcherOptions { pub max_buffer_size: usize, } diff --git a/espanso-match/src/rolling/matcher.rs b/espanso-match/src/rolling/matcher.rs index 136bed3..44cbd99 100644 --- a/espanso-match/src/rolling/matcher.rs +++ b/espanso-match/src/rolling/matcher.rs @@ -50,20 +50,12 @@ struct RollingMatcherStatePath<'a, Id> { events: Vec<(Event, IsWordSeparator)>, } +#[derive(Default)] pub struct RollingMatcherOptions { pub char_word_separators: Vec, pub key_word_separators: Vec, } -impl Default for RollingMatcherOptions { - fn default() -> Self { - Self { - char_word_separators: Vec::new(), - key_word_separators: Vec::new(), - } - } -} - pub struct RollingMatcher { char_word_separators: Vec, key_word_separators: Vec, diff --git a/espanso-match/src/rolling/mod.rs b/espanso-match/src/rolling/mod.rs index 57d60b7..ec2db0e 100644 --- a/espanso-match/src/rolling/mod.rs +++ b/espanso-match/src/rolling/mod.rs @@ -72,22 +72,13 @@ impl RollingMatch { } } +#[derive(Default)] pub struct StringMatchOptions { pub case_insensitive: bool, pub left_word: bool, pub right_word: bool, } -impl Default for StringMatchOptions { - fn default() -> Self { - Self { - case_insensitive: false, - left_word: false, - right_word: false, - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/espanso-modulo/src/sys/form/form.cpp b/espanso-modulo/src/sys/form/form.cpp index 3481314..8ff4bec 100644 --- a/espanso-modulo/src/sys/form/form.cpp +++ b/espanso-modulo/src/sys/form/form.cpp @@ -96,6 +96,7 @@ private: void Submit(); void OnSubmitBtn(wxCommandEvent& event); void OnCharHook(wxKeyEvent& event); + void OnListBoxEvent(wxCommandEvent& event); void UpdateHelpText(); void HandleNormalFocus(wxFocusEvent& event); void HandleMultilineFocus(wxFocusEvent& event); @@ -141,7 +142,6 @@ FormFrame::FormFrame(const wxString& title, const wxPoint& pos, const wxSize& si Bind(wxEVT_BUTTON, &FormFrame::OnSubmitBtn, this, ID_Submit); Bind(wxEVT_CHAR_HOOK, &FormFrame::OnCharHook, this, wxID_ANY); - // TODO: register ESC click handler: https://forums.wxwidgets.org/viewtopic.php?t=41926 this->SetClientSize(panel->GetBestSize()); this->CentreOnScreen(); @@ -218,6 +218,11 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m } ((wxListBox*)choice)->Bind(wxEVT_SET_FOCUS, &FormFrame::HandleNormalFocus, this, wxID_ANY); + // ListBoxes prevent the global CHAR_HOOK handler from handling the Return key + // correctly, so we need to handle the double click event too (which is triggered + // when the enter key is pressed). + // See: https://github.com/federico-terzi/espanso/issues/857 + ((wxListBox*)choice)->Bind(wxEVT_LISTBOX_DCLICK, &FormFrame::OnListBoxEvent, this, wxID_ANY); // Create the field wrapper std::unique_ptr field((FieldWrapper*) new ListFieldWrapper((wxListBox*) choice)); @@ -311,6 +316,10 @@ void FormFrame::OnCharHook(wxKeyEvent& event) { } } +void FormFrame::OnListBoxEvent(wxCommandEvent& event) { + Submit(); +} + extern "C" void interop_show_form(FormMetadata * _metadata, void (*callback)(ValuePair *values, int size, void *data), void *data) { // Setup high DPI support on Windows #ifdef __WXMSW__ 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; diff --git a/espanso-render/src/lib.rs b/espanso-render/src/lib.rs index 0517f9f..29296ce 100644 --- a/espanso-render/src/lib.rs +++ b/espanso-render/src/lib.rs @@ -42,20 +42,12 @@ pub enum RenderResult { Error(anyhow::Error), } +#[derive(Default)] pub struct Context<'a> { pub global_vars: Vec<&'a Variable>, pub templates: Vec<&'a Template>, } -impl<'a> Default for Context<'a> { - fn default() -> Self { - Self { - global_vars: Vec::new(), - templates: Vec::new(), - } - } -} - #[derive(Debug, Clone, PartialEq)] pub struct RenderOptions { pub casing_style: CasingStyle, @@ -77,23 +69,13 @@ pub enum CasingStyle { Uppercase, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Default)] pub struct Template { pub ids: Vec, pub body: String, pub vars: Vec, } -impl Default for Template { - fn default() -> Self { - Self { - ids: Vec::new(), - body: "".to_string(), - vars: Vec::new(), - } - } -} - #[derive(Debug, Clone, PartialEq)] pub struct Variable { pub name: String, diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml index 4716d8c..65def50 100644 --- a/espanso/Cargo.toml +++ b/espanso/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "2.1.1-alpha" +version = "2.1.2-alpha" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" diff --git a/espanso/src/cli/mod.rs b/espanso/src/cli/mod.rs index f7a3768..258b5a1 100644 --- a/espanso/src/cli/mod.rs +++ b/espanso/src/cli/mod.rs @@ -74,6 +74,7 @@ pub enum LogMode { CleanAndAppend, } +#[derive(Default)] pub struct CliModuleArgs { pub config_store: Option>, pub match_store: Option>, @@ -84,20 +85,6 @@ pub struct CliModuleArgs { pub cli_args: Option>, } -impl Default for CliModuleArgs { - fn default() -> Self { - Self { - config_store: None, - match_store: None, - is_legacy_config: false, - non_fatal_errors: Vec::new(), - paths: None, - paths_overrides: None, - cli_args: None, - } - } -} - pub struct PathsOverrides { pub config: Option, pub runtime: Option, diff --git a/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs b/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs index 163245b..bf7ac6c 100644 --- a/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs +++ b/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs @@ -81,6 +81,11 @@ impl<'a> ClipboardInjectorAdapter<'a> { custom_combination } else if cfg!(target_os = "macos") { vec![Key::Meta, Key::V] + } else if cfg!(target_os = "linux") && cfg!(feature = "wayland") { + // Because on Wayland we currently don't have app-specific configs (and therefore no patches) + // we switch to the more supported SHIFT+INSERT combination + // See: https://github.com/federico-terzi/espanso/issues/899 + vec![Key::Shift, Key::Insert] } else { vec![Key::Control, Key::V] }; diff --git a/espanso/src/cli/worker/engine/dispatch/executor/context_menu.rs b/espanso/src/cli/worker/engine/dispatch/executor/context_menu.rs index 01b8711..b982964 100644 --- a/espanso/src/cli/worker/engine/dispatch/executor/context_menu.rs +++ b/espanso/src/cli/worker/engine/dispatch/executor/context_menu.rs @@ -58,11 +58,7 @@ fn convert_to_ui_menu_item( espanso_engine::event::ui::MenuItem::Sub(sub) => { espanso_ui::menu::MenuItem::Sub(espanso_ui::menu::SubMenuItem { label: sub.label.clone(), - items: sub - .items - .iter() - .map(|item| convert_to_ui_menu_item(item)) - .collect(), + items: sub.items.iter().map(convert_to_ui_menu_item).collect(), }) } espanso_engine::event::ui::MenuItem::Separator => espanso_ui::menu::MenuItem::Separator, diff --git a/espanso/src/cli/worker/engine/funnel/key_state.rs b/espanso/src/cli/worker/engine/funnel/key_state.rs index f352219..8b50a97 100644 --- a/espanso/src/cli/worker/engine/funnel/key_state.rs +++ b/espanso/src/cli/worker/engine/funnel/key_state.rs @@ -76,18 +76,11 @@ impl KeyStateStore { } } +#[derive(Default)] struct KeyState { keys: HashMap, } -impl Default for KeyState { - fn default() -> Self { - Self { - keys: HashMap::new(), - } - } -} - struct KeyStatus { pressed_at: Option, } diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index 513bff5..8581a62 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -49,7 +49,9 @@ use crate::{ }, multiplex::MultiplexAdapter, render::{ - extension::{clipboard::ClipboardAdapter, form::FormProviderAdapter}, + extension::{ + choice::ChoiceSelectorAdapter, clipboard::ClipboardAdapter, form::FormProviderAdapter, + }, RendererAdapter, }, }, @@ -198,6 +200,9 @@ pub fn initialize_and_spawn( let shell_extension = espanso_render::extension::shell::ShellExtension::new(&paths.config); let form_adapter = FormProviderAdapter::new(&modulo_form_ui); let form_extension = espanso_render::extension::form::FormExtension::new(&form_adapter); + let choice_adapter = ChoiceSelectorAdapter::new(&modulo_search_ui); + let choice_extension = + espanso_render::extension::choice::ChoiceExtension::new(&choice_adapter); let renderer = espanso_render::create(vec![ &clipboard_extension, &date_extension, @@ -207,6 +212,7 @@ pub fn initialize_and_spawn( &script_extension, &shell_extension, &form_extension, + &choice_extension, ]); let renderer_adapter = RendererAdapter::new(&match_cache, &config_manager, &renderer); let path_provider = PathProviderAdapter::new(&paths); diff --git a/espanso/src/cli/worker/engine/process/middleware/render/extension/choice.rs b/espanso/src/cli/worker/engine/process/middleware/render/extension/choice.rs new file mode 100644 index 0000000..d2f245d --- /dev/null +++ b/espanso/src/cli/worker/engine/process/middleware/render/extension/choice.rs @@ -0,0 +1,55 @@ +/* + * 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 espanso_render::extension::choice::{ChoiceSelector, ChoiceSelectorResult}; + +use crate::gui::{SearchItem, SearchUI}; + +pub struct ChoiceSelectorAdapter<'a> { + search_ui: &'a dyn SearchUI, +} + +impl<'a> ChoiceSelectorAdapter<'a> { + pub fn new(search_ui: &'a dyn SearchUI) -> Self { + Self { search_ui } + } +} + +impl<'a> ChoiceSelector for ChoiceSelectorAdapter<'a> { + fn show(&self, choices: &[espanso_render::extension::choice::Choice]) -> ChoiceSelectorResult { + let items = convert_items(choices); + match self.search_ui.show(&items, None) { + Ok(Some(choice)) => ChoiceSelectorResult::Success(choice), + Ok(None) => ChoiceSelectorResult::Aborted, + Err(err) => ChoiceSelectorResult::Error(err), + } + } +} + +fn convert_items(choices: &[espanso_render::extension::choice::Choice]) -> Vec { + choices + .iter() + .map(|choice| SearchItem { + id: choice.id.to_string(), + label: choice.label.to_string(), + tag: None, + is_builtin: false, + }) + .collect() +} diff --git a/espanso/src/cli/worker/engine/process/middleware/render/extension/mod.rs b/espanso/src/cli/worker/engine/process/middleware/render/extension/mod.rs index d7cafac..e2497b5 100644 --- a/espanso/src/cli/worker/engine/process/middleware/render/extension/mod.rs +++ b/espanso/src/cli/worker/engine/process/middleware/render/extension/mod.rs @@ -17,5 +17,6 @@ * along with espanso. If not, see . */ +pub mod choice; pub mod clipboard; pub mod form; diff --git a/espanso/src/patch/mod.rs b/espanso/src/patch/mod.rs index fa6d065..8bb7055 100644 --- a/espanso/src/patch/mod.rs +++ b/espanso/src/patch/mod.rs @@ -31,6 +31,7 @@ pub fn patch_store(store: Box) -> Box { fn get_builtin_patches() -> Vec { #[cfg(target_os = "windows")] return vec![ + patches::win::brave::patch(), patches::win::onenote_for_windows_10::patch(), patches::win::vscode_win::patch(), ]; diff --git a/espanso/src/patch/patches/win/brave.rs b/espanso/src/patch/patches/win/brave.rs new file mode 100644 index 0000000..2e57be8 --- /dev/null +++ b/espanso/src/patch/patches/win/brave.rs @@ -0,0 +1,41 @@ +/* + * 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 std::sync::Arc; + +use crate::patch::patches::{PatchedConfig, Patches}; +use crate::patch::PatchDefinition; + +pub fn patch() -> PatchDefinition { + PatchDefinition { + name: module_path!().split(':').last().unwrap_or("unknown"), + is_enabled: || cfg!(target_os = "windows"), + should_patch: |app| app.exec.unwrap_or_default().contains("brave.exe"), + apply: |base, name| { + Arc::new(PatchedConfig::patch( + base, + name, + Patches { + pre_paste_delay: Some(400), + ..Default::default() + }, + )) + }, + } +} diff --git a/espanso/src/patch/patches/win/mod.rs b/espanso/src/patch/patches/win/mod.rs index 63fb862..a6762b9 100644 --- a/espanso/src/patch/patches/win/mod.rs +++ b/espanso/src/patch/patches/win/mod.rs @@ -17,5 +17,6 @@ * along with espanso. If not, see . */ +pub mod brave; pub mod onenote_for_windows_10; pub mod vscode_win; diff --git a/packager.py b/packager.py deleted file mode 100644 index 6a944a1..0000000 --- a/packager.py +++ /dev/null @@ -1,254 +0,0 @@ -import subprocess -import sys -import os -import platform -import hashlib -import click -import shutil -import toml -import hashlib -import glob -import urllib.request -from dataclasses import dataclass - -PACKAGER_TARGET_DIR = "target/packager" - -@dataclass -class PackageInfo: - name: str - version: str - description: str - publisher: str - url: str - modulo_version: str - -@click.group() -def cli(): - pass - -@cli.command() -@click.option('--skipcargo', default=False, is_flag=True, help="Skip cargo release build") -def build(skipcargo): - """Build espanso distribution""" - # Check operating system - TARGET_OS = "macos" - if platform.system() == "Windows": - TARGET_OS = "windows" - elif platform.system() == "Linux": - TARGET_OS = "linux" - - print("Detected OS:", TARGET_OS) - - print("Loading info from Cargo.toml") - cargo_info = toml.load("Cargo.toml") - package_info = PackageInfo(cargo_info["package"]["name"], - cargo_info["package"]["version"], - cargo_info["package"]["description"], - cargo_info["package"]["authors"][0], - cargo_info["package"]["homepage"], - cargo_info["modulo"]["version"]) - print(package_info) - - if not skipcargo: - print("Building release version...") - subprocess.run(["cargo", "build", "--release"]) - else: - print("Skipping build") - - if TARGET_OS == "windows": - build_windows(package_info) - elif TARGET_OS == "macos": - build_mac(package_info) - -def calculate_sha256(file): - with open(file, "rb") as f: - b = f.read() # read entire file as bytes - readable_hash = hashlib.sha256(b).hexdigest() - return readable_hash - -def build_windows(package_info): - print("Starting packaging process for Windows...") - - # Check Inno Setup - try: - subprocess.run(["iscc"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except FileNotFoundError: - raise Exception("Could not find Inno Setup compiler. Please install it from here: http://www.jrsoftware.org/isdl.php") - - print("Clearing target dirs") - - # Clearing previous build directory - if os.path.isdir(PACKAGER_TARGET_DIR): - print("Cleaning packager temp directory...") - shutil.rmtree(PACKAGER_TARGET_DIR) - - TARGET_DIR = os.path.join(PACKAGER_TARGET_DIR, "win") - os.makedirs(TARGET_DIR, exist_ok=True) - - modulo_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-win.exe".format(package_info.modulo_version) - modulo_sha_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-win.exe.sha256.txt".format(package_info.modulo_version) - print("Pulling modulo depencency from:", modulo_url) - modulo_target_file = os.path.join(TARGET_DIR, "modulo.exe") - urllib.request.urlretrieve(modulo_url, modulo_target_file) - print("Pulling SHA signature from:", modulo_sha_url) - modulo_sha_file = os.path.join(TARGET_DIR, "modulo.sha256") - urllib.request.urlretrieve(modulo_sha_url, modulo_sha_file) - print("Checking signatures...") - expected_sha = None - with open(modulo_sha_file, "r") as sha_f: - expected_sha = sha_f.read() - actual_sha = calculate_sha256(modulo_target_file) - if actual_sha != expected_sha: - raise Exception("Modulo SHA256 is not matching") - - print("Gathering CRT DLLs...") - msvc_dirs = glob.glob("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\*\\VC\\Redist\\MSVC\\*") - print("Found Redists: ", msvc_dirs) - - print("Determining best redist...") - - if len(msvc_dirs) == 0: - raise Exception("Cannot find redistributable dlls") - - msvc_dir = None - - for curr_dir in msvc_dirs: - dll_files = glob.glob(curr_dir + "\\x64\\*CRT\\*.dll") - print("Found dlls", dll_files, "in", curr_dir) - if any("vcruntime140_1.dll" in x.lower() for x in dll_files): - msvc_dir = curr_dir - break - - if msvc_dir is None: - raise Exception("Cannot find redist with VCRUNTIME140_1.dll") - - print("Using: ", msvc_dir) - - dll_files = glob.glob(msvc_dir + "\\x64\\*CRT\\*.dll") - - print("Found DLLs:") - include_list = [] - for dll in dll_files: - print("Including: "+dll) - include_list.append("Source: \""+dll+"\"; DestDir: \"{app}\"; Flags: ignoreversion") - - print("Including modulo") - include_list.append("Source: \""+os.path.abspath(modulo_target_file)+"\"; DestDir: \"{app}\"; Flags: ignoreversion") - - include = "\r\n".join(include_list) - - INSTALLER_NAME = f"espanso-win-installer" - - # Inno setup - shutil.copy("packager/win/modpath.iss", os.path.join(TARGET_DIR, "modpath.iss")) - - print("Processing inno setup template") - with open("packager/win/setupscript.iss", "r") as iss_script: - content = iss_script.read() - - # Replace variables - content = content.replace("{{{app_name}}}", package_info.name) - content = content.replace("{{{app_version}}}", package_info.version) - content = content.replace("{{{app_publisher}}}", package_info.publisher) - content = content.replace("{{{app_url}}}", package_info.url) - content = content.replace("{{{app_license}}}", os.path.abspath("LICENSE")) - content = content.replace("{{{app_icon}}}", os.path.abspath("packager/win/icon.ico")) - content = content.replace("{{{executable_path}}}", os.path.abspath("target/release/espanso.exe")) - content = content.replace("{{{output_dir}}}", os.path.abspath(TARGET_DIR)) - content = content.replace("{{{output_name}}}", INSTALLER_NAME) - content = content.replace("{{{dll_include}}}", include) - - with open(os.path.join(TARGET_DIR, "setupscript.iss"), "w") as output_script: - output_script.write(content) - - print("Compiling installer with Inno setup") - subprocess.run(["iscc", os.path.abspath(os.path.join(TARGET_DIR, "setupscript.iss"))]) - - print("Calculating the SHA256") - sha256_hash = hashlib.sha256() - with open(os.path.abspath(os.path.join(TARGET_DIR, INSTALLER_NAME+".exe")),"rb") as f: - # Read and update hash string value in blocks of 4K - for byte_block in iter(lambda: f.read(4096),b""): - sha256_hash.update(byte_block) - - hash_file = os.path.abspath(os.path.join(TARGET_DIR, "espanso-win-installer-sha256.txt")) - with open(hash_file, "w") as hf: - hf.write(sha256_hash.hexdigest()) - - -def build_mac(package_info): - print("Starting packaging process for MacOS...") - - print("Clearing target dirs") - - # Clearing previous build directory - if os.path.isdir(PACKAGER_TARGET_DIR): - print("Cleaning packager temp directory...") - shutil.rmtree(PACKAGER_TARGET_DIR) - - TARGET_DIR = os.path.join(PACKAGER_TARGET_DIR, "mac") - os.makedirs(TARGET_DIR, exist_ok=True) - - print("Compressing release to archive...") - target_name = f"espanso-mac.tar.gz" - archive_target = os.path.abspath(os.path.join(TARGET_DIR, target_name)) - subprocess.run(["tar", - "-C", os.path.abspath("target/release"), - "-cvf", - archive_target, - "espanso", - ]) - print(f"Created archive: {archive_target}") - - print("Calculating the SHA256") - sha256_hash = hashlib.sha256() - with open(archive_target,"rb") as f: - # Read and update hash string value in blocks of 4K - for byte_block in iter(lambda: f.read(4096),b""): - sha256_hash.update(byte_block) - - hash_file = os.path.abspath(os.path.join(TARGET_DIR, "espanso-mac-sha256.txt")) - with open(hash_file, "w") as hf: - hf.write(sha256_hash.hexdigest()) - - modulo_sha_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-mac.sha256.txt".format(package_info.modulo_version) - print("Pulling SHA signature from:", modulo_sha_url) - modulo_sha_file = os.path.join(TARGET_DIR, "modulo.sha256") - urllib.request.urlretrieve(modulo_sha_url, modulo_sha_file) - modulo_sha = None - with open(modulo_sha_file, "r") as sha_f: - modulo_sha = sha_f.read() - if modulo_sha is None: - raise Exception("Cannot determine modulo SHA") - - print("Processing Homebrew formula template") - with open("packager/mac/espanso.rb", "r") as formula_template: - content = formula_template.read() - - # Replace variables - content = content.replace("{{{app_desc}}}", package_info.description) - content = content.replace("{{{app_url}}}", package_info.url) - content = content.replace("{{{app_version}}}", package_info.version) - content = content.replace("{{{modulo_version}}}", package_info.modulo_version) - content = content.replace("{{{modulo_sha}}}", modulo_sha) - - # Calculate hash - with open(archive_target, "rb") as f: - bytes = f.read() - readable_hash = hashlib.sha256(bytes).hexdigest() - content = content.replace("{{{release_hash}}}", readable_hash) - - with open(os.path.join(TARGET_DIR, "espanso.rb"), "w") as output_script: - output_script.write(content) - - print("Done!") - - -if __name__ == '__main__': - print("[[ espanso packager ]]") - - # Check python version 3 - if sys.version_info[0] < 3: - raise Exception("Must be using Python 3") - - cli() \ No newline at end of file diff --git a/scripts/create_app_image.sh b/scripts/create_app_image.sh index 4779363..f3a31c4 100644 --- a/scripts/create_app_image.sh +++ b/scripts/create_app_image.sh @@ -8,7 +8,7 @@ BASE_DIR=$(pwd) mkdir -p $TOOL_DIR -if ls $TOOL_DIR/*.AppImage 1> /dev/null 2>&1; then +if ls $TOOL_DIR/linuxdeploy*.AppImage 1> /dev/null 2>&1; then echo "Skipping download of linuxdeploy" else echo "Downloading linuxdeploy tool" @@ -16,12 +16,36 @@ else chmod +x $TOOL_DIR/linuxdeploy*.AppImage fi +if ls $TOOL_DIR/appimagetool*.AppImage 1> /dev/null 2>&1; then + echo "Skipping download of appimagetool" +else + echo "Downloading appimagetool" + wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -P "$TOOL_DIR" + chmod +x $TOOL_DIR/appimagetool*.AppImage +fi + rm -Rf "$TARGET_DIR" mkdir -p $OUTPUT_DIR mkdir -p $BUILD_DIR echo Building AppImage into $OUTPUT_DIR pushd $OUTPUT_DIR -$TOOL_DIR/linuxdeploy*.AppImage --appimage-extract-and-run -e "$BASE_DIR/$EXEC_PATH" -d "$BASE_DIR/espanso/src/res/linux/espanso.desktop" -i "$BASE_DIR/espanso/src/res/linux/icon.png" --appdir $BUILD_DIR --output appimage +$TOOL_DIR/linuxdeploy*.AppImage --appimage-extract-and-run -e "$BASE_DIR/$EXEC_PATH" \ + -d "$BASE_DIR/espanso/src/res/linux/espanso.desktop" \ + -i "$BASE_DIR/espanso/src/res/linux/icon.png" \ + --appdir $BUILD_DIR \ + --output appimage chmod +x ./Espanso*.AppImage + +# Apply a workaround to fix this issue: https://github.com/federico-terzi/espanso/issues/900 +# See: https://github.com/project-slippi/Ishiiruka/issues/323#issuecomment-977415376 + +echo "Applying patch for libgmodule" + +./Espanso*.AppImage --appimage-extract +rm -Rf ./Espanso*.AppImage +rm -Rf squashfs-root/usr/lib/libgmodule* +$TOOL_DIR/appimagetool*.AppImage --appimage-extract-and-run -v squashfs-root +rm -Rf squashfs-root + popd \ No newline at end of file diff --git a/snap/hooks/remove b/snap/hooks/remove new file mode 100755 index 0000000..5f85fec --- /dev/null +++ b/snap/hooks/remove @@ -0,0 +1,12 @@ +#!/bin/sh + +echo "Stopping Espanso..." +killall espanso + +# Here I've also tried to unregister the Systemd service, but couldn't manage to. +# The remove hook is run as Root, but the systemd service is registered as a user. +# I couldn't find any (working) way to unregister a user systemd service as root. +# I've also tried these solutions: https://unix.stackexchange.com/questions/552922/stop-systemd-user-services-as-root-user +# but none seemed to work in this case. +# If you manage to find a way to do so, feel free to open an issue so that +# we can improve the hook! Thanks \ No newline at end of file diff --git a/snapcraft.yaml b/snapcraft.yaml index 8f5437c..c69662a 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: espanso -version: 2.1.1-alpha +version: 2.1.2-alpha summary: A Cross-platform Text Expander written in Rust description: | espanso is a Cross-platform, Text Expander written in Rust.