use crate::error::AppError;
use std::env;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tempfile::NamedTempFile;
pub fn edit_message(initial_message: &str, verbose_flag: bool) -> Result<String, AppError> {
let mut temp = NamedTempFile::new()?;
temp.write_all(initial_message.as_bytes())?;
temp.write_all(b"\n")?;
temp.flush()?;
open_editor(temp.path(), verbose_flag)?;
let edited = fs::read_to_string(temp.path())?;
let cleaned = clean_editor_message(&edited);
if cleaned.is_empty() {
return Err(AppError::Message(
"Commit message is empty after edit. Aborting.".to_string(),
));
}
Ok(cleaned)
}
pub fn detect_editor_for_status() -> String {
match resolve_editor() {
Ok((program, args)) => {
if args.is_empty() {
program
} else {
format!("{} {}", program, args.join(" "))
}
}
Err(err) => format!("unavailable ({err})"),
}
}
fn open_editor(path: &Path, verbose_flag: bool) -> Result<(), AppError> {
let (program, args) = resolve_editor()?;
if verbose_flag {
eprintln!(
"[verbose] opening editor '{program}' for {}",
path.display()
);
}
let status = Command::new(&program)
.args(args)
.arg(path)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map_err(|err| AppError::Message(format!("Failed to open editor '{program}': {err}")))?;
if status.success() {
Ok(())
} else {
Err(AppError::Message(format!(
"Editor '{program}' exited with non-zero status: {status}"
)))
}
}
fn resolve_editor() -> Result<(String, Vec<String>), AppError> {
if let Some(editor_raw) = env::var_os("EDITOR") {
let editor = editor_raw.to_string_lossy().trim().to_string();
if !editor.is_empty() {
let parsed = shlex::split(&editor).ok_or_else(|| {
AppError::Message(
"Failed to parse $EDITOR value. Set a simple command.".to_string(),
)
})?;
if !parsed.is_empty() {
return Ok((parsed[0].clone(), parsed[1..].to_vec()));
}
}
}
if command_exists("nano") {
return Ok(("nano".to_string(), Vec::new()));
}
if command_exists("vim") {
return Ok(("vim".to_string(), Vec::new()));
}
Err(AppError::Message(
"No editor found. Set $EDITOR, or install nano/vim.".to_string(),
))
}
fn command_exists(program: &str) -> bool {
if program.contains('/') {
return Path::new(program).exists();
}
let Some(paths) = env::var_os("PATH") else {
return false;
};
env::split_paths(&paths)
.map(|dir| dir.join(program))
.any(|candidate| is_executable_file(&candidate))
}
fn is_executable_file(path: &PathBuf) -> bool {
if !path.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
match fs::metadata(path) {
Ok(meta) => meta.permissions().mode() & 0o111 != 0,
Err(_) => false,
}
}
#[cfg(not(unix))]
{
true
}
}
fn clean_editor_message(input: &str) -> String {
let mut lines: Vec<&str> = input.lines().collect();
while lines.first().is_some_and(|line| line.trim().is_empty()) {
lines.remove(0);
}
while lines.last().is_some_and(|line| line.trim().is_empty()) {
lines.pop();
}
lines.join("\n").trim().to_string()
}
#[cfg(test)]
mod tests {
use super::clean_editor_message;
#[test]
fn strips_extra_blank_lines() {
let out = clean_editor_message("\n\nfeat(core): add test\n\n- body\n\n");
assert_eq!(out, "feat(core): add test\n\n- body");
}
}