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 - name: Install Linux dependencies
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
run: | 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 - name: Check clippy
run: | run: |
rustup component add clippy rustup component add clippy
cargo clippy -- -D warnings cargo clippy -- -D warnings
env:
MACOSX_DEPLOYMENT_TARGET: "10.13"
- name: Install cargo-make - name: Install cargo-make
run: | run: |
cargo install --force cargo-make cargo install --force cargo-make
@ -49,7 +52,8 @@ jobs:
cargo fmt --all -- --check cargo fmt --all -- --check
- name: Install Linux dependencies - name: Install Linux dependencies
run: | 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 - name: Check clippy
run: | run: |
rustup component add clippy rustup component add clippy

View File

@ -131,8 +131,12 @@ jobs:
cargo install --force cargo-make cargo install --force cargo-make
- name: Test - name: Test
run: cargo make test-binary --profile release run: cargo make test-binary --profile release
env:
MACOSX_DEPLOYMENT_TARGET: "10.13"
- name: Build - name: Build
run: cargo make create-bundle --profile release run: cargo make create-bundle --profile release
env:
MACOSX_DEPLOYMENT_TARGET: "10.13"
- name: Create ZIP archive - name: Create ZIP archive
run: | run: |
ditto -c -k --sequesterRsrc --keepParent target/mac/Espanso.app Espanso-Mac-Intel.zip 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]] [[package]]
name = "espanso" name = "espanso"
version = "2.0.2-alpha" version = "2.0.3-alpha"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"caps", "caps",

View File

@ -25,6 +25,11 @@ pub enum InputEvent {
Mouse(MouseEvent), Mouse(MouseEvent),
Keyboard(KeyboardEvent), Keyboard(KeyboardEvent),
HotKey(HotKeyEvent), 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)] #[derive(Debug, PartialEq)]

View File

@ -241,6 +241,13 @@ impl From<RawInputEvent> for Option<InputEvent> {
INPUT_EVENT_TYPE_KEYBOARD => { INPUT_EVENT_TYPE_KEYBOARD => {
let (key, variant) = key_code_to_key(raw.key_code); 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 value = if raw.buffer_len > 0 {
let raw_string_result = let raw_string_result =
CStr::from_bytes_with_nul(&raw.buffer[..((raw.buffer_len + 1) as usize)]); 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 // We only want KEY UP AND KEY DOWN events
if (raw->data.keyboard.Message != WM_KEYDOWN && raw->data.keyboard.Message != WM_KEYUP && 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; return 0;
} }
@ -156,9 +156,10 @@ LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM w
std::vector<BYTE> lpKeyState(256); std::vector<BYTE> lpKeyState(256);
if (GetKeyboardState(lpKeyState.data())) if (GetKeyboardState(lpKeyState.data()))
{ {
// This flag is needed to avoid chaning the keyboard state for some layouts. // This flag is needed to avoid changing the keyboard state for some layouts.
// Refer to issue: https://github.com/federico-terzi/espanso/issues/86 // The 1 << 2 (setting bit 2) part is needed due to this issue: https://github.com/federico-terzi/espanso/issues/86
UINT flags = 1 << 2; // 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); 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) if (result >= 1)
{ {
event.buffer_len = result; 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 else
{ {

View File

@ -85,6 +85,14 @@ pub struct DiscardPreviousEvent {
pub minimum_source_id: u32, 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)] #[derive(Debug, Clone, PartialEq)]
pub struct SecureInputEnabledEvent { pub struct SecureInputEnabledEvent {
pub app_name: String, pub app_name: String,

View File

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

View File

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

View File

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

View File

@ -68,6 +68,9 @@ impl<'a> Engine<'a> {
debug!("end of stream received"); debug!("end of stream received");
return ExitMode::Exit; 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, cause::CauseCompensateMiddleware,
cursor_hint::CursorHintMiddleware, cursor_hint::CursorHintMiddleware,
delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider}, delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider},
discard::EventsDiscardMiddleware,
markdown::MarkdownMiddleware, markdown::MarkdownMiddleware,
match_select::MatchSelectMiddleware, match_select::MatchSelectMiddleware,
matcher::MatcherMiddleware, matcher::MatcherMiddleware,
multiplex::MultiplexMiddleware, multiplex::MultiplexMiddleware,
past_discard::PastEventsDiscardMiddleware,
render::RenderMiddleware, render::RenderMiddleware,
}, },
DisableOptions, EnabledStatusProvider, MatchFilter, MatchInfoProvider, MatchProvider, DisableOptions, EnabledStatusProvider, MatchFilter, MatchInfoProvider, MatchProvider,
MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware, Multiplexer, PathProvider, MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware, ModifierStateProvider,
Processor, Renderer, UndoEnabledProvider, Multiplexer, PathProvider, Processor, Renderer, UndoEnabledProvider,
}; };
use crate::{ use crate::{
event::{Event, EventType}, event::{Event, EventType},
@ -69,18 +69,27 @@ impl<'a> DefaultProcessor<'a> {
match_provider: &'a dyn MatchProvider, match_provider: &'a dyn MatchProvider,
undo_enabled_provider: &'a dyn UndoEnabledProvider, undo_enabled_provider: &'a dyn UndoEnabledProvider,
enabled_status_provider: &'a dyn EnabledStatusProvider, enabled_status_provider: &'a dyn EnabledStatusProvider,
modifier_state_provider: &'a dyn ModifierStateProvider,
) -> DefaultProcessor<'a> { ) -> DefaultProcessor<'a> {
Self { Self {
event_queue: VecDeque::new(), event_queue: VecDeque::new(),
middleware: vec![ middleware: vec![
Box::new(PastEventsDiscardMiddleware::new()), Box::new(EventsDiscardMiddleware::new()),
Box::new(DisableMiddleware::new(disable_options)), Box::new(DisableMiddleware::new(disable_options)),
Box::new(IconStatusMiddleware::new()), 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(SuppressMiddleware::new(enabled_status_provider)),
Box::new(ContextMenuMiddleware::new()), Box::new(ContextMenuMiddleware::new()),
Box::new(HotKeyMiddleware::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(CauseCompensateMiddleware::new()),
Box::new(MultiplexMiddleware::new(multiplexer)), Box::new(MultiplexMiddleware::new(multiplexer)),
Box::new(RenderMiddleware::new(renderer)), Box::new(RenderMiddleware::new(renderer)),

View File

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

View File

@ -20,7 +20,13 @@
use log::{debug, error}; use log::{debug, error};
use super::super::Middleware; use super::super::Middleware;
use crate::event::{internal::MatchSelectedEvent, Event, EventType}; use crate::{
event::{
internal::{DiscardBetweenEvent, MatchSelectedEvent},
Event, EventType,
},
process::EventSequenceProvider,
};
pub trait MatchFilter { pub trait MatchFilter {
fn filter_active(&self, matches_ids: &[i32]) -> Vec<i32>; fn filter_active(&self, matches_ids: &[i32]) -> Vec<i32>;
@ -33,13 +39,19 @@ pub trait MatchSelector {
pub struct MatchSelectMiddleware<'a> { pub struct MatchSelectMiddleware<'a> {
match_filter: &'a dyn MatchFilter, match_filter: &'a dyn MatchFilter,
match_selector: &'a dyn MatchSelector, match_selector: &'a dyn MatchSelector,
event_sequence_provider: &'a dyn EventSequenceProvider,
} }
impl<'a> MatchSelectMiddleware<'a> { 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 { Self {
match_filter, match_filter,
match_selector, match_selector,
event_sequence_provider,
} }
} }
} }
@ -49,7 +61,7 @@ impl<'a> Middleware for MatchSelectMiddleware<'a> {
"match_select" "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 { if let EventType::MatchesDetected(m_event) = event.etype {
let matches_ids: Vec<i32> = m_event.matches.iter().map(|m| m.id).collect(); 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 // 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) { 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); let m = m_event.matches.into_iter().find(|m| m.id == selected_id);
if let Some(m) = m { if let Some(m) = m {
@ -90,7 +105,22 @@ impl<'a> Middleware for MatchSelectMiddleware<'a> {
} else { } else {
debug!("MatchSelectMiddleware did not receive any match selection"); debug!("MatchSelectMiddleware did not receive any match selection");
Event::caused_by(event.source_id, EventType::NOOP) 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; 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> { pub struct MatcherMiddleware<'a, State> {
matchers: &'a [&'a dyn Matcher<'a, State>], matchers: &'a [&'a dyn Matcher<'a, State>],
matcher_states: RefCell<VecDeque<Vec<State>>>, matcher_states: RefCell<VecDeque<Vec<State>>>,
max_history_size: usize, max_history_size: usize,
modifier_status_provider: &'a dyn ModifierStateProvider,
} }
impl<'a, State> MatcherMiddleware<'a, State> { impl<'a, State> MatcherMiddleware<'a, State> {
pub fn new( pub fn new(
matchers: &'a [&'a dyn Matcher<'a, State>], matchers: &'a [&'a dyn Matcher<'a, State>],
options_provider: &'a dyn MatcherMiddlewareConfigProvider, options_provider: &'a dyn MatcherMiddlewareConfigProvider,
modifier_status_provider: &'a dyn ModifierStateProvider,
) -> Self { ) -> Self {
let max_history_size = options_provider.max_history_size(); let max_history_size = options_provider.max_history_size();
@ -76,6 +90,7 @@ impl<'a, State> MatcherMiddleware<'a, State> {
matchers, matchers,
matcher_states: RefCell::new(VecDeque::new()), matcher_states: RefCell::new(VecDeque::new()),
max_history_size, max_history_size,
modifier_status_provider,
} }
} }
} }
@ -101,6 +116,17 @@ impl<'a, State> Middleware for MatcherMiddleware<'a, State> {
matcher_states.pop_back(); matcher_states.pop_back();
return event; 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 // 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 // Skip non-press events
false false
} else { } 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 // Skip modifier keys
!matches!( !matches!(
keyboard_event.key, 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 // TODO: test

View File

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

View File

@ -39,7 +39,8 @@ pub use middleware::disable::DisableOptions;
pub use middleware::image_resolve::PathProvider; pub use middleware::image_resolve::PathProvider;
pub use middleware::match_select::{MatchFilter, MatchSelector}; pub use middleware::match_select::{MatchFilter, MatchSelector};
pub use middleware::matcher::{ pub use middleware::matcher::{
MatchResult, Matcher, MatcherEvent, MatcherMiddlewareConfigProvider, MatchResult, Matcher, MatcherEvent, MatcherMiddlewareConfigProvider, ModifierState,
ModifierStateProvider,
}; };
pub use middleware::multiplex::Multiplexer; pub use middleware::multiplex::Multiplexer;
pub use middleware::render::{Renderer, RendererError}; pub use middleware::render::{Renderer, RendererError};
@ -63,6 +64,7 @@ pub fn default<'a, MatcherState>(
match_provider: &'a dyn MatchProvider, match_provider: &'a dyn MatchProvider,
undo_enabled_provider: &'a dyn UndoEnabledProvider, undo_enabled_provider: &'a dyn UndoEnabledProvider,
enabled_status_provider: &'a dyn EnabledStatusProvider, enabled_status_provider: &'a dyn EnabledStatusProvider,
modifier_state_provider: &'a dyn ModifierStateProvider,
) -> impl Processor + 'a { ) -> impl Processor + 'a {
default::DefaultProcessor::new( default::DefaultProcessor::new(
matchers, matchers,
@ -79,5 +81,6 @@ pub fn default<'a, MatcherState>(
match_provider, match_provider,
undo_enabled_provider, undo_enabled_provider,
enabled_status_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_prompt_accessibility() -> i32;
pub fn mac_utils_transition_to_foreground_app(); pub fn mac_utils_transition_to_foreground_app();
pub fn mac_utils_transition_to_background_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(test)]
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
mod tests { 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). // When called, convert the current process to a background app (hide the dock icon).
extern "C" void mac_utils_transition_to_background_app(); 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 #endif //ESPANSO_MAC_UTILS_H

View File

@ -89,3 +89,15 @@ void mac_utils_transition_to_background_app() {
ProcessSerialNumber psn = { 0, kCurrentProcess }; ProcessSerialNumber psn = { 0, kCurrentProcess };
TransformProcessType(&psn, kProcessTransformToUIElementApplication); 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() { 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(); build_native();
} }

View File

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

View File

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

View File

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

View File

@ -166,7 +166,7 @@ private:
bool SearchApp::OnInit() 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); frame->Show(true);
SetupWindowStyle(frame); SetupWindowStyle(frame);
Activate(frame); Activate(frame);
@ -198,7 +198,7 @@ SearchFrame::SearchFrame(const wxString &title, const wxPoint &pos, const wxSize
iconPanel = nullptr; iconPanel = nullptr;
if (searchMetadata->iconPath) if (searchMetadata->iconPath)
{ {
wxString iconPath = wxString(searchMetadata->iconPath); wxString iconPath = wxString::FromUTF8(searchMetadata->iconPath);
if (wxFileExists(iconPath)) if (wxFileExists(iconPath))
{ {
wxBitmap bitmap = wxBitmap(iconPath, wxBITMAP_TYPE_PNG); 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) { if (error_set_metadata->errors[i].level == ERROR_METADATA_LEVEL_WARNING) {
level = wxT("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); errors_text.Append(error_text);
} }
@ -171,7 +171,7 @@ bool TroubleshootingApp::OnInit()
if (troubleshooting_metadata->window_icon_path) 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); frame->Show(true);

View File

@ -54,7 +54,7 @@ DerivedWelcomeFrame::DerivedWelcomeFrame(wxWindow *parent)
if (welcome_metadata->tray_image_path) 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); this->tray_bitmap->SetBitmap(trayBitmap);
#ifdef __WXOSX__ #ifdef __WXOSX__
this->tray_info_label->SetLabel("You should see the espanso icon on the status bar:"); 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) 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); frame->Show(true);

View File

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

View File

@ -54,7 +54,7 @@ class WelcomeFrame : public wxFrame
public: 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(); ~WelcomeFrame();

View File

@ -130,7 +130,7 @@ DerivedFrame::DerivedFrame(wxWindow *parent)
if (wizard_metadata->welcome_image_path) 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); this->welcome_image->SetBitmap(welcomeBitmap);
} }
@ -140,12 +140,12 @@ DerivedFrame::DerivedFrame(wxWindow *parent)
if (wizard_metadata->accessibility_image_1_path) 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); this->accessibility_image1->SetBitmap(accessiblityImage1);
} }
if (wizard_metadata->accessibility_image_2_path) 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); this->accessibility_image2->SetBitmap(accessiblityImage2);
} }
@ -380,7 +380,7 @@ bool WizardApp::OnInit()
if (wizard_metadata->window_icon_path) 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); frame->Show(true);

View File

@ -24,7 +24,7 @@ use std::{
}; };
use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value}; use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
use log::{info, warn}; use log::{error, info};
use thiserror::Error; use thiserror::Error;
#[allow(clippy::upper_case_acronyms)] #[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."); info!("this debug information was shown because the 'debug' option is true.");
} }
let ignore_error = params if !output.status.success() {
.get("ignore_error") error!(
.and_then(|v| v.as_bool())
.copied()
.unwrap_or(false);
if !output.status.success() || !error_str.trim().is_empty() {
warn!(
"shell command exited with code: {} and error: {}", "shell command exited with code: {} and error: {}",
output.status, error_str output.status, error_str
); );
if !ignore_error {
return ExtensionResult::Error( return ExtensionResult::Error(
ShellExtensionError::ExecutionError(error_str.to_string()).into(), ShellExtensionError::ExecutionError(error_str.to_string()).into(),
); );
} }
}
let trim = params let trim = params
.get("trim") .get("trim")
@ -388,24 +380,4 @@ mod tests {
ExtensionResult::Error(_) 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] [package]
name = "espanso" name = "espanso"
version = "2.0.2-alpha" version = "2.0.3-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

@ -32,8 +32,8 @@ use crate::{
common_flags::*, common_flags::*,
exit_code::{ exit_code::{
DAEMON_ALREADY_RUNNING, DAEMON_FATAL_CONFIG_ERROR, DAEMON_GENERAL_ERROR, DAEMON_ALREADY_RUNNING, DAEMON_FATAL_CONFIG_ERROR, DAEMON_GENERAL_ERROR,
DAEMON_LEGACY_ALREADY_RUNNING, DAEMON_SUCCESS, WORKER_EXIT_ALL_PROCESSES, WORKER_RESTART, DAEMON_LEGACY_ALREADY_RUNNING, DAEMON_SUCCESS, WORKER_ERROR_EXIT_NO_CODE,
WORKER_SUCCESS, WORKER_EXIT_ALL_PROCESSES, WORKER_RESTART, WORKER_SUCCESS,
}, },
ipc::{create_ipc_client_to_worker, IPCEvent}, ipc::{create_ipc_client_to_worker, IPCEvent},
lock::{acquire_daemon_lock, acquire_legacy_lock, acquire_worker_lock}, lock::{acquire_daemon_lock, acquire_legacy_lock, acquire_worker_lock},
@ -271,6 +271,10 @@ fn spawn_worker(
.send(code) .send(code)
.expect("unable to forward worker exit 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 std::process::Command;
use anyhow::Result; use anyhow::Result;
use std::process::ExitStatus;
use thiserror::Error; use thiserror::Error;
use crate::cli::util::CommandExt; use crate::cli::util::CommandExt;
@ -31,8 +32,7 @@ pub fn launch_daemon(paths_overrides: &PathsOverrides) -> Result<()> {
command.args(&["daemon"]); command.args(&["daemon"]);
command.with_paths_overrides(paths_overrides); command.with_paths_overrides(paths_overrides);
let mut child = command.spawn()?; let result = spawn_and_wait(command)?;
let result = child.wait()?;
if result.success() { if result.success() {
Ok(()) Ok(())
@ -46,3 +46,33 @@ pub enum DaemonError {
#[error("unexpected error, 'espanso daemon' returned a non-zero exit code.")] #[error("unexpected error, 'espanso daemon' returned a non-zero exit code.")]
NonZeroExitCode, 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; 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() self.active().undo_backspace()
} }
} }

View File

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

View File

@ -81,6 +81,8 @@ pub fn init_and_spawn(
// Update the modifiers state // Update the modifiers state
if let Some((modifier, is_pressed)) = get_modifier_status(&event) { if let Some((modifier, is_pressed)) = get_modifier_status(&event) {
modifier_state_store_clone.update_state(modifier, is_pressed); 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) // 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 { struct ModifiersState {
@ -142,3 +149,44 @@ impl ModifierStatusProvider for ModifierStateStore {
self.is_any_conflicting_modifier_pressed() 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") { if cfg!(target_os = "macos") {
let si_event = op let si_event = op
.recv(&self.receiver) .recv(&self.receiver)
.expect("unable to select data from SecureInputSource receiver"); .expect("unable to select data from SecureInputSource receiver");
Event { Some(Event {
source_id: self.sequencer.next_id(), source_id: self.sequencer.next_id(),
etype: match si_event { etype: match si_event {
SecureInputEvent::Disabled => EventType::SecureInputDisabled, SecureInputEvent::Disabled => EventType::SecureInputDisabled,
@ -64,13 +64,12 @@ impl<'a> funnel::Source<'a> for SecureInputSource<'a> {
EventType::SecureInputEnabled(SecureInputEnabledEvent { app_name, app_path }) EventType::SecureInputEnabled(SecureInputEnabledEvent { app_name, app_path })
} }
}, },
} })
} else { } else {
println!("noop"); Some(Event {
Event {
source_id: self.sequencer.next_id(), source_id: self.sequencer.next_id(),
etype: EventType::NOOP, etype: EventType::NOOP,
} })
} }
} }
} }

View File

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

View File

@ -223,6 +223,7 @@ pub fn initialize_and_spawn(
&combined_match_cache, &combined_match_cache,
&config_manager, &config_manager,
&config_manager, &config_manager,
&modifier_state_store,
); );
let event_injector = EventInjectorAdapter::new(&*injector, &config_manager); 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_LEGACY_ALREADY_RUNNING: i32 = 3;
pub const WORKER_EXIT_ALL_PROCESSES: i32 = 50; pub const WORKER_EXIT_ALL_PROCESSES: i32 = 50;
pub const WORKER_RESTART: i32 = 51; pub const WORKER_RESTART: i32 = 51;
pub const WORKER_ERROR_EXIT_NO_CODE: i32 = 90;
pub const DAEMON_SUCCESS: i32 = 0; pub const DAEMON_SUCCESS: i32 = 0;
pub const DAEMON_ALREADY_RUNNING: i32 = 1; 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 { Ok(IconPaths {
form_icon: Some(extract_icon( form_icon: Some(extract_icon(
WINDOWS_LOGO_ICO_BINARY, 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"))?), search_icon: Some(extract_icon(ICON_BINARY, &runtime_dir.join("search.png"))?),
wizard_icon: Some(extract_icon( wizard_icon: Some(extract_icon(
WINDOWS_LOGO_ICO_BINARY, WINDOWS_LOGO_ICO_BINARY,
&runtime_dir.join("wizard.ico"), &runtime_dir.join("wizardv2.ico"),
)?), )?),
tray_icon_normal: Some(extract_icon( tray_icon_normal: Some(extract_icon(
WINDOWS_NORMAL_DARK_ICO_BINARY, WINDOWS_NORMAL_DARK_ICO_BINARY,
&runtime_dir.join("normal.ico"), &runtime_dir.join("normalv2.ico"),
)?), )?),
tray_icon_disabled: Some(extract_icon( tray_icon_disabled: Some(extract_icon(
WINDOWS_DISABLED_DARK_ICO_BINARY, 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: Some(extract_icon(
LOGO_NO_BACKGROUND_BINARY, LOGO_NO_BACKGROUND_BINARY,
&runtime_dir.join("icon_no_background.png"), &runtime_dir.join("icon_no_backgroundv2.png"),
)?), )?),
tray_explain_image: Some(extract_icon( tray_explain_image: Some(extract_icon(
WINDOWS_TRAY_EXPLAIN_IMAGE, WINDOWS_TRAY_EXPLAIN_IMAGE,
@ -100,17 +100,20 @@ pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> { pub fn load_icon_paths(runtime_dir: &Path) -> Result<IconPaths> {
Ok(IconPaths { Ok(IconPaths {
search_icon: Some(extract_icon(ICON_BINARY, &runtime_dir.join("search.png"))?), search_icon: Some(extract_icon(
tray_icon_normal: Some(extract_icon(MAC_BINARY, &runtime_dir.join("normal.png"))?), 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( tray_icon_disabled: Some(extract_icon(
MAC_DISABLED_BINARY, MAC_DISABLED_BINARY,
&runtime_dir.join("disabled.png"), &runtime_dir.join("disabledv2.png"),
)?), )?),
tray_icon_system_disabled: Some(extract_icon( tray_icon_system_disabled: Some(extract_icon(
MAC_SYSTEM_DISABLED_BINARY, 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: Some(extract_icon(
LOGO_NO_BACKGROUND_BINARY, LOGO_NO_BACKGROUND_BINARY,
&runtime_dir.join("icon_no_background.png"), &runtime_dir.join("icon_no_background.png"),

View File

@ -67,6 +67,17 @@ fn main() {
let mut cmd = Command::new("cargo"); let mut cmd = Command::new("cargo");
cmd.args(&args); 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 // Remove cargo/rust-specific env variables, as otherwise they mess up the
// nested cargo build call. // nested cargo build call.
let all_vars = envmnt::vars(); let all_vars = envmnt::vars();

View File

@ -1,5 +1,5 @@
name: espanso name: espanso
version: 0.7.3 version: 2.0.3-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.
@ -21,13 +21,17 @@ description: |
* Works with almost any program * Works with almost any program
* Works with Emojis ¯\_(ツ)_/¯ * Works with Emojis ¯\_(ツ)_/¯
* Works with Images * Works with Images
* Includes a powerful Search Bar
* Date expansion support * Date expansion support
* Custom scripts support * Custom scripts support
* Shell commands support * Shell commands support
* Support Forms
* App-specific configurations * App-specific configurations
* Expandable with packages * Expandable with packages
* Built-in package manager for espanso hub: https://hub.espanso.org/ * Built-in package manager for espanso hub: https://hub.espanso.org/
* File based configuration * File based configuration
* Support Regex triggers
* Experimental Wayland support (currently not available through Snap, visit the website for more info).
## Get Started ## Get Started
@ -47,23 +51,35 @@ parts:
source: . source: .
build-packages: build-packages:
- libssl-dev - libssl-dev
- libdbus-1-dev
- libwxgtk3.0-gtk3-dev
- pkg-config - pkg-config
- cmake - libxkbcommon-dev
- libxtst-dev - libxtst-dev
- libx11-dev - libx11-dev
- libxdo-dev
stage-packages: stage-packages:
- libx11-6 - libx11-6
- libxau6 - libxau6
- libxcb1 - libxcb1
- libxdmcp6 - libxdmcp6
- libxdo3
- libxext6 - libxext6
- libxinerama1 - libxinerama1
- libxkbcommon0 - libxkbcommon0
- libxtst6 - libxtst6
- libnotify-bin - 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: apps:
espanso: espanso: