virtual_desktop_manager/
window_info.rs

1//! Helper methods to get window information.
2
3use std::{collections::HashMap, fmt, ops::ControlFlow, sync::Arc};
4use windows::{
5    core::{Error, PWSTR},
6    Win32::{
7        Foundation::{CloseHandle, HANDLE, HWND},
8        System::Threading::{
9            OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_FORMAT,
10            PROCESS_QUERY_LIMITED_INFORMATION,
11        },
12        UI::WindowsAndMessaging::{GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId},
13    },
14};
15
16use crate::{nwg_ext::enum_child_windows, vd};
17
18/// Simple wrapper around [`enum_child_windows`].
19pub fn all_windows() -> Vec<HWND> {
20    let mut result = Vec::new();
21    enum_child_windows(None, |window| {
22        result.push(window);
23        ControlFlow::Continue(())
24    });
25    result
26}
27
28/// Get the title of a window.
29///
30/// # References
31///
32/// - Rust library for getting titles of all open windows:
33///   <https://github.com/HiruNya/window_titles/blob/924feffac93c9ac7238d6fa5c39c1453815a0408/src/winapi.rs>
34pub fn get_window_title(window: HWND) -> Result<String, Error> {
35    let mut length = unsafe { GetWindowTextLengthW(window) };
36    if length == 0 {
37        return Ok(String::new());
38    }
39    length += 1;
40    let mut title: Vec<u16> = vec![0; length as usize];
41    let len = unsafe { GetWindowTextW(window, &mut title) };
42    if len != 0 {
43        Ok(String::from_utf16(title[0..(len as usize)].as_ref())?)
44    } else {
45        Err(Error::from_win32())
46    }
47}
48
49/// Get the identifier of the process that created a specified window.
50///
51/// # References
52///
53/// - [GetWindowThreadProcessId function (winuser.h) - Win32 apps | Microsoft
54///   Learn](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowthreadprocessid)
55pub fn get_window_process_id(window: HWND) -> Result<u32, Error> {
56    let mut process_id = 0;
57    let thread_id = unsafe { GetWindowThreadProcessId(window, Some(&mut process_id)) };
58    if thread_id == 0 {
59        Err(Error::from_win32())
60    } else {
61        Ok(process_id)
62    }
63}
64
65/// Get the full name of a process.
66///
67/// # References
68///
69/// - [OpenProcess function (processthreadsapi.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess)
70/// - [QueryFullProcessImageNameW function (winbase.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-queryfullprocessimagenamew)
71/// - [windows - How to get the process name in C++ - Stack Overflow](https://stackoverflow.com/questions/4570174/how-to-get-the-process-name-in-c)
72pub fn get_process_full_name(process_id: u32) -> Result<String, Error> {
73    struct ProcessHandle(HANDLE);
74    impl ProcessHandle {
75        fn close(self) -> Result<(), Error> {
76            let handle = self.0;
77            std::mem::forget(self);
78            unsafe { CloseHandle(handle) }
79        }
80    }
81    impl Drop for ProcessHandle {
82        fn drop(&mut self) {
83            let _ = unsafe { CloseHandle(self.0) };
84        }
85    }
86    let handle = ProcessHandle(unsafe {
87        // Note: required permission is specified in QueryFullProcessImageNameW docs
88        OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, process_id)?
89    });
90    let mut buffer: Vec<u16> = vec![0; 1024];
91    let mut length = buffer.len() as u32;
92    unsafe {
93        QueryFullProcessImageNameW(
94            handle.0,
95            PROCESS_NAME_FORMAT(0),
96            PWSTR::from_raw(buffer.as_mut_ptr()),
97            &mut length,
98        )?;
99    }
100    handle.close()?;
101    Ok(String::from_utf16(&buffer[..length as usize])?)
102}
103
104/// Get the name of a process.
105#[allow(clippy::assigning_clones)]
106pub fn get_process_name(process_id: u32) -> Result<String, Error> {
107    let mut exe_path = get_process_full_name(process_id)?;
108    if let Some(slash) = exe_path.rfind(['\\', '/']) {
109        exe_path = exe_path[slash + 1..].to_owned();
110    }
111    if exe_path.ends_with(".exe") {
112        exe_path.truncate(exe_path.len() - 4);
113    }
114    Ok(exe_path)
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum VirtualDesktopInfo {
119    WindowPinned,
120    AppPinned,
121    AtDesktop {
122        /// GUID identifier for the virtual desktop.
123        desktop: vd::Desktop,
124        // Zero-based index for the virtual desktop when the info was gathered
125        // (it might have been moved after that).
126        index: u32,
127    },
128}
129impl fmt::Display for VirtualDesktopInfo {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match self {
132            Self::WindowPinned => write!(f, "Pinned Window"),
133            Self::AppPinned => write!(f, "Pinned App"),
134            Self::AtDesktop { index, .. } => fmt::Display::fmt(&(index + 1), f),
135        }
136    }
137}
138impl VirtualDesktopInfo {
139    pub fn new(window: HWND) -> vd::Result<Self> {
140        if vd::is_pinned_app(window)? {
141            Ok(Self::AppPinned)
142        } else if vd::is_pinned_window(window)? {
143            Ok(Self::WindowPinned)
144        } else {
145            let desktop = vd::get_window_desktop(window)?;
146            let index = desktop.get_index()?;
147            Ok(Self::AtDesktop { desktop, index })
148        }
149    }
150
151    /// Returns `true` if the virtual desktop info is [`WindowPinned`].
152    ///
153    /// [`WindowPinned`]: VirtualDesktopInfo::WindowPinned
154    #[must_use]
155    pub fn is_window_pinned(&self) -> bool {
156        matches!(self, Self::WindowPinned)
157    }
158
159    /// Returns `true` if the virtual desktop info is [`AppPinned`].
160    ///
161    /// [`AppPinned`]: VirtualDesktopInfo::AppPinned
162    #[must_use]
163    pub fn is_app_pinned(&self) -> bool {
164        matches!(self, Self::AppPinned)
165    }
166
167    /// Returns `true` if the virtual desktop info is [`AtDesktop`].
168    ///
169    /// [`AtDesktop`]: VirtualDesktopInfo::AtDesktop
170    #[must_use]
171    pub fn is_at_desktop(&self) -> bool {
172        matches!(self, Self::AtDesktop { .. })
173    }
174}
175
176#[derive(Debug, Clone)]
177pub enum GetAllError {
178    Title(Error),
179    ProcessId(Error),
180    ProcessName(Error),
181    VirtualDesktop(vd::Error),
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
185pub struct WindowHandle(pub isize);
186impl WindowHandle {
187    pub fn as_hwnd(self) -> HWND {
188        HWND(self.0 as *mut _)
189    }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct WindowInfo {
194    pub handle: WindowHandle,
195    pub title: String,
196    pub process_id: u32,
197    pub process_name: Arc<str>,
198    pub virtual_desktop: VirtualDesktopInfo,
199}
200impl WindowInfo {
201    pub fn get_all() -> Vec<WindowInfo> {
202        Self::try_get_all()
203            .filter_map(|res| match res {
204                Ok(info) => Some(info),
205                Err(e) => {
206                    tracing::trace!("Failed to get window info: {:?}", e);
207                    None
208                }
209            })
210            .collect()
211    }
212    pub fn try_get_all() -> impl Iterator<Item = Result<WindowInfo, GetAllError>> {
213        let mut process_names: HashMap<u32, Arc<str>> = HashMap::new();
214        all_windows()
215            .into_iter()
216            .map(move |handle| -> Result<WindowInfo, GetAllError> {
217                let virtual_desktop =
218                    VirtualDesktopInfo::new(handle).map_err(GetAllError::VirtualDesktop)?;
219                let title = get_window_title(handle).map_err(GetAllError::Title)?;
220                let process_id = get_window_process_id(handle).map_err(GetAllError::ProcessId)?;
221                let process_name = if let Some(name) = process_names.get(&process_id) {
222                    name.clone()
223                } else {
224                    let name = Arc::<str>::from(
225                        get_process_name(process_id).map_err(GetAllError::ProcessName)?,
226                    );
227                    process_names.insert(process_id, name.clone());
228                    name
229                };
230                Ok(WindowInfo {
231                    handle: WindowHandle(handle.0 as isize),
232                    title,
233                    process_id,
234                    process_name,
235                    virtual_desktop,
236                })
237            })
238    }
239}