1use 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 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 assert_eq!(custom_message.message_id, 0);
163 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 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 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
268pub fn refresh_icon_cache() {
270 unsafe {
271 SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None);
272 }
273}