fix: use select() on X11 fd for reliable event detection

XPending returned 0 even with active keyboard grab. Using select() on
the X11 connection file descriptor with 100ms timeout to reliably detect
when the X server sends events. Also adds XSelectInput on root window
and XConnectionNumber for the fd.
This commit is contained in:
Khoa Vo 2026-06-26 09:32:47 +07:00
parent d6ded6e706
commit cca68004ab
2 changed files with 84 additions and 9 deletions

View file

@ -743,6 +743,17 @@ fn run_with_x11(
capture.focus_lost = false;
}
// Wait for events with 100ms timeout, then re-grab if needed
if !capture.wait_for_event(100) {
// No events — check if grab is still held
if !capture.is_grabbed() {
eprintln!("[vietc] Keyboard grab lost — re-grabbing");
capture.grab_keyboard();
}
continue;
}
// Drain all available events
while let Some(event) = capture.next_event() {
if event.pressed {
// Skip autorepeat
@ -813,14 +824,6 @@ fn run_with_x11(
}
}
}
// Re-grab if the grab was silently lost (tray started, WM took focus, etc.)
if !capture.is_grabbed() {
eprintln!("[vietc] Keyboard grab lost — re-grabbing");
capture.grab_keyboard();
}
thread::sleep(Duration::from_millis(10));
}
}

View file

@ -38,6 +38,45 @@ struct X11Lib {
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,
}
// select() timeout struct
#[repr(C)]
struct Timeval {
tv_sec: i64,
tv_usec: i64,
}
#[repr(C)]
struct FdSet {
fds_bits: [u64; 16], // 1024 bits
}
extern "C" {
fn select(nfds: c_int, readfds: *mut FdSet, writefds: *mut FdSet, exceptfds: *mut FdSet, timeout: *mut Timeval) -> c_int;
}
fn fd_zero(set: &mut FdSet) {
set.fds_bits = [0u64; 16];
}
fn fd_set(fd: c_int, set: &mut FdSet) {
let idx = fd as usize / 64;
let bit = fd as usize % 64;
if idx < set.fds_bits.len() {
set.fds_bits[idx] |= 1u64 << bit;
}
}
fn fd_isset(fd: c_int, set: &FdSet) -> bool {
let idx = fd as usize / 64;
let bit = fd as usize % 64;
if idx < set.fds_bits.len() {
(set.fds_bits[idx] & (1u64 << bit)) != 0
} else {
false
}
}
impl X11Lib {
@ -81,6 +120,7 @@ impl X11Lib {
let x_flush = sym!("XFlush");
let x_select_input = sym!("XSelectInput");
let x_sync = sym!("XSync");
let x_connection_number = sym!("XConnectionNumber");
Ok(Self {
handle,
@ -96,6 +136,7 @@ impl X11Lib {
x_flush,
x_select_input,
x_sync,
x_connection_number,
})
}
}
@ -233,13 +274,44 @@ impl X11Capture {
if !self.grabbed {
return false;
}
unsafe { (self.lib.x_pending)(self.display) > 0 }
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 {
self.grabbed
}
/// Block until an event arrives, with a timeout in milliseconds.
/// Returns true if an event is available, false on timeout.
pub fn wait_for_event(&mut self, timeout_ms: u64) -> bool {
if !self.grabbed {
return false;
}
unsafe {
// Flush pending output first
(self.lib.x_flush)(self.display);
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: (timeout_ms / 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);
n > 0 && fd_isset(fd, &readfds)
}
}
pub fn next_event(&mut self) -> Option<X11KeyEvent> {
if !self.grabbed {
return None;