#![allow(clippy::upper_case_acronyms, unused_assignments, dead_code)]
#[cfg(not(target_os = "windows"))]
compile_error!("open is not supported on this platform");
use std::{
ffi::{OsStr, OsString},
os::windows::process::CommandExt as WinCommandExt,
process::{Command, Stdio},
sync::OnceLock,
};
pub use error::Error;
use error::ErrorKind;
pub use error::Result;
pub use shell::WindowsShell;
mod error;
mod shell;
const CREATE_NO_WINDOW: u32 = 0x08000000;
static DETECTED_SHELL: OnceLock<WindowsShell> = OnceLock::new();
pub fn that(path: impl AsRef<OsStr>) -> Result<()> {
let mut last_err = None;
for mut cmd in commands(path) {
match cmd.status_without_output() {
Ok(status) => {
return Ok(status).into_result(cmd);
}
Err(err) => last_err = Some(err),
}
}
Err(last_err.map_or_else(
|| Error::new(ErrorKind::NO_LAUNCHER, ""),
|err| Error::new(ErrorKind::IO, err.to_string().as_str()),
))
}
pub fn with(path: impl AsRef<OsStr>, app: impl Into<String>) -> Result<()> {
let mut cmd = with_command(path, app);
cmd.status_without_output().into_result(cmd)
}
pub fn commands<T: AsRef<OsStr>>(path: T) -> Vec<Command> {
let shell = detect_shell().as_str();
let mut cmd = Command::new(shell);
match shell {
"pwsh" => cmd
.arg("-NoProfile")
.arg("-Command")
.arg("Start-Process")
.arg(wrap_in_quotes(path.as_ref()))
.creation_flags(CREATE_NO_WINDOW),
"nu" => cmd
.arg("-c")
.arg(format!("open {}", wrap_in_quotes_string(path.as_ref())))
.creation_flags(CREATE_NO_WINDOW),
"cmd" => cmd
.arg("/c")
.arg("start")
.raw_arg("\"\"")
.raw_arg(wrap_in_quotes(path))
.creation_flags(CREATE_NO_WINDOW),
_ => panic!("No supported shell detected."),
};
vec![cmd]
}
pub fn with_command<T: AsRef<OsStr>>(path: T, app: impl Into<String>) -> Command {
let shell = detect_shell().as_str();
let mut cmd = Command::new(shell);
match shell {
"pwsh" => cmd
.arg("-NoProfile")
.arg("-Command")
.arg("Start-Process")
.arg(wrap_in_quotes(path.as_ref()))
.arg(wrap_in_quotes(app.into()))
.creation_flags(CREATE_NO_WINDOW),
"nu" => cmd
.arg("-c")
.arg(format!(
"open {} {}",
wrap_in_quotes_string(path.as_ref()),
wrap_in_quotes_string(app.into())
))
.creation_flags(CREATE_NO_WINDOW),
"cmd" => cmd
.arg("/c")
.arg("start")
.raw_arg("\"\"")
.raw_arg(wrap_in_quotes(path))
.raw_arg(wrap_in_quotes(app.into()))
.creation_flags(CREATE_NO_WINDOW),
_ => panic!("No supported shell detected."),
};
cmd
}
pub fn that_in_background(path: impl AsRef<OsStr>) -> std::thread::JoinHandle<Result<()>> {
let path = path.as_ref().to_os_string();
std::thread::spawn(|| that(path))
}
pub fn with_in_background<T: AsRef<OsStr>>(
path: T,
app: impl Into<String>,
) -> std::thread::JoinHandle<Result<()>> {
let path = path.as_ref().to_os_string();
let app = app.into();
std::thread::spawn(|| with(path, app))
}
fn detect_shell() -> WindowsShell {
*DETECTED_SHELL.get_or_init(|| match get_shell() {
Ok(shell) => shell,
Err(err) => {
panic!("Failed to detect a supported shell: {}", err);
}
})
}
fn get_shell() -> Result<WindowsShell> {
if Command::new("pwsh")
.arg("-Command")
.arg("$PSVersionTable.PSVersion")
.status_without_output()
.map_or(false, |status| status.success())
{
return "pwsh".try_into();
}
if Command::new("nu")
.arg("-c")
.arg("version")
.status_without_output()
.map_or(false, |status| status.success())
{
return "nu".try_into();
}
"cmd".try_into()
}
fn wrap_in_quotes<T: AsRef<OsStr>>(path: T) -> OsString {
let mut result = OsString::from("\"");
result.push(path);
result.push("\"");
result
}
fn wrap_in_quotes_string<T: AsRef<OsStr>>(path: T) -> String {
let path = path.as_ref().to_string_lossy();
format!("\"{}\"", path)
}
pub fn that_detached(path: impl AsRef<OsStr>) -> Result<()> {
#[cfg(not(feature = "shellexecute"))]
{
let mut last_err = None;
for mut cmd in commands(path) {
match cmd.spawn_detached() {
Ok(_) => {
return Ok(());
}
Err(err) => last_err = Some(err),
}
}
Err(last_err.map_or_else(
|| Error::new(ErrorKind::NO_LAUNCHER, ""),
|err| Error::new(ErrorKind::IO, err.to_string().as_str()),
))
}
#[cfg(feature = "shellexecute")]
{
that_detached_execute(path)
}
}
pub fn with_detached<T: AsRef<OsStr>>(path: T, app: impl Into<String>) -> Result<()> {
#[cfg(not(feature = "shellexecute"))]
{
let mut last_err = None;
let mut cmd = with_command(path, app);
match cmd.spawn_detached() {
Ok(_) => {
return Ok(()); }
Err(err) => {
last_err = Some(err); }
}
Err(last_err.map_or_else(
|| Error::new(ErrorKind::NO_LAUNCHER, ""),
|err| Error::new(ErrorKind::IO, err.to_string().as_str()),
))
}
#[cfg(feature = "shellexecute")]
{
with_detached_execute(path, app)
}
}
trait IntoResult<T> {
fn into_result(self, cmd: Command) -> T;
}
impl IntoResult<Result<()>> for std::io::Result<std::process::ExitStatus> {
fn into_result(self, cmd: Command) -> Result<()> {
match self {
Ok(status) if status.success() => Ok(()),
Ok(status) => Err(Error::new(
ErrorKind::COMMAND_FAILED,
format!("{cmd:?} ({})", status).as_str(),
)),
Err(err) => Err(err.into()),
}
}
}
trait CommandExt {
fn status_without_output(&mut self) -> std::io::Result<std::process::ExitStatus>;
fn spawn_detached(&mut self) -> std::io::Result<()>;
}
impl CommandExt for Command {
fn status_without_output(&mut self) -> std::io::Result<std::process::ExitStatus> {
self.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
}
fn spawn_detached(&mut self) -> std::io::Result<()> {
self.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
use std::os::windows::process::CommandExt;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
const CREATE_NO_WINDOW: u32 = 0x08000000;
self.creation_flags(CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW);
self.spawn().map(|_| ())
}
}
#[cfg(feature = "shellexecute")]
fn that_detached_execute<T: AsRef<OsStr>>(path: T) -> Result<()> {
let path = path.as_ref();
let is_dir = std::fs::metadata(path).map(|f| f.is_dir()).unwrap_or(false);
let path = wide(path);
if is_dir {
unsafe { ffi::CoInitialize(std::ptr::null()) };
let folder = unsafe { ffi::ILCreateFromPathW(path.as_ptr()) };
unsafe { SHOpenFolderAndSelectItems(folder, Some(&[folder]), 0)? };
return Ok(());
};
let mut info = ffi::SHELLEXECUTEINFOW {
cbSize: std::mem::size_of::<ffi::SHELLEXECUTEINFOW>() as _,
nShow: ffi::SW_SHOWNORMAL,
lpVerb: std::ptr::null(),
lpClass: std::ptr::null(),
lpFile: path.as_ptr(),
..unsafe { std::mem::zeroed() }
};
unsafe { ShellExecuteExW(&mut info) }
}
#[cfg(feature = "shellexecute")]
pub fn with_detached_execute<T: AsRef<OsStr>>(path: T, app: impl Into<String>) -> Result<()> {
let app = wide(app.into());
let path = wide(path);
let mut info = ffi::SHELLEXECUTEINFOW {
cbSize: std::mem::size_of::<ffi::SHELLEXECUTEINFOW>() as _,
nShow: ffi::SW_SHOWNORMAL,
lpFile: app.as_ptr(),
lpParameters: path.as_ptr(),
..unsafe { std::mem::zeroed() }
};
unsafe { ShellExecuteExW(&mut info) }
}
#[cfg(feature = "shellexecute")]
#[inline]
fn wide<T: AsRef<OsStr>>(input: T) -> Vec<u16> {
use std::os::windows::ffi::OsStrExt;
input
.as_ref()
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
#[allow(non_snake_case)]
#[cfg(feature = "shellexecute")]
unsafe fn ShellExecuteExW(info: *mut ffi::SHELLEXECUTEINFOW) -> Result<()> {
if ffi::ShellExecuteExW(info) == 1 {
Ok(())
} else {
Err(Error::new(
ErrorKind::IO,
std::io::Error::last_os_error().to_string().as_str(),
))
}
}
#[allow(non_snake_case)]
#[cfg(feature = "shellexecute")]
unsafe fn SHOpenFolderAndSelectItems(
pidlfolder: *const ffi::ITEMIDLIST,
apidl: Option<&[*const ffi::ITEMIDLIST]>,
dwflags: u32,
) -> Result<()> {
use std::convert::TryInto;
match ffi::SHOpenFolderAndSelectItems(
pidlfolder,
apidl.map_or(0, |slice| slice.len().try_into().unwrap()),
apidl.map_or(core::ptr::null(), |slice| slice.as_ptr()),
dwflags,
) {
0 => Ok(()),
error_code => Err(Error::new(
ErrorKind::IO,
std::io::Error::from_raw_os_error(error_code)
.to_string()
.as_str(),
)),
}
}
#[cfg(feature = "shellexecute")]
#[allow(non_snake_case)]
mod ffi {
pub const SW_SHOWNORMAL: i32 = 1;
#[cfg_attr(not(target_arch = "x86"), repr(C))]
#[cfg_attr(target_arch = "x86", repr(C, packed(1)))]
pub struct SHELLEXECUTEINFOW {
pub cbSize: u32,
pub fMask: u32,
pub hwnd: isize,
pub lpVerb: *const u16,
pub lpFile: *const u16,
pub lpParameters: *const u16,
pub lpDirectory: *const u16,
pub nShow: i32,
pub hInstApp: isize,
pub lpIDList: *mut core::ffi::c_void,
pub lpClass: *const u16,
pub hkeyClass: isize,
pub dwHotKey: u32,
pub Anonymous: SHELLEXECUTEINFOW_0,
pub hProcess: isize,
}
#[cfg_attr(not(target_arch = "x86"), repr(C))]
#[cfg_attr(target_arch = "x86", repr(C, packed(1)))]
pub union SHELLEXECUTEINFOW_0 {
pub hIcon: isize,
pub hMonitor: isize,
}
#[repr(C, packed(1))]
pub struct SHITEMID {
pub cb: u16,
pub abID: [u8; 1],
}
#[repr(C, packed(1))]
pub struct ITEMIDLIST {
pub mkid: SHITEMID,
}
#[link(name = "shell32")]
extern "system" {
pub fn ShellExecuteExW(info: *mut SHELLEXECUTEINFOW) -> isize;
pub fn ILCreateFromPathW(pszpath: *const u16) -> *mut ITEMIDLIST;
pub fn SHOpenFolderAndSelectItems(
pidlfolder: *const ITEMIDLIST,
cidl: u32,
apidl: *const *const ITEMIDLIST,
dwflags: u32,
) -> i32;
}
#[link(name = "ole32")]
extern "system" {
pub fn CoInitialize(pvreserved: *const core::ffi::c_void) -> i32;
}
}