use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
use std::str;
use tempfile::NamedTempFile;
use tracing::warn;
use crate::errors::EditError;
#[cfg(windows)]
fn git_sh_path() -> Option<PathBuf> {
let output = Command::new("where").arg("git").output().ok()?;
if !output.status.success() {
return None;
}
Path::new(str::from_utf8(&output.stdout).ok()?.trim())
.canonicalize()
.ok()?
.parent()?
.parent()?
.join(r"bin\sh.exe")
.canonicalize()
.ok()
}
#[cfg(not(windows))]
fn git_sh_path() -> Option<PathBuf> {
Some("/bin/sh".into())
}
fn git_editor() -> Option<String> {
if std::env::var("CARGO_VET_USE_FALLBACK_EDITOR").unwrap_or_default() == "1" {
return None;
}
let output = Command::new("git")
.arg("var")
.arg("GIT_EDITOR")
.output()
.ok()?;
if !output.status.success() {
return None;
}
Some(str::from_utf8(&output.stdout).ok()?.trim().to_owned())
}
#[cfg(windows)]
const FALLBACK_EDITOR: &str = "notepad.exe";
#[cfg(not(windows))]
const FALLBACK_EDITOR: &str = "nano";
pub fn editor_command() -> Command {
match (git_sh_path(), git_editor()) {
(Some(git_sh), Some(git_editor)) => {
let mut cmd = Command::new(git_sh);
cmd.arg("-c")
.arg(format!("{} \"$@\"", git_editor))
.arg(git_editor);
return cmd;
}
(_, None) => {
warn!("Unable to determine user's GIT_EDITOR");
}
(None, Some(_)) => {
warn!("Unable to locate user's git install to invoke GIT_EDITOR");
}
}
warn!("Falling back to running '{}' directly", FALLBACK_EDITOR);
Command::new(FALLBACK_EDITOR)
}
pub fn run_editor(path: &Path) -> io::Result<ExitStatus> {
editor_command().arg(path).status()
}
#[cfg(windows)]
const LINE_ENDING: &str = "\r\n";
#[cfg(not(windows))]
const LINE_ENDING: &str = "\n";
pub struct Editor<'a> {
tempfile: NamedTempFile,
comment_char: char,
#[allow(clippy::type_complexity)]
run_editor: Box<dyn FnOnce(&Path) -> io::Result<bool> + 'a>,
}
impl<'a> Editor<'a> {
pub fn new(name: &str) -> io::Result<Self> {
let tempfile = tempfile::Builder::new()
.prefix(&format!("{}.", name))
.tempfile()?;
Ok(Editor {
tempfile,
comment_char: '#',
run_editor: Box::new(|p| run_editor(p).map(|s| s.success())),
})
}
pub fn set_comment_char(&mut self, comment_char: char) {
self.comment_char = comment_char;
}
pub fn select_comment_char(&mut self, text: &str) {
let mut comment_chars = ['#', ';', '@', '!', '$', '%', '^', '&', '|', ':', '"', ';'];
for line in text.lines() {
for cc in &mut comment_chars {
if line.starts_with(*cc) {
*cc = '\0';
break;
}
}
}
self.set_comment_char(
comment_chars
.into_iter()
.find(|cc| *cc != '\0')
.expect("couldn't find a viable comment character"),
);
}
#[cfg(test)]
pub fn set_run_editor(&mut self, run_editor: impl FnOnce(&Path) -> io::Result<bool> + 'a) {
self.run_editor = Box::new(run_editor);
}
pub fn add_comments(&mut self, text: &str) -> io::Result<()> {
let text = text.trim();
if text.is_empty() {
write!(self.tempfile, "{}{}", self.comment_char, LINE_ENDING)?;
}
for line in text.lines() {
if line.is_empty() {
write!(self.tempfile, "{}{}", self.comment_char, LINE_ENDING)?;
} else {
write!(
self.tempfile,
"{} {}{}",
self.comment_char, line, LINE_ENDING
)?;
}
}
Ok(())
}
pub fn add_text(&mut self, text: &str) -> io::Result<()> {
let text = text.trim();
if text.is_empty() {
write!(self.tempfile, "{}", LINE_ENDING)?;
}
for line in text.lines() {
assert!(
!line.starts_with(self.comment_char),
"non-comment lines cannot start with a '{}' comment character",
self.comment_char
);
write!(self.tempfile, "{}{}", line, LINE_ENDING)?;
}
Ok(())
}
pub fn edit(self) -> Result<String, EditError> {
let path = self.tempfile.into_temp_path();
(self.run_editor)(&path).map_err(EditError::CouldntLaunch)?;
let mut lines: Vec<String> = Vec::new();
for line in BufReader::new(File::open(&path).map_err(EditError::CouldntOpen)?).lines() {
let line = line.map_err(EditError::CouldntRead)?;
if line.starts_with(self.comment_char) {
continue;
}
let line = line.trim_end();
if line.is_empty() && lines.last().map_or(true, |l| l.is_empty()) {
continue;
}
lines.push(line.to_owned());
}
match lines.last() {
None => return Ok(String::new()),
Some(line) if !line.is_empty() => lines.push(String::new()),
_ => {}
}
Ok(lines.join("\n"))
}
}