winapi_easy/
shell.rs

1//! Windows Shell functionality.
2
3use num_enum::{
4    FromPrimitive,
5    IntoPrimitive,
6};
7use windows::Win32::UI::Shell::Common::ITEMIDLIST;
8use windows::Win32::UI::Shell::{
9    ILCreateFromPathW,
10    SHCNRF_InterruptLevel,
11    SHCNRF_NewDelivery,
12    SHCNRF_RecursiveInterrupt,
13    SHCNRF_ShellLevel,
14    SHChangeNotification_Lock,
15    SHChangeNotification_Unlock,
16    SHChangeNotify,
17    SHChangeNotifyDeregister,
18    SHChangeNotifyEntry,
19    SHChangeNotifyRegister,
20    SHGetPathFromIDListEx,
21    SHCNE_ASSOCCHANGED,
22    SHCNE_CREATE,
23    SHCNE_DELETE,
24    SHCNE_ID,
25    SHCNE_MKDIR,
26    SHCNE_RENAMEFOLDER,
27    SHCNE_RENAMEITEM,
28    SHCNE_RMDIR,
29    SHCNE_UPDATEDIR,
30    SHCNE_UPDATEITEM,
31    SHCNF_IDLIST,
32};
33use windows::Win32::UI::WindowsAndMessaging::WM_APP;
34
35use std::cell::Cell;
36use std::ops::{
37    BitOr,
38    BitOrAssign,
39};
40use std::path::{
41    Path,
42    PathBuf,
43};
44use std::{
45    io,
46    ptr,
47};
48use windows::Win32::Foundation::{
49    HANDLE,
50    HWND,
51    LPARAM,
52    WPARAM,
53};
54
55use crate::com::ComTaskMemory;
56use crate::internal::{
57    CustomAutoDrop,
58    ReturnValue,
59};
60use crate::messaging::ThreadMessageLoop;
61use crate::string::{
62    max_path_extend,
63    ZeroTerminatedWideString,
64};
65use crate::ui::messaging::WindowMessageListener;
66use crate::ui::{
67    Window,
68    WindowClass,
69    WindowClassAppearance,
70    WindowHandle,
71};
72
73#[allow(dead_code)]
74#[derive(Clone, Debug)]
75pub(crate) struct PathChangeEvent {
76    pub event: FsChangeEvent,
77    pub path_1: Option<PathBuf>,
78    pub path_2: Option<PathBuf>,
79}
80
81impl Default for PathChangeEvent {
82    fn default() -> Self {
83        Self {
84            event: FsChangeEvent::Other(0),
85            path_1: None,
86            path_2: None,
87        }
88    }
89}
90
91#[derive(IntoPrimitive, FromPrimitive, Copy, Clone, Eq, PartialEq, Debug)]
92#[repr(u32)]
93pub(crate) enum FsChangeEvent {
94    ItemCreated = SHCNE_CREATE.0,
95    ItemRenamed = SHCNE_RENAMEITEM.0,
96    ItemUpdated = SHCNE_UPDATEITEM.0,
97    ItemDeleted = SHCNE_DELETE.0,
98    FolderCreated = SHCNE_MKDIR.0,
99    FolderRenamed = SHCNE_RENAMEFOLDER.0,
100    FolderUpdated = SHCNE_UPDATEDIR.0,
101    FolderDeleted = SHCNE_RMDIR.0,
102    #[num_enum(catch_all)]
103    Other(u32),
104}
105
106impl BitOr for FsChangeEvent {
107    type Output = FsChangeEvent;
108
109    fn bitor(self, rhs: Self) -> Self::Output {
110        Self::from(u32::from(self) | u32::from(rhs))
111    }
112}
113
114impl BitOrAssign for FsChangeEvent {
115    fn bitor_assign(&mut self, rhs: Self) {
116        *self = *self | rhs
117    }
118}
119
120impl From<FsChangeEvent> for SHCNE_ID {
121    fn from(value: FsChangeEvent) -> Self {
122        SHCNE_ID(value.into())
123    }
124}
125
126pub(crate) struct MonitoredPath<'a> {
127    pub path: &'a Path,
128    pub recursive: bool,
129}
130
131#[allow(dead_code)]
132pub(crate) fn monitor_path_changes<F>(
133    monitored_paths: &[MonitoredPath],
134    event_type: FsChangeEvent,
135    mut callback: F,
136) -> io::Result<()>
137where
138    F: FnMut(&PathChangeEvent) -> io::Result<()>,
139{
140    #[derive(Default)]
141    struct Listener {
142        data: Cell<PathChangeEvent>,
143    }
144    impl WindowMessageListener for Listener {
145        fn handle_custom_user_message(
146            &self,
147            _window: &WindowHandle,
148            message_id: u8,
149            w_param: WPARAM,
150            l_param: LPARAM,
151        ) {
152            assert_eq!(message_id, 0);
153            // See: https://stackoverflow.com/a/72001352
154            let mut raw_ppp_idl = ptr::null_mut();
155            let mut raw_event = 0;
156            let lock = unsafe {
157                SHChangeNotification_Lock(
158                    HANDLE(w_param.0 as *mut std::ffi::c_void),
159                    l_param.0 as u32,
160                    Some(&mut raw_ppp_idl),
161                    Some(&mut raw_event),
162                )
163            };
164            let _unlock_guard = CustomAutoDrop {
165                value: lock,
166                drop_fn: |x| unsafe {
167                    SHChangeNotification_Unlock(HANDLE(x.0))
168                        .if_null_panic_else_drop("Improper lock usage");
169                },
170            };
171
172            let raw_pid_list_pair = unsafe { std::slice::from_raw_parts(raw_ppp_idl, 2) };
173            let event_type = FsChangeEvent::from(raw_event as u32);
174
175            fn get_path_from_id_list(raw_id_list: &ITEMIDLIST) -> PathBuf {
176                let mut raw_path_buffer: ZeroTerminatedWideString =
177                    ZeroTerminatedWideString(vec![0; 32000]);
178                unsafe {
179                    // Unclear if paths > MAX_PATH are even supported here
180                    SHGetPathFromIDListEx(
181                        raw_id_list,
182                        raw_path_buffer.0.as_mut_slice(),
183                        Default::default(),
184                    )
185                    .if_null_panic_else_drop("Cannot get path from ID list");
186                }
187                raw_path_buffer.to_os_string().into()
188            }
189
190            let path_1 = unsafe { raw_pid_list_pair[0].as_ref() }.map(get_path_from_id_list);
191            let path_2 = unsafe { raw_pid_list_pair[1].as_ref() }.map(get_path_from_id_list);
192            self.data.replace(PathChangeEvent {
193                event: event_type,
194                path_1,
195                path_2,
196            });
197        }
198    }
199
200    // Unclear if it works if only some items are recursive
201    let recursive = monitored_paths.iter().any(|x| x.recursive);
202    let path_id_lists: Vec<(SHChangeNotifyEntry, ComTaskMemory<_>)> = monitored_paths
203        .iter()
204        .map(|monitored_path| {
205            let path_as_id_list: ComTaskMemory<_> = unsafe {
206                // MAX_PATH extension seems possible: https://stackoverflow.com/questions/9980943/bypassing-max-path-limitation-for-itemidlist#comment12771197_9980943
207                ILCreateFromPathW(
208                    ZeroTerminatedWideString::from_os_str(max_path_extend(
209                        monitored_path.path.as_os_str(),
210                    ))
211                    .as_raw_pcwstr(),
212                )
213                .into()
214            };
215            let raw_entry = SHChangeNotifyEntry {
216                pidl: path_as_id_list.0,
217                fRecursive: monitored_path.recursive.into(),
218            };
219            (raw_entry, path_as_id_list)
220        })
221        .collect();
222    let raw_entries: Vec<SHChangeNotifyEntry> = path_id_lists.iter().map(|x| x.0).collect();
223
224    let listener = Listener::default();
225
226    let window_class = WindowClass::register_new(
227        "Shell Change Listener Class",
228        WindowClassAppearance::empty(),
229    )?;
230    let window = Window::create_new(&window_class, &listener, "Shell Change Listener")?;
231    let reg_id = unsafe {
232        SHChangeNotifyRegister(
233            HWND::from(window.as_ref()),
234            SHCNRF_InterruptLevel
235                | SHCNRF_ShellLevel
236                | SHCNRF_NewDelivery
237                | if recursive {
238                    SHCNRF_RecursiveInterrupt
239                } else {
240                    Default::default()
241                },
242            u32::from(event_type).try_into().unwrap(),
243            WM_APP,
244            raw_entries.len().try_into().unwrap(),
245            raw_entries.as_ptr(),
246        )
247        .if_null_get_last_error()?
248    };
249    let _deregister_guard = CustomAutoDrop {
250        value: reg_id,
251        drop_fn: |x| unsafe {
252            SHChangeNotifyDeregister(*x)
253                .if_null_panic_else_drop("Notification listener not registered properly");
254        },
255    };
256
257    ThreadMessageLoop::run_thread_message_loop(|| callback(&listener.data.take()))?;
258    Ok(())
259}
260
261/// Forces a refresh of the Windows icon cache.
262pub fn refresh_icon_cache() {
263    unsafe {
264        SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None);
265    }
266}