Skip to content

Commit a81cb92

Browse files
committed
Revamp
1 parent 63317a1 commit a81cb92

File tree

6 files changed

+207
-61
lines changed

6 files changed

+207
-61
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- `open_browser()`, which uses the `$BROWSER` environment variable before falling back to `open()`.
10+
- WSL-specific implementation. Previously, WSL used the same implementation as Linux. Now, the strategy is to use the
11+
system `xdg-open` if available, otherwise we try using the system's `wslview` command from
12+
[`wslu`](https://github.com/wslutilities/wslu).
13+
### Changed
14+
- On Linux (non-WSL), the system `xdg-open` is now used if present. Otherwise, the bundled version is used, as before.
15+
- The command name in the `OpenError::ExitStatus` variant is now returned as a `Cow<'static, str>` instead of a
16+
`&'static str`.
817
### Removed
918
- `impl From<io::Error> for OpenError`.
1019

opener-bin/src/main.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,33 @@
33
deprecated_in_future,
44
macro_use_extern_crate,
55
missing_debug_implementations,
6-
unused_qualifications,
7-
clippy::cast_possible_truncation
6+
unused_qualifications
87
)]
98

109
use std::{path::PathBuf, process};
1110
use structopt::StructOpt;
1211

1312
#[derive(Debug, StructOpt)]
14-
struct Cli {
13+
struct Args {
14+
/// The path to open
1515
#[structopt(parse(from_os_str))]
1616
path: PathBuf,
17+
18+
/// Open the path with the `open_browser()` function instead of the `open` function
19+
#[structopt(long = "browser")]
20+
browser: bool,
1721
}
1822

1923
fn main() {
20-
let args = Cli::from_args();
24+
let args = Args::from_args();
25+
26+
let open_result = if args.browser {
27+
opener::open_browser(&args.path)
28+
} else {
29+
opener::open(&args.path)
30+
};
2131

22-
match opener::open(&args.path) {
32+
match open_result {
2333
Ok(()) => {
2434
println!("Opened path successfully.");
2535
}

opener/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,8 @@ maintenance = { status = "passively-maintained" }
1818
[dev-dependencies]
1919
version-sync = "0.9"
2020

21+
[target.'cfg(target_os = "linux")'.dependencies]
22+
wsl = "0.1"
23+
2124
[target.'cfg(windows)'.dependencies]
2225
winapi = { version = "0.3", features = ["shellapi"] }

opener/src/lib.rs

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![doc(html_root_url = "https://docs.rs/opener/0.4.1")]
22

33
//! This crate provides the [`open`] function, which opens a file or link with the default program
4-
//! configured on the system.
4+
//! configured on the system:
55
//!
66
//! ```no_run
77
//! # fn main() -> Result<(), ::opener::OpenError> {
@@ -14,10 +14,9 @@
1414
//! # }
1515
//! ```
1616
//!
17-
//! ## Platform Implementation Details
18-
//! On Windows the `ShellExecuteW` Windows API function is used. On Mac the system `open` command is
19-
//! used. On other platforms, the `xdg-open` script is used. The system `xdg-open` is not used;
20-
//! instead a version is embedded within this library.
17+
//! An [`open_browser`] function is also provided, for when you intend on opening a file or link in a
18+
//! browser, specifically. This function works like the [`open`] function, but explicitly allows
19+
//! overriding the browser launched by setting the `$BROWSER` environment variable.
2120
2221
#![warn(
2322
rust_2018_idioms,
@@ -35,18 +34,21 @@ mod macos;
3534
mod windows;
3635

3736
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
38-
use crate::linux_and_more::open as open_sys;
37+
use crate::linux_and_more as sys;
3938
#[cfg(target_os = "macos")]
40-
use crate::macos::open as open_sys;
39+
use crate::macos as sys;
4140
#[cfg(target_os = "windows")]
42-
use crate::windows::open as open_sys;
41+
use crate::windows as sys;
4342

4443
use std::{
44+
borrow::Cow,
45+
env,
4546
error::Error,
46-
ffi::OsStr,
47+
ffi::{OsStr, OsString},
4748
fmt::{self, Display, Formatter},
4849
io,
49-
process::ExitStatus,
50+
io::Read,
51+
process::{Child, Command, ExitStatus, Stdio},
5052
};
5153

5254
/// Opens a file or link with the system default program.
@@ -58,11 +60,56 @@ use std::{
5860
/// occurred as a direct result of opening the path. Errors beyond that point aren't caught. For
5961
/// example, `Ok(())` would be returned even if a file was opened with a program that can't read the
6062
/// file, or a dead link was opened in a browser.
63+
///
64+
/// ## Platform Implementation Details
65+
///
66+
/// - On Windows the `ShellExecuteW` Windows API function is used.
67+
/// - On Mac the system `open` command is used.
68+
/// - On Windows Subsystem for Linux (WSL), the system `xdg-open` will be used if available,
69+
/// otherwise the system `wslview` from [`wslu`] is used.
70+
/// - On non-WSL Linux and other platforms,
71+
/// the system `xdg-open` script is used if available, otherwise an `xdg-open` script embedded in
72+
/// this library is used.
73+
///
74+
/// [`wslu`]: https://github.com/wslutilities/wslu/
6175
pub fn open<P>(path: P) -> Result<(), OpenError>
6276
where
6377
P: AsRef<OsStr>,
6478
{
65-
open_sys(path.as_ref())
79+
sys::open(path.as_ref())
80+
}
81+
82+
/// Opens a file or link with the system default program, using the `BROWSER` environment variable
83+
/// when set.
84+
///
85+
/// If the `BROWSER` environment variable is set, the program specified by it is used to open the
86+
/// path. If not, behavior is identical to [`open()`].
87+
pub fn open_browser<P>(path: P) -> Result<(), OpenError>
88+
where
89+
P: AsRef<OsStr>,
90+
{
91+
let mut path = path.as_ref();
92+
if let Ok(browser_var) = env::var("BROWSER") {
93+
let windows_path;
94+
if is_wsl() && browser_var.ends_with(".exe") {
95+
if let Some(windows_path_2) = wsl_to_windows_path(path) {
96+
windows_path = windows_path_2;
97+
path = &windows_path;
98+
}
99+
};
100+
101+
let mut browser_child = Command::new(&browser_var)
102+
.arg(path)
103+
.stdin(Stdio::null())
104+
.stdout(Stdio::null())
105+
.stderr(Stdio::piped())
106+
.spawn()
107+
.map_err(OpenError::Io)?;
108+
109+
wait_child(&mut browser_child, browser_var.into())
110+
} else {
111+
sys::open(path)
112+
}
66113
}
67114

68115
/// An error type representing the failure to open a path. Possibly returned by the [`open`]
@@ -74,10 +121,10 @@ pub enum OpenError {
74121
/// An IO error occurred.
75122
Io(io::Error),
76123

77-
/// The command exited with a non-zero exit status.
124+
/// A command exited with a non-zero exit status.
78125
ExitStatus {
79126
/// A string that identifies the command.
80-
cmd: &'static str,
127+
cmd: Cow<'static, str>,
81128

82129
/// The failed process's exit status.
83130
status: ExitStatus,
@@ -123,3 +170,56 @@ impl Error for OpenError {
123170
}
124171
}
125172
}
173+
174+
#[cfg(target_os = "linux")]
175+
fn is_wsl() -> bool {
176+
wsl::is_wsl()
177+
}
178+
179+
#[cfg(not(target_os = "linux"))]
180+
fn is_wsl() -> bool {
181+
false
182+
}
183+
184+
#[cfg(target_os = "linux")]
185+
fn wsl_to_windows_path(path: &OsStr) -> Option<OsString> {
186+
use std::os::unix::ffi::OsStringExt;
187+
188+
let output = Command::new("wslpath")
189+
.arg("-w")
190+
.arg(path)
191+
.stdin(Stdio::null())
192+
.stdout(Stdio::piped())
193+
.stderr(Stdio::null())
194+
.output()
195+
.ok()?;
196+
197+
if !output.status.success() {
198+
return None;
199+
}
200+
201+
Some(OsString::from_vec(output.stdout))
202+
}
203+
204+
#[cfg(not(target_os = "linux"))]
205+
fn wsl_to_windows_path(path: &OsStr) -> Option<OsString> {
206+
unreachable!()
207+
}
208+
209+
fn wait_child(child: &mut Child, cmd_name: Cow<'static, str>) -> Result<(), OpenError> {
210+
let exit_status = child.wait().map_err(OpenError::Io)?;
211+
if exit_status.success() {
212+
Ok(())
213+
} else {
214+
let mut stderr_output = String::new();
215+
if let Some(stderr) = child.stderr.as_mut() {
216+
stderr.read_to_string(&mut stderr_output).ok();
217+
}
218+
219+
Err(OpenError::ExitStatus {
220+
cmd: cmd_name,
221+
status: exit_status,
222+
stderr: stderr_output,
223+
})
224+
}
225+
}

opener/src/linux_and_more.rs

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,83 @@
11
use crate::OpenError;
22
use std::{
33
ffi::OsStr,
4-
io::{Read, Write},
4+
io::Write,
55
process::{Command, Stdio},
66
};
77

88
const XDG_OPEN_SCRIPT: &[u8] = include_bytes!("xdg-open");
99

1010
pub(crate) fn open(path: &OsStr) -> Result<(), OpenError> {
11-
let mut cmd = Command::new("sh")
12-
.args(&["-s"])
11+
if crate::is_wsl() {
12+
wsl_open(path)
13+
} else {
14+
non_wsl_open(path)
15+
}
16+
}
17+
18+
fn wsl_open(path: &OsStr) -> Result<(), OpenError> {
19+
let system_xdg_open = Command::new("xdg-open")
1320
.arg(path)
14-
.stdin(Stdio::piped())
21+
.stdin(Stdio::null())
22+
.stdout(Stdio::null())
23+
.stderr(Stdio::piped())
24+
.spawn();
25+
26+
if let Ok(mut child) = system_xdg_open {
27+
return crate::wait_child(&mut child, "xdg-open (system)".into());
28+
}
29+
30+
let mut wslview = Command::new("wslview")
31+
.arg(path)
32+
.stdin(Stdio::null())
1533
.stdout(Stdio::null())
1634
.stderr(Stdio::piped())
1735
.spawn()
1836
.map_err(OpenError::Io)?;
1937

20-
let child_stdin = cmd.stdin.as_mut().unwrap();
21-
child_stdin
22-
.write_all(XDG_OPEN_SCRIPT)
23-
.map_err(OpenError::Io)?;
38+
crate::wait_child(&mut wslview, "wslview".into())
39+
}
40+
41+
fn non_wsl_open(path: &OsStr) -> Result<(), OpenError> {
42+
let system_xdg_open = Command::new("xdg-open")
43+
.arg(path)
44+
.stdin(Stdio::null())
45+
.stdout(Stdio::null())
46+
.stderr(Stdio::piped())
47+
.spawn();
2448

25-
let exit_status = cmd.wait().map_err(OpenError::Io)?;
26-
if exit_status.success() {
27-
Ok(())
49+
let system_xdg_open_used;
50+
let mut xdg_open = match system_xdg_open {
51+
Ok(child) => {
52+
system_xdg_open_used = true;
53+
child
54+
}
55+
Err(_) => {
56+
system_xdg_open_used = false;
57+
let mut sh = Command::new("sh")
58+
.arg("-s")
59+
.arg(path)
60+
.stdin(Stdio::piped())
61+
.stdout(Stdio::null())
62+
.stderr(Stdio::piped())
63+
.spawn()
64+
.map_err(OpenError::Io)?;
65+
66+
sh.stdin
67+
.as_mut()
68+
.unwrap()
69+
.write_all(XDG_OPEN_SCRIPT)
70+
.map_err(OpenError::Io)?;
71+
72+
sh
73+
}
74+
};
75+
76+
let cmd_name = if system_xdg_open_used {
77+
"xdg-open (system)"
2878
} else {
29-
let mut stderr_output = String::new();
30-
cmd.stderr
31-
.as_mut()
32-
.unwrap()
33-
.read_to_string(&mut stderr_output)
34-
.ok();
35-
36-
Err(OpenError::ExitStatus {
37-
cmd: "xdg-open (internal)",
38-
status: exit_status,
39-
stderr: stderr_output,
40-
})
41-
}
79+
"xdg-open (internal)"
80+
};
81+
82+
crate::wait_child(&mut xdg_open, cmd_name.into())
4283
}

opener/src/macos.rs

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,17 @@
11
use crate::OpenError;
22
use std::{
33
ffi::OsStr,
4-
io::Read,
54
process::{Command, Stdio},
65
};
76

87
pub(crate) fn open(path: &OsStr) -> Result<(), OpenError> {
9-
let mut cmd = Command::new("open")
8+
let mut open = Command::new("open")
109
.arg(path)
1110
.stdin(Stdio::null())
1211
.stdout(Stdio::null())
1312
.stderr(Stdio::piped())
1413
.spawn()
1514
.map_err(OpenError::Io)?;
1615

17-
let exit_status = cmd.wait().map_err(OpenError::Io)?;
18-
if exit_status.success() {
19-
Ok(())
20-
} else {
21-
let mut stderr_output = String::new();
22-
cmd.stderr
23-
.as_mut()
24-
.unwrap()
25-
.read_to_string(&mut stderr_output)
26-
.ok();
27-
28-
Err(OpenError::ExitStatus {
29-
cmd: "open",
30-
status: exit_status,
31-
stderr: stderr_output,
32-
})
33-
}
16+
crate::wait_child(&mut open, "open".into())
3417
}

0 commit comments

Comments
 (0)