Fix injection: skip ydotool for Unicode, use xdotool/xclip

- Simplify inject_replacement_atomic: backspaces via uinput always
- paste_string skips ydotool for non-ASCII text (crashes with 'no matching keycode')
- Use xdotool (X11) or clipboard (xclip) for Vietnamese characters
- Add detailed logging at EVERY fallback step
- Remove dead ydotoold auto-start code (not available in Ubuntu package)
This commit is contained in:
vndangkhoa 2026-06-24 17:47:09 +07:00
parent 0f1f08e79f
commit 44a4b032a6

View file

@ -24,23 +24,7 @@ unsafe impl Send for UinputInjector {}
unsafe impl Sync for UinputInjector {} unsafe impl Sync for UinputInjector {}
impl UinputInjector { impl UinputInjector {
fn start_ydotoold() {
// ydotoold must be running for ydotool to handle Unicode characters.
// ydotool in direct mode crashes with "no matching keycode" for
// non-ASCII chars. Start it once; ignore failure (daemon may already
// exist).
let _ = std::process::Command::new("ydotoold")
.arg("--fork")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
// Give it a moment to start
std::thread::sleep(std::time::Duration::from_millis(200));
}
pub fn new(name: &str) -> Result<Self, Box<dyn std::error::Error>> { pub fn new(name: &str) -> Result<Self, Box<dyn std::error::Error>> {
Self::start_ydotoold();
let file = OpenOptions::new() let file = OpenOptions::new()
.read(true) .read(true)
.write(true) .write(true)
@ -206,59 +190,20 @@ impl UinputInjector {
} }
/// Send backspaces and text through a single injection channel to avoid /// Send backspaces and text through a single injection channel to avoid
/// reordering between uinput (backspaces) and ydotool (text). /// reordering between input methods. Backspaces always go through uinput
/// (kernel device, no display server dependency). Text is typed via the
/// best available method: ydotool (uinput) for ASCII, xdotool (X11) or
/// clipboard for Unicode.
fn inject_replacement_atomic(&self, backspaces: usize, text: &str) -> InjectResult { fn inject_replacement_atomic(&self, backspaces: usize, text: &str) -> InjectResult {
// Use ydotool for everything — backspaces via `key BackSpace` and // Backspaces via uinput — reliable, no display server needed
// text via `type`. Since both go through ydotool's uinput device,
// the kernel delivers them in the correct order.
if backspaces > 0 || !text.is_empty() {
let mut args: Vec<&str> = Vec::new();
for _ in 0..backspaces {
args.push("key");
args.push("BackSpace");
}
if !text.is_empty() {
args.push("type");
args.push(text);
}
// ydotool runs directly (uses uinput, no display server needed)
let output = std::process::Command::new("ydotool")
.args(&args)
.output();
if let Ok(output) = output {
if output.status.success() {
return InjectResult::Success;
}
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
eprintln!("[vietc] ydotool failed: {}", stderr.trim());
}
}
}
// Fallback: wtype with -k BackSpace (Wayland) or uinput backspaces + paste
if backspaces > 0 || !text.is_empty() {
let mut wtype_args: Vec<&str> = Vec::new();
let mut bs_flags: Vec<String> = Vec::new();
for _ in 0..backspaces {
bs_flags.push("-k".to_string());
bs_flags.push("BackSpace".to_string());
}
for a in &bs_flags {
wtype_args.push(a);
}
wtype_args.push(text);
let output = Self::run_as_user("wtype", &wtype_args);
if output.status.success() {
return InjectResult::Success;
}
}
// Last resort: uinput backspaces + paste_string
if backspaces > 0 { if backspaces > 0 {
eprintln!("[vietc] uinput backspace x{}", backspaces);
for _ in 0..backspaces { for _ in 0..backspaces {
let _ = self.send_backspace(); let _ = self.send_backspace();
} }
} }
if !text.is_empty() { if !text.is_empty() {
eprintln!("[vietc] text injection: \"{}\"", text);
self.paste_string(text); self.paste_string(text);
} }
InjectResult::Success InjectResult::Success
@ -269,35 +214,61 @@ impl UinputInjector {
/// unavailable. Prefers ydotool (uinput, works everywhere) to avoid /// unavailable. Prefers ydotool (uinput, works everywhere) to avoid
/// clipboard pollution. /// clipboard pollution.
fn paste_string(&self, s: &str) { fn paste_string(&self, s: &str) {
// ydotool uses uinput (kernel device), works as root without any let has_unicode = s.chars().any(|c| c > '\x7f');
// display server access. No need for run_as_user.
if !has_unicode {
// Pure ASCII: ydotool works reliably (no keycode mapping issues).
let output = std::process::Command::new("ydotool") let output = std::process::Command::new("ydotool")
.args(["type", s]) .args(["type", s])
.output(); .output();
if let Ok(output) = output { if let Ok(output) = output {
if output.status.success() { if output.status.success() {
eprintln!("[vietc] ydotool OK");
return; return;
} }
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
eprintln!("[vietc] ydotool failed: {}", stderr.trim());
} }
eprintln!("[vietc] ydotool failed, trying xdotool..."); eprintln!("[vietc] ydotool failed, trying xdotool...");
}
} else {
eprintln!("[vietc] contains Unicode, skipping ydotool");
}
// Try xdotool (X11): needs DISPLAY, run through run_as_user // Try xdotool (X11): needs DISPLAY, run through run_as_user
eprintln!("[vietc] trying xdotool...");
let output = Self::run_as_user("xdotool", &["type", "--clearmodifiers", s]); let output = Self::run_as_user("xdotool", &["type", "--clearmodifiers", s]);
if output.status.success() { if output.status.success() {
eprintln!("[vietc] xdotool OK");
return; return;
} }
eprintln!("[vietc] xdotool not available, trying wtype..."); let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
eprintln!("[vietc] xdotool failed: {}", stderr.trim());
}
// Try wtype (Wayland-native): needs Wayland session, run through run_as_user // Try wtype (Wayland-native): needs Wayland session, run through run_as_user
eprintln!("[vietc] xdotool failed, trying wtype...");
let output = Self::run_as_user("wtype", &[s]); let output = Self::run_as_user("wtype", &[s]);
if output.status.success() { if output.status.success() {
eprintln!("[vietc] wtype OK");
return; return;
} }
eprintln!("[vietc] wtype not available, trying clipboard paste..."); let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
eprintln!("[vietc] wtype failed: {}", stderr.trim());
}
// Clipboard fallback: copy + paste via our uinput // Clipboard fallback: copy + paste via our uinput
eprintln!("[vietc] wtype failed, trying clipboard paste...");
let copied = self.copy_to_clipboard(s); let copied = self.copy_to_clipboard(s);
if copied { if copied {
eprintln!("[vietc] clipboard OK, sending Ctrl+V");
self.send_ctrl_v(); self.send_ctrl_v();
return; return;
} }
eprintln!("[vietc] WARNING: No injection method works for '{}'!", s); eprintln!("[vietc] WARNING: No injection method works for '{}'!", s);
} }
@ -309,6 +280,8 @@ impl UinputInjector {
let wayland_display = std::env::var("WAYLAND_DISPLAY").unwrap_or_default(); let wayland_display = std::env::var("WAYLAND_DISPLAY").unwrap_or_default();
let xdg_runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_default(); let xdg_runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_default();
let display = std::env::var("DISPLAY").unwrap_or_default(); let display = std::env::var("DISPLAY").unwrap_or_default();
eprintln!("[vietc] clipboard: is_root, SUDO_USER={} DISPLAY={} WAYLAND={} XDG_RUNTIME_DIR={}",
sudo_user, display, wayland_display, xdg_runtime_dir);
let mut cmd = std::process::Command::new("sudo"); let mut cmd = std::process::Command::new("sudo");
cmd.args(["-u", &sudo_user, "env"]); cmd.args(["-u", &sudo_user, "env"]);
if !wayland_display.is_empty() { if !wayland_display.is_empty() {
@ -321,6 +294,7 @@ impl UinputInjector {
cmd.arg(format!("DISPLAY={}", display)); cmd.arg(format!("DISPLAY={}", display));
} }
cmd.arg("wl-copy"); cmd.arg("wl-copy");
eprintln!("[vietc] clipboard: trying wl-copy via {:?}", cmd);
let result = cmd let result = cmd
.stdin(std::process::Stdio::piped()) .stdin(std::process::Stdio::piped())
.spawn() .spawn()
@ -331,24 +305,38 @@ impl UinputInjector {
}); });
if let Ok(status) = result { if let Ok(status) = result {
if status.success() { if status.success() {
eprintln!("[vietc] clipboard: wl-copy OK");
return true; return true;
} }
eprintln!("[vietc] clipboard: wl-copy failed (exit={:?})", status.code());
} else if let Err(ref e) = result {
eprintln!("[vietc] clipboard: wl-copy error: {}", e);
} }
} else {
eprintln!("[vietc] clipboard: is_root but no SUDO_USER");
} }
} else if std::process::Command::new("wl-copy") } else {
eprintln!("[vietc] clipboard: not root, trying wl-copy directly");
let result = std::process::Command::new("wl-copy")
.stdin(std::process::Stdio::piped()) .stdin(std::process::Stdio::piped())
.spawn() .spawn()
.and_then(|mut child| { .and_then(|mut child| {
use std::io::Write; use std::io::Write;
child.stdin.take().unwrap().write_all(s.as_bytes())?; child.stdin.take().unwrap().write_all(s.as_bytes())?;
child.wait() child.wait()
}) });
.map(|status| status.success()) if let Ok(status) = result {
.unwrap_or(false) if status.success() {
{ eprintln!("[vietc] clipboard: wl-copy OK");
return true; return true;
} }
eprintln!("[vietc] clipboard: wl-copy failed (exit={:?})", status.code());
} else if let Err(ref e) = result {
eprintln!("[vietc] clipboard: wl-copy error: {}", e);
}
}
// Try xclip (X11). When root, run as SUDO_USER so it can connect to X. // Try xclip (X11). When root, run as SUDO_USER so it can connect to X.
eprintln!("[vietc] clipboard: trying xclip...");
let xclip_result = if is_root { let xclip_result = if is_root {
if let Ok(sudo_user) = std::env::var("SUDO_USER") { if let Ok(sudo_user) = std::env::var("SUDO_USER") {
let display = std::env::var("DISPLAY").unwrap_or_default(); let display = std::env::var("DISPLAY").unwrap_or_default();
@ -359,6 +347,7 @@ impl UinputInjector {
} }
cmd.arg("xclip"); cmd.arg("xclip");
cmd.args(["-selection", "clipboard"]); cmd.args(["-selection", "clipboard"]);
eprintln!("[vietc] clipboard: xclip via {:?}", cmd);
cmd.stdin(std::process::Stdio::piped()) cmd.stdin(std::process::Stdio::piped())
.spawn() .spawn()
.and_then(|mut child| { .and_then(|mut child| {
@ -366,12 +355,24 @@ impl UinputInjector {
child.stdin.take().unwrap().write_all(s.as_bytes())?; child.stdin.take().unwrap().write_all(s.as_bytes())?;
child.wait() child.wait()
}) })
.map(|status| status.success()) .map(|status| {
.unwrap_or(false) if status.success() {
eprintln!("[vietc] clipboard: xclip OK");
} else { } else {
eprintln!("[vietc] clipboard: xclip failed (exit={:?})", status.code());
}
status.success()
})
.unwrap_or_else(|e| {
eprintln!("[vietc] clipboard: xclip error: {}", e);
false
})
} else {
eprintln!("[vietc] clipboard: is_root but no SUDO_USER in xclip path");
false false
} }
} else { } else {
eprintln!("[vietc] clipboard: not root, trying xclip directly");
std::process::Command::new("xclip") std::process::Command::new("xclip")
.args(["-selection", "clipboard"]) .args(["-selection", "clipboard"])
.stdin(std::process::Stdio::piped()) .stdin(std::process::Stdio::piped())
@ -381,15 +382,21 @@ impl UinputInjector {
child.stdin.take().unwrap().write_all(s.as_bytes())?; child.stdin.take().unwrap().write_all(s.as_bytes())?;
child.wait() child.wait()
}) })
.map(|status| status.success()) .map(|status| {
.unwrap_or(false) if status.success() {
eprintln!("[vietc] clipboard: xclip OK");
} else {
eprintln!("[vietc] clipboard: xclip failed (exit={:?})", status.code());
}
status.success()
})
.unwrap_or_else(|e| {
eprintln!("[vietc] clipboard: xclip error: {}", e);
false
})
}; };
if xclip_result { xclip_result
return true;
}
false
} }
/// Send Ctrl+V through our uinput device. /// Send Ctrl+V through our uinput device.