use std::sync::OnceLock;
use windows::Win32::{
Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM},
UI::{
Controls::WC_STATIC,
WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DeregisterShellHookWindow, DestroyWindow,
DispatchMessageW, GWLP_USERDATA, GWLP_WNDPROC, GetMessageW, GetWindowLongPtrW,
HWND_MESSAGE, MSG, RegisterShellHookWindow, RegisterWindowMessageW, SHELLHOOKINFO,
SetWindowLongPtrW, TranslateMessage, WINDOW_EX_STYLE, WINDOW_STYLE,
},
},
};
use windows::core::w;
use crate::log::*;
pub use windows::Win32::UI::WindowsAndMessaging::{
HSHELL_ACCESSIBILITYSTATE, HSHELL_ACTIVATESHELLWINDOW, HSHELL_APPCOMMAND, HSHELL_ENDTASK,
HSHELL_GETMINRECT, HSHELL_HIGHBIT, HSHELL_LANGUAGE, HSHELL_MONITORCHANGED, HSHELL_REDRAW,
HSHELL_SYSMENU, HSHELL_TASKMAN, HSHELL_WINDOWACTIVATED, HSHELL_WINDOWCREATED,
HSHELL_WINDOWDESTROYED, HSHELL_WINDOWREPLACED, HSHELL_WINDOWREPLACING,
};
pub const HSHELL_RUDEAPPACTIVATED: u32 = HSHELL_WINDOWACTIVATED | HSHELL_HIGHBIT;
pub const HSHELL_FLASH: u32 = HSHELL_REDRAW | HSHELL_HIGHBIT;
pub type ShellHookCallback = dyn FnMut(ShellHookMessage) -> bool + Send + 'static;
#[derive(Debug, Clone, Copy)]
pub enum ShellHookMessage {
WindowCreated(HWND),
WindowDestroyed(HWND),
ActivateShellWindow,
WindowActivated(HWND),
RudeAppActivated(HWND),
GetMinRect(HWND, RECT),
TaskMan(LPARAM),
Language(HWND),
SysMenu(LPARAM),
EndTask(HWND),
AccessibilityState(LPARAM),
Redraw(HWND),
Flash(HWND),
AppCommand(LPARAM),
WindowReplaced(HWND),
WindowReplacing(HWND),
MonitorChanged(HWND),
Unknown(WPARAM, LPARAM),
}
impl From<(WPARAM, LPARAM)> for ShellHookMessage {
fn from(value: (WPARAM, LPARAM)) -> Self {
let (wparam, lparam) = value;
match wparam.0 as u32 {
HSHELL_WINDOWCREATED => Self::WindowCreated(HWND(lparam.0 as _)),
HSHELL_WINDOWDESTROYED => Self::WindowDestroyed(HWND(lparam.0 as _)),
HSHELL_ACTIVATESHELLWINDOW => Self::ActivateShellWindow,
HSHELL_WINDOWACTIVATED => Self::WindowActivated(HWND(lparam.0 as _)),
HSHELL_RUDEAPPACTIVATED => Self::RudeAppActivated(HWND(lparam.0 as _)),
HSHELL_GETMINRECT => {
let info = unsafe { &*(lparam.0 as *const SHELLHOOKINFO) };
Self::GetMinRect(info.hwnd, info.rc)
}
HSHELL_TASKMAN => Self::TaskMan(lparam),
HSHELL_LANGUAGE => Self::Language(HWND(lparam.0 as _)),
HSHELL_SYSMENU => Self::SysMenu(lparam),
HSHELL_ENDTASK => Self::EndTask(HWND(lparam.0 as _)),
HSHELL_ACCESSIBILITYSTATE => Self::AccessibilityState(lparam),
HSHELL_REDRAW => Self::Redraw(HWND(lparam.0 as _)),
HSHELL_FLASH => Self::Flash(HWND(lparam.0 as _)),
HSHELL_APPCOMMAND => Self::AppCommand(lparam),
HSHELL_WINDOWREPLACED => Self::WindowReplaced(HWND(lparam.0 as _)),
HSHELL_WINDOWREPLACING => Self::WindowReplacing(HWND(lparam.0 as _)),
HSHELL_MONITORCHANGED => Self::MonitorChanged(HWND(lparam.0 as _)),
_ => Self::Unknown(wparam, lparam),
}
}
}
pub struct ShellHook {
_thread: Option<std::thread::JoinHandle<()>>,
hwnd: OnceLock<usize>,
}
impl ShellHook {
pub fn new(callback: Box<ShellHookCallback>) -> windows::core::Result<Self> {
Self::with_on_hooked(callback, |_| ())
}
pub fn with_on_hooked(
mut callback: Box<ShellHookCallback>,
on_hooked: impl FnOnce(&mut ShellHookCallback) + Send + 'static,
) -> windows::core::Result<Self> {
let hwnd = OnceLock::new();
let _thread = std::thread::spawn({
let hwnd_store = hwnd.clone();
move || {
let class_name = WC_STATIC;
let hwnd = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
class_name,
w!("ShellHookWindow"),
WINDOW_STYLE::default(),
0,
0,
0,
0,
Some(HWND_MESSAGE),
None,
None,
None,
)
}
.unwrap();
if hwnd.0.is_null() {
error!("Failed to create shell hook window");
return;
}
let _ = hwnd_store.set(hwnd.0 as usize);
let callback_ref = callback.as_mut() as *mut _;
let callback_ptr = Box::into_raw(Box::new(callback)) as isize;
unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, callback_ptr) };
unsafe { SetWindowLongPtrW(hwnd, GWLP_WNDPROC, window_proc as *const () as isize) };
if !unsafe { RegisterShellHookWindow(hwnd) }.as_bool() {
error!("Failed to register shell hook window");
return;
}
debug!("Shell hook window created: {:?}", hwnd);
on_hooked(unsafe { &mut *callback_ref });
let mut msg = MSG::default();
while unsafe { GetMessageW(&mut msg, None, 0, 0).as_bool() } {
let _ = unsafe { TranslateMessage(&msg) };
let _ = unsafe { DispatchMessageW(&msg) };
}
}
});
Ok(ShellHook {
_thread: Some(_thread),
hwnd,
})
}
pub fn hwnd(&self) -> Option<HWND> {
self.hwnd.get().map(|&h| HWND(h as _))
}
}
impl Drop for ShellHook {
fn drop(&mut self) {
if let Some(hwnd) = self.hwnd() {
_ = unsafe { DeregisterShellHookWindow(hwnd) };
unsafe {
let callback_ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA);
if callback_ptr != 0 {
_ = Box::from_raw(callback_ptr as *mut Box<ShellHookCallback>);
}
}
_ = unsafe { DestroyWindow(hwnd) };
}
}
}
static SHELL_HOOK_MSG: OnceLock<u32> = OnceLock::new();
fn shell_hook_msg() -> u32 {
*SHELL_HOOK_MSG.get_or_init(|| unsafe { RegisterWindowMessageW(w!("SHELLHOOK")) })
}
unsafe extern "system" fn window_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
if msg == shell_hook_msg() {
let callback = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) };
if callback != 0 {
let callback = unsafe { &mut *(callback as *mut Box<ShellHookCallback>) };
let r = callback(ShellHookMessage::from((wparam, lparam)));
return LRESULT(r as _);
}
}
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
#[cfg(test)]
mod tests {
use super::*;
use std::{thread, time::Duration};
#[test]
fn shell_hook() {
println!("Testing ShellHook - perform various window operations to see events");
let hook = ShellHook::new(Box::new(|msg: ShellHookMessage| {
println!("{msg:?}");
false
}))
.expect("Failed to create shell hook");
println!("Shell hook registered with hwnd={:?}", hook.hwnd());
println!("Test will complete in 1 seconds...\n");
thread::sleep(Duration::from_secs(1));
drop(hook);
println!("\nShell hook destroyed.");
}
#[ignore]
#[test]
fn shell_hook_manual() {
println!("Testing ShellHook - perform various window operations to see events");
let hook = ShellHook::new(Box::new(|msg: ShellHookMessage| {
println!("{msg:?}");
false
}))
.expect("Failed to create shell hook");
println!("Shell hook registered with hwnd={:?}", hook.hwnd());
println!("Perform window operations (open/close apps, alt+tab, etc.) to see events...");
println!("Test will complete in 30 seconds...\n");
thread::sleep(Duration::from_secs(30));
drop(hook);
println!("\nShell hook destroyed.");
}
}