fix: aggressive drain to prevent feedback loop + clean up debug logging
- Add aggressive drain loop in wait_for_event() when SKIP_RECORD_EVENTS is true: poll 5ms + drain, repeat until quiet (up to 50ms). This closes the timing gap where injected events arrived after drain_pipe returned but before the flag was cleared in the next iteration. - Remove verbose debug eprintln!/log_info from daemon (process_key, replay, inject, toggle, window change, etc.) - Add vietc-xrecord.c (C helper with XRecordEnableContext blocking mode) - Update build-appimage.sh to compile and bundle C helper
This commit is contained in:
parent
44d1b0a1d2
commit
ea5df93bce
4 changed files with 482 additions and 585 deletions
|
|
@ -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<OutputCommand> {
|
||||
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<dyn std::error::Error>> {
|
|||
|
||||
#[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(|| {
|
||||
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(|| {
|
||||
SKIP_RECORD_EVENTS.store(true, Ordering::Relaxed);
|
||||
execute_commands(&*injector, &commands, true);
|
||||
});
|
||||
// If history is empty and commands only had a bare backspace,
|
||||
// we need to actually send it
|
||||
if daemon.keystroke_history.is_empty() && commands.is_empty() {
|
||||
capture.without_grab(|| {
|
||||
let _ = injector.send_backspace();
|
||||
});
|
||||
}
|
||||
}
|
||||
'\n' => {
|
||||
pressed_keys.remove(&event.keycode);
|
||||
daemon.replay_reset();
|
||||
capture.without_grab(|| {
|
||||
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(|| {
|
||||
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(|| {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
121
packaging/appimage/vietc-xrecord.c
Normal file
121
packaging/appimage/vietc-xrecord.c
Normal file
|
|
@ -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 <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/record.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
|
@ -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<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_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<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: 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<Self, Box<dyn std::error::Error>> {
|
||||
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<X11KeyEvent>,
|
||||
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<unsafe extern "C" fn(*mut XKeyEvent, *mut c_char, c_int, *mut KeySym, *mut c_int) -> c_int>,
|
||||
}
|
||||
|
||||
static mut EVENT_QUEUE: Option<Arc<Mutex<EventQueue>>> = None;
|
||||
unsafe impl Send for LookupLib {}
|
||||
|
||||
unsafe extern "C" fn record_callback(_closure: *mut c_void, data: *mut XRecordInterceptData) {
|
||||
if data.is_null() {
|
||||
return;
|
||||
impl Drop for LookupLib {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
(self.x_close_display)(self.display);
|
||||
dlclose(self.handle);
|
||||
}
|
||||
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
|
||||
impl LookupLib {
|
||||
fn new() -> Option<Self> {
|
||||
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<char> {
|
||||
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 {
|
||||
0
|
||||
(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(),
|
||||
)
|
||||
};
|
||||
|
||||
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 len > 0 {
|
||||
let s = std::str::from_utf8(&buf[..len as usize]).ok()?;
|
||||
s.chars().next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref q) = EVENT_QUEUE {
|
||||
if let Ok(mut queue) = q.lock() {
|
||||
queue.queue.push_back(event);
|
||||
}
|
||||
}
|
||||
/// 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<std::process::ChildStdout>,
|
||||
lookup: LookupLib,
|
||||
event_queue: VecDeque<X11KeyEvent>,
|
||||
pub focus_lost: bool,
|
||||
}
|
||||
|
||||
|
|
@ -308,181 +178,206 @@ unsafe impl Send for X11Capture {}
|
|||
|
||||
impl X11Capture {
|
||||
pub fn new() -> Option<Self> {
|
||||
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 flags = libc::fcntl(pipe_fd, libc::F_GETFL);
|
||||
libc::fcntl(pipe_fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
|
||||
}
|
||||
|
||||
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");
|
||||
let lookup = lookup?;
|
||||
|
||||
Some(Self {
|
||||
lib,
|
||||
display,
|
||||
root,
|
||||
grabbed: false,
|
||||
record_context: ctx,
|
||||
record_display: display,
|
||||
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 {
|
||||
(self.lib.x_flush)(self.display);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Normal wait for events
|
||||
self.drain_pipe();
|
||||
|
||||
if !self.event_queue.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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,
|
||||
// Poll the pipe fd
|
||||
let mut pfd = PollFd {
|
||||
fd: self.pipe_fd,
|
||||
events: POLLIN,
|
||||
revents: 0,
|
||||
};
|
||||
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
|
||||
unsafe {
|
||||
poll(&mut pfd, 1, timeout_ms as i32);
|
||||
}
|
||||
|
||||
if pfd.revents & POLLIN != 0 {
|
||||
self.drain_pipe();
|
||||
}
|
||||
|
||||
// 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<X11KeyEvent> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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<F, T>(&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<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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for X11Capture {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if self.grabbed {
|
||||
self.ungrab_keyboard();
|
||||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
(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);
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue