use std::ops::Deref;
use windows::Win32::Foundation::{
ERROR_CANCELLED, ERROR_PATH_NOT_FOUND, HWND, RPC_E_CHANGED_MODE, S_FALSE, S_OK,
};
use windows::Win32::Storage::EnhancedStorage::{PKEY_ContentType, PKEY_Size};
use windows::Win32::System::Com::{
COINIT, COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE, CoInitializeEx, CoTaskMemFree,
CoUninitialize,
};
use windows::Win32::System::SystemServices::{SFGAO_FOLDER, SFGAO_STREAM};
use windows::Win32::UI::Shell::{
BHID_EnumItems, BHID_SFUIObject, IEnumShellItems, IShellItem, IShellItem2, IShellLinkW,
KF_FLAG_DEFAULT, PathIsRelativeW, SHCreateItemFromParsingName, SHCreateShellItem,
SHGetKnownFolderPath, SIGDN, SIGDN_DESKTOPABSOLUTEPARSING, SIGDN_PARENTRELATIVE,
SIGDN_PARENTRELATIVEEDITING,
};
use windows::Win32::UI::WindowsAndMessaging::{GetWindowTextLengthW, GetWindowTextW};
pub use windows::core::Result as WinRes;
use windows::core::{GUID, HRESULT, HSTRING, Interface};
pub const HRESULT_CANCELLED: HRESULT = ERROR_CANCELLED.to_hresult();
pub trait ShellItemEx {
fn get_display_name(&self, name_type: SIGDN) -> WinRes<HSTRING>;
fn relative_name(&self) -> HSTRING {
self.get_display_name(SIGDN_PARENTRELATIVE)
.expect("IShellItem must provide SIGDN_PARENTRELATIVE")
}
fn relative_editing_name(&self) -> HSTRING {
self.get_display_name(SIGDN_PARENTRELATIVEEDITING)
.expect("IShellItem must provide SIGDN_PARENTRELATIVEEDITING")
}
fn absolute_parsing_name(&self) -> HSTRING {
self.get_display_name(SIGDN_DESKTOPABSOLUTEPARSING)
.expect("IShellItem must provide SIGDN_DESKTOPABSOLUTEPARSING")
}
fn is_file(&self) -> WinRes<bool>;
fn is_directory(&self) -> WinRes<bool>;
fn size(&self) -> WinRes<u64>;
fn content_type(&self) -> WinRes<String>;
fn iter_children(&self) -> WinRes<ChildrenIter>;
fn link_target(&self) -> WinRes<IShellItem>;
}
pub struct ChildrenIter {
enum_items: IEnumShellItems,
}
impl Iterator for ChildrenIter {
type Item = IShellItem;
fn next(&mut self) -> Option<Self::Item> {
unsafe {
let fetched = &mut [None; 1];
self.enum_items.Next(fetched, None).ok()?;
fetched[0].take()
}
}
}
impl ShellItemEx for IShellItem {
fn get_display_name(&self, name_type: SIGDN) -> WinRes<HSTRING> {
unsafe {
let name = self.GetDisplayName(name_type)?;
let string = HSTRING::from_wide(name.as_wide());
CoTaskMemFree(Some(name.as_ptr() as _));
Ok(string)
}
}
fn is_file(&self) -> WinRes<bool> {
unsafe { Ok(self.GetAttributes(SFGAO_STREAM)?.contains(SFGAO_STREAM)) }
}
fn is_directory(&self) -> WinRes<bool> {
unsafe { Ok(self.GetAttributes(SFGAO_FOLDER)?.contains(SFGAO_FOLDER)) }
}
fn size(&self) -> WinRes<u64> {
unsafe {
let item: IShellItem2 = self.cast()?;
item.GetUInt64(&PKEY_Size)
}
}
fn content_type(&self) -> WinRes<String> {
unsafe {
let item: IShellItem2 = self.cast()?;
Ok(item.GetString(&PKEY_ContentType)?.to_string()?)
}
}
fn iter_children(&self) -> WinRes<ChildrenIter> {
let enumerate: IEnumShellItems = unsafe { self.BindToHandler(None, &BHID_EnumItems) }?;
Ok(ChildrenIter {
enum_items: enumerate,
})
}
fn link_target(&self) -> WinRes<IShellItem> {
unsafe {
let link: IShellLinkW = self.BindToHandler(None, &BHID_SFUIObject)?;
let target_id = link.GetIDList()?;
SHCreateShellItem(None, None, target_id)
}
}
}
pub fn init_com(coinit: COINIT) -> WinRes<bool> {
unsafe {
match CoInitializeEx(None, coinit) {
S_OK | S_FALSE => Ok(true),
RPC_E_CHANGED_MODE => Ok(false),
hr => Err(hr.into()),
}
}
}
pub fn quit_com() {
unsafe { CoUninitialize() }
}
pub fn with_com<T>(f: impl FnOnce() -> WinRes<T>) -> WinRes<T> {
struct Guard {
balanced: bool,
}
impl Drop for Guard {
fn drop(&mut self) {
if self.balanced {
unsafe { CoUninitialize() }
}
}
}
let balanced = init_com(COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)?;
let guard = Guard { balanced };
let result = f();
drop(guard);
result
}
pub fn create_item(absolute_parsing_name: &HSTRING) -> WinRes<IShellItem> {
unsafe { SHCreateItemFromParsingName(absolute_parsing_name, None) }
}
const PATH_SEPARATOR: u16 = b'\\' as u16;
const CURRENT_DIR: &[u16] = &[b'.' as u16];
const PARENT_DIR: &[u16] = &[b'.' as u16, b'.' as u16];
pub fn travel_to_item(from: &IShellItem, destination: &[u16]) -> WinRes<IShellItem> {
destination
.split(|&c| c == PATH_SEPARATOR)
.try_fold(from.to_owned(), |folder, segment| {
if segment == CURRENT_DIR {
Ok(folder)
} else if segment == PARENT_DIR {
unsafe { folder.GetParent() }
} else {
folder
.iter_children()?
.find(|item| {
item.relative_name().deref() == segment
|| item.relative_editing_name().deref() == segment
})
.ok_or(ERROR_PATH_NOT_FOUND.into())
}
})
}
pub fn create_item_in(folder: &IShellItem, name_or_path: &HSTRING) -> WinRes<IShellItem> {
if !unsafe { PathIsRelativeW(name_or_path) }.as_bool() {
let item = create_item(name_or_path)?;
return (item.absolute_parsing_name() == *name_or_path)
.then_some(item)
.ok_or(ERROR_PATH_NOT_FOUND.into());
}
travel_to_item(folder, name_or_path)
}
pub fn window_text(hwnd: HWND) -> HSTRING {
let length = unsafe { GetWindowTextLengthW(hwnd) };
if length <= 0 {
return HSTRING::new();
}
let mut buffer = vec![0u16; length as usize + 1];
let copied = unsafe { GetWindowTextW(hwnd, &mut buffer) };
HSTRING::from_wide(&buffer[..copied.max(0) as usize])
}
pub fn known_folder_path(id: &GUID) -> WinRes<String> {
unsafe {
let path = SHGetKnownFolderPath(id, KF_FLAG_DEFAULT, None)?;
let text = path.to_string()?;
CoTaskMemFree(Some(path.as_ptr() as _));
Ok(text)
}
}