use anyhow::{Context, Result};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
const BUNDLE_ID: &str = "com.cesarferreira.stax";
const DEFAULT_OPEN: &str = "/usr/bin/open";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Platform {
MacOs,
Unsupported(&'static str),
}
impl Platform {
fn current() -> Self {
if cfg!(target_os = "macos") {
Self::MacOs
} else {
Self::Unsupported(std::env::consts::OS)
}
}
}
trait CommandRunner {
fn run(&self, program: &Path, args: &[OsString]) -> Result<()>;
}
struct RealCommandRunner;
impl CommandRunner for RealCommandRunner {
fn run(&self, program: &Path, args: &[OsString]) -> Result<()> {
let status = Command::new(program)
.args(args)
.status()
.with_context(|| format!("failed to spawn {}", program.display()))?;
if status.success() {
Ok(())
} else {
anyhow::bail!("{} exited with {}", program.display(), status);
}
}
}
pub fn run(path: Option<PathBuf>) -> Result<()> {
run_with_runner(path, Platform::current(), &RealCommandRunner)
}
fn run_with_runner(
path: Option<PathBuf>,
platform: Platform,
runner: &dyn CommandRunner,
) -> Result<()> {
let repository = canonical_repository_path(path)?;
if let Platform::Unsupported(os) = platform {
anyhow::bail!("st gui is only supported on macOS; current platform is {os}");
}
let program = open_executable()?;
let args = vec![
OsString::from("-n"),
OsString::from("-b"),
OsString::from(BUNDLE_ID),
OsString::from("--args"),
repository.into_os_string(),
];
runner.run(&program, &args).with_context(|| {
format!(
"failed to launch Stax.app with {}; install it from GitHub Releases or run `make install-gui-app` for a local contributor build at $HOME/Applications/Stax.app",
program.display()
)
})
}
fn canonical_repository_path(path: Option<PathBuf>) -> Result<PathBuf> {
let path = match path {
Some(path) => path,
None => std::env::current_dir().context("failed to resolve current directory")?,
};
path.canonicalize().with_context(|| {
format!(
"failed to canonicalize GUI repository path {}",
path.display()
)
})
}
fn open_executable() -> Result<PathBuf> {
match std::env::var_os("STAX_GUI_OPEN_EXECUTABLE") {
Some(value) => {
let path = PathBuf::from(value);
if !path.is_absolute() {
anyhow::bail!(
"STAX_GUI_OPEN_EXECUTABLE must be an absolute path; install Stax.app from GitHub Releases or run `make install-gui-app` for a local contributor build"
);
}
Ok(path)
}
None => Ok(PathBuf::from(DEFAULT_OPEN)),
}
}
#[cfg(test)]
struct RecordingCommandRunner {
result: Result<(), String>,
program: std::cell::RefCell<Option<PathBuf>>,
args: std::cell::RefCell<Vec<OsString>>,
}
#[cfg(test)]
impl RecordingCommandRunner {
fn succeeding() -> Self {
Self {
result: Ok(()),
program: std::cell::RefCell::new(None),
args: std::cell::RefCell::new(Vec::new()),
}
}
fn failing() -> Self {
Self {
result: Err("missing application".into()),
program: std::cell::RefCell::new(None),
args: std::cell::RefCell::new(Vec::new()),
}
}
fn program(&self) -> PathBuf {
self.program
.borrow()
.clone()
.expect("recorded launcher program")
}
fn args(&self) -> Vec<OsString> {
self.args.borrow().clone()
}
}
#[cfg(test)]
impl CommandRunner for RecordingCommandRunner {
fn run(&self, program: &Path, args: &[OsString]) -> Result<()> {
*self.program.borrow_mut() = Some(program.to_path_buf());
*self.args.borrow_mut() = args.to_vec();
match &self.result {
Ok(()) => Ok(()),
Err(message) => anyhow::bail!("{message}"),
}
}
}
#[cfg(test)]
mod tests {
use super::{Platform, RecordingCommandRunner, run_with_runner};
use std::ffi::OsString;
use std::path::PathBuf;
#[test]
fn launcher_opens_a_fresh_instance_with_one_canonical_path_argument() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("repo with spaces");
std::fs::create_dir(&path).unwrap();
let runner = RecordingCommandRunner::succeeding();
run_with_runner(Some(path.clone()), Platform::MacOs, &runner).unwrap();
assert_eq!(runner.program(), PathBuf::from("/usr/bin/open"));
assert_eq!(
runner.args(),
vec![
OsString::from("-n"),
OsString::from("-b"),
OsString::from("com.cesarferreira.stax"),
OsString::from("--args"),
path.canonicalize().unwrap().into_os_string(),
]
);
}
#[test]
fn launcher_failure_points_to_the_repository_install_target() {
let temp = tempfile::tempdir().unwrap();
let error = run_with_runner(
Some(temp.path().to_path_buf()),
Platform::MacOs,
&RecordingCommandRunner::failing(),
)
.unwrap_err();
assert!(error.to_string().contains("make install-gui-app"));
assert!(error.to_string().contains("GitHub Releases"));
}
#[test]
fn unsupported_platform_never_runs_a_command() {
let temp = tempfile::tempdir().unwrap();
let runner = RecordingCommandRunner::succeeding();
let error = run_with_runner(
Some(temp.path().to_path_buf()),
Platform::Unsupported("linux"),
&runner,
)
.unwrap_err();
assert!(error.to_string().contains("only supported on macOS"));
assert!(runner.args().is_empty());
}
}