1use std::io::Write;
7use std::process::Command;
8
9use anyhow::{anyhow, bail, Context, Result};
10
11pub fn compose(prefill: &str) -> Result<String> {
15 let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
16 let tmp = tempfile::Builder::new()
17 .prefix("runtime-edit-")
18 .suffix(".md")
19 .tempfile()
20 .context("create tempfile")?;
21 let path = tmp.path().to_path_buf();
22 {
23 let mut f = std::fs::File::create(&path)?;
24 f.write_all(prefill.as_bytes())?;
25 }
26 let mut parts = editor.split_whitespace();
28 let bin = parts.next().ok_or_else(|| anyhow!("empty $EDITOR"))?;
29 let status = Command::new(bin)
30 .args(parts)
31 .arg(&path)
32 .status()
33 .with_context(|| format!("launch editor `{editor}`"))?;
34 if !status.success() {
35 bail!("editor exited with status {status}");
36 }
37 let body = std::fs::read_to_string(&path).context("read edited file")?;
38 if body.trim().is_empty() {
39 bail!("aborted: empty body");
40 }
41 Ok(body)
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47 use std::sync::Mutex;
48
49 static ENV_LOCK: Mutex<()> = Mutex::new(());
50
51 fn with_editor(editor: Option<&str>, f: impl FnOnce()) {
52 let _guard = ENV_LOCK.lock().unwrap();
53 let saved = std::env::var("EDITOR").ok();
54 match editor {
55 Some(value) => std::env::set_var("EDITOR", value),
56 None => std::env::remove_var("EDITOR"),
57 }
58 f();
59 match saved {
60 Some(value) => std::env::set_var("EDITOR", value),
61 None => std::env::remove_var("EDITOR"),
62 }
63 }
64
65 #[test]
66 fn compose_returns_prefill_when_editor_succeeds_without_changes() {
67 with_editor(Some("true"), || {
68 assert_eq!(compose("Issue body\n").unwrap(), "Issue body\n");
69 });
70 }
71
72 #[test]
73 fn compose_rejects_empty_body_after_successful_editor() {
74 with_editor(Some("true"), || {
75 let err = compose(" \n").expect_err("empty body aborts");
76 assert!(err.to_string().contains("aborted: empty body"));
77 });
78 }
79
80 #[test]
81 fn compose_reports_failing_editor_status() {
82 with_editor(Some("false"), || {
83 let err = compose("Issue body\n").expect_err("editor failure");
84 assert!(err.to_string().contains("editor exited with status"));
85 });
86 }
87}