ib_hook/windows/shell.rs
1/*!
2Monitor window operations: creating, activating, title redrawing, monitor changing...
3
4Installs a hook procedure that receives notifications useful to Windows shell applications.
5
6See:
7- [ShellProc callback function](https://learn.microsoft.com/en-us/windows/win32/winmsg/shellproc#parameters)
8- [RegisterShellHookWindow function (winuser.h)](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registershellhookwindow#remarks)
9- [Shell Events (a.k.a. Shell Hooks) - zhuman/ShellReplacement](https://github.com/zhuman/ShellReplacement/blob/master/wiki/ShellEvents.md)
10
11## Examples
12```no_run
13use ib_hook::windows::shell::{ShellHook, ShellHookMessage};
14{
15 let hook = ShellHook::new(Box::new(|msg: ShellHookMessage| {
16 println!("{msg:?}");
17 false
18 }))
19 .unwrap();
20
21 // Perform window operations to see received events...
22 std::thread::sleep(std::time::Duration::from_secs(30));
23}
24// Shell hook unregistered
25```
26
27## Disclaimer
28Ref:
29- https://github.com/YousefAliUK/FerroDock/blob/b405832a64c763f073b37d9a42a0690d0c15416b/src/events.rs
30- https://gist.github.com/Aetopia/347e7329158aa2c69df97bdf0b761d6f
31*/
32use std::sync::{Once, OnceLock};
33
34use windows::Win32::{
35 Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM},
36 UI::WindowsAndMessaging::{
37 CreateWindowExW, DefWindowProcW, DeregisterShellHookWindow, DestroyWindow,
38 DispatchMessageW, GWLP_USERDATA, GetMessageW, GetWindowLongPtrW, HWND_MESSAGE, MSG,
39 RegisterClassW, RegisterShellHookWindow, RegisterWindowMessageW, SHELLHOOKINFO,
40 SetWindowLongPtrW, TranslateMessage, WINDOW_EX_STYLE, WINDOW_STYLE, WNDCLASSW,
41 },
42};
43use windows::core::w;
44
45use crate::{log::*, process::module::Module};
46
47pub use windows::Win32::UI::WindowsAndMessaging::{
48 HSHELL_ACCESSIBILITYSTATE, HSHELL_ACTIVATESHELLWINDOW, HSHELL_APPCOMMAND, HSHELL_ENDTASK,
49 HSHELL_GETMINRECT, HSHELL_HIGHBIT, HSHELL_LANGUAGE, HSHELL_MONITORCHANGED, HSHELL_REDRAW,
50 HSHELL_SYSMENU, HSHELL_TASKMAN, HSHELL_WINDOWACTIVATED, HSHELL_WINDOWCREATED,
51 HSHELL_WINDOWDESTROYED, HSHELL_WINDOWREPLACED, HSHELL_WINDOWREPLACING,
52};
53
54// Missing shell hook constants from the windows crate
55pub const HSHELL_RUDEAPPACTIVATED: u32 = HSHELL_WINDOWACTIVATED | HSHELL_HIGHBIT;
56pub const HSHELL_FLASH: u32 = HSHELL_REDRAW | HSHELL_HIGHBIT;
57
58/// The return value should be `false` unless the message is [`ShellHookMessage::AppCommand`]
59/// and the callback handles the [`WM_COMMAND`] message. In this case, the return should be `true`.
60pub type ShellHookCallback = dyn FnMut(ShellHookMessage) -> bool + Send + 'static;
61
62/// Shell hook message variants.
63///
64/// These correspond to the shell hook messages sent via [`RegisterShellHookWindow`].
65///
66/// Ref:
67/// - [ShellProc callback function - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/winmsg/shellproc#parameters)
68/// - [RegisterShellHookWindow function (winuser.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registershellhookwindow#remarks)
69#[derive(Debug, Clone, Copy)]
70pub enum ShellHookMessage {
71 /// A top-level, unowned window has been created.
72 /// The window exists when the system calls this hook.
73 ///
74 /// A handle to the window being created.
75 WindowCreated(HWND),
76
77 /// A top-level, unowned window is about to be destroyed.
78 /// The window still exists when the system calls this hook.
79 ///
80 /// A handle to the top-level window being destroyed.
81 WindowDestroyed(HWND),
82
83 /// The shell should activate its main window.
84 ActivateShellWindow,
85
86 /// The activation has changed to a different top-level, unowned window.
87 ///
88 /// A handle to the activated window.
89 WindowActivated(HWND),
90
91 /// The activation has changed to a different top-level, unowned window in full-screen mode.
92 ///
93 /// A handle to the activated window.
94 ///
95 /// Ref: [c# - Does anybody know what means ShellHook message `HSHELL_RUDEAPPACTIVATED`? - Stack Overflow](https://stackoverflow.com/questions/1178020/does-anybody-know-what-means-shellhook-message-hshell-rudeappactivated)
96 RudeAppActivated(HWND),
97
98 /// A window is being minimized or maximized.
99 /// The system needs the coordinates of the minimized rectangle for the window.
100 ///
101 /// - A handle to the minimized or maximized window.
102 /// - A pointer to a RECT structure.
103 GetMinRect(HWND, RECT),
104
105 /// The user has selected the task list.
106 /// A shell application that provides a task list should return `TRUE` to prevent Windows from starting its task list.
107 ///
108 /// The param can be ignored.
109 TaskMan(LPARAM),
110
111 /// Keyboard language was changed or a new keyboard layout was loaded.
112 ///
113 /// - A handle to the window.
114 /// - A handle to a keyboard layout.
115 ///
116 /// May require DLL hook.
117 Language(HWND),
118
119 /// May require DLL hook.
120 SysMenu(LPARAM),
121
122 /// A handle to the window that should be forced to exit.
123 EndTask(HWND),
124
125 /// The accessibility state has changed.
126 ///
127 /// Indicates which accessibility feature has changed state.
128 /// This value is one of the following: `ACCESS_FILTERKEYS`, `ACCESS_MOUSEKEYS`, or `ACCESS_STICKYKEYS`.
129 ///
130 /// May require DLL hook.
131 AccessibilityState(LPARAM),
132
133 /// The title of a window in the task bar has been redrawn.
134 ///
135 /// A handle to the window that needs to be redrawn.
136 Redraw(HWND),
137
138 /// A handle to the window that needs to be flashed.
139 Flash(HWND),
140
141 /// The user completed an input event (for example, pressed an application command button on the mouse or an application command key on the keyboard),
142 /// and the application did not handle the [`WM_APPCOMMAND`] message generated by that input.
143 ///
144 /// - The [`APPCOMMAND`] which has been unhandled by the application or other hooks.
145 AppCommand(LPARAM),
146
147 /// A top-level window is being replaced.
148 /// The window exists when the system calls this hook.
149 ///
150 /// A handle to the window being replaced.
151 WindowReplaced(HWND),
152
153 /// A handle to the window replacing the top-level window.
154 WindowReplacing(HWND),
155
156 /// A handle to the window that moved to a different monitor.
157 MonitorChanged(HWND),
158
159 /// Unknown shell hook message.
160 Unknown(WPARAM, LPARAM),
161}
162
163impl From<(WPARAM, LPARAM)> for ShellHookMessage {
164 fn from(value: (WPARAM, LPARAM)) -> Self {
165 let (wparam, lparam) = value;
166 match wparam.0 as u32 {
167 HSHELL_WINDOWCREATED => Self::WindowCreated(HWND(lparam.0 as _)),
168 HSHELL_WINDOWDESTROYED => Self::WindowDestroyed(HWND(lparam.0 as _)),
169 HSHELL_ACTIVATESHELLWINDOW => Self::ActivateShellWindow,
170 HSHELL_WINDOWACTIVATED => Self::WindowActivated(HWND(lparam.0 as _)),
171 HSHELL_RUDEAPPACTIVATED => Self::RudeAppActivated(HWND(lparam.0 as _)),
172 HSHELL_GETMINRECT => {
173 let info = unsafe { &*(lparam.0 as *const SHELLHOOKINFO) };
174 Self::GetMinRect(info.hwnd, info.rc)
175 }
176 HSHELL_TASKMAN => Self::TaskMan(lparam),
177 HSHELL_LANGUAGE => Self::Language(HWND(lparam.0 as _)),
178 HSHELL_SYSMENU => Self::SysMenu(lparam),
179 HSHELL_ENDTASK => Self::EndTask(HWND(lparam.0 as _)),
180 HSHELL_ACCESSIBILITYSTATE => Self::AccessibilityState(lparam),
181 HSHELL_REDRAW => Self::Redraw(HWND(lparam.0 as _)),
182 HSHELL_FLASH => Self::Flash(HWND(lparam.0 as _)),
183 HSHELL_APPCOMMAND => Self::AppCommand(lparam),
184 HSHELL_WINDOWREPLACED => Self::WindowReplaced(HWND(lparam.0 as _)),
185 HSHELL_WINDOWREPLACING => Self::WindowReplacing(HWND(lparam.0 as _)),
186 HSHELL_MONITORCHANGED => Self::MonitorChanged(HWND(lparam.0 as _)),
187 _ => Self::Unknown(wparam, lparam),
188 }
189 }
190}
191
192pub struct ShellHook {
193 _thread: Option<std::thread::JoinHandle<()>>,
194 hwnd: OnceLock<usize>,
195}
196
197impl ShellHook {
198 pub fn new(callback: Box<ShellHookCallback>) -> windows::core::Result<Self> {
199 Self::with_on_hooked(callback, |_| ())
200 }
201
202 pub fn with_on_hooked(
203 mut callback: Box<ShellHookCallback>,
204 on_hooked: impl FnOnce(&mut ShellHookCallback) + Send + 'static,
205 ) -> windows::core::Result<Self> {
206 let hwnd = OnceLock::new();
207
208 // Start the message loop in a separate thread
209 let _thread = std::thread::spawn({
210 let hwnd_store = hwnd.clone();
211 move || {
212 /*
213 let shell_msg = unsafe { RegisterWindowMessageW(w!("SHELLHOOK")) };
214 SHELL_HOOK_MSG.set(shell_msg).ok();
215 */
216
217 let class_name = w!("ib_hook::shell");
218
219 let wc = WNDCLASSW {
220 lpfnWndProc: Some(window_proc),
221 hInstance: Module::current().0.into(),
222 lpszClassName: class_name,
223 ..Default::default()
224 };
225
226 // Only `RegisterClass` once
227 CLASS_REGISTER.call_once(|| {
228 if unsafe { RegisterClassW(&wc) } == 0 {
229 error!("Failed to register window class");
230 }
231 });
232
233 let hwnd = unsafe {
234 CreateWindowExW(
235 WINDOW_EX_STYLE::default(),
236 class_name,
237 w!("ShellHookWindow"),
238 WINDOW_STYLE::default(),
239 0,
240 0,
241 0,
242 0,
243 // Message-only window
244 Some(HWND_MESSAGE),
245 None,
246 Some(wc.hInstance),
247 None,
248 )
249 }
250 .unwrap();
251
252 if hwnd.0.is_null() {
253 error!("Failed to create shell hook window");
254 return;
255 }
256
257 // Store hwnd as usize in OnceLock
258 let _ = hwnd_store.set(hwnd.0 as usize);
259
260 // Set callback in window user data
261 let callback_ref = callback.as_mut() as *mut _;
262 let callback_ptr = Box::into_raw(Box::new(callback)) as isize;
263 unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, callback_ptr) };
264
265 if !unsafe { RegisterShellHookWindow(hwnd) }.as_bool() {
266 error!("Failed to register shell hook window");
267 return;
268 }
269
270 debug!("Shell hook window created: {:?}", hwnd);
271
272 // SAFETY: Callback will only be called in DispatchMessageW()
273 on_hooked(unsafe { &mut *callback_ref });
274
275 // Run the message loop
276 let mut msg = MSG::default();
277 while unsafe { GetMessageW(&mut msg, None, 0, 0).as_bool() } {
278 let _ = unsafe { TranslateMessage(&msg) };
279 let _ = unsafe { DispatchMessageW(&msg) };
280 }
281 }
282 });
283
284 Ok(ShellHook {
285 _thread: Some(_thread),
286 hwnd,
287 })
288 }
289
290 pub fn hwnd(&self) -> Option<HWND> {
291 self.hwnd.get().map(|&h| HWND(h as _))
292 }
293}
294
295impl Drop for ShellHook {
296 fn drop(&mut self) {
297 if let Some(hwnd) = self.hwnd() {
298 // Unregister from shell hook messages
299 _ = unsafe { DeregisterShellHookWindow(hwnd) };
300
301 // Clean up the callback
302 unsafe {
303 let callback_ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA);
304 if callback_ptr != 0 {
305 _ = Box::from_raw(callback_ptr as *mut Box<ShellHookCallback>);
306 }
307 }
308
309 // Destroy the window
310 _ = unsafe { DestroyWindow(hwnd) };
311 }
312 }
313}
314
315static SHELL_HOOK_MSG: OnceLock<u32> = OnceLock::new();
316
317fn shell_hook_msg() -> u32 {
318 *SHELL_HOOK_MSG.get_or_init(|| unsafe { RegisterWindowMessageW(w!("SHELLHOOK")) })
319}
320
321static CLASS_REGISTER: Once = Once::new();
322
323/// The return value should be zero unless the value of nCode is [`HSHELL_APPCOMMAND`]
324/// and the shell procedure handles the [`WM_COMMAND`] message. In this case, the return should be nonzero.
325unsafe extern "system" fn window_proc(
326 hwnd: HWND,
327 msg: u32,
328 wparam: WPARAM,
329 lparam: LPARAM,
330) -> LRESULT {
331 if msg == shell_hook_msg() {
332 let callback = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) };
333 if callback != 0 {
334 let callback = unsafe { &mut *(callback as *mut Box<ShellHookCallback>) };
335 let r = callback(ShellHookMessage::from((wparam, lparam)));
336 return LRESULT(r as _);
337 }
338 }
339
340 unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use std::{thread, time::Duration};
347
348 #[test]
349 fn shell_hook() {
350 println!("Testing ShellHook - perform various window operations to see events");
351
352 let hook = ShellHook::new(Box::new(|msg: ShellHookMessage| {
353 println!("{msg:?}");
354 false
355 }))
356 .expect("Failed to create shell hook");
357
358 println!("Shell hook registered with hwnd={:?}", hook.hwnd());
359 println!("Test will complete in 1 seconds...\n");
360
361 // Keep the hook alive for a bit to receive events
362 thread::sleep(Duration::from_secs(1));
363
364 // Drop hook explicitly to demonstrate cleanup
365 drop(hook);
366 println!("\nShell hook destroyed.");
367 }
368
369 #[ignore]
370 #[test]
371 fn shell_hook_manual() {
372 println!("Testing ShellHook - perform various window operations to see events");
373
374 let hook = ShellHook::new(Box::new(|msg: ShellHookMessage| {
375 println!("{msg:?}");
376 false
377 }))
378 .expect("Failed to create shell hook");
379
380 println!("Shell hook registered with hwnd={:?}", hook.hwnd());
381 println!("Perform window operations (open/close apps, alt+tab, etc.) to see events...");
382 println!("Test will complete in 30 seconds...\n");
383
384 // Keep the hook alive for a bit to receive events
385 thread::sleep(Duration::from_secs(30));
386
387 // Drop hook explicitly to demonstrate cleanup
388 drop(hook);
389 println!("\nShell hook destroyed.");
390 }
391}