1use std::{
2 env,
3 ffi::{OsStr, OsString},
4 fs,
5 io::{Read, Write},
6 process,
7};
8
9use crate::Result;
10
11pub struct Editor {
26 editor: OsString,
27 extension: String,
28 require_save: bool,
29 trim_newlines: bool,
30}
31
32fn get_default_editor() -> OsString {
33 if let Some(prog) = env::var_os("VISUAL") {
34 return prog;
35 }
36 if let Some(prog) = env::var_os("EDITOR") {
37 return prog;
38 }
39 if cfg!(windows) {
40 "notepad.exe".into()
41 } else {
42 "vi".into()
43 }
44}
45
46impl Default for Editor {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52impl Editor {
53 pub fn new() -> Self {
55 Self {
56 editor: get_default_editor(),
57 extension: ".txt".into(),
58 require_save: true,
59 trim_newlines: true,
60 }
61 }
62
63 pub fn executable<S: AsRef<OsStr>>(&mut self, val: S) -> &mut Self {
65 self.editor = val.as_ref().into();
66 self
67 }
68
69 pub fn extension(&mut self, val: &str) -> &mut Self {
71 self.extension = val.into();
72 self
73 }
74
75 pub fn require_save(&mut self, val: bool) -> &mut Self {
77 self.require_save = val;
78 self
79 }
80
81 pub fn trim_newlines(&mut self, val: bool) -> &mut Self {
85 self.trim_newlines = val;
86 self
87 }
88
89 pub fn edit(&self, s: &str) -> Result<Option<String>> {
94 let mut f = tempfile::Builder::new()
95 .prefix("edit-")
96 .suffix(&self.extension)
97 .rand_bytes(12)
98 .tempfile()?;
99 f.write_all(s.as_bytes())?;
100 f.flush()?;
101 let ts = fs::metadata(f.path())?.modified()?;
102
103 let s: String = self.editor.clone().into_string().unwrap();
104 let (cmd, args) = match shell_words::split(&s) {
105 Ok(mut parts) => {
106 let cmd = parts.remove(0);
107 (cmd, parts)
108 }
109 Err(_) => (s, vec![]),
110 };
111
112 let rv = process::Command::new(cmd)
113 .args(args)
114 .arg(f.path())
115 .spawn()?
116 .wait()?;
117
118 if rv.success() && self.require_save && ts >= fs::metadata(f.path())?.modified()? {
119 return Ok(None);
120 }
121
122 let mut new_f = fs::File::open(f.path())?;
123 let mut rv = String::new();
124 new_f.read_to_string(&mut rv)?;
125
126 if self.trim_newlines {
127 let len = rv.trim_end_matches(&['\n', '\r'][..]).len();
128 rv.truncate(len);
129 }
130
131 Ok(Some(rv))
132 }
133}