wintrack/lib.rs
1#![cfg(windows)]
2#![warn(missing_docs)]
3
4//! A library for monitoring window related events on Windows.
5//!
6//! This crate allows you to set a callback that will be called for common window events.
7//! The callback receives a [`WindowEvent`], which includes the [event kind](WindowEventKind) and
8//! a [snapshot of the window's state](WindowSnapshot) at the time of the event.
9//!
10//! This library allows you to listen for the following events:
11//! - [Foreground (active) window changed](WindowEventKind::ForegroundWindowChanged)
12//! - [Window title or name changed](WindowEventKind::WindowNameChanged)
13//! - [Window became visible (unminimized / moved onscreen)](WindowEventKind::WindowBecameVisible)
14//! - [Window became hidden (minimized / moved offscreen)](WindowEventKind::WindowBecameHidden)
15//! - [New window was created](WindowEventKind::WindowCreated)
16//! - [Window was destroyed or closed](WindowEventKind::WindowDestroyed)
17//! - [Window was moved or resized](WindowEventKind::WindowMovedOrResized)
18//!
19//! The [snapshot](WindowSnapshot) contains fields including
20//! title, rect, executable, and some lower level information.
21//!
22//! # Usage
23//! First, call [`try_hook`], which spawns a thread that sets a hook & listens for events.
24//! ```no_run
25//! wintrack::try_hook().expect("no hook should be set yet");
26//! ```
27//! Then, define a callback that will be called for each event.
28//! ```no_run
29//! wintrack::set_callback(Box::new(|evt| {
30//! // ignore events from zero-sized windows or windows with no title
31//! if evt.snapshot.rect.size() != (0, 0) && !evt.snapshot.title.is_empty() {
32//! dbg!(evt.snapshot);
33//! }
34//! }));
35//! ```
36//! At the end of your program, optionally [`unhook`] the hook.
37//! ```no_run
38//! wintrack::unhook().expect("should have set hook earlier")
39//! ```
40//!
41//! For an example of using a channel to collect events or listen for events in another location of your program,
42//! see the example in the documentation for [`set_callback`].
43
44#[cfg(not(windows))]
45compile_error!("The `wintrack` crate only supports Windows.");
46
47use std::panic;
48use std::thread::JoinHandle;
49use parking_lot::Mutex;
50use windows::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_INVALID_PARAMETER, ERROR_INVALID_THREAD_ID, ERROR_INVALID_WINDOW_HANDLE, ERROR_MOD_NOT_FOUND, ERROR_PROC_NOT_FOUND, HWND, LPARAM, WPARAM};
51use windows::core::{Error as WinErr, BOOL};
52use windows::Win32::System::Threading::GetCurrentThreadId;
53use windows::Win32::UI::Accessibility::{SetWinEventHook, HWINEVENTHOOK};
54use windows::Win32::UI::WindowsAndMessaging::{GetMessageW, PostThreadMessageW, CHILDID_SELF, EVENT_OBJECT_CREATE, EVENT_OBJECT_DESTROY, EVENT_OBJECT_HIDE, EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_NAMECHANGE, EVENT_OBJECT_SHOW, EVENT_SYSTEM_FOREGROUND, MSG, OBJECT_IDENTIFIER, OBJID_WINDOW, WINEVENT_OUTOFCONTEXT, WM_QUIT};
55pub use window_info::WinThreadId;
56
57mod window_info;
58pub use window_info::*;
59
60/// A window event.
61///
62/// Represents an event related to a window, like becoming foreground or title change,
63/// along with a [snapshot](WindowSnapshot) of the window when the event occurred.
64///
65/// Most likely, you'll get a window event from the callback set by [`set_callback`].
66///
67/// # Examples
68/// ```no_run
69/// # use wintrack::{WindowEvent, WindowEventKind, WindowSnapshot};
70/// wintrack::set_callback(Box::new(|event: WindowEvent| {
71/// // every event has a snapshot of the window's current state
72/// let snapshot: WindowSnapshot = event.snapshot;
73/// assert_eq!(snapshot.title, "Firefox");
74/// assert_eq!(snapshot.class_name, "MozillaWindowClass");
75///
76/// // ... and the kind of event that caused it
77/// if event.kind == WindowEventKind::ForegroundWindowChanged {
78/// assert!(snapshot.is_foreground);
79/// }
80/// }));
81/// ```
82#[derive(Clone, Debug, Eq, PartialEq, Hash)]
83pub struct WindowEvent {
84 /// The specific type of event that occurred.
85 pub kind: WindowEventKind,
86 /// A snapshot of the window's properties when the event occurred.
87 pub snapshot: WindowSnapshot,
88}
89
90/// The kind of the event that occurred for a window.
91///
92/// Each corresponds to a [Windows event constant](https://learn.microsoft.com/en-us/windows/win32/winauto/event-constants).
93#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
94pub enum WindowEventKind {
95 /// The foreground (active) window changed. ([`EVENT_SYSTEM_FOREGROUND`])
96 ForegroundWindowChanged,
97 /// The window title or name changed. ([`EVENT_OBJECT_NAMECHANGE`])
98 WindowNameChanged,
99 /// The window became visible (unminimized / moved onscreen). ([`EVENT_OBJECT_SHOW`])
100 WindowBecameVisible,
101 /// The window became hidden (minimized / moved offscreen). ([`EVENT_OBJECT_HIDE`])
102 WindowBecameHidden,
103 /// A new window was created. ([`EVENT_OBJECT_CREATE`])
104 WindowCreated,
105 /// A window was destroyed or closed. ([`EVENT_OBJECT_DESTROY`])
106 WindowDestroyed,
107 /// A window was moved or resized. ([`EVENT_OBJECT_LOCATIONCHANGE`])
108 WindowMovedOrResized,
109}
110
111impl WindowEventKind {
112 pub(crate) const ALL: [Self; 7] = [
113 Self::ForegroundWindowChanged,
114 Self::WindowNameChanged,
115 Self::WindowBecameVisible,
116 Self::WindowBecameHidden,
117 Self::WindowCreated,
118 Self::WindowDestroyed,
119 Self::WindowMovedOrResized,
120 ];
121
122 pub(crate) fn from_event_constant(event: u32) -> Option<Self> {
123 let ret = match event {
124 EVENT_SYSTEM_FOREGROUND => Some(Self::ForegroundWindowChanged),
125 EVENT_OBJECT_NAMECHANGE => Some(Self::WindowNameChanged),
126 EVENT_OBJECT_SHOW => Some(Self::WindowBecameVisible),
127 EVENT_OBJECT_HIDE => Some(Self::WindowBecameHidden),
128 EVENT_OBJECT_CREATE => Some(Self::WindowCreated),
129 EVENT_OBJECT_DESTROY => Some(Self::WindowDestroyed),
130 EVENT_OBJECT_LOCATIONCHANGE => Some(Self::WindowMovedOrResized),
131 _ => None,
132 };
133
134 // FIXME: move this to a test
135 if let Some(ret) = ret {
136 debug_assert_eq!(ret.event_constant(), event);
137 }
138
139 ret
140 }
141
142 pub(crate) fn event_constant(self) -> u32 {
143 match self {
144 Self::ForegroundWindowChanged => EVENT_SYSTEM_FOREGROUND,
145 Self::WindowNameChanged => EVENT_OBJECT_NAMECHANGE,
146 Self::WindowBecameVisible => EVENT_OBJECT_SHOW,
147 Self::WindowBecameHidden => EVENT_OBJECT_HIDE,
148 Self::WindowCreated => EVENT_OBJECT_CREATE,
149 Self::WindowDestroyed => EVENT_OBJECT_DESTROY,
150 Self::WindowMovedOrResized => EVENT_OBJECT_LOCATIONCHANGE,
151 }
152 }
153}
154
155unsafe extern "system" fn win_event_proc(
156 _h_win_event_hook: HWINEVENTHOOK,
157 event: u32,
158 hwnd: HWND,
159 id_object: i32,
160 id_child: i32,
161 _dw_event_thread: u32,
162 _dwms_event_time: u32,
163) {
164 if OBJECT_IDENTIFIER(id_object) == OBJID_WINDOW && id_child == CHILDID_SELF as _ {
165 let Some(kind) = WindowEventKind::from_event_constant(event) else {
166 return;
167 };
168
169 let snapshot = match WindowSnapshot::from_hwnd(hwnd) {
170 Ok(snapshot) => snapshot,
171 Err(_err) => {
172 // eprintln!("{err}"); // TODO: log error!
173 return;
174 }
175 };
176
177 if let Some(callback) = &STATE.lock().callback {
178 callback(WindowEvent { kind, snapshot });
179 }
180 }
181}
182
183/// A boxed closure/function pointer that provides [`WindowEvent`]s.
184pub type WindowEventCallback = Box<dyn Fn(WindowEvent) + Send>;
185
186struct WinHookState {
187 pub callback: Option<WindowEventCallback>,
188 pub thread: Option<(JoinHandle<Result<(), WinErr>>, WinThreadId)>,
189}
190
191static STATE: Mutex<WinHookState> = Mutex::new(WinHookState { callback: None, thread: None });
192
193/// Error returned by [`try_hook`].
194///
195/// Most likely, this will be caused by attempting to hook when a hook is already set,
196/// but in rare cases an error from the Win32 API may occur.
197#[derive(Debug, thiserror::Error, Eq, PartialEq)]
198pub enum TryHookError {
199 /// A hook was already previously set by this process.
200 /// To set another hook, first [unhook the previously set hook](unhook).
201 #[error("Hook already set; no need to set it again.")]
202 HookAlreadySet,
203 /// Internal error from Win32 API.
204 #[error("Failed to set hook: {0}")]
205 FailedToSetHook(WinErr),
206}
207
208/// Attempts to install a hook for monitoring [window events](WindowEvent).
209///
210/// Near this call (either before or after), you probably want to call [`set_callback`] to do something whenever the hook receives an event.
211/// Only one hook can be set at a time; attempting to set another hook will return [`TryHookError::HookAlreadySet`].
212pub fn try_hook() -> Result<(), TryHookError> {
213 let mut state = STATE.lock();
214
215 if state.thread.is_some() {
216 Err(TryHookError::HookAlreadySet)
217 } else {
218 match hook_inner() {
219 Ok(thread_id) => {
220 state.thread = Some(thread_id);
221
222 Ok(())
223 },
224 Err(err) => Err(TryHookError::FailedToSetHook(err)),
225 }
226 }
227}
228
229fn hook_inner() -> Result<(JoinHandle<Result<(), WinErr>>, WinThreadId), WinErr> {
230 let (tx, rx) = oneshot::channel();
231
232 let handle = std::thread::spawn(move || {
233 let event_const = WindowEventKind::ALL.map(WindowEventKind::event_constant);
234
235 let min = *event_const.iter().min().expect("should be at least one event kind");
236 let max = *event_const.iter().max().expect("should be at least one event kind");
237
238 // SAFETY: callback signature is correct, callback cannot capture locals due to being a fn ptr,
239 // event range is valid, thread will set up event loop
240 let hook = unsafe {
241 SetWinEventHook(min, max, None, Some(win_event_proc), 0, 0, WINEVENT_OUTOFCONTEXT)
242 };
243
244 let res = if hook.is_invalid() {
245 match WinErr::from_win32() {
246 err if err == WinErr::from(ERROR_INVALID_PARAMETER) => unreachable!("SetWinEventHook parameters should be correct"),
247 err if err == WinErr::from(ERROR_MOD_NOT_FOUND) => unreachable!("hmodwineventproc is null, so never should trigger this error"),
248 err if err == WinErr::from(ERROR_INVALID_THREAD_ID) => unreachable!("idthread is 0, so never should trigger this error"),
249 err if err == WinErr::from(ERROR_INVALID_FUNCTION) => unreachable!("function should have right signature & calling abi"),
250 err if err == WinErr::from(ERROR_PROC_NOT_FOUND) => unreachable!("not using a DLL"),
251 err => Err(err)
252 }
253 } else {
254 // SAFETY: always safe to call
255 let thread_id = unsafe { GetCurrentThreadId() };
256
257 Ok(WinThreadId::new(thread_id).expect("thread id should always be nonzero"))
258 };
259
260 tx.send(res).expect("rx should still exist");
261
262 let mut msg = MSG::default();
263
264 // SAFETY: msg is non-null & valid to write to (unique ptr due to &mut),
265 // and thread has a message queue to read from
266 match unsafe { GetMessageW(&mut msg, None, 0, 0) } {
267 BOOL(0) => {
268 assert_eq!(
269 msg.message, WM_QUIT,
270 // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessagew
271 "If the function retrieves a message other than WM_QUIT, the return value is nonzero."
272 );
273
274 Ok(())
275 }
276 BOOL(-1) => match WinErr::from_win32() {
277 err if err == WinErr::from(ERROR_INVALID_WINDOW_HANDLE) => unreachable!("shouldn't trigger since hwnd is None"),
278 err if err == WinErr::from(ERROR_INVALID_PARAMETER) => unreachable!("should be calling GetMessageW with correct params"),
279 err => Err(err),
280 },
281 bool => unreachable!("message queue should not recv any other messages ({:?}, msg: {})", bool, msg.message),
282 }
283
284 // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unhookwinevent
285 // "If the client's thread ends, the system automatically calls [UnhookWinEvent]"
286 });
287
288 rx.recv()
289 .expect("should eventually recv a message")
290 .map(|id| (handle, id))
291}
292
293/// Sets the callback called when a [window event](WindowEvent) occurs.
294///
295/// This function is how you can define what should happen upon a window event.
296/// Returns a previously set callback if it exists.
297///
298/// If you need to listen for the events in another location in your program,
299/// or need to collect them, you might want to set up a [channel](std::sync::mpsc).
300///
301/// # Panics
302/// If the callback provided ever panics, the program will panic as expected.
303///
304/// # Examples
305/// Debug print all* events:
306/// ```no_run
307/// wintrack::set_callback(Box::new(|evt| {
308/// // ignore events from zero-sized windows or windows with no title
309/// if evt.snapshot.rect.size() != (0, 0) && !evt.snapshot.title.is_empty() {
310/// dbg!(evt.snapshot);
311/// }
312/// }));
313/// ```
314/// Using a channel:
315/// ```no_run
316/// # use std::ffi::OsStr;
317/// # use wintrack::WindowEventKind;
318/// use std::sync::mpsc;
319///
320/// wintrack::try_hook().expect("hook should not be set yet");
321///
322/// let (tx, rx) = mpsc::channel();
323///
324/// wintrack::set_callback(Box::new(move |event| {
325/// let snapshot_exe = event.snapshot.executable.file_name();
326/// let is_firefox = snapshot_exe == Some(OsStr::new("firefox.exe"));
327///
328/// // only monitor name change events from Firefox
329/// // (this checks when the tab changes)
330/// if is_firefox && event.kind == WindowEventKind::WindowNameChanged {
331/// // send the event to the main thread
332/// let res = tx.send(event.snapshot);
333///
334/// if let Err(err) = res {
335/// // ...
336/// # _ = err;
337/// }
338/// }
339/// }));
340///
341/// while let Ok(browser_snapshot) = rx.recv() {
342/// // ...
343/// # _ = browser_snapshot;
344/// }
345/// ```
346pub fn set_callback(callback: WindowEventCallback) -> Option<WindowEventCallback> {
347 STATE.lock().callback.replace(callback)
348}
349
350/// Removes & returns the currently set callback if it exists.
351///
352/// A callback can be set using [`set_callback`].
353pub fn remove_callback() -> Option<WindowEventCallback> {
354 STATE.lock().callback.take()
355}
356
357/// Error returned by [`unhook`].
358///
359/// Most likely, this will be caused by attempting to unhook when no hook is sent.
360/// However, this can also error if there was a Win32 API error relating to the message queue.
361#[derive(Debug, thiserror::Error, Eq, PartialEq)]
362pub enum UnhookError {
363 /// No hook was set yet. To set a hook, use [`try_hook`].
364 #[error("No hook was set yet; call `try_hook()` to set a hook.")]
365 HookNotSet,
366 /// There was an error on the spawned hook thread (the thread that listens for events)
367 /// relating to the setup or shutdown of the event queue.
368 #[error("The hook thread failed: {0}")]
369 HookThreadError(WinErr),
370 /// There was an error with instructing the hook thread (thread that listens for events)
371 /// to quit. The [`unhook`] function failed to send [`WM_QUIT`].
372 #[error("Failed to quit to the hook thread (failed to send WM_QUIT): {0}")]
373 QuitMessageQueueError(WinErr),
374}
375
376/// Removes window event monitoring hook.
377///
378/// This function stops the thread that listens for [window events](WindowEvent).
379/// This *does not* call [`remove_callback`] to remove the set callback, but there's no harm in leaving it set.
380/// If a hook isn't set yet, this will return [`UnhookError::HookNotSet`].
381pub fn unhook() -> Result<(), UnhookError> {
382 let mut state = STATE.lock();
383
384 let Some((thread, thread_id)) = state.thread.take() else {
385 return Err(UnhookError::HookNotSet);
386 };
387
388 // SAFETY: thread is live and has message queue
389 match unsafe { PostThreadMessageW(thread_id.get(), WM_QUIT, WPARAM::default(), LPARAM::default()) } {
390 Ok(()) => match thread.join() {
391 Err(panic) => panic::resume_unwind(panic),
392 Ok(res) => match res {
393 Ok(()) => Ok(()),
394 Err(err) => Err(UnhookError::HookThreadError(err)),
395 }
396 }
397 Err(err) if err == WinErr::from(ERROR_INVALID_THREAD_ID) => panic!("WinHookState::thread should always point to a valid thread"),
398 Err(err) if err == WinErr::from(ERROR_INVALID_PARAMETER) => panic!("WinHookState::thread should always point to a valid thread"),
399 Err(err) => Err(UnhookError::QuitMessageQueueError(err)),
400 }
401}