| 1 | #![cfg_attr (docsrs, feature(doc_auto_cfg))] |
| 2 | |
| 3 | //! This crate provides the [`open`] function, which opens a file or link with the default program |
| 4 | //! configured on the system: |
| 5 | //! |
| 6 | //! ```no_run |
| 7 | //! # fn main() -> Result<(), ::opener::OpenError> { |
| 8 | //! // open a website |
| 9 | //! opener::open("https://www.rust-lang.org" )?; |
| 10 | //! |
| 11 | //! // open a file |
| 12 | //! opener::open("../Cargo.toml" )?; |
| 13 | //! # Ok(()) |
| 14 | //! # } |
| 15 | //! ``` |
| 16 | //! |
| 17 | //! An [`open_browser`] function is also provided, for when you intend on opening a file or link in |
| 18 | //! a browser, specifically. This function works like the [`open`] function, but explicitly allows |
| 19 | //! overriding the browser launched by setting the `$BROWSER` environment variable. |
| 20 | //! |
| 21 | //! # Crate features |
| 22 | //! |
| 23 | //! - **reveal** - Enables usage of the [`reveal`] function. On Linux this will pull in the |
| 24 | //! [`dbus`](https://docs.rs/dbus) crate. If the **dbus-vendored** feature is enabled (which it |
| 25 | //! is by default), dbus will use static linking. Otherwise it will be dynamically linked, and |
| 26 | //! some system dependencies will be required to build `dbus` and also to run any built binaries. |
| 27 | //! Refer to the [`dbus` docs](https://github.com/diwic/dbus-rs#requirements) for the specifics. |
| 28 | //! - **dbus-vendored** - See the **reveal** feature. |
| 29 | |
| 30 | #![warn ( |
| 31 | rust_2018_idioms, |
| 32 | deprecated_in_future, |
| 33 | macro_use_extern_crate, |
| 34 | missing_debug_implementations, |
| 35 | unused_qualifications |
| 36 | )] |
| 37 | |
| 38 | #[cfg (all(feature = "reveal" , target_os = "linux" ))] |
| 39 | mod freedesktop; |
| 40 | #[cfg (not(any(target_os = "windows" , target_os = "macos" )))] |
| 41 | mod linux_and_more; |
| 42 | #[cfg (target_os = "macos" )] |
| 43 | mod macos; |
| 44 | #[cfg (target_os = "windows" )] |
| 45 | mod windows; |
| 46 | |
| 47 | #[cfg (not(any(target_os = "windows" , target_os = "macos" )))] |
| 48 | use crate::linux_and_more as sys; |
| 49 | #[cfg (target_os = "macos" )] |
| 50 | use crate::macos as sys; |
| 51 | #[cfg (target_os = "windows" )] |
| 52 | use crate::windows as sys; |
| 53 | |
| 54 | use std::error::Error; |
| 55 | use std::ffi::{OsStr, OsString}; |
| 56 | use std::fmt::{self, Display, Formatter}; |
| 57 | use std::process::{Command, ExitStatus, Stdio}; |
| 58 | use std::{env, io}; |
| 59 | |
| 60 | /// Opens a file or link with the system default program. |
| 61 | /// |
| 62 | /// Note that a path like "rustup.rs" could potentially refer to either a file or a website. If you |
| 63 | /// want to open the website, you should add the "http://" prefix, for example. |
| 64 | /// |
| 65 | /// Also note that a result of `Ok(())` just means a way of opening the path was found, and no error |
| 66 | /// occurred as a direct result of opening the path. Errors beyond that point aren't caught. For |
| 67 | /// example, `Ok(())` would be returned even if a file was opened with a program that can't read the |
| 68 | /// file, or a dead link was opened in a browser. |
| 69 | /// |
| 70 | /// ## Platform Implementation Details |
| 71 | /// |
| 72 | /// - On Windows the `ShellExecuteW` Windows API function is used. |
| 73 | /// - On Mac the system `open` command is used. |
| 74 | /// - On Windows Subsystem for Linux (WSL), the system `wslview` from [`wslu`] is used if available, |
| 75 | /// otherwise the system `xdg-open` is used, if available. |
| 76 | /// - On non-WSL Linux and other platforms, |
| 77 | /// the system `xdg-open` script is used if available, otherwise an `xdg-open` script embedded in |
| 78 | /// this library is used. |
| 79 | /// |
| 80 | /// [`wslu`]: https://github.com/wslutilities/wslu/ |
| 81 | pub fn open<P>(path: P) -> Result<(), OpenError> |
| 82 | where |
| 83 | P: AsRef<OsStr>, |
| 84 | { |
| 85 | sys::open(path.as_ref()) |
| 86 | } |
| 87 | |
| 88 | /// Opens a file or link with the system default program, using the `BROWSER` environment variable |
| 89 | /// when set. |
| 90 | /// |
| 91 | /// If the `BROWSER` environment variable is set, the program specified by it is used to open the |
| 92 | /// path. If not, behavior is identical to [`open()`]. |
| 93 | pub fn open_browser<P>(path: P) -> Result<(), OpenError> |
| 94 | where |
| 95 | P: AsRef<OsStr>, |
| 96 | { |
| 97 | let mut path = path.as_ref(); |
| 98 | if let Ok(browser_var) = env::var("BROWSER" ) { |
| 99 | let windows_path; |
| 100 | if is_wsl() && browser_var.ends_with(".exe" ) { |
| 101 | if let Some(windows_path_2) = wsl_to_windows_path(path) { |
| 102 | windows_path = windows_path_2; |
| 103 | path = &windows_path; |
| 104 | } |
| 105 | }; |
| 106 | |
| 107 | Command::new(&browser_var) |
| 108 | .arg(path) |
| 109 | .stdin(Stdio::null()) |
| 110 | .stdout(Stdio::null()) |
| 111 | .stderr(Stdio::piped()) |
| 112 | .spawn() |
| 113 | .map_err(|err| OpenError::Spawn { |
| 114 | cmds: browser_var, |
| 115 | source: err, |
| 116 | })?; |
| 117 | |
| 118 | Ok(()) |
| 119 | } else { |
| 120 | sys::open(path) |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | /// Opens the default file explorer and reveals a file or folder in its containing folder. |
| 125 | /// |
| 126 | /// ## Errors |
| 127 | /// This function may or may not return an error if the path does not exist. |
| 128 | /// |
| 129 | /// ## Platform Implementation Details |
| 130 | /// - On Windows and Windows Subsystem for Linux (WSL) the `explorer.exe /select, <path>` command is used. |
| 131 | /// - On Mac the system `open -R` command is used. |
| 132 | /// - On non-WSL Linux the [`file-manager-interface`] or the [`org.freedesktop.portal.OpenURI`] DBus Interface is used if available, |
| 133 | /// falling back to opening the containing folder with [`open`]. |
| 134 | /// - On other platforms, the containing folder is shown with [`open`]. |
| 135 | /// |
| 136 | /// [`org.freedesktop.portal.OpenURI`]: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.OpenURI |
| 137 | /// [`file-manager-interface`]: https://freedesktop.org/wiki/Specifications/file-manager-interface/ |
| 138 | #[cfg (feature = "reveal" )] |
| 139 | pub fn reveal<P>(path: P) -> Result<(), OpenError> |
| 140 | where |
| 141 | P: AsRef<std::path::Path>, |
| 142 | { |
| 143 | sys::reveal(path.as_ref()) |
| 144 | } |
| 145 | |
| 146 | /// An error type representing the failure to open a path. Possibly returned by the [`open`] |
| 147 | /// function. |
| 148 | #[non_exhaustive ] |
| 149 | #[derive(Debug)] |
| 150 | pub enum OpenError { |
| 151 | /// An IO error occurred. |
| 152 | Io(io::Error), |
| 153 | |
| 154 | /// There was an error spawning command(s). |
| 155 | Spawn { |
| 156 | /// The command(s) that failed to spawn. |
| 157 | cmds: String, |
| 158 | |
| 159 | /// The underlying error. |
| 160 | source: io::Error, |
| 161 | }, |
| 162 | |
| 163 | /// A command exited with a non-zero exit status. |
| 164 | ExitStatus { |
| 165 | /// A string that identifies the command. |
| 166 | cmd: &'static str, |
| 167 | |
| 168 | /// The failed process's exit status. |
| 169 | status: ExitStatus, |
| 170 | |
| 171 | /// Anything the process wrote to stderr. |
| 172 | stderr: String, |
| 173 | }, |
| 174 | } |
| 175 | |
| 176 | impl Display for OpenError { |
| 177 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { |
| 178 | match self { |
| 179 | OpenError::Io(_) => { |
| 180 | write!(f, "IO error" )?; |
| 181 | } |
| 182 | OpenError::Spawn { cmds, source: _ } => { |
| 183 | write!(f, "error spawning command(s) '{cmds}'" )?; |
| 184 | } |
| 185 | OpenError::ExitStatus { |
| 186 | cmd, |
| 187 | status, |
| 188 | stderr, |
| 189 | } => { |
| 190 | write!(f, "command '{cmd}' did not execute successfully; {status}" )?; |
| 191 | |
| 192 | let stderr = stderr.trim(); |
| 193 | if !stderr.is_empty() { |
| 194 | write!(f, " \ncommand stderr: \n{stderr}" )?; |
| 195 | } |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | Ok(()) |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | impl Error for OpenError { |
| 204 | fn source(&self) -> Option<&(dyn Error + 'static)> { |
| 205 | match self { |
| 206 | OpenError::Io(inner: &Error) => Some(inner), |
| 207 | OpenError::Spawn { cmds: _, source: &Error } => Some(source), |
| 208 | OpenError::ExitStatus { .. } => None, |
| 209 | } |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | #[cfg (target_os = "linux" )] |
| 214 | fn is_wsl() -> bool { |
| 215 | sys::is_wsl() |
| 216 | } |
| 217 | |
| 218 | #[cfg (not(target_os = "linux" ))] |
| 219 | fn is_wsl() -> bool { |
| 220 | false |
| 221 | } |
| 222 | |
| 223 | #[cfg (target_os = "linux" )] |
| 224 | fn wsl_to_windows_path(path: &OsStr) -> Option<OsString> { |
| 225 | use bstr::ByteSlice; |
| 226 | use std::os::unix::ffi::OsStringExt; |
| 227 | |
| 228 | let output: ! = Command&mut Command::new("wslpath" ) |
| 229 | .arg("-w" ) |
| 230 | .arg(path) |
| 231 | .stdin(Stdio::null()) |
| 232 | .stdout(Stdio::piped()) |
| 233 | .stderr(cfg:Stdio::null()) |
| 234 | .output() |
| 235 | .ok()?; |
| 236 | |
| 237 | if !output.status.success() { |
| 238 | return None; |
| 239 | } |
| 240 | |
| 241 | Some(OsString::from_vec(output.stdout.trim_end().to_vec())) |
| 242 | } |
| 243 | |
| 244 | #[cfg (not(target_os = "linux" ))] |
| 245 | fn wsl_to_windows_path(_path: &OsStr) -> Option<OsString> { |
| 246 | unreachable!() |
| 247 | } |
| 248 | |
| 249 | #[cfg (not(target_os = "windows" ))] |
| 250 | fn wait_child(child: &mut std::process::Child, cmd_name: &'static str) -> Result<(), OpenError> { |
| 251 | use std::io::Read; |
| 252 | |
| 253 | let exit_status: ExitStatus = child.wait().map_err(OpenError::Io)?; |
| 254 | if exit_status.success() { |
| 255 | Ok(()) |
| 256 | } else { |
| 257 | let mut stderr_output = String::new(); |
| 258 | if let Some(stderr) = child.stderr.as_mut() { |
| 259 | stderr.read_to_string(&mut stderr_output).ok(); |
| 260 | } |
| 261 | |
| 262 | Err(OpenError::ExitStatus { |
| 263 | cmd: cmd_name, |
| 264 | status: exit_status, |
| 265 | stderr: stderr_output, |
| 266 | }) |
| 267 | } |
| 268 | } |
| 269 | |