use std::{ffi::CString, process, ptr, time::Duration};
use thiserror::Error;
use winapi::{
shared::{
minwindef::{BOOL, DWORD, FALSE, LPARAM, TRUE},
windef::HWND,
},
um::{
processthreadsapi::GetCurrentThreadId,
winuser::{
AttachThreadInput, EnumWindows, FindWindowA, GetForegroundWindow, GetWindow,
GetWindowThreadProcessId, IsHungAppWindow, IsIconic, IsWindowVisible,
SetForegroundWindow, ShowWindow, GW_OWNER, SW_RESTORE,
},
},
};
const SLEEP_DURATION: u64 = 10;
#[derive(Debug, Error)]
pub enum Error {
#[error("Failed to set foreground window")]
SetForegroundWindowFailed,
#[error("Failed to locate a top-level window for process {0:X}")]
FindTopLevelWindowFailed(u32),
#[error("Failed to activate window")]
WindowActivationFailed,
#[error("Cannot set hung window as foreground window")]
TargetWindowHung,
}
#[derive(Debug, Eq, PartialEq)]
struct Window {
hwnd: HWND,
}
impl Window {
pub unsafe fn from_raw_handle(hwnd: HWND) -> Self {
Self { hwnd }
}
pub fn process_id(&self) -> DWORD {
let mut process_id: DWORD = 0;
unsafe { GetWindowThreadProcessId(self.hwnd, &mut process_id as *mut _) };
process_id
}
pub fn thread_id(&self) -> DWORD {
unsafe { GetWindowThreadProcessId(self.hwnd, ptr::null_mut()) }
}
pub fn is_visible(&self) -> bool {
let result = unsafe { IsWindowVisible(self.hwnd) };
result != FALSE
}
fn inner_set_foreground(&self) -> Result<(), Error> {
if self.is_hung() {
return Err(Error::TargetWindowHung);
}
if self.is_foreground() {
return Ok(());
}
let previous_foreground_window = get_foreground_window();
unsafe { SetForegroundWindow(self.hwnd) };
std::thread::sleep(Duration::from_millis(SLEEP_DURATION));
let new_foreground_window = get_foreground_window();
if new_foreground_window == *self
|| new_foreground_window != previous_foreground_window
&& self.hwnd == unsafe { GetWindow(new_foreground_window.hwnd, GW_OWNER) }
{
Ok(())
} else {
Err(Error::SetForegroundWindowFailed)
}
}
pub fn try_set_foreground(&self, max_tries: i32) -> Result<(), Error> {
let detach = attach_foreground(self.thread_id());
for try_count in 1..=max_tries {
match self.inner_set_foreground() {
Ok(()) => {
detach();
return Ok(());
}
Err(err) if try_count == max_tries => {
detach();
return Err(err);
}
Err(_) => continue,
}
}
Ok(())
}
pub fn is_foreground(&self) -> bool {
let foreground = unsafe { GetForegroundWindow() };
foreground == self.hwnd
}
pub fn is_minimized(&self) -> bool {
let result = unsafe { IsIconic(self.hwnd) };
result != FALSE
}
pub fn is_hung(&self) -> bool {
let result = unsafe { IsHungAppWindow(self.hwnd) };
result != FALSE
}
pub fn restore_if_minimized(&self) {
if self.is_minimized() {
unsafe { ShowWindow(self.hwnd, SW_RESTORE) };
}
}
}
fn enum_windows<F>(mut func: F)
where
F: FnMut(Window) -> bool,
{
unsafe extern "system" fn callback<F>(hwnd: HWND, func_ptr: LPARAM) -> BOOL
where
F: FnMut(Window) -> bool,
{
let func: &mut &mut F = &mut *(func_ptr as *mut _);
func(Window::from_raw_handle(hwnd)) as BOOL
}
let func_ptr = &mut &mut func as *mut _;
let lparam = func_ptr as LPARAM;
unsafe { EnumWindows(Some(callback::<F>), lparam) };
}
fn attach_foreground(target_thread: u32) -> impl FnOnce() {
let foreground_window = get_foreground_window();
let foreground_thread = foreground_window.thread_id();
let current_thread = unsafe { GetCurrentThreadId() };
let did_attach_current_to_foreground = !foreground_window.is_hung()
&& unsafe { AttachThreadInput(current_thread, foreground_thread, TRUE) } != FALSE;
let did_attach_foreground_to_target =
unsafe { AttachThreadInput(foreground_thread, target_thread, TRUE) } != FALSE;
move || {
if did_attach_current_to_foreground {
unsafe { AttachThreadInput(current_thread, foreground_thread, FALSE) };
};
if did_attach_foreground_to_target {
unsafe { AttachThreadInput(foreground_thread, target_thread, FALSE) };
};
}
}
fn get_foreground_window() -> Window {
let mut foreground_hwnd = unsafe { GetForegroundWindow() };
if foreground_hwnd.is_null() {
foreground_hwnd = unsafe {
let window_name = CString::new("Shell_TrayWnd").unwrap();
FindWindowA(window_name.as_ptr(), ptr::null_mut())
}
}
unsafe { Window::from_raw_handle(foreground_hwnd) }
}
fn get_top_level_window(process: &process::Child) -> Result<Window, Error> {
let mut result_hwnd = ptr::null_mut();
enum_windows(|window| {
if window.process_id() == process.id() && window.is_visible() {
result_hwnd = window.hwnd;
false
} else {
true
}
});
if !result_hwnd.is_null() {
Ok(unsafe { Window::from_raw_handle(result_hwnd) })
} else {
Err(Error::FindTopLevelWindowFailed(process.id()))
}
}
pub fn activate_top_level_window(process: &process::Child) -> Result<(), Error> {
let window = get_top_level_window(process)?;
if window.is_foreground() {
return Ok(());
}
window.restore_if_minimized();
window.try_set_foreground(5)?;
Ok(())
}