use std::io::Write;
use std::process::Command;
use anyhow::{anyhow, bail, Context, Result};
pub fn compose(prefill: &str) -> Result<String> {
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
let tmp = tempfile::Builder::new()
.prefix("runtime-edit-")
.suffix(".md")
.tempfile()
.context("create tempfile")?;
let path = tmp.path().to_path_buf();
{
let mut f = std::fs::File::create(&path)?;
f.write_all(prefill.as_bytes())?;
}
let mut parts = editor.split_whitespace();
let bin = parts.next().ok_or_else(|| anyhow!("empty $EDITOR"))?;
let status = Command::new(bin)
.args(parts)
.arg(&path)
.status()
.with_context(|| format!("launch editor `{editor}`"))?;
if !status.success() {
bail!("editor exited with status {status}");
}
let body = std::fs::read_to_string(&path).context("read edited file")?;
if body.trim().is_empty() {
bail!("aborted: empty body");
}
Ok(body)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_editor(editor: Option<&str>, f: impl FnOnce()) {
let _guard = ENV_LOCK.lock().unwrap();
let saved = std::env::var("EDITOR").ok();
match editor {
Some(value) => std::env::set_var("EDITOR", value),
None => std::env::remove_var("EDITOR"),
}
f();
match saved {
Some(value) => std::env::set_var("EDITOR", value),
None => std::env::remove_var("EDITOR"),
}
}
#[test]
fn compose_returns_prefill_when_editor_succeeds_without_changes() {
with_editor(Some("true"), || {
assert_eq!(compose("Issue body\n").unwrap(), "Issue body\n");
});
}
#[test]
fn compose_rejects_empty_body_after_successful_editor() {
with_editor(Some("true"), || {
let err = compose(" \n").expect_err("empty body aborts");
assert!(err.to_string().contains("aborted: empty body"));
});
}
#[test]
fn compose_reports_failing_editor_status() {
with_editor(Some("false"), || {
let err = compose("Issue body\n").expect_err("editor failure");
assert!(err.to_string().contains("editor exited with status"));
});
}
}