use std::{
env, fs,
path::{Path, PathBuf},
process::{self, Command},
};
use anyhow::{anyhow, bail, Context as ResultExt, Result};
use crate::{edit, util::TempPath};
const ENV_VARS: &[&str] = &["VISUAL", "EDITOR"];
#[cfg(not(target_os = "windows"))]
const EDITORS: &[&str] = &["code --wait", "nano", "vim", "vi", "emacs"];
#[cfg(target_os = "windows")]
const EDITORS: &[&str] = &["code.exe --wait", "notepad.exe"];
pub struct Editor {
bin: PathBuf,
args: Vec<String>,
}
pub struct Child {
child: process::Child,
file: TempPath,
}
fn to_bin_and_args<S>(cmd: S) -> Option<(PathBuf, Vec<String>)>
where
S: AsRef<str>,
{
let mut split = cmd.as_ref().split_whitespace();
let bin: PathBuf = split.next()?.into();
let args: Vec<String> = split.map(Into::into).collect();
Some((bin, args))
}
impl Editor {
pub fn default() -> Result<Self> {
let (bin, args) = ENV_VARS
.iter()
.filter_map(|e| env::var(e).ok())
.filter_map(to_bin_and_args)
.chain(EDITORS.iter().filter_map(to_bin_and_args))
.find(|(bin, _)| which::which(bin).is_ok())
.ok_or_else(|| anyhow!("failed to determine default editor"))?;
Ok(Self { bin, args })
}
pub fn edit(self, path: &Path, contents: &str) -> Result<Child> {
let file = TempPath::new(path);
let Self { bin, args } = self;
fs::write(file.path(), &contents).context("failed to write to temporary file")?;
let child = Command::new(bin)
.args(args)
.arg(file.path())
.spawn()
.context("failed to spawn editor subprocess")?;
Ok(Child { child, file })
}
}
impl Child {
pub fn wait_and_update(self, original_contents: &str) -> Result<edit::Config> {
let Self { mut child, file } = self;
let exit_status = child.wait()?;
if exit_status.success() {
let contents =
fs::read_to_string(file.path()).context("failed to read from temporary file")?;
if contents == original_contents {
bail!("aborted editing!");
} else {
edit::Config::from_str(&contents)
.context("edited config is invalid, not updating config file")
}
} else {
bail!("editor terminated with {}", exit_status)
}
}
}