X11 capture: proper key tracking, direct clipboard, VNI default
- X11 keyboard capture via XGrabKeyboard (no input group needed) - Direct X11 clipboard for Unicode injection (no xclip/xdotool dependency) - Proper KeyPress/KeyRelease tracking (fix Ctrl+C, Alt+Tab, held keys) - Default input_method=vni, start_enabled=false - AppImage: bundled xclip, proper keyboard+VN SVG icon - Deb: Recommends libxtst6, xclip
This commit is contained in:
parent
38f3bca022
commit
bb0847a38f
9 changed files with 875 additions and 214 deletions
|
|
@ -79,13 +79,13 @@ impl Default for AppStateConfig {
|
|||
}
|
||||
|
||||
fn default_input_method() -> String {
|
||||
"telex".into()
|
||||
"vni".into()
|
||||
}
|
||||
fn default_toggle_key() -> String {
|
||||
"space".into()
|
||||
}
|
||||
fn default_start_enabled() -> bool {
|
||||
true
|
||||
false
|
||||
}
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
|
|
@ -278,9 +278,9 @@ vs = "với"
|
|||
fn parse_empty_config_uses_defaults() {
|
||||
let toml = "";
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.input_method, "telex");
|
||||
assert_eq!(config.input_method, "vni");
|
||||
assert_eq!(config.toggle_key, "space");
|
||||
assert!(config.start_enabled);
|
||||
assert!(!config.start_enabled);
|
||||
assert!(config.auto_restore.enabled);
|
||||
assert!(config.app_state.enabled);
|
||||
assert!(!config.app_state.english_apps.is_empty());
|
||||
|
|
@ -295,7 +295,7 @@ input_method = "vni"
|
|||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.input_method, "vni");
|
||||
assert_eq!(config.toggle_key, "space"); // default
|
||||
assert!(config.start_enabled); // default
|
||||
assert!(!config.start_enabled); // default
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ mod display;
|
|||
use app_state::AppStateManager;
|
||||
use config::Config;
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
use vietc_protocol::x11_capture::X11Capture;
|
||||
#[cfg(feature = "x11")]
|
||||
use vietc_protocol::x11_inject::X11Injector;
|
||||
|
||||
fn get_log_path() -> Option<PathBuf> {
|
||||
dirs::config_dir().map(|p| p.join("vietc").join("vietc.log"))
|
||||
}
|
||||
|
|
@ -403,6 +408,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
if display != display::DisplayServer::Wayland {
|
||||
if let Some(mut capture) = X11Capture::new() {
|
||||
if capture.grab_keyboard() {
|
||||
log_info("[vietc] X11 keyboard grabbed — using X11 capture/injection");
|
||||
return run_with_x11(
|
||||
capture,
|
||||
&mut daemon,
|
||||
shared_active_window,
|
||||
config_changed,
|
||||
status_changed,
|
||||
engine_enabled,
|
||||
);
|
||||
} else {
|
||||
log_info("[vietc] X11 grab failed, falling back to evdev");
|
||||
}
|
||||
} else {
|
||||
log_info("[vietc] X11 not available, falling back to evdev");
|
||||
}
|
||||
}
|
||||
|
||||
match open_keyboard_device() {
|
||||
Ok((device, path)) => {
|
||||
log_info(&format!("[vietc] Keyboard device: {}", path));
|
||||
|
|
@ -502,6 +528,123 @@ fn open_keyboard_device() -> Result<(evdev::Device, String), Box<dyn std::error:
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
fn run_with_x11(
|
||||
mut capture: X11Capture,
|
||||
daemon: &mut Daemon,
|
||||
shared_active_window: Arc<Mutex<String>>,
|
||||
config_changed: Arc<AtomicBool>,
|
||||
status_changed: Arc<AtomicBool>,
|
||||
_engine_enabled: Arc<AtomicBool>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let injector: Box<dyn vietc_protocol::KeyInjector> = Box::new(X11Injector::new()?);
|
||||
let mut last_active_window = String::new();
|
||||
// Track physically-held keys so we only inject press on KeyPress
|
||||
// and release on KeyRelease — without this, every KeyPress injects
|
||||
// press+release immediately, breaking held-key combos (Ctrl+C, Alt+Tab…).
|
||||
let mut pressed_keys: HashSet<u32> = HashSet::new();
|
||||
|
||||
loop {
|
||||
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 {
|
||||
log_info(&format!("[vietc] Window changed: '{}' -> '{}'", last_active_window, active_window));
|
||||
last_active_window = active_window.clone();
|
||||
daemon.engine.reset();
|
||||
}
|
||||
}
|
||||
|
||||
if daemon.config.app_state.enabled {
|
||||
let active_window = shared_active_window.lock().unwrap().clone();
|
||||
daemon.check_app_change_with(active_window);
|
||||
}
|
||||
|
||||
while let Some(event) = capture.next_event() {
|
||||
if event.pressed {
|
||||
// Skip autorepeat — key is already tracked as held
|
||||
if !pressed_keys.insert(event.keycode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Toggle key: Ctrl+Space
|
||||
if let Some(' ') = event.ch {
|
||||
if (event.state & 4) != 0 {
|
||||
pressed_keys.remove(&event.keycode);
|
||||
daemon.toggle();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Modifier or non-character key → forward press only
|
||||
if capture.is_modifier_pressed(event.state) || event.ch.is_none() {
|
||||
daemon.engine.reset();
|
||||
capture.without_grab(|| {
|
||||
let _ = injector.send_key_event(event.keycode as u16, 1);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Character key
|
||||
if let Some(ch) = event.ch {
|
||||
match ch {
|
||||
'\x08' => {
|
||||
daemon.engine.process_key('\x08');
|
||||
capture.without_grab(|| {
|
||||
let _ = injector.send_backspace();
|
||||
});
|
||||
// Keep in pressed_keys so release is forwarded
|
||||
}
|
||||
'\n' => {
|
||||
pressed_keys.remove(&event.keycode);
|
||||
daemon.engine.reset();
|
||||
capture.without_grab(|| {
|
||||
let _ = injector.send_key_event(event.keycode as u16, 1);
|
||||
let _ = injector.send_key_event(event.keycode as u16, 0);
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
let commands = daemon.process_key(ch);
|
||||
if !commands.is_empty() {
|
||||
// Engine consumed the key; remove from tracking
|
||||
pressed_keys.remove(&event.keycode);
|
||||
capture.without_grab(|| {
|
||||
execute_commands(&*injector, &commands, true);
|
||||
});
|
||||
} else {
|
||||
// Engine started composing; forward press+release immediately
|
||||
pressed_keys.remove(&event.keycode);
|
||||
capture.without_grab(|| {
|
||||
let _ = injector.send_key_event(event.keycode as u16, 1);
|
||||
let _ = injector.send_key_event(event.keycode as u16, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
fn run_with_evdev(
|
||||
mut device: evdev::Device,
|
||||
daemon: &mut Daemon,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ fn get_display(events: &[EngineEvent]) -> String {
|
|||
for _ in 0..*backspaces { display.pop(); }
|
||||
display.push_str(restored);
|
||||
}
|
||||
EngineEvent::Paste(text) => { display.push_str(text); }
|
||||
}
|
||||
}
|
||||
display
|
||||
|
|
|
|||
|
|
@ -43,6 +43,15 @@ else
|
|||
[ -f ui/target/release/vietc-tray ] && cp ui/target/release/vietc-tray "$APPDIR/usr/bin/"
|
||||
fi
|
||||
|
||||
# Bundle xclip as fallback for clipboard operations
|
||||
echo " Bundling xclip..."
|
||||
if command -v xclip &>/dev/null; then
|
||||
cp "$(which xclip)" "$APPDIR/usr/bin/"
|
||||
echo " xclip bundled"
|
||||
else
|
||||
echo " xclip not found on system, skipping"
|
||||
fi
|
||||
|
||||
# Desktop integration
|
||||
echo "[3/5] Installing desktop integration..."
|
||||
if [ -f "deb-build/vietc.desktop" ]; then
|
||||
|
|
@ -109,6 +118,67 @@ else
|
|||
cp "$APPDIR/usr/share/applications/vietc.desktop" "$APPDIR/"
|
||||
fi
|
||||
|
||||
# Icon — required by appimagetool (desktop file has Icon=vietc)
|
||||
# Use SVG from deb build if available, otherwise generate a keyboard icon
|
||||
if [ -f "deb-build/usr/share/icons/hicolor/256x256/apps/vietc.svg" ]; then
|
||||
cp "deb-build/usr/share/icons/hicolor/256x256/apps/vietc.svg" "$APPDIR/vietc.svg"
|
||||
elif [ -f "deb-build/usr/share/icons/hicolor/256x256/apps/vietc.png" ]; then
|
||||
cp "deb-build/usr/share/icons/hicolor/256x256/apps/vietc.png" "$APPDIR/vietc.png"
|
||||
else
|
||||
# Generate a proper keyboard+VN icon as SVG
|
||||
cat > "$APPDIR/vietc.svg" << 'SVGEOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||
<rect x="20" y="60" width="216" height="140" rx="16" fill="#2d2d2d" stroke="#1a1a1a" stroke-width="4"/>
|
||||
<rect x="36" y="76" width="184" height="108" rx="8" fill="#3d3d3d"/>
|
||||
<rect x="48" y="88" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="78" y="88" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="108" y="88" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="138" y="88" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="168" y="88" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="198" y="88" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="54" y="114" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="84" y="114" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="114" y="114" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="144" y="114" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="174" y="114" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="60" y="140" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="90" y="140" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="120" y="140" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="150" y="140" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="180" y="140" width="42" height="20" rx="3" fill="#f0f0f0"/>
|
||||
<rect x="72" y="166" width="112" height="16" rx="3" fill="#f0f0f0"/>
|
||||
<circle cx="216" cy="48" r="28" fill="#da251d"/>
|
||||
<text x="216" y="56" text-anchor="middle" fill="white" font-size="18" font-weight="bold" font-family="sans-serif">VN</text>
|
||||
</svg>
|
||||
SVGEOF
|
||||
fi
|
||||
|
||||
# Convert SVG to PNG for appimagetool (it prefers PNG for the root icon)
|
||||
if [ -f "$APPDIR/vietc.svg" ] && ! [ -f "$APPDIR/vietc.png" ]; then
|
||||
if command -v rsvg-convert &>/dev/null; then
|
||||
rsvg-convert -w 256 -h 256 "$APPDIR/vietc.svg" -o "$APPDIR/vietc.png"
|
||||
elif command -v inkscape &>/dev/null; then
|
||||
inkscape -w 256 -h 256 "$APPDIR/vietc.svg" --export-filename="$APPDIR/vietc.png" 2>/dev/null
|
||||
elif command -v convert &>/dev/null; then
|
||||
convert -background none "$APPDIR/vietc.svg" -resize 256x256 "$APPDIR/vietc.png" 2>/dev/null
|
||||
elif command -v python3 &>/dev/null; then
|
||||
python3 -c "
|
||||
import subprocess, sys
|
||||
try:
|
||||
subprocess.check_call(['rsvg-convert', '-w', '256', '-h', '256', '$APPDIR/vietc.svg', '-o', '$APPDIR/vietc.png'])
|
||||
except Exception:
|
||||
pass
|
||||
" 2>/dev/null
|
||||
fi
|
||||
# If no converter, appimagetool can use SVG directly
|
||||
fi
|
||||
|
||||
# Also put icon in hicolor for system installs via AppImage
|
||||
mkdir -p "$APPDIR/usr/share/icons/hicolor/256x256/apps"
|
||||
[ -f "$APPDIR/vietc.svg" ] && cp "$APPDIR/vietc.svg" "$APPDIR/usr/share/icons/hicolor/256x256/apps/"
|
||||
[ -f "$APPDIR/vietc.png" ] && cp "$APPDIR/vietc.png" "$APPDIR/usr/share/icons/hicolor/256x256/apps/"
|
||||
|
||||
# Create custom AppRun script
|
||||
cat > "$APPDIR/AppRun" << 'EOF'
|
||||
#!/bin/sh
|
||||
|
|
@ -128,9 +198,20 @@ ENV_PREFIX="env"
|
|||
[ -n "$XDG_RUNTIME_DIR" ] && ENV_PREFIX="$ENV_PREFIX XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"
|
||||
|
||||
# Start daemon (kill old non-root one first if we have root)
|
||||
# On X11 we can run without root (XGrabKeyboard + XTest injection needs no special permissions).
|
||||
# On Wayland, evdev requires root (input group) or uinput.
|
||||
NEED_ROOT=""
|
||||
if [ -n "$WAYLAND_DISPLAY" ]; then
|
||||
NEED_ROOT="yes"
|
||||
fi
|
||||
|
||||
if [ -z "$NEED_ROOT" ]; then
|
||||
# X11: no root needed
|
||||
pkill -x vietc 2>/dev/null; sleep 0.3
|
||||
"$HERE/usr/bin/vietc" >/dev/null &
|
||||
DAEMON_PID=$!
|
||||
else
|
||||
# Fix Wayland env for root: sudo resets XDG_RUNTIME_DIR, breaking wtype/wl-copy.
|
||||
# Only set WAYLAND_DISPLAY if the user actually has a Wayland session.
|
||||
if [ "$(id -u)" = "0" ] && [ -z "$XDG_RUNTIME_DIR" ] && [ -n "$SUDO_USER" ]; then
|
||||
USER_UID=$(id -u "$SUDO_USER" 2>/dev/null || echo 1000)
|
||||
export XDG_RUNTIME_DIR="/run/user/$USER_UID"
|
||||
|
|
@ -139,7 +220,7 @@ if [ "$(id -u)" = "0" ] && [ -z "$XDG_RUNTIME_DIR" ] && [ -n "$SUDO_USER" ]; the
|
|||
fi
|
||||
fi
|
||||
|
||||
if command -v pkexec >/dev/null && [ -z "$WAYLAND_DISPLAY" ]; then
|
||||
if command -v pkexec >/dev/null; then
|
||||
pkill -x vietc 2>/dev/null; sleep 0.5
|
||||
pkexec $ENV_PREFIX "$HERE/usr/bin/vietc" >/dev/null &
|
||||
DAEMON_PID=$!
|
||||
|
|
@ -162,6 +243,7 @@ elif command -v sudo >/dev/null; then
|
|||
sudo $ENV_PREFIX "$HERE/usr/bin/vietc" >/dev/null &
|
||||
DAEMON_PID=$!
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$DAEMON_PID" ] && ! pgrep -x vietc >/dev/null; then
|
||||
"$HERE/usr/bin/vietc" >/dev/null &
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ echo "=== Building Viet+ .deb package v${VERSION} ==="
|
|||
# Build binaries (all features: x11 + wayland)
|
||||
echo "[1/5] Building binaries..."
|
||||
cargo build --release --features "x11,wayland" --manifest-path "$PROJECT_ROOT/Cargo.toml"
|
||||
(cd "$PROJECT_ROOT/ui" && cargo build --release)
|
||||
(cd "$PROJECT_ROOT/ui" && cargo build --release) || echo " Warning: UI tray not built (libdbus-1-dev may be missing)"
|
||||
echo " Done."
|
||||
|
||||
# Clean and create staging
|
||||
|
|
@ -113,7 +113,7 @@ Section: utils
|
|||
Priority: optional
|
||||
Architecture: amd64
|
||||
Depends: libc6 (>= 2.31), libevdev2 (>= 1.9.0)
|
||||
Recommends: libwayland-client0 (>= 1.20), libx11-6
|
||||
Recommends: libwayland-client0 (>= 1.20), libx11-6, libxtst6, xclip
|
||||
Maintainer: Khoa Vo <vndangkhoa@gmail.com>
|
||||
Description: Viet+ — Vietnamese Input Method for Linux
|
||||
Zero-configuration Vietnamese input method engine supporting
|
||||
|
|
|
|||
|
|
@ -6,5 +6,8 @@ pub mod wayland_im;
|
|||
#[cfg(feature = "x11")]
|
||||
pub mod x11_inject;
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
pub mod x11_capture;
|
||||
|
||||
pub use inject::KeyInjector;
|
||||
pub use monitor::KeyMonitor;
|
||||
|
|
|
|||
305
protocol/src/x11_capture.rs
Normal file
305
protocol/src/x11_capture.rs
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
type Display = c_void;
|
||||
type Window = u64;
|
||||
type XID = u64;
|
||||
type Time = u64;
|
||||
|
||||
// X11 event types
|
||||
const KEY_PRESS: c_int = 2;
|
||||
const KEY_RELEASE: c_int = 3;
|
||||
|
||||
// X11 modifier masks
|
||||
const SHIFT_MASK: c_int = 1;
|
||||
const CONTROL_MASK: c_int = 4;
|
||||
const MOD1_MASK: c_int = 8; // Alt
|
||||
const MOD4_MASK: c_int = 64; // Super/Win
|
||||
|
||||
// Grab modes
|
||||
const GRAB_MODE_ASYNC: 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_next_event: unsafe extern "C" fn(*mut Display, *mut XEvent),
|
||||
x_lookup_string: unsafe extern "C" fn(*mut XKeyEvent, *mut c_char, c_int, *mut KeySym, *mut c_int) -> c_int,
|
||||
x_keysym_to_keycode: unsafe extern "C" fn(*mut Display, KeySym) -> u32,
|
||||
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,
|
||||
}
|
||||
|
||||
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))
|
||||
};
|
||||
}
|
||||
|
||||
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_next_event = sym!("XNextEvent");
|
||||
let x_lookup_string = sym!("XLookupString");
|
||||
let x_keysym_to_keycode = sym!("XKeysymToKeycode");
|
||||
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");
|
||||
|
||||
Ok(Self {
|
||||
handle,
|
||||
x_open_display,
|
||||
x_close_display,
|
||||
x_default_root_window,
|
||||
x_grab_keyboard,
|
||||
x_ungrab_keyboard,
|
||||
x_next_event,
|
||||
x_lookup_string,
|
||||
x_keysym_to_keycode,
|
||||
x_utf8_lookup_string,
|
||||
x_flush,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
_x: c_int,
|
||||
_y: c_int,
|
||||
_x_root: c_int,
|
||||
_y_root: c_int,
|
||||
state: c_int,
|
||||
keycode: u32,
|
||||
_same_screen: c_int,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
union XEventData {
|
||||
key: XKeyEvent,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct XEvent {
|
||||
_type: c_int,
|
||||
_pad: [u8; 24],
|
||||
data: XEventData,
|
||||
}
|
||||
|
||||
type KeySym = u64;
|
||||
|
||||
pub struct X11KeyEvent {
|
||||
pub keycode: u32,
|
||||
pub ch: Option<char>,
|
||||
pub pressed: bool,
|
||||
pub state: c_int,
|
||||
}
|
||||
|
||||
pub struct X11Capture {
|
||||
lib: X11Lib,
|
||||
display: *mut Display,
|
||||
root: Window,
|
||||
grabbed: bool,
|
||||
event_buf: Vec<u8>,
|
||||
}
|
||||
|
||||
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 X11: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let display = (lib.x_open_display)(std::ptr::null());
|
||||
if display.is_null() {
|
||||
eprintln!("[vietc] X11Capture: cannot open display. Is DISPLAY set?");
|
||||
return None;
|
||||
}
|
||||
|
||||
let root = (lib.x_default_root_window)(display);
|
||||
eprintln!("[vietc] X11Capture: initialized successfully");
|
||||
Some(Self {
|
||||
lib,
|
||||
display,
|
||||
root,
|
||||
grabbed: false,
|
||||
event_buf: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grab_keyboard(&mut self) -> bool {
|
||||
unsafe {
|
||||
let status = (self.lib.x_grab_keyboard)(
|
||||
self.display,
|
||||
self.root,
|
||||
0, // owner_events = False
|
||||
GRAB_MODE_ASYNC,
|
||||
GRAB_MODE_ASYNC,
|
||||
0, // CurrentTime
|
||||
) as i32;
|
||||
if status == 0 {
|
||||
self.grabbed = true;
|
||||
eprintln!("[vietc] X11Capture: grabbed keyboard successfully");
|
||||
true
|
||||
} else {
|
||||
eprintln!("[vietc] X11Capture: grab failed with status {}", status);
|
||||
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 next_event(&mut self) -> Option<X11KeyEvent> {
|
||||
if !self.grabbed {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut event: XEvent = unsafe { std::mem::zeroed() };
|
||||
unsafe {
|
||||
(self.lib.x_next_event)(self.display, &mut event);
|
||||
}
|
||||
|
||||
let _type = event._type;
|
||||
if _type != KEY_PRESS && _type != KEY_RELEASE {
|
||||
return self.next_event();
|
||||
}
|
||||
|
||||
let key_event = unsafe { &event.data.key };
|
||||
let ch = self.lookup_key(key_event);
|
||||
Some(X11KeyEvent {
|
||||
keycode: key_event.keycode,
|
||||
ch,
|
||||
pressed: _type == KEY_PRESS,
|
||||
state: key_event.state,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_modifier_pressed(&self, state: c_int) -> bool {
|
||||
(state & (CONTROL_MASK | MOD1_MASK | MOD4_MASK)) != 0
|
||||
}
|
||||
|
||||
pub fn with_grab<F, T>(&mut self, f: F) -> T
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
// Grab should already be held; just execute
|
||||
f()
|
||||
}
|
||||
|
||||
pub fn without_grab<F, T>(&mut self, f: F) -> T
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
self.ungrab_keyboard();
|
||||
let result = f();
|
||||
self.grab_keyboard();
|
||||
result
|
||||
}
|
||||
|
||||
fn lookup_key(&self, event: &XKeyEvent) -> Option<char> {
|
||||
let mut buf = [0u8; 32];
|
||||
let mut keysym: KeySym = 0;
|
||||
let len = unsafe {
|
||||
if let Some(xutf8) = self.lib.x_utf8_lookup_string {
|
||||
xutf8(
|
||||
event as *const XKeyEvent 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)(
|
||||
event as *const XKeyEvent 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) {
|
||||
if self.grabbed {
|
||||
self.ungrab_keyboard();
|
||||
}
|
||||
unsafe {
|
||||
(self.lib.x_close_display)(self.display);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,46 @@
|
|||
use super::inject::{InjectResult, KeyInjector};
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
type Display = c_void;
|
||||
type Window = u64;
|
||||
type Atom = u64;
|
||||
type Time = u64;
|
||||
|
||||
// Dynamic linker FFI
|
||||
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;
|
||||
}
|
||||
|
||||
const CURRENT_TIME: Time = 0;
|
||||
const PROP_MODE_REPLACE: c_int = 0;
|
||||
const NO_EVENT_MASK: i64 = 0;
|
||||
const INPUT_OUTPUT: c_int = 1;
|
||||
const COPY_FROM_PARENT: Window = 0;
|
||||
|
||||
const SELECTION_REQUEST: c_int = 30;
|
||||
const SELECTION_NOTIFY: c_int = 31;
|
||||
|
||||
struct X11Lib {
|
||||
x11_handle: *mut c_void,
|
||||
xtst_handle: *mut c_void,
|
||||
|
||||
// Symbols
|
||||
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_flush: unsafe extern "C" fn(*mut Display) -> c_int,
|
||||
x_test_fake_key_event: unsafe extern "C" fn(*mut Display, u32, c_int, u64) -> c_int,
|
||||
x_intern_atom: unsafe extern "C" fn(*mut Display, *const c_char, c_int) -> Atom,
|
||||
x_set_selection_owner: unsafe extern "C" fn(*mut Display, Atom, Window, Time) -> c_int,
|
||||
x_change_property: unsafe extern "C" fn(*mut Display, Window, Atom, Atom, c_int, c_int, *const c_void, c_int) -> c_int,
|
||||
x_get_selection_owner: unsafe extern "C" fn(*mut Display, Atom) -> Window,
|
||||
x_send_event: unsafe extern "C" fn(*mut Display, Window, c_int, i64, *const c_void) -> c_int,
|
||||
x_create_simple_window: unsafe extern "C" fn(*mut Display, Window, c_int, c_int, c_int, c_int, c_int, Atom, Atom) -> Window,
|
||||
x_map_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_next_event: unsafe extern "C" fn(*mut Display, *mut XEvent),
|
||||
}
|
||||
|
||||
impl X11Lib {
|
||||
|
|
@ -32,7 +52,7 @@ impl X11Lib {
|
|||
];
|
||||
let mut x11_handle = std::ptr::null_mut();
|
||||
for path in x11_paths {
|
||||
x11_handle = dlopen(path, 1); // RTLD_LAZY
|
||||
x11_handle = dlopen(path, 1);
|
||||
if !x11_handle.is_null() {
|
||||
break;
|
||||
}
|
||||
|
|
@ -57,24 +77,27 @@ impl X11Lib {
|
|||
return Err("Failed to load libXtst.so.6".into());
|
||||
}
|
||||
|
||||
let x_open_display = std::mem::transmute(dlsym(
|
||||
x11_handle,
|
||||
b"XOpenDisplay\0".as_ptr() as *const c_char,
|
||||
));
|
||||
let x_close_display = std::mem::transmute(dlsym(
|
||||
x11_handle,
|
||||
b"XCloseDisplay\0".as_ptr() as *const c_char,
|
||||
));
|
||||
let x_default_root_window = std::mem::transmute(dlsym(
|
||||
x11_handle,
|
||||
b"XDefaultRootWindow\0".as_ptr() as *const c_char,
|
||||
));
|
||||
let x_flush =
|
||||
std::mem::transmute(dlsym(x11_handle, b"XFlush\0".as_ptr() as *const c_char));
|
||||
let x_test_fake_key_event = std::mem::transmute(dlsym(
|
||||
xtst_handle,
|
||||
b"XTestFakeKeyEvent\0".as_ptr() as *const c_char,
|
||||
));
|
||||
macro_rules! sym {
|
||||
($handle:expr, $name:expr) => {
|
||||
std::mem::transmute(dlsym($handle, concat!($name, "\0").as_ptr() as *const c_char))
|
||||
};
|
||||
}
|
||||
|
||||
let x_open_display = sym!(x11_handle, "XOpenDisplay");
|
||||
let x_close_display = sym!(x11_handle, "XCloseDisplay");
|
||||
let x_default_root_window = sym!(x11_handle, "XDefaultRootWindow");
|
||||
let x_flush = sym!(x11_handle, "XFlush");
|
||||
let x_intern_atom = sym!(x11_handle, "XInternAtom");
|
||||
let x_set_selection_owner = sym!(x11_handle, "XSetSelectionOwner");
|
||||
let x_change_property = sym!(x11_handle, "XChangeProperty");
|
||||
let x_get_selection_owner = sym!(x11_handle, "XGetSelectionOwner");
|
||||
let x_send_event = sym!(x11_handle, "XSendEvent");
|
||||
let x_create_simple_window = sym!(x11_handle, "XCreateSimpleWindow");
|
||||
let x_map_window = sym!(x11_handle, "XMapWindow");
|
||||
let x_destroy_window = sym!(x11_handle, "XDestroyWindow");
|
||||
let x_pending = sym!(x11_handle, "XPending");
|
||||
let x_next_event = sym!(x11_handle, "XNextEvent");
|
||||
let x_test_fake_key_event = sym!(xtst_handle, "XTestFakeKeyEvent");
|
||||
|
||||
Ok(Self {
|
||||
x11_handle,
|
||||
|
|
@ -84,6 +107,16 @@ impl X11Lib {
|
|||
x_default_root_window,
|
||||
x_flush,
|
||||
x_test_fake_key_event,
|
||||
x_intern_atom,
|
||||
x_set_selection_owner,
|
||||
x_change_property,
|
||||
x_get_selection_owner,
|
||||
x_send_event,
|
||||
x_create_simple_window,
|
||||
x_map_window,
|
||||
x_destroy_window,
|
||||
x_pending,
|
||||
x_next_event,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -98,10 +131,8 @@ impl Drop for X11Lib {
|
|||
}
|
||||
}
|
||||
|
||||
// Linux-to-X11 keycode offset (X11 keycodes = Linux keycodes + 8)
|
||||
const X11_KEYCODE_OFFSET: u32 = 8;
|
||||
|
||||
// X11 keycodes for common ASCII characters
|
||||
fn char_to_keycode(ch: char) -> Option<(u32, bool)> {
|
||||
match ch {
|
||||
'a' => Some((30, false)),
|
||||
|
|
@ -156,53 +187,41 @@ fn char_to_keycode(ch: char) -> Option<(u32, bool)> {
|
|||
'X' => Some((45, true)),
|
||||
'Y' => Some((21, true)),
|
||||
'Z' => Some((44, true)),
|
||||
'0' => Some((11, false)),
|
||||
'1' => Some((2, false)),
|
||||
'2' => Some((3, false)),
|
||||
'3' => Some((4, false)),
|
||||
'4' => Some((5, false)),
|
||||
'5' => Some((6, false)),
|
||||
'6' => Some((7, false)),
|
||||
'7' => Some((8, false)),
|
||||
'8' => Some((9, false)),
|
||||
'9' => Some((10, false)),
|
||||
' ' => Some((57, false)),
|
||||
'.' => Some((52, false)),
|
||||
',' => Some((51, false)),
|
||||
'-' => Some((12, false)),
|
||||
'=' => Some((13, false)),
|
||||
';' => Some((39, false)),
|
||||
'\'' => Some((40, false)),
|
||||
'/' => Some((53, false)),
|
||||
'\\' => Some((43, false)),
|
||||
'`' => Some((41, false)),
|
||||
'0' => Some((11, false)),
|
||||
'1' => Some((2, false)),
|
||||
'2' => Some((3, false)),
|
||||
'3' => Some((4, false)),
|
||||
'4' => Some((5, false)),
|
||||
'5' => Some((6, false)),
|
||||
'6' => Some((7, false)),
|
||||
'7' => Some((8, false)),
|
||||
'8' => Some((9, false)),
|
||||
'9' => Some((10, false)),
|
||||
' ' => Some((57, false)),
|
||||
'.' => Some((52, false)),
|
||||
',' => Some((51, false)),
|
||||
'-' => Some((12, false)),
|
||||
'=' => Some((13, false)),
|
||||
';' => Some((39, false)),
|
||||
'\'' => Some((40, false)),
|
||||
'/' => Some((53, false)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct XSelectionRequestEvent {
|
||||
_type: c_int,
|
||||
_serial: u64,
|
||||
_send_event: c_int,
|
||||
_display: *mut Display,
|
||||
owner: Window,
|
||||
requestor: Window,
|
||||
selection: Atom,
|
||||
target: Atom,
|
||||
property: Atom,
|
||||
time: Time,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct XEvent {
|
||||
_type: c_int,
|
||||
_pad: [u8; 24],
|
||||
data: [u64; 6],
|
||||
}
|
||||
|
||||
pub struct X11Injector {
|
||||
lib: X11Lib,
|
||||
display: *mut Display,
|
||||
#[allow(dead_code)]
|
||||
window: Window,
|
||||
root: Window,
|
||||
clipboard_window: Window,
|
||||
atom_clipboard: Atom,
|
||||
atom_utf8: Atom,
|
||||
atom_targets: Atom,
|
||||
atom_string: Atom,
|
||||
clipboard_text: RefCell<String>,
|
||||
}
|
||||
|
||||
unsafe impl Send for X11Injector {}
|
||||
|
|
@ -216,33 +235,208 @@ impl X11Injector {
|
|||
if display.is_null() {
|
||||
return Err("Cannot open X11 display. Is DISPLAY set?".into());
|
||||
}
|
||||
let window = (lib.x_default_root_window)(display);
|
||||
let root = (lib.x_default_root_window)(display);
|
||||
|
||||
let atom_clipboard = (lib.x_intern_atom)(display, b"CLIPBOARD\0".as_ptr() as *const c_char, 0);
|
||||
let atom_utf8 = (lib.x_intern_atom)(display, b"UTF8_STRING\0".as_ptr() as *const c_char, 0);
|
||||
let atom_targets = (lib.x_intern_atom)(display, b"TARGETS\0".as_ptr() as *const c_char, 0);
|
||||
let atom_string = (lib.x_intern_atom)(display, b"STRING\0".as_ptr() as *const c_char, 0);
|
||||
|
||||
// Create a small hidden window for clipboard ownership
|
||||
let clipboard_window = (lib.x_create_simple_window)(
|
||||
display, root, 0, 0, 1, 1, 0, COPY_FROM_PARENT, COPY_FROM_PARENT,
|
||||
);
|
||||
(lib.x_map_window)(display, clipboard_window);
|
||||
|
||||
Ok(Self {
|
||||
lib,
|
||||
display,
|
||||
window,
|
||||
root,
|
||||
clipboard_window,
|
||||
atom_clipboard,
|
||||
atom_utf8,
|
||||
atom_targets,
|
||||
atom_string,
|
||||
clipboard_text: RefCell::new(String::new()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn set_clipboard_text(&self, text: &str) {
|
||||
*self.clipboard_text.borrow_mut() = text.to_string();
|
||||
unsafe {
|
||||
// Set the text as a property on our clipboard window
|
||||
(self.lib.x_change_property)(
|
||||
self.display,
|
||||
self.clipboard_window,
|
||||
self.atom_clipboard,
|
||||
self.atom_utf8,
|
||||
8, // 8-bit format
|
||||
PROP_MODE_REPLACE,
|
||||
text.as_ptr() as *const c_void,
|
||||
text.len() as c_int,
|
||||
);
|
||||
|
||||
// Also set as STRING (for apps that don't understand UTF8_STRING)
|
||||
(self.lib.x_change_property)(
|
||||
self.display,
|
||||
self.clipboard_window,
|
||||
self.atom_clipboard,
|
||||
self.atom_string,
|
||||
8,
|
||||
PROP_MODE_REPLACE,
|
||||
text.as_ptr() as *const c_void,
|
||||
text.len() as c_int,
|
||||
);
|
||||
|
||||
// Claim the CLIPBOARD selection
|
||||
(self.lib.x_set_selection_owner)(
|
||||
self.display,
|
||||
self.atom_clipboard,
|
||||
self.clipboard_window,
|
||||
CURRENT_TIME,
|
||||
);
|
||||
|
||||
(self.lib.x_flush)(self.display);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_pending_events(&self) {
|
||||
unsafe {
|
||||
while (self.lib.x_pending)(self.display) > 0 {
|
||||
let mut event: XEvent = std::mem::zeroed();
|
||||
(self.lib.x_next_event)(self.display, &mut event);
|
||||
if event._type == SELECTION_REQUEST {
|
||||
let req = &*(&event as *const XEvent as *const XSelectionRequestEvent);
|
||||
self.handle_selection_request(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_selection_request(&self, req: &XSelectionRequestEvent) {
|
||||
eprintln!(
|
||||
"[vietc] SelectionRequest: target={} requestor={}",
|
||||
req.target, req.requestor
|
||||
);
|
||||
|
||||
// Determine what property to use for the response
|
||||
let property = if req.property == 0 {
|
||||
req.target // Use the target atom as property if property is None
|
||||
} else {
|
||||
req.property
|
||||
};
|
||||
|
||||
unsafe {
|
||||
if req.target == self.atom_targets {
|
||||
// Respond with supported targets: TARGETS, UTF8_STRING, STRING
|
||||
let targets: [Atom; 3] = [self.atom_targets, self.atom_utf8, self.atom_string];
|
||||
(self.lib.x_change_property)(
|
||||
self.display,
|
||||
req.requestor,
|
||||
property,
|
||||
self.atom_targets,
|
||||
32, // 32-bit format
|
||||
PROP_MODE_REPLACE,
|
||||
targets.as_ptr() as *const c_void,
|
||||
targets.len() as c_int,
|
||||
);
|
||||
} else if req.target == self.atom_utf8 || req.target == self.atom_string {
|
||||
// Respond with the actual clipboard text
|
||||
(self.lib.x_change_property)(
|
||||
self.display,
|
||||
req.requestor,
|
||||
property,
|
||||
req.target,
|
||||
8, // 8-bit format
|
||||
PROP_MODE_REPLACE,
|
||||
self.clipboard_text.borrow().as_ptr() as *const c_void,
|
||||
self.clipboard_text.borrow().len() as c_int,
|
||||
);
|
||||
}
|
||||
|
||||
// Send SelectionNotify to inform the requestor
|
||||
let mut notify = std::mem::zeroed::<XSelectionNotifyEvent>();
|
||||
notify._type = SELECTION_NOTIFY as c_int;
|
||||
notify._display = self.display;
|
||||
notify.requestor = req.requestor;
|
||||
notify.selection = req.selection;
|
||||
notify.target = req.target;
|
||||
notify.property = if req.target == self.atom_targets
|
||||
|| req.target == self.atom_utf8
|
||||
|| req.target == self.atom_string
|
||||
{
|
||||
property
|
||||
} else {
|
||||
0 // PropertyNone = unsupported target
|
||||
};
|
||||
notify.time = req.time;
|
||||
|
||||
(self.lib.x_send_event)(
|
||||
self.display,
|
||||
req.requestor,
|
||||
0, // propagate = False
|
||||
NO_EVENT_MASK,
|
||||
¬ify as *const XSelectionNotifyEvent as *const c_void,
|
||||
);
|
||||
(self.lib.x_flush)(self.display);
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_via_clipboard(&self, backspaces: usize, text: &str) -> bool {
|
||||
// Set clipboard text directly via X11
|
||||
self.set_clipboard_text(text);
|
||||
|
||||
// Handle any pending SelectionRequest events that may have queued
|
||||
// (unlikely at this point, but be safe)
|
||||
self.handle_pending_events();
|
||||
|
||||
// Send backspaces via XTest
|
||||
if backspaces > 0 {
|
||||
for _ in 0..backspaces {
|
||||
self.send_keycode(14, false); // KEY_BACKSPACE
|
||||
}
|
||||
}
|
||||
|
||||
// Send Ctrl+V via XTest to paste
|
||||
unsafe {
|
||||
// X11 keycodes: 37 = Ctrl_L, 55 = V
|
||||
(self.lib.x_test_fake_key_event)(self.display, 37, 1, 0);
|
||||
(self.lib.x_test_fake_key_event)(self.display, 55, 1, 0);
|
||||
(self.lib.x_test_fake_key_event)(self.display, 55, 0, 0);
|
||||
(self.lib.x_test_fake_key_event)(self.display, 37, 0, 0);
|
||||
(self.lib.x_flush)(self.display);
|
||||
}
|
||||
|
||||
// Handle SelectionRequest events that come from the paste target
|
||||
// Process events with a short spin loop (up to ~50ms)
|
||||
for _ in 0..10 {
|
||||
// Brief sleep to let X11 events propagate
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
self.handle_pending_events();
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn send_keycode(&self, keycode: u32, shift: bool) {
|
||||
unsafe {
|
||||
if shift {
|
||||
(self.lib.x_test_fake_key_event)(self.display, 50, 1, 0); // Shift press
|
||||
(self.lib.x_test_fake_key_event)(self.display, 50, 1, 0);
|
||||
}
|
||||
(self.lib.x_test_fake_key_event)(self.display, keycode, 1, 0); // Key press
|
||||
(self.lib.x_test_fake_key_event)(self.display, keycode, 0, 0); // Key release
|
||||
(self.lib.x_test_fake_key_event)(self.display, keycode, 1, 0);
|
||||
(self.lib.x_test_fake_key_event)(self.display, keycode, 0, 0);
|
||||
if shift {
|
||||
(self.lib.x_test_fake_key_event)(self.display, 50, 0, 0); // Shift release
|
||||
(self.lib.x_test_fake_key_event)(self.display, 50, 0, 0);
|
||||
}
|
||||
(self.lib.x_flush)(self.display);
|
||||
}
|
||||
}
|
||||
|
||||
fn send_unicode_via_xdotool(&self, ch: char) {
|
||||
// For Unicode chars, try ydotool first (uinput-based, works as root),
|
||||
// then xdotool (X11 XTest) as fallback.
|
||||
let s = ch.to_string();
|
||||
|
||||
// Try ydotool first (uinput-based, works as root)
|
||||
let ydotool_ok = std::process::Command::new("ydotool")
|
||||
.args(["type", &s])
|
||||
.output()
|
||||
|
|
@ -251,6 +445,8 @@ impl X11Injector {
|
|||
if ydotool_ok {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try xdotool
|
||||
let xdotool_ok = std::process::Command::new("xdotool")
|
||||
.args(["type", "--clearmodifiers", &s])
|
||||
.output()
|
||||
|
|
@ -259,33 +455,47 @@ impl X11Injector {
|
|||
if xdotool_ok {
|
||||
return;
|
||||
}
|
||||
// Clipboard fallback: xclip + Ctrl+V via XTEST
|
||||
let copied = std::process::Command::new("xclip")
|
||||
.args(["-selection", "clipboard"])
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.and_then(|mut child| {
|
||||
use std::io::Write;
|
||||
child.stdin.take().unwrap().write_all(s.as_bytes())?;
|
||||
child.wait()
|
||||
})
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(false);
|
||||
if copied {
|
||||
unsafe {
|
||||
(self.lib.x_test_fake_key_event)(self.display, 37, 1, 0); // Ctrl press (X11 keycode)
|
||||
(self.lib.x_test_fake_key_event)(self.display, 55, 1, 0); // V press (X11 keycode)
|
||||
(self.lib.x_test_fake_key_event)(self.display, 55, 0, 0); // V release
|
||||
(self.lib.x_test_fake_key_event)(self.display, 37, 0, 0); // Ctrl release
|
||||
(self.lib.x_flush)(self.display);
|
||||
|
||||
// Fallback: direct X11 clipboard + Ctrl+V
|
||||
self.paste_via_clipboard(0, &s);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for X11Injector {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if self.clipboard_window != 0 && !self.display.is_null() {
|
||||
(self.lib.x_destroy_window)(self.display, self.clipboard_window);
|
||||
}
|
||||
(self.lib.x_close_display)(self.display);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct XSelectionNotifyEvent {
|
||||
_type: c_int,
|
||||
_serial: u64,
|
||||
_send_event: c_int,
|
||||
_display: *mut Display,
|
||||
requestor: Window,
|
||||
selection: Atom,
|
||||
target: Atom,
|
||||
property: Atom,
|
||||
time: Time,
|
||||
}
|
||||
|
||||
impl KeyInjector for X11Injector {
|
||||
fn send_key_event(&self, keycode: u16, value: i32) -> InjectResult {
|
||||
unsafe {
|
||||
(self.lib.x_test_fake_key_event)(self.display, keycode as u32, value, 0);
|
||||
(self.lib.x_flush)(self.display);
|
||||
}
|
||||
InjectResult::Success
|
||||
}
|
||||
|
||||
fn send_backspace(&self) -> InjectResult {
|
||||
self.send_keycode(14, false); // KEY_BACKSPACE
|
||||
self.send_keycode(14, false);
|
||||
InjectResult::Success
|
||||
}
|
||||
|
||||
|
|
@ -308,11 +518,10 @@ impl KeyInjector for X11Injector {
|
|||
|
||||
fn inject_replacement(&self, backspaces: usize, text: &str) -> InjectResult {
|
||||
let is_ascii = text.chars().all(|c| char_to_keycode(c).is_some());
|
||||
|
||||
if is_ascii {
|
||||
if backspaces > 0 {
|
||||
for _ in 0..backspaces {
|
||||
self.send_keycode(14, false); // KEY_BACKSPACE
|
||||
self.send_keycode(14, false);
|
||||
}
|
||||
}
|
||||
for ch in text.chars() {
|
||||
|
|
@ -323,80 +532,11 @@ impl KeyInjector for X11Injector {
|
|||
return InjectResult::Success;
|
||||
}
|
||||
|
||||
// Contains Unicode: try xdotool with both backspaces and text in a single command
|
||||
let has_xdotool = std::process::Command::new("which")
|
||||
.arg("xdotool")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
if has_xdotool {
|
||||
let mut args = Vec::new();
|
||||
if backspaces > 0 {
|
||||
args.push("key".to_string());
|
||||
for _ in 0..backspaces {
|
||||
args.push("BackSpace".to_string());
|
||||
}
|
||||
}
|
||||
if !text.is_empty() {
|
||||
args.push("type".to_string());
|
||||
args.push("--clearmodifiers".to_string());
|
||||
args.push(text.to_string());
|
||||
}
|
||||
|
||||
let ok = std::process::Command::new("xdotool")
|
||||
.args(&args)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
if ok {
|
||||
return InjectResult::Success;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Clipboard copy + paste.
|
||||
// Send backspaces via XTEST, then copy to clipboard, then paste (Ctrl+V) via XTEST.
|
||||
// Since all XTEST key events go through the same display connection, their ordering is guaranteed.
|
||||
let mut clipboard_cmd = std::process::Command::new("xclip");
|
||||
clipboard_cmd.args(["-selection", "clipboard"]);
|
||||
clipboard_cmd.stdin(std::process::Stdio::piped());
|
||||
let copied = clipboard_cmd
|
||||
.spawn()
|
||||
.and_then(|mut child| {
|
||||
use std::io::Write;
|
||||
child.stdin.take().unwrap().write_all(text.as_bytes())?;
|
||||
child.wait()
|
||||
})
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
if copied {
|
||||
if backspaces > 0 {
|
||||
for _ in 0..backspaces {
|
||||
self.send_keycode(14, false); // KEY_BACKSPACE
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
(self.lib.x_test_fake_key_event)(self.display, 37, 1, 0); // Ctrl press (X11 keycode)
|
||||
(self.lib.x_test_fake_key_event)(self.display, 55, 1, 0); // V press (X11 keycode)
|
||||
(self.lib.x_test_fake_key_event)(self.display, 55, 0, 0); // V release
|
||||
(self.lib.x_test_fake_key_event)(self.display, 37, 0, 0); // Ctrl release
|
||||
(self.lib.x_flush)(self.display);
|
||||
}
|
||||
InjectResult::Success
|
||||
} else {
|
||||
// Absolute last resort: backspaces via XTEST followed by individual unicode send_unicode_via_xdotool
|
||||
if backspaces > 0 {
|
||||
for _ in 0..backspaces {
|
||||
self.send_keycode(14, false); // KEY_BACKSPACE
|
||||
}
|
||||
}
|
||||
for ch in text.chars() {
|
||||
self.send_char(ch);
|
||||
}
|
||||
// Contains Unicode: use direct X11 clipboard + XTest Ctrl+V
|
||||
self.paste_via_clipboard(backspaces, text);
|
||||
InjectResult::Success
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) -> InjectResult {
|
||||
unsafe {
|
||||
(self.lib.x_flush)(self.display);
|
||||
|
|
@ -404,20 +544,7 @@ impl KeyInjector for X11Injector {
|
|||
InjectResult::Success
|
||||
}
|
||||
|
||||
/// Record that Unicode text was pasted via clipboard (for future delete/backspace support)
|
||||
fn update_pasted_text(&self, _text: &str) -> InjectResult {
|
||||
eprintln!(
|
||||
"[vietc] X11 update_pasted_text: recorded text (len={})",
|
||||
_text.len()
|
||||
);
|
||||
InjectResult::Success
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for X11Injector {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
(self.lib.x_close_display)(self.display);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Viet+ IME Configuration
|
||||
|
||||
input_method = "telex"
|
||||
input_method = "vni"
|
||||
toggle_key = "space"
|
||||
start_enabled = true
|
||||
start_enabled = false
|
||||
grab = true
|
||||
|
||||
[auto_restore]
|
||||
|
|
|
|||
Loading…
Reference in a new issue