use std::io;
use std::path::Path;
pub trait Launcher {
fn launch(&self, content_path: &Path) -> io::Result<()>;
}
pub struct Host;
#[cfg(target_os = "linux")]
impl Launcher for Host {
fn launch(&self, content_path: &Path) -> io::Result<()> {
spawn_detached("xdg-open", content_path)
}
}
#[cfg(target_os = "macos")]
impl Launcher for Host {
fn launch(&self, content_path: &Path) -> io::Result<()> {
spawn_detached("open", content_path)
}
}
#[cfg(target_os = "windows")]
impl Launcher for Host {
fn launch(&self, content_path: &Path) -> io::Result<()> {
shell::hand_over(content_path)
}
}
#[cfg(target_os = "windows")]
pub fn hand_the_foreground_on() {
shell::hand_the_foreground_on();
}
#[cfg(target_os = "windows")]
mod shell {
use std::io;
use std::os::windows::ffi::OsStrExt as _;
use std::path::Path;
use windows::core::PCWSTR;
use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED};
use windows::Win32::UI::Shell::{ShellExecuteExW, SEE_MASK_NOASYNC, SHELLEXECUTEINFOW};
use windows::Win32::UI::WindowsAndMessaging::{
AllowSetForegroundWindow, ASFW_ANY, SW_SHOWNORMAL,
};
pub(super) const MASK: u32 = SEE_MASK_NOASYNC;
#[allow(unsafe_code)]
pub(super) fn hand_the_foreground_on() {
let _ = unsafe { AllowSetForegroundWindow(ASFW_ANY) };
}
pub(super) fn hand_over(content_path: &Path) -> io::Result<()> {
let path: Vec<u16> = content_path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
std::thread::Builder::new()
.name("slipcase-open launch".to_owned())
.spawn(move || {
let _ = execute(&path);
})
.map(drop)
}
#[allow(unsafe_code)]
fn execute(path: &[u16]) -> io::Result<()> {
let _apartment = Apartment::enter();
let _ = unsafe { AllowSetForegroundWindow(ASFW_ANY) };
let mut how = SHELLEXECUTEINFOW {
cbSize: u32::try_from(std::mem::size_of::<SHELLEXECUTEINFOW>()).unwrap_or(0),
fMask: MASK,
lpVerb: PCWSTR::null(),
lpFile: PCWSTR(path.as_ptr()),
nShow: SW_SHOWNORMAL.0,
..Default::default()
};
unsafe { ShellExecuteExW(&raw mut how) }.map_err(|_| io::Error::last_os_error())
}
struct Apartment(bool);
impl Apartment {
#[allow(unsafe_code)]
fn enter() -> Self {
let how = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
Self(how.is_ok())
}
}
impl Drop for Apartment {
#[allow(unsafe_code)]
fn drop(&mut self) {
if self.0 {
unsafe { CoUninitialize() };
}
}
}
#[cfg(test)]
mod tests {
use super::MASK;
use windows::Win32::UI::Shell::{SEE_MASK_FLAG_NO_UI, SEE_MASK_NOZONECHECKS};
#[test]
fn the_zone_check_is_never_opted_out_of() {
assert_eq!(
MASK & SEE_MASK_NOZONECHECKS,
0,
"the zone check has been opted out of"
);
assert_eq!(
MASK & SEE_MASK_FLAG_NO_UI,
0,
"the warning the zone check raises has been suppressed"
);
}
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
impl Launcher for Host {
fn launch(&self, _content_path: &Path) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"no launcher for this platform",
))
}
}
#[cfg(unix)]
fn spawn_detached(program: &str, content_path: &Path) -> io::Result<()> {
use std::process::{Command, Stdio};
let mut child = Command::new(program)
.arg(content_path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let _ = child.try_wait();
Ok(())
}
#[cfg(test)]
pub mod testing {
use super::Launcher;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
#[derive(Default)]
pub struct Recording {
launched: Mutex<Vec<PathBuf>>,
refuse: bool,
}
impl Recording {
#[must_use]
pub fn refusing() -> Self {
Self {
refuse: true,
..Self::default()
}
}
#[must_use]
pub fn launched(&self) -> Vec<PathBuf> {
self.launched.lock().unwrap().clone()
}
}
impl Launcher for Recording {
fn launch(&self, content_path: &Path) -> io::Result<()> {
if self.refuse {
return Err(io::Error::new(io::ErrorKind::NotFound, "no handler"));
}
self.launched.lock().unwrap().push(content_path.to_owned());
Ok(())
}
}
}