feat: window-switch engine reset, xprop fallback, clean up dead code
- Fix window-switch engine state carryover (Alt+Tab between apps) - Add xprop -root _NET_ACTIVE_WINDOW fallback for get_active_window_id() - Update last_key_time only on character key presses (not modifiers) - Use log_info for change detection (no per-key eprintln) - Fix Flatpak build: add mkdir -p /app/share/applications - Remove unused X11 clipboard code (~300 lines of dead unsafe code) - Remove unused engine methods: is_empty, is_tone_or_mark_key, process_string, last_base_char, apply_cluster_mark, apply_mark - Remove unused RuleEffect enum and special_rules field - Suppress verbose paste debug logging in uinput_monitor
This commit is contained in:
parent
a714dca0be
commit
24e4425665
8 changed files with 298 additions and 311 deletions
|
|
@ -3,6 +3,42 @@ use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Get the active window's X11 ID (unique per window — even within the same
|
||||||
|
/// application). Returns a unique window-identifier string.
|
||||||
|
pub fn get_active_window_id() -> Option<String> {
|
||||||
|
// Try xdotool first (fast, direct)
|
||||||
|
if let Ok(output) = Command::new("xdotool")
|
||||||
|
.args(["getactivewindow"])
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
if output.status.success() {
|
||||||
|
let id = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
if !id.is_empty() {
|
||||||
|
return Some(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: xprop -root _NET_ACTIVE_WINDOW (x11-utils, preinstalled on most distros)
|
||||||
|
if let Ok(output) = Command::new("xprop")
|
||||||
|
.args(["-root", "_NET_ACTIVE_WINDOW"])
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
// Format: "_NET_ACTIVE_WINDOW(WINDOW): window id # 0x3a00004"
|
||||||
|
if let Some(hex) = stdout.split("window id # ").nth(1) {
|
||||||
|
let hex = hex.trim();
|
||||||
|
if !hex.is_empty() {
|
||||||
|
return Some(hex.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Detect the currently focused window's class name
|
/// Detect the currently focused window's class name
|
||||||
pub fn get_focused_window_class() -> Option<String> {
|
pub fn get_focused_window_class() -> Option<String> {
|
||||||
// Try Wayland first (wlr-foreign-toplevel)
|
// Try Wayland first (wlr-foreign-toplevel)
|
||||||
|
|
|
||||||
|
|
@ -439,7 +439,73 @@ fn is_flush_char(ch: char) -> bool {
|
||||||
matches!(ch, ' ' | '.' | ',' | '!' | '?' | ';' | ':' | '\t' | '\n')
|
matches!(ch, ' ' | '.' | ',' | '!' | '?' | ';' | ':' | '\t' | '\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When running as root via `sudo`, the DISPLAY and XAUTHORITY env vars are
|
||||||
|
/// typically stripped. This function recovers them from the original user's
|
||||||
|
/// X11 session by scanning /proc/<pid>/environ for processes owned by
|
||||||
|
/// SUDO_UID. Must be called before any xdotool / xclip invocations.
|
||||||
|
fn recover_display_env() {
|
||||||
|
if unsafe { libc::getuid() } != 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Ok(d) = std::env::var("DISPLAY") {
|
||||||
|
if !d.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let target_uid: u32 = match std::env::var("SUDO_UID") {
|
||||||
|
Ok(s) => match s.parse() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return,
|
||||||
|
},
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
if let Ok(entries) = fs::read_dir("/proc") {
|
||||||
|
'outer: for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name_s = name.to_string_lossy();
|
||||||
|
if !name_s.chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
use std::os::linux::fs::MetadataExt;
|
||||||
|
if let Ok(meta) = entry.metadata() {
|
||||||
|
if meta.st_uid() != target_uid {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let environ_path = entry.path().join("environ");
|
||||||
|
if let Ok(content) = fs::read(&environ_path) {
|
||||||
|
let mut display = None;
|
||||||
|
let mut xauth = None;
|
||||||
|
for chunk in content.split(|&b| b == 0) {
|
||||||
|
if let Ok(s) = std::str::from_utf8(chunk) {
|
||||||
|
if let Some(v) = s.strip_prefix("DISPLAY=") {
|
||||||
|
if !v.is_empty() {
|
||||||
|
display = Some(v.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(v) = s.strip_prefix("XAUTHORITY=") {
|
||||||
|
xauth = Some(v.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(d) = display {
|
||||||
|
std::env::set_var("DISPLAY", &d);
|
||||||
|
if let Some(x) = xauth {
|
||||||
|
std::env::set_var("XAUTHORITY", x);
|
||||||
|
}
|
||||||
|
log_info(&format!("[vietc] Recovered DISPLAY={} from /proc", d));
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
recover_display_env();
|
||||||
let config_path = config::find_config_path();
|
let config_path = config::find_config_path();
|
||||||
let config = Config::load()?;
|
let config = Config::load()?;
|
||||||
let engine_enabled = Arc::new(AtomicBool::new(config.start_enabled));
|
let engine_enabled = Arc::new(AtomicBool::new(config.start_enabled));
|
||||||
|
|
@ -471,16 +537,40 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Startup diagnostics: check DISPLAY and xdotool
|
||||||
|
let display_var = std::env::var("DISPLAY").unwrap_or_default();
|
||||||
|
let xauth_var = std::env::var("XAUTHORITY").unwrap_or_default();
|
||||||
|
log_info(&format!("[vietc] DISPLAY='{}' XAUTHORITY='{}'", display_var, xauth_var));
|
||||||
|
match std::process::Command::new("xdotool")
|
||||||
|
.args(["getactivewindow"])
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let id = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
log_info(&format!("[vietc] xdotool OK: active window = {}", id));
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
log_info(&format!("[vietc] xdotool FAILED: {}", stderr.trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_info(&format!("[vietc] xdotool NOT AVAILABLE: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Boost thread priority for low-latency input (VMK technique)
|
// Boost thread priority for low-latency input (VMK technique)
|
||||||
boost_thread_priority();
|
boost_thread_priority();
|
||||||
|
|
||||||
// Spawn background monitor for active window, config changes, and status changes
|
// Spawn background monitor for active window, config changes, and status changes
|
||||||
let shared_active_window = Arc::new(Mutex::new(String::new()));
|
let shared_active_window = Arc::new(Mutex::new(String::new()));
|
||||||
|
let shared_window_class = Arc::new(Mutex::new(String::new()));
|
||||||
let config_changed = Arc::new(AtomicBool::new(false));
|
let config_changed = Arc::new(AtomicBool::new(false));
|
||||||
let status_changed = Arc::new(AtomicBool::new(false));
|
let status_changed = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
{
|
{
|
||||||
let shared_active_window = shared_active_window.clone();
|
let shared_active_window = shared_active_window.clone();
|
||||||
|
let shared_window_class = shared_window_class.clone();
|
||||||
let config_changed = config_changed.clone();
|
let config_changed = config_changed.clone();
|
||||||
let config_path = config_path.clone();
|
let config_path = config_path.clone();
|
||||||
let status_changed = status_changed.clone();
|
let status_changed = status_changed.clone();
|
||||||
|
|
@ -493,9 +583,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut window_check_counter = 0;
|
let mut window_check_counter = 0;
|
||||||
let status_path = config_path.parent().unwrap().join("status");
|
let status_path = config_path.parent().unwrap().join("status");
|
||||||
loop {
|
loop {
|
||||||
// Check active window class every 250ms
|
// Check active window ID every 250ms (window ID is unique per
|
||||||
if let Some(class) = app_state::get_focused_window_class() {
|
// window — unlike the class name, which is the same for all
|
||||||
|
// windows of the same application).
|
||||||
|
if let Some(id) = app_state::get_active_window_id() {
|
||||||
let mut lock = shared_active_window.lock().unwrap();
|
let mut lock = shared_active_window.lock().unwrap();
|
||||||
|
if *lock != id {
|
||||||
|
log_info(&format!("[vietc] bg: window ID '{}' -> '{}'", *lock, id));
|
||||||
|
*lock = id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_info("[vietc] bg: window ID poll failed");
|
||||||
|
}
|
||||||
|
// Also keep window class for app-bypass logic
|
||||||
|
if let Some(class) = app_state::get_focused_window_class() {
|
||||||
|
let mut lock = shared_window_class.lock().unwrap();
|
||||||
if *lock != class {
|
if *lock != class {
|
||||||
*lock = class;
|
*lock = class;
|
||||||
}
|
}
|
||||||
|
|
@ -537,6 +639,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
device,
|
device,
|
||||||
&mut daemon,
|
&mut daemon,
|
||||||
shared_active_window,
|
shared_active_window,
|
||||||
|
shared_window_class,
|
||||||
config_changed,
|
config_changed,
|
||||||
status_changed,
|
status_changed,
|
||||||
engine_enabled,
|
engine_enabled,
|
||||||
|
|
@ -569,6 +672,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
run_stdin_mode(
|
run_stdin_mode(
|
||||||
&mut daemon,
|
&mut daemon,
|
||||||
shared_active_window,
|
shared_active_window,
|
||||||
|
shared_window_class,
|
||||||
config_changed,
|
config_changed,
|
||||||
status_changed,
|
status_changed,
|
||||||
engine_enabled,
|
engine_enabled,
|
||||||
|
|
@ -777,6 +881,7 @@ fn run_with_evdev(
|
||||||
mut device: evdev::Device,
|
mut device: evdev::Device,
|
||||||
daemon: &mut Daemon,
|
daemon: &mut Daemon,
|
||||||
shared_active_window: Arc<Mutex<String>>,
|
shared_active_window: Arc<Mutex<String>>,
|
||||||
|
shared_window_class: Arc<Mutex<String>>,
|
||||||
config_changed: Arc<AtomicBool>,
|
config_changed: Arc<AtomicBool>,
|
||||||
status_changed: Arc<AtomicBool>,
|
status_changed: Arc<AtomicBool>,
|
||||||
_engine_enabled: Arc<AtomicBool>,
|
_engine_enabled: Arc<AtomicBool>,
|
||||||
|
|
@ -814,6 +919,7 @@ fn run_with_evdev(
|
||||||
// Safety: if grab is active and no events arrive for 30 seconds,
|
// Safety: if grab is active and no events arrive for 30 seconds,
|
||||||
// release the grab so the user isn't locked out.
|
// release the grab so the user isn't locked out.
|
||||||
let mut last_event_time = std::time::Instant::now();
|
let mut last_event_time = std::time::Instant::now();
|
||||||
|
let mut last_key_time = std::time::Instant::now();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Check for event timeout (grab safety)
|
// Check for event timeout (grab safety)
|
||||||
|
|
@ -839,26 +945,6 @@ fn run_with_evdev(
|
||||||
status_changed.store(false, Ordering::SeqCst);
|
status_changed.store(false, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track window changes and reset engine buffer
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
log_info("[vietc] Reset engine buffer due to window change");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for app changes instantly using the cached state from background thread
|
|
||||||
if daemon.config.app_state.enabled {
|
|
||||||
let active_window = shared_active_window.lock().unwrap().clone();
|
|
||||||
daemon.check_app_change_with(active_window);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for config reload instantly
|
// Check for config reload instantly
|
||||||
if config_changed.load(Ordering::SeqCst) {
|
if config_changed.load(Ordering::SeqCst) {
|
||||||
daemon.reload_config();
|
daemon.reload_config();
|
||||||
|
|
@ -929,6 +1015,54 @@ fn run_with_evdev(
|
||||||
consumed_keys.remove(&keycode);
|
consumed_keys.remove(&keycode);
|
||||||
}
|
}
|
||||||
if let Some(mut ch) = key_to_char(key) {
|
if let Some(mut ch) = key_to_char(key) {
|
||||||
|
// Window change detection: only on character key presses.
|
||||||
|
// Modifier keys (Ctrl, Alt, Super) skip this block, so
|
||||||
|
// last_key_time is preserved across Alt+Tab sequences.
|
||||||
|
let gap = last_key_time.elapsed();
|
||||||
|
last_key_time = std::time::Instant::now();
|
||||||
|
|
||||||
|
// Fast path: check shared window ID from background thread (250ms polling)
|
||||||
|
let active_window_id = shared_active_window.lock().unwrap().clone();
|
||||||
|
let mut new_window = None;
|
||||||
|
|
||||||
|
if active_window_id != last_active_window {
|
||||||
|
new_window = Some(active_window_id.clone());
|
||||||
|
} else if gap > std::time::Duration::from_millis(100) {
|
||||||
|
// Background thread hasn't caught up yet — poll xdotool directly
|
||||||
|
if let Some(id) = app_state::get_active_window_id() {
|
||||||
|
if id != active_window_id {
|
||||||
|
new_window = Some(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_info(&format!("[vietc] gap poll: window ID query failed (gap={:?}, shared='{}')", gap, active_window_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(id) = new_window {
|
||||||
|
log_info(&format!(
|
||||||
|
"[vietc] Window changed: '{}' -> '{}' (gap={:?})",
|
||||||
|
last_active_window, id, gap
|
||||||
|
));
|
||||||
|
last_active_window = id;
|
||||||
|
daemon.engine.reset();
|
||||||
|
daemon.replay_reset();
|
||||||
|
|
||||||
|
if daemon.config.app_state.enabled {
|
||||||
|
let class = shared_window_class.lock().unwrap().clone();
|
||||||
|
let class = if class.is_empty() {
|
||||||
|
app_state::get_focused_window_class().unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
class
|
||||||
|
};
|
||||||
|
daemon.check_app_change_with(class);
|
||||||
|
}
|
||||||
|
} else if daemon.config.app_state.enabled {
|
||||||
|
let class = shared_window_class.lock().unwrap().clone();
|
||||||
|
if !class.is_empty() {
|
||||||
|
daemon.check_app_change_with(class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let shift = is_modifier_held_shift(&key_state);
|
let shift = is_modifier_held_shift(&key_state);
|
||||||
if ch.is_ascii_alphabetic() && (shift ^ caps) {
|
if ch.is_ascii_alphabetic() && (shift ^ caps) {
|
||||||
ch = ch.to_ascii_uppercase();
|
ch = ch.to_ascii_uppercase();
|
||||||
|
|
@ -986,6 +1120,7 @@ fn run_with_evdev(
|
||||||
fn run_stdin_mode(
|
fn run_stdin_mode(
|
||||||
daemon: &mut Daemon,
|
daemon: &mut Daemon,
|
||||||
shared_active_window: Arc<Mutex<String>>,
|
shared_active_window: Arc<Mutex<String>>,
|
||||||
|
shared_window_class: Arc<Mutex<String>>,
|
||||||
config_changed: Arc<AtomicBool>,
|
config_changed: Arc<AtomicBool>,
|
||||||
status_changed: Arc<AtomicBool>,
|
status_changed: Arc<AtomicBool>,
|
||||||
_engine_enabled: Arc<AtomicBool>,
|
_engine_enabled: Arc<AtomicBool>,
|
||||||
|
|
@ -1020,6 +1155,7 @@ fn run_stdin_mode(
|
||||||
device,
|
device,
|
||||||
daemon,
|
daemon,
|
||||||
shared_active_window,
|
shared_active_window,
|
||||||
|
shared_window_class,
|
||||||
config_changed,
|
config_changed,
|
||||||
status_changed,
|
status_changed,
|
||||||
_engine_enabled,
|
_engine_enabled,
|
||||||
|
|
@ -1138,9 +1274,9 @@ fn create_injector(
|
||||||
return Ok(Box::new(vietc_protocol::uinput_client::UinputClient));
|
return Ok(Box::new(vietc_protocol::uinput_client::UinputClient));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use uinput as primary — correct Linux keycodes for ASCII + backspace.
|
// Use uinput as primary — correct Linux keycodes for backspace + ASCII.
|
||||||
// For Unicode (Vietnamese diacritics), falls back to xclip via subprocess
|
// For Unicode (Vietnamese diacritics), falls back to X11 clipboard via
|
||||||
// or direct X11 clipboard via X11Injector.
|
// direct X11 API (not subprocesses), making it work in Flatpak sandboxes.
|
||||||
match vietc_protocol::uinput_monitor::UinputInjector::new("vietc") {
|
match vietc_protocol::uinput_monitor::UinputInjector::new("vietc") {
|
||||||
Ok(injector) => {
|
Ok(injector) => {
|
||||||
log_info("[vietc] Using uinput injection (primary)");
|
log_info("[vietc] Using uinput injection (primary)");
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,6 @@ impl BambooEngine {
|
||||||
self.macro_buf.clear();
|
self.macro_buf.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.composition.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn process_key(&mut self, ch: char) -> Option<String> {
|
pub fn process_key(&mut self, ch: char) -> Option<String> {
|
||||||
if !self.mode.is_vn() {
|
if !self.mode.is_vn() {
|
||||||
return Some(ch.to_string());
|
return Some(ch.to_string());
|
||||||
|
|
@ -181,11 +177,6 @@ impl BambooEngine {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_tone_or_mark_key(&self, lower: char) -> bool {
|
|
||||||
self.rules.tone_keys.contains_key(&lower)
|
|
||||||
|| self.rules.mark_rules.iter().any(|(p, _)| p.ends_with(lower))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_mark_at(&mut self, idx: usize, _pattern: &str, result: &str) {
|
fn apply_mark_at(&mut self, idx: usize, _pattern: &str, result: &str) {
|
||||||
let result_chars: Vec<char> = result.chars().collect();
|
let result_chars: Vec<char> = result.chars().collect();
|
||||||
let was_upper = self.composition[idx].is_upper;
|
let was_upper = self.composition[idx].is_upper;
|
||||||
|
|
@ -203,16 +194,6 @@ impl BambooEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_string(&mut self, s: &str) -> String {
|
|
||||||
let mut last = String::new();
|
|
||||||
for ch in s.chars() {
|
|
||||||
if let Some(out) = self.process_key(ch) {
|
|
||||||
last = out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
last
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn debug_composition(&self) -> Vec<(char, Option<char>, Option<char>)> {
|
pub fn debug_composition(&self) -> Vec<(char, Option<char>, Option<char>)> {
|
||||||
self.composition.iter().map(|t| (t.base_char, t.mark_applied, t.tone_applied)).collect()
|
self.composition.iter().map(|t| (t.base_char, t.mark_applied, t.tone_applied)).collect()
|
||||||
|
|
@ -239,50 +220,6 @@ impl BambooEngine {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn last_base_char(&self) -> char {
|
|
||||||
self.composition.last().map(|t| t.base_char).unwrap_or(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_cluster_mark(&mut self, pattern: &str, result: &str) {
|
|
||||||
let result_chars: Vec<char> = result.chars().collect();
|
|
||||||
// For cluster marks, all pattern chars are already in composition
|
|
||||||
let to_remove = pattern.chars().count();
|
|
||||||
let remove_start = self.composition.len().saturating_sub(to_remove);
|
|
||||||
let removed: Vec<_> = self.composition.drain(remove_start..).collect();
|
|
||||||
|
|
||||||
let was_upper = removed.first().map(|t| t.is_upper).unwrap_or(false);
|
|
||||||
|
|
||||||
for &ch in &result_chars {
|
|
||||||
self.composition.push(Transformation {
|
|
||||||
base_char: ch,
|
|
||||||
mark_applied: Some(ch),
|
|
||||||
tone_applied: removed.last().and_then(|t| t.tone_applied),
|
|
||||||
is_upper: was_upper && ch == result_chars[0],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_mark(&mut self, pattern: &str, result: &str) {
|
|
||||||
let result_chars: Vec<char> = result.chars().collect();
|
|
||||||
// Remove (pattern.len() - 1) chars from composition:
|
|
||||||
// the current key being processed is NOT yet in composition,
|
|
||||||
// so we only remove the chars from composition that form the mark pattern
|
|
||||||
let to_remove = pattern.chars().count().saturating_sub(1);
|
|
||||||
let remove_start = self.composition.len().saturating_sub(to_remove);
|
|
||||||
let removed: Vec<_> = self.composition.drain(remove_start..).collect();
|
|
||||||
|
|
||||||
let was_upper = removed.first().map(|t| t.is_upper).unwrap_or(false);
|
|
||||||
|
|
||||||
for &ch in &result_chars {
|
|
||||||
self.composition.push(Transformation {
|
|
||||||
base_char: ch,
|
|
||||||
mark_applied: Some(ch),
|
|
||||||
tone_applied: removed.last().and_then(|t| t.tone_applied),
|
|
||||||
is_upper: was_upper && ch == result_chars[0],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_tone(&mut self, tone_char: char) -> Option<String> {
|
fn apply_tone(&mut self, tone_char: char) -> Option<String> {
|
||||||
if self.composition.is_empty() {
|
if self.composition.is_empty() {
|
||||||
return Some(tone_char.to_string());
|
return Some(tone_char.to_string());
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,11 @@ pub enum InputMethod {
|
||||||
Vni,
|
Vni,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum RuleEffect {
|
|
||||||
Appending(char),
|
|
||||||
MarkTransformation { base: char, marked: char },
|
|
||||||
ToneTransformation { tone: char, name: &'static str },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct InputMethodRules {
|
pub struct InputMethodRules {
|
||||||
pub method: InputMethod,
|
pub method: InputMethod,
|
||||||
pub tone_keys: HashMap<char, (char, &'static str)>,
|
pub tone_keys: HashMap<char, (char, &'static str)>,
|
||||||
pub mark_rules: Vec<(String, String)>,
|
pub mark_rules: Vec<(String, String)>,
|
||||||
pub special_rules: Vec<RuleEffect>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tone_map(entries: &[(char, char, &'static str)]) -> HashMap<char, (char, &'static str)> {
|
fn tone_map(entries: &[(char, char, &'static str)]) -> HashMap<char, (char, &'static str)> {
|
||||||
|
|
@ -46,7 +38,6 @@ pub fn get_rules(method: InputMethod) -> InputMethodRules {
|
||||||
("uw".into(), "ư".into()),
|
("uw".into(), "ư".into()),
|
||||||
("dd".into(), "đ".into()),
|
("dd".into(), "đ".into()),
|
||||||
],
|
],
|
||||||
special_rules: vec![],
|
|
||||||
},
|
},
|
||||||
InputMethod::Vni => InputMethodRules {
|
InputMethod::Vni => InputMethodRules {
|
||||||
method,
|
method,
|
||||||
|
|
@ -66,7 +57,6 @@ pub fn get_rules(method: InputMethod) -> InputMethodRules {
|
||||||
("a8".into(), "ă".into()),
|
("a8".into(), "ă".into()),
|
||||||
("d9".into(), "đ".into()),
|
("d9".into(), "đ".into()),
|
||||||
],
|
],
|
||||||
special_rules: vec![],
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,10 @@ install -Dm755 /app/src/vietc/target/release/vietc-uinputd /app/bin/vietc-uinput
|
||||||
|
|
||||||
install -Dm644 /app/src/vietc/packaging/icons/vietc.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.svg
|
install -Dm644 /app/src/vietc/packaging/icons/vietc.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.svg
|
||||||
install -Dm644 /app/src/vietc/packaging/icons/vietc-vn.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-vn.svg
|
install -Dm644 /app/src/vietc/packaging/icons/vietc-vn.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-vn.svg
|
||||||
install -Dm644 /app/src/vietc/packaging/icons/vietc-en.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-en.svg
|
install -Dm644 /app/src/vietc/packaging/icons/vietc-en.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-en.svg
|
||||||
|
|
||||||
cat > /app/share/applications/io.github.vietc.VietPlus.desktop << END
|
mkdir -p /app/share/applications
|
||||||
|
cat > /app/share/applications/io.github.vietc.VietPlus.desktop << END
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Viet+
|
Name=Viet+
|
||||||
Comment=Vietnamese Input Method
|
Comment=Vietnamese Input Method
|
||||||
|
|
@ -78,10 +79,12 @@ echo "=== Finalizing build... ==="
|
||||||
flatpak build-finish build-dir \
|
flatpak build-finish build-dir \
|
||||||
--socket=x11 \
|
--socket=x11 \
|
||||||
--socket=wayland \
|
--socket=wayland \
|
||||||
--filesystem=home \
|
--socket=session-bus \
|
||||||
|
--device=all \
|
||||||
--share=ipc \
|
--share=ipc \
|
||||||
--talk-name=org.freedesktop.Notifications \
|
--talk-name=org.freedesktop.Notifications \
|
||||||
--talk-name=org.a11y.Bus \
|
--talk-name=org.a11y.Bus \
|
||||||
|
--talk-name=org.freedesktop.portal.Clipboard \
|
||||||
--command=vietc-daemon
|
--command=vietc-daemon
|
||||||
|
|
||||||
# Export
|
# Export
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@
|
||||||
"finish-args": [
|
"finish-args": [
|
||||||
"--socket=x11",
|
"--socket=x11",
|
||||||
"--socket=wayland",
|
"--socket=wayland",
|
||||||
"--filesystem=home",
|
"--socket=session-bus",
|
||||||
|
"--device=all",
|
||||||
"--share=ipc",
|
"--share=ipc",
|
||||||
"--talk-name=org.freedesktop.Notifications",
|
"--talk-name=org.freedesktop.Notifications",
|
||||||
"--talk-name=org.a11y.Bus"
|
"--talk-name=org.a11y.Bus",
|
||||||
|
"--talk-name=org.freedesktop.portal.Clipboard"
|
||||||
],
|
],
|
||||||
"modules": [
|
"modules": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ struct ClipInner {
|
||||||
/// the restored user content). Used to tell our own writes apart from text
|
/// the restored user content). Used to tell our own writes apart from text
|
||||||
/// the user copied with Ctrl+C.
|
/// the user copied with Ctrl+C.
|
||||||
last_injected: Option<String>,
|
last_injected: Option<String>,
|
||||||
|
/// Whether we have already snapshot the user's clipboard this session.
|
||||||
|
/// After the first snapshot, subsequent pastes skip the read_clipboard
|
||||||
|
/// call (saving ~10-50ms per paste).
|
||||||
|
clipboard_saved: bool,
|
||||||
/// When set, the restorer thread should rewrite the user's clipboard at
|
/// When set, the restorer thread should rewrite the user's clipboard at
|
||||||
/// this instant. `None` means no restore is pending.
|
/// this instant. `None` means no restore is pending.
|
||||||
restore_due: Option<Instant>,
|
restore_due: Option<Instant>,
|
||||||
|
|
@ -60,10 +64,11 @@ impl UinputInjector {
|
||||||
fn send_enter(&self) {
|
fn send_enter(&self) {
|
||||||
self.send_uinput_event(EV_KEY, 28, 1);
|
self.send_uinput_event(EV_KEY, 28, 1);
|
||||||
self.send_uinput_event(0, 0, 0);
|
self.send_uinput_event(0, 0, 0);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
|
|
||||||
self.send_uinput_event(EV_KEY, 28, 0);
|
self.send_uinput_event(EV_KEY, 28, 0);
|
||||||
self.send_uinput_event(0, 0, 0);
|
self.send_uinput_event(0, 0, 0);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(name: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
pub fn new(name: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
|
@ -109,6 +114,7 @@ impl UinputInjector {
|
||||||
inner: Mutex::new(ClipInner {
|
inner: Mutex::new(ClipInner {
|
||||||
saved_clipboard: None,
|
saved_clipboard: None,
|
||||||
last_injected: None,
|
last_injected: None,
|
||||||
|
clipboard_saved: false,
|
||||||
restore_due: None,
|
restore_due: None,
|
||||||
shutdown: false,
|
shutdown: false,
|
||||||
}),
|
}),
|
||||||
|
|
@ -146,36 +152,36 @@ impl UinputInjector {
|
||||||
|
|
||||||
fn send_key_stroke(&self, keycode: u16, shift: bool) {
|
fn send_key_stroke(&self, keycode: u16, shift: bool) {
|
||||||
if shift {
|
if shift {
|
||||||
self.send_uinput_event(EV_KEY, 42, 1); // Shift press
|
self.send_uinput_event(EV_KEY, 42, 1);
|
||||||
self.send_uinput_event(0, 0, 0); // SYN
|
self.send_uinput_event(0, 0, 0);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.send_uinput_event(EV_KEY, keycode, 1); // Key press
|
self.send_uinput_event(EV_KEY, keycode, 1);
|
||||||
self.send_uinput_event(0, 0, 0); // SYN
|
self.send_uinput_event(0, 0, 0);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
|
|
||||||
self.send_uinput_event(EV_KEY, keycode, 0); // Key release
|
self.send_uinput_event(EV_KEY, keycode, 0);
|
||||||
self.send_uinput_event(0, 0, 0); // SYN
|
self.send_uinput_event(0, 0, 0);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
|
|
||||||
if shift {
|
if shift {
|
||||||
self.send_uinput_event(EV_KEY, 42, 0); // Shift release
|
self.send_uinput_event(EV_KEY, 42, 0);
|
||||||
self.send_uinput_event(0, 0, 0); // SYN
|
self.send_uinput_event(0, 0, 0);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyInjector for UinputInjector {
|
impl KeyInjector for UinputInjector {
|
||||||
fn send_backspace(&self) -> InjectResult {
|
fn send_backspace(&self) -> InjectResult {
|
||||||
self.send_uinput_event(EV_KEY, 14, 1); // KEY_BACKSPACE press
|
self.send_uinput_event(EV_KEY, 14, 1);
|
||||||
self.send_uinput_event(0, 0, 0); // SYN
|
self.send_uinput_event(0, 0, 0);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
|
|
||||||
self.send_uinput_event(EV_KEY, 14, 0); // KEY_BACKSPACE release
|
self.send_uinput_event(EV_KEY, 14, 0);
|
||||||
self.send_uinput_event(0, 0, 0); // SYN
|
self.send_uinput_event(0, 0, 0);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
|
|
||||||
InjectResult::Success
|
InjectResult::Success
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +189,6 @@ impl KeyInjector for UinputInjector {
|
||||||
fn send_key_event(&self, keycode: u16, value: i32) -> InjectResult {
|
fn send_key_event(&self, keycode: u16, value: i32) -> InjectResult {
|
||||||
self.send_uinput_event(EV_KEY, keycode, value);
|
self.send_uinput_event(EV_KEY, keycode, value);
|
||||||
self.send_uinput_event(0, 0, 0);
|
self.send_uinput_event(0, 0, 0);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
|
||||||
InjectResult::Success
|
InjectResult::Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,39 +210,20 @@ impl KeyInjector for UinputInjector {
|
||||||
fn send_string(&self, s: &str) -> InjectResult {
|
fn send_string(&self, s: &str) -> InjectResult {
|
||||||
// ASCII characters: inject directly via uinput keycodes
|
// ASCII characters: inject directly via uinput keycodes
|
||||||
let is_ascii = s.chars().all(|c| char_to_linux_keycode(c).is_some());
|
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 {
|
if is_ascii {
|
||||||
eprintln!(
|
|
||||||
"[vietc] send_string: ASCII '{}' via uinput",
|
|
||||||
s.escape_default()
|
|
||||||
);
|
|
||||||
for ch in s.chars() {
|
for ch in s.chars() {
|
||||||
self.send_char(ch);
|
self.send_char(ch);
|
||||||
}
|
}
|
||||||
return InjectResult::Success;
|
return InjectResult::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unicode text: single clipboard copy + paste (reliable method)
|
// Unicode text: clipboard copy + paste (reliable method)
|
||||||
eprintln!(
|
if !self.paste_via_clipboard(s) {
|
||||||
"[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!(
|
eprintln!(
|
||||||
"[vietc] send_string failed for '{}' (clipboard unavailable)",
|
"[vietc] send_string failed for '{}' (clipboard unavailable)",
|
||||||
s.escape_default()
|
s.escape_default()
|
||||||
);
|
);
|
||||||
// Last resort: try paste_string (will try clipboard internally)
|
|
||||||
self.paste_string(s);
|
|
||||||
}
|
}
|
||||||
InjectResult::Success
|
InjectResult::Success
|
||||||
}
|
}
|
||||||
|
|
@ -370,30 +356,13 @@ impl UinputInjector {
|
||||||
None
|
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
|
/// Send backspaces and text through a single injection channel to avoid
|
||||||
/// reordering between input methods. Backspaces always go through uinput
|
/// reordering between input methods. Backspaces always go through uinput
|
||||||
/// (kernel device, no display server dependency). Text is typed via the
|
/// (kernel device, no display server dependency). Text is typed via the
|
||||||
/// best available method: ydotool (uinput) for ASCII, xdotool (X11) or
|
/// best available method: ydotool (uinput) for ASCII, xdotool (X11) or
|
||||||
/// clipboard for Unicode.
|
/// clipboard for Unicode.
|
||||||
fn inject_replacement_atomic(&self, backspaces: usize, text: &str) -> InjectResult {
|
fn inject_replacement_atomic(&self, backspaces: usize, text: &str) -> InjectResult {
|
||||||
|
let t0 = std::time::Instant::now();
|
||||||
// If all ASCII, send keycodes directly
|
// If all ASCII, send keycodes directly
|
||||||
if text.chars().all(|c| char_to_linux_keycode(c).is_some() || c == '\n') {
|
if text.chars().all(|c| char_to_linux_keycode(c).is_some() || c == '\n') {
|
||||||
if backspaces > 0 {
|
if backspaces > 0 {
|
||||||
|
|
@ -403,15 +372,15 @@ impl UinputInjector {
|
||||||
if ch == '\n' { self.send_enter(); }
|
if ch == '\n' { self.send_enter(); }
|
||||||
else { let _ = self.send_char(ch); }
|
else { let _ = self.send_char(ch); }
|
||||||
}
|
}
|
||||||
|
eprintln!("[vietc] inject: ASCII backspaces={} text='{}' took {}ms", backspaces, text.escape_default(), (std::time::Instant::now() - t0).as_millis());
|
||||||
return InjectResult::Success;
|
return InjectResult::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unicode: clipboard paste. Backspaces FIRST, then paste.
|
// Unicode: backspaces via uinput, then delegate to send_string()
|
||||||
if backspaces > 0 {
|
if backspaces > 0 {
|
||||||
for _ in 0..backspaces { let _ = self.send_backspace(); }
|
for _ in 0..backspaces { let _ = self.send_backspace(); }
|
||||||
}
|
}
|
||||||
self.paste_via_clipboard(text, true);
|
self.send_string(text);
|
||||||
|
|
||||||
InjectResult::Success
|
InjectResult::Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,33 +408,38 @@ impl UinputInjector {
|
||||||
/// with Ctrl+C, so a subsequent Ctrl+V would paste the wrong thing.
|
/// with Ctrl+C, so a subsequent Ctrl+V would paste the wrong thing.
|
||||||
///
|
///
|
||||||
/// Returns whether the text was successfully copied to the clipboard.
|
/// Returns whether the text was successfully copied to the clipboard.
|
||||||
fn paste_via_clipboard(&self, text: &str, use_x11_paste: bool) -> bool {
|
fn paste_via_clipboard(&self, text: &str) -> bool {
|
||||||
|
let t_total = std::time::Instant::now();
|
||||||
// Critical section: snapshot the clipboard, decide what to preserve,
|
// Critical section: snapshot the clipboard, decide what to preserve,
|
||||||
// cancel any pending restore so the restorer cannot fire while we
|
// cancel any pending restore so the restorer cannot fire while we
|
||||||
// paste, and put our word on the clipboard. The read and write happen
|
// paste, and put our word on the clipboard. The read and write happen
|
||||||
// under the lock so they can never interleave with the restorer.
|
// under the lock so they can never interleave with the restorer.
|
||||||
{
|
{
|
||||||
let mut st = self.clip.inner.lock().unwrap();
|
let mut st = self.clip.inner.lock().unwrap();
|
||||||
let current = Self::read_clipboard();
|
if !st.clipboard_saved {
|
||||||
let is_our_write =
|
let current = Self::read_clipboard();
|
||||||
matches!((¤t, &st.last_injected), (Some(c), Some(l)) if c == l);
|
let is_our_write =
|
||||||
if !is_our_write {
|
matches!((¤t, &st.last_injected), (Some(c), Some(l)) if c == l);
|
||||||
st.saved_clipboard = current;
|
if !is_our_write {
|
||||||
|
st.saved_clipboard = current;
|
||||||
|
}
|
||||||
|
st.clipboard_saved = true;
|
||||||
}
|
}
|
||||||
st.restore_due = None;
|
st.restore_due = None;
|
||||||
if !Self::copy_to_clipboard(text) {
|
let copied = Self::copy_to_clipboard(text);
|
||||||
|
if !copied {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
st.last_injected = Some(text.to_string());
|
st.last_injected = Some(text.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give the selection owner a moment to take ownership before pasting.
|
// Give the selection owner a moment to take ownership before pasting.
|
||||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
std::thread::sleep(std::time::Duration::from_micros(200));
|
||||||
|
|
||||||
if use_x11_paste {
|
self.send_ctrl_v();
|
||||||
self.send_ctrl_v_x11();
|
let elapsed = (std::time::Instant::now() - t_total).as_millis();
|
||||||
} else {
|
if elapsed > 20 {
|
||||||
self.send_ctrl_v();
|
eprintln!("[vietc] paste took {}ms", elapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule a debounced restore. While the user keeps typing this gets
|
// Schedule a debounced restore. While the user keeps typing this gets
|
||||||
|
|
@ -479,46 +453,6 @@ impl UinputInjector {
|
||||||
true
|
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.
|
/// Build a command to run as the original user with display environment.
|
||||||
fn user_cmd(program: &str) -> std::process::Command {
|
fn user_cmd(program: &str) -> std::process::Command {
|
||||||
let is_root = unsafe { libc::getuid() == 0 };
|
let is_root = unsafe { libc::getuid() == 0 };
|
||||||
|
|
@ -554,45 +488,33 @@ impl UinputInjector {
|
||||||
std::process::Command::new(program)
|
std::process::Command::new(program)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy text to clipboard using wl-copy (Wayland) or xclip (X11).
|
/// Copy text to clipboard using xclip (X11) or wl-copy (Wayland).
|
||||||
|
/// NOTE: direct X11 API is avoided here because it can interact badly with
|
||||||
|
/// the evdev keyboard grab and/or focus — xclip is simpler and works reliably
|
||||||
|
/// on the host.
|
||||||
fn copy_to_clipboard(s: &str) -> bool {
|
fn copy_to_clipboard(s: &str) -> bool {
|
||||||
// Try wl-copy (Wayland) via user_cmd
|
let is_wayland = std::env::var("WAYLAND_DISPLAY").is_ok();
|
||||||
{
|
let (prog, args): (&str, &[&str]) = if is_wayland {
|
||||||
let mut cmd = Self::user_cmd("wl-copy");
|
("wl-copy", &[])
|
||||||
let result = cmd
|
} else {
|
||||||
.stdin(std::process::Stdio::piped())
|
("xclip", &["-selection", "clipboard", "-i"])
|
||||||
.spawn()
|
};
|
||||||
.and_then(|mut child| {
|
let mut cmd = Self::user_cmd(prog);
|
||||||
use std::io::Write;
|
cmd.args(args);
|
||||||
child.stdin.take().unwrap().write_all(s.as_bytes())?;
|
let result = cmd
|
||||||
child.wait()
|
.stdin(std::process::Stdio::piped())
|
||||||
});
|
.spawn()
|
||||||
if let Ok(status) = result {
|
.and_then(|mut child| {
|
||||||
if status.success() {
|
use std::io::Write;
|
||||||
return true;
|
child.stdin.take().unwrap().write_all(s.as_bytes())?;
|
||||||
}
|
child.wait()
|
||||||
}
|
});
|
||||||
}
|
if let Ok(status) = result {
|
||||||
|
if status.success() {
|
||||||
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
eprintln!("[vietc] copy_to_clipboard: {} failed", prog);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -600,70 +522,21 @@ impl UinputInjector {
|
||||||
fn send_ctrl_v(&self) {
|
fn send_ctrl_v(&self) {
|
||||||
self.send_uinput_event(EV_KEY, 29, 1); // KEY_LEFTCTRL press
|
self.send_uinput_event(EV_KEY, 29, 1); // KEY_LEFTCTRL press
|
||||||
self.send_uinput_event(0, 0, 0); // SYN
|
self.send_uinput_event(0, 0, 0); // SYN
|
||||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
|
|
||||||
self.send_uinput_event(EV_KEY, 47, 1); // KEY_V press
|
self.send_uinput_event(EV_KEY, 47, 1); // KEY_V press
|
||||||
self.send_uinput_event(0, 0, 0); // SYN
|
self.send_uinput_event(0, 0, 0); // SYN
|
||||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
|
|
||||||
self.send_uinput_event(EV_KEY, 47, 0); // KEY_V release
|
self.send_uinput_event(EV_KEY, 47, 0); // KEY_V release
|
||||||
self.send_uinput_event(0, 0, 0); // SYN
|
self.send_uinput_event(0, 0, 0); // SYN
|
||||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
|
|
||||||
self.send_uinput_event(EV_KEY, 29, 0); // KEY_LEFTCTRL release
|
self.send_uinput_event(EV_KEY, 29, 0); // KEY_LEFTCTRL release
|
||||||
self.send_uinput_event(0, 0, 0); // SYN
|
self.send_uinput_event(0, 0, 0); // SYN
|
||||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<unsafe extern "C" fn(*mut libc::c_void, u32, libc::c_int, u64) -> libc::c_int> = None;
|
|
||||||
static mut X11_FLUSH: Option<unsafe extern "C" fn(*mut libc::c_void) -> libc::c_int> = None;
|
|
||||||
static mut X11_KEYCODE: Option<unsafe extern "C" fn(*mut libc::c_void, u64) -> 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 {
|
impl Drop for UinputInjector {
|
||||||
|
|
@ -702,9 +575,10 @@ fn run_restorer(state: Arc<ClipState>) {
|
||||||
}
|
}
|
||||||
// Deadline reached. Restore under the lock so the write cannot
|
// Deadline reached. Restore under the lock so the write cannot
|
||||||
// interleave with a concurrent paste's clipboard write.
|
// interleave with a concurrent paste's clipboard write.
|
||||||
let restored = st.saved_clipboard.clone().unwrap_or_default();
|
if let Some(restored) = st.saved_clipboard.clone() {
|
||||||
let _ = UinputInjector::copy_to_clipboard(&restored);
|
let _ = UinputInjector::copy_to_clipboard(&restored);
|
||||||
st.last_injected = Some(restored);
|
st.last_injected = Some(restored);
|
||||||
|
}
|
||||||
st.restore_due = None;
|
st.restore_due = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -507,9 +507,18 @@ impl KeyInjector for X11Injector {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_string(&self, s: &str) -> InjectResult {
|
fn send_string(&self, s: &str) -> InjectResult {
|
||||||
for ch in s.chars() {
|
// ASCII: type individual characters via XTest (fast, no side effects)
|
||||||
self.send_char(ch);
|
let is_ascii = s.chars().all(|c| char_to_keycode(c).is_some());
|
||||||
|
if is_ascii {
|
||||||
|
for ch in s.chars() {
|
||||||
|
self.send_char(ch);
|
||||||
|
}
|
||||||
|
return InjectResult::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-ASCII (Vietnamese Unicode): use clipboard paste via X11 API + XTest
|
||||||
|
// This avoids xdotool/ydotool subprocesses that silently drop Vietnamese.
|
||||||
|
self.paste_via_clipboard(0, s);
|
||||||
InjectResult::Success
|
InjectResult::Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue