#![allow(unsafe_code)]
use std::path::PathBuf;
use crate::error::ComputerError;
use crate::target::{assign_z_order, filter_windows_for_pid};
use crate::target::{
ActivationPolicy, AppInfo, ListedWindow, WindowBounds, WindowInfo, WindowList,
};
use windows::core::BOOL;
use windows::Win32::Foundation::{HWND, LPARAM, RECT};
use windows::Win32::System::Threading::{
OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_FORMAT, PROCESS_QUERY_LIMITED_INFORMATION,
};
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GetAncestor, GetWindowRect, GetWindowTextW, GetWindowThreadProcessId,
IsWindowVisible, GA_ROOT,
};
const TITLE_BUF_LEN: usize = 512;
pub(crate) fn build_apps(snapshots: &[WindowSnapshot]) -> Vec<AppInfo> {
use std::collections::BTreeMap;
let mut by_pid: BTreeMap<u32, &WindowSnapshot> = BTreeMap::new();
for snap in snapshots {
by_pid.entry(snap.pid).or_insert(snap);
}
let mut apps: Vec<AppInfo> = by_pid
.into_values()
.map(|snap| AppInfo {
name: snap.app_name.clone(),
bundle_id: None,
pid: snap.pid as i32,
path: snap
.exe_path
.as_ref()
.map(|path| path.to_string_lossy().into_owned()),
active: true,
hidden: false,
activation_policy: ActivationPolicy::Regular,
})
.collect();
apps.sort_by_key(|app| app.pid);
apps
}
pub(crate) fn build_windows(snapshots: &[WindowSnapshot]) -> WindowList {
let listed: Vec<ListedWindow> = snapshots
.iter()
.map(|snap| ListedWindow {
id: snap.hwnd,
pid: snap.pid as i32,
app: snap.app_name.clone(),
title: snap.title.clone(),
bounds: snap.bounds,
layer: 0,
on_screen: snap.visible,
})
.collect();
let windows: Vec<WindowInfo> = assign_z_order(listed);
let note = if snapshots.is_empty() {
Some("no top-level windows visible".to_string())
} else {
None
};
WindowList { windows, note }
}
#[derive(Debug, Clone)]
pub(crate) struct WindowSnapshot {
pub hwnd: u32,
pub pid: u32,
pub app_name: String,
pub title: String,
pub bounds: WindowBounds,
pub visible: bool,
pub exe_path: Option<PathBuf>,
}
pub(crate) fn enumerate_windows() -> Result<Vec<WindowSnapshot>, ComputerError> {
let mut snapshots: Vec<WindowSnapshot> = Vec::new();
let lparam = LPARAM(&mut snapshots as *mut _ as isize);
unsafe {
EnumWindows(Some(enum_callback), lparam).map_err(|error| {
ComputerError::ExecutionFailed(format!("EnumWindows failed: {error}"))
})?;
}
Ok(snapshots)
}
unsafe extern "system" fn enum_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
let root = GetAncestor(hwnd, GA_ROOT);
if root != hwnd {
return BOOL(1);
}
if !IsWindowVisible(hwnd).as_bool() {
return BOOL(1);
}
let mut pid: u32 = 0;
GetWindowThreadProcessId(hwnd, Some(&mut pid));
if pid == 0 {
return BOOL(1);
}
let mut title_buf = [0u16; TITLE_BUF_LEN];
let len = GetWindowTextW(hwnd, &mut title_buf);
let title = if len > 0 {
String::from_utf16_lossy(&title_buf[..len as usize])
} else {
String::new()
};
let mut rect = RECT::default();
let rect_ok = GetWindowRect(hwnd, &mut rect).is_ok();
let (exe_path, app_name) = match process_path(pid) {
Ok(path) => {
let name = module_base_name(&path).unwrap_or_else(|| String::from("unknown"));
(Some(path), name)
}
Err(_) => (None, String::from("unknown")),
};
let bounds = if rect_ok {
let width = rect.right.saturating_sub(rect.left).max(0) as u32;
let height = rect.bottom.saturating_sub(rect.top).max(0) as u32;
WindowBounds {
x: rect.left,
y: rect.top,
width,
height,
}
} else {
WindowBounds {
x: 0,
y: 0,
width: 0,
height: 0,
}
};
let snapshots = &mut *(lparam.0 as *mut Vec<WindowSnapshot>);
snapshots.push(WindowSnapshot {
hwnd: hwnd.0 as u32,
pid,
app_name,
title,
bounds,
visible: true,
exe_path,
});
BOOL(1)
}
fn process_path(pid: u32) -> windows::core::Result<PathBuf> {
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) }?;
let result = read_image_name(&handle);
let _ = unsafe { windows::Win32::Foundation::CloseHandle(handle) };
result
}
fn read_image_name(handle: &windows::Win32::Foundation::HANDLE) -> windows::core::Result<PathBuf> {
let mut buffer = [0u16; 1024];
let mut size = buffer.len() as u32;
unsafe {
QueryFullProcessImageNameW(
*handle,
PROCESS_NAME_FORMAT(0),
windows::core::PWSTR(buffer.as_mut_ptr()),
&mut size,
)?;
}
if size == 0 {
return Err(windows::core::Error::from(windows::core::HRESULT(0)));
}
let path = String::from_utf16_lossy(&buffer[..size as usize]);
Ok(PathBuf::from(path))
}
#[allow(dead_code)]
pub(crate) fn keep_pid(windows: &[WindowInfo], pid: i32) -> Vec<WindowInfo> {
filter_windows_for_pid(windows, pid)
}
fn module_base_name(path: &std::path::Path) -> Option<String> {
let file = path.file_name().or_else(|| path.file_stem())?;
let text = file.to_string_lossy();
let trimmed = text.strip_suffix(".exe").unwrap_or(&text);
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
#[allow(dead_code)]
pub(crate) fn window_by_id(windows: &[WindowInfo], id: u32) -> Option<&WindowInfo> {
windows.iter().find(|window| window.id == id)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn builds_apps_from_unique_pids() {
let snaps = vec![
WindowSnapshot {
hwnd: 1,
pid: 10,
app_name: "notepad".into(),
title: "Untitled".into(),
bounds: WindowBounds {
x: 0,
y: 0,
width: 100,
height: 100,
},
visible: true,
exe_path: Some(PathBuf::from(r"C:\Windows\notepad.exe")),
},
WindowSnapshot {
hwnd: 2,
pid: 10,
app_name: "notepad".into(),
title: "second".into(),
bounds: WindowBounds {
x: 0,
y: 0,
width: 100,
height: 100,
},
visible: true,
exe_path: Some(PathBuf::from(r"C:\Windows\notepad.exe")),
},
WindowSnapshot {
hwnd: 3,
pid: 11,
app_name: "explorer".into(),
title: "Files".into(),
bounds: WindowBounds {
x: 0,
y: 0,
width: 200,
height: 200,
},
visible: true,
exe_path: Some(PathBuf::from(r"C:\Windows\explorer.exe")),
},
];
let apps = build_apps(&snaps);
assert_eq!(apps.len(), 2);
assert_eq!(apps[0].pid, 10);
assert_eq!(apps[0].name, "notepad");
assert_eq!(apps[0].path.as_deref(), Some(r"C:\Windows\notepad.exe"));
assert_eq!(apps[1].pid, 11);
}
#[test]
fn builds_windows_in_z_order() {
let snaps = vec![
WindowSnapshot {
hwnd: 5,
pid: 20,
app_name: "notepad".into(),
title: "two".into(),
bounds: WindowBounds {
x: 0,
y: 0,
width: 1,
height: 1,
},
visible: true,
exe_path: None,
},
WindowSnapshot {
hwnd: 6,
pid: 20,
app_name: "notepad".into(),
title: "one".into(),
bounds: WindowBounds {
x: 0,
y: 0,
width: 1,
height: 1,
},
visible: true,
exe_path: None,
},
];
let list = build_windows(&snaps);
assert_eq!(list.windows.len(), 2);
assert_eq!(list.windows[0].id, 5);
assert_eq!(list.windows[0].z, 0);
assert_eq!(list.windows[1].id, 6);
assert_eq!(list.windows[1].z, 1);
assert_eq!(list.windows[0].pid, 20);
}
#[test]
fn empty_snapshots_yield_a_note() {
let list = build_windows(&[]);
assert!(list.windows.is_empty());
assert!(list.note.is_some());
}
#[test]
fn module_base_name_strips_extension_and_directory() {
assert_eq!(
module_base_name(&PathBuf::from(r"C:\Windows\System32\notepad.exe")),
Some("notepad".to_string())
);
assert_eq!(
module_base_name(&PathBuf::from("notepad")),
Some("notepad".to_string())
);
assert_eq!(module_base_name(&PathBuf::from(".exe")), None);
}
#[test]
fn keep_pid_renumbers_z_order() {
let wins = vec![
WindowInfo {
id: 1,
pid: 7,
app: "x".into(),
title: "a".into(),
z: 0,
bounds: WindowBounds {
x: 0,
y: 0,
width: 1,
height: 1,
},
layer: 0,
on_screen: true,
ax_matched: true,
},
WindowInfo {
id: 2,
pid: 8,
app: "y".into(),
title: "b".into(),
z: 1,
bounds: WindowBounds {
x: 0,
y: 0,
width: 1,
height: 1,
},
layer: 0,
on_screen: true,
ax_matched: true,
},
];
let filtered = keep_pid(&wins, 8);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].z, 0);
assert_eq!(filtered[0].id, 2);
}
}