- uinput injection is now primary on X11 (XTest fallback) - X11 XTest keycode offset +8 fixed for all send_keycode paths - Window switch detection on every keystroke (no more gap > 100ms guard) - Telex greyed out in tray with '(next version)' label - Flatpak and AppImage removed; only .deb packaging - All Cargo.toml versions bumped to 0.1.6
558 lines
19 KiB
Rust
558 lines
19 KiB
Rust
// SPDX-License-Identifier: MIT
|
|
use super::inject::{InjectResult, KeyInjector};
|
|
use std::cell::RefCell;
|
|
use std::ffi::{c_char, c_int, c_void};
|
|
|
|
type Display = c_void;
|
|
type Window = u64;
|
|
type Atom = u64;
|
|
type Time = u64;
|
|
|
|
extern "C" {
|
|
fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void;
|
|
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
|
|
fn dlclose(handle: *mut c_void) -> c_int;
|
|
}
|
|
|
|
const CURRENT_TIME: Time = 0;
|
|
const PROP_MODE_REPLACE: c_int = 0;
|
|
const NO_EVENT_MASK: i64 = 0;
|
|
const COPY_FROM_PARENT: Window = 0;
|
|
|
|
const SELECTION_REQUEST: c_int = 30;
|
|
const SELECTION_NOTIFY: c_int = 31;
|
|
|
|
struct X11Lib {
|
|
x11_handle: *mut c_void,
|
|
xtst_handle: *mut c_void,
|
|
|
|
x_open_display: unsafe extern "C" fn(*const c_char) -> *mut Display,
|
|
x_close_display: unsafe extern "C" fn(*mut Display) -> c_int,
|
|
x_default_root_window: unsafe extern "C" fn(*mut Display) -> Window,
|
|
x_flush: unsafe extern "C" fn(*mut Display) -> c_int,
|
|
x_test_fake_key_event: unsafe extern "C" fn(*mut Display, u32, c_int, u64) -> c_int,
|
|
x_intern_atom: unsafe extern "C" fn(*mut Display, *const c_char, c_int) -> Atom,
|
|
x_set_selection_owner: unsafe extern "C" fn(*mut Display, Atom, Window, Time) -> c_int,
|
|
x_change_property: unsafe extern "C" fn(*mut Display, Window, Atom, Atom, c_int, c_int, *const c_void, c_int) -> c_int,
|
|
x_send_event: unsafe extern "C" fn(*mut Display, Window, c_int, i64, *const c_void) -> c_int,
|
|
x_create_simple_window: unsafe extern "C" fn(*mut Display, Window, c_int, c_int, c_int, c_int, c_int, Atom, Atom) -> Window,
|
|
x_map_window: unsafe extern "C" fn(*mut Display, Window) -> c_int,
|
|
x_destroy_window: unsafe extern "C" fn(*mut Display, Window) -> c_int,
|
|
x_pending: unsafe extern "C" fn(*mut Display) -> c_int,
|
|
x_next_event: unsafe extern "C" fn(*mut Display, *mut XEvent),
|
|
}
|
|
|
|
impl X11Lib {
|
|
fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
|
unsafe {
|
|
let x11_paths = [
|
|
b"libX11.so.6\0".as_ptr() as *const c_char,
|
|
b"libX11.so\0".as_ptr() as *const c_char,
|
|
];
|
|
let mut x11_handle = std::ptr::null_mut();
|
|
for path in x11_paths {
|
|
x11_handle = dlopen(path, 1);
|
|
if !x11_handle.is_null() {
|
|
break;
|
|
}
|
|
}
|
|
if x11_handle.is_null() {
|
|
return Err("Failed to load libX11.so.6".into());
|
|
}
|
|
|
|
let xtst_paths = [
|
|
b"libXtst.so.6\0".as_ptr() as *const c_char,
|
|
b"libXtst.so\0".as_ptr() as *const c_char,
|
|
];
|
|
let mut xtst_handle = std::ptr::null_mut();
|
|
for path in xtst_paths {
|
|
xtst_handle = dlopen(path, 1);
|
|
if !xtst_handle.is_null() {
|
|
break;
|
|
}
|
|
}
|
|
if xtst_handle.is_null() {
|
|
dlclose(x11_handle);
|
|
return Err("Failed to load libXtst.so.6".into());
|
|
}
|
|
|
|
macro_rules! sym {
|
|
($handle:expr, $name:expr) => {
|
|
std::mem::transmute(dlsym($handle, concat!($name, "\0").as_ptr() as *const c_char))
|
|
};
|
|
}
|
|
|
|
let x_open_display = sym!(x11_handle, "XOpenDisplay");
|
|
let x_close_display = sym!(x11_handle, "XCloseDisplay");
|
|
let x_default_root_window = sym!(x11_handle, "XDefaultRootWindow");
|
|
let x_flush = sym!(x11_handle, "XFlush");
|
|
let x_intern_atom = sym!(x11_handle, "XInternAtom");
|
|
let x_set_selection_owner = sym!(x11_handle, "XSetSelectionOwner");
|
|
let x_change_property = sym!(x11_handle, "XChangeProperty");
|
|
let x_send_event = sym!(x11_handle, "XSendEvent");
|
|
let x_create_simple_window = sym!(x11_handle, "XCreateSimpleWindow");
|
|
let x_map_window = sym!(x11_handle, "XMapWindow");
|
|
let x_destroy_window = sym!(x11_handle, "XDestroyWindow");
|
|
let x_pending = sym!(x11_handle, "XPending");
|
|
let x_next_event = sym!(x11_handle, "XNextEvent");
|
|
let x_test_fake_key_event = sym!(xtst_handle, "XTestFakeKeyEvent");
|
|
|
|
Ok(Self {
|
|
x11_handle,
|
|
xtst_handle,
|
|
x_open_display,
|
|
x_close_display,
|
|
x_default_root_window,
|
|
x_flush,
|
|
x_test_fake_key_event,
|
|
x_intern_atom,
|
|
x_set_selection_owner,
|
|
x_change_property,
|
|
x_send_event,
|
|
x_create_simple_window,
|
|
x_map_window,
|
|
x_destroy_window,
|
|
x_pending,
|
|
x_next_event,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for X11Lib {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
dlclose(self.x11_handle);
|
|
dlclose(self.xtst_handle);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn char_to_keycode(ch: char) -> Option<(u32, bool)> {
|
|
match ch {
|
|
'a' => Some((30, false)),
|
|
'b' => Some((48, false)),
|
|
'c' => Some((46, false)),
|
|
'd' => Some((32, false)),
|
|
'e' => Some((18, false)),
|
|
'f' => Some((33, false)),
|
|
'g' => Some((34, false)),
|
|
'h' => Some((35, false)),
|
|
'i' => Some((23, false)),
|
|
'j' => Some((36, false)),
|
|
'k' => Some((37, false)),
|
|
'l' => Some((38, false)),
|
|
'm' => Some((50, false)),
|
|
'n' => Some((49, false)),
|
|
'o' => Some((24, false)),
|
|
'p' => Some((25, false)),
|
|
'q' => Some((16, false)),
|
|
'r' => Some((19, false)),
|
|
's' => Some((31, false)),
|
|
't' => Some((20, false)),
|
|
'u' => Some((22, false)),
|
|
'v' => Some((47, false)),
|
|
'w' => Some((17, false)),
|
|
'x' => Some((45, false)),
|
|
'y' => Some((21, false)),
|
|
'z' => Some((44, false)),
|
|
'A' => Some((30, true)),
|
|
'B' => Some((48, true)),
|
|
'C' => Some((46, true)),
|
|
'D' => Some((32, true)),
|
|
'E' => Some((18, true)),
|
|
'F' => Some((33, true)),
|
|
'G' => Some((34, true)),
|
|
'H' => Some((35, true)),
|
|
'I' => Some((23, true)),
|
|
'J' => Some((36, true)),
|
|
'K' => Some((37, true)),
|
|
'L' => Some((38, true)),
|
|
'M' => Some((50, true)),
|
|
'N' => Some((49, true)),
|
|
'O' => Some((24, true)),
|
|
'P' => Some((25, true)),
|
|
'Q' => Some((16, true)),
|
|
'R' => Some((19, true)),
|
|
'S' => Some((31, true)),
|
|
'T' => Some((20, true)),
|
|
'U' => Some((22, true)),
|
|
'V' => Some((47, true)),
|
|
'W' => Some((17, true)),
|
|
'X' => Some((45, true)),
|
|
'Y' => Some((21, true)),
|
|
'Z' => Some((44, true)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct XSelectionRequestEvent {
|
|
_type: c_int,
|
|
_serial: u64,
|
|
_send_event: c_int,
|
|
_display: *mut Display,
|
|
owner: Window,
|
|
requestor: Window,
|
|
selection: Atom,
|
|
target: Atom,
|
|
property: Atom,
|
|
time: Time,
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct XEvent {
|
|
_bytes: [u8; 192],
|
|
}
|
|
|
|
impl XEvent {
|
|
fn event_type(&self) -> c_int {
|
|
unsafe { std::ptr::read_unaligned(self._bytes.as_ptr() as *const c_int) }
|
|
}
|
|
}
|
|
|
|
pub struct X11Injector {
|
|
lib: X11Lib,
|
|
display: *mut Display,
|
|
clipboard_window: Window,
|
|
atom_clipboard: Atom,
|
|
atom_utf8: Atom,
|
|
atom_targets: Atom,
|
|
atom_string: Atom,
|
|
clipboard_text: RefCell<String>,
|
|
}
|
|
|
|
unsafe impl Send for X11Injector {}
|
|
unsafe impl Sync for X11Injector {}
|
|
|
|
impl X11Injector {
|
|
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
|
let lib = X11Lib::new()?;
|
|
unsafe {
|
|
let display = (lib.x_open_display)(std::ptr::null());
|
|
if display.is_null() {
|
|
return Err("Cannot open X11 display. Is DISPLAY set?".into());
|
|
}
|
|
let root = (lib.x_default_root_window)(display);
|
|
|
|
let atom_clipboard = (lib.x_intern_atom)(display, b"CLIPBOARD\0".as_ptr() as *const c_char, 0);
|
|
let atom_utf8 = (lib.x_intern_atom)(display, b"UTF8_STRING\0".as_ptr() as *const c_char, 0);
|
|
let atom_targets = (lib.x_intern_atom)(display, b"TARGETS\0".as_ptr() as *const c_char, 0);
|
|
let atom_string = (lib.x_intern_atom)(display, b"STRING\0".as_ptr() as *const c_char, 0);
|
|
|
|
// Create a small hidden window for clipboard ownership
|
|
let clipboard_window = (lib.x_create_simple_window)(
|
|
display, root, 0, 0, 1, 1, 0, COPY_FROM_PARENT, COPY_FROM_PARENT,
|
|
);
|
|
(lib.x_map_window)(display, clipboard_window);
|
|
|
|
Ok(Self {
|
|
lib,
|
|
display,
|
|
clipboard_window,
|
|
atom_clipboard,
|
|
atom_utf8,
|
|
atom_targets,
|
|
atom_string,
|
|
clipboard_text: RefCell::new(String::new()),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn set_clipboard_text(&self, text: &str) {
|
|
*self.clipboard_text.borrow_mut() = text.to_string();
|
|
unsafe {
|
|
// Set the text as a property on our clipboard window
|
|
(self.lib.x_change_property)(
|
|
self.display,
|
|
self.clipboard_window,
|
|
self.atom_clipboard,
|
|
self.atom_utf8,
|
|
8, // 8-bit format
|
|
PROP_MODE_REPLACE,
|
|
text.as_ptr() as *const c_void,
|
|
text.len() as c_int,
|
|
);
|
|
|
|
// Also set as STRING (for apps that don't understand UTF8_STRING)
|
|
(self.lib.x_change_property)(
|
|
self.display,
|
|
self.clipboard_window,
|
|
self.atom_clipboard,
|
|
self.atom_string,
|
|
8,
|
|
PROP_MODE_REPLACE,
|
|
text.as_ptr() as *const c_void,
|
|
text.len() as c_int,
|
|
);
|
|
|
|
// Claim the CLIPBOARD selection
|
|
(self.lib.x_set_selection_owner)(
|
|
self.display,
|
|
self.atom_clipboard,
|
|
self.clipboard_window,
|
|
CURRENT_TIME,
|
|
);
|
|
|
|
(self.lib.x_flush)(self.display);
|
|
}
|
|
}
|
|
|
|
fn handle_pending_events(&self) {
|
|
unsafe {
|
|
while (self.lib.x_pending)(self.display) > 0 {
|
|
let mut event: XEvent = std::mem::zeroed();
|
|
(self.lib.x_next_event)(self.display, &mut event);
|
|
if event.event_type() == SELECTION_REQUEST {
|
|
let req = &*(&event as *const XEvent as *const XSelectionRequestEvent);
|
|
self.handle_selection_request(req);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_selection_request(&self, req: &XSelectionRequestEvent) {
|
|
eprintln!(
|
|
"[vietc] SelectionRequest: target={} requestor={}",
|
|
req.target, req.requestor
|
|
);
|
|
|
|
// Determine what property to use for the response
|
|
let property = if req.property == 0 {
|
|
req.target // Use the target atom as property if property is None
|
|
} else {
|
|
req.property
|
|
};
|
|
|
|
unsafe {
|
|
if req.target == self.atom_targets {
|
|
// Respond with supported targets: TARGETS, UTF8_STRING, STRING
|
|
let targets: [Atom; 3] = [self.atom_targets, self.atom_utf8, self.atom_string];
|
|
(self.lib.x_change_property)(
|
|
self.display,
|
|
req.requestor,
|
|
property,
|
|
self.atom_targets,
|
|
32, // 32-bit format
|
|
PROP_MODE_REPLACE,
|
|
targets.as_ptr() as *const c_void,
|
|
targets.len() as c_int,
|
|
);
|
|
} else if req.target == self.atom_utf8 || req.target == self.atom_string {
|
|
// Respond with the actual clipboard text
|
|
(self.lib.x_change_property)(
|
|
self.display,
|
|
req.requestor,
|
|
property,
|
|
req.target,
|
|
8, // 8-bit format
|
|
PROP_MODE_REPLACE,
|
|
self.clipboard_text.borrow().as_ptr() as *const c_void,
|
|
self.clipboard_text.borrow().len() as c_int,
|
|
);
|
|
}
|
|
|
|
// Send SelectionNotify to inform the requestor
|
|
let mut notify = std::mem::zeroed::<XSelectionNotifyEvent>();
|
|
notify._type = SELECTION_NOTIFY as c_int;
|
|
notify._display = self.display;
|
|
notify.requestor = req.requestor;
|
|
notify.selection = req.selection;
|
|
notify.target = req.target;
|
|
notify.property = if req.target == self.atom_targets
|
|
|| req.target == self.atom_utf8
|
|
|| req.target == self.atom_string
|
|
{
|
|
property
|
|
} else {
|
|
0 // PropertyNone = unsupported target
|
|
};
|
|
notify.time = req.time;
|
|
|
|
(self.lib.x_send_event)(
|
|
self.display,
|
|
req.requestor,
|
|
0, // propagate = False
|
|
NO_EVENT_MASK,
|
|
¬ify as *const XSelectionNotifyEvent as *const c_void,
|
|
);
|
|
(self.lib.x_flush)(self.display);
|
|
}
|
|
}
|
|
|
|
fn paste_via_clipboard(&self, backspaces: usize, text: &str) -> bool {
|
|
// Set clipboard text directly via X11
|
|
self.set_clipboard_text(text);
|
|
|
|
// Handle any pending SelectionRequest events that may have queued
|
|
// (unlikely at this point, but be safe)
|
|
self.handle_pending_events();
|
|
|
|
// Send backspaces via XTest (X11 keycode 22 = backspace)
|
|
if backspaces > 0 {
|
|
for _ in 0..backspaces {
|
|
self.send_keycode(22, false);
|
|
}
|
|
}
|
|
|
|
// Send Ctrl+V via XTest to paste (evdev codes + 8 = X11)
|
|
unsafe {
|
|
(self.lib.x_test_fake_key_event)(self.display, 29 + 8, 1, 0); // Ctrl_L press
|
|
(self.lib.x_test_fake_key_event)(self.display, 47 + 8, 1, 0); // V press
|
|
(self.lib.x_test_fake_key_event)(self.display, 47 + 8, 0, 0); // V release
|
|
(self.lib.x_test_fake_key_event)(self.display, 29 + 8, 0, 0); // Ctrl_L release
|
|
(self.lib.x_flush)(self.display);
|
|
}
|
|
|
|
// Handle SelectionRequest events that come from the paste target
|
|
// Process events with a short spin loop (up to ~50ms)
|
|
for _ in 0..4 {
|
|
// Brief sleep to let X11 events propagate
|
|
std::thread::sleep(std::time::Duration::from_millis(5));
|
|
self.handle_pending_events();
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
fn send_keycode(&self, evdev_keycode: u32, shift: bool) {
|
|
let x11 = evdev_keycode + 8;
|
|
unsafe {
|
|
if shift {
|
|
(self.lib.x_test_fake_key_event)(self.display, 42 + 8, 1, 0); // Shift_L
|
|
}
|
|
(self.lib.x_test_fake_key_event)(self.display, x11, 1, 0);
|
|
(self.lib.x_test_fake_key_event)(self.display, x11, 0, 0);
|
|
if shift {
|
|
(self.lib.x_test_fake_key_event)(self.display, 42 + 8, 0, 0);
|
|
}
|
|
(self.lib.x_flush)(self.display);
|
|
}
|
|
}
|
|
|
|
fn send_unicode_via_xdotool(&self, ch: char) {
|
|
let s = ch.to_string();
|
|
|
|
// Try ydotool first (uinput-based, works as root)
|
|
let ydotool_ok = std::process::Command::new("ydotool")
|
|
.args(["type", &s])
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false);
|
|
if ydotool_ok {
|
|
return;
|
|
}
|
|
|
|
// Try xdotool
|
|
let xdotool_ok = std::process::Command::new("xdotool")
|
|
.args(["type", "--clearmodifiers", &s])
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false);
|
|
if xdotool_ok {
|
|
return;
|
|
}
|
|
|
|
// Fallback: direct X11 clipboard + Ctrl+V
|
|
self.paste_via_clipboard(0, &s);
|
|
}
|
|
}
|
|
|
|
impl Drop for X11Injector {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
if self.clipboard_window != 0 && !self.display.is_null() {
|
|
(self.lib.x_destroy_window)(self.display, self.clipboard_window);
|
|
}
|
|
(self.lib.x_close_display)(self.display);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct XSelectionNotifyEvent {
|
|
_type: c_int,
|
|
_serial: u64,
|
|
_send_event: c_int,
|
|
_display: *mut Display,
|
|
requestor: Window,
|
|
selection: Atom,
|
|
target: Atom,
|
|
property: Atom,
|
|
time: Time,
|
|
}
|
|
|
|
impl KeyInjector for X11Injector {
|
|
fn send_key_event(&self, keycode: u16, value: i32) -> InjectResult {
|
|
// X11 keycodes = Linux evdev keycodes + 8
|
|
let x11_keycode = keycode as u32 + 8;
|
|
unsafe {
|
|
(self.lib.x_test_fake_key_event)(self.display, x11_keycode, value, 0);
|
|
(self.lib.x_flush)(self.display);
|
|
}
|
|
InjectResult::Success
|
|
}
|
|
|
|
fn send_backspace(&self) -> InjectResult {
|
|
self.send_keycode(22, false); // X11 keycode 22 = backspace
|
|
InjectResult::Success
|
|
}
|
|
|
|
fn send_char(&self, ch: char) -> InjectResult {
|
|
if let Some((keycode, shift)) = char_to_keycode(ch) {
|
|
self.send_keycode(keycode, shift);
|
|
InjectResult::Success
|
|
} else {
|
|
self.send_unicode_via_xdotool(ch);
|
|
InjectResult::Success
|
|
}
|
|
}
|
|
|
|
fn send_string(&self, s: &str) -> InjectResult {
|
|
// ASCII: type individual characters via XTest (fast, no side effects)
|
|
let is_ascii = s.chars().all(|c| char_to_keycode(c).is_some());
|
|
if is_ascii {
|
|
for ch in s.chars() {
|
|
self.send_char(ch);
|
|
}
|
|
return InjectResult::Success;
|
|
}
|
|
|
|
// Non-ASCII (Vietnamese Unicode): use clipboard paste via X11 API + XTest
|
|
// This avoids xdotool/ydotool subprocesses that silently drop Vietnamese.
|
|
self.paste_via_clipboard(0, s);
|
|
InjectResult::Success
|
|
}
|
|
|
|
fn inject_replacement(&self, backspaces: usize, text: &str) -> InjectResult {
|
|
let is_ascii = text.chars().all(|c| char_to_keycode(c).is_some());
|
|
if is_ascii {
|
|
if backspaces > 0 {
|
|
for _ in 0..backspaces {
|
|
self.send_keycode(14, false);
|
|
}
|
|
}
|
|
for ch in text.chars() {
|
|
if let Some((keycode, shift)) = char_to_keycode(ch) {
|
|
self.send_keycode(keycode, shift);
|
|
}
|
|
}
|
|
return InjectResult::Success;
|
|
}
|
|
|
|
// Contains Unicode: use direct X11 clipboard + XTest Ctrl+V
|
|
self.paste_via_clipboard(backspaces, text);
|
|
InjectResult::Success
|
|
}
|
|
|
|
fn flush(&self) -> InjectResult {
|
|
unsafe {
|
|
(self.lib.x_flush)(self.display);
|
|
}
|
|
InjectResult::Success
|
|
}
|
|
|
|
fn update_pasted_text(&self, _text: &str) -> InjectResult {
|
|
InjectResult::Success
|
|
}
|
|
}
|