winapi_easy/
shell.rs

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