use super::super::core::{
self, AppInfo, SurfaceGeometry, TargetSpec, UnreadWindows, WindowInfo, WindowKey, WindowListing,
};
use std::sync::{Once, OnceLock};
use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, LPARAM, POINT, RECT};
use windows::Win32::Graphics::Dwm::{
DWMWA_CLOAKED, DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute,
};
use windows::Win32::Security::{GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation};
use windows::Win32::System::StationsAndDesktops::{
CloseDesktop, DESKTOP_CONTROL_FLAGS, DESKTOP_READOBJECTS, GetUserObjectInformationW,
OpenInputDesktop, UOI_NAME,
};
use windows::Win32::System::Threading::{
GetCurrentProcessId, OpenProcess, OpenProcessToken, PROCESS_NAME_WIN32,
PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW,
};
use windows::Win32::UI::HiDpi::{
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, SetProcessDpiAwarenessContext,
};
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GA_ROOT, GWL_EXSTYLE, GetAncestor, GetForegroundWindow, GetSystemMetrics,
GetWindowLongW, GetWindowRect, GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId,
IsIconic, IsWindow, IsWindowVisible, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN,
SM_YVIRTUALSCREEN, SetForegroundWindow, WS_EX_TOOLWINDOW, WindowFromPoint,
};
use windows::core::BOOL;
const FOREGROUND_RETRIES: usize = 5;
const FOREGROUND_PAUSE: std::time::Duration = std::time::Duration::from_millis(20);
pub(super) struct TargetWindow {
pub hwnd: HWND,
pub pid: u32,
pub app_name: String,
pub title: String,
pub surface: SurfaceGeometry,
}
pub(super) fn ensure_dpi_aware() {
static ONCE: Once = Once::new();
ONCE.call_once(|| unsafe {
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
});
}
pub(super) fn interactive_desktop_available() -> bool {
ensure_input_desktop().is_ok()
}
pub(super) fn resolve(target: &TargetSpec) -> anyhow::Result<TargetWindow> {
match target {
TargetSpec::Screen => Err(core::screen_act_error()),
TargetSpec::Focused => {
let hwnd = unsafe { GetForegroundWindow() };
if hwnd.0.is_null() {
return Err(core::taxonomy_error(
core::ERR_DEGRADED,
"no foreground window — the session is locked, the UAC secure desktop is up, \
or no window has focus; ask the owner to return to their desktop",
));
}
describe(hwnd)
}
TargetSpec::App { app_pid, app_name } => {
let windows = app_windows(*app_pid, app_name);
let window = windows
.iter()
.find(|w| w.active)
.or_else(|| windows.first())
.ok_or_else(|| core::surface_gone(app_name))?;
describe(window.hwnd)
}
TargetSpec::Window { app_name, key, .. } => resolve_key(app_name, key),
}
}
fn resolve_key(app_name: &str, key: &WindowKey) -> anyhow::Result<TargetWindow> {
let Some((pid, hwnd)) = parse_key(key) else {
return Err(core::surface_gone(app_name));
};
let alive =
unsafe { IsWindow(Some(hwnd)) }.as_bool() && pid_of(hwnd) == pid;
if !alive {
return Err(core::surface_gone(app_name));
}
describe(hwnd)
}
fn window_key(hwnd: HWND, pid: u32) -> WindowKey {
WindowKey::new(format!("hwnd:{pid}:{}", hwnd.0 as usize))
}
fn parse_key(key: &WindowKey) -> Option<(u32, HWND)> {
let (pid, hwnd) = key.as_str().strip_prefix("hwnd:")?.split_once(':')?;
let hwnd = hwnd.parse::<usize>().ok()?;
Some((pid.parse().ok()?, HWND(hwnd as *mut std::ffi::c_void)))
}
pub(super) fn surface_geometry(target: &TargetSpec) -> anyhow::Result<SurfaceGeometry> {
match target {
TargetSpec::Screen => screen_geometry(),
other => Ok(resolve(other)?.surface),
}
}
pub(super) fn screen_geometry() -> anyhow::Result<SurfaceGeometry> {
unsafe {
let (x, y) = (
GetSystemMetrics(SM_XVIRTUALSCREEN),
GetSystemMetrics(SM_YVIRTUALSCREEN),
);
let (width, height) = (
GetSystemMetrics(SM_CXVIRTUALSCREEN),
GetSystemMetrics(SM_CYVIRTUALSCREEN),
);
if width <= 0 || height <= 0 {
return Err(core::taxonomy_error(
core::ERR_DESKTOP_UNAVAILABLE,
format!("{SECURE_DESKTOP_REASON} — there is no screen to describe"),
));
}
Ok(SurfaceGeometry {
x: f64::from(x),
y: f64::from(y),
width: f64::from(width),
height: f64::from(height),
})
}
}
pub(super) fn top_level_at(x: i32, y: i32) -> Option<HWND> {
let hwnd = unsafe { WindowFromPoint(POINT { x, y }) };
if hwnd.0.is_null() {
return None;
}
let root = unsafe { GetAncestor(hwnd, GA_ROOT) };
Some(if root.0.is_null() { hwnd } else { root })
}
pub(super) fn foreground() -> Option<HWND> {
let hwnd = unsafe { GetForegroundWindow() };
(!hwnd.0.is_null()).then_some(hwnd)
}
pub(super) fn ensure_foreground(hwnd: HWND) -> anyhow::Result<()> {
if unsafe { GetForegroundWindow() } == hwnd {
return Ok(());
}
let title = title_of(hwnd);
Err(core::not_brought_forward(core::window_description(
&app_name(pid_of(hwnd), &title),
&title,
)))
}
pub(super) fn activate(hwnd: HWND) {
if unsafe { GetForegroundWindow() } == hwnd {
return;
}
unsafe {
let _ = SetForegroundWindow(hwnd);
}
for _ in 0..FOREGROUND_RETRIES {
if unsafe { GetForegroundWindow() } == hwnd {
return;
}
std::thread::sleep(FOREGROUND_PAUSE);
}
}
pub(super) fn ensure_actuation_allowed(hwnd: Option<HWND>) -> anyhow::Result<()> {
ensure_input_desktop()?;
let Some(hwnd) = hwnd else {
return Ok(());
};
if hwnd.0.is_null() || own_process_is_elevated() {
return Ok(());
}
let pid = pid_of(hwnd);
if is_own_process(pid) || !process_is_elevated(pid) {
return Ok(());
}
let title = title_of(hwnd);
let app = process_name(pid).unwrap_or_else(|| format!("pid {pid}"));
Err(core::permission_denied(format!(
"{} belongs to {app} (pid {pid}), which runs elevated — this non-elevated process cannot \
send it input or drive its accessibility elements (Windows blocks both); {}ask the owner \
to run that application without elevation",
core::window_description("", &title),
if super::super::capture_known_available() {
"a capture of the window may still work — "
} else {
""
}
)))
}
pub(super) fn ensure_not_own_window(hwnd: HWND) -> anyhow::Result<()> {
if !is_own_process(pid_of(hwnd)) {
return Ok(());
}
let title = title_of(hwnd);
Err(core::taxonomy_error(
core::ERR_SELF_TARGET,
format!(
"{} belongs to mahbot itself: this process cannot drive its own accessibility elements \
safely, because a same-process element action is answered in-process and there is no \
way onto the interface thread — it could freeze the dashboard or kill the service, so \
it is refused instead. Our windows stay listed, readable and capturable, and \
coordinate input still reaches them",
core::window_description("", &title)
),
))
}
fn ensure_input_desktop() -> anyhow::Result<()> {
let desktop =
unsafe { OpenInputDesktop(DESKTOP_CONTROL_FLAGS::default(), false, DESKTOP_READOBJECTS) };
let Ok(desktop) = desktop else {
return Err(secure_desktop_error());
};
let mut name = [0u16; 64];
let read = unsafe {
GetUserObjectInformationW(
windows::Win32::Foundation::HANDLE(desktop.0),
UOI_NAME,
Some(name.as_mut_ptr().cast()),
(name.len() * 2) as u32,
None,
)
};
unsafe {
let _ = CloseDesktop(desktop);
}
if read.is_err() {
return Err(secure_desktop_error());
}
let end = name.iter().position(|c| *c == 0).unwrap_or(name.len());
if String::from_utf16_lossy(&name[..end]).eq_ignore_ascii_case("Default") {
return Ok(());
}
Err(secure_desktop_error())
}
pub(super) const SECURE_DESKTOP_REASON: &str = "the UAC secure desktop, the lock screen or a \
non-interactive session owns the desktop";
fn secure_desktop_error() -> anyhow::Error {
core::taxonomy_error(
core::ERR_DESKTOP_UNAVAILABLE,
format!(
"{SECURE_DESKTOP_REASON} — nothing can be read or actuated there; ask the owner to return to their desktop"
),
)
}
fn own_process_is_elevated() -> bool {
static OWN: OnceLock<bool> = OnceLock::new();
*OWN.get_or_init(|| unsafe { process_is_elevated(GetCurrentProcessId()) })
}
fn process_is_elevated(pid: u32) -> bool {
unsafe {
let Ok(process) = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) else {
return false;
};
let mut token = HANDLE::default();
let opened = OpenProcessToken(process, TOKEN_QUERY, &raw mut token).is_ok();
let _ = CloseHandle(process);
if !opened {
return false;
}
let mut elevation = TOKEN_ELEVATION::default();
let mut size = 0u32;
let read = GetTokenInformation(
token,
TokenElevation,
Some(std::ptr::from_mut(&mut elevation).cast()),
std::mem::size_of::<TOKEN_ELEVATION>() as u32,
&raw mut size,
)
.is_ok();
let _ = CloseHandle(token);
read && elevation.TokenIsElevated != 0
}
}
struct Enumerated {
hwnd: HWND,
pid: u32,
app_name: String,
title: String,
active: bool,
surface: anyhow::Result<SurfaceGeometry>,
}
pub(super) fn enumerate_apps() -> Vec<AppInfo> {
let mut apps: Vec<AppInfo> = Vec::new();
for win in top_level_windows() {
if apps
.iter()
.any(|a| a.pid == Some(win.pid) && a.name.eq_ignore_ascii_case(&win.app_name))
{
continue;
}
apps.push(AppInfo {
pid: Some(win.pid),
name: win.app_name,
});
}
apps
}
struct LostFrames {
app_name: String,
count: usize,
first_error: String,
}
pub(super) fn list_windows(app: Option<&AppInfo>) -> WindowListing {
let windows = match app {
Some(app) => app_windows(app.pid, &app.name),
None => top_level_windows(),
};
let mut listing = WindowListing::default();
let mut lost: Vec<LostFrames> = Vec::new();
for win in windows {
let surface = match win.surface {
Ok(surface) => surface,
Err(error) => {
match lost
.iter_mut()
.find(|entry| entry.app_name.eq_ignore_ascii_case(&win.app_name))
{
Some(entry) => entry.count += 1,
None => lost.push(LostFrames {
app_name: win.app_name.clone(),
count: 1,
first_error: format!("{error:#}"),
}),
}
continue;
}
};
listing.windows.push(WindowInfo {
app_pid: Some(win.pid),
app_name: win.app_name,
title: win.title,
key: window_key(win.hwnd, win.pid),
surface,
});
}
listing.unread = lost
.into_iter()
.map(|entry| UnreadWindows {
app_name: entry.app_name,
reason: format!(
"{} {} no readable frame (first error: {})",
entry.count,
if entry.count == 1 {
"window has"
} else {
"windows have"
},
entry.first_error
),
})
.collect();
listing
}
pub(super) fn focused_window() -> anyhow::Result<WindowInfo> {
let hwnd = unsafe { GetForegroundWindow() };
if hwnd.0.is_null() {
return Err(core::taxonomy_error(
core::ERR_DEGRADED,
"no foreground window found — the session is locked, the UAC secure desktop is up, or \
no window has focus; ask the owner to return to their desktop, or enumerate \
apps/windows to pick a surface",
));
}
let resolved = describe(hwnd)?;
Ok(WindowInfo {
app_pid: Some(resolved.pid),
app_name: resolved.app_name,
title: resolved.title,
key: window_key(hwnd, resolved.pid),
surface: resolved.surface,
})
}
fn app_windows(pid: Option<u32>, app_name: &str) -> Vec<Enumerated> {
top_level_windows()
.into_iter()
.filter(|win| match pid {
Some(pid) => {
win.pid == pid
&& (app_name.is_empty() || win.app_name.eq_ignore_ascii_case(app_name))
}
None => win.app_name.eq_ignore_ascii_case(app_name),
})
.collect()
}
fn top_level_windows() -> Vec<Enumerated> {
let mut handles: Vec<HWND> = Vec::new();
unsafe {
let _ = EnumWindows(
Some(collect_window),
LPARAM(std::ptr::from_mut(&mut handles).cast::<std::ffi::c_void>() as isize),
);
}
let foreground = unsafe { GetForegroundWindow() };
let mut windows: Vec<Enumerated> = handles
.into_iter()
.filter(|hwnd| is_application_window(*hwnd))
.map(|hwnd| enumerate(hwnd, foreground))
.collect();
if foreground_joins_listing(&windows, foreground) {
windows.insert(0, enumerate(foreground, foreground));
}
windows
}
fn foreground_joins_listing(windows: &[Enumerated], foreground: HWND) -> bool {
if foreground.0.is_null() || windows.iter().any(|win| win.hwnd == foreground) {
return false;
}
unsafe { IsWindowVisible(foreground) }.as_bool() && !is_cloaked(foreground)
}
fn enumerate(hwnd: HWND, foreground: HWND) -> Enumerated {
let pid = pid_of(hwnd);
let title = title_of(hwnd);
Enumerated {
hwnd,
pid,
app_name: app_name(pid, &title),
title,
active: hwnd == foreground,
surface: surface_of(hwnd),
}
}
fn app_name(pid: u32, title: &str) -> String {
match process_name(pid) {
Some(stem) if stem.eq_ignore_ascii_case("ApplicationFrameHost") && !title.is_empty() => {
title.to_string()
}
Some(stem) => stem,
None => format!("pid {pid}"),
}
}
unsafe extern "system" fn collect_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let handles = unsafe { &mut *(lparam.0 as *mut Vec<HWND>) };
handles.push(hwnd);
BOOL(1)
}
fn is_application_window(hwnd: HWND) -> bool {
unsafe {
if !IsWindowVisible(hwnd).as_bool() || GetWindowTextLengthW(hwnd) == 0 {
return false;
}
let ex_style = GetWindowLongW(hwnd, GWL_EXSTYLE) as u32;
if ex_style & WS_EX_TOOLWINDOW.0 != 0 {
return false;
}
!is_cloaked(hwnd)
}
}
fn is_cloaked(hwnd: HWND) -> bool {
let mut cloaked = 0u32;
let queried = unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_CLOAKED,
std::ptr::from_mut(&mut cloaked).cast(),
std::mem::size_of::<u32>() as u32,
)
};
queried.is_ok() && cloaked != 0
}
fn surface_of(hwnd: HWND) -> anyhow::Result<SurfaceGeometry> {
let mut rect = RECT::default();
let frame = unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_EXTENDED_FRAME_BOUNDS,
std::ptr::from_mut(&mut rect).cast(),
std::mem::size_of::<RECT>() as u32,
)
};
if frame.is_err() {
return window_rect(hwnd);
}
Ok(geometry(rect))
}
pub(super) fn window_rect(hwnd: HWND) -> anyhow::Result<SurfaceGeometry> {
let mut rect = RECT::default();
unsafe { GetWindowRect(hwnd, &raw mut rect) }.map_err(|e| {
core::taxonomy_error(
core::ERR_NOT_MATCHED,
format!("window is gone or has no frame ({e}) — re-enumerate windows"),
)
})?;
Ok(geometry(rect))
}
fn geometry(rect: RECT) -> SurfaceGeometry {
SurfaceGeometry {
x: f64::from(rect.left),
y: f64::from(rect.top),
width: f64::from(rect.right - rect.left),
height: f64::from(rect.bottom - rect.top),
}
}
fn describe(hwnd: HWND) -> anyhow::Result<TargetWindow> {
let pid = pid_of(hwnd);
let title = title_of(hwnd);
if unsafe { IsIconic(hwnd) }.as_bool() {
return Err(core::minimized_window(&title));
}
Ok(TargetWindow {
hwnd,
pid,
app_name: app_name(pid, &title),
title,
surface: surface_of(hwnd)?,
})
}
fn pid_of(hwnd: HWND) -> u32 {
let mut pid = 0u32;
unsafe {
GetWindowThreadProcessId(hwnd, Some(&raw mut pid));
}
pid
}
fn is_own_process(pid: u32) -> bool {
pid != 0 && pid == unsafe { GetCurrentProcessId() }
}
fn title_of(hwnd: HWND) -> String {
core::cap_label(&title_text(hwnd))
}
pub(super) fn title_text(hwnd: HWND) -> String {
let length = unsafe { GetWindowTextLengthW(hwnd) };
if length <= 0 {
return String::new();
}
let wanted = core::MAX_NAME_CHARS * 2 + 2;
let mut buffer = vec![0u16; (length as usize).min(wanted) + 1];
let written = unsafe { GetWindowTextW(hwnd, &mut buffer) };
String::from_utf16_lossy(&buffer[..written.max(0) as usize])
}
fn process_name(pid: u32) -> Option<String> {
if pid == 0 {
return None;
}
let mut path = vec![0u16; 1024];
let mut length = u32::try_from(path.len()).ok()?;
unsafe {
let process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid).ok()?;
let queried = QueryFullProcessImageNameW(
process,
PROCESS_NAME_WIN32,
windows::core::PWSTR(path.as_mut_ptr()),
&raw mut length,
);
let _ = CloseHandle(process);
queried.ok()?;
}
let full = String::from_utf16_lossy(&path[..length as usize]);
let name = std::path::Path::new(&full)
.file_stem()
.map(|stem| stem.to_string_lossy().into_owned())?;
if name.is_empty() { None } else { Some(name) }
}