diff --git a/Cargo.lock b/Cargo.lock
index 0725ab5..3260b05 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -26,6 +26,8 @@ name = "espanso"
version = "1.0.0"
dependencies = [
"espanso-detect 0.1.0",
+ "espanso-ui 0.1.0",
+ "maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -34,6 +36,17 @@ version = "0.1.0"
dependencies = [
"cc 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)",
"enum-as-inner 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazycell 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "widestring 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "espanso-ui"
+version = "0.1.0"
+dependencies = [
+ "cc 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazycell 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)",
"widestring 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -46,6 +59,11 @@ dependencies = [
"unicode-segmentation 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
[[package]]
name = "log"
version = "0.4.14"
@@ -54,6 +72,11 @@ dependencies = [
"cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
[[package]]
name = "proc-macro2"
version = "1.0.24"
@@ -100,7 +123,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
"checksum enum-as-inner 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595"
"checksum heck 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
+"checksum lazycell 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
"checksum log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)" = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+"checksum maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
"checksum proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
"checksum quote 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
"checksum syn 1.0.60 (registry+https://github.com/rust-lang/crates.io-index)" = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081"
diff --git a/Cargo.toml b/Cargo.toml
index 4fd902e..8c71a4f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,4 +3,5 @@
members = [
"espanso",
"espanso-detect",
+ "espanso-ui",
]
\ No newline at end of file
diff --git a/espanso-detect/Cargo.toml b/espanso-detect/Cargo.toml
index 621e651..52cee9b 100644
--- a/espanso-detect/Cargo.toml
+++ b/espanso-detect/Cargo.toml
@@ -10,6 +10,7 @@ log = "0.4.14"
[target.'cfg(windows)'.dependencies]
widestring = "0.4.3"
+lazycell = "1.3.0"
[build-dependencies]
cc = "1.0.66"
diff --git a/espanso-detect/src/lib.rs b/espanso-detect/src/lib.rs
index 4f50a52..5b23648 100644
--- a/espanso-detect/src/lib.rs
+++ b/espanso-detect/src/lib.rs
@@ -20,4 +20,4 @@
pub mod event;
#[cfg(target_os = "windows")]
-pub mod win32;
\ No newline at end of file
+pub mod win32;
diff --git a/espanso-detect/src/win32/mod.rs b/espanso-detect/src/win32/mod.rs
index 09d5401..040179c 100644
--- a/espanso-detect/src/win32/mod.rs
+++ b/espanso-detect/src/win32/mod.rs
@@ -17,7 +17,10 @@
* along with espanso. If not, see .
*/
-use log::{trace, warn};
+use std::ffi::c_void;
+
+use lazycell::LazyCell;
+use log::{error, trace, warn};
use widestring::U16CStr;
use crate::event::Status::*;
@@ -57,35 +60,81 @@ pub struct RawInputEvent {
}
#[allow(improper_ctypes)]
-#[link(name = "native", kind = "static")]
+#[link(name = "espansodetect", kind = "static")]
extern "C" {
- pub fn raw_eventloop(
- _self: *const Win32Source,
+ pub fn detect_initialize(_self: *const Win32Source) -> *mut c_void;
+
+ pub fn detect_eventloop(
+ window: *const c_void,
event_callback: extern "C" fn(_self: *mut Win32Source, event: RawInputEvent),
- );
+ ) -> i32;
+
+ pub fn detect_destroy(window: *const c_void) -> i32;
}
pub type Win32SourceCallback = Box;
pub struct Win32Source {
- callback: Win32SourceCallback,
+ handle: *mut c_void,
+ callback: LazyCell,
}
impl Win32Source {
- pub fn new(callback: Win32SourceCallback) -> Win32Source {
- Self { callback }
+ pub fn new() -> Win32Source {
+ Self {
+ handle: std::ptr::null_mut(),
+ callback: LazyCell::new(),
+ }
}
- pub fn eventloop(&self) {
- unsafe {
- extern "C" fn callback(_self: *mut Win32Source, event: RawInputEvent) {
- let event: Option = event.into();
+
+ pub fn initialize(&mut self) {
+ let handle = unsafe { detect_initialize(self as *const Win32Source) };
+
+ if handle.is_null() {
+ panic!("Unable to initialize Win32EventLoop");
+ }
+
+ self.handle = handle;
+ }
+
+ pub fn eventloop(&self, event_callback: Win32SourceCallback) {
+ if self.handle.is_null() {
+ panic!("Attempt to start Win32Source eventloop without initialization");
+ }
+
+ if let Err(_) = self.callback.fill(event_callback) {
+ panic!("Unable to set Win32Source event callback");
+ }
+
+ extern "C" fn callback(_self: *mut Win32Source, event: RawInputEvent) {
+ let event: Option = event.into();
+ if let Some(callback) = unsafe { (*_self).callback.borrow() } {
if let Some(event) = event {
- unsafe { (*(*_self).callback)(event) }
+ callback(event)
} else {
trace!("Unable to convert raw event to input event");
}
}
+ }
- raw_eventloop(self as *const Win32Source, callback);
+ let error_code = unsafe { detect_eventloop(self.handle, callback) };
+
+ if error_code <= 0 {
+ panic!("Win32Source eventloop returned a negative error code");
+ }
+ }
+}
+
+impl Drop for Win32Source {
+ fn drop(&mut self) {
+ if self.handle.is_null() {
+ error!("Win32Source destruction cannot be performed, handle is null");
+ return;
+ }
+
+ let result = unsafe { detect_destroy(self.handle) };
+
+ if result != 0 {
+ error!("Win32EventLoop destruction returned non-zero code");
}
}
}
@@ -260,17 +309,19 @@ mod tests {
raw.key_code = 0x4B;
let result: Option = raw.into();
- assert_eq!(result.unwrap(), InputEvent::Keyboard(KeyboardEvent {
- key: Other(0x4B),
- status: Released,
- value: Some("k".to_string()),
- variant: None,
- }));
+ assert_eq!(
+ result.unwrap(),
+ InputEvent::Keyboard(KeyboardEvent {
+ key: Other(0x4B),
+ status: Released,
+ value: Some("k".to_string()),
+ variant: None,
+ })
+ );
}
#[test]
fn raw_to_input_event_mouse_works_correctly() {
-
let mut raw = default_raw_input_event();
raw.event_type = INPUT_EVENT_TYPE_MOUSE;
raw.status = INPUT_STATUS_RELEASED;
@@ -278,10 +329,13 @@ mod tests {
raw.key_code = INPUT_MOUSE_RIGHT_BUTTON;
let result: Option = raw.into();
- assert_eq!(result.unwrap(), InputEvent::Mouse(MouseEvent {
- status: Released,
- button: MouseButton::Right,
- }));
+ assert_eq!(
+ result.unwrap(),
+ InputEvent::Mouse(MouseEvent {
+ status: Released,
+ button: MouseButton::Right,
+ })
+ );
}
#[test]
@@ -299,13 +353,14 @@ mod tests {
#[test]
fn raw_to_input_event_returns_none_when_missing_type() {
let result: Option = RawInputEvent {
- event_type: 0, // Missing type
+ event_type: 0, // Missing type
buffer: [0; 24],
buffer_len: 0,
key_code: 123,
variant: INPUT_LEFT_VARIANT,
status: INPUT_STATUS_PRESSED,
- }.into();
+ }
+ .into();
assert!(result.is_none());
}
}
diff --git a/espanso-detect/src/win32/native.cpp b/espanso-detect/src/win32/native.cpp
index 6e586aa..9e7db4f 100644
--- a/espanso-detect/src/win32/native.cpp
+++ b/espanso-detect/src/win32/native.cpp
@@ -18,6 +18,7 @@
*/
#include "native.h"
+#include
#include
#include
#include
@@ -39,29 +40,41 @@
#include
// How many milliseconds must pass between events before refreshing the keyboard layout
-const long refreshKeyboardLayoutInterval = 2000;
-const USHORT mouseDownFlags = RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN |
+const long DETECT_REFRESH_KEYBOARD_LAYOUT_INTERVAL = 2000;
+const wchar_t *const DETECT_WINCLASS = L"EspansoDetect";
+const USHORT MOUSE_DOWN_FLAGS = RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN |
RI_MOUSE_BUTTON_1_DOWN | RI_MOUSE_BUTTON_2_DOWN | RI_MOUSE_BUTTON_3_DOWN |
RI_MOUSE_BUTTON_4_DOWN | RI_MOUSE_BUTTON_5_DOWN;
-const USHORT mouseUpFlags = RI_MOUSE_LEFT_BUTTON_UP | RI_MOUSE_RIGHT_BUTTON_UP | RI_MOUSE_MIDDLE_BUTTON_UP |
+const USHORT MOUSE_UP_FLAGS = RI_MOUSE_LEFT_BUTTON_UP | RI_MOUSE_RIGHT_BUTTON_UP | RI_MOUSE_MIDDLE_BUTTON_UP |
RI_MOUSE_BUTTON_1_UP | RI_MOUSE_BUTTON_2_UP | RI_MOUSE_BUTTON_3_UP |
RI_MOUSE_BUTTON_4_UP | RI_MOUSE_BUTTON_5_UP;
-DWORD lastKeyboardPressTick = 0;
-HKL currentKeyboardLayout;
-HWND window;
-const wchar_t *const winclass = L"Espanso";
+typedef struct {
+ HKL current_keyboard_layout;
+ DWORD last_key_press_tick;
-void *self = NULL;
-EventCallback event_callback = NULL;
+ // Rust interop
+ void * rust_instance;
+ EventCallback event_callback;
+} DetectVariables;
/*
- * Message handler procedure for the windows
+ * Message handler procedure for the window
*/
-LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp)
+LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp)
{
+ DetectVariables * variables = reinterpret_cast(GetWindowLongPtrW(window, GWLP_USERDATA));
+
switch (msg)
{
+ case WM_DESTROY:
+ PostQuitMessage(0);
+
+ // Free the window variables
+ delete variables;
+ SetWindowLongPtrW(window, GWLP_USERDATA, NULL);
+
+ return 0L;
case WM_INPUT: // Message relative to the RAW INPUT events
{
InputEvent event = {};
@@ -104,7 +117,7 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR
DWORD currentTick = GetTickCount();
// If enough time has passed between the last keypress and now, refresh the keyboard layout
- if ((currentTick - lastKeyboardPressTick) > refreshKeyboardLayoutInterval)
+ if ((currentTick - variables->last_key_press_tick) > DETECT_REFRESH_KEYBOARD_LAYOUT_INTERVAL)
{
// Because keyboard layouts on windows are Window-specific, to get the current
@@ -119,11 +132,11 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR
// It's not always valid, so update the current value only if available.
if (newKeyboardLayout != 0)
{
- currentKeyboardLayout = newKeyboardLayout;
+ variables->current_keyboard_layout = newKeyboardLayout;
}
}
- lastKeyboardPressTick = currentTick;
+ variables->last_key_press_tick = currentTick;
}
// Get keyboard state ( necessary to decode the associated Unicode char )
@@ -134,7 +147,7 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR
// Refer to issue: https://github.com/federico-terzi/espanso/issues/86
UINT flags = 1 << 2;
- int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), reinterpret_cast(event.buffer), (sizeof(event.buffer)/sizeof(event.buffer[0])) - 1, flags, currentKeyboardLayout);
+ int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), reinterpret_cast(event.buffer), (sizeof(event.buffer)/sizeof(event.buffer[0])) - 1, flags, variables->current_keyboard_layout);
// Handle the corresponding string if present
if (result >= 1)
@@ -187,16 +200,16 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR
else if (raw->header.dwType == RIM_TYPEMOUSE) // Mouse events
{
// Make sure the mouse event belongs to the supported ones
- if ((raw->data.mouse.usButtonFlags & (mouseDownFlags | mouseUpFlags)) == 0) {
+ if ((raw->data.mouse.usButtonFlags & (MOUSE_DOWN_FLAGS | MOUSE_UP_FLAGS)) == 0) {
return 0;
}
event.event_type = INPUT_EVENT_TYPE_MOUSE;
- if ((raw->data.mouse.usButtonFlags & mouseDownFlags) != 0)
+ if ((raw->data.mouse.usButtonFlags & MOUSE_DOWN_FLAGS) != 0)
{
event.status = INPUT_STATUS_PRESSED;
- } else if ((raw->data.mouse.usButtonFlags & mouseUpFlags) != 0) {
+ } else if ((raw->data.mouse.usButtonFlags & MOUSE_UP_FLAGS) != 0) {
event.status = INPUT_STATUS_RELEASED;
}
@@ -221,9 +234,9 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR
}
// If valid, send the event to the Rust layer
- if (event.event_type != 0 && self != NULL && event_callback != NULL)
+ if (event.event_type != 0 && variables->rust_instance != NULL && variables->event_callback != NULL)
{
- event_callback(self, event);
+ variables->event_callback(variables->rust_instance, event);
}
return 0;
@@ -233,17 +246,16 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR
}
}
-int32_t raw_eventloop(void *_self, EventCallback _callback)
+void * detect_initialize(void *_self)
{
- // Initialize the default keyboard layout
- currentKeyboardLayout = GetKeyboardLayout(0);
+ HWND window = NULL;
// Initialize the Worker window
// Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
WNDCLASSEX wndclass = {
sizeof(WNDCLASSEX), // cbSize: Size of this structure
0, // style: Class styles
- window_procedure, // lpfnWndProc: Pointer to the window procedure
+ detect_window_procedure, // lpfnWndProc: Pointer to the window procedure
0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure
0, // cbWndExtra: The number of extra bytes to allocate following the window instance.
GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class.
@@ -251,16 +263,22 @@ int32_t raw_eventloop(void *_self, EventCallback _callback)
LoadCursor(0, IDC_ARROW), // hCursor: A handle to the class cursor.
NULL, // hbrBackground: A handle to the class background brush.
NULL, // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu
- winclass, // lpszClassName: A pointer to a null-terminated string or is an atom.
+ DETECT_WINCLASS, // lpszClassName: A pointer to a null-terminated string or is an atom.
NULL // hIconSm: A handle to a small icon that is associated with the window class.
};
if (RegisterClassEx(&wndclass))
{
+ DetectVariables * variables = new DetectVariables();
+ variables->rust_instance = _self;
+
+ // Initialize the default keyboard layout
+ variables->current_keyboard_layout = GetKeyboardLayout(0);
+
// Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
window = CreateWindowEx(
0, // dwExStyle: The extended window style of the window being created.
- winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass
+ DETECT_WINCLASS, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass
L"Espanso Worker Window", // lpWindowName: The window name.
WS_OVERLAPPEDWINDOW, // dwStyle: The style of the window being created.
CW_USEDEFAULT, // X: The initial horizontal position of the window.
@@ -273,6 +291,8 @@ int32_t raw_eventloop(void *_self, EventCallback _callback)
NULL // lpParam: Pointer to a value to be passed to the window
);
+ SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<::LONG_PTR>(variables));
+
// Register raw inputs
RAWINPUTDEVICE Rid[2];
@@ -288,22 +308,27 @@ int32_t raw_eventloop(void *_self, EventCallback _callback)
if (RegisterRawInputDevices(Rid, 2, sizeof(Rid[0])) == FALSE)
{ // Something went wrong, error.
- return -1;
+ return nullptr;
}
}
else
{
// Something went wrong, error.
- return -2;
+ return nullptr;
}
- event_callback = _callback;
- self = _self;
+ return window;
+}
+int32_t detect_eventloop(void * window, EventCallback _callback)
+{
if (window)
{
+ DetectVariables * variables = reinterpret_cast(GetWindowLongPtrW((HWND) window, GWLP_USERDATA));
+ variables->event_callback = _callback;
+
// Hide the window
- ShowWindow(window, SW_HIDE);
+ ShowWindow((HWND) window, SW_HIDE);
// Enter the Event loop
MSG msg;
@@ -311,8 +336,11 @@ int32_t raw_eventloop(void *_self, EventCallback _callback)
DispatchMessage(&msg);
}
- event_callback = NULL;
- self = NULL;
-
return 1;
+}
+
+int32_t detect_destroy(void * window) {
+ if (window) {
+ return DestroyWindow((HWND) window);
+ }
}
\ No newline at end of file
diff --git a/espanso-detect/src/win32/native.h b/espanso-detect/src/win32/native.h
index afd5552..eeb6d7a 100644
--- a/espanso-detect/src/win32/native.h
+++ b/espanso-detect/src/win32/native.h
@@ -60,10 +60,16 @@ typedef struct {
int32_t status;
} InputEvent;
-typedef void (*EventCallback)(void * self, InputEvent data);
-extern EventCallback event_callback;
+typedef void (*EventCallback)(void * rust_istance, InputEvent data);
-// Initialize the Raw Input API and run the event loop. Blocking call.
-extern "C" int32_t raw_eventloop(void * self, EventCallback callback);
+
+// Initialize the Raw Input API and the Window.
+extern "C" void * detect_initialize(void * rust_istance);
+
+// Run the event loop. Blocking call.
+extern "C" int32_t detect_eventloop(void * window, EventCallback callback);
+
+// Destroy the given window.
+extern "C" int32_t detect_destroy(void * window);
#endif //ESPANSO_DETECT_H
\ No newline at end of file
diff --git a/espanso-ui/Cargo.toml b/espanso-ui/Cargo.toml
new file mode 100644
index 0000000..e1e28ad
--- /dev/null
+++ b/espanso-ui/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "espanso-ui"
+version = "0.1.0"
+authors = ["Federico Terzi "]
+edition = "2018"
+build="build.rs"
+
+[dependencies]
+log = "0.4.14"
+
+[target.'cfg(windows)'.dependencies]
+widestring = "0.4.3"
+lazycell = "1.3.0"
+
+[build-dependencies]
+cc = "1.0.66"
\ No newline at end of file
diff --git a/espanso-ui/build.rs b/espanso-ui/build.rs
new file mode 100644
index 0000000..8d917fd
--- /dev/null
+++ b/espanso-ui/build.rs
@@ -0,0 +1,57 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+#[cfg(target_os = "windows")]
+fn cc_config() {
+ println!("cargo:rerun-if-changed=src/win32/native.cpp");
+ println!("cargo:rerun-if-changed=src/win32/native.h");
+ cc::Build::new()
+ .cpp(true)
+ .include("src/win32/native.h")
+ .file("src/win32/native.cpp")
+ .compile("espansoui");
+
+ println!("cargo:rustc-link-lib=static=espansoui");
+ println!("cargo:rustc-link-lib=dylib=user32");
+ #[cfg(target_env = "gnu")]
+ println!("cargo:rustc-link-lib=dylib=stdc++");
+ #[cfg(target_env = "gnu")]
+ println!("cargo:rustc-link-lib=dylib=gdiplus");
+}
+
+#[cfg(target_os = "linux")]
+fn cc_config() {
+ println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
+ println!("cargo:rustc-link-lib=static=linuxbridge");
+ println!("cargo:rustc-link-lib=dylib=X11");
+ println!("cargo:rustc-link-lib=dylib=Xtst");
+ println!("cargo:rustc-link-lib=dylib=xdo");
+}
+
+#[cfg(target_os = "macos")]
+fn cc_config() {
+ println!("cargo:rustc-link-lib=dylib=c++");
+ println!("cargo:rustc-link-lib=static=macbridge");
+ println!("cargo:rustc-link-lib=framework=Cocoa");
+ println!("cargo:rustc-link-lib=framework=IOKit");
+}
+
+fn main() {
+ cc_config();
+}
diff --git a/espanso-ui/src/event.rs b/espanso-ui/src/event.rs
new file mode 100644
index 0000000..98a298a
--- /dev/null
+++ b/espanso-ui/src/event.rs
@@ -0,0 +1,23 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum UIEvent {
+ TrayIconClick,
+}
diff --git a/espanso-ui/src/icons.rs b/espanso-ui/src/icons.rs
new file mode 100644
index 0000000..5fea61e
--- /dev/null
+++ b/espanso-ui/src/icons.rs
@@ -0,0 +1,8 @@
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum TrayIcon {
+ Normal,
+ Disabled,
+
+ // For example, when macOS activates SecureInput
+ SystemDisabled,
+}
diff --git a/espanso-ui/src/lib.rs b/espanso-ui/src/lib.rs
new file mode 100644
index 0000000..58e04ee
--- /dev/null
+++ b/espanso-ui/src/lib.rs
@@ -0,0 +1,5 @@
+pub mod event;
+pub mod icons;
+
+#[cfg(target_os = "windows")]
+pub mod win32;
diff --git a/espanso-ui/src/win32/mod.rs b/espanso-ui/src/win32/mod.rs
new file mode 100644
index 0000000..ab9d9e3
--- /dev/null
+++ b/espanso-ui/src/win32/mod.rs
@@ -0,0 +1,269 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+use std::{
+ cmp::min,
+ collections::HashMap,
+ ffi::c_void,
+ sync::{
+ atomic::{AtomicPtr, Ordering},
+ Arc,
+ },
+ thread::ThreadId,
+};
+
+use lazycell::LazyCell;
+use log::{error, trace};
+use widestring::WideString;
+
+use crate::{event::UIEvent, icons::TrayIcon};
+
+// IMPORTANT: if you change these, also edit the native.h file.
+const MAX_FILE_PATH: usize = 260;
+const MAX_ICON_COUNT: usize = 3;
+
+const UI_EVENT_TYPE_ICON_CLICK: i32 = 1;
+//const UI_EVENT_TYPE_CONTEXT_MENU_CLICK: i32 = 2;
+
+// Take a look at the native.h header file for an explanation of the fields
+#[repr(C)]
+pub struct RawUIOptions {
+ pub show_icon: i32,
+
+ pub icon_paths: [[u16; MAX_FILE_PATH]; MAX_ICON_COUNT],
+ pub icon_paths_count: i32,
+}
+// Take a look at the native.h header file for an explanation of the fields
+#[repr(C)]
+pub struct RawUIEvent {
+ pub event_type: i32,
+}
+
+#[allow(improper_ctypes)]
+#[link(name = "espansoui", kind = "static")]
+extern "C" {
+ pub fn ui_initialize(_self: *const Win32EventLoop, options: RawUIOptions) -> *mut c_void;
+ pub fn ui_eventloop(
+ window_handle: *const c_void,
+ event_callback: extern "C" fn(_self: *mut Win32EventLoop, event: RawUIEvent),
+ ) -> i32;
+ pub fn ui_destroy(window_handle: *const c_void) -> i32;
+ pub fn ui_update_tray_icon(window_handle: *const c_void, index: i32);
+}
+
+pub struct Win32UIOptions<'a> {
+ pub show_icon: bool,
+ pub icon_paths: &'a Vec<(TrayIcon, String)>,
+}
+
+pub fn create(options: Win32UIOptions) -> (Win32Remote, Win32EventLoop) {
+ let handle: Arc> = Arc::new(AtomicPtr::new(std::ptr::null_mut()));
+
+ // Validate icons
+ if options.icon_paths.len() > MAX_ICON_COUNT {
+ panic!("Win32 UI received too many icon paths, please increase the MAX_ICON_COUNT constant to support more");
+ }
+
+ // Convert the icon paths to the internal representation
+ let mut icon_indexes: HashMap = HashMap::new();
+ let mut icons = Vec::new();
+ for (index, (tray_icon, path)) in options.icon_paths.iter().enumerate() {
+ icon_indexes.insert(tray_icon.clone(), index);
+ icons.push(path.clone());
+ }
+
+ let eventloop = Win32EventLoop::new(handle.clone(), icons, options.show_icon);
+ let remote = Win32Remote::new(handle, icon_indexes);
+
+ (remote, eventloop)
+}
+
+pub type Win32UIEventCallback = Box;
+
+pub struct Win32EventLoop {
+ handle: Arc>,
+
+ show_icon: bool,
+ icons: Vec,
+
+ // Internal
+ _event_callback: LazyCell,
+ _init_thread_id: LazyCell,
+}
+
+impl Win32EventLoop {
+ pub(crate) fn new(handle: Arc>, icons: Vec, show_icon: bool) -> Self {
+ Self {
+ handle,
+ icons,
+ show_icon,
+ _event_callback: LazyCell::new(),
+ _init_thread_id: LazyCell::new(),
+ }
+ }
+
+ pub fn initialize(&mut self) {
+ let window_handle = self.handle.load(Ordering::Acquire);
+ if !window_handle.is_null() {
+ panic!("Attempt to initialize Win32EventLoop on non-null window handle");
+ }
+
+ // Convert the icon paths to the raw representation
+ let mut icon_paths: [[u16; MAX_FILE_PATH]; MAX_ICON_COUNT] =
+ [[0; MAX_FILE_PATH]; MAX_ICON_COUNT];
+ for i in 0..self.icons.len() {
+ let wide_path = WideString::from_str(&self.icons[i]);
+ let len = min(wide_path.len(), MAX_FILE_PATH - 1);
+ icon_paths[i][0..len].clone_from_slice(&wide_path.as_slice()[..len]);
+ // TODO: test overflow, correct case
+ }
+
+ let options = RawUIOptions {
+ show_icon: if self.show_icon { 1 } else { 0 },
+ icon_paths,
+ icon_paths_count: self.icons.len() as i32,
+ };
+
+ let handle = unsafe { ui_initialize(self as *const Win32EventLoop, options) };
+
+ if handle.is_null() {
+ panic!("Unable to initialize Win32EventLoop");
+ }
+
+ self.handle.store(handle, Ordering::Release);
+
+ // TODO: explain
+ self
+ ._init_thread_id
+ .fill(std::thread::current().id())
+ .expect("Unable to set initialization thread id");
+ }
+
+ pub fn run(&self, event_callback: Win32UIEventCallback) {
+ // Make sure the run() method is called in the same thread as initialize()
+ if let Some(init_id) = self._init_thread_id.borrow() {
+ if init_id != &std::thread::current().id() {
+ panic!("Win32EventLoop run() and initialize() methods should be called in the same thread");
+ // TODO: test
+ }
+ }
+
+ let window_handle = self.handle.load(Ordering::Acquire);
+ if window_handle.is_null() {
+ panic!("Attempt to run Win32EventLoop on a null window handle");
+ // TODO: test
+ }
+
+ if let Err(_) = self._event_callback.fill(event_callback) {
+ panic!("Unable to set Win32EventLoop callback");
+ }
+
+ extern "C" fn callback(_self: *mut Win32EventLoop, event: RawUIEvent) {
+ if let Some(callback) = unsafe { (*_self)._event_callback.borrow() } {
+ let event: Option = event.into();
+ if let Some(event) = event {
+ callback(event)
+ } else {
+ trace!("Unable to convert raw event to input event");
+ }
+ }
+ }
+
+ let error_code = unsafe { ui_eventloop(window_handle, callback) };
+
+ if error_code <= 0 {
+ panic!("Win32EventLoop exited with <= 0 code")
+ }
+ }
+}
+
+impl Drop for Win32EventLoop {
+ fn drop(&mut self) {
+ let handle = self.handle.swap(std::ptr::null_mut(), Ordering::Acquire);
+ if handle.is_null() {
+ error!("Win32EventLoop destruction cannot be performed, handle is null");
+ return;
+ }
+
+ let result = unsafe { ui_destroy(handle) };
+
+ if result != 0 {
+ error!("Win32EventLoop destruction returned non-zero code");
+ }
+ }
+}
+
+pub struct Win32Remote {
+ handle: Arc>,
+
+ // Maps icon name to their index
+ icon_indexes: HashMap,
+}
+
+impl Win32Remote {
+ pub(crate) fn new(
+ handle: Arc>,
+ icon_indexes: HashMap,
+ ) -> Self {
+ Self {
+ handle,
+ icon_indexes,
+ }
+ }
+
+ pub fn update_tray_icon(&self, icon: TrayIcon) {
+ let handle = self.handle.load(Ordering::Acquire);
+ if handle.is_null() {
+ error!("Unable to update tray icon, pointer is null");
+ return;
+ }
+
+ if let Some(index) = self.icon_indexes.get(&icon) {
+ unsafe { ui_update_tray_icon(handle, (*index) as i32) }
+ } else {
+ error!("Unable to update tray icon, invalid icon id");
+ return;
+ }
+ }
+}
+
+impl From for Option {
+ fn from(raw: RawUIEvent) -> Option {
+ match raw.event_type {
+ // Keyboard events
+ UI_EVENT_TYPE_ICON_CLICK => {
+ return Some(UIEvent::TrayIconClick);
+ }
+ _ => {}
+ }
+
+ None
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn constants_are_not_changed_by_mistake() {
+ assert_eq!(MAX_FILE_PATH, 260);
+ assert_eq!(MAX_ICON_COUNT, 3);
+ }
+}
diff --git a/espanso-ui/src/win32/native.cpp b/espanso-ui/src/win32/native.cpp
new file mode 100644
index 0000000..3216200
--- /dev/null
+++ b/espanso-ui/src/win32/native.cpp
@@ -0,0 +1,276 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+#include "native.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define UNICODE
+
+#ifdef __MINGW32__
+#ifndef WINVER
+#define WINVER 0x0606
+#endif
+#define STRSAFE_NO_DEPRECATE
+#endif
+
+#include
+#include
+#include
+#pragma comment(lib, "Shell32.lib")
+#include
+
+#pragma comment(lib, "Gdi32.lib")
+#include
+
+#define APPWM_ICON_CLICK (WM_APP + 1)
+#define APPWM_SHOW_CONTEXT_MENU (WM_APP + 2)
+#define APPWM_UPDATE_TRAY_ICON (WM_APP + 3)
+
+const wchar_t *const ui_winclass = L"EspansoUI";
+
+typedef struct {
+ UIOptions options;
+ NOTIFYICONDATA nid;
+ HICON g_icons[MAX_ICON_COUNT];
+
+ // Rust interop
+ void *rust_instance;
+ EventCallback event_callback;
+} UIVariables;
+
+
+// Needed to detect when Explorer crashes
+UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated");
+
+/*
+ * Message handler procedure for the window
+ */
+LRESULT CALLBACK ui_window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp)
+{
+ UIEvent event = {};
+ UIVariables * variables = reinterpret_cast(GetWindowLongPtrW(window, GWLP_USERDATA));
+
+ switch (msg)
+ {
+ case WM_DESTROY:
+ PostQuitMessage(0);
+
+ // Free the tray icons
+ for (int i = 0; i < variables->options.icon_paths_count; i++)
+ {
+ DeleteObject(variables->g_icons[i]);
+ }
+
+ // Free the window variables
+ delete variables;
+ SetWindowLongPtrW(window, GWLP_USERDATA, NULL);
+
+ return 0L;
+ case WM_COMMAND: // Click on the tray icon context menu
+ {
+ UINT idItem = (UINT)LOWORD(wp);
+ UINT flags = (UINT)HIWORD(wp);
+
+ if (flags == 0)
+ {
+ std::cout << "click menu" << std::flush;
+ //context_menu_click_callback(manager_instance, (int32_t)idItem);
+ }
+
+ break;
+ }
+ case APPWM_SHOW_CONTEXT_MENU: // Request to show context menu
+ {
+ HMENU hPopupMenu = CreatePopupMenu();
+
+ // Create the menu
+
+ /*
+ int32_t count = static_cast(lp);
+ std::unique_ptr