/*
* 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())
},
}
}