Skip to main content

win_desktop_utils/
shell.rs

1use std::ffi::OsStr;
2use std::io;
3use std::os::windows::ffi::OsStrExt;
4use std::path::Path;
5use std::process::Command;
6
7use windows::core::PCWSTR;
8use windows::Win32::Foundation::HWND;
9use windows::Win32::UI::Shell::{SHFileOperationW, ShellExecuteW, SHFILEOPSTRUCTW};
10use windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
11
12use crate::error::{Error, Result};
13
14const FO_DELETE_CODE: u32 = 3;
15const FOF_SILENT: u16 = 0x0004;
16const FOF_NOCONFIRMATION: u16 = 0x0010;
17const FOF_ALLOWUNDO: u16 = 0x0040;
18const FOF_NOERRORUI: u16 = 0x0400;
19
20fn to_wide_os(value: &OsStr) -> Vec<u16> {
21    value.encode_wide().chain(std::iter::once(0)).collect()
22}
23
24fn to_wide_str(value: &str) -> Vec<u16> {
25    OsStr::new(value)
26        .encode_wide()
27        .chain(std::iter::once(0))
28        .collect()
29}
30
31fn to_double_null_path(value: &Path) -> Vec<u16> {
32    value
33        .as_os_str()
34        .encode_wide()
35        .chain(std::iter::once(0))
36        .chain(std::iter::once(0))
37        .collect()
38}
39
40fn shell_open_raw(target: &OsStr) -> Result<()> {
41    let operation = to_wide_str("open");
42    let target_w = to_wide_os(target);
43
44    let result = unsafe {
45        ShellExecuteW(
46            Some(HWND::default()),
47            PCWSTR(operation.as_ptr()),
48            PCWSTR(target_w.as_ptr()),
49            PCWSTR::null(),
50            PCWSTR::null(),
51            SW_SHOWNORMAL,
52        )
53    };
54
55    let code = result.0 as isize;
56    if code <= 32 {
57        Err(Error::WindowsApi {
58            context: "ShellExecuteW",
59            code: code as i32,
60        })
61    } else {
62        Ok(())
63    }
64}
65
66/// Opens a file or directory with the user's default Windows handler.
67pub fn open_with_default(target: impl AsRef<Path>) -> Result<()> {
68    let path = target.as_ref();
69
70    if path.as_os_str().is_empty() {
71        return Err(Error::InvalidInput("target cannot be empty"));
72    }
73
74    shell_open_raw(path.as_os_str())
75}
76
77/// Opens a URL with the user's default browser or registered handler.
78pub fn open_url(url: &str) -> Result<()> {
79    if url.trim().is_empty() {
80        return Err(Error::InvalidInput("url cannot be empty"));
81    }
82
83    shell_open_raw(OsStr::new(url))
84}
85
86/// Opens Explorer and selects the requested path.
87pub fn reveal_in_explorer(path: impl AsRef<Path>) -> Result<()> {
88    let path = path.as_ref();
89
90    if path.as_os_str().is_empty() {
91        return Err(Error::InvalidInput("path cannot be empty"));
92    }
93
94    let arg = format!("/select,{}", path.display());
95
96    Command::new("explorer.exe").arg(arg).spawn()?;
97
98    Ok(())
99}
100
101/// Sends a file or directory to the Windows Recycle Bin.
102///
103/// The path must be absolute and must exist.
104pub fn move_to_recycle_bin(path: impl AsRef<Path>) -> Result<()> {
105    let path = path.as_ref();
106
107    if path.as_os_str().is_empty() {
108        return Err(Error::InvalidInput("path cannot be empty"));
109    }
110
111    if !path.is_absolute() {
112        return Err(Error::InvalidInput("path must be absolute"));
113    }
114
115    if !path.exists() {
116        return Err(Error::Io(io::Error::new(
117            io::ErrorKind::NotFound,
118            "path does not exist",
119        )));
120    }
121
122    let from_w = to_double_null_path(path);
123
124    let mut op = SHFILEOPSTRUCTW {
125        hwnd: HWND::default(),
126        wFunc: FO_DELETE_CODE,
127        pFrom: PCWSTR(from_w.as_ptr()),
128        pTo: PCWSTR::null(),
129        fFlags: FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT,
130        fAnyOperationsAborted: false.into(),
131        hNameMappings: std::ptr::null_mut(),
132        lpszProgressTitle: PCWSTR::null(),
133    };
134
135    let result = unsafe { SHFileOperationW(&mut op) };
136
137    if result != 0 {
138        Err(Error::WindowsApi {
139            context: "SHFileOperationW(FO_DELETE)",
140            code: result,
141        })
142    } else if op.fAnyOperationsAborted.as_bool() {
143        Err(Error::WindowsApi {
144            context: "SHFileOperationW(FO_DELETE) aborted",
145            code: 0,
146        })
147    } else {
148        Ok(())
149    }
150}