From f30395b8a64f79137cee6355e845f1e442b46146 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 12 Apr 2022 22:08:06 +0200 Subject: [PATCH] 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 --- espanso-config/src/config/mod.rs | 6 + espanso-config/src/config/parse/mod.rs | 1 + espanso-config/src/config/parse/yaml.rs | 6 + espanso-config/src/config/resolve.rs | 5 + espanso-config/src/legacy/mod.rs | 4 + espanso-inject/build.rs | 11 + espanso-inject/src/lib.rs | 9 +- espanso-inject/src/x11/README.md | 53 - espanso-inject/src/x11/default/mod.rs | 598 +++++ espanso-inject/src/x11/ffi.rs | 4 +- espanso-inject/src/x11/mod.rs | 629 +---- espanso-inject/src/x11/xdotool/README.md | 2 + espanso-inject/src/x11/xdotool/ffi.rs | 61 + espanso-inject/src/x11/xdotool/mod.rs | 348 +++ .../src/x11/xdotool/vendor/COPYRIGHT | 24 + espanso-inject/src/x11/xdotool/vendor/xdo.c | 2266 +++++++++++++++++ espanso-inject/src/x11/xdotool/vendor/xdo.h | 938 +++++++ .../src/x11/xdotool/vendor/xdo_util.h | 24 + espanso/src/cli/worker/config.rs | 2 + .../dispatch/executor/clipboard_injector.rs | 2 + .../dispatch/executor/event_injector.rs | 1 + .../engine/dispatch/executor/key_injector.rs | 1 + .../worker/engine/dispatch/executor/mod.rs | 1 + espanso/src/patch/patches/mod.rs | 1 + 24 files changed, 4379 insertions(+), 618 deletions(-) delete mode 100644 espanso-inject/src/x11/README.md create mode 100644 espanso-inject/src/x11/default/mod.rs create mode 100644 espanso-inject/src/x11/xdotool/README.md create mode 100644 espanso-inject/src/x11/xdotool/ffi.rs create mode 100644 espanso-inject/src/x11/xdotool/mod.rs create mode 100644 espanso-inject/src/x11/xdotool/vendor/COPYRIGHT create mode 100644 espanso-inject/src/x11/xdotool/vendor/xdo.c create mode 100644 espanso-inject/src/x11/xdotool/vendor/xdo.h create mode 100644 espanso-inject/src/x11/xdotool/vendor/xdo_util.h diff --git a/espanso-config/src/config/mod.rs b/espanso-config/src/config/mod.rs index 945841e..bab51d9 100644 --- a/espanso-config/src/config/mod.rs +++ b/espanso-config/src/config/mod.rs @@ -166,6 +166,10 @@ pub trait Config: Send + Sync { // the built-in native module on X11. 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. // 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. @@ -212,6 +216,7 @@ pub trait Config: Send + Sync { secure_input_notification: {:?} x11_use_xclip_backend: {:?} + x11_use_xdotool_backend: {:?} win32_exclude_orphan_events: {:?} win32_keyboard_layout_cache_interval: {:?} @@ -246,6 +251,7 @@ pub trait Config: Send + Sync { self.secure_input_notification(), self.x11_use_xclip_backend(), + self.x11_use_xdotool_backend(), self.win32_exclude_orphan_events(), self.win32_keyboard_layout_cache_interval(), diff --git a/espanso-config/src/config/parse/mod.rs b/espanso-config/src/config/parse/mod.rs index 644b068..6f1d0c4 100644 --- a/espanso-config/src/config/parse/mod.rs +++ b/espanso-config/src/config/parse/mod.rs @@ -49,6 +49,7 @@ pub(crate) struct ParsedConfig { pub win32_exclude_orphan_events: Option, pub win32_keyboard_layout_cache_interval: Option, pub x11_use_xclip_backend: Option, + pub x11_use_xdotool_backend: Option, pub pre_paste_delay: Option, pub restore_clipboard_delay: Option, diff --git a/espanso-config/src/config/parse/yaml.rs b/espanso-config/src/config/parse/yaml.rs index 64558db..66605ad 100644 --- a/espanso-config/src/config/parse/yaml.rs +++ b/espanso-config/src/config/parse/yaml.rs @@ -121,6 +121,9 @@ pub(crate) struct YAMLConfig { #[serde(default)] pub x11_use_xclip_backend: Option, + #[serde(default)] + pub x11_use_xdotool_backend: Option, + // Include/Exclude #[serde(default)] pub includes: Option>, @@ -213,6 +216,7 @@ impl TryFrom for ParsedConfig { win32_exclude_orphan_events: yaml_config.win32_exclude_orphan_events, win32_keyboard_layout_cache_interval: yaml_config.win32_keyboard_layout_cache_interval, 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, includes: yaml_config.includes, @@ -273,6 +277,7 @@ mod tests { win32_exclude_orphan_events: false win32_keyboard_layout_cache_interval: 300 x11_use_xclip_backend: true + x11_use_xdotool_backend: true use_standard_includes: true includes: ["test1"] @@ -329,6 +334,7 @@ mod tests { win32_exclude_orphan_events: Some(false), win32_keyboard_layout_cache_interval: Some(300), x11_use_xclip_backend: Some(true), + x11_use_xdotool_backend: Some(true), pre_paste_delay: Some(300), evdev_modifier_delay: Some(40), diff --git a/espanso-config/src/config/resolve.rs b/espanso-config/src/config/resolve.rs index 7b7dcc3..352500c 100644 --- a/espanso-config/src/config/resolve.rs +++ b/espanso-config/src/config/resolve.rs @@ -331,6 +331,10 @@ impl Config for ResolvedConfig { fn x11_use_xclip_backend(&self) -> bool { 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 { @@ -417,6 +421,7 @@ impl ResolvedConfig { win32_exclude_orphan_events, win32_keyboard_layout_cache_interval, x11_use_xclip_backend, + x11_use_xdotool_backend, includes, excludes, extra_includes, diff --git a/espanso-config/src/legacy/mod.rs b/espanso-config/src/legacy/mod.rs index 24a94aa..3cdd8d7 100644 --- a/espanso-config/src/legacy/mod.rs +++ b/espanso-config/src/legacy/mod.rs @@ -410,6 +410,10 @@ impl Config for LegacyInteropConfig { fn x11_use_xclip_backend(&self) -> bool { false } + + fn x11_use_xdotool_backend(&self) -> bool { + false + } } struct LegacyMatchGroup { diff --git a/espanso-inject/build.rs b/espanso-inject/build.rs index 702678d..747f756 100644 --- a/espanso-inject/build.rs +++ b/espanso-inject/build.rs @@ -37,6 +37,9 @@ fn cc_config() { fn cc_config() { println!("cargo:rerun-if-changed=src/evdev/native.h"); 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() .include("src/evdev") .file("src/evdev/native.c") @@ -47,6 +50,14 @@ fn cc_config() { println!("cargo:rustc-link-lib=dylib=xkbcommon"); 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=Xtst"); } diff --git a/espanso-inject/src/lib.rs b/espanso-inject/src/lib.rs index ebc086e..e07ec7d 100644 --- a/espanso-inject/src/lib.rs +++ b/espanso-inject/src/lib.rs @@ -61,6 +61,10 @@ pub struct InjectionOptions { // Used to set a modifier-specific delay. // NOTE: Only relevant on Wayland systems. 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 { @@ -84,6 +88,7 @@ impl Default for InjectionOptions { delay: default_delay, disable_fast_inject: false, evdev_modifier_delay: 10, + x11_use_xdotool_fallback: false, } } } @@ -148,8 +153,8 @@ pub fn get_injector(options: InjectorCreationOptions) -> Result -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -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 \ No newline at end of file diff --git a/espanso-inject/src/x11/default/mod.rs b/espanso-inject/src/x11/default/mod.rs new file mode 100644 index 0000000..2501f76 --- /dev/null +++ b/espanso-inject/src/x11/default/mod.rs @@ -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 . + */ + +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, +} + +type CharMap = HashMap; +type SymMap = HashMap; + +pub struct X11DefaultInjector { + display: *mut Display, + + char_map: CharMap, + sym_map: SymMap, +} + +#[allow(clippy::new_without_default)] +impl X11DefaultInjector { + pub fn new() -> Result { + // 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>> { + let mut deadkeys = vec![None]; + let mut seen_keysyms: HashSet = 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> { + 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> { + 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 { + 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> = 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), +} diff --git a/espanso-inject/src/x11/ffi.rs b/espanso-inject/src/x11/ffi.rs index e456b3a..c5a6cf6 100644 --- a/espanso-inject/src/x11/ffi.rs +++ b/espanso-inject/src/x11/ffi.rs @@ -6,7 +6,7 @@ use std::{ os::raw::{c_char, c_long, c_uint, c_ulong}, }; -use libc::c_int; +use libc::{c_int, c_uchar}; pub enum Display {} pub type Window = u64; @@ -123,4 +123,6 @@ extern "C" { pub fn XFilterEvent(event: *mut XKeyEvent, window: c_ulong) -> c_int; pub fn XCloseIM(input_method: XIM) -> c_int; pub fn XFree(data: *mut c_void) -> c_int; + pub fn XKeycodeToKeysym(display: *mut Display, keycode: c_uchar, index: c_int) -> c_ulong; + pub fn XKeysymToString(keysym: c_ulong) -> *mut c_char; } diff --git a/espanso-inject/src/x11/mod.rs b/espanso-inject/src/x11/mod.rs index 84d4a6c..c0a0317 100644 --- a/espanso-inject/src/x11/mod.rs +++ b/espanso-inject/src/x11/mod.rs @@ -17,584 +17,89 @@ * along with espanso. If not, see . */ +use crate::Injector; + +use anyhow::{bail, ensure, Result}; +use log::{error, warn}; + +mod default; mod ffi; +mod xdotool; -use std::{ - collections::{HashMap, HashSet}, - ffi::{CStr, CString}, - 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, +pub struct X11ProxyInjector { + default_injector: Option, + xdotool_injector: Option, } -#[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, -} - -type CharMap = HashMap; -type SymMap = HashMap; - -pub struct X11Injector { - display: *mut Display, - - char_map: CharMap, - sym_map: SymMap, -} - -#[allow(clippy::new_without_default)] -impl X11Injector { +impl X11ProxyInjector { pub fn new() -> Result { - // Necessary to properly handle non-ascii chars - let empty_string = CString::new("")?; - unsafe { - libc::setlocale(libc::LC_ALL, empty_string.as_ptr()); + let default_injector = match default::X11DefaultInjector::new() { + Ok(injector) => Some(injector), + Err(err) => { + error!("X11DefaultInjector could not be initialized: {:?}", err); + warn!("falling back to xdotool injector"); + None + } + }; + + let xdotool_injector = match xdotool::X11XDOToolInjector::new() { + Ok(injector) => Some(injector), + Err(err) => { + error!("X11XDOToolInjector could not be initialized: {:?}", err); + None + } + }; + + if default_injector.is_none() && xdotool_injector.is_none() { + bail!("unable to initialize injectors, neither the default or xdotool fallback could be initialized"); } - 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, + Ok(X11ProxyInjector { + default_injector, + xdotool_injector, }) } - fn generate_maps(display: *mut Display) -> Result<(CharMap, SymMap)> { - debug!("generating key maps"); + fn get_active_injector(&self, options: &crate::InjectionOptions) -> Result<&dyn Injector> { + ensure!( + self.default_injector.is_some() || self.xdotool_injector.is_some(), + "unable to get active injector, neither default or xdotool fallback are available." + ); - 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) }; - } + if options.x11_use_xdotool_fallback { + if let Some(xdotool_injector) = self.xdotool_injector.as_ref() { + return Ok(xdotool_injector); + } else if let Some(default_injector) = self.default_injector.as_ref() { + return Ok(default_injector); } + } else if let Some(default_injector) = self.default_injector.as_ref() { + return Ok(default_injector); + } else if let Some(xdotool_injector) = self.xdotool_injector.as_ref() { + return Ok(xdotool_injector); } - 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>> { - let mut deadkeys = vec![None]; - let mut seen_keysyms: HashSet = 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> { - 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> { - 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 { - 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); - } - } - } - } - } + unreachable!() } } -impl Drop for X11Injector { - fn drop(&mut self) { - unsafe { - XCloseDisplay(self.display); - } +impl Injector for X11ProxyInjector { + fn send_string(&self, string: &str, options: crate::InjectionOptions) -> Result<()> { + self + .get_active_injector(&options)? + .send_string(string, options) + } + + fn send_keys(&self, keys: &[crate::keys::Key], options: crate::InjectionOptions) -> Result<()> { + self.get_active_injector(&options)?.send_keys(keys, options) + } + + fn send_key_combination( + &self, + keys: &[crate::keys::Key], + options: crate::InjectionOptions, + ) -> Result<()> { + self + .get_active_injector(&options)? + .send_key_combination(keys, options) } } - -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> = 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), -} diff --git a/espanso-inject/src/x11/xdotool/README.md b/espanso-inject/src/x11/xdotool/README.md new file mode 100644 index 0000000..a460e28 --- /dev/null +++ b/espanso-inject/src/x11/xdotool/README.md @@ -0,0 +1,2 @@ +This is a fallback injection module that relies on the awesome xdotool project: +https://github.com/jordansissel/xdotool \ No newline at end of file diff --git a/espanso-inject/src/x11/xdotool/ffi.rs b/espanso-inject/src/x11/xdotool/ffi.rs new file mode 100644 index 0000000..c3f8f17 --- /dev/null +++ b/espanso-inject/src/x11/xdotool/ffi.rs @@ -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, + ); +} diff --git a/espanso-inject/src/x11/xdotool/mod.rs b/espanso-inject/src/x11/xdotool/mod.rs new file mode 100644 index 0000000..726b053 --- /dev/null +++ b/espanso-inject/src/x11/xdotool/mod.rs @@ -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 . + */ + +use std::{ + convert::TryInto, + ffi::{CStr, CString}, +}; + +use crate::Injector; +use anyhow::{bail, Context, Result}; +use log::debug; + +mod ffi; +use self::ffi::{fast_send_keysequence_window, xdo_send_keysequence_window, xdo_t, CURRENTWINDOW}; + +use super::ffi::{ + Display, Window, XGetInputFocus, XKeycodeToKeysym, XKeysymToString, XQueryKeymap, + XTestFakeKeyEvent, +}; + +pub struct X11XDOToolInjector { + xdo: *const xdo_t, +} + +impl X11XDOToolInjector { + pub fn new() -> Result { + let xdo = unsafe { ffi::xdo_new(std::ptr::null()) }; + if xdo.is_null() { + bail!("unable to initialize xdo_t instance"); + } + + debug!("initialized xdo_t object"); + + Ok(Self { xdo }) + } + + fn xfake_release_all_keys(&self) { + let mut keys: [u8; 32] = [0; 32]; + unsafe { + XQueryKeymap((*self.xdo).xdpy, keys.as_mut_ptr()); + } + + #[allow(clippy::needless_range_loop)] + for i in 0..32 { + // Only those that are pressed should be changed + if keys[i] != 0 { + for k in 0..8 { + if (keys[i] & (1 << k)) != 0 { + let key_code = i * 8 + k; + unsafe { + XTestFakeKeyEvent((*self.xdo).xdpy, key_code as u32, 0, 0); + } + } + } + } + } + } + + fn get_focused_window(&self) -> Window { + let mut focused_window: Window = 0; + let mut revert_to = 0; + unsafe { + XGetInputFocus((*self.xdo).xdpy, &mut focused_window, &mut revert_to); + } + focused_window + } + + fn xfake_send_string( + &self, + string: &str, + options: crate::InjectionOptions, + ) -> anyhow::Result<()> { + // It may happen that when an expansion is triggered, some keys are still pressed. + // This causes a problem if the expanded match contains that character, as the injection + // will not be able to register that keypress (as it is already pressed). + // To solve the problem, before an expansion we get which keys are currently pressed + // and inject a key_release event so that they can be further registered. + self.xfake_release_all_keys(); + + let c_string = CString::new(string).context("unable to create CString")?; + let delay = options.delay * 1000; + + unsafe { + ffi::xdo_enter_text_window( + self.xdo, + CURRENTWINDOW, + c_string.as_ptr(), + delay.try_into().unwrap(), + ); + } + + Ok(()) + } + + fn fast_release_all_keys(&self) { + let mut keys: [u8; 32] = [0; 32]; + unsafe { + XQueryKeymap((*self.xdo).xdpy, keys.as_mut_ptr()); + } + + let focused_window = self.get_focused_window(); + + #[allow(clippy::needless_range_loop)] + for i in 0..32 { + // Only those that are pressed should be changed + if keys[i] != 0 { + for k in 0..8 { + if (keys[i] & (1 << k)) != 0 { + let key_code = i * 8 + k; + unsafe { + ffi::fast_send_event(self.xdo, focused_window, key_code.try_into().unwrap(), 0); + } + } + } + } + } + } + + fn fast_send_string(&self, string: &str, options: crate::InjectionOptions) -> anyhow::Result<()> { + // It may happen that when an expansion is triggered, some keys are still pressed. + // This causes a problem if the expanded match contains that character, as the injection + // will not be able to register that keypress (as it is already pressed). + // To solve the problem, before an expansion we get which keys are currently pressed + // and inject a key_release event so that they can be further registered. + self.fast_release_all_keys(); + + let c_string = CString::new(string).context("unable to create CString")?; + let delay = options.delay * 1000; + + unsafe { + ffi::fast_enter_text_window( + self.xdo, + self.get_focused_window(), + c_string.as_ptr(), + delay.try_into().unwrap(), + ); + } + + Ok(()) + } +} + +impl Injector for X11XDOToolInjector { + fn send_string(&self, string: &str, options: crate::InjectionOptions) -> anyhow::Result<()> { + if options.disable_fast_inject { + self.xfake_send_string(string, options) + } else { + self.fast_send_string(string, options) + } + } + + fn send_keys( + &self, + keys: &[crate::keys::Key], + options: crate::InjectionOptions, + ) -> anyhow::Result<()> { + let key_syms: Vec = keys + .iter() + .filter_map(|key| unsafe { convert_key_to_keysym((*self.xdo).xdpy, key) }) + .collect(); + + let delay = options.delay * 1000; + + for key in key_syms { + let c_str = CString::new(key).context("unable to generate CString")?; + + if options.disable_fast_inject { + unsafe { + xdo_send_keysequence_window( + self.xdo, + CURRENTWINDOW, + c_str.as_ptr(), + delay.try_into().unwrap(), + ); + } + } else { + unsafe { + fast_send_keysequence_window( + self.xdo, + self.get_focused_window(), + c_str.as_ptr(), + delay.try_into().unwrap(), + ); + } + } + } + + Ok(()) + } + + fn send_key_combination( + &self, + keys: &[crate::keys::Key], + options: crate::InjectionOptions, + ) -> anyhow::Result<()> { + let key_syms: Vec = keys + .iter() + .filter_map(|key| unsafe { convert_key_to_keysym((*self.xdo).xdpy, key) }) + .collect(); + let key_combination = key_syms.join("+"); + + let delay = options.delay * 1000; + + let c_key_combination = CString::new(key_combination).context("unable to generate CString")?; + + if options.disable_fast_inject { + unsafe { + xdo_send_keysequence_window( + self.xdo, + CURRENTWINDOW, + c_key_combination.as_ptr(), + delay.try_into().unwrap(), + ); + } + } else { + unsafe { + fast_send_keysequence_window( + self.xdo, + self.get_focused_window(), + c_key_combination.as_ptr(), + delay.try_into().unwrap(), + ); + } + } + + Ok(()) + } +} + +impl Drop for X11XDOToolInjector { + fn drop(&mut self) { + unsafe { ffi::xdo_free(self.xdo) } + } +} + +fn convert_key_to_keysym(display: *mut Display, key: &crate::keys::Key) -> Option { + match key { + crate::keys::Key::Alt => Some("Alt_L".to_string()), + crate::keys::Key::CapsLock => Some("Caps_Lock".to_string()), + crate::keys::Key::Control => Some("Control_L".to_string()), + crate::keys::Key::Meta => Some("Meta_L".to_string()), + crate::keys::Key::NumLock => Some("Num_Lock".to_string()), + crate::keys::Key::Shift => Some("Shift_L".to_string()), + crate::keys::Key::Enter => Some("Return".to_string()), + crate::keys::Key::Tab => Some("Tab".to_string()), + crate::keys::Key::Space => Some("space".to_string()), + crate::keys::Key::ArrowDown => Some("downarrow".to_string()), + crate::keys::Key::ArrowLeft => Some("leftarrow".to_string()), + crate::keys::Key::ArrowRight => Some("rightarrow".to_string()), + crate::keys::Key::ArrowUp => Some("uparrow".to_string()), + crate::keys::Key::End => Some("End".to_string()), + crate::keys::Key::Home => Some("Home".to_string()), + crate::keys::Key::PageDown => Some("Page_Down".to_string()), + crate::keys::Key::PageUp => Some("Page_Up".to_string()), + crate::keys::Key::Escape => Some("Escape".to_string()), + crate::keys::Key::Backspace => Some("BackSpace".to_string()), + crate::keys::Key::Insert => Some("Insert".to_string()), + crate::keys::Key::Delete => Some("Delete".to_string()), + crate::keys::Key::F1 => Some("F1".to_string()), + crate::keys::Key::F2 => Some("F2".to_string()), + crate::keys::Key::F3 => Some("F3".to_string()), + crate::keys::Key::F4 => Some("F4".to_string()), + crate::keys::Key::F5 => Some("F5".to_string()), + crate::keys::Key::F6 => Some("F6".to_string()), + crate::keys::Key::F7 => Some("F7".to_string()), + crate::keys::Key::F8 => Some("F8".to_string()), + crate::keys::Key::F9 => Some("F9".to_string()), + crate::keys::Key::F10 => Some("F10".to_string()), + crate::keys::Key::F11 => Some("F11".to_string()), + crate::keys::Key::F12 => Some("F12".to_string()), + crate::keys::Key::F13 => Some("F13".to_string()), + crate::keys::Key::F14 => Some("F14".to_string()), + crate::keys::Key::F15 => Some("F15".to_string()), + crate::keys::Key::F16 => Some("F16".to_string()), + crate::keys::Key::F17 => Some("F17".to_string()), + crate::keys::Key::F18 => Some("F18".to_string()), + crate::keys::Key::F19 => Some("F19".to_string()), + crate::keys::Key::F20 => Some("F20".to_string()), + crate::keys::Key::A => Some("a".to_string()), + crate::keys::Key::B => Some("b".to_string()), + crate::keys::Key::C => Some("c".to_string()), + crate::keys::Key::D => Some("d".to_string()), + crate::keys::Key::E => Some("e".to_string()), + crate::keys::Key::F => Some("f".to_string()), + crate::keys::Key::G => Some("g".to_string()), + crate::keys::Key::H => Some("h".to_string()), + crate::keys::Key::I => Some("i".to_string()), + crate::keys::Key::J => Some("j".to_string()), + crate::keys::Key::K => Some("k".to_string()), + crate::keys::Key::L => Some("l".to_string()), + crate::keys::Key::M => Some("m".to_string()), + crate::keys::Key::N => Some("n".to_string()), + crate::keys::Key::O => Some("o".to_string()), + crate::keys::Key::P => Some("p".to_string()), + crate::keys::Key::Q => Some("q".to_string()), + crate::keys::Key::R => Some("r".to_string()), + crate::keys::Key::S => Some("s".to_string()), + crate::keys::Key::T => Some("t".to_string()), + crate::keys::Key::U => Some("u".to_string()), + crate::keys::Key::V => Some("v".to_string()), + crate::keys::Key::W => Some("w".to_string()), + crate::keys::Key::X => Some("x".to_string()), + crate::keys::Key::Y => Some("y".to_string()), + crate::keys::Key::Z => Some("z".to_string()), + crate::keys::Key::N0 => Some("0".to_string()), + crate::keys::Key::N1 => Some("1".to_string()), + crate::keys::Key::N2 => Some("2".to_string()), + crate::keys::Key::N3 => Some("3".to_string()), + crate::keys::Key::N4 => Some("4".to_string()), + crate::keys::Key::N5 => Some("5".to_string()), + crate::keys::Key::N6 => Some("6".to_string()), + crate::keys::Key::N7 => Some("7".to_string()), + crate::keys::Key::N8 => Some("8".to_string()), + crate::keys::Key::N9 => Some("9".to_string()), + crate::keys::Key::Numpad0 => Some("KP_0".to_string()), + crate::keys::Key::Numpad1 => Some("KP_1".to_string()), + crate::keys::Key::Numpad2 => Some("KP_2".to_string()), + crate::keys::Key::Numpad3 => Some("KP_3".to_string()), + crate::keys::Key::Numpad4 => Some("KP_4".to_string()), + crate::keys::Key::Numpad5 => Some("KP_5".to_string()), + crate::keys::Key::Numpad6 => Some("KP_6".to_string()), + crate::keys::Key::Numpad7 => Some("KP_7".to_string()), + crate::keys::Key::Numpad8 => Some("KP_8".to_string()), + crate::keys::Key::Numpad9 => Some("KP_9".to_string()), + crate::keys::Key::Raw(key_code) => unsafe { + let key_sym = XKeycodeToKeysym(display, (*key_code).try_into().unwrap(), 0); + let string = XKeysymToString(key_sym); + let c_str = CStr::from_ptr(string); + Some(c_str.to_string_lossy().to_string()) + }, + } +} diff --git a/espanso-inject/src/x11/xdotool/vendor/COPYRIGHT b/espanso-inject/src/x11/xdotool/vendor/COPYRIGHT new file mode 100644 index 0000000..193aac5 --- /dev/null +++ b/espanso-inject/src/x11/xdotool/vendor/COPYRIGHT @@ -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. \ No newline at end of file diff --git a/espanso-inject/src/x11/xdotool/vendor/xdo.c b/espanso-inject/src/x11/xdotool/vendor/xdo.c new file mode 100644 index 0000000..6b779f5 --- /dev/null +++ b/espanso-inject/src/x11/xdotool/vendor/xdo.c @@ -0,0 +1,2266 @@ +/* xdo library + * - getwindowfocus contributed by Lee Pumphret + * - keysequence_{up,down} contributed by Magnus Boman + * + * See the following url for an explanation of how keymaps work in X11 + * http://www.in-ulm.de/~mascheck/X11/xmodmap.html + */ + +#ifndef _XOPEN_SOURCE +#define _XOPEN_SOURCE 500 +#endif /* _XOPEN_SOURCE */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "xdo.h" +#include "xdo_util.h" + +#define DEFAULT_DELAY 12 + +/** + * The number of tries to check for a wait condition before aborting. + * TODO(sissel): Make this tunable at runtime? + */ +#define MAX_TRIES 500 + +static void _xdo_populate_charcode_map(xdo_t *xdo); +static int _xdo_has_xtest(const xdo_t *xdo); + +static KeySym _xdo_keysym_from_char(const xdo_t *xdo, wchar_t key); +static void _xdo_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key); +static void _xdo_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym); +//static int _xdo_get_shiftcode_if_needed(const xdo_t *xdo, char key); + +static int _xdo_send_keysequence_window_to_keycode_list(const xdo_t *xdo, const char *keyseq, + charcodemap_t **keys, int *nkeys); +static int _xdo_send_keysequence_window_do(const xdo_t *xdo, Window window, const char *keyseq, + int pressed, int *modifier, useconds_t delay); +static int _xdo_ewmh_is_supported(const xdo_t *xdo, const char *feature); +static void _xdo_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk); +static void _xdo_send_key(const xdo_t *xdo, Window window, charcodemap_t *key, + int modstate, int is_press, useconds_t delay); +static void _xdo_send_modifier(const xdo_t *xdo, int modmask, int is_press); + +static int _xdo_query_keycode_to_modifier(XModifierKeymap *modmap, KeyCode keycode); +static int _xdo_mousebutton(const xdo_t *xdo, Window window, int button, int is_press); + +static int _is_success(const char *funcname, int code, const xdo_t *xdo); +static void _xdo_debug(const xdo_t *xdo, const char *format, ...); +static void _xdo_eprintf(const xdo_t *xdo, int hushable, const char *format, ...); + +/* context-free functions */ +static wchar_t _keysym_to_char(KeySym keysym); + +/* Default to -1, initialize it when we need it */ +static Atom atom_NET_WM_PID = -1; +static Atom atom_NET_WM_NAME = -1; +static Atom atom_WM_NAME = -1; +static Atom atom_STRING = -1; +static Atom atom_UTF8_STRING = -1; + +xdo_t* xdo_new(const char *display_name) { + Display *xdpy; + + if (display_name == NULL) { + display_name = XDisplayName(display_name); + } + +#define DISPLAY_HINT "Is there an Xorg or other X server running? You can try setting 'export DISPLAY=:0' and trying again." + if (display_name == NULL) { + fprintf(stderr, "Error: No DISPLAY environment variable is set. " DISPLAY_HINT "\n"); + return NULL; + } + + if (*display_name == '\0') { + fprintf(stderr, "Error: DISPLAY environment variable is empty. " DISPLAY_HINT "\n"); + return NULL; + } + + if ((xdpy = XOpenDisplay(display_name)) == NULL) { + return NULL; + } + + return xdo_new_with_opened_display(xdpy, display_name, 1); +} + +xdo_t* xdo_new_with_opened_display(Display *xdpy, const char *display, + int close_display_when_freed) { + xdo_t *xdo = NULL; + + if (xdpy == NULL) { + /* Can't use _xdo_eprintf yet ... */ + fprintf(stderr, "xdo_new: xdisplay I was given is a null pointer\n"); + return NULL; + } + + // This library and xdotool do not work correctly on Wayland/XWayland. + // Try to detect XWayland and warn the user about problems. + // TODO(sissel): This was disabled due to issue #346 + // -- xdotool works on XWayland for some operations, so it isn't helpful to refuse all usage on XWayland. + //if (appears_to_be_wayland(xdpy)) { + //fprintf(stderr, "The X server at %s appears to be XWayland. Unfortunately, XWayland does not correctly support the features used by libxdo and xdotool.\n", display); + //return NULL; + //} + + + /* XXX: Check for NULL here */ + xdo = malloc(sizeof(xdo_t)); + memset(xdo, 0, sizeof(xdo_t)); + + xdo->xdpy = xdpy; + xdo->close_display_when_freed = close_display_when_freed; + + if (display == NULL) { + display = "unknown"; + } + + if (getenv("XDO_QUIET")) { + xdo->quiet = True; + } + + if (_xdo_has_xtest(xdo)) { + xdo_enable_feature(xdo, XDO_FEATURE_XTEST); + _xdo_debug(xdo, "XTEST enabled."); + } else { + _xdo_eprintf(xdo, False, "Warning: XTEST extension unavailable on '%s'. Some" + " functionality may be disabled; See 'man xdotool' for more" + " info.", xdo->display_name); + xdo_disable_feature(xdo, XDO_FEATURE_XTEST); + } + + _xdo_populate_charcode_map(xdo); + return xdo; +} + +void xdo_free(xdo_t *xdo) { + if (xdo == NULL) + return; + + free(xdo->display_name); + free(xdo->charcodes); + if (xdo->xdpy && xdo->close_display_when_freed) + XCloseDisplay(xdo->xdpy); + + free(xdo); +} + +int xdo_wait_for_window_map_state(const xdo_t *xdo, Window wid, int map_state) { + int tries = MAX_TRIES; + XWindowAttributes attr; + attr.map_state = IsUnmapped; + while (tries > 0 && attr.map_state != map_state) { + XGetWindowAttributes(xdo->xdpy, wid, &attr); + usleep(30000); /* TODO(sissel): Use exponential backoff up to 1 second */ + tries--; + } + return 0; +} + +int xdo_map_window(const xdo_t *xdo, Window wid) { + int ret = 0; + ret = XMapWindow(xdo->xdpy, wid); + XFlush(xdo->xdpy); + return _is_success("XMapWindow", ret == 0, xdo); +} + +int xdo_unmap_window(const xdo_t *xdo, Window wid) { + int ret = 0; + ret = XUnmapWindow(xdo->xdpy, wid); + XFlush(xdo->xdpy); + return _is_success("XUnmapWindow", ret == 0, xdo); +} + +int xdo_reparent_window(const xdo_t *xdo, Window wid_source, Window wid_target) { + int ret = 0; + ret = XReparentWindow(xdo->xdpy, wid_source, wid_target, 0, 0); + XFlush(xdo->xdpy); + return _is_success("XReparentWindow", ret == 0, xdo); +} + +int xdo_get_window_location(const xdo_t *xdo, Window wid, + int *x_ret, int *y_ret, Screen **screen_ret) { + int ret; + XWindowAttributes attr; + ret = XGetWindowAttributes(xdo->xdpy, wid, &attr); + if (ret != 0) { + int x, y; + Window unused_child; + + /* The coordinates in attr are relative to the parent window. If + * the parent window is the root window, then the coordinates are + * correct. If the parent window isn't the root window --- which + * is likely --- then we translate them. */ + Window parent; + Window root; + Window* children; + unsigned int nchildren; + XQueryTree(xdo->xdpy, wid, &root, &parent, &children, &nchildren); + if (children != NULL) { + XFree(children); + } + if (parent == attr.root) { + x = attr.x; + y = attr.y; + } else { + XTranslateCoordinates(xdo->xdpy, wid, attr.root, + 0, 0, &x, &y, &unused_child); + } + + if (x_ret != NULL) { + *x_ret = x; + } + + if (y_ret != NULL) { + *y_ret = y; + } + + if (screen_ret != NULL) { + *screen_ret = attr.screen; + } + } + return _is_success("XGetWindowAttributes", ret == 0, xdo); +} + +int xdo_get_window_size(const xdo_t *xdo, Window wid, unsigned int *width_ret, + unsigned int *height_ret) { + int ret; + XWindowAttributes attr; + ret = XGetWindowAttributes(xdo->xdpy, wid, &attr); + if (ret != 0) { + if (width_ret != NULL) { + *width_ret = attr.width; + } + + if (height_ret != NULL) { + *height_ret = attr.height; + } + } + return _is_success("XGetWindowAttributes", ret == 0, xdo); +} + +int xdo_move_window(const xdo_t *xdo, Window wid, int x, int y) { + XWindowChanges wc; + int ret = 0; + wc.x = x; + wc.y = y; + + ret = XConfigureWindow(xdo->xdpy, wid, CWX | CWY, &wc); + return _is_success("XConfigureWindow", ret == 0, xdo); +} + +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) { + XSizeHints hints; + long supplied_return; + XGetWMNormalHints(xdo->xdpy, window, &hints, &supplied_return); + if (supplied_return & PResizeInc) { + width *= hints.width_inc; + height *= hints.height_inc; + } else { + fprintf(stderr, "No size hints found for window %ld\n", window); + *width_ret = width; + *height_ret = width; + } + + if (supplied_return & PBaseSize) { + width += hints.base_width; + height += hints.base_height; + } + + if (width_ret != NULL) { + *width_ret = width; + } + + if (height_ret != NULL) { + *height_ret = height; + } + + return XDO_SUCCESS; +} + +int xdo_set_window_size(const xdo_t *xdo, Window window, int width, int height, int flags) { + XWindowChanges wc; + int ret = 0; + int cw_flags = 0; + + if (flags & SIZE_USEHINTS) { + flags |= SIZE_USEHINTS_X | SIZE_USEHINTS_Y; + } + + wc.width = width; + wc.height = height; + + if (flags & SIZE_USEHINTS_X) { + xdo_translate_window_with_sizehint(xdo, window, width, height, (unsigned int*)&wc.width, + NULL); + } + + if (flags & SIZE_USEHINTS_Y) { + xdo_translate_window_with_sizehint(xdo, window, width, height, NULL, + (unsigned int*)&wc.height); + } + + if (width > 0) { + cw_flags |= CWWidth; + } + + if (height > 0) { + cw_flags |= CWHeight; + } + + ret = XConfigureWindow(xdo->xdpy, window, cw_flags, &wc); + XFlush(xdo->xdpy); + return _is_success("XConfigureWindow", ret == 0, xdo); +} + +int xdo_set_window_override_redirect(const xdo_t *xdo, Window wid, + int override_redirect) { + int ret; + XSetWindowAttributes wattr; + long mask = CWOverrideRedirect; + wattr.override_redirect = override_redirect; + ret = XChangeWindowAttributes(xdo->xdpy, wid, mask, &wattr); + + return _is_success("XChangeWindowAttributes", ret == 0, xdo); +} + +int xdo_set_window_class (const xdo_t *xdo, Window wid, const char *name, + const char *_class) { + int ret = 0; + XClassHint *hint = XAllocClassHint(); + XGetClassHint(xdo->xdpy, wid, hint); + if (name != NULL) + hint->res_name = (char*)name; + + if(_class != NULL) + hint->res_class = (char*)_class; + + ret = XSetClassHint(xdo->xdpy, wid, hint); + XFree(hint); + return _is_success("XSetClassHint", ret == 0, xdo); +} + +int xdo_set_window_urgency (const xdo_t *xdo, Window wid, int urgency) { + int ret = 0; + XWMHints *hint = XGetWMHints(xdo->xdpy, wid); + if (hint == NULL) + hint = XAllocWMHints(); + + if (urgency) + hint->flags = hint->flags | XUrgencyHint; + else + hint->flags = hint->flags & ~XUrgencyHint; + + ret = XSetWMHints(xdo->xdpy, wid, hint); + XFree(hint); + return _is_success("XSetWMHint", ret == 0, xdo); +} + +int xdo_set_window_property(const xdo_t *xdo, Window wid, const char *property, const char *value) { + + char netwm_property[256] = "_NET_"; + int ret = 0; + strcat(netwm_property, property); + + // Change the property + ret = XChangeProperty(xdo->xdpy, wid, + XInternAtom(xdo->xdpy, property, False), + XInternAtom(xdo->xdpy, "STRING", False), 8, + PropModeReplace, (unsigned char*)value, strlen(value)); + if (ret == 0) { + return _is_success("XChangeProperty", ret == 0, xdo); + } + + // Change _NET_ just in case for simpler NETWM compliance? + ret = XChangeProperty(xdo->xdpy, wid, + XInternAtom(xdo->xdpy, netwm_property, False), + XInternAtom(xdo->xdpy, "STRING", False), 8, + PropModeReplace, (unsigned char*)value, strlen(value)); + return _is_success("XChangeProperty", ret == 0, xdo); +} + +int xdo_focus_window(const xdo_t *xdo, Window wid) { + int ret = 0; + ret = XSetInputFocus(xdo->xdpy, wid, RevertToParent, CurrentTime); + XFlush(xdo->xdpy); + return _is_success("XSetInputFocus", ret == 0, xdo); +} + +int xdo_wait_for_window_size(const xdo_t *xdo, Window window, + unsigned int width, unsigned int height, + int flags, int to_or_from) { + unsigned int cur_width, cur_height; + /*unsigned int alt_width, alt_height;*/ + + //printf("Want: %udx%ud\n", width, height); + if (flags & SIZE_USEHINTS) { + xdo_translate_window_with_sizehint(xdo, window, width, height, + &width, &height); + } else { + unsigned int hint_width, hint_height; + /* TODO(sissel): fix compiler warning here, but it will require + * an ABI breakage by changing types... */ + xdo_translate_window_with_sizehint(xdo, window, 1, 1, + &hint_width, &hint_height); + //printf("Hint: %dx%d\n", hint_width, hint_height); + /* Find the nearest multiple (rounded down) of the hint height. */ + /*alt_width = (width - (width % hint_width));*/ + /*alt_height = (height - (height % hint_height));*/ + //printf("Alt: %udx%ud\n", alt_width, alt_height); + } + + int tries = MAX_TRIES; + xdo_get_window_size(xdo, window, &cur_width, + &cur_height); + //printf("Want: %udx%ud\n", width, height); + //printf("Alt: %udx%ud\n", alt_width, alt_height); + while (tries > 0 && (to_or_from == SIZE_TO + ? (cur_width != width && cur_height != height) + : (cur_width == width && cur_height == height))) { + xdo_get_window_size(xdo, window, (unsigned int *)&cur_width, + (unsigned int *)&cur_height); + usleep(30000); + tries--; + } + + return 0; +} + +int xdo_wait_for_window_active(const xdo_t *xdo, Window window, int active) { + Window activewin = 0; + int ret = 0; + int tries = MAX_TRIES; + + /* If active is true, wait until activewin is our window + * otherwise, wait until activewin is not our window */ + while (tries > 0 && + (active ? activewin != window : activewin == window)) { + ret = xdo_get_active_window(xdo, &activewin); + if (ret == XDO_ERROR) { + return ret; + } + usleep(30000); + tries--; + } + + return 0; +} + +int xdo_activate_window(const xdo_t *xdo, Window wid) { + int ret = 0; + long desktop = 0; + XEvent xev; + XWindowAttributes wattr; + + if (_xdo_ewmh_is_supported(xdo, "_NET_ACTIVE_WINDOW") == False) { + fprintf(stderr, + "Your windowmanager claims not to support _NET_ACTIVE_WINDOW, " + "so the attempt to activate the window was aborted.\n"); + return XDO_ERROR; + } + + /* If this window is on another desktop, let's go to that desktop first */ + + if (_xdo_ewmh_is_supported(xdo, "_NET_WM_DESKTOP") == True + && _xdo_ewmh_is_supported(xdo, "_NET_CURRENT_DESKTOP") == True) { + xdo_get_desktop_for_window(xdo, wid, &desktop); + xdo_set_current_desktop(xdo, desktop); + } + + memset(&xev, 0, sizeof(xev)); + xev.type = ClientMessage; + xev.xclient.display = xdo->xdpy; + xev.xclient.window = wid; + xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_ACTIVE_WINDOW", False); + xev.xclient.format = 32; + xev.xclient.data.l[0] = 2L; /* 2 == Message from a window pager */ + xev.xclient.data.l[1] = CurrentTime; + + XGetWindowAttributes(xdo->xdpy, wid, &wattr); + ret = XSendEvent(xdo->xdpy, wattr.screen->root, False, + SubstructureNotifyMask | SubstructureRedirectMask, + &xev); + + /* XXX: XSendEvent returns 0 on conversion failure, nonzero otherwise. + * Manpage says it will only generate BadWindow or BadValue errors */ + return _is_success("XSendEvent[EWMH:_NET_ACTIVE_WINDOW]", ret == 0, xdo); +} + +int xdo_set_number_of_desktops(const xdo_t *xdo, long ndesktops) { + /* XXX: This should support passing a screen number */ + XEvent xev; + Window root; + int ret = 0; + + if (_xdo_ewmh_is_supported(xdo, "_NET_NUMBER_OF_DESKTOPS") == False) { + fprintf(stderr, + "Your windowmanager claims not to support _NET_NUMBER_OF_DESKTOPS, " + "so the attempt to change the number of desktops was aborted.\n"); + return XDO_ERROR; + } + + root = RootWindow(xdo->xdpy, 0); + + memset(&xev, 0, sizeof(xev)); + xev.type = ClientMessage; + xev.xclient.display = xdo->xdpy; + xev.xclient.window = root; + xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_NUMBER_OF_DESKTOPS", + False); + xev.xclient.format = 32; + xev.xclient.data.l[0] = ndesktops; + + ret = XSendEvent(xdo->xdpy, root, False, + SubstructureNotifyMask | SubstructureRedirectMask, + &xev); + + return _is_success("XSendEvent[EWMH:_NET_NUMBER_OF_DESKTOPS]", ret == 0, xdo); +} + +int xdo_get_number_of_desktops(const xdo_t *xdo, long *ndesktops) { + Atom type; + int size; + long nitems; + unsigned char *data; + Window root; + Atom request; + + if (_xdo_ewmh_is_supported(xdo, "_NET_NUMBER_OF_DESKTOPS") == False) { + fprintf(stderr, + "Your windowmanager claims not to support _NET_NUMBER_OF_DESKTOPS, " + "so the attempt to query the number of desktops was aborted.\n"); + return XDO_ERROR; + } + + request = XInternAtom(xdo->xdpy, "_NET_NUMBER_OF_DESKTOPS", False); + root = XDefaultRootWindow(xdo->xdpy); + + data = xdo_get_window_property_by_atom(xdo, root, request, &nitems, &type, &size); + + if (nitems > 0) { + *ndesktops = *((long*)data); + } else { + *ndesktops = 0; + } + free(data); + + return _is_success("XGetWindowProperty[_NET_NUMBER_OF_DESKTOPS]", + *ndesktops == 0, xdo); +} + +int xdo_set_current_desktop(const xdo_t *xdo, long desktop) { + /* XXX: This should support passing a screen number */ + XEvent xev; + Window root; + int ret = 0; + + root = RootWindow(xdo->xdpy, 0); + + if (_xdo_ewmh_is_supported(xdo, "_NET_CURRENT_DESKTOP") == False) { + fprintf(stderr, + "Your windowmanager claims not to support _NET_CURRENT_DESKTOP, " + "so the attempt to change desktops was aborted.\n"); + return XDO_ERROR; + } + + memset(&xev, 0, sizeof(xev)); + xev.type = ClientMessage; + xev.xclient.display = xdo->xdpy; + xev.xclient.window = root; + xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_CURRENT_DESKTOP", + False); + xev.xclient.format = 32; + xev.xclient.data.l[0] = desktop; + xev.xclient.data.l[1] = CurrentTime; + + ret = XSendEvent(xdo->xdpy, root, False, + SubstructureNotifyMask | SubstructureRedirectMask, + &xev); + + return _is_success("XSendEvent[EWMH:_NET_CURRENT_DESKTOP]", ret == 0, xdo); +} + +int xdo_get_current_desktop(const xdo_t *xdo, long *desktop) { + Atom type; + int size; + long nitems; + unsigned char *data; + Window root; + + Atom request; + + if (_xdo_ewmh_is_supported(xdo, "_NET_CURRENT_DESKTOP") == False) { + fprintf(stderr, + "Your windowmanager claims not to support _NET_CURRENT_DESKTOP, " + "so the query for the current desktop was aborted.\n"); + return XDO_ERROR; + } + + request = XInternAtom(xdo->xdpy, "_NET_CURRENT_DESKTOP", False); + root = XDefaultRootWindow(xdo->xdpy); + + data = xdo_get_window_property_by_atom(xdo, root, request, &nitems, &type, &size); + + if (nitems > 0) { + *desktop = *((long*)data); + } else { + *desktop = -1; + } + free(data); + + return _is_success("XGetWindowProperty[_NET_CURRENT_DESKTOP]", + *desktop == -1, xdo); +} + +int xdo_set_desktop_for_window(const xdo_t *xdo, Window wid, long desktop) { + XEvent xev; + int ret = 0; + XWindowAttributes wattr; + XGetWindowAttributes(xdo->xdpy, wid, &wattr); + + if (_xdo_ewmh_is_supported(xdo, "_NET_WM_DESKTOP") == False) { + fprintf(stderr, + "Your windowmanager claims not to support _NET_WM_DESKTOP, " + "so the attempt to change a window's desktop location was " + "aborted.\n"); + return XDO_ERROR; + } + + memset(&xev, 0, sizeof(xev)); + xev.type = ClientMessage; + xev.xclient.display = xdo->xdpy; + xev.xclient.window = wid; + xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_WM_DESKTOP", + False); + xev.xclient.format = 32; + xev.xclient.data.l[0] = desktop; + xev.xclient.data.l[1] = 2; /* indicate we are messaging from a pager */ + + ret = XSendEvent(xdo->xdpy, wattr.screen->root, False, + SubstructureNotifyMask | SubstructureRedirectMask, + &xev); + + return _is_success("XSendEvent[EWMH:_NET_WM_DESKTOP]", ret == 0, xdo); +} + +int xdo_get_desktop_for_window(const xdo_t *xdo, Window wid, long *desktop) { + Atom type; + int size; + long nitems; + unsigned char *data; + Atom request; + + if (_xdo_ewmh_is_supported(xdo, "_NET_WM_DESKTOP") == False) { + fprintf(stderr, + "Your windowmanager claims not to support _NET_WM_DESKTOP, " + "so the attempt to query a window's desktop location was " + "aborted.\n"); + return XDO_ERROR; + } + + request = XInternAtom(xdo->xdpy, "_NET_WM_DESKTOP", False); + + data = xdo_get_window_property_by_atom(xdo, wid, request, &nitems, &type, &size); + + if (nitems > 0) { + *desktop = *((long*)data); + } else { + *desktop = -1; + } + free(data); + + return _is_success("XGetWindowProperty[_NET_WM_DESKTOP]", + *desktop == -1, xdo); +} + +int xdo_get_active_window(const xdo_t *xdo, Window *window_ret) { + Atom type; + int size; + long nitems; + unsigned char *data; + Atom request; + Window root; + + if (_xdo_ewmh_is_supported(xdo, "_NET_ACTIVE_WINDOW") == False) { + fprintf(stderr, + "Your windowmanager claims not to support _NET_ACTIVE_WINDOW, " + "so the attempt to query the active window aborted.\n"); + return XDO_ERROR; + } + + request = XInternAtom(xdo->xdpy, "_NET_ACTIVE_WINDOW", False); + root = XDefaultRootWindow(xdo->xdpy); + data = xdo_get_window_property_by_atom(xdo, root, request, &nitems, &type, &size); + + if (nitems > 0) { + *window_ret = *((Window*)data); + } else { + *window_ret = 0; + } + free(data); + + return _is_success("XGetWindowProperty[_NET_ACTIVE_WINDOW]", + *window_ret == 0, xdo); +} + +int xdo_select_window_with_click(const xdo_t *xdo, Window *window_ret) { + int screen_num; + Screen *screen; + xdo_get_mouse_location(xdo, NULL, NULL, &screen_num); + + screen = ScreenOfDisplay(xdo->xdpy, screen_num); + + /* Grab sync mode so we can ensure nothing changes while we figure + * out what the client window is. + * Also, everyone else who does 'select window' does it this way. + */ + Cursor cursor = XCreateFontCursor(xdo->xdpy, XC_target); + int grab_ret = 0; + grab_ret = XGrabPointer(xdo->xdpy, screen->root, False, ButtonReleaseMask, + GrabModeSync, GrabModeAsync, screen->root, cursor, CurrentTime); + if (grab_ret == AlreadyGrabbed) { + fprintf(stderr, "Attempt to grab the mouse failed. Something already has" + " the mouse grabbed. This can happen if you are dragging something" + " or if there is a popup currently shown\n"); + return XDO_ERROR; + } + + XEvent e; + XAllowEvents(xdo->xdpy, SyncPointer, CurrentTime); + XWindowEvent(xdo->xdpy, screen->root, ButtonReleaseMask, &e); + XUngrabPointer(xdo->xdpy, CurrentTime); + XFreeCursor(xdo->xdpy, cursor); + + if (e.xbutton.button != 1) { + fprintf(stderr, "window selection aborted with button %d\n", e.xbutton.button); + return XDO_ERROR; + } + + /* If there is no subwindow, then we clicked on the root window */ + if (e.xbutton.subwindow == 0) { + *window_ret = e.xbutton.root; + } else { + /* Random testing showed that 'root' always is the same as 'window' + * while 'subwindow' is the actual window we clicked on. Confusing... */ + *window_ret = e.xbutton.subwindow; + _xdo_debug(xdo, "Click on window %lu foo", *window_ret); + xdo_find_window_client(xdo, *window_ret, window_ret, XDO_FIND_CHILDREN); + } + return XDO_SUCCESS; +} + +/* XRaiseWindow is ignored in ion3 and Gnome2. Is it even useful? */ +int xdo_raise_window(const xdo_t *xdo, Window wid) { + int ret = 0; + ret = XRaiseWindow(xdo->xdpy, wid); + XFlush(xdo->xdpy); + return _is_success("XRaiseWindow", ret == 0, xdo); +} + +int xdo_move_mouse(const xdo_t *xdo, int x, int y, int screen) { + int ret = 0; + + /* There is a bug (feature?) in XTestFakeMotionEvent that causes + * the screen number in the request to be ignored. The internets + * seem to recommend XWarpPointer instead, ie; + * https://bugzilla.redhat.com/show_bug.cgi?id=518803 + */ + Window screen_root = RootWindow(xdo->xdpy, screen); + ret = XWarpPointer(xdo->xdpy, None, screen_root, 0, 0, 0, 0, x, y); + XFlush(xdo->xdpy); + return _is_success("XWarpPointer", ret == 0, xdo); +} + +int xdo_move_mouse_relative_to_window(const xdo_t *xdo, Window window, int x, int y) { + XWindowAttributes attr; + Window unused_child; + int root_x, root_y; + + XGetWindowAttributes(xdo->xdpy, window, &attr); + XTranslateCoordinates(xdo->xdpy, window, attr.root, + x, y, &root_x, &root_y, &unused_child); + return xdo_move_mouse(xdo, root_x, root_y, XScreenNumberOfScreen(attr.screen)); +} + +int xdo_move_mouse_relative(const xdo_t *xdo, int x, int y) { + int ret = 0; + ret = XTestFakeRelativeMotionEvent(xdo->xdpy, x, y, CurrentTime); + XFlush(xdo->xdpy); + return _is_success("XTestFakeRelativeMotionEvent", ret == 0, xdo); +} + +int _xdo_mousebutton(const xdo_t *xdo, Window window, int button, int is_press) { + int ret = 0; + + if (window == CURRENTWINDOW) { + ret = XTestFakeButtonEvent(xdo->xdpy, button, is_press, CurrentTime); + XFlush(xdo->xdpy); + return _is_success("XTestFakeButtonEvent(down)", ret == 0, xdo); + } else { + /* Send to specific window */ + int screen = 0; + XButtonEvent xbpe; + charcodemap_t *active_mod; + int active_mod_n; + + xdo_get_mouse_location(xdo, &xbpe.x_root, &xbpe.y_root, &screen); + xdo_get_active_modifiers(xdo, &active_mod, &active_mod_n); + + xbpe.window = window; + xbpe.button = button; + xbpe.display = xdo->xdpy; + xbpe.root = RootWindow(xdo->xdpy, screen); + xbpe.same_screen = True; /* Should we detect if window is on the same + screen as cursor? */ + xbpe.state = xdo_get_input_state(xdo); + + xbpe.subwindow = None; + xbpe.time = CurrentTime; + xbpe.type = (is_press ? ButtonPress : ButtonRelease); + + /* Get the coordinates of the cursor relative to xbpe.window and also find what + * subwindow it might be on */ + XTranslateCoordinates(xdo->xdpy, xbpe.root, xbpe.window, + xbpe.x_root, xbpe.y_root, &xbpe.x, &xbpe.y, &xbpe.subwindow); + + /* Normal behavior of 'mouse up' is that the modifier mask includes + * 'ButtonNMotionMask' where N is the button being released. This works the + * same way with keys, too. */ + if (!is_press) { /* is mouse up */ + switch(button) { + case 1: xbpe.state |= Button1MotionMask; break; + case 2: xbpe.state |= Button2MotionMask; break; + case 3: xbpe.state |= Button3MotionMask; break; + case 4: xbpe.state |= Button4MotionMask; break; + case 5: xbpe.state |= Button5MotionMask; break; + } + } + ret = XSendEvent(xdo->xdpy, window, True, ButtonPressMask, (XEvent *)&xbpe); + XFlush(xdo->xdpy); + free(active_mod); + return _is_success("XSendEvent(mousedown)", ret == 0, xdo); + } +} + +int xdo_mouse_up(const xdo_t *xdo, Window window, int button) { + return _xdo_mousebutton(xdo, window, button, False); +} + +int xdo_mouse_down(const xdo_t *xdo, Window window, int button) { + return _xdo_mousebutton(xdo, window, button, True); +} + +int xdo_get_mouse_location(const xdo_t *xdo, int *x_ret, int *y_ret, + int *screen_num_ret) { + return xdo_get_mouse_location2(xdo, x_ret, y_ret, screen_num_ret, NULL); +} + +int xdo_get_window_at_mouse(const xdo_t *xdo, Window *window_ret) { + return xdo_get_mouse_location2(xdo, NULL, NULL, NULL, window_ret); +} + +int xdo_get_mouse_location2(const xdo_t *xdo, int *x_ret, int *y_ret, + int *screen_num_ret, Window *window_ret) { + int ret = False; + int x = 0, y = 0, screen_num = 0; + int i = 0; + Window window = 0; + Window root = 0; + int dummy_int = 0; + unsigned int dummy_uint = 0; + int screencount = ScreenCount(xdo->xdpy); + + for (i = 0; i < screencount; i++) { + Screen *screen = ScreenOfDisplay(xdo->xdpy, i); + ret = XQueryPointer(xdo->xdpy, RootWindowOfScreen(screen), + &root, &window, + &x, &y, &dummy_int, &dummy_int, &dummy_uint); + if (ret == True) { + screen_num = i; + break; + } + } + + if (window_ret != NULL) { + /* Find the client window if we are not root. */ + if (window != root && window != 0) { + int findret; + Window client = 0; + + /* Search up the stack for a client window for this window */ + findret = xdo_find_window_client(xdo, window, &client, XDO_FIND_PARENTS); + if (findret == XDO_ERROR) { + /* If no client found, search down the stack */ + findret = xdo_find_window_client(xdo, window, &client, XDO_FIND_CHILDREN); + } + //fprintf(stderr, "%ld, %ld, %ld, %d\n", window, root, client, findret); + if (findret == XDO_SUCCESS) { + window = client; + } + } else { + window = root; + } + } + //printf("mouseloc root: %ld\n", root); + //printf("mouseloc window: %ld\n", window); + + if (ret == True) { + if (x_ret != NULL) *x_ret = x; + if (y_ret != NULL) *y_ret = y; + if (screen_num_ret != NULL) *screen_num_ret = screen_num; + if (window_ret != NULL) *window_ret = window; + } + + return _is_success("XQueryPointer", ret == False, xdo); +} + +int xdo_click_window(const xdo_t *xdo, Window window, int button) { + int ret = 0; + ret = xdo_mouse_down(xdo, window, button); + if (ret != XDO_SUCCESS) { + fprintf(stderr, "xdo_mouse_down failed, aborting click.\n"); + return ret; + } + usleep(DEFAULT_DELAY); + ret = xdo_mouse_up(xdo, window, button); + return ret; +} + +int xdo_click_window_multiple(const xdo_t *xdo, Window window, int button, + int repeat, useconds_t delay) { + int ret = 0; + while (repeat > 0) { + ret = xdo_click_window(xdo, window, button); + if (ret != XDO_SUCCESS) { + fprintf(stderr, "click failed with %d repeats remaining\n", repeat); + return ret; + } + repeat--; + + /* Sleeping even after the last click is important, so that a call to xdo_set_active_modifiers() + * right after won't think that the button is still pressed. */ + usleep(delay); + } /* while (repeat > 0) */ + return ret; +} /* int xdo_click_window_multiple */ + +/* XXX: Return proper code if errors found */ +int xdo_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay) { + + /* Since we're doing down/up, the delay should be based on the number + * of keys pressed (including shift). Since up/down is two calls, + * divide by two. */ + delay /= 2; + + /* XXX: Add error handling */ + //int nkeys = strlen(string); + //charcodemap_t *keys = calloc(nkeys, sizeof(charcodemap_t)); + charcodemap_t key; + //int modifier = 0; + setlocale(LC_CTYPE,""); + mbstate_t ps = { 0 }; + ssize_t len; + while ( (len = mbsrtowcs(&key.key, &string, 1, &ps)) ) { + if (len == -1) { + fprintf(stderr, "Invalid multi-byte sequence encountered\n"); + return XDO_ERROR; + } + _xdo_charcodemap_from_char(xdo, &key); + if (key.code == 0 && key.symbol == NoSymbol) { + fprintf(stderr, "I don't know which key produces '%lc', skipping.\n", + key.key); + continue; + } else { + //printf("Found key for %c\n", key.key); + //printf("code: %d\n", key.code); + //printf("sym: %s\n", XKeysymToString(key.symbol)); + } + + //printf(stderr, + //"Key '%c' maps to code %d / sym %lu in group %d / mods %d (%s)\n", + //key.key, key.code, key.symbol, key.group, key.modmask, + //(key.needs_binding == 1) ? "needs binding" : "ok"); + + //_xdo_send_key(xdo, window, keycode, modstate, True, delay); + //_xdo_send_key(xdo, window, keycode, modstate, False, delay); + xdo_send_keysequence_window_list_do(xdo, window, &key, 1, True, NULL, delay / 2); + key.needs_binding = 0; + xdo_send_keysequence_window_list_do(xdo, window, &key, 1, False, NULL, delay / 2); + + /* XXX: Flush here or at the end? or never? */ + //XFlush(xdo->xdpy); + } /* walk string generating a keysequence */ + + //free(keys); + return XDO_SUCCESS; +} + +int _xdo_send_keysequence_window_do(const xdo_t *xdo, Window window, const char *keyseq, + int pressed, int *modifier, useconds_t delay) { + int ret = 0; + charcodemap_t *keys = NULL; + int nkeys = 0; + + if (_xdo_send_keysequence_window_to_keycode_list(xdo, keyseq, &keys, &nkeys) == False) { + fprintf(stderr, "Failure converting key sequence '%s' to keycodes\n", keyseq); + return 1; + } + + ret = xdo_send_keysequence_window_list_do(xdo, window, keys, nkeys, pressed, modifier, delay); + free(keys); + + return ret; +} + +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) { + int i = 0; + int modstate = 0; + int keymapchanged = 0; + + /* Find an unused keycode in case we need to bind unmapped keysyms */ + KeySym *keysyms = NULL; + int keysyms_per_keycode = 0; + int scratch_keycode = 0; /* Scratch space for temporary keycode bindings */ + keysyms = XGetKeyboardMapping(xdo->xdpy, xdo->keycode_low, + xdo->keycode_high - xdo->keycode_low, + &keysyms_per_keycode); + + /* Find a keycode that is unused for scratchspace */ + for (i = xdo->keycode_low; i <= xdo->keycode_high; i++) { + int j = 0; + int key_is_empty = 1; + for (j = 0; j < keysyms_per_keycode; j++) { + /*char *symname;*/ + int symindex = (i - xdo->keycode_low) * keysyms_per_keycode + j; + /*symname = XKeysymToString(keysyms[symindex]);*/ + if (keysyms[symindex] != 0) { + key_is_empty = 0; + } else { + break; + } + } + if (key_is_empty) { + scratch_keycode = i; + break; + } + } + XFree(keysyms); + + /* Allow passing NULL for modifier in case we don't care about knowing + * the modifier map state after we finish */ + if (modifier == NULL) + modifier = &modstate; + + for (i = 0; i < nkeys; i++) { + if (keys[i].needs_binding == 1) { + KeySym keysym_list[] = { keys[i].symbol }; + _xdo_debug(xdo, "Mapping sym %lu to %d", keys[i].symbol, scratch_keycode); + XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1); + XSync(xdo->xdpy, False); + /* override the code in our current key to use the scratch_keycode */ + keys[i].code = scratch_keycode; + keymapchanged = 1; + } + + //fprintf(stderr, "keyseqlist_do: Sending %lc %s (%d, mods %x)\n", + //keys[i].key, (pressed ? "down" : "up"), keys[i].code, *modifier); + _xdo_send_key(xdo, window, &(keys[i]), *modifier, pressed, delay); + + if (keys[i].needs_binding == 1) { + /* If we needed to make a new keymapping for this keystroke, we + * should sync with the server now, after the keypress, so that + * the next mapping or removal doesn't conflict. */ + XSync(xdo->xdpy, False); + } + + if (pressed) { + *modifier |= keys[i].modmask; + } else { + *modifier &= ~(keys[i].modmask); + } + } + + + if (keymapchanged) { + KeySym keysym_list[] = { 0 }; + _xdo_debug(xdo, "Reverting scratch keycode (sym %lu to %d)", + keys[i].symbol, scratch_keycode); + XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1); + } + + /* Necessary? */ + XFlush(xdo->xdpy); + return XDO_SUCCESS; +} + + +int xdo_send_keysequence_window_down(const xdo_t *xdo, Window window, const char *keyseq, + useconds_t delay) { + return _xdo_send_keysequence_window_do(xdo, window, keyseq, True, NULL, delay); +} + +int xdo_send_keysequence_window_up(const xdo_t *xdo, Window window, const char *keyseq, + useconds_t delay) { + return _xdo_send_keysequence_window_do(xdo, window, keyseq, False, NULL, delay); +} + +int xdo_send_keysequence_window(const xdo_t *xdo, Window window, const char *keyseq, + useconds_t delay) { + int ret = 0; + int modifier = 0; + ret += _xdo_send_keysequence_window_do(xdo, window, keyseq, True, &modifier, delay / 2); + ret += _xdo_send_keysequence_window_do(xdo, window, keyseq, False, &modifier, delay / 2); + return ret; +} + +/* Add by Lee Pumphret 2007-07-28 + * Modified slightly by Jordan Sissel */ +int xdo_get_focused_window(const xdo_t *xdo, Window *window_ret) { + int ret = 0; + int unused_revert_ret; + + ret = XGetInputFocus(xdo->xdpy, window_ret, &unused_revert_ret); + + /* Xvfb with no window manager and given otherwise no input, with + * a single client, will return the current focused window as '1' + * I think this is a bug, so let's alert the user. */ + if (*window_ret == 1) { + fprintf(stderr, + "XGetInputFocus returned the focused window of %ld. " + "This is likely a bug in the X server.\n", *window_ret); + } + return _is_success("XGetInputFocus", ret == 0, xdo); +} + +int xdo_wait_for_window_focus(const xdo_t *xdo, Window window, int want_focus) { + Window focuswin = 0; + int ret; + int tries = MAX_TRIES; + ret = xdo_get_focused_window(xdo, &focuswin); + if (ret != 0) { + return ret; + } + + while (tries > 0 && + (want_focus ? focuswin != window : focuswin == window)) { + usleep(30000); /* TODO(sissel): Use exponential backoff up to 1 second */ + ret = xdo_get_focused_window(xdo, &focuswin); + if (ret != 0) { + return ret; + } + tries--; + } + return 0; +} + +/* 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. */ +int xdo_get_focused_window_sane(const xdo_t *xdo, Window *window_ret) { + xdo_get_focused_window(xdo, window_ret); + xdo_find_window_client(xdo, *window_ret, window_ret, XDO_FIND_PARENTS); + return _is_success("xdo_get_focused_window_sane", *window_ret == 0, xdo); +} + +int xdo_find_window_client(const xdo_t *xdo, Window window, Window *window_ret, + int direction) { + /* for XQueryTree */ + Window dummy, parent, *children = NULL; + unsigned int nchildren; + Atom atom_wmstate = XInternAtom(xdo->xdpy, "WM_STATE", False); + + int done = False; + while (!done) { + if (window == 0) { + return XDO_ERROR; + } + + long items; + _xdo_debug(xdo, "get_window_property on %lu", window); + xdo_get_window_property_by_atom(xdo, window, atom_wmstate, &items, NULL, NULL); + + if (items == 0) { + /* This window doesn't have WM_STATE property, keep searching. */ + _xdo_debug(xdo, "window %lu has no WM_STATE property, digging more.", window); + XQueryTree(xdo->xdpy, window, &dummy, &parent, &children, &nchildren); + + if (direction == XDO_FIND_PARENTS) { + _xdo_debug(xdo, "searching parents"); + /* Don't care about the children, but we still need to free them */ + if (children != NULL) + XFree(children); + window = parent; + } else if (direction == XDO_FIND_CHILDREN) { + _xdo_debug(xdo, "searching %d children", nchildren); + unsigned int i = 0; + int ret; + done = True; /* recursion should end us */ + for (i = 0; i < nchildren; i++) { + ret = xdo_find_window_client(xdo, children[i], &window, direction); + //fprintf(stderr, "findclient: %ld\n", window); + if (ret == XDO_SUCCESS) { + *window_ret = window; + break; + } + } + if (nchildren == 0) { + return XDO_ERROR; + } + if (children != NULL) + XFree(children); + } else { + fprintf(stderr, "Invalid find_client direction (%d)\n", direction); + *window_ret = 0; + if (children != NULL) + XFree(children); + return XDO_ERROR; + } + } else { + *window_ret = window; + done = True; + } + } + return XDO_SUCCESS; +} + +/* Helper functions */ +static KeySym _xdo_keysym_from_char(const xdo_t *xdo, wchar_t key) { + int i = 0; + int len = xdo->charcodes_len; + + //printf("Finding symbol for key '%c'\n", key); + for (i = 0; i < len; i++) { + //printf(" => %c vs %c (%d)\n", + //key, xdo->charcodes[i].key, (xdo->charcodes[i].key == key)); + if (xdo->charcodes[i].key == key) { + //printf(" => MATCH to symbol: %lu\n", xdo->charcodes[i].symbol); + return xdo->charcodes[i].symbol; + } + } + + if (key >= 0x100) key += 0x01000000; + if (XKeysymToString(key)) return key; + return NoSymbol; +} + +static void _xdo_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key) { + KeySym keysym = _xdo_keysym_from_char(xdo, key->key); + _xdo_charcodemap_from_keysym(xdo, key, keysym); + + /* If the character is an uppercase character within the Basic Latin or Latin-1 code block, + * then sending the capital character keycode will not work. + * We have to also send the shift modifier. + * There are only three ranges of capital letters to worry about */ + if ((key->key >= 0x41 && key->key <= 0x5A) || (key->key >= 0xC0 && key->key <= 0xD6) || (key->key >= 0xD8 && key->key <= 0xDE)) { + key->modmask = ShiftMask; + } +} + +static void _xdo_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym) { + int i = 0; + int len = xdo->charcodes_len; + + key->code = 0; + key->symbol = keysym; + key->group = 0; + key->modmask = 0; + key->needs_binding = 1; + + for (i = 0; i < len; i++) { + if (xdo->charcodes[i].symbol == keysym) { + key->code = xdo->charcodes[i].code; + key->group = xdo->charcodes[i].group; + key->modmask = xdo->charcodes[i].modmask; + key->needs_binding = 0; + return; + } + } +} + +static int _xdo_has_xtest(const xdo_t *xdo) { + int dummy; + return (XTestQueryExtension(xdo->xdpy, &dummy, &dummy, &dummy, &dummy) == True); +} + +static void _xdo_populate_charcode_map(xdo_t *xdo) { + /* assert xdo->display is valid */ + int keycodes_length = 0; + int idx = 0; + int keycode, group, groups, level, modmask, num_map; + + XDisplayKeycodes(xdo->xdpy, &(xdo->keycode_low), &(xdo->keycode_high)); + XModifierKeymap *modmap = XGetModifierMapping(xdo->xdpy); + KeySym *keysyms = XGetKeyboardMapping(xdo->xdpy, xdo->keycode_low, + xdo->keycode_high - xdo->keycode_low + 1, + &xdo->keysyms_per_keycode); + XFree(keysyms); + + /* Add 2 to the size because the range [low, high] is inclusive */ + /* Add 2 more for tab (\t) and newline (\n) */ + keycodes_length = ((xdo->keycode_high - xdo->keycode_low) + 1) + * xdo->keysyms_per_keycode; + + xdo->charcodes = calloc(keycodes_length, sizeof(charcodemap_t)); + XkbDescPtr desc = XkbGetMap(xdo->xdpy, XkbAllClientInfoMask, XkbUseCoreKbd); + + for (keycode = xdo->keycode_low; keycode <= xdo->keycode_high; keycode++) { + groups = XkbKeyNumGroups(desc, keycode); + for (group = 0; group < groups; group++) { + XkbKeyTypePtr key_type = XkbKeyKeyType(desc, keycode, group); + for (level = 0; level < key_type->num_levels; level++) { + KeySym keysym = XkbKeycodeToKeysym(xdo->xdpy, keycode, group, level); + modmask = 0; + + for (num_map = 0; num_map < key_type->map_count; num_map++) { + XkbKTMapEntryRec map = key_type->map[num_map]; + if (map.active && map.level == level) { + modmask = map.mods.mask; + break; + } + } + + xdo->charcodes[idx].key = _keysym_to_char(keysym); + xdo->charcodes[idx].code = keycode; + xdo->charcodes[idx].group = group; + xdo->charcodes[idx].modmask = modmask | _xdo_query_keycode_to_modifier(modmap, keycode); + xdo->charcodes[idx].symbol = keysym; + + idx++; + } + } + } + xdo->charcodes_len = idx; + XkbFreeClientMap(desc, 0, 1); + XFreeModifiermap(modmap); +} + +/* context-free functions */ +wchar_t _keysym_to_char(KeySym keysym) { + return (wchar_t)xkb_keysym_to_utf32(keysym); +} + +int _xdo_send_keysequence_window_to_keycode_list(const xdo_t *xdo, const char *keyseq, + charcodemap_t **keys, int *nkeys) { + char *tokctx = NULL; + const char *tok = NULL; + char *keyseq_copy = NULL, *strptr = NULL; + int i = 0; + + /* Array of keys to press, in order given by keyseq */ + int keys_size = 10; + + if (strcspn(keyseq, " \t\n.-[]{}\\|") != strlen(keyseq)) { + fprintf(stderr, "Error: Invalid key sequence '%s'\n", keyseq); + return False; + } + + *nkeys = 0; + *keys = calloc(keys_size, sizeof(charcodemap_t)); + keyseq_copy = strptr = strdup(keyseq); + while ((tok = strtok_r(strptr, "+", &tokctx)) != NULL) { + KeySym sym; + KeyCode key; + + if (strptr != NULL) + strptr = NULL; + + /* Check if 'tok' (string keysym) is an alias to another key */ + /* symbol_map comes from xdo.util */ + for (i = 0; symbol_map[i] != NULL; i+=2) + if (!strcasecmp(tok, symbol_map[i])) + tok = symbol_map[i + 1]; + + sym = XStringToKeysym(tok); + if (sym == NoSymbol) { + /* Accept a number as a explicit keycode */ + if (isdigit(tok[0])) { + key = (unsigned int) atoi(tok); + } else { + fprintf(stderr, "(symbol) No such key name '%s'. Ignoring it.\n", tok); + continue; + } + (*keys)[*nkeys].code = key; + (*keys)[*nkeys].symbol = sym; + (*keys)[*nkeys].group = 0; + (*keys)[*nkeys].modmask = 0; + (*keys)[*nkeys].needs_binding = 0; + if (key == 0) { + //fprintf(stderr, "No such key '%s'. Ignoring it.\n", tok); + (*keys)[*nkeys].needs_binding = 1; + } + } else { + _xdo_charcodemap_from_keysym(xdo, &(*keys)[*nkeys], sym); + } + + (*nkeys)++; + if (*nkeys == keys_size) { + keys_size *= 2; + *keys = realloc(*keys, keys_size * sizeof(KeyCode)); + } + } + + free(keyseq_copy); + return True; +} + +int _is_success(const char *funcname, int code, const xdo_t *xdo) { + /* Nonzero is failure. */ + if (code != 0 && !xdo->quiet) + fprintf(stderr, "%s failed (code=%d)\n", funcname, code); + return code; +} + +int xdo_get_window_property(const xdo_t *xdo, Window window, const char *property, + unsigned char **value, long *nitems, Atom *type, int *size) { + *value = xdo_get_window_property_by_atom(xdo, window, XInternAtom(xdo->xdpy, property, False), nitems, type, size); + if (*value == NULL) { + return XDO_ERROR; + } + return XDO_SUCCESS; +} + +/* Arbitrary window property retrieval + * slightly modified version from xprop.c from Xorg */ +unsigned char *xdo_get_window_property_by_atom(const xdo_t *xdo, Window window, Atom atom, + long *nitems, Atom *type, int *size) { + Atom actual_type; + int actual_format; + unsigned long _nitems; + /*unsigned long nbytes;*/ + unsigned long bytes_after; /* unused */ + unsigned char *prop; + int status; + + status = XGetWindowProperty(xdo->xdpy, window, atom, 0, (~0L), + False, AnyPropertyType, &actual_type, + &actual_format, &_nitems, &bytes_after, + &prop); + if (status == BadWindow) { + fprintf(stderr, "window id # 0x%lx does not exists!", window); + return NULL; + } if (status != Success) { + fprintf(stderr, "XGetWindowProperty failed!"); + return NULL; + } + + /* + *if (actual_format == 32) + * nbytes = sizeof(long); + *else if (actual_format == 16) + * nbytes = sizeof(short); + *else if (actual_format == 8) + * nbytes = 1; + *else if (actual_format == 0) + * nbytes = 0; + */ + + if (nitems != NULL) { + *nitems = _nitems; + } + + if (type != NULL) { + *type = actual_type; + } + + if (size != NULL) { + *size = actual_format; + } + return prop; +} + +int _xdo_ewmh_is_supported(const xdo_t *xdo, const char *feature) { + Atom type = 0; + long nitems = 0L; + int size = 0; + Atom *results = NULL; + long i = 0; + + Window root; + Atom request; + Atom feature_atom; + + request = XInternAtom(xdo->xdpy, "_NET_SUPPORTED", False); + feature_atom = XInternAtom(xdo->xdpy, feature, False); + root = XDefaultRootWindow(xdo->xdpy); + + results = (Atom *) xdo_get_window_property_by_atom(xdo, root, request, &nitems, &type, &size); + for (i = 0L; i < nitems; i++) { + if (results[i] == feature_atom) { + free(results); + return True; + } + } + free(results); + + return False; +} + +void _xdo_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk) { + xk->display = xdo->xdpy; + xk->subwindow = None; + xk->time = CurrentTime; + xk->same_screen = True; + + /* Should we set these at all? */ + xk->x = xk->y = xk->x_root = xk->y_root = 1; +} + +void _xdo_send_key(const xdo_t *xdo, Window window, charcodemap_t *key, + int modstate, int is_press, useconds_t delay) { + /* Properly ensure the modstate is set by finding a key + * that activates each bit in the modifier state */ + int mask = modstate | key->modmask; + int use_xtest = 0; + + if (window == CURRENTWINDOW) { + use_xtest = 1; + } else { + Window focuswin = 0; + xdo_get_focused_window(xdo, &focuswin); + if (focuswin == window) { + use_xtest = 1; + } + } + if (use_xtest) { + //printf("XTEST: Sending key %d %s\n", key->code, is_press ? "down" : "up"); + XkbStateRec state; + XkbGetState(xdo->xdpy, XkbUseCoreKbd, &state); + int current_group = state.group; + XkbLockGroup(xdo->xdpy, XkbUseCoreKbd, key->group); + if (mask) + _xdo_send_modifier(xdo, mask, is_press); + //printf("XTEST: Sending key %d %s %x %d\n", key->code, is_press ? "down" : "up", key->modmask, key->group); + XTestFakeKeyEvent(xdo->xdpy, key->code, is_press, CurrentTime); + XkbLockGroup(xdo->xdpy, XkbUseCoreKbd, current_group); + XSync(xdo->xdpy, False); + } else { + /* Since key events have 'state' (shift, etc) in the event, we don't + * need to worry about key press ordering. */ + XKeyEvent xk; + _xdo_init_xkeyevent(xdo, &xk); + xk.window = window; + xk.keycode = key->code; + xk.state = mask | (key->group << 13); + xk.type = (is_press ? KeyPress : KeyRelease); + XSendEvent(xdo->xdpy, xk.window, True, KeyPressMask, (XEvent *)&xk); + } + + /* Skipping the usleep if delay is 0 is much faster than calling usleep(0) */ + XFlush(xdo->xdpy); + if (delay > 0) { + usleep(delay); + } +} + +int _xdo_query_keycode_to_modifier(XModifierKeymap *modmap, KeyCode keycode) { + int i = 0, j = 0; + int max = modmap->max_keypermod; + + for (i = 0; i < 8; i++) { /* 8 modifier types, per XGetModifierMapping(3X) */ + for (j = 0; j < max && modmap->modifiermap[(i * max) + j]; j++) { + if (keycode == modmap->modifiermap[(i * max) + j]) { + switch (i) { + case ShiftMapIndex: return ShiftMask; break; + case LockMapIndex: return LockMask; break; + case ControlMapIndex: return ControlMask; break; + case Mod1MapIndex: return Mod1Mask; break; + case Mod2MapIndex: return Mod2Mask; break; + case Mod3MapIndex: return Mod3Mask; break; + case Mod4MapIndex: return Mod4Mask; break; + case Mod5MapIndex: return Mod5Mask; break; + } + } /* end if */ + } /* end loop j */ + } /* end loop i */ + + /* No modifier found for this keycode, return no mask */ + return 0; +} + +void _xdo_send_modifier(const xdo_t *xdo, int modmask, int is_press) { + XModifierKeymap *modifiers = XGetModifierMapping(xdo->xdpy); + int mod_index, mod_key, keycode; + + for (mod_index = ShiftMapIndex; mod_index <= Mod5MapIndex; mod_index++) { + if (modmask & (1 << mod_index)) { + for (mod_key = 0; mod_key < modifiers->max_keypermod; mod_key++) { + keycode = modifiers->modifiermap[mod_index * modifiers->max_keypermod + mod_key]; + if (keycode) { + XTestFakeKeyEvent(xdo->xdpy, keycode, is_press, CurrentTime); + XSync(xdo->xdpy, False); + break; + } + } + } + } + + XFreeModifiermap(modifiers); +} + +int xdo_get_active_modifiers(const xdo_t *xdo, charcodemap_t **keys, + int *nkeys) { + /* For each keyboard device, if an active key is a modifier, + * then add the keycode to the keycode list */ + + char keymap[32]; /* keycode map: 256 bits */ + int keys_size = 10; + int keycode = 0; + int mod_index, mod_key; + XModifierKeymap *modifiers = XGetModifierMapping(xdo->xdpy); + *nkeys = 0; + *keys = malloc(keys_size * sizeof(charcodemap_t)); + + XQueryKeymap(xdo->xdpy, keymap); + + for (mod_index = ShiftMapIndex; mod_index <= Mod5MapIndex; mod_index++) { + for (mod_key = 0; mod_key < modifiers->max_keypermod; mod_key++) { + keycode = modifiers->modifiermap[mod_index * modifiers->max_keypermod + mod_key]; + if (keycode && keymap[(keycode / 8)] & (1 << (keycode % 8))) { + /* This keycode is active and is a modifier, record it. */ + + /* Zero the charcodemap_t entry before using it. + * Fixes a bug reported by Hong-Leong Ong - where + * 'xdotool key --clearmodifiers ...' sometimes failed trying + * to clear modifiers that didn't exist since charcodemap_t's modmask was + * uninitialized */ + memset(*keys + *nkeys, 0, sizeof(charcodemap_t)); + + (*keys)[*nkeys].code = keycode; + (*nkeys)++; + + if (*nkeys == keys_size) { + keys_size *= 2; + *keys = realloc(keys, keys_size * sizeof(charcodemap_t)); + } + } + } + } + + XFreeModifiermap(modifiers); + + return XDO_SUCCESS; +} + +unsigned int xdo_get_input_state(const xdo_t *xdo) { + Window root, dummy; + int root_x, root_y, win_x, win_y; + unsigned int mask; + root = DefaultRootWindow(xdo->xdpy); + + XQueryPointer(xdo->xdpy, root, &dummy, &dummy, + &root_x, &root_y, &win_x, &win_y, &mask); + + return mask; +} + +const char **xdo_get_symbol_map(void) { + return symbol_map; +} + +int xdo_clear_active_modifiers(const xdo_t *xdo, Window window, charcodemap_t *active_mods, int active_mods_n) { + int ret = 0; + unsigned int input_state = xdo_get_input_state(xdo); + xdo_send_keysequence_window_list_do(xdo, window, active_mods, + active_mods_n, False, NULL, DEFAULT_DELAY); + + if (input_state & Button1MotionMask) + ret = xdo_mouse_up(xdo, window, 1); + if (!ret && input_state & Button2MotionMask) + ret = xdo_mouse_up(xdo, window, 2); + if (!ret && input_state & Button3MotionMask) + ret = xdo_mouse_up(xdo, window, 3); + if (!ret && input_state & Button4MotionMask) + ret = xdo_mouse_up(xdo, window, 4); + if (!ret && input_state & Button5MotionMask) + ret = xdo_mouse_up(xdo, window, 5); + if (!ret && input_state & LockMask) { + /* explicitly use down+up here since xdo_send_keysequence_window alone will track the modifiers + * incurred by a key (like shift, or caps) and send them on the 'up' sequence. + * That seems to break things with Caps_Lock only, so let's be explicit here. */ + ret = xdo_send_keysequence_window_down(xdo, window, "Caps_Lock", DEFAULT_DELAY); + ret += xdo_send_keysequence_window_up(xdo, window, "Caps_Lock", DEFAULT_DELAY); + } + + XSync(xdo->xdpy, False); + return ret; +} + +int xdo_set_active_modifiers(const xdo_t *xdo, Window window, charcodemap_t *active_mods, int active_mods_n) { + int ret = 0; + unsigned int input_state = xdo_get_input_state(xdo); + xdo_send_keysequence_window_list_do(xdo, window, active_mods, + active_mods_n, True, NULL, DEFAULT_DELAY); + if (input_state & Button1MotionMask) + ret = xdo_mouse_down(xdo, window, 1); + if (!ret && input_state & Button2MotionMask) + ret = xdo_mouse_down(xdo, window, 2); + if (!ret && input_state & Button3MotionMask) + ret = xdo_mouse_down(xdo, window, 3); + if (!ret && input_state & Button4MotionMask) + ret = xdo_mouse_down(xdo, window, 4); + if (!ret && input_state & Button5MotionMask) + ret = xdo_mouse_down(xdo, window, 5); + if (!ret && input_state & LockMask) { + /* explicitly use down+up here since xdo_send_keysequence_window alone will track the modifiers + * incurred by a key (like shift, or caps) and send them on the 'up' sequence. + * That seems to break things with Caps_Lock only, so let's be explicit here. */ + ret = xdo_send_keysequence_window_down(xdo, window, "Caps_Lock", DEFAULT_DELAY); + ret += xdo_send_keysequence_window_up(xdo, window, "Caps_Lock", DEFAULT_DELAY); + } + + XSync(xdo->xdpy, False); + return ret; +} + +int xdo_get_pid_window(const xdo_t *xdo, Window window) { + Atom type; + int size; + long nitems; + unsigned char *data; + int window_pid = 0; + + if (atom_NET_WM_PID == (Atom)-1) { + atom_NET_WM_PID = XInternAtom(xdo->xdpy, "_NET_WM_PID", False); + } + + data = xdo_get_window_property_by_atom(xdo, window, atom_NET_WM_PID, &nitems, &type, &size); + + if (nitems > 0) { + /* The data itself is unsigned long, but everyone uses int as pid values */ + window_pid = (int) *((unsigned long *)data); + } + free(data); + + return window_pid; +} + +int xdo_wait_for_mouse_move_from(const xdo_t *xdo, int origin_x, int origin_y) { + int x, y; + int ret = 0; + int tries = MAX_TRIES; + + ret = xdo_get_mouse_location(xdo, &x, &y, NULL); + while (tries > 0 && + (x == origin_x && y == origin_y)) { + usleep(30000); + ret = xdo_get_mouse_location(xdo, &x, &y, NULL); + tries--; + } + + return ret; +} + +int xdo_wait_for_mouse_move_to(const xdo_t *xdo, int dest_x, int dest_y) { + int x, y; + int ret = 0; + int tries = MAX_TRIES; + + ret = xdo_get_mouse_location(xdo, &x, &y, NULL); + while (tries > 0 && (x != dest_x && y != dest_y)) { + usleep(30000); + ret = xdo_get_mouse_location(xdo, &x, &y, NULL); + tries--; + } + + return ret; +} + +int xdo_get_desktop_viewport(const xdo_t *xdo, int *x_ret, int *y_ret) { + if (_xdo_ewmh_is_supported(xdo, "_NET_DESKTOP_VIEWPORT") == False) { + fprintf(stderr, + "Your windowmanager claims not to support _NET_DESKTOP_VIEWPORT, " + "so I cannot tell you the viewport position.\n"); + return XDO_ERROR; + } + + Atom type; + int size; + long nitems; + unsigned char *data; + Atom request = XInternAtom(xdo->xdpy, "_NET_DESKTOP_VIEWPORT", False); + Window root = RootWindow(xdo->xdpy, 0); + data = xdo_get_window_property_by_atom(xdo, root, request, &nitems, &type, &size); + + if (type != XA_CARDINAL) { + fprintf(stderr, + "Got unexpected type returned from _NET_DESKTOP_VIEWPORT." + " Expected CARDINAL, got %s\n", + XGetAtomName(xdo->xdpy, type)); + free(data); + return XDO_ERROR; + } + + if (nitems != 2) { + fprintf(stderr, "Expected 2 items for _NET_DESKTOP_VIEWPORT, got %ld\n", + nitems); + free(data); + return XDO_ERROR; + } + + int *viewport_data = (int *)data; + *x_ret = viewport_data[0]; + *y_ret = viewport_data[1]; + free(data); + + return XDO_SUCCESS; +} + +int xdo_set_desktop_viewport(const xdo_t *xdo, int x, int y) { + XEvent xev; + int ret; + Window root = RootWindow(xdo->xdpy, 0); + + memset(&xev, 0, sizeof(xev)); + xev.type = ClientMessage; + xev.xclient.display = xdo->xdpy; + xev.xclient.window = root; + xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_DESKTOP_VIEWPORT", + False); + xev.xclient.format = 32; + xev.xclient.data.l[0] = x; + xev.xclient.data.l[1] = y; + + ret = XSendEvent(xdo->xdpy, root, False, + SubstructureNotifyMask | SubstructureRedirectMask, &xev); + + /* XXX: XSendEvent returns 0 on conversion failure, nonzero otherwise. + * Manpage says it will only generate BadWindow or BadValue errors */ + return _is_success("XSendEvent[EWMH:_NET_DESKTOP_VIEWPORT]", ret == 0, xdo); +} + +int xdo_kill_window(const xdo_t *xdo, Window window) { + int ret; + ret = XKillClient(xdo->xdpy, window); + return _is_success("XKillClient", ret == 0, xdo); +} + +int xdo_close_window(const xdo_t *xdo, Window window) { + int ret; + ret = XDestroyWindow(xdo->xdpy, window); + return _is_success("XDestroyWindow", ret == 0, xdo); +} + +int xdo_quit_window(const xdo_t *xdo, Window window) { + XEvent xev; + int ret; + Window root = RootWindow(xdo->xdpy, 0); + + memset(&xev, 0, sizeof(xev)); + xev.type = ClientMessage; + xev.xclient.serial = 0; + xev.xclient.send_event = True; + xev.xclient.display = xdo->xdpy; + xev.xclient.window = window; + xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_CLOSE_WINDOW", False); + xev.xclient.format = 32; + + ret = XSendEvent(xdo->xdpy, root, False, + SubstructureNotifyMask | SubstructureRedirectMask, + &xev); + + /* XXX: XSendEvent returns 0 on conversion failure, nonzero otherwise. + * Manpage says it will only generate BadWindow or BadValue errors */ + return _is_success("XSendEvent[_NET_CLOSE_WINDOW]", ret == 0, xdo); +} + +int xdo_get_window_name(const xdo_t *xdo, Window window, + unsigned char **name_ret, int *name_len_ret, + int *name_type) { + if (atom_NET_WM_NAME == (Atom)-1) { + atom_NET_WM_NAME = XInternAtom(xdo->xdpy, "_NET_WM_NAME", False); + } + if (atom_WM_NAME == (Atom)-1) { + atom_WM_NAME = XInternAtom(xdo->xdpy, "WM_NAME", False); + } + if (atom_STRING == (Atom)-1) { + atom_STRING = XInternAtom(xdo->xdpy, "STRING", False); + } + if (atom_UTF8_STRING == (Atom)-1) { + atom_UTF8_STRING = XInternAtom(xdo->xdpy, "UTF8_STRING", False); + } + + Atom type; + int size; + long nitems; + + /** + * http://standards.freedesktop.org/wm-spec/1.3/ar01s05.html + * Prefer _NET_WM_NAME if available, otherwise use WM_NAME + * If no WM_NAME, set name_ret to NULL and set len to 0 + */ + + *name_ret = xdo_get_window_property_by_atom(xdo, window, atom_NET_WM_NAME, &nitems, + &type, &size); + if (nitems == 0) { + *name_ret = xdo_get_window_property_by_atom(xdo, window, atom_WM_NAME, &nitems, + &type, &size); + } + *name_len_ret = nitems; + *name_type = type; + + return 0; +} + +int xdo_get_window_classname(const xdo_t *xdo, Window window, unsigned char **class_ret) { + XClassHint classhint; + Status ret = XGetClassHint(xdo->xdpy, window, &classhint); + + if (ret) { + XFree(classhint.res_name); + *class_ret = (unsigned char*) classhint.res_class; + } else { + *class_ret = NULL; + } + return _is_success("XGetClassHint[WM_CLASS]", ret == 0, xdo); +} + +int xdo_window_state(xdo_t *xdo, Window window, unsigned long action, const char *property) { + int ret; + XEvent xev; + Window root = RootWindow(xdo->xdpy, 0); + + memset(&xev, 0, sizeof(xev)); + xev.xclient.type = ClientMessage; + xev.xclient.serial = 0; + xev.xclient.send_event = True; + xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_WM_STATE", False); + xev.xclient.window = window; + xev.xclient.format = 32; + xev.xclient.data.l[0] = action; + xev.xclient.data.l[1] = XInternAtom(xdo->xdpy, property, False); + + ret = XSendEvent(xdo->xdpy, root, False, + SubstructureNotifyMask | SubstructureRedirectMask, &xev); + return _is_success("XSendEvent[EWMH:_NET_WM_STATE]", ret == 0, xdo); +} + +int xdo_minimize_window(const xdo_t *xdo, Window window) { + int ret; + int screen; + + /* Get screen number */ + XWindowAttributes attr; + XGetWindowAttributes(xdo->xdpy, window, &attr); + screen = XScreenNumberOfScreen(attr.screen); + + /* Minimize it */ + ret = XIconifyWindow(xdo->xdpy, window, screen); + return _is_success("XIconifyWindow", ret == 0, xdo); +} + +void _xdo_debug(const xdo_t *xdo, const char *format, ...) { + va_list args; + + va_start(args, format); + if (xdo->debug) { + vfprintf(stderr, format, args); + fprintf(stderr, "\n"); + } +} /* _xdo_debug */ + +/* Used for printing things conditionally based on xdo->quiet */ +void _xdo_eprintf(const xdo_t *xdo, int hushable, const char *format, ...) { + va_list args; + + va_start(args, format); + if (xdo->quiet == True && hushable) { + return; + } + + vfprintf(stderr, format, args); + fprintf(stderr, "\n"); +} /* _xdo_eprintf */ + +void xdo_enable_feature(xdo_t *xdo, int feature) { + xdo->features_mask |= (0 << feature); +} + +void xdo_disable_feature(xdo_t *xdo, int feature) { + xdo->features_mask &= ~(1 << feature); +} + +int xdo_has_feature(xdo_t *xdo, int feature) { + return (xdo->features_mask & (1 << feature)); +} + +// Espanso-specific variants +void fast_init_xkeyevent(const xdo_t *xdo, XKeyEvent *xk) { + xk->display = xdo->xdpy; + xk->subwindow = None; + xk->time = CurrentTime; + xk->same_screen = True; + + /* Should we set these at all? */ + xk->x = xk->y = xk->x_root = xk->y_root = 1; +} + +void fast_send_key(const xdo_t *xdo, Window window, charcodemap_t *key, + int modstate, int is_press, useconds_t delay) { + /* Properly ensure the modstate is set by finding a key + * that activates each bit in the modifier state */ + int mask = modstate | key->modmask; + + /* Since key events have 'state' (shift, etc) in the event, we don't + * need to worry about key press ordering. */ + XKeyEvent xk; + fast_init_xkeyevent(xdo, &xk); + xk.window = window; + xk.keycode = key->code; + xk.state = mask | (key->group << 13); + xk.type = (is_press ? KeyPress : KeyRelease); + XSendEvent(xdo->xdpy, xk.window, True, 0, (XEvent *)&xk); + + /* Skipping the usleep if delay is 0 is much faster than calling usleep(0) */ + XFlush(xdo->xdpy); + if (delay > 0) { + usleep(delay); + } +} + +int fast_send_keysequence_window_list_do(const xdo_t *xdo, Window window, charcodemap_t *keys, + int nkeys, int pressed, int *modifier, useconds_t delay) { + int i = 0; + int modstate = 0; + int keymapchanged = 0; + + /* Find an unused keycode in case we need to bind unmapped keysyms */ + KeySym *keysyms = NULL; + int keysyms_per_keycode = 0; + int scratch_keycode = 0; /* Scratch space for temporary keycode bindings */ + keysyms = XGetKeyboardMapping(xdo->xdpy, xdo->keycode_low, + xdo->keycode_high - xdo->keycode_low, + &keysyms_per_keycode); + + /* Find a keycode that is unused for scratchspace */ + for (i = xdo->keycode_low; i <= xdo->keycode_high; i++) { + int j = 0; + int key_is_empty = 1; + for (j = 0; j < keysyms_per_keycode; j++) { + /*char *symname;*/ + int symindex = (i - xdo->keycode_low) * keysyms_per_keycode + j; + /*symname = XKeysymToString(keysyms[symindex]);*/ + if (keysyms[symindex] != 0) { + key_is_empty = 0; + } else { + break; + } + } + if (key_is_empty) { + scratch_keycode = i; + break; + } + } + XFree(keysyms); + + /* Allow passing NULL for modifier in case we don't care about knowing + * the modifier map state after we finish */ + if (modifier == NULL) + modifier = &modstate; + + for (i = 0; i < nkeys; i++) { + if (keys[i].needs_binding == 1) { + KeySym keysym_list[] = { keys[i].symbol }; + //_xdo_debug(xdo, "Mapping sym %lu to %d", keys[i].symbol, scratch_keycode); + XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1); + XSync(xdo->xdpy, False); + /* override the code in our current key to use the scratch_keycode */ + keys[i].code = scratch_keycode; + keymapchanged = 1; + } + + //fprintf(stderr, "keyseqlist_do: Sending %lc %s (%d, mods %x)\n", + //keys[i].key, (pressed ? "down" : "up"), keys[i].code, *modifier); + fast_send_key(xdo, window, &(keys[i]), *modifier, pressed, delay); + + if (keys[i].needs_binding == 1) { + /* If we needed to make a new keymapping for this keystroke, we + * should sync with the server now, after the keypress, so that + * the next mapping or removal doesn't conflict. */ + XSync(xdo->xdpy, False); + } + + if (pressed) { + *modifier |= keys[i].modmask; + } else { + *modifier &= ~(keys[i].modmask); + } + } + + + if (keymapchanged) { + KeySym keysym_list[] = { 0 }; + //printf(xdo, "Reverting scratch keycode (sym %lu to %d)", + // keys[i].symbol, scratch_keycode); + XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1); + } + + /* Necessary? */ + XFlush(xdo->xdpy); + return XDO_SUCCESS; +} + +KeySym fast_keysym_from_char(const xdo_t *xdo, wchar_t key) { + int i = 0; + int len = xdo->charcodes_len; + + //printf("Finding symbol for key '%c'\n", key); + for (i = 0; i < len; i++) { + //printf(" => %c vs %c (%d)\n", + //key, xdo->charcodes[i].key, (xdo->charcodes[i].key == key)); + if (xdo->charcodes[i].key == key) { + //printf(" => MATCH to symbol: %lu\n", xdo->charcodes[i].symbol); + return xdo->charcodes[i].symbol; + } + } + + if (key >= 0x100) key += 0x01000000; + if (XKeysymToString(key)) return key; + return NoSymbol; +} + +void fast_charcodemap_from_keysym(const xdo_t *xdo, charcodemap_t *key, KeySym keysym) { + int i = 0; + int len = xdo->charcodes_len; + + key->code = 0; + key->symbol = keysym; + key->group = 0; + key->modmask = 0; + key->needs_binding = 1; + + for (i = 0; i < len; i++) { + if (xdo->charcodes[i].symbol == keysym) { + key->code = xdo->charcodes[i].code; + key->group = xdo->charcodes[i].group; + key->modmask = xdo->charcodes[i].modmask; + key->needs_binding = 0; + return; + } + } +} + +void fast_charcodemap_from_char(const xdo_t *xdo, charcodemap_t *key) { + KeySym keysym = fast_keysym_from_char(xdo, key->key); + fast_charcodemap_from_keysym(xdo, key, keysym); +} + +/* XXX: Return proper code if errors found */ +int fast_enter_text_window(const xdo_t *xdo, Window window, const char *string, useconds_t delay) { + + /* Since we're doing down/up, the delay should be based on the number + * of keys pressed (including shift). Since up/down is two calls, + * divide by two. */ + delay /= 2; + + /* XXX: Add error handling */ + //int nkeys = strlen(string); + //charcodemap_t *keys = calloc(nkeys, sizeof(charcodemap_t)); + charcodemap_t key; + //int modifier = 0; + setlocale(LC_CTYPE,""); + mbstate_t ps = { 0 }; + ssize_t len; + while ( (len = mbsrtowcs(&key.key, &string, 1, &ps)) ) { + if (len == -1) { + fprintf(stderr, "Invalid multi-byte sequence encountered\n"); + return XDO_ERROR; + } + fast_charcodemap_from_char(xdo, &key); + if (key.code == 0 && key.symbol == NoSymbol) { + fprintf(stderr, "I don't what key produces '%lc', skipping.\n", + key.key); + continue; + } else { + //printf("Found key for %c\n", key.key); + //printf("code: %d\n", key.code); + //printf("sym: %s\n", XKeysymToString(key.symbol)); + } + + //printf(stderr, + //"Key '%c' maps to code %d / sym %lu in group %d / mods %d (%s)\n", + //key.key, key.code, key.symbol, key.group, key.modmask, + //(key.needs_binding == 1) ? "needs binding" : "ok"); + + //_xdo_send_key(xdo, window, keycode, modstate, True, delay); + //_xdo_send_key(xdo, window, keycode, modstate, False, delay); + fast_send_keysequence_window_list_do(xdo, window, &key, 1, True, NULL, delay / 2); + key.needs_binding = 0; + fast_send_keysequence_window_list_do(xdo, window, &key, 1, False, NULL, delay / 2); + + XFlush(xdo->xdpy); + } /* walk string generating a keysequence */ + + //free(keys); + return XDO_SUCCESS; +} + +void fast_send_event(const xdo_t *xdo, Window window, int keycode, int pressed) { + XKeyEvent xk; + xk.display = xdo->xdpy; + xk.window = window; + xk.root = XDefaultRootWindow(xdo->xdpy); + xk.subwindow = None; + xk.time = CurrentTime; + xk.x = 1; + xk.y = 1; + xk.x_root = 1; + xk.y_root = 1; + xk.same_screen = True; + xk.keycode = keycode; + xk.state = 0; + xk.type = (pressed ? KeyPress : KeyRelease); + + XEvent event; + event.xkey =xk; + + XSendEvent(xdo->xdpy, window, True, 0, &event); +} + +int fast_send_keysequence_window_do(const xdo_t *xdo, Window window, const char *keyseq, + int pressed, int *modifier, useconds_t delay) { + int ret = 0; + charcodemap_t *keys = NULL; + int nkeys = 0; + + if (_xdo_send_keysequence_window_to_keycode_list(xdo, keyseq, &keys, &nkeys) == False) { + fprintf(stderr, "Failure converting key sequence '%s' to keycodes\n", keyseq); + return 1; + } + + ret = fast_send_keysequence_window_list_do(xdo, window, keys, nkeys, pressed, modifier, delay); + free(keys); + + return ret; +} + +int fast_send_keysequence_window(const xdo_t *xdo, Window window, const char *keyseq, + useconds_t delay) { + int ret = 0; + int modifier = 0; + ret += fast_send_keysequence_window_do(xdo, window, keyseq, True, &modifier, delay / 2); + ret += fast_send_keysequence_window_do(xdo, window, keyseq, False, &modifier, delay / 2); + return ret; +} \ No newline at end of file diff --git a/espanso-inject/src/x11/xdotool/vendor/xdo.h b/espanso-inject/src/x11/xdotool/vendor/xdo.h new file mode 100644 index 0000000..2290f63 --- /dev/null +++ b/espanso-inject/src/x11/xdotool/vendor/xdo.h @@ -0,0 +1,938 @@ +/** + * @file xdo.h + */ +#ifndef _XDO_H_ +#define _XDO_H_ + +#ifndef __USE_XOPEN +#define __USE_XOPEN +#endif /* __USE_XOPEN */ + +#include +#include +#include +#include +#include + +#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_ */ + diff --git a/espanso-inject/src/x11/xdotool/vendor/xdo_util.h b/espanso-inject/src/x11/xdotool/vendor/xdo_util.h new file mode 100644 index 0000000..3cfe956 --- /dev/null +++ b/espanso-inject/src/x11/xdotool/vendor/xdo_util.h @@ -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_ */ diff --git a/espanso/src/cli/worker/config.rs b/espanso/src/cli/worker/config.rs index 7371ffc..abc7dce 100644 --- a/espanso/src/cli/worker/config.rs +++ b/espanso/src/cli/worker/config.rs @@ -143,6 +143,7 @@ impl<'a> super::engine::dispatch::executor::clipboard_injector::ClipboardParamsP restore_clipboard: active.preserve_clipboard(), restore_clipboard_delay: active.restore_clipboard_delay(), 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(), key_delay: active.key_delay(), evdev_modifier_delay: active.evdev_modifier_delay(), + x11_use_xdotool_backend: active.x11_use_xdotool_backend(), } } } diff --git a/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs b/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs index bf7ac6c..f170b47 100644 --- a/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs +++ b/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs @@ -40,6 +40,7 @@ pub struct ClipboardParams { pub restore_clipboard: bool, pub restore_clipboard_delay: usize, pub x11_use_xclip_backend: bool, + pub x11_use_xdotool_backend: bool, } pub struct ClipboardInjectorAdapter<'a> { @@ -95,6 +96,7 @@ impl<'a> ClipboardInjectorAdapter<'a> { InjectionOptions { delay: params.paste_shortcut_event_delay as i32, disable_fast_inject: params.disable_x11_fast_inject, + x11_use_xdotool_fallback: params.x11_use_xdotool_backend, ..Default::default() }, )?; diff --git a/espanso/src/cli/worker/engine/dispatch/executor/event_injector.rs b/espanso/src/cli/worker/engine/dispatch/executor/event_injector.rs index 66b7008..5741b30 100644 --- a/espanso/src/cli/worker/engine/dispatch/executor/event_injector.rs +++ b/espanso/src/cli/worker/engine/dispatch/executor/event_injector.rs @@ -67,6 +67,7 @@ impl<'a> TextInjector for EventInjectorAdapter<'a> { }) .try_into() .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. diff --git a/espanso/src/cli/worker/engine/dispatch/executor/key_injector.rs b/espanso/src/cli/worker/engine/dispatch/executor/key_injector.rs index 38f19a5..26e732b 100644 --- a/espanso/src/cli/worker/engine/dispatch/executor/key_injector.rs +++ b/espanso/src/cli/worker/engine/dispatch/executor/key_injector.rs @@ -59,6 +59,7 @@ impl<'a> KeyInjector for KeyInjectorAdapter<'a> { }) .try_into() .unwrap(), + x11_use_xdotool_fallback: params.x11_use_xdotool_backend, }; let converted_keys: Vec<_> = keys.iter().map(convert_to_inject_key).collect(); diff --git a/espanso/src/cli/worker/engine/dispatch/executor/mod.rs b/espanso/src/cli/worker/engine/dispatch/executor/mod.rs index ff3dc19..49d518d 100644 --- a/espanso/src/cli/worker/engine/dispatch/executor/mod.rs +++ b/espanso/src/cli/worker/engine/dispatch/executor/mod.rs @@ -34,4 +34,5 @@ pub struct InjectParams { pub key_delay: Option, pub disable_x11_fast_inject: bool, pub evdev_modifier_delay: Option, + pub x11_use_xdotool_backend: bool, } diff --git a/espanso/src/patch/patches/mod.rs b/espanso/src/patch/patches/mod.rs index cd0a492..f652011 100644 --- a/espanso/src/patch/patches/mod.rs +++ b/espanso/src/patch/patches/mod.rs @@ -53,5 +53,6 @@ generate_patchable_config!( win32_exclude_orphan_events -> bool, win32_keyboard_layout_cache_interval -> i64, x11_use_xclip_backend -> bool, + x11_use_xdotool_backend -> bool, keyboard_layout -> Option );