feat: implement Backspace-Replay pattern for perfect engine sync
- Add Engine::replay_keystrokes() — creates fresh engine and replays all keystrokes to compute correct screen output from scratch - Add Daemon::replay_and_inject() — tracks keystroke history and screen output, computes diff (backspaces + new text) on each keypress - Add Daemon::replay_backspace() — pops from history, replays, diffs - Handle flush chars (space, period, etc.) separately — commit word, type char, clear history - Add FocusIn/FocusOut detection for engine reset on focus loss - Add CPU pinning (P-cores 0-3) + nice(-10) priority boost - Clean up 9 dead code warnings (unused fields, constants, types) - Add replay_keystrokes tests for Telex, VNI, and backspace - 255 tests pass (was 252)
This commit is contained in:
parent
bb0847a38f
commit
3858aa955c
4 changed files with 338 additions and 39 deletions
|
|
@ -8,6 +8,31 @@ use std::time::Duration;
|
||||||
|
|
||||||
use vietc_engine::{Engine, EngineEvent, InputMethod};
|
use vietc_engine::{Engine, EngineEvent, InputMethod};
|
||||||
|
|
||||||
|
/// Pin current thread to performance cores (0-3) and boost priority.
|
||||||
|
/// Inspired by VMK's approach to minimize input latency on Intel hybrid CPUs.
|
||||||
|
fn boost_thread_priority() {
|
||||||
|
unsafe {
|
||||||
|
// Set nice value to -10 (higher priority than normal)
|
||||||
|
libc::setpriority(libc::PRIO_PROCESS, 0, -10);
|
||||||
|
|
||||||
|
// Try to pin to P-cores (cores 0-3 on Intel hybrid)
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let mut cpuset: libc::cpu_set_t = std::mem::zeroed();
|
||||||
|
// Pin to cores 0-3 (P-cores on Intel 12th gen+)
|
||||||
|
for i in 0..4 {
|
||||||
|
libc::CPU_SET(i, &mut cpuset);
|
||||||
|
}
|
||||||
|
let ret = libc::sched_setaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &cpuset);
|
||||||
|
if ret == 0 {
|
||||||
|
eprintln!("[vietc] Pinned to P-cores 0-3, nice=-10");
|
||||||
|
} else {
|
||||||
|
eprintln!("[vietc] CPU pinning failed ({}), nice=-10 still set", ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod app_state;
|
mod app_state;
|
||||||
mod config;
|
mod config;
|
||||||
mod display;
|
mod display;
|
||||||
|
|
@ -84,6 +109,13 @@ struct Daemon {
|
||||||
app_state: AppStateManager,
|
app_state: AppStateManager,
|
||||||
engine_enabled: Arc<AtomicBool>,
|
engine_enabled: Arc<AtomicBool>,
|
||||||
grab_enabled: bool,
|
grab_enabled: bool,
|
||||||
|
/// Backspace-Replay: all keystrokes in the current word being composed.
|
||||||
|
/// On each keypress, we replay the entire history through a fresh engine
|
||||||
|
/// to compute the correct screen output, eliminating state desync.
|
||||||
|
keystroke_history: Vec<char>,
|
||||||
|
/// What's currently displayed on screen for the current word.
|
||||||
|
/// Used to calculate how many backspaces we need before retyping.
|
||||||
|
screen_output: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Daemon {
|
impl Daemon {
|
||||||
|
|
@ -120,6 +152,8 @@ impl Daemon {
|
||||||
config_modified,
|
config_modified,
|
||||||
app_state,
|
app_state,
|
||||||
engine_enabled,
|
engine_enabled,
|
||||||
|
keystroke_history: Vec::new(),
|
||||||
|
screen_output: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -307,6 +341,130 @@ impl Daemon {
|
||||||
self.app_state.is_current_app_bypassed()
|
self.app_state.is_current_app_bypassed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Backspace-Replay: replay the entire keystroke history through a fresh
|
||||||
|
/// engine, compute what should be on screen, and return the commands
|
||||||
|
/// (backspaces to erase old + new text to type).
|
||||||
|
fn replay_and_inject(&mut self, ch: char) -> Vec<OutputCommand> {
|
||||||
|
let mut commands = Vec::new();
|
||||||
|
|
||||||
|
// Flush characters: commit current word, type the character, clear state
|
||||||
|
if is_flush_char(ch) {
|
||||||
|
if !self.screen_output.is_empty() {
|
||||||
|
let backspaces = self.screen_output.chars().count();
|
||||||
|
commands.push(OutputCommand::Backspace(backspaces));
|
||||||
|
commands.push(OutputCommand::Type(self.screen_output.clone()));
|
||||||
|
}
|
||||||
|
// Type the flush character itself
|
||||||
|
commands.push(OutputCommand::Type(ch.to_string()));
|
||||||
|
self.keystroke_history.clear();
|
||||||
|
self.screen_output.clear();
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new keystroke to history
|
||||||
|
self.keystroke_history.push(ch);
|
||||||
|
|
||||||
|
// Replay through fresh engine
|
||||||
|
let method = match self.config.input_method.as_str() {
|
||||||
|
"vni" => InputMethod::Vni,
|
||||||
|
_ => InputMethod::Telex,
|
||||||
|
};
|
||||||
|
let (new_output, did_flush) = Engine::replay_keystrokes(
|
||||||
|
method,
|
||||||
|
&self.config.macros,
|
||||||
|
&self.keystroke_history,
|
||||||
|
);
|
||||||
|
|
||||||
|
log_info(&format!(
|
||||||
|
"[vietc] replay: history_len={} old_screen='{}' new_output='{}' flush={}",
|
||||||
|
self.keystroke_history.len(),
|
||||||
|
self.screen_output,
|
||||||
|
new_output,
|
||||||
|
did_flush
|
||||||
|
));
|
||||||
|
|
||||||
|
if did_flush {
|
||||||
|
// Engine flushed a word — commit it and clear state
|
||||||
|
// The flush char (space/period/etc) was NOT in history, so we need to
|
||||||
|
// type whatever was on screen + the flush char
|
||||||
|
if !self.screen_output.is_empty() {
|
||||||
|
let backspaces = self.screen_output.chars().count();
|
||||||
|
commands.push(OutputCommand::Backspace(backspaces));
|
||||||
|
commands.push(OutputCommand::Type(self.screen_output.clone()));
|
||||||
|
}
|
||||||
|
self.keystroke_history.clear();
|
||||||
|
self.screen_output.clear();
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
if new_output != self.screen_output {
|
||||||
|
let backspaces = self.screen_output.chars().count();
|
||||||
|
if backspaces > 0 {
|
||||||
|
commands.push(OutputCommand::Backspace(backspaces));
|
||||||
|
}
|
||||||
|
if !new_output.is_empty() {
|
||||||
|
commands.push(OutputCommand::Type(new_output.clone()));
|
||||||
|
}
|
||||||
|
self.screen_output = new_output;
|
||||||
|
}
|
||||||
|
|
||||||
|
commands
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Backspace-Replay: pop from history, replay, and return commands to fix screen.
|
||||||
|
fn replay_backspace(&mut self) -> Vec<OutputCommand> {
|
||||||
|
let mut commands = Vec::new();
|
||||||
|
|
||||||
|
if self.keystroke_history.is_empty() {
|
||||||
|
// Nothing in history — just forward the backspace
|
||||||
|
commands.push(OutputCommand::Backspace(1));
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove last keystroke from history
|
||||||
|
self.keystroke_history.pop();
|
||||||
|
|
||||||
|
// Replay through fresh engine
|
||||||
|
let method = match self.config.input_method.as_str() {
|
||||||
|
"vni" => InputMethod::Vni,
|
||||||
|
_ => InputMethod::Telex,
|
||||||
|
};
|
||||||
|
let (new_output, _) = if self.keystroke_history.is_empty() {
|
||||||
|
(String::new(), false)
|
||||||
|
} else {
|
||||||
|
Engine::replay_keystrokes(
|
||||||
|
method,
|
||||||
|
&self.config.macros,
|
||||||
|
&self.keystroke_history,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
log_info(&format!(
|
||||||
|
"[vietc] replay_backspace: history_len={} old_screen='{}' new_output='{}'",
|
||||||
|
self.keystroke_history.len(),
|
||||||
|
self.screen_output,
|
||||||
|
new_output
|
||||||
|
));
|
||||||
|
|
||||||
|
// Calculate diff
|
||||||
|
let backspaces = self.screen_output.chars().count();
|
||||||
|
if backspaces > 0 {
|
||||||
|
commands.push(OutputCommand::Backspace(backspaces));
|
||||||
|
}
|
||||||
|
if !new_output.is_empty() {
|
||||||
|
commands.push(OutputCommand::Type(new_output.clone()));
|
||||||
|
}
|
||||||
|
self.screen_output = new_output;
|
||||||
|
|
||||||
|
commands
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the replay state (on flush, focus loss, modifier key, etc.)
|
||||||
|
fn replay_reset(&mut self) {
|
||||||
|
self.keystroke_history.clear();
|
||||||
|
self.screen_output.clear();
|
||||||
|
}
|
||||||
|
|
||||||
fn check_app_change_with(&mut self, new_class: String) {
|
fn check_app_change_with(&mut self, new_class: String) {
|
||||||
if let Some(should_enable) = self.app_state.update_with_app(new_class) {
|
if let Some(should_enable) = self.app_state.update_with_app(new_class) {
|
||||||
self.engine.set_enabled(should_enable);
|
self.engine.set_enabled(should_enable);
|
||||||
|
|
@ -321,6 +479,11 @@ enum OutputCommand {
|
||||||
Backspace(usize),
|
Backspace(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Characters that flush the current word and start a new one.
|
||||||
|
fn is_flush_char(ch: char) -> bool {
|
||||||
|
matches!(ch, ' ' | '.' | ',' | '!' | '?' | ';' | ':' | '\t' | '\n')
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config_path = config::find_config_path();
|
let config_path = config::find_config_path();
|
||||||
let config = Config::load()?;
|
let config = Config::load()?;
|
||||||
|
|
@ -353,6 +516,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Boost thread priority for low-latency input (VMK technique)
|
||||||
|
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 config_changed = Arc::new(AtomicBool::new(false));
|
let config_changed = Arc::new(AtomicBool::new(false));
|
||||||
|
|
@ -560,7 +726,7 @@ fn run_with_x11(
|
||||||
if active_window != last_active_window {
|
if active_window != last_active_window {
|
||||||
log_info(&format!("[vietc] Window changed: '{}' -> '{}'", last_active_window, active_window));
|
log_info(&format!("[vietc] Window changed: '{}' -> '{}'", last_active_window, active_window));
|
||||||
last_active_window = active_window.clone();
|
last_active_window = active_window.clone();
|
||||||
daemon.engine.reset();
|
daemon.replay_reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -569,9 +735,17 @@ fn run_with_x11(
|
||||||
daemon.check_app_change_with(active_window);
|
daemon.check_app_change_with(active_window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset on focus loss (VMK technique)
|
||||||
|
if capture.focus_lost {
|
||||||
|
eprintln!("[vietc] Focus lost — resetting engine state");
|
||||||
|
daemon.replay_reset();
|
||||||
|
pressed_keys.clear();
|
||||||
|
capture.focus_lost = false;
|
||||||
|
}
|
||||||
|
|
||||||
while let Some(event) = capture.next_event() {
|
while let Some(event) = capture.next_event() {
|
||||||
if event.pressed {
|
if event.pressed {
|
||||||
// Skip autorepeat — key is already tracked as held
|
// Skip autorepeat
|
||||||
if !pressed_keys.insert(event.keycode) {
|
if !pressed_keys.insert(event.keycode) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -580,54 +754,53 @@ fn run_with_x11(
|
||||||
if let Some(' ') = event.ch {
|
if let Some(' ') = event.ch {
|
||||||
if (event.state & 4) != 0 {
|
if (event.state & 4) != 0 {
|
||||||
pressed_keys.remove(&event.keycode);
|
pressed_keys.remove(&event.keycode);
|
||||||
|
daemon.replay_reset();
|
||||||
daemon.toggle();
|
daemon.toggle();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modifier or non-character key → forward press only
|
// Modifier or non-character key → forward press only, reset replay
|
||||||
if capture.is_modifier_pressed(event.state) || event.ch.is_none() {
|
if capture.is_modifier_pressed(event.state) || event.ch.is_none() {
|
||||||
daemon.engine.reset();
|
daemon.replay_reset();
|
||||||
capture.without_grab(|| {
|
capture.without_grab(|| {
|
||||||
let _ = injector.send_key_event(event.keycode as u16, 1);
|
let _ = injector.send_key_event(event.keycode as u16, 1);
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Character key
|
// Character key — use Backspace-Replay
|
||||||
if let Some(ch) = event.ch {
|
if let Some(ch) = event.ch {
|
||||||
match ch {
|
match ch {
|
||||||
'\x08' => {
|
'\x08' => {
|
||||||
daemon.engine.process_key('\x08');
|
// Backspace: replay pattern pops from history
|
||||||
|
let commands = daemon.replay_backspace();
|
||||||
|
pressed_keys.remove(&event.keycode);
|
||||||
|
capture.without_grab(|| {
|
||||||
|
execute_commands(&*injector, &commands, true);
|
||||||
|
});
|
||||||
|
// If history is empty and commands only had a bare backspace,
|
||||||
|
// we need to actually send it
|
||||||
|
if daemon.keystroke_history.is_empty() && commands.is_empty() {
|
||||||
capture.without_grab(|| {
|
capture.without_grab(|| {
|
||||||
let _ = injector.send_backspace();
|
let _ = injector.send_backspace();
|
||||||
});
|
});
|
||||||
// Keep in pressed_keys so release is forwarded
|
}
|
||||||
}
|
}
|
||||||
'\n' => {
|
'\n' => {
|
||||||
pressed_keys.remove(&event.keycode);
|
pressed_keys.remove(&event.keycode);
|
||||||
daemon.engine.reset();
|
daemon.replay_reset();
|
||||||
capture.without_grab(|| {
|
capture.without_grab(|| {
|
||||||
let _ = injector.send_key_event(event.keycode as u16, 1);
|
let _ = injector.send_key_event(event.keycode as u16, 1);
|
||||||
let _ = injector.send_key_event(event.keycode as u16, 0);
|
let _ = injector.send_key_event(event.keycode as u16, 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let commands = daemon.process_key(ch);
|
let commands = daemon.replay_and_inject(ch);
|
||||||
if !commands.is_empty() {
|
|
||||||
// Engine consumed the key; remove from tracking
|
|
||||||
pressed_keys.remove(&event.keycode);
|
pressed_keys.remove(&event.keycode);
|
||||||
capture.without_grab(|| {
|
capture.without_grab(|| {
|
||||||
execute_commands(&*injector, &commands, true);
|
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,72 @@ impl Engine {
|
||||||
event
|
event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replay a sequence of keystrokes through a fresh engine and return the
|
||||||
|
/// final screen output. This is the core of the Backspace-Replay pattern:
|
||||||
|
/// instead of tracking incremental state, we always recompute from scratch.
|
||||||
|
/// Returns (output_on_screen, did_flush).
|
||||||
|
/// `did_flush` means the engine processed a word boundary and the cursor
|
||||||
|
/// is now at a clean position — caller should clear keystroke history.
|
||||||
|
pub fn replay_keystrokes(
|
||||||
|
method: InputMethod,
|
||||||
|
macros: &std::collections::HashMap<String, String>,
|
||||||
|
keystrokes: &[char],
|
||||||
|
) -> (String, bool) {
|
||||||
|
let mut engine = Engine::new(method);
|
||||||
|
for (shortcut, expansion) in macros {
|
||||||
|
engine.add_macro(shortcut.clone(), expansion.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut last_output = String::new();
|
||||||
|
let mut did_flush = false;
|
||||||
|
|
||||||
|
for &ch in keystrokes {
|
||||||
|
if let Some(event) = engine.process_key(ch) {
|
||||||
|
match event {
|
||||||
|
EngineEvent::Replace { insert, .. } => {
|
||||||
|
last_output = insert;
|
||||||
|
}
|
||||||
|
EngineEvent::Flush(_word) => {
|
||||||
|
// Word was flushed. The flush char is NOT part of the word.
|
||||||
|
// The word is committed; clear tracking for current composing.
|
||||||
|
last_output.clear();
|
||||||
|
did_flush = true;
|
||||||
|
}
|
||||||
|
EngineEvent::Insert(text) => {
|
||||||
|
last_output = text;
|
||||||
|
}
|
||||||
|
EngineEvent::UndoTones { restored, .. } => {
|
||||||
|
last_output = restored;
|
||||||
|
}
|
||||||
|
EngineEvent::Paste(text) => {
|
||||||
|
last_output = text;
|
||||||
|
}
|
||||||
|
EngineEvent::AutoRestore(word) => {
|
||||||
|
last_output = word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Key consumed but no screen change — buffer is building
|
||||||
|
let buf = engine.buffer().to_string();
|
||||||
|
if !buf.is_empty() {
|
||||||
|
last_output = buf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the engine has a buffer that hasn't been flushed, that's on screen
|
||||||
|
let buf = engine.buffer().to_string();
|
||||||
|
if !buf.is_empty() {
|
||||||
|
last_output = buf;
|
||||||
|
did_flush = false; // Still composing
|
||||||
|
} else if did_flush {
|
||||||
|
// After flush, nothing is on screen for the composing word
|
||||||
|
last_output.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
(last_output, did_flush)
|
||||||
|
}
|
||||||
|
|
||||||
/// Update buffer with pasted text for subsequent edit operations (delete/backspace)
|
/// Update buffer with pasted text for subsequent edit operations (delete/backspace)
|
||||||
pub fn update_with_pasted_text(&mut self, text: &str) {
|
pub fn update_with_pasted_text(&mut self, text: &str) {
|
||||||
self.raw_buffer.clear();
|
self.raw_buffer.clear();
|
||||||
|
|
@ -497,4 +563,63 @@ mod tests {
|
||||||
panic!("Expected Replace event, got {:?}", event2);
|
panic!("Expected Replace event, got {:?}", event2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_replay_keystrokes_telex() {
|
||||||
|
let macros = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
// Replay "chao" -> should produce "chao" (no tone yet)
|
||||||
|
let (output, flush) = Engine::replay_keystrokes(
|
||||||
|
InputMethod::Telex,
|
||||||
|
¯os,
|
||||||
|
&['c', 'h', 'a', 'o'],
|
||||||
|
);
|
||||||
|
assert_eq!(output, "chao");
|
||||||
|
assert!(!flush);
|
||||||
|
|
||||||
|
// Replay "chaos" -> s adds acute accent: "cháo"
|
||||||
|
let (output, flush) = Engine::replay_keystrokes(
|
||||||
|
InputMethod::Telex,
|
||||||
|
¯os,
|
||||||
|
&['c', 'h', 'a', 'o', 's'],
|
||||||
|
);
|
||||||
|
assert_eq!(output, "cháo");
|
||||||
|
assert!(!flush);
|
||||||
|
|
||||||
|
// Replay "chaof" -> f adds grave accent: "chào"
|
||||||
|
let (output, flush) = Engine::replay_keystrokes(
|
||||||
|
InputMethod::Telex,
|
||||||
|
¯os,
|
||||||
|
&['c', 'h', 'a', 'o', 'f'],
|
||||||
|
);
|
||||||
|
assert_eq!(output, "chào");
|
||||||
|
assert!(!flush);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_replay_keystrokes_backspace() {
|
||||||
|
let macros = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
// Replay "chaos" then backspace -> engine pops 'o' from "cháo" → "chá"
|
||||||
|
let (output, _) = Engine::replay_keystrokes(
|
||||||
|
InputMethod::Telex,
|
||||||
|
¯os,
|
||||||
|
&['c', 'h', 'a', 'o', 's', '\x08'],
|
||||||
|
);
|
||||||
|
assert_eq!(output, "chá");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_replay_keystrokes_vni() {
|
||||||
|
let macros = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
// VNI: "chao1" → acute accent on last vowel
|
||||||
|
let (output, _) = Engine::replay_keystrokes(
|
||||||
|
InputMethod::Vni,
|
||||||
|
¯os,
|
||||||
|
&['c', 'h', 'a', 'o', '1'],
|
||||||
|
);
|
||||||
|
// Verify it produces accented output (engine applies tone to last vowel)
|
||||||
|
assert!(output.contains('á') || output.contains('ó'), "Expected toned output, got: {}", output);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,15 @@ use std::ffi::{c_char, c_int, c_void};
|
||||||
|
|
||||||
type Display = c_void;
|
type Display = c_void;
|
||||||
type Window = u64;
|
type Window = u64;
|
||||||
type XID = u64;
|
|
||||||
type Time = u64;
|
type Time = u64;
|
||||||
|
|
||||||
// X11 event types
|
// X11 event types
|
||||||
const KEY_PRESS: c_int = 2;
|
const KEY_PRESS: c_int = 2;
|
||||||
const KEY_RELEASE: c_int = 3;
|
const KEY_RELEASE: c_int = 3;
|
||||||
|
const FOCUS_IN: c_int = 9;
|
||||||
|
const FOCUS_OUT: c_int = 10;
|
||||||
|
|
||||||
// X11 modifier masks
|
// X11 modifier masks
|
||||||
const SHIFT_MASK: c_int = 1;
|
|
||||||
const CONTROL_MASK: c_int = 4;
|
const CONTROL_MASK: c_int = 4;
|
||||||
const MOD1_MASK: c_int = 8; // Alt
|
const MOD1_MASK: c_int = 8; // Alt
|
||||||
const MOD4_MASK: c_int = 64; // Super/Win
|
const MOD4_MASK: c_int = 64; // Super/Win
|
||||||
|
|
@ -33,7 +33,6 @@ struct X11Lib {
|
||||||
x_ungrab_keyboard: unsafe extern "C" fn(*mut Display, 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_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_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_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,
|
x_flush: unsafe extern "C" fn(*mut Display) -> c_int,
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +68,6 @@ impl X11Lib {
|
||||||
let x_ungrab_keyboard = sym!("XUngrabKeyboard");
|
let x_ungrab_keyboard = sym!("XUngrabKeyboard");
|
||||||
let x_next_event = sym!("XNextEvent");
|
let x_next_event = sym!("XNextEvent");
|
||||||
let x_lookup_string = sym!("XLookupString");
|
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 = dlsym(handle, b"Xutf8LookupString\0".as_ptr() as *const c_char);
|
||||||
let x_utf8_lookup_string = if x_utf8_lookup_string.is_null() {
|
let x_utf8_lookup_string = if x_utf8_lookup_string.is_null() {
|
||||||
None
|
None
|
||||||
|
|
@ -87,7 +85,6 @@ impl X11Lib {
|
||||||
x_ungrab_keyboard,
|
x_ungrab_keyboard,
|
||||||
x_next_event,
|
x_next_event,
|
||||||
x_lookup_string,
|
x_lookup_string,
|
||||||
x_keysym_to_keycode,
|
|
||||||
x_utf8_lookup_string,
|
x_utf8_lookup_string,
|
||||||
x_flush,
|
x_flush,
|
||||||
})
|
})
|
||||||
|
|
@ -149,7 +146,8 @@ pub struct X11Capture {
|
||||||
display: *mut Display,
|
display: *mut Display,
|
||||||
root: Window,
|
root: Window,
|
||||||
grabbed: bool,
|
grabbed: bool,
|
||||||
event_buf: Vec<u8>,
|
/// Set to true when FocusOut is received — caller should reset engine state
|
||||||
|
pub focus_lost: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe impl Send for X11Capture {}
|
unsafe impl Send for X11Capture {}
|
||||||
|
|
@ -178,7 +176,7 @@ impl X11Capture {
|
||||||
display,
|
display,
|
||||||
root,
|
root,
|
||||||
grabbed: false,
|
grabbed: false,
|
||||||
event_buf: Vec::new(),
|
focus_lost: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -225,6 +223,17 @@ impl X11Capture {
|
||||||
}
|
}
|
||||||
|
|
||||||
let _type = event._type;
|
let _type = event._type;
|
||||||
|
|
||||||
|
// Handle FocusIn/FocusOut — reset engine state when focus changes
|
||||||
|
if _type == FOCUS_OUT {
|
||||||
|
self.focus_lost = true;
|
||||||
|
return self.next_event();
|
||||||
|
}
|
||||||
|
if _type == FOCUS_IN {
|
||||||
|
self.focus_lost = false;
|
||||||
|
return self.next_event();
|
||||||
|
}
|
||||||
|
|
||||||
if _type != KEY_PRESS && _type != KEY_RELEASE {
|
if _type != KEY_PRESS && _type != KEY_RELEASE {
|
||||||
return self.next_event();
|
return self.next_event();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ extern "C" {
|
||||||
const CURRENT_TIME: Time = 0;
|
const CURRENT_TIME: Time = 0;
|
||||||
const PROP_MODE_REPLACE: c_int = 0;
|
const PROP_MODE_REPLACE: c_int = 0;
|
||||||
const NO_EVENT_MASK: i64 = 0;
|
const NO_EVENT_MASK: i64 = 0;
|
||||||
const INPUT_OUTPUT: c_int = 1;
|
|
||||||
const COPY_FROM_PARENT: Window = 0;
|
const COPY_FROM_PARENT: Window = 0;
|
||||||
|
|
||||||
const SELECTION_REQUEST: c_int = 30;
|
const SELECTION_REQUEST: c_int = 30;
|
||||||
|
|
@ -34,7 +33,6 @@ struct X11Lib {
|
||||||
x_intern_atom: unsafe extern "C" fn(*mut Display, *const c_char, c_int) -> Atom,
|
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_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_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_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_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_map_window: unsafe extern "C" fn(*mut Display, Window) -> c_int,
|
||||||
|
|
@ -90,7 +88,6 @@ impl X11Lib {
|
||||||
let x_intern_atom = sym!(x11_handle, "XInternAtom");
|
let x_intern_atom = sym!(x11_handle, "XInternAtom");
|
||||||
let x_set_selection_owner = sym!(x11_handle, "XSetSelectionOwner");
|
let x_set_selection_owner = sym!(x11_handle, "XSetSelectionOwner");
|
||||||
let x_change_property = sym!(x11_handle, "XChangeProperty");
|
let x_change_property = sym!(x11_handle, "XChangeProperty");
|
||||||
let x_get_selection_owner = sym!(x11_handle, "XGetSelectionOwner");
|
|
||||||
let x_send_event = sym!(x11_handle, "XSendEvent");
|
let x_send_event = sym!(x11_handle, "XSendEvent");
|
||||||
let x_create_simple_window = sym!(x11_handle, "XCreateSimpleWindow");
|
let x_create_simple_window = sym!(x11_handle, "XCreateSimpleWindow");
|
||||||
let x_map_window = sym!(x11_handle, "XMapWindow");
|
let x_map_window = sym!(x11_handle, "XMapWindow");
|
||||||
|
|
@ -110,7 +107,6 @@ impl X11Lib {
|
||||||
x_intern_atom,
|
x_intern_atom,
|
||||||
x_set_selection_owner,
|
x_set_selection_owner,
|
||||||
x_change_property,
|
x_change_property,
|
||||||
x_get_selection_owner,
|
|
||||||
x_send_event,
|
x_send_event,
|
||||||
x_create_simple_window,
|
x_create_simple_window,
|
||||||
x_map_window,
|
x_map_window,
|
||||||
|
|
@ -131,8 +127,6 @@ impl Drop for X11Lib {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const X11_KEYCODE_OFFSET: u32 = 8;
|
|
||||||
|
|
||||||
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)),
|
||||||
|
|
@ -215,7 +209,6 @@ struct XEvent {
|
||||||
pub struct X11Injector {
|
pub struct X11Injector {
|
||||||
lib: X11Lib,
|
lib: X11Lib,
|
||||||
display: *mut Display,
|
display: *mut Display,
|
||||||
root: Window,
|
|
||||||
clipboard_window: Window,
|
clipboard_window: Window,
|
||||||
atom_clipboard: Atom,
|
atom_clipboard: Atom,
|
||||||
atom_utf8: Atom,
|
atom_utf8: Atom,
|
||||||
|
|
@ -251,7 +244,6 @@ impl X11Injector {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
lib,
|
lib,
|
||||||
display,
|
display,
|
||||||
root,
|
|
||||||
clipboard_window,
|
clipboard_window,
|
||||||
atom_clipboard,
|
atom_clipboard,
|
||||||
atom_utf8,
|
atom_utf8,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue