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,
|
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")]
|
#[link(name = "X11")]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
pub fn XOpenDisplay(name: *const c_char) -> *mut Display;
|
pub fn XOpenDisplay(name: *const c_char) -> *mut Display;
|
||||||
pub fn XCloseDisplay(display: *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 XDefaultRootWindow(display: *mut Display) -> Window;
|
||||||
pub fn XGetInputFocus(
|
pub fn XGetInputFocus(
|
||||||
display: *mut Display,
|
display: *mut Display,
|
||||||
|
@ -82,4 +96,31 @@ extern "C" {
|
||||||
) -> c_int;
|
) -> c_int;
|
||||||
pub fn XSync(display: *mut Display, discard: c_int) -> 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 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;
|
mod ffi;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, HashSet},
|
||||||
ffi::{CStr, CString},
|
ffi::{CStr, CString},
|
||||||
os::raw::c_char,
|
os::raw::c_char,
|
||||||
slice,
|
slice,
|
||||||
|
@ -28,23 +28,29 @@ use std::{
|
||||||
|
|
||||||
use ffi::{
|
use ffi::{
|
||||||
Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay, XDefaultRootWindow,
|
Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay, XDefaultRootWindow,
|
||||||
XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XLookupString,
|
XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XQueryKeymap,
|
||||||
XQueryKeymap, XSendEvent, XSync, XTestFakeKeyEvent,
|
XSendEvent, XSync, XTestFakeKeyEvent,
|
||||||
};
|
};
|
||||||
use log::error;
|
use libc::c_void;
|
||||||
|
use log::{debug, error};
|
||||||
|
|
||||||
use crate::linux::raw_keys::convert_to_sym_array;
|
use crate::{linux::raw_keys::convert_to_sym_array, x11::ffi::Xutf8LookupString};
|
||||||
use anyhow::Result;
|
use anyhow::{bail, Result};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{keys, InjectionOptions, Injector};
|
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
|
// Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB
|
||||||
// keycode set (where ESC is 9).
|
// keycode set (where ESC is 9).
|
||||||
const EVDEV_OFFSET: u32 = 8;
|
const EVDEV_OFFSET: u32 = 8;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
struct KeyRecord {
|
struct KeyPair {
|
||||||
// Keycode
|
// Keycode
|
||||||
code: u32,
|
code: u32,
|
||||||
// Modifier state which combined with the code produces the char
|
// Modifier state which combined with the code produces the char
|
||||||
|
@ -52,6 +58,15 @@ struct KeyRecord {
|
||||||
state: u32,
|
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 CharMap = HashMap<String, KeyRecord>;
|
||||||
type SymMap = HashMap<KeySym, KeyRecord>;
|
type SymMap = HashMap<KeySym, KeyRecord>;
|
||||||
|
|
||||||
|
@ -76,7 +91,7 @@ impl X11Injector {
|
||||||
return Err(X11InjectorError::Init().into());
|
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 {
|
Ok(Self {
|
||||||
display,
|
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 char_map = HashMap::new();
|
||||||
let mut sym_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
|
// Cycle through all state/code combinations to populate the reverse lookup tables
|
||||||
for key_code in 0..256u32 {
|
for key_code in 0..256u32 {
|
||||||
for modifier_state in 0..256u32 {
|
for modifier_state in 0..256u32 {
|
||||||
|
for dead_key in deadkeys.iter() {
|
||||||
let code_with_offset = key_code + EVDEV_OFFSET;
|
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,
|
display,
|
||||||
keycode: code_with_offset,
|
keycode: dead_key.code,
|
||||||
state: modifier_state,
|
state: dead_key.state,
|
||||||
|
|
||||||
// These might not even need to be filled
|
// These might not even need to be filled
|
||||||
window: root_window,
|
window: 0,
|
||||||
root: root_window,
|
root: 0,
|
||||||
same_screen: 1,
|
same_screen: 1,
|
||||||
time: 0,
|
time: 0,
|
||||||
type_: KeyRelease,
|
type_: KeyPress,
|
||||||
x_root: 1,
|
x_root: 1,
|
||||||
y_root: 1,
|
y_root: 1,
|
||||||
x: 1,
|
x: 1,
|
||||||
|
@ -115,38 +167,143 @@ impl X11Injector {
|
||||||
send_event: 0,
|
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 sym: KeySym = 0;
|
||||||
let mut buffer: [c_char; 10] = [0; 10];
|
let mut buffer: [c_char; 10] = [0; 10];
|
||||||
|
|
||||||
let result = unsafe {
|
let result = unsafe {
|
||||||
XLookupString(
|
Xutf8LookupString(
|
||||||
&event,
|
input_context,
|
||||||
|
&mut key_event,
|
||||||
buffer.as_mut_ptr(),
|
buffer.as_mut_ptr(),
|
||||||
(buffer.len() - 1) as i32,
|
(buffer.len() - 1) as i32,
|
||||||
&mut sym,
|
&mut sym,
|
||||||
std::ptr::null(),
|
std::ptr::null_mut(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let key_record = KeyRecord {
|
let key_record = KeyRecord {
|
||||||
|
main: KeyPair {
|
||||||
code: code_with_offset,
|
code: code_with_offset,
|
||||||
state: modifier_state,
|
state: modifier_state,
|
||||||
|
},
|
||||||
|
preceding_dead_key,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keysym was found
|
// Keysym was found
|
||||||
if sym != 0 {
|
if sym != 0 {
|
||||||
sym_map.entry(sym).or_insert(key_record);
|
sym_map.entry(sym).or_insert(key_record);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Char was found
|
// Char was found
|
||||||
if result > 0 {
|
if result > 0 {
|
||||||
let raw_string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
|
let raw_string = unsafe { CStr::from_ptr(buffer.as_ptr()) };
|
||||||
let string = raw_string.to_string_lossy().to_string();
|
let string = raw_string.to_string_lossy().to_string();
|
||||||
char_map.entry(string).or_insert(key_record);
|
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>> {
|
fn convert_to_record_array(&self, syms: &[KeySym]) -> Result<Vec<KeyRecord>> {
|
||||||
|
@ -202,12 +359,12 @@ impl X11Injector {
|
||||||
|
|
||||||
// Render the state by applying the modifiers
|
// Render the state by applying the modifiers
|
||||||
for (mod_index, modifier) in modifiers_codes.iter().enumerate() {
|
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_state |= 1 << mod_index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
current_record.state = current_state;
|
current_record.main.state = current_state;
|
||||||
records.push(current_record);
|
records.push(current_record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,7 +380,7 @@ impl X11Injector {
|
||||||
focused_window
|
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 root_window = unsafe { XDefaultRootWindow(self.display) };
|
||||||
let mut event = XKeyEvent {
|
let mut event = XKeyEvent {
|
||||||
display: self.display,
|
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 the key requires any modifier, we need to send those events
|
||||||
if record.state != 0 {
|
if record.state != 0 {
|
||||||
self.xtest_send_modifiers(record.state, pressed);
|
self.xtest_send_modifiers(record.state, pressed);
|
||||||
|
@ -345,11 +502,21 @@ impl Injector for X11Injector {
|
||||||
|
|
||||||
for record in records? {
|
for record in records? {
|
||||||
if options.disable_fast_inject {
|
if options.disable_fast_inject {
|
||||||
self.xtest_send_key(&record, true, delay_us);
|
if let Some(deadkey) = &record.preceding_dead_key {
|
||||||
self.xtest_send_key(&record, false, delay_us);
|
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 {
|
} else {
|
||||||
self.send_key(focused_window, &record, true, delay_us);
|
if let Some(deadkey) = &record.preceding_dead_key {
|
||||||
self.send_key(focused_window, &record, false, delay_us);
|
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 {
|
for record in records {
|
||||||
if options.disable_fast_inject {
|
if options.disable_fast_inject {
|
||||||
self.xtest_send_key(&record, true, delay_us);
|
self.xtest_send_key(&record.main, true, delay_us);
|
||||||
self.xtest_send_key(&record, false, delay_us);
|
self.xtest_send_key(&record.main, false, delay_us);
|
||||||
} else {
|
} else {
|
||||||
self.send_key(focused_window, &record, true, delay_us);
|
self.send_key(focused_window, &record.main, true, delay_us);
|
||||||
self.send_key(focused_window, &record, false, delay_us);
|
self.send_key(focused_window, &record.main, false, delay_us);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -401,18 +568,18 @@ impl Injector for X11Injector {
|
||||||
// First press the keys
|
// First press the keys
|
||||||
for record in records.iter() {
|
for record in records.iter() {
|
||||||
if options.disable_fast_inject {
|
if options.disable_fast_inject {
|
||||||
self.xtest_send_key(record, true, delay_us);
|
self.xtest_send_key(&record.main, true, delay_us);
|
||||||
} else {
|
} else {
|
||||||
self.send_key(focused_window, record, true, delay_us);
|
self.send_key(focused_window, &record.main, true, delay_us);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then release them
|
// Then release them
|
||||||
for record in records.iter().rev() {
|
for record in records.iter().rev() {
|
||||||
if options.disable_fast_inject {
|
if options.disable_fast_inject {
|
||||||
self.xtest_send_key(record, false, delay_us);
|
self.xtest_send_key(&record.main, false, delay_us);
|
||||||
} else {
|
} 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