use windows::Win32::Foundation::{HWND, LPARAM};
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::UI::WindowsAndMessaging::{
EnumChildWindows, EnumWindows, GWL_STYLE, GetClassNameW, GetWindowLongW, GetWindowTextW,
GetWindowThreadProcessId, IsIconic, IsWindowVisible, WS_CAPTION,
};
const TEXT_LIMIT: usize = 512;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DialogInfo {
pub title: String,
pub body: String,
pub class: String,
}
#[must_use]
pub fn find_blocking_dialog() -> Option<DialogInfo> {
first_visible_dialog().map(describe)
}
pub(crate) fn first_visible_dialog() -> Option<HWND> {
let mut found: Option<HWND> = None;
let target = (&raw mut found) as isize;
let _ = unsafe { EnumWindows(Some(enumerate), LPARAM(target)) };
found
}
pub(crate) fn describe(handle: HWND) -> DialogInfo {
DialogInfo {
title: window_text(handle),
body: child_text(handle),
class: class_name(handle),
}
}
unsafe extern "system" fn enumerate(handle: HWND, target: LPARAM) -> windows_core::BOOL {
let visible = unsafe { IsWindowVisible(handle) }.as_bool();
if !visible || !looks_like_a_dialog(handle) || !belongs_to_this_process(handle) {
return true.into();
}
unsafe {
*(target.0 as *mut Option<HWND>) = Some(handle);
}
false.into()
}
fn looks_like_a_dialog(handle: HWND) -> bool {
if unsafe { IsIconic(handle) }.as_bool() {
return false;
}
let style = unsafe { GetWindowLongW(handle, GWL_STYLE) };
u32::try_from(style).is_ok_and(|bits| bits & WS_CAPTION.0 == WS_CAPTION.0)
}
fn class_name(handle: HWND) -> String {
let mut buffer = [0u16; 128];
let written = unsafe { GetClassNameW(handle, &mut buffer) };
let end = usize::try_from(written).unwrap_or(0).min(buffer.len());
buffer
.get(..end)
.map(String::from_utf16_lossy)
.unwrap_or_default()
}
fn belongs_to_this_process(handle: HWND) -> bool {
let mut owner = 0u32;
unsafe { GetWindowThreadProcessId(handle, Some(&raw mut owner)) };
owner == unsafe { GetCurrentProcessId() }
}
fn window_text(handle: HWND) -> String {
let mut buffer = [0u16; TEXT_LIMIT];
let written = unsafe { GetWindowTextW(handle, &mut buffer) };
let end = usize::try_from(written).unwrap_or(0).min(buffer.len());
buffer
.get(..end)
.map(String::from_utf16_lossy)
.unwrap_or_default()
}
fn child_text(parent: HWND) -> String {
let mut lines: Vec<String> = Vec::new();
let target = (&raw mut lines) as isize;
unsafe {
let _ = EnumChildWindows(Some(parent), Some(collect_child), LPARAM(target));
}
lines.retain(|line| !line.trim().is_empty());
lines.join("\n")
}
unsafe extern "system" fn collect_child(handle: HWND, target: LPARAM) -> windows_core::BOOL {
let text = window_text(handle);
if !text.is_empty() {
unsafe {
(*(target.0 as *mut Vec<String>)).push(text);
}
}
true.into()
}
#[cfg(test)]
mod tests {
use super::find_blocking_dialog;
#[test]
fn reports_no_dialog_in_a_headless_test_process() {
assert!(
find_blocking_dialog().is_none(),
"test process should have no modal dialog"
);
}
}