git_branchless_reword/dialoguer_edit.rs
1//! Fork of `dialoguer::edit`.
2//!
3//! Originally from <https://github.com/mitsuhiko/dialoguer/blob/40c7c90f04c8bcab4e26133fdf6ece30fd001bd0/src/edit.rs>
4//!
5//! There are bugs we want to fix and behaviors we want to customize, and their
6//! release schedule may not align with ours. This chunk of code is fairly
7//! small, so we can vendor it here.
8//!
9//! `dialoguer` is originally released under the MIT license:
10//!
11//! The MIT License (MIT)
12//! Copyright (c) 2017 Armin Ronacher <armin.ronacher@active-4.com>
13//!
14//! Permission is hereby granted, free of charge, to any person obtaining a copy
15//! of this software and associated documentation files (the "Software"), to deal
16//! in the Software without restriction, including without limitation the rights
17//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18//! copies of the Software, and to permit persons to whom the Software is
19//! furnished to do so, subject to the following conditions:
20//!
21//! The above copyright notice and this permission notice shall be included in all
22//! copies or substantial portions of the Software.
23//!
24//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30//! SOFTWARE.
31
32use std::env;
33use std::ffi::{OsStr, OsString};
34use std::fs;
35use std::io::{self, Read, Write};
36use std::process;
37
38/// Launches the default editor to edit a string.
39///
40/// ## Example
41///
42/// ```rust,no_run
43/// use git_branchless_reword::dialoguer_edit::Editor;
44///
45/// if let Some(rv) = Editor::new().edit("Enter a commit message").unwrap() {
46/// println!("Your message:");
47/// println!("{}", rv);
48/// } else {
49/// println!("Abort!");
50/// }
51/// ```
52pub struct Editor {
53 editor: OsString,
54 extension: String,
55 require_save: bool,
56 trim_newlines: bool,
57}
58
59fn get_default_editor() -> OsString {
60 if let Some(prog) = env::var_os("VISUAL") {
61 return prog;
62 }
63 if let Some(prog) = env::var_os("EDITOR") {
64 return prog;
65 }
66 if cfg!(windows) {
67 "notepad.exe".into()
68 } else {
69 "vi".into()
70 }
71}
72
73impl Default for Editor {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl Editor {
80 /// Creates a new editor.
81 pub fn new() -> Self {
82 Self {
83 editor: get_default_editor(),
84 extension: ".txt".into(),
85 require_save: true,
86 trim_newlines: true,
87 }
88 }
89
90 /// Sets a specific editor executable.
91 pub fn executable<S: AsRef<OsStr>>(&mut self, val: S) -> &mut Self {
92 self.editor = val.as_ref().into();
93 self
94 }
95
96 /// Sets a specific extension
97 pub fn extension(&mut self, val: &str) -> &mut Self {
98 self.extension = val.into();
99 self
100 }
101
102 /// Enables or disables the save requirement.
103 pub fn require_save(&mut self, val: bool) -> &mut Self {
104 self.require_save = val;
105 self
106 }
107
108 /// Enables or disables trailing newline stripping.
109 ///
110 /// This is on by default.
111 pub fn trim_newlines(&mut self, val: bool) -> &mut Self {
112 self.trim_newlines = val;
113 self
114 }
115
116 /// Launches the editor to edit a string.
117 ///
118 /// Returns `None` if the file was not saved or otherwise the
119 /// entered text.
120 pub fn edit(&self, s: &str) -> io::Result<Option<String>> {
121 let mut f = tempfile::Builder::new()
122 .prefix("COMMIT_EDITMSG-")
123 .suffix(&self.extension)
124 .rand_bytes(12)
125 .tempfile()?;
126 f.write_all(s.as_bytes())?;
127 f.flush()?;
128 let ts = fs::metadata(f.path())?.modified()?;
129
130 let s: String = self.editor.clone().into_string().unwrap();
131 let (cmd, args) = match shell_words::split(&s) {
132 Ok(mut parts) => {
133 let cmd = parts.remove(0);
134 (cmd, parts)
135 }
136 Err(_) => (s, vec![]),
137 };
138
139 let rv = process::Command::new(cmd)
140 .args(args)
141 .arg(f.path())
142 .spawn()?
143 .wait()?;
144
145 if rv.success() && self.require_save && ts >= fs::metadata(f.path())?.modified()? {
146 return Ok(None);
147 }
148
149 let mut new_f = fs::File::open(f.path())?;
150 let mut rv = String::new();
151 new_f.read_to_string(&mut rv)?;
152
153 if self.trim_newlines {
154 let len = rv.trim_end_matches(&['\n', '\r'][..]).len();
155 rv.truncate(len);
156 }
157
158 Ok(Some(rv))
159 }
160}