open_editor/
editor_call_builder.rs

1use std::{
2    env::{self, temp_dir},
3    path::{Path, PathBuf},
4    process::{Command, Stdio},
5};
6
7use crate::{ENV_VARS, editor::Editor, editor_kind::EditorKind, errors::OpenEditorError};
8
9pub struct EditorCallBuilder {
10    editor: Option<Editor>,
11    file_path: Option<PathBuf>,
12    custom_env_vars: Vec<String>,
13    wait: bool,
14    line_number: usize,
15    column_number: usize,
16}
17impl Default for EditorCallBuilder {
18    fn default() -> Self {
19        Self {
20            editor: None,
21            file_path: None,
22            custom_env_vars: vec![],
23            wait: true,
24            line_number: 1,
25            column_number: 1,
26        }
27    }
28}
29
30impl EditorCallBuilder {
31    /// Creates a new [`EditorCallBuilder`].
32    /// You can optionally set the line and column numbers later using the `at_line` and `at_column` methods.
33    ///
34    /// Finally, you can call the editor with the `open_editor`, `edit_string`, or `edit_string_mut` methods.
35    ///
36    /// # Errors
37    /// This function will return an error if the default editor cannot be found in the environment variables.
38    #[must_use]
39    pub fn new() -> Self {
40        Self::default()
41    }
42    #[must_use]
43    /// Sets the line number for the editor to open at.
44    pub fn at_line(self, line: usize) -> Self {
45        Self {
46            line_number: line,
47            ..self
48        }
49    }
50    #[must_use]
51    /// Sets the column number for the editor to open at.
52    pub fn at_column(self, line: usize) -> Self {
53        Self {
54            column_number: line,
55            ..self
56        }
57    }
58    /// Whether to wait for the editor to close before returning.
59    #[must_use]
60    pub fn wait_for_editor(self, value: bool) -> Self {
61        Self {
62            wait: value,
63            ..self
64        }
65    }
66    /// Add additional environment variables to look for the editor in. These variables
67    /// will have higher priority than `VISUAL` and `EDITOR`.
68    #[must_use]
69    pub fn with_env_vars(self, env_vars: &[&str]) -> Self {
70        let mut custom_env_vars = self.custom_env_vars;
71        custom_env_vars.extend(env_vars.iter().map(|&s| s.to_string()));
72        Self {
73            custom_env_vars,
74            ..self
75        }
76    }
77    /// Open the default editor and returns what was written in it.
78    ///
79    /// # Errors
80    /// If the editor call fails, or if the temporary file cannot be read or cleaned up,
81    /// or if the editor call fails.
82    pub fn open_editor(&self) -> Result<String, OpenEditorError> {
83        self.edit_string("")
84    }
85    /// Open the default editor and allows editing of a mutable string.
86    ///
87    /// # Errors
88    ///
89    /// If the editor call fails, or if the temporary file cannot be read or cleaned up,
90    /// or if the editor call fails.
91    pub fn edit_string_mut(&self, string: &mut String) -> Result<(), OpenEditorError> {
92        *string = self.edit_string(string)?;
93        Ok(())
94    }
95
96    /// Open the default editor and allows editing of a string which is then returned.
97    ///
98    /// # Errors
99    /// If the editor call fails, or if the temporary file cannot be read or cleaned up,
100    /// or if the editor call fails.
101    pub fn edit_string(&self, string: &str) -> Result<String, OpenEditorError> {
102        let file_path = match &self.file_path {
103            Some(path) => path,
104            None => &{
105                let mut filename = temp_dir();
106                filename.push(String::from("open_editor_tmp_file"));
107                filename
108            },
109        };
110        // Write the initial content to the temporary file
111        std::fs::write(file_path, string).map_err(OpenEditorError::FileManipulationFail)?;
112        self.open_file(file_path)?;
113        let result =
114            std::fs::read_to_string(file_path).map_err(OpenEditorError::FileManipulationFail)?;
115
116        // Clean up the temporary file after reading
117        std::fs::remove_file(file_path).map_err(|_| {
118            OpenEditorError::TempFileCleanupFail(file_path.to_string_lossy().into_owned())
119        })?;
120
121        Ok(result)
122    }
123    /// Opens the specified file in the editor.
124    ///
125    /// # Errors
126    /// This function will return an error if the editor call fails or if the file cannot be read.
127    pub fn open_file(&self, file_path: &Path) -> Result<(), OpenEditorError> {
128        let editor = match &self.editor {
129            Some(editor) => editor,
130            None => &self.get_default_editor()?,
131        };
132
133        // Build the actual Editor Call
134        let editor_call = EditorCall {
135            editor: editor.clone(),
136            file_path: file_path.to_path_buf(),
137            wait: self.wait,
138            line_number: self.line_number,
139            column_number: self.column_number,
140        };
141        editor_call.call()
142    }
143    /// Gets the default editor from the environment variables `VISUAL` or `EDITOR`.
144    fn get_default_editor(&self) -> Result<Editor, OpenEditorError> {
145        self.custom_env_vars
146            .clone()
147            .into_iter()
148            .chain(ENV_VARS.iter().map(|&s| s.to_string()))
149            .filter_map(env::var_os)
150            .filter(|var| !var.is_empty())
151            .map(|v| {
152                let path = Editor::get_full_path(v.clone());
153                (v.into_string().ok(), path)
154            })
155            .filter_map(|(v, path)| v.map(|v| (v, path)))
156            .map(|(v, cmd)| (Editor::new(EditorKind::from(v), cmd)))
157            .next()
158            .ok_or(OpenEditorError::NoEditorFound)
159    }
160}
161/// Represents a call to an editor with specific options.
162struct EditorCall {
163    editor: Editor,
164    file_path: PathBuf,
165    wait: bool,
166    line_number: usize,
167    column_number: usize,
168}
169impl EditorCall {
170    /// Calls the editor with options from the [`EditorCallBuilder`].
171    /// # Errors
172    ///
173    /// This function will return an error if the commands fails to execute or if the editor returns a non-zero exit code.
174    pub fn call(&self) -> Result<(), OpenEditorError> {
175        self.editor.validate_executable()?; // Ensure the editor binary is valid
176        let command = Command::new(&self.editor.binary_path)
177            .args(self.editor.editor_type.get_editor_args(
178                &self.file_path,
179                self.wait,
180                self.line_number,
181                self.column_number,
182            ))
183            .stdin(Stdio::inherit())
184            .stdout(Stdio::inherit())
185            .stderr(Stdio::inherit())
186            .spawn();
187
188        if !self.wait {
189            return Ok(());
190        }
191
192        match command {
193            Ok(output) => {
194                let output = output
195                    .wait_with_output()
196                    .map_err(|e| OpenEditorError::CommandFail { error: e })?;
197                if output.status.success() {
198                    Ok(())
199                } else {
200                    let stderr = String::from_utf8_lossy(&output.stderr);
201                    Err(OpenEditorError::EditorCallError {
202                        exit_code: output.status.code(),
203                        stderr: stderr.to_string(),
204                    })
205                }
206            }
207            Err(e) => Err(OpenEditorError::CommandFail { error: e }),
208        }
209    }
210}