#[cfg(all(not(alloc_frugal), target_os = "linux", feature = "gtk-native"))]
fn main() {
staged()
}
#[cfg(not(all(not(alloc_frugal), target_os = "linux", feature = "gtk-native")))]
fn main() {
println!(
"probe_window_on_screen: this build has no X11 session to ask (`--features \
desktop,gtk-native` on a Linux host with DISPLAY set stages it)."
);
}
#[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), target_os = "linux", feature = "gtk-native"))]
fn staged() {
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
if !gui_probe_allowed() {
std::process::exit(2);
}
const TITLE: &str = "rw on-screen probe (throwaway)";
const CAPTURE_AFTER: Duration = Duration::from_millis(700);
const SIZE: (u32, u32) = (480, 320);
let mut failures = 0usize;
let mut check = |held: bool, what: &str| {
if held {
println!(" OK {what}");
} else {
println!(" FAIL {what}");
failures += 1;
}
};
println!("=== window on screen 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 ["wmctrl", "xwininfo", "xwd"] {
if Command::new(tool)
.arg("-help")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_err()
&& which_missing(tool)
{
println!(
"unsupported host: `{tool}` is not installed, so the X server cannot be asked"
);
std::process::exit(2);
}
}
rust_widgets::init();
let window = rust_widgets::create_window(TITLE, 60, 60, SIZE.0, SIZE.1);
check(window != 0, "the library created a window");
let button = rust_widgets::create_button(window, "on screen", 24, 24, 160, 36);
check(button != 0, "a library-painted control was created inside it");
rust_widgets::show_widget(window);
let (report_tx, report_rx) = std::sync::mpsc::channel::<ScreenReport>();
let watcher = std::thread::spawn(move || {
std::thread::sleep(CAPTURE_AFTER);
let report = ask_the_x_server(TITLE, SIZE);
let _ = report_tx.send(report);
let _ = rust_widgets::quit();
});
let started = Instant::now();
rust_widgets::run();
let run_took = started.elapsed();
let report = report_rx.recv_timeout(Duration::from_secs(30)).expect("the watcher must report");
let _ = watcher.join();
println!(" run() returned after {run_took:?}");
println!("\nwhat the X server said:");
println!(" window manager lists it : {}", report.listed);
println!(" server map state : {:?}", report.map_state);
println!(
" server geometry : {}x{} (library asked for {}x{})",
report.width, report.height, SIZE.0, SIZE.1
);
match &report.capture {
Some(capture) => println!(
" xwd capture : {}x{} at {} bpp, {} distinct colours",
capture.width, capture.height, capture.bits_per_pixel, capture.distinct_colours
),
None => println!(
" xwd capture : none ({})",
report.capture_error.clone().unwrap_or_default()
),
}
check(report.listed, "the window manager lists a window with the library's title");
check(
report.map_state.as_deref() == Some("IsViewable"),
"the server reports the window as viewable (mapped, ancestors mapped, on screen)",
);
check(
report.width == SIZE.0 && report.height == SIZE.1,
"the server's geometry for the window is the size the library asked for",
);
match &report.capture {
Some(capture) => {
check(
capture.width == SIZE.0 && capture.height == SIZE.1,
"the captured image is the window's size, so this is a frame of it and not of something else",
);
check(
capture.distinct_colours > 2,
"the captured pixels hold more than a flat fill, i.e. the library's control was painted into them",
);
}
None => check(false, "the window's pixels could be read back with xwd"),
}
println!(
"\n{}",
if failures == 0 {
"probe_window_on_screen: OK (every check held)".to_string()
} else {
format!("probe_window_on_screen: {failures} check(s) failed")
}
);
std::process::exit(if failures == 0 { 0 } else { 1 });
}
#[cfg(all(not(alloc_frugal), target_os = "linux", feature = "gtk-native"))]
struct ScreenReport {
listed: bool,
map_state: Option<String>,
width: u32,
height: u32,
capture: Option<XwdCapture>,
capture_error: Option<String>,
}
#[cfg(all(not(alloc_frugal), target_os = "linux", feature = "gtk-native"))]
struct XwdCapture {
width: u32,
height: u32,
bits_per_pixel: u32,
distinct_colours: usize,
}
#[cfg(all(not(alloc_frugal), target_os = "linux", feature = "gtk-native"))]
fn which_missing(tool: &str) -> bool {
std::env::var_os("PATH")
.map(|path| {
!std::env::split_paths(&path).any(|dir| {
let candidate = dir.join(tool);
candidate.is_file() && is_executable(&candidate)
})
})
.unwrap_or(true)
}
#[cfg(all(not(alloc_frugal), target_os = "linux", feature = "gtk-native"))]
fn is_executable(path: &std::path::Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path).map(|meta| meta.permissions().mode() & 0o111 != 0).unwrap_or(false)
}
#[cfg(all(not(alloc_frugal), target_os = "linux", feature = "gtk-native"))]
fn ask_the_x_server(title: &str, expected: (u32, u32)) -> ScreenReport {
let mut report = ScreenReport {
listed: false,
map_state: None,
width: 0,
height: 0,
capture: None,
capture_error: None,
};
let listing = std::process::Command::new("wmctrl").arg("-l").output();
let id = match listing {
Ok(output) => String::from_utf8_lossy(&output.stdout)
.lines()
.find(|line| line.contains(title))
.and_then(|line| line.split_whitespace().next().map(str::to_owned)),
Err(error) => {
report.capture_error = Some(format!("wmctrl could not be run: {error}"));
None
}
};
let Some(id) = id else {
report.capture_error = Some(format!("no managed window is titled {title:?}"));
return report;
};
report.listed = true;
if let Ok(output) = std::process::Command::new("xwininfo").args(["-id", &id]).output() {
let text = String::from_utf8_lossy(&output.stdout);
report.map_state = text
.lines()
.find_map(|line| line.trim().strip_prefix("Map State:").map(|v| v.trim().to_owned()));
report.width = field(&text, "Width:");
report.height = field(&text, "Height:");
}
let dump = std::env::temp_dir().join(format!("rw_on_screen_{}.xwd", std::process::id()));
let capture = std::process::Command::new("xwd")
.args(["-id", &id, "-silent"])
.stdout(std::fs::File::create(&dump).expect("a writable temp file for the dump"))
.output();
match capture {
Ok(output) if output.status.success() => match std::fs::read(&dump) {
Ok(bytes) => match parse_xwd(&bytes) {
Ok(parsed) => {
if parsed.0 != expected.0 || parsed.1 != expected.1 {
report.capture_error = Some(format!(
"captured {}x{}, not the {}x{} the library created",
parsed.0, parsed.1, expected.0, expected.1
));
}
report.capture = Some(XwdCapture {
width: parsed.0,
height: parsed.1,
bits_per_pixel: parsed.2,
distinct_colours: parsed.3,
});
}
Err(error) => report.capture_error = Some(error),
},
Err(error) => {
report.capture_error = Some(format!("the dump could not be read: {error}"))
}
},
Ok(output) => report.capture_error = Some(format!("xwd exited with {}", output.status)),
Err(error) => report.capture_error = Some(format!("xwd could not be run: {error}")),
}
let _ = std::fs::remove_file(&dump);
report
}
#[cfg(all(not(alloc_frugal), target_os = "linux", feature = "gtk-native"))]
fn field(text: &str, label: &str) -> u32 {
text.lines()
.find_map(|line| line.trim().strip_prefix(label))
.and_then(|value| value.trim().parse().ok())
.unwrap_or(0)
}
#[cfg(all(not(alloc_frugal), target_os = "linux", feature = "gtk-native"))]
fn parse_xwd(bytes: &[u8]) -> Result<(u32, u32, u32, usize), String> {
fn u32_at(bytes: &[u8], offset: usize) -> u32 {
u32::from_be_bytes([bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]])
}
if bytes.len() < 100 {
return Err(format!("the dump is {} bytes, shorter than an XWD header", bytes.len()));
}
let header_size = u32_at(bytes, 0) as usize;
let bits_per_pixel = u32_at(bytes, 44);
let bytes_per_line = u32_at(bytes, 48) as usize;
let colour_map_entries = u32_at(bytes, 76) as usize;
let width = u32_at(bytes, 80);
let height = u32_at(bytes, 84);
let pixel_start = header_size + colour_map_entries * 12;
if bits_per_pixel == 0 || bytes_per_line == 0 || width == 0 || height == 0 {
return Err(format!(
"the dump declares {width}x{height} at {bits_per_pixel} bpp, which cannot be a frame"
));
}
let bytes_per_pixel = (bits_per_pixel / 8) as usize;
let needed = pixel_start + bytes_per_line * height as usize;
if bytes.len() < needed {
return Err(format!(
"the dump holds {} bytes but its header describes {needed}",
bytes.len()
));
}
const CAP: usize = 4096;
let mut seen = std::collections::HashSet::new();
'rows: for row in 0..height as usize {
let start = pixel_start + row * bytes_per_line;
for column in 0..width as usize {
let at = start + column * bytes_per_pixel;
let pixel = &bytes[at..at + bytes_per_pixel];
seen.insert(pixel.to_vec());
if seen.len() >= CAP {
break 'rows;
}
}
}
Ok((width, height, bits_per_pixel, seen.len()))
}