commit
b7e91d9b5d
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
@ -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
2
Cargo.lock
generated
|
@ -572,7 +572,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "espanso"
|
||||
version = "2.0.2-alpha"
|
||||
version = "2.0.3-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"caps",
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)]);
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -71,6 +71,7 @@ pub enum EventType {
|
|||
ImageResolved(internal::ImageResolvedEvent),
|
||||
MatchInjected,
|
||||
DiscardPrevious(internal::DiscardPreviousEvent),
|
||||
DiscardBetween(internal::DiscardBetweenEvent),
|
||||
Undo(internal::UndoEvent),
|
||||
|
||||
Disabled,
|
||||
|
|
|
@ -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);
|
||||
FunnelResult::Event(event)
|
||||
if let Some(event) = source.receive(op) {
|
||||
FunnelResult::Event(event)
|
||||
} else {
|
||||
FunnelResult::Skipped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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
|
|
@ -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,22 +87,40 @@ 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
|
||||
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 {
|
||||
Event::caused_by(
|
||||
event.source_id,
|
||||
EventType::MatchSelected(MatchSelectedEvent { chosen: m }),
|
||||
)
|
||||
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 {
|
||||
Event::caused_by(
|
||||
event.source_id,
|
||||
EventType::MatchSelected(MatchSelectedEvent { chosen: m }),
|
||||
)
|
||||
} else {
|
||||
error!("MatchSelectMiddleware could not find the correspondent match");
|
||||
Event::caused_by(event.source_id, EventType::NOOP)
|
||||
}
|
||||
} else {
|
||||
error!("MatchSelectMiddleware could not find the correspondent match");
|
||||
debug!("MatchSelectMiddleware did not receive any match selection");
|
||||
Event::caused_by(event.source_id, EventType::NOOP)
|
||||
}
|
||||
} 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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
|
@ -88,4 +88,16 @@ void mac_utils_transition_to_foreground_app() {
|
|||
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];
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,23 +191,15 @@ 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(),
|
||||
);
|
||||
}
|
||||
return ExtensionResult::Error(
|
||||
ShellExtensionError::ExecutionError(error_str.to_string()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
let trim = params
|
||||
|
@ -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(), ¶m)
|
||||
.into_success()
|
||||
.unwrap(),
|
||||
ExtensionOutput::Single("".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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?)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue
Block a user