#[cfg(all(not(alloc_frugal), unix_desktop, feature = "gtk-native"))]
fn main() {
staged()
}
#[cfg(not(all(not(alloc_frugal), unix_desktop, feature = "gtk-native")))]
fn main() {
println!(
"probe_keyboard_focus: this build has no X11 session to ask (`--features \
desktop,gtk-native` on a Linux host with DISPLAY set stages it)."
);
}
#[cfg(all(not(alloc_frugal), unix_desktop, feature = "gtk-native"))]
fn gui_probe_allowed() -> bool {
if std::env::var("RW_GUI_PROBE_OK").ok().as_deref() == Some("1") {
return true;
}
println!(
"this probe opens a real window and injects pointer/key events on the desktop it runs on; \
set RW_GUI_PROBE_OK=1 to stage it (and prefer a machine whose screen nobody is using)"
);
false
}
#[cfg(all(not(alloc_frugal), unix_desktop, feature = "gtk-native"))]
fn staged() {
use rust_widgets::app::WidgetHandle;
use std::process::Command;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
if !gui_probe_allowed() {
std::process::exit(2);
}
let title = format!("rw keyboard focus probe {} (throwaway)", std::process::id());
let typed = std::env::var("RW_PROBE_TEXT").unwrap_or_else(|_| "abc".to_string());
let expected = std::env::var("RW_PROBE_EXPECT").unwrap_or_else(|_| typed.clone());
const FIELD: (i32, i32, u32, u32) = (32, 40, 300, 36);
const SETTLE: Duration = Duration::from_millis(600);
let mut failures = 0usize;
let mut check = |held: bool, what: &str| {
if held {
println!(" OK {what}");
} else {
println!(" FAIL {what}");
failures += 1;
}
};
println!("=== keyboard focus probe ===");
println!("backend : {}", rust_widgets::platform::backend_name());
println!("runtime gui mode : {:?}", rust_widgets::platform::runtime_gui_mode());
println!("DISPLAY : {}", std::env::var("DISPLAY").unwrap_or_default());
for tool in ["python3", "wmctrl", "xwininfo", "xprop"] {
if Command::new(tool)
.arg("--help")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_err()
&& !which(tool)
{
println!("unsupported host: `{tool}` is not installed, so input cannot be staged");
std::process::exit(2);
}
}
rust_widgets::init();
let _probe_lock = ProbeLock::acquire();
let window = rust_widgets::create_window(&title, 140, 140, 520, 260);
check(window != 0, "the library created a window");
let handle = rust_widgets::app::WindowHandle::from_raw(window);
let field = handle.new_line_edit("", FIELD.0, FIELD.1, FIELD.2, FIELD.3);
let button = handle.new_button("unclicked", 32, 140, 160, 36);
let values: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let seen = Arc::clone(&values);
field.on_value_changed(move |text| {
if let Ok(mut values) = seen.lock() {
values.push(text.to_string());
}
});
let preedits: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let seen_preedit = Arc::clone(&preedits);
let preedit_hooked = field.on_ime_preedit(move |text, cursor| {
if let Ok(mut preedits) = seen_preedit.lock() {
preedits.push(format!("{text}@{cursor}"));
}
});
let ime_run = std::env::var_os("RW_PROBE_IME").is_some();
let clicks = Arc::new(AtomicUsize::new(0));
let clicked = Arc::clone(&clicks);
button.on_click(move || {
clicked.fetch_add(1, Ordering::SeqCst);
});
rust_widgets::show_widget(window);
let (report_tx, report_rx) = std::sync::mpsc::channel::<Result<String, String>>();
let watcher_title = title.clone();
let watcher = std::thread::spawn(move || {
std::thread::sleep(SETTLE);
let helper =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tools/x11_inject_keys.py");
let click_x = FIELD.0 + FIELD.2 as i32 / 2;
let click_y = FIELD.1 + FIELD.3 as i32 / 2;
let ime_activate = std::env::var_os("RW_PROBE_IME").is_some();
let output = Command::new("python3")
.arg(&helper)
.arg(&watcher_title)
.arg(&typed)
.args([
"--activate",
"--plain",
"--click-relative",
&click_x.to_string(),
&click_y.to_string(),
])
.args(if ime_activate { vec!["--ime-activate"] } else { Vec::new() })
.output();
let result = match output {
Ok(output) if output.status.success() => {
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
Ok(output) => Err(format!(
"injector exit {:?}: {}{}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)),
Err(error) => Err(format!("could not run the injector: {error}")),
};
std::thread::sleep(Duration::from_millis(400));
let _ = report_tx.send(result);
let _ = rust_widgets::quit();
});
rust_widgets::run();
let injection =
report_rx.recv_timeout(Duration::from_secs(30)).expect("the watcher must report");
let _ = watcher.join();
match &injection {
Ok(line) => println!(" injector : {}", line.trim()),
Err(error) => println!(" injector : {error}"),
}
let text = rust_widgets::get_widget_text(field.raw_id());
let values = values.lock().map(|values| values.clone()).unwrap_or_default();
let point =
rust_widgets::core::Point::new(FIELD.0 + FIELD.2 as i32 / 2, FIELD.1 + FIELD.3 as i32 / 2);
println!(" field id : {}", field.raw_id());
println!(
" field geometry : {:?}",
rust_widgets::widget::runtime::geometry_of(field.raw_id())
);
println!(
" focusable : {}",
rust_widgets::widget::runtime::widget_wants_keyboard_focus(field.raw_id())
);
println!(
" accepts IME : {}",
rust_widgets::widget::runtime::accepts_ime_input(field.raw_id())
);
println!(" focused widget : {:?}", rust_widgets::widget::runtime::focused_widget());
println!(
" widget under click : {:?}",
rust_widgets::widget::runtime::widget_at(window, point)
);
println!(" field text : {text:?}");
println!(" value notifications : {values:?}");
let preedits = preedits.lock().map(|preedits| preedits.clone()).unwrap_or_default();
println!(" preedit observer : {preedit_hooked}");
println!(" preedit notifications: {preedits:?}");
println!(" button clicks : {}", clicks.load(Ordering::SeqCst));
check(injection.is_ok(), "real X key events were injected into the window");
check(text == expected, "the clicked field received the typed characters exactly once");
check(
values.last().map(String::as_str) == Some(expected.as_str()),
"`on_value_changed` reported the final value without the host polling",
);
check(!values.is_empty(), "an edit produces at least one value notification");
check(preedit_hooked, "the field accepted a pre-edit observer");
if ime_run {
check(
preedits
.iter()
.any(|preedit| preedit.split('@').next().is_some_and(|text| !text.is_empty())),
"an IME composition was observable before the commit",
);
}
check(
clicks.load(Ordering::SeqCst) == 0,
"the button was not clicked, so focus moved to the field the pointer hit",
);
println!(
"\n{}",
if failures == 0 {
"probe_keyboard_focus: OK (every check held)".to_string()
} else {
format!("probe_keyboard_focus: {failures} check(s) failed")
}
);
std::process::exit(if failures == 0 { 0 } else { 1 });
}
#[cfg(all(not(alloc_frugal), unix_desktop, feature = "gtk-native"))]
struct ProbeLock {
path: std::path::PathBuf,
}
#[cfg(all(not(alloc_frugal), unix_desktop, feature = "gtk-native"))]
impl ProbeLock {
fn acquire() -> Self {
use std::io::Write;
use std::time::{Duration, Instant};
let display = std::env::var("DISPLAY").unwrap_or_else(|_| "default".to_string());
let safe: String = display
.chars()
.map(|character| if character.is_ascii_alphanumeric() { character } else { '_' })
.collect();
let path = std::env::temp_dir().join(format!("rw-gui-probe-{safe}.lock"));
let deadline = Instant::now() + Duration::from_secs(60);
loop {
match std::fs::OpenOptions::new().write(true).create_new(true).open(&path) {
Ok(mut file) => {
let _ = writeln!(file, "{}", std::process::id());
return Self { path };
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
let owner = std::fs::read_to_string(&path)
.ok()
.and_then(|contents| contents.trim().parse::<u32>().ok());
if let Some(pid) = owner {
if !std::path::Path::new(&format!("/proc/{pid}")).exists() {
let _ = std::fs::remove_file(&path);
continue;
}
}
if Instant::now() >= deadline {
panic!(
"another GUI probe on {display} still holds {}; run probes serially",
path.display()
);
}
println!("waiting for another GUI probe on {display} to finish...");
std::thread::sleep(Duration::from_millis(200));
}
Err(error) => panic!("could not create {}: {error}", path.display()),
}
}
}
}
#[cfg(all(not(alloc_frugal), unix_desktop, feature = "gtk-native"))]
impl Drop for ProbeLock {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
#[cfg(all(not(alloc_frugal), unix_desktop, feature = "gtk-native"))]
fn which(tool: &str) -> bool {
std::env::var_os("PATH")
.map(|path| std::env::split_paths(&path).any(|directory| directory.join(tool).is_file()))
.unwrap_or(false)
}