use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExternalOpen {
pub path: PathBuf,
pub document: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Program {
Wait(Vec<String>),
Opener(Vec<String>),
}
fn opener() -> Vec<String> {
if cfg!(windows) {
["cmd", "/c", "start", ""].map(String::from).to_vec()
} else if cfg!(target_os = "macos") {
vec!["open".to_string()]
} else {
vec!["xdg-open".to_string()]
}
}
pub fn program_for(document: bool, env: impl Fn(&str) -> Option<String>) -> Program {
if document {
return Program::Opener(opener());
}
for name in ["VISUAL", "EDITOR", "PAGER"] {
if let Some(value) = env(name).filter(|v| !v.trim().is_empty()) {
return Program::Wait(value.split_whitespace().map(String::from).collect());
}
}
if cfg!(windows) {
Program::Opener(opener())
} else {
Program::Wait(vec!["less".to_string()])
}
}
pub fn file_name(field: &str, row: usize, extension: &str) -> String {
let safe: String = field
.chars()
.map(|c| {
if c.is_alphanumeric() || matches!(c, '-' | '_' | '.') {
c
} else {
'_'
}
})
.take(60)
.collect();
let safe = safe.trim_matches('.');
let safe = if safe.is_empty() { "value" } else { safe };
format!("{safe}-row{row}.{extension}")
}
pub fn write_value(dir: &Path, name: &str, bytes: &[u8]) -> std::io::Result<PathBuf> {
let mut path = dir.join(name);
let mut n = 1;
while path.exists() {
n += 1;
path = dir.join(format!("{n}-{name}"));
}
std::fs::write(&path, bytes)?;
if cfg!(unix) {
let mut perms = std::fs::metadata(&path)?.permissions();
perms.set_readonly(true);
std::fs::set_permissions(&path, perms)?;
}
Ok(path)
}
pub fn run(program: &Program, path: &Path) -> std::io::Result<()> {
let (argv, wait) = match program {
Program::Wait(argv) => (argv, true),
Program::Opener(argv) => (argv, false),
};
let Some((first, rest)) = argv.split_first() else {
return Err(std::io::Error::other("no program to open the value with"));
};
if !wait {
let mut argv: Vec<&std::ffi::OsStr> = argv.iter().map(|a| a.as_ref()).collect();
argv.push(path.as_os_str());
return start(&argv);
}
let status = std::process::Command::new(first)
.args(rest)
.arg(path)
.status()?;
if !status.success() {
return Err(std::io::Error::other(format!(
"{first} exited with {status}"
)));
}
Ok(())
}
pub fn start<S: AsRef<std::ffi::OsStr>>(argv: &[S]) -> std::io::Result<()> {
let Some((first, rest)) = argv.split_first() else {
return Err(std::io::Error::other("no program to open with"));
};
let mut child = std::process::Command::new(first)
.args(rest)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?;
std::thread::spawn(move || child.wait());
Ok(())
}
#[cfg(test)]
mod tests;