win_open/
lib.rs

1//! Use this library to open a path or URL using the program configured on the system in a non-blocking fashion.
2//!
3//! # Usage
4//!
5//! Open the given URL in the default web browser, without blocking.
6//!
7//! ```no_run
8//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
9//! win_open::that("http://rust-lang.org")?;
10//! # Ok(())
11//! # }
12//! ```
13//!
14//! Alternatively, specify the program to be used to open the path or URL.
15//!
16//! ```no_run
17//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! win_open::with("http://rust-lang.org", "firefox")?;
19//! # Ok(())
20//! # }
21//! ```
22//!
23//! Or obtain the commands to launch a file or path without running them.
24//!
25//! ```no_run
26//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
27//! let cmds = win_open::commands("http://rust-lang.org")[0].status()?;
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! It's also possible to obtain a command that can be used to open a path in an application.
33//!
34//! ```no_run
35//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
36//! let status = win_open::with_command("http://rust-lang.org", "firefox").status()?;
37//! # Ok(())
38//! # }
39//! ```
40//!
41//! # Notes
42//!
43//! ## Nonblocking operation
44//!
45//! The functions provided are nonblocking as it will return even though the
46//! launched child process is still running. Note that depending on the operating
47//! system, spawning launch helpers, which this library does under the hood,
48//! might still take 100's of milliseconds.
49//!
50//! **Beware that on some platforms and circumstances, the launcher may block**.
51//! In this case, please use the [`commands()`] or [`with_command()`] accordingly
52//! to `spawn()` it without blocking.
53//!
54//! ## Error handling
55//!
56//! As an operating system program is used, the open operation can fail.
57//! Therefore, you are advised to check the result and behave
58//! accordingly, e.g. by letting the user know that the open operation failed.
59//!
60//! ```no_run
61//! let path = "http://rust-lang.org";
62//!
63//! match win_open::that(path) {
64//!     Ok(()) => println!("Opened '{}' successfully.", path),
65//!     Err(err) => eprintln!("An error occurred when opening '{}': {}", path, err),
66//! }
67//! ```
68
69#![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
91/// Open path with the default application without blocking.
92///
93/// # Examples
94///
95/// ```no_run
96/// let path = "http://rust-lang.org";
97///
98/// match win_open::that(path) {
99///     Ok(()) => println!("Opened '{}' successfully.", path),
100///     Err(err) => panic!("An error occurred when opening '{}': {}", path, err),
101/// }
102/// ```
103///
104/// # Errors
105///
106/// A [`Error`] is returned on failure. Because different operating systems
107/// handle errors differently it is recommend to not match on a certain error.
108///
109/// # Beware
110///
111/// Sometimes, depending on the platform and system configuration, launchers *can* block.
112/// If you want to be sure they don't, use [`that_in_background()`] or [`that_detached`] instead.
113pub 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
129/// Open path with the given application.
130///
131/// This function may block if the application or launcher doesn't detach itself.
132/// In that case, consider using [`with_in_background()`] or [`with_command()].
133///
134/// # Examples
135///
136/// ```no_run
137/// let path = "http://rust-lang.org";
138/// let app = "firefox";
139///
140/// match win_open::with(path, app) {
141///     Ok(()) => println!("Opened '{}' successfully.", path),
142///     Err(err) => panic!("An error occurred when opening '{}': {}", path, err),
143/// }
144/// ```
145///
146/// # Errors
147///
148/// A [`Error`] is returned on failure. Because different operating systems
149/// handle errors differently it is recommend to not match on a certain error.
150pub 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
155/// Get multiple commands that open `path` with the default application.
156///
157/// Each command represents a launcher to try.
158///
159/// # Examples
160///
161/// ```no_run
162/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
163/// let path = "http://rust-lang.org";
164/// assert!(win_open::commands(path)[0].status()?.success());
165/// # Ok(())
166/// # }
167/// ```
168pub 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
193/// Get a command that uses `app` to open `path`.
194///
195/// # Examples
196///
197/// ```no_run
198/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
199/// let path = "http://rust-lang.org";
200/// assert!(win_open::with_command(path, "app").status()?.success());
201/// # Ok(())
202/// # }
203/// ```
204pub 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
237/// Open path with the default application in a new thread to assure it's non-blocking.
238///
239/// See documentation of [`that()`] for more details.
240pub 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
245/// Open path with the given application in a new thread, which is useful if
246/// the program ends up to be blocking. Otherwise, prefer [`with()`] for
247/// straightforward error handling.
248///
249/// See documentation of [`with()`] for more details.
250pub 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
303/// Open path with the default application using a detached process. which is useful if
304/// the program ends up to be blocking or want to out-live your app
305///
306/// See documentation of [`that()`] for more details.
307pub 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
331/// Open path with the given application using a detached process, which is useful if
332/// the program ends up to be blocking or want to out-live your app. Otherwise, prefer [`with()`] for
333/// straightforward error handling.
334///
335/// See documentation of [`with()`] for more details.
336pub 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        // Try spawning the detached process
343        match cmd.spawn_detached() {
344            Ok(_) => {
345                return Ok(()); // Successfully spawned the detached process
346            }
347            Err(err) => {
348                last_err = Some(err); // Store the error if spawning fails
349            }
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        // This is pretty much lifted from the implementation in Alacritty:
396        // https://github.com/alacritty/alacritty/blob/b9c886872d1202fc9302f68a0bedbb17daa35335/alacritty/src/daemon.rs
397
398        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/// Encodes as wide and adds a null character.
453#[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/// Performs an operation on a specified file.
465///
466/// <https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecuteexw>
467///
468/// # Safety
469/// This function is unsafe because it interacts with raw pointers to the `SHELLEXECUTEINFOW` structure.
470/// The caller must ensure that:
471/// - The pointer `info` is valid and points to a properly initialized `SHELLEXECUTEINFOW` structure.
472/// - The `SHELLEXECUTEINFOW` structure should be correctly populated according to the Windows API documentation.
473/// - The memory referenced by the `info` pointer must remain valid for the duration of the function call.
474///
475/// Failing to meet these conditions could result in undefined behavior, such as dereferencing invalid memory
476/// or passing incorrect data to the Windows API, which could lead to incorrect results or crashes.
477#[allow(non_snake_case)]
478#[cfg(feature = "shellexecute")]
479unsafe fn ShellExecuteExW(info: *mut ffi::SHELLEXECUTEINFOW) -> Result<()> {
480    // ShellExecuteExW returns TRUE (i.e 1) on success
481    // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecuteexw#remarks
482    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// Taken from https://microsoft.github.io/windows-docs-rs/doc/windows/
493/// Opens a Windows Explorer window with specified items in a particular folder selected.
494///
495/// <https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems>
496///
497/// # Safety
498/// This function is unsafe because it interacts with raw pointers and calls a Windows API
499/// function that expects valid, non-null pointers to `ITEMIDLIST` structures. The caller
500/// must ensure that:
501/// - `pidlfolder` points to a valid `ITEMIDLIST` structure.
502/// - `apidl` points to a valid slice of pointers to `ITEMIDLIST` structures (if provided).
503/// - The pointers passed must remain valid for the duration of the function call, as they are used directly by the Windows API.
504///
505/// Failing to adhere to these safety guarantees could result in undefined behavior, such as dereferencing invalid memory.
506#[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    /// Activates and displays a window.
535    /// If the window is minimized, maximized, or arranged, the system restores it to its original size and position.
536    /// An application should specify this flag when displaying the window for the first time.
537    ///
538    /// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow>
539    pub const SW_SHOWNORMAL: i32 = 1;
540
541    // Taken from https://docs.rs/windows-sys/latest/windows_sys/
542    #[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    // Taken from https://docs.rs/windows-sys/latest/windows_sys/
563    #[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    // Taken from https://microsoft.github.io/windows-docs-rs/doc/windows/
571    #[repr(C, packed(1))]
572    pub struct SHITEMID {
573        pub cb: u16,
574        pub abID: [u8; 1],
575    }
576
577    // Taken from https://microsoft.github.io/windows-docs-rs/doc/windows/
578    #[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}