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}