evdev: poll all keyboard devices simultaneously; x11: replace XRecord capture with XQueryKeymap polling

- open_keyboard_device() -> open_keyboard_devices(): returns Vec of all
  keyboard-capable evdev devices instead of just the first one
- run_with_evdev() polls all device FDs via single libc::poll() call
- Each device maintains independent key_state tracking
- Added XQueryKeymap/XLookupString to X11Lib in protocol crate
- X11KeymapCapture: new struct that polls X11 keymap every 10ms via
  XQueryKeymap, diffs consecutive polls for press/release detection,
  and uses XLookupString/Xutf8LookupString for char conversion
- run_with_x11_keymap(): replaces segfaulting XRecord-based run_with_x11
  as the primary X11 fallback path
This commit is contained in:
Khoa Vo 2026-07-02 14:10:54 +07:00
parent 88a64224b6
commit 6b2b42639f
2 changed files with 576 additions and 309 deletions

View file

@ -855,15 +855,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}); });
} }
// Try evdev first (more reliable than X11 XRecord in most environments). // Try evdev first: open ALL keyboard-capable devices and poll them
// If the evdev device opens but produces no events (common in VMs where // simultaneously. This handles VMs where input arrives on a different
// keyboard input bypasses the evdev grab), run_with_evdev will exit via // event node than the first device found.
// the 30-second safety timeout — we fall through to X11 capture. match open_keyboard_devices() {
match open_keyboard_device() { Ok(mut devices) => {
Ok((device, path)) => {
log_info(&format!("[vietc] Keyboard device: {}", path));
match run_with_evdev( match run_with_evdev(
device, &mut devices,
&mut daemon, &mut daemon,
shared_active_window.clone(), shared_active_window.clone(),
shared_window_class.clone(), shared_window_class.clone(),
@ -890,19 +888,25 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "x11")] #[cfg(feature = "x11")]
if display != display::DisplayServer::Wayland { if display != display::DisplayServer::Wayland {
if let Some(capture) = X11Capture::new() { log_info("[vietc] Trying X11 keymap-based capture");
log_info("[vietc] X11 XRecord capture active — using X11 capture/injection"); match run_with_x11_keymap(
return run_with_x11(
capture,
&mut daemon, &mut daemon,
shared_active_window, shared_active_window.clone(),
shared_window_class, shared_window_class.clone(),
config_changed, config_changed.clone(),
status_changed, status_changed.clone(),
engine_enabled, engine_enabled.clone(),
); display,
} else { ) {
log_info("[vietc] X11 not available, falling back"); Ok(()) => {
log_info("[vietc] X11 keymap returned, falling through to stdin mode");
}
Err(e) => {
log_info(&format!(
"[vietc] X11 keymap exited with error: {} — falling back",
e
));
}
} }
} }
@ -920,12 +924,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
fn open_keyboard_device() -> Result<(evdev::Device, String), Box<dyn std::error::Error>> { fn open_keyboard_devices() -> Result<Vec<(evdev::Device, String)>, Box<dyn std::error::Error>> {
let dir = std::path::Path::new("/dev/input"); let dir = std::path::Path::new("/dev/input");
if !dir.exists() { if !dir.exists() {
return Err("No /dev/input directory".into()); return Err("No /dev/input directory".into());
} }
let mut devices: Vec<(evdev::Device, String)> = Vec::new();
let mut permission_denied_count = 0u32; let mut permission_denied_count = 0u32;
let mut total_event_count = 0u32; let mut total_event_count = 0u32;
@ -947,7 +952,12 @@ fn open_keyboard_device() -> Result<(evdev::Device, String), Box<dyn std::error:
.supported_keys() .supported_keys()
.is_some_and(|k| k.contains(evdev::Key::KEY_A)) .is_some_and(|k| k.contains(evdev::Key::KEY_A))
{ {
return Ok((device, format!("{} ({})", entry.path().display(), dev_name))); log_info(&format!(
"[vietc] Found keyboard device: {} ({})",
entry.path().display(),
dev_name
));
devices.push((device, format!("{} ({})", entry.path().display(), dev_name)));
} }
} }
Err(e) => { Err(e) => {
@ -960,8 +970,12 @@ fn open_keyboard_device() -> Result<(evdev::Device, String), Box<dyn std::error:
} }
} }
if !devices.is_empty() {
log_info(&format!("[vietc] Opened {} keyboard device(s)", devices.len()));
return Ok(devices);
}
if permission_denied_count > 0 { if permission_denied_count > 0 {
// Check if user is in the group but session hasn't refreshed
let in_group_db = std::process::Command::new("groups") let in_group_db = std::process::Command::new("groups")
.output() .output()
.map(|o| String::from_utf8_lossy(&o.stdout).contains("input")) .map(|o| String::from_utf8_lossy(&o.stdout).contains("input"))
@ -1006,19 +1020,27 @@ fn run_with_x11(
// press+release immediately, breaking held-key combos (Ctrl+C, Alt+Tab…). // press+release immediately, breaking held-key combos (Ctrl+C, Alt+Tab…).
let mut pressed_keys: HashSet<u32> = HashSet::new(); let mut pressed_keys: HashSet<u32> = HashSet::new();
eprintln!("[vietc] X11 event loop starting"); use std::io::Write;
let _ = std::io::stderr().write_all(b"[vietc] X11 event loop starting\n");
std::io::stderr().flush().ok();
loop { loop {
let _ = std::io::stderr().write_all(b"[vietc] X11: check status_changed\n");
std::io::stderr().flush().ok();
if status_changed.load(Ordering::SeqCst) { if status_changed.load(Ordering::SeqCst) {
daemon.sync_status_file(); daemon.sync_status_file();
status_changed.store(false, Ordering::SeqCst); status_changed.store(false, Ordering::SeqCst);
} }
let _ = std::io::stderr().write_all(b"[vietc] X11: check config_changed\n");
std::io::stderr().flush().ok();
if config_changed.load(Ordering::SeqCst) { if config_changed.load(Ordering::SeqCst) {
daemon.reload_config(); daemon.reload_config();
config_changed.store(false, Ordering::SeqCst); config_changed.store(false, Ordering::SeqCst);
} }
let _ = std::io::stderr().write_all(b"[vietc] X11: lock active_window\n");
std::io::stderr().flush().ok();
{ {
let active_window = shared_active_window.lock().unwrap().clone(); let active_window = shared_active_window.lock().unwrap().clone();
if active_window != last_active_window { if active_window != last_active_window {
@ -1027,6 +1049,8 @@ fn run_with_x11(
} }
} }
let _ = std::io::stderr().write_all(b"[vietc] X11: lock window_class\n");
std::io::stderr().flush().ok();
if daemon.config.app_state.enabled { if daemon.config.app_state.enabled {
let class = shared_window_class.lock().unwrap().clone(); let class = shared_window_class.lock().unwrap().clone();
if !class.is_empty() { if !class.is_empty() {
@ -1042,11 +1066,13 @@ fn run_with_x11(
} }
// Wait for events with 100ms timeout. // Wait for events with 100ms timeout.
// SKIP_RECORD_EVENTS may still be true from a previous injection — let _ = std::io::stderr().write_all(b"[vietc] X11: wait_for_event\n");
// drain_pipe drops any stale injected events while flag is true. std::io::stderr().flush().ok();
let _got_data = capture.wait_for_event(100); let _got_data = capture.wait_for_event(100);
// NOW safe to clear: any injected events from last iteration were dropped. // NOW safe to clear: any injected events from last iteration were dropped.
SKIP_RECORD_EVENTS.store(false, Ordering::Relaxed); SKIP_RECORD_EVENTS.store(false, Ordering::Relaxed);
let _ = std::io::stderr().write_all(b"[vietc] X11: next_event\n");
std::io::stderr().flush().ok();
let evt = capture.next_event(); let evt = capture.next_event();
if evt.is_none() { if evt.is_none() {
continue; continue;
@ -1118,8 +1144,143 @@ fn run_with_x11(
} }
} }
#[cfg(feature = "x11")]
fn run_with_x11_keymap(
daemon: &mut Daemon,
shared_active_window: Arc<Mutex<String>>,
shared_window_class: Arc<Mutex<String>>,
config_changed: Arc<AtomicBool>,
status_changed: Arc<AtomicBool>,
_engine_enabled: Arc<AtomicBool>,
display: display::DisplayServer,
) -> Result<(), Box<dyn std::error::Error>> {
use vietc_protocol::x11_inject::X11KeymapCapture;
let mut capture = X11KeymapCapture::new()?;
let injector = create_injector(display)?;
let mut last_active_window = String::new();
let mut last_window_class = String::new();
let mut key_state: HashSet<u32> = HashSet::new();
log_info("[vietc] X11 keymap capture active");
loop {
if SIGNAL_EXIT.load(Ordering::SeqCst) {
log_info("[vietc] Exiting on signal");
return Ok(());
}
if status_changed.load(Ordering::SeqCst) {
daemon.sync_status_file();
status_changed.store(false, Ordering::SeqCst);
}
if config_changed.load(Ordering::SeqCst) {
daemon.reload_config();
config_changed.store(false, Ordering::SeqCst);
}
{
let active_window = shared_active_window.lock().unwrap().clone();
if active_window != last_active_window {
last_active_window = active_window.clone();
daemon.replay_reset();
}
}
if daemon.config.app_state.enabled {
let class = shared_window_class.lock().unwrap().clone();
if !class.is_empty() && class != last_window_class {
last_window_class = class.clone();
daemon.check_app_change_with(class.clone());
}
}
// Poll keymap for changes every 10ms
let events = capture.poll();
if events.is_empty() {
std::thread::sleep(std::time::Duration::from_millis(10));
continue;
}
// Update persistent key state
for (keycode, pressed) in &events {
if *pressed {
key_state.insert(*keycode);
} else {
key_state.remove(keycode);
}
}
for (keycode, pressed) in &events {
if !*pressed {
continue;
}
let keycode = *keycode;
let shift_pressed = key_state.contains(&42) || key_state.contains(&54);
let ctrl_pressed = key_state.contains(&29) || key_state.contains(&97);
let alt_pressed = key_state.contains(&56) || key_state.contains(&100);
let caps_state = key_state.contains(&58);
let mut mod_state = 0i32;
if shift_pressed { mod_state |= 1; }
if caps_state { mod_state |= 2; }
if ctrl_pressed { mod_state |= 4; }
if alt_pressed { mod_state |= 8; }
let is_mod = ctrl_pressed || alt_pressed || key_state.contains(&125);
if is_mod {
continue;
}
// Engine toggle: Ctrl+Space
if ctrl_pressed && keycode == 57 {
daemon.toggle();
continue;
}
// Method toggle: Ctrl+LeftShift
if ctrl_pressed && shift_pressed {
daemon.toggle_method();
continue;
}
// Password detection
if daemon.config.app_state.enabled {
let is_pw = daemon.app_state.is_password_field();
let currently_enabled = daemon.engine.is_enabled();
if is_pw && currently_enabled {
daemon.engine.set_enabled(false);
daemon.write_status();
} else if !is_pw && !currently_enabled && daemon.config.start_enabled {
let default_state = daemon.app_state.get_default_state();
if default_state {
daemon.engine.set_enabled(true);
daemon.write_status();
}
}
}
// Use keymap lookup for character conversion
if let Some(ch) = capture.lookup_keycode(keycode, mod_state) {
let mut commands = daemon.process_key(ch);
if !commands.is_empty()
&& is_vn_control_key(daemon.app_state.effective_method(), ch)
{
for cmd in &mut commands {
if let OutputCommand::Backspace(ref mut n) = cmd {
*n += 1;
break;
}
}
}
execute_commands(&*injector, &commands, false);
}
}
}
}
fn run_with_evdev( fn run_with_evdev(
mut device: evdev::Device, devices: &mut Vec<(evdev::Device, String)>,
daemon: &mut Daemon, daemon: &mut Daemon,
shared_active_window: Arc<Mutex<String>>, shared_active_window: Arc<Mutex<String>>,
shared_window_class: Arc<Mutex<String>>, shared_window_class: Arc<Mutex<String>>,
@ -1130,8 +1291,10 @@ fn run_with_evdev(
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let injector = create_injector(display)?; let injector = create_injector(display)?;
let mut grabbed = if daemon.grab_enabled { // Use the first device for grab (only one device can be grabbed at a time)
match device.grab() { let primary_idx = 0usize;
let mut grabbed = if daemon.grab_enabled && !devices.is_empty() {
match devices[primary_idx].0.grab() {
Ok(()) => { Ok(()) => {
log_info("[vietc] Keyboard grabbed — race condition eliminated"); log_info("[vietc] Keyboard grabbed — race condition eliminated");
true true
@ -1146,64 +1309,64 @@ fn run_with_evdev(
} }
} }
} else { } else {
if !daemon.grab_enabled {
log_info("[vietc] Keyboard grab disabled (config grab = false)"); log_info("[vietc] Keyboard grab disabled (config grab = false)");
log_info("[vietc] Set grab = true in vietc.toml to enable (needs root)"); log_info("[vietc] Set grab = true in vietc.toml to enable (needs root)");
}
false false
}; };
let mut consumed_keys: HashSet<u16> = HashSet::new(); let mut consumed_keys: HashSet<u16> = HashSet::new();
let mut last_active_window = String::new(); let mut last_active_window = String::new();
let mut last_window_class = String::new(); let mut last_window_class = String::new();
// Skip counter: after Unicode injection, skip N upcoming events
// (they're auto-repeat pile-up from the injection delay)
let mut skip_count = 0u32; let mut skip_count = 0u32;
// Password detection: re-check every N key presses even without window change
// (catches in-terminal sudo prompts where window stays the same)
let mut password_check_counter: u32 = 0; let mut password_check_counter: u32 = 0;
// Safety: if grab is active and no events arrive for 3 seconds,
// release the grab so the user isn't locked out, and continue in
// non-grabbed mode (events reach both X and the daemon; daemon
// applies backspace corrections via uinput).
let mut last_event_time = std::time::Instant::now(); let mut last_event_time = std::time::Instant::now();
let mut last_key_time = std::time::Instant::now(); let mut last_key_time = std::time::Instant::now();
// Track consecutive idle polls for fast grab fallback
let mut idle_polls: u32 = 0; let mut idle_polls: u32 = 0;
// Track key states for each device independently
let mut device_states: Vec<(evdev::AttributeSet<evdev::Key>, bool)> = devices
.iter()
.map(|(d, _)| {
let caps = is_caps_lock_on(d);
let state = d.get_key_state().ok().unwrap_or_else(evdev::AttributeSet::new);
(state, caps)
})
.collect();
log_info("[vietc] Event loop started"); log_info("[vietc] Event loop started");
loop { loop {
// Check for signal (Ctrl+C, SIGTERM) — release grab before exit
if SIGNAL_EXIT.load(Ordering::SeqCst) { if SIGNAL_EXIT.load(Ordering::SeqCst) {
if grabbed { if grabbed && !devices.is_empty() {
let _ = device.ungrab(); let _ = devices[primary_idx].0.ungrab();
log_info("[vietc] Signal received — keyboard grab released"); log_info("[vietc] Signal received — keyboard grab released");
} }
log_info("[vietc] Exiting on signal"); log_info("[vietc] Exiting on signal");
return Ok(()); return Ok(());
} }
// Grab safety timeout: if the grabbed device produces no events
// (common in VMs where EVIOCGRAB breaks event delivery), release
// the grab after ~300ms idle and continue in non-grabbed mode
// where events reach both X and the daemon.
if grabbed && idle_polls >= 3 && last_event_time.elapsed() > std::time::Duration::from_millis(200) { if grabbed && idle_polls >= 3 && last_event_time.elapsed() > std::time::Duration::from_millis(200) {
log_info( log_info(
"[vietc] No events received via grab — releasing grab, continuing in non-grabbed evdev mode", "[vietc] No events received via grab — releasing grab, continuing in non-grabbed evdev mode",
); );
let _ = device.ungrab(); let _ = devices[primary_idx].0.ungrab();
grabbed = false; grabbed = false;
continue; continue;
} }
// Poll evdev fd with 100ms timeout so the loop stays responsive // Poll ALL devices simultaneously
// even when no keyboard events arrive (e.g. VM doesn't route input let mut pfds: Vec<libc::pollfd> = devices
// through the grabbed device). .iter()
let mut pfd = libc::pollfd { .map(|(d, _)| libc::pollfd {
fd: device.as_raw_fd(), fd: d.as_raw_fd(),
events: libc::POLLIN, events: libc::POLLIN,
revents: 0, revents: 0,
}; })
let poll_ret = unsafe { libc::poll(&mut pfd, 1, 100) }; .collect();
let poll_ret = unsafe { libc::poll(pfds.as_mut_ptr(), pfds.len() as libc::nfds_t, 100) };
if poll_ret < 0 { if poll_ret < 0 {
let err = std::io::Error::last_os_error(); let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted { if err.kind() == std::io::ErrorKind::Interrupted {
@ -1217,8 +1380,6 @@ fn run_with_evdev(
} }
if poll_ret == 0 { if poll_ret == 0 {
idle_polls += 1; idle_polls += 1;
// No events available — check for background window changes even
// without a keypress (the background thread polls every 250ms).
if daemon.config.app_state.enabled { if daemon.config.app_state.enabled {
let class = shared_window_class.lock().unwrap().clone(); let class = shared_window_class.lock().unwrap().clone();
if !class.is_empty() && class != last_window_class { if !class.is_empty() && class != last_window_class {
@ -1230,27 +1391,6 @@ fn run_with_evdev(
} }
idle_polls = 0; idle_polls = 0;
let caps = is_caps_lock_on(&device);
let mut key_state = device
.get_key_state()
.ok()
.unwrap_or_else(evdev::AttributeSet::new);
let events = match device.fetch_events() {
Ok(events) => events,
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
// SIGINT/SIGTERM received — loop back to signal check
continue;
}
log_info(&format!(
"[vietc] fetch_events error (non-interrupted): {:?} — exiting",
e
));
return Err(e.into());
}
};
last_event_time = std::time::Instant::now();
// Check for status changes instantly // Check for status changes instantly
if status_changed.load(Ordering::SeqCst) { if status_changed.load(Ordering::SeqCst) {
daemon.sync_status_file(); daemon.sync_status_file();
@ -1263,6 +1403,31 @@ fn run_with_evdev(
config_changed.store(false, Ordering::SeqCst); config_changed.store(false, Ordering::SeqCst);
} }
// Process events from whichever device(s) have data ready
for (i, pfd) in pfds.iter().enumerate() {
if (pfd.revents & libc::POLLIN) == 0 {
continue;
}
let (ref mut device, ref _name) = devices[i];
let caps = device_states[i].1;
let mut key_state = std::mem::take(&mut device_states[i].0);
let events = match device.fetch_events() {
Ok(events) => events,
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
continue;
}
log_info(&format!(
"[vietc] fetch_events error on device {}: {:?} — exiting",
i, e
));
return Err(e.into());
}
};
last_event_time = std::time::Instant::now();
for event in events { for event in events {
if let evdev::InputEventKind::Key(key) = event.kind() { if let evdev::InputEventKind::Key(key) = event.kind() {
let value = event.value(); let value = event.value();
@ -1305,7 +1470,6 @@ fn run_with_evdev(
daemon.write_status(); daemon.write_status();
log_info("[vietc] Password field detected — engine disabled"); log_info("[vietc] Password field detected — engine disabled");
} else if !is_pw && !currently_enabled && daemon.config.start_enabled { } else if !is_pw && !currently_enabled && daemon.config.start_enabled {
// Only re-enable if we're not in a manual toggle state
let default_state = daemon.app_state.get_default_state(); let default_state = daemon.app_state.get_default_state();
if default_state { if default_state {
daemon.engine.set_enabled(true); daemon.engine.set_enabled(true);
@ -1315,9 +1479,6 @@ fn run_with_evdev(
} }
if !grabbed { if !grabbed {
// Legacy mode: raw keystrokes reach the application directly.
// Use process_key for corrections; +1 backspace for control
// keys that landed on screen as literal characters.
if value != 1 { if value != 1 {
continue; continue;
} }
@ -1339,15 +1500,11 @@ fn run_with_evdev(
execute_commands(&*injector, &commands, false); execute_commands(&*injector, &commands, false);
} }
} else { } else {
// Grabbing mode: all output goes through uinput only.
// If Ctrl, Alt, or Meta/Super is pressed, bypass the engine completely and forward raw key events.
if is_modifier_pressed(&key_state) { if is_modifier_pressed(&key_state) {
injector.send_key_event(keycode, value); injector.send_key_event(keycode, value);
continue; continue;
} }
// Backspace in grab mode: pop engine, inject via uinput.
if key == evdev::Key::KEY_BACKSPACE { if key == evdev::Key::KEY_BACKSPACE {
if value == 1 || value == 2 { if value == 1 || value == 2 {
daemon.engine.process_key('\x08'); daemon.engine.process_key('\x08');
@ -1359,23 +1516,15 @@ fn run_with_evdev(
} }
if value == 1 { if value == 1 {
// Press: process through engine
if consumed_keys.contains(&keycode) { if consumed_keys.contains(&keycode) {
consumed_keys.remove(&keycode); consumed_keys.remove(&keycode);
} }
if let Some(mut ch) = key_to_char(key) { if let Some(mut ch) = key_to_char(key) {
// Window change detection: only on character key presses.
// Modifier keys (Ctrl, Alt, Super) skip this block, so
// last_key_time is preserved across Alt+Tab sequences.
let gap = last_key_time.elapsed(); let gap = last_key_time.elapsed();
last_key_time = std::time::Instant::now(); last_key_time = std::time::Instant::now();
// Fast path: check shared window ID from background thread (250ms polling)
let active_window_id = shared_active_window.lock().unwrap().clone(); let active_window_id = shared_active_window.lock().unwrap().clone();
let mut new_window = None; let mut new_window = None;
// On Wayland, window ID may not change (native Wayland apps
// don't have X11 IDs), so also check window class as a fallback.
let active_window_class = shared_window_class.lock().unwrap().clone(); let active_window_class = shared_window_class.lock().unwrap().clone();
if active_window_id != last_active_window { if active_window_id != last_active_window {
@ -1383,12 +1532,8 @@ fn run_with_evdev(
} else if !active_window_class.is_empty() } else if !active_window_class.is_empty()
&& active_window_class != last_window_class && active_window_class != last_window_class
{ {
// Window ID same but class changed — treat as window switch
// (this covers Wayland native app switches)
new_window = Some(active_window_class.clone()); new_window = Some(active_window_class.clone());
} else { } else {
// Always verify active window on every keypress — window
// switches under 100ms can leak the old engine buffer.
if let Some(id) = app_state::get_active_window_id() { if let Some(id) = app_state::get_active_window_id() {
if id != active_window_id { if id != active_window_id {
new_window = Some(id); new_window = Some(id);
@ -1402,8 +1547,6 @@ fn run_with_evdev(
last_active_window, id, gap last_active_window, id, gap
)); ));
last_active_window = id.clone(); last_active_window = id.clone();
// Save the window class when it changes (covers Wayland
// where IDs might be identical for different apps)
if !active_window_class.is_empty() { if !active_window_class.is_empty() {
last_window_class = active_window_class.clone(); last_window_class = active_window_class.clone();
} }
@ -1420,7 +1563,6 @@ fn run_with_evdev(
daemon.check_app_change_with(class); daemon.check_app_change_with(class);
} }
// Re-check password field status on window change
if daemon.config.password_detection.enabled { if daemon.config.password_detection.enabled {
let is_pw = daemon.app_state.check_password_field(); let is_pw = daemon.app_state.check_password_field();
if is_pw && daemon.engine.is_enabled() { if is_pw && daemon.engine.is_enabled() {
@ -1435,10 +1577,6 @@ fn run_with_evdev(
} }
} }
// Periodic password re-check (every 30 keystrokes) —
// catches in-terminal sudo prompts where the window
// doesn't change but the focused widget becomes a
// password field (detected via AT-SPI2).
if daemon.config.password_detection.enabled { if daemon.config.password_detection.enabled {
password_check_counter += 1; password_check_counter += 1;
if password_check_counter >= 30 { if password_check_counter >= 30 {
@ -1474,26 +1612,15 @@ fn run_with_evdev(
)); ));
consumed_keys.insert(keycode); consumed_keys.insert(keycode);
execute_commands(&*injector, &commands, false); execute_commands(&*injector, &commands, false);
// Flush chars: forward raw key after injection.
// When engine is disabled (English mode), the Insert event
// already contains the character — forwarding raw key
// would double-inject (double space on Ctrl+Space toggle).
if is_flush_char(ch) && daemon.engine.is_enabled() { if is_flush_char(ch) && daemon.engine.is_enabled() {
injector.send_key_event(keycode, 1); injector.send_key_event(keycode, 1);
injector.send_key_event(keycode, 0); injector.send_key_event(keycode, 0);
} }
// Skip upcoming auto-repeat pile-up from injection delay
skip_count = 3; skip_count = 3;
} else if daemon.engine.is_enabled() } else if daemon.engine.is_enabled()
&& is_vn_control_key(daemon.app_state.effective_method(), ch) && is_vn_control_key(daemon.app_state.effective_method(), ch)
&& daemon.engine.buffer().chars().count() <= buf_before && daemon.engine.buffer().chars().count() <= buf_before
{ {
// Tone/mark key truly absorbed with no effect (no
// literal character appended) — consume silently.
// When the key is instead kept as a literal base
// letter (e.g. leading "x", the "r" in "tr"), the
// buffer grows and we must forward it like any
// other character so it reaches the screen.
consumed_keys.insert(keycode); consumed_keys.insert(keycode);
} else { } else {
injector.send_key_event(keycode, 1); injector.send_key_event(keycode, 1);
@ -1502,14 +1629,12 @@ fn run_with_evdev(
injector.send_key_event(keycode, 1); injector.send_key_event(keycode, 1);
} }
} else if value == 2 { } else if value == 2 {
// Auto-repeat: skip if consumed or during injection drain
if consumed_keys.contains(&keycode) || skip_count > 0 { if consumed_keys.contains(&keycode) || skip_count > 0 {
if skip_count > 0 { skip_count -= 1; } if skip_count > 0 { skip_count -= 1; }
continue; continue;
} }
injector.send_key_event(keycode, 2); injector.send_key_event(keycode, 2);
} else if value == 0 { } else if value == 0 {
// Release: skip if consumed, else forward
if consumed_keys.contains(&keycode) { if consumed_keys.contains(&keycode) {
consumed_keys.remove(&keycode); consumed_keys.remove(&keycode);
continue; continue;
@ -1519,6 +1644,10 @@ fn run_with_evdev(
} }
} }
} }
// Save updated key state back
device_states[i].0 = key_state;
}
} }
} }
@ -1554,10 +1683,10 @@ fn run_stdin_mode(
config_changed.store(false, Ordering::SeqCst); config_changed.store(false, Ordering::SeqCst);
} }
if let Ok((device, path)) = open_keyboard_device() { if let Ok(mut devices) = open_keyboard_devices() {
log_info(&format!("[vietc] Keyboard device found: {}", path)); log_info(&format!("[vietc] Keyboard device(s) found: {}", devices.len()));
return run_with_evdev( return run_with_evdev(
device, &mut devices,
daemon, daemon,
shared_active_window, shared_active_window,
shared_window_class, shared_window_class,

View file

@ -7,6 +7,26 @@ type Display = c_void;
type Window = u64; type Window = u64;
type Atom = u64; type Atom = u64;
type Time = u64; type Time = u64;
type KeySym = u64;
#[repr(C)]
struct XKeyEvent {
_type: c_int,
_serial: u64,
_send_event: c_int,
_display: *mut Display,
window: u64,
_root: u64,
_subwindow: u64,
_time: u64,
_x: c_int,
_y: c_int,
_x_root: c_int,
_y_root: c_int,
state: c_int,
keycode: u32,
_same_screen: c_int,
}
extern "C" { extern "C" {
fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void; fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void;
@ -40,6 +60,9 @@ struct X11Lib {
x_destroy_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_pending: unsafe extern "C" fn(*mut Display) -> c_int,
x_next_event: unsafe extern "C" fn(*mut Display, *mut XEvent), x_next_event: unsafe extern "C" fn(*mut Display, *mut XEvent),
x_query_keymap: unsafe extern "C" fn(*mut Display, *mut c_char) -> c_int,
x_lookup_string: unsafe extern "C" fn(*const XKeyEvent, *mut c_char, c_int, *mut KeySym, *mut c_int) -> c_int,
x_utf8_lookup_string: Option<unsafe extern "C" fn(*mut c_void, *const XKeyEvent, *mut c_char, c_int, *mut KeySym, *mut c_int) -> c_int>,
} }
impl X11Lib { impl X11Lib {
@ -95,6 +118,12 @@ impl X11Lib {
let x_destroy_window = sym!(x11_handle, "XDestroyWindow"); let x_destroy_window = sym!(x11_handle, "XDestroyWindow");
let x_pending = sym!(x11_handle, "XPending"); let x_pending = sym!(x11_handle, "XPending");
let x_next_event = sym!(x11_handle, "XNextEvent"); let x_next_event = sym!(x11_handle, "XNextEvent");
let x_query_keymap = sym!(x11_handle, "XQueryKeymap");
let x_lookup_string = sym!(x11_handle, "XLookupString");
let x_utf8_lookup_string = {
let p = dlsym(x11_handle, b"Xutf8LookupString\0".as_ptr() as *const c_char);
if p.is_null() { None } else { Some(std::mem::transmute(p)) }
};
let x_test_fake_key_event = sym!(xtst_handle, "XTestFakeKeyEvent"); let x_test_fake_key_event = sym!(xtst_handle, "XTestFakeKeyEvent");
Ok(Self { Ok(Self {
@ -114,6 +143,9 @@ impl X11Lib {
x_destroy_window, x_destroy_window,
x_pending, x_pending,
x_next_event, x_next_event,
x_query_keymap,
x_lookup_string,
x_utf8_lookup_string,
}) })
} }
} }
@ -553,3 +585,109 @@ impl KeyInjector for X11Injector {
InjectResult::Success InjectResult::Success
} }
} }
/// X11 keymap-based capture: polls XQueryKeymap periodically to detect
/// key presses/releases. No XRecord, no XGrabKeyboard — works on any X11
/// system including VMs where evdev produces no events.
pub struct X11KeymapCapture {
lib: X11Lib,
display: *mut Display,
prev_keys: [u8; 32],
}
unsafe impl Send for X11KeymapCapture {}
impl X11KeymapCapture {
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".into());
}
Ok(Self {
lib,
display,
prev_keys: [0u8; 32],
})
}
}
/// Poll the current keymap and return any newly pressed or released keycodes.
/// Returns a Vec of (keycode_in_evdev_format, pressed) tuples.
/// X11 keycodes use offset 8 from evdev codes: evdev = x11 - 8.
pub fn poll(&mut self) -> Vec<(u32, bool)> {
let mut keys = [0u8; 32];
unsafe {
(self.lib.x_query_keymap)(self.display, keys.as_mut_ptr() as *mut c_char);
}
let mut events = Vec::new();
for i in 0..32 {
let changed = keys[i] ^ self.prev_keys[i];
if changed == 0 {
continue;
}
for bit in 0..8 {
if (changed >> bit) & 1 != 0 {
let x11_keycode = (i * 8 + bit) as u32;
let pressed = (keys[i] >> bit) & 1;
// Convert from X11 keycode to evdev keycode (subtract 8)
if x11_keycode >= 8 {
events.push((x11_keycode - 8, pressed == 1));
}
}
}
}
self.prev_keys = keys;
events
}
/// Convert an evdev keycode + modifier state to a character.
/// `state` is the X11 modifier bitmask (Shift=1, Lock=2, Ctrl=4, Mod1=8, etc.)
pub fn lookup_keycode(&self, keycode: u32, state: c_int) -> Option<char> {
let x11_keycode = keycode + 8;
unsafe {
let mut xke: XKeyEvent = std::mem::zeroed();
xke._type = 2; // KeyPress
xke._display = self.display;
xke.keycode = x11_keycode;
xke.state = state;
let mut buf = [0u8; 32];
let mut keysym: KeySym = 0;
let len = if let Some(xutf8) = self.lib.x_utf8_lookup_string {
xutf8(
std::ptr::null_mut(),
&mut xke as *mut XKeyEvent,
buf.as_mut_ptr() as *mut c_char,
buf.len() as c_int,
&mut keysym,
std::ptr::null_mut(),
)
} else {
(self.lib.x_lookup_string)(
&mut xke as *mut XKeyEvent,
buf.as_mut_ptr() as *mut c_char,
buf.len() as c_int,
&mut keysym,
std::ptr::null_mut(),
)
};
if len > 0 {
let s = std::str::from_utf8(&buf[..len as usize]).ok()?;
s.chars().next()
} else {
None
}
}
}
}
impl Drop for X11KeymapCapture {
fn drop(&mut self) {
unsafe {
(self.lib.x_close_display)(self.display);
}
}
}