Skip to main content

win_desktop_utils/
shell.rs

1use std::ffi::OsStr;
2use std::os::windows::ffi::OsStrExt;
3use std::path::Path;
4use std::process::Command;
5
6use windows::core::PCWSTR;
7use windows::Win32::Foundation::HWND;
8use windows::Win32::UI::Shell::{SHFileOperationW, ShellExecuteW, SHFILEOPSTRUCTW};
9use windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
10
11use crate::error::{Error, Result};
12
13const FO_DELETE_CODE: u32 = 3;
14const FOF_SILENT: u16 = 0x0004;
15const FOF_NOCONFIRMATION: u16 = 0x0010;
16const FOF_ALLOWUNDO: u16 = 0x0040;
17const FOF_NOERRORUI: u16 = 0x0400;
18
19fn to_wide_os(value: &OsStr) -> Vec<u16> {
20    value.encode_wide().chain(std::iter::once(0)).collect()
21}
22
23fn to_wide_str(value: &str) -> Vec<u16> {
24    OsStr::new(value)
25        .encode_wide()
26        .chain(std::iter::once(0))
27        .collect()
28}
29
30fn to_double_null_path(value: &Path) -> Vec<u16> {
31    value
32        .as_os_str()
33        .encode_wide()
34        .chain(std::iter::once(0))
35        .chain(std::iter::once(0))
36        .collect()
37}
38
39fn shell_open_raw(target: &OsStr) -> Result<()> {
40    let operation = to_wide_str("open");
41    let target_w = to_wide_os(target);
42
43    let result = unsafe {
44        ShellExecuteW(
45            Some(HWND::default()),
46            PCWSTR(operation.as_ptr()),
47            PCWSTR(target_w.as_ptr()),
48            PCWSTR::null(),
49            PCWSTR::null(),
50            SW_SHOWNORMAL,
51        )
52    };
53
54    let code = result.0 as isize;
55    if code <= 32 {
56        Err(Error::WindowsApi {
57            context: "ShellExecuteW",
58            code: code as i32,
59        })
60    } else {
61        Ok(())
62    }
63}
64
65/// Opens a file or directory with the user's default Windows handler.
66///
67/// # Errors
68///
69/// Returns [`Error::InvalidInput`] if `target` is empty.
70/// Returns [`Error::PathDoesNotExist`] if the target path does not exist.
71/// Returns [`Error::WindowsApi`] if `ShellExecuteW` reports failure.
72///
73/// # Examples
74///
75/// ```no_run
76/// win_desktop_utils::open_with_default(r"C:\Windows\notepad.exe")?;
77/// # Ok::<(), win_desktop_utils::Error>(())
78/// ```
79pub fn open_with_default(target: impl AsRef<Path>) -> Result<()> {
80    let path = target.as_ref();
81
82    if path.as_os_str().is_empty() {
83        return Err(Error::InvalidInput("target cannot be empty"));
84    }
85
86    if !path.exists() {
87        return Err(Error::PathDoesNotExist);
88    }
89
90    shell_open_raw(path.as_os_str())
91}
92
93/// Opens a URL with the user's default browser or registered handler.
94///
95/// This function checks only that the input is non-empty after trimming whitespace.
96/// URL validation is otherwise delegated to the Windows shell.
97///
98/// # Errors
99///
100/// Returns [`Error::InvalidInput`] if `url` is empty or whitespace only.
101/// Returns [`Error::WindowsApi`] if `ShellExecuteW` reports failure.
102///
103/// # Examples
104///
105/// ```no_run
106/// win_desktop_utils::open_url("https://www.rust-lang.org")?;
107/// # Ok::<(), win_desktop_utils::Error>(())
108/// ```
109pub fn open_url(url: &str) -> Result<()> {
110    if url.trim().is_empty() {
111        return Err(Error::InvalidInput("url cannot be empty"));
112    }
113
114    shell_open_raw(OsStr::new(url))
115}
116
117/// Opens Explorer and selects the requested path.
118///
119/// This function starts `explorer.exe` with `/select,` and the provided path.
120///
121/// # Errors
122///
123/// Returns [`Error::InvalidInput`] if `path` is empty.
124/// Returns [`Error::PathDoesNotExist`] if the path does not exist.
125/// Returns [`Error::Io`] if spawning `explorer.exe` fails.
126///
127/// # Examples
128///
129/// ```no_run
130/// win_desktop_utils::reveal_in_explorer(r"C:\Windows\notepad.exe")?;
131/// # Ok::<(), win_desktop_utils::Error>(())
132/// ```
133pub fn reveal_in_explorer(path: impl AsRef<Path>) -> Result<()> {
134    let path = path.as_ref();
135
136    if path.as_os_str().is_empty() {
137        return Err(Error::InvalidInput("path cannot be empty"));
138    }
139
140    if !path.exists() {
141        return Err(Error::PathDoesNotExist);
142    }
143
144    Command::new("explorer.exe")
145        .arg("/select,")
146        .arg(path)
147        .spawn()?;
148
149    Ok(())
150}
151
152/// Sends a file or directory to the Windows Recycle Bin.
153///
154/// The path must be absolute and must exist.
155///
156/// This function uses `SHFileOperationW` with `FO_DELETE` and `FOF_ALLOWUNDO`.
157///
158/// # Errors
159///
160/// Returns [`Error::InvalidInput`] if `path` is empty.
161/// Returns [`Error::PathNotAbsolute`] if the path is not absolute.
162/// Returns [`Error::PathDoesNotExist`] if the path does not exist.
163/// Returns [`Error::WindowsApi`] if the shell operation fails or is aborted.
164///
165/// # Examples
166///
167/// ```no_run
168/// let path = std::env::current_dir()?.join("temporary-file.txt");
169/// std::fs::write(&path, "temporary file")?;
170/// win_desktop_utils::move_to_recycle_bin(&path)?;
171/// # Ok::<(), Box<dyn std::error::Error>>(())
172/// ```
173pub fn move_to_recycle_bin(path: impl AsRef<Path>) -> Result<()> {
174    let path = path.as_ref();
175
176    if path.as_os_str().is_empty() {
177        return Err(Error::InvalidInput("path cannot be empty"));
178    }
179
180    if !path.is_absolute() {
181        return Err(Error::PathNotAbsolute);
182    }
183
184    if !path.exists() {
185        return Err(Error::PathDoesNotExist);
186    }
187
188    let from_w = to_double_null_path(path);
189
190    let mut op = SHFILEOPSTRUCTW {
191        hwnd: HWND::default(),
192        wFunc: FO_DELETE_CODE,
193        pFrom: PCWSTR(from_w.as_ptr()),
194        pTo: PCWSTR::null(),
195        fFlags: FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT,
196        fAnyOperationsAborted: false.into(),
197        hNameMappings: std::ptr::null_mut(),
198        lpszProgressTitle: PCWSTR::null(),
199    };
200
201    let result = unsafe { SHFileOperationW(&mut op) };
202
203    if result != 0 {
204        Err(Error::WindowsApi {
205            context: "SHFileOperationW(FO_DELETE)",
206            code: result,
207        })
208    } else if op.fAnyOperationsAborted.as_bool() {
209        Err(Error::WindowsApi {
210            context: "SHFileOperationW(FO_DELETE) aborted",
211            code: 0,
212        })
213    } else {
214        Ok(())
215    }
216}