window_enumerator/
enumerator.rs

1use std::os::windows::ffi::OsStringExt;
2use windows::core::*;
3use windows::Win32::Foundation::*;
4use windows::Win32::System::ProcessStatus::*;
5use windows::Win32::System::Threading::*;
6use windows::Win32::UI::WindowsAndMessaging::*;
7
8use crate::errors::{Result, WindowError};
9use crate::types::{FilterCriteria, WindowInfo, WindowPosition};
10use crate::utils;
11
12#[cfg(feature = "selection")]
13use crate::types::Selection;
14
15#[cfg(feature = "sorting")]
16use crate::types::SortCriteria;
17
18#[cfg(feature = "sorting")]
19use crate::models::WindowSorter;
20
21/// The main window enumeration and inspection interface.
22///
23/// This struct provides methods to discover, filter, and sort Windows windows
24/// with various criteria. It serves as the primary entry point for the library.
25pub struct WindowEnumerator {
26    windows: Vec<WindowInfo>,
27}
28
29impl WindowEnumerator {
30    /// Creates a new window enumerator.
31    ///
32    /// The enumerator starts with no windows loaded. Call [`enumerate_all_windows`]
33    /// to populate it with the current system windows.
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// use window_enumerator::WindowEnumerator;
39    ///
40    /// let enumerator = WindowEnumerator::new();
41    /// ```
42    ///
43    /// [`enumerate_all_windows`]: WindowEnumerator::enumerate_all_windows
44    pub fn new() -> Self {
45        Self {
46            windows: Vec::new(),
47        }
48    }
49
50    /// Enumerates all visible windows on the system.
51    ///
52    /// This method populates the internal window list with all currently
53    /// visible, non-child windows. Each window is assigned a 1-based index.
54    ///
55    /// # Errors
56    ///
57    /// Returns [`WindowError::WindowsApiError`] if the Windows API call fails.
58    ///
59    /// # Examples
60    ///
61    /// ```no_run
62    /// use window_enumerator::WindowEnumerator;
63    ///
64    /// let mut enumerator = WindowEnumerator::new();
65    /// enumerator.enumerate_all_windows().unwrap();
66    /// ```
67    pub fn enumerate_all_windows(&mut self) -> Result<()> {
68        self.windows.clear();
69
70        unsafe {
71            EnumWindows(
72                Some(Self::enum_windows_proc),
73                LPARAM(self as *mut _ as isize),
74            )
75            .map_err(|e| Error::new(e.code(), "Failed to enumerate windows".into()))?;
76        }
77
78        // Assign 1-based indices to each window
79        for (index, window) in self.windows.iter_mut().enumerate() {
80            window.index = index + 1;
81        }
82
83        Ok(())
84    }
85
86    /// Windows enumeration callback function.
87    unsafe extern "system" fn enum_windows_proc(hwnd: HWND, lparam: LPARAM) -> BOOL {
88        let enumerator = &mut *(lparam.0 as *mut WindowEnumerator);
89
90        // Skip invisible windows and child windows
91        if IsWindowVisible(hwnd).as_bool() && GetParent(hwnd).0 == 0 {
92            if let Ok(mut window_info) = enumerator.get_window_info(hwnd) {
93                // Temporary index, will be reassigned later
94                window_info.index = enumerator.windows.len() + 1;
95                enumerator.windows.push(window_info);
96            }
97        }
98
99        BOOL::from(true) // Continue enumeration
100    }
101
102    /// Gathers information about a specific window.
103    fn get_window_info(&self, hwnd: HWND) -> Result<WindowInfo> {
104        unsafe {
105            // Get window title
106            let title = Self::get_window_text(hwnd);
107
108            // Get window class name
109            let class_name = Self::get_class_name(hwnd);
110
111            // Get process ID
112            let pid = Self::get_process_id(hwnd);
113
114            // Get process information
115            let (process_name, process_file) = if pid > 0 {
116                Self::get_process_info(pid).unwrap_or_default()
117            } else {
118                (String::new(), std::path::PathBuf::new())
119            };
120
121            // Get window position and size
122            let position = Self::get_window_position(hwnd);
123
124            Ok(WindowInfo {
125                hwnd: hwnd.0,
126                pid,
127                title,
128                class_name,
129                process_name,
130                process_file,
131                position,
132                index: 0, // Temporary value, will be set later
133            })
134        }
135    }
136
137    /// Retrieves the text of a window.
138    unsafe fn get_window_text(hwnd: HWND) -> String {
139        let mut buffer = [0u16; 256];
140        let len = GetWindowTextW(hwnd, &mut buffer);
141        if len > 0 {
142            std::ffi::OsString::from_wide(&buffer[..len as usize])
143                .to_string_lossy()
144                .into_owned()
145        } else {
146            String::new()
147        }
148    }
149
150    /// Retrieves the class name of a window.
151    unsafe fn get_class_name(hwnd: HWND) -> String {
152        let mut buffer = [0u16; 256];
153        let len = GetClassNameW(hwnd, &mut buffer);
154        if len > 0 {
155            std::ffi::OsString::from_wide(&buffer[..len as usize])
156                .to_string_lossy()
157                .into_owned()
158        } else {
159            String::new()
160        }
161    }
162
163    /// Retrieves the process ID of a window.
164    unsafe fn get_process_id(hwnd: HWND) -> u32 {
165        let mut pid: u32 = 0;
166        GetWindowThreadProcessId(hwnd, Some(&mut pid));
167        pid
168    }
169
170    /// Retrieves the position and dimensions of a window.
171    unsafe fn get_window_position(hwnd: HWND) -> WindowPosition {
172        let mut rect = RECT::default();
173        if GetWindowRect(hwnd, &mut rect).is_ok() {
174            WindowPosition {
175                x: rect.left,
176                y: rect.top,
177                width: rect.right - rect.left,
178                height: rect.bottom - rect.top,
179            }
180        } else {
181            WindowPosition::default()
182        }
183    }
184
185    /// Retrieves process information for a given process ID.
186    unsafe fn get_process_info(pid: u32) -> Result<(String, std::path::PathBuf)> {
187        let process_handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid)?;
188
189        let mut file_buffer = [0u16; MAX_PATH as usize];
190        let len = GetProcessImageFileNameW(process_handle, &mut file_buffer);
191
192        if len > 0 {
193            let full_path = std::ffi::OsString::from_wide(&file_buffer[..len as usize]);
194            let path_buf = std::path::PathBuf::from(&full_path);
195
196            // Extract just the filename
197            let process_name = path_buf
198                .file_name()
199                .map(|name| name.to_string_lossy().into_owned())
200                .unwrap_or_default();
201
202            CloseHandle(process_handle).ok();
203            Ok((process_name, path_buf))
204        } else {
205            CloseHandle(process_handle).ok();
206            // 使用标准库的方法获取错误代码
207            let last_error = std::io::Error::last_os_error();
208            Err(WindowError::WindowsApiError(
209                last_error.raw_os_error().unwrap_or(0) as u32,
210            ))
211        }
212    }
213
214    /// Finds windows by title containing the specified string (case-insensitive).
215    ///
216    /// This is a convenience method for simple title-based filtering.
217    ///
218    /// # Arguments
219    ///
220    /// * `title_substring` - The substring to search for in window titles
221    ///
222    /// # Returns
223    ///
224    /// A vector containing windows whose titles contain the specified substring.
225    ///
226    /// # Examples
227    ///
228    /// ```no_run
229    /// use window_enumerator::WindowEnumerator;
230    ///
231    /// let mut enumerator = WindowEnumerator::new();
232    /// enumerator.enumerate_all_windows().unwrap();
233    ///
234    /// let chrome_windows = enumerator.find_by_title("Chrome");
235    /// for window in chrome_windows {
236    ///     window.print_compact();
237    /// }
238    /// ```
239    pub fn find_by_title(&self, title_substring: &str) -> Vec<WindowInfo> {
240        let criteria = FilterCriteria {
241            title_contains: Some(title_substring.to_string()),
242            ..Default::default()
243        };
244        self.filter_windows(&criteria)
245    }
246
247    /// Filters windows based on the specified criteria.
248    ///
249    /// # Arguments
250    ///
251    /// * `criteria` - The filter criteria to apply
252    ///
253    /// # Returns
254    ///
255    /// A vector containing only the windows that match all criteria.
256    ///
257    /// # Examples
258    ///
259    /// ```no_run
260    /// use window_enumerator::{WindowEnumerator, FilterCriteria};
261    ///
262    /// let mut enumerator = WindowEnumerator::new();
263    /// enumerator.enumerate_all_windows().unwrap();
264    ///
265    /// let criteria = FilterCriteria {
266    ///     title_contains: Some("Chrome".to_string()),
267    ///     ..Default::default()
268    /// };
269    /// let chrome_windows = enumerator.filter_windows(&criteria);
270    /// ```
271    pub fn filter_windows(&self, criteria: &FilterCriteria) -> Vec<WindowInfo> {
272        self.windows
273            .iter()
274            .filter(|window| utils::matches_criteria(window, criteria))
275            .cloned()
276            .collect()
277    }
278
279    /// Filters and sorts windows based on the specified criteria.
280    ///
281    /// Requires the `sorting` feature.
282    ///
283    /// # Arguments
284    ///
285    /// * `criteria` - The filter criteria to apply
286    /// * `sort_criteria` - The sort criteria to apply
287    ///
288    /// # Returns
289    ///
290    /// A vector containing the filtered and sorted windows.
291    #[cfg(feature = "sorting")]
292    pub fn filter_and_sort_windows(
293        &self,
294        criteria: &FilterCriteria,
295        sort_criteria: &SortCriteria,
296    ) -> Vec<WindowInfo> {
297        WindowSorter::filter_and_sort_windows(&self.windows, criteria, sort_criteria)
298    }
299
300    /// Filters windows with selection criteria.
301    ///
302    /// Requires the `selection` feature.
303    ///
304    /// # Arguments
305    ///
306    /// * `criteria` - The filter criteria to apply
307    /// * `selection` - The selection criteria to apply
308    ///
309    /// # Returns
310    ///
311    /// A vector containing the selected windows that match the filter criteria.
312    #[cfg(feature = "selection")]
313    pub fn filter_windows_with_selection(
314        &self,
315        criteria: &FilterCriteria,
316        selection: &Selection,
317    ) -> Vec<WindowInfo> {
318        let filtered = self.filter_windows(criteria);
319
320        match selection {
321            Selection::All => filtered,
322            Selection::Indices(indices) => filtered
323                .into_iter()
324                .filter(|window| indices.contains(&window.index))
325                .collect(),
326        }
327    }
328
329    /// Filters, sorts, and selects windows based on the specified criteria.
330    ///
331    /// Requires both `sorting` and `selection` features.
332    ///
333    /// # Arguments
334    ///
335    /// * `criteria` - The filter criteria to apply
336    /// * `sort_criteria` - The sort criteria to apply
337    /// * `selection` - The selection criteria to apply
338    ///
339    /// # Returns
340    ///
341    /// A vector containing the filtered, sorted, and selected windows.
342    #[cfg(all(feature = "sorting", feature = "selection"))]
343    pub fn filter_sort_windows_with_selection(
344        &self,
345        criteria: &FilterCriteria,
346        sort_criteria: &SortCriteria,
347        selection: &Selection,
348    ) -> Vec<WindowInfo> {
349        let filtered =
350            WindowSorter::filter_and_sort_windows(&self.windows, criteria, sort_criteria);
351
352        match selection {
353            Selection::All => filtered,
354            Selection::Indices(indices) => filtered
355                .into_iter()
356                .filter(|window| indices.contains(&window.index))
357                .collect(),
358        }
359    }
360
361    /// Returns a reference to all enumerated windows.
362    ///
363    /// # Returns
364    ///
365    /// A slice containing all windows that were enumerated.
366    pub fn get_windows(&self) -> &[WindowInfo] {
367        &self.windows
368    }
369
370    /// Retrieves a window by its 1-based index.
371    ///
372    /// # Arguments
373    ///
374    /// * `index` - The 1-based index of the window to retrieve
375    ///
376    /// # Returns
377    ///
378    /// `Some(&WindowInfo)` if a window with the given index exists, `None` otherwise.
379    pub fn get_window_by_index(&self, index: usize) -> Option<&WindowInfo> {
380        self.windows.iter().find(|w| w.index == index)
381    }
382
383    /// Prints all enumerated windows with their indices in a formatted table.
384    ///
385    /// This is useful for debugging and for users to see available windows
386    /// before making selections.
387    ///
388    /// # Examples
389    ///
390    /// ```no_run
391    /// use window_enumerator::WindowEnumerator;
392    ///
393    /// let mut enumerator = WindowEnumerator::new();
394    /// enumerator.enumerate_all_windows().unwrap();
395    /// enumerator.print_windows_with_indices();
396    /// ```
397    pub fn print_windows_with_indices(&self) {
398        println!("Index | Handle      | PID    | Position    | Title");
399        println!("------|-------------|--------|-------------|-------------------");
400        for window in &self.windows {
401            println!(
402                "{:5} | 0x{:08x} | {:6} | {:4},{:4}     | {}",
403                window.index,
404                window.hwnd,
405                window.pid,
406                window.position.x,
407                window.position.y,
408                window.title
409            );
410        }
411    }
412}
413
414impl Default for WindowEnumerator {
415    fn default() -> Self {
416        Self::new()
417    }
418}