1#![allow(clippy::upper_case_acronyms, unused_assignments, dead_code)]
70#[cfg(not(target_os = "windows"))]
71compile_error!("open is not supported on this platform");
72
73use std::{
74 ffi::{OsStr, OsString},
75 os::windows::process::CommandExt as WinCommandExt,
76 process::{Command, Stdio},
77 sync::OnceLock,
78};
79
80pub use error::Error;
81use error::ErrorKind;
82pub use error::Result;
83pub use shell::WindowsShell;
84
85mod error;
86mod shell;
87
88const CREATE_NO_WINDOW: u32 = 0x08000000;
89static DETECTED_SHELL: OnceLock<WindowsShell> = OnceLock::new();
90
91pub fn that(path: impl AsRef<OsStr>) -> Result<()> {
114 let mut last_err = None;
115 for mut cmd in commands(path) {
116 match cmd.status_without_output() {
117 Ok(status) => {
118 return Ok(status).into_result(cmd);
119 }
120 Err(err) => last_err = Some(err),
121 }
122 }
123 Err(last_err.map_or_else(
124 || Error::new(ErrorKind::NO_LAUNCHER, ""),
125 |err| Error::new(ErrorKind::IO, err.to_string().as_str()),
126 ))
127}
128
129pub fn with(path: impl AsRef<OsStr>, app: impl Into<String>) -> Result<()> {
151 let mut cmd = with_command(path, app);
152 cmd.status_without_output().into_result(cmd)
153}
154
155pub fn commands<T: AsRef<OsStr>>(path: T) -> Vec<Command> {
169 let shell = detect_shell().as_str();
170 let mut cmd = Command::new(shell);
171 match shell {
172 "pwsh" => cmd
173 .arg("-NoProfile")
174 .arg("-Command")
175 .arg("Start-Process")
176 .arg(wrap_in_quotes(path.as_ref()))
177 .creation_flags(CREATE_NO_WINDOW),
178 "nu" => cmd
179 .arg("-c")
180 .arg(format!("open {}", wrap_in_quotes_string(path.as_ref())))
181 .creation_flags(CREATE_NO_WINDOW),
182 "cmd" => cmd
183 .arg("/c")
184 .arg("start")
185 .raw_arg("\"\"")
186 .raw_arg(wrap_in_quotes(path))
187 .creation_flags(CREATE_NO_WINDOW),
188 _ => panic!("No supported shell detected."),
189 };
190 vec![cmd]
191}
192
193pub fn with_command<T: AsRef<OsStr>>(path: T, app: impl Into<String>) -> Command {
205 let shell = detect_shell().as_str();
206 let mut cmd = Command::new(shell);
207
208 match shell {
209 "pwsh" => cmd
210 .arg("-NoProfile")
211 .arg("-Command")
212 .arg("Start-Process")
213 .arg(wrap_in_quotes(path.as_ref()))
214 .arg(wrap_in_quotes(app.into()))
215 .creation_flags(CREATE_NO_WINDOW),
216 "nu" => cmd
217 .arg("-c")
218 .arg(format!(
219 "open {} {}",
220 wrap_in_quotes_string(path.as_ref()),
221 wrap_in_quotes_string(app.into())
222 ))
223 .creation_flags(CREATE_NO_WINDOW),
224 "cmd" => cmd
225 .arg("/c")
226 .arg("start")
227 .raw_arg("\"\"")
228 .raw_arg(wrap_in_quotes(path))
229 .raw_arg(wrap_in_quotes(app.into()))
230 .creation_flags(CREATE_NO_WINDOW),
231 _ => panic!("No supported shell detected."),
232 };
233
234 cmd
235}
236
237pub fn that_in_background(path: impl AsRef<OsStr>) -> std::thread::JoinHandle<Result<()>> {
241 let path = path.as_ref().to_os_string();
242 std::thread::spawn(|| that(path))
243}
244
245pub fn with_in_background<T: AsRef<OsStr>>(
251 path: T,
252 app: impl Into<String>,
253) -> std::thread::JoinHandle<Result<()>> {
254 let path = path.as_ref().to_os_string();
255 let app = app.into();
256 std::thread::spawn(|| with(path, app))
257}
258
259fn detect_shell() -> WindowsShell {
260 *DETECTED_SHELL.get_or_init(|| match get_shell() {
261 Ok(shell) => shell,
262 Err(err) => {
263 panic!("Failed to detect a supported shell: {}", err);
264 }
265 })
266}
267
268fn get_shell() -> Result<WindowsShell> {
269 if Command::new("pwsh")
270 .arg("-Command")
271 .arg("$PSVersionTable.PSVersion")
272 .status_without_output()
273 .map_or(false, |status| status.success())
274 {
275 return "pwsh".try_into();
276 }
277
278 if Command::new("nu")
279 .arg("-c")
280 .arg("version")
281 .status_without_output()
282 .map_or(false, |status| status.success())
283 {
284 return "nu".try_into();
285 }
286
287 "cmd".try_into()
288}
289
290fn wrap_in_quotes<T: AsRef<OsStr>>(path: T) -> OsString {
291 let mut result = OsString::from("\"");
292 result.push(path);
293 result.push("\"");
294
295 result
296}
297
298fn wrap_in_quotes_string<T: AsRef<OsStr>>(path: T) -> String {
299 let path = path.as_ref().to_string_lossy();
300 format!("\"{}\"", path)
301}
302
303pub fn that_detached(path: impl AsRef<OsStr>) -> Result<()> {
308 #[cfg(not(feature = "shellexecute"))]
309 {
310 let mut last_err = None;
311 for mut cmd in commands(path) {
312 match cmd.spawn_detached() {
313 Ok(_) => {
314 return Ok(());
315 }
316 Err(err) => last_err = Some(err),
317 }
318 }
319 Err(last_err.map_or_else(
320 || Error::new(ErrorKind::NO_LAUNCHER, ""),
321 |err| Error::new(ErrorKind::IO, err.to_string().as_str()),
322 ))
323 }
324
325 #[cfg(feature = "shellexecute")]
326 {
327 that_detached_execute(path)
328 }
329}
330
331pub fn with_detached<T: AsRef<OsStr>>(path: T, app: impl Into<String>) -> Result<()> {
337 #[cfg(not(feature = "shellexecute"))]
338 {
339 let mut last_err = None;
340 let mut cmd = with_command(path, app);
341
342 match cmd.spawn_detached() {
344 Ok(_) => {
345 return Ok(()); }
347 Err(err) => {
348 last_err = Some(err); }
350 }
351
352 Err(last_err.map_or_else(
353 || Error::new(ErrorKind::NO_LAUNCHER, ""),
354 |err| Error::new(ErrorKind::IO, err.to_string().as_str()),
355 ))
356 }
357
358 #[cfg(feature = "shellexecute")]
359 {
360 with_detached_execute(path, app)
361 }
362}
363
364trait IntoResult<T> {
365 fn into_result(self, cmd: Command) -> T;
366}
367
368impl IntoResult<Result<()>> for std::io::Result<std::process::ExitStatus> {
369 fn into_result(self, cmd: Command) -> Result<()> {
370 match self {
371 Ok(status) if status.success() => Ok(()),
372 Ok(status) => Err(Error::new(
373 ErrorKind::COMMAND_FAILED,
374 format!("{cmd:?} ({})", status).as_str(),
375 )),
376 Err(err) => Err(err.into()),
377 }
378 }
379}
380
381trait CommandExt {
382 fn status_without_output(&mut self) -> std::io::Result<std::process::ExitStatus>;
383 fn spawn_detached(&mut self) -> std::io::Result<()>;
384}
385
386impl CommandExt for Command {
387 fn status_without_output(&mut self) -> std::io::Result<std::process::ExitStatus> {
388 self.stdin(Stdio::null())
389 .stdout(Stdio::null())
390 .stderr(Stdio::null())
391 .status()
392 }
393
394 fn spawn_detached(&mut self) -> std::io::Result<()> {
395 self.stdin(Stdio::null())
399 .stdout(Stdio::null())
400 .stderr(Stdio::null());
401
402 use std::os::windows::process::CommandExt;
403 const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
404 const CREATE_NO_WINDOW: u32 = 0x08000000;
405 self.creation_flags(CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW);
406 self.spawn().map(|_| ())
407 }
408}
409
410#[cfg(feature = "shellexecute")]
411fn that_detached_execute<T: AsRef<OsStr>>(path: T) -> Result<()> {
412 let path = path.as_ref();
413 let is_dir = std::fs::metadata(path).map(|f| f.is_dir()).unwrap_or(false);
414
415 let path = wide(path);
416
417 if is_dir {
418 unsafe { ffi::CoInitialize(std::ptr::null()) };
419 let folder = unsafe { ffi::ILCreateFromPathW(path.as_ptr()) };
420 unsafe { SHOpenFolderAndSelectItems(folder, Some(&[folder]), 0)? };
421 return Ok(());
422 };
423
424 let mut info = ffi::SHELLEXECUTEINFOW {
425 cbSize: std::mem::size_of::<ffi::SHELLEXECUTEINFOW>() as _,
426 nShow: ffi::SW_SHOWNORMAL,
427 lpVerb: std::ptr::null(),
428 lpClass: std::ptr::null(),
429 lpFile: path.as_ptr(),
430 ..unsafe { std::mem::zeroed() }
431 };
432
433 unsafe { ShellExecuteExW(&mut info) }
434}
435
436#[cfg(feature = "shellexecute")]
437pub fn with_detached_execute<T: AsRef<OsStr>>(path: T, app: impl Into<String>) -> Result<()> {
438 let app = wide(app.into());
439 let path = wide(path);
440
441 let mut info = ffi::SHELLEXECUTEINFOW {
442 cbSize: std::mem::size_of::<ffi::SHELLEXECUTEINFOW>() as _,
443 nShow: ffi::SW_SHOWNORMAL,
444 lpFile: app.as_ptr(),
445 lpParameters: path.as_ptr(),
446 ..unsafe { std::mem::zeroed() }
447 };
448
449 unsafe { ShellExecuteExW(&mut info) }
450}
451
452#[cfg(feature = "shellexecute")]
454#[inline]
455fn wide<T: AsRef<OsStr>>(input: T) -> Vec<u16> {
456 use std::os::windows::ffi::OsStrExt;
457 input
458 .as_ref()
459 .encode_wide()
460 .chain(std::iter::once(0))
461 .collect()
462}
463
464#[allow(non_snake_case)]
478#[cfg(feature = "shellexecute")]
479unsafe fn ShellExecuteExW(info: *mut ffi::SHELLEXECUTEINFOW) -> Result<()> {
480 if ffi::ShellExecuteExW(info) == 1 {
483 Ok(())
484 } else {
485 Err(Error::new(
486 ErrorKind::IO,
487 std::io::Error::last_os_error().to_string().as_str(),
488 ))
489 }
490}
491
492#[allow(non_snake_case)]
507#[cfg(feature = "shellexecute")]
508unsafe fn SHOpenFolderAndSelectItems(
509 pidlfolder: *const ffi::ITEMIDLIST,
510 apidl: Option<&[*const ffi::ITEMIDLIST]>,
511 dwflags: u32,
512) -> Result<()> {
513 use std::convert::TryInto;
514
515 match ffi::SHOpenFolderAndSelectItems(
516 pidlfolder,
517 apidl.map_or(0, |slice| slice.len().try_into().unwrap()),
518 apidl.map_or(core::ptr::null(), |slice| slice.as_ptr()),
519 dwflags,
520 ) {
521 0 => Ok(()),
522 error_code => Err(Error::new(
523 ErrorKind::IO,
524 std::io::Error::from_raw_os_error(error_code)
525 .to_string()
526 .as_str(),
527 )),
528 }
529}
530
531#[cfg(feature = "shellexecute")]
532#[allow(non_snake_case)]
533mod ffi {
534 pub const SW_SHOWNORMAL: i32 = 1;
540
541 #[cfg_attr(not(target_arch = "x86"), repr(C))]
543 #[cfg_attr(target_arch = "x86", repr(C, packed(1)))]
544 pub struct SHELLEXECUTEINFOW {
545 pub cbSize: u32,
546 pub fMask: u32,
547 pub hwnd: isize,
548 pub lpVerb: *const u16,
549 pub lpFile: *const u16,
550 pub lpParameters: *const u16,
551 pub lpDirectory: *const u16,
552 pub nShow: i32,
553 pub hInstApp: isize,
554 pub lpIDList: *mut core::ffi::c_void,
555 pub lpClass: *const u16,
556 pub hkeyClass: isize,
557 pub dwHotKey: u32,
558 pub Anonymous: SHELLEXECUTEINFOW_0,
559 pub hProcess: isize,
560 }
561
562 #[cfg_attr(not(target_arch = "x86"), repr(C))]
564 #[cfg_attr(target_arch = "x86", repr(C, packed(1)))]
565 pub union SHELLEXECUTEINFOW_0 {
566 pub hIcon: isize,
567 pub hMonitor: isize,
568 }
569
570 #[repr(C, packed(1))]
572 pub struct SHITEMID {
573 pub cb: u16,
574 pub abID: [u8; 1],
575 }
576
577 #[repr(C, packed(1))]
579 pub struct ITEMIDLIST {
580 pub mkid: SHITEMID,
581 }
582
583 #[link(name = "shell32")]
584 extern "system" {
585 pub fn ShellExecuteExW(info: *mut SHELLEXECUTEINFOW) -> isize;
586 pub fn ILCreateFromPathW(pszpath: *const u16) -> *mut ITEMIDLIST;
587 pub fn SHOpenFolderAndSelectItems(
588 pidlfolder: *const ITEMIDLIST,
589 cidl: u32,
590 apidl: *const *const ITEMIDLIST,
591 dwflags: u32,
592 ) -> i32;
593 }
594
595 #[link(name = "ole32")]
596 extern "system" {
597 pub fn CoInitialize(pvreserved: *const core::ffi::c_void) -> i32;
598 }
599}