diff --git a/Cargo.lock b/Cargo.lock index 4743738..3739ef8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,3 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. [[package]] name = "cc" version = "1.0.41" diff --git a/build.rs b/build.rs index 951a45b..edf162b 100644 --- a/build.rs +++ b/build.rs @@ -1,11 +1,41 @@ extern crate cmake; use cmake::Config; +use std::path::PathBuf; + +/* OS SPECIFIC CONFIGS */ + +#[cfg(target_os = "windows")] +fn get_config() -> PathBuf { + Config::new("native/libwinbridge").build() +} + +#[cfg(target_os = "linux")] +fn get_config() -> PathBuf { + Config::new("native/liblinuxbridge").build() +} + +/* + OS CUSTOM CARGO CONFIG LINES + Note: this is where linked libraries should be specified. +*/ + +#[cfg(target_os = "windows")] +fn print_config() { + println!("cargo:rustc-link-lib=static=winbridge"); + println!("cargo:rustc-link-lib=static=user32"); +} + +#[cfg(target_os = "linux")] +fn print_config() { + println!("cargo:rustc-link-lib=static=linuxbridge"); + println!("cargo:rustc-link-lib=dylib=X11"); + println!("cargo:rustc-link-lib=dylib=Xtst"); +} fn main() { - let dst = Config::new("native/libwinbridge").build(); + let dst = get_config(); println!("cargo:rustc-link-search=native={}", dst.display()); - println!("cargo:rustc-link-lib=static=winbridge"); - println!("cargo:rustc-link-lib=static=user32"); + print_config(); } \ No newline at end of file diff --git a/native/liblinuxbridge/CMakeLists.txt b/native/liblinuxbridge/CMakeLists.txt new file mode 100644 index 0000000..dd74a8b --- /dev/null +++ b/native/liblinuxbridge/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.0) +project(liblinuxbridge) + +set (CMAKE_CXX_STANDARD 14) + +add_library(linuxbridge STATIC bridge.cpp bridge.h) + +install(TARGETS linuxbridge DESTINATION .) \ No newline at end of file diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp new file mode 100644 index 0000000..57706fa --- /dev/null +++ b/native/liblinuxbridge/bridge.cpp @@ -0,0 +1,185 @@ +#include "bridge.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* +This code uses the X11 Record Extension to receive keyboard +events. Documentation of this library can be found here: +https://www.x.org/releases/X11R7.6/doc/libXtst/recordlib.html + +We will refer to this extension as RE from now on. +*/ + +/* +This struct is needed to receive events from the RE. +The funny thing is: it's not defined there, it should though. +The only place this is mentioned is the libxnee library, +so check that out if you need a reference. +*/ +typedef union { + unsigned char type ; + xEvent event ; + xResourceReq req ; + xGenericReply reply ; + xError error ; + xConnSetupPrefix setup; +} XRecordDatum; + +/* +Connections to the X server, RE recommends 2 connections: +one for recording control and one for reading the recorded data. +*/ +Display *data_disp = NULL; +Display *ctrl_disp = NULL; + +XRecordRange *record_range; +XRecordContext context; + +// Callback invoked when a new key event occur. +void event_callback (XPointer, XRecordInterceptData*); + +KeypressCallback keypress_callback; +void * interceptor_instance; + +void register_keypress_callback(void * self, KeypressCallback callback) { + keypress_callback = callback; + interceptor_instance = self; +} + + +int32_t initialize() { + /* + Open the connections to the X server. + RE recommends to open 2 connections to the X server: + one for the recording control and one to read the protocol + data. + */ + ctrl_disp = XOpenDisplay(NULL); + data_disp = XOpenDisplay(NULL); + + if (!ctrl_disp || !data_disp) { // Display error + return -1; + } + + /* + We must set the ctrl_disp to sync mode, or, when we the enable + context in data_disp, there will be a fatal X error. + */ + XSynchronize(ctrl_disp, True); + + int dummy; + + // Make sure the X RE is installed in this system. + if (!XRecordQueryVersion(ctrl_disp, &dummy, &dummy)) { + return -2; + } + + // Make sure the X Keyboard Extension is installed + if (!XkbQueryExtension(ctrl_disp, &dummy, &dummy, &dummy, &dummy, &dummy)) { + return -3; + } + + // Initialize the record range, that is the kind of events we want to track. + record_range = XRecordAllocRange (); + if (!record_range) { + return -4; + } + record_range->device_events.first = KeyPress; + record_range->device_events.last = KeyRelease; + + // We want to get the keys from all clients + XRecordClientSpec client_spec; + client_spec = XRecordAllClients; + + // Initialize the context + context = XRecordCreateContext(ctrl_disp, 0, &client_spec, 1, &record_range, 1); + if (!context) { + return -5; + } +} + +int32_t eventloop() { + if (!XRecordEnableContext (data_disp, context, event_callback, NULL)) { + return -1; + } + + return 1; +} + +void cleanup() { + XRecordDisableContext(ctrl_disp, context); + XRecordFreeContext(ctrl_disp, context); + XFree (record_range); + XCloseDisplay(data_disp); + XCloseDisplay(ctrl_disp); +} + +void event_callback(XPointer p, XRecordInterceptData *hook) +{ + // Make sure the event comes from the X11 server + if (hook->category != XRecordFromServer) { + XRecordFreeData(hook); + return; + } + + // Cast the event payload to a XRecordDatum, needed later to access the fields + // This struct was hard to find and understand. Turn's out that all the + // required data are included in the "event" field of this structure. + // The funny thing is that it's not a XEvent as one might expect, + // but a xEvent, a very different beast defined in the Xproto.h header. + // I suggest you to look at that header if you want to understand where the + // upcoming field where taken from. + XRecordDatum *data = (XRecordDatum*) hook->data; + + int event_type = data->type; + int key_code = data->event.u.u.detail; + + // In order to convert the key_code into the corresponding string, + // we need to synthesize an artificial XKeyEvent, to feed later to the + // XLookupString function. + XKeyEvent event; + event.display = ctrl_disp; + event.window = data->event.u.focus.window; + event.root = XDefaultRootWindow(ctrl_disp); + event.subwindow = None; + event.time = data->event.u.keyButtonPointer.time; + event.x = 1; + event.y = 1; + event.x_root = 1; + event.y_root = 1; + event.same_screen = True; + event.keycode = key_code; + event.state = data->event.u.keyButtonPointer.state; + event.type = KeyPress; + + // Extract the corresponding chars. + std::array buffer; + int res = XLookupString(&event, buffer.data(), buffer.size(), NULL, NULL); + + switch (event_type) { + case KeyRelease: + printf ("%d %d KeyPress: \t%s\t%s\n", key_code, res, XKeysymToString(XkbKeycodeToKeysym(ctrl_disp, key_code, 0, 0)), buffer.data()); + if (res > 0) { // Send only printable chars, todo: change + keypress_callback(interceptor_instance, buffer.data(), buffer.size()); + } + break; +// case KeyPress: +// printf ("%d %d KeyPress: \t%s\t%s\t%d\n", keycode, res, XKeysymToString(XkbKeycodeToKeysym(ctrl_disp, keycode, 0, 0)), buff, buff[0]); +// break; + default: + break; + } + + XRecordFreeData(hook); +} \ No newline at end of file diff --git a/native/liblinuxbridge/bridge.h b/native/liblinuxbridge/bridge.h new file mode 100644 index 0000000..f543f84 --- /dev/null +++ b/native/liblinuxbridge/bridge.h @@ -0,0 +1,35 @@ +#ifndef ESPANSO_BRIDGE_H +#define ESPANSO_BRIDGE_H + +#include + +/* + * Initialize the X11 context and parameters + */ +extern "C" int32_t initialize(); + +/* + * Start the event loop indefinitely. Blocking call. + */ +extern "C" int32_t eventloop(); + +/* + * Clean all the X11 resources allocated during the initialization. + */ +extern "C" void cleanup(); + +/* + * Called when a new keypress is made, the first argument is an char array, + * while the second is the size of the array. + */ +typedef void (*KeypressCallback)(void * self, char *buffer, int32_t len); + +extern KeypressCallback keypress_callback; +extern void * interceptor_instance; + +/* + * Register the callback that will be called when a keypress was made + */ +extern "C" void register_keypress_callback(void *self, KeypressCallback callback); + +#endif //ESPANSO_BRIDGE_H diff --git a/src/keyboard/linux.rs b/src/keyboard/linux.rs new file mode 100644 index 0000000..af0f650 --- /dev/null +++ b/src/keyboard/linux.rs @@ -0,0 +1,63 @@ +use std::thread; +use std::sync::mpsc; + +#[repr(C)] +pub struct LinuxKeyboardInterceptor { + pub sender: mpsc::Sender +} + +impl super::KeyboardInterceptor for LinuxKeyboardInterceptor { + fn initialize(&self) { + unsafe { + register_keypress_callback(self,keypress_callback); + } + } + + fn start(&self) { + thread::spawn(|| { + unsafe { + initialize(); + eventloop(); + } + }); + } +} + +pub struct LinuxKeyboardSender { +} + +impl super::KeyboardSender for LinuxKeyboardSender { + fn send_string(&self, s: &str) { + println!("{}", s); + + } + + fn delete_string(&self, count: i32) { + + } +} + +// Native bridge code + +extern fn keypress_callback(_self: *mut LinuxKeyboardInterceptor, raw_buffer: *const u8, len: i32) { + unsafe { + // Convert the received buffer to a character + let buffer = std::slice::from_raw_parts(raw_buffer, len as usize); + let r = String::from_utf8_lossy(buffer).chars().nth(0); + + // Send the char through the channel + if let Some(c) = r { + //println!("'{}'",c); + (*_self).sender.send(c).unwrap(); + } + } +} + +#[allow(improper_ctypes)] +#[link(name="linuxbridge", kind="static")] +extern { + fn register_keypress_callback(s: *const LinuxKeyboardInterceptor, cb: extern fn(_self: *mut LinuxKeyboardInterceptor, *const u8, i32)); + fn initialize(); + fn eventloop(); + fn cleanup(); +} \ No newline at end of file diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs index 2efaebf..113e810 100644 --- a/src/keyboard/mod.rs +++ b/src/keyboard/mod.rs @@ -1,6 +1,9 @@ #[cfg(target_os = "windows")] mod windows; +#[cfg(target_os = "linux")] +mod linux; + use std::sync::mpsc; pub trait KeyboardInterceptor { @@ -13,6 +16,8 @@ pub trait KeyboardSender { fn delete_string(&self, count: i32); } +// WINDOWS IMPLEMENTATIONS + #[cfg(target_os = "windows")] pub fn get_interceptor(sender: mpsc::Sender) -> impl KeyboardInterceptor { windows::WindowsKeyboardInterceptor {sender} @@ -21,4 +26,16 @@ pub fn get_interceptor(sender: mpsc::Sender) -> impl KeyboardInterceptor { #[cfg(target_os = "windows")] pub fn get_sender() -> impl KeyboardSender { windows::WindowsKeyboardSender{} +} + +// LINUX IMPLEMENTATIONS + +#[cfg(target_os = "linux")] +pub fn get_interceptor(sender: mpsc::Sender) -> impl KeyboardInterceptor { + linux::LinuxKeyboardInterceptor {sender} +} + +#[cfg(target_os = "linux")] +pub fn get_sender() -> impl KeyboardSender { + linux::LinuxKeyboardSender{} } \ No newline at end of file diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index 9c3eed8..4644cf9 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -27,7 +27,6 @@ impl <'a> super::Matcher for ScrollingMatcher<'a> { self.current_set = old_matches; self.current_set.append(&mut new_matches); - let mut found = false; let mut foundMatch = None; for entry in self.current_set.iter_mut() {