feat: add alternative X11 injection backend based on libxdo (#1068)
* feat(inject): first steps towards xdotool inject fallback * feat(inject): progress in the xdotool fallback implementation * feat(config): add options for alternative xdotool backend * feat(core): wire up alternative x11 backend
This commit is contained in:
parent
088080dd63
commit
f30395b8a6
|
@ -166,6 +166,10 @@ pub trait Config: Send + Sync {
|
||||||
// the built-in native module on X11.
|
// the built-in native module on X11.
|
||||||
fn x11_use_xclip_backend(&self) -> bool;
|
fn x11_use_xclip_backend(&self) -> bool;
|
||||||
|
|
||||||
|
// If true, use an alternative injection backend based on the `xdotool` library.
|
||||||
|
// This might improve the situation for certain locales/layouts on X11.
|
||||||
|
fn x11_use_xdotool_backend(&self) -> bool;
|
||||||
|
|
||||||
// If true, filter out keyboard events without an explicit HID device source on Windows.
|
// If true, filter out keyboard events without an explicit HID device source on Windows.
|
||||||
// This is needed to filter out the software-generated events, including
|
// This is needed to filter out the software-generated events, including
|
||||||
// those from espanso, but might need to be disabled when using some software-level keyboards.
|
// those from espanso, but might need to be disabled when using some software-level keyboards.
|
||||||
|
@ -212,6 +216,7 @@ pub trait Config: Send + Sync {
|
||||||
secure_input_notification: {:?}
|
secure_input_notification: {:?}
|
||||||
|
|
||||||
x11_use_xclip_backend: {:?}
|
x11_use_xclip_backend: {:?}
|
||||||
|
x11_use_xdotool_backend: {:?}
|
||||||
win32_exclude_orphan_events: {:?}
|
win32_exclude_orphan_events: {:?}
|
||||||
win32_keyboard_layout_cache_interval: {:?}
|
win32_keyboard_layout_cache_interval: {:?}
|
||||||
|
|
||||||
|
@ -246,6 +251,7 @@ pub trait Config: Send + Sync {
|
||||||
self.secure_input_notification(),
|
self.secure_input_notification(),
|
||||||
|
|
||||||
self.x11_use_xclip_backend(),
|
self.x11_use_xclip_backend(),
|
||||||
|
self.x11_use_xdotool_backend(),
|
||||||
self.win32_exclude_orphan_events(),
|
self.win32_exclude_orphan_events(),
|
||||||
self.win32_keyboard_layout_cache_interval(),
|
self.win32_keyboard_layout_cache_interval(),
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ pub(crate) struct ParsedConfig {
|
||||||
pub win32_exclude_orphan_events: Option<bool>,
|
pub win32_exclude_orphan_events: Option<bool>,
|
||||||
pub win32_keyboard_layout_cache_interval: Option<i64>,
|
pub win32_keyboard_layout_cache_interval: Option<i64>,
|
||||||
pub x11_use_xclip_backend: Option<bool>,
|
pub x11_use_xclip_backend: Option<bool>,
|
||||||
|
pub x11_use_xdotool_backend: Option<bool>,
|
||||||
|
|
||||||
pub pre_paste_delay: Option<usize>,
|
pub pre_paste_delay: Option<usize>,
|
||||||
pub restore_clipboard_delay: Option<usize>,
|
pub restore_clipboard_delay: Option<usize>,
|
||||||
|
|
|
@ -121,6 +121,9 @@ pub(crate) struct YAMLConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub x11_use_xclip_backend: Option<bool>,
|
pub x11_use_xclip_backend: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub x11_use_xdotool_backend: Option<bool>,
|
||||||
|
|
||||||
// Include/Exclude
|
// Include/Exclude
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub includes: Option<Vec<String>>,
|
pub includes: Option<Vec<String>>,
|
||||||
|
@ -213,6 +216,7 @@ impl TryFrom<YAMLConfig> for ParsedConfig {
|
||||||
win32_exclude_orphan_events: yaml_config.win32_exclude_orphan_events,
|
win32_exclude_orphan_events: yaml_config.win32_exclude_orphan_events,
|
||||||
win32_keyboard_layout_cache_interval: yaml_config.win32_keyboard_layout_cache_interval,
|
win32_keyboard_layout_cache_interval: yaml_config.win32_keyboard_layout_cache_interval,
|
||||||
x11_use_xclip_backend: yaml_config.x11_use_xclip_backend,
|
x11_use_xclip_backend: yaml_config.x11_use_xclip_backend,
|
||||||
|
x11_use_xdotool_backend: yaml_config.x11_use_xdotool_backend,
|
||||||
|
|
||||||
use_standard_includes: yaml_config.use_standard_includes,
|
use_standard_includes: yaml_config.use_standard_includes,
|
||||||
includes: yaml_config.includes,
|
includes: yaml_config.includes,
|
||||||
|
@ -273,6 +277,7 @@ mod tests {
|
||||||
win32_exclude_orphan_events: false
|
win32_exclude_orphan_events: false
|
||||||
win32_keyboard_layout_cache_interval: 300
|
win32_keyboard_layout_cache_interval: 300
|
||||||
x11_use_xclip_backend: true
|
x11_use_xclip_backend: true
|
||||||
|
x11_use_xdotool_backend: true
|
||||||
|
|
||||||
use_standard_includes: true
|
use_standard_includes: true
|
||||||
includes: ["test1"]
|
includes: ["test1"]
|
||||||
|
@ -329,6 +334,7 @@ mod tests {
|
||||||
win32_exclude_orphan_events: Some(false),
|
win32_exclude_orphan_events: Some(false),
|
||||||
win32_keyboard_layout_cache_interval: Some(300),
|
win32_keyboard_layout_cache_interval: Some(300),
|
||||||
x11_use_xclip_backend: Some(true),
|
x11_use_xclip_backend: Some(true),
|
||||||
|
x11_use_xdotool_backend: Some(true),
|
||||||
|
|
||||||
pre_paste_delay: Some(300),
|
pre_paste_delay: Some(300),
|
||||||
evdev_modifier_delay: Some(40),
|
evdev_modifier_delay: Some(40),
|
||||||
|
|
|
@ -331,6 +331,10 @@ impl Config for ResolvedConfig {
|
||||||
fn x11_use_xclip_backend(&self) -> bool {
|
fn x11_use_xclip_backend(&self) -> bool {
|
||||||
self.parsed.x11_use_xclip_backend.unwrap_or(false)
|
self.parsed.x11_use_xclip_backend.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn x11_use_xdotool_backend(&self) -> bool {
|
||||||
|
self.parsed.x11_use_xdotool_backend.unwrap_or(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResolvedConfig {
|
impl ResolvedConfig {
|
||||||
|
@ -417,6 +421,7 @@ impl ResolvedConfig {
|
||||||
win32_exclude_orphan_events,
|
win32_exclude_orphan_events,
|
||||||
win32_keyboard_layout_cache_interval,
|
win32_keyboard_layout_cache_interval,
|
||||||
x11_use_xclip_backend,
|
x11_use_xclip_backend,
|
||||||
|
x11_use_xdotool_backend,
|
||||||
includes,
|
includes,
|
||||||
excludes,
|
excludes,
|
||||||
extra_includes,
|
extra_includes,
|
||||||
|
|
|
@ -410,6 +410,10 @@ impl Config for LegacyInteropConfig {
|
||||||
fn x11_use_xclip_backend(&self) -> bool {
|
fn x11_use_xclip_backend(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn x11_use_xdotool_backend(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LegacyMatchGroup {
|
struct LegacyMatchGroup {
|
||||||
|
|
|
@ -37,6 +37,9 @@ fn cc_config() {
|
||||||
fn cc_config() {
|
fn cc_config() {
|
||||||
println!("cargo:rerun-if-changed=src/evdev/native.h");
|
println!("cargo:rerun-if-changed=src/evdev/native.h");
|
||||||
println!("cargo:rerun-if-changed=src/evdev/native.c");
|
println!("cargo:rerun-if-changed=src/evdev/native.c");
|
||||||
|
println!("cargo:rerun-if-changed=src/x11/xdotool/vendor/xdo.c");
|
||||||
|
println!("cargo:rerun-if-changed=src/x11/xdotool/vendor/xdo.h");
|
||||||
|
println!("cargo:rerun-if-changed=src/x11/xdotool/vendor/xdo_util.h");
|
||||||
cc::Build::new()
|
cc::Build::new()
|
||||||
.include("src/evdev")
|
.include("src/evdev")
|
||||||
.file("src/evdev/native.c")
|
.file("src/evdev/native.c")
|
||||||
|
@ -47,6 +50,14 @@ fn cc_config() {
|
||||||
println!("cargo:rustc-link-lib=dylib=xkbcommon");
|
println!("cargo:rustc-link-lib=dylib=xkbcommon");
|
||||||
|
|
||||||
if cfg!(not(feature = "wayland")) {
|
if cfg!(not(feature = "wayland")) {
|
||||||
|
cc::Build::new()
|
||||||
|
.cpp(false)
|
||||||
|
.include("src/x11/xdotool/vendor/xdo.h")
|
||||||
|
.include("src/x11/xdotool/vendor/xdo_util.h")
|
||||||
|
.file("src/x11/xdotool/vendor/xdo.c")
|
||||||
|
.compile("xdotoolvendor");
|
||||||
|
|
||||||
|
println!("cargo:rustc-link-lib=static=xdotoolvendor");
|
||||||
println!("cargo:rustc-link-lib=dylib=X11");
|
println!("cargo:rustc-link-lib=dylib=X11");
|
||||||
println!("cargo:rustc-link-lib=dylib=Xtst");
|
println!("cargo:rustc-link-lib=dylib=Xtst");
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,10 @@ pub struct InjectionOptions {
|
||||||
// Used to set a modifier-specific delay.
|
// Used to set a modifier-specific delay.
|
||||||
// NOTE: Only relevant on Wayland systems.
|
// NOTE: Only relevant on Wayland systems.
|
||||||
pub evdev_modifier_delay: u32,
|
pub evdev_modifier_delay: u32,
|
||||||
|
|
||||||
|
// If true, use the xdotool fallback to perform the expansions.
|
||||||
|
// NOTE: Only relevant on Linux-X11 systems.
|
||||||
|
pub x11_use_xdotool_fallback: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for InjectionOptions {
|
impl Default for InjectionOptions {
|
||||||
|
@ -84,6 +88,7 @@ impl Default for InjectionOptions {
|
||||||
delay: default_delay,
|
delay: default_delay,
|
||||||
disable_fast_inject: false,
|
disable_fast_inject: false,
|
||||||
evdev_modifier_delay: 10,
|
evdev_modifier_delay: 10,
|
||||||
|
x11_use_xdotool_fallback: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,8 +153,8 @@ pub fn get_injector(options: InjectorCreationOptions) -> Result<Box<dyn Injector
|
||||||
info!("using EVDEVInjector");
|
info!("using EVDEVInjector");
|
||||||
Ok(Box::new(evdev::EVDEVInjector::new(options)?))
|
Ok(Box::new(evdev::EVDEVInjector::new(options)?))
|
||||||
} else {
|
} else {
|
||||||
info!("using X11Injector");
|
info!("using X11ProxyInjector");
|
||||||
Ok(Box::new(x11::X11Injector::new()?))
|
Ok(Box::new(x11::X11ProxyInjector::new()?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
Same approach as evdev, but the lookup logic is:
|
|
||||||
|
|
||||||
#include <locale.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include <X11/Xlibint.h>
|
|
||||||
#include <X11/Xlib.h>
|
|
||||||
#include <X11/Xutil.h>
|
|
||||||
#include <X11/cursorfont.h>
|
|
||||||
#include <X11/keysymdef.h>
|
|
||||||
#include <X11/keysym.h>
|
|
||||||
#include <X11/extensions/record.h>
|
|
||||||
#include <X11/extensions/XTest.h>
|
|
||||||
#include <X11/XKBlib.h>
|
|
||||||
#include <X11/Xatom.h>
|
|
||||||
|
|
||||||
Display *data_disp = NULL;
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
data_disp = XOpenDisplay(NULL);
|
|
||||||
|
|
||||||
for (int code = 0; code<256; code++) {
|
|
||||||
for (int state = 0; state < 256; state++) {
|
|
||||||
XKeyEvent event;
|
|
||||||
event.display = data_disp;
|
|
||||||
event.window = XDefaultRootWindow(data_disp);
|
|
||||||
event.root = XDefaultRootWindow(data_disp);
|
|
||||||
event.subwindow = None;
|
|
||||||
event.time = 0;
|
|
||||||
event.x = 1;
|
|
||||||
event.y = 1;
|
|
||||||
event.x_root = 1;
|
|
||||||
event.y_root = 1;
|
|
||||||
event.same_screen = True;
|
|
||||||
event.keycode = code + 8;
|
|
||||||
event.state = state;
|
|
||||||
event.type = KeyPress;
|
|
||||||
|
|
||||||
char buffer[10];
|
|
||||||
int res = XLookupString(&event, buffer, 9, NULL, NULL);
|
|
||||||
|
|
||||||
printf("hey %d %d %s\n", code, state, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
This way, we get the state mask associated with a character, and we can pass it directly when injecting a character:
|
|
||||||
https://github.com/federico-terzi/espanso/blob/master/native/liblinuxbridge/fast_xdo.cpp#L37
|
|
598
espanso-inject/src/x11/default/mod.rs
Normal file
598
espanso-inject/src/x11/default/mod.rs
Normal file
|
@ -0,0 +1,598 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
ffi::{CStr, CString},
|
||||||
|
os::raw::c_char,
|
||||||
|
slice,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::x11::ffi::{
|
||||||
|
Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay, XDefaultRootWindow,
|
||||||
|
XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XQueryKeymap,
|
||||||
|
XSendEvent, XSync, XTestFakeKeyEvent,
|
||||||
|
};
|
||||||
|
use libc::c_void;
|
||||||
|
use log::{debug, error};
|
||||||
|
|
||||||
|
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 crate::x11::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 KeyPair {
|
||||||
|
// Keycode
|
||||||
|
code: u32,
|
||||||
|
// Modifier state which combined with the code produces the char
|
||||||
|
// This is a bit mask:
|
||||||
|
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>;
|
||||||
|
|
||||||
|
pub struct X11DefaultInjector {
|
||||||
|
display: *mut Display,
|
||||||
|
|
||||||
|
char_map: CharMap,
|
||||||
|
sym_map: SymMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
impl X11DefaultInjector {
|
||||||
|
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 { crate::x11::ffi::XOpenDisplay(std::ptr::null()) };
|
||||||
|
if display.is_null() {
|
||||||
|
return Err(X11InjectorError::Init().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (char_map, sym_map) = Self::generate_maps(display)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
display,
|
||||||
|
char_map,
|
||||||
|
sym_map,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 preceding_dead_key = if let Some(dead_key) = dead_key {
|
||||||
|
let mut dead_key_event = XKeyEvent {
|
||||||
|
display,
|
||||||
|
keycode: dead_key.code,
|
||||||
|
state: dead_key.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 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 {
|
||||||
|
Xutf8LookupString(
|
||||||
|
input_context,
|
||||||
|
&mut key_event,
|
||||||
|
buffer.as_mut_ptr(),
|
||||||
|
(buffer.len() - 1) as i32,
|
||||||
|
&mut sym,
|
||||||
|
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) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>> {
|
||||||
|
syms
|
||||||
|
.iter()
|
||||||
|
.map(|sym| {
|
||||||
|
self
|
||||||
|
.sym_map
|
||||||
|
.get(sym)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| X11InjectorError::SymMapping(*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.main.code as u8)) {
|
||||||
|
current_state |= 1 << mod_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current_record.main.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: &KeyPair, 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: &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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 X11DefaultInjector {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
XCloseDisplay(self.display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Injector for X11DefaultInjector {
|
||||||
|
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::CharMapping(char).into())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
|
||||||
|
|
||||||
|
for record in records? {
|
||||||
|
if options.disable_fast_inject {
|
||||||
|
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 {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.main, true, delay_us);
|
||||||
|
self.xtest_send_key(&record.main, false, delay_us);
|
||||||
|
} else {
|
||||||
|
self.send_key(focused_window, &record.main, true, delay_us);
|
||||||
|
self.send_key(focused_window, &record.main, 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.main, true, delay_us);
|
||||||
|
} else {
|
||||||
|
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.main, false, delay_us);
|
||||||
|
} else {
|
||||||
|
self.send_key(focused_window, &record.main, false, delay_us);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum X11InjectorError {
|
||||||
|
#[error("failed to initialize x11 display")]
|
||||||
|
Init(),
|
||||||
|
|
||||||
|
#[error("missing vkey mapping for char `{0}`")]
|
||||||
|
CharMapping(String),
|
||||||
|
|
||||||
|
#[error("missing record mapping for sym `{0}`")]
|
||||||
|
SymMapping(u64),
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ use std::{
|
||||||
os::raw::{c_char, c_long, c_uint, c_ulong},
|
os::raw::{c_char, c_long, c_uint, c_ulong},
|
||||||
};
|
};
|
||||||
|
|
||||||
use libc::c_int;
|
use libc::{c_int, c_uchar};
|
||||||
|
|
||||||
pub enum Display {}
|
pub enum Display {}
|
||||||
pub type Window = u64;
|
pub type Window = u64;
|
||||||
|
@ -123,4 +123,6 @@ extern "C" {
|
||||||
pub fn XFilterEvent(event: *mut XKeyEvent, window: c_ulong) -> c_int;
|
pub fn XFilterEvent(event: *mut XKeyEvent, window: c_ulong) -> c_int;
|
||||||
pub fn XCloseIM(input_method: XIM) -> c_int;
|
pub fn XCloseIM(input_method: XIM) -> c_int;
|
||||||
pub fn XFree(data: *mut c_void) -> c_int;
|
pub fn XFree(data: *mut c_void) -> c_int;
|
||||||
|
pub fn XKeycodeToKeysym(display: *mut Display, keycode: c_uchar, index: c_int) -> c_ulong;
|
||||||
|
pub fn XKeysymToString(keysym: c_ulong) -> *mut c_char;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,584 +17,89 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use crate::Injector;
|
||||||
|
|
||||||
|
use anyhow::{bail, ensure, Result};
|
||||||
|
use log::{error, warn};
|
||||||
|
|
||||||
|
mod default;
|
||||||
mod ffi;
|
mod ffi;
|
||||||
|
mod xdotool;
|
||||||
|
|
||||||
use std::{
|
pub struct X11ProxyInjector {
|
||||||
collections::{HashMap, HashSet},
|
default_injector: Option<default::X11DefaultInjector>,
|
||||||
ffi::{CStr, CString},
|
xdotool_injector: Option<xdotool::X11XDOToolInjector>,
|
||||||
os::raw::c_char,
|
|
||||||
slice,
|
|
||||||
};
|
|
||||||
|
|
||||||
use ffi::{
|
|
||||||
Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay, XDefaultRootWindow,
|
|
||||||
XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XQueryKeymap,
|
|
||||||
XSendEvent, XSync, XTestFakeKeyEvent,
|
|
||||||
};
|
|
||||||
use libc::c_void;
|
|
||||||
use log::{debug, error};
|
|
||||||
|
|
||||||
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 KeyPair {
|
|
||||||
// Keycode
|
|
||||||
code: u32,
|
|
||||||
// Modifier state which combined with the code produces the char
|
|
||||||
// This is a bit mask:
|
|
||||||
state: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
impl X11ProxyInjector {
|
||||||
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>;
|
|
||||||
|
|
||||||
pub struct X11Injector {
|
|
||||||
display: *mut Display,
|
|
||||||
|
|
||||||
char_map: CharMap,
|
|
||||||
sym_map: SymMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::new_without_default)]
|
|
||||||
impl X11Injector {
|
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
// Necessary to properly handle non-ascii chars
|
let default_injector = match default::X11DefaultInjector::new() {
|
||||||
let empty_string = CString::new("")?;
|
Ok(injector) => Some(injector),
|
||||||
unsafe {
|
Err(err) => {
|
||||||
libc::setlocale(libc::LC_ALL, empty_string.as_ptr());
|
error!("X11DefaultInjector could not be initialized: {:?}", err);
|
||||||
}
|
warn!("falling back to xdotool injector");
|
||||||
|
|
||||||
let display = unsafe { ffi::XOpenDisplay(std::ptr::null()) };
|
|
||||||
if display.is_null() {
|
|
||||||
return Err(X11InjectorError::Init().into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let (char_map, sym_map) = Self::generate_maps(display)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
display,
|
|
||||||
char_map,
|
|
||||||
sym_map,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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 preceding_dead_key = if let Some(dead_key) = dead_key {
|
|
||||||
let mut dead_key_event = XKeyEvent {
|
|
||||||
display,
|
|
||||||
keycode: dead_key.code,
|
|
||||||
state: dead_key.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 dead_key_event, 0) };
|
|
||||||
|
|
||||||
Some(*dead_key)
|
|
||||||
} else {
|
|
||||||
None
|
None
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut key_event = XKeyEvent {
|
let xdotool_injector = match xdotool::X11XDOToolInjector::new() {
|
||||||
display,
|
Ok(injector) => Some(injector),
|
||||||
keycode: code_with_offset,
|
Err(err) => {
|
||||||
state: modifier_state,
|
error!("X11XDOToolInjector could not be initialized: {:?}", err);
|
||||||
|
None
|
||||||
// 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) };
|
if default_injector.is_none() && xdotool_injector.is_none() {
|
||||||
let mut sym: KeySym = 0;
|
bail!("unable to initialize injectors, neither the default or xdotool fallback could be initialized");
|
||||||
let mut buffer: [c_char; 10] = [0; 10];
|
|
||||||
|
|
||||||
let result = unsafe {
|
|
||||||
Xutf8LookupString(
|
|
||||||
input_context,
|
|
||||||
&mut key_event,
|
|
||||||
buffer.as_mut_ptr(),
|
|
||||||
(buffer.len() - 1) as i32,
|
|
||||||
&mut sym,
|
|
||||||
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) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Populated char_map with {} symbols", char_map.len());
|
Ok(X11ProxyInjector {
|
||||||
debug!("Populated sym_map with {} symbols", sym_map.len());
|
default_injector,
|
||||||
debug!("Detected {} dead key combinations", deadkeys.len());
|
xdotool_injector,
|
||||||
|
|
||||||
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>> {
|
|
||||||
syms
|
|
||||||
.iter()
|
|
||||||
.map(|sym| {
|
|
||||||
self
|
|
||||||
.sym_map
|
|
||||||
.get(sym)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| X11InjectorError::SymMapping(*sym).into())
|
|
||||||
})
|
})
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method was inspired by the wonderful xdotool by Jordan Sissel
|
fn get_active_injector(&self, options: &crate::InjectionOptions) -> Result<&dyn Injector> {
|
||||||
// https://github.com/jordansissel/xdotool
|
ensure!(
|
||||||
fn get_modifier_codes(&self) -> Vec<Vec<KeyCode>> {
|
self.default_injector.is_some() || self.xdotool_injector.is_some(),
|
||||||
let modifiers_ptr = unsafe { XGetModifierMapping(self.display) };
|
"unable to get active injector, neither default or xdotool fallback are available."
|
||||||
let modifiers = unsafe { *modifiers_ptr };
|
);
|
||||||
|
|
||||||
let mut modifiers_codes = Vec::new();
|
if options.x11_use_xdotool_fallback {
|
||||||
|
if let Some(xdotool_injector) = self.xdotool_injector.as_ref() {
|
||||||
for mod_index in 0..=7 {
|
return Ok(xdotool_injector);
|
||||||
let mut modifier_codes = Vec::new();
|
} else if let Some(default_injector) = self.default_injector.as_ref() {
|
||||||
for mod_key in 0..modifiers.max_keypermod {
|
return Ok(default_injector);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
} else if let Some(default_injector) = self.default_injector.as_ref() {
|
||||||
modifiers_codes.push(modifier_codes);
|
return Ok(default_injector);
|
||||||
|
} else if let Some(xdotool_injector) = self.xdotool_injector.as_ref() {
|
||||||
|
return Ok(xdotool_injector);
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe { XFreeModifiermap(modifiers_ptr) };
|
unreachable!()
|
||||||
|
|
||||||
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.main.code as u8)) {
|
|
||||||
current_state |= 1 << mod_index;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
current_record.main.state = current_state;
|
impl Injector for X11ProxyInjector {
|
||||||
records.push(current_record);
|
fn send_string(&self, string: &str, options: crate::InjectionOptions) -> Result<()> {
|
||||||
}
|
|
||||||
|
|
||||||
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: &KeyPair, 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: &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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
self
|
||||||
.char_map
|
.get_active_injector(&options)?
|
||||||
.get(&char)
|
.send_string(string, options)
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| X11InjectorError::CharMapping(char).into())
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let delay_us = options.delay as u32 * 1000; // Convert to micro seconds
|
|
||||||
|
|
||||||
for record in records? {
|
|
||||||
if options.disable_fast_inject {
|
|
||||||
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);
|
fn send_keys(&self, keys: &[crate::keys::Key], options: crate::InjectionOptions) -> Result<()> {
|
||||||
self.xtest_send_key(&record.main, false, delay_us);
|
self.get_active_injector(&options)?.send_keys(keys, options)
|
||||||
} else {
|
|
||||||
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);
|
fn send_key_combination(
|
||||||
self.send_key(focused_window, &record.main, false, delay_us);
|
&self,
|
||||||
|
keys: &[crate::keys::Key],
|
||||||
|
options: crate::InjectionOptions,
|
||||||
|
) -> Result<()> {
|
||||||
|
self
|
||||||
|
.get_active_injector(&options)?
|
||||||
|
.send_key_combination(keys, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.main, true, delay_us);
|
|
||||||
self.xtest_send_key(&record.main, false, delay_us);
|
|
||||||
} else {
|
|
||||||
self.send_key(focused_window, &record.main, true, delay_us);
|
|
||||||
self.send_key(focused_window, &record.main, 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.main, true, delay_us);
|
|
||||||
} else {
|
|
||||||
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.main, false, delay_us);
|
|
||||||
} else {
|
|
||||||
self.send_key(focused_window, &record.main, false, delay_us);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum X11InjectorError {
|
|
||||||
#[error("failed to initialize x11 display")]
|
|
||||||
Init(),
|
|
||||||
|
|
||||||
#[error("missing vkey mapping for char `{0}`")]
|
|
||||||
CharMapping(String),
|
|
||||||
|
|
||||||
#[error("missing record mapping for sym `{0}`")]
|
|
||||||
SymMapping(u64),
|
|
||||||
}
|
|
||||||
|
|
2
espanso-inject/src/x11/xdotool/README.md
Normal file
2
espanso-inject/src/x11/xdotool/README.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
This is a fallback injection module that relies on the awesome xdotool project:
|
||||||
|
https://github.com/jordansissel/xdotool
|
61
espanso-inject/src/x11/xdotool/ffi.rs
Normal file
61
espanso-inject/src/x11/xdotool/ffi.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use libc::{c_char, c_int, c_long, useconds_t, wchar_t};
|
||||||
|
|
||||||
|
use crate::x11::ffi::{Display, Window};
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct charcodemap_t {
|
||||||
|
pub key: wchar_t,
|
||||||
|
pub code: c_char,
|
||||||
|
pub symbol: c_long,
|
||||||
|
pub group: c_int,
|
||||||
|
pub modmask: c_int,
|
||||||
|
pub needs_binding: c_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct xdo_t {
|
||||||
|
pub xdpy: *mut Display,
|
||||||
|
pub display_name: *const c_char,
|
||||||
|
pub charcodes: *const charcodemap_t,
|
||||||
|
pub charcodes_len: c_int,
|
||||||
|
pub keycode_high: c_int,
|
||||||
|
pub keycode_low: c_int,
|
||||||
|
pub keysyms_per_keycode: c_int,
|
||||||
|
pub close_display_when_freed: c_int,
|
||||||
|
pub quiet: c_int,
|
||||||
|
pub debug: c_int,
|
||||||
|
pub features_mask: c_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CURRENTWINDOW: u64 = 0;
|
||||||
|
|
||||||
|
#[link(name = "xdotoolvendor", kind = "static")]
|
||||||
|
extern "C" {
|
||||||
|
pub fn xdo_new(display: *const c_char) -> *mut xdo_t;
|
||||||
|
pub fn xdo_free(xdo: *const xdo_t);
|
||||||
|
pub fn xdo_enter_text_window(
|
||||||
|
xdo: *const xdo_t,
|
||||||
|
window: Window,
|
||||||
|
string: *const c_char,
|
||||||
|
delay: useconds_t,
|
||||||
|
);
|
||||||
|
pub fn xdo_send_keysequence_window(
|
||||||
|
xdo: *const xdo_t,
|
||||||
|
window: Window,
|
||||||
|
keysequence: *const c_char,
|
||||||
|
delay: useconds_t,
|
||||||
|
);
|
||||||
|
pub fn fast_send_event(xdo: *const xdo_t, window: Window, keycode: c_int, pressed: c_int);
|
||||||
|
pub fn fast_enter_text_window(
|
||||||
|
xdo: *const xdo_t,
|
||||||
|
window: Window,
|
||||||
|
string: *const c_char,
|
||||||
|
delay: useconds_t,
|
||||||
|
);
|
||||||
|
pub fn fast_send_keysequence_window(
|
||||||
|
xdo: *const xdo_t,
|
||||||
|
window: Window,
|
||||||
|
keysequence: *const c_char,
|
||||||
|
delay: useconds_t,
|
||||||
|
);
|
||||||
|
}
|
348
espanso-inject/src/x11/xdotool/mod.rs
Normal file
348
espanso-inject/src/x11/xdotool/mod.rs
Normal file
|
@ -0,0 +1,348 @@
|
||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Self> {
|
||||||
|
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<String> = 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<String> = 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<String> {
|
||||||
|
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())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
24
espanso-inject/src/x11/xdotool/vendor/COPYRIGHT
vendored
Normal file
24
espanso-inject/src/x11/xdotool/vendor/COPYRIGHT
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
Copyright (c) 2007, 2008, 2009: Jordan Sissel.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of the Jordan Sissel nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY JORDAN SISSEL ``AS IS'' AND ANY
|
||||||
|
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL JORDAN SISSEL BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
2266
espanso-inject/src/x11/xdotool/vendor/xdo.c
vendored
Normal file
2266
espanso-inject/src/x11/xdotool/vendor/xdo.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
938
espanso-inject/src/x11/xdotool/vendor/xdo.h
vendored
Normal file
938
espanso-inject/src/x11/xdotool/vendor/xdo.h
vendored
Normal file
|
@ -0,0 +1,938 @@
|
||||||
|
/**
|
||||||
|
* @file xdo.h
|
||||||
|
*/
|
||||||
|
#ifndef _XDO_H_
|
||||||
|
#define _XDO_H_
|
||||||
|
|
||||||
|
#ifndef __USE_XOPEN
|
||||||
|
#define __USE_XOPEN
|
||||||
|
#endif /* __USE_XOPEN */
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/X.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <wchar.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mainpage
|
||||||
|
*
|
||||||
|
* libxdo helps you send fake mouse and keyboard input, search for windows,
|
||||||
|
* perform various window management tasks such as desktop changes, window
|
||||||
|
* movement, etc.
|
||||||
|
*
|
||||||
|
* For examples on libxdo usage, the xdotool source code is a good reference.
|
||||||
|
*
|
||||||
|
* @see xdo.h
|
||||||
|
* @see xdo_new
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When issuing a window size change, giving this flag will make the size
|
||||||
|
* change be relative to the size hints of the window. For terminals, this
|
||||||
|
* generally means that the window size will be relative to the font size,
|
||||||
|
* allowing you to change window sizes based on character rows and columns
|
||||||
|
* instead of pixels.
|
||||||
|
*/
|
||||||
|
#define SIZE_USEHINTS (1L << 0)
|
||||||
|
#define SIZE_USEHINTS_X (1L << 1)
|
||||||
|
#define SIZE_USEHINTS_Y (1L << 2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CURRENTWINDOW is a special identify for xdo input faking (mouse and
|
||||||
|
* keyboard) functions like xdo_send_keysequence_window that indicate we should target the
|
||||||
|
* current window, not a specific window.
|
||||||
|
*
|
||||||
|
* Generally, this means we will use XTEST instead of XSendEvent when sending
|
||||||
|
* events.
|
||||||
|
*/
|
||||||
|
#define CURRENTWINDOW (0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Map character to whatever information we need to be able to send
|
||||||
|
* this key (keycode, modifiers, group, etc)
|
||||||
|
*/
|
||||||
|
typedef struct charcodemap {
|
||||||
|
wchar_t key; /** the letter for this key, like 'a' */
|
||||||
|
KeyCode code; /** the keycode that this key is on */
|
||||||
|
KeySym symbol; /** the symbol representing this key */
|
||||||
|
int group; /** the keyboard group that has this key in it */
|
||||||
|
int modmask; /** the modifiers to apply when sending this key */
|
||||||
|
/** if this key need to be bound at runtime because it does not
|
||||||
|
* exist in the current keymap, this will be set to 1. */
|
||||||
|
int needs_binding;
|
||||||
|
} charcodemap_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
XDO_FEATURE_XTEST, /** Is XTest available? */
|
||||||
|
} XDO_FEATURES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main context.
|
||||||
|
*/
|
||||||
|
typedef struct xdo {
|
||||||
|
|
||||||
|
/** The Display for Xlib */
|
||||||
|
Display *xdpy;
|
||||||
|
|
||||||
|
/** The display name, if any. NULL if not specified. */
|
||||||
|
char *display_name;
|
||||||
|
|
||||||
|
/** @internal Array of known keys/characters */
|
||||||
|
charcodemap_t *charcodes;
|
||||||
|
|
||||||
|
/** @internal Length of charcodes array */
|
||||||
|
int charcodes_len;
|
||||||
|
|
||||||
|
/** @internal highest keycode value */
|
||||||
|
int keycode_high; /* highest and lowest keycodes */
|
||||||
|
|
||||||
|
/** @internal lowest keycode value */
|
||||||
|
int keycode_low; /* used by this X server */
|
||||||
|
|
||||||
|
/** @internal number of keysyms per keycode */
|
||||||
|
int keysyms_per_keycode;
|
||||||
|
|
||||||
|
/** Should we close the display when calling xdo_free? */
|
||||||
|
int close_display_when_freed;
|
||||||
|
|
||||||
|
/** Be extra quiet? (omits some error/message output) */
|
||||||
|
int quiet;
|
||||||
|
|
||||||
|
/** Enable debug output? */
|
||||||
|
int debug;
|
||||||
|
|
||||||
|
/** Feature flags, such as XDO_FEATURE_XTEST, etc... */
|
||||||
|
int features_mask;
|
||||||
|
|
||||||
|
} xdo_t;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search only window title. DEPRECATED - Use SEARCH_NAME
|
||||||
|
* @see xdo_search_windows
|
||||||
|
*/
|
||||||
|
#define SEARCH_TITLE (1UL << 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search only window class.
|
||||||
|
* @see xdo_search_windows
|
||||||
|
*/
|
||||||
|
#define SEARCH_CLASS (1UL << 1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search only window name.
|
||||||
|
* @see xdo_search_windows
|
||||||
|
*/
|
||||||
|
#define SEARCH_NAME (1UL << 2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search only window pid.
|
||||||
|
* @see xdo_search_windows
|
||||||
|
*/
|
||||||
|
#define SEARCH_PID (1UL << 3)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search only visible windows.
|
||||||
|
* @see xdo_search_windows
|
||||||
|
*/
|
||||||
|
#define SEARCH_ONLYVISIBLE (1UL << 4)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search only a specific screen.
|
||||||
|
* @see xdo_search.screen
|
||||||
|
* @see xdo_search_windows
|
||||||
|
*/
|
||||||
|
#define SEARCH_SCREEN (1UL << 5)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search only window class name.
|
||||||
|
* @see xdo_search
|
||||||
|
*/
|
||||||
|
#define SEARCH_CLASSNAME (1UL << 6)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search a specific desktop
|
||||||
|
* @see xdo_search.screen
|
||||||
|
* @see xdo_search_windows
|
||||||
|
*/
|
||||||
|
#define SEARCH_DESKTOP (1UL << 7)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search only window role.
|
||||||
|
* @see xdo_search
|
||||||
|
*/
|
||||||
|
#define SEARCH_ROLE (1UL << 8)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The window search query structure.
|
||||||
|
*
|
||||||
|
* @see xdo_search_windows
|
||||||
|
*/
|
||||||
|
typedef struct xdo_search {
|
||||||
|
const char *title; /** pattern to test against a window title */
|
||||||
|
const char *winclass; /** pattern to test against a window class */
|
||||||
|
const char *winclassname; /** pattern to test against a window class */
|
||||||
|
const char *winname; /** pattern to test against a window name */
|
||||||
|
const char *winrole; /** pattern to test against a window role */
|
||||||
|
int pid; /** window pid (From window atom _NET_WM_PID) */
|
||||||
|
long max_depth; /** depth of search. 1 means only toplevel windows */
|
||||||
|
int only_visible; /** boolean; set true to search only visible windows */
|
||||||
|
int screen; /** what screen to search, if any. If none given, search
|
||||||
|
all screens */
|
||||||
|
|
||||||
|
/** Should the tests be 'and' or 'or' ? If 'and', any failure will skip the
|
||||||
|
* window. If 'or', any success will keep the window in search results. */
|
||||||
|
enum { SEARCH_ANY, SEARCH_ALL } require;
|
||||||
|
|
||||||
|
/** bitmask of things you are searching for, such as SEARCH_NAME, etc.
|
||||||
|
* @see SEARCH_NAME, SEARCH_CLASS, SEARCH_PID, SEARCH_CLASSNAME, etc
|
||||||
|
*/
|
||||||
|
unsigned int searchmask;
|
||||||
|
|
||||||
|
/** What desktop to search, if any. If none given, search all screens. */
|
||||||
|
long desktop;
|
||||||
|
|
||||||
|
/** How many results to return? If 0, return all. */
|
||||||
|
unsigned int limit;
|
||||||
|
} xdo_search_t;
|
||||||
|
|
||||||
|
#define XDO_ERROR 1
|
||||||
|
#define XDO_SUCCESS 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new xdo_t instance.
|
||||||
|
*
|
||||||
|
* @param display the string display name, such as ":0". If null, uses the
|
||||||
|
* environment variable DISPLAY just like XOpenDisplay(NULL).
|
||||||
|
*
|
||||||
|
* @return Pointer to a new xdo_t or NULL on failure
|
||||||
|
*/
|
||||||
|
xdo_t* xdo_new(const char *display);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new xdo_t instance with an existing X11 Display instance.
|
||||||
|
*
|
||||||
|
* @param xdpy the Display pointer given by a previous XOpenDisplay()
|
||||||
|
* @param display the string display name
|
||||||
|
* @param close_display_when_freed If true, we will close the display when
|
||||||
|
* xdo_free is called. Otherwise, we leave it open.
|
||||||
|
*/
|
||||||
|
xdo_t* xdo_new_with_opened_display(Display *xdpy, const char *display,
|
||||||
|
int close_display_when_freed);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a string representing the version of this library
|
||||||
|
*/
|
||||||
|
const char *xdo_version(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Free and destroy an xdo_t instance.
|
||||||
|
*
|
||||||
|
* If close_display_when_freed is set, then we will also close the Display.
|
||||||
|
*/
|
||||||
|
void xdo_free(xdo_t *xdo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the mouse to a specific location.
|
||||||
|
*
|
||||||
|
* @param x the target X coordinate on the screen in pixels.
|
||||||
|
* @param y the target Y coordinate on the screen in pixels.
|
||||||
|
* @param screen the screen (number) you want to move on.
|
||||||
|
*/
|
||||||
|
int xdo_move_mouse(const xdo_t *xdo, int x, int y, int screen);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the mouse to a specific location relative to the top-left corner
|
||||||
|
* of a window.
|
||||||
|
*
|
||||||
|
* @param x the target X coordinate on the screen in pixels.
|
||||||
|
* @param y the target Y coordinate on the screen in pixels.
|
||||||
|
*/
|
||||||
|
int xdo_move_mouse_relative_to_window(const xdo_t *xdo, Window window, int x, int y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the mouse relative to it's current position.
|
||||||
|
*
|
||||||
|
* @param x the distance in pixels to move on the X axis.
|
||||||
|
* @param y the distance in pixels to move on the Y axis.
|
||||||
|
*/
|
||||||
|
int xdo_move_mouse_relative(const xdo_t *xdo, int x, int y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a mouse press (aka mouse down) for a given button at the current mouse
|
||||||
|
* location.
|
||||||
|
*
|
||||||
|
* @param window The window you want to send the event to or CURRENTWINDOW
|
||||||
|
* @param button The mouse button. Generally, 1 is left, 2 is middle, 3 is
|
||||||
|
* right, 4 is wheel up, 5 is wheel down.
|
||||||
|
*/
|
||||||
|
int xdo_mouse_down(const xdo_t *xdo, Window window, int button);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a mouse release (aka mouse up) for a given button at the current mouse
|
||||||
|
* location.
|
||||||
|
*
|
||||||
|
* @param window The window you want to send the event to or CURRENTWINDOW
|
||||||
|
* @param button The mouse button. Generally, 1 is left, 2 is middle, 3 is
|
||||||
|
* right, 4 is wheel up, 5 is wheel down.
|
||||||
|
*/
|
||||||
|
int xdo_mouse_up(const xdo_t *xdo, Window window, int button);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current mouse location (coordinates and screen number).
|
||||||
|
*
|
||||||
|
* @param x integer pointer where the X coordinate will be stored
|
||||||
|
* @param y integer pointer where the Y coordinate will be stored
|
||||||
|
* @param screen_num integer pointer where the screen number will be stored
|
||||||
|
*/
|
||||||
|
int xdo_get_mouse_location(const xdo_t *xdo, int *x, int *y, int *screen_num);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the window the mouse is currently over
|
||||||
|
*
|
||||||
|
* @param window_ret Window pointer where the window will be stored.
|
||||||
|
*/
|
||||||
|
int xdo_get_window_at_mouse(const xdo_t *xdo, Window *window_ret);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all mouse location-related data.
|
||||||
|
*
|
||||||
|
* If null is passed for any parameter, we simply do not store it.
|
||||||
|
* Useful if you only want the 'y' coordinate, for example.
|
||||||
|
*
|
||||||
|
* @param x integer pointer where the X coordinate will be stored
|
||||||
|
* @param y integer pointer where the Y coordinate will be stored
|
||||||
|
* @param screen_num integer pointer where the screen number will be stored
|
||||||
|
* @param window Window pointer where the window/client the mouse is over
|
||||||
|
* will be stored.
|
||||||
|
*/
|
||||||
|
int xdo_get_mouse_location2(const xdo_t *xdo, int *x_ret, int *y_ret,
|
||||||
|
int *screen_num_ret, Window *window_ret);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the mouse to move from a location. This function will block
|
||||||
|
* until the condition has been satisfied.
|
||||||
|
*
|
||||||
|
* @param origin_x the X position you expect the mouse to move from
|
||||||
|
* @param origin_y the Y position you expect the mouse to move from
|
||||||
|
*/
|
||||||
|
int xdo_wait_for_mouse_move_from(const xdo_t *xdo, int origin_x, int origin_y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the mouse to move to a location. This function will block
|
||||||
|
* until the condition has been satisfied.
|
||||||
|
*
|
||||||
|
* @param dest_x the X position you expect the mouse to move to
|
||||||
|
* @param dest_y the Y position you expect the mouse to move to
|
||||||
|
*/
|
||||||
|
int xdo_wait_for_mouse_move_to(const xdo_t *xdo, int dest_x, int dest_y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a click for a specific mouse button at the current mouse location.
|
||||||
|
*
|
||||||
|
* @param window The window you want to send the event to or CURRENTWINDOW
|
||||||
|
* @param button The mouse button. Generally, 1 is left, 2 is middle, 3 is
|
||||||
|
* right, 4 is wheel up, 5 is wheel down.
|
||||||
|
*/
|
||||||
|
int xdo_click_window(const xdo_t *xdo, Window window, int button);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a one or more clicks for a specific mouse button at the current mouse
|
||||||
|
* location.
|
||||||
|
*
|
||||||
|
* @param window The window you want to send the event to or CURRENTWINDOW
|
||||||
|
* @param button The mouse button. Generally, 1 is left, 2 is middle, 3 is
|
||||||
|
* right, 4 is wheel up, 5 is wheel down.
|
||||||
|
*/
|
||||||
|
int xdo_click_window_multiple(const xdo_t *xdo, Window window, int button,
|
||||||
|
int repeat, useconds_t delay);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type a string to the specified window.
|
||||||
|
*
|
||||||
|
* If you want to send a specific key or key sequence, such as "alt+l", you
|
||||||
|
* want instead xdo_send_keysequence_window(...).
|
||||||
|
*
|
||||||
|
* @param window The window you want to send keystrokes to or CURRENTWINDOW
|
||||||
|
* @param string The string to type, like "Hello world!"
|
||||||
|
* @param delay The delay between keystrokes in microseconds. 12000 is a decent
|
||||||
|
* choice if you don't have other plans.
|
||||||
|
*/
|
||||||
|
int xdo_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a keysequence to the specified window.
|
||||||
|
*
|
||||||
|
* This allows you to send keysequences by symbol name. Any combination
|
||||||
|
* of X11 KeySym names separated by '+' are valid. Single KeySym names
|
||||||
|
* are valid, too.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* "l"
|
||||||
|
* "semicolon"
|
||||||
|
* "alt+Return"
|
||||||
|
* "Alt_L+Tab"
|
||||||
|
*
|
||||||
|
* If you want to type a string, such as "Hello world." you want to instead
|
||||||
|
* use xdo_enter_text_window.
|
||||||
|
*
|
||||||
|
* @param window The window you want to send the keysequence to or
|
||||||
|
* CURRENTWINDOW
|
||||||
|
* @param keysequence The string keysequence to send.
|
||||||
|
* @param delay The delay between keystrokes in microseconds.
|
||||||
|
*/
|
||||||
|
int xdo_send_keysequence_window(const xdo_t *xdo, Window window,
|
||||||
|
const char *keysequence, useconds_t delay);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send key release (up) events for the given key sequence.
|
||||||
|
*
|
||||||
|
* @see xdo_send_keysequence_window
|
||||||
|
*/
|
||||||
|
int xdo_send_keysequence_window_up(const xdo_t *xdo, Window window,
|
||||||
|
const char *keysequence, useconds_t delay);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send key press (down) events for the given key sequence.
|
||||||
|
*
|
||||||
|
* @see xdo_send_keysequence_window
|
||||||
|
*/
|
||||||
|
int xdo_send_keysequence_window_down(const xdo_t *xdo, Window window,
|
||||||
|
const char *keysequence, useconds_t delay);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a series of keystrokes.
|
||||||
|
*
|
||||||
|
* @param window The window to send events to or CURRENTWINDOW
|
||||||
|
* @param keys The array of charcodemap_t entities to send.
|
||||||
|
* @param nkeys The length of the keys parameter
|
||||||
|
* @param pressed 1 for key press, 0 for key release.
|
||||||
|
* @param modifier Pointer to integer to record the modifiers activated by
|
||||||
|
* the keys being pressed. If NULL, we don't save the modifiers.
|
||||||
|
* @param delay The delay between keystrokes in microseconds.
|
||||||
|
*/
|
||||||
|
int xdo_send_keysequence_window_list_do(const xdo_t *xdo, Window window,
|
||||||
|
charcodemap_t *keys, int nkeys,
|
||||||
|
int pressed, int *modifier, useconds_t delay);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a window to have a specific map state.
|
||||||
|
*
|
||||||
|
* State possibilities:
|
||||||
|
* IsUnmapped - window is not displayed.
|
||||||
|
* IsViewable - window is mapped and shown (though may be clipped by windows
|
||||||
|
* on top of it)
|
||||||
|
* IsUnviewable - window is mapped but a parent window is unmapped.
|
||||||
|
*
|
||||||
|
* @param wid the window you want to wait for.
|
||||||
|
* @param map_state the state to wait for.
|
||||||
|
*/
|
||||||
|
int xdo_wait_for_window_map_state(const xdo_t *xdo, Window wid, int map_state);
|
||||||
|
|
||||||
|
#define SIZE_TO 0
|
||||||
|
#define SIZE_FROM 1
|
||||||
|
int xdo_wait_for_window_size(const xdo_t *xdo, Window window, unsigned int width,
|
||||||
|
unsigned int height, int flags, int to_or_from);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a window to a specific location.
|
||||||
|
*
|
||||||
|
* The top left corner of the window will be moved to the x,y coordinate.
|
||||||
|
*
|
||||||
|
* @param wid the window to move
|
||||||
|
* @param x the X coordinate to move to.
|
||||||
|
* @param y the Y coordinate to move to.
|
||||||
|
*/
|
||||||
|
int xdo_move_window(const xdo_t *xdo, Window wid, int x, int y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a window's sizing hints (if any) to a given width and height.
|
||||||
|
*
|
||||||
|
* This function wraps XGetWMNormalHints() and applies any
|
||||||
|
* resize increment and base size to your given width and height values.
|
||||||
|
*
|
||||||
|
* @param window the window to use
|
||||||
|
* @param width the unit width you want to translate
|
||||||
|
* @param height the unit height you want to translate
|
||||||
|
* @param width_ret the return location of the translated width
|
||||||
|
* @param height_ret the return location of the translated height
|
||||||
|
*/
|
||||||
|
int xdo_translate_window_with_sizehint(const xdo_t *xdo, Window window,
|
||||||
|
unsigned int width, unsigned int height,
|
||||||
|
unsigned int *width_ret, unsigned int *height_ret);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the window size.
|
||||||
|
*
|
||||||
|
* @param wid the window to resize
|
||||||
|
* @param w the new desired width
|
||||||
|
* @param h the new desired height
|
||||||
|
* @param flags if 0, use pixels for units. If SIZE_USEHINTS, then
|
||||||
|
* the units will be relative to the window size hints.
|
||||||
|
*/
|
||||||
|
int xdo_set_window_size(const xdo_t *xdo, Window wid, int w, int h, int flags);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change a window property.
|
||||||
|
*
|
||||||
|
* Example properties you can change are WM_NAME, WM_ICON_NAME, etc.
|
||||||
|
*
|
||||||
|
* @param wid The window to change a property of.
|
||||||
|
* @param property the string name of the property.
|
||||||
|
* @param value the string value of the property.
|
||||||
|
*/
|
||||||
|
int xdo_set_window_property(const xdo_t *xdo, Window wid, const char *property,
|
||||||
|
const char *value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the window's classname and or class.
|
||||||
|
*
|
||||||
|
* @param name The new class name. If NULL, no change.
|
||||||
|
* @param _class The new class. If NULL, no change.
|
||||||
|
*/
|
||||||
|
int xdo_set_window_class(const xdo_t *xdo, Window wid, const char *name,
|
||||||
|
const char *_class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the urgency hint for a window.
|
||||||
|
*/
|
||||||
|
int xdo_set_window_urgency (const xdo_t *xdo, Window wid, int urgency);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the override_redirect value for a window. This generally means
|
||||||
|
* whether or not a window manager will manage this window.
|
||||||
|
*
|
||||||
|
* If you set it to 1, the window manager will usually not draw borders on the
|
||||||
|
* window, etc. If you set it to 0, the window manager will see it like a
|
||||||
|
* normal application window.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
int xdo_set_window_override_redirect(const xdo_t *xdo, Window wid,
|
||||||
|
int override_redirect);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focus a window.
|
||||||
|
*
|
||||||
|
* @see xdo_activate_window
|
||||||
|
* @param wid the window to focus.
|
||||||
|
*/
|
||||||
|
int xdo_focus_window(const xdo_t *xdo, Window wid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raise a window to the top of the window stack. This is also sometimes
|
||||||
|
* termed as bringing the window forward.
|
||||||
|
*
|
||||||
|
* @param wid The window to raise.
|
||||||
|
*/
|
||||||
|
int xdo_raise_window(const xdo_t *xdo, Window wid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the window currently having focus.
|
||||||
|
*
|
||||||
|
* @param window_ret Pointer to a window where the currently-focused window
|
||||||
|
* will be stored.
|
||||||
|
*/
|
||||||
|
int xdo_get_focused_window(const xdo_t *xdo, Window *window_ret);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a window to have or lose focus.
|
||||||
|
*
|
||||||
|
* @param window The window to wait on
|
||||||
|
* @param want_focus If 1, wait for focus. If 0, wait for loss of focus.
|
||||||
|
*/
|
||||||
|
int xdo_wait_for_window_focus(const xdo_t *xdo, Window window, int want_focus);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the PID owning a window. Not all applications support this.
|
||||||
|
* It looks at the _NET_WM_PID property of the window.
|
||||||
|
*
|
||||||
|
* @param window the window to query.
|
||||||
|
* @return the process id or 0 if no pid found.
|
||||||
|
*/
|
||||||
|
int xdo_get_pid_window(const xdo_t *xdo, Window window);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like xdo_get_focused_window, but return the first ancestor-or-self window *
|
||||||
|
* having a property of WM_CLASS. This allows you to get the "real" or
|
||||||
|
* top-level-ish window having focus rather than something you may not expect
|
||||||
|
* to be the window having focused.
|
||||||
|
*
|
||||||
|
* @param window_ret Pointer to a window where the currently-focused window
|
||||||
|
* will be stored.
|
||||||
|
*/
|
||||||
|
int xdo_get_focused_window_sane(const xdo_t *xdo, Window *window_ret);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate a window. This is generally a better choice than xdo_focus_window
|
||||||
|
* for a variety of reasons, but it requires window manager support:
|
||||||
|
* - If the window is on another desktop, that desktop is switched to.
|
||||||
|
* - It moves the window forward rather than simply focusing it
|
||||||
|
*
|
||||||
|
* Requires your window manager to support this.
|
||||||
|
* Uses _NET_ACTIVE_WINDOW from the EWMH spec.
|
||||||
|
*
|
||||||
|
* @param wid the window to activate
|
||||||
|
*/
|
||||||
|
int xdo_activate_window(const xdo_t *xdo, Window wid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a window to be active or not active.
|
||||||
|
*
|
||||||
|
* Requires your window manager to support this.
|
||||||
|
* Uses _NET_ACTIVE_WINDOW from the EWMH spec.
|
||||||
|
*
|
||||||
|
* @param window the window to wait on
|
||||||
|
* @param active If 1, wait for active. If 0, wait for inactive.
|
||||||
|
*/
|
||||||
|
int xdo_wait_for_window_active(const xdo_t *xdo, Window window, int active);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a window. This mostly means to make the window visible if it is
|
||||||
|
* not currently mapped.
|
||||||
|
*
|
||||||
|
* @param wid the window to map.
|
||||||
|
*/
|
||||||
|
int xdo_map_window(const xdo_t *xdo, Window wid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmap a window
|
||||||
|
*
|
||||||
|
* @param wid the window to unmap
|
||||||
|
*/
|
||||||
|
int xdo_unmap_window(const xdo_t *xdo, Window wid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimize a window.
|
||||||
|
*/
|
||||||
|
int xdo_minimize_window(const xdo_t *xdo, Window wid);
|
||||||
|
|
||||||
|
#define _NET_WM_STATE_REMOVE 0 /* remove/unset property */
|
||||||
|
#define _NET_WM_STATE_ADD 1 /* add/set property */
|
||||||
|
#define _NET_WM_STATE_TOGGLE 2 /* toggle property */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get window classname
|
||||||
|
* @param window the window
|
||||||
|
* @param class_ret Pointer to the window classname WM_CLASS
|
||||||
|
*/
|
||||||
|
int xdo_get_window_classname(const xdo_t *xdo, Window window, unsigned char **class_ret);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change window state
|
||||||
|
* @param action the _NET_WM_STATE action
|
||||||
|
*/
|
||||||
|
int xdo_window_state(xdo_t *xdo, Window window, unsigned long action, const char *property);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reparents a window
|
||||||
|
*
|
||||||
|
* @param wid_source the window to reparent
|
||||||
|
* @param wid_target the new parent window
|
||||||
|
*/
|
||||||
|
int xdo_reparent_window(const xdo_t *xdo, Window wid_source, Window wid_target);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a window's location.
|
||||||
|
*
|
||||||
|
* @param wid the window to query
|
||||||
|
* @param x_ret pointer to int where the X location is stored. If NULL, X is
|
||||||
|
* ignored.
|
||||||
|
* @param y_ret pointer to int where the Y location is stored. If NULL, X is
|
||||||
|
* ignored.
|
||||||
|
* @param screen_ret Pointer to Screen* where the Screen* the window on is
|
||||||
|
* stored. If NULL, this parameter is ignored.
|
||||||
|
*/
|
||||||
|
int xdo_get_window_location(const xdo_t *xdo, Window wid,
|
||||||
|
int *x_ret, int *y_ret, Screen **screen_ret);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a window's size.
|
||||||
|
*
|
||||||
|
* @param wid the window to query
|
||||||
|
* @param width_ret pointer to unsigned int where the width is stored.
|
||||||
|
* @param height_ret pointer to unsigned int where the height is stored.
|
||||||
|
*/
|
||||||
|
int xdo_get_window_size(const xdo_t *xdo, Window wid, unsigned int *width_ret,
|
||||||
|
unsigned int *height_ret);
|
||||||
|
|
||||||
|
/* pager-like behaviors */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently-active window.
|
||||||
|
* Requires your window manager to support this.
|
||||||
|
* Uses _NET_ACTIVE_WINDOW from the EWMH spec.
|
||||||
|
*
|
||||||
|
* @param window_ret Pointer to Window where the active window is stored.
|
||||||
|
*/
|
||||||
|
int xdo_get_active_window(const xdo_t *xdo, Window *window_ret);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a window ID by clicking on it. This function blocks until a selection
|
||||||
|
* is made.
|
||||||
|
*
|
||||||
|
* @param window_ret Pointer to Window where the selected window is stored.
|
||||||
|
*/
|
||||||
|
int xdo_select_window_with_click(const xdo_t *xdo, Window *window_ret);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the number of desktops.
|
||||||
|
* Uses _NET_NUMBER_OF_DESKTOPS of the EWMH spec.
|
||||||
|
*
|
||||||
|
* @param ndesktops the new number of desktops to set.
|
||||||
|
*/
|
||||||
|
int xdo_set_number_of_desktops(const xdo_t *xdo, long ndesktops);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current number of desktops.
|
||||||
|
* Uses _NET_NUMBER_OF_DESKTOPS of the EWMH spec.
|
||||||
|
*
|
||||||
|
* @param ndesktops pointer to long where the current number of desktops is
|
||||||
|
* stored
|
||||||
|
*/
|
||||||
|
int xdo_get_number_of_desktops(const xdo_t *xdo, long *ndesktops);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to another desktop.
|
||||||
|
* Uses _NET_CURRENT_DESKTOP of the EWMH spec.
|
||||||
|
*
|
||||||
|
* @param desktop The desktop number to switch to.
|
||||||
|
*/
|
||||||
|
int xdo_set_current_desktop(const xdo_t *xdo, long desktop);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current desktop.
|
||||||
|
* Uses _NET_CURRENT_DESKTOP of the EWMH spec.
|
||||||
|
*
|
||||||
|
* @param desktop pointer to long where the current desktop number is stored.
|
||||||
|
*/
|
||||||
|
int xdo_get_current_desktop(const xdo_t *xdo, long *desktop);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a window to another desktop
|
||||||
|
* Uses _NET_WM_DESKTOP of the EWMH spec.
|
||||||
|
*
|
||||||
|
* @param wid the window to move
|
||||||
|
* @param desktop the desktop destination for the window
|
||||||
|
*/
|
||||||
|
int xdo_set_desktop_for_window(const xdo_t *xdo, Window wid, long desktop);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the desktop a window is on.
|
||||||
|
* Uses _NET_WM_DESKTOP of the EWMH spec.
|
||||||
|
*
|
||||||
|
* If your desktop does not support _NET_WM_DESKTOP, then '*desktop' remains
|
||||||
|
* unmodified.
|
||||||
|
*
|
||||||
|
* @param wid the window to query
|
||||||
|
* @param deskto pointer to long where the desktop of the window is stored
|
||||||
|
*/
|
||||||
|
int xdo_get_desktop_for_window(const xdo_t *xdo, Window wid, long *desktop);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for windows.
|
||||||
|
*
|
||||||
|
* @param search the search query.
|
||||||
|
* @param windowlist_ret the list of matching windows to return
|
||||||
|
* @param nwindows_ret the number of windows (length of windowlist_ret)
|
||||||
|
* @see xdo_search_t
|
||||||
|
*/
|
||||||
|
int xdo_search_windows(const xdo_t *xdo, const xdo_search_t *search,
|
||||||
|
Window **windowlist_ret, unsigned int *nwindows_ret);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic property fetch.
|
||||||
|
*
|
||||||
|
* @param window the window to query
|
||||||
|
* @param atom the Atom to request
|
||||||
|
* @param nitems the number of items
|
||||||
|
* @param type the type of the return
|
||||||
|
* @param size the size of the type
|
||||||
|
* @return data consisting of 'nitems' items of size 'size' and type 'type'
|
||||||
|
* will need to be cast to the type before using.
|
||||||
|
*/
|
||||||
|
unsigned char *xdo_get_window_property_by_atom(const xdo_t *xdo, Window window, Atom atom,
|
||||||
|
long *nitems, Atom *type, int *size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get property of window by name of atom.
|
||||||
|
*
|
||||||
|
* @param window the window to query
|
||||||
|
* @param property the name of the atom
|
||||||
|
* @param nitems the number of items
|
||||||
|
* @param type the type of the return
|
||||||
|
* @param size the size of the type
|
||||||
|
* @return data consisting of 'nitems' items of size 'size' and type 'type'
|
||||||
|
* will need to be cast to the type before using.
|
||||||
|
*/
|
||||||
|
int xdo_get_window_property(const xdo_t *xdo, Window window, const char *property,
|
||||||
|
unsigned char **value, long *nitems, Atom *type, int *size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current input state. This is a mask value containing any of the
|
||||||
|
* following: ShiftMask, LockMask, ControlMask, Mod1Mask, Mod2Mask, Mod3Mask,
|
||||||
|
* Mod4Mask, or Mod5Mask.
|
||||||
|
*
|
||||||
|
* @return the input mask
|
||||||
|
*/
|
||||||
|
unsigned int xdo_get_input_state(const xdo_t *xdo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If you need the symbol map, use this method.
|
||||||
|
*
|
||||||
|
* The symbol map is an array of string pairs mapping common tokens to X Keysym
|
||||||
|
* strings, such as "alt" to "Alt_L"
|
||||||
|
*
|
||||||
|
* @returns array of strings.
|
||||||
|
*/
|
||||||
|
const char **xdo_get_symbol_map(void);
|
||||||
|
|
||||||
|
/* active modifiers stuff */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of active keys. Uses XQueryKeymap.
|
||||||
|
*
|
||||||
|
* @param keys Pointer to the array of charcodemap_t that will be allocated
|
||||||
|
* by this function.
|
||||||
|
* @param nkeys Pointer to integer where the number of keys will be stored.
|
||||||
|
*/
|
||||||
|
int xdo_get_active_modifiers(const xdo_t *xdo, charcodemap_t **keys,
|
||||||
|
int *nkeys);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send any events necessary to clear the active modifiers.
|
||||||
|
* For example, if you are holding 'alt' when xdo_get_active_modifiers is
|
||||||
|
* called, then this method will send a key-up for 'alt'
|
||||||
|
*/
|
||||||
|
int xdo_clear_active_modifiers(const xdo_t *xdo, Window window,
|
||||||
|
charcodemap_t *active_mods,
|
||||||
|
int active_mods_n);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send any events necessary to make these modifiers active.
|
||||||
|
* This is useful if you just cleared the active modifiers and then wish
|
||||||
|
* to restore them after.
|
||||||
|
*/
|
||||||
|
int xdo_set_active_modifiers(const xdo_t *xdo, Window window,
|
||||||
|
charcodemap_t *active_mods,
|
||||||
|
int active_mods_n);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the position of the current viewport.
|
||||||
|
*
|
||||||
|
* This is only relevant if your window manager supports
|
||||||
|
* _NET_DESKTOP_VIEWPORT
|
||||||
|
*/
|
||||||
|
int xdo_get_desktop_viewport(const xdo_t *xdo, int *x_ret, int *y_ret);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the position of the current viewport.
|
||||||
|
*
|
||||||
|
* This is only relevant if your window manager supports
|
||||||
|
* _NET_DESKTOP_VIEWPORT
|
||||||
|
*/
|
||||||
|
int xdo_set_desktop_viewport(const xdo_t *xdo, int x, int y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kill a window and the client owning it.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
int xdo_kill_window(const xdo_t *xdo, Window window);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a window without trying to kill the client.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
int xdo_close_window(const xdo_t *xdo, Window window);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request that a window close, gracefully.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
int xdo_quit_window(const xdo_t *xdo, Window window);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a client window that is a parent of the window given
|
||||||
|
*/
|
||||||
|
#define XDO_FIND_PARENTS (0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a client window that is a child of the window given
|
||||||
|
*/
|
||||||
|
#define XDO_FIND_CHILDREN (1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a client window (child) in a given window. Useful if you get the
|
||||||
|
* window manager's decorator window rather than the client window.
|
||||||
|
*/
|
||||||
|
int xdo_find_window_client(const xdo_t *xdo, Window window, Window *window_ret,
|
||||||
|
int direction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a window's name, if any.
|
||||||
|
*
|
||||||
|
* @param window window to get the name of.
|
||||||
|
* @param name_ret character pointer pointer where the address of the window name will be stored.
|
||||||
|
* @param name_len_ret integer pointer where the length of the window name will be stored.
|
||||||
|
* @param name_type integer pointer where the type (atom) of the window name will be stored.
|
||||||
|
*/
|
||||||
|
int xdo_get_window_name(const xdo_t *xdo, Window window,
|
||||||
|
unsigned char **name_ret, int *name_len_ret,
|
||||||
|
int *name_type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable an xdo feature.
|
||||||
|
*
|
||||||
|
* This function is mainly used by libxdo itself, however, you may find it useful
|
||||||
|
* in your own applications.
|
||||||
|
*
|
||||||
|
* @see XDO_FEATURES
|
||||||
|
*/
|
||||||
|
void xdo_disable_feature(xdo_t *xdo, int feature);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable an xdo feature.
|
||||||
|
*
|
||||||
|
* This function is mainly used by libxdo itself, however, you may find it useful
|
||||||
|
* in your own applications.
|
||||||
|
*
|
||||||
|
* @see XDO_FEATURES
|
||||||
|
*/
|
||||||
|
void xdo_enable_feature(xdo_t *xdo, int feature);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a feature is enabled.
|
||||||
|
*
|
||||||
|
* This function is mainly used by libxdo itself, however, you may find it useful
|
||||||
|
* in your own applications.
|
||||||
|
*
|
||||||
|
* @see XDO_FEATURES
|
||||||
|
*/
|
||||||
|
int xdo_has_feature(xdo_t *xdo, int feature);
|
||||||
|
|
||||||
|
// Espanso-specific variants
|
||||||
|
|
||||||
|
KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key);
|
||||||
|
void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key);
|
||||||
|
void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym);
|
||||||
|
void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk);
|
||||||
|
void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key,
|
||||||
|
int modstate, int is_press, useconds_t delay);
|
||||||
|
int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay);
|
||||||
|
void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed);
|
||||||
|
int fast_send_keysequence_window(const xdo_t *xdo, Window window,
|
||||||
|
const char *keysequence, useconds_t delay);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
} /* extern "C" */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* ifndef _XDO_H_ */
|
||||||
|
|
24
espanso-inject/src/x11/xdotool/vendor/xdo_util.h
vendored
Normal file
24
espanso-inject/src/x11/xdotool/vendor/xdo_util.h
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/* xdo utility pieces
|
||||||
|
*
|
||||||
|
* $Id$
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _XDO_UTIL_H_
|
||||||
|
#define _XDO_UTIL_H_
|
||||||
|
|
||||||
|
#include "xdo.h"
|
||||||
|
|
||||||
|
/* human to Keysym string mapping */
|
||||||
|
static const char *symbol_map[] = {
|
||||||
|
"alt", "Alt_L",
|
||||||
|
"ctrl", "Control_L",
|
||||||
|
"control", "Control_L",
|
||||||
|
"meta", "Meta_L",
|
||||||
|
"super", "Super_L",
|
||||||
|
"shift", "Shift_L",
|
||||||
|
"enter", "Return",
|
||||||
|
"return", "Return",
|
||||||
|
NULL, NULL,
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* ifndef _XDO_UTIL_H_ */
|
|
@ -143,6 +143,7 @@ impl<'a> super::engine::dispatch::executor::clipboard_injector::ClipboardParamsP
|
||||||
restore_clipboard: active.preserve_clipboard(),
|
restore_clipboard: active.preserve_clipboard(),
|
||||||
restore_clipboard_delay: active.restore_clipboard_delay(),
|
restore_clipboard_delay: active.restore_clipboard_delay(),
|
||||||
x11_use_xclip_backend: active.x11_use_xclip_backend(),
|
x11_use_xclip_backend: active.x11_use_xclip_backend(),
|
||||||
|
x11_use_xdotool_backend: active.x11_use_xdotool_backend(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,6 +165,7 @@ impl<'a> super::engine::dispatch::executor::InjectParamsProvider for ConfigManag
|
||||||
inject_delay: active.inject_delay(),
|
inject_delay: active.inject_delay(),
|
||||||
key_delay: active.key_delay(),
|
key_delay: active.key_delay(),
|
||||||
evdev_modifier_delay: active.evdev_modifier_delay(),
|
evdev_modifier_delay: active.evdev_modifier_delay(),
|
||||||
|
x11_use_xdotool_backend: active.x11_use_xdotool_backend(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ pub struct ClipboardParams {
|
||||||
pub restore_clipboard: bool,
|
pub restore_clipboard: bool,
|
||||||
pub restore_clipboard_delay: usize,
|
pub restore_clipboard_delay: usize,
|
||||||
pub x11_use_xclip_backend: bool,
|
pub x11_use_xclip_backend: bool,
|
||||||
|
pub x11_use_xdotool_backend: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ClipboardInjectorAdapter<'a> {
|
pub struct ClipboardInjectorAdapter<'a> {
|
||||||
|
@ -95,6 +96,7 @@ impl<'a> ClipboardInjectorAdapter<'a> {
|
||||||
InjectionOptions {
|
InjectionOptions {
|
||||||
delay: params.paste_shortcut_event_delay as i32,
|
delay: params.paste_shortcut_event_delay as i32,
|
||||||
disable_fast_inject: params.disable_x11_fast_inject,
|
disable_fast_inject: params.disable_x11_fast_inject,
|
||||||
|
x11_use_xdotool_fallback: params.x11_use_xdotool_backend,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
|
@ -67,6 +67,7 @@ impl<'a> TextInjector for EventInjectorAdapter<'a> {
|
||||||
})
|
})
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
x11_use_xdotool_fallback: params.x11_use_xdotool_backend,
|
||||||
};
|
};
|
||||||
|
|
||||||
// We don't use the lines() method because it skips emtpy lines, which is not what we want.
|
// We don't use the lines() method because it skips emtpy lines, which is not what we want.
|
||||||
|
|
|
@ -59,6 +59,7 @@ impl<'a> KeyInjector for KeyInjectorAdapter<'a> {
|
||||||
})
|
})
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
x11_use_xdotool_fallback: params.x11_use_xdotool_backend,
|
||||||
};
|
};
|
||||||
|
|
||||||
let converted_keys: Vec<_> = keys.iter().map(convert_to_inject_key).collect();
|
let converted_keys: Vec<_> = keys.iter().map(convert_to_inject_key).collect();
|
||||||
|
|
|
@ -34,4 +34,5 @@ pub struct InjectParams {
|
||||||
pub key_delay: Option<usize>,
|
pub key_delay: Option<usize>,
|
||||||
pub disable_x11_fast_inject: bool,
|
pub disable_x11_fast_inject: bool,
|
||||||
pub evdev_modifier_delay: Option<usize>,
|
pub evdev_modifier_delay: Option<usize>,
|
||||||
|
pub x11_use_xdotool_backend: bool,
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,5 +53,6 @@ generate_patchable_config!(
|
||||||
win32_exclude_orphan_events -> bool,
|
win32_exclude_orphan_events -> bool,
|
||||||
win32_keyboard_layout_cache_interval -> i64,
|
win32_keyboard_layout_cache_interval -> i64,
|
||||||
x11_use_xclip_backend -> bool,
|
x11_use_xclip_backend -> bool,
|
||||||
|
x11_use_xdotool_backend -> bool,
|
||||||
keyboard_layout -> Option<RMLVOConfig>
|
keyboard_layout -> Option<RMLVOConfig>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user