open/
lib.rs

1//! Use this library to open a given path or URL in some application, obeying the current user's desktop configuration.
2//!
3//! It is expected that `http:` and `https:` URLs will open in a web browser, although the desktop configuration may override this.
4//! The choice of application for opening other path types is highly system-dependent.
5//!
6//! To always open a web browser, see the [webbrowser](https://docs.rs/webbrowser) crate, which offers functionality for that specific case.
7//!
8//! # Usage
9//!
10//! Open the given URL in the default web browser, without blocking.
11//!
12//! ```no_run
13//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
14//! open::that("http://rust-lang.org")?;
15//! # Ok(())
16//! # }
17//! ```
18//!
19//! Alternatively, specify the program to be used to open the path or URL.
20//!
21//! ```no_run
22//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
23//! open::with("http://rust-lang.org", "firefox")?;
24//! # Ok(())
25//! # }
26//! ```
27//!
28//! Or obtain the commands to launch a file or path without running them.
29//!
30//! ```no_run
31//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
32//! let cmds = open::commands("http://rust-lang.org")[0].status()?;
33//! # Ok(())
34//! # }
35//! ```
36//!
37//! It's also possible to obtain a command that can be used to open a path in an application.
38//!
39//! ```no_run
40//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
41//! let status = open::with_command("http://rust-lang.org", "firefox").status()?;
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! # Notes
47//!
48//! ## Nonblocking operation
49//!
50//! The functions provided are nonblocking as it will return even though the
51//! launched child process is still running. Note that depending on the operating
52//! system, spawning launch helpers, which this library does under the hood,
53//! might still take 100's of milliseconds.
54//!
55//! **Beware that on some platforms and circumstances, the launcher may block**.
56//! In this case, please use the [`commands()`] or [`with_command()`] accordingly
57//! to `spawn()` it without blocking.
58//!
59//! ## Error handling
60//!
61//! As an operating system program is used, the open operation can fail.
62//! Therefore, you are advised to check the result and behave
63//! accordingly, e.g. by letting the user know that the open operation failed.
64//!
65//! ```no_run
66//! let path = "http://rust-lang.org";
67//!
68//! match open::that(path) {
69//!     Ok(()) => println!("Opened '{}' successfully.", path),
70//!     Err(err) => eprintln!("An error occurred when opening '{}': {}", path, err),
71//! }
72//! ```
73//!
74//! ## UNIX Desktop
75//!
76//! The choice of application for opening a given URL or path on a UNIX desktop is highly dependent on the user's GUI framework (if any) and its configuration.
77//! `open::that()` tries a sequence of opener commands to open the specified path.
78//! The configuration of these openers is dependent on the window system.
79//!
80// On a console, without a window manager, results will likely be poor. The openers expect to be able to open in a new or existing window, something that consoles lack.
81//!
82//! On Windows WSL, `wslview` is tried first, then `xdg-open`. In other UNIX environments, `xdg-open` is tried first. If this fails, a sequence of other openers is tried.
83//!
84//! Currently the `BROWSER` environment variable is ignored even for `http:` and `https:` URLs unless the opener being used happens to respect it.
85//!
86//! It cannot be overemphasized how fragile this all is in UNIX environments. It is common for the various MIME tables to incorrectly specify the application "owning" a given filetype.
87//! It is common for openers to behave strangely. Use with caution, as this crate merely inherits a particular platforms shortcomings.
88
89#[cfg(target_os = "windows")]
90use windows as os;
91
92#[cfg(target_os = "macos")]
93use macos as os;
94
95#[cfg(target_os = "ios")]
96use ios as os;
97
98#[cfg(target_os = "visionos")]
99use ios as os;
100
101#[cfg(target_os = "haiku")]
102use haiku as os;
103
104#[cfg(target_os = "redox")]
105use redox as os;
106
107#[cfg(any(
108    target_os = "linux",
109    target_os = "android",
110    target_os = "freebsd",
111    target_os = "dragonfly",
112    target_os = "netbsd",
113    target_os = "openbsd",
114    target_os = "illumos",
115    target_os = "solaris",
116    target_os = "aix",
117    target_os = "hurd"
118))]
119use unix as os;
120
121#[cfg(not(any(
122    target_os = "linux",
123    target_os = "android",
124    target_os = "freebsd",
125    target_os = "dragonfly",
126    target_os = "netbsd",
127    target_os = "openbsd",
128    target_os = "illumos",
129    target_os = "solaris",
130    target_os = "ios",
131    target_os = "visionos",
132    target_os = "macos",
133    target_os = "windows",
134    target_os = "haiku",
135    target_os = "redox",
136    target_os = "aix",
137    target_os = "hurd"
138)))]
139compile_error!("open is not supported on this platform");
140
141use std::{
142    ffi::OsStr,
143    io,
144    process::{Command, Stdio},
145    thread,
146};
147
148/// Open path with the default application without blocking.
149///
150/// # Examples
151///
152/// ```no_run
153/// let path = "http://rust-lang.org";
154///
155/// match open::that(path) {
156///     Ok(()) => println!("Opened '{}' successfully.", path),
157///     Err(err) => panic!("An error occurred when opening '{}': {}", path, err),
158/// }
159/// ```
160///
161/// # Errors
162///
163/// A [`std::io::Error`] is returned on failure. Because different operating systems
164/// handle errors differently it is recommend to not match on a certain error.
165///
166/// # Beware
167///
168/// Sometimes, depending on the platform and system configuration, launchers *can* block.
169/// If you want to be sure they don't, use [`that_in_background()`] or [`that_detached`] instead.
170pub fn that(path: impl AsRef<OsStr>) -> io::Result<()> {
171    let mut last_err = None;
172    for mut cmd in commands(path) {
173        match cmd.status_without_output() {
174            Ok(status) => {
175                return Ok(status).into_result(&cmd);
176            }
177            Err(err) => last_err = Some(err),
178        }
179    }
180    Err(last_err.expect("no launcher worked, at least one error"))
181}
182
183/// Open path with the given application.
184///
185/// This function may block if the application or launcher doesn't detach itself.
186/// In that case, consider using [`with_in_background()`] or [`with_command()].
187///
188/// # Examples
189///
190/// ```no_run
191/// let path = "http://rust-lang.org";
192/// let app = "firefox";
193///
194/// match open::with(path, app) {
195///     Ok(()) => println!("Opened '{}' successfully.", path),
196///     Err(err) => panic!("An error occurred when opening '{}': {}", path, err),
197/// }
198/// ```
199///
200/// # Errors
201///
202/// A [`std::io::Error`] is returned on failure. Because different operating systems
203/// handle errors differently it is recommend to not match on a certain error.
204pub fn with(path: impl AsRef<OsStr>, app: impl Into<String>) -> io::Result<()> {
205    let mut cmd = with_command(path, app);
206    cmd.status_without_output().into_result(&cmd)
207}
208
209/// Get multiple commands that open `path` with the default application.
210///
211/// Each command represents a launcher to try.
212///
213/// # Examples
214///
215/// ```no_run
216/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
217/// let path = "http://rust-lang.org";
218/// assert!(open::commands(path)[0].status()?.success());
219/// # Ok(())
220/// # }
221/// ```
222pub fn commands(path: impl AsRef<OsStr>) -> Vec<Command> {
223    os::commands(path)
224}
225
226/// Get a command that uses `app` to open `path`.
227///
228/// # Examples
229///
230/// ```no_run
231/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
232/// let path = "http://rust-lang.org";
233/// assert!(open::with_command(path, "app").status()?.success());
234/// # Ok(())
235/// # }
236/// ```
237pub fn with_command(path: impl AsRef<OsStr>, app: impl Into<String>) -> Command {
238    os::with_command(path, app)
239}
240
241/// Open path with the default application in a new thread to assure it's non-blocking.
242///
243/// See documentation of [`that()`] for more details.
244pub fn that_in_background(path: impl AsRef<OsStr>) -> thread::JoinHandle<io::Result<()>> {
245    let path = path.as_ref().to_os_string();
246    thread::spawn(|| that(path))
247}
248
249/// Open path with the given application in a new thread, which is useful if
250/// the program ends up to be blocking. Otherwise, prefer [`with()`] for
251/// straightforward error handling.
252///
253/// See documentation of [`with()`] for more details.
254pub fn with_in_background<T: AsRef<OsStr>>(
255    path: T,
256    app: impl Into<String>,
257) -> thread::JoinHandle<io::Result<()>> {
258    let path = path.as_ref().to_os_string();
259    let app = app.into();
260    thread::spawn(|| with(path, app))
261}
262
263/// Open path with the default application using a detached process. which is useful if
264/// the program ends up to be blocking or want to out-live your app
265///
266/// See documentation of [`that()`] for more details.
267pub fn that_detached(path: impl AsRef<OsStr>) -> io::Result<()> {
268    #[cfg(any(not(feature = "shellexecute-on-windows"), not(windows)))]
269    {
270        let mut last_err = None;
271        for mut cmd in commands(path) {
272            match cmd.spawn_detached() {
273                Ok(_) => {
274                    return Ok(());
275                }
276                Err(err) => last_err = Some(err),
277            }
278        }
279        Err(last_err.expect("no launcher worked, at least one error"))
280    }
281
282    #[cfg(all(windows, feature = "shellexecute-on-windows"))]
283    {
284        windows::that_detached(path)
285    }
286}
287
288/// Open path with the given application using a detached process, which is useful if
289/// the program ends up to be blocking or want to out-live your app. Otherwise, prefer [`with()`] for
290/// straightforward error handling.
291///
292/// See documentation of [`with()`] for more details.
293pub fn with_detached<T: AsRef<OsStr>>(path: T, app: impl Into<String>) -> io::Result<()> {
294    #[cfg(any(not(feature = "shellexecute-on-windows"), not(windows)))]
295    {
296        let mut cmd = with_command(path, app);
297        cmd.spawn_detached()
298    }
299
300    #[cfg(all(windows, feature = "shellexecute-on-windows"))]
301    {
302        windows::with_detached(path, app)
303    }
304}
305
306trait IntoResult<T> {
307    fn into_result(self, cmd: &Command) -> T;
308}
309
310impl IntoResult<io::Result<()>> for io::Result<std::process::ExitStatus> {
311    fn into_result(self, cmd: &Command) -> io::Result<()> {
312        match self {
313            Ok(status) if status.success() => Ok(()),
314            Ok(status) => Err(io::Error::new(
315                io::ErrorKind::Other,
316                format!("Launcher {cmd:?} failed with {:?}", status),
317            )),
318            Err(err) => Err(err),
319        }
320    }
321}
322
323trait CommandExt {
324    fn status_without_output(&mut self) -> io::Result<std::process::ExitStatus>;
325    fn spawn_detached(&mut self) -> io::Result<()>;
326}
327
328impl CommandExt for Command {
329    fn status_without_output(&mut self) -> io::Result<std::process::ExitStatus> {
330        self.stdin(Stdio::null())
331            .stdout(Stdio::null())
332            .stderr(Stdio::null())
333            .status()
334    }
335
336    fn spawn_detached(&mut self) -> io::Result<()> {
337        // This is pretty much lifted from the implementation in Alacritty:
338        // https://github.com/alacritty/alacritty/blob/b9c886872d1202fc9302f68a0bedbb17daa35335/alacritty/src/daemon.rs
339
340        self.stdin(Stdio::null())
341            .stdout(Stdio::null())
342            .stderr(Stdio::null());
343
344        #[cfg(unix)]
345        unsafe {
346            use std::os::unix::process::CommandExt as _;
347
348            self.pre_exec(move || {
349                match libc::fork() {
350                    -1 => return Err(io::Error::last_os_error()),
351                    0 => (),
352                    _ => libc::_exit(0),
353                }
354
355                if libc::setsid() == -1 {
356                    return Err(io::Error::last_os_error());
357                }
358
359                Ok(())
360            });
361        }
362        #[cfg(windows)]
363        {
364            use std::os::windows::process::CommandExt;
365            const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
366            const CREATE_NO_WINDOW: u32 = 0x08000000;
367            self.creation_flags(CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW);
368        }
369
370        self.spawn().map(|_| ())
371    }
372}
373
374#[cfg(windows)]
375mod windows;
376
377#[cfg(target_os = "macos")]
378mod macos;
379
380#[cfg(target_os = "ios")]
381mod ios;
382
383#[cfg(target_os = "visionos")]
384mod ios;
385
386#[cfg(target_os = "haiku")]
387mod haiku;
388
389#[cfg(target_os = "redox")]
390mod redox;
391
392#[cfg(any(
393    target_os = "linux",
394    target_os = "android",
395    target_os = "freebsd",
396    target_os = "dragonfly",
397    target_os = "netbsd",
398    target_os = "openbsd",
399    target_os = "illumos",
400    target_os = "solaris",
401    target_os = "aix",
402    target_os = "hurd"
403))]
404mod unix;