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:
Khoa Vo 2026-06-26 07:56:52 +07:00
parent 38f3bca022
commit bb0847a38f
9 changed files with 875 additions and 214 deletions

View file

@ -79,13 +79,13 @@ impl Default for AppStateConfig {
} }
fn default_input_method() -> String { fn default_input_method() -> String {
"telex".into() "vni".into()
} }
fn default_toggle_key() -> String { fn default_toggle_key() -> String {
"space".into() "space".into()
} }
fn default_start_enabled() -> bool { fn default_start_enabled() -> bool {
true false
} }
fn default_true() -> bool { fn default_true() -> bool {
true true
@ -278,9 +278,9 @@ vs = "với"
fn parse_empty_config_uses_defaults() { fn parse_empty_config_uses_defaults() {
let toml = ""; let toml = "";
let config: Config = toml::from_str(toml).unwrap(); 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_eq!(config.toggle_key, "space");
assert!(config.start_enabled); assert!(!config.start_enabled);
assert!(config.auto_restore.enabled); assert!(config.auto_restore.enabled);
assert!(config.app_state.enabled); assert!(config.app_state.enabled);
assert!(!config.app_state.english_apps.is_empty()); assert!(!config.app_state.english_apps.is_empty());
@ -295,7 +295,7 @@ input_method = "vni"
let config: Config = toml::from_str(toml).unwrap(); let config: Config = toml::from_str(toml).unwrap();
assert_eq!(config.input_method, "vni"); assert_eq!(config.input_method, "vni");
assert_eq!(config.toggle_key, "space"); // default assert_eq!(config.toggle_key, "space"); // default
assert!(config.start_enabled); // default assert!(!config.start_enabled); // default
} }
#[test] #[test]

View file

@ -15,6 +15,11 @@ mod display;
use app_state::AppStateManager; use app_state::AppStateManager;
use config::Config; 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> { fn get_log_path() -> Option<PathBuf> {
dirs::config_dir().map(|p| p.join("vietc").join("vietc.log")) 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() { match open_keyboard_device() {
Ok((device, path)) => { Ok((device, path)) => {
log_info(&format!("[vietc] Keyboard 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( fn run_with_evdev(
mut device: evdev::Device, mut device: evdev::Device,
daemon: &mut Daemon, daemon: &mut Daemon,

View file

@ -24,6 +24,7 @@ fn get_display(events: &[EngineEvent]) -> String {
for _ in 0..*backspaces { display.pop(); } for _ in 0..*backspaces { display.pop(); }
display.push_str(restored); display.push_str(restored);
} }
EngineEvent::Paste(text) => { display.push_str(text); }
} }
} }
display display

View file

@ -43,6 +43,15 @@ else
[ -f ui/target/release/vietc-tray ] && cp ui/target/release/vietc-tray "$APPDIR/usr/bin/" [ -f ui/target/release/vietc-tray ] && cp ui/target/release/vietc-tray "$APPDIR/usr/bin/"
fi 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 # Desktop integration
echo "[3/5] Installing desktop integration..." echo "[3/5] Installing desktop integration..."
if [ -f "deb-build/vietc.desktop" ]; then if [ -f "deb-build/vietc.desktop" ]; then
@ -109,6 +118,67 @@ else
cp "$APPDIR/usr/share/applications/vietc.desktop" "$APPDIR/" cp "$APPDIR/usr/share/applications/vietc.desktop" "$APPDIR/"
fi 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 # Create custom AppRun script
cat > "$APPDIR/AppRun" << 'EOF' cat > "$APPDIR/AppRun" << 'EOF'
#!/bin/sh #!/bin/sh
@ -128,39 +198,51 @@ ENV_PREFIX="env"
[ -n "$XDG_RUNTIME_DIR" ] && ENV_PREFIX="$ENV_PREFIX XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" [ -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) # 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).
# Fix Wayland env for root: sudo resets XDG_RUNTIME_DIR, breaking wtype/wl-copy. # On Wayland, evdev requires root (input group) or uinput.
# Only set WAYLAND_DISPLAY if the user actually has a Wayland session. NEED_ROOT=""
if [ "$(id -u)" = "0" ] && [ -z "$XDG_RUNTIME_DIR" ] && [ -n "$SUDO_USER" ]; then if [ -n "$WAYLAND_DISPLAY" ]; then
USER_UID=$(id -u "$SUDO_USER" 2>/dev/null || echo 1000) NEED_ROOT="yes"
export XDG_RUNTIME_DIR="/run/user/$USER_UID"
if [ -d "/run/user/$USER_UID" ] && ls "/run/user/$USER_UID/wayland-*" >/dev/null 2>&1; then
export WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}"
fi
fi fi
if command -v pkexec >/dev/null && [ -z "$WAYLAND_DISPLAY" ]; then if [ -z "$NEED_ROOT" ]; then
pkill -x vietc 2>/dev/null; sleep 0.5 # X11: no root needed
pkexec $ENV_PREFIX "$HERE/usr/bin/vietc" >/dev/null & pkill -x vietc 2>/dev/null; sleep 0.3
"$HERE/usr/bin/vietc" >/dev/null &
DAEMON_PID=$! DAEMON_PID=$!
elif [ -n "$WAYLAND_DISPLAY" ]; then else
password="" # Fix Wayland env for root: sudo resets XDG_RUNTIME_DIR, breaking wtype/wl-copy.
if command -v kdialog >/dev/null; then if [ "$(id -u)" = "0" ] && [ -z "$XDG_RUNTIME_DIR" ] && [ -n "$SUDO_USER" ]; then
password=$(kdialog --password "Viet+ needs root privileges to grab the keyboard.") || password="" USER_UID=$(id -u "$SUDO_USER" 2>/dev/null || echo 1000)
elif command -v zenity >/dev/null; then export XDG_RUNTIME_DIR="/run/user/$USER_UID"
password=$(zenity --password --title="Viet+ needs root") || password="" if [ -d "/run/user/$USER_UID" ] && ls "/run/user/$USER_UID/wayland-*" >/dev/null 2>&1; then
elif command -v ssh-askpass >/dev/null; then export WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}"
password=$(ssh-askpass "Viet+ needs root privileges") || password="" fi
fi fi
if [ -n "$password" ]; then
if command -v pkexec >/dev/null; then
pkill -x vietc 2>/dev/null; sleep 0.5 pkill -x vietc 2>/dev/null; sleep 0.5
echo "$password" | sudo -S $ENV_PREFIX "$HERE/usr/bin/vietc" >/dev/null & pkexec $ENV_PREFIX "$HERE/usr/bin/vietc" >/dev/null &
DAEMON_PID=$!
elif [ -n "$WAYLAND_DISPLAY" ]; then
password=""
if command -v kdialog >/dev/null; then
password=$(kdialog --password "Viet+ needs root privileges to grab the keyboard.") || password=""
elif command -v zenity >/dev/null; then
password=$(zenity --password --title="Viet+ needs root") || password=""
elif command -v ssh-askpass >/dev/null; then
password=$(ssh-askpass "Viet+ needs root privileges") || password=""
fi
if [ -n "$password" ]; then
pkill -x vietc 2>/dev/null; sleep 0.5
echo "$password" | sudo -S $ENV_PREFIX "$HERE/usr/bin/vietc" >/dev/null &
DAEMON_PID=$!
fi
elif command -v sudo >/dev/null; then
pkill -x vietc 2>/dev/null; sleep 0.5
sudo $ENV_PREFIX "$HERE/usr/bin/vietc" >/dev/null &
DAEMON_PID=$! DAEMON_PID=$!
fi fi
elif command -v sudo >/dev/null; then
pkill -x vietc 2>/dev/null; sleep 0.5
sudo $ENV_PREFIX "$HERE/usr/bin/vietc" >/dev/null &
DAEMON_PID=$!
fi fi
if [ -z "$DAEMON_PID" ] && ! pgrep -x vietc >/dev/null; then if [ -z "$DAEMON_PID" ] && ! pgrep -x vietc >/dev/null; then

View file

@ -12,7 +12,7 @@ echo "=== Building Viet+ .deb package v${VERSION} ==="
# Build binaries (all features: x11 + wayland) # Build binaries (all features: x11 + wayland)
echo "[1/5] Building binaries..." echo "[1/5] Building binaries..."
cargo build --release --features "x11,wayland" --manifest-path "$PROJECT_ROOT/Cargo.toml" 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." echo " Done."
# Clean and create staging # Clean and create staging
@ -113,7 +113,7 @@ Section: utils
Priority: optional Priority: optional
Architecture: amd64 Architecture: amd64
Depends: libc6 (>= 2.31), libevdev2 (>= 1.9.0) 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> Maintainer: Khoa Vo <vndangkhoa@gmail.com>
Description: Viet+ — Vietnamese Input Method for Linux Description: Viet+ — Vietnamese Input Method for Linux
Zero-configuration Vietnamese input method engine supporting Zero-configuration Vietnamese input method engine supporting

View file

@ -6,5 +6,8 @@ pub mod wayland_im;
#[cfg(feature = "x11")] #[cfg(feature = "x11")]
pub mod x11_inject; pub mod x11_inject;
#[cfg(feature = "x11")]
pub mod x11_capture;
pub use inject::KeyInjector; pub use inject::KeyInjector;
pub use monitor::KeyMonitor; pub use monitor::KeyMonitor;

305
protocol/src/x11_capture.rs Normal file
View 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);
}
}
}

View file

@ -1,26 +1,46 @@
use super::inject::{InjectResult, KeyInjector}; use super::inject::{InjectResult, KeyInjector};
use std::cell::RefCell;
use std::ffi::{c_char, c_int, c_void}; use std::ffi::{c_char, c_int, c_void};
type Display = c_void; type Display = c_void;
type Window = u64; type Window = u64;
type Atom = u64;
type Time = u64;
// Dynamic linker FFI
extern "C" { extern "C" {
fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void; fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void;
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
fn dlclose(handle: *mut c_void) -> c_int; 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 { struct X11Lib {
x11_handle: *mut c_void, x11_handle: *mut c_void,
xtst_handle: *mut c_void, xtst_handle: *mut c_void,
// Symbols
x_open_display: unsafe extern "C" fn(*const c_char) -> *mut Display, x_open_display: unsafe extern "C" fn(*const c_char) -> *mut Display,
x_close_display: unsafe extern "C" fn(*mut Display) -> c_int, x_close_display: unsafe extern "C" fn(*mut Display) -> c_int,
x_default_root_window: unsafe extern "C" fn(*mut Display) -> Window, x_default_root_window: unsafe extern "C" fn(*mut Display) -> Window,
x_flush: unsafe extern "C" fn(*mut Display) -> c_int, 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_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 { impl X11Lib {
@ -32,7 +52,7 @@ impl X11Lib {
]; ];
let mut x11_handle = std::ptr::null_mut(); let mut x11_handle = std::ptr::null_mut();
for path in x11_paths { for path in x11_paths {
x11_handle = dlopen(path, 1); // RTLD_LAZY x11_handle = dlopen(path, 1);
if !x11_handle.is_null() { if !x11_handle.is_null() {
break; break;
} }
@ -57,24 +77,27 @@ impl X11Lib {
return Err("Failed to load libXtst.so.6".into()); return Err("Failed to load libXtst.so.6".into());
} }
let x_open_display = std::mem::transmute(dlsym( macro_rules! sym {
x11_handle, ($handle:expr, $name:expr) => {
b"XOpenDisplay\0".as_ptr() as *const c_char, std::mem::transmute(dlsym($handle, concat!($name, "\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_open_display = sym!(x11_handle, "XOpenDisplay");
)); let x_close_display = sym!(x11_handle, "XCloseDisplay");
let x_default_root_window = std::mem::transmute(dlsym( let x_default_root_window = sym!(x11_handle, "XDefaultRootWindow");
x11_handle, let x_flush = sym!(x11_handle, "XFlush");
b"XDefaultRootWindow\0".as_ptr() as *const c_char, let x_intern_atom = sym!(x11_handle, "XInternAtom");
)); let x_set_selection_owner = sym!(x11_handle, "XSetSelectionOwner");
let x_flush = let x_change_property = sym!(x11_handle, "XChangeProperty");
std::mem::transmute(dlsym(x11_handle, b"XFlush\0".as_ptr() as *const c_char)); let x_get_selection_owner = sym!(x11_handle, "XGetSelectionOwner");
let x_test_fake_key_event = std::mem::transmute(dlsym( let x_send_event = sym!(x11_handle, "XSendEvent");
xtst_handle, let x_create_simple_window = sym!(x11_handle, "XCreateSimpleWindow");
b"XTestFakeKeyEvent\0".as_ptr() as *const c_char, 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 { Ok(Self {
x11_handle, x11_handle,
@ -84,6 +107,16 @@ impl X11Lib {
x_default_root_window, x_default_root_window,
x_flush, x_flush,
x_test_fake_key_event, 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; const X11_KEYCODE_OFFSET: u32 = 8;
// X11 keycodes for common ASCII characters
fn char_to_keycode(ch: char) -> Option<(u32, bool)> { fn char_to_keycode(ch: char) -> Option<(u32, bool)> {
match ch { match ch {
'a' => Some((30, false)), 'a' => Some((30, false)),
@ -156,53 +187,41 @@ fn char_to_keycode(ch: char) -> Option<(u32, bool)> {
'X' => Some((45, true)), 'X' => Some((45, true)),
'Y' => Some((21, true)), 'Y' => Some((21, true)),
'Z' => Some((44, 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, _ => 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 { pub struct X11Injector {
lib: X11Lib, lib: X11Lib,
display: *mut Display, display: *mut Display,
#[allow(dead_code)] root: Window,
window: 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 {} unsafe impl Send for X11Injector {}
@ -216,33 +235,208 @@ impl X11Injector {
if display.is_null() { if display.is_null() {
return Err("Cannot open X11 display. Is DISPLAY set?".into()); 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 { Ok(Self {
lib, lib,
display, 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,
&notify 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) { fn send_keycode(&self, keycode: u32, shift: bool) {
unsafe { unsafe {
if shift { 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, 1, 0);
(self.lib.x_test_fake_key_event)(self.display, keycode, 0, 0); // Key release (self.lib.x_test_fake_key_event)(self.display, keycode, 0, 0);
if shift { 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); (self.lib.x_flush)(self.display);
} }
} }
fn send_unicode_via_xdotool(&self, ch: char) { 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(); let s = ch.to_string();
// Try ydotool first (uinput-based, works as root)
let ydotool_ok = std::process::Command::new("ydotool") let ydotool_ok = std::process::Command::new("ydotool")
.args(["type", &s]) .args(["type", &s])
.output() .output()
@ -251,6 +445,8 @@ impl X11Injector {
if ydotool_ok { if ydotool_ok {
return; return;
} }
// Try xdotool
let xdotool_ok = std::process::Command::new("xdotool") let xdotool_ok = std::process::Command::new("xdotool")
.args(["type", "--clearmodifiers", &s]) .args(["type", "--clearmodifiers", &s])
.output() .output()
@ -259,33 +455,47 @@ impl X11Injector {
if xdotool_ok { if xdotool_ok {
return; return;
} }
// Clipboard fallback: xclip + Ctrl+V via XTEST
let copied = std::process::Command::new("xclip") // Fallback: direct X11 clipboard + Ctrl+V
.args(["-selection", "clipboard"]) self.paste_via_clipboard(0, &s);
.stdin(std::process::Stdio::piped()) }
.spawn() }
.and_then(|mut child| {
use std::io::Write; impl Drop for X11Injector {
child.stdin.take().unwrap().write_all(s.as_bytes())?; fn drop(&mut self) {
child.wait() unsafe {
}) if self.clipboard_window != 0 && !self.display.is_null() {
.map(|status| status.success()) (self.lib.x_destroy_window)(self.display, self.clipboard_window);
.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);
} }
(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 { 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 { fn send_backspace(&self) -> InjectResult {
self.send_keycode(14, false); // KEY_BACKSPACE self.send_keycode(14, false);
InjectResult::Success InjectResult::Success
} }
@ -308,11 +518,10 @@ impl KeyInjector for X11Injector {
fn inject_replacement(&self, backspaces: usize, text: &str) -> InjectResult { fn inject_replacement(&self, backspaces: usize, text: &str) -> InjectResult {
let is_ascii = text.chars().all(|c| char_to_keycode(c).is_some()); let is_ascii = text.chars().all(|c| char_to_keycode(c).is_some());
if is_ascii { if is_ascii {
if backspaces > 0 { if backspaces > 0 {
for _ in 0..backspaces { for _ in 0..backspaces {
self.send_keycode(14, false); // KEY_BACKSPACE self.send_keycode(14, false);
} }
} }
for ch in text.chars() { for ch in text.chars() {
@ -323,80 +532,11 @@ impl KeyInjector for X11Injector {
return InjectResult::Success; return InjectResult::Success;
} }
// Contains Unicode: try xdotool with both backspaces and text in a single command // Contains Unicode: use direct X11 clipboard + XTest Ctrl+V
let has_xdotool = std::process::Command::new("which") self.paste_via_clipboard(backspaces, text);
.arg("xdotool") InjectResult::Success
.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);
}
InjectResult::Success
}
} }
fn flush(&self) -> InjectResult { fn flush(&self) -> InjectResult {
unsafe { unsafe {
(self.lib.x_flush)(self.display); (self.lib.x_flush)(self.display);
@ -404,20 +544,7 @@ impl KeyInjector for X11Injector {
InjectResult::Success InjectResult::Success
} }
/// Record that Unicode text was pasted via clipboard (for future delete/backspace support)
fn update_pasted_text(&self, _text: &str) -> InjectResult { fn update_pasted_text(&self, _text: &str) -> InjectResult {
eprintln!(
"[vietc] X11 update_pasted_text: recorded text (len={})",
_text.len()
);
InjectResult::Success InjectResult::Success
} }
} }
impl Drop for X11Injector {
fn drop(&mut self) {
unsafe {
(self.lib.x_close_display)(self.display);
}
}
}

View file

@ -1,8 +1,8 @@
# Viet+ IME Configuration # Viet+ IME Configuration
input_method = "telex" input_method = "vni"
toggle_key = "space" toggle_key = "space"
start_enabled = true start_enabled = false
grab = true grab = true
[auto_restore] [auto_restore]