Merge pull request #917 from federico-terzi/dev

v2.1.2-alpha
This commit is contained in:
Federico Terzi 2021-12-27 19:56:04 +01:00 committed by GitHub
commit 152402b74e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 331 additions and 422 deletions

View File

@ -31,7 +31,7 @@ RUN set -eux; \
cargo --version; \ cargo --version; \
rustc --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 COPY . espanso

View File

@ -33,8 +33,9 @@ jobs:
cargo clippy -- -D warnings cargo clippy -- -D warnings
env: env:
MACOSX_DEPLOYMENT_TARGET: "10.13" MACOSX_DEPLOYMENT_TARGET: "10.13"
- name: Install cargo-make - name: Install rust-script and cargo-make
run: | run: |
cargo install rust-script --version "0.7.0"
cargo install --force cargo-make --version 0.34.0 cargo install --force cargo-make --version 0.34.0
- name: Run test suite - name: Run test suite
run: cargo make test-binary run: cargo make test-binary
@ -58,8 +59,9 @@ jobs:
run: | run: |
rustup component add clippy rustup component add clippy
cargo clippy -p espanso --features wayland -- -D warnings cargo clippy -p espanso --features wayland -- -D warnings
- name: Install cargo-make - name: Install rust-script and cargo-make
run: | run: |
cargo install rust-script --version "0.7.0"
cargo install --force cargo-make --version 0.34.0 cargo install --force cargo-make --version 0.34.0
- name: Run test suite - name: Run test suite
run: cargo make test-binary --env NO_X11=true run: cargo make test-binary --env NO_X11=true
@ -72,8 +74,9 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install target - name: Install target
run: rustup update && rustup target add aarch64-apple-darwin run: rustup update && rustup target add aarch64-apple-darwin
- name: Install cargo-make - name: Install rust-script and cargo-make
run: | run: |
cargo install rust-script --version "0.7.0"
cargo install --force cargo-make --version 0.34.0 cargo install --force cargo-make --version 0.34.0
- name: Build - name: Build
run: | run: |

View File

@ -59,8 +59,9 @@ jobs:
- name: Print target version - name: Print target version
run: | run: |
echo Using version ${{ needs.extract-version.outputs.espanso_version }} echo Using version ${{ needs.extract-version.outputs.espanso_version }}
- name: Install cargo-make - name: Install rust-script and cargo-make
run: | run: |
cargo install rust-script --version "0.7.0"
cargo install --force cargo-make --version 0.34.0 cargo install --force cargo-make --version 0.34.0
- name: Test - name: Test
run: cargo make test-binary --profile release run: cargo make test-binary --profile release
@ -126,8 +127,9 @@ jobs:
- name: Print target version - name: Print target version
run: | run: |
echo Using version ${{ needs.extract-version.outputs.espanso_version }} echo Using version ${{ needs.extract-version.outputs.espanso_version }}
- name: Install cargo-make - name: Install rust-script and cargo-make
run: | run: |
cargo install rust-script --version "0.7.0"
cargo install --force cargo-make --version 0.34.0 cargo install --force cargo-make --version 0.34.0
- name: Test - name: Test
run: cargo make test-binary --profile release run: cargo make test-binary --profile release
@ -166,8 +168,9 @@ jobs:
echo Using version ${{ needs.extract-version.outputs.espanso_version }} echo Using version ${{ needs.extract-version.outputs.espanso_version }}
- name: Install rust target - name: Install rust target
run: rustup update && rustup target add aarch64-apple-darwin run: rustup update && rustup target add aarch64-apple-darwin
- name: Install cargo-make - name: Install rust-script and cargo-make
run: | run: |
cargo install rust-script --version "0.7.0"
cargo install --force cargo-make --version 0.34.0 cargo install --force cargo-make --version 0.34.0
- name: Build - name: Build
run: cargo make create-bundle --profile release --env BUILD_ARCH=aarch64-apple-darwin run: cargo make create-bundle --profile release --env BUILD_ARCH=aarch64-apple-darwin

2
Cargo.lock generated
View File

@ -572,7 +572,7 @@ dependencies = [
[[package]] [[package]]
name = "espanso" name = "espanso"
version = "2.1.1-alpha" version = "2.1.2-alpha"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"caps", "caps",

View File

@ -49,18 +49,11 @@ pub trait Clipboard {
} }
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Default)]
pub struct ClipboardOperationOptions { pub struct ClipboardOperationOptions {
pub use_xclip_backend: bool, pub use_xclip_backend: bool,
} }
impl Default for ClipboardOperationOptions {
fn default() -> Self {
Self {
use_xclip_backend: false,
}
}
}
#[allow(dead_code)] #[allow(dead_code)]
pub struct ClipboardOptions { pub struct ClipboardOptions {
// Wayland-only // Wayland-only

View File

@ -135,7 +135,7 @@ impl Clipboard for WaylandFallbackClipboard {
file.read_to_end(&mut data)?; file.read_to_end(&mut data)?;
self.invoke_command_with_timeout( 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, &data,
"wl-copy", "wl-copy",
) )
@ -148,7 +148,7 @@ impl Clipboard for WaylandFallbackClipboard {
_: &ClipboardOperationOptions, _: &ClipboardOperationOptions,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
self.invoke_command_with_timeout( 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(), html.as_bytes(),
"wl-copy", "wl-copy",
) )

View File

@ -30,7 +30,7 @@ pub struct XClipClipboard {
impl XClipClipboard { impl XClipClipboard {
pub fn new() -> Self { 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 let is_xclip_available = command
.map(|output| output.status.success()) .map(|output| output.status.success())
.unwrap_or(false); .unwrap_or(false);

View File

@ -37,7 +37,7 @@ use thiserror::Error;
const STANDARD_INCLUDES: &[&str] = &["../match/**/[!_]*.yml"]; const STANDARD_INCLUDES: &[&str] = &["../match/**/[!_]*.yml"];
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub(crate) struct ResolvedConfig { pub(crate) struct ResolvedConfig {
parsed: ParsedConfig, parsed: ParsedConfig,
@ -52,20 +52,6 @@ pub(crate) struct ResolvedConfig {
filter_exec: Option<Regex>, filter_exec: Option<Regex>,
} }
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 { impl Config for ResolvedConfig {
fn id(&self) -> i32 { fn id(&self) -> i32 {
self.id self.id
@ -202,13 +188,10 @@ impl Config for ResolvedConfig {
Some("left_shift") => Some(ToggleKey::LeftShift), Some("left_shift") => Some(ToggleKey::LeftShift),
Some("left_meta") | Some("left_cmd") => Some(ToggleKey::LeftMeta), Some("left_meta") | Some("left_cmd") => Some(ToggleKey::LeftMeta),
Some("off") => None, Some("off") => None,
None => Some(ToggleKey::Alt), None => None,
err => { err => {
error!( error!("invalid toggle_key specified {:?}", err);
"invalid toggle_key specified {:?}, falling back to ALT", None
err
);
Some(ToggleKey::Alt)
} }
} }
} }

View File

@ -701,18 +701,15 @@ impl LegacyConfigSet {
let mut sorted_triggers: Vec<String> = default let mut sorted_triggers: Vec<String> = default
.matches .matches
.iter() .iter()
.flat_map(|t| triggers_for_match(t)) .flat_map(triggers_for_match)
.collect(); .collect();
sorted_triggers.sort(); sorted_triggers.sort();
let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers); let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers);
for s in specific.iter() { for s in specific.iter() {
let mut specific_triggers: Vec<String> = s let mut specific_triggers: Vec<String> =
.matches s.matches.iter().flat_map(triggers_for_match).collect();
.iter()
.flat_map(|t| triggers_for_match(t))
.collect();
specific_triggers.sort(); specific_triggers.sort();
has_conflicts |= Self::list_has_conflicts(&specific_triggers); has_conflicts |= Self::list_has_conflicts(&specific_triggers);
} }

View File

@ -27,23 +27,13 @@ use super::{Match, Variable};
pub(crate) mod loader; pub(crate) mod loader;
mod path; mod path;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Default)]
pub(crate) struct MatchGroup { pub(crate) struct MatchGroup {
pub imports: Vec<String>, pub imports: Vec<String>,
pub global_vars: Vec<Variable>, pub global_vars: Vec<Variable>,
pub matches: Vec<Match>, pub matches: Vec<Match>,
} }
impl Default for MatchGroup {
fn default() -> Self {
Self {
imports: Vec::new(),
global_vars: Vec::new(),
matches: Vec::new(),
}
}
}
impl MatchGroup { impl MatchGroup {
// TODO: test // TODO: test
pub fn load(group_path: &Path) -> Result<(Self, Option<NonFatalErrorSet>)> { pub fn load(group_path: &Path) -> Result<(Self, Option<NonFatalErrorSet>)> {

View File

@ -132,19 +132,11 @@ pub enum UpperCasingStyle {
CapitalizeWords, CapitalizeWords,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct RegexCause { pub struct RegexCause {
pub regex: String, pub regex: String,
} }
impl Default for RegexCause {
fn default() -> Self {
Self {
regex: String::new(),
}
}
}
// Effects // Effects
#[derive(Debug, Clone, PartialEq, Eq, Hash, EnumAsInner)] #[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 struct ImageEffect {
pub path: String, pub path: String,
} }
impl Default for ImageEffect {
fn default() -> Self {
Self {
path: String::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Variable { pub struct Variable {
pub id: StructId, pub id: StructId,

View File

@ -89,6 +89,7 @@ impl Default for InjectionOptions {
} }
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Default)]
pub struct InjectorCreationOptions { pub struct InjectorCreationOptions {
// Only relevant in X11 Linux systems, use the EVDEV backend instead of X11. // Only relevant in X11 Linux systems, use the EVDEV backend instead of X11.
pub use_evdev: bool, pub use_evdev: bool,
@ -128,18 +129,6 @@ pub trait KeyboardStateProvider {
fn is_key_pressed(&self, code: u32) -> bool; 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")] #[cfg(target_os = "windows")]
pub fn get_injector(_options: InjectorCreationOptions) -> Result<Box<dyn Injector>> { pub fn get_injector(_options: InjectorCreationOptions) -> Result<Box<dyn Injector>> {
info!("using Win32Injector"); info!("using Win32Injector");

View File

@ -40,19 +40,11 @@ impl<Id> RegexMatch<Id> {
} }
} }
#[derive(Clone)] #[derive(Clone, Default)]
pub struct RegexMatcherState { pub struct RegexMatcherState {
buffer: String, buffer: String,
} }
impl Default for RegexMatcherState {
fn default() -> Self {
Self {
buffer: String::new(),
}
}
}
pub struct RegexMatcherOptions { pub struct RegexMatcherOptions {
pub max_buffer_size: usize, pub max_buffer_size: usize,
} }

View File

@ -50,20 +50,12 @@ struct RollingMatcherStatePath<'a, Id> {
events: Vec<(Event, IsWordSeparator)>, events: Vec<(Event, IsWordSeparator)>,
} }
#[derive(Default)]
pub struct RollingMatcherOptions { pub struct RollingMatcherOptions {
pub char_word_separators: Vec<String>, pub char_word_separators: Vec<String>,
pub key_word_separators: Vec<Key>, pub key_word_separators: Vec<Key>,
} }
impl Default for RollingMatcherOptions {
fn default() -> Self {
Self {
char_word_separators: Vec::new(),
key_word_separators: Vec::new(),
}
}
}
pub struct RollingMatcher<Id> { pub struct RollingMatcher<Id> {
char_word_separators: Vec<String>, char_word_separators: Vec<String>,
key_word_separators: Vec<Key>, key_word_separators: Vec<Key>,

View File

@ -72,22 +72,13 @@ impl<Id> RollingMatch<Id> {
} }
} }
#[derive(Default)]
pub struct StringMatchOptions { pub struct StringMatchOptions {
pub case_insensitive: bool, pub case_insensitive: bool,
pub left_word: bool, pub left_word: bool,
pub right_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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -96,6 +96,7 @@ private:
void Submit(); void Submit();
void OnSubmitBtn(wxCommandEvent& event); void OnSubmitBtn(wxCommandEvent& event);
void OnCharHook(wxKeyEvent& event); void OnCharHook(wxKeyEvent& event);
void OnListBoxEvent(wxCommandEvent& event);
void UpdateHelpText(); void UpdateHelpText();
void HandleNormalFocus(wxFocusEvent& event); void HandleNormalFocus(wxFocusEvent& event);
void HandleMultilineFocus(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_BUTTON, &FormFrame::OnSubmitBtn, this, ID_Submit);
Bind(wxEVT_CHAR_HOOK, &FormFrame::OnCharHook, this, wxID_ANY); 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->SetClientSize(panel->GetBestSize());
this->CentreOnScreen(); 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); ((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 // Create the field wrapper
std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ListFieldWrapper((wxListBox*) choice)); std::unique_ptr<FieldWrapper> 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) { extern "C" void interop_show_form(FormMetadata * _metadata, void (*callback)(ValuePair *values, int size, void *data), void *data) {
// Setup high DPI support on Windows // Setup high DPI support on Windows
#ifdef __WXMSW__ #ifdef __WXMSW__

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Choice> = 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<Vec<Choice>> = 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,
}

View File

@ -17,6 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
pub mod choice;
pub mod clipboard; pub mod clipboard;
pub mod date; pub mod date;
pub mod echo; pub mod echo;

View File

@ -42,20 +42,12 @@ pub enum RenderResult {
Error(anyhow::Error), Error(anyhow::Error),
} }
#[derive(Default)]
pub struct Context<'a> { pub struct Context<'a> {
pub global_vars: Vec<&'a Variable>, pub global_vars: Vec<&'a Variable>,
pub templates: Vec<&'a Template>, 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)] #[derive(Debug, Clone, PartialEq)]
pub struct RenderOptions { pub struct RenderOptions {
pub casing_style: CasingStyle, pub casing_style: CasingStyle,
@ -77,23 +69,13 @@ pub enum CasingStyle {
Uppercase, Uppercase,
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct Template { pub struct Template {
pub ids: Vec<String>, pub ids: Vec<String>,
pub body: String, pub body: String,
pub vars: Vec<Variable>, pub vars: Vec<Variable>,
} }
impl Default for Template {
fn default() -> Self {
Self {
ids: Vec::new(),
body: "".to_string(),
vars: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Variable { pub struct Variable {
pub name: String, pub name: String,

View File

@ -1,6 +1,6 @@
[package] [package]
name = "espanso" name = "espanso"
version = "2.1.1-alpha" version = "2.1.2-alpha"
authors = ["Federico Terzi <federicoterzi96@gmail.com>"] authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
license = "GPL-3.0" license = "GPL-3.0"
description = "Cross-platform Text Expander written in Rust" description = "Cross-platform Text Expander written in Rust"

View File

@ -74,6 +74,7 @@ pub enum LogMode {
CleanAndAppend, CleanAndAppend,
} }
#[derive(Default)]
pub struct CliModuleArgs { pub struct CliModuleArgs {
pub config_store: Option<Box<dyn ConfigStore>>, pub config_store: Option<Box<dyn ConfigStore>>,
pub match_store: Option<Box<dyn MatchStore>>, pub match_store: Option<Box<dyn MatchStore>>,
@ -84,20 +85,6 @@ pub struct CliModuleArgs {
pub cli_args: Option<ArgMatches<'static>>, pub cli_args: Option<ArgMatches<'static>>,
} }
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 struct PathsOverrides {
pub config: Option<PathBuf>, pub config: Option<PathBuf>,
pub runtime: Option<PathBuf>, pub runtime: Option<PathBuf>,

View File

@ -81,6 +81,11 @@ impl<'a> ClipboardInjectorAdapter<'a> {
custom_combination custom_combination
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
vec![Key::Meta, Key::V] 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 { } else {
vec![Key::Control, Key::V] vec![Key::Control, Key::V]
}; };

View File

@ -58,11 +58,7 @@ fn convert_to_ui_menu_item(
espanso_engine::event::ui::MenuItem::Sub(sub) => { espanso_engine::event::ui::MenuItem::Sub(sub) => {
espanso_ui::menu::MenuItem::Sub(espanso_ui::menu::SubMenuItem { espanso_ui::menu::MenuItem::Sub(espanso_ui::menu::SubMenuItem {
label: sub.label.clone(), label: sub.label.clone(),
items: sub items: sub.items.iter().map(convert_to_ui_menu_item).collect(),
.items
.iter()
.map(|item| convert_to_ui_menu_item(item))
.collect(),
}) })
} }
espanso_engine::event::ui::MenuItem::Separator => espanso_ui::menu::MenuItem::Separator, espanso_engine::event::ui::MenuItem::Separator => espanso_ui::menu::MenuItem::Separator,

View File

@ -76,18 +76,11 @@ impl KeyStateStore {
} }
} }
#[derive(Default)]
struct KeyState { struct KeyState {
keys: HashMap<u32, KeyStatus>, keys: HashMap<u32, KeyStatus>,
} }
impl Default for KeyState {
fn default() -> Self {
Self {
keys: HashMap::new(),
}
}
}
struct KeyStatus { struct KeyStatus {
pressed_at: Option<Instant>, pressed_at: Option<Instant>,
} }

View File

@ -49,7 +49,9 @@ use crate::{
}, },
multiplex::MultiplexAdapter, multiplex::MultiplexAdapter,
render::{ render::{
extension::{clipboard::ClipboardAdapter, form::FormProviderAdapter}, extension::{
choice::ChoiceSelectorAdapter, clipboard::ClipboardAdapter, form::FormProviderAdapter,
},
RendererAdapter, RendererAdapter,
}, },
}, },
@ -198,6 +200,9 @@ pub fn initialize_and_spawn(
let shell_extension = espanso_render::extension::shell::ShellExtension::new(&paths.config); let shell_extension = espanso_render::extension::shell::ShellExtension::new(&paths.config);
let form_adapter = FormProviderAdapter::new(&modulo_form_ui); let form_adapter = FormProviderAdapter::new(&modulo_form_ui);
let form_extension = espanso_render::extension::form::FormExtension::new(&form_adapter); 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![ let renderer = espanso_render::create(vec![
&clipboard_extension, &clipboard_extension,
&date_extension, &date_extension,
@ -207,6 +212,7 @@ pub fn initialize_and_spawn(
&script_extension, &script_extension,
&shell_extension, &shell_extension,
&form_extension, &form_extension,
&choice_extension,
]); ]);
let renderer_adapter = RendererAdapter::new(&match_cache, &config_manager, &renderer); let renderer_adapter = RendererAdapter::new(&match_cache, &config_manager, &renderer);
let path_provider = PathProviderAdapter::new(&paths); let path_provider = PathProviderAdapter::new(&paths);

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<SearchItem> {
choices
.iter()
.map(|choice| SearchItem {
id: choice.id.to_string(),
label: choice.label.to_string(),
tag: None,
is_builtin: false,
})
.collect()
}

View File

@ -17,5 +17,6 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
pub mod choice;
pub mod clipboard; pub mod clipboard;
pub mod form; pub mod form;

View File

@ -31,6 +31,7 @@ pub fn patch_store(store: Box<dyn ConfigStore>) -> Box<dyn ConfigStore> {
fn get_builtin_patches() -> Vec<PatchDefinition> { fn get_builtin_patches() -> Vec<PatchDefinition> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
return vec![ return vec![
patches::win::brave::patch(),
patches::win::onenote_for_windows_10::patch(), patches::win::onenote_for_windows_10::patch(),
patches::win::vscode_win::patch(), patches::win::vscode_win::patch(),
]; ];

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
},
))
},
}
}

View File

@ -17,5 +17,6 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
pub mod brave;
pub mod onenote_for_windows_10; pub mod onenote_for_windows_10;
pub mod vscode_win; pub mod vscode_win;

View File

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

View File

@ -8,7 +8,7 @@ BASE_DIR=$(pwd)
mkdir -p $TOOL_DIR 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" echo "Skipping download of linuxdeploy"
else else
echo "Downloading linuxdeploy tool" echo "Downloading linuxdeploy tool"
@ -16,12 +16,36 @@ else
chmod +x $TOOL_DIR/linuxdeploy*.AppImage chmod +x $TOOL_DIR/linuxdeploy*.AppImage
fi 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" rm -Rf "$TARGET_DIR"
mkdir -p $OUTPUT_DIR mkdir -p $OUTPUT_DIR
mkdir -p $BUILD_DIR mkdir -p $BUILD_DIR
echo Building AppImage into $OUTPUT_DIR echo Building AppImage into $OUTPUT_DIR
pushd $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 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 popd

12
snap/hooks/remove Executable file
View File

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

View File

@ -1,5 +1,5 @@
name: espanso name: espanso
version: 2.1.1-alpha version: 2.1.2-alpha
summary: A Cross-platform Text Expander written in Rust summary: A Cross-platform Text Expander written in Rust
description: | description: |
espanso is a Cross-platform, Text Expander written in Rust. espanso is a Cross-platform, Text Expander written in Rust.