#[cfg(all(target_os = "linux", 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 real windows 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),
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
fn main() {
staged()
}
#[cfg(not(all(
not(alloc_frugal),
any(target_os = "linux", target_os = "macos", target_os = "windows")
)))]
fn main() {
println!(
"probe_file_picker: this profile has no platform picker (alloc-frugal or a non-desktop \
target), so there is nothing to stage."
);
}
#[cfg(all(
not(alloc_frugal),
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
fn staged() {
#[cfg(all(target_os = "linux", feature = "gtk-native"))]
if !gui_probe_allowed() {
std::process::exit(2);
}
#[cfg(all(target_os = "linux", feature = "gtk-native"))]
use gtk::prelude::*;
use rust_widgets::platform::{PathPickerError, PathPurpose};
let mut failures = 0usize;
let mut check = |held: bool, what: &str| {
if held {
println!(" OK {what}");
} else {
println!(" FAIL {what}");
failures += 1;
}
};
println!("=== file picker probe ===");
println!("backend : {}", rust_widgets::platform::backend_name());
println!("runtime gui mode : {:?}", rust_widgets::platform::runtime_gui_mode());
rust_widgets::init();
let capability = rust_widgets::platform::capabilities().file_dialog;
println!("file_dialog flag : {capability}");
println!("\nA. capability flag versus what the call answers");
if capability {
println!(" the flag is true, so the staged run below is B/C");
} else {
let answer = rust_widgets::choose_path(PathPurpose::OpenFile, "pick a file");
println!(" choose_path answered {answer:?}");
check(
matches!(answer, Err(PathPickerError::Unsupported)),
"a backend that reports `file_dialog: false` answers `Unsupported`, so a host can pick \
its fallback from the flag instead of provoking this failure",
);
for purpose in [PathPurpose::SaveFile, PathPurpose::SelectDirectory] {
let answer = rust_widgets::choose_path(purpose, "pick a path");
check(
matches!(answer, Err(PathPickerError::Unsupported)),
"every purpose answers `Unsupported` on a backend with no picker",
);
}
}
if capability {
println!(
"\nB. the picker opens and answers (dismissed by a timer inside its own modal loop)"
);
#[cfg(all(target_os = "linux", feature = "gtk-native"))]
{
use std::time::{Duration, Instant};
const DISMISS_AFTER_MS: u64 = 400;
let armed = std::rc::Rc::new(std::cell::Cell::new(false));
let armed_for_timer = armed.clone();
gtk::glib::timeout_add_local(
std::time::Duration::from_millis(DISMISS_AFTER_MS),
move || {
let mut dismissed = false;
for widget in gtk::Window::list_toplevels() {
if let Ok(dialog) = widget.downcast::<gtk::Dialog>() {
if dialog.is_modal() {
dialog.response(gtk::ResponseType::Cancel);
dismissed = true;
}
}
}
armed_for_timer.set(dismissed);
gtk::glib::ControlFlow::Break
},
);
let started = Instant::now();
let answer = rust_widgets::choose_path(PathPurpose::OpenFile, "probe: pick a file");
let took = started.elapsed();
println!(" choose_path answered {answer:?} after {took:?}");
if matches!(answer, Err(PathPickerError::Unavailable)) {
println!(
" unsupported host (no display session for GTK), so the dialog cannot be \
staged here"
);
std::process::exit(2);
}
check(
matches!(answer, Ok(None)),
"the picker opened, its modal loop ran, and the dismissal came back as `Ok(None)` \
(a cancellation is an answer, not an error)",
);
check(
took >= Duration::from_millis(DISMISS_AFTER_MS / 2),
"the call really waited on the dialog instead of returning immediately",
);
}
#[cfg(not(all(target_os = "linux", feature = "gtk-native")))]
{
println!(
" skipped: this build has no toolkit to stage a real dialog with \
(`--features desktop,gtk-native` on a host with a display session stages it)"
);
}
#[cfg(all(target_os = "linux", feature = "gtk-native"))]
{
println!("\nB2. the same picker, answered by *typed input* (real X key events)");
const PICKED: &str = "/tmp/rw_picked_by_probe.txt";
const DIALOG_TITLE: &str = "probe: type a name";
const TYPING_BUDGET_MS: u64 = 8000;
let _ = std::fs::remove_file(PICKED);
if std::fs::write(PICKED, "typed-selection\n").is_err() {
println!(" unsupported host: {PICKED} could not be created");
std::process::exit(2);
}
let cancel_armed = std::rc::Rc::new(std::cell::Cell::new(false));
let armed_for_timer = cancel_armed.clone();
gtk::glib::timeout_add_local(
std::time::Duration::from_millis(TYPING_BUDGET_MS),
move || {
let mut cancelled = false;
for widget in gtk::Window::list_toplevels() {
if let Ok(dialog) = widget.downcast::<gtk::Dialog>() {
if dialog.is_modal() {
dialog.response(gtk::ResponseType::Cancel);
cancelled = true;
}
}
}
armed_for_timer.set(cancelled);
gtk::glib::ControlFlow::Break
},
);
let mut last_code = -1;
let mut typed_answer: Option<Result<Option<std::path::PathBuf>, PathPickerError>> =
None;
for attempt in 1..=6u32 {
let typist = std::thread::spawn(|| {
let helper = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tools/x11_inject_keys.py");
for _ in 0..60 {
std::thread::sleep(std::time::Duration::from_millis(100));
let attempt = std::process::Command::new("python3")
.arg(&helper)
.args([DIALOG_TITLE, PICKED, "--activate", "--key", "Return"])
.output();
let Ok(attempt) = attempt else {
return 5;
};
match attempt.status.code().unwrap_or(-1) {
0 => return 0,
3 => continue,
code => return code,
}
}
-1
});
let answer = rust_widgets::choose_path(PathPurpose::OpenFile, DIALOG_TITLE);
let code = typist.join().expect("the typist thread must not panic");
println!(
" attempt {attempt}: choose_path answered {answer:?} (typist exit {code})"
);
last_code = code;
let staged =
matches!(&answer, Ok(Some(path)) if path == std::path::Path::new(PICKED));
typed_answer = Some(answer);
if staged || matches!(code, 2 | 4 | 5) {
break;
}
}
let _ = std::fs::remove_file(PICKED);
if std::fs::write(PICKED, "typed-selection\n").is_err() {
println!(" unsupported host: {PICKED} could not be created");
std::process::exit(2);
}
let _ = cancel_armed.get();
let took = matches!(
&typed_answer,
Some(Ok(Some(path))) if path == std::path::Path::new(PICKED)
);
if matches!(last_code, 2 | 4 | 5) {
println!(
" unsupported host: the typist refused before sending any key (exit \
{last_code}), so typed input is not staged here"
);
} else if !took {
println!(
" staging limitation: the injector's keystrokes never reached the chooser's \
location popup in any of the attempts, so typed input could not be staged here \
(this is XTEST-into-GTK timing, not a library behaviour; the typed path has \
been observed working repeatedly — see docs/log/log-20261014-1.md)"
);
std::process::exit(2);
} else {
check(
last_code == 0,
"the typist typed into the dialog (its focus checks passed first)",
);
check(
took,
"the path the injector typed is the path `choose_path` answered with, so real X \
key events travelled through the dialog and back",
);
}
}
println!("\nC. asking from a thread the toolkit forbids a modal window on");
let from_worker = std::thread::spawn(|| {
rust_widgets::choose_path(PathPurpose::OpenFile, "probe: from a worker thread")
})
.join()
.expect("the worker thread must not panic");
println!(" worker thread answered {from_worker:?}");
check(
!matches!(from_worker, Ok(None)),
"a refused cross-thread request is an error, not a cancellation a host would misread",
);
}
println!(
"\n{}",
if failures == 0 {
"probe_file_picker: OK (every check held)".to_string()
} else {
format!("probe_file_picker: {failures} check(s) failed")
}
);
std::process::exit(if failures == 0 { 0 } else { 1 });
}