/* * This file is part of espanso. * * Copyright (C) 2019-2022 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::{ convert::TryInto, ffi::{CStr, CString}, }; use crate::Injector; use anyhow::{bail, Context, Result}; use log::debug; mod ffi; use self::ffi::{fast_send_keysequence_window, xdo_send_keysequence_window, xdo_t, CURRENTWINDOW}; use super::ffi::{ Display, Window, XGetInputFocus, XKeycodeToKeysym, XKeysymToString, XQueryKeymap, XTestFakeKeyEvent, }; pub struct X11XDOToolInjector { xdo: *const xdo_t, } impl X11XDOToolInjector { pub fn new() -> Result { let xdo = unsafe { ffi::xdo_new(std::ptr::null()) }; if xdo.is_null() { bail!("unable to initialize xdo_t instance"); } debug!("initialized xdo_t object"); Ok(Self { xdo }) } fn xfake_release_all_keys(&self) { let mut keys: [u8; 32] = [0; 32]; unsafe { XQueryKeymap((*self.xdo).xdpy, keys.as_mut_ptr()); } #[allow(clippy::needless_range_loop)] for i in 0..32 { // Only those that are pressed should be changed if keys[i] != 0 { for k in 0..8 { if (keys[i] & (1 << k)) != 0 { let key_code = i * 8 + k; unsafe { XTestFakeKeyEvent((*self.xdo).xdpy, key_code as u32, 0, 0); } } } } } } fn get_focused_window(&self) -> Window { let mut focused_window: Window = 0; let mut revert_to = 0; unsafe { XGetInputFocus((*self.xdo).xdpy, &mut focused_window, &mut revert_to); } focused_window } fn xfake_send_string( &self, string: &str, options: crate::InjectionOptions, ) -> anyhow::Result<()> { // It may happen that when an expansion is triggered, some keys are still pressed. // This causes a problem if the expanded match contains that character, as the injection // will not be able to register that keypress (as it is already pressed). // To solve the problem, before an expansion we get which keys are currently pressed // and inject a key_release event so that they can be further registered. self.xfake_release_all_keys(); let c_string = CString::new(string).context("unable to create CString")?; let delay = options.delay * 1000; unsafe { ffi::xdo_enter_text_window( self.xdo, CURRENTWINDOW, c_string.as_ptr(), delay.try_into().unwrap(), ); } Ok(()) } fn fast_release_all_keys(&self) { let mut keys: [u8; 32] = [0; 32]; unsafe { XQueryKeymap((*self.xdo).xdpy, keys.as_mut_ptr()); } let focused_window = self.get_focused_window(); #[allow(clippy::needless_range_loop)] for i in 0..32 { // Only those that are pressed should be changed if keys[i] != 0 { for k in 0..8 { if (keys[i] & (1 << k)) != 0 { let key_code = i * 8 + k; unsafe { ffi::fast_send_event(self.xdo, focused_window, key_code.try_into().unwrap(), 0); } } } } } } fn fast_send_string(&self, string: &str, options: crate::InjectionOptions) -> anyhow::Result<()> { // It may happen that when an expansion is triggered, some keys are still pressed. // This causes a problem if the expanded match contains that character, as the injection // will not be able to register that keypress (as it is already pressed). // To solve the problem, before an expansion we get which keys are currently pressed // and inject a key_release event so that they can be further registered. self.fast_release_all_keys(); let c_string = CString::new(string).context("unable to create CString")?; let delay = options.delay * 1000; unsafe { ffi::fast_enter_text_window( self.xdo, self.get_focused_window(), c_string.as_ptr(), delay.try_into().unwrap(), ); } Ok(()) } } impl Injector for X11XDOToolInjector { fn send_string(&self, string: &str, options: crate::InjectionOptions) -> anyhow::Result<()> { if options.disable_fast_inject { self.xfake_send_string(string, options) } else { self.fast_send_string(string, options) } } fn send_keys( &self, keys: &[crate::keys::Key], options: crate::InjectionOptions, ) -> anyhow::Result<()> { let key_syms: Vec = keys .iter() .filter_map(|key| unsafe { convert_key_to_keysym((*self.xdo).xdpy, key) }) .collect(); let delay = options.delay * 1000; for key in key_syms { let c_str = CString::new(key).context("unable to generate CString")?; if options.disable_fast_inject { unsafe { xdo_send_keysequence_window( self.xdo, CURRENTWINDOW, c_str.as_ptr(), delay.try_into().unwrap(), ); } } else { unsafe { fast_send_keysequence_window( self.xdo, self.get_focused_window(), c_str.as_ptr(), delay.try_into().unwrap(), ); } } } Ok(()) } fn send_key_combination( &self, keys: &[crate::keys::Key], options: crate::InjectionOptions, ) -> anyhow::Result<()> { let key_syms: Vec = keys .iter() .filter_map(|key| unsafe { convert_key_to_keysym((*self.xdo).xdpy, key) }) .collect(); let key_combination = key_syms.join("+"); let delay = options.delay * 1000; let c_key_combination = CString::new(key_combination).context("unable to generate CString")?; if options.disable_fast_inject { unsafe { xdo_send_keysequence_window( self.xdo, CURRENTWINDOW, c_key_combination.as_ptr(), delay.try_into().unwrap(), ); } } else { unsafe { fast_send_keysequence_window( self.xdo, self.get_focused_window(), c_key_combination.as_ptr(), delay.try_into().unwrap(), ); } } Ok(()) } } impl Drop for X11XDOToolInjector { fn drop(&mut self) { unsafe { ffi::xdo_free(self.xdo) } } } fn convert_key_to_keysym(display: *mut Display, key: &crate::keys::Key) -> Option { match key { crate::keys::Key::Alt => Some("Alt_L".to_string()), crate::keys::Key::CapsLock => Some("Caps_Lock".to_string()), crate::keys::Key::Control => Some("Control_L".to_string()), crate::keys::Key::Meta => Some("Meta_L".to_string()), crate::keys::Key::NumLock => Some("Num_Lock".to_string()), crate::keys::Key::Shift => Some("Shift_L".to_string()), crate::keys::Key::Enter => Some("Return".to_string()), crate::keys::Key::Tab => Some("Tab".to_string()), crate::keys::Key::Space => Some("space".to_string()), crate::keys::Key::ArrowDown => Some("downarrow".to_string()), crate::keys::Key::ArrowLeft => Some("leftarrow".to_string()), crate::keys::Key::ArrowRight => Some("rightarrow".to_string()), crate::keys::Key::ArrowUp => Some("uparrow".to_string()), crate::keys::Key::End => Some("End".to_string()), crate::keys::Key::Home => Some("Home".to_string()), crate::keys::Key::PageDown => Some("Page_Down".to_string()), crate::keys::Key::PageUp => Some("Page_Up".to_string()), crate::keys::Key::Escape => Some("Escape".to_string()), crate::keys::Key::Backspace => Some("BackSpace".to_string()), crate::keys::Key::Insert => Some("Insert".to_string()), crate::keys::Key::Delete => Some("Delete".to_string()), crate::keys::Key::F1 => Some("F1".to_string()), crate::keys::Key::F2 => Some("F2".to_string()), crate::keys::Key::F3 => Some("F3".to_string()), crate::keys::Key::F4 => Some("F4".to_string()), crate::keys::Key::F5 => Some("F5".to_string()), crate::keys::Key::F6 => Some("F6".to_string()), crate::keys::Key::F7 => Some("F7".to_string()), crate::keys::Key::F8 => Some("F8".to_string()), crate::keys::Key::F9 => Some("F9".to_string()), crate::keys::Key::F10 => Some("F10".to_string()), crate::keys::Key::F11 => Some("F11".to_string()), crate::keys::Key::F12 => Some("F12".to_string()), crate::keys::Key::F13 => Some("F13".to_string()), crate::keys::Key::F14 => Some("F14".to_string()), crate::keys::Key::F15 => Some("F15".to_string()), crate::keys::Key::F16 => Some("F16".to_string()), crate::keys::Key::F17 => Some("F17".to_string()), crate::keys::Key::F18 => Some("F18".to_string()), crate::keys::Key::F19 => Some("F19".to_string()), crate::keys::Key::F20 => Some("F20".to_string()), crate::keys::Key::A => Some("a".to_string()), crate::keys::Key::B => Some("b".to_string()), crate::keys::Key::C => Some("c".to_string()), crate::keys::Key::D => Some("d".to_string()), crate::keys::Key::E => Some("e".to_string()), crate::keys::Key::F => Some("f".to_string()), crate::keys::Key::G => Some("g".to_string()), crate::keys::Key::H => Some("h".to_string()), crate::keys::Key::I => Some("i".to_string()), crate::keys::Key::J => Some("j".to_string()), crate::keys::Key::K => Some("k".to_string()), crate::keys::Key::L => Some("l".to_string()), crate::keys::Key::M => Some("m".to_string()), crate::keys::Key::N => Some("n".to_string()), crate::keys::Key::O => Some("o".to_string()), crate::keys::Key::P => Some("p".to_string()), crate::keys::Key::Q => Some("q".to_string()), crate::keys::Key::R => Some("r".to_string()), crate::keys::Key::S => Some("s".to_string()), crate::keys::Key::T => Some("t".to_string()), crate::keys::Key::U => Some("u".to_string()), crate::keys::Key::V => Some("v".to_string()), crate::keys::Key::W => Some("w".to_string()), crate::keys::Key::X => Some("x".to_string()), crate::keys::Key::Y => Some("y".to_string()), crate::keys::Key::Z => Some("z".to_string()), crate::keys::Key::N0 => Some("0".to_string()), crate::keys::Key::N1 => Some("1".to_string()), crate::keys::Key::N2 => Some("2".to_string()), crate::keys::Key::N3 => Some("3".to_string()), crate::keys::Key::N4 => Some("4".to_string()), crate::keys::Key::N5 => Some("5".to_string()), crate::keys::Key::N6 => Some("6".to_string()), crate::keys::Key::N7 => Some("7".to_string()), crate::keys::Key::N8 => Some("8".to_string()), crate::keys::Key::N9 => Some("9".to_string()), crate::keys::Key::Numpad0 => Some("KP_0".to_string()), crate::keys::Key::Numpad1 => Some("KP_1".to_string()), crate::keys::Key::Numpad2 => Some("KP_2".to_string()), crate::keys::Key::Numpad3 => Some("KP_3".to_string()), crate::keys::Key::Numpad4 => Some("KP_4".to_string()), crate::keys::Key::Numpad5 => Some("KP_5".to_string()), crate::keys::Key::Numpad6 => Some("KP_6".to_string()), crate::keys::Key::Numpad7 => Some("KP_7".to_string()), crate::keys::Key::Numpad8 => Some("KP_8".to_string()), crate::keys::Key::Numpad9 => Some("KP_9".to_string()), crate::keys::Key::Raw(key_code) => unsafe { let key_sym = XKeycodeToKeysym(display, (*key_code).try_into().unwrap(), 0); let string = XKeysymToString(key_sym); let c_str = CStr::from_ptr(string); Some(c_str.to_string_lossy().to_string()) }, } }