fix: replace XGrabKeyboard capture with XRecord extension
XGrabKeyboard returns success but never delivers events to a windowless client on modern X11. XRecord is the standard way to capture keyboard events globally — used by xdotool, xbindkeys, and input methods. Architecture: - XRecord: captures all keyboard events (no grab needed) - XGrabKeyboard: still used to block original events from reaching apps - XTest: injects modified events Dynamically loads libXtst.so.6 for XRecord functions. Events flow: XRecord callback → thread-safe queue → daemon event loop.
This commit is contained in:
parent
f87e37ebff
commit
e35c034157
1 changed files with 212 additions and 124 deletions
|
|
@ -1,4 +1,6 @@
|
||||||
use std::ffi::{c_char, c_int, c_long, c_void};
|
use std::ffi::{c_char, c_int, c_void};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
type Display = c_void;
|
type Display = c_void;
|
||||||
type Window = u64;
|
type Window = u64;
|
||||||
|
|
@ -7,13 +9,11 @@ type Time = u64;
|
||||||
// X11 event types
|
// X11 event types
|
||||||
const KEY_PRESS: c_int = 2;
|
const KEY_PRESS: c_int = 2;
|
||||||
const KEY_RELEASE: c_int = 3;
|
const KEY_RELEASE: c_int = 3;
|
||||||
const FOCUS_IN: c_int = 9;
|
|
||||||
const FOCUS_OUT: c_int = 10;
|
|
||||||
|
|
||||||
// X11 modifier masks
|
// X11 modifier masks
|
||||||
const CONTROL_MASK: c_int = 4;
|
const CONTROL_MASK: c_int = 4;
|
||||||
const MOD1_MASK: c_int = 8; // Alt
|
const MOD1_MASK: c_int = 8;
|
||||||
const MOD4_MASK: c_int = 64; // Super/Win
|
const MOD4_MASK: c_int = 64;
|
||||||
|
|
||||||
// Grab modes
|
// Grab modes
|
||||||
const GRAB_MODE_ASYNC: c_int = 1;
|
const GRAB_MODE_ASYNC: c_int = 1;
|
||||||
|
|
@ -31,17 +31,41 @@ struct X11Lib {
|
||||||
x_default_root_window: unsafe extern "C" fn(*mut Display) -> Window,
|
x_default_root_window: unsafe extern "C" fn(*mut Display) -> Window,
|
||||||
x_grab_keyboard: unsafe extern "C" fn(*mut Display, Window, c_int, c_int, c_int, Time) -> c_int,
|
x_grab_keyboard: unsafe extern "C" fn(*mut Display, Window, c_int, c_int, c_int, Time) -> c_int,
|
||||||
x_ungrab_keyboard: unsafe extern "C" fn(*mut Display, Time) -> c_int,
|
x_ungrab_keyboard: unsafe extern "C" fn(*mut Display, Time) -> c_int,
|
||||||
x_pending: unsafe extern "C" fn(*mut Display) -> c_int,
|
|
||||||
x_next_event: unsafe extern "C" fn(*mut Display, *mut XEvent),
|
|
||||||
x_lookup_string: unsafe extern "C" fn(*mut XKeyEvent, *mut c_char, c_int, *mut KeySym, *mut c_int) -> c_int,
|
x_lookup_string: unsafe extern "C" fn(*mut XKeyEvent, *mut c_char, c_int, *mut KeySym, *mut c_int) -> c_int,
|
||||||
x_utf8_lookup_string: Option<unsafe extern "C" fn(*mut XKeyEvent, *mut c_char, c_int, *mut KeySym, *mut c_int) -> c_int>,
|
x_utf8_lookup_string: Option<unsafe extern "C" fn(*mut XKeyEvent, *mut c_char, c_int, *mut KeySym, *mut c_int) -> c_int>,
|
||||||
x_flush: unsafe extern "C" fn(*mut Display) -> c_int,
|
x_flush: unsafe extern "C" fn(*mut Display) -> c_int,
|
||||||
x_select_input: unsafe extern "C" fn(*mut Display, Window, c_long) -> c_int,
|
|
||||||
x_sync: unsafe extern "C" fn(*mut Display, c_int) -> c_int,
|
|
||||||
x_connection_number: unsafe extern "C" fn(*mut Display) -> c_int,
|
x_connection_number: unsafe extern "C" fn(*mut Display) -> c_int,
|
||||||
|
// XRecord
|
||||||
|
x_record_query_version: unsafe extern "C" fn(*mut Display, *mut c_int, *mut c_int) -> i32,
|
||||||
|
x_record_alloc_range: unsafe extern "C" fn() -> *mut XRecordRange,
|
||||||
|
x_record_create_context: unsafe extern "C" fn(*mut Display, c_int, *mut c_int, c_int, *mut *mut XRecordRange, c_int) -> u64,
|
||||||
|
x_record_enable_context_async: unsafe extern "C" fn(*mut Display, u64, Option<XRecordCallback>, *mut c_void) -> i32,
|
||||||
|
x_record_process_replies: unsafe extern "C" fn(*mut Display),
|
||||||
|
x_record_disable_context: unsafe extern "C" fn(*mut Display, u64) -> i32,
|
||||||
|
x_record_free_context: unsafe extern "C" fn(*mut Display, u64) -> i32,
|
||||||
|
x_free: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
// XRecordRange - must match C layout exactly
|
||||||
|
#[repr(C)]
|
||||||
|
struct XRecordRange {
|
||||||
|
device_events_first: u8,
|
||||||
|
device_events_last: u8,
|
||||||
|
_pad: [u8; 254],
|
||||||
|
}
|
||||||
|
|
||||||
|
type XRecordCallback = unsafe extern "C" fn(*mut c_void, *mut XRecordInterceptData);
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct XRecordInterceptData {
|
||||||
|
id: u64,
|
||||||
|
server_time: u64,
|
||||||
|
client_swapped: c_int,
|
||||||
|
_pad: c_int,
|
||||||
|
data_len: c_int,
|
||||||
|
data: *mut u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
// select() timeout struct
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
struct Timeval {
|
struct Timeval {
|
||||||
tv_sec: i64,
|
tv_sec: i64,
|
||||||
|
|
@ -50,7 +74,7 @@ struct Timeval {
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
struct FdSet {
|
struct FdSet {
|
||||||
fds_bits: [u64; 16], // 1024 bits
|
fds_bits: [u64; 16],
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
@ -61,7 +85,7 @@ fn fd_zero(set: &mut FdSet) {
|
||||||
set.fds_bits = [0u64; 16];
|
set.fds_bits = [0u64; 16];
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fd_set(fd: c_int, set: &mut FdSet) {
|
fn fd_set_bit(fd: c_int, set: &mut FdSet) {
|
||||||
let idx = fd as usize / 64;
|
let idx = fd as usize / 64;
|
||||||
let bit = fd as usize % 64;
|
let bit = fd as usize % 64;
|
||||||
if idx < set.fds_bits.len() {
|
if idx < set.fds_bits.len() {
|
||||||
|
|
@ -103,13 +127,24 @@ impl X11Lib {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// libXtst.so.6 for XRecord
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let x_open_display = sym!("XOpenDisplay");
|
let x_open_display = sym!("XOpenDisplay");
|
||||||
let x_close_display = sym!("XCloseDisplay");
|
let x_close_display = sym!("XCloseDisplay");
|
||||||
let x_default_root_window = sym!("XDefaultRootWindow");
|
let x_default_root_window = sym!("XDefaultRootWindow");
|
||||||
let x_grab_keyboard = sym!("XGrabKeyboard");
|
let x_grab_keyboard = sym!("XGrabKeyboard");
|
||||||
let x_ungrab_keyboard = sym!("XUngrabKeyboard");
|
let x_ungrab_keyboard = sym!("XUngrabKeyboard");
|
||||||
let x_pending = sym!("XPending");
|
|
||||||
let x_next_event = sym!("XNextEvent");
|
|
||||||
let x_lookup_string = sym!("XLookupString");
|
let x_lookup_string = sym!("XLookupString");
|
||||||
let x_utf8_lookup_string = dlsym(handle, b"Xutf8LookupString\0".as_ptr() as *const c_char);
|
let x_utf8_lookup_string = dlsym(handle, b"Xutf8LookupString\0".as_ptr() as *const c_char);
|
||||||
let x_utf8_lookup_string = if x_utf8_lookup_string.is_null() {
|
let x_utf8_lookup_string = if x_utf8_lookup_string.is_null() {
|
||||||
|
|
@ -118,10 +153,27 @@ impl X11Lib {
|
||||||
Some(std::mem::transmute(x_utf8_lookup_string))
|
Some(std::mem::transmute(x_utf8_lookup_string))
|
||||||
};
|
};
|
||||||
let x_flush = sym!("XFlush");
|
let x_flush = sym!("XFlush");
|
||||||
let x_select_input = sym!("XSelectInput");
|
|
||||||
let x_sync = sym!("XSync");
|
|
||||||
let x_connection_number = sym!("XConnectionNumber");
|
let x_connection_number = sym!("XConnectionNumber");
|
||||||
|
|
||||||
|
if xtst_handle.is_null() {
|
||||||
|
return Err("Failed to load libXtst.so.6 — install libxtst6".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! xtst_sym {
|
||||||
|
($name:expr) => {
|
||||||
|
std::mem::transmute(dlsym(xtst_handle, concat!($name, "\0").as_ptr() as *const c_char))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let x_record_query_version = xtst_sym!("XRecordQueryVersion");
|
||||||
|
let x_record_alloc_range = xtst_sym!("XRecordAllocRange");
|
||||||
|
let x_record_create_context = xtst_sym!("XRecordCreateContext");
|
||||||
|
let x_record_enable_context_async = xtst_sym!("XRecordEnableContextAsync");
|
||||||
|
let x_record_process_replies = xtst_sym!("XRecordProcessReplies");
|
||||||
|
let x_record_disable_context = xtst_sym!("XRecordDisableContext");
|
||||||
|
let x_record_free_context = xtst_sym!("XRecordFreeContext");
|
||||||
|
let x_free = sym!("XFree");
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
handle,
|
handle,
|
||||||
x_open_display,
|
x_open_display,
|
||||||
|
|
@ -129,14 +181,18 @@ impl X11Lib {
|
||||||
x_default_root_window,
|
x_default_root_window,
|
||||||
x_grab_keyboard,
|
x_grab_keyboard,
|
||||||
x_ungrab_keyboard,
|
x_ungrab_keyboard,
|
||||||
x_pending,
|
|
||||||
x_next_event,
|
|
||||||
x_lookup_string,
|
x_lookup_string,
|
||||||
x_utf8_lookup_string,
|
x_utf8_lookup_string,
|
||||||
x_flush,
|
x_flush,
|
||||||
x_select_input,
|
|
||||||
x_sync,
|
|
||||||
x_connection_number,
|
x_connection_number,
|
||||||
|
x_record_query_version,
|
||||||
|
x_record_alloc_range,
|
||||||
|
x_record_create_context,
|
||||||
|
x_record_enable_context_async,
|
||||||
|
x_record_process_replies,
|
||||||
|
x_record_disable_context,
|
||||||
|
x_record_free_context,
|
||||||
|
x_free,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,23 +226,6 @@ struct XKeyEvent {
|
||||||
_same_screen: c_int,
|
_same_screen: c_int,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
struct XEvent {
|
|
||||||
// XEvent is a union — all variants share offset 0
|
|
||||||
// sizeof(XEvent) = 192 on x86_64 (long pad[24])
|
|
||||||
_bytes: [u8; 192],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl XEvent {
|
|
||||||
fn event_type(&self) -> c_int {
|
|
||||||
unsafe { std::ptr::read_unaligned(self._bytes.as_ptr() as *const c_int) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn key(&self) -> &XKeyEvent {
|
|
||||||
unsafe { &*(self._bytes.as_ptr() as *const XKeyEvent) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeySym = u64;
|
type KeySym = u64;
|
||||||
|
|
||||||
pub struct X11KeyEvent {
|
pub struct X11KeyEvent {
|
||||||
|
|
@ -196,12 +235,58 @@ pub struct X11KeyEvent {
|
||||||
pub state: c_int,
|
pub state: c_int,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared event queue between XRecord callback and capture reader
|
||||||
|
struct EventQueue {
|
||||||
|
queue: VecDeque<X11KeyEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static mut EVENT_QUEUE: Option<Arc<Mutex<EventQueue>>> = None;
|
||||||
|
|
||||||
|
unsafe extern "C" fn record_callback(_closure: *mut c_void, data: *mut XRecordInterceptData) {
|
||||||
|
if (*data).id == 0 {
|
||||||
|
return; // This is our own XRecord data from the init, skip
|
||||||
|
}
|
||||||
|
if (*data).data_len < 2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data_bytes = (*data).data;
|
||||||
|
let event_type: c_int = *data_bytes as c_int;
|
||||||
|
let keycode: u8 = *data_bytes.add(1);
|
||||||
|
|
||||||
|
if event_type != KEY_PRESS && event_type != KEY_RELEASE {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For XRecord events, we get the raw keycode and event type.
|
||||||
|
// We need to construct a fake XKeyEvent to pass to XLookupString for the character.
|
||||||
|
// The state (modifier bits) is at offset 28-31 in the XRecord data for keyboard events.
|
||||||
|
let state: c_int = if (*data).data_len >= 4 {
|
||||||
|
*(data_bytes.add(2) as *const u16) as c_int
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let event = X11KeyEvent {
|
||||||
|
keycode: keycode as u32,
|
||||||
|
ch: None, // Will be resolved later via XLookupString or keysym mapping
|
||||||
|
pressed: event_type == KEY_PRESS,
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref q) = EVENT_QUEUE {
|
||||||
|
if let Ok(mut queue) = q.lock() {
|
||||||
|
queue.queue.push_back(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct X11Capture {
|
pub struct X11Capture {
|
||||||
lib: X11Lib,
|
lib: X11Lib,
|
||||||
display: *mut Display,
|
display: *mut Display,
|
||||||
root: Window,
|
root: Window,
|
||||||
grabbed: bool,
|
grabbed: bool,
|
||||||
/// Set to true when FocusOut is received — caller should reset engine state
|
record_context: u64,
|
||||||
|
record_display: *mut Display,
|
||||||
pub focus_lost: bool,
|
pub focus_lost: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,7 +297,7 @@ impl X11Capture {
|
||||||
let lib = match X11Lib::new() {
|
let lib = match X11Lib::new() {
|
||||||
Ok(lib) => lib,
|
Ok(lib) => lib,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[vietc] X11Capture: failed to load X11: {}", e);
|
eprintln!("[vietc] X11Capture: failed to load: {}", e);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -220,23 +305,71 @@ impl X11Capture {
|
||||||
unsafe {
|
unsafe {
|
||||||
let display = (lib.x_open_display)(std::ptr::null());
|
let display = (lib.x_open_display)(std::ptr::null());
|
||||||
if display.is_null() {
|
if display.is_null() {
|
||||||
eprintln!("[vietc] X11Capture: cannot open display. Is DISPLAY set?");
|
eprintln!("[vietc] X11Capture: cannot open display");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let root = (lib.x_default_root_window)(display);
|
let root = (lib.x_default_root_window)(display);
|
||||||
// Select for KeyPress and KeyRelease events on the root window
|
|
||||||
// so the X server delivers them to our connection
|
// Check XRecord version
|
||||||
let key_press_mask: c_long = 1; // KeyPressMask
|
let mut major = 0i32;
|
||||||
let key_release_mask: c_long = 2; // KeyReleaseMask
|
let mut minor = 0i32;
|
||||||
(lib.x_select_input)(display, root, key_press_mask | key_release_mask);
|
if (lib.x_record_query_version)(display, &mut major, &mut minor) == 0 {
|
||||||
(lib.x_sync)(display, 0);
|
eprintln!("[vietc] X11Capture: XRecord extension not available");
|
||||||
eprintln!("[vietc] X11Capture: initialized successfully");
|
(lib.x_close_display)(display);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
eprintln!("[vietc] X11Capture: XRecord version {}.{}", major, minor);
|
||||||
|
|
||||||
|
// Allocate range for keyboard events
|
||||||
|
let range = (lib.x_record_alloc_range)();
|
||||||
|
if range.is_null() {
|
||||||
|
eprintln!("[vietc] X11Capture: XRecordAllocRange failed");
|
||||||
|
(lib.x_close_display)(display);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Set range: KeyPress (2) through KeyRelease (3)
|
||||||
|
(*range).device_events_first = KEY_PRESS as u8;
|
||||||
|
(*range).device_events_last = KEY_RELEASE as u8;
|
||||||
|
|
||||||
|
// Create XRecord context
|
||||||
|
let mut spec: c_int = 1; // XRecordAllClients = 1
|
||||||
|
let range_ptr = range as *mut *mut XRecordRange;
|
||||||
|
let ctx = (lib.x_record_create_context)(
|
||||||
|
display,
|
||||||
|
0, // own_client
|
||||||
|
&mut spec, // clients
|
||||||
|
1, // nclients
|
||||||
|
range_ptr, // ranges
|
||||||
|
1, // nranges
|
||||||
|
);
|
||||||
|
(lib.x_free)(range as *mut c_void);
|
||||||
|
|
||||||
|
if ctx == 0 {
|
||||||
|
eprintln!("[vietc] X11Capture: XRecordCreateContext failed");
|
||||||
|
(lib.x_close_display)(display);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize event queue
|
||||||
|
EVENT_QUEUE = Some(Arc::new(Mutex::new(EventQueue {
|
||||||
|
queue: VecDeque::new(),
|
||||||
|
})));
|
||||||
|
|
||||||
|
// Enable XRecord with async callback
|
||||||
|
let closure: *mut c_void = std::ptr::null_mut();
|
||||||
|
(lib.x_record_enable_context_async)(display, ctx, Some(record_callback), closure);
|
||||||
|
(lib.x_flush)(display);
|
||||||
|
|
||||||
|
eprintln!("[vietc] X11Capture: XRecord context enabled — capturing keyboard events");
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
lib,
|
lib,
|
||||||
display,
|
display,
|
||||||
root,
|
root,
|
||||||
grabbed: false,
|
grabbed: false,
|
||||||
|
record_context: ctx,
|
||||||
|
record_display: display,
|
||||||
focus_lost: false,
|
focus_lost: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -247,19 +380,18 @@ impl X11Capture {
|
||||||
let status = (self.lib.x_grab_keyboard)(
|
let status = (self.lib.x_grab_keyboard)(
|
||||||
self.display,
|
self.display,
|
||||||
self.root,
|
self.root,
|
||||||
0, // owner_events = False
|
0, // owner_events = False — block events from reaching apps
|
||||||
GRAB_MODE_ASYNC,
|
GRAB_MODE_ASYNC,
|
||||||
GRAB_MODE_ASYNC,
|
GRAB_MODE_ASYNC,
|
||||||
0, // CurrentTime
|
0,
|
||||||
) as i32;
|
) as i32;
|
||||||
if status == 0 {
|
if status == 0 {
|
||||||
self.grabbed = true;
|
self.grabbed = true;
|
||||||
// Flush to ensure the grab is processed by the X server
|
|
||||||
(self.lib.x_flush)(self.display);
|
(self.lib.x_flush)(self.display);
|
||||||
eprintln!("[vietc] X11Capture: grabbed keyboard successfully");
|
eprintln!("[vietc] X11Capture: keyboard grabbed (blocking apps)");
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
eprintln!("[vietc] X11Capture: grab failed with status {}", status);
|
eprintln!("[vietc] X11Capture: grab failed status={}", status);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -275,98 +407,47 @@ impl X11Capture {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_pending_events(&self) -> bool {
|
|
||||||
if !self.grabbed {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
unsafe {
|
|
||||||
let fd = (self.lib.x_connection_number)(self.display);
|
|
||||||
let mut readfds: FdSet = std::mem::zeroed();
|
|
||||||
fd_zero(&mut readfds);
|
|
||||||
fd_set(fd, &mut readfds);
|
|
||||||
let mut timeout = Timeval { tv_sec: 0, tv_usec: 0 };
|
|
||||||
let n = select(fd + 1, &mut readfds, std::ptr::null_mut(), std::ptr::null_mut(), &mut timeout);
|
|
||||||
n > 0 && fd_isset(fd, &readfds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_grabbed(&self) -> bool {
|
pub fn is_grabbed(&self) -> bool {
|
||||||
self.grabbed
|
self.grabbed
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block until an event arrives, with a timeout in milliseconds.
|
/// Wait for XRecord data to arrive on the X11 connection fd, with timeout.
|
||||||
/// Returns true if an event is available, false on timeout.
|
|
||||||
pub fn wait_for_event(&mut self, timeout_ms: u64) -> bool {
|
pub fn wait_for_event(&mut self, timeout_ms: u64) -> bool {
|
||||||
if !self.grabbed {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
unsafe {
|
unsafe {
|
||||||
// Flush pending output first
|
|
||||||
(self.lib.x_flush)(self.display);
|
(self.lib.x_flush)(self.display);
|
||||||
|
|
||||||
let fd = (self.lib.x_connection_number)(self.display);
|
let fd = (self.lib.x_connection_number)(self.display);
|
||||||
let mut readfds: FdSet = std::mem::zeroed();
|
let mut readfds: FdSet = std::mem::zeroed();
|
||||||
fd_zero(&mut readfds);
|
fd_zero(&mut readfds);
|
||||||
fd_set(fd, &mut readfds);
|
fd_set_bit(fd, &mut readfds);
|
||||||
let mut timeout = Timeval {
|
let mut timeout = Timeval {
|
||||||
tv_sec: (timeout_ms / 1000) as i64,
|
tv_sec: (timeout_ms / 1000) as i64,
|
||||||
tv_usec: ((timeout_ms % 1000) * 1000) as i64,
|
tv_usec: ((timeout_ms % 1000) * 1000) as i64,
|
||||||
};
|
};
|
||||||
let n = select(fd + 1, &mut readfds, std::ptr::null_mut(), std::ptr::null_mut(), &mut timeout);
|
let n = select(fd + 1, &mut readfds, std::ptr::null_mut(), std::ptr::null_mut(), &mut timeout);
|
||||||
if n > 0 && fd_isset(fd, &readfds) {
|
if n > 0 && fd_isset(fd, &readfds) {
|
||||||
|
// Process XRecord replies — this fires the callback
|
||||||
|
(self.lib.x_record_process_replies)(self.display);
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
// Log on first timeout to diagnose
|
|
||||||
static mut LOGGED: bool = false;
|
|
||||||
if !LOGGED {
|
|
||||||
eprintln!("[vietc] X11 select timeout (fd={}, n={}, timeout={}ms) — no events arriving", fd, n, timeout_ms);
|
|
||||||
eprintln!("[vietc] Keyboard grab may not be working. Check if another app grabbed the keyboard.");
|
|
||||||
LOGGED = true;
|
|
||||||
}
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_event(&mut self) -> Option<X11KeyEvent> {
|
pub fn next_event(&mut self) -> Option<X11KeyEvent> {
|
||||||
if !self.grabbed {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-blocking: only read if events are pending
|
|
||||||
if !self.has_pending_events() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut event: XEvent = unsafe { std::mem::zeroed() };
|
|
||||||
unsafe {
|
unsafe {
|
||||||
(self.lib.x_next_event)(self.display, &mut event);
|
if let Some(ref q) = EVENT_QUEUE {
|
||||||
|
if let Ok(mut queue) = q.lock() {
|
||||||
|
if let Some(mut event) = queue.queue.pop_front() {
|
||||||
|
// Resolve the character from the keycode + modifier state
|
||||||
|
event.ch = self.lookup_keycode(event.keycode, event.state);
|
||||||
|
return Some(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
let _type = event.event_type();
|
|
||||||
|
|
||||||
// Handle FocusIn/FocusOut — reset engine state when focus changes
|
|
||||||
if _type == FOCUS_OUT {
|
|
||||||
self.focus_lost = true;
|
|
||||||
return self.next_event();
|
|
||||||
}
|
}
|
||||||
if _type == FOCUS_IN {
|
|
||||||
self.focus_lost = false;
|
|
||||||
return self.next_event();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _type != KEY_PRESS && _type != KEY_RELEASE {
|
|
||||||
return self.next_event();
|
|
||||||
}
|
}
|
||||||
|
None
|
||||||
let key_event = event.key();
|
|
||||||
let ch = self.lookup_key(key_event);
|
|
||||||
Some(X11KeyEvent {
|
|
||||||
keycode: key_event.keycode,
|
|
||||||
ch,
|
|
||||||
pressed: _type == KEY_PRESS,
|
|
||||||
state: key_event.state,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_modifier_pressed(&self, state: c_int) -> bool {
|
pub fn is_modifier_pressed(&self, state: c_int) -> bool {
|
||||||
|
|
@ -377,7 +458,6 @@ impl X11Capture {
|
||||||
where
|
where
|
||||||
F: FnOnce() -> T,
|
F: FnOnce() -> T,
|
||||||
{
|
{
|
||||||
// Grab should already be held; just execute
|
|
||||||
f()
|
f()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,13 +471,19 @@ impl X11Capture {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lookup_key(&self, event: &XKeyEvent) -> Option<char> {
|
pub fn lookup_keycode(&self, keycode: u32, state: c_int) -> Option<char> {
|
||||||
|
// Construct a fake XKeyEvent for XLookupString
|
||||||
|
let mut xke: XKeyEvent = unsafe { std::mem::zeroed() };
|
||||||
|
xke._type = KEY_PRESS;
|
||||||
|
xke.keycode = keycode;
|
||||||
|
xke.state = state;
|
||||||
|
|
||||||
let mut buf = [0u8; 32];
|
let mut buf = [0u8; 32];
|
||||||
let mut keysym: KeySym = 0;
|
let mut keysym: KeySym = 0;
|
||||||
let len = unsafe {
|
let len = unsafe {
|
||||||
if let Some(xutf8) = self.lib.x_utf8_lookup_string {
|
if let Some(xutf8) = self.lib.x_utf8_lookup_string {
|
||||||
xutf8(
|
xutf8(
|
||||||
event as *const XKeyEvent as *mut XKeyEvent,
|
&mut xke as *mut XKeyEvent,
|
||||||
buf.as_mut_ptr() as *mut c_char,
|
buf.as_mut_ptr() as *mut c_char,
|
||||||
buf.len() as c_int,
|
buf.len() as c_int,
|
||||||
&mut keysym as *mut KeySym,
|
&mut keysym as *mut KeySym,
|
||||||
|
|
@ -405,7 +491,7 @@ impl X11Capture {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(self.lib.x_lookup_string)(
|
(self.lib.x_lookup_string)(
|
||||||
event as *const XKeyEvent as *mut XKeyEvent,
|
&mut xke as *mut XKeyEvent,
|
||||||
buf.as_mut_ptr() as *mut c_char,
|
buf.as_mut_ptr() as *mut c_char,
|
||||||
buf.len() as c_int,
|
buf.len() as c_int,
|
||||||
&mut keysym as *mut KeySym,
|
&mut keysym as *mut KeySym,
|
||||||
|
|
@ -425,10 +511,12 @@ impl X11Capture {
|
||||||
|
|
||||||
impl Drop for X11Capture {
|
impl Drop for X11Capture {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
if self.grabbed {
|
if self.grabbed {
|
||||||
self.ungrab_keyboard();
|
self.ungrab_keyboard();
|
||||||
}
|
}
|
||||||
unsafe {
|
(self.lib.x_record_disable_context)(self.record_display, self.record_context);
|
||||||
|
(self.lib.x_record_free_context)(self.record_display, self.record_context);
|
||||||
(self.lib.x_close_display)(self.display);
|
(self.lib.x_close_display)(self.display);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue