use std::ffi::OsStr;
use std::process::{Command, Stdio};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Opener {
pub program: &'static str,
pub args: &'static [&'static str],
}
pub fn opener_for(os: &str) -> Option<Opener> {
let opener = match os {
"macos" => Opener {
program: "open",
args: &[],
},
"linux" | "freebsd" | "netbsd" | "openbsd" | "dragonfly" | "solaris" | "illumos" => {
Opener {
program: "xdg-open",
args: &[],
}
}
"windows" => Opener {
program: "cmd",
args: &["/C", "start", ""],
},
_ => return None,
};
Some(opener)
}
pub fn opener() -> Option<Opener> {
opener_for(std::env::consts::OS)
}
pub fn open(path: &OsStr) -> Result<(), String> {
open_with(opener(), path)
}
fn open_with(opener: Option<Opener>, path: &OsStr) -> Result<(), String> {
let opener =
opener.ok_or_else(|| format!("no default opener known for {}", std::env::consts::OS))?;
spawn(opener, path).map_err(|error| {
format!(
"cannot open {} with {}: {error}",
path.to_string_lossy(),
opener.program
)
})
}
fn spawn(opener: Opener, path: &OsStr) -> std::io::Result<()> {
Command::new(opener.program)
.args(opener.args)
.arg(path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_known_platform_names_its_opener() {
assert_eq!(
opener_for("macos"),
Some(Opener {
program: "open",
args: &[]
})
);
assert_eq!(
opener_for("linux"),
Some(Opener {
program: "xdg-open",
args: &[]
})
);
assert_eq!(
opener_for("windows"),
Some(Opener {
program: "cmd",
args: &["/C", "start", ""]
})
);
}
#[test]
fn the_bsds_share_the_freedesktop_opener() {
for os in ["freebsd", "netbsd", "openbsd", "dragonfly", "illumos"] {
assert_eq!(opener_for(os).unwrap().program, "xdg-open", "{os}");
}
}
#[test]
fn an_unknown_platform_has_no_opener() {
assert_eq!(opener_for("haiku"), None);
}
#[test]
fn the_platform_ite_is_built_for_has_an_opener() {
assert!(opener().is_some(), "{}", std::env::consts::OS);
}
#[test]
fn the_path_is_the_final_argument_and_the_child_is_detached() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("marker");
let opener = Opener {
program: "sh",
args: &["-c", "printf %s \"$0\" > \"$(dirname \"$0\")/out\""],
};
spawn(opener, target.as_os_str()).unwrap();
let out = dir.path().join("out");
for _ in 0..50 {
if out.exists() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert_eq!(
std::fs::read_to_string(out).unwrap(),
target.display().to_string()
);
}
#[test]
fn an_unstartable_opener_names_the_path_and_the_program() {
let error = open_with(
Some(Opener {
program: "ite-nonexistent-opener",
args: &[],
}),
OsStr::new("/some/file"),
)
.unwrap_err();
assert!(error.contains("/some/file"), "{error}");
assert!(error.contains("ite-nonexistent-opener"), "{error}");
}
#[test]
fn a_platform_without_an_opener_says_so_rather_than_guessing() {
let error = open_with(None, OsStr::new("/some/file")).unwrap_err();
assert!(error.contains("no default opener"), "{error}");
}
}