// SPDX-License-Identifier: MIT use std::fs::{File, OpenOptions}; use std::os::unix::io::AsRawFd; use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant}; use super::inject::{InjectResult, KeyInjector}; /// How long to wait after the last Unicode paste before restoring the user's /// real clipboard. Each paste pushes this deadline back, so a burst of typing /// only triggers a single restore once the user pauses — the user's clipboard /// is never pasted into the text while the target app might still be reading /// our freshly pasted word. const RESTORE_DEBOUNCE: Duration = Duration::from_millis(600); const UINPUT_MAX_NAME_SIZE: usize = 80; const UI_SET_EVBIT: u64 = 0x40045564; const UI_SET_KEYBIT: u64 = 0x40045565; #[allow(dead_code)] const UI_SET_ABSBIT: u64 = 0x40045566; const UI_DEV_CREATE: u64 = 0x5501; const UI_DEV_DESTROY: u64 = 0x5502; const UI_DEV_SETUP: u64 = 0x405c5503; const EV_KEY: u16 = 0x01; #[allow(dead_code)] const EV_ABS: u16 = 0x03; const KEY_MAX: u32 = 0x1ff; /// Shared clipboard bookkeeping between the injection path and the background /// restorer thread. struct ClipInner { /// The user's real clipboard contents, saved before we overwrite the /// clipboard to inject Unicode text, so we can restore it afterwards. saved_clipboard: Option, /// The last text we wrote to the clipboard ourselves (an injected word or /// the restored user content). Used to tell our own writes apart from text /// the user copied with Ctrl+C. last_injected: Option, /// When set, the restorer thread should rewrite the user's clipboard at /// this instant. `None` means no restore is pending. restore_due: Option, /// Set on shutdown so the restorer thread can exit. shutdown: bool, } struct ClipState { inner: Mutex, cv: Condvar, } pub struct UinputInjector { file: File, clip: Arc, } unsafe impl Send for UinputInjector {} unsafe impl Sync for UinputInjector {} impl UinputInjector { fn send_enter(&self) { self.send_uinput_event(EV_KEY, 28, 1); self.send_uinput_event(0, 0, 0); std::thread::sleep(std::time::Duration::from_millis(2)); self.send_uinput_event(EV_KEY, 28, 0); self.send_uinput_event(0, 0, 0); std::thread::sleep(std::time::Duration::from_millis(2)); } pub fn new(name: &str) -> Result> { let file = OpenOptions::new() .read(true) .write(true) .open("/dev/uinput")?; let fd = file.as_raw_fd(); // Enable EV_KEY ioctl(fd, UI_SET_EVBIT, EV_KEY as u64) .map_err(|e| format!("UI_SET_EVBIT failed: {}", e))?; // Enable all key codes we'll need for code in 0..=KEY_MAX { ioctl(fd, UI_SET_KEYBIT, code as u64) .map_err(|e| format!("UI_SET_KEYBIT {} failed: {}", code, e))?; } // Create uinput device let mut usetup: uinput_setup = unsafe { std::mem::zeroed() }; let name_bytes = name.as_bytes(); let copy_len = name_bytes.len().min(UINPUT_MAX_NAME_SIZE - 1); for (i, &byte) in name_bytes.iter().enumerate().take(copy_len) { usetup.name[i] = byte as i8; } usetup.name[copy_len] = 0; usetup.id.bustype = 0x03; // BUS_USB usetup.id.vendor = 0x1234; usetup.id.product = 0x5678; usetup.id.version = 1; ioctl(fd, UI_DEV_SETUP, &usetup as *const uinput_setup as u64) .map_err(|e| format!("UI_DEV_SETUP failed: {}", e))?; ioctl(fd, UI_DEV_CREATE, 0).map_err(|e| format!("UI_DEV_CREATE failed: {}", e))?; // Small delay for device to be ready std::thread::sleep(std::time::Duration::from_millis(10)); let clip = Arc::new(ClipState { inner: Mutex::new(ClipInner { saved_clipboard: None, last_injected: None, restore_due: None, shutdown: false, }), cv: Condvar::new(), }); { let clip = Arc::clone(&clip); std::thread::spawn(move || run_restorer(clip)); } Ok(Self { file, clip }) } fn send_uinput_event(&self, type_: u16, code: u16, value: i32) { let event = input_event { time: timeval { tv_sec: 0, tv_usec: 0, }, type_, code, value, }; unsafe { let ptr = &event as *const input_event as *const u8; let len = std::mem::size_of::(); let _ = libc::write( self.file.as_raw_fd() as libc::c_int, ptr as *const libc::c_void, len, ); } } fn send_key_stroke(&self, keycode: u16, shift: bool) { if shift { self.send_uinput_event(EV_KEY, 42, 1); // Shift press self.send_uinput_event(0, 0, 0); // SYN std::thread::sleep(std::time::Duration::from_millis(2)); } self.send_uinput_event(EV_KEY, keycode, 1); // Key press self.send_uinput_event(0, 0, 0); // SYN std::thread::sleep(std::time::Duration::from_millis(2)); self.send_uinput_event(EV_KEY, keycode, 0); // Key release self.send_uinput_event(0, 0, 0); // SYN std::thread::sleep(std::time::Duration::from_millis(2)); if shift { self.send_uinput_event(EV_KEY, 42, 0); // Shift release self.send_uinput_event(0, 0, 0); // SYN std::thread::sleep(std::time::Duration::from_millis(2)); } } } impl KeyInjector for UinputInjector { fn send_backspace(&self) -> InjectResult { self.send_uinput_event(EV_KEY, 14, 1); // KEY_BACKSPACE press self.send_uinput_event(0, 0, 0); // SYN std::thread::sleep(std::time::Duration::from_millis(2)); self.send_uinput_event(EV_KEY, 14, 0); // KEY_BACKSPACE release self.send_uinput_event(0, 0, 0); // SYN std::thread::sleep(std::time::Duration::from_millis(2)); InjectResult::Success } fn send_key_event(&self, keycode: u16, value: i32) -> InjectResult { self.send_uinput_event(EV_KEY, keycode, value); self.send_uinput_event(0, 0, 0); std::thread::sleep(std::time::Duration::from_millis(2)); InjectResult::Success } fn send_char(&self, ch: char) -> InjectResult { if let Some(keycode) = char_to_linux_keycode(ch) { let needs_shift = ch.is_uppercase() || "!@#$%^&*()_+{}|:\"<>?".contains(ch); self.send_key_stroke(keycode, needs_shift); return InjectResult::Success; } // Vietnamese Unicode char: map to base ASCII and send via uinput let ascii = strip_vn_diacritic(ch); if let Some(keycode) = char_to_linux_keycode(ascii) { let needs_shift = ascii.is_uppercase(); self.send_key_stroke(keycode, needs_shift); } InjectResult::Success } fn send_string(&self, s: &str) -> InjectResult { // ASCII characters: inject directly via uinput keycodes let is_ascii = s.chars().all(|c| char_to_linux_keycode(c).is_some()); eprintln!( "[vietc] send_string: len={}, is_ascii={}", s.len(), is_ascii ); if is_ascii { eprintln!( "[vietc] send_string: ASCII '{}' via uinput", s.escape_default() ); for ch in s.chars() { self.send_char(ch); } return InjectResult::Success; } // Unicode text: single clipboard copy + paste (reliable method) eprintln!( "[vietc] send_string: Unicode '{}' - using clipboard", s.escape_default() ); let copied = self.paste_via_clipboard(s, false); if copied { eprintln!("[vietc] send_string complete (clipboard)"); return InjectResult::Success; } else { eprintln!( "[vietc] send_string failed for '{}' (clipboard unavailable)", s.escape_default() ); // Last resort: try paste_string (will try clipboard internally) self.paste_string(s); } InjectResult::Success } fn inject_replacement(&self, backspaces: usize, text: &str) -> InjectResult { self.inject_replacement_atomic(backspaces, text) } fn flush(&self) -> InjectResult { InjectResult::Success } /// Record that Unicode text was pasted via clipboard (for future delete/backspace support) fn update_pasted_text(&self, text: &str) -> InjectResult { // Text tracking happens through OutputCommand pipeline in daemon // This is called after clipboard paste to inform engine of pasted content eprintln!( "[vietc] update_pasted_text: recorded '{}' (len={})", text.escape_default(), text.len() ); InjectResult::Success } } impl UinputInjector { /// Get the original non-root username when running as root. /// Checks SUDO_USER (sudo), PKEXEC_UID (pkexec), /proc/self/loginuid, /// and falls back to `logname`. fn get_original_username() -> Option { let is_root = unsafe { libc::getuid() == 0 }; if !is_root { return None; } if let Ok(user) = std::env::var("SUDO_USER") { if !user.is_empty() { return Some(user); } } if let Ok(uid_str) = std::env::var("PKEXEC_UID") { if let Ok(uid) = uid_str.parse::() { unsafe { let pw = libc::getpwuid(uid); if !pw.is_null() { let name = std::ffi::CStr::from_ptr((*pw).pw_name) .to_string_lossy() .into_owned(); if !name.is_empty() { return Some(name); } } } } } if let Ok(content) = std::fs::read_to_string("/proc/self/loginuid") { if let Ok(uid) = content.trim().parse::() { if uid != 4294967295 { unsafe { let pw = libc::getpwuid(uid); if !pw.is_null() { let name = std::ffi::CStr::from_ptr((*pw).pw_name) .to_string_lossy() .into_owned(); if !name.is_empty() { return Some(name); } } } } } } if let Ok(output) = std::process::Command::new("logname").output() { if output.status.success() { let name = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !name.is_empty() { return Some(name); } } } None } /// Get original non-root UID and GID when running as root. fn get_original_uid_gid() -> Option<(u32, u32)> { let is_root = unsafe { libc::getuid() == 0 }; if !is_root { return None; } let mut target_uid = None; if let Ok(uid_str) = std::env::var("SUDO_UID") { if let Ok(uid) = uid_str.parse::() { target_uid = Some(uid); } } if target_uid.is_none() { if let Ok(uid_str) = std::env::var("PKEXEC_UID") { if let Ok(uid) = uid_str.parse::() { target_uid = Some(uid); } } } if target_uid.is_none() { if let Ok(content) = std::fs::read_to_string("/proc/self/loginuid") { if let Ok(uid) = content.trim().parse::() { if uid != 4294967295 { target_uid = Some(uid); } } } } if let Some(uid) = target_uid { unsafe { let pw = libc::getpwuid(uid); if !pw.is_null() { let gid = (*pw).pw_gid; return Some((uid, gid)); } } } None } /// Run an external command as the original user if we're root. /// Uses native OS setuid/setgid to avoid slow PAM/logging/sudo startup overhead. fn run_as_user(program: &str, args: &[&str]) -> std::process::Output { let mut cmd = Self::user_cmd(program); cmd.args(args); match cmd.output() { Ok(output) => output, Err(e) => { eprintln!("[vietc] Failed to run {}: {}", program, e); std::process::Output { status: std::process::ExitStatus::default(), stdout: vec![], stderr: format!("{}\n", e).into_bytes(), } } } } /// Send backspaces and text through a single injection channel to avoid /// 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 { // If all ASCII, send keycodes directly if text.chars().all(|c| char_to_linux_keycode(c).is_some() || c == '\n') { if backspaces > 0 { for _ in 0..backspaces { let _ = self.send_backspace(); } } for ch in text.chars() { if ch == '\n' { self.send_enter(); } else { let _ = self.send_char(ch); } } return InjectResult::Success; } // Unicode: clipboard paste. Backspaces FIRST, then paste. if backspaces > 0 { for _ in 0..backspaces { let _ = self.send_backspace(); } } self.paste_via_clipboard(text, true); InjectResult::Success } /// Read the user's current clipboard contents (wl-paste on Wayland, xclip /// on X11). Returns None if no clipboard tool is available or it is empty. fn read_clipboard() -> Option { let is_wayland = std::env::var("WAYLAND_DISPLAY").is_ok(); let (prog, args): (&str, &[&str]) = if is_wayland { ("wl-paste", &["-n"]) } else { ("xclip", &["-selection", "clipboard", "-o"]) }; let mut cmd = Self::user_cmd(prog); cmd.args(args); let output = cmd.output().ok()?; if !output.status.success() { return None; } Some(String::from_utf8_lossy(&output.stdout).into_owned()) } /// Inject Unicode `text` by placing it on the clipboard and sending Ctrl+V, /// while preserving the user's own clipboard contents. Without this, every /// Vietnamese word the user types would overwrite whatever they had copied /// with Ctrl+C, so a subsequent Ctrl+V would paste the wrong thing. /// /// Returns whether the text was successfully copied to the clipboard. fn paste_via_clipboard(&self, text: &str, use_x11_paste: bool) -> bool { // Critical section: snapshot the clipboard, decide what to preserve, // cancel any pending restore so the restorer cannot fire while we // paste, and put our word on the clipboard. The read and write happen // under the lock so they can never interleave with the restorer. { let mut st = self.clip.inner.lock().unwrap(); let current = Self::read_clipboard(); let is_our_write = matches!((¤t, &st.last_injected), (Some(c), Some(l)) if c == l); if !is_our_write { st.saved_clipboard = current; } st.restore_due = None; if !Self::copy_to_clipboard(text) { return false; } st.last_injected = Some(text.to_string()); } // Give the selection owner a moment to take ownership before pasting. std::thread::sleep(std::time::Duration::from_millis(5)); if use_x11_paste { self.send_ctrl_v_x11(); } else { self.send_ctrl_v(); } // Schedule a debounced restore. While the user keeps typing this gets // pushed back, so the user's clipboard is only restored once typing // settles — never overwriting our freshly pasted word mid-stream. { let mut st = self.clip.inner.lock().unwrap(); st.restore_due = Some(Instant::now() + RESTORE_DEBOUNCE); } self.clip.cv.notify_all(); true } /// Copy text to clipboard and paste via Ctrl+V through our uinput device. /// Only used as a last resort if Wayland/X11 direct typing tools are unavailable. /// Tries xdotool first (X11/XWayland), then clipboard fallback. fn paste_string(&self, s: &str) { let is_wayland = std::env::var("WAYLAND_DISPLAY").is_ok(); if is_wayland { eprintln!("[vietc] paste_string: trying wtype..."); let output = Self::run_as_user("wtype", &["--", s]); if output.status.success() { eprintln!("[vietc] paste_string: wtype success"); return; } eprintln!("[vietc] paste_string: wtype failed, trying clipboard..."); } else { // Try xdotool first (works on X11 and XWayland for UTF-8) eprintln!("[vietc] paste_string: trying xdotool..."); let output = Self::run_as_user("xdotool", &["type", s]); if output.status.success() { eprintln!("[vietc] paste_string: xdotool success"); // Record pasted text for future delete/backspace operations let _ = Self::run_as_user("vietc", &["update-pasted", "-text", s]); return; } eprintln!("[vietc] paste_string: xdotool failed, trying clipboard..."); } // Clipboard fallback: copy + paste via our uinput device let copied = Self::copy_to_clipboard(s); if copied { eprintln!("[vietc] paste_string: clipboard OK, sending Ctrl+V"); self.send_ctrl_v(); return; } eprintln!( "[vietc] WARNING: No injection method works for '{}'!", s.escape_default() ); } /// Build a command to run as the original user with display environment. fn user_cmd(program: &str) -> std::process::Command { let is_root = unsafe { libc::getuid() == 0 }; if is_root { if let Some((uid, gid)) = Self::get_original_uid_gid() { 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 display = std::env::var("DISPLAY").unwrap_or_default(); let xauthority = std::env::var("XAUTHORITY").unwrap_or_default(); use std::os::unix::process::CommandExt; let mut cmd = std::process::Command::new(program); cmd.uid(uid).gid(gid); if !wayland_display.is_empty() { cmd.env("WAYLAND_DISPLAY", wayland_display); } if !xdg_runtime_dir.is_empty() { cmd.env("XDG_RUNTIME_DIR", xdg_runtime_dir); } if !display.is_empty() { cmd.env("DISPLAY", display); } if !xauthority.is_empty() { cmd.env("XAUTHORITY", xauthority); } if let Some(username) = Self::get_original_username() { cmd.env("HOME", format!("/home/{}", username)); } return cmd; } } std::process::Command::new(program) } /// Copy text to clipboard using wl-copy (Wayland) or xclip (X11). fn copy_to_clipboard(s: &str) -> bool { // Try wl-copy (Wayland) via user_cmd { let mut cmd = Self::user_cmd("wl-copy"); let result = cmd .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() }); if let Ok(status) = result { if status.success() { return true; } } } // Try xclip (X11) via user_cmd { let mut cmd = Self::user_cmd("xclip"); cmd.args(["-selection", "clipboard"]); let result = cmd .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 result { return true; } } false } /// Send Ctrl+V through our uinput device. fn send_ctrl_v(&self) { self.send_uinput_event(EV_KEY, 29, 1); // KEY_LEFTCTRL press self.send_uinput_event(0, 0, 0); // SYN std::thread::sleep(std::time::Duration::from_millis(5)); self.send_uinput_event(EV_KEY, 47, 1); // KEY_V press self.send_uinput_event(0, 0, 0); // SYN std::thread::sleep(std::time::Duration::from_millis(5)); self.send_uinput_event(EV_KEY, 47, 0); // KEY_V release self.send_uinput_event(0, 0, 0); // SYN std::thread::sleep(std::time::Duration::from_millis(5)); self.send_uinput_event(EV_KEY, 29, 0); // KEY_LEFTCTRL release self.send_uinput_event(0, 0, 0); // SYN std::thread::sleep(std::time::Duration::from_millis(10)); } /// Send Ctrl+V via X11 XTest (avoids uinput kernel feedback loop). /// Uses a lazily-opened persistent X11 connection. fn send_ctrl_v_x11(&self) { if std::env::var("WAYLAND_DISPLAY").is_ok() { self.send_ctrl_v(); return; } // Persistent X11 state (raw pointers, only used from injection thread) static mut X11_DPY: *mut libc::c_void = std::ptr::null_mut(); static mut X11_KEY: Option libc::c_int> = None; static mut X11_FLUSH: Option libc::c_int> = None; static mut X11_KEYCODE: Option u32> = None; static X11_INIT: std::sync::Once = std::sync::Once::new(); X11_INIT.call_once(|| { unsafe { let lib = libc::dlopen(b"libX11.so.6\0".as_ptr() as *const libc::c_char, 1); if lib.is_null() { return; } let xtst = libc::dlopen(b"libXtst.so.6\0".as_ptr() as *const libc::c_char, 1); if xtst.is_null() { libc::dlclose(lib); return; } type FnOpen = unsafe extern "C" fn(*const libc::c_char) -> *mut libc::c_void; let xopen: FnOpen = std::mem::transmute(libc::dlsym(lib, b"XOpenDisplay\0".as_ptr() as *const libc::c_char)); let dpy = xopen(std::ptr::null()); if dpy.is_null() { libc::dlclose(xtst); libc::dlclose(lib); return; } X11_DPY = dpy; X11_KEY = Some(std::mem::transmute(libc::dlsym(xtst, b"XTestFakeKeyEvent\0".as_ptr() as *const libc::c_char))); X11_FLUSH = Some(std::mem::transmute(libc::dlsym(lib, b"XFlush\0".as_ptr() as *const libc::c_char))); X11_KEYCODE = Some(std::mem::transmute(libc::dlsym(lib, b"XKeysymToKeycode\0".as_ptr() as *const libc::c_char))); } }); unsafe { if X11_DPY.is_null() || X11_KEY.is_none() { self.send_ctrl_v(); return; } let dpy = X11_DPY; let xkey = X11_KEY.unwrap(); let xflush = X11_FLUSH.unwrap(); let xkeycode = X11_KEYCODE.unwrap(); let ctrl_kc = xkeycode(dpy, 0xFFE3); let v_kc = xkeycode(dpy, 0x0076); xkey(dpy, ctrl_kc, 1, 0); xkey(dpy, v_kc, 1, 0); xkey(dpy, v_kc, 0, 0); xkey(dpy, ctrl_kc, 0, 0); xflush(dpy); std::thread::sleep(std::time::Duration::from_millis(10)); } } } impl Drop for UinputInjector { fn drop(&mut self) { { let mut st = self.clip.inner.lock().unwrap(); st.shutdown = true; } self.clip.cv.notify_all(); let _ = ioctl(self.file.as_raw_fd(), UI_DEV_DESTROY, 0); } } /// Background thread: once no Unicode paste has happened for `RESTORE_DEBOUNCE`, /// rewrite the user's real clipboard so Ctrl+V keeps working. fn run_restorer(state: Arc) { loop { let mut st = state.inner.lock().unwrap(); loop { if st.shutdown { return; } match st.restore_due { None => { st = state.cv.wait(st).unwrap(); } Some(due) => { let now = Instant::now(); if now >= due { break; } let (guard, _) = state.cv.wait_timeout(st, due - now).unwrap(); st = guard; } } } // Deadline reached. Restore under the lock so the write cannot // interleave with a concurrent paste's clipboard write. let restored = st.saved_clipboard.clone().unwrap_or_default(); let _ = UinputInjector::copy_to_clipboard(&restored); st.last_injected = Some(restored); st.restore_due = None; } } fn strip_vn_diacritic(ch: char) -> char { match ch { 'à' | 'á' | 'ả' | 'ã' | 'ạ' | 'ă' | 'ằ' | 'ắ' | 'ẳ' | 'ẵ' | 'ặ' | 'â' | 'ầ' | 'ấ' | 'ẩ' | 'ẫ' | 'ậ' => 'a', 'À' | 'Á' | 'Ả' | 'Ã' | 'Ạ' | 'Ă' | 'Ằ' | 'Ắ' | 'Ẳ' | 'Ẵ' | 'Ặ' | 'Â' | 'Ầ' | 'Ấ' | 'Ẩ' | 'Ẫ' | 'Ậ' => 'A', 'è' | 'é' | 'ẻ' | 'ẽ' | 'ẹ' | 'ê' | 'ề' | 'ế' | 'ể' | 'ễ' | 'ệ' => 'e', 'È' | 'É' | 'Ẻ' | 'Ẽ' | 'Ẹ' | 'Ê' | 'Ề' | 'Ế' | 'Ể' | 'Ễ' | 'Ệ' => 'E', 'ì' | 'í' | 'ỉ' | 'ĩ' | 'ị' => 'i', 'Ì' | 'Í' | 'Ỉ' | 'Ĩ' | 'Ị' => 'I', 'ò' | 'ó' | 'ỏ' | 'õ' | 'ọ' | 'ô' | 'ồ' | 'ố' | 'ổ' | 'ỗ' | 'ộ' | 'ơ' | 'ờ' | 'ớ' | 'ở' | 'ỡ' | 'ợ' => 'o', 'Ò' | 'Ó' | 'Ỏ' | 'Õ' | 'Ọ' | 'Ô' | 'Ồ' | 'Ố' | 'Ổ' | 'Ỗ' | 'Ộ' | 'Ơ' | 'Ờ' | 'Ớ' | 'Ở' | 'Ỡ' | 'Ợ' => 'O', 'ù' | 'ú' | 'ủ' | 'ũ' | 'ụ' | 'ư' | 'ừ' | 'ứ' | 'ử' | 'ữ' | 'ự' => 'u', 'Ù' | 'Ú' | 'Ủ' | 'Ũ' | 'Ụ' | 'Ư' | 'Ừ' | 'Ứ' | 'Ử' | 'Ữ' | 'Ự' => 'U', 'ỳ' | 'ý' | 'ỷ' | 'ỹ' | 'ỵ' => 'y', 'Ỳ' | 'Ý' | 'Ỷ' | 'Ỹ' | 'Ỵ' => 'Y', 'đ' => 'd', 'Đ' => 'D', other => other, } } fn char_to_linux_keycode(ch: char) -> Option { match ch.to_ascii_lowercase() { 'a' => Some(30), 'b' => Some(48), 'c' => Some(46), 'd' => Some(32), 'e' => Some(18), 'f' => Some(33), 'g' => Some(34), 'h' => Some(35), 'i' => Some(23), 'j' => Some(36), 'k' => Some(37), 'l' => Some(38), 'm' => Some(50), 'n' => Some(49), 'o' => Some(24), 'p' => Some(25), 'q' => Some(16), 'r' => Some(19), 's' => Some(31), 't' => Some(20), 'u' => Some(22), 'v' => Some(47), 'w' => Some(17), 'x' => Some(45), 'y' => Some(21), 'z' => Some(44), '0' => Some(11), '1' => Some(2), '2' => Some(3), '3' => Some(4), '4' => Some(5), '5' => Some(6), '6' => Some(7), '7' => Some(8), '8' => Some(9), '9' => Some(10), ' ' => Some(57), '.' => Some(52), ',' => Some(51), '-' => Some(12), '=' => Some(13), ';' => Some(39), '\'' => Some(40), '/' => Some(53), '\\' => Some(43), _ => None, } } // ioctl helper fn ioctl( fd: std::os::unix::io::RawFd, request: u64, arg: u64, ) -> Result> { unsafe { let result = libc::ioctl(fd, request, arg); if result < 0 { Err(format!("ioctl failed: {}", std::io::Error::last_os_error()).into()) } else { Ok(result) } } } #[repr(C)] struct input_event { time: timeval, type_: u16, code: u16, value: i32, } #[repr(C)] #[derive(Clone, Copy)] struct timeval { tv_sec: libc::time_t, tv_usec: libc::suseconds_t, } #[repr(C)] struct uinput_setup { id: input_id, name: [i8; UINPUT_MAX_NAME_SIZE], ff_effects_max: u32, } #[repr(C)] #[derive(Clone, Copy)] struct input_id { bustype: u16, vendor: u16, product: u16, version: u16, }