diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 86bcf2a..7e5f37c 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -42,6 +42,7 @@ use config::Config; #[cfg(feature = "x11")] use vietc_protocol::x11_capture::X11Capture; +use vietc_protocol::x11_capture::SKIP_RECORD_EVENTS; #[cfg(feature = "x11")] use vietc_protocol::x11_inject::X11Injector; @@ -173,10 +174,6 @@ impl Daemon { if let Ok(content) = fs::read_to_string(&status_path) { let expect_enabled = content.trim() == "vn"; if self.engine.is_enabled() != expect_enabled { - log_info(&format!( - "[vietc] Syncing enabled status from file: {}", - expect_enabled - )); self.engine.set_enabled(expect_enabled); self.engine_enabled.store(expect_enabled, Ordering::SeqCst); } @@ -193,7 +190,6 @@ impl Daemon { return false; } - log_info("[vietc] Config changed, reloading..."); match Config::load_from(&self.config_path) { Ok(new_config) => { let method = match new_config.input_method.as_str() { @@ -216,51 +212,21 @@ impl Daemon { self.grab_enabled = new_config.grab; self.config = new_config; self.config_modified = modified; - log_info("[vietc] Config reloaded successfully"); true } - Err(e) => { - log_info(&format!("[vietc] Failed to reload config: {}", e)); - false - } + Err(_) => false, } } fn process_key(&mut self, ch: char) -> Vec { let mut commands = Vec::new(); - // Log each keystroke with character info - log_info(&format!( - "[vietc] process_key: U+{:04X} '{}' raw_buffer='{}' enabled={}", - ch as u32, - ch, - self.engine.buffer(), - self.engine.is_enabled() - )); - if let Some(event) = self.engine.process_key(ch) { - log_info(&format!( - "[vietc] key='{}' buf='{}' -> {:?}", - ch, - self.engine.buffer(), - event - )); match event { EngineEvent::Flush(text) => { - log_info(&format!( - "[vietc] Flush text len={}, bytes={} text={}", - text.len(), - text.len() * 3, - text.escape_default() - )); commands.push(OutputCommand::Type(text)); } EngineEvent::Insert(text) => { - log_info(&format!( - "[vietc] Insert text len={}, text={}", - text.len(), - text - )); commands.push(OutputCommand::Type(text)); } EngineEvent::AutoRestore(word) => { @@ -269,10 +235,6 @@ impl Daemon { commands.push(OutputCommand::Type(word)); } EngineEvent::Replace { backspaces, insert } => { - log_info(&format!( - "[vietc] Replace BS={} text=\"{}\"", - backspaces, insert - )); commands.push(OutputCommand::Backspace(backspaces)); commands.push(OutputCommand::Type(insert)); } @@ -280,31 +242,16 @@ impl Daemon { backspaces, restored, } => { - log_info(&format!( - "[vietc] UndoTones BS={} restored=\"{}\"", - backspaces, restored - )); commands.push(OutputCommand::Backspace(backspaces)); commands.push(OutputCommand::Type(restored)); } EngineEvent::Paste(text) => { - log_info(&format!( - "[vietc] Paste raw text len={}, bytes={} text={}", - text.len(), - text.len() * 3, - text.escape_default() - )); - // Exit paste mode after pasting self.engine.exit_paste_mode(); commands.push(OutputCommand::Type(text)); } } } else { - log_info(&format!( - "[vietc] key='{}' -> (no event, buf='{}')", - ch, - self.engine.buffer() - )); + // No event — key was consumed or ignored by engine } commands @@ -312,25 +259,13 @@ impl Daemon { fn toggle(&mut self) { let new_state = self.app_state.toggle_current_app(); - log_info(&format!( - "[vietc] toggle: engine.enabled={}", - self.engine.is_enabled() - )); self.engine.set_enabled(new_state); self.write_status(); // Reset engine buffer when enabling Vietnamese mode to clear stale state if new_state { - log_info(&format!( - "[vietc] reset() called - raw_buffer='{}' before reset", - self.engine.buffer() - )); self.engine.reset(); - log_info(&format!( - "[vietc] after reset() - raw_buffer='{}'", - self.engine.buffer() - )); } } @@ -375,14 +310,6 @@ impl Daemon { &self.keystroke_history, ); - log_info(&format!( - "[vietc] replay: history_len={} old_screen='{}' new_output='{}' flush={}", - self.keystroke_history.len(), - self.screen_output, - new_output, - did_flush - )); - if did_flush { // Engine flushed a word — commit it and clear state // The flush char (space/period/etc) was NOT in history, so we need to @@ -439,13 +366,6 @@ impl Daemon { ) }; - log_info(&format!( - "[vietc] replay_backspace: history_len={} old_screen='{}' new_output='{}'", - self.keystroke_history.len(), - self.screen_output, - new_output - )); - // Calculate diff let backspaces = self.screen_output.chars().count(); if backspaces > 0 { @@ -576,7 +496,7 @@ fn main() -> Result<(), Box> { #[cfg(feature = "x11")] if display != display::DisplayServer::Wayland { - if let Some(mut capture) = X11Capture::new() { + if let Some(capture) = X11Capture::new() { // XRecord captures events globally — no grab needed for capture. // XGrabKeyboard on the same display as XRecord breaks event delivery. log_info("[vietc] X11 XRecord capture active — using X11 capture/injection"); @@ -724,7 +644,6 @@ fn run_with_x11( { let active_window = shared_active_window.lock().unwrap().clone(); if active_window != last_active_window { - log_info(&format!("[vietc] Window changed: '{}' -> '{}'", last_active_window, active_window)); last_active_window = active_window.clone(); daemon.replay_reset(); } @@ -737,23 +656,22 @@ fn run_with_x11( // Reset on focus loss (VMK technique) if capture.focus_lost { - eprintln!("[vietc] Focus lost — resetting engine state"); daemon.replay_reset(); pressed_keys.clear(); capture.focus_lost = false; } - // Wait for events with 100ms timeout - let got_data = capture.wait_for_event(100); + // Wait for events with 100ms timeout. + // SKIP_RECORD_EVENTS may still be true from a previous injection — + // drain_pipe drops any stale injected events while flag is true. + let _got_data = capture.wait_for_event(100); + // NOW safe to clear: any injected events from last iteration were dropped. + SKIP_RECORD_EVENTS.store(false, Ordering::Relaxed); let evt = capture.next_event(); if evt.is_none() { - if got_data { - eprintln!("[vietc] DEBUG: select said data but no event in queue"); - } continue; } let event = evt.unwrap(); - eprintln!("[vietc] GOT KEY EVENT: keycode={} pressed={} ch={:?} state={}", event.keycode, event.pressed, event.ch, event.state); // Process this event { @@ -776,9 +694,9 @@ fn run_with_x11( // Modifier or non-character key → forward press only, reset replay if capture.is_modifier_pressed(event.state) || event.ch.is_none() { daemon.replay_reset(); - capture.without_grab(|| { - let _ = injector.send_key_event(event.keycode as u16, 1); - }); + SKIP_RECORD_EVENTS.store(true, Ordering::Relaxed); + let _ = injector.send_key_event(event.keycode as u16, 1); + // Flag stays true — cleared at top of next iteration after drain continue; } @@ -786,43 +704,34 @@ fn run_with_x11( if let Some(ch) = event.ch { match ch { '\x08' => { - // Backspace: replay pattern pops from history let commands = daemon.replay_backspace(); pressed_keys.remove(&event.keycode); - capture.without_grab(|| { - execute_commands(&*injector, &commands, true); - }); - // If history is empty and commands only had a bare backspace, - // we need to actually send it + SKIP_RECORD_EVENTS.store(true, Ordering::Relaxed); + execute_commands(&*injector, &commands, true); if daemon.keystroke_history.is_empty() && commands.is_empty() { - capture.without_grab(|| { - let _ = injector.send_backspace(); - }); + let _ = injector.send_backspace(); } } '\n' => { pressed_keys.remove(&event.keycode); daemon.replay_reset(); - capture.without_grab(|| { - let _ = injector.send_key_event(event.keycode as u16, 1); - let _ = injector.send_key_event(event.keycode as u16, 0); - }); + SKIP_RECORD_EVENTS.store(true, Ordering::Relaxed); + let _ = injector.send_key_event(event.keycode as u16, 1); + let _ = injector.send_key_event(event.keycode as u16, 0); } _ => { let commands = daemon.replay_and_inject(ch); pressed_keys.remove(&event.keycode); - capture.without_grab(|| { - execute_commands(&*injector, &commands, true); - }); + SKIP_RECORD_EVENTS.store(true, Ordering::Relaxed); + execute_commands(&*injector, &commands, true); } } } } else { // Key release — only inject if we were tracking this key if pressed_keys.remove(&event.keycode) { - capture.without_grab(|| { - let _ = injector.send_key_event(event.keycode as u16, 0); - }); + SKIP_RECORD_EVENTS.store(true, Ordering::Relaxed); + let _ = injector.send_key_event(event.keycode as u16, 0); } } } @@ -1139,31 +1048,17 @@ fn execute_commands( } else { *count }; - log_info(&format!( - "[vietc] cmd: Backspace({}) -> adjusted={}", - count, adjusted - )); pending_backspaces += adjusted; } OutputCommand::Type(text) => { - log_info(&format!("[vietc] cmd: Type(\"{}\")", text)); pending_text.push_str(text); } } } if pending_backspaces > 0 || !pending_text.is_empty() { - log_info(&format!( - "[vietc] inject: BS={} text=\"{}\"", - pending_backspaces, pending_text - )); - - // Use injector for text (ydotool/xdotool/wtype) let _ = injector.inject_replacement(pending_backspaces, &pending_text); } else if !commands.is_empty() { - // Empty text but commands exist (e.g. Backspace only or Flush empty string) - log_info(&format!("[vietc] inject: BS={}", pending_backspaces)); - let _ = injector.inject_replacement(pending_backspaces, &pending_text); } diff --git a/packaging/appimage/build-appimage.sh b/packaging/appimage/build-appimage.sh index 12069c5..b564c2c 100644 --- a/packaging/appimage/build-appimage.sh +++ b/packaging/appimage/build-appimage.sh @@ -52,6 +52,17 @@ else echo " xclip not found on system, skipping" fi +# Compile and bundle vietc-xrecord (C helper for XRecord keyboard capture) +echo " Compiling vietc-xrecord..." +if command -v gcc &>/dev/null; then + gcc -O2 -o "$APPDIR/usr/bin/vietc-xrecord" "$SCRIPT_DIR/vietc-xrecord.c" -lX11 -lXtst -ldl 2>&1 + echo " vietc-xrecord bundled" +else + echo " gcc not found, trying cc..." + cc -O2 -o "$APPDIR/usr/bin/vietc-xrecord" "$SCRIPT_DIR/vietc-xrecord.c" -lX11 -lXtst -ldl 2>&1 + echo " vietc-xrecord bundled" +fi + # Desktop integration echo "[3/5] Installing desktop integration..." if [ -f "deb-build/vietc.desktop" ]; then diff --git a/packaging/appimage/vietc-xrecord.c b/packaging/appimage/vietc-xrecord.c new file mode 100644 index 0000000..b66bc8c --- /dev/null +++ b/packaging/appimage/vietc-xrecord.c @@ -0,0 +1,121 @@ +// vietc-xrecord: Captures keyboard events via XRecord (blocking mode) +// and writes fixed-size binary records to stdout. +// The parent vietc daemon reads events from the pipe. +// +// Pipe event format: 8 bytes, packed +// keycode: u8 (0 for focus events) +// pressed: u8 (1=press, 0=release, 0 for focus) +// state: u16 (modifier mask for keys, 1=FocusIn, 2=FocusOut) +// pad: [u8;4] +// +// Compile: gcc -O2 -o vietc-xrecord vietc-xrecord.c -lX11 -lXtst + +#include +#include +#include +#include +#include +#include +#include + +#pragma pack(push, 1) +typedef struct { + unsigned char keycode; + unsigned char pressed; + unsigned short state; + unsigned char padding[4]; +} PipeEvent; +#pragma pack(pop) + +static void write_event(const PipeEvent *ev) { + const char *ptr = (const char *)ev; + size_t remaining = sizeof(PipeEvent); + while (remaining > 0) { + ssize_t n = write(STDOUT_FILENO, ptr, remaining); + if (n < 0) { + if (errno == EINTR) continue; + _exit(1); + } + ptr += n; + remaining -= n; + } +} + +static void record_cb(XPointer closure, XRecordInterceptData *data) { + if (data->category != XRecordFromServer) + return; + if (data->data_len < 2) + return; + + unsigned char *ev = data->data; + unsigned char event_type = ev[0]; + + if (event_type != 2 && event_type != 3 && + event_type != 9 && event_type != 10) + return; + + PipeEvent out; + memset(&out, 0, sizeof(out)); + + switch (event_type) { + case 2: /* KeyPress */ + out.keycode = ev[1]; + out.pressed = 1; + if (data->data_len >= 4) + out.state = *(unsigned short *)(ev + 2); + write_event(&out); + break; + + case 3: /* KeyRelease */ + out.keycode = ev[1]; + out.pressed = 0; + if (data->data_len >= 4) + out.state = *(unsigned short *)(ev + 2); + write_event(&out); + break; + + case 9: /* FocusIn */ + out.keycode = 0; + out.pressed = 0; + out.state = 1; + write_event(&out); + break; + + case 10: /* FocusOut */ + out.keycode = 0; + out.pressed = 0; + out.state = 2; + write_event(&out); + break; + + default: + break; + } +} + +int main(void) { + Display *dpy = XOpenDisplay(NULL); + if (!dpy) { fprintf(stderr, "vietc-xrecord: no display\n"); return 1; } + + int major = 0, minor = 0; + XRecordQueryVersion(dpy, &major, &minor); + + XRecordRange *range = XRecordAllocRange(); + if (!range) { fprintf(stderr, "vietc-xrecord: XRecordAllocRange failed\n"); return 1; } + range->device_events.first = KeyPress; + range->device_events.last = FocusOut; + + XRecordClientSpec spec = XRecordAllClients; + XRecordContext ctx = XRecordCreateContext(dpy, 0, &spec, 1, &range, 1); + XFree(range); + if (!ctx) { fprintf(stderr, "vietc-xrecord: XRecordCreateContext failed\n"); return 1; } + + fprintf(stderr, "vietc-xrecord: ready (XRecord %d.%d, ctx=%ld)\n", major, minor, (long)ctx); + fflush(stderr); + + /* BLOCK here — callback fires for each keyboard/focus event */ + XRecordEnableContext(dpy, ctx, record_cb, NULL); + + XCloseDisplay(dpy); + return 0; +} diff --git a/protocol/src/x11_capture.rs b/protocol/src/x11_capture.rs index eeea8cc..7323d8d 100644 --- a/protocol/src/x11_capture.rs +++ b/protocol/src/x11_capture.rs @@ -1,231 +1,44 @@ -use std::ffi::{c_char, c_int, c_void}; use std::collections::VecDeque; -use std::sync::{Arc, Mutex}; +use std::ffi::{c_char, c_int, c_void}; +use std::io::{Read, BufRead}; +use std::os::unix::io::AsRawFd; +use std::process::{Command, Child, Stdio}; +use std::sync::atomic::{AtomicBool, Ordering}; type Display = c_void; -type Window = u64; -type Time = u64; -// X11 event types const KEY_PRESS: c_int = 2; -const KEY_RELEASE: c_int = 3; -// X11 modifier masks const CONTROL_MASK: c_int = 4; const MOD1_MASK: c_int = 8; const MOD4_MASK: c_int = 64; -// Grab modes -const GRAB_MODE_ASYNC: c_int = 1; - -// XRecord categories -const XRECORD_FROM_SERVER: c_int = 1; - 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; -} - -struct X11Lib { - 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_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_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 c_int>, - x_flush: unsafe extern "C" fn(*mut Display) -> c_int, - x_connection_number: unsafe extern "C" fn(*mut Display) -> c_int, - x_pending: 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, *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: 32 bytes total -// device_events is at offset 18 (XRecordRange8: first=offset 18, last=offset 19) -#[repr(C)] -struct XRecordRange { - _bytes: [u8; 32], -} - -type XRecordCallback = unsafe extern "C" fn(*mut c_void, *mut XRecordInterceptData); - -// XRecordInterceptData: matches C layout exactly -// C: { XID id_base; Time server_time; unsigned long client_seq; -// int category; Bool client_swapped; unsigned char *data; unsigned long data_len; } -#[repr(C)] -struct XRecordInterceptData { - id_base: u64, // XID - server_time: u64, // Time - client_seq: u64, // unsigned long - category: c_int, - client_swapped: c_int, - data: *mut u8, - data_len: u64, // unsigned long + fn poll(fds: *mut PollFd, nfds: u64, timeout: i32) -> i32; } #[repr(C)] -struct Timeval { - tv_sec: i64, - tv_usec: i64, +struct PollFd { + fd: i32, + events: i16, + revents: i16, } -#[repr(C)] -struct FdSet { - fds_bits: [u64; 16], -} +const POLLIN: i16 = 1; -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_bit(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 { - fn new() -> Result> { - unsafe { - let paths = [ - b"libX11.so.6\0".as_ptr() as *const c_char, - b"libX11.so\0".as_ptr() as *const c_char, - ]; - let mut handle = std::ptr::null_mut(); - for path in paths { - handle = dlopen(path, 1); - if !handle.is_null() { - break; - } - } - if handle.is_null() { - return Err("Failed to load libX11.so.6".into()); - } - - macro_rules! sym { - ($name:expr) => { - std::mem::transmute(dlsym(handle, concat!($name, "\0").as_ptr() as *const c_char)) - }; - } - - // 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_close_display = sym!("XCloseDisplay"); - let x_default_root_window = sym!("XDefaultRootWindow"); - let x_grab_keyboard = sym!("XGrabKeyboard"); - let x_ungrab_keyboard = sym!("XUngrabKeyboard"); - 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 = if x_utf8_lookup_string.is_null() { - None - } else { - Some(std::mem::transmute(x_utf8_lookup_string)) - }; - let x_flush = sym!("XFlush"); - let x_connection_number = sym!("XConnectionNumber"); - let x_pending = sym!("XPending"); - - 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 { - handle, - x_open_display, - x_close_display, - x_default_root_window, - x_grab_keyboard, - x_ungrab_keyboard, - x_lookup_string, - x_utf8_lookup_string, - x_flush, - x_connection_number, - x_pending, - 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, - }) - } - } -} - -impl Drop for X11Lib { - fn drop(&mut self) { - unsafe { - dlclose(self.handle); - } - } -} - -#[derive(Copy, Clone)] #[repr(C)] struct XKeyEvent { _type: c_int, _serial: u64, _send_event: c_int, _display: *mut Display, - window: Window, - _root: Window, - _subwindow: Window, - _time: Time, + window: u64, + _root: u64, + _subwindow: u64, + _time: u64, _x: c_int, _y: c_int, _x_root: c_int, @@ -244,63 +57,120 @@ pub struct X11KeyEvent { pub state: c_int, } -// Shared event queue between XRecord callback and capture reader -struct EventQueue { - queue: VecDeque, +pub static SKIP_RECORD_EVENTS: AtomicBool = AtomicBool::new(false); + +struct LookupLib { + handle: *mut c_void, + display: *mut Display, + x_close_display: unsafe extern "C" fn(*mut Display) -> 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 c_int>, } -static mut EVENT_QUEUE: Option>> = None; +unsafe impl Send for LookupLib {} -unsafe extern "C" fn record_callback(_closure: *mut c_void, data: *mut XRecordInterceptData) { - if data.is_null() { - return; - } - if (*data).category != XRECORD_FROM_SERVER { - return; - } - let data_len_bytes = (*data).data_len * 4; // data_len is in 4-byte units - if data_len_bytes < 2 { - return; - } - let data_bytes = (*data).data; - if data_bytes.is_null() { - return; - } - 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; - } - - // XRecord data layout for keyboard events: type(1) + keycode(1) + state(2) - let state: c_int = if data_len_bytes >= 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); +impl Drop for LookupLib { + fn drop(&mut self) { + unsafe { + (self.x_close_display)(self.display); + dlclose(self.handle); } } } +impl LookupLib { + fn new() -> Option { + unsafe { + let paths = [ + b"libX11.so.6\0".as_ptr() as *const c_char, + b"libX11.so\0".as_ptr() as *const c_char, + ]; + let mut handle = std::ptr::null_mut(); + for path in paths { + handle = dlopen(path, 1); + if !handle.is_null() { break; } + } + if handle.is_null() { return None; } + + macro_rules! sym { + ($name:expr) => { + std::mem::transmute(dlsym(handle, concat!($name, "\0").as_ptr() as *const c_char)) + }; + } + + let x_open_display: unsafe extern "C" fn(*const c_char) -> *mut Display = sym!("XOpenDisplay"); + let display = x_open_display(std::ptr::null()); + if display.is_null() { + dlclose(handle); + return None; + } + + Some(Self { + handle, + display, + x_close_display: sym!("XCloseDisplay"), + x_lookup_string: sym!("XLookupString"), + x_utf8_lookup_string: { + let p = dlsym(handle, b"Xutf8LookupString\0".as_ptr() as *const c_char); + if p.is_null() { None } else { Some(std::mem::transmute(p)) } + }, + }) + } + } + + fn lookup_keycode(&self, keycode: u32, state: c_int) -> Option { + unsafe { + let mut xke: XKeyEvent = std::mem::zeroed(); + xke._type = KEY_PRESS; + xke.keycode = keycode; + xke.state = state; + + let mut buf = [0u8; 32]; + let mut keysym: KeySym = 0; + let len = if let Some(xutf8) = self.x_utf8_lookup_string { + xutf8( + &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.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 + } + } + } +} + +/// Pipe event from C helper: 8 bytes, packed. +#[repr(C, packed)] +#[derive(Clone, Copy)] +struct PipeEvent { + keycode: u8, + pressed: u8, + state: u16, + _padding: [u8; 4], +} + pub struct X11Capture { - lib: X11Lib, - display: *mut Display, - root: Window, - grabbed: bool, - record_context: u64, - record_display: *mut Display, + child: Child, + pipe_fd: i32, + pipe_stdout: Option, + lookup: LookupLib, + event_queue: VecDeque, pub focus_lost: bool, } @@ -308,181 +178,206 @@ unsafe impl Send for X11Capture {} impl X11Capture { pub fn new() -> Option { - let lib = match X11Lib::new() { - Ok(lib) => lib, - Err(e) => { - eprintln!("[vietc] X11Capture: failed to load: {}", e); - return None; - } - }; + let lookup = LookupLib::new(); + let xrecord_path = find_xrecord_binary(); + eprintln!("[vietc] X11Capture: spawning vietc-xrecord from {}", xrecord_path); + let mut child = Command::new(&xrecord_path) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .ok()?; + + // Wait for "ready" on stderr + { + let stderr = child.stderr.take()?; + let mut reader = std::io::BufReader::new(stderr); + let mut line = String::new(); + reader.read_line(&mut line).ok()?; + eprintln!("[vietc] vietc-xrecord: {}", line.trim()); + } + + let stdout = child.stdout.take()?; + let pipe_fd = stdout.as_raw_fd(); + + // Set pipe to non-blocking unsafe { - let display = (lib.x_open_display)(std::ptr::null()); - if display.is_null() { - eprintln!("[vietc] X11Capture: cannot open display"); - return None; - } - - let root = (lib.x_default_root_window)(display); - - // Check XRecord version - let mut major = 0i32; - let mut minor = 0i32; - if (lib.x_record_query_version)(display, &mut major, &mut minor) == 0 { - eprintln!("[vietc] X11Capture: XRecord extension not available"); - (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: device_events at offset 18 - // XRecordRange8: first byte, last byte - (*range)._bytes[18] = KEY_PRESS as u8; // device_events.first - (*range)._bytes[19] = KEY_RELEASE as u8; // device_events.last - eprintln!("[vietc] X11Capture: range set (KeyPress={}, KeyRelease={})", KEY_PRESS, KEY_RELEASE); - - // Create XRecord context - // XRecordClientSpec is XID = unsigned long (8 bytes on x86_64) - let mut spec: u64 = 3; // XRecordAllClients = 3 - let mut range_ptr: *mut XRecordRange = range; - let ctx = (lib.x_record_create_context)( - display, - 0, // own_client - &mut spec as *mut u64 as *mut c_int, // client_spec (pointer to unsigned long) - 1, // nclients - &mut range_ptr, // ranges (pointer to array of range pointers) - 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; - } - eprintln!("[vietc] X11Capture: XRecord context created (ctx={})", ctx); - - // 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 { - lib, - display, - root, - grabbed: false, - record_context: ctx, - record_display: display, - focus_lost: false, - }) + let flags = libc::fcntl(pipe_fd, libc::F_GETFL); + libc::fcntl(pipe_fd, libc::F_SETFL, flags | libc::O_NONBLOCK); } + + let lookup = lookup?; + + Some(Self { + child, + pipe_fd, + pipe_stdout: Some(stdout), + lookup, + event_queue: VecDeque::new(), + focus_lost: false, + }) } - pub fn grab_keyboard(&mut self) -> bool { - unsafe { - let status = (self.lib.x_grab_keyboard)( - self.display, - self.root, - 0, // owner_events = False — block events from reaching apps - GRAB_MODE_ASYNC, - GRAB_MODE_ASYNC, - 0, - ) as i32; - if status == 0 { - self.grabbed = true; - (self.lib.x_flush)(self.display); - eprintln!("[vietc] X11Capture: keyboard grabbed (blocking apps)"); - true - } else { - eprintln!("[vietc] X11Capture: grab failed status={}", status); - false - } - } - } + pub fn grab_keyboard(&mut self) -> bool { false } + pub fn ungrab_keyboard(&mut self) {} + pub fn is_grabbed(&self) -> bool { false } - pub fn ungrab_keyboard(&mut self) { - if self.grabbed { - unsafe { - (self.lib.x_ungrab_keyboard)(self.display, 0); - (self.lib.x_flush)(self.display); - } - self.grabbed = false; - } - } - - pub fn is_grabbed(&self) -> bool { - self.grabbed - } - - /// Wait for XRecord data to arrive, with timeout. - /// Uses XPending() first (checks Xlib internal buffer), then select() on fd. + /// Wait for events from the C helper pipe with timeout. pub fn wait_for_event(&mut self, timeout_ms: u64) -> bool { + // If SKIP_RECORD_EVENTS is true, aggressively drain all pending events + // before clearing the flag. This prevents feedback loops where injected + // events arrive after drain_pipe returns but before the flag is cleared. + if SKIP_RECORD_EVENTS.load(Ordering::Relaxed) { + let deadline = std::time::Instant::now() + std::time::Duration::from_millis(50); + loop { + self.drain_pipe(); + if std::time::Instant::now() >= deadline { + break; + } + // Poll with short timeout to check for more data + let mut pfd = PollFd { + fd: self.pipe_fd, + events: POLLIN, + revents: 0, + }; + unsafe { + poll(&mut pfd, 1, 5); // 5ms poll + } + if pfd.revents & POLLIN == 0 { + // No more data, check one more time after a tiny sleep + std::thread::sleep(std::time::Duration::from_micros(500)); + self.drain_pipe(); + break; + } + } + } + + // Normal wait for events + self.drain_pipe(); + + if !self.event_queue.is_empty() { + return true; + } + + // Poll the pipe fd + let mut pfd = PollFd { + fd: self.pipe_fd, + events: POLLIN, + revents: 0, + }; unsafe { - (self.lib.x_flush)(self.display); + poll(&mut pfd, 1, timeout_ms as i32); + } - // First check: XPending reads from Xlib's internal buffer. - // XRecord data may already be buffered there by a previous read. - let pending = (self.lib.x_pending)(self.display); - if pending > 0 { - (self.lib.x_record_process_replies)(self.display); - return true; - } + if pfd.revents & POLLIN != 0 { + self.drain_pipe(); + } - // Second check: select() on the X11 socket fd - let fd = (self.lib.x_connection_number)(self.display); - let mut readfds: FdSet = std::mem::zeroed(); - fd_zero(&mut readfds); - fd_set_bit(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); - if n > 0 && fd_isset(fd, &readfds) { - // Flush to move data from socket into Xlib buffer - (self.lib.x_flush)(self.display); - (self.lib.x_record_process_replies)(self.display); - true - } else { - false + // Check if child is still alive + if let Ok(None) = self.child.try_wait() { + // Still running + } else { + eprintln!("[vietc] vietc-xrecord process died, restarting..."); + self.restart_xrecord(); + } + + !self.event_queue.is_empty() + } + + fn drain_pipe(&mut self) { + if let Some(ref mut stdout) = self.pipe_stdout { + let mut buf = [0u8; 8]; + let mut filled = 0usize; + loop { + match stdout.read(&mut buf[filled..]) { + Ok(0) => break, + Ok(n) => { + filled += n; + while filled >= 8 { + let ev: PipeEvent = unsafe { std::mem::transmute(buf) }; + + // Skip injected events when flag is set (prevents feedback loops) + if SKIP_RECORD_EVENTS.load(Ordering::Relaxed) { + // Still handle focus events even during skip + if ev.keycode == 0 && ev.state == 2 { + self.focus_lost = true; + } + } else if ev.keycode == 0 && ev.pressed == 0 { + if ev.state == 2 { + self.focus_lost = true; + } + } else { + let event = X11KeyEvent { + keycode: ev.keycode as u32, + ch: None, + pressed: ev.pressed == 1, + state: ev.state as c_int, + }; + self.event_queue.push_back(event); + } + + filled -= 8; + if filled > 0 { + buf.copy_within(8..8 + filled, 0); + } + } + } + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break, + Err(_) => break, + } } } } + fn restart_xrecord(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + self.pipe_stdout = None; + + let xrecord_path = find_xrecord_binary(); + if let Ok(mut child) = Command::new(&xrecord_path) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + if let Some(stderr) = child.stderr.take() { + let mut reader = std::io::BufReader::new(stderr); + let mut line = String::new(); + let _ = reader.read_line(&mut line); + eprintln!("[vietc] vietc-xrecord restarted: {}", line.trim()); + } + if let Some(stdout) = child.stdout.take() { + let fd = stdout.as_raw_fd(); + unsafe { + let flags = libc::fcntl(fd, libc::F_GETFL); + libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK); + } + self.pipe_fd = fd; + self.pipe_stdout = Some(stdout); + } + self.child = child; + } + } + pub fn next_event(&mut self) -> Option { - unsafe { - 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); - } - } - } + if let Some(mut event) = self.event_queue.pop_front() { + event.ch = self.lookup.lookup_keycode(event.keycode, event.state); + Some(event) + } else { + None } - None } pub fn is_modifier_pressed(&self, state: c_int) -> bool { (state & (CONTROL_MASK | MOD1_MASK | MOD4_MASK)) != 0 } + /// Drain any pending events from the pipe without adding them to the queue. + /// Used after injection to clear feedback events while SKIP_RECORD_EVENTS is set. + pub fn drain_injected(&mut self) { + self.drain_pipe(); + } + pub fn with_grab(&mut self, f: F) -> T where F: FnOnce() -> T, @@ -494,63 +389,38 @@ impl X11Capture { where F: FnOnce() -> T, { - if self.grabbed { - self.ungrab_keyboard(); - let result = f(); - self.grab_keyboard(); - result - } else { - f() - } - } - - pub fn lookup_keycode(&self, keycode: u32, state: c_int) -> Option { - // 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 keysym: KeySym = 0; - let len = unsafe { - if let Some(xutf8) = self.lib.x_utf8_lookup_string { - xutf8( - &mut xke as *mut XKeyEvent, - buf.as_mut_ptr() as *mut c_char, - buf.len() as c_int, - &mut keysym as *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 as *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 - } + f() } } impl Drop for X11Capture { fn drop(&mut self) { - unsafe { - if self.grabbed { - self.ungrab_keyboard(); - } - (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); - } + let _ = self.child.kill(); + let _ = self.child.wait(); } } + +fn find_xrecord_binary() -> String { + if let Ok(output) = std::process::Command::new("which").arg("vietc-xrecord").output() { + if output.status.success() { + return String::from_utf8_lossy(&output.stdout).trim().to_string(); + } + } + + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + let path = dir.join("vietc-xrecord"); + if path.exists() { + return path.to_string_lossy().to_string(); + } + } + } + + for p in &["/usr/bin/vietc-xrecord", "/usr/local/bin/vietc-xrecord"] { + if std::path::Path::new(p).exists() { + return p.to_string(); + } + } + + "vietc-xrecord".to_string() +}