fix(inject): improve X11 injector to handle dead keys. Fix #881
This commit is contained in:
parent
57b2f194e5
commit
73214cb59a
|
@ -47,17 +47,31 @@ pub struct XModifierKeymap {
|
|||
pub modifiermap: *mut KeyCode,
|
||||
}
|
||||
|
||||
// XCreateIC values
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const XIMPreeditNothing: c_int = 0x0008;
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const XIMStatusNothing: c_int = 0x0400;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const XNClientWindow_0: &[u8] = b"clientWindow\0";
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const XNInputStyle_0: &[u8] = b"inputStyle\0";
|
||||
|
||||
pub enum _XIC {}
|
||||
pub enum _XIM {}
|
||||
pub enum _XrmHashBucketRec {}
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub type XIC = *mut _XIC;
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub type XIM = *mut _XIM;
|
||||
pub type XrmDatabase = *mut _XrmHashBucketRec;
|
||||
|
||||
#[link(name = "X11")]
|
||||
extern "C" {
|
||||
pub fn XOpenDisplay(name: *const c_char) -> *mut Display;
|
||||
pub fn XCloseDisplay(display: *mut Display);
|
||||
pub fn XLookupString(
|
||||
event: *const XKeyEvent,
|
||||
buffer_return: *mut c_char,
|
||||
bytes_buffer: c_int,
|
||||
keysym_return: *mut KeySym,
|
||||
status_in_out: *const c_void,
|
||||
) -> c_int;
|
||||
pub fn XDefaultRootWindow(display: *mut Display) -> Window;
|
||||
pub fn XGetInputFocus(
|
||||
display: *mut Display,
|
||||
|
@ -82,4 +96,31 @@ extern "C" {
|
|||
) -> c_int;
|
||||
pub fn XSync(display: *mut Display, discard: c_int) -> c_int;
|
||||
pub fn XQueryKeymap(display: *mut Display, keys_return: *mut u8);
|
||||
pub fn XOpenIM(
|
||||
display: *mut Display,
|
||||
db: XrmDatabase,
|
||||
res_name: *mut c_char,
|
||||
res_class: *mut c_char,
|
||||
) -> XIM;
|
||||
pub fn XCreateIC(
|
||||
input_method: XIM,
|
||||
p2: *const u8,
|
||||
p3: c_int,
|
||||
p4: *const u8,
|
||||
p5: c_int,
|
||||
p6: *const c_void,
|
||||
) -> XIC;
|
||||
pub fn XDestroyIC(input_context: XIC);
|
||||
pub fn XmbResetIC(input_context: XIC) -> *mut c_char;
|
||||
pub fn Xutf8LookupString(
|
||||
input_context: XIC,
|
||||
event: *mut XKeyEvent,
|
||||
buffer: *mut c_char,
|
||||
buff_size: c_int,
|
||||
keysym_return: *mut c_ulong,
|
||||
status_return: *mut c_int,
|
||||
) -> c_int;
|
||||
pub fn XFilterEvent(event: *mut XKeyEvent, window: c_ulong) -> c_int;
|
||||
pub fn XCloseIM(input_method: XIM) -> c_int;
|
||||
pub fn XFree(data: *mut c_void) -> c_int;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
mod ffi;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::{CStr, CString},
|
||||
os::raw::c_char,
|
||||
slice,
|
||||
|
@ -28,23 +28,29 @@ use std::{
|
|||
|
||||
use ffi::{
|
||||
Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay, XDefaultRootWindow,
|
||||
XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XLookupString,
|
||||
XQueryKeymap, XSendEvent, XSync, XTestFakeKeyEvent,
|
||||
XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XQueryKeymap,
|
||||
XSendEvent, XSync, XTestFakeKeyEvent,
|
||||
};
|
||||
use log::error;
|
||||
use libc::c_void;
|
||||
use log::{debug, error};
|
||||
|
||||
use crate::linux::raw_keys::convert_to_sym_array;
|
||||
use anyhow::Result;
|
||||
use crate::{linux::raw_keys::convert_to_sym_array, x11::ffi::Xutf8LookupString};
|
||||
use anyhow::{bail, Result};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{keys, InjectionOptions, Injector};
|
||||
|
||||
use self::ffi::{
|
||||
XCloseIM, XCreateIC, XDestroyIC, XFilterEvent, XFree, XIMPreeditNothing, XIMStatusNothing,
|
||||
XNClientWindow_0, XNInputStyle_0, XOpenIM, XmbResetIC, XIC,
|
||||
};
|
||||
|
||||
// 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 {
|
||||
struct KeyPair {
|
||||
// Keycode
|
||||
code: u32,
|
||||
// Modifier state which combined with the code produces the char
|
||||
|
@ -52,6 +58,15 @@ struct KeyRecord {
|
|||
state: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct KeyRecord {
|
||||
main: KeyPair,
|
||||
|
||||
// Under some keyboard layouts (de, es), a deadkey
|
||||
// press might be needed to generate the right char
|
||||
preceding_dead_key: Option<KeyPair>,
|
||||
}
|
||||
|
||||
type CharMap = HashMap<String, KeyRecord>;
|
||||
type SymMap = HashMap<KeySym, KeyRecord>;
|
||||
|
||||
|
@ -76,7 +91,7 @@ impl X11Injector {
|
|||
return Err(X11InjectorError::Init().into());
|
||||
}
|
||||
|
||||
let (char_map, sym_map) = Self::generate_maps(display);
|
||||
let (char_map, sym_map) = Self::generate_maps(display)?;
|
||||
|
||||
Ok(Self {
|
||||
display,
|
||||
|
@ -85,27 +100,64 @@ impl X11Injector {
|
|||
})
|
||||
}
|
||||
|
||||
fn generate_maps(display: *mut Display) -> (CharMap, SymMap) {
|
||||
fn generate_maps(display: *mut Display) -> Result<(CharMap, SymMap)> {
|
||||
debug!("generating key maps");
|
||||
|
||||
let mut char_map = HashMap::new();
|
||||
let mut sym_map = HashMap::new();
|
||||
|
||||
let root_window = unsafe { XDefaultRootWindow(display) };
|
||||
let input_method = unsafe {
|
||||
XOpenIM(
|
||||
display,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if input_method.is_null() {
|
||||
bail!("could not open input method");
|
||||
}
|
||||
let _im_guard = scopeguard::guard((), |_| {
|
||||
unsafe { XCloseIM(input_method) };
|
||||
});
|
||||
|
||||
let input_context = unsafe {
|
||||
XCreateIC(
|
||||
input_method,
|
||||
XNInputStyle_0.as_ptr(),
|
||||
XIMPreeditNothing | XIMStatusNothing,
|
||||
XNClientWindow_0.as_ptr(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if input_context.is_null() {
|
||||
bail!("could not open input context");
|
||||
}
|
||||
let _ic_guard = scopeguard::guard((), |_| {
|
||||
unsafe { XDestroyIC(input_context) };
|
||||
});
|
||||
|
||||
let deadkeys = Self::find_deadkeys(display, &input_context)?;
|
||||
|
||||
// Cycle through all state/code combinations to populate the reverse lookup tables
|
||||
for key_code in 0..256u32 {
|
||||
for modifier_state in 0..256u32 {
|
||||
for dead_key in deadkeys.iter() {
|
||||
let code_with_offset = key_code + EVDEV_OFFSET;
|
||||
let event = XKeyEvent {
|
||||
|
||||
let preceding_dead_key = if let Some(dead_key) = dead_key {
|
||||
let mut dead_key_event = XKeyEvent {
|
||||
display,
|
||||
keycode: code_with_offset,
|
||||
state: modifier_state,
|
||||
keycode: dead_key.code,
|
||||
state: dead_key.state,
|
||||
|
||||
// These might not even need to be filled
|
||||
window: root_window,
|
||||
root: root_window,
|
||||
window: 0,
|
||||
root: 0,
|
||||
same_screen: 1,
|
||||
time: 0,
|
||||
type_: KeyRelease,
|
||||
type_: KeyPress,
|
||||
x_root: 1,
|
||||
y_root: 1,
|
||||
x: 1,
|
||||
|
@ -115,38 +167,143 @@ impl X11Injector {
|
|||
send_event: 0,
|
||||
};
|
||||
|
||||
unsafe { XFilterEvent(&mut dead_key_event, 0) };
|
||||
|
||||
Some(*dead_key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut key_event = XKeyEvent {
|
||||
display,
|
||||
keycode: code_with_offset,
|
||||
state: modifier_state,
|
||||
|
||||
// These might not even need to be filled
|
||||
window: 0,
|
||||
root: 0,
|
||||
same_screen: 1,
|
||||
time: 0,
|
||||
type_: KeyPress,
|
||||
x_root: 1,
|
||||
y_root: 1,
|
||||
x: 1,
|
||||
y: 1,
|
||||
subwindow: 0,
|
||||
serial: 0,
|
||||
send_event: 0,
|
||||
};
|
||||
|
||||
unsafe { XFilterEvent(&mut key_event, 0) };
|
||||
let mut sym: KeySym = 0;
|
||||
let mut buffer: [c_char; 10] = [0; 10];
|
||||
|
||||
let result = unsafe {
|
||||
XLookupString(
|
||||
&event,
|
||||
Xutf8LookupString(
|
||||
input_context,
|
||||
&mut key_event,
|
||||
buffer.as_mut_ptr(),
|
||||
(buffer.len() - 1) as i32,
|
||||
&mut sym,
|
||||
std::ptr::null(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
let key_record = KeyRecord {
|
||||
main: KeyPair {
|
||||
code: code_with_offset,
|
||||
state: modifier_state,
|
||||
},
|
||||
preceding_dead_key,
|
||||
};
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
// We need to reset the context state to prevent
|
||||
// deadkeys effect to propagate to the next combination
|
||||
let _reset = unsafe { XmbResetIC(input_context) };
|
||||
unsafe { XFree(_reset as *mut c_void) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(char_map, sym_map)
|
||||
debug!("Populated char_map with {} symbols", char_map.len());
|
||||
debug!("Populated sym_map with {} symbols", sym_map.len());
|
||||
debug!("Detected {} dead key combinations", deadkeys.len());
|
||||
|
||||
Ok((char_map, sym_map))
|
||||
}
|
||||
|
||||
fn find_deadkeys(display: *mut Display, input_context: &XIC) -> Result<Vec<Option<KeyPair>>> {
|
||||
let mut deadkeys = vec![None];
|
||||
let mut seen_keysyms: HashSet<KeySym> = HashSet::new();
|
||||
|
||||
// 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 mut event = XKeyEvent {
|
||||
display,
|
||||
keycode: code_with_offset,
|
||||
state: modifier_state,
|
||||
|
||||
// These might not even need to be filled
|
||||
window: 0,
|
||||
root: 0,
|
||||
same_screen: 1,
|
||||
time: 0,
|
||||
type_: KeyPress,
|
||||
x_root: 1,
|
||||
y_root: 1,
|
||||
x: 1,
|
||||
y: 1,
|
||||
subwindow: 0,
|
||||
serial: 0,
|
||||
send_event: 0,
|
||||
};
|
||||
|
||||
let filter = unsafe { XFilterEvent(&mut event, 0) };
|
||||
if filter == 1 {
|
||||
let mut sym: KeySym = 0;
|
||||
let mut buffer: [c_char; 10] = [0; 10];
|
||||
|
||||
unsafe {
|
||||
Xutf8LookupString(
|
||||
*input_context,
|
||||
&mut event,
|
||||
buffer.as_mut_ptr(),
|
||||
(buffer.len() - 1) as i32,
|
||||
&mut sym,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
if sym != 0 && !seen_keysyms.contains(&sym) {
|
||||
let key_record = KeyPair {
|
||||
code: code_with_offset,
|
||||
state: modifier_state,
|
||||
};
|
||||
deadkeys.push(Some(key_record));
|
||||
seen_keysyms.insert(sym);
|
||||
}
|
||||
}
|
||||
|
||||
let _reset = unsafe { XmbResetIC(*input_context) };
|
||||
unsafe { XFree(_reset as *mut c_void) };
|
||||
}
|
||||
}
|
||||
|
||||
Ok(deadkeys)
|
||||
}
|
||||
|
||||
fn convert_to_record_array(&self, syms: &[KeySym]) -> Result<Vec<KeyRecord>> {
|
||||
|
@ -202,12 +359,12 @@ impl X11Injector {
|
|||
|
||||
// Render the state by applying the modifiers
|
||||
for (mod_index, modifier) in modifiers_codes.iter().enumerate() {
|
||||
if modifier.contains(&(record.code as u8)) {
|
||||
if modifier.contains(&(record.main.code as u8)) {
|
||||
current_state |= 1 << mod_index;
|
||||
}
|
||||
}
|
||||
|
||||
current_record.state = current_state;
|
||||
current_record.main.state = current_state;
|
||||
records.push(current_record);
|
||||
}
|
||||
|
||||
|
@ -223,7 +380,7 @@ impl X11Injector {
|
|||
focused_window
|
||||
}
|
||||
|
||||
fn send_key(&self, window: Window, record: &KeyRecord, pressed: bool, delay_us: u32) {
|
||||
fn send_key(&self, window: Window, record: &KeyPair, pressed: bool, delay_us: u32) {
|
||||
let root_window = unsafe { XDefaultRootWindow(self.display) };
|
||||
let mut event = XKeyEvent {
|
||||
display: self.display,
|
||||
|
@ -269,7 +426,7 @@ impl X11Injector {
|
|||
}
|
||||
}
|
||||
|
||||
fn xtest_send_key(&self, record: &KeyRecord, pressed: bool, delay_us: u32) {
|
||||
fn xtest_send_key(&self, record: &KeyPair, 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);
|
||||
|
@ -345,11 +502,21 @@ impl Injector for X11Injector {
|
|||
|
||||
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);
|
||||
if let Some(deadkey) = &record.preceding_dead_key {
|
||||
self.xtest_send_key(deadkey, true, delay_us);
|
||||
self.xtest_send_key(deadkey, false, delay_us);
|
||||
}
|
||||
|
||||
self.xtest_send_key(&record.main, true, delay_us);
|
||||
self.xtest_send_key(&record.main, false, delay_us);
|
||||
} else {
|
||||
self.send_key(focused_window, &record, true, delay_us);
|
||||
self.send_key(focused_window, &record, false, delay_us);
|
||||
if let Some(deadkey) = &record.preceding_dead_key {
|
||||
self.send_key(focused_window, deadkey, true, delay_us);
|
||||
self.send_key(focused_window, deadkey, false, delay_us);
|
||||
}
|
||||
|
||||
self.send_key(focused_window, &record.main, true, delay_us);
|
||||
self.send_key(focused_window, &record.main, false, delay_us);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -371,11 +538,11 @@ impl Injector for X11Injector {
|
|||
|
||||
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);
|
||||
self.xtest_send_key(&record.main, true, delay_us);
|
||||
self.xtest_send_key(&record.main, false, delay_us);
|
||||
} else {
|
||||
self.send_key(focused_window, &record, true, delay_us);
|
||||
self.send_key(focused_window, &record, false, delay_us);
|
||||
self.send_key(focused_window, &record.main, true, delay_us);
|
||||
self.send_key(focused_window, &record.main, false, delay_us);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -401,18 +568,18 @@ impl Injector for X11Injector {
|
|||
// First press the keys
|
||||
for record in records.iter() {
|
||||
if options.disable_fast_inject {
|
||||
self.xtest_send_key(record, true, delay_us);
|
||||
self.xtest_send_key(&record.main, true, delay_us);
|
||||
} else {
|
||||
self.send_key(focused_window, record, true, delay_us);
|
||||
self.send_key(focused_window, &record.main, 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);
|
||||
self.xtest_send_key(&record.main, false, delay_us);
|
||||
} else {
|
||||
self.send_key(focused_window, record, false, delay_us);
|
||||
self.send_key(focused_window, &record.main, false, delay_us);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user