use std::collections::HashSet;
use std::sync::Mutex;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Receiver, Sender};
use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, LPARAM, WPARAM};
use windows::Win32::System::Threading::{CreateEventW, GetCurrentThreadId, SetEvent};
use windows::Win32::UI::Accessibility::{HWINEVENTHOOK, SetWinEventHook};
use windows::Win32::UI::WindowsAndMessaging::{GetMessageW, MSG, PostThreadMessageW, WM_QUIT};
const EVENT_SYSTEM_FOREGROUND: u32 = 0x0003;
const EVENT_SYSTEM_MINIMIZESTART: u32 = 0x0016;
const EVENT_SYSTEM_MINIMIZEEND: u32 = 0x0017;
const EVENT_OBJECT_CREATE: u32 = 0x8000;
const EVENT_OBJECT_DESTROY: u32 = 0x8001;
const EVENT_OBJECT_SHOW: u32 = 0x8002;
const EVENT_OBJECT_HIDE: u32 = 0x8003;
const EVENT_OBJECT_STATECHANGE: u32 = 0x800A;
const EVENT_OBJECT_LOCATIONCHANGE: u32 = 0x800B;
const EVENT_OBJECT_NAMECHANGE: u32 = 0x800C;
const OBJID_WINDOW: i32 = 0x0000;
const WINEVENT_OUTOFCONTEXT: u32 = 0x0000;
#[derive(Debug)]
pub enum HookEvent {
Created {
hwnd: isize,
},
Destroyed {
hwnd: isize,
},
Foreground {
hwnd: isize,
},
MinimizeStart {
hwnd: isize,
},
MinimizeEnd {
hwnd: isize,
},
Shown {
hwnd: isize,
},
Hidden {
hwnd: isize,
},
StateChange {
hwnd: isize,
},
NameChange {
hwnd: isize,
},
LocationChange {
hwnd: isize,
},
}
static HOOK_SENDER: OnceLock<Sender<HookEvent>> = OnceLock::new();
static HOOK_SIGNAL: OnceLock<isize> = OnceLock::new();
static FLOAT_HWNS: OnceLock<Mutex<HashSet<isize>>> = OnceLock::new();
static FLOAT_TRACKING_ACTIVE: AtomicBool = AtomicBool::new(true);
fn unpoison<T>(
lock: std::sync::LockResult<std::sync::MutexGuard<'_, T>>,
) -> std::sync::MutexGuard<'_, T> {
lock.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[must_use]
pub fn float_tracking_active() -> bool {
FLOAT_TRACKING_ACTIVE.load(Ordering::Acquire)
}
pub fn set_float_tracking_active(enabled: bool) {
FLOAT_TRACKING_ACTIVE.store(enabled, Ordering::Release);
}
pub fn add_float_hwnd(hwnd: isize) {
if let Some(set) = FLOAT_HWNS.get() {
unpoison(set.lock()).insert(hwnd);
}
}
pub fn remove_float_hwnd(hwnd: isize) {
if let Some(set) = FLOAT_HWNS.get() {
unpoison(set.lock()).remove(&hwnd);
}
}
pub fn set_float_hwnds(hwnds: &[isize]) {
if let Some(set) = FLOAT_HWNS.get() {
let mut guard = unpoison(set.lock());
guard.clear();
guard.extend(hwnds.iter().copied());
}
}
#[must_use]
pub fn is_float_hwnd(hwnd: isize) -> bool {
match FLOAT_HWNS.get() {
Some(set) => unpoison(set.lock()).contains(&hwnd),
None => false,
}
}
#[must_use]
pub fn float_hwnds_snapshot() -> Vec<isize> {
match FLOAT_HWNS.get() {
Some(set) => unpoison(set.lock()).iter().copied().collect(),
None => Vec::new(),
}
}
pub struct HookThreadHandle {
os_thread_id: u32,
}
impl HookThreadHandle {
pub fn stop(&self) {
unsafe {
let _ = PostThreadMessageW(self.os_thread_id, WM_QUIT, WPARAM(0), LPARAM(0));
}
}
}
impl Drop for HookThreadHandle {
fn drop(&mut self) {
self.stop();
}
}
pub struct HookSignal(HANDLE);
impl HookSignal {
pub fn raw(&self) -> HANDLE {
self.0
}
}
impl Drop for HookSignal {
fn drop(&mut self) {
unsafe {
let _ = CloseHandle(self.0);
}
}
}
unsafe impl Send for HookSignal {}
#[cfg(test)]
impl HookThreadHandle {
pub(crate) fn test_dummy() -> Self {
Self { os_thread_id: 0 }
}
}
#[cfg(test)]
impl HookSignal {
pub(crate) fn test_dummy() -> Self {
let handle = unsafe {
CreateEventW(None, true, false, windows::core::PCWSTR::null())
.expect("CreateEventW should not fail in test_dummy")
};
Self(handle)
}
}
pub fn start_hook_thread(
desktop_name: Option<String>,
) -> Result<(Receiver<HookEvent>, HookThreadHandle, HookSignal), String> {
#[cfg(not(debug_assertions))]
if desktop_name.is_some() {
return Err(
"--desktop is not supported in release builds (desktop isolation is debug-only)"
.to_owned(),
);
}
let (sender, receiver) = mpsc::channel();
let (tid_tx, tid_rx) = mpsc::channel::<u32>();
HOOK_SENDER
.set(sender)
.map_err(|_| "start_hook_thread called more than once — this is a bug".to_owned())?;
let signal_event = unsafe { CreateEventW(None, true, false, windows::core::PCWSTR::null()) }
.map_err(|e| format!("failed to create hook signal event: {e}"))?;
HOOK_SIGNAL
.set(signal_event.0 as isize)
.map_err(|_| "start_hook_thread called more than once — this is a bug".to_owned())?;
FLOAT_HWNS
.set(Mutex::new(HashSet::new()))
.map_err(|_| "start_hook_thread called more than once — this is a bug".to_owned())?;
let hook_signal = HookSignal(signal_event);
std::thread::spawn(move || {
#[cfg(debug_assertions)]
if let Some(ref name) = desktop_name
&& let Err(e) = super::desktop::switch_to_desktop(name)
{
log::error!("hook thread: {e}");
return;
}
#[cfg(not(debug_assertions))]
let _ = desktop_name;
let os_tid = unsafe { GetCurrentThreadId() };
let _ = tid_tx.send(os_tid);
run_hook_loop();
});
let os_thread_id = tid_rx.recv().map_err(|_| {
"hook thread failed to start (exited before reporting thread ID)".to_owned()
})?;
log::info!("hook thread started (OS tid={os_thread_id})");
Ok((receiver, HookThreadHandle { os_thread_id }, hook_signal))
}
fn run_hook_loop() {
let hooks = register_hooks();
if hooks.is_empty() {
log::error!("hook thread: failed to register any hooks");
return;
}
log::info!("hook thread: {} hook(s) registered", hooks.len());
let mut msg = MSG::default();
loop {
let ret = unsafe { GetMessageW(&mut msg, None, 0, 0) };
match ret.0 {
0 => {
log::info!("hook thread: WM_QUIT received, exiting");
break;
}
-1 => {
log::error!("hook thread: GetMessageW returned -1");
break;
}
_ => {
}
}
}
for hook in &hooks {
unsafe {
let _ = windows::Win32::UI::Accessibility::UnhookWinEvent(*hook);
}
}
log::info!("hook thread: stopped");
}
fn register_hooks() -> Vec<HWINEVENTHOOK> {
let mut hooks = Vec::new();
let h = unsafe {
SetWinEventHook(
EVENT_OBJECT_CREATE,
EVENT_OBJECT_DESTROY,
None,
Some(hook_callback),
0,
0,
WINEVENT_OUTOFCONTEXT,
)
};
if is_valid_hook(h) {
hooks.push(h);
log::debug!("hook registered: CREATE/DESTROY");
} else {
log::error!("failed to hook CREATE/DESTROY");
}
let h = unsafe {
SetWinEventHook(
EVENT_SYSTEM_FOREGROUND,
EVENT_SYSTEM_FOREGROUND,
None,
Some(hook_callback),
0,
0,
WINEVENT_OUTOFCONTEXT,
)
};
if is_valid_hook(h) {
hooks.push(h);
log::debug!("hook registered: FOREGROUND");
} else {
log::error!("failed to hook FOREGROUND");
}
let h = unsafe {
SetWinEventHook(
EVENT_SYSTEM_MINIMIZESTART,
EVENT_SYSTEM_MINIMIZEEND,
None,
Some(hook_callback),
0,
0,
WINEVENT_OUTOFCONTEXT,
)
};
if is_valid_hook(h) {
hooks.push(h);
log::debug!("hook registered: MINIMIZE events");
} else {
log::error!("failed to hook MINIMIZE events");
}
let h = unsafe {
SetWinEventHook(
EVENT_OBJECT_SHOW,
EVENT_OBJECT_HIDE,
None,
Some(hook_callback),
0,
0,
WINEVENT_OUTOFCONTEXT,
)
};
if is_valid_hook(h) {
hooks.push(h);
log::debug!("hook registered: SHOW/HIDE");
} else {
log::error!("failed to hook SHOW/HIDE");
}
let h = unsafe {
SetWinEventHook(
EVENT_OBJECT_STATECHANGE,
EVENT_OBJECT_STATECHANGE,
None,
Some(hook_callback),
0,
0,
WINEVENT_OUTOFCONTEXT,
)
};
if is_valid_hook(h) {
hooks.push(h);
log::debug!("hook registered: STATECHANGE");
} else {
log::error!("failed to hook STATECHANGE");
}
let h = unsafe {
SetWinEventHook(
EVENT_OBJECT_NAMECHANGE,
EVENT_OBJECT_NAMECHANGE,
None,
Some(hook_callback),
0,
0,
WINEVENT_OUTOFCONTEXT,
)
};
if is_valid_hook(h) {
hooks.push(h);
log::debug!("hook registered: NAMECHANGE");
} else {
log::error!("failed to hook NAMECHANGE");
}
let h = unsafe {
SetWinEventHook(
EVENT_OBJECT_LOCATIONCHANGE,
EVENT_OBJECT_LOCATIONCHANGE,
None,
Some(hook_callback),
0,
0,
WINEVENT_OUTOFCONTEXT,
)
};
if is_valid_hook(h) {
hooks.push(h);
log::debug!("hook registered: LOCATIONCHANGE (float-filtered)");
} else {
log::error!("failed to hook LOCATIONCHANGE");
}
hooks
}
fn is_valid_hook(h: HWINEVENTHOOK) -> bool {
!h.is_invalid()
}
unsafe extern "system" fn hook_callback(
_h_win_event_hook: HWINEVENTHOOK,
event: u32,
hwnd: HWND,
id_object: i32,
_id_child: i32,
_dw_event_thread: u32,
_dwms_event_time: u32,
) {
if id_object != OBJID_WINDOW {
return;
}
let sender = match HOOK_SENDER.get() {
Some(s) => s,
None => return,
};
let hwnd_val = hwnd.0 as isize;
if event == EVENT_OBJECT_LOCATIONCHANGE
&& (!FLOAT_TRACKING_ACTIVE.load(Ordering::Acquire) || !is_float_hwnd(hwnd_val))
{
return;
}
let hook_event = match event {
EVENT_OBJECT_CREATE => HookEvent::Created { hwnd: hwnd_val },
EVENT_OBJECT_DESTROY => HookEvent::Destroyed { hwnd: hwnd_val },
EVENT_SYSTEM_FOREGROUND => HookEvent::Foreground { hwnd: hwnd_val },
EVENT_SYSTEM_MINIMIZESTART => HookEvent::MinimizeStart { hwnd: hwnd_val },
EVENT_SYSTEM_MINIMIZEEND => HookEvent::MinimizeEnd { hwnd: hwnd_val },
EVENT_OBJECT_SHOW => HookEvent::Shown { hwnd: hwnd_val },
EVENT_OBJECT_HIDE => HookEvent::Hidden { hwnd: hwnd_val },
EVENT_OBJECT_STATECHANGE => HookEvent::StateChange { hwnd: hwnd_val },
EVENT_OBJECT_NAMECHANGE => HookEvent::NameChange { hwnd: hwnd_val },
EVENT_OBJECT_LOCATIONCHANGE => HookEvent::LocationChange { hwnd: hwnd_val },
_ => return, };
let _ = sender.send(hook_event);
if let Some(&raw) = HOOK_SIGNAL.get() {
let _ = unsafe { SetEvent(HANDLE(raw as *mut core::ffi::c_void)) };
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hook_event_shown_carries_hwnd() {
let ev = HookEvent::Shown { hwnd: 12345 };
match ev {
HookEvent::Shown { hwnd } => assert_eq!(hwnd, 12345),
_ => panic!("expected Shown"),
}
}
#[test]
fn hook_event_hidden_carries_hwnd() {
let ev = HookEvent::Hidden { hwnd: 67890 };
match ev {
HookEvent::Hidden { hwnd } => assert_eq!(hwnd, 67890),
_ => panic!("expected Hidden"),
}
}
#[test]
fn event_object_show_hide_constants_correct() {
assert_eq!(EVENT_OBJECT_SHOW, 0x8002);
assert_eq!(EVENT_OBJECT_HIDE, 0x8003);
const _: () = assert!(EVENT_OBJECT_SHOW < EVENT_OBJECT_HIDE);
}
#[test]
fn hook_event_name_change_carries_hwnd() {
let ev = HookEvent::NameChange { hwnd: 24680 };
match ev {
HookEvent::NameChange { hwnd } => assert_eq!(hwnd, 24680),
_ => panic!("expected NameChange"),
}
}
#[test]
fn hook_event_state_change_carries_hwnd() {
let ev = HookEvent::StateChange { hwnd: 13579 };
match ev {
HookEvent::StateChange { hwnd } => assert_eq!(hwnd, 13579),
_ => panic!("expected StateChange"),
}
}
#[test]
fn hook_event_location_change_carries_hwnd() {
let ev = HookEvent::LocationChange { hwnd: 11235 };
match ev {
HookEvent::LocationChange { hwnd } => assert_eq!(hwnd, 11235),
_ => panic!("expected LocationChange"),
}
}
#[test]
fn event_object_statechange_namechange_constants_correct() {
assert_eq!(EVENT_OBJECT_STATECHANGE, 0x800A);
assert_eq!(EVENT_OBJECT_NAMECHANGE, 0x800C);
assert_eq!(EVENT_OBJECT_LOCATIONCHANGE, 0x800B);
const _: () = assert!(EVENT_OBJECT_STATECHANGE < EVENT_OBJECT_LOCATIONCHANGE);
const _: () = assert!(EVENT_OBJECT_LOCATIONCHANGE < EVENT_OBJECT_NAMECHANGE);
}
#[test]
fn float_hwnd_set_add_remove_membership() {
FLOAT_HWNS.set(Mutex::new(HashSet::new())).ok();
add_float_hwnd(100);
add_float_hwnd(200);
assert!(is_float_hwnd(100));
assert!(is_float_hwnd(200));
assert!(!is_float_hwnd(300));
remove_float_hwnd(100);
assert!(!is_float_hwnd(100));
set_float_hwnds(&[300, 400]);
assert!(!is_float_hwnd(200));
assert!(is_float_hwnd(300));
assert!(is_float_hwnd(400));
let snap = float_hwnds_snapshot();
assert_eq!(snap.len(), 2);
assert!(snap.contains(&300));
assert!(snap.contains(&400));
}
#[test]
fn float_tracking_active_flag_round_trip() {
let original = float_tracking_active();
set_float_tracking_active(false);
assert!(!float_tracking_active());
set_float_tracking_active(true);
assert!(float_tracking_active());
set_float_tracking_active(original);
}
}