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::{SHFILEOPSTRUCTW, SHFileOperationW, ShellExecuteW};
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.
67///
68/// # Errors
69///
70/// Returns [`Error::InvalidInput`] if `target` is empty.
71/// Returns [`Error::WindowsApi`] if `ShellExecuteW` reports failure.
72pub fn open_with_default(target: impl AsRef<Path>) -> Result<()> {
73    let path = target.as_ref();
74
75    if path.as_os_str().is_empty() {
76        return Err(Error::InvalidInput("target cannot be empty"));
77    }
78
79    shell_open_raw(path.as_os_str())
80}
81
82/// Opens a URL with the user's default browser or registered handler.
83///
84/// This function checks only that the input is non-empty after trimming whitespace.
85/// URL validation is otherwise delegated to the Windows shell.
86///
87/// # Errors
88///
89/// Returns [`Error::InvalidInput`] if `url` is empty or whitespace only.
90/// Returns [`Error::WindowsApi`] if `ShellExecuteW` reports failure.
91pub fn open_url(url: &str) -> Result<()> {
92    if url.trim().is_empty() {
93        return Err(Error::InvalidInput("url cannot be empty"));
94    }
95
96    shell_open_raw(OsStr::new(url))
97}
98
99/// Opens Explorer and selects the requested path.
100///
101/// This function starts `explorer.exe` with `/select,` and the provided path.
102///
103/// # Errors
104///
105/// Returns [`Error::InvalidInput`] if `path` is empty.
106/// Returns [`Error::Io`] if spawning `explorer.exe` fails.
107pub fn reveal_in_explorer(path: impl AsRef<Path>) -> Result<()> {
108    let path = path.as_ref();
109
110    if path.as_os_str().is_empty() {
111        return Err(Error::InvalidInput("path cannot be empty"));
112    }
113
114    Command::new("explorer.exe")
115        .arg("/select,")
116        .arg(path)
117        .spawn()?;
118
119    Ok(())
120}
121
122/// Sends a file or directory to the Windows Recycle Bin.
123///
124/// The path must be absolute and must exist.
125///
126/// This function uses `SHFileOperationW` with `FO_DELETE` and `FOF_ALLOWUNDO`.
127///
128/// # Errors
129///
130/// Returns [`Error::InvalidInput`] if `path` is empty or not absolute.
131/// Returns [`Error::Io`] if the path does not exist.
132/// Returns [`Error::WindowsApi`] if the shell operation fails or is aborted.
133pub fn move_to_recycle_bin(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.is_absolute() {
141        return Err(Error::InvalidInput("path must be absolute"));
142    }
143
144    if !path.exists() {
145        return Err(Error::Io(io::Error::new(
146            io::ErrorKind::NotFound,
147            "path does not exist",
148        )));
149    }
150
151    let from_w = to_double_null_path(path);
152
153    let mut op = SHFILEOPSTRUCTW {
154        hwnd: HWND::default(),
155        wFunc: FO_DELETE_CODE,
156        pFrom: PCWSTR(from_w.as_ptr()),
157        pTo: PCWSTR::null(),
158        fFlags: FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT,
159        fAnyOperationsAborted: false.into(),
160        hNameMappings: std::ptr::null_mut(),
161        lpszProgressTitle: PCWSTR::null(),
162    };
163
164    let result = unsafe { SHFileOperationW(&mut op) };
165
166    if result != 0 {
167        Err(Error::WindowsApi {
168            context: "SHFileOperationW(FO_DELETE)",
169            code: result,
170        })
171    } else if op.fAnyOperationsAborted.as_bool() {
172        Err(Error::WindowsApi {
173            context: "SHFileOperationW(FO_DELETE) aborted",
174            code: 0,
175        })
176    } else {
177        Ok(())
178    }
179}