Merge pull request #799 from federico-terzi/dev

v2.0.3-alpha release
This commit is contained in:
Federico Terzi 2021-10-17 17:21:20 +02:00 committed by GitHub
commit b7e91d9b5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 432 additions and 140 deletions

View File

@ -25,11 +25,14 @@ jobs:
- name: Install Linux dependencies
if: ${{ runner.os == 'Linux' }}
run: |
sudo apt install libx11-dev libxtst-dev libxkbcommon-dev libdbus-1-dev libwxgtk3.0-gtk3-dev
sudo apt-get update
sudo apt-get install -y libx11-dev libxtst-dev libxkbcommon-dev libdbus-1-dev libwxgtk3.0-gtk3-dev
- name: Check clippy
run: |
rustup component add clippy
cargo clippy -- -D warnings
env:
MACOSX_DEPLOYMENT_TARGET: "10.13"
- name: Install cargo-make
run: |
cargo install --force cargo-make
@ -49,7 +52,8 @@ jobs:
cargo fmt --all -- --check
- name: Install Linux dependencies
run: |
sudo apt install libxkbcommon-dev libwxgtk3.0-gtk3-dev libdbus-1-dev
sudo apt-get update
sudo apt-get install -y libxkbcommon-dev libwxgtk3.0-gtk3-dev libdbus-1-dev
- name: Check clippy
run: |
rustup component add clippy

View File

@ -131,8 +131,12 @@ jobs:
cargo install --force cargo-make
- name: Test
run: cargo make test-binary --profile release
env:
MACOSX_DEPLOYMENT_TARGET: "10.13"
- name: Build
run: cargo make create-bundle --profile release
env:
MACOSX_DEPLOYMENT_TARGET: "10.13"
- name: Create ZIP archive
run: |
ditto -c -k --sequesterRsrc --keepParent target/mac/Espanso.app Espanso-Mac-Intel.zip

2
Cargo.lock generated
View File

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

View File

@ -25,6 +25,11 @@ pub enum InputEvent {
Mouse(MouseEvent),
Keyboard(KeyboardEvent),
HotKey(HotKeyEvent),
// Special event type only used on macOS
// This is sent after a global keyboard shortcut is released
// See https://github.com/federico-terzi/espanso/issues/791
AllModifiersReleased,
}
#[derive(Debug, PartialEq)]

View File

@ -241,6 +241,13 @@ impl From<RawInputEvent> for Option<InputEvent> {
INPUT_EVENT_TYPE_KEYBOARD => {
let (key, variant) = key_code_to_key(raw.key_code);
// When a global keyboard shortcut is relased, the callback returns an event with keycode 0
// and status 0.
// We need to handle it for this reason: https://github.com/federico-terzi/espanso/issues/791
if raw.key_code == 0 && raw.status == 0 {
return Some(InputEvent::AllModifiersReleased);
}
let value = if raw.buffer_len > 0 {
let raw_string_result =
CStr::from_bytes_with_nul(&raw.buffer[..((raw.buffer_len + 1) as usize)]);

View File

@ -116,7 +116,7 @@ LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM w
{
// We only want KEY UP AND KEY DOWN events
if (raw->data.keyboard.Message != WM_KEYDOWN && raw->data.keyboard.Message != WM_KEYUP &&
raw->data.keyboard.Message != WM_SYSKEYDOWN)
raw->data.keyboard.Message != WM_SYSKEYDOWN && raw->data.keyboard.Message != WM_SYSKEYUP)
{
return 0;
}
@ -156,9 +156,10 @@ LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM w
std::vector<BYTE> lpKeyState(256);
if (GetKeyboardState(lpKeyState.data()))
{
// This flag is needed to avoid chaning the keyboard state for some layouts.
// Refer to issue: https://github.com/federico-terzi/espanso/issues/86
UINT flags = 1 << 2;
// This flag is needed to avoid changing the keyboard state for some layouts.
// The 1 << 2 (setting bit 2) part is needed due to this issue: https://github.com/federico-terzi/espanso/issues/86
// while the 1 (setting bit 0) part is needed due to this issue: https://github.com/federico-terzi/espanso/issues/552
UINT flags = 1 << 2 | 1;
int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), reinterpret_cast<LPWSTR>(event.buffer), (sizeof(event.buffer)/sizeof(event.buffer[0])) - 1, flags, variables->current_keyboard_layout);
@ -166,6 +167,15 @@ LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM w
if (result >= 1)
{
event.buffer_len = result;
// Filter out the value if the key was pressed while the ALT key was down
// but not if AltGr is down (which is a shortcut to ALT+CTRL on some keyboards, such
// as the italian one).
// This is needed in conjunction with the fix for: https://github.com/federico-terzi/espanso/issues/725
if ((lpKeyState[VK_MENU] & 0x80) != 0 && (lpKeyState[VK_CONTROL] & 0x80) == 0) {
memset(event.buffer, 0, sizeof(event.buffer));
event.buffer_len = 0;
}
}
else
{

View File

@ -85,6 +85,14 @@ pub struct DiscardPreviousEvent {
pub minimum_source_id: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DiscardBetweenEvent {
// All Events with a source_id between start_id (included) and end_id (excluded)
// will be discarded
pub start_id: u32,
pub end_id: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SecureInputEnabledEvent {
pub app_name: String,

View File

@ -71,6 +71,7 @@ pub enum EventType {
ImageResolved(internal::ImageResolvedEvent),
MatchInjected,
DiscardPrevious(internal::DiscardPreviousEvent),
DiscardBetween(internal::DiscardBetweenEvent),
Undo(internal::UndoEvent),
Disabled,

View File

@ -48,7 +48,10 @@ impl<'a> Funnel for DefaultFunnel<'a> {
.expect("invalid source index returned by select operation");
// Receive (and convert) the event
let event = source.receive(op);
if let Some(event) = source.receive(op) {
FunnelResult::Event(event)
} else {
FunnelResult::Skipped
}
}
}

View File

@ -27,7 +27,7 @@ mod default;
pub trait Source<'a> {
fn register(&'a self, select: &mut Select<'a>) -> usize;
fn receive(&'a self, op: SelectedOperation) -> Event;
fn receive(&'a self, op: SelectedOperation) -> Option<Event>;
}
pub trait Funnel {
@ -36,6 +36,7 @@ pub trait Funnel {
pub enum FunnelResult {
Event(Event),
Skipped,
EndOfStream,
}

View File

@ -68,6 +68,9 @@ impl<'a> Engine<'a> {
debug!("end of stream received");
return ExitMode::Exit;
}
FunnelResult::Skipped => {
// This event has been skipped, no need to handle it
}
}
}
}

View File

@ -25,16 +25,16 @@ use super::{
cause::CauseCompensateMiddleware,
cursor_hint::CursorHintMiddleware,
delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider},
discard::EventsDiscardMiddleware,
markdown::MarkdownMiddleware,
match_select::MatchSelectMiddleware,
matcher::MatcherMiddleware,
multiplex::MultiplexMiddleware,
past_discard::PastEventsDiscardMiddleware,
render::RenderMiddleware,
},
DisableOptions, EnabledStatusProvider, MatchFilter, MatchInfoProvider, MatchProvider,
MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware, Multiplexer, PathProvider,
Processor, Renderer, UndoEnabledProvider,
MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware, ModifierStateProvider,
Multiplexer, PathProvider, Processor, Renderer, UndoEnabledProvider,
};
use crate::{
event::{Event, EventType},
@ -69,18 +69,27 @@ impl<'a> DefaultProcessor<'a> {
match_provider: &'a dyn MatchProvider,
undo_enabled_provider: &'a dyn UndoEnabledProvider,
enabled_status_provider: &'a dyn EnabledStatusProvider,
modifier_state_provider: &'a dyn ModifierStateProvider,
) -> DefaultProcessor<'a> {
Self {
event_queue: VecDeque::new(),
middleware: vec![
Box::new(PastEventsDiscardMiddleware::new()),
Box::new(EventsDiscardMiddleware::new()),
Box::new(DisableMiddleware::new(disable_options)),
Box::new(IconStatusMiddleware::new()),
Box::new(MatcherMiddleware::new(matchers, matcher_options_provider)),
Box::new(MatcherMiddleware::new(
matchers,
matcher_options_provider,
modifier_state_provider,
)),
Box::new(SuppressMiddleware::new(enabled_status_provider)),
Box::new(ContextMenuMiddleware::new()),
Box::new(HotKeyMiddleware::new()),
Box::new(MatchSelectMiddleware::new(match_filter, match_selector)),
Box::new(MatchSelectMiddleware::new(
match_filter,
match_selector,
event_sequence_provider,
)),
Box::new(CauseCompensateMiddleware::new()),
Box::new(MultiplexMiddleware::new(multiplexer)),
Box::new(RenderMiddleware::new(renderer)),

View File

@ -24,42 +24,54 @@ use log::trace;
use super::super::Middleware;
use crate::event::{Event, EventType, SourceId};
/// This middleware discards all events that have a source_id smaller than its
/// configured threshold. This useful to discard past events that might have
/// been stuck in the event queue for too long.
pub struct PastEventsDiscardMiddleware {
source_id_threshold: RefCell<SourceId>,
/// This middleware discards all events that have a source_id between
/// the given maximum and minimum.
/// This useful to discard past events that might have been stuck in the
/// event queue for too long, or events generated while the search bar was open.
pub struct EventsDiscardMiddleware {
min_id_threshold: RefCell<SourceId>,
max_id_threshold: RefCell<SourceId>,
}
impl PastEventsDiscardMiddleware {
impl EventsDiscardMiddleware {
pub fn new() -> Self {
Self {
source_id_threshold: RefCell::new(0),
min_id_threshold: RefCell::new(0),
max_id_threshold: RefCell::new(0),
}
}
}
impl Middleware for PastEventsDiscardMiddleware {
impl Middleware for EventsDiscardMiddleware {
fn name(&self) -> &'static str {
"past_discard"
"discard"
}
fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
let mut source_id_threshold = self.source_id_threshold.borrow_mut();
let mut min_id_threshold = self.min_id_threshold.borrow_mut();
let mut max_id_threshold = self.max_id_threshold.borrow_mut();
// Filter out previous events
if event.source_id < *source_id_threshold {
if event.source_id < *max_id_threshold && event.source_id >= *min_id_threshold {
trace!("discarding previous event: {:?}", event);
return Event::caused_by(event.source_id, EventType::NOOP);
}
// Update the minimum threshold
// Update the thresholds
if let EventType::DiscardPrevious(m_event) = &event.etype {
trace!(
"updating minimum source id threshold for events to: {}",
"updating discard max_id_threshold threshold for events to: {}",
m_event.minimum_source_id
);
*source_id_threshold = m_event.minimum_source_id;
*max_id_threshold = m_event.minimum_source_id;
} else if let EventType::DiscardBetween(m_event) = &event.etype {
trace!(
"updating discard thresholds for events to: max={} min={}",
m_event.end_id,
m_event.start_id
);
*max_id_threshold = m_event.end_id;
*min_id_threshold = m_event.start_id;
}
event

View File

@ -20,7 +20,13 @@
use log::{debug, error};
use super::super::Middleware;
use crate::event::{internal::MatchSelectedEvent, Event, EventType};
use crate::{
event::{
internal::{DiscardBetweenEvent, MatchSelectedEvent},
Event, EventType,
},
process::EventSequenceProvider,
};
pub trait MatchFilter {
fn filter_active(&self, matches_ids: &[i32]) -> Vec<i32>;
@ -33,13 +39,19 @@ pub trait MatchSelector {
pub struct MatchSelectMiddleware<'a> {
match_filter: &'a dyn MatchFilter,
match_selector: &'a dyn MatchSelector,
event_sequence_provider: &'a dyn EventSequenceProvider,
}
impl<'a> MatchSelectMiddleware<'a> {
pub fn new(match_filter: &'a dyn MatchFilter, match_selector: &'a dyn MatchSelector) -> Self {
pub fn new(
match_filter: &'a dyn MatchFilter,
match_selector: &'a dyn MatchSelector,
event_sequence_provider: &'a dyn EventSequenceProvider,
) -> Self {
Self {
match_filter,
match_selector,
event_sequence_provider,
}
}
}
@ -49,7 +61,7 @@ impl<'a> Middleware for MatchSelectMiddleware<'a> {
"match_select"
}
fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
if let EventType::MatchesDetected(m_event) = event.etype {
let matches_ids: Vec<i32> = m_event.matches.iter().map(|m| m.id).collect();
@ -75,7 +87,10 @@ impl<'a> Middleware for MatchSelectMiddleware<'a> {
}
}
_ => {
let start_event_id = self.event_sequence_provider.get_next_id();
// Multiple matches, we need to ask the user which one to use
let next_event =
if let Some(selected_id) = self.match_selector.select(&valid_ids, m_event.is_search) {
let m = m_event.matches.into_iter().find(|m| m.id == selected_id);
if let Some(m) = m {
@ -90,7 +105,22 @@ impl<'a> Middleware for MatchSelectMiddleware<'a> {
} else {
debug!("MatchSelectMiddleware did not receive any match selection");
Event::caused_by(event.source_id, EventType::NOOP)
}
};
let end_event_id = self.event_sequence_provider.get_next_id();
// We want to prevent espanso from "stacking up" events while the search bar is open,
// therefore we filter out all events that were generated while the search bar was open.
// See also: https://github.com/federico-terzi/espanso/issues/781
dispatch(Event::caused_by(
event.source_id,
EventType::DiscardBetween(DiscardBetweenEvent {
start_id: start_event_id,
end_id: end_event_id,
}),
));
next_event
}
};
}

View File

@ -57,18 +57,32 @@ pub trait MatcherMiddlewareConfigProvider {
fn max_history_size(&self) -> usize;
}
pub trait ModifierStateProvider {
fn get_modifier_state(&self) -> ModifierState;
}
#[derive(Debug, Clone)]
pub struct ModifierState {
pub is_ctrl_down: bool,
pub is_alt_down: bool,
pub is_meta_down: bool,
}
pub struct MatcherMiddleware<'a, State> {
matchers: &'a [&'a dyn Matcher<'a, State>],
matcher_states: RefCell<VecDeque<Vec<State>>>,
max_history_size: usize,
modifier_status_provider: &'a dyn ModifierStateProvider,
}
impl<'a, State> MatcherMiddleware<'a, State> {
pub fn new(
matchers: &'a [&'a dyn Matcher<'a, State>],
options_provider: &'a dyn MatcherMiddlewareConfigProvider,
modifier_status_provider: &'a dyn ModifierStateProvider,
) -> Self {
let max_history_size = options_provider.max_history_size();
@ -76,6 +90,7 @@ impl<'a, State> MatcherMiddleware<'a, State> {
matchers,
matcher_states: RefCell::new(VecDeque::new()),
max_history_size,
modifier_status_provider,
}
}
}
@ -101,6 +116,17 @@ impl<'a, State> Middleware for MatcherMiddleware<'a, State> {
matcher_states.pop_back();
return event;
}
// We need to filter out some keyboard events if they are generated
// while some modifier keys are pressed, otherwise we could have
// wrong matches being detected.
// See: https://github.com/federico-terzi/espanso/issues/725
if should_skip_key_event_due_to_modifier_press(
&self.modifier_status_provider.get_modifier_state(),
) {
trace!("skipping keyboard event because incompatible modifiers are pressed");
return event;
}
}
// Some keys (such as the arrow keys) and mouse clicks prevent espanso from building
@ -161,6 +187,17 @@ fn is_event_of_interest(event_type: &EventType) -> bool {
// Skip non-press events
false
} else {
// Skip linux Keyboard (XKB) Extension function and modifier keys
// In hex, they have the byte 3 = 0xfe
// See list in "keysymdef.h" file
if cfg!(target_os = "linux") {
if let Key::Other(raw_code) = &keyboard_event.key {
if (65025..=65276).contains(raw_code) {
return false;
}
}
}
// Skip modifier keys
!matches!(
keyboard_event.key,
@ -205,4 +242,16 @@ fn is_invalidating_event(event_type: &EventType) -> bool {
}
}
fn should_skip_key_event_due_to_modifier_press(modifier_state: &ModifierState) -> bool {
if cfg!(target_os = "macos") {
modifier_state.is_meta_down
} else if cfg!(target_os = "windows") {
false
} else if cfg!(target_os = "linux") {
modifier_state.is_alt_down || modifier_state.is_meta_down
} else {
unreachable!()
}
}
// TODO: test

View File

@ -23,6 +23,7 @@ pub mod context_menu;
pub mod cursor_hint;
pub mod delay_modifiers;
pub mod disable;
pub mod discard;
pub mod exit;
pub mod hotkey;
pub mod icon_status;
@ -31,7 +32,6 @@ pub mod markdown;
pub mod match_select;
pub mod matcher;
pub mod multiplex;
pub mod past_discard;
pub mod render;
pub mod search;
pub mod suppress;

View File

@ -39,7 +39,8 @@ pub use middleware::disable::DisableOptions;
pub use middleware::image_resolve::PathProvider;
pub use middleware::match_select::{MatchFilter, MatchSelector};
pub use middleware::matcher::{
MatchResult, Matcher, MatcherEvent, MatcherMiddlewareConfigProvider,
MatchResult, Matcher, MatcherEvent, MatcherMiddlewareConfigProvider, ModifierState,
ModifierStateProvider,
};
pub use middleware::multiplex::Multiplexer;
pub use middleware::render::{Renderer, RendererError};
@ -63,6 +64,7 @@ pub fn default<'a, MatcherState>(
match_provider: &'a dyn MatchProvider,
undo_enabled_provider: &'a dyn UndoEnabledProvider,
enabled_status_provider: &'a dyn EnabledStatusProvider,
modifier_state_provider: &'a dyn ModifierStateProvider,
) -> impl Processor + 'a {
default::DefaultProcessor::new(
matchers,
@ -79,5 +81,6 @@ pub fn default<'a, MatcherState>(
match_provider,
undo_enabled_provider,
enabled_status_provider,
modifier_state_provider,
)
}

View File

@ -29,4 +29,6 @@ extern "C" {
pub fn mac_utils_prompt_accessibility() -> i32;
pub fn mac_utils_transition_to_foreground_app();
pub fn mac_utils_transition_to_background_app();
pub fn mac_utils_start_headless_eventloop();
pub fn mac_utils_exit_headless_eventloop();
}

View File

@ -113,6 +113,20 @@ pub fn convert_to_background_app() {
}
}
#[cfg(target_os = "macos")]
pub fn start_headless_eventloop() {
unsafe {
ffi::mac_utils_start_headless_eventloop();
}
}
#[cfg(target_os = "macos")]
pub fn exit_headless_eventloop() {
unsafe {
ffi::mac_utils_exit_headless_eventloop();
}
}
#[cfg(test)]
#[cfg(target_os = "macos")]
mod tests {

View File

@ -40,4 +40,8 @@ extern "C" void mac_utils_transition_to_foreground_app();
// When called, convert the current process to a background app (hide the dock icon).
extern "C" void mac_utils_transition_to_background_app();
// Start and stop a "headless" eventloop to receive NSApplication events.
extern "C" void mac_utils_start_headless_eventloop();
extern "C" void mac_utils_exit_headless_eventloop();
#endif //ESPANSO_MAC_UTILS_H

View File

@ -89,3 +89,15 @@ void mac_utils_transition_to_background_app() {
ProcessSerialNumber psn = { 0, kCurrentProcess };
TransformProcessType(&psn, kProcessTransformToUIElementApplication);
}
void mac_utils_start_headless_eventloop() {
NSApplication * application = [NSApplication sharedApplication];
[NSApp run];
}
void mac_utils_exit_headless_eventloop() {
dispatch_async(dispatch_get_main_queue(), ^(void) {
[NSApp stop:nil];
[NSApp abortModal];
});
}

View File

@ -457,5 +457,22 @@ fn build_native() {
}
fn main() {
println!("cargo:rerun-if-changed=src/x11/native/native.h");
println!("cargo:rerun-if-changed=src/sys/interop/interop.h");
println!("cargo:rerun-if-changed=src/sys/form/form.cpp");
println!("cargo:rerun-if-changed=src/sys/common/mac.h");
println!("cargo:rerun-if-changed=src/sys/common/mac.mm");
println!("cargo:rerun-if-changed=src/sys/common/common.h");
println!("cargo:rerun-if-changed=src/sys/common/common.cpp");
println!("cargo:rerun-if-changed=src/sys/welcome/welcome_gui.h");
println!("cargo:rerun-if-changed=src/sys/welcome/welcome_gui.cpp");
println!("cargo:rerun-if-changed=src/sys/welcome/welcome.cpp");
println!("cargo:rerun-if-changed=src/sys/troubleshooting/troubleshooting_gui.h");
println!("cargo:rerun-if-changed=src/sys/troubleshooting/troubleshooting_gui.cpp");
println!("cargo:rerun-if-changed=src/sys/troubleshooting/troubleshooting.cpp");
println!("cargo:rerun-if-changed=src/sys/search/search.cpp");
println!("cargo:rerun-if-changed=src/sys/wizard/wizard.cpp");
println!("cargo:rerun-if-changed=src/sys/wizard/wizard_gui.cpp");
println!("cargo:rerun-if-changed=src/sys/wizard/wizard_gui.h");
build_native();
}

View File

@ -26,9 +26,8 @@
#include "mac.h"
#endif
void setFrameIcon(const char * iconPath, wxFrame * frame) {
if (iconPath) {
wxString iconPath(iconPath);
void setFrameIcon(wxString iconPath, wxFrame * frame) {
if (!iconPath.IsEmpty()) {
wxBitmapType imgType = wxICON_DEFAULT_TYPE;
#ifdef __WXMSW__

View File

@ -27,7 +27,7 @@
#include <wx/wx.h>
#endif
void setFrameIcon(const char * iconPath, wxFrame * frame);
void setFrameIcon(wxString iconPath, wxFrame * frame);
void Activate(wxFrame * frame);

View File

@ -102,8 +102,8 @@ enum
bool FormApp::OnInit()
{
FormFrame *frame = new FormFrame(formMetadata->windowTitle, wxPoint(50, 50), wxSize(450, 340) );
setFrameIcon(formMetadata->iconPath, frame);
FormFrame *frame = new FormFrame(wxString::FromUTF8(formMetadata->windowTitle), wxPoint(50, 50), wxSize(450, 340) );
setFrameIcon(wxString::FromUTF8(formMetadata->iconPath), frame);
frame->Show( true );
Activate(frame);

View File

@ -166,7 +166,7 @@ private:
bool SearchApp::OnInit()
{
SearchFrame *frame = new SearchFrame(searchMetadata->windowTitle, wxPoint(50, 50), wxSize(450, 340));
SearchFrame *frame = new SearchFrame(wxString::FromUTF8(searchMetadata->windowTitle), wxPoint(50, 50), wxSize(450, 340));
frame->Show(true);
SetupWindowStyle(frame);
Activate(frame);
@ -198,7 +198,7 @@ SearchFrame::SearchFrame(const wxString &title, const wxPoint &pos, const wxSize
iconPanel = nullptr;
if (searchMetadata->iconPath)
{
wxString iconPath = wxString(searchMetadata->iconPath);
wxString iconPath = wxString::FromUTF8(searchMetadata->iconPath);
if (wxFileExists(iconPath))
{
wxBitmap bitmap = wxBitmap(iconPath, wxBITMAP_TYPE_PNG);

View File

@ -81,7 +81,7 @@ public:
if (error_set_metadata->errors[i].level == ERROR_METADATA_LEVEL_WARNING) {
level = wxT("WARNING");
}
wxString error_text = wxString::Format(wxT("[%s] %s\n"), level, error_set_metadata->errors[i].message);
wxString error_text = wxString::Format(wxT("[%s] %s\n"), level, wxString::FromUTF8(error_set_metadata->errors[i].message));
errors_text.Append(error_text);
}
@ -171,7 +171,7 @@ bool TroubleshootingApp::OnInit()
if (troubleshooting_metadata->window_icon_path)
{
setFrameIcon(troubleshooting_metadata->window_icon_path, frame);
setFrameIcon(wxString::FromUTF8(troubleshooting_metadata->window_icon_path), frame);
}
frame->Show(true);

View File

@ -54,7 +54,7 @@ DerivedWelcomeFrame::DerivedWelcomeFrame(wxWindow *parent)
if (welcome_metadata->tray_image_path)
{
wxBitmap trayBitmap = wxBitmap(welcome_metadata->tray_image_path, wxBITMAP_TYPE_PNG);
wxBitmap trayBitmap = wxBitmap(wxString::FromUTF8(welcome_metadata->tray_image_path), wxBITMAP_TYPE_PNG);
this->tray_bitmap->SetBitmap(trayBitmap);
#ifdef __WXOSX__
this->tray_info_label->SetLabel("You should see the espanso icon on the status bar:");
@ -91,7 +91,7 @@ bool WelcomeApp::OnInit()
if (welcome_metadata->window_icon_path)
{
setFrameIcon(welcome_metadata->window_icon_path, frame);
setFrameIcon(wxString::FromUTF8(welcome_metadata->window_icon_path), frame);
}
frame->Show(true);

View File

@ -45,7 +45,7 @@
<property name="minimum_size"></property>
<property name="name">WelcomeFrame</property>
<property name="pos"></property>
<property name="size">521,544</property>
<property name="size">521,597</property>
<property name="style">wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU</property>
<property name="subclass">; ; forward_declare</property>
<property name="title">Espanso is running!</property>

View File

@ -54,7 +54,7 @@ class WelcomeFrame : public wxFrame
public:
WelcomeFrame( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Espanso is running!"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 521,544 ), long style = wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU|wxTAB_TRAVERSAL );
WelcomeFrame( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Espanso is running!"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 521,597 ), long style = wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU|wxTAB_TRAVERSAL );
~WelcomeFrame();

View File

@ -130,7 +130,7 @@ DerivedFrame::DerivedFrame(wxWindow *parent)
if (wizard_metadata->welcome_image_path)
{
wxBitmap welcomeBitmap = wxBitmap(wizard_metadata->welcome_image_path, wxBITMAP_TYPE_PNG);
wxBitmap welcomeBitmap = wxBitmap(wxString::FromUTF8(wizard_metadata->welcome_image_path), wxBITMAP_TYPE_PNG);
this->welcome_image->SetBitmap(welcomeBitmap);
}
@ -140,12 +140,12 @@ DerivedFrame::DerivedFrame(wxWindow *parent)
if (wizard_metadata->accessibility_image_1_path)
{
wxBitmap accessiblityImage1 = wxBitmap(wizard_metadata->accessibility_image_1_path, wxBITMAP_TYPE_PNG);
wxBitmap accessiblityImage1 = wxBitmap(wxString::FromUTF8(wizard_metadata->accessibility_image_1_path), wxBITMAP_TYPE_PNG);
this->accessibility_image1->SetBitmap(accessiblityImage1);
}
if (wizard_metadata->accessibility_image_2_path)
{
wxBitmap accessiblityImage2 = wxBitmap(wizard_metadata->accessibility_image_2_path, wxBITMAP_TYPE_PNG);
wxBitmap accessiblityImage2 = wxBitmap(wxString::FromUTF8(wizard_metadata->accessibility_image_2_path), wxBITMAP_TYPE_PNG);
this->accessibility_image2->SetBitmap(accessiblityImage2);
}
@ -380,7 +380,7 @@ bool WizardApp::OnInit()
if (wizard_metadata->window_icon_path)
{
setFrameIcon(wizard_metadata->window_icon_path, frame);
setFrameIcon(wxString::FromUTF8(wizard_metadata->window_icon_path), frame);
}
frame->Show(true);

View File

@ -24,7 +24,7 @@ use std::{
};
use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
use log::{info, warn};
use log::{error, info};
use thiserror::Error;
#[allow(clippy::upper_case_acronyms)]
@ -191,24 +191,16 @@ impl Extension for ShellExtension {
info!("this debug information was shown because the 'debug' option is true.");
}
let ignore_error = params
.get("ignore_error")
.and_then(|v| v.as_bool())
.copied()
.unwrap_or(false);
if !output.status.success() || !error_str.trim().is_empty() {
warn!(
if !output.status.success() {
error!(
"shell command exited with code: {} and error: {}",
output.status, error_str
);
if !ignore_error {
return ExtensionResult::Error(
ShellExtensionError::ExecutionError(error_str.to_string()).into(),
);
}
}
let trim = params
.get("trim")
@ -388,24 +380,4 @@ mod tests {
ExtensionResult::Error(_)
));
}
#[test]
#[cfg(not(target_os = "windows"))]
fn ignore_error() {
let extension = ShellExtension::new(&PathBuf::new());
let param = vec![
("cmd".to_string(), Value::String("exit 1".to_string())),
("ignore_error".to_string(), Value::Bool(true)),
]
.into_iter()
.collect::<Params>();
assert_eq!(
extension
.calculate(&Default::default(), &Default::default(), &param)
.into_success()
.unwrap(),
ExtensionOutput::Single("".to_string())
);
}
}

View File

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

View File

@ -32,8 +32,8 @@ use crate::{
common_flags::*,
exit_code::{
DAEMON_ALREADY_RUNNING, DAEMON_FATAL_CONFIG_ERROR, DAEMON_GENERAL_ERROR,
DAEMON_LEGACY_ALREADY_RUNNING, DAEMON_SUCCESS, WORKER_EXIT_ALL_PROCESSES, WORKER_RESTART,
WORKER_SUCCESS,
DAEMON_LEGACY_ALREADY_RUNNING, DAEMON_SUCCESS, WORKER_ERROR_EXIT_NO_CODE,
WORKER_EXIT_ALL_PROCESSES, WORKER_RESTART, WORKER_SUCCESS,
},
ipc::{create_ipc_client_to_worker, IPCEvent},
lock::{acquire_daemon_lock, acquire_legacy_lock, acquire_worker_lock},
@ -271,6 +271,10 @@ fn spawn_worker(
.send(code)
.expect("unable to forward worker exit code");
}
} else {
exit_notify
.send(WORKER_ERROR_EXIT_NO_CODE)
.expect("unable to forward worker exit code");
}
}
})

View File

@ -20,6 +20,7 @@
use std::process::Command;
use anyhow::Result;
use std::process::ExitStatus;
use thiserror::Error;
use crate::cli::util::CommandExt;
@ -31,8 +32,7 @@ pub fn launch_daemon(paths_overrides: &PathsOverrides) -> Result<()> {
command.args(&["daemon"]);
command.with_paths_overrides(paths_overrides);
let mut child = command.spawn()?;
let result = child.wait()?;
let result = spawn_and_wait(command)?;
if result.success() {
Ok(())
@ -46,3 +46,33 @@ pub enum DaemonError {
#[error("unexpected error, 'espanso daemon' returned a non-zero exit code.")]
NonZeroExitCode,
}
#[cfg(not(target_os = "macos"))]
fn spawn_and_wait(mut command: Command) -> Result<ExitStatus> {
let mut child = command.spawn()?;
Ok(child.wait()?)
}
// On macOS, if we simply wait for the daemon process to terminate, the application will
// appear as "Not Responding" after a few seconds, even though it's working correctly.
// To avoid this undesirable behavior, we spawn an headless eventloop so that the
// launcher looks "alive", while waiting for the daemon
#[cfg(target_os = "macos")]
fn spawn_and_wait(mut command: Command) -> Result<ExitStatus> {
let mut child = command.spawn()?;
let result = std::thread::Builder::new()
.name("daemon-monitor-thread".to_owned())
.spawn(move || {
let results = child.wait();
espanso_mac_utils::exit_headless_eventloop();
results
})?;
espanso_mac_utils::start_headless_eventloop();
let thread_result = result.join().expect("unable to join daemon-monitor-thread");
Ok(thread_result?)
}

View File

@ -168,6 +168,13 @@ impl<'a> espanso_engine::process::UndoEnabledProvider for ConfigManager<'a> {
return false;
}
// Because we cannot filter out espanso-generated events when using the X11 record injection
// method, we need to disable undo_backspace to avoid looping (espanso picks up its own
// injections, causing the program to misbehave)
if cfg!(target_os = "linux") && self.active().disable_x11_fast_inject() {
return false;
}
self.active().undo_backspace()
}
}

View File

@ -37,12 +37,12 @@ impl<'a> funnel::Source<'a> for DetectSource {
select.recv(&self.receiver)
}
fn receive(&self, op: SelectedOperation) -> Event {
fn receive(&self, op: SelectedOperation) -> Option<Event> {
let (input_event, source_id) = op
.recv(&self.receiver)
.expect("unable to select data from DetectSource receiver");
match input_event {
InputEvent::Keyboard(keyboard_event) => Event {
InputEvent::Keyboard(keyboard_event) => Some(Event {
source_id,
etype: EventType::Keyboard(KeyboardEvent {
key: convert_to_engine_key(keyboard_event.key),
@ -50,20 +50,21 @@ impl<'a> funnel::Source<'a> for DetectSource {
status: convert_to_engine_status(keyboard_event.status),
variant: keyboard_event.variant.map(convert_to_engine_variant),
}),
},
InputEvent::Mouse(mouse_event) => Event {
}),
InputEvent::Mouse(mouse_event) => Some(Event {
source_id,
etype: EventType::Mouse(MouseEvent {
status: convert_to_engine_status(mouse_event.status),
button: convert_to_engine_mouse_button(mouse_event.button),
}),
},
InputEvent::HotKey(hotkey_event) => Event {
}),
InputEvent::HotKey(hotkey_event) => Some(Event {
source_id,
etype: EventType::HotKey(HotKeyEvent {
hotkey_id: hotkey_event.hotkey_id,
}),
},
}),
InputEvent::AllModifiersReleased => None,
}
}
}

View File

@ -45,13 +45,13 @@ impl<'a> funnel::Source<'a> for ExitSource<'a> {
select.recv(&self.exit_signal)
}
fn receive(&self, op: SelectedOperation) -> Event {
fn receive(&self, op: SelectedOperation) -> Option<Event> {
let mode = op
.recv(&self.exit_signal)
.expect("unable to select data from ExitSource receiver");
Event {
Some(Event {
source_id: self.sequencer.next_id(),
etype: EventType::ExitRequested(mode),
}
})
}
}

View File

@ -81,6 +81,8 @@ pub fn init_and_spawn(
// Update the modifiers state
if let Some((modifier, is_pressed)) = get_modifier_status(&event) {
modifier_state_store_clone.update_state(modifier, is_pressed);
} else if let InputEvent::AllModifiersReleased = &event {
modifier_state_store_clone.clear_state();
}
// Update the key state (if needed)

View File

@ -91,6 +91,13 @@ impl ModifierStateStore {
}
}
}
pub fn clear_state(&self) {
let mut state = self.state.lock().expect("unable to obtain modifier state");
for (_, status) in &mut state.modifiers {
status.release();
}
}
}
struct ModifiersState {
@ -142,3 +149,44 @@ impl ModifierStatusProvider for ModifierStateStore {
self.is_any_conflicting_modifier_pressed()
}
}
impl espanso_engine::process::ModifierStateProvider for ModifierStateStore {
fn get_modifier_state(&self) -> espanso_engine::process::ModifierState {
let mut state = self.state.lock().expect("unable to obtain modifier state");
let mut is_ctrl_down = false;
let mut is_alt_down = false;
let mut is_meta_down = false;
for (modifier, status) in &mut state.modifiers {
if status.is_outdated() {
warn!(
"detected outdated modifier records for {:?}, releasing the state",
modifier
);
status.release();
}
if status.is_pressed() {
match modifier {
Modifier::Ctrl => {
is_ctrl_down = true;
}
Modifier::Alt => {
is_alt_down = true;
}
Modifier::Meta => {
is_meta_down = true;
}
_ => {}
}
}
}
espanso_engine::process::ModifierState {
is_ctrl_down,
is_alt_down,
is_meta_down,
}
}
}

View File

@ -50,13 +50,13 @@ impl<'a> funnel::Source<'a> for SecureInputSource<'a> {
}
}
fn receive(&self, op: SelectedOperation) -> Event {
fn receive(&self, op: SelectedOperation) -> Option<Event> {
if cfg!(target_os = "macos") {
let si_event = op
.recv(&self.receiver)
.expect("unable to select data from SecureInputSource receiver");
Event {
Some(Event {
source_id: self.sequencer.next_id(),
etype: match si_event {
SecureInputEvent::Disabled => EventType::SecureInputDisabled,
@ -64,13 +64,12 @@ impl<'a> funnel::Source<'a> for SecureInputSource<'a> {
EventType::SecureInputEnabled(SecureInputEnabledEvent { app_name, app_path })
}
},
}
})
} else {
println!("noop");
Event {
Some(Event {
source_id: self.sequencer.next_id(),
etype: EventType::NOOP,
}
})
}
}
}

View File

@ -46,12 +46,12 @@ impl<'a> funnel::Source<'a> for UISource<'a> {
select.recv(&self.ui_receiver)
}
fn receive(&self, op: SelectedOperation) -> Event {
fn receive(&self, op: SelectedOperation) -> Option<Event> {
let ui_event = op
.recv(&self.ui_receiver)
.expect("unable to select data from UISource receiver");
Event {
Some(Event {
source_id: self.sequencer.next_id(),
etype: match ui_event {
UIEvent::TrayIconClick => EventType::TrayIconClicked,
@ -60,6 +60,6 @@ impl<'a> funnel::Source<'a> for UISource<'a> {
}
UIEvent::Heartbeat => EventType::Heartbeat,
},
}
})
}
}

View File

@ -223,6 +223,7 @@ pub fn initialize_and_spawn(
&combined_match_cache,
&config_manager,
&config_manager,
&modifier_state_store,
);
let event_injector = EventInjectorAdapter::new(&*injector, &config_manager);

View File

@ -23,6 +23,7 @@ pub const WORKER_GENERAL_ERROR: i32 = 2;
pub const WORKER_LEGACY_ALREADY_RUNNING: i32 = 3;
pub const WORKER_EXIT_ALL_PROCESSES: i32 = 50;
pub const WORKER_RESTART: i32 = 51;
pub const WORKER_ERROR_EXIT_NO_CODE: i32 = 90;
pub const DAEMON_SUCCESS: i32 = 0;
pub const DAEMON_ALREADY_RUNNING: i32 = 1;

View File

@ -69,25 +69,25 @@ pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> {
Ok(IconPaths {
form_icon: Some(extract_icon(
WINDOWS_LOGO_ICO_BINARY,
&runtime_dir.join("form.ico"),
&runtime_dir.join("formv2.ico"),
)?),
search_icon: Some(extract_icon(ICON_BINARY, &runtime_dir.join("search.png"))?),
wizard_icon: Some(extract_icon(
WINDOWS_LOGO_ICO_BINARY,
&runtime_dir.join("wizard.ico"),
&runtime_dir.join("wizardv2.ico"),
)?),
tray_icon_normal: Some(extract_icon(
WINDOWS_NORMAL_DARK_ICO_BINARY,
&runtime_dir.join("normal.ico"),
&runtime_dir.join("normalv2.ico"),
)?),
tray_icon_disabled: Some(extract_icon(
WINDOWS_DISABLED_DARK_ICO_BINARY,
&runtime_dir.join("disabled.ico"),
&runtime_dir.join("disabledv2.ico"),
)?),
logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("icon.png"))?),
logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("iconv2.png"))?),
logo_no_background: Some(extract_icon(
LOGO_NO_BACKGROUND_BINARY,
&runtime_dir.join("icon_no_background.png"),
&runtime_dir.join("icon_no_backgroundv2.png"),
)?),
tray_explain_image: Some(extract_icon(
WINDOWS_TRAY_EXPLAIN_IMAGE,
@ -100,17 +100,20 @@ pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> {
#[cfg(target_os = "macos")]
pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> {
Ok(IconPaths {
search_icon: Some(extract_icon(ICON_BINARY, &runtime_dir.join("search.png"))?),
tray_icon_normal: Some(extract_icon(MAC_BINARY, &runtime_dir.join("normal.png"))?),
search_icon: Some(extract_icon(
ICON_BINARY,
&runtime_dir.join("searchv2.png"),
)?),
tray_icon_normal: Some(extract_icon(MAC_BINARY, &runtime_dir.join("normalv2.png"))?),
tray_icon_disabled: Some(extract_icon(
MAC_DISABLED_BINARY,
&runtime_dir.join("disabled.png"),
&runtime_dir.join("disabledv2.png"),
)?),
tray_icon_system_disabled: Some(extract_icon(
MAC_SYSTEM_DISABLED_BINARY,
&runtime_dir.join("systemdisabled.png"),
&runtime_dir.join("systemdisabledv2.png"),
)?),
logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("icon.png"))?),
logo: Some(extract_icon(ICON_BINARY, &runtime_dir.join("iconv2.png"))?),
logo_no_background: Some(extract_icon(
LOGO_NO_BACKGROUND_BINARY,
&runtime_dir.join("icon_no_background.png"),

View File

@ -67,6 +67,17 @@ fn main() {
let mut cmd = Command::new("cargo");
cmd.args(&args);
// If compiling for macOS x86-64, set the minimum supported version
// to 10.13
let is_macos = cfg!(target_os = "macos");
let is_x86_arch = cfg!(target_arch = "x86_64");
if is_macos
&& (override_target_arch == "current" && is_x86_arch
|| override_target_arch == "x86_64-apple-darwin")
{
cmd.env("MACOSX_DEPLOYMENT_TARGET", "10.13");
}
// Remove cargo/rust-specific env variables, as otherwise they mess up the
// nested cargo build call.
let all_vars = envmnt::vars();

View File

@ -1,5 +1,5 @@
name: espanso
version: 0.7.3
version: 2.0.3-alpha
summary: A Cross-platform Text Expander written in Rust
description: |
espanso is a Cross-platform, Text Expander written in Rust.
@ -21,13 +21,17 @@ description: |
* Works with almost any program
* Works with Emojis ¯\_(ツ)_/¯
* Works with Images
* Includes a powerful Search Bar
* Date expansion support
* Custom scripts support
* Shell commands support
* Support Forms
* App-specific configurations
* Expandable with packages
* Built-in package manager for espanso hub: https://hub.espanso.org/
* File based configuration
* Support Regex triggers
* Experimental Wayland support (currently not available through Snap, visit the website for more info).
## Get Started
@ -47,23 +51,35 @@ parts:
source: .
build-packages:
- libssl-dev
- libdbus-1-dev
- libwxgtk3.0-gtk3-dev
- pkg-config
- cmake
- libxkbcommon-dev
- libxtst-dev
- libx11-dev
- libxdo-dev
stage-packages:
- libx11-6
- libxau6
- libxcb1
- libxdmcp6
- libxdo3
- libxext6
- libxinerama1
- libxkbcommon0
- libxtst6
- libnotify-bin
- xclip
- libdbus-1-3
- libssl1.1
- libwxbase3.0-0v5
- libwxgtk3.0-0v5
- libatk-bridge2.0-0
- libatspi2.0-0
- libcairo-gobject2
- libepoxy0
- libgtk-3-0
- libwayland-client0
- libwayland-cursor0
- libwayland-egl1
- libwxgtk3.0-gtk3-0v5
apps:
espanso: