434 lines
12 KiB
Rust
434 lines
12 KiB
Rust
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
mod ffi;
|
|
|
|
use std::{
|
|
collections::HashMap,
|
|
ffi::{CStr, CString},
|
|
os::raw::c_char,
|
|
slice,
|
|
};
|
|
|
|
use ffi::{
|
|
Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay, XDefaultRootWindow,
|
|
XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XLookupString,
|
|
XQueryKeymap, XSendEvent, XSync, XTestFakeKeyEvent,
|
|
};
|
|
use log::error;
|
|
|
|
use crate::linux::raw_keys::convert_to_sym_array;
|
|
use anyhow::Result;
|
|
use thiserror::Error;
|
|
|
|
use crate::{keys, InjectionOptions, Injector};
|
|
|
|
// Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB
|
|
// keycode set (where ESC is 9).
|
|
const EVDEV_OFFSET: u32 = 8;
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct KeyRecord {
|
|
// Keycode
|
|
code: u32,
|
|
// Modifier state which combined with the code produces the char
|
|
// This is a bit mask:
|
|
state: u32,
|
|
}
|
|
|
|
type CharMap = HashMap<String, KeyRecord>;
|
|
type SymMap = HashMap<KeySym, KeyRecord>;
|
|
|
|
pub struct X11Injector {
|
|
display: *mut Display,
|
|
|
|
char_map: CharMap,
|
|
sym_map: SymMap,
|
|
}
|
|
|
|
#[allow(clippy::new_without_default)]
|
|
impl X11Injector {
|
|
pub fn new() -> Result<Self> {
|
|
// Necessary to properly handle non-ascii chars
|
|
let empty_string = CString::new("")?;
|
|
unsafe {
|
|
libc::setlocale(libc::LC_ALL, empty_string.as_ptr());
|
|
}
|
|
|
|
let display = unsafe { ffi::XOpenDisplay(std::ptr::null()) };
|
|
if display.is_null() {
|
|
return Err(X11InjectorError::InitFailure().into());
|
|
}
|
|
|
|
let (char_map, sym_map) = Self::generate_maps(display);
|
|
|
|
Ok(Self {
|
|
display,
|
|
char_map,
|
|
sym_map,
|
|
})
|
|
}
|
|
|
|
fn generate_maps(display: *mut Display) -> (CharMap, SymMap) {
|
|
let mut char_map = HashMap::new();
|
|
let mut sym_map = HashMap::new();
|
|
|
|
let root_window = unsafe { XDefaultRootWindow(display) };
|
|
|
|
// Cycle through all state/code combinations to populate the reverse lookup tables
|
|
for key_code in 0..256u32 {
|
|
for modifier_state in 0..256u32 {
|
|
let code_with_offset = key_code + EVDEV_OFFSET;
|
|
let event = XKeyEvent {
|
|
display,
|
|
keycode: code_with_offset,
|
|
state: modifier_state,
|
|
|
|
// These might not even need to be filled
|
|
window: root_window,
|
|
root: root_window,
|
|
same_screen: 1,
|
|
time: 0,
|
|
type_: KeyRelease,
|
|
x_root: 1,
|
|
y_root: 1,
|
|
x: 1,
|
|
y: 1,
|
|
subwindow: 0,
|
|
serial: 0,
|
|
send_event: 0,
|
|
};
|
|
|
|
let mut sym: KeySym = 0;
|
|
let mut buffer: [c_char; 10] = [0; 10];
|
|
let result = unsafe {
|
|
XLookupString(
|
|
&event,
|
|
buffer.as_mut_ptr(),
|
|
(buffer.len() - 1) as i32,
|
|
&mut sym,
|
|
std::ptr::null(),
|
|
)
|
|
};
|
|
|
|
let key_record = KeyRecord {
|
|
code: code_with_offset,
|
|
state: modifier_state,
|
|
};
|
|
|
|
// Keysym was found
|
|
if sym != 0 {
|
|
sym_map.entry(sym).or_insert(key_record);
|
|
}
|
|
|
|
// Char was found
|
|
if result > 0 {
|
|
let raw_string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
|
|
let string = raw_string.to_string_lossy().to_string();
|
|
char_map.entry(string).or_insert(key_record);
|
|
}
|
|
}
|
|
}
|
|
|
|
(char_map, sym_map)
|
|
}
|
|
|
|
fn convert_to_record_array(&self, syms: &[KeySym]) -> Result<Vec<KeyRecord>> {
|
|
syms
|
|
.iter()
|
|
.map(|sym| {
|
|
self
|
|
.sym_map
|
|
.get(&sym)
|
|
.cloned()
|
|
.ok_or_else(|| X11InjectorError::SymMappingFailure(*sym).into())
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
// This method was inspired by the wonderful xdotool by Jordan Sissel
|
|
// https://github.com/jordansissel/xdotool
|
|
fn get_modifier_codes(&self) -> Vec<Vec<KeyCode>> {
|
|
let modifiers_ptr = unsafe { XGetModifierMapping(self.display) };
|
|
let modifiers = unsafe { *modifiers_ptr };
|
|
|
|
let mut modifiers_codes = Vec::new();
|
|
|
|
for mod_index in 0..=7 {
|
|
let mut modifier_codes = Vec::new();
|
|
for mod_key in 0..modifiers.max_keypermod {
|
|
let modifier_map = unsafe {
|
|
slice::from_raw_parts(
|
|
modifiers.modifiermap,
|
|
(8 * modifiers.max_keypermod) as usize,
|
|
)
|
|
};
|
|
let keycode = modifier_map[(mod_index * modifiers.max_keypermod + mod_key) as usize];
|
|
if keycode != 0 {
|
|
modifier_codes.push(keycode);
|
|
}
|
|
}
|
|
modifiers_codes.push(modifier_codes);
|
|
}
|
|
|
|
unsafe { XFreeModifiermap(modifiers_ptr) };
|
|
|
|
modifiers_codes
|
|
}
|
|
|
|
fn render_key_combination(&self, original_records: &[KeyRecord]) -> Vec<KeyRecord> {
|
|
let modifiers_codes = self.get_modifier_codes();
|
|
let mut records = Vec::new();
|
|
|
|
let mut current_state = 0u32;
|
|
for record in original_records {
|
|
let mut current_record = *record;
|
|
|
|
// Render the state by applying the modifiers
|
|
for (mod_index, modifier) in modifiers_codes.iter().enumerate() {
|
|
if modifier.contains(&(record.code as u8)) {
|
|
current_state |= 1 << mod_index;
|
|
}
|
|
}
|
|
|
|
current_record.state = current_state;
|
|
records.push(current_record);
|
|
}
|
|
|
|
records
|
|
}
|
|
|
|
fn get_focused_window(&self) -> Window {
|
|
let mut focused_window: Window = 0;
|
|
let mut revert_to = 0;
|
|
unsafe {
|
|
XGetInputFocus(self.display, &mut focused_window, &mut revert_to);
|
|
}
|
|
focused_window
|
|
}
|
|
|
|
fn send_key(&self, window: Window, record: &KeyRecord, pressed: bool, delay_us: u32) {
|
|
let root_window = unsafe { XDefaultRootWindow(self.display) };
|
|
let mut event = XKeyEvent {
|
|
display: self.display,
|
|
keycode: record.code,
|
|
state: record.state,
|
|
window,
|
|
root: root_window,
|
|
same_screen: 1,
|
|
time: 0,
|
|
type_: if pressed { KeyPress } else { KeyRelease },
|
|
x_root: 1,
|
|
y_root: 1,
|
|
x: 1,
|
|
y: 1,
|
|
subwindow: 0,
|
|
serial: 0,
|
|
send_event: 0,
|
|
};
|
|
unsafe {
|
|
XSendEvent(self.display, window, 1, 0, &mut event);
|
|
XFlush(self.display);
|
|
}
|
|
|
|
if delay_us != 0 {
|
|
unsafe {
|
|
libc::usleep(delay_us);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn xtest_send_modifiers(&self, modmask: u32, pressed: bool) {
|
|
let modifiers_codes = self.get_modifier_codes();
|
|
for (mod_index, modifier_codes) in modifiers_codes.into_iter().enumerate() {
|
|
if (modmask & (1 << mod_index)) != 0 {
|
|
for keycode in modifier_codes {
|
|
let is_press = if pressed { 1 } else { 0 };
|
|
unsafe {
|
|
XTestFakeKeyEvent(self.display, keycode as u32, is_press, 0);
|
|
XSync(self.display, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn xtest_send_key(&self, record: &KeyRecord, pressed: bool, delay_us: u32) {
|
|
// If the key requires any modifier, we need to send those events
|
|
if record.state != 0 {
|
|
self.xtest_send_modifiers(record.state, pressed);
|
|
}
|
|
|
|
let is_press = if pressed { 1 } else { 0 };
|
|
unsafe {
|
|
XTestFakeKeyEvent(self.display, record.code, is_press, 0);
|
|
XSync(self.display, 0);
|
|
XFlush(self.display);
|
|
}
|
|
|
|
if delay_us != 0 {
|
|
unsafe {
|
|
libc::usleep(delay_us);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn xtest_release_all_keys(&self) {
|
|
let mut keys: [u8; 32] = [0; 32];
|
|
unsafe {
|
|
XQueryKeymap(self.display, 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.display, key_code as u32, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for X11Injector {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
XCloseDisplay(self.display);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Injector for X11Injector {
|
|
fn send_string(&self, string: &str, options: InjectionOptions) -> Result<()> {
|
|
let focused_window = self.get_focused_window();
|
|
|
|
if options.disable_fast_inject {
|
|
self.xtest_release_all_keys();
|
|
}
|
|
|
|
// Compute all the key record sequence first to make sure a mapping is available
|
|
let records: Result<Vec<KeyRecord>> = string
|
|
.chars()
|
|
.map(|c| c.to_string())
|
|
.map(|char| {
|
|
self
|
|
.char_map
|
|
.get(&char)
|
|
.cloned()
|
|
.ok_or_else(|| X11InjectorError::CharMappingFailure(char).into())
|
|
})
|
|
.collect();
|
|
|
|
let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
|
|
|
|
for record in records? {
|
|
if options.disable_fast_inject {
|
|
self.xtest_send_key(&record, true, delay_us);
|
|
self.xtest_send_key(&record, false, delay_us);
|
|
} else {
|
|
self.send_key(focused_window, &record, true, delay_us);
|
|
self.send_key(focused_window, &record, false, delay_us);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn send_keys(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
|
|
let focused_window = self.get_focused_window();
|
|
|
|
// Compute all the key record sequence first to make sure a mapping is available
|
|
let syms = convert_to_sym_array(keys)?;
|
|
let records = self.convert_to_record_array(&syms)?;
|
|
|
|
if options.disable_fast_inject {
|
|
self.xtest_release_all_keys();
|
|
}
|
|
|
|
let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
|
|
|
|
for record in records {
|
|
if options.disable_fast_inject {
|
|
self.xtest_send_key(&record, true, delay_us);
|
|
self.xtest_send_key(&record, false, delay_us);
|
|
} else {
|
|
self.send_key(focused_window, &record, true, delay_us);
|
|
self.send_key(focused_window, &record, false, delay_us);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn send_key_combination(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> {
|
|
let focused_window = self.get_focused_window();
|
|
|
|
// Compute all the key record sequence first to make sure a mapping is available
|
|
let syms = convert_to_sym_array(keys)?;
|
|
let records = self.convert_to_record_array(&syms)?;
|
|
|
|
// Render the correct modifier mask for the given sequence
|
|
let records = self.render_key_combination(&records);
|
|
|
|
if options.disable_fast_inject {
|
|
self.xtest_release_all_keys();
|
|
}
|
|
|
|
let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
|
|
|
|
// First press the keys
|
|
for record in records.iter() {
|
|
if options.disable_fast_inject {
|
|
self.xtest_send_key(&record, true, delay_us);
|
|
} else {
|
|
self.send_key(focused_window, &record, true, delay_us);
|
|
}
|
|
}
|
|
|
|
// Then release them
|
|
for record in records.iter().rev() {
|
|
if options.disable_fast_inject {
|
|
self.xtest_send_key(&record, false, delay_us);
|
|
} else {
|
|
self.send_key(focused_window, &record, false, delay_us);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum X11InjectorError {
|
|
#[error("failed to initialize x11 display")]
|
|
InitFailure(),
|
|
|
|
#[error("missing vkey mapping for char `{0}`")]
|
|
CharMappingFailure(String),
|
|
|
|
#[error("missing record mapping for sym `{0}`")]
|
|
SymMappingFailure(u64),
|
|
}
|