diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index feab87a..e0064be 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -4,6 +4,8 @@ #include #include #include +#include + #include #include #include @@ -13,6 +15,7 @@ #include #include #include +#include extern "C" { // Needed to avoid C++ compiler name mangling #include } @@ -210,4 +213,105 @@ void trigger_paste() { void trigger_terminal_paste() { xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+Shift+v", 8000); +} + +// SYSTEM MODULE + +// Function taken from the wmlib tool source code +char *get_property(Display *disp, Window win, + Atom xa_prop_type, char *prop_name, unsigned long *size) +{ + unsigned long ret_nitems, ret_bytes_after, tmp_size; + Atom xa_prop_name, xa_ret_type; + unsigned char *ret_prop; + int ret_format; + char *ret; + int size_in_byte; + + xa_prop_name = XInternAtom(disp, prop_name, False); + + if (XGetWindowProperty(disp, win, xa_prop_name, 0, 4096 / 4, False, + xa_prop_type, &xa_ret_type, &ret_format, &ret_nitems, + &ret_bytes_after, &ret_prop) != Success) + return NULL; + + if (xa_ret_type != xa_prop_type) + { + XFree(ret_prop); + return NULL; + } + + switch(ret_format) { + case 8: size_in_byte = sizeof(char); break; + case 16: size_in_byte = sizeof(short); break; + case 32: size_in_byte = sizeof(long); break; + } + + tmp_size = size_in_byte * ret_nitems; + ret = (char*) malloc(tmp_size + 1); + memcpy(ret, ret_prop, tmp_size); + ret[tmp_size] = '\0'; + + if (size) *size = tmp_size; + + XFree(ret_prop); + return ret; +} + +// Function taken from Window Management Library for Ruby +char *xwm_get_win_title(Display *disp, Window win) +{ + char *wname = (char*)get_property(disp,win, XA_STRING, "WM_NAME", NULL); + char *nwname = (char*)get_property(disp,win, XInternAtom(disp, + "UTF8_STRING", False), "_NET_WM_NAME", NULL); + + return nwname ? nwname : (wname ? wname : NULL); +} + +int32_t get_active_window_name(char * buffer, int32_t size) { + Display *disp = XOpenDisplay(NULL); + + if (!disp) { + return -1; + } + + // Get the active window + Window win; + int revert_to_return; + XGetInputFocus(disp, &win, &revert_to_return); + + char * title = xwm_get_win_title(disp, win); + + snprintf(buffer, size, "%s", title); + + XFree(title); + + XCloseDisplay(disp); + + return 1; +} + +int32_t get_active_window_class(char * buffer, int32_t size) { + Display *disp = XOpenDisplay(NULL); + + if (!disp) { + return -1; + } + + // Get the active window + Window win; + int revert_to_return; + XGetInputFocus(disp, &win, &revert_to_return); + + XClassHint hint; + + if (XGetClassHint(disp, win, &hint)) { + snprintf(buffer, size, "%s", hint.res_class); + XFree(hint.res_name); + XFree(hint.res_class); + } + + XCloseDisplay(disp); + + return 1; } \ No newline at end of file diff --git a/native/liblinuxbridge/bridge.h b/native/liblinuxbridge/bridge.h index dabe854..92d8276 100644 --- a/native/liblinuxbridge/bridge.h +++ b/native/liblinuxbridge/bridge.h @@ -52,4 +52,17 @@ extern "C" void trigger_paste(); */ extern "C" void trigger_terminal_paste(); + +// SYSTEM MODULE + +/* + * Return the active windows's WM_NAME + */ +extern "C" int32_t get_active_window_name(char * buffer, int32_t size); + +/* + * Return the active windows's WM_CLASS + */ +extern "C" int32_t get_active_window_class(char * buffer, int32_t size); + #endif //ESPANSO_BRIDGE_H diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs new file mode 100644 index 0000000..aa44951 --- /dev/null +++ b/src/bridge/linux.rs @@ -0,0 +1,21 @@ +use std::os::raw::{c_void, c_char}; + +#[allow(improper_ctypes)] +#[link(name="linuxbridge", kind="static")] +extern { + // System + pub fn get_active_window_name(buffer: *mut c_char, size: i32) -> i32; + pub fn get_active_window_class(buffer: *mut c_char, size: i32) -> i32; + + // Keyboard + pub fn register_keypress_callback(s: *const c_void, + cb: extern fn(_self: *mut c_void, *const u8, + i32, i32, i32)); + pub fn initialize(); + pub fn eventloop(); + pub fn cleanup(); + pub fn send_string(string: *const c_char); + pub fn delete_string(count: i32); + pub fn trigger_paste(); + pub fn trigger_terminal_paste(); +} \ No newline at end of file diff --git a/src/bridge/mod.rs b/src/bridge/mod.rs new file mode 100644 index 0000000..a86b635 --- /dev/null +++ b/src/bridge/mod.rs @@ -0,0 +1,8 @@ +#[cfg(target_os = "windows")] +pub(crate) mod windows; + +#[cfg(target_os = "linux")] +pub(crate) mod linux; + +#[cfg(target_os = "macos")] +pub(crate) mod macos; \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index e821ba9..a0046f7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -67,6 +67,7 @@ impl Configs { } } +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct ConfigSet { default: Configs, specific: Vec, @@ -78,17 +79,21 @@ impl ConfigSet { panic!("Invalid config directory"); } - let default_file = espanso_dir.join(DEFAULT_CONFIG_FILE_NAME); - let default = Configs::load_config(default_file); + let default_file = dir_path.join(DEFAULT_CONFIG_FILE_NAME); + let default = Configs::load_config(default_file.as_path()); let mut specific = Vec::new(); - for entry in fs::read_dir(dir_path)? { - let entry = entry?; - let path = entry.path(); + for entry in fs::read_dir(dir_path) + .expect("Cannot read espanso config directory!") { - let config = Configs::load_config(path.as_path()); - specific.push(config); + let entry = entry; + if let Ok(entry) = entry { + let path = entry.path(); + + let config = Configs::load_config(path.as_path()); + specific.push(config); + } } ConfigSet { @@ -103,7 +108,7 @@ impl ConfigSet { let espanso_dir = home_dir.join(".espanso"); // Create the espanso dir if id doesn't exist - let res = create_dir_all(espanso_dir); + let res = create_dir_all(espanso_dir.as_path()); if let Ok(_) = res { let default_file = espanso_dir.join(DEFAULT_CONFIG_FILE_NAME); @@ -120,4 +125,24 @@ impl ConfigSet { panic!("Could not generate default position for config file"); } + + pub fn toggle_key(&self) -> &KeyModifier { + &self.default.toggle_key + } + + pub fn toggle_interval(&self) -> u32 { + self.default.toggle_interval + } + + pub fn backspace_limit(&self) -> i32 { + self.default.backspace_limit + } + + pub fn backend(&self) -> &BackendType { + &BackendType::Inject // TODO make dynamic based on system current active app + } + + pub fn matches(&self) -> &Vec { + &self.default.matches + } } \ No newline at end of file diff --git a/src/engine.rs b/src/engine.rs index 55afc3b..0f86cd8 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,6 +1,6 @@ use crate::matcher::{Match, MatchReceiver}; use crate::keyboard::KeyboardSender; -use crate::config::Configs; +use crate::config::ConfigSet; use crate::config::BackendType; use crate::clipboard::ClipboardManager; use std::sync::Arc; @@ -8,12 +8,12 @@ use std::sync::Arc; pub struct Engine where S: KeyboardSender, C: ClipboardManager { sender: S, clipboard_manager: Arc, - configs: Configs, + config_set: ConfigSet, } impl Engine where S: KeyboardSender, C: ClipboardManager{ - pub fn new(sender: S, clipboard_manager: Arc, configs: Configs) -> Engine where S: KeyboardSender, C: ClipboardManager { - Engine{sender, clipboard_manager, configs } + pub fn new(sender: S, clipboard_manager: Arc, config_set: ConfigSet) -> Engine where S: KeyboardSender, C: ClipboardManager { + Engine{sender, clipboard_manager, config_set } } } @@ -21,7 +21,7 @@ impl MatchReceiver for Engine where S: KeyboardSender, C: Clipboard fn on_match(&self, m: &Match) { self.sender.delete_string(m.trigger.len() as i32); - match self.configs.backend { + match self.config_set.backend() { BackendType::Inject => { // Send the expected string. On linux, newlines are managed automatically // while on windows and macos, we need to emulate a Enter key press. diff --git a/src/keyboard/linux.rs b/src/keyboard/linux.rs index 91a3214..f5ccd67 100644 --- a/src/keyboard/linux.rs +++ b/src/keyboard/linux.rs @@ -1,10 +1,12 @@ -use std::thread; +use std::{thread}; use std::sync::mpsc; -use std::os::raw::c_char; +use std::os::raw::{c_char, c_void}; use std::ffi::CString; use crate::keyboard::{KeyEvent, KeyModifier}; use crate::keyboard::KeyModifier::*; +use crate::bridge::linux::*; + #[repr(C)] pub struct LinuxKeyboardInterceptor { pub sender: mpsc::Sender @@ -13,7 +15,8 @@ pub struct LinuxKeyboardInterceptor { impl super::KeyboardInterceptor for LinuxKeyboardInterceptor { fn initialize(&self) { unsafe { - register_keypress_callback(self,keypress_callback); + let self_ptr = self as *const LinuxKeyboardInterceptor as *const c_void; + register_keypress_callback( self_ptr,keypress_callback); initialize(); // TODO: check initialization return codes } } @@ -60,9 +63,11 @@ impl super::KeyboardSender for LinuxKeyboardSender { // Native bridge code -extern fn keypress_callback(_self: *mut LinuxKeyboardInterceptor, raw_buffer: *const u8, len: i32, +extern fn keypress_callback(_self: *mut c_void, raw_buffer: *const u8, len: i32, is_modifier: i32, key_code: i32) { unsafe { + let _self = _self as *mut LinuxKeyboardInterceptor; + if is_modifier == 0 { // Char event // Convert the received buffer to a character let buffer = std::slice::from_raw_parts(raw_buffer, len as usize); @@ -87,19 +92,4 @@ extern fn keypress_callback(_self: *mut LinuxKeyboardInterceptor, raw_buffer: *c } } } -} - -#[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, i32, i32)); - fn initialize(); - fn eventloop(); - fn cleanup(); - fn send_string(string: *const c_char); - fn delete_string(count: i32); - fn trigger_paste(); - fn trigger_terminal_paste(); } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d8b583d..380153c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,18 +3,21 @@ use crate::keyboard::KeyboardInterceptor; use crate::matcher::Matcher; use crate::matcher::scrolling::ScrollingMatcher; use crate::engine::Engine; -use crate::config::Configs; +use crate::config::{Configs, ConfigSet}; use crate::ui::UIManager; use crate::clipboard::ClipboardManager; +use crate::system::SystemManager; use std::thread; use clap::{App, Arg}; use std::path::Path; -mod keyboard; -mod matcher; +mod ui; +mod bridge; mod engine; mod config; -mod ui; +mod system; +mod matcher; +mod keyboard; mod clipboard; const VERSION: &'static str = env!("CARGO_PKG_VERSION"); @@ -28,7 +31,7 @@ fn main() { .short("c") .long("config") .value_name("FILE") - .help("Sets a custom config file. If not specified, reads the default $HOME/.espanso file, creating it if not present.") + .help("Sets a custom config directory. If not specified, reads the default $HOME/.espanso/default.yaml file, creating it if not present.") .takes_value(true)) .arg(Arg::with_name("dump") .long("dump") @@ -39,23 +42,27 @@ fn main() { .help("Sets the level of verbosity")) .get_matches(); - let configs = match matches.value_of("config") { - None => {Configs::load_default()}, - Some(path) => {Configs::load(Path::new(path))}, + let config_set = match matches.value_of("config") { + None => {ConfigSet::load_default()}, + Some(path) => {ConfigSet::load(Path::new(path))}, }; if matches.is_present("dump") { - println!("{:#?}", configs); + println!("{:#?}", config_set); return; } - espanso_main(configs); + espanso_main(config_set); } -fn espanso_main(configs: Configs) { +fn espanso_main(config_set: ConfigSet) { let ui_manager = ui::get_uimanager(); ui_manager.notify("Hello guys"); + let system_manager = system::get_manager(); + println!("{}", system_manager.get_current_window_title().unwrap()); + println!("{}", system_manager.get_current_window_class().unwrap()); + let clipboard_manager = clipboard::get_manager(); let clipboard_manager_arc = Arc::new(clipboard_manager); @@ -65,10 +72,10 @@ fn espanso_main(configs: Configs) { let engine = Engine::new(sender, Arc::clone(&clipboard_manager_arc), - configs.clone()); + config_set.clone()); thread::spawn(move || { - let matcher = ScrollingMatcher::new(configs.clone(), engine); + let matcher = ScrollingMatcher::new(config_set.clone(), engine); matcher.watch(rxc); }); diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index 3f1a1f1..17660ca 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -1,13 +1,13 @@ use crate::matcher::{Match, MatchReceiver}; use std::cell::RefCell; use crate::keyboard::KeyModifier; -use crate::config::Configs; +use crate::config::ConfigSet; use crate::keyboard::KeyModifier::BACKSPACE; use std::time::SystemTime; use std::collections::VecDeque; pub struct ScrollingMatcher<'a, R> where R: MatchReceiver{ - configs: Configs, + config_set: ConfigSet, receiver: R, current_set_queue: RefCell>>>, toggle_press_time: RefCell, @@ -28,7 +28,7 @@ impl <'a, R> super::Matcher<'a> for ScrollingMatcher<'a, R> where R: MatchReceiv let mut current_set_queue = self.current_set_queue.borrow_mut(); - let new_matches: Vec = self.configs.matches.iter() + let new_matches: Vec = self.config_set.matches().iter() .filter(|&x| x.trigger.chars().nth(0).unwrap() == c) .map(|x | MatchEntry{start: 1, _match: &x}) .collect(); @@ -60,7 +60,7 @@ impl <'a, R> super::Matcher<'a> for ScrollingMatcher<'a, R> where R: MatchReceiv current_set_queue.push_back(combined_matches); - if current_set_queue.len() as i32 > (self.configs.backspace_limit + 1) { + if current_set_queue.len() as i32 > (self.config_set.backspace_limit() + 1) { current_set_queue.pop_front(); } @@ -73,10 +73,10 @@ impl <'a, R> super::Matcher<'a> for ScrollingMatcher<'a, R> where R: MatchReceiv } fn handle_modifier(&'a self, m: KeyModifier) { - if m == self.configs.toggle_key { + if m == *self.config_set.toggle_key() { let mut toggle_press_time = self.toggle_press_time.borrow_mut(); if let Ok(elapsed) = toggle_press_time.elapsed() { - if elapsed.as_millis() < self.configs.toggle_interval as u128 { + if elapsed.as_millis() < self.config_set.toggle_interval() as u128 { let mut is_enabled = self.is_enabled.borrow_mut(); *is_enabled = !(*is_enabled); @@ -99,12 +99,12 @@ impl <'a, R> super::Matcher<'a> for ScrollingMatcher<'a, R> where R: MatchReceiv } } impl <'a, R> ScrollingMatcher<'a, R> where R: MatchReceiver { - pub fn new(configs: Configs, receiver: R) -> ScrollingMatcher<'a, R> { + pub fn new(config_set: ConfigSet, receiver: R) -> ScrollingMatcher<'a, R> { let current_set_queue = RefCell::new(VecDeque::new()); let toggle_press_time = RefCell::new(SystemTime::now()); ScrollingMatcher{ - configs, + config_set, receiver, current_set_queue, toggle_press_time, diff --git a/src/system/linux.rs b/src/system/linux.rs new file mode 100644 index 0000000..588df52 --- /dev/null +++ b/src/system/linux.rs @@ -0,0 +1,58 @@ +use std::os::raw::c_char; + +use crate::bridge::linux::{get_active_window_name, get_active_window_class}; +use std::ffi::CStr; + +pub struct LinuxSystemManager { + +} + +impl super::SystemManager for LinuxSystemManager { + fn initialize(&self) { + + } + + fn get_current_window_title(&self) -> Option { + unsafe { + let mut buffer : [c_char; 100] = [0; 100]; + let res = get_active_window_name(buffer.as_mut_ptr(), buffer.len() as i32); + + if res > 0 { + let c_string = CStr::from_ptr(buffer.as_ptr()); + + let string = c_string.to_str(); + if let Ok(string) = string { + return Some((*string).to_owned()); + } + } + } + + None + } + + fn get_current_window_class(&self) -> Option { + unsafe { + let mut buffer : [c_char; 100] = [0; 100]; + let res = get_active_window_class(buffer.as_mut_ptr(), buffer.len() as i32); + + if res > 0 { + let c_string = CStr::from_ptr(buffer.as_ptr()); + + let string = c_string.to_str(); + if let Ok(string) = string { + return Some((*string).to_owned()); + } + } + } + + None + } + + fn get_current_window_executable(&self) -> Option { + unimplemented!() + } +} + +impl LinuxSystemManager { + +} \ No newline at end of file diff --git a/src/system/mod.rs b/src/system/mod.rs new file mode 100644 index 0000000..5aedc9f --- /dev/null +++ b/src/system/mod.rs @@ -0,0 +1,23 @@ +#[cfg(target_os = "windows")] +mod windows; + +#[cfg(target_os = "linux")] +mod linux; + +#[cfg(target_os = "macos")] +mod macos; + +pub trait SystemManager { + fn initialize(&self); + fn get_current_window_title(&self) -> Option; + fn get_current_window_class(&self) -> Option; + fn get_current_window_executable(&self) -> Option; +} + +// LINUX IMPLEMENTATION +#[cfg(target_os = "linux")] +pub fn get_manager() -> impl SystemManager { + let manager = linux::LinuxSystemManager{}; + manager.initialize(); + manager +} \ No newline at end of file