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 {
use super::*;
fn env(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
let pairs: Vec<(String, String)> = pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
move |name| {
pairs
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.clone())
}
}
#[test]
fn text_goes_to_the_editor_the_environment_names() {
assert_eq!(
program_for(false, env(&[("EDITOR", "code --wait"), ("PAGER", "less")])),
Program::Wait(vec!["code".into(), "--wait".into()])
);
assert_eq!(
program_for(false, env(&[("VISUAL", "vim"), ("EDITOR", "nano")])),
Program::Wait(vec!["vim".into()])
);
assert_eq!(
program_for(false, env(&[("EDITOR", " "), ("PAGER", "most")])),
Program::Wait(vec!["most".into()]),
"blank is unset"
);
let none = program_for(false, env(&[]));
if cfg!(windows) {
assert_eq!(none, Program::Opener(opener()));
} else {
assert_eq!(none, Program::Wait(vec!["less".into()]));
}
assert_eq!(
program_for(true, env(&[("EDITOR", "vim")])),
Program::Opener(opener())
);
}
#[test]
fn names_are_safe_and_files_read_only() {
assert_eq!(
file_name("payload json", 3, "json"),
"payload_json-row3.json"
);
assert_eq!(file_name("../x", 1, "txt"), "_x-row1.txt");
assert_eq!(file_name("", 1, "bin"), "value-row1.bin");
let dir = tempfile::tempdir().unwrap();
let path = write_value(dir.path(), "a.txt", b"one").unwrap();
if cfg!(unix) {
assert!(std::fs::metadata(&path).unwrap().permissions().readonly());
}
let again = write_value(dir.path(), "a.txt", b"two").unwrap();
assert_ne!(again, path);
assert_eq!(std::fs::read(again).unwrap(), b"two");
}
}